pyface-7.4.0/0000755000076500000240000000000014176460551013743 5ustar cwebsterstaff00000000000000pyface-7.4.0/PKG-INFO0000644000076500000240000000725414176460551015050 0ustar cwebsterstaff00000000000000Metadata-Version: 2.1 Name: pyface Version: 7.4.0 Summary: traits-capable windowing framework Home-page: http://docs.enthought.com/pyface Author: David C. Morrill, et al. Author-email: dmorrill@enthought.com Maintainer: ETS Developers Maintainer-email: enthought-dev@enthought.com License: BSD Download-URL: https://github.com/enthought/pyface Description: ========================================== Pyface: Traits-capable Windowing Framework ========================================== The Pyface project contains a toolkit-independent GUI abstraction layer, which is used to support the "visualization" features of the Traits package. Thus, you can write code in terms of the Traits API (views, items, editors, etc.), and let Pyface and your selected toolkit and back-end take care of the details of displaying them. The following GUI backends are supported: - PySide2 (stable) and PySide6 (experimental) - PyQt5 (stable) and PyQt6 (in development) - wxPython 4 (experimental) Installation ------------ GUI backends are marked as optional dependencies of Pyface. Some features or infrastructures may also require additional dependencies. To install with PySide2 dependencies:: $ pip install pyface[pyside2] To install with PySide6 dependencies (experimental):: $ pip install pyface[pyside6] To install with PyQt5 dependencies:: $ pip install pyface[pyqt5] To install with wxPython4 dependencies (experimental):: $ pip install pyface[wx] ``pillow`` is an optional dependency for the PILImage class:: $ pip install pyface[pillow] To install with additional test dependencies:: $ pip install pyface[test] Documentation ------------- * `Online Documentation `_. * `API Documentation `_. Prerequisites ------------- Pyface depends on: * `Traits `_ * a GUI toolkit as described above * Pygments for syntax highlighting in the Qt code editor widget. * some widgets may have additional optional dependencies such as NumPy or Pillow. Platform: Windows Platform: Linux Platform: Mac OS-X Platform: Unix Platform: Solaris Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Science/Research Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: MacOS Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: OS Independent Classifier: Operating System :: POSIX Classifier: Operating System :: Unix Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Topic :: Scientific/Engineering Classifier: Topic :: Software Development Classifier: Topic :: Software Development :: Libraries Requires-Python: >=3.6 Description-Content-Type: text/x-rst Provides-Extra: wx Provides-Extra: pyqt Provides-Extra: pyqt5 Provides-Extra: pyqt6 Provides-Extra: pyside2 Provides-Extra: pyside6 Provides-Extra: pillow Provides-Extra: test pyface-7.4.0/pyface/0000755000076500000240000000000014176460550015211 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/tree/0000755000076500000240000000000014176460550016150 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/tree/node_monitor.py0000644000076500000240000000752014176222673021224 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 monitor for appearance and structural changes to a node. """ import logging from traits.api import Any, Event, HasTraits from .node_event import NodeEvent # Create a logger for this module. logger = logging.getLogger(__name__) class NodeMonitor(HasTraits): """ A monitor for appearance and structural changes to a node. """ # 'NodeMonitor' interface ---------------------------------------------# # The node that we are monitoring. node = Any() # Events ---- # Fired when child nodes in the node that we are monitoring have changed in # some way that affects their appearance but NOT their structure. nodes_changed = Event(NodeEvent) # Fired when child nodes have been inserted into the node that we are # monitoring. nodes_inserted = Event(NodeEvent) # Fired when child nodes have been removed from the node that we are # monitoring. nodes_removed = Event(NodeEvent) # Fired when child nodes have been replaced in the node that we are # monitoring. nodes_replaced = Event(NodeEvent) # Fired when the structure of the node that we are monitoring has changed # DRASTICALLY (i.e., we do not have enough information to make individual # changes/inserts/removals). structure_changed = Event(NodeEvent) # ------------------------------------------------------------------------ # 'NodeMonitor' interface. # ------------------------------------------------------------------------ # public methods ------------------------------------------------------- def start(self): """ Start listening to changes to the node. """ if self.node.obj is not None: self._setup_trait_change_handlers(self.node.obj) def stop(self): """ Stop listening to changes to the node. """ if self.node.obj is not None: self._setup_trait_change_handlers(self.node.obj, remove=True) def fire_nodes_changed(self, children=[]): """ Fires the nodes changed event. """ self.nodes_changed = NodeEvent(node=self.node, children=children) def fire_nodes_inserted(self, children, index=-1): """ Fires the nodes inserted event. If the index is -1 it means the nodes were appended. fixme: The tree and model should probably have an 'appended' event. """ self.nodes_inserted = NodeEvent( node=self.node, children=children, index=index ) def fire_nodes_removed(self, children): """ Fires the nodes removed event. """ self.nodes_removed = NodeEvent(node=self.node, children=children) def fire_nodes_replaced(self, old_children, new_children): """ Fires the nodes replaced event. """ self.nodes_replaced = NodeEvent( node=self.node, old_children=old_children, children=new_children ) def fire_structure_changed(self): """ Fires the structure changed event. """ self.structure_changed = NodeEvent(node=self.node) return # protected methods ---------------------------------------------------- def _setup_trait_change_handlers(self, obj, remove=False): """ Add or remove trait change handlers to/from a node. """ logger.debug( "%s trait listeners on (%s) in NodeMonitor (%s)", (remove and "Removing" or "Adding"), obj, self, ) pass # derived classes should do something here! return pyface-7.4.0/pyface/tree/tree.py0000644000076500000240000000113714176222673017465 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 implementation of a tree control with a model/ui architecture. """ # Import the toolkit specific version. from pyface.toolkit import toolkit_object Tree = toolkit_object("tree.tree:Tree") pyface-7.4.0/pyface/tree/images/0000755000076500000240000000000014176460550017415 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/tree/images/document.png0000644000076500000240000000051514176222673021744 0ustar cwebsterstaff00000000000000PNG  IHDRabKGD pHYs  tIME8`&BIDAT8˭=n0H[!!l"A!QsL$^Xj,{|3m1 Z;騬8xt/5ǒ4MW(w=ιURW/P&AH৞"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`pyface-7.4.0/pyface/tree/images/closed_folder.png0000644000076500000240000000102514176222673022727 0ustar cwebsterstaff00000000000000PNG  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`pyface-7.4.0/pyface/tree/node_tree_model.py0000644000076500000240000002323114176222673021651 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 model for a tree control with extensible node types. """ from traits.api import Dict, Instance from .node_manager import NodeManager from .tree_model import TreeModel class NodeTreeModel(TreeModel): """ The model for a tree control with extensible node types. """ # 'NodeTreeModel' interface -------------------------------------------- # The node manager looks after all node types. node_manager = Instance(NodeManager, ()) # Private interface ---------------------------------------------------- # Node monitors. _monitors = Dict() # ------------------------------------------------------------------------ # 'TreeModel' interface. # ------------------------------------------------------------------------ def has_children(self, node): """ Returns True if a node has children, otherwise False. This method is provided in case the model has an efficient way to determine whether or not a node has any children without having to actually get the children themselves. """ # Determine the node type for this node. node_type = self.node_manager.get_node_type(node) if node_type.allows_children(node): has_children = node_type.has_children(node) else: has_children = False return has_children def get_children(self, node): """ Returns the children of a node. """ # Determine the node type for this node. node_type = self.node_manager.get_node_type(node) # Does the node allow children (ie. a folder allows children, a file # does not)? if node_type.allows_children(node): # Get the node's children. children = node_type.get_children(node) else: children = [] return children def get_default_action(self, node): """ Returns the default action for a node. """ # Determine the node type for this node. node_type = self.node_manager.get_node_type(node) return node_type.get_default_action(node) def get_drag_value(self, node): """ Get the value that is dragged for a node. By default the drag value is the node itself. """ # Determine the node type for this node. node_type = self.node_manager.get_node_type(node) return node_type.get_drag_value(node) def can_drop(self, node, data): """ Returns True if a node allows an object to be dropped onto it. """ # Determine the node type for this node. node_type = self.node_manager.get_node_type(node) return node_type.can_drop(node, data) def drop(self, node, data): """ Drops an object onto a node. """ # Determine the node type for this node. node_type = self.node_manager.get_node_type(node) node_type.drop(node, data) def get_image(self, node, selected, expanded): """ Returns the label image for a node. Return None (the default) if no image is required. """ # Determine the node type for this node. node_type = self.node_manager.get_node_type(node) return node_type.get_image(node, selected, expanded) def get_key(self, node): """ Generate a unique key for a node. """ return self.node_manager.get_key(node) def get_selection_value(self, node): """ Get the value that is used when a node is selected. By default the selection value is the node itself. """ # Determine the node type for this node. node_type = self.node_manager.get_node_type(node) return node_type.get_selection_value(node) def get_text(self, node): """ Returns the label text for a node. Return None if no text is required. By default we return 'str(node)'. """ # Determine the node type for this node. node_type = self.node_manager.get_node_type(node) return node_type.get_text(node) def can_set_text(self, node, text): """ Returns True if the node's label can be set. """ # Determine the node type for this node. node_type = self.node_manager.get_node_type(node) return node_type.can_set_text(node, text) def set_text(self, node, text): """ Sets the label text for a node. """ # Determine the node type for this node. node_type = self.node_manager.get_node_type(node) return node_type.set_text(node, text) def is_collapsible(self, node): """ Returns True if the node is collapsible, otherwise False. """ # Determine the node type for this node. node_type = self.node_manager.get_node_type(node) return node_type.is_collapsible(node) def is_draggable(self, node): """ Returns True if the node is draggable, otherwise False. """ # Determine the node type for this node. node_type = self.node_manager.get_node_type(node) return node_type.is_draggable(node) def is_editable(self, node): """ Returns True if the node is editable, otherwise False. If the node is editable, its text can be set via the UI. """ # Determine the node type for this node. node_type = self.node_manager.get_node_type(node) return node_type.is_editable(node) def is_expandable(self, node): """ Returns True if the node is expandanble, otherwise False. """ # Determine the node type for this node. node_type = self.node_manager.get_node_type(node) return node_type.is_expandable(node) def add_listener(self, node): """ Adds a listener for changes to a node. """ # Determine the node type for this node. node_type = self.node_manager.get_node_type(node) # Create a monitor to listen for changes to the node. monitor = node_type.get_monitor(node) if monitor is not None: self._start_monitor(monitor) self._monitors[self.node_manager.get_key(node)] = monitor def remove_listener(self, node): """ Removes a listener for changes to a node. """ key = self.node_manager.get_key(node) monitor = self._monitors.get(key) if monitor is not None: self._stop_monitor(monitor) del self._monitors[key] return # ------------------------------------------------------------------------ # 'NodeTreeModel' interface. # ------------------------------------------------------------------------ def get_context_menu(self, node): """ Returns the context menu for a node. """ # Determine the node type for this node. node_type = self.node_manager.get_node_type(node) return node_type.get_context_menu(node) # ------------------------------------------------------------------------ # 'Private' interface # ------------------------------------------------------------------------ def _start_monitor(self, monitor): """ Starts a monitor. """ monitor.observe(self._on_nodes_changed, "nodes_changed") monitor.observe(self._on_nodes_inserted, "nodes_inserted") monitor.observe(self._on_nodes_removed, "nodes_removed") monitor.observe(self._on_nodes_replaced, "nodes_replaced") monitor.observe(self._on_structure_changed, "structure_changed") monitor.start() def _stop_monitor(self, monitor): """ Stops a monitor. """ monitor.observe(self._on_nodes_changed, "nodes_changed", remove=True) monitor.observe(self._on_nodes_inserted, "nodes_inserted", remove=True) monitor.observe(self._on_nodes_removed, "nodes_removed", remove=True) monitor.observe(self._on_nodes_replaced, "nodes_replaced", remove=True) monitor.observe( self._on_structure_changed, "structure_changed", remove=True ) monitor.stop() return # Trait event handlers ------------------------------------------------- # Static ---- # fixme: Commented this out as listeners are added and removed by the tree. # This caused duplicate monitors to be created for the root node. ## def _root_changed(self, old, new): ## """ Called when the root of the model has been changed. """ ## if old is not None: ## # Remove a listener for structure/appearance changes ## self.remove_listener(old) ## if new is not None: ## # Wire up a listener for structure/appearance changes ## self.add_listener(new) ## return # Dynamic ---- def _on_nodes_changed(self, event): """ Called when nodes have changed. """ self.nodes_changed = event.new def _on_nodes_inserted(self, event): """ Called when nodes have been inserted. """ self.nodes_inserted = event.new def _on_nodes_removed(self, event): """ Called when nodes have been removed. """ self.nodes_removed = event.new def _on_nodes_replaced(self, event): """ Called when nodes have been replaced. """ self.nodes_replaced = event.new def _on_structure_changed(self, event): """ Called when the structure of a node has changed drastically. """ self.structure_changed = event.new return pyface-7.4.0/pyface/tree/trait_list_node_type.py0000644000076500000240000000346014176222673022753 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 node type for a trait list. """ from traits.api import Any, Str from .node_type import NodeType class TraitListNodeType(NodeType): """ The node type for a trait list. """ # 'TraitListNodeType' interface ---------------------------------------- # The type of object that provides the trait list. klass = Any() # The label text. text = Str() # The name of the trait. trait_name = Str() # ------------------------------------------------------------------------ # 'NodeType' interface. # ------------------------------------------------------------------------ def is_type_for(self, node): """ Returns True if this node type recognizes a node. """ is_type_for = ( isinstance(node, list) and hasattr(node, "object") and isinstance(node.object, self.klass) and node.name == self.trait_name ) return is_type_for def allows_children(self, node): """ Does the node allow children (ie. a folder vs a file). """ return True def has_children(self, node): """ Returns True if a node has children, otherwise False. """ return len(node) > 0 def get_children(self, node): """ Returns the children of a node. """ return node def get_text(self, node): """ Returns the label text for a node. """ return self.text pyface-7.4.0/pyface/tree/__init__.py0000644000076500000240000000000014176222673020251 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/tree/trait_dict_node_type.py0000644000076500000240000000351214176222673022721 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 node type for a trait dictionary. """ from traits.api import Any, Str from .node_type import NodeType class TraitDictNodeType(NodeType): """ The node type for a trait dictionary. """ # 'TraitDictNodeType' interface ---------------------------------------- # The type of object that provides the trait dictionary. klass = Any() # The label text. text = Str() # The trait name. trait_name = Str() # ------------------------------------------------------------------------ # 'NodeType' interface. # ------------------------------------------------------------------------ def is_type_for(self, node): """ Returns True if this node type recognizes a node. """ is_type_for = ( isinstance(node, dict) and hasattr(node, "object") and isinstance(node.object, self.klass) and node.name == self.trait_name ) return is_type_for def allows_children(self, node): """ Does the node allow children (ie. a folder vs a file). """ return True def has_children(self, node): """ Returns True if a node has children, otherwise False. """ return len(node) > 0 def get_children(self, node): """ Returns the children of a node. """ return list(node.values()) def get_text(self, node): """ Returns the label text for a node. """ return self.text pyface-7.4.0/pyface/tree/api.py0000644000076500000240000000227314176222673017301 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 ``pyface.tree`` subpackage. - :class:`~.NodeEvent` - :class:`~.NodeMonitor` - :class:`~.NodeManager` - :class:`~.NodeTree` - :class:`~.NodeTreeModel` - :class:`~.NodeType` - :class:`~.TraitDictNodeType` - :class:`~.TraitListNodeType` - :class:`~.TreeModel` Note that the following classes are only available in the Wx toolkit at the moment. - :class:`~.Tree`. """ from .node_event import NodeEvent from .node_monitor import NodeMonitor from .node_manager import NodeManager from .node_tree import NodeTree from .node_tree_model import NodeTreeModel from .node_type import NodeType from .trait_dict_node_type import TraitDictNodeType from .trait_list_node_type import TraitListNodeType from .tree_model import TreeModel # Tree has not yet been ported to qt from .tree import Tree pyface-7.4.0/pyface/tree/node_tree.py0000644000076500000240000001304614176222673020474 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 tree control with extensible node types. """ # avoid deprecation warning from inspect import getfullargspec from pyface.action.api import ActionEvent from traits.api import Instance, List, observe, Property from .node_manager import NodeManager from .node_type import NodeType from .node_tree_model import NodeTreeModel from .tree import Tree class NodeTree(Tree): """ A tree control with extensible node types. """ # 'Tree' interface ----------------------------------------------------- # The model that provides the data for the tree. model = Instance(NodeTreeModel, ()) # 'NodeTree' interface ------------------------------------------------- # The node manager looks after all node types. node_manager = Property(Instance(NodeManager)) # The node types in the tree. node_types = Property(List(NodeType)) # ------------------------------------------------------------------------ # 'NodeTree' interface. # ------------------------------------------------------------------------ # Properties ----------------------------------------------------------- # node_manager def _get_node_manager(self): """ Returns the root node of the tree. """ return self.model.node_manager def _set_node_manager(self, node_manager): """ Sets the root node of the tree. """ self.model.node_manager = node_manager return # node_types def _get_node_types(self): """ Returns the node types in the tree. """ return self.model.node_manager.node_types def _set_node_types(self, node_types): """ Sets the node types in the tree. """ self.model.node_manager.node_types = node_types return # ------------------------------------------------------------------------ # 'Tree' interface. # ------------------------------------------------------------------------ # Trait event handlers ------------------------------------------------- @observe("node_activated") def _perform_default_action_on_activated_node(self, event): """ Called when a node has been activated (i.e., double-clicked). """ obj = event.new default_action = self.model.get_default_action(obj) if default_action is not None: self._perform_default_action(default_action, obj) @observe("node_right_clicked") def _show_menu_on_right_clicked_object(self, event): """ Called when the right mouse button is clicked on the tree. """ obj_point = event.new obj, point = obj_point # Add the node that the right-click occurred on to the selection. self.select(obj) # fixme: This is a hack to allow us to attach the node that the # right-clicked occurred on to the action event. self._context = obj # Ask the model for the node's context menu. menu_manager = self.model.get_context_menu(obj) if menu_manager is not None: self._popup_menu(menu_manager, obj, point) return # ------------------------------------------------------------------------ # 'ActionController' interface. # ------------------------------------------------------------------------ def add_to_menu(self, menu_item): """ Adds a menu item to a menu bar. """ pass def add_to_toolbar(self, toolvar_item): """ Adds a tool bar item to a tool bar. """ pass def can_add_to_menu(self, action): """ Returns True iff an action can be added to the menu. """ return True def perform(self, action, event): """ Perform an action. """ # fixme: We need a more formal event structure! event.widget = self event.context = self._context # fixme: the 'perform' method without taking an event is deprecated! args, varargs, varkw, defaults = getfullargspec(action.perform) # If the only argument is 'self' then this is the DEPRECATED # interface. if len(args) == 1: action.perform() else: action.perform(event) return # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _create_action_event(self, obj): """ Return a new action event for the specified object. """ return ActionEvent(widget=self, context=obj) def _perform_default_action(self, action, obj): """ Perform the default action on the specified object. """ action.perform(self._create_action_event(obj)) def _popup_menu(self, menu_manager, obj, point): """ Popup the menu described by the menu manager. """ # Create the actual menu control. menu = menu_manager.create_menu(self.control, self) if not menu.is_empty(): # Show the menu. If an action is selected it will be performed # *before* this call returns. menu.show(*point) # This gives the actions in the menu manager a chance to cleanup # any event listeners etc. menu_manager.destroy() return pyface-7.4.0/pyface/tree/node_event.py0000644000076500000240000000165514176222673020661 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 event fired by the tree models/node monitors etc. """ from traits.api import Any, HasTraits, Int, List # Classes for event traits. class NodeEvent(HasTraits): """ The event fired by the tree models/node monitors etc. """ # The node that has changed. node = Any() # The nodes (if any) that have been inserted/removed/changed. children = List() # The nodes (if any) that have been replaced. old_children = List() # The starting index for nodes that have been inserted. index = Int() pyface-7.4.0/pyface/tree/tree_model.py0000644000076500000240000001202414176222673020642 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Model for tree views. """ from traits.api import Any, HasTraits, Event from .node_event import NodeEvent class TreeModel(HasTraits): """ Model for tree views. """ # 'TreeModel' interface ------------------------------------------------ # The root of the model. root = Any() # Fired when nodes in the tree have changed in some way that affects their # appearance but NOT their structure or position in the tree. nodes_changed = Event(NodeEvent) # Fired when nodes have been inserted into the tree. nodes_inserted = Event(NodeEvent) # Fired when nodes have been removed from the tree. nodes_removed = Event(NodeEvent) # Fired when nodes have been replaced in the tree. nodes_replaced = Event(NodeEvent) # Fire when the structure of the tree has changed DRASTICALLY from a given # node down. structure_changed = Event(NodeEvent) # ------------------------------------------------------------------------ # 'TreeModel' interface. # ------------------------------------------------------------------------ def has_children(self, node): """ Returns True if a node has children, otherwise False. This method is provided in case the model has an efficient way to determine whether or not a node has any children without having to actually get the children themselves. """ raise NotImplementedError() def get_children(self, node): """ Returns the children of a node. """ raise NotImplementedError() def get_drag_value(self, node): """ Get the value that is dragged for a node. By default the drag value is the node itself. """ return node def can_drop(self, node, obj): """ Returns True if a node allows an object to be dropped onto it. """ return False def drop(self, node, obj): """ Drops an object onto a node. """ raise NotImplementedError() def get_image(self, node, selected, expanded): """ Returns the label image for a node. Return None (the default) if no image is required. """ return None def get_key(self, node): """ Generate a unique key for a node. """ try: key = hash(node) except: key = id(node) return key def get_selection_value(self, node): """ Get the value that is used when a node is selected. By default the selection value is the node itself. """ return node def get_text(self, node): """ Returns the label text for a node. Return None if no text is required. By default we return 'str(node)'. """ return str(node) def can_set_text(self, node, text): """ Returns True if the node's label can be set. """ return len(text.strip()) > 0 def set_text(self, node, text): """ Sets the label text for a node. """ pass def is_collapsible(self, node): """ Returns True if the node is collapsible, otherwise False. """ return True def is_draggable(self, node): """ Returns True if the node is draggable, otherwise False. """ return True def is_editable(self, node): """ Returns True if the node is editable, otherwise False. If the node is editable, its text can be set via the UI. """ return False def is_expandable(self, node): """ Returns True if the node is expandanble, otherwise False. """ return True def add_listener(self, node): """ Adds a listener for changes to a node. """ pass def remove_listener(self, node): """ Removes a listener for changes to a node. """ pass def fire_nodes_changed(self, node, children): """ Fires the nodes changed event. """ self.nodes_changed = NodeEvent(node=node, children=children) def fire_nodes_inserted(self, node, children): """ Fires the nodes inserted event. """ self.nodes_inserted = NodeEvent(node=node, children=children) def fire_nodes_removed(self, node, children): """ Fires the nodes removed event. """ self.nodes_removed = NodeEvent(node=node, children=children) def fire_nodes_replaced(self, node, old_children, new_children): """ Fires the nodes removed event. """ self.nodes_replaced = NodeEvent( node=node, old_children=old_children, children=new_children ) def fire_structure_changed(self, node): """ Fires the structure changed event. """ self.structure_changed = NodeEvent(node=node) return pyface-7.4.0/pyface/tree/node_manager.py0000644000076500000240000001037014176222673021144 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 node manager looks after a collection of node types. """ import logging from traits.api import HasPrivateTraits, List, observe from .node_type import NodeType # Create a logger for this module. logger = logging.getLogger(__name__) class NodeManager(HasPrivateTraits): """ The node manager looks after a collection of node types. """ # 'NodeManager' interface -----------------------------------------# # All registered node types. node_types = List(NodeType) # fixme: Where should the system actions go? The node tree, the node # tree model, here?!? system_actions = List() # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, **traits): """ Creates a new tree model. """ # Base class constructor. super().__init__(**traits) # This saves looking up a node's type every time. If we ever have # nodes that change type dynamically then we will obviously have to # re-think this (although we should probably re-think dynamic type # changes first ;^). self._node_to_type_map = {} # { Any node : NodeType node_type } return # ------------------------------------------------------------------------ # 'NodeManager' interface. # ------------------------------------------------------------------------ # fixme: This is the only API call that we currently have that manipulates # the manager's node types. Should we make the 'node_types' list # available via the public API? def add_node_type(self, node_type): """ Adds a new node type to the manager. """ node_type.node_manager = self self.node_types.append(node_type) def get_node_type(self, node): """ Returns the node's type. Returns None if none of the manager's node types recognize the node. """ # Generate the key for the node to type map. key = self.get_key(node) # Check the cache first. node_type = self._node_to_type_map.get(key, None) if node_type is None: # If we haven't seen this node before then attempt to find a node # type that 'recognizes' it. # # fixme: We currently take the first node type that recognizes the # node. This obviously means that ordering of node types is # important, but we don't have an interface for controlling the # order. Maybe sort on some 'precedence' trait on the node type? for node_type in self.node_types: if node_type.is_type_for(node): self._node_to_type_map[key] = node_type break else: node_type = None if node_type is None: logger.warning("no node type for %s" % str(node)) return node_type def get_key(self, node): """ Generates a unique key for a node. In this case, 'unique' means unqiue within the node manager. """ # We do it like this 'cos, for example, using id() on a string doesn't # give us what we want, but things like lists aren't hashable, so we # can't always use hash()). try: key = hash(node) except: key = id(node) return key # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ @observe("node_types") def _update_node_manager_on_new_node_types(self, event): """ Called when the entire list of node types has been changed. """ new = event.new for node_type in new: node_type.node_manager = self return pyface-7.4.0/pyface/tree/node_type.py0000644000076500000240000001707014176222673020517 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 base class for all node types. """ from traits.api import Any, HasPrivateTraits, Instance from pyface.api import Image, ImageResource from pyface.action.api import Action, Group from pyface.action.api import MenuManager class NodeType(HasPrivateTraits): """ The base class for all node types. """ # The default image used to represent nodes that DO NOT allow children. DOCUMENT = ImageResource("document") # The default image used to represent nodes that allow children and are NOT # expanded. CLOSED_FOLDER = ImageResource("closed_folder") # The default image used to represent nodes that allow children and ARE # expanded. OPEN_FOLDER = ImageResource("open_folder") # 'NodeType' interface ------------------------------------------------- # The node manager that the type belongs to. node_manager = Instance("pyface.tree.node_manager.NodeManager") # The image used to represent nodes that DO NOT allow children. image = Image(DOCUMENT) # The image used to represent nodes that allow children and are NOT # expanded. closed_image = Image(CLOSED_FOLDER) # The image used to represent nodes that allow children and ARE expanded. open_image = Image(OPEN_FOLDER) # The default actions/groups/menus available on nodes of this type (shown # on the context menu). actions = Any # List # The default action for nodes of this type. The default action is # performed when a node is activated (i.e., double-clicked). default_action = Instance(Action) # The default actions/groups/menus for creating new children within nodes # of this type (shown in the 'New' menu of the context menu). new_actions = Any # List # ------------------------------------------------------------------------ # 'NodeType' interface. # ------------------------------------------------------------------------ # These methods are specific to the 'NodeType' interface --------------- def is_type_for(self, node): """ Returns True if a node is deemed to be of this type. """ raise NotImplementedError() def allows_children(self, node): """ Does the node allow children (ie. a folder vs a file). """ return False def get_actions(self, node): """ Returns the node-specific actions for a node. """ return self.actions def get_context_menu(self, node): """ Returns the context menu for a node. """ sat = Group(id="SystemActionsTop") nsa = Group(id="NodeSpecificActions") sab = Group(id="SystemActionsBottom") # The 'New' menu. new_actions = self.get_new_actions(node) if new_actions is not None and len(new_actions) > 0: sat.append(MenuManager(name="New", *new_actions)) # Node-specific actions. actions = self.get_actions(node) if actions is not None and len(actions) > 0: for item in actions: nsa.append(item) # System actions (actions available on ALL nodes). system_actions = self.node_manager.system_actions if len(system_actions) > 0: for item in system_actions: sab.append(item) context_menu = MenuManager(sat, nsa, sab) context_menu.dump() return context_menu def get_copy_value(self, node): """ Get the value that is copied for a node. By default, returns the node itself. """ return node def get_default_action(self, node): """ Returns the default action for a node. """ return self.default_action def get_new_actions(self, node): """ Returns the new actions for a node. """ return self.new_actions def get_paste_value(self, node): """ Get the value that is pasted for a node. By default, returns the node itself. """ return node def get_monitor(self, node): """ Returns a monitor that detects changes to a node. Returns None by default, which indicates that the node is not monitored. """ return None # These methods are exactly the same as the 'TreeModel' interface -----# def has_children(self, node): """ Returns True if a node has children, otherwise False. You only need to implement this method if children are allowed for the node (ie. 'allows_children' returns True). """ return False def get_children(self, node): """ Returns the children of a node. You only need to implement this method if children are allowed for the node. """ raise NotImplementedError() def get_drag_value(self, node): """ Get the value that is dragged for a node. By default, returns the node itself. """ return node def can_drop(self, node, data): """ Returns True if a node allows an object to be dropped onto it. """ return False def drop(self, obj, data): """ Drops an object onto a node. """ raise NotImplementedError() def get_image(self, node, selected, expanded): """ Returns the label image for a node. """ if self.allows_children(node): if expanded: order = ["open_image", "closed_image", "image"] default = self.OPEN_FOLDER else: order = ["closed_image", "open_image", "image"] default = self.CLOSED_FOLDER else: order = ["image", "open_image", "closed_image"] default = self.DOCUMENT # Use the search order to look for a trait that is NOT None. for name in order: image = getattr(self, name) if image is not None: break # If no such trait is found then use the default image. else: image = default return image def get_selection_value(self, node): """ Get the value that is used when a node is selected. By default the selection value is the node itself. """ return node def get_text(self, node): """ Returns the label text for a node. """ return str(node) def can_set_text(self, node, text): """ Returns True if the node's label can be set. """ return len(text.strip()) > 0 def set_text(self, node, text): """ Sets the label text for a node. """ pass def is_collapsible(self, node): """ Returns True if the node is collapsible, otherwise False. """ return True def is_draggable(self, node): """ Returns True if the node is draggablee, otherwise False. """ return True def can_rename(self, node): """ Returns True if the node can be renamed, otherwise False. """ return False def is_editable(self, node): """ Returns True if the node is editable, otherwise False. If the node is editable, its text can be set via the UI. DEPRECATED: Use 'can_rename'. """ return self.can_rename(node) def is_expandable(self, node): """ Returns True if the node is expandanble, otherwise False. """ return True pyface-7.4.0/pyface/python_shell.py0000644000076500000240000000113414176222673020274 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 implementation of an interactive Python shell. """ # Import the toolkit specific version. from .toolkit import toolkit_object PythonShell = toolkit_object("python_shell:PythonShell") pyface-7.4.0/pyface/ipython_widget.py0000644000076500000240000000161014176222673020620 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 implementation of an IPython shell. """ # Import the toolkit specific version. try: import IPython.frontend # noqa: F401 except ImportError: raise ImportError( """ ________________________________________________________________________________ Could not load the Wx frontend for ipython. You need to have ipython >= 0.9 installed to use the ipython widget.""" ) from .toolkit import toolkit_object IPythonWidget = toolkit_object("ipython_widget:IPythonWidget") pyface-7.4.0/pyface/ui/0000755000076500000240000000000014176460550015626 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/__init__.py0000644000076500000240000000000014176222673017727 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/wx/0000755000076500000240000000000014176460551016265 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/wx/tree/0000755000076500000240000000000014176460551017224 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/wx/tree/tree.py0000644000076500000240000012651614176222673020551 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 tree control with a model/ui architecture. """ import logging import os import warnings import wx from traits.api import ( Any, Bool, Callable, Enum, Event, Instance, Int, List, Property, Str, Tuple, ) from pyface.filter import Filter from pyface.key_pressed_event import KeyPressedEvent from pyface.sorter import Sorter from pyface.tree.tree_model import TreeModel from pyface.ui.wx.gui import GUI from pyface.ui.wx.image_list import ImageList from pyface.ui.wx.layout_widget import LayoutWidget from pyface.wx.drag_and_drop import PythonDropSource, PythonDropTarget # Create a logger for this module. logger = logging.getLogger(__name__) class _Tree(wx.TreeCtrl): """ The wx tree control that we delegate to. We use this derived class so that we can detect the destruction of the tree and remove model listeners etc. """ # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, tree, parent, wxid, style): """ Creates a new tree. """ # Base class constructor. super().__init__(parent, wxid, style=style) # The tree that we are the toolkit-specific delegate for. self._tree = tree def Destroy(self): """ Destructor. """ # Stop listenting to the model! self._tree._remove_model_listeners(self._tree.model) super().Destroy() class Tree(LayoutWidget): """ A tree control with a model/ui architecture. """ # The default tree style. STYLE = wx.TR_EDIT_LABELS | wx.TR_HAS_BUTTONS | wx.CLIP_CHILDREN # 'Tree' interface ----------------------------------------------------- # The tree's filters (empty if no filtering is required). filters = List(Instance(Filter)) # Mode for lines connecting tree nodes which emphasize hierarchy: # 'appearance' - only on when lines look good, # 'on' - always on, 'off' - always off # NOTE: on and off are ignored in favor of show_lines for now lines_mode = Enum("appearance", "on", "off") # The model that provides the data for the tree. model = Instance(TreeModel, ()) # The root of the tree (this is for convenience, it just delegates to # the tree's model). root = Property(Any) # The objects currently selected in the tree. selection = List(Any) # Selection mode. selection_mode = Enum("single", "extended") # Should an image be shown for each node? show_images = Bool(True) # Should lines be drawn between levels in the tree. show_lines = Bool(True) # Should the root of the tree be shown? show_root = Bool(True) # The tree's sorter (None if no sorting is required). sorter = Instance(Sorter) # Events ---- # A right-click occurred on the control (not a node!). control_right_clicked = Event # (Point) # A key was pressed while the tree has focus. key_pressed = Event(Instance(KeyPressedEvent)) # A node has been activated (ie. double-clicked). node_activated = Event # (Any) # A drag operation was started on a node. node_begin_drag = Event # (Any) # A (non-leaf) node has been collapsed. node_collapsed = Event # (Any) # A (non-leaf) node has been expanded. node_expanded = Event # (Any) # A left-click occurred on a node. # # Tuple(node, point). node_left_clicked = Event # (Tuple) # A right-click occurred on a node. # # Tuple(node, point) node_right_clicked = Event # (Tuple) # Private interface ---------------------------------------------------- # A name to distinguish the tree for debugging! # # fixme: This turns out to be kinda useful... Should 'Widget' have a name # trait? _name = Str("Anonymous tree") # An optional callback to detect the end of a label edit. This is # useful because the callback will be invoked even if the node label was # not actually changed. _label_edit_callback = Callable # Flag for allowing selection events to be ignored _ignore_selection_events = Bool(False) # The size of the icons in the tree. _image_size = Tuple(Int, Int) # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, parent, image_size=(16, 16), **traits): """ Creates a new tree. 'parent' is the toolkit-specific control that is the tree's parent. 'image_size' is a tuple in the form (int width, int height) that specifies the size of the images (if required) displayed in the tree. """ create = traits.pop('create', True) # Base class constructors. super().__init__(parent=parent, _image_size=image_size, **traits) if create: self.create() warnings.warn( "automatic widget creation is deprecated and will be removed " "in a future Pyface version, use create=False and explicitly " "call create() for future behaviour", PendingDeprecationWarning, ) def _create_control(self, parent): # Create the toolkit-specific control. self.control = tree = _Tree( self, parent, wxid=wx.ID_ANY, style=self._get_style() ) # Wire up the wx tree events. tree.Bind(wx.EVT_CHAR, self._on_char) tree.Bind(wx.EVT_LEFT_DOWN, self._on_left_down) # fixme: This is not technically correct as context menus etc should # appear on a right up (or right click). Unfortunately, if we # change this to 'EVT_RIGHT_UP' wx does not fire the event unless the # right mouse button is double clicked 8^() Sad, but true! tree.Bind(wx.EVT_RIGHT_DOWN, self._on_right_down) # fixme: This is not technically correct as we would really like to use # 'EVT_TREE_ITEM_ACTIVATED'. Unfortunately, (in 2.6 at least), it # throws an exception when the 'Enter' key is pressed as the wx tree # item Id in the event seems to be invalid. It also seems to cause # any child frames that my be created in response to the event to # appear *behind* the parent window, which is, errrr, not great ;^) tree.Bind(wx.EVT_LEFT_DCLICK, self._on_tree_item_activated) tree.Bind(wx.EVT_TREE_ITEM_COLLAPSING, self._on_tree_item_collapsing) tree.Bind(wx.EVT_TREE_ITEM_COLLAPSED, self._on_tree_item_collapsed) 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_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_BEGIN_DRAG, self._on_tree_begin_drag) tree.Bind(wx.EVT_TREE_SEL_CHANGED, self._on_tree_sel_changed) tree.Bind(wx.EVT_TREE_DELETE_ITEM, self._on_tree_delete_item) # Enable the tree as a drag and drop target. tree.SetDropTarget(PythonDropTarget(self)) # The image list is a wxPython-ism that caches all images used in the # control. self._image_list = ImageList(*self._image_size) if self.show_images: tree.AssignImageList(self._image_list) # Mapping from node to wx tree item Ids. self._node_to_id_map = {} # Add the root node. if self.root is not None: self._add_root_node(self.root) # Listen for changes to the model. self._add_model_listeners(self.model) return tree # ------------------------------------------------------------------------ # 'Tree' interface. # ------------------------------------------------------------------------ # Properties ----------------------------------------------------------- def _get_root(self): """ Returns the root node of the tree. """ return self.model.root def _set_root(self, root): """ Sets the root node of the tree. """ self.model.root = root return # Methods -------------------------------------------------------------# def collapse(self, node): """ Collapses the specified node. """ wxid = self._get_wxid(node) if wxid is not None: self.control.Collapse(wxid) def edit_label(self, node, callback=None): """ Edits the label of the specified node. If a callback is specified it will be called when the label edit completes WHETHER OR NOT the label was actually changed. The callback must take exactly 3 arguments:- (tree, node, label) """ wxid = self._get_wxid(node) if wxid is not None: self._label_edit_callback = callback self.control.EditLabel(wxid) def expand(self, node): """ Expands the specified node. """ wxid = self._get_wxid(node) if wxid is not None: self.control.Expand(wxid) def expand_all(self): """ Expands every node in the tree. """ if self.show_root: self._expand_item(self._get_wxid(self.root)) else: for child in self._get_children(self.root): self._expand_item(self._get_wxid(child)) def get_parent(self, node): """ Returns the parent of a node. This will only work iff the node has been displayed in the tree. If it hasn't then None is returned. """ # Has the node actually appeared in the tree yet? wxid = self._get_wxid(node) if wxid is not None: pid = self.control.GetItemParent(wxid) # The item data is a tuple. The first element indicates whether or # not we have already populated the item with its children. The # second element is the actual item data. populated, parent = self.control.GetItemData(pid) else: parent = None return parent def is_expanded(self, node): """ Returns True if the node is expanded, otherwise False. """ wxid = self._get_wxid(node) if wxid is not None: # If the root node is hidden then it is always expanded! if node is self.root and not self.show_root: is_expanded = True else: is_expanded = self.control.IsExpanded(wxid) else: is_expanded = False return is_expanded def is_selected(self, node): """ Returns True if the node is selected, otherwise False. """ wxid = self._get_wxid(node) if wxid is not None: is_selected = self.control.IsSelected(wxid) else: is_selected = False return is_selected def refresh(self, node): """ Refresh the tree starting from the specified node. Call this when the structure of the content has changed DRAMATICALLY. """ # Has the node actually appeared in the tree yet? pid = self._get_wxid(node) if pid is not None: # Delete all of the node's children and re-add them. self.control.DeleteChildren(pid) self.control.SetItemData(pid, (False, node)) # Does the node have any children? has_children = self._has_children(node) self.control.SetItemHasChildren(pid, has_children) # fixme: At least on Windows, wx does not fire an expanding # event for a hidden root node, so we have to populate the node # manually. if node is self.root and not self.show_root: # Add the child nodes. for child in self._get_children(node): self._add_node(pid, child) else: # Expand it. if self.control.IsExpanded(pid): self.control.Collapse(pid) self.control.Expand(pid) def select(self, node): """ Selects the specified node. """ wxid = self._get_wxid(node) if wxid is not None: self.control.SelectItem(wxid) def set_selection(self, list): """ Selects the specified list of nodes. """ logger.debug("Setting selection to [%s] within Tree [%s]", list, self) # Update the control to reflect the target list by unselecting # everything and then selecting each item in the list. During this # process, we want to avoid changing our own selection. self._ignore_selection_events = True self.control.UnselectAll() for node in list: try: self.select(node) except: logger.exception("Unable to select node [%s]", node) self._ignore_selection_events = False # Update our selection to reflect the final selection state. self.selection = self._get_selection() # ------------------------------------------------------------------------ # 'PythonDropTarget' interface. # ------------------------------------------------------------------------ def on_drag_over(self, x, y, obj, default_drag_result): """ Called when a node is dragged over the tree. """ result = wx.DragNone # Find the node that we are dragging over... node = self._get_drag_drop_node(x, y) if node is not None: # Ask the model if the node allows the object to be dropped onto # it. if self.model.can_drop(node, obj): result = default_drag_result return result def on_drop(self, x, y, obj, default_drag_result): """ Called when a node is dropped on the tree. """ # Find the node that we are dragging over... node = self._get_drag_drop_node(x, y) if node is not None: self.model.drop(node, obj) return default_drag_result # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _get_wxid(self, node): """ Returns the wxid for the specified node. Returns None if the node has not yet appeared in the tree. """ # The model must generate a unique key for each node (unique within the # model). key = self.model.get_key(node) return self._node_to_id_map.get(key, None) def _set_wxid(self, node, wxid): """ Sets the wxid for the specified node. """ # The model must generate a unique key for each node (unique within the # model). key = self.model.get_key(node) self._node_to_id_map[key] = wxid def _remove_wxid(self, node): """ Removes the wxid for the specified node. """ # The model must generate a unique key for each node (unique within the # model). key = self.model.get_key(node) try: del self._node_to_id_map[key] except KeyError: # fixme: No, really, this is a serious one... How do we get in this # situation. It came up when using the canvas stuff... logger.warning("removing node: %s" % str(node)) def _get_style(self): """ Returns the wx style flags for creating the tree control. """ # Start with the default flags. style = self.STYLE # Turn lines off for appearance on *nix. # ...for now, show_lines determines if lines are on or off, but # eventually lines_mode may eliminate the need for show_lines if self.lines_mode == "appearance" and os.name == "posix": self.show_lines = False if not self.show_lines: style = style | wx.TR_NO_LINES if not self.show_root: # fixme: It looks a little weird, but it we don't have the # 'lines at root' style then wx won't draw the expand/collapse # image on non-leaf nodes at the root level 8^() style = style | wx.TR_HIDE_ROOT | wx.TR_LINES_AT_ROOT if self.selection_mode != "single": style = style | wx.TR_MULTIPLE | wx.TR_EXTENDED return style def _add_model_listeners(self, model): """ Adds listeners for model changes. """ # Listen for changes to the model. model.observe(self._on_root_changed, "root") model.observe(self._on_nodes_changed, "nodes_changed") model.observe(self._on_nodes_inserted, "nodes_inserted") model.observe(self._on_nodes_removed, "nodes_removed") model.observe(self._on_nodes_replaced, "nodes_replaced") model.observe(self._on_structure_changed, "structure_changed") def _remove_model_listeners(self, model): """ Removes listeners for model changes. """ # Unhook the model event listeners. model.observe(self._on_root_changed, "root", remove=True) model.observe(self._on_nodes_changed, "nodes_changed", remove=True) model.observe(self._on_nodes_inserted, "nodes_inserted", remove=True) model.observe(self._on_nodes_removed, "nodes_removed", remove=True) model.observe(self._on_nodes_replaced, "nodes_replaced", remove=True) model.observe( self._on_structure_changed, "structure_changed", remove=True ) # unwire the wx tree events. tree = self.control tree.Unbind(wx.EVT_CHAR) tree.Unbind(wx.EVT_LEFT_DOWN) # fixme: This is not technically correct as context menus etc should # appear on a right up (or right click). Unfortunately, if we # change this to 'EVT_RIGHT_UP' wx does not fire the event unless the # right mouse button is double clicked 8^() Sad, but true! tree.Unbind(wx.EVT_RIGHT_DOWN) # fixme: This is not technically correct as we would really like to use # 'EVT_TREE_ITEM_ACTIVATED'. Unfortunately, (in 2.6 at least), it # throws an exception when the 'Enter' key is pressed as the wx tree # item Id in the event seems to be invalid. It also seems to cause # any child frames that my be created in response to the event to # appear *behind* the parent window, which is, errrr, not great ;^) tree.Unbind(wx.EVT_LEFT_DCLICK) tree.Unbind(wx.EVT_TREE_ITEM_ACTIVATED) tree.Unbind(wx.EVT_TREE_ITEM_COLLAPSING) tree.Unbind(wx.EVT_TREE_ITEM_COLLAPSED) tree.Unbind(wx.EVT_TREE_ITEM_EXPANDING) tree.Unbind(wx.EVT_TREE_ITEM_EXPANDED) tree.Unbind(wx.EVT_TREE_BEGIN_LABEL_EDIT) tree.Unbind(wx.EVT_TREE_END_LABEL_EDIT) tree.Unbind(wx.EVT_TREE_BEGIN_DRAG) tree.Unbind(wx.EVT_TREE_SEL_CHANGED) tree.Unbind(wx.EVT_TREE_DELETE_ITEM) def _add_root_node(self, node): """ Adds the root node. """ # Get the tree item image index and the label text. image_index = self._get_image_index(node) text = self._get_text(node) # Add the node. wxid = self.control.AddRoot(text, image_index, image_index) # This gives the model a chance to wire up trait handlers etc. self.model.add_listener(node) # If the root node is hidden, get its children. if not self.show_root: # Add the child nodes. for child in self._get_children(node): self._add_node(wxid, child) # Does the node have any children? has_children = self._has_children(node) self.control.SetItemHasChildren(wxid, has_children) # The item data is a tuple. The first element indicates whether or not # we have already populated the item with its children. The second # element is the actual item data (which in our case is an arbitrary # Python object provided by the tree model). if self.show_root: self.control.SetItemData(wxid, (not self.show_root, node)) # Make sure that we can find the node's Id. self._set_wxid(node, wxid) # Automatically expand the root. if self.show_root: self.control.Expand(wxid) def _add_node(self, pid, node): """ Adds 'node' as a child of the node identified by 'pid'. If 'pid' is None then we are adding the root node. """ # Get the tree item image index and the label text. image_index = self._get_image_index(node) text = self._get_text(node) # Add the node. wxid = self.control.AppendItem(pid, text, image_index, image_index) # This gives the model a chance to wire up trait handlers etc. self.model.add_listener(node) # Does the node have any children? has_children = self._has_children(node) self.control.SetItemHasChildren(wxid, has_children) # The item data is a tuple. The first element indicates whether or not # we have already populated the item with its children. The second # element is the actual item data (which in our case is an arbitrary # Python object provided by the tree model). self.control.SetItemData(wxid, (False, node)) # Make sure that we can find the node's Id. self._set_wxid(node, wxid) def _insert_node(self, pid, node, index): """ Inserts 'node' as a child of the node identified by 'pid'. If 'pid' is None then we are adding the root node. """ # Get the tree item image index and the label text. image_index = self._get_image_index(node) text = self._get_text(node) # Add the node. wxid = self.control.Sizer.InsertBefore( pid, index, text, image_index, image_index ) # This gives the model a chance to wire up trait handlers etc. self.model.add_listener(node) # Does the node have any children? has_children = self._has_children(node) self.control.SetItemHasChildren(wxid, has_children) # The item data is a tuple. The first element indicates whether or not # we have already populated the item with its children. The second # element is the actual item data (which in our case is an arbitrary # Python object provided by the tree model). self.control.SetItemData(wxid, (False, node)) # Make sure that we can find the node's Id. self._set_wxid(node, wxid) def _remove_node(self, wxid, node): """ Removes a node from the tree. """ # This gives the model a chance to remove trait handlers etc. self.model.remove_listener(node) # Remove the reference to the item's data. self._remove_wxid(node) self.control.SetItemData(wxid, None) def _update_node(self, wxid, node): """ Updates the image and text of the specified node. """ # Get the tree item image index. image_index = self._get_image_index(node) self.control.SetItemImage(wxid, image_index, wx.TreeItemIcon_Normal) self.control.SetItemImage(wxid, image_index, wx.TreeItemIcon_Selected) # Get the tree item text. text = self._get_text(node) self.control.SetItemText(wxid, text) def _has_children(self, node): """ Returns True if a node has children. """ # fixme: To be correct we *should* apply filtering here, but that # seems to blow a hole throught models that have some efficient # mechanism for determining whether or not they have children. There # is also a precedent for doing it this way in Windoze, where a node # gets marked as though it can be expanded, even thought when the # expansion occurs, no children are present! return self.model.has_children(node) def _get_children(self, node): """ Get the children of a node. """ children = self.model.get_children(node) # Filtering.... filtered_children = [] for child in children: for filter in self.filters: if not filter.select(self, node, child): break else: filtered_children.append(child) # Sorting... if self.sorter is not None: self.sorter.sort(self, node, filtered_children) return filtered_children def _get_image_index(self, node): """ Returns the tree item image index for a node. """ expanded = self.is_expanded(node) selected = self.is_selected(node) # Get the image used to represent the node. image = self.model.get_image(node, selected, expanded) if image is not None: image_index = self._image_list.GetIndex(image) else: image_index = -1 return image_index def _get_drag_drop_node(self, x, y): """ Returns the node that is being dragged/dropped on. Returns None if the cursor is not over the icon or label of a node. """ data, wxid, flags, point = self._hit_test((x, y)) if data is not None: populated, node = data else: node = None return node def _get_text(self, node): """ Returns the tree item text for a node. """ text = self.model.get_text(node) if text is None: text = "" return text def _unpack_event(self, event, wxid=None): """ Unpacks the event to see whether a tree item was involved. """ try: point = event.GetPosition() except: point = event.GetPoint() return self._hit_test(point, wxid) def _hit_test(self, point, wxid=None): """ Determines whether a point is within a node's label or icon. """ flags = wx.TREE_HITTEST_ONITEMLABEL if (wxid is None) or (not wxid.IsOk()): wxid, flags = self.control.HitTest(point) # Warning: On GTK we have to check the flags before we call 'GetItemData' # because if we call it when the hit test returns 'nowhere' it will # barf (on Windows it simply returns 'None' 8^() if flags & wx.TREE_HITTEST_NOWHERE: data = None elif ( flags & wx.TREE_HITTEST_ONITEMICON or flags & wx.TREE_HITTEST_ONITEMLABEL ): data = self.control.GetItemData(wxid) # fixme: Not sure why 'TREE_HITTEST_NOWHERE' doesn't catch everything! else: data = None return data, wxid, flags, point def _get_selection(self): """ Returns a list of the selected nodes """ selection = [] for wxid in self.control.GetSelections(): data = self.control.GetItemData(wxid) if data is not None: populated, node = data selection.append(self.model.get_selection_value(node)) return selection def _expand_item(self, wxid): """ Recursively expand a tree item. """ self.control.Expand(wxid) cid, cookie = self.control.GetFirstChild(wxid) while cid.IsOk(): self._expand_item(cid) cid, cookie = self.control.GetNextChild(wxid, cookie) return # Trait event handlers ------------------------------------------------- def _on_root_changed(self, event): """ Called when the root of the model has changed. """ root = event.new # Delete everything... if self.control is not None: self.control.DeleteAllItems() self._node_to_id_map = {} # ... and then add the root item back in. if root is not None: self._add_root_node(root) def _on_nodes_changed(self, event): """ Called when nodes have been changed. """ node_event = event.new self._update_node(self._get_wxid(node_event.node), node_event.node) for child in node_event.children: cid = self._get_wxid(child) if cid is not None: self._update_node(cid, child) def _on_nodes_inserted(self, event): """ Called when nodes have been inserted. """ node_event = event.new parent = node_event.node children = node_event.children index = node_event.index # Has the node actually appeared in the tree yet? pid = self._get_wxid(parent) if pid is not None: # The item data is a tuple. The first element indicates whether or # not we have already populated the item with its children. The # second element is the actual item data. if self.show_root or parent is not self.root: populated, node = self.control.GetItemData(pid) else: populated = True # If the node is not yet populated then just get the children and # add them. if not populated: for child in self._get_children(parent): self._add_node(pid, child) # Otherwise, insert them. else: # An index of -1 means append! if index == -1: index = self.control.GetChildrenCount(pid, False) for child in children: self._insert_node(pid, child, index) index += 1 # The element is now populated! if self.show_root or parent is not self.root: self.control.SetItemData(pid, (True, parent)) # Does the node have any children now? has_children = self.control.GetChildrenCount(pid) > 0 self.control.SetItemHasChildren(pid, has_children) # If the node is not expanded then expand it. if not self.is_expanded(parent): self.expand(parent) def _on_nodes_removed(self, event): """ Called when nodes have been removed. """ node_event = event.new parent = node_event.node # Has the node actually appeared in the tree yet? pid = self._get_wxid(parent) if pid is not None: for child in node_event.children: cid = self._get_wxid(child) if cid is not None: self.control.Delete(cid) # Does the node have any children left? has_children = self.control.GetChildrenCount(pid) > 0 self.control.SetItemHasChildren(pid, has_children) def _on_nodes_replaced(self, event): """ Called when nodes have been replaced. """ node_event = event.new old_new_children = zip(node_event.old_children, node_event.children) for old_child, new_child in old_new_children: cid = self._get_wxid(old_child) if cid is not None: # Remove listeners from the old node. self.model.remove_listener(old_child) # Delete all of the node's children. self.control.DeleteChildren(cid) # Update the visual appearance of the node. self._update_node(cid, new_child) # Update the node data. # # The item data is a tuple. The first element indicates # whether or not we have already populated the item with its # children. The second element is the actual item data (which # in our case is an arbitrary Python object provided by the # tree model). self.control.SetItemData(cid, (False, new_child)) # Remove the old node from the node to Id map. self._remove_wxid(old_child) # Add the new node to the node to Id map. self._set_wxid(new_child, cid) # Add listeners to the new node. self.model.add_listener(new_child) # Does the new node have any children? has_children = self._has_children(new_child) self.control.SetItemHasChildren(cid, has_children) # Update the tree's selection (in case the old node that was replaced # was selected, the selection should now include the new node). self.selection = self._get_selection() def _on_structure_changed(self, event): """ Called when the structure of a node has changed drastically. """ node_event = event.new self.refresh(node_event.node) return # wx event handlers ---------------------------------------------------- def _on_char(self, event): """ Called when a key is pressed when the tree has focus. """ self.key_pressed = KeyPressedEvent( alt_down=event.altDown, control_down=event.controlDown, shift_down=event.shiftDown, key_code=event.KeyCode, ) event.Skip() def _on_left_down(self, event): """ Called when the left mouse button is clicked on the tree. """ data, id, flags, point = self._unpack_event(event) # Save point for tree_begin_drag method to workaround a bug in ?? when # wx.TreeEvent.GetPoint returns only (0,0). This happens under linux # when using wx-2.4.2.4, for instance. self._point_left_clicked = point # Did the left click occur on a tree item? if data is not None: populated, node = data # Trait event notification. self.node_left_clicked = node, point # Give other event handlers a chance. event.Skip() def _on_right_down(self, event): """ Called when the right mouse button is clicked on the tree. """ data, id, flags, point = self._unpack_event(event) # Did the right click occur on a tree item? if data is not None: populated, node = data # Trait event notification. self.node_right_clicked = node, point # Otherwise notify that the control itself was clicked else: self.control_right_clicked = point # Give other event handlers a chance. event.Skip() def _on_tree_item_activated(self, event): """ Called when a tree item is activated (i.e., double clicked). """ # fixme: See the comment where the events are wired up for more # information. ## # Which item was activated? ## wxid = event.GetItem() # Which item was activated. point = event.GetPosition() wxid, flags = self.control.HitTest(point) # The item data is a tuple. The first element indicates whether or not # we have already populated the item with its children. The second # element is the actual item data. populated, node = self.control.GetItemData(wxid) # Trait event notiification. self.node_activated = node def _on_tree_item_collapsing(self, event): """ Called when a tree item is about to collapse. """ # Which item is collapsing? wxid = event.GetItem() # The item data is a tuple. The first element indicates whether or not # we have already populated the item with its children. The second # element is the actual item data. populated, node = self.control.GetItemData(wxid) # Give the model a chance to veto the collapse. if not self.model.is_collapsible(node): event.Veto() def _on_tree_item_collapsed(self, event): """ Called when a tree item has been collapsed. """ # Which item was collapsed? wxid = event.GetItem() # The item data is a tuple. The first element indicates whether or not # we have already populated the item with its children. The second # element is the actual item data. populated, node = self.control.GetItemData(wxid) # Make sure that the item's 'closed' icon is displayed etc. self._update_node(wxid, node) # Trait event notification. self.node_collapsed = node def _on_tree_item_expanding(self, event): """ Called when a tree item is about to expand. """ # Which item is expanding? wxid = event.GetItem() # The item data is a tuple. The first element indicates whether or not # we have already populated the item with its children. The second # element is the actual item data. populated, node = self.control.GetItemData(wxid) # Give the model a chance to veto the expansion. if self.model.is_expandable(node): # Lazily populate the item's children. if not populated: # Add the child nodes. for child in self._get_children(node): self._add_node(wxid, child) # The element is now populated! self.control.SetItemData(wxid, (True, node)) else: event.Veto() def _on_tree_item_expanded(self, event): """ Called when a tree item has been expanded. """ # Which item was expanded? wxid = event.GetItem() # The item data is a tuple. The first element indicates whether or not # we have already populated the item with its children. The second # element is the actual item data. populated, node = self.control.GetItemData(wxid) # Make sure that the node's 'open' icon is displayed etc. self._update_node(wxid, node) # Trait event notification. self.node_expanded = node def _on_tree_begin_label_edit(self, event): """ Called when the user has started editing an item's label. """ wxid = event.GetItem() # The item data is a tuple. The first element indicates whether or not # we have already populated the item with its children. The second # element is the actual item data. populated, node = self.control.GetItemData(wxid) # Give the model a chance to veto the edit. if not self.model.is_editable(node): event.Veto() def _on_tree_end_label_edit(self, event): """ Called when the user has finished editing am item's label. """ wxid = event.GetItem() # The item data is a tuple. The first element indicates whether or not # we have already populated the item with its children. The second # element is the actual item data. populated, node = self.control.GetItemData(wxid) # Give the model a chance to veto the edit. label = event.GetLabel() # Making sure the new label is not an empty string if ( label is not None and len(label) > 0 and self.model.can_set_text(node, label) ): def end_label_edit(): """ Called to complete the label edit. """ # Set the node's text. self.model.set_text(node, label) # If a label edit callback was specified (in the call to # 'edit_label'), then call it). if self._label_edit_callback is not None: self._label_edit_callback(self, node, label) return # We use a deffered call here, because a name change can trigger # the structure of a node to change, and hence the actual tree # nodes might get moved/deleted before the label edit operation has # completed. When this happens wx gets very confused! By using # 'invoke_later' we allow the label edit to complete. GUI.invoke_later(end_label_edit) else: event.Veto() # If a label edit callback was specified (in the call to # 'edit_label'), then call it). if self._label_edit_callback is not None: self._label_edit_callback(self, node, label) def _on_tree_begin_drag(self, event): """ Called when a drag operation is starting on a tree item. """ # Get the node, its id and the point where the event occurred. data, wxid, flags, point = self._unpack_event(event, event.GetItem()) if point == (0, 0): # Apply workaround for GTK. point = self.point_left_clicked wxid, flags = self.HitTest(point) data = self.control.GetItemData(wxid) if data is not None: populated, node = data # Give the model a chance to veto the drag. if self.model.is_draggable(node): # We ask the model for the actual value to drag. drag_value = self.model.get_drag_value(node) # fixme: This is a terrible hack to get the binding x passed # during a drag operation. Bindings should probably *always* # be dragged and our drag and drop mechanism should allow # extendable ways to extract the actual data. from pyface.wx.drag_and_drop import clipboard clipboard.node = [node] # Make sure that the tree selection is updated before we start # the drag. If we don't do this then if the first thing a # user does is drag a tree item (i.e., without a separate click # to select it first) then the selection appears empty. self.selection = self._get_selection() # Start the drag. PythonDropSource(self.control, drag_value, self) # Trait event notification. self.node_begin_drag = node else: event.Veto() return # fixme: This is part of the drag and drop hack... def on_dropped(self): """ Callback invoked when a drag/drop operation has completed. """ from pyface.wx.drag_and_drop import clipboard clipboard.node = None def _on_tree_sel_changed(self, event): """ Called when the selection is changed. """ # Update our record of the selection to whatever was selected in the # tree UNLESS we are ignoring selection events. if not self._ignore_selection_events: # Trait notification. self.selection = self._get_selection() def _on_tree_delete_item(self, event): """ Called when a tree item is being been deleted. """ # Which item is being deleted? wxid = event.GetItem() # Check if GetItemData() returned a valid to tuple to unpack # ...if so, remove the node from the tree, otherwise just return # # fixme: Whoever addeed this code (and the comment above) didn't say # when this was occurring. This is method is called in response to a wx # event to delete an item and hence the item data should never be None # surely?!? Was it happening just on one platform?!? if self.control is None: return try: data = self.control.GetItemData(wxid) except Exception: # most likely control is in the process of being destroyed data = None if data is not None: # The item data is a tuple. The first element indicates whether or # not we have already populated the item with its children. The # second element is the actual item data. populated, node = data # Remove the node. self._remove_node(wxid, node) pyface-7.4.0/pyface/ui/wx/tree/__init__.py0000644000076500000240000000000014176222673021324 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/wx/python_shell.py0000644000076500000240000002515614176222673021361 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Enthought pyface package component """ import builtins import os import sys import types import warnings from wx.py.shell import Shell as PyShellBase import wx from traits.api import Event, provides from traits.util.clean_strings import python_name from pyface.wx.drag_and_drop import PythonDropTarget from pyface.i_python_shell import IPythonShell, MPythonShell from pyface.key_pressed_event import KeyPressedEvent from .layout_widget import LayoutWidget @provides(IPythonShell) class PythonShell(MPythonShell, LayoutWidget): """ The toolkit specific implementation of a PythonShell. See the IPythonShell interface for the API documentation. """ # 'IPythonShell' interface --------------------------------------------- command_executed = Event() key_pressed = Event(KeyPressedEvent) # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ # FIXME v3: Either make this API consistent with other Widget sub-classes # or make it a sub-class of HasTraits. def __init__(self, parent=None, **traits): """ Creates a new pager. """ create = traits.pop("create", True) # Base class constructor. super().__init__(parent=parent, **traits) if create: # Create the toolkit-specific control that represents the widget. self.create() warnings.warn( "automatic widget creation is deprecated and will be removed " "in a future Pyface version, use create=False and explicitly " "call create() for future behaviour", PendingDeprecationWarning, ) # ------------------------------------------------------------------------ # 'IPythonShell' interface. # ------------------------------------------------------------------------ def interpreter(self): return self.control.interp def execute_command(self, command, hidden=True): if hidden: self.control.hidden_push(command) else: # Replace the edit area text with command, then run it: self.control.Execute(command) def execute_file(self, path, hidden=True): # Note: The code in this function is largely ripped from IPython's # Magic.py, FakeModule.py, and iplib.py. filename = os.path.basename(path) # Run in a fresh, empty namespace main_mod = types.ModuleType("__main__") prog_ns = main_mod.__dict__ prog_ns["__file__"] = filename prog_ns["__nonzero__"] = lambda: True # Make sure that the running script gets a proper sys.argv as if it # were run from a system shell. save_argv = sys.argv sys.argv = [filename] # Make sure that the running script thinks it is the main module save_main = sys.modules["__main__"] sys.modules["__main__"] = main_mod # Redirect sys.std* to control or null old_stdin = sys.stdin old_stdout = sys.stdout old_stderr = sys.stderr if hidden: sys.stdin = sys.stdout = sys.stderr = _NullIO() else: sys.stdin = sys.stdout = sys.stderr = self.control # Execute the file try: if not hidden: self.control.clearCommand() self.control.write('# Executing "%s"\n' % path) exec(open(path).read(), prog_ns, prog_ns) if not hidden: self.control.prompt() finally: # Ensure key global stuctures are restored sys.argv = save_argv sys.modules["__main__"] = save_main sys.stdin = old_stdin sys.stdout = old_stdout sys.stderr = old_stderr # Update the interpreter with the new namespace del prog_ns["__name__"] del prog_ns["__file__"] del prog_ns["__nonzero__"] self.interpreter().locals.update(prog_ns) def get_history(self): """ Return the current command history and index. Returns ------- history : list of str The list of commands in the new history. history_index : int from 0 to len(history) The current item in the command history navigation. """ return self.control.history, self.control.historyIndex def set_history(self, history, history_index): """ Replace the current command history and index with new ones. Parameters ---------- history : list of str The list of commands in the new history. history_index : int from 0 to len(history) The current item in the command history navigation. """ if not 0 <= history_index <= len(history): history_index = len(history) self.control.history = list(history) self.control.historyIndex = history_index # ------------------------------------------------------------------------ # 'IWidget' interface. # ------------------------------------------------------------------------ def _create_control(self, parent): shell = PyShell(parent, -1) # Listen for key press events. shell.Bind(wx.EVT_CHAR, self._wx_on_char) # Enable the shell as a drag and drop target. shell.SetDropTarget(PythonDropTarget(self)) # Set up to be notified whenever a Python statement is executed: shell.handlers.append(self._on_command_executed) return shell # ------------------------------------------------------------------------ # 'PythonDropTarget' handler interface. # ------------------------------------------------------------------------ def on_drop(self, x, y, obj, default_drag_result): """ Called when a drop occurs on the shell. """ # If we can't create a valid Python identifier for the name of an # object we use this instead. name = "dragged" if ( hasattr(obj, "name") and isinstance(obj.name, str) and len(obj.name) > 0 ): py_name = python_name(obj.name) # Make sure that the name is actually a valid Python identifier. try: if eval(py_name, {py_name: True}): name = py_name except: pass self.control.interp.locals[name] = obj self.control.run(name) self.control.SetFocus() # We always copy into the shell since we don't want the data # removed from the source return wx.DragCopy def on_drag_over(self, x, y, obj, default_drag_result): """ Always returns wx.DragCopy to indicate we will be doing a copy.""" return wx.DragCopy # ------------------------------------------------------------------------ # Private handler interface. # ------------------------------------------------------------------------ def _wx_on_char(self, event): """ Called whenever a change is made to the text of the document. """ # This was originally in the python_shell plugin, but is toolkit # specific. if event.AltDown() and event.GetKeyCode() == 317: zoom = self.shell.control.GetZoom() if zoom != 20: self.control.SetZoom(zoom + 1) elif event.AltDown() and event.GetKeyCode() == 319: zoom = self.shell.control.GetZoom() if zoom != -10: self.control.SetZoom(zoom - 1) self.key_pressed = KeyPressedEvent( alt_down=event.AltDown() == 1, control_down=event.ControlDown() == 1, shift_down=event.ShiftDown() == 1, key_code=event.GetKeyCode(), event=event, ) # Give other event handlers a chance. event.Skip() class PyShell(PyShellBase): def __init__( self, parent, id=-1, pos=wx.DefaultPosition, size=wx.DefaultSize, style=wx.CLIP_CHILDREN, introText="", locals=None, InterpClass=None, *args, **kwds ): self.handlers = [] # save a reference to the original raw_input() function since # wx.py.shell dosent reassign it back to the original on destruction self.raw_input = input super().__init__( parent, id, pos, size, style, introText, locals, InterpClass, *args, **kwds ) def hidden_push(self, command): """ Send a command to the interpreter for execution without adding output to the display. """ wx.BeginBusyCursor() try: self.waiting = True self.more = self.interp.push(command) self.waiting = False if not self.more: self.addHistory(command.rstrip()) for handler in self.handlers: handler() finally: # This needs to be out here to make this works with # traits.util.refresh.refresh() wx.EndBusyCursor() def push(self, command): """Send command to the interpreter for execution.""" self.write(os.linesep) self.hidden_push(command) self.prompt() def Destroy(self): """Cleanup before destroying the control...namely, return std I/O and the raw_input() function back to their rightful owners! """ self.redirectStdout(False) self.redirectStderr(False) self.redirectStdin(False) builtins.raw_input = self.raw_input self.destroy() super().Destroy() class _NullIO(object): """ A portable /dev/null for use with PythonShell.execute_file. """ def tell(self): return 0 def read(self, n=-1): return "" def readline(self, length=None): return "" def readlines(self): return [] def write(self, s): pass def writelines(self, list): pass def isatty(self): return 0 def flush(self): pass def close(self): pass def seek(self, pos, mode=0): pass pyface-7.4.0/pyface/ui/wx/ipython_widget.py0000644000076500000240000004176614176222673021713 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 wx-backend Pyface widget for an embedded IPython shell. """ import builtins import codeop import re import sys import warnings import IPython from IPython.frontend.wx.wx_frontend import WxController from IPython.kernel.core.interpreter import Interpreter import wx from apptools.io.file import File as EnthoughtFile from pyface.i_python_shell import IPythonShell from pyface.key_pressed_event import KeyPressedEvent from traits.api import Event, Instance, provides, Str from traits.util.clean_strings import python_name from pyface.wx.drag_and_drop import PythonDropTarget from .widget import Widget # Constants. IPYTHON_VERSION = tuple(map(int, IPython.Release.version_base.split("."))) class IPythonController(WxController): """ An WxController for IPython version >= 0.9. Adds a few extras. """ def __init__(self, *args, **kwargs): WxController.__init__(self, *args, **kwargs) # Add a magic to clear the screen def cls(args): self.ClearAll() self.ipython0.magic_cls = cls class IPython010Controller(IPythonController): """ A WxController hacked/patched specifically for the 0.10 branch. """ def execute_command(self, command, hidden=False): # XXX: Overriden to fix bug where executing a hidden command still # causes the prompt number to increase. super().execute_command(command, hidden) if hidden: self.shell.current_cell_number -= 1 class IPython09Controller(IPythonController): """ A WxController hacked/patched specifically for the 0.9 branch. """ # In the parent class, this is a property that expects the # container to be a frame, thus it fails when modified. # The title of the IPython windows (not displayed in Envisage) title = Str() # Cached value of the banner for the IPython shell. # NOTE: The WxController object (declared in wx_frontend module) contains # a 'banner' attribute. If this is None, WxController sets the # banner = IPython banner + an additional string ("This is the wx # frontend, by Gael Varoquaux. This is EXPERIMENTAL code."). We want to # set banner = the IPython banner. This means 'banner' needs to be # a property, since in the __init__ method of WxController, the IPython # shell object is created AND started (meaning the banner is written to # stdout). _banner = None def _get_banner(self): """ Returns the IPython banner. """ if self._banner is None: # 'ipython0' gets set in the __init__ method of the base class. if hasattr(self, "ipython0"): self._banner = self.ipython0.BANNER return self._banner def _set_banner(self, value): self._banner = value banner = property(_get_banner, _set_banner) def __init__(self, *args, **kwargs): # This is a hack to avoid the IPython exception hook to trigger # on exceptions (https://bugs.launchpad.net/bugs/337105) # XXX: This is horrible: module-level monkey patching -> side # effects. from IPython import iplib iplib.InteractiveShell.isthreaded = True # Suppress all key input, to avoid waiting def my_rawinput(x=None): return "\n" old_rawinput = builtins.raw_input builtins.raw_input = my_rawinput IPythonController.__init__(self, *args, **kwargs) builtins.raw_input = old_rawinput # XXX: This is bugware for IPython bug: # https://bugs.launchpad.net/ipython/+bug/270998 # Fix all the magics with no docstrings: for funcname in dir(self.ipython0): if not funcname.startswith("magic"): continue func = getattr(self.ipython0, funcname) try: if func.__doc__ is None: func.__doc__ = "" except AttributeError: """ Avoid "attribute '__doc__' of 'instancemethod' objects is not writable". """ def complete(self, line): """ Returns a list of possible completions for line. Overridden from the base class implementation to fix bugs in retrieving the completion text from line. """ completion_text = self._get_completion_text(line) suggestion, completions = super().complete(completion_text) new_line = line[: -len(completion_text)] + suggestion return new_line, completions def is_complete(self, string): """ Check if a string forms a complete, executable set of commands. For the line-oriented frontend, multi-line code is not executed as soon as it is complete: the users has to enter two line returns. Overridden from the base class (linefrontendbase.py in IPython\frontend to handle a bug with using the '\' symbol in multi-line inputs. """ # FIXME: There has to be a nicer way to do this. Th code is # identical to the base class implementation, except for the if .. # statement on line 146. if string in ("", "\n"): # Prefiltering, eg through ipython0, may return an empty # string although some operations have been accomplished. We # thus want to consider an empty string as a complete # statement. return True elif len(self.input_buffer.split("\n")) > 2 and not re.findall( r"\n[\t ]*\n[\t ]*$", string ): return False else: self.capture_output() try: # Add line returns here, to make sure that the statement is # complete (except if '\' was used). # This should probably be done in a different place (like # maybe 'prefilter_input' method? For now, this works. clean_string = string.rstrip("\n") if not clean_string.endswith("\\"): clean_string += "\n\n" is_complete = codeop.compile_command( clean_string, "", "exec" ) self.release_output() except Exception: # XXX: Hack: return True so that the # code gets executed and the error captured. is_complete = True return is_complete def execute_command(self, command, hidden=False): """ Execute a command, not only in the model, but also in the view. """ # XXX: This needs to be moved to the IPython codebase. if hidden: result = self.shell.execute(command) # XXX: Fix bug where executing a hidden command still causes the # prompt number to increase. self.shell.current_cell_number -= 1 return result else: # XXX: we are not storing the input buffer previous to the # execution, as this forces us to run the execution # input_buffer a yield, which is not good. # #current_buffer = self.shell.control.input_buffer command = command.rstrip() if len(command.split("\n")) > 1: # The input command is several lines long, we need to # force the execution to happen command += "\n" cleaned_command = self.prefilter_input(command) self.input_buffer = command # Do not use wx.Yield() (aka GUI.process_events()) to avoid # recursive yields. self.ProcessEvent(wx.PaintEvent()) self.write("\n") if not self.is_complete(cleaned_command + "\n"): self._colorize_input_buffer() self.render_error("Incomplete or invalid input") self.new_prompt( self.input_prompt_template.substitute( number=(self.last_result["number"] + 1) ) ) return False self._on_enter() return True def clear_screen(self): """ Empty completely the widget. """ self.ClearAll() self.new_prompt( self.input_prompt_template.substitute( number=(self.last_result["number"] + 1) ) ) def continuation_prompt(self): """Returns the current continuation prompt. Overridden to generate a continuation prompt matching the length of the current prompt.""" # This assumes that the prompt is always of the form 'In [#]'. n = self.last_result["number"] promptstr = "In [%d]" % n return "." * len(promptstr) + ":" def _popup_completion(self, create=False): """ Updates the popup completion menu if it exists. If create is true, open the menu. Overridden from the base class implementation to filter out delimiters from the input buffer. """ # FIXME: The implementation in the base class (wx_frontend.py in # IPython/wx/frontend) is faulty in that it doesn't filter out # special characters (such as parentheses, '=') in the input buffer # correctly. # For example, (a): typing 's=re.' does not pop up the menu. # (b): typing 'x[0].' brings up a menu for x[0] but the offset is # incorrect and so, upon selection from the menu, the text is pasted # incorrectly. # I am patching this here instead of in the IPython module, but at some # point, this needs to be merged in. if self.debug: print("_popup_completion", self.input_buffer, file=sys.__stdout__) line = self.input_buffer if create or ( self.AutoCompActive() and line and not line[-1] == "." ): suggestion, completions = self.complete(line) if completions: offset = len(self._get_completion_text(line)) self.pop_completion(completions, offset=offset) if self.debug: print(completions, file=sys.__stdout__) def _get_completion_text(self, line): """ Returns the text to be completed by breaking the line at specified delimiters. """ # Break at: spaces, '=', all parentheses (except if balanced). # FIXME2: In the future, we need to make the implementation similar to # that in the 'pyreadline' module (modes/basemode.py) where we break at # each delimiter and try to complete the residual line, until we get a # successful list of completions. expression = r"\s|=|,|:|\((?!.*\))|\[(?!.*\])|\{(?!.*\})" complete_sep = re.compile(expression) text = complete_sep.split(line)[-1] return text def _on_enter(self): """ Called when the return key is pressed in a line editing buffer. Overridden from the base class implementation (in IPython/frontend/linefrontendbase.py) to include a continuation prompt. """ current_buffer = self.input_buffer cleaned_buffer = self.prefilter_input( current_buffer.replace(self.continuation_prompt(), "") ) if self.is_complete(cleaned_buffer): self.execute(cleaned_buffer, raw_string=current_buffer) else: self.input_buffer = ( current_buffer + self.continuation_prompt() + self._get_indent_string( current_buffer.replace(self.continuation_prompt(), "")[:-1] ) ) if len(current_buffer.split("\n")) == 2: self.input_buffer += "\t\t" if current_buffer[:-1].split("\n")[-1].rstrip().endswith(":"): self.input_buffer += "\t" @provides(IPythonShell) class IPythonWidget(Widget): """ The toolkit specific implementation of a PythonShell. See the IPythonShell interface for the API documentation. """ # 'IPythonShell' interface --------------------------------------------- command_executed = Event() key_pressed = Event(KeyPressedEvent) # 'IPythonWidget' interface -------------------------------------------- interp = Instance(Interpreter, ()) # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ # FIXME v3: Either make this API consistent with other Widget sub-classes # or make it a sub-class of HasTraits. def __init__(self, parent, **traits): """ Creates a new pager. """ warnings.warn( "the Wx IPython widget us deprecated and will be removed in a " "future Pyface version", PendingDeprecationWarning, ) # Base class constructor. super().__init__(**traits) # Create the toolkit-specific control that represents the widget. self.control = self._create_control(parent) # ------------------------------------------------------------------------ # 'IPythonShell' interface. # ------------------------------------------------------------------------ def interpreter(self): return self.interp def execute_command(self, command, hidden=True): self.control.execute_command(command, hidden=hidden) self.command_executed = True def execute_file(self, path, hidden=True): self.control.execute_command("%run " + '"%s"' % path, hidden=hidden) self.command_executed = True # ------------------------------------------------------------------------ # Protected 'IWidget' interface. # ------------------------------------------------------------------------ def _create_control(self, parent): # Create the controller based on the version of the installed IPython klass = IPythonController if IPYTHON_VERSION[0] == 0: if IPYTHON_VERSION[1] == 9: klass = IPython09Controller elif IPYTHON_VERSION[1] == 10: klass = IPython010Controller shell = klass(parent, -1, shell=self.interp) # Listen for key press events. shell.Bind(wx.EVT_CHAR, self._wx_on_char) # Enable the shell as a drag and drop target. shell.SetDropTarget(PythonDropTarget(self)) return shell # ------------------------------------------------------------------------ # 'PythonDropTarget' handler interface. # ------------------------------------------------------------------------ def on_drop(self, x, y, obj, default_drag_result): """ Called when a drop occurs on the shell. """ # If this is a file, we'll just print the file name if isinstance(obj, EnthoughtFile): self.control.write(obj.absolute_path) elif ( isinstance(obj, list) and len(obj) == 1 and isinstance(obj[0], EnthoughtFile) ): self.control.write(obj[0].absolute_path) else: # Not a file, we'll inject the object in the namespace # If we can't create a valid Python identifier for the name of an # object we use this instead. name = "dragged" if ( hasattr(obj, "name") and isinstance(obj.name, str) and len(obj.name) > 0 ): py_name = python_name(obj.name) # Make sure that the name is actually a valid Python identifier. try: if eval(py_name, {py_name: True}): name = py_name except: pass self.interp.user_ns[name] = obj self.execute_command(name, hidden=False) self.control.SetFocus() # We always copy into the shell since we don't want the data # removed from the source return wx.DragCopy def on_drag_over(self, x, y, obj, default_drag_result): """ Always returns wx.DragCopy to indicate we will be doing a copy.""" return wx.DragCopy # ------------------------------------------------------------------------ # Private handler interface. # ------------------------------------------------------------------------ def _wx_on_char(self, event): """ Called whenever a change is made to the text of the document. """ self.key_pressed = KeyPressedEvent( alt_down=event.AltDown() == 1, control_down=event.ControlDown() == 1, shift_down=event.ShiftDown() == 1, key_code=event.GetKeyCode(), event=event, ) # Give other event handlers a chance. event.Skip() pyface-7.4.0/pyface/ui/wx/layout_widget.py0000644000076500000240000000707614176222673021532 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 provides from pyface.i_layout_item import DEFAULT_SIZE from pyface.i_layout_widget import ILayoutWidget, MLayoutWidget from pyface.ui.wx.widget import Widget #: The special "default" size for Wx Size objects. WX_DEFAULT_SIZE = -1 @provides(ILayoutWidget) class LayoutWidget(MLayoutWidget, Widget): """ A widget which can participate as part of a layout. This is an abstract class, as Widget._create_control needs to be implemented at a minimum. """ def _set_control_minimum_size(self, size): """ Set the minimum size of the control. """ wx_size = _size_to_wx_size(size) self.control.SetMinSize(wx_size) def _get_control_minimum_size(self): """ Get the minimum size of the control. """ wx_size = self.control.GetMinSize() return _wx_size_to_size(wx_size) def _set_control_maximum_size(self, size): """ Set the maximum size of the control. """ wx_size = _size_to_wx_size(size) self.control.SetMaxSize(wx_size) def _get_control_maximum_size(self): """ Get the maximum size of the control. """ wx_size = self.control.GetMaxSize() return _wx_size_to_size(wx_size) def _set_control_stretch(self, size_policy): """ Set the stretch factor of the control. In Wx the stretch factor is set at layout time and can't be changed without removing and adding the widget back into a layout. """ pass def _get_control_stretch(self): """ Get the stretch factor of the control. In Wx the stretch factor is set at layout time and can't be obtained from the widget itself. As a result, this method simply returns the current trait value. This method is only used for testing. """ return self.stretch def _set_control_size_policy(self, size_policy): """ Set the size policy of the control In Wx the size policy is set at layout time and can't be changed without removing and adding the widget back into a layout. """ pass def _get_control_size_policy(self): """ Get the size policy of the control In Wx the size policy is set at layout time and can't be obtained from the widget itself. As a result, this method simply returns the current trait value. This method is only used for testing. """ return self.size_policy def _size_to_wx_size(size): """ Convert a size tuple to a wx.Size instance. Parameters ---------- size : tuple of (width, height) The width and height as a tuple of ints. Returns ------- wx_size : wx.Size instance A corresponding wx Size instance. """ return wx.Size(*( x if x != DEFAULT_SIZE else WX_DEFAULT_SIZE for x in size )) def _wx_size_to_size(wx_size): """ Convert a wx.Size instance to a size tuple. Parameters ---------- wx_size : wx.Size instance A wx Size instance. Returns ------- size : tuple of (width, height) The corresponding width and height as a tuple of ints. """ return (wx_size.GetWidth(), wx_size.GetHeight()) pyface-7.4.0/pyface/ui/wx/mdi_application_window.py0000644000076500000240000001327414176222673023372 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ An MDI top-level application window. """ import wx from traits.api import Bool, Int, Tuple from pyface.ui_traits import Image from .application_window import ApplicationWindow from .image_resource import ImageResource try: # import wx.aui from wx.lib.agw import aui # noqa: F401 AUI = True except ImportError: AUI = False class MDIApplicationWindow(ApplicationWindow): """ An MDI top-level application window. The application window has support for a menu bar, tool bar and a status bar (all of which are optional). Usage: Create a sub-class of this class and override the protected '_create_contents' method. """ # 'MDIApplicationWindow' interface ------------------------------------- # The workarea background image. background_image = Image(ImageResource("background")) # Should we tile the workarea background image? The alternative is to # scale it. Be warned that scaling the image allows for 'pretty' images, # but is MUCH slower than tiling. tile_background_image = Bool(True) # WX HACK FIXME # UPDATE: wx 2.6.1 does NOT fix this issue. _wx_offset = Tuple(Int, Int) # ------------------------------------------------------------------------ # 'MDIApplicationWindow' interface. # ------------------------------------------------------------------------ def create_child_window(self, title=None, is_mdi=True, float=True): """ Create a child window. """ if title is None: title = self.title if is_mdi: return wx.MDIChildFrame(self.control, -1, title) else: if float: style = wx.DEFAULT_FRAME_STYLE | wx.FRAME_FLOAT_ON_PARENT else: style = wx.DEFAULT_FRAME_STYLE return wx.Frame(self.control, -1, title, style=style) # ------------------------------------------------------------------------ # Protected 'Window' interface. # ------------------------------------------------------------------------ def _create_contents(self, parent): """ Create the contents of the MDI window. """ # Create the 'trim' widgets (menu, tool and status bars etc). self._create_trim_widgets(self.control) # The work-area background image (it can be tiled or scaled). self._image = self.background_image.create_image() self._bmp = self._image.ConvertToBitmap() # Frame events. # # We respond to size events to layout windows around the MDI frame. self.control.Bind(wx.EVT_SIZE, self._on_size) # Client window events. client_window = self.control.GetClientWindow() client_window.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background) try: self._wx_offset = client_window.GetPosition().Get() except: self._wx_offset = (0, 0) if AUI: # Let the AUI manager look after the frame. self._aui_manager.SetManagedWindow(self.control) contents = super()._create_contents(parent) return contents def _create_control(self, parent): """ Create the toolkit-specific control that represents the window. """ control = wx.MDIParentFrame( parent, -1, self.title, style=wx.DEFAULT_FRAME_STYLE, size=self.size, pos=self.position, ) return control # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _tile_background_image(self, dc, width, height): """ Tiles the background image. """ w = self._bmp.GetWidth() h = self._bmp.GetHeight() x = 0 while x < width: y = 0 while y < height: dc.DrawBitmap(self._bmp, x, y) y = y + h x = x + w def _scale_background_image(self, dc, width, height): """ Scales the background image. """ # Scale the image (if necessary). image = self._image if image.GetWidth() != width or image.GetHeight() != height: image = self._image.Copy() image.Rescale(width, height) # Convert it to a bitmap and draw it. dc.DrawBitmap(image.ConvertToBitmap(), 0, 0) return ## wx event handlers --------------------------------------------------- def _on_size(self, event): """ Called when the frame is resized. """ wx.adv.LayoutAlgorithm().LayoutMDIFrame(self.control) event.Skip() def _on_erase_background(self, event): """ Called when the background of the MDI client window is erased. """ # fixme: Close order... if self.control is None: return frame = self.control dc = event.GetDC() if not dc: dc = wx.ClientDC(frame.GetClientWindow()) size = frame.GetClientSize() # Currently you have two choices, tile the image or scale it. Be # warned that scaling is MUCH slower than tiling. if self.tile_background_image: self._tile_background_image(dc, size.width, size.height) else: self._scale_background_image(dc, size.width, size.height) pyface-7.4.0/pyface/ui/wx/image_widget.py0000644000076500000240000001630114176222673021266 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 clickable/draggable widget containing an image. """ import warnings import wx from traits.api import Any, Bool, Event from .layout_widget import LayoutWidget class ImageWidget(LayoutWidget): """ A clickable/draggable widget containing an image. """ # 'ImageWidget' interface ---------------------------------------------# # The bitmap. bitmap = Any() # Is the widget selected? selected = Bool(False) # Events ---- # A key was pressed while the tree is in focus. key_pressed = Event() # A node has been activated (ie. double-clicked). node_activated = Event() # A drag operation was started on a node. node_begin_drag = Event() # A (non-leaf) node has been collapsed. node_collapsed = Event() # A (non-leaf) node has been expanded. node_expanded = Event() # A left-click occurred on a node. node_left_clicked = Event() # A right-click occurred on a node. node_right_clicked = Event() # Private interface ---------------------------------------------------- _selected = Any() # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, parent, **traits): """ Creates a new widget. """ # Base class constructors. create = traits.pop('create', True) # Base-class constructors. super().__init__(parent=parent, **traits) # Create the widget! if create: self.create() warnings.warn( "automatic widget creation is deprecated and will be removed " "in a future Pyface version, use create=False and explicitly " "call create() for future behaviour", PendingDeprecationWarning, ) def _create_control(self, parent): # Add some padding around the image. size = (self.bitmap.GetWidth() + 10, self.bitmap.GetHeight() + 10) # Create the toolkit-specific control. self.control = wx.Window(parent, -1, size=size) self.control.__tag__ = "hack" self._mouse_over = False self._button_down = False # Set up mouse event handlers: self.control.Bind(wx.EVT_ENTER_WINDOW, self._on_enter_window) self.control.Bind(wx.EVT_LEAVE_WINDOW, self._on_leave_window) self.control.Bind(wx.EVT_LEFT_DCLICK, self._on_left_dclick) self.control.Bind(wx.EVT_LEFT_DOWN, self._on_left_down) self.control.Bind(wx.EVT_LEFT_UP, self._on_left_up) self.control.Bind(wx.EVT_PAINT, self._on_paint) # Pens used to draw the 'selection' marker: # ZZZ: Make these class instances when moved to the wx toolkit code. self._selectedPenDark = wx.Pen( wx.SystemSettings.GetColour(wx.SYS_COLOUR_3DSHADOW), 1, wx.SOLID ) self._selectedPenLight = wx.Pen( wx.SystemSettings.GetColour(wx.SYS_COLOUR_3DHIGHLIGHT), 1, wx.SOLID ) return self.control # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ # Trait event handlers ------------------------------------------------- def _bitmap_changed(self, bitmap): """ Called when the widget's bitmap is changed. """ if self.control is not None: self.control.Refresh() def _selected_changed(self, selected): """ Called when the selected state of the widget is changed. """ if selected: for control in self.GetParent().GetChildren(): if hasattr(control, "__tag__"): if control.Selected(): control.Selected(False) break self.Refresh() return # wx event handlers ---------------------------------------------------- def _on_enter_window(self, event): """ Called when the mouse enters the widget. """ if self._selected is not None: self._mouse_over = True self.Refresh() def _on_leave_window(self, event): """ Called when the mouse leaves the widget. """ if self._mouse_over: self._mouse_over = False self.Refresh() def _on_left_dclick(self, event): """ Called when the left mouse button is double-clicked. """ # print 'left dclick' event.Skip() def _on_left_down(self, event=None): """ Called when the left mouse button goes down on the widget. """ # print 'left down' if self._selected is not None: self.CaptureMouse() self._button_down = True self.Refresh() event.Skip() def _on_left_up(self, event=None): """ Called when the left mouse button goes up on the widget. """ # print 'left up' need_refresh = self._button_down if need_refresh: self.ReleaseMouse() self._button_down = False if self._selected is not None: wdx, wdy = self.GetClientSize().Get() 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() return if need_refresh: self.Refresh() event.Skip() def _on_paint(self, event=None): """ Called when the widget needs repainting. """ wdc = wx.PaintDC(self.control) wdx, wdy = self.control.GetClientSize().Get() bitmap = self.bitmap 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: 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) return pyface-7.4.0/pyface/ui/wx/expandable_panel.py0000644000076500000240000001200314176222673022116 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 Layered panel. """ import warnings import wx from traits.api import Dict, Str from pyface.ui_traits import Image from .expandable_header import ExpandableHeader from .image_resource import ImageResource from .layout_widget import LayoutWidget class ExpandablePanel(LayoutWidget): """ An expandable panel. """ # The default style. STYLE = wx.CLIP_CHILDREN collapsed_image = Image(ImageResource("mycarat1")) expanded_image = Image(ImageResource("mycarat2")) _layers = Dict(Str) _headers = Dict(Str) # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, parent=None, **traits): """ Creates a new LayeredPanel. """ create = traits.pop("create", True) # Base class constructor. super().__init__(parent=parent, **traits) # Create the toolkit-specific control that represents the widget. if create: self.create() warnings.warn( "automatic widget creation is deprecated and will be removed " "in a future Pyface version, use create=False and explicitly " "call create() for future behaviour", PendingDeprecationWarning, ) # ------------------------------------------------------------------------ # 'Expandale' interface. # ------------------------------------------------------------------------ def add_panel(self, name, layer): """ Adds a layer with the specified name. All layers are hidden when they are added. Use 'show_layer' to make a layer visible. """ parent = self.control sizer = self.control.GetSizer() # Add the heading text. header = self._create_header(parent, text=name) sizer.Add(header, 0, wx.EXPAND) # Add the layer to our sizer. sizer.Add(layer, 1, wx.EXPAND) # All layers are hidden when they are added. Use 'show_layer' to make # a layer visible. sizer.Show(layer, False) # fixme: Should we warn if a layer is being overridden? self._layers[name] = layer return layer def remove_panel(self, name): """ Removes a layer and its header from the container.""" if name not in self._layers: return sizer = self.control.GetSizer() panel = self._layers[name] header = self._headers[name] # sizer.Remove(panel) panel.Destroy() # sizer.Remove(header) header.Destroy() sizer.Layout() # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _create_control(self, parent): """ Create the toolkit-specific control that represents the widget. """ panel = wx.Panel(parent, -1, style=self.STYLE) sizer = wx.BoxSizer(wx.VERTICAL) panel.SetSizer(sizer) panel.SetAutoLayout(True) return panel def _create_header(self, parent, text): """ Creates a panel header. """ sizer = wx.BoxSizer(wx.HORIZONTAL) panel = wx.Panel(parent, -1, style=wx.CLIP_CHILDREN) panel.SetSizer(sizer) panel.SetAutoLayout(True) # Add the panel header. heading = ExpandableHeader(panel, title=text, create=False) heading.create() sizer.Add(heading.control, 1, wx.EXPAND) # connect observers heading.observe(self._on_button, "panel_expanded") heading.observe(self._on_panel_closed, "panel_closed") # Resize the panel to match the sizer's minimum size. sizer.Fit(panel) # hang onto it for when we destroy self._headers[text] = panel return panel # event handlers ---------------------------------------------------- def _on_button(self, event): """ called when one of the expand/contract buttons is pressed. """ header = event.new name = header.title visible = header.state sizer = self.control.GetSizer() sizer.Show(self._layers[name], visible) sizer.Layout() # fixme: Errrr, maybe we can NOT do this! w, h = self.control.GetSize().Get() self.control.SetSize((w + 1, h + 1)) self.control.SetSize((w, h)) def _on_panel_closed(self, event): """ Called when the close button is clicked in a header. """ header = event.new name = header.title self.remove_panel(name) pyface-7.4.0/pyface/ui/wx/tasks/0000755000076500000240000000000014176460551017412 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/wx/tasks/advanced_editor_area_pane.py0000644000076500000240000000205714176222673025077 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 provides from pyface.tasks.i_advanced_editor_area_pane import IAdvancedEditorAreaPane from .editor_area_pane import EditorAreaPane # ---------------------------------------------------------------------------- # 'AdvancedEditorAreaPane' class. # ---------------------------------------------------------------------------- @provides(IAdvancedEditorAreaPane) class AdvancedEditorAreaPane(EditorAreaPane): """ The toolkit-specific implementation of an AdvancedEditorAreaPane. See the IAdvancedEditorAreaPane interface for API documentation. """ # No additional functionality over the standard EditorAreaPane in wx yet. pyface-7.4.0/pyface/ui/wx/tasks/task_window_backend.py0000644000076500000240000001711214176222673023767 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 from pyface.wx.aui import aui from traits.api import Instance, List, Str from .main_window_layout import MainWindowLayout from pyface.tasks.i_task_window_backend import MTaskWindowBackend from pyface.tasks.task_layout import PaneItem, TaskLayout # Logging logger = logging.getLogger(__name__) class AUILayout(TaskLayout): """ The layout for a main window's dock area using AUI Perspectives """ perspective = Str() class TaskWindowBackend(MTaskWindowBackend): """ The toolkit-specific implementation of a TaskWindowBackend. See the ITaskWindowBackend interface for API documentation. """ # Private interface ---------------------------------------------------- _main_window_layout = Instance(MainWindowLayout) # ------------------------------------------------------------------------ # 'ITaskWindowBackend' interface. # ------------------------------------------------------------------------ def create_contents(self, parent): """ Create and return the TaskWindow's contents. """ # No extra control needed for wx (it's all handled by the AUIManager in # the ApplicationWindow) but we do need to handle some events here self.window._aui_manager.Bind( aui.EVT_AUI_PANE_CLOSE, self._pane_close_requested ) def destroy(self): """ Destroy the backend. """ pass def hide_task(self, state): """ Assuming the specified TaskState is active, hide its controls. """ # Save the task's layout in case it is shown again later. self.window._active_state.layout = self.get_layout() # Now hide its controls. self.window._aui_manager.DetachPane(state.central_pane.control) state.central_pane.control.Hide() for dock_pane in state.dock_panes: logger.debug("hiding dock pane %s" % dock_pane.id) self.window._aui_manager.DetachPane(dock_pane.control) dock_pane.control.Hide() # Remove any tabbed notebooks left over after all the panes have been # removed self.window._aui_manager.UpdateNotebook() # Remove any still-left over stuff (i.e. toolbars) for info in self.window._aui_manager.GetAllPanes(): logger.debug("hiding remaining pane: %s" % info.name) control = info.window self.window._aui_manager.DetachPane(control) control.Hide() def show_task(self, state): """ Assuming no task is currently active, show the controls of the specified TaskState. """ # Show the central pane. info = ( aui.AuiPaneInfo() .Caption("Central") .Dockable(False) .Floatable(False) .Name("Central") .CentrePane() .Maximize() ) logger.debug("adding central pane to %s" % self.window) self.window._aui_manager.AddPane(state.central_pane.control, info) self.window._aui_manager.Update() # Show the dock panes. self._layout_state(state) def get_toolbars(self, task=None): if task is None: state = self.window._active_state else: state = self.window._get_state(task) toolbars = [] for tool_bar_manager in state.tool_bar_managers: info = self.window._aui_manager.GetPane(tool_bar_manager.id) toolbars.append(info) return toolbars def show_toolbars(self, toolbars): for info in toolbars: info.Show() self.window._aui_manager.Update() # Methods for saving and restoring the layout -------------------------# def get_layout(self): """ Returns a TaskLayout for the current state of the window. """ # Extract the layout from the main window. layout = AUILayout(id=self.window._active_state.task.id) self._main_window_layout.state = self.window._active_state self._main_window_layout.get_layout(layout, self.window) return layout def set_layout(self, layout): """ Applies a TaskLayout (which should be suitable for the active task) to the window. """ self.window._active_state.layout = layout self._layout_state(self.window._active_state) self.window._aui_manager.Update() # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _layout_state(self, state): """ Layout the dock panes in the specified TaskState using its TaskLayout. """ # # Assign the window's corners to the appropriate dock areas. # for name, corner in CORNER_MAP.iteritems(): # area = getattr(state.layout, name + '_corner') # self.control.setCorner(corner, AREA_MAP[area]) # Add all panes in the TaskLayout. self._main_window_layout.state = state self._main_window_layout.set_layout(state.layout, self.window) # Trait initializers --------------------------------------------------- def __main_window_layout_default(self): return TaskWindowLayout() # Signal handlers -----------------------------------------------------# def _pane_close_requested(self, evt): pane = evt.GetPane() logger.debug("_pane_close_requested: pane=%s" % pane.name) for dock_pane in self.window.dock_panes: logger.debug( "_pane_close_requested: checking pane=%s" % dock_pane.pane_name ) if dock_pane.pane_name == pane.name: logger.debug("_pane_close_requested: FOUND PANE!!!!!!") dock_pane.visible = False break def _focus_changed_signal(self, old, new): if self.window.active_task: panes = [self.window.central_pane] + self.window.dock_panes for pane in panes: if new and pane.control.isAncestorOf(new): pane.has_focus = True elif old and pane.control.isAncestorOf(old): pane.has_focus = False class TaskWindowLayout(MainWindowLayout): """ A MainWindowLayout for a TaskWindow. """ # 'TaskWindowLayout' interface ----------------------------------------- consumed = List() state = Instance("pyface.tasks.task_window.TaskState") # ------------------------------------------------------------------------ # 'MainWindowLayout' abstract interface. # ------------------------------------------------------------------------ def _get_dock_widget(self, pane): """ Returns the control associated with a PaneItem. """ for dock_pane in self.state.dock_panes: if dock_pane.id == pane.id: self.consumed.append(dock_pane.control) return dock_pane.control return None def _get_pane(self, dock_widget): """ Returns a PaneItem for a control. """ for dock_pane in self.state.dock_panes: if dock_pane.control == dock_widget: return PaneItem(id=dock_pane.id) return None pyface-7.4.0/pyface/ui/wx/tasks/editor_area_pane.py0000644000076500000240000001401514176222673023247 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 from pyface.tasks.i_editor_area_pane import IEditorAreaPane, MEditorAreaPane from traits.api import observe, provides import wx from pyface.wx.aui import aui, PyfaceAuiNotebook from .task_pane import TaskPane # Logging logger = logging.getLogger(__name__) # ---------------------------------------------------------------------------- # 'EditorAreaPane' class. # ---------------------------------------------------------------------------- @provides(IEditorAreaPane) class EditorAreaPane(TaskPane, MEditorAreaPane): """ The toolkit-specific implementation of a EditorAreaPane. See the IEditorAreaPane interface for API documentation. """ style = ( aui.AUI_NB_WINDOWLIST_BUTTON | aui.AUI_NB_TAB_MOVE | aui.AUI_NB_SCROLL_BUTTONS | aui.AUI_NB_CLOSE_ON_ACTIVE_TAB ) # ------------------------------------------------------------------------ # 'TaskPane' interface. # ------------------------------------------------------------------------ def create(self, parent): """ Create and set the toolkit-specific control that represents the pane. """ logger.debug("editor pane parent: %s" % parent) # Create and configure the tab widget. self.control = control = PyfaceAuiNotebook(parent, agwStyle=self.style) # Connect to the widget's signals. control.Bind( aui.EVT_AUINOTEBOOK_PAGE_CHANGED, self._update_active_editor ) control.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self._close_requested) def destroy(self): """ Destroy the toolkit-specific control that represents the pane. """ for editor in self.editors: self.remove_editor(editor) super().destroy() # ------------------------------------------------------------------------ # 'IEditorAreaPane' interface. # ------------------------------------------------------------------------ def activate_editor(self, editor): """ Activates the specified editor in the pane. """ index = self.control.GetPageIndex(editor.control) self.control.SetSelection(index) def add_editor(self, editor): """ Adds an editor to the pane. """ editor.editor_area = self editor.create(self.control) self.control.AddPage(editor.control, self._get_label(editor)) try: index = self.control.GetPageIndex(editor.control) self.control.SetPageToolTip(index, editor.tooltip) except AttributeError: pass self.editors.append(editor) self._update_tab_bar(event=None) # The EVT_AUINOTEBOOK_PAGE_CHANGED event is not sent when the first # editor is added. if len(self.editors) == 1: self.active_editor = editor def remove_editor(self, editor): """ Removes an editor from the pane. """ self.editors.remove(editor) index = self.control.GetPageIndex(editor.control) logger.debug("Removing page %d" % index) self.control.RemovePage(index) editor.destroy() editor.editor_area = None self._update_tab_bar(event=None) if not self.editors: self.active_editor = None # ------------------------------------------------------------------------ # Protected interface. # ------------------------------------------------------------------------ def _get_label(self, editor): """ Return a tab label for an editor. """ label = editor.name if editor.dirty: label = "*" + label if not label: label = " " # bug in agw that fails on empty label return label def _get_editor_with_control(self, control): """ Return the editor with the specified control. """ for editor in self.editors: if editor.control == control: return editor return None # Trait change handlers ------------------------------------------------ @observe("editors:items:[dirty, name]") def _update_label(self, event): editor = event.object index = self.control.GetPageIndex(editor.control) self.control.SetPageText(index, self._get_label(editor)) @observe("editors:items:tooltip") def _update_tooltip(self, event): editor = event.object self.control.SetPageToolTip(editor.control, editor.tooltip) # Signal handlers -----------------------------------------------------# def _close_requested(self, evt): index = evt.GetSelection() logger.debug("_close_requested: index=%d" % index) control = self.control.GetPage(index) editor = self._get_editor_with_control(control) # Veto the event even though we are going to delete the tab, otherwise # the notebook will delete the editor wx control and the call to # editor.close() will fail. IEditorAreaPane.remove_editor() needs # the control to exist so it can remove it from the list of managed # editors. evt.Veto() editor.close() def _update_active_editor(self, evt): index = evt.GetSelection() logger.debug("index=%d" % index) if index == wx.NOT_FOUND: self.active_editor = None else: logger.debug("num pages=%d" % self.control.GetPageCount()) control = self.control.GetPage(index) self.active_editor = self._get_editor_with_control(control) @observe("hide_tab_bar") def _update_tab_bar(self, event): if self.control is not None: pass # Can't actually hide the tab bar on wx.aui pyface-7.4.0/pyface/ui/wx/tasks/split_editor_area_pane.py0000644000076500000240000000202314176222673024456 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 provides from pyface.tasks.i_editor_area_pane import IEditorAreaPane from .editor_area_pane import EditorAreaPane # ---------------------------------------------------------------------------- # 'AdvancedEditorAreaPane' class. # ---------------------------------------------------------------------------- @provides(IEditorAreaPane) class SplitEditorAreaPane(EditorAreaPane): """ The toolkit-specific implementation of an AdvancedEditorAreaPane. See the IAdvancedEditorAreaPane interface for API documentation. """ # No additional functionality over the standard EditorAreaPane in wx yet. pyface-7.4.0/pyface/ui/wx/tasks/__init__.py0000644000076500000240000000000014176222673021512 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/wx/tasks/task_pane.py0000644000076500000240000000317414176222673021737 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.tasks.i_task_pane import ITaskPane, MTaskPane from traits.api import provides import wx # Logging import logging logger = logging.getLogger(__name__) @provides(ITaskPane) class TaskPane(MTaskPane): """ The toolkit-specific implementation of a TaskPane. See the ITaskPane interface for API documentation. """ # ------------------------------------------------------------------------ # 'ITaskPane' interface. # ------------------------------------------------------------------------ def create(self, parent): """ Create and set the toolkit-specific control that represents the pane. """ self.control = wx.Panel(parent) def destroy(self): """ Destroy the toolkit-specific control that represents the pane. """ if self.control is not None: logger.debug("Destroying %s" % self.control) self.task.window._aui_manager.DetachPane(self.control) self.control.Hide() self.control.Destroy() self.control = None def set_focus(self): """ Gives focus to the control that represents the pane. """ if self.control is not None: self.control.SetFocus() pyface-7.4.0/pyface/ui/wx/tasks/dock_pane.py0000644000076500000240000002052014176222673021707 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 from pyface.tasks.i_dock_pane import IDockPane, MDockPane from traits.api import ( Bool, observe, Property, provides, Tuple, Str, Int, ) import wx from pyface.wx.aui import aui from .task_pane import TaskPane # Constants. AREA_MAP = { "left": aui.AUI_DOCK_LEFT, "right": aui.AUI_DOCK_RIGHT, "top": aui.AUI_DOCK_TOP, "bottom": aui.AUI_DOCK_BOTTOM, } INVERSE_AREA_MAP = dict((int(v), k) for k, v in AREA_MAP.items()) # Logging logger = logging.getLogger(__name__) @provides(IDockPane) class DockPane(TaskPane, MDockPane): """ The toolkit-specific implementation of a DockPane. See the IDockPane interface for API documentation. """ # Keep a reference to the Aui pane name in order to update dock state pane_name = Str() # Whether the title bar of the pane is currently visible. caption_visible = Bool(True) # AUI ring number; note that panes won't be movable out of their ring # number. This is a way to isolate panes dock_layer = Int(0) # 'IDockPane' interface ------------------------------------------------ size = Property(Tuple) # Protected traits ----------------------------------------------------- _receiving = Bool(False) # ------------------------------------------------------------------------ # 'ITaskPane' interface. # ------------------------------------------------------------------------ @classmethod def get_hierarchy(cls, parent, indent=""): lines = ["%s%s %s" % (indent, str(parent), parent.GetName())] for child in parent.GetChildren(): lines.append(cls.get_hierarchy(child, indent + " ")) return "\n".join(lines) def create(self, parent): """ Create and set the dock widget that contains the pane contents. """ # wx doesn't need a wrapper control, so the contents become the control self.control = self.create_contents(parent) # hide the pane till the task gets activated, whereupon it will take # its visibility from the task state self.control.Hide() # Set the widget's object name. This important for AUI Manager state # saving. Use the task ID and the pane ID to avoid collisions when a # pane is present in multiple tasks attached to the same window. self.pane_name = self.task.id + ":" + self.id logger.debug( "dock_pane.create: %s HIERARCHY:\n%s" % (self.pane_name, self.get_hierarchy(parent, " ")) ) def get_new_info(self): info = aui.AuiPaneInfo().Name(self.pane_name).DestroyOnClose(False) # size? # Configure the dock widget according to the DockPane settings. self.update_dock_area(info) self.update_dock_features(info) self.update_dock_title(info) self.update_floating(info) self.update_visible(info) return info def add_to_manager(self, row=None, pos=None, tabify_pane=None): info = self.get_new_info() if tabify_pane is not None: target = tabify_pane.get_pane_info() logger.debug( "dock_pane.add_to_manager: Tabify! %s onto %s" % (self.pane_name, target.name) ) else: target = None if row is not None: info.Row(row) if pos is not None: info.Position(pos) self.task.window._aui_manager.AddPane( self.control, info, target=target ) def validate_traits_from_pane_info(self): """ Sync traits from the AUI pane info. Useful after perspective restore to make sure e.g. visibility state is set correctly. """ info = self.get_pane_info() self.visible = info.IsShown() def destroy(self): """ Destroy the toolkit-specific control that represents the contents. """ if self.control is not None: logger.debug("Destroying %s" % self.control) self.task.window._aui_manager.DetachPane(self.control) # Some containers (e.g. TraitsDockPane) will destroy the control # before we get here (e.g. traitsui.ui.UI.finish by way of # TraitsDockPane.destroy), so check to see if it's already been # destroyed. Fortunately, the Reparent in DetachPane still seems # to work on a destroyed control. if self.control: self.control.Hide() self.control.Destroy() self.control = None # ------------------------------------------------------------------------ # 'IDockPane' interface. # ------------------------------------------------------------------------ def create_contents(self, parent): """ Create and return the toolkit-specific contents of the dock pane. """ return wx.Window(parent, name=self.task.id + ":" + self.id) # Trait property getters/setters --------------------------------------- def _get_size(self): if self.control is not None: return self.control.GetSize().Get() return (-1, -1) # Trait change handlers ------------------------------------------------ def get_pane_info(self): info = self.task.window._aui_manager.GetPane(self.pane_name) return info def commit_layout(self, layout=True): if layout: self.task.window._aui_manager.Update() else: self.task.window._aui_manager.UpdateWithoutLayout() def commit_if_active(self, layout=True): # Only attempt to commit the AUI changes if the area if the task is active. main_window = self.task.window.control if main_window and self.task == self.task.window.active_task: self.commit_layout(layout) else: logger.debug("task not active so not committing...") def update_dock_area(self, info): info.Direction(AREA_MAP[self.dock_area]) logger.debug( "info: dock_area=%s dir=%s" % (self.dock_area, info.dock_direction) ) @observe("dock_area") def _set_dock_area(self, event): logger.debug("trait change: dock_area") if self.control is not None: info = self.get_pane_info() self.update_dock_area(info) self.commit_if_active() def update_dock_features(self, info): info.CloseButton(self.closable) info.Floatable(self.floatable) info.Movable(self.movable) info.CaptionVisible(self.caption_visible) info.Layer(self.dock_layer) @observe("closable,floatable,movable,caption_visible,dock_layer") def _set_dock_features(self, event): if self.control is not None: info = self.get_pane_info() self.update_dock_features(info) self.commit_if_active() def update_dock_title(self, info): info.Caption(self.name) @observe("name") def _set_dock_title(self, event): if self.control is not None: info = self.get_pane_info() self.update_dock_title(info) # Don't need to refresh everything if only the name is changing self.commit_if_active(False) def update_floating(self, info): if self.floating: info.Float() else: info.Dock() @observe("floating") def _set_floating(self, event): if self.control is not None: info = self.get_pane_info() self.update_floating(info) self.commit_if_active() def update_visible(self, info): if self.visible: info.Show() else: info.Hide() @observe("visible") def _set_visible(self, event): logger.debug( "_set_visible %s on pane=%s, control=%s" % (self.visible, self.pane_name, self.control) ) if self.control is not None: info = self.get_pane_info() self.update_visible(info) self.commit_if_active() pyface-7.4.0/pyface/ui/wx/tasks/main_window_layout.py0000644000076500000240000001567314176222673023711 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 from traits.api import HasTraits from .dock_pane import AREA_MAP, INVERSE_AREA_MAP from pyface.tasks.task_layout import ( PaneItem, Tabbed, Splitter, ) # row/col orientation for AUI ORIENTATION_NEEDS_NEW_ROW = { "horizontal": {"top": False, "bottom": False, "left": True, "right": True}, "vertical": {"top": True, "bottom": True, "left": False, "right": False}, } # Logging. logger = logging.getLogger(__name__) class MainWindowLayout(HasTraits): """ A class for applying declarative layouts to an AUI managed window. """ # ------------------------------------------------------------------------ # 'MainWindowLayout' interface. # ------------------------------------------------------------------------ def get_layout(self, layout, window): """ Get the layout by adding sublayouts to the specified DockLayout. """ logger.debug("get_layout: %s" % layout) layout.perspective = window._aui_manager.SavePerspective() logger.debug("get_layout: saving perspective %s" % layout.perspective) def set_layout(self, layout, window): """ Applies a DockLayout to the window. """ logger.debug("set_layout: %s" % layout) if hasattr(layout, "perspective"): self._set_layout_from_aui(layout, window) return # Perform the layout. This will assign fixed sizes to the dock widgets # to enforce size constraints specified in the PaneItems. for name, direction in AREA_MAP.items(): sublayout = getattr(layout, name) if sublayout: self.set_layout_for_area(sublayout, direction) self._add_dock_panes(window) def _add_dock_panes(self, window): # Add all panes not assigned an area by the TaskLayout. mgr = window._aui_manager for dock_pane in self.state.dock_panes: info = mgr.GetPane(dock_pane.pane_name) if not info.IsOk(): logger.debug( "_add_dock_panes: managing pane %s" % dock_pane.pane_name ) dock_pane.add_to_manager() else: logger.debug( "_add_dock_panes: arleady managed pane: %s" % dock_pane.pane_name ) def _set_layout_from_aui(self, layout, window): # The central pane will have already been added, but we need to add all # of the dock panes to the manager before the call to LoadPerspective logger.debug("_set_layout_from_aui: using saved perspective") self._add_dock_panes(window) logger.debug( "_set_layout_from_aui: restoring perspective %s" % layout.perspective ) window._aui_manager.LoadPerspective(layout.perspective) for dock_pane in self.state.dock_panes: logger.debug("validating dock pane traits for %s" % dock_pane.id) dock_pane.validate_traits_from_pane_info() def set_layout_for_area(self, layout, direction, row=None, pos=None): """ Applies a LayoutItem to the specified dock area. """ # AUI doesn't have full, arbitrary row/col positions, nor infinitely # splittable areas. Top and bottom docks are only splittable # vertically, and within each vertical split each can be split # horizontally and that's it. Similarly, left and right docks can # only be split horizontally and within each horizontal split can be # split vertically. logger.debug("set_layout_for_area: %s" % INVERSE_AREA_MAP[direction]) if isinstance(layout, PaneItem): dock_pane = self._get_dock_pane(layout) if dock_pane is None: raise MainWindowLayoutError("Unknown dock pane %r" % layout) dock_pane.dock_area = INVERSE_AREA_MAP[direction] logger.debug("layout size (%d,%d)" % (layout.width, layout.height)) dock_pane.add_to_manager(row=row, pos=pos) dock_pane.visible = True elif isinstance(layout, Tabbed): active_pane = first_pane = None for item in layout.items: dock_pane = self._get_dock_pane(item) dock_pane.dock_area = INVERSE_AREA_MAP[direction] if item.id == layout.active_tab: active_pane = dock_pane dock_pane.add_to_manager(tabify_pane=first_pane) if not first_pane: first_pane = dock_pane dock_pane.visible = True # Activate the appropriate tab, if possible. if not active_pane: # By default, AUI will activate the last widget. active_pane = first_pane if active_pane: mgr = active_pane.task.window._aui_manager info = active_pane.get_pane_info() mgr.ShowPane(info.window, True) elif isinstance(layout, Splitter): dock_area = INVERSE_AREA_MAP[direction] needs_new_row = ORIENTATION_NEEDS_NEW_ROW[layout.orientation][ dock_area ] if needs_new_row: if row is None: row = 0 else: row += 1 for i, item in enumerate(layout.items): self.set_layout_for_area(item, direction, row, pos) row += 1 else: pos = 0 for i, item in enumerate(layout.items): self.set_layout_for_area(item, direction, row, pos) pos += 1 else: raise MainWindowLayoutError("Unknown layout item %r" % layout) # ------------------------------------------------------------------------ # 'MainWindowLayout' abstract interface. # ------------------------------------------------------------------------ def _get_dock_widget(self, pane): """ Returns the QDockWidget associated with a PaneItem. """ raise NotImplementedError() def _get_pane(self, dock_widget): """ Returns a PaneItem for a QDockWidget. """ raise NotImplementedError() def _get_dock_pane(self, pane): """ Returns the DockPane associated with a PaneItem. """ for dock_pane in self.state.dock_panes: if dock_pane.id == pane.id: return dock_pane return None class MainWindowLayoutError(ValueError): """ Exception raised when a malformed LayoutItem is passed to the MainWindowLayout. """ pass pyface-7.4.0/pyface/ui/wx/tasks/editor.py0000644000076500000240000000326714176222673021263 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.tasks.i_editor import IEditor, MEditor from traits.api import Bool, Property, provides import wx @provides(IEditor) class Editor(MEditor): """ The toolkit-specific implementation of a Editor. See the IEditor interface for API documentation. """ # 'IEditor' interface -------------------------------------------------# has_focus = Property(Bool) # ------------------------------------------------------------------------ # 'IEditor' interface. # ------------------------------------------------------------------------ def create(self, parent): """ Create and set the toolkit-specific control that represents the pane. """ self.control = wx.Window(parent, name="Editor") def destroy(self): """ Destroy the toolkit-specific control that represents the pane. """ if self.control is not None: self.control.Destroy() self.control = None # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _get_has_focus(self): if self.control is not None: return self.control.FindFocus() == self.control return False pyface-7.4.0/pyface/ui/wx/confirmation_dialog.py0000644000076500000240000000767014176222673022661 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Enthought pyface package component """ import wx from traits.api import Bool, Enum, provides, Str from pyface.i_confirmation_dialog import ( IConfirmationDialog, MConfirmationDialog, ) from pyface.constant import CANCEL, YES, NO from pyface.ui_traits import Image from .dialog import Dialog from .image_resource import ImageResource @provides(IConfirmationDialog) class ConfirmationDialog(MConfirmationDialog, Dialog): """ The toolkit specific implementation of a ConfirmationDialog. See the IConfirmationDialog interface for the API documentation. """ # 'IConfirmationDialog' interface -------------------------------------# cancel = Bool(False) default = Enum(NO, YES, CANCEL) image = Image() message = Str() informative = Str() detail = Str() no_label = Str() yes_label = Str() # ------------------------------------------------------------------------ # Protected 'IDialog' interface. # ------------------------------------------------------------------------ def _create_buttons(self, parent): sizer = wx.StdDialogButtonSizer() # 'YES' button. if self.yes_label: label = self.yes_label else: label = "Yes" self._yes = yes = wx.Button(parent, wx.ID_YES, label) if self.default == YES: yes.SetDefault() parent.Bind(wx.EVT_BUTTON, self._on_yes, yes) sizer.AddButton(yes) # 'NO' button. if self.no_label: label = self.no_label else: label = "No" self._no = no = wx.Button(parent, wx.ID_NO, label) if self.default == NO: no.SetDefault() parent.Bind(wx.EVT_BUTTON, self._on_no, no) sizer.AddButton(no) if self.cancel: # 'Cancel' button. if self.no_label: label = self.cancel_label else: label = "Cancel" self._cancel = cancel = wx.Button(parent, wx.ID_CANCEL, label) if self.default == CANCEL: cancel.SetDefault() parent.Bind(wx.EVT_BUTTON, self._wx_on_cancel, cancel) sizer.AddButton(cancel) sizer.Realize() return sizer def _create_dialog_area(self, parent): panel = wx.Panel(parent, -1) sizer = wx.BoxSizer(wx.HORIZONTAL) panel.SetSizer(sizer) panel.SetAutoLayout(True) # The image. if self.image is None: image_rc = ImageResource("warning") else: image_rc = self.image image = wx.StaticBitmap(panel, -1, image_rc.create_bitmap()) sizer.Add(image, 0, wx.EXPAND | wx.ALL, 10) # The message. if self.informative: message = self.message + "\n\n" + self.informative else: message = self.message message = wx.StaticText(panel, -1, message) sizer.Add(message, 1, wx.EXPAND | wx.TOP, 15) # Resize the panel to match the sizer. sizer.Fit(panel) return panel # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ # wx event handlers ---------------------------------------------------- def _on_yes(self, event): """ Called when the 'Yes' button is pressed. """ self.control.EndModal(wx.ID_YES) def _on_no(self, event): """ Called when the 'No' button is pressed. """ self.control.EndModal(wx.ID_NO) pyface-7.4.0/pyface/ui/wx/color.py0000644000076500000240000000270014176222673017755 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Color conversion routines for the wx toolkit. This module provides a couple of utility methods to support the pyface.color.Color class to_toolkit and from_toolkit methods. """ import wx from pyface.color import channels_to_ints, ints_to_channels def toolkit_color_to_rgba(wx_colour): """ Convert a wx.Colour to an RGBA tuple. Parameters ---------- wx_color : wx.Colour A wx.Colour object. Returns ------- rgba : tuple A tuple of 4 floating point values between 0.0 and 1.0 inclusive. """ values = ( wx_colour.Red(), wx_colour.Green(), wx_colour.Blue(), wx_colour.Alpha(), ) return ints_to_channels(values) def rgba_to_toolkit_color(rgba): """ Convert an RGBA tuple to a wx.Colour. Parameters ---------- rgba : tuple A tuple of 4 floating point values between 0.0 and 1.0 inclusive. Returns ------- wx_color : wx.Colour A wx.Colour object. """ values = channels_to_ints(rgba) return wx.Colour(*values) pyface-7.4.0/pyface/ui/wx/single_choice_dialog.py0000644000076500000240000000554014176222673022756 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 dialog that allows the user to chose a single item from a list. """ import wx from traits.api import Any, Bool, List, Str, provides from pyface.i_single_choice_dialog import ( ISingleChoiceDialog, MSingleChoiceDialog, ) from .dialog import Dialog @provides(ISingleChoiceDialog) class SingleChoiceDialog(MSingleChoiceDialog, Dialog): """ A dialog that allows the user to chose a single item from a list. """ # 'ISingleChoiceDialog' interface -------------------------------------# #: Whether or not the dialog can be cancelled (Wx Only). cancel = Bool(True) #: List of objects to choose from. choices = List(Any) #: The object chosen, if any. choice = Any() #: An optional attribute to use for the name of each object in the dialog. name_attribute = Str() #: The message to display to the user. message = Str() # ------------------------------------------------------------------------ # Protected 'IDialog' interface. # ------------------------------------------------------------------------ def _create_contents(self, parent): """ Creates the window contents. """ # In this case, wx does it all for us in 'wx.SingleChoiceDialog' pass # ------------------------------------------------------------------------ # 'IWindow' interface. # ------------------------------------------------------------------------ def close(self): """ Closes the window. """ # Get the chosen object. if self.control is not None: self.choice = self.choices[self.control.GetSelection()] # Let the window close as normal. super().close() # ------------------------------------------------------------------------ # Protected 'IWidget' interface. # ------------------------------------------------------------------------ def _create_control(self, parent): """ Create the toolkit-specific control that represents the window. """ style = wx.DEFAULT_DIALOG_STYLE | wx.CLIP_CHILDREN | wx.OK if self.cancel: style |= wx.CANCEL if self.resizeable: style |= wx.RESIZE_BORDER dialog = wx.SingleChoiceDialog( parent, self.message, self.title, self._choice_strings(), style, self.position, ) if self.size != (-1, -1): dialog.SetSize(self.size) return dialog pyface-7.4.0/pyface/ui/wx/widget.py0000644000076500000240000000754014176222673020131 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Enthought pyface package component """ import wx from traits.api import Any, Bool, HasTraits, Instance, Str, provides from pyface.i_widget import IWidget, MWidget @provides(IWidget) class Widget(MWidget, HasTraits): """ The toolkit specific implementation of a Widget. See the IWidget interface for the API documentation. """ # 'IWidget' interface ---------------------------------------------------- #: The toolkit specific control that represents the widget. control = Any() #: The control's optional parent control. parent = Any() #: Whether or not the control is visible visible = Bool(True) #: Whether or not the control is enabled enabled = Bool(True) #: A tooltip for the widget. tooltip = Str() #: An optional context menu for the widget. context_menu = Instance("pyface.action.menu_manager.MenuManager") # ------------------------------------------------------------------------ # 'IWidget' interface. # ------------------------------------------------------------------------ def show(self, visible): """ Show or hide the widget. Parameter --------- visible : bool Visible should be ``True`` if the widget should be shown. """ self.visible = visible if self.control is not None: self.control.Show(visible) def enable(self, enabled): """ Enable or disable the widget. Parameter --------- enabled : bool The enabled state to set the widget to. """ self.enabled = enabled if self.control is not None: self.control.Enable(enabled) def focus(self): """ Set the keyboard focus to this widget. """ if self.control is not None: self.control.SetFocus() def has_focus(self): """ Does the widget currently have keyboard focus? Returns ------- focus_state : bool Whether or not the widget has keyboard focus. """ return ( self.control is not None and self.control.HasFocus() ) def destroy(self): if self.control is not None: control = self.control super().destroy() control.Destroy() # ------------------------------------------------------------------------ # Private interface # ------------------------------------------------------------------------ def _get_control_tooltip(self): """ Toolkit specific method to get the control's tooltip. """ return self.control.GetToolTipText() def _set_control_tooltip(self, tooltip): """ Toolkit specific method to set the control's tooltip. """ self.control.SetToolTip(tooltip) def _observe_control_context_menu(self, remove=False): """ Toolkit specific method to change the control menu observer. """ if remove: self.control.Unbind( wx.EVT_CONTEXT_MENU, handler=self._handle_control_context_menu ) else: self.control.Bind( wx.EVT_CONTEXT_MENU, self._handle_control_context_menu ) def _handle_control_context_menu(self, event): """ Signal handler for displaying context menu. """ if self.control is not None and self.context_menu is not None: menu = self.context_menu.create_menu(self.control) self.control.PopupMenu(menu) pyface-7.4.0/pyface/ui/wx/directory_dialog.py0000644000076500000240000000452614176222673022172 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Enthought pyface package component """ import wx from traits.api import Bool, provides, Str from pyface.i_directory_dialog import IDirectoryDialog, MDirectoryDialog from .dialog import Dialog @provides(IDirectoryDialog) class DirectoryDialog(MDirectoryDialog, Dialog): """ The toolkit specific implementation of a DirectoryDialog. See the IDirectoryDialog interface for the API documentation. """ # 'IDirectoryDialog' interface ----------------------------------------- default_path = Str() message = Str() new_directory = Bool(True) path = Str() # ------------------------------------------------------------------------ # Protected 'IDialog' interface. # ------------------------------------------------------------------------ def _create_contents(self, parent): # In wx this is a canned dialog. pass # ------------------------------------------------------------------------ # 'IWindow' interface. # ------------------------------------------------------------------------ def close(self): # Get the path of the chosen directory. self.path = str(self.control.GetPath()) # Let the window close as normal. super().close() # ------------------------------------------------------------------------ # Protected 'IWidget' interface. # ------------------------------------------------------------------------ def _create_control(self, parent): # The default style. style = wx.FD_OPEN # Create the wx style depending on which buttons are required etc. if self.new_directory: style = style | wx.DD_NEW_DIR_BUTTON if self.message: message = self.message else: message = "Choose a directory" # Create the actual dialog. return wx.DirDialog( parent, message=message, defaultPath=self.default_path, style=style ) pyface-7.4.0/pyface/ui/wx/message_dialog.py0000644000076500000240000000445514176222673021613 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Enthought pyface package component """ import wx from traits.api import Enum, provides, Str from pyface.i_message_dialog import IMessageDialog, MMessageDialog from .dialog import Dialog # Map the ETS severity to the corresponding wx standard icon. _SEVERITY_TO_ICON_MAP = { "information": wx.ICON_INFORMATION, "warning": wx.ICON_WARNING, "error": wx.ICON_ERROR, } @provides(IMessageDialog) class MessageDialog(MMessageDialog, Dialog): """ The toolkit specific implementation of a MessageDialog. See the IMessageDialog interface for the API documentation. """ # 'IMessageDialog' interface ------------------------------------------- message = Str() informative = Str() detail = Str() severity = Enum("information", "warning", "error") # unused trait, this functionality is only supported on Qt text_format = Enum("auto", "plain", "rich") # ------------------------------------------------------------------------ # Protected 'IDialog' interface. # ------------------------------------------------------------------------ def _create_contents(self, parent): # In wx this is a canned dialog. pass # ------------------------------------------------------------------------ # Protected 'IWidget' interface. # ------------------------------------------------------------------------ def _create_control(self, parent): # The message. if self.informative: message = self.message + "\n\n" + self.informative else: message = self.message style = _SEVERITY_TO_ICON_MAP[self.severity] | wx.OK | wx.STAY_ON_TOP if self.resizeable: style |= wx.RESIZE_BORDER dlg = wx.MessageDialog( parent, message, self.title, style, self.position ) if self.size != (-1, -1): dlg.SetSize(self.size) return dlg pyface-7.4.0/pyface/ui/wx/window.py0000644000076500000240000001520514176222673020152 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Enthought pyface package component """ import wx from traits.api import Event, Property, Tuple, Str, VetoableEvent, provides from pyface.i_window import IWindow, MWindow from pyface.key_pressed_event import KeyPressedEvent from .system_metrics import SystemMetrics from .widget import Widget @provides(IWindow) class Window(MWindow, Widget): """ The toolkit specific implementation of a Window. See the IWindow interface for the API documentation. """ # 'IWindow' interface ----------------------------------------------------- position = Property(Tuple) size = Property(Tuple) title = Str() # Window Events ---------------------------------------------------------- #: The window has been opened. opened = Event() #: The window is about to open. opening = VetoableEvent() #: The window has been activated. activated = Event() #: The window has been closed. closed = Event() #: The window is about to be closed. closing = VetoableEvent() #: The window has been deactivated. deactivated = Event() #: A key was pressed while the window had focus. # FIXME v3: This smells of a hack. What's so special about key presses? # FIXME v3: Str key_pressed = Event(KeyPressedEvent) size = Property(Tuple) # Private interface ------------------------------------------------------ # Shadow trait for position. _position = Tuple((-1, -1)) # Shadow trait for size. _size = Tuple((-1, -1)) # ------------------------------------------------------------------------- # 'IWindow' interface. # ------------------------------------------------------------------------- def activate(self): self.control.Iconize(False) self.control.Raise() def show(self, visible): self.control.Show(visible) # ------------------------------------------------------------------------- # Protected 'IWindow' interface. # ------------------------------------------------------------------------- def _add_event_listeners(self): super()._add_event_listeners() self.control.Bind(wx.EVT_ACTIVATE, self._wx_on_activate) self.control.Bind(wx.EVT_SHOW, self._wx_on_show) self.control.Bind(wx.EVT_CLOSE, self._wx_on_close) self.control.Bind(wx.EVT_SIZE, self._wx_on_control_size) self.control.Bind(wx.EVT_MOVE, self._wx_on_control_move) self.control.Bind(wx.EVT_CHAR, self._wx_on_char) # ------------------------------------------------------------------------- # Protected 'IWidget' interface. # ------------------------------------------------------------------------- def _create_control(self, parent): # create a basic window control style = ( wx.DEFAULT_FRAME_STYLE | wx.FRAME_NO_WINDOW_MENU | wx.CLIP_CHILDREN ) control = wx.Frame( parent, -1, self.title, style=style, size=self.size, pos=self.position, ) control.SetBackgroundColour(SystemMetrics().dialog_background_color) control.Enable(self.enabled) # XXX starting with self.visible true is generally a bad idea control.Show(self.visible) return control # ------------------------------------------------------------------------- # Private interface. # ------------------------------------------------------------------------- def _get_position(self): """ Property getter for position. """ return self._position def _set_position(self, position): """ Property setter for position. """ if self.control is not None: self.control.SetPosition(position) old = self._position self._position = position self.trait_property_changed("position", old, position) def _get_size(self): """ Property getter for size. """ return self._size def _set_size(self, size): """ Property setter for size. """ if self.control is not None: self.control.SetSize(size) old = self._size self._size = size self.trait_property_changed("size", old, size) def _title_changed(self, title): """ Static trait change handler. """ if self.control is not None: self.control.SetTitle(title) # wx event handlers ------------------------------------------------------ def _wx_on_activate(self, event): """ Called when the frame is being activated or deactivated. """ if event.GetActive(): self.activated = self else: self.deactivated = self event.Skip() def _wx_on_show(self, event): """ Called when the frame is being activated or deactivated. """ self.visible = event.IsShown() event.Skip() def _wx_on_close(self, event): """ Called when the frame is being closed. """ self.close() def _wx_on_control_move(self, event): """ Called when the window is resized. """ # Get the real position and set the trait without performing # notification. # WXBUG - From the API documentation you would think that you could # call event.GetPosition directly, but that would be wrong. The pixel # reported by that call is the pixel just below the window menu and # just right of the Windows-drawn border. try: self._position = ( event.GetEventObject().GetPosition().Get() ) # Sizer.GetPosition().Get() except: pass event.Skip() def _wx_on_control_size(self, event): """ Called when the window is resized. """ # Get the new size and set the shadow trait without performing # notification. wxsize = event.GetSize() self._size = (wxsize.GetWidth(), wxsize.GetHeight()) event.Skip() def _wx_on_char(self, event): """ Called when a key is pressed when the tree has focus. """ self.key_pressed = KeyPressedEvent( alt_down=event.altDown, control_down=event.controlDown, shift_down=event.shiftDown, key_code=event.KeyCode, event=event, ) event.Skip() pyface-7.4.0/pyface/ui/wx/image_resource.py0000644000076500000240000000564514176222673021643 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Enthought pyface package component """ import os import wx from traits.api import Any, HasTraits, List, Property, provides from traits.api import Str from pyface.i_image_resource import IImageResource, MImageResource @provides(IImageResource) class ImageResource(MImageResource, HasTraits): """ The toolkit specific implementation of an ImageResource. See the IImageResource interface for the API documentation. """ # Private interface ---------------------------------------------------- # The resource manager reference for the image. _ref = Any() # 'ImageResource' interface -------------------------------------------- absolute_path = Property(Str) name = Str() search_path = List() # ------------------------------------------------------------------------ # 'IImage' interface. # ------------------------------------------------------------------------ def create_bitmap(self, size=None): return self.create_image(size).ConvertToBitmap() def create_icon(self, size=None): ref = self._get_ref(size) if ref is not None: icon = wx.Icon(self.absolute_path, wx.BITMAP_TYPE_ANY) else: image = self._get_image_not_found_image() # We have to convert the image to a bitmap first and then create an # icon from that. bmp = image.ConvertToBitmap() icon = wx.Icon() icon.CopyFromBitmap(bmp) return icon def image_size(cls, image): """ Get the size of a toolkit image Parameters ---------- image : toolkit image A toolkit image to compute the size of. Returns ------- size : tuple The (width, height) tuple giving the size of the image. """ size = image.GetSize() return size.Get() # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _get_absolute_path(self): # FIXME: This doesn't quite wotk the new notion of image size. We # should find out who is actually using this trait, and for what! # (AboutDialog uses it to include the path name in some HTML.) ref = self._get_ref() if ref is not None: absolute_path = os.path.abspath(self._ref.filename) else: absolute_path = self._get_image_not_found().absolute_path return absolute_path pyface-7.4.0/pyface/ui/wx/multi_toolbar_window.py0000644000076500000240000001200614176222673023102 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 top-level application window that supports multiple toolbars. """ import wx from pyface.action.api import ToolBarManager from traits.api import Dict, Enum, Instance, List from .application_window import ApplicationWindow class MultiToolbarWindow(ApplicationWindow): """ A top-level application window that supports multiple toolbars. The multi-toolbar window has support for a menu bar, status bar, and multiple toolbars (all of which are optional). """ # The toolbars in the order they were added to the window. _tool_bar_managers = List(Instance(ToolBarManager)) # Map of toolbar to screen location. _tool_bar_locations = Dict( Instance(ToolBarManager), Enum("top", "bottom", "left", "right") ) # ------------------------------------------------------------------------ # Protected 'Window' interface. # ------------------------------------------------------------------------ def _create_contents(self, parent): panel = super()._create_contents(parent) self._create_trim_widgets(parent) return panel def _create_trim_widgets(self, parent): # The frame's icon. self._set_window_icon() # Add the (optional) menu bar. self._create_menu_bar(parent) # Add the (optional) status bar. self._create_status_bar(parent) # Add the (optional) tool bars. self.sizer = self._create_tool_bars(parent) def _create_tool_bars(self, parent): """ Create the tool bars for this window. """ if len(self._tool_bar_managers) > 0: # Create a top level sizer to handle to main layout and attach # it to the parent frame. self.main_sizer = sizer = wx.BoxSizer(wx.VERTICAL) parent.SetSizer(sizer) parent.SetAutoLayout(True) for tool_bar_manager in self._tool_bar_managers: location = self._tool_bar_locations[tool_bar_manager] sizer = self._create_tool_bar( parent, sizer, tool_bar_manager, location ) return sizer return None def _create_tool_bar(self, parent, sizer, tool_bar_manager, location): """ Create and add the toolbar to the parent window at the specified location. Returns the sizer where the remaining content should be added. For 'top' and 'left' toolbars, we can return the same sizer that contains the toolbar, because subsequent additions will be added below or to the right of those toolbars. For 'right' and 'bottom' toolbars, we create a spacer toolbar to hold the content. """ tool_bar = tool_bar_manager.create_tool_bar(parent) if location == "top": child_sizer = wx.BoxSizer(wx.VERTICAL) child_sizer.Add(tool_bar, 0, wx.ALL | wx.ALIGN_LEFT | wx.EXPAND) sizer.Add(child_sizer, 1, wx.ALL | wx.EXPAND) if location == "bottom": toolbar_sizer = wx.BoxSizer(wx.VERTICAL) # Add the placeholder for the content before adding the toolbar. child_sizer = self._create_content_spacer(toolbar_sizer) # Add the tool bar. toolbar_sizer.Add(tool_bar, 0, wx.ALL | wx.ALIGN_TOP | wx.EXPAND) sizer.Add(toolbar_sizer, 1, wx.ALL | wx.EXPAND) if location == "left": child_sizer = wx.BoxSizer(wx.HORIZONTAL) child_sizer.Add(tool_bar, 0, wx.ALL | wx.ALIGN_TOP | wx.EXPAND) sizer.Add(child_sizer, 1, wx.ALL | wx.EXPAND) if location == "right": toolbar_sizer = wx.BoxSizer(wx.HORIZONTAL) # Add the placeholder for the content before adding the toolbar. child_sizer = self._create_content_spacer(toolbar_sizer) # Add the tool bar. toolbar_sizer.Add(tool_bar, 0, wx.ALL | wx.ALIGN_TOP | wx.EXPAND) sizer.Add(toolbar_sizer, 1, wx.ALL | wx.EXPAND) return child_sizer def _create_content_spacer(self, sizer): spacer = wx.BoxSizer(wx.VERTICAL) sizer.Add(spacer, 1, wx.ALL | wx.EXPAND) return spacer # ------------------------------------------------------------------------ # Public MultiToolbarWindow interface # ------------------------------------------------------------------------ def add_tool_bar(self, tool_bar_manager, location="top"): """ Add a toolbar in the specified location. Valid locations are 'top', 'bottom', 'left', and 'right' """ self._tool_bar_managers.append(tool_bar_manager) self._tool_bar_locations[tool_bar_manager] = location pyface-7.4.0/pyface/ui/wx/list_box.py0000644000076500000240000001072214176222673020465 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 list box widget with a model-view architecture. """ import warnings import wx from traits.api import Event, Instance, Int from pyface.list_box_model import ListBoxModel from .layout_widget import LayoutWidget class ListBox(LayoutWidget): """ A simple list box widget with a model-view architecture. """ # The model that provides the data for the list box. model = Instance(ListBoxModel) # The objects currently selected in the list. selection = Int(-1) # Events. # An item has been activated. item_activated = Event() # Default style. STYLE = wx.LB_SINGLE | wx.LB_HSCROLL | wx.LB_NEEDED_SB def __init__(self, parent=None, **traits): """ Creates a new list box. """ create = traits.pop('create', True) # Base-class constructors. super().__init__(parent=parent, **traits) # Create the widget! if create: self.create() warnings.warn( "automatic widget creation is deprecated and will be removed " "in a future Pyface version, use create=False and explicitly " "call create() for future behaviour", PendingDeprecationWarning, ) def _create(self): super()._create() self._populate() # Listen for changes to the model. self.model.observe(self._on_model_changed, "list_changed") def dispose(self): self.model.observe( self._on_model_changed, "list_changed", remove=True ) self.model.dispose() # ------------------------------------------------------------------------ # 'ListBox' interface. # ------------------------------------------------------------------------ def refresh(self): """ Refreshes the list box. """ # For now we just clear out the entire list. self.control.Clear() # Populate the list. self._populate() # ------------------------------------------------------------------------ # wx event handlers. # ------------------------------------------------------------------------ def _on_item_selected(self, event): """ Called when an item in the list is selected. """ listbox = event.GetEventObject() self.selection = listbox.GetSelection() def _on_item_activated(self, event): """ Called when an item in the list is activated. """ listbox = event.GetEventObject() index = listbox.GetSelection() # Trait event notification. self.item_activated = index # ------------------------------------------------------------------------ # Trait handlers. # ------------------------------------------------------------------------ # Static --------------------------------------------------------------- def _selection_changed(self, index): """ Called when the selected item is changed. """ if index != -1: self.control.SetSelection(index) # Dynamic -------------------------------------------------------------# def _on_model_changed(self, event): """ Called when the model has changed. """ # For now we just clear out the entire list. self.refresh() # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _create_control(self, parent): """ Creates the widget. """ control = wx.ListBox(parent, -1, style=self.STYLE) # Wire it up! control.Bind( wx.EVT_LISTBOX, self._on_item_selected, id=self.control.GetId() ) control.Bind( wx.EVT_LISTBOX_DCLICK, self._on_item_activated, id=self.control.GetId(), ) # Populate the list. return control def _populate(self): """ Populates the list box. """ for index in range(self.model.get_item_count()): label, item = self.model.get_item_at(index) self.control.Append(label, item) pyface-7.4.0/pyface/ui/wx/util/0000755000076500000240000000000014176460551017242 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/wx/util/image_helpers.py0000644000076500000240000001066414176222673022430 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Helper functions for working with images This module provides helper functions for converting between numpy arrays and Qt wx.Images, as well as between the various image types in a standardized way. """ from enum import IntEnum import wx class ScaleMode(IntEnum): fast = wx.IMAGE_QUALITY_NORMAL smooth = wx.IMAGE_QUALITY_HIGH class AspectRatio(IntEnum): ignore = 0 keep_constrain = 1 keep_expand = 2 def image_to_bitmap(image): """ Convert a wx.Image to a wx.Bitmap. Parameters ---------- image : wx.Image The wx.Image to convert. Return ------ bitmap : wx.Bitmap The corresponding wx.Bitmap. """ return image.ConvertToBitmap() def bitmap_to_image(bitmap): """ Convert a wx.Bitmap to a wx.Image. Parameters ---------- bitmap : wx.Bitmap The wx.Bitmap to convert. Return ------ image : wx.Image The corresponding wx.Image. """ return bitmap.ConvertToImage() def bitmap_to_icon(bitmap): """ Convert a wx.Bitmap to a wx.Icon. Parameters ---------- bitmap : wx.Bitmap The wx.Bitmap to convert. Return ------ icon : wx.Icon The corresponding wx.Icon. """ return wx.Icon(bitmap) def resize_image(image, size, aspect_ratio=AspectRatio.ignore, mode=ScaleMode.fast): """ Resize a toolkit image to the given size. """ image_size = image.GetSize() width, height = _get_size_for_aspect_ratio(image_size, size, aspect_ratio) return image.Scale(width, height, mode) def resize_bitmap(bitmap, size, aspect_ratio=AspectRatio.ignore, mode=ScaleMode.fast): """ Resize a toolkit bitmap to the given size. """ image = bitmap_to_image(bitmap) image = resize_image(image, size, aspect_ratio, mode) return image_to_bitmap(image) def image_to_array(image): """ Convert a wx.Image to a numpy array. This copies the data returned from wx. Parameters ---------- image : wx.Image The wx.Image that we want to extract the values from. The format must be either RGB32 or ARGB32. Return ------ array : ndarray An N x M x 4 array of unsigned 8-bit ints as RGBA values. """ import numpy as np width, height = image.GetSize() rgb_data = np.array(image.GetData(), dtype='uint8').reshape(width, height, 3) if image.HasAlpha(): alpha = np.array(image.GetAlpha(), dtype='uint8').reshape(width, height) else: alpha = np.full((width, height), 0xff, dtype='uint8') array = np.empty(shape=(width, height, 4), dtype='uint8') array[:, :, :3] = rgb_data array[:, :, 3] = alpha return array def array_to_image(array): """ Convert a numpy array to a wx.Image. This copies the data before passing it to wx. Parameters ---------- array : ndarray An N x M x {3, 4} array of unsigned 8-bit ints. The image format is assumed to be RGB or RGBA, based on the shape. Return ------ image : wx.Image The wx.Image created from the data. """ if array.ndim != 3: raise ValueError("Array must be either RGB or RGBA values.") height, width, channels = array.shape if channels == 3: image = wx.Image(width, height, array.tobytes()) elif channels == 4: image = wx.Image( width, height, array[..., :3].tobytes(), array[..., 3].tobytes(), ) else: raise ValueError("Array must be either RGB or RGBA values.") return image def _get_size_for_aspect_ratio(image_size, size, aspect_ratio): width, height = size image_width, image_height = image_size if aspect_ratio != AspectRatio.ignore: if aspect_ratio == AspectRatio.keep_constrain: scale = min(width/image_width, height/image_height) else: scale = max(width/image_width, height/image_height) width = int(round(scale * image_width)) height = int(round(scale * image_height)) return (width, height) pyface-7.4.0/pyface/ui/wx/util/tests/0000755000076500000240000000000014176460551020404 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/wx/util/tests/__init__.py0000644000076500000240000000000014176222673022504 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/wx/util/tests/test_image_helpers.py0000644000076500000240000001550614176222673024631 0ustar cwebsterstaff00000000000000# Copyright (c) 2005-2022, Enthought Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only # under the conditions described 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 traits.testing.optional_dependencies import numpy as np, requires_numpy from ..image_helpers import ( bitmap_to_icon, bitmap_to_image, image_to_array, image_to_bitmap, array_to_image, AspectRatio, ScaleMode, resize_image, resize_bitmap, ) class TestImageHelpers(unittest.TestCase): def test_image_to_bitmap(self): wximage = wx.Image(32, 64) wximage.SetRGB(wx.Rect(0, 0, 32, 64), 0x44, 0x88, 0xcc) wxbitmap = image_to_bitmap(wximage) self.assertIsInstance(wxbitmap, wx.Bitmap) def test_bitmap_to_image(self): wximage = wx.Image(32, 64) wximage.SetRGB(wx.Rect(0, 0, 32, 64), 0x44, 0x88, 0xcc) wxbitmap = wximage.ConvertToBitmap() wximage = bitmap_to_image(wxbitmap) self.assertIsInstance(wximage, wx.Image) def test_bitmap_to_icon(self): wximage = wx.Image(32, 64) wximage.SetRGB(wx.Rect(0, 0, 32, 64), 0x44, 0x88, 0xcc) wxbitmap = wximage.ConvertToBitmap() wximage = bitmap_to_icon(wxbitmap) self.assertIsInstance(wximage, wx.Icon) def test_resize_image(self): wximage = wx.Image(32, 64) wximage.SetRGB(wx.Rect(0, 0, 32, 64), 0x44, 0x88, 0xcc) wximage = resize_image(wximage, (128, 128)) self.assertIsInstance(wximage, wx.Image) self.assertEqual(wximage.GetWidth(), 128) self.assertEqual(wximage.GetHeight(), 128) def test_resize_image_smooth(self): wximage = wx.Image(32, 64) wximage.SetRGB(wx.Rect(0, 0, 32, 64), 0x44, 0x88, 0xcc) wximage = resize_image(wximage, (128, 128), mode=ScaleMode.smooth) self.assertIsInstance(wximage, wx.Image) self.assertEqual(wximage.GetWidth(), 128) self.assertEqual(wximage.GetHeight(), 128) def test_resize_image_constrain(self): wximage = wx.Image(32, 64) wximage.SetRGB(wx.Rect(0, 0, 32, 64), 0x44, 0x88, 0xcc) wximage = resize_image(wximage, (128, 128), AspectRatio.keep_constrain) self.assertIsInstance(wximage, wx.Image) self.assertEqual(wximage.GetWidth(), 64) self.assertEqual(wximage.GetHeight(), 128) def test_resize_image_expand(self): wximage = wx.Image(32, 64) wximage.SetRGB(wx.Rect(0, 0, 32, 64), 0x44, 0x88, 0xcc) wximage = resize_image(wximage, (128, 128), AspectRatio.keep_expand) self.assertIsInstance(wximage, wx.Image) self.assertEqual(wximage.GetWidth(), 128) self.assertEqual(wximage.GetHeight(), 256) def test_resize_bitmap(self): wximage = wx.Image(32, 64) wximage.SetRGB(wx.Rect(0, 0, 32, 64), 0x44, 0x88, 0xcc) wxbitmap = wximage.ConvertToBitmap() wxbitmap = resize_bitmap(wxbitmap, (128, 128)) self.assertIsInstance(wxbitmap, wx.Bitmap) self.assertEqual(wxbitmap.GetWidth(), 128) self.assertEqual(wxbitmap.GetHeight(), 128) def test_resize_bitmap_smooth(self): wximage = wx.Image(32, 64) wximage.SetRGB(wx.Rect(0, 0, 32, 64), 0x44, 0x88, 0xcc) wxbitmap = wximage.ConvertToBitmap() wxbitmap = resize_bitmap(wxbitmap, (128, 128), mode=ScaleMode.smooth) self.assertIsInstance(wxbitmap, wx.Bitmap) self.assertEqual(wxbitmap.GetWidth(), 128) self.assertEqual(wxbitmap.GetHeight(), 128) def test_resize_bitmap_constrain(self): wximage = wx.Image(32, 64) wximage.SetRGB(wx.Rect(0, 0, 32, 64), 0x44, 0x88, 0xcc) wxbitmap = wximage.ConvertToBitmap() wxbitmap = resize_bitmap(wxbitmap, (128, 128), AspectRatio.keep_constrain) self.assertIsInstance(wxbitmap, wx.Bitmap) self.assertEqual(wxbitmap.GetWidth(), 64) self.assertEqual(wxbitmap.GetHeight(), 128) def test_resize_bitmap_expand(self): wximage = wx.Image(32, 64) wximage.SetRGB(wx.Rect(0, 0, 32, 64), 0x44, 0x88, 0xcc) wxbitmap = wximage.ConvertToBitmap() wxbitmap = resize_bitmap(wxbitmap, (128, 128), AspectRatio.keep_expand) self.assertIsInstance(wxbitmap, wx.Bitmap) self.assertEqual(wxbitmap.GetWidth(), 128) self.assertEqual(wxbitmap.GetHeight(), 256) @requires_numpy class TestArrayImageHelpers(unittest.TestCase): def test_image_to_array_rgb(self): wximage = wx.Image(32, 64) wximage.SetRGB(wx.Rect(0, 0, 32, 64), 0x44, 0x88, 0xcc) array = image_to_array(wximage) self.assertEqual(array.shape, (32, 64, 4)) self.assertEqual(array.dtype, np.dtype('uint8')) self.assertTrue(np.all(array[:, :, 3] == 0xff)) self.assertTrue(np.all(array[:, :, 0] == 0x44)) self.assertTrue(np.all(array[:, :, 1] == 0x88)) self.assertTrue(np.all(array[:, :, 2] == 0xcc)) def test_image_to_array_rgba(self): wximage = wx.Image(32, 64) wximage.SetRGB(wx.Rect(0, 0, 32, 64), 0x44, 0x88, 0xcc) wximage.InitAlpha() wximage.SetAlpha(np.full((32*64,), 0xee, dtype='uint8')) array = image_to_array(wximage) self.assertEqual(array.shape, (32, 64, 4)) self.assertEqual(array.dtype, np.dtype('uint8')) self.assertTrue(np.all(array[:, :, 0] == 0x44)) self.assertTrue(np.all(array[:, :, 1] == 0x88)) self.assertTrue(np.all(array[:, :, 2] == 0xcc)) self.assertTrue(np.all(array[:, :, 3] == 0xee)) def test_array_to_image_rgb(self): array = np.empty((64, 32, 3), dtype='uint8') array[:, :, 0] = 0x44 array[:, :, 1] = 0x88 array[:, :, 2] = 0xcc wximage = array_to_image(array) self.assertEqual(wximage.GetWidth(), 32) self.assertEqual(wximage.GetHeight(), 64) self.assertFalse(wximage.HasAlpha()) def test_array_to_image_rgba(self): array = np.empty((64, 32, 4), dtype='uint8') array[:, :, 0] = 0x44 array[:, :, 1] = 0x88 array[:, :, 2] = 0xcc array[:, :, 3] = 0xee wximage = array_to_image(array) self.assertEqual(wximage.GetWidth(), 32) self.assertEqual(wximage.GetHeight(), 64) self.assertTrue(wximage.HasAlpha()) def test_array_to_image_bad_channels(self): array = np.empty((64, 32, 2), dtype='uint8') array[:, :, 0] = 0x44 array[:, :, 1] = 0x88 with self.assertRaises(ValueError): array_to_image(array) def test_array_to_image_bad_ndim(self): array = np.full((64, 32), 0x44, dtype='uint8') with self.assertRaises(ValueError): array_to_image(array) pyface-7.4.0/pyface/ui/wx/util/__init__.py0000644000076500000240000000000014176222673021342 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/wx/system_metrics.py0000644000076500000240000000325114176222673021713 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Enthought pyface package component """ import sys import wx from traits.api import HasTraits, Int, Property, provides, Tuple from pyface.i_system_metrics import ISystemMetrics, MSystemMetrics @provides(ISystemMetrics) class SystemMetrics(MSystemMetrics, HasTraits): """ The toolkit specific implementation of a SystemMetrics. See the ISystemMetrics interface for the API documentation. """ # 'ISystemMetrics' interface ------------------------------------------- screen_width = Property(Int) screen_height = Property(Int) dialog_background_color = Property(Tuple) # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _get_screen_width(self): return wx.SystemSettings.GetMetric(wx.SYS_SCREEN_X) def _get_screen_height(self): return wx.SystemSettings.GetMetric(wx.SYS_SCREEN_Y) def _get_dialog_background_color(self): if sys.platform == "darwin": # wx lies. color = wx.Colour(232, 232, 232) else: color = wx.SystemSettings.GetColour(wx.SYS_COLOUR_MENUBAR).Get() return (color[0] / 255.0, color[1] / 255.0, color[2] / 255.0) pyface-7.4.0/pyface/ui/wx/dialog.py0000644000076500000240000001165314176222673020105 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Enthought pyface package component """ import sys import wx from traits.api import Bool, Enum, Int, provides, Str from pyface.i_dialog import IDialog, MDialog from pyface.constant import OK, CANCEL, YES, NO from .window import Window # Map wx dialog related constants to the pyface equivalents. _RESULT_MAP = { wx.ID_OK: OK, wx.ID_CANCEL: CANCEL, wx.ID_YES: YES, wx.ID_NO: NO, wx.ID_CLOSE: CANCEL, # There seems to be a bug in wx.SingleChoiceDialog that allows it to return # 0 when it is closed via the window (closing it via the buttons works just # fine). 0: CANCEL, } @provides(IDialog) class Dialog(MDialog, Window): """ The toolkit specific implementation of a Dialog. See the IDialog interface for the API documentation. """ # 'IDialog' interface -------------------------------------------------# cancel_label = Str() help_id = Str() help_label = Str() ok_label = Str() resizeable = Bool(True) return_code = Int(OK) style = Enum("modal", "nonmodal") # 'IWindow' interface -------------------------------------------------# title = Str("Dialog") # ------------------------------------------------------------------------ # Protected 'IDialog' interface. # ------------------------------------------------------------------------ def _create_buttons(self, parent): sizer = wx.StdDialogButtonSizer() # The 'OK' button. if self.ok_label: label = self.ok_label else: label = "OK" self._wx_ok = ok = wx.Button(parent, wx.ID_OK, label) ok.SetDefault() parent.Bind(wx.EVT_BUTTON, self._wx_on_ok, id=wx.ID_OK) sizer.AddButton(ok) # The 'Cancel' button. if self.cancel_label: label = self.cancel_label else: label = "Cancel" self._wx_cancel = cancel = wx.Button(parent, wx.ID_CANCEL, label) parent.Bind(wx.EVT_BUTTON, self._wx_on_cancel, id=wx.ID_CANCEL) sizer.AddButton(cancel) # The 'Help' button. if len(self.help_id) > 0: if self.help_label: label = self.help_label else: label = "Help" help = wx.Button(parent, wx.ID_HELP, label) parent.Bind(wx.EVT_BUTTON, self._wx_on_help, id=wx.ID_HELP) sizer.AddButton(help) sizer.Realize() return sizer def _create_contents(self, parent): sizer = wx.BoxSizer(wx.VERTICAL) parent.SetSizer(sizer) parent.SetAutoLayout(True) # The 'guts' of the dialog. dialog_area = self._create_dialog_area(parent) sizer.Add(dialog_area, 1, wx.EXPAND | wx.ALL, 5) # The buttons. buttons = self._create_buttons(parent) sizer.Add(buttons, 0, wx.ALIGN_RIGHT | wx.ALL, 5) # Resize the dialog to match the sizer's minimal size. if self.size != (-1, -1): parent.SetSize(self.size) else: sizer.Fit(parent) parent.CentreOnParent() def _create_dialog_area(self, parent): panel = wx.Panel(parent, -1) panel.SetBackgroundColour("red") panel.SetSize((100, 200)) return panel def _show_modal(self): if sys.platform == "darwin": # Calling Show(False) is needed on the Mac for the modal dialog # to show up at all. self.control.Show(False) return _RESULT_MAP[self.control.ShowModal()] # ------------------------------------------------------------------------ # Protected 'IWidget' interface. # ------------------------------------------------------------------------ def _create_control(self, parent): style = wx.DEFAULT_DIALOG_STYLE | wx.CLIP_CHILDREN if self.resizeable: style |= wx.RESIZE_BORDER return wx.Dialog( parent, -1, self.title, self.position, self.size, style ) # wx event handlers ---------------------------------------------------- def _wx_on_ok(self, event): """ Called when the 'OK' button is pressed. """ self.return_code = OK # Let the default handler close the dialog appropriately. event.Skip() def _wx_on_cancel(self, event): """ Called when the 'Cancel' button is pressed. """ self.return_code = CANCEL # Let the default handler close the dialog appropriately. event.Skip() def _wx_on_help(self, event): """ Called when the 'Help' button is pressed. """ pass pyface-7.4.0/pyface/ui/wx/images/0000755000076500000240000000000014176460551017532 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/wx/images/heading_level_1.png0000644000076500000240000000424014176222673023247 0ustar cwebsterstaff00000000000000PNG  IHDRv;lgAMA7tEXtSoftwareAdobe ImageReadyqe<2IDATxb?(%@u0/# b\ t0VyY:@'ssHa50nV)n$ٽx***\0,28 a fП:|ۜUtBxd|,d` 2GOV;CMv:cPL'@Pz#; FӚWķ3u*;)DRsnBK-!JyLRnb*EWj4I_} @E0ś&+a: 8"'7nxywV81xK0 [RtNmu4nz27ŋsh³X<xT3yGE_2`?썶vmKAeUi +?3\. -XGiSY#),.[~S Òc9K_<ڶȕ-e@PYE)'Ժ (i;*pм{ ==*0 |~YwOCM6mhkc0Zdqr8#_<'_ \T0@]k|s 21G/R E-9X#XuϖG+hRgjI躧Q`;3\q%`)#\0`r;_LLV}' /T6>26p lːbwγ]kxK)R/DA+3rx:Kޏ:9za'0lB>jh;UbgV)'"0$g JYR(B~.^6.MkM1[0Bvgpك 6Tכ`.Kne"+]i`5!`c=›]1]iK5 IwA#7 ˞Bm W߻0K#f>?0B3@)fL4X:$R|NnD;Ptehe?rBgGWݽHĺ&A&yqD/ҭPLN; Gb P K@#:\[E nPZFH DBP[VKP;0!+$G#<  0~ep}GG%F*X'RDeꀘzQd{ L@@ \%0U?j:EEfp q8wG‘WS2 t`(Yr"6e.7 ͡buDQ|?u/*Uz,q#s͜[?F )}..pJr@be?Ф g!IhY+ iFHox^2BZ?5m\AAQt:#z[`YYIcaRv| (a`XaM lkDn7'57L0.ТIDrT2ɡ`q)IENDB`pyface-7.4.0/pyface/ui/wx/images/warning.png0000644000076500000240000000257714176222673021721 0ustar cwebsterstaff00000000000000PNG  IHDR DsRGBPLTE      )$1) ;->4B3C7@7K:E<TA8WI[H9 SK: ; < ;==<= AeP@ D BCC D F JHFH GHF r[KJK I NJONTOM|cQO P SzgRfTSPRQRSST ^TU YZo*q[s\u r blyx3vpu::tu{}};| Ȃ} Ņ}ƆɈ ; ʉ ˊ͋ȍބ"߅# 3 ГŚ՗ߕӛ͡ߟ̥֤)ש$ܩ/ӱ2߬2ҮG3ݳ546չ ж` Ѹi..0&   "b$&22'34**? @7ABJ90MEUNVOHQY`ASbiT\refmn<{AtRNS@fbKGDH pHYs{tIME "(ZMIDAT8c` xzj#<{3 B߻=o^^gfKA=KLZKM6FFƈyf.{T%O@1gb5`@ 7d`Ssa*v}N֥cjǾ\@w^wժu */^ܷ*]O**ټsJh׮*_nݪUJ.Jo$Tмiڵkȣ0$i(PA'vm<`m iYrL[ ޶`m %kd oۻwoM(ڻ [ ޽ S+ 7&CUÒ:ЀM`C|<"}Uld)w؀C1| hD0܀m?9p+d0PGMd?MM£ O/{eHcT;u(p5}>v 55Uujl*CB*S_&- ae+NvGLH0pe6`PzxaTZ;{)ߝ:) @.%dTԉY`! =aR)e P'{;,&%VW}~7 ,ſ8 L5Ndґ g5+l U h0:v6xN:dj%ΰg5_M{1jæ"CzZýQ"yF\AiHr `(_Ĕx-9!ױH} T @OLϚ@'`uD6F&4EVŰ*=!}[㴂2|ZHj9~,`x,̻!Mwj̋:׾6^ qp؁S~ղ)!-A9MU%qbdo4ak0b+Q#zgn>#YF <*1G-).&2aMnp@ЎriKJQZ!Bɐ!p_Gт>. 99ՂYhG.aϸR+ ؼ4Wdkpe:~LaisD?ԝ]v0eϭNW^ Ci@ :~tʰs7ז[.Akr~ÐF|zHOY}| u Lg1:amMLv&/Xjg~^ rBogS~^ɧ-·ᚣ /[N֌W2 avF].J/ß}Z!d Ŋ6nRխSړ^֋5X\"TNbYUgjL.Do0@ex\CyB5u ၓ'n7ARC~"ݘM ӁoDGb*Ck QL!(Z\4/S?l\l@R@)PLЋ`: &Q@0zڱCOZIENDB`pyface-7.4.0/pyface/ui/wx/images/carat_closed.png0000644000076500000240000000034314176222673022664 0ustar cwebsterstaff00000000000000PNG  IHDR gAMA7tEXtSoftwareAdobe ImageReadyqe<uIDATxb? ``-X\ss8yBBQiy( "vs+ \\=QL 7V` P| @ p}0EȾIENDB`pyface-7.4.0/pyface/ui/wx/images/panel_gradient_over.png0000644000076500000240000000405414176222673024253 0ustar cwebsterstaff00000000000000PNG  IHDRbgAMA7tEXtSoftwareAdobe ImageReadyqe<IDATxl 0 ϰF` jӦJiΎ`H`V=i!q|f5*cc;us企m-_N.-2ah5H M=)&YB41т׮V} ȸa4 o:XB(X2Ѫ{K Ök'Wj;o$aJx2EgS`ʊ\|7mHS]3sx3DYH!Q&JyytѣcEr8vgO zЭjb6$eHZ*Dy`!HH#G} ( )gLHAn-\7%h vUHH$8r$)*g#X.\"j2~-{W:l؁Z34qDVTm!uDna"R:(>q=[,+e,+mU:TySPQFK،ݥYٵMFvH(S1ρD .(\82~Yt+3ew@ѧl/ǡ$onգG, `J i݂1<ԹB ) úk24ghl7;~ C7TP&M }$J<'aHf!*師8`~ K 19MkW*h1aVcj,&F2֚i Af]u6`Som _On 5Ltjl<}cCߧڜtwENfҶӰ  b*NkKa%V̥AO2_Ex.X,|:,5ڮ9 c 8Wx$zTmS!S\]В-V0h[$Dۋi43^c.v [#^afݮ=P lnǶKMZq8q\ B>[@*&#C ] $ud @!Ce5ؙE>=XF7et{<6=./>aDx֕ZGKaF=Z/#qT$FϮ&" rbPGn^!ՂALCa_4&BΆbbDo-IENDB`pyface-7.4.0/pyface/ui/wx/images/application.ico0000644000076500000240000006410614176222673022541 0ustar cwebsterstaff00000000000000h hV     N#00300 %B( @uF{L^I|y{RByV~~Kzzz}}}[քRIzyyhanU^X/['c=O4`2rd!C .9 8Tt,(D+Fg6_jWP];lQ IV7p\S"<&-NH5K3Tqm 1M0k$GEfuAL@?J :%e)BZ#o> pRisjb*Y(  @~~~SvvvyxxtstjjjrrryyytstlllmmmN|y{uuuOU~HyzpwDvUڂqqqGW}IiLJJ{I|\{>u[քlll𐌏RKIByk{F{J/PhɷgaHMOQ-ܺ{qx&mt} L`FS\of *3<@y!Wb. I %#vDd+19u^ Ӡ'T?996pʀRs~~4XYKZ[K??(0 ` |||_yyyvvvvvvuuummm49zzzxxxuuussstttZggg lyyy{{{wwwjjjlll}}}{{{qqqkkkuuuduuuhhhkkkvvv^ǀNyhhhP{`ʃlllxxxs"iBx=up[zdƅuos]n=u6nukkknnnp|HI|J{|@v8ondkHxBu6nuuulllͨXЁOKcÃTyI|DxYтwouVwFzAuV~mmmﵵ~szUSOnPNJ}Fy~x}^xH|FyAvppp\xmtYVRqSSNK~{a{MJ}Fzsssξ~[ԃZUlˌUzXR^؇w}`ÀPLXρuuuQw^]Z[ny\T{pxZTNtxxx#Sm]WXwkЍtZWb|}}}!Qo[W|]݈}|}`Zak~XXXx}u񙙙qlBFi??( @6mZPWp}_jʊW}dxmtOvOYtnh_emtVԀjݏwFvbAxTbu_kb]ވPZTklkRHOblw~yyyb{mwZ\ZfrM}[yW\U}@sxG}ypemNzMWzlxI|uuuJt\idٌr[؄Z~t{h`\^Dwooo|uql}}}]qdYZvx;qaqzmbiR_}il»Q>uiiiJX~[ӃXsor`ىWNTR{\yY݆d}m\Dy\uX؁~UNH|}y|K~VrjbglikNxWFymmmwww]AvM|V~\RX8nY=rOCwUMHw{{{Gz|ty_܈hۏVL?tS[Q|w{l}sYu^mdj\>vD{IAwI~WQ`lz[Մdy\YO-M--M]Md-M--M]]ctdMz77cM(t77n4}Gzonno(ctcܸ/=2ڣt 6e%ګcfSb wqN~G(SA\䖸":\?v4gMd{L=K֏D?tC\?u] ŜpA\l tؓ\\?4tgc}(r耔L 5L\\lcgd+r>nFˁLLy$t*MиsXrE>FEŜL_tdߊ3iʗrHxŜޡ&v~=r5m}y|d}mDyCv@s7nl}}}jjj֌Up}JK~I|GzU}NzCw@tHwpemNxExCv@s6mpppkkkIxmtX؁OMK~G|nqzG}GzDw;q|w{\uH|ExCv?sR{|||nnnլjlkSROMH~~YuNK~H{AvjʊfrI~H{ExCv;qpppblVTRPIVzQNK~G{[Ӄl}sLJ}H{Fy>uppp_}iYVTRMWzUQNJ~[ՄmtNMK~H{AxrrrռZ\Z[YWTOVrXURLiɉlxQOMK~D{uuuZPW]ވ[YWTx`l\YUOhSRPMVԀuuuUW}d_[YVdٌY݆\YX~t{`ىWURMu~~~zzzRWNT[y_\YT]qd^Zw~\ZWTbz}}}jjj_RHOYt_\Yhۏ\ixyjݏ_]Zb{xmtZjbgPZTZv[؄W|ra_܈\y_kb|ty {mdjjagsor`\^\RXh_e|zzzqqq1񓓓- Dhqd= AfqfA (0` 4j[QWr|bjʊUexnuPuOYtng^dmtTjݏwGu`ĂAxT`r_kb^ߊN_TjnlMDIblzyyyb}pzw][\itM|Ww[]V}?svG}xpinO{M[}jvI|vtuJtXjdٌk}\؅Yu|f}bY_Cvpno|tqm}}}ZrbY]wy;r_t}laiR_}in»R>thihJ]]͂Ysor`ىXNTR{_{Y߆e}m]Dz`vX؁ULG|}y|LVrjbgkjkLxVFyolnxvw_AvM}Ỳ^V[9nX=rPDxTOHw{{{Fy{tx_܈k׎UM?tS[Q|w{l}sYu_mek\?wD{KAxJXQ`lz[Մdy\YO7gMM]Mg*gMM]M-cɬM--(]-M--M(]t-td*c-(z-7dc-(g77tttt7}-zMtt޽b-zg77on4P}ڽdz]b~~Obg]c77fP7}yl?P]Mcbܞf4 \??egڡ*D????8SK4q\???}bzc7PPd6\\????ctPPZlo(\\???}7]򔔧B\\?.^p??uc8\\???=c*ULLL\\y"BD\???*A\\??? d-M}GŜLLD^GHAL\\ t6L\\???tc(HŜLLKSFLL\?nߡC8L\\??:ɡdtCŜL@LL\ydMLL\\?/d-ɞrŜzxLLy:5LL\\g-ɭ[rGhrLL ɮ5ҜLL\\oɮ-߭GrʭhrL tɮHŜLL÷t-ڣrbI3XrA$ttHŜLLttg [jirיko [;irtZrŜLf 7tcthiirOJBFirrWV77rrŜA77-9)|iir Phi0 |rr^7M7J[ w>t;r?sS}wnnnjjjvvv Іj=r;pq=p;oXw~]zfˇ~laiPuDwCvAt@s>q8nLxjjjsssg M|EyDwCvBu@s?r=qe}:q7mlNJwuwhrlEyDwCvAt@s>q;oEtwwwjjj{{{v~V~H|GzFyExCvBu>sb}s}?wAt?r7lmekUvFyDwCvAt@s>q:oV}mmmrrr| 뚚y}k}JJ}I|H{FyExCv?txO{FyCvAt>rBtxvxf~nG|FyExCvAt@s>q7myyyooo>>>MumsRMLK~I|H{FyExO{huH}H{FyDwBu:onckM{H|FyExCvBu@s=qHvlllppp8jvOOMLK~I|H{EydX|LJ}H{FyDwAuS~pinXxI|H{FyExCvBu@s8oqqqnnn}pin`{RPOMLK~I|F{ttiqRNLJ}H{FyDw=sxsw`vKI|H{FyExCvBu=rpxxxoooܡg]c[́TRQOMLK~F|fhgRPNLJ}H{Fy?u}z|gtKK~J}H{FyExDwAu]ȁ}}}ooo榦bX_Z݅UTRQONLH~_sfURPNLJ}H{Ax~jtMLK~J}H{GzExBvT}qqq榦]TZ[݆WUTRQONI[obWUSPNLJ}DzlvONMK~J}H{GzDxU~rrrץZRW\˂YWVTRQOK]]\YWUSPNLF}~kxPPNMK~J}H{Fz_ƂsssaZ_]wZYWVTSQLyeZa[YWUSQNJ|w{g}RRPNMK~J}H|nuuu|y{Zuc]ZYWVTSPlNJzZ|]ZWUSQ]؆yrwbÂUSRPNMK~F}yyyvvv}BQHN^\ZYWVTSYRr]_\ZWUPzynv\WUSRPOMUրwwwvvv8㧧a\_Xf_\ZYWVTOf\b]ׄ^\ZX[t}YXWUSRPLtyyy~LAH\ȁ_\[YXVSoʍN_Ta^\X||syhʉ\ZYWUTQ`āw~||||}~NGK^ߊ_\[YXVS~Rcb]oz_]\ZYWT[لxsv}}}ꈈptqrMIL\̂_\[YXUjԍwntSinu|kُa_^\ZY^΂qln~~~dK@GXj^][YXTqmp|wdba_^]bui`fܛ]X\NHLWh[}[ZXW}|||||zeedb^Ā]m]WZpjnӏ#ڙxtw]V[TJPXNT[PW_V[kijQNPC9?E;BI?EODK_W\{vyؕÕy񗗗𔔔sZZZᔔ|铓薖 NÔ㒒쑑ޏ>FᒒᐐE??pyface-7.4.0/pyface/ui/wx/images/carat_open.png0000644000076500000240000000035214176222673022354 0ustar cwebsterstaff00000000000000PNG  IHDR gAMA7tEXtSoftwareAdobe ImageReadyqe<|IDATxb?!@L Dbpva-T*@ b9yP @V(@arW.`iyEyG Fb 1tĞIENDB`pyface-7.4.0/pyface/ui/wx/heading_text.py0000644000076500000240000000364014176222673021306 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Enthought pyface package component """ import wx from traits.api import provides from pyface.i_heading_text import IHeadingText, MHeadingText from pyface.ui_traits import Image from .image_resource import ImageResource from .layout_widget import LayoutWidget @provides(IHeadingText) class HeadingText(MHeadingText, LayoutWidget): """ The Wx-specific implementation of a HeadingText. """ # 'IHeadingText' interface --------------------------------------------- #: Background image. This is deprecated and no-longer used. image = Image(ImageResource("heading_level_1")) # ------------------------------------------------------------------------ # 'IWidget' interface. # ------------------------------------------------------------------------ def _create_control(self, parent): """ Create the toolkit-specific control that represents the widget. """ control = wx.StaticText(parent) return control # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _set_control_text(self, text): """ Set the text on the toolkit specific widget. """ # Bold the text. Wx supports a limited subset of HTML for rich text. text = f"{text}" self.control.SetLabelMarkup(text) def _get_control_text(self): """ Get the text on the toolkit specific widget. """ return self.control.GetLabelText() pyface-7.4.0/pyface/ui/wx/pil_image.py0000644000076500000240000000324314176222673020570 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 provides from pyface.i_pil_image import IPILImage, MPILImage from pyface.ui.wx.util.image_helpers import resize_image @provides(IPILImage) class PILImage(MPILImage): """ The toolkit specific implementation of a PILImage. """ # ------------------------------------------------------------------------ # 'IImage' interface. # ------------------------------------------------------------------------ def create_image(self, size=None): """ Creates a Wx image for this image. Parameters ---------- size : (int, int) or None The desired size as a (width, height) tuple, or None if wanting default image size. Returns ------- image : wx.Image The toolkit image corresponding to the image and the specified size. """ image = self.image wx_image = wx.EmptyImage(self.image.size[0], self.image.size[1]) wx_image.SetData(image.convert("RGB").tobytes()) if image.mode == "RGBA": wx_image.InitAlpha() wx_image.SetAlpha(image.getchannel("A").tobytes()) if size is not None: return resize_image(wx_image, size) else: return wx_image pyface-7.4.0/pyface/ui/wx/timer/0000755000076500000240000000000014176460551017405 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/wx/timer/timer.py0000644000076500000240000000214714176222673021104 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 `wx.Timer` subclass that invokes a specified callback periodically. """ import wx from traits.api import Instance from pyface.timer.i_timer import BaseTimer class CallbackTimer(wx.Timer): def __init__(self, timer): super().__init__() self.timer = timer def Notify(self): self.timer.perform() wx.GetApp().Yield(True) class PyfaceTimer(BaseTimer): """ Abstract base class for Wx toolkit timers. """ #: The wx.Timer for the PyfaceTimer. _timer = Instance(wx.Timer) def _start(self): self._timer.Start(int(self.interval * 1000)) def _stop(self): self._timer.Stop() def __timer_default(self): return CallbackTimer(self) pyface-7.4.0/pyface/ui/wx/timer/__init__.py0000644000076500000240000000062714176222673021524 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! pyface-7.4.0/pyface/ui/wx/timer/do_later.py0000644000076500000240000000102614176222673021550 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ DoLaterTimer class Provided for backward compatibility. """ from pyface.timer.do_later import DoLaterTimer # noqa: F401 pyface-7.4.0/pyface/ui/wx/tests/0000755000076500000240000000000014176460551017427 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/wx/tests/__init__.py0000644000076500000240000000000014176222673021527 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/wx/tests/bad_import.py0000644000076500000240000000106714176222673022126 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 used to test what happens when there is an unrelated import error # when importing a toolkit object raise ImportError("No module named nonexistent") pyface-7.4.0/pyface/ui/wx/font_dialog.py0000644000076500000240000000405114176222673021125 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 interface for a dialog that allows the user to select a font. """ import wx from traits.api import provides from pyface.font import Font from pyface.ui_traits import PyfaceFont from pyface.i_font_dialog import IFontDialog from .dialog import Dialog # The WxPython version in a convenient to compare form. wx_version = tuple(int(x) for x in wx.__version__.split('.')[:3]) @provides(IFontDialog) class FontDialog(Dialog): """ A dialog for selecting fonts. """ # 'IFontDialog' interface ---------------------------------------------- #: The font in the dialog. font = PyfaceFont() # ------------------------------------------------------------------------ # 'IDialog' interface. # ------------------------------------------------------------------------ def _create_contents(self, parent): # In wx this is a canned dialog. pass # ------------------------------------------------------------------------ # 'IWindow' interface. # ------------------------------------------------------------------------ def close(self): font_data = self.control.GetFontData() wx_font = font_data.GetChosenFont() self.font = Font.from_toolkit(wx_font) super(FontDialog, self).close() # ------------------------------------------------------------------------ # 'IWidget' interface. # ------------------------------------------------------------------------ def _create_control(self, parent): wx_font = self.font.to_toolkit() data = wx.FontData() data.SetInitialFont(wx_font) dialog = wx.FontDialog(parent, data) return dialog pyface-7.4.0/pyface/ui/wx/wizard/0000755000076500000240000000000014176460551017565 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/wx/wizard/wizard_page.py0000644000076500000240000000470714176222673022444 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 page in a wizard. """ import wx from traits.api import Bool, HasTraits, provides, Str, Tuple from pyface.api import HeadingText from pyface.wizard.i_wizard_page import IWizardPage, MWizardPage @provides(IWizardPage) class WizardPage(MWizardPage, HasTraits): """ The toolkit specific implementation of a WizardPage. See the IWizardPage interface for the API documentation. """ # 'IWizardPage' interface ---------------------------------------------# id = Str() next_id = Str() last_page = Bool(False) complete = Bool(False) heading = Str() subheading = Str() size = Tuple() # ------------------------------------------------------------------------ # 'IWizardPage' interface. # ------------------------------------------------------------------------ def create_page(self, parent): """ Creates the wizard page. """ # FIXME: implement support for the size trait. panel = wx.Panel(parent, -1, style=wx.CLIP_CHILDREN) sizer = wx.BoxSizer(wx.VERTICAL) panel.SetSizer(sizer) panel.SetAutoLayout(True) # The 'pretty' heading ;^) if len(self.heading) > 0: title = HeadingText(panel, text=self.heading) sizer.Add(title.control, 0, wx.EXPAND | wx.BOTTOM, 5) if len(self.subheading) > 0: subtitle = wx.StaticText(panel, -1, self.subheading) sizer.Add(subtitle, 0, wx.EXPAND | wx.BOTTOM, 5) # The page content. content = self._create_page_content(panel) sizer.Add(content, 1, wx.EXPAND) return panel # ------------------------------------------------------------------------ # Protected 'IWizardPage' interface. # ------------------------------------------------------------------------ def _create_page_content(self, parent): """ Creates the actual page content. """ # Dummy implementation - override! panel = wx.Panel(parent, -1, style=wx.CLIP_CHILDREN) panel.SetBackgroundColour("yellow") return panel pyface-7.4.0/pyface/ui/wx/wizard/__init__.py0000644000076500000240000000062714176222673021704 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! pyface-7.4.0/pyface/ui/wx/wizard/wizard.py0000644000076500000240000001316714176222673021450 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 base class for all pyface wizards. """ import wx from traits.api import Bool, Instance, List, Property, provides, Str from pyface.api import Dialog, LayeredPanel from pyface.wizard.i_wizard import IWizard, MWizard from pyface.wizard.i_wizard_controller import IWizardController from pyface.wizard.i_wizard_page import IWizardPage @provides(IWizard) class Wizard(MWizard, Dialog): """ The base class for all pyface wizards. See the IWizard interface for the API documentation. """ # 'IWizard' interface -------------------------------------------------- pages = Property(List(IWizardPage)) controller = Instance(IWizardController) show_cancel = Bool(True) # 'IWindow' interface -------------------------------------------------- title = Str("Wizard") # private traits ------------------------------------------------------- _layered_panel = Instance(LayeredPanel) # ------------------------------------------------------------------------ # Protected 'IDialog' interface. # ------------------------------------------------------------------------ def _create_dialog_area(self, parent): """ Creates the main content of the dialog. """ self._layered_panel = panel = LayeredPanel(parent=parent, create=False) panel.create() # fixme: Specific size? panel.control.SetSize((100, 200)) return panel.control def _create_buttons(self, parent): """ Creates the buttons. """ sizer = wx.BoxSizer(wx.HORIZONTAL) # 'Back' button. self._back = back = wx.Button(parent, -1, "Back") parent.Bind(wx.EVT_BUTTON, self._on_back, back) sizer.Add(back, 0) # 'Next' button. self._next = next = wx.Button(parent, -1, "Next") parent.Bind(wx.EVT_BUTTON, self._on_next, next) sizer.Add(next, 0, wx.LEFT, 5) next.SetDefault() # 'Finish' button. self._finish = finish = wx.Button(parent, wx.ID_OK, "Finish") finish.Enable(self.controller.complete) parent.Bind(wx.EVT_BUTTON, self._wx_on_ok, finish) sizer.Add(finish, 0, wx.LEFT, 5) # 'Cancel' button. if self.show_cancel: self._cancel = cancel = wx.Button(parent, wx.ID_CANCEL, "Cancel") parent.Bind(wx.EVT_BUTTON, self._wx_on_cancel, cancel) sizer.Add(cancel, 0, wx.LEFT, 10) # 'Help' button. if len(self.help_id) > 0: help = wx.Button(parent, wx.ID_HELP, "Help") parent.Bind(wx.EVT_BUTTON, self._wx_on_help, help) sizer.Add(help, 0, wx.LEFT, 10) return sizer # ------------------------------------------------------------------------ # Protected 'MWizard' interface. # ------------------------------------------------------------------------ def _show_page(self, page): """ Show the specified page. """ panel = self._layered_panel # If the page has not yet been shown then create it. if not panel.has_layer(page.id): panel.add_layer(page.id, page.create_page(panel.control)) # Show the page. layer = panel.show_layer(page.id) layer.SetFocus() # Set the current page in the controller. # # fixme: Shouldn't this interface be reversed? Maybe calling # 'next_page' on the controller should cause it to set its own current # page? self.controller.current_page = page def _update(self, event): """ Enables/disables buttons depending on the state of the wizard. """ controller = self.controller current_page = controller.current_page is_first_page = controller.is_first_page(current_page) is_last_page = controller.is_last_page(current_page) # 'Next button'. if self._next is not None: self._next.Enable(current_page.complete and not is_last_page) # 'Back' button. if self._back is not None: self._back.Enable(not is_first_page) # 'Finish' button. if self._finish is not None: self._finish.Enable(controller.complete) # If this is the last page then the 'Finish' button is the default # button, otherwise the 'Next' button is the default button. if is_last_page: if self._finish is not None: self._finish.SetDefault() else: if self._next is not None: self._next.SetDefault() # Trait handlers ------------------------------------------------------- def _controller_default(self): """ Provide a default controller. """ from pyface.wizard.wizard_controller import WizardController return WizardController() def _get_pages(self): """ Returns the pages in the wizard. """ return self.controller.pages def _set_pages(self, pages): """ Sets the pages in the wizard. """ self.controller.pages = pages # wx event handlers ---------------------------------------------------- def _on_next(self, event): """ Called when the 'Next' button is pressed. """ self.next() def _on_back(self, event): """ Called when the 'Back' button is pressed. """ self.previous() pyface-7.4.0/pyface/ui/wx/resource_manager.py0000644000076500000240000000360514176222673022165 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Enthought pyface package component """ import os import tempfile from io import BytesIO import wx from pyface.resource.api import ResourceFactory class PyfaceResourceFactory(ResourceFactory): """ The implementation of a shared resource manager. """ # ------------------------------------------------------------------------ # 'ResourceFactory' toolkit interface. # ------------------------------------------------------------------------ def image_from_file(self, filename): """ Creates an image from the data in the specified filename. """ # N.B 'wx.BITMAP_TYPE_ANY' tells wxPython to attempt to autodetect the # --- image format. return wx.Image(filename, wx.BITMAP_TYPE_ANY) def image_from_data(self, data, filename=None): """ Creates an image from the specified data. """ return wx.Image(BytesIO(data)) handle = None if filename is None: # If there is currently no way in wx to create an image from data, # we have write it out to a temporary file and then read it back in: handle, filename = tempfile.mkstemp() # Write it out... tf = open(filename, "wb") tf.write(data) tf.close() # ... and read it back in! Lovely 8^() image = wx.Image(filename, wx.BITMAP_TYPE_ANY) # Remove the temporary file. if handle is not None: os.close(handle) os.unlink(filename) return image pyface-7.4.0/pyface/ui/wx/__init__.py0000644000076500000240000000062714176222673020404 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! pyface-7.4.0/pyface/ui/wx/splash_screen.py0000644000076500000240000000764214176222673021502 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Enthought pyface package component """ from logging import DEBUG import wx import wx.adv from traits.api import Any, Bool, Int, provides from traits.api import Tuple, Str from pyface.i_splash_screen import ISplashScreen, MSplashScreen from pyface.ui_traits import Image from pyface.wx.util.font_helper import new_font_like from .image_resource import ImageResource from .window import Window @provides(ISplashScreen) class SplashScreen(MSplashScreen, Window): """ The toolkit specific implementation of a SplashScreen. See the ISplashScreen interface for the API documentation. """ # 'ISplashScreen' interface -------------------------------------------- image = Image(ImageResource("splash")) log_level = Int(DEBUG) show_log_messages = Bool(True) text = Str() text_color = Any() text_font = Any() text_location = Tuple(5, 5) # ------------------------------------------------------------------------ # Protected 'IWidget' interface. # ------------------------------------------------------------------------ def _create_control(self, parent): # Get the splash screen image. image = self.image.create_image() splash_screen = wx.adv.SplashScreen( # The bitmap to display on the splash screen. image.ConvertToBitmap(), # Splash Style. wx.adv.SPLASH_NO_TIMEOUT | wx.adv.SPLASH_CENTRE_ON_SCREEN, # Timeout in milliseconds (we don't currently timeout!). 0, # The parent of the splash screen. parent, # wx Id. -1, # Window style. style=wx.SIMPLE_BORDER | wx.FRAME_NO_TASKBAR, ) # By default we create a font slightly bigger and slightly more italic # than the normal system font ;^) The font is used inside the event # handler for 'EVT_PAINT'. self._wx_default_text_font = new_font_like( wx.NORMAL_FONT, point_size=wx.NORMAL_FONT.GetPointSize() + 1, style=wx.ITALIC, ) # This allows us to write status text on the splash screen. splash_screen.Bind(wx.EVT_PAINT, self._on_paint) return splash_screen # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _text_changed(self): """ Called when the splash screen text has been changed. """ # Passing 'False' to 'Refresh' means "do not erase the background". if self.control is not None: self.control.Refresh(False) self.control.Update() wx.GetApp().Yield(True) def _on_paint(self, event): """ Called when the splash window is being repainted. """ if self.control is not None: # Get the window that the splash image is drawn in. window = self.control # .GetSplashWindow() dc = wx.PaintDC(window) if self.text_font is None: text_font = self._wx_default_text_font else: text_font = self.text_font dc.SetFont(text_font) if self.text_color is None: text_color = "black" else: text_color = self.text_color dc.SetTextForeground(text_color) x, y = self.text_location dc.DrawText(self.text, x, y) # Let the normal wx paint handling do its stuff. event.Skip() pyface-7.4.0/pyface/ui/wx/font.py0000644000076500000240000001276114176222673017615 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 conversion utilities This module provides facilities for converting between pyface Font objects and Wx Font objects, trying to keep as much similarity as possible between them. """ import wx # font weight and size features changed in wxPython 4.1/wxWidgets 3.1 wx_python_4_1 = (wx.VERSION >= (4, 1)) wx_family_to_generic_family = { wx.FONTFAMILY_DEFAULT: 'default', wx.FONTFAMILY_DECORATIVE: 'fantasy', wx.FONTFAMILY_ROMAN: 'serif', wx.FONTFAMILY_SCRIPT: 'cursive', wx.FONTFAMILY_SWISS: 'sans-serif', wx.FONTFAMILY_MODERN: 'monospace', wx.FONTFAMILY_TELETYPE: 'typewriter', } generic_family_to_wx_family = { 'default': wx.FONTFAMILY_DEFAULT, 'fantasy': wx.FONTFAMILY_DECORATIVE, 'decorative': wx.FONTFAMILY_DECORATIVE, 'serif': wx.FONTFAMILY_ROMAN, 'roman': wx.FONTFAMILY_ROMAN, 'cursive': wx.FONTFAMILY_SCRIPT, 'script': wx.FONTFAMILY_SCRIPT, 'sans-serif': wx.FONTFAMILY_SWISS, 'swiss': wx.FONTFAMILY_SWISS, 'monospace': wx.FONTFAMILY_MODERN, 'modern': wx.FONTFAMILY_MODERN, 'typewriter': wx.FONTFAMILY_TELETYPE, 'teletype': wx.FONTFAMILY_TELETYPE, } if wx_python_4_1: weight_to_wx_weight = { 100: wx.FONTWEIGHT_THIN, 200: wx.FONTWEIGHT_EXTRALIGHT, 300: wx.FONTWEIGHT_LIGHT, 400: wx.FONTWEIGHT_NORMAL, 500: wx.FONTWEIGHT_MEDIUM, 600: wx.FONTWEIGHT_SEMIBOLD, 700: wx.FONTWEIGHT_BOLD, 800: wx.FONTWEIGHT_EXTRABOLD, 900: wx.FONTWEIGHT_HEAVY, 1000: wx.FONTWEIGHT_EXTRAHEAVY, } wx_weight_to_weight = { wx.FONTWEIGHT_THIN: 'thin', wx.FONTWEIGHT_EXTRALIGHT: 'extra-light', wx.FONTWEIGHT_LIGHT: 'light', wx.FONTWEIGHT_NORMAL: 'normal', wx.FONTWEIGHT_MEDIUM: 'medium', wx.FONTWEIGHT_SEMIBOLD: 'semibold', wx.FONTWEIGHT_BOLD: 'bold', wx.FONTWEIGHT_EXTRABOLD: 'extra-bold', wx.FONTWEIGHT_HEAVY: 'heavy', wx.FONTWEIGHT_EXTRAHEAVY: 'extra-heavy', wx.FONTWEIGHT_MAX: 'extra-heavy', } else: weight_to_wx_weight = { 100: wx.FONTWEIGHT_LIGHT, 200: wx.FONTWEIGHT_LIGHT, 300: wx.FONTWEIGHT_LIGHT, 400: wx.FONTWEIGHT_NORMAL, 500: wx.FONTWEIGHT_NORMAL, 600: wx.FONTWEIGHT_BOLD, 700: wx.FONTWEIGHT_BOLD, 800: wx.FONTWEIGHT_BOLD, 900: wx.FONTWEIGHT_MAX, 1000: wx.FONTWEIGHT_MAX, } wx_weight_to_weight = { wx.FONTWEIGHT_LIGHT: 'light', wx.FONTWEIGHT_NORMAL: 'normal', wx.FONTWEIGHT_BOLD: 'bold', wx.FONTWEIGHT_MAX: 'extra-heavy', } style_to_wx_style = { 'normal': wx.FONTSTYLE_NORMAL, 'oblique': wx.FONTSTYLE_SLANT, 'italic': wx.FONTSTYLE_ITALIC, } wx_style_to_style = {value: key for key, value in style_to_wx_style.items()} def font_to_toolkit_font(font): """ Convert a Pyface font to a wx.font Font. Wx fonts have no notion of stretch values or small-caps or overline variants, so these are ignored when converting. Parameters ---------- font : pyface.font.Font The Pyface font to convert. Returns ------- wx_font : wx.font.Font The best matching wx font. """ size = font.size for family in font.family: if family in generic_family_to_wx_family: default_family = generic_family_to_wx_family[family] break else: default_family = wx.FONTFAMILY_DEFAULT weight = weight_to_wx_weight[font.weight_] style = style_to_wx_style[font.style] underline = ('underline' in font.decorations) # get a default font candidate wx_font = wx.Font(size, default_family, style, weight, underline) for face in font.family: # don't try to match generic family if face in generic_family_to_wx_family: break wx_font = wx.Font( size, default_family, style, weight, underline, face) # we have a match, so stop if wx_font.GetFaceName().lower() == face.lower(): break wx_font.SetStrikethrough('strikethrough' in font.decorations) return wx_font def toolkit_font_to_properties(toolkit_font): """ Convert a Wx Font to a dictionary of font properties. Parameters ---------- toolkit_font : wx.font.Font The Wx font to convert. Returns ------- properties : dict Font properties suitable for use in creating a Pyface Font. """ family = wx_family_to_generic_family[toolkit_font.GetFamily()] face = toolkit_font.GetFaceName() if wx_python_4_1: size = toolkit_font.GetFractionalPointSize() else: size = toolkit_font.GetPointSize() style = wx_style_to_style[toolkit_font.GetStyle()] weight = wx_weight_to_weight[toolkit_font.GetWeight()] decorations = set() if toolkit_font.GetUnderlined(): decorations.add('underline') if toolkit_font.GetStrikethrough(): decorations.add('strikethrough') return { 'family': [face, family], 'size': size, 'weight': weight, 'stretch': 'normal', 'style': style, 'variants': set(), 'decorations': decorations, } pyface-7.4.0/pyface/ui/wx/expandable_header.py0000644000076500000240000001502414176222673022255 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 header for an entry in a collection of expandables. The header provides a visual indicator of the current state, a text label, and a 'remove' button. """ import warnings import wx from traits.api import Event, Str, Bool from pyface.wx.util.font_helper import new_font_like from pyface.ui_traits import Image from .image_resource import ImageResource from .widget import Widget class ExpandableHeader(Widget): """ A header for an entry in a collection of expandables. The header provides a visual indicator of the current state, a text label, and a 'remove' button. """ #: The title of the panel. title = Str("Panel") #: The carat image to show when the panel is collapsed. collapsed_carat_image = Image(ImageResource("carat_closed")) #: The carat image to show when the panel is expanded. expanded_carat_image = Image(ImageResource("carat_open")) #: The backing header image when the mouse is elsewhere #: This is not used and deprecated. header_bar_image = Image(ImageResource("panel_gradient")) #: The backing header image when the mouse is over #: This is not used and deprecated. header_mouseover_image = Image(ImageResource("panel_gradient_over")) #: The image to use for the close button. #: This is not used and deprecated. remove_image = Image(ImageResource("close")) #: Represents the current state of the panel. True means expanded. state = Bool(False) # Events ---- #: The panel has been expanded or collapsed panel_expanded = Event() #: The panel has been closed panel_closed = Event() _CARAT_X = 4 _CARAT_Y = 4 _TEXT_Y = 0 _TEXT_X_OFFSET = 10 # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, parent=None, container=None, **traits): """ Creates the panel. """ if container is not None: warnings.warn( "the container parameter is deprecated and will be " "removed in a future Pyface release", DeprecationWarning, ) self.observe( lambda event: container.remove_panel(event.new), "panel_closed", ) create = traits.pop("create", True) # Base class constructor. super().__init__(parent=parent, **traits) # Create the toolkit-specific control that represents the widget. if create: self.create() warnings.warn( "automatic widget creation is deprecated and will be removed " "in a future Pyface version, use create=False and explicitly " "call create() for future behaviour", PendingDeprecationWarning, ) # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _create_control(self, parent): """ Create the toolkit-specific control that represents the widget. """ collapsed_carat = self.collapsed_carat_image.create_image() self._collapsed_bmp = collapsed_carat.ConvertToBitmap() self._carat_w = self._collapsed_bmp.GetWidth() expanded_carat = self.expanded_carat_image.create_image() self._expanded_bmp = expanded_carat.ConvertToBitmap() # create our panel and initialize it appropriately sizer = wx.BoxSizer(wx.VERTICAL) panel = wx.Panel(parent, -1, style=wx.CLIP_CHILDREN | wx.BORDER_SIMPLE) panel.SetSizer(sizer) panel.SetAutoLayout(True) # create the remove button remove = wx.BitmapButton.NewCloseButton(panel, -1) sizer.Add(remove, 0, wx.ALIGN_RIGHT, 5) # Create a suitable font. self._font = new_font_like( wx.NORMAL_FONT, point_size=wx.NORMAL_FONT.GetPointSize() - 1 ) height = self._get_preferred_height(parent, self.title, self._font) panel.SetMinSize((-1, height+2)) panel.Bind(wx.EVT_PAINT, self._on_paint) panel.Bind(wx.EVT_LEFT_DOWN, self._on_down) panel.Bind(wx.EVT_BUTTON, self._on_remove) return panel def _get_preferred_height(self, parent, text, font): """ Calculates the preferred height of the widget. """ dc = wx.MemoryDC() dc.SetFont(font) metrics = dc.GetFontMetrics() text_h = metrics.height + 2 * self._TEXT_Y # add in height of buttons carat_h = self._collapsed_bmp.GetHeight() + 2 * self._CARAT_Y return max(text_h, carat_h) def _draw_carat_button(self, dc): """ Draws the button at the correct coordinates. """ if self.state: bmp = self._expanded_bmp else: bmp = self._collapsed_bmp dc.DrawBitmap(bmp, self._CARAT_X, self._CARAT_Y, True) def _draw_title(self, dc): """ Draws the text label for the header. """ dc.SetFont(self._font) # Render the text. dc.DrawText( self.title, self._carat_w + self._TEXT_X_OFFSET, self._TEXT_Y ) def _draw(self, dc): """ Draws the control. """ # Draw the title text self._draw_title(dc) # Draw the carat button self._draw_carat_button(dc) # ------------------------------------------------------------------------ # wx event handlers. # ------------------------------------------------------------------------ def _on_paint(self, event): """ Called when the background of the panel is erased. """ # print('ImageButton._on_erase_background') dc = wx.PaintDC(self.control) self._draw(dc) def _on_down(self, event): """ Called when button is pressed. """ self.state = not self.state self.control.Refresh() # fire an event so any listeners can pick up the change self.panel_expanded = self event.Skip() def _on_remove(self, event): """ Called when remove button is pressed. """ self.panel_closed = self pyface-7.4.0/pyface/ui/wx/preference/0000755000076500000240000000000014176460551020403 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/wx/preference/__init__.py0000644000076500000240000000000014176222673022503 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/wx/preference/preference_dialog.py0000644000076500000240000001576614176222673024432 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 preference dialog. """ import wx from traits.api import Any, Dict, Float, Instance, Str from pyface.preference.preference_node import PreferenceNode from pyface.ui.wx.heading_text import HeadingText from pyface.ui.wx.layered_panel import LayeredPanel from pyface.ui.wx.split_dialog import SplitDialog from pyface.ui.wx.viewer.tree_viewer import TreeViewer from pyface.viewer.default_tree_content_provider import ( DefaultTreeContentProvider, ) class PreferenceDialog(SplitDialog): """ The preference dialog. """ # 'Dialog' interface --------------------------------------------------- # The dialog title. title = Str("Preferences") # 'SplitDialog' interface ---------------------------------------------# # The ratio of the size of the left/top pane to the right/bottom pane. ratio = Float(0.25) # 'PreferenceDialog' interface ----------------------------------------- # The root of the preference hierarchy. root = Instance(PreferenceNode) # Private interface ---------------------------------------------------- # The preference pages in the dialog (they are created lazily). _pages = Dict() # The current visible preference page. _current_page = Any() # ------------------------------------------------------------------------ # Protected 'Dialog' interface. # ------------------------------------------------------------------------ def _create_buttons(self, parent): """ Creates the buttons. """ sizer = wx.BoxSizer(wx.HORIZONTAL) # 'Done' button. done = wx.Button(parent, wx.ID_OK, "Done") done.SetDefault() parent.Bind(wx.EVT_BUTTON, self._wx_on_ok, wx.ID_OK) sizer.Add(done) return sizer # ------------------------------------------------------------------------ # Protected 'SplitDialog' interface. # ------------------------------------------------------------------------ def _create_lhs(self, parent): """ Creates the panel containing the preference page tree. """ return self._create_tree(parent) def _create_rhs(self, parent): """ Creates the panel containing the selected preference page. """ panel = wx.Panel(parent, -1, style=wx.CLIP_CHILDREN) sizer = wx.BoxSizer(wx.VERTICAL) panel.SetSizer(sizer) panel.SetAutoLayout(True) # The 'pretty' title bar ;^) self.__title = HeadingText(parent=panel, create=False) self.__title.create() sizer.Add( self.__title.control, 0, wx.EXPAND | wx.BOTTOM | wx.LEFT | wx.RIGHT, 5, ) # The preference page of the node currently selected in the tree. self._layered_panel = LayeredPanel( parent=panel, create=False, min_width=-1, min_height=-1, ) self._layered_panel.create() sizer.Add( self._layered_panel.control, 1, wx.EXPAND | wx.TOP | wx.LEFT | wx.RIGHT, 5, ) # The 'Restore Defaults' button etc. buttons = self._create_page_buttons(panel) sizer.Add(buttons, 0, wx.ALIGN_RIGHT | wx.TOP | wx.RIGHT, 5) # A separator. line = wx.StaticLine(panel, -1) sizer.Add(line, 0, wx.EXPAND | wx.TOP | wx.LEFT | wx.RIGHT, 5) # Resize the panel to fit the sizer's minimum size. sizer.Fit(panel) return panel # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _create_tree(self, parent): """ Creates the preference page tree. """ tree_viewer = TreeViewer( parent, input=self.root, show_images=False, show_root=False, content_provider=DefaultTreeContentProvider(), ) tree_viewer.observe(self._on_selection_changed, "selection") return tree_viewer.control def _create_page_buttons(self, parent): """ Creates the 'Restore Defaults' button, etc. At the moment the "etc." is an optional 'Help' button. """ self._button_sizer = sizer = wx.BoxSizer(wx.HORIZONTAL) # 'Help' button. Comes first so 'Restore Defaults' doesn't jump around. self._help = help = wx.Button(parent, -1, "Help") parent.Bind(wx.EVT_BUTTON, self._on_help, help.GetId()) sizer.Add(help, 0, wx.RIGHT, 5) # 'Restore Defaults' button. restore = wx.Button(parent, -1, "Restore Defaults") parent.Bind(wx.EVT_BUTTON, self._on_restore_defaults, restore.GetId()) sizer.Add(restore) return sizer # ------------------------------------------------------------------------ # wx event handlers. # ------------------------------------------------------------------------ def _on_restore_defaults(self, event): """ Called when the 'Restore Defaults' button is pressed. """ page = self._pages[self._layered_panel.current_layer_name] page.restore_defaults() def _on_help(self, event): """ Called when the 'Help' button is pressed. """ page = self._pages[self._layered_panel.current_layer_name] page.show_help_topic() return # ------------------------------------------------------------------------ # Trait event handlers. # ------------------------------------------------------------------------ def _on_selection_changed(self, event): """ Called when a node in the tree is selected. """ selection = event.new if len(selection) > 0: # The tree is in single selection mode. node = selection[0] # We only show the help button if the selected node has a help # topic Id. if len(node.help_id) > 0: self._button_sizer.Show(self._help, True) else: self._button_sizer.Show(self._help, False) # Show the selected preference page. layered_panel = self._layered_panel parent = self._layered_panel.control # If we haven't yet displayed the node's preference page during the # lifetime of this dialog, then we have to create it. if not layered_panel.has_layer(node.name): page = node.create_page() layered_panel.add_layer(node.name, page.create_control(parent)) self._pages[node.name] = page layered_panel.show_layer(node.name) self.__title.text = node.name return pyface-7.4.0/pyface/ui/wx/application_window.py0000644000076500000240000001735314176222673022543 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Enthought pyface package component """ import wx from traits.api import Instance, List, Str, observe, provides from pyface.action.api import MenuBarManager, StatusBarManager from pyface.action.api import ToolBarManager from pyface.i_application_window import ( IApplicationWindow, MApplicationWindow, ) from pyface.ui_traits import Image from pyface.wx.aui import aui, PyfaceAuiManager from .image_resource import ImageResource from .window import Window @provides(IApplicationWindow) class ApplicationWindow(MApplicationWindow, Window): """ The toolkit specific implementation of an ApplicationWindow. See the IApplicationWindow interface for the API documentation. """ # 'IApplicationWindow' interface --------------------------------------- icon = Image() menu_bar_manager = Instance(MenuBarManager) status_bar_manager = Instance(StatusBarManager) tool_bar_manager = Instance(ToolBarManager) # If the underlying toolkit supports multiple toolbars then you can use # this list instead. tool_bar_managers = List(ToolBarManager) # 'IWindow' interface -------------------------------------------------# # fixme: We can't set the default value of the actual 'size' trait here as # in the toolkit-specific event handlers for window size and position # changes, we set the value of the shadow '_size' trait. The problem is # that by doing that traits never knows that the trait has been set and # hence always returns the default value! Using a trait initializer method # seems to work however (e.g. 'def _size_default'). Hmmmm.... ## size = (800, 600) title = Str("Pyface") # ------------------------------------------------------------------------ # Protected 'IApplicationWindow' interface. # ------------------------------------------------------------------------ def _create_contents(self, parent): panel = wx.Panel(parent, -1, name="ApplicationWindow") panel.SetBackgroundColour("blue") return panel def _create_menu_bar(self, parent): if self.menu_bar_manager is not None: menu_bar = self.menu_bar_manager.create_menu_bar(parent) self.control.SetMenuBar(menu_bar) def _create_status_bar(self, parent): if self.status_bar_manager is not None: status_bar = self.status_bar_manager.create_status_bar(parent) self.control.SetStatusBar(status_bar) def _create_tool_bar(self, parent): tool_bar_managers = self._get_tool_bar_managers() if len(tool_bar_managers) > 0: for tool_bar_manager in reversed(tool_bar_managers): tool_bar = tool_bar_manager.create_tool_bar(parent, aui=True) self._add_toolbar_to_aui_manager(tool_bar) self._aui_manager.Update() def _set_window_icon(self): if self.icon is None: icon = ImageResource("application.ico") else: icon = self.icon if self.control is not None: self.control.SetIcon(icon.create_icon()) # ------------------------------------------------------------------------ # 'Window' interface. # ------------------------------------------------------------------------ def _size_default(self): """ Trait initialiser. """ return (800, 600) # ------------------------------------------------------------------------ # Protected 'IWidget' interface. # ------------------------------------------------------------------------ def _create(self): super()._create() self._aui_manager = PyfaceAuiManager() self._aui_manager.SetManagedWindow(self.control) # Keep a reference to the AUI Manager in the control because Panes # will need to access it in order to lay themselves out self.control._aui_manager = self._aui_manager contents = self._create_contents(self.control) self._aui_manager.AddPane(contents, aui.AuiPaneInfo().CenterPane()) self._create_trim_widgets(self.control) # Updating the AUI manager actually commits all of the pane's added # to it (this allows batch updates). self._aui_manager.Update() def _create_control(self, parent): style = ( wx.DEFAULT_FRAME_STYLE | wx.FRAME_NO_WINDOW_MENU | wx.CLIP_CHILDREN ) control = wx.Frame( parent, -1, self.title, style=style, size=self.size, pos=self.position, ) # Mac/Win needs this, otherwise background color is black attr = control.GetDefaultAttributes() control.SetBackgroundColour(attr.colBg) return control def destroy(self): if self.control: self._aui_manager.UnInit() super().destroy() # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _add_toolbar_to_aui_manager(self, tool_bar): """ Add a toolbar to the AUI manager. """ info = self._get_tool_bar_pane_info(tool_bar) self._aui_manager.AddPane(tool_bar, info) def _get_tool_bar_pane_info(self, tool_bar): info = aui.AuiPaneInfo() info.Caption(tool_bar.tool_bar_manager.name) info.LeftDockable(False) info.Name(tool_bar.tool_bar_manager.id) info.RightDockable(False) info.ToolbarPane() info.Top() return info def _get_tool_bar_managers(self): """ Return all tool bar managers specified for the window. """ # fixme: V3 remove the old-style single toolbar option! if self.tool_bar_manager is not None: tool_bar_managers = [self.tool_bar_manager] else: tool_bar_managers = self.tool_bar_managers return tool_bar_managers def _wx_enable_tool_bar(self, tool_bar, enabled): """ Enable/Disablea tool bar. """ # AUI toolbars cannot be enabled/disabled. def _wx_show_tool_bar(self, tool_bar, visible): """ Hide/Show a tool bar. """ pane = self._aui_manager.GetPane(tool_bar.tool_bar_manager.id) if visible: pane.Show() else: # Without this workaround, toolbars know the sizes of other # hidden toolbars and leave gaps in the toolbar dock pane.window.Show(False) self._aui_manager.DetachPane(pane.window) info = self._get_tool_bar_pane_info(pane.window) info.Hide() self._aui_manager.AddPane(pane.window, info) self._aui_manager.Update() return # Trait change handlers ------------------------------------------------ def _menu_bar_manager_changed(self): if self.control is not None: self._create_menu_bar(self.control) def _status_bar_manager_changed(self, old, new): if self.control is not None: if old is not None: self.control.SetStatusBar(None) old.remove_status_bar(self.control) self._create_status_bar(self.control) @observe("tool_bar_manager, tool_bar_managers.items") def _update_tool_bar_managers(self, event): if self.control is not None: self._create_tool_bar(self.control) def _icon_changed(self): self._set_window_icon() pyface-7.4.0/pyface/ui/wx/progress_dialog.py0000644000076500000240000003151214176222673022025 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 progress bar intended to run in the UI thread """ import warnings import wx import time from traits.api import Bool, Instance, Int, Property, Str from pyface.i_progress_dialog import MProgressDialog from pyface.ui_traits import Orientation from .widget import Widget from .window import Window class ProgressBar(Widget): """ A simple progress bar dialog intended to run in the UI thread """ #: The progress bar's parent control. parent = Instance(wx.Window) #: The progress bar's control. control = Instance(wx.Gauge) #: The orientation of the progress bar. direction = Orientation("horizontal") #: The maximum value for the progress bar. _max = Int() def __init__( self, parent, minimum=0, maximum=100, direction="horizontal", size=(200, -1), **traits, ): """ Constructs a progress bar which can be put into a panel, or optionaly, its own window """ create = traits.pop("create", True) # XXX minimum is ignored - it either should be deprecated or supported super().__init__( parent=parent, _max=maximum, direction=direction, size=size, **traits, ) if create: self.create() warnings.warn( "automatic widget creation is deprecated and will be removed " "in a future Pyface version, use create=False and explicitly " "call create() for future behaviour", PendingDeprecationWarning, ) def _create_control(self, parent): style = wx.GA_HORIZONTAL if self.direction == "vertical": style = wx.GA_VERTICAL return wx.Gauge(parent, -1, self._max, style=style, size=self.size) def update(self, value): """ Update the progress bar to the desired value. """ if self._max == 0: self.control.Pulse() else: self.control.SetValue(value) self.control.Update() def _show(self): # Show the parent self.parent.Show() # Show the toolkit-specific control in the parent self.control.Show() class ProgressDialog(MProgressDialog, Window): """ A simple progress dialog window which allows itself to be updated """ #: The progress bar progress_bar = Instance(ProgressBar) #: The window title title = Str() #: The text message to display in the dialog message = Property() #: The minimum value of the progress range min = Int() #: The minimum value of the progress range max = Int() #: The margin around the progress bar margin = Int(5) #: Whether or not the progress dialog can be cancelled can_cancel = Bool(False) #: Whether or not to show the time taken show_time = Bool(False) #: Whether or not to show the percent completed show_percent = Bool(False) #: Whether or not the dialog was cancelled by the user _user_cancelled = Bool(False) #: The text of the message label _message_text = Str() #: The size of the dialog dialog_size = Instance(wx.Size) # Label for the 'cancel' button cancel_button_label = Str("Cancel") #: The widget showing the message text _message_control = Instance(wx.StaticText) #: The widget showing the time elapsed _elapsed_control = Instance(wx.StaticText) #: The widget showing the estimated time to completion _estimated_control = Instance(wx.StaticText) #: The widget showing the estimated time remaining _remaining_control = Instance(wx.StaticText) def __init__(self, *args, **kw): if "message" in kw: self._message_text = kw.pop("message") # initialize the start time in case some tries updating # before open() is called self._start_time = 0 super().__init__(*args, **kw) # ------------------------------------------------------------------------- # IWindow Interface # ------------------------------------------------------------------------- def open(self): """ Opens the window. """ super().open() self._start_time = time.time() wx.GetApp().Yield(True) def close(self): """ Closes the window. """ if self.progress_bar is not None: self.progress_bar.destroy() self.progress_bar = None if self._message_control is not None: self._message_control = None super().close() # ------------------------------------------------------------------------- # IProgressDialog Interface # ------------------------------------------------------------------------- def change_message(self, value): """ Change the displayed message in the progress dialog Parameters ---------- message : str or unicode The new message to display. """ self._message_text = value if self._message_control is not None: self._message_control.SetLabel(value) self._message_control.Update() msg_control_size = self._message_control.GetSize() self.dialog_size.x = max( self.dialog_size.x, msg_control_size.x + 2 * self.margin ) if self.control is not None: self.control.SetClientSize(self.dialog_size) self.control.GetSizer().Layout() def update(self, value): """ Update the progress bar to the desired value If the value is >= the maximum and the progress bar is not contained in another panel the parent window will be closed. Parameters ---------- value : The progress value to set. """ if self.progress_bar is None: # the developer is trying to update a progress bar which is already # done. Allow it, but do nothing return (False, False) self.progress_bar.update(value) # A bit hackish, but on Windows if another window sets focus, the # other window will come to the top, obscuring the progress dialog. # Only do this if the control is a top level window, so windows which # embed a progress dialog won't keep popping to the top # When we do embed the dialog, self.control may be None since the # embedding might just be grabbing the guts of the control. This happens # in the Traits UI ProgressEditor. if self.control is not None and self.control.IsTopLevel(): self.control.Raise() if self.max > 0: percent = (float(value) - self.min) / (self.max - self.min) if self.show_time and (percent != 0): current_time = time.time() elapsed = current_time - self._start_time estimated = elapsed / percent remaining = estimated - elapsed self._set_time_label(elapsed, self._elapsed_control) self._set_time_label(estimated, self._estimated_control) self._set_time_label(remaining, self._remaining_control) if self.show_percent: self._percent_control = "%3f" % ((percent * 100) % 1) if value >= self.max or self._user_cancelled: self.close() else: if self._user_cancelled: self.close() wx.GetApp().Yield(True) return (not self._user_cancelled, False) # ------------------------------------------------------------------------- # Private Interface # ------------------------------------------------------------------------- def _on_cancel(self, event): self._user_cancelled = True self.close() def _on_close(self, event): self._user_cancelled = True return self.close() def _set_time_label(self, value, control): hours = value // 3600 minutes = (value % 3600) // 60 seconds = value % 60 label = "%u:%02u:%02u" % (hours, minutes, seconds) control.SetLabel(label) def _get_message(self): return self._message_text def _create_buttons(self, dialog, parent_sizer): """ Creates the buttons. """ sizer = wx.BoxSizer(wx.HORIZONTAL) self._cancel = None if self.can_cancel: # 'Cancel' button. self._cancel = cancel = wx.Button( dialog, wx.ID_CANCEL, self.cancel_button_label ) dialog.Bind(wx.EVT_BUTTON, self._on_cancel, id=wx.ID_CANCEL) sizer.Add(cancel, 0, wx.LEFT, 10) button_size = cancel.GetSize() self.dialog_size.x = max( self.dialog_size.x, button_size.x + 2 * self.margin ) self.dialog_size.y += button_size.y + 2 * self.margin parent_sizer.Add(sizer, 0, wx.ALIGN_RIGHT | wx.ALL, self.margin) def _create_label(self, dialog, parent_sizer, text): local_sizer = wx.BoxSizer() dummy = wx.StaticText(dialog, -1, text) label = wx.StaticText(dialog, -1, "unknown") local_sizer.Add(dummy, 1, wx.ALIGN_LEFT) local_sizer.Add(label, 1, wx.ALIGN_LEFT | wx.ALIGN_RIGHT, self.margin) parent_sizer.Add( local_sizer, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.TOP, self.margin ) return label def _create_gauge(self, dialog, parent_sizer): self.progress_bar = ProgressBar(dialog, self.min, self.max, create=False) self.progress_bar.create() parent_sizer.Add( self.progress_bar.control, 0, wx.CENTER | wx.ALL, self.margin ) horiz_spacer = 50 progress_bar_size = self.progress_bar.control.GetSize() self.dialog_size.x = max( self.dialog_size.x, progress_bar_size.x + 2 * self.margin + horiz_spacer, ) self.dialog_size.y += progress_bar_size.y + 2 * self.margin def _create_message(self, dialog, parent_sizer): self._message_control = wx.StaticText(dialog, -1, self.message) parent_sizer.Add( self._message_control, 0, wx.LEFT | wx.TOP, self.margin ) msg_control_size = self._message_control.GetSize() self.dialog_size.x = max( self.dialog_size.x, msg_control_size.x + 2 * self.margin ) self.dialog_size.y += msg_control_size.y + 2 * self.margin def _create_percent(self, dialog, parent_sizer): if not self.show_percent: return raise NotImplementedError() def _create_timer(self, dialog, parent_sizer): if not self.show_time: return self._elapsed_control = self._create_label( dialog, parent_sizer, "Elapsed time : " ) self._estimated_control = self._create_label( dialog, parent_sizer, "Estimated time : " ) self._remaining_control = self._create_label( dialog, parent_sizer, "Remaining time : " ) elapsed_size = self._elapsed_control.GetSize() estimated_size = self._estimated_control.GetSize() remaining_size = self._remaining_control.GetSize() timer_size = wx.Size() timer_size.x = max(elapsed_size.x, estimated_size.x, remaining_size.x) timer_size.y = elapsed_size.y + estimated_size.y + remaining_size.y self.dialog_size.x = max( self.dialog_size.x, timer_size.x + 2 * self.margin ) self.dialog_size.y += timer_size.y + 2 * self.margin def _create_control(self, parent): """ Creates the window contents. This method is intended to be overridden if necessary. By default we just create an empty panel. """ style = ( wx.DEFAULT_FRAME_STYLE | wx.FRAME_NO_WINDOW_MENU | wx.CLIP_CHILDREN ) dialog = wx.Frame( parent, -1, self.title, style=style, size=self.size, pos=self.position, ) sizer = wx.BoxSizer(wx.VERTICAL) dialog.SetSizer(sizer) dialog.SetAutoLayout(True) self.dialog_size = wx.Size() # The 'guts' of the dialog. self._create_message(dialog, sizer) self._create_gauge(dialog, sizer) self._create_percent(dialog, sizer) self._create_timer(dialog, sizer) self._create_buttons(dialog, sizer) dialog.SetClientSize(self.dialog_size) dialog.CentreOnParent() return dialog pyface-7.4.0/pyface/ui/wx/layered_panel.py0000644000076500000240000001057514176222673021454 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 Layered panel. """ import wx from wx.lib.scrolledpanel import ScrolledPanel from traits.api import Int, provides from pyface.i_layered_panel import ILayeredPanel, MLayeredPanel from .layout_widget import LayoutWidget @provides(ILayeredPanel) class LayeredPanel(MLayeredPanel, LayoutWidget): """ A Layered panel. A layered panel contains one or more named layers, with only one layer visible at any one time (think of a 'tab' control minus the tabs!). Each layer is a toolkit-specific control. """ # The default style. STYLE = wx.CLIP_CHILDREN # The minimum for the panel, which is the maximum of the minimum # sizes of the layers min_width = Int(0) min_height = Int(0) # ------------------------------------------------------------------------ # 'LayeredPanel' interface. # ------------------------------------------------------------------------ def add_layer(self, name, layer): """ Adds a layer with the specified name. All layers are hidden when they are added. Use 'show_layer' to make a layer visible. """ # Add the layer to our sizer. sizer = self.control.GetSizer() sizer.Add(layer, 1, wx.EXPAND) # All layers are hidden when they are added. Use 'show_layer' to make # a layer visible. sizer.Show(layer, False) # fixme: Should we warn if a layer is being overridden? self._layers[name] = layer # fixme: The minimum size stuff that was added for linux broke the # sizing on Windows (at least for the preference dialog). The # preference dialog now sets the minimum width and height to -1 so # that this layout code doesn't get executed. if self.min_width != -1 or self.min_height != -1: if layer.GetSizer() is None: return layer min_size = layer.GetSizer().CalcMin() needs_layout = False if min_size.GetWidth() > self.min_width: self.min_width = min_size.GetWidth() needs_layout = True if min_size.GetHeight() > self.min_height: self.min_height = min_size.GetHeight() needs_layout = True if needs_layout: # Reset our size hints and relayout self.control.SetSizeHints(self.min_width, self.min_height) self.control.GetSizer().Layout() # fixme: Force our parent to reset it's size hints to its # minimum parent = self.control.GetParent() parent.GetSizer().SetSizeHints(parent) parent.GetSizer().Layout() return layer def show_layer(self, name): """ Shows the layer with the specified name. """ # Hide the current layer (if one is displayed). if self.current_layer is not None: self._hide_layer(self.current_layer) # Show the specified layer. layer = self._show_layer(name, self._layers[name]) return layer # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _create_control(self, parent): """ Create the toolkit-specific control that represents the widget. """ panel = ScrolledPanel(parent, -1, style=self.STYLE) sizer = wx.BoxSizer(wx.VERTICAL) panel.SetSizer(sizer) panel.SetAutoLayout(True) panel.SetupScrolling() return panel def _hide_layer(self, layer): """ Hides the specified layer. """ sizer = self.control.GetSizer() sizer.Show(layer, False) sizer.Layout() def _show_layer(self, name, layer): """ Shows the specified layer. """ sizer = self.control.GetSizer() sizer.Show(layer, True) sizer.Layout() self.current_layer = layer self.current_layer_name = name return layer pyface-7.4.0/pyface/ui/wx/action/0000755000076500000240000000000014176460551017542 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/wx/action/tool_bar_manager.py0000644000076500000240000003422214176222673023413 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 wx specific implementation of the tool bar manager. """ import wx from traits.api import Bool, Enum, Instance, Str, Tuple from pyface.wx.aui import aui as AUI from pyface.image_cache import ImageCache from pyface.action.action_manager import ActionManager class ToolBarManager(ActionManager): """ A tool bar manager realizes itself in errr, a tool bar control. """ # 'ToolBarManager' interface ------------------------------------------- # Is the tool bar enabled? enabled = Bool(True) # Is the tool bar visible? visible = Bool(True) # The size of tool images (width, height). image_size = Tuple((16, 16)) # The toolbar name (used to distinguish multiple toolbars). name = Str("ToolBar") # The orientation of the toolbar. orientation = Enum("horizontal", "vertical") # Should we display the name of each tool bar tool under its image? show_tool_names = Bool(True) # Should we display the horizontal divider? show_divider = Bool(False) # Private interface ---------------------------------------------------- # Cache of tool images (scaled to the appropriate size). _image_cache = Instance(ImageCache) # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, *args, **traits): """ Creates a new tool bar manager. """ # Base class contructor. super().__init__(*args, **traits) # An image cache to make sure that we only load each image used in the # tool bar exactly once. self._image_cache = ImageCache(self.image_size[0], self.image_size[1]) return # ------------------------------------------------------------------------ # 'ToolBarManager' interface. # ------------------------------------------------------------------------ def create_tool_bar(self, parent, controller=None, aui=False): """ Creates a tool bar. """ # If a controller is required it can either be set as a trait on the # tool bar manager (the trait is part of the 'ActionManager' API), or # passed in here (if one is passed in here it takes precedence over the # trait). if controller is None: controller = self.controller # Determine the wx style for the tool bar based on any optional # settings. style = wx.NO_BORDER | wx.CLIP_CHILDREN if aui: aui_style = AUI.AUI_TB_PLAIN_BACKGROUND if self.show_tool_names: aui_style |= AUI.AUI_TB_TEXT if self.orientation != "horizontal": aui_style |= AUI.AUI_TB_VERTICAL if not self.show_divider: style |= wx.TB_NODIVIDER tool_bar = _AuiToolBar( self, parent, -1, style=style, agwStyle=aui_style ) else: style |= wx.TB_FLAT if self.show_tool_names: style |= wx.TB_TEXT if self.orientation == "horizontal": style |= wx.TB_HORIZONTAL else: style |= wx.TB_VERTICAL if not self.show_divider: style |= wx.TB_NODIVIDER tool_bar = _ToolBar(self, parent, -1, style=style) # fixme: Setting the tool bitmap size seems to be the only way to # change the height of the toolbar in wx. tool_bar.SetToolBitmapSize(self.image_size) # Add all of items in the manager's groups to the tool bar. self._wx_add_tools(parent, tool_bar, controller) # Make the tools appear in the tool bar (without this you will see # nothing!). tool_bar.Realize() # fixme: Without the following hack, only the first item in a radio # group can be selected when the tool bar is first realised 8^() self._wx_set_initial_tool_state(tool_bar) return tool_bar # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _wx_add_tools(self, parent, tool_bar, controller): """ Adds tools for all items in the list of groups. """ previous_non_empty_group = None for group in self.groups: if len(group.items) > 0: # Is a separator required? if previous_non_empty_group is not None and group.separator: tool_bar.AddSeparator() previous_non_empty_group = group # Create a tool bar tool for each item in the group. for item in group.items: item.add_to_toolbar( parent, tool_bar, self._image_cache, controller, self.show_tool_names, ) def _wx_set_initial_tool_state(self, tool_bar): """ Workaround for the wxPython tool bar bug. Without this, only the first item in a radio group can be selected when the tool bar is first realised 8^() """ for group in self.groups: checked = False for item in group.items: # If the group is a radio group, set the initial checked state # of every tool in it. if item.action.style == "radio": if item.control_id is not None: # Only set checked state if control has been created. # Using extra_actions of tasks, it appears that this # may be called multiple times. tool_bar.ToggleTool( item.control_id, item.action.checked ) checked = checked or item.action.checked # Every item in a radio group MUST be 'radio' style, so we # can just skip to the next group. else: break # We get here if the group is a radio group. else: # If none of the actions in the group is specified as 'checked' # we will check the first one. if not checked and len(group.items) > 0: group.items[0].action.checked = True class _ToolBar(wx.ToolBar): """ The toolkit-specific tool bar implementation. """ # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, tool_bar_manager, parent, id, style): """ Constructor. """ wx.ToolBar.__init__(self, parent, -1, style=style) # Listen for changes to the tool bar manager's enablement and # visibility. self.tool_bar_manager = tool_bar_manager self.tool_bar_manager.observe( self._on_tool_bar_manager_enabled_changed, "enabled" ) self.tool_bar_manager.observe( self._on_tool_bar_manager_visible_changed, "visible" ) return # ------------------------------------------------------------------------ # Trait change handlers. # ------------------------------------------------------------------------ def _on_tool_bar_manager_enabled_changed(self, event): """ Dynamic trait change handler. """ event.object.window._wx_enable_tool_bar(self, event.new) def _on_tool_bar_manager_visible_changed(self, event): """ Dynamic trait change handler. """ event.object.window._wx_show_tool_bar(self, event.new) class _AuiToolBar(AUI.AuiToolBar): """ The toolkit-specific tool bar implementation for AUI windows. """ # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, tool_bar_manager, parent, id, style, agwStyle): """ Constructor. """ super().__init__(parent, -1, style=style, agwStyle=agwStyle) # Listen for changes to the tool bar manager's enablement and # visibility. self.tool_bar_manager = tool_bar_manager self.tool_bar_manager.observe( self._on_tool_bar_manager_enabled_changed, "enabled" ) self.tool_bar_manager.observe( self._on_tool_bar_manager_visible_changed, "visible" ) # we need to defer hiding tools until first time Realize is called so # we can get the correct order of the toolbar for reinsertion at the # correct position self.initially_hidden_tool_ids = [] # map of tool ids to a tuple: position in full toolbar and the # ToolBarTool itself. Can't keep a weak reference here because once # removed from the toolbar the item would be garbage collected. self.tool_map = {} def Realize(self): if len(self.tool_map) == 0: for pos in range(self.GetToolsCount()): tool = self.GetToolByPos(pos) self.tool_map[tool.GetId()] = (pos, tool) AUI.AuiToolBar.Realize(self) if len(self.initially_hidden_tool_ids) > 0: for tool_id in self.initially_hidden_tool_ids: self.RemoveTool(tool_id) self.initially_hidden_tool_ids = [] self.ShowTool = self.ShowToolPostRealize def ShowTool(self, tool_id, state): """Used before realization to flag which need to be initially hidden """ if not state: self.initially_hidden_tool_ids.append(tool_id) def ShowToolPostRealize(self, tool_id, state): """Normal ShowTool method, activated after first call to Realize """ tool = self.FindById(tool_id) if state and tool is None: self.InsertToolInOrder(tool_id) self.EnableTool(tool_id, True) self.Realize() # Update the toolbar in the AUI manager to force toolbar resize try: wx.CallAfter( self.tool_bar_manager.controller.task.window._aui_manager.Update ) except: pass elif not state and tool is not None: self.RemoveTool(tool_id) # Update the toolbar in the AUI manager to force toolbar resize try: wx.CallAfter( self.tool_bar_manager.controller.task.window._aui_manager.Update ) except: pass def InsertToolInOrder(self, tool_id): orig_pos, tool = self.tool_map[tool_id] pos = -1 for pos in range(self.GetToolsCount()): existing_orig_pos, _ = self.tool_map[tool_id] if existing_orig_pos > orig_pos: break self.InsertToolItem(pos + 1, tool) ## Additional convenience functions for the normal AGW AUI toolbar def AddLabelTool( self, id, label, bitmap, bmpDisabled, kind, shortHelp, longHelp, clientData, ): "The full AddTool() function." return self.AddTool( id, label, bitmap, bmpDisabled, kind, shortHelp, longHelp, clientData, None, ) def InsertToolItem(self, pos, tool): self._items[pos:pos] = [tool] return tool def DeleteTool(self, tool_id): """ Removes the specified tool from the toolbar and deletes it. :param integer `tool_id`: the :class:`AuiToolBarItem` identifier. :returns: ``True`` if the tool was deleted, ``False`` otherwise. :note: Note that it is unnecessary to call :meth:`Realize` for the change to take place, it will happen immediately. """ tool = self.RemoveTool(tool_id) if tool is not None: tool.Destroy() return True return False def RemoveTool(self, tool_id): """ Removes the specified tool from the toolbar but doesn't delete it. :param integer `tool_id`: the :class:`AuiToolBarItem` identifier. :returns: ``True`` if the tool was deleted, ``False`` otherwise. :note: Note that it is unnecessary to call :meth:`Realize` for the change to take place, it will happen immediately. """ idx = self.GetToolIndex(tool_id) if idx >= 0 and idx < len(self._items): self._items.pop(idx) self.Realize() return True return False FindById = AUI.AuiToolBar.FindTool GetToolState = AUI.AuiToolBar.GetToolToggled GetToolsCount = AUI.AuiToolBar.GetToolCount def GetToolByPos(self, pos): return self._items[pos] def OnSize(self, event): # Quickly short-circuit if the toolbar isn't realized if not hasattr(self, "_absolute_min_size"): return AUI.AuiToolBar.OnSize(self, event) # ------------------------------------------------------------------------ # Trait change handlers. # ------------------------------------------------------------------------ def _on_tool_bar_manager_enabled_changed(self, event): """ Dynamic trait change handler. """ try: event.object.controller.task.window._wx_enable_tool_bar( self, event.new ) except: pass def _on_tool_bar_manager_visible_changed(self, event): """ Dynamic trait change handler. """ try: event.object.controller.task.window._wx_show_tool_bar( self, event.new ) except: pass return pyface-7.4.0/pyface/ui/wx/action/menu_manager.py0000644000076500000240000001531414176222673022557 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 wx specific implementation of a menu manager. """ import wx from traits.api import Str, Bool from pyface.action.action_manager import ActionManager from pyface.action.action_manager_item import ActionManagerItem from pyface.action.group import Group class MenuManager(ActionManager, ActionManagerItem): """ A menu manager realizes itself in a menu control. This could be a sub-menu or a context (popup) menu. """ # 'MenuManager' interface ---------------------------------------------# # The menu manager's name (if the manager is a sub-menu, this is what its # label will be). name = Str() # Does the menu require a separator before the menu item name? separator = Bool(True) # ------------------------------------------------------------------------ # 'MenuManager' interface. # ------------------------------------------------------------------------ def create_menu(self, parent, controller=None): """ Creates a menu representation of the manager. """ # If a controller is required it can either be set as a trait on the # menu manager (the trait is part of the 'ActionManager' API), or # passed in here (if one is passed in here it takes precedence over the # trait). if controller is None: controller = self.controller return _Menu(self, parent, controller) # ------------------------------------------------------------------------ # 'ActionManagerItem' interface. # ------------------------------------------------------------------------ def add_to_menu(self, parent, menu, controller): """ Adds the item to a menu. """ sub = self.create_menu(parent, controller) id = sub.GetId() # fixme: Nasty hack to allow enabling/disabling of menus. sub._id = id sub._menu = menu menu.Append(id, self.name, sub) def add_to_toolbar(self, parent, tool_bar, image_cache, controller): """ Adds the item to a tool bar. """ raise ValueError("Cannot add a menu manager to a toolbar.") class _Menu(wx.Menu): """ The toolkit-specific menu control. """ # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, manager, parent, controller): """ Creates a new tree. """ # Base class constructor. wx.Menu.__init__(self) # The parent of the menu. self._parent = parent # The manager that the menu is a view of. self._manager = manager # The controller. self._controller = controller # List of menu items self.menu_items = [] # Create the menu structure. self.refresh() # Listen to the manager being updated. self._manager.observe(self.refresh, "changed") self._manager.observe(self._on_enabled_changed, "enabled") return def dispose(self): self._manager.observe(self.refresh, "changed", remove=True) self._manager.observe(self._on_enabled_changed, "enabled", remove=True) # Removes event listeners from downstream menu items self.clear() # ------------------------------------------------------------------------ # '_Menu' interface. # ------------------------------------------------------------------------ def clear(self): """ Clears the items from the menu. """ for item in self.GetMenuItems(): if item.GetSubMenu() is not None: item.GetSubMenu().clear() self.Delete(item.GetId()) for item in self.menu_items: item.dispose() self.menu_items = [] def is_empty(self): """ Is the menu empty? """ return self.GetMenuItemCount() == 0 def refresh(self, event=None): """ Ensures that the menu reflects the state of the manager. """ self.clear() manager = self._manager parent = self._parent previous_non_empty_group = None for group in manager.groups: previous_non_empty_group = self._add_group( parent, group, previous_non_empty_group ) def show(self, x=None, y=None): """ Show the menu at the specified location. """ if x is None or y is None: self._parent.PopupMenu(self) else: self._parent.PopupMenu(self, x, y) return # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _on_enabled_changed(self, event): """ Dynamic trait change handler. """ # fixme: Nasty hack to allow enabling/disabling of menus. # # We cannot currently (AFAIK) disable menus on the menu bar. Hence # we don't give them an '_id'... if hasattr(self, "_id"): self._menu.Enable(self._id, event.new) def _add_group(self, parent, group, previous_non_empty_group=None): """ Adds a group to a menu. """ if len(group.items) > 0: # Is a separator required? if previous_non_empty_group is not None and group.separator: self.AppendSeparator() # Create actions and sub-menus for each contribution item in # the group. for item in group.items: if isinstance(item, Group): if len(item.items) > 0: self._add_group(parent, item, previous_non_empty_group) if ( previous_non_empty_group is not None and previous_non_empty_group.separator and item.separator ): self.AppendSeparator() previous_non_empty_group = item else: if isinstance(item, MenuManager): if item.separator: self.AppendSeparator() previous_non_empty_group = item item.add_to_menu(parent, self, self._controller) previous_non_empty_group = group return previous_non_empty_group pyface-7.4.0/pyface/ui/wx/action/tool_palette_manager.py0000644000076500000240000001106314176222673024303 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 tool bar manager realizes itself in a tool palette control. """ from traits.api import Bool, Instance, Tuple from pyface.image_cache import ImageCache from pyface.action.action_manager import ActionManager from .tool_palette import ToolPalette class ToolPaletteManager(ActionManager): """ A tool bar manager realizes itself in a tool palette bar control. """ # 'ToolPaletteManager' interface --------------------------------------- # The size of tool images (width, height). image_size = Tuple((16, 16)) # Should we display the name of each tool bar tool under its image? show_tool_names = Bool(True) # Private interface ---------------------------------------------------- # Cache of tool images (scaled to the appropriate size). _image_cache = Instance(ImageCache) # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, *args, **traits): """ Creates a new tool bar manager. """ # Base class contructor. super().__init__(*args, **traits) # An image cache to make sure that we only load each image used in the # tool bar exactly once. self._image_cache = ImageCache(self.image_size[0], self.image_size[1]) return # ------------------------------------------------------------------------ # 'ToolPaletteManager' interface. # ------------------------------------------------------------------------ def create_tool_palette(self, parent, controller=None): """ Creates a tool bar. """ # Create the control. tool_palette = ToolPalette(parent) # Add all of items in the manager's groups to the tool bar. self._add_tools(tool_palette, self.groups) self._set_initial_tool_state(tool_palette, self.groups) return tool_palette # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _add_tools(self, tool_palette, groups): """ Adds tools for all items in a list of groups. """ # previous_non_empty_group = None for group in self.groups: if len(group.items) > 0: # Is a separator required? ## FIXME : Does the palette need the notion of a separator? ## if previous_non_empty_group is not None and group.separator: ## tool_bar.AddSeparator() ## ## previous_non_empty_group = group # Create a tool bar tool for each item in the group. for item in group.items: control_id = item.add_to_palette( tool_palette, self._image_cache, self.show_tool_names ) item.control_id = control_id tool_palette.realize() def _set_initial_tool_state(self, tool_palette, groups): """ Workaround for the wxPython tool bar bug. Without this, only the first item in a radio group can be selected when the tool bar is first realised 8^() """ for group in groups: checked = False for item in group.items: # If the group is a radio group, set the initial checked state # of every tool in it. if item.action.style == "radio": tool_palette.toggle_tool( item.control_id, item.action.checked ) checked = checked or item.action.checked # Every item in a radio group MUST be 'radio' style, so we # can just skip to the next group. else: break # We get here if the group is a radio group. else: # If none of the actions in the group is specified as 'checked' # we will check the first one. if not checked and len(group.items) > 0: group.items[0].action.checked = True return pyface-7.4.0/pyface/ui/wx/action/status_bar_manager.py0000644000076500000240000000642614176222673023766 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 status bar manager realizes itself in a status bar control. """ import wx from traits.api import Any, HasTraits, List, Property, Str class StatusBarManager(HasTraits): """ A status bar manager realizes itself in a status bar control. """ # The message displayed in the first field of the status bar. message = Property # The messages to be displayed in the status bar fields. messages = List(Str) # The toolkit-specific control that represents the status bar. status_bar = Any() # ------------------------------------------------------------------------ # 'StatusBarManager' interface. # ------------------------------------------------------------------------ def create_status_bar(self, parent): """ Creates a status bar. """ if self.status_bar is None: self.status_bar = wx.StatusBar(parent) self.status_bar._pyface_control = self if len(self.messages) > 1: self.status_bar.SetFieldsCount(len(self.messages)) for i in range(len(self.messages)): self.status_bar.SetStatusText(self.messages[i], i) else: self.status_bar.SetStatusText(self.message) return self.status_bar def remove_status_bar(self, parent): """ Removes a status bar. """ if self.status_bar is not None: self.status_bar.Destroy() self.status_bar._pyface_control = None self.status_bar = None # ------------------------------------------------------------------------ # Property handlers. # ------------------------------------------------------------------------ def _get_message(self): """ Property getter. """ if len(self.messages) > 0: message = self.messages[0] else: message = "" return message def _set_message(self, value): """ Property setter. """ if len(self.messages) > 0: old = self.messages[0] self.messages[0] = value else: old = "" self.messages.append(value) self.trait_property_changed("message", old, value) return # ------------------------------------------------------------------------ # Trait event handlers. # ------------------------------------------------------------------------ def _messages_changed(self): """ Sets the text displayed on the status bar. """ if self.status_bar is not None: for i in range(len(self.messages)): self.status_bar.SetStatusText(self.messages[i], i) def _messages_items_changed(self): """ Sets the text displayed on the status bar. """ if self.status_bar is not None: for i in range(len(self.messages)): self.status_bar.SetStatusText(self.messages[i], i) return pyface-7.4.0/pyface/ui/wx/action/__init__.py0000644000076500000240000000062714176222673021661 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! pyface-7.4.0/pyface/ui/wx/action/tool_palette.py0000644000076500000240000001313314176222673022611 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ View of an ActionManager drawn as a rectangle of buttons. """ import wx from pyface.widget import Widget from traits.api import Bool, Dict, Int, List, Tuple # HTML templates. # FIXME : Not quite the right color. HTML = """ %s """ PART = """""" class ToolPalette(Widget): tools = List() id_tool_map = Dict() tool_id_to_button_map = Dict() button_size = Tuple((25, 25), Int, Int) is_realized = Bool(False) tool_listeners = Dict() # Maps a button id to its tool id. button_tool_map = Dict() # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, parent, **traits): """ Creates a new tool palette. """ # Base class constructor. super().__init__(**traits) # Create the toolkit-specific control that represents the widget. self.control = self._create_control(parent) return # ------------------------------------------------------------------------ # ToolPalette interface. # ------------------------------------------------------------------------ def add_tool(self, label, bmp, kind, tooltip, longtip): """ Add a tool with the specified properties to the palette. Return an id that can be used to reference this tool in the future. """ wxid = wx.NewIdRef() params = (wxid, label, bmp, kind, tooltip, longtip) self.tools.append(params) self.id_tool_map[wxid] = params if self.is_realized: self._reflow() return wxid def toggle_tool(self, id, checked): """ Toggle the tool identified by 'id' to the 'checked' state. If the button is a toggle or radio button, the button will be checked if the 'checked' parameter is True; unchecked otherwise. If the button is a standard button, this method is a NOP. """ button = self.tool_id_to_button_map.get(id, None) if button is not None and hasattr(button, "SetToggle"): button.SetToggle(checked) def enable_tool(self, id, enabled): """ Enable or disable the tool identified by 'id'. """ button = self.tool_id_to_button_map.get(id, None) if button is not None: button.SetEnabled(enabled) def on_tool_event(self, id, callback): """ Register a callback for events on the tool identified by 'id'. """ callbacks = self.tool_listeners.setdefault(id, []) callbacks.append(callback) def realize(self): """ Realize the control so that it can be displayed. """ self.is_realized = True self._reflow() def get_tool_state(self, id): """ Get the toggle state of the tool identified by 'id'. """ button = self.tool_id_to_button_map.get(id, None) if hasattr(button, "GetToggle"): if button.GetToggle(): state = 1 else: state = 0 else: state = 0 return state # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _create_control(self, parent): html_window = wx.html.HtmlWindow(parent, -1, style=wx.CLIP_CHILDREN) return html_window def _reflow(self): """ Reflow the layout. """ # Create a bit of html for each tool. parts = [] for param in self.tools: parts.append(PART % (str(param[0]), self.button_size)) # Create the entire html page. html = HTML % "".join(parts) # Set the HTML on the widget. This will create all of the buttons. self.control.SetPage(html) for param in self.tools: self._initialize_tool(param) def _initialize_tool(self, param): """ Initialize the tool palette button. """ wxid, label, bmp, kind, tooltip, longtip = param panel = self.control.FindWindowById(wxid) sizer = wx.BoxSizer(wx.VERTICAL) panel.SetSizer(sizer) panel.SetAutoLayout(True) panel.SetWindowStyleFlag(wx.CLIP_CHILDREN) from wx.lib.buttons import GenBitmapToggleButton, GenBitmapButton if kind == "radio": button = GenBitmapToggleButton( panel, -1, None, size=self.button_size ) else: button = GenBitmapButton(panel, -1, None, size=self.button_size) self.button_tool_map[button.GetId()] = wxid self.tool_id_to_button_map[wxid] = button panel.Bind(wx.EVT_BUTTON, self._on_button, button) button.SetBitmapLabel(bmp) button.SetToolTip(label) sizer.Add(button, 0, wx.EXPAND) def _on_button(self, event): button_id = event.GetId() tool_id = self.button_tool_map.get(button_id, None) if tool_id is not None: for listener in self.tool_listeners.get(tool_id, []): listener(event) return pyface-7.4.0/pyface/ui/wx/action/action_item.py0000644000076500000240000005552414176222673022423 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 wx specific implementations the action manager internal classes. """ from inspect import getfullargspec import wx from traits.api import Any, Bool, HasTraits from pyface.action.action_event import ActionEvent _STYLE_TO_KIND_MAP = { "push": wx.ITEM_NORMAL, "radio": wx.ITEM_RADIO, "toggle": wx.ITEM_CHECK, "widget": None, } class _MenuItem(HasTraits): """ A menu item representation of an action item. """ # '_MenuItem' interface ------------------------------------------------ # Is the item checked? checked = Bool(False) # A controller object we delegate taking actions through (if any). controller = Any() # Is the item enabled? enabled = Bool(True) # Is the item visible? visible = Bool(True) # The radio group we are part of (None if the menu item is not part of such # a group). group = Any() # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, parent, menu, item, controller): """ Creates a new menu item for an action item. """ self.item = item # Create an appropriate menu item depending on the style of the action. # # N.B. Don't try to use -1 as the Id for the menu item... wx does not # like it! action = item.action label = action.name kind = _STYLE_TO_KIND_MAP[action.style] longtip = action.description or action.tooltip if action.style == "widget": raise NotImplementedError( "WxPython does not support widgets in menus" ) if len(action.accelerator) > 0: label = label + "\t" + action.accelerator # This just helps with debugging when people forget to specify a name # for their action (without this wx just barfs which is not very # helpful!). if len(label) == 0: label = item.action.__class__.__name__ if getattr(action, "menu_role", False): if action.menu_role == "About": self.control_id = wx.ID_ABOUT elif action.menu_role == "Preferences": self.control_id = wx.ID_PREFERENCES elif action.menu_role == "Quit": self.control_id = wx.ID_EXIT else: self.control_id = wx.ID_ANY self.control = wx.MenuItem(menu, self.control_id, label, longtip, kind) # If the action has an image then display it. if action.image is not None: try: self.control.SetBitmap(action.image.create_bitmap()) except Exception: # Some platforms don't allow radio buttons to have # bitmaps, so just ignore the exception if it happens pass menu.Append(self.control) menu.menu_items.append(self) # Set the initial enabled/disabled state of the action. self.control.Enable(action.enabled and action.visible) # Set the initial checked state. if action.style in ["radio", "toggle"]: self.control.Check(action.checked) # Wire it up...create an ugly flag since some platforms dont skip the # event when we thought they would self._skip_menu_event = False parent.Bind(wx.EVT_MENU, self._on_menu, self.control) # Listen for trait changes on the action (so that we can update its # enabled/disabled/checked state etc). action.observe(self._on_action_enabled_changed, "enabled") action.observe(self._on_action_visible_changed, "visible") action.observe(self._on_action_checked_changed, "checked") action.observe(self._on_action_name_changed, "name") action.observe(self._on_action_image_changed, "image") if controller is not None: self.controller = controller controller.add_to_menu(self) def dispose(self): action = self.item.action action.observe(self._on_action_enabled_changed, "enabled", remove=True) action.observe(self._on_action_visible_changed, "visible", remove=True) action.observe(self._on_action_checked_changed, "checked", remove=True) action.observe(self._on_action_name_changed, "name", remove=True) action.observe(self._on_action_image_changed, "image", remove=True) # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ # Trait event handlers ------------------------------------------------- def _enabled_changed(self): """ Called when our 'enabled' trait is changed. """ self.control.Enable(self.enabled and self.visible) def _visible_changed(self): """ Called when our 'visible' trait is changed. """ self.control.Enable(self.visible and self.enabled) def _checked_changed(self): """ Called when our 'checked' trait is changed. """ if self.item.action.style == "radio": # fixme: Not sure why this is even here, we had to guard it to # make it work? Must take a look at svn blame! # FIXME v3: Note that menu_checked() doesn't seem to exist, so we # comment it out and do the following instead. # if self.group is not None: # self.group.menu_checked(self) # If we're turning this one on, then we need to turn all the others # off. But if we're turning this one off, don't worry about the # others. if self.checked: for item in self.item.parent.items: if item is not self.item: item.action.checked = False self.control.Check(self.checked) def _on_action_enabled_changed(self, event): """ Called when the enabled trait is changed on an action. """ action = event.object self.control.Enable(action.enabled and action.visible) def _on_action_visible_changed(self, event): """ Called when the visible trait is changed on an action. """ action = event.object self.control.Enable(action.visible and action.enabled) def _on_action_checked_changed(self, event): """ Called when the checked trait is changed on an action. """ action = event.object if self.item.action.style == "radio": # fixme: Not sure why this is even here, we had to guard it to # make it work? Must take a look at svn blame! # FIXME v3: Note that menu_checked() doesn't seem to exist, so we # comment it out and do the following instead. # if self.group is not None: # self.group.menu_checked(self) # If we're turning this one on, then we need to turn all the others # off. But if we're turning this one off, don't worry about the # others. if action.checked: for item in self.item.parent.items: if item is not self.item: item.action.checked = False # This will *not* emit a menu event because of this ugly flag self._skip_menu_event = True self.control.Check(action.checked) self._skip_menu_event = False def _on_action_name_changed(self, event): """ Called when the name trait is changed on an action. """ action = event.object label = action.name if len(action.accelerator) > 0: label = label + "\t" + action.accelerator self.control.SetText(label) def _on_action_image_changed(self, event): """ Called when the name trait is changed on an action. """ action = event.object if self.control is not None: self.control.SetIcon(action.image.create_icon()) return # wx event handlers ---------------------------------------------------- def _on_menu(self, event): """ Called when the menu item is clicked. """ # if the ugly flag is set, do not perform the menu event if self._skip_menu_event: return action = self.item.action action_event = ActionEvent() is_checkable = action.style in ["radio", "toggle"] # Perform the action! if self.controller is not None: if is_checkable: # fixme: There is a difference here between having a controller # and not in that in this case we do not set the checked state # of the action! This is confusing if you start off without a # controller and then set one as the action now behaves # differently! self.checked = self.control.IsChecked() == 1 # Most of the time, action's do no care about the event (it # contains information about the time the event occurred etc), so # we only pass it if the perform method requires it. This is also # useful as Traits UI controllers *never* require the event. argspec = getfullargspec(self.controller.perform) # If the only arguments are 'self' and 'action' then don't pass # the event! if len(argspec.args) == 2: self.controller.perform(action) else: self.controller.perform(action, action_event) else: if is_checkable: action.checked = self.control.IsChecked() == 1 # Most of the time, action's do no care about the event (it # contains information about the time the event occurred etc), so # we only pass it if the perform method requires it. argspec = getfullargspec(action.perform) # If the only argument is 'self' then don't pass the event! if len(argspec.args) == 1: action.perform() else: action.perform(action_event) class _Tool(HasTraits): """ A tool bar tool representation of an action item. """ # '_Tool' interface ---------------------------------------------------- # Is the item checked? checked = Bool(False) # A controller object we delegate taking actions through (if any). controller = Any() # Is the item enabled? enabled = Bool(True) # Is the item visible? visible = Bool(True) # The radio group we are part of (None if the tool is not part of such a # group). group = Any() # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__( self, parent, tool_bar, image_cache, item, controller, show_labels ): """ Creates a new tool bar tool for an action item. """ self.item = item self.tool_bar = tool_bar # Create an appropriate tool depending on the style of the action. action = self.item.action label = action.name # Tool bar tools never have '...' at the end! if label.endswith("..."): label = label[:-3] # And they never contain shortcuts. label = label.replace("&", "") # If the action has an image then convert it to a bitmap (as required # by the toolbar). if action.image is not None: image = action.image.create_image( self.tool_bar.GetToolBitmapSize() ) path = action.image.absolute_path bmp = image_cache.get_bitmap(path) else: from pyface.api import ImageResource image = ImageResource("image_not_found") bmp = image.create_bitmap() kind = _STYLE_TO_KIND_MAP[action.style] tooltip = action.tooltip longtip = action.description if not show_labels: label = "" else: self.tool_bar.SetSize((-1, 50)) if action.style == "widget": widget = action.create_control(self.tool_bar) self.control = tool_bar.AddControl(widget, label) self.control_id = self.control.GetId() else: self.control = tool_bar.AddTool( wx.ID_ANY, label, bmp, wx.NullBitmap, kind, tooltip, longtip, None, ) self.control_id = self.control.GetId() # Set the initial checked state. tool_bar.ToggleTool(self.control_id, action.checked) if hasattr(tool_bar, "ShowTool"): # Set the initial enabled/disabled state of the action. tool_bar.EnableTool(self.control_id, action.enabled) # Set the initial visibility tool_bar.ShowTool(self.control_id, action.visible) else: # Set the initial enabled/disabled state of the action. tool_bar.EnableTool( self.control_id, action.enabled and action.visible ) # Wire it up. parent.Bind(wx.EVT_TOOL, self._on_tool, self.control) # Listen for trait changes on the action (so that we can update its # enabled/disabled/checked state etc). action.observe(self._on_action_enabled_changed, "enabled") action.observe(self._on_action_visible_changed, "visible") action.observe(self._on_action_checked_changed, "checked") if controller is not None: self.controller = controller controller.add_to_toolbar(self) def dispose(self): action = self.item.action action.observe(self._on_action_enabled_changed, "enabled", remove=True) action.observe(self._on_action_visible_changed, "visible", remove=True) action.observe(self._on_action_checked_changed, "checked", remove=True) # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ # Trait event handlers ------------------------------------------------- def _enabled_changed(self): """ Called when our 'enabled' trait is changed. """ if hasattr(self.tool_bar, "ShowTool"): self.tool_bar.EnableTool(self.control_id, self.enabled) else: self.tool_bar.EnableTool( self.control_id, self.enabled and self.visible ) def _visible_changed(self): """ Called when our 'visible' trait is changed. """ if hasattr(self.tool_bar, "ShowTool"): self.tool_bar.ShowTool(self.control_id, self.visible) else: self.tool_bar.EnableTool( self.control_id, self.enabled and self.visible ) def _checked_changed(self): """ Called when our 'checked' trait is changed. """ if self.item.action.style == "radio": # FIXME v3: Note that toolbar_checked() doesn't seem to exist, so # we comment it out and do the following instead. # self.group.toolbar_checked(self) # If we're turning this one on, then we need to turn all the others # off. But if we're turning this one off, don't worry about the # others. if self.checked: for item in self.item.parent.items: if item is not self.item: item.action.checked = False self.tool_bar.ToggleTool(self.control_id, self.checked) def _on_action_enabled_changed(self, event): """ Called when the enabled trait is changed on an action. """ action = event.object if hasattr(self.tool_bar, "ShowTool"): self.tool_bar.EnableTool(self.control_id, action.enabled) else: self.tool_bar.EnableTool( self.control_id, action.enabled and action.visible ) def _on_action_visible_changed(self, event): """ Called when the visible trait is changed on an action. """ action = event.object if hasattr(self.tool_bar, "ShowTool"): self.tool_bar.ShowTool(self.control_id, action.visible) else: self.tool_bar.EnableTool( self.control_id, self.enabled and action.visible ) def _on_action_checked_changed(self, event): """ Called when the checked trait is changed on an action. """ action = event.object if action.style == "radio": # If we're turning this one on, then we need to turn all the others # off. But if we're turning this one off, don't worry about the # others. if event.new: for item in self.item.parent.items: if item is not self.item: item.action.checked = False # This will *not* emit a tool event. self.tool_bar.ToggleTool(self.control_id, event.new) return # wx event handlers ---------------------------------------------------- def _on_tool(self, event): """ Called when the tool bar tool is clicked. """ action = self.item.action action_event = ActionEvent() # Perform the action! if self.controller is not None: # fixme: There is a difference here between having a controller # and not in that in this case we do not set the checked state # of the action! This is confusing if you start off without a # controller and then set one as the action now behaves # differently! self.checked = self.tool_bar.GetToolState(self.control_id) == 1 # Most of the time, action's do no care about the event (it # contains information about the time the event occurred etc), so # we only pass it if the perform method requires it. This is also # useful as Traits UI controllers *never* require the event. argspec = getfullargspec(self.controller.perform) # If the only arguments are 'self' and 'action' then don't pass # the event! if len(argspec.args) == 2: self.controller.perform(action) else: self.controller.perform(action, action_event) else: action.checked = self.tool_bar.GetToolState(self.control_id) == 1 # Most of the time, action's do no care about the event (it # contains information about the time the event occurred etc), so # we only pass it if the perform method requires it. argspec = getfullargspec(action.perform) # If the only argument is 'self' then don't pass the event! if len(argspec.args) == 1: action.perform() else: action.perform(action_event) class _PaletteTool(HasTraits): """ A tool palette representation of an action item. """ # '_PaletteTool' interface --------------------------------------------- # The radio group we are part of (None if the tool is not part of such a # group). group = Any() # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, tool_palette, image_cache, item, show_labels): """ Creates a new tool palette tool for an action item. """ self.item = item self.tool_palette = tool_palette action = self.item.action label = action.name if action.style == "widget": raise NotImplementedError( "WxPython does not support widgets in palettes" ) # Tool palette tools never have '...' at the end. if label.endswith("..."): label = label[:-3] # And they never contain shortcuts. label = label.replace("&", "") path = action.image.absolute_path bmp = image_cache.get_bitmap(path) kind = action.style tooltip = action.tooltip longtip = action.description if not show_labels: label = "" # Add the tool to the tool palette. self.tool_id = tool_palette.add_tool( label, bmp, kind, tooltip, longtip ) tool_palette.toggle_tool(self.tool_id, action.checked) tool_palette.enable_tool(self.tool_id, action.enabled) tool_palette.on_tool_event(self.tool_id, self._on_tool) # Listen to the trait changes on the action (so that we can update its # enabled/disabled/checked state etc). action.observe(self._on_action_enabled_changed, "enabled") action.observe(self._on_action_checked_changed, "checked") return def dispose(self): action = self.item.action action.observe(self._on_action_enabled_changed, "enabled", remove=True) action.observe(self._on_action_checked_changed, "checked", remove=True) # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ # Trait event handlers ------------------------------------------------- def _on_action_enabled_changed(self, event): """ Called when the enabled trait is changed on an action. """ action = event.object self.tool_palette.enable_tool(self.tool_id, action.enabled) def _on_action_checked_changed(self, event): """ Called when the checked trait is changed on an action. """ action = event.object if action.style == "radio": # If we're turning this one on, then we need to turn all the others # off. But if we're turning this one off, don't worry about the # others. if event.new: for item in self.item.parent.items: if item is not self.item: item.action.checked = False # This will *not* emit a tool event. self.tool_palette.toggle_tool(self.tool_id, event.new) return # Tool palette event handlers -----------------------------------------# def _on_tool(self, event): """ Called when the tool palette button is clicked. """ action = self.item.action action_event = ActionEvent() # Perform the action! action.checked = self.tool_palette.get_tool_state(self.tool_id) == 1 action.perform(action_event) return pyface-7.4.0/pyface/ui/wx/action/menu_bar_manager.py0000644000076500000240000000307414176222673023403 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 wx specific implementation of a menu bar manager. """ import wx from pyface.action.action_manager import ActionManager class MenuBarManager(ActionManager): """ A menu bar manager realizes itself in errr, a menu bar control. """ # ------------------------------------------------------------------------ # 'MenuBarManager' interface. # ------------------------------------------------------------------------ def create_menu_bar(self, parent, controller=None): """ Creates a menu bar representation of the manager. """ # If a controller is required it can either be set as a trait on the # menu bar manager (the trait is part of the 'ActionManager' API), or # passed in here (if one is passed in here it takes precedence over the # trait). if controller is None: controller = self.controller menu_bar = wx.MenuBar() # Every item in every group must be a menu manager. for group in self.groups: for item in group.items: menu = item.create_menu(parent, controller) menu_bar.Append(menu, item.name) return menu_bar pyface-7.4.0/pyface/ui/wx/viewer/0000755000076500000240000000000014176460551017566 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/wx/viewer/table_viewer.py0000644000076500000240000003027414176222673022617 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 viewer for tabular data. """ import warnings import wx from traits.api import Event, Instance, Int, Tuple from pyface.ui.wx.image_list import ImageList from pyface.ui_traits import TraitsUIColor as Color from pyface.viewer.content_viewer import ContentViewer from pyface.viewer.table_column_provider import TableColumnProvider from pyface.viewer.table_content_provider import TableContentProvider from pyface.viewer.table_label_provider import TableLabelProvider version_4_0 = (wx.VERSION < (4, 1)) class TableViewer(ContentViewer): """ A viewer for tabular data. """ # The content provider provides the actual table data. content_provider = Instance(TableContentProvider) # The label provider provides, err, the labels for the items in the table # (a label can have text and/or an image). label_provider = Instance(TableLabelProvider, ()) # The column provider provides information about the columns in the table # (column headers, width etc). column_provider = Instance(TableColumnProvider, ()) # The colours used to render odd and even numbered rows. even_row_background = Color("white") odd_row_background = Color((245, 245, 255)) # A row has been selected. row_selected = Event() # A row has been activated. row_activated = Event() # A drag operation was started on a node. row_begin_drag = Event() # The size of the icons in the table. _image_size = Tuple(Int, Int) def __init__(self, parent, image_size=(16, 16), **traits): """ Creates a new table viewer. 'parent' is the toolkit-specific control that is the table's parent. 'image_size' is a tuple in the form (int width, int height) that specifies the size of the images (if any) displayed in the table. """ create = traits.pop('create', True) # Base class constructors. super().__init__(parent=parent, _image_size=image_size, **traits) if create: self.create() warnings.warn( "automatic widget creation is deprecated and will be removed " "in a future Pyface version, use create=False and explicitly " "call create() for future behaviour", PendingDeprecationWarning, ) def _create_control(self, parent): # Create the toolkit-specific control. self.control = table = _Table(parent, self._image_size, self) # Table events. table.Bind(wx.EVT_LIST_ITEM_SELECTED, self._on_item_selected) table.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self._on_item_activated) table.Bind(wx.EVT_LIST_BEGIN_DRAG, self._on_list_begin_drag) table.Bind(wx.EVT_LIST_BEGIN_RDRAG, self._on_list_begin_rdrag) table.Bind( wx.EVT_LIST_BEGIN_LABEL_EDIT, self._on_list_begin_label_edit ) table.Bind(wx.EVT_LIST_END_LABEL_EDIT, self._on_list_end_label_edit) # fixme: Bug[732104] indicates that this event does not get fired # in a virtual list control (it *does* get fired in a regular list # control 8^(). table.Bind(wx.EVT_LIST_ITEM_DESELECTED, self._on_item_deselected) # Create the widget! self._create_widget(parent) # We use a dynamic handler instead of a static handler here, as we # don't want to react if the input is set in the constructor. self.observe(self._on_input_changed, "input") return table # ------------------------------------------------------------------------ # 'TableViewer' interface. # ------------------------------------------------------------------------ def select_row(self, row): """ Select the specified row. """ self.control.SetItemState( row, wx.LIST_STATE_SELECTED, wx.LIST_STATE_SELECTED ) self.control.SetItemState( row, wx.LIST_STATE_FOCUSED, wx.LIST_STATE_FOCUSED ) # Make sure that the selected row is visible. fudge = max(0, row - 5) self.EnsureVisible(fudge) # Trait event notification. self.row_selected = row return # ------------------------------------------------------------------------ # Trait event handlers. # ------------------------------------------------------------------------ def _on_input_changed(self, event): """ Called when the input is changed. """ # Update the table contents. self._update_contents() if event.old is None: self._update_column_widths() return # ------------------------------------------------------------------------ # wx event handlers. # ------------------------------------------------------------------------ def _on_item_selected(self, event): """ Called when an item in the list is selected. """ # Get the index of the row that was selected (nice wx interface huh?!). row = event.Index # Trait event notification. self.row_selected = row return # fixme: Bug[732104] indicates that this event does not get fired in a # virtual list control (it *does* get fired in a regular list control 8^(). def _on_item_deselected(self, event): """ Called when an item in the list is selected. """ # Trait event notification. self.row_selected = -1 def _on_item_activated(self, event): """ Called when an item in the list is activated. """ # Get the index of the row that was activated (nice wx interface!). row = event.Index # Trait event notification. self.row_activated = row def _on_list_begin_drag(self, event=None, is_rdrag=False): """ Called when a drag operation is starting on a list item. """ # Trait notification. self.row_begin_drag = event.GetIndex() def _on_list_begin_rdrag(self, event=None): """ Called when a drag operation is starting on a list item. """ self._on_list_begin_drag(event, True) def _on_list_begin_label_edit(self, event=None): """ Called when a label edit is started. """ event.Veto() def _on_list_end_label_edit(self, event=None): """ Called when a label edit is completed. """ return # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ FORMAT_MAP = { "left": wx.LIST_FORMAT_LEFT, "right": wx.LIST_FORMAT_RIGHT, "center": wx.LIST_FORMAT_CENTRE, "centre": wx.LIST_FORMAT_CENTRE, } def _create_widget(self, parent): """ Creates the widget. """ # Set up a default list item descriptor. info = wx.ListItem() info.m_mask = wx.LIST_MASK_TEXT | wx.LIST_MASK_FORMAT # Set the column headers. for index in range(self.column_provider.column_count): # Header text. info.m_text = self.column_provider.get_label(self, index) # Alignment of header text AND ALL cells in the column. alignment = self.column_provider.get_alignment(self, index) info.m_format = self.FORMAT_MAP.get(alignment, wx.LIST_FORMAT_LEFT) self.control.InsertColumn(index, info) # # Update the table contents and the column widths. self._update_contents() self._update_column_widths() def _update_contents(self): """ Updates the table content. """ self._elements = [] if self.input is not None: # Filtering... for element in self.content_provider.get_elements(self.input): for filter in self.filters: if not filter.select(self, self.input, element): break else: self._elements.append(element) # Sorting... if self.sorter is not None: self.sorter.sort(self, self.input, self._elements) # Setting this causes a refresh! self.control.SetItemCount(len(self._elements)) def _update_column_widths(self): """ Updates the column widths. """ # Set all columns to be the size of their largest item, or the size of # their header whichever is the larger. for column in range(self.control.GetColumnCount()): width = self.column_provider.get_width(self, column) if width == -1: width = self._get_column_width(column) self.control.SetColumnWidth(column, width) def _get_column_width(self, column): """ Return an appropriate width for the specified column. """ self.control.SetColumnWidth(column, wx.LIST_AUTOSIZE_USEHEADER) header_width = self.control.GetColumnWidth(column) if self.control.GetItemCount() == 0: width = header_width else: self.control.SetColumnWidth(column, wx.LIST_AUTOSIZE) data_width = self.control.GetColumnWidth(column) width = max(header_width, data_width) return width class _Table(wx.ListCtrl): # (ULC.UltimateListCtrl):# """ The wx control that we use to implement the table viewer. """ # Default style. STYLE = ( wx.LC_REPORT | wx.LC_HRULES | wx.LC_VRULES | wx.STATIC_BORDER | wx.LC_SINGLE_SEL | wx.LC_VIRTUAL | wx.LC_EDIT_LABELS | wx.CLIP_CHILDREN ) def __init__(self, parent, image_size, viewer): """ Creates a new table viewer. 'parent' is the toolkit-specific control that is the table's parent. 'image_size' is a tuple in the form (int width, int height) that specifies the size of the icons (if any) displayed in the table. """ # The vierer that we are providing the control for. self._viewer = viewer # Base-class constructor. wx.ListCtrl.__init__(self, parent, -1, style=self.STYLE) # Table item images. self._image_list = ImageList(image_size[0], image_size[1]) self.AssignImageList(self._image_list, wx.IMAGE_LIST_SMALL) # Set up attributes to show alternate rows with a different background # colour. if version_4_0: self._even_row_attribute = wx.ListItemAttr() else: self._even_row_attribute = wx.ItemAttr() self._even_row_attribute.SetBackgroundColour( self._viewer.even_row_background ) if version_4_0: self._odd_row_attribute = wx.ListItemAttr() else: self._odd_row_attribute = wx.ItemAttr() self._odd_row_attribute.SetBackgroundColour( self._viewer.odd_row_background ) return # ------------------------------------------------------------------------ # Virtual 'ListCtrl' interface. # ------------------------------------------------------------------------ def OnGetItemText(self, row, column_index): """ Returns the text for the specified CELL. """ viewer = self._viewer element = viewer._elements[row] return viewer.label_provider.get_text(viewer, element, column_index) def OnGetItemImage(self, row): """ Returns the image for the specified ROW. """ viewer = self._viewer element = viewer._elements[row] # Get the icon used to represent the node. image = viewer.label_provider.get_image(viewer, element) if image is not None: image_index = self._image_list.GetIndex(image.absolute_path) else: image_index = -1 return image_index def OnGetItemAttr(self, row): """ Returns the attribute for the specified row. """ if row % 2 == 0: attribute = self._even_row_attribute else: attribute = self._odd_row_attribute return attribute pyface-7.4.0/pyface/ui/wx/viewer/__init__.py0000644000076500000240000000000014176222673021666 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/wx/viewer/tree_viewer.py0000644000076500000240000005113114176222673022462 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 viewer based on a tree control. """ import warnings import wx from traits.api import Bool, Enum, Event, Instance, Int, List, Tuple from pyface.ui.wx.image_list import ImageList from pyface.viewer.content_viewer import ContentViewer from pyface.viewer.tree_content_provider import TreeContentProvider from pyface.viewer.tree_label_provider import TreeLabelProvider from pyface.wx.drag_and_drop import PythonDropSource class TreeViewer(ContentViewer): """ A viewer based on a tree control. """ # The default tree style. STYLE = wx.TR_EDIT_LABELS | wx.TR_HAS_BUTTONS | wx.CLIP_CHILDREN # 'TreeViewer' interface ----------------------------------------------- # The content provider provides the actual tree data. content_provider = Instance(TreeContentProvider) # The label provider provides, err, the labels for the items in the tree # (a label can have text and/or an image). label_provider = Instance(TreeLabelProvider, ()) # Selection mode (must be either of 'single' or 'extended'). selection_mode = Enum("single", "extended") # The currently selected elements. selection = List() # Should an image be shown for each element? show_images = Bool(True) # Should the root of the tree be shown? show_root = Bool(True) # Events ---- # An element has been activated (ie. double-clicked). element_activated = Event() # A drag operation was started on an element. element_begin_drag = Event() # An element that has children has been collapsed. element_collapsed = Event() # An element that has children has been expanded. element_expanded = Event() # A left-click occurred on an element. element_left_clicked = Event() # A right-click occurred on an element. element_right_clicked = Event() # A key was pressed while the tree is in focus. key_pressed = Event() # The size of the icons in the tree. _image_size = Tuple(Int, Int) # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, parent, image_size=(16, 16), **traits): """ Creates a new tree viewer. 'parent' is the toolkit-specific control that is the tree's parent. 'image_size' is a tuple in the form (int width, int height) that specifies the size of the label images (if any) displayed in the tree. """ create = traits.pop('create', True) # Base class constructors. super().__init__(parent=parent, _image_size=image_size, **traits) if create: self.create() warnings.warn( "automatic widget creation is deprecated and will be removed " "in a future Pyface version, use create=False and explicitly " "call create() for future behaviour", PendingDeprecationWarning, ) def _create_control(self, parent): # Create the toolkit-specific control. self.control = tree = wx.TreeCtrl(parent, -1, style=self._get_style()) # Wire up the wx tree events. tree.Bind(wx.EVT_CHAR, self._on_char) tree.Bind(wx.EVT_LEFT_DOWN, self._on_left_down) tree.Bind(wx.EVT_RIGHT_DOWN, self._on_right_down) tree.Bind(wx.EVT_TREE_ITEM_ACTIVATED, self._on_tree_item_activated) tree.Bind(wx.EVT_TREE_ITEM_COLLAPSED, self._on_tree_item_collapsed) tree.Bind(wx.EVT_TREE_ITEM_COLLAPSING, self._on_tree_item_collapsing) tree.Bind(wx.EVT_TREE_ITEM_EXPANDED, self._on_tree_item_expanded) tree.Bind(wx.EVT_TREE_ITEM_EXPANDING, self._on_tree_item_expanding) 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_BEGIN_DRAG, self._on_tree_begin_drag) tree.Bind(wx.EVT_TREE_SEL_CHANGED, self._on_tree_sel_changed) # The image list is a wxPython-ism that caches all images used in the # control. self._image_list = ImageList(*self._image_size) if self.show_images: tree.AssignImageList(self._image_list) # Mapping from element to wx tree item Ids. self._element_to_id_map = {} # Add the root item. if self.input is not None: self._add_element(None, self.input) return tree # ------------------------------------------------------------------------ # 'TreeViewer' interface. # ------------------------------------------------------------------------ def is_expanded(self, element): """ Returns True if the element is expanded, otherwise False. """ key = self._get_key(element) if key in self._element_to_id_map: is_expanded = self.control.IsExpanded(self._element_to_id_map[key]) else: is_expanded = False return is_expanded def is_selected(self, element): """ Returns True if the element is selected, otherwise False. """ key = self._get_key(element) if key in self._element_to_id_map: is_selected = self.control.IsSelected(self._element_to_id_map[key]) else: is_selected = False return is_selected def refresh(self, element): """ Refresh the tree starting from the specified element. Call this when the STRUCTURE of the content has changed. """ # Has the element actually appeared in the tree yet? pid = self._element_to_id_map.get(self._get_key(element), None) if pid is not None: # The item data is a tuple. The first element indicates whether or # not we have already populated the item with its children. The # second element is the actual item data. populated, element = self.control.GetItemData(pid) # fixme: We should find a cleaner way other than deleting all of # the element's children and re-adding them! self._delete_children(pid) self.control.SetItemData(pid, (False, element)) # Does the element have any children? has_children = self.content_provider.has_children(element) self.control.SetItemHasChildren(pid, has_children) # Expand it. self.control.Expand(pid) else: print("**** pid is None!!! ****") def update(self, element): """ Update the tree starting from the specified element. Call this when the APPEARANCE of the content has changed. """ pid = self._element_to_id_map.get(self._get_key(element), None) if pid is not None: self._refresh_element(pid, element) for child in self.content_provider.get_children(element): cid = self._element_to_id_map.get(self._get_key(child), None) if cid is not None: self._refresh_element(cid, child) return # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _get_style(self): """ Returns the wx style flags for creating the tree control. """ # Start with the default flags. style = self.STYLE if not self.show_root: style = style | wx.TR_HIDE_ROOT | wx.TR_LINES_AT_ROOT if self.selection_mode != "single": style = style | wx.TR_MULTIPLE | wx.TR_EXTENDED return style def _add_element(self, pid, element): """ Adds 'element' as a child of the element identified by 'pid'. If 'pid' is None then we are adding the root element. """ # Get the tree item image index and text. image_index = self._get_image_index(element) text = self._get_text(element) # Add the element. if pid is None: wxid = self.control.AddRoot(text, image_index, image_index) else: wxid = self.control.AppendItem(pid, text, image_index, image_index) # If we are adding the root element but the root is hidden, get its # children. if pid is None and not self.show_root: children = self.content_provider.get_children(element) for child in children: self._add_element(wxid, child) # Does the element have any children? has_children = self.content_provider.has_children(element) self.control.SetItemHasChildren(wxid, has_children) # The item data is a tuple. The first element indicates whether or not # we have already populated the item with its children. The second # element is the actual item data. if pid is None: if self.show_root: self.control.SetItemData(wxid, (False, element)) else: self.control.SetItemData(wxid, (False, element)) # Make sure that we can find the element's Id later. self._element_to_id_map[self._get_key(element)] = wxid # If we are adding the root item then automatically expand it. if pid is None and self.show_root: self.control.Expand(wxid) def _get_image_index(self, element): """ Returns the tree item image index for an element. """ # Get the image used to represent the element. image = self.label_provider.get_image(self, element) if image is not None: image_index = self._image_list.GetIndex(image.absolute_path) else: image_index = -1 return image_index def _get_key(self, element): """ Generate the key for the element to id map. """ try: key = hash(element) except: key = id(element) return key def _get_text(self, element): """ Returns the tree item text for an element. """ text = self.label_provider.get_text(self, element) if text is None: text = "" return text def _refresh_element(self, wxid, element): """ Refreshes the image and text of the specified element. """ # Get the tree item image index. image_index = self._get_image_index(element) self.control.SetItemImage(wxid, image_index, wx.TreeItemIcon_Normal) self.control.SetItemImage(wxid, image_index, wx.TreeItemIcon_Selected) # Get the tree item text. text = self._get_text(element) self.control.SetItemText(wxid, text) # Does the item have any children? has_children = self.content_provider.has_children(element) self.control.SetItemHasChildren(wxid, has_children) def _unpack_event(self, event): """ Unpacks the event to see whether a tree element was involved. """ try: point = event.GetPosition() except: point = event.GetPoint() wxid, flags = self.control.HitTest(point) # Warning: On GTK we have to check the flags before we call 'GetItemData' # because if we call it when the hit test returns 'nowhere' it will # barf (on Windows it simply returns 'None' 8^() if flags & wx.TREE_HITTEST_NOWHERE: data = None else: data = self.control.GetItemData(wxid) return data, wxid, flags, point def _get_selection(self): """ Returns a list of the selected elements. """ elements = [] for wxid in self.control.GetSelections(): data = self.control.GetItemData(wxid) if data is not None: populated, element = data elements.append(element) # 'data' can be None here if (for example) the element has been # deleted. # # fixme: Can we stop this happening?!?!? else: pass return elements def _delete_children(self, pid): """ Recursively deletes the children of the specified element. """ cookie = 0 (cid, cookie) = self.control.GetFirstChild(pid, cookie) while cid.IsOk(): # Recursively delete the child's children. self._delete_children(cid) # Remove the reference to the item's data. populated, element = self.control.GetItemData(cid) del self._element_to_id_map[self._get_key(element)] self.control.SetItemData(cid, None) # Next! (cid, cookie) = self.control.GetNextChild(pid, cookie) self.control.DeleteChildren(pid) return # Trait event handlers ------------------------------------------------- def _input_changed(self): """ Called when the tree's input has been changed. """ # Delete everything... if self.control is not None: self.control.DeleteAllItems() self._element_to_id_map = {} # ... and then add the root item back in. if self.input is not None: self._add_element(None, self.input) def _element_begin_drag_changed(self, element): """ Called when a drag is started on a element. """ # We ask the label provider for the actual value to drag. drag_value = self.label_provider.get_drag_value(self, element) # Start the drag. PythonDropSource(self.control, drag_value) return # wx event handlers ---------------------------------------------------- def _on_right_down(self, event): """ Called when the right mouse button is clicked on the tree. """ data, id, flags, point = self._unpack_event(event) # Did the right click occur on a tree item? if data is not None: populated, element = data # Trait notification. self.element_right_clicked = (element, point) # Give other event handlers a chance. event.Skip() def _on_left_down(self, event): """ Called when the left mouse button is clicked on the tree. """ data, wxid, flags, point = self._unpack_event(event) # Save point for tree_begin_drag method to workaround a bug in ?? when # wx.TreeEvent.GetPoint returns only (0,0). This happens under linux # when using wx-2.4.2.4, for instance. self._point_left_clicked = point # Did the left click occur on a tree item? if data is not None: populated, element = data # Trait notification. self.element_left_clicked = (element, point) # Give other event handlers a chance. event.Skip() def _on_tree_item_expanding(self, event): """ Called when a tree item is about to expand. """ # Which item is expanding? wxid = event.GetItem() # The item data is a tuple. The first element indicates whether or not # we have already populated the item with its children. The second # element is the actual item data. populated, element = self.control.GetItemData(wxid) # Give the label provider a chance to veto the expansion. if self.label_provider.is_expandable(self, element): # Lazily populate the item's children. if not populated: children = self.content_provider.get_children(element) # Sorting... if self.sorter is not None: self.sorter.sort(self, element, children) # Filtering.... for child in children: for filter in self.filters: if not filter.select(self, element, child): break else: self._add_element(wxid, child) # The element is now populated! self.control.SetItemData(wxid, (True, element)) else: event.Veto() def _on_tree_item_expanded(self, event): """ Called when a tree item has been expanded. """ # Which item was expanded? wxid = event.GetItem() # The item data is a tuple. The first element indicates whether or not # we have already populated the item with its children. The second # element is the actual item data. populated, element = self.control.GetItemData(wxid) # Make sure that the element's 'open' icon is displayed etc. self._refresh_element(wxid, element) # Trait notification. self.element_expanded = element def _on_tree_item_collapsing(self, event): """ Called when a tree item is about to collapse. """ # Which item is collapsing? wxid = event.GetItem() # The item data is a tuple. The first element indicates whether or not # we have already populated the item with its children. The second # element is the actual item data. populated, element = self.control.GetItemData(wxid) # Give the label provider a chance to veto the collapse. if not self.label_provider.is_collapsible(self, element): event.Veto() def _on_tree_item_collapsed(self, event): """ Called when a tree item has been collapsed. """ # Which item was collapsed? wxid = event.GetItem() # The item data is a tuple. The first element indicates whether or not # we have already populated the item with its children. The second # element is the actual item data. populated, element = self.control.GetItemData(wxid) # Make sure that the element's 'closed' icon is displayed etc. self._refresh_element(wxid, element) # Trait notification. self.element_collapsed = element def _on_tree_item_activated(self, event): """ Called when a tree item is activated (i.e., double clicked). """ # Which item was activated? wxid = event.GetItem() # The item data is a tuple. The first element indicates whether or not # we have already populated the item with its children. The second # element is the actual item data. populated, element = self.control.GetItemData(wxid) # Trait notification. self.element_activated = element def _on_tree_sel_changed(self, event): """ Called when the selection is changed. """ # Trait notification. self.selection = self._get_selection() def _on_tree_begin_drag(self, event): """ Called when a drag operation is starting on a tree item. """ # Get the element, its id and the point where the event occurred. data, wxid, flags, point = self._unpack_event(event) if point == (0, 0): # Apply workaround. point = self._point_left_clicked wxid, flags = self.control.HitTest(point) data = self.control.GetItemData(wxid) if data is not None: populated, element = data # Trait notification. self.element_begin_drag = element def _on_tree_begin_label_edit(self, event): """ Called when the user has started editing an item's label. """ wxid = event.GetItem() # The item data is a tuple. The first element indicates whether or not # we have already populated the item with its children. The second # element is the actual item data. populated, element = self.control.GetItemData(wxid) # Give the label provider a chance to veto the edit. if not self.label_provider.is_editable(self, element): event.Veto() def _on_tree_end_label_edit(self, event): """ Called when the user has finished editing an item's label. """ wxid = event.GetItem() # The item data is a tuple. The first element indicates whether or not # we have already populated the item with its children. The second # element is the actual item data. populated, element = self.control.GetItemData(wxid) # Give the label provider a chance to veto the edit. label = event.GetLabel() if not self.label_provider.set_text(self, element, label): event.Veto() def _on_char(self, event): """ Called when a key is pressed when the tree has focus. """ # Trait notification. self.key_pressed = event.GetKeyCode() return pyface-7.4.0/pyface/ui/wx/workbench/0000755000076500000240000000000014176460551020247 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/wx/workbench/editor_set_structure_handler.py0000755000076500000240000000561414176222673026611 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 handler used to restore editors. """ import logging from pyface.dock.api import SetStructureHandler logger = logging.getLogger(__name__) class EditorSetStructureHandler(SetStructureHandler): """ The handler used to restore editors. This is part of the 'dock window' API. It is used to resolve dock control Ids when setting the structure of a dock window. """ # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, window_layout, editor_mementos): """ Creates a new handler. """ self.window_layout = window_layout self.editor_mementos = editor_mementos return # ------------------------------------------------------------------------ # 'SetStructureHandler' interface. # ------------------------------------------------------------------------ def resolve_id(self, id): """ Resolves an unresolved dock control id. """ window_layout = self.window_layout window = window_layout.window try: # Get the memento for the editor with this Id. memento = self._get_editor_memento(id) # Ask the editor manager to create an editor from the memento. editor = window.editor_manager.set_editor_memento(memento) # Get the editor's toolkit-specific control. # # fixme: This is using a 'private' method on the window layout. # This may be ok since this structure handler is really part of the # layout! control = window_layout._wx_get_editor_control(editor) # fixme: This is ugly manipulating the editors list from in here! window.editors.append(editor) except: logger.warning("could not restore editor [%s]", id) control = None return control # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _get_editor_memento(self, id): """ Return the editor memento for the editor with the specified Id. Raises a 'ValueError' if no such memento exists. """ editor_memento = self.editor_mementos.get(id) if editor_memento is None: raise ValueError("no editor memento with Id %s" % id) return editor_memento pyface-7.4.0/pyface/ui/wx/workbench/view_set_structure_handler.py0000755000076500000240000000365614176222673026301 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 handler used to restore views. """ import logging from pyface.dock.api import SetStructureHandler logger = logging.getLogger(__name__) class ViewSetStructureHandler(SetStructureHandler): """ The handler used to restore views. This is part of the 'dock window' API. It is used to resolve dock control IDs when setting the structure of a dock window. """ # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, window_layout): """ Creates a new handler. """ self.window_layout = window_layout return # ------------------------------------------------------------------------ # 'SetStructureHandler' interface. # ------------------------------------------------------------------------ def resolve_id(self, id): """ Resolves an unresolved dock control *id*. """ window_layout = self.window_layout window = window_layout.window view = window.get_view_by_id(id) if view is not None: # Get the view's toolkit-specific control. # # fixme: This is using a 'private' method on the window layout. # This may be ok since this is really part of the layout! control = window_layout._wx_get_view_control(view) else: logger.warning("could not restore view [%s]", id) control = None return control pyface-7.4.0/pyface/ui/wx/workbench/__init__.py0000644000076500000240000000062714176222673022366 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! pyface-7.4.0/pyface/ui/wx/workbench/view.py0000644000076500000240000000341014176222673021572 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Enthought pyface package component """ from traits.api import Bool from pyface.workbench.i_view import MView class View(MView): """ The toolkit specific implementation of a View. See the IView interface for the API documentation. """ # Trait to indicate if the dock window containing the view should be # closeable. See FIXME comment in the _wx_create_view_dock_control method # in workbench_window_layout.py. closeable = Bool(False) # ------------------------------------------------------------------------ # 'IWorkbenchPart' interface. # ------------------------------------------------------------------------ def create_control(self, parent): """ Create the toolkit-specific control that represents the part. """ import wx # By default we create a red panel! control = wx.Panel(parent, -1) control.SetBackgroundColour("red") control.SetSize((100, 200)) return control def destroy_control(self): """ Destroy the toolkit-specific control that represents the part. """ if self.control is not None: self.control.Destroy() self.control = None def set_focus(self): """ Set the focus to the appropriate control in the part. """ if self.control is not None: self.control.SetFocus() return pyface-7.4.0/pyface/ui/wx/workbench/workbench_window_layout.py0000644000076500000240000006653214176222673025604 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 wx implementation of the workbench window layout interface. """ import pickle import logging import wx from pyface.dock.api import DOCK_BOTTOM, DOCK_LEFT, DOCK_RIGHT from pyface.dock.api import DOCK_TOP from pyface.dock.api import DockControl, DockRegion, DockSection from pyface.dock.api import DockSizer from traits.api import Delegate from pyface.workbench.i_workbench_window_layout import MWorkbenchWindowLayout from .editor_set_structure_handler import EditorSetStructureHandler from .view_set_structure_handler import ViewSetStructureHandler from .workbench_dock_window import WorkbenchDockWindow # Logging. logger = logging.getLogger(__name__) # Mapping from view position to the appropriate dock window constant. _POSITION_MAP = { "top": DOCK_TOP, "bottom": DOCK_BOTTOM, "left": DOCK_LEFT, "right": DOCK_RIGHT, } class WorkbenchWindowLayout(MWorkbenchWindowLayout): """ The wx implementation of the workbench window layout interface. See the 'IWorkbenchWindowLayout' interface for the API documentation. """ # 'IWorkbenchWindowLayout' interface ----------------------------------- editor_area_id = Delegate("window") # ------------------------------------------------------------------------ # 'IWorkbenchWindowLayout' interface. # ------------------------------------------------------------------------ def activate_editor(self, editor): """ Activate an editor. """ # This brings the dock control tab to the front. self._wx_editor_dock_window.activate_control(editor.id) editor.set_focus() return editor def activate_view(self, view): """ Activate a view. """ # This brings the dock control tab to the front. self._wx_view_dock_window.activate_control(view.id) view.set_focus() return view def add_editor(self, editor, title): """ Add an editor. """ try: self._wx_add_editor(editor, title) except Exception: logger.exception("error creating editor control <%s>", editor.id) return editor def add_view(self, view, position=None, relative_to=None, size=(-1, -1)): """ Add a view. """ try: self._wx_add_view(view, position, relative_to, size) view.visible = True except Exception: logger.exception("error creating view control <%s>", view.id) # Even though we caught the exception, it sometimes happens that # the view's control has been created as a child of the application # window (or maybe even the dock control). We should destroy the # control to avoid bad UI effects. view.destroy_control() # Additionally, display an error message to the user. self.window.error("Unable to add view %s" % view.id) return view def close_editor(self, editor): """ Close and editor. """ self._wx_editor_dock_window.close_control(editor.id) return editor def close_view(self, view): """ Close a view. """ self.hide_view(view) return view def close(self): """ Close the entire window layout. """ self._wx_editor_dock_window.close() self._wx_view_dock_window.close() def create_initial_layout(self, parent): """ Create the initial window layout. """ # The view dock window is where all of the views live. It also contains # a nested dock window where all of the editors live. self._wx_view_dock_window = WorkbenchDockWindow(parent) # The editor dock window (which is nested inside the view dock window) # is where all of the editors live. self._wx_editor_dock_window = WorkbenchDockWindow( self._wx_view_dock_window.control ) editor_dock_window_sizer = DockSizer(contents=DockSection()) self._wx_editor_dock_window.control.SetSizer(editor_dock_window_sizer) # Nest the editor dock window in the view dock window. editor_dock_window_control = DockControl( id=self.editor_area_id, name="Editors", control=self._wx_editor_dock_window.control, style="fixed", width=self.window.editor_area_size[0], height=self.window.editor_area_size[1], ) view_dock_window_sizer = DockSizer( contents=[editor_dock_window_control] ) self._wx_view_dock_window.control.SetSizer(view_dock_window_sizer) return self._wx_view_dock_window.control def contains_view(self, view): """ Return True if the view exists in the window layout. """ view_control = self._wx_view_dock_window.get_control(view.id, False) return view_control is not None def hide_editor_area(self): """ Hide the editor area. """ dock_control = self._wx_view_dock_window.get_control( self.editor_area_id, visible_only=False ) dock_control.show(False, layout=True) def hide_view(self, view): """ Hide a view. """ dock_control = self._wx_view_dock_window.get_control( view.id, visible_only=False ) dock_control.show(False, layout=True) view.visible = False return view def refresh(self): """ Refresh the window layout to reflect any changes. """ self._wx_view_dock_window.update_layout() def reset_editors(self): """ Activate the first editor in every group. """ self._wx_editor_dock_window.reset_regions() def reset_views(self): """ Activate the first view in every group. """ self._wx_view_dock_window.reset_regions() def show_editor_area(self): """ Show the editor area. """ dock_control = self._wx_view_dock_window.get_control( self.editor_area_id, visible_only=False ) dock_control.show(True, layout=True) def show_view(self, view): """ Show a view. """ dock_control = self._wx_view_dock_window.get_control( view.id, visible_only=False ) dock_control.show(True, layout=True) view.visible = True def is_editor_area_visible(self): dock_control = self._wx_view_dock_window.get_control( self.editor_area_id, visible_only=False ) return dock_control.visible # Methods for saving and restoring the layout -------------------------# def get_view_memento(self): structure = self._wx_view_dock_window.get_structure() # We always return a clone. return pickle.loads(pickle.dumps(structure)) def set_view_memento(self, memento): # We always use a clone. memento = pickle.loads(pickle.dumps(memento)) # The handler knows how to resolve view Ids when setting the dock # window structure. handler = ViewSetStructureHandler(self) # Set the layout of the views. self._wx_view_dock_window.set_structure(memento, handler) # fixme: We should be able to do this in the handler but we don't get a # reference to the actual dock control in 'resolve_id'. for view in self.window.views: control = self._wx_view_dock_window.get_control(view.id) if control is not None: self._wx_initialize_view_dock_control(view, control) view.visible = control.visible else: view.visible = False def get_editor_memento(self): # Get the layout of the editors. structure = self._wx_editor_dock_window.get_structure() # Get a memento to every editor. editor_references = self._get_editor_references() return (structure, editor_references) def set_editor_memento(self, memento): # fixme: Mementos might want to be a bit more formal than tuples! structure, editor_references = memento if len(structure.contents) > 0: # The handler knows how to resolve editor Ids when setting the dock # window structure. handler = EditorSetStructureHandler(self, editor_references) # Set the layout of the editors. self._wx_editor_dock_window.set_structure(structure, handler) # fixme: We should be able to do this in the handler but we don't # get a reference to the actual dock control in 'resolve_id'. for editor in self.window.editors: control = self._wx_editor_dock_window.get_control(editor.id) if control is not None: self._wx_initialize_editor_dock_control(editor, control) return # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _wx_add_editor(self, editor, title): """ Adds an editor. """ # Create a dock control that contains the editor. editor_dock_control = self._wx_create_editor_dock_control(editor) # If there are no other editors open (i.e., this is the first one!), # then create a new region to put the editor in. controls = self._wx_editor_dock_window.get_controls() if len(controls) == 0: # Get a reference to the empty editor section. sizer = self._wx_editor_dock_window.control.GetSizer() section = sizer.GetContents() # Add a region containing the editor dock control. region = DockRegion(contents=[editor_dock_control]) section.contents = [region] # Otherwise, add the editor to the same region as the first editor # control. # # fixme: We might want a more flexible placement strategy at some # point! else: region = controls[0].parent region.add(editor_dock_control) # fixme: Without this the window does not draw properly (manually # resizing the window makes it better!). self._wx_editor_dock_window.update_layout() def _wx_add_view(self, view, position, relative_to, size): """ Adds a view. """ # If no specific position is specified then use the view's default # position. if position is None: position = view.position # Create a dock control that contains the view. dock_control = self._wx_create_view_dock_control(view) if position == "with": # Does the item we are supposed to be positioned 'with' actual # exist? with_item = self._wx_view_dock_window.get_control(relative_to.id) # If so then we put the items in the same tab group. if with_item is not None: self._wx_add_view_with(dock_control, relative_to) # Otherwise, just fall back to the 'left' of the editor area. else: self._wx_add_view_relative(dock_control, None, "left", size) else: self._wx_add_view_relative( dock_control, relative_to, position, size ) return # fixme: Make the view dock window a sub class of dock window, and add # 'add_with' and 'add_relative_to' as methods on that. # # fixme: This is a good idea in theory, but the sizing is a bit iffy, as # it requires the window to be passed in to calculate the relative size # of the control. We could just calculate that here and pass in absolute # pixel sizes to the dock window subclass? def _wx_add_view_relative(self, dock_control, relative_to, position, size): """ Adds a view relative to another item. """ # If no 'relative to' Id is specified then we assume that the position # is relative to the editor area. if relative_to is None: relative_to_item = self._wx_view_dock_window.get_control( self.editor_area_id, visible_only=False ) # Find the item that we are adding the view relative to. else: relative_to_item = self._wx_view_dock_window.get_control( relative_to.id ) # Set the size of the dock control. self._wx_set_item_size(dock_control, size) # The parent of a dock control is a dock region. region = relative_to_item.parent section = region.parent section.add(dock_control, region, _POSITION_MAP[position]) def _wx_add_view_with(self, dock_control, with_obj): """ Adds a view in the same region as another item. """ # Find the item that we are adding the view 'with'. with_item = self._wx_view_dock_window.get_control(with_obj.id) if with_item is None: raise ValueError("Cannot find item %s" % with_obj) # The parent of a dock control is a dock region. with_item.parent.add(dock_control) def _wx_set_item_size(self, dock_control, size): """ Sets the size of a dock control. """ window_width, window_height = self.window.control.GetSize().Get() width, height = size if width != -1: dock_control.width = int(window_width * width) if height != -1: dock_control.height = int(window_height * height) def _wx_create_editor_dock_control(self, editor): """ Creates a dock control that contains the specified editor. """ self._wx_get_editor_control(editor) # Wrap a dock control around it. editor_dock_control = DockControl( id=editor.id, name=editor.name, closeable=True, control=editor.control, style="tab", # fixme: Create a subclass of dock control and give it a proper # editor trait! _editor=editor, ) # Hook up the 'on_close' and trait change handlers etc. self._wx_initialize_editor_dock_control(editor, editor_dock_control) return editor_dock_control def _wx_create_view_dock_control(self, view): """ Creates a dock control that contains the specified view. """ # Get the view's toolkit-specific control. control = self._wx_get_view_control(view) # Check if the dock control should be 'closeable'. # FIXME: The 'fixme' comment below suggests some issue with closing a # view by clicking 'X' rather than just hiding the view. The two actions # appear to do the same thing however, so I'm not sure if the comment # below is an out-of-date comment. This needs more investigation. # For the time being, I am making a view closeable if it has a # 'closeable' trait set to True. closeable = view.closeable # Wrap a dock control around it. view_dock_control = DockControl( id=view.id, name=view.name, # fixme: We would like to make views closeable, but closing via the # tab is different than calling show(False, layout=True) on the # control! If we use a close handler can we change that?!? closeable=closeable, control=control, style=view.style_hint, # fixme: Create a subclass of dock control and give it a proper # view trait! _view=view, ) # Hook up the 'on_close' and trait change handlers etc. self._wx_initialize_view_dock_control(view, view_dock_control) return view_dock_control def _wx_get_editor_control(self, editor): """ Returns the editor's toolkit-specific control. If the editor has not yet created its control, we will ask it to create it here. """ if editor.control is None: parent = self._wx_editor_dock_window.control # This is the toolkit-specific control that represents the 'guts' # of the editor. self.editor_opening = editor editor.control = editor.create_control(parent) self.editor_opened = editor # Hook up toolkit-specific events that are managed by the framework # etc. self._wx_initialize_editor_control(editor) return editor.control def _wx_initialize_editor_control(self, editor): """ Initializes the toolkit-specific control for an editor. This is used to hook events managed by the framework etc. """ def on_set_focus(event): """ Called when the control gets the focus. """ editor.has_focus = True # Let the default wx event handling do its thang. event.Skip() def on_kill_focus(event): """ Called when the control gets the focus. """ editor.has_focus = False # Let the default wx event handling do its thang. event.Skip() return self._wx_add_focus_listeners( editor.control, on_set_focus, on_kill_focus ) def _wx_get_view_control(self, view): """ Returns a view's toolkit-specific control. If the view has not yet created its control, we will ask it to create it here. """ if view.control is None: parent = self._wx_view_dock_window.control # Make sure that the view knows which window it is in. view.window = self.window # This is the toolkit-specific control that represents the 'guts' # of the view. self.view_opening = view view.control = view.create_control(parent) self.view_opened = view # Hook up toolkit-specific events that are managed by the # framework etc. self._wx_initialize_view_control(view) return view.control def _wx_initialize_view_control(self, view): """ Initializes the toolkit-specific control for a view. This is used to hook events managed by the framework. """ def on_set_focus(event): """ Called when the control gets the focus. """ view.has_focus = True # Let the default wx event handling do its thang. event.Skip() def on_kill_focus(event): """ Called when the control gets the focus. """ view.has_focus = False # Let the default wx event handling do its thang. event.Skip() return self._wx_add_focus_listeners(view.control, on_set_focus, on_kill_focus) def _wx_add_focus_listeners(self, control, on_set_focus, on_kill_focus): """ Recursively adds focus listeners to a control. """ # NOTE: If we are passed a wx control that isn't correctly initialized # (like when the TraitsUIView isn't properly creating it) but it is # actually a wx control, then we get weird exceptions from trying to # register event handlers. The exception messages complain that # the passed control is a str object instead of a wx object. if on_set_focus is not None: # control.Bind(wx.EVT_SET_FOCUS, on_set_focus) control.Bind(wx.EVT_SET_FOCUS, on_set_focus) if on_kill_focus is not None: # control.Bind(wx.EVT_KILL_FOCUS, on_kill_focus) control.Bind(wx.EVT_KILL_FOCUS, on_kill_focus) for child in control.GetChildren(): self._wx_add_focus_listeners(child, on_set_focus, on_kill_focus) def _wx_initialize_editor_dock_control(self, editor, editor_dock_control): """ Initializes an editor dock control. fixme: We only need this method because of a problem with the dock window API in the 'SetStructureHandler' class. Currently we do not get a reference to the dock control in 'resolve_id' and hence we cannot set up the 'on_close' and trait change handlers etc. """ # Some editors append information to their name to indicate status (in # our case this is often a 'dirty' indicator that shows when the # contents of an editor have been modified but not saved). When the # dock window structure is persisted it contains the name of each dock # control, which obviously includes any appended state information. # Here we make sure that when the dock control is recreated its name is # set to the editor name and nothing more! editor_dock_control.set_name(editor.name) # fixme: Should we roll the traits UI stuff into the default editor. if hasattr(editor, "ui") and editor.ui is not None: from traitsui.dockable_view_element import DockableViewElement # This makes the control draggable outside of the main window. # editor_dock_control.export = 'pyface.workbench.editor' editor_dock_control.dockable = DockableViewElement( should_close=True, ui=editor.ui ) editor_dock_control.on_close = self._wx_on_editor_closed def on_id_changed(event): editor = event.object editor_dock_control.id = editor.id return editor.observe(on_id_changed, "id") def on_name_changed(event): editor = event.object editor_dock_control.set_name(editor.name) return editor.observe(on_name_changed, "name") def on_activated_changed(event): editor_dock_control = event.object if editor_dock_control._editor is not None: editor_dock_control._editor.set_focus() return editor_dock_control.observe(on_activated_changed, "activated") def _wx_initialize_view_dock_control(self, view, view_dock_control): """ Initializes a view dock control. fixme: We only need this method because of a problem with the dock window API in the 'SetStructureHandler' class. Currently we do not get a reference to the dock control in 'resolve_id' and hence we cannot set up the 'on_close' and trait change handlers etc. """ # Some views append information to their name to indicate status (in # our case this is often a 'dirty' indicator that shows when the # contents of a view have been modified but not saved). When the # dock window structure is persisted it contains the name of each dock # control, which obviously includes any appended state information. # Here we make sure that when the dock control is recreated its name is # set to the view name and nothing more! view_dock_control.set_name(view.name) # fixme: Should we roll the traits UI stuff into the default editor. if hasattr(view, "ui") and view.ui is not None: from traitsui.dockable_view_element import DockableViewElement # This makes the control draggable outside of the main window. # view_dock_control.export = 'pyface.workbench.view' # If the ui's 'view' trait has an 'export' field set, pass that on # to the dock control. This makes the control detachable from the # main window (if 'export' is not an empty string). if view.ui.view is not None: view_dock_control.export = view.ui.view.export view_dock_control.dockable = DockableViewElement( should_close=True, ui=view.ui ) view_dock_control.on_close = self._wx_on_view_closed def on_id_changed(event): view = event.object view_dock_control.id = view.id return view.observe(on_id_changed, "id") def on_name_changed(event): view = event.object view_dock_control.set_name(view.name) return view.observe(on_name_changed, "name") def on_activated_changed(event): view_dock_control = event.object if view_dock_control._view is not None: view_dock_control._view.set_focus() return view_dock_control.observe(on_activated_changed, "activated") return # Trait change handlers ------------------------------------------------ # Static ---- def _window_changed(self, old, new): """ Static trait change handler. """ if old is not None: old.observe( self._wx_on_editor_area_size_changed, "editor_area_size", remove=True, ) if new is not None: new.observe( self._wx_on_editor_area_size_changed, "editor_area_size" ) # Dynamic ---- def _wx_on_editor_area_size_changed(self, event): """ Dynamic trait change handler. """ window_width, window_height = self.window.control.GetSize().Get() # Get the dock control that contains the editor dock window. control = self._wx_view_dock_window.get_control(self.editor_area_id) # We actually resize the region that the editor area is in. region = control.parent region.width = int(event.new[0] * window_width) region.height = int(event.new[1] * window_height) return # Dock window handlers ------------------------------------------------- # fixme: Should these just fire events that the window listens to? def _wx_on_view_closed(self, dock_control, force): """ Called when a view is closed via the dock window control. """ view = self.window.get_view_by_id(dock_control.id) if view is not None: logger.debug("workbench destroying view control <%s>", view) try: view.visible = False self.view_closing = view view.destroy_control() self.view_closed = view except: logger.exception("error destroying view control <%s>", view) return True def _wx_on_editor_closed(self, dock_control, force): """ Called when an editor is closed via the dock window control. """ dock_control._editor = None editor = self.window.get_editor_by_id(dock_control.id) ## import weakref ## editor_ref = weakref.ref(editor) if editor is not None: logger.debug("workbench destroying editor control <%s>", editor) try: # fixme: We would like this event to be vetoable, but it isn't # just yet (we will need to modify the dock window package). self.editor_closing = editor editor.destroy_control() self.editor_closed = editor except: logger.exception( "error destroying editor control <%s>", editor ) ## import gc ## gc.collect() ## print 'Editor references', len(gc.get_referrers(editor)) ## for r in gc.get_referrers(editor): ## print '********************************************' ## print type(r), id(r), r ## del editor ## gc.collect() ## print 'Is editor gone?', editor_ref() is None, 'ref', editor_ref() return True pyface-7.4.0/pyface/ui/wx/workbench/editor.py0000644000076500000240000000304614176222673022113 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Enthought pyface package component """ from pyface.workbench.i_editor import MEditor class Editor(MEditor): """ The toolkit specific implementation of an Editor. See the IEditor interface for the API documentation. """ # ------------------------------------------------------------------------ # 'IWorkbenchPart' interface. # ------------------------------------------------------------------------ def create_control(self, parent): """ Create the toolkit-specific control that represents the part. """ import wx # By default we create a yellow panel! control = wx.Panel(parent, -1) control.SetBackgroundColour("yellow") control.SetSize((100, 200)) return control def destroy_control(self): """ Destroy the toolkit-specific control that represents the part. """ if self.control is not None: self.control.Destroy() self.control = None def set_focus(self): """ Set the focus to the appropriate control in the part. """ if self.control is not None: self.control.SetFocus() return pyface-7.4.0/pyface/ui/wx/workbench/workbench_dock_window.py0000755000076500000240000000772414176222673025210 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Base class for workbench dock windows. """ import logging from pyface.dock.api import DockGroup, DockRegion, DockWindow logger = logging.getLogger(__name__) class WorkbenchDockWindow(DockWindow): """ Base class for workbench dock windows. This class just adds a few useful methods to the standard 'DockWindow' interface. Hopefully at some stage these can be part of that API too! """ # ------------------------------------------------------------------------ # Protected 'DockWindow' interface. # ------------------------------------------------------------------------ def _right_up(self, event): """ Handles the right mouse button being released. We override this to stop the default dock window context menus from appearing. """ pass # ------------------------------------------------------------------------ # 'WorkbenchDockWindow' interface. # ------------------------------------------------------------------------ def activate_control(self, id): """ Activates the dock control with the specified Id. Does nothing if no such dock control exists (well, it *does* write a debug message to the logger). """ control = self.get_control(id) if control is not None: logger.debug("activating control <%s>", id) control.activate() else: logger.debug("no control <%s> to activate", id) def close_control(self, id): """ Closes the dock control with the specified Id. Does nothing if no such dock control exists (well, it *does* write a debug message to the logger). """ control = self.get_control(id) if control is not None: logger.debug("closing control <%s>", id) control.close() else: logger.debug("no control <%s> to close", id) def get_control(self, id, visible_only=True): """ Returns the dock control with the specified Id. Returns None if no such dock control exists. """ for control in self.get_controls(visible_only): if control.id == id: break else: control = None return control def get_controls(self, visible_only=True): """ Returns all of the dock controls in the window. """ sizer = self.control.GetSizer() section = sizer.GetContents() return section.get_controls(visible_only=visible_only) def get_regions(self, group): """ Returns all dock regions in a dock group (recursively). """ regions = [] for item in group.contents: if isinstance(item, DockRegion): regions.append(item) if isinstance(item, DockGroup): regions.extend(self.get_regions(item)) return regions def get_structure(self): """ Returns the window structure (minus the content). """ sizer = self.control.GetSizer() return sizer.GetStructure() def reset_regions(self): """ Activates the first dock control in every region. """ sizer = self.control.GetSizer() section = sizer.GetContents() for region in self.get_regions(section): if len(region.contents) > 0: region.contents[0].activate(layout=False) def set_structure(self, structure, handler=None): """ Sets the window structure. """ sizer = self.control.GetSizer() sizer.SetStructure(self.control.GetParent(), structure, handler) return pyface-7.4.0/pyface/ui/wx/color_dialog.py0000644000076500000240000000445214176222673021302 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 interface for a dialog that allows the user to select a color. """ import wx from traits.api import Bool, provides from pyface.color import Color from pyface.ui_traits import PyfaceColor from pyface.i_color_dialog import IColorDialog from .dialog import Dialog # The WxPython version in a convenient to compare form. wx_version = tuple(int(x) for x in wx.__version__.split('.')[:3]) @provides(IColorDialog) class ColorDialog(Dialog): """ A dialog for selecting colors. """ # 'IColorDialog' interface ---------------------------------------------- #: The color in the dialog. color = PyfaceColor() #: Whether or not to allow the user to chose an alpha value. Only works #: for wxPython 4.1 and higher. show_alpha = Bool(False) # ------------------------------------------------------------------------ # 'IDialog' interface. # ------------------------------------------------------------------------ def _create_contents(self, parent): # In wx this is a canned dialog. pass # ------------------------------------------------------------------------ # 'IWindow' interface. # ------------------------------------------------------------------------ def close(self): colour_data = self.control.GetColourData() wx_colour = colour_data.GetColour() self.color = Color.from_toolkit(wx_colour) super(ColorDialog, self).close() # ------------------------------------------------------------------------ # 'IWidget' interface. # ------------------------------------------------------------------------ def _create_control(self, parent): wx_colour = self.color.to_toolkit() data = wx.ColourData() data.SetColour(wx_colour) if wx_version >= (4, 1): data.SetChooseAlpha(self.show_alpha) dialog = wx.ColourDialog(parent, data) return dialog pyface-7.4.0/pyface/ui/wx/split_widget.py0000644000076500000240000000777214176222673021353 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Mix-in class for split widgets. """ import wx from traits.api import provides from pyface.i_split_widget import ISplitWidget, MSplitWidget @provides(ISplitWidget) class SplitWidget(MSplitWidget): """ The toolkit specific implementation of a SplitWidget. See the ISPlitWidget interface for the API documentation. """ # ------------------------------------------------------------------------ # Protected 'ISplitWidget' interface. # ------------------------------------------------------------------------ def _create_splitter(self, parent): """ Create the toolkit-specific control that represents the widget. """ sizer = wx.BoxSizer(wx.VERTICAL) splitter = wx.SplitterWindow(parent, -1, style=wx.CLIP_CHILDREN) splitter.SetSizer(sizer) splitter.SetAutoLayout(True) # If we don't set the minimum pane size, the user can drag the sash and # make one pane disappear! splitter.SetMinimumPaneSize(50) # Left hand side/top. lhs = self._create_lhs(splitter) sizer.Add(lhs, 1, wx.EXPAND) # Right hand side/bottom. rhs = self._create_rhs(splitter) sizer.Add(rhs, 1, wx.EXPAND) # Resize the splitter to fit the sizer's minimum size. sizer.Fit(splitter) # Split the window in the appropriate direction. # # fixme: Notice that on the initial split, we DON'T specify the split # ratio. If we do then sadly, wx won't let us move the sash 8^() if self.direction == "vertical": splitter.SplitVertically(lhs, rhs) else: splitter.SplitHorizontally(lhs, rhs) # We respond to the FIRST size event to make sure that the split ratio # is correct when the splitter is laid out in its parent. splitter.Bind(wx.EVT_SIZE, self._on_size) return splitter def _create_lhs(self, parent): """ Creates the left hand/top panel depending on the direction. """ if self.lhs is not None: lhs = self.lhs(parent) if not isinstance(lhs, wx.Window): lhs = lhs.control else: # Dummy implementation - override! lhs = wx.Panel(parent, -1) lhs.SetBackgroundColour("yellow") lhs.SetSize((300, 200)) return lhs def _create_rhs(self, parent): """ Creates the right hand/bottom panel depending on the direction. """ if self.rhs is not None: rhs = self.rhs(parent) if not isinstance(rhs, wx.Window): rhs = rhs.control else: # Dummy implementation - override! rhs = wx.Panel(parent, -1) rhs.SetBackgroundColour("green") rhs.SetSize((100, 200)) return rhs # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ # wx event handlers ---------------------------------------------------- def _on_size(self, event): """ Called when the frame is resized. """ splitter = event.GetEventObject() width, height = splitter.GetSize().Get() # Make sure that the split ratio is correct. if self.direction == "vertical": position = int(width * self.ratio) else: position = int(height * self.ratio) splitter.SetSashPosition(position) # Since we only care about the FIRST size event, remove ourselves as # a listener. # splitter.Unbind(wx.EVT_SIZE) return pyface-7.4.0/pyface/ui/wx/image_button.py0000644000076500000240000002371514176222673021325 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Description: Image and text-based pyface button/toolbar/radio button control. """ An image and text-based control that can be used as a normal, radio or toolbar button. """ import warnings import wx from numpy import array, frombuffer, reshape, ravel, dtype from traits.api import Any, Bool, Str, Range, Enum, Event from pyface.ui_traits import Image, Orientation from .layout_widget import LayoutWidget # ------------------------------------------------------------------------------- # Constants: # ------------------------------------------------------------------------------- # Text color used when a button is disabled: DisabledTextColor = wx.Colour(128, 128, 128) # ------------------------------------------------------------------------------- # 'ImageButton' class: # ------------------------------------------------------------------------------- class ImageButton(LayoutWidget): """ An image and text-based control that can be used as a normal, radio or toolbar button. """ # 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 ) # --------------------------------------------------------------------------- # Trait definitions: # --------------------------------------------------------------------------- # The image: image = Image() # The (optional) label: label = Str() # Extra padding to add to both the left and right sides: width_padding = Range(0, 31, 7) # Extra padding to add to both the top and 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 = Orientation() # Is the control selected ('radio' or 'checkbox' style)? selected = Bool(False) # Fired when a 'button' or 'toolbar' style control is clicked: clicked = Event() _image = Any() # --------------------------------------------------------------------------- # Initializes the object: # --------------------------------------------------------------------------- def __init__(self, parent, **traits): """ Creates a new image control. """ create = traits.pop("create", True) super().__init__(parent=parent, **traits) if create: self.create() warnings.warn( "automatic widget creation is deprecated and will be removed " "in a future Pyface version, use create=False and explicitly " "call create() for future behaviour", PendingDeprecationWarning, ) def _create_control(self, parent): self._recalc_size() control = wx.Window(parent, -1, size=wx.Size(self._dx, self._dy)) control._owner = self self._mouse_over = self._button_down = False # Set up mouse event handlers: control.Bind(wx.EVT_ENTER_WINDOW, self._on_enter_window) control.Bind(wx.EVT_LEAVE_WINDOW, self._on_leave_window) control.Bind(wx.EVT_LEFT_DOWN, self._on_left_down) control.Bind(wx.EVT_LEFT_UP, self._on_left_up) control.Bind(wx.EVT_PAINT, self._on_paint) return control def _recalc_size(self): # Calculate the size of the button: idx = idy = tdx = tdy = 0 if self._image is not None: idx = self._image.GetWidth() idy = self._image.GetHeight() if self.label != "": dc = wx.ScreenDC() dc.SetFont(wx.NORMAL_FONT) tdx, tdy = dc.GetTextExtent(self.label) wp2 = self.width_padding + 2 hp2 = self.height_padding + 2 if self.orientation == "horizontal": self._ix = wp2 spacing = (idx > 0) * (tdx > 0) * 4 self._tx = self._ix + idx + spacing dx = idx + tdx + spacing dy = max(idy, tdy) self._iy = hp2 + ((dy - idy) // 2) self._ty = hp2 + ((dy - tdy) // 2) else: self._iy = hp2 spacing = (idy > 0) * (tdy > 0) * 2 self._ty = self._iy + idy + spacing dx = max(idx, tdx) dy = idy + tdy + spacing self._ix = wp2 + ((dx - idx) // 2) self._tx = wp2 + ((dx - tdx) // 2) # Create the toolkit-specific control: self._dx = dx + wp2 + wp2 self._dy = dy + hp2 + hp2 # --------------------------------------------------------------------------- # Handles the 'image' trait being changed: # --------------------------------------------------------------------------- def _image_changed(self, image): self._image = self._mono_image = None if image is not None: self._img = image.create_image() self._image = self._img.ConvertToBitmap() self._recalc_size() self.control.SetSize(wx.Size(self._dx, self._dy)) if self.control is not None: self.control.Refresh() # --------------------------------------------------------------------------- # Handles the 'selected' trait being changed: # --------------------------------------------------------------------------- def _selected_changed(self, selected): """ Handles the 'selected' trait being changed. """ if selected and (self.style == "radio"): for control in self.control.GetParent().GetChildren(): owner = getattr(control, "_owner", None) if ( isinstance(owner, ImageButton) and owner.selected and (owner is not self) ): owner.selected = False break self.control.Refresh() # -- wx event handlers ---------------------------------------------------------- def _on_enter_window(self, event): """ Called when the mouse enters the widget. """ if self.style != "button": self._mouse_over = True self.control.Refresh() def _on_leave_window(self, event): """ Called when the mouse leaves the widget. """ if self._mouse_over: self._mouse_over = False self.control.Refresh() def _on_left_down(self, event): """ Called when the left mouse button goes down on the widget. """ self._button_down = True self.control.CaptureMouse() self.control.Refresh() def _on_left_up(self, event): """ Called when the left mouse button goes up on the widget. """ control = self.control control.ReleaseMouse() self._button_down = False wdx, wdy = control.GetClientSize().Get() x, y = event.GetX(), event.GetY() control.Refresh() if (0 <= x < wdx) and (0 <= y < wdy): if self.style == "radio": self.selected = True elif self.style == "checkbox": self.selected = not self.selected else: self.clicked = True def _on_paint(self, event): """ Called when the widget needs repainting. """ wdc = wx.PaintDC(self.control) wdx, wdy = self.control.GetClientSize().Get() ox = (wdx - self._dx) / 2 oy = (wdy - self._dy) / 2 disabled = not self.control.IsEnabled() if self._image is not None: image = self._image if disabled: if self._mono_image is None: img = self._img data = reshape( frombuffer(img.GetData(), dtype("uint8")), (-1, 3) ) * array([[0.297, 0.589, 0.114]]) g = data[:, 0] + data[:, 1] + data[:, 2] data[:, 0] = data[:, 1] = data[:, 2] = g img.SetData(ravel(data.astype(dtype("uint8"))).tostring()) img.SetMaskColour(0, 0, 0) self._mono_image = img.ConvertToBitmap() self._img = None image = self._mono_image wdc.DrawBitmap(image, ox + self._ix, oy + self._iy, True) if self.label != "": if disabled: wdc.SetTextForeground(DisabledTextColor) wdc.SetFont(wx.NORMAL_FONT) wdc.DrawText(self.label, ox + self._tx, oy + self._ty) pens = [self._selectedPenLight, self._selectedPenDark] bd = self._button_down style = self.style is_rc = style in ("radio", "checkbox") if bd or (style == "button") or (is_rc and self.selected): if is_rc: bd = 1 - bd 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) elif self._mouse_over and (not self.selected): 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) pyface-7.4.0/pyface/ui/wx/fields/0000755000076500000240000000000014176460551017533 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/wx/fields/field.py0000644000076500000240000000153214176222673021172 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 Wx-specific implementation of the text field class """ from traits.api import Any, provides from pyface.fields.i_field import IField, MField from pyface.ui.wx.layout_widget import LayoutWidget @provides(IField) class Field(MField, LayoutWidget): """ The Wx-specific implementation of the field class This is an abstract class which is not meant to be instantiated. """ #: The value held by the field. value = Any() pyface-7.4.0/pyface/ui/wx/fields/toggle_field.py0000644000076500000240000001104014176222673022526 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 Wx-specific implementation of the toggle field class """ import wx from traits.api import provides from pyface.fields.i_toggle_field import IToggleField, MToggleField from .field import Field @provides(IToggleField) class ToggleField(MToggleField, Field): """ The Wx-specific implementation of the toggle field class """ # ------------------------------------------------------------------------ # Private interface # ------------------------------------------------------------------------ # Toolkit control interface --------------------------------------------- def _get_control_value(self): """ Toolkit specific method to get the control's value. """ return self.control.GetValue() def _get_control_text(self): """ Toolkit specific method to get the control's text. """ return self.control.GetLabel() def _set_control_value(self, value): """ Toolkit specific method to set the control's value. """ self.control.SetValue(value) def _set_control_text(self, text): """ Toolkit specific method to set the control's text. """ self.control.SetLabel(text) def _set_control_icon(self, icon): """ Toolkit specific method to set the control's icon. """ # don't support icons on Wx for now pass class CheckBoxField(ToggleField): """ The Wx-specific implementation of the checkbox class """ def _create_control(self, parent): """ Create the toolkit-specific control that represents the widget. """ control = wx.CheckBox(parent) return control def _set_control_value(self, value): """ Toolkit specific method to set the control's value. """ super()._set_control_value(value) event = wx.CommandEvent(wx.EVT_CHECKBOX.typeId, self.control.GetId()) event.SetInt(value) wx.PostEvent(self.control.GetEventHandler(), event) def _observe_control_value(self, remove=False): """ Toolkit specific method to change the control value observer. """ if remove: self.control.Unbind(wx.EVT_CHECKBOX, handler=self._update_value) else: self.control.Bind(wx.EVT_CHECKBOX, self._update_value) class RadioButtonField(ToggleField): """ The Wx-specific implementation of the radio button class This is intended to be used in groups, and shouldn't be used by itself. """ def _create_control(self, parent): """ Create the toolkit-specific control that represents the widget. """ control = wx.RadioButton(parent) return control def _set_control_value(self, value): """ Toolkit specific method to set the control's value. """ super()._set_control_value(value) event = wx.CommandEvent(wx.EVT_RADIOBUTTON.typeId, self.control.GetId()) event.SetInt(value) wx.PostEvent(self.control.GetEventHandler(), event) def _observe_control_value(self, remove=False): """ Toolkit specific method to change the control value observer. """ if remove: self.control.Unbind(wx.EVT_RADIOBUTTON, handler=self._update_value) else: self.control.Bind(wx.EVT_RADIOBUTTON, self._update_value) class ToggleButtonField(ToggleField): """ The Wx-specific implementation of the toggle button class """ def _create_control(self, parent): """ Create the toolkit-specific control that represents the widget. """ control = wx.ToggleButton(parent) return control def _set_control_value(self, value): """ Toolkit specific method to set the control's value. """ super()._set_control_value(value) event = wx.CommandEvent(wx.EVT_TOGGLEBUTTON.typeId, self.control.GetId()) event.SetInt(value) wx.PostEvent(self.control.GetEventHandler(), event) def _observe_control_value(self, remove=False): """ Toolkit specific method to change the control value observer. """ if remove: self.control.Unbind( wx.EVT_TOGGLEBUTTON, handler=self._update_value, ) else: self.control.Bind(wx.EVT_TOGGLEBUTTON, self._update_value) pyface-7.4.0/pyface/ui/wx/fields/__init__.py0000644000076500000240000000000014176222673021633 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/wx/fields/combo_field.py0000644000076500000240000000723314176222673022355 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 Wx-specific implementation of the combo field class """ import wx from traits.api import provides from pyface.fields.i_combo_field import IComboField, MComboField from .field import Field @provides(IComboField) class ComboField(MComboField, Field): """ The Wx-specific implementation of the combo field class """ # ------------------------------------------------------------------------ # IWidget interface # ------------------------------------------------------------------------ def _create_control(self, parent): """ Create the toolkit-specific control that represents the widget. """ control = wx.Choice(parent, -1) return control # ------------------------------------------------------------------------ # Private interface # ------------------------------------------------------------------------ def _update_value(self, event): """ Handle a change to the value from user interaction """ # do normal focus event stuff if isinstance(event, wx.FocusEvent): event.Skip() if self.control is not None: self.value = self.values[event.GetInt()] # Toolkit control interface --------------------------------------------- def _get_control_value(self): """ Toolkit specific method to get the control's value. """ index = self.control.GetSelection() if index != -1: return self.values[index] else: raise IndexError("no value selected") def _get_control_text(self): """ Toolkit specific method to get the control's text content. """ index = self.control.GetSelection() if index != -1: return self.control.GetString(index) else: raise IndexError("no value selected") def _set_control_value(self, value): """ Toolkit specific method to set the control's value. """ index = self.values.index(value) self.control.SetSelection(index) event = wx.CommandEvent(wx.EVT_CHOICE.typeId, self.control.GetId()) event.SetInt(index) wx.PostEvent(self.control.GetEventHandler(), event) def _observe_control_value(self, remove=False): """ Toolkit specific method to change the control value observer. """ if remove: self.control.Unbind(wx.EVT_CHOICE, handler=self._update_value) else: self.control.Bind(wx.EVT_CHOICE, self._update_value) def _get_control_text_values(self): """ Toolkit specific method to get the control's text values. """ values = [] for i in range(self.control.GetCount()): values.append(self.control.GetString(i)) return values def _set_control_values(self, values): """ Toolkit specific method to set the control's values. """ current_value = self.value self.control.Clear() for i, value in enumerate(values): item = self.formatter(value) if isinstance(item, tuple): image, text = item else: text = item self.control.Insert(text, i, value) if current_value in values: self._set_control_value(current_value) else: self._set_control_value(self.value) pyface-7.4.0/pyface/ui/wx/fields/text_field.py0000644000076500000240000000671114176222673022242 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 Wx-specific implementation of the text field class """ import wx from traits.api import provides from pyface.fields.i_text_field import ITextField, MTextField from .field import Field @provides(ITextField) class TextField(MTextField, Field): """ The Wx-specific implementation of the text field class """ # ------------------------------------------------------------------------ # IWidget interface # ------------------------------------------------------------------------ def _create_control(self, parent): """ Create the toolkit-specific control that represents the widget. """ style = wx.TE_PROCESS_ENTER if self.echo == "password": style |= wx.TE_PASSWORD control = wx.TextCtrl(parent, -1, value=self.value, style=style) return control # ------------------------------------------------------------------------ # Private interface # ------------------------------------------------------------------------ def _update_value(self, event): # do normal focus event stuff if isinstance(event, wx.FocusEvent): event.Skip() if self.control is not None: self.value = self.control.GetValue() def _get_control_value(self): """ Toolkit specific method to get the control's value. """ return self.control.GetValue() def _set_control_value(self, value): """ Toolkit specific method to set the control's value. """ self.control.SetValue(value) def _observe_control_value(self, remove=False): """ Toolkit specific method to change the control value observer. """ if remove: self.control.Unbind(wx.EVT_TEXT, handler=self._update_value) else: self.control.Bind(wx.EVT_TEXT, self._update_value) def _get_control_placeholder(self): """ Toolkit specific method to set the control's placeholder. """ return self.control.GetHint() def _set_control_placeholder(self, placeholder): """ Toolkit specific method to set the control's placeholder. """ self.control.SetHint(placeholder) def _get_control_echo(self): """ Toolkit specific method to get the control's echo. """ return self.echo def _set_control_echo(self, echo): """ Toolkit specific method to set the control's echo. """ # Can't change echo on Wx after control has been created." pass def _get_control_read_only(self): """ Toolkit specific method to get the control's read_only state. """ return not self.control.IsEditable() def _set_control_read_only(self, read_only): """ Toolkit specific method to set the control's read_only state. """ self.control.SetEditable(not read_only) def _observe_control_editing_finished(self, remove=False): """ Change observation of whether editing is finished. """ if remove: self.control.Unbind(wx.EVT_TEXT_ENTER, handler=self._update_value) else: self.control.Bind(wx.EVT_TEXT_ENTER, self._update_value) pyface-7.4.0/pyface/ui/wx/fields/spin_field.py0000644000076500000240000000464114176222673022227 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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: Enthought, Inc. # Description: """ The Wx-specific implementation of the spin field class """ import wx from traits.api import provides from pyface.fields.i_spin_field import ISpinField, MSpinField from .field import Field @provides(ISpinField) class SpinField(MSpinField, Field): """ The Wx-specific implementation of the spin field class """ # ------------------------------------------------------------------------ # IWidget interface # ------------------------------------------------------------------------ def _create_control(self, parent): """ Create the toolkit-specific control that represents the widget. """ control = wx.SpinCtrl(parent, style=wx.TE_PROCESS_ENTER) return control # ------------------------------------------------------------------------ # Private interface # ------------------------------------------------------------------------ def _get_control_value(self): """ Toolkit specific method to get the control's value. """ return self.control.GetValue() def _set_control_value(self, value): """ Toolkit specific method to set the control's value. """ self.control.SetValue(value) event = wx.SpinEvent(wx.EVT_SPINCTRL.typeId, self.control.GetId()) event.SetInt(value) wx.PostEvent(self.control.GetEventHandler(), event) def _observe_control_value(self, remove=False): """ Toolkit specific method to change the control value observer. """ if remove: self.control.Unbind(wx.EVT_SPINCTRL, handler=self._update_value) else: self.control.Bind(wx.EVT_SPINCTRL, self._update_value) def _get_control_bounds(self): """ Toolkit specific method to get the control's bounds. """ return (self.control.GetMin(), self.control.GetMax()) def _set_control_bounds(self, bounds): """ Toolkit specific method to set the control's bounds. """ self.control.SetRange(*bounds) pyface-7.4.0/pyface/ui/wx/fields/time_field.py0000644000076500000240000000451114176222673022210 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 Wx-specific implementation of the time field class """ from datetime import time import wx.adv from traits.api import provides from pyface.fields.i_time_field import ITimeField, MTimeField from .field import Field @provides(ITimeField) class TimeField(MTimeField, Field): """ The Wx-specific implementation of the time field class """ # ------------------------------------------------------------------------ # IWidget interface # ------------------------------------------------------------------------ def _create_control(self, parent): """ Create the toolkit-specific control that represents the widget. """ control = wx.adv.TimePickerCtrl(parent) return control # ------------------------------------------------------------------------ # Private interface # ------------------------------------------------------------------------ def _get_control_value(self): """ Toolkit specific method to get the control's value. """ return time(*self.control.GetTime()) def _set_control_value(self, value): """ Toolkit specific method to set the control's value. """ self.control.SetTime(value.hour, value.minute, value.second) wxdatetime = wx.DateTime.Now() wxdatetime.SetHour(value.hour) wxdatetime.SetMinute(value.minute) wxdatetime.SetSecond(value.second) event = wx.adv.DateEvent( self.control, wxdatetime, wx.adv.EVT_TIME_CHANGED.typeId ) wx.PostEvent(self.control.GetEventHandler(), event) def _observe_control_value(self, remove=False): """ Toolkit specific method to change the control value observer. """ if remove: self.control.Unbind( wx.adv.EVT_TIME_CHANGED, handler=self._update_value ) else: self.control.Bind(wx.adv.EVT_TIME_CHANGED, self._update_value) pyface-7.4.0/pyface/ui/wx/beep.py0000644000076500000240000000106114176222673017551 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 2012 Philip Chimento """Sound the system bell, Wx implementation.""" import wx def beep(): """Sound the system bell.""" wx.Bell() pyface-7.4.0/pyface/ui/wx/image_list.py0000644000076500000240000000620714176222673020762 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 cached image list. """ import wx from .image_resource import ImageResource # fixme: rename to 'CachedImageList'?!? class ImageList(wx.ImageList): """ A cached image list. """ def __init__(self, width, height): """ Creates a new cached image list. """ # Base-class constructor. wx.ImageList.__init__(self, width, height) self._width = width self._height = height # Cache of the indexes of the images in the list! self._cache = {} # {filename : index} return # ------------------------------------------------------------------------ # 'ImageList' interface. # ------------------------------------------------------------------------ def GetIndex(self, filename): """ Returns the index of the specified image. The image will be loaded and added to the image list if it is not already there. """ # Try the cache first. index = self._cache.get(filename) if index is None: # Were we passed an image resource? if isinstance(filename, ImageResource): # Create an image. image = filename.create_image(size=(self._width, self._height)) # If the filename is a string then it is the filename of some kind # of image (e.g 'foo.gif', 'image/foo.png' etc). elif isinstance(filename, str): # Load the image from the file. image = wx.Image(filename, wx.BITMAP_TYPE_ANY) # Otherwise the filename is *actually* an icon (in our case, # probably related to a MIME type). else: # Create a bitmap from the icon. bmp = wx.Bitmap(self._width, self._height) bmp.CopyFromIcon(filename) # Turn it into an image so that we can scale it. image = wx.ImageFromBitmap(bmp) # We force all images in the cache to be the same size. self._scale(image) # We also force them to be bitmaps! bmp = image.ConvertToBitmap() # Add the bitmap to the actual list... index = self.Add(bmp) # ... and update the cache. self._cache[filename] = index return index # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _scale(self, image): """ Scales the specified image (if necessary). """ if ( image.GetWidth() != self._width or image.GetHeight() != self._height ): image.Rescale(self._width, self._height) return image pyface-7.4.0/pyface/ui/wx/xrc_dialog.py0000644000076500000240000000747214176222673020765 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 dialog that is loaded from an XRC resource file. """ import os.path # Major packages. import wx import wx.xrc from traits.api import Instance, Str import traits.util.resource from .dialog import Dialog # ---------------------------------------------------------------------------- # class 'XrcDialog' # ---------------------------------------------------------------------------- class XrcDialog(Dialog): """A dialog that is loaded from an XRC resource file. """ # ------------------------------------------------------------------------ # Traits # ------------------------------------------------------------------------ # 'XrcDialog' interface -------------------------------------------- # Path to the xrc file relative to the class's module xrc_file = Str() # The ID of the dialog in the file id = Str("dialog") # The resource object resource = Instance(wx.xrc.XmlResource) # ------------------------------------------------------------------------ # 'Dialog' interface # ------------------------------------------------------------------------ def _create_control(self, parent): """ Creates the dialog and loads it in from the resource file. """ classpath = traits.util.resource.get_path(self) path = os.path.join(classpath, self.xrc_file) self.resource = wx.xrc.XmlResource(path) return self.resource.LoadDialog(parent, self.id) def _create_contents(self, dialog): """ Calls add_handlers. The actual content is created in _create_control by loading a resource file. """ # Wire up the standard buttons # We change the ID on OK and CANCEL to the standard ids # so we get the default behavior okbutton = self.XRCCTRL("OK") if okbutton is not None: # Change the ID and set the handler okbutton.SetId(wx.ID_OK) self.control.Bind(wx.EVT_BUTTON, okbutton.GetId(), self._on_ok) cancelbutton = self.XRCCTRL("CANCEL") if cancelbutton is not None: # Change the ID and set the handler cancelbutton.SetId(wx.ID_CANCEL) self.control.Bind( wx.EVT_BUTTON, self._on_cancel, cancelbutton.GetId() ) helpbutton = self.XRCCTRL("HELP") if helpbutton is not None: self.control.Bind(wx.EVT_BUTTON, self._on_help, helpbutton.GetId()) self._add_handlers() # ------------------------------------------------------------------------ # 'XrcDialog' interface # ------------------------------------------------------------------------ def XRCID(self, name): """ Returns the numeric widget id for the given name. """ return wx.xrc.XRCID(name) def XRCCTRL(self, name): """ Returns the control with the given name. """ return self.control.Window.FindWindowById(self.XRCID(name)) def set_validator(self, name, validator): """ Sets the validator on the named control. """ self.XRCCTRL(name).SetValidator(validator) # ------------------------------------------------------------------------ # 'XrcDialog' protected interface # ------------------------------------------------------------------------ def _add_handlers(self): """ Override to add event handlers. """ return pyface-7.4.0/pyface/ui/wx/about_dialog.py0000644000076500000240000001017114176222673021271 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Enthought pyface package component """ import sys import wx import wx.html import wx.lib.wxpTag from traits.api import List, provides, Str from pyface.i_about_dialog import IAboutDialog, MAboutDialog from pyface.ui_traits import Image from .dialog import Dialog from .image_resource import ImageResource _DIALOG_TEXT = """

%s

Python %s
wxPython %s

%s

Copyright © 2003-2010 Enthought, Inc.

""" @provides(IAboutDialog) class AboutDialog(MAboutDialog, Dialog): """ The toolkit specific implementation of an AboutDialog. See the IAboutDialog interface for the API documentation. """ # 'IAboutDialog' interface --------------------------------------------- additions = List(Str) copyrights = List(Str) image = Image(ImageResource("about")) # ------------------------------------------------------------------------ # Protected 'IDialog' interface. # ------------------------------------------------------------------------ def _create_contents(self, parent): if parent.GetParent() is not None: title = parent.GetParent().GetTitle() else: title = "" # Set the title. self.title = "About %s" % title # Load the image to be displayed in the about box. image = self.image.create_image() # The width of a wx HTML window is fixed (and is given in the # constructor). We set it to the width of the image plus a fudge # factor! The height of the window depends on the content. width = image.GetWidth() + 80 html = wx.html.HtmlWindow(parent, -1, size=(width, -1)) # Set the page contents. html.SetPage(self._create_html()) # Make the 'OK' button the default button. ok_button = parent.FindWindowById( wx.ID_OK ) # html.Window.FindWindowById(wx.ID_OK) ok_button.SetDefault() # Set the height of the HTML window to match the height of the content. internal = html.GetInternalRepresentation() html.SetSize((-1, internal.GetHeight())) # Make the dialog client area big enough to display the HTML window. # We add a fudge factor to the height here, although I'm not sure why # it should be necessary, the HTML window should report its required # size!?! width, height = html.GetSize().Get() parent.SetClientSize((width, height + 10)) def _create_html(self): # Load the image to be displayed in the about box. path = self.image.absolute_path # The additional strings. additions = "
".join(self.additions) # Get the version numbers. py_version = sys.version[0:sys.version.find("(")] wx_version = wx.VERSION_STRING # The additional copyright strings. copyrights = "
".join( ["Copyright © %s" % line for line in self.copyrights] ) # Get the text of the OK button. if self.ok_label is None: ok = "OK" else: ok = self.ok_label return _DIALOG_TEXT % ( path, additions, py_version, wx_version, copyrights, ok, ) pyface-7.4.0/pyface/ui/wx/python_editor.py0000644000076500000240000002103214176222673021525 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Enthought pyface package component """ import warnings import wx.stc from traits.api import Bool, Event, provides, Str from pyface.i_python_editor import IPythonEditor, MPythonEditor from pyface.key_pressed_event import KeyPressedEvent from pyface.wx.python_stc import PythonSTC, faces from .layout_widget import LayoutWidget @provides(IPythonEditor) class PythonEditor(MPythonEditor, LayoutWidget): """ The toolkit specific implementation of a PythonEditor. See the IPythonEditor interface for the API documentation. """ # 'IPythonEditor' interface -------------------------------------------- dirty = Bool(False) path = Str() show_line_numbers = Bool(True) # Events ---- changed = Event() key_pressed = Event(KeyPressedEvent) # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, parent=None, **traits): """ Creates a new pager. """ create = traits.pop("create", True) # Base class constructor. super().__init__(parent=parent, **traits) if create: # Create the widget's toolkit-specific control. self.create() warnings.warn( "automatic widget creation is deprecated and will be removed " "in a future Pyface version, use create=False and explicitly " "call create() for future behaviour", PendingDeprecationWarning, ) return # ------------------------------------------------------------------------ # 'PythonEditor' interface. # ------------------------------------------------------------------------ def load(self, path=None): """ Loads the contents of the editor. """ if path is None: path = self.path # We will have no path for a new script. if len(path) > 0: f = open(self.path, "r") text = f.read() f.close() else: text = "" self.control.SetText(text) self.dirty = False def save(self, path=None): """ Saves the contents of the editor. """ if path is None: path = self.path f = open(path, "w") f.write(self.control.GetText()) f.close() self.dirty = False def set_style(self, n, fore, back): self.control.StyleSetForeground(n, fore) self.control.StyleSetBackground(n, back) self.control.StyleSetFaceName(n, "courier new") self.control.StyleSetSize(n, faces["size"]) def select_line(self, lineno): """ Selects the specified line. """ start = self.control.PositionFromLine(lineno) end = self.control.GetLineEndPosition(lineno) self.control.SetSelection(start, end) return # ------------------------------------------------------------------------ # Trait handlers. # ------------------------------------------------------------------------ def _path_changed(self): """ Handle a change to path. """ self._changed_path() return # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _create_control(self, parent): """ Creates the toolkit-specific control for the widget. """ # Base-class constructor. self.control = stc = PythonSTC(parent, -1) # No folding! stc.SetProperty("fold", "0") # Mark the maximum line size. stc.SetEdgeMode(wx.stc.STC_EDGE_LINE) stc.SetEdgeColumn(79) # Display line numbers in the margin. if self.show_line_numbers: stc.SetMarginType(1, wx.stc.STC_MARGIN_NUMBER) stc.SetMarginWidth(1, 45) self.set_style(wx.stc.STC_STYLE_LINENUMBER, "#000000", "#c0c0c0") else: stc.SetMarginWidth(1, 4) self.set_style(wx.stc.STC_STYLE_LINENUMBER, "#ffffff", "#ffffff") # Create 'tabs' out of spaces! stc.SetUseTabs(False) # One 'tab' is 4 spaces. stc.SetIndent(4) # Line ending mode. stc.SetEOLMode(wx.stc.STC_EOL_LF) # Unix # self.SetEOLMode(wx.stc.STC_EOL_CR) # Apple Mac # self.SetEOLMode(wx.stc.STC_EOL_CRLF) # Windows # ------------------------------------------------------------------------ # Global styles for all languages. # ------------------------------------------------------------------------ self.set_style(wx.stc.STC_STYLE_DEFAULT, "#000000", "#ffffff") self.set_style(wx.stc.STC_STYLE_CONTROLCHAR, "#000000", "#ffffff") self.set_style(wx.stc.STC_STYLE_BRACELIGHT, "#000000", "#ffffff") self.set_style(wx.stc.STC_STYLE_BRACEBAD, "#000000", "#ffffff") # ------------------------------------------------------------------------ # Python styles. # ------------------------------------------------------------------------ # White space self.set_style(wx.stc.STC_P_DEFAULT, "#000000", "#ffffff") # Comment self.set_style(wx.stc.STC_P_COMMENTLINE, "#007f00", "#ffffff") # Number self.set_style(wx.stc.STC_P_NUMBER, "#007f7f", "#ffffff") # String self.set_style(wx.stc.STC_P_STRING, "#7f007f", "#ffffff") # Single quoted string self.set_style(wx.stc.STC_P_CHARACTER, "#7f007f", "#ffffff") # Keyword self.set_style(wx.stc.STC_P_WORD, "#00007f", "#ffffff") # Triple quotes self.set_style(wx.stc.STC_P_TRIPLE, "#7f0000", "#ffffff") # Triple double quotes self.set_style(wx.stc.STC_P_TRIPLEDOUBLE, "#ff0000", "#ffffff") # Class name definition self.set_style(wx.stc.STC_P_CLASSNAME, "#0000ff", "#ffffff") # Function or method name definition self.set_style(wx.stc.STC_P_DEFNAME, "#007f7f", "#ffffff") # Operators self.set_style(wx.stc.STC_P_OPERATOR, "#000000", "#ffffff") # Identifiers self.set_style(wx.stc.STC_P_IDENTIFIER, "#000000", "#ffffff") # Comment-blocks self.set_style(wx.stc.STC_P_COMMENTBLOCK, "#007f00", "#ffffff") # End of line where string is not closed self.set_style(wx.stc.STC_P_STRINGEOL, "#000000", "#ffffff") # ------------------------------------------------------------------------ # Events. # ------------------------------------------------------------------------ # By default, the will fire EVT_STC_CHANGE evented for all mask values # (STC_MODEVENTMASKALL). This generates too many events. stc.SetModEventMask( wx.stc.STC_MOD_INSERTTEXT | wx.stc.STC_MOD_DELETETEXT | wx.stc.STC_PERFORMED_UNDO | wx.stc.STC_PERFORMED_REDO ) # Listen for changes to the file. stc.Bind(wx.stc.EVT_STC_CHANGE, self._on_stc_changed) # Listen for key press events. stc.Bind(wx.EVT_CHAR, self._on_char) # Load the editor's contents. self.load() return stc def destroy(self): """ Destroy the toolkit control. """ if self.control is not None: self.control.Unbind(wx.stc.EVT_STC_CHANGE) self.control.Unbind(wx.EVT_CHAR) super().destroy() # wx event handlers ---------------------------------------------------- def _on_stc_changed(self, event): """ Called whenever a change is made to the text of the document. """ self.dirty = True self.changed = True # Give other event handlers a chance. event.Skip() def _on_char(self, event): """ Called whenever a change is made to the text of the document. """ self.key_pressed = KeyPressedEvent( alt_down=event.altDown, control_down=event.controlDown, shift_down=event.shiftDown, key_code=event.KeyCode, event=event, ) # Give other event handlers a chance. event.Skip() return pyface-7.4.0/pyface/ui/wx/init.py0000644000076500000240000000216614176222673017610 0ustar cwebsterstaff00000000000000# (C) Copyright 2007 Riverbank Computing Limited # (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.trait_notifiers import set_ui_handler, ui_handler from pyface.base_toolkit import Toolkit from .gui import GUI # It's possible that it has already been initialised. _app = wx.GetApp() if _app is None: _app = wx.App() # stop logging to a modal window by default # (apps can override by setting a different active target) _log = wx.LogStderr() wx.Log.SetActiveTarget(_log) # create the toolkit object toolkit_object = Toolkit("pyface", "wx", "pyface.ui.wx") # ensure that Traits has a UI handler appropriate for the toolkit. if ui_handler is None: # Tell the traits notification handlers to use this UI handler set_ui_handler(GUI.invoke_later) pyface-7.4.0/pyface/ui/wx/clipboard.py0000644000076500000240000001610414176222673020601 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 from io import BytesIO import logging from pickle import dumps, load, loads import wx from traits.api import provides from pyface.i_clipboard import IClipboard, BaseClipboard logger = logging.getLogger(__name__) # Data formats PythonObjectFormat = wx.DataFormat("PythonObject") TextFormat = wx.DataFormat(wx.DF_TEXT) FileFormat = wx.DataFormat(wx.DF_FILENAME) # Shortcuts cb = wx.TheClipboard @contextmanager def _ensure_clipboard(): """ Ensure use of X11 clipboard rather than primary selection on X11. X11 allows pasting from either the clipboard or the primary selection. This context manager ensures that the clipboard is always used for Pyface, no matter what the wider application state is currently using. On non-X11 platforms this does nothing. """ using_primary = cb.IsUsingPrimarySelection() if using_primary: cb.UsePrimarySelection(False) try: yield finally: cb.UsePrimarySelection(True) else: yield @contextmanager def _close_clipboard(flush=False): """ Ensures clipboard is closed and (optionally) flushed. Parameters ---------- flush : bool Whether or not to flush the clipboard. Should be true when setting data to the clipboard. """ try: yield finally: cb.Close() if flush: cb.Flush() @provides(IClipboard) class Clipboard(BaseClipboard): """ WxPython implementation of the IClipboard interface. Python object data is transmitted as bytes consisting of the pickled class object followed by the corresponding pickled instance object. This means that copy/paste of Python objects may not work unless compatible Python libraries are available at the pasting location. """ # --------------------------------------------------------------------------- # 'data' property methods: # --------------------------------------------------------------------------- def _get_has_data(self): result = False with _ensure_clipboard(): if cb.Open(): with _close_clipboard(): result = ( cb.IsSupported(TextFormat) or cb.IsSupported(FileFormat) or cb.IsSupported(PythonObjectFormat) ) return result # --------------------------------------------------------------------------- # 'object_data' property methods: # --------------------------------------------------------------------------- def _get_object_data(self): result = None with _ensure_clipboard(): if cb.Open(): with _close_clipboard(): if cb.IsSupported(PythonObjectFormat): cdo = wx.CustomDataObject(PythonObjectFormat) if cb.GetData(cdo): file = BytesIO(cdo.GetData()) _ = load(file) result = load(file) return result def _set_object_data(self, data): with _ensure_clipboard(): if cb.Open(): with _close_clipboard(flush=True): cdo = wx.CustomDataObject(PythonObjectFormat) cdo.SetData(dumps(data.__class__) + dumps(data)) # fixme: There seem to be cases where the '-1' value creates # pickles that can't be unpickled (e.g. some TraitDictObject's) # cdo.SetData(dumps(data, -1)) cb.SetData(cdo) def _get_has_object_data(self): return self._has_this_data(PythonObjectFormat) def _get_object_type(self): result = "" with _ensure_clipboard(): if cb.Open(): with _close_clipboard(): if cb.IsSupported(PythonObjectFormat): cdo = wx.CustomDataObject(PythonObjectFormat) if cb.GetData(cdo): try: # We may not be able to load the required class: result = loads(cdo.GetData()) except Exception: logger.exception("Cannot load data from clipboard.") return result # --------------------------------------------------------------------------- # 'text_data' property methods: # --------------------------------------------------------------------------- def _get_text_data(self): result = "" with _ensure_clipboard(): if cb.Open(): with _close_clipboard(): if cb.IsSupported(TextFormat): tdo = wx.TextDataObject() if cb.GetData(tdo): result = tdo.GetText() return result def _set_text_data(self, data): with _ensure_clipboard(): if cb.Open(): with _close_clipboard(flush=True): cb.SetData(wx.TextDataObject(str(data))) def _get_has_text_data(self): return self._has_this_data(TextFormat) # --------------------------------------------------------------------------- # 'file_data' property methods: # --------------------------------------------------------------------------- def _get_file_data(self): with _ensure_clipboard(): result = [] if cb.Open(): with _close_clipboard(): if cb.IsSupported(FileFormat): tfo = wx.FileDataObject() if cb.GetData(tfo): result = tfo.GetFilenames() return result def _set_file_data(self, data): with _ensure_clipboard(): if cb.Open(): with _close_clipboard(flush=True): tfo = wx.FileDataObject() if isinstance(data, str): tfo.AddFile(data) else: for filename in data: tfo.AddFile(filename) cb.SetData(tfo) def _get_has_file_data(self): return self._has_this_data(FileFormat) # --------------------------------------------------------------------------- # Private helper methods: # --------------------------------------------------------------------------- def _has_this_data(self, format): result = False with _ensure_clipboard(): if cb.Open(): with _close_clipboard(): result = cb.IsSupported(format) return result pyface-7.4.0/pyface/ui/wx/data_view/0000755000076500000240000000000014176460551020230 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/wx/data_view/data_wrapper.py0000644000076500000240000000523414176222673023260 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 wx import CustomDataObject, DataFormat, DataObject, DataObjectComposite from traits.api import Instance, provides from pyface.data_view.i_data_wrapper import IDataWrapper, MDataWrapper @provides(IDataWrapper) class DataWrapper(MDataWrapper): """ WxPython implementaton of IDataWrapper. This wraps a DataObjectComposite which is assumed to contain a collection of CustomDataObjects that store data associated by mimetype. Any other DataObjects in the DataObjectComposite are ignored. """ #: We always have a a composite data object with custom data objects in it toolkit_data = Instance( DataObjectComposite, args=(), allow_none=False, ) def mimetypes(self): """ Return a set of mimetypes holding data. Returns ------- mimetypes : set of str The set of mimetypes currently storing data in the toolkit data object. """ return { wx_format.GetId() for wx_format in self.toolkit_data.GetAllFormats() } def get_mimedata(self, mimetype): """ Get raw data for the given media type. Parameters ---------- mimetype : str The mime media type to be extracted. Returns ------- mimedata : bytes The mime media data as bytes. """ wx_format = DataFormat(mimetype) if self.toolkit_data.IsSupported(wx_format): data_object = self.toolkit_data.GetObject(wx_format) if isinstance(data_object, CustomDataObject): return bytes(data_object.GetData()) return None def set_mimedata(self, mimetype, raw_data): """ Set raw data for the given media type. Parameters ---------- mimetype : str The mime media type to be extracted. mimedata : bytes The mime media data encoded as bytes.. """ wx_format = DataFormat(mimetype) if self.toolkit_data.IsSupported(wx_format, dir=DataObject.Set): data_object = self.toolkit_data.GetObject(wx_format) else: data_object = CustomDataObject(wx_format) self.toolkit_data.Add(data_object) data_object.SetData(raw_data) pyface-7.4.0/pyface/ui/wx/data_view/tests/0000755000076500000240000000000014176460551021372 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/wx/data_view/tests/__init__.py0000644000076500000240000000000014176222673023472 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/wx/data_view/tests/test_data_wrapper.py0000644000076500000240000000415714176222673025464 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 wx except ImportError: wx_available = False else: from pyface.ui.wx.data_view.data_wrapper import DataWrapper wx_available = True @unittest.skipUnless(wx_available, "Test requires wx") class TestDataWrapper(unittest.TestCase): def test_get_mimedata(self): toolkit_data = wx.DataObjectComposite() text_data = wx.CustomDataObject(wx.DataFormat('text/plain')) text_data.SetData(b'hello world') toolkit_data.Add(text_data) data_wrapper = DataWrapper(toolkit_data=toolkit_data) self.assertEqual(data_wrapper.mimetypes(), {'text/plain'}) self.assertEqual(data_wrapper.get_mimedata('text/plain'), b'hello world') def test_set_mimedata(self): data_wrapper = DataWrapper() toolkit_data = data_wrapper.toolkit_data data_wrapper.set_mimedata('text/plain', b'hello world') self.assertEqual(data_wrapper.mimetypes(), {'text/plain'}) wx_format = wx.DataFormat('text/plain') self.assertTrue(toolkit_data.IsSupported(wx_format)) text_data = toolkit_data.GetObject(wx_format) self.assertEqual(text_data.GetData(), b'hello world') def test_ignore_non_custom(self): toolkit_data = wx.DataObjectComposite() html_data = wx.HTMLDataObject() html_data.SetHTML("hello world") toolkit_data.Add(html_data) text_data = wx.CustomDataObject(wx.DataFormat('text/plain')) text_data.SetData(b'hello world') toolkit_data.Add(text_data) data_wrapper = DataWrapper(toolkit_data=toolkit_data) self.assertTrue('text/plain' in data_wrapper.mimetypes()) self.assertEqual(data_wrapper.get_mimedata('text/plain'), b'hello world') pyface-7.4.0/pyface/ui/wx/data_view/__init__.py0000644000076500000240000000000014176222673022330 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/wx/data_view/data_view_model.py0000644000076500000240000001461014176222673023730 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 from pyface.data_view.data_view_errors import ( DataViewGetError, DataViewSetError ) from pyface.data_view.index_manager import Root from wx.dataview import DataViewItem, DataViewModel as wxDataViewModel logger = logging.getLogger(__name__) # XXX This file is scaffolding and may need to be rewritten or expanded class DataViewModel(wxDataViewModel): """ A wxDataViewModel that understands AbstractDataModels. """ def __init__(self, model): super().__init__() self.model = model @property def model(self): return self._model @model.setter def model(self, model): if hasattr(self, '_model'): # disconnect trait listeners self._model.observe( self.on_structure_changed, 'structure_changed', dispatch='ui', remove=True, ) self._model.observe( self.on_values_changed, 'values_changed', dispatch='ui', remove=True, ) self._model = model else: # model is being initialized self._model = model # hook up trait listeners self._model.observe( self.on_structure_changed, 'structure_changed', dispatch='ui', ) self._model.observe( self.on_values_changed, 'values_changed', dispatch='ui', ) def on_structure_changed(self, event): self.Cleared() def on_values_changed(self, event): top, left, bottom, right = event.new if top == () and bottom == (): # this is a column header change, reset everything self.Cleared() elif left == () and right == (): # this is a row header change # XXX this is currently not supported and not needed pass else: for i, (top_row, bottom_row) in enumerate(zip(top, bottom)): if top_row != bottom_row: break top = top[:i+1] bottom = bottom[:i+1] if top == bottom and left == right: # single value change self.ValueChanged(self._to_item(top), left[0]) elif top == bottom: # single item change self.ItemChanged(self._to_item(top)) else: # multiple item change items = [ self._to_item(top[:i] + [row]) for row in range(top[i], bottom[i]+1) ] self.ItemsChanged(items) def GetParent(self, item): index = self._to_index(item) if index == Root: return DataViewItem() parent, row = self.model.index_manager.get_parent_and_row(index) parent_id = self.model.index_manager.id(parent) if parent_id == 0: return DataViewItem() return DataViewItem(parent_id) def GetChildren(self, item, children): index = self._to_index(item) row_index = self.model.index_manager.to_sequence(index) n_children = self.model.get_row_count(row_index) for i in range(n_children): child_index = self.model.index_manager.create_index(index, i) child_id = self.model.index_manager.id(child_index) children.append(DataViewItem(child_id)) return n_children def IsContainer(self, item): row_index = self._to_row_index(item) return self.model.can_have_children(row_index) def HasValue(self, item, column): return True def HasChildren(self, item): row_index = self._to_row_index(item) return self.model.has_child_rows(row_index) def GetValue(self, item, column): row_index = self._to_row_index(item) if column == 0: column_index = () else: column_index = (column - 1,) value_type = self.model.get_value_type(row_index, column_index) try: if value_type.has_text(self.model, row_index, column_index): return value_type.get_text(self.model, row_index, column_index) except DataViewGetError: return '' except Exception: # unexpected error, log and raise logger.exception( "get data failed: row %r, column %r", row_index, column_index, ) raise return '' def SetValue(self, value, item, column): row_index = self._to_row_index(item) if column == 0: column_index = () else: column_index = (column - 1,) try: value_type = self.model.get_value_type(row_index, column_index) value_type.set_text(self.model, row_index, column_index, value) except DataViewSetError: return False except Exception: logger.exception( "SetValue failed: row %r, column %r, value %r", row_index, column_index, value, ) return False else: return True def GetColumnCount(self): return self.model.get_column_count() + 1 def GetColumnType(self, column): # XXX This may need refinement when we deal with different editor types return "string" def _to_row_index(self, item): id = item.GetID() if id is None: id = 0 index = self.model.index_manager.from_id(int(id)) return self.model.index_manager.to_sequence(index) def _to_item(self, row_index): if len(row_index) == 0: return DataViewItem() index = self.model.index_manager.from_sequence(row_index) id = self.model.index_manager.id(index) return DataViewItem(id) def _to_index(self, item): id = item.GetID() if id is None: id = 0 return self.model.index_manager.from_id(int(id)) pyface-7.4.0/pyface/ui/wx/data_view/data_view_widget.py0000644000076500000240000001663714176222673024126 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 import wx from wx.dataview import ( DataViewCtrl, DataViewEvent, DataViewItemArray, DataViewModel as wxDataViewModel, DATAVIEW_CELL_EDITABLE, DATAVIEW_CELL_ACTIVATABLE, DV_MULTIPLE, DV_NO_HEADER, EVT_DATAVIEW_SELECTION_CHANGED, wxEVT_DATAVIEW_SELECTION_CHANGED ) from traits.api import Enum, Instance, observe, provides from pyface.data_view.i_data_view_widget import ( IDataViewWidget, MDataViewWidget ) from pyface.data_view.data_view_errors import DataViewGetError from pyface.ui.wx.layout_widget import LayoutWidget from .data_view_model import DataViewModel logger = logging.getLogger(__name__) # XXX This file is scaffolding and may need to be rewritten @provides(IDataViewWidget) class DataViewWidget(MDataViewWidget, LayoutWidget): """ The Wx implementation of the DataViewWidget. """ #: What can be selected. selection_type = Enum("row") #: How selections are modified. selection_mode = Enum("extended", "single") # Private traits -------------------------------------------------------- #: The QAbstractItemModel instance used by the view. This will #: usually be a DataViewModel subclass. _item_model = Instance(wxDataViewModel) # ------------------------------------------------------------------------ # IDataViewWidget Interface # ------------------------------------------------------------------------ def _create_item_model(self): """ Create the DataViewItemModel which wraps the data model. """ self._item_model = DataViewModel(self.data_model) def _get_control_header_visible(self): """ Method to get the control's header visibility. """ return not self.control.GetWindowStyleFlag() & DV_NO_HEADER def _set_control_header_visible(self, header_visible): """ Method to set the control's header visibility. """ old_visible = self._get_control_header_visible() if header_visible != old_visible: self.control.ToggleWindowStyle(DV_NO_HEADER) def _get_control_selection_type(self): """ Toolkit specific method to get the selection type. """ return "row" def _set_control_selection_type(self, selection_type): """ Toolkit specific method to change the selection type. """ if selection_type != "row": warnings.warn( "{!r} selection_type not supported in Wx".format( selection_type ), RuntimeWarning, ) def _get_control_selection_mode(self): """ Toolkit specific method to get the selection mode. """ if self.control.GetWindowStyleFlag() & DV_MULTIPLE: return "extended" else: return "single" def _set_control_selection_mode(self, selection_mode): """ Toolkit specific method to change the selection mode. """ if selection_mode not in {'extended', 'single'}: warnings.warn( "{!r} selection_mode not supported in Wx".format( selection_mode ), RuntimeWarning, ) return old_mode = self._get_control_selection_mode() if selection_mode != old_mode: self.control.ToggleWindowStyle(DV_MULTIPLE) def _get_control_selection(self): """ Toolkit specific method to get the selection. """ return [ (self._item_model._to_row_index(item), ()) for item in self.control.GetSelections() ] def _set_control_selection(self, selection): """ Toolkit specific method to change the selection. """ wx_selection = DataViewItemArray() for row, column in selection: item = self._item_model._to_item(row) wx_selection.append(item) self.control.SetSelections(wx_selection) if wx.VERSION >= (4, 1): if len(wx_selection) > 0: item = wx_selection[-1] else: # a dummy item because we have nothing specific item = self._item_model._to_item((0,)) event = DataViewEvent( wxEVT_DATAVIEW_SELECTION_CHANGED, self.control, item, ) else: event = DataViewEvent( wxEVT_DATAVIEW_SELECTION_CHANGED, ) wx.PostEvent(self.control, event) def _observe_control_selection(self, remove=False): """ Toolkit specific method to watch for changes in the selection. """ if remove: self.control.Unbind( EVT_DATAVIEW_SELECTION_CHANGED, handler=self._update_selection, ) else: self.control.Bind( EVT_DATAVIEW_SELECTION_CHANGED, self._update_selection, ) # ------------------------------------------------------------------------ # Widget Interface # ------------------------------------------------------------------------ def _create_control(self, parent): """ Create the DataViewWidget's toolkit control. """ self._create_item_model() control = DataViewCtrl(parent) control.AssociateModel(self._item_model) # required for wxPython refcounting system self._item_model.DecRef() # create columns for view value_type = self._item_model.model.get_value_type([], []) try: text = value_type.get_text(self._item_model.model, [], []) except DataViewGetError: text = '' except Exception: # unexpected error, log and raise logger.exception("get header data failed: column ()") raise control.AppendTextColumn(text, 0, mode=DATAVIEW_CELL_ACTIVATABLE) for column in range(self._item_model.GetColumnCount()-1): value_type = self._item_model.model.get_value_type([], [column]) try: text = value_type.get_text(self._item_model.model, [], [column]) except DataViewGetError: text = '' except Exception: # unexpected error, log and raise logger.exception( "get header data failed: column (%r,)", column, ) raise control.AppendTextColumn( text, column+1, mode=DATAVIEW_CELL_EDITABLE, ) return control def destroy(self): """ Perform any actions required to destroy the control. """ super().destroy() # ensure that we release the reference to the item model self._item_model = None # ------------------------------------------------------------------------ # Private methods # ------------------------------------------------------------------------ # Trait observers @observe('data_model', dispatch='ui') def update_item_model(self, event): if self._item_model is not None: self._item_model.model = event.new pyface-7.4.0/pyface/ui/wx/gui.py0000644000076500000240000000756714176222673017443 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Enthought pyface package component """ import logging import sys import wx from traits.api import Bool, HasTraits, provides, Str from pyface.util.guisupport import start_event_loop_wx from pyface.i_gui import IGUI, MGUI # Logging. logger = logging.getLogger(__name__) @provides(IGUI) class GUI(MGUI, HasTraits): # 'GUI' interface -----------------------------------------------------# busy = Bool(False) started = Bool(False) state_location = Str() # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, splash_screen=None): # Display the (optional) splash screen. self._splash_screen = splash_screen if self._splash_screen is not None: self._splash_screen.open() # ------------------------------------------------------------------------ # 'GUI' class interface. # ------------------------------------------------------------------------ @classmethod def invoke_after(cls, millisecs, callable, *args, **kw): wx.CallLater(millisecs, callable, *args, **kw) @classmethod def invoke_later(cls, callable, *args, **kw): wx.CallAfter(callable, *args, **kw) @classmethod def set_trait_after(cls, millisecs, obj, trait_name, new): wx.CallLater(millisecs, setattr, obj, trait_name, new) @classmethod def set_trait_later(cls, obj, trait_name, new): wx.CallAfter(setattr, obj, trait_name, new) @staticmethod def process_events(allow_user_events=True): if allow_user_events: wx.GetApp().Yield(True) else: wx.SafeYield() @staticmethod def set_busy(busy=True): if busy: GUI._cursor = wx.BusyCursor() else: GUI._cursor = None # ------------------------------------------------------------------------ # 'GUI' interface. # ------------------------------------------------------------------------ def start_event_loop(self): """ Start the GUI event loop. """ if self._splash_screen is not None: self._splash_screen.close() # Make sure that we only set the 'started' trait after the main loop # has really started. self.set_trait_after(10, self, "started", True) # A hack to force menus to appear for applications run on Mac OS X. if sys.platform == "darwin": def _mac_os_x_hack(): f = wx.Frame(None, -1) f.Show(True) f.Close() self.invoke_later(_mac_os_x_hack) logger.debug("---------- starting GUI event loop ----------") start_event_loop_wx() self.started = False def stop_event_loop(self): """ Stop the GUI event loop. """ logger.debug("---------- stopping GUI event loop ----------") wx.GetApp().ExitMainLoop() # ------------------------------------------------------------------------ # Trait handlers. # ------------------------------------------------------------------------ def _state_location_default(self): """ The default state location handler. """ return self._default_state_location() def _busy_changed(self, new): """ The busy trait change handler. """ if new: self._wx_cursor = wx.BusyCursor() else: del self._wx_cursor return pyface-7.4.0/pyface/ui/wx/image_cache.py0000644000076500000240000000521514176222673021050 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Enthought pyface package component """ import wx from traits.api import HasTraits, provides from pyface.i_image_cache import IImageCache, MImageCache @provides(IImageCache) class ImageCache(MImageCache, HasTraits): """ The toolkit specific implementation of an ImageCache. See the IImageCache interface for the API documentation. """ # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, width, height): self._width = width self._height = height # The images in the cache! self._images = {} # {filename : wx.Image} # The images in the cache converted to bitmaps. self._bitmaps = {} # {filename : wx.Bitmap} return # ------------------------------------------------------------------------ # 'ImageCache' interface. # ------------------------------------------------------------------------ def get_image(self, filename): # Try the cache first. image = self._images.get(filename) if image is None: # Load the image from the file and add it to the list. # # N.B 'wx.BITMAP_TYPE_ANY' tells wxPython to attempt to autodetect # --- the image format. image = wx.Image(filename, wx.BITMAP_TYPE_ANY) # We force all images in the cache to be the same size. if ( image.GetWidth() != self._width or image.GetHeight() != self._height ): image.Rescale(self._width, self._height) # Add the bitmap to the cache! self._images[filename] = image return image def get_bitmap(self, filename): # Try the cache first. bmp = self._bitmaps.get(filename) if bmp is None: # Get the image. image = self.get_image(filename) # Convert the alpha channel to a mask. image.ConvertAlphaToMask() # Convert it to a bitmaps! bmp = image.ConvertToBitmap() # Add the bitmap to the cache! self._bitmaps[filename] = bmp return bmp pyface-7.4.0/pyface/ui/wx/file_dialog.py0000644000076500000240000000752314176222673021105 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Enthought pyface package component """ import os import wx from traits.api import Enum, Int, List, provides, Str from pyface.i_file_dialog import IFileDialog, MFileDialog from .dialog import Dialog @provides(IFileDialog) class FileDialog(MFileDialog, Dialog): """ The toolkit specific implementation of a FileDialog. See the IFileDialog interface for the API documentation. """ # 'IFileDialog' interface ---------------------------------------------# action = Enum("open", "open files", "save as") default_directory = Str() default_filename = Str() default_path = Str() directory = Str() filename = Str() path = Str() paths = List(Str) wildcard = Str() wildcard_index = Int(0) # ------------------------------------------------------------------------ # Protected 'IDialog' interface. # ------------------------------------------------------------------------ def _create_contents(self, parent): # In wx this is a canned dialog. pass # ------------------------------------------------------------------------ # 'IWindow' interface. # ------------------------------------------------------------------------ def close(self): # Get the path of the chosen directory. self.path = str(self.control.GetPath()) # Work around wx bug throwing exception on cancel of file dialog if len(self.path) > 0: self.paths = self.control.GetPaths() else: self.paths = [] # Extract the directory and filename. self.directory, self.filename = os.path.split(self.path) # Get the index of the selected filter. self.wildcard_index = self.control.GetFilterIndex() # Let the window close as normal. super().close() # ------------------------------------------------------------------------ # Protected 'IWidget' interface. # ------------------------------------------------------------------------ def _create_control(self, parent): # If the caller provided a default path instead of a default directory # and filename, split the path into it directory and filename # components. if ( len(self.default_path) != 0 and len(self.default_directory) == 0 and len(self.default_filename) == 0 ): default_directory, default_filename = os.path.split( self.default_path ) else: default_directory = self.default_directory default_filename = self.default_filename if self.action == "open": style = wx.FD_OPEN elif self.action == "open files": style = wx.FD_OPEN | wx.FD_MULTIPLE else: style = wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT # Create the actual dialog. dialog = wx.FileDialog( parent, self.title, defaultDir=default_directory, defaultFile=default_filename, style=style, wildcard=self.wildcard.rstrip("|"), ) dialog.SetFilterIndex(self.wildcard_index) return dialog # ------------------------------------------------------------------------ # Trait handlers. # ------------------------------------------------------------------------ def _wildcard_default(self): """ Return the default wildcard. """ return self.WILDCARD_ALL pyface-7.4.0/pyface/ui/wx/grid/0000755000076500000240000000000014176460551017212 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/wx/grid/composite_grid_model.py0000644000076500000240000003073714176222673023766 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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, Instance, List, Union from .grid_model import GridModel, GridRow class CompositeGridModel(GridModel): """ A CompositeGridModel is a model whose underlying data is a collection of other grid models. """ # The models this model is comprised of. data = List(Instance(GridModel)) # The rows in the model. rows = Union(None, List(Instance(GridRow))) # The cached data indexes. _data_index = Dict() # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, **traits): """ Create a CompositeGridModel object. """ # Base class constructor super().__init__(**traits) self._row_count = None # ------------------------------------------------------------------------ # 'GridModel' interface. # ------------------------------------------------------------------------ def get_column_count(self): """ Return the number of columns for this table. """ # for the composite grid model, this is simply the sum of the # column counts for the underlying models count = 0 for model in self.data: count += model.get_column_count() return count def get_column_name(self, index): """ Return the name of the column specified by the (zero-based) index. """ model, new_index = self._resolve_column_index(index) return model.get_column_name(new_index) def get_column_size(self, index): """ Return the size in pixels of the column indexed by col. A value of -1 or None means use the default. """ model, new_index = self._resolve_column_index(index) return model.get_column_size(new_index) def get_cols_drag_value(self, cols): """ Return the value to use when the specified columns are dragged or copied and pasted. cols is a list of column indexes. """ values = [] for col in cols: model, real_col = self._resolve_column_index(col) values.append(model.get_cols_drag_value([real_col])) return values def get_cols_selection_value(self, cols): """ Return the value to use when the specified cols are selected. This value should be enough to specify to other listeners what is going on in the grid. rows is a list of row indexes. """ return self.get_cols_drag_value(self, cols) def get_column_context_menu(self, col): """ Return a MenuManager object that will generate the appropriate context menu for this column.""" model, new_index = self._resolve_column_index(col) return model.get_column_context_menu(new_index) def sort_by_column(self, col, reverse=False): """ Sort model data by the column indexed by col. The reverse flag indicates that the sort should be done in reverse. """ pass def is_column_read_only(self, index): """ Return True if the column specified by the zero-based index is read-only. """ model, new_index = self._resolve_column_index(index) return model.is_column_read_only(new_index) def get_row_count(self): """ Return the number of rows for this table. """ # see if we've already calculated the row_count if self._row_count is None: row_count = 0 # return the maximum rows of any of the contained models for model in self.data: rows = model.get_row_count() if rows > row_count: row_count = rows # save the result for next time self._row_count = row_count return self._row_count def get_row_name(self, index): """ Return the name of the row specified by the (zero-based) index. """ label = None # if the rows list exists then grab the label from there... if self.rows is not None: if len(self.rows) > index: label = self.rows[index].label # ... otherwise generate it from the zero-based index. else: label = str(index + 1) return label def get_rows_drag_value(self, rows): """ Return the value to use when the specified rows are dragged or copied and pasted. rows is a list of row indexes. """ row_values = [] for rindex in rows: row = [] for model in self.data: new_data = model.get_rows_drag_value([rindex]) # if it's a list then we assume that it represents more than # one column's worth of values if isinstance(new_data, list): row.extend(new_data) else: row.append(new_data) # now save our new row value row_values.append(row) return row_values def is_row_read_only(self, index): """ Return True if the row specified by the zero-based index is read-only. """ read_only = False if self.rows is not None and len(self.rows) > index: read_only = self.rows[index].read_only return read_only def get_type(self, row, col): """ Return the type of the value stored in the table at (row, col). """ model, new_col = self._resolve_column_index(col) return model.get_type(row, new_col) def get_value(self, row, col): """ Return the value stored in the table at (row, col). """ model, new_col = self._resolve_column_index(col) return model.get_value(row, new_col) def get_cell_selection_value(self, row, col): """ Return the value stored in the table at (row, col). """ model, new_col = self._resolve_column_index(col) return model.get_cell_selection_value(row, new_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. Note that the objects in selection_list are model-specific. """ coords = [] for selection in selection_list: # we have to look through each of the models in order # for the selected object for model in self.data: cells = model.resolve_selection([selection]) # we know this model found the object if cells comes back # non-empty if cells is not None and len(cells) > 0: coords.extend(cells) break return coords # fixme: this context menu stuff is going in here for now, but it # seems like this is really more of a view piece than a model piece. # this is how the tree control does it, however, so we're duplicating # that here. def get_cell_context_menu(self, row, col): """ Return a MenuManager object that will generate the appropriate context menu for this cell.""" model, new_col = self._resolve_column_index(col) return model.get_cell_context_menu(row, new_col) def is_cell_empty(self, row, col): """ Returns True if the cell at (row, col) has a None value, False otherwise.""" model, new_col = self._resolve_column_index(col) if model is None: return True else: return model.is_cell_empty(row, new_col) def is_cell_editable(self, row, col): """ Returns True if the cell at (row, col) is editable, False otherwise. """ model, new_col = self._resolve_column_index(col) return model.is_cell_editable(row, new_col) def is_cell_read_only(self, row, col): """ Returns True if the cell at (row, col) is not editable, False otherwise. """ model, new_col = self._resolve_column_index(col) return model.is_cell_read_only(row, new_col) def get_cell_bg_color(self, row, col): """ Return a wxColour object specifying what the background color of the specified cell should be. """ model, new_col = self._resolve_column_index(col) return model.get_cell_bg_color(row, new_col) def get_cell_text_color(self, row, col): """ Return a wxColour object specifying what the text color of the specified cell should be. """ model, new_col = self._resolve_column_index(col) return model.get_cell_text_color(row, new_col) def get_cell_font(self, row, col): """ Return a wxFont object specifying what the font of the specified cell should be. """ model, new_col = self._resolve_column_index(col) return model.get_cell_font(row, new_col) def get_cell_halignment(self, row, col): """ Return a string specifying what the horizontal alignment of the specified cell should be. Return 'left' for left alignment, 'right' for right alignment, or 'center' for center alignment. """ model, new_col = self._resolve_column_index(col) return model.get_cell_halignment(row, new_col) def get_cell_valignment(self, row, col): """ Return a string specifying what the vertical alignment of the specified cell should be. Return 'top' for top alignment, 'bottom' for bottom alignment, or 'center' for center alignment. """ model, new_col = self._resolve_column_index(col) return model.get_cell_valignment(row, new_col) # ------------------------------------------------------------------------ # protected 'GridModel' interface. # ------------------------------------------------------------------------ def _delete_rows(self, pos, num_rows): """ Implementation method for delete_rows. Should return the number of rows that were deleted. """ for model in self.data: model._delete_rows(pos, num_rows) return num_rows def _insert_rows(self, pos, num_rows): """ Implementation method for insert_rows. Should return the number of rows that were inserted. """ for model in self.data: model._insert_rows(pos, num_rows) return num_rows def _set_value(self, row, col, value): """ Implementation method for set_value. Should return the number of rows, if any, that were appended. """ model, new_col = self._resolve_column_index(col) model._set_value(row, new_col, value) return 0 # ------------------------------------------------------------------------ # private interface # ------------------------------------------------------------------------ def _resolve_column_index(self, index): """ Resolves a column index into the correct model and adjusted index. Returns the target model and the corrected index. """ real_index = index cached = None # self._data_index.get(index) if cached is not None: model, col_index = cached else: model = None for m in self.data: cols = m.get_column_count() if real_index < cols: model = m break else: real_index -= cols self._data_index[index] = (model, real_index) return model, real_index def _data_changed(self): """ Called when the data trait is changed. Since this is called when our underlying models change, the cached results of the column lookups is wrong and needs to be invalidated. """ self._data_index.clear() def _data_items_changed(self): """ Called when the members of the data trait have changed. Since this is called when our underlying model change, the cached results of the column lookups is wrong and needs to be invalidated. """ self._data_index.clear() pyface-7.4.0/pyface/ui/wx/grid/grid.py0000644000076500000240000020041314176222673020512 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 grid control with a model/ui architecture. """ from os.path import abspath, exists import sys import warnings import wx import wx.lib.gridmovers as grid_movers from wx.grid import ( Grid as wxGrid, GridCellAttr, GridCellEditor, GridTableBase, GridTableMessage, GRIDTABLE_NOTIFY_ROWS_APPENDED, GRIDTABLE_NOTIFY_ROWS_DELETED, GRIDTABLE_NOTIFY_ROWS_INSERTED, GRIDTABLE_NOTIFY_COLS_APPENDED, GRIDTABLE_NOTIFY_COLS_DELETED, GRIDTABLE_NOTIFY_COLS_INSERTED, GRIDTABLE_REQUEST_VIEW_GET_VALUES, GRID_VALUE_STRING, ) from traits.api import ( Bool, Enum, Event, Instance, Int, Undefined, Union, ) from pyface.timer.api import do_later from pyface.ui.wx.layout_widget import LayoutWidget from pyface.ui_traits import TraitsUIColor as Color, TraitsUIFont as Font from pyface.wx.drag_and_drop import ( PythonDropSource, PythonDropTarget, ) from pyface.wx.drag_and_drop import clipboard as enClipboard, FileDropSource from .grid_model import GridModel # Is this code running on MS Windows? is_win32 = sys.platform == "win32" ASCII_C = 67 class Grid(LayoutWidget): """ A grid control with a model/ui architecture. """ # 'Grid' interface ----------------------------------------------------- # The model that provides the data for the grid. model = Instance(GridModel, ()) # Should grid lines be shown on the table? enable_lines = Bool(True) # The color to show gridlines in grid_line_color = Color("blue") # Show row headers? show_row_headers = Bool(True) # Show column headers? show_column_headers = Bool(True) # The default font to use for text in labels default_label_font = Font(None) # The default background color for labels default_label_bg_color = Color(wx.Colour(236, 233, 216)) # The default text color for labels default_label_text_color = Color("black") # The color to use for a selection background selection_bg_color = Union(None, Color, default_value=wx.Colour(49, 106, 197)) # The color to use for a selection foreground/text selection_text_color = Union(None, Color, default_value=wx.Colour(255, 255, 255)) # The default font to use for text in cells default_cell_font = Font(None) # The default text color to use for text in cells default_cell_text_color = Color("black") # The default background color to use for editable cells default_cell_bg_color = Color("white") # The default background color to use for read-only cells default_cell_read_only_color = Color(wx.Colour(248, 247, 241)) # Should the grid be read-only? If this is set to false, individual # cells can still declare themselves read-only. read_only = Bool(False) # Selection mode. selection_mode = Enum("cell", "rows", "cols", "") # Sort data when a column header is clicked? allow_column_sort = Bool(True) # Sort data when a row header is clicked? allow_row_sort = Bool(False) # pixel height of column labels column_label_height = Int(32) # pixel width of row labels row_label_width = Int(82) # auto-size columns and rows? autosize = Bool(False) # Allow single-click access to cell-editors? edit_on_first_click = Bool(True) # Events ---- # A cell has been activated (ie. double-clicked). cell_activated = Event() # The current selection has changed. selection_changed = Event() # A drag operation was started on a cell. cell_begin_drag = Event() # A left-click occurred on a cell. cell_left_clicked = Event() # A left-click occurred on a cell at specific location # Useful if the cell contains multiple controls though the hit test # is left to the consumer of the event cell_left_clicked_location = Event() # A right-click occurred on a cell. cell_right_clicked = Event() # protected variables to store the location of the clicked event _x_clicked = Int() _y_clicked = Int() # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, parent, **traits): """ Creates a new grid. 'parent' is the toolkit-specific control that is the grid's parent. """ create = traits.pop('create', True) # Base class constructors. super().__init__(parent=parent, **traits) if create: self.create() warnings.warn( "automatic widget creation is deprecated and will be removed " "in a future Pyface version, use create=False and explicitly " "call create() for future behaviour", PendingDeprecationWarning, ) def _create_control(self, parent): # Flag set when columns are resizing: self._user_col_size = False # Create the toolkit-specific control. self._grid = grid = wxGrid(parent, -1) grid.grid = self self._moveTo = None self._edit = False grid.Bind(wx.EVT_IDLE, self._on_idle) # Set when moving edit cursor: grid._no_reset_col = False grid._no_reset_row = False # initialize the current selection self.__current_selection = () self.__initialize_counts(self.model) self.__initialize_sort_state() # Don't display any extra space around the rows and columns. grid.SetMargins(0, 0) # Provides more accurate scrolling behavior without creating large # margins on the bottom and right. The down side is that it makes # scrolling using the scroll bar buttons painfully slow: grid.SetScrollLineX(1) grid.SetScrollLineY(1) # Tell the grid to get its data from the model. # # N.B The terminology used in the wxPython API is a little confusing! # --- The 'SetTable' method is actually setting the model used by # the grid (which is the view)! # # The second parameter to 'SetTable' tells the grid to take ownership # of the model and to destroy it when it is done. Otherwise you would # need to keep a reference to the model and manually destroy it later # (by calling it's Destroy method). # # fixme: We should create a default model if one is not supplied. # The wx virtual table hook. self._grid_table_base = _GridTableBase(self.model, self) # keep the onership of the table in this class grid.SetTable(self._grid_table_base, takeOwnership=False) # Enable column and row moving: grid_movers.GridColMover(grid) grid_movers.GridRowMover(grid) grid.Bind(grid_movers.EVT_GRID_COL_MOVE, self._on_col_move) grid.Bind(grid_movers.EVT_GRID_ROW_MOVE, self._on_row_move) self.model.observe(self._on_model_content_changed, "content_changed") self.model.observe( self._on_model_structure_changed, "structure_changed" ) self.model.observe(self._on_row_sort, "row_sorted") self.model.observe(self._on_column_sort, "column_sorted") self.observe(self._on_new_model, "model") # hook up style trait handlers - note that we have to use # dynamic notification hook-ups because these handlers should # not be called until after the control object is initialized. # static trait notifiers get called when the object inits. self.observe(self._on_enable_lines_changed, "enable_lines") self.observe(self._on_grid_line_color_changed, "grid_line_color") self.observe(self._on_default_label_font_changed, "default_label_font") self.observe( self._on_default_label_bg_color_changed, "default_label_bg_color" ) self.observe( self._on_default_label_text_color_changed, "default_label_text_color", ) self.observe(self._on_selection_bg_color_changed, "selection_bg_color") self.observe( self._on_selection_text_color_changed, "selection_text_color" ) self.observe(self._on_default_cell_font_changed, "default_cell_font") self.observe( self._on_default_cell_text_color_changed, "default_cell_text_color" ) self.observe( self._on_default_cell_bg_color_changed, "default_cell_bg_color" ) self.observe(self._on_read_only_changed, "read_only") self.observe(self._on_selection_mode_changed, "selection_mode") self.observe( self._on_column_label_height_changed, "column_label_height" ) self.observe(self._on_row_label_width_changed, "row_label_width") self.observe( self._on_show_column_headers_changed, "show_column_headers" ) self.observe(self._on_show_row_headers_changed, "show_row_headers") # Initialize wx handlers: self._notify_select = True grid.Bind(wx.grid.EVT_GRID_SELECT_CELL, self._on_select_cell) grid.Bind(wx.grid.EVT_GRID_RANGE_SELECT, self._on_range_select) grid.Bind(wx.grid.EVT_GRID_COL_SIZE, self._on_col_size) grid.Bind(wx.grid.EVT_GRID_ROW_SIZE, self._on_row_size) # This starts the cell editor on a double-click as well as on a second # click: grid.Bind(wx.grid.EVT_GRID_CELL_LEFT_DCLICK, self._on_cell_left_dclick) # Notify when cells are clicked on: grid.Bind(wx.grid.EVT_GRID_CELL_RIGHT_CLICK, self._on_cell_right_click) grid.Bind( wx.grid.EVT_GRID_CELL_RIGHT_DCLICK, self._on_cell_right_dclick ) grid.Bind( wx.grid.EVT_GRID_LABEL_RIGHT_CLICK, self._on_label_right_click ) grid.Bind(wx.grid.EVT_GRID_LABEL_LEFT_CLICK, self._on_label_left_click) if is_win32: grid.Bind(wx.grid.EVT_GRID_EDITOR_HIDDEN, self._on_editor_hidden) # We handle key presses to change the behavior of the and # keys to make manual data entry smoother. grid.Bind(wx.EVT_KEY_DOWN, self._on_key_down) # We handle control resize events to adjust column widths grid.Bind(wx.EVT_SIZE, self._on_size) # Handle drags: self._corner_window = grid.GetGridCornerLabelWindow() self._grid_window = gw = grid.GetGridWindow() self._row_window = rw = grid.GetGridRowLabelWindow() self._col_window = cw = grid.GetGridColLabelWindow() # Handle mouse button state changes: self._ignore = False for window in (gw, rw, cw): window.Bind(wx.EVT_MOTION, self._on_grid_motion) window.Bind(wx.EVT_LEFT_DOWN, self._on_left_down) window.Bind(wx.EVT_LEFT_UP, self._on_left_up) # Initialize the row and column models: self.__initialize_rows(self.model) self.__initialize_columns(self.model) self.__initialize_fonts() # Handle trait style settings: self.__initialize_style_settings() # Enable the grid as a drag and drop target: self._grid.SetDropTarget(PythonDropTarget(self)) self.__autosize() self._edit = False grid.Bind(wx.EVT_IDLE, self._on_idle) return grid def dispose(self): # Remove all wx handlers: grid = self._grid if grid is not None: grid.Unbind(wx.grid.EVT_GRID_SELECT_CELL) grid.Unbind(wx.grid.EVT_GRID_RANGE_SELECT) grid.Unbind(wx.grid.EVT_GRID_COL_SIZE) grid.Unbind(wx.grid.EVT_GRID_ROW_SIZE) grid.Unbind(wx.grid.EVT_GRID_CELL_LEFT_DCLICK) grid.Unbind(wx.grid.EVT_GRID_CELL_RIGHT_CLICK) grid.Unbind(wx.grid.EVT_GRID_CELL_RIGHT_DCLICK) grid.Unbind(wx.grid.EVT_GRID_LABEL_RIGHT_CLICK) grid.Unbind(wx.grid.EVT_GRID_LABEL_LEFT_CLICK) grid.Unbind(wx.grid.EVT_GRID_EDITOR_CREATED) if is_win32: grid.Unbind(wx.grid.EVT_GRID_EDITOR_HIDDEN) grid.Unbind(wx.EVT_KEY_DOWN) grid.Unbind(wx.EVT_SIZE) self._grid_window.Unbind(wx.EVT_PAINT) for window in (self._grid_window, self._row_window, self._col_window): window.Unbind(wx.EVT_MOTION) window.Unbind(wx.EVT_LEFT_DOWN) window.Unbind(wx.EVT_LEFT_UP) self.model.observe( self._on_model_content_changed, "content_changed", remove=True ) self.model.observe( self._on_model_structure_changed, "structure_changed", remove=True ) self.model.observe(self._on_row_sort, "row_sorted", remove=True) self.model.observe(self._on_column_sort, "column_sorted", remove=True) self.observe(self._on_new_model, "model", remove=True) self.observe( self._on_enable_lines_changed, "enable_lines", remove=True ) self.observe( self._on_grid_line_color_changed, "grid_line_color", remove=True ) self.observe( self._on_default_label_font_changed, "default_label_font", remove=True, ) self.observe( self._on_default_label_bg_color_changed, "default_label_bg_color", remove=True, ) self.observe( self._on_default_label_text_color_changed, "default_label_text_color", remove=True, ) self.observe( self._on_selection_bg_color_changed, "selection_bg_color", remove=True, ) self.observe( self._on_selection_text_color_changed, "selection_text_color", remove=True, ) self.observe( self._on_default_cell_font_changed, "default_cell_font", remove=True, ) self.observe( self._on_default_cell_text_color_changed, "default_cell_text_color", remove=True, ) self.observe( self._on_default_cell_bg_color_changed, "default_cell_bg_color", remove=True, ) self.observe( self._on_read_only_changed, "read_only", remove=True ) self.observe( self._on_selection_mode_changed, "selection_mode", remove=True ) self.observe( self._on_column_label_height_changed, "column_label_height", remove=True, ) self.observe( self._on_row_label_width_changed, "row_label_width", remove=True ) self.observe( self._on_show_column_headers_changed, "show_column_headers", remove=True, ) self.observe( self._on_show_row_headers_changed, "show_row_headers", remove=True ) self._grid_table_base.dispose() self._grid = None # ------------------------------------------------------------------------ # Trait event handlers. # ------------------------------------------------------------------------ def _on_new_model(self, event): """ When we get a new model reinitialize grid match to that model. """ self._grid_table_base.model = self.model self.__initialize_counts(self.model) self._on_model_changed() if self.autosize: # Note that we don't call AutoSize() here, because autosizing # the rows looks like crap. self._grid.AutoSizeColumns(False) def _on_model_content_changed(self, event): """ A notification method called when the data in the underlying model changes. """ self._grid.ForceRefresh() def _on_model_structure_changed(self, event=None): """ A notification method called when the underlying model has changed. Responsible for making sure the view object updates correctly. """ # Disable any active editors in order to prevent a wx crash bug: self._edit = False grid = self._grid # Make sure any current active editor has been disabled: grid.DisableCellEditControl() # More wacky fun with wx. We have to manufacture the appropriate # grid messages and send them off to make sure the grid updates # correctly: should_autosize = False # First check to see if rows have been added or deleted: row_count = self.model.get_row_count() delta = row_count - self._row_count self._row_count = row_count if delta > 0: # Rows were added: msg = GridTableMessage( self._grid_table_base, GRIDTABLE_NOTIFY_ROWS_APPENDED, delta ) grid.ProcessTableMessage(msg) should_autosize = True elif delta < 0: # Rows were deleted: msg = GridTableMessage( self._grid_table_base, GRIDTABLE_NOTIFY_ROWS_DELETED, row_count, -delta, ) grid.ProcessTableMessage(msg) should_autosize = True # Now check for column changes: col_count = self.model.get_column_count() delta = col_count - self._col_count self._col_count = col_count if delta > 0: # Columns were added: msg = GridTableMessage( self._grid_table_base, GRIDTABLE_NOTIFY_COLS_APPENDED, delta ) grid.ProcessTableMessage(msg) should_autosize = True elif delta < 0: # Columns were deleted: msg = GridTableMessage( self._grid_table_base, GRIDTABLE_NOTIFY_COLS_DELETED, col_count, -delta, ) grid.ProcessTableMessage(msg) should_autosize = True # Finally make sure we update for any new values in the table: msg = GridTableMessage( self._grid_table_base, GRIDTABLE_REQUEST_VIEW_GET_VALUES ) grid.ProcessTableMessage(msg) if should_autosize: self.__autosize() # We have to make sure the editor/renderer cache in the GridTableBase # object is cleaned out: self._grid_table_base._clear_cache() grid.AdjustScrollbars() self._refresh() def _on_row_sort(self, event): """ Handles a row_sorted event from the underlying model. """ # First grab the new data out of the event: if event.new.index < 0: self._current_sorted_row = None else: self._current_sorted_row = event.new.index self._row_sort_reversed = event.new.reversed # Since the label may have changed we may need to autosize again: # fixme: when we change how we represent the sorted column # this should go away. self.__autosize() # Make sure everything updates to reflect the changes: self._on_model_structure_changed() def _on_column_sort(self, event): """ Handles a column_sorted event from the underlying model. """ # first grab the new data out of the event if event.new.index < 0: self._current_sorted_col = None else: self._current_sorted_col = event.new.index self._col_sort_reversed = event.new.reversed # since the label may have changed we may need to autosize again # fixme: when we change how we represent the sorted column # this should go away. self.__autosize() # make sure everything updates to reflect the changes self._on_model_structure_changed() def _on_enable_lines_changed(self, event=None): """ Handle a change to the enable_lines trait. """ self._grid.EnableGridLines(self.enable_lines) def _on_grid_line_color_changed(self, event=None): """ Handle a change to the enable_lines trait. """ self._grid.SetGridLineColour(self.grid_line_color) def _on_default_label_font_changed(self, event=None): """ Handle a change to the default_label_font trait. """ font = self.default_label_font if font is None: font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) font.SetWeight(wx.BOLD) self._grid.SetLabelFont(font) def _on_default_label_text_color_changed(self, event=None): """ Handle a change to the default_cell_text_color trait. """ if self.default_label_text_color is not None: color = self.default_label_text_color self._grid.SetLabelTextColour(color) self._grid.ForceRefresh() def _on_default_label_bg_color_changed(self, event=None): """ Handle a change to the default_cell_text_color trait. """ if self.default_label_bg_color is not None: color = self.default_label_bg_color self._grid.SetLabelBackgroundColour(color) self._grid.ForceRefresh() def _on_selection_bg_color_changed(self, event=None): """ Handle a change to the selection_bg_color trait. """ if self.selection_bg_color is not None: self._grid.SetSelectionBackground(self.selection_bg_color) def _on_selection_text_color_changed(self, event=None): """ Handle a change to the selection_text_color trait. """ if self.selection_text_color is not None: self._grid.SetSelectionForeground(self.selection_text_color) def _on_default_cell_font_changed(self, event=None): """ Handle a change to the default_cell_font trait. """ if self.default_cell_font is not None: self._grid.SetDefaultCellFont(self.default_cell_font) self._grid.ForceRefresh() def _on_default_cell_text_color_changed(self, event=None): """ Handle a change to the default_cell_text_color trait. """ if self.default_cell_text_color is not None: color = self.default_cell_text_color self._grid.SetDefaultCellTextColour(color) self._grid.ForceRefresh() def _on_default_cell_bg_color_changed(self, event=None): """ Handle a change to the default_cell_bg_color trait. """ if self.default_cell_bg_color is not None: color = self.default_cell_bg_color self._grid.SetDefaultCellBackgroundColour(color) self._grid.ForceRefresh() def _on_read_only_changed(self, event=None): """ Handle a change to the read_only trait. """ # should the whole grid be read-only? if self.read_only: self._grid.EnableEditing(False) else: self._grid.EnableEditing(True) def _on_selection_mode_changed(self, event=None): """ Handle a change to the selection_mode trait. """ # should we allow individual cells to be selected or only rows # or only columns if self.selection_mode == "cell": self._grid.SetSelectionMode(wxGrid.SelectCells) elif self.selection_mode == "rows": self._grid.SetSelectionMode(wxGrid.SelectRows) elif self.selection_mode == "cols": self._grid.SetSelectionMode(wxGrid.SelectColumns) def _on_column_label_height_changed(self, event=None): """ Handle a change to the column_label_height trait. """ # handle setting for height of column labels if self.column_label_height is not None: self._grid.SetColLabelSize(self.column_label_height) def _on_row_label_width_changed(self, event=None): """ Handle a change to the row_label_width trait. """ # handle setting for width of row labels if self.row_label_width is not None: self._grid.SetRowLabelSize(self.row_label_width) def _on_show_column_headers_changed(self, event=None): """ Handle a change to the show_column_headers trait. """ if not self.show_column_headers: self._grid.SetColLabelSize(0) else: self._grid.SetColLabelSize(self.column_label_height) def _on_show_row_headers_changed(self, event=None): """ Handle a change to the show_row_headers trait. """ if not self.show_row_headers: self._grid.SetRowLabelSize(0) else: self._grid.SetRowLabelSize(self.row_label_width) # ------------------------------------------------------------------------ # 'Grid' interface. # ------------------------------------------------------------------------ def get_selection(self): """ Return a list of the currently selected objects. """ return self.__get_selection() def set_selection(self, selection_list, notify=True): """ Set the current selection to the objects listed in selection_list. Note that these objects are model-specific, as the grid depends on the underlying model to translate these objects into grid coordinates. A ValueError will be raised if the objects are not in the proper format for the underlying model. """ # Set the 'should we notify the model of the selection change' flag: self._notify_select = notify # First use the model to resolve the object list into a set of # grid coordinates cells = self.model.resolve_selection(selection_list) # Now make sure all those grid coordinates get set properly: grid = self._grid grid.BeginBatch() grid.ClearSelection() mode = self.selection_mode if mode == "rows": self._select_rows(cells) elif mode != "": for selection in cells: row, col = max(0, selection[0]), max(0, selection[1]) grid.SelectBlock(row, col, row, col, True) grid.EndBatch() # Indicate that the selection has been changed: if notify: self.__fire_selection_changed() self._notify_select = True def stop_editing_indices(self, indices): """ If editing is occuring in a row in 'indices', stop editing. """ if self._grid.GetGridCursorRow() in indices: self._grid.DisableCellEditControl() # ------------------------------------------------------------------------ # wx event handlers. # ------------------------------------------------------------------------ def _on_size(self, evt): """ Called when the grid is resized. """ self.__autosize() evt.Skip() def _on_editor_hidden(self, evt): # This is a hack to make clicking on a window button while a grid # editor is active work correctly under Windows. Normally, when the # user clicks on the button to exit grid cell editing and perform the # button function, only the grid cell editing is terminated under # Windows. A second click on the button is required to actually # trigger the button functionality. We circumvent this problem by # generating a 'fake' button click event. Technically this solution # is not correct since the button click should be generated on mouse # up, but I'm not sure if we will get the mouse up event, so we do it # here instead. Note that this handler is only set-up when the OS is # Windows. control = wx.FindWindowAtPointer() if isinstance(control, wx.Button): do_later( wx.PostEvent, control, wx.CommandEvent( wx.wxEVT_COMMAND_BUTTON_CLICKED, control.GetId() ), ) def _on_left_down(self, evt): """ Called when the left mouse button is pressed. """ grid = self._grid self._x_clicked = evt.GetX() self._y_clicked = evt.GetY() self._ignore = (grid.XToEdgeOfCol(evt.GetX()) != wx.NOT_FOUND) or ( grid.YToEdgeOfRow(evt.GetY()) != wx.NOT_FOUND ) evt.Skip() def _on_left_up(self, evt): """ Called when the left mouse button is released. """ self._ignore = False evt.Skip() def _on_motion(self, evt): """ Called when the mouse moves. """ evt.Skip() if evt.Dragging() and not evt.ControlDown(): data = self.__get_drag_value() if isinstance(data, str): file = abspath(data) if exists(file): FileDropSource(self._grid, file) return PythonDropSource(self._grid, data) def _on_grid_motion(self, evt): if evt.GetEventObject() is self._grid_window: x, y = self._grid.CalcUnscrolledPosition(evt.GetPosition().Get()) row = self._grid.YToRow(y) col = self._grid.XToCol(x) # Notify the model that the mouse has moved into the cell at row,col, # only if the row and col are valid. if (row >= 0) and (col >= 0): self.model.mouse_cell = (row, col) # If we are not ignoring mouse events, call _on_motion. if not self._ignore: self._on_motion(evt) evt.Skip() def _on_select_cell(self, evt): """ Called when the user has moved to another cell. """ row, col = evt.GetRow(), evt.GetCol() self._moveTo = (row, col) self.cell_left_clicked = self.model.click = (row, col) # Try to find a renderer for this cell: renderer = self.model.get_cell_renderer(row, col) # If the renderer has defined a handler then call it: result = False if renderer is not None: result = renderer.on_left_click(self, row, col) # print("self._grid.GetParent()", self._grid.GetParent().GetParent().GetParent()) # if the handler didn't tell us to stop further handling then skip if not result: if (self.selection_mode != "") or (not self.edit_on_first_click): self._grid.SelectBlock(row, col, row, col, evt.ControlDown()) self._edit = True evt.Skip() def _on_range_select(self, evt): if evt.Selecting(): if (self.selection_mode == "cell") and evt.ControlDown(): self._grid.SelectBlock( evt.GetTopRow(), evt.GetLeftCol(), evt.GetBottomRow(), evt.GetRightCol(), True, ) if self._notify_select: self.__fire_selection_changed() def _on_col_size(self, evt): """ Called when the user changes a column's width. """ self.__autosize() evt.Skip() def _on_row_size(self, evt): """ Called when the user changes a row's height. """ self._grid.AdjustScrollbars() evt.Skip() def _on_idle(self, evt): """ Immediately jumps into editing mode, bypassing the usual select mode of a spreadsheet. See also self.OnSelectCell(). """ if self._edit and self.edit_on_first_click: if self._grid.CanEnableCellControl(): self._grid.EnableCellEditControl() self._edit = False evt.Skip() def _on_cell_left_dclick(self, evt): """ Called when the left mouse button was double-clicked. From the wxPython demo code:- 'I do this because I don't like the default behaviour of not starting the cell editor on double clicks, but only a second click.' Fair enuff! """ row, col = evt.GetRow(), evt.GetCol() data = self.model.get_value(row, col) self.cell_activated = data # Tell the model that a cell was double-clicked on: self.model.dclick = (row, col) # Try to find a renderer for this cell renderer = self.model.get_cell_renderer(row, col) # If the renderer has defined a handler then call it if renderer is not None: renderer.on_left_dclick(self, row, col) def _on_cell_right_dclick(self, evt): """ Called when the right mouse button was double-clicked. From the wxPython demo code:- 'I do this because I don't like the default behaviour of not starting the cell editor on double clicks, but only a second click.' Fair enuff! """ row, col = evt.GetRow(), evt.GetCol() # try to find a renderer for this cell renderer = self.model.get_cell_renderer(row, col) # if the renderer has defined a handler then call it if renderer is not None: renderer.on_right_dclick(self, row, col) def _on_cell_right_click(self, evt): """ Called when a right click occurred in a cell. """ row, col = evt.GetRow(), evt.GetCol() # try to find a renderer for this cell renderer = self.model.get_cell_renderer(row, col) # if the renderer has defined a handler then call it result = False if renderer is not None: result = renderer.on_right_click(self, row, col) # if the handler didn't tell us to stop further handling then skip if not result: # ask model for the appropriate context menu menu_manager = self.model.get_cell_context_menu(row, col) # get the underlying menu object if menu_manager is not None: controller = None if type(menu_manager) is tuple: menu_manager, controller = menu_manager menu = menu_manager.create_menu(self._grid, controller) # if it has anything in it pop it up if menu.GetMenuItemCount() > 0: # Popup the menu (if an action is selected it will be # performed before before 'PopupMenu' returns). x, y = evt.GetPosition().Get() self._grid.PopupMenu(menu, x - 10, y - 10) self.cell_right_clicked = (row, col) evt.Skip() def _on_label_right_click(self, evt): """ Called when a right click occurred on a label. """ row, col = evt.GetRow(), evt.GetCol() # a row value of -1 means this click happened on a column. # vice versa, a col value of -1 means a row click. menu_manager = None if row == -1: menu_manager = self.model.get_column_context_menu(col) else: menu_manager = self.model.get_row_context_menu(row) if menu_manager is not None: # get the underlying menu object menu = menu_manager.create_menu(self._grid) # if it has anything in it pop it up if menu.GetMenuItemCount() > 0: # Popup the menu (if an action is selected it will be performed # before before 'PopupMenu' returns). self._grid.PopupMenu(menu, evt.GetPosition().Get()) elif col >= 0: cws = getattr(self, "_cached_widths", None) if (cws is not None) and (0 <= col < len(cws)): cws[col] = None self.__autosize() evt.Skip() def _on_label_left_click(self, evt): """ Called when a left click occurred on a label. """ row, col = evt.GetRow(), evt.GetCol() # A row value of -1 means this click happened on a column. # vice versa, a col value of -1 means a row click. if (row == -1) and self.allow_column_sort and evt.ControlDown(): self._column_sort(col) elif (col == -1) and self.allow_row_sort and evt.ControlDown(): self._row_sort(row) evt.Skip() def _column_sort(self, col): """ Sorts the data on the specified column **col**. """ self._grid.Freeze() if (col == self._current_sorted_col) and (not self._col_sort_reversed): # If this column is currently sorted on then reverse it: self.model.sort_by_column(col, True) elif (col == self._current_sorted_col) and self._col_sort_reversed: # If this column is currently reverse-sorted then unsort it: try: self.model.no_column_sort() except NotImplementedError: # If an unsort function is not implemented then just # reverse the sort: self.model.sort_by_column(col, False) else: # Sort the data: self.model.sort_by_column(col, False) self._grid.Thaw() def _row_sort(self, row): self._grid.Freeze() if (row == self._current_sorted_row) and (not self._row_sort_reversed): # If this row is currently sorted on then reverse it: self.model.sort_by_row(row, True) elif row == self._current_sorted_row and self._row_sort_reversed: # If this row is currently reverse-sorted then unsort it: try: self.model.no_row_sort() except NotImplementedError: # If an unsort function is not implemented then just # reverse the sort: self.model.sort_by_row(row, False) else: # Sort the data: self.model.sort_by_row(row, False) self._grid.Thaw() def _on_key_down(self, evt): """ Called when a key is pressed. """ # This changes the behaviour of the and keys to make # manual data entry smoother! # # Don't change the behavior if the key is pressed as this # has meaning to the edit control. evt.Skip() def _move_to_next_cell(self, expandSelection=False): """ Move to the 'next' cell. """ # Complete the edit on the current cell. self._grid.DisableCellEditControl() # Try to move to the next column. success = self._grid.MoveCursorRight(expandSelection) # If the move failed then we must be at the end of a row. if not success: # Move to the first column in the next row. newRow = self._grid.GetGridCursorRow() + 1 if newRow < self._grid.GetNumberRows(): self._grid.SetGridCursor(newRow, 0) self._grid.MakeCellVisible(newRow, 0) else: # This would be a good place to add a new row if your app # needs to do that. pass return success def _move_to_previous_cell(self, expandSelection=False): """ Move to the 'previous' cell. """ # Complete the edit on the current cell. self._grid.DisableCellEditControl() # Try to move to the previous column (without expanding the current # selection). success = self._grid.MoveCursorLeft(expandSelection) # If the move failed then we must be at the start of a row. if not success: # Move to the last column in the previous row. newRow = self._grid.GetGridCursorRow() - 1 if newRow >= 0: self._grid.SetGridCursor( newRow, self._grid.GetNumberCols() - 1 ) self._grid.MakeCellVisible( newRow, self._grid.GetNumberCols() - 1 ) def _refresh(self): self._grid.GetParent().Layout() def _on_col_move(self, evt): """ Called when a column move is taking place. """ self._ignore = True # Get the column being moved: frm = evt.GetMoveColumn() # Get the column to insert it before: to = evt.GetBeforeColumn() # Tell the model to update its columns: if self.model._move_column(frm, to): # Modify the grid: grid = self._grid cols = grid.GetNumberCols() widths = [grid.GetColSize(i) for i in range(cols)] width = widths[frm] del widths[frm] to -= frm < to widths.insert(to, width) grid.BeginBatch() grid.ProcessTableMessage( GridTableMessage( self._grid_table_base, GRIDTABLE_NOTIFY_COLS_DELETED, frm, 1, ) ) grid.ProcessTableMessage( GridTableMessage( self._grid_table_base, GRIDTABLE_NOTIFY_COLS_INSERTED, to, 1, ) ) [grid.SetColSize(i, widths[i]) for i in range(min(frm, to), cols)] grid.EndBatch() def _on_row_move(self, evt): """ Called when a row move is taking place. """ self._ignore = True # Get the column being moved: frm = evt.GetMoveRow() # Get the column to insert it before: to = evt.GetBeforeRow() # Tell the model to update its rows: if self.model._move_row(frm, to): # Notify the grid: grid = self._grid rows = grid.GetNumberRows() heights = [grid.GetRowSize(i) for i in range(rows)] height = heights[frm] del heights[frm] to -= frm < to heights.insert(to, height) grid.BeginBatch() grid.ProcessTableMessage( GridTableMessage( self._grid_table_base, GRIDTABLE_NOTIFY_ROWS_DELETED, frm, 1, ) ) grid.ProcessTableMessage( GridTableMessage( self._grid_table_base, GRIDTABLE_NOTIFY_ROWS_INSERTED, to, 1, ) ) [grid.SetRowSize(i, heights[i]) for i in range(min(frm, to), rows)] grid.EndBatch() # ------------------------------------------------------------------------ # PythonDropTarget interface. # ------------------------------------------------------------------------ def wx_dropped_on(self, x, y, drag_object, drag_result): # first resolve the x/y coords into a grid row/col row, col = self.__resolve_grid_coords(x, y) result = wx.DragNone if row != -1 and col != -1: # now ask the model if the target cell can accept this object valid_target = self.model.is_valid_cell_value( row, col, drag_object ) # if this is a valid target then attempt to set the value if valid_target: # find the data data = drag_object # sometimes a 'node' attribute on the clipboard gets set # to a binding. if this happens we want to use it, otherwise # we want to just use the drag_object passed to us if ( hasattr(enClipboard, "node") and enClipboard.node is not None ): data = enClipboard.node # now make sure the value gets set in the model self.model.set_value(row, col, data) result = drag_result return result def wx_drag_over(self, x, y, drag_object, drag_result): # first resolve the x/y coords into a grid row/col row, col = self.__resolve_grid_coords(x, y) result = wx.DragNone if row != -1 and col != -1: # now ask the model if the target cell can accept this object valid_target = self.model.is_valid_cell_value( row, col, drag_object ) if valid_target: result = drag_result return result # ------------------------------------------------------------------------ # private interface. # ------------------------------------------------------------------------ def __initialize_fonts(self): """ Initialize the label fonts. """ self._on_default_label_font_changed() self._on_default_cell_font_changed() self._on_default_cell_text_color_changed() self._on_grid_line_color_changed() self._grid.SetColLabelAlignment(wx.ALIGN_CENTRE, wx.ALIGN_CENTRE) self._grid.SetRowLabelAlignment(wx.ALIGN_RIGHT, wx.ALIGN_CENTRE) def __initialize_rows(self, model): """ Initialize the row headers. """ # should we really be doing this? for row in range(model.get_row_count()): if model.is_row_read_only(row): attr = wx.grid.GridCellAttr() attr.SetReadOnly() # attr.SetRenderer(None) # attr.SetBackgroundColour('linen') self._grid.SetRowAttr(row, attr) def __initialize_columns(self, model): """ Initialize the column headers. """ # should we really be doing this? for column in range(model.get_column_count()): if model.is_column_read_only(column): attr = wx.grid.GridCellAttr() attr.SetReadOnly() # attr.SetRenderer(None) # attr.SetBackgroundColour('linen') self._grid.SetColAttr(column, attr) def __initialize_counts(self, model): """ Initializes the row and column counts. """ if model is not None: self._row_count = model.get_row_count() else: self._row_count = 0 if model is not None: self._col_count = model.get_column_count() else: self._col_count = 0 def __initialize_sort_state(self): """ Initializes the row and column counts. """ self._current_sorted_col = None self._current_sorted_row = None self._col_sort_reversed = False self._row_sort_reversed = False def __initialize_style_settings(self): # make sure all the handlers for traits defining styles get called self._on_enable_lines_changed() self._on_read_only_changed() self._on_selection_mode_changed() self._on_column_label_height_changed() self._on_row_label_width_changed() self._on_show_column_headers_changed() self._on_show_row_headers_changed() self._on_default_cell_bg_color_changed() self._on_default_label_bg_color_changed() self._on_default_label_text_color_changed() self._on_selection_bg_color_changed() self._on_selection_text_color_changed() def __get_drag_value(self): """ Calculates the drag value based on the current selection. """ return self.model.get_cell_drag_value( self._grid.GetGridCursorRow(), self._grid.GetGridCursorCol() ) def __get_selection(self): """ Returns a list of values for the current selection. """ rows, cols = self.__get_selected_rows_and_cols() if len(rows) > 0: rows.sort() value = self.model.get_rows_selection_value(rows) elif len(cols) > 0: cols.sort() value = self.model.get_cols_selection_value(cols) else: # our final option -- grab the cell that the cursor is currently in row = self._grid.GetGridCursorRow() col = self._grid.GetGridCursorCol() value = self.model.get_cell_selection_value(row, col) if value is not None: value = [value] if value is None: value = [] return value def __get_selected_rows_and_cols(self): """ Return lists of the selected rows and the selected columns. """ # note: because the wx grid control is so screwy, we have limited # the selection behavior. we only allow single cells to be selected, # or whole row, or whole columns. rows = self._grid.GetSelectedRows() cols = self._grid.GetSelectedCols() # because of wx we have to check this as well # note that all this malarkey is working on the assumption that # only entire rows or entire columns or single cells are selected. top_left = self._grid.GetSelectionBlockTopLeft() bottom_right = self._grid.GetSelectionBlockBottomRight() selection_mode = self._grid.GetSelectionMode() if selection_mode == wxGrid.SelectRows: # handle rows differently. figure out which rows were # selected. turns out that in this case, wx adds a "block" # per row, so we have to cycle over the list returned by # the GetSelectionBlock routines for i in range(len(top_left)): top_point = top_left[i] bottom_point = bottom_right[i] for row_index in range(top_point[0], bottom_point[0] + 1): rows.append(row_index) elif selection_mode == wxGrid.SelectColumns: # again, in this case we know that only whole columns can be # selected for i in range(len(top_left)): top_point = top_left[i] bottom_point = bottom_right[i] for col_index in range(top_point[1], bottom_point[1] + 1): cols.append(col_index) elif selection_mode == wxGrid.SelectCells: # this is the case where the selection_mode is cell, which also # allows complete columns or complete rows to be selected. # first find the size of the grid row_size = self.model.get_row_count() col_size = self.model.get_column_count() for i in range(len(top_left)): top_point = top_left[i] bottom_point = bottom_right[i] # precalculate whether this is a row or column select row_select = ( top_point[1] == 0 and bottom_point[1] == col_size - 1 ) col_select = ( top_point[0] == 0 and bottom_point[0] == row_size - 1 ) if row_select: for row_index in range(top_point[0], bottom_point[0] + 1): rows.append(row_index) if col_select: for col_index in range(top_point[1], bottom_point[1] + 1): cols.append(top_point[0]) return (rows, cols) def __fire_selection_changed(self): self.selection_changed = True def __autosize(self): """ Autosize the grid with appropriate flags. """ model = self.model grid = self._grid if grid is not None and self.autosize: grid.AutoSizeColumns(False) grid.AutoSizeRows(False) # Whenever we size the grid we need to take in to account any # explicitly set column sizes: grid.BeginBatch() dx, dy = grid.GetClientSize().Get() n = model.get_column_count() pdx = 0 wdx = 0.0 widths = [] cached = getattr(self, "_cached_widths", None) current = [grid.GetColSize(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]) or # hack: For some reason wx always seems to adjust column 0 by # 1 pixel from what we set it to (at least on Windows), so we # need to add a little fudge factor just for this column: ((i == 0) and (abs(current[i] + cw) <= 1)) ): width = model.get_column_size(i) if width is None: width = 0.0 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) # The '-1' below adjusts for an off by 1 error in the way the wx.Grid # control determines whether or not it needs a horizontal scroll bar: adx = max(0, dx - pdx - 1) for i in range(n): width = cached[i] if width < 0: width = widths[i] if width <= 1.0: w = max(30, int(round((adx * width) / wdx))) wdx -= width width = w adx -= width cached[i] = -w grid.SetColSize(i, width) grid.AdjustScrollbars() grid.EndBatch() grid.ForceRefresh() def __resolve_grid_coords(self, x, y): """ Resolve the specified x and y coordinates into row/col coordinates. Returns row, col. """ # the x,y coordinates here are Unscrolled coordinates. # They must be changed to scrolled coordinates. x, y = self._grid.CalcUnscrolledPosition(x, y) # now we need to get the row and column from the grid # but we need to first remove the RowLabel and ColumnLabel # bounding boxes if self.show_row_headers: x = x - self._grid.GetGridRowLabelWindow().GetRect().width if self.show_column_headers: y = y - self._grid.GetGridColLabelWindow().GetRect().height return (self._grid.YToRow(y), self._grid.XToCol(x)) def _select_rows(self, cells): """ Selects all of the rows specified by a list of (row,column) pairs. """ # For a large set of rows, simply calling 'SelectBlock' on the Grid # object for each row is very inefficient, so we first make a pass over # all of the cells to merge them into contiguous ranges as much as # possible: sb = self._grid.SelectBlock # Extract the rows and sort them: rows = [row for row, column in cells] rows.sort() # Now find contiguous ranges of rows, and select the current range # whenever a break in the sequence is found: first = last = -999 for row in rows: if row == (last + 1): last = row else: if first >= 0: sb(first, 0, last, 0, True) first = last = row # Handle the last pending range of lines to be selected: if first >= 0: sb(first, 0, last, 0, True) class _GridTableBase(GridTableBase): """ A private adapter for the underlying wx grid implementation. """ # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, model, grid): """ Creates a new table base. """ # Base class constructor. GridTableBase.__init__(self) # The Pyface model that provides the data. self.model = model self._grid = grid # hacky state variables so we can identify when rows have been # added or deleted. self._row_count = -1 self._col_count = -1 # caches for editors and renderers self._editor_cache = {} self._renderer_cache = {} def dispose(self): # Make sure dispose gets called on all traits editors: for editor in self._editor_cache.values(): editor.dispose() self._editor_cache = {} for renderer in self._renderer_cache.values(): renderer.dispose() self._renderer_cache = {} # ------------------------------------------------------------------------ # 'wxGridTableBase' interface. # ------------------------------------------------------------------------ def GetNumberRows(self): """ Return the number of rows in the model. """ # because wx is such a wack job we have to store away the row # and column counts so we can figure out when rows or cols have # been appended or deleted. lacking a better place to do this # we just set the local variable every time GetNumberRows is called. self._row_count = self.model.get_row_count() return self._row_count def GetNumberCols(self): """ Return the number of columns in the model. """ # see comment in GetNumberRows for logic here self._col_count = self.model.get_column_count() return self._col_count def IsEmptyCell(self, row, col): """ Is the specified cell empty? """ return self.model.is_cell_empty(row, col) def GetValue(self, row, col): """ Get the value at the specified row and column. """ return self.model.get_value(row, col) def SetValue(self, row, col, value): """ Set the value at the specified row and column. """ return self.model.set_value(row, col, value) def GetRowLabelValue(self, row): """ Called when the grid needs to display a row label. """ label = self.model.get_row_name(row) if row == self._grid._current_sorted_row: if self._grid._row_sort_reversed: if is_win32: ulabel = str(label, "ascii") + " \u00ab" label = ulabel.encode("latin-1") else: label += " <<" elif is_win32: ulabel = str(label, "ascii") + " \u00bb" label = ulabel.encode("latin-1") else: label += " >>" return label def GetColLabelValue(self, col): """ Called when the grid needs to display a column label. """ label = self.model.get_column_name(col) if col == self._grid._current_sorted_col: if self._grid._col_sort_reversed: if is_win32: ulabel = str(label, "ascii") + " \u00ab" label = ulabel.encode("latin-1") else: label += " <<" elif is_win32: ulabel = str(label, "ascii") + " \u00bb" label = ulabel.encode("latin-1") else: label += " >>" return label def GetTypeName(self, row, col): """ Called to determine the kind of editor/renderer to use. This doesn't necessarily have to be the same type used natively by the editor/renderer if they know how to convert. """ typename = None try: typename = self.model.get_type(row, col) except NotImplementedError: pass if typename is None: typename = GRID_VALUE_STRING return typename def DeleteRows(self, pos, num_rows): """ Called when the view is deleting rows. """ # clear the cache self._clear_cache() return self.model.delete_rows(pos, num_rows) def InsertRows(self, pos, num_rows): """ Called when the view is inserting rows. """ # clear the cache self._clear_cache() return self.model.insert_rows(pos, num_rows) def AppendRows(self, num_rows): """ Called when the view is inserting rows. """ # clear the cache self._clear_cache() pos = self.model.get_row_count() return self.model.insert_rows(pos, num_rows) def DeleteCols(self, pos, num_cols): """ Called when the view is deleting columns. """ # clear the cache self._clear_cache() return self.model.delete_columns(pos, num_cols) def InsertCols(self, pos, num_cols): """ Called when the view is inserting columns. """ # clear the cache self._clear_cache() return self.model.insert_columns(pos, num_cols) def AppendCols(self, num_cols): """ Called when the view is inserting columns. """ # clear the cache self._clear_cache() pos = self.model.get_column_count() return self.model.insert_columns(pos, num_cols) def GetAttr(self, row, col, kind): """ Retrieve the cell attribute object for the specified cell. """ result = GridCellAttr() # we only handle cell requests, for other delegate to the supa if kind != GridCellAttr.Cell and kind != GridCellAttr.Any: return result rows = self.model.get_row_count() cols = self.model.get_column_count() # First look in the cache for the editor: editor = self._editor_cache.get((row, col)) if editor is None: if (row >= rows) or (col >= cols): editor = DummyGridCellEditor() else: # Ask the underlying model for an editor for this cell: editor = self.model.get_cell_editor(row, col) if editor is not None: self._editor_cache[(row, col)] = editor editor._grid_info = (self._grid._grid, row, col) if False: # editor is not None: # Note: We have to increment the reference to keep the # underlying code from destroying our object. editor.IncRef() result.SetEditor(editor) # try to find a renderer for this cell renderer = None if row < rows and col < cols: renderer = self.model.get_cell_renderer(row, col) if renderer is not None and renderer.renderer is not None: renderer.renderer.IncRef() result.SetRenderer(renderer.renderer) # look to see if this cell is editable read_only = False if row < rows and col < cols: read_only = ( self.model.is_cell_read_only(row, col) or self.model.is_row_read_only(row) or self.model.is_column_read_only(col) ) result.SetReadOnly(read_only) if read_only: read_only_color = self._grid.default_cell_read_only_color if ( read_only_color is not None and read_only_color is not Undefined ): result.SetBackgroundColour(read_only_color) # check to see if colors or fonts are specified for this cell bgcolor = None if row < rows and col < cols: bgcolor = self.model.get_cell_bg_color(row, col) else: bgcolor = self._grid.default_cell_bg_color if bgcolor is not None: result.SetBackgroundColour(bgcolor) text_color = None if row < rows and col < cols: text_color = self.model.get_cell_text_color(row, col) else: text_color = self._grid.default_cell_text_color if text_color is not None: result.SetTextColour(text_color) cell_font = None if row < rows and col < cols: cell_font = self.model.get_cell_font(row, col) else: cell_font = self._grid.default_cell_font if cell_font is not None: result.SetFont(cell_font) # check for alignment definition for this cell halignment = valignment = None if row < rows and col < cols: halignment = self.model.get_cell_halignment(row, col) valignment = self.model.get_cell_valignment(row, col) if halignment is not None and valignment is not None: if halignment == "center": h = wx.ALIGN_CENTRE elif halignment == "right": h = wx.ALIGN_RIGHT else: h = wx.ALIGN_LEFT if valignment == "top": v = wx.ALIGN_TOP elif valignment == "bottom": v = wx.ALIGN_BOTTOM else: v = wx.ALIGN_CENTRE result.SetAlignment(h, v) return result # ------------------------------------------------------------------------ # private interface. # ------------------------------------------------------------------------ def _clear_cache(self): """ Clean out the editor/renderer cache. """ # Dispose of the editors in the cache after a brief delay, so as # to allow completion of the current event: do_later(self._editor_dispose, list(self._editor_cache.values())) self._editor_cache = {} self._renderer_cache = {} def _editor_dispose(self, editors): for editor in editors: editor.dispose() class DummyGridCellEditor(GridCellEditor): def Show(self, show, attr): return pyface-7.4.0/pyface/ui/wx/grid/simple_grid_model.py0000644000076500000240000002136314176222673023250 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 SimpleGridModel simply builds a table from a 2-dimensional list/array containing the data. Optionally users can pass in specifications for rows and columns. By default these are built off the data itself, with row/column labels as the index + 1.""" from pyface.action.api import Action, Group, MenuManager from traits.api import Any, Instance, List, Union from pyface.wx.drag_and_drop import clipboard as enClipboard from .grid_model import GridColumn, GridModel, GridRow class SimpleGridModel(GridModel): """ A SimpleGridModel simply builds a table from a 2-dimensional list/array containing the data. Optionally users can pass in specifications for rows and columns. By default these are built off the data itself, with row/column labels as the index + 1.""" # A 2-dimensional list/array containing the grid data. data = Any() # The rows in the model. rows = Union(None, List(Instance(GridRow))) # The columns in the model. columns = Union(None, List(Instance(GridColumn))) # ------------------------------------------------------------------------ # 'GridModel' interface. # ------------------------------------------------------------------------ def get_column_count(self): """ Return the number of columns for this table. """ if self.columns is not None: # if we have an explicit declaration then use it count = len(self.columns) else: # otherwise look at the length of the first row # note: the data had better be 2D count = len(self.data[0]) return count def get_column_name(self, index): """ Return the name of the column specified by the (zero-based) index. """ if self.columns is not None: # if we have an explicit declaration then use it try: name = self.columns[index].label except IndexError: name = "" else: # otherwise return the index plus 1 name = str(index + 1) return name def get_cols_drag_value(self, cols): """ Return the value to use when the specified columns are dragged or copied and pasted. cols is a list of column indexes. """ # if there is only one column in cols, then we return a 1-dimensional # list if len(cols) == 1: value = self.__get_data_column(cols[0]) else: # iterate over every column, building a list of the values in that # column value = [] for col in cols: value.append(self.__get_data_column(col)) return value def is_column_read_only(self, index): """ Return True if the column specified by the zero-based index is read-only. """ # if there is no declaration then assume the column is not # read only read_only = False if self.columns is not None: # if we have an explicit declaration then use it try: read_only = self.columns[index].read_only except IndexError: pass return read_only def get_row_count(self): """ Return the number of rows for this table. """ if self.rows is not None: # if we have an explicit declaration then use it count = len(self.rows) else: # otherwise look at the data count = len(self.data) return count def get_row_name(self, index): """ Return the name of the row specified by the (zero-based) index. """ if self.rows is not None: # if we have an explicit declaration then use it try: name = self.rows[index].label except IndexError: name = str(index + 1) else: # otherwise return the index plus 1 name = str(index + 1) return name def get_rows_drag_value(self, rows): """ Return the value to use when the specified rows are dragged or copied and pasted. rows is a list of row indexes. """ # if there is only one row in rows, then we return a 1-dimensional # list if len(rows) == 1: value = self.__get_data_row(rows[0]) else: # iterate over every row, building a list of the values in that # row value = [] for row in rows: value.append(self.__get_data_row(row)) return value def is_row_read_only(self, index): """ Return True if the row specified by the zero-based index is read-only. """ # if there is no declaration then assume the row is not # read only read_only = False if self.rows is not None: # if we have an explicit declaration then use it try: read_only = self.rows[index].read_only except IndexError: pass return read_only def get_value(self, row, col): """ Return the value stored in the table at (row, col). """ try: return self.data[row][col] except IndexError: pass return "" def is_cell_empty(self, row, col): """ Returns True if the cell at (row, col) has a None value, False otherwise.""" if row >= self.get_row_count() or col >= self.get_column_count(): empty = True else: try: value = self.get_value(row, col) empty = value is None except IndexError: empty = True return empty def get_cell_context_menu(self, row, col): """ Return a MenuManager object that will generate the appropriate context menu for this cell.""" context_menu = MenuManager( Group(_CopyAction(self, row, col, name="Copy"), id="Group") ) return context_menu def is_cell_editable(self, row, col): """ Returns True if the cell at (row, col) is editable, False otherwise. """ return True # ------------------------------------------------------------------------ # protected 'GridModel' interface. # ------------------------------------------------------------------------ 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 (row, col) does not exist. """ new_rows = 0 try: self.data[row][col] = value except IndexError: # Add a new row. self.data.append([0] * self.GetNumberCols()) self.data[row][col] = value new_rows = 1 return new_rows def _delete_rows(self, pos, num_rows): """ Removes rows pos through pos + num_rows from the model. """ if pos + num_rows >= self.get_row_count(): num_rows = self.get_rows_count() - pos del self.data[pos, pos + num_rows] return num_rows # ------------------------------------------------------------------------ # private interface. # ------------------------------------------------------------------------ def __get_data_column(self, col): """ Return a 1-d list of data from the column indexed by col. """ row_count = self.get_row_count() coldata = [] for row in range(row_count): try: coldata.append(self.get_value(row, col)) except IndexError: coldata.append(None) return coldata def __get_data_row(self, row): """ Return a 1-d list of data from the row indexed by row. """ col_count = self.get_column_count() rowdata = [] for col in range(col_count): try: rowdata.append(self.get_value(row, col)) except IndexError: rowdata.append(None) return rowdata # Private class class _CopyAction(Action): def __init__(self, model, row, col, **kw): super().__init__(**kw) self._model = model self._row = row self._col = col def perform(self): # grab the specified value from the model and add it to the # clipboard value = self._model.get_cell_drag_value(self._row, self._col) enClipboard.data = value pyface-7.4.0/pyface/ui/wx/grid/images/0000755000076500000240000000000014176460551020457 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/wx/grid/images/table_edit.png0000644000076500000240000000612114176222673023262 0ustar cwebsterstaff00000000000000PNG  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-gAMA|Q cHRMz%u0`:o_FlIDATxڤ/CaG.]HT%b; k?C QhJT]{{㺆g:9-9-hd2;6ɣ'o&o}RRg2Y޻UtАRb?]12ODB^ѩBRH)D"^ B.AcvoXZ^$_uB?i nz[X:Z ( vbm8%4͏mYre<:u+Wۮ-Ϸ9H5'WjH>}c[M -dsyݿC JaOu׽wύYIENDB`pyface-7.4.0/pyface/ui/wx/grid/images/checked.png0000644000076500000240000000063414176222673022557 0ustar cwebsterstaff00000000000000PNG  IHDRabKGD pHYs  tIME - (Ig)IDAT8œjPZ'BI5Щ?K2)84KBHJP ⢁3.+ZB!,cPŶ B{뺙^ 8&RAey0$QɷҤd"tRqѽnО5f3C_l Q`>g<S=coSerX)h5bݭ9]PT@ga }kj-b~/O븅>z2@HPCM,',Iwd^QcfIENDB`pyface-7.4.0/pyface/ui/wx/grid/images/unchecked.png0000644000076500000240000000034514176222673023121 0ustar cwebsterstaff00000000000000PNG  IHDRabKGDC pHYs  tIME (7rIDAT8 0 E-n!%A{z'AQQ j.b-.x,{ʙsF8f$7Rvg6afvi%愝P7NCxwEtIENDB`pyface-7.4.0/pyface/ui/wx/grid/images/image_not_found.png0000644000076500000240000000136714176222673024332 0ustar cwebsterstaff00000000000000PNG  IHDRabKGD pHYs  tIME 6eDtEXtCommentMenu-sized icon ========== (c) 2003 Jakub 'jimmac' Steiner, http://jimmac.musichall.cz created with the GIMP, http://www.gimp.orggGIDATxڥkSQwf$ZbېBk+ŕ]\W @t!n]Bp#"FED(U5ƒ4sq__Ru>sΙ3הgOYZ,j5yo =DVmx$K&zky*01 R>{U8xx/W.B ށ *J\¹i&bf64TQu;E:΅&Jƞ}lQ,h[D @^ Fdžqf[ { >mʡ#{!=a0;B,;sJd D= "6*pZV%]^ώa&! 6j?t''xrfwNKpu#`R"c =HIGA?r /Ȥwa?o(VmP3;?٦_?Vd9AIENDB`pyface-7.4.0/pyface/ui/wx/grid/trait_grid_cell_adapter.py0000644000076500000240000002076214176222673024423 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Adapter class to make trait editor controls work inside of a grid. """ import wx from wx.grid import GridCellEditor from wx import SIZE_ALLOW_MINUS_ONE def get_control(control): if isinstance(control, wx.Control): return control for control in control.GetChildren(): result = get_control(control) if result is not None: return result return None class TraitGridCellAdapter(GridCellEditor): """ Wrap a trait editor as a GridCellEditor object. """ def __init__( self, trait_editor_factory, obj, name, description, handler=None, context=None, style="simple", width=-1.0, height=-1.0, ): """ Build a new TraitGridCellAdapter object. """ super().__init__() self._factory = trait_editor_factory self._style = style self._width = width self._height = height self._editor = None self._obj = obj self._name = name self._description = description self._handler = handler self._context = context def Create(self, parent, id, evtHandler): """ Called to create the control, which must derive from wxControl. """ from traitsui.api import UI, default_handler # If the editor has already been created, ignore the request: if hasattr(self, "_control"): return handler = self._handler if handler is None: handler = default_handler() if self._context is None: ui = UI(handler=handler) else: context = self._context.copy() context["table_editor_object"] = context["object"] context["object"] = self._obj ui = UI(handler=handler, context=context) # Link the editor's undo history in to the main ui undo history if the # UI object is available: factory = self._factory if factory._ui is not None: ui.history = factory._ui.history # make sure the factory knows this is a grid_cell editor factory.is_grid_cell = True factory_method = getattr(factory, self._style + "_editor") self._editor = factory_method( ui, self._obj, self._name, self._description, parent ) # Tell the editor to actually build the editing widget: self._editor.prepare(parent) # Find the control to use as the editor: self._control = control = self._editor.control # Calculate and save the required editor height: grid, row, col = getattr(self, "_grid_info", (None, None, None)) width, height = control.GetBestSize() self_height = self._height if self_height > 1.0: height = int(self_height) elif (self_height >= 0.0) and (grid is not None): height = int(self_height * grid.GetSize().Get()[1]) self_width = self._width if self_width > 1.0: width = int(self_width) elif (self_width >= 0.0) and (grid is not None): width = int(self_width * grid.GetSize().Get()[0]) self._edit_width, self._edit_height = width, height # Set up the first control found within the cell editor as the cell # editor control: control = get_control(control) if control is not None: self.SetControl(control) def SetSize(self, rect): """ Called to position/size the edit control within the cell rectangle. If you don't fill the cell (the rect) then be sure to override PaintBackground and do something meaningful there. """ changed = False edit_width, edit_height = rect.width, rect.height grid, row, col = getattr(self, "_grid_info", (None, None, None)) if (grid is not None) and self._editor.scrollable: edit_width, cur_width = self._edit_width, grid.GetColSize(col) restore_width = getattr(grid, "_restore_width", None) if restore_width is not None: cur_width = restore_width if (edit_width > cur_width) or (restore_width is not None): edit_width = max(edit_width, cur_width) grid._restore_width = cur_width grid.SetColSize(col, edit_width + 1 + (col == 0)) changed = True else: edit_width = cur_width edit_height, cur_height = self._edit_height, grid.GetRowSize(row) restore_height = getattr(grid, "_restore_height", None) if restore_height is not None: cur_height = restore_height if (edit_height > cur_height) or (restore_height is not None): edit_height = max(edit_height, cur_height) grid._restore_height = cur_height grid.SetRowSize(row, edit_height + 1 + (row == 0)) changed = True else: edit_height = cur_height if changed: grid.ForceRefresh() self._control.SetSize( rect.x + 1, rect.y + 1, edit_width, edit_height, SIZE_ALLOW_MINUS_ONE, ) if changed: grid.MakeCellVisible( grid.GetGridCursorRow(), grid.GetGridCursorCol() ) def Show(self, show, attr): """ Show or hide the edit control. You can use the attr (if not None) to set colours or fonts for the control. """ if self.IsCreated(): super().Show(show, attr) def PaintBackground(self, rect, attr): """ Draws the part of the cell not occupied by the edit control. The base class version just fills it with background colour from the attribute. In this class the edit control fills the whole cell so don't do anything at all in order to reduce flicker. """ def BeginEdit(self, row, col, grid): """ Make sure the control is ready to edit. """ # We have to manually set the focus to the control self._editor.update_editor() control = self._control control.Show(True) control.SetFocus() if isinstance(control, wx.TextCtrl): control.SetSelection(-1, -1) def EndEdit(self, row, col, grid): """ Do anything necessary to complete the editing. """ self._control.Show(False) changed = False grid, row, col = self._grid_info if grid._no_reset_col: grid._no_reset_col = False else: width = getattr(grid, "_restore_width", None) if width is not None: del grid._restore_width grid.SetColSize(col, width) changed = True if grid._no_reset_row: grid._no_reset_row = False else: height = getattr(grid, "_restore_height", None) if height is not None: del grid._restore_height grid.SetRowSize(row, height) changed = True if changed: grid.ForceRefresh() def Reset(self): """ Reset the value in the control back to its starting value. """ # fixme: should we be using the undo history here? def StartingKey(self, evt): """ If the editor is enabled by pressing keys on the grid, this will be called to let the editor do something about that first key if desired. """ def StartingClick(self): """ If the editor is enabled by clicking on the cell, this method will be called to allow the editor to simulate the click on the control if needed. """ def Destroy(self): """ Final cleanup. """ self._editor.dispose() def Clone(self): """ Create a new object which is the copy of this one. """ return TraitGridCellAdapter( self._factory, self._obj, self._name, self._description, style=self._style, ) def dispose(self): if self._editor is not None: self._editor.dispose() pyface-7.4.0/pyface/ui/wx/grid/grid_cell_image_renderer.py0000644000076500000240000001254314176222673024546 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 standard string renderer normally would. """ import wx from wx.grid import GridCellRenderer from wx.grid import GridCellStringRenderer from wx import SOLID, Brush, Rect, TRANSPARENT_PEN class GridCellImageRenderer(GridCellRenderer): """ A renderer which will display a cell-specific image in addition to some text displayed in the same way the standard string renderer normally would. """ def __init__(self, provider=None): """ Build a new PyGridCellImageRenderer. 'provider', if provided, should implement get_image_for_cell(row, col) to specify an image to appear in the cell at row, col. """ GridCellRenderer.__init__(self) # save the string renderer to use for text. self._string_renderer = GridCellStringRenderer() self._provider = provider return # ------------------------------------------------------------------------ # GridCellRenderer interface. # ------------------------------------------------------------------------ def Draw(self, grid, attr, dc, rect, row, col, isSelected): """ Draw the appropriate icon into the specified grid cell. """ # clear the cell first if isSelected: bgcolor = grid.GetSelectionBackground() else: bgcolor = grid.GetCellBackgroundColour(row, col) dc.SetBackgroundMode(SOLID) dc.SetBrush(Brush(bgcolor, SOLID)) dc.SetPen(TRANSPARENT_PEN) dc.DrawRectangle(rect.x, rect.y, rect.width, rect.height) # find the correct image for this cell bmp = self._get_image(grid, row, col) # find the correct text for this cell text = self._get_text(grid, row, col) # figure out placement -- we try to center things! # fixme: we should be responding to the horizontal/vertical # alignment info in the attr object! size = self.GetBestSize(grid, attr, dc, row, col) halign, valign = attr.GetAlignment() # width first wdelta = rect.width - size.GetWidth() x = rect.x if halign == wx.ALIGN_CENTRE and wdelta > 0: x += wdelta / 2 # now height hdelta = rect.height - size.GetHeight() y = rect.y if valign == wx.ALIGN_CENTRE and hdelta > 0: y += hdelta / 2 dc.SetClippingRegion(*rect) if bmp is not None: # now draw our image into it dc.DrawBitmap(bmp, x, y, 1) x += bmp.GetWidth() if text is not None and text != "": width = rect.x + rect.width - x height = rect.y + rect.height - y # draw any text that should be included new_rect = Rect(x, y, width, height) self._string_renderer.Draw( grid, attr, dc, new_rect, row, col, isSelected ) dc.DestroyClippingRegion() def GetBestSize(self, grid, attr, dc, row, col): """ Determine best size for the cell. """ # find the correct image bmp = self._get_image(grid, row, col) if bmp is not None: bmp_size = wx.Size(bmp.GetWidth(), bmp.GetHeight()) else: bmp_size = wx.Size(0, 0) # find the correct text for this cell text = self._get_text(grid, row, col) if text is not None: text_size = self._string_renderer.GetBestSize( grid, attr, dc, row, col ) else: text_size = wx.Size(0, 0) result = wx.Size( bmp_size.width + text_size.width, max(bmp_size.height, text_size.height), ) return result def Clone(self): return GridCellImageRenderer(self._provider) # ------------------------------------------------------------------------ # protected 'GridCellIconRenderer' interface. # ------------------------------------------------------------------------ def _get_image(self, grid, row, col): """ Returns the correct bmp for the data at row, col. """ bmp = None if self._provider is not None and hasattr( self._provider, "get_image_for_cell" ): # get the image from the specified provider img = self._provider.get_image_for_cell(grid, row, col) if img is not None: bmp = img.create_bitmap() else: bmp = None return bmp def _get_text(self, grid, row, col): """ Returns the correct text for the data at row, col. """ text = None if self._provider is not None and hasattr( self._provider, "get_text_for_cell" ): # get the image from the specified provider text = self._provider.get_text_for_cell(grid, row, col) else: text = grid.GetCellValue(row, col) return text pyface-7.4.0/pyface/ui/wx/grid/tests/0000755000076500000240000000000014176460551020354 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/wx/grid/tests/__init__.py0000644000076500000240000000000014176222673022454 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/wx/grid/tests/test_simple_grid_model.py0000644000076500000240000000437014176222673025450 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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: from pyface.ui.wx.grid.api import GridRow, GridColumn, SimpleGridModel except ImportError: wx_available = False else: wx_available = True @unittest.skipUnless(wx_available, "Wx is not available") class CompositeGridModelTestCase(unittest.TestCase): def setUp(self): self.model = SimpleGridModel( data=[[None, 2], [3, 4]], rows=[GridRow(label="foo"), GridRow(label="bar")], columns=[GridColumn(label="cfoo"), GridColumn(label="cbar")], ) def test_get_column_count(self): self.assertEqual(self.model.get_column_count(), 2) def test_get_row_count(self): self.assertEqual(self.model.get_row_count(), 2) def test_get_row_name(self): # Regardless of the rows specified in the composed models, the # composite model returns its own rows. self.assertEqual(self.model.get_row_name(0), "foo") self.assertEqual(self.model.get_row_name(1), "bar") def test_get_column_name(self): self.assertEqual(self.model.get_column_name(0), "cfoo") self.assertEqual(self.model.get_column_name(1), "cbar") def test_get_value(self): self.assertEqual(self.model.get_value(0, 0), None) self.assertEqual(self.model.get_value(0, 1), 2) self.assertEqual(self.model.get_value(1, 0), 3) self.assertEqual(self.model.get_value(1, 1), 4) def test_is_cell_empty(self): rows = self.model.get_row_count() columns = self.model.get_column_count() self.assertEqual( self.model.is_cell_empty(0, 0), True, "Cell containing None should be empty.", ) self.assertEqual( self.model.is_cell_empty(rows+1, columns+1), True, "Cell outside the range of values should be empty.", ) return pyface-7.4.0/pyface/ui/wx/grid/tests/test_composite_grid_model.py0000644000076500000240000000724714176222673026167 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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: from pyface.ui.wx.grid.api import ( CompositeGridModel, GridRow, GridColumn, SimpleGridModel, ) except ImportError: wx_available = False else: wx_available = True @unittest.skipUnless(wx_available, "Wx is not available") class CompositeGridModelTestCase(unittest.TestCase): def setUp(self): self.model_1 = SimpleGridModel( data=[[1, 2], [3, 4]], rows=[GridRow(label="foo"), GridRow(label="bar")], columns=[GridColumn(label="cfoo"), GridColumn(label="cbar")], ) self.model_2 = SimpleGridModel( data=[[3, 4, 5], [6, 7, 8]], rows=[GridRow(label="baz"), GridRow(label="bat")], columns=[ GridColumn(label="cfoo_2"), GridColumn(label="cbar_2"), GridColumn(label="cbaz_2"), ], ) self.model = CompositeGridModel(data=[self.model_1, self.model_2]) def test_get_column_count(self): column_count_1 = self.model_1.get_column_count() column_count_2 = self.model_2.get_column_count() self.assertEqual( self.model.get_column_count(), column_count_1 + column_count_2 ) def test_get_row_count(self): self.assertEqual(self.model.get_row_count(), 2) def test_get_row_name(self): # Regardless of the rows specified in the composed models, the # composite model returns its own rows. self.assertEqual(self.model.get_row_name(0), "1") self.assertEqual(self.model.get_row_name(1), "2") def test_get_column_name(self): self.assertEqual(self.model.get_column_name(0), "cfoo") self.assertEqual(self.model.get_column_name(1), "cbar") self.assertEqual(self.model.get_column_name(2), "cfoo_2") self.assertEqual(self.model.get_column_name(3), "cbar_2") self.assertEqual(self.model.get_column_name(4), "cbaz_2") def test_get_value(self): self.assertEqual(self.model.get_value(0, 0), 1) self.assertEqual(self.model.get_value(0, 1), 2) self.assertEqual(self.model.get_value(0, 2), 3) self.assertEqual(self.model.get_value(0, 3), 4) self.assertEqual(self.model.get_value(0, 4), 5) self.assertEqual(self.model.get_value(1, 0), 3) self.assertEqual(self.model.get_value(1, 1), 4) self.assertEqual(self.model.get_value(1, 2), 6) self.assertEqual(self.model.get_value(1, 3), 7) self.assertEqual(self.model.get_value(1, 4), 8) def test_is_cell_empty(self): rows = self.model.get_row_count() columns = self.model.get_column_count() self.assertEqual( self.model.is_cell_empty(0, 0), False, "Cell (0,0) should not be empty.", ) self.assertEqual( self.model.is_cell_empty(rows, 0), True, "Cell below the table should be empty.", ) self.assertEqual( self.model.is_cell_empty(0, columns), True, "Cell right of the table should be empty.", ) self.assertEqual( self.model.is_cell_empty(rows, columns), True, "Cell below and right of table should be empty.", ) return pyface-7.4.0/pyface/ui/wx/grid/__init__.py0000644000076500000240000000062714176222673021331 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! pyface-7.4.0/pyface/ui/wx/grid/grid_cell_renderer.py0000644000076500000240000000227114176222673023401 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 Any, HasTraits class GridCellRenderer(HasTraits): # The toolkit-specific renderer for this cell. renderer = Any() # A handler to be invoked on right-button mouse clicks. def on_right_click(self, grid, row, col): pass # A handler to be invoked on right-button mouse double clicks. def on_right_dclick(self, grid, row, col): pass # A handler to be invoked on left-button mouse clicks. def on_left_click(self, grid, row, col): pass # A handler to be invoked on left-button mouse double clicks. def on_left_dclick(self, grid, row, col): pass # A handler to be invoked on key press. def on_key(self, grid, row, col, key_event): pass # Clean-up! def dispose(self): pass pyface-7.4.0/pyface/ui/wx/grid/edit_renderer.py0000644000076500000240000000225314176222673022402 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 .edit_image_renderer import EditImageRenderer from .grid_cell_renderer import GridCellRenderer class EditRenderer(GridCellRenderer): def __init__(self, **traits): # base-class constructor super().__init__(**traits) # initialize the renderer, if it hasn't already been initialized if self.renderer is None: self.renderer = EditImageRenderer() def on_left_click(self, grid, row, col): """ Calls edit_traits on the object represented by the row. """ obj = grid.model.get_rows_drag_value([row])[0] # allow editting if the obj does not have an editable trait # or if the editable trait is True if (not hasattr(obj, "editable")) or obj.editable: obj.edit_traits(kind="live") pyface-7.4.0/pyface/ui/wx/grid/mapped_grid_cell_image_renderer.py0000644000076500000240000000362014176222673026070 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 standard string renderer normally would, all data retrieved from specified value maps. """ from .grid_cell_image_renderer import GridCellImageRenderer class MappedGridCellImageRenderer(GridCellImageRenderer): """ Maps data values to image and text. """ def __init__(self, image_map=None, text_map=None): # Base-class constructor. We pass ourself as the provider super().__init__(self) self.image_map = image_map self.text_map = text_map def get_image_for_cell(self, grid, row, col): if self.image_map is None: return value = self._get_value(grid, row, col) if value in self.image_map: result = self.image_map[value] else: result = None return result def get_text_for_cell(self, grid, row, col): if self.text_map is None: return value = self._get_value(grid, row, col) if value in self.text_map: result = self.text_map[value] else: result = None return result def _get_value(self, grid, row, col): # first grab the PyGridTableBase object base = grid.GetTable() # from that we can get the pyface-level model object model = base.model # retrieve the unformatted value from the model and return it return model.get_cell_drag_value(row, col) pyface-7.4.0/pyface/ui/wx/grid/checkbox_renderer.py0000644000076500000240000000224114176222673023240 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 from .checkbox_image_renderer import CheckboxImageRenderer from .grid_cell_renderer import GridCellRenderer logger = logging.getLogger(__name__) class CheckboxRenderer(GridCellRenderer): def __init__(self, **traits): # base-class constructor super().__init__(**traits) # initialize the renderer, if it hasn't already been initialized if self.renderer is None: self.renderer = CheckboxImageRenderer() def on_left_click(self, grid, row, col): """ Toggles the value. """ value = grid.model.get_cell_drag_value(row, col) try: grid.model.set_value(row, col, not value) except Exception: logger.exception("Can't set cell value") return True pyface-7.4.0/pyface/ui/wx/grid/api.py0000644000076500000240000000144114176222673020336 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 .grid import Grid from .grid_model import GridModel, GridSortEvent from .composite_grid_model import CompositeGridModel from .inverted_grid_model import InvertedGridModel from .simple_grid_model import SimpleGridModel, GridRow, GridColumn from .trait_grid_model import ( TraitGridModel, TraitGridColumn, TraitGridSelection, ) from .grid_cell_renderer import GridCellRenderer pyface-7.4.0/pyface/ui/wx/grid/trait_grid_model.py0000644000076500000240000005703414176222673023106 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 TraitGridModel builds a grid from a list of traits objects. Each row represents on object, each column one trait from those objects. All the objects must be of the same type. Optionally a user may pass in a list of trait names defining which traits will be shown in the columns and in which order. If this list is not passed in, then the first object is inspected and every trait from that object gets a column.""" from functools import cmp_to_key from traits.api import ( Any, Bool, Callable, Dict, HasTraits, Instance, Int, List, Str, Type, Union, ) from traits.observation.api import match from .grid_model import GridColumn, GridModel, GridSortEvent from .trait_grid_cell_adapter import TraitGridCellAdapter # The classes below are part of the table specification. class TraitGridColumn(GridColumn): """ Structure for holding column specifications in a TraitGridModel. """ # The trait name for this column. This takes precedence over method name = Union(None, Str) # A method name to call to get the value for this column method = Union(None, Str) # A method to be used to sort on this column sorter = Callable # A dictionary of formats for the display of different types. If it is # defined as a callable, then that callable must accept a single argument. formats = Dict(Type, Union(Str, Callable)) # A name to designate the type of this column typename = Union(None, Str) # note: context menus should go in here as well? but we need # more info than we have available at this point size = Int(-1) class TraitGridSelection(HasTraits): """ Structure for holding specification information. """ # The selected object obj = Instance(HasTraits) # The specific trait selected on the object trait_name = Union(None, Str) # The meat. class TraitGridModel(GridModel): """ A TraitGridModel builds a grid from a list of traits objects. Each row represents on object, each column one trait from those objects. All the objects must be of the same type. Optionally a user may pass in a list of trait names defining which traits will be shown in the columns and in which order. If this list is not passed in, then the first object is inspected and every trait from that object gets a column.""" # A 2-dimensional list/array containing the grid data. data = List(Any) # The column definitions columns = Union(None, List(Union(None, Str, Instance(TraitGridColumn)))) # The trait to look at to get the row name row_name_trait = Union(None, Str) # Allow column sorting? allow_column_sort = Bool(True) # A factory to generate new rows. If this is not None then it must # be a no-argument function. row_factory = Callable # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, **traits): """ Create a TraitGridModel object. """ # Base class constructor super().__init__(**traits) # if no columns are pass in then create the list of names # from the first trait in the list. if the list is empty, # the columns should be an empty list as well. self._auto_columns = self.columns if self.columns is None or len(self.columns) == 0: if self.data is not None and len(self.data) > 0: self._auto_columns = [] # we only add traits that aren't events, since events # are write-only for name, trait in self.data[0].traits().items(): if trait.type != "event": self._auto_columns.append(TraitGridColumn(name=name)) else: self._auto_columns = [] # attach trait handlers to the list object self.observe(self._on_data_changed, "data") self.observe(self._on_data_items_changed, "data:items") # attach appropriate trait handlers to objects in the list self.__manage_data_listeners(self.data) # attach a listener to the column definitions so we refresh when # they change self.observe(self._on_columns_changed, "columns") self.observe(self._on_columns_items_changed, "columns:items") # attach listeners to the column definitions themselves self.__manage_column_listeners(self.columns) # attach a listener to the row_name_trait self.observe(self._on_row_name_trait_changed, "row_name_trait") # ------------------------------------------------------------------------ # 'GridModel' interface. # ------------------------------------------------------------------------ def get_column_count(self): """ Return the number of columns for this table. """ return len(self._auto_columns) def get_column_name(self, index): """ Return the label of the column specified by the (zero-based) index. """ try: name = col = self._auto_columns[index] if isinstance(col, TraitGridColumn): if col.label is not None: name = col.label else: name = col.name except IndexError: name = "" return name def get_column_size(self, index): """ Return the size in pixels of the column indexed by col. A value of -1 or None means use the default. """ size = -1 try: col = self._auto_columns[index] if isinstance(col, TraitGridColumn): size = col.size except IndexError: pass return size def get_cols_drag_value(self, cols): """ Return the value to use when the specified columns are dragged or copied and pasted. cols is a list of column indexes. """ # iterate over every column, building a list of the values in that # column value = [] for col in cols: value.append(self.__get_data_column(col)) return value def get_cols_selection_value(self, cols): """ Returns a list of TraitGridSelection objects containing the object corresponding to the grid rows and the traits corresponding to the specified columns. """ values = [] for obj in self.data: for col in cols: values.append( TraitGridSelection( obj=obj, trait_name=self.__get_column_name(col) ) ) return values def sort_by_column(self, col, reverse=False): """ Sort model data by the column indexed by col. """ # first check to see if we allow sorts by column if not self.allow_column_sort: return # see if a sorter is specified for this column try: column = self._auto_columns[col] name = self.__get_column_name(col) # by default we use cmp to sort on the traits key = None if ( isinstance(column, TraitGridColumn) and column.sorter is not None ): key = cmp_to_key(column.sorter) except IndexError: return def key_function(a): trait = getattr(a, name, None) if key: return key(trait) self.data.sort(key=key_function, reverse=reverse) # now fire an event to tell the grid we're sorted self.column_sorted = GridSortEvent(index=col, reversed=reverse) def is_column_read_only(self, index): """ Return True if the column specified by the zero-based index is read-only. """ return self.__get_column_readonly(index) def get_row_count(self): """ Return the number of rows for this table. """ if self.data is not None: count = len(self.data) else: count = 0 return count def get_row_name(self, index): """ Return the name of the row specified by the (zero-based) index. """ if self.row_name_trait is not None: try: row = self._get_row(index) if hasattr(row, self.row_name_trait): name = getattr(row, self.row_name_trait) except IndexError: name = str(index + 1) else: name = str(index + 1) return name def get_rows_drag_value(self, rows): """ Return the value to use when the specified rows are dragged or copied and pasted. rows is a list of row indexes. If there is only one row listed, return the corresponding trait object. If more than one row is listed then return a list of objects. """ # return a list of objects value = [] for index in rows: try: # note that we can't use get_value for this because it # sometimes returns strings instead of the actual value, # e.g. in cases where a float_format is specified value.append(self._get_row(index)) except IndexError: value.append(None) return value def get_rows_selection_value(self, rows): """ Returns a list of TraitGridSelection objects containing the object corresponding to the selected rows. """ values = [] for row_index in rows: values.append(TraitGridSelection(obj=self.data[row_index])) return values def is_row_read_only(self, index): """ Return True if the row specified by the zero-based index is read-only. """ return False def get_cell_editor(self, row, col): """ Return the editor for the specified cell. """ # print 'TraitGridModel.get_cell_editor row: ', row, ' col: ', col obj = self.data[row] trait_name = self.__get_column_name(col) trait = obj.base_trait(trait_name) if trait is None: return None factory = trait.get_editor() return TraitGridCellAdapter(factory, obj, trait_name, "") def get_cell_drag_value(self, row, col): """ Return the value to use when the specified cell is dragged or copied and pasted. """ # find the name of the column indexed by col # note that this code is the same as the get_value code but without # the potential string formatting column = self.__get_column(col) obj = self._get_row(row) value = self._get_data_from_row(obj, column) return value def get_cell_selection_value(self, row, col): """ Returns a TraitGridSelection object specifying the data stored in the table at (row, col). """ obj = self.data[row] trait_name = self.__get_column_name(col) return TraitGridSelection(obj=obj, trait_name=trait_name) def resolve_selection(self, selection_list): """ Returns a list of (row, col) grid-cell coordinates that correspond to the objects in objlist. 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 TraitGridModel, the objects in objlist must be TraitGridSelection objects. """ cells = [] for selection in selection_list: try: row = self.data.index(selection.obj) except ValueError: continue column = -1 if selection.trait_name is not None: column = self._get_column_index_by_trait(selection.trait_name) if column is None: continue cells.append((row, column)) return cells def get_type(self, row, col): """ Return the value stored in the table at (row, col). """ typename = self.__get_column_typename(col) return typename def get_value(self, row, col): """ Return the value stored in the table at (row, col). """ value = self.get_cell_drag_value(row, col) formats = self.__get_column_formats(col) if ( value is not None and formats is not None and type(value) in formats and formats[type(value)] is not None ): try: format = formats[type(value)] if callable(format): value = format(value) else: value = format % value except TypeError: # not enough arguments? wrong kind of arguments? pass return value def is_cell_empty(self, row, col): """ Returns True if the cell at (row, col) has a None value, False otherwise.""" value = self.get_value(row, col) return value is None def is_cell_editable(self, row, col): """ Returns True if the cell at (row, col) is editable, False otherwise. """ return not self.is_column_read_only(col) # ------------------------------------------------------------------------ # protected 'GridModel' interface. # ------------------------------------------------------------------------ def _insert_rows(self, pos, num_rows): """ Inserts num_rows at pos and fires an event iff a factory method for new rows is defined. Otherwise returns 0. """ count = 0 if self.row_factory is not None: new_data = [] for i in range(num_rows): new_data.append(self.row_factory()) 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. """ if pos + num_rows >= self.get_row_count(): num_rows = self.get_rows_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 (row, col) does not exist. """ # print 'TraitGridModel._set_value: new: ', value new_rows = 0 # find the column indexed by col column = self.__get_column(col) obj = self._get_row(row) success = False if obj is not None: success = self._set_data_on_row(obj, column, value) else: # 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 obj = self._get_row(self.get_row_count() - 1) success = self._set_data_on_row(obj, column, value) if not success: # fixme: what do we do in this case? veto the set somehow? raise # an exception? pass return new_rows # ------------------------------------------------------------------------ # protected interface. # ------------------------------------------------------------------------ def _get_row(self, index): """ Return the object that corresponds to the row at index. Override this to handle very large data sets. """ return self.data[index] def _get_data_from_row(self, row, column): """ Retrieve the data specified by column for this row. Attribute can be either a member of the row object, or a no-argument method on that object. Override this method to provide alternative ways of accessing the data in the object. """ value = None if row is not None and column is not None: if not isinstance(column, TraitGridColumn): # first handle the case where the column # definition might be just a string if hasattr(row, column): value = getattr(row, column) elif column.name is not None and hasattr(row, column.name): # this is the case when the trait name is specified value = getattr(row, column.name) elif column.method is not None and hasattr(row, column.method): # this is the case when an object method is specified value = getattr(row, column.method)() if value is None: return None else: return str(value) # value def _set_data_on_row(self, row, column, value): """ Retrieve the data specified by column for this row. Attribute can be either a member of the row object, or a no-argument method on that object. Override this method to provide alternative ways of accessing the data in the object. """ success = False if row is not None and column is not None: if not isinstance(column, TraitGridColumn): if hasattr(row, column): # sometimes the underlying grid gives us 0/1 instead # of True/False. do some conversion here to make that # case worl. # if type(getattr(row, column)) == bool and \ # type(value) != bool: # convert the value to a boolean # value = bool(value) setattr(row, column, value) success = True elif column.name is not None and hasattr(row, column.name): # sometimes the underlying grid gives us 0/1 instead # of True/False. do some conversion here to make that # case worl. # if type(getattr(row, column.name)) == bool and \ # type(value) != bool: # convert the value to a boolean # value = bool(value) setattr(row, column.name, value) success = True # do nothing in the method case as we don't allow rows # defined to return a method value to set the value return success def _insert_rows_into_model(self, pos, new_data): """ Insert the given new rows into the model. Override this method to handle very large data sets. """ for data in new_data: self.data.insert(pos, data) pos += 1 def _delete_rows_from_model(self, pos, num_rows): """ Delete the specified rows from the model. Override this method to handle very large data sets. """ del self.data[pos, pos + num_rows] return num_rows # ------------------------------------------------------------------------ # trait handlers # ------------------------------------------------------------------------ def _on_row_name_trait_changed(self, event): """ Force the grid to refresh when any underlying trait changes. """ self.fire_content_changed() def _on_columns_changed(self, event): """ Force the grid to refresh when any underlying trait changes. """ self.__manage_column_listeners(event.old, remove=True) self.__manage_column_listeners(self.columns) self._auto_columns = self.columns self.fire_structure_changed() def _on_columns_items_changed(self, event): """ Force the grid to refresh when any underlying trait changes. """ self.__manage_column_listeners(event.removed, remove=True) self.__manage_column_listeners(event.added) self.fire_structure_changed() def _on_contained_trait_changed(self, event): """ Force the grid to refresh when any underlying trait changes. """ self.fire_content_changed() def _on_data_changed(self, event): """ Force the grid to refresh when the underlying list changes. """ self.__manage_data_listeners(event.old, remove=True) self.__manage_data_listeners(self.data) self.fire_structure_changed() def _on_data_items_changed(self, event): """ Force the grid to refresh when the underlying list changes. """ # if an item was removed then remove that item's listener self.__manage_data_listeners(event.removed, remove=True) # if items were added then add trait change listeners on those items self.__manage_data_listeners(event.added) self.fire_content_changed() # ------------------------------------------------------------------------ # private interface. # ------------------------------------------------------------------------ def __get_data_column(self, col): """ Return a 1-d list of data from the column indexed by col. """ row_count = self.get_row_count() coldata = [] for row in range(row_count): try: val = self.get_value(row, col) if val is None: coldata.append(None) else: coldata.append(val) # self.get_value(row, col)) except IndexError: coldata.append(None) return coldata def __get_column(self, col): try: column = self._auto_columns[col] except IndexError: column = None return column def __get_column_name(self, col): name = column = self.__get_column(col) if isinstance(column, TraitGridColumn): name = column.name return name def __get_column_typename(self, col): column = self.__get_column(col) typename = None if isinstance(column, TraitGridColumn): typename = column.typename return typename def __get_column_readonly(self, col): read_only = False column = self.__get_column(col) if isinstance(column, TraitGridColumn): read_only = column.read_only return read_only def __get_column_formats(self, col): formats = None column = self.__get_column(col) if isinstance(column, TraitGridColumn): formats = column.formats return formats def _get_column_index_by_trait(self, trait_name): cols = self._auto_columns for i in range(len(cols)): col = cols[i] if isinstance(col, TraitGridColumn): col_name = col.name else: col_name = col if col_name == trait_name: return i return None def __manage_data_listeners(self, list, remove=False): # attach appropriate trait handlers to objects in the list if list is not None: for item in list: item.observe( self._on_contained_trait_changed, match(lambda name, trait: True), remove=remove ) def __manage_column_listeners(self, collist, remove=False): if collist is not None: for col in collist: if isinstance(col, TraitGridColumn): col.observe( self._on_columns_changed, match(lambda name, trait: True), remove=remove, ) pyface-7.4.0/pyface/ui/wx/grid/grid_model.py0000644000076500000240000003415114176222673021676 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Model for grid views. """ from traits.api import ( Bool, Event, HasPrivateTraits, HasTraits, Instance, Int, Str, Tuple, ) # The classes below are part of the table specification. class GridRow(HasTraits): """ Structure for holding row/column specifications. """ # Is the data in this column read-only? read_only = Bool(False) # The label for this column label = Str() # We specify the same info for rows and for columns, but add a GridColumn # name for clarity. GridColumn = GridRow class GridSortData(HasTraits): """ An event that signals a sorting has taken place. The index attribute should be set to the row or column on which the data was sorted. An index of -1 indicates that no sort has been applied (or a previous sort has been unapplied). The reversed flag indicates that the sort has been done in reverse order. """ index = Int(-1) reversed = Bool(False) # for backwards compatibility GridSortEvent = GridSortData class GridModel(HasPrivateTraits): """ Model for grid views. """ # 'GridModel' interface ------------------------------------------------ # Fire when the structure of the underlying grid model has changed. structure_changed = Event() # Fire when the content of the underlying grid model has changed. content_changed = Event() # Column model is currently sorted on. column_sorted = Instance(GridSortData) # The cell (row, col) the mouse is currently in. mouse_cell = Tuple(Int, Int) # Events ---- # A row was inserted or appended to this model rows_added = Event() # A column was inserted or appended to this model columns_added = Event() # A row sort took place row_sorted = Event() # Event fired when a cell is clicked on: click = Event # = (row, column) that was clicked on # Event fired when a cell is double-clicked on: dclick = Event # = (row, column) that was double-clicked on # ------------------------------------------------------------------------ # 'GridModel' interface -- Subclasses MUST override the following # ------------------------------------------------------------------------ def get_column_count(self): """ Return the number of columns for this table. """ raise NotImplementedError() def get_column_name(self, index): """ Return the name of the column specified by the (zero-based) index. """ raise NotImplementedError() def get_row_count(self): """ Return the number of rows for this table. """ raise NotImplementedError() def get_row_name(self, index): """ Return the name of the row specified by the (zero-based) index. """ raise NotImplementedError() def get_value(self, row, col): """ Return the value stored in the table at (row, col). """ raise NotImplementedError() def is_cell_empty(self, row, col): """ Returns True if the cell at (row, col) has a None value, False otherwise.""" raise NotImplementedError() # ------------------------------------------------------------------------ # 'GridModel' interface -- Subclasses MAY override the following # ------------------------------------------------------------------------ def get_cols_drag_value(self, cols): """ Return the value to use when the specified columns are dragged or copied and pasted. cols is a list of column indexes. """ pass def get_cols_selection_value(self, cols): """ Return the value to use when the specified cols are selected. This value should be enough to specify to other listeners what is going on in the grid. rows is a list of row indexes. """ cols_data = [] row_count = self.get_row_count() for col in cols: col_data = [] for row in range(row_count): col_data.append(self.get_value(row, col)) cols_data.append(col_data) return cols_data def get_column_context_menu(self, col): """ Return a MenuManager object that will generate the appropriate context menu for this column.""" pass def get_column_size(self, col): """ Return the size in pixels of the column indexed by col. A value of -1 or None means use the default. """ return None def sort_by_column(self, col, reverse=False): """ Sort model data by the column indexed by col. The reverse flag indicates that the sort should be done in reverse. """ pass def no_column_sort(self): """ Turn off any column sorting of the model data. """ raise NotImplementedError() def is_column_read_only(self, index): """ Return True if the column specified by the zero-based index is read-only. """ return False def get_rows_drag_value(self, rows): """ Return the value to use when the specified rows are dragged or copied and pasted. rows is a list of row indexes. """ pass def get_rows_selection_value(self, rows): """ Return the value to use when the specified rows are selected. This value should be enough to specify to other listeners what is going on in the grid. rows is a list of row indexes. """ rows_data = [] column_count = self.get_column_count() for row in rows: row_data = [] for col in range(column_count): row_data.append(self.get_value(row, col)) rows_data.append(row_data) return rows_data def get_row_context_menu(self, row): """ Return a MenuManager object that will generate the appropriate context menu for this row.""" pass def get_row_size(self, row): """ Return the size in pixels of the row indexed by 'row'. A value of -1 or None means use the default. """ return None def sort_by_row(self, row, reverse=False): """ Sort model data by the data row indexed by row. The reverse flag indicates that the sort should be done in reverse. """ pass def no_row_sort(self): """ Turn off any row sorting of the model data. """ raise NotImplementedError() def is_row_read_only(self, index): """ Return True if the row specified by the zero-based index is read-only. """ return False def get_type(self, row, col): """ Return the value stored in the table at (row, col). """ raise NotImplementedError() def get_cell_drag_value(self, row, col): """ Return the value to use when the specified cell is dragged or copied and pasted. """ # by default we just use the cell value return self.get_value(row, col) def get_cell_selection_value(self, row, col): """ Return the value stored in the table at (row, col). """ pass def get_cell_editor(self, row, col): """ Return the editor for the specified cell. """ return None def get_cell_renderer(self, row, col): """ Return the renderer for the specified cell. """ return None 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. Note that the objects in selection_list are model-specific. """ return selection_list # fixme: this context menu stuff is going in here for now, but it # seems like this is really more of a view piece than a model piece. # this is how the tree control does it, however, so we're duplicating # that here. def get_cell_context_menu(self, row, col): """ Return a MenuManager object that will generate the appropriate context menu for this cell.""" pass 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, False otherwise. """ return False 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. Note that subclasses should not override this method, but should override the _set_value method instead. """ # grids are passing only strings, this is temp workaraound if isinstance(value, str): try: value = int(value) except ValueError: try: value = float(value) except ValueError: value = value self._set_value(row, col, value) self.fire_content_changed() def is_cell_read_only(self, row, col): """ Returns True if the cell at (row, col) is not editable, False otherwise. """ return False def get_cell_bg_color(self, row, col): """ Return a wxColour object specifying what the background color of the specified cell should be. """ return None def get_cell_text_color(self, row, col): """ Return a wxColour object specifying what the text color of the specified cell should be. """ return None def get_cell_font(self, row, col): """ Return a wxFont object specifying what the font of the specified cell should be. """ return None def get_cell_halignment(self, row, col): """ Return a string specifying what the horizontal alignment of the specified cell should be. Return 'left' for left alignment, 'right' for right alignment, or 'center' for center alignment. """ return None def get_cell_valignment(self, row, col): """ Return a string specifying what the vertical alignment of the specified cell should be. Return 'top' for top alignment, 'bottom' for bottom alignment, or 'center' for center alignment. """ return None # ------------------------------------------------------------------------ # 'GridModel' interface -- Subclasses MAY NOT override the following # ------------------------------------------------------------------------ def fire_content_changed(self): """ Fires the appearance changed event. """ self.content_changed = "changed" def fire_structure_changed(self): """ Fires the appearance changed event. """ self.structure_changed = "changed" def delete_rows(self, pos, num_rows): """ Removes rows pos through pos + num_rows from the model. Subclasses should not override this method, but should override _delete_rows instead. """ deleted = self._delete_rows(pos, num_rows) if deleted > 0: self.fire_structure_changed() return True def insert_rows(self, pos, num_rows): """ Inserts rows at pos through pos + num_rows into the model. Subclasses should not override this method, but should override _insert_rows instead. """ inserted = self._insert_rows(pos, num_rows) if inserted > 0: self.fire_structure_changed() return True def delete_columns(self, pos, num_cols): """ Removes columns pos through pos + num_cols from the model. Subclasses should not override this method, but should override _delete_columns instead. """ deleted = self._delete_columns(pos, num_cols) if deleted > 0: self.fire_structure_changed() return True def insert_columns(self, pos, num_cols): """ Inserts columns at pos through pos + num_cols into the model. Subclasses should not override this method, but should override _insert_columns instead. """ inserted = self._insert_columns(pos, num_cols) if inserted > 0: self.fire_structure_changed() return True # ------------------------------------------------------------------------ # protected 'GridModel' interface -- Subclasses should override these # if they wish to support the # specific actions. # ------------------------------------------------------------------------ def _delete_rows(self, pos, num_rows): """ Implementation method for delete_rows. Should return the number of rows that were deleted. """ pass def _insert_rows(self, pos, num_rows): """ Implementation method for insert_rows. Should return the number of rows that were inserted. """ pass def _delete_columns(self, pos, num_cols): """ Implementation method for delete_cols. Should return the number of columns that were deleted. """ pass def _insert_columns(self, pos, num_cols): """ Implementation method for insert_columns. Should return the number of columns that were inserted. """ pass def _set_value(self, row, col, value): """ Implementation method for set_value. Should return the number of rows or columns, if any, that were appended. """ pass def _move_column(self, frm, to): """ Moves a specified **frm** column to before the specified **to** column. Returns **True** if successful; **False** otherwise. """ return False def _move_row(self, frm, to): """ Moves a specified **frm** row to before the specified **to** row. Returns **True** if successful; **False** otherwise. """ return False pyface-7.4.0/pyface/ui/wx/grid/checkbox_image_renderer.py0000644000076500000240000000203614176222673024404 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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. """ from pyface.image_resource import ImageResource from .mapped_grid_cell_image_renderer import MappedGridCellImageRenderer checked_image_map = { True: ImageResource("checked"), False: ImageResource("unchecked"), } class CheckboxImageRenderer(MappedGridCellImageRenderer): def __init__(self, display_text=False): text_map = None if display_text: text_map = {True: "True", False: "False"} # Base-class constructor super().__init__(checked_image_map, text_map) return pyface-7.4.0/pyface/ui/wx/grid/edit_image_renderer.py0000644000076500000240000000221114176222673023536 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.image_resource import ImageResource # Local import from .grid_cell_image_renderer import GridCellImageRenderer class EditImageRenderer(GridCellImageRenderer): image = ImageResource("table_edit") def __init__(self, **kw): super().__init__(self, **kw) def get_image_for_cell(self, grid, row, col): """ returns the image resource for the table_edit bitmap """ # show the icon if the obj does not have an editable trait # or if the editable trait is True obj = grid.GetTable().model.get_rows_drag_value([row])[0] if (not hasattr(obj, "editable")) or obj.editable: return self.image return None def _get_text(self, grid, row, col): return None pyface-7.4.0/pyface/ui/wx/grid/inverted_grid_model.py0000644000076500000240000000700614176222673023575 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ An adapter model that inverts all of its row/column targets. Use this class with the CompositeGridModel to make models with different orientations match, or use it to visually flip the data without modifying the underlying model's sense of row and column. """ from traits.api import Instance from .grid_model import GridModel class InvertedGridModel(GridModel): """ An adapter model that inverts all of its row/column targets. Use this class with the CompositeGridModel to make models with different orientations match, or use it to visually flip the data without modifying the underlying model's sense of row and column. """ model = Instance(GridModel, ()) # ------------------------------------------------------------------------ # 'GridModel' interface. # ------------------------------------------------------------------------ def get_column_count(self): return self.model.get_row_count() def get_column_name(self, index): return self.model.get_row_name(index) def get_cols_drag_value(self, cols): return self.model.get_rows_drag_value(cols) def get_cols_selection_value(self, cols): return self.model.get_rows_selection_value(cols) def get_column_context_menu(self, col): return self.model.get_row_context_menu(col) def sort_by_column(self, col, reverse=False): return self.model.sort_by_row(col, reverse) def is_column_read_only(self, index): return self.model.is_row_read_only(index) def get_row_count(self): return self.model.get_column_count() def get_row_name(self, index): return self.model.get_column_name(index) def get_rows_drag_value(self, rows): return self.model.get_cols_drag_value(rows) def get_rows_selection_value(self, rows): return self.model.get_cols_selection_value(rows) def get_row_context_menu(self, row): return self.model.get_col_context_menu(row) def sort_by_row(self, row, reverse=False): return self.model.sort_by_col(row, reverse) def is_row_read_only(self, index): return self.model.is_column_read_only(index) def delete_rows(self, pos, num_rows): return self.model.delete_cols(pos, num_rows) def insert_cols(self, pos, num_rows): return self.model.insert_rows(pos, num_rows) def get_value(self, row, col): return self.model.get_value(col, row) def get_cell_drag_value(self, row, col): return self.model.get_cell_drag_value(col, row) def get_cell_selection_value(self, row, col): return self.model.get_cell_selection_value(col, row) def resolve_selection(self, selection_list): return self.model.resolve_selection(selection_list) def get_cell_context_menu(self, row, col): return self.model.get_cell_context_menu(col, row) def set_value(self, row, col, value): return self.model.set_value(col, row, value) def is_cell_empty(self, row, col): return self.model.is_cell_empty(col, row) def is_cell_editable(self, row, col): return self.model.is_cell_editable(col, row) pyface-7.4.0/pyface/ui/null/0000755000076500000240000000000014176460550016600 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/null/color.py0000644000076500000240000000264514176222673020301 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Color conversion routines for the null toolkit. This module provides a couple of utility methods to support the pyface.color.Color class to_toolkit and from_toolkit methods. For definiteness, the null toolkit uses tuples of RGBA values from 0 to 255 to represent colors. """ from pyface.color import channels_to_ints, ints_to_channels def toolkit_color_to_rgba(color): """ Convert a hex tuple to an RGBA tuple. Parameters ---------- color : tuple A tuple of integer values from 0 to 255 inclusive. Returns ------- rgba : tuple A tuple of 4 floating point values between 0.0 and 1.0 inclusive. """ return ints_to_channels(color) def rgba_to_toolkit_color(rgba): """ Convert an RGBA tuple to a hex tuple. Parameters ---------- color : tuple A tuple of integer values from 0 to 255 inclusive. Returns ------- rgba : tuple A tuple of 4 floating point values between 0.0 and 1.0 inclusive. """ return channels_to_ints(rgba) pyface-7.4.0/pyface/ui/null/widget.py0000644000076500000240000000201714176222673020437 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 Any, HasTraits, provides from pyface.i_widget import IWidget, MWidget @provides(IWidget) class Widget(MWidget, HasTraits): """ The toolkit specific implementation of a Widget. See the IWidget interface for the API documentation. """ # 'IWidget' interface -------------------------------------------------# control = Any() parent = Any() # ------------------------------------------------------------------------ # 'IWidget' interface. # ------------------------------------------------------------------------ def destroy(self): self.control = None pyface-7.4.0/pyface/ui/null/window.py0000644000076500000240000000450014176222673020462 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 Event, Property, provides, Str from traits.api import Tuple from pyface.i_window import IWindow, MWindow from pyface.key_pressed_event import KeyPressedEvent from .widget import Widget @provides(IWindow) class Window(MWindow, Widget): """ The toolkit specific implementation of a Window. See the IWindow interface for the API documentation. """ # 'IWindow' interface -------------------------------------------------# position = Property(Tuple) size = Property(Tuple) title = Str() # Events ----- activated = Event() closed = Event() closing = Event() deactivated = Event() key_pressed = Event(KeyPressedEvent) opened = Event() opening = Event() # Private interface ---------------------------------------------------- # Shadow trait for position. _position = Tuple((-1, -1)) # Shadow trait for size. _size = Tuple((-1, -1)) # ------------------------------------------------------------------------ # 'IWindow' interface. # ------------------------------------------------------------------------ def show(self, visible): pass # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _get_position(self): """ Property getter for position. """ return self._position def _set_position(self, position): """ Property setter for position. """ old = self._position self._position = position self.trait_property_changed("position", old, position) def _get_size(self): """ Property getter for size. """ return self._size def _set_size(self, size): """ Property setter for size. """ old = self._size self._size = size self.trait_property_changed("size", old, size) pyface-7.4.0/pyface/ui/null/image_resource.py0000644000076500000240000000412214176222673022144 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 from traits.api import Any, HasTraits, List, Property, provides from traits.api import Str from pyface.i_image_resource import IImageResource, MImageResource @provides(IImageResource) class ImageResource(MImageResource, HasTraits): """ The 'null' toolkit specific implementation of an ImageResource. See the IImageResource interface for the API documentation. """ # Private interface ---------------------------------------------------- # The resource manager reference for the image. _ref = Any() # 'ImageResource' interface -------------------------------------------- absolute_path = Property(Str) name = Str() search_path = List() # ------------------------------------------------------------------------ # 'IImage' interface. # ------------------------------------------------------------------------ def create_bitmap(self, size=None): return self.create_image(size) def create_icon(self, size=None): return self.create_image(size) # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _get_absolute_path(self): # FIXME: This doesn't quite work with the new notion of image size. We # should find out who is actually using this trait, and for what! # (AboutDialog uses it to include the path name in some HTML.) ref = self._get_ref() if ref is not None: absolute_path = os.path.abspath(self._ref.filename) else: absolute_path = self._get_image_not_found().absolute_path return absolute_path pyface-7.4.0/pyface/ui/null/tests/0000755000076500000240000000000014176460550017742 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/null/tests/__init__.py0000644000076500000240000000000014176222673022043 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/null/tests/bad_import.py0000644000076500000240000000106714176222673022442 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 used to test what happens when there is an unrelated import error # when importing a toolkit object raise ImportError("No module named nonexistent") pyface-7.4.0/pyface/ui/null/resource_manager.py0000644000076500000240000000217414176222673022501 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.resource.api import ResourceFactory class PyfaceResourceFactory(ResourceFactory): """ The implementation of a shared resource manager. """ # ------------------------------------------------------------------------ # 'ResourceFactory' toolkit interface. # ------------------------------------------------------------------------ def image_from_file(self, filename): """ Creates an image from the data in the specified filename. """ # Just return the data as a string for now. f = open(filename, "rb") data = f.read() f.close() return data def image_from_data(self, data): """ Creates an image from the specified data. """ return data pyface-7.4.0/pyface/ui/null/__init__.py0000644000076500000240000000000014176222673020701 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/null/action/0000755000076500000240000000000014176460550020055 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/null/action/tool_bar_manager.py0000644000076500000240000000477314176222673023737 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 'null' backend specific implementation of the tool bar manager. """ from traits.api import Bool, Enum, Instance, Tuple from pyface.image_cache import ImageCache from pyface.action.action_manager import ActionManager class ToolBarManager(ActionManager): """ A tool bar manager realizes itself in errr, a tool bar control. """ # 'ToolBarManager' interface ------------------------------------------- # The size of tool images (width, height). image_size = Tuple((16, 16)) # The orientation of the toolbar. orientation = Enum("horizontal", "vertical") # Should we display the name of each tool bar tool under its image? show_tool_names = Bool(True) # Should we display the horizontal divider? show_divider = Bool(True) # Private interface ---------------------------------------------------- # Cache of tool images (scaled to the appropriate size). _image_cache = Instance(ImageCache) # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, *args, **traits): """ Creates a new tool bar manager. """ # Base class contructor. super().__init__(*args, **traits) # An image cache to make sure that we only load each image used in the # tool bar exactly once. self._image_cache = ImageCache(self.image_size[0], self.image_size[1]) return # ------------------------------------------------------------------------ # 'ToolBarManager' interface. # ------------------------------------------------------------------------ def create_tool_bar(self, parent, controller=None): """ Creates a tool bar. """ # If a controller is required it can either be set as a trait on the # tool bar manager (the trait is part of the 'ActionManager' API), or # passed in here (if one is passed in here it takes precedence over the # trait). if controller is None: controller = self.controller return None pyface-7.4.0/pyface/ui/null/action/menu_manager.py0000644000076500000240000000412114176222673023065 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 'null' backend specific implementation of a menu manager. """ from traits.api import Str from pyface.action.action_manager import ActionManager from pyface.action.action_manager_item import ActionManagerItem class MenuManager(ActionManager, ActionManagerItem): """ A menu manager realizes itself in a menu control. This could be a sub-menu or a context (popup) menu. """ # 'MenuManager' interface ---------------------------------------------# # The menu manager's name (if the manager is a sub-menu, this is what its # label will be). name = Str() # ------------------------------------------------------------------------ # 'MenuManager' interface. # ------------------------------------------------------------------------ def create_menu(self, parent, controller=None): """ Creates a menu representation of the manager. """ # If a controller is required it can either be set as a trait on the # menu manager (the trait is part of the 'ActionManager' API), or # passed in here (if one is passed in here it takes precedence over the # trait). if controller is None: controller = self.controller return None # ------------------------------------------------------------------------ # 'ActionManagerItem' interface. # ------------------------------------------------------------------------ def add_to_menu(self, parent, menu, controller): """ Adds the item to a menu. """ def add_to_toolbar(self, parent, tool_bar, image_cache, controller): """ Adds the item to a tool bar. """ raise ValueError("Cannot add a menu manager to a toolbar.") pyface-7.4.0/pyface/ui/null/action/tool_palette_manager.py0000644000076500000240000000401714176222673024620 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 tool bar manager realizes itself in a tool palette control. """ from traits.api import Bool, Instance, Tuple from pyface.image_cache import ImageCache from pyface.action.action_manager import ActionManager class ToolPaletteManager(ActionManager): """ A tool bar manager realizes itself in a tool palette bar control. """ # 'ToolPaletteManager' interface --------------------------------------- # The size of tool images (width, height). image_size = Tuple((16, 16)) # Should we display the name of each tool bar tool under its image? show_tool_names = Bool(True) # Private interface ---------------------------------------------------- # Cache of tool images (scaled to the appropriate size). _image_cache = Instance(ImageCache) # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, *args, **traits): """ Creates a new tool bar manager. """ # Base class contructor. super().__init__(*args, **traits) # An image cache to make sure that we only load each image used in the # tool bar exactly once. self._image_cache = ImageCache(self.image_size[0], self.image_size[1]) return # ------------------------------------------------------------------------ # 'ToolPaletteManager' interface. # ------------------------------------------------------------------------ def create_tool_palette(self, parent, controller=None): """ Creates a tool bar. """ return None pyface-7.4.0/pyface/ui/null/action/status_bar_manager.py0000644000076500000240000000443614176222673024301 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 status bar manager realizes itself in a status bar control. """ from traits.api import Any, HasTraits, List, Property, Str class StatusBarManager(HasTraits): """ A status bar manager realizes itself in a status bar control. """ # The manager's unique identifier (if it has one). id = Str() # The message displayed in the first field of the status bar. message = Property # The messages to be displayed in the status bar fields. messages = List(Str) # The toolkit-specific control that represents the status bar. status_bar = Any() # ------------------------------------------------------------------------ # 'StatusBarManager' interface. # ------------------------------------------------------------------------ def create_status_bar(self, parent): """ Creates a status bar. """ return self.status_bar # ------------------------------------------------------------------------ # Property handlers. # ------------------------------------------------------------------------ def _get_message(self): if len(self.messages) > 0: message = self.messages[0] else: message = "" return message def _set_message(self, value): if len(self.messages) > 0: old = self.messages[0] self.messages[0] = value else: old = "" self.messages.append(old) self.trait_property_changed("message", old, value) return # ------------------------------------------------------------------------ # Trait event handlers. # ------------------------------------------------------------------------ def _messages_changed(self): """ Sets the text displayed on the status bar. """ def _messages_items_changed(self): """ Sets the text displayed on the status bar. """ return pyface-7.4.0/pyface/ui/null/action/__init__.py0000644000076500000240000000062714176222673022175 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! pyface-7.4.0/pyface/ui/null/action/tool_palette.py0000644000076500000240000000535314176222673023132 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ View of an ActionManager drawn as a rectangle of buttons. """ from pyface.widget import Widget from traits.api import Bool, Dict, Int, List, Tuple class ToolPalette(Widget): tools = List() id_tool_map = Dict() tool_id_to_button_map = Dict() button_size = Tuple((25, 25), Int, Int) is_realized = Bool(False) tool_listeners = Dict() # Maps a button id to its tool id. button_tool_map = Dict() # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, parent, **traits): """ Creates a new tool palette. """ # Base class constructor. super().__init__(**traits) # Create the toolkit-specific control that represents the widget. self.control = self._create_control(parent) return # ------------------------------------------------------------------------ # ToolPalette interface. # ------------------------------------------------------------------------ def add_tool(self, label, bmp, kind, tooltip, longtip): """ Add a tool with the specified properties to the palette. Return an id that can be used to reference this tool in the future. """ return 1 def toggle_tool(self, id, checked): """ Toggle the tool identified by 'id' to the 'checked' state. If the button is a toggle or radio button, the button will be checked if the 'checked' parameter is True; unchecked otherwise. If the button is a standard button, this method is a NOP. """ def enable_tool(self, id, enabled): """ Enable or disable the tool identified by 'id'. """ def on_tool_event(self, id, callback): """ Register a callback for events on the tool identified by 'id'. """ def realize(self): """ Realize the control so that it can be displayed. """ def get_tool_state(self, id): """ Get the toggle state of the tool identified by 'id'. """ state = 0 return state # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _create_control(self, parent): return None pyface-7.4.0/pyface/ui/null/action/action_item.py0000644000076500000240000000644714176222673022737 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 'null' specific implementations of the action manager internal classes. """ from traits.api import Any, Bool, HasTraits class _MenuItem(HasTraits): """ A menu item representation of an action item. """ # '_MenuItem' interface ------------------------------------------------ # Is the item checked? checked = Bool(False) # A controller object we delegate taking actions through (if any). controller = Any() # Is the item enabled? enabled = Bool(True) # Is the item visible? visible = Bool(True) # The radio group we are part of (None if the menu item is not part of such # a group). group = Any() # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, parent, menu, item, controller): """ Creates a new menu item for an action item. """ self.item = item self.control_id = 1 self.control = None if controller is not None: self.controller = controller controller.add_to_menu(self) class _Tool(HasTraits): """ A tool bar tool representation of an action item. """ # '_Tool' interface ---------------------------------------------------- # Is the item checked? checked = Bool(False) # A controller object we delegate taking actions through (if any). controller = Any() # Is the item enabled? enabled = Bool(True) # Is the item visible? visible = Bool(True) # The radio group we are part of (None if the tool is not part of such a # group). group = Any() # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__( self, parent, tool_bar, image_cache, item, controller, show_labels ): """ Creates a new tool bar tool for an action item. """ self.item = item self.tool_bar = tool_bar self.control_id = 1 self.control = None if controller is not None: self.controller = controller controller.add_to_toolbar(self) class _PaletteTool(HasTraits): """ A tool palette representation of an action item. """ # '_PaletteTool' interface --------------------------------------------- # The radio group we are part of (None if the tool is not part of such a # group). group = Any() # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, tool_palette, image_cache, item, show_labels): """ Creates a new tool palette tool for an action item. """ self.item = item self.tool_palette = tool_palette pyface-7.4.0/pyface/ui/null/action/menu_bar_manager.py0000644000076500000240000000244514176222673023720 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 'null' backend specific implementation of a menu bar manager. """ from pyface.action.action_manager import ActionManager class MenuBarManager(ActionManager): """ A menu bar manager realizes itself in errr, a menu bar control. """ # ------------------------------------------------------------------------ # 'MenuBarManager' interface. # ------------------------------------------------------------------------ def create_menu_bar(self, parent, controller=None): """ Creates a menu bar representation of the manager. """ # If a controller is required it can either be set as a trait on the # menu bar manager (the trait is part of the 'ActionManager' API), or # passed in here (if one is passed in here it takes precedence over the # trait). if controller is None: controller = self.controller return None pyface-7.4.0/pyface/ui/null/init.py0000644000076500000240000000163014176222673020117 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # (C) Copyright 2007 Riverbank Computing Limited # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Initialize this backend. """ from pyface.base_toolkit import Toolkit toolkit_object = Toolkit("pyface", "null", "pyface.ui.null") pyface-7.4.0/pyface/ui/null/clipboard.py0000644000076500000240000000410214176222673021110 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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: Evan Patterson # Date: 06/29/09 # ------------------------------------------------------------------------------ from traits.api import provides from pyface.i_clipboard import IClipboard, BaseClipboard @provides(IClipboard) class Clipboard(BaseClipboard): """ A dummy clipboard implementationf for the null backend. """ # --------------------------------------------------------------------------- # 'data' property methods: # --------------------------------------------------------------------------- def _get_has_data(self): return False # --------------------------------------------------------------------------- # 'object_data' property methods: # --------------------------------------------------------------------------- def _get_object_data(self): pass def _set_object_data(self, data): pass def _get_has_object_data(self): return False def _get_object_type(self): return "" # --------------------------------------------------------------------------- # 'text_data' property methods: # --------------------------------------------------------------------------- def _get_text_data(self): return False def _set_text_data(self, data): pass def _get_has_text_data(self): pass # --------------------------------------------------------------------------- # 'file_data' property methods: # --------------------------------------------------------------------------- def _get_file_data(self): pass def _set_file_data(self, data): pass def _get_has_file_data(self): return False pyface-7.4.0/pyface/ui/qt4/0000755000076500000240000000000014176460550016336 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/qt4/python_shell.py0000644000076500000240000005575014176253531021433 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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: Evan Patterson import builtins from code import compile_command, InteractiveInterpreter from io import StringIO import sys from time import time import warnings from pyface.qt import QtCore, QtGui from pygments.lexers import PythonLexer from traits.api import Event, provides from traits.util.clean_strings import python_name from .code_editor.pygments_highlighter import PygmentsHighlighter from .console.api import ( BracketMatcher, CallTipWidget, CompletionLexer, HistoryConsoleWidget, ) from pyface.i_python_shell import IPythonShell, MPythonShell from pyface.key_pressed_event import KeyPressedEvent from .layout_widget import LayoutWidget @provides(IPythonShell) class PythonShell(MPythonShell, LayoutWidget): """ The toolkit specific implementation of a PythonShell. See the IPythonShell interface for the API documentation. """ # 'IPythonShell' interface --------------------------------------------- command_executed = Event() key_pressed = Event(KeyPressedEvent) # -------------------------------------------------------------------------- # 'object' interface # -------------------------------------------------------------------------- # FIXME v3: Either make this API consistent with other Widget sub-classes # or make it a sub-class of HasTraits. def __init__(self, parent=None, **traits): create = traits.pop("create", True) super().__init__(parent=parent, **traits) if create: # Create the toolkit-specific control that represents the widget. self.create() warnings.warn( "automatic widget creation is deprecated and will be removed " "in a future Pyface version, use create=False and explicitly " "call create() for future behaviour", PendingDeprecationWarning, ) # -------------------------------------------------------------------------- # 'IPythonShell' interface # -------------------------------------------------------------------------- def interpreter(self): return self.control.interpreter def execute_command(self, command, hidden=True): self.control.execute(command, hidden=hidden) def execute_file(self, path, hidden=True): self.control.execute_file(path, hidden=hidden) def get_history(self): """ Return the current command history and index. Returns ------- history : list of str The list of commands in the new history. history_index : int from 0 to len(history) The current item in the command history navigation. """ return self.control._history, self.control._history_index def set_history(self, history, history_index): """ Replace the current command history and index with new ones. Parameters ---------- history : list of str The list of commands in the new history. history_index : int The current item in the command history navigation. """ if not 0 <= history_index <= len(history): history_index = len(history) self.control._set_history(history, history_index) # -------------------------------------------------------------------------- # 'IWidget' interface. # -------------------------------------------------------------------------- def _create_control(self, parent): return PyfacePythonWidget(self, parent) def _add_event_listeners(self): super()._add_event_listeners() # Connect signals for events. self.control.executed.connect(self._on_command_executed) self._event_filter.signal.connect(self._on_obj_drop) def _remove_event_listeners(self): if self.control is not None: # Disconnect signals for events. self.control.executed.disconnect(self._on_command_executed) self._event_filter.signal.disconnect(self._on_obj_drop) self.control._remove_event_listeners() super()._remove_event_listeners() def __event_filter_default(self): return _DropEventEmitter(self.control) # -------------------------------------------------------------------------- # 'Private' interface. # -------------------------------------------------------------------------- def _on_obj_drop(self, obj): """ Handle dropped objects and add to interpreter local namespace. """ # If we can't create a valid Python identifier for the name of an # object we use this instead. name = "dragged" if ( hasattr(obj, "name") and isinstance(obj.name, str) and len(obj.name) > 0 ): py_name = python_name(obj.name) # Make sure that the name is actually a valid Python identifier. try: if eval(py_name, {py_name: True}): name = py_name except Exception: pass self.control.interpreter.locals[name] = obj self.control.execute(name) self.control._control.setFocus() class PythonWidget(HistoryConsoleWidget): """ A basic in-process Python interpreter. """ # Emitted when a command has been executed in the interpeter. executed = QtCore.Signal() # -------------------------------------------------------------------------- # 'object' interface # -------------------------------------------------------------------------- def __init__(self, parent=None): super().__init__(parent) # PythonWidget attributes. self.locals = dict(__name__="__console__", __doc__=None) self.interpreter = InteractiveInterpreter(self.locals) # PythonWidget protected attributes. self._buffer = StringIO() self._bracket_matcher = BracketMatcher(self._control) self._call_tip_widget = CallTipWidget(self._control) self._completion_lexer = CompletionLexer(PythonLexer()) self._hidden = False self._highlighter = PythonWidgetHighlighter(self) self._last_refresh_time = 0 # file-like object attributes. self.encoding = sys.stdin.encoding # Configure the ConsoleWidget. self.tab_width = 4 self._set_continuation_prompt("... ") # Configure the CallTipWidget. self._call_tip_widget.setFont(self.font) self.font_changed.connect(self._call_tip_widget.setFont) # Connect signal handlers. document = self._control.document() document.contentsChange.connect(self._document_contents_change) # Display the banner and initial prompt. self.reset() def _remove_event_listeners(self): self.font_changed.disconnect(self._call_tip_widget.setFont) document = self._control.document() document.contentsChange.disconnect(self._document_contents_change) self._bracket_matcher._remove_event_listeners() super()._remove_event_listeners() # -------------------------------------------------------------------------- # file-like object interface # -------------------------------------------------------------------------- def flush(self): """ Flush the buffer by writing its contents to the screen. """ self._buffer.seek(0) text = self._buffer.getvalue() self._buffer.close() self._buffer = StringIO() self._append_plain_text(text) self._control.moveCursor(QtGui.QTextCursor.MoveOperation.End) def readline(self, prompt=None): """ Read and return one line of input from the user. """ return self._readline(prompt) def write(self, text, refresh=True): """ Write text to the buffer, possibly flushing it if 'refresh' is set. """ if not self._hidden: self._buffer.write(text) if refresh: current_time = time() if current_time - self._last_refresh_time > 0.05: self.flush() self._last_refresh_time = current_time def writelines(self, lines, refresh=True): """ Write a list of lines to the buffer. """ for line in lines: self.write(line, refresh=refresh) # --------------------------------------------------------------------------- # 'ConsoleWidget' abstract interface # --------------------------------------------------------------------------- def _is_complete(self, source, interactive): """ Returns whether 'source' can be completely processed and a new prompt created. When triggered by an Enter/Return key press, 'interactive' is True; otherwise, it is False. """ if interactive: lines = source.splitlines() if len(lines) == 1: try: return compile_command(source) is not None except: # We'll let the interpeter handle the error. return True else: return lines[-1].strip() == "" else: return True def _execute(self, source, hidden): """ Execute 'source'. If 'hidden', do not show any output. See parent class :meth:`execute` docstring for full details. """ # Save the current std* and point them here old_stdin = sys.stdin old_stdout = sys.stdout old_stderr = sys.stderr sys.stdin = sys.stdout = sys.stderr = self # Run the source code in the interpeter self._hidden = hidden try: self.interpreter.runsource(source) finally: self._hidden = False # Restore std* unless the executed changed them if sys.stdin is self: sys.stdin = old_stdin if sys.stdout is self: sys.stdout = old_stdout if sys.stderr is self: sys.stderr = old_stderr self.executed.emit() self._show_interpreter_prompt() def _prompt_started_hook(self): """ Called immediately after a new prompt is displayed. """ if not self._reading: self._highlighter.highlighting_on = True def _prompt_finished_hook(self): """ Called immediately after a prompt is finished, i.e. when some input will be processed and a new prompt displayed. """ if not self._reading: self._highlighter.highlighting_on = False def _tab_pressed(self): """ Called when the tab key is pressed. Returns whether to continue processing the event. """ # Perform tab completion if: # 1) The cursor is in the input buffer. # 2) There is a non-whitespace character before the cursor. text = self._get_input_buffer_cursor_line() if text is None: return False complete = bool(text[: self._get_input_buffer_cursor_column()].strip()) if complete: self._complete() return not complete # --------------------------------------------------------------------------- # 'ConsoleWidget' protected interface # --------------------------------------------------------------------------- def _event_filter_console_keypress(self, event): """ Reimplemented for smart backspace. """ if ( event.key() == QtCore.Qt.Key.Key_Backspace and not event.modifiers() & QtCore.Qt.KeyboardModifier.AltModifier ): # Smart backspace: remove four characters in one backspace if: # 1) everything left of the cursor is whitespace # 2) the four characters immediately left of the cursor are spaces col = self._get_input_buffer_cursor_column() cursor = self._control.textCursor() if col > 3 and not cursor.hasSelection(): text = self._get_input_buffer_cursor_line()[:col] if text.endswith(" ") and not text.strip(): cursor.movePosition( QtGui.QTextCursor.MoveOperation.Left, QtGui.QTextCursor.MoveMode.KeepAnchor, 4 ) cursor.removeSelectedText() return True return super()._event_filter_console_keypress(event) def _insert_continuation_prompt(self, cursor): """ Reimplemented for auto-indentation. """ super()._insert_continuation_prompt(cursor) source = self.input_buffer space = 0 for c in source.splitlines()[-1]: if c == "\t": space += 4 elif c == " ": space += 1 else: break if source.rstrip().endswith(":"): space += 4 cursor.insertText(" " * space) # --------------------------------------------------------------------------- # 'PythonWidget' public interface # --------------------------------------------------------------------------- def execute_file(self, path, hidden=False): """ Attempts to execute file with 'path'. If 'hidden', no output is shown. """ self.execute("exec(open(%s).read())" % repr(path), hidden=hidden) def reset(self): """ Resets the widget to its initial state. Similar to ``clear``, but also re-writes the banner. """ self._reading = False self._highlighter.highlighting_on = False self._control.clear() self._append_plain_text(self._get_banner()) self._show_interpreter_prompt() # --------------------------------------------------------------------------- # 'PythonWidget' protected interface # --------------------------------------------------------------------------- def _call_tip(self): """ Shows a call tip, if appropriate, at the current cursor location. """ # Decide if it makes sense to show a call tip cursor = self._get_cursor() cursor.movePosition(QtGui.QTextCursor.MoveOperation.Left) if cursor.document().characterAt(cursor.position()) != "(": return False context = self._get_context(cursor) if not context: return False # Look up the context and show a tip for it symbol, leftover = self._get_symbol_from_context(context) doc = getattr(symbol, "__doc__", None) if doc is not None and not leftover: self._call_tip_widget.show_call_info(doc=doc) return True return False def _complete(self): """ Performs completion at the current cursor location. """ context = self._get_context() if context: symbol, leftover = self._get_symbol_from_context(context) if len(leftover) == 1: leftover = leftover[0] if symbol is None: names = list(self.interpreter.locals.keys()) names += list(builtins.__dict__.keys()) else: names = dir(symbol) completions = [n for n in names if n.startswith(leftover)] if completions: cursor = self._get_cursor() cursor.movePosition( QtGui.QTextCursor.MoveOperation.Left, n=len(context[-1]) ) self._complete_with_items(cursor, completions) def _get_banner(self): """ Gets a banner to display at the beginning of a session. """ banner = ( 'Python %s on %s\nType "help", "copyright", "credits" or ' '"license" for more information.' ) return banner % (sys.version, sys.platform) def _get_context(self, cursor=None): """ Gets the context for the specified cursor (or the current cursor if none is specified). """ if cursor is None: cursor = self._get_cursor() cursor.movePosition( QtGui.QTextCursor.MoveOperation.StartOfBlock, QtGui.QTextCursor.MoveMode.KeepAnchor ) text = cursor.selection().toPlainText() return self._completion_lexer.get_context(text) def _get_symbol_from_context(self, context): """ Find a python object in the interpeter namespace from a context (a list of names). """ context = list(map(str, context)) if len(context) == 0: return None, context base_symbol_string = context[0] symbol = self.interpreter.locals.get(base_symbol_string, None) if symbol is None: symbol = builtins.__dict__.get(base_symbol_string, None) if symbol is None: return None, context context = context[1:] for i, name in enumerate(context): new_symbol = getattr(symbol, name, None) if new_symbol is None: return symbol, context[i:] else: symbol = new_symbol return symbol, [] def _show_interpreter_prompt(self): """ Shows a prompt for the interpreter. """ self.flush() self._show_prompt(">>> ") # Signal handlers ---------------------------------------------------- def _document_contents_change(self, position, removed, added): """ Called whenever the document's content changes. Display a call tip if appropriate. """ # Calculate where the cursor should be *after* the change: position += added if position == self._get_cursor().position(): self._call_tip() # ------------------------------------------------------------------------------- # 'PythonWidgetHighlighter' class: # ------------------------------------------------------------------------------- class PythonWidgetHighlighter(PygmentsHighlighter): """ A PygmentsHighlighter that can be turned on and off and that ignores prompts. """ def __init__(self, python_widget): super().__init__(python_widget._control.document()) self._current_offset = 0 self._python_widget = python_widget self.highlighting_on = False def highlightBlock(self, string): """ Highlight a block of text. Reimplemented to highlight selectively. """ if not self.highlighting_on: return # The input to this function is a unicode string that may contain # paragraph break characters, non-breaking spaces, etc. Here we acquire # the string as plain text so we can compare it. current_block = self.currentBlock() string = self._python_widget._get_block_plain_text(current_block) # Decide whether to check for the regular or continuation prompt. if current_block.contains(self._python_widget._prompt_pos): prompt = self._python_widget._prompt else: prompt = self._python_widget._continuation_prompt # Don't highlight the part of the string that contains the prompt. if string.startswith(prompt): self._current_offset = len(prompt) string = string[len(prompt):] else: self._current_offset = 0 super().highlightBlock(string) def rehighlightBlock(self, block): """ Reimplemented to temporarily enable highlighting if disabled. """ old = self.highlighting_on self.highlighting_on = True super().rehighlightBlock(block) self.highlighting_on = old def setFormat(self, start, count, format): """ Reimplemented to highlight selectively. """ start += self._current_offset super().setFormat(start, count, format) # ------------------------------------------------------------------------------- # 'PyfacePythonWidget' class: # ------------------------------------------------------------------------------- class PyfacePythonWidget(PythonWidget): """ A PythonWidget customized to support the IPythonShell interface. """ # -------------------------------------------------------------------------- # 'object' interface # -------------------------------------------------------------------------- def __init__(self, pyface_widget, *args, **kw): """ Reimplemented to store a reference to the Pyface widget which contains this control. """ self._pyface_widget = pyface_widget super().__init__(*args, **kw) # --------------------------------------------------------------------------- # 'QWidget' interface # --------------------------------------------------------------------------- def keyPressEvent(self, event): """ Reimplemented to generate Pyface key press events. """ # Pyface doesn't seem to be Str aware. Only keep the key code if it # corresponds to a single Latin1 character. kstr = event.text() try: kcode = ord(str(kstr)) except: kcode = 0 mods = event.modifiers() self._pyface_widget.key_pressed = KeyPressedEvent( alt_down=((mods & QtCore.Qt.KeyboardModifier.AltModifier) == QtCore.Qt.KeyboardModifier.AltModifier), control_down=( (mods & QtCore.Qt.KeyboardModifier.ControlModifier) == QtCore.Qt.KeyboardModifier.ControlModifier ), shift_down=( (mods & QtCore.Qt.KeyboardModifier.ShiftModifier) == QtCore.Qt.KeyboardModifier.ShiftModifier ), key_code=kcode, event=event, ) super().keyPressEvent(event) class _DropEventEmitter(QtCore.QObject): """ Handle object drops on widget. """ signal = QtCore.Signal(object) def __init__(self, widget): QtCore.QObject.__init__(self, widget) self.widget = widget widget.setAcceptDrops(True) widget.installEventFilter(self) def eventFilter(self, source, event): """ Handle drop events on widget. """ typ = event.type() if typ == QtCore.QEvent.Type.DragEnter: if hasattr(event.mimeData(), "instance"): # It is pymimedata and has instance data obj = event.mimeData().instance() if obj is not None: event.accept() return True elif typ == QtCore.QEvent.Type.Drop: if hasattr(event.mimeData(), "instance"): # It is pymimedata and has instance data obj = event.mimeData().instance() if obj is not None: self.signal.emit(obj) event.accept() return True return QtCore.QObject.eventFilter(self, source, event) pyface-7.4.0/pyface/ui/qt4/layout_widget.py0000644000076500000240000001031514176253531021567 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 enum import Enum from traits.api import provides from pyface.qt import QtGui from pyface.i_layout_item import DEFAULT_SIZE from pyface.i_layout_widget import ILayoutWidget, MLayoutWidget from pyface.ui.qt4.widget import Widget #: Maximum widget size (some versions of PyQt don't export it) QWIDGETSIZE_MAX = getattr(QtGui, "QWIDGETSIZE_MAX", 1 << 24 - 1) class SizePolicies(Enum): """ Qt values for size policies Note that Qt has additional values that are not mapped to Pyface size policies. """ fixed = QtGui.QSizePolicy.Policy.Fixed preferred = QtGui.QSizePolicy.Policy.Preferred expand = QtGui.QSizePolicy.Policy.Expanding @provides(ILayoutWidget) class LayoutWidget(MLayoutWidget, Widget): """ A widget which can participate as part of a layout. """ def _set_control_minimum_size(self, size): size = tuple( x if x != DEFAULT_SIZE else 0 for x in size ) self.control.setMinimumSize(*size) def _get_control_minimum_size(self): size = self.control.minimumSize() return (size.width(), size.height()) def _set_control_maximum_size(self, size): size = tuple( x if x != DEFAULT_SIZE else QWIDGETSIZE_MAX for x in size ) self.control.setMaximumSize(*size) def _get_control_maximum_size(self): size = self.control.maximumSize() return (size.width(), size.height()) def _set_control_stretch(self, stretch): """ Set the stretch factor of the control. """ new_size_policy = _clone_size_policy(self.control.sizePolicy()) new_size_policy.setHorizontalStretch(stretch[0]) new_size_policy.setVerticalStretch(stretch[1]) self.control.setSizePolicy(new_size_policy) def _get_control_stretch(self): """ Get the stretch factor of the control. This method is only used for testing. """ size_policy = self.control.sizePolicy() return ( size_policy.horizontalStretch(), size_policy.verticalStretch(), ) def _set_control_size_policy(self, size_policy): new_size_policy = _clone_size_policy(self.control.sizePolicy()) if size_policy[0] != "default": new_size_policy.setHorizontalPolicy( SizePolicies[size_policy[0]].value ) if size_policy[1] != "default": new_size_policy.setVerticalPolicy( SizePolicies[size_policy[1]].value ) self.control.setSizePolicy(new_size_policy) def _get_control_size_policy(self): size_policy = self.control.sizePolicy() if self.size_policy[0] != "default": horizontal_policy = SizePolicies( size_policy.horizontalPolicy()).name else: horizontal_policy = "default" if self.size_policy[1] != "default": vertical_policy = SizePolicies( size_policy.verticalPolicy()).name else: vertical_policy = "default" return (horizontal_policy, vertical_policy) def _clone_size_policy(size_policy): """ Clone the state of an existing QSizePolicy object This is required because there is no standard Qt copy or clone method. """ new_size_policy = QtGui.QSizePolicy() new_size_policy.setHorizontalPolicy( size_policy.horizontalPolicy() ) new_size_policy.setVerticalPolicy( size_policy.verticalPolicy() ) new_size_policy.setHorizontalStretch( size_policy.horizontalStretch() ) new_size_policy.setVerticalStretch( size_policy.verticalStretch() ) new_size_policy.setHeightForWidth( size_policy.hasHeightForWidth() ) new_size_policy.setWidthForHeight( size_policy.hasWidthForHeight() ) return new_size_policy pyface-7.4.0/pyface/ui/qt4/tasks/0000755000076500000240000000000014176460551017464 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/qt4/tasks/advanced_editor_area_pane.py0000644000076500000240000006242614176253531025154 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 sys from pyface.qt import QtCore, QtGui, is_pyside, is_qt6 from traits.api import ( Any, Callable, DelegatesTo, Instance, List, observe, provides, Tuple ) from pyface.tasks.i_advanced_editor_area_pane import IAdvancedEditorAreaPane from pyface.tasks.i_editor_area_pane import MEditorAreaPane from .editor_area_pane import EditorAreaDropFilter from .main_window_layout import MainWindowLayout, PaneItem from .task_pane import TaskPane from .util import set_focus # ---------------------------------------------------------------------------- # 'AdvancedEditorAreaPane' class. # ---------------------------------------------------------------------------- @provides(IAdvancedEditorAreaPane) class AdvancedEditorAreaPane(TaskPane, MEditorAreaPane): """ The toolkit-specific implementation of an AdvancedEditorAreaPane. See the IAdvancedEditorAreaPane interface for API documentation. """ # Private interface ---------------------------------------------------- _main_window_layout = Instance(MainWindowLayout) #: A list of connected Qt signals to be removed before destruction. #: First item in the tuple is the Qt signal. The second item is the event #: handler. _connections_to_remove = List(Tuple(Any, Callable)) # ------------------------------------------------------------------------ # 'TaskPane' interface. # ------------------------------------------------------------------------ def create(self, parent): """ Create and set the toolkit-specific control that represents the pane. """ self.control = EditorAreaWidget(self, parent) self._filter = EditorAreaDropFilter(self) self.control.installEventFilter(self._filter) # Add shortcuts for scrolling through tabs. if sys.platform == "darwin": next_seq = "Ctrl+}" prev_seq = "Ctrl+{" else: next_seq = "Ctrl+PgDown" prev_seq = "Ctrl+PgUp" shortcut = QtGui.QShortcut(QtGui.QKeySequence(next_seq), self.control) shortcut.activated.connect(self._next_tab) self._connections_to_remove.append( (shortcut.activated, self._next_tab) ) shortcut = QtGui.QShortcut(QtGui.QKeySequence(prev_seq), self.control) shortcut.activated.connect(self._previous_tab) self._connections_to_remove.append( (shortcut.activated, self._previous_tab) ) # Add shortcuts for switching to a specific tab. mod = "Ctrl+" if sys.platform == "darwin" else "Alt+" mapper = QtCore.QSignalMapper(self.control) if is_pyside and is_qt6: mapper.mappedInt.connect(self._activate_tab) self._connections_to_remove.append( (mapper.mappedInt, self._activate_tab) ) else: mapper.mapped.connect(self._activate_tab) self._connections_to_remove.append( (mapper.mapped, self._activate_tab) ) for i in range(1, 10): sequence = QtGui.QKeySequence(mod + str(i)) shortcut = QtGui.QShortcut(sequence, self.control) shortcut.activated.connect(mapper.map) self._connections_to_remove.append( (shortcut.activated, mapper.map) ) mapper.setMapping(shortcut, i - 1) def destroy(self): """ Destroy the toolkit-specific control that represents the pane. """ self.control.removeEventFilter(self._filter) self.control._remove_event_listeners() self._filter = None for editor in self.editors: editor_widget = editor.control.parent() self.control.destroy_editor_widget(editor_widget) editor.editor_area = None self.active_editor = None while self._connections_to_remove: signal, handler = self._connections_to_remove.pop() signal.disconnect(handler) super().destroy() # ------------------------------------------------------------------------ # 'IEditorAreaPane' interface. # ------------------------------------------------------------------------ def activate_editor(self, editor): """ Activates the specified editor in the pane. """ editor_widget = editor.control.parent() editor_widget.setVisible(True) editor_widget.raise_() editor.control.setFocus() self.active_editor = editor def add_editor(self, editor): """ Adds an editor to the pane. """ editor.editor_area = self editor_widget = EditorWidget(editor, self.control) self.control.add_editor_widget(editor_widget) self.editors.append(editor) def remove_editor(self, editor): """ Removes an editor from the pane. """ editor_widget = editor.control.parent() self.editors.remove(editor) self.control.remove_editor_widget(editor_widget) editor.editor_area = None if not self.editors: self.active_editor = None # ------------------------------------------------------------------------ # 'IAdvancedEditorAreaPane' interface. # ------------------------------------------------------------------------ def get_layout(self): """ Returns a LayoutItem that reflects the current state of the editors. """ return self._main_window_layout.get_layout_for_area( QtCore.Qt.DockWidgetArea.LeftDockWidgetArea ) def set_layout(self, layout): """ Applies a LayoutItem to the editors in the pane. """ if layout is not None: self._main_window_layout.set_layout_for_area( layout, QtCore.Qt.DockWidgetArea.LeftDockWidgetArea ) # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _activate_tab(self, index): """ Activates the tab with the specified index, if there is one. """ widgets = self.control.get_dock_widgets_ordered() if index < len(widgets): self.activate_editor(widgets[index].editor) def _next_tab(self): """ Activate the tab after the currently active tab. """ if self.active_editor: widgets = self.control.get_dock_widgets_ordered() index = widgets.index(self.active_editor.control.parent()) + 1 if index < len(widgets): self.activate_editor(widgets[index].editor) def _previous_tab(self): """ Activate the tab before the currently active tab. """ if self.active_editor: widgets = self.control.get_dock_widgets_ordered() index = widgets.index(self.active_editor.control.parent()) - 1 if index >= 0: self.activate_editor(widgets[index].editor) def _get_label(self, editor): """ Return a tab label for an editor. """ label = editor.name if editor.dirty: label = "*" + label return label # Trait initializers --------------------------------------------------- def __main_window_layout_default(self): return EditorAreaMainWindowLayout(editor_area=self) # Trait change handlers ------------------------------------------------ @observe("editors:items:[dirty, name]") def _update_label(self, event): editor = event.object editor.control.parent().update_title() @observe("editors:items:tooltip") def _update_tooltip(self, event): editor = event.object editor.control.parent().update_tooltip() # ---------------------------------------------------------------------------- # Auxillary classes. # ---------------------------------------------------------------------------- class EditorAreaMainWindowLayout(MainWindowLayout): """ A MainWindowLayout for implementing AdvancedEditorAreaPane. Used for getting and setting layouts for the pane. """ # 'MainWindowLayout' interface ----------------------------------------- control = DelegatesTo("editor_area") # 'TaskWindowLayout' interface ----------------------------------------- editor_area = Instance(AdvancedEditorAreaPane) # ------------------------------------------------------------------------ # 'MainWindowLayout' abstract interface. # ------------------------------------------------------------------------ def _get_dock_widget(self, pane): """ Returns the QDockWidget associated with a PaneItem. """ try: editor = self.editor_area.editors[pane.id] return editor.control.parent() except IndexError: return None def _get_pane(self, dock_widget): """ Returns a PaneItem for a QDockWidget. """ for i, editor in enumerate(self.editor_area.editors): if editor.control == dock_widget.widget(): return PaneItem(id=i) return None class EditorAreaWidget(QtGui.QMainWindow): """ An auxillary widget for implementing AdvancedEditorAreaPane. """ # ------------------------------------------------------------------------ # 'EditorAreaWidget' interface. # ------------------------------------------------------------------------ def __init__(self, editor_area, parent=None): super().__init__(parent) self.editor_area = editor_area self.reset_drag() # Fish out the rubber band used by Qt to indicate a drop region. We use # it to determine which dock widget is the hover widget. for child in self.children(): if isinstance(child, QtGui.QRubberBand): child.installEventFilter(self) self._rubber_band = child break # Monitor focus changes so we can set the active editor. QtGui.QApplication.instance().focusChanged.connect(self._update_active_editor) # Configure the QMainWindow. # FIXME: Currently animation is not supported. self.setAcceptDrops(True) self.setAnimated(False) self.setDockNestingEnabled(True) self.setDocumentMode(True) self.setFocusPolicy(QtCore.Qt.FocusPolicy.StrongFocus) self.setTabPosition( QtCore.Qt.DockWidgetArea.AllDockWidgetAreas, QtGui.QTabWidget.TabPosition.North ) def _remove_event_listeners(self): """ Disconnects focusChanged signal of the application """ app = QtGui.QApplication.instance() app.focusChanged.disconnect(self._update_active_editor) def add_editor_widget(self, editor_widget): """ Adds a dock widget to the editor area. """ editor_widget.installEventFilter(self) self.addDockWidget(QtCore.Qt.DockWidgetArea.LeftDockWidgetArea, editor_widget) # Try to place the editor in a sensible spot. top_left = None for widget in self.get_dock_widgets(): if top_left is None or ( widget.pos().manhattanLength() < top_left.pos().manhattanLength() ): top_left = widget if top_left: self.tabifyDockWidget(top_left, editor_widget) top_left.set_title_bar(False) # Qt will not give the dock widget focus by default. self.editor_area.activate_editor(editor_widget.editor) def destroy_editor_widget(self, editor_widget): """ Destroys a dock widget in the editor area. """ editor_widget.hide() editor_widget.removeEventFilter(self) editor_widget._remove_event_listeners() editor_widget.editor.destroy() self.removeDockWidget(editor_widget) def get_dock_widgets(self): """ Gets all visible dock widgets. """ return [ child for child in self.children() if isinstance(child, QtGui.QDockWidget) and child.isVisible() ] def get_dock_widgets_for_bar(self, tab_bar): """ Get the dock widgets, in order, attached to given tab bar. Because QMainWindow locks this info down, we have resorted to a hack. """ pos = tab_bar.pos() key = lambda w: QtGui.QVector2D(pos - w.pos()).lengthSquared() all_widgets = self.get_dock_widgets() if all_widgets: current = min(all_widgets, key=key) widgets = self.tabifiedDockWidgets(current) widgets.insert(tab_bar.currentIndex(), current) return widgets return [] def get_dock_widgets_ordered(self, visible_only=False): """ Gets all dock widgets in left-to-right, top-to-bottom order. """ children = [] for child in self.children(): if ( child.isWidgetType() and child.isVisible() and ( (isinstance(child, QtGui.QTabBar) and not visible_only) or ( isinstance(child, QtGui.QDockWidget) and ( visible_only or not self.tabifiedDockWidgets(child) ) ) ) ): children.append(child) children.sort(key=lambda _child: (_child.pos().y(), _child.pos().x())) widgets = [] for child in children: if isinstance(child, QtGui.QTabBar): widgets.extend(self.get_dock_widgets_for_bar(child)) else: widgets.append(child) return widgets def remove_editor_widget(self, editor_widget): """ Removes a dock widget from the editor area. """ # Get the tabs in this editor's dock area before removing it. tabified = self.tabifiedDockWidgets(editor_widget) if tabified: widgets = self.get_dock_widgets_ordered() tabified = [ widget for widget in widgets if widget in tabified or widget == editor_widget ] visible = self.get_dock_widgets_ordered(visible_only=True) # Destroy and remove the editor. Get the active widget first, since it # may be destroyed! next_widget = self.editor_area.active_editor.control.parent() self.destroy_editor_widget(editor_widget) # Ensure that the appropriate editor is activated. editor_area = self.editor_area choices = tabified if len(tabified) >= 2 else visible if len(choices) >= 2 and editor_widget == next_widget: i = choices.index(editor_widget) next_widget = ( choices[i + 1] if i + 1 < len(choices) else choices[i - 1] ) editor_area.activate_editor(next_widget.editor) # Update tab bar hide state. if len(tabified) == 2: next_widget.editor.control.parent().set_title_bar(True) if editor_area.hide_tab_bar and len(editor_area.editors) == 1: editor_area.editors[0].control.parent().set_title_bar(False) def reset_drag(self): """ Clear out all drag state. """ self._drag_widget = None self._hover_widget = None self._tear_handled = False self._tear_widgets = [] def set_hover_widget(self, widget): """ Set the dock widget being 'hovered over' during a drag. """ old_widget = self._hover_widget self._hover_widget = widget if old_widget: if old_widget in self._tear_widgets: if len(self._tear_widgets) == 1: old_widget.set_title_bar(True) elif not self.tabifiedDockWidgets(old_widget): old_widget.set_title_bar(True) if widget: if widget in self._tear_widgets: if len(self._tear_widgets) == 1: widget.set_title_bar(False) elif len(self.tabifiedDockWidgets(widget)) == 1: widget.set_title_bar(False) # ------------------------------------------------------------------------ # Event handlers. # ------------------------------------------------------------------------ def childEvent(self, event): """ Reimplemented to gain access to the tab bars as they are created. """ super().childEvent(event) if event.polished(): child = event.child() if isinstance(child, QtGui.QTabBar): # Use UniqueConnections since Qt recycles the tab bars. child.installEventFilter(self) child.currentChanged.connect( self._update_editor_in_focus, QtCore.Qt.ConnectionType.UniqueConnection ) child.setTabsClosable(True) child.setUsesScrollButtons(True) child.tabCloseRequested.connect( self._tab_close_requested, QtCore.Qt.ConnectionType.UniqueConnection ) # FIXME: We would like to have the tabs movable, but this # confuses the QMainWindowLayout. For now, we disable this. # child.setMovable(True) def eventFilter(self, obj, event): """ Reimplemented to dispatch to sub-handlers. """ if isinstance(obj, QtGui.QDockWidget): return self._filter_dock_widget(obj, event) elif isinstance(obj, QtGui.QRubberBand): return self._filter_rubber_band(obj, event) elif isinstance(obj, QtGui.QTabBar): return self._filter_tab_bar(obj, event) return False def _filter_dock_widget(self, widget, event): """ Support hover widget state tracking. """ if self._drag_widget and event.type() == QtCore.QEvent.Type.Resize: if widget.geometry() == self._rubber_band.geometry(): self.set_hover_widget(widget) elif ( self._drag_widget == widget and event.type() == QtCore.QEvent.Type.Move ): if len(self._tear_widgets) == 1 and not self._tear_handled: widget = self._tear_widgets[0] widget.set_title_bar(True) self._tear_handled = True elif ( self._drag_widget == widget and event.type() == QtCore.QEvent.Type.MouseButtonRelease ): self.reset_drag() return False def _filter_rubber_band(self, rubber_band, event): """ Support hover widget state tracking. """ if self._drag_widget and event.type() in ( QtCore.QEvent.Type.Resize, QtCore.QEvent.Type.Move, ): self.set_hover_widget(None) return False def _filter_tab_bar(self, tab_bar, event): """ Support 'tearing off' a tab. """ if event.type() == QtCore.QEvent.Type.MouseMove: if tab_bar.rect().contains(event.pos()): self.reset_drag() else: if not self._drag_widget: index = tab_bar.currentIndex() self._tear_widgets = self.get_dock_widgets_for_bar(tab_bar) self._drag_widget = widget = self._tear_widgets.pop(index) pos = QtCore.QPoint(0, 0) press_event = QtGui.QMouseEvent( QtCore.QEvent.Type.MouseButtonPress, pos, widget.mapToGlobal(pos), QtCore.Qt.MouseButton.LeftButton, QtCore.Qt.MouseButton.LeftButton, event.modifiers(), ) QtCore.QCoreApplication.sendEvent(widget, press_event) return True event = QtGui.QMouseEvent( QtCore.QEvent.Type.MouseMove, event.pos(), event.globalPos(), event.button(), event.buttons(), event.modifiers(), ) QtCore.QCoreApplication.sendEvent(self._drag_widget, event) return True elif event.type() == QtCore.QEvent.Type.ToolTip: # QDockAreaLayout forces the tooltips to be QDockWidget.windowTitle, # so we provide the tooltips manually. widgets = self.get_dock_widgets_for_bar(tab_bar) index = tab_bar.tabAt(event.pos()) tooltip = widgets[index].editor.tooltip if index >= 0 else "" if tooltip: QtGui.QToolTip.showText(event.globalPos(), tooltip, tab_bar) return True return False def focusInEvent(self, event): """ Assign focus to the active editor, if possible. """ active_editor = self.editor_area.active_editor if active_editor: set_focus(active_editor.control) # ------------------------------------------------------------------------ # Signal handlers. # ------------------------------------------------------------------------ def _update_active_editor(self, old, new): """ Handle an application-level focus change. """ if new is not None: for editor in self.editor_area.editors: control = editor.control if control is not None and control.isAncestorOf(new): self.editor_area.active_editor = editor break else: if not self.editor_area.editors: self.editor_area.active_editor = None def _update_editor_in_focus(self, index): """ Handle a tab selection. """ widgets = self.get_dock_widgets_for_bar(self.sender()) if index < len(widgets): editor_widget = widgets[index] editor_widget.editor.control.setFocus() def _tab_close_requested(self, index): """ Handle a tab close request. """ editor_widget = self.get_dock_widgets_for_bar(self.sender())[index] editor_widget.editor.close() class EditorWidget(QtGui.QDockWidget): """ An auxillary widget for implementing AdvancedEditorAreaPane. """ def __init__(self, editor, parent=None): super().__init__(parent) self.editor = editor self.editor.create(self) self.setAllowedAreas(QtCore.Qt.DockWidgetArea.LeftDockWidgetArea) self.setFeatures( QtGui.QDockWidget.DockWidgetFeature.DockWidgetClosable | QtGui.QDockWidget.DockWidgetFeature.DockWidgetMovable ) self.setWidget(editor.control) self.update_title() # Update the minimum size. contents_minsize = editor.control.minimumSize() style = self.style() contents_minsize.setHeight( contents_minsize.height() + style.pixelMetric(style.PM_DockWidgetHandleExtent) ) self.setMinimumSize(contents_minsize) self.dockLocationChanged.connect(self.update_title_bar) self.visibilityChanged.connect(self.update_title_bar) def _remove_event_listeners(self): self.dockLocationChanged.disconnect(self.update_title_bar) self.visibilityChanged.disconnect(self.update_title_bar) def update_title(self): title = self.editor.editor_area._get_label(self.editor) self.setWindowTitle(title) title_bar = self.titleBarWidget() if isinstance(title_bar, EditorTitleBarWidget): title_bar.setTabText(0, title) def update_tooltip(self): title_bar = self.titleBarWidget() if isinstance(title_bar, EditorTitleBarWidget): title_bar.setTabToolTip(0, self.editor.tooltip) def update_title_bar(self): if self not in self.parent()._tear_widgets: tabbed = self.parent().tabifiedDockWidgets(self) self.set_title_bar(not tabbed) def set_title_bar(self, title_bar): current = self.titleBarWidget() editor_area = self.editor.editor_area if ( title_bar and editor_area and (not editor_area.hide_tab_bar or len(editor_area.editors) > 1) ): if not isinstance(current, EditorTitleBarWidget): self.setTitleBarWidget(EditorTitleBarWidget(self)) elif current is None or isinstance(current, EditorTitleBarWidget): self.setTitleBarWidget(QtGui.QWidget()) class EditorTitleBarWidget(QtGui.QTabBar): """ An auxillary widget for implementing AdvancedEditorAreaPane. """ def __init__(self, editor_widget): super().__init__(editor_widget) self.addTab(editor_widget.windowTitle()) self.setTabToolTip(0, editor_widget.editor.tooltip) self.setDocumentMode(True) self.setExpanding(False) self.setTabsClosable(True) self.tabCloseRequested.connect(editor_widget.editor.close) def mousePressEvent(self, event): self.parent().parent()._drag_widget = self.parent() event.ignore() def mouseMoveEvent(self, event): event.ignore() def mouseReleaseEvent(self, event): event.ignore() pyface-7.4.0/pyface/ui/qt4/tasks/task_window_backend.py0000644000076500000240000001604314176253531024040 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 traits.api import Instance, List from pyface.tasks.i_task_window_backend import MTaskWindowBackend from pyface.tasks.task_layout import PaneItem, TaskLayout from .dock_pane import AREA_MAP, INVERSE_AREA_MAP from .main_window_layout import MainWindowLayout # Constants. CORNER_MAP = { "top_left": QtCore.Qt.Corner.TopLeftCorner, "top_right": QtCore.Qt.Corner.TopRightCorner, "bottom_left": QtCore.Qt.Corner.BottomLeftCorner, "bottom_right": QtCore.Qt.Corner.BottomRightCorner, } class TaskWindowBackend(MTaskWindowBackend): """ The toolkit-specific implementation of a TaskWindowBackend. See the ITaskWindowBackend interface for API documentation. """ # Private interface ---------------------------------------------------- _main_window_layout = Instance(MainWindowLayout) # ------------------------------------------------------------------------ # 'ITaskWindowBackend' interface. # ------------------------------------------------------------------------ def create_contents(self, parent): """ Create and return the TaskWindow's contents. """ app = QtGui.QApplication.instance() app.focusChanged.connect(self._focus_changed_signal) return QtGui.QStackedWidget(parent) def destroy(self): """ Destroy the backend. """ app = QtGui.QApplication.instance() app.focusChanged.disconnect(self._focus_changed_signal) # signal to layout we don't need it any more self._main_window_layout.control = None def hide_task(self, state): """ Assuming the specified TaskState is active, hide its controls. """ # Save the task's layout in case it is shown again later. self.window._active_state.layout = self.get_layout() # Now hide its controls. self.control.centralWidget().removeWidget(state.central_pane.control) for dock_pane in state.dock_panes: # Warning: The layout behavior is subtly different (and wrong!) if # the order of these two statement is switched. dock_pane.control.hide() self.control.removeDockWidget(dock_pane.control) def show_task(self, state): """ Assuming no task is currently active, show the controls of the specified TaskState. """ # Show the central pane. self.control.centralWidget().addWidget(state.central_pane.control) # Show the dock panes. self._layout_state(state) # Methods for saving and restoring the layout -------------------------# def get_layout(self): """ Returns a TaskLayout for the current state of the window. """ # Extract the layout from the main window. layout = TaskLayout(id=self.window._active_state.task.id) self._main_window_layout.state = self.window._active_state self._main_window_layout.get_layout(layout) # Extract the window's corner configuration. for name, corner in CORNER_MAP.items(): area = INVERSE_AREA_MAP[int(self.control.corner(corner))] setattr(layout, name + "_corner", area) return layout def set_layout(self, layout): """ Applies a TaskLayout (which should be suitable for the active task) to the window. """ self.window._active_state.layout = layout self._layout_state(self.window._active_state) # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _layout_state(self, state): """ Layout the dock panes in the specified TaskState using its TaskLayout. """ # Assign the window's corners to the appropriate dock areas. for name, corner in CORNER_MAP.items(): area = getattr(state.layout, name + "_corner") self.control.setCorner(corner, AREA_MAP[area]) # Add all panes in the TaskLayout. self._main_window_layout.state = state self._main_window_layout.set_layout(state.layout) # Add all panes not assigned an area by the TaskLayout. for dock_pane in state.dock_panes: if dock_pane.control not in self._main_window_layout.consumed: dock_area = AREA_MAP[dock_pane.dock_area] self.control.addDockWidget(dock_area, dock_pane.control) # By default, these panes are not visible. However, if a pane # has been explicitly set as visible, honor that setting. if dock_pane.visible: dock_pane.control.show() # Trait initializers --------------------------------------------------- def __main_window_layout_default(self): return TaskWindowLayout(control=self.control) # Signal handlers -----------------------------------------------------# def _focus_changed_signal(self, old, new): if self.window.active_task: panes = [self.window.central_pane] + self.window.dock_panes for pane in panes: if new and pane.control.isAncestorOf(new): pane.has_focus = True elif old and pane.control.isAncestorOf(old): pane.has_focus = False class TaskWindowLayout(MainWindowLayout): """ A MainWindowLayout for a TaskWindow. """ # 'TaskWindowLayout' interface ----------------------------------------- consumed = List() state = Instance("pyface.tasks.task_window.TaskState") # ------------------------------------------------------------------------ # 'MainWindowLayout' interface. # ------------------------------------------------------------------------ def set_layout(self, layout): """ Applies a DockLayout to the window. """ self.consumed = [] super().set_layout(layout) # ------------------------------------------------------------------------ # 'MainWindowLayout' abstract interface. # ------------------------------------------------------------------------ def _get_dock_widget(self, pane): """ Returns the QDockWidget associated with a PaneItem. """ for dock_pane in self.state.dock_panes: if dock_pane.id == pane.id: self.consumed.append(dock_pane.control) return dock_pane.control return None def _get_pane(self, dock_widget): """ Returns a PaneItem for a QDockWidget. """ for dock_pane in self.state.dock_panes: if dock_pane.control == dock_widget: return PaneItem(id=dock_pane.id) return None pyface-7.4.0/pyface/ui/qt4/tasks/editor_area_pane.py0000644000076500000240000002400114176253531023312 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 sys from pyface.tasks.i_editor_area_pane import IEditorAreaPane, MEditorAreaPane from traits.api import Any, Callable, List, observe, provides, Tuple from pyface.qt import QtCore, QtGui, is_qt6, is_pyside from .task_pane import TaskPane from .util import set_focus # ---------------------------------------------------------------------------- # 'EditorAreaPane' class. # ---------------------------------------------------------------------------- @provides(IEditorAreaPane) class EditorAreaPane(TaskPane, MEditorAreaPane): """ The toolkit-specific implementation of a EditorAreaPane. See the IEditorAreaPane interface for API documentation. """ # Private interface ---------------------------------------------------# #: A list of connected Qt signals to be removed before destruction. #: First item in the tuple is the Qt signal. The second item is the event #: handler. _connections_to_remove = List(Tuple(Any, Callable)) # ------------------------------------------------------------------------ # 'TaskPane' interface. # ------------------------------------------------------------------------ def create(self, parent): """ Create and set the toolkit-specific control that represents the pane. """ # Create and configure the tab widget. self.control = control = EditorAreaWidget(self, parent) self._filter = EditorAreaDropFilter(self) control.installEventFilter(self._filter) control.tabBar().setVisible(not self.hide_tab_bar) # Connect to the widget's signals. control.currentChanged.connect(self._update_active_editor) self._connections_to_remove.append( (control.currentChanged, self._update_active_editor) ) control.tabCloseRequested.connect(self._close_requested) self._connections_to_remove.append( (control.tabCloseRequested, self._close_requested) ) # Add shortcuts for scrolling through tabs. if sys.platform == "darwin": next_seq = "Ctrl+}" prev_seq = "Ctrl+{" else: next_seq = "Ctrl+PgDown" prev_seq = "Ctrl+PgUp" shortcut = QtGui.QShortcut(QtGui.QKeySequence(next_seq), self.control) shortcut.activated.connect(self._next_tab) self._connections_to_remove.append( (shortcut.activated, self._next_tab) ) shortcut = QtGui.QShortcut(QtGui.QKeySequence(prev_seq), self.control) shortcut.activated.connect(self._previous_tab) self._connections_to_remove.append( (shortcut.activated, self._previous_tab) ) # Add shortcuts for switching to a specific tab. mod = "Ctrl+" if sys.platform == "darwin" else "Alt+" mapper = QtCore.QSignalMapper(self.control) if is_pyside and is_qt6: mapper.mappedInt.connect(self.control.setCurrentIndex) self._connections_to_remove.append( (mapper.mappedInt, self.control.setCurrentIndex) ) else: mapper.mapped.connect(self.control.setCurrentIndex) self._connections_to_remove.append( (mapper.mapped, self.control.setCurrentIndex) ) for i in range(1, 10): sequence = QtGui.QKeySequence(mod + str(i)) shortcut = QtGui.QShortcut(sequence, self.control) shortcut.activated.connect(mapper.map) self._connections_to_remove.append( (shortcut.activated, mapper.map) ) mapper.setMapping(shortcut, i - 1) def destroy(self): """ Destroy the toolkit-specific control that represents the pane. """ self.control.removeEventFilter(self._filter) self._filter = None for editor in self.editors: self.remove_editor(editor) while self._connections_to_remove: signal, handler = self._connections_to_remove.pop() signal.disconnect(handler) super().destroy() # ------------------------------------------------------------------------ # 'IEditorAreaPane' interface. # ------------------------------------------------------------------------ def activate_editor(self, editor): """ Activates the specified editor in the pane. """ self.control.setCurrentWidget(editor.control) def add_editor(self, editor): """ Adds an editor to the pane. """ editor.editor_area = self editor.create(self.control) index = self.control.addTab(editor.control, self._get_label(editor)) self.control.setTabToolTip(index, editor.tooltip) self.editors.append(editor) self._update_tab_bar(event=None) # The 'currentChanged' signal, used below, is not emitted when the first # editor is added. if len(self.editors) == 1: self.active_editor = editor def remove_editor(self, editor): """ Removes an editor from the pane. """ self.editors.remove(editor) self.control.removeTab(self.control.indexOf(editor.control)) editor.destroy() editor.editor_area = None self._update_tab_bar(event=None) if not self.editors: self.active_editor = None # ------------------------------------------------------------------------ # Protected interface. # ------------------------------------------------------------------------ def _get_label(self, editor): """ Return a tab label for an editor. """ label = editor.name if editor.dirty: label = "*" + label return label def _get_editor_with_control(self, control): """ Return the editor with the specified control. """ for editor in self.editors: if editor.control == control: return editor return None def _next_tab(self): """ Activate the tab after the currently active tab. """ self.control.setCurrentIndex(self.control.currentIndex() + 1) def _previous_tab(self): """ Activate the tab before the currently active tab. """ self.control.setCurrentIndex(self.control.currentIndex() - 1) # Trait change handlers ------------------------------------------------ @observe("editors:items:[dirty, name]") def _update_label(self, event): editor = event.object index = self.control.indexOf(editor.control) self.control.setTabText(index, self._get_label(editor)) @observe("editors:items:tooltip") def _update_tooltip(self, event): editor = event.object index = self.control.indexOf(editor.control) self.control.setTabToolTip(index, editor.tooltip) # Signal handlers -----------------------------------------------------# def _close_requested(self, index): control = self.control.widget(index) editor = self._get_editor_with_control(control) editor.close() def _update_active_editor(self): index = self.control.currentIndex() if index == -1: self.active_editor = None else: control = self.control.widget(index) self.active_editor = self._get_editor_with_control(control) @observe("hide_tab_bar") def _update_tab_bar(self, event): if self.control is not None: visible = self.control.count() > 1 if self.hide_tab_bar else True self.control.tabBar().setVisible(visible) # ---------------------------------------------------------------------------- # Auxillary classes. # ---------------------------------------------------------------------------- class EditorAreaWidget(QtGui.QTabWidget): """ An auxillary widget for implementing AdvancedEditorAreaPane. """ def __init__(self, editor_area, parent=None): super().__init__(parent) self.editor_area = editor_area # Configure the QTabWidget. self.setAcceptDrops(True) self.setDocumentMode(True) self.setFocusPolicy(QtCore.Qt.FocusPolicy.StrongFocus) self.setFocusProxy(None) self.setMovable(True) self.setTabsClosable(True) self.setUsesScrollButtons(True) def focusInEvent(self, event): """ Assign focus to the active editor, if possible. """ active_editor = self.editor_area.active_editor if active_editor: set_focus(active_editor.control) class EditorAreaDropFilter(QtCore.QObject): """ Implements drag and drop support. """ def __init__(self, editor_area): super().__init__() self.editor_area = editor_area def eventFilter(self, object, event): """ Handle drag and drop events with MIME type 'text/uri-list'. """ if event.type() in (QtCore.QEvent.Type.DragEnter, QtCore.QEvent.Type.Drop): # Build list of accepted files. extensions = tuple(self.editor_area.file_drop_extensions) file_paths = [] for url in event.mimeData().urls(): file_path = url.toLocalFile() if file_path.endswith(extensions): file_paths.append(file_path) # Accept the event if we have at least one accepted file. if event.type() == QtCore.QEvent.Type.DragEnter: if file_paths: event.acceptProposedAction() # Dispatch the events. elif event.type() == QtCore.QEvent.Type.Drop: for file_path in file_paths: self.editor_area.file_dropped = file_path return True return super().eventFilter(object, event) pyface-7.4.0/pyface/ui/qt4/tasks/split_editor_area_pane.py0000644000076500000240000012200014176253531024523 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 sys from pyface.tasks.i_editor_area_pane import IEditorAreaPane, MEditorAreaPane from traits.api import ( Any, Bool, cached_property, Callable, Dict, Instance, List, observe, Property, provides, Str, Tuple, ) from pyface.qt import is_qt4, QtCore, QtGui, is_qt6, is_pyside from pyface.action.api import Action, Group, MenuManager from pyface.tasks.task_layout import PaneItem, Tabbed, Splitter from pyface.mimedata import PyMimeData from pyface.api import FileDialog from pyface.constant import OK from pyface.drop_handler import IDropHandler, BaseDropHandler, FileDropHandler from .task_pane import TaskPane # ---------------------------------------------------------------------------- # 'SplitEditorAreaPane' class. # ---------------------------------------------------------------------------- @provides(IEditorAreaPane) class SplitEditorAreaPane(TaskPane, MEditorAreaPane): """ The toolkit-specific implementation of an SplitEditorAreaPane. See the IEditorAreaPane interface for API documentation. """ # SplitEditorAreaPane interface ------------------------------------- # Currently active tabwidget active_tabwidget = Instance(QtGui.QTabWidget) # list of installed drop handlers drop_handlers = List(IDropHandler) # Additional callback functions. Few useful callbacks that can be included: # 'new': new file action (takes no argument) # 'open': open file action (takes file_path as single argument) # 'open_dialog': show the open file dialog (responsibility of the callback, # takes no argument), overrides 'open' callback # They are used to create shortcut buttons for these actions in the empty # pane that gets created when the user makes a split callbacks = Dict({}, key=Str, value=Callable) # The constructor of the empty widget which comes up when one creates a split create_empty_widget = Callable # Private interface --------------------------------------------------- #: A list of connected Qt signals to be removed before destruction. #: First item in the tuple is the Qt signal. The second item is the event #: handler. _connections_to_remove = List(Tuple(Any, Callable)) _private_drop_handlers = List(IDropHandler) _all_drop_handlers = Property( List(IDropHandler), observe=["drop_handlers", "_private_drop_handlers"], ) def __private_drop_handlers_default(self): """ By default, two private drop handlers are installed: 1. For dropping of tabs from one pane to other 2. For dropping of supported files from file-browser pane or outside the application """ return [ TabDropHandler(), FileDropHandler( extensions=self.file_drop_extensions, open_file=lambda path: self.trait_set(file_dropped=path), ), ] @cached_property def _get__all_drop_handlers(self): return self.drop_handlers + self._private_drop_handlers def _create_empty_widget_default(self): return lambda: self.active_tabwidget.create_empty_widget() # ------------------------------------------------------------------------ # 'TaskPane' interface. # ------------------------------------------------------------------------ def create(self, parent): """ Create and set the toolkit-specific control that represents the pane. """ # Create and configure the Editor Area Widget. self.control = EditorAreaWidget(self, parent) self.active_tabwidget = self.control.tabwidget() # handle application level focus changes QtGui.QApplication.instance().focusChanged.connect(self._focus_changed) # set key bindings self.set_key_bindings() def destroy(self): """ Destroy the toolkit-specific control that represents the pane. """ # disconnect application level focus change signals first, else it gives # weird runtime errors trying to access non-existent objects QtGui.QApplication.instance().focusChanged.disconnect( self._focus_changed ) for editor in self.editors[:]: self.remove_editor(editor) while self._connections_to_remove: signal, handler = self._connections_to_remove.pop() signal.disconnect(handler) # Remove reference to active tabwidget so that it can be deleted # together with the main control self.active_tabwidget = None super().destroy() # ------------------------------------------------------------------------ # 'IEditorAreaPane' interface. # ------------------------------------------------------------------------ def activate_editor(self, editor): """ Activates the specified editor in the pane. """ active_tabwidget = self._get_editor_tabwidget(editor) active_tabwidget.setCurrentWidget(editor.control) self.active_tabwidget = active_tabwidget editor_widget = editor.control.parent() editor_widget.setVisible(True) editor_widget.raise_() # Set focus on last active widget in editor if possible if editor.control.focusWidget(): editor.control.focusWidget().setFocus() else: editor.control.setFocus() # Set active_editor at the end of the method so that the notification # occurs when everything is ready. self.active_editor = editor def add_editor(self, editor): """ Adds an editor to the active_tabwidget """ editor.editor_area = self editor.create(self.active_tabwidget) index = self.active_tabwidget.addTab( editor.control, self._get_label(editor) ) # There seem to be a bug in pyside or qt, where the index is set to 1 # when you create the first tab. This is a hack to fix it. if self.active_tabwidget.count() == 1: index = 0 self.active_tabwidget.setTabToolTip(index, editor.tooltip) self.editors.append(editor) def remove_editor(self, editor): """ Removes an editor from the associated tabwidget """ tabwidget, index = self._get_editor_tabwidget_index(editor) tabwidget.removeTab(index) self.editors.remove(editor) editor.destroy() editor.editor_area = None if not self.editors: self.active_editor = None # ------------------------------------------------------------------------ # 'IAdvancedEditorAreaPane' interface. # ------------------------------------------------------------------------ def get_layout(self): """ Returns a LayoutItem that reflects the current state of the tabwidgets in the split framework. """ return self.control.get_layout() def set_layout(self, layout): """ Applies the given LayoutItem. """ self.control.set_layout(layout) # ------------------------------------------------------------------------ # 'SplitEditorAreaPane' interface. # ------------------------------------------------------------------------ def get_context_menu(self, pos): """ Return a context menu containing split/collapse actions. Parameters ---------- pos : QtCore.QPoint Mouse position in global coordinates for which the context menu was requested. Returns ------- menu : pyface.action.menu_manager.MenuManager or None Context menu, or None if the given position doesn't correspond to any of the tab widgets. """ menu = MenuManager() for tabwidget in self.tabwidgets(): widget_pos = tabwidget.mapFromGlobal(pos) if tabwidget.rect().contains(widget_pos): splitter = tabwidget.parent() break else: # no split/collapse context menu for positions outside any # tabwidget region return None # add split actions (only show for non-empty tabwidgets) if not splitter.is_empty(): split_group = Group( Action( id="split_hor", name="Create new pane to the right", on_perform=lambda: splitter.split( orientation=QtCore.Qt.Orientation.Horizontal ), ), Action( id="split_ver", name="Create new pane below", on_perform=lambda: splitter.split( orientation=QtCore.Qt.Orientation.Vertical ), ), id="split", ) menu.append(split_group) # add collapse action (only show for collapsible splitters) if splitter.is_collapsible(): if splitter is splitter.parent().leftchild: if splitter.parent().orientation() == QtCore.Qt.Orientation.Horizontal: text = "Merge with right pane" else: text = "Merge with bottom pane" else: if splitter.parent().orientation() == QtCore.Qt.Orientation.Horizontal: text = "Merge with left pane" else: text = "Merge with top pane" collapse_group = Group( Action( id="merge", name=text, on_perform=lambda: splitter.collapse(), ), id="collapse", ) menu.append(collapse_group) return menu # ------------------------------------------------------------------------ # Protected interface. # ------------------------------------------------------------------------ def _get_label(self, editor): """ Return a tab label for an editor. """ try: label = editor.name if editor.dirty: label = "*" + label except AttributeError: label = "" return label def _get_editor(self, editor_widget): """ Returns the editor corresponding to editor_widget """ for editor in self.editors: if editor.control is editor_widget: return editor return None def set_key_bindings(self): """ Set keyboard shortcuts for tabbed navigation """ # Add shortcuts for scrolling through tabs. if sys.platform == "darwin": next_seq = "Ctrl+}" prev_seq = "Ctrl+{" else: next_seq = "Ctrl+PgDown" prev_seq = "Ctrl+PgUp" shortcut = QtGui.QShortcut(QtGui.QKeySequence(next_seq), self.control) shortcut.activated.connect(self._next_tab) self._connections_to_remove.append( (shortcut.activated, self._next_tab) ) shortcut = QtGui.QShortcut(QtGui.QKeySequence(prev_seq), self.control) shortcut.activated.connect(self._previous_tab) self._connections_to_remove.append( (shortcut.activated, self._previous_tab) ) # Add shortcuts for switching to a specific tab. mod = "Ctrl+" if sys.platform == "darwin" else "Alt+" mapper = QtCore.QSignalMapper(self.control) if is_pyside and is_qt6: mapper.mappedInt.connect(self._activate_tab) self._connections_to_remove.append( (mapper.mappedInt, self._activate_tab) ) else: mapper.mapped.connect(self._activate_tab) self._connections_to_remove.append( (mapper.mapped, self._activate_tab) ) for i in range(1, 10): sequence = QtGui.QKeySequence(mod + str(i)) shortcut = QtGui.QShortcut(sequence, self.control) shortcut.activated.connect(mapper.map) self._connections_to_remove.append( (shortcut.activated, mapper.map) ) mapper.setMapping(shortcut, i - 1) def _activate_tab(self, index): """ Activates the tab with the specified index, if there is one. """ self.active_tabwidget.setCurrentIndex(index) current_widget = self.active_tabwidget.currentWidget() for editor in self.editors: if current_widget == editor.control: self.activate_editor(editor) def _next_tab(self): """ Activate the tab after the currently active tab. """ index = self.active_tabwidget.currentIndex() new_index = ( index + 1 if index < self.active_tabwidget.count() - 1 else 0 ) self._activate_tab(new_index) def _previous_tab(self): """ Activate the tab before the currently active tab. """ index = self.active_tabwidget.currentIndex() new_index = ( index - 1 if index > 0 else self.active_tabwidget.count() - 1 ) self._activate_tab(new_index) def tabwidgets(self): """ Returns the list of tabwidgets associated with the current editor area. """ return self.control.tabwidgets() def _get_editor_tabwidget(self, editor): """ Given an editor, return its tabwidget. """ return editor.control.parent().parent() def _get_editor_tabwidget_index(self, editor): """ Given an editor, return its tabwidget and index. """ tabwidget = self._get_editor_tabwidget(editor) index = tabwidget.indexOf(editor.control) return tabwidget, index # Trait change handlers ------------------------------------------------ @observe("editors:items:[dirty, name]") def _update_label(self, event): editor = event.object tabwidget, index = self._get_editor_tabwidget_index(editor) tabwidget.setTabText(index, self._get_label(editor)) @observe("editors:items:tooltip") def _update_tooltip(self, event): editor = event.object tabwidget, index = self._get_editor_tabwidget_index(editor) tabwidget.setTabToolTip(index, editor.tooltip) # Signal handlers -----------------------------------------------------# def _find_ancestor_draggable_tab_widget(self, control): """ Find the draggable tab widget to which a widget belongs. """ while not isinstance(control, DraggableTabWidget): control = control.parent() return control def _focus_changed(self, old, new): """Set the active tabwidget after an application-level change in focus. """ if new is not None: if isinstance(new, DraggableTabWidget): if new.editor_area == self: self.active_tabwidget = new elif isinstance(new, QtGui.QTabBar): if self.control.isAncestorOf(new): self.active_tabwidget = self._find_ancestor_draggable_tab_widget( new ) else: # Check if any of the editor widgets have focus. # If so, make it active. for editor in self.editors: control = editor.control if control is not None and control.isAncestorOf(new): active_tabwidget = self._find_ancestor_draggable_tab_widget( control ) active_tabwidget.setCurrentWidget(control) self.active_tabwidget = active_tabwidget break def _active_tabwidget_changed(self, new): """Set the active editor whenever the active tabwidget updates. """ if new is None or new.parent().is_empty(): active_editor = None else: active_editor = self._get_editor(new.currentWidget()) self.active_editor = active_editor # ---------------------------------------------------------------------------- # Auxiliary classes. # ---------------------------------------------------------------------------- class EditorAreaWidget(QtGui.QSplitter): """ Container widget to hold a QTabWidget which are separated by other QTabWidgets via splitters. An EditorAreaWidget is essentially a Node object in the editor area layout tree. """ def __init__(self, editor_area, parent=None, tabwidget=None): """ Creates an EditorAreaWidget object. editor_area : global SplitEditorAreaPane instance parent : parent splitter tabwidget : tabwidget object contained by this splitter """ super().__init__(parent=parent) self.editor_area = editor_area if not tabwidget: tabwidget = DraggableTabWidget( editor_area=self.editor_area, parent=self ) # add the tabwidget to the splitter self.addWidget(tabwidget) # showing the tabwidget after reparenting tabwidget.show() # Initializes left and right children to None (since no initial splitter # children are present) self.leftchild = None self.rightchild = None def get_layout(self): """ Returns a LayoutItem that reflects the layout of the current splitter. """ ORIENTATION_MAP = { QtCore.Qt.Orientation.Horizontal: "horizontal", QtCore.Qt.Orientation.Vertical: "vertical", } # obtain layout based on children layouts if not self.is_leaf(): layout = Splitter( self.leftchild.get_layout(), self.rightchild.get_layout(), orientation=ORIENTATION_MAP[self.orientation()], ) # obtain the Tabbed layout else: if self.is_empty(): layout = Tabbed( PaneItem(id=-1, width=self.width(), height=self.height()), active_tab=0, ) else: items = [] for i in range(self.tabwidget().count()): widget = self.tabwidget().widget(i) # mark identification for empty_widget editor = self.editor_area._get_editor(widget) item_id = self.editor_area.editors.index(editor) item_width = self.width() item_height = self.height() items.append( PaneItem( id=item_id, width=item_width, height=item_height ) ) layout = Tabbed( *items, active_tab=self.tabwidget().currentIndex() ) return layout def set_layout(self, layout): """ Applies the given LayoutItem to current splitter. """ ORIENTATION_MAP = { "horizontal": QtCore.Qt.Orientation.Horizontal, "vertical": QtCore.Qt.Orientation.Vertical, } # if not a leaf splitter if isinstance(layout, Splitter): self.split(orientation=ORIENTATION_MAP[layout.orientation]) self.leftchild.set_layout(layout=layout.items[0]) self.rightchild.set_layout(layout=layout.items[1]) # setting sizes of children along splitter direction if layout.orientation == "horizontal": sizes = [self.leftchild.width(), self.rightchild.width()] self.resize(sum(sizes), self.leftchild.height()) else: sizes = [self.leftchild.height(), self.rightchild.height()] self.resize(self.leftchild.width(), sum(sizes)) self.setSizes(sizes) # if it is a leaf splitter elif isinstance(layout, Tabbed): # don't clear-out empty_widget's information if all it contains is an # empty_widget if not self.is_empty(): self.tabwidget().clear() for item in layout.items: if not item.id == -1: editor = self.editor_area.editors[item.id] self.tabwidget().addTab( editor.control, self.editor_area._get_label(editor) ) self.resize(item.width, item.height) self.tabwidget().setCurrentIndex(layout.active_tab) def tabwidget(self): """ Obtain the tabwidget associated with current EditorAreaWidget (returns None for non-leaf splitters) """ for child in self.children(): if isinstance(child, QtGui.QTabWidget): return child return None def tabwidgets(self): """ Return a list of tabwidgets associated with current splitter or any of its descendants. """ tabwidgets = [] if self.is_leaf(): tabwidgets.append(self.tabwidget()) else: tabwidgets.extend(self.leftchild.tabwidgets()) tabwidgets.extend(self.rightchild.tabwidgets()) return tabwidgets def sibling(self): """ Returns another child of its parent. Returns None if it can't find any sibling. """ parent = self.parent() if self.is_root(): return None if self is parent.leftchild: return parent.rightchild elif self is parent.rightchild: return parent.leftchild def is_root(self): """ Returns True if the current EditorAreaWidget is the root widget. """ parent = self.parent() if isinstance(parent, EditorAreaWidget): return False else: return True def is_leaf(self): """ Returns True if the current EditorAreaWidget is a leaf, i.e., it has a tabwidget as one of it's immediate child. """ # a leaf has it's leftchild and rightchild None if not self.leftchild and not self.rightchild: return True return False def is_empty(self): """ Returns True if the current splitter's tabwidget doesn't contain any tab. """ return bool(self.tabwidget().empty_widget) def is_collapsible(self): """ Returns True if the current splitter can be collapsed to its sibling, i.e. if it is (a) either empty, or (b) it has a sibling which is a leaf. """ if self.is_root(): return False if self.is_empty(): return True sibling = self.sibling() if sibling.is_leaf(): return True else: return False def split(self, orientation=QtCore.Qt.Orientation.Horizontal): """ Split the current splitter into two children splitters. The current splitter's tabwidget is moved to the left child while a new empty tabwidget is added to the right child. orientation : whether to split horizontally or vertically """ # set splitter orientation self.setOrientation(orientation) orig_size = self.sizes()[0] # create new children self.leftchild = EditorAreaWidget( self.editor_area, parent=self, tabwidget=self.tabwidget() ) self.rightchild = EditorAreaWidget( self.editor_area, parent=self, tabwidget=None ) # add newly generated children self.addWidget(self.leftchild) self.addWidget(self.rightchild) # set equal sizes of splits self.setSizes([orig_size // 2, orig_size // 2]) # make the rightchild's tabwidget active & show its empty widget self.editor_area.active_tabwidget = self.rightchild.tabwidget() def collapse(self): """ Collapses the current splitter and its sibling splitter to their parent splitter. Merges together the tabs of both's tabwidgets. Does nothing if the current splitter is not collapsible. """ if not self.is_collapsible(): return parent = self.parent() sibling = self.sibling() # this will happen only if self is empty, else it will not be # collapsible at all if sibling and (not sibling.is_leaf()): parent.setOrientation(sibling.orientation()) # reparent sibling's children to parent parent.addWidget(sibling.leftchild) parent.addWidget(sibling.rightchild) parent.leftchild = sibling.leftchild parent.rightchild = sibling.rightchild # blindly make the first tabwidget active as it is not clear which # tabwidget should get focus now (FIXME??) self.editor_area.active_tabwidget = parent.tabwidgets()[0] self.setParent(None) sibling.setParent(None) return # save original currentwidget to make active later # (if self is empty, make the currentwidget of sibling active) if not self.is_empty(): orig_currentWidget = self.tabwidget().currentWidget() else: orig_currentWidget = sibling.tabwidget().currentWidget() left = parent.leftchild.tabwidget() right = parent.rightchild.tabwidget() target = DraggableTabWidget( editor_area=self.editor_area, parent=parent ) # add tabs of left and right tabwidgets to target for source in (left, right): # Note: addTab removes widgets from source tabwidget, so # grabbing all the source widgets beforehand # (not grabbing empty_widget) widgets = [ source.widget(i) for i in range(source.count()) if not source.widget(i) is source.empty_widget ] for editor_widget in widgets: editor = self.editor_area._get_editor(editor_widget) target.addTab( editor_widget, self.editor_area._get_label(editor) ) # add target to parent parent.addWidget(target) # make target the new active tabwidget and make the original focused # widget active in the target too self.editor_area.active_tabwidget = target target.setCurrentWidget(orig_currentWidget) # remove parent's splitter children parent.leftchild = None parent.rightchild = None self.setParent(None) sibling.setParent(None) class DraggableTabWidget(QtGui.QTabWidget): """ Implements a QTabWidget with event filters for tab drag and drop """ def __init__(self, editor_area, parent): """ editor_area : global SplitEditorAreaPane instance parent : parent of the tabwidget """ super().__init__(parent) self.editor_area = editor_area # configure QTabWidget self.setTabBar(DraggableTabBar(editor_area=editor_area, parent=self)) self.setDocumentMode(True) self.setFocusPolicy(QtCore.Qt.FocusPolicy.StrongFocus) self.setFocusProxy(None) self.setMovable(False) # handling move events myself self.setTabsClosable(True) self.setAutoFillBackground(True) # set drop and context menu policies self.setAcceptDrops(True) self.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.DefaultContextMenu) # connecting signals self.tabCloseRequested.connect(self._close_requested) self.currentChanged.connect(self._current_changed) # shows the custom empty widget containing buttons for relevant actions self.show_empty_widget() def show_empty_widget(self): """ Shows the empty widget (containing buttons to open new file, and collapse the split). """ self.empty_widget = None self.editor_area.active_tabwidget = self # callback to editor_area's public `create_empty_widget` Callable trait empty_widget = self.editor_area.create_empty_widget() self.addTab(empty_widget, "") self.empty_widget = empty_widget self.setFocus() # don't allow tab closing if empty widget comes up on a root tabwidget if self.parent().is_root(): self.setTabsClosable(False) self.setTabText(0, " ") def hide_empty_widget(self): """ Hides the empty widget (containing buttons to open new file, and collapse the split) based on whether the tabwidget is empty or not. """ index = self.indexOf(self.empty_widget) self.removeTab(index) self.empty_widget.deleteLater() self.empty_widget = None self.setTabsClosable(True) def create_empty_widget(self): """ Creates the QFrame object to be shown when the current tabwidget is empty. """ frame = QtGui.QFrame(parent=self) frame.setFrameShape(QtGui.QFrame.Shape.StyledPanel) layout = QtGui.QVBoxLayout(frame) # Add new file button and open file button only if the `callbacks` trait # of the editor_area has a callable for key `new` and key `open` new_file_action = self.editor_area.callbacks.get("new", None) open_file_action = self.editor_area.callbacks.get("open_dialog", None) open_show_dialog = False if open_file_action is None: open_file_action = self.editor_area.callbacks.get("open", None) open_show_dialog = True if not (new_file_action and open_file_action): return frame layout.addStretch() # generate new file button newfile_btn = QtGui.QPushButton("Create a new file", parent=frame) newfile_btn.clicked.connect(new_file_action) layout.addWidget(newfile_btn, alignment=QtCore.Qt.AlignmentFlag.AlignHCenter) # generate label label = QtGui.QLabel(parent=frame) label.setText( """ or """ ) layout.addWidget(label, alignment=QtCore.Qt.AlignmentFlag.AlignHCenter) # generate open button open_btn = QtGui.QPushButton( "Select files from your computer", parent=frame ) def _open(): if open_show_dialog: open_dlg = FileDialog(action="open") open_dlg.open() self.editor_area.active_tabwidget = self if open_dlg.return_code == OK: open_file_action(open_dlg.path) else: open_file_action() open_btn.clicked.connect(_open) layout.addWidget(open_btn, alignment=QtCore.Qt.AlignmentFlag.AlignHCenter) # generate label label = QtGui.QLabel(parent=frame) label.setText( """ Tip: You can also drag and drop files/tabs here. """ ) layout.addWidget(label, alignment=QtCore.Qt.AlignmentFlag.AlignHCenter) layout.addStretch() frame.setLayout(layout) return frame def get_names(self): """ Utility function to return names of all the editors open in the current tabwidget. """ names = [] for i in range(self.count()): editor_widget = self.widget(i) editor = self.editor_area._get_editor(editor_widget) if editor: names.append(editor.name) return names # Signal handlers ---------------------------------------------------- def _close_requested(self, index): """ Re-implemented to close the editor when it's tab is closed """ widget = self.widget(index) # if close requested on empty_widget, collapse the pane and return if widget is self.empty_widget: self.parent().collapse() return editor = self.editor_area._get_editor(widget) editor.close() def _current_changed(self, index): """Re-implemented to update active editor """ self.setCurrentIndex(index) editor_widget = self.widget(index) self.editor_area.active_editor = self.editor_area._get_editor( editor_widget ) def tabInserted(self, index): """ Re-implemented to hide empty_widget when adding a new widget """ # sets tab tooltip only if a real editor was added (not an empty_widget) editor = self.editor_area._get_editor(self.widget(index)) if editor: self.setTabToolTip(index, editor.tooltip) if self.empty_widget: self.hide_empty_widget() def tabRemoved(self, index): """ Re-implemented to show empty_widget again if all tabs are removed """ if not self.count() and not self.empty_widget: self.show_empty_widget() ## Event handlers -----------------------------------------------------# def contextMenuEvent(self, event): """ To show collapse context menu even on empty tabwidgets Parameters ---------- event : QtGui.QContextMenuEvent """ local_pos = event.pos() if self.empty_widget is not None or self.tabBar().rect().contains( local_pos ): # Only display if we are in the tab bar region or the whole area if # we are displaying the default empty widget. global_pos = self.mapToGlobal(local_pos) menu = self.editor_area.get_context_menu(pos=global_pos) if menu is not None: qmenu = menu.create_menu(self) qmenu.exec_(global_pos) def dragEnterEvent(self, event): """ Re-implemented to highlight the tabwidget on drag enter """ for handler in self.editor_area._all_drop_handlers: if handler.can_handle_drop(event, self): self.editor_area.active_tabwidget = self self.setBackgroundRole(QtGui.QPalette.ColorRole.Highlight) event.acceptProposedAction() return super().dragEnterEvent(event) def dropEvent(self, event): """ Re-implemented to handle drop events """ for handler in self.editor_area._all_drop_handlers: if handler.can_handle_drop(event, self): handler.handle_drop(event, self) self.setBackgroundRole(QtGui.QPalette.ColorRole.Window) event.acceptProposedAction() break def dragLeaveEvent(self, event): """ Clear widget highlight on leaving """ self.setBackgroundRole(QtGui.QPalette.ColorRole.Window) return super().dragLeaveEvent(event) class DraggableTabBar(QtGui.QTabBar): """ Implements a QTabBar with event filters for tab drag """ def __init__(self, editor_area, parent): super().__init__(parent) self.editor_area = editor_area self.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.DefaultContextMenu) self.drag_obj = None def mousePressEvent(self, event): if event.button() == QtCore.Qt.MouseButton.LeftButton: index = self.tabAt(event.pos()) tabwidget = self.parent() if tabwidget.widget(index) and ( not tabwidget.widget(index) == tabwidget.empty_widget ): self.drag_obj = TabDragObject( start_pos=event.pos(), tabBar=self ) return super().mousePressEvent(event) def mouseMoveEvent(self, event): """ Re-implemented to create a drag event when the mouse is moved for a sufficient distance while holding down mouse button. """ # go into the drag logic only if a drag_obj is active if self.drag_obj: # is the left mouse button still pressed? if not event.buttons() == QtCore.Qt.MouseButton.LeftButton: pass # has the mouse been dragged for sufficient distance? elif ( event.pos() - self.drag_obj.start_pos ).manhattanLength() < QtGui.QApplication.startDragDistance(): pass # initiate drag else: drag = QtGui.QDrag(self.drag_obj.widget) mimedata = PyMimeData(data=self.drag_obj, pickle=False) drag.setPixmap(self.drag_obj.get_pixmap()) drag.setHotSpot(self.drag_obj.get_hotspot()) drag.setMimeData(mimedata) drag.exec_() self.drag_obj = None # deactivate the drag_obj again return return super().mouseMoveEvent(event) def mouseReleaseEvent(self, event): """ Re-implemented to deactivate the drag when mouse button is released """ self.drag_obj = None return super().mouseReleaseEvent(event) class TabDragObject(object): """ Class to hold information related to tab dragging/dropping """ def __init__(self, start_pos, tabBar): """ Parameters ---------- start_pos : position in tabBar coordinates where the drag was started tabBar : tabBar containing the tab on which drag started """ self.start_pos = start_pos self.from_index = tabBar.tabAt(self.start_pos) self.from_editor_area = tabBar.parent().editor_area self.widget = tabBar.parent().widget(self.from_index) self.from_tabbar = tabBar def get_pixmap(self): """ Returns the drag pixmap including page widget and tab rectangle. """ # instatiate the painter object with gray-color filled pixmap tabBar = self.from_tabbar tab_rect = tabBar.tabRect(self.from_index) size = self.widget.rect().size() result_pixmap = QtGui.QPixmap(size) painter = QtGui.QStylePainter(result_pixmap, tabBar) painter.fillRect(result_pixmap.rect(), QtCore.Qt.GlobalColor.lightGray) painter.setCompositionMode(QtGui.QPainter.CompositionMode.CompositionMode_SourceOver) optTabBase = QtGui.QStyleOptionTabBarBase() optTabBase.initFrom(tabBar) painter.drawPrimitive(QtGui.QStyle.PrimitiveElement.PE_FrameTabBarBase, optTabBase) # region of active tab if is_qt4: # grab wasn't introduced until Qt5 pixmap1 = QtGui.QPixmap.grabWidget(tabBar, tab_rect) else: pixmap1 = tabBar.grab(tab_rect) painter.drawPixmap(0, 0, pixmap1) # region of the page widget if is_qt4: pixmap2 = QtGui.QPixmap.grabWidget(self.widget) else: pixmap2 = self.widget.grab() painter.drawPixmap( 0, tab_rect.height(), size.width(), size.height(), pixmap2 ) # finish painting painter.end() return result_pixmap def get_hotspot(self): return ( self.start_pos - self.from_tabbar.tabRect(self.from_index).topLeft() ) # ---------------------------------------------------------------------------- # Default drop handlers. # ---------------------------------------------------------------------------- class TabDropHandler(BaseDropHandler): """ Class to handle tab drop events """ # whether to allow dragging of tabs across different opened windows allow_cross_window_drop = Bool(False) def can_handle_drop(self, event, target): if isinstance(event.mimeData(), PyMimeData) and isinstance( event.mimeData().instance(), TabDragObject ): if not self.allow_cross_window_drop: drag_obj = event.mimeData().instance() return drag_obj.from_editor_area == target.editor_area else: return True return False def handle_drop(self, event, target): # get the drop object back drag_obj = event.mimeData().instance() # extract widget label # (editor_area is common to both source and target in most cases but when # the dragging happens across different windows, they are not, and hence it # must be pulled in directly from the source) editor = target.editor_area._get_editor(drag_obj.widget) label = target.editor_area._get_label(editor) # if drop occurs at a tab bar, insert the tab at that position if not target.tabBar().tabAt(event.pos()) == -1: index = target.tabBar().tabAt(event.pos()) target.insertTab(index, drag_obj.widget, label) else: # if the drag initiated from the same tabwidget, put the tab # back at the original index if target is drag_obj.from_tabbar.parent(): target.insertTab(drag_obj.from_index, drag_obj.widget, label) # else, just add it at the end else: target.addTab(drag_obj.widget, label) # make the dropped widget active target.setCurrentWidget(drag_obj.widget) pyface-7.4.0/pyface/ui/qt4/tasks/util.py0000644000076500000240000000353214176253531021014 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 # ---------------------------------------------------------------------------- # Functions. # ---------------------------------------------------------------------------- def set_focus(control): """ Assign keyboard focus to the given control. Ideally, we would just call ``setFocus()`` on the control and let Qt do the right thing. Unfortunately, this method is implemented in the most naive manner possible, and is essentially a no-op if the toplevel widget does not itself accept focus. We adopt the following procedure: 1. If the control itself accepts focus, use it. This is important since the control may have custom focus dispatching logic. 2. Otherwise, if there is a child widget of the control that previously had focus, use it. 3. Finally, have Qt determine the next item using its internal logic. Qt will only restrict itself to this widget's children if it is a Qt::Window or Qt::SubWindow, hence the hack below. """ if control.focusPolicy() != QtCore.Qt.FocusPolicy.NoFocus: control.setFocus() else: widget = control.focusWidget() if widget: widget.setFocus() else: flags = control.windowFlags() control.setWindowFlags(flags | QtCore.Qt.WindowType.SubWindow) try: control.focusNextChild() finally: control.setWindowFlags(flags) pyface-7.4.0/pyface/ui/qt4/tasks/tests/0000755000076500000240000000000014176460551020626 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/qt4/tasks/tests/test_dock_pane.py0000644000076500000240000000601114176253531024156 0ustar cwebsterstaff00000000000000# Copyright (c) 2014-2022 by 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! import sys import unittest from pyface.qt import QtCore from pyface.tasks.api import ( DockPane, EditorAreaPane, PaneItem, Task, TaskFactory, TaskLayout, TasksApplication) class MyDockPane(DockPane): id = "my_dock_pane" name = u"My dock pane" class MyTask(Task): id = "my_task" name = u"My task" def _default_layout_default(self): return TaskLayout(left=PaneItem("my_dock_pane", width=200)) def create_central_pane(self): return EditorAreaPane() def create_dock_panes(self): return [MyDockPane()] class MyApplication(TasksApplication): id = "my_application" name = u"My application" def _task_factories_default(self): return [ TaskFactory( id="my_task_factory", name=u"My task factory", factory=MyTask, ), ] class TestDockPane(unittest.TestCase): @unittest.skipUnless(sys.platform == "darwin", "only applicable to macOS") def test_dock_windows_visible_on_macos(self): # Regression test for enthought/pyface#427: check that dock panes # are displayed on macOS even when the application doesn't have # focus. tool_attributes = [] def check_panes_and_exit(app_event): app = app_event.application for window in app.windows: for dock_pane in window.dock_panes: attr = dock_pane.control.testAttribute( QtCore.Qt.WidgetAttribute.WA_MacAlwaysShowToolWindow) tool_attributes.append(attr) app.exit() app = MyApplication() app.on_trait_change(check_panes_and_exit, "application_initialized") app.run() self.assertTrue(tool_attributes) for attr in tool_attributes: self.assertTrue(attr) def test_dock_windows_undock(self): # Regression test for enthought/pyface#1028: check that undocking # dockpanes doesn't crash tool_attributes = [] def check_panes_and_exit(app_event): app = app_event.application app.windows[0].dock_panes[0].control.setFloating(True) for window in app.windows: for dock_pane in window.dock_panes: attr = dock_pane.dock_area tool_attributes.append(attr) app.exit() app = MyApplication() app.on_trait_change(check_panes_and_exit, "application_initialized") app.run() self.assertTrue(tool_attributes) for attr in tool_attributes: self.assertEqual(attr, 'left') pyface-7.4.0/pyface/ui/qt4/tasks/tests/__init__.py0000644000076500000240000000000014176222673022726 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/qt4/tasks/tests/test_main_window_layout.py0000644000076500000240000001017214176222673026151 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.tasks.api import TaskLayout, PaneItem from pyface.toolkit import toolkit_object from pyface.window import Window try: from pyface.qt import QtGui from pyface.ui.qt4.tasks.main_window_layout import MainWindowLayout except ImportError: if toolkit_object.toolkit == "qt4": raise GuiTestAssistant = toolkit_object("util.gui_test_assistant:GuiTestAssistant") def create_dummy_dock_widget(parent): """ Create a dummy QDockWidget with a dummy child widget for test. Parameters ---------- parent : QObject Returns ------- dock_widget : QDockWidget """ dock_widget = QtGui.QDockWidget(parent) content_widget = QtGui.QWidget(parent) dock_widget.setWidget(content_widget) return dock_widget @unittest.skipIf( toolkit_object.toolkit != "qt4", "This test targets Qt specific MainWindowLayout. " "Current toolkit is not Qt." ) class TestMainWindowLayout(unittest.TestCase, GuiTestAssistant): """ Test Qt specific MainWindowLayout. Note that MainWindowLayout does not have a toolkit-agnostic interface in the ``pyface.tasks`` package. Therefore this test is Qt-only. """ def setUp(self): GuiTestAssistant.setUp(self) self.window = Window(size=(500, 500)) self.window._create() def tearDown(self): if self.window.control is not None: with self.delete_widget(self.window.control): self.window.destroy() del self.window GuiTestAssistant.tearDown(self) def setup_window_with_central_widget(self): # Add a central widget to the main window. # The main window takes ownership of the child widget. central_widget = QtGui.QWidget(parent=self.window.control) self.window.control.setCentralWidget(central_widget) def test_set_pane_item_width_in_main_window_layout(self): # Test the dock pane width is as expected. self.setup_window_with_central_widget() # Set the dock widget expected width to be smaller than the window # for a meaningful test. expected_width = self.window.size[0] // 2 window_layout = MainWindowLayout(control=self.window.control) dock_layout = TaskLayout( left=PaneItem(width=expected_width) ) dock_widget = create_dummy_dock_widget(parent=self.window.control) patch_get_dock_widget = mock.patch.object( MainWindowLayout, "_get_dock_widget", return_value=dock_widget, ) # when with self.event_loop(): with patch_get_dock_widget: window_layout.set_layout(dock_layout) # then size = dock_widget.widget().size() self.assertEqual(size.width(), expected_width) def test_set_pane_item_height_in_main_window_layout(self): # Test the dock pane height is as expected. self.setup_window_with_central_widget() # Set the dock widget expected height to be smaller than the window # for a meaningful test. expected_height = self.window.size[1] // 2 window_layout = MainWindowLayout(control=self.window.control) dock_layout = TaskLayout( bottom=PaneItem(height=expected_height) ) dock_widget = create_dummy_dock_widget(parent=self.window.control) patch_get_dock_widget = mock.patch.object( MainWindowLayout, "_get_dock_widget", return_value=dock_widget, ) # when with self.event_loop(): with patch_get_dock_widget: window_layout.set_layout(dock_layout) # then size = dock_widget.widget().size() self.assertEqual(size.height(), expected_height) pyface-7.4.0/pyface/ui/qt4/tasks/tests/test_split_editor_area_pane.py0000644000076500000240000005001614176253531026733 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 SplitEditorAreaPane class. """ import os import tempfile import unittest from traits.api import Instance from pyface.qt import QtGui, QtCore from pyface.tasks.split_editor_area_pane import ( EditorAreaWidget, SplitEditorAreaPane, ) from pyface.tasks.api import ( Editor, PaneItem, Splitter, Tabbed, Task, TaskWindow, ) from pyface.util.guisupport import get_app_qt4 from pyface.ui.qt4.util.testing import event_loop class ViewWithTabsEditor(Editor): """ Test editor, displaying a labels in tabs. """ name = "Test Editor" def create(self, parent): """ Create and set the toolkit-specific contents of the editor. """ control = QtGui.QTabWidget() control.addTab(QtGui.QLabel("tab 1"), "group 1") control.addTab(QtGui.QLabel("tab 2"), "group 2") self.control = control def destroy(self): """ Destroy the toolkit-specific control that represents the editor. """ self.control = None class SplitEditorAreaPaneTestTask(Task): """ A test task containing a SplitEditorAreaPane. """ id = "test_task" name = "Test Task" editor_area = Instance(SplitEditorAreaPane, ()) def create_central_pane(self): return self.editor_area class TestEditorAreaWidget(unittest.TestCase): """ Tests for the SplitEditorAreaPane class. """ def _setUp_split(self, parent=None): """ Sets up the root splitter for splitting. Returns this root. parent : parent of the returned root """ root = EditorAreaWidget( editor_area=SplitEditorAreaPane(), parent=parent ) btn0 = QtGui.QPushButton("0") btn1 = QtGui.QPushButton("1") tabwidget = root.tabwidget() tabwidget.addTab(btn0, "0") tabwidget.addTab(btn1, "1") tabwidget.setCurrentIndex(1) return root def test_split(self): """ Does split function work correct? """ # setup root = self._setUp_split() tabwidget = root.tabwidget() btn0 = tabwidget.widget(0) btn1 = tabwidget.widget(1) # perform root.split(orientation=QtCore.Qt.Orientation.Horizontal) # test # do we get correct leftchild and rightchild? self.assertIsNotNone(root.leftchild) self.assertIsNotNone(root.rightchild) self.assertIsInstance(root.leftchild, EditorAreaWidget) self.assertIsInstance(root.rightchild, EditorAreaWidget) self.assertEqual(root.leftchild.count(), 1) self.assertEqual(root.rightchild.count(), 1) # are the tabwidgets laid out correctly? self.assertEqual(root.leftchild.tabwidget(), tabwidget) self.assertIsNotNone(root.rightchild.tabwidget().empty_widget) # are the contents of the left tabwidget correct? self.assertEqual(root.leftchild.tabwidget().count(), 2) self.assertEqual(root.leftchild.tabwidget().widget(0), btn0) self.assertEqual(root.leftchild.tabwidget().widget(1), btn1) self.assertEqual(root.leftchild.tabwidget().currentWidget(), btn1) # does the right tabwidget contain nothing but the empty widget? self.assertEqual(root.rightchild.tabwidget().count(), 1) self.assertEqual( root.rightchild.tabwidget().widget(0), root.rightchild.tabwidget().empty_widget, ) # do we have an equally sized split? self.assertEqual(root.leftchild.width(), root.rightchild.width()) # is the rightchild active? self.assertEqual( root.editor_area.active_tabwidget, root.rightchild.tabwidget() ) def _setUp_collapse(self, parent=None): """ Creates a root, its leftchild and rightchild, so that collapse can be tested on one of the children. Returns the root, leftchild and rightchild of such layout. parent : parent of the returned root """ # setup leftchild left = EditorAreaWidget(editor_area=SplitEditorAreaPane(), parent=None) btn0 = QtGui.QPushButton("btn0") btn1 = QtGui.QPushButton("btn1") tabwidget = left.tabwidget() tabwidget.addTab(btn0, "0") tabwidget.addTab(btn1, "1") tabwidget.setCurrentIndex(1) # setup rightchild right = EditorAreaWidget(editor_area=left.editor_area, parent=None) btn2 = QtGui.QPushButton("btn2") btn3 = QtGui.QPushButton("btn3") tabwidget = right.tabwidget() tabwidget.addTab(btn2, "2") tabwidget.addTab(btn3, "3") tabwidget.setCurrentIndex(0) # setup root root = EditorAreaWidget(editor_area=left.editor_area, parent=parent) tabwidget = root.tabwidget() tabwidget.setParent(None) root.addWidget(left) root.addWidget(right) root.leftchild = left root.rightchild = right return root, left, right def test_collapse_nonempty(self): """ Test for collapse function when the source of collapse is not an empty tabwidget. This would result in a new tabwidget which merges the tabs of the collapsing tabwidgets. """ # setup root root, _, right = self._setUp_collapse() btn2 = right.tabwidget().widget(0) # perform collapse on rightchild root.rightchild.collapse() # test # has the root now become the leaf? self.assertEqual(root.count(), 1) self.assertIsInstance(root.widget(0), QtGui.QTabWidget) # how does the combined list look? self.assertEqual(root.tabwidget().count(), 4) self.assertEqual(root.tabwidget().currentWidget(), btn2) def test_collapse_empty(self): """ Test for collapse function when the collapse origin is an empty tabwidget. It's sibling can have an arbitrary layout and the result would be such that this layout is transferred to the parent. """ # setup root = EditorAreaWidget(editor_area=SplitEditorAreaPane(), parent=None) tabwidget = root.tabwidget() tabwidget.setParent(None) left, left_left, left_right = self._setUp_collapse(parent=root) right = EditorAreaWidget(editor_area=root.editor_area, parent=root) root.leftchild = left root.rightchild = right # perform collapse on leftchild right.collapse() # test # is the layout of root now same as left? self.assertEqual(root.count(), 2) self.assertEqual(root.leftchild, left_left) self.assertEqual(root.rightchild, left_right) # are the contents of left_left and left_right preserved self.assertEqual(root.leftchild.tabwidget().count(), 2) self.assertEqual(root.rightchild.tabwidget().count(), 2) self.assertEqual(root.leftchild.tabwidget().currentIndex(), 1) self.assertEqual(root.rightchild.tabwidget().currentIndex(), 0) # what is the current active_tabwidget? self.assertEqual( root.editor_area.active_tabwidget, root.leftchild.tabwidget() ) def test_persistence(self): """ Tests whether get_layout/set_layout work correctly by setting a given layout and getting back the obtained layout. """ # setup the test layout - one horizontal split and one vertical split # on the rightchild of horizontal split, where the top tabwidget of # the vertical split is empty. layout = Splitter( Tabbed(PaneItem(id=0, width=600, height=600), active_tab=0), Splitter( Tabbed(PaneItem(id=-1, width=600, height=300), active_tab=0), Tabbed( PaneItem(id=1, width=600, height=300), PaneItem(id=2, width=600, height=300), active_tab=0, ), orientation="vertical", ), orientation="horizontal", ) # a total of 3 files are needed to give this layout - one on the # leftchild of horizontal split, and the other two on the bottom # tabwidget of the rightchild's vertical split file0 = open(os.path.join(tempfile.gettempdir(), "file0"), "w+b") file1 = open(os.path.join(tempfile.gettempdir(), "file1"), "w+b") file2 = open(os.path.join(tempfile.gettempdir(), "file2"), "w+b") # adding the editors editor_area = SplitEditorAreaPane() editor_area.create(parent=None) editor_area.add_editor(Editor(obj=file0, tooltip="test_tooltip0")) editor_area.add_editor(Editor(obj=file1, tooltip="test_tooltip1")) editor_area.add_editor(Editor(obj=file2, tooltip="test_tooltip2")) # test tooltips self.assertEqual( editor_area.active_tabwidget.tabToolTip(0), "test_tooltip0" ) self.assertEqual( editor_area.active_tabwidget.tabToolTip(1), "test_tooltip1" ) self.assertEqual( editor_area.active_tabwidget.tabToolTip(2), "test_tooltip2" ) # test set_layout # set the layout editor_area.set_layout(layout) # file0 goes to left pane? left = editor_area.control.leftchild editor = editor_area._get_editor(left.tabwidget().widget(0)) self.assertEqual(editor.obj, file0) # right pane is a splitter made of two panes? right = editor_area.control.rightchild self.assertFalse(right.is_leaf()) # right pane is vertical splitter? self.assertEqual(right.orientation(), QtCore.Qt.Orientation.Vertical) # top pane of this vertical split is empty? right_top = right.leftchild self.assertTrue(right_top.is_empty()) # bottom pane is not empty? right_bottom = right.rightchild self.assertFalse(right_bottom.is_empty()) # file1 goes first on bottom pane? editor = editor_area._get_editor(right_bottom.tabwidget().widget(0)) self.assertEqual(editor.obj, file1) # file2 goes second on bottom pane? editor = editor_area._get_editor(right_bottom.tabwidget().widget(1)) self.assertEqual(editor.obj, file2) # file1 tab is active? self.assertEqual(right_bottom.tabwidget().currentIndex(), 0) # test get_layout # obtain layout layout_new = editor_area.get_layout() # is the top level a horizontal splitter? self.assertIsInstance(layout_new, Splitter) self.assertEqual(layout_new.orientation, "horizontal") # tests on left child left = layout_new.items[0] self.assertIsInstance(left, Tabbed) self.assertEqual(left.items[0].id, 0) # tests on right child right = layout_new.items[1] self.assertIsInstance(right, Splitter) self.assertEqual(right.orientation, "vertical") # tests on top pane of right child right_top = right.items[0] self.assertIsInstance(right_top, Tabbed) self.assertEqual(right_top.items[0].id, -1) # tests on bottom pane of right child right_bottom = right.items[1] self.assertIsInstance(right_bottom, Tabbed) self.assertEqual(right_bottom.items[0].id, 1) self.assertEqual(right_bottom.items[1].id, 2) # Close all of the opened temporary files file0.close() file1.close() file2.close() def test_context_menu_merge_text_left_right_split(self): # Regression test for enthought/pyface#422 window = TaskWindow() task = SplitEditorAreaPaneTestTask() editor_area = task.editor_area window.add_task(task) with event_loop(): window.open() editor_area_widget = editor_area.control with event_loop(): editor_area_widget.split(orientation=QtCore.Qt.Orientation.Horizontal) # Get the tabs. left_tab, right_tab = editor_area_widget.tabwidgets() # Check left context menu merge text. left_tab_center = left_tab.mapToGlobal(left_tab.rect().center()) left_context_menu = editor_area.get_context_menu(left_tab_center) self.assertEqual( left_context_menu.find_item("merge").action.name, "Merge with right pane", ) # And the right context menu merge text. right_tab_center = right_tab.mapToGlobal(right_tab.rect().center()) right_context_menu = editor_area.get_context_menu(right_tab_center) self.assertEqual( right_context_menu.find_item("merge").action.name, "Merge with left pane", ) with event_loop(): window.close() def test_context_menu_merge_text_top_bottom_split(self): # Regression test for enthought/pyface#422 window = TaskWindow() task = SplitEditorAreaPaneTestTask() editor_area = task.editor_area window.add_task(task) with event_loop(): window.open() editor_area_widget = editor_area.control with event_loop(): editor_area_widget.split(orientation=QtCore.Qt.Orientation.Vertical) # Get the tabs. top_tab, bottom_tab = editor_area_widget.tabwidgets() # Check top context menu merge text. top_tab_center = top_tab.mapToGlobal(top_tab.rect().center()) top_context_menu = editor_area.get_context_menu(top_tab_center) self.assertEqual( top_context_menu.find_item("merge").action.name, "Merge with bottom pane", ) # And the bottom context menu merge text. bottom_tab_center = bottom_tab.mapToGlobal(bottom_tab.rect().center()) bottom_context_menu = editor_area.get_context_menu(bottom_tab_center) self.assertEqual( bottom_context_menu.find_item("merge").action.name, "Merge with top pane", ) with event_loop(): window.close() def test_no_context_menu_if_outside_tabwidgets(self): # Check that the case of a position not in any of the tab widgets # is handled correctly. window = TaskWindow() task = SplitEditorAreaPaneTestTask() window.add_task(task) with event_loop(): window.open() editor_area = task.editor_area editor_area_widget = editor_area.control tab_widget, = editor_area_widget.tabwidgets() # Position is relative to the receiving widget, so (-1, -1) should be # reliably outside. pos = QtCore.QPoint(-1, -1) context_menu_event = QtGui.QContextMenuEvent( QtGui.QContextMenuEvent.Reason.Mouse, pos ) global_pos = editor_area_widget.mapToGlobal(pos) self.assertIsNone(editor_area.get_context_menu(global_pos)) # Exercise the context menu code to make sure it doesn't raise. (It # should do nothing.) with event_loop(): tab_widget.contextMenuEvent(context_menu_event) with event_loop(): window.close() def test_active_tabwidget_after_editor_containing_tabs_gets_focus(self): # Regression test: if an editor contains tabs, a change in focus # sets the editor area pane `active_tabwidget` to one of those tabs, # rather than the editor's tab, after certain operations (e.g., # navigating the editor tabs using keyboard shortcuts). window = TaskWindow() task = SplitEditorAreaPaneTestTask() editor_area = task.editor_area window.add_task(task) # Show the window. with event_loop(): window.open() with event_loop(): app = get_app_qt4() app.setActiveWindow(window.control) # Add and activate an editor which contains tabs. editor = ViewWithTabsEditor() with event_loop(): editor_area.add_editor(editor) with event_loop(): editor_area.activate_editor(editor) # Check that the active tabwidget is the right one. self.assertIs( editor_area.active_tabwidget, editor_area.control.tabwidget() ) with event_loop(): window.close() def test_active_editor_after_focus_change(self): window = TaskWindow(size=(800, 600)) task = SplitEditorAreaPaneTestTask() editor_area = task.editor_area window.add_task(task) # Show the window. with event_loop(): window.open() with event_loop(): app = get_app_qt4() app.setActiveWindow(window.control) # Add and activate an editor which contains tabs. left_editor = ViewWithTabsEditor() right_editor = ViewWithTabsEditor() with event_loop(): editor_area.add_editor(left_editor) with event_loop(): editor_area.control.split(orientation=QtCore.Qt.Orientation.Horizontal) with event_loop(): editor_area.add_editor(right_editor) editor_area.activate_editor(right_editor) self.assertEqual(editor_area.active_editor, right_editor) with event_loop(): left_editor.control.setFocus() self.assertIsNotNone(editor_area.active_editor) self.assertEqual(editor_area.active_editor, left_editor) with event_loop(): window.close() def test_editor_label_change_inactive(self): # regression test for pyface#523 window = TaskWindow(size=(800, 600)) task = SplitEditorAreaPaneTestTask() editor_area = task.editor_area window.add_task(task) # Show the window. with event_loop(): window.open() with event_loop(): app = get_app_qt4() app.setActiveWindow(window.control) # Add and activate an editor which contains tabs. left_editor = ViewWithTabsEditor() right_editor = ViewWithTabsEditor() with event_loop(): editor_area.add_editor(left_editor) with event_loop(): editor_area.control.split(orientation=QtCore.Qt.Orientation.Horizontal) with event_loop(): editor_area.add_editor(right_editor) editor_area.activate_editor(right_editor) # change the name of the inactive editor left_editor.name = "New Name" # the text of the editor's tab should have changed left_tabwidget = editor_area.tabwidgets()[0] index = left_tabwidget.indexOf(left_editor.control) tab_text = left_tabwidget.tabText(index) self.assertEqual(tab_text, "New Name") def test_editor_tooltip_change_inactive(self): # regression test related to pyface#523 window = TaskWindow(size=(800, 600)) task = SplitEditorAreaPaneTestTask() editor_area = task.editor_area window.add_task(task) # Show the window. with event_loop(): window.open() with event_loop(): app = get_app_qt4() app.setActiveWindow(window.control) # Add and activate an editor which contains tabs. left_editor = ViewWithTabsEditor() right_editor = ViewWithTabsEditor() with event_loop(): editor_area.add_editor(left_editor) with event_loop(): editor_area.control.split(orientation=QtCore.Qt.Orientation.Horizontal) with event_loop(): editor_area.add_editor(right_editor) editor_area.activate_editor(right_editor) # change the name of the inactive editor left_editor.tooltip = "New Tooltip" # the text of the editor's tab should have changed left_tabwidget = editor_area.tabwidgets()[0] index = left_tabwidget.indexOf(left_editor.control) tab_tooltip = left_tabwidget.tabToolTip(index) self.assertEqual(tab_tooltip, "New Tooltip") pyface-7.4.0/pyface/ui/qt4/tasks/__init__.py0000644000076500000240000000000014176222673021564 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/qt4/tasks/task_pane.py0000644000076500000240000000277214176222673022014 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.tasks.i_task_pane import ITaskPane, MTaskPane from traits.api import provides from pyface.qt import QtGui from .util import set_focus @provides(ITaskPane) class TaskPane(MTaskPane): """ The toolkit-specific implementation of a TaskPane. See the ITaskPane interface for API documentation. """ # ------------------------------------------------------------------------ # 'ITaskPane' interface. # ------------------------------------------------------------------------ def create(self, parent): """ Create and set the toolkit-specific control that represents the pane. """ self.control = QtGui.QWidget(parent) def destroy(self): """ Destroy the toolkit-specific control that represents the pane. """ if self.control is not None: self.control.hide() self.control.setParent(None) self.control = None def set_focus(self): """ Gives focus to the control that represents the pane. """ if self.control is not None: set_focus(self.control) pyface-7.4.0/pyface/ui/qt4/tasks/dock_pane.py0000644000076500000240000001636114176253531021766 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 from pyface.tasks.i_dock_pane import IDockPane, MDockPane from traits.api import Bool, observe, Property, provides, Tuple from pyface.qt import QtCore, QtGui from .task_pane import TaskPane from .util import set_focus # Constants. AREA_MAP = { "left": QtCore.Qt.DockWidgetArea.LeftDockWidgetArea, "right": QtCore.Qt.DockWidgetArea.RightDockWidgetArea, "top": QtCore.Qt.DockWidgetArea.TopDockWidgetArea, "bottom": QtCore.Qt.DockWidgetArea.BottomDockWidgetArea, } INVERSE_AREA_MAP = dict((int(v), k) for k, v in AREA_MAP.items()) @provides(IDockPane) class DockPane(TaskPane, MDockPane): """ The toolkit-specific implementation of a DockPane. See the IDockPane interface for API documentation. """ # 'IDockPane' interface ------------------------------------------------ size = Property(Tuple) # Protected traits ----------------------------------------------------- _receiving = Bool(False) # ------------------------------------------------------------------------ # 'ITaskPane' interface. # ------------------------------------------------------------------------ def create(self, parent): """ Create and set the dock widget that contains the pane contents. """ self.control = control = QtGui.QDockWidget(parent) # Set the widget's object name. This important for QMainWindow state # saving. Use the task ID and the pane ID to avoid collisions when a # pane is present in multiple tasks attached to the same window. control.setObjectName(self.task.id + ":" + self.id) # Ensure that undocked ("floating") windows are visible on macOS # when focus is switched, for consistency with Linux and Windows. control.setAttribute(QtCore.Qt.WidgetAttribute.WA_MacAlwaysShowToolWindow) # Configure the dock widget according to the DockPane settings. self._set_dock_features(event=None) self._set_dock_title(event=None) self._set_floating(event=None) self._set_visible(event=None) # Connect signal handlers for updating DockPane traits. control.dockLocationChanged.connect(self._receive_dock_area) control.topLevelChanged.connect(self._receive_floating) control.visibilityChanged.connect(self._receive_visible) # Add the pane contents to the dock widget. contents = self.create_contents(control) control.setWidget(contents) # For some reason the QDockWidget doesn't respect the minimum size # of its widgets contents_minsize = contents.minimumSize() style = control.style() contents_minsize.setHeight( contents_minsize.height() + style.pixelMetric(style.PM_DockWidgetHandleExtent) ) control.setMinimumSize(contents_minsize) # Hide the control by default. Otherwise, the widget will visible in its # parent immediately! control.hide() def destroy(self): """ Destroy the toolkit-specific control that represents the pane. """ if self.control is not None: control = self.control control.dockLocationChanged.disconnect(self._receive_dock_area) control.topLevelChanged.disconnect(self._receive_floating) control.visibilityChanged.disconnect(self._receive_visible) super().destroy() def set_focus(self): """ Gives focus to the control that represents the pane. """ if self.control is not None: set_focus(self.control.widget()) # ------------------------------------------------------------------------ # 'IDockPane' interface. # ------------------------------------------------------------------------ def create_contents(self, parent): """ Create and return the toolkit-specific contents of the dock pane. """ return QtGui.QWidget(parent) # ------------------------------------------------------------------------ # Protected interface. # ------------------------------------------------------------------------ @contextmanager def _signal_context(self): """ Defines a context appropriate for Qt signal callbacks. Necessary to prevent feedback between Traits and Qt event handlers. """ original = self._receiving self._receiving = True yield self._receiving = original # Trait property getters/setters --------------------------------------- def _get_size(self): if self.control is not None: return (self.control.width(), self.control.height()) return (-1, -1) # Trait change handlers ------------------------------------------------ @observe("dock_area") def _set_dock_area(self, event): if self.control is not None and not self._receiving: # Only attempt to adjust the area if the task is active. main_window = self.task.window.control if main_window and self.task == self.task.window.active_task: # Qt will automatically remove the dock widget from its previous # area, if it had one. main_window.addDockWidget( AREA_MAP[self.dock_area], self.control ) @observe("closable,floatable,movable") def _set_dock_features(self, event): if self.control is not None: features = QtGui.QDockWidget.DockWidgetFeature.NoDockWidgetFeatures if self.closable: features |= QtGui.QDockWidget.DockWidgetFeature.DockWidgetClosable if self.floatable: features |= QtGui.QDockWidget.DockWidgetFeature.DockWidgetFloatable if self.movable: features |= QtGui.QDockWidget.DockWidgetFeature.DockWidgetMovable self.control.setFeatures(features) @observe("name") def _set_dock_title(self, event): if self.control is not None: self.control.setWindowTitle(self.name) @observe("floating") def _set_floating(self, event): if self.control is not None and not self._receiving: self.control.setFloating(self.floating) @observe("visible") def _set_visible(self, event): if self.control is not None and not self._receiving: self.control.setVisible(self.visible) # Signal handlers -----------------------------------------------------# def _receive_dock_area(self, area): with self._signal_context(): if int(area) in INVERSE_AREA_MAP: self.dock_area = INVERSE_AREA_MAP[int(area)] def _receive_floating(self, floating): with self._signal_context(): self.floating = floating def _receive_visible(self): with self._signal_context(): if self.control is not None: self.visible = self.control.isVisible() pyface-7.4.0/pyface/ui/qt4/tasks/main_window_layout.py0000644000076500000240000003200614176253531023745 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 itertools import combinations import logging from pyface.qt import QtCore, QtGui, is_qt4 from traits.api import Any, HasTraits from pyface.tasks.task_layout import ( LayoutContainer, PaneItem, Tabbed, Splitter, HSplitter, VSplitter, ) from .dock_pane import AREA_MAP # Contants. ORIENTATION_MAP = { "horizontal": QtCore.Qt.Orientation.Horizontal, "vertical": QtCore.Qt.Orientation.Vertical, } # Logging. logger = logging.getLogger(__name__) class MainWindowLayout(HasTraits): """ A class for applying declarative layouts to a QMainWindow. """ # 'MainWindowLayout' interface ----------------------------------------- # The QMainWindow control to lay out. control = Any() # ------------------------------------------------------------------------ # 'MainWindowLayout' interface. # ------------------------------------------------------------------------ def get_layout(self, layout, include_sizes=True): """ Get the layout by adding sublayouts to the specified DockLayout. """ for name, q_dock_area in AREA_MAP.items(): sublayout = self.get_layout_for_area(q_dock_area, include_sizes) setattr(layout, name, sublayout) def get_layout_for_area(self, q_dock_area, include_sizes=True): """ Gets a LayoutItem for the specified dock area. """ # Build the initial set of leaf-level items. items = set() rects = {} for child in self.control.children(): # Iterate through *visibile* dock widgets. (Inactive tabbed dock # widgets are "visible" but have invalid positions.) if ( isinstance(child, QtGui.QDockWidget) and child.isVisible() and self.control.dockWidgetArea(child) == q_dock_area and child.x() >= 0 and child.y() >= 0 ): # Get the list of dock widgets in this tab group in order. geometry = child.geometry() tabs = [ tab for tab in self.control.tabifiedDockWidgets(child) if tab.isVisible() ] if tabs: tab_bar = self._get_tab_bar(child) tab_index = tab_bar.currentIndex() tabs.insert(tab_index, child) geometry = tab_bar.geometry().united(geometry) # Create the leaf-level item for the child. if tabs: panes = [ self._prepare_pane(dock_widget, include_sizes) for dock_widget in tabs ] item = Tabbed(*panes, active_tab=panes[tab_index].id) else: item = self._prepare_pane(child, include_sizes) items.add(item) rects[item] = geometry # Build the layout tree bottom-up, in multiple passes. while len(items) > 1: add, remove = set(), set() for item1, item2 in combinations(items, 2): if item1 not in remove and item2 not in remove: rect1, rect2 = rects[item1], rects[item2] orient = self._get_division_orientation(rect1, rect2, True) if orient == QtCore.Qt.Orientation.Horizontal: if rect1.y() < rect2.y(): item = VSplitter(item1, item2) else: item = VSplitter(item2, item1) elif orient == QtCore.Qt.Orientation.Vertical: if rect1.x() < rect2.x(): item = HSplitter(item1, item2) else: item = HSplitter(item2, item1) else: continue rects[item] = rect1.united(rect2) add.add(item) remove.update((item1, item2)) if add or remove: items.update(add) items.difference_update(remove) else: # Raise an exception instead of falling into an infinite loop. raise RuntimeError( "Unable to extract layout from QMainWindow." ) if items: return items.pop() return None def set_layout(self, layout): """ Applies a DockLayout to the window. """ # Remove all existing dock widgets. for child in self.control.children(): if isinstance(child, QtGui.QDockWidget): child.hide() self.control.removeDockWidget(child) # Perform the layout. This will assign fixed sizes to the dock widgets # to enforce size constraints specified in the PaneItems. for name, q_dock_area in AREA_MAP.items(): sublayout = getattr(layout, name) if sublayout: self.set_layout_for_area( sublayout, q_dock_area, _toplevel_call=False ) if is_qt4: # Remove the fixed sizes once Qt activates the layout. QtCore.QTimer.singleShot(0, self._reset_fixed_sizes) def set_layout_for_area( self, layout, q_dock_area, _toplevel_added=False, _toplevel_call=True ): """ Applies a LayoutItem to the specified dock area. """ # If we try to do the layout bottom-up, Qt will become confused. In # order to do it top-down, we have know which dock widget is # "effectively" top level, requiring us to reach down to the leaves of # the layout. (This is really only an issue for Splitter layouts, since # Tabbed layouts are, for our purposes, leaves.) if isinstance(layout, PaneItem): if not _toplevel_added: widget = self._prepare_toplevel_for_item(layout) if widget: self.control.addDockWidget(q_dock_area, widget) widget.show() elif isinstance(layout, Tabbed): active_widget = first_widget = None for item in layout.items: widget = self._prepare_toplevel_for_item(item) if not widget: continue if item.id == layout.active_tab: active_widget = widget if first_widget: self.control.tabifyDockWidget(first_widget, widget) else: if not _toplevel_added: self.control.addDockWidget(q_dock_area, widget) first_widget = widget widget.show() # Activate the appropriate tab, if possible. if not active_widget: # By default, Qt will activate the last widget. active_widget = first_widget if active_widget: # It seems that the 'raise_' call only has an effect after the # QMainWindow has performed its internal layout. QtCore.QTimer.singleShot(0, active_widget.raise_) elif isinstance(layout, Splitter): # Perform top-level splitting as per above comment. orient = ORIENTATION_MAP[layout.orientation] prev_widget = None for item in layout.items: widget = self._prepare_toplevel_for_item(item) if not widget: continue if prev_widget: self.control.splitDockWidget(prev_widget, widget, orient) elif not _toplevel_added: self.control.addDockWidget(q_dock_area, widget) prev_widget = widget widget.show() # Now we can recurse. for i, item in enumerate(layout.items): self.set_layout_for_area( item, q_dock_area, _toplevel_added=True, _toplevel_call=False, ) else: raise MainWindowLayoutError("Unknown layout item %r" % layout) if is_qt4: # Remove the fixed sizes once Qt activates the layout. if _toplevel_call: QtCore.QTimer.singleShot(0, self._reset_fixed_sizes) # ------------------------------------------------------------------------ # 'MainWindowLayout' abstract interface. # ------------------------------------------------------------------------ def _get_dock_widget(self, pane): """ Returns the QDockWidget associated with a PaneItem. """ raise NotImplementedError() def _get_pane(self, dock_widget): """ Returns a PaneItem for a QDockWidget. """ raise NotImplementedError() # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _get_division_orientation(self, one, two, splitter=False): """ Returns whether there is a division between two visible QWidgets. Divided in context means that the widgets are adjacent and aligned along the direction of the adjaceny. """ united = one.united(two) if splitter: sep = self.control.style().pixelMetric( QtGui.QStyle.PixelMetric.PM_DockWidgetSeparatorExtent, None, self.control ) united.adjust(0, 0, -sep, -sep) if ( one.x() == two.x() and one.width() == two.width() and united.height() == one.height() + two.height() ): return QtCore.Qt.Orientation.Horizontal elif ( one.y() == two.y() and one.height() == two.height() and united.width() == one.width() + two.width() ): return QtCore.Qt.Orientation.Vertical return 0 def _get_tab_bar(self, dock_widget): """ Returns the tab bar associated with the given QDockWidget, or None if there is no tab bar. """ dock_geometry = dock_widget.geometry() for child in self.control.children(): if isinstance(child, QtGui.QTabBar) and child.isVisible(): geometry = child.geometry() if self._get_division_orientation(dock_geometry, geometry): return child return None def _prepare_pane(self, dock_widget, include_sizes=True): """ Returns a sized PaneItem for a QDockWidget. """ pane = self._get_pane(dock_widget) if include_sizes: pane.width = dock_widget.widget().width() pane.height = dock_widget.widget().height() return pane def _prepare_toplevel_for_item(self, layout): """ Returns a sized toplevel QDockWidget for a LayoutItem. """ if isinstance(layout, PaneItem): dock_widget = self._get_dock_widget(layout) if dock_widget is None: logger.warning( "Cannot retrieve dock widget for pane %r" % layout.id ) else: if is_qt4: if layout.width > 0: dock_widget.widget().setFixedWidth(layout.width) if layout.height > 0: dock_widget.widget().setFixedHeight(layout.height) else: sizeHint = lambda: QtCore.QSize(layout.width, layout.height) dock_widget.widget().sizeHint = sizeHint return dock_widget elif isinstance(layout, LayoutContainer): return self._prepare_toplevel_for_item(layout.items[0]) else: raise MainWindowLayoutError("Leaves of layout must be PaneItems") def _reset_fixed_sizes(self): """ Clears any fixed sizes assined to QDockWidgets. """ if self.control is None: return QWIDGETSIZE_MAX = (1 << 24) - 1 # Not exposed by Qt bindings. for child in self.control.children(): if isinstance(child, QtGui.QDockWidget): child.widget().setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX) child.widget().setMinimumSize(0, 0) # QDockWidget somehow manages to set its own # min/max sizes and hence that too needs to be reset. child.setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX) child.setMinimumSize(0, 0) class MainWindowLayoutError(ValueError): """ Exception raised when a malformed LayoutItem is passed to the MainWindowLayout. """ pass pyface-7.4.0/pyface/ui/qt4/tasks/editor.py0000644000076500000240000000332114176222673021324 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.tasks.i_editor import IEditor, MEditor from traits.api import Bool, Property, provides from pyface.qt import QtGui @provides(IEditor) class Editor(MEditor): """ The toolkit-specific implementation of a Editor. See the IEditor interface for API documentation. """ # 'IEditor' interface -------------------------------------------------# has_focus = Property(Bool) # ------------------------------------------------------------------------ # 'IEditor' interface. # ------------------------------------------------------------------------ def create(self, parent): """ Create and set the toolkit-specific control that represents the pane. """ self.control = QtGui.QWidget(parent) def destroy(self): """ Destroy the toolkit-specific control that represents the pane. """ if self.control is not None: self.control.hide() self.control.deleteLater() self.control = None # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _get_has_focus(self): if self.control is not None: return self.control.hasFocus() return False pyface-7.4.0/pyface/ui/qt4/confirmation_dialog.py0000644000076500000240000001107114176253531022716 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # (C) Copyright 2007 Riverbank Computing Limited # 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 from pyface.qt import QtCore, QtGui from traits.api import Bool, Dict, Enum, provides, Str from pyface.i_confirmation_dialog import ( IConfirmationDialog, MConfirmationDialog, ) from pyface.constant import CANCEL, YES, NO from pyface.ui_traits import Image from .dialog import Dialog, _RESULT_MAP @provides(IConfirmationDialog) class ConfirmationDialog(MConfirmationDialog, Dialog): """ The toolkit specific implementation of a ConfirmationDialog. See the IConfirmationDialog interface for the API documentation. """ # 'IConfirmationDialog' interface -------------------------------------# cancel = Bool(False) default = Enum(NO, YES, CANCEL) image = Image() message = Str() informative = Str() detail = Str() no_label = Str() yes_label = Str() # If we create custom buttons with the various roles, then we need to # keep track of the buttons so we can see what the user clicked. It's # not correct nor sufficient to check the return result from QMessageBox.exec_(). # (As of Qt 4.5.1, even clicking a button with the YesRole would lead to # exec_() returning QDialog.DialogCode.Rejected. _button_result_map = Dict() # ------------------------------------------------------------------------ # Protected 'IDialog' interface. # ------------------------------------------------------------------------ def _create_contents(self, parent): # In PyQt this is a canned dialog. pass # ------------------------------------------------------------------------ # Protected 'IWidget' interface. # ------------------------------------------------------------------------ def _create_control(self, parent): dlg = QtGui.QMessageBox(parent) dlg.setWindowTitle(self.title) dlg.setText(self.message) dlg.setInformativeText(self.informative) dlg.setDetailedText(self.detail) if self.size != (-1, -1): dlg.resize(*self.size) if self.position != (-1, -1): dlg.move(*self.position) if self.image is None: dlg.setIcon(QtGui.QMessageBox.Icon.Warning) else: dlg.setIconPixmap(self.image.create_image()) # The 'Yes' button. if self.yes_label: btn = dlg.addButton(self.yes_label, QtGui.QMessageBox.ButtonRole.YesRole) else: btn = dlg.addButton(QtGui.QMessageBox.StandardButton.Yes) self._button_result_map[btn] = YES if self.default == YES: dlg.setDefaultButton(btn) # The 'No' button. if self.no_label: btn = dlg.addButton(self.no_label, QtGui.QMessageBox.ButtonRole.NoRole) else: btn = dlg.addButton(QtGui.QMessageBox.StandardButton.No) self._button_result_map[btn] = NO if self.default == NO: dlg.setDefaultButton(btn) # The 'Cancel' button. if self.cancel: if self.cancel_label: btn = dlg.addButton( self.cancel_label, QtGui.QMessageBox.ButtonRole.RejectRole ) else: btn = dlg.addButton(QtGui.QMessageBox.StandardButton.Cancel) self._button_result_map[btn] = CANCEL if self.default == CANCEL: dlg.setDefaultButton(btn) return dlg def _show_modal(self): self.control.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal) retval = self.control.exec_() if self.control is None: # dialog window closed if self.cancel: # if cancel is available, close is Cancel return CANCEL return self.default clicked_button = self.control.clickedButton() if clicked_button in self._button_result_map: retval = self._button_result_map[clicked_button] else: retval = _RESULT_MAP[retval] return retval pyface-7.4.0/pyface/ui/qt4/color.py0000644000076500000240000000267414176222673020041 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Color conversion routines for the Qt toolkit. This module provides a couple of utility methods to support the pyface.color.Color class to_toolkit and from_toolkit methods. """ from pyface.qt.QtGui import QColor from pyface.color import channels_to_ints, ints_to_channels def toolkit_color_to_rgba(qcolor): """ Convert a QColor to an RGBA tuple. Parameters ---------- qcolor : QColor A QColor object. Returns ------- rgba_tuple : tuple A tuple of 4 floating point values between 0.0 and 1.0 inclusive. """ values = ( qcolor.red(), qcolor.green(), qcolor.blue(), qcolor.alpha(), ) return ints_to_channels(values) def rgba_to_toolkit_color(rgba): """ Convert an RGBA tuple to a QColor. Parameters ---------- rgba_tuple : tuple A tuple of 4 floating point values between 0.0 and 1.0 inclusive. Returns ------- qcolor : QColor A QColor object. """ values = channels_to_ints(rgba) return QColor(*values) pyface-7.4.0/pyface/ui/qt4/single_choice_dialog.py0000644000076500000240000000753114176253531023027 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 traits.api import Any, List, Str, provides from pyface.constant import CANCEL from pyface.i_single_choice_dialog import ( ISingleChoiceDialog, MSingleChoiceDialog, ) from .dialog import Dialog, _RESULT_MAP @provides(ISingleChoiceDialog) class SingleChoiceDialog(MSingleChoiceDialog, Dialog): """ A dialog that allows the user to chose a single item from a list. Note that due to limitations of the QInputDialog class, the cancel trait is ignored, and the list of displayed strings must be unique. """ # 'ISingleChoiceDialog' interface -------------------------------------# #: List of objects to choose from. choices = List(Any) #: The object chosen, if any. choice = Any() #: An optional attribute to use for the name of each object in the dialog. name_attribute = Str() #: The message to display to the user. message = Str() def set_dialog_choice(self, choice): if self.control is not None: if self.name_attribute != "": choice = getattr(choice, self.name_attribute) self.control.setTextValue(str(choice)) # ------------------------------------------------------------------------ # Protected 'IDialog' interface. # ------------------------------------------------------------------------ def _create_contents(self, parent): """ Creates the window contents. """ # In this case, Qt does it all for us in 'QInputDialog' pass def _show_modal(self): self.control.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal) retval = self.control.exec_() if self.control is None: # dialog window closed, treat as Cancel, nullify choice retval = CANCEL else: retval = _RESULT_MAP[retval] return retval # ------------------------------------------------------------------------ # 'IWindow' interface. # ------------------------------------------------------------------------ def close(self): """ Closes the window. """ # Get the chosen object. if self.control is not None and self.return_code != CANCEL: text = self.control.textValue() choices = self._choice_strings() if text in choices: idx = self._choice_strings().index(text) self.choice = self.choices[idx] else: self.choice = None else: self.choice = None # Let the window close as normal. super().close() # ------------------------------------------------------------------------ # Protected 'IWidget' interface. # ------------------------------------------------------------------------ def _create_control(self, parent): """ Create the toolkit-specific control that represents the window. """ dialog = QtGui.QInputDialog(parent) dialog.setOption(QtGui.QInputDialog.InputDialogOption.UseListViewForComboBoxItems, True) dialog.setWindowTitle(self.title) dialog.setLabelText(self.message) # initialize choices: set initial value to first choice choices = self._choice_strings() dialog.setComboBoxItems(choices) dialog.setTextValue(choices[0]) if self.size != (-1, -1): self.resize(*self.size) if self.position != (-1, -1): self.move(*self.position) return dialog pyface-7.4.0/pyface/ui/qt4/widget.py0000644000076500000240000001362014176253531020174 0ustar cwebsterstaff00000000000000# (C) Copyright 2007 Riverbank Computing Limited # (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 from pyface.qt.QtCore import Qt from traits.api import Any, Bool, HasTraits, Instance, Str, provides from pyface.i_widget import IWidget, MWidget @provides(IWidget) class Widget(MWidget, HasTraits): """ The toolkit specific implementation of a Widget. See the IWidget interface for the API documentation. """ # 'IWidget' interface ---------------------------------------------------- #: The toolkit specific control that represents the widget. control = Any() #: The control's optional parent control. parent = Any() #: Whether or not the control is visible visible = Bool(True) #: Whether or not the control is enabled enabled = Bool(True) #: A tooltip for the widget. tooltip = Str() #: An optional context menu for the widget. context_menu = Instance("pyface.action.menu_manager.MenuManager") # Private interface ---------------------------------------------------- #: The event filter for the widget. _event_filter = Instance(QtCore.QObject) # ------------------------------------------------------------------------ # 'IWidget' interface. # ------------------------------------------------------------------------ def show(self, visible): """ Show or hide the widget. Parameter --------- visible : bool Visible should be ``True`` if the widget should be shown. """ self.visible = visible if self.control is not None: self.control.setVisible(visible) def enable(self, enabled): """ Enable or disable the widget. Parameter --------- enabled : bool The enabled state to set the widget to. """ self.enabled = enabled if self.control is not None: self.control.setEnabled(enabled) def focus(self): """ Set the keyboard focus to this widget. """ if self.control is not None: self.control.setFocus() def has_focus(self): """ Does the widget currently have keyboard focus? Returns ------- focus_state : bool Whether or not the widget has keyboard focus. """ return ( self.control is not None and self.control.hasFocus() ) def destroy(self): if self.control is not None: self.control.hide() self.control.deleteLater() super().destroy() def _add_event_listeners(self): super()._add_event_listeners() self.control.installEventFilter(self._event_filter) def _remove_event_listeners(self): if self._event_filter is not None: if self.control is not None: self.control.removeEventFilter(self._event_filter) self._event_filter = None super()._remove_event_listeners() # ------------------------------------------------------------------------ # Private interface # ------------------------------------------------------------------------ def _get_control_tooltip(self): """ Toolkit specific method to get the control's tooltip. """ return self.control.toolTip() def _set_control_tooltip(self, tooltip): """ Toolkit specific method to set the control's tooltip. """ self.control.setToolTip(tooltip) def _observe_control_context_menu(self, remove=False): """ Toolkit specific method to change the control menu observer. """ if remove: self.control.setContextMenuPolicy(Qt.ContextMenuPolicy.DefaultContextMenu) self.control.customContextMenuRequested.disconnect( self._handle_control_context_menu ) else: self.control.customContextMenuRequested.connect( self._handle_control_context_menu ) self.control.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) def _handle_control_context_menu(self, pos): """ Signal handler for displaying context menu. """ if self.control is not None and self.context_menu is not None: menu = self.context_menu.create_menu(self.control) menu.show(pos.x(), pos.y()) # Trait change handlers -------------------------------------------------- def _visible_changed(self, new): if self.control is not None: self.show(new) def _enabled_changed(self, new): if self.control is not None: self.enable(new) def __event_filter_default(self): return WidgetEventFilter(self) class WidgetEventFilter(QtCore.QObject): """ An internal class that watches for certain events on behalf of the Widget instance. This filter watches for show and hide events to make sure that visible state of the widget is the opposite of Qt's isHidden() state. This is needed in case other code hides the toolkit widget """ def __init__(self, widget): """ Initialise the event filter. """ QtCore.QObject.__init__(self) self._widget = widget def eventFilter(self, obj, event): """ Adds any event listeners required by the window. """ widget = self._widget # Sanity check. if obj is not widget.control: return False event_type = event.type() if event_type in {QtCore.QEvent.Type.Show, QtCore.QEvent.Type.Hide}: widget.visible = not widget.control.isHidden() return False pyface-7.4.0/pyface/ui/qt4/directory_dialog.py0000644000076500000240000000512014176253531022230 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # (C) Copyright 2007 Riverbank Computing Limited # 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 from pyface.qt import QtGui from traits.api import Bool, provides, Str from pyface.i_directory_dialog import IDirectoryDialog, MDirectoryDialog from .dialog import Dialog @provides(IDirectoryDialog) class DirectoryDialog(MDirectoryDialog, Dialog): """ The toolkit specific implementation of a DirectoryDialog. See the IDirectoryDialog interface for the API documentation. """ # 'IDirectoryDialog' interface ----------------------------------------- default_path = Str() message = Str() new_directory = Bool(True) path = Str() # ------------------------------------------------------------------------ # Protected 'IDialog' interface. # ------------------------------------------------------------------------ def _create_contents(self, parent): # In PyQt this is a canned dialog. pass # ------------------------------------------------------------------------ # 'IWindow' interface. # ------------------------------------------------------------------------ def close(self): # Get the path of the chosen directory. files = self.control.selectedFiles() if files: self.path = str(files[0]) else: self.path = "" # Let the window close as normal. super().close() # ------------------------------------------------------------------------ # Protected 'IWidget' interface. # ------------------------------------------------------------------------ def _create_control(self, parent): dlg = QtGui.QFileDialog(parent, self.title, self.default_path) dlg.setViewMode(QtGui.QFileDialog.ViewMode.Detail) dlg.setFileMode(QtGui.QFileDialog.FileMode.Directory) if not self.new_directory: dlg.setOptions(QtGui.QFileDialog.Option.ReadOnly) if self.message: dlg.setLabelText(QtGui.QFileDialog.DialogLabel.LookIn, self.message) return dlg pyface-7.4.0/pyface/ui/qt4/message_dialog.py0000644000076500000240000000560614176253531021661 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # (C) Copyright 2007 Riverbank Computing Limited # 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 from pyface.qt import QtCore, QtGui from traits.api import Enum, provides, Str from pyface.i_message_dialog import IMessageDialog, MMessageDialog from .dialog import Dialog # Map the ETS severity to the corresponding PyQt standard icon. _SEVERITY_TO_ICON_MAP = { "information": QtGui.QMessageBox.Icon.Information, "warning": QtGui.QMessageBox.Icon.Warning, "error": QtGui.QMessageBox.Icon.Critical, } _TEXT_FORMAT_MAP = { "auto": QtCore.Qt.TextFormat.AutoText, "plain": QtCore.Qt.TextFormat.PlainText, "rich": QtCore.Qt.TextFormat.RichText, } @provides(IMessageDialog) class MessageDialog(MMessageDialog, Dialog): """ The toolkit specific implementation of a MessageDialog. See the IMessageDialog interface for the API documentation. """ # 'IMessageDialog' interface ------------------------------------------- message = Str() informative = Str() detail = Str() severity = Enum("information", "warning", "error") text_format = Enum("auto", "plain", "rich") # ------------------------------------------------------------------------ # Protected 'IDialog' interface. # ------------------------------------------------------------------------ def _create_contents(self, parent): # In PyQt this is a canned dialog. pass # ------------------------------------------------------------------------ # Protected 'IWidget' interface. # ------------------------------------------------------------------------ def _create_control(self, parent): # FIXME: should be possble to set ok_label, but not implemented message_box = QtGui.QMessageBox( _SEVERITY_TO_ICON_MAP[self.severity], self.title, self.message, QtGui.QMessageBox.StandardButton.Ok, parent, ) message_box.setInformativeText(self.informative) message_box.setDetailedText(self.detail) message_box.setEscapeButton(QtGui.QMessageBox.StandardButton.Ok) message_box.setTextFormat(_TEXT_FORMAT_MAP[self.text_format]) if self.size != (-1, -1): message_box.resize(*self.size) if self.position != (-1, -1): message_box.move(*self.position) return message_box pyface-7.4.0/pyface/ui/qt4/window.py0000644000076500000240000002103014176253531020212 0ustar cwebsterstaff00000000000000# (C) Copyright 2007 Riverbank Computing Limited # (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 weakref from pyface.qt import QtCore, QtGui from traits.api import ( Enum, Event, Property, Tuple, Str, VetoableEvent, provides, ) from pyface.i_window import IWindow, MWindow from pyface.key_pressed_event import KeyPressedEvent from .gui import GUI from .widget import Widget @provides(IWindow) class Window(MWindow, Widget): """ The toolkit specific implementation of a Window. See the IWindow interface for the API documentation. """ # 'IWindow' interface ----------------------------------------------------- position = Property(Tuple) size = Property(Tuple) size_state = Enum("normal", "maximized") title = Str() # Window Events ---------------------------------------------------------- #: The window has been opened. opened = Event() #: The window is about to open. opening = VetoableEvent() #: The window has been activated. activated = Event() #: The window has been closed. closed = Event() #: The window is about to be closed. closing = VetoableEvent() #: The window has been deactivated. deactivated = Event() # Private interface ------------------------------------------------------ #: Shadow trait for position. _position = Tuple((-1, -1)) #: Shadow trait for size. _size = Tuple((-1, -1)) # ------------------------------------------------------------------------- # 'IWindow' interface. # ------------------------------------------------------------------------- def activate(self): self.control.activateWindow() self.control.raise_() # explicitly fire activated trait as signal doesn't create Qt event self.activated = self # ------------------------------------------------------------------------- # Protected 'IWindow' interface. # ------------------------------------------------------------------------- def _create_control(self, parent): """ Create a default QMainWindow. """ control = QtGui.QMainWindow(parent) if self.size != (-1, -1): control.resize(*self.size) if self.position != (-1, -1): control.move(*self.position) if self.size_state != "normal": self._size_state_changed(self.size_state) control.setWindowTitle(self.title) control.setEnabled(self.enabled) # XXX starting with visible true is not recommended control.setVisible(self.visible) return control # ------------------------------------------------------------------------- # 'IWidget' interface. # ------------------------------------------------------------------------- def destroy(self): if self.control is not None: # Avoid problems with recursive calls. # Widget.destroy() sets self.control to None, # so we need a reference to control control = self.control # Widget.destroy() hides the widget, sets self.control to None # and deletes it later, so we call it before control.close() # This is not strictly necessary (closing the window in fact # hides it), but the close may trigger an application shutdown, # which can take a long time and may also attempt to recursively # destroy the window again. super().destroy() control.close() # ------------------------------------------------------------------------- # Private interface. # ------------------------------------------------------------------------- def _get_position(self): """ Property getter for position. """ return self._position def _set_position(self, position): """ Property setter for position. """ if self.control is not None: self.control.move(*position) old = self._position self._position = position self.trait_property_changed("position", old, position) def _get_size(self): """ Property getter for size. """ return self._size def _set_size(self, size): """ Property setter for size. """ if self.control is not None: self.control.resize(*size) old = self._size self._size = size self.trait_property_changed("size", old, size) def _size_state_changed(self, state): control = self.control if control is None: return # Nothing to do here if state == "maximized": control.setWindowState( control.windowState() | QtCore.Qt.WindowState.WindowMaximized ) elif state == "normal": control.setWindowState( control.windowState() & ~QtCore.Qt.WindowState.WindowMaximized ) def _title_changed(self, title): """ Static trait change handler. """ if self.control is not None: self.control.setWindowTitle(title) def __event_filter_default(self): return WindowEventFilter(self) class WindowEventFilter(QtCore.QObject): """ An internal class that watches for certain events on behalf of the Window instance. """ def __init__(self, window): """ Initialise the event filter. """ QtCore.QObject.__init__(self) # use a weakref to fix finalization issues with circular references # we don't want to be the last thing holding a reference to the window self._window = weakref.ref(window) def eventFilter(self, obj, e): """ Adds any event listeners required by the window. """ window = self._window() # Sanity check. if window is None or obj is not window.control: return False typ = e.type() if typ == QtCore.QEvent.Type.Close: # Do not destroy the window during its event handler. GUI.invoke_later(window.close) if window.control is not None: e.ignore() return True if typ == QtCore.QEvent.Type.WindowActivate: window.activated = window elif typ == QtCore.QEvent.Type.WindowDeactivate: window.deactivated = window elif typ in {QtCore.QEvent.Type.Show, QtCore.QEvent.Type.Hide}: window.visible = window.control.isVisible() elif typ == QtCore.QEvent.Type.Resize: # Get the new size and set the shadow trait without performing # notification. size = e.size() window._size = (size.width(), size.height()) elif typ == QtCore.QEvent.Type.Move: # Get the real position and set the trait without performing # notification. Don't use event.pos(), as this excludes the window # frame geometry. pos = window.control.pos() window._position = (pos.x(), pos.y()) elif typ == QtCore.QEvent.Type.KeyPress: # Pyface doesn't seem to be Str aware. Only keep the key code # if it corresponds to a single Latin1 character. kstr = e.text() try: kcode = ord(str(kstr)) except: kcode = 0 mods = e.modifiers() window.key_pressed = KeyPressedEvent( alt_down=( (mods & QtCore.Qt.KeyboardModifier.AltModifier) == QtCore.Qt.KeyboardModifier.AltModifier ), control_down=( (mods & QtCore.Qt.KeyboardModifier.ControlModifier) == QtCore.Qt.KeyboardModifier.ControlModifier ), shift_down=( (mods & QtCore.Qt.KeyboardModifier.ShiftModifier) == QtCore.Qt.KeyboardModifier.ShiftModifier ), key_code=kcode, event=e, ) elif typ == QtCore.QEvent.Type.WindowStateChange: # set the size_state of the window. state = obj.windowState() if state & QtCore.Qt.WindowState.WindowMaximized: window.size_state = "maximized" else: window.size_state = "normal" return False pyface-7.4.0/pyface/ui/qt4/image_resource.py0000644000076500000240000000567314176222673021716 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # (C) Copyright 2007 Riverbank Computing Limited # 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 import os from pyface.qt import QtGui from traits.api import Any, HasTraits, List, Property, provides from traits.api import Str from pyface.i_image_resource import IImageResource, MImageResource @provides(IImageResource) class ImageResource(MImageResource, HasTraits): """ The toolkit specific implementation of an ImageResource. See the IImageResource interface for the API documentation. """ # Private interface ---------------------------------------------------- # The resource manager reference for the image. _ref = Any() # 'ImageResource' interface -------------------------------------------- absolute_path = Property(Str) name = Str() search_path = List() # ------------------------------------------------------------------------ # 'IImage' interface. # ------------------------------------------------------------------------ # Qt doesn't specifically require bitmaps anywhere so just use images. create_bitmap = MImageResource.create_image def create_icon(self, size=None): ref = self._get_ref(size) if ref is not None: image = ref.load() else: image = self._get_image_not_found_image() return QtGui.QIcon(image) def image_size(cls, image): """ Get the size of a toolkit image Parameters ---------- image : toolkit image A toolkit image to compute the size of. Returns ------- size : tuple The (width, height) tuple giving the size of the image. """ size = image.size() return (size.width(), size.height()) # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _get_absolute_path(self): # FIXME: This doesn't quite work the new notion of image size. We # should find out who is actually using this trait, and for what! # (AboutDialog uses it to include the path name in some HTML.) ref = self._get_ref() if ref is not None: absolute_path = os.path.abspath(self._ref.filename) else: absolute_path = self._get_image_not_found().absolute_path return absolute_path pyface-7.4.0/pyface/ui/qt4/util/0000755000076500000240000000000014176460551017314 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/qt4/util/modal_dialog_tester.py0000644000076500000240000002720314176222673023674 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 class to facilitate testing components that use TraitsUI or Qt Dialogs. """ import contextlib import sys import traceback from pyface.api import GUI, OK, CANCEL, YES, NO from pyface.qt import QtCore, QtGui from traits.api import Undefined from .event_loop_helper import EventLoopHelper from .testing import find_qt_widget BUTTON_TEXT = {OK: "OK", CANCEL: "Cancel", YES: "Yes", NO: "No"} class ModalDialogTester(object): """ Test helper for code that open a traits ui or QDialog window. Usage ----- :: # Common usage calling a `function` that will open a dialog and then # accept the dialog info. tester = ModalDialogTester(function) tester.open_and_run(when_opened=lambda x: x.close(accept=True)) self.assertEqual(tester.result, ) # Even if the dialog was not opened upon calling `function`, # `result` is assigned and the test may not fail. # To test if the dialog was once opened: self.assertTrue(tester.dialog_was_opened) .. note:: - Proper operation assumes that at all times the dialog is a modal window. - Errors and failures during the when_opened call do not register with the unittest testcases because they take place on a deferred call in the event loop. It is advised that the `capture_error` context manager is used from the GuiTestAssistant when necessary. """ def __init__(self, function): #: The command to call that will cause a dialog to open. self.function = function self._assigned = False self._result = Undefined self._qt_app = QtGui.QApplication.instance() self._gui = GUI() self._event_loop_error = [] self._helper = EventLoopHelper(qt_app=self._qt_app, gui=self._gui) self._dialog_widget = None self.dialog_was_opened = False @property def result(self): """ The return value of the provided function. """ return self._result @result.setter def result(self, value): """ Setter methods for the result attribute. """ self._assigned = True self._result = value def open_and_run(self, when_opened, *args, **kwargs): """ Execute the function to open the dialog and run ``when_opened``. Parameters ---------- when_opened : callable A callable to be called when the dialog has been created and opened. The callable with be called with the tester instance as argument. *args, **kwargs : Additional arguments to be passed to the `function` attribute of the tester. Raises ------ AssertionError if an assertion error was captured during the deferred calls that open and close the dialog. RuntimeError if a result value has not been assigned within 15 seconds after calling `self.function` Any other exception that was captured during the deferred calls that open and close the dialog. .. note:: This method is synchronous """ condition_timer = QtCore.QTimer() def handler(): """ Run the when_opened as soon as the dialog has opened. """ if self.dialog_opened(): self._gui.invoke_later(when_opened, self) self.dialog_was_opened = True else: condition_timer.start() # Setup and start the timer to fire the handler every 100 msec. condition_timer.setInterval(100) condition_timer.setSingleShot(True) condition_timer.timeout.connect(handler) condition_timer.start() self._assigned = False try: # open the dialog on a deferred call. self._gui.invoke_later(self.open, *args, **kwargs) # wait in the event loop until timeout or a return value assigned. self._helper.event_loop_until_condition( condition=self.value_assigned, timeout=15 ) finally: condition_timer.stop() condition_timer.timeout.disconnect(handler) self._helper.event_loop() self.assert_no_errors_collected() def open_and_wait(self, when_opened, *args, **kwargs): """ Execute the function to open the dialog and wait to be closed. Parameters ---------- when_opened : callable A callable to be called when the dialog has been created and opened. The callable with be called with the tester instance as argument. *args, **kwargs : Additional arguments to be passed to the `function` attribute of the tester. Raises ------ AssertionError if an assertion error was captured during the deferred calls that open and close the dialog. RuntimeError if the dialog has not been closed within 15 seconds after calling `self.function`. Any other exception that was captured during the deferred calls that open and close the dialog. .. note:: This method is synchronous """ condition_timer = QtCore.QTimer() def handler(): """ Run the when_opened as soon as the dialog has opened. """ if self.dialog_opened(): self._dialog_widget = self.get_dialog_widget() self._gui.invoke_later(when_opened, self) self.dialog_was_opened = True else: condition_timer.start() def condition(): if self._dialog_widget is None: return False else: value = self.get_dialog_widget() != self._dialog_widget if value: # process any pending events so that we have a clean # event loop before we exit. self._helper.event_loop() return value # Setup and start the timer to signal the handler every 100 msec. condition_timer.setInterval(100) condition_timer.setSingleShot(True) condition_timer.timeout.connect(handler) condition_timer.start() self._assigned = False try: # open the dialog on a deferred call. self._gui.invoke_later(self.open, *args, **kwargs) # wait in the event loop until timeout or a return value assigned. self._helper.event_loop_until_condition( condition=condition, timeout=15 ) finally: condition_timer.stop() condition_timer.timeout.disconnect(handler) self._dialog_widget = None self._helper.event_loop() self.assert_no_errors_collected() def open(self, *args, **kwargs): """ Execute the function that will cause a dialog to be opened. Parameters ---------- *args, **kwargs : Arguments to be passed to the `function` attribute of the tester. .. note:: This method is synchronous """ with self.capture_error(): self.result = self.function(*args, **kwargs) def close(self, accept=False): """ Close the dialog by accepting or rejecting. """ with self.capture_error(): widget = self.get_dialog_widget() if accept: self._gui.invoke_later(widget.accept) else: self._gui.invoke_later(widget.reject) @contextlib.contextmanager def capture_error(self): """ Capture exceptions, to be used while running inside an event loop. When errors and failures take place through an invoke later command they might not be caught by the unittest machinery. This context manager when used inside a deferred call, will capture the fact that an error has occurred and the user can later use the `check for errors` command which will raise an error or failure if necessary. """ try: yield except Exception: self._event_loop_error.append( (sys.exc_info()[0], traceback.format_exc()) ) def assert_no_errors_collected(self): """ Assert that the tester has not collected any errors. """ if len(self._event_loop_error) > 0: msg = "The following error(s) were detected:\n\n{0}" tracebacks = [] for type_, message in self._event_loop_error: if isinstance(type_, AssertionError): msg = "The following failure(s) were detected:\n\n{0}" tracebacks.append(message) raise type_(msg.format("\n\n".join(tracebacks))) def click_widget(self, text, type_=QtGui.QPushButton): """ Execute click on the widget of `type_` with `text`. This strips '&' chars from the string, since usage varies from platform to platform. """ control = self.get_dialog_widget() def test(widget): # XXX asking for widget.text() causes occasional segfaults on Linux # and pyqt (both 4 and 5). Not sure why this is happening. # See issue #282 return widget.text().replace("&", "") == text widget = find_qt_widget(control, type_, test=test) if widget is None: # this will only occur if there is some problem with the test raise RuntimeError("Could not find matching child widget.") widget.click() def click_button(self, button_id): text = BUTTON_TEXT[button_id] self.click_widget(text) def value_assigned(self): """ A value was assigned to the result attribute. """ result = self._assigned if result: # process any pending events so that we have a clean # even loop before we exit. self._helper.event_loop() return result def dialog_opened(self): """ Check that the dialog has opened. """ dialog = self.get_dialog_widget() if dialog is None: return False if hasattr(dialog, "_ui"): # This is a traitsui dialog, we need one more check. ui = dialog._ui return ui.info.initialized else: # This is a simple QDialog. return dialog.isVisible() def get_dialog_widget(self): """ Get a reference to the active modal QDialog widget. """ # It might make sense to also check for active window and active popup # window if this Tester is used for non-modal windows. return self._qt_app.activeModalWidget() def has_widget(self, text=None, type_=QtGui.QPushButton): """ Return true if there is a widget of `type_` with `text`. """ if text is None: test = None else: test = lambda qwidget: qwidget.text() == text return self.find_qt_widget(type_=type_, test=test) is not None def find_qt_widget(self, type_=QtGui.QPushButton, test=None): """ Return the widget of `type_` for which `test` returns true. """ if test is None: test = lambda x: True window = self.get_dialog_widget() return find_qt_widget(window, type_, test=test) pyface-7.4.0/pyface/ui/qt4/util/event_loop_helper.py0000644000076500000240000001255314176222673023406 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 threading from pyface.i_gui import IGUI from pyface.qt import QtCore, QtGui from traits.api import HasStrictTraits, Instance class ConditionTimeoutError(RuntimeError): pass @contextlib.contextmanager def dont_quit_when_last_window_closed(qt_app): """ Suppress exit of the application when the last window is closed. """ flag = qt_app.quitOnLastWindowClosed() qt_app.setQuitOnLastWindowClosed(False) try: yield finally: qt_app.setQuitOnLastWindowClosed(flag) class EventLoopHelper(HasStrictTraits): qt_app = Instance(QtGui.QApplication) gui = Instance(IGUI) def event_loop_with_timeout(self, repeat=2, timeout=10.0): """Helper function to send all posted events to the event queue and wait for them to be processed. This runs the real event loop and does not emulate it with QApplication.processEvents. Parameters ---------- repeat : int Number of times to process events. Default is 2. timeout: float, optional, keyword only Number of seconds to run the event loop in the case that the trait change does not occur. Default value is 10.0. Notes ----- `timeout` is rounded to the nearest millisecond. """ def repeat_loop(condition, repeat): # We sendPostedEvents to ensure that enaml events are processed self.qt_app.sendPostedEvents() repeat = repeat - 1 if repeat <= 0: self.gui.invoke_later(condition.set) else: self.gui.invoke_later( repeat_loop, condition=condition, repeat=repeat ) condition = threading.Event() self.gui.invoke_later(repeat_loop, repeat=repeat, condition=condition) self.event_loop_until_condition( condition=condition.is_set, timeout=timeout ) def event_loop(self, repeat=1): """Emulates an event loop `repeat` times with QApplication.processEvents. Parameters ---------- repeat : int Number of times to process events. Default is 1. """ for i in range(repeat): self.qt_app.sendPostedEvents() self.qt_app.processEvents() def event_loop_until_condition(self, condition, timeout=10.0): """Runs the real Qt event loop until the provided condition evaluates to True. Raises ConditionTimeoutError if the timeout occurs before the condition is satisfied. Parameters ---------- condition : callable A callable to determine if the stop criteria have been met. This should accept no arguments. timeout : float Number of seconds to run the event loop in the case that the trait change does not occur. Notes ----- `timeout` is rounded to the nearest millisecond. """ def handler(): if condition(): self.qt_app.quit() # Make sure we don't get a premature exit from the event loop. with dont_quit_when_last_window_closed(self.qt_app): condition_timer = QtCore.QTimer() condition_timer.setInterval(50) condition_timer.timeout.connect(handler) timeout_timer = QtCore.QTimer() timeout_timer.setSingleShot(True) timeout_timer.setInterval(round(timeout * 1000)) timeout_timer.timeout.connect(self.qt_app.quit) timeout_timer.start() condition_timer.start() try: self.qt_app.exec_() if not condition(): raise ConditionTimeoutError( "Timed out waiting for condition" ) finally: timeout_timer.stop() condition_timer.stop() @contextlib.contextmanager def delete_widget(self, widget, timeout=1.0): """Runs the real Qt event loop until the widget provided has been deleted. Raises ConditionTimeoutError on timeout. Parameters ---------- widget : QObject The widget whose deletion will stop the event loop. timeout : float Number of seconds to run the event loop in the case that the widget is not deleted. Notes ----- `timeout` is rounded to the nearest millisecond. """ timer = QtCore.QTimer() timer.setSingleShot(True) timer.setInterval(round(timeout * 1000)) timer.timeout.connect(self.qt_app.quit) widget.destroyed.connect(self.qt_app.quit) yield timer.start() self.qt_app.exec_() if not timer.isActive(): # We exited the event loop on timeout raise ConditionTimeoutError( "Could not destroy widget before timeout: {!r}".format(widget) ) pyface-7.4.0/pyface/ui/qt4/util/image_helpers.py0000644000076500000240000001116714176253531022476 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Helper functions for working with images This module provides helper functions for converting between numpy arrays and Qt QImages, as well as between the various image types in a standardized way. """ from enum import Enum from pyface.qt import qt_api from pyface.qt.QtCore import Qt from pyface.qt.QtGui import QImage, QPixmap, QIcon class ScaleMode(Enum): fast = Qt.TransformationMode.FastTransformation smooth = Qt.TransformationMode.SmoothTransformation class AspectRatio(Enum): ignore = Qt.AspectRatioMode.IgnoreAspectRatio keep_constrain = Qt.AspectRatioMode.KeepAspectRatio keep_expand = Qt.AspectRatioMode.KeepAspectRatioByExpanding def image_to_bitmap(image): """ Convert a QImage to a QPixmap. Parameters ---------- image : QImage The QImage to convert. Return ------ bitmap : QPixmap The corresponding QPixmap. """ return QPixmap.fromImage(image) def bitmap_to_image(bitmap): """ Convert a QPixmap to a QImage. Parameters ---------- bitmap : QPixmap The QPixmap to convert. Return ------ image : QImage The corresponding QImage. """ return bitmap.toImage() def bitmap_to_icon(bitmap): """ Convert a QPixmap to a QIcon. Parameters ---------- bitmap : QPixmap The QPixmap to convert. Return ------ icon : QIcon The corresponding QIcon. """ return QIcon(bitmap) def resize_image(image, size, aspect_ratio=AspectRatio.ignore, mode=ScaleMode.fast): """ Resize a toolkit image to the given size. """ return image.scaled(*size, aspect_ratio.value, mode.value) def resize_bitmap(bitmap, size, aspect_ratio=AspectRatio.ignore, mode=ScaleMode.fast): """ Resize a toolkit bitmap to the given size. """ return bitmap.scaled(*size, aspect_ratio.value, mode.value) def image_to_array(image): """ Convert a QImage to a numpy array. This copies the data returned from Qt. Parameters ---------- image : QImage The QImage that we want to extract the values from. The format must be either RGB32 or ARGB32. Return ------ array : ndarray An N x M x 4 array of unsigned 8-bit ints as RGBA values. """ import numpy as np width, height = image.width(), image.height() channels = image.pixelFormat().channelCount() data = image.bits() if qt_api in {'pyqt', 'pyqt5'}: data = data.asarray(width * height * channels) array = np.array(data, dtype='uint8') array.shape = (height, width, channels) if image.format() in {QImage.Format.Format_RGB32, QImage.Format.Format_ARGB32}: # comes in as BGRA, but want RGBA array = array[:, :, [2, 1, 0, 3]] else: raise ValueError( "Unsupported QImage format {}".format(image.format()) ) return array def array_to_image(array): """ Convert a numpy array to a QImage. This copies the data before passing it to Qt. Parameters ---------- array : ndarray An N x M x {3, 4} array of unsigned 8-bit ints. The image format is assumed to be RGB or RGBA, based on the shape. Return ------ image : QImage The QImage created from the data. The pixel format is QImage.Format.Format_RGB32. """ import numpy as np if array.ndim != 3: raise ValueError("Array must be either RGB or RGBA values.") height, width, channels = array.shape data = np.empty((height, width, 4), dtype='uint8') if channels == 3: data[:, :, [2, 1, 0]] = array data[:, :, 3] = 0xff elif channels == 4: data[:, :, [2, 1, 0, 3]] = array else: raise ValueError("Array must be either RGB or RGBA values.") bytes_per_line = 4 * width if channels == 3: image = QImage(data.data, width, height, bytes_per_line, QImage.Format.Format_RGB32) elif channels == 4: image = QImage(data.data, width, height, bytes_per_line, QImage.Format.Format_ARGB32) image._numpy_data = data return image # backwards compatible names - will be removed in Pyface 8 array_to_QImage = array_to_image QImage_to_array = image_to_array pyface-7.4.0/pyface/ui/qt4/util/tests/0000755000076500000240000000000014176460551020456 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/qt4/util/tests/test_modal_dialog_tester.py0000644000076500000240000001362614176222673026101 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 tabular editor tester. """ import unittest from io import StringIO from pyface.qt import QtGui, is_qt6 from pyface.api import Dialog, MessageDialog, OK, CANCEL from traits.api import HasStrictTraits from pyface.ui.qt4.util.testing import silence_output from pyface.ui.qt4.util.gui_test_assistant import GuiTestAssistant from pyface.ui.qt4.util.modal_dialog_tester import ModalDialogTester from pyface.util.testing import skip_if_no_traitsui class MyClass(HasStrictTraits): def default_traits_view(self): from traitsui.api import CancelButton, OKButton, View view = View( buttons=[OKButton, CancelButton], resizable=False, title="My class dialog", ) return view def run(self): ui = self.edit_traits(kind="livemodal") if ui.result: return "accepted" else: return "rejected" def do_not_show_dialog(self): return True class TestModalDialogTester(GuiTestAssistant, unittest.TestCase): """ Tests for the modal dialog tester. """ # Tests ---------------------------------------------------------------- def test_on_message_dialog(self): dialog = MessageDialog() tester = ModalDialogTester(dialog.open) # accept tester.open_and_run(when_opened=lambda x: x.close(accept=True)) self.assertTrue(tester.value_assigned()) self.assertEqual(tester.result, OK) self.assertTrue(tester.dialog_was_opened) # reject tester.open_and_run(when_opened=lambda x: x.close()) self.assertTrue(tester.value_assigned()) self.assertEqual(tester.result, CANCEL) self.assertTrue(tester.dialog_was_opened) @skip_if_no_traitsui def test_on_traitsui_dialog(self): my_class = MyClass() tester = ModalDialogTester(my_class.run) # accept tester.open_and_run(when_opened=lambda x: x.close(accept=True)) self.assertTrue(tester.value_assigned()) self.assertEqual(tester.result, "accepted") self.assertTrue(tester.dialog_was_opened) # reject tester.open_and_run(when_opened=lambda x: x.close()) self.assertTrue(tester.value_assigned()) self.assertEqual(tester.result, "rejected") self.assertTrue(tester.dialog_was_opened) @skip_if_no_traitsui def test_dialog_was_not_opened_on_traitsui_dialog(self): my_class = MyClass() tester = ModalDialogTester(my_class.do_not_show_dialog) # it runs okay tester.open_and_run(when_opened=lambda x: x.close(accept=True)) self.assertTrue(tester.value_assigned()) self.assertEqual(tester.result, True) # but no dialog is opened self.assertFalse(tester.dialog_was_opened) @unittest.skipIf(is_qt6, "TEMPORARY: getting tests to run on pyside6") def test_capture_errors_on_failure(self): dialog = MessageDialog() tester = ModalDialogTester(dialog.open) def failure(tester): try: with tester.capture_error(): # this failure will appear in the console and get recorded self.fail() finally: tester.close() self.gui.process_events() with self.assertRaises(AssertionError): alt_stderr = StringIO with silence_output(err=alt_stderr): tester.open_and_run(when_opened=failure) self.assertIn("raise self.failureException(msg)", alt_stderr) @unittest.skipIf(is_qt6, "TEMPORARY: getting tests to run on pyside6") def test_capture_errors_on_error(self): dialog = MessageDialog() tester = ModalDialogTester(dialog.open) def raise_error(tester): try: with tester.capture_error(): # this error will appear in the console and get recorded 1 / 0 finally: tester.close() self.gui.process_events() with self.assertRaises(ZeroDivisionError): alt_stderr = StringIO with silence_output(err=alt_stderr): tester.open_and_run(when_opened=raise_error) self.assertIn("ZeroDivisionError", alt_stderr) @unittest.skip("has_widget code not working as designed. Issue #282.") def test_has_widget(self): dialog = Dialog() tester = ModalDialogTester(dialog.open) def check_and_close(tester): try: with tester.capture_error(): self.assertTrue( tester.has_widget("OK", QtGui.QAbstractButton) ) self.assertFalse( tester.has_widget(text="I am a virtual button") ) finally: tester.close() tester.open_and_run(when_opened=check_and_close) @unittest.skip("has_widget code not working as designed. Issue #282.") def test_find_widget(self): dialog = Dialog() tester = ModalDialogTester(dialog.open) def check_and_close(tester): try: with tester.capture_error(): widget = tester.find_qt_widget( type_=QtGui.QAbstractButton, test=lambda x: x.text() == "OK", ) self.assertIsInstance(widget, QtGui.QPushButton) finally: tester.close() tester.open_and_run(when_opened=check_and_close) pyface-7.4.0/pyface/ui/qt4/util/tests/__init__.py0000644000076500000240000000000014176222673022556 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/qt4/util/tests/test_image_helpers.py0000644000076500000240000001630414176253531024675 0ustar cwebsterstaff00000000000000# Copyright (c) 2005-2022, Enthought Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only # under the conditions described 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 sys from traits.testing.optional_dependencies import numpy as np, requires_numpy from pyface.qt import qt_api from pyface.qt.QtGui import QColor, QIcon, QImage, QPixmap from ..image_helpers import ( bitmap_to_icon, bitmap_to_image, image_to_array, image_to_bitmap, array_to_image, AspectRatio, ScaleMode, resize_image, resize_bitmap, ) class TestImageHelpers(unittest.TestCase): def test_image_to_bitmap(self): qimage = QImage(32, 64, QImage.Format.Format_RGB32) qimage.fill(QColor(0x44, 0x88, 0xcc)) qpixmap = image_to_bitmap(qimage) self.assertIsInstance(qpixmap, QPixmap) def test_bitmap_to_image(self): qpixmap = QPixmap(32, 64) qpixmap.fill(QColor(0x44, 0x88, 0xcc)) qimage = bitmap_to_image(qpixmap) self.assertIsInstance(qimage, QImage) def test_bitmap_to_icon(self): qpixmap = QPixmap(32, 64) qpixmap.fill(QColor(0x44, 0x88, 0xcc)) qimage = bitmap_to_icon(qpixmap) self.assertIsInstance(qimage, QIcon) def test_resize_image(self): qimage = QImage(32, 64, QImage.Format.Format_RGB32) qimage.fill(QColor(0x44, 0x88, 0xcc)) qimage = resize_image(qimage, (128, 128)) self.assertIsInstance(qimage, QImage) self.assertEqual(qimage.width(), 128) self.assertEqual(qimage.height(), 128) def test_resize_image_smooth(self): qimage = QImage(32, 64, QImage.Format.Format_RGB32) qimage.fill(QColor(0x44, 0x88, 0xcc)) qimage = resize_image(qimage, (128, 128), mode=ScaleMode.smooth) self.assertIsInstance(qimage, QImage) self.assertEqual(qimage.width(), 128) self.assertEqual(qimage.height(), 128) def test_resize_image_constrain(self): qimage = QImage(32, 64, QImage.Format.Format_RGB32) qimage.fill(QColor(0x44, 0x88, 0xcc)) qimage = resize_image(qimage, (128, 128), AspectRatio.keep_constrain) self.assertIsInstance(qimage, QImage) self.assertEqual(qimage.width(), 64) self.assertEqual(qimage.height(), 128) def test_resize_image_expand(self): qimage = QImage(32, 64, QImage.Format.Format_RGB32) qimage.fill(QColor(0x44, 0x88, 0xcc)) qimage = resize_image(qimage, (128, 128), AspectRatio.keep_expand) self.assertIsInstance(qimage, QImage) self.assertEqual(qimage.width(), 128) self.assertEqual(qimage.height(), 256) def test_resize_bitmap(self): qpixmap = QPixmap(32, 64) qpixmap.fill(QColor(0x44, 0x88, 0xcc)) qpixmap = resize_bitmap(qpixmap, (128, 128)) self.assertIsInstance(qpixmap, QPixmap) self.assertEqual(qpixmap.width(), 128) self.assertEqual(qpixmap.height(), 128) def test_resize_bitmap_smooth(self): qpixmap = QPixmap(32, 64) qpixmap.fill(QColor(0x44, 0x88, 0xcc)) qpixmap = resize_bitmap(qpixmap, (128, 128), mode=ScaleMode.smooth) self.assertIsInstance(qpixmap, QPixmap) self.assertEqual(qpixmap.width(), 128) self.assertEqual(qpixmap.height(), 128) def test_resize_bitmap_constrain(self): qpixmap = QPixmap(32, 64) qpixmap.fill(QColor(0x44, 0x88, 0xcc)) qpixmap = resize_bitmap(qpixmap, (128, 128), AspectRatio.keep_constrain) self.assertIsInstance(qpixmap, QPixmap) self.assertEqual(qpixmap.width(), 64) self.assertEqual(qpixmap.height(), 128) def test_resize_bitmap_expand(self): qpixmap = QPixmap(32, 64) qpixmap.fill(QColor(0x44, 0x88, 0xcc)) qpixmap = resize_bitmap(qpixmap, (128, 128), AspectRatio.keep_expand) self.assertIsInstance(qpixmap, QPixmap) self.assertEqual(qpixmap.width(), 128) self.assertEqual(qpixmap.height(), 256) @requires_numpy class TestArrayImageHelpers(unittest.TestCase): def test_image_to_array_rgb(self): qimage = QImage(32, 64, QImage.Format.Format_RGB32) qimage.fill(QColor(0x44, 0x88, 0xcc)) array = image_to_array(qimage) self.assertEqual(array.shape, (64, 32, 4)) self.assertEqual(array.dtype, np.dtype('uint8')) self.assertTrue(np.all(array[:, :, 3] == 0xff)) self.assertTrue(np.all(array[:, :, 0] == 0x44)) self.assertTrue(np.all(array[:, :, 1] == 0x88)) self.assertTrue(np.all(array[:, :, 2] == 0xcc)) def test_image_to_array_rgba(self): qimage = QImage(32, 64, QImage.Format.Format_ARGB32) qimage.fill(QColor(0x44, 0x88, 0xcc, 0xee)) array = image_to_array(qimage) self.assertEqual(array.shape, (64, 32, 4)) self.assertEqual(array.dtype, np.dtype('uint8')) self.assertTrue(np.all(array[:, :, 0] == 0x44)) self.assertTrue(np.all(array[:, :, 1] == 0x88)) self.assertTrue(np.all(array[:, :, 2] == 0xcc)) self.assertTrue(np.all(array[:, :, 3] == 0xee)) def test_image_to_array_bad(self): qimage = QImage(32, 64, QImage.Format.Format_RGB30) qimage.fill(QColor(0x44, 0x88, 0xcc)) with self.assertRaises(ValueError): image_to_array(qimage) @unittest.skipIf( qt_api == 'pyside2' and sys.platform == 'linux', "Pyside2 QImage.pixel returns signed integers on linux" ) def test_array_to_image_rgb(self): array = np.empty((64, 32, 3), dtype='uint8') array[:, :, 0] = 0x44 array[:, :, 1] = 0x88 array[:, :, 2] = 0xcc qimage = array_to_image(array) self.assertEqual(qimage.width(), 32) self.assertEqual(qimage.height(), 64) self.assertEqual(qimage.format(), QImage.Format.Format_RGB32) self.assertTrue(all( qimage.pixel(i, j) == 0xff4488cc for i in range(32) for j in range(64) )) @unittest.skipIf( qt_api == 'pyside2' and sys.platform == 'linux', "Pyside2 QImage.pixel returns signed integers on linux" ) def test_array_to_image_rgba(self): array = np.empty((64, 32, 4), dtype='uint8') array[:, :, 0] = 0x44 array[:, :, 1] = 0x88 array[:, :, 2] = 0xcc array[:, :, 3] = 0xee qimage = array_to_image(array) self.assertEqual(qimage.width(), 32) self.assertEqual(qimage.height(), 64) self.assertEqual(qimage.format(), QImage.Format.Format_ARGB32) self.assertTrue(all( qimage.pixel(i, j) == 0xee4488cc for i in range(32) for j in range(64) )) def test_array_to_image_bad_channels(self): array = np.empty((64, 32, 2), dtype='uint8') array[:, :, 0] = 0x44 array[:, :, 1] = 0x88 with self.assertRaises(ValueError): array_to_image(array) def test_array_to_image_bad_ndim(self): array = np.full((64, 32), 0x44, dtype='uint8') with self.assertRaises(ValueError): array_to_image(array) pyface-7.4.0/pyface/ui/qt4/util/tests/test_datetime.py0000644000076500000240000000232414176222673023665 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 Qt dates and times. """ import datetime import unittest from pyface.qt.QtCore import QTime from ..datetime import pytime_to_qtime, qtime_to_pytime class TestTimeConversion(unittest.TestCase): def test_pytime_to_qtime(self): pytime = datetime.time(9, 8, 7, 123456) qtime = pytime_to_qtime(pytime) self.assertEqual(qtime.hour(), 9) self.assertEqual(qtime.minute(), 8) self.assertEqual(qtime.second(), 7) self.assertEqual(qtime.msec(), 123) def test_qtime_to_pytime(self): qtime = QTime(9, 8, 7, 123) pytime = qtime_to_pytime(qtime) self.assertEqual(pytime.hour, 9) self.assertEqual(pytime.minute, 8) self.assertEqual(pytime.second, 7) self.assertEqual(pytime.microsecond, 123000) pyface-7.4.0/pyface/ui/qt4/util/tests/test_gui_test_assistant.py0000644000076500000240000000661514176222673026014 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 pyface.ui.qt4.util.gui_test_assistant import GuiTestAssistant from traits.api import Event, HasStrictTraits class ExampleObject(HasStrictTraits): """ Test class; target for test_event_loop_until_traits_change. """ spam = Event() eggs = Event() class TestGuiTestAssistant(GuiTestAssistant, unittest.TestCase): def test_event_loop_until_traits_change_with_single_trait_success(self): # event_loop_until_traits_change calls self.fail on timeout. obj = ExampleObject() # Successful case. with self.event_loop_until_traits_change(obj, "spam"): obj.spam = True def test_event_loop_until_traits_change_with_single_trait_failure(self): # event_loop_until_traits_change calls self.fail on timeout. obj = ExampleObject() # Failing case. with self.assertRaises(AssertionError): with self.event_loop_until_traits_change(obj, "spam", timeout=0.1): obj.eggs = True def test_event_loop_until_traits_change_with_multiple_traits_success(self): # event_loop_until_traits_change calls self.fail on timeout. obj = ExampleObject() with self.event_loop_until_traits_change(obj, "spam", "eggs"): obj.spam = True obj.eggs = True def test_event_loop_until_traits_change_with_multiple_traits_failure(self): # event_loop_until_traits_change calls self.fail on timeout. obj = ExampleObject() with self.assertRaises(AssertionError): with self.event_loop_until_traits_change( obj, "spam", "eggs", timeout=0.1 ): obj.eggs = True # event_loop_until_traits_change calls self.fail on timeout. with self.assertRaises(AssertionError): with self.event_loop_until_traits_change( obj, "spam", "eggs", timeout=0.1 ): obj.spam = True def test_event_loop_until_traits_change_with_no_traits_success(self): # event_loop_until_traits_change calls self.fail on timeout. obj = ExampleObject() # Successful case. with self.event_loop_until_traits_change(obj): pass def test_assert_eventually_true_in_gui_success(self): my_list = [] timer = CallbackTimer( interval=0.05, callback=my_list.append, args=("bob",), repeat=1 ) timer.start() try: self.assertEventuallyTrueInGui(lambda: len(my_list) > 0) self.assertEqual(my_list, ["bob"]) finally: timer.stop() def test_assert_eventually_true_in_gui_already_true(self): my_list = ["bob"] self.assertEventuallyTrueInGui(lambda: len(my_list) > 0) def test_assert_eventually_true_in_gui_failure(self): my_list = [] with self.assertRaises(AssertionError): self.assertEventuallyTrueInGui( lambda: len(my_list) > 0, timeout=0.1 ) pyface-7.4.0/pyface/ui/qt4/util/tests/test_event_loop_helper.py0000644000076500000240000000161414176222673025603 0ustar cwebsterstaff00000000000000# Copyright (c) 2005-2022, Enthought Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only # under the conditions described 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, provides from pyface.i_gui import IGUI from pyface.ui.qt4.util.event_loop_helper import EventLoopHelper @provides(IGUI) class DummyGUI(HasTraits): pass class TestEventLoopHelper(unittest.TestCase): def test_gui_trait_expects_IGUI_interface(self): # Trivial test where we simply set the trait # and the test passes because no errors are raised. event_loop_helper = EventLoopHelper() event_loop_helper.gui = DummyGUI() pyface-7.4.0/pyface/ui/qt4/util/__init__.py0000644000076500000240000000000014176222673021414 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/qt4/util/gui_test_assistant.py0000644000076500000240000002754714176222673023622 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 gc import threading import unittest.mock as mock from pyface.qt.QtGui import QApplication from pyface.ui.qt4.gui import GUI from traits.testing.api import UnittestTools from traits.testing.unittest_tools import ( _TraitsChangeCollector as TraitsChangeCollector, ) from .testing import find_qt_widget from .event_loop_helper import EventLoopHelper, ConditionTimeoutError class GuiTestAssistant(UnittestTools): # 'TestCase' protocol -------------------------------------------------# def setUp(self): qt_app = QApplication.instance() if qt_app is None: qt_app = QApplication([]) self.qt_app = qt_app self.gui = GUI() self.event_loop_helper = EventLoopHelper( qt_app=self.qt_app, gui=self.gui ) try: import traitsui.api # noqa: F401 except ImportError: self.traitsui_raise_patch = None else: self.traitsui_raise_patch = mock.patch( "traitsui.qt4.ui_base._StickyDialog.raise_" ) self.traitsui_raise_patch.start() def new_activate(self): self.control.activateWindow() self.pyface_raise_patch = mock.patch( "pyface.ui.qt4.window.Window.activate", new_callable=lambda: new_activate, ) self.pyface_raise_patch.start() def tearDown(self): # Process any tasks that a misbehaving test might have left on the # queue. with self.event_loop_with_timeout(repeat=5): pass # Some top-level widgets may only be present due to cyclic garbage not # having been collected; force a garbage collection before we decide to # close windows. This may need several rounds. for _ in range(10): if not gc.collect(): break if len(self.qt_app.topLevelWidgets()) > 0: with self.event_loop_with_timeout(repeat=5): self.gui.invoke_later(self.qt_app.closeAllWindows) self.pyface_raise_patch.stop() if self.traitsui_raise_patch is not None: self.traitsui_raise_patch.stop() del self.pyface_raise_patch del self.traitsui_raise_patch del self.event_loop_helper del self.gui del self.qt_app # 'GuiTestAssistant' protocol -----------------------------------------# @contextlib.contextmanager def event_loop(self, repeat=1): """Artificially replicate the event loop by Calling sendPostedEvents and processEvents ``repeat`` number of times. If the events to be processed place more events in the queue, begin increasing the value of ``repeat``, or consider using ``event_loop_until_condition`` instead. Parameters ---------- repeat : int Number of times to process events. """ yield self.event_loop_helper.event_loop(repeat=repeat) @contextlib.contextmanager def delete_widget(self, widget, timeout=1.0): """Runs the real Qt event loop until the widget provided has been deleted. Parameters ---------- widget : QObject The widget whose deletion will stop the event loop. timeout : float Number of seconds to run the event loop in the case that the widget is not deleted. """ try: with self.event_loop_helper.delete_widget(widget, timeout=timeout): yield except ConditionTimeoutError: self.fail( "Could not destroy widget before timeout: {!r}".format(widget) ) @contextlib.contextmanager def event_loop_until_condition(self, condition, timeout=10.0): """Runs the real Qt event loop until the provided condition evaluates to True. This should not be used to wait for widget deletion. Use delete_widget() instead. Notes ----- This runs the real Qt event loop, polling the condition every 50 ms and returning as soon as the condition becomes true. If the condition does not become true within the given timeout, a ConditionTimeoutError is raised. Because the state of the condition is only polled every 50 ms, it may fail to detect transient states that appear and disappear within a 50 ms window. Code should ensure that any state that is being tested by the condition cannot revert to a False value once it becomes True. Parameters ---------- condition : callable A callable to determine if the stop criteria have been met. This should accept no arguments. timeout : float Number of seconds to run the event loop in the case that the condition is not satisfied. """ try: yield self.event_loop_helper.event_loop_until_condition( condition, timeout=timeout ) except ConditionTimeoutError: self.fail("Timed out waiting for condition") def assertEventuallyTrueInGui(self, condition, timeout=10.0): """ Assert that the given condition becomes true if we run the GUI event loop for long enough. Notes ----- This assertion runs the real Qt event loop, polling the condition every 50 ms and returning as soon as the condition becomes true. If the condition does not become true within the given timeout, the assertion fails. Because the state of the condition is only polled every 50 ms, it may fail to detect transient states that appear and disappear within a 50 ms window. Tests should ensure that any state that is being tested by the condition cannot revert to a False value once it becomes True. Parameters ---------- condition : callable() -> bool Callable accepting no arguments and returning a bool. timeout : float Maximum length of time to wait for the condition to become true, in seconds. Raises ------ self.failureException If the condition does not become true within the given timeout. """ try: self.event_loop_helper.event_loop_until_condition( condition, timeout=timeout ) except ConditionTimeoutError: self.fail("Timed out waiting for condition to become true.") @contextlib.contextmanager def assertTraitChangesInEventLoop( self, obj, trait, condition, count=1, timeout=10.0 ): """Runs the real Qt event loop, collecting trait change events until the provided condition evaluates to True. Parameters ---------- obj : HasTraits The HasTraits instance whose trait will change. trait : str The extended trait name of trait changes to listen to. condition : callable A callable to determine if the stop criteria have been met. This takes obj as the only argument. count : int The expected number of times the event should be fired. The default is to expect one event. timeout : float Number of seconds to run the event loop in the case that the trait change does not occur. """ condition_ = lambda: condition(obj) collector = TraitsChangeCollector(obj=obj, trait_name=trait) collector.start_collecting() try: try: yield collector self.event_loop_helper.event_loop_until_condition( condition_, timeout=timeout ) except ConditionTimeoutError: actual_event_count = collector.event_count msg = ( "Expected {} event on {} to be fired at least {} " "times, but the event was only fired {} times " "before timeout ({} seconds)." ) msg = msg.format( trait, obj, count, actual_event_count, timeout ) self.fail(msg) finally: collector.stop_collecting() @contextlib.contextmanager def event_loop_until_traits_change(self, traits_object, *traits, **kw): """Run the real application event loop until a change notification for all of the specified traits is received. Paramaters ---------- traits_object : HasTraits instance The object on which to listen for a trait events traits : one or more str The names of the traits to listen to for events timeout : float, optional, keyword only Number of seconds to run the event loop in the case that the trait change does not occur. Default value is 10.0. """ timeout = kw.pop("timeout", 10.0) condition = threading.Event() traits = set(traits) recorded_changes = set() # Correctly handle the corner case where there are no traits. if not traits: condition.set() def set_event(trait): recorded_changes.add(trait) if recorded_changes == traits: condition.set() def make_handler(trait): def handler(event): set_event(trait) return handler handlers = {trait: make_handler(trait) for trait in traits} for trait, handler in handlers.items(): traits_object.observe(handler, trait) try: with self.event_loop_until_condition( condition=condition.is_set, timeout=timeout ): yield finally: for trait, handler in handlers.items(): traits_object.observe(handler, trait, remove=True) @contextlib.contextmanager def event_loop_with_timeout(self, repeat=2, timeout=10.0): """Helper context manager to send all posted events to the event queue and wait for them to be processed. This differs from the `event_loop()` context manager in that it starts the real event loop rather than emulating it with `QApplication.processEvents()` Parameters ---------- repeat : int Number of times to process events. Default is 2. timeout : float, optional, keyword only Number of seconds to run the event loop in the case that the trait change does not occur. Default value is 10.0. """ yield self.event_loop_helper.event_loop_with_timeout( repeat=repeat, timeout=timeout ) def find_qt_widget(self, start, type_, test=None): """Recursively walks the Qt widget tree from Qt widget `start` until it finds a widget of type `type_` (a QWidget subclass) that satisfies the provided `test` method. Parameters ---------- start : QWidget The widget from which to start walking the tree type_ : type A subclass of QWidget to use for an initial type filter while walking the tree test : callable A filter function that takes one argument (the current widget being evaluated) and returns either True or False to determine if the widget matches the required criteria. """ return find_qt_widget(start, type_, test=test) pyface-7.4.0/pyface/ui/qt4/util/testing.py0000644000076500000240000000730714176222673021353 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Tools for testing. """ from contextlib import contextmanager import os import sys from pyface.qt.QtCore import QTimer from pyface.util.guisupport import get_app_qt4 @contextmanager def event_loop(): """ Post and process the Qt events at the exit of the code block. """ app = get_app_qt4() yield app.sendPostedEvents() app.processEvents() @contextmanager def delete_widget(widget, timeout=1.0): """ Context manager that executes the event loop so that we can safely delete a Qt widget. """ app = get_app_qt4() timer = QTimer() timer.setSingleShot(True) timer.setInterval(round(timeout * 1000)) timer.timeout.connect(app.quit) widget.destroyed.connect(app.quit) yield timer.start() app.exec_() if not timer.isActive(): # We exited the event loop on timeout. msg = "Could not destroy widget before timeout: {!r}" raise AssertionError(msg.format(widget)) @contextmanager def _convert_none_to_null_handle(stream): """ If 'stream' is None, provide a temporary handle to /dev/null. """ if stream is None: out = open(os.devnull, "w") try: yield out finally: out.close() else: yield stream @contextmanager def silence_output(out=None, err=None): """ Re-direct the stderr and stdout streams while in the block. """ with _convert_none_to_null_handle(out) as out: with _convert_none_to_null_handle(err) as err: _old_stderr = sys.stderr _old_stderr.flush() _old_stdout = sys.stdout _old_stdout.flush() try: sys.stdout = out sys.stderr = err yield finally: sys.stdout = _old_stdout sys.stderr = _old_stderr def print_qt_widget_tree(widget, level=0): """ Debugging helper to print out the Qt widget tree starting at a particular `widget`. Parameters ---------- widget : QObject The root widget in the tree to print. level : int The current level in the tree. Used internally for displaying the tree level. """ level = level + 4 if level == 0: print() print(" " * level, widget) for child in widget.children(): print_qt_widget_tree(child, level=level) if level == 0: print() def find_qt_widget(start, type_, test=None): """Recursively walks the Qt widget tree from Qt widget `start` until it finds a widget of type `type_` (a QWidget subclass) that satisfies the provided `test` method. Parameters ---------- start : QWidget The widget from which to start walking the tree type_ : type A subclass of QWidget to use for an initial type filter while walking the tree test : callable A filter function that takes one argument (the current widget being evaluated) and returns either True or False to determine if the widget matches the required criteria. """ if test is None: test = lambda widget: True if isinstance(start, type_): if test(start): return start for child in start.children(): widget = find_qt_widget(child, type_, test=test) if widget: return widget return None pyface-7.4.0/pyface/ui/qt4/util/datetime.py0000644000076500000240000000272214176222673021466 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 Qt dates and times. """ from pyface.qt import is_pyqt from pyface.qt.QtCore import QTime def qtime_to_pytime(qtime): """ Convert a QTime to a Python datetime.time This is needed to paper over the differences between PyQt and PySide. Parameters ---------- qtime : QTime The Qt QTime to convert Returns ------- pytime : datetime.time The corresponding datetime.time. """ if is_pyqt: return qtime.toPyTime() else: return qtime.toPython() def pytime_to_qtime(pytime): """ Convert a Python datetime.time to a QTime This is a convenience function to construct a qtime from a Python time. This loses any :attr:`fold` information in the Python time. Parameters ---------- pytime : datetime.time The datetime.time to convert Returns ------- qtime : QTime The corresponding Qt QTime. """ return QTime( pytime.hour, pytime.minute, pytime.second, pytime.microsecond // 1000 ) pyface-7.4.0/pyface/ui/qt4/system_metrics.py0000644000076500000240000000507714176253531021772 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # (C) Copyright 2007 Riverbank Computing Limited # 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 from pyface.qt import QtGui, is_qt4 from traits.api import HasTraits, Int, Property, provides, Tuple from pyface.i_system_metrics import ISystemMetrics, MSystemMetrics @provides(ISystemMetrics) class SystemMetrics(MSystemMetrics, HasTraits): """ The toolkit specific implementation of a SystemMetrics. See the ISystemMetrics interface for the API documentation. """ # 'ISystemMetrics' interface ------------------------------------------- screen_width = Property(Int) screen_height = Property(Int) dialog_background_color = Property(Tuple) # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _get_screen_width(self): # QDesktopWidget.screenGeometry() is deprecated and Qt docs # suggest using screens() instead, but screens in not available in qt4 # see issue: enthought/pyface#721 if not is_qt4: return QtGui.QApplication.instance().screens()[0].availableGeometry().width() else: return QtGui.QApplication.instance().desktop().availableGeometry().width() def _get_screen_height(self): # QDesktopWidget.screenGeometry(int screen) is deprecated and Qt docs # suggest using screens() instead, but screens in not available in qt4 # see issue: enthought/pyface#721 if not is_qt4: return ( QtGui.QApplication.instance().screens()[0].availableGeometry().height() ) else: return ( QtGui.QApplication.instance().desktop().availableGeometry().height() ) def _get_dialog_background_color(self): color = ( QtGui.QApplication.instance() .palette() .color(QtGui.QPalette.ColorRole.Window) ) return (color.redF(), color.greenF(), color.blueF()) pyface-7.4.0/pyface/ui/qt4/dialog.py0000644000076500000240000001470114176253531020151 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # (C) Copyright 2007 Riverbank Computing Limited # 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 from pyface.qt import QtCore, QtGui from traits.api import ( Any, Bool, Callable, Enum, Int, List, provides, Str, Tuple ) from pyface.i_dialog import IDialog, MDialog from pyface.constant import OK, CANCEL, YES, NO from .window import Window # Map PyQt dialog related constants to the pyface equivalents. _RESULT_MAP = { int(QtGui.QDialog.DialogCode.Accepted): OK, int(QtGui.QDialog.DialogCode.Rejected): CANCEL, int(QtGui.QMessageBox.StandardButton.Ok): OK, int(QtGui.QMessageBox.StandardButton.Cancel): CANCEL, int(QtGui.QMessageBox.StandardButton.Yes): YES, int(QtGui.QMessageBox.StandardButton.No): NO, } @provides(IDialog) class Dialog(MDialog, Window): """ The toolkit specific implementation of a Dialog. See the IDialog interface for the API documentation. """ # 'IDialog' interface -------------------------------------------------# cancel_label = Str() help_id = Str() help_label = Str() ok_label = Str() resizeable = Bool(True) return_code = Int(OK) style = Enum("modal", "nonmodal") # 'IWindow' interface -------------------------------------------------# title = Str("Dialog") # Private interface ---------------------------------------------------# #: A list of connected Qt signals to be removed before destruction. #: First item in the tuple is the Qt signal. The second item is the event #: handler. _connections_to_remove = List(Tuple(Any, Callable)) # ------------------------------------------------------------------------ # Protected 'IDialog' interface. # ------------------------------------------------------------------------ def _create_buttons(self, parent): buttons = QtGui.QDialogButtonBox() # 'OK' button. if self.ok_label: btn = buttons.addButton( self.ok_label, QtGui.QDialogButtonBox.ButtonRole.AcceptRole ) else: btn = buttons.addButton(QtGui.QDialogButtonBox.StandardButton.Ok) btn.setDefault(True) btn.clicked.connect(self.control.accept) self._connections_to_remove.append((btn.clicked, self.control.accept)) # 'Cancel' button. if self.cancel_label: btn = buttons.addButton( self.cancel_label, QtGui.QDialogButtonBox.ButtonRole.RejectRole ) else: btn = buttons.addButton(QtGui.QDialogButtonBox.StandardButton.Cancel) btn.clicked.connect(self.control.reject) self._connections_to_remove.append((btn.clicked, self.control.reject)) # 'Help' button. # FIXME v3: In the original code the only possible hook into the help # was to reimplement self._on_help(). However this was a private # method. Obviously nobody uses the Help button. For the moment we # display it but can't actually use it. if len(self.help_id) > 0: if self.help_label: buttons.addButton( self.help_label, QtGui.QDialogButtonBox.ButtonRole.HelpRole ) else: buttons.addButton(QtGui.QDialogButtonBox.StandardButton.Help) return buttons def _create_contents(self, parent): layout = QtGui.QVBoxLayout() if not self.resizeable: layout.setSizeConstraint(QtGui.QLayout.SizeConstraint.SetFixedSize) layout.addWidget(self._create_dialog_area(parent)) layout.addWidget(self._create_buttons(parent)) parent.setLayout(layout) def _create_dialog_area(self, parent): panel = QtGui.QWidget(parent) panel.setMinimumSize(QtCore.QSize(100, 200)) palette = panel.palette() palette.setColor(QtGui.QPalette.ColorRole.Window, QtGui.QColor("red")) panel.setPalette(palette) panel.setAutoFillBackground(True) return panel def _show_modal(self): dialog = self.control dialog.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal) # Suppress the context-help button hint, which # results in a non-functional "?" button on Windows. dialog.setWindowFlags( dialog.windowFlags() & ~QtCore.Qt.WindowType.WindowContextHelpButtonHint ) retval = dialog.exec_() return _RESULT_MAP[retval] # ------------------------------------------------------------------------- # 'IWidget' interface. # ------------------------------------------------------------------------- def destroy(self): while self._connections_to_remove: signal, handler = self._connections_to_remove.pop() signal.disconnect(handler) super().destroy() # ------------------------------------------------------------------------ # Protected 'IWidget' interface. # ------------------------------------------------------------------------ def _create_control(self, parent): dlg = QtGui.QDialog(parent) # Setting return code and firing close events is handled for 'modal' in # MDialog's open method. For 'nonmodal', we do it here. if self.style == "nonmodal": dlg.finished.connect(self._finished_fired) self._connections_to_remove.append( (dlg.finished, self._finished_fired) ) if self.size != (-1, -1): dlg.resize(*self.size) if self.position != (-1, -1): dlg.move(*self.position) dlg.setWindowTitle(self.title) return dlg # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _finished_fired(self, result): """ Called when the dialog is closed (and nonmodal). """ self.return_code = _RESULT_MAP[result] self.close() pyface-7.4.0/pyface/ui/qt4/images/0000755000076500000240000000000014176460550017603 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/qt4/images/heading_level_1.png0000644000076500000240000000424014176222673023321 0ustar cwebsterstaff00000000000000PNG  IHDRv;lgAMA7tEXtSoftwareAdobe ImageReadyqe<2IDATxb?(%@u0/# b\ t0VyY:@'ssHa50nV)n$ٽx***\0,28 a fП:|ۜUtBxd|,d` 2GOV;CMv:cPL'@Pz#; FӚWķ3u*;)DRsnBK-!JyLRnb*EWj4I_} @E0ś&+a: 8"'7nxywV81xK0 [RtNmu4nz27ŋsh³X<xT3yGE_2`?썶vmKAeUi +?3\. -XGiSY#),.[~S Òc9K_<ڶȕ-e@PYE)'Ժ (i;*pм{ ==*0 |~YwOCM6mhkc0Zdqr8#_<'_ \T0@]k|s 21G/R E-9X#XuϖG+hRgjI躧Q`;3\q%`)#\0`r;_LLV}' /T6>26p lːbwγ]kxK)R/DA+3rx:Kޏ:9za'0lB>jh;UbgV)'"0$g JYR(B~.^6.MkM1[0Bvgpك 6Tכ`.Kne"+]i`5!`c=›]1]iK5 IwA#7 ˞Bm W߻0K#f>?0B3@)fL4X:$R|NnD;Ptehe?rBgGWݽHĺ&A&yqD/ҭPLN; Gb P K@#:\[E nPZFH DBP[VKP;0!+$G#<  0~ep}GG%F*X'RDeꀘzQd{ L@@ \%0U?j:EEfp q8wG‘WS2 t`(Yr"6e.7 ͡buDQ|?u/*Uz,q#s͜[?F )}..pJr@be?Ф g!IhY+ iFHox^2BZ?5m\AAQt:#z[`YYIcaRv| (a`XaM lkDn7'57L0.ТIDrT2ɡ`q)IENDB`pyface-7.4.0/pyface/ui/qt4/images/application.png0000644000076500000240000000651614176222673022626 0ustar cwebsterstaff00000000000000PNG  IHDR00W IDATxkp]usKWWҕ,ɲ$lyMhz 2ҁN:L3~ 3mg )i !fM a(jKeY}{{Wlikf^{{ . "i&?fEc̗-1cLVD P5LȻZׯ?tm)}%ֿa\dɉ@UDc0|{ _ <3aEVcLu]$a6Q!Et2|h|%~qHӟ1|*d27~s=jzzzP(1QEJ)1l6Y\\T*غu+m0 E2"RMREf%ٴqFlقyLbbb#G4M!a'p<,CݍZ233y$lݽy6yg'cDzΩSc|KwuשI`޽r`"/f||yG/P.Wb2C(X,~%Peqq͛7fZ!33u @ 1Emz{~6r(PJ1==RH6^/=\ev = p_e1AjH: m+:]ͷ/|E\%MΤ)j->Zi! Nh.(lJz2f{%B'QݸqFoΉ'x[o/u7i$ 7`8?ca.RfgǾCx4c̞8 DuJ@lA o a5VX֭Ckl޾_.$wA\f?V#cC+6֚i/sG?c5d7yU2ԑ!QH]R~l!N8qSׯ۹s^~*j?6GGGqz^W3Gzjq?jxn7~|" )7;E2MOoQɦN s4's5݈HA)uzꩧ('NȍY $*_/dttI^]h1ڴF Umbk :q6ר*Pd24ڵiT*_PJ Q9v2#[@}AibB/Q1u$q"=[lTFioEǂ֚Lg*&:溓0le2cFlZ8 277ć]"9:dIb;00y'ޜ$ς?%3%dˁs~Q]`7֭rrSlO4MDS'?pum#;I;0ES3t}Bj5xcб&.b9"qħ\ zD٨ Xny"rE1fyAHfL0zl$-8Nޠ KeE+Lw [/8̚+$lu6Ƙ6˲e" Vi#us'pH$l8&hx=f`tc-,"mP)'XV@:t/i_2n2'y`ɖ8gu_y8[q88*3hqb(nb5:Pv5Ơ,ƴhg} ;luEv< C!Mͺ\#" f1۶H[9 H6۟0 ÚjAE Zyߞ?G|,*/r2~ٮ Dy[YKnfEDh`ɩSHcu0DD{3 +=AWk71/EkІ!yoLȺ:m h=`oD >l ۶_nd2bc l \!̗QR1ή,1aJ$K-h4W$XNʊ<\.SI$;mwyL&S*Zfa}ȩ,b%D Ϝ4_nm&u"+dGI\&I1R%tF0c)QW18~.x^SQT셮u9kK&vj ͋NHl k>ÇS-W:c1kĿAM<[nerrJz1JyotT>fDC蔏ќߍ A{r,O3d)t4)xl8zcG:9sYG^ ڎH\uvs=zRD*x<\s!qnjrRFAjb h0h0F-8 slgmt0m%=;Ag" E$b2hb1#_GCRNX\q8pb(_O{침Rt]w`hh(D`uhJ>;M=}ev~rmS,k>`T*p]˶^&N$mZJ:bm9r JqOf2/=b#U{{鉢DWEdR ˲"r8k?xp}e}߿IDln,+NLƘdՇ~ip] rA.ŕpYZAIENDB`pyface-7.4.0/pyface/ui/qt4/heading_text.py0000644000076500000240000000405514176253531021356 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # (C) Copyright 2007 Riverbank Computing Limited # 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 from pyface.qt import QtGui from traits.api import provides from pyface.i_heading_text import IHeadingText, MHeadingText from .layout_widget import LayoutWidget @provides(IHeadingText) class HeadingText(MHeadingText, LayoutWidget): """ The Qt-specific implementation of a HeadingText. """ # ------------------------------------------------------------------------ # 'IWidget' interface. # ------------------------------------------------------------------------ def _create_control(self, parent): """ Create the toolkit-specific control that represents the widget. """ control = QtGui.QLabel(parent) control.setSizePolicy( QtGui.QSizePolicy.Policy.Preferred, QtGui.QSizePolicy.Policy.Fixed ) return control # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _set_control_text(self, text): """ Set the text on the toolkit specific widget. """ # Bold the text. Qt supports a limited subset of HTML for rich text. text = f"{text}" self.control.setText(text) def _get_control_text(self): """ Get the text on the toolkit specific widget. """ text = self.control.text() # remove the bolding from the text text = text[3:-4] return text pyface-7.4.0/pyface/ui/qt4/pil_image.py0000644000076500000240000000267714176222673020654 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 provides from pyface.i_pil_image import IPILImage, MPILImage from pyface.ui.qt4.util.image_helpers import resize_image @provides(IPILImage) class PILImage(MPILImage): """ The toolkit specific implementation of a PILImage. """ # ------------------------------------------------------------------------ # 'IImage' interface. # ------------------------------------------------------------------------ def create_image(self, size=None): """ Creates a Qt image for this image. Parameters ---------- size : (int, int) or None The desired size as a (width, height) tuple, or None if wanting default image size. Returns ------- image : QImage The toolkit image corresponding to the image and the specified size. """ from PIL.ImageQt import ImageQt image = ImageQt(self.image) if size is not None: return resize_image(image, size) else: return image pyface-7.4.0/pyface/ui/qt4/timer/0000755000076500000240000000000014176460551017457 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/qt4/timer/timer.py0000644000076500000240000000201714176222673021152 0ustar cwebsterstaff00000000000000# (C) Copyright 2007 Riverbank Computing Limited # (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 Instance from pyface.qt.QtCore import QTimer from pyface.timer.i_timer import BaseTimer class PyfaceTimer(BaseTimer): """ Abstract base class for Qt toolkit timers. """ #: The QTimer for the PyfaceTimer. _timer = Instance(QTimer, (), allow_none=False) def __init__(self, **traits): traits.setdefault("_timer", QTimer()) super().__init__(**traits) self._timer.timeout.connect(self.perform) def _start(self): self._timer.start(int(self.interval * 1000)) def _stop(self): self._timer.stop() pyface-7.4.0/pyface/ui/qt4/timer/__init__.py0000644000076500000240000000062714176222673021576 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! pyface-7.4.0/pyface/ui/qt4/timer/do_later.py0000644000076500000240000000102614176222673021622 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ DoLaterTimer class Provided for backward compatibility. """ from pyface.timer.do_later import DoLaterTimer # noqa: F401 pyface-7.4.0/pyface/ui/qt4/tests/0000755000076500000240000000000014176460551017501 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/qt4/tests/test_qt_imports.py0000644000076500000240000000314314176222673023315 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 sys import unittest import warnings class TestPyfaceQtImports(unittest.TestCase): def test_imports(self): # check that all Qt API imports work import pyface.qt.QtCore # noqa: F401 import pyface.qt.QtGui # noqa: F401 import pyface.qt.QtNetwork # noqa: F401 import pyface.qt.QtOpenGL # noqa: F401 import pyface.qt.QtSvg # noqa: F401 import pyface.qt.QtTest # noqa: F401 import pyface.qt.QtMultimedia # noqa: F401 import pyface.qt.QtMultimediaWidgets # noqa: F401 @unittest.skipIf(sys.version_info > (3, 6), "WebKit is not available") def test_import_web_kit(self): import pyface.qt.QtWebKit # noqa: F401 def test_import_QtScript(self): # QtScript is not supported on PyQt5/PySide2 and # this import will raise a deprecation warning. with warnings.catch_warnings(record=True) as w: # Cause all warnings to always be triggered. warnings.simplefilter("always", category=DeprecationWarning) import pyface.qt.QtScript # noqa: F401 self.assertTrue(len(w) == 1) for warn in w: self.assertEqual(warn.category, DeprecationWarning) pyface-7.4.0/pyface/ui/qt4/tests/test_progress_dialog.py0000644000076500000240000001125414176222673024301 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 ..progress_dialog import ProgressDialog from ..util.gui_test_assistant import GuiTestAssistant class TestProgressDialog(unittest.TestCase, GuiTestAssistant): def setUp(self): GuiTestAssistant.setUp(self) self.dialog = ProgressDialog() def tearDown(self): if self.dialog.control is not None: with self.delete_widget(self.dialog.control): self.dialog.destroy() GuiTestAssistant.tearDown(self) def test_create(self): # test that creation and destruction works as expected self.dialog._create() self.gui.process_events() self.assertIsNotNone(self.dialog.control) self.assertIsNotNone(self.dialog.progress_bar) self.assertIsNotNone(self.dialog._message_control) self.assertIsNone(self.dialog._elapsed_control) self.assertIsNone(self.dialog._estimated_control) self.assertIsNone(self.dialog._remaining_control) self.dialog.destroy() self.gui.process_events() def test_show_time(self): # test that creation works with show_time self.dialog.show_time = True self.dialog._create() self.gui.process_events() self.assertIsNotNone(self.dialog._elapsed_control) self.assertIsNotNone(self.dialog._estimated_control) self.assertIsNotNone(self.dialog._remaining_control) self.dialog.destroy() self.gui.process_events() def test_show_percent(self): # test that creation works with show_percent self.dialog.show_percent = True self.dialog._create() self.gui.process_events() self.assertEqual(self.dialog.progress_bar.format(), "%p%") self.dialog.destroy() self.gui.process_events() def test_update(self): self.dialog.min = 0 self.dialog.max = 10 self.dialog.open() for i in range(11): result = self.dialog.update(i) self.gui.process_events() self.assertEqual(result, (True, False)) if i < 10: self.assertEqual(self.dialog.progress_bar.value(), i) self.assertIsNone(self.dialog.control) self.gui.process_events() def test_update_no_control(self): # note: inconsistent implementation with Wx self.dialog.min = 0 self.dialog.max = 10 result = self.dialog.update(1) self.assertEqual(result, (None, None)) self.gui.process_events() def test_change_message(self): self.dialog.min = 0 self.dialog.max = 10 self.dialog.open() for i in range(11): self.dialog.change_message("Updating {}".format(i)) result = self.dialog.update(i) self.gui.process_events() self.assertEqual(result, (True, False)) self.assertEqual(self.dialog.message, "Updating {}".format(i)) self.assertEqual( self.dialog._message_control.text(), "Updating {}".format(i) ) self.assertIsNone(self.dialog.control) self.gui.process_events() def test_change_message_trait(self): self.dialog.min = 0 self.dialog.max = 10 self.dialog.open() for i in range(11): self.dialog.message = "Updating {}".format(i) result = self.dialog.update(i) self.gui.process_events() self.assertEqual(result, (True, False)) self.assertEqual(self.dialog.message, "Updating {}".format(i)) self.assertEqual( self.dialog._message_control.text(), "Updating {}".format(i) ) self.assertIsNone(self.dialog.control) self.gui.process_events() def test_update_show_time(self): self.dialog.min = 0 self.dialog.max = 10 self.dialog.show_time = True self.dialog.open() for i in range(11): result = self.dialog.update(i) self.gui.process_events() self.assertEqual(result, (True, False)) self.assertNotEqual(self.dialog._elapsed_control.text(), "") self.assertNotEqual(self.dialog._estimated_control.text(), "") self.assertNotEqual(self.dialog._remaining_control.text(), "") self.assertIsNone(self.dialog.control) self.gui.process_events() pyface-7.4.0/pyface/ui/qt4/tests/__init__.py0000644000076500000240000000000014176222673021601 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/qt4/tests/test_mimedata.py0000644000076500000240000001345414176222673022703 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 pickle import dumps from pyface.qt import QtCore from ..mimedata import PyMimeData, str2bytes class PMDSubclass(PyMimeData): pass class PyMimeDataTestCase(unittest.TestCase): # Basic functionality tests def test_pickle(self): md = PyMimeData(data=0) self.assertEqual(md._local_instance, 0) self.assertTrue(md.hasFormat(PyMimeData.MIME_TYPE)) self.assertFalse(md.hasFormat(PyMimeData.NOPICKLE_MIME_TYPE)) self.assertEqual( md.data(PyMimeData.MIME_TYPE).data(), dumps(int) + dumps(0) ) def test_nopickle(self): md = PyMimeData(data=0, pickle=False) self.assertEqual(md._local_instance, 0) self.assertTrue(md.hasFormat(PyMimeData.NOPICKLE_MIME_TYPE)) self.assertFalse(md.hasFormat(PyMimeData.MIME_TYPE)) self.assertEqual( md.data(PyMimeData.NOPICKLE_MIME_TYPE).data(), str2bytes(str(id(0))), ) def test_cant_pickle(self): unpicklable = lambda: None md = PyMimeData(data=unpicklable) self.assertEqual(md._local_instance, unpicklable) self.assertTrue(md.hasFormat(PyMimeData.NOPICKLE_MIME_TYPE)) self.assertFalse(md.hasFormat(PyMimeData.MIME_TYPE)) self.assertEqual( md.data(PyMimeData.NOPICKLE_MIME_TYPE).data(), str2bytes(str(id(unpicklable))), ) def test_coerce_pymimedata(self): md = PyMimeData(data=0) md2 = PyMimeData.coerce(md) self.assertEqual(md, md2) def test_coerce_subclass(self): md = PMDSubclass(data=0) md2 = PyMimeData.coerce(md) self.assertEqual(md, md2) def test_coerce_QMimeData(self): md = QtCore.QMimeData() md.setText("test") md2 = PyMimeData.coerce(md) self.assertTrue(md2.hasText()) self.assertEqual(md2.text(), "test") def test_coerce_object(self): md = PyMimeData.coerce(0) self.assertEqual(md._local_instance, 0) self.assertTrue(md.hasFormat(PyMimeData.MIME_TYPE)) self.assertFalse(md.hasFormat(PyMimeData.NOPICKLE_MIME_TYPE)) self.assertEqual( md.data(PyMimeData.MIME_TYPE).data(), dumps(int) + dumps(0) ) def test_coerce_unpicklable(self): unpicklable = lambda: None md = PyMimeData.coerce(unpicklable) self.assertEqual(md._local_instance, unpicklable) self.assertFalse(md.hasFormat(PyMimeData.MIME_TYPE)) self.assertTrue(md.hasFormat(PyMimeData.NOPICKLE_MIME_TYPE)) def test_coerce_list(self): md = PyMimeData.coerce([0]) self.assertEqual(md._local_instance, [0]) self.assertTrue(md.hasFormat(PyMimeData.MIME_TYPE)) self.assertFalse(md.hasFormat(PyMimeData.NOPICKLE_MIME_TYPE)) self.assertEqual( md.data(PyMimeData.MIME_TYPE).data(), dumps(list) + dumps([0]) ) def test_coerce_list_pymimedata(self): md = PyMimeData(data=0) md2 = PyMimeData.coerce([md]) self.assertEqual(md2._local_instance, [0]) self.assertTrue(md2.hasFormat(PyMimeData.MIME_TYPE)) self.assertFalse(md2.hasFormat(PyMimeData.NOPICKLE_MIME_TYPE)) self.assertEqual( md2.data(PyMimeData.MIME_TYPE).data(), dumps(list) + dumps([0]) ) def test_coerce_list_pymimedata_nopickle(self): md = PyMimeData(data=0, pickle=False) md2 = PyMimeData.coerce([md]) self.assertEqual(md2._local_instance, [0]) self.assertFalse(md2.hasFormat(PyMimeData.MIME_TYPE)) self.assertTrue(md2.hasFormat(PyMimeData.NOPICKLE_MIME_TYPE)) def test_coerce_list_pymimedata_mixed(self): md1 = PyMimeData(data=0, pickle=False) md2 = PyMimeData(data=0) md = PyMimeData.coerce([md1, md2]) self.assertEqual(md._local_instance, [0, 0]) self.assertFalse(md.hasFormat(PyMimeData.MIME_TYPE)) self.assertTrue(md.hasFormat(PyMimeData.NOPICKLE_MIME_TYPE)) def test_subclass_coerce_pymimedata(self): md = PyMimeData(data=0) md2 = PMDSubclass.coerce(md) self.assertTrue(isinstance(md2, PMDSubclass)) self.assertTrue(md2.hasFormat(PyMimeData.MIME_TYPE)) self.assertFalse(md2.hasFormat(PyMimeData.NOPICKLE_MIME_TYPE)) self.assertEqual( md2.data(PyMimeData.MIME_TYPE).data(), dumps(int) + dumps(0) ) def test_instance(self): md = PyMimeData(data=0) self.assertEqual(md.instance(), 0) def test_instance_unpickled(self): md = PyMimeData(data=0) # remove local instance to simulate cross-process md._local_instance = None self.assertEqual(md.instance(), 0) def test_instance_nopickle(self): md = PyMimeData(data=0, pickle=False) # remove local instance to simulate cross-process md._local_instance = None self.assertEqual(md.instance(), None) def test_instance_type(self): md = PyMimeData(data=0) self.assertEqual(md.instanceType(), int) def test_instance_type_unpickled(self): md = PyMimeData(data=0) # remove local instance to simulate cross-process md._local_instance = None self.assertEqual(md.instanceType(), int) def test_instance_type_nopickle(self): md = PyMimeData(data=0, pickle=False) # remove local instance to simulate cross-process md._local_instance = None self.assertEqual(md.instanceType(), None) pyface-7.4.0/pyface/ui/qt4/tests/test_gui.py0000644000076500000240000000475214176222673021707 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Qt-specific tests for the Qt GUI implementation. """ import unittest from traits.api import Event, HasStrictTraits, Instance from pyface.api import GUI from pyface.i_gui import IGUI from pyface.qt import QtCore from pyface.util.guisupport import get_app_qt4, is_event_loop_running_qt4 class SimpleApplication(HasStrictTraits): """ Simple application that attempts to set a trait at start time, and immediately exits in response to that trait. """ # The GUI instance underlying this app. gui = Instance(IGUI) # Event fired after the event loop starts. application_running = Event() def __init__(self): super().__init__() self.gui = GUI() def start(self): """ Start the application. """ # This shouldn't be executed until after the event loop is running. self.gui.set_trait_later(self, "application_running", True) self.gui.start_event_loop() def stop(self): self.gui.stop_event_loop() class TestGui(unittest.TestCase): def test_set_trait_later_runs_later(self): # Regression test for enthought/pyface#272. application = SimpleApplication() application_running = [] def exit_app(event): # Record whether the event loop is running or not, then exit. application_running.append(is_event_loop_running_qt4()) application.stop() application.observe(exit_app, "application_running") # Make sure that the application stops after 10 seconds, no matter # what. qt_app = get_app_qt4() timeout_timer = QtCore.QTimer() timeout_timer.setSingleShot(True) timeout_timer.setInterval(10000) # 10 second timeout timeout_timer.timeout.connect(qt_app.quit) timeout_timer.start() try: application.start() finally: timeout_timer.stop() # Attempt to leave the QApplication in a reasonably clean # state in case of failure. qt_app.sendPostedEvents() self.assertTrue(application_running[0]) pyface-7.4.0/pyface/ui/qt4/tests/bad_import.py0000644000076500000240000000106714176222673022200 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 used to test what happens when there is an unrelated import error # when importing a toolkit object raise ImportError("No module named nonexistent") pyface-7.4.0/pyface/ui/qt4/tests/test_message_dialog.py0000644000076500000240000000557614176253531024070 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Qt-specific tests for the MessageDialog """ import contextlib import unittest from pyface.api import MessageDialog from pyface.qt import QtCore, QtGui from pyface.ui.qt4.util.gui_test_assistant import GuiTestAssistant class TestMessageDialog(GuiTestAssistant, unittest.TestCase): def test_escape_button_no_details(self): dialog = MessageDialog( parent=None, title="Dialog title", message="Printer on fire", informative="Your printer is on fire", severity="error", size=(600, 400), ) with self.create_dialog(dialog): escape_button = dialog.control.escapeButton() ok_button = dialog.control.button(QtGui.QMessageBox.StandardButton.Ok) # It's possible for both the above to be None, so double check. self.assertIsNotNone(escape_button) self.assertIs(escape_button, ok_button) def test_escape_button_with_details(self): dialog = MessageDialog( parent=None, title="Dialog title", message="Printer on fire", informative="Your printer is on fire", details="Temperature exceeds 1000 degrees", severity="error", size=(600, 400), ) with self.create_dialog(dialog): escape_button = dialog.control.escapeButton() ok_button = dialog.control.button(QtGui.QMessageBox.StandardButton.Ok) # It's possible for both the above to be None, so double check. self.assertIsNotNone(escape_button) self.assertIs(escape_button, ok_button) def test_text_format(self): dialog = MessageDialog( parent=None, title="Dialog title", message="Printer on fire", informative="Your printer is on fire", details="Temperature exceeds 1000 degrees", severity="error", text_format="plain", size=(600, 400), ) with self.create_dialog(dialog): text_format = dialog.control.textFormat() self.assertEqual(text_format, QtCore.Qt.TextFormat.PlainText) @contextlib.contextmanager def create_dialog(self, dialog): """ Create a dialog, then destroy at the end of a with block. """ with self.event_loop(): dialog._create() try: yield finally: with self.event_loop(): dialog.destroy() pyface-7.4.0/pyface/ui/qt4/font_dialog.py0000644000076500000240000000370114176253531021175 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 dialog that allows the user to select a font. """ from pyface.qt import QtGui from traits.api import provides from pyface.font import Font from pyface.ui_traits import PyfaceFont from pyface.i_font_dialog import IFontDialog from .dialog import Dialog @provides(IFontDialog) class FontDialog(Dialog): """ A dialog that allows the user to choose a font. """ # 'IFontDialog' interface ---------------------------------------------- #: The font in the dialog. font = PyfaceFont() # ------------------------------------------------------------------------ # 'IDialog' interface. # ------------------------------------------------------------------------ def _create_contents(self, parent): # In PyQt this is a canned dialog so there are no contents. pass # ------------------------------------------------------------------------ # 'IWindow' interface. # ------------------------------------------------------------------------ def close(self): if self.control.result() == QtGui.QDialog.DialogCode.Accepted: qfont = self.control.selectedFont() self.font = Font.from_toolkit(qfont) return super(FontDialog, self).close() # ------------------------------------------------------------------------ # 'IWindow' interface. # ------------------------------------------------------------------------ def _create_control(self, parent): qfont = self.font.to_toolkit() dialog = QtGui.QFontDialog(qfont, parent) return dialog pyface-7.4.0/pyface/ui/qt4/wizard/0000755000076500000240000000000014176460551017637 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/qt4/wizard/wizard_page.py0000644000076500000240000000775714176253531022523 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # (C) Copyright 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 """ A page in a wizard. """ from pyface.qt import QtGui from traits.api import Bool, HasTraits, provides, Str, Tuple from pyface.wizard.i_wizard_page import IWizardPage, MWizardPage @provides(IWizardPage) class WizardPage(MWizardPage, HasTraits): """ The toolkit specific implementation of a WizardPage. See the IWizardPage interface for the API documentation. """ # 'IWizardPage' interface ---------------------------------------------# id = Str() next_id = Str() last_page = Bool(False) complete = Bool(False) heading = Str() subheading = Str() size = Tuple() # ------------------------------------------------------------------------ # 'IWizardPage' interface. # ------------------------------------------------------------------------ def create_page(self, parent): """ Creates the wizard page. """ content = self._create_page_content(parent) # We allow some flexibility with the sort of control we are given. if not isinstance(content, QtGui.QWizardPage): wp = _WizardPage(self) if isinstance(content, QtGui.QLayout): wp.setLayout(content) else: assert isinstance(content, QtGui.QWidget) lay = QtGui.QVBoxLayout() lay.addWidget(content) wp.setLayout(lay) content = wp # Honour any requested page size. if self.size: width, height = self.size if width > 0: content.setMinimumWidth(width) if height > 0: content.setMinimumHeight(height) content.setTitle(self.heading) content.setSubTitle(self.subheading) return content # ------------------------------------------------------------------------ # Protected 'IWizardPage' interface. # ------------------------------------------------------------------------ def _create_page_content(self, parent): """ Creates the actual page content. """ # Dummy implementation - override! control = QtGui.QWidget(parent) palette = control.palette() palette.setColor(QtGui.QPalette.ColorRole.Window, QtGui.QColor("yellow")) control.setPalette(palette) control.setAutoFillBackground(True) return control class _WizardPage(QtGui.QWizardPage): """ A QWizardPage sub-class that hooks into the IWizardPage's 'complete' trait. """ def __init__(self, page): """ Initialise the object. """ QtGui.QWizardPage.__init__(self) self.pyface_wizard = None page.observe(self._on_complete_changed, "complete") self._page = page def initializePage(self): """ Reimplemented to call the IWizard's 'next'. """ if self.pyface_wizard is not None: self.pyface_wizard.next() def cleanupPage(self): """ Reimplemented to call the IWizard's 'previous'. """ if self.pyface_wizard is not None: self.pyface_wizard.previous() def isComplete(self): """ Reimplemented to return the state of the 'complete' trait. """ return self._page.complete def _on_complete_changed(self, event): """ The trait handler for when the page's completion state changes. """ self.completeChanged.emit() pyface-7.4.0/pyface/ui/qt4/wizard/__init__.py0000644000076500000240000000062714176222673021756 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! pyface-7.4.0/pyface/ui/qt4/wizard/wizard.py0000644000076500000240000001541214176253531021512 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # (C) Copyright 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 """ The base class for all pyface wizards. """ from pyface.qt import QtCore, QtGui from traits.api import Bool, Instance, List, Property, provides, Str from pyface.api import Dialog from pyface.wizard.i_wizard import IWizard, MWizard from pyface.wizard.i_wizard_controller import IWizardController from pyface.wizard.i_wizard_page import IWizardPage @provides(IWizard) class Wizard(MWizard, Dialog): """ The base class for all pyface wizards. See the IWizard interface for the API documentation. """ # 'IWizard' interface -------------------------------------------------# pages = Property(List(IWizardPage)) controller = Instance(IWizardController) show_cancel = Bool(True) # 'IWindow' interface -------------------------------------------------# title = Str("Wizard") # ------------------------------------------------------------------------ # 'IWizard' interface. # ------------------------------------------------------------------------ # Override MWizard implementation to do nothing. We still call these methods # because it expected by IWizard, and users may wish to hook in custom code # before changing a page. def next(self): pass def previous(self): pass # ------------------------------------------------------------------------ # Protected 'IDialog' interface. # ------------------------------------------------------------------------ def _create_contents(self, parent): pass # ------------------------------------------------------------------------ # Protected 'IWidget' interface. # ------------------------------------------------------------------------ def _create_control(self, parent): control = _Wizard(parent, self) control.setOptions( QtGui.QWizard.WizardOption.NoDefaultButton | QtGui.QWizard.WizardOption.NoBackButtonOnStartPage ) control.setModal(self.style == "modal") control.setWindowTitle(self.title) # Necessary for 'nonmodal'. See Dialog for more info. if self.style == "nonmodal": control.finished.connect(self._finished_fired) if self.size != (-1, -1): size = QtCore.QSize(*self.size) control.setMinimumSize(size) control.resize(size) if not self.show_cancel: control.setOption(QtGui.QWizard.WizardOption.NoCancelButton) if self.help_id: control.setOption(QtGui.QWizard.WizardOption.HaveHelpButton) control.helpRequested.connect(self._help_requested) # Add the initial pages. for page in self.pages: page.pyface_wizard = self control.addWizardPage(page) # Set the start page. control.setStartWizardPage() return control # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _help_requested(self): """ Called when the 'Help' button is pressed. """ # FIXME: Hook into a help system. print("Show help for", self.help_id) # Trait handlers ------------------------------------------------------- def _get_pages(self): """ The pages getter. """ return self.controller.pages def _set_pages(self, pages): """ The pages setter. """ # Remove pages from the old list that appear in the new list. The old # list will now contain pages that are no longer in the wizard. old_pages = self.pages new_pages = [] for page in pages: try: old_pages.remove(page) except ValueError: new_pages.append(page) # Dispose of the old pages. for page in old_pages: page.dispose_page() # If we have created the control then we need to add the new pages, # otherwise we leave it until the control is created. if self.control: for page in new_pages: self.control.addWizardPage(page) self.controller.pages = pages def _controller_default(self): """ Provide a default controller. """ from pyface.wizard.wizard_controller import WizardController return WizardController() class _Wizard(QtGui.QWizard): """ A QWizard sub-class that hooks into the controller to determine the next page to show. """ def __init__(self, parent, pyface_wizard): """ Initialise the object. """ QtGui.QWizard.__init__(self, parent) self._pyface_wizard = pyface_wizard self._controller = pyface_wizard.controller self._ids = {} self.currentIdChanged.connect(self._update_controller) def addWizardPage(self, page): """ Add a page that provides IWizardPage. """ # We must pass a parent otherwise TraitsUI does the wrong thing. qpage = page.create_page(self) qpage.pyface_wizard = self._pyface_wizard id = self.addPage(qpage) self._ids[id] = page def setStartWizardPage(self): """ Set the first page. """ page = self._controller.get_first_page() id = self._page_to_id(page) if id >= 0: self.setStartId(id) def nextId(self): """ Reimplemented to return the id of the next page to display. """ if self.currentId() < 0: return self._page_to_id(self._controller.get_first_page()) current = self._ids[self.currentId()] next = self._controller.get_next_page(current) return self._page_to_id(next) def _update_controller(self, id): """ Called when the current page has changed. """ # Keep the controller in sync with the wizard. self._controller.current_page = self._ids.get(id) def _page_to_id(self, page): """ Return the id of the given page. """ if page is None: id = -1 else: for id, p in self._ids.items(): if p is page: break else: id = -1 return id pyface-7.4.0/pyface/ui/qt4/resource_manager.py0000644000076500000240000000355414176253531022237 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # (C) Copyright 2007 Riverbank Computing Limited # 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 from pyface.qt import QtCore, QtGui, QtSvg from pyface.resource.api import ResourceFactory class PyfaceResourceFactory(ResourceFactory): """ The implementation of a shared resource manager. """ # ------------------------------------------------------------------------ # 'ResourceFactory' interface. # ------------------------------------------------------------------------ def image_from_file(self, filename): """ Creates an image from the data in the specified filename. """ # Although QPixmap can load SVG directly, it does not respect the # default size, so we use a QSvgRenderer to get the default size. if filename.endswith((".svg", ".SVG")): renderer = QtSvg.QSvgRenderer(filename) pixmap = QtGui.QPixmap(renderer.defaultSize()) pixmap.fill(QtCore.Qt.GlobalColor.transparent) painter = QtGui.QPainter(pixmap) renderer.render(painter) else: pixmap = QtGui.QPixmap(filename) return pixmap def image_from_data(self, data, filename=None): """ Creates an image from the specified data. """ image = QtGui.QPixmap() image.loadFromData(data) return image pyface-7.4.0/pyface/ui/qt4/__init__.py0000644000076500000240000000062714176222673020456 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! pyface-7.4.0/pyface/ui/qt4/splash_screen.py0000644000076500000240000000532414176253531021544 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # (C) Copyright 2007 Riverbank Computing Limited # 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 from logging import DEBUG from traits.api import Any, Bool, Int, Tuple, Str, provides from pyface.qt import QtCore, QtGui from pyface.i_splash_screen import ISplashScreen, MSplashScreen from pyface.ui_traits import Image from .image_resource import ImageResource from .window import Window @provides(ISplashScreen) class SplashScreen(MSplashScreen, Window): """ The toolkit specific implementation of a SplashScreen. See the ISplashScreen interface for the API documentation. """ # 'ISplashScreen' interface -------------------------------------------- image = Image(ImageResource("splash")) log_level = Int(DEBUG) show_log_messages = Bool(True) text = Str() text_color = Any() text_font = Any() text_location = Tuple(5, 5) # ------------------------------------------------------------------------ # Protected 'IWidget' interface. # ------------------------------------------------------------------------ def _create_control(self, parent): splash_screen = QtGui.QSplashScreen(self.image.create_image()) self._qt4_show_message(splash_screen) return splash_screen # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _text_changed(self): """ Called when the splash screen text has been changed. """ if self.control is not None: self._qt4_show_message(self.control) def _qt4_show_message(self, control): """ Set the message text for a splash screen control. """ if self.text_font is not None: control.setFont(self.text_font) if self.text_color is None: text_color = QtCore.Qt.GlobalColor.black else: # Until we get the type of this trait finalised (ie. when TraitsUI # supports PyQt) convert it explcitly to a colour. text_color = QtGui.QColor(self.text_color) control.showMessage(self.text, QtCore.Qt.AlignmentFlag.AlignLeft, text_color) pyface-7.4.0/pyface/ui/qt4/font.py0000644000076500000240000001350414176253531017660 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 conversion utilities This module provides facilities for converting between pyface Font objects and Qt QFont objects, trying to keep as much similarity as possible between them. """ from pyface.qt.QtGui import QFont qt_family_to_generic_family = { QFont.StyleHint.AnyStyle: 'default', QFont.StyleHint.System: 'default', QFont.Decorative: 'fantasy', QFont.Serif: 'serif', QFont.StyleHint.Cursive: 'cursive', QFont.SansSerif: 'sans-serif', QFont.StyleHint.Monospace: 'monospace', QFont.TypeWriter: 'typewriter', } generic_family_to_qt_family = { 'default': QFont.StyleHint.System, 'fantasy': QFont.Decorative, 'decorative': QFont.Decorative, 'serif': QFont.Serif, 'roman': QFont.Serif, 'cursive': QFont.StyleHint.Cursive, 'script': QFont.StyleHint.Cursive, 'sans-serif': QFont.SansSerif, 'swiss': QFont.SansSerif, 'monospace': QFont.StyleHint.Monospace, 'modern': QFont.StyleHint.Monospace, 'typewriter': QFont.TypeWriter, 'teletype': QFont.TypeWriter, } weight_to_qt_weight = { 100: QFont.Weight.Thin, 200: QFont.Weight.ExtraLight, 300: QFont.Weight.Light, 400: QFont.Weight.Normal, 500: QFont.Weight.Medium, 600: QFont.Weight.DemiBold, 700: QFont.Weight.Bold, 800: QFont.Weight.ExtraBold, 900: QFont.Weight.Black, 1000: 99, } qt_weight_to_weight = { QFont.Weight.Thin: 'thin', QFont.Weight.ExtraLight: 'extra-light', QFont.Weight.Light: 'light', QFont.Weight.Normal: 'normal', QFont.Weight.Medium: 'medium', QFont.Weight.DemiBold: 'demi-bold', QFont.Weight.Bold: 'bold', QFont.Weight.ExtraBold: 'extra-bold', QFont.Weight.Black: 'black', 99: 'extra-heavy', } style_to_qt_style = { 'normal': QFont.Style.StyleNormal, 'oblique': QFont.Style.StyleOblique, 'italic': QFont.Style.StyleItalic, } qt_style_to_style = {value: key for key, value in style_to_qt_style.items()} def font_to_toolkit_font(font): """ Convert a Pyface font to a Qfont. Parameters ---------- font : pyface.font.Font The Pyface font to convert. Returns ------- qt_font : QFont The best matching Qt font. """ qt_font = QFont() families = [] default_family = None for family in font.family: if family not in generic_family_to_qt_family: families.append(family) elif default_family is None: default_family = family if families and hasattr(qt_font, 'setFamilies'): # Qt 5.13 and later qt_font.setFamilies(families) elif families: qt_font.setFamily(families[0]) # Note: possibily could use substitutions here, # but not sure if global (which would be bad, so we don't) if default_family is not None: qt_font.setStyleHint(generic_family_to_qt_family[default_family]) qt_font.setPointSizeF(font.size) qt_font.setWeight(weight_to_qt_weight[font.weight_]) qt_font.setStretch(font.stretch) qt_font.setStyle(style_to_qt_style[font.style]) qt_font.setUnderline('underline' in font.decorations) qt_font.setStrikeOut('strikethrough' in font.decorations) qt_font.setOverline('overline' in font.decorations) if 'small-caps' in font.variants: qt_font.setCapitalization(QFont.Capitalization.SmallCaps) return qt_font def toolkit_font_to_properties(toolkit_font): """ Convert a QFont to a dictionary of font properties. Parameters ---------- toolkit_font : QFont The Qt QFont to convert. Returns ------- properties : dict Font properties suitable for use in creating a Pyface Font. """ family = [] if toolkit_font.family(): family.append(toolkit_font.family()) if hasattr(toolkit_font, 'families'): # Qt 5.13 and later family.extend(toolkit_font.families()) family.append(qt_family_to_generic_family[toolkit_font.styleHint()]) size = toolkit_font.pointSizeF() style = qt_style_to_style[toolkit_font.style()] weight = map_to_nearest(toolkit_font.weight(), qt_weight_to_weight) stretch = toolkit_font.stretch() if stretch == 0: # stretch 0 means any stretch is allowed, we default to no stretch stretch = 100.0 variants = set() if toolkit_font.capitalization() == QFont.Capitalization.SmallCaps: variants.add('small-caps') decorations = set() if toolkit_font.underline(): decorations.add('underline') if toolkit_font.strikeOut(): decorations.add('strikethrough') if toolkit_font.overline(): decorations.add('overline') return { 'family': family, 'size': size, 'weight': weight, 'stretch': stretch, 'style': style, 'variants': variants, 'decorations': decorations, } def map_to_nearest(target, mapping): """ Given mapping with keys from 0 and 99, return closest value. Parameters ---------- target : int The value to map. mapping : dict A dictionary with integer keys ranging from 0 to 99. Returns ------- value : any The value corresponding to the nearest key. In the case of a tie, the first value is returned. """ if target in mapping: return mapping[target] distance = 100 nearest = None for key in mapping: if abs(target - key) < distance: distance = abs(target - key) nearest = key return mapping[nearest] pyface-7.4.0/pyface/ui/qt4/application_window.py0000644000076500000240000001573114176253531022610 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # (C) Copyright 2007 Riverbank Computing Limited # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 sys from pyface.qt import QtGui from pyface.action.api import MenuBarManager, StatusBarManager from pyface.action.api import ToolBarManager from traits.api import Instance, List, observe, provides, Str from pyface.i_application_window import IApplicationWindow, MApplicationWindow from pyface.ui_traits import Image from .image_resource import ImageResource from .window import Window @provides(IApplicationWindow) class ApplicationWindow(MApplicationWindow, Window): """ The toolkit specific implementation of an ApplicationWindow. See the IApplicationWindow interface for the API documentation. """ # 'IApplicationWindow' interface --------------------------------------- #: The icon to display in the application window title bar. icon = Image() #: The menu bar manager for the window. menu_bar_manager = Instance(MenuBarManager) #: The status bar manager for the window. status_bar_manager = Instance(StatusBarManager) #: DEPRECATED: The tool bar manager for the window. tool_bar_manager = Instance(ToolBarManager) #: The collection of tool bar managers for the window. tool_bar_managers = List(ToolBarManager) # 'IWindow' interface -------------------------------------------------# #: The window title. title = Str("Pyface") # ------------------------------------------------------------------------ # Protected 'IApplicationWindow' interface. # ------------------------------------------------------------------------ def _create_contents(self, parent): panel = QtGui.QWidget(parent) palette = QtGui.QPalette(panel.palette()) palette.setColor(QtGui.QPalette.ColorRole.Window, QtGui.QColor("blue")) panel.setPalette(palette) panel.setAutoFillBackground(True) return panel def _create_menu_bar(self, parent): if self.menu_bar_manager is not None: menu_bar = self.menu_bar_manager.create_menu_bar(parent) self.control.setMenuBar(menu_bar) def _create_status_bar(self, parent): if self.status_bar_manager is not None: status_bar = self.status_bar_manager.create_status_bar(parent) # QMainWindow automatically makes the status bar visible, but we # have delegated this responsibility to the status bar manager. self.control.setStatusBar(status_bar) status_bar.setVisible(self.status_bar_manager.visible) def _create_tool_bar(self, parent): tool_bar_managers = self._get_tool_bar_managers() visible = self.control.isVisible() for tool_bar_manager in tool_bar_managers: # Add the tool bar and make sure it is visible. tool_bar = tool_bar_manager.create_tool_bar(parent) self.control.addToolBar(tool_bar) tool_bar.show() # Make sure that the tool bar has a name so its state can be saved. if len(tool_bar.objectName()) == 0: tool_bar.setObjectName(tool_bar_manager.name) if sys.platform == "darwin": # Work around bug in Qt on OS X where creating a tool bar with a # QMainWindow parent hides the window. See # http://bugreports.qt.nokia.com/browse/QTBUG-5069 for more info. self.control.setVisible(visible) def _set_window_icon(self): if self.icon is None: icon = ImageResource("application.png") else: icon = self.icon if self.control is not None: self.control.setWindowIcon(icon.create_icon()) # ------------------------------------------------------------------------ # 'Window' interface. # ------------------------------------------------------------------------ def _size_default(self): """ Trait initialiser. """ return (800, 600) # ------------------------------------------------------------------------ # Protected 'IWidget' interface. # ------------------------------------------------------------------------ def _create(self): super()._create() contents = self._create_contents(self.control) self.control.setCentralWidget(contents) self._create_trim_widgets(self.control) def _create_control(self, parent): control = super()._create_control(parent) control.setObjectName("ApplicationWindow") control.setAnimated(False) control.setDockNestingEnabled(True) return control # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _get_tool_bar_managers(self): """ Return all tool bar managers specified for the window. """ # fixme: V3 remove the old-style single toolbar option! if self.tool_bar_manager is not None: tool_bar_managers = [self.tool_bar_manager] else: tool_bar_managers = self.tool_bar_managers return tool_bar_managers # Trait change handlers ------------------------------------------------ # QMainWindow takes ownership of the menu bar and the status bar upon # assignment. For this reason, it is unnecessary to delete the old controls # in the following two handlers. @observe("menu_bar_manager") def _menu_bar_manager_updated(self, event): if self.control is not None: self._create_menu_bar(self.control) @observe("status_bar_manager") def _status_bar_manager_updated(self, event): if self.control is not None: if event.old is not None: event.old.destroy_status_bar() self._create_status_bar(self.control) @observe("tool_bar_manager, tool_bar_managers.items") def _update_tool_bar_managers(self, event): if self.control is not None: # Remove the old toolbars. for child in self.control.children(): if isinstance(child, QtGui.QToolBar): self.control.removeToolBar(child) child.deleteLater() # Add the new toolbars. self._create_tool_bar(self.control) @observe("icon") def _icon_updated(self, event): self._set_window_icon() pyface-7.4.0/pyface/ui/qt4/progress_dialog.py0000644000076500000240000002273314176253531022101 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 progress bar intended to run in the UI thread """ import time from pyface.qt import QtGui, QtCore from traits.api import ( Any, Bool, Callable, Instance, Int, List, Str, provides, Tuple ) from pyface.i_progress_dialog import IProgressDialog, MProgressDialog from .window import Window @provides(IProgressDialog) class ProgressDialog(MProgressDialog, Window): """ A simple progress dialog window which allows itself to be updated """ # FIXME: buttons are not set up correctly yet #: The progress bar widget progress_bar = Instance(QtGui.QProgressBar) #: The window title title = Str() #: The text message to display in the dialog message = Str() #: The minimum value of the progress range min = Int() #: The minimum value of the progress range max = Int() #: The margin around the progress bar margin = Int(5) #: Whether or not the progress dialog can be cancelled can_cancel = Bool(False) # The IProgressDialog interface doesn't declare this, but since this is a # feature of the QT backend ProgressDialog that doesn't appear in WX, we # offer an option to disable it. can_ok = Bool(False) #: Whether or not to show the time taken (not implemented in Qt) show_time = Bool(False) #: Whether or not to show the percent completed show_percent = Bool(False) #: The size of the dialog dialog_size = Instance(QtCore.QRect) #: Label for the 'cancel' button cancel_button_label = Str("Cancel") #: Whether or not the dialog was cancelled by the user _user_cancelled = Bool(False) #: The widget showing the message text _message_control = Instance(QtGui.QLabel) #: The widget showing the time elapsed _elapsed_control = Instance(QtGui.QLabel) #: The widget showing the estimated time to completion _estimated_control = Instance(QtGui.QLabel) #: The widget showing the estimated time remaining _remaining_control = Instance(QtGui.QLabel) # Private interface ---------------------------------------------------# #: A list of connected Qt signals to be removed before destruction. #: First item in the tuple is the Qt signal. The second item is the event #: handler. _connections_to_remove = List(Tuple(Any, Callable)) # ------------------------------------------------------------------------- # IWindow Interface # ------------------------------------------------------------------------- def open(self): """ Opens the window. """ super().open() self._start_time = time.time() def close(self): """ Closes the window. """ self.progress_bar.destroy() self.progress_bar = None super().close() # ------------------------------------------------------------------------- # 'IWidget' interface. # ------------------------------------------------------------------------- def destroy(self): while self._connections_to_remove: signal, handler = self._connections_to_remove.pop() signal.disconnect(handler) super().destroy() # ------------------------------------------------------------------------- # IProgressDialog Interface # ------------------------------------------------------------------------- def update(self, value): """ Update the progress bar to the desired value If the value is >= the maximum and the progress bar is not contained in another panel the parent window will be closed. Parameters ---------- value : The progress value to set. """ if self.progress_bar is None: return None, None if self.max > 0: self.progress_bar.setValue(value) if self.max != self.min: percent = (float(value) - self.min) / (self.max - self.min) else: percent = 1.0 if self.show_time and (percent != 0): current_time = time.time() elapsed = current_time - self._start_time estimated = elapsed / percent remaining = estimated - elapsed self._set_time_label(elapsed, self._elapsed_control) self._set_time_label(estimated, self._estimated_control) self._set_time_label(remaining, self._remaining_control) if value >= self.max or self._user_cancelled: self.close() else: self.progress_bar.setValue(self.progress_bar.value() + value) if self._user_cancelled: self.close() QtGui.QApplication.processEvents() return (not self._user_cancelled, False) # ------------------------------------------------------------------------- # Private Interface # ------------------------------------------------------------------------- def reject(self, event): self._user_cancelled = True self.close() def _set_time_label(self, value, control): hours = value / 3600 minutes = (value % 3600) / 60 seconds = value % 60 label = "%1u:%02u:%02u" % (hours, minutes, seconds) control.setText(label) def _create_buttons(self, dialog, layout): """ Creates the buttons. """ if not (self.can_cancel or self.can_ok): return # Create the button. buttons = QtGui.QDialogButtonBox() if self.can_cancel: buttons.addButton( self.cancel_button_label, QtGui.QDialogButtonBox.ButtonRole.RejectRole ) if self.can_ok: buttons.addButton(QtGui.QDialogButtonBox.StandardButton.Ok) # TODO: hookup the buttons to our methods, this may involve subclassing from QDialog if self.can_cancel: buttons.rejected.connect(dialog.reject) self._connections_to_remove.append( (buttons.rejected, dialog.reject) ) if self.can_ok: buttons.accepted.connect(dialog.accept) self._connections_to_remove.append( (buttons.accepted, dialog.accept) ) layout.addWidget(buttons) def _create_label(self, dialog, layout, text): dummy = QtGui.QLabel(text, dialog) dummy.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignLeft) label = QtGui.QLabel("unknown", dialog) label.setAlignment( QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignLeft | QtCore.Qt.AlignmentFlag.AlignRight ) sub_layout = QtGui.QHBoxLayout() sub_layout.addWidget(dummy) sub_layout.addWidget(label) layout.addLayout(sub_layout) return label def _create_gauge(self, dialog, layout): self.progress_bar = QtGui.QProgressBar(dialog) self.progress_bar.setRange(self.min, self.max) layout.addWidget(self.progress_bar) if self.show_percent: self.progress_bar.setFormat("%p%") else: self.progress_bar.setFormat("%v") def _create_message(self, dialog, layout): label = QtGui.QLabel(self.message, dialog) label.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignLeft) layout.addWidget(label) self._message_control = label def _create_percent(self, dialog, parent_sizer): if not self.show_percent: return raise NotImplementedError() def _create_timer(self, dialog, layout): if not self.show_time: return self._elapsed_control = self._create_label( dialog, layout, "Elapsed time : " ) self._estimated_control = self._create_label( dialog, layout, "Estimated time : " ) self._remaining_control = self._create_label( dialog, layout, "Remaining time : " ) def _create_control(self, parent): return QtGui.QDialog(parent) def _create(self): super()._create() self._create_contents(self.control) def _create_contents(self, parent): dialog = parent layout = QtGui.QVBoxLayout(dialog) layout.setContentsMargins( self.margin, self.margin, self.margin, self.margin ) # The 'guts' of the dialog. self._create_message(dialog, layout) self._create_gauge(dialog, layout) self._create_timer(dialog, layout) self._create_buttons(dialog, layout) dialog.setWindowTitle(self.title) parent.setLayout(layout) # ------------------------------------------------------------------------- # Trait change handlers # ------------------------------------------------------------------------- def _max_changed(self, new): if self.progress_bar is not None: self.progress_bar.setMaximum(new) def _min_changed(self, new): if self.progress_bar is not None: self.progress_bar.setMinimum(new) def _message_changed(self, new): if self._message_control is not None: self._message_control.setText(new) pyface-7.4.0/pyface/ui/qt4/layered_panel.py0000644000076500000240000000423714176222673021524 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 Layered panel. """ from traits.api import Int, provides from pyface.qt.QtGui import QStackedWidget from pyface.i_layered_panel import ILayeredPanel, MLayeredPanel from .layout_widget import LayoutWidget @provides(ILayeredPanel) class LayeredPanel(MLayeredPanel, LayoutWidget): """ A Layered panel. A layered panel contains one or more named layers, with only one layer visible at any one time (think of a 'tab' control minus the tabs!). Each layer is a toolkit-specific control. """ # The minimum sizes of the panel. Ignored in Qt. min_width = Int(0) min_height = Int(0) # ------------------------------------------------------------------------ # 'ILayeredPanel' interface. # ------------------------------------------------------------------------ def add_layer(self, name, layer): """ Adds a layer with the specified name. All layers are hidden when they are added. Use 'show_layer' to make a layer visible. """ self.control.addWidget(layer) self._layers[name] = layer return layer def show_layer(self, name): """ Shows the layer with the specified name. """ layer = self._layers[name] layer_index = self.control.indexOf(layer) self.control.setCurrentIndex(layer_index) self.current_layer = layer self.current_layer_name = name return layer # ------------------------------------------------------------------------ # "IWidget" interface. # ------------------------------------------------------------------------ def _create_control(self, parent): """ Create the toolkit-specific control that represents the widget. """ control = QStackedWidget(parent) return control pyface-7.4.0/pyface/ui/qt4/action/0000755000076500000240000000000014176460550017613 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/qt4/action/tool_bar_manager.py0000644000076500000240000001645614176253531023473 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # (C) Copyright 2007 Riverbank Computing Limited # 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 # ------------------------------------------------------------------------------ from pyface.qt import QtCore, QtGui from traits.api import Bool, Enum, Instance, List, Str, Tuple from pyface.image_cache import ImageCache from pyface.action.action_manager import ActionManager class ToolBarManager(ActionManager): """ A tool bar manager realizes itself in errr, a tool bar control. """ # 'ToolBarManager' interface ------------------------------------------- # Is the tool bar enabled? enabled = Bool(True) # Is the tool bar visible? visible = Bool(True) # The size of tool images (width, height). image_size = Tuple((16, 16)) # The toolbar name (used to distinguish multiple toolbars). name = Str("ToolBar") # The orientation of the toolbar. orientation = Enum("horizontal", "vertical") # Should we display the name of each tool bar tool under its image? show_tool_names = Bool(True) # Should we display the horizontal divider? show_divider = Bool(True) # Private interface ---------------------------------------------------- # Cache of tool images (scaled to the appropriate size). _image_cache = Instance(ImageCache) #: Keep track of all created toolbars in order to properly dispose of them _toolbars = List() # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, *args, **traits): """ Creates a new tool bar manager. """ # Base class constructor. super().__init__(*args, **traits) # An image cache to make sure that we only load each image used in the # tool bar exactly once. self._image_cache = ImageCache(self.image_size[0], self.image_size[1]) return # ------------------------------------------------------------------------ # 'ToolBarManager' interface. # ------------------------------------------------------------------------ def create_tool_bar(self, parent, controller=None): """ Creates a tool bar. """ # If a controller is required it can either be set as a trait on the # tool bar manager (the trait is part of the 'ActionManager' API), or # passed in here (if one is passed in here it takes precedence over the # trait). if controller is None: controller = self.controller # Create the control. tool_bar = _ToolBar(self, parent) self._toolbars.append(tool_bar) tool_bar.setObjectName(self.id) tool_bar.setWindowTitle(self.name) if self.show_tool_names: tool_bar.setToolButtonStyle(QtCore.Qt.ToolButtonStyle.ToolButtonTextUnderIcon) if self.orientation == "horizontal": tool_bar.setOrientation(QtCore.Qt.Orientation.Horizontal) else: tool_bar.setOrientation(QtCore.Qt.Orientation.Vertical) # We would normally leave it to the current style to determine the icon # size. w, h = self.image_size tool_bar.setIconSize(QtCore.QSize(w, h)) # Add all of items in the manager's groups to the tool bar. self._qt4_add_tools(parent, tool_bar, controller) return tool_bar # ------------------------------------------------------------------------ # 'ActionManager' interface. # ------------------------------------------------------------------------ def destroy(self): while self._toolbars: toolbar = self._toolbars.pop() toolbar.dispose() super().destroy() # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _qt4_add_tools(self, parent, tool_bar, controller): """ Adds tools for all items in the list of groups. """ previous_non_empty_group = None for group in self.groups: if len(group.items) > 0: # Is a separator required? if previous_non_empty_group is not None and group.separator: separator = tool_bar.addSeparator() group.observe( self._separator_visibility_method(separator), "visible" ) previous_non_empty_group = group # Create a tool bar tool for each item in the group. for item in group.items: item.add_to_toolbar( parent, tool_bar, self._image_cache, controller, self.show_tool_names, ) def _separator_visibility_method(self, separator): """ Method to return closure to set visibility of group separators. """ return lambda event: separator.setVisible(event.new) class _ToolBar(QtGui.QToolBar): """ The toolkit-specific tool bar implementation. """ # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, tool_bar_manager, parent): """ Constructor. """ QtGui.QToolBar.__init__(self, parent) # List of tools self.tools = [] # Listen for changes to the tool bar manager's enablement and # visibility. self.tool_bar_manager = tool_bar_manager self.tool_bar_manager.observe( self._on_tool_bar_manager_enabled_changed, "enabled" ) self.tool_bar_manager.observe( self._on_tool_bar_manager_visible_changed, "visible" ) return def dispose(self): self.tool_bar_manager.observe( self._on_tool_bar_manager_enabled_changed, "enabled", remove=True ) self.tool_bar_manager.observe( self._on_tool_bar_manager_visible_changed, "visible", remove=True ) # Removes event listeners from downstream tools and clears their # references for item in self.tools: item.dispose() self.tools = [] # ------------------------------------------------------------------------ # Trait change handlers. # ------------------------------------------------------------------------ def _on_tool_bar_manager_enabled_changed(self, event): """ Dynamic trait change handler. """ self.setEnabled(event.new) def _on_tool_bar_manager_visible_changed(self, event): """ Dynamic trait change handler. """ self.setVisible(event.new) return pyface-7.4.0/pyface/ui/qt4/action/menu_manager.py0000644000076500000240000002071114176222673022626 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # (C) Copyright 2007 Riverbank Computing Limited # 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 """ The PyQt specific implementation of a menu manager. """ from pyface.qt import QtCore, QtGui from traits.api import Instance, List, Str from pyface.action.action_manager import ActionManager from pyface.action.action_manager_item import ActionManagerItem from pyface.action.action_item import _Tool, Action from pyface.action.group import Group class MenuManager(ActionManager, ActionManagerItem): """ A menu manager realizes itself in a menu control. This could be a sub-menu or a context (popup) menu. """ # 'MenuManager' interface ---------------------------------------------# # The menu manager's name (if the manager is a sub-menu, this is what its # label will be). name = Str() # The default action for tool button when shown in a toolbar (Qt only) action = Instance(Action) # Private interface ---------------------------------------------------# #: Keep track of all created menus in order to properly dispose of them _menus = List() # ------------------------------------------------------------------------ # 'MenuManager' interface. # ------------------------------------------------------------------------ def create_menu(self, parent, controller=None): """ Creates a menu representation of the manager. """ # If a controller is required it can either be set as a trait on the # menu manager (the trait is part of the 'ActionManager' API), or # passed in here (if one is passed in here it takes precedence over the # trait). if controller is None: controller = self.controller menu = _Menu(self, parent, controller) self._menus.append(menu) return menu # ------------------------------------------------------------------------ # 'ActionManager' interface. # ------------------------------------------------------------------------ def destroy(self): while self._menus: menu = self._menus.pop() menu.dispose() super().destroy() # ------------------------------------------------------------------------ # 'ActionManagerItem' interface. # ------------------------------------------------------------------------ def add_to_menu(self, parent, menu, controller): """ Adds the item to a menu. """ submenu = self.create_menu(parent, controller) submenu.menuAction().setText(self.name) menu.addMenu(submenu) def add_to_toolbar( self, parent, tool_bar, image_cache, controller, show_labels=True ): """ Adds the item to a tool bar. """ menu = self.create_menu(parent, controller) if self.action: tool_action = _Tool( parent, tool_bar, image_cache, self, controller, show_labels ).control tool_action.setMenu(menu) else: tool_action = menu.menuAction() tool_bar.addAction(tool_action) tool_action.setText(self.name) tool_button = tool_bar.widgetForAction(tool_action) tool_button.setPopupMode( tool_button.MenuButtonPopup if self.action else tool_button.InstantPopup ) class _Menu(QtGui.QMenu): """ The toolkit-specific menu control. """ # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, manager, parent, controller): """ Creates a new tree. """ # Base class constructor. QtGui.QMenu.__init__(self, parent) # The parent of the menu. self._parent = parent # The manager that the menu is a view of. self._manager = manager # The controller. self._controller = controller # List of menu items self.menu_items = [] # Create the menu structure. self.refresh() # Listen to the manager being updated. self._manager.observe(self.refresh, "changed") self._manager.observe(self._on_enabled_changed, "enabled") self._manager.observe(self._on_visible_changed, "visible") self._manager.observe(self._on_name_changed, "name") self._manager.observe(self._on_image_changed, "action:image") self.setEnabled(self._manager.enabled) self.menuAction().setVisible(self._manager.visible) return def dispose(self): self._manager.observe(self.refresh, "changed", remove=True) self._manager.observe(self._on_enabled_changed, "enabled", remove=True) self._manager.observe(self._on_visible_changed, "visible", remove=True) self._manager.observe(self._on_name_changed, "name", remove=True) self._manager.observe(self._on_image_changed, "action:image", remove=True) # Removes event listeners from downstream menu items self.clear() # ------------------------------------------------------------------------ # '_Menu' interface. # ------------------------------------------------------------------------ def clear(self): """ Clears the items from the menu. """ for item in self.menu_items: item.dispose() self.menu_items = [] super().clear() def is_empty(self): """ Is the menu empty? """ return self.isEmpty() def refresh(self, event=None): """ Ensures that the menu reflects the state of the manager. """ self.clear() manager = self._manager parent = self._parent previous_non_empty_group = None for group in manager.groups: previous_non_empty_group = self._add_group( parent, group, previous_non_empty_group ) self.setEnabled(manager.enabled) def show(self, x=None, y=None): """ Show the menu at the specified location. """ if x is None or y is None: point = QtGui.QCursor.pos() else: point = QtCore.QPoint(x, y) self.popup(point) # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _on_enabled_changed(self, event): """ Dynamic trait change handler. """ self.setEnabled(event.new) def _on_visible_changed(self, event): """ Dynamic trait change handler. """ self.menuAction().setVisible(event.new) def _on_name_changed(self, event): """ Dynamic trait change handler. """ self.menuAction().setText(event.new) def _on_image_changed(self, event): """ Dynamic trait change handler. """ self.menuAction().setIcon(event.new.create_icon()) def _add_group(self, parent, group, previous_non_empty_group=None): """ Adds a group to a menu. """ if len(group.items) > 0: # Is a separator required? if previous_non_empty_group is not None and group.separator: self.addSeparator() # Create actions and sub-menus for each contribution item in # the group. for item in group.items: if isinstance(item, Group): if len(item.items) > 0: self._add_group(parent, item, previous_non_empty_group) if ( previous_non_empty_group is not None and previous_non_empty_group.separator and item.separator ): self.addSeparator() previous_non_empty_group = item else: item.add_to_menu(parent, self, self._controller) previous_non_empty_group = group return previous_non_empty_group pyface-7.4.0/pyface/ui/qt4/action/status_bar_manager.py0000644000076500000240000001043114176222673024027 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # (C) Copyright 2007 Riverbank Computing Limited # 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 # ------------------------------------------------------------------------------ from pyface.qt import QtGui from traits.api import Any, Bool, HasTraits, List, Property, Str class StatusBarManager(HasTraits): """ A status bar manager realizes itself in a status bar control. """ # The message displayed in the first field of the status bar. message = Property # The messages to be displayed in the status bar fields. messages = List(Str) # The toolkit-specific control that represents the status bar. status_bar = Any() # Whether to show a size grip on the status bar. size_grip = Bool(False) # Whether the status bar is visible. visible = Bool(True) # ------------------------------------------------------------------------ # 'StatusBarManager' interface. # ------------------------------------------------------------------------ def create_status_bar(self, parent): """ Creates a status bar. """ if self.status_bar is None: self.status_bar = QtGui.QStatusBar(parent) self.status_bar.setSizeGripEnabled(self.size_grip) self.status_bar.setVisible(self.visible) if len(self.messages) > 1: self._show_messages() else: self.status_bar.showMessage(self.message) return self.status_bar def destroy_status_bar(self): """ Destroys the status bar. """ if self.status_bar is not None: self.status_bar.deleteLater() self.status_bar = None # ------------------------------------------------------------------------ # Property handlers. # ------------------------------------------------------------------------ def _get_message(self): if len(self.messages) > 0: message = self.messages[0] else: message = "" return message def _set_message(self, value): if len(self.messages) > 0: old = self.messages[0] self.messages[0] = value else: old = "" self.messages.append(old) self.trait_property_changed("message", old, value) # ------------------------------------------------------------------------ # Trait event handlers. # ------------------------------------------------------------------------ def _messages_changed(self): """ Sets the text displayed on the status bar. """ if self.status_bar is not None: self._show_messages() def _messages_items_changed(self): """ Sets the text displayed on the status bar. """ if self.status_bar is not None: self._show_messages() def _size_grip_changed(self): """ Turns the size grip on the status bar on and off. """ if self.status_bar is not None: self.status_bar.setSizeGripEnabled(self.size_grip) def _visible_changed(self): """ Turns the status bar visibility on and off. """ if self.status_bar is not None: self.status_bar.setVisible(self.visible) # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _show_messages(self): """ Display the list of messages. """ # FIXME v3: At the moment we just string them together but we may # decide to put all but the first message into separate widgets. We # probably also need to extend the API to allow a "message" to be a # widget - depends on what wx is capable of. self.status_bar.showMessage(" ".join(self.messages)) pyface-7.4.0/pyface/ui/qt4/action/__init__.py0000644000076500000240000000062714176222673021733 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! pyface-7.4.0/pyface/ui/qt4/action/action_item.py0000644000076500000240000005207014176253531022463 0ustar cwebsterstaff00000000000000# (C) Copyright 2007 Riverbank Computing Limited # (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 PyQt specific implementations the action manager internal classes. """ from inspect import getfullargspec from pyface.qt import QtGui from traits.api import Any, Bool, HasTraits from pyface.action.action_event import ActionEvent class PyfaceWidgetAction(QtGui.QWidgetAction): def __init__(self, parent, action): super().__init__(parent) self.action = action def createWidget(self, parent): widget = self.action.create_control(parent) widget._action = self.action return widget class _MenuItem(HasTraits): """ A menu item representation of an action item. """ # '_MenuItem' interface ------------------------------------------------ # Is the item checked? checked = Bool(False) # A controller object we delegate taking actions through (if any). controller = Any() # Is the item enabled? enabled = Bool(True) # Is the item visible? visible = Bool(True) # The radio group we are part of (None if the menu item is not part of such # a group). group = Any() # The toolkit control. control = Any() # The toolkit control id. control_id = None # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, parent, menu, item, controller): """ Creates a new menu item for an action item. """ self.item = item action = item.action # FIXME v3: This is a wx'ism and should be hidden in the toolkit code. self.control_id = None if action.style == "widget": self.control = PyfaceWidgetAction(parent, action) menu.addAction(self.control) elif action.image is None: self.control = menu.addAction( action.name, self._qt4_on_triggered, action.accelerator ) else: self.control = menu.addAction( action.image.create_icon(), action.name, self._qt4_on_triggered, action.accelerator, ) menu.menu_items.append(self) self.control.setToolTip(action.tooltip) self.control.setWhatsThis(action.description) self.control.setEnabled(action.enabled) self.control.setVisible(action.visible) if getattr(action, "menu_role", False): if action.menu_role == "About": self.control.setMenuRole(QtGui.QAction.MenuRole.AboutRole) elif action.menu_role == "Preferences": self.control.setMenuRole(QtGui.QAction.MenuRole.PreferencesRole) if action.style == "toggle": self.control.setCheckable(True) self.control.setChecked(action.checked) elif action.style == "radio": # Create an action group if it hasn't already been done. try: ag = item.parent._qt4_ag except AttributeError: ag = item.parent._qt4_ag = QtGui.QActionGroup(parent) self.control.setActionGroup(ag) self.control.setCheckable(True) self.control.setChecked(action.checked) # Listen for trait changes on the action (so that we can update its # enabled/disabled/checked state etc). action.observe(self._on_action_enabled_changed, "enabled") action.observe(self._on_action_visible_changed, "visible") action.observe(self._on_action_checked_changed, "checked") action.observe(self._on_action_name_changed, "name") action.observe(self._on_action_accelerator_changed, "accelerator") action.observe(self._on_action_image_changed, "image") action.observe(self._on_action_tooltip_changed, "tooltip") # Detect if the control is destroyed. self.control.destroyed.connect(self._qt4_on_destroyed) if controller is not None: self.controller = controller controller.add_to_menu(self) def dispose(self): action = self.item.action action.observe(self._on_action_enabled_changed, "enabled", remove=True) action.observe(self._on_action_visible_changed, "visible", remove=True) action.observe(self._on_action_checked_changed, "checked", remove=True) action.observe(self._on_action_name_changed, "name", remove=True) action.observe( self._on_action_accelerator_changed, "accelerator", remove=True ) action.observe(self._on_action_image_changed, "image", remove=True) action.observe(self._on_action_tooltip_changed, "tooltip", remove=True) # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _qt4_on_destroyed(self, control=None): """ Delete the reference to the control to avoid attempting to talk to it again. """ self.control = None def _qt4_on_triggered(self): """ Called when the menu item has been clicked. """ action = self.item.action action_event = ActionEvent() is_checkable = action.style in ["radio", "toggle"] # Perform the action! if self.controller is not None: if is_checkable: self.checked = action.checked = self.control.isChecked() # Most of the time, action's do no care about the event (it # contains information about the time the event occurred etc), so # we only pass it if the perform method requires it. This is also # useful as Traits UI controllers *never* require the event. argspec = getfullargspec(self.controller.perform) # If the only arguments are 'self' and 'action' then don't pass # the event! if len(argspec.args) == 2: self.controller.perform(action) else: self.controller.perform(action, action_event) else: if is_checkable: self.checked = action.checked = self.control.isChecked() # Most of the time, action's do no care about the event (it # contains information about the time the event occurred etc), so # we only pass it if the perform method requires it. argspec = getfullargspec(action.perform) # If the only argument is 'self' then don't pass the event! if len(argspec.args) == 1: action.perform() else: action.perform(action_event) # Trait event handlers ------------------------------------------------- def _enabled_changed(self): """ Called when our 'enabled' trait is changed. """ if self.control is not None: self.control.setEnabled(self.enabled) def _visible_changed(self): """ Called when our 'visible' trait is changed. """ if self.control is not None: self.control.setVisible(self.visible) def _checked_changed(self): """ Called when our 'checked' trait is changed. """ if self.control is not None: self.control.setChecked(self.checked) def _on_action_enabled_changed(self, event): """ Called when the enabled trait is changed on an action. """ action = event.object if self.control is not None: self.control.setEnabled(action.enabled) def _on_action_visible_changed(self, event): """ Called when the visible trait is changed on an action. """ action = event.object if self.control is not None: self.control.setVisible(action.visible) def _on_action_checked_changed(self, event): """ Called when the checked trait is changed on an action. """ action = event.object if self.control is not None: self.control.setChecked(action.checked) def _on_action_name_changed(self, event): """ Called when the name trait is changed on an action. """ action = event.object if self.control is not None: self.control.setText(action.name) def _on_action_accelerator_changed(self, event): """ Called when the accelerator trait is changed on an action. """ action = event.object if self.control is not None: self.control.setShortcut(action.accelerator) def _on_action_image_changed(self, event): """ Called when the accelerator trait is changed on an action. """ action = event.object if self.control is not None: self.control.setIcon(action.image.create_icon()) def _on_action_tooltip_changed(self, event): """ Called when the accelerator trait is changed on an action. """ action = event.object if self.control is not None: self.control.setToolTip(action.tooltip) class _Tool(HasTraits): """ A tool bar tool representation of an action item. """ # '_Tool' interface ---------------------------------------------------- # Is the item checked? checked = Bool(False) # A controller object we delegate taking actions through (if any). controller = Any() # Is the item enabled? enabled = Bool(True) # Is the item visible? visible = Bool(True) # The radio group we are part of (None if the tool is not part of such a # group). group = Any() # The toolkit control. control = Any() # The toolkit control id. control_id = None # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__( self, parent, tool_bar, image_cache, item, controller, show_labels ): """ Creates a new tool bar tool for an action item. """ self.item = item self.tool_bar = tool_bar action = item.action if action.style == "widget": widget = action.create_control(tool_bar) self.control = tool_bar.addWidget(widget) elif action.image is None: self.control = tool_bar.addAction(action.name) else: size = tool_bar.iconSize() image = action.image.create_icon((size.width(), size.height())) self.control = tool_bar.addAction(image, action.name) tool_bar.tools.append(self) self.control.triggered.connect(self._qt4_on_triggered) self.control.setToolTip(action.tooltip) self.control.setWhatsThis(action.description) self.control.setEnabled(action.enabled) self.control.setVisible(action.visible) if action.style == "toggle": self.control.setCheckable(True) self.control.setChecked(action.checked) elif action.style == "radio": # Create an action group if it hasn't already been done. try: ag = item.parent._qt4_ag except AttributeError: ag = item.parent._qt4_ag = QtGui.QActionGroup(parent) self.control.setActionGroup(ag) self.control.setCheckable(True) self.control.setChecked(action.checked) # Keep a reference in the action. This is done to make sure we live as # long as the action (and still respond to its signals) and don't die # if the manager that created us is garbage collected. self.control._tool_instance = self # Listen for trait changes on the action (so that we can update its # enabled/disabled/checked state etc). action.observe(self._on_action_enabled_changed, "enabled") action.observe(self._on_action_visible_changed, "visible") action.observe(self._on_action_checked_changed, "checked") action.observe(self._on_action_name_changed, "name") action.observe(self._on_action_accelerator_changed, "accelerator") action.observe(self._on_action_image_changed, "image") action.observe(self._on_action_tooltip_changed, "tooltip") # Detect if the control is destroyed. self.control.destroyed.connect(self._qt4_on_destroyed) if controller is not None: self.controller = controller controller.add_to_toolbar(self) def dispose(self): action = self.item.action action.observe(self._on_action_enabled_changed, "enabled", remove=True) action.observe(self._on_action_visible_changed, "visible", remove=True) action.observe(self._on_action_checked_changed, "checked", remove=True) action.observe(self._on_action_name_changed, "name", remove=True) action.observe( self._on_action_accelerator_changed, "accelerator", remove=True ) action.observe(self._on_action_image_changed, "image", remove=True) action.observe(self._on_action_tooltip_changed, "tooltip", remove=True) # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _qt4_on_destroyed(self, control=None): """ Delete the reference to the control to avoid attempting to talk to it again. """ self.control = None def _qt4_on_triggered(self): """ Called when the tool bar tool is clicked. """ action = self.item.action action_event = ActionEvent() # Perform the action! if self.controller is not None: self.checked = action.checked = self.control.isChecked() # Most of the time, action's do no care about the event (it # contains information about the time the event occurred etc), so # we only pass it if the perform method requires it. This is also # useful as Traits UI controllers *never* require the event. argspec = getfullargspec(self.controller.perform) # If the only arguments are 'self' and 'action' then don't pass # the event! if len(argspec.args) == 2: self.controller.perform(action) else: self.controller.perform(action, action_event) else: self.checked = action.checked = self.control.isChecked() # Most of the time, action's do no care about the event (it # contains information about the time the event occurred etc), so # we only pass it if the perform method requires it. argspec = getfullargspec(action.perform) # If the only argument is 'self' then don't pass the event! if len(argspec.args) == 1: action.perform() else: action.perform(action_event) # Trait event handlers ------------------------------------------------- def _enabled_changed(self): """ Called when our 'enabled' trait is changed. """ if self.control is not None: self.control.setEnabled(self.enabled) def _visible_changed(self): """ Called when our 'visible' trait is changed. """ if self.control is not None: self.control.setVisible(self.visible) def _checked_changed(self): """ Called when our 'checked' trait is changed. """ if self.control is not None: self.control.setChecked(self.checked) def _on_action_enabled_changed(self, event): """ Called when the enabled trait is changed on an action. """ action = event.object if self.control is not None: self.control.setEnabled(action.enabled) def _on_action_visible_changed(self, event): """ Called when the visible trait is changed on an action. """ action = event.object if self.control is not None: self.control.setVisible(action.visible) def _on_action_checked_changed(self, event): """ Called when the checked trait is changed on an action. """ action = event.object if self.control is not None: self.control.setChecked(action.checked) def _on_action_name_changed(self, event): """ Called when the name trait is changed on an action. """ action = event.object if self.control is not None: self.control.setText(action.name) def _on_action_accelerator_changed(self, event): """ Called when the accelerator trait is changed on an action. """ action = event.object if self.control is not None: self.control.setShortcut(action.accelerator) def _on_action_image_changed(self, event): """ Called when the accelerator trait is changed on an action. """ action = event.object if self.control is not None: size = self.tool_bar.iconSize() self.control.setIcon( action.image.create_icon((size.width(), size.height())) ) def _on_action_tooltip_changed(self, event): """ Called when the accelerator trait is changed on an action. """ action = event.object if self.control is not None: self.control.setToolTip(action.tooltip) class _PaletteTool(HasTraits): """ A tool palette representation of an action item. """ # '_PaletteTool' interface --------------------------------------------- # The radio group we are part of (None if the tool is not part of such a # group). group = Any() # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, tool_palette, image_cache, item, show_labels): """ Creates a new tool palette tool for an action item. """ self.item = item self.tool_palette = tool_palette action = self.item.action label = action.name if action.style == "widget": raise NotImplementedError( "Qt does not support widgets in palettes" ) # Tool palette tools never have '...' at the end. if label.endswith("..."): label = label[:-3] # And they never contain shortcuts. label = label.replace("&", "") path = action.image.absolute_path bmp = image_cache.get_bitmap(path) kind = action.style tooltip = action.tooltip longtip = action.description if not show_labels: label = "" # Add the tool to the tool palette. self.tool_id = tool_palette.add_tool( label, bmp, kind, tooltip, longtip ) tool_palette.toggle_tool(self.tool_id, action.checked) tool_palette.enable_tool(self.tool_id, action.enabled) tool_palette.on_tool_event(self.tool_id, self._on_tool) # Listen to the trait changes on the action (so that we can update its # enabled/disabled/checked state etc). action.observe(self._on_action_enabled_changed, "enabled") action.observe(self._on_action_checked_changed, "checked") return # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ # Trait event handlers ------------------------------------------------- def _on_action_enabled_changed(self, event): """ Called when the enabled trait is changed on an action. """ action = event.object self.tool_palette.enable_tool(self.tool_id, action.enabled) def _on_action_checked_changed(self, event): """ Called when the checked trait is changed on an action. """ action = event.object if action.style == "radio": # If we're turning this one on, then we need to turn all the others # off. But if we're turning this one off, don't worry about the # others. if event.new: for item in self.item.parent.items: if item is not self.item: item.action.checked = False # This will *not* emit a tool event. self.tool_palette.toggle_tool(self.tool_id, event.new) return # Tool palette event handlers -----------------------------------------# def _on_tool(self, event): """ Called when the tool palette button is clicked. """ action = self.item.action action_event = ActionEvent() # Perform the action! action.checked = self.tool_palette.get_tool_state(self.tool_id) == 1 action.perform(action_event) return def dispose(self): action = self.item.action action.observe(self._on_action_enabled_changed, "enabled", remove=True) action.observe(self._on_action_checked_changed, "checked", remove=True) pyface-7.4.0/pyface/ui/qt4/action/menu_bar_manager.py0000644000076500000240000000433314176222673023454 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # (C) Copyright 2007 Riverbank Computing Limited # 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 """ The PyQt specific implementation of a menu bar manager. """ import sys from pyface.qt import QtGui from pyface.action.action_manager import ActionManager class MenuBarManager(ActionManager): """ A menu bar manager realizes itself in errr, a menu bar control. """ # ------------------------------------------------------------------------ # 'MenuBarManager' interface. # ------------------------------------------------------------------------ def create_menu_bar(self, parent, controller=None): """ Creates a menu bar representation of the manager. """ # If a controller is required it can either be set as a trait on the # menu bar manager (the trait is part of the 'ActionManager' API), or # passed in here (if one is passed in here it takes precedence over the # trait). if controller is None: controller = self.controller # Create the menu bar. Work around disappearing menu bars on OS X # (particulary on PySide but also under certain circumstances on PyQt4). if isinstance(parent, QtGui.QMainWindow) and sys.platform == "darwin": parent.menuBar().setParent(None) menu_bar = parent.menuBar() else: menu_bar = QtGui.QMenuBar(parent) # Every item in every group must be a menu manager. for group in self.groups: for item in group.items: menu = item.create_menu(parent, controller) menu.menuAction().setText(item.name) menu_bar.addMenu(menu) return menu_bar pyface-7.4.0/pyface/ui/qt4/workbench/0000755000076500000240000000000014176460551020321 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/qt4/workbench/images/0000755000076500000240000000000014176460551021566 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/qt4/workbench/images/spinner.gif0000644000076500000240000000331114176222673023732 0ustar cwebsterstaff00000000000000GIF89aԔ```@@@DDDnnnܤ$$$(((000666>>>jjjrrrŠPPP^^^:::\\\|||ↆhhhfffFFF xxx֮ƼzzzLLLRRRZZZbbbBBB<<B;>CD ;+F &03 8<݄!'14,< "PF`! ,;WX []<U Y-\$_&JOSHTJ^*\GP2 #7HKQ:6L=FF'MijNA:bIT@?R/D924I ::/E`ӃA&V,ZJCaTT9Ij! , i^,CK jl8^ghQCRk "dNJnj2H0T.-435GЄ`eC6:f:=σG5AT770/6@C(CBA;@/܆F r Z@! ,?(N!lG9'g[jNJA#HlF=B]ON*#F`8"p A/`9"07qE "T=36n F o)! ,aoe&? +qe4H2D< @33 %F8<;J @@ce[l>8TBjuaB7KAǐ0Cv^ ur6b?ՅY)3T3+'s 6F0:.ntn830֭! ,0@9(+ATA8cJq >`GT6:"VVA6@>/B4eb= oRTD3>hF^M'0F7BLd63Ӑ]jl7ۅ&NG+TTc1SOJvwn\-Yr;"0:TD AB8Q"6/37;(I@0=:GJ73QfA6D,<3e34.1&2*^Z\3#*N DK$+C9 \T6Jc -:gY(DZe u,AKXqoPt2Q=FAl!+ OjLM^aG1N\@ÕA;pyface-7.4.0/pyface/ui/qt4/workbench/tests/0000755000076500000240000000000014176460551021463 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/qt4/workbench/tests/__init__.py0000644000076500000240000000000014176222673023563 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/qt4/workbench/tests/test_workbench_window_layout.py0000644000076500000240000000305514176222673030046 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 sys import unittest from unittest import mock from pyface.ui.qt4.workbench.split_tab_widget import SplitTabWidget from pyface.ui.qt4.workbench.workbench_window_layout import ( WorkbenchWindowLayout, ) class TestWorkbenchWindowLayout(unittest.TestCase): @unittest.skipIf(sys.version_info == (3, 8), "Can't mock SplitTabWidget") def test_change_of_active_qt_editor(self): # Test error condition for enthought/mayavi#321 # This doesn't work on Python 3.8 because of some incompatibility # between unittest.mock.Mock and Qt, I think mock_split_tab_widget = mock.Mock(spec=SplitTabWidget) layout = WorkbenchWindowLayout(_qt4_editor_area=mock_split_tab_widget) class DummyEvent: def __init__(self, new): self.new = new # This should not throw layout._qt4_active_editor_changed(DummyEvent(new=None)) self.assertEqual(mock_split_tab_widget.setTabTextColor.called, False) mock_active_editor = mock.Mock() layout._qt4_active_editor_changed(mock_active_editor) self.assertEqual(mock_split_tab_widget.setTabTextColor.called, True) pyface-7.4.0/pyface/ui/qt4/workbench/__init__.py0000644000076500000240000000062714176222673022440 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! pyface-7.4.0/pyface/ui/qt4/workbench/view.py0000644000076500000240000000353614176253531021652 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # (C) Copyright 2007 Riverbank Computing Limited # 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 from pyface.workbench.i_view import MView class View(MView): """ The toolkit specific implementation of a View. See the IView interface for the API documentation. """ # ------------------------------------------------------------------------ # 'IWorkbenchPart' interface. # ------------------------------------------------------------------------ def create_control(self, parent): """ Create the toolkit-specific control that represents the part. """ from pyface.qt import QtGui control = QtGui.QWidget(parent) palette = control.palette() palette.setColor(QtGui.QPalette.ColorRole.Window, QtGui.QColor("red")) control.setPalette(palette) control.setAutoFillBackground(True) return control def destroy_control(self): """ Destroy the toolkit-specific control that represents the part. """ if self.control is not None: self.control.hide() self.control.deleteLater() self.control = None def set_focus(self): """ Set the focus to the appropriate control in the part. """ if self.control is not None: self.control.setFocus() return pyface-7.4.0/pyface/ui/qt4/workbench/workbench_window_layout.py0000755000076500000240000005331114176253531025645 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # (C) Copyright 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 import logging from pyface.qt import QtCore, QtGui from traits.api import Instance, observe from pyface.message_dialog import error from pyface.workbench.i_workbench_window_layout import MWorkbenchWindowLayout from .split_tab_widget import SplitTabWidget # Logging. logger = logging.getLogger(__name__) # For mapping positions relative to the editor area. _EDIT_AREA_MAP = { "left": QtCore.Qt.DockWidgetArea.LeftDockWidgetArea, "right": QtCore.Qt.DockWidgetArea.RightDockWidgetArea, "top": QtCore.Qt.DockWidgetArea.TopDockWidgetArea, "bottom": QtCore.Qt.DockWidgetArea.BottomDockWidgetArea, } # For mapping positions relative to another view. _VIEW_AREA_MAP = { "left": (QtCore.Qt.Orientation.Horizontal, True), "right": (QtCore.Qt.Orientation.Horizontal, False), "top": (QtCore.Qt.Orientation.Vertical, True), "bottom": (QtCore.Qt.Orientation.Vertical, False), } class WorkbenchWindowLayout(MWorkbenchWindowLayout): """ The Qt4 implementation of the workbench window layout interface. See the 'IWorkbenchWindowLayout' interface for the API documentation. """ # Private interface ---------------------------------------------------- # The widget that provides the editor area. We keep (and use) this # separate reference because we can't always assume that it has been set to # be the main window's central widget. _qt4_editor_area = Instance(SplitTabWidget) # ------------------------------------------------------------------------ # 'IWorkbenchWindowLayout' interface. # ------------------------------------------------------------------------ def activate_editor(self, editor): if editor.control is not None: editor.control.show() self._qt4_editor_area.setCurrentWidget(editor.control) editor.set_focus() return editor def activate_view(self, view): # FIXME v3: This probably doesn't work as expected. view.control.raise_() view.set_focus() return view def add_editor(self, editor, title): if editor is None: return None try: self._qt4_editor_area.addTab( self._qt4_get_editor_control(editor), title ) if editor._loading_on_open: self._qt4_editor_tab_spinner(editor, "", True) except Exception: logger.exception("error creating editor control [%s]", editor.id) return editor def add_view(self, view, position=None, relative_to=None, size=(-1, -1)): if view is None: return None try: self._qt4_add_view(view, position, relative_to, size) view.visible = True except Exception: logger.exception("error creating view control [%s]", view.id) # Even though we caught the exception, it sometimes happens that # the view's control has been created as a child of the application # window (or maybe even the dock control). We should destroy the # control to avoid bad UI effects. view.destroy_control() # Additionally, display an error message to the user. error( self.window.control, "Unable to add view [%s]" % view.id, "Workbench Plugin Error", ) return view def close_editor(self, editor): if editor.control is not None: editor.control.close() return editor def close_view(self, view): self.hide_view(view) return view def close(self): # Don't fire signals for editors that have destroyed their controls. self._qt4_editor_area.editor_has_focus.disconnect( self._qt4_editor_focus ) self._qt4_editor_area.clear() # Delete all dock widgets. for v in self.window.views: if self.contains_view(v): self._qt4_delete_view_dock_widget(v) def create_initial_layout(self, parent): self._qt4_editor_area = editor_area = SplitTabWidget(parent) editor_area.editor_has_focus.connect(self._qt4_editor_focus) # We are interested in focus changes but we get them from the editor # area rather than qApp to allow the editor area to restrict them when # needed. editor_area.focus_changed.connect(self._qt4_view_focus_changed) editor_area.tabTextChanged.connect(self._qt4_editor_title_changed) editor_area.new_window_request.connect(self._qt4_new_window_request) editor_area.tab_close_request.connect(self._qt4_tab_close_request) editor_area.tab_window_changed.connect(self._qt4_tab_window_changed) return editor_area def contains_view(self, view): return hasattr(view, "_qt4_dock") def hide_editor_area(self): self._qt4_editor_area.hide() def hide_view(self, view): view._qt4_dock.hide() view.visible = False return view def refresh(self): # Nothing to do. pass def reset_editors(self): self._qt4_editor_area.setCurrentIndex(0) def reset_views(self): # Qt doesn't provide information about the order of dock widgets in a # dock area. pass def show_editor_area(self): self._qt4_editor_area.show() def show_view(self, view): view._qt4_dock.show() view.visible = True # Methods for saving and restoring the layout -------------------------# def get_view_memento(self): # Get the IDs of the views in the main window. This information is # also in the QMainWindow state, but that is opaque. view_ids = [v.id for v in self.window.views if self.contains_view(v)] # Everything else is provided by QMainWindow. state = self.window.control.saveState() return (0, (view_ids, state)) def set_view_memento(self, memento): version, mdata = memento # There has only ever been version 0 so far so check with an assert. assert version == 0 # Now we know the structure of the memento we can "parse" it. view_ids, state = mdata # Get a list of all views that have dock widgets and mark them. dock_views = [v for v in self.window.views if self.contains_view(v)] for v in dock_views: v._qt4_gone = True # Create a dock window for all views that had one last time. for v in self.window.views: # Make sure this is in a known state. v.visible = False for vid in view_ids: if vid == v.id: # Create the dock widget if needed and make sure that it is # invisible so that it matches the state of the visible # trait. Things will all come right when the main window # state is restored below. self._qt4_create_view_dock_widget(v).setVisible(False) if v in dock_views: delattr(v, "_qt4_gone") break # Remove any remain unused dock widgets. for v in dock_views: try: delattr(v, "_qt4_gone") except AttributeError: pass else: self._qt4_delete_view_dock_widget(v) # Restore the state. This will update the view's visible trait through # the dock window's toggle action. self.window.control.restoreState(state) def get_editor_memento(self): # Get the layout of the editors. editor_layout = self._qt4_editor_area.saveState() # Get a memento for each editor that describes its contents. editor_references = self._get_editor_references() return (0, (editor_layout, editor_references)) def set_editor_memento(self, memento): version, mdata = memento # There has only ever been version 0 so far so check with an assert. assert version == 0 # Now we know the structure of the memento we can "parse" it. editor_layout, editor_references = mdata def resolve_id(id): # Get the memento for the editor contents (if any). editor_memento = editor_references.get(id) if editor_memento is None: return None # Create the restored editor. editor = self.window.editor_manager.set_editor_memento( editor_memento ) if editor is None: return None # Save the editor. self.window.editors.append(editor) # Create the control if needed and return it. return self._qt4_get_editor_control(editor) self._qt4_editor_area.restoreState(editor_layout, resolve_id) def get_toolkit_memento(self): return (0, {"geometry": self.window.control.saveGeometry()}) def set_toolkit_memento(self, memento): if hasattr(memento, "toolkit_data"): data = memento.toolkit_data if isinstance(data, tuple) and len(data) == 2: version, datadict = data if version == 0: geometry = datadict.pop("geometry", None) if geometry is not None: self.window.control.restoreGeometry(geometry) def is_editor_area_visible(self): return self._qt4_editor_area.isVisible() # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _qt4_editor_focus(self, new): """ Handle an editor getting the focus. """ for editor in self.window.editors: control = editor.control editor.has_focus = control is new or ( control is not None and new in control.children() ) def _qt4_editor_title_changed(self, control, title): """ Handle the title being changed """ for editor in self.window.editors: if editor.control == control: editor.name = str(title) def _qt4_editor_tab_spinner(self, event): editor = event.object # Do we need to do this verification? tw, tidx = self._qt4_editor_area._tab_widget(editor.control) if event.new: tw.show_button(tidx) else: tw.hide_button(tidx) if not event.new and not editor == self.window.active_editor: self._qt4_editor_area.setTabTextColor( editor.control, QtCore.Qt.GlobalColor.red ) @observe("window:active_editor") def _qt4_active_editor_changed(self, event): """ Handle change of active editor """ # Reset tab title to foreground color editor = event.new if editor is not None: self._qt4_editor_area.setTabTextColor(editor.control) def _qt4_view_focus_changed(self, old, new): """ Handle the change of focus for a view. """ focus_part = None if new is not None: # Handle focus changes to views. for view in self.window.views: if view.control is not None and view.control.isAncestorOf(new): view.has_focus = True focus_part = view break if old is not None: # Handle focus changes from views. for view in self.window.views: if ( view is not focus_part and view.control is not None and view.control.isAncestorOf(old) ): view.has_focus = False break def _qt4_new_window_request(self, pos, control): """ Handle a tab tear-out request from the splitter widget. """ editor = self._qt4_remove_editor_with_control(control) kind = self.window.editor_manager.get_editor_kind(editor) window = self.window.workbench.create_window() window.open() window.add_editor(editor) window.editor_manager.add_editor(editor, kind) window.position = (pos.x(), pos.y()) window.size = self.window.size window.activate_editor(editor) editor.window = window def _qt4_tab_close_request(self, control): """ Handle a tabCloseRequest from the splitter widget. """ for editor in self.window.editors: if editor.control == control: editor.close() break def _qt4_tab_window_changed(self, control): """ Handle a tab drag to a different WorkbenchWindow. """ editor = self._qt4_remove_editor_with_control(control) kind = self.window.editor_manager.get_editor_kind(editor) while not control.isWindow(): control = control.parent() for window in self.window.workbench.windows: if window.control == control: window.editors.append(editor) window.editor_manager.add_editor(editor, kind) window.layout._qt4_get_editor_control(editor) window.activate_editor(editor) editor.window = window break def _qt4_remove_editor_with_control(self, control): """ Finds the editor associated with 'control' and removes it. Returns the editor, or None if no editor was found. """ for editor in self.window.editors: if editor.control == control: self.editor_closing = editor control.removeEventFilter(self._qt4_mon) self.editor_closed = editor # Make sure that focus events get fired if this editor is # subsequently added to another window. editor.has_focus = False return editor def _qt4_get_editor_control(self, editor): """ Create the editor control if it hasn't already been done. """ if editor.control is None: self.editor_opening = editor # We must provide a parent (because TraitsUI checks for it when # deciding what sort of panel to create) but it can't be the editor # area (because it will be automatically added to the base # QSplitter). editor.control = editor.create_control(self.window.control) editor.control.setObjectName(editor.id) editor.observe(self._qt4_editor_tab_spinner, "_loading") self.editor_opened = editor def on_name_changed(event): editor = event.object self._qt4_editor_area.setWidgetTitle(editor.control, editor.name) editor.observe(on_name_changed, "name") self._qt4_monitor(editor.control) return editor.control def _qt4_add_view(self, view, position, relative_to, size): """ Add a view. """ # If no specific position is specified then use the view's default # position. if position is None: position = view.position dw = self._qt4_create_view_dock_widget(view, size) mw = self.window.control try: rel_dw = relative_to._qt4_dock except AttributeError: rel_dw = None if rel_dw is None: # If we are trying to add a view with a non-existent item, then # just default to the left of the editor area. if position == "with": position = "left" # Position the view relative to the editor area. try: dwa = _EDIT_AREA_MAP[position] except KeyError: raise ValueError("unknown view position: %s" % position) mw.addDockWidget(dwa, dw) elif position == "with": # FIXME v3: The Qt documentation says that the second should be # placed above the first, but it always seems to be underneath (ie. # hidden) which is not what the user is expecting. mw.tabifyDockWidget(rel_dw, dw) else: try: orient, swap = _VIEW_AREA_MAP[position] except KeyError: raise ValueError("unknown view position: %s" % position) mw.splitDockWidget(rel_dw, dw, orient) # The Qt documentation implies that the layout direction can be # used to position the new dock widget relative to the existing one # but I could only get the button positions to change. Instead we # move things around afterwards if required. if swap: mw.removeDockWidget(rel_dw) mw.splitDockWidget(dw, rel_dw, orient) rel_dw.show() def _qt4_create_view_dock_widget(self, view, size=(-1, -1)): """ Create a dock widget that wraps a view. """ # See if it has already been created. try: dw = view._qt4_dock except AttributeError: dw = QtGui.QDockWidget(view.name, self.window.control) dw.setWidget(_ViewContainer(size, self.window.control)) dw.setObjectName(view.id) dw.toggleViewAction().toggled.connect( self._qt4_handle_dock_visibility ) dw.visibilityChanged.connect(self._qt4_handle_dock_visibility) # Save the dock window. view._qt4_dock = dw def on_name_changed(event): view._qt4_dock.setWindowTitle(view.name) view.observe(on_name_changed, "name") # Make sure the view control exists. if view.control is None: # Make sure that the view knows which window it is in. view.window = self.window try: view.control = view.create_control(dw.widget()) except: # Tidy up if the view couldn't be created. delattr(view, "_qt4_dock") self.window.control.removeDockWidget(dw) dw.deleteLater() del dw raise dw.widget().setCentralWidget(view.control) return dw def _qt4_delete_view_dock_widget(self, view): """ Delete a view's dock widget. """ dw = view._qt4_dock # Disassociate the view from the dock. if view.control is not None: view.control.setParent(None) delattr(view, "_qt4_dock") # Delete the dock (and the view container). self.window.control.removeDockWidget(dw) dw.deleteLater() def _qt4_handle_dock_visibility(self, checked): """ Handle the visibility of a dock window changing. """ # Find the dock window by its toggle action. for v in self.window.views: try: dw = v._qt4_dock except AttributeError: continue sender = dw.sender() if sender is dw.toggleViewAction() or sender in dw.children(): # Toggling the action or pressing the close button on # the view v.visible = checked def _qt4_monitor(self, control): """ Install an event filter for a view or editor control to keep an eye on certain events. """ # Create the monitoring object if needed. try: mon = self._qt4_mon except AttributeError: mon = self._qt4_mon = _Monitor(self) control.installEventFilter(mon) class _Monitor(QtCore.QObject): """ This class monitors a view or editor control. """ def __init__(self, layout): QtCore.QObject.__init__(self, layout.window.control) self._layout = layout def eventFilter(self, obj, e): if isinstance(e, QtGui.QCloseEvent): for editor in self._layout.window.editors: if editor.control is obj: self._layout.editor_closing = editor editor.destroy_control() self._layout.editor_closed = editor break return False class _ViewContainer(QtGui.QMainWindow): """ This class is a container for a view that allows an initial size (specified as a tuple) to be set. """ def __init__(self, size, main_window): """ Initialise the object. """ QtGui.QMainWindow.__init__(self) # Save the size and main window. self._width, self._height = size self._main_window = main_window def sizeHint(self): """ Reimplemented to return the initial size or the view's current size. """ sh = self.centralWidget().sizeHint() if self._width > 0: if self._width > 1: w = self._width else: w = self._main_window.width() * self._width sh.setWidth(int(w)) if self._height > 0: if self._height > 1: h = self._height else: h = self._main_window.height() * self._height sh.setHeight(int(h)) return sh def showEvent(self, e): """ Reimplemented to use the view's current size once shown. """ self._width = self._height = -1 QtGui.QMainWindow.showEvent(self, e) pyface-7.4.0/pyface/ui/qt4/workbench/editor.py0000644000076500000240000000471314176253531022164 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # (C) Copyright 2007 Riverbank Computing Limited # 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 from traits.api import Event, Bool from pyface.workbench.i_editor import MEditor class Editor(MEditor): """ The toolkit specific implementation of an Editor. See the IEditor interface for the API documentation. """ # Traits for showing spinner _loading = Event(Bool) _loading_on_open = Bool(False) # ------------------------------------------------------------------------ # 'IWorkbenchPart' interface. # ------------------------------------------------------------------------ def create_control(self, parent): """ Create the toolkit-specific control that represents the part. """ from pyface.qt import QtCore, QtGui # By default we create a yellow panel! control = QtGui.QWidget(parent) pal = control.palette() pal.setColour(QtGui.QPalette.ColorRole.Window, QtCore.Qt.GlobalColor.yellow) control.setPalette(pal) control.setAutoFillBackground(True) control.resize(100, 200) return control def destroy_control(self): """ Destroy the toolkit-specific control that represents the part. """ if self.control is not None: # The `close` method emits a closeEvent event which is listened # by the workbench window layout, which responds by calling # destroy_control again. # We copy the control locally and set it to None immediately # to make sure this block of code is executed exactly once. _control = self.control self.control = None _control.hide() _control.close() _control.deleteLater() def set_focus(self): """ Set the focus to the appropriate control in the part. """ if self.control is not None: self.control.setFocus() return pyface-7.4.0/pyface/ui/qt4/workbench/split_tab_widget.py0000644000076500000240000010744714176253531024232 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # (C) Copyright 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 # ------------------------------------------------------------------------------ import sys from pyface.qt import QtCore, QtGui, qt_api from pyface.image_resource import ImageResource class SplitTabWidget(QtGui.QSplitter): """ The SplitTabWidget class is a hierarchy of QSplitters the leaves of which are QTabWidgets. Any tab may be moved around with the hierarchy automatically extended and reduced as required. """ # Signals for WorkbenchWindowLayout to handle new_window_request = QtCore.Signal(QtCore.QPoint, QtGui.QWidget) tab_close_request = QtCore.Signal(QtGui.QWidget) tab_window_changed = QtCore.Signal(QtGui.QWidget) editor_has_focus = QtCore.Signal(QtGui.QWidget) focus_changed = QtCore.Signal(QtGui.QWidget, QtGui.QWidget) # The different hotspots of a QTabWidget. An non-negative value is a tab # index and the hotspot is to the left of it. tabTextChanged = QtCore.Signal(QtGui.QWidget, str) _HS_NONE = -1 _HS_AFTER_LAST_TAB = -2 _HS_NORTH = -3 _HS_SOUTH = -4 _HS_EAST = -5 _HS_WEST = -6 _HS_OUTSIDE = -7 def __init__(self, *args): """ Initialise the instance. """ QtGui.QSplitter.__init__(self, *args) self.clear() QtGui.QApplication.instance().focusChanged.connect(self._focus_changed) def clear(self): """ Restore the widget to its pristine state. """ w = None for i in range(self.count()): w = self.widget(i) w.hide() w.deleteLater() del w self._repeat_focus_changes = True self._rband = None self._selected_tab_widget = None self._selected_hotspot = self._HS_NONE self._current_tab_w = None self._current_tab_idx = -1 def saveState(self): """ Returns a Python object containing the saved state of the widget. Widgets are saved only by their object name. """ return self._save_qsplitter(self) def _save_qsplitter(self, qsplitter): # A splitter state is a tuple of the QSplitter state (as a string) and # the list of child states. sp_ch_states = [] # Save the children. for i in range(qsplitter.count()): ch = qsplitter.widget(i) if isinstance(ch, _TabWidget): # A tab widget state is a tuple of the current tab index and # the list of individual tab states. tab_states = [] for t in range(ch.count()): # A tab state is a tuple of the widget's object name and # the title. name = str(ch.widget(t).objectName()) title = str(ch.tabText(t)) tab_states.append((name, title)) ch_state = (ch.currentIndex(), tab_states) else: # Recurse down the tree of splitters. ch_state = self._save_qsplitter(ch) sp_ch_states.append(ch_state) return (QtGui.QSplitter.saveState(qsplitter).data(), sp_ch_states) def restoreState(self, state, factory): """ Restore the contents from the given state (returned by a previous call to saveState()). factory is a callable that is passed the object name of the widget that is in the state and needs to be restored. The callable returns the restored widget. """ # Ensure we are not restoring to a non-empty widget. assert self.count() == 0 self._restore_qsplitter(state, factory, self) def _restore_qsplitter(self, state, factory, qsplitter): sp_qstate, sp_ch_states = state # Go through each child state which will consist of a tuple of two # objects. We use the type of the first to determine if the child is a # tab widget or another splitter. for ch_state in sp_ch_states: if isinstance(ch_state[0], int): current_idx, tabs = ch_state new_tab = _TabWidget(self) # Go through each tab and use the factory to restore the page. for name, title in tabs: page = factory(name) if page is not None: new_tab.addTab(page, title) # Only add the new tab widget if it is used. if new_tab.count() > 0: qsplitter.addWidget(new_tab) # Set the correct tab as the current one. new_tab.setCurrentIndex(current_idx) else: del new_tab else: new_qsp = QtGui.QSplitter() # Recurse down the tree of splitters. self._restore_qsplitter(ch_state, factory, new_qsp) # Only add the new splitter if it is used. if new_qsp.count() > 0: qsplitter.addWidget(new_qsp) else: del new_qsp # Restore the QSplitter state (being careful to get the right # implementation). QtGui.QSplitter.restoreState(qsplitter, sp_qstate) def addTab(self, w, text): """ Add a new tab to the main tab widget. """ # Find the first tab widget going down the left of the hierarchy. This # will be the one in the top left corner. if self.count() > 0: ch = self.widget(0) while not isinstance(ch, _TabWidget): assert isinstance(ch, QtGui.QSplitter) ch = ch.widget(0) else: # There is no tab widget so create one. ch = _TabWidget(self) self.addWidget(ch) idx = ch.insertTab(self._current_tab_idx + 1, w, text) # If the tab has been added to the current tab widget then make it the # current tab. if ch is not self._current_tab_w: self._set_current_tab(ch, idx) ch.tabBar().setFocus() def _close_tab_request(self, w): """ A close button was clicked in one of out _TabWidgets """ self.tab_close_request.emit(w) def setCurrentWidget(self, w): """ Make the given widget current. """ tw, tidx = self._tab_widget(w) if tw is not None: self._set_current_tab(tw, tidx) def setActiveIcon(self, w, icon=None): """ Set the active icon on a widget. """ tw, tidx = self._tab_widget(w) if tw is not None: if icon is None: icon = tw.active_icon() tw.setTabIcon(tidx, icon) def setTabTextColor(self, w, color=None): """ Set the tab text color on a particular widget w """ tw, tidx = self._tab_widget(w) if tw is not None: if color is None: # null color reverts to foreground role color color = QtGui.QColor() tw.tabBar().setTabTextColor(tidx, color) def setWidgetTitle(self, w, title): """ Set the title for the given widget. """ tw, idx = self._tab_widget(w) if tw is not None: tw.setTabText(idx, title) def _tab_widget(self, w): """ Return the tab widget and index containing the given widget. """ for tw in self.findChildren(_TabWidget, None): idx = tw.indexOf(w) if idx >= 0: return (tw, idx) return (None, None) def _set_current_tab(self, tw, tidx): """ Set the new current tab. """ # Handle the trivial case. if self._current_tab_w is tw and self._current_tab_idx == tidx: return if tw is not None: tw.setCurrentIndex(tidx) # Save the new current widget. self._current_tab_w = tw self._current_tab_idx = tidx def _set_focus(self): """ Set the focus to an appropriate widget in the current tab. """ # Only try and change the focus if the current focus isn't already a # child of the widget. w = self._current_tab_w.widget(self._current_tab_idx) fw = self.window().focusWidget() if fw is not None and not w.isAncestorOf(fw): # Find a widget to focus using the same method as # QStackedLayout::setCurrentIndex(). First try the last widget # with the focus. nfw = w.focusWidget() if nfw is None: # Next, try the first child widget in the focus chain. nfw = fw.nextInFocusChain() while nfw is not fw: if ( nfw.focusPolicy() & QtCore.Qt.FocusPolicy.TabFocus and nfw.focusProxy() is None and nfw.isVisibleTo(w) and nfw.isEnabled() and w.isAncestorOf(nfw) ): break nfw = nfw.nextInFocusChain() else: # Fallback to the tab page widget. nfw = w nfw.setFocus() def _focus_changed(self, old, new): """ Handle a change in focus that affects the current tab. """ # It is possible for the C++ layer of this object to be deleted between # the time when the focus change signal is emitted and time when the # slots are dispatched by the Qt event loop. This may be a bug in PyQt4. if qt_api == "pyqt": import sip if sip.isdeleted(self): return if self._repeat_focus_changes: self.focus_changed.emit(old, new) if new is None: return elif isinstance(new, _DragableTabBar): ntw = new.parent() ntidx = ntw.currentIndex() else: ntw, ntidx = self._tab_widget_of(new) if ntw is not None: self._set_current_tab(ntw, ntidx) # See if the widget that has lost the focus is ours. otw, _ = self._tab_widget_of(old) if otw is not None or ntw is not None: if ntw is None: nw = None else: nw = ntw.widget(ntidx) self.editor_has_focus.emit(nw) def _tab_widget_of(self, target): """ Return the tab widget and index of the widget that contains the given widget. """ for tw in self.findChildren(_TabWidget, None): for tidx in range(tw.count()): w = tw.widget(tidx) if w is not None and w.isAncestorOf(target): return (tw, tidx) return (None, None) def _move_left(self, tw, tidx): """ Move the current tab to the one logically to the left. """ tidx -= 1 if tidx < 0: # Find the tab widget logically to the left. twlist = self.findChildren(_TabWidget, None) i = twlist.index(tw) i -= 1 if i < 0: i = len(twlist) - 1 tw = twlist[i] # Move the to right most tab. tidx = tw.count() - 1 self._set_current_tab(tw, tidx) tw.setFocus() def _move_right(self, tw, tidx): """ Move the current tab to the one logically to the right. """ tidx += 1 if tidx >= tw.count(): # Find the tab widget logically to the right. twlist = self.findChildren(_TabWidget, None) i = twlist.index(tw) i += 1 if i >= len(twlist): i = 0 tw = twlist[i] # Move the to left most tab. tidx = 0 self._set_current_tab(tw, tidx) tw.setFocus() def _select(self, pos): tw, hs, hs_geom = self._hotspot(pos) # See if the hotspot has changed. if self._selected_tab_widget is not tw or self._selected_hotspot != hs: if self._selected_tab_widget is not None: self._rband.hide() if tw is not None and hs != self._HS_NONE: if self._rband: self._rband.deleteLater() position = QtCore.QPoint(*hs_geom[0:2]) window = tw.window() self._rband = QtGui.QRubberBand( QtGui.QRubberBand.Shape.Rectangle, window ) self._rband.move(window.mapFromGlobal(position)) self._rband.resize(*hs_geom[2:4]) self._rband.show() self._selected_tab_widget = tw self._selected_hotspot = hs def _drop(self, pos, stab_w, stab): self._rband.hide() # Get the destination locations. dtab_w = self._selected_tab_widget dhs = self._selected_hotspot if dhs == self._HS_NONE: return elif dhs != self._HS_OUTSIDE: dsplit_w = dtab_w.parent() while not isinstance(dsplit_w, SplitTabWidget): dsplit_w = dsplit_w.parent() self._selected_tab_widget = None self._selected_hotspot = self._HS_NONE # See if the tab is being moved to a new window. if dhs == self._HS_OUTSIDE: # Disable tab tear-out for now. It works, but this is something that # should be turned on manually. We need an interface for this. # ticon, ttext, ttextcolor, tbuttn, twidg = self._remove_tab(stab_w, stab) # self.new_window_request.emit(pos, twidg) return # See if the tab is being moved to an existing tab widget. if dhs >= 0 or dhs == self._HS_AFTER_LAST_TAB: # Make sure it really is being moved. if stab_w is dtab_w: if stab == dhs: return if ( dhs == self._HS_AFTER_LAST_TAB and stab == stab_w.count() - 1 ): return QtGui.QApplication.instance().blockSignals(True) ticon, ttext, ttextcolor, tbuttn, twidg = self._remove_tab( stab_w, stab ) if dhs == self._HS_AFTER_LAST_TAB: idx = dtab_w.addTab(twidg, ticon, ttext) dtab_w.tabBar().setTabTextColor(idx, ttextcolor) elif dtab_w is stab_w: # Adjust the index if necessary in case the removal of the tab # from its old position has skewed things. dst = dhs if dhs > stab: dst -= 1 idx = dtab_w.insertTab(dst, twidg, ticon, ttext) dtab_w.tabBar().setTabTextColor(idx, ttextcolor) else: idx = dtab_w.insertTab(dhs, twidg, ticon, ttext) dtab_w.tabBar().setTabTextColor(idx, ttextcolor) if tbuttn: dtab_w.show_button(idx) dsplit_w._set_current_tab(dtab_w, idx) else: # Ignore drops to the same tab widget when it only has one tab. if stab_w is dtab_w and stab_w.count() == 1: return QtGui.QApplication.instance().blockSignals(True) # Remove the tab from its current tab widget and create a new one # for it. ticon, ttext, ttextcolor, tbuttn, twidg = self._remove_tab( stab_w, stab ) new_tw = _TabWidget(dsplit_w) idx = new_tw.addTab(twidg, ticon, ttext) new_tw.tabBar().setTabTextColor(0, ttextcolor) if tbuttn: new_tw.show_button(idx) # Get the splitter containing the destination tab widget. dspl = dtab_w.parent() dspl_idx = dspl.indexOf(dtab_w) if dhs in (self._HS_NORTH, self._HS_SOUTH): dspl, dspl_idx = dsplit_w._horizontal_split( dspl, dspl_idx, dhs ) else: dspl, dspl_idx = dsplit_w._vertical_split(dspl, dspl_idx, dhs) # Add the new tab widget in the right place. dspl.insertWidget(dspl_idx, new_tw) dsplit_w._set_current_tab(new_tw, 0) dsplit_w._set_focus() # Signal that the tab's SplitTabWidget has changed, if necessary. if dsplit_w != self: self.tab_window_changed.emit(twidg) QtGui.QApplication.instance().blockSignals(False) def _horizontal_split(self, spl, idx, hs): """ Returns a tuple of the splitter and index where the new tab widget should be put. """ if spl.orientation() == QtCore.Qt.Orientation.Vertical: if hs == self._HS_SOUTH: idx += 1 elif spl is self and spl.count() == 1: # The splitter is the root and only has one child so we can just # change its orientation. spl.setOrientation(QtCore.Qt.Orientation.Vertical) if hs == self._HS_SOUTH: idx = -1 else: new_spl = QtGui.QSplitter(QtCore.Qt.Orientation.Vertical) new_spl.addWidget(spl.widget(idx)) spl.insertWidget(idx, new_spl) if hs == self._HS_SOUTH: idx = -1 else: idx = 0 spl = new_spl return (spl, idx) def _vertical_split(self, spl, idx, hs): """ Returns a tuple of the splitter and index where the new tab widget should be put. """ if spl.orientation() == QtCore.Qt.Orientation.Horizontal: if hs == self._HS_EAST: idx += 1 elif spl is self and spl.count() == 1: # The splitter is the root and only has one child so we can just # change its orientation. spl.setOrientation(QtCore.Qt.Orientation.Horizontal) if hs == self._HS_EAST: idx = -1 else: new_spl = QtGui.QSplitter(QtCore.Qt.Orientation.Horizontal) new_spl.addWidget(spl.widget(idx)) spl.insertWidget(idx, new_spl) if hs == self._HS_EAST: idx = -1 else: idx = 0 spl = new_spl return (spl, idx) def _remove_tab(self, tab_w, tab): """ Remove a tab from a tab widget and return a tuple of the icon, label text and the widget so that it can be recreated. """ icon = tab_w.tabIcon(tab) text = tab_w.tabText(tab) text_color = tab_w.tabBar().tabTextColor(tab) button = tab_w.tabBar().tabButton(tab, QtGui.QTabBar.ButtonPosition.LeftSide) w = tab_w.widget(tab) tab_w.removeTab(tab) return (icon, text, text_color, button, w) def _hotspot(self, pos): """ Return a tuple of the tab widget, hotspot and hostspot geometry (as a tuple) at the given position. """ global_pos = self.mapToGlobal(pos) miss = (None, self._HS_NONE, None) # Get the bounding rect of the cloned QTbarBar. top_widget = QtGui.QApplication.instance().topLevelAt(global_pos) if isinstance(top_widget, QtGui.QTabBar): cloned_rect = top_widget.frameGeometry() else: cloned_rect = None # Determine which visible SplitTabWidget, if any, is under the cursor # (compensating for the cloned QTabBar that may be rendered over it). split_widget = None for top_widget in QtGui.QApplication.instance().topLevelWidgets(): for split_widget in top_widget.findChildren(SplitTabWidget, None): visible_region = split_widget.visibleRegion() widget_pos = split_widget.mapFromGlobal(global_pos) if cloned_rect and split_widget.geometry().contains( widget_pos ): visible_rect = visible_region.boundingRect() widget_rect = QtCore.QRect( split_widget.mapFromGlobal(cloned_rect.topLeft()), split_widget.mapFromGlobal(cloned_rect.bottomRight()), ) if not visible_rect.intersected(widget_rect).isEmpty(): break elif visible_region.contains(widget_pos): break else: split_widget = None if split_widget: break # Handle a drag outside of any split tab widget. if not split_widget: if self.window().frameGeometry().contains(global_pos): return miss else: return (None, self._HS_OUTSIDE, None) # Go through each tab widget. pos = split_widget.mapFromGlobal(global_pos) for tw in split_widget.findChildren(_TabWidget, None): if tw.geometry().contains(tw.parent().mapFrom(split_widget, pos)): break else: return miss # See if the hotspot is in the widget area. widg = tw.currentWidget() if widg is not None: # Get the widget's position relative to its parent. wpos = widg.parent().mapFrom(split_widget, pos) if widg.geometry().contains(wpos): # Get the position of the widget relative to itself (ie. the # top left corner is (0, 0)). p = widg.mapFromParent(wpos) x = p.x() y = p.y() h = widg.height() w = widg.width() # Get the global position of the widget. gpos = widg.mapToGlobal(widg.pos()) gx = gpos.x() gy = gpos.y() # The corners of the widget belong to the north and south # sides. if y < h / 4: return (tw, self._HS_NORTH, (gx, gy, w, h / 4)) if y >= (3 * h) / 4: return ( tw, self._HS_SOUTH, (gx, gy + (3 * h) / 4, w, h / 4), ) if x < w / 4: return (tw, self._HS_WEST, (gx, gy, w / 4, h)) if x >= (3 * w) / 4: return ( tw, self._HS_EAST, (gx + (3 * w) / 4, gy, w / 4, h), ) return miss # See if the hotspot is in the tab area. tpos = tw.mapFrom(split_widget, pos) tab_bar = tw.tabBar() top_bottom = tw.tabPosition() in ( QtGui.QTabWidget.TabPosition.North, QtGui.QTabWidget.TabPosition.South, ) for i in range(tw.count()): rect = tab_bar.tabRect(i) if rect.contains(tpos): w = rect.width() h = rect.height() # Get the global position. gpos = tab_bar.mapToGlobal(rect.topLeft()) gx = gpos.x() gy = gpos.y() if top_bottom: off = pos.x() - rect.x() ext = w gx -= w / 2 else: off = pos.y() - rect.y() ext = h gy -= h / 2 # See if it is in the left (or top) half or the right (or # bottom) half. if off < ext / 2: return (tw, i, (gx, gy, w, h)) if top_bottom: gx += w else: gy += h if i + 1 == tw.count(): return (tw, self._HS_AFTER_LAST_TAB, (gx, gy, w, h)) return (tw, i + 1, (gx, gy, w, h)) else: rect = tab_bar.rect() if rect.contains(tpos): gpos = tab_bar.mapToGlobal(rect.topLeft()) gx = gpos.x() gy = gpos.y() w = rect.width() h = rect.height() if top_bottom: tab_widths = sum( tab_bar.tabRect(i).width() for i in range(tab_bar.count()) ) w -= tab_widths gx += tab_widths else: tab_heights = sum( tab_bar.tabRect(i).height() for i in range(tab_bar.count()) ) h -= tab_heights gy -= tab_heights return (tw, self._HS_AFTER_LAST_TAB, (gx, gy, w, h)) return miss active_style = """QTabWidget::pane { /* The tab widget frame */ border: 2px solid #00FF00; } """ inactive_style = """QTabWidget::pane { /* The tab widget frame */ border: 2px solid #C2C7CB; margin: 0px; } """ class _TabWidget(QtGui.QTabWidget): """ The _TabWidget class is a QTabWidget with a dragable tab bar. """ # The active icon. It is created when it is first needed. _active_icon = None _spinner_data = None def __init__(self, root, *args): """ Initialise the instance. """ QtGui.QTabWidget.__init__(self, *args) # XXX this requires Qt > 4.5 if sys.platform == "darwin": self.setDocumentMode(True) # self.setStyleSheet(inactive_style) self._root = root # We explicitly pass the parent to the tab bar ctor to work round a bug # in PyQt v4.2 and earlier. self.setTabBar(_DragableTabBar(self._root, self)) self.setTabsClosable(True) self.tabCloseRequested.connect(self._close_tab) if not (_TabWidget._spinner_data): _TabWidget._spinner_data = ImageResource("spinner.gif") def show_button(self, index): lbl = QtGui.QLabel(self) movie = QtGui.QMovie( _TabWidget._spinner_data.absolute_path, parent=lbl ) movie.setCacheMode(QtGui.QMovie.CacheMode.CacheAll) movie.setScaledSize(QtCore.QSize(16, 16)) lbl.setMovie(movie) movie.start() self.tabBar().setTabButton(index, QtGui.QTabBar.ButtonPosition.LeftSide, lbl) def hide_button(self, index): curr = self.tabBar().tabButton(index, QtGui.QTabBar.ButtonPosition.LeftSide) if curr: curr.close() self.tabBar().setTabButton(index, QtGui.QTabBar.ButtonPosition.LeftSide, None) def active_icon(self): """ Return the QIcon to be used to indicate an active tab page. """ if _TabWidget._active_icon is None: # The gradient start and stop colours. start = QtGui.QColor(0, 255, 0) stop = QtGui.QColor(0, 63, 0) size = self.iconSize() width = size.width() height = size.height() pm = QtGui.QPixmap(size) p = QtGui.QPainter() p.begin(pm) # Fill the image background from the tab background. p.initFrom(self.tabBar()) p.fillRect(0, 0, width, height, p.background()) # Create the colour gradient. rg = QtGui.QRadialGradient(width / 2, height / 2, width) rg.setColorAt(0.0, start) rg.setColorAt(1.0, stop) # Draw the circle. p.setBrush(rg) p.setPen(QtCore.Qt.PenStyle.NoPen) p.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing) p.drawEllipse(0, 0, width, height) p.end() _TabWidget._active_icon = QtGui.QIcon(pm) return _TabWidget._active_icon def _still_needed(self): """ Delete the tab widget (and any relevant parent splitters) if it is no longer needed. """ if self.count() == 0: prune = self parent = prune.parent() # Go up the QSplitter hierarchy until we find one with at least one # sibling. while parent is not self._root and parent.count() == 1: prune = parent parent = prune.parent() prune.hide() prune.deleteLater() def tabRemoved(self, idx): """ Reimplemented to update the record of the current tab if it is removed. """ self._still_needed() if ( self._root._current_tab_w is self and self._root._current_tab_idx == idx ): self._root._current_tab_w = None def _close_tab(self, index): """ Close the current tab. """ self._root._close_tab_request(self.widget(index)) class _IndependentLineEdit(QtGui.QLineEdit): def keyPressEvent(self, e): QtGui.QLineEdit.keyPressEvent(self, e) if e.key() == QtCore.Qt.Key.Key_Escape: self.hide() class _DragableTabBar(QtGui.QTabBar): """ The _DragableTabBar class is a QTabBar that can be dragged around. """ def __init__(self, root, parent): """ Initialise the instance. """ QtGui.QTabBar.__init__(self, parent) # XXX this requires Qt > 4.5 if sys.platform == "darwin": self.setDocumentMode(True) self._root = root self._drag_state = None # LineEdit to change tab bar title te = _IndependentLineEdit("", self) te.hide() te.editingFinished.connect(te.hide) te.returnPressed.connect(self._setCurrentTabText) self._title_edit = te def resizeEvent(self, e): # resize edit tab if self._title_edit.isVisible(): self._resize_title_edit_to_current_tab() QtGui.QTabBar.resizeEvent(self, e) def keyPressEvent(self, e): """ Reimplemented to handle traversal across different tab widgets. """ if e.key() == QtCore.Qt.Key.Key_Left: self._root._move_left(self.parent(), self.currentIndex()) elif e.key() == QtCore.Qt.Key.Key_Right: self._root._move_right(self.parent(), self.currentIndex()) else: e.ignore() def mouseDoubleClickEvent(self, e): self._resize_title_edit_to_current_tab() te = self._title_edit te.setText(self.tabText(self.currentIndex())[1:]) te.setFocus() te.selectAll() te.show() def mousePressEvent(self, e): """ Reimplemented to handle mouse press events. """ # There is something odd in the focus handling where focus temporarily # moves elsewhere (actually to a View) when switching to a different # tab page. We suppress the notification so that the workbench doesn't # temporarily make the View active. self._root._repeat_focus_changes = False QtGui.QTabBar.mousePressEvent(self, e) self._root._repeat_focus_changes = True # Update the current tab. self._root._set_current_tab(self.parent(), self.currentIndex()) self._root._set_focus() if e.button() != QtCore.Qt.MouseButton.LeftButton: return if self._drag_state is not None: return # Potentially start dragging if the tab under the mouse is the current # one (which will eliminate disabled tabs). tab = self._tab_at(e.pos()) if tab < 0 or tab != self.currentIndex(): return self._drag_state = _DragState(self._root, self, tab, e.pos()) def mouseMoveEvent(self, e): """ Reimplemented to handle mouse move events. """ QtGui.QTabBar.mouseMoveEvent(self, e) if self._drag_state is None: return if self._drag_state.dragging: self._drag_state.drag(e.pos()) else: self._drag_state.start_dragging(e.pos()) # If the mouse has moved far enough that dragging has started then # tell the user. if self._drag_state.dragging: QtGui.QApplication.setOverrideCursor(QtCore.Qt.CursorShape.OpenHandCursor) def mouseReleaseEvent(self, e): """ Reimplemented to handle mouse release events. """ QtGui.QTabBar.mouseReleaseEvent(self, e) if e.button() != QtCore.Qt.MouseButton.LeftButton: if e.button() == QtCore.Qt.MidddleButton: self.tabCloseRequested.emit(self.tabAt(e.pos())) return if self._drag_state is not None and self._drag_state.dragging: QtGui.QApplication.restoreOverrideCursor() self._drag_state.drop(e.pos()) self._drag_state = None def _tab_at(self, pos): """ Return the index of the tab at the given point. """ for i in range(self.count()): if self.tabRect(i).contains(pos): return i return -1 def _setCurrentTabText(self): idx = self.currentIndex() text = self._title_edit.text() self.setTabText(idx, "\u25b6" + text) self._root.tabTextChanged.emit(self.parent().widget(idx), text) def _resize_title_edit_to_current_tab(self): idx = self.currentIndex() tab = QtGui.QStyleOptionTabV3() self.initStyleOption(tab, idx) rect = self.style().subElementRect(QtGui.QStyle.SubElement.SE_TabBarTabText, tab) self._title_edit.setGeometry(rect.adjusted(0, 8, 0, -8)) class _DragState(object): """ The _DragState class handles most of the work when dragging a tab. """ def __init__(self, root, tab_bar, tab, start_pos): """ Initialise the instance. """ self.dragging = False self._root = root self._tab_bar = tab_bar self._tab = tab self._start_pos = QtCore.QPoint(start_pos) self._clone = None def start_dragging(self, pos): """ Start dragging a tab. """ if ( pos - self._start_pos ).manhattanLength() <= QtGui.QApplication.startDragDistance(): return self.dragging = True # Create a clone of the tab being moved (except for its icon). otb = self._tab_bar tab = self._tab ctb = self._clone = QtGui.QTabBar() if sys.platform == "darwin" and QtCore.QT_VERSION >= 0x40500: ctb.setDocumentMode(True) ctb.setAttribute(QtCore.Qt.WidgetAttribute.WA_TransparentForMouseEvents) ctb.setWindowFlags( QtCore.Qt.WindowType.FramelessWindowHint | QtCore.Qt.WindowType.Tool | QtCore.Qt.WindowType.X11BypassWindowManagerHint ) ctb.setWindowOpacity(0.5) ctb.setElideMode(otb.elideMode()) ctb.setShape(otb.shape()) ctb.addTab(otb.tabText(tab)) ctb.setTabTextColor(0, otb.tabTextColor(tab)) # The clone offset is the position of the clone relative to the mouse. trect = otb.tabRect(tab) self._clone_offset = trect.topLeft() - pos # The centre offset is the position of the center of the clone relative # to the mouse. The center of the clone determines the hotspot, not # the position of the mouse. self._centre_offset = trect.center() - pos self.drag(pos) ctb.show() def drag(self, pos): """ Handle the movement of the cloned tab during dragging. """ self._clone.move(self._tab_bar.mapToGlobal(pos) + self._clone_offset) self._root._select( self._tab_bar.mapTo(self._root, pos + self._centre_offset) ) def drop(self, pos): """ Handle the drop of the cloned tab. """ self.drag(pos) self._clone = None global_pos = self._tab_bar.mapToGlobal(pos) self._root._drop(global_pos, self._tab_bar.parent(), self._tab) self.dragging = False pyface-7.4.0/pyface/ui/qt4/color_dialog.py0000644000076500000240000000425614176253531021353 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 dialog that allows the user to select a color. """ from pyface.qt import QtGui from traits.api import Bool, provides from pyface.color import Color from pyface.ui_traits import PyfaceColor from pyface.i_color_dialog import IColorDialog from .dialog import Dialog @provides(IColorDialog) class ColorDialog(Dialog): """ A dialog that allows the user to choose a color. """ # 'IColorDialog' interface ---------------------------------------------- #: The color in the dialog. color = PyfaceColor() #: Whether or not to allow the user to chose an alpha value. show_alpha = Bool(False) # ------------------------------------------------------------------------ # 'IDialog' interface. # ------------------------------------------------------------------------ def _create_contents(self, parent): # In PyQt this is a canned dialog so there are no contents. pass # ------------------------------------------------------------------------ # 'IWindow' interface. # ------------------------------------------------------------------------ def close(self): if self.control.result() == QtGui.QDialog.DialogCode.Accepted: qcolor = self.control.selectedColor() self.color = Color.from_toolkit(qcolor) return super(ColorDialog, self).close() # ------------------------------------------------------------------------ # 'IWindow' interface. # ------------------------------------------------------------------------ def _create_control(self, parent): qcolor = self.color.to_toolkit() dialog = QtGui.QColorDialog(qcolor, parent) if self.show_alpha: dialog.setOptions(QtGui.QColorDialog.ColorDialogOption.ShowAlphaChannel) return dialog pyface-7.4.0/pyface/ui/qt4/split_widget.py0000644000076500000240000000555014176253531021412 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # (C) Copyright 2007 Riverbank Computing Limited # 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 """ Mix-in class for split widgets. """ from pyface.qt import QtCore, QtGui from traits.api import provides from pyface.i_split_widget import ISplitWidget, MSplitWidget @provides(ISplitWidget) class SplitWidget(MSplitWidget): """ The toolkit specific implementation of a SplitWidget. See the ISPlitWidget interface for the API documentation. """ # ------------------------------------------------------------------------ # Protected 'ISplitWidget' interface. # ------------------------------------------------------------------------ def _create_splitter(self, parent): """ Create the toolkit-specific control that represents the widget. """ splitter = QtGui.QSplitter(parent) # Yes, this is correct. if self.direction == "horizontal": splitter.setOrientation(QtCore.Qt.Orientation.Vertical) # Only because the wx implementation does the same. splitter.setChildrenCollapsible(False) # Left hand side/top. splitter.addWidget(self._create_lhs(splitter)) # Right hand side/bottom. splitter.addWidget(self._create_rhs(splitter)) # Set the initial splitter position. if self.direction == "horizontal": pos = splitter.sizeHint().height() else: pos = splitter.sizeHint().width() splitter.setSizes( [int(pos * self.ratio), int(pos * (1.0 - self.ratio))] ) return splitter def _create_lhs(self, parent): """ Creates the left hand/top panel depending on the direction. """ if self.lhs is not None: lhs = self.lhs(parent) if not isinstance(lhs, QtGui.QWidget): lhs = lhs.control else: # Dummy implementation - override! lhs = QtGui.QWidget(parent) return lhs def _create_rhs(self, parent): """ Creates the right hand/bottom panel depending on the direction. """ if self.rhs is not None: rhs = self.rhs(parent) if not isinstance(rhs, QtGui.QWidget): rhs = rhs.control else: # Dummy implementation - override! rhs = QtGui.QWidget(parent) return rhs pyface-7.4.0/pyface/ui/qt4/code_editor/0000755000076500000240000000000014176460550020616 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/qt4/code_editor/pygments_highlighter.py0000644000076500000240000002160714176253531025421 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 pygments.lexer import RegexLexer, _TokenType, Text, Error from pygments.lexers import CLexer, CppLexer, PythonLexer, get_lexer_by_name from pygments.styles.default import DefaultStyle from pygments.token import Comment def get_tokens_unprocessed(self, text, stack=("root",)): """ Split ``text`` into (tokentype, text) pairs. Monkeypatched to store the final stack on the object itself. The `text` parameter that gets passed is only the current line, so to highlight things like multiline strings correctly, we need to retrieve the state from the previous line (this is done in PygmentsHighlighter, below), and use it to continue processing the current line. """ pos = 0 tokendefs = self._tokens if hasattr(self, "_saved_state_stack"): statestack = list(self._saved_state_stack) else: statestack = list(stack) statetokens = tokendefs[statestack[-1]] while 1: for rexmatch, action, new_state in statetokens: m = rexmatch(text, pos) if m: if action is not None: if type(action) is _TokenType: yield pos, action, m.group() else: for item in action(self, m): yield item pos = m.end() if new_state is not None: # state transition if isinstance(new_state, tuple): for state in new_state: if state == "#pop": statestack.pop() elif state == "#push": statestack.append(statestack[-1]) else: statestack.append(state) elif isinstance(new_state, int): # pop del statestack[new_state:] elif new_state == "#push": statestack.append(statestack[-1]) else: assert False, "wrong state def: %r" % new_state statetokens = tokendefs[statestack[-1]] break else: try: if text[pos] == "\n": # at EOL, reset state to "root" pos += 1 statestack = ["root"] statetokens = tokendefs["root"] yield pos, Text, "\n" continue yield pos, Error, text[pos] pos += 1 except IndexError: break self._saved_state_stack = list(statestack) # Monkeypatch! RegexLexer.get_tokens_unprocessed = get_tokens_unprocessed # Even with the above monkey patch to store state, multiline comments do not # work since they are stateless (Pygments uses a single multiline regex for # these comments, but Qt lexes by line). So we need to add a state for comments # to the C and C++ lexers. This means that nested multiline comments will appear # to be valid C/C++, but this is better than the alternative for now. def replace_pattern(tokens, new_pattern): """ Given a RegexLexer token dictionary 'tokens', replace all patterns that match the token specified in 'new_pattern' with 'new_pattern'. """ for state in tokens.values(): for index, pattern in enumerate(state): if isinstance(pattern, tuple) and pattern[1] == new_pattern[1]: state[index] = new_pattern # More monkeypatching! comment_start = (r"/\*", Comment.Multiline, "comment") comment_state = [ (r"[^*/]", Comment.Multiline), (r"/\*", Comment.Multiline, "#push"), (r"\*/", Comment.Multiline, "#pop"), (r"[*/]", Comment.Multiline), ] replace_pattern(CLexer.tokens, comment_start) replace_pattern(CppLexer.tokens, comment_start) CLexer.tokens["comment"] = comment_state CppLexer.tokens["comment"] = comment_state class BlockUserData(QtGui.QTextBlockUserData): """ Storage for the user data associated with each line. """ syntax_stack = ("root",) def __init__(self, **kwds): QtGui.QTextBlockUserData.__init__(self) for key, value in kwds.items(): setattr(self, key, value) def __repr__(self): attrs = ["syntax_stack"] kwds = ", ".join( ["%s=%r" % (attr, getattr(self, attr)) for attr in attrs] ) return "BlockUserData(%s)" % kwds class PygmentsHighlighter(QtGui.QSyntaxHighlighter): """ Syntax highlighter that uses Pygments for parsing. """ def __init__(self, parent, lexer=None): super().__init__(parent) try: self._lexer = get_lexer_by_name(lexer) except: self._lexer = PythonLexer() self._style = DefaultStyle # Caches for formats and brushes. self._brushes = {} self._formats = {} def highlightBlock(self, qstring): """ Highlight a block of text. """ qstring = str(qstring) prev_data = self.previous_block_data() if prev_data is not None: self._lexer._saved_state_stack = prev_data.syntax_stack elif hasattr(self._lexer, "_saved_state_stack"): del self._lexer._saved_state_stack index = 0 # Lex the text using Pygments for token, text in self._lexer.get_tokens(qstring): l = len(text) format = self._get_format(token) if format is not None: self.setFormat(index, l, format) index += l if hasattr(self._lexer, "_saved_state_stack"): data = BlockUserData(syntax_stack=self._lexer._saved_state_stack) self.currentBlock().setUserData(data) # there is a bug in pyside and it will crash unless we # hold on to the reference a little longer data = self.currentBlock().userData() # Clean up for the next go-round. del self._lexer._saved_state_stack def previous_block_data(self): """ Convenience method for returning the previous block's user data. """ return self.currentBlock().previous().userData() def _get_format(self, token): """ Returns a QTextCharFormat for token or None. """ if token in self._formats: return self._formats[token] result = None while not self._style.styles_token(token): token = token.parent for key, value in self._style.style_for_token(token).items(): if value: if result is None: result = QtGui.QTextCharFormat() if key == "color": result.setForeground(self._get_brush(value)) elif key == "bgcolor": result.setBackground(self._get_brush(value)) elif key == "bold": result.setFontWeight(QtGui.QFont.Weight.Bold) elif key == "italic": result.setFontItalic(True) elif key == "underline": result.setUnderlineStyle( QtGui.QTextCharFormat.UnderlineStyle.SingleUnderline ) elif key == "sans": result.setFontStyleHint(QtGui.QFont.SansSerif) elif key == "roman": result.setFontStyleHint(QtGui.QFont.StyleHint.Times) elif key == "mono": result.setFontStyleHint(QtGui.QFont.TypeWriter) elif key == "border": # Borders are normally used for errors. We can't do a border # so instead we do a wavy underline result.setUnderlineStyle( QtGui.QTextCharFormat.UnderlineStyle.WaveUnderline ) result.setUnderlineColor(self._get_color(value)) self._formats[token] = result return result def _get_brush(self, color): """ Returns a brush for the color. """ result = self._brushes.get(color) if result is None: qcolor = self._get_color(color) result = QtGui.QBrush(qcolor) self._brushes[color] = result return result def _get_color(self, color): qcolor = QtGui.QColor() qcolor.setRgb( int(color[:2], base=16), int(color[2:4], base=16), int(color[4:6], base=16), ) return qcolor pyface-7.4.0/pyface/ui/qt4/code_editor/tests/0000755000076500000240000000000014176460550021760 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/qt4/code_editor/tests/test_code_widget.py0000644000076500000240000000676214176253531025660 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.qt import QtCore, QtGui from pyface.qt.QtTest import QTest from pyface.ui.qt4.code_editor.code_widget import ( CodeWidget, AdvancedCodeWidget, ) class TestCodeWidget(unittest.TestCase): @classmethod def setUpClass(cls): cls.qapp = QtGui.QApplication.instance() or QtGui.QApplication([]) def tearDown(self): self.qapp.processEvents() def test_different_lexer(self): # Setting a different lexer should not fail. # See enthought/traitsui#982 cw = CodeWidget(None, lexer="yaml") text = "number: 1" cw.setPlainText(text) def test_readonly_editor(self): cw = CodeWidget(None) text = "Some\nText" cw.setPlainText(text) def check(typed, expected): cursor = cw.textCursor() cursor.setPosition(0) cw.setTextCursor(cursor) QTest.keyClicks(cw, typed) self.assertEqual(cw.toPlainText(), expected) cw.setReadOnly(True) check("More", text) cw.setReadOnly(False) check("Extra", "Extra" + text) def test_readonly_replace_widget(self): acw = AdvancedCodeWidget(None) text = "Some\nText" acw.code.setPlainText(text) acw.show() # On some platforms, Find/Replace do not have default keybindings FindKey = QtGui.QKeySequence("Ctrl+F") ReplaceKey = QtGui.QKeySequence("Ctrl+H") patcher_find = mock.patch("pyface.qt.QtGui.QKeySequence.StandardKey.Find", FindKey) patcher_replace = mock.patch( "pyface.qt.QtGui.QKeySequence.StandardKey.Replace", ReplaceKey ) patcher_find.start() patcher_replace.start() self.addCleanup(patcher_find.stop) self.addCleanup(patcher_replace.stop) def click_key_seq(widget, key_seq): if not isinstance(key_seq, QtGui.QKeySequence): key_seq = QtGui.QKeySequence(key_seq) try: # QKeySequence on python3-pyside does not have `len` first_key = key_seq[0] except IndexError: return False key = QtCore.Qt.Key(first_key & ~QtCore.Qt.KeyboardModifier.KeyboardModifierMask) modifier = QtCore.Qt.KeyboardModifier( first_key & QtCore.Qt.KeyboardModifier.KeyboardModifierMask ) QTest.keyClick(widget, key, modifier) return True acw.code.setReadOnly(True) if click_key_seq(acw, FindKey): self.assertTrue(acw.find.isVisible()) acw.find.hide() acw.code.setReadOnly(False) if click_key_seq(acw, FindKey): self.assertTrue(acw.find.isVisible()) acw.find.hide() acw.code.setReadOnly(True) if click_key_seq(acw, ReplaceKey): self.assertFalse(acw.replace.isVisible()) acw.code.setReadOnly(False) if click_key_seq(acw, ReplaceKey): self.assertTrue(acw.replace.isVisible()) acw.replace.hide() self.assertFalse(acw.replace.isVisible()) pyface-7.4.0/pyface/ui/qt4/code_editor/tests/__init__.py0000644000076500000240000000000014176222673024061 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/qt4/code_editor/__init__.py0000644000076500000240000000062714176222673022736 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! pyface-7.4.0/pyface/ui/qt4/code_editor/code_widget.py0000644000076500000240000007160014176253531023450 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 sys from pyface.qt import QtCore, QtGui from .find_widget import FindWidget from .gutters import LineNumberWidget, StatusGutterWidget from .replace_widget import ReplaceWidget from .pygments_highlighter import PygmentsHighlighter class CodeWidget(QtGui.QPlainTextEdit): """ A widget for viewing and editing code. """ # ------------------------------------------------------------------------ # CodeWidget interface # ------------------------------------------------------------------------ focus_lost = QtCore.Signal() def __init__( self, parent, should_highlight_current_line=False, font=None, lexer=None ): super().__init__(parent) self.highlighter = PygmentsHighlighter(self.document(), lexer) self.line_number_widget = LineNumberWidget(self) self.status_widget = StatusGutterWidget(self) if font is None: # Set a decent fixed width font for this platform. font = QtGui.QFont() if sys.platform == "win32": # Prefer Consolas, but fall back to Courier if necessary. font.setFamily("Consolas") if not font.exactMatch(): font.setFamily("Courier") elif sys.platform == "darwin": font.setFamily("Monaco") else: font.setFamily("Monospace") font.setStyleHint(QtGui.QFont.TypeWriter) self.set_font(font) # Whether we should highlight the current line or not. self.should_highlight_current_line = should_highlight_current_line # What that highlight color should be. self.line_highlight_color = self.palette().alternateBase() # Auto-indentation behavior self.auto_indent = True self.smart_backspace = True # Tab settings self.tabs_as_spaces = True self.tab_width = 4 self.indent_character = ":" self.comment_character = "#" # Set up gutter widget and current line highlighting self.blockCountChanged.connect(self.update_line_number_width) self.updateRequest.connect(self.update_line_numbers) self.cursorPositionChanged.connect(self.highlight_current_line) self.update_line_number_width() self.highlight_current_line() # Don't wrap text self.setLineWrapMode(QtGui.QPlainTextEdit.LineWrapMode.NoWrap) # Key bindings self.indent_key = QtGui.QKeySequence(QtCore.Qt.Key.Key_Tab) self.unindent_key = QtGui.QKeySequence( QtCore.Qt.Modifier.SHIFT + QtCore.Qt.Key.Key_Backtab ) self.comment_key = QtGui.QKeySequence( QtCore.Qt.Modifier.CTRL + QtCore.Qt.Key.Key_Slash ) self.backspace_key = QtGui.QKeySequence(QtCore.Qt.Key.Key_Backspace) def _remove_event_listeners(self): self.blockCountChanged.disconnect(self.update_line_number_width) self.updateRequest.disconnect(self.update_line_numbers) self.cursorPositionChanged.disconnect(self.highlight_current_line) def lines(self): """ Return the number of lines. """ return self.blockCount() def set_line_column(self, line, column): """ Move the cursor to a particular line/column number. These line and column numbers are 1-indexed. """ # Allow the caller to ignore either line or column by passing None. line0, col0 = self.get_line_column() if line is None: line = line0 if column is None: column = col0 line -= 1 column -= 1 block = self.document().findBlockByLineNumber(line) line_start = block.position() position = line_start + column cursor = self.textCursor() cursor.setPosition(position) self.setTextCursor(cursor) def get_line_column(self): """ Get the current line and column numbers. These line and column numbers are 1-indexed. """ cursor = self.textCursor() pos = cursor.position() line = cursor.blockNumber() + 1 line_start = cursor.block().position() column = pos - line_start + 1 return line, column def get_selected_text(self): """ Return the currently selected text. """ return str(self.textCursor().selectedText()) def set_font(self, font): """ Set the new QFont. """ self.document().setDefaultFont(font) self.line_number_widget.set_font(font) self.update_line_number_width() def update_line_number_width(self, nblocks=0): """ Update the width of the line number widget. """ left = 0 if not self.line_number_widget.isHidden(): left = self.line_number_widget.digits_width() self.setViewportMargins(left, 0, 0, 0) def update_line_numbers(self, rect, dy): """ Update the line numbers. """ if dy: self.line_number_widget.scroll(0, dy) self.line_number_widget.update( 0, rect.y(), self.line_number_widget.width(), rect.height() ) if rect.contains(self.viewport().rect()): self.update_line_number_width() def set_info_lines(self, info_lines): self.status_widget.info_lines = info_lines self.status_widget.update() def set_warn_lines(self, warn_lines): self.status_widget.warn_lines = warn_lines self.status_widget.update() def set_error_lines(self, error_lines): self.status_widget.error_lines = error_lines self.status_widget.update() def highlight_current_line(self): """ Highlight the line with the cursor. """ if self.should_highlight_current_line: selection = QtGui.QTextEdit.ExtraSelection() selection.format.setBackground(self.line_highlight_color) selection.format.setProperty( QtGui.QTextFormat.Property.FullWidthSelection, True ) selection.cursor = self.textCursor() selection.cursor.clearSelection() self.setExtraSelections([selection]) def autoindent_newline(self): tab = "\t" if self.tabs_as_spaces: tab = " " * self.tab_width cursor = self.textCursor() text = cursor.block().text() trimmed = text.rstrip() current_indent_pos = self._get_indent_position(text) cursor.beginEditBlock() # Create the new line. There is no need to move to the new block, as # the insertBlock will do that automatically cursor.insertBlock() # Remove any leading whitespace from the current line after = cursor.block().text() trimmed_after = after.rstrip() pos = after.index(trimmed_after) for i in range(pos): cursor.deleteChar() if self.indent_character and trimmed.endswith(self.indent_character): # indent one level indent = text[:current_indent_pos] + tab else: # indent to the same level indent = text[:current_indent_pos] cursor.insertText(indent) cursor.endEditBlock() self.ensureCursorVisible() def block_indent(self): cursor = self.textCursor() if not cursor.hasSelection(): # Insert a tabulator self.line_indent(cursor) else: # Indent every selected line sel_blocks = self._get_selected_blocks() cursor.clearSelection() cursor.beginEditBlock() for block in sel_blocks: cursor.setPosition(block.position()) self.line_indent(cursor) cursor.endEditBlock() self._show_selected_blocks(sel_blocks) def block_unindent(self): cursor = self.textCursor() if not cursor.hasSelection(): # Unindent current line position = cursor.position() cursor.beginEditBlock() removed = self.line_unindent(cursor) position = max(position - removed, 0) cursor.endEditBlock() cursor.setPosition(position) self.setTextCursor(cursor) else: # Unindent every selected line sel_blocks = self._get_selected_blocks() cursor.clearSelection() cursor.beginEditBlock() for block in sel_blocks: cursor.setPosition(block.position()) self.line_unindent(cursor) cursor.endEditBlock() self._show_selected_blocks(sel_blocks) def block_comment(self): """the comment char will be placed at the first non-whitespace char of the first line. For example: if foo: bar will be commented as: #if foo: # bar """ cursor = self.textCursor() if not cursor.hasSelection(): text = cursor.block().text() current_indent_pos = self._get_indent_position(text) if text[current_indent_pos] == self.comment_character: self.line_uncomment(cursor, current_indent_pos) else: self.line_comment(cursor, current_indent_pos) else: sel_blocks = self._get_selected_blocks() text = sel_blocks[0].text() indent_pos = self._get_indent_position(text) comment = True for block in sel_blocks: text = block.text() if ( len(text) > indent_pos and text[indent_pos] == self.comment_character ): # Already commented. comment = False break cursor.clearSelection() cursor.beginEditBlock() for block in sel_blocks: cursor.setPosition(block.position()) if comment: if block.length() < indent_pos: cursor.insertText(" " * indent_pos) self.line_comment(cursor, indent_pos) else: self.line_uncomment(cursor, indent_pos) cursor.endEditBlock() self._show_selected_blocks(sel_blocks) def line_comment(self, cursor, position): cursor.movePosition(QtGui.QTextCursor.MoveOperation.StartOfBlock) cursor.movePosition( QtGui.QTextCursor.MoveOperation.Right, QtGui.QTextCursor.MoveMode.MoveAnchor, position ) cursor.insertText(self.comment_character) def line_uncomment(self, cursor, position=0): cursor.movePosition(QtGui.QTextCursor.MoveOperation.StartOfBlock) text = cursor.block().text() new_text = text[:position] + text[position + 1:] cursor.movePosition( QtGui.QTextCursor.MoveOperation.EndOfBlock, QtGui.QTextCursor.MoveMode.KeepAnchor ) cursor.removeSelectedText() cursor.insertText(new_text) def line_indent(self, cursor): tab = "\t" if self.tabs_as_spaces: tab = " " cursor.insertText(tab) def line_unindent(self, cursor): """ Unindents the cursor's line. Returns the number of characters removed. """ tab = "\t" if self.tabs_as_spaces: tab = " " cursor.movePosition(QtGui.QTextCursor.MoveOperation.StartOfBlock) if cursor.block().text().startswith(tab): new_text = cursor.block().text()[len(tab):] cursor.movePosition( QtGui.QTextCursor.MoveOperation.EndOfBlock, QtGui.QTextCursor.MoveMode.KeepAnchor ) cursor.removeSelectedText() cursor.insertText(new_text) return len(tab) else: return 0 def word_under_cursor(self): """ Return the word under the cursor. """ cursor = self.textCursor() cursor.select(QtGui.QTextCursor.SelectionType.WordUnderCursor) return str(cursor.selectedText()) # ------------------------------------------------------------------------ # QWidget interface # ------------------------------------------------------------------------ # FIXME: This is a quick hack to be able to access the keyPressEvent # from the rest editor. This should be changed to work within the traits # framework. def keyPressEvent_action(self, event): pass def keyPressEvent(self, event): if self.isReadOnly(): return super().keyPressEvent(event) key_sequence = QtGui.QKeySequence(event.key() + int(event.modifiers())) self.keyPressEvent_action(event) # FIXME: see above # If the cursor is in the middle of the first line, pressing the "up" # key causes the cursor to go to the start of the first line, i.e. the # beginning of the document. Likewise, if the cursor is somewhere in the # last line, the "down" key causes it to go to the end. cursor = self.textCursor() if key_sequence.matches(QtGui.QKeySequence(QtCore.Qt.Key.Key_Up)): cursor.movePosition(QtGui.QTextCursor.MoveOperation.StartOfLine) if cursor.atStart(): self.setTextCursor(cursor) event.accept() elif key_sequence.matches(QtGui.QKeySequence(QtCore.Qt.Key.Key_Down)): cursor.movePosition(QtGui.QTextCursor.MoveOperation.EndOfLine) if cursor.atEnd(): self.setTextCursor(cursor) event.accept() elif self.auto_indent and key_sequence.matches( QtGui.QKeySequence(QtCore.Qt.Key.Key_Return) ): event.accept() return self.autoindent_newline() elif key_sequence.matches(self.indent_key): event.accept() return self.block_indent() elif key_sequence.matches(self.unindent_key): event.accept() return self.block_unindent() elif key_sequence.matches(self.comment_key): event.accept() return self.block_comment() elif ( self.auto_indent and self.smart_backspace and key_sequence.matches(self.backspace_key) and self._backspace_should_unindent() ): event.accept() return self.block_unindent() return super().keyPressEvent(event) def resizeEvent(self, event): QtGui.QPlainTextEdit.resizeEvent(self, event) contents = self.contentsRect() self.line_number_widget.setGeometry( QtCore.QRect( contents.left(), contents.top(), self.line_number_widget.digits_width(), contents.height(), ) ) # use the viewport width to determine the right edge. This allows for # the propper placement w/ and w/o the scrollbar right_pos = ( self.viewport().width() + self.line_number_widget.width() + 1 - self.status_widget.sizeHint().width() ) self.status_widget.setGeometry( QtCore.QRect( right_pos, contents.top(), self.status_widget.sizeHint().width(), contents.height(), ) ) def focusOutEvent(self, event): QtGui.QPlainTextEdit.focusOutEvent(self, event) self.focus_lost.emit() def sizeHint(self): # Suggest a size that is 80 characters wide and 40 lines tall. style = self.style() opt = QtGui.QStyleOptionHeader() font_metrics = QtGui.QFontMetrics(self.document().defaultFont()) # 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 = font_metrics.horizontalAdvance(" ") * 80 else: width = font_metrics.width(" ") * 80 width += self.line_number_widget.sizeHint().width() width += self.status_widget.sizeHint().width() width += style.pixelMetric(QtGui.QStyle.PixelMetric.PM_ScrollBarExtent, opt, self) height = font_metrics.height() * 40 return QtCore.QSize(width, height) # ------------------------------------------------------------------------ # Private methods # ------------------------------------------------------------------------ def _get_indent_position(self, line): trimmed = line.rstrip() if len(trimmed) != 0: return line.index(trimmed) else: # if line is all spaces, treat it as the indent position return len(line) def _show_selected_blocks(self, selected_blocks): """ Assumes contiguous blocks """ cursor = self.textCursor() cursor.clearSelection() cursor.setPosition(selected_blocks[0].position()) cursor.movePosition(QtGui.QTextCursor.MoveOperation.StartOfBlock) cursor.movePosition( QtGui.QTextCursor.MoveOperation.NextBlock, QtGui.QTextCursor.MoveMode.KeepAnchor, len(selected_blocks), ) cursor.movePosition( QtGui.QTextCursor.MoveOperation.EndOfBlock, QtGui.QTextCursor.MoveMode.KeepAnchor ) self.setTextCursor(cursor) def _get_selected_blocks(self): cursor = self.textCursor() if cursor.position() > cursor.anchor(): start_pos = cursor.anchor() end_pos = cursor.position() else: start_pos = cursor.position() end_pos = cursor.anchor() cursor.setPosition(start_pos) cursor.movePosition(QtGui.QTextCursor.MoveOperation.StartOfBlock) blocks = [cursor.block()] while cursor.movePosition(QtGui.QTextCursor.MoveOperation.NextBlock): block = cursor.block() if block.position() < end_pos: blocks.append(block) return blocks def _backspace_should_unindent(self): cursor = self.textCursor() # Don't unindent if we have a selection. if cursor.hasSelection(): return False column = cursor.columnNumber() # Don't unindent if we are at the beggining of the line if column < self.tab_width: return False else: # Unindent if we are at the indent position return column == self._get_indent_position(cursor.block().text()) class AdvancedCodeWidget(QtGui.QWidget): """ Advanced widget for viewing and editing code, with support for search & replace """ # ------------------------------------------------------------------------ # AdvancedCodeWidget interface # ------------------------------------------------------------------------ def __init__(self, parent, font=None, lexer=None): super().__init__(parent) self.code = CodeWidget(self, font=font, lexer=lexer) self.find = FindWidget(self) self.find.hide() self.replace = ReplaceWidget(self) self.replace.hide() self.replace.replace_button.setEnabled(False) self.replace.replace_all_button.setEnabled(False) self.active_find_widget = None self.previous_find_widget = None self.code.selectionChanged.connect(self._update_replace_enabled) self.find.line_edit.returnPressed.connect(self.find_next) self.find.next_button.clicked.connect(self.find_next) self.find.prev_button.clicked.connect(self.find_prev) self.replace.line_edit.returnPressed.connect(self.find_next) self.replace.line_edit.textChanged.connect( self._update_replace_all_enabled ) self.replace.next_button.clicked.connect(self.find_next) self.replace.prev_button.clicked.connect(self.find_prev) self.replace.replace_button.clicked.connect(self.replace_next) self.replace.replace_all_button.clicked.connect(self.replace_all) layout = QtGui.QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) layout.addWidget(self.code) layout.addWidget(self.find) layout.addWidget(self.replace) self.setLayout(layout) def _remove_event_listeners(self): self.code.selectionChanged.disconnect(self._update_replace_enabled) self.find.line_edit.returnPressed.disconnect(self.find_next) self.find.next_button.clicked.disconnect(self.find_next) self.find.prev_button.clicked.disconnect(self.find_prev) self.replace.line_edit.returnPressed.disconnect(self.find_next) self.replace.line_edit.textChanged.disconnect( self._update_replace_all_enabled ) self.replace.next_button.clicked.disconnect(self.find_next) self.replace.prev_button.clicked.disconnect(self.find_prev) self.replace.replace_button.clicked.disconnect(self.replace_next) self.replace.replace_all_button.clicked.disconnect(self.replace_all) self.code._remove_event_listeners() def lines(self): """ Return the number of lines. """ return self.code.lines() def set_line_column(self, line, column): """ Move the cursor to a particular line/column position. """ self.code.set_line_column(line, column) def get_line_column(self): """ Get the current line and column numbers. """ return self.code.get_line_column() def get_selected_text(self): """ Return the currently selected text. """ return self.code.get_selected_text() def set_info_lines(self, info_lines): self.code.set_info_lines(info_lines) def set_warn_lines(self, warn_lines): self.code.set_warn_lines(warn_lines) def set_error_lines(self, error_lines): self.code.set_error_lines(error_lines) def enable_find(self): self.replace.hide() self.find.show() self.find.setFocus() if self.active_find_widget == self.replace or ( not self.active_find_widget and self.previous_find_widget == self.replace ): self.find.line_edit.setText(self.replace.line_edit.text()) self.find.line_edit.selectAll() self.active_find_widget = self.find def enable_replace(self): self.find.hide() self.replace.show() self.replace.setFocus() if self.active_find_widget == self.find or ( not self.active_find_widget and self.previous_find_widget == self.find ): self.replace.line_edit.setText(self.find.line_edit.text()) self.replace.line_edit.selectAll() self.active_find_widget = self.replace def find_in_document(self, search_text, direction="forward", replace=None): """ Finds the next occurance of the desired text and optionally replaces it. If 'replace' is None, a regular search will be executed, otherwise it will replace the occurance with the value of 'replace'. Returns the number of occurances found (0 or 1) """ if not search_text: return wrap = self.active_find_widget.wrap_action.isChecked() document = self.code.document() find_cursor = None flags = QtGui.QTextDocument.FindFlags(0) if self.active_find_widget.case_action.isChecked(): flags |= QtGui.QTextDocument.FindFlag.FindCaseSensitively if self.active_find_widget.word_action.isChecked(): flags |= QtGui.QTextDocument.FindFlag.FindWholeWords if direction == "backward": flags |= QtGui.QTextDocument.FindFlag.FindBackward find_cursor = document.find(search_text, self.code.textCursor(), flags) if find_cursor.isNull() and wrap: if direction == "backward": find_cursor = document.find( search_text, document.characterCount() - 1, flags ) else: find_cursor = document.find(search_text, 0, flags) if not find_cursor.isNull(): if replace is not None: find_cursor.beginEditBlock() find_cursor.removeSelectedText() find_cursor.insertText(replace) find_cursor.endEditBlock() find_cursor.movePosition( QtGui.QTextCursor.MoveOperation.Left, QtGui.QTextCursor.MoveMode.MoveAnchor, len(replace), ) find_cursor.movePosition( QtGui.QTextCursor.MoveOperation.Right, QtGui.QTextCursor.MoveMode.KeepAnchor, len(replace), ) self.code.setTextCursor(find_cursor) else: self.code.setTextCursor(find_cursor) return find_cursor else: # else not found: beep or indicate? return None def find_next(self): if not self.active_find_widget: self.enable_find() search_text = str(self.active_find_widget.line_edit.text()) cursor = self.find_in_document(search_text=search_text) if cursor: return 1 return 0 def find_prev(self): if not self.active_find_widget: self.enable_find() search_text = str(self.active_find_widget.line_edit.text()) cursor = self.find_in_document( search_text=search_text, direction="backward" ) if cursor: return 1 return 0 def replace_next(self): search_text = self.replace.line_edit.text() replace_text = self.replace.replace_edit.text() cursor = self.code.textCursor() if cursor.selectedText() == search_text: cursor.beginEditBlock() cursor.removeSelectedText() cursor.insertText(replace_text) cursor.endEditBlock() return self.find_next() return 0 def replace_all(self): search_text = str(self.replace.line_edit.text()) replace_text = str(self.replace.replace_edit.text()) count = 0 cursor = self.code.textCursor() cursor.beginEditBlock() while ( self.find_in_document( search_text=search_text, replace=replace_text ) is not None ): count += 1 cursor.endEditBlock() return count def print_(self, printer): """ Convenience method to call 'print_' on the CodeWidget. """ self.code.print_(printer) def ensureCursorVisible(self): self.code.ensureCursorVisible() def centerCursor(self): self.code.centerCursor() # ------------------------------------------------------------------------ # QWidget interface # ------------------------------------------------------------------------ def keyPressEvent(self, event): key_sequence = QtGui.QKeySequence(event.key() + int(event.modifiers())) if key_sequence.matches(QtGui.QKeySequence.StandardKey.Find): self.enable_find() elif key_sequence.matches(QtGui.QKeySequence.StandardKey.Replace): if not self.code.isReadOnly(): self.enable_replace() elif key_sequence.matches(QtGui.QKeySequence(QtCore.Qt.Key.Key_Escape)): if self.active_find_widget: self.find.hide() self.replace.hide() self.code.setFocus() self.previous_find_widget = self.active_find_widget self.active_find_widget = None return super().keyPressEvent(event) # ------------------------------------------------------------------------ # Private methods # ------------------------------------------------------------------------ def _update_replace_enabled(self): selection = self.code.textCursor().selectedText() find_text = self.replace.line_edit.text() self.replace.replace_button.setEnabled(selection == find_text) def _update_replace_all_enabled(self, text): self.replace.replace_all_button.setEnabled(len(text)) if __name__ == "__main__": def set_trace(): from PyQt4.QtCore import pyqtRemoveInputHook pyqtRemoveInputHook() import pdb pdb.Pdb().set_trace(sys._getframe().f_back) app = QtGui.QApplication(sys.argv) window = AdvancedCodeWidget(None) if len(sys.argv) > 1: f = open(sys.argv[1], "r") window.code.setPlainText(f.read()) window.code.set_info_lines([3, 4, 8]) window.resize(640, 640) window.show() sys.exit(app.exec_()) pyface-7.4.0/pyface/ui/qt4/code_editor/replace_widget.py0000644000076500000240000000442314176253531024150 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 weakref from pyface.qt import QtGui, QtCore from .find_widget import FindWidget class ReplaceWidget(FindWidget): def __init__(self, parent): # We explicitly call __init__ on the classes which FindWidget inherits from # instead of calling FindWidget.__init__. super(FindWidget, self).__init__(parent) self.adv_code_widget = weakref.ref(parent) # 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): self.button_size = self.fontMetrics().horizontalAdvance("Replace All") + 20 else: self.button_size = self.fontMetrics().width("Replace All") + 20 form_layout = QtGui.QFormLayout() form_layout.addRow("Fin&d", self._create_find_control()) form_layout.addRow("Rep&lace", self._create_replace_control()) layout = QtGui.QHBoxLayout() layout.addLayout(form_layout) close_button = QtGui.QPushButton("Close") layout.addWidget(close_button, 1, QtCore.Qt.AlignmentFlag.AlignRight) close_button.clicked.connect(self.hide) self.setLayout(layout) def _create_replace_control(self): control = QtGui.QWidget(self) self.replace_edit = QtGui.QLineEdit() self.replace_button = QtGui.QPushButton("&Replace") self.replace_button.setFixedWidth(self.button_size) self.replace_all_button = QtGui.QPushButton("Replace &All") self.replace_all_button.setFixedWidth(self.button_size) layout = QtGui.QHBoxLayout() layout.addWidget(self.replace_edit) layout.addWidget(self.replace_button) layout.addWidget(self.replace_all_button) layout.addStretch(2) layout.setContentsMargins(0, 0, 0, 0) control.setLayout(layout) return control pyface-7.4.0/pyface/ui/qt4/code_editor/find_widget.py0000644000076500000240000000546014176253531023457 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 weakref from pyface.qt import QtGui, QtCore class FindWidget(QtGui.QWidget): def __init__(self, parent): super().__init__(parent) self.adv_code_widget = weakref.ref(parent) # 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): self.button_size = self.fontMetrics().horizontalAdvance("Replace All") + 20 else: self.button_size = self.fontMetrics().width("Replace All") + 20 form_layout = QtGui.QFormLayout() form_layout.addRow("Fin&d", self._create_find_control()) layout = QtGui.QHBoxLayout() layout.addLayout(form_layout) close_button = QtGui.QPushButton("Close") layout.addWidget(close_button, 1, QtCore.Qt.AlignmentFlag.AlignRight) close_button.clicked.connect(self.hide) self.setLayout(layout) def setFocus(self): self.line_edit.setFocus() def _create_find_control(self): control = QtGui.QWidget(self) self.line_edit = QtGui.QLineEdit() self.next_button = QtGui.QPushButton("&Next") self.next_button.setFixedWidth(self.button_size) self.prev_button = QtGui.QPushButton("&Prev") self.prev_button.setFixedWidth(self.button_size) self.options_button = QtGui.QPushButton("&Options") self.options_button.setFixedWidth(self.button_size) options_menu = QtGui.QMenu(self) self.case_action = QtGui.QAction("Match &case", options_menu) self.case_action.setCheckable(True) self.word_action = QtGui.QAction("Match words", options_menu) self.word_action.setCheckable(True) self.wrap_action = QtGui.QAction("Wrap search", options_menu) self.wrap_action.setCheckable(True) self.wrap_action.setChecked(True) options_menu.addAction(self.case_action) options_menu.addAction(self.word_action) options_menu.addAction(self.wrap_action) self.options_button.setMenu(options_menu) layout = QtGui.QHBoxLayout() layout.addWidget(self.line_edit) layout.addWidget(self.next_button) layout.addWidget(self.prev_button) layout.addWidget(self.options_button) layout.addStretch(2) layout.setContentsMargins(0, 0, 0, 0) control.setLayout(layout) return control pyface-7.4.0/pyface/ui/qt4/code_editor/gutters.py0000644000076500000240000001043614176253531022670 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 math from pyface.qt import QtCore, QtGui class GutterWidget(QtGui.QWidget): min_width = 5 def sizeHint(self): return QtCore.QSize(self.min_width, 0) def paintEvent(self, event): """ Paint the line numbers. """ painter = QtGui.QPainter(self) painter.fillRect(event.rect(), self.pallette().window()) def wheelEvent(self, event): """ Delegate mouse wheel events to parent for seamless scrolling. """ self.parent().wheelEvent(event) class StatusGutterWidget(GutterWidget): """ Draws status markers """ def __init__(self, *args, **kw): super().__init__(*args, **kw) self.error_lines = [] self.warn_lines = [] self.info_lines = [] def sizeHint(self): return QtCore.QSize(10, 0) def paintEvent(self, event): """ Paint the line numbers. """ painter = QtGui.QPainter(self) painter.fillRect(event.rect(), self.palette().window()) cw = self.parent() pixels_per_block = self.height() / float(cw.blockCount()) for line in self.info_lines: painter.fillRect( QtCore.QRect(0, line * pixels_per_block, self.width(), 3), QtCore.Qt.GlobalColor.green, ) for line in self.warn_lines: painter.fillRect( QtCore.QRect(0, line * pixels_per_block, self.width(), 3), QtCore.Qt.GlobalColor.yellow, ) for line in self.error_lines: painter.fillRect( QtCore.QRect(0, line * pixels_per_block, self.width(), 3), QtCore.Qt.GlobalColor.red, ) class LineNumberWidget(GutterWidget): """ Draw line numbers. """ min_char_width = 4 def fontMetrics(self): # QWidget's fontMetrics method does not provide an up to date # font metrics, just one corresponding to the initial font return QtGui.QFontMetrics(self.font) def set_font(self, font): self.font = font def digits_width(self): nlines = max(1, self.parent().blockCount()) ndigits = max( self.min_char_width, int(math.floor(math.log10(nlines) + 1)) ) # 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 = max( self.fontMetrics().horizontalAdvance("0" * ndigits) + 3, self.min_width ) else: width = max( self.fontMetrics().width("0" * ndigits) + 3, self.min_width ) return width def sizeHint(self): return QtCore.QSize(self.digits_width(), 0) def paintEvent(self, event): """ Paint the line numbers. """ painter = QtGui.QPainter(self) painter.setFont(self.font) painter.fillRect(event.rect(), self.palette().window()) cw = self.parent() block = cw.firstVisibleBlock() blocknum = block.blockNumber() top = ( cw.blockBoundingGeometry(block) .translated(cw.contentOffset()) .top() ) bottom = top + int(cw.blockBoundingRect(block).height()) painter.setBrush(self.palette().windowText()) while block.isValid() and top <= event.rect().bottom(): if block.isVisible() and bottom >= event.rect().top(): painter.drawText( 0, top, self.width() - 2, self.fontMetrics().height(), QtCore.Qt.AlignmentFlag.AlignRight, str(blocknum + 1), ) block = block.next() top = bottom bottom = top + int(cw.blockBoundingRect(block).height()) blocknum += 1 pyface-7.4.0/pyface/ui/qt4/fields/0000755000076500000240000000000014176460550017604 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/qt4/fields/field.py0000644000076500000240000000153314176222673021245 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 Qt-specific implementation of the text field class """ from traits.api import Any, provides from pyface.fields.i_field import IField, MField from pyface.ui.qt4.layout_widget import LayoutWidget @provides(IField) class Field(MField, LayoutWidget): """ The Qt-specific implementation of the field class This is an abstract class which is not meant to be instantiated. """ #: The value held by the field. value = Any() pyface-7.4.0/pyface/ui/qt4/fields/toggle_field.py0000644000076500000240000000614514176222673022612 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 Qt-specific implementation of the toggle field class """ from traits.api import provides from pyface.fields.i_toggle_field import IToggleField, MToggleField from pyface.qt.QtGui import ( QCheckBox, QIcon, QPushButton, QRadioButton ) from .field import Field @provides(IToggleField) class ToggleField(MToggleField, Field): """ The Qt-specific implementation of the toggle field class """ # ------------------------------------------------------------------------ # Private interface # ------------------------------------------------------------------------ # Toolkit control interface --------------------------------------------- def _get_control_value(self): """ Toolkit specific method to get the control's value. """ return self.control.isChecked() def _get_control_text(self): """ Toolkit specific method to get the control's text. """ return self.control.text() def _set_control_value(self, value): """ Toolkit specific method to set the control's value. """ return self.control.setChecked(value) def _set_control_text(self, text): """ Toolkit specific method to set the control's text. """ return self.control.setText(text) def _set_control_icon(self, icon): """ Toolkit specific method to set the control's icon. """ if icon is not None: self.control.setIcon(icon.create_icon()) else: self.control.setIcon(QIcon()) def _observe_control_value(self, remove=False): """ Toolkit specific method to change the control value observer. """ if remove: self.control.toggled.disconnect(self._update_value) else: self.control.toggled.connect(self._update_value) class CheckBoxField(ToggleField): """ The Qt-specific implementation of the checkbox class """ def _create_control(self, parent): """ Create the toolkit-specific control that represents the widget. """ control = QCheckBox(parent) return control class RadioButtonField(ToggleField): """ The Qt-specific implementation of the radio button class This is intended to be used in groups, and shouldn't be used by itself. """ def _create_control(self, parent): """ Create the toolkit-specific control that represents the widget. """ control = QRadioButton(parent) return control class ToggleButtonField(ToggleField): """ The Qt-specific implementation of the toggle button class """ def _create_control(self, parent): """ Create the toolkit-specific control that represents the widget. """ control = QPushButton(parent) control.setCheckable(True) return control pyface-7.4.0/pyface/ui/qt4/fields/__init__.py0000644000076500000240000000000014176222673021705 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/qt4/fields/combo_field.py0000644000076500000240000000735314176253531022427 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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: Enthought, Inc. # Description: """ The Qt-specific implementation of the combo field class """ from traits.api import provides from pyface.fields.i_combo_field import IComboField, MComboField from pyface.qt.QtCore import Qt from pyface.qt.QtGui import QComboBox from .field import Field @provides(IComboField) class ComboField(MComboField, Field): """ The Qt-specific implementation of the combo field class """ # ------------------------------------------------------------------------ # IWidget interface # ------------------------------------------------------------------------ def _create_control(self, parent): """ Create the toolkit-specific control that represents the widget. """ control = QComboBox(parent) control.setInsertPolicy(QComboBox.InsertPolicy.NoInsert) control.setEditable(False) return control # ------------------------------------------------------------------------ # Private interface # ------------------------------------------------------------------------ def _update_value(self, value): """ Handle a change to the value from user interaction """ self.value = self._get_control_value() # Toolkit control interface --------------------------------------------- def _get_control_value(self): """ Toolkit specific method to get the control's value. """ index = self.control.currentIndex() if index != -1: return self.control.itemData(index) else: raise IndexError("no value selected") def _get_control_text(self): """ Toolkit specific method to get the control's value. """ index = self.control.currentIndex() if index != -1: return self.control.itemData(index, Qt.ItemDataRole.DisplayRole) else: raise IndexError("no value selected") def _set_control_value(self, value): """ Toolkit specific method to set the control's value. """ index = self.values.index(value) self.control.setCurrentIndex(index) self.control.activated.emit(index) def _observe_control_value(self, remove=False): """ Toolkit specific method to change the control value observer. """ if remove: self.control.activated.disconnect(self._update_value) else: self.control.activated.connect(self._update_value) def _get_control_text_values(self): """ Toolkit specific method to get the control's values. """ model = self.control.model() values = [] for i in range(model.rowCount()): values.append(model.item(i)) return values def _set_control_values(self, values): """ Toolkit specific method to set the control's values. """ current_value = self.value self.control.clear() for value in self.values: item = self.formatter(value) if isinstance(item, tuple): image, text = item icon = image.create_icon() self.control.addItem(icon, text, userData=value) else: self.control.addItem(item, userData=value) if current_value in values: self._set_control_value(current_value) else: self._set_control_value(self.value) pyface-7.4.0/pyface/ui/qt4/fields/text_field.py0000644000076500000240000000744314176253531022314 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 Qt-specific implementation of the text field class """ from traits.api import Map, provides from pyface.fields.i_text_field import ITextField, MTextField from pyface.qt.QtGui import QLineEdit from .field import Field ECHO_TO_QT_ECHO_MODE = { "normal": QLineEdit.EchoMode.Normal, "password": QLineEdit.EchoMode.Password, "none": QLineEdit.EchoMode.NoEcho, "when_editing": QLineEdit.EchoMode.PasswordEchoOnEdit, } QT_ECHO_MODE_TO_ECHO = { value: key for key, value in ECHO_TO_QT_ECHO_MODE.items() } # mapped trait for Qt line edit echo modes Echo = Map(ECHO_TO_QT_ECHO_MODE, default_value="normal") @provides(ITextField) class TextField(MTextField, Field): """ The Qt-specific implementation of the text field class """ #: Display typed text, or one of several hidden "password" modes. echo = Echo # ------------------------------------------------------------------------ # IWidget interface # ------------------------------------------------------------------------ def _create_control(self, parent): """ Create the toolkit-specific control that represents the widget. """ control = QLineEdit(parent) return control # ------------------------------------------------------------------------ # Private interface # ------------------------------------------------------------------------ def _get_control_value(self): """ Toolkit specific method to get the control's value. """ return self.control.text() def _set_control_value(self, value): """ Toolkit specific method to set the control's value. """ self.control.setText(value) # fire update if self.update_text == "editing_finished": self.control.editingFinished.emit() else: self.control.textEdited.emit(value) def _observe_control_value(self, remove=False): """ Toolkit specific method to change the control value observer. """ if remove: self.control.textEdited.disconnect(self._update_value) else: self.control.textEdited.connect(self._update_value) def _get_control_placeholder(self): """ Toolkit specific method to set the control's placeholder. """ return self.control.placeholderText() def _set_control_placeholder(self, placeholder): """ Toolkit specific method to set the control's placeholder. """ self.control.setPlaceholderText(placeholder) def _get_control_echo(self): """ Toolkit specific method to get the control's echo. """ return QT_ECHO_MODE_TO_ECHO[self.control.echoMode()] def _set_control_echo(self, echo): """ Toolkit specific method to set the control's echo. """ self.control.setEchoMode(ECHO_TO_QT_ECHO_MODE[echo]) def _get_control_read_only(self): """ Toolkit specific method to get the control's read_only state. """ return self.control.isReadOnly() def _set_control_read_only(self, read_only): """ Toolkit specific method to set the control's read_only state. """ self.control.setReadOnly(read_only) def _observe_control_editing_finished(self, remove=False): """ Change observation of whether editing is finished. """ if remove: self.control.editingFinished.disconnect(self._editing_finished) else: self.control.editingFinished.connect(self._editing_finished) pyface-7.4.0/pyface/ui/qt4/fields/spin_field.py0000644000076500000240000000437214176222673022302 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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: Enthought, Inc. # Description: """ The Qt-specific implementation of the spin field class """ from traits.api import provides from pyface.fields.i_spin_field import ISpinField, MSpinField from pyface.qt.QtGui import QSpinBox from .field import Field @provides(ISpinField) class SpinField(MSpinField, Field): """ The Qt-specific implementation of the spin field class """ # ------------------------------------------------------------------------ # IWidget interface # ------------------------------------------------------------------------ def _create_control(self, parent): """ Create the toolkit-specific control that represents the widget. """ control = QSpinBox(parent) return control # ------------------------------------------------------------------------ # Private interface # ------------------------------------------------------------------------ def _get_control_value(self): """ Toolkit specific method to get the control's value. """ return self.control.value() def _set_control_value(self, value): """ Toolkit specific method to set the control's value. """ self.control.setValue(value) def _observe_control_value(self, remove=False): """ Toolkit specific method to change the control value observer. """ if remove: self.control.valueChanged[int].disconnect(self._update_value) else: self.control.valueChanged[int].connect(self._update_value) def _get_control_bounds(self): """ Toolkit specific method to get the control's bounds. """ return (self.control.minimum(), self.control.maximum()) def _set_control_bounds(self, bounds): """ Toolkit specific method to set the control's bounds. """ self.control.setRange(*bounds) pyface-7.4.0/pyface/ui/qt4/fields/time_field.py0000644000076500000240000000371114176222673022263 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 Qt-specific implementation of the time field class """ from traits.api import provides from pyface.qt.QtGui import QTimeEdit from pyface.fields.i_time_field import ITimeField, MTimeField from pyface.ui.qt4.util.datetime import pytime_to_qtime, qtime_to_pytime from .field import Field @provides(ITimeField) class TimeField(MTimeField, Field): """ The Qt-specific implementation of the time field class """ # ------------------------------------------------------------------------ # IWidget interface # ------------------------------------------------------------------------ def _create_control(self, parent): """ Create the toolkit-specific control that represents the widget. """ control = QTimeEdit(parent) return control # ------------------------------------------------------------------------ # Private interface # ------------------------------------------------------------------------ def _get_control_value(self): """ Toolkit specific method to get the control's value. """ return qtime_to_pytime(self.control.time()) def _set_control_value(self, value): """ Toolkit specific method to set the control's value. """ self.control.setTime(pytime_to_qtime(value)) def _observe_control_value(self, remove=False): """ Toolkit specific method to change the control value observer. """ if remove: self.control.timeChanged.disconnect(self._update_value) else: self.control.timeChanged.connect(self._update_value) pyface-7.4.0/pyface/ui/qt4/beep.py0000644000076500000240000000112314176222673017622 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 2012 Philip Chimento """Sound the system bell, Qt implementation.""" from pyface.qt import QtGui def beep(): """Sound the system bell.""" QtGui.QApplication.beep() pyface-7.4.0/pyface/ui/qt4/about_dialog.py0000644000076500000240000001006614176253531021343 0ustar cwebsterstaff00000000000000# (C) Copyright 2007 Riverbank Computing Limited # (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 from traits.api import Any, Callable, List, provides, Str, Tuple from pyface.qt import QtCore, QtGui from pyface.i_about_dialog import IAboutDialog, MAboutDialog from pyface.ui_traits import Image from .dialog import Dialog from .image_resource import ImageResource # The HTML displayed in the QLabel. _DIALOG_TEXT = """

%s

Python %s
Qt %s

%s

Copyright © 2003-2022 Enthought, Inc.
Copyright © 2007 Riverbank Computing Limited

""" @provides(IAboutDialog) class AboutDialog(MAboutDialog, Dialog): """ The toolkit specific implementation of an AboutDialog. See the IAboutDialog interface for the API documentation. """ # 'IAboutDialog' interface --------------------------------------------- additions = List(Str) copyrights = List(Str) image = Image(ImageResource("about")) # Private interface ---------------------------------------------------# #: A list of connected Qt signals to be removed before destruction. #: First item in the tuple is the Qt signal. The second item is the event #: handler. _connections_to_remove = List(Tuple(Any, Callable)) # ------------------------------------------------------------------------- # 'IWidget' interface. # ------------------------------------------------------------------------- def destroy(self): while self._connections_to_remove: signal, handler = self._connections_to_remove.pop() signal.disconnect(handler) super().destroy() # ------------------------------------------------------------------------ # Protected 'IDialog' interface. # ------------------------------------------------------------------------ def _create_contents(self, parent): label = QtGui.QLabel() if self.title == "": if parent.parent() is not None: title = parent.parent().windowTitle() else: title = "" # Set the title. self.title = "About %s" % title # Set the page contents. label.setText(self._create_html()) # Create the button. buttons = QtGui.QDialogButtonBox() if self.ok_label: buttons.addButton(self.ok_label, QtGui.QDialogButtonBox.ButtonRole.AcceptRole) else: buttons.addButton(QtGui.QDialogButtonBox.StandardButton.Ok) buttons.accepted.connect(parent.accept) self._connections_to_remove.append((buttons.accepted, parent.accept)) lay = QtGui.QVBoxLayout() lay.addWidget(label) lay.addWidget(buttons) parent.setLayout(lay) def _create_html(self): # Load the image to be displayed in the about box. path = self.image.absolute_path # The additional strings. additions = "
".join(self.additions) # Get the version numbers. py_version = platform.python_version() qt_version = QtCore.__version__ # The additional copyright strings. copyrights = "
".join( ["Copyright © %s" % line for line in self.copyrights] ) return _DIALOG_TEXT % ( path, additions, py_version, qt_version, copyrights, ) pyface-7.4.0/pyface/ui/qt4/python_editor.py0000644000076500000240000001632314176253531021603 0ustar cwebsterstaff00000000000000# (C) Copyright 2007 Riverbank Computing Limited # (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 traits.api import Bool, Event, provides, Str from pyface.i_python_editor import IPythonEditor, MPythonEditor from pyface.key_pressed_event import KeyPressedEvent from pyface.ui.qt4.layout_widget import LayoutWidget from pyface.ui.qt4.code_editor.code_widget import AdvancedCodeWidget @provides(IPythonEditor) class PythonEditor(MPythonEditor, LayoutWidget): """ The toolkit specific implementation of a PythonEditor. See the IPythonEditor interface for the API documentation. """ # 'IPythonEditor' interface -------------------------------------------- dirty = Bool(False) path = Str() show_line_numbers = Bool(True) # Events ---- changed = Event() key_pressed = Event(KeyPressedEvent) # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, parent=None, **traits): create = traits.pop("create", True) super().__init__(parent=parent, **traits) if create: self.create() warnings.warn( "automatic widget creation is deprecated and will be removed " "in a future Pyface version, use create=False and explicitly " "call create() for future behaviour", PendingDeprecationWarning, ) # ------------------------------------------------------------------------ # 'PythonEditor' interface. # ------------------------------------------------------------------------ def load(self, path=None): """ Loads the contents of the editor. """ if path is None: path = self.path # We will have no path for a new script. if len(path) > 0: f = open(self.path, "r") text = f.read() f.close() else: text = "" self.control.code.setPlainText(text) self.dirty = False def save(self, path=None): """ Saves the contents of the editor. """ if path is None: path = self.path f = open(path, "w") f.write(self.control.code.toPlainText()) f.close() self.dirty = False def select_line(self, lineno): """ Selects the specified line. """ self.control.code.set_line_column(lineno, 0) self.control.code.moveCursor( QtGui.QTextCursor.MoveOperation.EndOfLine, QtGui.QTextCursor.MoveMode.KeepAnchor ) # ------------------------------------------------------------------------ # 'Widget' interface. # ------------------------------------------------------------------------ def _add_event_listeners(self): super()._add_event_listeners() self.control.code.installEventFilter(self._event_filter) # Connect signals for text changes. self.control.code.modificationChanged.connect(self._on_dirty_changed) self.control.code.textChanged.connect(self._on_text_changed) def _remove_event_listeners(self): if self.control is not None: # Disconnect signals for text changes. self.control.code.modificationChanged.disconnect( self._on_dirty_changed ) self.control.code.textChanged.disconnect(self._on_text_changed) # Disconnect signals from control and other dependent widgets self.control._remove_event_listeners() if self._event_filter is not None: self.control.code.removeEventFilter(self._event_filter) super()._remove_event_listeners() def __event_filter_default(self): return PythonEditorEventFilter(self, self.control) # ------------------------------------------------------------------------ # Trait handlers. # ------------------------------------------------------------------------ def _path_changed(self): self._changed_path() def _show_line_numbers_changed(self): if self.control is not None: self.control.code.line_number_widget.setVisible( self.show_line_numbers ) self.control.code.update_line_number_width() # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _create_control(self, parent): """ Creates the toolkit-specific control for the widget. """ self.control = control = AdvancedCodeWidget(parent) self._show_line_numbers_changed() # Load the editor's contents. self.load() return control def _on_dirty_changed(self, dirty): """ Called whenever a change is made to the dirty state of the document. """ self.dirty = dirty def _on_text_changed(self): """ Called whenever a change is made to the text of the document. """ self.changed = True class PythonEditorEventFilter(QtCore.QObject): """ A thin wrapper around the advanced code widget to handle the key_pressed Event. """ def __init__(self, editor, parent): super().__init__(parent) self.__editor = editor def eventFilter(self, obj, event): """ Reimplemented to trap key presses. """ if ( self.__editor.control and obj == self.__editor.control and event.type() == QtCore.QEvent.Type.FocusOut ): # Hack for Traits UI compatibility. self.__editor.control.lostFocus.emit() elif ( self.__editor.control and obj == self.__editor.control.code and event.type() == QtCore.QEvent.Type.KeyPress ): # Pyface doesn't seem to be Str aware. Only keep the key code # if it corresponds to a single Latin1 character. kstr = event.text() try: kcode = ord(str(kstr)) except: kcode = 0 mods = event.modifiers() self.key_pressed = KeyPressedEvent( alt_down=( (mods & QtCore.Qt.KeyboardModifier.AltModifier) == QtCore.Qt.KeyboardModifier.AltModifier ), control_down=( (mods & QtCore.Qt.KeyboardModifier.ControlModifier) == QtCore.Qt.KeyboardModifier.ControlModifier ), shift_down=( (mods & QtCore.Qt.KeyboardModifier.ShiftModifier) == QtCore.Qt.KeyboardModifier.ShiftModifier ), key_code=kcode, event=event, ) return super().eventFilter(obj, event) pyface-7.4.0/pyface/ui/qt4/init.py0000644000076500000240000000310714176222673017656 0ustar cwebsterstaff00000000000000# (C) Copyright 2007 Riverbank Computing Limited # (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 sys from traits.trait_notifiers import set_ui_handler, ui_handler from pyface.qt import QtGui from pyface.base_toolkit import Toolkit from .gui import GUI # It's possible that it has already been initialised. _app = QtGui.QApplication.instance() if _app is None: try: # pyface.qt.QtWebKit tries QtWebEngineWidgets first, but # if QtWebEngineWidgets is present, it must be imported prior to # creating a QCoreApplication instance, otherwise importing # QtWebEngineWidgets later would fail (see enthought/pyface#581). # Import it here first before creating the instance. from pyface.qt import QtWebKit # noqa: F401 except ImportError: # This error will be raised in the context where # QtWebKit/QtWebEngine widgets are required. pass _app = QtGui.QApplication(sys.argv) # create the toolkit object toolkit_object = Toolkit("pyface", "qt4", "pyface.ui.qt4") # ensure that Traits has a UI handler appropriate for the toolkit. if ui_handler is None: # Tell the traits notification handlers to use this UI handler set_ui_handler(GUI.invoke_later) pyface-7.4.0/pyface/ui/qt4/clipboard.py0000644000076500000240000000707314176222673020660 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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: Evan Patterson # Date: 06/26/09 # ------------------------------------------------------------------------------ from io import BytesIO from pickle import dumps, load, loads from pyface.qt import QtCore, QtGui from traits.api import provides from pyface.i_clipboard import IClipboard, BaseClipboard # Shortcuts cb = QtGui.QApplication.clipboard() # Custom MIME type representing python objects PYTHON_TYPE = "python/object" @provides(IClipboard) class Clipboard(BaseClipboard): # --------------------------------------------------------------------------- # 'data' property methods: # --------------------------------------------------------------------------- def _get_has_data(self): return self.has_object_data or self.has_text_data or self.has_file_data # --------------------------------------------------------------------------- # 'object_data' property methods: # --------------------------------------------------------------------------- def _get_object_data(self): obj = None mime_data = cb.mimeData() if mime_data.hasFormat(PYTHON_TYPE): serialized_data = BytesIO(mime_data.data(PYTHON_TYPE).data()) # Loading the serialized data the first time returns the klass _ = load(serialized_data) # Loading it a second time returns the actual object obj = load(serialized_data) return obj def _set_object_data(self, data): mime_data = QtCore.QMimeData() serialized_data = dumps(data.__class__) + dumps(data) mime_data.setData(PYTHON_TYPE, QtCore.QByteArray(serialized_data)) cb.setMimeData(mime_data) def _get_has_object_data(self): return cb.mimeData().hasFormat(PYTHON_TYPE) def _get_object_type(self): result = "" mime_data = cb.mimeData() if mime_data.hasFormat(PYTHON_TYPE): try: # We may not be able to load the required class: result = loads(mime_data.data(PYTHON_TYPE).data()) except: pass return result # --------------------------------------------------------------------------- # 'text_data' property methods: # --------------------------------------------------------------------------- def _get_text_data(self): return cb.text() def _set_text_data(self, data): cb.setText(data) def _get_has_text_data(self): return cb.mimeData().hasText() # --------------------------------------------------------------------------- # 'file_data' property methods: # --------------------------------------------------------------------------- def _get_file_data(self): mime_data = cb.mimeData() if mime_data.hasUrls(): return [url.toString() for url in mime_data.urls()] else: return [] def _set_file_data(self, data): if isinstance(data, str): data = [data] mime_data = QtCore.QMimeData() mime_data.setUrls([QtCore.QUrl(path) for path in data]) cb.setMimeData(mime_data) def _get_has_file_data(self): return cb.mimeData().hasUrls() pyface-7.4.0/pyface/ui/qt4/data_view/0000755000076500000240000000000014176460550020301 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/qt4/data_view/data_wrapper.py0000644000076500000240000000343114176222673023327 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 Instance, provides from pyface.data_view.i_data_wrapper import IDataWrapper, MDataWrapper from pyface.qt.QtCore import QMimeData @provides(IDataWrapper) class DataWrapper(MDataWrapper): """ Qt implementaton of IDataWrapper. This wraps a QMimeData in a straightforward way. """ toolkit_data = Instance(QMimeData, args=(), allow_none=False) def mimetypes(self): """ Return a set of mimetypes holding data. Returns ------- mimetypes : set of str The set of mimetypes currently storing data in the toolkit data object. """ return set(self.toolkit_data.formats()) def get_mimedata(self, mimetype): """ Get raw data for the given media type. Parameters ---------- mimetype : str The mime media type to be extracted. Returns ------- mimedata : bytes The mime media data as bytes. """ return self.toolkit_data.data(mimetype).data() def set_mimedata(self, mimetype, raw_data): """ Set raw data for the given media type. Parameters ---------- mimetype : str The mime media type to be extracted. mimedata : bytes The mime media data encoded as bytes.. """ return self.toolkit_data.setData(mimetype, raw_data) pyface-7.4.0/pyface/ui/qt4/data_view/tests/0000755000076500000240000000000014176460550021443 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/qt4/data_view/tests/__init__.py0000644000076500000240000000000014176222673023544 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/qt4/data_view/tests/test_data_view_item_model.py0000644000076500000240000000471714176222673027230 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.testing.optional_dependencies import numpy as np, requires_numpy from pyface.qt.QtCore import QMimeData # This import results in an error without numpy installed # see enthought/pyface#742 if np is not None: from pyface.data_view.data_models.api import ArrayDataModel from pyface.data_view.exporters.row_exporter import RowExporter from pyface.data_view.data_formats import table_format from pyface.data_view.value_types.api import FloatValue from pyface.ui.qt4.data_view.data_view_item_model import DataViewItemModel @requires_numpy class TestDataViewItemModel(TestCase): def setUp(self): self.item_model = self._create_item_model() def _create_item_model(self): self.data = np.arange(120.0).reshape(4, 5, 6) self.model = ArrayDataModel(data=self.data, value_type=FloatValue()) return DataViewItemModel( model=self.model, selection_type='row', exporters=[], ) def _make_indexes(self, indices): return [ self.item_model._to_model_index(row, column) for row, column in indices ] def test_mimeData(self): self.item_model.exporters = [RowExporter(format=table_format)] indexes = self._make_indexes([ ((0, row), (column,)) for column in range(2, 5) for row in range(2, 4) ]) mime_data = self.item_model.mimeData(indexes) self.assertIsInstance(mime_data, QMimeData) self.assertTrue(mime_data.hasFormat('text/plain')) raw_data = mime_data.data('text/plain').data() data = table_format.deserialize(bytes(raw_data)) np.testing.assert_array_equal( data, [ ['12', '13', '14', '15', '16', '17'], ['18', '19', '20', '21', '22', '23'], ] ) def test_mimeData_empty(self): mime_data = self.item_model.mimeData([]) self.assertIsInstance(mime_data, QMimeData) # exact contents depend on Qt, so won't test more deeply pyface-7.4.0/pyface/ui/qt4/data_view/tests/test_data_wrapper.py0000644000076500000240000000240414176222673025527 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 pyface.qt.QtCore import QMimeData from pyface.ui.qt4.data_view.data_wrapper import DataWrapper class TestDataWrapper(TestCase): def test_get_mimedata(self): toolkit_data = QMimeData() toolkit_data.setData('text/plain', b'hello world') data_wrapper = DataWrapper(toolkit_data=toolkit_data) self.assertEqual(data_wrapper.mimetypes(), {'text/plain'}) self.assertEqual(data_wrapper.get_mimedata('text/plain'), b'hello world') def test_set_mimedata(self): data_wrapper = DataWrapper() data_wrapper.set_mimedata('text/plain', b'hello world') toolkit_data = data_wrapper.toolkit_data self.assertEqual(set(toolkit_data.formats()), {'text/plain'}) self.assertEqual( toolkit_data.data('text/plain').data(), b'hello world' ) pyface-7.4.0/pyface/ui/qt4/data_view/__init__.py0000644000076500000240000000000014176222673022402 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/qt4/data_view/data_view_widget.py0000644000076500000240000002210114176253531024154 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 from traits.api import Callable, Enum, Instance, observe, provides from pyface.qt.QtCore import QAbstractItemModel from pyface.qt.QtGui import ( QAbstractItemView, QItemSelection, QItemSelectionModel, QTreeView ) from pyface.data_view.i_data_view_widget import ( IDataViewWidget, MDataViewWidget ) from pyface.ui.qt4.layout_widget import LayoutWidget from .data_view_item_model import DataViewItemModel # XXX This file is scaffolding and may need to be rewritten logger = logging.getLogger(__name__) qt_selection_types = { "row": QAbstractItemView.SelectionBehavior.SelectRows, "column": QAbstractItemView.SelectionBehavior.SelectColumns, "item": QAbstractItemView.SelectionBehavior.SelectItems, } pyface_selection_types = { value: key for key, value in qt_selection_types.items() } qt_selection_modes = { "none": QAbstractItemView.SelectionMode.NoSelection, "single": QAbstractItemView.SelectionMode.SingleSelection, "extended": QAbstractItemView.SelectionMode.ExtendedSelection, } pyface_selection_modes = { value: key for key, value in qt_selection_modes.items() } class DataViewTreeView(QTreeView): """ QTreeView subclass that handles drag and drop via DropHandlers. """ _widget = None def dragEnterEvent(self, event): drop_handler = self._get_drop_handler(event) if drop_handler is not None: event.acceptProposedAction() else: super().dragEnterEvent(event) def dragMoveEvent(self, event): drop_handler = self._get_drop_handler(event) if drop_handler is not None: event.acceptProposedAction() else: super().dragMoveEvent(event) def dragLeaveEvent(self, event): drop_handler = self._get_drop_handler(event) if drop_handler is not None: event.acceptProposedAction() else: super().dragLeaveEvent(event) def dropEvent(self, event): drop_handler = self._get_drop_handler(event) if drop_handler is not None: drop_handler.handle_drop(event, self) event.acceptProposedAction() else: super().dropEvent(event) def _get_drop_handler(self, event): if self._widget is not None: widget = self._widget for drop_handler in widget.drop_handlers: if drop_handler.can_handle_drop(event, self): return drop_handler return None @provides(IDataViewWidget) class DataViewWidget(MDataViewWidget, LayoutWidget): """ The Qt implementation of the DataViewWidget. """ #: Factory for the underlying Qt control, to facilitate replacement control_factory = Callable(DataViewTreeView) # IDataViewWidget Interface traits -------------------------------------- #: What can be selected. Qt supports additional selection types. selection_type = Enum("row", "column", "item") #: How selections are modified. Qt supports turning off selections. selection_mode = Enum("extended", "none", "single") # IWidget Interface traits ---------------------------------------------- control = Instance(QAbstractItemView) # Private traits -------------------------------------------------------- #: The QAbstractItemModel instance used by the view. This will #: usually be a DataViewItemModel subclass. _item_model = Instance(QAbstractItemModel) # ------------------------------------------------------------------------ # IDataViewWidget Interface # ------------------------------------------------------------------------ def _create_item_model(self): """ Create the DataViewItemModel which wraps the data model. """ self._item_model = DataViewItemModel( self.data_model, self.selection_type, self.exporters, ) def _get_control_header_visible(self): """ Method to get the control's header visibility. """ return not self.control.isHeaderHidden() def _set_control_header_visible(self, header_visible): """ Method to set the control's header visibility. """ self.control.setHeaderHidden(not header_visible) def _get_control_selection_type(self): """ Toolkit specific method to get the selection type. """ qt_selection_type = self.control.selectionBehavior() return pyface_selection_types[qt_selection_type] def _set_control_selection_type(self, selection_type): """ Toolkit specific method to change the selection type. """ qt_selection_type = qt_selection_types[selection_type] self.control.setSelectionBehavior(qt_selection_type) self._item_model.selectionType = selection_type def _get_control_selection_mode(self): """ Toolkit specific method to get the selection mode. """ qt_selection_mode = self.control.selectionMode() return pyface_selection_modes[qt_selection_mode] def _set_control_selection_mode(self, selection_mode): """ Toolkit specific method to change the selection mode. """ qt_selection_mode = qt_selection_modes[selection_mode] self.control.setSelectionMode(qt_selection_mode) def _get_control_selection(self): """ Toolkit specific method to get the selection. """ indices = self.control.selectedIndexes() if self.selection_type == 'row': return self._item_model._extract_rows(indices) elif self.selection_type == 'column': return self._item_model._extract_columns(indices) else: return self._item_model._extract_indices(indices) def _set_control_selection(self, selection): """ Toolkit specific method to change the selection. """ selection_model = self.control.selectionModel() select_flags = QItemSelectionModel.SelectionFlag.Select qt_selection = QItemSelection() if self.selection_type == 'row': select_flags |= QItemSelectionModel.SelectionFlag.Rows for row, column in selection: index = self._item_model._to_model_index(row, (0,)) qt_selection.select(index, index) elif self.selection_type == 'column': select_flags |= QItemSelectionModel.SelectionFlag.Columns for row, column in selection: index = self._item_model._to_model_index( row + (0,), column) qt_selection.select(index, index) else: for row, column in selection: index = self._item_model._to_model_index(row, column) qt_selection.select(index, index) selection_model.clearSelection() selection_model.select(qt_selection, select_flags) def _observe_control_selection(self, remove=False): selection_model = self.control.selectionModel() if remove: try: selection_model.selectionChanged.disconnect( self._update_selection ) except (TypeError, RuntimeError): # has already been disconnected logger.info("selectionChanged already disconnected") else: selection_model.selectionChanged.connect(self._update_selection) # ------------------------------------------------------------------------ # IWidget Interface # ------------------------------------------------------------------------ def _create_control(self, parent): """ Create the DataViewWidget's toolkit control. """ self._create_item_model() control = self.control_factory(parent) control._widget = self control.setUniformRowHeights(True) control.setAnimated(True) control.setDragEnabled(True) control.setModel(self._item_model) control.setAcceptDrops(True) control.setDropIndicatorShown(True) return control def destroy(self): """ Perform any actions required to destroy the control. """ if self.control is not None: self.control.setModel(None) # ensure that we release references self.control._widget = None self._item_model = None super().destroy() # ------------------------------------------------------------------------ # Private methods # ------------------------------------------------------------------------ # Trait observers @observe('data_model', dispatch='ui') def _update_item_model(self, event): if self._item_model is not None: self._item_model.model = event.new @observe('exporters.items', dispatch='ui') def _update_exporters(self, event): if self._item_model is not None: self._item_model.exporters = self.exporters pyface-7.4.0/pyface/ui/qt4/data_view/data_view_item_model.py0000644000076500000240000003352714176253531025025 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 from pyface.qt import is_qt4 from pyface.qt.QtCore import QAbstractItemModel, QMimeData, QModelIndex, Qt from pyface.qt.QtGui import QColor from pyface.data_view.abstract_data_model import AbstractDataModel from pyface.data_view.abstract_value_type import CheckState from pyface.data_view.data_view_errors import ( DataViewGetError, DataViewSetError ) from pyface.data_view.index_manager import Root from .data_wrapper import DataWrapper logger = logging.getLogger(__name__) # XXX This file is scaffolding and may need to be rewritten WHITE = QColor(255, 255, 255) BLACK = QColor(0, 0, 0) set_check_state_map = { Qt.CheckState.Checked: CheckState.CHECKED, Qt.CheckState.Unchecked: CheckState.UNCHECKED, } get_check_state_map = { CheckState.CHECKED: Qt.CheckState.Checked, CheckState.UNCHECKED: Qt.CheckState.Unchecked, } class DataViewItemModel(QAbstractItemModel): """ A QAbstractItemModel that understands AbstractDataModels. """ def __init__(self, model, selection_type, exporters, parent=None): super().__init__(parent) self.model = model self.selectionType = selection_type self.exporters = exporters self.destroyed.connect(self._on_destroyed) @property def model(self): return self._model @model.setter def model(self, model: AbstractDataModel): self._disconnect_model_observers() if hasattr(self, '_model'): self.beginResetModel() self._model = model self.endResetModel() else: # model is being initialized self._model = model self._connect_model_observers() # model event listeners def on_structure_changed(self, event): self.beginResetModel() self.endResetModel() def on_values_changed(self, event): top, left, bottom, right = event.new if top == () and bottom == (): # this is a column header change self.headerDataChanged.emit(Qt.Orientation.Horizontal, left[0], right[0]) elif left == () and right == (): # this is a row header change # XXX this is currently not supported and not needed pass else: for i, (top_row, bottom_row) in enumerate(zip(top, bottom)): if top_row != bottom_row: break top = top[:i+1] bottom = bottom[:i+1] top_left = self._to_model_index(top, left) bottom_right = self._to_model_index(bottom, right) self.dataChanged.emit(top_left, bottom_right) # Structure methods def parent(self, index): if not index.isValid(): return QModelIndex() parent = index.internalPointer() if parent == Root: return QModelIndex() grandparent, row = self.model.index_manager.get_parent_and_row(parent) return self.createIndex(row, 0, grandparent) def index(self, row, column, parent): if parent.isValid(): parent_index = self.model.index_manager.create_index( parent.internalPointer(), parent.row(), ) else: parent_index = Root index = self.createIndex(row, column, parent_index) return index def rowCount(self, index=QModelIndex()): row_index = self._to_row_index(index) try: if self.model.can_have_children(row_index): return self.model.get_row_count(row_index) except Exception: logger.exception("Error in rowCount") return 0 def columnCount(self, index=QModelIndex()): row_index = self._to_row_index(index) try: # the number of columns is constant; leaf rows return 0 if self.model.can_have_children(row_index): return self.model.get_column_count() + 1 except Exception: logger.exception("Error in columnCount") return 0 # Data methods def flags(self, index): row = self._to_row_index(index) column = self._to_column_index(index) value_type = self.model.get_value_type(row, column) if row == () and column == (): return Qt.ItemFlag.ItemIsEnabled flags = Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsDragEnabled if not is_qt4 and not self.model.can_have_children(row): flags |= Qt.ItemFlag.ItemNeverHasChildren try: if value_type: if value_type.has_editor_value(self.model, row, column): flags |= Qt.ItemFlag.ItemIsEditable if ( value_type.has_check_state(self.model, row, column) and self.model.can_set_value(row, column) ): flags |= Qt.ItemFlag.ItemIsUserCheckable except DataViewGetError: # expected error, ignore pass except Exception: # unexpected error, log and raise logger.exception( "get flags failed: row %r, column %r", row, column, ) raise return flags def data(self, index, role=Qt.ItemDataRole.DisplayRole): row = self._to_row_index(index) column = self._to_column_index(index) value_type = self.model.get_value_type(row, column) try: if not value_type: return None if role == Qt.ItemDataRole.DisplayRole: if value_type.has_text(self.model, row, column): return value_type.get_text(self.model, row, column) elif role == Qt.ItemDataRole.EditRole: if value_type.has_editor_value(self.model, row, column): return value_type.get_editor_value(self.model, row, column) elif role == Qt.ItemDataRole.DecorationRole: if value_type.has_image(self.model, row, column): image = value_type.get_image(self.model, row, column) if image is not None: return image.create_image() elif role == Qt.ItemDataRole.BackgroundRole: if value_type.has_color(self.model, row, column): color = value_type.get_color(self.model, row, column) if color is not None: return color.to_toolkit() elif role == Qt.ItemDataRole.ForegroundRole: if value_type.has_color(self.model, row, column): color = value_type.get_color(self.model, row, column) if color is not None and color.is_dark: return WHITE else: return BLACK elif role == Qt.ItemDataRole.CheckStateRole: if value_type.has_check_state(self.model, row, column): value = value_type.get_check_state(self.model, row, column) return get_check_state_map[value] elif role == Qt.ItemDataRole.ToolTipRole: if value_type.has_tooltip(self.model, row, column): return value_type.get_tooltip(self.model, row, column) except DataViewGetError: # expected error, ignore pass except Exception: # unexpected error, log and raise logger.exception( "get data failed: row %r, column %r", row, column, ) raise return None def setData(self, index, value, role=Qt.ItemDataRole.EditRole): row = self._to_row_index(index) column = self._to_column_index(index) value_type = self.model.get_value_type(row, column) if not value_type: return False try: if role == Qt.ItemDataRole.EditRole: if value_type.has_editor_value(self.model, row, column): value_type.set_editor_value(self.model, row, column, value) elif role == Qt.ItemDataRole.DisplayRole: if value_type.has_text(self.model, row, column): value_type.set_text(self.model, row, column, value) elif role == Qt.ItemDataRole.CheckStateRole: if value_type.has_check_state(self.model, row, column): state = set_check_state_map[value] value_type.set_check_state(self.model, row, column, state) except DataViewSetError: return False except Exception: # unexpected error, log and persevere logger.exception( "setData failed: row %r, column %r, value %r", row, column, value, ) return False else: return True def headerData(self, section, orientation, role=Qt.ItemDataRole.DisplayRole): if orientation == Qt.Orientation.Horizontal: row = () if section == 0: column = () else: column = (section - 1,) else: # XXX not currently used, but here for symmetry and completeness row = (section,) column = () value_type = self.model.get_value_type(row, column) try: if role == Qt.ItemDataRole.DisplayRole: if value_type.has_text(self.model, row, column): return value_type.get_text(self.model, row, column) except DataViewGetError: # expected error, ignore pass except Exception: # unexpected error, log and raise logger.exception( "get header data failed: row %r, column %r", row, column, ) raise return None def mimeData(self, indexes): mimedata = super().mimeData(indexes) if mimedata is None: mimedata = QMimeData() data_wrapper = DataWrapper(toolkit_data=mimedata) indices = self._normalize_indices(indexes) for exporter in self.exporters: try: exporter.add_data(data_wrapper, self.model, indices) except Exception: # unexpected error, log and raise logger.exception( "data export failed: mimetype {}, indices {}", exporter.format.mimetype, indices, ) raise return data_wrapper.toolkit_data # Private utility methods def _on_destroyed(self): self._disconnect_model_observers() self._model = None def _connect_model_observers(self): if getattr(self, "_model", None) is not None: self._model.observe( self.on_structure_changed, 'structure_changed', dispatch='ui', ) self._model.observe( self.on_values_changed, 'values_changed', dispatch='ui', ) def _disconnect_model_observers(self): if getattr(self, "_model", None) is not None: self._model.observe( self.on_structure_changed, 'structure_changed', dispatch='ui', remove=True, ) self._model.observe( self.on_values_changed, 'values_changed', dispatch='ui', remove=True, ) def _to_row_index(self, index): if not index.isValid(): row_index = () else: parent = index.internalPointer() if parent == Root: row_index = () else: row_index = self.model.index_manager.to_sequence(parent) row_index += (index.row(),) return row_index def _to_column_index(self, index): if not index.isValid(): return () else: column = index.column() if column == 0: return () else: return (column - 1,) def _to_model_index(self, row_index, column_index): if len(row_index) == 0: return QModelIndex() index = self.model.index_manager.from_sequence(row_index[:-1]) row = row_index[-1] if len(column_index) == 0: column = 0 else: column = column_index[0] + 1 return self.createIndex(row, column, index) def _extract_rows(self, indices): rows = [] for index in indices: row = self._to_row_index(index) if (row, ()) not in rows: rows.append((row, ())) return rows def _extract_columns(self, indices): columns = [] for index in indices: row = self._to_row_index(index)[:-1] column = self._to_column_index(index) if (row, column) not in columns: columns.append((row, column)) return columns def _extract_indices(self, indices): return [ (self._to_row_index(index), self._to_column_index(index)) for index in indices ] def _normalize_indices(self, indices): if self.selectionType == 'row': return self._extract_rows(indices) elif self.selectionType == 'column': return self._extract_columns(indices) else: return self._extract_indices(indices) pyface-7.4.0/pyface/ui/qt4/mimedata.py0000644000076500000240000001227214176222673020477 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 pickle import dumps, load, loads, PickleError import warnings import io from pyface.qt import QtCore # ------------------------------------------------------------------------------- # 'PyMimeData' class: # ------------------------------------------------------------------------------- def str2bytes(s): return bytes(s, "ascii") class PyMimeData(QtCore.QMimeData): """ The PyMimeData wraps a Python instance as MIME data. """ # The MIME type for instances. MIME_TYPE = "application/x-ets-qt4-instance" NOPICKLE_MIME_TYPE = "application/x-ets-qt4-instance-no-pickle" def __init__(self, data=None, pickle=True): """ Initialise the instance. """ QtCore.QMimeData.__init__(self) # Keep a local reference to be returned if possible. self._local_instance = data if pickle: if data is not None: # We may not be able to pickle the data. try: pdata = dumps(data) # This format (as opposed to using a single sequence) allows # the type to be extracted without unpickling the data. self.setData(self.MIME_TYPE, dumps(data.__class__) + pdata) except (PickleError, TypeError, AttributeError): # if pickle fails, still try to create a draggable warnings.warn( ( "Could not pickle dragged object %s, " + "using %s mimetype instead" ) % (repr(data), self.NOPICKLE_MIME_TYPE), RuntimeWarning, ) self.setData( self.NOPICKLE_MIME_TYPE, str2bytes(str(id(data))) ) else: self.setData(self.NOPICKLE_MIME_TYPE, str2bytes(str(id(data)))) @classmethod def coerce(cls, md): """ Wrap a QMimeData or a python object to a PyMimeData. """ # See if the data is already of the right type. If it is then we know # we are in the same process. if isinstance(md, cls): return md if isinstance(md, PyMimeData): # if it is a PyMimeData, migrate all its data, subclasses should # override this method if it doesn't do thgs correctly for them data = md.instance() nmd = cls() nmd._local_instance = data for format in md.formats(): nmd.setData(format, md.data(format)) elif isinstance(md, QtCore.QMimeData): # if it is a QMimeData, migrate all its data nmd = cls() for format in md.formats(): nmd.setData(format, md.data(format)) else: # by default, try to pickle the coerced object pickle = True # See if the data is a list, if so check for any items which are # themselves of the right type. If so, extract the instance and # track whether we should pickle. # XXX lists should suffice for now, but may want other containers if isinstance(md, list): pickle = not any( item.hasFormat(cls.NOPICKLE_MIME_TYPE) for item in md if isinstance(item, QtCore.QMimeData) ) md = [ item.instance() if isinstance(item, PyMimeData) else item for item in md ] # Arbitrary python object, wrap it into PyMimeData nmd = cls(md, pickle) return nmd def instance(self): """ Return the instance. """ if self._local_instance is not None: return self._local_instance if not self.hasFormat(self.MIME_TYPE): # We have no pickled python data defined. return None stream = io.BytesIO(self.data(self.MIME_TYPE).data()) try: # Skip the type. load(stream) # Recreate the instance. return load(stream) except PickleError: pass return None def instanceType(self): """ Return the type of the instance. """ if self._local_instance is not None: return self._local_instance.__class__ try: if self.hasFormat(self.MIME_TYPE): return loads(self.data(self.MIME_TYPE).data()) except PickleError: pass return None def localPaths(self): """ The list of local paths from url list, if any. """ ret = [] for url in self.urls(): if url.scheme() == "file": ret.append(url.toLocalFile()) return ret pyface-7.4.0/pyface/ui/qt4/gui.py0000644000076500000240000001507014176253531017476 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # (C) Copyright 2007 Riverbank Computing Limited # 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 import logging from pyface.qt import QtCore, QtGui from traits.api import Bool, HasTraits, observe, provides, Str from pyface.util.guisupport import start_event_loop_qt4 from pyface.i_gui import IGUI, MGUI # Logging. logger = logging.getLogger(__name__) @provides(IGUI) class GUI(MGUI, HasTraits): """ The toolkit specific implementation of a GUI. See the IGUI interface for the API documentation. """ # 'GUI' interface -----------------------------------------------------# busy = Bool(False) started = Bool(False) state_location = Str() # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, splash_screen=None): # Display the (optional) splash screen. self._splash_screen = splash_screen if self._splash_screen is not None: self._splash_screen.open() # ------------------------------------------------------------------------ # 'GUI' class interface. # ------------------------------------------------------------------------ @classmethod def invoke_after(cls, millisecs, callable, *args, **kw): _FutureCall(millisecs, callable, *args, **kw) @classmethod def invoke_later(cls, callable, *args, **kw): _FutureCall(0, callable, *args, **kw) @classmethod def set_trait_after(cls, millisecs, obj, trait_name, new): _FutureCall(millisecs, setattr, obj, trait_name, new) @classmethod def set_trait_later(cls, obj, trait_name, new): _FutureCall(0, setattr, obj, trait_name, new) @staticmethod def process_events(allow_user_events=True): if allow_user_events: events = QtCore.QEventLoop.ProcessEventsFlag.AllEvents else: events = QtCore.QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents QtCore.QCoreApplication.processEvents(events) @staticmethod def set_busy(busy=True): if busy: QtGui.QApplication.setOverrideCursor(QtCore.Qt.CursorShape.WaitCursor) else: QtGui.QApplication.restoreOverrideCursor() # ------------------------------------------------------------------------ # 'GUI' interface. # ------------------------------------------------------------------------ def start_event_loop(self): if self._splash_screen is not None: self._splash_screen.close() # Make sure that we only set the 'started' trait after the main loop # has really started. self.set_trait_later(self, "started", True) logger.debug("---------- starting GUI event loop ----------") start_event_loop_qt4() self.started = False def stop_event_loop(self): logger.debug("---------- stopping GUI event loop ----------") QtGui.QApplication.quit() # ------------------------------------------------------------------------ # Trait handlers. # ------------------------------------------------------------------------ def _state_location_default(self): """ The default state location handler. """ return self._default_state_location() @observe("busy") def _update_busy_state(self, event): """ The busy trait change handler. """ new = event.new if new: QtGui.QApplication.setOverrideCursor(QtCore.Qt.CursorShape.WaitCursor) else: QtGui.QApplication.restoreOverrideCursor() class _FutureCall(QtCore.QObject): """ This is a helper class that is similar to the wx FutureCall class. """ # Keep a list of references so that they don't get garbage collected. _calls = [] # Manage access to the list of instances. _calls_mutex = QtCore.QMutex() # A new Qt event type for _FutureCalls _pyface_event = QtCore.QEvent.Type(QtCore.QEvent.registerEventType()) def __init__(self, ms, callable, *args, **kw): super().__init__() # Save the arguments. self._ms = ms self._callable = callable self._args = args self._kw = kw # Save the instance. self._calls_mutex.lock() try: self._calls.append(self) finally: 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 here, which would be simpler, because # that only works on QThreads. We want regular Python threads to work. event = QtCore.QEvent(self._pyface_event) QtGui.QApplication.postEvent(self, event) def event(self, event): """ QObject event handler. """ if event.type() == self._pyface_event: if self._ms == 0: # Invoke the callable now try: self._callable(*self._args, **self._kw) finally: # 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) else: # Invoke the callable (puts it at the end of the event queue) QtCore.QTimer.singleShot(self._ms, self._dispatch) return True return super().event(event) def _dispatch(self): """ Invoke the callable. """ try: self._callable(*self._args, **self._kw) finally: self._finished() def _finished(self): """ Remove the call from the list, so it can be garbage collected. """ self._calls_mutex.lock() try: self._calls.remove(self) finally: self._calls_mutex.unlock() pyface-7.4.0/pyface/ui/qt4/image_cache.py0000644000076500000240000000533414176222673021124 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # (C) Copyright 2007 Riverbank Computing Limited # 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 from pyface.qt import QtGui from traits.api import HasTraits, provides from pyface.i_image_cache import IImageCache, MImageCache @provides(IImageCache) class ImageCache(MImageCache, HasTraits): """ The toolkit specific implementation of an ImageCache. See the IImageCache interface for the API documentation. """ # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, width, height): self._width = width self._height = height # ------------------------------------------------------------------------ # 'ImageCache' interface. # ------------------------------------------------------------------------ def get_image(self, filename): image = QtGui.QPixmapCache.find(filename) if image is not None: scaled = self._qt4_scale(image) if scaled is not image: # The Qt cache is application wide so we only keep the last # size asked for. QtGui.QPixmapCache.remove(filename) QtGui.QPixmapCache.insert(filename, scaled) else: # Load the image from the file and add it to the cache. image = QtGui.QPixmap(filename) scaled = self._qt4_scale(image) QtGui.QPixmapCache.insert(filename, scaled) return scaled # Qt doesn't distinguish between bitmaps and images. get_bitmap = get_image # ------------------------------------------------------------------------ # Private 'ImageCache' interface. # ------------------------------------------------------------------------ def _qt4_scale(self, image): """ Scales the given image if necessary. """ # Although Qt won't scale the image if it doesn't need to, it will make # a deep copy which we don't need. if image.width() != self._width or image.height() != self._height: image = image.scaled(self._width, self._height) return image pyface-7.4.0/pyface/ui/qt4/file_dialog.py0000644000076500000240000001256514176253531021156 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # (C) Copyright 2007 Riverbank Computing Limited # 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 import os from pyface.qt import QtCore, QtGui from traits.api import Enum, Int, List, provides, Str from pyface.i_file_dialog import IFileDialog, MFileDialog from .dialog import Dialog @provides(IFileDialog) class FileDialog(MFileDialog, Dialog): """ The toolkit specific implementation of a FileDialog. See the IFileDialog interface for the API documentation. """ # 'IFileDialog' interface ---------------------------------------------# action = Enum("open", "open files", "save as") default_directory = Str() default_filename = Str() default_path = Str() directory = Str() filename = Str() path = Str() paths = List(Str) wildcard = Str() wildcard_index = Int(0) # ------------------------------------------------------------------------ # 'MFileDialog' *CLASS* interface. # ------------------------------------------------------------------------ # In Windows, Qt needs only a * while wx needs a *.* WILDCARD_ALL = "All files (*)|*" @classmethod def create_wildcard(cls, description, extension): """ Creates a wildcard for a given extension. """ if isinstance(extension, str): pattern = extension else: pattern = " ".join(extension) return "%s (%s)|%s|" % (description, pattern, pattern) # ------------------------------------------------------------------------ # Protected 'IDialog' interface. # ------------------------------------------------------------------------ def _create_contents(self, parent): # In PyQt this is a canned dialog. pass # ------------------------------------------------------------------------ # 'IWindow' interface. # ------------------------------------------------------------------------ def close(self): # Get the path of the chosen directory. files = self.control.selectedFiles() if files: self.path = str(files[0]) self.paths = [str(file) for file in files] else: self.path = "" self.paths = [""] # Extract the directory and filename. self.directory, self.filename = os.path.split(self.path) # Get the index of the selected filter. self.wildcard_index = self.control.nameFilters().index( self.control.selectedNameFilter() ) # Let the window close as normal. super().close() # ------------------------------------------------------------------------ # Protected 'IWidget' interface. # ------------------------------------------------------------------------ def _create_control(self, parent): # If the caller provided a default path instead of a default directory # and filename, split the path into it directory and filename # components. if ( len(self.default_path) != 0 and len(self.default_directory) == 0 and len(self.default_filename) == 0 ): default_directory, default_filename = os.path.split( self.default_path ) else: default_directory = self.default_directory default_filename = self.default_filename # Convert the filter. filters = [] for filter_list in self.wildcard.split("|")[::2]: # Qt uses spaces instead of semicolons for extension separators filter_list = filter_list.replace(";", " ") filters.append(filter_list) # Set the default directory. if not default_directory: default_directory = QtCore.QDir.currentPath() dlg = QtGui.QFileDialog(parent, self.title, default_directory) dlg.setViewMode(QtGui.QFileDialog.ViewMode.Detail) dlg.selectFile(default_filename) dlg.setNameFilters(filters) if self.wildcard_index < len(filters): dlg.selectNameFilter(filters[self.wildcard_index]) if self.action == "open": dlg.setAcceptMode(QtGui.QFileDialog.AcceptMode.AcceptOpen) dlg.setFileMode(QtGui.QFileDialog.FileMode.ExistingFile) elif self.action == "open files": dlg.setAcceptMode(QtGui.QFileDialog.AcceptMode.AcceptOpen) dlg.setFileMode(QtGui.QFileDialog.FileMode.ExistingFiles) else: dlg.setAcceptMode(QtGui.QFileDialog.AcceptMode.AcceptSave) dlg.setFileMode(QtGui.QFileDialog.FileMode.AnyFile) return dlg # ------------------------------------------------------------------------ # Trait handlers. # ------------------------------------------------------------------------ def _wildcard_default(self): """ Return the default wildcard. """ return self.WILDCARD_ALL pyface-7.4.0/pyface/ui/qt4/console/0000755000076500000240000000000014176460550020000 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/qt4/console/bracket_matcher.py0000644000076500000240000001027414176253531023473 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 bracket matching for Q[Plain]TextEdit widgets. """ from pyface.qt import QtCore, QtGui class BracketMatcher(QtCore.QObject): """ Matches square brackets, braces, and parentheses based on cursor position. """ # Protected class variables. _opening_map = {"(": ")", "{": "}", "[": "]"} _closing_map = {")": "(", "}": "{", "]": "["} # -------------------------------------------------------------------------- # 'QObject' interface # -------------------------------------------------------------------------- def __init__(self, text_edit): """ Create a call tip manager that is attached to the specified Qt text edit widget. """ assert isinstance(text_edit, (QtGui.QTextEdit, QtGui.QPlainTextEdit)) super().__init__() # The format to apply to matching brackets. self.format = QtGui.QTextCharFormat() self.format.setBackground(QtGui.QColor("silver")) self._text_edit = text_edit text_edit.cursorPositionChanged.connect(self._cursor_position_changed) def _remove_event_listeners(self): self._text_edit.cursorPositionChanged.disconnect( self._cursor_position_changed ) # -------------------------------------------------------------------------- # Protected interface # -------------------------------------------------------------------------- def _find_match(self, position): """ Given a valid position in the text document, try to find the position of the matching bracket. Returns -1 if unsuccessful. """ # Decide what character to search for and what direction to search in. document = self._text_edit.document() start_char = document.characterAt(position) search_char = self._opening_map.get(start_char) if search_char: increment = 1 else: search_char = self._closing_map.get(start_char) if search_char: increment = -1 else: return -1 # Search for the character. char = start_char depth = 0 while position >= 0 and position < document.characterCount(): if char == start_char: depth += 1 elif char == search_char: depth -= 1 if depth == 0: break position += increment char = document.characterAt(position) else: position = -1 return position def _selection_for_character(self, position): """ Convenience method for selecting a character. """ selection = QtGui.QTextEdit.ExtraSelection() cursor = self._text_edit.textCursor() cursor.setPosition(position) cursor.movePosition( QtGui.QTextCursor.MoveOperation.NextCharacter, QtGui.QTextCursor.MoveMode.KeepAnchor ) selection.cursor = cursor selection.format = self.format return selection # Signal handlers ---------------------------------------------------- def _cursor_position_changed(self): """ Updates the document formatting based on the new cursor position. """ # Clear out the old formatting. self._text_edit.setExtraSelections([]) # Attempt to match a bracket for the new cursor position. cursor = self._text_edit.textCursor() if not cursor.hasSelection(): position = cursor.position() - 1 match_position = self._find_match(position) if match_position != -1: extra_selections = [ self._selection_for_character(pos) for pos in (position, match_position) ] self._text_edit.setExtraSelections(extra_selections) pyface-7.4.0/pyface/ui/qt4/console/history_console_widget.py0000644000076500000240000001471514176253531025147 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 .console_widget import ConsoleWidget class HistoryConsoleWidget(ConsoleWidget): """ A ConsoleWidget that keeps a history of the commands that have been executed and provides a readline-esque interface to this history. """ # --------------------------------------------------------------------------- # 'object' interface # --------------------------------------------------------------------------- def __init__(self, *args, **kw): super().__init__(*args, **kw) # HistoryConsoleWidget protected variables. self._history = [] self._history_index = 0 self._history_prefix = "" # --------------------------------------------------------------------------- # 'ConsoleWidget' public interface # --------------------------------------------------------------------------- def execute(self, source=None, hidden=False, interactive=False): """ Reimplemented to the store history. """ if not hidden: history = self.input_buffer if source is None else source executed = super().execute(source, hidden, interactive) if executed and not hidden: # Save the command unless it was an empty string or was identical # to the previous command. history = history.rstrip() if history and (not self._history or self._history[-1] != history): self._history.append(history) # Move the history index to the most recent item. self._history_index = len(self._history) return executed # --------------------------------------------------------------------------- # 'ConsoleWidget' abstract interface # --------------------------------------------------------------------------- def _up_pressed(self): """ Called when the up key is pressed. Returns whether to continue processing the event. """ prompt_cursor = self._get_prompt_cursor() if self._get_cursor().blockNumber() == prompt_cursor.blockNumber(): # Set a search prefix based on the cursor position. col = self._get_input_buffer_cursor_column() input_buffer = self.input_buffer if self._history_index == len(self._history) or ( self._history_prefix and col != len(self._history_prefix) ): self._history_index = len(self._history) self._history_prefix = input_buffer[:col] # Perform the search. self.history_previous(self._history_prefix) # Go to the first line of the prompt for seemless history scrolling. # Emulate readline: keep the cursor position fixed for a prefix # search. cursor = self._get_prompt_cursor() if self._history_prefix: cursor.movePosition( QtGui.QTextCursor.MoveOperation.Right, n=len(self._history_prefix) ) else: cursor.movePosition(QtGui.QTextCursor.MoveOperation.EndOfLine) self._set_cursor(cursor) return False return True def _down_pressed(self): """ Called when the down key is pressed. Returns whether to continue processing the event. """ end_cursor = self._get_end_cursor() if self._get_cursor().blockNumber() == end_cursor.blockNumber(): # Perform the search. self.history_next(self._history_prefix) # Emulate readline: keep the cursor position fixed for a prefix # search. (We don't need to move the cursor to the end of the buffer # in the other case because this happens automatically when the # input buffer is set.) if self._history_prefix: cursor = self._get_prompt_cursor() cursor.movePosition( QtGui.QTextCursor.MoveOperation.Right, n=len(self._history_prefix) ) self._set_cursor(cursor) return False return True # --------------------------------------------------------------------------- # 'HistoryConsoleWidget' public interface # --------------------------------------------------------------------------- def history_previous(self, prefix=""): """ If possible, set the input buffer to a previous item in the history. Parameters: ----------- prefix : str, optional If specified, search for an item with this prefix. """ index = self._history_index while index > 0: index -= 1 history = self._history[index] if history.startswith(prefix): break else: history = None if history is not None: self._history_index = index self.input_buffer = history def history_next(self, prefix=""): """ Set the input buffer to a subsequent item in the history, or to the original search prefix if there is no such item. Parameters: ----------- prefix : str, optional If specified, search for an item with this prefix. """ while self._history_index < len(self._history) - 1: self._history_index += 1 history = self._history[self._history_index] if history.startswith(prefix): break else: self._history_index = len(self._history) history = prefix self.input_buffer = history # --------------------------------------------------------------------------- # 'HistoryConsoleWidget' protected interface # --------------------------------------------------------------------------- def _set_history(self, history, history_index=None): """ Replace the current history with a sequence of history items. """ if history_index is None: history_index = len(history) self._history = list(history) self._history_index = history_index pyface-7.4.0/pyface/ui/qt4/console/__init__.py0000644000076500000240000000000014176222673022101 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/ui/qt4/console/api.py0000644000076500000240000000117714176222673021133 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 .bracket_matcher import BracketMatcher from .call_tip_widget import CallTipWidget from .completion_lexer import CompletionLexer from .console_widget import ConsoleWidget from .history_console_widget import HistoryConsoleWidget pyface-7.4.0/pyface/ui/qt4/console/call_tip_widget.py0000644000076500000240000002232614176253531023510 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 re from unicodedata import category from pyface.qt import QtCore, QtGui class CallTipWidget(QtGui.QLabel): """ Shows call tips by parsing the current text of Q[Plain]TextEdit. """ # -------------------------------------------------------------------------- # 'QObject' interface # -------------------------------------------------------------------------- def __init__(self, text_edit): """ Create a call tip manager that is attached to the specified Qt text edit widget. """ assert isinstance(text_edit, (QtGui.QTextEdit, QtGui.QPlainTextEdit)) super().__init__(None, QtCore.Qt.WindowType.ToolTip) self._hide_timer = QtCore.QBasicTimer() self._text_edit = text_edit self.setFont(text_edit.document().defaultFont()) self.setForegroundRole(QtGui.QPalette.ColorRole.ToolTipText) self.setBackgroundRole(QtGui.QPalette.ColorRole.ToolTipBase) self.setPalette(QtGui.QToolTip.palette()) self.setAlignment(QtCore.Qt.AlignmentFlag.AlignLeft) self.setIndent(1) self.setFrameStyle(QtGui.QFrame.Shape.NoFrame) self.setMargin( 1 + self.style().pixelMetric( QtGui.QStyle.PixelMetric.PM_ToolTipLabelFrameWidth, None, self ) ) self.setWindowOpacity( self.style().styleHint( QtGui.QStyle.StyleHint.SH_ToolTipLabel_Opacity, None, self, None ) / 255.0 ) def eventFilter(self, obj, event): """ Reimplemented to hide on certain key presses and on text edit focus changes. """ if obj == self._text_edit: etype = event.type() if etype == QtCore.QEvent.Type.KeyPress: key = event.key() if key in (QtCore.Qt.Key.Key_Enter, QtCore.Qt.Key.Key_Return): self.hide() elif key == QtCore.Qt.Key.Key_Escape: self.hide() return True elif etype == QtCore.QEvent.Type.FocusOut: self.hide() elif etype == QtCore.QEvent.Type.Enter: self._hide_timer.stop() elif etype == QtCore.QEvent.Type.Leave: self._leave_event_hide() return super().eventFilter(obj, event) def timerEvent(self, event): """ Reimplemented to hide the widget when the hide timer fires. """ if event.timerId() == self._hide_timer.timerId(): self._hide_timer.stop() self.hide() # -------------------------------------------------------------------------- # 'QWidget' interface # -------------------------------------------------------------------------- def enterEvent(self, event): """ Reimplemented to cancel the hide timer. """ super().enterEvent(event) self._hide_timer.stop() def hideEvent(self, event): """ Reimplemented to disconnect signal handlers and event filter. """ super().hideEvent(event) self._text_edit.cursorPositionChanged.disconnect( self._cursor_position_changed ) self._text_edit.removeEventFilter(self) def leaveEvent(self, event): """ Reimplemented to start the hide timer. """ super().leaveEvent(event) self._leave_event_hide() def paintEvent(self, event): """ Reimplemented to paint the background panel. """ painter = QtGui.QStylePainter(self) option = QtGui.QStyleOptionFrame() option.initFrom(self) painter.drawPrimitive(QtGui.QStyle.PrimitiveElement.PE_PanelTipLabel, option) painter.end() super().paintEvent(event) def setFont(self, font): """ Reimplemented to allow use of this method as a slot. """ super().setFont(font) def showEvent(self, event): """ Reimplemented to connect signal handlers and event filter. """ super().showEvent(event) self._text_edit.cursorPositionChanged.connect( self._cursor_position_changed ) self._text_edit.installEventFilter(self) # -------------------------------------------------------------------------- # 'CallTipWidget' interface # -------------------------------------------------------------------------- def show_call_info(self, call_line=None, doc=None, maxlines=20): """ Attempts to show the specified call line and docstring at the current cursor location. The docstring is possibly truncated for length. """ if doc: match = re.match("(?:[^\n]*\n){%i}" % maxlines, doc) if match: doc = doc[: match.end()] + "\n[Documentation continues...]" else: doc = "" if call_line: doc = "\n\n".join([call_line, doc]) return self.show_tip(doc) def show_tip(self, tip): """ Attempts to show the specified tip at the current cursor location. """ # Attempt to find the cursor position at which to show the call tip. text_edit = self._text_edit cursor = text_edit.textCursor() search_pos = cursor.position() - 1 self._start_position, _ = self._find_parenthesis( search_pos, forward=False ) if self._start_position == -1: return False # Set the text and resize the widget accordingly. self.setText(tip) self.resize(self.sizeHint()) # Locate and show the widget. Place the tip below the current line # unless it would be off the screen. In that case, place it above # the current line. padding = 3 # Distance in pixels between cursor bounds and tip box. cursor_rect = text_edit.cursorRect(cursor) if QtCore.__version_info__ >= (5, 10): screen = text_edit.window().windowHandle().screen() screen_rect = screen.availableGeometry() else: screen_rect = QtGui.QApplication.desktop().screenGeometry(text_edit) point = text_edit.mapToGlobal(cursor_rect.bottomRight()) point.setY(point.y() + padding) tip_height = self.size().height() if point.y() + tip_height > screen_rect.height(): point = text_edit.mapToGlobal(cursor_rect.topRight()) point.setY(point.y() - tip_height - padding) self.move(point) self.show() return True # -------------------------------------------------------------------------- # Protected interface # -------------------------------------------------------------------------- def _find_parenthesis(self, position, forward=True): """ If 'forward' is True (resp. False), proceed forwards (resp. backwards) through the line that contains 'position' until an unmatched closing (resp. opening) parenthesis is found. Returns a tuple containing the position of this parenthesis (or -1 if it is not found) and the number commas (at depth 0) found along the way. """ commas = depth = 0 document = self._text_edit.document() char = document.characterAt(position) # Search until a match is found or a non-printable character is # encountered. while category(char) != "Cc" and position > 0: if char == "," and depth == 0: commas += 1 elif char == ")": if forward and depth == 0: break depth += 1 elif char == "(": if not forward and depth == 0: break depth -= 1 position += 1 if forward else -1 char = document.characterAt(position) else: position = -1 return position, commas def _leave_event_hide(self): """ Hides the tooltip after some time has passed (assuming the cursor is not over the tooltip). """ if ( not self._hide_timer.isActive() and # If Enter events always came after Leave events, we wouldn't need # this check. But on Mac OS, it sometimes happens the other way # around when the tooltip is created. QtGui.QApplication.topLevelAt(QtGui.QCursor.pos()) != self ): self._hide_timer.start(300, self) # Signal handlers ---------------------------------------------------- def _cursor_position_changed(self): """ Updates the tip based on user cursor movement. """ cursor = self._text_edit.textCursor() if cursor.position() <= self._start_position: self.hide() else: position, commas = self._find_parenthesis(self._start_position + 1) if position != -1: self.hide() pyface-7.4.0/pyface/ui/qt4/console/console_widget.py0000644000076500000240000023263514176253531023371 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ An abstract base class for console-type widgets. """ # FIXME: This file and the others in this directory have been ripped, more or # less intact, out of IPython. At some point we should figure out a more # maintainable solution. # ----------------------------------------------------------------------------- # Imports # ----------------------------------------------------------------------------- import os from os.path import commonprefix import re import sys from textwrap import dedent from unicodedata import category from pyface.qt import QtCore, QtGui # ----------------------------------------------------------------------------- # Functions # ----------------------------------------------------------------------------- def is_letter_or_number(char): """ Returns whether the specified unicode character is a letter or a number. """ cat = category(char) return cat.startswith("L") or cat.startswith("N") # ----------------------------------------------------------------------------- # Classes # ----------------------------------------------------------------------------- class ConsoleWidget(QtGui.QWidget): """ An abstract base class for console-type widgets. This class has functionality for: * Maintaining a prompt and editing region * Providing the traditional Unix-style console keyboard shortcuts * Performing tab completion * Paging text ConsoleWidget also provides a number of utility methods that will be convenient to implementors of a console-style widget. """ # Configuration ------------------------------------------------------ # The maximum number of lines of text before truncation. Specifying a # non-positive number disables text truncation (not recommended). buffer_size = 500 # The type of underlying text widget to use. Valid values are 'plain', which # specifies a QPlainTextEdit, and 'rich', which specifies a QTextEdit. # NOTE: this value can only be specified during initialization. kind = "plain" # The type of paging to use. Valid values are: # 'inside' : The widget pages like a traditional terminal. # 'hsplit' : When paging is requested, the widget is split # horizontally. The top pane contains the console, and the # bottom pane contains the paged text. # 'vsplit' : Similar to 'hsplit', except that a vertical splitter used. # 'custom' : No action is taken by the widget beyond emitting a # 'custom_page_requested(str)' signal. # 'none' : The text is written directly to the console. # NOTE: this value can only be specified during initialization. paging = "inside" # Whether to override ShortcutEvents for the keybindings defined by this # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take # priority (when it has focus) over, e.g., window-level menu shortcuts. override_shortcuts = False # Signals ------------------------------------------------------------ # Signals that indicate ConsoleWidget state. copy_available = QtCore.Signal(bool) redo_available = QtCore.Signal(bool) undo_available = QtCore.Signal(bool) # Signal emitted when paging is needed and the paging style has been # specified as 'custom'. custom_page_requested = QtCore.Signal(object) # Signal emitted when the font is changed. font_changed = QtCore.Signal(QtGui.QFont) # Protected class variables ------------------------------------------ # When the control key is down, these keys are mapped. _ctrl_down_remap = { QtCore.Qt.Key.Key_B: QtCore.Qt.Key.Key_Left, QtCore.Qt.Key.Key_F: QtCore.Qt.Key.Key_Right, QtCore.Qt.Key.Key_A: QtCore.Qt.Key.Key_Home, QtCore.Qt.Key.Key_P: QtCore.Qt.Key.Key_Up, QtCore.Qt.Key.Key_N: QtCore.Qt.Key.Key_Down, QtCore.Qt.Key.Key_D: QtCore.Qt.Key.Key_Delete, } if not sys.platform == "darwin": # On OS X, Ctrl-E already does the right thing, whereas End moves the # cursor to the bottom of the buffer. _ctrl_down_remap[QtCore.Qt.Key.Key_E] = QtCore.Qt.Key.Key_End # The shortcuts defined by this widget. We need to keep track of these to # support 'override_shortcuts' above. _shortcuts = set(_ctrl_down_remap.keys()) | set( [QtCore.Qt.Key.Key_C, QtCore.Qt.Key.Key_G, QtCore.Qt.Key.Key_O, QtCore.Qt.Key.Key_V] ) # --------------------------------------------------------------------------- # 'QObject' interface # --------------------------------------------------------------------------- def __init__(self, parent=None): """ Create a ConsoleWidget. Parameters: ----------- parent : QWidget, optional [default None] The parent for this widget. """ super().__init__(parent) # A list of connected Qt signals to be removed before destruction. # First item in the tuple is the Qt signal. The second item is the # event handler. self._connections_to_remove = [] # Create the layout and underlying text widget. layout = QtGui.QStackedLayout(self) layout.setContentsMargins(0, 0, 0, 0) self._control = self._create_control() self._page_control = None self._splitter = None if self.paging in ("hsplit", "vsplit"): self._splitter = QtGui.QSplitter() if self.paging == "hsplit": self._splitter.setOrientation(QtCore.Qt.Orientation.Horizontal) else: self._splitter.setOrientation(QtCore.Qt.Orientation.Vertical) self._splitter.addWidget(self._control) layout.addWidget(self._splitter) else: layout.addWidget(self._control) # Create the paging widget, if necessary. if self.paging in ("inside", "hsplit", "vsplit"): self._page_control = self._create_page_control() if self._splitter: self._page_control.hide() self._splitter.addWidget(self._page_control) else: layout.addWidget(self._page_control) # Initialize protected variables. Some variables contain useful state # information for subclasses; they should be considered read-only. self._continuation_prompt = "> " self._continuation_prompt_html = None self._executing = False self._filter_drag = False self._filter_resize = False self._prompt = "" self._prompt_html = None self._prompt_pos = 0 self._prompt_sep = "" self._reading = False self._reading_callback = None self._tab_width = 8 self._text_completing_pos = 0 self._filename = "python.html" self._png_mode = None # Set a monospaced font. self.reset_font() # Configure actions. action = QtGui.QAction("Print", None) action.setEnabled(True) printkey = QtGui.QKeySequence(QtGui.QKeySequence.StandardKey.Print) if printkey.matches("Ctrl+P") and sys.platform != "darwin": # Only override the default if there is a collision. # Qt ctrl = cmd on OSX, so that the match gets a false positive. printkey = "Ctrl+Shift+P" action.setShortcut(printkey) action.triggered.connect(self.print_) self._connections_to_remove.append((action.triggered, self.print_)) self.addAction(action) self._print_action = action action = QtGui.QAction("Save as HTML/XML", None) action.setEnabled(self.can_export()) action.setShortcut(QtGui.QKeySequence.StandardKey.Save) action.triggered.connect(self.export) self._connections_to_remove.append((action.triggered, self.export)) self.addAction(action) self._export_action = action action = QtGui.QAction("Select All", None) action.setEnabled(True) action.setShortcut(QtGui.QKeySequence.StandardKey.SelectAll) action.triggered.connect(self.select_all) self._connections_to_remove.append((action.triggered, self.select_all)) self.addAction(action) self._select_all_action = action def eventFilter(self, obj, event): """ Reimplemented to ensure a console-like behavior in the underlying text widgets. """ etype = event.type() if etype == QtCore.QEvent.Type.KeyPress: # Re-map keys for all filtered widgets. key = event.key() if ( self._control_key_down(event.modifiers()) and key in self._ctrl_down_remap ): new_event = QtGui.QKeyEvent( QtCore.QEvent.Type.KeyPress, self._ctrl_down_remap[key], QtCore.Qt.KeyboardModifier.NoModifier, ) QtGui.QApplication.sendEvent(obj, new_event) return True elif obj == self._control: return self._event_filter_console_keypress(event) elif obj == self._page_control: return self._event_filter_page_keypress(event) # Make middle-click paste safe. elif ( etype == QtCore.QEvent.Type.MouseButtonRelease and event.button() == QtCore.Qt.MouseButton.MiddleButton and obj == self._control.viewport() ): cursor = self._control.cursorForPosition(event.pos()) self._control.setTextCursor(cursor) self.paste(QtGui.QClipboard.Mode.Selection) return True # Manually adjust the scrollbars *after* a resize event is dispatched. elif etype == QtCore.QEvent.Type.Resize and not self._filter_resize: self._filter_resize = True QtGui.QApplication.sendEvent(obj, event) self._adjust_scrollbars() self._filter_resize = False return True # Override shortcuts for all filtered widgets. elif ( etype == QtCore.QEvent.Type.ShortcutOverride and self.override_shortcuts and self._control_key_down(event.modifiers()) and event.key() in self._shortcuts ): event.accept() # Ensure that drags are safe. The problem is that the drag starting # logic, which determines whether the drag is a Copy or Move, is locked # down in QTextControl. If the widget is editable, which it must be if # we're not executing, the drag will be a Move. The following hack # prevents QTextControl from deleting the text by clearing the selection # when a drag leave event originating from this widget is dispatched. # The fact that we have to clear the user's selection is unfortunate, # but the alternative--trying to prevent Qt from using its hardwired # drag logic and writing our own--is worse. elif ( etype == QtCore.QEvent.Type.DragEnter and obj == self._control.viewport() and event.source() == self._control.viewport() ): self._filter_drag = True elif ( etype == QtCore.QEvent.Type.DragLeave and obj == self._control.viewport() and self._filter_drag ): cursor = self._control.textCursor() cursor.clearSelection() self._control.setTextCursor(cursor) self._filter_drag = False # Ensure that drops are safe. elif etype == QtCore.QEvent.Type.Drop and obj == self._control.viewport(): cursor = self._control.cursorForPosition(event.pos()) if self._in_buffer(cursor.position()): text = event.mimeData().text() self._insert_plain_text_into_buffer(cursor, text) # Qt is expecting to get something here--drag and drop occurs in its # own event loop. Send a DragLeave event to end it. QtGui.QApplication.sendEvent(obj, QtGui.QDragLeaveEvent()) return True return super().eventFilter(obj, event) def _remove_event_listeners(self): while self._connections_to_remove: signal, handler = self._connections_to_remove.pop() signal.disconnect(handler) # --------------------------------------------------------------------------- # 'QWidget' interface # --------------------------------------------------------------------------- def sizeHint(self): """ Reimplemented to suggest a size that is 80 characters wide and 25 lines high. """ font_metrics = QtGui.QFontMetrics(self.font) margin = ( self._control.frameWidth() + self._control.document().documentMargin() ) * 2 style = self.style() splitwidth = style.pixelMetric(QtGui.QStyle.PixelMetric.PM_SplitterWidth) # Note 1: Despite my best efforts to take the various margins into # account, the width is still coming out a bit too small, so we include # a fudge factor of one character here. # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due # to a Qt bug on certain Mac OS systems where it returns 0. # 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 = font_metrics.horizontalAdvance(" ") * 81 + margin else: width = font_metrics.width(" ") * 81 + margin width += style.pixelMetric(QtGui.QStyle.PixelMetric.PM_ScrollBarExtent) if self.paging == "hsplit": width = width * 2 + splitwidth height = font_metrics.height() * 25 + margin if self.paging == "vsplit": height = height * 2 + splitwidth return QtCore.QSize(int(width), int(height)) # --------------------------------------------------------------------------- # 'ConsoleWidget' public interface # --------------------------------------------------------------------------- def can_copy(self): """ Returns whether text can be copied to the clipboard. """ return self._control.textCursor().hasSelection() def can_cut(self): """ Returns whether text can be cut to the clipboard. """ cursor = self._control.textCursor() return ( cursor.hasSelection() and self._in_buffer(cursor.anchor()) and self._in_buffer(cursor.position()) ) def can_paste(self): """ Returns whether text can be pasted from the clipboard. """ if self._control.textInteractionFlags() & QtCore.Qt.TextInteractionFlag.TextEditable: return bool(QtGui.QApplication.clipboard().text()) return False def can_export(self): """Returns whether we can export. Currently only rich widgets can export html. """ return self.kind == "rich" def clear(self, keep_input=True): """ Clear the console. Parameters: ----------- keep_input : bool, optional (default True) If set, restores the old input buffer if a new prompt is written. """ if self._executing: self._control.clear() else: if keep_input: input_buffer = self.input_buffer self._control.clear() self._show_prompt() if keep_input: self.input_buffer = input_buffer def copy(self): """ Copy the currently selected text to the clipboard. """ self._control.copy() def cut(self): """ Copy the currently selected text to the clipboard and delete it if it's inside the input buffer. """ self.copy() if self.can_cut(): self._control.textCursor().removeSelectedText() def execute(self, source=None, hidden=False, interactive=False): """ Executes source or the input buffer, possibly prompting for more input. Parameters: ----------- source : str, optional The source to execute. If not specified, the input buffer will be used. If specified and 'hidden' is False, the input buffer will be replaced with the source before execution. hidden : bool, optional (default False) If set, no output will be shown and the prompt will not be modified. In other words, it will be completely invisible to the user that an execution has occurred. interactive : bool, optional (default False) Whether the console is to treat the source as having been manually entered by the user. The effect of this parameter depends on the subclass implementation. Raises: ------- RuntimeError If incomplete input is given and 'hidden' is True. In this case, it is not possible to prompt for more input. Returns: -------- A boolean indicating whether the source was executed. """ # WARNING: The order in which things happen here is very particular, in # large part because our syntax highlighting is fragile. If you change # something, test carefully! # Decide what to execute. if source is None: source = self.input_buffer if not hidden: # A newline is appended later, but it should be considered part # of the input buffer. source += "\n" elif not hidden: self.input_buffer = source # Execute the source or show a continuation prompt if it is incomplete. complete = self._is_complete(source, interactive) if hidden: if complete: self._execute(source, hidden) else: error = 'Incomplete noninteractive input: "%s"' raise RuntimeError(error % source) else: if complete: self._append_plain_text("\n") self._executing_input_buffer = self.input_buffer self._executing = True self._prompt_finished() # The maximum block count is only in effect during execution. # This ensures that _prompt_pos does not become invalid due to # text truncation. self._control.document().setMaximumBlockCount(self.buffer_size) # Setting a positive maximum block count will automatically # disable the undo/redo history, but just to be safe: self._control.setUndoRedoEnabled(False) # Perform actual execution. self._execute(source, hidden) else: # Do this inside an edit block so continuation prompts are # removed seamlessly via undo/redo. cursor = self._get_end_cursor() cursor.beginEditBlock() cursor.insertText("\n") self._insert_continuation_prompt(cursor) cursor.endEditBlock() # Do not do this inside the edit block. It works as expected # when using a QPlainTextEdit control, but does not have an # effect when using a QTextEdit. I believe this is a Qt bug. self._control.moveCursor(QtGui.QTextCursor.MoveOperation.End) return complete def _get_input_buffer(self): """ The text that the user has entered entered at the current prompt. """ # If we're executing, the input buffer may not even exist anymore due to # the limit imposed by 'buffer_size'. Therefore, we store it. if self._executing: return self._executing_input_buffer cursor = self._get_end_cursor() cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.MoveMode.KeepAnchor) input_buffer = cursor.selection().toPlainText() # Strip out continuation prompts. return input_buffer.replace("\n" + self._continuation_prompt, "\n") def _set_input_buffer(self, string): """ Replaces the text in the input buffer with 'string'. """ # For now, it is an error to modify the input buffer during execution. if self._executing: raise RuntimeError("Cannot change input buffer during execution.") # Remove old text. cursor = self._get_end_cursor() cursor.beginEditBlock() cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.MoveMode.KeepAnchor) cursor.removeSelectedText() # Insert new text with continuation prompts. self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string) cursor.endEditBlock() self._control.moveCursor(QtGui.QTextCursor.MoveOperation.End) input_buffer = property(_get_input_buffer, _set_input_buffer) def _get_font(self): """ The base font being used by the ConsoleWidget. """ return self._control.document().defaultFont() def _set_font(self, font): """ Sets the base font for the ConsoleWidget to the specified QFont. """ font_metrics = QtGui.QFontMetrics(font) # 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 = font_metrics.horizontalAdvance(" ") else: width = font_metrics.width(" ") self._control.setTabStopDistance(self.tab_width * width) self._control.document().setDefaultFont(font) if self._page_control: self._page_control.document().setDefaultFont(font) self.font_changed.emit(font) font = property(_get_font, _set_font) def paste(self, mode=QtGui.QClipboard.Mode.Clipboard): """ Paste the contents of the clipboard into the input region. Parameters: ----------- mode : QClipboard::Mode, optional [default QClipboard::Clipboard] Controls which part of the system clipboard is used. This can be used to access the selection clipboard in X11 and the Find buffer in Mac OS. By default, the regular clipboard is used. """ if self._control.textInteractionFlags() & QtCore.Qt.TextInteractionFlag.TextEditable: # Make sure the paste is safe. self._keep_cursor_in_buffer() cursor = self._control.textCursor() # Remove any trailing newline, which confuses the GUI and forces the # user to backspace. text = QtGui.QApplication.clipboard().text(mode).rstrip() self._insert_plain_text_into_buffer(cursor, dedent(text)) def print_(self, printer=None): """ Print the contents of the ConsoleWidget to the specified QPrinter. """ if not printer: printer = QtGui.QPrinter() if QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.DialogCode.Accepted: return self._control.print_(printer) def export(self, parent=None): """Export HTML/XML in various modes from one Dialog.""" parent = parent or None # sometimes parent is False dialog = QtGui.QFileDialog(parent, "Save Console as...") dialog.setAcceptMode(QtGui.QFileDialog.AcceptMode.AcceptSave) filters = [ "HTML with PNG figures (*.html *.htm)", "XHTML with inline SVG figures (*.xhtml *.xml)", ] dialog.setNameFilters(filters) if self._filename: dialog.selectFile(self._filename) root, ext = os.path.splitext(self._filename) if ext.lower() in (".xml", ".xhtml"): dialog.selectNameFilter(filters[-1]) if dialog.exec_(): filename = str(dialog.selectedFiles()[0]) self._filename = filename choice = str(dialog.selectedNameFilter()) if choice.startswith("XHTML"): exporter = self.export_xhtml else: exporter = self.export_html try: return exporter(filename) except Exception as e: title = self.window().windowTitle() msg = "Error while saving to: %s\n" % filename + str(e) QtGui.QMessageBox.warning( self, title, msg, QtGui.QMessageBox.StandardButton.Ok, QtGui.QMessageBox.StandardButton.Ok, ) return None def export_html(self, filename): """ Export the contents of the ConsoleWidget as HTML. Parameters: ----------- filename : str The file to be saved. inline : bool, optional [default True] If True, include images as inline PNGs. Otherwise, include them as links to external PNG files, mimicking web browsers' "Web Page, Complete" behavior. """ # N.B. this is overly restrictive, but Qt's output is # predictable... img_re = re.compile(r'') html = self.fix_html_encoding(str(self._control.toHtml().toUtf8())) if self._png_mode: # preference saved, don't ask again if img_re.search(html): inline = self._png_mode == "inline" else: inline = True elif img_re.search(html): # there are images widget = QtGui.QWidget() layout = QtGui.QVBoxLayout(widget) title = self.window().windowTitle() msg = "Exporting HTML with PNGs" info = ( "Would you like inline PNGs (single large html file) or " + "external image files?" ) checkbox = QtGui.QCheckBox("&Don't ask again") checkbox.setShortcut("D") ib = QtGui.QPushButton("&Inline", self) ib.setShortcut("I") eb = QtGui.QPushButton("&External", self) eb.setShortcut("E") box = QtGui.QMessageBox(QtGui.QMessageBox.Icon.Question, title, msg) box.setInformativeText(info) box.addButton(ib, QtGui.QMessageBox.ButtonRole.NoRole) box.addButton(eb, QtGui.QMessageBox.ButtonRole.YesRole) box.setDefaultButton(ib) layout.setSpacing(0) layout.addWidget(box) layout.addWidget(checkbox) widget.setLayout(layout) widget.show() reply = box.exec_() inline = reply == 0 if checkbox.checkState(): # don't ask anymore, always use this choice if inline: self._png_mode = "inline" else: self._png_mode = "external" else: # no images inline = True if inline: path = None else: root, ext = os.path.splitext(filename) path = root + "_files" if os.path.isfile(path): raise OSError("%s exists, but is not a directory." % path) f = open(filename, "w") try: f.write( img_re.sub( lambda x: self.image_tag(x, path=path, format="png"), html ) ) except Exception as e: f.close() raise e else: f.close() return filename def export_xhtml(self, filename): """ Export the contents of the ConsoleWidget as XHTML with inline SVGs. """ f = open(filename, "w") try: # N.B. this is overly restrictive, but Qt's output is # predictable... img_re = re.compile(r'') html = str(self._control.toHtml().toUtf8()) # Hack to make xhtml header -- note that we are not doing # any check for valid xml offset = html.find("") assert offset > -1 html = ( '\n' + html[offset + 6:] ) # And now declare UTF-8 encoding html = self.fix_html_encoding(html) f.write( img_re.sub( lambda x: self.image_tag(x, path=None, format="svg"), html ) ) except Exception as e: f.close() raise e else: f.close() return filename def fix_html_encoding(self, html): """ Return html string, with a UTF-8 declaration added to . Assumes that html is Qt generated and has already been UTF-8 encoded and coerced to a python string. If the expected head element is not found, the given object is returned unmodified. This patching is needed for proper rendering of some characters (e.g., indented commands) when viewing exported HTML on a local system (i.e., without seeing an encoding declaration in an HTTP header). C.f. http://www.w3.org/International/O-charset for details. """ offset = html.find("") if offset > -1: html = ( html[: offset + 6] + '\n\n' + html[offset + 6:] ) return html def image_tag(self, match, path=None, format="png"): """ Return (X)HTML mark-up for the image-tag given by match. Parameters ---------- match : re.SRE_Match A match to an HTML image tag as exported by Qt, with match.group("Name") containing the matched image ID. path : string|None, optional [default None] If not None, specifies a path to which supporting files may be written (e.g., for linked images). If None, all images are to be included inline. format : "png"|"svg", optional [default "png"] Format for returned or referenced images. Subclasses supporting image display should override this method. """ # Default case -- not enough information to generate tag return "" def prompt_to_top(self): """ Moves the prompt to the top of the viewport. """ if not self._executing: prompt_cursor = self._get_prompt_cursor() if self._get_cursor().blockNumber() < prompt_cursor.blockNumber(): self._set_cursor(prompt_cursor) self._set_top_cursor(prompt_cursor) def redo(self): """ Redo the last operation. If there is no operation to redo, nothing happens. """ self._control.redo() def reset_font(self): """ Sets the font to the default fixed-width font for this platform. """ if sys.platform == "win32": # Consolas ships with Vista/Win7, fallback to Courier if needed family, fallback = "Consolas", "Courier" elif sys.platform == "darwin": # OSX always has Monaco, no need for a fallback family, fallback = "Monaco", None else: # FIXME: remove Consolas as a default on Linux once our font # selections are configurable by the user. family, fallback = "Consolas", "Monospace" # Check whether we got what we wanted using QFontInfo, since # exactMatch() is overly strict and returns false in too many cases. font = QtGui.QFont(family) font_info = QtGui.QFontInfo(font) if fallback is not None and font_info.family() != family: font = QtGui.QFont(fallback) font.setPointSize(QtGui.QApplication.font().pointSize()) font.setStyleHint(QtGui.QFont.TypeWriter) self._set_font(font) def change_font_size(self, delta): """Change the font size by the specified amount (in points). """ font = self.font font.setPointSize(font.pointSize() + delta) self._set_font(font) def select_all(self): """ Selects all the text in the buffer. """ self._control.selectAll() def _get_tab_width(self): """ The width (in terms of space characters) for tab characters. """ return self._tab_width def _set_tab_width(self, tab_width): """ Sets the width (in terms of space characters) for tab characters. """ font_metrics = QtGui.QFontMetrics(self.font) # 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 = font_metrics.horizontalAdvance(" ") else: width = font_metrics.width(" ") self._control.setTabStopDistance(tab_width * width) self._tab_width = tab_width tab_width = property(_get_tab_width, _set_tab_width) def undo(self): """ Undo the last operation. If there is no operation to undo, nothing happens. """ self._control.undo() # --------------------------------------------------------------------------- # 'ConsoleWidget' abstract interface # --------------------------------------------------------------------------- def _is_complete(self, source, interactive): """ Returns whether 'source' can be executed. When triggered by an Enter/Return key press, 'interactive' is True; otherwise, it is False. """ raise NotImplementedError() def _execute(self, source, hidden): """ Execute 'source'. If 'hidden', do not show any output. """ raise NotImplementedError() def _prompt_started_hook(self): """ Called immediately after a new prompt is displayed. """ pass def _prompt_finished_hook(self): """ Called immediately after a prompt is finished, i.e. when some input will be processed and a new prompt displayed. """ pass def _up_pressed(self): """ Called when the up key is pressed. Returns whether to continue processing the event. """ return True def _down_pressed(self): """ Called when the down key is pressed. Returns whether to continue processing the event. """ return True def _tab_pressed(self): """ Called when the tab key is pressed. Returns whether to continue processing the event. """ return False # -------------------------------------------------------------------------- # 'ConsoleWidget' protected interface # -------------------------------------------------------------------------- def _append_html(self, html): """ Appends html at the end of the console buffer. """ cursor = self._get_end_cursor() self._insert_html(cursor, html) def _append_html_fetching_plain_text(self, html): """ Appends 'html', then returns the plain text version of it. """ cursor = self._get_end_cursor() return self._insert_html_fetching_plain_text(cursor, html) def _append_plain_text(self, text): """ Appends plain text at the end of the console buffer, processing ANSI codes if enabled. """ cursor = self._get_end_cursor() self._insert_plain_text(cursor, text) def _append_plain_text_keeping_prompt(self, text): """ Writes 'text' after the current prompt, then restores the old prompt with its old input buffer. """ input_buffer = self.input_buffer self._append_plain_text("\n") self._prompt_finished() self._append_plain_text(text) self._show_prompt() self.input_buffer = input_buffer def _cancel_text_completion(self): """ If text completion is progress, cancel it. """ if self._text_completing_pos: self._clear_temporary_buffer() self._text_completing_pos = 0 def _clear_temporary_buffer(self): """ Clears the "temporary text" buffer, i.e. all the text following the prompt region. """ # Select and remove all text below the input buffer. cursor = self._get_prompt_cursor() prompt = self._continuation_prompt.lstrip() while cursor.movePosition(QtGui.QTextCursor.MoveOperation.NextBlock): temp_cursor = QtGui.QTextCursor(cursor) temp_cursor.select(QtGui.QTextCursor.SelectionType.BlockUnderCursor) text = temp_cursor.selection().toPlainText().lstrip() if not text.startswith(prompt): break else: # We've reached the end of the input buffer and no text follows. return cursor.movePosition(QtGui.QTextCursor.MoveOperation.Left) # Grab the newline. cursor.movePosition( QtGui.QTextCursor.MoveOperation.End, QtGui.QTextCursor.MoveMode.KeepAnchor ) cursor.removeSelectedText() # After doing this, we have no choice but to clear the undo/redo # history. Otherwise, the text is not "temporary" at all, because it # can be recalled with undo/redo. Unfortunately, Qt does not expose # fine-grained control to the undo/redo system. if self._control.isUndoRedoEnabled(): self._control.setUndoRedoEnabled(False) self._control.setUndoRedoEnabled(True) def _complete_with_items(self, cursor, items): """ Performs completion with 'items' at the specified cursor location. """ self._cancel_text_completion() if len(items) == 1: cursor.setPosition( self._control.textCursor().position(), QtGui.QTextCursor.MoveMode.KeepAnchor, ) cursor.insertText(items[0]) elif len(items) > 1: current_pos = self._control.textCursor().position() prefix = commonprefix(items) if prefix: cursor.setPosition(current_pos, QtGui.QTextCursor.MoveMode.KeepAnchor) cursor.insertText(prefix) current_pos = cursor.position() cursor.beginEditBlock() self._append_plain_text("\n") self._page(self._format_as_columns(items)) cursor.endEditBlock() cursor.setPosition(current_pos) self._control.moveCursor(QtGui.QTextCursor.MoveOperation.End) self._control.setTextCursor(cursor) self._text_completing_pos = current_pos def _context_menu_make(self, pos): """ Creates a context menu for the given QPoint (in widget coordinates). """ menu = QtGui.QMenu(self) cut_action = menu.addAction("Cut", self.cut) cut_action.setEnabled(self.can_cut()) cut_action.setShortcut(QtGui.QKeySequence.StandardKey.Cut) copy_action = menu.addAction("Copy", self.copy) copy_action.setEnabled(self.can_copy()) copy_action.setShortcut(QtGui.QKeySequence.StandardKey.Copy) paste_action = menu.addAction("Paste", self.paste) paste_action.setEnabled(self.can_paste()) paste_action.setShortcut(QtGui.QKeySequence.StandardKey.Paste) menu.addSeparator() menu.addAction(self._select_all_action) menu.addSeparator() menu.addAction(self._export_action) menu.addAction(self._print_action) return menu def _control_key_down(self, modifiers, include_command=False): """ Given a KeyboardModifiers flags object, return whether the Control key is down. Parameters: ----------- include_command : bool, optional (default True) Whether to treat the Command key as a (mutually exclusive) synonym for Control when in Mac OS. """ # Note that on Mac OS, ControlModifier corresponds to the Command key # while MetaModifier corresponds to the Control key. if sys.platform == "darwin": down = include_command and (modifiers & QtCore.Qt.KeyboardModifier.ControlModifier) return bool(down) ^ bool(modifiers & QtCore.Qt.KeyboardModifier.MetaModifier) else: return bool(modifiers & QtCore.Qt.KeyboardModifier.ControlModifier) def _create_control(self): """ Creates and connects the underlying text widget. """ # Create the underlying control. if self.kind == "plain": control = QtGui.QPlainTextEdit() elif self.kind == "rich": control = QtGui.QTextEdit() control.setAcceptRichText(False) # Install event filters. The filter on the viewport is needed for # mouse events and drag events. control.installEventFilter(self) control.viewport().installEventFilter(self) # Connect signals. control.cursorPositionChanged.connect(self._cursor_position_changed) self._connections_to_remove.append( (control.cursorPositionChanged, self._cursor_position_changed) ) control.customContextMenuRequested.connect( self._custom_context_menu_requested ) self._connections_to_remove.append( (control.customContextMenuRequested, self._custom_context_menu_requested) ) control.copyAvailable.connect(self.copy_available) self._connections_to_remove.append( (control.copyAvailable, self.copy_available) ) control.redoAvailable.connect(self.redo_available) self._connections_to_remove.append( (control.redoAvailable, self.redo_available) ) control.undoAvailable.connect(self.undo_available) self._connections_to_remove.append( (control.undoAvailable, self.undo_available) ) # Hijack the document size change signal to prevent Qt from adjusting # the viewport's scrollbar. We are relying on an implementation detail # of Q(Plain)TextEdit here, which is potentially dangerous, but without # this functionality we cannot create a nice terminal interface. layout = control.document().documentLayout() layout.documentSizeChanged.disconnect() layout.documentSizeChanged.connect(self._adjust_scrollbars) # The document layout doesn't stay the same therefore its signal is # not explicitly disconnected on destruction # Configure the control. control.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu) control.setReadOnly(True) control.setUndoRedoEnabled(False) control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOn) return control def _create_page_control(self): """ Creates and connects the underlying paging widget. """ if self.kind == "plain": control = QtGui.QPlainTextEdit() elif self.kind == "rich": control = QtGui.QTextEdit() control.installEventFilter(self) control.setReadOnly(True) control.setUndoRedoEnabled(False) control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOn) return control def _event_filter_console_keypress(self, event): """ Filter key events for the underlying text widget to create a console-like interface. """ intercepted = False cursor = self._control.textCursor() position = cursor.position() key = event.key() ctrl_down = self._control_key_down(event.modifiers()) alt_down = event.modifiers() & QtCore.Qt.KeyboardModifier.AltModifier shift_down = event.modifiers() & QtCore.Qt.KeyboardModifier.ShiftModifier # Special sequences ---------------------------------------------- if event.matches(QtGui.QKeySequence.StandardKey.Copy): self.copy() intercepted = True elif event.matches(QtGui.QKeySequence.StandardKey.Cut): self.cut() intercepted = True elif event.matches(QtGui.QKeySequence.StandardKey.Paste): self.paste() intercepted = True # Special modifier logic ----------------------------------------- elif key in (QtCore.Qt.Key.Key_Return, QtCore.Qt.Key.Key_Enter): intercepted = True # Special handling when tab completing in text mode. self._cancel_text_completion() if self._in_buffer(position): if self._reading: self._append_plain_text("\n") self._reading = False if self._reading_callback: self._reading_callback() # If the input buffer is a single line or there is only # whitespace after the cursor, execute. Otherwise, split the # line with a continuation prompt. elif not self._executing: cursor.movePosition( QtGui.QTextCursor.MoveOperation.End, QtGui.QTextCursor.MoveMode.KeepAnchor ) at_end = len(cursor.selectedText().strip()) == 0 single_line = ( self._get_end_cursor().blockNumber() == self._get_prompt_cursor().blockNumber() ) if (at_end or shift_down or single_line) and not ctrl_down: self.execute(interactive=not shift_down) else: # Do this inside an edit block for clean undo/redo. cursor.beginEditBlock() cursor.setPosition(position) cursor.insertText("\n") self._insert_continuation_prompt(cursor) cursor.endEditBlock() # Ensure that the whole input buffer is visible. # FIXME: This will not be usable if the input buffer is # taller than the console widget. self._control.moveCursor(QtGui.QTextCursor.MoveOperation.End) self._control.setTextCursor(cursor) # Control/Cmd modifier ------------------------------------------- elif ctrl_down: if key == QtCore.Qt.Key.Key_G: self._keyboard_quit() intercepted = True elif key == QtCore.Qt.Key.Key_K: if self._in_buffer(position): cursor.movePosition( QtGui.QTextCursor.MoveOperation.EndOfLine, QtGui.QTextCursor.MoveMode.KeepAnchor, ) if not cursor.hasSelection(): # Line deletion (remove continuation prompt) cursor.movePosition( QtGui.QTextCursor.MoveOperation.NextBlock, QtGui.QTextCursor.MoveMode.KeepAnchor, ) cursor.movePosition( QtGui.QTextCursor.MoveOperation.Right, QtGui.QTextCursor.MoveMode.KeepAnchor, len(self._continuation_prompt), ) cursor.removeSelectedText() intercepted = True elif key == QtCore.Qt.Key.Key_L: self.prompt_to_top() intercepted = True elif key == QtCore.Qt.Key.Key_O: if self._page_control and self._page_control.isVisible(): self._page_control.setFocus() intercepted = True elif key == QtCore.Qt.Key.Key_Y: self.paste() intercepted = True elif key in (QtCore.Qt.Key.Key_Backspace, QtCore.Qt.Key.Key_Delete): intercepted = True elif key == QtCore.Qt.Key.Key_Plus: self.change_font_size(1) intercepted = True elif key == QtCore.Qt.Key.Key_Minus: self.change_font_size(-1) intercepted = True # Alt modifier --------------------------------------------------- elif alt_down: if key == QtCore.Qt.Key.Key_B: self._set_cursor(self._get_word_start_cursor(position)) intercepted = True elif key == QtCore.Qt.Key.Key_F: self._set_cursor(self._get_word_end_cursor(position)) intercepted = True elif key == QtCore.Qt.Key.Key_Backspace: cursor = self._get_word_start_cursor(position) cursor.setPosition(position, QtGui.QTextCursor.MoveMode.KeepAnchor) cursor.removeSelectedText() intercepted = True elif key == QtCore.Qt.Key.Key_D: cursor = self._get_word_end_cursor(position) cursor.setPosition(position, QtGui.QTextCursor.MoveMode.KeepAnchor) cursor.removeSelectedText() intercepted = True elif key == QtCore.Qt.Key.Key_Delete: intercepted = True elif key == QtCore.Qt.Key.Key_Greater: self._control.moveCursor(QtGui.QTextCursor.MoveOperation.End) intercepted = True elif key == QtCore.Qt.Key.Key_Less: self._control.setTextCursor(self._get_prompt_cursor()) intercepted = True # No modifiers --------------------------------------------------- else: if shift_down: anchormode = QtGui.QTextCursor.MoveMode.KeepAnchor else: anchormode = QtGui.QTextCursor.MoveMode.MoveAnchor if key == QtCore.Qt.Key.Key_Escape: self._keyboard_quit() intercepted = True elif key == QtCore.Qt.Key.Key_Up: if self._reading or not self._up_pressed(): intercepted = True else: prompt_line = self._get_prompt_cursor().blockNumber() intercepted = cursor.blockNumber() <= prompt_line elif key == QtCore.Qt.Key.Key_Down: if self._reading or not self._down_pressed(): intercepted = True else: end_line = self._get_end_cursor().blockNumber() intercepted = cursor.blockNumber() == end_line elif key == QtCore.Qt.Key.Key_Tab: if not self._reading: intercepted = not self._tab_pressed() elif key == QtCore.Qt.Key.Key_Left: # Move to the previous line line, col = cursor.blockNumber(), cursor.columnNumber() if line > self._get_prompt_cursor().blockNumber() and col == len( self._continuation_prompt ): self._control.moveCursor( QtGui.QTextCursor.MoveOperation.PreviousBlock, mode=anchormode ) self._control.moveCursor( QtGui.QTextCursor.MoveOperation.EndOfBlock, mode=anchormode ) intercepted = True # Regular left movement else: intercepted = not self._in_buffer(position - 1) elif key == QtCore.Qt.Key.Key_Right: original_block_number = cursor.blockNumber() cursor.movePosition(QtGui.QTextCursor.MoveOperation.Right, mode=anchormode) if cursor.blockNumber() != original_block_number: cursor.movePosition( QtGui.QTextCursor.MoveOperation.Right, n=len(self._continuation_prompt), mode=anchormode, ) self._set_cursor(cursor) intercepted = True elif key == QtCore.Qt.Key.Key_Home: start_line = cursor.blockNumber() if start_line == self._get_prompt_cursor().blockNumber(): start_pos = self._prompt_pos else: cursor.movePosition( QtGui.QTextCursor.MoveOperation.StartOfBlock, QtGui.QTextCursor.MoveMode.KeepAnchor, ) start_pos = cursor.position() start_pos += len(self._continuation_prompt) cursor.setPosition(position) if shift_down and self._in_buffer(position): cursor.setPosition(start_pos, QtGui.QTextCursor.MoveMode.KeepAnchor) else: cursor.setPosition(start_pos) self._set_cursor(cursor) intercepted = True elif key == QtCore.Qt.Key.Key_Backspace: # Line deletion (remove continuation prompt) line, col = cursor.blockNumber(), cursor.columnNumber() if ( not self._reading and col == len(self._continuation_prompt) and line > self._get_prompt_cursor().blockNumber() ): cursor.beginEditBlock() cursor.movePosition( QtGui.QTextCursor.MoveOperation.StartOfBlock, QtGui.QTextCursor.MoveMode.KeepAnchor, ) cursor.removeSelectedText() cursor.deletePreviousChar() cursor.endEditBlock() intercepted = True # Regular backwards deletion else: anchor = cursor.anchor() if anchor == position: intercepted = not self._in_buffer(position - 1) else: intercepted = not self._in_buffer( min(anchor, position) ) elif key == QtCore.Qt.Key.Key_Delete: # Line deletion (remove continuation prompt) if ( not self._reading and self._in_buffer(position) and cursor.atBlockEnd() and not cursor.hasSelection() ): cursor.movePosition( QtGui.QTextCursor.MoveOperation.NextBlock, QtGui.QTextCursor.MoveMode.KeepAnchor, ) cursor.movePosition( QtGui.QTextCursor.MoveOperation.Right, QtGui.QTextCursor.MoveMode.KeepAnchor, len(self._continuation_prompt), ) cursor.removeSelectedText() intercepted = True # Regular forwards deletion: else: anchor = cursor.anchor() intercepted = not self._in_buffer( anchor ) or not self._in_buffer(position) # Don't move the cursor if Control/Cmd is pressed to allow copy-paste # using the keyboard in any part of the buffer. if not self._control_key_down(event.modifiers(), include_command=True): self._keep_cursor_in_buffer() return intercepted def _event_filter_page_keypress(self, event): """ Filter key events for the paging widget to create console-like interface. """ key = event.key() ctrl_down = self._control_key_down(event.modifiers()) alt_down = event.modifiers() & QtCore.Qt.KeyboardModifier.AltModifier if ctrl_down: if key == QtCore.Qt.Key.Key_O: self._control.setFocus() return True elif alt_down: if key == QtCore.Qt.Key.Key_Greater: self._page_control.moveCursor(QtGui.QTextCursor.MoveOperation.End) return True elif key == QtCore.Qt.Key.Key_Less: self._page_control.moveCursor(QtGui.QTextCursor.MoveOperation.Start) return True elif key in (QtCore.Qt.Key.Key_Q, QtCore.Qt.Key.Key_Escape): if self._splitter: self._page_control.hide() else: self.layout().setCurrentWidget(self._control) return True elif key in (QtCore.Qt.Key.Key_Enter, QtCore.Qt.Key.Key_Return): new_event = QtGui.QKeyEvent( QtCore.QEvent.Type.KeyPress, QtCore.Qt.Key.Key_PageDown, QtCore.Qt.KeyboardModifier.NoModifier, ) QtGui.QApplication.sendEvent(self._page_control, new_event) return True elif key == QtCore.Qt.Key.Key_Backspace: new_event = QtGui.QKeyEvent( QtCore.QEvent.Type.KeyPress, QtCore.Qt.Key.Key_PageUp, QtCore.Qt.KeyboardModifier.NoModifier, ) QtGui.QApplication.sendEvent(self._page_control, new_event) return True return False def _format_as_columns(self, items, separator=" "): """ Transform a list of strings into a single string with columns. Parameters ---------- items : sequence of strings The strings to process. separator : str, optional [default is two spaces] The string that separates columns. Returns ------- The formatted string. """ # Note: this code is adapted from columnize 0.3.2. # See http://code.google.com/p/pycolumnize/ # Calculate the number of characters available. width = self._control.viewport().width() char_width = QtGui.QFontMetrics(self.font).width(" ") displaywidth = max(10, (width / char_width) - 1) # Some degenerate cases. size = len(items) if size == 0: return "\n" elif size == 1: return "%s\n" % items[0] # Try every row count from 1 upwards array_index = lambda nrows, row, col: nrows * col + row for nrows in range(1, size): ncols = (size + nrows - 1) // nrows colwidths = [] totwidth = -len(separator) for col in range(ncols): # Get max column width for this column colwidth = 0 for row in range(nrows): i = array_index(nrows, row, col) if i >= size: break x = items[i] colwidth = max(colwidth, len(x)) colwidths.append(colwidth) totwidth += colwidth + len(separator) if totwidth > displaywidth: break if totwidth <= displaywidth: break # The smallest number of rows computed and the max widths for each # column has been obtained. Now we just have to format each of the rows. string = "" for row in range(nrows): texts = [] for col in range(ncols): i = row + nrows * col if i >= size: texts.append("") else: texts.append(items[i]) while texts and not texts[-1]: del texts[-1] for col in range(len(texts)): texts[col] = texts[col].ljust(colwidths[col]) string += "%s\n" % separator.join(texts) return string def _get_block_plain_text(self, block): """ Given a QTextBlock, return its unformatted text. """ cursor = QtGui.QTextCursor(block) cursor.movePosition(QtGui.QTextCursor.MoveOperation.StartOfBlock) cursor.movePosition( QtGui.QTextCursor.MoveOperation.EndOfBlock, QtGui.QTextCursor.MoveMode.KeepAnchor ) return cursor.selection().toPlainText() def _get_cursor(self): """ Convenience method that returns a cursor for the current position. """ return self._control.textCursor() def _get_end_cursor(self): """ Convenience method that returns a cursor for the last character. """ cursor = self._control.textCursor() cursor.movePosition(QtGui.QTextCursor.MoveOperation.End) return cursor def _get_input_buffer_cursor_column(self): """ Returns the column of the cursor in the input buffer, excluding the contribution by the prompt, or -1 if there is no such column. """ prompt = self._get_input_buffer_cursor_prompt() if prompt is None: return -1 else: cursor = self._control.textCursor() return cursor.columnNumber() - len(prompt) def _get_input_buffer_cursor_line(self): """ Returns the text of the line of the input buffer that contains the cursor, or None if there is no such line. """ prompt = self._get_input_buffer_cursor_prompt() if prompt is None: return None else: cursor = self._control.textCursor() text = self._get_block_plain_text(cursor.block()) return text[len(prompt):] def _get_input_buffer_cursor_prompt(self): """ Returns the (plain text) prompt for line of the input buffer that contains the cursor, or None if there is no such line. """ if self._executing: return None cursor = self._control.textCursor() if cursor.position() >= self._prompt_pos: if cursor.blockNumber() == self._get_prompt_cursor().blockNumber(): return self._prompt else: return self._continuation_prompt else: return None def _get_prompt_cursor(self): """ Convenience method that returns a cursor for the prompt position. """ cursor = self._control.textCursor() cursor.setPosition(self._prompt_pos) return cursor def _get_selection_cursor(self, start, end): """ Convenience method that returns a cursor with text selected between the positions 'start' and 'end'. """ cursor = self._control.textCursor() cursor.setPosition(start) cursor.setPosition(end, QtGui.QTextCursor.MoveMode.KeepAnchor) return cursor def _get_word_start_cursor(self, position): """ Find the start of the word to the left the given position. If a sequence of non-word characters precedes the first word, skip over them. (This emulates the behavior of bash, emacs, etc.) """ document = self._control.document() position -= 1 while position >= self._prompt_pos and not is_letter_or_number( document.characterAt(position) ): position -= 1 while position >= self._prompt_pos and is_letter_or_number( document.characterAt(position) ): position -= 1 cursor = self._control.textCursor() cursor.setPosition(position + 1) return cursor def _get_word_end_cursor(self, position): """ Find the end of the word to the right the given position. If a sequence of non-word characters precedes the first word, skip over them. (This emulates the behavior of bash, emacs, etc.) """ document = self._control.document() end = self._get_end_cursor().position() while position < end and not is_letter_or_number( document.characterAt(position) ): position += 1 while position < end and is_letter_or_number( document.characterAt(position) ): position += 1 cursor = self._control.textCursor() cursor.setPosition(position) return cursor def _insert_continuation_prompt(self, cursor): """ Inserts new continuation prompt using the specified cursor. """ if self._continuation_prompt_html is None: self._insert_plain_text(cursor, self._continuation_prompt) else: self._continuation_prompt = self._insert_html_fetching_plain_text( cursor, self._continuation_prompt_html ) def _insert_html(self, cursor, html): """ Inserts HTML using the specified cursor in such a way that future formatting is unaffected. """ cursor.beginEditBlock() cursor.insertHtml(html) # After inserting HTML, the text document "remembers" it's in "html # mode", which means that subsequent calls adding plain text will result # in unwanted formatting, lost tab characters, etc. The following code # hacks around this behavior, which I consider to be a bug in Qt, by # (crudely) resetting the document's style state. cursor.movePosition( QtGui.QTextCursor.MoveOperation.Left, QtGui.QTextCursor.MoveMode.KeepAnchor ) if cursor.selection().toPlainText() == " ": cursor.removeSelectedText() else: cursor.movePosition(QtGui.QTextCursor.MoveOperation.Right) cursor.insertText(" ", QtGui.QTextCharFormat()) cursor.endEditBlock() def _insert_html_fetching_plain_text(self, cursor, html): """ Inserts HTML using the specified cursor, then returns its plain text version. """ cursor.beginEditBlock() cursor.removeSelectedText() start = cursor.position() self._insert_html(cursor, html) end = cursor.position() cursor.setPosition(start, QtGui.QTextCursor.MoveMode.KeepAnchor) text = cursor.selection().toPlainText() cursor.setPosition(end) cursor.endEditBlock() return text def _insert_plain_text(self, cursor, text): """ Inserts plain text using the specified cursor, processing ANSI codes if enabled. """ cursor.insertText(text) def _insert_plain_text_into_buffer(self, cursor, text): """ Inserts text into the input buffer using the specified cursor (which must be in the input buffer), ensuring that continuation prompts are inserted as necessary. """ lines = text.splitlines(True) if lines: cursor.beginEditBlock() cursor.insertText(lines[0]) for line in lines[1:]: if self._continuation_prompt_html is None: cursor.insertText(self._continuation_prompt) else: self._continuation_prompt = self._insert_html_fetching_plain_text( cursor, self._continuation_prompt_html ) cursor.insertText(line) cursor.endEditBlock() def _in_buffer(self, position=None): """ Returns whether the current cursor (or, if specified, a position) is inside the editing region. """ cursor = self._control.textCursor() if position is None: position = cursor.position() else: cursor.setPosition(position) line = cursor.blockNumber() prompt_line = self._get_prompt_cursor().blockNumber() if line == prompt_line: return position >= self._prompt_pos elif line > prompt_line: cursor.movePosition(QtGui.QTextCursor.MoveOperation.StartOfBlock) prompt_pos = cursor.position() + len(self._continuation_prompt) return position >= prompt_pos return False def _keep_cursor_in_buffer(self): """ Ensures that the cursor is inside the editing region. Returns whether the cursor was moved. """ moved = not self._in_buffer() if moved: cursor = self._control.textCursor() cursor.movePosition(QtGui.QTextCursor.MoveOperation.End) self._control.setTextCursor(cursor) return moved def _keyboard_quit(self): """ Cancels the current editing task ala Ctrl-G in Emacs. """ if self._text_completing_pos: self._cancel_text_completion() else: self.input_buffer = "" def _page(self, text, html=False): """ Displays text using the pager if it exceeds the height of the viewport. Parameters: ----------- html : bool, optional (default False) If set, the text will be interpreted as HTML instead of plain text. """ line_height = QtGui.QFontMetrics(self.font).height() minlines = self._control.viewport().height() / line_height if self.paging != "none" and re.match( "(?:[^\n]*\n){%i}" % minlines, text ): if self.paging == "custom": self.custom_page_requested.emit(text) else: self._page_control.clear() cursor = self._page_control.textCursor() if html: self._insert_html(cursor, text) else: self._insert_plain_text(cursor, text) self._page_control.moveCursor(QtGui.QTextCursor.MoveOperation.Start) self._page_control.viewport().resize(self._control.size()) if self._splitter: self._page_control.show() self._page_control.setFocus() else: self.layout().setCurrentWidget(self._page_control) elif html: self._append_plain_html(text) else: self._append_plain_text(text) def _prompt_finished(self): """ Called immediately after a prompt is finished, i.e. when some input will be processed and a new prompt displayed. """ self._control.setReadOnly(True) self._prompt_finished_hook() def _prompt_started(self): """ Called immediately after a new prompt is displayed. """ # Temporarily disable the maximum block count to permit undo/redo and # to ensure that the prompt position does not change due to truncation. self._control.document().setMaximumBlockCount(0) self._control.setUndoRedoEnabled(True) self._control.setReadOnly(False) self._control.moveCursor(QtGui.QTextCursor.MoveOperation.End) self._executing = False self._prompt_started_hook() def _readline(self, prompt="", callback=None): """ Reads one line of input from the user. Parameters ---------- prompt : str, optional The prompt to print before reading the line. callback : callable, optional A callback to execute with the read line. If not specified, input is read *synchronously* and this method does not return until it has been read. Returns ------- If a callback is specified, returns nothing. Otherwise, returns the input string with the trailing newline stripped. """ if self._reading: raise RuntimeError( "Cannot read a line. Widget is already reading." ) if not callback and not self.isVisible(): # If the user cannot see the widget, this function cannot return. raise RuntimeError( "Cannot synchronously read a line if the widget " "is not visible!" ) self._reading = True self._show_prompt(prompt, newline=False) if callback is None: self._reading_callback = None while self._reading: QtCore.QCoreApplication.processEvents() return self.input_buffer.rstrip("\n") else: self._reading_callback = lambda: callback( self.input_buffer.rstrip("\n") ) def _set_continuation_prompt(self, prompt, html=False): """ Sets the continuation prompt. Parameters ---------- prompt : str The prompt to show when more input is needed. html : bool, optional (default False) If set, the prompt will be inserted as formatted HTML. Otherwise, the prompt will be treated as plain text, though ANSI color codes will be handled. """ if html: self._continuation_prompt_html = prompt else: self._continuation_prompt = prompt self._continuation_prompt_html = None def _set_cursor(self, cursor): """ Convenience method to set the current cursor. """ self._control.setTextCursor(cursor) def _set_top_cursor(self, cursor): """ Scrolls the viewport so that the specified cursor is at the top. """ scrollbar = self._control.verticalScrollBar() scrollbar.setValue(scrollbar.maximum()) original_cursor = self._control.textCursor() self._control.setTextCursor(cursor) self._control.ensureCursorVisible() self._control.setTextCursor(original_cursor) def _show_prompt(self, prompt=None, html=False, newline=True): """ Writes a new prompt at the end of the buffer. Parameters ---------- prompt : str, optional The prompt to show. If not specified, the previous prompt is used. html : bool, optional (default False) Only relevant when a prompt is specified. If set, the prompt will be inserted as formatted HTML. Otherwise, the prompt will be treated as plain text, though ANSI color codes will be handled. newline : bool, optional (default True) If set, a new line will be written before showing the prompt if there is not already a newline at the end of the buffer. """ # Insert a preliminary newline, if necessary. if newline: cursor = self._get_end_cursor() if cursor.position() > 0: cursor.movePosition( QtGui.QTextCursor.MoveOperation.Left, QtGui.QTextCursor.MoveMode.KeepAnchor ) if cursor.selection().toPlainText() != "\n": self._append_plain_text("\n") # Write the prompt. self._append_plain_text(self._prompt_sep) if prompt is None: if self._prompt_html is None: self._append_plain_text(self._prompt) else: self._append_html(self._prompt_html) else: if html: self._prompt = self._append_html_fetching_plain_text(prompt) self._prompt_html = prompt else: self._append_plain_text(prompt) self._prompt = prompt self._prompt_html = None self._prompt_pos = self._get_end_cursor().position() self._prompt_started() # Signal handlers ---------------------------------------------------- def _adjust_scrollbars(self): """ Expands the vertical scrollbar beyond the range set by Qt. """ # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp # and qtextedit.cpp. document = self._control.document() scrollbar = self._control.verticalScrollBar() viewport_height = self._control.viewport().height() if isinstance(self._control, QtGui.QPlainTextEdit): maximum = max(0, document.lineCount() - 1) step = viewport_height / self._control.fontMetrics().lineSpacing() else: # QTextEdit does not do line-based layout and blocks will not in # general have the same height. Therefore it does not make sense to # attempt to scroll in line height increments. maximum = document.size().height() step = viewport_height diff = maximum - scrollbar.maximum() scrollbar.setRange(0, maximum) scrollbar.setPageStep(int(step)) # Compensate for undesirable scrolling that occurs automatically due to # maximumBlockCount() text truncation. if diff < 0 and document.blockCount() == document.maximumBlockCount(): scrollbar.setValue(scrollbar.value() + diff) def _cursor_position_changed(self): """ Clears the temporary buffer based on the cursor position. """ if self._text_completing_pos: document = self._control.document() if self._text_completing_pos < document.characterCount(): cursor = self._control.textCursor() pos = cursor.position() text_cursor = self._control.textCursor() text_cursor.setPosition(self._text_completing_pos) if ( pos < self._text_completing_pos or cursor.blockNumber() > text_cursor.blockNumber() ): self._clear_temporary_buffer() self._text_completing_pos = 0 else: self._clear_temporary_buffer() self._text_completing_pos = 0 def _custom_context_menu_requested(self, pos): """ Shows a context menu at the given QPoint (in widget coordinates). """ menu = self._context_menu_make(pos) menu.exec_(self._control.mapToGlobal(pos)) pyface-7.4.0/pyface/ui/qt4/console/completion_lexer.py0000644000076500000240000000550314176222673023727 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 pygments.token import Token, is_token_subtype class CompletionLexer(object): """ Uses Pygments and some auxillary information to lex code snippets for symbol contexts. """ # Maps Lexer names to a list of possible name separators separator_map = { "C": [".", "->"], "C++": [".", "->", "::"], "Python": ["."], } def __init__(self, lexer): """ Create a CompletionLexer using the specified Pygments lexer. """ self.lexer = lexer def get_context(self, string): """ Assuming the cursor is at the end of the specified string, get the context (a list of names) for the symbol at cursor position. """ context = [] reversed_tokens = list(self._lexer.get_tokens(string)) reversed_tokens.reverse() # Pygments often tacks on a newline when none is specified in the input. # Remove this newline. if ( reversed_tokens and reversed_tokens[0][1].endswith("\n") and not string.endswith("\n") ): reversed_tokens.pop(0) current_op = "" for token, text in reversed_tokens: if is_token_subtype(token, Token.Name): # Handle a trailing separator, e.g 'foo.bar.' if current_op in self._name_separators: if not context: context.insert(0, "") # Handle non-separator operators and punction. elif current_op: break context.insert(0, text) current_op = "" # Pygments doesn't understand that, e.g., '->' is a single operator # in C++. This is why we have to build up an operator from # potentially several tokens. elif token is Token.Operator or token is Token.Punctuation: current_op = text + current_op # Break on anything that is not a Operator, Punctuation, or Name. else: break return context def get_lexer(self, lexer): return self._lexer def set_lexer(self, lexer, name_separators=None): self._lexer = lexer if name_separators is None: self._name_separators = self.separator_map.get(lexer.name, ["."]) else: self._name_separators = list(name_separators) lexer = property(get_lexer, set_lexer) pyface-7.4.0/pyface/layout_widget.py0000644000076500000240000000103614176222673020445 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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-pane splitter widget. """ from .toolkit import toolkit_object LayoutWidget = toolkit_object("layout_widget:LayoutWidget") pyface-7.4.0/pyface/i_drop_handler.py0000644000076500000240000000314314176222673020537 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 Interface class IDropHandler(Interface): """ Interface for a drop event handler, which provides API to check if the drop can be handled or not, and then handle it if possible. """ def can_handle_drop(self, event, target): """ Whether or not a drag event can be handled This is used to give feedback to the user about whether a drop is possible via the shape of the cursor or similar indicators. Parameters ---------- event : drag event A drag event with information about the object being dragged. target : toolkit widget The widget that would be dropped on. Returns ------- can_drop : bool True if the current drop handler can handle the given drag event occurring on the given target widget. """ def handle_drop(self, event, target): """ Performs drop action when drop event occurs on target widget. Parameters ---------- event : drop event A drop event with information about the object being dropped. target : toolkit widget The widget that would be dropped on """ pyface-7.4.0/pyface/mdi_application_window.py0000644000076500000240000000117414176222673022313 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # Note: The MDIApplicationWindow is currently wx-specific # Import the toolkit specific version. from .toolkit import toolkit_object MDIApplicationWindow = toolkit_object( "mdi_application_window:MDIApplicationWindow" ) pyface-7.4.0/pyface/image_widget.py0000644000076500000240000000112114176222673020205 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # Note: The ImageWidget is currently wx-specific # Import the toolkit specific version. from .toolkit import toolkit_object ImageWidget = toolkit_object("image_widget:ImageWidget") pyface-7.4.0/pyface/expandable_panel.py0000644000076500000240000000114114176222673021044 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # Note: The ExpandablePanel is currently wx-specific # Import the toolkit specific version. from .toolkit import toolkit_object ExpandablePanel = toolkit_object("expandable_panel:ExpandablePanel") pyface-7.4.0/pyface/tasks/0000755000076500000240000000000014176460550016336 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/tasks/i_advanced_editor_area_pane.py0000644000076500000240000000364514176222673024340 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.tasks.i_editor_area_pane import IEditorAreaPane class IAdvancedEditorAreaPane(IEditorAreaPane): """ A splitable central pane that contains tabbed editors. """ # ------------------------------------------------------------------------ # 'IAdvancedEditorAreaPane' interface. # ------------------------------------------------------------------------ def get_layout(self): """ Returns a LayoutItem that reflects the current state of the editors. Because editors do not have IDs, they are identified by their index in the list of editors. For example, PaneItem(0) refers to the first editor. If there are no open editors, returns None. """ def set_layout(self, layout): """ Applies a LayoutItem to the editors in the pane. The layout should have panes with IDs as described in ``get_layout()``. For example, if one wanted to open two editors side by side, with the first to the left of the right, something like this would appropriate:: editor_area.edit(File('foo.py')) editor_area.edit(File('bar.py')) editor_area.set_layout(VSplitter(PaneItem(0), PaneItem(1))) Editors that are not included in the layout will be tabbified with other editors in an undefined manner. If the layout is None, this method is a no-op. Hence it is always safe to call this method with the results of ``get_layout()``. """ pyface-7.4.0/pyface/tasks/topological_sort.py0000644000076500000240000000120214176222673022270 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # Deprecated module - will be removed in a future Pyface release. # use pyface.action.schema._topological_sort instead from pyface.action.schema._topological_sort import ( # noqa: F401 before_after_sort, topological_sort, ) pyface-7.4.0/pyface/tasks/task.py0000644000076500000240000000651314176222673017661 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 StatusBarManager from traits.api import Callable, HasTraits, Instance, List, Str from .action.schema import MenuBarSchema, ToolBarSchema from .action.schema_addition import SchemaAddition from pyface.tasks.task_layout import TaskLayout class Task(HasTraits): """ A collection of pane, menu, tool bar, and status bar factories. The central class in the Tasks plugin, a Task is responsible for describing a set of user interface elements, as well as mediating between its view (a TaskWindow) and an application-specific model. """ #: The task's identifier. id = Str() #: The task's user-visible name. name = Str() #: The default layout to use for the task. If not overridden, only the #: central pane is displayed. default_layout = Instance(TaskLayout, ()) #: A list of extra IDockPane factories for the task. These dock panes are #: used in conjunction with the dock panes returned by #: create_dock_panes(). extra_dock_pane_factories = List(Callable) #: The window to which the task is attached. Set by the framework. window = Instance("pyface.tasks.task_window.TaskWindow") # Actions -------------------------------------------------------------# #: The menu bar for the task. menu_bar = Instance(MenuBarSchema) #: The (optional) status bar for the task. status_bar = Instance(StatusBarManager) #: The list of tool bars for the tasks. tool_bars = List(ToolBarSchema) #: A list of extra actions, groups, and menus that are inserted into menu #: bars and tool bars constructed from the above schemas. extra_actions = List(SchemaAddition) # ------------------------------------------------------------------------ # 'Task' interface. # ------------------------------------------------------------------------ def activated(self): """ Called after the task has been activated in a TaskWindow. """ pass def create_central_pane(self): """ Create and return the central pane, which must implement ITaskPane. """ raise NotImplementedError() def create_dock_panes(self): """ Create and return the task's dock panes (IDockPane instances). This method is called *after* create_central_pane() when the task is added to a TaskWindow. """ return [] def initialized(self): """ Called when the task is about to be activated in a TaskWindow for the first time. Override this method to perform any initialization that requires the Task's panes to be instantiated. Note that this method, when called, is called before activated(). """ pass def prepare_destroy(self): """ Called when the task is about to be removed from its TaskWindow. Override this method to perform any cleanup before the task's controls are destroyed. """ pass pyface-7.4.0/pyface/tasks/traits_editor.py0000644000076500000240000000364314176222673021574 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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, Instance from pyface.tasks.editor import Editor class TraitsEditor(Editor): """ An Editor that displays a Traits UI View. """ # TraitsEditor interface ----------------------------------------------- #: The model object to view. If not specified, the editor is used instead. model = Instance(HasTraits) #: The UI object associated with the Traits view, if it has been #: constructed. ui = Instance("traitsui.ui.UI") # ------------------------------------------------------------------------ # 'HasTraits' interface. # ------------------------------------------------------------------------ def trait_context(self): """ Use the model object for the Traits UI context, if appropriate. """ if self.model: return {"object": self.model, "editor": self} return super().trait_context() # ------------------------------------------------------------------------ # 'IEditor' interface. # ------------------------------------------------------------------------ def create(self, parent): """ Create and set the toolkit-specific contents of the editor. """ self.ui = self.edit_traits(kind="subpanel", parent=parent) self.control = self.ui.control def destroy(self): """ Destroy the toolkit-specific control that represents the editor. """ self.control = None if self.ui is not None: self.ui.dispose() self.ui = None pyface-7.4.0/pyface/tasks/advanced_editor_area_pane.py0000644000076500000240000000113214176222673024015 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ - :attr:`~.AdvancedEditorAreaPane` """ from pyface.toolkit import toolkit_object AdvancedEditorAreaPane = toolkit_object( "tasks.advanced_editor_area_pane:" "AdvancedEditorAreaPane" ) pyface-7.4.0/pyface/tasks/task_window_backend.py0000644000076500000240000000110214176222673022704 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ - :attr:`~.TaskWindowBackend` """ from pyface.toolkit import toolkit_object TaskWindowBackend = toolkit_object( "tasks.task_window_backend:TaskWindowBackend" ) pyface-7.4.0/pyface/tasks/task_layout.py0000644000076500000240000001331414176222673021253 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 io import StringIO import sys from traits.api import ( Enum, HasStrictTraits, Int, Instance, List, Str, Union, ) class LayoutItem(HasStrictTraits): """ The base class for all Task-related layout objects. """ def __repr__(self): return self.pformat() def iterleaves(self): yield self def pargs(self): return [] def pformat(self, indent=0, multiline=False): """ Pretty-format the layout item. Returns a string. """ stream = StringIO() self.pstream(stream, indent, multiline) return stream.getvalue() def pprint(self, indent=0, multiline=False): """ Pretty-prints the layout item. """ self.pstream(sys.stdout, indent, multiline) def pstream(self, stream, indent=0, multiline=False): """ Pretty-formats the layout item to a stream. """ call = self.__class__.__name__ + "(" indent += len(call) stream.write(call) args = [(None, arg) for arg in self.pargs()] traits = [] for name, trait in sorted(self.traits().items()): if not trait.pretty_skip and not trait.transient: value = getattr(self, name) if trait.default != value: traits.append((name, value)) traits.sort() args.extend(traits) for i, (name, value) in enumerate(args): arg_indent = indent if name: arg_indent += len(name) + 1 stream.write(name + "=") if isinstance(value, LayoutItem): value.pstream(stream, arg_indent, multiline) else: stream.write(repr(value)) if i < len(args) - 1: stream.write(",") if multiline: stream.write("\n" + indent * " ") else: stream.write(" ") stream.write(")") class LayoutContainer(LayoutItem): """ The base class for all layout items that contain other layout items. """ items = List(pretty_skip=True) def __init__(self, *items, **traits): # Items may either be specified as a positional arg or a kwarg. if items: if "items" in traits: raise ValueError( "Received 'items' as positional and keyword argument." ) else: traits["items"] = list(items) super().__init__(**traits) def iterleaves(self): for item in self.items: for leaf in item.iterleaves(): yield leaf def pargs(self): return self.items class PaneItem(LayoutItem): """ A pane in a Task layout. """ #: The ID of the item. If the item refers to a TaskPane, this is the ID of #: that TaskPane. id = Union(Str, Int, default_value="", pretty_skip=True) #: The width of the pane in pixels. If not specified, the pane will be #: sized according to its size hint. width = Int(-1) #: The height of the pane in pixels. If not specified, the pane will be #: sized according to its size hint. height = Int(-1) def __init__(self, id="", **traits): super().__init__(**traits) self.id = id def pargs(self): return [self.id] class Tabbed(LayoutContainer): """ A tab area in a Task layout. """ #: A tabbed layout can only contain PaneItems as sub-items. Splitters and #: other Tabbed layouts are not allowed. items = List(PaneItem, pretty_skip=True) #: The ID of the TaskPane which is active in layout. If not specified, the #: first pane is active. active_tab = Union(Str, Int, default_value="") class Splitter(LayoutContainer): """ A split area in a Task layout. """ #: The orientation of the splitter. orientation = Enum("horizontal", "vertical") #: The sub-items of the splitter, which are PaneItems, Tabbed layouts, and #: other Splitters. items = List( Union( Instance(PaneItem), Instance(Tabbed), Instance("pyface.tasks.task_layout.Splitter"), ), pretty_skip=True, ) class HSplitter(Splitter): """ A convenience class for horizontal splitters. """ orientation = Str("horizontal") class VSplitter(Splitter): """ A convenience class for vertical splitters. """ orientation = Str("vertical") class DockLayout(LayoutItem): """ The layout for a main window's dock area. """ # The layouts for the task's dock panes. left = Union(Instance(PaneItem), Instance(Tabbed), Instance(Splitter)) right = Union(Instance(PaneItem), Instance(Tabbed), Instance(Splitter)) top = Union(Instance(PaneItem), Instance(Tabbed), Instance(Splitter)) bottom = Union(Instance(PaneItem), Instance(Tabbed), Instance(Splitter)) #: Assignments of dock areas to the window's corners. By default, the top #: and bottom dock areas extend into both of the top and both of the #: bottom corners, respectively. top_left_corner = Enum("top", "left") top_right_corner = Enum("top", "right") bottom_left_corner = Enum("bottom", "left") bottom_right_corner = Enum("bottom", "right") class TaskLayout(DockLayout): """ The layout for a Task. """ #: The ID of the task for which this is a layout. id = Str() pyface-7.4.0/pyface/tasks/editor_area_pane.py0000644000076500000240000000106014176222673022170 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ - :attr:`~.EditorAreaPane` """ from pyface.toolkit import toolkit_object EditorAreaPane = toolkit_object("tasks.editor_area_pane:EditorAreaPane") pyface-7.4.0/pyface/tasks/split_editor_area_pane.py0000644000076500000240000000131014176222673023401 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ - :attr:`~.SplitEditorAreaPane` - :attr:`~.EditorAreaWidget` """ from pyface.toolkit import toolkit_object SplitEditorAreaPane = toolkit_object( "tasks.split_editor_area_pane:" "SplitEditorAreaPane" ) EditorAreaWidget = toolkit_object( "tasks.split_editor_area_pane:" "EditorAreaWidget" ) pyface-7.4.0/pyface/tasks/i_editor.py0000644000076500000240000000636114176222673020516 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 ( Any, Bool, Event, HasTraits, Interface, Instance, Property, Str, Vetoable, VetoableEvent, cached_property, ) class IEditor(Interface): """ The base interface for all panes (central and dock) in a Task. """ #: The editor's user-visible name. name = Str() #: The tooltip to show for the editor's tab, if any. tooltip = Str() #: The toolkit-specific control that represents the editor. control = Any() #: The object that the editor is editing. obj = Any() #: Has the editor's object been modified but not saved? dirty = Bool() #: The editor area to which the editor belongs. editor_area = Instance("pyface.tasks.i_editor_area_pane.IEditorAreaPane") #: Is the editor active in the editor area? is_active = Bool() #: Does the editor currently have the focus? has_focus = Bool() #: Fired when the editor has been requested to close. closing = VetoableEvent() #: Fired when the editor has been closed. closed = Event() # ------------------------------------------------------------------------ # 'IEditor' interface. # ------------------------------------------------------------------------ def close(self): """ Close the editor. """ def create(self, parent): """ Create and set the toolkit-specific control that represents the editor. """ def destroy(self): """ Destroy the toolkit-specific control that represents the editor. """ class MEditor(HasTraits): """ Mixin containing common code for toolkit-specific implementations. """ # 'IEditor' interface -------------------------------------------------# name = Str() tooltip = Str() control = Any() obj = Any() dirty = Bool(False) editor_area = Instance("pyface.tasks.i_editor_area_pane.IEditorAreaPane") is_active = Property(Bool, observe="editor_area.active_editor") has_focus = Bool(False) closing = VetoableEvent() closed = Event() # ------------------------------------------------------------------------ # 'IEditor' interface. # ------------------------------------------------------------------------ def close(self): """ Close the editor. """ if self.control is not None: self.closing = event = Vetoable() if not event.veto: self.editor_area.remove_editor(self) self.closed = True # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ @cached_property def _get_is_active(self): if self.editor_area is not None: return self.editor_area.active_editor == self return False pyface-7.4.0/pyface/tasks/enaml_editor.py0000644000076500000240000000111014176222673021345 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.tasks.editor import Editor from pyface.tasks.enaml_pane import EnamlPane class EnamlEditor(EnamlPane, Editor): """ Create an Editor for Enaml Components. """ pyface-7.4.0/pyface/tasks/i_task_pane.py0000644000076500000240000000345214176222673021173 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 Any, Bool, HasTraits, Interface, Instance, Str from pyface.tasks.task import Task class ITaskPane(Interface): """ The base interface for all panes (central and dock) in a Task. """ #: The pane's identifier, unique within a Task. id = Str() #: The pane's user-visible name. name = Str() #: The toolkit-specific control that represents the pane. control = Any() #: Does the pane currently have focus? has_focus = Bool() #: The task with which the pane is associated. Set by the framework. task = Instance(Task) # ------------------------------------------------------------------------ # 'ITaskPane' interface. # ------------------------------------------------------------------------ def create(self, parent): """ Create and set the toolkit-specific control that represents the pane. """ def destroy(self): """ Destroy the toolkit-specific control that represents the pane. """ def set_focus(self): """ Gives focus to the control that represents the pane. """ class MTaskPane(HasTraits): """ Mixin containing common code for toolkit-specific implementations. """ # 'ITaskPane' interface ------------------------------------------------ id = Str() name = Str() control = Any() has_focus = Bool(False) task = Instance(Task) pyface-7.4.0/pyface/tasks/task_window.py0000644000076500000240000004070714176222673021253 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 from pyface.action.api import MenuBarManager, StatusBarManager, ToolBarManager from pyface.api import ApplicationWindow from traits.api import ( Bool, Callable, HasStrictTraits, Instance, List, Property, Str, Vetoable, observe, ) from pyface.tasks.action.task_action_manager_builder import ( TaskActionManagerBuilder, ) from pyface.tasks.i_dock_pane import IDockPane from pyface.tasks.i_task_pane import ITaskPane from pyface.tasks.task import Task, TaskLayout from pyface.tasks.task_window_backend import TaskWindowBackend from pyface.tasks.task_window_layout import TaskWindowLayout # Logging. logger = logging.getLogger(__name__) class TaskWindow(ApplicationWindow): """ The the top-level window to which tasks can be assigned. A TaskWindow is responsible for creating and the managing the controls of its tasks. """ # IWindow interface ---------------------------------------------------- #: Unless a title is specifically assigned, delegate to the active task. title = Property(Str, observe=["active_task.name", "_title"]) # TaskWindow interface ------------------------------------------------ #: The pane (central or dock) in the active task that currently has focus. active_pane = Instance(ITaskPane) #: The active task for this window. active_task = Instance(Task) #: The list of all tasks currently attached to this window. All panes of #: the inactive tasks are hidden. tasks = List(Task) #: The central pane of the active task, which is always visible. central_pane = Instance(ITaskPane) #: The list of all dock panes in the active task, which may or may not be #: visible. dock_panes = List(IDockPane) #: The factory for the window's TaskActionManagerBuilder, which is #: instantiated to translate menu and tool bar schemas into Pyface action #: managers. This attribute can overridden to introduce custom logic into #: the translation process, although this is not usually necessary. action_manager_builder_factory = Callable(TaskActionManagerBuilder) # Protected traits ----------------------------------------------------- _active_state = Instance("pyface.tasks.task_window.TaskState") _states = List(Instance("pyface.tasks.task_window.TaskState")) _title = Str() _window_backend = Instance(TaskWindowBackend) # ------------------------------------------------------------------------ # 'Widget' interface. # ------------------------------------------------------------------------ def destroy(self): """ Overridden to ensure that all task panes are cleanly destroyed. """ # Allow the TaskWindowBackend to clean up first. self._window_backend.destroy() # Don't use 'remove_task' here to avoid changing the active state and # thereby removing the window's menus and toolbars. This can lead to # undesirable animations when the window is being closed. for state in self._states: self._destroy_state(state) super().destroy() # ------------------------------------------------------------------------ # 'Window' interface. # ------------------------------------------------------------------------ def open(self): """ Opens the window. Overridden to make the 'opening' event vetoable and to activate a task if one has not already been activated. Returns whether the window was opened. """ self.opening = event = Vetoable() if not event.veto: # Create the control, if necessary. if self.control is None: self._create() # Activate a task, if necessary. if self._active_state is None and self._states: self.activate_task(self._states[0].task) self.show(True) self.opened = self return self.control is not None and not event.veto # ------------------------------------------------------------------------ # Protected 'IApplicationWindow' interface. # ------------------------------------------------------------------------ def _create_contents(self, parent): """ Delegate to the TaskWindowBackend. """ return self._window_backend.create_contents(parent) # ------------------------------------------------------------------------ # 'TaskWindow' interface. # ------------------------------------------------------------------------ def activate_task(self, task): """ Activates a task that has already been added to the window. """ state = self._get_state(task) if state and state != self._active_state: # Hide the panes of the currently active task, if necessary. if self._active_state is not None: self._window_backend.hide_task(self._active_state) # Initialize the new task, if necessary. if not state.initialized: task.initialized() state.initialized = True # Display the panes of the new task. self._window_backend.show_task(state) # Activate the new task. The menus, toolbars, and status bar will be # replaced at this time. self._active_state = state task.activated() elif not state: logger.warning( "Cannot activate task %r: task does not belong to the " "window." % task ) def add_task(self, task): """ Adds a task to the window. The task is not activated. """ if task.window is not None: logger.error( "Cannot add task %r: task has already been added " "to a window!" % task ) return task.window = self state = TaskState(task=task, layout=task.default_layout) self._states.append(state) # Make sure the underlying control has been created, even if it is not # yet visible. if self.control is None: self._create() # Create the central pane. state.central_pane = task.create_central_pane() state.central_pane.task = task state.central_pane.create(self.control) # Create the dock panes. state.dock_panes = task.create_dock_panes() for dock_pane_factory in task.extra_dock_pane_factories: state.dock_panes.append(dock_pane_factory(task=task)) for dock_pane in state.dock_panes: dock_pane.task = task dock_pane.create(self.control) # Build the menu and tool bars. builder = self.action_manager_builder_factory(task=task) state.menu_bar_manager = builder.create_menu_bar_manager() state.status_bar_manager = task.status_bar state.tool_bar_managers = builder.create_tool_bar_managers() def remove_task(self, task): """ Removes a task that has already been added to the window. All the task's panes are destroyed. """ state = self._get_state(task) if state: # If the task is active, make sure it is de-activated before # deleting its controls. if self._active_state == state: self._window_backend.hide_task(state) self._active_state = None self._destroy_state(state) self._states.remove(state) else: logger.warning( "Cannot remove task %r: task does not belong to the " "window." % task ) def focus_next_pane(self): """ Shifts focus to the "next" pane, taking into account the active pane and the pane geometry. """ if self._active_state: panes = self._get_pane_ring() index = 0 if self.active_pane: index = (panes.index(self.active_pane) + 1) % len(panes) panes[index].set_focus() def focus_previous_pane(self): """ Shifts focus to the "previous" pane, taking into account the active pane and the pane geometry. """ if self._active_state: panes = self._get_pane_ring() index = -1 if self.active_pane: index = panes.index(self.active_pane) - 1 panes[index].set_focus() def get_central_pane(self, task): """ Returns the central pane for the specified task. """ state = self._get_state(task) return state.central_pane if state else None def get_dock_pane(self, id, task=None): """ Returns the dock pane in the task with the specified ID, or None if no such dock pane exists. If a task is not specified, the active task is used. """ if task is None: state = self._active_state else: state = self._get_state(task) return state.get_dock_pane(id) if state else None def get_dock_panes(self, task): """ Returns the dock panes for the specified task. """ state = self._get_state(task) return state.dock_panes[:] if state else [] def get_task(self, id): """ Returns the task with the specified ID, or None if no such task exists. """ state = self._get_state(id) return state.task if state else None # Methods for saving and restoring the layout -------------------------# def get_layout(self): """ Returns a TaskLayout (for the active task) that reflects the state of the window. """ if self._active_state: return self._window_backend.get_layout() return None def set_layout(self, layout): """ Applies a TaskLayout (which should be suitable for the active task) to the window. """ if self._active_state: self._window_backend.set_layout(layout) def reset_layout(self): """ Restores the active task's default TaskLayout. """ if self.active_task: self.set_layout(self.active_task.default_layout) def get_window_layout(self): """ Returns a TaskWindowLayout for the current state of the window. """ result = TaskWindowLayout( position=self.position, size=self.size, size_state=self.size_state ) for state in self._states: if state == self._active_state: result.active_task = state.task.id layout = self._window_backend.get_layout() else: layout = state.layout.clone_traits() layout.id = state.task.id result.items.append(layout) return result def set_window_layout(self, window_layout): """ Applies a TaskWindowLayout to the window. """ # Set window size before laying it out. self.position = window_layout.position self.size = window_layout.size self.size_state = window_layout.size_state # Store layouts for the tasks, including the active task. for layout in window_layout.items: if isinstance(layout, str): continue state = self._get_state(layout.id) if state: state.layout = layout else: logger.warning( "Cannot apply layout for task %r: task does not " "belong to the window." % layout.id ) # Attempt to activate the requested task. state = self._get_state(window_layout.get_active_task()) if state: # If the requested active task is already active, calling # ``activate`` is a no-op, so we must force a re-layout. if state == self._active_state: self._window_backend.set_layout(state.layout) else: self.activate_task(state.task) # ------------------------------------------------------------------------ # Protected 'TaskWindow' interface. # ------------------------------------------------------------------------ def _destroy_state(self, state): """ Destroy all controls associated with a Task state. """ # Notify the task that it is about to be destroyed. state.task.prepare_destroy() # Destroy action managers associated with the task, unless the task is # active, in which case this will be handled by our superclass. if state != self._active_state: if state.menu_bar_manager: state.menu_bar_manager.destroy() for tool_bar_manager in state.tool_bar_managers: tool_bar_manager.destroy() # Destroy all controls associated with the task. for dock_pane in state.dock_panes: dock_pane.destroy() state.central_pane.destroy() state.task.window = None def _get_pane_ring(self): """ Returns a list of visible panes ordered for focus switching. """ # Proceed clockwise through the dock areas. # TODO: Also take into account ordering within dock areas. panes = [] if self._active_state: layout = self.get_layout() panes.append(self.central_pane) for area in ("top", "right", "bottom", "left"): item = getattr(layout, area) if item: panes.extend( [ self.get_dock_pane(pane_item.id) for pane_item in item.iterleaves() ] ) return panes def _get_state(self, id_or_task): """ Returns the TaskState that contains the specified Task, or None if no such state exists. """ for state in self._states: if state.task == id_or_task or state.task.id == id_or_task: return state return None # Trait initializers --------------------------------------------------- def __window_backend_default(self): return TaskWindowBackend(window=self) # Trait property getters/setters --------------------------------------- def _get_title(self): if self._title or self.active_task is None: return self._title return self.active_task.name def _set_title(self, title): self._title = title # Trait change handlers ------------------------------------------------ @observe("_active_state") def _update_traits_given_new_active_state(self, event): state = event.new if state is None: self.active_task = self.central_pane = None self.dock_panes = [] self.menu_bar_manager = self.status_bar_manager = None self.tool_bar_managers = [] else: self.active_task = state.task self.central_pane = state.central_pane self.dock_panes = state.dock_panes self.menu_bar_manager = state.menu_bar_manager self.status_bar_manager = state.status_bar_manager self.tool_bar_managers = state.tool_bar_managers @observe("central_pane:has_focus, dock_panes:items:has_focus") def _focus_updated(self, event): if event.new: self.active_pane = event.object @observe("_states.items") def _states_updated(self, event): self.tasks = [state.task for state in self._states] class TaskState(HasStrictTraits): """ An object used internally by TaskWindow to maintain the state associated with an attached Task. """ task = Instance(Task) layout = Instance(TaskLayout) initialized = Bool(False) central_pane = Instance(ITaskPane) dock_panes = List(IDockPane) menu_bar_manager = Instance(MenuBarManager) status_bar_manager = Instance(StatusBarManager) tool_bar_managers = List(ToolBarManager) def get_dock_pane(self, id): """ Returns the dock pane with the specified id, or None if no such dock pane exists. """ for pane in self.dock_panes: if pane.id == id: return pane return None pyface-7.4.0/pyface/tasks/enaml_dock_pane.py0000644000076500000240000000450314176222673022013 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 Instance from pyface.tasks.dock_pane import DockPane class EnamlDockPane(DockPane): """ Create a Dock pane for Enaml Components. """ # ------------------------------------------------------------------------ # 'EnamlDockPane' interface # ------------------------------------------------------------------------ #: The Enaml component defining the contents of the DockPane. component = Instance("enaml.widgets.toolkit_object.ToolkitObject") def create_component(self): """ Return an Enaml component defining the contents of the DockPane. Returns ------- component : ToolkitObject """ raise NotImplementedError() # ------------------------------------------------------------------------ # 'IDockPane' interface. # ------------------------------------------------------------------------ def create_contents(self, parent): """ Return the toolkit-specific control that represents the pane. """ self.component = self.create_component() # Initialize the proxy. self.component.initialize() # Activate the proxy. if not self.component.proxy_is_active: self.component.activate_proxy() # Fish the Qt control out of the proxy. That's our DockPane content. contents = self.component.proxy.widget return contents # ------------------------------------------------------------------------ # 'ITaskPane' interface. # ------------------------------------------------------------------------ def destroy(self): """ Destroy the toolkit-specific control that represents the pane. """ control = self.control if control is not None: control.hide() self.component.destroy() control.setParent(None) control.deleteLater() self.control = None self.component = None pyface-7.4.0/pyface/tasks/tests/0000755000076500000240000000000014176460550017500 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/tasks/tests/test_task_window.py0000644000076500000240000001362614176222673023454 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.api import UnittestTools from pyface.tasks.api import Task from ..task_window import TaskWindow def _task_window_with_named_tasks(*names, **kwargs): tasks = [Task(name=name) for name in names] first_active = kwargs.pop("first_active", False) if first_active: kwargs["active_task"] = tasks[0] task = TaskWindow(tasks=tasks, **kwargs) return task class TestTaskWindow(unittest.TestCase, UnittestTools): def test_title_default(self): task_window = TaskWindow() # default is empty self.assertEqual(task_window.title, "") def test_title_no_active_task(self): task_window = _task_window_with_named_tasks("Test Task", "Test Task 2") # should be empty self.assertEqual(task_window.title, "") def test_title_activate_task(self): task_window = _task_window_with_named_tasks("Test Task") task = task_window.tasks[0] # activate task with self.assertTraitChanges(task_window, "title", count=1): task_window.active_task = task self.assertEqual(task_window.title, "Test Task") def test_title_change_active_task_name(self): task_window = _task_window_with_named_tasks( "Test Task", first_active=True ) task_1 = task_window.tasks[0] # change task name with self.assertTraitChanges(task_window, "title", count=1): task_1.name = "Changed Name" self.assertEqual(task_window.title, "Changed Name") def test_title_change_active_task(self): task_window = _task_window_with_named_tasks( "Test Task 1", "Test Task 2", first_active=True ) task = task_window.tasks[1] # change active task with self.assertTraitChanges(task_window, "title", count=1): task_window.active_task = task self.assertEqual(task_window.title, "Test Task 2") def test_title_change_deactivate_task(self): task_window = _task_window_with_named_tasks( "Test Task 1", first_active=True ) # change active task with self.assertTraitChanges(task_window, "title", count=1): task_window.active_task = None self.assertEqual(task_window.title, "") def test_set_title_no_tasks(self): task_window = _task_window_with_named_tasks() # set window title with self.assertTraitChanges(task_window, "title", count=1): task_window.title = "Window title" self.assertEqual(task_window.title, "Window title") def test_set_title_change_title(self): task_window = _task_window_with_named_tasks(title="Window Title") # set window title with self.assertTraitChanges(task_window, "title", count=1): task_window.title = "New Window title" self.assertEqual(task_window.title, "New Window title") def test_set_title_no_active_task(self): task_window = _task_window_with_named_tasks("Test Task") # set window title with self.assertTraitChanges(task_window, "title", count=1): task_window.title = "Window title" self.assertEqual(task_window.title, "Window title") def test_set_title_active_task(self): task_window = _task_window_with_named_tasks( "Test Task", first_active=True ) # set window title with self.assertTraitChanges(task_window, "title", count=1): task_window.title = "Window title" self.assertEqual(task_window.title, "Window title") def test_set_title_activate_task(self): task_window = _task_window_with_named_tasks( "Test Task", title="Window title" ) task = task_window.tasks[0] # change activate task (trait fires, no window title change) with self.assertTraitChanges(task_window, "title", count=1): task_window.active_task = task self.assertEqual(task_window.title, "Window title") def test_set_title_change_active_task_name(self): task_window = _task_window_with_named_tasks( "Test Task", title="Window title", first_active=True ) task = task_window.tasks[0] # change task name (trait fires, no window title change) with self.assertTraitChanges(task_window, "title", count=1): task.name = "Changed Name" self.assertEqual(task_window.title, "Window title") def test_set_title_change_active_task(self): task_window = _task_window_with_named_tasks( "Test Task", "Test Task 2", title="Window title", active_first=True ) task = task_window.tasks[1] # change task name (trait fires, no window title change) with self.assertTraitChanges(task_window, "title", count=1): task_window.active_task = task self.assertEqual(task_window.title, "Window title") def test_reset_title_active_task(self): task_window = _task_window_with_named_tasks( "Test Task", title="Window title", first_active=True ) # reset window title with self.assertTraitChanges(task_window, "title", count=1): task_window.title = "" self.assertEqual(task_window.title, "Test Task") def test_reset_title(self): task_window = _task_window_with_named_tasks( "Test Task", title="Window title" ) # set window title with self.assertTraitChanges(task_window, "title", count=1): task_window.title = "" self.assertEqual(task_window.title, "") pyface-7.4.0/pyface/tasks/tests/test_tasks_application.py0000644000076500000240000001014214176222673024621 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 Bool, observe from pyface.application_window import ApplicationWindow from pyface.toolkit import toolkit_object from ..tasks_application import TasksApplication GuiTestAssistant = toolkit_object("util.gui_test_assistant:GuiTestAssistant") no_gui_test_assistant = GuiTestAssistant.__name__ == "Unimplemented" EVENTS = [ "starting", "started", "application_initialized", "stopping", "stopped", ] class TestingApp(TasksApplication): #: Whether the app should start cleanly. start_cleanly = Bool(True) #: Whether the app should stop cleanly. stop_cleanly = Bool(True) #: Whether to try invoking exit method. do_exit = Bool(False) #: Whether the exit should be invoked as an error exit. error_exit = Bool(False) #: Whether to try force the exit (ie. ignore vetoes). force_exit = Bool(False) #: Whether to veto a call to the exit method. veto_exit = Bool(False) #: Whether to veto a closing a window. veto_close = Bool(False) #: Whether or not a call to the exit method was vetoed. exit_vetoed = Bool(False) #: Whether exit preparation happened. exit_prepared = Bool(False) #: Whether exit preparation raises an error. exit_prepared_error = Bool(False) def start(self): if not self.start_cleanly: return False super().start() window = self.windows[0] window.observe(self._on_window_closing, "closing") return True def stop(self): super().stop() return self.stop_cleanly def _on_window_closing(self, event): window = event.new if self.veto_close_window and not self.exit_vetoed: window.veto = True self.exit_vetoed = True @observe('exiting') def _set_veto_on_exiting_event(self, event): vetoable_event = event.new vetoable_event.veto = self.veto_exit self.exit_vetoed = self.veto_exit def _prepare_exit(self): if not self.exit_vetoed: self.exit_prepared = True if self.exit_prepared_error: raise Exception("Exit preparation failed") super()._prepare_exit() @unittest.skipIf(no_gui_test_assistant, "No GuiTestAssistant") class TestApplication(unittest.TestCase, GuiTestAssistant): def setUp(self): GuiTestAssistant.setUp(self) self.application_events = [] if toolkit_object.toolkit == "wx": import wx self.event_loop() wx.GetApp().DeletePendingEvents() else: self.event_loop() def tearDown(self): GuiTestAssistant.tearDown(self) def event_listener(self, event): application_event = event.new self.application_events.append(application_event) def connect_listeners(self, app): for event in EVENTS: app.observe(self.event_listener, event) def test_defaults(self): from traits.etsconfig.api import ETSConfig app = TasksApplication() self.assertEqual(app.home, ETSConfig.application_home) self.assertEqual(app.user_data, ETSConfig.user_data) self.assertEqual(app.company, ETSConfig.company) def test_lifecycle(self): app = TasksApplication() self.connect_listeners(app) window = ApplicationWindow() app.observe(lambda _: app.add_window(window), "started") with self.assertMultiTraitChanges([app], EVENTS, []): self.gui.invoke_after(1000, app.exit) result = app.run() self.assertTrue(result) event_order = [event.event_type for event in self.application_events] self.assertEqual(event_order, EVENTS) self.assertEqual(app.windows, []) pyface-7.4.0/pyface/tasks/tests/test_enaml_task_pane.py0000644000076500000240000000467614176222673024251 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.etsconfig.api import ETSConfig # Skip tests if Enaml is not installed or we're using the wx backend. SKIP_REASON = None if ETSConfig.toolkit not in ["", "qt4"]: SKIP_REASON = "Enaml does not support WX" else: try: from enaml.widgets.api import Label from traits_enaml.testing.gui_test_assistant import GuiTestAssistant except ImportError: SKIP_REASON = "traits_enaml is not installed" if SKIP_REASON is not None: # Dummy class so that the TestEnamlTaskPane class definition below # doesn't fail. class GuiTestAssistant(object): # noqa: F811 pass from pyface.tasks.api import EnamlTaskPane class DummyTaskPane(EnamlTaskPane): def create_component(self): return Label(text="test label") @unittest.skipIf(SKIP_REASON is not None, SKIP_REASON) class TestEnamlTaskPane(GuiTestAssistant, unittest.TestCase): # ------------------------------------------------------------------------ # 'TestCase' interface # ------------------------------------------------------------------------ def setUp(self): GuiTestAssistant.setUp(self) self.task_pane = DummyTaskPane() with self.event_loop(): self.task_pane.create(None) def tearDown(self): if self.task_pane.control is not None: with self.delete_widget(self.task_pane.control): self.task_pane.destroy() del self.task_pane GuiTestAssistant.tearDown(self) # ------------------------------------------------------------------------ # Tests # ------------------------------------------------------------------------ def test_creation(self): self.assertIsInstance(self.task_pane.component, Label) self.assertIsNotNone(self.task_pane.control) def test_destroy(self): task_pane = self.task_pane with self.delete_widget(task_pane.control): task_pane.destroy() self.assertIsNone(task_pane.control) # Second destruction is a no-op. task_pane.destroy() pyface-7.4.0/pyface/tasks/tests/__init__.py0000644000076500000240000000000014176222673021601 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/tasks/tests/test_task_action_manager_builder.py0000644000076500000240000003500114176222673026611 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 pyface.action.api import ( Action, ActionItem, ActionManager, Group, MenuManager, MenuBarManager, ) from pyface.action.schema.api import ( GroupSchema, MenuSchema, MenuBarSchema, SchemaAddition, ) from pyface.tasks.action.task_action_manager_builder import ( TaskActionManagerBuilder, ) from pyface.tasks.api import Task class ActionManagerBuilderTestCase(unittest.TestCase): # 'TestCase' protocol -------------------------------------------------# def setUp(self): """ Create some dummy actions to use while testing. """ for i in range(1, 7): action_id = "action%i" % i setattr( self, action_id, Action(id=action_id, name="Action %i" % i) ) # 'ActionManagerBuilderTestCase' protocol -----------------------------# def assertActionElementsEqual(self, first, second): """ Checks that two action managers are (logically) equivalent. """ children1 = children2 = [] self.assertEqual(type(first), type(second)) self.assertEqual(first.id, second.id) if isinstance(first, ActionItem): self.assertEqual(first.action.name, second.action.name) elif isinstance(first, ActionManager): if not isinstance(first, MenuBarManager): self.assertEqual(first.name, second.name) children1, children2 = first.groups, second.groups elif isinstance(first, Group): self.assertEqual(first.separator, second.separator) children1, children2 = first.items, second.items self.assertEqual(len(children1), len(children2)) for i in range(len(children1)): self.assertActionElementsEqual(children1[i], children2[i]) def reset_unique_ids(self): import pyface.util.id_helper as id_helper id_helper.object_counter = id_helper._ObjectCounter() @contextmanager def unique_id_context_manager(self): self.reset_unique_ids() try: yield finally: self.reset_unique_ids() # Tests ---------------------------------------------------------------- def test_simple_menu_bar(self): """ Does constructing a simple menu with no additions work? """ schema = MenuBarSchema( MenuSchema(self.action1, self.action2, id="File", name="&File"), MenuSchema(self.action3, self.action4, id="Edit", name="&Edit"), ) builder = TaskActionManagerBuilder(task=Task(menu_bar=schema)) actual = builder.create_menu_bar_manager() desired = MenuBarManager( MenuManager(self.action1, self.action2, id="File", name="&File"), MenuManager(self.action3, self.action4, id="Edit", name="&Edit"), id="MenuBar", ) self.assertActionElementsEqual(actual, desired) # Tests about schema additions ----------------------------------------- def test_additions_menu_bar(self): """ Does constructing a menu with a few additions work? """ schema = MenuBarSchema( MenuSchema( GroupSchema(self.action1, self.action2, id="FileGroup"), id="File", ) ) extras = [ SchemaAddition( factory=lambda: self.action3, before="action1", path="MenuBar/File/FileGroup", ), SchemaAddition( factory=lambda: self.action4, before="action1", path="MenuBar/File/FileGroup", ), SchemaAddition( factory=lambda: self.action5, path="MenuBar/File/FileGroup" ), ] builder = TaskActionManagerBuilder( task=Task(menu_bar=schema, extra_actions=extras) ) actual = builder.create_menu_bar_manager() desired = MenuBarManager( MenuManager( Group( self.action3, self.action4, self.action1, self.action2, self.action5, id="FileGroup", ), id="File", ), id="MenuBar", ) self.assertActionElementsEqual(actual, desired) def test_extra_menu(self): """ Test contributing a whole new menu to the menu bar. """ # Initial menu. schema = MenuBarSchema( MenuSchema( GroupSchema(self.action1, id="FileGroup"), id="FileMenu" ) ) # Contributed menu. extra_menu = MenuSchema( GroupSchema(self.action2, id="BarGroup"), id="DummyActionsMenu" ) extra_actions = [ SchemaAddition( path="MenuBar", factory=lambda: extra_menu, id="DummyActionsSMenu", ) ] # Build the final menu. builder = TaskActionManagerBuilder( task=Task(menu_bar=schema, extra_actions=extra_actions) ) actual = builder.create_menu_bar_manager() desired = MenuBarManager( MenuManager(Group(self.action1, id="FileGroup"), id="FileMenu"), MenuManager( Group(self.action2, id="BarGroup"), id="DummyActionsMenu" ), id="MenuBar", ) self.assertActionElementsEqual(actual, desired) # Tests about merging schemas -----------------------------------------# def test_merging_redundant_items(self): """ Menus and groups with matching path are merged together. """ # Initial menu. schema = MenuBarSchema( MenuSchema( GroupSchema(self.action1, id="FileGroup"), name="File menu number one", id="FileMenu", ) ) # Contributed menus. extra_menu = MenuSchema( GroupSchema(self.action2, id="FileGroup"), name="File menu number two", id="FileMenu", ) extra_actions = [ SchemaAddition( path="MenuBar", factory=lambda: extra_menu, id="DummyActionsSMenu", ) ] # Build the final menu. builder = TaskActionManagerBuilder( task=Task(menu_bar=schema, extra_actions=extra_actions) ) actual = builder.create_menu_bar_manager() # Note that we expect the name of the menu to be inherited from # the menu in the menu bar schema that is defined first. desired = MenuBarManager( MenuManager( Group(self.action1, self.action2, id="FileGroup"), name="File menu number one", id="FileMenu", ), id="MenuBar", ) self.assertActionElementsEqual(actual, desired) def test_unwanted_merge(self): """ Test that we don't have automatic merges due to forgetting to set a schema ID. """ with self.unique_id_context_manager(): # Initial menu. schema = MenuBarSchema( MenuSchema( GroupSchema(self.action1, id="FileGroup"), name="File 1" ) ) # Contributed menus. extra_menu = MenuSchema( GroupSchema(self.action2, id="FileGroup"), name="File 2" ) extra_actions = [ SchemaAddition( path="MenuBar", factory=lambda: extra_menu, id="DummyActionsSMenu", ) ] # Build the final menu. builder = TaskActionManagerBuilder( task=Task(menu_bar=schema, extra_actions=extra_actions) ) actual = builder.create_menu_bar_manager() # Note that we expect the name of the menu to be inherited from # the menu in the menu bar schema that is defined first. desired = MenuBarManager( MenuManager( Group(self.action1, id="FileGroup"), name="File 1", id="MenuSchema_1", ), MenuManager( Group(self.action2, id="FileGroup"), name="File 2", id="MenuSchema_2", ), id="MenuBar", ) self.assertActionElementsEqual(actual, desired) def test_merging_items_with_same_id_but_different_class(self): """ Schemas with the same path but different types (menus, groups) are not merged together. Having a group and a menu with the same path is of course bad practice, but we need a predictable outcome. """ # Initial menu. schema = MenuBarSchema( MenuSchema( GroupSchema(self.action1, id="FileGroup"), id="FileSchema" ) ) # Contributed menus. extra_group = GroupSchema(self.action2, id="FileSchema") extra_actions = [ SchemaAddition( path="MenuBar", factory=(lambda: extra_group), id="DummyActionsSMenu", ) ] # Build the final menu. builder = TaskActionManagerBuilder( task=Task(menu_bar=schema, extra_actions=extra_actions) ) actual = builder.create_menu_bar_manager() desired = MenuBarManager( MenuManager(Group(self.action1, id="FileGroup"), id="FileSchema"), Group(self.action2, id="FileSchema"), id="MenuBar", ) self.assertActionElementsEqual(actual, desired) def test_merging_redundant_items_that_are_not_schemas(self): """ Items that are not schemas cannot be merged, but we should not crash, either. """ # Initial menu. schema = MenuBarSchema( # This menu is not a schema... MenuManager(Group(self.action1, id="FileGroup"), id="FileMenu") ) # Contributed menus. extra_menu = MenuSchema( GroupSchema(self.action2, id="FileGroup"), id="FileMenu" ) extra_actions = [ SchemaAddition( path="MenuBar", factory=lambda: extra_menu, id="DummyActionsSMenu", ) ] # Build the final menu. builder = TaskActionManagerBuilder( task=Task(menu_bar=schema, extra_actions=extra_actions) ) actual = builder.create_menu_bar_manager() desired = MenuBarManager( MenuManager(Group(self.action1, id="FileGroup"), id="FileMenu"), MenuManager(Group(self.action2, id="FileGroup"), id="FileMenu"), id="MenuBar", ) self.assertActionElementsEqual(actual, desired) # Tests about ordering ------------------------------------------------- def test_absolute_ordering(self): """ Does specifying absolute_position work? """ schema = MenuBarSchema( MenuSchema( GroupSchema(self.action1, self.action2, id="FileGroup"), id="File", ) ) extras = [ SchemaAddition( factory=lambda: self.action3, absolute_position="last", path="MenuBar/File/FileGroup", ), SchemaAddition( factory=lambda: self.action4, absolute_position="first", path="MenuBar/File/FileGroup", ), SchemaAddition( factory=lambda: self.action5, absolute_position="first", path="MenuBar/File/FileGroup", ), ] builder = TaskActionManagerBuilder( task=Task(menu_bar=schema, extra_actions=extras) ) actual = builder.create_menu_bar_manager() desired = MenuBarManager( MenuManager( Group( self.action4, self.action5, self.action1, self.action2, self.action3, id="FileGroup", ), id="File", ), id="MenuBar", ) self.assertActionElementsEqual(actual, desired) def test_absolute_and_before_after(self): """ Does specifying absolute_position along with before, after work? """ schema = MenuBarSchema( MenuSchema( GroupSchema(self.action1, self.action2, id="FileGroup"), id="File", ) ) extras = [ SchemaAddition( factory=lambda: self.action3, id="action3", after="action2", path="MenuBar/File/FileGroup", ), SchemaAddition( factory=lambda: self.action4, after="action3", path="MenuBar/File/FileGroup", ), SchemaAddition( factory=lambda: self.action5, id="action5", absolute_position="last", path="MenuBar/File/FileGroup", ), SchemaAddition( factory=lambda: self.action6, absolute_position="last", before="action5", path="MenuBar/File/FileGroup", ), ] builder = TaskActionManagerBuilder( task=Task(menu_bar=schema, extra_actions=extras) ) actual = builder.create_menu_bar_manager() desired = MenuBarManager( MenuManager( Group( self.action1, self.action2, self.action3, self.action4, self.action6, self.action5, id="FileGroup", ), id="File", ), id="MenuBar", ) self.assertActionElementsEqual(actual, desired) pyface-7.4.0/pyface/tasks/tests/test_task_layout.py0000644000076500000240000000347014176222673023456 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.tasks.api import HSplitter, PaneItem, Tabbed, VSplitter from ..task_layout import LayoutContainer class LayoutItemsTestCase(unittest.TestCase): """ Testing that the layout types play nice with each other. This is a regression test for issue #87 (https://github.com/enthought/pyface/issues/87) """ def setUp(self): self.items = [HSplitter(), PaneItem(), Tabbed(), VSplitter()] def test_hsplitter_items(self): layout = HSplitter(*self.items) self.assertEqual(layout.items, self.items) def test_tabbed_items(self): # Tabbed items only accept PaneItems items = [PaneItem(), PaneItem()] layout = Tabbed(*items) self.assertEqual(layout.items, items) def test_vsplitter_items(self): layout = VSplitter(*self.items) self.assertEqual(layout.items, self.items) def test_layout_container_positional_items(self): items = self.items container = LayoutContainer(*items) self.assertListEqual(items, container.items) def test_layout_container_keyword_items(self): items = self.items container = LayoutContainer(items=items) self.assertListEqual(items, container.items) def test_layout_container_keyword_and_positional_items(self): items = self.items with self.assertRaises(ValueError): LayoutContainer(*items, items=items) pyface-7.4.0/pyface/tasks/tests/test_split_editor_area_pane.py0000644000076500000240000000251214176222673025607 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.tasks.api import SplitEditorAreaPane from pyface.toolkit import toolkit_object GuiTestAssistant = toolkit_object("util.gui_test_assistant:GuiTestAssistant") no_gui_test_assistant = GuiTestAssistant.__name__ == "Unimplemented" @unittest.skipIf(no_gui_test_assistant, "No GuiTestAssistant") class TestSplitEditorAreaPane(unittest.TestCase, GuiTestAssistant): def setUp(self): GuiTestAssistant.setUp(self) self.area_pane = SplitEditorAreaPane() def tearDown(self): if self.area_pane.control is not None: with self.delete_widget(self.area_pane.control): self.area_pane.destroy() GuiTestAssistant.tearDown(self) def test_create_destroy(self): # test that creating and destroying works as expected with self.event_loop(): self.area_pane.create(None) with self.event_loop(): self.area_pane.destroy() pyface-7.4.0/pyface/tasks/tests/test_enaml_editor.py0000644000076500000240000000504614176222673023562 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.etsconfig.api import ETSConfig # Skip tests if Enaml is not installed or we're using the wx backend. SKIP_REASON = None if ETSConfig.toolkit not in ["", "qt4"]: SKIP_REASON = "Enaml does not support WX" else: try: from enaml.widgets.api import Label from traits_enaml.testing.gui_test_assistant import GuiTestAssistant except ImportError: SKIP_REASON = "traits_enaml is not installed" if SKIP_REASON is not None: # Dummy class so that the TestEnamlTaskPane class definition below # doesn't fail. class GuiTestAssistant(object): # noqa: F811 pass from traits.api import Str from pyface.tasks.api import EnamlEditor class DummyStrEditor(EnamlEditor): obj = Str() def create_component(self): return Label(text=self.obj) @unittest.skipIf(SKIP_REASON is not None, SKIP_REASON) class TestEnamlEditor(GuiTestAssistant, unittest.TestCase): # ------------------------------------------------------------------------ # 'TestCase' interface # ------------------------------------------------------------------------ def setUp(self): GuiTestAssistant.setUp(self) self.obj = "test message" self.editor = DummyStrEditor(obj=self.obj) with self.event_loop(): self.editor.create(None) def tearDown(self): if self.editor.control is not None: with self.delete_widget(self.editor.control): self.editor.destroy() del self.editor GuiTestAssistant.tearDown(self) # ------------------------------------------------------------------------ # Tests # ------------------------------------------------------------------------ def test_creation(self): self.assertIsInstance(self.editor.component, Label) self.assertEqual(self.editor.component.text, self.obj) self.assertIsNotNone(self.editor.control) def test_destroy(self): editor = self.editor with self.delete_widget(editor.control): editor.destroy() self.assertIsNone(editor.control) # Second destruction is a no-op. editor.destroy() pyface-7.4.0/pyface/tasks/tests/test_advanced_editor_area_pane.py0000644000076500000240000000357314176222673026231 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.tasks.api import Editor, AdvancedEditorAreaPane from pyface.toolkit import toolkit_object GuiTestAssistant = toolkit_object("util.gui_test_assistant:GuiTestAssistant") no_gui_test_assistant = GuiTestAssistant.__name__ == "Unimplemented" @unittest.skipIf(no_gui_test_assistant, "No GuiTestAssistant") class TestAdvancedEditorAreaPane(unittest.TestCase, GuiTestAssistant): def setUp(self): GuiTestAssistant.setUp(self) self.area_pane = AdvancedEditorAreaPane() def tearDown(self): if self.area_pane.control is not None: with self.delete_widget(self.area_pane.control): self.area_pane.destroy() GuiTestAssistant.tearDown(self) def test_create_destroy(self): # test that creating and destroying works as expected with self.event_loop(): self.area_pane.create(None) with self.event_loop(): self.area_pane.destroy() def test_create_destroy_with_editor(self): # test that creating and destroying works as expected when there are # editors with self.event_loop(): self.area_pane.create(None) with self.event_loop(): editor = self.area_pane.create_editor("Hello", Editor) with self.event_loop(): self.area_pane.add_editor(editor) with self.event_loop(): self.area_pane.activate_editor(editor) with self.event_loop(): self.area_pane.destroy() pyface-7.4.0/pyface/tasks/tests/test_dock_pane_toggle_group.py0000644000076500000240000001045714176222673025622 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.action.schema.api import SMenu, SMenuBar, SGroup from pyface.tasks.action.api import DockPaneToggleGroup from pyface.tasks.api import DockPane, Task, TaskPane, TaskWindow from pyface.gui import GUI from traits.api import List from traits.etsconfig.api import ETSConfig USING_WX = ETSConfig.toolkit not in ["", "qt4"] class BogusTask(Task): id = "tests.bogus_task" name = "Bogus Task" dock_panes = List() def create_central_pane(self): return TaskPane(id="tests.bogus_task.central_pane") def create_dock_panes(self): self.dock_panes = dock_panes = [ DockPane(id="tests.bogus_task.dock_pane_2", name="Dock Pane 2"), DockPane(id="tests.bogus_task.dock_pane_1", name="Dock Pane 1"), ] return dock_panes def _menu_bar_default(self): menu_bar = SMenuBar( SMenu( SGroup( group_factory=DockPaneToggleGroup, id="tests.bogus_task.DockPaneToggleGroup", ), id="View", name="&View", ) ) return menu_bar class DockPaneToggleGroupTestCase(unittest.TestCase): @unittest.skipIf(USING_WX, "TaskWindowBackend is not implemented in WX") def setUp(self): self.gui = GUI() # Set up the bogus task with its window. self.task = BogusTask() self.window = window = TaskWindow() window.add_task(self.task) self.task_state = window._get_state(self.task) # Fish the dock pane toggle group from the menu bar manager. dock_pane_toggle_group = [] def find_doc_pane_toggle(item): if item.id == "tests.bogus_task.DockPaneToggleGroup": dock_pane_toggle_group.append(item) self.task_state.menu_bar_manager.walk(find_doc_pane_toggle) self.dock_pane_toggle_group = dock_pane_toggle_group[0] def tearDown(self): del self.task del self.task_state del self.dock_pane_toggle_group if self.window.control is not None: self.window.destroy() self.gui.process_events() del self.window del self.gui def get_dock_pane_toggle_action_names(self): names = [ action_item.action.name for action_item in self.dock_pane_toggle_group.items ] return names # Tests ---------------------------------------------------------------- def test_group_content_at_startup(self): # Check that there are 2 dock panes in the group at the beginning. self.assertEqual(2, len(self.dock_pane_toggle_group.items)) # Names are sorted by the group. names = self.get_dock_pane_toggle_action_names() expected_names = ["Dock Pane 1", "Dock Pane 2"] self.assertEqual(list(sorted(expected_names)), list(sorted(names))) def test_react_to_dock_pane_added(self): # Add a dock pane to the task. self.task_state.dock_panes.append( DockPane(id="tests.bogus_task.dock_pane_0", name="Dock Pane 0") ) # Check that there are 3 dock panes in the group. self.assertEqual(3, len(self.dock_pane_toggle_group.items)) # Names are sorted by the group. names = self.get_dock_pane_toggle_action_names() expected_names = ["Dock Pane 0", "Dock Pane 1", "Dock Pane 2"] self.assertEqual(list(sorted(expected_names)), list(sorted(names))) def test_react_to_dock_pane_removed(self): # Remove a dock pane from the task. self.task_state.dock_panes.remove(self.task.dock_panes[0]) # Check that there is only 1 dock pane left in the group. self.assertEqual(1, len(self.dock_pane_toggle_group.items)) names = self.get_dock_pane_toggle_action_names() expected_names = ["Dock Pane 1"] self.assertEqual(list(sorted(expected_names)), list(sorted(names))) pyface-7.4.0/pyface/tasks/tests/test_enaml_dock_pane.py0000644000076500000240000000473614176222673024224 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.etsconfig.api import ETSConfig # Skip tests if Enaml is not installed or we're using the wx backend. SKIP_REASON = None if ETSConfig.toolkit not in ["", "qt4"]: SKIP_REASON = "Enaml does not support WX" else: try: from enaml.widgets.api import Label from traits_enaml.testing.gui_test_assistant import GuiTestAssistant except ImportError: SKIP_REASON = "traits_enaml is not installed" if SKIP_REASON is not None: # Dummy class so that the TestEnamlTaskPane class definition below # doesn't fail. class GuiTestAssistant(object): # noqa: F811 pass from pyface.tasks.api import EnamlDockPane, Task class DummyDockPane(EnamlDockPane): def create_component(self): return Label(text="test label") @unittest.skipIf(SKIP_REASON is not None, SKIP_REASON) class TestEnamlDockPane(GuiTestAssistant, unittest.TestCase): # ------------------------------------------------------------------------ # 'TestCase' interface # ------------------------------------------------------------------------ def setUp(self): GuiTestAssistant.setUp(self) self.dock_pane = DummyDockPane(task=Task(id="dummy_task")) with self.event_loop(): self.dock_pane.create(None) def tearDown(self): if self.dock_pane.control is not None: with self.delete_widget(self.dock_pane.control): self.dock_pane.destroy() del self.dock_pane GuiTestAssistant.tearDown(self) # ------------------------------------------------------------------------ # Tests # ------------------------------------------------------------------------ def test_creation(self): self.assertIsInstance(self.dock_pane.component, Label) self.assertIsNotNone(self.dock_pane.control) def test_destroy(self): dock_pane = self.dock_pane with self.delete_widget(dock_pane.control): dock_pane.destroy() self.assertIsNone(dock_pane.control) # Second destruction is a no-op. dock_pane.destroy() pyface-7.4.0/pyface/tasks/tests/test_editor_area_pane.py0000644000076500000240000000441014176222673024373 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.etsconfig.api import ETSConfig from pyface.tasks.api import Editor, EditorAreaPane from pyface.toolkit import toolkit_object GuiTestAssistant = toolkit_object("util.gui_test_assistant:GuiTestAssistant") no_gui_test_assistant = GuiTestAssistant.__name__ == "Unimplemented" USING_WX = ETSConfig.toolkit not in ["", "qt4"] class EditorAreaPaneTestCase(unittest.TestCase): @unittest.skipIf(USING_WX, "EditorAreaPane is not implemented in WX") def test_create_editor(self): """ Does creating an editor work? """ area = EditorAreaPane() area.register_factory(Editor, lambda obj: isinstance(obj, int)) self.assertTrue(isinstance(area.create_editor(0), Editor)) @unittest.skipIf(USING_WX, "EditorAreaPane is not implemented in WX") def test_factories(self): """ Does registering and unregistering factories work? """ area = EditorAreaPane() area.register_factory(Editor, lambda obj: isinstance(obj, int)) self.assertEqual(area.get_factory(0), Editor) self.assertEqual(area.get_factory("foo"), None) area.unregister_factory(Editor) self.assertEqual(area.get_factory(0), None) @unittest.skipIf(no_gui_test_assistant, "No GuiTestAssistant") class TestEditorAreaPane(unittest.TestCase, GuiTestAssistant): def setUp(self): GuiTestAssistant.setUp(self) self.area_pane = EditorAreaPane() def tearDown(self): if self.area_pane.control is not None: with self.delete_widget(self.area_pane.control): self.area_pane.destroy() GuiTestAssistant.tearDown(self) def test_create_destroy(self): # test that creating and destroying works as expected with self.event_loop(): self.area_pane.create(None) with self.event_loop(): self.area_pane.destroy() pyface-7.4.0/pyface/tasks/traits_dock_pane.py0000644000076500000240000000426314176222673022230 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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, Instance from pyface.tasks.dock_pane import DockPane class TraitsDockPane(DockPane): """ A DockPane that displays a Traits UI View. """ # TraitsDockPane interface --------------------------------------------- #: The model object to view. If not specified, the pane is used instead. model = Instance(HasTraits) #: The UI object associated with the Traits view, if it has been #: constructed. ui = Instance("traitsui.ui.UI") # ------------------------------------------------------------------------ # 'HasTraits' interface. # ------------------------------------------------------------------------ def trait_context(self): """ Use the model object for the Traits UI context, if appropriate. """ if self.model: return {"object": self.model, "pane": self} return super().trait_context() # ------------------------------------------------------------------------ # 'ITaskPane' interface. # ------------------------------------------------------------------------ def destroy(self): """ Destroy the toolkit-specific control that represents the pane. """ # Destroy the Traits-generated control inside the dock control. self.ui.dispose() self.ui = None # Destroy the dock control. super().destroy() # ------------------------------------------------------------------------ # 'IDockPane' interface. # ------------------------------------------------------------------------ def create_contents(self, parent): """ Create and return the toolkit-specific contents of the dock pane. """ self.ui = self.edit_traits(kind="subpanel", parent=parent) return self.ui.control pyface-7.4.0/pyface/tasks/__init__.py0000644000076500000240000000000014176222673020437 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/tasks/task_pane.py0000644000076500000240000000102714176222673020657 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ - :attr:`~.TaskPane` """ from pyface.toolkit import toolkit_object TaskPane = toolkit_object("tasks.task_pane:TaskPane") pyface-7.4.0/pyface/tasks/contrib/0000755000076500000240000000000014176460550017776 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/tasks/contrib/python_shell.py0000644000076500000240000000722714176222673023072 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 defining a simple Python shell task. This task provides a view with a simple Python shell. This shouldn't be confused with a more full-featured shell, such as those provided by IPython. """ import logging from traits.api import Str, List, Dict, Instance from pyface.api import PythonShell, FileDialog, OK from pyface.action.schema.api import SMenu, SMenuBar from pyface.tasks.api import Task, TaskPane from pyface.tasks.action.api import TaskAction # set up logging logger = logging.getLogger() class PythonShellPane(TaskPane): """ A Tasks Pane containing a Pyface PythonShell """ id = "pyface.tasks.contrib.python_shell.pane" name = "Python Shell" editor = Instance(PythonShell) bindings = List(Dict) commands = List(Str) def create(self, parent): """ Create the python shell task pane This wraps the standard pyface PythonShell """ logger.debug("PythonShellPane: creating python shell pane") self.editor = PythonShell(parent) self.control = self.editor.control # bind namespace logger.debug("PythonShellPane: binding variables") for binding in self.bindings: for name, value in binding.items(): self.editor.bind(name, value) # execute commands logger.debug("PythonShellPane: executing startup commands") for command in self.commands: self.editor.execute_command(command) logger.debug("PythonShellPane: created") def destroy(self): """ Destroy the python shell task pane """ logger.debug("PythonShellPane: destroying python shell pane") self.editor.destroy() self.control = self.editor = None logger.debug("PythonShellPane: destroyed") class PythonShellTask(Task): """ A task which provides a simple Python Shell to the user. """ # Task Interface id = "pyface.tasks.contrib.python_shell" name = "Python Shell" # The list of bindings for the shell bindings = List(Dict) # The list of commands to run on shell startup commands = List(Str) # the IPythonShell instance that we are interacting with pane = Instance(PythonShellPane) # Task Interface menu_bar = SMenuBar( SMenu( TaskAction(name="Open...", method="open", accelerator="Ctrl+O"), id="File", name="&File", ), SMenu(id="View", name="&View"), ) def create_central_pane(self): """ Create a view pane with a Python shell """ logger.debug("Creating Python shell pane in central pane") self.pane = PythonShellPane( bindings=self.bindings, commands=self.commands ) return self.pane # PythonShellTask API def open(self): """ Shows a dialog to open a file. """ logger.debug("PythonShellTask: opening file") dialog = FileDialog(parent=self.window.control, wildcard="*.py") if dialog.open() == OK: self._open_file(dialog.path) # Private API def _open_file(self, path): """ Execute the selected file in the editor's interpreter """ logger.debug('PythonShellTask: executing file "%s"' % path) self.pane.editor.execute_file(path) pyface-7.4.0/pyface/tasks/contrib/__init__.py0000644000076500000240000000000014176222673022077 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/tasks/contrib/README.txt0000644000076500000240000000034714176222673021502 0ustar cwebsterstaff00000000000000This subpackage is for Tasks components that are generic and re-usable, but not required for the proper functioning of Tasks. In other words, if this subpackage were to be completely removed, there should be no breakage in Tasks. pyface-7.4.0/pyface/tasks/enaml_task_pane.py0000644000076500000240000000112314176222673022030 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.tasks.task_pane import TaskPane from pyface.tasks.enaml_pane import EnamlPane class EnamlTaskPane(EnamlPane, TaskPane): """ Create a Task pane for Enaml Components. """ pyface-7.4.0/pyface/tasks/i_task_window_backend.py0000644000076500000240000000564314176222673023232 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 ( Any, DelegatesTo, HasTraits, Instance, Interface, provides, ) class ITaskWindowBackend(Interface): """ The TaskWindow layout interface. TaskWindow delegates to an ITaskWindowBackend object for toolkit-specific layout functionality. """ #: The root control of the TaskWindow to which the layout belongs. control = Any() #: The TaskWindow to which the layout belongs. window = Instance("pyface.tasks.task_window.TaskWindow") # ------------------------------------------------------------------------ # 'ITaskWindowBackend' interface. # ------------------------------------------------------------------------ def create_contents(self, parent): """ Create and return the TaskWindow's contents. (See IWindow.) """ def destroy(self): """ Destroy the backend. Note that TaskWindow will destroy the widget created in create_contents, but this method may be used to perform additional cleanup. """ def hide_task(self, state): """ Assuming the specified TaskState is active, hide its controls. """ def show_task(self, state): """ Assuming no task is currently active, show the controls of the specified TaskState. """ # Methods for saving and restoring the layout -------------------------# def get_layout(self): """ Returns a TaskLayout for the current state of the window. """ def set_layout(self, layout): """ Applies a TaskLayout (which should be suitable for the active task) to the window. """ @provides(ITaskWindowBackend) class MTaskWindowBackend(HasTraits): """ Mixin containing common coe for toolkit-specific implementations. """ # 'ITaskWindowBackend' interface --------------------------------------- control = DelegatesTo("window") window = Instance("pyface.tasks.task_window.TaskWindow") # ------------------------------------------------------------------------ # 'ITaskWindowBackend' interface. # ------------------------------------------------------------------------ def create_contents(self, parent): raise NotImplementedError() def destroy(self): pass def hide_task(self, state): raise NotImplementedError() def show_task(self, state): raise NotImplementedError() def get_layout(self): raise NotImplementedError() def set_layout(self, layout): raise NotImplementedError() pyface-7.4.0/pyface/tasks/api.py0000644000076500000240000000454314176222673017471 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 ``pyface.tasks`` submodule. Tasks-specific Interfaces ------------------------- - :class:`~.IDockPane` - :class:`~pyface.tasks.i_editor.IEditor` - :class:`~.IEditorAreaPane` - :class:`~.ITaskPane` Tasks, Tasks Application and related classes -------------------------------------------- - :class:`~.AdvancedEditorAreaPane` - :class:`~.DockPane` - :class:`~.Editor` - :class:`~.EditorAreaPane` - :class:`~.SplitEditorAreaPane` - :class:`~.Task` - :class:`~.TasksApplication` - :class:`~.TaskFactory` - :class:`~.TaskPane` - :class:`~.TaskWindow` Tasks layout ------------ - :class:`~.TaskLayout` - :class:`~.TaskWindowLayout` - :class:`~.PaneItem` - :class:`~.Tabbed` - :class:`~.Splitter` - :class:`~.HSplitter` - :class:`~.VSplitter` Traits-specific Tasks classes ----------------------------- - :class:`~.TraitsDockPane` - :class:`~.TraitsEditor` - :class:`~.TraitsTaskPane` Enaml-specific Tasks functionality ---------------------------------- - :class:`~.EnamlDockPane` - :class:`~.EnamlEditor` - :class:`~.EnamlTaskPane` """ from .advanced_editor_area_pane import AdvancedEditorAreaPane from .split_editor_area_pane import SplitEditorAreaPane from .dock_pane import DockPane from .editor import Editor from .editor_area_pane import EditorAreaPane from .enaml_dock_pane import EnamlDockPane from .enaml_editor import EnamlEditor from .enaml_task_pane import EnamlTaskPane from .i_dock_pane import IDockPane from .i_editor import IEditor from .i_editor_area_pane import IEditorAreaPane from .i_task_pane import ITaskPane from .task import Task from .tasks_application import TasksApplication, TaskFactory from .task_layout import ( TaskLayout, PaneItem, Tabbed, Splitter, HSplitter, VSplitter, ) from .task_pane import TaskPane from .task_window import TaskWindow from .task_window_layout import TaskWindowLayout from .traits_dock_pane import TraitsDockPane from .traits_editor import TraitsEditor from .traits_task_pane import TraitsTaskPane pyface-7.4.0/pyface/tasks/enaml_pane.py0000644000076500000240000000534014176222673021013 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Base class defining common code for EnamlTaskPane and EnamlEditor. """ from traits.api import HasTraits, Instance class EnamlPane(HasTraits): """ Base class defining common code for EnamlTaskPane and EnamlEditor. """ # ------------------------------------------------------------------------ # 'EnamlPane' interface # ------------------------------------------------------------------------ #: The Enaml component defining the contents of the TaskPane. component = Instance("enaml.widgets.toolkit_object.ToolkitObject") def create_component(self): """ Return an Enaml component defining the contents of the pane. Returns ------- component : ToolkitObject """ raise NotImplementedError() # ------------------------------------------------------------------------ # 'TaskPane'/'Editor' interface # ------------------------------------------------------------------------ def create(self, parent): """ Create the toolkit-specific control that represents the editor. """ from enaml.widgets.constraints_widget import ProxyConstraintsWidget self.component = self.create_component() # We start with an invisible component to avoid flicker. We restore the # initial state after the Qt control is parented. visible = self.component.visible self.component.visible = False # Initialize the proxy. self.component.initialize() # Activate the proxy. if not self.component.proxy_is_active: self.component.activate_proxy() # Fish the Qt control out of the proxy. That's our TaskPane content. self.control = self.component.proxy.widget # Set the parent if parent is not None: self.control.setParent(parent) # Restore the visibility state self.component.visible = visible if isinstance(self.component, ProxyConstraintsWidget): self.component.proxy.request_relayout() def destroy(self): """ Destroy the toolkit-specific control that represents the editor. """ control = self.control if control is not None: control.hide() self.component.destroy() control.deleteLater() self.control = None self.component = None pyface-7.4.0/pyface/tasks/action/0000755000076500000240000000000014176460550017613 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/tasks/action/task_action.py0000644000076500000240000001136314176222673022472 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 Instance, Property, Str, cached_property from pyface.tasks.api import Editor, Task, TaskPane from pyface.action.listening_action import ListeningAction class TaskAction(ListeningAction): """ An Action that makes a callback to a Task. Note that this is a convenience class. Actions associated with a Task need not inherit TaskAction, although they must, of course, inherit Action. """ # ListeningAction interface -------------------------------------------- object = Property(observe="task") # TaskAction interface ------------------------------------------------- #: The Task with which the action is associated. Set by the framework. task = Instance(Task) # ------------------------------------------------------------------------ # Protected interface. # ------------------------------------------------------------------------ def _get_object(self): return self.task def destroy(self): # Disconnect listeners to task and dependent properties. self.task = None super().destroy() class TaskWindowAction(TaskAction): """ An Action that makes a callback to a Task's window. """ # ListeningAction interface -------------------------------------------- object = Property(observe="task.window") # ------------------------------------------------------------------------ # Protected interface. # ------------------------------------------------------------------------ def _get_object(self): if self.task: return self.task.window return None class CentralPaneAction(TaskAction): """ An Action that makes a callback to a Task's central pane. """ # ListeningAction interface -------------------------------------------- object = Property(observe="central_pane") # CentralPaneAction interface -----------------------------------------# #: The central pane with which the action is associated. central_pane = Property(Instance(TaskPane), observe="task") # ------------------------------------------------------------------------ # Protected interface. # ------------------------------------------------------------------------ @cached_property def _get_central_pane(self): if self.task and self.task.window is not None: return self.task.window.get_central_pane(self.task) return None def _get_object(self): return self.central_pane class DockPaneAction(TaskAction): """ An Action the makes a callback to one of a Task's dock panes. """ # ListeningAction interface -------------------------------------------- object = Property(observe="dock_pane") # DockPaneAction interface --------------------------------------------- #: The dock pane with which the action is associated. Set by the framework. dock_pane = Property(Instance(TaskPane), observe="task") #: The ID of the dock pane with which the action is associated. dock_pane_id = Str() # ------------------------------------------------------------------------ # Protected interface. # ------------------------------------------------------------------------ @cached_property def _get_dock_pane(self): if self.task and self.task.window is not None: return self.task.window.get_dock_pane(self.dock_pane_id, self.task) return None def _get_object(self): return self.dock_pane class EditorAction(CentralPaneAction): """ An action that makes a callback to the active editor in an editor pane. """ # ListeningAction interface -------------------------------------------- object = Property(observe="active_editor") # EditorAction interface ----------------------------------------------- #: The active editor in the central pane with which the action is associated. active_editor = Property( Instance(Editor), observe="central_pane.active_editor" ) # ------------------------------------------------------------------------ # Protected interface. # ------------------------------------------------------------------------ @cached_property def _get_active_editor(self): if self.central_pane is not None: return self.central_pane.active_editor return None def _get_object(self): return self.active_editor pyface-7.4.0/pyface/tasks/action/task_action_manager_builder.py0000644000076500000240000000513114176222673025666 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 Instance, List, Property from pyface.action.schema.action_manager_builder import ActionManagerBuilder from pyface.action.schema.schema_addition import SchemaAddition from pyface.tasks.task import Task class TaskActionManagerBuilder(ActionManagerBuilder): """ ActionManagerBuilder for Tasks. This provides some additional convenience methods for extracting schema information from a task and using it to build menu bar and toolbar managers directly. """ #: The Task to build menubars and toolbars for. task = Instance(Task) #: The schema additions provided by the Task. additions = Property(List(SchemaAddition), observe='task.extra_actions') # ------------------------------------------------------------------------ # 'TaskActionManagerBuilder' interface. # ------------------------------------------------------------------------ def create_menu_bar_manager(self): """ Create a menu bar manager from the task's menu bar schema and additions. """ if self.task.menu_bar: return self.create_action_manager(self.task.menu_bar) return None def create_tool_bar_managers(self): """ Create tool bar managers from the tasks's tool bar schemas and additions. """ schemas = self.task.tool_bars + self.get_additional_toolbar_schemas() return [ self.create_action_manager(schema) for schema in self._get_ordered_schemas(schemas) ] # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ # Trait initializers --------------------------------------------------- def _controller_default(self): from .task_action_controller import TaskActionController return TaskActionController(task=self.task) # Trait properties ----------------------------------------------------- def _get_additions(self): # keep synchronized to task's extra actions, since that may change if self.task: return self.task.extra_actions else: return [] pyface-7.4.0/pyface/tasks/action/task_action_controller.py0000644000076500000240000000354014176222673024733 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 Instance from pyface.tasks.task import Task from pyface.tasks.action.task_action import TaskAction class TaskActionController(ActionController): """ An action controller for menu and tool bars. The controller is used to 'hook' the invocation of every action on the menu and tool bars. This is done so that additional, Task-specific information can be added to action events. Currently, we attach a reference to the Task. """ # TaskActionController interface --------------------------------------- #: The task that this is the controller for. task = Instance(Task) # ------------------------------------------------------------------------ # 'ActionController' interface. # ------------------------------------------------------------------------ def perform(self, action, event): """ Control an action invocation. """ event.task = self.task return action.perform(event) def add_to_menu(self, item): """ Called when an action item is added to a menu/menubar. """ action = item.item.action if isinstance(action, TaskAction): action.task = self.task def add_to_toolbar(self, item): """ Called when an action item is added to a toolbar. """ action = item.item.action if isinstance(action, TaskAction): action.task = self.task pyface-7.4.0/pyface/tasks/action/listening_action.py0000644000076500000240000000115514176222673023522 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 exists purely for backwards compatibility. Please use :class:`pyface.action.listening_action.ListeningAction` instead. """ from pyface.action.listening_action import ListeningAction # noqa: F401 pyface-7.4.0/pyface/tasks/action/schema_addition.py0000644000076500000240000000105114176222673023277 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # Deprecated module - will be removed in a future Pyface release. from pyface.action.schema.schema_addition import SchemaAddition # noqa: F401 pyface-7.4.0/pyface/tasks/action/__init__.py0000644000076500000240000000000014176222673021714 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/tasks/action/dock_pane_toggle_group.py0000644000076500000240000001017014176222673024666 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 Group for toggling the visibility of a task's dock panes. """ from pyface.action.api import Action, ActionItem, Group from traits.api import ( cached_property, Instance, List, observe, Property, Str, ) from pyface.tasks.i_dock_pane import IDockPane class DockPaneToggleAction(Action): """ An Action for toggling the visibility of a dock pane. """ # 'DockPaneToggleAction' interface ------------------------------------- dock_pane = Instance(IDockPane) # 'Action' interface --------------------------------------------------- name = Property(Str, observe="dock_pane.name") style = "toggle" tooltip = Property(Str, observe="name") # ------------------------------------------------------------------------ # 'Action' interface. # ------------------------------------------------------------------------ def destroy(self): super().destroy() # Make sure that we are not listening to changes to the pane anymore. # In traits style, we will set the basic object to None and have the # listener check that if it is still there. self.dock_pane = None def perform(self, event=None): if self.dock_pane: self.dock_pane.visible = not self.dock_pane.visible # ------------------------------------------------------------------------ # Protected interface. # ------------------------------------------------------------------------ def _get_name(self): if self.dock_pane is None: return "UNDEFINED" return self.dock_pane.name def _get_tooltip(self): return "Toggles the visibility of the %s pane." % self.name @observe("dock_pane.visible") def _update_checked(self, event): if self.dock_pane: self.checked = self.dock_pane.visible @observe("dock_pane.closable") def _update_visible(self, event): if self.dock_pane: self.visible = self.dock_pane.closable class DockPaneToggleGroup(Group): """ A Group for toggling the visibility of a task's dock panes. """ # 'Group' interface ---------------------------------------------------- id = "DockPaneToggleGroup" items = List() # 'DockPaneToggleGroup' interface -------------------------------------# task = Property(observe="parent.controller") @cached_property def _get_task(self): manager = self.get_manager() if manager is None or manager.controller is None: return None return manager.controller.task dock_panes = Property(observe="task.window._states.items.dock_panes") @cached_property def _get_dock_panes(self): if self.task is None or self.task.window is None: return [] task_state = self.task.window._get_state(self.task) return task_state.dock_panes def get_manager(self): # FIXME: Is there no better way to get a reference to the menu manager? manager = self while isinstance(manager, Group): manager = manager.parent return manager # Private interface ---------------------------------------------------- @observe("dock_panes.items") def _dock_panes_updated(self, event): """Recreate the group items when dock panes have been added/removed. """ # Remove the previous group items. self.destroy() items = [] for dock_pane in self.dock_panes: action = DockPaneToggleAction(dock_pane=dock_pane) items.append(ActionItem(action=action)) items.sort(key=lambda item: item.action.name) self.items = items # Inform the parent menu manager. manager = self.get_manager() manager.changed = True pyface-7.4.0/pyface/tasks/action/api.py0000644000076500000240000000463514176222673020750 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 ``pyface.tasks.action`` subpackage. Tasks-specific Action Controller -------------------------------- - :class:`~.TaskActionController` Tasks-specific Action Manager factory ------------------------------------- - :class:`~.TaskActionManagerBuilder` Tasks-specific Actions ---------------------- - :class:`~.CentralPaneAction` - :class:`~.DockPaneAction` - :class:`~.EditorAction` - :class:`~.TaskAction` - :class:`~.TaskWindowAction` - :class:`~.TasksApplicationAction` Useful Tasks Actions and Groups ------------------------------- - :class:`~.DockPaneToggleGroup` - :class:`~.TaskToggleGroup` - :class:`~.TaskWindowToggleGroup` - :class:`~.CreateTaskWindowAction` ActionSchemas and aliases ------------------------- Import of these objects from pyface.tasks.actions.api is deprecated and will be removed in a future Pyface release. Instead use pyface.action.schema.api. - :class:`~.ActionSchema` - :class:`~.GroupSchema` - :class:`~.MenuSchema` - :class:`~.MenuBarSchema` - :class:`~.ToolBarSchema` - :attr:`~.SGroup` - :attr:`~.SMenu` - :attr:`~.SMenuBar` - :attr:`~.SToolBar` Schema Addition --------------- Import of this object from pyface.tasks.actions.api is deprecated and will be removed in a future Pyface release. Instead use pyface.action.schema.api. - :class:`~.SchemaAddition` """ from .dock_pane_toggle_group import DockPaneToggleGroup from .schema import ( ActionSchema, GroupSchema, MenuSchema, MenuBarSchema, ToolBarSchema, SGroup, SMenu, SMenuBar, SToolBar, ) from .schema_addition import SchemaAddition from .task_action import ( CentralPaneAction, DockPaneAction, EditorAction, TaskAction, TaskWindowAction, ) from .task_action_controller import TaskActionController from .task_action_manager_builder import TaskActionManagerBuilder from .task_toggle_group import TaskToggleGroup from .task_window_toggle_group import TaskWindowToggleGroup from .tasks_application_action import ( CreateTaskWindowAction, TasksApplicationAction, ) pyface-7.4.0/pyface/tasks/action/tasks_application_action.py0000644000076500000240000000275214176222673025242 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 Instance from pyface.action.api import GUIApplicationAction from pyface.tasks.tasks_application import TasksApplication from pyface.tasks.task_window_layout import TaskWindowLayout class TasksApplicationAction(GUIApplicationAction): #: The Tasks application the action applies to. application = Instance(TasksApplication) class CreateTaskWindowAction(TasksApplicationAction): """ A standard 'New Window' menu action. """ name = "New Window" accelerator = "Ctrl+N" #: The task window wayout to use when creating the new window. layout = Instance("pyface.tasks.task_window_layout.TaskWindowLayout") def perform(self, event=None): window = self.application.create_window(layout=self.layout) self.application.add_window(window) def _layout_default(self): if self.application.default_layout: layout = self.application.default_layout[0] else: layout = TaskWindowLayout() if self.task_factories: layout.items = [self.task_factories[0].id] return layout pyface-7.4.0/pyface/tasks/action/task_toggle_group.py0000644000076500000240000000773114176222673023716 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 Action, ActionItem, Group from traits.api import Any, List, Instance, Property, Str, observe from pyface.tasks.task import Task from pyface.tasks.task_window import TaskWindow class TaskToggleAction(Action): """ An action for activating a task. """ # 'Action' interface --------------------------------------------------- #: The user-visible name of the action, matches the task name. name = Property(Str, observe="task.name") #: The action is a toggle menu item. style = "toggle" #: The tooltip to display for the menu item. tooltip = Property(Str, observe="name") # 'TaskToggleAction' interface ----------------------------------------- #: The Task with which the action is associated. task = Instance(Task) # ------------------------------------------------------------------------ # 'Action' interface. # ------------------------------------------------------------------------ def destroy(self): super().destroy() # Make sure that we are not listening to changes in the task anymore # In traits style, we will set the basic object to None and have the # listener check that if it is still there. self.task = None def perform(self, event=None): window = self.task.window window.activate_task(self.task) # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _get_name(self): if self.task is None: return "UNDEFINED" return self.task.name def _get_tooltip(self): return "Switch to the %s task." % self.name @observe("task.window.active_task") def _update_checked(self, event): if self.task: window = self.task.window self.checked = ( window is not None and window.active_task == self.task ) class TaskToggleGroup(Group): """ A menu for changing the active task in a task window. """ # 'ActionManager' interface -------------------------------------------- id = "TaskToggleGroup" items = List() # 'TaskToggleGroup' interface ------------------------------------------ #: The ActionManager to which the group belongs. manager = Any() #: The window that contains the group. window = Instance(TaskWindow) # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _get_items(self): items = [] if len(self.window.tasks) > 1: # at least two tasks, so something to toggle items = [ ActionItem(action=TaskToggleAction(task=task)) for task in self.window.tasks ] return items def _rebuild(self, event): # Clear out the old group, then build the new one. self.destroy() self.items = self._get_items() # Inform our manager that it needs to be rebuilt. self.manager.changed = True # Trait initializers --------------------------------------------------- def _items_default(self): self.window.observe(self._rebuild, "tasks.items") return self._get_items() def _manager_default(self): manager = self while isinstance(manager, Group): manager = manager.parent return manager def _window_default(self): return self.manager.controller.task.window pyface-7.4.0/pyface/tasks/action/schema.py0000644000076500000240000000127414176222673021433 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # Deprecated module - will be removed in a future Pyface release. from pyface.action.schema.schema import ( # noqa: F401 ActionSchema, GroupSchema, MenuBarSchema, MenuSchema, Schema, SubSchema, SGroup, SMenu, SMenuBar, SToolBar, ToolBarSchema, ) pyface-7.4.0/pyface/tasks/action/task_window_toggle_group.py0000644000076500000240000000771514176222673025307 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 Action, ActionItem, Group from traits.api import Any, Instance, List, Property, Str, observe class TaskWindowToggleAction(Action): """ An action for activating an application window. """ # 'Action' interface ----------------------------------------------------- #: The name of the action for the window. name = Property(Str, observe="window.title") #: The action is a toggle action. style = "toggle" # 'TaskWindowToggleAction' interface ------------------------------------- #: The window to use for this action. window = Instance("pyface.tasks.task_window.TaskWindow") # ------------------------------------------------------------------------- # 'Action' interface. # ------------------------------------------------------------------------- def perform(self, event=None): if self.window: self.window.activate() # ------------------------------------------------------------------------- # Private interface. # ------------------------------------------------------------------------- def _get_name(self): if self.window.title: return self.window.title return "" @observe("window:activated") def _window_activated(self, event): self.checked = True @observe("window:deactivated") def _window_deactivated(self, event): self.checked = False class TaskWindowToggleGroup(Group): """ A Group for toggling the activation state of an application's windows. """ # 'Group' interface ------------------------------------------------------ #: The id of the action group. id = "TaskWindowToggleGroup" #: The actions in the action group items = List() # 'TaskWindowToggleGroup' interface -------------------------------------- #: The application that contains the group. application = Instance("pyface.tasks.tasks_application.TasksApplication") #: The ActionManager to which the group belongs. manager = Any() # ------------------------------------------------------------------------- # 'Group' interface. # ------------------------------------------------------------------------- def destroy(self): """ Called when the group is no longer required. """ super().destroy() if self.application: self.application.observe( self._rebuild, "windows.items", remove=True ) # ------------------------------------------------------------------------- # Private interface. # ------------------------------------------------------------------------- def _get_items(self): items = [] for window in self.application.windows: active = window == self.application.active_window action = TaskWindowToggleAction(window=window, checked=active) items.append(ActionItem(action=action)) return items def _rebuild(self, event): # Clear out the old group, then build the new one. for item in self.items: item.destroy() self.items = self._get_items() # Inform our manager that it needs to be rebuilt. self.manager.changed = True # Trait initializers ----------------------------------------------------- def _items_default(self): self.application.observe(self._rebuild, "windows.items") return self._get_items() def _manager_default(self): manager = self while isinstance(manager, Group): manager = manager.parent return manager pyface-7.4.0/pyface/tasks/tasks_application.py0000644000076500000240000002114114176222673022421 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 a base Task application class to create the event loop, and launch the creation of tasks and corresponding windows. """ import logging from traits.api import ( Callable, HasStrictTraits, List, Instance, Property, Str, cached_property, observe, ) from traits.observation.api import trait from pyface.gui_application import GUIApplication logger = logging.getLogger(__name__) class TaskFactory(HasStrictTraits): """ A factory for creating a Task with some additional metadata. """ #: The task factory's unique identifier. This ID is assigned to all tasks #: created by the factory. id = Str() #: The task factory's user-visible name. name = Str() #: A callable with the following signature: #: #: callable(\**traits) -> Task #: #: Often this attribute will simply be a Task subclass. factory = Callable def create(self, **traits): """ Creates the Task. The default implementation simply calls the 'factory' attribute. """ return self.factory(**traits) class TasksApplication(GUIApplication): """ A base class for Pyface tasks applications. """ # ------------------------------------------------------------------------- # 'TaskApplication' interface # ------------------------------------------------------------------------- # Task management -------------------------------------------------------- #: List of all running tasks tasks = List(Instance("pyface.tasks.task.Task")) #: Currently active Task if any. active_task = Property( observe=trait("active_window").trait("active_task", optional=True) ) #: List of all application task factories. task_factories = List() #: The default layout for the application. If not specified, a single #: window will be created with the first available task factory. default_layout = List( Instance("pyface.tasks.task_window_layout.TaskWindowLayout") ) #: Hook to add global schema additions to tasks/windows extra_actions = List( Instance("pyface.action.schema.schema_addition.SchemaAddition") ) #: Hook to add global dock pane additions to tasks/windows extra_dock_pane_factories = List(Callable) # Window lifecycle methods ----------------------------------------------- def create_task(self, id): """ Creates the Task with the specified ID. Parameters ---------- id : str The id of the task factory to use. Returns ------- The new Task, or None if there is not a suitable TaskFactory. """ factory = self._get_task_factory(id) if factory is None: logger.warning("Could not find task factory {}".format(id)) return None task = factory.create(id=factory.id) task.extra_actions.extend(self.extra_actions) task.extra_dock_pane_factories.extend(self.extra_dock_pane_factories) return task def create_window(self, layout=None, **kwargs): """ Connect task to application and open task in a new window. Parameters ---------- layout : TaskLayout instance or None The pane layout for the window. **kwargs : dict Additional keyword arguments to pass to the window factory. Returns ------- window : ITaskWindow instance or None The new TaskWindow. """ from pyface.tasks.task_window_layout import TaskWindowLayout window = super().create_window(**kwargs) if layout is not None: for task_id in layout.get_tasks(): task = self.create_task(task_id) if task is not None: window.add_task(task) else: msg = "Missing factory for task with ID %r" logger.error(msg, task_id) else: # Create an empty layout to set default size and position only layout = TaskWindowLayout() window.set_window_layout(layout) return window def _create_windows(self): """ Create the initial windows to display from the default layout. """ for layout in self.default_layout: window = self.create_window(layout) self.add_window(window) self.active_window = window # ------------------------------------------------------------------------- # Private interface # ------------------------------------------------------------------------- def _get_task_factory(self, id): """ Returns the TaskFactory with the specified ID, or None. """ for factory in self.task_factories: if factory.id == id: return factory return None # Destruction utilities --------------------------------------------------- @observe("windows:items:closed") def _on_window_closed(self, event): """ Listener that ensures window handles are released when closed. """ window = event.object if getattr(window, "active_task", None) in self.tasks: self.tasks.remove(window.active_task) super()._on_window_closed(event) # Trait initializers and property getters --------------------------------- def _window_factory_default(self): """ Default to TaskWindow. This will be sufficient in many cases as customized behaviour comes from the Task and the TaskWindow is just a shell. """ from pyface.tasks.task_window import TaskWindow return TaskWindow def _default_layout_default(self): from pyface.tasks.task_window_layout import TaskWindowLayout window_layout = TaskWindowLayout() if self.task_factories: window_layout.items = [self.task_factories[0].id] return [window_layout] def _extra_actions_default(self): """ Extra application-wide menu items This adds a collection of standard Tasks application menu items and groups to a Task's set of menus. Whether or not they actually appear depends on whether the appropriate menus are provided by the Task. These default additions assume that the window will hold an editor pane so that Ctrl-N and Ctrl-W will be bound to creating/closing new editors rather than new task windows. """ from pyface.action.api import ( AboutAction, CloseActiveWindowAction, ExitAction, ) from pyface.action.schema.api import SMenu, SchemaAddition from pyface.tasks.action.api import ( CreateTaskWindowAction, TaskWindowToggleGroup, ) return [ SchemaAddition( factory=CreateTaskWindowAction.factory( application=self, accelerator="Ctrl+Shift+N" ), path="MenuBar/File/new_group", ), SchemaAddition( id="close_action", factory=CloseActiveWindowAction.factory( application=self, accelerator="Ctrl+Shift+W" ), path="MenuBar/File/close_group", ), SchemaAddition( id="exit_action", factory=ExitAction.factory(application=self), path="MenuBar/File/close_group", absolute_position="last", ), SchemaAddition( # id='Window', factory=lambda: SMenu( TaskWindowToggleGroup(application=self), id="Window", name="&Window", ), path="MenuBar", after="View", before="Help", ), SchemaAddition( id="about_action", factory=AboutAction.factory(application=self), path="MenuBar/Help", absolute_position="first", ), ] @cached_property def _get_active_task(self): if self.active_window is not None: return getattr(self.active_window, "active_task", None) else: return None pyface-7.4.0/pyface/tasks/i_dock_pane.py0000644000076500000240000000560014176222673021146 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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, Enum, HasTraits, Tuple from pyface.tasks.i_task_pane import ITaskPane class IDockPane(ITaskPane): """ A pane that is useful but unessential for a task. Dock panes are arranged around the central pane in dock areas, and can, in general, be moved, resized, and hidden by the user. """ #: If enabled, the pane will have a button to close it, and a visibility #: toggle button will be added to the View menu. Otherwise, the pane's #: visibility will only be adjustable programmatically, though the #: 'visible' attribute. closable = Bool(True) #: The dock area in which the pane is currently present. dock_area = Enum("left", "right", "top", "bottom") #: Whether the pane can be detached from the main window. floatable = Bool(True) #: Whether the pane is currently detached from the main window. floating = Bool(False) #: Whether the pane can be moved from one dock area to another. movable = Bool(True) #: The size of the dock pane. Note that this value is read-only. size = Tuple() #: Whether the pane is currently visible. visible = Bool(False) # ------------------------------------------------------------------------ # 'IDockPane' interface. # ------------------------------------------------------------------------ def create_contents(self, parent): """ Create and return the toolkit-specific contents of the dock pane. """ def hide(self): """ Convenience method to hide the dock pane. """ def show(self): """ Convenience method to show the dock pane. """ class MDockPane(HasTraits): """ Mixin containing common code for toolkit-specific implementations. """ # 'IDockPane' interface ------------------------------------------------ closable = Bool(True) dock_area = Enum("left", "right", "top", "bottom") floatable = Bool(True) floating = Bool(False) movable = Bool(True) size = Tuple() visible = Bool(False) caption_visible = Bool(True) dock_layer = Bool(0) # ------------------------------------------------------------------------ # 'IDockPane' interface. # ------------------------------------------------------------------------ def hide(self): """ Convenience method to hide the dock pane. """ self.visible = False def show(self): """ Convenience method to show the dock pane. """ self.visible = True pyface-7.4.0/pyface/tasks/task_window_layout.py0000644000076500000240000000377014176222673022647 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 List, Str, Tuple, Enum, Instance, Union from pyface.tasks.task_layout import LayoutContainer, TaskLayout class TaskWindowLayout(LayoutContainer): """ The layout of a TaskWindow. """ #: The ID of the active task. If unspecified, the first task will be #: active. active_task = Str() #: The tasks contained in the window. If an ID is specified, the task will #: use its default layout. Otherwise, it will use the specified TaskLayout items = List(Union(Str, Instance(TaskLayout)), pretty_skip=True) #: The position of the window. position = Tuple(-1, -1) #: The size of the window. size = Tuple(800, 600) #: Whether or not the application is maximized. size_state = Enum("normal", "maximized") def get_active_task(self): """ Returns the ID of the active task in the layout, or None if there is no active task. """ if self.active_task: return self.active_task elif self.items: first = self.items[0] return first if isinstance(first, str) else first.id return None def get_tasks(self): """ Returns the IDs of the tasks in the layout. """ return [ (item if isinstance(item, str) else item.id) for item in self.items ] def is_equivalent_to(self, layout): """ Returns whether two layouts are equivalent, i.e. whether they contain the same tasks. """ return isinstance(layout, TaskWindowLayout) and set( self.get_tasks() ) == set(layout.get_tasks()) pyface-7.4.0/pyface/tasks/dock_pane.py0000644000076500000240000000102714176222673020635 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ - :attr:`~.DockPane` """ from pyface.toolkit import toolkit_object DockPane = toolkit_object("tasks.dock_pane:DockPane") pyface-7.4.0/pyface/tasks/traits_task_pane.py0000644000076500000240000000360214176222673022246 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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, Instance from .task_pane import TaskPane class TraitsTaskPane(TaskPane): """ A TaskPane that displays a Traits UI View. """ # TraitsTaskPane interface --------------------------------------------- #: The model object to view. If not specified, the pane is used instead. model = Instance(HasTraits) #: The UI object associated with the Traits view, if it has been #: constructed. ui = Instance("traitsui.ui.UI") # ------------------------------------------------------------------------ # 'HasTraits' interface. # ------------------------------------------------------------------------ def trait_context(self): """ Use the model object for the Traits UI context, if appropriate. """ if self.model: return {"object": self.model, "pane": self} return super().trait_context() # ------------------------------------------------------------------------ # 'ITaskPane' interface. # ------------------------------------------------------------------------ def create(self, parent): """ Create and set the toolkit-specific control that represents the pane. """ self.ui = self.edit_traits(kind="subpanel", parent=parent) self.control = self.ui.control def destroy(self): """ Destroy the toolkit-specific control that represents the pane. """ self.ui.dispose() self.control = self.ui = None pyface-7.4.0/pyface/tasks/editor.py0000644000076500000240000000101614176222673020176 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ - :attr:`~.Editor` """ from pyface.toolkit import toolkit_object Editor = toolkit_object("tasks.editor:Editor") pyface-7.4.0/pyface/tasks/i_editor_area_pane.py0000644000076500000240000001532014176222673022504 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 from traits.api import ( Bool, Callable, Dict, Event, File, HasTraits, Instance, List, Str, ) from pyface.tasks.i_editor import IEditor from pyface.tasks.i_task_pane import ITaskPane # Logger. logger = logging.getLogger(__name__) class IEditorAreaPane(ITaskPane): """ A central pane that contains tabbed editors. There are currently two implementations of this interface in Tasks. EditorAreaPane provides a simple, tabbed editor area. AdvancedEditorAreaPane additionally permits arbitrary splitting of the editor area so that editors can be displayed side-by-side. """ # 'IEditorAreaPane' interface -----------------------------------------# #: The currently active editor. active_editor = Instance(IEditor) #: The list of all the visible editors in the pane. editors = List(IEditor) #: A list of extensions for file types to accept via drag and drop. #: Note: This functionality is provided because it is very common, but #: drag and drop support is in general highly toolkit-specific. If more #: sophisticated support is required, subclass an editor area #: implementation. file_drop_extensions = List(Str) #: A file with a supported extension was dropped into the editor area. file_dropped = Event(File) #: Whether to hide the tab bar when there is only a single editor. hide_tab_bar = Bool(False) # ------------------------------------------------------------------------ # 'IEditorAreaPane' interface. # ------------------------------------------------------------------------ def activate_editor(self, editor): """ Activates the specified editor in the pane. """ def add_editor(self, editor): """ Adds an editor to the pane. """ def create_editor(self, obj, factory=None): """ Creates an editor for an object. If a factory is specified, it will be used instead of the editor factory registry. Otherwise, this method will return None if a suitable factory cannot be found in the registry. Note that the editor is not added to the pane. """ def edit(self, obj, factory=None, use_existing=True): """ Edit an object. This is a convenience method that creates and adds an editor for the specified object. If 'use_existing' is set and the object is already being edited, then that editor will be activated and a new editor will not be created. Returns the (possibly new) editor for the object. """ def get_editor(self, obj): """ Returns the editor for an object. Returns None if the object is not being edited. """ def get_factory(self, obj): """ Returns an editor factory suitable for editing an object. Returns None if there is no such editor factory. """ def register_factory(self, factory, filter): """ Registers a factory for creating editors. The 'factory' parameter is a callabe of form: callable(editor_area=editor_area, obj=obj) -> IEditor Often, factory will be a class that provides the 'IEditor' interface. The 'filter' parameter is a callable of form: callable(obj) -> bool that indicates whether the editor factory is suitable for an object. If multiple factories apply to a single object, it is undefined which factory is used. On the other hand, multiple filters may be registered for a single factory, in which case only one must apply for the factory to be selected. """ def remove_editor(self, editor): """ Removes an editor from the pane. """ def unregister_factory(self, factory): """ Unregisters a factory for creating editors. """ class MEditorAreaPane(HasTraits): # 'IEditorAreaPane' interface -----------------------------------------# active_editor = Instance(IEditor) editors = List(IEditor) file_drop_extensions = List(Str) file_dropped = Event(File) hide_tab_bar = Bool(False) # Protected traits ----------------------------------------------------- _factory_map = Dict(Callable, List(Callable)) # ------------------------------------------------------------------------ # 'IEditorAreaPane' interface. # ------------------------------------------------------------------------ def create_editor(self, obj, factory=None): """ Creates an editor for an object. """ if factory is None: factory = self.get_factory(obj) if factory is not None: return factory(editor_area=self, obj=obj) return None def edit(self, obj, factory=None, use_existing=True): """ Edit an object. """ if use_existing: # Is the object already being edited in the window? editor = self.get_editor(obj) if editor is not None: self.activate_editor(editor) return editor # If not, create an editor for it. editor = self.create_editor(obj, factory) if editor is None: logger.warning("Cannot create editor for obj %r", obj) else: self.add_editor(editor) self.activate_editor(editor) return editor def get_editor(self, obj): """ Returns the editor for an object. """ for editor in self.editors: if editor.obj == obj: return editor return None def get_factory(self, obj): """ Returns an editor factory suitable for editing an object. """ for factory, filters in self._factory_map.items(): for filter_ in filters: # FIXME: We should swallow exceptions, but silently? try: if filter_(obj): return factory except: pass return None def register_factory(self, factory, filter): """ Registers a factory for creating editors. """ self._factory_map.setdefault(factory, []).append(filter) def unregister_factory(self, factory): """ Unregisters a factory for creating editors. """ if factory in self._factory_map: del self._factory_map[factory] pyface-7.4.0/pyface/i_font_dialog.py0000644000076500000240000000144614176222673020367 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 interface for a dialog that allows the user to select a color. """ from pyface.ui_traits import PyfaceFont from pyface.i_dialog import IDialog class IFontDialog(IDialog): """ The interface for a dialog that allows the user to choose a font. """ # 'IFontDialog' interface ---------------------------------------------# #: The font in the dialog. font = PyfaceFont() pyface-7.4.0/pyface/confirmation_dialog.py0000644000076500000240000000256014176222673021577 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 implementation of a dialog that prompts the user for confirmation. """ from .constant import NO from .toolkit import toolkit_object ConfirmationDialog = toolkit_object("confirmation_dialog:ConfirmationDialog") def confirm(parent, message, title=None, cancel=False, default=NO): """ Convenience method to show a confirmation dialog. Parameters ---------- parent : toolkit widget or None The parent control for the dialog. message : str The text of the message to display. title : str The text of the dialog title. cancel : bool ``True`` if the dialog should contain a Cancel button. default : NO, YES or CANCEL Which button should be the default button. """ if title is None: title = "Confirmation" dialog = ConfirmationDialog( parent=parent, message=message, cancel=cancel, default=default, title=title, ) return dialog.open() pyface-7.4.0/pyface/color.py0000644000076500000240000002011714176222673016704 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Color classes and corresponding trait types for Pyface. The base Color class holds red, green, blue and alpha channel values as a tuple of normalized values from 0.0 to 1.0. Various property traits pull out the individual channel values and supply values for the HSV and HSL colour spaces (with and without alpha). The ``from_toolkit`` and ``to_toolkit`` methods allow conversion to and from native toolkit color objects. """ import colorsys from traits.api import ( Bool, HasStrictTraits, Property, Range, Tuple, cached_property ) from pyface.util.color_helpers import channels_to_ints, is_dark from pyface.util.color_helpers import ints_to_channels # noqa: F401 from pyface.util.color_parser import parse_text #: A trait holding a single channel value. Channel = Range(0.0, 1.0, value=1.0, channel=True) #: A trait holding three channel values. ChannelTuple = Tuple(Channel, Channel, Channel) #: A trait holding four channel values. AlphaChannelTuple = Tuple(Channel, Channel, Channel, Channel) class Color(HasStrictTraits): """ A mutable specification of a color with alpha. This is a class designed to be used by user interface elements which need to color some or all of the interface element. Each color has a number of different representations as channel tuples, each channel holding a value between 0.0 and 1.0, inclusive. The standard red, green, blue and alpha channels are also provided as convenience properties. Methods are provided to convert to and from toolkit-specific color objects. Colors implement equality testing, but are not hashable as they are mutable, and so are not suitable for use as dictionary keys. If you need a dictionary key, use an appropriate channel tuple from the object. """ #: A tuple holding the red, green, blue, and alpha channels. rgba = AlphaChannelTuple() #: A tuple holding the red, green, and blue channels. rgb = Property(ChannelTuple(), observe='rgba') #: The red channel. red = Property(Channel, observe='rgba') #: The green channel. green = Property(Channel, observe='rgba') #: The blue channel. blue = Property(Channel, observe='rgba') #: The alpha channel. alpha = Property(Channel, observe='rgba') #: A tuple holding the hue, saturation, value, and alpha channels. hsva = Property(AlphaChannelTuple, observe='rgba') #: A tuple holding the hue, saturation, and value channels. hsv = Property(ChannelTuple, observe='rgb') #: A tuple holding the hue, lightness, saturation, and alpha channels. hlsa = Property(AlphaChannelTuple, observe='rgba') #: A tuple holding the hue, lightness, and saturation channels. hls = Property(ChannelTuple, observe='rgb') #: Whether the color is dark for contrast purposes. is_dark = Property(Bool, observe='rgba') @classmethod def from_str(cls, text, **traits): """ Create a new Color object from a string. Parameters ---------- text : str A string holding the representation of the color. This can be: - a color name, including all CSS color names, plus any additional names found in pyface.color.color_table. The names are normalized to lower case and stripped of whitespace, hyphens and underscores. - a hex representation of the color in the form '#RGB', '#RGBA', '#RRGGBB', '#RRGGBBAA', '#RRRRGGGGBBBB', or '#RRRRGGGGBBBBAAAA'. **traits Any additional trait values to be passed as keyword arguments. Raises ------ ColorParseError If the string cannot be converted to a valid color. """ space, channels = parse_text(text) if space in traits: raise TypeError( "from_str() got multiple values for keyword argument " + repr(space) ) traits[space] = channels return cls(**traits) @classmethod def from_toolkit(cls, toolkit_color, **traits): """ Create a new Color object from a toolkit color object. Parameters ---------- toolkit_color : toolkit object A toolkit color object, such as a Qt QColor or a Wx wx.Colour. **traits Any additional trait values to be passed as keyword arguments. """ from pyface.toolkit import toolkit_object toolkit_color_to_rgba = toolkit_object('color:toolkit_color_to_rgba') rgba = toolkit_color_to_rgba(toolkit_color) return cls(rgba=rgba, **traits) def to_toolkit(self): """ Create a new toolkit color object from a Color object. Returns ------- toolkit_color : toolkit object A toolkit color object, such as a Qt QColor or a Wx wx.Colour. """ from pyface.toolkit import toolkit_object rgba_to_toolkit_color = toolkit_object('color:rgba_to_toolkit_color') return rgba_to_toolkit_color(self.rgba) def hex(self): """ Provide a hex representation of the Color object. Note that because the hex value is restricted to 0-255 integer values for each channel, the representation is not exact. Returns ------- hex : str A hex string in standard ``#RRGGBBAA`` format that represents the color. """ values = channels_to_ints(self.rgba) return "#{:02X}{:02X}{:02X}{:02X}".format(*values) def __eq__(self, other): if isinstance(other, Color): return self.rgba == other.rgba return NotImplemented def __str__(self): return "({:0.5}, {:0.5}, {:0.5}, {:0.5})".format(*self.rgba) def __repr__(self): return "{}(rgba={!r})".format(self.__class__.__name__, self.rgba) def _get_red(self): return self.rgba[0] def _set_red(self, value): r, g, b, a = self.rgba self.rgba = (value, g, b, a) def _get_green(self): return self.rgba[1] def _set_green(self, value): r, g, b, a = self.rgba self.rgba = (r, value, b, a) def _get_blue(self): return self.rgba[2] def _set_blue(self, value): r, g, b, a = self.rgba self.rgba = (r, g, value, a) def _get_alpha(self): return self.rgba[3] def _set_alpha(self, value): r, g, b, a = self.rgba self.rgba = (r, g, b, value) @cached_property def _get_rgb(self): return self.rgba[:-1] def _set_rgb(self, value): r, g, b = value self.rgba = (r, g, b, self.rgba[3]) @cached_property def _get_hsva(self): r, g, b, a = self.rgba h, s, v = colorsys.rgb_to_hsv(r, g, b) return (h, s, v, a) def _set_hsva(self, value): h, s, v, a = value r, g, b = colorsys.hsv_to_rgb(h, s, v) self.rgba = (r, g, b, a) @cached_property def _get_hsv(self): r, g, b = self.rgb return colorsys.rgb_to_hsv(r, g, b) def _set_hsv(self, value): h, s, v = value r, g, b = colorsys.hsv_to_rgb(h, s, v) self.rgb = (r, g, b) @cached_property def _get_hlsa(self): r, g, b, a = self.rgba h, l, s = colorsys.rgb_to_hls(r, g, b) return (h, l, s, a) def _set_hlsa(self, value): h, l, s, a = value r, g, b = colorsys.hls_to_rgb(h, l, s) self.rgba = (r, g, b, a) @cached_property def _get_hls(self): r, g, b = self.rgb return colorsys.rgb_to_hls(r, g, b) def _set_hls(self, value): h, l, s = value r, g, b = colorsys.hls_to_rgb(h, l, s) self.rgb = (r, g, b) @cached_property def _get_is_dark(self): return is_dark(self.rgb) pyface-7.4.0/pyface/single_choice_dialog.py0000644000076500000240000000307514176222673021704 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 dialog that allows the user to chose a single item from a list. """ from .constant import OK from .toolkit import toolkit_object # Import the toolkit specific version. SingleChoiceDialog = toolkit_object("single_choice_dialog:SingleChoiceDialog") # Convenience functions. def choose_one(parent, message, choices, title="Choose", cancel=True): """ Convenience method to show an information message dialog. Parameters ---------- parent : toolkit control or None The toolkit control that should be the parent of the dialog. message : str The text of the message to display. choices : list List of objects to choose from. title : str The text of the dialog title. cancel : bool Whether or not the dialog can be cancelled. Returns ------- choice : any The selected object, or None if cancelled. """ dialog = SingleChoiceDialog( parent=parent, message=message, choices=choices, title=title, cancel=cancel, ) result = dialog.open() if result == OK: choice = dialog.choice else: choice = None return choice pyface-7.4.0/pyface/widget.py0000644000076500000240000000111014176222673017041 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 base implementation of all pyface widgets. """ # Import the toolkit specific version. from .toolkit import toolkit_object Widget = toolkit_object("widget:Widget") pyface-7.4.0/pyface/i_layered_panel.py0000644000076500000240000000706114176222673020705 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Interace and mixins for layered panels. A layered panel contains one or more named layers, with only one layer visible at any one time (think of a 'tab' control minus the tabs!). """ import warnings from traits.api import Any, Dict, HasTraits, Interface, Str class ILayeredPanel(Interface): """ A Layered panel. A layered panel contains one or more named layers, with only one layer visible at any one time (think of a 'tab' control minus the tabs!). Each layer is a toolkit-specific control. """ # "ILayeredPanel' interface -------------------------------------------- # The toolkit-specific control of the currently displayed layer. current_layer = Any() # The name of the currently displayed layer. current_layer_name = Str() # ------------------------------------------------------------------------ # 'ILayeredPanel' interface. # ------------------------------------------------------------------------ def add_layer(self, name, layer): """ Adds a layer with the specified name. All layers are hidden when they are added. Use 'show_layer' to make a layer visible. """ def show_layer(self, name): """ Shows the layer with the specified name. """ def has_layer(self, name): """ Does the panel contain a layer with the specified name? """ class MLayeredPanel(HasTraits): """ A Layered panel mixin. A layered panel contains one or more named layers, with only one layer visible at any one time (think of a 'tab' control minus the tabs!). Each layer is a toolkit-specific control. """ # "ILayeredPanel' interface -------------------------------------------- # The toolkit-specific control of the currently displayed layer. current_layer = Any() # The name of the currently displayed layer. current_layer_name = Str() # Private traits ------------------------------------------------------- # The a map of layer names to toolkit controls in the panel. _layers = Dict(Str) # ------------------------------------------------------------------------ # 'ILayeredPanel' interface. # ------------------------------------------------------------------------ def has_layer(self, name): """ Does the panel contain a layer with the specified name? """ return name in self._layers # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, parent=None, **traits): """ Creates a new LayeredPanel. """ create = traits.pop("create", True) # Base class constructor. super().__init__(parent=parent, **traits) if create: # Create the toolkit-specific control that represents the widget. self.create() warnings.warn( "automatic widget creation is deprecated and will be removed " "in a future Pyface version, use create=False and explicitly " "call create() for future behaviour", PendingDeprecationWarning, ) pyface-7.4.0/pyface/toolkit.py0000644000076500000240000000135514176222673017256 0ustar cwebsterstaff00000000000000# (C) Copyright 2007 Riverbank Computing Limited # (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 toolkit object for the current backend toolkit See :py:func:`pyface.base_toolkit.find_toolkit` for details on the loading algorithm. """ from .base_toolkit import find_toolkit # The toolkit function. toolkit = toolkit_object = find_toolkit("pyface.toolkits") pyface-7.4.0/pyface/directory_dialog.py0000644000076500000240000000120414176222673021105 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 implementation of a dialog that allows the user to browse for a directory. """ # Import the toolkit specific version. from .toolkit import toolkit_object DirectoryDialog = toolkit_object("directory_dialog:DirectoryDialog") pyface-7.4.0/pyface/message_dialog.py0000644000076500000240000000735214176222673020537 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 implementation of a dialog that displays a message. """ # Import the toolkit specific version. from .toolkit import toolkit_object MessageDialog = toolkit_object("message_dialog:MessageDialog") # Convenience functions. def information( parent, message, title="Information", detail="", informative="", text_format="auto" ): """ Convenience method to show an information message dialog. Parameters ---------- parent : toolkit control or None The toolkit control that should be the parent of the dialog. message : str The text of the message to display. title : str The text of the dialog title. detail : str Further details about the message (displayed when the user clicks "Show details"). informative : str Explanatory text to display along with the message. text_format : str Specifies what text format to use in the resulting message dialog. One of "auto", "plain", or "rich". Only supported on the qt backend. """ dialog = MessageDialog( parent=parent, message=message, title=title, severity="information", detail=detail, informative=informative, text_format=text_format, ) dialog.open() def warning( parent, message, title="Warning", detail="", informative="", text_format="auto" ): """ Convenience function to show a warning message dialog. Parameters ---------- parent : toolkit control or None The toolkit control that should be the parent of the dialog. message : str The text of the message to display. title : str The text of the dialog title. detail : str Further details about the message (displayed when the user clicks "Show details"). informative : str Explanatory text to display along with the message. text_format : str Specifies what text format to use in the resulting message dialog. One of "auto", "plain", or "rich". Only supported on the qt backend. """ dialog = MessageDialog( parent=parent, message=message, title=title, severity="warning", detail=detail, informative=informative, text_format=text_format, ) dialog.open() def error( parent, message, title="Error", detail="", informative="", text_format="auto" ): """ Convenience function to show an error message dialog. Parameters ---------- parent : toolkit control or None The toolkit control that should be the parent of the dialog. message : str The text of the message to display. title : str The text of the dialog title. detail : str Further details about the message (displayed when the user clicks "Show details"). informative : str Explanatory text to display along with the message. text_format : str Specifies what text format to use in the resulting message dialog. One of "auto", "plain", or "rich". Only supported on the qt backend. """ dialog = MessageDialog( parent=parent, message=message, title=title, severity="error", detail=detail, informative=informative, text_format=text_format, ) dialog.open() pyface-7.4.0/pyface/i_color_dialog.py0000644000076500000240000000165114176222673020535 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 interface for a dialog that allows the user to select a color. """ from traits.api import Bool from pyface.ui_traits import PyfaceColor from pyface.i_dialog import IDialog class IColorDialog(IDialog): """ The interface for a dialog that allows the user to choose a color. """ # 'IColorDialog' interface ---------------------------------------------# #: The color in the dialog. color = PyfaceColor() #: Whether or not to allow the user to chose an alpha value. show_alpha = Bool(False) pyface-7.4.0/pyface/window.py0000644000076500000240000000112614176222673017074 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 abstract implementation of all pyface top-level windows. """ # Import the toolkit specific version. from .toolkit import toolkit_object Window = toolkit_object("window:Window") pyface-7.4.0/pyface/image_resource.py0000644000076500000240000000113014176222673020551 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 implementation of an image resource. """ # Import the toolkit specific version. from .toolkit import toolkit_object ImageResource = toolkit_object("image_resource:ImageResource") pyface-7.4.0/pyface/multi_toolbar_window.py0000644000076500000240000000115614176222673022033 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # Note: The MultiToolbarWindow is currently wx-specific # Import the toolkit specific version. from .toolkit import toolkit_object MultiToolbarWindow = toolkit_object("multi_toolbar_window:MultiToolbarWindow") pyface-7.4.0/pyface/list_box.py0000644000076500000240000000110114176222673017401 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # Note: The ListBox is currently wx-specific # Import the toolkit specific version. from .toolkit import toolkit_object ListBox = toolkit_object("list_box:ListBox") pyface-7.4.0/pyface/util/0000755000076500000240000000000014176460551016167 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/util/id_helper.py0000644000076500000240000000314214176222673020475 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Helper functions to automatically generate unique IDs. """ from weakref import WeakKeyDictionary class _ObjectCounter(object): """ Counts objects. """ def __init__(self): self._objects_registry = WeakKeyDictionary() def get_count(self, obj): """ Return the number of times an object was seen. Objects must be hashable. """ if obj in self._objects_registry: count = self._objects_registry[obj] else: count = 0 return count def next_count(self, obj): """ Increase and return the number of times an object was seen. Objects must be hashable. """ count = self.get_count(obj) self._objects_registry[obj] = count + 1 return self._objects_registry[obj] # Global object counter. object_counter = _ObjectCounter() def get_unique_id(object): """ Return a unique ID of the form ClassName_X, where X is an integer. It is only guaranteed that IDs are unique to a specific Python session, not across sessions. """ class_ = object.__class__ name = class_.__name__ number = object_counter.next_count(class_) return name + "_" + str(number) pyface-7.4.0/pyface/util/modal_dialog_tester.py0000644000076500000240000000117014176222673022542 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 implementation of a Modal Dialog Tester. """ # Import the toolkit specific version. from pyface.toolkit import toolkit_object ModalDialogTester = toolkit_object( "util.modal_dialog_tester:ModalDialogTester" ) pyface-7.4.0/pyface/util/color_parser.py0000644000076500000240000002603714176222673021244 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Color string parser for Pyface. The ``parse_text`` function allows the creation of Color objects from CSS-style strings, including all the CSS names colours or hex strings starting with "#". If there is no match, a ColorParseError is raised. A dictionary of named colours is available as the color_table module-level dictionary. This dictionary holds all CSS colour names, plus a number of other names such as "transparent". Additionally, two utility functions ``channels_to_ints`` and ``ints_to_channels`` are provided to assist in converting between floating point and integer values. """ import re from .color_helpers import ints_to_channels #: A dictionary mapping known color names to rgba tuples. color_table = { "aliceblue": (0.941, 0.973, 1.000, 1.0), "antiquewhite": (0.980, 0.922, 0.843, 1.0), "aqua": (0.000, 1.000, 1.000, 1.0), "aquamarine": (0.498, 1.000, 0.831, 1.0), "azure": (0.941, 1.000, 1.000, 1.0), "beige": (0.961, 0.961, 0.863, 1.0), "bisque": (1.000, 0.894, 0.769, 1.0), "black": (0.000, 0.000, 0.000, 1.0), "blanchedalmond": (1.000, 0.922, 0.804, 1.0), "blue": (0.000, 0.000, 1.000, 1.0), "blueviolet": (0.541, 0.169, 0.886, 1.0), "brown": (0.647, 0.165, 0.165, 1.0), "burlywood": (0.871, 0.722, 0.529, 1.0), "cadetblue": (0.373, 0.620, 0.627, 1.0), "chartreuse": (0.498, 1.000, 0.000, 1.0), "chocolate": (0.824, 0.412, 0.118, 1.0), "coral": (1.000, 0.498, 0.314, 1.0), "cornflowerblue": (0.392, 0.584, 0.929, 1.0), "cornsilk": (1.000, 0.973, 0.863, 1.0), "crimson": (0.863, 0.078, 0.235, 1.0), "cyan": (0.000, 1.000, 1.000, 1.0), "darkblue": (0.000, 0.000, 0.545, 1.0), "darkcyan": (0.000, 0.545, 0.545, 1.0), "darkgoldenrod": (0.722, 0.525, 0.043, 1.0), "darkgray": (0.663, 0.663, 0.663, 1.0), "darkgreen": (0.000, 0.392, 0.000, 1.0), "darkgrey": (0.663, 0.663, 0.663, 1.0), "darkkhaki": (0.741, 0.718, 0.420, 1.0), "darkmagenta": (0.545, 0.000, 0.545, 1.0), "darkolivegreen": (0.333, 0.420, 0.184, 1.0), "darkorange": (1.000, 0.549, 0.000, 1.0), "darkorchid": (0.600, 0.196, 0.800, 1.0), "darkred": (0.545, 0.000, 0.000, 1.0), "darksalmon": (0.914, 0.588, 0.478, 1.0), "darkseagreen": (0.561, 0.737, 0.561, 1.0), "darkslateblue": (0.282, 0.239, 0.545, 1.0), "darkslategray": (0.184, 0.310, 0.310, 1.0), "darkslategrey": (0.184, 0.310, 0.310, 1.0), "darkturquoise": (0.000, 0.808, 0.820, 1.0), "darkviolet": (0.580, 0.000, 0.827, 1.0), "deeppink": (1.000, 0.078, 0.576, 1.0), "deepskyblue": (0.000, 0.749, 1.000, 1.0), "dimgray": (0.412, 0.412, 0.412, 1.0), "dimgrey": (0.412, 0.412, 0.412, 1.0), "dodgerblue": (0.118, 0.565, 1.000, 1.0), "firebrick": (0.698, 0.133, 0.133, 1.0), "floralwhite": (1.000, 0.980, 0.941, 1.0), "forestgreen": (0.133, 0.545, 0.133, 1.0), "fuchsia": (1.000, 0.000, 1.000, 1.0), "gainsboro": (0.863, 0.863, 0.863, 1.0), "ghostwhite": (0.973, 0.973, 1.000, 1.0), "gold": (1.000, 0.843, 0.000, 1.0), "goldenrod": (0.855, 0.647, 0.125, 1.0), "gray": (0.502, 0.502, 0.502, 1.0), "green": (0.000, 0.502, 0.000, 1.0), "greenyellow": (0.678, 1.000, 0.184, 1.0), "grey": (0.502, 0.502, 0.502, 1.0), "honeydew": (0.941, 1.000, 0.941, 1.0), "hotpink": (1.000, 0.412, 0.706, 1.0), "indianred": (0.804, 0.361, 0.361, 1.0), "indigo": (0.294, 0.000, 0.510, 1.0), "ivory": (1.000, 1.000, 0.941, 1.0), "khaki": (0.941, 0.902, 0.549, 1.0), "lavender": (0.902, 0.902, 0.980, 1.0), "lavenderblush": (1.000, 0.941, 0.961, 1.0), "lawngreen": (0.486, 0.988, 0.000, 1.0), "lemonchiffon": (1.000, 0.980, 0.804, 1.0), "lightblue": (0.678, 0.847, 0.902, 1.0), "lightcoral": (0.941, 0.502, 0.502, 1.0), "lightcyan": (0.878, 1.000, 1.000, 1.0), "lightgoldenrodyellow": (0.980, 0.980, 0.824, 1.0), "lightgray": (0.827, 0.827, 0.827, 1.0), "lightgreen": (0.565, 0.933, 0.565, 1.0), "lightgrey": (0.827, 0.827, 0.827, 1.0), "lightpink": (1.000, 0.714, 0.757, 1.0), "lightsalmon": (1.000, 0.627, 0.478, 1.0), "lightseagreen": (0.125, 0.698, 0.667, 1.0), "lightskyblue": (0.529, 0.808, 0.980, 1.0), "lightslategray": (0.467, 0.533, 0.600, 1.0), "lightslategrey": (0.467, 0.533, 0.600, 1.0), "lightsteelblue": (0.690, 0.769, 0.871, 1.0), "lightyellow": (1.000, 1.000, 0.878, 1.0), "lime": (0.000, 1.000, 0.000, 1.0), "limegreen": (0.196, 0.804, 0.196, 1.0), "linen": (0.980, 0.941, 0.902, 1.0), "magenta": (1.000, 0.000, 1.000, 1.0), "maroon": (0.502, 0.000, 0.000, 1.0), "mediumaquamarine": (0.400, 0.804, 0.667, 1.0), "mediumblue": (0.000, 0.000, 0.804, 1.0), "mediumorchid": (0.729, 0.333, 0.827, 1.0), "mediumpurple": (0.576, 0.439, 0.859, 1.0), "mediumseagreen": (0.235, 0.702, 0.443, 1.0), "mediumslateblue": (0.482, 0.408, 0.933, 1.0), "mediumspringgreen": (0.000, 0.980, 0.604, 1.0), "mediumturquoise": (0.282, 0.820, 0.800, 1.0), "mediumvioletred": (0.780, 0.082, 0.522, 1.0), "midnightblue": (0.098, 0.098, 0.439, 1.0), "mintcream": (0.961, 1.000, 0.980, 1.0), "mistyrose": (1.000, 0.894, 0.882, 1.0), "moccasin": (1.000, 0.894, 0.710, 1.0), "navajowhite": (1.000, 0.871, 0.678, 1.0), "navy": (0.000, 0.000, 0.502, 1.0), "oldlace": (0.992, 0.961, 0.902, 1.0), "olive": (0.502, 0.502, 0.000, 1.0), "olivedrab": (0.420, 0.557, 0.137, 1.0), "orange": (1.000, 0.647, 0.000, 1.0), "orangered": (1.000, 0.271, 0.000, 1.0), "orchid": (0.855, 0.439, 0.839, 1.0), "palegoldenrod": (0.933, 0.910, 0.667, 1.0), "palegreen": (0.596, 0.984, 0.596, 1.0), "paleturquoise": (0.686, 0.933, 0.933, 1.0), "palevioletred": (0.859, 0.439, 0.576, 1.0), "papayawhip": (1.000, 0.937, 0.835, 1.0), "peachpuff": (1.000, 0.855, 0.725, 1.0), "peru": (0.804, 0.522, 0.247, 1.0), "pink": (1.000, 0.753, 0.796, 1.0), "plum": (0.867, 0.627, 0.867, 1.0), "powderblue": (0.690, 0.878, 0.902, 1.0), "purple": (0.502, 0.000, 0.502, 1.0), "red": (1.000, 0.000, 0.000, 1.0), "rosybrown": (0.737, 0.561, 0.561, 1.0), "royalblue": (0.255, 0.412, 0.882, 1.0), "saddlebrown": (0.545, 0.271, 0.075, 1.0), "salmon": (0.980, 0.502, 0.447, 1.0), "sandybrown": (0.957, 0.643, 0.376, 1.0), "seagreen": (0.180, 0.545, 0.341, 1.0), "seashell": (1.000, 0.961, 0.933, 1.0), "sienna": (0.627, 0.322, 0.176, 1.0), "silver": (0.753, 0.753, 0.753, 1.0), "skyblue": (0.529, 0.808, 0.922, 1.0), "slateblue": (0.416, 0.353, 0.804, 1.0), "slategray": (0.439, 0.502, 0.565, 1.0), "slategrey": (0.439, 0.502, 0.565, 1.0), "snow": (1.000, 0.980, 0.980, 1.0), "springgreen": (0.000, 1.000, 0.498, 1.0), "steelblue": (0.275, 0.510, 0.706, 1.0), "tan": (0.824, 0.706, 0.549, 1.0), "teal": (0.000, 0.502, 0.502, 1.0), "thistle": (0.847, 0.749, 0.847, 1.0), "tomato": (1.000, 0.388, 0.278, 1.0), "turquoise": (0.251, 0.878, 0.816, 1.0), "violet": (0.933, 0.510, 0.933, 1.0), "wheat": (0.961, 0.871, 0.702, 1.0), "white": (1.000, 1.000, 1.000, 1.0), "whitesmoke": (0.961, 0.961, 0.961, 1.0), "yellow": (1.000, 1.000, 0.000, 1.0), "yellowgreen": (0.604, 0.804, 0.196, 1.0), "rebeccapurple": (0.4, 0.2, 0.6, 1.0), # Several aliases for transparent "clear": (0.0, 0.0, 0.0, 0.0), "transparent": (0.0, 0.0, 0.0, 0.0), "none": (0.0, 0.0, 0.0, 0.0), } # Translation table for stripping extraneous characters out of names. ignored = str.maketrans({' ': None, '-': None, '_': None}) def _parse_name(text): """ Parse a color name. Parameters ---------- text : str A string holding a color name, including all CSS color names, plus any additional names found in pyface.color.color_table. The names are normalized to lower case and stripped of whitespace, hyphens and underscores. Returns ------- result : (space, channels), or None Either a tuple of the form ('rgba', channels), where channels is a tuple of 4 floating point values between 0.0 and 1.0, inclusive; or None if there is no matching color name. """ text = text.lower() text = text.translate(ignored) if text in color_table: return 'rgba', color_table[text] return None def _parse_hex(text): """ Parse a hex form of a color. Parameters ---------- text : str A string holding a hex representation of the color in the form '#RGB', '#RGBA', '#RRGGBB', '#RRGGBBAA', '#RRRRGGGGBBBB', or '#RRRRGGGGBBBBAAAA'. Returns ------- result : (space, channels), or None Either a tuple of the form (space, channels), where space is one of 'rgb' or 'rgba' and channels is a tuple of 3 or 4 floating point values between 0.0 and 1.0, inclusive; or None if no hex representation could be matched. """ text = text.strip() if re.match("#[0-9a-fA-F]+", text) is None: return None text = text[1:] if len(text) in {3, 4}: step = 1 elif len(text) in {6, 8}: step = 2 elif len(text) in {12, 16}: step = 4 else: return None maximum = (1 << 4 * step) - 1 channels = ints_to_channels( (int(text[i:i+step], 16) for i in range(0, len(text), step)), maximum=maximum, ) space = 'rgb' if len(channels) == 3 else 'rgba' return space, channels class ColorParseError(ValueError): """ An Exception raised when parsing fails. """ pass def parse_text(text): """ Parse a text representation of a color. Parameters ---------- text : str A string holding the representation of the color. This can be: - a color name, including all CSS color names, plus any additional names found in pyface.color.color_table. The names are normalized to lower case and stripped of whitespace, hyphens and underscores. - a hex representation of the color in the form '#RGB', '#RGBA', '#RRGGBB', '#RRGGBBAA', '#RRRRGGGGBBBB', or '#RRRRGGGGBBBBAAAA'. Returns ------- space : str A string describing the color space for the channels. Will be one of 'rgb' or 'rgba'. channels : tuple of floats The channel values as a tuple of 3 or 4 floating point values between 0.0 and 1.0, inclusive. Raises ------ ColorParseError If the string cannot be converted to a valid color. """ result = None for parser in _parse_hex, _parse_name: result = parser(text) if result is not None: return result else: raise ColorParseError( 'Unable to parse color value in string {!r}'.format(text) ) pyface-7.4.0/pyface/util/guisupport.py0000644000076500000240000001456614176222673020777 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Support for creating GUI apps and starting event loops. IPython's GUI integration allows interative plotting and GUI usage in IPython session. IPython has two different types of GUI integration: 1. The terminal based IPython supports GUI event loops through Python's PyOS_InputHook. PyOS_InputHook is a hook that Python calls periodically whenever raw_input is waiting for a user to type code. We implement GUI support in the terminal by setting PyOS_InputHook to a function that iterates the event loop for a short while. It is important to note that in this situation, the real GUI event loop is NOT run in the normal manner, so you can't use the normal means to detect that it is running. 2. In the two process IPython kernel/frontend, the GUI event loop is run in the kernel. In this case, the event loop is run in the normal manner by calling the function or method of the GUI toolkit that starts the event loop. In addition to starting the GUI event loops in one of these two ways, IPython will *always* create an appropriate GUI application object when GUi integration is enabled. If you want your GUI apps to run in IPython you need to do two things: 1. Test to see if there is already an existing main application object. If there is, you should use it. If there is not an existing application object you should create one. 2. Test to see if the GUI event loop is running. If it is, you should not start it. If the event loop is not running you may start it. This module contains functions for each toolkit that perform these things in a consistent manner. Because of how PyOS_InputHook runs the event loop you cannot detect if the event loop is running using the traditional calls (such as ``wx.GetApp.IsMainLoopRunning()`` in wxPython). If PyOS_InputHook is set These methods will return a false negative. That is, they will say the event loop is not running, when is actually is. To work around this limitation we proposed the following informal protocol: * Whenever someone starts the event loop, they *must* set the ``_in_event_loop`` attribute of the main application object to ``True``. This should be done regardless of how the event loop is actually run. * Whenever someone stops the event loop, they *must* set the ``_in_event_loop`` attribute of the main application object to ``False``. * If you want to see if the event loop is running, you *must* use ``hasattr`` to see if ``_in_event_loop`` attribute has been set. If it is set, you *must* use its value. If it has not been set, you can query the toolkit in the normal manner. * If you want GUI support and no one else has created an application or started the event loop you *must* do this. We don't want projects to attempt to defer these things to someone else if they themselves need it. The functions below implement this logic for each GUI toolkit. If you need to create custom application subclasses, you will likely have to modify this code for your own purposes. This code can be copied into your own project so you don't have to depend on IPython. """ # ----------------------------------------------------------------------------- # Copyright (C) 2008-2010 The IPython Development Team # # Distributed under the terms of the BSD License. The full license is in # the file COPYING, distributed as part of this software. # ----------------------------------------------------------------------------- # ----------------------------------------------------------------------------- # Imports # ----------------------------------------------------------------------------- # Prevent name conflict with local wx package. # ----------------------------------------------------------------------------- # wx # ----------------------------------------------------------------------------- def get_app_wx(*args, **kwargs): """Create a new wx app or return an exiting one.""" import wx app = wx.GetApp() if app is None: if "redirect" not in kwargs: kwargs["redirect"] = False app = wx.App(*args, **kwargs) return app def is_event_loop_running_wx(app=None): """Is the wx event loop running.""" if app is None: app = get_app_wx() if hasattr(app, "_in_event_loop"): return app._in_event_loop else: return app.IsMainLoopRunning() def start_event_loop_wx(app=None): """Start the wx event loop in a consistent manner.""" if app is None: app = get_app_wx() if not is_event_loop_running_wx(app): app._in_event_loop = True app.MainLoop() app._in_event_loop = False else: app._in_event_loop = True # ----------------------------------------------------------------------------- # qt4 # ----------------------------------------------------------------------------- def get_app_qt4(*args, **kwargs): """Create a new qt4 app or return an existing one.""" from pyface.qt import QtGui app = QtGui.QApplication.instance() if app is None: if not args: args = ([""],) app = QtGui.QApplication(*args, **kwargs) return app def is_event_loop_running_qt4(app=None): """Is the qt4 event loop running.""" if app is None: app = get_app_qt4([""]) if hasattr(app, "_in_event_loop"): return app._in_event_loop else: # Does qt4 provide a other way to detect this? return False def start_event_loop_qt4(app=None): """Start the qt4 event loop in a consistent manner.""" if app is None: app = get_app_qt4([""]) if not is_event_loop_running_qt4(app): app._in_event_loop = True app.exec_() app._in_event_loop = False else: app._in_event_loop = True # ----------------------------------------------------------------------------- # Tk # ----------------------------------------------------------------------------- # ----------------------------------------------------------------------------- # gtk # ----------------------------------------------------------------------------- pyface-7.4.0/pyface/util/event_loop_helper.py0000644000076500000240000000114614176222673022255 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 implementation of EventLoopHelper. """ # Import the toolkit specific version. from pyface.toolkit import toolkit_object EventLoopHelper = toolkit_object("util.event_loop_helper:EventLoopHelper") pyface-7.4.0/pyface/util/image_helpers.py0000644000076500000240000000316114176222673021347 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Helper functions for working with images This module provides helper functions for converting between numpy arrays and toolkit images, as well as between the various image types in a standardized way. Helper functions ---------------- - :data:`~.array_to_image` - :data:`~.bitmap_to_icon` - :data:`~.bitmap_to_image` - :data:`~.image_to_array` - :data:`~.image_to_bitmap` - :data:`~.resize_image` - :data:`~.resize_bitmap` Options for resizing images --------------------------- - :data:`~.ScaleMode` - :data:`~.AspectRatio` """ from pyface.toolkit import toolkit_object # Enum types for function arguments ScaleMode = toolkit_object("util.image_helpers:ScaleMode") AspectRatio = toolkit_object("util.image_helpers:AspectRatio") # Helper functions array_to_image = toolkit_object("util.image_helpers:array_to_image") bitmap_to_icon = toolkit_object("util.image_helpers:bitmap_to_icon") bitmap_to_image = toolkit_object("util.image_helpers:bitmap_to_image") image_to_array = toolkit_object("util.image_helpers:image_to_array") image_to_bitmap = toolkit_object("util.image_helpers:image_to_bitmap") resize_image = toolkit_object("util.image_helpers:resize_image") resize_bitmap = toolkit_object("util.image_helpers:resize_bitmap") pyface-7.4.0/pyface/util/tests/0000755000076500000240000000000014176460551017331 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/util/tests/test_modal_dialog_tester.py0000644000076500000240000000143414176222673024746 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 import unittest from pyface.toolkit import toolkit is_wx = (toolkit.toolkit == "wx") class TestModalDialogTester(unittest.TestCase): @unittest.skipIf(is_wx, "wx is not supported") def test_import(self): from pyface.util.modal_dialog_tester import ModalDialogTester self.assertNotEqual(ModalDialogTester.__name__, "Unimplemented") pyface-7.4.0/pyface/util/tests/__init__.py0000644000076500000240000000000014176222673021431 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/util/tests/test_image_helpers.py0000644000076500000240000000140014176222673023542 0ustar cwebsterstaff00000000000000# Copyright (c) 2005-2022, Enthought Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only # under the conditions described 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 TestImageHelpers(unittest.TestCase): def test_imports(self): # actual functions are tested in toolkits from ..image_helpers import ( # noqa: F401 AspectRatio, ScaleMode, array_to_image, bitmap_to_icon, bitmap_to_image, image_to_array, image_to_bitmap, resize_bitmap, resize_image, ) pyface-7.4.0/pyface/util/tests/test_color_helpers.py0000644000076500000240000001136514176222673023611 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 pyface.util.color_helpers import ( channels_to_ints, ints_to_channels, is_dark, relative_luminance ) class TestChannelConversion(TestCase): def test_ints_to_channels(self): values = (102, 102, 0, 255) channels = ints_to_channels(values) self.assertEqual(channels, (0.4, 0.4, 0.0, 1.0)) def test_ints_to_channels_maximum(self): values = (6, 6, 0, 15) channels = ints_to_channels(values, maximum=15) self.assertEqual(channels, (0.4, 0.4, 0.0, 1.0)) def test_channels_to_ints(self): channels = (0.4, 0.4, 0.0, 1.0) values = channels_to_ints(channels) self.assertEqual(values, (102, 102, 0, 255)) def test_channels_to_ints_maximum(self): channels = (0.4, 0.4, 0.0, 1.0) values = channels_to_ints(channels, maximum=15) self.assertEqual(values, (6, 6, 0, 15)) def test_round_trip(self): """ Test to assert stability of values through round-trips """ for value in range(256): with self.subTest(int=value): result = channels_to_ints(ints_to_channels([value])) self.assertEqual(result, (value,)) def test_round_trip_maximum(self): """ Test to assert stability of values through round-trips """ for value in range(65536): with self.subTest(int=value): result = channels_to_ints( ints_to_channels( [value], maximum=65535, ), maximum=65535, ) self.assertEqual(result, (value,)) class TestRelativeLuminance(TestCase): def test_white(self): rgb = (1.0, 1.0, 1.0) result = relative_luminance(rgb) self.assertEqual(result, 1.0) def test_black(self): rgb = (0.0, 0.0, 0.0) result = relative_luminance(rgb) self.assertEqual(result, 0.0) def test_red(self): rgb = (1.0, 0.0, 0.0) result = relative_luminance(rgb) self.assertAlmostEqual(result, 0.2126) def test_green(self): rgb = (0.0, 1.0, 0.0) result = relative_luminance(rgb) self.assertAlmostEqual(result, 0.7152) def test_blue(self): rgb = (0.0, 0.0, 1.0) result = relative_luminance(rgb) self.assertAlmostEqual(result, 0.0722) def test_yellow(self): rgb = (1.0, 1.0, 0.0) result = relative_luminance(rgb) self.assertAlmostEqual(result, 0.2126 + 0.7152) def test_cyan(self): rgb = (0.0, 1.0, 1.0) result = relative_luminance(rgb) self.assertAlmostEqual(result, 0.7152 + 0.0722) def test_magenta(self): rgb = (1.0, 0.0, 1.0) result = relative_luminance(rgb) self.assertAlmostEqual(result, 0.2126 + 0.0722) def test_dark_grey(self): rgb = (0.01, 0.01, 0.01) result = relative_luminance(rgb) self.assertAlmostEqual(result, 0.01/12.92) def test_medium_grey(self): rgb = (0.5, 0.5, 0.5) result = relative_luminance(rgb) self.assertAlmostEqual(result, (0.555/1.055)**2.4) class TestIsDark(TestCase): def test_white(self): rgb = (1.0, 1.0, 1.0) result = is_dark(rgb) self.assertFalse(result) def test_black(self): rgb = (0.0, 0.0, 0.0) result = is_dark(rgb) self.assertTrue(result) def test_red(self): rgb = (1.0, 0.0, 0.0) result = is_dark(rgb) self.assertFalse(result) def test_green(self): rgb = (0.0, 1.0, 0.0) result = is_dark(rgb) self.assertFalse(result) def test_blue(self): rgb = (0.0, 0.0, 1.0) result = is_dark(rgb) self.assertTrue(result) def test_yellow(self): rgb = (1.0, 1.0, 0.0) result = is_dark(rgb) self.assertFalse(result) def test_cyan(self): rgb = (0.0, 1.0, 1.0) result = is_dark(rgb) self.assertFalse(result) def test_magenta(self): rgb = (1.0, 0.0, 1.0) result = is_dark(rgb) self.assertFalse(result) def test_dark_grey(self): rgb = (0.01, 0.01, 0.01) result = is_dark(rgb) self.assertTrue(result) def test_medium_grey(self): rgb = (0.5, 0.5, 0.5) result = is_dark(rgb) self.assertFalse(result) pyface-7.4.0/pyface/util/tests/test_font_parser.py0000644000076500000240000001465214176222673023275 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 itertools import chain, combinations from unittest import TestCase from ..font_parser import ( DECORATIONS, GENERIC_FAMILIES, NOISE, STRETCHES, STYLES, VARIANTS, WEIGHTS, simple_parser, ) class TestSimpleParser(TestCase): def test_empty(self): properties = simple_parser("") self.assertEqual( properties, { 'family': ["default"], 'size': 12.0, 'weight': "normal", 'stretch': "normal", 'style': "normal", 'variants': set(), 'decorations': set(), }, ) def test_typical(self): properties = simple_parser( "10 pt bold condensed italic underline Helvetica sans-serif") self.assertEqual( properties, { 'family': ["helvetica", "sans-serif"], 'size': 10.0, 'weight': "bold", 'stretch': "condensed", 'style': "italic", 'variants': set(), 'decorations': {"underline"}, }, ) def test_noise(self): for noise in NOISE: with self.subTest(noise=noise): properties = simple_parser(noise) self.assertEqual( properties, { 'family': ["default"], 'size': 12.0, 'weight': "normal", 'stretch': "normal", 'style': "normal", 'variants': set(), 'decorations': set(), }, ) def test_generic_families(self): for family in GENERIC_FAMILIES: with self.subTest(family=family): properties = simple_parser(family) self.assertEqual( properties, { 'family': [family], 'size': 12.0, 'weight': "normal", 'stretch': "normal", 'style': "normal", 'variants': set(), 'decorations': set(), }, ) def test_size(self): for size in [12, 24, 12.5]: with self.subTest(size=size): properties = simple_parser(str(size)) self.assertEqual( properties, { 'family': ["default"], 'size': size, 'weight': "normal", 'stretch': "normal", 'style': "normal", 'variants': set(), 'decorations': set(), }, ) def test_weight(self): for weight in WEIGHTS: with self.subTest(weight=weight): properties = simple_parser(weight) self.assertEqual( properties, { 'family': ["default"], 'size': 12.0, 'weight': weight, 'stretch': "normal", 'style': "normal", 'variants': set(), 'decorations': set(), }, ) def test_stretch(self): for stretch in STRETCHES: with self.subTest(stretch=stretch): properties = simple_parser(stretch) self.assertEqual( properties, { 'family': ["default"], 'size': 12.0, 'weight': "normal", 'stretch': stretch, 'style': "normal", 'variants': set(), 'decorations': set(), }, ) def test_style(self): for style in STYLES: with self.subTest(style=style): properties = simple_parser(style) self.assertEqual( properties, { 'family': ["default"], 'size': 12.0, 'weight': "normal", 'stretch': "normal", 'style': style, 'variants': set(), 'decorations': set(), }, ) def test_variant(self): for variant in VARIANTS: with self.subTest(variant=variant): properties = simple_parser(variant) self.assertEqual( properties, { 'family': ["default"], 'size': 12.0, 'weight': "normal", 'stretch': "normal", 'style': "normal", 'variants': {variant}, 'decorations': set(), }, ) def test_decorations(self): # get powerset iterator of DECORATIONS all_decorations = chain.from_iterable( combinations(DECORATIONS, n) for n in range(len(DECORATIONS) + 1) ) for decorations in all_decorations: with self.subTest(decorations=decorations): properties = simple_parser(" ".join(decorations)) self.assertEqual( properties, { 'family': ["default"], 'size': 12.0, 'weight': "normal", 'stretch': "normal", 'style': "normal", 'variants': set(), 'decorations': set(decorations), }, ) pyface-7.4.0/pyface/util/tests/test_gui_test_assistant.py0000644000076500000240000000143014176222673024655 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 import unittest from pyface.toolkit import toolkit is_wx = (toolkit.toolkit == "wx") class TestGuiTestAssistant(unittest.TestCase): @unittest.skipIf(is_wx, "wx is not supported") def test_import(self): from pyface.util.gui_test_assistant import GuiTestAssistant self.assertNotEqual(GuiTestAssistant.__name__, "Unimplemented") pyface-7.4.0/pyface/util/tests/test_id_helper.py0000644000076500000240000000274714176222673022710 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 scripting tools. """ import unittest from pyface.util.id_helper import get_unique_id, object_counter class IDHelperTestCase(unittest.TestCase): """ Test the scripting tools. """ # Tests ---------------------------------------------------------------- def test_object_counter(self): from traits.api import WeakRef class Bogus(object): weak = WeakRef class Foo(object): foo = 3 foo = Foo() self.assertEqual(object_counter.get_count(Bogus), 0) self.assertEqual(object_counter.next_count(Bogus), 1) self.assertEqual(object_counter.next_count(Bogus), 2) self.assertEqual(object_counter.get_count(Bogus), 2) self.assertEqual(object_counter.next_count(foo), 1) self.assertEqual(object_counter.next_count(Bogus), 3) def test_get_unique_id(self): class Bogus(object): pass bogus_1 = Bogus() bogus_2 = Bogus() self.assertEqual(get_unique_id(bogus_1), "Bogus_1") self.assertEqual(get_unique_id(bogus_2), "Bogus_2") pyface-7.4.0/pyface/util/tests/test_color_parser.py0000644000076500000240000000547114176222673023444 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 ..color_parser import ( ColorParseError, color_table, parse_text, _parse_hex, _parse_name ) class TestParseHex(TestCase): def test_hex_3(self): space, channels = _parse_hex('#06c') self.assertEqual(space, 'rgb') self.assertEqual(channels, (0.0, 0.4, 0.8)) def test_hex_4(self): space, channels = _parse_hex('#06cf') self.assertEqual(space, 'rgba') self.assertEqual(channels, (0.0, 0.4, 0.8, 1.0)) def test_hex_6(self): space, channels = _parse_hex('#0066cc') self.assertEqual(space, 'rgb') self.assertEqual(channels, (0.0, 0.4, 0.8)) def test_hex_8(self): space, channels = _parse_hex('#0066ccff') self.assertEqual(space, 'rgba') self.assertEqual(channels, (0.0, 0.4, 0.8, 1.0)) def test_hex_12(self): space, channels = _parse_hex('#00006666cccc') self.assertEqual(space, 'rgb') self.assertEqual(channels, (0.0, 0.4, 0.8)) def test_hex_16(self): space, channels = _parse_hex('#00006666ccccffff') self.assertEqual(space, 'rgba') self.assertEqual(channels, (0.0, 0.4, 0.8, 1.0)) def test_hex_bad(self): result = _parse_hex('#0c') self.assertIsNone(result) class TestParseName(TestCase): def test_names(self): for name, value in color_table.items(): with self.subTest(color=name): space, channels = _parse_name(name) self.assertEqual(space, 'rgba') self.assertEqual(channels, value) def test_name_space(self): space, channels = _parse_name('rebecca purple') self.assertEqual(space, 'rgba') self.assertEqual(channels, (0.4, 0.2, 0.6, 1.0)) def test_name_capitals(self): space, channels = _parse_name('RebeccaPurple') self.assertEqual(space, 'rgba') self.assertEqual(channels, (0.4, 0.2, 0.6, 1.0)) class TestParseText(TestCase): def test_name(self): space, channels = parse_text('rebeccapurple') self.assertEqual(space, 'rgba') self.assertEqual(channels, (0.4, 0.2, 0.6, 1.0)) def test_hex(self): space, channels = parse_text('#663399ff') self.assertEqual(space, 'rgba') self.assertEqual(channels, (0.4, 0.2, 0.6, 1.0)) def test_error(self): with self.assertRaises(ColorParseError): parse_text('invalidcolorname') pyface-7.4.0/pyface/util/tests/test__optional_dependencies.py0000644000076500000240000000272414176222673025442 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 _optional_dependencies module """ import logging import unittest from pyface.util._optional_dependencies import optional_import class TestOptionalImport(unittest.TestCase): """ Test optional import context manager """ def test_optional_import(self): # Test excusing dependency and the logging behaviour logger = logging.getLogger(self.id()) with self.assertLogs(logger, level="DEBUG") as log_context: with optional_import( "random_missing_lib", "fail to import", logger): # assume this library is not importable. import random_missing_lib # noqa: F401 log, = log_context.output self.assertIn("fail to import", log) def test_optional_import_reraise(self): # Test if the import error was about something else, reraise logger = logging.getLogger(self.id()) with self.assertRaises(ImportError): with optional_import("some_random_lib", "", logger): import some_random_missing_lib # noqa: F401 pyface-7.4.0/pyface/util/tests/test_event_loop_helper.py0000644000076500000240000000142414176222673024455 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 import unittest from pyface.toolkit import toolkit is_wx = (toolkit.toolkit == "wx") class TestEventLoopHelper(unittest.TestCase): @unittest.skipIf(is_wx, "wx is not supported") def test_import(self): from pyface.util.event_loop_helper import EventLoopHelper self.assertNotEqual(EventLoopHelper.__name__, "Unimplemented") pyface-7.4.0/pyface/util/__init__.py0000644000076500000240000000062714176222673020306 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! pyface-7.4.0/pyface/util/gui_test_assistant.py0000644000076500000240000000115614176222673022461 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 implementation of a Gui Test Assistant. """ # Import the toolkit specific version. from pyface.toolkit import toolkit_object GuiTestAssistant = toolkit_object("util.gui_test_assistant:GuiTestAssistant") pyface-7.4.0/pyface/util/_optional_dependencies.py0000644000076500000240000000227714176222673023244 0ustar cwebsterstaff00000000000000 # (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Utilities for handling optional import dependencies.""" import contextlib @contextlib.contextmanager def optional_import(dependency_name, msg, logger): """ Context manager for capturing ImportError for a particular optional dependency. If such an error occurs, it will be silenced and a debug message will be logged. Parameters ---------- dependency_name : str Name of the module that may fail to be imported. If matched, the ImportError will be silenced msg : str Log message to be emitted. logger : Logger Logger to use for logging messages. """ try: yield except ImportError as exception: if exception.name == dependency_name: logger.debug(msg, exc_info=True) else: raise pyface-7.4.0/pyface/util/fix_introspect_bug.py0000644000076500000240000001313314176222673022440 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 adds a fix for wx.py's introspect module. In order to do code-completion, the function `introspect.getAttributeName` accesses all the attributes of the current object. This causes severe problems for modules like tvtk which depend on lazy importing. The original introspect module also has severe problems with large Numeric arrays because it calls str() on the Numeric object in order to find its methods. This file defines a fixed function that works fine with lazy objects and large Numeric arrays. This fixed function is injected into the introspect module. """ # Import introspect. from wx.py import introspect # The fixed function. def getAttributeNames( object, includeMagic=1, includeSingle=1, includeDouble=1 ): """Return list of unique attributes, including inherited, for object.""" attributes = [] dict = {} if not introspect.hasattrAlwaysReturnsTrue(object): # Add some attributes that don't always get picked up. special_attrs = [ "__bases__", "__class__", "__dict__", "__name__", "__closure__", "__code__", "___kwdefaults__", "__doc__", "__globals__", ] attributes += [attr for attr in special_attrs if hasattr(object, attr)] # For objects that have traits, get all the trait names since # these do not show up in dir(object). if hasattr(object, "trait_names"): try: attributes += object.trait_names() except TypeError: pass if includeMagic: try: attributes += object._getAttributeNames() except: pass # Get all attribute names. attrdict = getAllAttributeNames(object) # Store the object's dir. object_dir = dir(object) for (obj_type_name, technique, count), attrlist in attrdict.items(): # This complexity is necessary to avoid accessing all the # attributes of the object. This is very handy for objects # whose attributes are lazily evaluated. if type(object).__name__ == obj_type_name and technique == "dir": attributes += attrlist else: attributes += [ attr for attr in attrlist if attr not in object_dir and hasattr(object, attr) ] # Remove duplicates from the attribute list. for item in attributes: dict[item] = None attributes = list(dict.keys()) # new-style swig wrappings can result in non-string attributes # e.g. ITK http://www.itk.org/ attributes = [ attribute for attribute in attributes if isinstance(attribute, str) ] attributes.sort(key=lambda x: x.upper()) if not includeSingle: attributes = filter( lambda item: item[0] != "_" or item[1] == "_", attributes ) if not includeDouble: attributes = filter(lambda item: item[:2] != "__", attributes) return attributes # Replace introspect's version with ours. introspect.getAttributeNames = getAttributeNames # This is also a modified version of the function which does not use # str(object). def getAllAttributeNames(obj): """Return dict of all attributes, including inherited, for an object. Recursively walk through a class and all base classes. """ attrdict = {} # (object, technique, count): [list of attributes] # !!! # Do Not use hasattr() as a test anywhere in this function, # because it is unreliable with remote objects: xmlrpc, soap, etc. # They always return true for hasattr(). # !!! try: # This could(?) fail if the type is poorly defined without # even a name. key = type(obj).__name__ except: key = "anonymous" # Wake up sleepy objects - a hack for ZODB objects in "ghost" state. wakeupcall = dir(obj) del wakeupcall # Get attributes available through the normal convention. attributes = dir(obj) attrdict[(key, "dir", len(attributes))] = attributes # Get attributes from the object's dictionary, if it has one. try: attributes = list(obj.__dict__.keys()) attributes.sort() except: # Must catch all because object might have __getattr__. pass else: attrdict[(key, "__dict__", len(attributes))] = attributes # For a class instance, get the attributes for the class. try: klass = obj.__class__ except: # Must catch all because object might have __getattr__. pass else: if klass is obj: # Break a circular reference. This happens with extension # classes. pass else: attrdict.update(getAllAttributeNames(klass)) # Also get attributes from any and all parent classes. try: bases = obj.__bases__ except: # Must catch all because object might have __getattr__. pass else: if isinstance(bases, tuple): for base in bases: if isinstance(base, (tuple, object)): # Break a circular reference. Happens in Python 2.2. & 3.6 (prob others) pass else: attrdict.update(getAllAttributeNames(base)) return attrdict pyface-7.4.0/pyface/util/color_helpers.py0000644000076500000240000000715714176222673021414 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Routines supporting color computations Most of what is needed is provided by Python's builtin colorsys module, but we need a few additional routines for things that are not covered by that code. """ def channels_to_ints(channels, maximum=255): """ Convert an iterable of floating point channel values to integers. Values are rounded to the nearest integer, rather than truncated. Parameters ---------- channels : iterable of float An iterable of channel values, each value between 0.0 and 1.0, inclusive. maximum : int The maximum value of the integer range. Common values are 15, 65535 or 255, which is the default. Returns ------- values : tuple of int A tuple of values as integers between 0 and max, inclusive. """ return tuple(int(round(channel * maximum)) for channel in channels) def ints_to_channels(values, maximum=255): """ Convert an iterable of integers to floating point channel values. Parameters ---------- values : tuple of int An iterable of values as integers between 0 and max, inclusive. maximum : int The maximum value of the integer range. Common values are 15, 65535 or 255, which is the default. Returns ------- channels : iterable of float A tuple of channel values, each value between 0.0 and 1.0, inclusive. """ return tuple(value / maximum for value in values) def relative_luminance(rgb): """ The relative luminance of the color. This value is the critical value when comparing colors for contrast when displayed next to each other, in particular for readability of text. Parameters ---------- rgb : tuple of red, green, blue values A tuple of values representing red, green and blue components of the color, as values from 0.0 to 1.0. Returns ------- luminance : float The relative luminance of the color. References ---------- Web Contrast Accessibility Guidelines https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef """ gamma_corrected = [ x/12.92 if x <= 0.03928 else ((x + 0.055)/1.055)**2.4 for x in rgb ] luminance = ( 0.2126 * gamma_corrected[0] + 0.7152 * gamma_corrected[1] + 0.0722 * gamma_corrected[2] ) return luminance def is_dark(rgb): """ Is the color dark to human perception? A color is dark if white contasts better with it according to the WC3 definition of contrast ratio. This is allows GUI code to choose either black or white as a contrasting color for things like text on a colored background. Parameters ---------- rgb : tuple of red, green, blue values A tuple of values representing red, green and blue components of the color, as values from 0.0 to 1.0. References ---------- Understanding Web Contrast Accessibility Guidelines https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html#contrast-ratiodef """ lumininance = relative_luminance(rgb) black_contrast = (lumininance + 0.05) / 0.05 white_contrast = 1.05 / (lumininance + 0.05) return white_contrast > black_contrast pyface-7.4.0/pyface/util/testing.py0000644000076500000240000000427414176222673020226 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 functools import wraps import re from unittest import TestSuite from packaging.version import Version from traits import __version__ as TRAITS_VERSION 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 has_traitsui(): """ Is traitsui installed and sufficiently recent? """ try: import traitsui # noqa: F401 except ImportError: return False from pyface.toolkit import toolkit if toolkit.toolkit.startswith("qt"): from pyface.qt import is_qt6 if is_qt6: return Version(traitsui.__version__) >= Version("7.4") return True def skip_if_no_traitsui(test): """ Decorator that skips test if traitsui not available """ @wraps(test) def new_test(self): if has_traitsui(): test(self) else: self.skipTest("Can't import traitsui.") return new_test def is_traits_version_ge(version): """ Return true if the traits version is greater than or equal to the required value. Parameters ---------- version : str Version to be parsed. e.g. "6.0" """ traits_version = Version(TRAITS_VERSION) given_version = Version(version) return traits_version >= given_version pyface-7.4.0/pyface/util/font_parser.py0000644000076500000240000001066214176222673021071 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! GENERIC_FAMILIES = { 'default', 'fantasy', 'decorative', 'serif', 'roman', 'cursive', 'script', 'sans-serif', 'swiss', 'monospace', 'modern', 'typewriter', 'teletype' } WEIGHTS = { 'thin', 'extra-light', 'light', 'regular', 'medium', 'demi-bold', 'bold', 'extra-bold', 'heavy', 'extra-heavy' } STRETCHES = { 'ultra-condensed', 'extra-condensed', 'condensed', 'semi-condensed', 'semi-expanded', 'expanded', 'extra-expanded', 'ultra-expanded' } STYLES = {'italic', 'oblique'} VARIANTS = {'small-caps'} DECORATIONS = {'underline', 'strikethrough', 'overline'} NOISE = {'pt', 'point', 'px', 'family'} class FontParseError(ValueError): """An exception raised when font parsing fails.""" pass def simple_parser(description): """An extremely simple font description parser. The parser is simple, and works by splitting the description on whitespace and examining each resulting token for understood terms: Size The first numeric term is treated as the font size. Weight The following weight terms are accepted: 'thin', 'extra-light', 'light', 'regular', 'medium', 'demi-bold', 'bold', 'extra-bold', 'heavy', 'extra-heavy'. Stretch The following stretch terms are accepted: 'ultra-condensed', 'extra-condensed', 'condensed', 'semi-condensed', 'semi-expanded', 'expanded', 'extra-expanded', 'ultra-expanded'. Style The following style terms are accepted: 'italic', 'oblique'. Variant The following variant terms are accepted: 'small-caps'. Decorations The following decoration terms are accepted: 'underline', 'strikethrough', 'overline'. Generic Families The following generic family terms are accepted: 'default', 'fantasy', 'decorative', 'serif', 'roman', 'cursive', 'script', 'sans-serif', 'swiss', 'monospace', 'modern', 'typewriter', 'teletype'. In addtion, the parser ignores the terms 'pt', 'point', 'px', and 'family'. Any remaining terms are combined into the typeface name. There is no expected order to the terms. This parser is roughly compatible with the various ad-hoc parsers in TraitsUI and Kiva, allowing for the slight differences between them and adding support for additional options supported by Pyface fonts, such as stretch and variants. Parameters ---------- description : str The font description to be parsed. Returns ------- properties : dict Font properties suitable for use in creating a Pyface Font. Notes ----- This is not a particularly good parser, as it will fail to properly parse something like "10 pt times new roman" or "14 pt computer modern" since they have generic font names as part of the font face name. """ face = [] generic_family = "" size = None weight = "normal" stretch = "normal" style = "normal" variants = set() decorations = set() for word in description.lower().split(): if word in NOISE: continue elif word in GENERIC_FAMILIES: generic_family = word elif word in WEIGHTS: weight = word elif word in STRETCHES: stretch = word elif word in STYLES: style = word elif word in VARIANTS: variants.add(word) elif word in DECORATIONS: decorations.add(word) else: if size is None: try: size = float(word) continue except ValueError: pass face.append(word) family = [] if face: family.append(" ".join(face)) if generic_family: family.append(generic_family) if not family: family = ["default"] if size is None: size = 12 return { 'family': family, 'size': size, 'weight': weight, 'stretch': stretch, 'style': style, 'variants': variants, 'decorations': decorations, } pyface-7.4.0/pyface/system_metrics.py0000644000076500000240000000117514176222673020643 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 implementation of access to system metrics (screen width and height etc). """ # Import the toolkit specific version. from .toolkit import toolkit_object SystemMetrics = toolkit_object("system_metrics:SystemMetrics") pyface-7.4.0/pyface/i_image_resource.py0000644000076500000240000001177714176222673021103 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 interface for an image resource. """ from collections.abc import Sequence from traits.api import HasTraits, List, Str from pyface.i_image import IImage from pyface.resource_manager import resource_manager from pyface.resource.resource_path import resource_module, resource_path class IImageResource(IImage): """ The interface for an image resource. An image resource describes the location of an image and provides a way to create a toolkit-specific image on demand. """ # 'ImageResource' interface -------------------------------------------- #: The absolute path to the image. absolute_path = Str() #: The name of the image. name = Str() #: A list of directories, classes or instances that will be used to search #: for the image (see the resource manager for more details). search_path = List() @classmethod def image_size(cls, image): """ Get the size of a toolkit image Parameters ---------- image : toolkit image A toolkit image to compute the size of. Returns ------- size : tuple The (width, height) tuple giving the size of the image. """ class MImageResource(HasTraits): """ The mixin class that contains common code for toolkit specific implementations of the IImageResource interface. Implements: __init__(), create_image() """ # Private interface ---------------------------------------------------- #: The image-not-found image. Note that it is not a trait. _image_not_found = None # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, name, search_path=None): self.name = name if isinstance(search_path, str): _path = [search_path] elif isinstance(search_path, Sequence): _path = search_path elif search_path is not None: _path = [search_path] else: _path = [resource_path()] self.search_path = _path + [resource_module()] # ------------------------------------------------------------------------ # 'ImageResource' interface. # ------------------------------------------------------------------------ def create_image(self, size=None): """ Creates a toolkit-specific image for this resource. Parameters ---------- size : (int, int) or None The desired size as a width, height tuple, or None if wanting default image size. Returns ------- image : toolkit image The toolkit image corresponding to the resource and the specified size. """ ref = self._get_ref(size) if ref is not None: image = ref.load() else: image = self._get_image_not_found_image() return image # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _get_ref(self, size=None): """ Return the resource manager reference to the image. Parameters ---------- size : (int, int) or None The desired size as a width, height tuple, or None if wanting default image size. Returns ------- ref : ImageReference instance The reference to the requested image. """ if self._ref is None: self._ref = resource_manager.locate_image( self.name, self.search_path, size ) return self._ref def _get_image_not_found_image(self): """ Returns the 'image not found' image. Returns ------- image : toolkit image The 'not found' toolkit image. """ not_found = self._get_image_not_found() if self is not not_found: image = not_found.create_image() else: raise ValueError("cannot locate the file for 'image_not_found'") return image @classmethod def _get_image_not_found(cls): """ Returns the 'image not found' image resource. Returns ------- not_found : ImageResource instance An image resource for the the 'not found' image. """ if cls._image_not_found is None: from pyface.image_resource import ImageResource cls._image_not_found = ImageResource("image_not_found") return cls._image_not_found pyface-7.4.0/pyface/_version.py0000644000076500000240000000147514176460546017423 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 IS GENERATED FROM SETUP.PY #: The full version of the package, including a development suffix #: for unreleased versions of the package. version = '7.4.0' #: The full version of the package, same as 'version' #: Kept for backward compatibility full_version = version #: The Git revision from which this release was made. git_revision = 'Unknown' #: Flag whether this is a final release is_released = True pyface-7.4.0/pyface/i_message_dialog.py0000644000076500000240000000231014176222673021034 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 interface for a dialog that displays a message. """ from traits.api import Enum, HasTraits, Str from pyface.i_dialog import IDialog class IMessageDialog(IDialog): """ The interface for a dialog that displays a message. """ # 'IMessageDialog' interface ------------------------------------------- #: The message to display in the dialog. message = Str() #: More information about the message to be displayed. informative = Str() #: More detail about the message to be displayed in the dialog. detail = Str() #: The severity of the message. severity = Enum("information", "warning", "error") class MMessageDialog(HasTraits): """ The mixin class that contains common code for toolkit specific implementations of the IMessageDialog interface. """ pyface-7.4.0/pyface/dialog.py0000644000076500000240000000111414176222673017021 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 abstract implementation of all pyface dialogs. """ # Import the toolkit specific version. from .toolkit import toolkit_object Dialog = toolkit_object("dialog:Dialog") pyface-7.4.0/pyface/i_split_widget.py0000644000076500000240000001010614176222673020571 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Mix-in class for split widgets. """ from traits.api import Callable, Float, HasTraits, Interface from pyface.ui_traits import Orientation class ISplitWidget(Interface): """ Mix-in class for split widgets. A split widget is one that is split in two either horizontally or vertically. """ # 'ISplitWidget' interface --------------------------------------------- #: The direction in which the widget is split. # #: Splitting vertically means there will be a left hand panel and a right #: hand panel, splitting horizontally means there will be a top panel and #: a bottom panel. direction = Orientation() #: The ratio of the size of the left/top pane to the right/bottom pane. ratio = Float(0.5) #: An optional callable that provides the left hand/top panel. lhs = Callable #: An optional callable that provides the right hand/bottom panel. rhs = Callable # ------------------------------------------------------------------------ # Protected 'ISplitWidget' interface. # ------------------------------------------------------------------------ def _create_splitter(self, parent): """ Create the toolkit-specific control that represents the widget. Parameters ---------- parent : toolkit control The toolkit control that contains the splitter. Returns ------- splitter : toolkit control The toolkit control for the splitter. """ def _create_lhs(self, parent): """ Creates the left hand/top panel depending on the direction. Parameters ---------- parent : toolkit control The splitter's toolkit control. Returns ------- lhs : toolkit control The toolkit control for the lhs. """ def _create_rhs(self, parent): """ Creates the right hand/bottom panel depending on the direction. Parameters ---------- parent : toolkit control The splitter's toolkit control. Returns ------- rhs : toolkit control The toolkit control for the rhs. """ class MSplitWidget(HasTraits): """ The mixin class that contains common code for toolkit specific implementations of the ISplitWidget interface. """ # 'ISplitWidget' interface --------------------------------------------- #: The direction in which the widget is split. # #: Splitting vertically means there will be a left hand panel and a right #: hand panel, splitting horizontally means there will be a top panel and #: a bottom panel. direction = Orientation() #: The ratio of the size of the left/top pane to the right/bottom pane. ratio = Float(0.5) #: An optional callable that provides the left hand/top panel. lhs = Callable #: An optional callable that provides the right hand/bottom panel. rhs = Callable def _create_lhs(self, parent): """ Creates the left hand/top panel depending on the direction. Parameters ---------- parent : toolkit control The splitter's toolkit control. Returns ------- lhs : toolkit control The toolkit control for the lhs. """ raise NotImplementedError() def _create_rhs(self, parent): """ Creates the right hand/bottom panel depending on the direction. Parameters ---------- parent : toolkit control The splitter's toolkit control. Returns ------- rhs : toolkit control The toolkit control for the rhs. """ raise NotImplementedError() pyface-7.4.0/pyface/i_system_metrics.py0000644000076500000240000000230614176222673021150 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 interface to system metrics (screen width and height etc). """ from traits.api import HasTraits, Int, Interface, Tuple class ISystemMetrics(Interface): """ The interface to system metrics (screen width and height etc). """ # 'ISystemMetrics' interface ------------------------------------------- #: The width of the screen in pixels. screen_width = Int() #: The height of the screen in pixels. screen_height = Int() #: Background color of a standard dialog window as a tuple of RGB values #: between 0.0 and 1.0. # FIXME v3: Why isn't this a traits colour? dialog_background_color = Tuple() class MSystemMetrics(HasTraits): """ The mixin class that contains common code for toolkit specific implementations of the ISystemMetrics interface. """ pyface-7.4.0/pyface/images/0000755000076500000240000000000014176460550016456 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/images/question.png0000644000076500000240000000347114176222673021042 0ustar cwebsterstaff00000000000000PNG  IHDR szzsBIT|d pHYsu85tEXtSoftwarewww.inkscape.org<IDATXŗkW37v]ZdA6]JiXUhR?E mMڦ!-ibh4l$r6tP{qvwvg\3;evRbINs|%u;{Qʇ!:|wJs|gRNoh&07S`]ajrR\.7䦊;E_OOo+ϭvN% *!ǕNL^;K?[^L޷6ս$5(ApSb,RgׂMz~O>k1DƦiNϢN^J{}TEtS{(oI)xTw->jG7 챶.~֘T:[ǺEX5gƘ9ʉѓr')8GW>׷if{[;Ye1UH;+~|Wgv081X鿴S|oP˻M*S‚fW ~D 6El#j7wqB1pq^t%ƊwQViLw2Vt?]mDDD}M~e7oz&Cs-  `ooc]Heo}OֵV_ e0~30\,VˈSt'bT.^wPEƇ>rbf1nd5{[6$)%cM)7s(_QJK7!""GN-U3t}Rj¶k`0dZ+w5cwxthoI4 X=xٽ~;'/^w弹8 Ѕz5: '/y+iͶАg}xU")}$&7r)+#>k&ŀ3$ __x-kۖI=0 X^{8=Z Mc ܗ#5 ߟz:y/X}E>f[iܰsceKKtFofŦΆgIVK]ju֐I#6 T&vGIᛉ{UIBtt|!;rp򨪆듾_7 _K.IENDB`pyface-7.4.0/pyface/images/background.jpg0000644000076500000240000007052414176222673021311 0ustar cwebsterstaff00000000000000JFIFHHExifMM*bj(1r2iHHAdobe Photoshop 7.02004:11:12 11:44:20d(&dHHJFIFHH Adobe_CMAdobed            "?   3!1AQa"q2B#$Rb34rC%Scs5&DTdE£t6UeuF'Vfv7GWgw5!1AQaq"2B#R3$brCScs4%&5DTdEU6teuFVfv'7GWgw ?e%I9c7$$e%I9cI$I$I$I$I$LPhotoshop 3.08BIM%8BIMHH8BIM&?8BIM 8BIM8BIM 8BIM 8BIM' 8BIMH/fflff/ff2Z5-8BIMp8BIM8BIM8BIM@@8BIM8BIMId backgrounddnullboundsObjcRct1Top longLeftlongBtomlongRghtlongdslicesVlLsObjcslicesliceIDlonggroupIDlongoriginenum ESliceOrigin autoGeneratedTypeenum ESliceTypeImg boundsObjcRct1Top longLeftlongBtomlongRghtlongdurlTEXTnullTEXTMsgeTEXTaltTagTEXTcellTextIsHTMLboolcellTextTEXT horzAlignenumESliceHorzAligndefault vertAlignenumESliceVertAligndefault bgColorTypeenumESliceBGColorTypeNone topOutsetlong leftOutsetlong bottomOutsetlong rightOutsetlong8BIM8BIM8BIM  dJFIFHH Adobe_CMAdobed            "?   3!1AQa"q2B#$Rb34rC%Scs5&DTdE£t6UeuF'Vfv7GWgw5!1AQaq"2B#R3$brCScs4%&5DTdEU6teuFVfv'7GWgw ?e%I9c7$$e%I9cI$I$I$I$I$8BIM!UAdobe PhotoshopAdobe Photoshop 7.08BIMHhttp://ns.adobe.com/xap/1.0/ adobe:docid:photoshop:ad8d0f28-34ca-11d9-8347-878451da9c84 Adobed@d      u!"1A2# QBa$3Rqb%C&4r 5'S6DTsEF7Gc(UVWdte)8fu*9:HIJXYZghijvwxyzm!1"AQ2aqB#Rb3 $Cr4%ScD&5T6Ed' sFtUeuV7)(GWf8vgwHXhx9IYiy*:JZjz ?7A{{^׺u~z׺u{{^׺u{3oAu}u?￧u}{w]{{w'޺^׿O~{=u_~uu_(AV}y~{'=u:]_7o>z˯GSu?_~뫛mzv }Ͽcu>׺?:˯_}{(ןgWOu޺S>ׇ_﷯yuc{{^׉}ϯq׿?_~:+׿ߺ=zw}޼׹yy[~^z qןgנz@{^$u _ǯ|_ǿy}o.ׯ^}ǽuxcj{E'?>ozZz|?˭ӯ93׳_׿uuw}uz׺u}{}zwu~?{^A{{^ׯ{_^u~z׺u}{{^׺߳=zֺ}^zb=^]s׾}uqׇ^oߺϯ_߿yu=c}{# g93ׯz׺Ǯ׏]_?>C֩ߟg?>'ׯשO}{_ߏ˯u~{ߺ]]u~:^z{o߾}{_ϿuA~{߾}{_ߺ^ׯz{z׸~{ߺ^ׯ~)sfsuﳯ_5^ֺ#o~~}uO׾λ޺^~?ǯ_u,u~o~׷~g]~}=|}Ooz.޽gMzOw}߼׼?ǟ~]{ϿW?/~^?>׮/N?[{^ï}={?={ϯ__~<:^~پ}t^u{o^u^}{_߼^c]{~={qOw}o{^ٶz {}z׺׺u߿uq{ׯ{#޺]_7#u׽uZ٫]ּ߳h}~{[u~?^:^6[ߺzyW}~{{^9GW޺:{νOO׸cm_O٢og]~μ?ߺ~׾κ7+9zxׯ{w~׸u?޾]{u~uUuKqy^ٚΩg_ǽu{z:^ׯǯu^ﳯu~^z׺sߺ_ِ>κ {^_׺~u~~֫w=z{׾7}{ϯ_#׿~׺z_ي?ucᄒOu_}o~ws#{-n=zmo~g]}?ꎽ<ׯo{xO߽u^׸u?>_>N~αg_ߺ^uzz}k_u}{}{#ׯu~>{_xurO:=zu_~z}u^|=uׯ'ߺ^'߾}{_?v^{z׺:_jGg]r8_N uluAׯ^}}yyzz~`~ߺ~?N=u?׺{{z'k}mz^>gsz]z3ϿuWׯ_EϿu{_W޺]_:O־޽￧u׽޺^xazWϠ^{^}~z׺u}uwWZ^}{_{_޺^q־]JQ.=z[O~>{^}~{:7oN}_:^_~u]߽Wߗ^}˭u~}uu[&ר:B>Og}:O[:o{_ߺ^ߺ^z^[ߏ^{_#u﷯_^:k/6Ogc^u_~z=^:^~}wz]u^刺u?~}z{Z.wgaן~u=Vwq{[#Ͻu^{ߗZ~}??ׯokߺ:}?@c{Wzczן: Wz>~]z?׿_ֱǯ_o{{)?^:?}{_ߺ׺o~~]z׷\:]\}u uz^~{_ߺ^yu]+kވw{}zqu{z{ZO: yOo_#}u~~u=zaO^y{{:^m~=zO~]{~ߺ٧^=Xׯ<}~[?<}=o~N}yuϯso׿uׯ>׺>}8Wzw]_~}wu.q9}ou??uu: u^ߺ]￯uu.u~:ϯ_ߺ]ߺ]_ߺˮ]{z<]_﷯u~د^#=S޿ϿuOuW~]vOGí}zïycO~gOߺ+ׯOz]ygׯ׫׿^z>ǯuu{_Mu.ׯZ:uu?Žغ: uu^ׯׯE{_ߺ^Ou~z׺׺u׺خ__g:'^]?~{o͸{׾=ʽoo~ֺo^wxǯ^׿c` o{ߟ~mo_sWuآA}{ׯßǽu:uׯ#]{_7ͽyu^ӟߏz]zk~u^ؚz u׿u׺u^w^׺u[ׯ#^Su؎?=z Pu?}׺H׺W|?ߺu{z^}˭c{ߺ]Y]_{ϯu}9=]z}>׫؂z uߺ˯_'߾]k׿[ߺ^'^:˭1׿-׺Z+׿ׯ{4_vz g_߸Wׯߺ^uuׯߺ^}u['ߺ]ߺ_nןg1^_ߺ]}{z{z:~Ɲu~o_|9^Qw}ߺ_Ã:)Nr~ߺז:ߺzk=b﾿_g: yu˯u׿~?ï_uׯ{5^z^Oߟqzׯ^}{V゙z uu^}u^^^>׺Ƚu^wo{_+{J"}o޺CZ?ƭo{uZmoQcu?z)xu{~Xǎ?uu}Sˮ/yuu@-^޽W_ݽ{_'ߺ^:^GuWcxׯ{u}ׯ{_޺^~[u6z uz]޺]_ߺ^G}{_߾}{ׯ}{_pz=uȽu^.O_AGHϿ׿oz{?~׺?>׺{[ױZ{^?W_ߺ^~OGǭu=z_${=1WuߺDOzxuwy}:ׯ?ߺ]ߟ.ׯ~=c]_G tׯ{{^ׯ޺^~.~׺uz^WP}=t^]~:zߺ^^u>ױWo>`}ǿOߺ^=o+k_}|o?O~^]| Zz?c߾ν׾_~u׾'~zן:}u޿?:x{Z׼?}?kߺsտggAuSu~u~zNׯׯïuSu߿u{u_>ֿcϯz:^~_ߺ^{zz>׿}~>::>ֱ׿o>ﳯ_}Ͽu}oϽcg׿Ϻ yu{uu{ߺ^'m#׼Eu{~|x}hu޺~]xߏ=:׽ǽ.п{>c{X{^xׯ]{{^{z^׽uϯuuѿ_g1ׯǟ߼^o޺^'}{}uS޺^׺~׺uq^}GׯϽu׿uҿ;K: uaqW{~w~t:k]ߞ/z{c{^]_uwߺǯ>׼v}Pyu_{^7]kΝӿ ?1wo{^߽|^ׯo{z{^^￧m{~}u^׺߾]{ׇ~?-׿Ծ?1wׯc^z^u~:^}ߺ]ߺ]_޸u~wد^վ?z}߱׫=y_}?^]oq׺GSqqzugf{Z]{[~o^+{Ϯͯ?]z}־R=z~]u_=}{=us{}?~]{pwmx}y?=[Wǿ}{_~x'߸u_׾z u^>׾޽{Oׯ:߿u[{^?~z{_ߺ^}~ot>оk?1׽u{{޺^cu^ׯ?:^u{z׺u^ѾKOAϿSg˯_/N׮O)o~[׮qxRz~z_{^?K{^׿?bk^K}}Î}p뻟ߩ׳Ҿ/qcϯ_u.ߏ˯SӯNm{z>=^so:Oqu~z~]w ׇ]_mc{?_>Ny_Ӿ : u^~o]ߟ>޺]_o^cϽuׯ#ߺ^{Z]_׸uuԽg:y^ïu~zָ{ϟ[zw}{_ߺ^uu׺ս 1׾?㞼z^g#ߺ^'޺^׺ﯿun{Z{޾}{^׺uOzֽ?{y/ׯ{3ׇˮq{޺ϯA>1׺/>}ǿu={&^uo{˯uug_[_׽> |?_߸aϽu>{vZﹿu>׺{}{ߺ]~z{}yuнAc~z׺u^׸u~{'ߺzu^׺uuVѽobz׺ߺ]_߾޽~׺u~׺ߺ^u~w{ͽSҽoA~uׯ{{^u{׿oߺ^ogu~^^ߍz^}׾}{m{ӽ1??bc׿ycǿuꎼO_n=W~>׍zs~-.y>׼5֏˯}?ӟ~:?~[_~y[ߪxu_Խ.\{cA~K^ߓ~wߺ}=x|xsc~8﷯W={ߏ^k|n?އ^u?}ۭpSս> |zxu>u={{˯uuߺ^}/ߺ^sk~K[_ߟ}{׸uֽ t׺ǿuﳯ{^:u=uߺ^ׯk|}u~}z=b׼b{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~мb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ѽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ҽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ӽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Լb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~ռb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~ּb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~׼b{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~мb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ѽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ҽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ӽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Լb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~ռb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~ּb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~׼b{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~мb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ѽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ҽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ӽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Լb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~ռb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~ּb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~׼b{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~мb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ѽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ҽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ӽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Լb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~ռb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~ּb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~׼b{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~мb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ѽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ҽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ӽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Լb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~ռb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~ּb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~׼b{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~мb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ѽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ҽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ӽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Լb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~ռb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~ּb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~׼b{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~мb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ѽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ҽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ӽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Լb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~ռb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~ּb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~׼b{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~мb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ѽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ҽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ӽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Լb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~ռb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~ּb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~׼b{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~мb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ѽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ҽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ӽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Լb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~ռb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~ּb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~׼b{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~мb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ѽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ҽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ӽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Լb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~ռb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~ּb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~׼b{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~мb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ѽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ҽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ӽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Լb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~ռb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~ּb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~׼b{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~мb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ѽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ҽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ӽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Լb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~ռb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~ּb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~׼b{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~мb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ѽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ҽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ӽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Լb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~ռb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~ּb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~׼b{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~мb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ѽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ҽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ӽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Լb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~ռb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~ּb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~׼b{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~мb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ѽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ҽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ӽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Լb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~ռb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~ּb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~׼b{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~мb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ѽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ҽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ӽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Լb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~ռb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~ּb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~׼b{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~мb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ѽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ҽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ӽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Լb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~ռb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~ּb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~׼b{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~мb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ѽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ҽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ӽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Լb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~ռb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~ּb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~׼b{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~мb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ѽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ҽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ӽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Լb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~ռb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~ּb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~׼b{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~мb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ѽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ҽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Ӽb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~Լb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~ռb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~ּb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~׼b{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~мb{ߺ^׽u~{ߺ^׽u~{ߺ^׽u~pyface-7.4.0/pyface/images/splash.png0000644000076500000240000024315414176222673020471 0ustar cwebsterstaff00000000000000PNG  IHDRY]Ɇ&sBIT|d pHYs B4tEXtCreation Time06/05/04Mq%tEXtSoftwareMacromedia Fireworks MX 2004vprVWx헿o@ǟ\'9`Ab1 lLH "2  ?`r1ld@:Vwq\EBc9ۯ}|v6Z+ s]T,tc J+GUcQ̕ʋ2/TP( #COϿ?#8={pK޴} k<%3}D ZT&Gr"϶׿uX:$DF%~閟$ )y7H& 6SaSbA:DyɃ`B; G/s|F5.`1R/pM svQ33SO3_p) 'G,rqe^r o-'ˑ"ӊyFYpFƵHt\1#}")ԧDZ3^Mv$}T1o yX?cK<{Էaߍ%O<O5jNv FW,"sѥ[IX6xW>+ 6qx1=y|n݅|ΎY8=nɛZϓ@](ꃾ*dOSwVt19>@c&pôgǙh# gO&|{atWƫYgx[˛9\;f+Mhd&Qxhg3T*d=bO6N}} \C8co4נ\Zt;’T7MMGӆ˵N$񰰿ձ_hnI{+qnA^oͯjYкGCw$<\89G"Y:~ҎԦCp.Oz;.fiPCb!{a>:رPlMCŦb?lkpPcܯܭO!4:]a839X&VkbŚX&Xf]P;df/4y4i? W ׃ ɾ]E}?UC\q+qCZ=>l.<4Pw7^HkVYgs3f6( H79VJ^Nh%u8JbnE8)99{-fy2ݏfdܙw :+e#Ź%񬰂 vX֕A? l˜Q+#hX$U-ښB1e 3r0dKcŵ,`C3 ۢt %ډJ#ƹXۼvW%i$_]^Uyq粯3 ζ|. y-tԝE\JGl[ldNiɟ۹.\ v%%1hHM|(SK0<ћ34~3FuxGZM0}kÝڄϡq5hl\٢MAEXcѷ 8a5]9sm8K3ue%o6QMo[pc5U q G4Gџ(i+F `p(T*aAU[џ0S ٔ)v抝23^T/Eb(+2`ЉT)׺yh&I=NЋr" 0WI nD{Q {h+GmR2PhIbR2z7e6~n[̹˙сWR[ 5tqBqPBhMρbh@C&/$R3;iG: "'xä7{$Cbc u0)t6M.DĶpI¢Ѩ?H>x꿊;^MqݔhDsmDww+FSiGRM`t`Su \P:ٜ(vq+<:/{Sf IqƥKW>>|ljͺYbNu>MgS:DI>kfiswȭސ.ݡfv>SQ0s XEx`R[Tp|'WґTE;sBm11; u*iɔW. >*e:v HpG^C|GVʲO湜hyϣtmɼ9>[,>h2{r= E䵌dGo̰l[lYfygaWsKzI甏c?p'oȎLgʩ|Hz*A71m9&O/&726Qe++MZ tH9+q,n Jn " VYX0ٌzbj80HG'Ucu *yN#ut)"reiq&'UCo*x6 P7"AFЊeD}a`a93^,`yf1MΙ}4/w7(GtX_M+uBA2&wޟVWN2aw\Y4$7 x)®b vUwI7{#o98PSkվj^<|:R:ZKk~.bzɌkIn$7*IFGa#E\fxQ*It;jb/n k^l5A0LNĐW̹ Xv1EQ4r<}EtYT-ZNjɉ^,CU6fUS*}6h:Qe)ĭB``[&OyB`/fdDHmd|8Ey9U,qZ㞜&Ǥ fx*d4f#nU%'yJ  J F1iR:a֨^_R| \ (49I&NfqxVr2ITL$>N8 n4R"\_"Uu*_mCsYŚ.Ccs*A] Ue }{'LZprЋ& ?M|(!i$CCi#xDWjFO_}Q]ib ጸ5H\h爮j/jqkb4z'1hEbt.I&SzOaKVv:1I+6A??]T!Hy~o2y$@N`Ǝ}/4bG4at:}|:4aԷZSÓ/Hm[8w/ȥވOGjtg[mT4+Ѩ(ird=A?yʫIDG4ڄ#8X/s)\G;SK Z*[9CߧK(rwri\u/+wiSoUx&= 0++(4-?J)bϝO0O^'M%mUrnJi**jX9-y!TG)r瞜z_r,WqR#,k*K?'Zsr?{ha}i)L*ǒ m˾Wv˅c ߓlZ0e(m.F.$q?eٟBm1vRC-)Si?}y.VQc'oOv-DFbktF|@wRk^#EdkGF.֮zW~9xϮ1{7ט}0#K txkk&Q5B#4[#B{gޕ5@gF+wktwkt^z< /syF5BA˘yF5Bgޭzгjͼ[c3֘Y}3̻5F1M2fޭzwk^#@ W)B/kUhmja:eKAZd=g]WmY wzoΥ3ZRHu+ؾֵZ⌡.Oײκ]X>VE77zL>)KGљh<^XmeǢy1%mߔX B@DzUA_%Fv5F1z_Y|ܐի^TmY͵ a4ݠxLіLD4q0%tXQ[h@>~biUN&c‡kb׌X}I+_ɲ5pWm ZIʸ3w-C+6~^fmiρ{oF^͈nUb7oLIWr71J?ʵ*eׁCGRb);'>O [r,}k|Onzô>hJ;ԚHI=tF8A: \F-W6/}&J5JQzk(;~eW,dո[х u_;/B͓cNo/佣"7g粴g+3̮9GdSfQE#\5|Tp:caVo.P~I \nRAY0;=#Rr,I#Wz(_v7OX`"9=2Kcm|Qb\G~u1kb2CV[7ܽ'99T;V|u5杌FUuz,'>9[@&KF:x1^{֓I.EDG_Kvsݤ[%EN:CxzSFY>HI~ag-L=V o8֘OPT2.GM+Y")W6(Dk]Fd1O넣u8Y[h{v5MFR_3kUVogK\>~Ws$B_l[.c´ff>efɹWY5R+эʊ#u(1wÍ$b9PFRFL_!רyA;_}c|<R=V!j5Bd~$e03q{3e26ǿ7s٣_v-UW<ì^je2箞L~1XjٵXpTj59Ns={x{)r\a{IA앦w+ +YD~U;jݢnQw]pxoŬXW _{Wm*XXyvׇv^x#yk od &1s]WS/F\>FdQ]Ga#^j5/sH\-pk,yX>"kɧ{}H_tw_ȑ6*k\O<>3=sߒ|}/hجxtu!;/eY_c3l{$QN$>݅{都(O] ;$>ݓLaýݧOP{ ~3`CG[ܔ#GbfQM7LM(oh??T&@!vpATŃe=[a?|yx7_c+(yCԙN14g-iGp!=dCG*5a>K?DQ>"!yDWuʎR䜐|=P~+u{ 2DhRhR='T 1KIByV')^,SzIJRt ZR鑔YH45)T{4Td8$su, D.T{T)I)Jq/fx:L r4 &RG4i] 61gmuaY!XHYd Q'VqGq}G- Hm#g$ @>P~{J}D?ffEٴ$,h5`Oy|U JQBNGmm @ 1O=ZI>mAOb_ AHkwhq'@ )L+>0*tC3"CUp6!pVutr zje=8F)xVtYpB BB^bvKniN ^PK^C+Mx \y0v1KD%:$Z%F-x Ujj%ޘȯ(D^- D?&jRPPxF E7.k`(&J1Q]C1Q⍉/%h:VxFC`o[C/p/^+qkLrtΪC9{|'3ݺ+ӫ[wL灴ҽ=̊l!{LmWÖLmm}#ۏ4喷GRɸGdr[E}W)z.2*<&vS$.AKՁMʙ'3W1XݦwroDBT(O}\vpa*\&VA2:Q&TE ++ΕʬW;$SnfI:1.A'!H(2`!mc܆zHƭjaE<\ {4H s3 != r,mpYIΡ0Az> 9SX[@cֳ7x|/xƧ{1Aًw8C|ݡp&Tz _2NJ'ѵ0~^M{fCj6YۀƇbh;kl&`6J8OS{7:77McJ$OAB,a:w8r9DcX% !WK{<&h?>i+ 4yen֚#oN<^O.`D#f;/M@m29yb@H f!8 M?gA"WО720a.$at=<I,:CAM,]$-(a\ E2 cyzrN5: ?' 9o^l,[9! nm޾Fh홋ؼV"[Fh놟ঔ7dㆦ7uneVA}eY _Jh*_oIeE|5%-MLem`pZ FpHšשyW~pNI+kz")lo%;_˖,^:q (/St T&ziF튯%c}'Q*wy\ѩ~f"9X; Mc7z!7*B0|ѢKQܠG>3C$]שnc_:m,46lKNgޗoB4yy߬Fzz5P;ow(/ Yx YZ kjƿ|V;$4NqSHԭy&ٟ;Ư^j>peC]VnZutN{TO|o,ۉź8oQ"Jw~#5x"T{Ro ѥE-}ViU<GJDž8 Lt.nk6O/:eP[b@w!X{?72K@ޖTs]ꠈ-6 S=s:{~8д͎n7KΡ޶O4[Ѧn%\:'jM?յ>$jtK'(ikeTl#9Hpo##0}=XھLaޡ]Uҽ㾅sǨryjكg3g"}TU"e-bYjT>rF==쇨z G[,WJE+u$T:<)tD`+f-)؂ la|S lW j? iH6:V3>+gƇ/MTdRFj⇔=*Yv[Ȋ%,5-,˿oٽYvӆyD}2-?gp`;HG,{od^Weᰄu8WܭSmMO RoԹkLRzJj[)cyue7.,$6R#mlt=DVPzKW3XrҜqjM[ mV9KO ;9y@ 4fIs=ehYjݞ-E&N]|ݰXע^f2{8J&KEsW_,/˖/,[6YM-/W?xWcWg\-e\]4q@Z(M4J+~ڼZnGiu(MW|s!j6_WYu\gĕìz0Wo#1ڻxև- 2 2 2 2*+z%~gɬ>/m֝-~U,~֢DtK{,)Lg6=vV6o]h,nsa9)1m,mpھ=zc|Y7;1WQTM?VN 4j -hJG94 ?-翃^~?X4 ]yc5>DI˿Z2zޛvś0wP\ٱHj7=7;PKȖiM^2(2={9%Sjg槵6v~5棻)0.CYۓi*"|YϞZl5)tٺ5Fe>r:u#GwZlvd%ݞv5YUX7ۗ BܼY崱)Wxteٮu pRWW,fGxEh]5V>45Xs֞ ~Wub;㫖ӊ\;QAtU _`\/OX.cQ\<^2@ݫ3o,&Ɨ3qQZso./ JtOs1*ݜ+Kx۲.Sǹ=o z|s u&d]ڼԿzytZQ]MΝ72 ]r ]2+ ](˃Z΃ !BohTz= ?*EQahFyuT:oTZ]VW\ :Z|f˸6k 㫖ӊjrl\\Ƣ%ڭOY,(vSֹ*eyﴈƗ3qt\./&醟X2nbTz93/?*]N_-_s=s?o~ѾEͻY Mț_Zϛϣӊ+e[yC- >ߍ"X"9;k(wݚbF䓘(Ͽ貥S_Edv3-mFc˖"+J;exH?F7SZEg_q9?E[/6m}ol 8󣴥=EDjWDDZ[z\~7QsѼJr^v̹ZUSSր7E˵yB˷xXˈg|ˎxڍ :"YdFu/.F`KƋ ,k@fuڋW݌H]],є~7 3:3."|2{}8zi錿R fF->QfyY}w*5{( ,\}0ֿlcfmـ eu*58:nJDymRƈ.o2~\"3:躙9HGYtP%ɿ4H`L;z%XWWxȺׯ$.ZT n{k6#0Ռyd O d$|;k{p #_&|Vl&7V.bF8h7, oZDFD &Ӣ{筊˿ooy~־5?OnʌMRa\.i=mzJ5e5,;VW[7՞Nۙν*Ad\wڱvS?ٕc:}4]=|\Kjt+mLWE[}3^UXz"{R0bf^GwT'gv,i+o/+f}JC-W(ƂTƂfS,ĽVJSYTpU*u(S^x]1г?_@r|莅骄%Jj')J8[r (z.OPz&O= M5t)y֟TRj~YԙzWRᕍ%r(gV@cW}L$fАoAgfiЌf\ODчR{K%Q) Ҭ(SD6V(%1K\XO`m³ $=(Z$B YK":9<R%l9WK{<&VXIVyx8 EZp Aв}bDXi+I6OR$Dd1Kا6DdY3LDA5c% C &"=NAeXG9`!!qmt` Bmc0v逾$—쐸4u Fj0ӫQ0 Ç8 ohyЁ[! iѼtBMA#ye,p?ab[,1l8rچՇ@%ihʒ\%l BYOr-g":HJMfyF@QjU$^`a-rĉPC!>,3a:N8nr֭p> ۫/^-G36ddCjo#I_{ @D#gjG6?AD#CF{8:QiG68 9ɦֱP8do:ͧM;2woY$z{ю{OaY?DqO\8z% |7rg-V<@?C-˲~e= :GgHُgq=(gOtjr$T#.s)\G;SB&MRoə&=R7rwr4zp/+qܩ~oeb{@׫ z4f牻ZIr*}NOO Sbki2X9*)jXi9sʸuy!gդ;V޹w88q> 4]vW%S??O[z5θ-x>|X4q8y_l ǥB['lJk6k^]vzn]*@)+ܧéکԻFA$}F}uUעme\8JK|.CI6ƨ/kUh^Kȹ92Yh{+ ./cbcy 2qBnÞ]]Ȏ#4mzfI8uXM4L}EPyQ%Kl~$bK=gC;]rk<Ȇt\2@ܔ4j u'bCosT>bC:*Z u~"P{IݢSldBm2蠖6T9cju*789ڨAuP[ Rc)5*l̠tm^}L^ذ-;y_JI mn}nըls(KD/ Yx YZ 9u12SpQk"Ͳ[\R0uou@{to͢f1obu) g&( & 4 y4\uI5 oq4:]4%jeԂ^$c6$mi0<+vU_]ņS6.;2[>pjWV}౯p{em\x4MJ|0{"Ϥ(1l@rSjU6{ʽ:`/U:-lhSCM[Vki%o.7 գisaY[#ry6*sqBvcT<\Q4UΜgVQMFz`#ﴙZsȷ!p5nЋn2`of()lygγ"Ѧer٣EgQH@i_|S/3>s4H~2&|'7fi̫ d@І:7Dm+j.Ahgmhn?, CBK"`Vق- lƇ:. |E᎜cS>*m5㳂ɮz@k|_lŮRFjGYWPʬbe7:.o9gOƄ2;~eٽYv-헢5M5o|x堔v/\kֹkE))OoN$ATTtB.5۵+U}%S3aUUi >ۼiͧG}Y uu{="ܖZ;]vkuVc]bk7{Ue*u,U1^-\L^X/[Xfܳ\og6 W_W7]Oq#nze rDj/UՕ 2 2 2 2*+z%~gɬ>/m֝-~U,~֢DtK{,)Lg6=vV6o]h,nsa9)1m,mpھ=zc|Y7;1WQTM?VN 4j -hJG94 ?-翃^~?X4 ]yc5>DI˿Z2zޛvś0wP\ٱHj7=7;PKȖiM^2(2={9%Sjg槵6v~5棻)0.CYۓi*"|YϞZl5)tٺ5Fe>r:u#GwZlvd%ݞv5YUX7ۗ BܼY崱)Wxteٮu pRWW,fGxEh]5V>45Xs֞ ~Wub;㫖ӊ\;QAtU _`\/OX.cQ\<^2@ݫ3o,&Ɨ3qQZso./ JtOs1*ݜ+Kx۲.Sǹ=o z|s u&d]ڼԿzytZQ]MΝ72 ]r ]2+ ](˃Z΃ !BohTz= ?*EQahFyuT:oTZ]VW\ :Z|f˸6k 㫖ӊjrl\\Ƣ%ڭOY,(vSֹ*eyﴈƗ3qt\./&醟X2nbTz93/?*]N_-_s=s?o~ѾEͻY Mț_Zϛϣӊ+e[yC- >ߍ"X"9;k(wݚbF䓘(Ͽ貥S_Edv3-mFc˖"+J;exH?F7SZEg_q9?E[/6m}ol 8󣴥=EDjWDDZ[z\~7QsѼJr^v̹ZUSSր7E˵yB˷xXˈg|ˎxڍ :"YdFu/.F`KƋ ,k@fuڋW݌H]],є~7 3:3."|2{}8zi錿R fF->QfyY}w*5{( ,\}0ֿlcfmـ eu*58:nJDymRƈ.o2~\"3:躙9HGYtP%ɿ4H`L;z%XWWxȺׯ$.ZT n{k6#0Ռyd O d$|;k{p #_&|Vl&7V.bF8h7, oZDFD &Ӣ{筊˿ooy~־5?OnʌMRa\.i=mzJ5e5,;VW[7՞Nۙν*Ad\wڱvS?ٕc:}4]=|\Kjt+mLWE[}3^UXz"{R0bf^GwT'gv,i+o/+f}JC-W(ƂTƂfS,ĽVJSYTpU*u(S^x]1г?_@r|莅骄%Jj')J8[r (z.OPz&O= M5t)y֟TRj~YԙzWRᕍ%r(gV@cW}L$fАoAgß$ecxy7dtO+19r}?T%'HFYY*)KM2(?5Tp r wrf<Hz<.Xe\UEND?8|%}`yqgϷH=9xϣ,OC|pG=!=p|؜܇?wQ['tWJ8~H pok)*^G{q>ir;šە9_P=2=ؑk)[U?Z&p=Y<@//;d'1|tw9[XD(Nt)(,@3?36Ea#>*g侀]uv }F[qGpP[J{41CY(;St{6kϡ$fY@pE%4$1>Oc E*$qpB 2!iO˴r| `i cq2x$>9jERVJyJEVqK׸L"z KU9Ze!xZ<YNuD/% ĮM2j3d~ĆW9U+ k$< U@PЭ)A -  Rj${9eyIi$=x.BsK2FrzcT"Ƞ: .h ;MurPט UJimHUUP(~"14VH&.6KؤM M( +)I +DDal*q^WIf6xYU&Oj8h}4Sc<-x@^ e V&IdjC La Wܬ0Nrq=e4g93c0t'qB `0Fzk?c$=tJ=`D<~$V5{=t9az_MNzr1jr ?-1N( { p (;HDplk: T *ZT'PA .=Tn4cP=t9KaOKӔN!@ibT 6!@ ZF1D3 J`#|n(9:}-{8Y, 2,Ktlك{^`ˆ#,D)0+H~L{qQы P֢YCNS[]8tlE&l24$IA'.œL0Q[<8QƒN a!*Pnp,YXj 2 Ls.X `5a}C[ArBh1419'@~_C9&"tKpD.3бbf4E*Q`7p6(1hq`#&c!LN¢ V@A ӰqUh. ¡#5jN v„`fXCifABB[5"y]/mxdkɍY->H$A;8Z~'I1bRnElSd0ۮ:qtʞu$O=xD]sxPڠШ+{c)ȞRue-6YnSg/=ט&ޱ]r@ 2({b?& 583M@"J^Y<<9,&ȝr] P0(ιId1EtbWݤ$D :N'=pMu 2IQ вEuR`<-8Dd,Y֍9 f֒\/pt=|Ү9.GbMpR?S\,qB.\ie1:,8?O).q|wV͜9)PeH%rOw7%E!ɒKyԖ-|q1X+sI9 )FJ$WGe=Ahe2-åP%n$$M"0ihQ'!\L77܃ HXblD&עj଄4kt؄@XïyH;4!$MB%C7]: lp.bmn&KG kAfIxS/`0.pZD\f-|L$aY. tOd29r ֡lsD&-L^Jrb-mY1/5 fm7 t 6KMo$Oqk^51U+F41t1Nq\4|l/ Z#:l]3.`-l5bdGb$7 AhQ#\@H er4GZk<)F4%8~(l1qx⮛l:1_N5q{b\TPtnZ fJBKy85!8Pa&6µq֢iNj $J?I[HܖQd^%K"Y Z :IVZ.`bՒIT y,I@gIķ$bN(CJ'hC ^O\Be`xZI|dޠM`Cr.i^ג!f $ fsZL5aRY8aپBlwb;bw:<[^2tDu4q+8~@p":-$Kz6A8 3=AM\&18  Tq;]f,TH(A2C93 8T0SMe2K}bGq# T;lPafbގX"18 m7](n9θ6bbA3ny}q/ r rLm_tg: u(bG (rJҜve0EiQ=\ l"e8ɶAPF-x(yeC& w ~4kT;8oSJϸl̰ǭp^ne.4$+dY=.3r, MS‹y4q!h;< ˬ=iĨE*&Ap3MqO^"^bCP &9vۓ&fzjPXx :ތq1?.YwsTR`)]-؁x?4?!\:8 mVLJ\2-T悃d^`%8[LKFө(j8*^;e|7&m9LS^,LyH[.0ȕH(m-|^,`YEN. ءm\T@l3ܱ/& +0O bK4UCNSJ_΀`IwOs--ЭpV:bn>.[LǥaY]8p'б'h &۲pz"He,6FiCtzCoی74E&45Eb [r ɕnFrjfHRƕ'%3GC2Xs #aMG$b>t#p0M'ŭmmb@d l:y/#H&xĤ8Rtg Ϗ̒ئ{)E#[)%uv^L[|NQ헳?q3 V+ꅌҏAYJ1Ñ~Rrqdl4eyVم\s[;X&$R ޵%.qprn3-N-JBG ;' ~/>ԣܼ+KqO^&h1Aw3ʂGEPq#Pr~l TZHj$Ky::NhRAp51v'o=,D>9'1J ы^Lyen6A)'-/I/=jN)>ƖAoܥNt"tY) G8N'!1)z|ơ\qyZ\op|Iqތ7xIlCs1 @TrA#s~2zX$8Wc ùٻ2ݠ 2{ҢǸr߽iA tZ֚2W׍ x$. /Dg|6 =91/0+t[aMm!.u92䣎s[<< @hЖ;,ibFGH7b@pN+&g j@ +4(!5bnB VSs1rLRuwZ(1c~9˸t__@W C *붽 V;R'pߨ Q V I MH.E%JÙW h@لb3J7c̄[imT<%^QpىMZS a-5{X_D&Ǥd,iDӉ@n6 bT6h-%ซ /W \%! 2Ѕn\6I}^p*@p&vU@ރI1.15UWfK#֤-vpT?foEf-vy^#B7ՅR;ԅ2ܛnB oMx7u|7CPƛ{3ލ](ͽx.ތwS4}qo) %A/̅r!ܗ aL@/gIOŘO\B)/]ǵ-<:&S M\MhKE avB<[Mhg$4ywKqs\d04]-wei08%S/c-?N`=W34I&}& '#E `Z`.E(&vX7S&nokH;,[j'GWD/~#&H",ō̙CVV0ߝ_uWuWu>n;zNʬ|eep'䎖2AȪ]*-2H%$^ǟE~Nt\1rrM>9Y[z]D 1dB)[OYr3c5p2D"dXK˙BAnTv\;ږY\d&h1+TL(GoNBQ 0vY-(1vTEٲ[ c BA4d4=͠":;\"e|(;KHfv+zCmC2GF,,NxUjwN\g騽-mIcjU7eYqRE ^Zlސo|78[]ƍb[63*0f8*ٔp0v{S? ӟz.wMBe+0ܺ@ټBmeXg'#JEK`tjSѺ׏ea _űKfl@5FiѲ1-y;ž;]`eL3D{MF({ T繢OKpmK״J]"2z\Zމ Cq>u!HQF[Ϸ9 E-VZVk>'SIIx]ׁ58gr$OM+HQa\Nhv Dk]*F\+HSR(Vڱt;$N.\>^Ve$059 UfH`XV_N hjod MnJO0>8X ɩ@bƖr0" /dAI!Pd 0Kٚ **q,ߊgi[f@Of/ԛ )`T &8q%=Wr8_q5=Ws# )q!vjo4懲MjclX r;97RPz/\=ESXdAo'Vc M0!ubT2>U||SUhPCxc`-x+GtlM L|VѺ"A]քa)Zߵ]n QZ[A a=WOӳO>i6_0Y'CA/  @+K[nb:a]и#uHm+>E BzЦM kh/ {L#s3`1•Alt9EAlv Ζ]] jR5E0b/60AF.xzm׼C_BZF#\sHd1%(GY6 {?B8I$w=c-g1U\<9/a|(WZ|ܑ/dz׽:_Ryf@7 y*ov٬G[oD-g1Ɂ B{7fQ&( !\@\1&@b )84[cvS͛->MZ\`TWP.f x'G wwh@CUt!9C=FFa`sFρ`SFρ`Ba`R Qt\ĸ_v@vh}k<2 a_o6EKthw%SBj9 EkTg:b0ޘH?sGr?7MTX`!zXС2_t`7DKvxfD #EZFfc3*fDƂ:4Z/ :қSHPN $qM -0bV6bMw#BvFVx/U1 }TLj[z_!><"Nu8x>"n pK! &tf8Kks~߂K*#5O02 @DF(N½~]cA!ŀ;eeAdz7c(3a( (a5lAiuTIU .92*L-G+m(\6G% <"DZ'Z5`eЈAQI%_h@k \PfjY 2Q`~K[תy2|hah`+tSx9_ASFW n"Ha3*ODtٻ .0ƨ  >VJL#)42F׊Iߖ}!B24/}ԧTQ#Kc_}F=#E*o3fDUy> S`W˘qH T@a:GG4g~5sU|PK>rX2}d#xdeg"u(6r nl h 6J G(c, t^  Fx+9`@fz pb&gnX3ah;1x/:&ajV<3ҙ#nͤE(8ya1LnCHp&O xyV l+<{YC ').SyxOMbMv.0k^`羓ОOdmT#9$?(f荭x>s7+j35i]8xD yhH߉✂Nr5lhxL/`"s TkOmx%Lb:( ce28H0<&`I r9Kgeb!ᴴ 2ɓ/Tr)*wf$2x wdVFc9Cl(CIRfq|P8-߀Za`B5.ð Ŕ#0h&5M>Qeq;>M>CJH6 GZ19t*U0kV$$],XNn20V8m|` OS2J3^A[~74M)b+fL-oKfXT PLveM+:ht"}h]Saň8=Á I1"倳E A/ۉFb-(uZ( l3 ɹ,*3+h.%gԐYst-GmR[swX]9Vrhy?b/՛Q[A>ƊVMIU҆*GX ЬdYeyAH[nCgy6ePvưY]9nF0zl_#[3G[s4W-#&#:xcdGo{8BAL[. cQƌbD?NR{k$u}zS_+ MR'0"' jL}a1'PbAD_3n&>/} HTc$-h";{-7kC֑w3ɧW}CEZ/P TWRcNu8N* ,Hcv#w4\H-qJYAH艉NHe"fDj-중+bXqվ,W">zIW YzIJj6*yC7R\Wf[1ds ToDoTe [9A THSz*/r[9]V"Ƒl5d⽶)0MO7bRZVOx%h*]ۉ mQzW XkE'r*kNb6,yTԡ㈷d79x1^AcV Y-A>(>~C!?Ria,EYa]tO@)ˊZ9-?\t@7| WGn{L%0cG]~nT {oJ24%A '@X ?*zv?~Tsqx(HNBTaGAtsdщ2y=Er0*.Q]1 vkL]1VM ?7ݨ79DAxB)9t>\TaXW9#\Ȝ T֏UhE3˽/<Ȏu E yտ\888W^BӉ.AR콠q~Pޛr@"ZKن_'#@]}~,q^ x!T}U+${WWZYM-d҃X|E3~ܠ; i͂ᤑݤ^M w|VDrL/܄s"+&߷MߥH.63oQڰT. No8\rkx@VaRZ<))"f[[y8pwrѺL[˄3M掷_~/m P<n@' xoG3}T ,w%#8y2D>IKiF8vuγgGN0DŽ=MZ^(j\QګVRA$ CDdưiI+y@$)|p4{{ $AM,ޏghWMSexc;5--mn5> `!KNyTp2Q)5:ՂHaM(O\yuS \|T:K75,BT4uXUe .`ɲǀ pJ4tA+۲&}C{ӣR$8F ގJ#u*;,VNpI~̍=EAv̍p|g l#Hk}5IBE ޶r2dlK)zFr38ٝSI>;1ed\0bfE3&0ȡg $9fbfGڌbL1@Grj3dϑBNmF`7.tl-Ǝ`rN>Hrd+>WB bQ^)֐Ɛ}چ̽f ۧ1$t\@C1$M 1 &z, Ԇ$$" xnϦIN-V9(z(=_eA^l5C',tȌrk]}hUr犔wBb`5"VM͆m@H5XtAHGz |VQc[G ˛ Ywc0!\{ ߪBomjL FWFg qEI( H+*S A%lYD%23 mu୕GcnUi2EA(QQ}x8]ޅ3X1M_΅6_Z-)EguaϺ1#wKV̡b>!a(>_iAbz96ƉL ׁs)ɢ'V`<˯ 礲nL]Q;y3B4`Eo^M3&aFL0 1X<+]w^6eUs$C`FU+EQZu[vԚ\=ӰhNbu?jҦYJ$6Q#&HZAOL&Or #xHr<G1?qU 4`'04Oa9g\X 5C-v1: Ak0\0WiC{adi[@NU9:k񳖼MGĠY[~ccet$zpzyZ0r A1ԙ5 V'H`  CX*[C- }A"Ӏr:a# l|_O -8 6@`z@ Jg }DPX |,x1Azx1yFB%$lYeF=u 6)i'X~gH`8WUx# dv"@O'~EEK uOHEkۓXj;4I Fi( (`FF"YEagkNC5P` =`GCf_{tPh(H\%.CsVܖnXlNZxLP`T 󥀻<5 \9 \8 \8 p dE>ow⬍|3` P5z( =Z[c|` dU+Ώ!s\kd|p\b}OEK:Z$6PZ,FGLj 9g!.Y"FL(< ɬXck&ָXyLipif17bLMát YiÈwyBy5iamt +MVIǡ8:[xFz% u+ OA EP1 B656^!w-;>91PP^UJׅ05Ԙh.VPW_:%i@E%p륭A0i %hiK7N}}cW?n~q5{~9O?X[`ʿq2zT!M GP$}zfZф~w ZU tjX\1|,A F'Q& @\) 1ѿ˳+hGW><5VV}F?E|vΒ܍~COr>}M}"; OzrG=Ʊq󍢾yW>l۝BN٥Z%yiM\B`tѐxte=h.tlĸF#eX2±>!+>*Y&99р^!Il{M)-j Bd'& Ƈ0|Vr?& ס3:YQ. 9mO"$Ƌolk_:HIe<ɩB2DZC)tÂ$hMs&'VwV<АQd/)x.-4!dɸԌ@r]:f0E{6#?,U4&!yi N;@AMg*@ǾvF*8pSٮK0x/Wzw7T@-Q]7r:BS>Mu!magާG*Y]:B= t,nrt3Nv9 W0MvW;XҙgДgf0xQ)ZB\'G]G^/F^ևF3_N6g~v&bᄐ}_}*o_ďL^^^CbR~.~K|>/}5뗽@_˯O4EcګTV}ʩJQ3Y*@Y*^e࿞?\=^K??/Si7ܤk ӏ/_>*^@WT~:<~/߼gpueo;ot_u'gx`U݋o? ŷ:Ql+EzԿ*GBm'΋13S=`i< 9qTƞNDus|\kQOa^&7|G xM; y{Q EuaVH70,RAʇ<|TB:#=yB,llڄl HZy]_nokMCt;}E@$8tAަe5 O9ssp')U6u*;T>z2ZO b%˲+fZ\dqoz~+>4adɰ``HK~r7HqFD6;CzXMM%ʊߓܗ7oSV;|u/ߦT1)ɷoW6_]  IpHջ loG+EO׋Vsd iNs}Xh1 :PjikPyt|B*wp~~Z,S+!_ ~HWO@Sc/$~o|gxo?#>^?{2V7~^ߥ='I{(mהmoҾ[y*},Wo5LŻ/jd^}~՛ݏzG۹'C{woҶ1oL %}V/t|IM.7m\ovK^=xޔx^'30I |>Sus\iܔ+k.zk LD*em?&SHE>}Leq<)BMɕE#bhaygyA۽j-z~P81]L>Aӝ&ClvF_6sƧ:=*^ tp=>}/|w˥?\WWZfk뿡VK_ܕأX V]5)s@W_өշ{&oX'w:яvI?JqU1Njf;'_l@>2J8_ڝA'Fׯ7Jc]]ϝ{EJG^Նv֪3cQj$jEN,P6Ś.*'zHz=OV\dyVϒ7;y_+0z5 ݲ>ױ~/@bˉsz _t' Ufbkf7p3sizJ<ӏ>g+@*ǔ6];__x=ES[z!o^\Yr^#F"O'ϧ>%C: `>|ګ||fzk8;\6VY!hit_TgaLIV$4lo(6#rs9ٌ>W`F;cߩٗmoBdkc{{{oCC>q!|F볾1$;e!=@& 3JTԑ1d+ kkns*b1eb"Y/x NtbuҢTڮRiM F39* Lr&dwvПxFb)KKe~G@[eX[iw+]A+^4.Jk/^{Ӫ?R=̃Ww>0x4942#]3IP 4y--⦻_Z*+nk,Rޭ65CB`f>nEՀ2~^ߴWwBK4 }>؊P k S7 $ 0]$ӣ^\#+n58$O*iQ y4 j]Z1lCO6I9Zf]^^,+oMӗ5]Fi3cM!0"ж?ۘ/.i鋚ui鐧iWS> y7d\0&ȗː>,N]l[2H nHUhU}tVхT dt.ʦEcƩ4'3}q2?iX*+nSMWQ Z -]|t-oXFF`/4%_Z㛎_Bǟt-ZTF%|q)J7%ߔd%ʫv$mio2i ].Wn+7@2U}x2B T.;#X^,+)ʊ;H+wT&v4Jq4ԟs<&]tkGU :֌^ƿ318㙫Qkڱs{>jllUr}WON|I/K=̫;=^8h'ߒb9)&w$ aS-uWɏO#ld_7o7i2$mOz2%ȎGC9IYŵ2-avRYqnul3 4K1 T>k"i@E6gNtsai4l^PJk津4diG+ϕ,B"gHp&Lfj y2OsW A “7x,v+ 7T=^fVyAy 008oSS RP}ɗ]WX^E n.f!>lz [C z >q{kr.cX+ -Cl aUO8^0q{8މDZߘ[$^0X;7NJðXV@/{kw9=_{q[/hɌqeywFɝ qhNŕf|ѣ=vb3h >8WRtعYPDH5vv&QeD.4NvfhG;$"6sFfQlԯ٩_O_3jP?v; sG$ᤘR[̇ fN19eg*[p\hɵF7gC 9⻣`S[/ndMDY/s{<~<_gU3ƾ[3S[ŭ[#tey)f(v9ocU/ﬠH(.TNi%^qL{:)bixF=_JZElj9&w1' SKG81e>2 Ϲ@Yb]躮 5Z`r3 ãw=6C,t aP짧6IB5{=1}eꘝYGFM5?j 3 KU`0:rttZZqD>Iifr T,bNP3IZsb" V?kdmzecgjҗ{[zM?ދ{s:X罚.P`{HZ:;GzXmM[6+YZ}ך/l{V@oV` ǽrV@B̚P#1"1ܼEhVyT&`iU6['+lMOU<):u T; =Ol]Z4LRk-a,-jR<5 %N(zة>#Hz֙V㛱"V#\V_o 4ߤȼ\cv.$>O?Y2"r#&7{Xq3b-$h+-g 9kjhdNĝsʰME"\1 k8*1b,I& L̉:l%c1'7+Yz?>N-) l cv/3K'%s2=$ Woq 8C΢ʊ;ٻ{D2OTNI=knrwA0) JN "Zu 7/4RgQC]6$X j* c^WLŁŲ6se.9ũƼƩÉKًAE⎷HZ|ګ /G1;Em{>ЗHVD"<{*t+Vt?TVf6+p0<>m+:>j0v]!t3Dnbbᄂݗow^~;ͯH^oB4.^fKi Ȑ> qM4髆/J+?vxH'5c|xC{_w߿E۫uȓ~ dpt톮яgi  F>##'/u%g$}~7V|3x6(]]3mkBSx]0{5|Q_V ؄&MfЃ{&;;ujh>u5z/LcFT яjVc{AZڔ^p u9ZҀ ن؛=ؓmOM6s[ YN\f_sgzUN wRm#%+% cVC~:LBL :\:mmkBT}rxԱ 0 [~JPAѐV^N!dno;zMK@? 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 %mkBT4xԱ 0Ќ6{P$ }6aF3gɖPzɧ]Rؼ<U]#7ECQ{_o^1#,!9oT27%{uhK;;0~-{؞wͱjYmkBT%x@`:v!{q^9e%@ H.# :XJ 0AHJ|Ogȿ=σie0< ~&Ѷ/J}+oئQsoJE]ilnc6Ȧe1~]o8n[}p],gel.ٴ?D-ѯ{ZC,I.z(ZQg) Mb_G.~3?y;~KeqbT쳎rZ}&Gksy;tvLs\?lƱs_U?16߱_"\9b]yOa<ȳ/r2Il2׫.2f} wZ~CZh;{byz\۾)9'qyq^dzŠ~}GQ?? >M-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`$jwmkBTxA 7Ͳ4:8d]~_?P(Z!ymkBTxmA`w@kNQ tt`:R\; ;i8HAQç7ft:smw.o9qh/Wu.Kk3[JL_y\.9|Uo"e}䯔uCyS9}܅UܓeX6)r܇_2:bKgv}*׶z^O74K콏S[yųmc[cUiW/1cGۑ1VXmPr%Bǵ$i,?zF0)祾>ŻG|.}܁sEHykZƐK_|{8LGNyxn.23)'}I:I!wͷ㘥EZc1tM:O{"!TnǷչ^#Wo5w{ (mkBTKx1nA`n\@$=ERlOn`vKe7(ehMC #ѓ#$n`vS%$ n捭}3ϛg<⩿3sw{!erO7Ewg[3U%(}KSƛOOb:绘6_5JOEe̮ uL$c|-cf^'guX$'ME}E$~Xϓ7ɺ/yxq|9q2M]&\qϛf'~>f~幼޼Ղ㘲͞}<뾰ŴabVhSMuM9̳U9dzڟ%/Gnx&9f͟]?[+ϻ__a7w[[=xmorQ Q&mkBTNx=nG`߀zTU%% HI`N'݀jFs87wwF@"<ŃݝoVoo;c?GG(_Lr0rr<ގ9ߧԹH{l]n:Μ\m>>~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)Pr*gڰFG 4%ms?P^@=/;B/f_C mkBTnxұ P El@6hR0BJ 5lQH.2>ő>Pe~v>6E{X71F-LWKFlܟ~]'# mkBTx1@E8 pD t i$ HvfU8PJe{E뮱o?֟8ĭu_5zһ~fqԿ+SN?+ھ`rmkBTxԱMPll67p z{6 lh_:K/EO{3M*avmM~#V}2w!be}yX}H9i"P?W_wLi6ζ)5OC_iS>oGOw|lS@YwvL߈ws[Jmo)}N}b}3{uUMspv'ʢ6mkBTxԱ 0EQ6 d6IOS]$w)Yrײwae若E3?>L|Y-6&~GmkBT#xӡ0EB'<-0h 3DV/Rмc>,a W&To|wֿY}n`r/.לk*mkBTx}+(H,"H$"#X$,QԈZs>U{ ..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) IDATxׁ2G?,kkԳq\Ґr)ETٽURAWW@6rE󢹢X]Z;WmRnU4uq~4"sqʿ"^ѯHY5;p~!g ߼y]-ޛ7 0@DDDD #,"""0dE!( YDDDD`""""CQ"EDDD,"""0dE!( YDDDD`""""CQ"EDDD,"""0dE!( YDDDD`""""CQ"EDDD,"""0dE!( YDDC^CUU̠V9DҦ~7¡:* 4MCZm?p@[E~1d%TUeрa""JRM "rN9wKX<4] 0,G`tG wZ7-ݙžؖ%wDDD4Йs+8y~<Μ[z[` T| ז.Աtjkoʬֽ-u 5z;xwA?""$`gέW.`BO.%|۵ҸʋE_ sLۃG?;A""aJӯ\׿"Nrޒ X.+cpgW(NVŷ5>a .\š}O}(Q1d%ԟ}Y^S<([%""J$&׿NV_oi XA X'_=ѷKDDHJ <,`,p_'f+LDDDJbFw܆~غy.6s/KCye pB< _O[~!+a&w=wo!lmmQ={5| !ȥ hQ]O<_<;'JzI Ȕe2(A>w-i4Mþ}Nlfl8\ Kk`Y6u,GG8 Y-,A X:M%(xDZEjÆ mu9J%Z< :d%ȹ6 h^׀eߏVqhB{7}nPU7mugllm\.w3Zb>vdڍg!w/[_\jNUSy/cȢPQVQTPVW(38yyepa99B-\G2 `~]DlaaRH,ː$ u* BZ.$Me**//#e8<~CzU(j۾j Y;݊!+q֍=N~ȓ"X2ʻL Xkh- DDԛ\.UUQ[TU0Ia3.'z%|vj 2@g- zJshNx4!69Ͻ??҅VgqʵMZoeض-u:3o><3 Յ //M!%t|- F,"u/h.>z)>|- CVtqg_k>x{c"""Ӂp HYݜp0aDV(k@眪p(e  |>X=Y N[|>EQBhYMZm0(z>dd5<_Y9y܇0Iq&' [a}l<;>lބ4 kEQ011cv- ,)ZXX(=rZ0TUorb80rX0dE+g|cbr`r4H {]cH"M077yO|GEX /RZ] ]Q.C6?ߏRzǏGcFw똞{^0dEʵNV^ 1%^wk} X;Tw̃߾ Uo:PZ B+J4 b/uBUαELNNFJ4M#qjÇG:,w{0dEʵpc9wmrv 69AV=Y\rwv%CAkxcnn.`R0==ɇ}-5{{:똝ER |[[ jQ~ X!(;˯C|GQ.Е$PVpqI",Y[fvW!uLOO:4r9dY({u"* TU }4{GWoBk:Cc0) 0mr-,KQyP6RUCT* M .d2bbbqK %tui,Z5!\gW4,,,`I X\9Nӭ#z zoe@zkޏ$I( ( HRd2d2Ϩf_<B4_XXˣ0 ݇0^Mh poK[RЬ)pOMk/^Ġ\,#I$>|X|PG9E^sN8h%s5¢F/Y͵zwc NV^S-`K1\hЦ'Y?MOtZx^P/4x$IBX "Ţps=~Mӄ%:Br9,dU a O4,þPSϐc"(/&[RS@׶2vtdn_>ZS(BVUYC"رc>%J^ `"&wK$6;;&ǐWOWi=!>ECx p|sǶr#`R2bvA:t(y=v|^)臵Hȗ$ ¼׏D7ׂÈ!GWX]a-=% eBs4[ht,k;ˡNy[j#ڣ8n,:Qܶ;MCڶMܱm_vXn\M+I(35%":\'4M*{@˳i+m$\/Z Y=:tymrBY>w|йdf]p X>{=^Y;_̥%"DzPLho҆ףB&~<%ฅ"zBo@lkUCX5d5<&e02#@`I{_~5uksͫSl_/nG\"w/V Ydb҅4,þPwxNu,ãm>au{ڛ=`ksޫSj5nfp^ @h89L݀$Ҹzp 1>U]6㱏 |{֠r9VwBQո<1d}N֞BLh=̅sVf>лァ `cy\[]8qa !RlMl;{we86Wp V@sҿ t$)<|M8 ?81d¯S=H#6p3.R o_E+`=2!ghaNl}]xcGz{QyX.- ZLJ$2uSyR3x={,`\u A~xxE`vbOV j( YyNSy㷥nŞ{vR}x:dKbвDc1 Y=ܽ +x@s XF𜽾avp#$zA J 4z5u3d@ X=,[g!fكU > dx!khz@kD{E%DM?kqer YdbAsp  k{疵_[ l!{@rlek;ltPyNCAiiB zg V#^n.dzz|=x $~8k5pXD?vqKY{ iAB- Y={;,`95z玵jKHC.^ Tqi}Z#dZRV O dq.yY"c/Y1d(s'!>ny۞)5 jǰRv> Z#!%IRLxi"қzla]uBCk()z;yi:ubK)`=:,$ZqA;:m,ѐ%:|$bZDzԩSRIӜNmzԼ Xx,2Z ^ A{,~A(H ^;ujU}-AP%K*B>-j ;ρbh % CV)ن ֞[^,km~H?VX jHӁcx-h-h4"ZKJ$ i}a A!?l],Y6UO?VX< lj똟*_"WWz!HIBÆr9{e2B+ Y!x6eyLXxѠU }!׵R$4ۿE1N"YtpZ311[Fu?4 e4SNB @+`֪C q 9F3&RϝjdCUU᡺::fggiB{%uiaùпz333<yix0d3}3cn,å_=a~NǁwlETUt(izrG}ΗȇmTZîX,FQ^GT ,9!+DS,z za+k_^A0Ä _>rDu<zb4MѣGzR 177ח^"K]hZ3\dY Z _^"D4LOO8JbiS0Lr܉G?x?N^Ӑukit2^=;c޴ oâh`nnssseL`mWuj5j5x,χv)4Mf( dYv4 C4TՁ@d|瀕ej5|ӟZ,Ţ fV&E JEx$I£!+d_pX|eձO " u+ lJ\,QfZXX|>0ÇO{j5"4M }JJ|l#,3ELOOVbw EXyb B[pGqh;X|mu!̈,[)`e {xs@H*EQb_=4'9r333bT*ILOO 6w,N#5|$T* }!K,9Yz-8~>`ˀCذq@b XC;W'{p||SSSM,>a`K%E&$jC(J܃d28vTUJBt: EQ(J*PT b(3A󡬋o߾$`n3i\o忎'`s@z_g:r/jںjOEV Un RNQHԚ,˲P@z<_^뾕 IDAThgayy1d*I!oI۲,ң7(m1_ig yEQZJT%6 m0lv=YI l6۶KVzJ4FFFJ*Tɲq:/^|!JwanҖt:c}n?t/Wu `CV%9,,}CV0',m8mvY߻oJ5MDacJ8 /^Ov p`JЂW)̠fu˶h1d%NxS]c=],f,"(0d%J?UE~ޤ$A +` Hrxd,"q XA&Ȱ^wz;QzFFFbj ' YhV/ٖGo൯O1ٓEDQ`JA|_s(pϟ4/Cj[ UQ6@[oꎸa+Ϲy^?aT*+SKhaJ~,{}wm{[G7QoOV.5D0d%-Gޓwlkh96,Ή'Z7vc6Q8'+InVC}.uIUUߡ|>t:ShaOV|Ɋ:`P_o[+l'*JoZCDCVlۿ$,z yzxDpb/EÅIKϬnXŠc@2u& ~x+_tnEܮ;ǚK=@4΢VUccc13| "`l; X> :pA`#o.o>\m jnnwQ,8|pĭ""bJ? xRW^??^,ꚦiT*V̱eGA*uDDM YIq ρ \{d-Y.?f*$it]ٳgQհ(p|>2`Ql ZO+jm,,raPj5ʍ$I(\bǐd&\*? n@3`m=]Bh !k.Ms_Xנy9W$ J|>B%;fZ+ds觸BI|W#_RdYZNe r9\@`6 Zw1ppnA`燛Wffh4PUN*T +EQ$AQ(l6^+"8 #M(/W*Jh,oCOAifھ7w2t]o.]qd2sZZL۾t:Ȳd2ȲܧaZ޾ 44 O+[r)"ͣ쩊PV$IghV!(A4Q" ׃Dž7 1h00d%as1ѰbJ 0%⾾jae _DD` Y5*^*Kj<}""$)< *o>"“k}͝MDDo Y$B_jrl*y)ߠe-p} \DD4`KDCV빽>۶W:@}bo \ne8m큲s۶ A\d( Y$L ەnH4\ٷ""u0meH Eaƍ;g-gg=52Y_־ՅP-DDDyXn&['Hyk30Ctn޼7m$ZEDD1cȊ}n>#\nCc%r{+dg"nXN܆:q\ XDD3(m)t9.{ydkn[{3ݼySx|,ׄ\ r:'<'G~5!M~>n/ r5aW.jC% CVLhrԂ^~R'\57lʼn!+N^-^nHYc Y}HpsCV? !l}Ͷ(ϞNm;!"">;0n><dRp Vm"""CqV.c ? ~=FWs,|w aTNk?C߷8Aʸ$ġ@""J$X)+nx""Aǐ5 DÓyaM OhHqaa0Ah1d'f"""" !( YDDDD`""""CQ"EDDD,"""0dE!( YDDDD`""""CQ"EDDD,"""0dE!( YDDDD`""""CQ"EDDD,"""0dE!( YDDDD`""""CQ"ŅwIIENDB`pyface-7.4.0/pyface/images/image_not_found.png0000644000076500000240000000136714176222673022332 0ustar cwebsterstaff00000000000000PNG  IHDRabKGD pHYs  tIME 6eDtEXtCommentMenu-sized icon ========== (c) 2003 Jakub 'jimmac' Steiner, http://jimmac.musichall.cz created with the GIMP, http://www.gimp.orggGIDATxڥkSQwf$ZbېBk+ŕ]\W @t!n]Bp#"FED(U5ƒ4sq__Ru>sΙ3הgOYZ,j5yo =DVmx$K&zky*01 R>{U8xx/W.B ށ *J\¹i&bf64TQu;E:΅&Jƞ}lQ,h[D @^ Fdžqf[ { >mʡ#{!=a0;B,;sJd D= "6*pZV%]^ώa&! 6j?t''xrfwNKpu#`R"c =HIGA?r /Ȥwa?o(VmP3;?٦_?Vd9AIENDB`pyface-7.4.0/pyface/images/about.png0000644000076500000240000002331214176222673020301 0ustar cwebsterstaff00000000000000PNG  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`pyface-7.4.0/pyface/images/close.png0000644000076500000240000000055414176222673020277 0ustar cwebsterstaff00000000000000PNG  IHDRbKGDhv|ay pHYsOtIME  IDATm=KBQ`eC m  t@K[ Ff- CP(B jt)ǹvNKcx1z~בJFGsӥ}}Fl(Ff}9Z[dj}"}L"sJp`Vչ`Ef7 /5:G$u 2R^I\-P}V5jMI_-Q_z _ͬFIENDB`pyface-7.4.0/pyface/ui_traits.py0000644000076500000240000003170714176222673017600 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 pyface library. """ from collections.abc import Sequence import logging try: import numpy as np except ImportError: np = None from traits.api import ( ABCHasStrictTraits, DefaultValue, Enum, Range, TraitError, TraitFactory, TraitType, ) from traits.trait_base import get_resource_path from pyface.color import Color from pyface.font import Font from pyface.i_image import IImage from pyface.util.color_parser import ColorParseError from pyface.util.font_parser import simple_parser, FontParseError logger = logging.getLogger(__name__) # ------------------------------------------------------------------------------- # Images # ------------------------------------------------------------------------------- # cache of lookups from string to ImageResource instance image_resource_cache = {} # cache of conversions of ImageResource instances to toolkit bitmaps image_bitmap_cache = {} def convert_image(value, level=3): """ Converts a specified value to an ImageResource if possible. """ if not isinstance(value, str): return value key = value is_pyface_image = value.startswith("@") if not is_pyface_image: search_path = get_resource_path(level) key = "%s[%s]" % (value, search_path) result = image_resource_cache.get(key) if result is None: if is_pyface_image: try: from .image.image import ImageLibrary result = ImageLibrary.image_resource(value) except Exception as exc: logger.error("Can't load image resource '%s'." % value) logger.exception(exc) result = None else: from pyface.image_resource import ImageResource result = ImageResource(value, search_path=[search_path]) image_resource_cache[key] = result return result def convert_bitmap(image): """ Converts an ImageResource to a bitmap using a cache. """ from pyface.i_image_resource import IImageResource if not isinstance(image, IImageResource): # don't try to cache non-ImageResource IImages as they may be # dynamically changing return image.create_bitmap() bitmap = image_bitmap_cache.get(image) if (bitmap is None) and (image is not None): image_bitmap_cache[image] = bitmap = image.create_bitmap() return bitmap class Image(TraitType): """ Defines a trait whose value must be a IImage or a string that can be converted to an IImageResource. """ #: Define the default value for the trait. default_value = None #: A description of the type of value this trait accepts. info_text = "an IImage or string that can be used to define an ImageResource" # noqa: E501 def __init__(self, value=None, **metadata): """ Creates an Image trait. Parameters ---------- value : string or ImageResource The default value for the Image, either an IImage object, or a string from which an ImageResource object can be derived. """ super().__init__(convert_image(value), **metadata) def validate(self, object, name, value): """ Validates that a specified value is valid for this trait. """ if value is None: return None new_value = convert_image(value, 4) if isinstance(new_value, IImage): return new_value self.error(object, name, value) def create_editor(self): """ Returns the default UI editor for the trait. """ from traitsui.editors.api import ImageEditor return ImageEditor() # ------------------------------------------------------------------------------- # Color # ------------------------------------------------------------------------------- class PyfaceColor(TraitType): """ A Trait which casts strings and tuples to a Pyface Color value. """ #: The default value should be a tuple (factory, args, kwargs) default_value_type = DefaultValue.callable_and_args def __init__(self, value=None, **metadata): if value is not None: color = self.validate(None, None, value) default_value = (Color, (), {'rgba': color.rgba}) else: default_value = (Color, (), {}) super().__init__(default_value, **metadata) def validate(self, object, name, value): if isinstance(value, Color): return value if isinstance(value, str): try: return Color.from_str(value) except ColorParseError: self.error(object, name, value) is_array = ( np is not None and isinstance(value, (np.ndarray, np.void)) ) if is_array or isinstance(value, Sequence): channels = tuple(value) if len(channels) == 4: return Color(rgba=channels) elif len(channels) == 3: return Color(rgb=channels) self.error(object, name, value) def info(self): return ( "a Pyface Color, a #-hexadecimal rgb or rgba string, a standard " "color name, or a sequence of RGBA or RGB values between 0 and 1" ) # ------------------------------------------------------------------------------- # Font # ------------------------------------------------------------------------------- class PyfaceFont(TraitType): """ A Trait which casts strings to a Pyface Font value. """ #: The default value should be a tuple (factory, args, kwargs) default_value_type = DefaultValue.callable_and_args #: The parser to use when converting text to keyword args. This should #: accept a string and return a dictionary of Font class trait values (ie. #: "family", "size", "weight", etc.). parser = None def __init__(self, value=None, *, parser=simple_parser, **metadata): self.parser = parser if value is not None: try: font = self.validate(None, None, value) except TraitError: raise ValueError( "expected " + self.info() + f", but got {value!r}" ) default_value = ( Font, (), font.trait_get(transient=lambda x: not x), ) else: default_value = (Font, (), {}) super().__init__(default_value, **metadata) def validate(self, object, name, value): if isinstance(value, Font): return value if isinstance(value, str): try: return Font(**self.parser(value)) except FontParseError: self.error(object, name, value) self.error(object, name, value) def info(self): return ( "a Pyface Font, or a string describing a Pyface Font" ) # ------------------------------------------------------------------------------- # Borders, Margins and Layout # ------------------------------------------------------------------------------- class BaseMB(ABCHasStrictTraits): def __init__(self, *args, **traits): """ Map posiitonal arguments to traits. If one value is provided it is taken as the value for all sides. If two values are provided, then the first argument is used for left and right, while the second is used for top and bottom. If 4 values are provided, then the arguments are mapped to left, right, top, and bottom, respectively. """ n = len(args) if n > 0: if n == 1: left = right = top = bottom = args[0] elif n == 2: left = right = args[0] top = bottom = args[1] elif n == 4: left, right, top, bottom = args else: raise TraitError( "0, 1, 2 or 4 arguments expected, but %d " "specified" % n ) traits.update( {"left": left, "right": right, "top": top, "bottom": bottom} ) super().__init__(**traits) class Margin(BaseMB): # The amount of padding/margin at the top: top = Range(-32, 32, 0) # The amount of padding/margin at the bottom: bottom = Range(-32, 32, 0) # The amount of padding/margin on the left: left = Range(-32, 32, 0) # The amount of padding/margin on the right: right = Range(-32, 32, 0) class Border(BaseMB): # The amount of border at the top: top = Range(0, 32, 0) # The amount of border at the bottom: bottom = Range(0, 32, 0) # The amount of border on the left: left = Range(0, 32, 0) # The amount of border on the right: right = Range(0, 32, 0) class HasMargin(TraitType): """ Defines a trait whose value must be a Margin object or an integer or tuple value that can be converted to one. """ # The desired value class: klass = Margin # Define the default value for the trait: default_value = Margin(0) # A description of the type of value this trait accepts: info_text = ( "a Margin instance, or an integer in the range from -32 to 32 " "or a tuple with 1, 2 or 4 integers in that range that can be " "used to define one" ) def validate(self, object, name, value): """ Validates that a specified value is valid for this trait. """ if isinstance(value, int): try: value = self.klass(value) except Exception: self.error(object, name, value) elif isinstance(value, tuple): try: value = self.klass(*value) except Exception: self.error(object, name, value) if isinstance(value, self.klass): return value self.error(object, name, value) def get_default_value(self): """ Returns a tuple of the form: (default_value_type, default_value) which describes the default value for this trait. """ dv = self.default_value dvt = self.default_value_type if dvt < 0: if isinstance(dv, int): dv = self.klass(dv) elif isinstance(dv, tuple): dv = self.klass(*dv) if not isinstance(dv, self.klass): return super().get_default_value() self.default_value_type = dvt = DefaultValue.callable_and_args dv = (self.klass, (), dv.trait_get()) return (dvt, dv) class HasBorder(HasMargin): """ Defines a trait whose value must be a Border object or an integer or tuple value that can be converted to one. """ # The desired value class: klass = Border # Define the default value for the trait: default_value = Border(0) # A description of the type of value this trait accepts: info_text = ( "a Border instance, or an integer in the range from 0 to 32 " "or a tuple with 1, 2 or 4 integers in that range that can be " "used to define one" ) #: The position of an image relative to its associated text. Position = Enum("left", "right", "above", "below") #: The alignment of text within a control. Alignment = Enum("default", "left", "center", "right") #: Whether the orientation of a widget's contents is horizontal or vertical. Orientation = Enum("vertical", "horizontal") # ------------------------------------------------------------------------------- # Legacy TraitsUI Color and Font Traits # ------------------------------------------------------------------------------- def TraitsUIColor(*args, **metadata): """ Returns a trait whose value must be a GUI toolkit-specific color. This is copied from the deprecated trait that is in traits.api. It adds a deferred dependency on TraitsUI. This trait will be replaced by native Pyface color traits in Pyface 8.0. New code should not use this trait. """ from traitsui.toolkit_traits import ColorTrait return ColorTrait(*args, **metadata) TraitsUIColor = TraitFactory(TraitsUIColor) def TraitsUIFont(*args, **metadata): """ Returns a trait whose value must be a GUI toolkit-specific font. This is copied from the deprecated trait that is in traits.api. It adds a deferred dependency on TraitsUI. This trait will be replaced by native Pyface font traits in Pyface 8.0. New code should not use this trait. """ from traitsui.toolkit_traits import FontTrait return FontTrait(*args, **metadata) TraitsUIFont = TraitFactory(TraitsUIFont) pyface-7.4.0/pyface/heading_text.py0000644000076500000240000000106714176222673020234 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Heading text. """ # Import the toolkit specific version. from .toolkit import toolkit_object HeadingText = toolkit_object("heading_text:HeadingText") pyface-7.4.0/pyface/pil_image.py0000644000076500000240000000110314176222673017506 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 implementation of a IPILImage. """ # Import the toolkit specific version. from .toolkit import toolkit_object PILImage = toolkit_object("pil_image:PILImage") pyface-7.4.0/pyface/array_image.py0000644000076500000240000000571314176222673020053 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 Array, HasStrictTraits, provides from pyface.i_image import IImage from pyface.util.image_helpers import ( array_to_image, image_to_bitmap, bitmap_to_icon, resize_image ) #: Trait type for image arrays. ImageArray = Array(shape=(None, None, (3, 4)), dtype='uint8') @provides(IImage) class ArrayImage(HasStrictTraits): """ An IImage stored in an RGB(A) numpy array. """ # 'ArrayImage' interface ------------------------------------------------ #: The bytes of the image. data = ImageArray() # ------------------------------------------------------------------------ # 'IImage' interface. # ------------------------------------------------------------------------ def create_image(self, size=None): """ Creates a toolkit-specific image for this array. Parameters ---------- size : (int, int) or None The desired size as a width, height tuple, or None if wanting default image size. Returns ------- image : toolkit image The toolkit image corresponding to the image and the specified size. """ image = array_to_image(self.data) if size is not None: image = resize_image(image, size) return image def create_bitmap(self, size=None): """ Creates a toolkit-specific bitmap image for this array. Parameters ---------- size : (int, int) or None The desired size as a width, height tuple, or None if wanting default image size. Returns ------- image : toolkit bitmap The toolkit bitmap corresponding to the image and the specified size. """ return image_to_bitmap(self.create_image(size)) def create_icon(self, size=None): """ Creates a toolkit-specific icon for this array. Parameters ---------- size : (int, int) or None The desired size as a width, height tuple, or None if wanting default icon size. Returns ------- image : toolkit icon The toolkit image corresponding to the image and the specified size as an icon. """ return bitmap_to_icon(self.create_bitmap(size)) # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, data, **traits): super().__init__(data=data, **traits) pyface-7.4.0/pyface/sizers/0000755000076500000240000000000014176460550016530 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/sizers/__init__.py0000644000076500000240000000062714176222673020650 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! pyface-7.4.0/pyface/sizers/flow.py0000644000076500000240000001341414176222673020056 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 pyface.timer.api import do_later # ------------------------------------------------------------------------------- # 'FlowSizer' class: # ------------------------------------------------------------------------------- class FlowSizer(wx.PySizer): # --------------------------------------------------------------------------- # Initializes the object: # --------------------------------------------------------------------------- def __init__(self, orient=wx.HORIZONTAL): super().__init__() self._orient = orient self._frozen = False self._needed_size = None # --------------------------------------------------------------------------- # Calculates the minimum size needed by the sizer: # --------------------------------------------------------------------------- def CalcMin(self): """ Calculates the minimum size needed by the sizer. """ if self._needed_size is not None: return self._needed_size horizontal = self._orient == wx.HORIZONTAL dx = dy = 0 for item in self.GetChildren(): idx, idy = item.CalcMin() if horizontal: dy = max(dy, idy) else: dx = max(dx, idx) return wx.Size(dx, dy) # --------------------------------------------------------------------------- # Layout the contents of the sizer based on the sizer's current size and # position: # --------------------------------------------------------------------------- def RecalcSizes(self): """ Layout the contents of the sizer based on the sizer's current size and position. """ horizontal = self._orient == wx.HORIZONTAL x, y = self.GetPosition() dx, dy = self.GetSize().Get() x0, y0 = x, y ex = x + dx ey = y + dy mdx = mdy = sdx = sdy = 0 visible = True cur_max = 0 for item in self.GetChildren(): idx, idy = item.CalcMin() expand = item.GetFlag() & wx.EXPAND if horizontal: if (x > x0) and ((x + idx) > ex): x = x0 y += mdy + sdy mdy = sdy = 0 if y >= ey: visible = False cur_max = max(idy, cur_max) if expand: idy = cur_max if item.IsSpacer(): sdy = max(sdy, idy) if x == x0: idx = 0 item.SetDimension(wx.Point(x, y), wx.Size(idx, idy)) item.Show(visible) x += idx mdy = max(mdy, idy) else: if (y > y0) and ((y + idy) > ey): y = y0 x += mdx + sdx mdx = sdx = 0 if x >= ex: visible = False cur_max = max(idx, cur_max) if expand: idx = cur_max if item.IsSpacer(): sdx = max(sdx, idx) if y == y0: idy = 0 item.SetDimension(wx.Point(x, y), wx.Size(idx, idy)) item.Show(visible) y += idy mdx = max(mdx, idx) if (not visible) and (self._needed_size is None): max_dx = max_dy = 0 if horizontal: max_dy = max(dy, y + mdy + sdy - y0) else: max_dx = max(dx, x + mdx + sdx - x0) self._needed_size = wx.Size(max_dx, max_dy) if not self._frozen: self._do_parent("_freeze") do_later(self._do_parent, "_thaw") else: self._needed_size = None # --------------------------------------------------------------------------- # Prevents the specified window from doing any further screen updates: # --------------------------------------------------------------------------- def _freeze(self, window): """ Prevents the specified window from doing any further screen updates. """ window.Freeze() self._frozen = True # --------------------------------------------------------------------------- # Lays out a specified window and then allows it to be updated again: # --------------------------------------------------------------------------- def _thaw(self, window): """ Lays out a specified window and then allows it to be updated again. """ window.Layout() window.Refresh() if self._frozen: self._frozen = False window.Thaw() # --------------------------------------------------------------------------- # Does a specified operation on the sizer's parent window: # --------------------------------------------------------------------------- def _do_parent(self, method): """ Does a specified operation on the sizer's parent window. """ i = 0 while True: try: item = self.GetItem(i) if item is None: break i += 1 except: return if item.IsWindow(): getattr(self, method)(item.GetWindow().GetParent()) return pyface-7.4.0/pyface/timer/0000755000076500000240000000000014176460550016331 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/timer/i_timer.py0000644000076500000240000002012314176222673020333 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Interfaces and base classes for cross-toolkit timers This module defines interfaces for toolkit event-loop based timers. It also provides a base implementation that can be easily specialized for a particular back-end, and mixins that provide additional capabilities. """ from abc import abstractmethod import time from traits.api import ( ABCHasTraits, Bool, Callable, Dict, Event, Float, HasTraits, Int, Interface, Property, Range, Tuple, provides, Union, ) perf_counter = time.perf_counter class ITimer(Interface): """ Interface for timer classes. This is a base interface which doesn't specify any particular notification mechanism. """ # ITimer interface ------------------------------------------------------- #: The interval at which to call the callback in seconds. interval = Range(low=0.0) #: The number of times to repeat the callback, or None if no limit. repeat = Union(None, Int) #: The maximum length of time to run in seconds, or None if no limit. expire = Union(None, Float) #: Whether or not the timer is currently running. active = Bool() # ------------------------------------------------------------------------- # ITimer interface # ------------------------------------------------------------------------- @classmethod def timer(cls, **traits): """ Convenience method that creates and starts a timer. """ pass @classmethod def single_shot(cls, **traits): """ Convenience method that creates and starts a single-shot timer. """ pass def start(self): """ Start the timer. """ pass def stop(self): """ Stop the timer. """ pass def perform(self): """ The method that will be called by the timer. """ pass class IEventTimer(ITimer): """ Interface for timers which fire a trait event periodically. """ # IEventTimer interface -------------------------------------------------- #: A traits Event to fire when the callback happens. timeout = Event() class ICallbackTimer(ITimer): """ Interface for timers which call a callback periodically. """ # ICallbackTimer interface ----------------------------------------------- #: The callback to make, or None if no callback. callback = Callable #: Positional arguments to give the callback. args = Tuple() #: Keyword arguments to give the callback. kwargs = Dict() @provides(ITimer) class BaseTimer(ABCHasTraits): """ Base class for timer classes. This class has a class variable which tracks active timers to prevent failures caused by garbage collection. A timer is added to this tracker when it is started if the repeat value is not None. """ # BaseTimer interface ---------------------------------------------------- #: Class variable tracking all active timers. _active_timers = set() # ITimer interface ------------------------------------------------------- #: The interval at which to call the callback in seconds. interval = Range(low=0.0, value=0.05) #: The number of times to repeat the callback, or None if no limit. repeat = Union(None, Int) #: The maximum length of time to run in seconds, or None if no limit. expire = Union(None, Float) #: Property that controls the state of the timer. active = Property(Bool, observe="_active") # Private interface ------------------------------------------------------ #: Whether or not the timer is currently running. _active = Bool() #: The most recent start time. _start_time = Float() # ------------------------------------------------------------------------- # ITimer interface # ------------------------------------------------------------------------- @classmethod def timer(cls, **traits): """ Convenience method that creates and starts a timer. """ timer = cls(**traits) timer.start() return timer @classmethod def single_shot(cls, **traits): timer = cls(repeat=1, **traits) timer.start() return timer def start(self): """ Start the timer. """ if not self._active: if self.repeat is not None: self._active_timers.add(self) if self.expire is not None: self._start_time = perf_counter() self._active = True self._start() def stop(self): """ Stop the timer. """ if self._active: self._active_timers.discard(self) self._stop() self._active = False def perform(self): """ Perform the callback. The timer will stop if repeats is not None and less than 1, or if the `_perform` method raises StopIteration. """ if self.expire is not None: if perf_counter() - self._start_time > self.expire: self.stop() return if self.repeat is not None: self.repeat -= 1 try: self._perform() except StopIteration: self.stop() except: self.stop() raise else: if self.repeat is not None and self.repeat <= 0: self.stop() self.repeat = 0 # BaseTimer Protected methods def _start(self): """ Start the toolkit timer. Subclasses should overrided this method. """ raise NotImplementedError() def _stop(self): """ Stop the toolkit timer. Subclasses should overrided this method. """ raise NotImplementedError() @abstractmethod def _perform(self): """ perform the appropriate action. Subclasses should overrided this method. """ raise NotImplementedError() # ------------------------------------------------------------------------- # Private interface # ------------------------------------------------------------------------- # Trait property handlers ------------------------------------------------ def _get_active(self): return self._active def _set_active(self, value): if value: self.start() else: self.stop() @provides(IEventTimer) class MEventTimer(HasTraits): """ Mixin for event timer classes. Other code can listen to the `timeout` event using standard traits listeners. """ # IEventTimer interface -------------------------------------------------- #: A traits Event to fire when the callback happens. timeout = Event() # ------------------------------------------------------------------------- # ITimer interface # ------------------------------------------------------------------------- # ITimer Protected methods ----------------------------------------------- def _perform(self): """ Fire the event. """ self.timeout = True @provides(ITimer) class MCallbackTimer(HasTraits): """ Mixin for callback timer classes. """ # ICallbackTimer interface ----------------------------------------------- #: The callback to make. callback = Callable #: Positional arguments to give the callback. args = Tuple() #: Keyword arguments to give the callback. kwargs = Dict() # ------------------------------------------------------------------------- # ITimer interface # ------------------------------------------------------------------------- # ITimer Protected methods ----------------------------------------------- def _perform(self): """ Perform the callback. """ self.callback(*self.args, **self.kwargs) pyface-7.4.0/pyface/timer/timer.py0000644000076500000240000000400514176222673020024 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Event-loop based timers that perform actions periodically. Note that if a timer goes out of scope without a reference to being saved, there is nothing keeping the underlying toolkit timer alive and it will be garbage collected, meaning that the timer will stop firing (or indeed, may never fire). """ from pyface.toolkit import toolkit_object from pyface.timer.i_timer import MCallbackTimer, MEventTimer PyfaceTimer = toolkit_object("timer.timer:PyfaceTimer") class EventTimer(MEventTimer, PyfaceTimer): pass class CallbackTimer(MCallbackTimer, PyfaceTimer): pass class Timer(CallbackTimer): """ Subclass of CallbackTimer that matches the old API """ def __init__(self, millisecs, callable, *args, **kwargs): """ Initialize and start the timer. Initialize instance to invoke the given `callable` with given arguments and keyword args after every `millisecs` (milliseconds). """ interval = millisecs / 1000.0 super().__init__( interval=interval, callback=callable, args=args, kwargs=kwargs ) self.start() def Notify(self): """ Alias for `perform` to match old API. """ self.perform() def Start(self, millisecs=None): """ Alias for `start` to match old API. """ if millisecs is not None: self.interval = millisecs / 1000.0 self.start() def Stop(self): """ Alias for `stop` to match old API. """ self.stop() def IsRunning(self): """ Alias for is_running property to match old API. """ return self._active pyface-7.4.0/pyface/timer/tests/0000755000076500000240000000000014176460550017473 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/timer/tests/__init__.py0000644000076500000240000000000014176222673021574 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/timer/tests/test_timer.py0000644000076500000240000003123614176222673022233 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 unittest import TestCase, skipIf from pyface.toolkit import toolkit_object from ..i_timer import perf_counter from ..timer import CallbackTimer, EventTimer, Timer GuiTestAssistant = toolkit_object("util.gui_test_assistant:GuiTestAssistant") no_gui_test_assistant = GuiTestAssistant.__name__ == "Unimplemented" class ConditionHandler(object): def __init__(self): self.count = 0 self.times = [] self.called = False def callback(self, event=None): self.times.append(perf_counter()) self.count += 1 self.called = True def is_called(self): return self.called def called_n(self, repeat): return lambda: self.count >= repeat @skipIf(no_gui_test_assistant, "No GuiTestAssistant") class TestEventTimer(TestCase, GuiTestAssistant): """ Test the EventTimer. """ def setUp(self): GuiTestAssistant.setUp(self) def tearDown(self): GuiTestAssistant.tearDown(self) def test_basic(self): timer = EventTimer() self.assertIsNone(timer.repeat) self.assertFalse(timer.active) timer.start() try: self.assertTrue(timer.active) self.event_loop_helper.event_loop() self.assertTrue(timer.active) finally: timer.stop() self.assertFalse(timer.active) def test_timer_method(self): timer = EventTimer.timer() try: self.assertTrue(timer.active) self.event_loop_helper.event_loop() self.assertTrue(timer.active) finally: timer.stop() self.assertFalse(timer.active) def test_single_shot_method(self): timer = EventTimer.single_shot() handler = ConditionHandler() timer.observe(handler.callback, "timeout") try: self.assertTrue(timer.active) self.event_loop_helper.event_loop_until_condition( lambda: not timer.active ) self.assertFalse(timer.active) finally: timer.stop() self.assertFalse(timer.active) self.assertEqual(handler.count, 1) def test_set_active(self): timer = EventTimer() self.assertIsNone(timer.repeat) self.assertFalse(timer.active) timer.active = True try: self.assertTrue(timer.active) self.event_loop_helper.event_loop() self.assertTrue(timer.active) finally: timer.active = False self.assertFalse(timer.active) def test_timeout_event(self): timer = EventTimer() handler = ConditionHandler() timer.observe(handler.callback, "timeout") timer.start() try: self.event_loop_helper.event_loop_until_condition( handler.is_called ) finally: timer.stop() def test_repeat(self): timer = EventTimer(repeat=4) handler = ConditionHandler() timer.observe(handler.callback, "timeout") timer.start() try: self.event_loop_helper.event_loop_until_condition( lambda: not timer.active ) self.assertFalse(timer.active) finally: timer.stop() self.assertEqual(handler.count, 4) def test_interval(self): timer = EventTimer(repeat=4, interval=0.1) handler = ConditionHandler() timer.observe(handler.callback, "timeout") timer.start() try: self.event_loop_helper.event_loop_until_condition( lambda: not timer.active ) self.assertFalse(timer.active) finally: timer.stop() self.assertEqual(handler.count, 4) expected_times = [timer._start_time + 0.1 * i + 0.1 for i in range(4)] # give feedback in case of failure if not all( expected <= actual for expected, actual in zip(expected_times, handler.times) ): print(handler.times) self.assertTrue( all( expected <= actual for expected, actual in zip(expected_times, handler.times) ) ) def test_expire(self): timer = EventTimer(expire=1.0, interval=0.1) handler = ConditionHandler() timer.observe(handler.callback, "timeout") timer.start() try: self.event_loop_helper.event_loop_until_condition( lambda: not timer.active ) self.assertFalse(timer.active) finally: timer.stop() # give feedback in case of failure if not all( t < timer._start_time + timer.expire + 0.01 for t in handler.times ): print(handler.times[-1], timer._start_time + timer.expire) self.assertTrue( all( t < timer._start_time + timer.expire + 0.01 for t in handler.times ) ) @skipIf(no_gui_test_assistant, "No GuiTestAssistant") class TestCallbackTimer(TestCase, GuiTestAssistant): """ Test the CallbackTimer. """ def setUp(self): GuiTestAssistant.setUp(self) def tearDown(self): GuiTestAssistant.tearDown(self) def test_basic(self): handler = ConditionHandler() timer = CallbackTimer(callback=handler.callback) self.assertIsNone(timer.repeat) self.assertFalse(timer.active) timer.start() try: self.assertTrue(timer.active) self.event_loop_helper.event_loop() self.assertTrue(timer.active) finally: timer.stop() self.assertFalse(timer.active) def test_timer_method(self): handler = ConditionHandler() timer = CallbackTimer.timer(callback=handler.callback) try: self.assertTrue(timer.active) self.event_loop_helper.event_loop() self.assertTrue(timer.active) finally: timer.stop() self.assertFalse(timer.active) def test_single_shot_method(self): handler = ConditionHandler() timer = CallbackTimer.single_shot(callback=handler.callback) try: self.assertTrue(timer.active) self.event_loop_helper.event_loop_until_condition( lambda: not timer.active ) self.assertFalse(timer.active) finally: timer.stop() self.assertFalse(timer.active) self.assertEqual(handler.count, 1) def test_set_active(self): handler = ConditionHandler() timer = CallbackTimer(callback=handler.callback) self.assertIsNone(timer.repeat) self.assertFalse(timer.active) timer.active = True try: self.assertTrue(timer.active) self.event_loop_helper.event_loop() self.assertTrue(timer.active) finally: timer.active = False self.assertFalse(timer.active) def test_timeout_event(self): handler = ConditionHandler() timer = CallbackTimer(callback=handler.callback) timer.start() try: self.event_loop_helper.event_loop_until_condition( handler.is_called ) finally: timer.stop() def test_repeat(self): handler = ConditionHandler() timer = CallbackTimer(callback=handler.callback, repeat=4) timer.start() try: self.event_loop_helper.event_loop_until_condition( lambda: not timer.active ) self.assertFalse(timer.active) finally: timer.stop() self.assertEqual(handler.count, 4) def test_interval(self): handler = ConditionHandler() timer = CallbackTimer( callback=handler.callback, repeat=4, interval=0.1 ) timer.start() try: self.event_loop_helper.event_loop_until_condition( lambda: not timer.active ) self.assertFalse(timer.active) finally: timer.stop() self.assertEqual(handler.count, 4) expected_times = [timer._start_time + 0.1 * i + 0.1 for i in range(4)] # give feedback in case of failure if not all( expected <= actual for expected, actual in zip(expected_times, handler.times) ): print(handler.times) self.assertTrue( all( expected <= actual for expected, actual in zip(expected_times, handler.times) ) ) def test_expire(self): handler = ConditionHandler() timer = CallbackTimer( callback=handler.callback, interval=0.1, expire=1.0 ) timer.start() try: self.event_loop_helper.event_loop_until_condition( lambda: not timer.active ) self.assertFalse(timer.active) finally: timer.stop() # give feedback in case of failure if not all( t < timer._start_time + timer.expire + 0.01 for t in handler.times ): print(handler.times[-1], timer._start_time + timer.expire) self.assertTrue( all( t < timer._start_time + timer.expire + 0.01 for t in handler.times ) ) def test_stop_iteration(self): def do_stop_iteration(): raise StopIteration() timer = CallbackTimer(callback=do_stop_iteration) timer.start() try: self.event_loop_helper.event_loop_until_condition( lambda: not timer.active ) self.assertFalse(timer.active) finally: timer.stop() @skipIf(no_gui_test_assistant, "No GuiTestAssistant") class TestTimer(TestCase, GuiTestAssistant): """ Test the CallbackTimer. """ def setUp(self): GuiTestAssistant.setUp(self) def tearDown(self): GuiTestAssistant.tearDown(self) def test_basic(self): handler = ConditionHandler() timer = Timer(250, handler.callback) try: self.assertTrue(timer.IsRunning()) self.event_loop_helper.event_loop() self.assertTrue(timer.IsRunning()) finally: timer.Stop() self.assertFalse(timer.IsRunning()) def test_restart(self): handler = ConditionHandler() timer = Timer(20, handler.callback) timer.Stop() # Ensure that it is indeed stopped. self.assertFalse(timer.IsRunning()) count = handler.count # Wait to see if the timer has indeed stopped. self.event_loop_helper.event_loop() time.sleep(0.1) self.assertEqual(handler.count, count) timer.Start() try: self.assertTrue(timer.IsRunning()) self.event_loop_helper.event_loop_until_condition( lambda: handler.count > count ) self.assertTrue(timer.IsRunning()) finally: timer.Stop() self.assertFalse(timer.IsRunning()) def test_repeat(self): handler = ConditionHandler() start_time = perf_counter() timer = Timer(250, handler.callback) try: self.assertTrue(timer.IsRunning()) self.event_loop_helper.event_loop_until_condition( handler.called_n(4) ) self.assertTrue(timer.IsRunning()) finally: timer.Stop() self.assertFalse(timer.IsRunning()) # The callback may be called more than 4 times depending # on the order when condition timer in event_loop_unit_condition # is called relative to the Timer here. Timer accuracy also depends # on whether the event loop is interrupted by the system. # The objective is that the timer should not be called too frequently. expected_times = [ start_time + 0.2 * i + 0.2 for i in range(handler.count) ] self.assertTrue( all( actual >= expected for actual, expected in zip(handler.times, expected_times) ), "Expected calls after {} times, got {})".format( expected_times, handler.times ), ) pyface-7.4.0/pyface/timer/tests/test_do_later.py0000644000076500000240000000746214176222673022710 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.toolkit import toolkit_object from ..i_timer import perf_counter from ..do_later import DoLaterTimer, do_after, do_later GuiTestAssistant = toolkit_object("util.gui_test_assistant:GuiTestAssistant") no_gui_test_assistant = GuiTestAssistant.__name__ == "Unimplemented" class ConditionHandler(object): def __init__(self): self.count = 0 self.times = [] def callback(self): self.times.append(perf_counter()) self.count += 1 @skipIf(no_gui_test_assistant, "No GuiTestAssistant") class TestDoLaterTimer(TestCase, GuiTestAssistant): """ Test the DoLaterTimer. """ def setUp(self): GuiTestAssistant.setUp(self) def tearDown(self): GuiTestAssistant.tearDown(self) def test_basic(self): handler = ConditionHandler() start_time = perf_counter() length = 500 timer = DoLaterTimer(length, handler.callback, (), {}) try: self.assertTrue(timer.active) self.event_loop_helper.event_loop_until_condition( lambda: not timer.active ) self.assertFalse(timer.active) finally: timer.stop() self.assertEqual(handler.count, 1) # should take no less than 85% of time requested expected_length = (length / 1000.0) * 0.85 expected_time = start_time + expected_length self.assertLessEqual( expected_time, handler.times[0], "Expected call after {} seconds, took {} seconds)".format( expected_length, handler.times[0] - start_time ), ) @skipIf(no_gui_test_assistant, "No GuiTestAssistant") class TestDoLater(TestCase, GuiTestAssistant): """ Test do_later. """ def setUp(self): GuiTestAssistant.setUp(self) def tearDown(self): GuiTestAssistant.tearDown(self) def test_basic(self): handler = ConditionHandler() timer = do_later(handler.callback) try: self.assertTrue(timer.active) self.event_loop_helper.event_loop_until_condition( lambda: not timer.active ) self.assertFalse(timer.active) finally: timer.stop() self.assertEqual(handler.count, 1) @skipIf(no_gui_test_assistant, "No GuiTestAssistant") class TestDoAfter(TestCase, GuiTestAssistant): """ Test do_after. """ def setUp(self): GuiTestAssistant.setUp(self) def tearDown(self): GuiTestAssistant.tearDown(self) def test_basic(self): handler = ConditionHandler() start_time = perf_counter() length = 500 timer = do_after(length, handler.callback) try: self.assertTrue(timer.active) self.event_loop_helper.event_loop_until_condition( lambda: not timer.active ) self.assertFalse(timer.active) finally: timer.stop() self.assertEqual(handler.count, 1) # should take no less than 85% of time requested expected_length = (length / 1000.0) * 0.85 expected_time = start_time + expected_length self.assertLessEqual( expected_time, handler.times[0], "Expected call after {} seconds, took {} seconds)".format( expected_length, handler.times[0] - start_time ), ) pyface-7.4.0/pyface/timer/__init__.py0000644000076500000240000000000014176222673020432 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/timer/api.py0000644000076500000240000000153314176222673017460 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 ``pyface.timer`` subpackage. - :func:`~.do_later` - :func:`~.do_after` - :class:`~.DoLaterTimer` - :class:`~.CallbackTimer` - :class:`~.EventTimer` - :class:`~.Timer` Interfaces ---------- - :class:`~.ICallbackTimer` - :class:`~.IEventTimer` - :class:`~.ITimer` """ from .do_later import do_later, do_after, DoLaterTimer from .i_timer import ICallbackTimer, IEventTimer, ITimer from .timer import CallbackTimer, EventTimer, Timer pyface-7.4.0/pyface/timer/do_later.py0000644000076500000240000000373314176222673020504 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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: Enthought, Inc. # Description: from pyface.timer.timer import CallbackTimer, Timer class DoLaterTimer(Timer): """ Performs a callback once at a later time. This is not used by the `do_later` functions and is only provided for backwards compatibility of the API. """ #: The perform the callback once. repeat = 1 def __init__(self, interval, callable, args, kw_args): # Adapt the old DoLaterTimer initializer to the Timer initializer. super().__init__(interval, callable, *args, **kw_args) def do_later(callable, *args, **kwargs): """ Does something 50 milliseconds from now. Parameters ---------- callable : callable The callable to call in 50ms time. args, kwargs : tuple, dict Arguments to be passed through to the callable. """ return CallbackTimer.single_shot( interval=0.05, callback=callable, args=args, kwargs=kwargs ) def do_after(interval, callable, *args, **kwargs): """ Does something after some specified time interval. Parameters ---------- interval : float The time interval in milliseconds to wait before calling. callable : callable The callable to call. args Positional arguments to be passed through to the callable. kwargs Keyword arguments to be passed through to the callable. Arguments to be passed through to the callable. """ return CallbackTimer.single_shot( interval=interval / 1000.0, callback=callable, args=args, kwargs=kwargs ) pyface-7.4.0/pyface/split_panel.py0000644000076500000240000000312614176222673020101 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 panel that is split in two either horizontally or vertically. """ import warnings from pyface.split_widget import SplitWidget from pyface.widget import Widget class SplitPanel(Widget, SplitWidget): """ A panel that is split in two either horizontally or vertically. """ # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, parent=None, **traits): """ Creates a new panel. """ create = traits.pop("create", True) # Base class constructor. super().__init__(parent=parent, **traits) if create: # Create the widget's toolkit-specific control. self.create() warnings.warn( "automatic widget creation is deprecated and will be removed " "in a future Pyface version, use create=False and explicitly " "call create() for future behaviour", PendingDeprecationWarning, ) def _create_control(self, parent): """ Create the toolkit control """ return self._create_splitter(parent) pyface-7.4.0/pyface/i_about_dialog.py0000644000076500000240000000217514176222673020533 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 interface for a simple 'About' dialog. """ from traits.api import HasTraits, List, Str from pyface.i_dialog import IDialog from pyface.ui_traits import Image class IAboutDialog(IDialog): """ The interface for a simple 'About' dialog. """ # 'IAboutDialog' interface --------------------------------------------- #: Additional strings to be added to the dialog. additions = List(Str) #: Additional copyright strings to be added above the standard ones. copyrights = List(Str) #: The image displayed in the dialog. image = Image() class MAboutDialog(HasTraits): """ The mixin class that contains common code for toolkit specific implementations of the IAboutDialog interface. """ pyface-7.4.0/pyface/tests/0000755000076500000240000000000014176460550016353 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/tests/test_split_application_window.py0000644000076500000240000000700314176222673025073 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 ..heading_text import HeadingText from ..split_application_window import SplitApplicationWindow from ..toolkit import toolkit_object GuiTestAssistant = toolkit_object("util.gui_test_assistant:GuiTestAssistant") no_gui_test_assistant = GuiTestAssistant.__name__ == "Unimplemented" @unittest.skipIf(no_gui_test_assistant, "No GuiTestAssistant") class TestSplitApplicationWindow(unittest.TestCase, GuiTestAssistant): def setUp(self): GuiTestAssistant.setUp(self) self.window = SplitApplicationWindow() def tearDown(self): if self.window.control is not None: with self.delete_widget(self.window.control): self.window.destroy() del self.window GuiTestAssistant.tearDown(self) def test_destroy(self): # test that destroy works even when no control with self.event_loop(): self.window.destroy() def test_open_close(self): # test that opening and closing works as expected with self.assertTraitChanges(self.window, "opening", count=1): with self.assertTraitChanges(self.window, "opened", count=1): with self.event_loop(): self.window.open() with self.assertTraitChanges(self.window, "closing", count=1): with self.assertTraitChanges(self.window, "closed", count=1): with self.event_loop(): self.window.close() def test_horizontal_split(self): # test that horizontal split works self.window.direction = "horizontal" with self.assertTraitChanges(self.window, "opening", count=1): with self.assertTraitChanges(self.window, "opened", count=1): with self.event_loop(): self.window.open() with self.assertTraitChanges(self.window, "closing", count=1): with self.assertTraitChanges(self.window, "closed", count=1): with self.event_loop(): self.window.close() def test_contents(self): # test that contents works self.window.lhs = HeadingText self.window.rhs = HeadingText with self.assertTraitChanges(self.window, "opening", count=1): with self.assertTraitChanges(self.window, "opened", count=1): with self.event_loop(): self.window.open() with self.assertTraitChanges(self.window, "closing", count=1): with self.assertTraitChanges(self.window, "closed", count=1): with self.event_loop(): self.window.close() def test_ratio(self): # test that ratio split works self.window.ratio = 0.25 with self.assertTraitChanges(self.window, "opening", count=1): with self.assertTraitChanges(self.window, "opened", count=1): with self.event_loop(): self.window.open() with self.assertTraitChanges(self.window, "closing", count=1): with self.assertTraitChanges(self.window, "closed", count=1): with self.event_loop(): self.window.close() pyface-7.4.0/pyface/tests/test_array_image.py0000644000076500000240000000251614176222673022252 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.optional_dependencies import numpy as np, requires_numpy from ..array_image import ArrayImage @requires_numpy class TestArrayImage(unittest.TestCase): def setUp(self): self.data = np.full((32, 64, 4), 0xee, dtype='uint8') def test_init(self): image = ArrayImage(self.data) self.assertIs(image.data, self.data) def test_init_data_required(self): with self.assertRaises(TypeError): ArrayImage() def test_create_image(self): image = ArrayImage(self.data) toolkit_image = image.create_image() self.assertIsNotNone(toolkit_image) def test_create_bitmap(self): image = ArrayImage(self.data) bitmap = image.create_bitmap() self.assertIsNotNone(bitmap) def test_create_icon(self): image = ArrayImage(self.data) icon = image.create_icon() self.assertIsNotNone(icon) pyface-7.4.0/pyface/tests/test_toolkit.py0000644000076500000240000000656414176222673021466 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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: # Starting Python 3.8, importlib.metadata is available in the Python # standard library and starting Python 3.10, the "select" interface is # available on EntryPoints. from importlib.metadata import entry_points except ImportError: from importlib_metadata import entry_points import pyface.toolkit class TestToolkit(unittest.TestCase): def test_missing_import(self): # test that we get an undefined object if no toolkit implementation cls = pyface.toolkit.toolkit_object("tests:Missing") with self.assertRaises(NotImplementedError): cls() def test_bad_import(self): # test that we don't filter unrelated import errors with self.assertRaises(ImportError): pyface.toolkit.toolkit_object("tests.bad_import:Missing") def test_core_plugins(self): # test that we can see appropriate core entrypoints # This compatibility layer can be removed when we drop support for # Python < 3.10. Ref https://github.com/enthought/pyface/issues/999. all_entry_points = entry_points() if hasattr(all_entry_points, "select"): plugins = { ep.name for ep in entry_points().select(group='pyface.toolkits') } else: plugins = { ep.name for ep in entry_points()['pyface.toolkits'] } self.assertLessEqual({"qt4", "wx", "qt", "null"}, plugins) def test_toolkit_object(self): # test that the Toolkit class works as expected # note that if this fails many other things will too from pyface.tests.test_new_toolkit.init import toolkit_object from pyface.tests.test_new_toolkit.widget import Widget as TestWidget Widget = toolkit_object("widget:Widget") self.assertEqual(Widget, TestWidget) def test_toolkit_object_overriden(self): # test that the Toolkit class search paths can be overridden from pyface.tests.test_new_toolkit.widget import Widget as TestWidget toolkit_object = pyface.toolkit.toolkit_object old_packages = toolkit_object.packages toolkit_object.packages = [ "pyface.tests.test_new_toolkit" ] + old_packages try: Widget = toolkit_object("widget:Widget") self.assertEqual(Widget, TestWidget) finally: toolkit_object.packages = old_packages def test_toolkit_object_not_overriden(self): # test that the Toolkit class works when object not overridden toolkit_object = pyface.toolkit.toolkit_object TestWindow = toolkit_object("window:Window") old_packages = toolkit_object.packages toolkit_object.packages = [ "pyface.tests.test_new_toolkit" ] + old_packages try: Window = toolkit_object("window:Window") self.assertEqual(Window, TestWindow) finally: toolkit_object.packages = old_packages pyface-7.4.0/pyface/tests/test_splash_screen_log_handler.py0000644000076500000240000000241414176222673025156 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 ..splash_screen_log_handler import SplashScreenLogHandler class DummySplashScreen(HasTraits): text = Str("original") class DummyRecord(object): def __init__(self, message): self.message = message def getMessage(self): return self.message class TestSplashScreenLogHandler(unittest.TestCase): def setUp(self): self.ss = DummySplashScreen() self.sslh = SplashScreenLogHandler(self.ss) def test_unicode_message(self): self.assertEqual(self.ss.text, "original") message = "G\u00f6khan" self.sslh.emit(DummyRecord(message)) self.assertEqual(self.ss.text, message + "...") def test_ascii_message(self): message = "Goekhan" self.sslh.emit(DummyRecord(message)) self.assertEqual(self.ss.text, message + "...") pyface-7.4.0/pyface/tests/python_shell_script.py0000644000076500000240000000103314176222673023020 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # dummy script for testing python shell and python editor widgets # simple import import sys # noqa: F401 # set a variable x = 1 pyface-7.4.0/pyface/tests/test_about_dialog.py0000644000076500000240000000753414176222673022430 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 ..about_dialog import AboutDialog from ..constant import OK, CANCEL from ..toolkit import toolkit_object from ..window import Window GuiTestAssistant = toolkit_object("util.gui_test_assistant:GuiTestAssistant") no_gui_test_assistant = GuiTestAssistant.__name__ == "Unimplemented" ModalDialogTester = toolkit_object( "util.modal_dialog_tester:ModalDialogTester" ) no_modal_dialog_tester = ModalDialogTester.__name__ == "Unimplemented" @unittest.skipIf(no_gui_test_assistant, "No GuiTestAssistant") class TestAboutDialog(unittest.TestCase, GuiTestAssistant): def setUp(self): GuiTestAssistant.setUp(self) self.dialog = AboutDialog() def tearDown(self): if self.dialog.control is not None: with self.delete_widget(self.dialog.control): self.dialog.destroy() self.dialog = None GuiTestAssistant.tearDown(self) def test_create(self): # test that creation and destruction works as expected with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() def test_destroy(self): # test that destroy works even when no control with self.event_loop(): self.dialog.destroy() def test_create_parent(self): # test that creation and destruction works as expected with a parent parent = Window() self.dialog.parent = parent.control with self.event_loop(): parent._create() self.dialog._create() with self.event_loop(): self.dialog.destroy() with self.event_loop(): parent.destroy() @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_accept(self): # test that accept works as expected # XXX duplicate of Dialog test, not needed? tester = ModalDialogTester(self.dialog.open) tester.open_and_run(when_opened=lambda x: x.close(accept=True)) self.assertEqual(tester.result, OK) self.assertEqual(self.dialog.return_code, OK) @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_close(self): # test that closing works as expected # XXX duplicate of Dialog test, not needed? tester = ModalDialogTester(self.dialog.open) tester.open_and_run(when_opened=lambda x: self.dialog.close()) self.assertEqual(tester.result, CANCEL) self.assertEqual(self.dialog.return_code, CANCEL) @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_parent(self): # test that lifecycle works with a parent parent = Window() self.dialog.parent = parent.control parent.open() tester = ModalDialogTester(self.dialog.open) tester.open_and_run(when_opened=lambda x: x.close(accept=True)) with self.event_loop(): parent.close() self.assertEqual(tester.result, OK) self.assertEqual(self.dialog.return_code, OK) def test__create_html(self): # test that the html content is properly created self.dialog.additions.extend(["test line 1", "test line 2"]) self.dialog.copyrights.extend(["copyright", "copyleft"]) html = self.dialog._create_html() self.assertIn("test line 1
test line 2
", html) self.assertIn( "Copyright © copyright
Copyright © copyleft", html ) pyface-7.4.0/pyface/tests/test_font_dialog.py0000644000076500000240000000555614176222673022266 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 ..font import Font from ..font_dialog import FontDialog, get_font from ..toolkit import toolkit_object from ..util.font_parser import simple_parser GuiTestAssistant = toolkit_object("util.gui_test_assistant:GuiTestAssistant") no_gui_test_assistant = GuiTestAssistant.__name__ == "Unimplemented" ModalDialogTester = toolkit_object( "util.modal_dialog_tester:ModalDialogTester" ) no_modal_dialog_tester = ModalDialogTester.__name__ == "Unimplemented" @unittest.skipIf(no_gui_test_assistant, "No GuiTestAssistant") class TestFontDialog(unittest.TestCase, GuiTestAssistant): def setUp(self): GuiTestAssistant.setUp(self) self.dialog = FontDialog(font="12 Helvetica sans-serif") def tearDown(self): if self.dialog.control is not None: with self.delete_widget(self.dialog.control): self.dialog.destroy() del self.dialog GuiTestAssistant.tearDown(self) def test_font(self): # test that fonts are translated as expected self.dialog.font = "10 bold condensed Helvetica sans-serif" self.assertFontEqual( self.dialog.font, Font(**simple_parser("10 bold condensed Helvetica sans-serif")), ) def test_create(self): # test that creation and destruction works as expected with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() def test_destroy(self): # test that destroy works even when no control with self.event_loop(): self.dialog.destroy() def test_close(self): # test that close works with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.close() def assertFontEqual(self, font1, font2): state1 = font1.trait_get(transient=lambda x: not x) state2 = font2.trait_get(transient=lambda x: not x) self.assertEqual(state1, state2) @unittest.skipIf(no_gui_test_assistant, "No GuiTestAssistant") class TestGetFont(unittest.TestCase, GuiTestAssistant): @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_close(self): # test that cancel works as expected tester = ModalDialogTester( lambda: get_font(None, "12 Helvetica sans-serif") ) tester.open_and_run(when_opened=lambda x: x.close(accept=False)) self.assertEqual(tester.result, None) pyface-7.4.0/pyface/tests/test_split_panel.py0000644000076500000240000000552314176222673022305 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 ..heading_text import HeadingText from ..split_panel import SplitPanel from ..toolkit import toolkit_object from ..window import Window GuiTestAssistant = toolkit_object("util.gui_test_assistant:GuiTestAssistant") no_gui_test_assistant = GuiTestAssistant.__name__ == "Unimplemented" @unittest.skipIf(no_gui_test_assistant, "No GuiTestAssistant") class TestHeadingText(unittest.TestCase, GuiTestAssistant): def setUp(self): GuiTestAssistant.setUp(self) self.window = Window() self.window._create() def tearDown(self): if self.widget.control is not None: with self.delete_widget(self.widget.control): self.widget.destroy() if self.window.control is not None: with self.delete_widget(self.window.control): self.window.destroy() del self.widget del self.window GuiTestAssistant.tearDown(self) def test_lifecycle(self): # test that destroy works with self.event_loop(): with self.assertWarns(PendingDeprecationWarning): self.widget = SplitPanel(self.window.control) self.assertIsNotNone(self.widget.control) with self.event_loop(): self.widget.destroy() def test_two_stage_create(self): # test that create=False works self.widget = SplitPanel(create=False) self.assertIsNone(self.widget.control) with self.event_loop(): self.widget.parent = self.window.control self.widget.create() self.assertIsNotNone(self.widget.control) with self.event_loop(): self.widget.destroy() def test_horizontal(self): # test that horizontal split works with self.event_loop(): self.widget = SplitPanel( self.window.control, direction="horizontal" ) with self.event_loop(): self.widget.destroy() def test_ratio(self): # test that ratio works with self.event_loop(): self.widget = SplitPanel(self.window.control, ratio=0.25) with self.event_loop(): self.widget.destroy() def test_contents(self): # test that contents works with self.event_loop(): self.widget = SplitPanel( self.window.control, lhs=HeadingText, rhs=HeadingText ) with self.event_loop(): self.widget.destroy() pyface-7.4.0/pyface/tests/test_progress_dialog.py0000644000076500000240000001224714176222673023157 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 ..progress_dialog import ProgressDialog from ..toolkit import toolkit_object GuiTestAssistant = toolkit_object("util.gui_test_assistant:GuiTestAssistant") no_gui_test_assistant = GuiTestAssistant.__name__ == "Unimplemented" ModalDialogTester = toolkit_object( "util.modal_dialog_tester:ModalDialogTester" ) no_modal_dialog_tester = ModalDialogTester.__name__ == "Unimplemented" @unittest.skipIf(no_gui_test_assistant, "No GuiTestAssistant") class TestProgressDialog(unittest.TestCase, GuiTestAssistant): def setUp(self): GuiTestAssistant.setUp(self) self.dialog = ProgressDialog() def tearDown(self): if self.dialog.control is not None: with self.delete_widget(self.dialog.control): self.dialog.destroy() del self.dialog GuiTestAssistant.tearDown(self) def test_create(self): # test that creation and destruction works as expected with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() def test_destroy(self): # test that destroy works even when no control with self.event_loop(): self.dialog.destroy() def test_can_cancel(self): # test that creation works with can_cancel self.dialog.can_cancel = True with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() def test_can_ok(self): # test that creation works with can_ok self.dialog.can_ok = True with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() def test_show_time(self): # test that creation works with show_time self.dialog.show_time = True with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() @unittest.skip("not implemented in any backend") def test_show_percent(self): # test that creation works with show_percent self.dialog.show_percent = True with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() def test_update(self): self.dialog.min = 0 self.dialog.max = 10 self.dialog.open() for i in range(11): with self.event_loop(): result = self.dialog.update(i) self.assertEqual(result, (True, False)) self.assertIsNone(self.dialog.control) @unittest.skip("inconsistent implementations") def test_update_no_control(self): self.dialog.min = 0 self.dialog.max = 10 with self.event_loop(): result = self.dialog.update(1) self.assertEqual(result, (None, None)) def test_incomplete_update(self): self.dialog.min = 0 self.dialog.max = 10 self.can_cancel = True self.dialog.open() for i in range(5): with self.event_loop(): result = self.dialog.update(i) self.assertEqual(result, (True, False)) self.assertIsNotNone(self.dialog.control) with self.event_loop(): self.dialog.close() self.assertIsNone(self.dialog.control) def test_change_message(self): self.dialog.min = 0 self.dialog.max = 10 self.dialog.open() for i in range(11): with self.event_loop(): self.dialog.change_message("Updating {}".format(i)) result = self.dialog.update(i) self.assertEqual(result, (True, False)) self.assertEqual(self.dialog.message, "Updating {}".format(i)) self.assertIsNone(self.dialog.control) def test_update_show_time(self): self.dialog.min = 0 self.dialog.max = 10 self.dialog.show_time = True self.dialog.open() for i in range(11): with self.event_loop(): result = self.dialog.update(i) self.assertEqual(result, (True, False)) self.assertIsNone(self.dialog.control) def test_update_degenerate(self): self.dialog.min = 0 self.dialog.max = 0 self.dialog.open() for i in range(10): with self.event_loop(): result = self.dialog.update(i) self.assertEqual(result, (True, False)) with self.event_loop(): self.dialog.close() # XXX not really sure what correct behaviour is here def test_update_negative(self): self.dialog.min = 0 self.dialog.max = -10 with self.assertRaises(AttributeError): with self.event_loop(): self.dialog.open() self.assertIsNone(self.dialog.control) pyface-7.4.0/pyface/tests/test_gui_application.py0000644000076500000240000002254614176222673023146 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 from shutil import rmtree from tempfile import mkdtemp import unittest from traits.api import Bool, observe from ..application_window import ApplicationWindow from ..gui_application import GUIApplication from ..toolkit import toolkit_object GuiTestAssistant = toolkit_object("util.gui_test_assistant:GuiTestAssistant") no_gui_test_assistant = GuiTestAssistant.__name__ == "Unimplemented" EVENTS = [ "starting", "started", "application_initialized", "stopping", "stopped", ] class TestingApp(GUIApplication): #: Whether the app should start cleanly. start_cleanly = Bool(True) #: Whether the app should stop cleanly. stop_cleanly = Bool(True) #: Whether to try invoking exit method. do_exit = Bool(False) #: Whether the exit should be invoked as an error exit. error_exit = Bool(False) #: Whether to try force the exit (ie. ignore vetoes). force_exit = Bool(False) #: Whether to veto a call to the exit method. veto_exit = Bool(False) #: Whether to veto a opening a window. veto_open_window = Bool(False) #: Whether to veto a closing a window. veto_close_window = Bool(False) #: Whether or not a call to the open a window was vetoed. window_open_vetoed = Bool(False) #: Whether or not a call to the exit method was vetoed. exit_vetoed = Bool(False) #: Whether exit preparation happened. exit_prepared = Bool(False) #: Whether exit preparation raises an error. exit_prepared_error = Bool(False) def start(self): if not self.start_cleanly: return False super().start() window = self.windows[0] window.observe(self._on_window_closing, "closing") return True def stop(self): super().stop() return self.stop_cleanly def _on_window_closing(self, event): window = event.new if self.veto_close_window and not self.exit_vetoed: window.veto = True self.exit_vetoed = True @observe("application_initialized") def _update_window_open_vetoed(self, event): self.window_open_vetoed = ( len(self.windows) > 0 and self.windows[0].control is None ) @observe('exiting') def _set_veto_on_exiting_event(self, event): vetoable_event = event.new vetoable_event.veto = self.veto_exit self.exit_vetoed = self.veto_exit def _prepare_exit(self): super()._prepare_exit() if not self.exit_vetoed: self.exit_prepared = True if self.exit_prepared_error: raise Exception("Exit preparation failed") @observe("windows:items:opening") def _on_activate_window(self, event): if self.veto_open_window: window = event.new window.veto = self.veto_open_window @unittest.skipIf(no_gui_test_assistant, "No GuiTestAssistant") class TestGUIApplication(unittest.TestCase, GuiTestAssistant): def setUp(self): GuiTestAssistant.setUp(self) self.application_events = [] if toolkit_object.toolkit == "wx": import wx self.event_loop() wx.GetApp().DeletePendingEvents() else: self.event_loop() def tearDown(self): GuiTestAssistant.tearDown(self) def event_listener(self, event): application_event = event.new self.application_events.append(application_event) def connect_listeners(self, app): for event in EVENTS: app.observe(self.event_listener, event) def test_defaults(self): from traits.etsconfig.api import ETSConfig app = GUIApplication() self.assertEqual(app.home, ETSConfig.application_home) self.assertEqual(app.user_data, ETSConfig.user_data) self.assertEqual(app.company, ETSConfig.company) def test_initialize_application_home(self): dirpath = mkdtemp() home = os.path.join(dirpath, "test") app = GUIApplication(home=home) app.initialize_application_home() try: self.assertTrue(os.path.exists(home)) finally: rmtree(dirpath) def test_lifecycle(self): app = GUIApplication() self.connect_listeners(app) window = ApplicationWindow() app.observe(lambda _: app.add_window(window), "started") with self.assertMultiTraitChanges([app], EVENTS, []): self.gui.invoke_after(1000, app.exit) result = app.run() self.assertTrue(result) event_order = [event.event_type for event in self.application_events] self.assertEqual(event_order, EVENTS) self.assertEqual(app.windows, []) def test_exit_prepare_error(self): app = TestingApp(exit_prepared_error=True) self.connect_listeners(app) with self.assertMultiTraitChanges([app], EVENTS, []): self.gui.invoke_after(1000, app.exit) result = app.run() self.assertTrue(result) self.assertFalse(app.exit_vetoed) self.assertTrue(app.exit_prepared) event_order = [event.event_type for event in self.application_events] self.assertEqual(event_order, EVENTS) self.assertEqual(app.windows, []) def test_veto_exit(self): app = TestingApp(veto_exit=True) self.connect_listeners(app) with self.assertMultiTraitChanges([app], EVENTS, []): self.gui.invoke_after(1000, app.exit) self.gui.invoke_after(2000, app.exit, force=True) result = app.run() self.assertTrue(result) self.assertTrue(app.exit_vetoed) self.assertFalse(app.exit_prepared) event_order = [event.event_type for event in self.application_events] self.assertEqual(event_order, EVENTS) self.assertEqual(app.windows, []) def test_veto_open_window(self): app = TestingApp(veto_open_window=True) self.connect_listeners(app) with self.assertMultiTraitChanges([app], EVENTS, []): self.gui.invoke_after(1000, app.exit) result = app.run() self.assertTrue(result) self.assertTrue(app.window_open_vetoed) self.assertFalse(app.exit_vetoed) self.assertTrue(app.exit_prepared) event_order = [event.event_type for event in self.application_events] self.assertEqual(event_order, EVENTS) self.assertEqual(app.windows, []) def test_veto_close_window(self): app = TestingApp(veto_close_window=True) self.connect_listeners(app) with self.assertMultiTraitChanges([app], EVENTS, []): self.gui.invoke_after(1000, app.exit) self.gui.invoke_after(2000, app.exit, force=True) result = app.run() self.assertTrue(result) self.assertTrue(app.exit_vetoed) self.assertFalse(app.exit_prepared) event_order = [event.event_type for event in self.application_events] self.assertEqual(event_order, EVENTS) self.assertEqual(app.windows, []) def test_force_exit(self): app = TestingApp(do_exit=True, force_exit=True, veto_exit=True) self.connect_listeners(app) with self.assertMultiTraitChanges([app], EVENTS, []): self.gui.invoke_after(100, app.exit, True) result = app.run() self.assertTrue(result) self.assertFalse(app.exit_vetoed) self.assertTrue(app.exit_prepared) event_order = [event.event_type for event in self.application_events] self.assertEqual(event_order, EVENTS) self.assertEqual(app.windows, []) def test_force_exit_close_veto(self): app = TestingApp(do_exit=True, force_exit=True, veto_close_window=True) self.connect_listeners(app) with self.assertMultiTraitChanges([app], EVENTS, []): self.gui.invoke_after(1000, app.exit, True) result = app.run() self.assertTrue(result) self.assertFalse(app.exit_vetoed) self.assertTrue(app.exit_prepared) event_order = [event.event_type for event in self.application_events] self.assertEqual(event_order, EVENTS) self.assertEqual(app.windows, []) def test_bad_start(self): app = TestingApp(start_cleanly=False) self.connect_listeners(app) with self.assertMultiTraitChanges([app], EVENTS[:1], EVENTS[1:]): result = app.run() self.assertFalse(result) event_order = [event.event_type for event in self.application_events] self.assertEqual(event_order, EVENTS[:1]) self.assertEqual(app.windows, []) def test_bad_stop(self): app = TestingApp(stop_cleanly=False) self.connect_listeners(app) with self.assertMultiTraitChanges([app], EVENTS[:-1], EVENTS[-1:]): self.gui.invoke_after(1000, app.exit, True) result = app.run() self.assertFalse(result) event_order = [event.event_type for event in self.application_events] self.assertEqual(event_order, EVENTS[:-1]) self.assertEqual(app.windows, []) pyface-7.4.0/pyface/tests/images/0000755000076500000240000000000014176460550017620 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/tests/images/core.png0000644000076500000240000000736014176222673021266 0ustar cwebsterstaff00000000000000PNG  IHDR@@iqgAMA7tEXtSoftwareAdobe ImageReadyqe<IDATxb?H0@ l~}`0W :3| #F/r 0=qs:0 ($l АCXDY,X|d 1'fА 4XU^fr~9@aCY44S#+ W_t's AY4$Awӟ^^[ MY4"78@@ = |`j2]A ef@ GY7/!PONxfbԴ~ ObP`B6 \032H*3\|`Ͽoa ؽX~;!C)7_A <А ^<߇?'>=J182ɳU8030~bxߏ՟ T ,x# o s0 0Yi;0;#5`cw^I#˗2\; '9#@ Z~ 5~8z2hY1.g 1~ z0\9*P hM?hH0K<' .ct0f3, 7?gJ1bPCj#G `,XYY=} *s0|P vs#`{%6Do }f, cxΟ^;x» Wwc F< 8;,@211=*..VbpÞ={Uā>v2̟;wPqPm"&40C6psOg_ס'ﲻpbA/ox<A|<*++3\rɓ' e8q"ñXY܀jXe5Ì3{ǗKk3.ԡhÕ'-'- rjPUB15,7770F>|sw["nܸ/R #{1HǏz:櫪r]x R0/Ir2{I7m| {A6qvf'}3~"5 s8$p `v/_~w?~= s8ifnPjcx%X>}111y?3@-"y=L 2 .| &\ -ONȰ(hkF"%x (;;;3 ,߃co߾1 ARw^'N ڵPjymlwO)(5nS @fNh;3L0͛7pρ- ?iKKKp3dx (T+ C(1(~V}be@KhAWQ#"t === ax5#$ @a:HV__AXXZ<(RYT~ b[k~ mb b 5551J 0 eX`` ɁjP!@`{A435D(ā\p<0W`ԃRVE-4[ Be@1mlly`y9kc!j$JR]/c/dd!(b 9D#g A^XC`MϠ@ pPrvMu9)gZ$vhO(3 |AIT]!T ǃ0,@a!{&aD!#M SZ+P+T BްT,aY"Dy >=Oqho$(6~5oV 5o߾%#@n6b ~Li* \`iP53pGV , D䤎\("P#(ddYhJ4W,y `O a` r? A1'tl@@ syPKT Zm և :t0(Aa7NhB p/u((9ry=SBԄ>=$٠= p#GeP PWWG!>(KIC{Ffh poCAU rvϟ #r0 >Q`A_ pGPbZe (6_,,,9 (11jPR3W<;1 +@}x r! 4Jc=moyߔ6W$(3,I9Fh Ey+N`2`X; LX1/@q pвw+ Ee3` -#ho;u2N { <#+6urTRt`)% lf^RWSSWg4oth)uPj@4;S@=A^^) @yԐB=JQR`G޼y4AV>h<dsj$ B I*`f۷uuu"=qP vU^EOe@ PMD6|||̪򀖁<y1砭TE"fnT.rNf&PSJ|PVyhhnA5~7Q#"vvͰVall (Pl#<*hADΞ=  σfqd'űg"ezZ$8syA(KLII >d? $X9"NNNM===PϏ$T<AJPl뿄;y%5d9@A:p\PQQe= ,-k[nFA (x5(} SC"w#t`HQOF@@d4`5hP A߃o-;Pw{ -^  (]&в(4aa eV@pA.az33=zƒ-Y Df Qh9 ~h FVCX4z" T6Z8;-49'=C[@147Oǝ IENDB`pyface-7.4.0/pyface/tests/test_font.py0000644000076500000240000002243614176222673020743 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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, TraitError from pyface.font import Font, FontSize, FontStretch, SIZES, STRETCHES from pyface.toolkit import toolkit_object class FontSizeDummy(HasStrictTraits): size = FontSize() size_default_1 = FontSize(14.0) size_default_2 = FontSize("14.0") size_default_3 = FontSize("14.0pt") size_default_4 = FontSize("large") class TestFontSizeTrait(unittest.TestCase): def test_font_size_trait_defaults(self): dummy = FontSizeDummy() self.assertEqual(dummy.size, 12.0) self.assertEqual(dummy.size_default_1, 14.0) self.assertEqual(dummy.size_default_2, 14.0) self.assertEqual(dummy.size_default_3, 14.0) self.assertEqual(dummy.size_default_4, 14.0) def test_font_sizes(self): dummy = FontSizeDummy() for size, value in SIZES.items(): with self.subTest(size=size): dummy.size = size self.assertEqual(dummy.size, value) def test_font_size_trait_invalid_default(self): for size in ["badvalue", -1.0, "-1.0", "0pt"]: with self.subTest(size=size): with self.assertRaises(TraitError): FontSize(size) def test_font_size_trait_validate(self): dummy = FontSizeDummy() for size, expected in [ (14.0, 14.0), ("15.0", 15.0), ("16pt", 16.0), ("17.0px", 17.0), ]: with self.subTest(size=size): dummy.size = size self.assertEqual(dummy.size, expected) def test_font_size_trait_invalid_validate(self): dummy = FontSizeDummy() for size in ["badvalue", -1.0, "-1.0", "0pt"]: with self.subTest(size=size): with self.assertRaises(TraitError): dummy.size = size class FontStretchDummy(HasStrictTraits): stretch = FontStretch() stretch_default_1 = FontStretch(150) stretch_default_2 = FontStretch("150.0") stretch_default_3 = FontStretch("150.0%") stretch_default_4 = FontStretch("expanded") class TestFontStretchTrait(unittest.TestCase): def test_font_stretch_trait_defaults(self): dummy = FontStretchDummy() self.assertEqual(dummy.stretch, 100.0) self.assertEqual(dummy.stretch_default_1, 150.0) self.assertEqual(dummy.stretch_default_2, 150.0) self.assertEqual(dummy.stretch_default_3, 150.0) self.assertEqual(dummy.stretch_default_4, 125.0) def test_font_stretches(self): dummy = FontStretchDummy() for stretch, value in STRETCHES.items(): with self.subTest(stretch=stretch): dummy.stretch = stretch self.assertEqual(dummy.stretch, value) def test_font_stretch_trait_invalid_default(self): for stretch in ["badvalue", 49.5, "49.5", "49.5%", 200.1]: with self.subTest(stretch=stretch): with self.assertRaises(TraitError): FontStretch(stretch) def test_font_stretch_trait_validate(self): dummy = FontStretchDummy() for stretch, expected in [ (150.0, 150.0), ("125", 125.0), ("50%", 50.0), ("ultra-expanded", 200.0) ]: with self.subTest(stretch=stretch): dummy.stretch = stretch self.assertEqual(dummy.stretch, expected) def test_font_stretch_trait_invalid_validate(self): dummy = FontStretchDummy() for stretch in ["badvalue", 49.5, "200.1", "49.9%"]: with self.subTest(stretch=stretch): with self.assertRaises(TraitError): dummy.stretch = stretch class TestFont(unittest.TestCase): def test_default(self): font = Font() self.assertEqual(font.family, ['default']) self.assertEqual(font.size, 12.0) self.assertEqual(font.weight, 'normal') self.assertEqual(font.stretch, 100) self.assertEqual(font.style, 'normal') self.assertEqual(font.variants, set()) def test_typical(self): font = Font( family=['Helvetica', 'sans-serif'], size='large', weight='demi-bold', stretch='condensed', style='italic', variants={'small-caps'}, decorations={'underline'}, ) self.assertEqual(font.family, ['Helvetica', 'sans-serif']) self.assertEqual(font.size, 14.0) self.assertEqual(font.weight, 'demi-bold') self.assertEqual(font.weight_, 600) self.assertEqual(font.stretch, 75) self.assertEqual(font.style, 'italic') self.assertEqual(font.variants, {'small-caps'}) self.assertEqual(font.decorations, {'underline'}) def test_family_sequence(self): font = Font(family=('Helvetica', 'sans-serif')) self.assertEqual(font.family, ['Helvetica', 'sans-serif']) def test_variants_frozenset(self): font = Font(variants=frozenset({'small-caps'})) self.assertEqual(font.variants, {'small-caps'}) def test_decorations_frozenset(self): font = Font(decorations=frozenset({'underline'})) self.assertEqual(font.decorations, {'underline'}) def test_str(self): font = Font() description = str(font) self.assertEqual(description, "12pt default") def test_str_typical(self): font = Font( family=['Comic Sans', 'decorative'], size='large', weight='demi-bold', stretch='condensed', style='italic', variants={'small-caps'}, decorations={'underline'}, ) description = str(font) self.assertEqual( description, "italic small-caps underline demi-bold 75% 14pt " "'Comic Sans', decorative" ) def test_repr(self): font = Font() text = repr(font) # this is little more than a smoke check, but good enough self.assertTrue(text.startswith('Font(')) def test_repr_typical(self): font = Font( family=['Helvetica', 'sans-serif'], size='large', weight='demi-bold', stretch='condensed', style='italic', variants={'small-caps'}, decorations={'underline'}, ) text = repr(font) # this is little more than a smoke check, but good enough self.assertTrue(text.startswith('Font(')) def test_to_toolkit(self): font = Font() # smoke test toolkit_font = font.to_toolkit() self.assertIsNotNone(toolkit_font) def test_to_toolkit_typical(self): font = Font( family=['Helvetica', 'sans-serif'], size='large', weight='demi-bold', stretch='condensed', style='italic', variants={'small-caps'}, decorations={'underline', 'strikethrough', 'overline'}, ) # smoke test toolkit_font = font.to_toolkit() self.assertIsNotNone(toolkit_font) def test_toolkit_default_roundtrip(self): font = Font() # smoke test result = Font.from_toolkit(font.to_toolkit()) # defaults should round-trip self.assertTrue(result.family[-1], 'default') self.assertEqual(result.size, font.size) self.assertEqual(result.weight, font.weight) self.assertEqual(result.stretch, font.stretch) self.assertEqual(result.variants, font.variants) self.assertEqual(result.decorations, font.decorations) def test_from_toolkit_typical(self): font = Font( family=['Helvetica', 'sans-serif'], size='large', weight='bold', stretch='condensed', style='italic', variants={'small-caps'}, decorations={'underline', 'strikethrough', 'overline'}, ) # smoke test result = Font.from_toolkit(font.to_toolkit()) # we expect some things should round-trip no matter what system self.assertEqual(result.size, font.size) self.assertEqual(result.weight, font.weight) self.assertEqual(result.style, font.style) def test_toolkit_font_to_properties(self): toolkit_font_to_properties = toolkit_object( 'font:toolkit_font_to_properties') font = Font( family=['Helvetica', 'sans-serif'], size='large', weight='demi-bold', stretch='condensed', style='italic', variants={'small-caps'}, decorations={'underline', 'strikethrough', 'overline'}, ) properties = toolkit_font_to_properties(font.to_toolkit()) self.assertEqual( set(properties.keys()), { 'family', 'size', 'stretch', 'weight', 'style', 'variants', 'decorations' } ) pyface-7.4.0/pyface/tests/test_application_window.py0000644000076500000240000002154314176222673023665 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 ..action.api import ( Action, MenuManager, MenuBarManager, StatusBarManager, ToolBarManager, ) from ..application_window import ApplicationWindow from ..toolkit import toolkit_object from ..image_resource import ImageResource GuiTestAssistant = toolkit_object("util.gui_test_assistant:GuiTestAssistant") no_gui_test_assistant = GuiTestAssistant.__name__ == "Unimplemented" @unittest.skipIf(no_gui_test_assistant, "No GuiTestAssistant") class TestApplicationWindow(unittest.TestCase, GuiTestAssistant): def setUp(self): GuiTestAssistant.setUp(self) self.window = ApplicationWindow() def tearDown(self): if self.window.control is not None: with self.delete_widget(self.window.control): self.window.destroy() GuiTestAssistant.tearDown(self) def test_close(self): # test that close works even when no control self.window.close() def test_open_close(self): # test that opening and closing works as expected with self.assertTraitChanges(self.window, "opening", count=1): with self.assertTraitChanges(self.window, "opened", count=1): with self.event_loop(): self.window.open() with self.assertTraitChanges(self.window, "closing", count=1): with self.assertTraitChanges(self.window, "closed", count=1): with self.event_loop(): self.window.close() def test_show(self): # test that show and hide works as expected with self.event_loop(): self.window._create() self.window.show(True) with self.event_loop(): self.window.show(False) with self.event_loop(): self.window.close() def test_activate(self): # test that activation works as expected with self.event_loop(): self.window.open() with self.event_loop(): self.window.activate() with self.event_loop(): self.window.close() def test_position(self): # test that default position works as expected self.window.position = (100, 100) with self.event_loop(): self.window.open() with self.event_loop(): self.window.close() def test_reposition(self): # test that changing position works as expected with self.event_loop(): self.window.open() with self.event_loop(): self.window.position = (100, 100) with self.event_loop(): self.window.close() def test_size(self): # test that default size works as expected self.window.size = (100, 100) with self.event_loop(): self.window.open() with self.event_loop(): self.window.close() def test_resize(self): # test that changing size works as expected self.window.open() with self.event_loop(): self.window.size = (100, 100) with self.event_loop(): self.window.close() def test_title(self): # test that default title works as expected self.window.title = "Test Title" with self.event_loop(): self.window.open() with self.event_loop(): self.window.close() def test_retitle(self): # test that changing title works as expected with self.event_loop(): self.window.open() with self.event_loop(): self.window.title = "Test Title" with self.event_loop(): self.window.close() def test_menubar(self): # test that menubar gets created as expected self.window.menu_bar_manager = MenuBarManager( MenuManager( Action(name="New"), Action(name="Open"), Action(name="Save"), Action(name="Close"), Action(name="Quit"), name="File", ) ) with self.event_loop(): self.window._create() with self.event_loop(): self.window.show(True) with self.event_loop(): self.window.show(False) with self.event_loop(): self.window.close() def test_menubar_multiple_menus(self): # test that menubar gets created as expected self.window.menu_bar_manager = MenuBarManager( MenuManager( Action(name="New"), Action(name="Open"), Action(name="Save"), Action(name="Close"), Action(name="Quit"), name="File", ), MenuManager( Action(name="Zoom in"), Action(name="Zoom out"), name="View", ) ) with self.event_loop(): self.window._create() with self.event_loop(): self.window.show(True) with self.event_loop(): self.window.show(False) with self.event_loop(): self.window.close() def test_toolbar(self): # test that toolbar gets created as expected self.window.tool_bar_managers = [ ToolBarManager( Action(name="New", image=ImageResource("core")), Action(name="Open", image=ImageResource("core")), Action(name="Save", image=ImageResource("core")), Action(name="Close", image=ImageResource("core")), Action(name="Quit", image=ImageResource("core")), ) ] with self.event_loop(): self.window._create() with self.event_loop(): self.window.show(True) with self.event_loop(): self.window.show(False) with self.event_loop(): self.window.close() def test_toolbar_changed(self): # test that toolbar gets changed as expected self.window.tool_bar_managers = [ ToolBarManager( Action(name="New", image=ImageResource("core")), Action(name="Open", image=ImageResource("core")), Action(name="Save", image=ImageResource("core")), Action(name="Close", image=ImageResource("core")), Action(name="Quit", image=ImageResource("core")), ) ] with self.event_loop(): self.window._create() with self.event_loop(): self.window.show(True) with self.event_loop(): self.window.tool_bar_managers = [ ToolBarManager( Action(name="New", image=ImageResource("core")), Action(name="Open", image=ImageResource("core")), Action(name="Save", image=ImageResource("core")), Action(name="Close", image=ImageResource("core")), Action(name="Quit", image=ImageResource("core")), ) ] with self.event_loop(): self.window.show(False) with self.event_loop(): self.window.close() def test_statusbar(self): # test that status bar gets created as expected self.window.status_bar_manager = StatusBarManager( message="hello world" ) with self.event_loop(): self.window._create() with self.event_loop(): self.window.show(True) with self.event_loop(): self.window.show(False) with self.event_loop(): self.window.close() def test_statusbar_changed(self): # test that status bar gets changed as expected self.window.status_bar_manager = StatusBarManager( message="hello world" ) with self.event_loop(): self.window._create() with self.event_loop(): self.window.show(True) with self.event_loop(): self.window.status_bar_manager = StatusBarManager( message="goodbye world" ) with self.event_loop(): self.window.show(False) with self.event_loop(): self.window.close() def test_icon(self): # test that status bar gets created as expected self.window.icon = ImageResource("core") with self.event_loop(): self.window._create() with self.event_loop(): self.window.show(True) with self.event_loop(): self.window.show(False) with self.event_loop(): self.window.close() pyface-7.4.0/pyface/tests/test_splash_screen.py0000644000076500000240000000761514176222673022630 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 ..image_resource import ImageResource from ..splash_screen import SplashScreen from ..toolkit import toolkit_object GuiTestAssistant = toolkit_object("util.gui_test_assistant:GuiTestAssistant") no_gui_test_assistant = GuiTestAssistant.__name__ == "Unimplemented" @unittest.skipIf(no_gui_test_assistant, "No GuiTestAssistant") class TestWindow(unittest.TestCase, GuiTestAssistant): def setUp(self): GuiTestAssistant.setUp(self) self.window = SplashScreen() def tearDown(self): if self.window.control is not None: with self.delete_widget(self.window.control): self.window.destroy() del self.window GuiTestAssistant.tearDown(self) def test_destroy(self): # test that destroy works even when no control with self.event_loop(): self.window.destroy() def test_open_close(self): # test that opening and closing works as expected with self.assertTraitChanges(self.window, "opening", count=1): with self.assertTraitChanges(self.window, "opened", count=1): with self.event_loop(): self.window.open() with self.assertTraitChanges(self.window, "closing", count=1): with self.assertTraitChanges(self.window, "closed", count=1): with self.event_loop(): self.window.close() def test_show(self): # test that show works as expected with self.event_loop(): self.window._create() with self.event_loop(): self.window.show(True) with self.event_loop(): self.window.show(False) with self.event_loop(): self.window.destroy() def test_image(self): # test that images work self.window.image = ImageResource("core") with self.assertTraitChanges(self.window, "opening", count=1): with self.assertTraitChanges(self.window, "opened", count=1): with self.event_loop(): self.window.open() with self.assertTraitChanges(self.window, "closing", count=1): with self.assertTraitChanges(self.window, "closed", count=1): with self.event_loop(): self.window.close() def test_text(self): # test that images work self.window.text = "Splash screen" with self.assertTraitChanges(self.window, "opening", count=1): with self.assertTraitChanges(self.window, "opened", count=1): with self.event_loop(): self.window.open() with self.assertTraitChanges(self.window, "closing", count=1): with self.assertTraitChanges(self.window, "closed", count=1): with self.event_loop(): self.window.close() def test_text_changed(self): # test that images work # XXX this throws a non-failing exception on wx # - probably the way the test is written. with self.assertTraitChanges(self.window, "opening", count=1): with self.assertTraitChanges(self.window, "opened", count=1): with self.event_loop(): self.window.open() with self.event_loop(): self.window.text = "Splash screen" with self.assertTraitChanges(self.window, "closing", count=1): with self.assertTraitChanges(self.window, "closed", count=1): with self.event_loop(): self.window.close() pyface-7.4.0/pyface/tests/test_clipboard.py0000644000076500000240000000570514176222673021734 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.etsconfig.api import ETSConfig from ..clipboard import clipboard is_wx = (ETSConfig.toolkit == 'wx') class TestObject(object): def __init__(self, **kwargs): for key, value in kwargs.items(): setattr(self, key, value) def __eq__(self, other): if isinstance(other, TestObject): return all( getattr(other, key) == value for key, value in self.__dict__.items() ) class TestClipboard(unittest.TestCase): def setUp(self): self.clipboard = clipboard def test_set_text_data(self): self.clipboard.data = "test" self.assertTrue(self.clipboard.has_data) self.assertEqual(self.clipboard.data_type, "str") self.assertEqual(self.clipboard.data, "test") self.assertTrue(self.clipboard.has_text_data) self.assertEqual(self.clipboard.text_data, "test") self.assertFalse(self.clipboard.has_file_data) self.assertFalse(self.clipboard.has_object_data) def test_set_text_data_unicode(self): self.clipboard.data = "test" self.assertTrue(self.clipboard.has_data) self.assertEqual(self.clipboard.data_type, "str") self.assertEqual(self.clipboard.data, "test") self.assertTrue(self.clipboard.has_text_data) self.assertEqual(self.clipboard.text_data, "test") self.assertFalse(self.clipboard.has_file_data) self.assertFalse(self.clipboard.has_object_data) @unittest.skip("backends not consistent") def test_set_file_data(self): self.clipboard.data = ["file:///images"] self.assertTrue(self.clipboard.has_data) self.assertEqual(self.clipboard.data_type, "file") self.assertEqual(self.clipboard.data, ["/images"]) self.assertTrue(self.clipboard.has_file_data) self.assertEqual(self.clipboard.file_data, ["/images"]) self.assertFalse(self.clipboard.has_text_data) self.assertFalse(self.clipboard.has_object_data) def test_set_object_data(self): data = TestObject(foo="bar", baz=1) self.clipboard.data = data self.assertTrue(self.clipboard.has_data) self.assertEqual(self.clipboard.data_type, TestObject) self.assertEqual(self.clipboard.data, data) self.assertTrue(self.clipboard.has_object_data) self.assertEqual(self.clipboard.object_type, TestObject) self.assertEqual(self.clipboard.object_data, data) self.assertFalse(self.clipboard.has_text_data) self.assertFalse(self.clipboard.has_file_data) pyface-7.4.0/pyface/tests/test_split_dialog.py0000644000076500000240000000431614176222673022444 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 ..heading_text import HeadingText from ..split_dialog import SplitDialog from ..toolkit import toolkit_object GuiTestAssistant = toolkit_object("util.gui_test_assistant:GuiTestAssistant") no_gui_test_assistant = GuiTestAssistant.__name__ == "Unimplemented" @unittest.skipIf(no_gui_test_assistant, "No GuiTestAssistant") class TestDialog(unittest.TestCase, GuiTestAssistant): def setUp(self): GuiTestAssistant.setUp(self) self.dialog = SplitDialog() def tearDown(self): if self.dialog.control is not None: with self.delete_widget(self.dialog.control): self.dialog.destroy() del self.dialog GuiTestAssistant.tearDown(self) def test_create(self): # test that creation and destruction works as expected with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() def test_destroy(self): # test that destroy works even when no control with self.event_loop(): self.dialog.destroy() def test_horizontal(self): # test that horizontal split works self.dialog.direction = "horizontal" with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() def test_ratio(self): # test that ratio works self.dialog.ratio = 0.25 with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() def test_contents(self): # test that contents works self.dialog.lhs = HeadingText self.dialog.rhs = HeadingText with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() pyface-7.4.0/pyface/tests/test_base_toolkit.py0000644000076500000240000000445314176222673022453 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.etsconfig.api import ETSConfig from pyface.base_toolkit import find_toolkit, import_toolkit class TestToolkit(unittest.TestCase): def test_import_null_toolkit(self): toolkit = import_toolkit("null") self.assertEqual(toolkit.package, "pyface") self.assertEqual(toolkit.toolkit, "null") def test_missing_toolkit(self): # test that we get an error with an undefined toolkit with self.assertRaises(RuntimeError): import_toolkit("nosuchtoolkit") def test_find_current_toolkit_no_etsconfig(self): old_etsconfig_toolkit = ETSConfig._toolkit ETSConfig._toolkit = "" try: toolkit = find_toolkit("pyface.toolkits", old_etsconfig_toolkit) self.assertEqual(toolkit.package, "pyface") self.assertEqual(toolkit.toolkit, old_etsconfig_toolkit) self.assertEqual(ETSConfig.toolkit, old_etsconfig_toolkit) finally: ETSConfig._toolkit = old_etsconfig_toolkit def test_find_null_toolkit_no_etsconfig(self): old_etsconfig_toolkit = ETSConfig._toolkit ETSConfig._toolkit = "" try: toolkit = find_toolkit("pyface.toolkits", "null") self.assertEqual(toolkit.package, "pyface") self.assertEqual(toolkit.toolkit, "null") self.assertEqual(ETSConfig.toolkit, "null") finally: ETSConfig._toolkit = old_etsconfig_toolkit def test_find_nonexistent_toolkit_no_etsconfig(self): old_etsconfig_toolkit = ETSConfig._toolkit ETSConfig._toolkit = "" try: toolkit = find_toolkit("pyface.toolkits", "nonexistent") self.assertEqual(toolkit.package, "pyface") self.assertEqual(toolkit.toolkit, "null") self.assertEqual(ETSConfig.toolkit, "null") finally: ETSConfig._toolkit = old_etsconfig_toolkit pyface-7.4.0/pyface/tests/__init__.py0000644000076500000240000000000014176222673020454 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/tests/test_confirmation_dialog.py0000644000076500000240000003467614176222673024015 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 ..confirmation_dialog import ConfirmationDialog, confirm from ..constant import YES, NO, OK, CANCEL from ..image_resource import ImageResource from ..toolkit import toolkit_object from ..window import Window is_qt = toolkit_object.toolkit == "qt4" if is_qt: from pyface.qt import qt_api GuiTestAssistant = toolkit_object("util.gui_test_assistant:GuiTestAssistant") no_gui_test_assistant = GuiTestAssistant.__name__ == "Unimplemented" ModalDialogTester = toolkit_object( "util.modal_dialog_tester:ModalDialogTester" ) no_modal_dialog_tester = ModalDialogTester.__name__ == "Unimplemented" is_pyqt5 = is_qt and qt_api == "pyqt5" is_pyqt4_linux = is_qt and qt_api == "pyqt" and platform.system() == "Linux" @unittest.skipIf(no_gui_test_assistant, "No GuiTestAssistant") class TestConfirmationDialog(unittest.TestCase, GuiTestAssistant): def setUp(self): GuiTestAssistant.setUp(self) self.dialog = ConfirmationDialog() def tearDown(self): if self.dialog.control is not None: with self.delete_widget(self.dialog.control): self.dialog.destroy() self.dialog = None GuiTestAssistant.tearDown(self) def test_create(self): # test that creation and destruction works as expected with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() def test_destroy(self): # test that destroy works even when no control with self.event_loop(): self.dialog.destroy() def test_size(self): # test that size works as expected self.dialog.size = (100, 100) with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() def test_position(self): # test that position works as expected self.dialog.position = (100, 100) with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() def test_create_parent(self): # test that creation and destruction works as expected with a parent with self.event_loop(): parent = Window() self.dialog.parent = parent.control parent._create() with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() with self.event_loop(): parent.destroy() def test_create_yes_renamed(self): # test that creation and destruction works as expected with ok_label self.dialog.yes_label = "Sure" with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() def test_create_no_renamed(self): # test that creation and destruction works as expected with ok_label self.dialog.no_label = "No Way" with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() def test_create_yes_default(self): # test that creation and destruction works as expected with ok_label self.dialog.default = YES with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() def test_create_cancel(self): # test that creation and destruction works with cancel button self.dialog.cancel = True with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() def test_create_cancel_renamed(self): # test that creation and destruction works with cancel button self.dialog.cancel = True self.dialog.cancel_label = "Back" with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() def test_create_cancel_default(self): # test that creation and destruction works as expected with ok_label self.dialog.cancel = True self.dialog.default = CANCEL with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() def test_create_image(self): # test that creation and destruction works with a non-standard image self.dialog.image = ImageResource("core") with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_close(self): # test that closing works as expected # XXX duplicate of Dialog test, not needed? tester = ModalDialogTester(self.dialog.open) tester.open_and_run(when_opened=lambda x: self.dialog.close()) self.assertEqual(tester.result, NO) self.assertEqual(self.dialog.return_code, NO) @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_close_with_cancel(self): # test that closing works as expected self.dialog.cancel = True tester = ModalDialogTester(self.dialog.open) tester.open_and_run(when_opened=lambda x: self.dialog.close()) self.assertEqual(tester.result, CANCEL) self.assertEqual(self.dialog.return_code, CANCEL) @unittest.skipIf( is_pyqt5, "Confirmation dialog click tests don't work on pyqt5." ) # noqa @unittest.skipIf( is_pyqt4_linux, "Confirmation dialog click tests don't work reliably on linux. Issue #282.", ) # noqa @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_yes(self): # test that Yes works as expected tester = ModalDialogTester(self.dialog.open) tester.open_and_wait(when_opened=lambda x: x.click_button(YES)) self.assertEqual(tester.result, YES) self.assertEqual(self.dialog.return_code, YES) @unittest.skipIf( is_pyqt5, "Confirmation dialog click tests don't work on pyqt5." ) # noqa @unittest.skipIf( is_pyqt4_linux, "Confirmation dialog click tests don't work reliably on linux. Issue #282.", ) # noqa @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_renamed_yes(self): self.dialog.yes_label = "Sure" # test that Yes works as expected if renamed tester = ModalDialogTester(self.dialog.open) tester.open_and_wait(when_opened=lambda x: x.click_widget("Sure")) self.assertEqual(tester.result, YES) self.assertEqual(self.dialog.return_code, YES) @unittest.skipIf( is_pyqt5, "Confirmation dialog click tests don't work on pyqt5." ) # noqa @unittest.skipIf( is_pyqt4_linux, "Confirmation dialog click tests don't work reliably on linux. Issue #282.", ) # noqa @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_no(self): # test that No works as expected tester = ModalDialogTester(self.dialog.open) tester.open_and_wait(when_opened=lambda x: x.click_button(NO)) self.assertEqual(tester.result, NO) self.assertEqual(self.dialog.return_code, NO) @unittest.skipIf( is_pyqt5, "Confirmation dialog click tests don't work on pyqt5." ) # noqa @unittest.skipIf( is_pyqt4_linux, "Confirmation dialog click tests don't work reliably on linux. Issue #282.", ) # noqa @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_renamed_no(self): self.dialog.no_label = "No way" # test that No works as expected if renamed tester = ModalDialogTester(self.dialog.open) tester.open_and_wait(when_opened=lambda x: x.click_widget("No way")) self.assertEqual(tester.result, NO) self.assertEqual(self.dialog.return_code, NO) @unittest.skipIf( is_pyqt5, "Confirmation dialog click tests don't work on pyqt5." ) # noqa @unittest.skipIf( is_pyqt4_linux, "Confirmation dialog click tests don't work reliably on linux. Issue #282.", ) # noqa @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_cancel(self): self.dialog.cancel = True # test that Cancel works as expected tester = ModalDialogTester(self.dialog.open) tester.open_and_wait(when_opened=lambda x: x.click_button(CANCEL)) self.assertEqual(tester.result, CANCEL) self.assertEqual(self.dialog.return_code, CANCEL) @unittest.skipIf( is_pyqt5, "Confirmation dialog click tests don't work on pyqt5." ) # noqa @unittest.skipIf( is_pyqt4_linux, "Confirmation dialog click tests don't work reliably on linux. Issue #282.", ) # noqa @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_cancel_renamed(self): self.dialog.cancel = True self.dialog.cancel_label = "Back" # test that Cancel works as expected tester = ModalDialogTester(self.dialog.open) tester.open_and_wait(when_opened=lambda x: x.click_widget("Back")) self.assertEqual(tester.result, CANCEL) self.assertEqual(self.dialog.return_code, CANCEL) @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_parent(self): # test that lifecycle works with a parent parent = Window() self.dialog.parent = parent.control with self.event_loop(): parent.open() tester = ModalDialogTester(self.dialog.open) tester.open_and_run(when_opened=lambda x: x.close(accept=True)) with self.event_loop(): parent.close() self.assertEqual(tester.result, OK) self.assertEqual(self.dialog.return_code, OK) @unittest.skipIf(no_gui_test_assistant, "No GuiTestAssistant") class TestConfirm(unittest.TestCase, GuiTestAssistant): def setUp(self): GuiTestAssistant.setUp(self) def tearDown(self): GuiTestAssistant.tearDown(self) @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_reject(self): # test that cancel works as expected tester = ModalDialogTester( lambda: confirm(None, "message", cancel=True) ) tester.open_and_run(when_opened=lambda x: x.close(accept=False)) self.assertEqual(tester.result, CANCEL) @unittest.skipIf( is_pyqt5, "Confirmation dialog click tests don't work on pyqt5." ) # noqa @unittest.skipIf( is_pyqt4_linux, "Confirmation dialog click tests don't work reliably on linux. Issue #282.", ) # noqa @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_yes(self): # test that yes works as expected tester = ModalDialogTester(lambda: confirm(None, "message")) tester.open_and_wait(when_opened=lambda x: x.click_button(YES)) self.assertEqual(tester.result, YES) @unittest.skipIf( is_pyqt5, "Confirmation dialog click tests don't work on pyqt5." ) # noqa @unittest.skipIf( is_pyqt4_linux, "Confirmation dialog click tests don't work reliably on linux. Issue #282.", ) # noqa @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_no(self): # test that yes works as expected tester = ModalDialogTester(lambda: confirm(None, "message")) tester.open_and_wait(when_opened=lambda x: x.click_button(NO)) self.assertEqual(tester.result, NO) @unittest.skipIf( is_pyqt5, "Confirmation dialog click tests don't work on pyqt5." ) # noqa @unittest.skipIf( is_pyqt4_linux, "Confirmation dialog click tests don't work reliably on linux. Issue #282.", ) # noqa @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_cancel(self): # test that cancel works as expected tester = ModalDialogTester( lambda: confirm(None, "message", cancel=True) ) tester.open_and_wait(when_opened=lambda x: x.click_button(CANCEL)) self.assertEqual(tester.result, CANCEL) @unittest.skipIf( is_pyqt5, "Confirmation dialog click tests don't work on pyqt5." ) # noqa @unittest.skipIf( is_pyqt4_linux, "Confirmation dialog click tests don't work reliably on linux. Issue #282.", ) # noqa @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_title(self): # test that title works as expected tester = ModalDialogTester( lambda: confirm(None, "message", title="Title") ) tester.open_and_run(when_opened=lambda x: x.click_button(NO)) self.assertEqual(tester.result, NO) @unittest.skipIf( is_pyqt5, "Confirmation dialog click tests don't work on pyqt5." ) # noqa @unittest.skipIf( is_pyqt4_linux, "Confirmation dialog click tests don't work reliably on linux. Issue #282.", ) # noqa @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_default_yes(self): # test that default works as expected tester = ModalDialogTester( lambda: confirm(None, "message", default=YES) ) tester.open_and_run(when_opened=lambda x: x.click_button(YES)) self.assertEqual(tester.result, YES) @unittest.skipIf( is_pyqt5, "Confirmation dialog click tests don't work on pyqt5." ) # noqa @unittest.skipIf( is_pyqt4_linux, "Confirmation dialog click tests don't work reliably on linux. Issue #282.", ) # noqa @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_default_cancel(self): # test that default works as expected tester = ModalDialogTester( lambda: confirm(None, "message", cancel=True, default=YES) ) tester.open_and_run(when_opened=lambda x: x.click_button(CANCEL)) self.assertEqual(tester.result, CANCEL) pyface-7.4.0/pyface/tests/test_color_dialog.py0000644000076500000240000000611314176222673022424 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 ..color import Color from ..color_dialog import ColorDialog, get_color from ..toolkit import toolkit_object GuiTestAssistant = toolkit_object("util.gui_test_assistant:GuiTestAssistant") no_gui_test_assistant = GuiTestAssistant.__name__ == "Unimplemented" ModalDialogTester = toolkit_object( "util.modal_dialog_tester:ModalDialogTester" ) no_modal_dialog_tester = ModalDialogTester.__name__ == "Unimplemented" @unittest.skipIf(no_gui_test_assistant, "No GuiTestAssistant") class TestColorDialog(unittest.TestCase, GuiTestAssistant): def setUp(self): GuiTestAssistant.setUp(self) self.dialog = ColorDialog(color="rebeccapurple") def tearDown(self): if self.dialog.control is not None: with self.delete_widget(self.dialog.control): self.dialog.destroy() del self.dialog GuiTestAssistant.tearDown(self) def test_color(self): # test that colors are translated as expected self.dialog.color = "red" self.assertEqual(self.dialog.color, Color.from_str("red")) def test_create(self): # test that creation and destruction works as expected with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() def test_destroy(self): # test that destroy works even when no control with self.event_loop(): self.dialog.destroy() def test_close(self): # test that close works with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.close() def test_show_alpha(self): # test that creation and destruction works with show_alpha True self.dialog.show_alpha = True with self.event_loop(): self.dialog._create() @unittest.skipIf(no_gui_test_assistant, "No GuiTestAssistant") class TestGetColor(unittest.TestCase, GuiTestAssistant): @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_close(self): # test that cancel works as expected tester = ModalDialogTester( lambda: get_color(None, "rebeccapurple") ) tester.open_and_run(when_opened=lambda x: x.close(accept=False)) self.assertEqual(tester.result, None) @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_close_show_alpha(self): # test that cancel works as expected tester = ModalDialogTester( lambda: get_color(None, "rebeccapurple", True) ) tester.open_and_run(when_opened=lambda x: x.close(accept=False)) self.assertEqual(tester.result, None) pyface-7.4.0/pyface/tests/test_single_choice_dialog.py0000644000076500000240000002030014176222673024073 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.etsconfig.api import ETSConfig from ..single_choice_dialog import SingleChoiceDialog from ..constant import OK, CANCEL from ..toolkit import toolkit_object from ..window import Window GuiTestAssistant = toolkit_object("util.gui_test_assistant:GuiTestAssistant") no_gui_test_assistant = GuiTestAssistant.__name__ == "Unimplemented" ModalDialogTester = toolkit_object( "util.modal_dialog_tester:ModalDialogTester" ) # noqa: E501 no_modal_dialog_tester = ModalDialogTester.__name__ == "Unimplemented" USING_QT = ETSConfig.toolkit not in ["", "wx"] @unittest.skipIf(no_gui_test_assistant, "No GuiTestAssistant") class TestSingleChoiceDialog(unittest.TestCase, GuiTestAssistant): def setUp(self): GuiTestAssistant.setUp(self) self.dialog = SingleChoiceDialog(choices=["red", "blue", "green"]) def tearDown(self): if self.dialog.control is not None: with self.delete_widget(self.dialog.control): self.dialog.destroy() self.dialog = None GuiTestAssistant.tearDown(self) def test_create(self): # test that creation and destruction works as expected with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() def test_destroy(self): # test that destroy works even when no control with self.event_loop(): self.dialog.destroy() def test_create_cancel(self): # test that creation and destruction works no cancel button self.dialog.cancel = False with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() def test_create_parent(self): # test that creation and destruction works as expected with a parent with self.event_loop(): parent = Window() self.dialog.parent = parent.control parent._create() with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() with self.event_loop(): parent.destroy() def test_message(self): # test that creation and destruction works as expected with message self.dialog.message = "This is the message" with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() def test_choice_strings(self): # test that choice strings work using simple strings self.assertEqual( self.dialog._choice_strings(), ["red", "blue", "green"] ) def test_choice_strings_convert(self): # test that choice strings work using simple strings self.dialog.choices = [1, 2, 3] self.assertEqual(self.dialog._choice_strings(), ["1", "2", "3"]) def test_choice_strings_name_attribute(self): # test that choice strings work using attribute name of objects class Item(object): def __init__(self, description): self.description = description self.dialog.choices = [Item(name) for name in ["red", "blue", "green"]] self.dialog.name_attribute = "description" self.assertEqual( self.dialog._choice_strings(), ["red", "blue", "green"] ) def test_choice_strings_name_attribute_convert(self): # test that choice strings work using attribute name of objects class Item(object): def __init__(self, description): self.description = description self.dialog.choices = [Item(name) for name in [1, 2, 3]] self.dialog.name_attribute = "description" self.assertEqual(self.dialog._choice_strings(), ["1", "2", "3"]) def test_choice_strings_empty(self): # test that choice strings work using simple strings self.dialog.choices = [] with self.assertRaises(ValueError): self.dialog._choice_strings() def test_choice_strings_duplicated(self): # test that choice strings work using simple strings self.dialog.choices = ["red", "green", "blue", "green"] with self.assertRaises(ValueError): self.dialog._choice_strings() @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_accept(self): # test that accept works as expected tester = ModalDialogTester(self.dialog.open) tester.open_and_run(when_opened=lambda x: x.close(accept=True)) self.assertEqual(tester.result, OK) self.assertEqual(self.dialog.return_code, OK) self.assertEqual(self.dialog.choice, "red") @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_reject(self): # test that accept works as expected tester = ModalDialogTester(self.dialog.open) tester.open_and_run(when_opened=lambda x: x.close(accept=False)) self.assertEqual(tester.result, CANCEL) self.assertEqual(self.dialog.return_code, CANCEL) self.assertIsNone(self.dialog.choice) @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_close(self): # test that closing works as expected tester = ModalDialogTester(self.dialog.open) tester.open_and_run( when_opened=lambda x: x.get_dialog_widget().close() ) self.assertEqual(tester.result, CANCEL) self.assertEqual(self.dialog.return_code, CANCEL) self.assertIsNone(self.dialog.choice) @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_parent(self): # test that lifecycle works with a parent parent = Window() self.dialog.parent = parent.control parent.open() tester = ModalDialogTester(self.dialog.open) tester.open_and_run(when_opened=lambda x: x.close(accept=True)) with self.event_loop(): parent.close() self.assertEqual(tester.result, OK) self.assertEqual(self.dialog.return_code, OK) @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_change_choice_accept(self): # test that if we change choice it's reflected in result def select_green_and_ok(tester): control = tester.get_dialog_widget() control.setTextValue("green") tester.close(accept=True) tester = ModalDialogTester(self.dialog.open) tester.open_and_run(when_opened=select_green_and_ok) self.assertEqual(tester.result, OK) self.assertEqual(self.dialog.return_code, OK) self.assertEqual(self.dialog.choice, "green") @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_change_choice_with_reject(self): # test that lifecycle works with a parent def select_green_and_cancel(tester): control = tester.get_dialog_widget() control.setTextValue("green") tester.close(accept=False) tester = ModalDialogTester(self.dialog.open) tester.open_and_run(when_opened=select_green_and_cancel) self.assertEqual(tester.result, CANCEL) self.assertEqual(self.dialog.return_code, CANCEL) self.assertIsNone(self.dialog.choice) @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_change_choice_with_close(self): # test that lifecycle works with a parent def select_green_and_close(tester): control = tester.get_dialog_widget() control.setTextValue("green") control.close() tester = ModalDialogTester(self.dialog.open) tester.open_and_run(when_opened=select_green_and_close) self.assertEqual(tester.result, CANCEL) self.assertEqual(self.dialog.return_code, CANCEL) self.assertIsNone(self.dialog.choice) pyface-7.4.0/pyface/tests/test_heading_text.py0000644000076500000240000000576014176222673022441 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 ..heading_text import HeadingText from ..image_resource import ImageResource from ..toolkit import toolkit_object from ..window import Window GuiTestAssistant = toolkit_object("util.gui_test_assistant:GuiTestAssistant") no_gui_test_assistant = GuiTestAssistant.__name__ == "Unimplemented" is_wx = toolkit_object.toolkit == "wx" @unittest.skipIf(no_gui_test_assistant, "No GuiTestAssistant") class TestHeadingText(unittest.TestCase, GuiTestAssistant): def setUp(self): GuiTestAssistant.setUp(self) self.window = Window() self.window._create() def tearDown(self): if self.widget.control is not None: with self.delete_widget(self.widget.control): self.widget.destroy() if self.window.control is not None: with self.delete_widget(self.window.control): self.window.destroy() del self.widget del self.window GuiTestAssistant.tearDown(self) def test_lifecycle(self): # test that destroy works with self.event_loop(): with self.assertWarns(PendingDeprecationWarning): self.widget = HeadingText(self.window.control) self.assertIsNotNone(self.widget.control) with self.event_loop(): self.widget.destroy() def test_two_stage_create(self): # test that create=False works self.widget = HeadingText(create=False) self.assertIsNone(self.widget.control) with self.event_loop(): self.widget.parent = self.window.control self.widget.create() self.assertIsNotNone(self.widget.control) with self.event_loop(): self.widget.destroy() def test_text(self): # test that create works with text with self.event_loop(): self.widget = HeadingText( self.window.control, text="Hello", create=False, ) self.widget.create() self.assertEqual(self.widget.text, "Hello") self.assertEqual(self.widget._get_control_text(), "Hello") with self.event_loop(): self.widget.destroy() @unittest.skipUnless(is_wx, "Only Wx supports background images") def test_image(self): # test that image raises a deprecation warning with self.event_loop(): with self.assertWarns(PendingDeprecationWarning): self.widget = HeadingText( self.window.control, create=False, image=ImageResource("core.png"), ) pyface-7.4.0/pyface/tests/test_resource_manager.py0000644000076500000240000000636514176222673023321 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 collections.abc import Sequence import importlib.util import os import shutil import tempfile import unittest import pyface # a package with images as package resources from ..resource_manager import PyfaceResourceFactory from ..resource_manager import ResourceManager IMAGE_PATH = os.path.join(os.path.dirname(__file__), "images", "core.png") class TestPyfaceResourceFactory(unittest.TestCase): def setUp(self): self.resource_factory = PyfaceResourceFactory() def test_image_from_file(self): self.resource_factory.image_from_file(IMAGE_PATH) def test_image_from_data(self): with open(IMAGE_PATH, "rb") as fp: data = fp.read() self.resource_factory.image_from_data(data) def test_locate_image(self): class ASequence(Sequence): def __init__(self, data): self.data = data def __getitem__(self, i): return self.data[i] def __len__(self): return len(self.data) sequence = ASequence([os.path.dirname(IMAGE_PATH)]) resource_manager = ResourceManager() img_ref = resource_manager.locate_image("core.png", sequence) self.assertEqual(IMAGE_PATH, img_ref.filename) def test_locate_image_with_module(self): # ResourceManager should be able to find the images/close.png which # is included in pyface package data. resource_manager = ResourceManager() image_ref = resource_manager.locate_image("close.png", [pyface]) self.assertGreater(len(image_ref.data), 0) def test_locate_image_with_module_missing_file(self): # The required image is not found, locate_image should return None. resource_manager = ResourceManager() image_ref = resource_manager.locate_image( "does_not_exist.png", [pyface] ) self.assertIsNone(image_ref) def test_locate_image_with_name_being_dunder_main(self): # When a module is not a package, we will fall back to use __file__ # given a module from which there is an image in the same folder with tempfile.TemporaryDirectory() as tmp_dir: shutil.copyfile(IMAGE_PATH, os.path.join(tmp_dir, "random.png")) # create an empty file for creating a module. py_filepath = os.path.join(tmp_dir, "tmp.py") with open(py_filepath, "w", encoding="utf-8"): pass spec = importlib.util.spec_from_file_location( "__main__", py_filepath ) module = importlib.util.module_from_spec(spec) resource_manager = ResourceManager( resource_factory=PyfaceResourceFactory() ) # when image_ref = resource_manager.load_image("random.png", [module]) # then self.assertIsNotNone(image_ref) pyface-7.4.0/pyface/tests/test_image_cache.py0000644000076500000240000000310214176222673022167 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 from ..image_cache import ImageCache IMAGE_PATH = os.path.join(os.path.dirname(__file__), "images", "core.png") class TestPyfaceResourceFactory(unittest.TestCase): def setUp(self): self.image_cache = ImageCache(32, 32) def test_get_image(self): self.image_cache.get_image(IMAGE_PATH) def test_get_bitmap(self): self.image_cache.get_bitmap(IMAGE_PATH) def test_get_image_twice(self): self.image_cache.get_image(IMAGE_PATH) self.image_cache.get_image(IMAGE_PATH) def test_get_bitmap_twice(self): self.image_cache.get_bitmap(IMAGE_PATH) self.image_cache.get_bitmap(IMAGE_PATH) def test_get_image_different_sizes(self): other_image_cache = ImageCache(48, 48) image1 = self.image_cache.get_image(IMAGE_PATH) image2 = other_image_cache.get_image(IMAGE_PATH) self.assertNotEqual(image1, image2) def test_get_bitmap_different_sizes(self): other_image_cache = ImageCache(48, 48) bitmap1 = self.image_cache.get_bitmap(IMAGE_PATH) bitmap2 = other_image_cache.get_bitmap(IMAGE_PATH) self.assertNotEqual(bitmap1, bitmap2) pyface-7.4.0/pyface/tests/test_color.py0000644000076500000240000001604114176222673021106 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.testing.api import UnittestTools from pyface.color import Color class TestColor(UnittestTools, TestCase): def assert_tuple_almost_equal(self, tuple_1, tuple_2): self.assertEqual(len(tuple_1), len(tuple_2)) for x, y in zip(tuple_1, tuple_2): self.assertAlmostEqual(x, y) def test_init(self): color = Color() self.assertEqual(color.rgba, (1.0, 1.0, 1.0, 1.0)) def test_init_rgba(self): color = Color(rgba=(0.4, 0.2, 0.6, 0.8)) self.assertEqual(color.rgba, (0.4, 0.2, 0.6, 0.8)) def test_init_rgb(self): color = Color(rgb=(0.4, 0.2, 0.6)) self.assertEqual(color.rgba, (0.4, 0.2, 0.6, 1.0)) def test_init_r_g_b_a(self): color = Color(red=0.4, green=0.2, blue=0.6, alpha=0.8) self.assertEqual(color.rgba, (0.4, 0.2, 0.6, 0.8)) def test_init_r_g_b(self): color = Color(red=0.4, green=0.2, blue=0.6) self.assertEqual(color.rgba, (0.4, 0.2, 0.6, 1.0)) def test_init_hsva(self): color = Color(hsva=(0.4, 0.2, 0.6, 0.8)) self.assert_tuple_almost_equal(color.rgba, (0.48, 0.6, 0.528, 0.8)) def test_init_hsv(self): color = Color(hsv=(0.4, 0.2, 0.6)) self.assert_tuple_almost_equal(color.rgba, (0.48, 0.6, 0.528, 1.0)) def test_init_hlsa(self): color = Color(hlsa=(0.4, 0.2, 0.6, 0.8)) self.assert_tuple_almost_equal(color.rgba, (0.08, 0.32, 0.176, 0.8)) def test_init_hls(self): color = Color(hls=(0.4, 0.2, 0.6)) self.assert_tuple_almost_equal(color.rgba, (0.08, 0.32, 0.176, 1.0)) def test_from_str_name(self): color = Color.from_str('rebeccapurple') self.assertEqual(color.rgba, (0.4, 0.2, 0.6, 1.0)) def test_from_str_hex(self): color = Color.from_str('#663399ff') self.assertEqual(color.rgba, (0.4, 0.2, 0.6, 1.0)) def test_from_str_extra_argument(self): color = Color.from_str('#663399', alpha=0.5) self.assertEqual(color.rgba, (0.4, 0.2, 0.6, 0.5)) def test_from_str_duplicate_argument(self): with self.assertRaises(TypeError): Color.from_str('rebeccapurple', rgba=(1.0, 1.0, 1.0, 1.0)) def test_toolkit_round_trip(self): color = Color(rgba=(0.4, 0.2, 0.6, 0.8)) toolkit_color = color.to_toolkit() result = Color.from_toolkit(toolkit_color) self.assertEqual(result.rgba, (0.4, 0.2, 0.6, 0.8)) def test_hex(self): color = Color(rgba=(0.4, 0.2, 0.6, 0.8)) hex_value = color.hex() self.assertEqual(hex_value, "#663399CC") def test_hex_black(self): color = Color(rgba=(0.0, 0.0, 0.0, 1.0)) hex_value = color.hex() self.assertEqual(hex_value, "#000000FF") def test_eq(self): color_1 = Color(rgba=(0.4, 0.2, 0.6, 0.8)) color_2 = Color(rgba=(0.4, 0.2, 0.6, 0.8)) self.assertTrue(color_1 == color_2) self.assertFalse(color_1 != color_2) def test_eq_not_equal(self): color_1 = Color(rgba=(0.4, 0.2, 0.6, 0.8)) color_2 = Color(rgba=(0.4, 0.4, 0.6, 0.8)) self.assertTrue(color_1 != color_2) self.assertFalse(color_1 == color_2) def test_eq_other(self): color = Color(rgba=(0.4, 0.2, 0.6, 0.8)) self.assertFalse(color == 1) self.assertTrue(color != 1) def test_not_eq(self): color_1 = Color(rgba=(0.4, 0.2, 0.6, 0.8)) color_2 = Color(rgba=(0.0, 0.0, 0.0, 1.0)) self.assertTrue(color_1 != color_2) self.assertFalse(color_1 == color_2) def test_str(self): color = Color(rgba=(0.4, 0.2, 0.6, 0.8)) result = str(color) self.assertEqual(result, "(0.4, 0.2, 0.6, 0.8)") def test_repr(self): color = Color(rgba=(0.4, 0.2, 0.6, 0.8)) result = repr(color) self.assertEqual(result, "Color(rgba=(0.4, 0.2, 0.6, 0.8))") def test_get_red(self): color = Color(rgba=(0.4, 0.2, 0.6, 0.8)) self.assertEqual(color.red, 0.4) def test_set_red(self): color = Color(rgba=(0.4, 0.2, 0.6, 0.8)) color.red = 1.0 self.assertEqual(color.rgba, (1.0, 0.2, 0.6, 0.8)) def test_get_green(self): color = Color(rgba=(0.4, 0.2, 0.6, 0.8)) self.assertEqual(color.green, 0.2) def test_set_green(self): color = Color(rgba=(0.4, 0.2, 0.6, 0.8)) color.green = 1.0 self.assertEqual(color.rgba, (0.4, 1.0, 0.6, 0.8)) def test_get_blue(self): color = Color(rgba=(0.4, 0.2, 0.6, 0.8)) self.assertEqual(color.blue, 0.6) def test_set_blue(self): color = Color(rgba=(0.4, 0.2, 0.6, 0.8)) color.blue = 1.0 self.assertEqual(color.rgba, (0.4, 0.2, 1.0, 0.8)) def test_get_alpha(self): color = Color(rgba=(0.4, 0.2, 0.6, 0.8)) self.assertEqual(color.alpha, 0.8) def test_set_alpha(self): color = Color(rgba=(0.4, 0.2, 0.6, 0.8)) color.alpha = 1.0 self.assertEqual(color.rgba, (0.4, 0.2, 0.6, 1.0)) def test_get_rgb(self): color = Color(rgba=(0.4, 0.2, 0.6, 0.8)) self.assertEqual(color.rgb, (0.4, 0.2, 0.6)) def test_set_rgb(self): color = Color(rgba=(0.4, 0.2, 0.6, 0.8)) color.rgb = (0.6, 0.8, 0.4) self.assertEqual(color.rgba, (0.6, 0.8, 0.4, 0.8)) def test_get_hsv(self): color = Color(rgba=(0.48, 0.6, 0.528, 0.8)) self.assert_tuple_almost_equal(color.hsv, (0.4, 0.2, 0.6)) def test_set_hsv(self): color = Color() color.hsv = (0.4, 0.2, 0.6) self.assert_tuple_almost_equal(color.rgba, (0.48, 0.6, 0.528, 1.0)) def test_get_hsva(self): color = Color(rgba=(0.48, 0.6, 0.528, 0.8)) self.assert_tuple_almost_equal(color.hsva, (0.4, 0.2, 0.6, 0.8)) def test_set_hsva(self): color = Color() color.hsva = (0.4, 0.2, 0.6, 0.8) self.assert_tuple_almost_equal(color.rgba, (0.48, 0.6, 0.528, 0.8)) def test_get_hls(self): color = Color(rgba=(0.08, 0.32, 0.176, 0.8)) self.assert_tuple_almost_equal(color.hls, (0.4, 0.2, 0.6)) def test_set_hls(self): color = Color() color.hls = (0.4, 0.2, 0.6) self.assert_tuple_almost_equal(color.rgba, (0.08, 0.32, 0.176, 1)) def test_get_hlsa(self): color = Color(rgba=(0.08, 0.32, 0.176, 0.8)) self.assert_tuple_almost_equal(color.hlsa, (0.4, 0.2, 0.6, 0.8)) def test_set_hlsa(self): color = Color() color.hlsa = (0.4, 0.2, 0.6, 0.8) self.assert_tuple_almost_equal(color.rgba, (0.08, 0.32, 0.176, 0.8)) def test_get_is_dark(self): color = Color(rgba=(0.08, 0.32, 0.176, 0.8)) self.assertTrue(color.is_dark) pyface-7.4.0/pyface/tests/test_widget.py0000644000076500000240000002503114176253531021247 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 sys import unittest from traits.api import Instance from traits.testing.api import UnittestTools from pyface.testing.widget_mixin import WidgetMixin from ..application_window import ApplicationWindow from ..toolkit import toolkit_object from ..widget import Widget GuiTestAssistant = toolkit_object("util.gui_test_assistant:GuiTestAssistant") no_gui_test_assistant = GuiTestAssistant.__name__ == "Unimplemented" is_qt = (toolkit_object.toolkit in {"qt4", "qt"}) is_linux = (sys.platform == "linux") is_mac = (sys.platform == "darwin") class ConcreteWidget(Widget): def _create_control(self, parent): if toolkit_object.toolkit == "wx": import wx control = wx.Window(parent) control.Enable(self.enabled) control.Show(self.visible) elif toolkit_object.toolkit in {"qt4", "qt"}: from pyface.qt import QtGui from pyface.qt.QtCore import Qt control = QtGui.QWidget(parent) control.setEnabled(self.enabled) control.setVisible(self.visible) control.setFocusPolicy(Qt.FocusPolicy.StrongFocus) else: control = None return control class MainWindow(ApplicationWindow): widget = Instance(ConcreteWidget) def _create_contents(self, parent): """ Create and return the window's contents. Parameters ---------- parent : toolkit control The window's toolkit control to be used as the parent for widgets in the contents. Returns ------- control : toolkit control A control to be used for contents of the window. """ self.widget = ConcreteWidget(parent=parent) self.widget._create() return self.widget.control class TestWidget(unittest.TestCase, UnittestTools): def setUp(self): self.widget = Widget() def tearDown(self): del self.widget def test_defaults(self): self.assertTrue(self.widget.enabled) self.assertTrue(self.widget.visible) def test_create(self): # create is not Implemented with self.assertRaises(NotImplementedError): self.widget.create() def test__create(self): # _create is not Implemented with self.assertRaises(NotImplementedError): self.widget._create() def test_destroy(self): # test that destroy works even when no control self.widget.destroy() def test_show(self): with self.assertTraitChanges(self.widget, "visible", count=1): self.widget.show(False) self.assertFalse(self.widget.visible) def test_visible(self): with self.assertTraitChanges(self.widget, "visible", count=1): self.widget.visible = False self.assertFalse(self.widget.visible) def test_enable(self): with self.assertTraitChanges(self.widget, "enabled", count=1): self.widget.enable(False) self.assertFalse(self.widget.enabled) def test_enabled(self): with self.assertTraitChanges(self.widget, "enabled", count=1): self.widget.enabled = False self.assertFalse(self.widget.enabled) @unittest.skipIf(no_gui_test_assistant, "No GuiTestAssistant") class TestConcreteWidget(unittest.TestCase, GuiTestAssistant): def setUp(self): GuiTestAssistant.setUp(self) self.widget = ConcreteWidget() def tearDown(self): if self.widget.control is not None: with self.delete_widget(self.widget.control): self.widget.destroy() del self.widget GuiTestAssistant.tearDown(self) def test_lifecycle(self): with self.event_loop(): self.widget.create() with self.event_loop(): self.widget.destroy() def test_initialize(self): self.widget.visible = False self.widget.enabled = False with self.event_loop(): self.widget.create() self.assertFalse(self.widget.control.isVisible()) self.assertFalse(self.widget.control.isEnabled()) def test_show(self): with self.event_loop(): self.widget.create() with self.assertTraitChanges(self.widget, "visible", count=1): with self.event_loop(): self.widget.show(False) self.assertFalse(self.widget.control.isVisible()) def test_visible(self): with self.event_loop(): self.widget.create() with self.assertTraitChanges(self.widget, "visible", count=1): with self.event_loop(): self.widget.visible = False self.assertFalse(self.widget.control.isVisible()) def test_contents_visible(self): window = MainWindow() window._create() try: with self.event_loop(): window.open() # widget visible trait stays True when parent is hidden with self.assertTraitDoesNotChange(window.widget, "visible"): with self.event_loop(): window.visible = False # widget visible trait stays True when parent is shown with self.assertTraitDoesNotChange(window.widget, "visible"): with self.event_loop(): window.visible = True finally: window.destroy() def test_contents_hidden(self): window = MainWindow() window._create() try: with self.event_loop(): window.open() window.widget.visible = False # widget visible trait stays False when parent is hidden with self.assertTraitDoesNotChange(window.widget, "visible"): with self.event_loop(): window.visible = False # widget visible trait stays False when parent is shown with self.assertTraitDoesNotChange(window.widget, "visible"): with self.event_loop(): window.visible = True finally: window.destroy() @unittest.skipUnless(is_qt, "Qt-specific test of hidden state") def test_contents_hide_external_change(self): window = MainWindow() window._create() try: with self.event_loop(): window.open() # widget visibile trait stays True when parent is hidden with self.assertTraitDoesNotChange(window.widget, "visible"): with self.event_loop(): window.visible = False self.assertFalse(window.widget.control.isVisible()) self.assertFalse(window.widget.control.isHidden()) # widget visibile trait becomes False when widget is hidden with self.assertTraitChanges(window.widget, "visible"): with self.event_loop(): window.widget.control.hide() self.assertFalse(window.widget.visible) self.assertFalse(window.widget.control.isVisible()) self.assertTrue(window.widget.control.isHidden()) # widget visibile trait stays False when parent is shown with self.assertTraitDoesNotChange(window.widget, "visible"): with self.event_loop(): window.visible = True self.assertFalse(window.widget.control.isVisible()) self.assertTrue(window.widget.control.isHidden()) finally: window.destroy() @unittest.skipUnless(is_qt, "Qt-specific test of hidden state") def test_show_widget_with_parent_is_invisible_qt(self): # Test setting the widget visible to true when its parent visibility # is false. window = MainWindow() window._create() try: # given with self.event_loop(): window.open() window.widget.visible = False with self.event_loop(): window.visible = False # when with self.event_loop(): window.widget.visible = True # then self.assertTrue(window.widget.visible) self.assertFalse(window.widget.control.isVisible()) self.assertFalse(window.widget.control.isHidden()) finally: window.destroy() @unittest.skipUnless(is_qt, "Qt-specific test of hidden state") def test_show_widget_then_parent_is_invisible_qt(self): # Test showing the widget when the parent is also visible, and then # make the parent invisible window = MainWindow() window._create() try: # given with self.event_loop(): window.open() window.visible = True with self.event_loop(): window.widget.visible = True # when with self.event_loop(): window.visible = False # then self.assertTrue(window.widget.visible) self.assertFalse(window.widget.control.isVisible()) self.assertFalse(window.widget.control.isHidden()) finally: window.destroy() def test_enable(self): with self.event_loop(): self.widget.create() with self.assertTraitChanges(self.widget, "enabled", count=1): with self.event_loop(): self.widget.enable(False) self.assertFalse(self.widget.control.isEnabled()) def test_enabled(self): with self.event_loop(): self.widget.create() with self.assertTraitChanges(self.widget, "enabled", count=1): with self.event_loop(): self.widget.enabled = False self.assertFalse(self.widget.control.isEnabled()) @unittest.skipUnless( is_mac, "Broken on Linux and Windows", ) def test_focus(self): with self.event_loop(): self.widget.create() self.assertFalse(self.widget.has_focus()) with self.event_loop(): self.widget.focus() self.assertTrue(self.widget.has_focus()) class TestWidgetCommon(WidgetMixin, unittest.TestCase): def _create_widget(self): return ConcreteWidget(parent=self.parent.control, tooltip='Dummy') pyface-7.4.0/pyface/tests/test_file_dialog.py0000644000076500000240000000641114176222673022226 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 from ..file_dialog import FileDialog from ..toolkit import toolkit_object GuiTestAssistant = toolkit_object("util.gui_test_assistant:GuiTestAssistant") no_gui_test_assistant = GuiTestAssistant.__name__ == "Unimplemented" ModalDialogTester = toolkit_object( "util.modal_dialog_tester:ModalDialogTester" ) no_modal_dialog_tester = ModalDialogTester.__name__ == "Unimplemented" @unittest.skipIf(no_gui_test_assistant, "No GuiTestAssistant") class TestFileDialog(unittest.TestCase, GuiTestAssistant): def setUp(self): GuiTestAssistant.setUp(self) self.dialog = FileDialog() def tearDown(self): if self.dialog.control is not None: with self.delete_widget(self.dialog.control): self.dialog.destroy() del self.dialog GuiTestAssistant.tearDown(self) def test_create_wildcard(self): wildcard = FileDialog.create_wildcard("Python", "*.py") self.assertTrue(len(wildcard) != 0) def test_create_wildcard_multiple(self): wildcard = FileDialog.create_wildcard( "Python", ["*.py", "*.pyo", "*.pyc", "*.pyd"] ) self.assertTrue(len(wildcard) != 0) def test_create(self): # test that creation and destruction works as expected with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() def test_destroy(self): # test that destroy works even when no control with self.event_loop(): self.dialog.destroy() def test_close(self): # test that close works with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.close() def test_default_path(self): # test that default path works self.dialog.default_path = os.path.join("images", "core.png") with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.close() def test_default_dir_and_file(self): # test that default dir and path works self.dialog.default_directory = "images" self.dialog.default_filename = "core.png" with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.close() def test_open_files(self): # test that open files action works self.dialog.action = "open files" with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.close() def test_save_as(self): # test that open files action works self.dialog.action = "save as" with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.close() # XXX would be nice to actually test with an open dialog, but not right now pyface-7.4.0/pyface/tests/test_window.py0000644000076500000240000002325714176222673021306 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 ..constant import CANCEL, NO, OK, YES from ..toolkit import toolkit_object from ..window import Window is_qt = toolkit_object.toolkit == "qt4" if is_qt: from pyface.qt import qt_api GuiTestAssistant = toolkit_object("util.gui_test_assistant:GuiTestAssistant") no_gui_test_assistant = GuiTestAssistant.__name__ == "Unimplemented" ModalDialogTester = toolkit_object( "util.modal_dialog_tester:ModalDialogTester" ) no_modal_dialog_tester = ModalDialogTester.__name__ == "Unimplemented" is_pyqt5 = is_qt and qt_api == "pyqt5" is_pyqt4_linux = is_qt and qt_api == "pyqt" and platform.system() == "Linux" @unittest.skipIf(no_gui_test_assistant, "No GuiTestAssistant") class TestWindow(unittest.TestCase, GuiTestAssistant): def setUp(self): GuiTestAssistant.setUp(self) self.window = Window() def tearDown(self): if self.window.control is not None: with self.delete_widget(self.window.control): self.window.destroy() self.window = None GuiTestAssistant.tearDown(self) def test_destroy(self): # test that destroy works even when no control with self.event_loop(): self.window.destroy() def test_open_close(self): # test that opening and closing works as expected with self.assertTraitChanges(self.window, "opening", count=1): with self.assertTraitChanges(self.window, "opened", count=1): with self.event_loop(): self.window.open() with self.assertTraitChanges(self.window, "closing", count=1): with self.assertTraitChanges(self.window, "closed", count=1): with self.event_loop(): self.window.close() def test_show(self): # test that showing works as expected with self.event_loop(): self.window._create() with self.event_loop(): self.window.show(True) with self.event_loop(): self.window.show(False) with self.event_loop(): self.window.destroy() def test_activate(self): # test that activation works as expected with self.event_loop(): self.window.open() with self.event_loop(): self.window.activate() with self.event_loop(): self.window.close() def test_position(self): # test that default position works as expected self.window.position = (100, 100) with self.event_loop(): self.window.open() with self.event_loop(): self.window.close() def test_reposition(self): # test that changing position works as expected with self.event_loop(): self.window.open() with self.event_loop(): self.window.position = (100, 100) with self.event_loop(): self.window.close() def test_size(self): # test that default size works as expected self.window.size = (100, 100) with self.event_loop(): self.window.open() with self.event_loop(): self.window.close() def test_resize(self): # test that changing size works as expected with self.event_loop(): self.window.open() with self.event_loop(): self.window.size = (100, 100) with self.event_loop(): self.window.close() def test_title(self): # test that default title works as expected self.window.title = "Test Title" with self.event_loop(): self.window.open() with self.event_loop(): self.window.close() def test_retitle(self): # test that changing title works as expected with self.event_loop(): self.window.open() with self.event_loop(): self.window.title = "Test Title" with self.event_loop(): self.window.close() def test_show_event(self): with self.event_loop(): self.window.open() with self.event_loop(): self.window.visible = False with self.assertTraitChanges(self.window, "visible", count=1): with self.event_loop(): self.window.control.show() self.assertTrue(self.window.visible) def test_hide_event(self): with self.event_loop(): self.window.open() with self.assertTraitChanges(self.window, "visible", count=1): with self.event_loop(): self.window.control.hide() self.assertFalse(self.window.visible) @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") @unittest.skipIf( is_pyqt5, "Confirmation dialog click tests don't work on pyqt5." ) @unittest.skipIf( is_pyqt4_linux, "Confirmation dialog click tests don't work reliably on linux. Issue #282.", ) def test_confirm_reject(self): # test that cancel works as expected tester = ModalDialogTester( lambda: self.window.confirm("message", cancel=True) ) tester.open_and_run(when_opened=lambda x: x.close(accept=False)) self.assertEqual(tester.result, CANCEL) @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") @unittest.skipIf( is_pyqt5, "Confirmation dialog click tests don't work on pyqt5." ) @unittest.skipIf( is_pyqt4_linux, "Confirmation dialog click tests don't work reliably on linux. Issue #282.", ) def test_confirm_yes(self): # test that yes works as expected tester = ModalDialogTester(lambda: self.window.confirm("message")) tester.open_and_wait(when_opened=lambda x: x.click_button(YES)) self.assertEqual(tester.result, YES) @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") @unittest.skipIf( is_pyqt5, "Confirmation dialog click tests don't work on pyqt5." ) @unittest.skipIf( is_pyqt4_linux, "Confirmation dialog click tests don't work reliably on linux. Issue #282.", ) def test_confirm_no(self): # test that no works as expected tester = ModalDialogTester(lambda: self.window.confirm("message")) tester.open_and_wait(when_opened=lambda x: x.click_button(NO)) self.assertEqual(tester.result, NO) @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") @unittest.skipIf( is_pyqt5, "Confirmation dialog click tests don't work on pyqt5." ) @unittest.skipIf( is_pyqt4_linux, "Confirmation dialog click tests don't work reliably on linux. Issue #282.", ) def test_confirm_cancel(self): # test that cncel works as expected tester = ModalDialogTester( lambda: self.window.confirm("message", cancel=True) ) tester.open_and_wait(when_opened=lambda x: x.click_button(CANCEL)) self.assertEqual(tester.result, CANCEL) @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_information_accept(self): self._check_message_dialog_accept(self.window.information) @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") @unittest.skipIf( is_pyqt5, "Message dialog click tests don't work on pyqt5." ) @unittest.skipIf( is_pyqt4_linux, "Message dialog click tests don't work reliably on linux. Issue #282.", ) def test_information_ok(self): self._check_message_dialog_ok(self.window.information) @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_warning_accept(self): self._check_message_dialog_accept(self.window.warning) @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") @unittest.skipIf( is_pyqt5, "Message dialog click tests don't work on pyqt5." ) @unittest.skipIf( is_pyqt4_linux, "Message dialog click tests don't work reliably on linux. Issue #282.", ) def test_warning_ok(self): self._check_message_dialog_ok(self.window.warning) @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_error_accept(self): self._check_message_dialog_accept(self.window.error) @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") @unittest.skipIf( is_pyqt5, "Message dialog click tests don't work on pyqt5." ) @unittest.skipIf( is_pyqt4_linux, "Message dialog click tests don't work reliably on linux. Issue #282.", ) def test_error_ok(self): self._check_message_dialog_ok(self.window.error) def _check_message_dialog_ok(self, method): tester = self._setup_tester(method) tester.open_and_wait(when_opened=lambda x: x.click_button(OK)) self.assertIsNone(tester.result) def _check_message_dialog_accept(self, method): tester = self._setup_tester(method) tester.open_and_run(when_opened=lambda x: x.close(accept=True)) self.assertIsNone(tester.result) def _setup_tester(self, method): kwargs = { "title": "Title", "detail": "Detail", "informative": "Informative", } tester = ModalDialogTester(lambda: method("message", **kwargs)) return tester pyface-7.4.0/pyface/tests/test_layered_panel.py0000644000076500000240000001146014176222673022574 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 ..heading_text import HeadingText from ..layered_panel import LayeredPanel from ..toolkit import toolkit_object from ..window import Window GuiTestAssistant = toolkit_object("util.gui_test_assistant:GuiTestAssistant") no_gui_test_assistant = GuiTestAssistant.__name__ == "Unimplemented" @unittest.skipIf(no_gui_test_assistant, "No GuiTestAssistant") class TestLayeredPanel(unittest.TestCase, GuiTestAssistant): def setUp(self): GuiTestAssistant.setUp(self) self.window = Window() self.window._create() def tearDown(self): if self.widget.control is not None: with self.delete_widget(self.widget.control): self.widget.destroy() if self.window.control is not None: with self.delete_widget(self.window.control): self.window.destroy() del self.widget del self.window GuiTestAssistant.tearDown(self) def test_lifecycle(self): # test that create and destory work self.widget = LayeredPanel(create=False) self.assertIsNone(self.widget.control) with self.event_loop(): self.widget.parent = self.window.control self.widget.create() self.assertIsNotNone(self.widget.control) with self.event_loop(): self.widget.destroy() self.assertIsNone(self.widget.control) def test_add_layer(self): # test that a layer can be added self.widget = LayeredPanel(create=False) with self.event_loop(): self.widget.parent = self.window.control self.widget.create() layer_widget = HeadingText(parent=self.window.control, create=False) with self.event_loop(): layer_widget.create() try: with self.event_loop(): self.widget.add_layer("test 1", layer_widget.control) self.assertIn("test 1", self.widget._layers) self.assertIs(self.widget._layers["test 1"], layer_widget.control) finally: with self.event_loop(): layer_widget.destroy() with self.event_loop(): self.widget.destroy() def test_show_layer(self): # test that a layer can be shown self.widget = LayeredPanel(create=False) with self.event_loop(): self.widget.parent = self.window.control self.widget.create() layer_widget_1 = HeadingText(parent=self.window.control, create=False) layer_widget_2 = HeadingText(parent=self.window.control, create=False) with self.event_loop(): layer_widget_1.create() layer_widget_2.create() self.widget.add_layer("test 1", layer_widget_1.control) self.widget.add_layer("test 2", layer_widget_2.control) try: with self.event_loop(): self.widget.show_layer("test 1") self.assertEqual(self.widget.current_layer_name, "test 1") self.assertIs(self.widget.current_layer, layer_widget_1.control) with self.event_loop(): self.widget.show_layer("test 2") self.assertEqual(self.widget.current_layer_name, "test 2") self.assertIs(self.widget.current_layer, layer_widget_2.control) finally: with self.event_loop(): layer_widget_1.destroy() layer_widget_2.destroy() with self.event_loop(): self.widget.destroy() def test_has_layer(self): # test that a has_layer works self.widget = LayeredPanel(create=False) with self.event_loop(): self.widget.parent = self.window.control self.widget.create() layer_widget_1 = HeadingText(parent=self.window.control, create=False) layer_widget_2 = HeadingText(parent=self.window.control, create=False) with self.event_loop(): layer_widget_1.create() layer_widget_2.create() self.widget.add_layer("test 1", layer_widget_1.control) self.widget.add_layer("test 2", layer_widget_2.control) try: self.assertTrue(self.widget.has_layer("test 1")) self.assertTrue(self.widget.has_layer("test 2")) finally: with self.event_loop(): layer_widget_1.destroy() layer_widget_2.destroy() with self.event_loop(): self.widget.destroy() pyface-7.4.0/pyface/tests/test_dialog.py0000644000076500000240000002265114176222673021233 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 ..dialog import Dialog from ..constant import OK, CANCEL from ..toolkit import toolkit_object is_qt = toolkit_object.toolkit == "qt4" if is_qt: from pyface.qt import qt_api GuiTestAssistant = toolkit_object("util.gui_test_assistant:GuiTestAssistant") no_gui_test_assistant = GuiTestAssistant.__name__ == "Unimplemented" ModalDialogTester = toolkit_object( "util.modal_dialog_tester:ModalDialogTester" ) no_modal_dialog_tester = ModalDialogTester.__name__ == "Unimplemented" is_pyqt5 = is_qt and qt_api == "pyqt5" is_pyqt4_linux = is_qt and qt_api == "pyqt" and platform.system() == "Linux" @unittest.skipIf(no_gui_test_assistant, "No GuiTestAssistant") class TestDialog(unittest.TestCase, GuiTestAssistant): def setUp(self): GuiTestAssistant.setUp(self) self.dialog = Dialog() def tearDown(self): if self.dialog.control is not None: with self.delete_widget(self.dialog.control): self.dialog.destroy() self.dialog = None GuiTestAssistant.tearDown(self) def test_create(self): # test that creation and destruction works as expected with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() def test_destroy(self): # test that destroy works even when no control with self.event_loop(): self.dialog.destroy() def test_size(self): # test that size works as expected self.dialog.size = (100, 100) with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() def test_position(self): # test that position works as expected self.dialog.position = (100, 100) with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() def test_create_ok_renamed(self): # test that creation and destruction works as expected with ok_label self.dialog.ok_label = "Sure" with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() def test_create_cancel_renamed(self): # test that creation and destruction works as expected with cancel_label self.dialog.cancel_label = "I Don't Think So" with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() def test_create_help(self): # test that creation and destruction works as expected with help self.dialog.help_id = "test_help" with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() def test_create_help_label(self): # test that creation and destruction works as expected with help self.dialog.help_id = "test_help" self.dialog.help_label = "Assistance" with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_accept(self): # test that accept works as expected tester = ModalDialogTester(self.dialog.open) tester.open_and_run(when_opened=lambda x: x.close(accept=True)) self.assertEqual(tester.result, OK) self.assertEqual(self.dialog.return_code, OK) @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_reject(self): # test that reject works as expected tester = ModalDialogTester(self.dialog.open) tester.open_and_run(when_opened=lambda x: x.close(accept=False)) self.assertEqual(tester.result, CANCEL) self.assertEqual(self.dialog.return_code, CANCEL) @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_close(self): # test that closing works as expected tester = ModalDialogTester(self.dialog.open) tester.open_and_run(when_opened=lambda x: self.dialog.close()) self.assertEqual(tester.result, CANCEL) self.assertEqual(self.dialog.return_code, CANCEL) @unittest.skipIf( is_pyqt5, "Dialog click tests don't work on pyqt5." ) # noqa @unittest.skipIf( is_pyqt4_linux, "Dialog click tests don't work reliably on linux. Issue #282.", ) # noqa @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_ok(self): # test that OK works as expected tester = ModalDialogTester(self.dialog.open) tester.open_and_wait(when_opened=lambda x: x.click_button(OK)) self.assertEqual(tester.result, OK) self.assertEqual(self.dialog.return_code, OK) @unittest.skipIf( is_pyqt5, "Dialog click tests don't work on pyqt5." ) # noqa @unittest.skipIf( is_pyqt4_linux, "Dialog click tests don't work reliably on linux. Issue #282.", ) # noqa @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_cancel(self): # test that cancel works as expected tester = ModalDialogTester(self.dialog.open) tester.open_and_run(when_opened=lambda x: x.click_button(CANCEL)) self.assertEqual(tester.result, CANCEL) self.assertEqual(self.dialog.return_code, CANCEL) @unittest.skipIf( is_pyqt5, "Dialog click tests don't work on pyqt5." ) # noqa @unittest.skipIf( is_pyqt4_linux, "Dialog click tests don't work reliably on linux. Issue #282.", ) # noqa @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_renamed_ok(self): self.dialog.ok_label = "Sure" # test that OK works as expected if renames tester = ModalDialogTester(self.dialog.open) tester.open_and_wait(when_opened=lambda x: x.click_widget("Sure")) self.assertEqual(tester.result, OK) self.assertEqual(self.dialog.return_code, OK) @unittest.skipIf( is_pyqt5, "Dialog click tests don't work on pyqt5." ) # noqa @unittest.skipIf( is_pyqt4_linux, "Dialog click tests don't work reliably on linux. Issue #282.", ) # noqa @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_renamed_cancel(self): self.dialog.cancel_label = "I Don't Think So" # test that OK works as expected if renames tester = ModalDialogTester(self.dialog.open) tester.open_and_wait( when_opened=lambda x: x.click_widget("I Don't Think So") ) self.assertEqual(tester.result, CANCEL) self.assertEqual(self.dialog.return_code, CANCEL) @unittest.skipIf( is_pyqt5, "Dialog click tests don't work on pyqt5." ) # noqa @unittest.skipIf( is_pyqt4_linux, "Dialog click tests don't work reliably on linux. Issue #282.", ) # noqa @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_help(self): def click_help_and_close(tester): tester.click_widget("Help") tester.close(accept=True) self.dialog.help_id = "help_test" # test that OK works as expected if renames tester = ModalDialogTester(self.dialog.open) tester.open_and_wait(when_opened=click_help_and_close) self.assertEqual(tester.result, OK) self.assertEqual(self.dialog.return_code, OK) @unittest.skipIf( is_pyqt5, "Dialog click tests don't work on pyqt5." ) # noqa @unittest.skipIf( is_pyqt4_linux, "Dialog click tests don't work reliably on linux. Issue #282.", ) # noqa @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_renamed_help(self): def click_help_and_close(tester): tester.click_widget("Assistance") tester.close(accept=True) self.dialog.help_id = "help_test" self.dialog.help_label = "Assistance" # test that OK works as expected if renames tester = ModalDialogTester(self.dialog.open) tester.open_and_wait(when_opened=click_help_and_close) self.assertEqual(tester.result, OK) self.assertEqual(self.dialog.return_code, OK) def test_nonmodal_close(self): # test that closing works as expected self.dialog.style = "nonmodal" result = self.dialog.open() with self.event_loop(): self.dialog.close() self.assertEqual(result, OK) self.assertEqual(self.dialog.return_code, OK) def test_not_resizable(self): # test that a resizable dialog can be created # XXX use nonmodal for better cross-platform coverage self.dialog.style = "nonmodal" self.dialog.resizable = False with self.event_loop(): result = self.dialog.open() with self.event_loop(): self.dialog.close() self.assertEqual(result, OK) self.assertEqual(self.dialog.return_code, OK) pyface-7.4.0/pyface/tests/test_application.py0000644000076500000240000001470414176222673022277 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 from shutil import rmtree from tempfile import mkdtemp from unittest import TestCase from traits.api import Bool, observe from traits.testing.api import UnittestTools from ..application import ApplicationExit, Application EVENTS = [ "starting", "started", "application_initialized", "stopping", "stopped", ] class TestingApp(Application): #: Whether the app should start cleanly. start_cleanly = Bool(True) #: Whether the app should stop cleanly. stop_cleanly = Bool(True) #: Whether to try invoking exit method. do_exit = Bool(False) #: Whether the exit should be invoked as an error exit. error_exit = Bool(False) #: Whether to try force the exit (ie. ignore vetoes). force_exit = Bool(False) #: Whether to veto a call to the exit method. veto_exit = Bool(False) #: Whether or not a call to the exit method was vetoed. exit_vetoed = Bool(False) #: Whether exit preparation happened. exit_prepared = Bool(False) #: Whether exit preparation raises an error. exit_prepared_error = Bool(False) def start(self): super().start() return self.start_cleanly def stop(self): super().stop() return self.stop_cleanly def _run(self): super()._run() if self.do_exit: if self.error_exit: raise ApplicationExit("error message") else: self.exit(self.force_exit) self.exit_vetoed = True return True @observe('exiting') def _set_veto_on_exiting_event(self, event): vetoable_event = event.new vetoable_event.veto = self.veto_exit def _prepare_exit(self): self.exit_prepared = True if self.exit_prepared_error: raise Exception("Exit preparation failed") class TestApplication(TestCase, UnittestTools): def setUp(self): self.application_events = [] def event_listener(self, event): application_event = event.new self.application_events.append(application_event) def connect_listeners(self, app): for event in EVENTS: app.observe(self.event_listener, event) def test_defaults(self): from traits.etsconfig.api import ETSConfig app = Application() self.assertEqual(app.home, ETSConfig.application_home) self.assertEqual(app.user_data, ETSConfig.user_data) self.assertEqual(app.company, ETSConfig.company) def test_initialize_application_home(self): dirpath = mkdtemp() home = os.path.join(dirpath, "test") app = Application(home=home) app.initialize_application_home() try: self.assertTrue(os.path.exists(home)) finally: rmtree(dirpath) def test_lifecycle(self): app = Application() self.connect_listeners(app) with self.assertMultiTraitChanges([app], EVENTS, []): result = app.run() self.assertTrue(result) event_order = [event.event_type for event in self.application_events] self.assertEqual(event_order, EVENTS) def test_exit(self): app = TestingApp(do_exit=True) self.connect_listeners(app) with self.assertMultiTraitChanges([app], EVENTS, []): result = app.run() self.assertTrue(result) self.assertFalse(app.exit_vetoed) self.assertTrue(app.exit_prepared) event_order = [event.event_type for event in self.application_events] self.assertEqual(event_order, EVENTS) def test_exit_prepare_error(self): app = TestingApp(do_exit=True, exit_prepared_error=True) self.connect_listeners(app) with self.assertMultiTraitChanges([app], EVENTS, []): result = app.run() self.assertTrue(result) self.assertFalse(app.exit_vetoed) self.assertTrue(app.exit_prepared) event_order = [event.event_type for event in self.application_events] self.assertEqual(event_order, EVENTS) def test_veto_exit(self): app = TestingApp(do_exit=True, veto_exit=True) self.connect_listeners(app) with self.assertMultiTraitChanges([app], EVENTS, []): result = app.run() self.assertTrue(result) self.assertTrue(app.exit_vetoed) self.assertFalse(app.exit_prepared) event_order = [event.event_type for event in self.application_events] self.assertEqual(event_order, EVENTS) def test_force_exit(self): app = TestingApp(do_exit=True, force_exit=True, veto_exit=True) self.connect_listeners(app) with self.assertMultiTraitChanges([app], EVENTS, []): result = app.run() self.assertTrue(result) self.assertFalse(app.exit_vetoed) self.assertTrue(app.exit_prepared) event_order = [event.event_type for event in self.application_events] self.assertEqual(event_order, EVENTS) def test_error_exit(self): app = TestingApp(do_exit=True, error_exit=True) self.connect_listeners(app) with self.assertMultiTraitChanges([app], EVENTS, []): result = app.run() self.assertFalse(result) self.assertFalse(app.exit_vetoed) event_order = [event.event_type for event in self.application_events] self.assertEqual(event_order, EVENTS) def test_bad_start(self): app = TestingApp(start_cleanly=False) self.connect_listeners(app) with self.assertMultiTraitChanges([app], EVENTS[:1], EVENTS[1:]): result = app.run() self.assertFalse(result) event_order = [event.event_type for event in self.application_events] self.assertEqual(event_order, EVENTS[:1]) def test_bad_stop(self): app = TestingApp(stop_cleanly=False) self.connect_listeners(app) with self.assertMultiTraitChanges([app], EVENTS[:-1], EVENTS[-1:]): result = app.run() self.assertFalse(result) event_order = [event.event_type for event in self.application_events] self.assertEqual(event_order, EVENTS[:-1]) pyface-7.4.0/pyface/tests/test_api.py0000644000076500000240000001132414176222673020540 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 pyface.api """ import unittest from traits.etsconfig.api import ETSConfig is_wx = (ETSConfig.toolkit == 'wx') is_qt = ETSConfig.toolkit.startswith('qt') class TestApi(unittest.TestCase): """ Test importable items in any environment.""" def test_api_importable(self): # make sure api is importable with the most minimal # required dependencies, including in the absence of toolkit backends. from pyface import api # noqa: F401 @unittest.skipIf(not is_qt, "This test is for qt.") class TestApiQt(unittest.TestCase): """ Test importable items in a Qt environment.""" def test_importable_items_minimal(self): # Test items should be importable in a minimal Qt environment # Pygments is excused. Attempt to import PythonEditor or PythonShell # will fail in an environment without pygments will fail, just as it # would if these items were imported directly from the corresponding # subpackages. from pyface.api import ( # noqa: F401 AboutDialog, Alignment, Application, ApplicationWindow, Border, BaseDropHandler, CANCEL, Clipboard, ConfirmationDialog, Dialog, DirectoryDialog, ExpandablePanel, FileDialog, FileDropHandler, Filter, GUI, GUIApplication, HasBorder, HasMargin, HeadingText, Image, ImageCache, ImageResource, ImageWidget, KeyPressedEvent, LayeredPanel, MDIApplicationWindow, MDIWindowMenu, Margin, MessageDialog, MultiToolbarWindow, NO, OK, ProgressDialog, # PythonEditor and PythonShell are omitted SingleChoiceDialog, Sorter, SplashScreen, SplitApplicationWindow, SplitDialog, SplitPanel, SystemMetrics, Widget, Window, YES, beep, choose_one, clipboard, confirm, error, information, warning, ) def test_python_editor_python_shell_importable(self): # If pygments is in the environment, PythonEditor and PythonShell # should be importable. try: import pygments # noqa: F401 except ImportError: raise self.skipTest("This test requires pygments.") from pyface.api import ( # noqa: F401 PythonEditor, PythonShell, ) @unittest.skipIf(not is_wx, "This test is for wx.") class TestApiWx(unittest.TestCase): """ Test importable items in a wx environment.""" def test_importable_items(self): # These items should always be importable for wx environment from pyface.api import ( # noqa: F401 AboutDialog, Alignment, Application, ApplicationWindow, Border, CANCEL, Clipboard, ConfirmationDialog, BaseDropHandler, Dialog, DirectoryDialog, ExpandablePanel, FileDialog, FileDropHandler, Filter, GUI, GUIApplication, HasBorder, HasMargin, HeadingText, Image, ImageCache, ImageResource, ImageWidget, KeyPressedEvent, LayeredPanel, MDIApplicationWindow, MDIWindowMenu, Margin, MessageDialog, MultiToolbarWindow, NO, OK, ProgressDialog, PythonEditor, PythonShell, SingleChoiceDialog, Sorter, SplashScreen, SplitApplicationWindow, SplitDialog, SplitPanel, SystemMetrics, Widget, Window, YES, beep, choose_one, clipboard, confirm, error, fix_introspect_bug, information, warning, ) pyface-7.4.0/pyface/tests/test_new_toolkit/0000755000076500000240000000000014176460550021750 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/tests/test_new_toolkit/widget.py0000644000076500000240000000111014176222673023600 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # Dummy widget module for testing entrypoints from traits.api import provides from pyface.i_widget import IWidget, MWidget @provides(IWidget) class Widget(MWidget): pass pyface-7.4.0/pyface/tests/test_new_toolkit/__init__.py0000644000076500000240000000000014176222673024051 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/tests/test_new_toolkit/init.py0000644000076500000240000000106514176222673023271 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # Dummy toolkit for testing entrypoints from pyface.base_toolkit import Toolkit toolkit_object = Toolkit("pyface", "test", "pyface.tests.test_new_toolkit") pyface-7.4.0/pyface/tests/test_python_shell.py0000644000076500000240000001376514176222673022512 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 sys import unittest from ..python_shell import PythonShell from ..toolkit import toolkit_object from ..window import Window GuiTestAssistant = toolkit_object("util.gui_test_assistant:GuiTestAssistant") no_gui_test_assistant = GuiTestAssistant.__name__ == "Unimplemented" PYTHON_SCRIPT = os.path.abspath( os.path.join(os.path.dirname(__file__), "python_shell_script.py") ) @unittest.skipIf(no_gui_test_assistant, "No GuiTestAssistant") class TestPythonShell(unittest.TestCase, GuiTestAssistant): def setUp(self): GuiTestAssistant.setUp(self) self.window = Window() self.window._create() def tearDown(self): if self.widget.control is not None: with self.delete_widget(self.widget.control): self.widget.destroy() if self.window.control is not None: with self.delete_widget(self.window.control): self.window.destroy() del self.widget del self.window GuiTestAssistant.tearDown(self) def test_lifecycle(self): # test that destroy works with self.event_loop(): with self.assertWarns(PendingDeprecationWarning): self.widget = PythonShell(self.window.control) self.assertIsNotNone(self.widget.control) with self.event_loop(): self.widget.destroy() def test_two_stage_create(self): # test that create=False works self.widget = PythonShell(create=False) self.assertIsNone(self.widget.control) with self.event_loop(): self.widget.parent = self.window.control self.widget.create() self.assertIsNotNone(self.widget.control) with self.event_loop(): self.widget.destroy() def test_bind(self): # test that binding a variable works self.widget = PythonShell(parent=self.window.control, create=False) with self.event_loop(): self.widget.create() with self.event_loop(): self.widget.bind("x", 1) self.assertEqual(self.widget.interpreter().locals.get("x"), 1) with self.event_loop(): self.widget.destroy() def test_execute_command(self): # test that executing a command works self.widget = PythonShell(parent=self.window.control, create=False) with self.event_loop(): self.widget.create() with self.assertTraitChanges(self.widget, "command_executed", count=1): with self.event_loop(): self.widget.execute_command("x = 1") self.assertEqual(self.widget.interpreter().locals.get("x"), 1) with self.event_loop(): self.widget.destroy() def test_execute_command_not_hidden(self): # test that executing a non-hidden command works self.widget = PythonShell(parent=self.window.control, create=False) with self.event_loop(): self.widget.create() with self.assertTraitChanges(self.widget, "command_executed", count=1): with self.event_loop(): self.widget.execute_command("x = 1", hidden=False) self.assertEqual(self.widget.interpreter().locals.get("x"), 1) with self.event_loop(): self.widget.destroy() def test_execute_file(self): # test that executing a file works self.widget = PythonShell(parent=self.window.control, create=False) with self.event_loop(): self.widget.create() # XXX inconsistent behaviour between backends # with self.assertTraitChanges(self.widget, 'command_executed', count=1): with self.event_loop(): self.widget.execute_file(PYTHON_SCRIPT) self.assertEqual(self.widget.interpreter().locals.get("x"), 1) self.assertEqual(self.widget.interpreter().locals.get("sys"), sys) with self.event_loop(): self.widget.destroy() def test_execute_file_not_hidden(self): # test that executing a file works self.widget = PythonShell(parent=self.window.control, create=False) with self.event_loop(): self.widget.create() # XXX inconsistent behaviour between backends # with self.assertTraitChanges(self.widget, 'command_executed', count=1): with self.event_loop(): self.widget.execute_file(PYTHON_SCRIPT, hidden=False) self.assertEqual(self.widget.interpreter().locals.get("x"), 1) self.assertEqual(self.widget.interpreter().locals.get("sys"), sys) with self.event_loop(): self.widget.destroy() def test_get_history(self): # test that command history can be extracted self.widget = PythonShell(parent=self.window.control, create=False) with self.event_loop(): self.widget.create() with self.event_loop(): self.widget.execute_command("x = 1", hidden=False) history, history_index = self.widget.get_history() self.assertEqual(history, ["x = 1"]) self.assertEqual(history_index, 1) with self.event_loop(): self.widget.destroy() def test_set_history(self): # test that command history can be updated self.widget = PythonShell(parent=self.window.control, create=False) with self.event_loop(): self.widget.create() with self.event_loop(): self.widget.set_history(["x = 1", "y = x + 1"], 1) history, history_index = self.widget.get_history() self.assertEqual(history, ["x = 1", "y = x + 1"]) self.assertEqual(history_index, 1) with self.event_loop(): self.widget.destroy() pyface-7.4.0/pyface/tests/test_message_dialog.py0000644000076500000240000002134414176222673022735 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 ..message_dialog import MessageDialog, information, warning, error from ..constant import OK from ..toolkit import toolkit_object from ..window import Window is_qt = toolkit_object.toolkit == "qt4" if is_qt: from pyface.qt import qt_api GuiTestAssistant = toolkit_object("util.gui_test_assistant:GuiTestAssistant") no_gui_test_assistant = GuiTestAssistant.__name__ == "Unimplemented" ModalDialogTester = toolkit_object( "util.modal_dialog_tester:ModalDialogTester" ) no_modal_dialog_tester = ModalDialogTester.__name__ == "Unimplemented" is_pyqt5 = is_qt and qt_api == "pyqt5" is_pyqt4_linux = is_qt and qt_api == "pyqt" and platform.system() == "Linux" USING_QT = is_qt @unittest.skipIf(no_gui_test_assistant, "No GuiTestAssistant") class TestMessageDialog(unittest.TestCase, GuiTestAssistant): def setUp(self): GuiTestAssistant.setUp(self) self.dialog = MessageDialog() def tearDown(self): if self.dialog.control is not None: with self.delete_widget(self.dialog.control): self.dialog.destroy() del self.dialog GuiTestAssistant.tearDown(self) def test_create(self): # test that creation and destruction works as expected with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() def test_destroy(self): # test that destroy works even when no control with self.event_loop(): self.dialog.destroy() def test_size(self): # test that size works as expected self.dialog.size = (100, 100) with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() def test_position(self): # test that position works as expected self.dialog.position = (100, 100) with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() def test_create_parent(self): # test that creation and destruction works as expected with a parent parent = Window() self.dialog.parent = parent.control with self.event_loop(): parent._create() self.dialog._create() with self.event_loop(): self.dialog.destroy() parent.destroy() def test_create_ok_renamed(self): # test that creation and destruction works as expected with ok_label self.dialog.ok_label = "Sure" with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() def test_message(self): # test that creation and destruction works as expected with message self.dialog.message = "This is the message" with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() def test_informative(self): # test that creation and destruction works with informative self.dialog.message = "This is the message" self.dialog.informative = "This is the additional message" with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() def test_detail(self): # test that creation and destruction works with detail self.dialog.message = "This is the message" self.dialog.informative = "This is the additional message" self.dialog.detail = "This is the detail" with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() def test_warning(self): # test that creation and destruction works with warning message self.dialog.severity = "warning" with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() def test_error(self): # test that creation and destruction works with error message self.dialog.severity = "error" with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() @unittest.skipIf( is_pyqt5, "Message dialog click tests don't work on pyqt5." ) @unittest.skipIf( is_pyqt4_linux, "Message dialog click tests don't work reliably on linux. Issue #282.", ) @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_accept(self): # test that accept works as expected # XXX duplicate of Dialog test, not needed? tester = ModalDialogTester(self.dialog.open) tester.open_and_run(when_opened=lambda x: x.close(accept=True)) self.assertEqual(tester.result, OK) self.assertEqual(self.dialog.return_code, OK) @unittest.skipIf( is_pyqt5, "Message dialog click tests don't work on pyqt5." ) @unittest.skipIf( is_pyqt4_linux, "Message dialog click tests don't work reliably on linux. Issue #282.", ) @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_close(self): # test that closing works as expected # XXX duplicate of Dialog test, not needed? tester = ModalDialogTester(self.dialog.open) tester.open_and_run(when_opened=lambda x: self.dialog.close()) self.assertEqual(tester.result, OK) self.assertEqual(self.dialog.return_code, OK) @unittest.skipIf( is_pyqt5, "Message dialog click tests don't work on pyqt5." ) @unittest.skipIf( is_pyqt4_linux, "Message dialog click tests don't work reliably on linux. Issue #282.", ) @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_ok(self): # test that OK works as expected tester = ModalDialogTester(self.dialog.open) tester.open_and_wait(when_opened=lambda x: x.click_button(OK)) self.assertEqual(tester.result, OK) self.assertEqual(self.dialog.return_code, OK) @unittest.skipIf(USING_QT, "Can't change OK label in Qt") @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_renamed_ok(self): self.dialog.ok_label = "Sure" # test that OK works as expected if renamed tester = ModalDialogTester(self.dialog.open) tester.open_and_wait(when_opened=lambda x: x.click_widget("Sure")) self.assertEqual(tester.result, OK) self.assertEqual(self.dialog.return_code, OK) @unittest.skipIf( is_pyqt5, "Message dialog click tests don't work on pyqt5." ) @unittest.skipIf( is_pyqt4_linux, "Message dialog click tests don't work reliably on linux. Issue #282.", ) @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") def test_parent(self): # test that lifecycle works with a parent parent = Window() self.dialog.parent = parent.control parent.open() tester = ModalDialogTester(self.dialog.open) tester.open_and_run(when_opened=lambda x: x.close(accept=True)) with self.event_loop(): parent.close() self.assertEqual(tester.result, OK) self.assertEqual(self.dialog.return_code, OK) @unittest.skipIf(no_gui_test_assistant, "No GuiTestAssistant") @unittest.skipIf(no_modal_dialog_tester, "ModalDialogTester unavailable") class TestMessageDialogHelpers(unittest.TestCase, GuiTestAssistant): def test_information(self): self._check_dialog(information) def test_warning(self): self._check_dialog(warning) def test_error(self): self._check_dialog(error) def _check_dialog(self, helper): message = "message" kwargs = { "title": "Title", "detail": "Detail", "informative": "Informative", } # smoke test, since dialog helper is opaque result = self._open_and_close(helper, message, **kwargs) self.assertIsNone(result) def _open_and_close(self, helper, message, **kwargs): parent = Window() parent.open() def when_opened(x): x.close(accept=True) tester = ModalDialogTester(helper) tester.open_and_wait(when_opened, parent.control, message, **kwargs) parent.close() return tester.result pyface-7.4.0/pyface/tests/test_image_resource.py0000644000076500000240000001001314176222673022752 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 platform import unittest # importlib.resources is new in Python 3.7, and importlib.resources.files is # new in Python 3.9, so for Python < 3.9 we must rely on the 3rd party # importlib_resources package. try: from importlib.resources import files except ImportError: from importlib_resources import files import pyface import pyface.tests from ..image_resource import ImageResource from ..toolkit import toolkit_object is_qt = toolkit_object.toolkit == "qt4" if is_qt: from pyface.qt import qt_api is_pyqt4_windows = ( is_qt and qt_api == "pyqt" and platform.system() == "Windows" ) SEARCH_PATH = os.fspath(files("pyface") / "images") IMAGE_PATH = os.fspath(files("pyface.tests") / "images" / "core.png") class TestImageResource(unittest.TestCase): def setUp(self): # clear cached "not found" image ImageResource._image_not_found = None def test_create_image(self): image_resource = ImageResource("core") image = image_resource.create_image() self.assertIsNotNone(image) self.assertEqual(image_resource.absolute_path, IMAGE_PATH) def test_create_image_again(self): image_resource = ImageResource("core") image = image_resource.create_image() self.assertIsNotNone(image) self.assertEqual(image_resource.absolute_path, IMAGE_PATH) def test_create_image_search_path(self): image_resource = ImageResource("splash", [SEARCH_PATH]) self.assertEqual( image_resource.search_path, [SEARCH_PATH, pyface.tests] ) image = image_resource.create_image() self.assertIsNotNone(image) self.assertEqual( image_resource.absolute_path, os.path.join(SEARCH_PATH, "splash.png"), ) def test_create_image_search_path_string(self): image_resource = ImageResource("splash", SEARCH_PATH) self.assertEqual( image_resource.search_path, [SEARCH_PATH, pyface.tests] ) image = image_resource.create_image() self.assertIsNotNone(image) self.assertEqual( image_resource.absolute_path, os.path.join(SEARCH_PATH, "splash.png"), ) def test_create_image_missing(self): image_resource = ImageResource("doesnt_exist.png") image = image_resource.create_image() self.assertIsNotNone(image) self.assertIsNotNone(image_resource._image_not_found) def test_create_bitmap(self): image_resource = ImageResource("core.png") image = image_resource.create_bitmap() self.assertIsNotNone(image) self.assertEqual(image_resource.absolute_path, IMAGE_PATH) def test_create_icon(self): image_resource = ImageResource("core.png") image = image_resource.create_icon() self.assertIsNotNone(image) self.assertEqual(image_resource.absolute_path, IMAGE_PATH) def test_image_size(self): image_resource = ImageResource("core") image = image_resource.create_image() size = image_resource.image_size(image) self.assertEqual(image_resource._ref.filename, IMAGE_PATH) self.assertEqual(size, (64, 64)) @unittest.skipIf( is_pyqt4_windows, "QPixmap bug returns (0, 0). Issue #301." ) # noqa def test_image_size_search_path(self): image_resource = ImageResource("splash", [SEARCH_PATH]) image = image_resource.create_image() size = image_resource.image_size(image) self.assertEqual( image_resource.absolute_path, os.path.join(SEARCH_PATH, "splash.png"), ) self.assertEqual(size, (601, 203)) pyface-7.4.0/pyface/tests/test_system_metrics.py0000644000076500000240000000175314176222673023046 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 ..system_metrics import SystemMetrics class TestSystemMetrics(unittest.TestCase): def setUp(self): self.metrics = SystemMetrics() def test_width(self): width = self.metrics.screen_width self.assertGreaterEqual(width, 0) def test_height(self): height = self.metrics.screen_height self.assertGreaterEqual(height, 0) def test_background_color(self): color = self.metrics.dialog_background_color self.assertIn(len(color), [3, 4]) self.assertTrue(all(0 <= channel <= 1 for channel in color)) pyface-7.4.0/pyface/tests/test_directory_dialog.py0000644000076500000240000000523014176222673023311 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 from ..directory_dialog import DirectoryDialog from ..toolkit import toolkit_object GuiTestAssistant = toolkit_object("util.gui_test_assistant:GuiTestAssistant") no_gui_test_assistant = GuiTestAssistant.__name__ == "Unimplemented" ModalDialogTester = toolkit_object( "util.modal_dialog_tester:ModalDialogTester" ) no_modal_dialog_tester = ModalDialogTester.__name__ == "Unimplemented" @unittest.skipIf(no_gui_test_assistant, "No GuiTestAssistant") class TestDirectoryDialog(unittest.TestCase, GuiTestAssistant): def setUp(self): GuiTestAssistant.setUp(self) self.dialog = DirectoryDialog() def tearDown(self): if self.dialog.control is not None: with self.delete_widget(self.dialog.control): self.dialog.destroy() del self.dialog GuiTestAssistant.tearDown(self) def test_create(self): # test that creation and destruction works as expected with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.destroy() def test_destroy(self): # test that destroy works even when no control with self.event_loop(): self.dialog.destroy() def test_close(self): # test that close works with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.close() def test_default_path(self): # test that default path works self.dialog.default_path = os.path.join("images", "core.png") with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.close() def test_no_new_directory(self): # test that block on new directories works self.dialog.new_directory = False with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.close() def test_message(self): # test that message setting works self.dialog.message = "Select a directory" with self.event_loop(): self.dialog._create() with self.event_loop(): self.dialog.close() # XXX would be nice to actually test with an open dialog, but not right now pyface-7.4.0/pyface/tests/test_beep.py0000644000076500000240000000111114176222673020673 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 ..beep import beep class TestBeep(unittest.TestCase): def test_beep(self): # does it call without error - the best we can do beep() pyface-7.4.0/pyface/tests/test_ui_traits.py0000644000076500000240000006457214176222673022007 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 # importlib.resources is new in Python 3.7, and importlib.resources.files is # new in Python 3.9, so for Python < 3.9 we must rely on the 3rd party # importlib_resources package. try: from importlib.resources import files except ImportError: from importlib_resources import files try: import PIL.Image except ImportError: PIL = None from traits.api import DefaultValue, HasTraits, TraitError from traits.testing.optional_dependencies import numpy as np, requires_numpy from traits.testing.api import UnittestTools from ..color import Color from ..font import Font from ..image_resource import ImageResource from ..ui_traits import ( Border, HasBorder, HasMargin, Image, Margin, PyfaceColor, PyfaceFont, image_resource_cache, image_bitmap_cache, ) IMAGE_PATH = os.fspath(files("pyface.tests") / "images" / "core.png") class ImageClass(HasTraits): image = Image class ColorClass(HasTraits): color = PyfaceColor() class FontClass(HasTraits): font = PyfaceFont() class HasMarginClass(HasTraits): margin = HasMargin class HasBorderClass(HasTraits): border = HasBorder class TestImageTrait(unittest.TestCase, UnittestTools): def setUp(self): # clear all cached images image_resource_cache.clear() image_bitmap_cache.clear() # clear cached "not found" image ImageResource._image_not_found = None def test_defaults(self): image_class = ImageClass() self.assertIsNone(image_class.image) def test_init_local_image(self): from pyface.image_resource import ImageResource image_class = ImageClass(image=ImageResource("core.png")) self.assertIsInstance(image_class.image, ImageResource) self.assertEqual(image_class.image.name, "core.png") self.assertEqual( image_class.image.absolute_path, os.path.abspath(IMAGE_PATH) ) def test_init_pyface_image(self): from pyface.image_resource import ImageResource image_class = ImageClass(image="about") image_class.image.create_image() self.assertIsInstance(image_class.image, ImageResource) self.assertEqual(image_class.image.name, "about") self.assertIsNone(image_class.image._image_not_found) self.assertIsNotNone(image_class.image._ref.data) def test_init_pyface_image_library(self): from pyface.image_resource import ImageResource image_class = ImageClass(image="@icons:dialog-warning") self.assertIsInstance(image_class.image, ImageResource) self.assertEqual(image_class.image.name, "dialog-warning.png") self.assertIsNone(image_class.image._image_not_found) self.assertEqual( image_class.image._ref.file_name, "dialog-warning.png" ) self.assertEqual(image_class.image._ref.volume_name, "icons") @requires_numpy def test_init_array_image(self): from pyface.array_image import ArrayImage data = np.full((32, 64, 4), 0xee, dtype='uint8') image = ArrayImage(data) image_class = ImageClass(image=image) self.assertIsInstance(image_class.image, ArrayImage) self.assertTrue((image_class.image.data == data).all()) @unittest.skipIf(PIL is None, "PIL/Pillow is not available") def test_init_pil_image(self): from pyface.pil_image import PILImage pil_image = PIL.Image.open(IMAGE_PATH) image = PILImage(pil_image) image_class = ImageClass(image=image) self.assertIsInstance(image_class.image, PILImage) class TestMargin(unittest.TestCase): def test_defaults(self): margin = Margin() self.assertEqual(margin.top, 0) self.assertEqual(margin.bottom, 0) self.assertEqual(margin.left, 0) self.assertEqual(margin.right, 0) def test_init_one_arg(self): margin = Margin(4) self.assertEqual(margin.top, 4) self.assertEqual(margin.bottom, 4) self.assertEqual(margin.left, 4) self.assertEqual(margin.right, 4) def test_init_two_args(self): margin = Margin(4, 2) self.assertEqual(margin.top, 2) self.assertEqual(margin.bottom, 2) self.assertEqual(margin.left, 4) self.assertEqual(margin.right, 4) def test_init_four_args(self): margin = Margin(4, 2, 3, 1) self.assertEqual(margin.top, 3) self.assertEqual(margin.bottom, 1) self.assertEqual(margin.left, 4) self.assertEqual(margin.right, 2) class TestPyfaceColor(unittest.TestCase): def test_init(self): trait = PyfaceColor() self.assertEqual(trait.default_value, (Color, (), {})) self.assertEqual( trait.default_value_type, DefaultValue.callable_and_args, ) def test_init_name(self): trait = PyfaceColor("rebeccapurple") self.assertEqual( trait.default_value, (Color, (), {'rgba': (0.4, 0.2, 0.6, 1.0)}) ) def test_init_hex(self): trait = PyfaceColor("#663399ff") self.assertEqual( trait.default_value, (Color, (), {'rgba': (0.4, 0.2, 0.6, 1.0)}) ) def test_init_color(self): trait = PyfaceColor(Color(rgba=(0.4, 0.2, 0.6, 1.0))) self.assertEqual( trait.default_value, (Color, (), {'rgba': (0.4, 0.2, 0.6, 1.0)}) ) def test_init_tuple(self): trait = PyfaceColor((0.4, 0.2, 0.6, 1.0)) self.assertEqual( trait.default_value, (Color, (), {'rgba': (0.4, 0.2, 0.6, 1.0)}) ) def test_init_list(self): trait = PyfaceColor([0.4, 0.2, 0.6, 1.0]) self.assertEqual( trait.default_value, (Color, (), {'rgba': (0.4, 0.2, 0.6, 1.0)}) ) @requires_numpy def test_init_array(self): trait = PyfaceColor(np.array([0.4, 0.2, 0.6, 1.0])) self.assertEqual( trait.default_value, (Color, (), {'rgba': (0.4, 0.2, 0.6, 1.0)}) ) @requires_numpy def test_init_array_structured_dtype(self): """ Test if "typical" RGBA structured array value works. """ arr = np.array( [(0.4, 0.2, 0.6, 1.0)], dtype=np.dtype([ ('red', float), ('green', float), ('blue', float), ('alpha', float), ]), ) trait = PyfaceColor(arr[0]) self.assertEqual( trait.default_value, (Color, (), {'rgba': (0.4, 0.2, 0.6, 1.0)}) ) def test_init_invalid(self): with self.assertRaises(TraitError): PyfaceColor((0.4, 0.2)) def test_validate_color(self): color = Color(rgba=(0.4, 0.2, 0.6, 1.0)) trait = PyfaceColor() validated = trait.validate(None, None, color) self.assertIs( validated, color ) def test_validate_name(self): color = Color(rgba=(0.4, 0.2, 0.6, 1.0)) trait = PyfaceColor() validated = trait.validate(None, None, "rebeccapurple") self.assertEqual( validated, color ) def test_validate_hex(self): color = Color(rgba=(0.4, 0.2, 0.6, 1.0)) trait = PyfaceColor() validated = trait.validate(None, None, "#663399ff") self.assertEqual( validated, color ) def test_validate_tuple(self): color = Color(rgba=(0.4, 0.2, 0.6, 0.8)) trait = PyfaceColor() validated = trait.validate(None, None, (0.4, 0.2, 0.6, 0.8)) self.assertEqual( validated, color ) def test_validate_list(self): color = Color(rgba=(0.4, 0.2, 0.6, 0.8)) trait = PyfaceColor() validated = trait.validate(None, None, [0.4, 0.2, 0.6, 0.8]) self.assertEqual( validated, color ) def test_validate_rgb_list(self): color = Color(rgba=(0.4, 0.2, 0.6, 1.0)) trait = PyfaceColor() validated = trait.validate(None, None, [0.4, 0.2, 0.6]) self.assertEqual( validated, color ) def test_validate_bad_string(self): trait = PyfaceColor() with self.assertRaises(TraitError): trait.validate(None, None, "not a color") def test_validate_bad_object(self): trait = PyfaceColor() with self.assertRaises(TraitError): trait.validate(None, None, object()) def test_info(self): trait = PyfaceColor() self.assertIsInstance(trait.info(), str) def test_default_trait(self): color_class = ColorClass() self.assertEqual(color_class.color, Color()) def test_set_color(self): color = Color(rgba=(0.4, 0.2, 0.6, 1.0)) color_class = ColorClass(color=color) self.assertIs(color_class.color, color) def test_set_name(self): color = Color(rgba=(0.4, 0.2, 0.6, 1.0)) color_class = ColorClass(color="rebeccapurple") self.assertEqual(color_class.color, color) def test_set_hex(self): color = Color(rgba=(0.4, 0.2, 0.6, 1.0)) color_class = ColorClass(color="#663399ff") self.assertEqual(color_class.color, color) def test_set_tuple(self): color = Color(rgba=(0.4, 0.2, 0.6, 1.0)) color_class = ColorClass(color=(0.4, 0.2, 0.6, 1.0)) self.assertEqual(color_class.color, color) def test_set_list(self): color = Color(rgba=(0.4, 0.2, 0.6, 1.0)) color_class = ColorClass(color=[0.4, 0.2, 0.6, 1.0]) self.assertEqual(color_class.color, color) @requires_numpy def test_set_array(self): color = Color(rgba=(0.4, 0.2, 0.6, 1.0)) color_class = ColorClass(color=np.array([0.4, 0.2, 0.6, 1.0])) self.assertEqual(color_class.color, color) @requires_numpy def test_set_structured_dtype(self): color = Color(rgba=(0.4, 0.2, 0.6, 1.0)) arr = np.array( [(0.4, 0.2, 0.6, 1.0)], dtype=np.dtype([ ('red', float), ('green', float), ('blue', float), ('alpha', float), ]), ) color_class = ColorClass(color=arr[0]) self.assertEqual(color_class.color, color) class TestPyfaceFont(unittest.TestCase): def test_init(self): trait = PyfaceFont() self.assertEqual(trait.default_value, (Font, (), {})) self.assertEqual( trait.default_value_type, DefaultValue.callable_and_args, ) def test_init_empty_string(self): trait = PyfaceFont("") self.assertEqual( trait.default_value, ( Font, (), { 'family': ["default"], 'size': 12.0, 'weight': "normal", 'stretch': 100, 'style': "normal", 'variants': set(), 'decorations': set(), }, ) ) def test_init_typical_string(self): trait = PyfaceFont( "10 pt bold condensed italic underline Helvetica sans-serif") self.assertEqual( trait.default_value, ( Font, (), { 'family': ["helvetica", "sans-serif"], 'size': 10.0, 'weight': "bold", 'stretch': 75.0, 'style': "italic", 'variants': set(), 'decorations': {"underline"}, }, ) ) def test_init_font(self): font = Font( family=["helvetica", "sans-serif"], size=10.0, weight="bold", stretch=75.0, style="italic", variants=set(), decorations={"underline"}, ) trait = PyfaceFont(font) self.assertEqual( trait.default_value, ( Font, (), { 'family': ["helvetica", "sans-serif"], 'size': 10.0, 'weight': "bold", 'stretch': 75.0, 'style': "italic", 'variants': set(), 'decorations': {"underline"}, }, ) ) def test_init_invalid(self): with self.assertRaises(ValueError): PyfaceFont(0) def test_set_empty_string(self): font_class = FontClass() font_class.font = "" self.assertFontEqual(font_class.font, Font()) def test_set_typical_string(self): font_class = FontClass() font_class.font = "10 pt bold condensed italic underline Helvetica sans-serif" # noqa: E501 self.assertFontEqual( font_class.font, Font( family=["helvetica", "sans-serif"], size=10.0, weight="bold", stretch=75.0, style="italic", variants=set(), decorations={"underline"}, ), ) def test_set_font(self): font_class = FontClass() font = Font( family=["helvetica", "sans-serif"], size=10.0, weight="bold", stretch=75.0, style="italic", variants=set(), decorations={"underline"}, ) font_class.font = font self.assertIs(font_class.font, font) def test_set_failure(self): font_class = FontClass() with self.assertRaises(TraitError): font_class.font = None def assertFontEqual(self, font1, font2): state1 = font1.trait_get(transient=lambda x: not x) state2 = font2.trait_get(transient=lambda x: not x) self.assertEqual(state1, state2) class TestHasMargin(unittest.TestCase, UnittestTools): def test_defaults(self): has_margin = HasMarginClass() margin = has_margin.margin self.assertEqual(margin.top, 0) self.assertEqual(margin.bottom, 0) self.assertEqual(margin.left, 0) self.assertEqual(margin.right, 0) def test_unspecified_default(self): trait = HasMargin() trait.default_value_type = DefaultValue.unspecified (dvt, dv) = trait.get_default_value() self.assertEqual(dvt, DefaultValue.callable_and_args) self.assertEqual( dv, (Margin, (), {"top": 0, "bottom": 0, "left": 0, "right": 0}) ) def test_default_int(self): class HasMarginClass(HasTraits): margin = HasMargin(4) has_margin = HasMarginClass() margin = has_margin.margin self.assertEqual(margin.top, 4) self.assertEqual(margin.bottom, 4) self.assertEqual(margin.left, 4) self.assertEqual(margin.right, 4) def test_default_one_tuple(self): class HasMarginClass(HasTraits): margin = HasMargin((4,)) has_margin = HasMarginClass() margin = has_margin.margin self.assertEqual(margin.top, 4) self.assertEqual(margin.bottom, 4) self.assertEqual(margin.left, 4) self.assertEqual(margin.right, 4) def test_default_two_tuple(self): class HasMarginClass(HasTraits): margin = HasMargin((4, 2)) has_margin = HasMarginClass() margin = has_margin.margin self.assertEqual(margin.top, 2) self.assertEqual(margin.bottom, 2) self.assertEqual(margin.left, 4) self.assertEqual(margin.right, 4) def test_default_four_tuple(self): class HasMarginClass(HasTraits): margin = HasMargin((4, 2, 3, 1)) has_margin = HasMarginClass() margin = has_margin.margin self.assertEqual(margin.top, 3) self.assertEqual(margin.bottom, 1) self.assertEqual(margin.left, 4) self.assertEqual(margin.right, 2) def test_default_margin(self): m = Margin(left=4, right=2, top=3, bottom=1) class HasMarginClass(HasTraits): margin = HasMargin(m) has_margin = HasMarginClass() margin = has_margin.margin self.assertEqual(margin.top, 3) self.assertEqual(margin.bottom, 1) self.assertEqual(margin.left, 4) self.assertEqual(margin.right, 2) def test_init_int(self): has_margin = HasMarginClass(margin=4) margin = has_margin.margin self.assertEqual(margin.top, 4) self.assertEqual(margin.bottom, 4) self.assertEqual(margin.left, 4) self.assertEqual(margin.right, 4) def test_init_one_tuple(self): has_margin = HasMarginClass(margin=(4,)) margin = has_margin.margin self.assertEqual(margin.top, 4) self.assertEqual(margin.bottom, 4) self.assertEqual(margin.left, 4) self.assertEqual(margin.right, 4) def test_init_two_tuple(self): has_margin = HasMarginClass(margin=(4, 2)) margin = has_margin.margin self.assertEqual(margin.top, 2) self.assertEqual(margin.bottom, 2) self.assertEqual(margin.left, 4) self.assertEqual(margin.right, 4) def test_init_four_tuple(self): has_margin = HasMarginClass(margin=(4, 2, 3, 1)) margin = has_margin.margin self.assertEqual(margin.top, 3) self.assertEqual(margin.bottom, 1) self.assertEqual(margin.left, 4) self.assertEqual(margin.right, 2) def test_init_margin(self): margin = Margin() has_margin = HasMarginClass(margin=margin) self.assertEqual(has_margin.margin, margin) def test_set_int(self): has_margin = HasMarginClass() with self.assertTraitChanges(has_margin, "margin", 1): has_margin.margin = 4 margin = has_margin.margin self.assertEqual(margin.top, 4) self.assertEqual(margin.bottom, 4) self.assertEqual(margin.left, 4) self.assertEqual(margin.right, 4) def test_set_one_tuple(self): has_margin = HasMarginClass() with self.assertTraitChanges(has_margin, "margin", 1): has_margin.margin = (4,) margin = has_margin.margin self.assertEqual(margin.top, 4) self.assertEqual(margin.bottom, 4) self.assertEqual(margin.left, 4) self.assertEqual(margin.right, 4) def test_set_two_tuple(self): has_margin = HasMarginClass() with self.assertTraitChanges(has_margin, "margin", 1): has_margin.margin = (4, 2) margin = has_margin.margin self.assertEqual(margin.top, 2) self.assertEqual(margin.bottom, 2) self.assertEqual(margin.left, 4) self.assertEqual(margin.right, 4) def test_set_four_tuple(self): has_margin = HasMarginClass() with self.assertTraitChanges(has_margin, "margin", 1): has_margin.margin = (4, 2, 3, 1) margin = has_margin.margin self.assertEqual(margin.top, 3) self.assertEqual(margin.bottom, 1) self.assertEqual(margin.left, 4) self.assertEqual(margin.right, 2) def test_set_margin(self): margin = Margin() has_margin = HasMarginClass() with self.assertTraitChanges(has_margin, "margin", 1): has_margin.margin = margin self.assertEqual(has_margin.margin, margin) def test_set_invalid(self): has_margin = HasMarginClass() with self.assertRaises(TraitError): has_margin.margin = (1, 2, 3) class TestBorder(unittest.TestCase): def test_defaults(self): border = Border() self.assertEqual(border.top, 0) self.assertEqual(border.bottom, 0) self.assertEqual(border.left, 0) self.assertEqual(border.right, 0) def test_init_one_arg(self): border = Border(4) self.assertEqual(border.top, 4) self.assertEqual(border.bottom, 4) self.assertEqual(border.left, 4) self.assertEqual(border.right, 4) def test_init_two_args(self): border = Border(4, 2) self.assertEqual(border.top, 2) self.assertEqual(border.bottom, 2) self.assertEqual(border.left, 4) self.assertEqual(border.right, 4) def test_init_four_args(self): border = Border(4, 2, 3, 1) self.assertEqual(border.top, 3) self.assertEqual(border.bottom, 1) self.assertEqual(border.left, 4) self.assertEqual(border.right, 2) class TestHasBorder(unittest.TestCase, UnittestTools): def test_defaults(self): has_border = HasBorderClass() border = has_border.border self.assertEqual(border.top, 0) self.assertEqual(border.bottom, 0) self.assertEqual(border.left, 0) self.assertEqual(border.right, 0) def test_unspecified_default(self): trait = HasBorder() trait.default_value_type = DefaultValue.unspecified (dvt, dv) = trait.get_default_value() self.assertEqual(dvt, DefaultValue.callable_and_args) self.assertEqual( dv, (Border, (), {"top": 0, "bottom": 0, "left": 0, "right": 0}) ) def test_default_int(self): class HasBorderClass(HasTraits): border = HasBorder(4) has_border = HasBorderClass() border = has_border.border self.assertEqual(border.top, 4) self.assertEqual(border.bottom, 4) self.assertEqual(border.left, 4) self.assertEqual(border.right, 4) def test_default_one_tuple(self): class HasBorderClass(HasTraits): border = HasBorder((4,)) has_border = HasBorderClass() border = has_border.border self.assertEqual(border.top, 4) self.assertEqual(border.bottom, 4) self.assertEqual(border.left, 4) self.assertEqual(border.right, 4) def test_default_two_tuple(self): class HasBorderClass(HasTraits): border = HasBorder((4, 2)) has_border = HasBorderClass() border = has_border.border self.assertEqual(border.top, 2) self.assertEqual(border.bottom, 2) self.assertEqual(border.left, 4) self.assertEqual(border.right, 4) def test_default_four_tuple(self): class HasBorderClass(HasTraits): border = HasBorder((4, 2, 3, 1)) has_border = HasBorderClass() border = has_border.border self.assertEqual(border.top, 3) self.assertEqual(border.bottom, 1) self.assertEqual(border.left, 4) self.assertEqual(border.right, 2) def test_default_border(self): m = Margin(left=4, right=2, top=3, bottom=1) class HasBorderClass(HasTraits): border = HasBorder(m) has_border = HasBorderClass() border = has_border.border self.assertEqual(border.top, 3) self.assertEqual(border.bottom, 1) self.assertEqual(border.left, 4) self.assertEqual(border.right, 2) def test_init_int(self): has_border = HasBorderClass(border=4) border = has_border.border self.assertEqual(border.top, 4) self.assertEqual(border.bottom, 4) self.assertEqual(border.left, 4) self.assertEqual(border.right, 4) def test_init_one_tuple(self): has_border = HasBorderClass(border=(4,)) border = has_border.border self.assertEqual(border.top, 4) self.assertEqual(border.bottom, 4) self.assertEqual(border.left, 4) self.assertEqual(border.right, 4) def test_init_two_tuple(self): has_border = HasBorderClass(border=(4, 2)) border = has_border.border self.assertEqual(border.top, 2) self.assertEqual(border.bottom, 2) self.assertEqual(border.left, 4) self.assertEqual(border.right, 4) def test_init_four_tuple(self): has_border = HasBorderClass(border=(4, 2, 3, 1)) border = has_border.border self.assertEqual(border.top, 3) self.assertEqual(border.bottom, 1) self.assertEqual(border.left, 4) self.assertEqual(border.right, 2) def test_init_border(self): border = Border() has_border = HasBorderClass(border=border) self.assertEqual(has_border.border, border) def test_set_int(self): has_border = HasBorderClass() with self.assertTraitChanges(has_border, "border", 1): has_border.border = 4 border = has_border.border self.assertEqual(border.top, 4) self.assertEqual(border.bottom, 4) self.assertEqual(border.left, 4) self.assertEqual(border.right, 4) def test_set_one_tuple(self): has_border = HasBorderClass() with self.assertTraitChanges(has_border, "border", 1): has_border.border = (4,) border = has_border.border self.assertEqual(border.top, 4) self.assertEqual(border.bottom, 4) self.assertEqual(border.left, 4) self.assertEqual(border.right, 4) def test_set_two_tuple(self): has_border = HasBorderClass() with self.assertTraitChanges(has_border, "border", 1): has_border.border = (4, 2) border = has_border.border self.assertEqual(border.top, 2) self.assertEqual(border.bottom, 2) self.assertEqual(border.left, 4) self.assertEqual(border.right, 4) def test_set_four_tuple(self): has_border = HasBorderClass() with self.assertTraitChanges(has_border, "border", 1): has_border.border = (4, 2, 3, 1) border = has_border.border self.assertEqual(border.top, 3) self.assertEqual(border.bottom, 1) self.assertEqual(border.left, 4) self.assertEqual(border.right, 2) def test_set_border(self): border = Border() has_border = HasBorderClass() with self.assertTraitChanges(has_border, "border", 1): has_border.border = border self.assertEqual(has_border.border, border) def test_set_invalid(self): has_border = HasBorderClass() with self.assertRaises(TraitError): has_border.border = (1, 2, 3) pyface-7.4.0/pyface/tests/test_pil_image.py0000644000076500000240000000336414176222673021722 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 logging import unittest # importlib.resources is new in Python 3.7, and importlib.resources.files is # new in Python 3.9, so for Python < 3.9 we must rely on the 3rd party # importlib_resources package. try: from importlib.resources import files except ImportError: from importlib_resources import files from pyface.util._optional_dependencies import optional_import Image = None with optional_import( "pillow", msg="PILImage not available due to missing pillow.", logger=logging.getLogger(__name__)): from PIL import Image from ..pil_image import PILImage IMAGE_PATH = os.fspath(files("pyface.tests") / "images" / "core.png") @unittest.skipIf(Image is None, "Pillow not available") class TestPILImage(unittest.TestCase): def setUp(self): self.pil_image = Image.open(IMAGE_PATH) def test_create_image(self): image = PILImage(self.pil_image) toolkit_image = image.create_image() self.assertIsNotNone(toolkit_image) self.assertEqual(image.image, self.pil_image) def test_create_bitmap(self): image = PILImage(self.pil_image) bitmap = image.create_bitmap() self.assertIsNotNone(bitmap) def test_create_icon(self): image = PILImage(self.pil_image) icon = image.create_icon() self.assertIsNotNone(icon) pyface-7.4.0/pyface/tests/test_python_editor.py0000644000076500000240000000725514176222673022666 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 from ..python_editor import PythonEditor from ..toolkit import toolkit_object from ..window import Window GuiTestAssistant = toolkit_object("util.gui_test_assistant:GuiTestAssistant") no_gui_test_assistant = GuiTestAssistant.__name__ == "Unimplemented" PYTHON_SCRIPT = os.path.join( os.path.dirname(__file__), "python_shell_script.py" ) @unittest.skipIf(no_gui_test_assistant, "No GuiTestAssistant") class TestPythonEditor(unittest.TestCase, GuiTestAssistant): def setUp(self): GuiTestAssistant.setUp(self) self.window = Window() self.window._create() def tearDown(self): if self.widget.control is not None: with self.delete_widget(self.widget.control): self.widget.destroy() if self.window.control is not None: with self.delete_widget(self.window.control): self.window.destroy() del self.widget del self.window GuiTestAssistant.tearDown(self) def test_lifecycle(self): # test that destroy works with self.event_loop(): with self.assertWarns(PendingDeprecationWarning): self.widget = PythonEditor(self.window.control) self.assertIsNotNone(self.widget.control) self.assertFalse(self.widget.dirty) with self.event_loop(): self.widget.destroy() def test_two_stage_create(self): # test that create and destroy work self.widget = PythonEditor(create=False) self.assertIsNone(self.widget.control) with self.event_loop(): self.widget.parent = self.window.control self.widget.create() self.assertIsNotNone(self.widget.control) self.assertFalse(self.widget.dirty) with self.event_loop(): self.widget.destroy() def test_show_line_numbers(self): # test that destroy works with self.event_loop(): self.widget = PythonEditor( parent=self.window.control, show_line_numbers=False, create=False, ) self.widget.create() with self.event_loop(): self.widget.show_line_numbers = True with self.event_loop(): self.widget.show_line_numbers = False with self.event_loop(): self.widget.destroy() def test_load(self): # test that destroy works with self.event_loop(): self.widget = PythonEditor( parent=self.window.control, create=False, ) self.widget.create() with self.assertTraitChanges(self.widget, "changed", count=1): with self.event_loop(): self.widget.path = PYTHON_SCRIPT self.assertFalse(self.widget.dirty) with self.event_loop(): self.widget.destroy() def test_select_line(self): # test that destroy works with self.event_loop(): self.widget = PythonEditor( parent=self.window.control, path=PYTHON_SCRIPT, create=False, ) self.widget.create() with self.event_loop(): self.widget.select_line(3) with self.event_loop(): self.widget.destroy() pyface-7.4.0/pyface/font_dialog.py0000644000076500000240000000224214176222673020052 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 implementation of a dialog that allows the user to select a font. """ from .constant import OK from .toolkit import toolkit_object FontDialog = toolkit_object("font_dialog:FontDialog") def get_font(parent, font): """ Convenience function that displays a font dialog. Parameters ---------- parent : toolkit control The parent toolkit control for the modal dialog. font : Font or font description The initial Font object or string describing the font. Returns ------- font : Font or None The selected font, or None if the user made no selection. """ dialog = FontDialog(parent=parent, font=font) result = dialog.open() if result == OK: return dialog.font else: return None pyface-7.4.0/pyface/wizard/0000755000076500000240000000000014176460551016512 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/wizard/chained_wizard.py0000644000076500000240000000470214176222673022043 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 wizard model that can be chained with other wizards. """ from traits.api import Instance, observe from .i_wizard import IWizard from .wizard import Wizard class ChainedWizard(Wizard): """ A wizard model that can be chained with other wizards. """ # 'ChainedWizard' interface -------------------------------------------- # The wizard following this wizard in the chain. next_wizard = Instance(IWizard) # ------------------------------------------------------------------------ # 'ChainedWizard' interface. # ------------------------------------------------------------------------ # Trait handlers. -----------------------------------------------------# def _controller_default(self): """ Provide a default controller. """ from .chained_wizard_controller import ChainedWizardController return ChainedWizardController() # Trait event handlers. ------------------------------------------------ # Static ---- @observe("next_wizard") def _reset_next_controller_and_update(self, event): """ Handle the next wizard being changed. """ if event.new is not None: self.controller.next_controller = event.new.controller if self.control is not None: # FIXME: Do we need to call _create_buttons? Buttons would have # added when the main dialog area was created (for the first # wizard), and calling update should update the state of these # buttons. Do we need to check if buttons are already present in # the dialog area? What is use case for calling _create_buttons? # self._create_buttons(self.control) self._update() @observe("controller") def _reset_traits_on_controller_and_update(self, event): """ handle the controller being changed. """ if event.new is not None and self.next_wizard is not None: self.controller.next_controller = self.next_wizard.controller if self.control is not None: self._update() return pyface-7.4.0/pyface/wizard/simple_wizard.py0000644000076500000240000000102514176222673021734 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ SimpleWizard is deprecated. Use Wizard instead. """ from .wizard import Wizard class SimpleWizard(Wizard): pass pyface-7.4.0/pyface/wizard/i_wizard_page.py0000644000076500000240000000473014176222673021675 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 interface for a page in a wizard. """ from traits.api import Bool, HasTraits, Interface, Str, Tuple class IWizardPage(Interface): """ The interface for a page in a wizard. """ # 'IWizardPage' interface ---------------------------------------------# # The unique Id of the page within the wizard. id = Str() # The Id of the next page. next_id = Str() # Set if this is the last page of the wizard. It can be ignored for # simple linear wizards. last_page = Bool(False) # Is the page complete (i.e. should the 'Next' button be enabled)? complete = Bool(False) # The page heading. heading = Str() # The page sub-heading. subheading = Str() # The size of the page. size = Tuple() # ------------------------------------------------------------------------ # 'IWizardPage' interface. # ------------------------------------------------------------------------ def create_page(self, parent): """ Creates the wizard page. """ def dispose_page(self): """ Disposes the wizard page. Subclasses are expected to override this method if they need to dispose of the contents of a page. """ # ------------------------------------------------------------------------ # Protected 'IWizardPage' interface. # ------------------------------------------------------------------------ def _create_page_content(self, parent): """ Creates the actual page content. """ class MWizardPage(HasTraits): """ The mixin class that contains common code for toolkit specific implementations of the IWizardPage interface. Implements: dispose_page() """ # ------------------------------------------------------------------------ # 'IWizardPage' interface. # ------------------------------------------------------------------------ def dispose_page(self): """ Disposes the wizard page. Subclasses are expected to override this method if they need to dispose of the contents of a page. """ pass pyface-7.4.0/pyface/wizard/wizard_page.py0000644000076500000240000000113314176222673021357 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 implementation of a page in a wizard. """ # Import the toolkit specific version. from pyface.toolkit import toolkit_object WizardPage = toolkit_object("wizard.wizard_page:WizardPage") pyface-7.4.0/pyface/wizard/__init__.py0000644000076500000240000000062714176222673020631 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! pyface-7.4.0/pyface/wizard/simple_wizard_controller.py0000644000076500000240000000112214176222673024175 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ SimpleWizardController is deprecated. Use WizardController instead. """ from .wizard_controller import WizardController class SimpleWizardController(WizardController): pass pyface-7.4.0/pyface/wizard/chained_wizard_controller.py0000644000076500000240000001437114176222673024311 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 wizard controller that can be chained with others. """ from traits.api import Instance, observe from .i_wizard_controller import IWizardController from .wizard_controller import WizardController class ChainedWizardController(WizardController): """ A wizard controller that can be chained with others. """ # 'ChainedWizardController' interface ---------------------------------# # The next chained wizard controller. next_controller = Instance(IWizardController) # ------------------------------------------------------------------------ # 'IWizardController' interface. # ------------------------------------------------------------------------ def get_next_page(self, page): """ Returns the next page. """ next_page = None if page in self._pages: if page is not self._pages[-1]: index = self._pages.index(page) next_page = self._pages[index + 1] else: if self.next_controller is not None: next_page = self.next_controller.get_first_page() else: if self.next_controller is not None: next_page = self.next_controller.get_next_page(page) return next_page def get_previous_page(self, page): """ Returns the previous page. """ if page in self._pages: index = self._pages.index(page) previous_page = self._pages[index - 1] else: if self.next_controller is not None: if self.next_controller.is_first_page(page): previous_page = self._pages[-1] else: previous_page = self.next_controller.get_previous_page( page ) else: previous_page = None return previous_page def is_first_page(self, page): """ Is the page the first page? """ return page is self._pages[0] def is_last_page(self, page): """ Is the page the last page? """ if page in self._pages: # If page is not this controller's last page, then it cannot be # *the* last page. if page is not self._pages[-1]: is_last = False # Otherwise, it is *the* last page if this controller has no next # controller or the next controller has no pages. else: if self.next_controller is None: is_last = True else: is_last = self.next_controller.is_last_page(page) else: if self.next_controller is not None: is_last = self.next_controller.is_last_page(page) elif len(self._pages) > 0: is_last = False else: is_last = True return is_last def dispose_pages(self): """ Dispose the wizard's pages. """ for page in self._pages: page.dispose_page() if self.next_controller is not None: self.next_controller.dispose_pages() return # ------------------------------------------------------------------------ # 'ChainedWizardController' interface. # ------------------------------------------------------------------------ def _get_pages(self): """ Returns the pages in the wizard. """ pages = self._pages[:] if self.next_controller is not None: pages.extend(self.next_controller.pages) return pages def _set_pages(self, pages): """ Sets the pages in the wizard. """ self._pages = pages return # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _update(self): """ Checks the completion status of the controller. """ # The entire wizard is complete when ALL pages are complete. for page in self._pages: if not page.complete: self.complete = False break else: if self.next_controller is not None: # fixme: This is a abstraction leak point, since _update is not # part of the wizard_controller interface! self.next_controller._update() self.complete = self.next_controller.complete else: self.complete = True return # Trait event handlers ------------------------------------------------- # Static ---- @observe("current_page") def _reset_observers_on_current_page_and_update(self, event): """ Called when the current page is changed. """ old, new = event.old, event.new if old is not None: old.observe( self._on_page_complete, "complete", remove=True ) if new is not None: new.observe(self._on_page_complete, "complete") if self.next_controller is not None: self.next_controller.current_page = new self._update() @observe("next_controller") def _reset_observers_on_next_controller_and_update(self, event): """ Called when the next controller is changed. """ old, new = event.old, event.new if old is not None: old.observe( self._on_controller_complete, "complete", remove=True ) if new is not None: new.observe(self._on_controller_complete, "complete") self._update() return # Dynamic ---- def _on_controller_complete(self, event): """ Called when the next controller's complete state changes. """ self._update() def _on_page_complete(self, event): """ Called when the current page is complete. """ self._update() return pyface-7.4.0/pyface/wizard/wizard_controller.py0000644000076500000240000001145014176222673022631 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 wizard controller that has a static list of pages. """ from traits.api import ( Bool, HasTraits, Instance, List, Property, provides, observe ) from .i_wizard_controller import IWizardController from .i_wizard_page import IWizardPage @provides(IWizardController) class WizardController(HasTraits): """ A wizard controller that has a static list of pages. """ # 'IWizardController' interface ---------------------------------------- # The pages under the control of this controller. pages = Property(List(IWizardPage)) # The current page. current_page = Instance(IWizardPage) # Set if the wizard is complete. complete = Bool(False) # Protected 'IWizardController' interface -----------------------------# # Shadow trait for the 'pages' property. _pages = List(IWizardPage) # ------------------------------------------------------------------------ # 'IWizardController' interface. # ------------------------------------------------------------------------ def get_first_page(self): """ Returns the first page. """ if self._pages: return self._pages[0] return None def get_next_page(self, page): """ Returns the next page. """ if page.last_page: pass elif page.next_id: for p in self._pages: if p.id == page.next_id: return p else: index = self._pages.index(page) + 1 if index < len(self._pages): return self._pages[index] return None def get_previous_page(self, page): """ Returns the previous page. """ for p in self._pages: next = self.get_next_page(p) if next is page: return p return None def is_first_page(self, page): """ Is the page the first page? """ return page is self._pages[0] def is_last_page(self, page): """ Is the page the last page? """ if page.last_page: return True if page.next_id: return False return page is self._pages[-1] def dispose_pages(self): """ Dispose the wizard pages. """ for page in self._pages: page.dispose_page() return # ------------------------------------------------------------------------ # 'WizardController' interface. # ------------------------------------------------------------------------ def _get_pages(self): """ Returns the pages in the wizard. """ return self._pages[:] def _set_pages(self, pages): """ Sets the pages in the wizard. """ self._pages = pages # Make sure the current page is valid. # If the current page is None (i.e., the current page has # not been set yet), do not set it here. The current page will # get set when the wizard calls _show_page. if ( self.current_page is not None and self.current_page not in self._pages ): self.current_page = self._pages[0] else: self._update() return # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _update(self): """ Checks the completion status of the controller. """ # The entire wizard is complete when the last page is complete. if self.current_page is None: self.complete = False elif self.is_last_page(self.current_page): self.complete = self.current_page.complete else: self.complete = False return # Trait event handlers ------------------------------------------------- # Static ---- @observe("current_page") def _reset_observers_on_current_page_and_update(self, event): """ Called when the current page is changed. """ old, new = event.old, event.new if old is not None: old.observe( self._on_page_complete, "complete", remove=True ) if new is not None: new.observe(self._on_page_complete, "complete") self._update() return # Dynamic ---- def _on_page_complete(self, event): """ Called when the current page is complete. """ self._update() return pyface-7.4.0/pyface/wizard/i_wizard.py0000644000076500000240000001100014176222673020665 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 interface for all pyface wizards. """ from traits.api import Bool, HasTraits, Instance, List, Str from pyface.i_dialog import IDialog from .i_wizard_controller import IWizardController from .i_wizard_page import IWizardPage class IWizard(IDialog): """ The interface for all pyface wizards. """ # 'IWizard' interface -------------------------------------------------# # The pages in the wizard. pages = List(IWizardPage) # The wizard controller provides the pages displayed in the wizard, and # determines when the wizard is complete etc. controller = Instance(IWizardController) # Should the 'Cancel' button be displayed? show_cancel = Bool(True) # 'IWindow' interface -------------------------------------------------# # The dialog title. title = Str("Wizard") # ------------------------------------------------------------------------ # 'IWizard' interface. # ------------------------------------------------------------------------ def next(self): """ Advance to the next page in the wizard. """ def previous(self): """ Return to the previous page in the wizard. """ class MWizard(HasTraits): """ The mixin class that contains common code for toolkit specific implementations of the IWizard interface. Implements: next(), previous() Reimplements: _create_contents() """ # ------------------------------------------------------------------------ # 'IWizard' interface. # ------------------------------------------------------------------------ def next(self): """ Advance to the next page in the wizard. """ page = self.controller.get_next_page(self.controller.current_page) self._show_page(page) def previous(self): """ Return to the previous page in the wizard. """ page = self.controller.get_previous_page(self.controller.current_page) self._show_page(page) return # ------------------------------------------------------------------------ # Protected 'IWindow' interface. # ------------------------------------------------------------------------ def _create_contents(self, parent): """ Creates the window contents. """ # This creates the dialog and button areas. super()._create_contents(parent) # Wire up the controller. self._initialize_controller(self.controller) # Show the first page. self._show_page(self.controller.get_first_page()) return # ------------------------------------------------------------------------ # Protected MWizard interface. # ------------------------------------------------------------------------ def _show_page(self, page): """ Show the specified page. """ # Set the current page in the controller. # # fixme: Shouldn't this interface be reversed? Maybe calling # 'next_page' on the controller should cause it to set its own current # page? self.controller.current_page = page def _update(self, event): """ Enables/disables buttons depending on the state of the wizard. """ pass # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _initialize_controller(self, controller): """ Initializes the wizard controller. """ controller.observe(self._update, "complete") controller.observe(self._on_current_page_changed, "current_page") return # Trait event handlers ------------------------------------------------- def _on_current_page_changed(self, event): """ Called when the current page is changed. """ if event.old is not None: event.old.observe(self._update, "complete", remove=True) if event.new is not None: event.new.observe(self._update, "complete") self._update(event=None) def _on_closed_changed(self): """ Called when the wizard is closed. """ self.controller.dispose_pages() return pyface-7.4.0/pyface/wizard/api.py0000644000076500000240000000227014176222673017637 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 ``pyface.wizard`` subpackage. - :class:`~.WizardPage` - :class:`~.Wizard` - :class:`~.WizardController` - :class:`~.ChainedWizard` - :class:`~.ChainedWizardController` Interfaces ---------- - :class:`~.IWizardPage` - :class:`~.IWizard` - :class:`~.IWizardController` """ from .i_wizard_page import IWizardPage from .wizard_page import WizardPage from .i_wizard import IWizard from .wizard import Wizard from .i_wizard_controller import IWizardController from .wizard_controller import WizardController from .chained_wizard import ChainedWizard from .chained_wizard_controller import ChainedWizardController # These are deprecated. Use Wizard and WizardController instead. from .simple_wizard import SimpleWizard from .simple_wizard_controller import SimpleWizardController pyface-7.4.0/pyface/wizard/wizard.py0000644000076500000240000000110314176222673020360 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 implementation of a wizard. """ # Import the toolkit specific version. from pyface.toolkit import toolkit_object Wizard = toolkit_object("wizard.wizard:Wizard") pyface-7.4.0/pyface/wizard/i_wizard_controller.py0000644000076500000240000000312214176222673023136 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 interface for all pyface wizard controllers. """ from traits.api import Bool, Interface, Instance, List from .i_wizard_page import IWizardPage class IWizardController(Interface): """ The interface for all pyface wizard controllers. """ # 'IWizardController' interface ---------------------------------------- # The pages under the control of this controller. pages = List(IWizardPage) # The current page. current_page = Instance(IWizardPage) # Set if the wizard complete. complete = Bool(False) # ------------------------------------------------------------------------ # 'IWizardController' interface. # ------------------------------------------------------------------------ def get_first_page(self): """ Returns the first page in the model. """ def get_next_page(self, page): """ Returns the next page. """ def get_previous_page(self, page): """ Returns the previous page. """ def is_first_page(self, page): """ Is the page the first page? """ def is_last_page(self, page): """ Is the page the last page? """ def dispose_pages(self): """ Dispose all the pages. """ pyface-7.4.0/pyface/resource_manager.py0000644000076500000240000000141414176222673021106 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 implementation of a shared resource manager. """ from pyface.resource.api import ResourceManager # Import the toolkit specific version. from .toolkit import toolkit_object PyfaceResourceFactory = toolkit_object( "resource_manager:PyfaceResourceFactory" ) #: A shared instance. resource_manager = ResourceManager(resource_factory=PyfaceResourceFactory()) pyface-7.4.0/pyface/qt/0000755000076500000240000000000014176460550015635 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/qt/QtGui.py0000644000076500000240000000475414176253531017251 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 . import qt_api if qt_api == "pyqt": from PyQt4.Qt import QKeySequence, QTextCursor from PyQt4.QtGui import * # forward-compatible font weights # see https://doc.qt.io/qt-5/qfont.html#Weight-enum QFont.Weight.ExtraLight = 12 QFont.Weight.Medium = 57 QFont.Weight.ExtraBold = 81 elif qt_api == "pyqt5": from PyQt5.QtGui import * from PyQt5.QtWidgets import * from PyQt5.QtPrintSupport import * from PyQt5.QtCore import ( QAbstractProxyModel, QItemSelection, QItemSelectionModel, QItemSelectionRange, QSortFilterProxyModel, QStringListModel, ) QStyleOptionTabV2 = QStyleOptionTab QStyleOptionTabV3 = QStyleOptionTab QStyleOptionTabBarBaseV2 = QStyleOptionTabBarBase elif qt_api == "pyqt6": from PyQt6.QtGui import * from PyQt6.QtWidgets import * from PyQt6.QtPrintSupport import * from PyQt6.QtCore import ( QAbstractProxyModel, QItemSelection, QItemSelectionModel, QItemSelectionRange, QSortFilterProxyModel, QStringListModel, ) QStyleOptionTabV2 = QStyleOptionTab QStyleOptionTabV3 = QStyleOptionTab QStyleOptionTabBarBaseV2 = QStyleOptionTabBarBase elif qt_api == "pyside6": from PySide6.QtGui import * from PySide6.QtWidgets import * from PySide6.QtPrintSupport import * from PySide6.QtCore import ( QAbstractProxyModel, QItemSelection, QItemSelectionModel, QItemSelectionRange, QSortFilterProxyModel, ) QStyleOptionTabV2 = QStyleOptionTab QStyleOptionTabV3 = QStyleOptionTab QStyleOptionTabBarBaseV2 = QStyleOptionTabBarBase else: from PySide2.QtGui import * from PySide2.QtWidgets import * from PySide2.QtPrintSupport import * from PySide2.QtCore import ( QAbstractProxyModel, QItemSelection, QItemSelectionModel, QItemSelectionRange, QSortFilterProxyModel, ) QStyleOptionTabV2 = QStyleOptionTab QStyleOptionTabV3 = QStyleOptionTab QStyleOptionTabBarBaseV2 = QStyleOptionTabBarBase pyface-7.4.0/pyface/qt/QtOpenGL.py0000644000076500000240000000127714176222673017651 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 . import qt_api if qt_api == "pyqt": from PyQt4.QtOpenGL import * elif qt_api == "pyqt5": from PyQt5.QtOpenGL import * elif qt_api == "pyqt6": from PyQt6.QtOpenGL import * elif qt_api == "pyside6": from PySide6.QtOpenGL import * else: from PySide2.QtOpenGL import * pyface-7.4.0/pyface/qt/QtCore.py0000644000076500000240000000324014176222673017405 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 . import qt_api if qt_api == "pyqt": from PyQt4.QtCore import * from PyQt4.QtCore import pyqtProperty as Property from PyQt4.QtCore import pyqtSignal as Signal from PyQt4.QtCore import pyqtSlot as Slot from PyQt4.Qt import QCoreApplication from PyQt4.Qt import Qt __version__ = QT_VERSION_STR __version_info__ = tuple(map(int, QT_VERSION_STR.split("."))) elif qt_api == "pyqt5": from PyQt5.QtCore import * from PyQt5.QtCore import pyqtProperty as Property from PyQt5.QtCore import pyqtSignal as Signal from PyQt5.QtCore import pyqtSlot as Slot from PyQt5.Qt import QCoreApplication from PyQt5.Qt import Qt __version__ = QT_VERSION_STR __version_info__ = tuple(map(int, QT_VERSION_STR.split("."))) elif qt_api == "pyqt6": from PyQt6.QtCore import * from PyQt6.QtCore import pyqtProperty as Property from PyQt6.QtCore import pyqtSignal as Signal from PyQt6.QtCore import pyqtSlot as Slot __version__ = QT_VERSION_STR __version_info__ = tuple(map(int, QT_VERSION_STR.split("."))) elif qt_api == "pyside6": from PySide6.QtCore import * from PySide6 import __version__, __version_info__ else: from PySide2.QtCore import * from PySide2 import __version__, __version_info__ pyface-7.4.0/pyface/qt/QtTest.py0000644000076500000240000000126514176222673017441 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 . import qt_api if qt_api == "pyqt": from PyQt4.QtTest import * elif qt_api == "pyqt5": from PyQt5.QtTest import * elif qt_api == "pyqt6": from PyQt6.QtTest import * elif qt_api == "pyside6": from PySide6.QtTest import * else: from PySide2.QtTest import * pyface-7.4.0/pyface/qt/QtWebKit.py0000644000076500000240000000424414176222673017707 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 . import qt_api if qt_api == "pyqt": from PyQt4.QtWebKit import * elif qt_api == "pyqt5": from PyQt5.QtWidgets import * try: from PyQt5.QtWebEngine import * from PyQt5.QtWebEngineWidgets import ( QWebEngineHistory as QWebHistory, QWebEngineHistoryItem as QWebHistoryItem, QWebEnginePage as QWebPage, QWebEngineView as QWebView, QWebEngineSettings as QWebSettings, ) except ImportError: from PyQt5.QtWebKit import * from PyQt5.QtWebKitWidgets import * elif qt_api == "pyqt6": from PyQt6.QtWidgets import * from PyQt6.QtWebEngineCore import ( QWebEngineHistory as QWebHistory, QWebEngineHistoryItem as QWebHistoryItem, QWebEnginePage as QWebPage, QWebEngineSettings as QWebSettings, ) from PyQt6.QtWebEngineWidgets import ( QWebEngineView as QWebView, ) elif qt_api == "pyside6": from PySide6.QtWidgets import * from PySide6.QtWebEngineCore import ( QWebEngineHistory as QWebHistory, QWebEngineSettings as QWebSettings, QWebEnginePage as QWebPage, QWebEngineHistoryItem as QWebHistoryItem, ) from PySide6.QtWebEngineWidgets import ( QWebEngineView as QWebView, ) else: from PySide2.QtWidgets import * # WebKit is currently in flux in PySide2 try: from PySide2.QtWebEngineWidgets import ( # QWebEngineHistory as QWebHistory, QWebEngineHistoryItem as QWebHistoryItem, QWebEnginePage as QWebPage, QWebEngineView as QWebView, QWebEngineSettings as QWebSettings, ) except ImportError: from PySide2.QtWebKit import * from PySide2.QtWebKitWidgets import * pyface-7.4.0/pyface/qt/QtScript.py0000644000076500000240000000112314176222673017757 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 . import qt_api if qt_api == "pyqt": from PyQt4.QtScript import * else: import warnings warnings.warn(DeprecationWarning("QtScript is not supported in PyQt5/PySide2")) pyface-7.4.0/pyface/qt/__init__.py0000644000076500000240000000330514176222673017751 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 import os import sys QtAPIs = [ ("pyside2", "PySide2"), ("pyside6", "PySide6"), ("pyqt5", "PyQt5"), ("pyqt6", "PyQt6"), ("pyqt", "PyQt4"), ] api_names, modules = zip(*QtAPIs) qt_api = None # have we already imported a Qt API? for api_name, module in QtAPIs: if module in sys.modules: qt_api = api_name break else: # does our environment give us a preferred API? qt_api = os.environ.get("QT_API") # if we have no preference, is a Qt API available? Or fail with ImportError. if qt_api is None: for api_name, module in QtAPIs: try: importlib.import_module(module) importlib.import_module(".QtCore", module) qt_api = api_name break except ImportError: continue else: raise ImportError("Cannot import any of " + ", ".join(modules)) # otherwise check QT_API value is valid elif qt_api not in api_names: msg = ( "Invalid Qt API %r, valid values are: " + ', '.join(api_names) ) % qt_api raise RuntimeError(msg) # useful constants is_qt4 = qt_api in {"pyqt"} is_qt5 = qt_api in {"pyqt5", "pyside2"} is_qt6 = qt_api in {"pyqt6", "pyside6"} is_pyqt = qt_api in {"pyqt", "pyqt5", "pyqt6"} is_pyside = qt_api in {"pyside", "pyside2", "pyside6"} pyface-7.4.0/pyface/qt/QtSvg.py0000644000076500000240000000126014176222673017254 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 . import qt_api if qt_api == "pyqt": from PyQt4.QtSvg import * elif qt_api == "pyqt5": from PyQt5.QtSvg import * elif qt_api == "pyqt6": from PyQt6.QtSvg import * elif qt_api == "pyside6": from PySide6.QtSvg import * else: from PySide2.QtSvg import * pyface-7.4.0/pyface/qt/QtMultimediaWidgets.py0000644000076500000240000000150014176222673022133 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 . import qt_api if qt_api == "pyqt5": from PyQt5.QtMultimediaWidgets import * elif qt_api == "pyqt6": from PyQt6.QtMultimediaWidgets import * elif qt_api == "pyside6": from PySide6.QtMultimediaWidgets import * elif qt_api == "pyside2": from PySide2.QtMultimediaWidgets import * else: warnings.warn( "Qt 4 does not support QtMultimediaWidgets", DeprecationWarning ) pyface-7.4.0/pyface/qt/QtNetwork.py0000644000076500000240000000130414176222673020145 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 . import qt_api if qt_api == "pyqt": from PyQt4.QtNetwork import * elif qt_api == "pyqt5": from PyQt5.QtNetwork import * elif qt_api == "pyqt6": from PyQt6.QtNetwork import * elif qt_api == "pyside6": from PySide6.QtNetwork import * else: from PySide2.QtNetwork import * pyface-7.4.0/pyface/qt/QtMultimedia.py0000644000076500000240000000141714176222673020613 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 . import qt_api if qt_api == "pyqt5": from PyQt5.QtMultimedia import * elif qt_api == "pyqt6": from PyQt6.QtMultimedia import * elif qt_api == "pyside6": from PySide6.QtMultimedia import * elif qt_api == "pyside2": from PySide2.QtMultimedia import * else: warnings.warn("Qt 4 does not support QtMultimedia", DeprecationWarning) pyface-7.4.0/pyface/__init__.py0000644000076500000240000000555514176222673017336 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Reusable MVC-based components for Traits-based applications. Part of the TraitsUI project of the Enthought Tool Suite. """ try: from pyface._version import full_version as __version__ except ImportError: __version__ = "not-built" __requires__ = [ 'importlib-metadata>=3.6.0; python_version<"3.8"', 'importlib-resources>=1.1.0; python_version<"3.9"', "traits>=6.2", ] __extras_require__ = { "wx": ["wxpython>=4", "numpy"], "pyqt": ["pyqt>=4.10", "pygments"], "pyqt5": ["pyqt5", "pygments"], "pyqt6": ["pyqt6", "pygments"], "pyside2": ["pyside2", "shiboken2", "pygments"], "pyside6": ["pyside6", "pygments"], "pillow": ["pillow"], "test": ["packaging"], } # ============================= 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 import environ from os.path import dirname from pyface.util.testing import filter_tests from unittest import TestSuite # Make sure the right toolkit is up and running before importing tests from pyface.toolkit import toolkit_object # noqa: F401 this_dir = dirname(__file__) package_tests = loader.discover(start_dir=this_dir, pattern=pattern) # List of regular expression for filtering test using the test id. exclusion_patterns = [] # Environment variable for skipping more tests. # e.g. etstool.py in the source tree root sets this to skip packages for # specific toolkit additional_exclude = environ.get("EXCLUDE_TESTS", None) if additional_exclude is not None: exclusion_patterns.append(additional_exclude) filtered_package_tests = TestSuite() for test_suite in package_tests: for exclusion_pattern in exclusion_patterns: test_suite = filter_tests(test_suite, exclusion_pattern) filtered_package_tests.addTest(test_suite) return filtered_package_tests pyface-7.4.0/pyface/i_python_editor.py0000644000076500000240000000475014176222673020772 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 widget for editing Python code. """ from traits.api import Bool, Event, HasTraits, Str from pyface.i_layout_widget import ILayoutWidget from pyface.key_pressed_event import KeyPressedEvent class IPythonEditor(ILayoutWidget): """ A widget for editing Python code. """ # 'IPythonEditor' interface -------------------------------------------- #: Has the file in the editor been modified? dirty = Bool(False) #: The pathname of the file being edited. path = Str() #: Should line numbers be shown in the margin? show_line_numbers = Bool(True) # Events ---- #: The contents of the editor has changed. changed = Event() #: A key has been pressed. key_pressed = Event(KeyPressedEvent) # ------------------------------------------------------------------------ # 'IPythonEditor' interface. # ------------------------------------------------------------------------ def load(self, path=None): """ Loads the contents of the editor. Parameters ---------- path : str or None The path to the file to load. """ def save(self, path=None): """ Saves the contents of the editor. Parameters ---------- path : str or None The path to the file to save. """ # FIXME v3: This is very dependent on the underlying implementation. def set_style(self, n, fore, back): """ Set the foreground and background colors for a particular style and set the font and size to default values. """ def select_line(self, lineno): """ Selects the specified line. Parameters ---------- lineno : int The line number to select. """ class MPythonEditor(HasTraits): """ The mixin class that contains common code for toolkit specific implementations of the IPythonEditor interface. Implements: _changed_path() """ def _changed_path(self): """ Called when the path to the file is changed. """ if self.control is not None: self.load() pyface-7.4.0/pyface/wx/0000755000076500000240000000000014176460551015650 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/wx/scrolled_message_dialog.py0000644000076500000240000000250314176222673023055 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 wx.lib.layoutf import Layoutf class ScrolledMessageDialog(wx.Dialog): def __init__( self, parent, msg, caption, pos=wx.DefaultPosition, size=(500, 300) ): wx.Dialog.__init__(self, parent, -1, caption, pos, size) x, y = pos if x == -1 and y == -1: self.CenterOnScreen(wx.BOTH) text = wx.TextCtrl( self, -1, msg, wx.DefaultPosition, wx.DefaultSize, wx.TE_READONLY | wx.TE_MULTILINE | wx.HSCROLL | wx.TE_RICH2, ) font = wx.Font(8, wx.MODERN, wx.NORMAL, wx.NORMAL) text.SetStyle(0, len(msg), wx.TextAttr(font=font)) ok = wx.Button(self, wx.ID_OK, "OK") text.SetConstraints(Layoutf("t=t5#1;b=t5#2;l=l5#1;r=r5#1", (self, ok))) ok.SetConstraints(Layoutf("b=b5#1;x%w50#1;w!80;h!25", (self,))) self.SetAutoLayout(1) self.Layout() pyface-7.4.0/pyface/wx/color.py0000644000076500000240000000133614176222673017344 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Color utilities. """ from numpy import asarray, array # fixme: This should move into enable. def wx_to_enable_color(color): """ Convert a wx color spec. to an enable color spec. """ enable_color = array((1.0, 1.0, 1.0, 1.0)) enable_color[:3] = asarray(color.Get()) / 255.0 return tuple(enable_color) pyface-7.4.0/pyface/wx/util/0000755000076500000240000000000014176460551016625 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/wx/util/__init__.py0000644000076500000240000000000014176222673020725 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/wx/util/font_helper.py0000644000076500000240000000171414176222673021510 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 working with wx Fonts. """ import wx def new_font_like(font, **kw): """ Creates a new font, like another one, only different. Maybe. """ point_size = kw.get("point_size", font.GetPointSize()) family = kw.get("family", font.GetFamily()) style = kw.get("style", font.GetStyle()) weight = kw.get("weight", font.GetWeight()) underline = kw.get("underline", font.GetUnderlined()) face_name = kw.get("face_name", font.GetFaceName()) return wx.Font(point_size, family, style, weight, underline, face_name) pyface-7.4.0/pyface/wx/dialog.py0000644000076500000240000000705614176222673017472 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Dialog utilities. """ import wx # A file dialog wildcard for Python files. WILDCARD_PY = "Python files (*.py)|*.py|" # A file dialog wildcard for text files. WILDCARD_TXT = "Text files (*.txt)|*.txt|" # A file dialog wildcard for all files. WILDCARD_ALL = "All files (*.*)|*.*" # A file dialog wildcard for Zip archives. WILDCARD_ZIP = "Zip files (*.zip)|*.zip|" class OpenFileDialog(wx.FileDialog): """ An open-file dialog. """ def __init__(self, parent=None, **kw): """ Constructor. """ style = wx.OPEN | wx.HIDE_READONLY # Base-class constructor. wx.FileDialog.__init__(self, parent, "Open", style=style, **kw) class OpenDirDialog(wx.DirDialog): """ An open-directory dialog. """ def __init__(self, parent=None, **kw): """ Constructor. """ style = wx.OPEN | wx.HIDE_READONLY | wx.DD_NEW_DIR_BUTTON # Base-class constructor. wx.DirDialog.__init__(self, parent, "Open", style=style, **kw) class SaveFileAsDialog(wx.FileDialog): """ A save-file dialog. """ def __init__(self, parent=None, **kw): """ Constructor. """ style = wx.SAVE | wx.OVERWRITE_PROMPT # Base-class constructor. wx.FileDialog.__init__(self, parent, "Save As", style=style, **kw) def confirmation(parent, message, title=None, default=wx.NO_DEFAULT): """ Displays a confirmation dialog. """ dialog = wx.MessageDialog( parent, message, _get_title(title, parent, "Confirmation"), wx.YES_NO | default | wx.ICON_EXCLAMATION | wx.STAY_ON_TOP, ) result = dialog.ShowModal() dialog.Destroy() return result def yes_no_cancel(parent, message, title=None, default=wx.NO_DEFAULT): """ Displays a Yes/No/Cancel dialog. """ dialog = wx.MessageDialog( parent, message, _get_title(title, parent, "Confirmation"), wx.YES_NO | wx.CANCEL | default | wx.ICON_EXCLAMATION | wx.STAY_ON_TOP, ) result = dialog.ShowModal() dialog.Destroy() return result def information(parent, message, title=None): """ Displays a modal information dialog. """ dialog = wx.MessageDialog( parent, message, _get_title(title, parent, "Information"), wx.OK | wx.ICON_INFORMATION | wx.STAY_ON_TOP, ) dialog.ShowModal() dialog.Destroy() def warning(parent, message, title=None): """ Displays a modal warning dialog. """ dialog = wx.MessageDialog( parent, message, _get_title(title, parent, "Warning"), wx.OK | wx.ICON_EXCLAMATION | wx.STAY_ON_TOP, ) dialog.ShowModal() dialog.Destroy() def error(parent, message, title=None): """ Displays a modal error dialog. """ dialog = wx.MessageDialog( parent, message, _get_title(title, parent, "Error"), wx.OK | wx.ICON_ERROR | wx.STAY_ON_TOP, ) dialog.ShowModal() dialog.Destroy() def _get_title(title, parent, default): """ Get a sensible title for a dialog! """ if title is None: if parent is not None: title = parent.GetTitle() else: title = default return title pyface-7.4.0/pyface/wx/spreadsheet/0000755000076500000240000000000014176460551020157 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/wx/spreadsheet/abstract_grid_view.py0000644000076500000240000001432714176222673024403 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 wx.grid import Grid from wx.grid import GridCellFloatRenderer, GridCellFloatEditor class AbstractGridView(Grid): """ Enthought's default spreadsheet view. Uses a virtual data source. THIS CLASS IS NOT LIMITED TO ONLY DISPLAYING LOG DATA! """ def __init__(self, parent, ID=-1, **kw): Grid.__init__(self, parent, ID, **kw) # We have things set up to edit on a single click - so we have to select # an initial cursor location that is off of the screen otherwise a cell # will be in edit mode as soon as the grid fires up. self.moveTo = [1000, 1] self.edit = False # this seems like a busy idle ... self.Bind(wx.EVT_IDLE, self.OnIdle) # Enthought specific display controls ... self.init_labels() self.init_data_types() self.init_handlers() self.Bind(wx.grid.EVT_GRID_EDITOR_CREATED, self._on_editor_created) def init_labels(self): self.SetLabelFont( wx.Font( self.GetFont().GetPointSize(), wx.SWISS, wx.NORMAL, wx.BOLD ) ) self.SetGridLineColour("blue") self.SetColLabelAlignment(wx.ALIGN_CENTRE, wx.ALIGN_CENTRE) self.SetRowLabelAlignment(wx.ALIGN_LEFT, wx.ALIGN_CENTRE) def init_data_types(self): """ If the model says a cell is of a specified type, the grid uses the specific renderer and editor set in this method. """ self.RegisterDataType( "LogData", GridCellFloatRenderer(precision=3), GridCellFloatEditor(), ) def init_handlers(self): self.Bind(wx.grid.EVT_GRID_CELL_LEFT_CLICK, self.OnCellLeftClick) self.Bind(wx.grid.EVT_GRID_CELL_RIGHT_CLICK, self.OnCellRightClick) self.Bind(wx.grid.EVT_GRID_CELL_LEFT_DCLICK, self.OnCellLeftDClick) self.Bind(wx.grid.EVT_GRID_CELL_RIGHT_DCLICK, self.OnCellRightDClick) self.Bind(wx.grid.EVT_GRID_LABEL_LEFT_CLICK, self.OnLabelLeftClick) self.Bind(wx.grid.EVT_GRID_LABEL_RIGHT_CLICK, self.OnLabelRightClick) self.Bind(wx.grid.EVT_GRID_LABEL_LEFT_DCLICK, self.OnLabelLeftDClick) self.Bind(wx.grid.EVT_GRID_LABEL_RIGHT_DCLICK, self.OnLabelRightDClick) self.Bind(wx.grid.EVT_GRID_ROW_SIZE, self.OnRowSize) self.Bind(wx.grid.EVT_GRID_COL_SIZE, self.OnColSize) self.Bind(wx.grid.EVT_GRID_RANGE_SELECT, self.OnRangeSelect) self.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.OnCellChange) self.Bind(wx.grid.EVT_GRID_SELECT_CELL, self.OnSelectCell) self.Bind(wx.grid.EVT_GRID_EDITOR_SHOWN, self.OnEditorShown) self.Bind(wx.grid.EVT_GRID_EDITOR_HIDDEN, self.OnEditorHidden) self.Bind(wx.grid.EVT_GRID_EDITOR_CREATED, self.OnEditorCreated) def SetColLabelsVisible(self, show=True): """ This only works if you 'hide' then 'show' the labels. """ if not show: self._default_col_label_size = self.GetColLabelSize() self.SetColLabelSize(0) else: self.SetColLabelSize(self._default_col_label_size) def SetRowLabelsVisible(self, show=True): """ This only works if you 'hide' then 'show' the labels. """ if not show: self._default_row_label_size = self.GetRowLabelSize() self.SetRowLabelSize(0) else: self.SetRowLabelSize(self._default_row_label_size) def SetTable(self, table, *args): """ Some versions of wxPython do not return the correct table - hence we store our own copy here - weak ref? todo - does this apply to Enthought? """ self._table = table return Grid.SetTable(self, table, *args) def GetTable(self): # Terminate editing of the current cell to force an update of the table self.DisableCellEditControl() return self._table def Reset(self): """ Resets the view based on the data in the table. Call this when rows are added or destroyed. """ self._table.ResetView(self) def OnCellLeftClick(self, evt): evt.Skip() def OnCellRightClick(self, evt): # print self.GetDefaultRendererForCell(evt.GetRow(), evt.GetCol()) evt.Skip() def OnCellLeftDClick(self, evt): if self.CanEnableCellControl(): self.EnableCellEditControl() evt.Skip() def OnCellRightDClick(self, evt): evt.Skip() def OnLabelLeftClick(self, evt): evt.Skip() def OnLabelRightClick(self, evt): evt.Skip() def OnLabelLeftDClick(self, evt): evt.Skip() def OnLabelRightDClick(self, evt): evt.Skip() def OnRowSize(self, evt): evt.Skip() def OnColSize(self, evt): evt.Skip() def OnRangeSelect(self, evt): # if evt.Selecting(): # print "OnRangeSelect: top-left %s, bottom-right %s\n" % (evt.GetTopLeftCoords(), evt.GetBottomRightCoords()) evt.Skip() def OnCellChange(self, evt): evt.Skip() def OnIdle(self, evt): """ Immediately jumps into editing mode, bypassing the usual select mode of a spreadsheet. See also self.OnSelectCell(). """ if self.edit: if self.CanEnableCellControl(): self.EnableCellEditControl() self.edit = False if self.moveTo is not None: self.SetGridCursor(self.moveTo[0], self.moveTo[1]) self.moveTo = None evt.Skip() def OnSelectCell(self, evt): """ Immediately jumps into editing mode, bypassing the usual select mode of a spreadsheet. See also self.OnIdle(). """ self.edit = True evt.Skip() def OnEditorShown(self, evt): evt.Skip() def OnEditorHidden(self, evt): evt.Skip() def OnEditorCreated(self, evt): evt.Skip() pyface-7.4.0/pyface/wx/spreadsheet/unit_renderer.py0000644000076500000240000000776614176222673023417 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 try: from scimath.units.unit_parser import unit_parser except ImportError: unit_parser = None from .default_renderer import DefaultRenderer class UnitRenderer(DefaultRenderer): def DrawForeground(self, grid, attr, dc, rect, row, col, isSelected): dc.SetBackgroundMode(wx.TRANSPARENT) text = grid.model.GetValue(row, col) # print 'Rendering ', row, col, text, text.__class__ dc.SetFont(self.font) dc.DrawText(text, rect.x + 1, rect.y + 1) def DrawBackground(self, grid, attr, dc, rect, row, col, isSelected): """ Erases whatever is already in the cell by drawing over it. """ # We have to set the clipping region on the grid's DC, # otherwise the text will spill over to the next cell dc.SetClippingRegion(rect) # overwrite anything currently in the cell ... dc.SetBackgroundMode(wx.SOLID) dc.SetPen(wx.Pen(wx.WHITE, 1, wx.SOLID)) text = grid.model.GetValue(row, col) if isSelected: dc.SetBrush(DefaultRenderer.selected_cells) elif unit_parser and unit_parser.parse_unit(text).is_valid(): dc.SetBrush(DefaultRenderer.normal_cells) else: dc.SetBrush(DefaultRenderer.error_cells) dc.DrawRectangle(rect.x, rect.y, rect.width, rect.height) return # ------------------------------------------------------------------------------- class MultiUnitRenderer(DefaultRenderer): def __init__( self, color="black", font="ARIAL", fontsize=8, suppress_warnings=False ): self.suppress_warnings = suppress_warnings DefaultRenderer.__init__(self, color, font, fontsize) def DrawForeground(self, grid, attr, dc, rect, row, col, isSelected): dc.SetBackgroundMode(wx.TRANSPARENT) text = grid.model.GetValue(row, col) dc.SetFont(self.font) dc.DrawText(text, rect.x + 1, rect.y + 1) def DrawBackground(self, grid, attr, dc, rect, row, col, isSelected): """ Erases whatever is already in the cell by drawing over it. """ # We have to set the clipping region on the grid's DC, # otherwise the text will spill over to the next cell dc.SetClippingRegion(rect) # overwrite anything currently in the cell ... dc.SetBackgroundMode(wx.SOLID) dc.SetPen(wx.Pen(wx.WHITE, 1, wx.SOLID)) text = grid.model.GetValue(row, col) if unit_parser: this_unit = unit_parser.parse_unit(text, self.suppress_warnings) else: this_unit = None # Todo - clean up this hardcoded logic/column position mess family = grid.model.GetValue(row, 6) # AI units of 'impedance ((kg/s)*(g/cc))' creates list of 3, not 2! try: family, other_text = family[:-1].split("(") except: family, other_text = family[:-1].split(" ") if unit_parser: other_unit = unit_parser.parse_unit( other_text, self.suppress_warnings ) dimensionally_equivalent = this_unit.can_convert(other_unit) else: other_unit = None dimensionally_equivalent = False if isSelected: dc.SetBrush(DefaultRenderer.selected_cells) elif not this_unit or not this_unit.is_valid(): dc.SetBrush(DefaultRenderer.error_cells) elif not dimensionally_equivalent: dc.SetBrush(DefaultRenderer.warn_cells) else: dc.SetBrush(DefaultRenderer.normal_cells) dc.DrawRectangle(rect.x, rect.y, rect.width, rect.height) return pyface-7.4.0/pyface/wx/spreadsheet/font_renderer.py0000644000076500000240000000611214176222673023366 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 .default_renderer import DefaultRenderer class FontRenderer(DefaultRenderer): """Render data in the specified color and font and fontsize. """ def DrawForeground(self, grid, attr, dc, rect, row, col, isSelected): text = grid.model.GetValue(row, col) dc.SetTextForeground(self.color) dc.SetFont(self.font) dc.DrawText(text, rect.x + 1, rect.y + 1) def DrawOld(self, grid, attr, dc, rect, row, col, isSelected): # Here we draw text in a grid cell using various fonts # and colors. We have to set the clipping region on # the grid's DC, otherwise the text will spill over # to the next cell dc.SetClippingRegion(rect) # clear the background dc.SetBackgroundMode(wx.SOLID) if isSelected: dc.SetBrush(wx.Brush(wx.BLUE, wx.SOLID)) dc.SetPen(wx.Pen(wx.BLUE, 1, wx.SOLID)) else: dc.SetBrush(wx.Brush(wx.WHITE, wx.SOLID)) dc.SetPen(wx.Pen(wx.WHITE, 1, wx.SOLID)) dc.DrawRectangle(rect.x, rect.y, rect.width, rect.height) text = grid.model.GetValue(row, col) dc.SetBackgroundMode(wx.SOLID) # change the text background based on whether the grid is selected # or not if isSelected: dc.SetBrush(self.selectedBrush) dc.SetTextBackground("blue") else: dc.SetBrush(self.normalBrush) dc.SetTextBackground("white") dc.SetTextForeground(self.color) dc.SetFont(self.font) dc.DrawText(text, rect.x + 1, rect.y + 1) # Okay, now for the advanced class :) # Let's add three dots "..." # to indicate that that there is more text to be read # when the text is larger than the grid cell width, height = dc.GetTextExtent(text) if width > rect.width - 2: width, height = dc.GetTextExtent("...") x = rect.x + 1 + rect.width - 2 - width dc.DrawRectangle(x, rect.y + 1, width + 1, height) dc.DrawText("...", x, rect.y + 1) dc.DestroyClippingRegion() class FontRendererFactory88(object): """ I don't grok why this Factory (which I copied from the wx demo) was ever necessary? """ def __init__(self, color, font, fontsize): """ (color, font, fontsize) -> set of a factory to generate renderers when called. func = MegaFontRenderFactory(color, font, fontsize) renderer = func(table) """ self.color = color self.font = font self.fontsize = fontsize def __call__(self, table): return FontRenderer(table, self.color, self.font, self.fontsize) pyface-7.4.0/pyface/wx/spreadsheet/virtual_model.py0000644000076500000240000002147014176222673023404 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 wx.grid import ( PyGridTableBase, GridCellAttr, GridTableMessage, ) from wx.grid import ( GRIDTABLE_NOTIFY_ROWS_DELETED, GRIDTABLE_NOTIFY_ROWS_APPENDED, ) from wx.grid import ( GRIDTABLE_NOTIFY_COLS_DELETED, GRIDTABLE_NOTIFY_COLS_APPENDED, ) from wx.grid import GRIDTABLE_REQUEST_VIEW_GET_VALUES class VirtualModel(PyGridTableBase): """ A custom wxGrid Table that expects a user supplied data source. THIS CLASS IS NOT LIMITED TO ONLY DISPLAYING LOG DATA! """ def __init__(self, data, column_names): """data is currently a list of the form [(rowname, dictionary), dictionary.get(colname, None) returns the data for a cell """ PyGridTableBase.__init__(self) self.set_data_source(data) self.colnames = column_names # self.renderers = {"DEFAULT_RENDERER":DefaultRenderer()} # self.editors = {} # we need to store the row length and col length to see if the table has changed size self._rows = self.GetNumberRows() self._cols = self.GetNumberCols() # ------------------------------------------------------------------------------- # Implement/override the methods from PyGridTableBase # ------------------------------------------------------------------------------- def GetNumberCols(self): return len(self.colnames) def GetNumberRows(self): return len(self._data) def GetColLabelValue(self, col): return self.colnames[col] def GetRowLabelValue(self, row): return self._data[row][0] def GetValue(self, row, col): return str(self._data[row][1].get(self.GetColLabelValue(col), "")) def GetRawValue(self, row, col): return self._data[row][1].get(self.GetColLabelValue(col), "") def SetValue(self, row, col, value): print("Setting value %d %d %s" % (row, col, value)) print("Before ", self.GetValue(row, col)) self._data[row][1][self.GetColLabelValue(col)] = value print("After ", self.GetValue(row, col)) """ def GetTypeName(self, row, col): if col == 2 or col == 6: res = "MeasurementUnits" elif col == 7: res = GRID_VALUE_BOOL else: res = self.base_GetTypeName(row, col) # print 'asked for type of col ', col, ' ' ,res return res""" # ------------------------------------------------------------------------------- # Accessors for the Enthought data model (a dict of dicts) # ------------------------------------------------------------------------------- def get_data_source(self): """ The data structure we provide the data in. """ return self._data def set_data_source(self, source): self._data = source return # ------------------------------------------------------------------------------- # Methods controlling updating and editing of cells in grid # ------------------------------------------------------------------------------- def ResetView(self, grid): """ (wxGrid) -> Reset the grid view. Call this to update the grid if rows and columns have been added or deleted """ grid.BeginBatch() for current, new, delmsg, addmsg in [ ( self._rows, self.GetNumberRows(), GRIDTABLE_NOTIFY_ROWS_DELETED, GRIDTABLE_NOTIFY_ROWS_APPENDED, ), ( self._cols, self.GetNumberCols(), GRIDTABLE_NOTIFY_COLS_DELETED, GRIDTABLE_NOTIFY_COLS_APPENDED, ), ]: if new < current: msg = GridTableMessage(self, delmsg, new, current - new) grid.ProcessTableMessage(msg) elif new > current: msg = GridTableMessage(self, addmsg, new - current) grid.ProcessTableMessage(msg) self.UpdateValues(grid) grid.EndBatch() self._rows = self.GetNumberRows() self._cols = self.GetNumberCols() # update the renderers # self._updateColAttrs(grid) # self._updateRowAttrs(grid) too expensive to use on a large grid # update the scrollbars and the displayed part of the grid grid.AdjustScrollbars() grid.ForceRefresh() def UpdateValues(self, grid): """Update all displayed values""" # This sends an event to the grid table to update all of the values msg = GridTableMessage(self, GRIDTABLE_REQUEST_VIEW_GET_VALUES) grid.ProcessTableMessage(msg) def GetAttr88(self, row, col, someExtraParameter): print("Overridden GetAttr ", row, col) """Part of a workaround to avoid use of attributes, queried by _PropertyGrid's IsCurrentCellReadOnly""" # property = self.GetPropertyForCoordinate( row, col ) # object = self.GetObjectForCoordinate( row, col ) # if property.ReadOnly( object ): attr = GridCellAttr() attr.SetReadOnly(1) return attr # return None def _updateColAttrs88(self, grid): """ wxGrid -> update the column attributes to add the appropriate renderer given the column name. """ for col, colname in enumerate(self.colnames): attr = GridCellAttr() # attr.SetAlignment(ALIGN_LEFT, ALIGN_CENTRE) if colname in self.renderers: # renderer = self.plugins[colname](self) renderer = self.renderers[colname] # if renderer.colSize: # grid.SetColSize(col, renderer.colSize) # if renderer.rowSize: # grid.SetDefaultRowSize(renderer.rowSize) # attr.SetReadOnly(False) # attr.SetRenderer(renderer) else: renderer = self.renderers["DEFAULT_RENDERER"] # .Clone() attr.SetRenderer(renderer) """else: #renderer = GridCellFloatRenderer(6,2) #attr.SetReadOnly(True) #attr.SetRenderer(renderer)""" if colname in self.editors: editor = self.editors[colname] attr.SetEditor(editor) grid.SetColAttr(col, attr) return # ------------------------------------------------------------------------------ # code to manipulate the table (non wx related) # ------------------------------------------------------------------------------ def AppendRow(self, row): """ Append a tupe containing (name, data) """ name, data = row print("Appending ", name) self._data.append(row) """entry = {} for name in self.colnames: entry[name] = "Appended_%i"%row return""" def DeleteCols88(self, cols): """ cols -> delete the columns from the dataset cols hold the column indices """ # we'll cheat here and just remove the name from the # list of column names. The data will remain but # it won't be shown deleteCount = 0 cols = cols[:] cols.sort() for i in cols: self.colnames.pop(i - deleteCount) # we need to advance the delete count # to make sure we delete the right columns deleteCount += 1 if not len(self.colnames): self.data = [] def DeleteRow(self, row): name, data = row print("Deleting ", name) self._data.remove(row) def DeleteRows88(self, rows): """ rows -> delete the rows from the dataset rows hold the row indices """ deleteCount = 0 rows = rows[:] rows.sort() for i in rows: self._data.pop(i - deleteCount) # we need to advance the delete count # to make sure we delete the right rows deleteCount += 1 def SortColumn88(self, col): """ to do - never tested tried to rename data to _data and _data to _tmp_data col -> sort the data based on the column indexed by col """ name = self.colnames[col] _tmp_data = [] for row in self._data: rowname, entry = row _tmp_data.append((entry.get(name, None), row)) _tmp_data.sort() self._data = [] for sortvalue, row in _tmp_data: self._data.append(row) pyface-7.4.0/pyface/wx/spreadsheet/__init__.py0000644000076500000240000000062714176222673022276 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! pyface-7.4.0/pyface/wx/spreadsheet/default_renderer.py0000644000076500000240000000762414176222673024055 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 string import atof import wx from wx.grid import PyGridCellRenderer class DefaultRenderer(PyGridCellRenderer): """ This renderer provides the default representation of an Enthought spreadsheet cell. """ selected_cells = wx.Brush(wx.Colour(255, 255, 200), wx.SOLID) normal_cells = wx.Brush("white", wx.SOLID) odd_cells = wx.Brush(wx.Colour(240, 240, 240), wx.SOLID) error_cells = wx.Brush(wx.Colour(255, 122, 122), wx.SOLID) warn_cells = wx.Brush(wx.Colour(255, 242, 0), wx.SOLID) def __init__(self, color="black", font="ARIAL", fontsize=8): PyGridCellRenderer.__init__(self) self.color = color self.foundary = font self.fontsize = fontsize self.font = wx.Font( fontsize, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, 0, font ) def Clone(self): return DefaultRenderer(self.color, self.foundary, self.fontsize) def Draw(self, grid, attr, dc, rect, row, col, isSelected): self.DrawBackground(grid, attr, dc, rect, row, col, isSelected) self.DrawForeground(grid, attr, dc, rect, row, col, isSelected) dc.DestroyClippingRegion() def DrawBackground(self, grid, attr, dc, rect, row, col, isSelected): """ Erases whatever is already in the cell by drawing over it. """ # We have to set the clipping region on the grid's DC, # otherwise the text will spill over to the next cell dc.SetClippingRegion(rect) # overwrite anything currently in the cell ... dc.SetBackgroundMode(wx.SOLID) dc.SetPen(wx.Pen(wx.WHITE, 1, wx.SOLID)) if isSelected: dc.SetBrush(DefaultRenderer.selected_cells) elif row % 2: dc.SetBrush(DefaultRenderer.normal_cells) else: dc.SetBrush(DefaultRenderer.odd_cells) dc.DrawRectangle(rect.x, rect.y, rect.width, rect.height) def DrawForeground(self, grid, attr, dc, rect, row, col, isSelected): """ Draws the cell (text) on top of the existing background color. """ dc.SetBackgroundMode(wx.TRANSPARENT) text = grid.model.GetValue(row, col) dc.SetTextForeground(self.color) dc.SetFont(self.font) dc.DrawText(self.FormatText(text), rect.x + 1, rect.y + 1) self.DrawEllipses(grid, attr, dc, rect, row, col, isSelected) def FormatText(self, text): """ Formats numbers to 3 decimal places. """ try: text = "%0.3f" % atof(text) except: pass return text def DrawEllipses(self, grid, attr, dc, rect, row, col, isSelected): """ Adds three dots "..." to indicate the cell is truncated. """ text = grid.model.GetValue(row, col) if not isinstance(text, str): msg = 'Problem appending "..." to cell: %d %d' % (row, col) raise TypeError(msg) width, height = dc.GetTextExtent(text) if width > rect.width - 2: width, height = dc.GetTextExtent("...") x = rect.x + 1 + rect.width - 2 - width dc.DrawRectangle(x, rect.y + 1, width + 1, height) dc.DrawText("...", x, rect.y + 1) def GetBestSize88(self, grid, attr, dc, row, col): """ This crashes the app - hmmmm. """ size = PyGridCellRenderer.GetBestSize(self, grid, attr, dc, row, col) print("-----------------------------", size) return size # ------------------------------------------------------------------------------- pyface-7.4.0/pyface/wx/progress_meter.py0000644000076500000240000000140514176222673021263 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 class ProgressDialog(wx.ProgressDialog): def __init__(self, *args, **kwds): wx.ProgressDialog.__init__(self, *args, **kwds) def SetButtonLabel(self, title): """ Change the Cancel button label to something else eg Stop.""" button = self.Window.FindWindowById(wx.ID_CANCEL) button.SetLabel(title) return pyface-7.4.0/pyface/wx/spacer.py0000644000076500000240000000232314176222673017500 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 panel used as a spacer. It is in a separate class so that you can set the background color in a single place to give visual feedback on sizer layouts (in particular, FlexGridSizer layouts). """ import wx class Spacer(wx.Panel): """ A panel used as a spacer. """ def __init__(self, parent, id, **kw): """ Creates a spacer. """ # Base-class constructor. wx.Panel.__init__(self, parent, id, **kw) # Create the widget! self._create_widget() return # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _create_widget(self): """ Create the widget! """ # self.SetBackgroundColour("brown") return pyface-7.4.0/pyface/wx/divider.py0000644000076500000240000000207314176222673017653 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 thin visual divider. """ import wx class Divider(wx.StaticLine): """ A thin visual divider. """ def __init__(self, parent, id, **kw): """ Creates a divider. """ # Base-class constructor. wx.StaticLine.__init__(self, parent, id, style=wx.LI_HORIZONTAL, **kw) # Create the widget! self._create_widget() return # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _create_widget(self): """ Creates the widget. """ self.SetSize((1, 1)) return pyface-7.4.0/pyface/wx/shell.py0000644000076500000240000015525214176222673017344 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 PyCrust Shell is an interactive text control in which a user types in commands to be sent to the interpreter. This particular shell is based on wxPython's wxStyledTextCtrl. The latest files are always available at the SourceForge project page at http://sourceforge.net/projects/pycrust/. Sponsored by Orbtech - Your source for Python programming expertise.""" __author__ = "Patrick K. O'Brien " __cvsid__ = "$Id: shell.py,v 1.2 2003/06/13 17:59:34 dmorrill Exp $" __revision__ = "$Revision: 1.2 $"[11:-2] import wx import wx.stc from wx import * from wx.stc import * wx.StyledTextCtrl = wx.stc.StyledTextCtrl import keyword import os import sys from wx.py.pseudo import PseudoFileIn, PseudoFileOut, PseudoFileErr from wx.py.version import VERSION from .drag_and_drop import PythonObject from .drag_and_drop import clipboard as enClipboard sys.ps3 = "<-- " # Input prompt. NAVKEYS = ( wx.WXK_END, wx.WXK_LEFT, wx.WXK_RIGHT, wx.WXK_UP, wx.WXK_DOWN, wx.WXK_PAGEUP, wx.WXK_PAGEDOWN ) if wxPlatform == "__WXMSW__": faces = { "times": "Times New Roman", "mono": "Courier New", "helv": "Lucida Console", "lucida": "Lucida Console", "other": "Comic Sans MS", "size": 10, "lnsize": 9, "backcol": "#FFFFFF", } # Versions of wxPython prior to 2.3.2 had a sizing bug on Win platform. # The font was 2 points too large. So we need to reduce the font size. if (wxMAJOR_VERSION, wxMINOR_VERSION, wxRELEASE_NUMBER) < (2, 3, 2): faces["size"] -= 2 faces["lnsize"] -= 2 else: # GTK faces = { "times": "Times", "mono": "Courier", "helv": "Helvetica", "other": "new century schoolbook", "size": 12, "lnsize": 10, "backcol": "#FFFFFF", } class ShellFacade(object): """Simplified interface to all shell-related functionality. This is a semi-transparent facade, in that all attributes of other are still accessible, even though only some are visible to the user.""" name = "PyCrust Shell Interface" revision = __revision__ def __init__(self, other): """Create a ShellFacade instance.""" methods = [ "ask", "clear", "pause", "prompt", "quit", "redirectStderr", "redirectStdin", "redirectStdout", "run", "runfile", "wrap", "zoom", ] for method in methods: self.__dict__[method] = getattr(other, method) d = self.__dict__ d["other"] = other d[ "helpText" ] = """ * Key bindings: Home Go to the beginning of the command or line. Shift+Home Select to the beginning of the command or line. Shift+End Select to the end of the line. End Go to the end of the line. Ctrl+C Copy selected text, removing prompts. Ctrl+Shift+C Copy selected text, retaining prompts. Ctrl+X Cut selected text. Ctrl+V Paste from clipboard. Ctrl+Shift+V Paste and run multiple commands from clipboard. Ctrl+Up Arrow Retrieve Previous History item. Alt+P Retrieve Previous History item. Ctrl+Down Arrow Retrieve Next History item. Alt+N Retrieve Next History item. Shift+Up Arrow Insert Previous History item. Shift+Down Arrow Insert Next History item. F8 Command-completion of History item. (Type a few characters of a previous command and then press F8.) F9 Pop-up window of matching History items. (Type a few characters of a previous command and then press F9.) """ def help(self): """Display some useful information about how to use the shell.""" self.write(self.helpText) def __getattr__(self, name): if hasattr(self.other, name): return getattr(self.other, name) else: raise AttributeError(name) def __setattr__(self, name, value): if name in self.__dict__: self.__dict__[name] = value elif hasattr(self.other, name): return setattr(self.other, name, value) else: raise AttributeError(name) def _getAttributeNames(self): """Return list of magic attributes to extend introspection.""" list = [ "autoCallTip", "autoComplete", "autoCompleteCaseInsensitive", "autoCompleteIncludeDouble", "autoCompleteIncludeMagic", "autoCompleteIncludeSingle", ] list.sort() return list class Shell(wx.StyledTextCtrl): """PyCrust Shell based on wxStyledTextCtrl.""" name = "PyCrust Shell" revision = __revision__ def __init__( self, parent, id=-1, pos=wx.DefaultPosition, size=wx.DefaultSize, style=wx.CLIP_CHILDREN, introText="", locals=None, InterpClass=None, *args, **kwds ): """Create a PyCrust Shell instance.""" wx.StyledTextCtrl.__init__(self, parent, id, pos, size, style) # Grab these so they can be restored by self.redirect* methods. self.stdin = sys.stdin self.stdout = sys.stdout self.stderr = sys.stderr self.handlers = [] self.python_obj_paste_handler = None # Add the current working directory "." to the search path. sys.path.insert(0, os.curdir) # Import a default interpreter class if one isn't provided. if InterpClass is None: from wx.py.interpreter import Interpreter else: Interpreter = InterpClass # Create default locals so we have something interesting. shellLocals = { "__name__": "PyCrust-Shell", "__doc__": "PyCrust-Shell, The PyCrust Python Shell.", "__version__": VERSION, } # Add the dictionary that was passed in. if locals: shellLocals.update(locals) # Create a replacement for stdin. self.reader = PseudoFileIn(self.readline) self.reader.input = "" self.reader.isreading = 0 # Set up the interpreter. self.interp = Interpreter( locals=shellLocals, rawin=self.raw_input, stdin=self.reader, stdout=PseudoFileOut(self.writeOut), stderr=PseudoFileErr(self.writeErr), *args, **kwds ) # Find out for which keycodes the interpreter will autocomplete. self.autoCompleteKeys = self.interp.getAutoCompleteKeys() # Keep track of the last non-continuation prompt positions. self.promptPosStart = 0 self.promptPosEnd = 0 # Keep track of multi-line commands. self.more = 0 # Create the command history. Commands are added into the front of # the list (ie. at index 0) as they are entered. self.historyIndex # is the current position in the history; it gets incremented as you # retrieve the previous command, decremented as you retrieve the # next, and reset when you hit Enter. self.historyIndex == -1 means # you're on the current command, not in the history. self.history = [] self.historyIndex = -1 self.historyPrefix = 0 # Assign handlers for keyboard events. self.Bind(EVT_KEY_DOWN, self.OnKeyDown) self.Bind(EVT_CHAR, self.OnChar) # Assign handlers for wxSTC events. self.Bind(EVT_STC_UPDATEUI, self.OnUpdateUI) self.Bind(EVT_STC_USERLISTSELECTION, self.OnHistorySelected) # Configure various defaults and user preferences. self.config() # Display the introductory banner information. try: self.showIntro(introText) except: pass # Assign some pseudo keywords to the interpreter's namespace. try: self.setBuiltinKeywords() except: pass # Add 'shell' to the interpreter's local namespace. try: self.setLocalShell() except: pass # Do this last so the user has complete control over their # environment. They can override anything they want. try: self.execStartupScript(self.interp.startupScript) except: pass def destroy(self): # del self.interp pass def config(self): """Configure shell based on user preferences.""" self.SetMarginType(1, wx.STC_MARGIN_NUMBER) self.SetMarginWidth(1, 40) self.SetLexer(wx.STC_LEX_PYTHON) self.SetKeyWords(0, " ".join(keyword.kwlist)) self.setStyles(faces) self.SetViewWhiteSpace(0) self.SetTabWidth(4) self.SetUseTabs(0) # Do we want to automatically pop up command completion options? self.autoComplete = 1 self.autoCompleteIncludeMagic = 1 self.autoCompleteIncludeSingle = 1 self.autoCompleteIncludeDouble = 1 self.autoCompleteCaseInsensitive = 1 self.AutoCompSetIgnoreCase(self.autoCompleteCaseInsensitive) self.AutoCompSetSeparator(ord("\n")) # Do we want to automatically pop up command argument help? self.autoCallTip = 1 self.CallTipSetBackground(wxColour(255, 255, 232)) self.wrap() try: self.SetEndAtLastLine(false) except AttributeError: pass def showIntro(self, text=""): """Display introductory text in the shell.""" if text: if not text.endswith(os.linesep): text += os.linesep self.write(text) try: self.write(self.interp.introText) except AttributeError: pass wxCallAfter(self.ScrollToLine, 0) def setBuiltinKeywords(self): """Create pseudo keywords as part of builtins. This simply sets "close", "exit" and "quit" to a helpful string. """ import builtins builtins.close = ( builtins.exit ) = ( builtins.quit ) = "Click on the close button to leave the application." def quit(self): """Quit the application.""" # XXX Good enough for now but later we want to send a close event. # In the close event handler we can make sure they want to quit. # Other applications, like PythonCard, may choose to hide rather than # quit so we should just post the event and let the surrounding app # decide what it wants to do. self.write("Click on the close button to leave the application.") def setLocalShell(self): """Add 'shell' to locals as reference to ShellFacade instance.""" self.interp.locals["shell"] = ShellFacade(other=self) def execStartupScript(self, startupScript): """Execute the user's PYTHONSTARTUP script if they have one.""" if startupScript and os.path.isfile(startupScript): startupText = "Startup script executed: " + startupScript self.push( "print(%s);exec(open(%s).read())" % (repr(startupText), repr(startupScript)) ) else: self.push("") def setStyles(self, faces): """Configure font size, typeface and color for lexer.""" # Default style self.StyleSetSpec( wxSTC_STYLE_DEFAULT, "face:%(mono)s,size:%(size)d,back:%(backcol)s" % faces, ) self.StyleClearAll() # Built in styles self.StyleSetSpec( wxSTC_STYLE_LINENUMBER, "back:#C0C0C0,face:%(mono)s,size:%(lnsize)d" % faces, ) self.StyleSetSpec(wxSTC_STYLE_CONTROLCHAR, "face:%(mono)s" % faces) self.StyleSetSpec(wxSTC_STYLE_BRACELIGHT, "fore:#0000FF,back:#FFFF88") self.StyleSetSpec(wxSTC_STYLE_BRACEBAD, "fore:#FF0000,back:#FFFF88") # Python styles self.StyleSetSpec(wxSTC_P_DEFAULT, "face:%(mono)s" % faces) self.StyleSetSpec( wxSTC_P_COMMENTLINE, "fore:#007F00,face:%(mono)s" % faces ) self.StyleSetSpec(wxSTC_P_NUMBER, "") self.StyleSetSpec(wxSTC_P_STRING, "fore:#7F007F,face:%(mono)s" % faces) self.StyleSetSpec( wxSTC_P_CHARACTER, "fore:#7F007F,face:%(mono)s" % faces ) self.StyleSetSpec(wxSTC_P_WORD, "fore:#00007F,bold") self.StyleSetSpec(wxSTC_P_TRIPLE, "fore:#7F0000") self.StyleSetSpec(wxSTC_P_TRIPLEDOUBLE, "fore:#000033,back:#FFFFE8") self.StyleSetSpec(wxSTC_P_CLASSNAME, "fore:#0000FF,bold") self.StyleSetSpec(wxSTC_P_DEFNAME, "fore:#007F7F,bold") self.StyleSetSpec(wxSTC_P_OPERATOR, "") self.StyleSetSpec(wxSTC_P_IDENTIFIER, "") self.StyleSetSpec(wxSTC_P_COMMENTBLOCK, "fore:#7F7F7F") self.StyleSetSpec( wxSTC_P_STRINGEOL, "fore:#000000,face:%(mono)s,back:#E0C0E0,eolfilled" % faces, ) def OnUpdateUI(self, evt): """Check for matching braces.""" braceAtCaret = -1 braceOpposite = -1 charBefore = None caretPos = self.GetCurrentPos() if caretPos > 0: charBefore = self.GetCharAt(caretPos - 1) # *** Patch to fix bug in wxSTC for wxPython < 2.3.3. if charBefore < 0: charBefore = 32 # Mimic a space. # *** styleBefore = self.GetStyleAt(caretPos - 1) # Check before. if ( charBefore and chr(charBefore) in "[]{}()" and styleBefore == wx.STC_P_OPERATOR ): braceAtCaret = caretPos - 1 # Check after. if braceAtCaret < 0: charAfter = self.GetCharAt(caretPos) # *** Patch to fix bug in wxSTC for wxPython < 2.3.3. if charAfter < 0: charAfter = 32 # Mimic a space. # *** styleAfter = self.GetStyleAt(caretPos) if ( charAfter and chr(charAfter) in "[]{}()" and styleAfter == wxSTC_P_OPERATOR ): braceAtCaret = caretPos if braceAtCaret >= 0: braceOpposite = self.BraceMatch(braceAtCaret) if braceAtCaret != -1 and braceOpposite == -1: self.BraceBadLight(braceAtCaret) else: self.BraceHighlight(braceAtCaret, braceOpposite) def OnChar(self, event): """Keypress event handler. Only receives an event if OnKeyDown calls event.Skip() for the corresponding event.""" # Prevent modification of previously submitted commands/responses. if not self.CanEdit(): return key = event.KeyCode() currpos = self.GetCurrentPos() stoppos = self.promptPosEnd # Return (Enter) needs to be ignored in this handler. if key == WXK_RETURN: pass elif key in self.autoCompleteKeys: # Usually the dot (period) key activates auto completion. # Get the command between the prompt and the cursor. # Add the autocomplete character to the end of the command. command = self.GetTextRange(stoppos, currpos) if command == "": self.historyShow() else: command += chr(key) self.write(chr(key)) if self.autoComplete: self.autoCompleteShow(command) elif key == ord("("): # The left paren activates a call tip and cancels # an active auto completion. if self.AutoCompActive(): self.AutoCompCancel() # Get the command between the prompt and the cursor. # Add the '(' to the end of the command. self.ReplaceSelection("") command = self.GetTextRange(stoppos, currpos) + "(" self.write("(") if self.autoCallTip: self.autoCallTipShow(command) else: # Allow the normal event handling to take place. event.Skip() def OnKeyDown(self, event): """Key down event handler.""" # Prevent modification of previously submitted commands/responses. key = event.KeyCode() controlDown = event.ControlDown() altDown = event.AltDown() shiftDown = event.ShiftDown() currpos = self.GetCurrentPos() selecting = self.GetSelectionStart() != self.GetSelectionEnd() # Return (Enter) is used to submit a command to the interpreter. if not controlDown and key == WXK_RETURN: if self.AutoCompActive(): event.Skip() return if self.CallTipActive(): self.CallTipCancel() self.processLine() # Ctrl+Return (Cntrl+Enter) is used to insert a line break. elif controlDown and key == WXK_RETURN: if self.AutoCompActive(): self.AutoCompCancel() if self.CallTipActive(): self.CallTipCancel() if not self.more and ( self.GetTextRange(self.promptPosEnd, self.GetCurrentPos()) == "" ): self.historyShow() else: self.insertLineBreak() # If the auto-complete window is up let it do its thing. elif self.AutoCompActive(): event.Skip() # Let Ctrl-Alt-* get handled normally. elif controlDown and altDown: event.Skip() # Clear the current, unexecuted command. elif key == WXK_ESCAPE: if self.CallTipActive(): event.Skip() else: self.clearCommand() # Cut to the clipboard. elif (controlDown and key in (ord("X"), ord("x"))) or ( shiftDown and key == WXK_DELETE ): self.Cut() # Copy to the clipboard. elif ( controlDown and not shiftDown and key in (ord("C"), ord("c"), WXK_INSERT) ): self.Copy() # Copy to the clipboard, including prompts. elif ( controlDown and shiftDown and key in (ord("C"), ord("c"), WXK_INSERT) ): self.CopyWithPrompts() # Home needs to be aware of the prompt. elif key == WXK_HOME: home = self.promptPosEnd if currpos > home: self.SetCurrentPos(home) if not selecting and not shiftDown: self.SetAnchor(home) self.EnsureCaretVisible() else: event.Skip() # # The following handlers modify text, so we need to see if there # is a selection that includes text prior to the prompt. # # Don't modify a selection with text prior to the prompt. elif selecting and key not in NAVKEYS and not self.CanEdit(): pass # Paste from the clipboard. elif ( controlDown and not shiftDown and key in (ord("V"), ord("v")) ) or (shiftDown and not controlDown and key == WXK_INSERT): self.Paste() # Paste from the clipboard, run commands. elif controlDown and shiftDown and key in (ord("V"), ord("v")): self.PasteAndRun() # Replace with the previous command from the history buffer. elif (controlDown and key == WXK_UP) or ( altDown and key in (ord("P"), ord("p")) ): self.OnHistoryReplace(step=+1) # Replace with the next command from the history buffer. elif (controlDown and key == WXK_DOWN) or ( altDown and key in (ord("N"), ord("n")) ): self.OnHistoryReplace(step=-1) # Insert the previous command from the history buffer. elif (shiftDown and key == WXK_UP) and self.CanEdit(): self.OnHistoryInsert(step=+1) # Insert the next command from the history buffer. elif (shiftDown and key == WXK_DOWN) and self.CanEdit(): self.OnHistoryInsert(step=-1) # Search up the history for the text in front of the cursor. elif key == WXK_F8: self.OnHistorySearch() # Show all history entries that match the command typed so far: elif key == WXK_F9: self.historyShow(self.getCommand(rstrip=0)) # Don't backspace over the latest non-continuation prompt. elif key == WXK_BACK: if selecting and self.CanEdit(): event.Skip() elif currpos > self.promptPosEnd: event.Skip() # Only allow these keys after the latest prompt. elif key == WXK_DELETE: if self.CanEdit(): event.Skip() elif key == WXK_TAB: if self.CanEdit() and not self.topLevelComplete(): event.Skip() # Don't toggle between insert mode and overwrite mode. elif key == WXK_INSERT: pass # Don't allow line deletion. elif controlDown and key in (ord("L"), ord("l")): pass # Don't allow line transposition. elif controlDown and key in (ord("T"), ord("t")): pass # Basic navigation keys should work anywhere. elif key in NAVKEYS: event.Skip() # Protect the readonly portion of the shell. elif not self.CanEdit(): pass else: event.Skip() def clearCommand(self): """Delete the current, unexecuted command.""" startpos = self.promptPosEnd endpos = self.GetTextLength() self.SetSelection(startpos, endpos) self.ReplaceSelection("") self.more = 0 def OnHistoryReplace(self, step): """Replace with the previous/next command from the history buffer.""" if not self.historyPrefix: self.historyPrefix = 1 self.historyMatches = None prefix = self.getCommand(rstrip=0) n = len(prefix) if n > 0: self.historyMatches = matches = [] for command in self.history: if command[:n] == prefix and command not in matches: matches.append(command) self.clearCommand() self.replaceFromHistory(step, self.historyMatches) def replaceFromHistory(self, step, history=None): """Replace selection with command from the history buffer.""" self.ReplaceSelection("") if history is None: history = self.history newindex = self.historyIndex + step if -1 <= newindex <= len(history): self.historyIndex = newindex if 0 <= newindex <= len(history) - 1: command = history[self.historyIndex] command = command.replace("\n", os.linesep + sys.ps2) self.ReplaceSelection(command) def OnHistoryInsert(self, step): """Insert the previous/next command from the history buffer.""" if not self.CanEdit(): return startpos = self.GetCurrentPos() self.replaceFromHistory(step) endpos = self.GetCurrentPos() self.SetSelection(endpos, startpos) def OnHistorySearch(self): """Search up the history buffer for the text in front of the cursor.""" if not self.CanEdit(): return startpos = self.GetCurrentPos() # The text up to the cursor is what we search for. numCharsAfterCursor = self.GetTextLength() - startpos searchText = self.getCommand(rstrip=0) if numCharsAfterCursor > 0: searchText = searchText[:-numCharsAfterCursor] if not searchText: return # Search upwards from the current history position and loop back # to the beginning if we don't find anything. if (self.historyIndex <= -1) or ( self.historyIndex >= len(self.history) - 2 ): searchOrder = list(range(len(self.history))) else: searchOrder = list( range(self.historyIndex + 1, len(self.history)) ) + list(range(self.historyIndex)) for i in searchOrder: command = self.history[i] if command[: len(searchText)] == searchText: # Replace the current selection with the one we've found. self.ReplaceSelection(command[len(searchText):]) endpos = self.GetCurrentPos() self.SetSelection(endpos, startpos) # We've now warped into middle of the history. self.historyIndex = i break def setStatusText(self, text): """Display status information.""" # This method will most likely be replaced by the enclosing app # to do something more interesting, like write to a status bar. print(text) def insertLineBreak(self): """Insert a new line break.""" if self.CanEdit(): self.write(os.linesep) self.more = 1 self.prompt() def processLine(self): """Process the line of text at which the user hit Enter.""" # The user hit ENTER and we need to decide what to do. They could be # sitting on any line in the shell. thepos = self.GetCurrentPos() startpos = self.promptPosEnd endpos = self.GetTextLength() # If they hit RETURN inside the current command, execute the command. if self.CanEdit(): self.SetCurrentPos(endpos) self.interp.more = 0 command = self.GetTextRange(startpos, endpos) lines = command.split(os.linesep + sys.ps2) lines = [line.rstrip() for line in lines] command = "\n".join(lines) if self.reader.isreading: if not command: # Match the behavior of the standard Python shell when # the user hits return without entering a value. command = "\n" self.reader.input = command self.write(os.linesep) else: self.push(command) # Or replace the current command with the other command. else: # If the line contains a command (even an invalid one). if self.getCommand(rstrip=0): command = self.getMultilineCommand() self.clearCommand() self.write(command) # Otherwise, put the cursor back where we started. else: self.SetCurrentPos(thepos) self.SetAnchor(thepos) def getMultilineCommand(self, rstrip=1): """Extract a multi-line command from the editor. The command may not necessarily be valid Python syntax.""" # XXX Need to extract real prompts here. Need to keep track of the # prompt every time a command is issued. ps1 = str(sys.ps1) ps1size = len(ps1) ps2 = str(sys.ps2) ps2size = len(ps2) # This is a total hack job, but it works. text = self.GetCurLine()[0] line = self.GetCurrentLine() while text[:ps2size] == ps2 and line > 0: line -= 1 self.GotoLine(line) text = self.GetCurLine()[0] if text[:ps1size] == ps1: line = self.GetCurrentLine() self.GotoLine(line) startpos = self.GetCurrentPos() + ps1size line += 1 self.GotoLine(line) while self.GetCurLine()[0][:ps2size] == ps2: line += 1 self.GotoLine(line) stoppos = self.GetCurrentPos() command = self.GetTextRange(startpos, stoppos) command = command.replace(os.linesep + sys.ps2, "\n") command = command.rstrip() command = command.replace("\n", os.linesep + sys.ps2) else: command = "" if rstrip: command = command.rstrip() return command def getCommand(self, text=None, rstrip=1): """Extract a command from text which may include a shell prompt. The command may not necessarily be valid Python syntax.""" if not text: text = self.GetCurLine()[0] # Strip the prompt off the front of text leaving just the command. command = self.lstripPrompt(text) if command == text: command = "" # Real commands have prompts. if rstrip: command = command.rstrip() return command def lstripPrompt(self, text): """Return text without a leading prompt.""" ps1 = str(sys.ps1) ps1size = len(ps1) ps2 = str(sys.ps2) ps2size = len(ps2) # Strip the prompt off the front of text. if text[:ps1size] == ps1: text = text[ps1size:] elif text[:ps2size] == ps2: text = text[ps2size:] return text def push(self, command): """Send command to the interpreter for execution.""" self.write(os.linesep) busy = wxBusyCursor() self.more = self.interp.push(command) del busy if not self.more: self.addHistory(command.rstrip()) for handler in self.handlers: handler() self.prompt() def addHistory(self, command): """Add command to the command history.""" # Reset the history position. self.historyIndex = -1 self.historyPrefix = 0 # Insert this command into the history, unless it's a blank # line or the same as the last command. if command != "" and ( len(self.history) == 0 or command != self.history[0] ): self.history.insert(0, command) def write(self, text): """Display text in the shell. Replace line endings with OS-specific endings.""" text = self.fixLineEndings(text) self.AddText(text) self.EnsureCaretVisible() def fixLineEndings(self, text): """Return text with line endings replaced by OS-specific endings.""" lines = text.split("\r\n") for l in range(len(lines)): chunks = lines[l].split("\r") for c in range(len(chunks)): chunks[c] = os.linesep.join(chunks[c].split("\n")) lines[l] = os.linesep.join(chunks) text = os.linesep.join(lines) return text def prompt(self): """Display appropriate prompt for the context, either ps1, ps2 or ps3. If this is a continuation line, autoindent as necessary.""" isreading = self.reader.isreading skip = 0 if isreading: prompt = str(sys.ps3) elif self.more: prompt = str(sys.ps2) else: prompt = str(sys.ps1) pos = self.GetCurLine()[1] if pos > 0: if isreading: skip = 1 else: self.write(os.linesep) if not self.more: self.promptPosStart = self.GetCurrentPos() if not skip: self.write(prompt) if not self.more: self.promptPosEnd = self.GetCurrentPos() # Keep the undo feature from undoing previous responses. self.EmptyUndoBuffer() # XXX Add some autoindent magic here if more. if self.more: self.write(" " * 4) # Temporary hack indentation. self.EnsureCaretVisible() self.ScrollToColumn(0) def readline(self): """Replacement for stdin.readline().""" input = "" reader = self.reader reader.isreading = 1 self.prompt() try: while not reader.input: wx.Yield() input = reader.input finally: reader.input = "" reader.isreading = 0 return input def raw_input(self, prompt=""): """Return string based on user input.""" if prompt: self.write(prompt) return self.readline() def ask(self, prompt="Please enter your response:"): """Get response from the user using a dialog box.""" dialog = wx.TextEntryDialog(None, prompt, "Input Dialog (Raw)", "") try: if dialog.ShowModal() == wxID_OK: text = dialog.GetValue() return text finally: dialog.Destroy() return "" def pause(self): """Halt execution pending a response from the user.""" self.ask("Press enter to continue:") def clear(self): """Delete all text from the shell.""" self.ClearAll() def run(self, command, prompt=1, verbose=1): """Execute command within the shell as if it was typed in directly. >>> shell.run('print "this"') >>> print "this" this >>> """ # Go to the very bottom of the text. endpos = self.GetTextLength() self.SetCurrentPos(endpos) command = command.rstrip() if prompt: self.prompt() if verbose: self.write(command) self.push(command) def runfile(self, filename): """Execute all commands in file as if they were typed into the shell.""" file = open(filename) try: self.prompt() for command in file.readlines(): if command[:6] == "shell.": # Run shell methods silently. self.run(command, prompt=0, verbose=0) else: self.run(command, prompt=0, verbose=1) finally: file.close() def autoCompleteShow(self, command): """Display auto-completion popup list.""" list = self.interp.getAutoCompleteList( command, includeMagic=self.autoCompleteIncludeMagic, includeSingle=self.autoCompleteIncludeSingle, includeDouble=self.autoCompleteIncludeDouble, ) if list: options = "\n".join(list) offset = 0 self.AutoCompShow(offset, options) def autoCallTipShow(self, command): """Display argument spec and docstring in a popup bubble thingie.""" if self.CallTipActive: self.CallTipCancel() (name, argspec, tip) = self.interp.getCallTip(command) if argspec: startpos = self.GetCurrentPos() self.write(argspec + ")") endpos = self.GetCurrentPos() self.SetSelection(endpos, startpos) if tip: curpos = self.GetCurrentPos() tippos = curpos - (len(name) + 1) fallback = curpos - self.GetColumn(curpos) # In case there isn't enough room, only go back to the fallback. tippos = max(tippos, fallback) self.CallTipShow(tippos, tip) def historyShow(self, prefix=""): items = [] for item in self.history: item = item.replace("\n", "\\n") if (prefix == item[: len(prefix)]) and item not in items: items.append(item) self.UserListShow(1, "\n".join(items)) def OnHistorySelected(self, event): command = event.GetText() if command.find("\\n") >= 0: command += "\\n" command = command.replace("\\n", os.linesep + sys.ps2) self.clearCommand() self.write(command) # Process the command if the 'Enter' key was pressed: key = event.GetKey() if key == 28 or key == 1241712: # Is there a 'name' for the Enter key? self.processLine() def topLevelComplete(self): command = self.getCommand(rstrip=0) completions = self.interp.getTopLevelCompletions(command) if len(completions) == 0: return 0 if len(completions) == 1: self.write(completions[0][len(command):]) else: self.AutoCompShow(len(command), "\n".join(completions)) return 1 def writeOut(self, text): """Replacement for stdout.""" self.write(text) def writeErr(self, text): """Replacement for stderr.""" self.write(text) def redirectStdin(self, redirect=1): """If redirect is true then sys.stdin will come from the shell.""" if redirect: sys.stdin = self.reader else: sys.stdin = self.stdin def redirectStdout(self, redirect=1): """If redirect is true then sys.stdout will go to the shell.""" if redirect: sys.stdout = PseudoFileOut(self.writeOut) else: sys.stdout = self.stdout def redirectStderr(self, redirect=1): """If redirect is true then sys.stderr will go to the shell.""" if redirect: sys.stderr = PseudoFileErr(self.writeErr) else: sys.stderr = self.stderr def CanCut(self): """Return true if text is selected and can be cut.""" if ( self.GetSelectionStart() != self.GetSelectionEnd() and self.GetSelectionStart() >= self.promptPosEnd and self.GetSelectionEnd() >= self.promptPosEnd ): return 1 else: return 0 def CanCopy(self): """Return true if text is selected and can be copied.""" return self.GetSelectionStart() != self.GetSelectionEnd() def CanPaste(self): """Return true if a paste should succeed.""" if self.CanEdit() and ( wx.StyledTextCtrl.CanPaste(self) or wx.TheClipboard.IsSupported(PythonObject) ): return 1 else: return 0 def CanEdit(self): """Return true if editing should succeed.""" if self.GetSelectionStart() != self.GetSelectionEnd(): if ( self.GetSelectionStart() >= self.promptPosEnd and self.GetSelectionEnd() >= self.promptPosEnd ): return 1 else: return 0 else: return self.GetCurrentPos() >= self.promptPosEnd def Cut(self): """Remove selection and place it on the clipboard.""" if self.CanCut() and self.CanCopy(): if self.AutoCompActive(): self.AutoCompCancel() if self.CallTipActive: self.CallTipCancel() self.Copy() self.ReplaceSelection("") def Copy(self): """Copy selection and place it on the clipboard.""" if self.CanCopy(): command = self.GetSelectedText() command = command.replace(os.linesep + sys.ps2, os.linesep) command = command.replace(os.linesep + sys.ps1, os.linesep) command = self.lstripPrompt(text=command) data = wxTextDataObject(command) if wx.TheClipboard.Open(): wx.TheClipboard.SetData(data) wx.TheClipboard.Close() def CopyWithPrompts(self): """Copy selection, including prompts, and place it on the clipboard.""" if self.CanCopy(): command = self.GetSelectedText() data = wx.TextDataObject(command) if wx.TheClipboard.Open(): wx.TheClipboard.SetData(data) wx.TheClipboard.Close() def Paste(self): """Replace selection with clipboard contents.""" if self.CanPaste() and wxTheClipboard.Open(): try: if wxTheClipboard.IsSupported(wxDataFormat(wxDF_TEXT)): data = wxTextDataObject() if wx.TheClipboard.GetData(data): self.ReplaceSelection("") command = data.GetText() command = command.rstrip() command = self.fixLineEndings(command) command = self.lstripPrompt(text=command) command = command.replace(os.linesep + sys.ps2, "\n") command = command.replace(os.linesep, "\n") command = command.replace("\n", os.linesep + sys.ps2) self.write(command) if ( wx.TheClipboard.IsSupported(PythonObject) and self.python_obj_paste_handler is not None ): # note that the presence of a PythonObject on the # clipboard is really just a signal to grab the data # from our singleton clipboard instance data = enClipboard.data self.python_obj_paste_handler(data) finally: wx.TheClipboard.Close() def PasteAndRun(self): """Replace selection with clipboard contents, run commands.""" if wx.TheClipboard.Open(): if wx.TheClipboard.IsSupported(wxDataFormat(wxDF_TEXT)): data = wx.TextDataObject() if wx.TheClipboard.GetData(data): endpos = self.GetTextLength() self.SetCurrentPos(endpos) startpos = self.promptPosEnd self.SetSelection(startpos, endpos) self.ReplaceSelection("") text = data.GetText() text = text.strip() text = self.fixLineEndings(text) text = self.lstripPrompt(text=text) text = text.replace(os.linesep + sys.ps1, "\n") text = text.replace(os.linesep + sys.ps2, "\n") text = text.replace(os.linesep, "\n") lines = text.split("\n") commands = [] command = "" for line in lines: if line.strip() != "" and line.lstrip() == line: # New command. if command: # Add the previous command to the list. commands.append(command) # Start a new command, which may be multiline. command = line else: # Multiline command. Add to the command. command += "\n" command += line commands.append(command) for command in commands: command = command.replace("\n", os.linesep + sys.ps2) self.write(command) self.processLine() wx.TheClipboard.Close() def wrap(self, wrap=1): """Sets whether text is word wrapped.""" try: self.SetWrapMode(wrap) except AttributeError: return "Wrapping is not available in this version of PyCrust." def zoom(self, points=0): """Set the zoom level. This number of points is added to the size of all fonts. It may be positive to magnify or negative to reduce.""" self.SetZoom(points) wxID_SELECTALL = wx.NewId() ID_AUTOCOMP = wx.NewId() ID_AUTOCOMP_SHOW = wx.NewId() ID_AUTOCOMP_INCLUDE_MAGIC = wx.NewId() ID_AUTOCOMP_INCLUDE_SINGLE = wx.NewId() ID_AUTOCOMP_INCLUDE_DOUBLE = wx.NewId() ID_CALLTIPS = wx.NewId() ID_CALLTIPS_SHOW = wx.NewId() ID_FILLING = wx.NewId() ID_FILLING_AUTO_UPDATE = wx.NewId() ID_FILLING_SHOW_METHODS = wx.NewId() ID_FILLING_SHOW_CLASS = wx.NewId() ID_FILLING_SHOW_DICT = wx.NewId() ID_FILLING_SHOW_DOC = wx.NewId() ID_FILLING_SHOW_MODULE = wx.NewId() class ShellMenu(object): """Mixin class to add standard menu items.""" def createMenus(self): m = self.fileMenu = wxMenu() m.AppendSeparator() m.Append(wxID_EXIT, "E&xit", "Exit PyCrust") m = self.editMenu = wxMenu() m.Append(wxID_UNDO, "&Undo \tCtrl+Z", "Undo the last action") m.Append(wxID_REDO, "&Redo \tCtrl+Y", "Redo the last undone action") m.AppendSeparator() m.Append(wxID_CUT, "Cu&t \tCtrl+X", "Cut the selection") m.Append(wxID_COPY, "&Copy \tCtrl+C", "Copy the selection") m.Append(wxID_PASTE, "&Paste \tCtrl+V", "Paste") m.AppendSeparator() m.Append(wxID_CLEAR, "Cle&ar", "Delete the selection") m.Append(wxID_SELECTALL, "Select A&ll", "Select all text") m = self.autocompMenu = wxMenu() m.Append( ID_AUTOCOMP_SHOW, "Show Auto Completion", "Show auto completion during dot syntax", 1, ) m.Append( ID_AUTOCOMP_INCLUDE_MAGIC, "Include Magic Attributes", "Include attributes visible to __getattr__ and __setattr__", 1, ) m.Append( ID_AUTOCOMP_INCLUDE_SINGLE, "Include Single Underscores", "Include attibutes prefixed by a single underscore", 1, ) m.Append( ID_AUTOCOMP_INCLUDE_DOUBLE, "Include Double Underscores", "Include attibutes prefixed by a double underscore", 1, ) m = self.calltipsMenu = wxMenu() m.Append( ID_CALLTIPS_SHOW, "Show Call Tips", "Show call tips with argument specifications", 1, ) m = self.optionsMenu = wxMenu() m.AppendMenu( ID_AUTOCOMP, "&Auto Completion", self.autocompMenu, "Auto Completion Options", ) m.AppendMenu( ID_CALLTIPS, "&Call Tips", self.calltipsMenu, "Call Tip Options" ) if hasattr(self, "crust"): fm = self.fillingMenu = wxMenu() fm.Append( ID_FILLING_AUTO_UPDATE, "Automatic Update", "Automatically update tree view after each command", 1, ) fm.Append( ID_FILLING_SHOW_METHODS, "Show Methods", "Show methods and functions in the tree view", 1, ) fm.Append( ID_FILLING_SHOW_CLASS, "Show __class__", "Show __class__ entries in the tree view", 1, ) fm.Append( ID_FILLING_SHOW_DICT, "Show __dict__", "Show __dict__ entries in the tree view", 1, ) fm.Append( ID_FILLING_SHOW_DOC, "Show __doc__", "Show __doc__ entries in the tree view", 1, ) fm.Append( ID_FILLING_SHOW_MODULE, "Show __module__", "Show __module__ entries in the tree view", 1, ) m.AppendMenu(ID_FILLING, "&Filling", fm, "Filling Options") m = self.helpMenu = wxMenu() m.AppendSeparator() m.Append(wxID_ABOUT, "&About...", "About PyCrust") b = self.menuBar = wxMenuBar() b.Append(self.fileMenu, "&File") b.Append(self.editMenu, "&Edit") b.Append(self.optionsMenu, "&Options") b.Append(self.helpMenu, "&Help") self.SetMenuBar(b) self.Bind(EVT_MENU, self.OnExit, id=wx.ID_EXIT) self.Bind(EVT_MENU, self.OnUndo, id=wx.ID_UNDO) self.Bind(EVT_MENU, self.OnRedo, id=wx.ID_REDO) self.Bind(EVT_MENU, self.OnCut, id=wx.ID_CUT) self.Bind(EVT_MENU, self.OnCopy, id=wx.ID_COPY) self.Bind(EVT_MENU, self.OnPaste, id=wx.ID_PASTE) self.Bind(EVT_MENU, self.OnClear, id=wx.ID_CLEAR) self.Bind(EVT_MENU, self.OnSelectAll, id=wx.ID_SELECTALL) self.Bind(EVT_MENU, self.OnAbout, id=wx.ID_ABOUT) self.Bind(EVT_MENU, self.OnAutoCompleteShow, ID_AUTOCOMP_SHOW) self.Bind( EVT_MENU, self.OnAutoCompleteIncludeMagic, id=ID_AUTOCOMP_INCLUDE_MAGIC, ) self.Bind( EVT_MENU, self.OnAutoCompleteIncludeSingle, id=ID_AUTOCOMP_INCLUDE_SINGLE, ) self.Bind( EVT_MENU, self.OnAutoCompleteIncludeDouble, id=ID_AUTOCOMP_INCLUDE_DOUBLE, ) self.Bind(EVT_MENU, self.OnCallTipsShow, id=ID_CALLTIPS_SHOW) self.Bind(EVT_UPDATE_UI, self.OnUpdateMenu, id=wx.ID_UNDO) self.Bind(EVT_UPDATE_UI, self.OnUpdateMenu, id=wx.ID_REDO) self.Bind(EVT_UPDATE_UI, self.OnUpdateMenu, id=wx.ID_CUT) self.Bind(EVT_UPDATE_UI, self.OnUpdateMenu, id=wx.ID_COPY) self.Bind(EVT_UPDATE_UI, self.OnUpdateMenu, id=wx.ID_PASTE) self.Bind(EVT_UPDATE_UI, self.OnUpdateMenu, id=wx.ID_CLEAR) self.Bind(EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_AUTOCOMP_SHOW) self.Bind( EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_AUTOCOMP_INCLUDE_MAGIC ) self.Bind( EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_AUTOCOMP_INCLUDE_SINGLE ) self.Bind( EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_AUTOCOMP_INCLUDE_DOUBLE ) self.Bind(EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_CALLTIPS_SHOW) if hasattr(self, "crust"): self.Bind( EVT_MENU, self.OnFillingAutoUpdate, id=ID_FILLING_AUTO_UPDATE ) self.Bind( EVT_MENU, self.OnFillingShowMethods, id=ID_FILLING_SHOW_METHODS ) self.Bind( EVT_MENU, self.OnFillingShowClass, id=ID_FILLING_SHOW_CLASS ) self.Bind( EVT_MENU, self.OnFillingShowDict, id=ID_FILLING_SHOW_DICT ) self.Bind(EVT_MENU, self.OnFillingShowDoc, id=ID_FILLING_SHOW_DOC) self.Bind( EVT_MENU, self.OnFillingShowModule, id=ID_FILLING_SHOW_MODULE ) self.Bind( EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_FILLING_AUTO_UPDATE ) self.Bind( EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_FILLING_SHOW_METHODS ) self.Bind( EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_FILLING_SHOW_CLASS ) self.Bind( EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_FILLING_SHOW_DICT ) self.Bind(EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_FILLING_SHOW_DOC) self.Bind( EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_FILLING_SHOW_MODULE ) def OnExit(self, event): self.Close(True) def OnUndo(self, event): self.shell.Undo() def OnRedo(self, event): self.shell.Redo() def OnCut(self, event): self.shell.Cut() def OnCopy(self, event): self.shell.Copy() def OnPaste(self, event): self.shell.Paste() def OnClear(self, event): self.shell.Clear() def OnSelectAll(self, event): self.shell.SelectAll() def OnAbout(self, event): """Display an About PyCrust window.""" import sys title = "About PyCrust" text = ( "PyCrust %s\n\n" % VERSION + "Yet another Python shell, only flakier.\n\n" + "Half-baked by Patrick K. O'Brien,\n" + "the other half is still in the oven.\n\n" + "Shell Revision: %s\n" % self.shell.revision + "Interpreter Revision: %s\n\n" % self.shell.interp.revision + "Python Version: %s\n" % sys.version.split()[0] + "wxPython Version: %s\n" % wx.__version__ + "Platform: %s\n" % sys.platform ) dialog = wxMessageDialog(self, text, title, wxOK | wxICON_INFORMATION) dialog.ShowModal() dialog.Destroy() def OnAutoCompleteShow(self, event): self.shell.autoComplete = event.IsChecked() def OnAutoCompleteIncludeMagic(self, event): self.shell.autoCompleteIncludeMagic = event.IsChecked() def OnAutoCompleteIncludeSingle(self, event): self.shell.autoCompleteIncludeSingle = event.IsChecked() def OnAutoCompleteIncludeDouble(self, event): self.shell.autoCompleteIncludeDouble = event.IsChecked() def OnCallTipsShow(self, event): self.shell.autoCallTip = event.IsChecked() def OnFillingAutoUpdate(self, event): tree = self.crust.filling.fillingTree tree.autoUpdate = event.IsChecked() tree.if_autoUpdate() def OnFillingShowMethods(self, event): tree = self.crust.filling.fillingTree tree.showMethods = event.IsChecked() tree.update() def OnFillingShowClass(self, event): tree = self.crust.filling.fillingTree tree.showClass = event.IsChecked() tree.update() def OnFillingShowDict(self, event): tree = self.crust.filling.fillingTree tree.showDict = event.IsChecked() tree.update() def OnFillingShowDoc(self, event): tree = self.crust.filling.fillingTree tree.showDoc = event.IsChecked() tree.update() def OnFillingShowModule(self, event): tree = self.crust.filling.fillingTree tree.showModule = event.IsChecked() tree.update() def OnUpdateMenu(self, event): """Update menu items based on current status.""" id = event.GetId() if id == wxID_UNDO: event.Enable(self.shell.CanUndo()) elif id == wxID_REDO: event.Enable(self.shell.CanRedo()) elif id == wxID_CUT: event.Enable(self.shell.CanCut()) elif id == wxID_COPY: event.Enable(self.shell.CanCopy()) elif id == wxID_PASTE: event.Enable(self.shell.CanPaste()) elif id == wxID_CLEAR: event.Enable(self.shell.CanCut()) elif id == ID_AUTOCOMP_SHOW: event.Check(self.shell.autoComplete) elif id == ID_AUTOCOMP_INCLUDE_MAGIC: event.Check(self.shell.autoCompleteIncludeMagic) elif id == ID_AUTOCOMP_INCLUDE_SINGLE: event.Check(self.shell.autoCompleteIncludeSingle) elif id == ID_AUTOCOMP_INCLUDE_DOUBLE: event.Check(self.shell.autoCompleteIncludeDouble) elif id == ID_CALLTIPS_SHOW: event.Check(self.shell.autoCallTip) elif id == ID_FILLING_AUTO_UPDATE: event.Check(self.crust.filling.fillingTree.autoUpdate) elif id == ID_FILLING_SHOW_METHODS: event.Check(self.crust.filling.fillingTree.showMethods) elif id == ID_FILLING_SHOW_CLASS: event.Check(self.crust.filling.fillingTree.showClass) elif id == ID_FILLING_SHOW_DICT: event.Check(self.crust.filling.fillingTree.showDict) elif id == ID_FILLING_SHOW_DOC: event.Check(self.crust.filling.fillingTree.showDoc) elif id == ID_FILLING_SHOW_MODULE: event.Check(self.crust.filling.fillingTree.showModule) pyface-7.4.0/pyface/wx/__init__.py0000644000076500000240000000062714176222673017767 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! pyface-7.4.0/pyface/wx/font.py0000644000076500000240000000364214176222673017176 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 utilities. """ import wx def clone_font(font): """ Clones the specified font. """ point_size = font.GetPointSize() family = font.GetFamily() style = font.GetStyle() weight = font.GetWeight() underline = font.GetUnderlined() face_name = font.GetFaceName() clone = wx.Font(point_size, family, style, weight, underline, face_name) return clone def set_font_size(window, size): """ Recursively sets the font size starting from 'window'. """ font = window.GetFont() clone = clone_font(font) clone.SetPointSize(size) window.SetFont(clone) sizer = window.GetSizer() if sizer is not None: sizer.Layout() window.Refresh() for child in window.GetChildren(): set_font_size(child, size) def increase_font_size(window, delta=2): """ Recursively increases the font size starting from 'window'. """ font = window.GetFont() clone = clone_font(font) clone.SetPointSize(font.GetPointSize() + delta) window.SetFont(clone) sizer = window.GetSizer() if sizer is not None: sizer.Layout() window.Refresh() for child in window.GetChildren(): increase_font_size(child, delta) def decrease_font_size(window, delta=2): """ Recursively decreases the font size starting from 'window'. """ increase_font_size(window, delta=-2) def set_bold_font(window): """ Set 'window's font to be bold. """ font = window.GetFont() font.SetWeight(wx.BOLD) window.SetFont(font) return pyface-7.4.0/pyface/wx/switcher.py0000644000076500000240000001775014176222673020065 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Classes to provide a switcher. """ # paranoid checkin in case Mr Chilver's changes break the distribution code # todo - it wasn't paranoia - reconcile this with lazy_switcher.py at some point import wx from wx.lib.scrolledpanel import ScrolledPanel as wxScrolledPanel from traits.api import HasTraits class SwitcherModel(HasTraits): """ Base class for switcher models. """ __traits__ = { # The index of the selected 'page'. "selected": -1 } def __init__(self): """ Creates a new switcher model. """ # The items to display in the switcher control. self.items = [] # (str label, object value) return # ------------------------------------------------------------------------ # 'SwitcherModel' interface. # ------------------------------------------------------------------------ def create_page(self, parent, index): """ Creates a page for the switcher panel. """ raise NotImplementedError() class SwitcherControl(wx.Panel): """ The default switcher control (a combo box). """ def __init__(self, parent, id, model, label=None, **kw): """ Create a new switcher control. """ # Base-class constructor. wx.Panel.__init__(self, parent, id, **kw) # The switcher model that we are a controller for. self.model = model # The optional label. self.label = label # Create the widget! self._create_widget(model, label) # Listen for when the selected item in the model is changed. model.observe(self._on_selected_changed, "selected") return # ------------------------------------------------------------------------ # Trait event handlers. # ------------------------------------------------------------------------ def _on_selected_changed(self, event): """ Called when the selected item in the model is changed. """ selected = event.new self.combo.SetSelection(selected) return # ------------------------------------------------------------------------ # wx event handlers. # ------------------------------------------------------------------------ def _on_combobox(self, event): """ Called when the combo box selection is changed. """ combo = event.GetEventObject() # Update the model. self.model.selected = combo.GetSelection() return # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _create_widget(self, model, label): """ Creates the widget. """ self.sizer = sizer = wx.BoxSizer(wx.VERTICAL) self.SetSizer(sizer) self.SetAutoLayout(True) # Switcher combo. sizer.Add(self._combo(self, model, label), 1, wx.EXPAND) # Resize the panel to match the sizer's minimal size. sizer.Fit(self) def _combo(self, parent, model, label): """ Creates the switcher combo box. """ sizer = wx.BoxSizer(wx.HORIZONTAL) # Label. if label is not None: text = wx.StaticText(parent, -1, label) sizer.Add(text, 0, wx.ALIGN_CENTER | wx.ALL, 5) # Combo. self.combo = combo = wx.ComboBox( parent, -1, style=wx.CB_DROPDOWN | wx.CB_READONLY ) sizer.Add(combo, 1, wx.EXPAND | wx.ALIGN_CENTER | wx.ALL, 5) # Ask the model for the available options. items = model.items if len(items) > 0: for name, data in model.items: combo.Append(name, data) # Listen for changes to the selected item. combo.Bind(wx.EVT_COMBOBOX, self._on_combobox) # If the model's selected variable has been set ... if model.selected != -1: combo.SetSelection(model.selected) return sizer class SwitcherPanel(wxScrolledPanel): """ The default switcher panel. """ def __init__(self, parent, id, model, label=None, cache=True, **kw): # Base-class constructor. wxScrolledPanel.__init__(self, parent, id, **kw) self.SetupScrolling() # The switcher model that we are a panel for. self.model = model # Should we cache pages as we create them? self.cache = cache # The page cache (if caching was requested). self._page_cache = {} # The currently displayed page. self.current = None # Create the widget! self._create_widget(model, label) # Listen for when the selected item in the model is changed. model.observe(self._on_selected_changed, "selected") return # ------------------------------------------------------------------------ # Trait event handlers. # ------------------------------------------------------------------------ def _on_selected_changed(self, event): """ Called when the selected item in the model is changed. """ selected = event.new self._show_page(selected) return # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _create_widget(self, model, label): """ Creates the widget. """ self.sizer = sizer = wx.BoxSizer(wx.VERTICAL) self.SetSizer(sizer) self.SetAutoLayout(True) if model.selected != -1: self._show_page(model.selected) # Nothing to add here as we add the panel contents lazily! pass # Resize the panel to match the sizer's minimal size. sizer.Fit(self) def _show_page(self, index): """ Shows the page at the specified index. """ # If a page is already displayed then hide it. if self.current is not None: self.current.Show(False) self.sizer.Remove(self.current) # Is the page in the cache? page = self._page_cache.get(index) if not self.cache or page is None: # If not then ask our panel factory to create it. page = self.model.create_page(self, index) # Add it to the cache! self._page_cache[index] = page # Display the page. self.sizer.Add(page, 1, wx.EXPAND) page.Show(True) self.current = page # Force a new layout of the sizer's children but KEEPING the current # dimension. self.sizer.Layout() class Switcher(wx.Panel): """ A switcher. """ def __init__(self, parent, id, model, label=None, **kw): # Base-class constructor. wx.Panel.__init__(self, parent, id, **kw) # The model that we are a switcher for. self.model = model # Create the widget! self._create_widget(model, label) return # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _create_widget(self, model, label): """ Creates the widget. """ self.sizer = sizer = wx.BoxSizer(wx.VERTICAL) self.SetSizer(sizer) self.SetAutoLayout(True) # Switcher control. self.control = control = SwitcherControl(self, -1, model, label) sizer.Add(control, 0, wx.EXPAND) # Switcher panel. self.panel = panel = SwitcherPanel(self, -1, model, label) sizer.Add(panel, 1, wx.EXPAND) # Resize the panel to match the sizer's minimal size. sizer.Fit(self) return pyface-7.4.0/pyface/wx/python_stc.py0000644000076500000240000003501514176222673020421 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 wx import stc import keyword # ---------------------------------------------------------------------- demoText = """\ ## This version of the editor has been set up to edit Python source ## code. Here is a copy of wxPython/demo/Main.py to play with. """ # ---------------------------------------------------------------------- if wx.Platform == "__WXMSW__": faces = { "times": "Times New Roman", "mono": "Courier New", "helv": "Arial", "other": "Comic Sans MS", "size": 10, "size2": 8, } else: faces = { "times": "Times", "mono": "Courier", "helv": "Helvetica", "other": "new century schoolbook", "size": 12, "size2": 10, } # ---------------------------------------------------------------------- class PythonSTC(stc.StyledTextCtrl): def __init__(self, parent, ID): stc.StyledTextCtrl.__init__( self, parent, ID, style=wx.NO_FULL_REPAINT_ON_RESIZE ) self.CmdKeyAssign(ord("B"), stc.STC_SCMOD_CTRL, stc.STC_CMD_ZOOMIN) self.CmdKeyAssign(ord("N"), stc.STC_SCMOD_CTRL, stc.STC_CMD_ZOOMOUT) self.SetLexer(stc.STC_LEX_PYTHON) self.SetKeyWords(0, " ".join(keyword.kwlist)) self.SetProperty("fold", "1") self.SetProperty("tab.timmy.whinge.level", "1") self.SetMargins(0, 0) self.SetViewWhiteSpace(False) # self.SetBufferedDraw(False) self.SetEdgeMode(stc.STC_EDGE_BACKGROUND) self.SetEdgeColumn(78) # Setup a margin to hold fold markers # WHAT IS THIS VALUE? WHAT ARE THE OTHER FLAGS? DOES IT MATTER? self.SetFoldFlags(16) # mic # self.SetMarginType(2, stc.STC_MARGIN_SYMBOL) # self.SetMarginMask(2, stc.STC_MASK_FOLDERS) # self.SetMarginSensitive(2, True) # self.SetMarginWidth(2, 12) # line numbers in the margin self.SetMarginType(1, stc.STC_MARGIN_NUMBER) self.SetMarginWidth(1, 25) if 0: # simple folder marks, like the old version self.MarkerDefine( stc.STC_MARKNUM_FOLDER, stc.STC_MARK_ARROW, "navy", "navy" ) self.MarkerDefine( stc.STC_MARKNUM_FOLDEROPEN, stc.STC_MARK_ARROWDOWN, "navy", "navy", ) # Set these to an invisible mark self.MarkerDefine( stc.STC_MARKNUM_FOLDEROPENMID, stc.STC_MARK_BACKGROUND, "white", "black", ) self.MarkerDefine( stc.STC_MARKNUM_FOLDERMIDTAIL, stc.STC_MARK_BACKGROUND, "white", "black", ) self.MarkerDefine( stc.STC_MARKNUM_FOLDERSUB, stc.STC_MARK_BACKGROUND, "white", "black", ) self.MarkerDefine( stc.STC_MARKNUM_FOLDERTAIL, stc.STC_MARK_BACKGROUND, "white", "black", ) elif 0: # more involved "outlining" folder marks self.MarkerDefine( stc.STC_MARKNUM_FOLDEREND, stc.STC_MARK_BOXPLUSCONNECTED, "white", "black", ) self.MarkerDefine( stc.STC_MARKNUM_FOLDEROPENMID, stc.STC_MARK_BOXMINUSCONNECTED, "white", "black", ) self.MarkerDefine( stc.STC_MARKNUM_FOLDERMIDTAIL, stc.STC_MARK_TCORNER, "white", "black", ) self.MarkerDefine( stc.STC_MARKNUM_FOLDERTAIL, stc.STC_MARK_LCORNER, "white", "black", ) self.MarkerDefine( stc.STC_MARKNUM_FOLDERSUB, stc.STC_MARK_VLINE, "white", "black" ) self.MarkerDefine( stc.STC_MARKNUM_FOLDER, stc.STC_MARK_BOXPLUS, "white", "black" ) self.MarkerDefine( stc.STC_MARKNUM_FOLDEROPEN, stc.STC_MARK_BOXMINUS, "white", "black", ) self.Bind(stc.EVT_STC_UPDATEUI, self.OnUpdateUI) self.Bind(stc.EVT_STC_MARGINCLICK, self.OnMarginClick) # Make some styles, The lexer defines what each style is used for, we # just have to define what each style looks like. This set is adapted # from Scintilla sample property files. self.StyleClearAll() # Global default styles for all languages self.StyleSetSpec( stc.STC_STYLE_DEFAULT, "face:%(helv)s,size:%(size)d" % faces ) self.StyleSetSpec( stc.STC_STYLE_LINENUMBER, "back:#C0C0C0,face:%(helv)s,size:%(size2)d" % faces, ) self.StyleSetSpec(stc.STC_STYLE_CONTROLCHAR, "face:%(other)s" % faces) self.StyleSetSpec( stc.STC_STYLE_BRACELIGHT, "fore:#FFFFFF,back:#0000FF,bold" ) self.StyleSetSpec( stc.STC_STYLE_BRACEBAD, "fore:#000000,back:#FF0000,bold" ) # Python styles # White space self.StyleSetSpec( stc.STC_P_DEFAULT, "fore:#808080,face:%(helv)s,size:%(size)d" % faces, ) # Comment self.StyleSetSpec( stc.STC_P_COMMENTLINE, "fore:#007F00,face:%(other)s,size:%(size)d" % faces, ) # Number self.StyleSetSpec( stc.STC_P_NUMBER, "fore:#007F7F,size:%(size)d" % faces ) # String self.StyleSetSpec( stc.STC_P_STRING, "fore:#7F007F,italic,face:%(times)s,size:%(size)d" % faces, ) # Single quoted string self.StyleSetSpec( stc.STC_P_CHARACTER, "fore:#7F007F,italic,face:%(times)s,size:%(size)d" % faces, ) # Keyword self.StyleSetSpec( stc.STC_P_WORD, "fore:#00007F,bold,size:%(size)d" % faces ) # Triple quotes self.StyleSetSpec( stc.STC_P_TRIPLE, "fore:#7F0000,size:%(size)d" % faces ) # Triple double quotes self.StyleSetSpec( stc.STC_P_TRIPLEDOUBLE, "fore:#7F0000,size:%(size)d" % faces ) # Class name definition self.StyleSetSpec( stc.STC_P_CLASSNAME, "fore:#0000FF,bold,underline,size:%(size)d" % faces, ) # Function or method name definition self.StyleSetSpec( stc.STC_P_DEFNAME, "fore:#007F7F,bold,size:%(size)d" % faces ) # Operators self.StyleSetSpec(stc.STC_P_OPERATOR, "bold,size:%(size)d" % faces) # Identifiers self.StyleSetSpec( stc.STC_P_IDENTIFIER, "fore:#808080,face:%(helv)s,size:%(size)d" % faces, ) # Comment-blocks self.StyleSetSpec( stc.STC_P_COMMENTBLOCK, "fore:#7F7F7F,size:%(size)d" % faces ) # End of line where string is not closed self.StyleSetSpec( stc.STC_P_STRINGEOL, "fore:#000000,face:%(mono)s,back:#E0C0E0,eol,size:%(size)d" % faces, ) self.SetCaretForeground("BLUE") self.Bind(wx.EVT_KEY_DOWN, self.OnKeyPressed) def OnKeyPressed(self, event): if self.CallTipActive(): self.CallTipCancel() key = event.KeyCode if key == 32 and event.ControlDown(): pos = self.GetCurrentPos() # Tips if event.ShiftDown(): self.CallTipSetBackground("yellow") self.CallTipShow(pos, "param1, param2") # Code completion else: # fixme: What is this mess!!! # lst = [] # for x in range(50000): # lst.append('%05d' % x) # st = " ".join(lst) # print len(st) # self.AutoCompShow(0, st) # fixme: What is this mess!!! kw = keyword.kwlist[:] kw.append("zzzzzz") kw.append("aaaaa") kw.append("__init__") kw.append("zzaaaaa") kw.append("zzbaaaa") kw.append("this_is_a_longer_value") kw.append("this_is_a_much_much_much_much_longer_value") kw.sort() # Python sorts are case sensitive self.AutoCompSetIgnoreCase(False) # so this needs to match self.AutoCompShow(0, " ".join(kw)) else: event.Skip() def OnUpdateUI(self, evt): # check for matching braces braceAtCaret = -1 braceOpposite = -1 charBefore = None caretPos = self.GetCurrentPos() if caretPos > 0: charBefore = self.GetCharAt(caretPos - 1) styleBefore = self.GetStyleAt(caretPos - 1) # check before if ( charBefore and chr(charBefore) in "[]{}()" and styleBefore == stc.STC_P_OPERATOR ): braceAtCaret = caretPos - 1 # check after if braceAtCaret < 0: charAfter = self.GetCharAt(caretPos) styleAfter = self.GetStyleAt(caretPos) if ( charAfter and chr(charAfter) in "[]{}()" and styleAfter == stc.STC_P_OPERATOR ): braceAtCaret = caretPos if braceAtCaret >= 0: braceOpposite = self.BraceMatch(braceAtCaret) if braceAtCaret != -1 and braceOpposite == -1: self.BraceBadLight(braceAtCaret) else: self.BraceHighlight(braceAtCaret, braceOpposite) # pt = self.PointFromPosition(braceOpposite) # self.Refresh(True, Rect(pt.x, pt.y, 5,5)) # print pt # self.Refresh(False) def OnMarginClick(self, evt): # fold and unfold as needed if evt.GetMargin() == 2: if evt.GetShift() and evt.GetControl(): self.FoldAll() else: lineClicked = self.LineFromPosition(evt.GetPosition()) if ( self.GetFoldLevel(lineClicked) & stc.STC_FOLDLEVELHEADERFLAG ): if evt.GetShift(): self.SetFoldExpanded(lineClicked, True) self.Expand(lineClicked, True, True, 1) elif evt.GetControl(): if self.GetFoldExpanded(lineClicked): self.SetFoldExpanded(lineClicked, False) self.Expand(lineClicked, False, True, 0) else: self.SetFoldExpanded(lineClicked, True) self.Expand(lineClicked, True, True, 100) else: self.ToggleFold(lineClicked) def FoldAll(self): lineCount = self.GetLineCount() expanding = True # find out if we are folding or unfolding for lineNum in range(lineCount): if self.GetFoldLevel(lineNum) & stc.STC_FOLDLEVELHEADERFLAG: expanding = not self.GetFoldExpanded(lineNum) break lineNum = 0 while lineNum < lineCount: level = self.GetFoldLevel(lineNum) if ( level & stc.STC_FOLDLEVELHEADERFLAG and (level & stc.STC_FOLDLEVELNUMBERMASK) == stc.STC_FOLDLEVELBASE ): if expanding: self.SetFoldExpanded(lineNum, True) lineNum = self.Expand(lineNum, True) lineNum = lineNum - 1 else: lastChild = self.GetLastChild(lineNum, -1) self.SetFoldExpanded(lineNum, False) if lastChild > lineNum: self.HideLines(lineNum + 1, lastChild) lineNum = lineNum + 1 def Expand(self, line, doExpand, force=False, visLevels=0, level=-1): lastChild = self.GetLastChild(line, level) line = line + 1 while line <= lastChild: if force: if visLevels > 0: self.ShowLines(line, line) else: self.HideLines(line, line) else: if doExpand: self.ShowLines(line, line) if level == -1: level = self.GetFoldLevel(line) if level & stc.STC_FOLDLEVELHEADERFLAG: if force: if visLevels > 1: self.SetFoldExpanded(line, True) else: self.SetFoldExpanded(line, False) line = self.Expand(line, doExpand, force, visLevels - 1) else: if doExpand and self.GetFoldExpanded(line): line = self.Expand(line, True, force, visLevels - 1) else: line = self.Expand(line, False, force, visLevels - 1) else: line = line + 1 return line # ---------------------------------------------------------------------- _USE_PANEL = 1 def runTest(frame, nb, log): if not _USE_PANEL: ed = p = stc.PythonSTC(nb, -1) else: p = wx.Panel(nb, -1, style=wx.NO_FULL_REPAINT_ON_RESIZE) ed = PythonSTC(p, -1) s = wx.BoxSizer(wx.HORIZONTAL) s.Add(ed, 1, wx.EXPAND) p.SetSizer(s) p.SetAutoLayout(True) ed.SetText(demoText + open("Main.py").read()) ed.EmptyUndoBuffer() ed.Colourise(0, -1) # line numbers in the margin ed.SetMarginType(1, stc.STC_MARGIN_NUMBER) ed.SetMarginWidth(1, 25) return p # ---------------------------------------------------------------------- overview = """\ Once again, no docs yet. Sorry. But this and this should be helpful. """ pyface-7.4.0/pyface/wx/sized_panel.py0000644000076500000240000000256614176222673020531 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 panel sized by a sizer. """ import wx class SizedPanel(wx.Panel): """ A panel sized by a sizer. """ def __init__(self, parent, wxid, sizer, **kw): """ Creates a new sized panel. """ # Base-class constructor. wx.Panel.__init__(self, parent, wxid, **kw) # Set up the panel's sizer. self.SetSizer(sizer) self.SetAutoLayout(True) # A quick reference to our sizer (at least quicker than using # 'self.GetSizer()' ;^). self.sizer = sizer return # ------------------------------------------------------------------------ # 'SizedPanel' interface. # ------------------------------------------------------------------------ def Fit(self): """ Resizes the panel to match the sizer's minimal size. """ self.sizer.Fit(self) def Layout(self): """ Lays out the sizer without changing the panel geometry. """ self.sizer.Layout() return pyface-7.4.0/pyface/wx/pager.py0000644000076500000240000000561714176222673017332 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 pager contains a set of pages, but only shows one at a time. """ import wx from wx.lib.scrolledpanel import ScrolledPanel as wxScrolledPanel class Pager(wxScrolledPanel): """ A pager contains a set of pages, but only shows one at a time. """ def __init__(self, parent, wxid, **kw): """ Creates a new pager. """ # Base-class constructor. wxScrolledPanel.__init__(self, parent, wxid, **kw) self.SetupScrolling() # The pages in the pager! self._pages = {} # { str name : wx.Window page } # The page that is currently displayed. self._current_page = None # Create the widget! self._create_widget() return # ------------------------------------------------------------------------ # 'Pager' interface. # ------------------------------------------------------------------------ def add_page(self, name, page): """ Adds a page with the specified name. """ self._pages[name] = page # Make the pager panel big enought ot hold the biggest page. # # fixme: I have a feeling this needs some testing! sw, sh = self.GetSize() pw, ph = page.GetSize() self.SetSize((max(sw, pw), max(sh, ph))) # All pages are added as hidden. Use 'show_page' to make a page # visible. page.Show(False) return page def show_page(self, name): """ Shows the page with the specified name. """ # Hide the current page (if one is displayed). if self._current_page is not None: self._hide_page(self._current_page) # Show the specified page. page = self._show_page(self._pages[name]) # Resize the panel to match the sizer's minimal size. self._sizer.Fit(self) return page # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _create_widget(self): """ Creates the widget. """ self._sizer = sizer = wx.BoxSizer(wx.VERTICAL) self.SetSizer(sizer) self.SetAutoLayout(True) def _hide_page(self, page): """ Hides the specified page. """ page.Show(False) self._sizer.Remove(page) def _show_page(self, page): """ Shows the specified page. """ page.Show(True) self._sizer.Add(page, 1, wx.EXPAND) self._current_page = page return page pyface-7.4.0/pyface/wx/lazy_switcher.py0000644000076500000240000001714314176222673021120 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Classes to provide a switcher. """ import wx from wx.lib.scrolledpanel import ScrolledPanel as wxScrolledPanel from traits.api import HasTraits, Int class SwitcherModel(HasTraits): """ Base class for switcher models. """ # The index of the selected 'page'. selected = Int(-1) def __init__(self): """ Creates a new switcher model. """ # The items to display in the switcher control. self.items = [] # (str label, object value) return # ------------------------------------------------------------------------ # 'SwitcherModel' interface. # ------------------------------------------------------------------------ def create_page(self, parent, index): """ Creates a page for the switcher panel. """ raise NotImplementedError() class SwitcherControl(wx.Panel): """ The default switcher control (a combo box). """ def __init__(self, parent, id, model, label=None, **kw): """ Creates a new switcher control. """ # Base-class constructor. wx.Panel.__init__(self, parent, id, **kw) # The switcher model that we are a controller for. self.model = model # The optional label. self.label = label # Create the widget! self._create_widget(model, label) # Listen for when the selected item in the model is changed. model.observe(self._on_selected_changed, "selected") return # ------------------------------------------------------------------------ # Trait event handlers. # ------------------------------------------------------------------------ def _on_selected_changed(self, event): """ Called when the selected item in the model is changed. """ selected = event.new self.combo.SetSelection(selected) return # ------------------------------------------------------------------------ # wx event handlers. # ------------------------------------------------------------------------ def _on_combobox(self, event): """ Called when the combo box selection is changed. """ combo = event.GetEventObject() # Update the model. self.model.selected = combo.GetSelection() return # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _create_widget(self, model, label): """ Creates the widget.""" self.sizer = sizer = wx.BoxSizer(wx.VERTICAL) self.SetSizer(sizer) self.SetAutoLayout(True) # Switcher combo. sizer.Add(self._combo(self, model, label), 1, wx.EXPAND) # Resize the panel to match the sizer's minimal size. sizer.Fit(self) def _combo(self, parent, model, label): """ Creates the switcher combo. """ sizer = wx.BoxSizer(wx.HORIZONTAL) # Label. if label is not None: text = wx.StaticText(parent, -1, label) sizer.Add(text, 0, wx.ALIGN_CENTER | wx.ALL, 5) # Combo. self.combo = combo = wx.ComboBox( parent, -1, style=wx.CB_DROPDOWN | wx.CB_READONLY ) sizer.Add(combo, 1, wx.EXPAND | wx.ALIGN_CENTER | wx.ALL, 5) # Ask the model for the available options. items = model.items if len(items) > 0: for name, data in model.items: combo.Append(name, data) # Listen for changes to the selected item. self.Bind(wx.EVT_COMBOBOX, self._on_combobox, id=combo.GetId()) # If the model's selected variable has been set ... if model.selected != -1: combo.SetSelection(model.selected) return sizer class SwitcherPanel(wxScrolledPanel): """ The default switcher panel. """ def __init__(self, parent, id, model, label=None, cache=True, **kw): # Base-class constructor. wxScrolledPanel.__init__(self, parent, id, **kw) self.SetupScrolling() # The switcher model that we are a panel for. self.model = model # Should we cache pages as we create them? self.cache = cache # The page cache (if caching was requested). self._page_cache = {} # The currently displayed page. self.current = None # Create the widget! self._create_widget(model, label) return # ------------------------------------------------------------------------ # 'SwitcherPanel' interface. # ------------------------------------------------------------------------ def show_page(self, index): """ Shows the page at the specified index. """ self._show_page(index) return # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _create_widget(self, model, label): """ Creates the widget. """ self.sizer = sizer = wx.BoxSizer(wx.VERTICAL) self.SetSizer(sizer) self.SetAutoLayout(True) # Nothing to add here as we add the panel contents lazily! pass # Resize the panel to match the sizer's minimal size. sizer.Fit(self) def _show_page(self, index): """ Shows the page at the specified index. """ # If a page is already displayed then hide it. if self.current is not None: self.current.Show(False) self.sizer.Remove(self.current) # Is the page in the cache? page = self._page_cache.get(index) if not self.cache or page is None: # If not then ask our panel factory to create it. page = self.model.create_page(self, index) # Add it to the cache! self._page_cache[index] = page # Display the page. self.sizer.Add(page, 15, wx.EXPAND, 5) page.Show(True) self.current = page # Force a new layout of the sizer's children but KEEPING the current # dimension. self.sizer.Layout() class Switcher(wx.Panel): """ A switcher. """ def __init__(self, parent, id, model, label=None, **kw): """ Create a new switcher. """ # Base-class constructor. wx.Panel.__init__(self, parent, id, **kw) # The model that we are a switcher for. self.model = model # Create the widget! self._create_widget(model, label) return # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _create_widget(self, model, label): """ Creates the widget. """ self.sizer = sizer = wx.BoxSizer(wx.VERTICAL) self.SetSizer(sizer) self.SetAutoLayout(True) # Switcher control. self.control = control = SwitcherControl(self, -1, model, label) sizer.Add(control, 0, wx.EXPAND) # Switcher panel. self.panel = panel = SwitcherPanel(self, -1, model, label) sizer.Add(panel, 1, wx.EXPAND) # Resize the panel to match the sizer's minimal size. sizer.Fit(self) return pyface-7.4.0/pyface/wx/aui.py0000644000076500000240000002662714176222673017016 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 wx # Multiple AUI versions are no longer supported; the C version in wx.aui is not # capable of supporting the windowing flexibility needed by tasks. Therefore, # only AGW's pure-python AUI implementation is used. from wx.lib.agw import aui # Logger. logger = logging.getLogger(__name__) # AGW's library does need some patching for some usability differences desired # for pyface but not for the standard wxPython version class PyfaceAuiNotebook(aui.AuiNotebook): if wx.version() >= "3.": SetPageToolTip = aui.AuiNotebook.SetPageTooltip GetPageToolTip = aui.AuiNotebook.GetPageTooltip class PyfaceAuiManager(aui.AuiManager): # The standard AuiManager dock resizing attempts to adjust all the docks to # provide some sort of best fit, but when there are more than two panes in # a dock it isn't very intuitive. The modifications to these three methods # tries to keep as many sizers fixes as it can and only adjust the one that # is added. def CalculateDockSizerLimits(self, dock): # Replacement for default calculation for min/max dock sizes. Instead # of adjusting the sizes of all the docks, only adjusts one to make the # dock insertion process a little more like what the user expected. docks, panes = aui.CopyDocksAndPanes2(self._docks, self._panes) sash_size = self._art.GetMetric(aui.AUI_DOCKART_SASH_SIZE) opposite_size = self.GetOppositeDockTotalSize( docks, dock.dock_direction ) for tmpDock in docks: if ( tmpDock.dock_direction == dock.dock_direction and tmpDock.dock_layer == dock.dock_layer and tmpDock.dock_row == dock.dock_row ): tmpDock.size = 1 break neighbor_docks = [] horizontal = ( dock.dock_direction == aui.AUI_DOCK_LEFT or dock.dock_direction == aui.AUI_DOCK_RIGHT ) right_or_down = ( dock.dock_direction == aui.AUI_DOCK_RIGHT or dock.dock_direction == aui.AUI_DOCK_BOTTOM ) for d in docks: if ( d.dock_direction == dock.dock_direction and d.dock_layer == dock.dock_layer ): if horizontal: neighbor_docks.append((d.rect.x, d.rect.width)) else: neighbor_docks.append((d.rect.y, d.rect.height)) neighbor_docks.sort() sizer, panes, docks, uiparts = self.LayoutAll( panes, docks, [], True, False ) client_size = self._frame.GetClientSize() sizer.SetDimension(0, 0, client_size.x, client_size.y) sizer.Layout() for part in uiparts: part.rect = wx.Rect( part.sizer_item.GetPosition(), part.sizer_item.GetSize() ) if part.type == aui.AuiDockUIPart.typeDock: part.dock.rect = part.rect sizer.Destroy() new_dock = None for tmpDock in docks: if ( tmpDock.dock_direction == dock.dock_direction and tmpDock.dock_layer == dock.dock_layer and tmpDock.dock_row == dock.dock_row ): new_dock = tmpDock break partnerDock = self.GetPartnerDock(dock) if partnerDock: if horizontal: pos = dock.rect.x size = dock.rect.width else: pos = dock.rect.y size = dock.rect.height min_pos = pos max_pos = pos + size if right_or_down: for p, s in neighbor_docks: if p >= pos: max_pos = p + s - sash_size break else: min_pos = p + sash_size else: for p, s in neighbor_docks: if p > pos: max_pos = p + s - sash_size break else: min_pos = p + sash_size return min_pos, max_pos direction = new_dock.dock_direction if direction == aui.AUI_DOCK_LEFT: minPix = new_dock.rect.x + new_dock.rect.width maxPix = client_size.x - opposite_size - sash_size elif direction == aui.AUI_DOCK_TOP: minPix = new_dock.rect.y + new_dock.rect.height maxPix = client_size.y - opposite_size - sash_size elif direction == aui.AUI_DOCK_RIGHT: minPix = opposite_size maxPix = new_dock.rect.x - sash_size elif direction == aui.AUI_DOCK_BOTTOM: minPix = opposite_size maxPix = new_dock.rect.y - sash_size return minPix, maxPix def GetPartnerDockFromPos(self, dock, point): """Get the neighboring dock located at the given position, used to find the other dock that is going to change size when resizing the specified dock. """ horizontal = ( dock.dock_direction == aui.AUI_DOCK_LEFT or dock.dock_direction == aui.AUI_DOCK_RIGHT ) right_or_down = ( dock.dock_direction == aui.AUI_DOCK_RIGHT or dock.dock_direction == aui.AUI_DOCK_BOTTOM ) if horizontal: pos = point.x else: pos = point.y neighbor_docks = [] for d in self._docks: if ( d.dock_direction == dock.dock_direction and d.dock_layer == dock.dock_layer ): if horizontal: neighbor_docks.append((d.rect.x, d.rect.width, d)) else: neighbor_docks.append((d.rect.y, d.rect.height, d)) neighbor_docks.sort() last = None if right_or_down: for p, s, d in neighbor_docks: if pos < p + s: if d.dock_row == dock.dock_row: d = last break last = d else: neighbor_docks.reverse() for p, s, d in neighbor_docks: if pos > p: if d.dock_row == dock.dock_row: d = last break last = d return d def RestrictResize(self, clientPt, screenPt, createDC): """ Common method between :meth:`DoEndResizeAction` and :meth:`OnLeftUp_Resize`. """ dock = self._action_part.dock pane = self._action_part.pane if createDC: if wx.Platform == "__WXMAC__": dc = wx.ClientDC(self._frame) else: dc = wx.ScreenDC() aui.DrawResizeHint(dc, self._action_rect) self._action_rect = wx.Rect() newPos = clientPt - self._action_offset if self._action_part.type == aui.AuiDockUIPart.typeDockSizer: minPix, maxPix = self.CalculateDockSizerLimits(dock) else: if not self._action_part.pane: return minPix, maxPix = self.CalculatePaneSizerLimits(dock, pane) if self._action_part.orientation == wx.HORIZONTAL: newPos.y = aui.Clip(newPos.y, minPix, maxPix) else: newPos.x = aui.Clip(newPos.x, minPix, maxPix) if self._action_part.type == aui.AuiDockUIPart.typeDockSizer: partner = self.GetPartnerDockFromPos(dock, newPos) sash_size = self._art.GetMetric(aui.AUI_DOCKART_SASH_SIZE) button_size = self._art.GetMetric(aui.AUI_DOCKART_PANE_BUTTON_SIZE) new_dock_size = 0 direction = dock.dock_direction if direction == aui.AUI_DOCK_LEFT: new_dock_size = newPos.x - dock.rect.x elif direction == aui.AUI_DOCK_TOP: new_dock_size = newPos.y - dock.rect.y elif direction == aui.AUI_DOCK_RIGHT: new_dock_size = ( dock.rect.x + dock.rect.width - newPos.x - sash_size ) elif direction == aui.AUI_DOCK_BOTTOM: new_dock_size = ( dock.rect.y + dock.rect.height - newPos.y - sash_size ) delta = new_dock_size - dock.size if delta < -dock.size + sash_size: delta = -dock.size + sash_size elif -button_size < delta < button_size: delta = button_size * (1 if delta > 0 else -1) if partner: if delta > partner.size - sash_size: delta = partner.size - sash_size partner.size -= delta dock.size += delta self.Update() else: # determine the new pixel size that the user wants # this will help us recalculate the pane's proportion if dock.IsHorizontal(): oldPixsize = pane.rect.width newPixsize = oldPixsize + newPos.x - self._action_part.rect.x else: oldPixsize = pane.rect.height newPixsize = oldPixsize + newPos.y - self._action_part.rect.y totalPixsize, totalProportion = self.GetTotalPixSizeAndProportion( dock ) partnerPane = self.GetPartnerPane(dock, pane) # prevent division by zero if totalPixsize <= 0 or totalProportion <= 0 or not partnerPane: return # adjust for the surplus while ( oldPixsize > 0 and totalPixsize > 10 and oldPixsize * totalProportion / totalPixsize < pane.dock_proportion ): totalPixsize -= 1 # calculate the new proportion of the pane newProportion = newPixsize * totalProportion / totalPixsize newProportion = aui.Clip(newProportion, 1, totalProportion) deltaProp = newProportion - pane.dock_proportion if partnerPane.dock_proportion - deltaProp < 1: deltaProp = partnerPane.dock_proportion - 1 newProportion = pane.dock_proportion + deltaProp # borrow the space from our neighbor pane to the # right or bottom (depending on orientation) partnerPane.dock_proportion -= deltaProp pane.dock_proportion = newProportion self.Update() return True def UpdateWithoutLayout(self): """If the layout in the AUI manager is not changing, this can be called to refresh all the panes but preventing a big time usage doing a re- layout that isn't necessary. """ pane_count = len(self._panes) for ii in range(pane_count): p = self._panes[ii] if p.window and p.IsShown() and p.IsDocked(): p.window.Refresh() p.window.Update() if wx.Platform == "__WXMAC__": self._frame.Refresh() else: self.Repaint() pyface-7.4.0/pyface/wx/image_list.py0000644000076500000240000000652514176222673020350 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 cached image list. """ import wx # fixme: rename to 'CachedImageList'?!? class ImageList(wx.ImageList): """ A cached image list. """ def __init__(self, width, height): """ Creates a new cached image list. """ # Base-class constructor. wx.ImageList.__init__(self, width, height) self._width = width self._height = height # Cache of the indexes of the images in the list! self._cache = {} # {filename : index} return # ------------------------------------------------------------------------ # 'ImageList' interface. # ------------------------------------------------------------------------ def GetIndex(self, filename): """ Returns the index of the specified image. The image will be loaded and added to the image list if it is not already there. """ # If the icon is a string then it is the filename of some kind of # image (e.g 'foo.gif', 'image/foo.png' etc). if isinstance(filename, str): # Try the cache first. index = self._cache.get(filename) if index is None: # Load the image from the file and add it to the list. # # N.B 'wx.BITMAP_TYPE_ANY' tells wxPython to attempt to # autodetect the image format. image = wx.Image(filename, wx.BITMAP_TYPE_ANY) # We force all images in the cache to be the same size. self._scale(image) # We also force them to be bitmaps! bmp = image.ConvertToBitmap() # Add the bitmap to the actual list... index = self.Add(bmp) # ... and update the cache. self._cache[filename] = index # Otherwise the icon is *actually* an icon (in our case, probably # related to a MIME type). else: # image = filename # self._scale(image) # bmp = image.ConvertToBitmap() # index = self.Add(bmp) # return index icon = filename # We also force them to be bitmaps! bmp = wx.Bitmap(self._width, self._height) bmp.CopyFromIcon(icon) # We force all images in the cache to be the same size. image = wx.ImageFromBitmap(bmp) self._scale(image) bmp = image.ConvertToBitmap() index = self.Add(bmp) return index # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _scale(self, image): """ Scales the specified image (if necessary). """ if ( image.GetWidth() != self._width or image.GetHeight() != self._height ): image.Rescale(self._width, self._height) return image pyface-7.4.0/pyface/wx/drag_and_drop.py0000644000076500000240000002435314176222673021015 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Drag and drop utilities. """ from inspect import getfullargspec import wx class Clipboard(object): """ The clipboard is used when dragging and dropping Python objects. """ # fixme: This obviously only works within a single process! pass clipboard = Clipboard() clipboard.drop_source = None clipboard.source = None clipboard.data = None 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, str): 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. """ class FileDropTarget(wx.FileDropTarget): """ Drop target for files. """ def __init__(self, handler): """ Constructor. """ # Base-class constructor. wx.FileDropTarget.__init__(self) self.handler = handler def OnDropFiles(self, x, y, filenames): """ Called when the files have been dropped. """ for filename in filenames: self.handler(x, y, filename) # Return True to accept the data, False to veto it. return True # The data format for Python objects! PythonObject = wx.DataFormat("PythonObject") class PythonDropSource(wx.DropSource): """ Drop source for Python objects. """ def __init__(self, source, data, handler=None, allow_move=True): """ Creates a new drop source. A drop source should be created for *every* drag operation. If allow_move is False then the operation will default to a copy and only copy operations will be allowed. """ # The handler can either be a function that will be called when # the data has been dropped onto the target, or an instance that # supports the 'on_dropped' method. self.handler = handler self.allow_move = allow_move # Put the data to be dragged on the clipboard. clipboard.data = data clipboard.source = source clipboard.drop_source = self # Create our own data format and use it in a custom data object. data_object = wx.CustomDataObject(PythonObject) data_object.SetData(b"dummy") # And finally, create the drop source and begin the drag # and drop opperation. wx.DropSource.__init__(self, source) self.SetData(data_object) if allow_move: flags = wx.Drag_DefaultMove | wx.Drag_AllowMove else: flags = wx.Drag_CopyOnly self.result = self.DoDragDrop(flags) def on_dropped(self, drag_result): """ Called when the data has been dropped. """ if self.handler is not None: if hasattr(self.handler, "on_dropped"): # For backward compatibility we accept handler functions # with either 1 or 3 args, including self. If there are # 3 args then we pass the data and the drag_result. args = getfullargspec(self.handler.on_dropped)[0] if len(args) == 3: self.handler.on_dropped(clipboard.data, drag_result) else: self.handler.on_dropped() else: # print self.handler # In this case we assume handler is a function. # For backward compatibility we accept handler functions # with either 0 or 2 args. If there are 2 args then # we pass the data and drag_result args = getfullargspec(self.handler)[0] if len(args) == 2: self.handler(clipboard.data, drag_result) else: self.handler() class PythonDropTarget(wx.DropTarget): """ Drop target for Python objects. """ def __init__(self, handler): """ Constructor The handler can be either a function that will be called when *any* data is dropped onto the target, or an instance that supports the 'wx_drag_over' and 'wx_dropped_on' methods. The latter case allows the target to veto the drop. """ # Base-class constructor. super().__init__() # The handler can either be a function that will be called when # any data is dropped onto the target, or an instance that supports # the 'wx_drag_over' and 'wx_dropped_on' methods. The latter case # allows the target to veto the drop. self.handler = handler # Specify the type of data we will accept. self.data_object = wx.DataObjectComposite() self.data = wx.CustomDataObject(PythonObject) self.data_object.Add(self.data, preferred=True) self.file_data = wx.FileDataObject() self.data_object.Add(self.file_data) self.SetDataObject(self.data_object) def OnData(self, x, y, default_drag_result): """ Called when OnDrop returns True. """ # First, if we have a source in the clipboard and the source # doesn't allow moves then change the default to copy if ( clipboard.drop_source is not None and not clipboard.drop_source.allow_move ): default_drag_result = wx.DragCopy elif clipboard.drop_source is None: # This means we might be receiving a file; try to import # the right packages to nicely handle a file drop. If those # packages can't be imported, then just pass through. if self.GetData(): try: from apptools.io import File from apptools.naming.api import Binding names = self.file_data.GetFilenames() files = [] bindings = [] for name in names: f = File(name) files.append(f) bindings.append(Binding(name=name, obj=f)) clipboard.data = files clipboard.node = bindings except ImportError: pass # Pass the object on the clipboard it to the handler. # # fixme: We allow 'wx_dropped_on' and 'on_drop' because both Dave # and Martin want different things! Unify! if hasattr(self.handler, "wx_dropped_on"): drag_result = self.handler.wx_dropped_on( x, y, clipboard.data, default_drag_result ) elif hasattr(self.handler, "on_drop"): drag_result = self.handler.on_drop( x, y, clipboard.data, default_drag_result ) else: self.handler(x, y, clipboard.data) drag_result = default_drag_result # Let the source of the drag/drop know that the operation is complete. drop_source = clipboard.drop_source if drop_source is not None: drop_source.on_dropped(drag_result) # Clean out the drop source! clipboard.drop_source = None # The return value tells the source what to do with the original data # (move, copy, etc.). In this case we just return the suggested value # given to us. return default_drag_result # Some virtual methods that track the progress of the drag. def OnDragOver(self, x, y, default_drag_result): """ Called when a data object is being dragged over the target. """ # First, if we have a source in the clipboard and the source # doesn't allow moves then change the default to copy data = clipboard.data if clipboard.drop_source is None: if not hasattr(self.handler, "wx_drag_any"): # this is probably a file being dragged in, so just return return default_drag_result data = None elif not clipboard.drop_source.allow_move: default_drag_result = wx.DragCopy # The value returned here tells the source what kind of visual feedback # to give. For example, if wxDragCopy is returned then only the copy # cursor will be shown, even if the source allows moves. You can use # the passed in (x,y) to determine what kind of feedback to give. # In this case we return the suggested value which is based on whether # the Ctrl key is pressed. # # fixme: We allow 'wx_drag_over' and 'on_drag_over' because both Dave # and Martin want different things! Unify! if hasattr(self.handler, "wx_drag_any"): drag_result = self.handler.wx_drag_any( x, y, data, default_drag_result ) elif hasattr(self.handler, "wx_drag_over"): drag_result = self.handler.wx_drag_over( x, y, data, default_drag_result ) elif hasattr(self.handler, "on_drag_over"): drag_result = self.handler.on_drag_over( x, y, data, default_drag_result ) else: drag_result = default_drag_result return drag_result def OnLeave(self): """ Called when the mouse leaves the drop target. """ if hasattr(self.handler, "wx_drag_leave"): self.handler.wx_drag_leave(clipboard.data) def OnDrop(self, x, y): """ Called when the user drops a data object on the target. Return 'False' to veto the operation. """ return True pyface-7.4.0/pyface/wx/image.py0000644000076500000240000000136714176222673017314 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 wx from traits.util.resource import get_path def get_bitmap(root, name): """ Convenience function that returns a bitmap root - either an instance of a class or a path name - name of png file to load """ path = os.path.join(get_path(root), name) bmp = wx.Bitmap(path, wx.BITMAP_TYPE_PNG) return bmp pyface-7.4.0/pyface/wx/image_cache.py0000644000076500000240000000414714176222673020436 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ An image cache. """ import wx class ImageCache(object): """ An image cache. """ def __init__(self, width, height): """ Creates a new image cache. """ self._width = width self._height = height # The images in the cache! self._images = {} # {filename : bitmap} return # ------------------------------------------------------------------------ # 'ImageCache' interface. # ------------------------------------------------------------------------ def get_image(self, filename): """ Returns the specified image (currently as a bitmap). """ # Try the cache first. bmp = self._images.get(filename) if bmp is None: # Load the image from the file and add it to the list. # # N.B 'wx.BITMAP_TYPE_ANY' tells wxPython to attempt to autodetect # --- the image format. image = wx.Image(filename, wx.BITMAP_TYPE_ANY) # We force all images in the cache to be the same size. self._scale(image) # We also force them to be bitmaps! bmp = image.ConvertToBitmap() # Add the bitmap to the cache! self._images[filename] = bmp return bmp # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _scale(self, image): """ Scales the specified image (if necessary). """ if ( image.GetWidth() != self._width or image.GetHeight() != self._height ): image.Rescale(self._width, self._height) return image pyface-7.4.0/pyface/wx/grid/0000755000076500000240000000000014176460551016575 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/wx/grid/grid.py0000644000076500000240000002464614176222673020111 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 grid (spreadsheet) widget. """ import wx from wx.grid import ( Grid as wxGrid, GridTableMessage, GRIDTABLE_NOTIFY_ROWS_APPENDED, GRIDTABLE_NOTIFY_ROWS_DELETED, GRIDTABLE_NOTIFY_COLS_APPENDED, GRIDTABLE_NOTIFY_COLS_DELETED, ) class Grid(wxGrid): """ A grid (spreadsheet) widget. """ def __init__(self, parent, model): """ Constructor. """ # Base class constructor. wxGrid.__init__(self, parent, -1) # The model that provides the data and row/column information. self.model = None # Don't display any extra space around the rows and columns. self.SetMargins(0, 0) # Tell the grid to get its data from the model. # # N.B The terminology used in the wxPython API is a little confusing! # --- The 'SetTable' method is actually setting the model used by # the grid (which is the view)! # # The second parameter to 'SetTable' tells the grid to take ownership # of the model and to destroy it when it is done. Otherwise you would # need to keep a reference to the model and manually destroy it later # (by calling it's Destroy method). # # fixme: We should create a default model if one is not supplied. self.SetTable(model._grid_table_base, True) model.observe(self._on_model_changed, "model_changed") self.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self._on_cell_change) self.Bind(wx.grid.EVT_GRID_SELECT_CELL, self._on_select_cell) # This starts the cell editor on a double-click as well as on a second # click. self.Bind(wx.grid.EVT_GRID_CELL_LEFT_DCLICK, self._on_cell_left_dclick) # This pops up a context menu. # wx.grid.EVT_GRID_CELL_RIGHT_CLICK(self, self._on_cell_right_click) # We handle key presses to change the behavior of the and # keys to make manual data entry smoother. self.Bind(wx.EVT_KEY_DOWN, self._on_key_down) # Initialize the row and column models. self._initialize_rows(model) self._initialize_columns(model) self._initialize_fonts() def _initialize_fonts(self): """ Initialize the label fonts. """ self.SetLabelFont(wx.Font(8, wx.SWISS, wx.NORMAL, wx.BOLD)) self.SetGridLineColour("blue") self.SetColLabelAlignment(wx.ALIGN_CENTRE, wx.ALIGN_CENTRE) self.SetRowLabelAlignment(wx.ALIGN_LEFT, wx.ALIGN_CENTRE) def _initialize_rows(self, model): """ Initialize the row headers. """ if not model.show_row_headers: self.SetRowLabelSize(0) else: for index, row in enumerate(model.rows): if row.readonly: attr = wx.grid.GridCellAttr() attr.SetReadOnly() attr.SetRenderer(None) attr.SetBackgroundColour("linen") self.SetRowAttr(index, attr) def _initialize_columns(self, model): """ Initialize the column headers. """ if not model.show_column_headers: self.SetColLabelSize(0) else: for index, column in enumerate(model.columns): if column.readonly: attr = wx.grid.GridCellAttr() attr.SetReadOnly() attr.SetRenderer(None) attr.SetBackgroundColour("linen") self.SetColAttr(index, attr) return # ------------------------------------------------------------------------ # wx event handlers. # ------------------------------------------------------------------------ def _on_cell_change(self, evt): """ Called when the contents of a cell have been changed. """ evt.Skip() def _on_select_cell(self, evt): """ Called when the user has moved to another cell. """ evt.Skip() def _on_cell_left_dclick(self, evt): """ Called when the left mouse button was double-clicked. From the wxPython demo code:- 'I do this because I don't like the default behaviour of not starting the cell editor on double clicks, but only a second click.' Fair enuff! """ if self.CanEnableCellControl(): self.EnableCellEditControl() def _on_cell_right_click(self, evt): """ Called when a right click occurred in a cell. """ row = evt.GetRow() # The last row in the table is not part of the actual data, it is just # there to allow the user to enter a new row. Hence they cannot delete # it! if row < self.GetNumberRows() - 1: # Complete the edit on the current cell. self.DisableCellEditControl() # Make the row the ONLY one selected. self.SelectRow(row) # Popup a context menu allowing the user to delete the row. menu = wx.Menu() menu.Append(101, "Delete Row") self.Bind(wx.EVT_MENU, self._on_delete_row, id=101) self.PopupMenu(menu, evt.GetPosition()) def _on_key_down(self, evt): """ Called when a key is pressed. """ # This changes the behaviour of the and keys to make # manual data entry smoother! # # Don't change the behavior if the key is pressed as this # has meaning to the edit control. key_code = evt.GetKeyCode() if key_code == wx.WXK_RETURN and not evt.ControlDown(): self._move_to_next_cell(evt.ShiftDown()) elif key_code == wx.WXK_TAB and not evt.ControlDown(): if evt.ShiftDown(): self._move_to_previous_cell() else: self._move_to_next_cell() else: evt.Skip() def _on_delete_row(self, evt): """ Called when the 'Delete Row' context menu item is selected. """ # Get the selected row (there must be exactly one at this point!). selected_rows = self.GetSelectedRows() if len(selected_rows) == 1: self.DeleteRows(selected_rows[0], 1) return # ------------------------------------------------------------------------ # Trait event handlers. # ------------------------------------------------------------------------ def _on_model_changed(self, event): """ Called when the model has changed. """ message = event.new self.BeginBatch() self.ProcessTableMessage(message) self.EndBatch() return # ------------------------------------------------------------------------ # 'Grid' interface. # ------------------------------------------------------------------------ def Reset(self): print("Reset") # attr = grid.GridCellAttr() # renderer = MyRenderer() # attr.SetRenderer(renderer) # self.SetColSize(0, 50) # self.SetColAttr(0, attr) self.ForceRefresh() def ResetView(self, grid): """ (wxGrid) -> Reset the grid view. Call this to update the grid if rows and columns have been added or deleted """ print("*************************VirtualModel.reset_view") grid = self grid.BeginBatch() for current, new, delmsg, addmsg in [ ( self._rows, self.GetNumberRows(), GRIDTABLE_NOTIFY_ROWS_DELETED, GRIDTABLE_NOTIFY_ROWS_APPENDED, ), ( self._cols, self.GetNumberCols(), GRIDTABLE_NOTIFY_COLS_DELETED, GRIDTABLE_NOTIFY_COLS_APPENDED, ), ]: if new < current: msg = GridTableMessage(self, delmsg, new, current - new) grid.ProcessTableMessage(msg) elif new > current: msg = GridTableMessage(self, addmsg, new - current) grid.ProcessTableMessage(msg) self.UpdateValues(grid) grid.EndBatch() self._rows = self.GetNumberRows() self._cols = self.GetNumberCols() # update the renderers # self._updateColAttrs(grid) # self._updateRowAttrs(grid) too expensive to use on a large grid # update the scrollbars and the displayed part of the grid grid.AdjustScrollbars() grid.ForceRefresh() return # ------------------------------------------------------------------------ # Protected interface. # ------------------------------------------------------------------------ def _move_to_next_cell(self, expandSelection=False): """ Move to the 'next' cell. """ # Complete the edit on the current cell. self.DisableCellEditControl() # Try to move to the next column. success = self.MoveCursorRight(expandSelection) # If the move failed then we must be at the end of a row. if not success: # Move to the first column in the next row. newRow = self.GetGridCursorRow() + 1 if newRow < self.GetNumberRows(): self.SetGridCursor(newRow, 0) self.MakeCellVisible(newRow, 0) else: # This would be a good place to add a new row if your app # needs to do that. pass return success def _move_to_previous_cell(self, expandSelection=False): """ Move to the 'previous' cell. """ # Complete the edit on the current cell. self.DisableCellEditControl() # Try to move to the previous column (without expanding the current # selection). success = self.MoveCursorLeft(expandSelection) # If the move failed then we must be at the start of a row. if not success: # Move to the last column in the previous row. newRow = self.GetGridCursorRow() - 1 if newRow >= 0: self.SetGridCursor(newRow, self.GetNumberCols() - 1) self.MakeCellVisible(newRow, self.GetNumberCols() - 1) return pyface-7.4.0/pyface/wx/grid/__init__.py0000644000076500000240000000062714176222673020714 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! pyface-7.4.0/pyface/wx/grid/api.py0000644000076500000240000000102414176222673017716 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 .grid import Grid from .grid_column import GridColumn from .grid_model import GridModel from .grid_row import GridRow pyface-7.4.0/pyface/wx/grid/grid_model.py0000644000076500000240000001734314176222673021265 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 model that provides data for a grid. """ from wx.grid import ( GridTableBase, GridTableMessage, GRIDTABLE_NOTIFY_ROWS_APPENDED, ) from traits.api import Any, Bool, HasTraits, Event, List from .grid_column import GridColumn from .grid_row import GridRow class GridModel(HasTraits): """ A model that provides data for a grid. """ # fixme : factor this default model into "SimpleGridModel" or similar # An optional 2-dimensional list/array containing the grid data. data = Any() # The rows in the model. rows = List(GridRow) # The columns in the model. columns = List(GridColumn) # Show row headers? show_row_headers = Bool(True) # Show column headers? show_column_headers = Bool(True) # Fired when the data in the model has changed. model_changed = Event() def __init__(self, **traits): """ Create a new grid model. """ # Base class constructors. HasTraits.__init__(self, **traits) # The wx virtual table hook. self._grid_table_base = _GridTableBase(self) if len(self.columns) == 0 and self.data is not None: print("Building default table column model") columns = [] # Assume data is rectangular and use the length of the first row. for i in range(len(self.data[0])): columns.append(GridColumn(label=str(i))) self.columns = columns return # ------------------------------------------------------------------------ # 'wxGridTableBase' interface. # ------------------------------------------------------------------------ def GetNumberRows(self): """ Return the number of rows in the model. """ return len(self.data) def GetNumberCols(self): """ Return the number of columns in the model. """ return len(self.columns) def IsEmptyCell(self, row, col): """ Is the specified cell empty? """ try: return not self.data[row][col] except IndexError: return True # Get/Set values in the table. The Python versions of these methods can # handle any data-type, (as long as the Editor and Renderer understands the # type too,) not just strings as in the C++ version. def GetValue(self, row, col): """ Get the value at the specified row and column. """ try: return self.data[row][col] except IndexError: pass return "" def SetValue(self, row, col, value): """ Set the value at the specified row and column. """ try: self.data[row][col] = value except IndexError: # Add a new row. self.data.append([0] * self.GetNumberCols()) self.data[row][col] = value # Tell the grid that we've added a row. # # N.B wxGridTableMessage(table, whatWeDid, howMany) message = GridTableMessage(self, GRIDTABLE_NOTIFY_ROWS_APPENDED, 1) # Trait event notification. self.model_changed = message def GetRowLabelValue(self, row): """ Called when the grid needs to display a row label. """ return str(row) def GetColLabelValue(self, col): """ Called when the grid needs to display a column label. """ return self.columns[col].label def GetTypeName(self, row, col): """ Called to determine the kind of editor/renderer to use. This doesn't necessarily have to be the same type used natively by the editor/renderer if they know how to convert. """ return self.columns[col].type def CanGetValueAs(self, row, col, type_name): """ Called to determine how the data can be fetched. This allows you to enforce some type-safety in the grid. """ column_typename = self.GetTypeName(row, col) return type_name == column_typename def CanSetValueAs(self, row, col, type_name): """ Called to determine how the data can be stored. This allows you to enforce some type-safety in the grid. """ return self.CanGetValueAs(row, col, type_name) def DeleteRows(self, pos, num_rows): """ Called when the view is deleting rows. """ del self.data[pos:pos + num_rows] # Tell the grid that we've deleted some rows. # # N.B Because of a bug in wxPython we have to send a "rows appended" # --- message with a negative number, instead of the "rows deleted" # message 8^() TINSTAFS! message = GridTableMessage( self, GRIDTABLE_NOTIFY_ROWS_APPENDED, -num_rows ) # Trait event notification. self.model_changed = message return True class _GridTableBase(GridTableBase): """ A model that provides data for a grid. """ # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, model): """ Creates a new table base. """ # Base class constructor. GridTableBase.__init__(self) # The Pyface model that provides the data. self.model = model return # ------------------------------------------------------------------------ # 'wxGridTableBase' interface. # ------------------------------------------------------------------------ def GetNumberRows(self): """ Return the number of rows in the model. """ return self.model.GetNumberRows() def GetNumberCols(self): """ Return the number of columns in the model. """ return self.model.GetNumberCols() def IsEmptyCell(self, row, col): """ Is the specified cell empty? """ return self.model.IsEmptyCell(row, col) def GetValue(self, row, col): """ Get the value at the specified row and column. """ return self.model.GetValue(row, col) def SetValue(self, row, col, value): """ Set the value at the specified row and column. """ return self.model.SetValue(row, col, value) def GetRowLabelValue(self, row): """ Called when the grid needs to display a row label. """ return self.model.GetRowLabelValue(row) def GetColLabelValue(self, col): """ Called when the grid needs to display a column label. """ return self.model.GetColLabelValue(col) def GetTypeName(self, row, col): """ Called to determine the kind of editor/renderer to use. This doesn't necessarily have to be the same type used natively by the editor/renderer if they know how to convert. """ return self.model.GetTypeName(row, col) def CanGetValueAs(self, row, col, type_name): """ Called to determine how the data can be fetched. This allows you to enforce some type-safety in the grid. """ return self.model.CanGetValueAs(row, col, type_name) def CanSetValueAs(self, row, col, type_name): """ Called to determine how the data can be stored. This allows you to enforce some type-safety in the grid. """ return self.model.CanSetValueAs(row, col, type_name) def DeleteRows(self, pos, num_rows): """ Called when the view is deleting rows. """ return self.model.DeleteRows(pos, num_rows) pyface-7.4.0/pyface/wx/grid/grid_column.py0000644000076500000240000000134714176222673021457 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 description of a column in a grid. """ from traits.api import Bool, HasTraits, Str class GridColumn(HasTraits): """ A description of a column in a grid. """ # Column header. label = Str() # Type name of data allowed in the column. type = Str() # Is the column read-only? readonly = Bool(False) pyface-7.4.0/pyface/wx/grid/grid_row.py0000644000076500000240000000125414176222673020766 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 description of a row in a grid. """ from traits.api import HasTraits class GridRow(HasTraits): """ A description of a row in a grid. """ def __init__(self, row_data): """ Create a new row. """ self.__dict__.update(row_data) return pyface-7.4.0/pyface/splash_screen.py0000644000076500000240000000112314176222673020413 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 implementation of a splash screen. """ # Import the toolkit specific version. from .toolkit import toolkit_object SplashScreen = toolkit_object("splash_screen:SplashScreen") pyface-7.4.0/pyface/i_single_choice_dialog.py0000644000076500000240000000354514176222673022216 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 interface for a dialog that prompts for a choice from a list. """ from traits.api import Any, HasTraits, List, Str from pyface.i_dialog import IDialog class ISingleChoiceDialog(IDialog): """ The interface for a dialog that prompts for a choice from a list. """ # 'ISingleChoiceDialog' interface -------------------------------------# #: List of objects to choose from. choices = List(Any) #: The object chosen, if any. choice = Any() #: An optional attribute to use for the name of each object in the dialog. name_attribute = Str() #: The message to display to the user. message = Str() class MSingleChoiceDialog(HasTraits): """ The mixin class that contains common code for toolkit specific implementations of the IConfirmationDialog interface. """ def _choice_strings(self): """ Returns the list of strings to display in the dialog. """ choices = self.choices if self.name_attribute != "": # choices is a list of objects with this attribute choices = [getattr(obj, self.name_attribute) for obj in choices] choices = [str(obj) for obj in choices] if len(choices) == 0: raise ValueError("SingleChoiceDialog requires at least 1 choice.") elif len(choices) != len(set(choices)): raise ValueError( "Dialog choices {} contain repeated string value." % choices ) return choices pyface-7.4.0/pyface/undo/0000755000076500000240000000000014176460551016157 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/undo/abstract_command.py0000644000076500000240000000646714176222673022050 0ustar cwebsterstaff00000000000000# (C) Copyright 2007-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: # ------------------------------------------------------------------------------ # Enthought library imports. from traits.api import Any, HasTraits, Str, provides # Local imports. from .i_command import ICommand @provides(ICommand) class AbstractCommand(HasTraits): """The AbstractCommand class is an abstract base class that implements the ICommand interface. """ #### 'ICommand' interface ################################################# #: This is the data on which the command operates. data = Any() #: This is the name of the command as it will appear in any GUI element. It #: may include '&' which will be automatically removed whenever it is #: inappropriate. name = Str() ########################################################################### # 'ICommand' interface. ########################################################################### def do(self): """This is called by the command stack to do the command and to return any value. The command must save any state necessary for the 'redo()' and 'undo()' methods to work. The class's __init__() must also ensure that deep copies of any arguments are made if appropriate. It is guaranteed that this will only ever be called once and that it will be called before any call to 'redo()' or 'undo()'. """ raise NotImplementedError def merge(self, other): """This is called by the command stack to try and merge another command with this one. True is returned if the commands were merged. 'other' is the command that is about to be executed. If the commands are merged then 'other' will discarded and not placed on the command stack. A subsequent undo or redo of this modified command must have the same effect as the two original commands. """ # By default merges never happen. return False def redo(self): """This is called by the command stack to redo the command. Any returned value will replace the value that the command stack references from the original call to 'do()' or previous call to 'redo()'. """ raise NotImplementedError def undo(self): """ This is called by the command stack to undo the command. """ raise NotImplementedError pyface-7.4.0/pyface/undo/command_stack.py0000644000076500000240000002454214176222673021344 0ustar cwebsterstaff00000000000000# (C) Copyright 2008-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: # ------------------------------------------------------------------------------ # Enthought library imports. from traits.api import ( Bool, HasTraits, Instance, Int, List, Property, Str, provides, ) # Local imports. from .abstract_command import AbstractCommand from .i_command import ICommand from .i_command_stack import ICommandStack from .i_undo_manager import IUndoManager class _StackEntry(HasTraits): """ The _StackEntry class is a single entry on a command stack. """ #### '_StackEntry' interface ############################################## #: Set if the entry corresponds to a clean point on the stack. clean = Bool(False) #: The command instance. command = Instance(ICommand) #: The sequence number of the entry. sequence_nr = Int() class _MacroCommand(AbstractCommand): """ The _MacroCommand class is an internal command that handles macros. """ #### '_MacroCommand' interface ############################################ #: The commands that make up this macro. macro_commands = List(Instance(ICommand)) ########################################################################### # 'ICommand' interface. ########################################################################### def do(self): """ Invoke the command. """ # This is a dummy. return None def merge(self, other): """ Try and merge a command. """ if len(self.macro_commands) == 0: merged = False else: merged = self.macro_commands[-1].merge(other) return merged def redo(self): """ Redo the sub-commands. """ for cmd in self.macro_commands: cmd.redo() # Macros cannot return values. return None def undo(self): """ Undo the sub-commands. """ for cmd in self.macro_commands: cmd.undo() @provides(ICommandStack) class CommandStack(HasTraits): """The CommandStack class is the default implementation of the ICommandStack interface. """ #### 'ICommandStack' interface ############################################ #: This is the clean state of the stack. Its value changes as commands are #: undone and redone. It can also be explicity set to mark the current #: stack position as being clean (when the data is saved to disk for #: example). clean = Property(Bool) #: This is the name of the command that can be redone. It will be empty if #: there is no command that can be redone. It is maintained by the undo #: stack. redo_name = Property(Str) #: This is the undo manager that manages this stack. undo_manager = Instance(IUndoManager) #: This is the name of the command that can be undone. It will be empty if #: there is no command that can be undone. It is maintained by the undo #: stack. undo_name = Property(Str) #### Private interface #################################################### # The current index into the stack (ie. the last command that was done). _index = Int(-1) # The current macro stack. _macro_stack = List(Instance(_MacroCommand)) # The stack itself. _stack = List(Instance(_StackEntry)) ########################################################################### # 'ICommandStack' interface. ########################################################################### def begin_macro(self, name): """This begins a macro by creating an empty command with the given 'name'. All subsequent calls to 'push()' create commands that will be children of the empty command until the next call to 'end_macro()'. Macros may be nested. The stack is disabled (ie. nothing can be undone or redone) while a macro is being created (ie. while there is an outstanding 'end_macro()' call). """ command = _MacroCommand(name=name) self.push(command) self._macro_stack.append(command) def clear(self): """This clears the stack, without undoing or redoing any commands, and leaves the stack in a clean state. It is typically used when all changes to the data have been abandoned. """ self._index = -1 self._stack = [] self._macro_stack = [] self.undo_manager.stack_updated = self def end_macro(self): """ This ends a macro. """ try: self._macro_stack.pop() except IndexError: pass def push(self, command): """This executes a command and saves it on the command stack so that it can be subsequently undone and redone. 'command' is an instance that implements the ICommand interface. Its 'do()' method is called to execute the command. If any value is returned by 'do()' then it is returned by 'push()'. """ # See if the command can be merged with the previous one. if len(self._macro_stack) == 0: if self._index >= 0 and not self._stack[self._index].clean: merged = self._stack[self._index].command.merge(command) else: merged = False else: merged = self._macro_stack[-1].merge(command) # Increment the global sequence number. if not merged: self.undo_manager.sequence_nr += 1 # Execute the command. result = command.do() # Update the stack state for a merged command. if merged: if len(self._macro_stack) == 0: # If not in macro mode, remove everything after the current # command from the stack. del self._stack[self._index+1:] self.undo_manager.stack_updated = self return result # Only update the command stack if there is no current macro. if len(self._macro_stack) == 0: # Remove everything on the stack after the last command that was # done. self._index += 1 del self._stack[self._index:] # Create a new stack entry and add it to the stack. entry = _StackEntry( command=command, sequence_nr=self.undo_manager.sequence_nr ) self._stack.append(entry) self.undo_manager.stack_updated = self else: # Add the command to the parent macro command. self._macro_stack[-1].macro_commands.append(command) return result def redo(self, sequence_nr=0): """If 'sequence_nr' is 0 then the last command that was undone is redone and any result returned. Otherwise commands are redone up to and including the given 'sequence_nr' and any result of the last of these is returned. """ # Make sure a redo is valid in the current context. if self.redo_name == "": return None if sequence_nr == 0: result = self._redo_one() else: result = None while self._index + 1 < len(self._stack): if self._stack[self._index + 1].sequence_nr > sequence_nr: break result = self._redo_one() self.undo_manager.stack_updated = self return result def undo(self, sequence_nr=0): """If 'sequence_nr' is 0 then the last command is undone. Otherwise commands are undone up to and including the given 'sequence_nr'. """ # Make sure an undo is valid in the current context. if self.undo_name == "": return if sequence_nr == 0: self._undo_one() else: while self._index >= 0: if self._stack[self._index].sequence_nr <= sequence_nr: break self._undo_one() self.undo_manager.stack_updated = self ########################################################################### # Private interface. ########################################################################### def _redo_one(self): """ Redo the command at the current index and return the result. """ self._index += 1 entry = self._stack[self._index] return entry.command.redo() def _undo_one(self): """ Undo the command at the current index. """ entry = self._stack[self._index] self._index -= 1 entry.command.undo() def _get_clean(self): """ Get the clean state of the stack. """ if self._index >= 0: clean = self._stack[self._index].clean else: clean = True return clean def _set_clean(self, clean): """ Set the clean state of the stack. """ if self._index >= 0: self._stack[self._index].clean = clean def _get_redo_name(self): """ Get the name of the redo command, if any. """ redo_name = "" if len(self._macro_stack) == 0 and self._index + 1 < len(self._stack): redo_name = self._stack[self._index + 1].command.name.replace( "&", "" ) return redo_name def _get_undo_name(self): """ Get the name of the undo command, if any. """ undo_name = "" if len(self._macro_stack) == 0 and self._index >= 0: command = self._stack[self._index].command undo_name = command.name.replace("&", "") return undo_name pyface-7.4.0/pyface/undo/tests/0000755000076500000240000000000014176460551017321 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/undo/tests/testing_commands.py0000644000076500000240000000232614176222673023235 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 from pyface.undo.api import AbstractCommand class SimpleCommand(AbstractCommand): """ Simplest command possible operating on an integer. """ name = "Increment by 1" data = Int() def do(self): self.redo() def redo(self): self.data += 1 def undo(self): self.data -= 1 class UnnamedCommand(SimpleCommand): name = "" class MergeableCommand(SimpleCommand): name = "Increment" amount = Int(1) def do(self): self.redo() def redo(self): self.data += self.amount def undo(self): self.data -= self.amount def merge(self, other): if not isinstance(other, MergeableCommand): return False self.data += other.amount self.amount += other.amount return True pyface-7.4.0/pyface/undo/tests/test_undo_manager.py0000644000076500000240000000513214176222673023373 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.api import UnittestTools from pyface.undo.api import CommandStack, UndoManager from pyface.undo.tests.testing_commands import SimpleCommand class TestUndoManager(unittest.TestCase, UnittestTools): def setUp(self): self.stack_a = CommandStack() self.stack_b = CommandStack() self.undo_manager = UndoManager() self.stack_a.undo_manager = self.undo_manager self.stack_b.undo_manager = self.undo_manager self.undo_manager.active_stack = self.stack_a self.command = SimpleCommand() # Command pushing tests --------------------------------------------------- def test_undo(self): self.assertEqual(self.stack_a._index, -1) self.stack_a.push(self.command) self.assertEqual(self.stack_a._index, 0) with self.assertTraitChanges( self.undo_manager, 'stack_updated', count=1): self.undo_manager.undo() self.assertEqual(self.stack_a._index, -1) def test_redo(self): self.assertEqual(self.stack_a._index, -1) self.stack_a.push(self.command) self.undo_manager.undo() self.assertEqual(self.stack_a._index, -1) with self.assertTraitChanges( self.undo_manager, 'stack_updated', count=1): self.undo_manager.redo() self.assertEqual(self.stack_a._index, 0) def test_change_active_stack(self): for _ in range(5): self.stack_a.push(self.command) self.assertEqual(self.stack_a._index, 4) self.undo_manager.active_stack = self.stack_b for _ in range(5): self.stack_b.push(self.command) self.assertEqual(self.stack_b._index, 4) for _ in range(3): self.undo_manager.undo() self.undo_manager.redo() self.assertEqual(self.stack_a._index, 4) self.assertEqual(self.stack_b._index, 2) def test_active_stack_clean(self): self.assertTrue(self.undo_manager.active_stack_clean) self.stack_a.push(self.command) self.assertFalse(self.undo_manager.active_stack_clean) self.undo_manager.active_stack = None self.assertTrue(self.undo_manager.active_stack_clean) pyface-7.4.0/pyface/undo/tests/test_command_stack.py0000644000076500000240000002220014176222673023532 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 pyface.undo.api import CommandStack, UndoManager from pyface.undo.tests.testing_commands import ( MergeableCommand, SimpleCommand, UnnamedCommand, ) from traits.testing.api import UnittestTools class TestCommandStack(UnittestTools, unittest.TestCase): def setUp(self): self.stack = CommandStack() undo_manager = UndoManager() self.stack.undo_manager = undo_manager self.command = SimpleCommand() # Command pushing tests --------------------------------------------------- def test_empty_command_stack(self): with self.assert_n_commands_pushed(self.stack, 0): pass def test_1_command_pushed(self): with self.assert_n_commands_pushed(self.stack, 1): with self.assertTraitChanges(self.stack.undo_manager, 'stack_updated', count=1): self.stack.push(self.command) def test_n_command_pushed(self): n = 4 with self.assert_n_commands_pushed(self.stack, n): for i in range(n): with self.assertTraitChanges(self.stack.undo_manager, 'stack_updated', count=1): self.stack.push(self.command) def test_push_after_undo(self): with self.assert_n_commands_pushed(self.stack, 1): self.stack.push(self.command) self.stack.undo() with self.assertTraitChanges(self.stack.undo_manager, 'stack_updated', count=1): self.stack.push(self.command) def test_push_after_n_undo(self): with self.assert_n_commands_pushed(self.stack, 1): n = 4 for i in range(n): self.stack.push(self.command) n = 4 for i in range(n): self.stack.undo() with self.assertTraitChanges(self.stack.undo_manager, 'stack_updated', count=1): self.stack.push(self.command) # Command merging tests --------------------------------------------------- def test_1_merge_command_pushed(self): self.command = MergeableCommand() with self.assert_n_commands_pushed(self.stack, 1): with self.assertTraitChanges(self.stack.undo_manager, 'stack_updated', count=1): self.stack.push(self.command) def test_n_merge_command_pushed(self): n = 4 with self.assert_n_commands_pushed(self.stack, 1): self.command = MergeableCommand() self.stack.push(self.command) for i in range(n): command = MergeableCommand() with self.assertTraitChanges(self.stack.undo_manager, 'stack_updated', count=1): self.stack.push(command) self.assertEqual(self.command.amount, n+1) def test_merge_after_undo(self): with self.assert_n_commands_pushed(self.stack, 2): self.stack.push(self.command) command = MergeableCommand() self.stack.push(command) command = SimpleCommand() self.stack.push(command) self.stack.undo() command = MergeableCommand() with self.assertTraitChanges(self.stack.undo_manager, 'stack_updated', count=1): self.stack.push(command) def test_merge_after_clean(self): with self.assert_n_commands_pushed(self.stack, 2): command = MergeableCommand() self.stack.push(command) self.stack.clean = True command = MergeableCommand() with self.assertTraitChanges(self.stack.undo_manager, 'stack_updated', count=1): self.stack.push(command) # Undo/Redo tests --------------------------------------------------------- def test_undo_1_command(self): with self.assert_n_commands_pushed_and_undone(self.stack, 1): self.stack.push(self.command) self.assertEqual(self.stack.undo_name, self.command.name) with self.assertTraitChanges(self.stack.undo_manager, 'stack_updated', count=1): self.stack.undo() def test_undo_n_command(self): n = 4 with self.assert_n_commands_pushed_and_undone(self.stack, n): for i in range(n): self.stack.push(self.command) for i in range(n): self.stack.undo() def test_undo_redo_sequence_nr(self): n = 4 for i in range(n): self.stack.push(self.command) self.assertEqual(self.stack._index, 3) # undo back to the 1st command in the stack self.stack.undo(1) self.assertEqual(self.stack._index, 0) # redo back to the 3rd command in the stack self.stack.redo(3) self.assertEqual(self.stack._index, 2) def test_undo_unnamed_command(self): unnamed_command = UnnamedCommand() with self.assert_n_commands_pushed(self.stack, 1): self.stack.push(unnamed_command) # But the command cannot be undone because it has no name self.assertEqual(self.stack.undo_name, "") # This is a no-op self.stack.undo() def test_undo_redo_1_command(self): with self.assert_n_commands_pushed(self.stack, 1): self.stack.push(self.command) self.stack.undo() self.stack.redo() # Macro tests ------------------------------------------------------------- def test_define_macro(self): with self.assert_n_commands_pushed(self.stack, 1): add_macro(self.stack, num_commands=2) def test_undo_macro(self): with self.assert_n_commands_pushed_and_undone(self.stack, 1): # The 2 pushes are viewed as 1 command add_macro(self.stack, num_commands=2) self.stack.undo() # Cleanliness tests ------------------------------------------------------- def test_empty_stack_is_clean(self): self.assertTrue(self.stack.clean) def test_non_empty_stack_is_dirty(self): self.stack.push(self.command) self.assertFalse(self.stack.clean) def test_make_clean(self): # This makes it dirty by default self.stack.push(self.command) # Make the current tip of the stack clean self.stack.clean = True self.assertTrue(self.stack.clean) def test_make_dirty(self): # Start from a clean state: self.stack.push(self.command) self.stack.clean = True self.stack.clean = False self.assertFalse(self.stack.clean) def test_save_push_undo_is_clean(self): self.stack.push(self.command) self.stack.clean = True self.stack.push(self.command) self.stack.undo() self.assertTrue(self.stack.clean) def test_save_push_save_undo_is_clean(self): self.stack.push(self.command) self.stack.clean = True self.stack.push(self.command) self.stack.clean = True self.stack.undo() self.assertTrue(self.stack.clean) def test_push_undo_save_redo_is_dirty(self): self.stack.push(self.command) self.stack.undo() self.stack.clean = True self.stack.redo() self.assertFalse(self.stack.clean) def test_clear(self): n = 5 for _ in range(n): self.stack.push(self.command) self.stack.clear() self.assertEqual(self.stack._stack, []) self.assertTrue(self.stack.clean) # Assertion helpers ------------------------------------------------------- @contextmanager def assert_n_commands_pushed(self, stack, n): current_length = len(stack._stack) yield # N commands have been pushed... self.assertEqual(len(stack._stack), current_length + n) # ... and the state is at the tip of the stack... self.assertEqual(stack._index, current_length + n - 1) @contextmanager def assert_n_commands_pushed_and_undone(self, stack, n): current_length = len(stack._stack) yield # N commands have been pushed and then reverted. The stack still # contains the commands... self.assertEqual(len(stack._stack), n) # ... but we are back to the initial (clean) state self.assertEqual(stack._index, current_length - 1) def add_macro(stack, num_commands=2): command = SimpleCommand() stack.begin_macro("Increment n times") try: for i in range(num_commands): stack.push(command) finally: stack.end_macro() pyface-7.4.0/pyface/undo/tests/__init__.py0000644000076500000240000000062714176222673021440 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! pyface-7.4.0/pyface/undo/__init__.py0000644000076500000240000000072414176222673020274 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Supports undoing and scripting application commands. """ pyface-7.4.0/pyface/undo/i_undo_manager.py0000644000076500000240000000614214176222673021504 0ustar cwebsterstaff00000000000000# (C) Copyright 2007-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: # ------------------------------------------------------------------------------ # Enthought library imports. from traits.api import Bool, Event, Instance, Int, Interface, Str class IUndoManager(Interface): """The undo manager interface. An undo manager is responsible for one or more command stacks. Typically an application would have a single undo manager. """ #### 'IUndoManager' interface ############################################# #: This is the currently active command stack and may be None. Typically it #: is set when some sort of editor becomes active. #: IUndoManager and ICommandStack depend on one another, hence we can't #: directly import ICommandStack and use it here. active_stack = Instance("pyface.undo.api.ICommandStack") #: This reflects the clean state of the currently active command stack. It #: is intended to support a "document modified" indicator in the GUI. It is #: maintained by the undo manager. active_stack_clean = Bool() #: This is the name of the command that can be redone. It will be empty if #: there is no command that can be redone. It is maintained by the undo #: manager. redo_name = Str() #: This is the sequence number of the next command to be performed. It is #: incremented immediately before a command is invoked (by its 'do()' #: method). sequence_nr = Int() #: This event is fired when the index of a command stack changes. Note that #: it may not be the active stack. stack_updated = Event(Instance("pyface.undo.api.ICommandStack")) #: This is the name of the command that can be undone. It will be empty if #: there is no command that can be undone. It is maintained by the undo #: manager. undo_name = Str() ########################################################################### # 'IUndoManager' interface. ########################################################################### def redo(self): """ Redo the last undone command of the active command stack. """ def undo(self): """ Undo the last command of the active command stack. """ pyface-7.4.0/pyface/undo/i_command.py0000644000076500000240000000623014176222673020461 0ustar cwebsterstaff00000000000000# (C) Copyright 2007-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: # ------------------------------------------------------------------------------ # Enthought library imports. from traits.api import Any, Interface, Str class ICommand(Interface): """The command interface. The state of the data can be changed by passing an instance that implements this interface to the 'push()' method of a command stack along with any arguments. """ #### 'ICommand' interface ################################################# #: This is the data on which the command operates. data = Any() #: This is the name of the command as it will appear in any GUI element. It #: may include '&' which will be automatically removed whenever it is #: inappropriate. name = Str() ########################################################################### # 'ICommand' interface. ########################################################################### def do(self): """This is called by the command stack to do the command and to return any value. The command must save any state necessary for the 'redo()' and 'undo()' methods to work. The class's __init__() must also ensure that deep copies of any arguments are made if appropriate. It is guaranteed that this will only ever be called once and that it will be called before any call to 'redo()' or 'undo()'. """ def merge(self, other): """This is called by the command stack to try and merge another command with this one. True is returned if the commands were merged. 'other' is the command that is about to be executed. If the commands are merged then 'other' will discarded and not placed on the command stack. A subsequent undo or redo of this modified command must have the same effect as the two original commands. """ def redo(self): """This is called by the command stack to redo the command. Any returned value will replace the value that the command stack references from the original call to 'do()' or previous call to 'redo()'. """ def undo(self): """ This is called by the command stack to undo the command. """ pyface-7.4.0/pyface/undo/api.py0000644000076500000240000000221014176222673017276 0ustar cwebsterstaff00000000000000# ------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: # ------------------------------------------------------------------------------ """ API for ``pyface.undo``. Interfaces and Implementations ------------------------------ - :class:`~.ICommand` - :class:`~.AbstractCommand` - :class:`~.ICommandStack` - :class:`~.CommandStack` - :class:`~.IUndoManager` - :class:`~.UndoManager` """ from .abstract_command import AbstractCommand from .command_stack import CommandStack from .i_command import ICommand from .i_command_stack import ICommandStack from .i_undo_manager import IUndoManager from .undo_manager import UndoManager pyface-7.4.0/pyface/undo/action/0000755000076500000240000000000014176460551017434 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/undo/action/command_action.py0000644000076500000240000000457414176222673022774 0ustar cwebsterstaff00000000000000# (C) Copyright 2007-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: # ------------------------------------------------------------------------------ # Enthought library imports. from pyface.action.api import Action from traits.api import Any, Callable, Instance from ..i_command_stack import ICommandStack class CommandAction(Action): """The CommandAction class is an Action class that wraps undo/redo commands. It is only useful for commands that do not take any arguments or return any result. """ #### 'CommandAction' interface ############################################ #: The command to create when the action is performed. command = Callable() #: The command stack onto which the command will be pushed when the action #: is performed. command_stack = Instance(ICommandStack) #: This is the data on which the command operates. data = Any() ########################################################################### # 'Action' interface. ########################################################################### def perform(self, event): """This is reimplemented to push a new command instance onto the command stack. """ self.command_stack.push(self.command(data=self.data)) def _name_default(self): """ This gets the action name from the command. """ if self.command: name = self.command().name else: name = "" return name pyface-7.4.0/pyface/undo/action/undo_action.py0000644000076500000240000000404214176222673022311 0ustar cwebsterstaff00000000000000# (C) Copyright 2007-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: # ------------------------------------------------------------------------------ # Local imports. from .abstract_command_stack_action import AbstractCommandStackAction class UndoAction(AbstractCommandStackAction): """ An action that undos the last command of the active command stack. """ ########################################################################### # 'Action' interface. ########################################################################### def perform(self, event): """ Perform the action. """ self.undo_manager.undo() ########################################################################### # 'AbstractUndoAction' interface. ########################################################################### def _update_action(self): """ Update the state of the action. """ name = self.undo_manager.undo_name if name: name = "&Undo " + name self.enabled = True else: name = "&Undo" self.enabled = False self.name = name pyface-7.4.0/pyface/undo/action/tests/0000755000076500000240000000000014176460551020576 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/undo/action/tests/test_actions.py0000644000076500000240000000331014176222673023645 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.undo.api import CommandStack, UndoManager from pyface.undo.tests.testing_commands import SimpleCommand from pyface.undo.action.api import RedoAction, UndoAction class TestRedoAction(unittest.TestCase): def setUp(self): self.stack = CommandStack() self.undo_manager = UndoManager() self.stack.undo_manager = self.undo_manager self.undo_manager.active_stack = self.stack self.command = SimpleCommand() def test_update(self): redo_action = RedoAction(command=self.command, undo_manager=self.undo_manager) self.stack.push(self.command) self.undo_manager.undo() self.assertTrue(redo_action.enabled) self.assertEqual(redo_action.name, "&Redo Increment by 1") class TestUndoAction(unittest.TestCase): def setUp(self): self.stack = CommandStack() self.undo_manager = UndoManager() self.stack.undo_manager = self.undo_manager self.undo_manager.active_stack = self.stack self.command = SimpleCommand() def test_update(self): undo_action = UndoAction(command=self.command, undo_manager=self.undo_manager) self.stack.push(self.command) self.assertTrue(undo_action.enabled) self.assertEqual(undo_action.name, "&Undo Increment by 1") pyface-7.4.0/pyface/undo/action/tests/__init__.py0000644000076500000240000000062714176222673022715 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! pyface-7.4.0/pyface/undo/action/__init__.py0000644000076500000240000000062714176222673021553 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! pyface-7.4.0/pyface/undo/action/redo_action.py0000644000076500000240000000406014176222673022275 0ustar cwebsterstaff00000000000000# (C) Copyright 2007-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: # ------------------------------------------------------------------------------ # Local imports. from .abstract_command_stack_action import AbstractCommandStackAction class RedoAction(AbstractCommandStackAction): """An action that redos the last command undone of the active command stack. """ ########################################################################### # 'Action' interface. ########################################################################### def perform(self, event): """ Perform the action. """ self.undo_manager.redo() ########################################################################### # 'AbstractUndoAction' interface. ########################################################################### def _update_action(self): """ Update the state of the action. """ name = self.undo_manager.redo_name if name: name = "&Redo " + name self.enabled = True else: name = "&Redo" self.enabled = False self.name = name pyface-7.4.0/pyface/undo/action/api.py0000644000076500000240000000254314176222673020564 0ustar cwebsterstaff00000000000000# (C) Copyright 2007-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: # ------------------------------------------------------------------------------ """ API for ``pyface.undo.action``. CommandAction and useful subclasses ----------------------------------- - :class:`~.CommandAction` - :class:`~.RedoAction` - :class:`~.UndoAction` """ from .command_action import CommandAction from .redo_action import RedoAction from .undo_action import UndoAction pyface-7.4.0/pyface/undo/action/abstract_command_stack_action.py0000644000076500000240000000627314176222673026042 0ustar cwebsterstaff00000000000000# (C) Copyright 2008-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: # ------------------------------------------------------------------------------ # Enthought library imports. from pyface.action.api import Action from traits.api import Instance # Local library imports from ..i_undo_manager import IUndoManager class AbstractCommandStackAction(Action): """The abstract base class for all actions that operate on a command stack. """ #### 'AbstractCommandStackAction' interface ############################### #: The undo manager. undo_manager = Instance(IUndoManager) ########################################################################### # 'object' interface. ########################################################################### def __init__(self, **traits): """ Initialise the instance. """ super().__init__(**traits) self.undo_manager.observe( self._on_stack_updated, "stack_updated" ) # Update the action to initialise it. self._update_action() ########################################################################### # 'Action' interface. ########################################################################### def destroy(self): """Called when the action is no longer required. By default this method does nothing, but this would be a great place to unhook trait listeners etc. """ self.undo_manager.observe( self._on_stack_updated, "stack_updated", remove=True ) ########################################################################### # Protected interface. ########################################################################### def _update_action(self): """ Update the state of the action. """ raise NotImplementedError ########################################################################### # Private interface. ########################################################################### def _on_stack_updated(self, event): """ Handle changes to the state of a command stack. """ stack = event.new # Ignore unless it is the active stack. if stack is self.undo_manager.active_stack: self._update_action() pyface-7.4.0/pyface/undo/undo_manager.py0000644000076500000240000001057514176222673021201 0ustar cwebsterstaff00000000000000# (C) Copyright 2008-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: # ------------------------------------------------------------------------------ # Enthought library imports. from traits.api import ( Bool, Event, HasTraits, Instance, Int, Property, Str, provides, observe, ) # Local imports. from .i_undo_manager import IUndoManager @provides(IUndoManager) class UndoManager(HasTraits): """The UndoManager class is the default implementation of the IUndoManager interface. """ #### 'IUndoManager' interface ############################################# #: This is the currently active command stack and may be None. Typically it #: is set when some sort of editor becomes active. active_stack = Instance("pyface.undo.api.ICommandStack") #: This reflects the clean state of the currently active command stack. It #: is intended to support a "document modified" indicator in the GUI. It is #: maintained by the undo manager. active_stack_clean = Property(Bool) #: This is the name of the command that can be redone. It will be empty if #: there is no command that can be redone. It is maintained by the undo #: manager. redo_name = Property(Str) #: This is the sequence number of the next command to be performed. It is #: incremented immediately before a command is invoked (by its 'do()' #: method). sequence_nr = Int() #: This event is fired when the index of a command stack changes. The value #: of the event is the stack that has changed. Note that it may not be the #: active stack. stack_updated = Event() #: This is the name of the command that can be undone. It will be empty if #: there is no command that can be undone. It is maintained by the undo #: manager. undo_name = Property(Str) ########################################################################### # 'IUndoManager' interface. ########################################################################### def redo(self): """ Redo the last undone command of the active command stack. """ if self.active_stack is not None: self.active_stack.redo() def undo(self): """ Undo the last command of the active command stack. """ if self.active_stack is not None: self.active_stack.undo() ########################################################################### # Private interface. ########################################################################### @observe("active_stack") def _update_stack_updated(self, event): """ Handle a different stack becoming active. """ new = event.new # Pretend that the stack contents have changed. self.stack_updated = new def _get_active_stack_clean(self): """ Get the current clean state. """ if self.active_stack is None: active_stack_clean = True else: active_stack_clean = self.active_stack.clean return active_stack_clean def _get_redo_name(self): """ Get the current redo name. """ if self.active_stack is None: redo_name = "" else: redo_name = self.active_stack.redo_name return redo_name def _get_undo_name(self): """ Get the current undo name. """ if self.active_stack is None: undo_name = "" else: undo_name = self.active_stack.undo_name return undo_name pyface-7.4.0/pyface/undo/i_command_stack.py0000644000076500000240000001033314176222673021645 0ustar cwebsterstaff00000000000000# (C) Copyright 2007-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Riverbank Computing Limited # Description: # ------------------------------------------------------------------------------ # Enthought library imports. from traits.api import Bool, Instance, Interface, Str # Local imports. from .i_undo_manager import IUndoManager class ICommandStack(Interface): """The command stack interface. A command stack is responsible for managing the changes to a data model and recording those changes so that they can be undone or redone. """ #### 'ICommandStack' interface ############################################ #: This is the clean state of the stack. Its value changes as commands are #: undone and redone. It can also be explicity set to mark the current #: stack position as being clean (when the data is saved to disk for #: example). clean = Bool() #: This is the name of the command that can be redone. It will be empty if #: there is no command that can be redone. It is maintained by the undo #: stack. redo_name = Str() #: This is the undo manager that manages this stack. undo_manager = Instance(IUndoManager) #: This is the name of the command that can be undone. It will be empty if #: there is no command that can be undone. It is maintained by the undo #: stack. undo_name = Str() ########################################################################### # 'ICommandStack' interface. ########################################################################### def begin_macro(self, name): """This begins a macro by creating an empty command with the given 'name'. The commands passed to all subsequent calls to 'push()' will be contained in the macro until the next call to 'end_macro()'. Macros may be nested. The stack is disabled (ie. nothing can be undone or redone) while a macro is being created (ie. while there is an outstanding 'end_macro()' call). """ def clear(self): """This clears the stack, without undoing or redoing any commands, and leaves the stack in a clean state. It is typically used when all changes to the data have been abandoned. """ def end_macro(self): """ This ends a macro. """ def push(self, command): """This executes a command and saves it on the command stack so that it can be subsequently undone and redone. 'command' is an instance that implements the ICommand interface. Its 'do()' method is called to execute the command. If any value is returned by 'do()' then it is returned by 'push()'. The command stack will keep a reference to the result so that it can recognise it as an argument to a subsequent command (which allows a script to properly save a result needed later). """ def redo(self, sequence_nr=0): """If 'sequence_nr' is 0 then the last command that was undone is redone and any result returned. Otherwise commands are redone up to and including the given 'sequence_nr' and any result of the last of these is returned. """ def undo(self, sequence_nr=0): """If 'sequence_nr' is 0 then the last command is undone. Otherwise commands are undone up to and including the given 'sequence_nr'. """ pyface-7.4.0/pyface/font.py0000644000076500000240000003272414176222673016543 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Toolkit-independent font utilities. Pyface fonts are intended to be generic, but able to be mapped fairly well to most backend toolkit font descriptions. In most cases we can describe fonts along the common dimensions that are used by CSS, Wx, and Qt. However when it comes to actually working with a font, the toolkit needs to take the description and produce something that is as close as possible to the specification, but within the constraints of the toolkit, operating system and available fonts on the machine where this is being executed. Because of this inherent ambiguity in font specification, this system tries to be flexible in what it accepts as a font specification, rather than trying to specify a unique canoncial form. Font Properties --------------- The properties that fonts have are: Font Family A list of font family names in order of preference, such as "Helvetica" or "Comic Sans". In the case of a font that has been selected by the toolkit this list will have one value which is the actual font family name. There are several generic font family names that can be used as fall-backs in case all preferred fonts are unavailable. The allowed values are: "default" The application's default system font. "fantasy" A primarily decorative font, but with recognisable characters. "decorative" A synonym for "fantasy". "serif" A proportional serif font, such as Times New Roman or Garamond. "roman" A synonym for "serif". "cursive" A font which resembles hand-written cursive text, such as Zapf Chancery. "script" A synonym for "cursive". "sans-serif" A proportional sans-serif font, such as Helvetica or Arial. "swiss" A synonym for "sans-serif". "monospace" A fixed-pitch sans-serif font, such as Source Code Pro or Roboto Mono. Commonly used for display of code. "modern" A synonym for "monospace". "typewriter" A fixed-pitch serif font which resembles typewritten text, such as Courier. Commonly used for display of code. "teletype" A synonym for "typewriter". These special names will be converted into appropriate toolkit flags which correspond to these generic font specifications. Weight How thick or dark the font glyphs are. These can be given as a number from 1 (lightest) to 999 (darkest), but are typically specified by a multiple of 100 from 100 to 900, with a number of synonyms such as 'light' and 'bold' available for those values. Stretch The amount of horizontal compression or expansion to apply to the glyphs. These can be given as a percentage between 50% and 200%, or by strings such as 'condensed' and 'expanded' that correspond to those values. Style This selects either 'oblique' or 'italic' variants typefaces of the given font family. If neither is wanted, the value is 'normal'. Size The overall size of the glyphs. This can be expressed either as the numeric size in points, or as a string such as "small" or "large". Variants A set of additional font style specifiers, such as "small-caps", "strikethrough", "underline" or "overline", where supported by the underlying toolkit. Font Specificiation Class ------------------------- The Pyface Font class is a HasStrictTraits class which specifies a requested font. It has methods that convert the Font class to and from a toolkit Font class. """ from traits.api import ( BaseCFloat, CList, CSet, Enum, HasStrictTraits, Map, Str, ) from traits.trait_type import NoDefaultSpecified #: Font weight synonyms. #: These are alternate convenience names for font weights. #: The intent is to allow a developer to use a common name (eg. "bold") instead #: of having to remember the corresponding number (eg. 700). #: These come from: #: - the OpenType specification: https://docs.microsoft.com/en-us/typography/opentype/spec/os2#usweightclass #: - QFont weights: https://doc.qt.io/qt-5/qfont.html#Weight-enum #: - WxPython font weights: https://wxpython.org/Phoenix/docs/html/wx.FontWeight.enumeration.html #: - CSS Common weight name mapping: https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight#common_weight_name_mapping #: - values used by Enable: https://github.com/enthought/enable/blob/78d2e494097fac71cc5c73efef5fb464963fb4db/kiva/fonttools/_constants.py#L90-L105 #: See also: https://gist.github.com/lukaszgrolik/5849599 WEIGHTS = {str(i): i for i in range(100, 1001, 100)} WEIGHTS.update({ 'thin': 100, 'hairline': 100, 'extra-light': 200, 'ultra-light': 200, 'ultralight': 200, 'light': 300, 'normal': 400, 'regular': 400, 'book': 400, 'medium': 500, 'roman': 500, 'semi-bold': 600, 'demi-bold': 600, 'demi': 600, 'bold': 700, 'extra-bold': 800, 'ultra-bold': 800, 'extra bold': 800, 'black': 900, 'heavy': 900, 'extra-heavy': 1000, }) #: Font stretch synonyms. #: These are alternate convenience names for font stretch/width values. #: The intent is to allow a developer to use a common name (eg. "expanded") #: instead of having to remember the corresponding number (eg. 125). #: These come from: #: - the OpenType specification: https://docs.microsoft.com/en-us/typography/opentype/spec/os2#uswidthclass #: - QFont stetch: https://doc.qt.io/qt-5/qfont.html#Stretch-enum #: - CSS font-stretch: https://developer.mozilla.org/en-US/docs/Web/CSS/font-stretch #: - values used by Enable: https://github.com/enthought/enable/blob/78d2e494097fac71cc5c73efef5fb464963fb4db/kiva/fonttools/_constants.py#L78-L88 STRETCHES = { 'ultra-condensed': 50, 'extra-condensed': 62.5, 'condensed': 75, 'semi-condensed': 87.5, 'normal': 100, 'semi-expanded': 112.5, 'expanded': 125, 'extra-expanded': 150, 'ultra-expanded': 200, } #: Font size synonyms. #: These are alternate convenience names for font size values. #: The intent is to allow a developer to use a common name (eg. "small") #: instead of having to remember the corresponding number (eg. 10). #: These come from CSS font-size: https://developer.mozilla.org/en-US/docs/Web/CSS/font-size SIZES = { 'xx-small': 7.0, 'x-small': 9.0, 'small': 10.0, 'medium': 12.0, 'large': 14.0, 'x-large': 18.0, 'xx-large': 20.0, } STYLES = ('normal', 'italic', 'oblique') #: Font variants. Currently only small caps variants are exposed in Qt, and #: nothing in Wx. In the future this could include things like swashes, #: numeric variants, and so on, as exposed in the toolkit. VARIANTS = ['small-caps'] #: Additional markings on or around the glyphs of the font that are not part #: of the glyphs themselves. Currently Qt and Wx support underline and #: strikethrough, and Qt supports overline. In the future overlines and other #: decorations may be supported, as exposed in the toolkit. DECORATIONS = ['underline', 'strikethrough', 'overline'] #: A trait for font families. FontFamily = CList(Str, ['default']) #: A trait for font weights. FontWeight = Map(WEIGHTS, default_value='normal') #: A trait for font styles. FontStyle = Enum(STYLES) #: A trait for font variant properties. FontVariants = CSet(Enum(VARIANTS)) #: A trait for font decorator properties. FontDecorations = CSet(Enum(DECORATIONS)) class FontStretch(BaseCFloat): """ Trait type for font stretches. The is a CFloat trait which holds floating point values between 50 and 200, inclusive. In addition to values which can be converted to floats, this trait also accepts named synonyms for sizes which are converted to the associated commonly accepted weights: - 'ultra-condensed': 50 - 'extra-condensed': 62.5 - 'condensed': 75 - 'semi-condensed': 87.5 - 'normal': 100 - 'semi-expanded': 112.5 - 'expanded': 125 - 'extra-expanded': 150 - 'ultra-expanded': 200 """ #: The default value for the trait. default_value = 100.0 def __init__(self, default_value=NoDefaultSpecified, **metadata): if default_value != NoDefaultSpecified: default_value = self.validate(None, None, default_value) super().__init__(default_value, **metadata) def validate(self, object, name, value): if isinstance(value, str) and value.endswith('%'): value = value[:-1] value = STRETCHES.get(value, value) value = super().validate(object, name, value) if not 50 <= value <= 200: self.error(object, name, value) return value def info(self): info = ( "a float from 50 to 200, " "a value that can convert to a float from 50 to 200, " ) info += ', '.join(repr(key) for key in SIZES) info += ( " or a string with a float value from 50 to 200 followed by '%'" ) return info class FontSize(BaseCFloat): """ Trait type for font sizes. The is a CFloat trait which also allows values which are keys of the size dictionary, and also ignores trailing 'pt' ot 'px' annotation in string values. The value stored is a float. """ #: The default value for the trait. default_value = 12.0 def __init__(self, default_value=NoDefaultSpecified, **metadata): if default_value != NoDefaultSpecified: default_value = self.validate(None, None, default_value) super().__init__(default_value, **metadata) def validate(self, object, name, value): if ( isinstance(value, str) and (value.endswith('pt') or value.endswith('px')) ): value = value[:-2] value = SIZES.get(value, value) value = super().validate(object, name, value) if value <= 0: self.error(object, name, value) return value def info(self): info = ( "a positive float, a value that can convert to a positive float, " ) info += ', '.join(repr(key) for key in SIZES) info += ( " or a string with a positive float value followed by 'pt' or 'px'" ) return info class Font(HasStrictTraits): """A toolkit-independent font specification. This class represents a *request* for a font with certain characteristics, not a concrete font that can be used for drawing. Font objects returned from the toolkit may or may not match what was requested, depending on the capabilities of the toolkit, OS, and the fonts installed on a particular computer. """ #: The preferred font families. family = FontFamily() #: The weight of the font. weight = FontWeight() #: How much the font is expanded or compressed. stretch = FontStretch() #: The style of the font. style = FontStyle() #: The size of the font. size = FontSize() #: The font variants. variants = FontVariants() #: The font decorations. decorations = FontDecorations() @classmethod def from_toolkit(cls, toolkit_font): """ Create a Font from a toolkit font object. Parameters ---------- toolkit_font : any A toolkit font to be converted to a corresponding class instance, within the limitations of the options supported by the class. """ from pyface.toolkit import toolkit_object toolkit_font_to_properties = toolkit_object( 'font:toolkit_font_to_properties') return cls(**toolkit_font_to_properties(toolkit_font)) def to_toolkit(self): """ Create a toolkit font object from the Font instance. Returns ------- toolkit_font : any A toolkit font which matches the property of the font as closely as possible given the constraints of the toolkit. """ from pyface.toolkit import toolkit_object font_to_toolkit_font = toolkit_object('font:font_to_toolkit_font') return font_to_toolkit_font(self) def __str__(self): """ Produce a CSS2-style representation of the font. """ terms = [] if self.style != 'normal': terms.append(self.style) terms.extend( variant for variant in VARIANTS if variant in self.variants ) terms.extend( decoration for decoration in DECORATIONS if decoration in self.decorations ) if self.weight != 'normal': terms.append(self.weight) if self.stretch != 100: terms.append("{:g}%".format(self.stretch)) size = self.size # if size is an integer we want "12pt" not "12.pt" if int(size) == size: size = int(size) terms.append("{}pt".format(size)) terms.append( ', '.join( repr(family) if ' ' in family else family for family in self.family ) ) return ' '.join(terms) def __repr__(self): traits = self.trait_get(self.editable_traits()) trait_args = ', '.join( "{}={!r}".format(name, value) for name, value in traits.items() ) return "{}({})".format(self.__class__.__name__, trait_args) pyface-7.4.0/pyface/i_layout_item.py0000644000076500000240000000221314176222673020426 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 Enum, Int, Interface, Tuple #: Value that indicates the default size values should be used. DEFAULT_SIZE = -1 #: Trait for sizes of widgets. Size = Tuple(Int(DEFAULT_SIZE), Int(DEFAULT_SIZE)) #: Trait for size policy values. SizePolicy = Enum("default", "fixed", "preferred", "expand") class ILayoutItem(Interface): """ An item that can participate in layout. """ #: The minimum size that the item can take. minimum_size = Size #: The maximum size that the item can take. maximum_size = Size #: Weight factor used to distribute extra space between widgets. stretch = Tuple(Int, Int) #: How the item should behave when more space is available. size_policy = Tuple(SizePolicy, SizePolicy) pyface-7.4.0/pyface/i_pil_image.py0000644000076500000240000000426314176222673020030 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 interface for a PIL Image. """ from traits.api import HasStrictTraits, Instance from pyface.i_image import IImage class IPILImage(IImage): """ The interface for a image that wraps a PIL Image. """ # 'IPILImage' interface -------------------------------------------- #: The PIL Image instance. image = Instance("PIL.Image.Image") class MPILImage(HasStrictTraits): """ The base implementation mixin for a image that wraps a PIL Image. """ # 'IPILImage' interface -------------------------------------------- #: The PIL Image instance. image = Instance("PIL.Image.Image") def __init__(self, image, **traits): super().__init__(image=image, **traits) def create_bitmap(self, size=None): """ Creates a bitmap image for this image. Parameters ---------- size : (int, int) or None The desired size as a (width, height) tuple, or None if wanting default image size. Returns ------- image : bitmap The toolkit bitmap corresponding to the image and the specified size. """ from pyface.util.image_helpers import image_to_bitmap return image_to_bitmap(self.create_image(size)) def create_icon(self, size=None): """ Creates an icon for this image. Parameters ---------- size : (int, int) or None The desired size as a (width, height) tuple, or None if wanting default icon size. Returns ------- image : icon The toolkit image corresponding to the image and the specified size as an icon. """ from pyface.util.image_helpers import bitmap_to_icon return bitmap_to_icon(self.create_bitmap(size)) pyface-7.4.0/pyface/image/0000755000076500000240000000000014176460550016273 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/image/tests/0000755000076500000240000000000014176460550017435 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/image/tests/test_image.py0000644000076500000240000005345214176222673022143 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 closing from os import stat from importlib_resources import files from pathlib import Path import shutil import tempfile import time import unittest from zipfile import ZipFile, ZIP_DEFLATED from pyface.image_resource import ImageResource from pyface.ui_traits import Border, Margin from ..image import ( FastZipFile, ImageLibrary, ImageVolume, ImageVolumeInfo, ZipFileReference, join_image_name, split_image_name, time_stamp_for, ) LIBRARY_DIR = files('pyface.image') / "library" ICONS_FILE = LIBRARY_DIR / "icons.zip" TEST_IMAGES_DIR = files('pyface.tests') / "images" class TestJoinImageName(unittest.TestCase): def test_simple(self): image_name = join_image_name("icons", "red_ball.jpg") self.assertEqual(image_name, "@icons:red_ball.jpg") def test_extension(self): image_name = join_image_name("icons", "red_ball.png") self.assertEqual(image_name, "@icons:red_ball") def test_double_extension(self): image_name = join_image_name("icons", "red_ball.foo.png") self.assertEqual(image_name, "@icons:red_ball.foo.png") class TestSplitImageName(unittest.TestCase): def test_simple(self): volume_name, file_name = split_image_name("@icons:red_ball.jpg") self.assertEqual(volume_name, "icons") self.assertEqual(file_name, "red_ball.jpg") def test_extension(self): volume_name, file_name = split_image_name("@icons:red_ball") self.assertEqual(volume_name, "icons") self.assertEqual(file_name, "red_ball.png") def test_no_at_symbol(self): volume_name, file_name = split_image_name("icons:red_ball.jpg") # XXX this should probably raise rather than doing this self.assertEqual(volume_name, "cons") self.assertEqual(file_name, "red_ball.jpg") def test_no_colon(self): volume_name, file_name = split_image_name("@red_ball.jpg") # XXX this should probably raise rather than doing this self.assertEqual(volume_name, "red_ball.jp") self.assertEqual(file_name, "@red_ball.jpg") def test_no_colon_or_at_symbol(self): volume_name, file_name = split_image_name("red_ball.jpg") # XXX this should probably raise rather than doing this self.assertEqual(volume_name, "ed_ball.jp") self.assertEqual(file_name, "red_ball.jpg") class TestFastZipFile(unittest.TestCase): def test_read_icons_red_ball(self): zf = FastZipFile(path=ICONS_FILE) file_bytes = zf.read("red_ball.png") self.assertTrue(file_bytes.startswith(b"\x89PNG")) def test_namelist_icons(self): zf = FastZipFile(path=ICONS_FILE) names = zf.namelist() self.assertTrue("red_ball.png" in names) self.assertEqual(names, zf.zf.namelist()) def test_close_icons(self): zf = FastZipFile(path=ICONS_FILE) actual_zf = zf.zf self.assertIs(zf._zf, actual_zf) zf.close() self.assertIsNone(zf._zf) def test_eventual_zipfile_close(self): zf = FastZipFile(path=ICONS_FILE) self.assertFalse(zf._running) start_time = time.time() actual_zf = zf.zf end_time = time.time() self.assertIsNotNone(actual_zf) self.assertGreaterEqual(zf.time_stamp, start_time) self.assertLessEqual(zf.time_stamp, end_time) self.assertTrue(zf._running) self.assertIs(zf._zf, actual_zf) # wait for thread to clean-up zipfile # XXX this is not nice while time.time() <= end_time + 3.0: time.sleep(1.0) self.assertFalse(zf._running) self.assertIsNone(zf._zf) class TestImageVolume(unittest.TestCase): def test_init_empty(self): volume = ImageVolume() self.assertEqual(volume.name, "") self.assertEqual(volume.images, []) self.assertEqual(volume.catalog, {}) self.assertEqual(volume.category, "General") self.assertEqual(volume.keywords, []) self.assertEqual(volume.aliases, []) self.assertEqual(volume.path, "") self.assertTrue(volume.is_zip_file) self.assertIsNone(volume.zip_file) self.assertEqual(volume.time_stamp, "") self.assertEqual(len(volume.info), 1) self.assertIsInstance(volume.info[0], ImageVolumeInfo) def test_empty_zipfile(self): with tempfile.TemporaryDirectory() as dir_path: path = Path(dir_path) / "test.zip" ZipFile(path, "w", ZIP_DEFLATED).close() with closing(FastZipFile(path=path)) as zf: time_stamp = time_stamp_for(stat(path).st_mtime) volume = ImageVolume(name="test", path=path, zip_file=zf) self.assertEqual(volume.name, "test") self.assertEqual(volume.images, []) self.assertEqual(volume.catalog, {}) self.assertEqual(volume.category, "General") self.assertEqual(volume.keywords, []) self.assertEqual(volume.aliases, []) self.assertEqual(Path(volume.path), path) self.assertTrue(volume.is_zip_file) self.assertIs(volume.zip_file, zf) self.assertEqual(volume.time_stamp, time_stamp) self.assertEqual(len(volume.info), 1) self.assertIsInstance(volume.info[0], ImageVolumeInfo) def test_empty_directory(self): with tempfile.TemporaryDirectory() as dir_path: path = Path(dir_path) / "test" path.mkdir() time_stamp = time_stamp_for(stat(path).st_mtime) volume = ImageVolume(name="test", path=path, is_zip_file=False) self.assertEqual(volume.name, "test") self.assertEqual(volume.images, []) self.assertEqual(volume.catalog, {}) self.assertEqual(volume.category, "General") self.assertEqual(volume.keywords, []) self.assertEqual(volume.aliases, []) self.assertEqual(Path(volume.path), path) self.assertFalse(volume.is_zip_file) self.assertIsNone(volume.zip_file) self.assertEqual(volume.time_stamp, time_stamp) self.assertEqual(len(volume.info), 1) self.assertIsInstance(volume.info[0], ImageVolumeInfo) def test_empty_directory_save(self): with tempfile.TemporaryDirectory() as dir_path: path = Path(dir_path) / "test" path.mkdir() time_stamp = time_stamp_for(stat(path).st_mtime) volume = ImageVolume(name="test", path=path, is_zip_file=False) result = volume.save() self.assertTrue(result) filenames = {file.name for file in path.iterdir()} self.assertEqual(filenames, {"image_volume.py", "image_info.py", "license.txt"}) # test that new file is readable time_stamp = time_stamp_for(stat(path).st_mtime) volume_2 = ImageVolume(name="test", path=path, is_zip_file=False) self.assertEqual(volume_2.name, "test") self.assertEqual(volume_2.images, []) self.assertEqual(volume_2.catalog, {}) self.assertEqual(volume_2.category, "General") self.assertEqual(volume_2.keywords, []) self.assertEqual(volume_2.aliases, []) self.assertEqual(Path(volume_2.path), path) self.assertFalse(volume_2.is_zip_file) self.assertIsNone(volume_2.zip_file) self.assertEqual(volume_2.time_stamp, time_stamp) self.assertEqual(len(volume_2.info), 1) self.assertIsInstance(volume_2.info[0], ImageVolumeInfo) def test_empty_zipfile_save(self): with tempfile.TemporaryDirectory() as dir_path: path = Path(dir_path) / "test.zip" ZipFile(path, "w", ZIP_DEFLATED).close() with closing(FastZipFile(path=path)) as zf: time_stamp = time_stamp_for(stat(path).st_mtime) volume = ImageVolume(name="test", path=path, zip_file=zf) result = volume.save() self.assertTrue(result) with closing(FastZipFile(path=path)) as zf: self.assertEqual(set(zf.namelist()), {"image_volume.py", "image_info.py", "license.txt"}) # test that new file is readable with closing(FastZipFile(path=path)) as zf: time_stamp = time_stamp_for(stat(path).st_mtime) volume_2 = ImageVolume(name="test", path=path, zip_file=zf) self.assertEqual(volume_2.name, "test") self.assertEqual(volume_2.images, []) self.assertEqual(volume_2.catalog, {}) self.assertEqual(volume_2.category, "General") self.assertEqual(volume_2.keywords, []) self.assertEqual(volume_2.aliases, []) self.assertEqual(Path(volume_2.path), path) self.assertTrue(volume_2.is_zip_file) self.assertIs(volume_2.zip_file, zf) self.assertEqual(volume_2.time_stamp, time_stamp) self.assertEqual(len(volume_2.info), 1) self.assertIsInstance(volume_2.info[0], ImageVolumeInfo) def test_save_directory(self): with tempfile.TemporaryDirectory() as dir_path: path = Path(dir_path) / "test" path.mkdir() time_stamp = time_stamp_for(stat(path).st_mtime) image_file = path / "core.png" shutil.copyfile(TEST_IMAGES_DIR / "core.png", image_file) # this should create an default ImageInfo object for the image # file as a side-effect of loading. volume = ImageVolume(name="test", path=path, is_zip_file=False) self.assertEqual(len(volume.images), 1) self.assertGreaterEqual(volume.time_stamp, time_stamp) image = volume.images[0] self.assertEqual(image.image_name, "@test:core") self.assertEqual(image.name, "core") self.assertEqual(image.volume, volume) self.assertEqual(image.description, "") self.assertEqual(image.category, "General") self.assertEqual(image.keywords, []) self.assertEqual(image.width, 64) self.assertEqual(image.height, 64) self.assertIsInstance(image.border, Border) self.assertIsInstance(image.content, Margin) self.assertIsInstance(image.label, Margin) self.assertEqual(image.alignment, "default") self.assertEqual(image.copyright, 'No copyright information specified.') result = volume.save() self.assertTrue(result) filenames = {file.name for file in path.iterdir()} self.assertEqual(filenames, {"core.png", "image_volume.py", "image_info.py", "license.txt"}) # test that new file is readable time_stamp = time_stamp_for(stat(path).st_mtime) volume_2 = ImageVolume(name="test", path=path, is_zip_file=False) self.assertEqual(volume_2.name, "test") self.assertEqual(len(volume_2.images), 1) self.assertEqual(len(volume_2.catalog), 1) self.assertIn("@test:core", volume_2.catalog) self.assertEqual(volume_2.category, "General") self.assertEqual(volume_2.keywords, []) self.assertEqual(volume_2.aliases, []) self.assertEqual(Path(volume_2.path), path) self.assertFalse(volume_2.is_zip_file) self.assertIsNone(volume_2.zip_file) self.assertEqual(volume_2.time_stamp, time_stamp) self.assertEqual(len(volume_2.info), 1) self.assertIsInstance(volume_2.info[0], ImageVolumeInfo) image = volume_2.images[0] self.assertEqual(image.image_name, "@test:core") self.assertEqual(image.name, "core") self.assertEqual(image.volume, volume_2) self.assertEqual(image.description, "") self.assertEqual(image.category, "General") self.assertEqual(image.keywords, []) self.assertEqual(image.width, 64) self.assertEqual(image.height, 64) self.assertIsInstance(image.border, Border) self.assertIsInstance(image.content, Margin) self.assertIsInstance(image.label, Margin) self.assertEqual(image.alignment, "default") self.assertEqual(image.copyright, 'No copyright information specified.') # do one more save to smoke-test other code paths volume_2.save() def test_save_zipfile(self): with tempfile.TemporaryDirectory() as dir_path: path = Path(dir_path) / "test.zip" with ZipFile(path, "w", ZIP_DEFLATED) as zf: zf.write(TEST_IMAGES_DIR / "core.png", "core.png") with closing(FastZipFile(path=path)) as zf: time_stamp = time_stamp_for(stat(path).st_mtime) # this should create an default ImageInfo object for the image # file as a side-effect of loading. volume = ImageVolume(name="test", path=path, zip_file=zf) self.assertEqual(len(volume.images), 1) self.assertGreaterEqual(volume.time_stamp, time_stamp) image = volume.images[0] self.assertEqual(image.image_name, "@test:core") self.assertEqual(image.name, "core") self.assertEqual(image.volume, volume) self.assertEqual(image.description, "") self.assertEqual(image.category, "General") self.assertEqual(image.keywords, []) self.assertEqual(image.width, 64) self.assertEqual(image.height, 64) self.assertIsInstance(image.border, Border) self.assertIsInstance(image.content, Margin) self.assertIsInstance(image.label, Margin) self.assertEqual(image.alignment, "default") self.assertEqual(image.copyright, 'No copyright information specified.') result = volume.save() self.assertTrue(result) with closing(FastZipFile(path=path)) as zf: self.assertEqual(set(zf.namelist()), {"core.png", "image_volume.py", "image_info.py", "license.txt"}) # test that new file is readable with closing(FastZipFile(path=path)) as zf: time_stamp = time_stamp_for(stat(path).st_mtime) volume_2 = ImageVolume(name="test", path=path, zip_file=zf) self.assertEqual(volume_2.name, "test") self.assertEqual(len(volume_2.images), 1) self.assertEqual(len(volume_2.catalog), 1) self.assertIn("@test:core", volume_2.catalog) self.assertEqual(volume_2.category, "General") self.assertEqual(volume_2.keywords, []) self.assertEqual(volume_2.aliases, []) self.assertEqual(Path(volume_2.path), path) self.assertTrue(volume_2.is_zip_file) self.assertIs(volume_2.zip_file, zf) self.assertEqual(volume_2.time_stamp, time_stamp) self.assertEqual(len(volume_2.info), 1) self.assertIsInstance(volume_2.info[0], ImageVolumeInfo) image = volume_2.images[0] self.assertEqual(image.image_name, "@test:core") self.assertEqual(image.name, "core") self.assertEqual(image.volume, volume_2) self.assertEqual(image.description, "") self.assertEqual(image.category, "General") self.assertEqual(image.keywords, []) self.assertEqual(image.height, 64) self.assertEqual(image.width, 64) self.assertIsInstance(image.border, Border) self.assertIsInstance(image.content, Margin) self.assertIsInstance(image.label, Margin) self.assertEqual(image.alignment, "default") self.assertEqual(image.copyright, 'No copyright information specified.') # do one more save to smoke-test other code paths volume_2.save() def test_icons_zipfile_volume(self): time_stamp = time_stamp_for(stat(ICONS_FILE).st_mtime) with closing(FastZipFile(path=ICONS_FILE)) as zf: volume = ImageVolume(name="icons", path=ICONS_FILE, zip_file=zf) self.assertEqual(volume.name, "icons") self.assertTrue( any( image.image_name == "@icons:red_ball" for image in volume.images ) ) self.assertTrue("@icons:red_ball" in volume.catalog) self.assertEqual(volume.category, "General") self.assertEqual(volume.keywords, []) self.assertEqual(volume.aliases, []) self.assertEqual(Path(volume.path), ICONS_FILE) self.assertTrue(volume.is_zip_file) self.assertIs(volume.zip_file, zf) self.assertEqual(volume.time_stamp, time_stamp) self.assertEqual(len(volume.info), 1) self.assertIsInstance(volume.info[0], ImageVolumeInfo) def test_icons_image_resource(self): with closing(FastZipFile(path=ICONS_FILE)) as zf: volume = ImageVolume(name="icons", path=ICONS_FILE, zip_file=zf) image = volume.image_resource("@icons:red_ball") self.assertIsInstance(image, ImageResource) self.assertIsInstance(image._ref, ZipFileReference) self.assertTrue(ICONS_FILE.samefile(image._ref.path)) def test_icons_image_resource_missing(self): with closing(FastZipFile(path=ICONS_FILE)) as zf: volume = ImageVolume(name="icons", path=ICONS_FILE, zip_file=zf) image = volume.image_resource("@icons:does_not_exist") self.assertIsInstance(image, ImageResource) self.assertIsInstance(image._ref, ZipFileReference) self.assertTrue(ICONS_FILE.samefile(image._ref.path)) def test_icons_image_data(self): with closing(FastZipFile(path=ICONS_FILE)) as zf: volume = ImageVolume(name="icons", path=ICONS_FILE, zip_file=zf) data = volume.image_data("@icons:red_ball") self.assertTrue(data.startswith(b"\x89PNG")) def test_icons_image_data_missing(self): with closing(FastZipFile(path=ICONS_FILE)) as zf: volume = ImageVolume(name="icons", path=ICONS_FILE, zip_file=zf) with self.assertRaises(KeyError): volume.image_data("@icons:does_not_exist") def test_icons_volume_info(self): with closing(FastZipFile(path=ICONS_FILE)) as zf: volume = ImageVolume(name="icons", path=ICONS_FILE, zip_file=zf) volume_info = volume.volume_info("@icons:red_ball") self.assertIs(volume_info, volume.info[0]) def test_volume_info(self): volume = ImageVolume( name="test", info=[ ImageVolumeInfo(image_names=["@test:one", "@test:two"]), ImageVolumeInfo(image_names=["@test:three", "@test:two"]), ] ) volume_info = volume.volume_info("@test:two") self.assertIs(volume_info, volume.info[0]) volume_info = volume.volume_info("@test:three") self.assertIs(volume_info, volume.info[1]) with self.assertRaises(ValueError): volume.volume_info("@test:four") class TestImageLibrary(unittest.TestCase): # XXX These are more in the flavor of integration tests def test_default_volumes(self): volume_names = {volume.name for volume in ImageLibrary.volumes} self.assertEqual(volume_names, {'icons', 'std'}) def test_default_catalog(self): self.assertEqual(set(ImageLibrary.catalog.keys()), {'icons', 'std'}) def test_default_images(self): image_names = {image.image_name for image in ImageLibrary.images} self.assertTrue("@icons:red_ball" in image_names) def test_image_info(self): red_ball_image_info = ImageLibrary.image_info("@icons:red_ball") self.assertEqual(red_ball_image_info.name, "red_ball") self.assertEqual(red_ball_image_info.image_name, "@icons:red_ball") self.assertEqual(red_ball_image_info.width, 16) self.assertEqual(red_ball_image_info.height, 16) def test_image_info_missing(self): missing_image_info = ImageLibrary.image_info("@icons:does_not_exist") self.assertIsNone(missing_image_info) def test_image_info_volume_missing(self): missing_image_info = ImageLibrary.image_info("@missing:does_not_exist") self.assertIsNone(missing_image_info) def test_image_resource(self): red_ball_image = ImageLibrary.image_resource("@icons:red_ball") self.assertIsInstance(red_ball_image, ImageResource) self.assertIsInstance(red_ball_image._ref, ZipFileReference) self.assertTrue(ICONS_FILE.samefile(red_ball_image._ref.path)) def test_image_resource_missing(self): missing_image = ImageLibrary.image_info("@icons:does_not_exist") self.assertIsNone(missing_image) def test_image_resource_missing_volume(self): missing_image = ImageLibrary.image_info("@missing:does_not_exist") self.assertIsNone(missing_image) def test_find_volume(self): volume = ImageLibrary.find_volume("@icons:red_ball") self.assertIsInstance(volume, ImageVolume) self.assertEqual(volume.name, "icons") self.assertTrue(ICONS_FILE.samefile(volume.path)) pyface-7.4.0/pyface/image/tests/__init__.py0000644000076500000240000000000114176222673021537 0ustar cwebsterstaff00000000000000 pyface-7.4.0/pyface/image/__init__.py0000644000076500000240000000062714176222673020413 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! pyface-7.4.0/pyface/image/library/0000755000076500000240000000000014176460550017737 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/image/library/icons.zip0000644000076500000240000004735414176222673021615 0ustar cwebsterstaff00000000000000PKjWPw array_node.pngkLOy4!Iі&ndKi X_ CFfPʠev:&`$B01K aNWPl?νwsk)H1pM*ShM0[>DڴllGb-ARI5$fH j6@fzH2+;ğĶ q#Ɖb A^ N_2@E(Z!_؁Om$.T[MCɓBthȽY4C\I'Vx7#2Ru<9z{Oʂ!'\VK`:ڗk5 Uauw,(S-\Jv>CJ(~/K|^+Ҳ;DHۭ ^i׳/DuA{;^l8!^)v'Ch۶8Oa,4ˡ}DQT)RpH'cx!9l6onq5 Mk~oV7{c A~ cPRHTO*Q k'|~8(qiFBۄI]nFd9)MʉnyL,X S7IQCܙ0lA 0c8qpM\3wiZB:G,7tDhNG0]tGYE\D|Lkz w{ tͺ$ܾZl`@^P"AFSI [zULw}L*yBR~Z8S2vvvwcEcKbu>eFab|%aY.X,6}ړSs_|1}FPoo7.F#ff975n=ӝ}[̙Rr=|7kJuPKjWPz\;b8JeoOI3iaPwBW< AiqnqqJ<-ͺw|s:boG)`[QDXu5d RP4T{C((?OYJmt *+ۤAg/18#Ѐс/'.p*~8|z2ʬYhaԹs]J6ߕBt¡ PԔmm-[~?B\.WbBLIUTD&'K7 B5kx[UpwLyqՑ Nm᫤ٳsܦ)څLJJ8r8((躞ĵw/OR&M]C8 TSav G/,L8IdӉ|o^sSi#+'GSwg [`k)={xb4\$`4nɏw0 M@ k耝}@L i( p M/IENDB`PKjWP7* ! dict_node.png[Lƿ\${)-T0["s[ڔ[l @X,"Z(-e-r)V%hԸeb,J| YKl_OrsvN@a<mXl*n؅;.D-"ƱwD> uBY7d=wBy7RF聢2.웃|~A 'R{2a: 4 'BP@9 EiCHp JR6a(]AF9G*jPS Z2䑱ȔO&=5ZNe=t~"lҐ Yj/j[ R.SW=Y̷}[JZL*~E& <ͶR2rѐu4rodyv4)Ս1 z|j&TjTE'到͐S)I=h^c$1{nm?K/+˃[ߚ[~Tԁ e߄Nz9!Ҝ8k򻗖G]Ӂ,=}ۚ'1:odbr\[?*G PڳǏ5g%JGv"JNV&;̾=n s%V*/':G[s9zֲF|>@tL߿_*֏[7WnV]ZPKjWP$float_node.png]H\"t߾?DRYAj$]SYeRQJX^!c:u9[kTBT">r[e4*jԧҥuI L)AeCUAt j+16#\FSP`F6b)͈Eň]"h%2dS[ŕiJ?c `H ^z j<8u1/q|MWԽa[45Lo=K ;Z&h6I>Z>M+H ŠNβPKjWPjP{folder-new.png{PNG  IHDRabKGDC pHYs B(xtIME UO-IDAT8˥=hSaޖ{SI۴$ V\JEAM]AP['qpE.@%-A-Bh!?!!AŇ܍L&s5hkU_z>MP\g/[}C(˧=}$?  il ci1 l22v\ޛБl*gH֡(UmSk繼Pa3. rf_>cvMl/~p0Urs0~d4Ydi1S(e`6H<%A4jF')εbm;M  T -v[7H0t]|TtZ|}OT#[mm:%b@VXF"R AbH 0"duE\6%,V4_=EP ԅ0Z`={QmAP pkk/Ggs]H"/\7ᚩIENDB`PKjWP~Ƿfunction_node.pngPNG  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`PKjWPސ~$ int_node.png]HߙK] EWx7Bm'7[Ӗ̥æo7&"Mu[$ml̍6sS6Hn 眗\ޱZP R6p7<}>[C_֪O$h$$Ib/IB2{In%=%+ɸc'AL1NR |;t#.R R!ᴓ3caaTn5\ C>Cu0Ah` Ag]`3~ Cw-1c0mq#0ѻs Dx mvFvx-L%1³R.H.,VChAiV+i/JVOF$q`硰vFw8Ox6whC^VYu2G|6koo৷ލ?^"][ 7y>.V7΍MN>(:-?1\/bDvM?󪤸4"U9˚5-WOG | hRn 7XUee?PKjWPkF  list_node.pngQmP =v&#izhK5LԎUbvK[>2 Wi U[T.]\è̘as~<_gN^+],્D^8ys+X`$}\|'"_²JX ; y dP(B(PA^ U1TPTAYePVBU@UR؎Byrh"UP@Վ?| B#@pB.Am Fm&,Gh:4Hs6Ql"]z)%.%MݤG{NY7(~J ;E9ZovʰӶa.NClrQ6 q Sl4'ܜ 'A|t?|I >_'I%g铜aʜ_p2gOqW\>'ɷ4Fo5 4W\=͖nxϝ3{>1Xl_]u%G :#Ί-1xNJp@asI}D5/|)/9'ZNfC|2eܯ'rsˬcf~MF$xj=l٬HlkioUӲҞx-{ڮ21 j@a n]z=}˗s!?sOeU>qxpc_NX8X0ɭ'CGC6fmD̵:Νbѡݎ_?n]:;ur*ܱVU Ŷm$IBvrzӶ=},c>c4o)xux79:.V'Z'}-WP; zu BwV?>B7@FUr,qsD* V֠GhNeD*ÏV:§$Mt>@`22=bT2n VKE64Ӵ{qH| ^3^};_Ϧi,z.KLAtdYffd ,!/n Sg>ə>=ʌM_ö-&~ ;1)$7`[]= ]+ Ĉj;FHf?yʲQĘ.^n@7B"_LszIENDB`PKjWP- none_node.png sb``p  $5tW ] S ] [ W W W _ _ ̠P $JJ*Z" - j@Ƥǫy3gΜ={ܹsϟpŋ/]t+W\zڵkׯ_q͛7oݺu;wܽ{޽{G?~ɓO>˗_flap~ƒ `g7@$owVY #536uFK<}]ٯ1hL~~ P1Vr .OΏ?}*:ʒ[ukLYIeʺ(-l+*aΉ}&Mĥ`ѪeD'l1v5Kim\ьW/]q;b7Y!1Sg & GE:&PKjWPobject_node.png}PNG  IHDRasBIT|d pHYs|4ktEXtSoftwarewww.inkscape.org<IDAT8;hQ{cg_l6*H0-T0tvTHlXhl,RР 1+dpvgv#Eb8wseYRz"HZ䍻FH$`{1q7m|X-a/ԭ235(L8`@l0'c({xy`uYeeGv$NI~3t1\sQ@)ku =Nem @?EoD a20CFzuP=lh:e0b @Nd#͌2, |vnb?hOܫ= ^f Mk~oV7{c A~ cPRHV[@m83,-3_DI8!1-7Fst5Z]Լ+z:l=3j1rQoRLck$$UTT!u*ݤRCbEL/1bHKAbIl.buGLA:.5^҆ZR8*83 <\`-<& FVlF&"#ڌGv#~ ь=HHād&ۑųy7z1sfE,an a,E \x3q,XI =IN!GzE|X<2+F*q*#볚mTّZ"ł$Y*7qE_eeGUR$+wvvwY]A~ XټO>sǺzE6~璒ڙRs/Vϐ,j+{H\{F䑌>ϳ_ ~߷oOeʩ>udlVnq3/PKjWPkf red_ball.pngfPNG  IHDRa pHYs  ~IDATx=K$Zrt!}%!!j!B DP!JBB"(͗Aaj? L6a|0º"(:L`T(j:mފ0X6bQK%M4Eއ^ɤrh7`<{4`  ~0vX&gmh,\;8<7]7`j𗐖(䁢IENDB`PKjWP root_node.png}PNG  IHDRasBIT|d pHYs|4ktEXtSoftwarewww.inkscape.org<IDAT8;hQ{cg_l6*H0-T0tvTHlXhl,RР 1+dpvgv#Eb8wseYRz"HZ䍻FH$`{1q7m|X-a/ԭ235(L8`@l0'c({xy`uYeeGv$NI~3t1\sQ@)ku =Nem @?EoD a20CFzuP=lh:e0b @Nd#͌2, |vnb?hOܫ= ^f Mk~oV7{c A~ cPRHMf] h kIENDB`PKjWP{s$'string_node.png_LR&$nr?˭Q LD/a]e9 BZRsmrMy9cVe;w=*FID*ΤrH}'i-׬ I#=j!MCQSiČ'ftc~J=Al ĆH7KLػtR2aũU\F+8+:m財ێ}! .7=ncpQSxLbd ixB"20|S 9Y"'N@|h ,y#ã5=:@.x(Z,ofڿ,PQOrI1}HY_UPEmH*;}=ZTr5!:{kY_-luƫ 73 v}pUȿvj2iepRZP.+ow:ܧ+WiwsUG?PKjWP{pktop_left_origin.pngkPNG  IHDR<" pHYs  ~IDATx1kPCG2CԣxEf^=h0YT АE z};%-b7x  'aGJ)-?NNRJNy*yB)81NKj'I46VQVjz3fx<&",+^(n'^pm;c, ˲aX(Ppė[t:<|b|>_gZoYEu"|:?\__.Qm=\I@aH/1 jul&WR4Mɲ?[*4MDc&ٌ{F#D0, &`۶#/a^_2b8^v<}fi777K.mۈx8V+$y#Ѩ(<<',{{66! JP4t:H)YרE }wwoV )^v#jvPVk'm9y7 MӰ, ˲muqu|_Ajj>y1aya1IAj/xGDUUiJdYFUUuye53N8~)˒j[%J)aH)躎8EQрx΂M\MՅOK?R}+CcƧʤN76]nC'm>j4|hd(&cՁ z`Y5ofeuda  'kʞSIENDB`PKjWPtuple_node.pngQ]L#!۲ƨ Жu[ZNZ",B,Zhiv|:hgJ-QY> C>l=l>|Xbb|orwO M^\y:ʘV>+ÊBI!A cA@I!H}!l2d~L@> ydalIC|E( @Bi%s(ZDy/8$!( MA2$(eP\ri͠|&*QD]TjR'>SM `$DF#>LMrQj=>xM.9C\oz> 0Hn>fff]lS'd Mv$w》)yG<|z/t|~!rd'y"r2s_% k*z̼+5Ϗ l6c\{kLӫӫsVDĒ_neG:&rGeɵoܧ5}պ. ՘PhߝX4;FK>lwJb~qAZ^~N,LN]QtI0K߾t#kgQe{يxuZY*ճ<_ 2q~앁;˳M鈹"oث~ =]KJ~DuY>kMwA{K]Щ o?iPKjWP{image_volume.pyP1n0 mN p.4Y:v i% Ǫ! G";,nA_"8f'?7KWY2ΪR7XsM# Fo/dǻA?)epԺMe׷2nA7Ƅ27rV)ݖHWdt8traits_node.pngPKjWP9Atuple_node.pngPKjWP{mDimage_volume.pyPKjWPZ2jX ! jEimage_info.pyPKjWPMNZh Glicense.txtPK+Hpyface-7.4.0/pyface/image/library/image_LICENSE.txt0000644000076500000240000000112214176222673022722 0ustar cwebsterstaff00000000000000The icons are mostly derived work from other icons. As such they are licensed accordingly to the original license: Project License File ---------------------------------------------------------------------------- Enthought BSD 3-Clause LICENSE.txt Unless stated in this file, icons are the work of Enthought, and are released under a 3 clause BSD license. Files and orginal authors: ---------------------------------------------------------------------------- pyface/image/library: std.zip | Enthought pyface-7.4.0/pyface/image/library/std.zip0000644000076500000240000016326214176222673021271 0ustar cwebsterstaff00000000000000PKjWPץ alert16.png_PNG  IHDRa pHYs  ~RIDATxKZaϟq(F9^hpv8d^sNֈ,jQEE$D!]xws*[l]|nߗ<-ܦE";|n͝E"(8P`,<RBp )Q2mz$ܶpxq|+4*>CDZa08a+NTQpYe=~ SL{j p8v :2J4jUQܝygSxC%f(;eƚAyXW~?^#hSmk~?}>z(\Xfk"c(YanKkQdWc @d,cxy'ҽ}}]#$)1M64Npw1AIܖE=G(Z9Q%@1kˉ}FŠ e 2D9]0#asy\Q+W((i"pvfUIsۼ$%&$Q Do_y69%IENDB`PKjWPBE5.png sb``p i@$?5R B xnPOǐ9o.9վ3cӑ Nns4f-V,$сQnz^s6쐘~t 5[lnٛ>xd`>sov[nYiS {IzVɏ<ܞ:IVT)]wċU-3&~G>wkF绞YN3?plۮd]>S+W ,c=yѾ5Z6޿fy\W {ǥW^9'>?ʎ-fxÕ.~ejYW|~fR9r >*r^qa{3|Mn+o|)=g:'?A?S4!]e8$]V{V.2BzB}׎7^53fNٸ\v uvOZ>w<^ uKN-z5^ګW_ĝ99ͳRχ8o©Bx|^^yāW%]5 J`Z?'AY?Ѧw,u<]\9%4PKjWPzqABG5.png sb``p i@$?5R B xnϞ.!s޽\.spm K <2t-^iɮ9u崊 ZV֔)xܙIcMC#>F`%~T[WyV)TʐwZ{ L _ija}if7W VtGg )C5c6|gx$Zj\~cUKg.0sVxye}+M``lmșR2[}zccݣ{x\OrWVKnF^>VTwqwQ~-Ne߹MF{V,%ӯ}Y8'Lw!k˲)2AvOx{%_4ʦ<*a?۲`*&"˭-.z%ܹs}_7s5Tk[~Դ;u[`ݧՏn93al?"v]jXU"=aJh/ |*άۻ6%%N7eSB͞ "n\&pnjg_1b&30vRm溭3C$P;r /7685gSqI Ek9Lޣ_8ww?X!&̠Lnřye=zWF?j9 rbZ *g.Zy-F7e2fqɿK,u}Y ,*7 ?[xF57ڜO}6enrX!iOxy˾W؎Y2XZ=Nj~OXe筞tϑG~&86r/T̜p~]w}Րv0)K)i%'V\! dsC X&2xsJhPKjWP|BG6.png sb``p i@$?5R B xnV=Oǐ9o^N83s}L *ӌ ЪsR< bUq=8,s;ZX-MC\upxOV*=A!Ϟ ^O|mbENM dlģ)Ⱥ n{ekD|v,{KeI I?<DZk ۲|cיKN4l%σkߔe o{3־ ro2q­"{LgT7yɱCuU 5|֡y/=>s6dg{UJׇv|wqmfmF>ns?ݳ_u\үSj:_a%wkt0˵cS 4s4c;5H֏|]YnejJ.׿15r݌ݙ7b:1jowl7uTh4MiխE&}t+9}1_|\3@3nhzǎSz!!MwkCn2)\(4zog.>zs/q%ŋ2köUD<o`׺Ǿ/旬^.1zi%]{>"ټgg=/\(c^b̳U-٤MQ@ob1dE<o<}޵twkk_> /pQԶ1_ëЩ_ p[j"^2{!ڦ߂67ɩpj:=iTy͏^4\ŸĠ1!VN|kǬy6"R_1jpJ~<_U*F`B[q+?eNHh۳M_*v~\W  vSy<} %նX.tf9V^e]|9iB(0`&q?X2xsJhPKjWPyBH5.pngՕ7}gfM-:FKZ!zReJ$ByDd;,X!$d22=ihVR ej:U6E\͸a6WU sJn\lFKYTL׻;/v5"]b2#˭&h$Bq-klPS5F&<16Zjg;7X:L0`Wi5o>)h~p2y$C'bۄTNl~ F_UShAb5SGzxMA>yuN|=g2݇$ Ue ݊fLr "R21^}zx\ '|[A< T[\2pG( #G8/%.L"Ğ ۹љQgsx7ͦkBj񷅊֨#SKpsA%rY2&d׊x/ZŹZ[}bUO9<nEImLo3]95/y+%蕩tP` j8EY䟈'ipha N2Eg: 5o)Ipo.2Ro:2Vդa8ȁy|;tOQn(̩y㔢A m}k5z6،Zųpi|L֤dӛխs0 ڿ8[.GOdsW][{]E G$ʁ*AI kLuSӃ -[ I <`04 ĹOiU/H zE1313N۵|uFl0T J+F֎~Bt+pn$*o*dY2 =dJDx|N#"]ѵx 0@ RC:yԿ/}٠Ѩiѓg:cj 2UI\ 垇|2g'r9@(|*Փ&GE'c2/b&O>6\3I76èP3g\j[V|fEqTm]~?Nφ@^,6FFR@ 6{$M*#6o`HKs+&>o&o|r2ZijKFyyF}snHDu؃ 㗎#e|>^ͬ!Wr0Tz%Kv,:tIb"+ju}+u_i`G?q'hεr&v^۫%F#ELNXܯH,]"k}=Pz6=C$j+)\،:-O)*ˆUjh1ߒwm:0 !{Jj`#4ΩHiҔ`0A0g'NL͇se|=pggDA.7'mZ"]gG7[ nϙTXUL eI-*'kU |,cydHsfmUqJd.z}xg6Q}j&𛋣cyvUJP~,?_vʁNhiw=sr2_ʈ<"sEu1(.…֑2FjG %Pwꩅhz/ kǿUxnۻNd(-&SL&N[2J5qh.r׀_iXmSϳ[Sc߷[ κo]_/:_Djxq|*)W/間s11*KUc^zK{0<# 3k&tV2;nU b*put EJx-;(7\\4,Kcqq͍EnfC~ ]`B` 擢{ @ ӀHnj=py3.ٰMc7孺ڸ:};W'̒}3_E6tULŤmheG6 bAY`zQKS \J6wu33T$N|-q(y'h-uKt촰@-è b@ u>1$pnؾVB4&K +ҷP>3#Bd _+恐Akms=7xrфsNNtnMF ٣B>#z,j<.ymQ45'g gN8(3ddhج!ڞ?\F (Dag{*%,#ohOϗ l>CGt e g^rmkڳݍ̠gjꙤHv#F|і p5h{yScETqj 8 "cL\k0 =bz]8A-tMki,HCx54U XEW =QX܂[Kle)5ا+;;$SV3m&AMb<HO@~D/>0Da{ف5YJi#BR$h3ʘ?$ʃ4EN7;:_u`V|9P}m;ՂBܘ ~ U{Ĕ=$ywMu{\/U#dCi=9xVj+qbQ H2|D ޿P7䒏QbRȫFnPq%' yV}1mgD3 W|B[ߧm4!< D".Ml.v5gE}Bb'ɋjltP֢00ߧ6b, >$q yaZ|7*| #֤|FyEP<+GaMAUwm3UPggqJ9Zb02k"bl$F"i6$dr !53$ia! ~P9D|/Z_PqRbeLQ0]0eܧ `&'eMKۚ؉Oސ, Fwuß+-|(M.; Պ\ks:FL_iskHڋ%+D]Jw9*u%Pױ|U)83ryMе`D2;GžEs ݦ6uX4N<#w c_+/РCb}Jb1}[k:B73bjzohus*-eƘ_UGޫS6 T&$29ROEpR!dX ؒ`³4A[D\=7a%^!ʜB1E&3>0b@zPW "mO 7 d6a5a.Q\N]MܰkG@8C[_0|wɊp$wtyeZXVbu:: sـre#0|uELs"C{\x YlrUh~ϘعF%;Հ)B"Ut^X/?]dG=ȅJI8 AAB ߔUa{vc|q2dzDB_u݇geArc{ En>"m.qѳ':i|EK,}-c1u 1LDI2=R;6فIJJ?S1[*7*QlZ^"ޢ6~?$,|sЂX!S4jg|4{9B va`) |0K;MBB#%fOgow%\mSS:).\ZUfLr:(Pŝktn/eqo79[ƁQ (>+"$})J Y<+=v6GQ,-g+-vډDnSnw弥I]Tʲ;*v}%_1#ΰ!A),CFfBx2#gز?c۵n}{oy 17ӂww^$]-X3f($Jv TO%GOnoO"Dte`4˱doXO;t4+䡈0g8X4x[(ŵ?xn>{)+SeP}%2%Ճ𳴥@|7C"0mgFG|10q ҇a+-JNbZrh8g@onPX4E?_>L}l:B%t8bo (4̌~tLI1i ?U)R)lp?IASh} Mas 5\alá apnBлl#TH{rvǡ`fd Gt BelS坯;^sR<&3 >86[ҳ37ʿ}u:HPZQ i%Or9DVr^0ҫ/$˄)m{R  Mn]ю \( vNl~?wW(A1#^ s1'ϳź!Kv(IBj"&r,.b̤%.s?dąapSW!ڨ *Jn7 DnFd!,,s{/R9ni%ɀ*w5Ƀ!?+L{6`I>gaڽtςt t6un&&3o [-^x\=7"Iz&Yl, IJyVj!D:^D{oS>Ζ^?I *`}mɎJXkkaӛ%ڸǭݮMW)o^&UMBYj`2R w`d̂ųg8:bjbpZ NjIiO'oƍs(Q%=?MGs&x]e|1 P푮ƥ&#ДDR:ox92'sp備y\]_򡦕6>lD17m *IMnDXm8Pe#%~QJ)RэE? ceZj y[SN^% 1â4/,%LjoN{Be Z KY DkyNmz4-ZAUEƁޙx)$?FR|ZX1D%"Ѡ38,}Ѥ8Rdqs0tD:S\"/#VUB䀺JH>zF `RDl|)2eI<)~/`AW]L f"OiZ0v X 6E 46jA͍t BBc0u0Bc/ݐstզQS6y6@N6q~XYnl`x֓|JTfGm}x½akm@cX,>ʚeo'?y֧p*^~uL| 3~0W3+U>AvbS]_5n=АoQÖ*A`B]g<;4Bk!̥ fp3{~ʑgiړrfV+A{nb="o?gJa48 76V\C bEBDJeo/e۳,t bf%n08bcɬVU)k_kV5=}UzJ1&PQS12b̵^'MjdG2BѲLȖd9֮gBW5񯸹Jț%ژԕ~8L$V;zU+ }y%֪ґ @RԑxҡrT85ʱR|i@j~Ca\$)_>l2!l6  7ygdrw#c9<'3Gȕ-2!SrE1GutZ_ f4FGsi&l,A=6vdq|20Cl\_: dth5Ԋnxh#PTXQ"+xľ.VF"pP5ئxɦp nȽnOH_%*՚`+𜾸ŝ犺Qdsѽj x쫭ku# ZⓎ!67 1v^(wǻ(=rߵAŢ=~XHzq֋6R507pt )".>±bB.w{K5%G;quu~3Oրm[q}z`]8rB_5ŋ!tTW;xs^R${d0:G/*>o)Ų>9>ԓvOY| ?NK}ud`{/Hi `AF)Z%A'  i~fmjG~߳L%DLH1iR'\K'vxuf9f².Q!{ojC5-xjRI!SL|Օ'{2E>-IyU*g=ص~|nƆ{Z6.ᅵt{ xՂθHY\T:=:>5pE,Ljϱmk" EY ZfS8̍hxTpHX+D}ﰕw<"#Ÿr>15kOsP~Y Kbyq[]׭xqIJ%xU!+A{ټv9rVӜF5,;o6@`\ѰJuOsRUݗJȨCbS"vmh{ARF'5_27!ҖaVH0JUaaVH*bQ>99U~b9)nB3AoffVz,M7e7\ 0jޭ£-5CSHTzOB! 3ZZ%ɫAǦ_w?Fcet-m016wSSufz8z[ͅɊtnZPa;AFlu8`+귋>>mS3^D;fs n$j)Xcp]1nx<FiU/swy?5 \4c(P\t3L"𷢲%Çл58BB_=0ْ] 0y油$8C1O!j.Ĥt $y|Q9-մVx~`*(lJ@^%X+?Q6CGz*tlV\xe&f4aLoBBq=R-0.#ѴYŨ~?.3Hkš$OrRzW3SP8T%Qr`֊H(35T"W[BƗlE &~a+Y…i#uaO y("|L==C=2YEVoUp&FPKjWP ېH),BlackChromeT.pngzSOvp BKNq@qwwB@ ťw((noλ̜w>fG5xTx๲&F_~1斑|0=eqZ u3w%O翎6Vb4Iᥠfkava^G!#acJbzQ i2ڐlԲrb`+,E9%?!p!-kmdeJY5[ xK_/p"BuXB6[#w#^_@Ύ>2H}uw!dzAUiR@Ģ#C{4䎨wdr=ƻC?%7ۼͥK֬#Z =z|dwy}"@lf{fNr*]vZX=vCyCa2Q ,2(7} "s'.j0;@pQIŸ<8߀|~Fݸ`oW^{:Ɩr9oוVt? rz.MV 'ʀʏfHځa05ceDUM 4шA}2 lmmȞjZ إR"Au^476x.[(c*1} ^ka9 V4K7OAyQ7SN;YT漋O\33@/[ڒKfMh$LDS}u# -什N K:oÄ*Q WϚaD=->^bϴAcuZ?,H m=37޾S Eb~J''qoi9b۪ƕޯᆨ.|/2 %)pћ'uq_\n̹*) \]_<&BFֻ„8h*`:,ec##{yhAGA'knyzU+/B=_if1޼.k= AM!aEpx}ըqY?rt8>W?qYIQ]zx툅mqz \\>zYQa|_d&.ᨇM$&W2(;;ܪo|KRHNYgabsۋo_>\zZ7 yI km4wTH6F` oQ4kʣziu_>(Ӥ@*|:挡Qx[0wKeǶeU5u!ߥEU  k);#4SB1kGr'ϞMX!cWCVVf];<w-8TƒԍԢb|$57[oQ~ K7pv:J'봳G]NjoTT κL|Lޔ\.1̟^|9M ^VоPKص~=ȆvrV>?2sSp-~K?dn,%$JgEz8%ʌj 3UtK>k.çрG_%BJcǵI[P.;ぽ,c=]HS(!Cf0jL+:JгjUi@]qȌWP )C< |[6?K O d`|W -ϤX,7s;p|_\%KI }v| "۸ɧdF/)l]E#I1oAϻ{T%a+&z2(G-.lW̵yO`)il/Gy9PO+.0BN4pcN_窠Xq-(rrh^`GG>'n,@_"GM̌ԥq҈4r_s',Vu;_3Hjj2Ə#R?HJ_+Ȓ5$fZqb1lM%hzk 9 ({!H.}u6o`aK ZY``Tqfgbuډs4//ӞaҰ~cN!;!DuԎv6TiߜX0 1o2f };eg5Ѱ['Jt%?B\B-ZO rGVjRF~) {U9ywLtTahAD3o5~8E,n徬BLNSA(ص&Omڟ\&omψo1\>[P8%1=#qG,a*ۮ Sƈ3㿓B3  $՞oYX:']W5n|F-ߘ5L 2"]̭R o:eTSk ɴԳou /I]$ZJ´1YckEެb,#q"fhFM4EE̋>Vc(Ն\ՍBҍs $ [@[zSB訪 K 1;gd,,*;eG TbWȐIA& V4RKR`ۻ>/\vF( zLA)ocp V=>Us55!)ąJv"TJ·Gd[=rqC&[vKE~<0ܽq}y5ཆVy#q>. ZY.YdzOT;T2*CJSpٙ{)C5&}'4͇+X'< `Z{E9r|V2 vܩu }w:y\(WC^l 1y)lv&Bj4ӾGNsg²Y\DŽCyw66>ɩCx6k+"\bYc#t0t9Yh^ DABl-Ī8FHK’0x5PBDΘ%^ׯ*Hdⶆi+kjiڦn* HD9 |7]Y (.]8Vr骱th6E ڴz%fT} Ztms&()Wk(g#ݝG(0 4tqWvj$ ZҤV6gvHW:/LVTH0wV97u"kHo$—7{sj'<ԛ4^m r!@vA~( @8IR%g 14EVͺ3zk[RʥYem|'3kћjG%Lh-p`d)fCJlgrٞMͮ_}S(-!<,3{8CVOiRm%0̈́-bjDW7i]#.4*fos;v=& '>6/)J+ath ?ge[=# ه P pîp1l>)MٍYjVP"he$Bmau.ZJJ gmL:υXVq!^[w2&2OȿzT OΗ'?gD< hv[ݕ;ً RG7M}qO+icO}i;,8G:a]!M^]~>v;}u+9*]Yfr]ozX-?[P+Ib~tsy+Y <}iV%u4q=d*#.K~])~KK~fznn1\#Aox_M RK3y7;$UpG Ug$Kf02,&\KUZֿR_GI}dwU~b.9gIf#փeLP\HXר Pcoѐ>0hې;ic9et*e! ;qė|j|yIiKqFhHIXPAO/^Ӑ \.}lSbaS=^xp\|j4'z|3~rS'4:T>噆1zs+7E"(c Z8ER-Uĥߤix "ݍ|CuXGa{8w}slH-);;Yi/co[5Y![3 öKA @e<q׾BOE}}J2X {#{29{ f-:Sk҉ 3~_BnKOl6=v,O BׂYUz[W,em]^6QM_-RvV;uA eYp\Ў$cj_3?)o@`.Yio.7XUׂ`^ZjxM`㕞 W"!)"3X7oʅ-7ˆ"XB I5A^r-N/e{tgn|mU:deC]U)97fRya2# 8ꪎR!Wn  \X[hQ=4tC!^kKS1ɷ.]=qՆ} ̿n&w!#XGAfh .e@ ڭ .7ϤZ9,"baoC5]GMZ g~.eJO|YYB ڍe<Mog*C?w,GCVBX%ӪZ>?\'lgxx3ɘ$~W{ǥ텻gwh&S3e+{."5$"qwp1{ި4gc]x)_V|Zz&yԓ ბ"!Sd&4Ѭlт#@fJӥ>7T^AI5g~d˶0LӘtd&œ}F6Ƹwm nt-y-J7l$LŕϳC0ပWU *WxNu vb> \(fȼ-$;em`Z +ws|^ףxȓm.*M# Ř>=r?aM@\AEl%H 눨4U e-\f5@j}ѩ|pIҼJVʻpy{c\YD8LXBy Śv'"~ϨQ]Zc=ɜ(TWfXnSZMCFsKkɥwPG-]`&1YȔ6GY7q=[Cߞ%%M9Hi<{=ۥw-TatYNެy @2>g~!V#=arv$dn`<A.&bW<ӫAyi2?ZF3zn g y D61-.$\< =H=E)/2לٶePC@,]fAJXM͢fҔ1a&g"6zMr6 ~o@wyԟ۰D5__mjqpFt)Ϛj7Md{-l#=ΝCB<˫. lŭU+QF*G`ckF̀LT 0/T~з>b^}kpu4mcc#nTQKp;~QxxFl;{oѿ;63 >Bկz]|ɑFw#d!]ccG(!Wθ8!0AM~aILhFvN:6N84N:4*uw*ZxyYĽBٍ&$i' ;5@eNLtNh"ŭXe>KsSo[ŮD A3~w(:x TΞ =僲B_r0cN5=~15NdQE"^~+>HN;J̬u:ژ8*IAHDHݮKVN}`,a*'3'Qo¸*Xmc>|f~!E0L2K6]y~j, tp;ǃ(_Pa!KaԖqc.rH'3ʥKV<[ DI|j o}(:~.6D4_l|[ewt :y)q s ܈wC:!E3;/x~וsl]Ps}%w\m  NB|CGj 7H#x E3ݎSqHm:?rl:*U/>@MhP{ b >KoMΐ9;OmyPJmACSO EDA8D㘯¼ ><>*253l.O)|10i%e\`2*hl RűxUT, Β?1C0:%&LԀ8q$=uh8N;|I.Ջ7oMAOߥ,%iF@:HK+p뱱X$4mcթ"{DtO}PZ M;*Jz`Oa尲"6|Q2Ϫ_>o"E)i?E%pbUD/P;0q ) ,zӝ+i/]`Q{ V [=8P[\> fMPe^aߵEJ]s`*)Nt Rm<YM~a7U߉G?據 s&BTZGIX 7P?+LShz9?9F M_:fVY Qҍ'O:)UDT+I0Z=cz /r Q+-K!+pK],Bmyg ϳ4jFVzT*rX׏ǎyЁkZC.Z IpP٫.Jc< 1@24&F)̢g}Ky=~Mqzvaa#cAeTOz _Ma G (O |~ڣ'w>mmcMu+P?wxTJA@ǦIe" 4a5]\X#FH608(}HDRXR;P 3pZY}q~uwbv =q ҧ!+,!MI16F޺.m5t4~l+2o+˻=#8imq~U_^Yc eÐg Jol̹~~w+6~~64Sr8 x)) o>rqXNh)2 (zk}(5?`  L h"B+@p¹{{{nw777vW12?L0(`QТ , .3P0#|ufp0",..z_N0k(s EP@fb0# J9<0Bs/_67Iֆ6t%j&P?lYLpҥ76x4 sm(1uT JWh!|, K@ɓ'8se-JmI&`UTf)PuC Dt>`6eWŋJ 2?Jt`$EDCj"j\DаP?ys^<P ,--%J0q\cN0Qnh"r;[99'9@baւ1 5,CZр,j){@A(_^>1`4S)h|Fp8G ȏ?1< -!M,5 )_<%:= {nx]l҂p6 r߾}vm`U,߿uwRك jՀh"&[n.S*Zt1M O ܹݻw[?xh y + |!# 7oK}{t"r@aFWH!͏7 >|^voF`!ch4c蚪P[HS[~^vi`"Jy b,+)a0``nnnfkk˵:33Q1 2cH3 WҬj6CȲ!~ݵvvkkSN}EQEiFӯ5+:>k-,CH`<2s>t:q"Ba~:c#2F# lCS]IENDB`PKjWP43 jGG3.png sb``p i $-?IR B xn.!s~\#qA}u=3B]Vy'w(t8:.=Z17J BE4vI7\v䰌{o3?j1k]7}2Zum X02lO)= { p޸ukUhH*Ѓ _z۷Wޖ)dNJ9<?P\lEoU5u={U"Ӿ{n GK9{EGK^N3ٳ={=ucĕUo|x3g1b%j~oعsΘrki){Ǐ<-~mƓOYX?8puĺ>/9-ꪪ< l o޼yrfdjPѤ.,.Qi #:խ1HRMs :Έ2}{|{߿M~uM ^Ύǎg%|' Zxg6H1_朗@W?޼z.q !Z/7{/ ^}`L@ ' %j\qR6iu7e`{^֕>>W{`nfVQYyZ쩑bO,DN֝2lؾ}:0n}]}^l;(I5K?\1{8AoꟜg.;k$ò 7^l?(=#}COAnnٳϤݹy-:3O=50 [۳}}}$l2>2=TmqQ3{v)ZSAi$x唰ԓ)q[ ̕==U0 ϑٿmٷ_ SRZL@r &LP{=s}>}0zgic j~ǣ@ַv-׫ ט3gV)(x8?kfaZ%a&yNI"&Xr.&\?&%/쪴A#JfIPƕ[3׍>Z\]DTAQqraW PS 6~/|YRv1nk/]J%)Xq2èAo]51y[SP e6Bgz!-Yz+fw݀k^bJʥ7-{p F1W@gVa 7 JZ^D9K~FW*h s\Vpg@)sv-87U},IΔhhM|ӑ@۞g LG&vךGWwoyđrY ]QNQPvN_׿ɮFЍobz uu)"─wѻ5/uב6Du-Q$?WXLLLW?NV56zxxdCaQ==}{h4k;ˆ =03J&"gOBz{'#V/A}Kˋãf:df+ccVyw8tE 7`Nu %WQ+܊ 8J+jxh nPА;SNN%ʤNQNH:ܼDz&tV+)BpÂ;?wLS3jbb+?az1as8!dDO6S&e⺫f <7!O\ж{>zc 3$g4cZhZ&Ȧ fitBsԵe淚 ]+A@F׌Sඒ,69+@|q-:Blm5$6$s ȍ 3fzwXFe ?MK n ,~m0pDF/ox'٢ |T(=-Z&"T%l6$SItAG&6w]̿ώJ:;k' | rz; d~KçufoU=6Jii rFO^Z1fg{P;;I@;'a&`|}[p#@S0Opwxii):ٳ܆7ZT T!@-:EL`~SC}-*88?Ւˡ&Sx1%VzA,(ۗ0W+7ߜ8rr!HW~] / .-} e:6S8|Y4~dcVd1C_!n**5ةb@KC1JAKX([PT)"288x t܋VQ+jh*qD:u=E6`lg%KA}3 "@Ju.bs- *ͯZw/H.ٞ;אHᢷ'=OX$(ꘃPYF iI?YՀ  R:Z '{bMK|||U$9̬309요LffnqNW;-CiER@ v& g e|Y˗Jt[lDZ/] dϻ8]]srV_7~ؑpMŨRXA EIc );uSabx_76x'kcu.pJ?neu?6VOuIV+\A IjcŸB[W-? &-WiۊnbĿ+.֛!帙UGYI-D \󇄗ّ}\pNGI,8RqCc#z?Q;3w=]g"ZB(-tdB:~՞ hsnx/HFmN+ƨ6eAe'nX7R{R:.tY 4=۹ %5HE8~cgt҄.:Ega+]w 18d&錋S]n Zg_e":/C(Lw^ =i+1II-3Dwv]dz+O8(w7WM"zp`M,\@tajPrwm V#քJ3UJS:{c ?Hf%WK __ΒϚd{M8jn?~adاrz2cD M:E яxT";pԅP;4{G^B)xч<8,/9&X)(!Y!`Hxr ;h[jh=XE˺AB BZ;9(AC}!L:{zƔѓ P\4s)DND /<`n#L[@1_a_`CbTdb6eQ-fu!oVC |Pem:86؃T|ئ)s:>R?2t,s9jŗEw&JƍV7^ @@\@K֐~(z,DyRY;Ty^Yyբh ?H4nL`r8/;'M;9G<(KP21~e}ʱG8%;T4Y~|fFٙ/v09$ +/o|&xv..xY'$$^#1;;;.KSRX֑URPa;ۥBhK}},IOVTܻRvppPΎ0ULJ )Qs .$֯R+bkJy@`nЦ%S4^|/v3K+l}}(DL"GU:0bvţ)Fx\0$h#-F-@0;SHpL8Dj/(lu Zai/8=Zr{R|%p7.tu?9ש(ze z**) >h?6M}~d9L'9VvIϨ5WTT?BNwHEA񶵕ۻfaM*zק˪TSl=#X&Ԫ_?QZգ+GڋSdNCɐl!I P}=>>+E;mKII'7tv3wƂT=+kk055b3Cg'7~"YL`Cr)v@ZH\-ä$s1 #VV;8PIxȣQ"ǔkvfȘ=#.cWr{A,;d; yyy0 %=UBM0?Ӝ?A۲'C]v"4|=t8+|ZqȾ~f^Ϸ(:Qb$6+"g.f;Gcqш"e˅K-+&$1-휐P'=Q"딈Ӣsf{V;}3mYΔxZOį9c.0q7@華gAҶ &j,l 7|:zehi00Zv111)9}{w^VOⅅfjt+^FO^'З5V{$w*v+_4J3]:?Q{?=5uO vc, ?J.:t &?S0re/'UW afS`vU%G\+j|2"\A(#_q/JH/\`'/lM_(MEf}}}ҊYj#T}V7q6gV]$xT?cv* ͓vx~7q)0e%@@5:&BvzB:M~3j4bJ'h(+zOCB(xm2%%%E0Z::,P*m/|Ol!ק9t祪fjFݍIRUok]!U (;;[~ $OlM!znYU+6xE<[L` e4ONw\xYίKOog&t\wFh~UEE.σBKJzx@Rʦ ,z+I"9֠3MR=hO_VĜŗ$%_7Z+?6B4 }]mg#0& >P j$fўz>+oV:OAڨΡҗsvjCRJ5PDڻa A;nege6y}AΌA3,Ny#TT"`^-4'8I}$%_d9?b(yN=Tr-V24o[O!ؗ/ o[ߤ88cZ ޗ}SZPQyqꙃ1=$ 1aNN聏 3$>"pKmqæn#.3V uI_R4ڄ_o~l6a״(X)=;,kP=w*6,ve#PbR~eqAG mX37BR~P&TRrG' I%_ʺ7gw`Kv["B~:Z.fLYʽ Jҏ{,m_Nbjjti\cxַ0g[h; 3A#=|}n4=ݩ$5v sU!WKm_?ziq!~r܋IpO5z=z 8%O@ Mesm&x QF>>y)h NZ+ٸ㱣%V}g(~~l&@*1kBb-.`{UNCA01tSb}G?%7ޡb1U*ޮ%n3 V'@Ћ&wԿa @}wԿay=KG"Vٿ_-ޢӔ7ma#M( jK|"OKoÞF\-k}Ɔ⧱KdSW>20N ^|)1^ǽ`KX?<뤙5AcDQրLHA&_6An 98]$ G 0ܕOj H'5^ ff/5 ,.\Ko5"]V͠ M!_lUMF}_lK场,AMR.Q/y;ѥwL,?x_k %vaC|Gܖ0My1#rp")Zf;}HGnZd!,l>N_ib9aYd1YE+#&J׉}Ibhw؉zDHԓZVh,G\TfAcGGץ%1h{w U^.,R( I9Dz=ubE*6Dn&]۵';0mY'']]as++oN$Y^pSv[ 7652,@ް:x.2e8]*M&EE訩k::䤽߂qT65<˕tHIK堍?~6~!#RDC[Sw#m #tBh{`UQ]) l=6u<CYb_G1}Yr(HlgK;d_q{?K&R4RQTst~KF,Gܓ\ڸtBZ\$36حy_:;WfeQF1eɬ^!)CD[[O}1YsQDT ϤC72h.}a[0@70SA8p4}q1@=njDg||ĻlCuׇ76඀FЖӟi[Osr^QY1 l+eh\:C/)gooF1F9s: kTw#ऐ]8o{=܀l)[埣aI;*Ӻ'LNYjh74ln]i7o}!b DG&wnFnLڶ;\oC4~4A{U[跉'>`iu-T Sȋm/.S >+]XJ>~c /5[7r˔" WvUX<9b]}h1]#.2~fv|YAyw([1Mţof= •8e>ʗ7wo_$%(;NU=S|v#&ޚ$/-)qjk:R €/ޢia}PH ~(ĥpIA{\T F[u/>ƒh9zOà^t<iE0:ڥYUL# /gǩU՛Ue[JP= h;8LiA{f\5D0o*7UvW^f%cC;hE>(zZ.PVmx e &ջ#8՛nWQ5p2;_}iy<^3^HBٹ% @ xj< OE/(x7 ^o=#6.:> R&fRƏ:+ + Bϲϰ(M\U@w=9ɓ1( x}Qʨ'l/ pCğYa.[UU-e0`f#;5I}‘wn^ƨDzNӞ@ EO;~T9bz֪& ^DF`NR' +]fȓU"Y msm=?SLC__E2A** u/S5.Yg̳K@Ip3gs_4:=F6V T第޻wT %N1HDZ!kPQX'5BYOACqΏ3WN - $q.4p-__VۏS111Ttӕm/ÇU2q='Ga" ta&o _ߧU#O1 (..~U0A@3 βljs!8a~iimDgEfnz p6nnbDPvi)F`LFgMH믖UεW*BZ~-FHUU,U)$s670#esnU ѷ.ёl3 bw9WyDM/=&bYkc=‡fJu|7NzC.+R/mLTqŒZAZUP@Pԇ@"a^[#\ʠیK+9vhE6 ~-<:YY[S71ai:ٳ]~5[8,#($B|ݻDy#oޜ{LA˴O+|\S!M^CU&<]̺XI(wyi6thf&_"D]شsm#???=T@DĪw Y{Y~~>:6QOdj75C*_<wXUvI^j`a[ZZ^$r P 9@/(,sm?b!9[;̽~L Y\Zr46iл&%9DEUY[iͳS}/#K1~1t,@F* t/ *XHd6̿}}]EK51-z4GZh;[QE*6!܏;l"Y-P$G±<{WՋY4w!B{|b.z_Q[SOܜ5syߊ:VB&;~ =f#ݢ>H3kNkB" EG4d;'b VWd9ggg(+sm7) 3Gh.6v'(hfxΒ1bXC4dءg*Fl:wS>?]%7bo, ZyYsq)%퓁t`ۻP@щ25}m- 3!r?9Pq)2(.K_v~mo۝xH* 8y!%-V^?~MC *+qqavl&Ls6ثcͽͩ״ ̑$،ﶒudѶ$auu_;:ҌCz~ ..% vvw{zJvCw~s46fDy<ǭt/R(!N]qCCD&|~#sҁR$tLfflPW pL9~j322T">(J3sVfeC񣩩y^:ηmmyہ WD).(X RA}Ѷ%|bno$&2Jj9JO՝4zp̛W QrҀAui @F!I ܯD<|DlD2+Wr]/ DY 䧤N ߨURR*36s=ci!ӧO 2agDi2 95C#е5:K_hYkQ$7o}ooe*,lVA>Z d+oTKJn99uu;B"$B/s-~|=C".3SmxVǜ{,3u?~ʕp57^Fá~i2&oD,22b66 ,EY@S ԲODOhtSUωe}3ҟ]cn 75q`25J3rrbR-Ugkja< |=vGW5#n̍?iA#om MMe]caa{3c@l{W9!--U??F]4g ku5F5͕ 'WT\C d,b3(١os.O|GFG \l0?G<עKΖ˘ 9B%?}Gg?3gBѧ''rga1F~l=yJ(pz5u9.bRЇaH{4?=~yFLwƾwob@M![JT A舩L!jth^% m d4Sg,V'~Y\).#B@s3,E y{uUR8-AmGTOIٳԻm]GEWW˷/U6qؠc8@_^D$+Jr00a6lZ;|'[\K>srk}kDt}e g֟$$%]ui)jhR_5mrLѮnKӓ^yF|M3h=7'o>{'Y KUVWW V 4Tww7h̜SSU 7SKJb˛R1(7222}\V'QmTu}2;skQ_c/kϬu8^Vf΁E$ܐ|X.T\\y@XڠiE(eh!2U6l366F"נ#_j+/.ٻjⓐTmzRI͛9wU4獌NO# * `w'_Px1)9&'')($ @Hgڇř:~'z C[|*y)XD[Dbw73x(,*zcE3Lu}T@QSaS hU+mҖ6߾AgKځ9geq;m4[[;oU+oOMԢljp]`?/KOw"/oQ= I2RR MMʅ'G@>jrX篭ҁx||vZA}ʌ~Ne3iꚛ;n[4 +u3b#K/2MI跗Pan/^êf;o:n(Kħ6 `9Oni0h?KT\^^^QZZ0uO8/F*~~466fغWm剕ޒW}|[d@V]Pmi3cx}S;G-SI}EC3$QP#+>cqkuA/٢fyuNp{]0|{6n?ev7^ N߾Zl MiLw:} uz*}E[ZBKѕkqҩ+?3&P(W܉/Zښھtd/kGuJNلdw #.w(Budl$H=J߮AJ>^; ߻04ºBpϟY-v8*fy^vG_+qCH^鋽/z>ÇLEz|ۃu뷻I©i05~q`0c3ښzAPKjWP"ś)GreyItemInset.pngPNG  IHDRsBIT|d pHYs  ~tEXtSoftwareAdobe FireworksONtEXtCreation Time11/26/07uUYIDAThk+Uǿ3s_nڽ _EAJt֊ EݸBgQ7Pu&[Hm(5i3 sI}L2s29$fc Qu ! ȆXkq䛽/O 9Hqg`! CaS|cR/..^f̌4Hg pBb&ш`izCBHG/{mOnxi׽!O9cT*j|`WW>YIw-U9Z)~g D@mIz/@NQ~?hSZgjM~@֑ayyӄlkЯQkeIO Fq$ILz0ttvmWJeԤBbBOEMDR6gKKKE\.HRg^]{iVZfRf@)uc)S˳P[Պkm[kJV=\__:<<9IENDB`PKjWPPAhorizontal_drag.png sb``p @$;8 <"bB xn`cHŜ6r:($~WG愨sb$_JϬ?N5Sq5v>֦53sZ2lWԔxv fw\f(i>0xsJhPKjWPm˾horizontal_splitter.png sb``p 3 H6R@1!ftP01b+y8X?W1AwƍÑ ښRaou}Ye۟c⒠`5Y/N?fwN~py+rMgtsYPKjWPT~Iinset_grey.png:PNG  IHDRľI pHYs  ~wIDATxKL[qJvegQJ݌4MΦ>R$H"T(JB"71mNlbm0 ZfffT9'{`cc2}yl6Z8\.FB\~fZZZB!]KK >x.)/hFqq1l6pzܸq`0((@("J$!N4%߿zJ:,qAΞ=fǃף BB읊5ňtvvH$H$tvv2662S`M8uV:^1Z{蠣CnzzzUU@>G4?btvUSho޼Iww7$IRTx=*^@cc#qjjjp\|-b1` BUJH 111}>}CaX\?3 ֭[d2޾} 8|0%%%TWWrr8x<. zzzH&;88l U_WBn8u!U0L1&''yJ[=ޢ"f3NsD"?ńӳ묛3oM cǨGKK 9H+>Y7M@bPUUjIb%uGGGY޽{w}aHٌ墩i!Wӹ8SSSܽ{Op鶴?,,,p B-Kf,ؾ5}|fggY[[SxojtDQ}l<:pƛ1Tܾ}\.<>4.2[.\'oHEG~ۖb:}q(nBЖ-"u6N !>!FFF̸zϞ=0)?Abtuum88<<,(P&!2>>w}}>4.aNlnnG$Km&B(duqj~~c-ZΪGr9}oVB@EcVWW;tGRz<`lll˦$SSSBLOO癙annϟ{9uFC_eccXZZbqqQGKKK,//fQϏ 2d#wI}.l3Boo2j`K IENDB`PKjWP3|{JJ07.png sb``p I $W/eR B xnOǐ9o/ 2(P~(*Ľ^@ROҡxxas}p`tsYPKjWPKIJ08.png sb``p ) $ M R B xnoOǐ9o/*600ފ a9Yަ6-އ7n2SRNA!e7 CZCVQG]F``tsYPKjWPfEoTJ0A.png sb``p ) $ M R B xnF6Oǐ9o/60=~c*K`?//n~[7ܶE x\v~1=Z"A0<]\9%4PKjWPԲTOLB.pngOPNG  IHDR pHYs  ~IDATx!@ H0p1 ]q$($B _bZ@mA?Ym?9At.DF7P3d-Yw g'Zk)616 $ѡAH#POGaQ@ B" AH}2p\ b"7SPATԴXq$}h1#A:CYځ"_] g|j92r.&IENDB`PKjWP5 TOLG.pngOPNG  IHDR pHYs  ~IDATx!@ H0p?Ի IP0 $}MѝN*v[j–;|ØS8DGLa>8 k AdĘZ 0cRur2prBachS( B" AH[4p,! $ѵPAH# +GE &ɱ{>DE8h(LH" o'c8P|3% U}wC[x8IENDB`PKjWPldWRLT.pngRPNG  IHDR pHYs  ~IDATxۡ@} ë`PcIG:IP0{;!#>TZKw5:ۡ-1q1E|p ""ZȈ1""b#1l\k-E &Ѧ8D8:7G"iYD< B"=AHk7p/"V!V“ALS&}*1pЈ+.D4"OD "#p1?Hg(K;W%W߉XmuشűIENDB`PKjWPQר'"notebook_close.png"PNG  IHDR֕ pHYsodIDATxNQY.],]|,\`03Ms`, Qjimtcv,LM0q\_uo=gù///\G|jKP8 iB-gL>pL @7؞0 U!8)\;G%"5VvtlqlRt'-HٕUr]`#b]G88,)W,+o5_a N@oq9k.$tGĤjLve"Llf)HӍ*;U%nVO7Jyp grf̶c"G^JeQ*VF*pL)$X8.X G*/Hޚd̶c"rl)`zS$'c cH{C ȰW;_RՖ*SyF2@ڶ+ry?ț"L7XS?'N /9t<^]57~_  Ѡ䛷$}@ Yƒ)w) ǡzsz88Kƒ)TISu7 c SӟqTs#,??b]tEXtAuthorUlead Systems, Inc.>vIENDB`PKjWPT"notebook_open.png sb``p s@ Hv8 8 <"0) z.!sI8^/gEg>KS2rMm{xy-˛,I ~Kgfh|~-)-X"5cFПoo7oϱ5o2P͚vph0gПv@Ӎ_xy^kKj7:y9;+*vEBijBQ>7vHs_L:?y:!?k28@w̓(f o}hݡvkj7%?MN#ݷMv$| 6~nMDN>kuӛ TRҗ Սo>Ldq2m3"NxʘVL>vFю?Ϸ%X;׳p8xHx|Bo۟ؕ{{Z[_p꧌SYSl~*S\}gE|Uzy+n=hHw-d!(/]YcQa2W&-,fi*vgҿ}?ꖽ&3V~UwbB|*-m L_eF=ח̦>XS{0{͕'%s?{[mi-"gY?o[~ș۟ڦ5U^l ־xq#Jkv{/}VE,'S_zI9B1uM̳o|^=3놄}k ֘m 2Sn 7y_jk ߭xC?S׈Ғ"Мbϼdve@U ~.PKjWPqgLtab_active.png sb``p  @,$R B xn`cHŜ y829qAK9\nž5.jҳ03|ggX^%$7j>|ޝIg0)3T05K?;밡wQ/% ~.PKjWP5_tab_background.png sb``p m@,$R B xn`cHŜ 68z?Å&~Tv׿  1?ja?!,'nW0t ;ןy/FܥJ[].) PKjWP`Pw tab_hover.png sb``p  @,$R B xn`cHŜ668?zqAPE_c ,wk$S~ c3\u֜ozmq`΁f\7; g^:p<[  G>2t;0ܮ*`rblm0vIK]LB%ʐxI LVHL 1k`cQRkWӜCY G"Nn4Z!5+$m??_ *0sFhKzVdς88ßJiCháC>]8!m?9uH)/PO#iEߵ7=6KH)޶<  ;{R 60!NO0PC`tƃme_0F!N+<~]x>Ԉ j ٩z0aIO]I-}mDk]" { sUQFhoAҢ9qe8&[}+ zHaWHIt_Ie]͜6PKjWP/s=A* image_info.pyn0{"wtR:?)iJ]/&U2 !FĞ~NZLC\D !W6]pv6gqL'tZ- <3jyj9vi~K+^5{m,t#(KA<ĂČv@Rqrж"3e{7:=$4^ ŗ_MuOoRAR~}N$3pBt&]Sz4z4z.P-JAF}i%Yf[M%T4JJLEuzyf[$n^~؄{f\'7KX%,!Lω; !+[2bfDƆLCS> ]]QmnFc)Xfz,G3@5d߄(k"z jDr1G«3584@r>Y]LMqQAtlo<*j`7FYO@ֻkzSLϫ%g!].i  Nϛ _td6P*ypBrmAqP#%8Eǐ9Cj:nlPjm5[_u¥w˅0dͲ]eV)dj&,#1z,_`M^ Ҟ퓲 I+%^TDD{Ɨ"<)vȐ|TxQ֋f[Sp( xk.0Du-HsbζitSyY;%{gHo!iZB5*lED?PVoNUDP ]տPKjWP}< license.txtE=n0 wZ.IA-&&*KHK$ǧ-ri ~]oMpᧆ ApG.b9ҹs43ta+Ծ-O b/h=:v$zbE %2-FA0 ᔃ6܂ (self.time_stamp + 2.0): if self._zf is not None: self._zf.close() self._zf = None self._running = None self.access.release() break self.access.release() # ------------------------------------------------------------------------------- # 'ImageInfo' class: # ------------------------------------------------------------------------------- class ImageInfo(HasPrivateTraits): """ Defines a class that contains information about a specific Traits UI image. """ #: The volume this image belongs to: volume = Instance("ImageVolume") #: The user friendly name of the image: name = Str() #: The full image name (e.g. '@standard:floppy'): image_name = Str() #: A description of the image: description = Str() #: The category that the image belongs to: category = Str("General") #: A list of keywords used to describe/categorize the image: keywords = List(Str) #: The image width (in pixels): width = Int() #: The image height (in pixels): height = Int() #: 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 the label: alignment = Alignment #: The copyright that applies to this image: copyright = Property #: The license that applies to this image: license = Property #: A read-only string containing the Python code needed to construct this #: ImageInfo object: image_info_code = Property # -- Default Value Implementations ------------------------------------------ def _name_default(self): return split_image_name(self.image_name)[1] def _width_default(self): if self.volume is None: return 0 image = self.volume.image_resource(self.image_name) if image is None: self.height = 0 return 0 width, self.height = image.image_size(image.create_image()) return width def _height_default(self): if self.volume is None: return 0 image = self.volume.image_resource(self.image_name) if image is None: self.width = 0 return 0 self.width, height = image.image_size(image.create_image()) return height # -- Property Implementations ----------------------------------------------- def _get_image_info_code(self): data = dict( (name, repr(value)) for name, value in self.trait_get( "name", "image_name", "description", "category", "keywords", "alignment", ).items() ) data.update(self.trait_get("width", "height")) sides = ["left", "right", "top", "bottom"] data.update(("b" + name, getattr(self.border, name)) for name in sides) data.update( ("c" + name, getattr(self.content, name)) for name in sides ) data.update(("l" + name, getattr(self.label, name)) for name in sides) return ImageInfoTemplate % data def _get_copyright(self): return self._volume_info("copyright") def _get_license(self): return self._volume_info("license") # -- Private Methods -------------------------------------------------------- def _volume_info(self, name): """ Returns the VolumeInfo object that applies to this image. """ info = self.volume.volume_info(self.image_name) if info is not None: return getattr(info, name, "Unknown") return "Unknown" # ------------------------------------------------------------------------------- # 'ImageVolumeInfo' class: # ------------------------------------------------------------------------------- class ImageVolumeInfo(HasPrivateTraits): #: A general description of the images: description = Str("No volume description specified.") #: The copyright that applies to the images: copyright = Str("No copyright information specified.") #: The license that applies to the images: license = Str("No license information specified.") #: The list of image names within the volume the information applies to. #: Note that an empty list means that the information applies to all images #: in the volume: image_names = List(Str) #: A read-only string containing the Python code needed to construct this #: ImageVolumeInfo object: image_volume_info_code = Property #: A read-only string containing the text describing the volume info: image_volume_info_text = Property # -- Property Implementations ----------------------------------------------- @cached_property def _get_image_volume_info_code(self): data = dict( (name, repr(getattr(self, name))) for name in ["description", "copyright", "license", "image_names"] ) return ImageVolumeInfoCodeTemplate % data @cached_property def _get_image_volume_info_text(self): description = self.description.replace("\n", "\n ") license = self.license.replace("\n", "\n ").strip() image_names = self.image_names image_names.sort() if len(image_names) == 0: image_names = ["All"] images = "\n".join([" - " + image_name for image_name in image_names]) return ImageVolumeInfoTextTemplate % ( description, self.copyright, license, images, ) # -- Public Methods --------------------------------------------------------- def clone(self): """ Returns a copy of the ImageVolumeInfo object. """ return self.clone(["description", "copyright", "license"]) # ------------------------------------------------------------------------------- # 'ImageVolume' class: # ------------------------------------------------------------------------------- class ImageVolume(HasPrivateTraits): #: The canonical name of this volume: name = Str() #: The list of volume descriptors that apply to this volume: info = List(ImageVolumeInfo) #: The category that the volume belongs to: category = Str("General") #: A list of keywords used to describe the volume: keywords = List(Str) #: The list of aliases for this volume: aliases = List(Str) #: The path of the file that defined this volume: path = File() #: Is the path a zip file? is_zip_file = Bool(True) #: The FastZipFile object used to access the underlying zip file: zip_file = Instance(FastZipFile) #: The list of images available in the volume: images = List(ImageInfo) #: A dictionary mapping image names to ImageInfo objects: catalog = Property(observe="images") #: The time stamp of when the image library was last modified: time_stamp = Str() #: A read-only string containing the Python code needed to construct this #: ImageVolume object: image_volume_code = Property #: A read-only string containing the Python code needed to construct the #: 'images' list for this ImageVolume object: images_code = Property #: A read-only string containing the text describing the contents of the #: volume (description, copyright, license information, and the images they #: apply to): license_text = Property # -- Public Methods --------------------------------------------------------- def update(self): """ Updates the contents of the image volume from the underlying image store, and saves the results. """ # Unlink all our current images: for image in self.images: image.volume = None # Make sure the images are up to date by deleting any current value: self.reset_traits(["images"]) # Save the new image volume information: self.save() def save(self): """ Saves the contents of the image volume using the current contents of the **ImageVolume**. """ path = self.path if not self.is_zip_file: # Make sure the directory is writable: if not access(path, R_OK | W_OK | X_OK): return False # Make sure the directory and zip file are writable: elif (not access(dirname(path), R_OK | W_OK | X_OK)) or ( exists(path) and (not access(path, W_OK)) ): return False # Pre-compute the images code, because it can require a long time # to load all of the images so that we can determine their size, and we # don't want that time to interfere with the time stamp of the image # volume: images_code = self.images_code if not self.is_zip_file: # We need to time stamp when this volume info was generated, but # it needs to be the same or newer then the time stamp of the file # it is in. So we use the current time plus a 'fudge factor' to # allow for some slop in when the OS actually time stamps the file: self.time_stamp = time_stamp_for(time.time() + 5.0) # Write the volume manifest source code to a file: write_file(join(path, "image_volume.py"), self.image_volume_code) # Write the image info source code to a file: write_file(join(path, "image_info.py"), images_code) # Write a separate license file for human consumption: write_file(join(path, "license.txt"), self.license_text) return True # Create a temporary name for the new .zip file: file_name = path + ".###" # Create the new zip file: new_zf = ZipFile(file_name, "w", ZIP_DEFLATED) try: # Get the current zip file: cur_zf = self.zip_file # Copy all of the image files from the current zip file to the new # zip file: for name in cur_zf.namelist(): if name not in dont_copy_list: new_zf.writestr(name, cur_zf.read(name)) # Temporarily close the current zip file while we replace it with # the new version: cur_zf.close() # We need to time stamp when this volume info was generated, but # it needs to be the same or newer then the time stamp of the file # it is in. So we use the current time plus a 'fudge factor' to # allow for some slop in when the OS actually time stamps the file: self.time_stamp = time_stamp_for(time.time() + 10.0) # Write the volume manifest source code to the zip file: new_zf.writestr("image_volume.py", self.image_volume_code) # Write the image info source code to the zip file: new_zf.writestr("image_info.py", images_code) # Write a separate license file for human consumption: new_zf.writestr("license.txt", self.license_text) # Done creating the new zip file: new_zf.close() new_zf = None # Rename the original file to a temporary name, so we can give the # new file the original name. Note that unlocking the original zip # file after the previous close sometimes seems to take a while, # which is why we repeatedly try the rename until it either succeeds # or takes so long that it must have failed for another reason: temp_name = path + ".$$$" for i in range(50): try: rename(path, temp_name) break except Exception: time.sleep(0.1) try: rename(file_name, path) file_name = temp_name except: rename(temp_name, path) raise finally: if new_zf is not None: new_zf.close() remove(file_name) return True def image_resource(self, image_name): """ Returns the ImageResource object for the specified **image_name**. """ # Get the name of the image file: volume_name, file_name = split_image_name(image_name) if self.is_zip_file: # See if we already have the image file cached in the file system: cache_file = self._check_cache(file_name) if cache_file is None: # If not cached, then create a zip file reference: ref = ZipFileReference( resource_factory=resource_manager.resource_factory, zip_file=self.zip_file, path=self.path, volume_name=self.name, file_name=file_name, ) else: # Otherwise, create a cache file reference: ref = ImageReference( resource_manager.resource_factory, filename=cache_file ) else: # Otherwise, create a normal file reference: ref = ImageReference( resource_manager.resource_factory, filename=join(self.path, file_name), ) # Create the ImageResource object using the reference (note that the # ImageResource class will not allow us to specify the reference in the # constructor): resource = ImageResource(file_name) resource._ref = ref # Return the ImageResource: return resource def image_data(self, image_name): """ Returns the image data (i.e. file contents) for the specified image name. """ volume_name, file_name = split_image_name(image_name) if self.is_zip_file: return self.zip_file.read(file_name) else: return read_file(join(self.path, file_name)) def volume_info(self, image_name): """ Returns the ImageVolumeInfo object that corresponds to the image specified by **image_name**. """ for info in self.info: if (len(info.image_names) == 0) or ( image_name in info.image_names ): return info raise ValueError( "Volume info for image name {} not found.".format(repr(info)) ) # -- Default Value Implementations ------------------------------------------ def _info_default(self): return [ImageVolumeInfo()] def _images_default(self): return self._load_image_info() # -- Property Implementations ----------------------------------------------- @cached_property def _get_catalog(self): return dict((image.image_name, image) for image in self.images) def _get_image_volume_code(self): data = dict( (name, repr(value)) for name, value in self.trait_get( "description", "category", "keywords", "aliases", "time_stamp" ).items() ) data["info"] = ",\n".join( info.image_volume_info_code for info in self.info ) return ImageVolumeTemplate % data def _get_images_code(self): images = ",\n".join(info.image_info_code for info in self.images) return ImageVolumeImagesTemplate % images def _get_license_text(self): return ("\n\n%s\n" % ("-" * 79)).join( [info.image_volume_info_text for info in self.info] ) # -- Private Methods -------------------------------------------------------- def _load_image_info(self): """ Returns the list of ImageInfo objects for the images in the volume. """ # If there is no current path, then return a default list of images: if self.path == "": return [] time_stamp = time_stamp_for(stat(self.path)[ST_MTIME]) volume_name = self.name old_images = [] cur_images = [] if self.is_zip_file: zf = self.zip_file # Get the names of all top-level entries in the zip file: names = zf.namelist() # Check to see if there is an image info manifest file: if "image_info.py" in names: # Load the manifest code and extract the images list: old_images = get_python_value( zf.read("image_info.py"), "images" ) # Check to see if our time stamp is up to date with the file: if self.time_stamp < time_stamp: # If not, create an ImageInfo object for all image files # contained in the .zip file: for name in names: root, ext = splitext(name) if ext in ImageFileExts: cur_images.append( ImageInfo( name=root, image_name=join_image_name(volume_name, name), ) ) else: image_info_path = join(self.path, "image_info.py") if exists(image_info_path): # Load the manifest code and extract the images list: old_images = get_python_value( read_file(image_info_path), "images" ) # Check to see if our time stamp is up to data with the file: if self.time_stamp < time_stamp: # If not, create an ImageInfo object for each image file # contained in the path: for name in listdir(self.path): root, ext = splitext(name) if ext in ImageFileExts: cur_images.append( ImageInfo( name=root, image_name=join_image_name(volume_name, name), ) ) # Merge the old and current images into a single up to date list: if len(cur_images) == 0: images = old_images else: cur_image_set = dict( [(image.image_name, image) for image in cur_images] ) for old_image in old_images: cur_image = cur_image_set.get(old_image.image_name) if cur_image is not None: cur_image_set[old_image.image_name] = old_image cur_image.volume = self old_image.width = cur_image.width old_image.height = cur_image.height cur_image.volume = None images = list(cur_image_set.values()) # Set the new time stamp of the volume: self.time_stamp = time_stamp # Return the resulting sorted list as the default value: images.sort(key=lambda item: item.image_name) # Make sure all images reference this volume: for image in images: image.volume = self return images def _check_cache(self, file_name): """ Checks to see if the specified zip file name has been saved in the image cache. If it has, it returns the fully-qualified cache file name to use; otherwise it returns None. """ cache_file = join(image_cache_path, self.name, file_name) if exists(cache_file) and ( time_stamp_for(stat(cache_file)[ST_MTIME]) > self.time_stamp ): return cache_file return None # ------------------------------------------------------------------------------- # 'ZipFileReference' class: # ------------------------------------------------------------------------------- class ZipFileReference(ResourceReference): #: The zip file to read; zip_file = Instance(FastZipFile) #: The volume name: volume_name = Str() #: The file within the zip file: file_name = Str() #: The name of the cached image file: cache_file = File() # -- The 'ResourceReference' API -------------------------------------------- #: The file name of the image (in this case, the cache file name): filename = Property # -- ResourceReference Interface Implementation ----------------------------- def load(self): """ Loads the resource. """ # Check if the cache file has already been created: cache_file = self.cache_file if cache_file == "": # Extract the data from the zip file: data = self.zip_file.read(self.file_name) # Try to create an image from the data, without writing it to a # file first: image = self.resource_factory.image_from_data(data, Undefined) if image is not None: return image # Make sure the correct image cache directory exists: cache_dir = join(image_cache_path, self.volume_name) if not exists(cache_dir): makedirs(cache_dir) # Write the image data to the cache file: cache_file = join(cache_dir, self.file_name) with open(cache_file, "wb") as fh: fh.write(data) # Save the cache file name in case we are called again: self.cache_file = cache_file # Release our reference to the zip file object: self.zip_file = None # Return the image data from the image cache file: return self.resource_factory.image_from_file(cache_file) # -- Property Implementations ----------------------------------------------- def _get_filename(self): if self.cache_file == "": self.load() return self.cache_file # ------------------------------------------------------------------------------- # 'ImageLibrary' class: # ------------------------------------------------------------------------------- class ImageLibrary(HasPrivateTraits): """ Manages Traits UI image libraries. """ #: The list of available image volumes in the library: volumes = List(ImageVolume) #: The volume dictionary (the keys are volume names, and the values are the #: corresponding ImageVolume objects): catalog = Dict(Str, ImageVolume) #: The list of available images in the library: images = Property(List, observe="volumes.items.images") # -- Private Traits --------------------------------------------------------- #: Mapping from a 'virtual' library name to a 'real' library name: aliases = Dict() # -- Public methods --------------------------------------------------------- def image_info(self, image_name): """ Returns the ImageInfo object corresponding to a specified **image_name**. """ volume = self.find_volume(image_name) if volume is not None: return volume.catalog.get(image_name) return None def image_resource(self, image_name): """ Returns an ImageResource object for the specified image name. """ # If no volume was specified, use the standard volume: if image_name.find(":") < 0: image_name = "@images:%s" % image_name[1:] # Find the correct volume, possible resolving any aliases used: volume = self.find_volume(image_name) # Find the image within the volume and return its ImageResource object: if volume is not None: return volume.image_resource(image_name) # Otherwise, the volume was not found: return None def find_volume(self, image_name): """ Returns the ImageVolume object corresponding to the specified **image_name** or None if the volume cannot be found. """ # Extract the volume name from the image name: volume_name, file_name = split_image_name(image_name) # Find the correct volume, possibly resolving any aliases used: catalog = self.catalog aliases = self.aliases while volume_name not in catalog: volume_name = aliases.get(volume_name) if volume_name is None: return None return catalog[volume_name] def add_volume(self, file_name=None): """ If **file_name** is a file, it adds an image volume specified by **file_name** to the image library. If **file_name** is a directory, it adds all image libraries contained in the directory to the image library. If **file_name** is omitted, all image libraries located in the *images* directory contained in the same directory as the caller are added. """ # If no file name was specified, derive a path from the caller's # source code location: if file_name is None: file_name = join(get_resource_path(2), "images") if isfile(file_name): # Load an image volume from the specified file: volume = self._add_volume(file_name) if volume is None: raise TraitError( "'%s' is not a valid image volume." % file_name ) if volume.name in self.catalog: self._duplicate_volume(volume.name) self.catalog[volume.name] = volume self.volumes.append(volume) elif isdir(file_name): # Load all image volumes from the specified path: catalog = self.catalog volumes = self._add_path(file_name) for volume in volumes: if volume.name in catalog: self._duplicate_volume(volume.name) catalog[volume.name] = volume self.volumes.extend(volumes) else: # Handle an unrecognized argument: raise TraitError( "The add method argument must be None or a file " "or directory path, but '%s' was specified." % file_name ) def add_path(self, volume_name, path=None): """ Adds the directory specified by **path** as a *virtual* volume called **volume_name**. All image files contained within path define the contents of the volume. If **path** is None, the *images* contained in the 'images' subdirectory of the same directory as the caller are is used as the path for the *virtual* volume.. """ # Make sure we don't already have a volume with that name: if volume_name in self.catalog: raise TraitError( ("The volume name '%s' is already in the image " "library.") % volume_name ) # If no path specified, derive one from the caller's source code # location: if path is None: path = join(get_resource_path(2), "images") # Make sure that the specified path is a directory: if not isdir(path): raise TraitError( "The image volume path '%s' does not exist." % path ) # Create the ImageVolume to describe the path's contents: image_volume_path = join(path, "image_volume.py") if exists(image_volume_path): volume = get_python_value(read_file(image_volume_path), "volume") else: volume = ImageVolume() # Set up the rest of the volume information: volume.trait_set(name=volume_name, path=path, is_zip_file=False) # Try to bring the volume information up to date if necessary: if volume.time_stamp < time_stamp_for(stat(path)[ST_MTIME]): # Note that the save could fail if the volume is read-only, but # that's OK, because we're only trying to do the save in case # a developer had added or deleted some image files, which would # require write access to the volume: volume.save() # Add the new volume to the library: self.catalog[volume_name] = volume self.volumes.append(volume) def extract(self, file_name, image_names): """ Builds a new image volume called **file_name** from the list of image names specified by **image_names**. Each image name should be of the form: '@volume:name'. """ # Get the volume name and file extension: volume_name, ext = splitext(basename(file_name)) # If no extension specified, add the '.zip' file extension: if ext == "": file_name += ".zip" # Create the ImageVolume object to describe the new volume: volume = ImageVolume(name=volume_name) # Make sure the zip file does not already exists: if exists(file_name): raise TraitError("The '%s' file already exists." % file_name) # Create the zip file: zf = ZipFile(file_name, "w", ZIP_DEFLATED) # Add each of the specified images to it and the ImageVolume: error = True aliases = set() keywords = set() images = [] info = {} try: for image_name in set(image_names): # Verify the image name is legal: if (image_name[:1] != "@") or (image_name.find(":") < 0): raise TraitError( ( "The image name specified by '%s' is " "not of the form: @volume:name." ) % image_name ) # Get the reference volume and image file names: image_volume_name, image_file_name = split_image_name( image_name ) # Get the volume for the image name: image_volume = self.find_volume(image_name) if image_volume is None: raise TraitError( ( "Could not find the image volume " "specified by '%s'." ) % image_name ) # Get the image info: image_info = image_volume.catalog.get(image_name) if image_info is None: raise TraitError( ("Could not find the image specified by " "'%s'.") % image_name ) # Add the image info to the list of images: images.append(image_info) # Add the image file to the zip file: zf.writestr( image_file_name, image_volume.image_data(image_name) ) # Add the volume alias needed by the image (if any): if image_volume_name != volume_name: if image_volume_name not in aliases: aliases.add(image_volume_name) # Add the volume keywords as well: for keyword in image_volume.keywords: keywords.add(keyword) # Add the volume info for the image: volume_info = image_volume.volume_info(image_name) vinfo = info.get(image_volume_name) if vinfo is None: info[image_volume_name] = vinfo = volume_info.clone() vinfo.image_names.append(image_name) # Create the list of images for the volume: images.sort(key=lambda item: item.image_name) volume.images = images # Create the list of aliases for the volume: volume.aliases = list(aliases) # Create the list of keywords for the volume: volume.keywords = list(keywords) # Create the final volume info list for the volume: volume.info = list(info.values()) # Write the volume manifest source code to the zip file: zf.writestr("image_volume.py", volume.image_volume_code) # Write the image info source code to the zip file: zf.writestr("image_info.py", volume.images_code) # Write a separate licenses file for human consumption: zf.writestr("license.txt", volume.license_text) # Indicate no errors occurred: error = False finally: zf.close() if error: remove(file_name) # -- Default Value Implementations ------------------------------------------ def _volumes_default(self): result = [] # Check for and add the 'application' image library: app_library = join(dirname(abspath(sys.argv[0])), "library") if isdir(app_library): result.extend(self._add_path(app_library)) # Get all volumes in the standard Traits UI image library directory: result.extend(self._add_path(join(get_resource_path(1), "library"))) # Check to see if there is an environment variable specifying a list # of paths containing image libraries: paths = environ.get("TRAITS_IMAGES") if paths is not None: # Determine the correct OS path separator to use: separator = ";" if system() != "Windows": separator = ":" # Add all image volumes found in each path in the environment # variable: for path in paths.split(separator): result.extend(self._add_path(path)) # Return the list of default volumes found: return result def _catalog_default(self): return dict([(volume.name, volume) for volume in self.volumes]) # -- Property Implementations ----------------------------------------------- @cached_property def _get_images(self): return self._get_images_list() # -- Private Methods -------------------------------------------------------- def _get_images_list(self): """ Returns the list of all library images. """ # Merge the list of images from each volume: images = [] for volume in self.volumes: images.extend(volume.images) # Sort the result: images.sort(key=lambda image: image.image_name) # Return the images list: return images def _add_path(self, path): """ Returns a list of ImageVolume objects, one for each image library located in the specified **path**. """ result = [] # Make sure the path is a directory: if isdir(path): # Find each zip file in the directory: for base in listdir(path): if splitext(base)[1] == ".zip": # Try to create a volume from the zip file and add it to # the result: volume = self._add_volume(join(path, base)) if volume is not None: result.append(volume) # Return the list of volumes found: return result def _add_volume(self, path): """ Returns an ImageVolume object for the image library specified by **path**. If **path** does not specify a valid ImageVolume, None is returned. """ path = abspath(path) # Make sure the path is a valid zip file: if is_zipfile(path): # Create a fast zip file for reading: zf = FastZipFile(path=path) # Extract the volume name from the path: volume_name = splitext(basename(path))[0] # Get the names of all top-level entries in the zip file: names = zf.namelist() # Check to see if there is a manifest file: if "image_volume.py" in names: # Load the manifest code and extract the volume object: volume = get_python_value(zf.read("image_volume.py"), "volume") # Set the volume name: volume.name = volume_name # Try to add all of the external volume references as # aliases for this volume: self._add_aliases(volume) # Set the path to this volume: volume.path = path # Save the reference to the zip file object we are using: volume.zip_file = zf else: # Create a new volume from the zip file: volume = ImageVolume(name=volume_name, path=path, zip_file=zf) # If this volume is not up to date, update it: if volume.time_stamp < time_stamp_for(stat(path)[ST_MTIME]): # Note that the save could fail if the volume is read-only, but # that's OK, because we're only trying to do the save in case # a developer had added or deleted some image files, which would # require write access to the volume: volume.save() # Return the volume: return volume # Indicate no volume was found: return None def _add_aliases(self, volume): """ Try to add all of the external volume references as aliases for this volume. """ aliases = self.aliases volume_name = volume.name for vname in volume.aliases: if (vname in aliases) and (volume_name != aliases[vname]): raise TraitError( ( "Image library error: " "Attempt to alias '%s' to '%s' when it is " "already aliased to '%s'" ) % (vname, volume_name, aliases[volume_name]) ) aliases[vname] = volume_name def _duplicate_volume(self, volume_name): """ Raises a duplicate volume name error. """ raise TraitError( ( "Attempted to add an image volume called '%s' when " "a volume with that name is already defined." ) % volume_name ) # Create the singleton image object: ImageLibrary = ImageLibrary() pyface-7.4.0/pyface/i_gui.py0000644000076500000240000001334214176222673016664 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 interface of a pyface GUI. """ import logging import os from traits.etsconfig.api import ETSConfig from traits.api import Bool, HasTraits, Interface, Str # Logging. logger = logging.getLogger(__name__) class IGUI(Interface): """ The interface of a pyface GUI. """ # 'GUI' interface -----------------------------------------------------# #: Is the GUI busy (i.e. should the busy cursor, often an hourglass, be #: displayed)? busy = Bool(False) #: Has the GUI's event loop been started? started = Bool(False) #: A directory on the local file system that we can read and write to at #: will. This is used to persist layout information etc. Note that #: individual toolkits will have their own directory. state_location = Str() # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, splash_screen=None): """ Initialise a new GUI. Parameters ---------- splash_screen : ISplashScreen instance or None An optional splash screen that will be displayed until the event loop is started. """ # ------------------------------------------------------------------------ # 'GUI' class interface. # ------------------------------------------------------------------------ @staticmethod def allow_interrupt(): """ Override SIGINT to prevent swallowing KeyboardInterrupt Override the SIGINT handler to ensure the process can be interrupted. This prevents GUI toolkits from swallowing KeyboardInterrupt exceptions. Warning: do not call this method if you intend your application to be run interactively. """ @classmethod def invoke_after(cls, millisecs, callable, *args, **kw): """ Call a callable after a specific delay in the main GUI thread. Parameters ---------- millisecs : float Delay in milliseconds callable : callable Callable to be called after the delay args, kwargs : Arguments and keyword arguments to be used when calling. """ @classmethod def invoke_later(cls, callable, *args, **kw): """ Call a callable in the main GUI thread. Parameters ---------- callable : callable Callable to be called after the delay args, kwargs : Arguments and keyword arguments to be used when calling. """ @classmethod def set_trait_after(cls, millisecs, obj, trait_name, new): """ Sets a trait after a specific delay in the main GUI thread. Parameters ---------- millisecs : float Delay in milliseconds obj : HasTraits instance Object on which the trait is to be set trait_name : str The name of the trait to set new : any The value to set. """ @classmethod def set_trait_later(cls, obj, trait_name, new): """ Sets a trait in the main GUI thread. Parameters ---------- obj : HasTraits instance Object on which the trait is to be set trait_name : str The name of the trait to set new : any The value to set. """ @staticmethod def process_events(allow_user_events=True): """ Process any pending GUI events. Parameters ---------- allow_user_events : bool If allow_user_events is ``False`` then user generated events are not processed. """ @staticmethod def set_busy(busy=True): """Specify if the GUI is busy. Parameters ---------- busy : bool If ``True`` is passed, the cursor is set to a 'busy' cursor. Passing ``False`` will reset the cursor to the default. """ # ------------------------------------------------------------------------ # 'GUI' interface. # ------------------------------------------------------------------------ def start_event_loop(self): """ Start the GUI event loop. """ def stop_event_loop(self): """ Stop the GUI event loop. """ class MGUI(HasTraits): """ The mixin class that contains common code for toolkit specific implementations of the IGUI interface. Implements: _default_state_location() """ @staticmethod def allow_interrupt(): """ Override SIGINT to prevent swallowing KeyboardInterrupt Override the SIGINT handler to ensure the process can be interrupted. This prevents GUI toolkits from swallowing KeyboardInterrupt exceptions. Warning: do not call this method if you intend your application to be run interactively. """ import signal signal.signal(signal.SIGINT, signal.SIG_DFL) def _default_state_location(self): """ Return the default state location. """ state_location = os.path.join( ETSConfig.application_home, "pyface", ETSConfig.toolkit ) if not os.path.exists(state_location): os.makedirs(state_location) logger.debug("GUI state location is <%s>", state_location) return state_location pyface-7.4.0/pyface/application.py0000644000076500000240000002473514176222673020103 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 the :py:class:`Application` class for Pyface, Tasks and similar applications. Although the primary use cases are for GUI applications, the :py:class:`Application` class does not have any explicit dependency on GUI code, and can be used for CLI or server applications. Usual usage is to subclass :py:class:`Application`, overriding at least the :py:meth:`Application._run` method, but usually the :py:meth:`Application.start` and :py:meth:`Application.stop` methods as well. However the class can be used as-is by listening to the :py:attr:`Application.application_initialized` event and performing appropriate work there:: def do_work(event): print("Hello world") app = Application() app.observe(do_work, 'application_initialized') """ import logging import os from traits.api import ( Directory, Event, HasStrictTraits, Instance, ReadOnly, Str, Vetoable, VetoableEvent, ) logger = logging.getLogger(__name__) class ApplicationException(Exception): """ Exception subclass for Application-centric exceptions """ pass class ApplicationExit(ApplicationException): """ Exception which indicates application should try to exit. If no arguments, then assumed to be a normal exit, otherwise the arguments give information about the problem. """ pass class ApplicationEvent(HasStrictTraits): """ An event associated with an application """ #: The application that the event happened to. application = ReadOnly #: The type of application event. event_type = ReadOnly class Application(HasStrictTraits): """ A base class for applications. This class handles the basic lifecycle of an application and a few fundamental facilities. It is suitable as a base for any application, not just GUI applications. """ # 'Application' traits ---------------------------------------------------- # Branding ---------------------------------------------------------------- #: Human-readable application name name = Str("Pyface Application") #: Human-readable company name company = Str() #: Human-readable description of the application description = Str() # Infrastructure --------------------------------------------------------- #: The application's globally unique identifier. id = Str() #: Application home directory (for preferences, logging, etc.) home = Directory() #: User data directory (for user files, projects, etc) user_data = Directory() # Application lifecycle -------------------------------------------------- #: Fired when the application is starting. Called immediately before the #: start method is run. starting = Event(Instance(ApplicationEvent)) #: Upon successful completion of the start method. started = Event(Instance(ApplicationEvent)) #: Fired after the GUI event loop has been started during the run method. application_initialized = Event(Instance(ApplicationEvent)) #: Fired when the application is starting. Called immediately before the #: stop method is run. exiting = VetoableEvent() #: Fired when the application is starting. Called immediately before the #: stop method is run. stopping = Event(Instance(ApplicationEvent)) #: Upon successful completion of the stop method. stopped = Event(Instance(ApplicationEvent)) # ------------------------------------------------------------------------- # Application interface # ------------------------------------------------------------------------- # Application lifecycle methods ------------------------------------------ def start(self): """ Start the application, setting up things that are required Subclasses should call the superclass start() method before doing any work themselves. """ return True def stop(self): """ Stop the application, cleanly releasing resources if possible. Subclasses should call the superclass stop() method after doing any work themselves. """ return True def run(self): """ Run the application. Return ------ status : bool Whether or not the application ran normally """ run = stopped = False # Start up the application. logger.info("---- Application starting ----") self._fire_application_event("starting") started = self.start() if started: logger.info("---- Application started ----") self._fire_application_event("started") try: run = self._run() except ApplicationExit as exc: if exc.args == (): logger.info("---- ApplicationExit raised ----") else: logger.exception("---- ApplicationExit raised ----") run = exc.args == () finally: # Try to shut the application down. logger.info("---- Application stopping ----") self._fire_application_event("stopping") stopped = self.stop() if stopped: self._fire_application_event("stopped") logger.info("---- Application stopped ----") return started and run and stopped def exit(self, force=False): """ Exits the application. This method handles a request to shut down the application by the user, eg. from a menu. If force is False, the application can potentially veto the close event, leaving the application in the state that it was before the exit method was called. Parameters ---------- force : bool, optional (default False) If set, windows will receive no closing events and will be destroyed unconditionally. This can be useful for reliably tearing down regression tests, but should be used with caution. Raises ------ ApplicationExit Some subclasses may trigger the exit by raising ApplicationExit. """ logger.info("---- Application exit started ----") if force or self._can_exit(): try: self._prepare_exit() except Exception: logger.exception("Error preparing for application exit") finally: logger.info("---- Application exit ----") self._exit() else: logger.info("---- Application exit vetoed ----") # Initialization utilities ----------------------------------------------- def initialize_application_home(self): """ Set up the home directory for the application This is where logs, preference files and other config files will be stored. """ if not os.path.exists(self.home): logger.info("Application home directory does not exist, creating") os.makedirs(self.home) # ------------------------------------------------------------------------- # Private interface # ------------------------------------------------------------------------- # Main method ------------------------------------------------------------- def _run(self): """ Actual implementation of running the application This should be completely overriden by applications which want to actually do something. Usually this method starts an event loop and blocks, but for command-line applications this could be where the main application logic is called from. """ # Fire a notification that the app is running. If the app has an # event loop (eg. a GUI, Tornado web app, etc.) then this should be # fired _after_ the event loop starts using an appropriate callback # (eg. gui.set_trait_later). self._fire_application_event("application_initialized") return True # Utilities --------------------------------------------------------------- def _fire_application_event(self, event_type): event = ApplicationEvent(application=self, event_type=event_type) setattr(self, event_type, event) # Destruction methods ----------------------------------------------------- def _can_exit(self): """ Is exit vetoed by anything? The default behaviour is to fire the :py:attr:`exiting` event and check to see if any listeners veto. Subclasses may wish to override to perform additional checks. Returns ------- can_exit : bool Return True if exit is OK, False if something vetoes the exit. """ self.exiting = event = Vetoable() return not event.veto def _prepare_exit(self): """ Do any application-level state saving and clean-up Subclasses should override this method. """ pass def _exit(self): """ Shut down the application This is where application event loops and similar should be shut down. """ # invoke a normal exit from the application raise ApplicationExit() # Traits defaults --------------------------------------------------------- def _id_default(self): """ Use the application's directory as the id """ from traits.etsconfig.api import ETSConfig return ETSConfig._get_application_dirname() def _home_default(self): """ Default home comes from ETSConfig. """ from traits.etsconfig.api import ETSConfig return os.path.join(ETSConfig.application_data, self.id) def _user_data_default(self): """ Default user_data comes from ETSConfig. """ from traits.etsconfig.api import ETSConfig return ETSConfig.user_data def _company_default(self): """ Default company comes from ETSConfig. """ from traits.etsconfig.api import ETSConfig return ETSConfig.company def _description_default(self): """ Default description is the docstring of the application class. """ from inspect import getdoc text = getdoc(self) return text pyface-7.4.0/pyface/preference/0000755000076500000240000000000014176460550017327 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/preference/preference_node.py0000644000076500000240000000410714176222673023030 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Abstract base class for a node in a preference dialog. A preference node has a label and an image which are used to represent the node in a preference dialog (usually in the form of a tree). """ from pyface.viewer.tree_item import TreeItem from traits.api import Str class PreferenceNode(TreeItem): """ Abstract base class for a node in a preference dialog. A preference node has a name and an image which are used to represent the node in a preference dialog (usually in the form of a tree). """ # 'PreferenceNode' interface ------------------------------------------- # The node's unique Id. id = Str() # The node's image. image = Str() # The node name. name = Str() # The Id of the help topic for the node. help_id = Str() # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __str__(self): """ Returns the string representation of the item. """ return self.name # ------------------------------------------------------------------------ # 'PreferenceNode' interface. # ------------------------------------------------------------------------ def create_page(self): """ Creates the preference page for this node. """ raise NotImplementedError() def lookup(self, id): """ Returns the child of this node with the specified Id. Returns None if no such child exists. """ for node in self.children: if node.id == id: break else: node = None return node pyface-7.4.0/pyface/preference/__init__.py0000644000076500000240000000062714176222673021447 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! pyface-7.4.0/pyface/preference/api.py0000644000076500000240000000126114176222673020454 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 ``pyface.preference`` subpackage. - :class:`~.PreferencePage` - :class:`~.PreferenceDialog` - :class:`~.PreferenceNode` """ from .preference_page import PreferencePage from .preference_dialog import PreferenceDialog from .preference_node import PreferenceNode pyface-7.4.0/pyface/preference/preference_dialog.py0000644000076500000240000000113714176222673023342 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 preference dialog. """ # Import the toolkit specific version. from pyface.toolkit import toolkit_object PreferenceDialog = toolkit_object("preference.preference_dialog:PreferenceDialog") pyface-7.4.0/pyface/preference/preference_page.py0000644000076500000240000000226314176222673023020 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Abstract base class for all preference pages. """ from traits.api import HasTraits # fixme: in JFace this extends from 'DialogPage' which we don't have yet! class PreferencePage(HasTraits): """ Abstract base class for all preference pages. """ # ------------------------------------------------------------------------ # 'PreferencePage' interface. # ------------------------------------------------------------------------ def create_control(self, parent): """ Creates the toolkit-specific control for the page. """ raise NotImplementedError() def restore_defaults(self): """ Restore the default preferences. """ pass def show_help_topic(self): """ Show the help topic for this preference page.""" pass pyface-7.4.0/pyface/i_file_dialog.py0000644000076500000240000000744214176222673020342 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 interface for a dialog that allows the user to open/save files etc. """ import sys from traits.api import Enum, HasTraits, Int, List, Str from pyface.i_dialog import IDialog class IFileDialog(IDialog): """ The interface for a dialog that allows the user to open/save files etc. """ # 'IFileDialog' interface ---------------------------------------------# #: The 'action' that the user is peforming on the directory ("open files" #: differs from "open" in that the former supports multiple selections). action = Enum("open", "open files", "save as") #: The default directory. default_directory = Str() #: The default filename. default_filename = Str() #: The default path (directory and filename) of the chosen file. This is #: only used when the *default_directory* and *default_filename* are not set #: and is equivalent to setting both. default_path = Str() #: The directory containing the chosen file. directory = Str() #: The name (basename only) of the chosen file. filename = Str() #: The path (directory and filename) of the chosen file. To be used when only #: single selection is allowed: if *action* is "open files", use *paths* instead. path = Str() #: The paths (directory and filename) of the chosen files. To be used when #: multiple selection is allowed. paths = List(Str()) #: The wildcard used to restrict the set of files. wildcard = Str() #: The index of the selected wildcard. wildcard_index = Int(0) class MFileDialog(HasTraits): """ The mixin class that contains common code for toolkit specific implementations of the IFileDialog interface. Implements: create_wildcard() """ # FIXME v3: These are referenced elsewhere so maybe should be part of the # interface. The format is toolkit specific and so shouldn't be exposed. # The create_wildcard() class method (why isn't it a static method?) should # be the basis for this - but nothing seems to use it. For now the PyQt # implementation will convert the wx format to its own. Actually we need # the format to be portable between toolkits - so stick with PyQt # supporting the wx format or invent a data structure. #: A file dialog wildcard for Python files. WILDCARD_PY = "Python files (*.py)|*.py|" #: A file dialog wildcard for text files. WILDCARD_TXT = "Text files (*.txt)|*.txt|" #: A file dialog wildcard for all files. if sys.platform == "win32": WILDCARD_ALL = "All files (*.*)|*.*|" else: WILDCARD_ALL = "All files (*)|*" #: A file dialog wildcard for Zip archives. WILDCARD_ZIP = "Zip files (*.zip)|*.zip|" # ------------------------------------------------------------------------ # 'MFileDialog' *CLASS* interface. # ------------------------------------------------------------------------ @classmethod def create_wildcard(cls, description, extension): """ Creates a wildcard for a given extension. Parameters ---------- description : str A human-readable description of the pattern. extenstion : list The wildcard patterns for the extension. """ if isinstance(extension, str): pattern = extension else: pattern = ";".join(extension) return "%s (%s)|%s|" % (description, pattern, pattern) pyface-7.4.0/pyface/i_confirmation_dialog.py0000644000076500000240000000336114176222673022107 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 interface for a dialog that prompts the user for confirmation. """ from traits.api import Bool, Enum, HasTraits, Str from pyface.constant import CANCEL, NO, YES from pyface.i_dialog import IDialog from pyface.ui_traits import Image class IConfirmationDialog(IDialog): """ The interface for a dialog that prompts the user for confirmation. """ # 'IConfirmationDialog' interface -------------------------------------# #: Should the cancel button be displayed? cancel = Bool(False) #: The default button. default = Enum(NO, YES, CANCEL) #: The image displayed with the message. The default is toolkit specific. image = Image() #: The message displayed in the body of the dialog (use the inherited #: 'title' trait to set the title of the dialog itself). message = Str() #: Some informative text to display below the main message informative = Str() #: Some additional details that can be exposed by the user detail = Str() #: The label for the 'no' button. The default is toolkit specific. no_label = Str() #: The label for the 'yes' button. The default is toolkit specific. yes_label = Str() class MConfirmationDialog(HasTraits): """ The mixin class that contains common code for toolkit specific implementations of the IConfirmationDialog interface. """ pyface-7.4.0/pyface/api.py0000644000076500000240000001313614176222673016342 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 ``pyface`` package. - :class:`~.Application` - :class:`~.ApplicationWindow` - :attr:`~.clipboard` - :class:`~.Clipboard` - :class:`~.GUI` - :class:`~.GUIApplication` - :class:`~.ImageResource` - :class:`~.KeyPressedEvent` - :class:`~.SplashScreen` - :class:`~.SplitApplicationWindow` - :class:`~.SplitPanel` - :class:`~.SystemMetrics` - :class:`~.Window` - :class:`~.Widget` Dialogs ------- - :class:`~.AboutDialog` - :class:`~.confirm` - :class:`~.ConfirmationDialog` - :class:`~.Dialog` - :class:`~.DirectoryDialog` - :class:`~.FileDialog` - :class:`~.error` - :class:`~.information` - :class:`~.warning` - :class:`~.MessageDialog` - :class:`~.ProgressDialog` - :class:`~.choose_one` - :class:`~.SingleChoiceDialog` - :class:`~.SplitDialog` Constants --------- - :class:`~.OK` - :class:`~.CANCEL` - :class:`~.YES` - :class:`~.NO` UI Traits --------- - :attr:`~.Alignment` - :class:`~.Border` - :class:`~.HasBorder` - :class:`~.HasMargin` - :class:`~.Image` - :class:`~.Margin` Miscellaneous ------------- - :class:`~.ArrayImage` - :class:`~.BaseDropHandler` - :class:`~.beep` - :class:`~.FileDropHandler` - :class:`~.Filter` - :class:`~.HeadingText` - :class:`~.ImageCache` - :class:`~.LayeredPanel` - :class:`~.PILImage` - :class:`~.PythonEditor` - :class:`~.PythonShell` - :class:`~.Sorter` Note that the :class:`~.ArrayImage` is only available if the ``numpy`` package is available in the Python environment. Note that the :class:`~.PILImage` is only available if the ``pillow`` package is available in the Python environment. Note that the :class:`~.PythonEditor` and :class:`~.PythonShell` classes are only available if the ``pygments`` package is available in the Python environment. Wx-specific classes ------------------- - :class:`~.ExpandablePanel` - :class:`~.ImageWidget` - :class:`~.MDIApplicationWindow` - :class:`~.MDIWindowMenu` - :class:`~.MultiToolbarWindow` """ import logging as _logging from .about_dialog import AboutDialog from .application import Application from .application_window import ApplicationWindow from .beep import beep from .clipboard import clipboard, Clipboard from .confirmation_dialog import confirm, ConfirmationDialog from .constant import OK, CANCEL, YES, NO from .dialog import Dialog from .directory_dialog import DirectoryDialog from .drop_handler import BaseDropHandler, FileDropHandler from .file_dialog import FileDialog from .filter import Filter from .gui import GUI from .gui_application import GUIApplication from .heading_text import HeadingText from .image_cache import ImageCache from .image_resource import ImageResource from .key_pressed_event import KeyPressedEvent from .layered_panel import LayeredPanel from .message_dialog import error, information, warning, MessageDialog from .progress_dialog import ProgressDialog from .util._optional_dependencies import optional_import as _optional_import # Excuse numpy dependency, otherwise re-raise with _optional_import( "numpy", msg="ArrayImage not available due to missing numpy.", logger=_logging.getLogger(__name__)): # We need to manually try importing numpy because the ``ArrayImage`` # import will end up raising a ``TraitError`` exception instead of an # ``ImportError``, which isnt caught by ``_optional_import``. import numpy from .array_image import ArrayImage del numpy # Excuse pillow dependency, otherwise re-raise with _optional_import( "pillow", msg="PILImage not available due to missing pillow.", logger=_logging.getLogger(__name__)): from .pil_image import PILImage # Excuse pygments dependency (for Qt), otherwise re-raise with _optional_import( "pygments", msg="PythonEditor not available due to missing pygments.", logger=_logging.getLogger(__name__)): from .python_editor import PythonEditor with _optional_import( "pygments", msg="PythonShell not available due to missing pygments.", logger=_logging.getLogger(__name__)): from .python_shell import PythonShell from .sorter import Sorter from .single_choice_dialog import choose_one, SingleChoiceDialog from .splash_screen import SplashScreen from .split_application_window import SplitApplicationWindow from .split_dialog import SplitDialog from .split_panel import SplitPanel from .system_metrics import SystemMetrics from .ui_traits import Alignment, Border, HasBorder, HasMargin, Image, Margin from .window import Window from .widget import Widget # ---------------------------------------------------------------------------- # Legacy and Wx-specific imports. # ---------------------------------------------------------------------------- # These widgets currently only have Wx implementations # will return Unimplemented for Qt. from .expandable_panel import ExpandablePanel from .image_widget import ImageWidget from .mdi_application_window import MDIApplicationWindow from .mdi_window_menu import MDIWindowMenu from .multi_toolbar_window import MultiToolbarWindow # This code isn't toolkit widget code, but is wx-specific from traits.etsconfig.api import ETSConfig if ETSConfig.toolkit == "wx": # Fix for broken Pycrust introspect module. # XXX move this somewhere better? - CJW 2017 from .util import fix_introspect_bug del ETSConfig pyface-7.4.0/pyface/testing/0000755000076500000240000000000014176460550016666 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/testing/widget_mixin.py0000644000076500000240000000570314176222673021736 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.mock import patch from traits.testing.api import UnittestTools from pyface.action.api import Action, MenuManager from pyface.gui import GUI from pyface.window import Window class WidgetMixin(UnittestTools): """ Mixin which provides standard methods for all widgets. """ def setUp(self): self.gui = GUI() self.parent = self._create_parent() self.parent._create() self.addCleanup(self._destroy_parent) self.gui.process_events() self.widget = self._create_widget() self.parent.open() self.gui.process_events() def _create_parent(self): return Window() def _create_widget(self): raise NotImplementedError() def _create_widget_control(self): self.widget._create() self.addCleanup(self._destroy_widget) self.widget.show(True) self.gui.process_events() def _destroy_parent(self): self.parent.destroy() self.gui.process_events() self.parent = None def _destroy_widget(self): self.widget.destroy() self.gui.process_events() self.widget = None def test_widget_tooltip(self): self._create_widget_control() self.widget.tooltip = "New tooltip." self.gui.process_events() self.assertEqual(self.widget._get_control_tooltip(), "New tooltip.") def test_widget_tooltip_cleanup(self): widget = self._create_widget() with patch.object(widget, '_tooltip_updated', return_value=None) as updated: widget._create() try: widget.show(True) self.gui.process_events() finally: widget.destroy() self.gui.process_events() widget.tooltip = "New tooltip." updated.assert_not_called() widget = None def test_widget_menu(self): self._create_widget_control() self.widget.context_menu = MenuManager(Action(name="Test"), name="Test") self.gui.process_events() def test_widget_context_menu_cleanup(self): widget = self._create_widget() with patch.object(widget, '_context_menu_updated', return_value=None) as updated: widget._create() try: widget.show(True) self.gui.process_events() finally: widget.destroy() self.gui.process_events() widget.context_menu = MenuManager(Action(name="Test"), name="Test") updated.assert_not_called() widget = None pyface-7.4.0/pyface/testing/__init__.py0000644000076500000240000000000014176222673020767 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/testing/layout_widget_mixin.py0000644000076500000240000000541214176222673023330 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.testing.widget_mixin import WidgetMixin class LayoutWidgetMixin(WidgetMixin): """ Test mixin for classes which inherit LayoutWidget. """ def test_minimum_size(self): # create a widget with a minimum size self.widget.minimum_size = (100, 100) self.widget.create() minimum_size = self.widget._get_control_minimum_size() self.gui.process_events() self.assertEqual(minimum_size, (100, 100)) # change the minimum size self.widget.minimum_size = (50, 50) self.gui.process_events() minimum_size = self.widget._get_control_minimum_size() self.assertEqual(minimum_size, (50, 50)) def test_maximum_size(self): # create a widget with a maximum size self.widget.maximum_size = (1000, 1000) self.widget.create() maximum_size = self.widget._get_control_maximum_size() self.gui.process_events() self.assertEqual(maximum_size, (1000, 1000)) # change the maximum size self.widget.maximum_size = (50, 50) self.gui.process_events() maximum_size = self.widget._get_control_maximum_size() self.assertEqual(maximum_size, (50, 50)) def test_stretch(self): # create a widget with a maximum size self.widget.stretch = (2, 3) self.widget.create() stretch = self.widget._get_control_stretch() self.gui.process_events() self.assertEqual(stretch, (2, 3)) # change the maximum size self.widget.stretch = (5, 0) self.gui.process_events() maximum_size = self.widget._get_control_stretch() self.assertEqual(maximum_size, (5, 0)) def test_size_policy(self): # create a widget with a maximum size self.widget.create() self.assertEqual( self.widget._get_control_size_policy(), ("default", "default"), ) for horizontal in ["fixed", "preferred", "expand"]: for vertical in ["fixed", "preferred", "expand"]: with self.subTest(horizontal=horizontal, vertical=vertical): self.widget.size_policy = (horizontal, vertical) self.gui.process_events() self.assertEqual( self.widget._get_control_size_policy(), (horizontal, vertical), ) pyface-7.4.0/pyface/application_window.py0000644000076500000240000000115714176222673021463 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 implementation of a top-level application window. """ # Import the toolkit specific version. from .toolkit import toolkit_object ApplicationWindow = toolkit_object("application_window:ApplicationWindow") pyface-7.4.0/pyface/sorter.py0000644000076500000240000000460214176222673017105 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Base class for all sorters. """ from traits.api import HasTraits class Sorter(HasTraits): """ Abstract base class for all sorters. """ # ------------------------------------------------------------------------ # 'ViewerSorter' interface. # ------------------------------------------------------------------------ def sort(self, widget, parent, nodes): """ Sorts a list of nodes IN PLACE. 'widget' is the widget that we are sorting nodes for. 'parent' is the parent node. 'nodes' is the list of nodes to sort. Returns the list that was sorted IN PLACE (for convenience). """ # This creates a comparison function with the names 'widget' and # 'parent' bound to the corresponding arguments to this method. def key(node): """ Comparator. """ return self.key(widget, parent, node) nodes.sort(key=key) return nodes def key(self, widget, parent, node): category = self.category(widget, parent, node) text = widget.model.get_text(node) return (category, text) def category(self, widget, parent, node): """ Returns the category (an integer) for an node. 'parent' is the parent node. 'nodes' is the node to return the category for. Categories are used to sort nodes into bins. The bins are arranged in ascending numeric order. The nodes within a bin are arranged as dictated by the sorter's 'compare' method. By default all nodes are given the same category (0). """ return 0 def is_sorter_trait(self, node, trait_name): """ Is the sorter affected by changes to a node's trait? 'node' is the node. 'trait_name' is the name of the trait. Returns True if the sorter would be affected by changes to the trait named 'trait_name' on the specified node. By default we return False. """ return False pyface-7.4.0/pyface/progress_dialog.py0000644000076500000240000000116414176222673020752 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 progress dialog window which allows itself to be updated """ # Import the toolkit specific version. from .toolkit import toolkit_object ProgressDialog = toolkit_object("progress_dialog:ProgressDialog") pyface-7.4.0/pyface/layered_panel.py0000644000076500000240000000113314176222673020367 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # Note: The MultiToolbarWindow is currently wx-specific # Import the toolkit specific version. from .toolkit import toolkit_object LayeredPanel = toolkit_object("layered_panel:LayeredPanel") pyface-7.4.0/pyface/action/0000755000076500000240000000000014176460550016466 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/action/tool_bar_manager.py0000644000076500000240000000125214176222673022335 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 toolkit-specific tool bar manager that realizes itself in a tool bar control. - :attr:`~.ToolBarManager` """ # Import the toolkit specific version. from pyface.toolkit import toolkit_object ToolBarManager = toolkit_object("action.tool_bar_manager:ToolBarManager") pyface-7.4.0/pyface/action/action_event.py0000644000076500000240000000235314176222673021523 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 event passed to an action's 'perform' method. """ import time from traits.api import Float, HasTraits class ActionEvent(HasTraits): """ The event passed to an action's 'perform' method. """ # 'ActionEvent' interface ---------------------------------------------# #: When the action was performed (time.time()). when = Float() # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, **traits): """ Creates a new action event. Note: Every keyword argument becomes a public attribute of the event. """ # Base-class constructor. super().__init__(**traits) # When the action was performed. self.when = time.time() pyface-7.4.0/pyface/action/menu_manager.py0000644000076500000240000000122514176222673021500 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 toolkit-specific menu manager that realizes itself in a menu control. - :attr:`~.MenuManager` """ # Import the toolkit specific version. from pyface.toolkit import toolkit_object MenuManager = toolkit_object("action.menu_manager:MenuManager") pyface-7.4.0/pyface/action/field_action.py0000644000076500000240000000443714176222673021472 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 Any, Constant, Dict, Str, Type from pyface.fields.i_field import IField from .action import Action from .action_event import ActionEvent class FieldAction(Action): """ A widget action containing an IField When the value in the field is changed, the `on_perform` method is called with the new value as the argument. """ #: This is a widget action. style = Constant("widget") #: The field to display. field_type = Type(IField) #: The default trait values for the field. field_defaults = Dict(Str, Any) def create_control(self, parent): """ Called when creating a "widget" style action. This constructs an IField-based control directly and binds changes to the value to the `value_updated` method. Parameters ---------- parent : toolkit control The toolkit control, usually a toolbar. Returns ------- control : toolkit control A toolkit control or None. """ field = self.field_type(parent=parent, **self.field_defaults) field._create() field.observe(self.value_updated, "value") field.control._field = field return field.control def value_updated(self, event): """ Handle changes to the field value by calling perform. The event passed to `perform` has the `value` as an attribute. """ value = event.new action_event = ActionEvent(value=value) self.perform(action_event) def perform(self, event): """ Performs the action. This dispacthes to the on_perform method with the new value passed as an argument. Parameters ---------- event : ActionEvent instance The event which triggered the action. """ if self.on_perform is not None: self.on_perform(event.value) pyface-7.4.0/pyface/action/tool_palette_manager.py0000644000076500000240000000130414176222673023225 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 toolkit-specific tool bar manager that realizes itself in a tool palette control. - :attr:`~.ToolPaletteManager` """ # Import the toolkit specific version. from pyface.toolkit import toolkit_object ToolPaletteManager = toolkit_object( "action.tool_palette_manager:ToolPaletteManager" ) pyface-7.4.0/pyface/action/window_action.py0000644000076500000240000000306314176222673021710 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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: Enthought, Inc. # Description: """ Abstract base class for all window actions. """ from pyface.window import Window from traits.api import Instance, Property from pyface.action.listening_action import ListeningAction class WindowAction(ListeningAction): """ Abstract base class for window actions. """ # 'ListeningAction' interface -------------------------------------------- object = Property(observe="window") # 'WindowAction' interface ----------------------------------------------- #: The window that the action is associated with. window = Instance(Window) # ------------------------------------------------------------------------ # Protected interface. # ------------------------------------------------------------------------ def _get_object(self): return self.window def destroy(self): # Disconnect listeners to window and dependent properties. self.window = None super().destroy() class CloseWindowAction(WindowAction): """ Close the specified window """ name = "Close" accelerator = "Ctrl+W" method = "close" pyface-7.4.0/pyface/action/gui_application_action.py0000644000076500000240000000615614176222673023556 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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: Enthought, Inc. # Description: """ Abstract base class for all application actions. """ import platform from traits.api import Instance, Property, cached_property from pyface.action.listening_action import ListeningAction IS_WINDOWS = platform.system() == "Windows" class GUIApplicationAction(ListeningAction): """ Abstract base class for GUI Application actions. """ # 'ListeningAction' interface -------------------------------------------- object = Property(observe="application") # 'WindowAction' interface ----------------------------------------------- #: The application that the action is associated with. application = Instance("pyface.gui_application.GUIApplication") # ------------------------------------------------------------------------ # Protected interface. # ------------------------------------------------------------------------ def _get_object(self): return self.application def destroy(self): # Disconnect listeners to application and dependent properties. self.application = None super().destroy() class ActiveWindowAction(GUIApplicationAction): """ Abstract base class for application active window actions. """ # 'ListeningAction' interface -------------------------------------------- object = Property(observe="application.active_window") # ------------------------------------------------------------------------ # Protected interface. # ------------------------------------------------------------------------ @cached_property def _get_object(self): if self.application is not None: return self.application.active_window class CreateWindowAction(GUIApplicationAction): """ A standard 'New Window' menu action. """ name = "New Window" accelerator = "Ctrl+N" def perform(self, event=None): window = self.application.create_window() self.application.add_window(window) class ExitAction(GUIApplicationAction): """ A standard 'Quit' or 'Exit' menu action. """ accelerator = "Alt+F4" if IS_WINDOWS else "Ctrl+Q" method = "exit" def _name_default(self): return ("Exit " if IS_WINDOWS else "Quit ") + self.application.name class AboutAction(GUIApplicationAction): """ A standard 'About' dialog menu action. """ method = "do_about" def _name_default(self): return "About " + self.application.name class CloseActiveWindowAction(ActiveWindowAction): """ A standard 'Close window' menu action at the application level. This method closes the active window of the application. """ name = "Close Window" accelerator = "Ctrl+W" method = "close" pyface-7.4.0/pyface/action/images/0000755000076500000240000000000014176460550017733 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/action/images/action.png0000644000076500000240000000116314176222673021721 0ustar cwebsterstaff00000000000000PNG  IHDRabKGDxӖ+ pHYs  tIME 7|lIDAT8˥MkQ;4 Zt&I1P[њ ".DDHopnŝ`h~`mLT$չ.1"}އsJ)^זbo>PۘC:&lƹy.!{ſXmX.z@h޵'`#;v]QsgEG I%% x[9dv>{O68cD ٰXXM; O1鋠P@pDGtvCEHS+uZ!,;œ7#pqi+N {d%mIܶ ;17U!S" NQ&=ĩ8氟$jLs)vM"gINaT`']FnR<]~MjMQte܋J^YQi6Ȕ%Ju2eIhegkj)Lw$NŹu.4_.%.kIENDB`pyface-7.4.0/pyface/action/listening_action.py0000644000076500000240000001235014176222673022374 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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: Enthought, Inc. # Description: import logging from pyface.action.action import Action from traits.api import Any, Str, observe, Undefined # Logging. logger = logging.getLogger(__name__) class ListeningAction(Action): """ An Action that listens and makes a callback to an object. """ # ListeningAction interface ---------------------------------------------- #: The (extended) name of the method to call. By default, the on_perform #: function will be called with the event. method = Str() #: The (extended) name of the attribute that determines whether the action #: is enabled. By default, the action is always enabled when an object is #: set. enabled_name = Str() #: The (extended) name of the attribute that determines whether the action #: is visible. By default, the action is always visible. visible_name = Str() #: The object to which the names above apply. object = Any() # ------------------------------------------------------------------------- # 'Action' interface. # ------------------------------------------------------------------------- def destroy(self): """ Called when the action is no longer required. Removes all the task listeners. """ if self.object: if self.enabled_name: self.object.observe( self._enabled_update, self.enabled_name, remove=True ) if self.visible_name: self.object.observe( self._visible_update, self.visible_name, remove=True ) def perform(self, event=None): """ Call the appropriate function. This looks for a method to call based on the extended method name stored in the :py:attr:`method` trait. If the method is empty, then this follows the usual Action method resolution. """ if self.method != "": method = self._get_attr(self.object, self.method) if method: method() else: super().perform(event) # ------------------------------------------------------------------------- # Protected interface. # ------------------------------------------------------------------------- def _get_attr(self, obj, name, default=None): """ Perform an extended look up of a dotted name. """ try: for attr in name.split("."): # Perform the access in the Trait name style: if the object is # None, assume it simply hasn't been initialized and don't show # the warning. if obj is None: return default else: obj = getattr(obj, attr) except AttributeError: logger.error("Did not find name %r on %r" % (attr, obj)) return default return obj # Trait change handlers -------------------------------------------------- @observe('enabled_name') def _enabled_name_updated(self, event): old, new = event.old, event.new obj = self.object if obj is not None: if old: obj.observe(self._enabled_update, old, remove=True) if new: obj.observe(self._enabled_update, new) self._enabled_update() @observe('visible_name') def _visible_name_updated(self, event): old, new = event.old, event.new obj = self.object if obj is not None: if old: obj.observe(self._visible_update, old, remove=True) if new: obj.observe(self._visible_update, new) self._visible_update() @observe('object') def _object_updated(self, event): old, new = event.old, event.new for kind in ("enabled", "visible"): method = getattr(self, "_%s_update" % kind) name = getattr(self, "%s_name" % kind) if name: if old and old is not Undefined: old.observe(method, name, remove=True) if new: new.observe(method, name) method() def _enabled_update(self, event=None): if self.enabled_name: if self.object: self.enabled = bool( self._get_attr(self.object, self.enabled_name, False) ) else: self.enabled = False else: self.enabled = bool(self.object) def _visible_update(self, event=None): if self.visible_name: if self.object: self.visible = bool( self._get_attr(self.object, self.visible_name, False) ) else: self.visible = False else: self.visible = True pyface-7.4.0/pyface/action/tests/0000755000076500000240000000000014176460550017630 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/action/tests/test_group.py0000644000076500000240000001566214176222673022411 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.api import UnittestTools from ..action import Action from ..action_item import ActionItem from ..group import Group class TestActionItem(unittest.TestCase, UnittestTools): def setUp(self): # test whether function is called by updating list # XXX should really use mock self.memo = [] def perform(): self.memo.append("called") self.perform = perform self.action = Action(name="Test", on_perform=perform) self.action_item = ActionItem(action=self.action) def test_init_action_item(self): group = Group(self.action_item) self.assertEqual(group.items, [self.action_item]) self.assertEqual(self.action_item.parent, group) def test_init_action(self): group = Group(self.action) self.assertEqual(len(group.items), 1) self.assertEqual(group.items[0].action, self.action) self.assertEqual(group.items[0].parent, group) def test_init_callable(self): group = Group(self.perform) self.assertEqual(len(group.items), 1) self.assertEqual(group.items[0].action.on_perform, self.perform) self.assertEqual(group.items[0].action.name, "Perform") self.assertEqual(group.items[0].parent, group) def test_init_nothing(self): group = Group() self.assertEqual(group.items, []) def test_append(self): group = Group(self.action_item) action_item2 = ActionItem(action=Action(name="Action 2")) # XXX items doesn't fire a change event. Should it? group.append(action_item2) self.assertEqual(group.items, [self.action_item, action_item2]) self.assertEqual(action_item2.parent, group) def test_append_action(self): group = Group(self.action_item) action2 = Action(name="Action 2") # XXX items doesn't fire a change event. Should it? group.append(action2) self.assertEqual(len(group.items), 2) self.assertEqual(group.items[0], self.action_item) self.assertEqual(group.items[1].action, action2) self.assertEqual(group.items[1].parent, group) def test_append_callable(self): group = Group(self.action_item) # XXX items doesn't fire a change event. Should it? group.append(self.perform) self.assertEqual(len(group.items), 2) self.assertEqual(group.items[0], self.action_item) self.assertEqual(group.items[1].action.on_perform, self.perform) self.assertEqual(group.items[1].action.name, "Perform") self.assertEqual(group.items[1].parent, group) def test_clear(self): group = Group(self.action_item) # XXX items doesn't fire a change event. Should it? group.clear() self.assertEqual(group.items, []) # XXX clear doesn't set items' parent to None, but remove does... # self.assertIsNone(self.action_item.parent) def test_destroy(self): group = Group(self.action_item) # XXX items doesn't fire a change event. Should it? # XXX should mock action item's to ensure that destroy is called group.destroy() self.assertEqual(group.items, [self.action_item]) def test_insert(self): group = Group(self.action_item) action_item2 = ActionItem(action=Action(name="Action 2")) # XXX items doesn't fire a change event. Should it? group.insert(1, action_item2) self.assertEqual(group.items, [self.action_item, action_item2]) self.assertEqual(action_item2.parent, group) def test_insert_action(self): group = Group(self.action_item) action2 = Action(name="Action 2") # XXX items doesn't fire a change event. Should it? group.insert(1, action2) self.assertEqual(len(group.items), 2) self.assertEqual(group.items[0], self.action_item) self.assertEqual(group.items[1].action, action2) self.assertEqual(group.items[1].parent, group) def test_insert_callable(self): group = Group(self.action_item) # XXX items doesn't fire a change event. Should it? group.insert(1, self.perform) self.assertEqual(len(group.items), 2) self.assertEqual(group.items[0], self.action_item) self.assertEqual(group.items[1].action.on_perform, self.perform) self.assertEqual(group.items[1].action.name, "Perform") self.assertEqual(group.items[1].parent, group) def test_insert_at_start(self): group = Group(self.action_item) action_item2 = ActionItem(action=Action(name="Action 2")) # XXX items doesn't fire a change event. Should it? group.insert(0, action_item2) self.assertEqual(group.items, [action_item2, self.action_item]) self.assertEqual(action_item2.parent, group) def test_remove(self): group = Group(self.action_item) # XXX items doesn't fire a change event. Should it? group.remove(self.action_item) self.assertEqual(group.items, []) self.assertIsNone(self.action_item.parent) def test_remove_missing(self): group = Group() with self.assertRaises(ValueError): group.remove(self.action_item) def test_insert_before(self): group = Group(self.action_item) action_item2 = ActionItem(action=Action(name="Action 2")) # XXX items doesn't fire a change event. Should it? group.insert_before(self.action_item, action_item2) self.assertEqual(group.items, [action_item2, self.action_item]) self.assertEqual(action_item2.parent, group) def test_insert_after(self): group = Group(self.action_item) action_item2 = ActionItem(action=Action(name="Action 2")) # XXX items doesn't fire a change event. Should it? group.insert_after(self.action_item, action_item2) self.assertEqual(group.items, [self.action_item, action_item2]) self.assertEqual(action_item2.parent, group) def test_find(self): group = Group(self.action_item) item = group.find("Test") self.assertEqual(item, self.action_item) def test_find_missing(self): group = Group(self.action_item) item = group.find("Not here") self.assertIsNone(item) def test_enabled_changed(self): group = Group(self.action_item) group.enabled = False self.assertFalse(self.action_item.enabled) self.assertFalse(self.action.enabled) group.enabled = True self.assertTrue(self.action_item.enabled) self.assertTrue(self.action.enabled) pyface-7.4.0/pyface/action/tests/__init__.py0000644000076500000240000000000014176222673021731 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/action/tests/test_traitsui_widget_action.py0000644000076500000240000001261614176222673026015 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 from traits.testing.api import UnittestTools from pyface.gui import GUI from pyface.toolkit import toolkit from pyface.util.testing import has_traitsui from pyface.window import Window from ..traitsui_widget_action import TraitsUIWidgetAction @unittest.skipIf(not has_traitsui(), "TraitsUI not installed") @unittest.skipIf(toolkit.toolkit == "wx", "wxPython not supported") class TestTraitsUIWidgetAction(unittest.TestCase, UnittestTools): def setUp(self): self.gui = GUI() self.parent = Window() self.parent._create() self.parent.open() self.addCleanup(self._destroy_parent) self.gui.process_events() def _destroy_parent(self): self.parent.destroy() self.gui.process_events() self.parent = None def create_model(self): from traitsui.api import View, Item class SimpleEnum(HasTraits): value = Enum("a", "b", "c") view = View(Item("value")) return SimpleEnum() def test_traitsui_widget_action(self): from traitsui.api import View, Item class SimpleEnumAction(TraitsUIWidgetAction): value = Enum("a", "b", "c") view = View(Item("value")) action = SimpleEnumAction(name="Simple") control = action.create_control(self.parent.control) self.gui.process_events() editor = control._ui.get_editors("value")[0] with self.assertTraitChanges(action, "value", count=1): if toolkit.toolkit in {"qt", "qt4"}: editor.control.setCurrentIndex(1) editor.control.activated.emit(1) elif toolkit.toolkit == "wx": import wx event = wx.CommandEvent( wx.EVT_CHOICE.typeId, editor.control.GetId() ) event.SetString("b") wx.PostEvent(editor.control.GetEventHandler(), event) else: self.skipTest("Unknown toolkit") self.gui.process_events() self.assertEqual(action.value, "b") def test_traitsui_widget_action_model(self): from traitsui.api import View, Item class SimpleEnumAction(TraitsUIWidgetAction): view = View(Item("value")) model = self.create_model() action = SimpleEnumAction(name="Simple", model=model) control = action.create_control(self.parent.control) self.gui.process_events() editor = control._ui.get_editors("value")[0] with self.assertTraitChanges(model, "value", count=1): if toolkit.toolkit in {"qt", "qt4"}: editor.control.setCurrentIndex(1) editor.control.activated.emit(1) elif toolkit.toolkit == "wx": import wx event = wx.CommandEvent( wx.EVT_CHOICE.typeId, editor.control.GetId() ) event.SetString("b") wx.PostEvent(editor.control.GetEventHandler(), event) else: self.skipTest("Unknown toolkit") self.gui.process_events() self.assertEqual(model.value, "b") def test_traitsui_widget_action_model_view(self): from traitsui.api import HGroup, View, Item class ComplexEnumAction(TraitsUIWidgetAction): value = Enum("a", "b", "c") view = View(HGroup(Item("value"), Item("action.value"))) model = self.create_model() action = ComplexEnumAction(name="Simple", model=model) control = action.create_control(self.parent.control) self.gui.process_events() editor = control._ui.get_editors("value")[0] with self.assertTraitChanges(model, "value", count=1): if toolkit.toolkit in {"qt", "qt4"}: editor.control.setCurrentIndex(1) editor.control.activated.emit(1) elif toolkit.toolkit == "wx": import wx event = wx.CommandEvent( wx.EVT_CHOICE.typeId, editor.control.GetId() ) event.SetString("b") wx.PostEvent(editor.control.GetEventHandler(), event) else: self.skipTest("Unknown toolkit") self.gui.process_events() self.assertEqual(model.value, "b") editor = control._ui.get_editors("value")[1] with self.assertTraitChanges(action, "value", count=1): if toolkit.toolkit in {"qt", "qt4"}: editor.control.setCurrentIndex(2) editor.control.activated.emit(2) elif toolkit.toolkit == "wx": event = wx.CommandEvent( wx.EVT_CHOICE.typeId, editor.control.GetId() ) event.SetString("c") wx.PostEvent(editor.control.GetEventHandler(), event) else: self.skipTest("Unknown toolkit") self.gui.process_events() self.assertEqual(action.value, "c") pyface-7.4.0/pyface/action/tests/test_action_controller.py0000644000076500000240000000357214176222673024772 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 ..action import Action from ..action_controller import ActionController from ..action_event import ActionEvent class TestActionController(unittest.TestCase): def setUp(self): # test whether function is called by updating list # XXX should really use mock self.memo = [] def perform(): self.memo.append("called") self.action = Action(name="Test", on_perform=perform) self.action_controller = ActionController() def test_perform(self): # test whether function is called by updating list # XXX should really use mock event = ActionEvent() self.action_controller.perform(self.action, event) self.assertEqual(self.memo, ["called"]) def test_perform_none(self): action = Action(name="Test") event = ActionEvent() # does nothing, but shouldn't error self.action_controller.perform(action, event) def test_can_add_to_menu(self): result = self.action_controller.can_add_to_menu(self.action) self.assertTrue(result) def test_add_to_menu(self): # does nothing, shouldn't fail self.action_controller.add_to_menu(self.action) def test_can_add_to_toolbar(self): result = self.action_controller.can_add_to_toolbar(self.action) self.assertTrue(result) def test_add_to_toolbar(self): # does nothing, shouldn't fail self.action_controller.add_to_toolbar(self.action) pyface-7.4.0/pyface/action/tests/test_gui_application_action.py0000644000076500000240000000367614176222673025763 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.api import UnittestTools from pyface.gui_application import GUIApplication from ..gui_application_action import GUIApplicationAction from ..action_event import ActionEvent class TestAction(unittest.TestCase, UnittestTools): def setUp(self): self.application = GUIApplication() def test_defaults(self): action = GUIApplicationAction() event = ActionEvent() # does nothing, but shouldn't error action.perform(event) self.assertTrue(action.enabled) self.assertTrue(action.visible) self.assertIsNone(action.object) def test_application(self): action = GUIApplicationAction(application=self.application) event = ActionEvent() # does nothing, but shouldn't error action.perform(event) self.assertTrue(action.enabled) self.assertTrue(action.visible) self.assertEqual(action.object, self.application) def test_application_changed(self): action = GUIApplicationAction() self.assertIsNone(action.object) with self.assertTraitChanges(action, "object", 1): action.application = self.application self.assertEqual(action.object, self.application) with self.assertTraitChanges(action, "object", 1): action.application = None self.assertIsNone(action.object) def test_destroy(self): action = GUIApplicationAction(application=self.application) action.destroy() self.assertEqual(action.object, None) pyface-7.4.0/pyface/action/tests/test_action_item.py0000644000076500000240000001541314176222673023542 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.api import UnittestTools from pyface.image_cache import ImageCache from pyface.toolkit import toolkit_object from pyface.window import Window from ..action import Action from ..action_controller import ActionController from ..action_item import ActionItem from ..menu_manager import MenuManager from ..menu_bar_manager import MenuBarManager from ..tool_bar_manager import ToolBarManager class FalseActionController(ActionController): def can_add_to_menu(self, action): """ Returns True if the action can be added to a menu/menubar. """ return False def can_add_to_toolbar(self, action): """ Returns True if the action can be added to a toolbar. """ return False class TestActionItem(unittest.TestCase, UnittestTools): def setUp(self): # test whether function is called by updating list # XXX should really use mock self.memo = [] def perform(): self.memo.append("called") self.action = Action(name="Test", on_perform=perform) def control_factory(self, parent, action): if toolkit_object.toolkit == "wx": import wx control = wx.Control(parent) elif toolkit_object.toolkit == "qt4": from pyface.qt import QtGui control = QtGui.QWidget(parent) else: control = None return control def test_default_id(self): action_item = ActionItem(action=self.action) self.assertEqual(action_item.id, "Test") def test_enabled_changed(self): # XXX these are only one-way changes, which seems wrong. action_item = ActionItem(action=self.action) with self.assertTraitChanges(self.action, "enabled", count=1): action_item.enabled = False self.assertFalse(self.action.enabled) with self.assertTraitChanges(self.action, "enabled", count=1): action_item.enabled = True self.assertTrue(self.action.enabled) def test_visible_changed(self): # XXX these are only one-way changes, which seems wrong. action_item = ActionItem(action=self.action) with self.assertTraitChanges(self.action, "visible", count=1): action_item.visible = False self.assertFalse(self.action.visible) with self.assertTraitChanges(self.action, "visible", count=1): action_item.visible = True self.assertTrue(self.action.visible) def test_destroy(self): action_item = ActionItem(action=self.action) # XXX test that it calls action.destroy action_item.destroy() def test_add_to_menu(self): window = Window() window.open() action_item = ActionItem(action=self.action) menu_bar_manager = MenuBarManager() menu_manager = MenuManager(name="Test") menu_bar = menu_bar_manager.create_menu_bar(window.control) menu = menu_manager.create_menu(menu_bar) action_item.add_to_menu(window.control, menu, None) window.close() def test_add_to_menu_controller(self): window = Window() window.open() action_item = ActionItem(action=self.action) menu_bar_manager = MenuBarManager() menu_manager = MenuManager(name="Test") menu_bar = menu_bar_manager.create_menu_bar(window.control) menu = menu_manager.create_menu(menu_bar) controller = ActionController() action_item.add_to_menu(window.control, menu, controller) window.close() def test_add_to_menu_controller_false(self): window = Window() window.open() action_item = ActionItem(action=self.action) menu_bar_manager = MenuBarManager() menu_manager = MenuManager(name="Test") menu_bar = menu_bar_manager.create_menu_bar(window.control) menu = menu_manager.create_menu(menu_bar) controller = FalseActionController() action_item.add_to_menu(window.control, menu, controller) window.close() def test_add_to_toolbar(self): window = Window() window.open() action_item = ActionItem(action=self.action) toolbar_manager = ToolBarManager(name="Test") image_cache = ImageCache(height=32, width=32) menu = toolbar_manager.create_tool_bar(window.control) action_item.add_to_toolbar( window.control, menu, image_cache, None, True ) window.close() def test_add_to_toolbar_no_label(self): window = Window() window.open() action_item = ActionItem(action=self.action) toolbar_manager = ToolBarManager(name="Test") image_cache = ImageCache(height=32, width=32) menu = toolbar_manager.create_tool_bar(window.control) action_item.add_to_toolbar( window.control, menu, image_cache, None, False ) window.close() def test_add_to_toolbar_controller(self): window = Window() window.open() action_item = ActionItem(action=self.action) toolbar_manager = ToolBarManager(name="Test") image_cache = ImageCache(height=32, width=32) menu = toolbar_manager.create_tool_bar(window.control) controller = ActionController() action_item.add_to_toolbar( window.control, menu, image_cache, controller, True ) window.close() def test_add_to_toolbar_controller_false(self): window = Window() window.open() action_item = ActionItem(action=self.action) toolbar_manager = ToolBarManager(name="Test") image_cache = ImageCache(height=32, width=32) menu = toolbar_manager.create_tool_bar(window.control) controller = FalseActionController() action_item.add_to_toolbar( window.control, menu, image_cache, controller, True ) window.close() def test_add_to_toolbar_widget(self): self.action.style = "widget" self.action.control_factory = self.control_factory window = Window() window.open() action_item = ActionItem(action=self.action) toolbar_manager = ToolBarManager(name="Test") image_cache = ImageCache(height=32, width=32) menu = toolbar_manager.create_tool_bar(window.control) try: action_item.add_to_toolbar( window.control, menu, image_cache, None, True ) finally: window.close() pyface-7.4.0/pyface/action/tests/test_listening_action.py0000644000076500000240000001600314176222673024574 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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, Bool, HasTraits from traits.testing.api import UnittestTools from ..listening_action import ListeningAction from ..action_event import ActionEvent class WatchedObject(HasTraits): #: Trait to watch for enabled state is_enabled = Bool(True) #: Other trait to watch for enabled state is_also_enabled = Bool(True) #: Trait to watch for visible state is_visible = Bool(True) #: Other trait to watch for visible state is_also_visible = Bool(True) #: Flag that is set when method called was_called = Bool() #: Child object to test dotted lookup child = Any() def callback(self): self.was_called = True class TestListeningAction(unittest.TestCase, UnittestTools): def setUp(self): self.object = WatchedObject() def perform_with_callback(self, action): # test whether function is called by updating list # XXX should really use mock memo = [] def perform(): memo.append("called") action.on_perform = perform event = ActionEvent() action.perform(event) return memo def test_defaults(self): action = ListeningAction() event = ActionEvent() # does nothing, but shouldn't error action.perform(event) self.assertTrue(action.enabled) self.assertTrue(action.visible) def test_perform_no_object(self): action = ListeningAction() memo = self.perform_with_callback(action) self.assertEqual(memo, ["called"]) def test_perform_no_method(self): action = ListeningAction(object=self.object) memo = self.perform_with_callback(action) self.assertFalse(self.object.was_called) self.assertEqual(memo, ["called"]) def test_perform_method(self): action = ListeningAction(object=self.object, method="callback") memo = self.perform_with_callback(action) self.assertTrue(self.object.was_called) self.assertEqual(memo, []) def test_perform_method_missing(self): action = ListeningAction(object=self.object, method="fallback") # does nothing, but shouldn't error memo = self.perform_with_callback(action) self.assertFalse(self.object.was_called) self.assertEqual(memo, []) def test_perform_child_method(self): self.object.child = WatchedObject() action = ListeningAction(object=self.object, method="child.callback") memo = self.perform_with_callback(action) self.assertTrue(self.object.child.was_called) self.assertFalse(self.object.was_called) self.assertEqual(memo, []) def test_perform_missing_child_method(self): action = ListeningAction(object=self.object, method="child.callback") # does nothing, but shouldn't error memo = self.perform_with_callback(action) self.assertFalse(self.object.was_called) self.assertEqual(memo, []) def test_enabled(self): action = ListeningAction(object=self.object, enabled_name="is_enabled") self.assertTrue(action.enabled) with self.assertTraitChanges(action, "enabled", 1): self.object.is_enabled = False self.assertFalse(action.enabled) with self.assertTraitChanges(action, "enabled", 1): self.object.is_enabled = True self.assertTrue(action.enabled) def test_enabled_child(self): self.object.child = WatchedObject() action = ListeningAction( object=self.object, enabled_name="child.is_enabled" ) self.assertTrue(action.enabled) with self.assertTraitChanges(action, "enabled", 1): self.object.child.is_enabled = False self.assertFalse(action.enabled) with self.assertTraitChanges(action, "enabled", 1): self.object.child.is_enabled = True self.assertTrue(action.enabled) def test_enabled_missing_child(self): action = ListeningAction( object=self.object, enabled_name="child.is_enabled" ) self.assertFalse(action.enabled) with self.assertTraitChanges(action, "enabled", 1): self.object.child = WatchedObject() self.assertTrue(action.enabled) with self.assertTraitChanges(action, "enabled", 1): self.object.child = None self.assertFalse(action.enabled) def test_enabled_name_change(self): self.object.is_also_enabled = False action = ListeningAction(object=self.object, enabled_name="is_enabled") self.assertTrue(action.enabled) with self.assertTraitChanges(action, "enabled", 1): action.enabled_name = "is_also_enabled" self.assertFalse(action.enabled) def test_visible(self): action = ListeningAction(object=self.object, visible_name="is_visible") self.assertTrue(action.visible) with self.assertTraitChanges(action, "visible", 1): self.object.is_visible = False self.assertFalse(action.visible) with self.assertTraitChanges(action, "visible", 1): self.object.is_visible = True self.assertTrue(action.visible) def test_visible_child(self): self.object.child = WatchedObject() action = ListeningAction( object=self.object, visible_name="child.is_visible" ) self.assertTrue(action.visible) with self.assertTraitChanges(action, "visible", 1): self.object.child.is_visible = False self.assertFalse(action.visible) with self.assertTraitChanges(action, "visible", 1): self.object.child.is_visible = True self.assertTrue(action.visible) def test_visible_missing_child(self): action = ListeningAction( object=self.object, visible_name="child.is_visible" ) self.assertFalse(action.visible) with self.assertTraitChanges(action, "visible", 1): self.object.child = WatchedObject() self.assertTrue(action.visible) with self.assertTraitChanges(action, "visible", 1): self.object.child = None self.assertFalse(action.visible) def test_visible_name_change(self): self.object.is_also_visible = False action = ListeningAction(object=self.object, visible_name="is_visible") self.assertTrue(action.visible) with self.assertTraitChanges(action, "visible", 1): action.visible_name = "is_also_visible" self.assertFalse(action.visible) def test_destroy(self): action = ListeningAction(object=self.object) action.destroy() pyface-7.4.0/pyface/action/tests/test_action_manager.py0000644000076500000240000002312014176222673024210 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.api import UnittestTools from ..action import Action from ..action_item import ActionItem from ..action_manager import ActionManager from ..group import Group class TestActionItem(unittest.TestCase, UnittestTools): def setUp(self): # test whether function is called by updating list # XXX should really use mock self.memo = [] def perform(): self.memo.append("called") self.perform = perform self.action = Action(name="Test", on_perform=perform) self.action_item = ActionItem(action=self.action) self.group = Group(id="test") def test_init_group(self): action_manager = ActionManager(self.group) default_group = action_manager._get_default_group() self.assertEqual(action_manager.groups, [self.group, default_group]) self.assertEqual(self.group.parent, action_manager) def test_init_string(self): action_manager = ActionManager("Test") default_group = action_manager._get_default_group() self.assertEqual(len(action_manager.groups), 2) self.assertEqual(action_manager.groups[0].id, "Test") self.assertEqual(action_manager.groups[-1], default_group) self.assertEqual(action_manager.groups[0].parent, action_manager) def test_init_action_item(self): action_manager = ActionManager(self.action_item) default_group = action_manager._get_default_group() self.assertEqual(action_manager.groups, [default_group]) self.assertEqual(default_group.items, [self.action_item]) self.assertEqual(self.action_item.parent, default_group) def test_init_group_action_item(self): action_manager = ActionManager(self.group, self.action_item) default_group = action_manager._get_default_group() self.assertEqual(action_manager.groups, [self.group, default_group]) self.assertEqual(self.group.items, [self.action_item]) self.assertEqual(self.action_item.parent, self.group) def test_init_action(self): action_manager = ActionManager(self.action) default_group = action_manager._get_default_group() self.assertEqual(action_manager.groups, [default_group]) self.assertEqual(default_group.items[0].action, self.action) self.assertEqual(default_group.items[0].parent, default_group) def test_init_nothing(self): action_manager = ActionManager() default_group = action_manager._get_default_group() self.assertEqual(action_manager.groups, [default_group]) self.assertEqual(len(default_group.items), 0) def test_append(self): action_manager = ActionManager() action_manager.append(self.group) default_group = action_manager._get_default_group() self.assertEqual(action_manager.groups, [default_group, self.group]) self.assertEqual(self.group.parent, action_manager) def test_append_2(self): action_manager = ActionManager(self.group) group2 = Group() action_manager.append(group2) default_group = action_manager._get_default_group() self.assertEqual( action_manager.groups, [self.group, default_group, group2] ) self.assertEqual(group2.parent, action_manager) def test_append_string(self): action_manager = ActionManager(self.group) action_manager.append("Test string") self.assertEqual(len(action_manager.groups), 3) self.assertEqual(action_manager.groups[2].id, "Test string") def test_append_item(self): action_manager = ActionManager() action_manager.append(self.action_item) default_group = action_manager._get_default_group() self.assertEqual(action_manager.groups, [default_group]) self.assertEqual(default_group.items, [self.action_item]) def test_append_item_2(self): action_manager = ActionManager() action_manager.append(self.group) action_manager.append(self.action_item) default_group = action_manager._get_default_group() self.assertEqual(action_manager.groups, [default_group, self.group]) self.assertEqual(default_group.items, [self.action_item]) def test_append_item_order(self): # Regression test for enthought/pyface#289 expected = [ self.action_item, ActionItem(action=Action(name="Test2")), ActionItem(action=Action(name="Test3")), ] action_manager = ActionManager() for item in expected: action_manager.append(item) default_group = action_manager._get_default_group() self.assertEqual(default_group.items, expected) def test_destroy(self): action_manager = ActionManager(self.group) # XXX items doesn't fire a change event. Should it? # XXX should mock group to ensure that destroy is called action_manager.destroy() def test_insert(self): action_manager = ActionManager() action_manager.insert(0, self.group) default_group = action_manager._get_default_group() self.assertEqual(action_manager.groups, [self.group, default_group]) self.assertEqual(self.group.parent, action_manager) def test_insert_2(self): action_manager = ActionManager(self.group) group2 = Group() action_manager.insert(1, group2) default_group = action_manager._get_default_group() self.assertEqual( action_manager.groups, [self.group, group2, default_group] ) self.assertEqual(group2.parent, action_manager) def test_insert_string(self): action_manager = ActionManager(self.group) action_manager.insert(0, "Test string") self.assertEqual(len(action_manager.groups), 3) self.assertEqual(action_manager.groups[0].id, "Test string") def test_insert_item(self): action_manager = ActionManager() action_manager.insert(0, self.action_item) default_group = action_manager._get_default_group() self.assertEqual(action_manager.groups, [default_group]) self.assertEqual(default_group.items, [self.action_item]) def test_insert_item_2(self): action_manager = ActionManager() action_manager.append(self.group) action_manager.insert(0, self.action_item) default_group = action_manager._get_default_group() self.assertEqual(action_manager.groups, [default_group, self.group]) self.assertEqual(default_group.items, [self.action_item]) def test_find_group(self): action_manager = ActionManager(self.group) group = action_manager.find_group("test") self.assertEqual(group, self.group) def test_find_group_missing(self): action_manager = ActionManager(self.group) group = action_manager.find_group("not here") self.assertIsNone(group) def test_find_item(self): self.group.append(self.action_item) action_manager = ActionManager(self.group) item = action_manager.find_item("Test") self.assertEqual(item, self.action_item) def test_find_item_missing(self): self.group.append(self.action_item) action_manager = ActionManager(self.group) item = action_manager.find_item("Not here") self.assertIsNone(item) def test_find_item_hierarchy(self): action_manager = ActionManager(self.group) action_manager_2 = ActionManager(self.action_item, id="test2") self.group.append(action_manager_2) item = action_manager.find_item("test2/Test") self.assertEqual(item, self.action_item) def test_walk_hierarchy(self): action_manager = ActionManager(self.group) action_manager_2 = ActionManager(self.action_item, id="test2") self.group.append(action_manager_2) result = [] action_manager.walk(result.append) self.assertEqual( result, [ action_manager, self.group, action_manager_2, action_manager_2._get_default_group(), self.action_item, action_manager._get_default_group(), ], ) def test_enabled_changed(self): self.group.append(self.action_item) action_manager = ActionManager(self.group) action_manager.enabled = False self.assertFalse(self.group.enabled) self.assertFalse(self.action_item.enabled) self.assertFalse(self.action.enabled) action_manager.enabled = True self.assertTrue(self.group.enabled) self.assertTrue(self.action_item.enabled) self.assertTrue(self.action.enabled) def test_visible_changed(self): self.group.append(self.action_item) action_manager = ActionManager(self.group) action_manager.visible = False self.assertFalse(self.group.visible) # XXX group doesn't make items invisible # self.assertFalse(self.action_item.enabled) # self.assertFalse(self.action.enabled) action_manager.visible = True self.assertTrue(self.group.visible) # XXX group doesn't make items visible # self.assertTrue(self.action_item.enabled) # self.assertTrue(self.action.enabled) pyface-7.4.0/pyface/action/tests/test_field_action.py0000644000076500000240000000607614176222673023674 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.fields.api import ComboField, SpinField, TextField from pyface.gui import GUI from pyface.window import Window from ..field_action import FieldAction class TestFieldAction(unittest.TestCase): def setUp(self): self.gui = GUI() self.parent = Window() self.parent._create() self.addCleanup(self._destroy_parent) def _destroy_parent(self): self.parent.destroy() self.parent = None def test_combo_field_action(self): # test whether function is called by updating list # XXX should really use mock memo = [] def perform(value): memo.append(value) action = FieldAction( name="Dummy", field_type=ComboField, field_defaults={ "values": ["a", "b", "c"], "value": "a", "tooltip": "Dummy", }, on_perform=perform, ) control = action.create_control(self.parent.control) try: self.gui.process_events() control._field.value = "b" self.gui.process_events() self.assertEqual(memo, ["b"]) finally: control._field.destroy() def test_text_field_action(self): # test whether function is called by updating list # XXX should really use mock memo = [] def perform(value): memo.append(value) action = FieldAction( name="Dummy", field_type=TextField, field_defaults={"value": "a", "tooltip": "Dummy"}, on_perform=perform, ) control = action.create_control(self.parent.control) try: self.gui.process_events() control._field.value = "b" self.gui.process_events() self.assertEqual(memo, ["b"]) finally: control._field.destroy() def test_spin_field_action(self): # test whether function is called by updating list # XXX should really use mock memo = [] def perform(value): memo.append(value) action = FieldAction( name="Dummy", field_type=SpinField, field_defaults={ "value": 1, "bounds": (0, 100), "tooltip": "Dummy", }, on_perform=perform, ) control = action.create_control(self.parent.control) try: self.gui.process_events() control._field.value = 5 self.gui.process_events() self.assertEqual(memo, [5]) finally: control._field.destroy() pyface-7.4.0/pyface/action/tests/test_action_event.py0000644000076500000240000000131714176222673023723 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 import unittest from ..action_event import ActionEvent class TestActionEvent(unittest.TestCase): def test_init(self): t0 = time.time() event = ActionEvent() t1 = time.time() self.assertGreaterEqual(event.when, t0) self.assertLessEqual(event.when, t1) pyface-7.4.0/pyface/action/tests/test_action.py0000644000076500000240000000354314176222673022525 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 ..action import Action from ..action_event import ActionEvent class TestAction(unittest.TestCase): def test_default_id(self): action = Action(name="Test") self.assertEqual(action.id, "Test") def test_id(self): action = Action(name="Test", id="test") self.assertEqual(action.id, "test") def test_perform(self): # test whether function is called by updating list # XXX should really use mock memo = [] def perform(): memo.append("called") action = Action(name="Test", on_perform=perform) event = ActionEvent() action.perform(event) self.assertEqual(memo, ["called"]) def test_perform_none(self): action = Action(name="Test") event = ActionEvent() # does nothing, but shouldn't error action.perform(event) def test_destroy(self): action = Action(name="Test") # does nothing, but shouldn't error action.destroy() def test_widget_action(self): # test whether function is called by updating list # XXX should really use mock memo = [] def control_factory(parent, action): memo.append((parent, action)) action = Action( name="Dummy", style="widget", control_factory=control_factory ) parent = None action.create_control(parent) self.assertEqual(memo, [(parent, action)]) pyface-7.4.0/pyface/action/status_bar_manager.py0000644000076500000240000000126614176222673022710 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 toolkit-specific status bar manager that realizes itself in a status bar control. - :attr:`~.StatusBarManager` """ # Import the toolkit specific version. from pyface.toolkit import toolkit_object StatusBarManager = toolkit_object("action.status_bar_manager:StatusBarManager") pyface-7.4.0/pyface/action/action_manager.py0000644000076500000240000002717314176222673022023 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Abstract base class for all action managers. """ from traits.api import ( Bool, Constant, Event, HasTraits, Instance, List, observe, Property, Str ) from pyface.action.action_controller import ActionController from pyface.action.group import Group class ActionManager(HasTraits): """ Abstract base class for all action managers. An action manager contains a list of groups, with each group containing a list of items. There are currently three concrete sub-classes: 1) 'MenuBarManager' 2) 'MenuManager' 3) 'ToolBarManager' """ # 'ActionManager' interface -------------------------------------------- #: The Id of the default group. DEFAULT_GROUP = Constant("additions") #: The action controller (if any) used to control how actions are performed. controller = Instance(ActionController) #: Is the action manager enabled? enabled = Bool(True) #: All of the contribution groups in the manager. groups = Property(List(Group)) #: The manager's unique identifier (if it has one). id = Str() #: Is the action manager visible? visible = Bool(True) # Events ---- #: fixme: We probably need more granular events than this! changed = Event() # Private interface ---------------------------------------------------- #: All of the contribution groups in the manager. _groups = List(Group) # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, *args, **traits): """ Creates a new action manager. Parameters ---------- args : collection of strings, Group instances, or ActionManagerItem instances Positional arguments are interpreted as Items or Groups managed by the action manager. Notes ----- If a Group is passed as a positional agrument then it is added to the manager and any subsequent Items arguments are appended to the Group until another Group is encountered. If a string is passed, a Group is created with id set to the string. """ # Base class constructor. super().__init__(**traits) # The last group in every manager is the group with Id 'additions'. # # fixme: The side-effect of this is to ensure that the 'additions' # group has been created. Is the 'additions' group even a good idea? group = self._get_default_group() # Add all items to the manager. for arg in args: # We allow a group to be defined by simply specifying a string (its # Id). if isinstance(arg, str): # Create a group with the specified Id. arg = Group(id=arg) # If the item is a group then add it just before the default group # (ie. we always keep the default group as the last group in the # manager). if isinstance(arg, Group): self.insert(-1, arg) group = arg # Otherwise, the item is an action manager item so add it to the # current group. else: group.append(arg) # ------------------------------------------------------------------------ # 'ActionManager' interface. # ------------------------------------------------------------------------ # Trait properties ----------------------------------------------------- def _get_groups(self): return self._groups[:] # Trait change handlers ------------------------------------------------ @observe('enabled') def _enabled_updated(self, event): for group in self._groups: group.enabled = event.new @observe('visible') def _visible_updated(self, event): for group in self._groups: group.visible = event.new # Methods -------------------------------------------------------------# def append(self, item): """ Append an item to the manager. Parameters ---------- item : string, Group instance or ActionManagerItem instance The item to append. Notes ----- If the item is a group, the Group is appended to the manager's list of groups. It the item is a string, then a group is created with the string as the ``id`` and the new group is appended to the list of groups. If the item is an ActionManagerItem then the item is appended to the manager's default group. """ item = self._prepare_item(item) if isinstance(item, Group): group = self._groups else: group = self._get_default_group() group.append(item) return group def destroy(self): """ Called when the manager is no longer required. By default this method simply calls 'destroy' on all of the manager's groups. """ for group in self.groups: group.destroy() def insert(self, index, item): """ Insert an item into the manager at the specified index. Parameters ---------- index : int The position at which to insert the object item : string, Group instance or ActionManagerItem instance The item to insert. Notes ----- If the item is a group, the Group is inserted into the manager's list of groups. It the item is a string, then a group is created with the string as the ``id`` and the new group is inserted into the list of groups. If the item is an ActionManagerItem then the item is inserted into the manager's defualt group. """ item = self._prepare_item(item) if isinstance(item, Group): group = self._groups else: group = self._get_default_group() group.insert(index, item) return group def find_group(self, id): """ Find a group with a specified Id. Parameters ---------- id : str The id of the group to find. Returns ------- group : Group instance The group which matches the id, or None if no such group exists. """ for group in self._groups: if group.id == id: return group else: return None def find_item(self, path): """ Find an item using a path. Parameters ---------- path : str A '/' separated list of contribution Ids. Returns ------- item : ActionManagerItem or None Returns the matching ActionManagerItem, or None if any component of the path is not found. """ components = path.split("/") # If there is only one component, then the path is just an Id so look # it up in this manager. if len(components) > 0: item = self._find_item(components[0]) if len(components) > 1 and item is not None: item = item.find_item("/".join(components[1:])) else: item = None return item def walk(self, fn): """ Walk the manager applying a function at every item. The components are walked in pre-order. Parameters ---------- fn : callable A callable to apply to the tree of groups and items, starting with the manager. """ fn(self) for group in self._groups: self.walk_group(group, fn) def walk_group(self, group, fn): """ Walk a group applying a function at every item. The components are walked in pre-order. Parameters ---------- fn : callable A callable to apply to the tree of groups and items. """ fn(group) for item in group.items: if isinstance(item, Group): self.walk_group(item, fn) else: self.walk_item(item, fn) def walk_item(self, item, fn): """ Walk an item (may be a sub-menu manager remember!). The components are walked in pre-order. Parameters ---------- fn : callable A callable to apply to the tree of items and subgroups. """ if hasattr(item, "groups"): item.walk(fn) else: fn(item) # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _get_default_group(self): """ Returns the manager's default group. This will create this group if it doesn't already exist. Returns ------- group : Group instance The manager's default group. """ group = self.find_group(self.DEFAULT_GROUP) if group is None: group = self._prepare_item(self.DEFAULT_GROUP) self._groups.append(group) return group def _prepare_item(self, item): """ Prepare an item to be added to this ActionManager. Parameters ---------- item : string, Group instance or ActionManagerItem instance The item to be added to this ActionManager Returns ------- item : Group or ActionManagerItem Modified item """ # 1) The item is a 'Group' instance. if isinstance(item, Group): item.parent = self # 2) The item is a string. elif isinstance(item, str): # Create a group with that Id. item = Group(id=item) item.parent = self return item def _find_item(self, id): """ Find an item with a spcified Id. Parameters ---------- id : str The id of the item to be found. Returns ------- item : ActionManagerItem or None Returns the item with the specified Id, or None if no such item exists. """ for group in self.groups: item = group.find(id) if item is not None: return item else: return None # ------------------------------------------------------------------------ # Debugging interface. # ------------------------------------------------------------------------ def dump(self, indent=""): """ Render a manager! """ print(indent, "Manager", self.id) indent += " " for group in self._groups: self.render_group(group, indent) def render_group(self, group, indent=""): """ Render a group! """ print(indent, "Group", group.id) indent += " " for item in group.items: if isinstance(item, Group): print("Surely, a group cannot contain another group!!!!") self.render_group(item, indent) else: self.render_item(item, indent) def render_item(self, item, indent=""): """ Render an item! """ if hasattr(item, "groups"): item.dump(indent) else: print(indent, "Item", item.id) pyface-7.4.0/pyface/action/__init__.py0000644000076500000240000000062714176222673020606 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! pyface-7.4.0/pyface/action/action.py0000644000076500000240000001052014176222673020315 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 base class for all actions. """ from functools import partial from traits.api import Bool, Callable, Enum, HasTraits, Str from pyface.ui_traits import Image class Action(HasTraits): """ The base class for all actions. An action is the non-UI side of a command which can be triggered by the end user. Actions are typically associated with buttons, menu items and tool bar tools. When the user triggers the command via the UI, the action's 'perform' method is invoked to do the actual work. """ # 'Action' interface --------------------------------------------------- #: Keyboard accelerator (by default the action has NO accelerator). accelerator = Str() #: Is the action checked? This is only relevant if the action style is #: 'radio' or 'toggle'. checked = Bool(False) #: A longer description of the action (used for context sensitive help etc). #: If no description is specified, the tooltip is used instead (and if there #: is no tooltip, then well, maybe you just hate your users ;^). description = Str() #: Is the action enabled? enabled = Bool(True) #: Is the action visible? visible = Bool(True) #: The action's unique identifier (may be None). id = Str() #: The action's image (displayed on tool bar tools etc). image = Image #: The action's name (displayed on menus/tool bar tools etc). name = Str() #: An (optional) callable that will be invoked when the action is performed. on_perform = Callable #: The action's style. style = Enum("push", "radio", "toggle", "widget") #: A short description of the action used for tooltip text etc. tooltip = Str() #: An (optional) callable to create the toolkit control for widget style. control_factory = Callable # ------------------------------------------------------------------------ # 'Action' interface. # ------------------------------------------------------------------------ # Initializers --------------------------------------------------------- def _id_default(self): """ Initializes the 'id' trait. The default is the ``name`` trait. """ return self.name # Methods -------------------------------------------------------------# def create_control(self, parent): """ Called when creating a "widget" style action. By default this will call whatever callable is supplied via the 'control_factory' trait which is a callable that should take the parent control and the action as arguments and return an appropriate toolkit control. Some operating systems (Mac OS in particular) may limit what widgets can be displayed in menus. This method is only used when the 'style' is "widget" and is ignored by other styles. Parameters ---------- parent : toolkit control The toolkit control, usually a toolbar. Returns ------- control : toolkit control A toolkit control or None. """ if self.style == "widget" and self.control_factory is not None: return self.control_factory(parent, self) return None def destroy(self): """ Called when the action is no longer required. By default this method does nothing, but this would be a great place to unhook trait listeners etc. """ def perform(self, event): """ Performs the action. Parameters ---------- event : ActionEvent instance The event which triggered the action. """ if self.on_perform is not None: self.on_perform() @classmethod def factory(cls, *args, **kwargs): """ Create a factory for an action with the given arguments. This is particularly useful for passing context to Tasks schema additions. """ return partial(cls, *args, **kwargs) pyface-7.4.0/pyface/action/api.py0000644000076500000240000000472014176222673017616 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 ``pyface.action`` subpackage. Actions ------- - :class:`~.Action` - :class:`~.FieldAction` - :class:`~.GUIApplicationAction` - :class:`~.ListeningAction` - :class:`~.TraitsUIWidgetAction` - :class:`~.WindowAction` Action Controller ----------------- - :class:`~pyface.action.action_controller.ActionController` Action Event ------------ - :class:`~.ActionEvent` Action Managers --------------- - :class:`~.ActionManager` - ``MenuManager`` - ``MenuBarManager`` - ``StatusBarManager`` - ``ToolBarManager`` - ``ToolPaletteManager``, only for the Wx toolkit. Action Manager Items -------------------- - :class:`~.ActionManagerItem` - :class:`~.ActionItem` Layout support -------------- - :class:`~.Group` - :class:`~.Separator` Useful Application and Window actions ------------------------------------- - :class:`~.AboutAction` - :class:`~.CloseActiveWindowAction` - :class:`~.CreateWindowAction` - :class:`~.ExitAction` - :class:`~.CloseWindowAction` """ from .action import Action from .action_controller import ActionController from .action_event import ActionEvent from .action_item import ActionItem from .action_manager import ActionManager from .action_manager_item import ActionManagerItem from .field_action import FieldAction from .group import Group, Separator from .gui_application_action import ( AboutAction, CloseActiveWindowAction, CreateWindowAction, ExitAction, GUIApplicationAction, ) from .listening_action import ListeningAction from .menu_manager import MenuManager from .menu_bar_manager import MenuBarManager from .status_bar_manager import StatusBarManager from .tool_bar_manager import ToolBarManager from .traitsui_widget_action import TraitsUIWidgetAction from .window_action import CloseWindowAction, WindowAction # This part of the module handles widgets that are still wx specific. This # will all be removed when everything has been ported to PyQt and pyface # becomes toolkit agnostic. from traits.etsconfig.api import ETSConfig if ETSConfig.toolkit == "wx": from .tool_palette_manager import ToolPaletteManager pyface-7.4.0/pyface/action/schema/0000755000076500000240000000000014176460550017726 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/action/schema/_topological_sort.py0000644000076500000240000000713214176222673024027 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 collections import OrderedDict, defaultdict import logging # Logging. logger = logging.getLogger(__name__) def before_after_sort(items): """ Sort a sequence of items with 'before', 'after', and 'id' attributes. The sort is topological. If an item does not specify a 'before' or 'after', it is placed after the preceding item. If a cycle is found in the dependencies, a warning is logged and the order of the items is undefined. """ # Handle a degenerate case for which the logic below will fail (because # prev_item will not be set). if len(items) < 2: return items # Build a set of pairs representing the graph. item_map = dict((item.id, item) for item in items if item.id) pairs = [] prev_item = None for item in items: # Attempt to use 'before' and 'after' to make pairs. new_pairs = [] if hasattr(item, "before") and item.before: parent, child = item, item_map.get(item.before) if child: new_pairs.append((parent, child)) if hasattr(item, "after") and item.after: parent, child = item_map.get(item.after), item if parent: new_pairs.append((parent, child)) # If we have any pairs, use them. Otherwise, use the previous unmatched # item as a parent, if possible. if new_pairs: pairs.extend(new_pairs) else: if prev_item: pairs.append((prev_item, item)) prev_item = item # Now perform the actual sort. result, has_cycle = topological_sort(pairs) if has_cycle: logger.warning("Cycle in before/after sort for items %r", items) return result def topological_sort(pairs): """ Topologically sort a list of (parent, child) pairs. Returns a tuple containing the list of elements sorted in dependency order (parent to child order), if possible, and a boolean indicating whether the graph contains cycles. A simple algorithm due to Kahn, in which vertices are chosen from the graph in the same order as the eventual topological sort, is used. Note that this implementation is stable in the following sense: if we have the input list [..., (parent, child1), ..., (parent, child2), ...], then child1 will be before child2 in the output list (if there there is no additional dependency forcing another ordering). """ # Represent the graph in dictionary form. graph = OrderedDict() num_parents = defaultdict(int) for parent, child in pairs: graph.setdefault(parent, []).append(child) num_parents[child] += 1 # Begin with the parent-less items. result = [item for item in graph if num_parents[item] == 0] # Descend through graph, removing parents as we go. for parent in result: if parent in graph: for child in graph[parent]: num_parents[child] -= 1 if num_parents[child] == 0: result.append(child) del graph[parent] # If there's a cycle, just throw in whatever is left over. has_cycle = bool(graph) if has_cycle: result.extend(list(graph.keys())) return result, has_cycle pyface-7.4.0/pyface/action/schema/action_manager_builder.py0000644000076500000240000002473114176222673024766 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Base action manager builder class This module provides a base class that takes a schema for an action manager and builds the concrete action manager and its groups and items, folding in schema additions. """ import logging from collections import defaultdict from traits.api import HasTraits, Instance, List from .schema import Schema, ToolBarSchema from .schema_addition import SchemaAddition from ._topological_sort import before_after_sort # Logging. logger = logging.getLogger(__name__) class ActionManagerBuilder(HasTraits): """ Builds action managers from schemas, merging schema additions. """ #: An ActionController to use with the action managers created by the #: builder. May be None. controller = Instance("pyface.action.action_controller.ActionController") #: Schema additions to apply to all managers built by this builder. #: Any additions which do not match part of the supplied schema will be #: ignored. additions = List(Instance(SchemaAddition)) # ------------------------------------------------------------------------ # 'ActionManagerBuilder' interface. # ------------------------------------------------------------------------ def create_action_manager(self, schema): """ Create a manager for the given schema using the additions. Any additions whose paths do not match the supplied Parameters ---------- schema : Schema An Schema for an ActionManager subclass (ie. one of MenuBarSchema, MenuSchema, or ToolBarSchema). Returns ------- manager : ActionManager The concrete ActionManager instance corresponding to the Schema with addtions. This does not yet have concrete toolkit widgets associated with it: usually those will be created separately. """ additions_map = defaultdict(list) for addition in self.additions: if addition.path: additions_map[addition.path].append(addition) manager = self._create_action_manager_recurse(schema, additions_map) manager.controller = self.controller return manager def get_additional_toolbar_schemas(self): """ Get any top-level toolbars from additions. Unlike menus, there is no base toolbar manager, so additions which contribute new toolbars appear with no path. It is up to the class using the builder how it wants to handle these additional toolbars. Returns ------- schemas : list of ToolBarSchema The additional toolbars specified in self.additions. """ schemas = [] for addition in self.additions: if not addition.path: schema = addition.factory() if isinstance(schema, ToolBarSchema): schemas.append(schema) else: logger.error( "Invalid top-level schema addition: %r. Only " "ToolBar schemas can be path-less.", schema, ) return schemas def prepare_item(self, item, path): """ Called immediately after a concrete Pyface item has been created (or, in the case of items that are not produced from schemas, immediately before they are processed). This hook can be used to perform last-minute transformations or configuration. Returns a concrete Pyface item. Parameters ---------- item : pyface.action item A concrete Pyface item. path : str The path to the item in the Schema. Returns ------- item : pyface.action item A modified or transformed Pyface item. """ return item # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _get_ordered_schemas(self, schemas): begin = [] middle = [] end = [] for schema in schemas: absolute_position = getattr(schema, "absolute_position", None) if absolute_position is None: middle.append(schema) elif absolute_position == "last": end.append(schema) else: begin.append(schema) schemas = ( before_after_sort(begin) + before_after_sort(middle) + before_after_sort(end) ) return schemas def _group_items_by_id(self, items): """ Group a list of action items by their ID. Action items are Schemas and Groups, MenuManagers, etc. Return a dictionary {item_id: list_of_items}, and a list containing all the ids ordered by their appearance in the `all_items` list. The ordered IDs are used as a replacement for an ordered dictionary, to keep compatibility with Python <2.7 . """ ordered_items_ids = [] id_to_items = defaultdict(list) for item in items: if item.id not in id_to_items: ordered_items_ids.append(item.id) id_to_items[item.id].append(item) return id_to_items, ordered_items_ids def _group_items_by_class(self, items): """ Group a list of action items by their class. Action items are Schemas and Groups, MenuManagers, etc. Return a dictionary {item_class: list_of_items}, and a list containing all the classes ordered by their appearance in the `all_items` list. The ordered classes are used as a replacement for an ordered dictionary, to keep compatibility with Python <2.7 . """ ordered_items_class = [] class_to_items = defaultdict(list) for item in items: if item.__class__ not in class_to_items: ordered_items_class.append(item.__class__) class_to_items[item.__class__].append(item) return class_to_items, ordered_items_class def _unpack_schema_additions(self, items): """ Unpack additions, since they may themselves be schemas. """ unpacked_items = [] for item in items: if isinstance(item, SchemaAddition): unpacked_items.append(item.factory()) else: unpacked_items.append(item) return unpacked_items def _merge_items_with_same_path(self, id_to_items, ordered_items_ids): """ Merge items with the same path if possible. Items must be subclasses of `Schema` and they must be instances of the same class to be merged. """ merged_items = [] for item_id in ordered_items_ids: items_with_same_id = id_to_items[item_id] # Group items by class. class_to_items, ordered_items_class = self._group_items_by_class( items_with_same_id ) for items_class in ordered_items_class: items_with_same_class = class_to_items[items_class] if len(items_with_same_class) == 1: merged_items.extend(items_with_same_class) else: # Only schemas can be merged. if issubclass(items_class, Schema): # Merge into a single schema. items_content = sum( (item.items for item in items_with_same_class), [] ) merged_item = items_with_same_class[0].clone_traits() merged_item.items = items_content merged_items.append(merged_item) else: merged_items.extend(items_with_same_class) return merged_items def _preprocess_schemas(self, schema, additions, path): """ Sort and merge a schema and a set of schema additions. """ # Determine the order of the items at this path. if additions[path]: all_items = self._get_ordered_schemas( schema.items + additions[path] ) else: all_items = schema.items unpacked_items = self._unpack_schema_additions(all_items) id_to_items, ordered_items_ids = self._group_items_by_id( unpacked_items ) merged_items = self._merge_items_with_same_path( id_to_items, ordered_items_ids ) return merged_items def _create_action_manager_recurse(self, schema, additions, path=""): """ Recursively create a manager for the given schema and additions map. Items with the same path are merged together in a single entry if possible (i.e., if they have the same class). When a list of items is merged, their children are added to a clone of the first item in the list. As a consequence, traits like menu names etc. are inherited from the first item. """ from pyface.action.action_manager import ActionManager # Compute the new action path. if path: path = path + "/" + schema.id else: path = schema.id preprocessed_items = self._preprocess_schemas(schema, additions, path) # Create the actual children by calling factory items. children = [] for item in preprocessed_items: if isinstance(item, Schema): item = self._create_action_manager_recurse( item, additions, path ) else: item = self.prepare_item(item, path + "/" + item.id) if isinstance(item, ActionManager): # Give even non-root action managers a reference to the # controller so that custom Groups, MenuManagers, etc. can get # access to the state it holds. item.controller = self.controller children.append(item) # Finally, create the pyface.action instance for this schema. return self.prepare_item(schema.create(children), path) pyface-7.4.0/pyface/action/schema/schema_addition.py0000644000076500000240000000511114176222673023413 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 Callable, HasTraits, Str, Enum # NOTE # This module should never import directly or indirectly any toolkit-dependent # code. This permits it to be used declaratively in a toolkit-agnostic way # if needed. class SchemaAddition(HasTraits): """ An addition to an existing menu bar or tool bar schema. """ #: The schema addition's identifier. This optional, but if left #: unspecified, other schema additions will be unable to refer to this one. id = Str() #: A callable to create the item. Should have the following signature: #: callable() -> Action | ActionItem | Group | MenuManager | #: GroupSchema | MenuSchema #: #: If the result is a schema, it will itself admit of extension by other #: additions. If not, the result will be fixed. factory = Callable #: A forward-slash-separated path through the action hierarchy to the menu #: to add the action, group or menu to. For example: #: - To add an item to the menu bar: ``path = "MenuBar"`` #: - To add an item to the tool bar: ``path = "ToolBar"`` #: - To add an item to a sub-menu: ``path = "MenuBar/File/New"`` path = Str() #: The item appears after the item with this ID. #: - for groups, this is the ID of another group. #: - for menus and actions, this is the ID of another menu or action. after = Str() #: The action appears before the item with this ID. #: - for groups, this is the ID of another group. #: - for menus and actions, this is the ID of another menu or action. before = Str() #: The action appears at the absolute specified position first or last. #: This is useful for example to keep the File menu the first menu in a #: menubar, the help menu the last etc. #: If multiple actions in a schema have absolute_position 'first', they #: will appear in the same order specified; unless 'before' and 'after' #: traits are set to sort these multiple items. #: This trait takes precedence over 'after' and 'before', and values of #: those traits that are not compatible with the absolute_position are #: ignored. absolute_position = Enum(None, "first", "last") pyface-7.4.0/pyface/action/schema/tests/0000755000076500000240000000000014176460550021070 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/action/schema/tests/__init__.py0000644000076500000240000000000014176222673023171 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/action/schema/tests/test_topological_sort.py0000644000076500000240000000570614176222673026076 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 pyface.action.schema._topological_sort import ( before_after_sort, topological_sort, ) class TestItem(HasTraits): id = Int() before = Int() after = Int() def __init__(self, id, **traits): super().__init__(id=id, **traits) def __hash__(self): return hash(self.id) def __eq__(self, other): return self.id == other.id def __repr__(self): return repr(self.id) class TopologicalSortTestCase(unittest.TestCase): def test_before_after_sort_1(self): """ Does the before-after sort work? """ items = [ TestItem(1), TestItem(2), TestItem(3, before=2), TestItem(4, after=1), TestItem(5), ] actual = before_after_sort(items) desired = [ TestItem(1), TestItem(3), TestItem(4), TestItem(2), TestItem(5), ] self.assertEqual(actual, desired) def test_before_after_sort_2(self): """ Does the before-after sort work when both 'before' and 'after' are set? """ items = [ TestItem(1), TestItem(2), TestItem(3), TestItem(4, after=2, before=3), ] actual = before_after_sort(items) desired = [TestItem(1), TestItem(2), TestItem(4), TestItem(3)] self.assertEqual(actual, desired) def test_before_after_sort_3(self): """ Does the degenerate case for the before-after sort work? """ actual = before_after_sort([TestItem(1)]) desired = [TestItem(1)] self.assertEqual(actual, desired) def test_topological_sort_1(self): """ Does a basic topological sort work? """ pairs = [(1, 2), (3, 5), (4, 6), (1, 3), (1, 4), (1, 6), (2, 4)] result, has_cycles = topological_sort(pairs) self.assertTrue(not has_cycles) self.assertEqual(result, [1, 2, 3, 4, 5, 6]) def test_topological_sort_2(self): """ Does another basic topological sort work? """ pairs = [(1, 2), (1, 3), (2, 4), (3, 4), (5, 6), (4, 5)] result, has_cycles = topological_sort(pairs) self.assertTrue(not has_cycles) self.assertEqual(result, [1, 2, 3, 4, 5, 6]) def test_topological_sort_3(self): """ Does cycle detection work? """ pairs = [(1, 2), (2, 3), (3, 1)] result, has_cycles = topological_sort(pairs) self.assertTrue(has_cycles) pyface-7.4.0/pyface/action/schema/tests/test_action_manager_builder.py0000644000076500000240000003373514176222673027173 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 pyface.action.api import ( Action, ActionItem, ActionManager, Group, MenuManager, MenuBarManager, ) from pyface.action.schema.api import ( GroupSchema, MenuSchema, MenuBarSchema, SchemaAddition, ) from ..action_manager_builder import ( ActionManagerBuilder, ) class ActionManagerBuilderTestCase(unittest.TestCase): # 'TestCase' protocol -------------------------------------------------# def setUp(self): """ Create some dummy actions to use while testing. """ for i in range(1, 7): action_id = "action%i" % i setattr( self, action_id, Action(id=action_id, name="Action %i" % i) ) # 'ActionManagerBuilderTestCase' protocol -----------------------------# def assertActionElementsEqual(self, first, second): """ Checks that two action managers are (logically) equivalent. """ children1 = children2 = [] self.assertEqual(type(first), type(second)) self.assertEqual(first.id, second.id) if isinstance(first, ActionItem): self.assertEqual(first.action.name, second.action.name) elif isinstance(first, ActionManager): if not isinstance(first, MenuBarManager): self.assertEqual(first.name, second.name) children1, children2 = first.groups, second.groups elif isinstance(first, Group): self.assertEqual(first.separator, second.separator) children1, children2 = first.items, second.items self.assertEqual(len(children1), len(children2)) for i in range(len(children1)): self.assertActionElementsEqual(children1[i], children2[i]) def reset_unique_ids(self): import pyface.util.id_helper as id_helper id_helper.object_counter = id_helper._ObjectCounter() @contextmanager def unique_id_context_manager(self): self.reset_unique_ids() try: yield finally: self.reset_unique_ids() # Tests ---------------------------------------------------------------- def test_simple_menu_bar(self): """ Does constructing a simple menu with no additions work? """ schema = MenuBarSchema( MenuSchema(self.action1, self.action2, id="File", name="&File"), MenuSchema(self.action3, self.action4, id="Edit", name="&Edit"), ) builder = ActionManagerBuilder() actual = builder.create_action_manager(schema) desired = MenuBarManager( MenuManager(self.action1, self.action2, id="File", name="&File"), MenuManager(self.action3, self.action4, id="Edit", name="&Edit"), id="MenuBar", ) self.assertActionElementsEqual(actual, desired) # Tests about schema additions ----------------------------------------- def test_additions_menu_bar(self): """ Does constructing a menu with a few additions work? """ schema = MenuBarSchema( MenuSchema( GroupSchema(self.action1, self.action2, id="FileGroup"), id="File", ) ) additions = [ SchemaAddition( factory=lambda: self.action3, before="action1", path="MenuBar/File/FileGroup", ), SchemaAddition( factory=lambda: self.action4, before="action1", path="MenuBar/File/FileGroup", ), SchemaAddition( factory=lambda: self.action5, path="MenuBar/File/FileGroup" ), ] builder = ActionManagerBuilder(additions=additions) actual = builder.create_action_manager(schema) desired = MenuBarManager( MenuManager( Group( self.action3, self.action4, self.action1, self.action2, self.action5, id="FileGroup", ), id="File", ), id="MenuBar", ) self.assertActionElementsEqual(actual, desired) def test_extra_menu(self): """ Test contributing a whole new menu to the menu bar. """ # Initial menu. schema = MenuBarSchema( MenuSchema( GroupSchema(self.action1, id="FileGroup"), id="FileMenu" ) ) # Contributed menu. extra_menu = MenuSchema( GroupSchema(self.action2, id="BarGroup"), id="DummyActionsMenu" ) additions = [ SchemaAddition( path="MenuBar", factory=lambda: extra_menu, id="DummyActionsSMenu", ) ] # Build the final menu. builder = ActionManagerBuilder(additions=additions) actual = builder.create_action_manager(schema) desired = MenuBarManager( MenuManager(Group(self.action1, id="FileGroup"), id="FileMenu"), MenuManager( Group(self.action2, id="BarGroup"), id="DummyActionsMenu" ), id="MenuBar", ) self.assertActionElementsEqual(actual, desired) # Tests about merging schemas -----------------------------------------# def test_merging_redundant_items(self): """ Menus and groups with matching path are merged together. """ # Initial menu. schema = MenuBarSchema( MenuSchema( GroupSchema(self.action1, id="FileGroup"), name="File menu number one", id="FileMenu", ) ) # Contributed menus. extra_menu = MenuSchema( GroupSchema(self.action2, id="FileGroup"), name="File menu number two", id="FileMenu", ) additions = [ SchemaAddition( path="MenuBar", factory=lambda: extra_menu, id="DummyActionsSMenu", ) ] # Build the final menu. builder = ActionManagerBuilder(additions=additions) actual = builder.create_action_manager(schema) # Note that we expect the name of the menu to be inherited from # the menu in the menu bar schema that is defined first. desired = MenuBarManager( MenuManager( Group(self.action1, self.action2, id="FileGroup"), name="File menu number one", id="FileMenu", ), id="MenuBar", ) self.assertActionElementsEqual(actual, desired) def test_unwanted_merge(self): """ Test that we don't have automatic merges due to forgetting to set a schema ID. """ with self.unique_id_context_manager(): # Initial menu. schema = MenuBarSchema( MenuSchema( GroupSchema(self.action1, id="FileGroup"), name="File 1" ) ) # Contributed menus. extra_menu = MenuSchema( GroupSchema(self.action2, id="FileGroup"), name="File 2" ) additions = [ SchemaAddition( path="MenuBar", factory=lambda: extra_menu, id="DummyActionsSMenu", ) ] # Build the final menu. builder = ActionManagerBuilder(additions=additions) actual = builder.create_action_manager(schema) # Note that we expect the name of the menu to be inherited from # the menu in the menu bar schema that is defined first. desired = MenuBarManager( MenuManager( Group(self.action1, id="FileGroup"), name="File 1", id="MenuSchema_1", ), MenuManager( Group(self.action2, id="FileGroup"), name="File 2", id="MenuSchema_2", ), id="MenuBar", ) self.assertActionElementsEqual(actual, desired) def test_merging_items_with_same_id_but_different_class(self): """ Schemas with the same path but different types (menus, groups) are not merged together. Having a group and a menu with the same path is of course bad practice, but we need a predictable outcome. """ # Initial menu. schema = MenuBarSchema( MenuSchema( GroupSchema(self.action1, id="FileGroup"), id="FileSchema" ) ) # Contributed menus. extra_group = GroupSchema(self.action2, id="FileSchema") additions = [ SchemaAddition( path="MenuBar", factory=(lambda: extra_group), id="DummyActionsSMenu", ) ] # Build the final menu. builder = ActionManagerBuilder(additions=additions) actual = builder.create_action_manager(schema) desired = MenuBarManager( MenuManager(Group(self.action1, id="FileGroup"), id="FileSchema"), Group(self.action2, id="FileSchema"), id="MenuBar", ) self.assertActionElementsEqual(actual, desired) def test_merging_redundant_items_that_are_not_schemas(self): """ Items that are not schemas cannot be merged, but we should not crash, either. """ # Initial menu. schema = MenuBarSchema( # This menu is not a schema... MenuManager(Group(self.action1, id="FileGroup"), id="FileMenu") ) # Contributed menus. extra_menu = MenuSchema( GroupSchema(self.action2, id="FileGroup"), id="FileMenu" ) additions = [ SchemaAddition( path="MenuBar", factory=lambda: extra_menu, id="DummyActionsSMenu", ) ] # Build the final menu. builder = ActionManagerBuilder(additions=additions) actual = builder.create_action_manager(schema) desired = MenuBarManager( MenuManager(Group(self.action1, id="FileGroup"), id="FileMenu"), MenuManager(Group(self.action2, id="FileGroup"), id="FileMenu"), id="MenuBar", ) self.assertActionElementsEqual(actual, desired) # Tests about ordering ------------------------------------------------- def test_absolute_ordering(self): """ Does specifying absolute_position work? """ schema = MenuBarSchema( MenuSchema( GroupSchema(self.action1, self.action2, id="FileGroup"), id="File", ) ) additions = [ SchemaAddition( factory=lambda: self.action3, absolute_position="last", path="MenuBar/File/FileGroup", ), SchemaAddition( factory=lambda: self.action4, absolute_position="first", path="MenuBar/File/FileGroup", ), SchemaAddition( factory=lambda: self.action5, absolute_position="first", path="MenuBar/File/FileGroup", ), ] builder = ActionManagerBuilder(additions=additions) actual = builder.create_action_manager(schema) desired = MenuBarManager( MenuManager( Group( self.action4, self.action5, self.action1, self.action2, self.action3, id="FileGroup", ), id="File", ), id="MenuBar", ) self.assertActionElementsEqual(actual, desired) def test_absolute_and_before_after(self): """ Does specifying absolute_position along with before, after work? """ schema = MenuBarSchema( MenuSchema( GroupSchema(self.action1, self.action2, id="FileGroup"), id="File", ) ) additions = [ SchemaAddition( factory=lambda: self.action3, id="action3", after="action2", path="MenuBar/File/FileGroup", ), SchemaAddition( factory=lambda: self.action4, after="action3", path="MenuBar/File/FileGroup", ), SchemaAddition( factory=lambda: self.action5, id="action5", absolute_position="last", path="MenuBar/File/FileGroup", ), SchemaAddition( factory=lambda: self.action6, absolute_position="last", before="action5", path="MenuBar/File/FileGroup", ), ] builder = ActionManagerBuilder(additions=additions) actual = builder.create_action_manager(schema) desired = MenuBarManager( MenuManager( Group( self.action1, self.action2, self.action3, self.action4, self.action6, self.action5, id="FileGroup", ), id="File", ), id="MenuBar", ) self.assertActionElementsEqual(actual, desired) pyface-7.4.0/pyface/action/schema/__init__.py0000644000076500000240000000000014176222673022027 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/action/schema/api.py0000644000076500000240000000212014176222673021046 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 ``pyface.action.schema`` subpackage. Schemas and Aliases ------------------- - :class:`~.ActionSchema` - :class:`~.GroupSchema` - :class:`~.MenuBarSchema` - :class:`~.MenuSchema` - :attr:`~.SGroup` - :attr:`~.SMenu` - :attr:`~.SMenuBar` - :attr:`~.SToolBar` - :class:`~.ToolBarSchema` Builder and Schema Additions ---------------------------- - :class:`~.ActionManagerBuilder` - :class:`~.SchemaAddition` """ from .action_manager_builder import ActionManagerBuilder from .schema import ( ActionSchema, GroupSchema, MenuBarSchema, MenuSchema, SGroup, SMenu, SMenuBar, SToolBar, ToolBarSchema, ) from .schema_addition import SchemaAddition pyface-7.4.0/pyface/action/schema/schema.py0000644000076500000240000001505014176222673021543 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Schema class definitions This module defines the Schema """ from traits.api import ( Bool, Callable, Enum, HasTraits, Instance, List, Property, Str, Tuple, Union, ) from pyface.util.id_helper import get_unique_id # NOTE # This module should never import directly or indirectly any toolkit-dependent # code at the top level. This permits it to be used declaratively in a # toolkit-agnostic way if needed. # Trait definitions. SubSchema = Union( None, Instance("pyface.action.action.Action"), Instance("pyface.action.action_item.ActionItem"), Instance("pyface.action.group.Group"), Instance("pyface.action.menu_manager.MenuManager"), Instance("pyface.action.schema.schema.GroupSchema"), Instance("pyface.action.schema.schema.MenuSchema"), Instance("pyface.action.schema.schema.Schema"), ) class Schema(HasTraits): """ The abstract base class for all action schemas. """ #: The schema's identifier (unique within its parent schema). id = Str() #: The list of sub-items in the schema. These items can be other #: (non-top-level) schema or concrete instances from the Pyface API. items = List(SubSchema) def __init__(self, *items, **traits): """ Creates a new schema. """ super().__init__(**traits) self.items.extend(items) def create(self, children): """ Create the appropriate pyface.action instance with the specified child items. """ raise NotImplementedError() # Trait initializers --------------------------------------------------- def _id_default(self): return get_unique_id(self) class ActionSchema(Schema): """ Action schema for Pyface Actions. An action schema cannot have children. It is used as an action factory to make sure a larger schema (e.g., a menu schema) can be used multiple times. Without using an ActionSchema, a reference to the action is added to every menu created from the schema. When one of the menus is destroyed, the action is also destroyed and is made unusable. """ #: A factory for the Action instance. action_factory = Callable() #: Items is overwritten to be empty and read-only to avoid assigning to #: it by mistake. items = Property() def _get_items(self): return [] def create(self, children): """ Create the appropriate Pyface Action instance. """ traits = dict(id=self.id) return self.action_factory(**traits) # Trait initializers --------------------------------------------------- def _action_factory_default(self): from pyface.action.action import Action return Action class GroupSchema(Schema): """ A schema for a Pyface Group. """ #: A factory for instantiating a pyface Group. group_factory = Callable() #: Does the group require a separator when it is visualized? separator = Bool(True) def create(self, children): traits = dict(id=self.id, separator=self.separator) return self.group_factory(*children, **traits) # Trait initializers --------------------------------------------------- def _group_factory_default(self): from pyface.action.group import Group return Group class MenuSchema(Schema): """ A schema for a Pyface MenuManager. """ #: The menu's user visible name. name = Str() #: Does the menu require a separator before the menu item? separator = Bool(False) #: The default action for tool button when shown in a toolbar (Qt only) action = Instance("pyface.action.action.Action") #: A factory for instantiating a pyface MenuManager. menu_manager_factory = Callable() def create(self, children): traits = dict(id=self.id, name=self.name, separator=self.separator) if self.action: traits["action"] = self.action return self.menu_manager_factory(*children, **traits) # Trait initializers --------------------------------------------------- def _menu_manager_factory_default(self): from pyface.action.menu_manager import MenuManager return MenuManager class MenuBarSchema(Schema): """ A schema for a Pyface MenuBarManager. """ #: Assign a default ID for menu bar schemas. id = "MenuBar" #: A factory for instantiating a pyface MenuBarManager. menu_bar_manager_factory = Callable() def create(self, children): traits = dict(id=self.id) return self.menu_bar_manager_factory(*children, **traits) # Trait initializers --------------------------------------------------- def _menu_bar_manager_factory_default(self): from pyface.action.menu_bar_manager import MenuBarManager return MenuBarManager class ToolBarSchema(Schema): """ A schema for a Pyface ToolBarManager. """ #: Assign a default ID for tool bar schemas. id = "ToolBar" #: The tool bar's user visible name. Note that this name may not be used on #: all platforms. name = Str("Tool Bar") #: The size of tool images (width, height). image_size = Tuple((16, 16)) #: The orientation of the toolbar. orientation = Enum("horizontal", "vertical") #: Should we display the horizontal divider? show_divider = Bool(True) #: Should we display the name of each tool bar tool under its image? show_tool_names = Bool(True) #: A factory for instantiating a pyface ToolBarManager tool_bar_manager_factory = Callable() def create(self, children): traits = dict( id=self.id, name=self.name, image_size=self.image_size, orientation=self.orientation, show_divider=self.show_divider, show_tool_names=self.show_tool_names, ) return self.tool_bar_manager_factory(*children, **traits) # Trait initializers --------------------------------------------------- def _tool_bar_manager_factory_default(self): from pyface.action.tool_bar_manager import ToolBarManager return ToolBarManager # Convenience abbreviations. SGroup = GroupSchema SMenu = MenuSchema SMenuBar = MenuBarSchema SToolBar = ToolBarSchema pyface-7.4.0/pyface/action/action_manager_item.py0000644000076500000240000000504614176222673023034 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Abstract base class for all action manager items. """ from traits.api import Bool, HasTraits, Instance, Str class ActionManagerItem(HasTraits): """ Abstract base class for all action manager items. An action manager item represents a contribution to a shared UI resource such as a menu bar, menu or tool bar. Action manager items know how to add themselves to menu bars, menus and tool bars. In a tool bar a contribution item is represented as a tool or a separator. In a menu bar a contribution item is a menu, and in a menu a contribution item is a menu item or separator. """ #: The item's unique identifier ('unique' in this case means unique within #: its group) id = Str() #: The group the item belongs to. parent = Instance("pyface.action.api.Group") #: Is the item enabled? enabled = Bool(True) #: Is the item visible? visible = Bool(True) # ------------------------------------------------------------------------ # 'ActionManagerItem' interface. # ------------------------------------------------------------------------ def add_to_menu(self, parent, menu, controller): """ Adds the item to a menu. Parameters ---------- parent : toolkit control The parent of the new menu item control. menu : toolkit menu The menu to add the action item to. controller : ActionController instance or None The controller to use. """ raise NotImplementedError() def add_to_toolbar(self, parent, tool_bar, image_cache, controller): """ Adds the item to a tool bar. Parameters ---------- parent : toolkit control The parent of the new menu item control. tool_bar : toolkit toolbar The toolbar to add the action item to. image_cache : ImageCache instance The image cache for resized images. controller : ActionController instance or None The controller to use. show_labels : bool Should the toolbar item show a label. """ raise NotImplementedError() pyface-7.4.0/pyface/action/group.py0000644000076500000240000001746714176222673020215 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 group of action manager items. """ from functools import partial from traits.api import Any, Bool, HasTraits, List, observe, Property, Str from traits.trait_base import user_name_for from pyface.action.action import Action from pyface.action.action_item import ActionItem class Group(HasTraits): """ A group of action manager items. By default, a group declares itself as requiring a separator when it is visualized, but this can be changed by setting its 'separator' trait to False. """ # 'Group' interface ---- #: Is the group enabled? enabled = Bool(True) #: Is the group visible? visible = Bool(True) #: The group's unique identifier (only needs to be unique within the action #: manager that the group belongs to). id = Str() #: All of the items in the group. items = Property #: The action manager that the group belongs to. parent = Any # Instance('pyface.action.ActionManager') #: Does this group require a separator when it is visualized? separator = Bool(True) # Private interface ---- #: All of the items in the group. _items = List # (ActionManagerItem) # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, *items, **traits): """ Creates a new menu manager. Parameters ---------- items : collection of ActionManagerItems Items to add to the group. """ # Base class constructor. super().__init__(**traits) # Add any specified items. for item in items: self.append(item) # ------------------------------------------------------------------------ # 'Group' interface. # ------------------------------------------------------------------------ # Trait Properties ----------------------------------------------------- def _get_items(self): return self._items[:] # Trait change handlers ------------------------------------------------ @observe("enabled") def _enabled_updated(self, event): for item in self.items: item.enabled = event.new # Methods -------------------------------------------------------------# def append(self, item): """ Appends an item to the group. Parameters ---------- item : ActionManagerItem, Action or callable The item to append. Returns ------- item : ActionManagerItem The actually inserted item. Notes ----- If the item is an ActionManagerItem instance it is simply appended. If the item is an Action instance, an ActionItem is created for the action, and that is appended. If the item is a callable, then an Action is created for the callable, and then that is handled as above. """ return self.insert(len(self._items), item) def clear(self): """ Remove all items from the group. """ self._items = [] def destroy(self): """ Called when the manager is no longer required. By default this method simply calls 'destroy' on all items in the group. """ for item in self.items: item.destroy() def insert(self, index, item): """ Inserts an item into the group at the specified index. Parameters ---------- index : int The position to insert the item. item : ActionManagerItem, Action or callable The item to insert. Returns ------- item : ActionManagerItem The actually inserted item. Notes ----- If the item is an ActionManagerItem instance it is simply inserted. If the item is an Action instance, an ActionItem is created for the action, and that is inserted. If the item is a callable, then an Action is created for the callable, and then that is handled as above. """ if isinstance(item, Action): item = ActionItem(action=item) elif callable(item): name = user_name_for(item.__name__) item = ActionItem(action=Action(name=name, on_perform=item)) item.parent = self self._items.insert(index, item) return item def remove(self, item): """ Removes an item from the group. Parameters ---------- item : ActionManagerItem The item to remove. """ self._items.remove(item) item.parent = None def insert_before(self, before, item): """ Inserts an item into the group before the specified item. Parameters ---------- before : ActionManagerItem The item to insert before. item : ActionManagerItem, Action or callable The item to insert. Returns ------- index, item : int, ActionManagerItem The position inserted, and the item actually inserted. Notes ----- If the item is an ActionManagerItem instance it is simply inserted. If the item is an Action instance, an ActionItem is created for the action, and that is inserted. If the item is a callable, then an Action is created for the callable, and then that is handled as above. """ index = self._items.index(before) self.insert(index, item) return (index, item) def insert_after(self, after, item): """ Inserts an item into the group after the specified item. Parameters ---------- before : ActionManagerItem The item to insert after. item : ActionManagerItem, Action or callable The item to insert. Returns ------- index, item : int, ActionManagerItem The position inserted, and the item actually inserted. Notes ----- If the item is an ActionManagerItem instance it is simply inserted. If the item is an Action instance, an ActionItem is created for the action, and that is inserted. If the item is a callable, then an Action is created for the callable, and then that is handled as above. """ index = self._items.index(after) self.insert(index + 1, item) return (index, item) def find(self, id): """ Find the item with the specified id. Parameters ---------- id : str The id of the item Returns ------- item : ActionManagerItem The item with the specified Id, or None if no such item exists. """ for item in self._items: if item.id == id: return item else: return None @classmethod def factory(cls, *args, **kwargs): """ Create a factory for a group with the given arguments. This is particularly useful for passing context to Tasks schema additions. """ return partial(cls, *args, **kwargs) class Separator(Group): """ A convenience class. This is only used in 'cheap and cheerful' applications that create menus like:: file_menu = MenuManager( CopyAction(), Separator(), ExitAction() ) Hopefully, 'Separator' is more readable than 'Group'... """ pass pyface-7.4.0/pyface/action/traitsui_widget_action.py0000644000076500000240000000446314176222673023615 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 Constant, HasTraits, Instance from .action import Action class TraitsUIWidgetAction(Action): """ A widget action containing a TraitsUI. If a object is supplied, then the UI is generated from the object's view, otherwise the ui is generated on using the Action object. Notes ----- This is currently only supported by the Qt backend. """ # TraitsUIWidgetAction traits ------------------------------------------- #: The underlying traits model to be displayed, or None. model = Instance(HasTraits) # Action traits --------------------------------------------------------- #: This is a widget action. style = Constant("widget") # ------------------------------------------------------------------------ # Action interface # ------------------------------------------------------------------------ def create_control(self, parent): """ Called when creating a "widget" style action. This constructs an TraitsUI subpanel-based control. It does no binding to the `perform` method. Parameters ---------- parent : toolkit control The toolkit control, usually a toolbar. Returns ------- control : toolkit control A toolkit control or None. """ ui = self.edit_traits(kind="subpanel", parent=parent) control = ui.control control._ui = ui return control # ------------------------------------------------------------------------ # HasTraits interface # ------------------------------------------------------------------------ def trait_context(self): """ Use the model object for the Traits UI context, if appropriate. """ if self.model is not None: context = {"object": self.model, "action": self} return context return super().trait_context() pyface-7.4.0/pyface/action/action_item.py0000644000076500000240000001304414176222673021337 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ An action manager item that represents an actual action. """ from traits.api import Any, Instance, List, Property, Str, observe from pyface.action.action import Action from pyface.action.action_manager_item import ActionManagerItem # Import the toolkit specific versions of the internal classes. from pyface.toolkit import toolkit_object _MenuItem = toolkit_object("action.action_item:_MenuItem") _Tool = toolkit_object("action.action_item:_Tool") _PaletteTool = toolkit_object("action.action_item:_PaletteTool") class ActionItem(ActionManagerItem): """ An action manager item that represents an actual action. """ # 'ActionManagerItem' interface ---------------------------------------- #: The item's unique identifier ('unique' in this case means unique within #: its group). id = Property(Str) # 'ActionItem' interface ----------------------------------------------- #: The action! action = Instance(Action) #: The toolkit specific control created for this item. control = Any() #: The toolkit specific Id of the control created for this item. # #: We have to keep the Id as well as the control because wx tool bar tools #: are created as 'wxObjectPtr's which do not have Ids, and the Id is #: required to manipulate the state of a tool via the tool bar 8^( # FIXME v3: Why is this part of the public interface? control_id = Any() # Private interface ---------------------------------------------------- #: All of the internal instances that wrap this item. _wrappers = List(Any) # ------------------------------------------------------------------------ # 'ActionManagerItem' interface. # ------------------------------------------------------------------------ # Trait properties ----------------------------------------------------- def _get_id(self): return self.action.id # Trait change handlers ------------------------------------------------ @observe('enabled') def _enabled_updated(self, event): self.action.enabled = event.new @observe('visible') def _visible_updated(self, event): self.action.visible = event.new @observe("_wrappers:items:control") def _on_destroy(self, event): """ Handle the destruction of the wrapper. """ if event.new is None: self._wrappers.remove(event.object) # ------------------------------------------------------------------------ # 'ActionItem' interface. # ------------------------------------------------------------------------ def add_to_menu(self, parent, menu, controller): """ Add the item to a menu. Parameters ---------- parent : toolkit control The parent of the new menu item control. menu : toolkit menu The menu to add the action item to. controller : ActionController instance or None The controller to use. """ if (controller is None) or controller.can_add_to_menu(self.action): wrapper = _MenuItem(parent, menu, self, controller) # fixme: Martin, who uses this information? if controller is None: self.control = wrapper.control self.control_id = wrapper.control_id self._wrappers.append(wrapper) def add_to_toolbar( self, parent, tool_bar, image_cache, controller, show_labels=True ): """ Adds the item to a tool bar. Parameters ---------- parent : toolkit control The parent of the new menu item control. tool_bar : toolkit toolbar The toolbar to add the action item to. image_cache : ImageCache instance The image cache for resized images. controller : ActionController instance or None The controller to use. show_labels : bool Should the toolbar item show a label. """ if (controller is None) or controller.can_add_to_toolbar(self.action): wrapper = _Tool( parent, tool_bar, image_cache, self, controller, show_labels ) # fixme: Martin, who uses this information? if controller is None: self.control = wrapper.control self.control_id = wrapper.control_id self._wrappers.append(wrapper) def add_to_palette(self, tool_palette, image_cache, show_labels=True): """ Adds the item to a tool palette. Parameters ---------- parent : toolkit control The parent of the new menu item control. tool_palette : toolkit tool palette The tool palette to add the action item to. image_cache : ImageCache instance The image cache for resized images. show_labels : bool Should the toolbar item show a label. """ wrapper = _PaletteTool(tool_palette, image_cache, self, show_labels) self._wrappers.append(wrapper) def destroy(self): """ Called when the action is no longer required. By default this method calls 'destroy' on the action itself. """ self.action.destroy() pyface-7.4.0/pyface/action/menu_bar_manager.py0000644000076500000240000000125214176222673022324 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 toolkit-specific menu bar manager that realizes itself in a menu bar control. - :attr:`~.MenuBarManager` """ # Import the toolkit specific version. from pyface.toolkit import toolkit_object MenuBarManager = toolkit_object("action.menu_bar_manager:MenuBarManager") pyface-7.4.0/pyface/action/action_controller.py0000644000076500000240000000464214176222673022570 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 default action controller for menus, menu bars and tool bars. """ from traits.api import HasTraits class ActionController(HasTraits): """ The default action controller for menus, menu bars and tool bars. """ # ------------------------------------------------------------------------ # 'ActionController' interface. # ------------------------------------------------------------------------ def perform(self, action, event): """ Control an action invocation. 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). """ return action.perform(event) def can_add_to_menu(self, action): """ Can add an action to a menu Parameters ---------- action : Action instance The action to consider. Returns ------- can_add : bool ``True`` if the action can be added to a menu/menubar. """ return True def add_to_menu(self, action): """ Called when an action is added to the a menu/menubar. Parameters ---------- action : Action instance The action added to the menu. """ pass def can_add_to_toolbar(self, action): """ Returns True if the action can be added to a toolbar. Parameters ---------- action : Action instance The action to consider. Returns ------- can_add : bool ``True`` if the action can be added to a toolbar. """ return True def add_to_toolbar(self, action): """ Called when an action is added to the a toolbar. Parameters ---------- action : Action instance The action added to the toolbar. """ pass pyface-7.4.0/pyface/viewer/0000755000076500000240000000000014176460551016513 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/viewer/table_column_provider.py0000644000076500000240000000225114176222673023444 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Base class for all table column providers. """ from .column_provider import ColumnProvider class TableColumnProvider(ColumnProvider): """ Base class for all table label providers. By default an item has no icon, and 'str' is used to generate its label. """ # This class currently does not specialize the base class in any way. # It is here to (hopefully) make the APIs for the viewer, content and label # provider classes more consistent. In particular, if you are building a # table viewer you sub-class 'TableContentProvider' and # 'TableLabelProvider'. For a tree viewer you sub-class # 'TreeContentProvider' and 'TreeLabelProvider' instead of some # combination of the specific and generic viewer classes as in JFace. pass pyface-7.4.0/pyface/viewer/label_provider.py0000644000076500000240000000265214176222673022064 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Abstract base class for label providers. """ from traits.api import HasTraits class LabelProvider(HasTraits): """ Abstract base class for label providers. By default an element has no label image, and 'str' is used to generate its label text. """ # ------------------------------------------------------------------------ # 'LabelProvider' interface. # ------------------------------------------------------------------------ def get_image(self, viewer, element): """ Returns the label image for an element. """ return None def get_text(self, viewer, element): """ Returns the label text for an element. """ return str(element) def set_text(self, tree, element, text): """ Sets the text representation of a node. Returns True if setting the text succeeded, otherwise False. """ return True def is_editable(self, viewer, element): """ Can the label text be changed via the viewer? """ return False pyface-7.4.0/pyface/viewer/viewer_filter.py0000644000076500000240000000365614176222673021746 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Abstract base class for all viewer filters. """ from traits.api import HasTraits class ViewerFilter(HasTraits): """ Abstract base class for all viewer filters. """ # ------------------------------------------------------------------------ # 'ViewerFilter' interface. # ------------------------------------------------------------------------ def filter(self, viewer, parent, elements): """ Filters a list of elements. 'viewer' is the viewer that we are filtering elements for. 'parent' is the parent element. 'elements' is the list of elements to filter. Returns a list containing only those elements for which 'select' returns True. """ return [e for e in elements if self.select(viewer, parent, e)] def select(self, viewer, parent, element): """ Returns True if the element is 'allowed' (ie. NOT filtered). 'viewer' is the viewer that we are filtering elements for. 'parent' is the parent element. 'element' is the element to select. By default we return True. """ return True def is_filter_trait(self, element, trait_name): """ Is the filter affected by changes to an element's trait? 'element' is the element. 'trait_name' is the name of the trait. Returns True if the filter would be affected by changes to the trait named 'trait_name' on the specified element. By default we return False. """ return False pyface-7.4.0/pyface/viewer/tree_item.py0000644000076500000240000000741414176222673021051 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 generic base-class for items in a tree data structure. An example:: root = TreeItem(data='Root') fruit = TreeItem(data='Fruit') fruit.append(TreeItem(data='Apple', allows_children=False)) fruit.append(TreeItem(data='Orange', allows_children=False)) fruit.append(TreeItem(data='Pear', allows_children=False)) root.append(fruit) veg = TreeItem(data='Veg') veg.append(TreeItem(data='Carrot', allows_children=False)) veg.append(TreeItem(data='Cauliflower', allows_children=False)) veg.append(TreeItem(data='Sprout', allows_children=False)) root.append(veg) """ from traits.api import Any, Bool, HasTraits, Instance, List, Property class TreeItem(HasTraits): """ A generic base-class for items in a tree data structure. """ # 'TreeItem' interface ------------------------------------------------- # Does this item allow children? allows_children = Bool(True) # The item's children. children = List(Instance("TreeItem")) # Arbitrary data associated with the item. data = Any() # Does the item have any children? has_children = Property(Bool) # The item's parent. parent = Instance("TreeItem") # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __str__(self): """ Returns the informal string representation of the object. """ if self.data is None: s = "" else: s = str(self.data) return s # ------------------------------------------------------------------------ # 'TreeItem' interface. # ------------------------------------------------------------------------ # Properties ----------------------------------------------------------- # has_children def _get_has_children(self): """ True iff the item has children. """ return len(self.children) != 0 # Methods -------------------------------------------------------------# def append(self, child): """ Appends a child to this item. This removes the child from its current parent (if it has one). """ return self.insert(len(self.children), child) def insert(self, index, child): """ Inserts a child into this item at the specified index. This removes the child from its current parent (if it has one). """ if child.parent is not None: child.parent.remove(child) child.parent = self self.children.insert(index, child) return child def remove(self, child): """ Removes a child from this item. """ child.parent = None self.children.remove(child) return child def insert_before(self, before, child): """ Inserts a child into this item before the specified item. This removes the child from its current parent (if it has one). """ index = self.children.index(before) self.insert(index, child) return (index, child) def insert_after(self, after, child): """ Inserts a child into this item after the specified item. This removes the child from its current parent (if it has one). """ index = self.children.index(after) self.insert(index + 1, child) return (index, child) pyface-7.4.0/pyface/viewer/viewer.py0000644000076500000240000000125214176222673020367 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Abstract base class for all viewers. """ from pyface.layout_widget import LayoutWidget class Viewer(LayoutWidget): """ Abstract base class for all viewers. A viewer is a model-based adapter on some underlying toolkit-specific widget. """ pass pyface-7.4.0/pyface/viewer/viewer_sorter.py0000644000076500000240000000565214176222673021775 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Abstract base class for all viewer sorters. """ from traits.api import HasTraits class ViewerSorter(HasTraits): """ Abstract base class for all viewer sorters. """ # ------------------------------------------------------------------------ # 'ViewerSorter' interface. # ------------------------------------------------------------------------ def sort(self, viewer, parent, elements): """ Sorts a list of elements IN PLACE. 'viewer' is the viewer that we are sorting elements for. 'parent' is the parent element. 'elements' is the list of elements to sort. Returns the list that was sorted IN PLACE (for convenience). """ # This creates a comparison function with the names 'viewer' and # 'parent' bound to the corresponding arguments to this method. def key(element): """ Key function. """ return self.key(viewer, parent, element) elements.sort(key=key) return elements def key(self, viewer, parent, element): """ Returns the result of comparing two elements. 'viewer' is the viewer that we are sorting elements for. 'parent' is the parent element. 'element' is the the first element being sorted. """ # Get the category category = self.category(viewer, parent, element) # Get the label if hasattr(viewer, "label_provider"): label = viewer.label_provider.get_text(viewer, element) else: label = viewer.node_model.get_text(viewer, element) return (category, label) def category(self, viewer, parent, element): """ Returns the category (an integer) for an element. 'parent' is the parent element. 'elements' is the element to return the category for. Categories are used to sort elements into bins. The bins are arranged in ascending numeric order. The elements within a bin are arranged as dictated by the sorter's 'compare' method. By default all elements are given the same category (0). """ return 0 def is_sorter_trait(self, element, trait_name): """ Is the sorter affected by changes to an element's trait? 'element' is the element. 'trait_name' is the name of the trait. Returns True if the sorter would be affected by changes to the trait named 'trait_name' on the specified element. By default we return False. """ return False pyface-7.4.0/pyface/viewer/table_content_provider.py0000644000076500000240000000227114176222673023623 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Abstract base class for table content providers. """ from .content_provider import ContentProvider class TableContentProvider(ContentProvider): """ Abstract base class for table content providers. Table content providers are used by (surprise, surprise) table viewers! """ # This class currently does not specialize the base class in any way. # It is here to (hopefully) make the APIs for the viewer, content and label # provider classes more consistent. In particular, if you are building a # table viewer you sub-class 'TableContentProvider' and # 'TableLabelProvider'. For a tree viewer you sub-class # 'TreeContentProvider' and 'TreeLabelProvider' instead of some # combination of the specific and generic viewer classes as in JFace. pass pyface-7.4.0/pyface/viewer/content_viewer.py0000644000076500000240000000345514176222673022130 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Abstract base class for all content viewers. """ from traits.api import Any, Instance, List from .viewer import Viewer from .viewer_filter import ViewerFilter from .viewer_sorter import ViewerSorter class ContentViewer(Viewer): """ Abstract base class for all content viewers. A content viewer is a model-based adapter on some underlying toolkit-specific widget that acceses the model via a content provider and a label provider. The content provider provides the actual elements in the model. The label provider provides a label for each element consisting of text and/or an image. """ # The domain object that is the root of the viewer's data. input = Any() # The content provider provides the data elements for the viewer. # # Derived classes specialize this trait with the specific type of the # content provider that they require (e.g. the tree viewer MUST have a # 'TreeContentProvider'). content_provider = Any() # The label provider provides labels for each element. # # Derived classes specialize this trait with the specific type of the label # provider that they require (e.g. the table viewer MUST have a # 'TableLabelProvider'). label_provider = Any() # The viewer's sorter (None if no sorting is required). sorter = Instance(ViewerSorter) # The viewer's filters. filters = List(ViewerFilter) pyface-7.4.0/pyface/viewer/table_viewer.py0000644000076500000240000000113014176222673021531 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 viewer based on a table control. """ # Import the toolkit specific version. from pyface.toolkit import toolkit_object TableViewer = toolkit_object("viewer.table_viewer:TableViewer") pyface-7.4.0/pyface/viewer/column_provider.py0000644000076500000240000000303514176222673022276 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Base class for all column providers. """ from traits.api import HasTraits, Int class ColumnProvider(HasTraits): """ Base class for all column providers. By default a column's label is 'Column n' and is 100 pixels wide. """ # The number of columns. column_count = Int() # ------------------------------------------------------------------------ # 'TableColumnProvider' interface. # ------------------------------------------------------------------------ def get_label(self, viewer, column_index): """ Returns the label for a column. """ return "Column %d" % column_index def get_width(self, viewer, column_index): """ Returns the width of a column. Returning -1 (the default) means that the column will be sized to fit its longest item (or its column header if it is longer than any item). """ return -1 def get_alignment(self, viewer, column_index): """ Returns the alignment of the column header and cells. Returns, 'left', 'right', 'centre' or 'center' ('left' by default). """ return "left" pyface-7.4.0/pyface/viewer/__init__.py0000644000076500000240000000062714176222673020632 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! pyface-7.4.0/pyface/viewer/tree_viewer.py0000644000076500000240000000112414176222673021404 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 viewer based on a tree control. """ # Import the toolkit specific version. from pyface.toolkit import toolkit_object TreeViewer = toolkit_object("viewer.tree_viewer:TreeViewer") pyface-7.4.0/pyface/viewer/tree_label_provider.py0000644000076500000240000000340114176222673023074 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Base class for all tree label providers. """ from .label_provider import LabelProvider class TreeLabelProvider(LabelProvider): """ Base class for all tree label providers. By default an element has no label image, and 'str' is used to generate its label text. """ # ------------------------------------------------------------------------ # 'LabelProvider' interface. # ------------------------------------------------------------------------ def set_text(self, viewer, element, text): """ Sets the text representation of a node. Returns True if setting the text succeeded, otherwise False. """ return len(text.strip()) > 0 # ------------------------------------------------------------------------ # 'TreeLabelProvider' interface. # ------------------------------------------------------------------------ def get_drag_value(self, viewer, element): """ Get the value that is dragged for an element. By default the drag value is the element itself. """ return element def is_collapsible(self, viewer, element): """ Returns True is the element is collapsible, otherwise False. """ return True def is_expandable(self, viewer, node): """ Returns True is the node is expandanble, otherwise False. """ return True pyface-7.4.0/pyface/viewer/api.py0000644000076500000240000000337214176222673017644 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 ``pyface.viewer`` subpacakge. - :class:`~.ColumnProvider` - :class:`~.ContentProvider` - :class:`~.ContentViewer` - :class:`~.DefaultTreeContentProvider` - :class:`~.LabelProvider` - :class:`~.TableColumnProvider` - :class:`~.TableContentProvider` - :class:`~.TableLabelProvider` - :class:`~.TreeContentProvider` - :class:`~.TreeLabelProvider` - :class:`~.TreeItem` - :class:`~.Viewer` - :class:`~.ViewerFilter` - :class:`~.ViewerSorter` Note that the following classes are only available in the Wx toolkit at the moment. - :class:`~.TableViewer`. - :class:`~.TreeViewer`. """ from .column_provider import ColumnProvider from .content_provider import ContentProvider from .content_viewer import ContentViewer from .default_tree_content_provider import DefaultTreeContentProvider from .label_provider import LabelProvider from .table_column_provider import TableColumnProvider from .table_content_provider import TableContentProvider from .table_label_provider import TableLabelProvider from .tree_content_provider import TreeContentProvider from .tree_label_provider import TreeLabelProvider from .tree_item import TreeItem from .viewer import Viewer from .viewer_filter import ViewerFilter from .viewer_sorter import ViewerSorter # these are only implemented in wx at the moment from .table_viewer import TableViewer from .tree_viewer import TreeViewer pyface-7.4.0/pyface/viewer/default_tree_content_provider.py0000644000076500000240000000447014176222673025202 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 default tree content provider. """ from .tree_content_provider import TreeContentProvider from .tree_item import TreeItem class DefaultTreeContentProvider(TreeContentProvider): """ The default tree content provider. """ # ------------------------------------------------------------------------ # 'TreeContentProvider' interface. # ------------------------------------------------------------------------ def get_parent(self, item): """ Returns the parent of an item. """ return item.parent def get_children(self, item): """ Returns the children of an item. """ return item.children def has_children(self, item): """ True iff the item has children. """ return item.has_children # ------------------------------------------------------------------------ # 'DefaultTreeContentProvider' interface. # ------------------------------------------------------------------------ def append(self, parent, child): """ Appends 'child' to the 'parent' item. """ return self.insert(parent, len(parent.children), child) def insert_before(self, parent, before, child): """ Inserts 'child' into 'parent' item before 'before'. """ index, child = parent.insert_before(before, child) return (index, child) def insert(self, parent, index, child): """ Inserts 'child' into the 'parent' item at 'index'. """ parent.insert(index, child) return child def remove(self, parent, child): """ Removes 'child' from the 'parent' item. """ parent.remove(child) return child # ------------------------------------------------------------------------ # Protected interface. # ------------------------------------------------------------------------ def _create_item(self, **kw): """ Creates a new item. """ return TreeItem(**kw) pyface-7.4.0/pyface/viewer/table_label_provider.py0000644000076500000240000000226614176222673023234 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Base class for all table label providers. """ from .label_provider import LabelProvider class TableLabelProvider(LabelProvider): """ Base class for all table label providers. By default an item has no icon, and 'str' is used to generate its label. """ # ------------------------------------------------------------------------ # 'TableLabelProvider' interface. # ------------------------------------------------------------------------ def get_image(self, viewer, element, column_index=0): """ Returns the filename of the label image for an element. """ return None def get_text(self, viewer, element, column_index=0): """ Returns the label text for an element. """ return "%s column %d" % (str(element), column_index) pyface-7.4.0/pyface/viewer/tree_content_provider.py0000644000076500000240000000367114176222673023500 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Abstract base class for tree content providers. """ from .content_provider import ContentProvider class TreeContentProvider(ContentProvider): """ Abstract base class for tree content providers. Tree content providers are used by (surprise, surprise) tree viewers! """ # ------------------------------------------------------------------------ # 'ContentProvider' interface. # ------------------------------------------------------------------------ def get_elements(self, element): """ Returns a list of the elements to display in a viewer. Returns a list of elements to display in a viewer when its (ie. the viewer's) input is set to the given element. The returned list should not be modified by the viewer. """ return self.get_children(element) # ------------------------------------------------------------------------ # 'TreeContentProvider' interface. # ------------------------------------------------------------------------ def get_parent(self, element): """ Returns the parent of an element. Returns None if the element either has no parent (ie. it is the root of the tree), or if the parent cannot be computed. """ return None def get_children(self, element): """ Returns the children of an element. """ raise NotImplementedError() def has_children(self, element): """ Returns True iff the element has children, otherwise False. """ raise NotImplementedError() pyface-7.4.0/pyface/viewer/content_provider.py0000644000076500000240000000215114176222673022451 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Abstract base class for content providers. """ from traits.api import HasTraits class ContentProvider(HasTraits): """ Abstract base class for content providers. """ # ------------------------------------------------------------------------ # 'ContentProvider' interface. # ------------------------------------------------------------------------ def get_elements(self, element): """ Returns a list of the elements to display in a viewer. Returns a list of elements to display in a viewer when its (ie. the viewer's) input is set to the given element. The returned list should not be modified by the viewer. """ raise NotImplementedError() pyface-7.4.0/pyface/i_image_cache.py0000644000076500000240000000452114176222673020304 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 interface for an image cache. """ from traits.api import HasTraits, Interface class IImageCache(Interface): """ The interface for an image cache. """ # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, width, height): """ Creates a new image cache for images of the given size. Parameters ---------- width : int The width of the images in pixels height : int The height of the images in pixels """ # ------------------------------------------------------------------------ # 'ImageCache' interface. # ------------------------------------------------------------------------ def get_image(self, filename): """ Returns the scaled image specified. Parameters ---------- filename : str The name of the file containing the image. Returns ------- scaled : toolkit image The image referred to in the file, scaled to the cache's width and height. """ # FIXME v3: The need to distinguish between bitmaps and images is toolkit # specific so, strictly speaking, the conversion to a bitmap should be done # wherever the toolkit actually needs it. def get_bitmap(self, filename): """ Returns the scaled image specified as a bitmap. Parameters ---------- filename : str The name of the file containing the image. Returns ------- scaled : toolkit bitmap The image referred to in the file, scaled to the cache's width and height, as a bitmap. """ class MImageCache(HasTraits): """ The mixin class that contains common code for toolkit specific implementations of the IImageCache interface. """ pyface-7.4.0/pyface/gui_application.py0000644000076500000240000002322614176222673020741 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 a :py:class:`GUIApplication` subclass of :py:class:`pyface.application.Application`. This adds cross-platform GUI application support to the base class via :py:class:`pyface.application.GUI`. At a minimum this class expects to be provided with a factory that returns :py:class:`pyface.i_window.IWindow` instances. For pure Pyface applications this is most likely to be a subclass of :py:class:`pyface.application_window.ApplicationWindow`. """ import logging from traits.api import ( Bool, Callable, Instance, List, ReadOnly, Tuple, Undefined, Vetoable, observe, ) from .application import Application from .i_dialog import IDialog from .i_splash_screen import ISplashScreen from .i_window import IWindow from .ui_traits import Image logger = logging.getLogger(__name__) def default_window_factory(application, **kwargs): """ The default window factory returns an application window. This is almost never the right thing, but allows users to get off the ground with the base class. """ from pyface.application_window import ApplicationWindow return ApplicationWindow(**kwargs) class GUIApplication(Application): """ A basic Pyface GUI application. """ # 'GUIApplication' traits ------------------------------------------------- # Branding --------------------------------------------------------------- #: The splash screen for the application. No splash screen by default splash_screen = Instance(ISplashScreen) #: The about dialog for the application. about_dialog = Instance(IDialog) #: Icon for the application (used in window titlebars) icon = Image #: Logo of the application (used in splash screens and about dialogs) logo = Image # Window management ------------------------------------------------------ #: The window factory to use when creating a window for the application. window_factory = Callable(default_window_factory) #: Default window size window_size = Tuple((800, 600)) #: Currently active Window if any active_window = Instance(IWindow) #: List of all open windows in the application windows = List(Instance(IWindow)) #: The Pyface GUI instance for the application gui = ReadOnly # Protected interface ---------------------------------------------------- #: Flag if the exiting of the application was explicitely requested by user # An 'explicit' exit is when the 'exit' method is called. # An 'implicit' exit is when the user closes the last open window. _explicit_exit = Bool(False) # ------------------------------------------------------------------------- # 'GUIApplication' interface # ------------------------------------------------------------------------- # Window lifecycle methods ----------------------------------------------- def create_window(self, **kwargs): """ Create a new application window. By default uses the :py:attr:`window_factory` to do this. Subclasses can override if they want to do something different or additional. Parameters ---------- **kwargs : dict Additional keyword arguments to pass to the window factory. Returns ------- window : IWindow instance or None The new IWindow instance. """ window = self.window_factory(application=self, **kwargs) if window.size == (-1, -1): window.size = self.window_size if not window.title: window.title = self.name if self.icon: window.icon = self.icon return window def add_window(self, window): """ Add a new window to the windows we are tracking. """ # Keep a handle on all windows created so that non-active windows don't # get garbage collected self.windows.append(window) # Something might try to veto the opening of the window. opened = window.open() if opened: window.activate() # Action handlers -------------------------------------------------------- def do_about(self): """ Display the about dialog, if it exists. """ if self.about_dialog is not None: self.about_dialog.open() # ------------------------------------------------------------------------- # 'Application' interface # ------------------------------------------------------------------------- def start(self): """ Start the application, setting up things that are required Subclasses should open at least one ApplicationWindow or subclass in their start method, and should call the superclass start() method before doing any work themselves. """ from pyface.gui import GUI ok = super().start() if ok: # create the GUI so that the splash screen comes up first thing if self.gui is Undefined: self.gui = GUI(splash_screen=self.splash_screen) # create the initial windows to show self._create_windows() return ok # ------------------------------------------------------------------------- # 'GUIApplication' Private interface # ------------------------------------------------------------------------- def _create_windows(self): """ Create the initial windows to display. By default calls :py:meth:`create_window` once. Subclasses can override this method. """ window = self.create_window() self.add_window(window) # ------------------------------------------------------------------------- # 'Application' private interface # ------------------------------------------------------------------------- def _run(self): """ Actual implementation of running the application: starting the GUI event loop. """ # Fire a notification that the app is running. This is guaranteed to # happen after all initialization has occurred and the event loop has # started. A listener for this event is a good place to do things # where you want the event loop running. self.gui.invoke_later( self._fire_application_event, "application_initialized" ) # start the GUI - script blocks here self.gui.start_event_loop() return True # Destruction methods ----------------------------------------------------- def _can_exit(self): """ Check with each window to see if it can be closed The fires closing events for each window, and returns False if any listener vetos. """ if not super()._can_exit(): return False for window in reversed(self.windows): window.closing = event = Vetoable() if event.veto: return False else: return True def _prepare_exit(self): """ Close each window """ # ensure copy of list, as we modify original list while closing for window in list(reversed(self.windows)): window.destroy() window.closed = window def _exit(self): """ Shut down the event loop """ self.gui.stop_event_loop() # Trait default handlers ------------------------------------------------ def _window_factory_default(self): """ Default to ApplicationWindow This is almost never the right thing, but allows users to get off the ground with the base class. """ from pyface.application_window import ApplicationWindow return lambda application, **kwargs: ApplicationWindow(**kwargs) def _splash_screen_default(self): """ Default SplashScreen """ from pyface.splash_screen import SplashScreen dialog = SplashScreen() if self.logo: dialog.image = self.logo return dialog def _about_dialog_default(self): """ Default AboutDialog """ from html import escape from pyface.about_dialog import AboutDialog additions = [ "

{}

".format(escape(self.name)), "Copyright © 2022 {}, all rights reserved".format( escape(self.company) ), "", ] additions += [escape(line) for line in self.description.split("\n\n")] dialog = AboutDialog( title="About {}".format(self.name), additions=additions ) if self.logo: dialog.image = self.logo return dialog # Trait listeners -------------------------------------------------------- @observe("windows:items:activated") def _on_activate_window(self, event): """ Listener that tracks currently active window. """ window = event.object if window in self.windows: self.active_window = window @observe("windows:items:deactivated") def _on_deactivate_window(self, event): """ Listener that tracks currently active window. """ self.active_window = None @observe("windows:items:closed") def _on_window_closed(self, event): """ Listener that ensures window handles are released when closed. """ window = event.object if window in self.windows: self.windows.remove(window) pyface-7.4.0/pyface/i_python_shell.py0000644000076500000240000001004214176222673020602 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 interface for an interactive Python shell. """ from traits.api import Event, HasTraits from pyface.key_pressed_event import KeyPressedEvent from pyface.i_layout_widget import ILayoutWidget class IPythonShell(ILayoutWidget): """ The interface for an interactive Python shell. """ # 'IPythonShell' interface --------------------------------------------- #: A command has been executed. command_executed = Event() #: A key has been pressed. key_pressed = Event(KeyPressedEvent) # ------------------------------------------------------------------------ # 'IPythonShell' interface. # ------------------------------------------------------------------------ def interpreter(self): """ Get the shell's interpreter Returns ------- interpreter : InteractiveInterpreter instance Returns the InteractiveInterpreter instance. """ def bind(self, name, value): """ Binds a name to a value in the interpreter's namespace. Parameters ---------- name : str The python idetifier to bind the value to. value : any The python object to be bound into the interpreter's namespace. """ def execute_command(self, command, hidden=True): """ Execute a command in the interpreter. Parameters ---------- command : str A Python command to execute. hidden : bool If 'hidden' is True then nothing is shown in the shell - not even a blank line. """ def execute_file(self, path, hidden=True): """ Execute a file in the interpeter. Parameters ---------- path : str The path to the Python file to execute. hidden : bool If 'hidden' is True then nothing is shown in the shell - not even a blank line. """ def get_history(self): """ Return the current command history and index. Returns ------- history : list of str The list of commands in the new history. history_index : int from 0 to len(history) The current item in the command history navigation. """ def set_history(self, history, history_index): """ Replace the current command history and index with new ones. Parameters ---------- history : list of str The list of commands in the new history. history_index : int The current item in the command history navigation. """ class MPythonShell(HasTraits): """ The mixin class that contains common code for toolkit specific implementations of the IPythonShell interface. Implements: bind(), _on_command_executed() """ # ------------------------------------------------------------------------ # 'IPythonShell' interface. # ------------------------------------------------------------------------ def bind(self, name, value): """ Binds a name to a value in the interpreter's namespace. Parameters ---------- name : str The python idetifier to bind the value to. value : any The python object to be bound into the interpreter's namespace. """ self.interpreter().locals[name] = value # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _on_command_executed(self): """ Called when a command has been executed in the shell. """ self.command_executed = self pyface-7.4.0/pyface/drop_handler.py0000644000076500000240000000424414176222673020232 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.i_drop_handler import IDropHandler from traits.api import Callable, HasTraits, List, provides, Str @provides(IDropHandler) class BaseDropHandler(HasTraits): """ Basic drop handler """ # BaseDropHandler interface --------------------------------------------- #: Returns True if the current drop handler can handle the given drag event #: occurring on the given target widget. on_can_handle = Callable #: Performs drop action when drop event occurs on target widget. on_handle = Callable # IDropHandler interface ------------------------------------------------ def can_handle_drop(self, event, target): return self.on_can_handle(event, target) def handle_drop(self, event, target): return self.on_handle(event, target) @provides(IDropHandler) class FileDropHandler(HasTraits): """ Class to handle backward compatible file drop events """ # FileDropHandler interface --------------------------------------------- #: supported extensions extensions = List(Str) #: Called when file is opened. Takes single argument: path of file open_file = Callable # IDropHandler interface ------------------------------------------------ def can_handle_drop(self, event, target): """ Does the drop event contails file data with matching extensions """ if event.mimeData().hasUrls(): for url in event.mimeData().urls(): file_path = url.toLocalFile() if file_path.endswith(tuple(self.extensions)): return True return False def handle_drop(self, event, target): """ Open the file using the supplied callback """ for url in event.mimeData().urls(): self.open_file(url.toLocalFile()) pyface-7.4.0/pyface/workbench/0000755000076500000240000000000014176460551017174 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/workbench/perspective.py0000755000076500000240000001447514176222673022116 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 default perspective. """ import logging from traits.api import Bool, HasTraits, List, provides, Str, Tuple from .i_perspective import IPerspective from .perspective_item import PerspectiveItem # Logging. logger = logging.getLogger(__name__) @provides(IPerspective) class Perspective(HasTraits): """ The default perspective. """ # The ID of the default perspective. DEFAULT_ID = "pyface.workbench.default" # The name of the default perspective. DEFAULT_NAME = "Default" # 'IPerspective' interface --------------------------------------------- # The perspective's unique identifier (unique within a workbench window). id = Str(DEFAULT_ID) # The perspective's name. name = Str(DEFAULT_NAME) # The contents of the perspective. contents = List(PerspectiveItem) # The size of the editor area in this perspective. A value of (-1, -1) # indicates that the workbench window should choose an appropriate size # based on the sizes of the views in the perspective. editor_area_size = Tuple((-1, -1)) # Is the perspective enabled? enabled = Bool(True) # Should the editor area be shown in this perspective? show_editor_area = Bool(True) # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __str__(self): """ Return an informal string representation of the object. """ return "Perspective(%s)" % self.id # ------------------------------------------------------------------------ # 'Perspective' interface. # ------------------------------------------------------------------------ # Initializers --------------------------------------------------------- def _id_default(self): """ Trait initializer. """ # If no Id is specified then use the name. return self.name # Methods -------------------------------------------------------------# def create(self, window): """ Create the perspective in a workbench window. For most cases you should just be able to set the 'contents' trait to lay out views as required. However, you can override this method if you want to have complete control over how the perspective is created. """ # Set the size of the editor area. if self.editor_area_size != (-1, -1): window.editor_area_size = self.editor_area_size # If the perspective has specific contents then add just those. if len(self.contents) > 0: self._add_contents(window, self.contents) # Otherwise, add all of the views defined in the window at their # default positions realtive to the editor area. else: self._add_all(window) # Activate the first view in every region. window.reset_views() def show(self, window): """ Called when the perspective is shown in a workbench window. The default implementation does nothing, but you can override this method if you want to do something whenever the perspective is activated. """ return # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _add_contents(self, window, contents): """ Adds the specified contents. """ # If we are adding specific contents then we ignore any default view # visibility. # # fixme: This is a bit ugly! Why don't we pass the visibility in to # 'window.add_view'? for view in window.views: view.visible = False for item in contents: self._add_perspective_item(window, item) def _add_perspective_item(self, window, item): """ Adds a perspective item to a window. """ # If no 'relative_to' is specified then the view is positioned # relative to the editor area. if len(item.relative_to) > 0: relative_to = window.get_view_by_id(item.relative_to) else: relative_to = None # fixme: This seems a bit ugly, having to reach back up to the # window to get the view. Maybe its not that bad? view = window.get_view_by_id(item.id) if view is not None: # fixme: This is probably not the ideal way to sync view traits # and perspective_item traits. view.style_hint = item.style_hint # Add the view to the window. window.add_view( view, item.position, relative_to, (item.width, item.height) ) else: # The reason that we don't just barf here is that a perspective # might use views from multiple plugins, and we probably want to # continue even if one or two of them aren't present. # # fixme: This is worth keeping an eye on though. If we end up with # a strict mode that throws exceptions early and often for # developers, then this might be a good place to throw one ;^) logger.error("missing view for perspective item <%s>" % item.id) def _add_all(self, window): """ Adds *all* of the window's views defined in the window. """ for view in window.views: if view.visible: self._add_view(window, view) def _add_view(self, window, view): """ Adds a view to a window. """ # If no 'relative_to' is specified then the view is positioned # relative to the editor area. if len(view.relative_to) > 0: relative_to = window.get_view_by_id(view.relative_to) else: relative_to = None # Add the view to the window. window.add_view( view, view.position, relative_to, (view.width, view.height) ) return pyface-7.4.0/pyface/workbench/window_event.py0000644000076500000240000000155314176222673022263 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Window events. """ from traits.api import HasTraits, Instance, Vetoable from .workbench_window import WorkbenchWindow class WindowEvent(HasTraits): """ A window lifecycle event. """ # 'WindowEvent' interface ---------------------------------------------# # The window that the event occurred on. window = Instance(WorkbenchWindow) class VetoableWindowEvent(WindowEvent, Vetoable): """ A vetoable window lifecycle event. """ pass pyface-7.4.0/pyface/workbench/i_editor.py0000755000076500000240000001021614176222673021350 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 interface of a workbench editor. """ import uuid from traits.api import ( Any, Bool, Event, VetoableEvent, Vetoable, Instance, ) from traits.api import provides from .i_workbench_part import IWorkbenchPart, MWorkbenchPart class IEditor(IWorkbenchPart): """ The interface of a workbench editor. """ # The optional command stack. command_stack = Instance("pyface.undo.api.ICommandStack") # Is the object that the editor is editing 'dirty' i.e., has it been # modified but not saved? dirty = Bool(False) # The object that the editor is editing. # # The framework sets this when the editor is created. obj = Any() # Editor Lifecycle Events ---------------------------------------------# # Fired when the editor is closing. closing = VetoableEvent() # Fired when the editor is closed. closed = Event() # Methods -------------------------------------------------------------# def close(self): """ Close the editor. This method is not currently called by the framework itself as the user is normally in control of the editor lifecycle. Call this if you want to control the editor lifecycle programmatically. """ @provides(IEditor) class MEditor(MWorkbenchPart): """ Mixin containing common code for toolkit-specific implementations. """ # 'IEditor' interface -------------------------------------------------# # The optional command stack. command_stack = Instance("pyface.undo.api.ICommandStack") # Is the object that the editor is editing 'dirty' i.e., has it been # modified but not saved? dirty = Bool(False) # The object that the editor is editing. # # The framework sets this when the editor is created. obj = Any() # Editor Lifecycle Events ---------------------------------------------# # Fired when the editor is opening. opening = VetoableEvent() # Fired when the editor has been opened. open = Event() # Fired when the editor is closing. closing = Event(VetoableEvent) # Fired when the editor is closed. closed = Event() # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __str__(self): """ Return an informal string representation of the object. """ return "Editor(%s)" % self.id # ------------------------------------------------------------------------ # 'IWorkbenchPart' interface. # ------------------------------------------------------------------------ def _id_default(self): """ Trait initializer. """ # If no Id is specified then use a random uuid # this gaurantees (barring *really* unusual cases) that there are no # collisions between the ids of editors. return uuid.uuid4().hex # ------------------------------------------------------------------------ # 'IEditor' interface. # ------------------------------------------------------------------------ def close(self): """ Close the editor. """ if self.control is not None: self.closing = event = Vetoable() if not event.veto: self.window.close_editor(self) self.closed = True return # Initializers --------------------------------------------------------- def _command_stack_default(self): """ Trait initializer. """ # We make sure the undo package is entirely optional. try: from pyface.undo.api import CommandStack except ImportError: return None return CommandStack(undo_manager=self.window.workbench.undo_manager) pyface-7.4.0/pyface/workbench/i_perspective.py0000755000076500000240000000273714176222673022424 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 perspective interface. """ from traits.api import Bool, Interface, List, Str, Tuple from .perspective_item import PerspectiveItem class IPerspective(Interface): """ The perspective interface. """ # The perspective's unique identifier (unique within a workbench window). id = Str() # The perspective's name. name = Str() # The contents of the perspective. contents = List(PerspectiveItem) # The size of the editor area in this perspective. A value of (-1, -1) # indicates that the workbench window should choose an appropriate size # based on the sizes of the views in the perspective. editor_area_size = Tuple() # Is the perspective enabled? enabled = Bool() # Should the editor area be shown in this perspective? show_editor_area = Bool() # Methods -------------------------------------------------------------# def create(self, window): """ Create the perspective in a workbench window. """ def show(self, window): """ Called when the perspective is shown in a workbench window. """ pyface-7.4.0/pyface/workbench/perspective_item.py0000755000076500000240000000440414176222673023123 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ An item in a Perspective contents list. """ from traits.api import Enum, Float, HasTraits, provides, Str from .i_perspective_item import IPerspectiveItem @provides(IPerspectiveItem) class PerspectiveItem(HasTraits): """ An item in a Perspective contents list. """ # The Id of the view to display in the perspective. id = Str() # The position of the view relative to the item specified in the # 'relative_to' trait. # # 'top' puts the view above the 'relative_to' item. # 'bottom' puts the view below the 'relative_to' item. # 'left' puts the view to the left of the 'relative_to' item. # 'right' puts the view to the right of the 'relative_to' item. # 'with' puts the view in the same region as the 'relative_to' item. # # If the position is specified as 'with' you must specify a 'relative_to' # item other than the editor area (i.e., you cannot position a view 'with' # the editor area). position = Enum("left", "top", "bottom", "right", "with") # The Id of the view to position relative to. If this is not specified # (or if no view exists with this Id) then the view will be placed relative # to the editor area. relative_to = Str() # The width of the item (as a fraction of the window width). # # e.g. 0.5 == half the window width. # # Note that this is treated as a suggestion, and it may not be possible # for the workbench to allocate the space requested. width = Float(-1) # The height of the item (as a fraction of the window height). # # e.g. 0.5 == half the window height. # # Note that this is treated as a suggestion, and it may not be possible # for the workbench to allocate the space requested. height = Float(-1) # The style of the dock control created. style_hint = Enum("tab", "vertical", "horizontal", "fixed") pyface-7.4.0/pyface/workbench/tests/0000755000076500000240000000000014176460551020336 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/workbench/tests/__init__.py0000644000076500000240000000000014176222673022436 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/workbench/tests/test_workbench_window.py0000644000076500000240000001616514176222673025332 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 tempfile import shutil import unittest from unittest import mock from traits.testing.api import UnittestTools from pyface.workbench.perspective import Perspective from pyface.workbench.api import Workbench from pyface.workbench.workbench_window import ( WorkbenchWindow, WorkbenchWindowLayout, WorkbenchWindowMemento, ) class TestWorkbenchWindowUserPerspective(unittest.TestCase, UnittestTools): def setUp(self): # A perspective with show_editor_area switched on self.with_editor = Perspective( show_editor_area=True, id="test_id", name="test_name" ) # A perspective with show_editor_area switched off self.without_editor = Perspective( show_editor_area=False, id="test_id2", name="test_name2" ) # Where the state file should be saved self.state_location = tempfile.mkdtemp(dir="./") # Make sure the temporary directory is removed self.addCleanup(self.rm_tempdir) def rm_tempdir(self): shutil.rmtree(self.state_location) def get_workbench_with_window(self): workbench = Workbench() workbench_window = WorkbenchWindow() workbench.windows = [workbench_window] # Saved perspectives should go to the temporary directory workbench.state_location = self.state_location # Mock the layout for the workbench window workbench_window.layout = mock.MagicMock(spec=WorkbenchWindowLayout) workbench_window.layout.window = workbench_window return workbench, workbench_window def show_perspective(self, workbench_window, perspective): workbench_window.active_perspective = perspective workbench_window.layout.is_editor_area_visible = mock.MagicMock( return_value=perspective.show_editor_area ) def test_editor_area_with_perspectives(self): """ Test show_editor_area is respected while switching perspective""" # The workbench and workbench window with layout mocked workbench, workbench_window = self.get_workbench_with_window() workbench.active_window = workbench_window # Add perspectives workbench.user_perspective_manager.add(self.with_editor) workbench.user_perspective_manager.add(self.without_editor) # There are the methods we want to test if they are called workbench_window.show_editor_area = mock.MagicMock() workbench_window.hide_editor_area = mock.MagicMock() # Mock more things for initialing the Workbench Window workbench_window._memento = WorkbenchWindowMemento() workbench_window._initial_layout = workbench_window._memento # Show a perspective with an editor area self.show_perspective(workbench_window, self.with_editor) # show_editor_area should be called self.assertTrue(workbench_window.show_editor_area.called) # Show a perspective withOUT an editor area workbench_window.hide_editor_area.reset_mock() self.show_perspective(workbench_window, self.without_editor) # hide_editor_area should be called self.assertTrue(workbench_window.hide_editor_area.called) # The with_editor has been seen so this will be restored from the memento workbench_window.show_editor_area.reset_mock() self.show_perspective(workbench_window, self.with_editor) # show_editor_area should be called self.assertTrue(workbench_window.show_editor_area.called) def test_editor_area_restore_from_saved_state(self): """ Test if show_editor_area is restored properly from saved state """ # The workbench and workbench window with layout mocked workbench, workbench_window = self.get_workbench_with_window() workbench.active_window = workbench_window # Add perspectives workbench.user_perspective_manager.add(self.with_editor) workbench.user_perspective_manager.add(self.without_editor) # Mock for initialising the workbench window workbench_window._memento = WorkbenchWindowMemento() workbench_window._initial_layout = workbench_window._memento # Mock layout functions for pickling # We only care about show_editor_area and not the layout in this test layout_functions = { "get_view_memento.return_value": (0, (None, None)), "get_editor_memento.return_value": (0, (None, None)), "get_toolkit_memento.return_value": (0, dict(geometry="")), } workbench_window.layout.configure_mock(**layout_functions) # The following records perspective mementos to workbench_window._memento self.show_perspective(workbench_window, self.without_editor) self.show_perspective(workbench_window, self.with_editor) # Save the window layout to a state file workbench._save_window_layout(workbench_window) # We only needed the state file for this test del workbench_window del workbench # We create another workbench which uses the state location # and we test if we can retore the saved perspective correctly workbench, workbench_window = self.get_workbench_with_window() # Mock window factory since we already created a workbench window workbench.window_factory = mock.MagicMock( return_value=workbench_window ) # There are the methods we want to test if they are called workbench_window.show_editor_area = mock.MagicMock() workbench_window.hide_editor_area = mock.MagicMock() # This restores the perspectives and mementos workbench.create_window() # Create contents workbench_window._create_contents(mock.Mock()) # Perspective mementos should be restored self.assertIn( self.with_editor.id, workbench_window._memento.perspective_mementos ) self.assertIn( self.without_editor.id, workbench_window._memento.perspective_mementos, ) # Since the with_editor perspective is used last, # it should be used as initial perspective self.assertTrue(workbench_window.show_editor_area.called) # Try restoring the perspective without editor # The restored perspectives are not the same instance as before # We need to get them using their id perspective_without_editor = workbench_window.get_perspective_by_id( self.without_editor.id ) # Show the perspective with editor area workbench_window.hide_editor_area.reset_mock() self.show_perspective(workbench_window, perspective_without_editor) # make sure hide_editor_area is called self.assertTrue(workbench_window.hide_editor_area.called) pyface-7.4.0/pyface/workbench/workbench_window.py0000755000076500000240000006625014176222673023134 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 workbench window. """ import logging from pyface.api import ApplicationWindow, GUI from traits.api import ( Constant, Delegate, Instance, List, Str, Tuple, Undefined, Vetoable, observe, ) from .i_editor import IEditor from .i_editor_manager import IEditorManager from .i_perspective import IPerspective from .i_view import IView from .i_workbench_part import IWorkbenchPart from .perspective import Perspective from .workbench_window_layout import WorkbenchWindowLayout from .workbench_window_memento import WorkbenchWindowMemento # Logging. logger = logging.getLogger(__name__) class WorkbenchWindow(ApplicationWindow): """ A workbench window. """ # 'IWorkbenchWindow' interface ----------------------------------------- # The view or editor that currently has the focus. active_part = Instance(IWorkbenchPart) # The editor manager is used to create/restore editors. editor_manager = Instance(IEditorManager) # The current selection within the window. selection = List() # The workbench that the window belongs to. workbench = Instance("pyface.workbench.api.IWorkbench") # Editors ----------------------- # The active editor. active_editor = Instance(IEditor) # The visible (open) editors. editors = List(IEditor) # The Id of the editor area. editor_area_id = Constant("pyface.workbench.editors") # The (initial) size of the editor area (the user is free to resize it of # course). editor_area_size = Tuple((100, 100)) # Fired when an editor is about to be opened (or restored). editor_opening = Delegate("layout") # Event(IEditor) # Fired when an editor has been opened (or restored). editor_opened = Delegate("layout") # Event(IEditor) # Fired when an editor is about to be closed. editor_closing = Delegate("layout") # Event(IEditor) # Fired when an editor has been closed. editor_closed = Delegate("layout") # Event(IEditor) # Views ------------------------- # The active view. active_view = Instance(IView) # The available views (note that this is *all* of the views, not just those # currently visible). # # Views *cannot* be shared between windows as each view has a reference to # its toolkit-specific control etc. views = List(IView) # Perspectives -----------------# # The active perspective. active_perspective = Instance(IPerspective) # The available perspectives. If no perspectives are specified then the # a single instance of the 'Perspective' class is created. perspectives = List(IPerspective) # The Id of the default perspective. # # There are two situations in which this is used: # # 1. When the window is being created from scratch (i.e., not restored). # # If this is the empty string, then the first perspective in the list of # perspectives is shown (if there are no perspectives then an instance # of the default 'Perspective' class is used). If this is *not* the # empty string then the perspective with this Id is shown. # # 2. When the window is being restored. # # If this is the empty string, then the last perspective that was # visible when the window last closed is shown. If this is not the empty # string then the perspective with this Id is shown. # default_perspective_id = Str() # 'WorkbenchWindow' interface -----------------------------------------# # The window layout is responsible for creating and managing the internal # structure of the window (i.e., it knows how to add and remove views and # editors etc). layout = Instance(WorkbenchWindowLayout) # 'Private' interface -------------------------------------------------# # The state of the window suitable for pickling etc. _memento = Instance(WorkbenchWindowMemento) # ------------------------------------------------------------------------ # 'Window' interface. # ------------------------------------------------------------------------ def open(self): """ Open the window. Overridden to make the 'opening' event vetoable. Return True if the window opened successfully; False if the open event was vetoed. """ logger.debug("window %s opening", self) # Trait notification. self.opening = event = Vetoable() if not event.veto: if self.control is None: self._create() self.show(True) # Trait notification. self.opened = self logger.debug("window %s opened", self) else: logger.debug("window %s open was vetoed", self) # fixme: This is not actually part of the Pyface 'Window' API (but # maybe it should be). We return this to indicate whether the window # actually opened. return self.control is not None def close(self): """ Closes the window. Overridden to make the 'closing' event vetoable. Return True if the window closed successfully (or was not even open!), False if the close event was vetoed. """ logger.debug("window %s closing", self) if self.control is not None: # Trait notification. self.closing = event = Vetoable() # fixme: Hack to mimic vetoable events! if not event.veto: # Give views and editors a chance to cleanup after themselves. self.destroy_views(self.views) self.destroy_editors(self.editors) # Cleanup the window layout (event handlers, etc.) self.layout.close() # Cleanup the toolkit-specific control. self.destroy() # Cleanup our reference to the control so that we can (at least # in theory!) be opened again. self.control = None # Trait notification. self.closed = self logger.debug("window %s closed", self) else: logger.debug("window %s close was vetoed", self) else: logger.debug("window %s is not open", self) # FIXME v3: This is not actually part of the Pyface 'Window' API (but # maybe it should be). We return this to indicate whether the window # actually closed. return self.control is None # ------------------------------------------------------------------------ # Protected 'Window' interface. # ------------------------------------------------------------------------ def _create_contents(self, parent): """ Create and return the window contents. """ # Create the initial window layout. contents = self.layout.create_initial_layout(parent) # Save the initial window layout so that we can reset it when changing # to a perspective that has not been seen yet. self._initial_layout = self.layout.get_view_memento() # Are we creating the window from scratch or restoring it from a # memento? if self._memento is None: self._memento = WorkbenchWindowMemento() else: self._restore_contents() # Set the initial perspective. self.active_perspective = self._get_initial_perspective() return contents # ------------------------------------------------------------------------ # 'WorkbenchWindow' interface. # ------------------------------------------------------------------------ # Initializers --------------------------------------------------------- def _editor_manager_default(self): """ Trait initializer. """ from .editor_manager import EditorManager return EditorManager(window=self) def _layout_default(self): """ Trait initializer. """ return WorkbenchWindowLayout(window=self) # Methods -------------------------------------------------------------# def activate_editor(self, editor): """ Activates an editor. """ self.layout.activate_editor(editor) def activate_view(self, view): """ Activates a view. """ self.layout.activate_view(view) def add_editor(self, editor, title=None): """ Adds an editor. If no title is specified, the editor's name is used. """ if title is None: title = editor.name self.layout.add_editor(editor, title) self.editors.append(editor) def add_view(self, view, position=None, relative_to=None, size=(-1, -1)): """ Adds a view. """ self.layout.add_view(view, position, relative_to, size) # This case allows for views that are created and added dynamically # (i.e. they were not even known about when the window was created). if view not in self.views: self.views.append(view) def close_editor(self, editor): """ Closes an editor. """ self.layout.close_editor(editor) def close_view(self, view): """ Closes a view. fixme: Currently views are never 'closed' in the same sense as an editor is closed. Views are merely hidden. """ self.hide_view(view) def create_editor(self, obj, kind=None): """ Create an editor for an object. Return None if no editor can be created for the object. """ return self.editor_manager.create_editor(self, obj, kind) def destroy_editors(self, editors): """ Destroy a list of editors. """ for editor in editors: if editor.control is not None: editor.destroy_control() def destroy_views(self, views): """ Destroy a list of views. """ for view in views: if view.control is not None: view.destroy_control() def edit(self, obj, kind=None, use_existing=True): """ Edit an object. 'kind' is simply passed through to the window's editor manager to allow it to create a particular kind of editor depending on context etc. If 'use_existing' is True and the object is already being edited in the window then the existing editor will be activated (i.e., given focus, brought to the front, etc.). If 'use_existing' is False, then a new editor will be created even if one already exists. """ if use_existing: # Is the object already being edited in the window? editor = self.get_editor(obj, kind) if editor is not None: # If so, activate the existing editor (i.e., bring it to the # front, give it the focus etc). self.activate_editor(editor) return editor # Otherwise, create an editor for it. editor = self.create_editor(obj, kind) if editor is None: logger.warning("no editor for object %s", obj) self.add_editor(editor) self.activate_editor(editor) return editor def get_editor(self, obj, kind=None): """ Return the editor that is editing an object. Return None if no such editor exists. """ return self.editor_manager.get_editor(self, obj, kind) def get_editor_by_id(self, id): """ Return the editor with the specified Id. Return None if no such editor exists. """ for editor in self.editors: if editor.id == id: break else: editor = None return editor def get_part_by_id(self, id): """ Return the workbench part with the specified Id. Return None if no such part exists. """ return self.get_view_by_id(id) or self.get_editor_by_id(id) def get_perspective_by_id(self, id): """ Return the perspective with the specified Id. Return None if no such perspective exists. """ for perspective in self.perspectives: if perspective.id == id: break else: if id == Perspective.DEFAULT_ID: perspective = Perspective() else: perspective = None return perspective def get_perspective_by_name(self, name): """ Return the perspective with the specified name. Return None if no such perspective exists. """ for perspective in self.perspectives: if perspective.name == name: break else: perspective = None return perspective def get_view_by_id(self, id): """ Return the view with the specified Id. Return None if no such view exists. """ for view in self.views: if view.id == id: break else: view = None return view def hide_editor_area(self): """ Hide the editor area. """ self.layout.hide_editor_area() def hide_view(self, view): """ Hide a view. """ self.layout.hide_view(view) def refresh(self): """ Refresh the window to reflect any changes. """ self.layout.refresh() def reset_active_perspective(self): """ Reset the active perspective back to its original contents. """ perspective = self.active_perspective # If the perspective has been seen before then delete its memento. if perspective.id in self._memento.perspective_mementos: # Remove the perspective's memento. del self._memento.perspective_mementos[perspective.id] # Re-display the perspective (because a memento no longer exists for # the perspective, its 'create_contents' method will be called again). self._show_perspective(perspective, perspective) def reset_all_perspectives(self): """ Reset all perspectives back to their original contents. """ # Remove all perspective mementos (except user perspectives). for id in self._memento.perspective_mementos.keys(): if not id.startswith("__user_perspective"): del self._memento.perspective_mementos[id] # Re-display the active perspective. self._show_perspective( self.active_perspective, self.active_perspective ) def reset_editors(self): """ Activate the first editor in every tab. """ self.layout.reset_editors() def reset_views(self): """ Activate the first view in every tab. """ self.layout.reset_views() def show_editor_area(self): """ Show the editor area. """ self.layout.show_editor_area() def show_view(self, view): """ Show a view. """ # If the view is already in the window layout, but hidden, then just # show it. # # fixme: This is a little gorpy, reaching into the window layout here, # but currently this is the only thing that knows whether or not the # view exists but is hidden. if self.layout.contains_view(view): self.layout.show_view(view) # Otherwise, we have to add the view to the layout. else: self._add_view_in_default_position(view) self.refresh() return # Methods for saving and restoring the layout -------------------------# def get_memento(self): """ Return the state of the window suitable for pickling etc. """ # The size and position of the window. self._memento.size = self.size self._memento.position = self.position # The Id of the active perspective. self._memento.active_perspective_id = self.active_perspective.id # The layout of the active perspective. self._memento.perspective_mementos[self.active_perspective.id] = ( self.layout.get_view_memento(), self.active_view and self.active_view.id or None, self.layout.is_editor_area_visible(), ) # The layout of the editor area. self._memento.editor_area_memento = self.layout.get_editor_memento() # Any extra toolkit-specific data. self._memento.toolkit_data = self.layout.get_toolkit_memento() return self._memento def set_memento(self, memento): """ Restore the state of the window from a memento. """ # All we do here is save a reference to the memento - we don't actually # do anything with it until the window is opened. # # This obviously means that you can't set the memento of a window # that is already open, but I can't see a use case for that anyway! self._memento = memento return # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _add_view_in_default_position(self, view): """ Adds a view in its 'default' position. """ # Is the view in the current perspectives contents list? If it is then # we use the positioning information in the perspective item. Otherwise # we will use the default positioning specified in the view itself. item = self._get_perspective_item(self.active_perspective, view) if item is None: item = view # fixme: This only works because 'PerspectiveItem' and 'View' have the # identical 'position', 'relative_to', 'width' and 'height' traits! We # need to unify these somehow! relative_to = self.get_view_by_id(item.relative_to) size = (item.width, item.height) self.add_view(view, item.position, relative_to, size) def _get_initial_perspective(self, *methods): """ Return the initial perspective. """ methods = [ # If a default perspective was specified then we prefer that over # any other perspective. self._get_default_perspective, # If there was no default perspective then try the perspective that # was active the last time the application was run. self._get_previous_perspective, # If there was no previous perspective, then try the first one that # we know about. self._get_first_perspective, ] for method in methods: perspective = method() if perspective is not None: break # If we have no known perspectives, make a new blank one up. else: logger.warning("no known perspectives - creating a new one") perspective = Perspective() return perspective def _get_default_perspective(self): """ Return the default perspective. Return None if no default perspective was specified or it no longer exists. """ id = self.default_perspective_id if len(id) > 0: perspective = self.get_perspective_by_id(id) if perspective is None: logger.warning("default perspective %s no longer available", id) else: perspective = None return perspective def _get_previous_perspective(self): """ Return the previous perspective. Return None if there has been no previous perspective or it no longer exists. """ id = self._memento.active_perspective_id if len(id) > 0: perspective = self.get_perspective_by_id(id) if perspective is None: logger.warning("previous perspective %s no longer available", id) else: perspective = None return perspective def _get_first_perspective(self): """ Return the first perspective in our list of perspectives. Return None if no perspectives have been defined. """ if len(self.perspectives) > 0: perspective = self.perspectives[0] else: perspective = None return perspective def _get_perspective_item(self, perspective, view): """ Return the perspective item for a view. Return None if the view is not mentioned in the perspectives contents. """ # fixme: Errrr, shouldn't this be a method on the window?!? for item in perspective.contents: if item.id == view.id: break else: item = None return item def _hide_perspective(self, perspective): """ Hide a perspective. """ # fixme: This is a bit ugly but... when we restore the layout we ignore # the default view visibility. for view in self.views: view.visible = False # Save the current layout of the perspective. self._memento.perspective_mementos[perspective.id] = ( self.layout.get_view_memento(), self.active_view and self.active_view.id or None, self.layout.is_editor_area_visible(), ) def _show_perspective(self, old, new): """ Show a perspective. """ # If the perspective has been seen before then restore it. memento = self._memento.perspective_mementos.get(new.id) if memento is not None: # Show the editor area? # We need to set the editor area before setting the views if len(memento) == 2: logger.warning("Restoring perspective from an older version.") editor_area_visible = True else: editor_area_visible = memento[2] # Show the editor area if it is set to be visible if editor_area_visible: self.show_editor_area() else: self.hide_editor_area() self.active_editor = None # Now set the views view_memento, active_view_id = memento[:2] self.layout.set_view_memento(view_memento) # Make sure the active part, view and editor reflect the new # perspective. view = self.get_view_by_id(active_view_id) if view is not None: self.active_view = view # Otherwise, this is the first time the perspective has been seen # so create it. else: if old is not None: # Reset the window layout to its initial state. self.layout.set_view_memento(self._initial_layout) # Create the perspective in the window. new.create(self) # Make sure the active part, view and editor reflect the new # perspective. self.active_view = None # Show the editor area? if new.show_editor_area: self.show_editor_area() else: self.hide_editor_area() self.active_editor = None # Inform the perspective that it has been shown. new.show(self) # This forces the dock window to update its layout. if old is not None: self.refresh() def _restore_contents(self): """ Restore the contents of the window. """ self.layout.set_editor_memento(self._memento.editor_area_memento) self.size = self._memento.size self.position = self._memento.position # Set the toolkit-specific data last because it may override the generic # implementation. # FIXME: The primary use case is to let Qt restore the window's geometry # wholesale, including maximization state. If we ever go Qt-only, this # is a good area to refactor. self.layout.set_toolkit_memento(self._memento) return # Trait change handlers ------------------------------------------------ # Static ---- def _active_perspective_changed(self, old, new): """ Static trait change handler. """ logger.debug("active perspective changed from <%s> to <%s>", old, new) # Hide the old perspective... if old is not None: self._hide_perspective(old) # ... and show the new one. if new is not None: self._show_perspective(old, new) def _active_editor_changed(self, old, new): """ Static trait change handler. """ logger.debug("active editor changed from <%s> to <%s>", old, new) self.active_part = new def _active_part_changed(self, old, new): """ Static trait change handler. """ if new is None: self.selection = [] else: self.selection = new.selection logger.debug("active part changed from <%s> to <%s>", old, new) def _active_view_changed(self, old, new): """ Static trait change handler. """ logger.debug("active view changed from <%s> to <%s>", old, new) self.active_part = new def _views_changed(self, old, new): """ Static trait change handler. """ # Cleanup any old views. for view in old: view.window = None # Initialize any new views. for view in new: view.window = self def _views_items_changed(self, event): """ Static trait change handler. """ # Cleanup any old views. for view in event.removed: view.window = None # Initialize any new views. for view in event.added: view.window = self return # Dynamic ---- @observe("layout:editor_closed") def _on_editor_closed(self, event): """ Dynamic trait change handler. """ if event.new is None or event.new is Undefined: return index = self.editors.index(event.new) del self.editors[index] if event.new is self.active_editor: if len(self.editors) > 0: index = min(index, len(self.editors) - 1) # If the user closed the editor manually then this method is # being called from a toolkit-specific event handler. Because # of that we have to make sure that we don't change the focus # from within this method directly hence we activate the editor # later in the GUI thread. GUI.invoke_later(self.activate_editor, self.editors[index]) else: self.active_editor = None return @observe("editors:items:has_focus") def _on_editor_has_focus_changed(self, event): """ Dynamic trait change handler. """ if event.new: self.active_editor = event.object return @observe("views:items:has_focus") def _has_focus_changed_for_view(self, event): """ Dynamic trait change handler. """ if event.new: self.active_view = event.object return @observe("views:items:visible") def _visible_changed_for_view(self, event): """ Dynamic trait change handler. """ if not event.new: if event.object is self.active_view: self.active_view = None return pyface-7.4.0/pyface/workbench/__init__.py0000644000076500000240000000112114176222673021301 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 warnings.warn( PendingDeprecationWarning( "Workbench will be moved from pyface.workbench to apptools.workbench at a future point" ), stacklevel=2, ) pyface-7.4.0/pyface/workbench/traits_ui_view.py0000644000076500000240000000637714176222673022621 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 view whose content is provided by a traits UI. """ import logging from traits.api import Any, Instance, Str from .view import View # Logging. logger = logging.getLogger(__name__) class TraitsUIView(View): """ A view whose content is provided by a traits UI. """ # 'TraitsUIView' interface --------------------------------------------- # The object that we povide a traits UI of (this defaults to the view # iteself ie. 'self'). obj = Any() # The traits UI that represents the view. # # The framework sets this to the value returned by 'create_ui'. ui = Instance("traitsui.ui.UI") # The name of the traits UI view used to create the UI (if not specified, # the default traits UI view is used). view = Str() # ------------------------------------------------------------------------ # 'IWorkbenchPart' interface. # ------------------------------------------------------------------------ # Trait initializers --------------------------------------------------- def _name_default(self): """ Trait initializer. """ return str(self.obj) # Methods -------------------------------------------------------------# def create_control(self, parent): """ Creates the toolkit-specific control that represents the editor. 'parent' is the toolkit-specific control that is the editor's parent. Overridden to call 'create_ui' to get the traits UI. """ self.ui = self.create_ui(parent) return self.ui.control def destroy_control(self): """ Destroys the toolkit-specific control that represents the editor. Overridden to call 'dispose' on the traits UI. """ # Give the traits UI a chance to clean itself up. if self.ui is not None: logger.debug("disposing traits UI for view [%s]", self) self.ui.dispose() self.ui = None # Break reference to the control, so the view is created afresh # next time. self.control = None return # ------------------------------------------------------------------------ # 'TraitsUIView' interface. # ------------------------------------------------------------------------ # Trait initializers --------------------------------------------------- def _obj_default(self): """ Trait initializer. """ return self # Methods -------------------------------------------------------------# def create_ui(self, parent): """ Creates the traits UI that represents the editor. By default it calls 'edit_traits' on the view's 'model'. If you want more control over the creation of the traits UI then override! """ ui = self.obj.edit_traits( parent=parent, view=self.view, kind="subpanel" ) return ui pyface-7.4.0/pyface/workbench/workbench.py0000755000076500000240000003311714176222673021541 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 workbench. """ import pickle import logging import os from traits.etsconfig.api import ETSConfig from pyface.api import NO from traits.api import Bool, Callable, Event, HasTraits, provides from traits.api import Instance, List, Str, Vetoable from traits.api import VetoableEvent from .i_editor_manager import IEditorManager from .i_workbench import IWorkbench from .user_perspective_manager import UserPerspectiveManager from .workbench_window import WorkbenchWindow from .window_event import WindowEvent, VetoableWindowEvent # Logging. logger = logging.getLogger(__name__) @provides(IWorkbench) class Workbench(HasTraits): """ A workbench. There is exactly *one* workbench per application. The workbench can create any number of workbench windows. """ # 'IWorkbench' interface ----------------------------------------------- # The active workbench window (the last one to get focus). active_window = Instance(WorkbenchWindow) # The editor manager is used to create/restore editors. editor_manager = Instance(IEditorManager) # The optional application scripting manager. script_manager = Instance("apptools.appscripting.api.IScriptManager") # A directory on the local file system that we can read and write to at # will. This is used to persist window layout information, etc. state_location = Str() # The optional undo manager. undo_manager = Instance("pyface.undo.api.IUndoManager") # The user-defined perspectives manager. user_perspective_manager = Instance(UserPerspectiveManager) # All of the workbench windows created by the workbench. windows = List(WorkbenchWindow) # Workbench lifecycle events ------------------------------------------- # Fired when the workbench is about to exit. # # This can be caused by either:- # # a) The 'exit' method being called. # b) The last open window being closed. # exiting = VetoableEvent() # Fired when the workbench has exited. exited = Event() # Window lifecycle events ---------------------------------------------# # Fired when a workbench window has been created. window_created = Event(WindowEvent) # Fired when a workbench window is opening. window_opening = Event(VetoableWindowEvent) # Fired when a workbench window has been opened. window_opened = Event(WindowEvent) # Fired when a workbench window is closing. window_closing = Event(VetoableWindowEvent) # Fired when a workbench window has been closed. window_closed = Event(WindowEvent) # 'Workbench' interface ------------------------------------------------ # The factory that is used to create workbench windows. This is used in # the default implementation of 'create_window'. If you override that # method then you obviously don't need to set this trait! window_factory = Callable # Private interface ---------------------------------------------------- # An 'explicit' exit is when the the 'exit' method is called. # An 'implicit' exit is when the user closes the last open window. _explicit_exit = Bool(False) # ------------------------------------------------------------------------ # 'IWorkbench' interface. # ------------------------------------------------------------------------ def create_window(self, **kw): """ Factory method that creates a new workbench window. """ window = self.window_factory(workbench=self, **kw) # Add on any user-defined perspectives. window.perspectives.extend(self.user_perspective_manager.perspectives) # Restore the saved window memento (if there is one). self._restore_window_layout(window) # Listen for the window being activated/opened/closed etc. Activated in # this context means 'gets the focus'. # # NOTE: 'activated' is not fired on a window when the window first # opens and gets focus. It is only fired when the window comes from # lower in the stack to be the active window. window.observe(self._on_window_activated, "activated") window.observe(self._on_window_opening, "opening") window.observe(self._on_window_opened, "opened") window.observe(self._on_window_closing, "closing") window.observe(self._on_window_closed, "closed") # Event notification. self.window_created = WindowEvent(window=window) return window def exit(self): """ Exits the workbench. This closes all open workbench windows. This method is not called when the user clicks the close icon. Nor when they do an Alt+F4 in Windows. It is only called when the application menu File->Exit item is selected. Returns True if the exit succeeded, False if it was vetoed. """ logger.debug("**** exiting the workbench ****") # Event notification. self.exiting = event = Vetoable() if not event.veto: # This flag is checked in '_on_window_closing' to see what kind of # exit is being performed. self._explicit_exit = True if len(self.windows) > 0: exited = self._close_all_windows() # The degenerate case where no workbench windows have ever been # created! else: # Trait notification. self.exited = self exited = True # Whether the exit succeeded or not, we are no longer in the # process of exiting! self._explicit_exit = False else: exited = False if not exited: logger.debug("**** exit of the workbench vetoed ****") return exited # Convenience methods on the active window ----------------------------- def edit(self, obj, kind=None, use_existing=True): """ Edit an object in the active workbench window. """ return self.active_window.edit(obj, kind, use_existing) def get_editor(self, obj, kind=None): """ Return the editor that is editing an object. Returns None if no such editor exists. """ if self.active_window is None: return None return self.active_window.get_editor(obj, kind) def get_editor_by_id(self, id): """ Return the editor with the specified Id. Returns None if no such editor exists. """ return self.active_window.get_editor_by_id(id) # Message dialogs ---- def confirm(self, message, title=None, cancel=False, default=NO): """ Convenience method to show a confirmation dialog. """ return self.active_window.confirm(message, title, cancel, default) def information(self, message, title="Information"): """ Convenience method to show an information message dialog. """ return self.active_window.information(message, title) def warning(self, message, title="Warning"): """ Convenience method to show a warning message dialog. """ return self.active_window.warning(message, title) def error(self, message, title="Error"): """ Convenience method to show an error message dialog. """ return self.active_window.error(message, title) # ------------------------------------------------------------------------ # 'Workbench' interface. # ------------------------------------------------------------------------ # Initializers --------------------------------------------------------- def _state_location_default(self): """ Trait initializer. """ # It would be preferable to base this on GUI.state_location. state_location = os.path.join( ETSConfig.application_home, "pyface", "workbench", ETSConfig.toolkit, ) if not os.path.exists(state_location): os.makedirs(state_location) logger.debug("workbench state location is %s", state_location) return state_location def _undo_manager_default(self): """ Trait initializer. """ # We make sure the undo package is entirely optional. try: from pyface.undo.api import UndoManager except ImportError: return None return UndoManager() def _user_perspective_manager_default(self): """ Trait initializer. """ return UserPerspectiveManager(state_location=self.state_location) # ------------------------------------------------------------------------ # Protected 'Workbench' interface. # ------------------------------------------------------------------------ def _create_window(self, **kw): """ Factory method that creates a new workbench window. """ raise NotImplementedError() # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _close_all_windows(self): """ Closes all open windows. Returns True if all windows were closed, False if the user changed their mind ;^) """ # We take a copy of the windows list because as windows are closed # they are removed from it! windows = self.windows[:] windows.reverse() for window in windows: # We give the user chance to cancel the exit as each window is # closed. if not window.close(): all_closed = False break else: all_closed = True return all_closed def _restore_window_layout(self, window): """ Restore the window layout. """ filename = os.path.join(self.state_location, "window_memento") if os.path.exists(filename): try: # If the memento class itself has been modified then there # is a chance that the unpickle will fail. If so then we just # carry on as if there was no memento! f = open(filename, "rb") memento = pickle.load(f) f.close() # The memento doesn't actually get used until the window is # opened, so there is nothing to go wrong in this step! window.set_memento(memento) # If *anything* goes wrong then simply log the error and carry on # with no memento! except: logger.exception("restoring window layout from %s", filename) def _save_window_layout(self, window): """ Save the window layout. """ # Save the window layout. f = open(os.path.join(self.state_location, "window_memento"), "wb") pickle.dump(window.get_memento(), f) f.close() return # Trait change handlers ------------------------------------------------ def _on_window_activated(self, event): """ Dynamic trait change handler. """ window = event.object logger.debug("window %s activated", window) self.active_window = window def _on_window_opening(self, event): """ Dynamic trait change handler. """ window = event.object # Event notification. self.window_opening = window_event = VetoableWindowEvent(window=window) if window_event.veto: event.new.veto = True def _on_window_opened(self, event): """ Dynamic trait change handler. """ window = event.object # We maintain a list of all open windows so that (amongst other things) # we can detect when the user is attempting to close the last one. self.windows.append(window) # This is necessary because the activated event is not fired when a # window is first opened and gets focus. It is only fired when the # window comes from lower in the stack to be the active window. self.active_window = window # Event notification. self.window_opened = WindowEvent(window=window) def _on_window_closing(self, event): """ Dynamic trait change handler. """ window = event.object # Event notification. self.window_closing = window_event = VetoableWindowEvent(window=window) if window_event.veto: event.new.veto = True else: # Is this the last open window? if len(self.windows) == 1: # If this is an 'implicit exit' then make sure that we fire the # appropriate workbench lifecycle events. if not self._explicit_exit: # Event notification. self.exiting = window_event = Vetoable() if window_event.veto: event.new.veto = True if not event.new.veto: # Save the window size, position and layout. self._save_window_layout(window) def _on_window_closed(self, event): """ Dynamic trait change handler. """ window = event.object self.windows.remove(window) # Event notification. self.window_closed = WindowEvent(window=window) # Was this the last window? if len(self.windows) == 0: # Event notification. self.exited = self return pyface-7.4.0/pyface/workbench/api.py0000755000076500000240000000164414176222673020330 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 .i_editor import IEditor from .editor import Editor from .i_editor_manager import IEditorManager from .editor_manager import EditorManager from .i_perspective import IPerspective from .perspective import Perspective from .perspective_item import PerspectiveItem from .i_view import IView from .view import View from .i_workbench import IWorkbench from .workbench import Workbench from .workbench_window import WorkbenchWindow from .traits_ui_editor import TraitsUIEditor from .traits_ui_view import TraitsUIView pyface-7.4.0/pyface/workbench/i_workbench_part.py0000644000076500000240000000637014176222673023075 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 interface for workbench parts. """ from traits.api import Any, Bool, HasTraits, Instance, Interface from traits.api import List, provides, Str class IWorkbenchPart(Interface): """ The interface for workbench parts. A workbench part is a visual section within the workbench. There are two sub-types, 'View' and 'Editor'. """ # The toolkit-specific control that represents the part. # # The framework sets this to the value returned by 'create_control'. control = Any() # Does the part currently have the focus? has_focus = Bool(False) # The part's globally unique identifier. id = Str() # The part's name (displayed to the user). name = Str() # The current selection within the part. selection = List() # The workbench window that the part is in. # # The framework sets this when the part is created. window = Instance("pyface.workbench.api.WorkbenchWindow") # Methods -------------------------------------------------------------# def create_control(self, parent): """ Create the toolkit-specific control that represents the part. The parameter *parent* is the toolkit-specific control that is the parts's parent. Return the toolkit-specific control. """ def destroy_control(self): """ Destroy the toolkit-specific control that represents the part. Return None. """ def set_focus(self): """ Set the focus to the appropriate control in the part. Return None. """ @provides(IWorkbenchPart) class MWorkbenchPart(HasTraits): """ Mixin containing common code for toolkit-specific implementations. """ # 'IWorkbenchPart' interface ------------------------------------------- # The toolkit-specific control that represents the part. # # The framework sets this to the value returned by 'create_control'. control = Any() # Does the part currently have the focus? has_focus = Bool(False) # The part's globally unique identifier. id = Str() # The part's name (displayed to the user). name = Str() # The current selection within the part. selection = List() # The workbench window that the part is in. # # The framework sets this when the part is created. window = Instance("pyface.workbench.api.WorkbenchWindow") # Methods -------------------------------------------------------------# def create_control(self, parent): """ Create the toolkit-specific control that represents the part. """ raise NotImplementedError() def destroy_control(self): """ Destroy the toolkit-specific control that represents the part. """ raise NotImplementedError() def set_focus(self): """ Set the focus to the appropriate control in the part. """ raise NotImplementedError() pyface-7.4.0/pyface/workbench/action/0000755000076500000240000000000014176460551020451 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/workbench/action/delete_user_perspective_action.py0000644000076500000240000000524314176222673027276 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ An action that deletes a user perspective. """ from pyface.api import YES from .user_perspective_action import UserPerspectiveAction class DeleteUserPerspectiveAction(UserPerspectiveAction): """ An action that deletes a user perspective. """ # 'Action' interface --------------------------------------------------- # The action's unique identifier (may be None). id = "pyface.workbench.action.delete_user_perspective_action" # The action's name (displayed on menus/tool bar tools etc). name = "Delete Perspective" # ------------------------------------------------------------------------ # 'Action' interface. # ------------------------------------------------------------------------ def perform(self, event): """ Perform the action. """ window = event.window manager = window.workbench.user_perspective_manager # The perspective to delete. perspective = window.active_perspective # Make sure that the user isn't having second thoughts! message = ( 'Are you sure you want to delete the "%s" perspective?' % perspective.name ) answer = window.confirm(message, title="Confirm Delete") if answer == YES: # Set the active perspective to be the first remaining perspective. # # There is always a default NON-user perspective (even if no # perspectives are explicitly defined) so we should never(!) not # be able to find one! window.active_perspective = self._get_next_perspective(window) # Remove the perspective from the window. window.perspectives.remove(perspective) # Remove it from the user perspective manager. manager.remove(perspective.id) return # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _get_next_perspective(self, window): """ Return the first perspective that is not the active one! """ if window.active_perspective is window.perspectives[0]: index = 1 else: index = 0 return window.perspectives[index] pyface-7.4.0/pyface/workbench/action/tool_bar_manager.py0000644000076500000240000000277014176222673024325 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 tool bar manager for the Envisage workbench window. """ import pyface.action.api as pyface from traits.api import Instance from .action_controller import ActionController class ToolBarManager(pyface.ToolBarManager): """ The tool bar manager for the Envisage workbench window. """ # 'ToolBarManager' interface ------------------------------------------- # The workbench window that we are the tool bar manager for. window = Instance("pyface.workbench.api.WorkbenchWindow") # ------------------------------------------------------------------------ # 'ToolBarManager' interface. # ------------------------------------------------------------------------ def create_tool_bar(self, parent, controller=None, **kwargs): """ Creates a tool bar representation of the manager. """ # The controller handles the invocation of every action. if controller is None: controller = ActionController(window=self.window) tool_bar = super().create_tool_bar( parent, controller=controller, **kwargs ) return tool_bar pyface-7.4.0/pyface/workbench/action/workbench_action.py0000644000076500000240000000156414176222673024351 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Abstract base class for all workbench actions. """ from pyface.workbench.api import WorkbenchWindow from pyface.action.api import Action from traits.api import Instance class WorkbenchAction(Action): """ Abstract base class for all workbench actions. """ # 'WorkbenchAction' interface -----------------------------------------# # The workbench window that the action is in. # # This is set by the framework. window = Instance(WorkbenchWindow) pyface-7.4.0/pyface/workbench/action/view_chooser.py0000644000076500000240000001450514176222673023525 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 UI that allows the user to choose a view. """ from pyface.workbench.api import IView, WorkbenchWindow from traits.api import Any, HasTraits, Instance, List, Str from traits.api import TraitError, Undefined from traitsui.api import Item, TreeEditor, TreeNode, View from traitsui.menu import Action # fixme: Non-api import! class Category(HasTraits): """ A view category. """ # The name of the category. name = Str() # The views in the category. views = List() class WorkbenchWindowTreeNode(TreeNode): """ A tree node for workbench windows that displays the window's views. The views are grouped by their category. """ # 'TreeNode' interface ------------------------------------------------- # List of object classes that the node applies to. node_for = [WorkbenchWindow] # ------------------------------------------------------------------------ # 'TreeNode' interface. # ------------------------------------------------------------------------ def get_children(self, object): """ Get the object's children. """ # Collate the window's views into categories. categories_by_name = self._get_categories_by_name(object) categories = list(categories_by_name.values()) categories.sort(key=lambda category: category.name) return categories # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _get_categories_by_name(self, window): """ Return a dictionary containing all categories keyed by name. """ categories_by_name = {} for view in window.views: category = categories_by_name.get(view.category) if category is None: category = Category(name=view.category) categories_by_name[view.category] = category category.views.append(view) return categories_by_name class IViewTreeNode(TreeNode): """ A tree node for objects that implement the 'IView' interface. This node does *not* recognise objects that can be *adapted* to the 'IView' interface, only those that actually implement it. If we wanted to allow for adaptation we would have to work out a way for the rest of the 'TreeNode' code to access the adapter, not the original object. We could, of course override every method, but that seems a little, errr, tedious. We could probably do with something like in the Pyface tree where there is a method that returns the actual object that we want to manipulate. """ def is_node_for(self, obj): """ Returns whether this is the node that handles a specified object. """ # By checking for 'is obj' here, we are *not* allowing adaptation (if # we were allowing adaptation it would be 'is not None'). See the class # doc string for details. return IView(obj, Undefined) is obj def get_icon(self, obj, is_expanded): """ Returns the icon for a specified object. """ if obj.image is not None: icon = obj.image else: # fixme: A bit of magic here! Is there a better way to say 'use # the default leaf icon'? icon = "" return icon class ViewChooser(HasTraits): """ Allow the user to choose a view. This implementation shows views in a tree grouped by category. """ # The window that contains the views to choose from. window = Instance("pyface.workbench.api.WorkbenchWindow") # The currently selected tree item (at any point in time this might be # either None, a view category, or a view). selected = Any() # The selected view (None if the selected item is not a view). view = Instance(IView) # Traits UI views -----------------------------------------------------# traits_ui_view = View( Item( name="window", editor=TreeEditor( nodes=[ WorkbenchWindowTreeNode( auto_open=True, label="=Views", rename=False, copy=False, delete=False, insert=False, menu=None, ), TreeNode( node_for=[Category], auto_open=True, children="views", label="name", rename=False, copy=False, delete=False, insert=False, menu=None, ), IViewTreeNode( auto_open=False, label="name", rename=False, copy=False, delete=False, insert=False, menu=None, ), ], editable=False, hide_root=True, selected="selected", show_icons=True, ), show_label=False, ), buttons=[Action(name="OK", enabled_when="view is not None"), "Cancel"], resizable=True, style="custom", title="Show View", width=0.2, height=0.4, ) # ------------------------------------------------------------------------ # 'ViewChooser' interface. # ------------------------------------------------------------------------ def _selected_changed(self, old, new): """ Static trait change handler. """ # If the assignment fails then the selected object does *not* implement # the 'IView' interface. try: self.view = new except TraitError: self.view = None return pyface-7.4.0/pyface/workbench/action/user_perspective_action.py0000644000076500000240000000364714176222673025762 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 base class for user perspective actions. """ from traits.api import observe from .workbench_action import WorkbenchAction class UserPerspectiveAction(WorkbenchAction): """ The base class for user perspective actions. Instances of this class (or its subclasses ;^) are enabled only when the active perspective is a user perspective. """ # ------------------------------------------------------------------------ # 'Action' interface. # ------------------------------------------------------------------------ def destroy(self): """ Destroy the action. """ # This removes the active perspective listener. self.window = None return # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _is_user_perspective(self, perspective): """ Is the specified perspective a user perspective? """ # fixme: This seems a bit of a smelly way to make the determinaction! id = perspective.id return (id[:19] == "__user_perspective_") and (id[-2:] == "__") @observe("window.active_perspective") def _refresh_enabled(self, event): """ Refresh the enabled state of the action. """ self.enabled = ( self.window is not None and self.window.active_perspective is not None and self._is_user_perspective(self.window.active_perspective) ) return pyface-7.4.0/pyface/workbench/action/rename_user_perspective_action.py0000644000076500000240000000304514176222673027301 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ An action that renames a user perspective. """ from .user_perspective_action import UserPerspectiveAction from .user_perspective_name import UserPerspectiveName class RenameUserPerspectiveAction(UserPerspectiveAction): """ An action that renames a user perspective. """ # 'Action' interface --------------------------------------------------- # The action's unique identifier (may be None). id = "pyface.workbench.action.rename_user_perspective_action" # The action's name (displayed on menus/tool bar tools etc). name = "Rename Perspective..." # ------------------------------------------------------------------------ # 'Action' interface. # ------------------------------------------------------------------------ def perform(self, event): """ Perform the action. """ window = event.window manager = window.workbench.user_perspective_manager # Get the new name. upn = UserPerspectiveName(name=window.active_perspective.name) if upn.edit_traits(view="rename_view").result: manager.rename(window.active_perspective, upn.name.strip()) return pyface-7.4.0/pyface/workbench/action/reset_all_perspectives_action.py0000644000076500000240000000263414176222673027134 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ An action that resets *all* perspectives. """ from pyface.api import YES from .workbench_action import WorkbenchAction # The message used when confirming the action. MESSAGE = "Do you want to reset ALL perspectives to their defaults?" class ResetAllPerspectivesAction(WorkbenchAction): """ An action that resets *all* perspectives. """ # 'Action' interface --------------------------------------------------- # The action's unique identifier (may be None). id = "pyface.workbench.action.reset_all_perspectives" # The action's name (displayed on menus/tool bar tools etc). name = "Reset All Perspectives" # ------------------------------------------------------------------------ # 'Action' interface. # ------------------------------------------------------------------------ def perform(self, event): """ Perform the action. """ window = self.window if window.confirm(MESSAGE) == YES: window.reset_all_perspectives() return pyface-7.4.0/pyface/workbench/action/setattr_action.py0000644000076500000240000000231214176222673024045 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ An action that sets an attribute. """ from traits.api import Any, Str from .workbench_action import WorkbenchAction class SetattrAction(WorkbenchAction): """ An action that sets an attribute. """ # 'SetattrAction' interface -------------------------------------------- # The object that we set the attribute on. obj = Any() # The name of the attribute that we set. attribute_name = Str() # The value that we set the attribute to. value = Any() # ------------------------------------------------------------------------ # 'Action' interface. # ------------------------------------------------------------------------ def perform(self, event): """ Performs the action. """ setattr(self.obj, self.attribute_name, self.value) return pyface-7.4.0/pyface/workbench/action/__init__.py0000755000076500000240000000000014176222673022554 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/workbench/action/show_view_action.py0000644000076500000240000000346514176222673024403 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ An action that shows a dialog to allow the user to choose a view. """ from .view_chooser import ViewChooser from .workbench_action import WorkbenchAction class ShowViewAction(WorkbenchAction): """ An action that shows a dialog to allow the user to choose a view. """ # 'Action' interface --------------------------------------------------- # The action's unique identifier (may be None). id = "pyface.workbench.action.show_view" # The action's name (displayed on menus/tool bar tools etc). name = "Show View" # ------------------------------------------------------------------------ # 'Action' interface. # ------------------------------------------------------------------------ def perform(self, event): """ Perform the action. """ chooser = ViewChooser(window=self.window) ui = chooser.edit_traits(parent=self.window.control, kind="livemodal") # If the user closes the dialog by using the window manager's close button # (e.g. the little [x] in the top corner), ui.result is True, but chooser.view # might be None, so we need an explicit check for that. if ui.result and chooser.view is not None: # This shows the view... chooser.view.show() # ... and this makes it active (brings it to the front, gives it # focus etc). chooser.view.activate() return pyface-7.4.0/pyface/workbench/action/api.py0000644000076500000240000000104214176222673021572 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 .menu_bar_manager import MenuBarManager from .tool_bar_manager import ToolBarManager from .view_menu_manager import ViewMenuManager pyface-7.4.0/pyface/workbench/action/user_perspective_name.py0000644000076500000240000000477114176222673025424 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Object with views for naming or renaming a user perspective. """ from traits.api import Bool, HasTraits, Constant, String from traitsui.api import View, Item, VGroup # Trait definitions -------------------------------------------------------- # Define a trait which can not be the empty string: NotEmptyString = String(minlen=1) class UserPerspectiveName(HasTraits): """ Object with views for naming or renaming a user perspective. """ # ------------------------------------------------------------------------ # 'UserPerspectiveName' interface. # ------------------------------------------------------------------------ # The name of the new user perspective. name = NotEmptyString # Should the editor area be shown in this perpsective? show_editor_area = Bool(True) # Help notes when creating a new view. new_help = Constant( """Note: - The new perspective will initially be empty. - Add new views to the perspective by selecting them from the 'View' menu. - Drag the notebook tabs and splitter bars to arrange the views within the perspective.""" ) # Traits views --------------------------------------------------------- new_view = View( VGroup( VGroup("name", "show_editor_area"), VGroup("_", Item("new_help", style="readonly"), show_labels=False), ), title="New User Perspective", id="envisage.workbench.action." "new_user_perspective_action.UserPerspectiveName", buttons=["OK", "Cancel"], kind="livemodal", width=300, ) save_as_view = View( "name", title="Save User Perspective As", id="envisage.workbench.action." "save_as_user_perspective_action.UserPerspectiveName", buttons=["OK", "Cancel"], kind="livemodal", width=300, ) rename_view = View( "name", title="Rename User Perspective", id="envisage.workbench.action." "rename_user_perspective_action.UserPerspectiveName", buttons=["OK", "Cancel"], kind="livemodal", width=300, ) pyface-7.4.0/pyface/workbench/action/new_user_perspective_action.py0000644000076500000240000000350014176222673026617 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ An action that creates a new (and empty) user perspective. """ from .user_perspective_name import UserPerspectiveName from .workbench_action import WorkbenchAction class NewUserPerspectiveAction(WorkbenchAction): """ An action that creates a new (and empty) user perspective. """ # 'Action' interface --------------------------------------------------- # The action's unique identifier. id = "pyface.workbench.action.new_user_perspective_action" # The action's name. name = "New Perspective..." # ------------------------------------------------------------------------ # 'Action' interface. # ------------------------------------------------------------------------ def perform(self, event): """ Peform the action. """ window = event.window manager = window.workbench.user_perspective_manager # Get the details of the new perspective. upn = UserPerspectiveName(name="User Perspective %d" % manager.next_id) if upn.edit_traits(view="new_view").result: # Create a new (and empty) user perspective. perspective = manager.create_perspective( upn.name.strip(), upn.show_editor_area ) # Add it to the window... window.perspectives.append(perspective) # ... and make it the active perspective. window.active_perspective = perspective return pyface-7.4.0/pyface/workbench/action/save_as_user_perspective_action.py0000644000076500000240000000364414176222673027460 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ An action that saves the active perspective as a user perspective. """ from .user_perspective_name import UserPerspectiveName from .workbench_action import WorkbenchAction class SaveAsUserPerspectiveAction(WorkbenchAction): """ An action that saves the active perspective as a user perspective. """ # 'Action' interface --------------------------------------------------- # The action's unique identifier. id = "pyface.workbench.action.save_as_user_perspective_action" # The action's name (displayed on menus/tool bar tools etc). name = "Save Perspective As..." # ------------------------------------------------------------------------ # 'Action' interface. # ------------------------------------------------------------------------ def perform(self, event): """ Perform the action. """ window = event.window manager = window.workbench.user_perspective_manager # Get the name of the new perspective. upn = UserPerspectiveName(name=window.active_perspective.name) if upn.edit_traits(view="save_as_view").result: # Make a clone of the active perspective, but give it the new name. perspective = manager.clone_perspective( window, window.active_perspective, name=upn.name.strip() ) # Add it to the window... window.perspectives.append(perspective) # ... and make it the active perspective. window.active_perspective = perspective return pyface-7.4.0/pyface/workbench/action/toggle_view_visibility_action.py0000644000076500000240000000623614176222673027152 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ An action that toggles a view's visibility (ie. hides/shows it). """ from pyface.workbench.api import IView from traits.api import Delegate, Instance from .workbench_action import WorkbenchAction class ToggleViewVisibilityAction(WorkbenchAction): """ An action that toggles a view's visibility (ie. hides/shows it). """ # 'Action' interface --------------------------------------------------- # The action's unique identifier (may be None). id = Delegate("view", modify=True) # The action's name (displayed on menus/tool bar tools etc). name = Delegate("view", modify=True) # The action's style. style = "toggle" # 'ViewAction' interface ----------------------------------------------- # The view that we toggle the visibility for. view = Instance(IView) # ------------------------------------------------------------------------ # 'Action' interface. # ------------------------------------------------------------------------ def destroy(self): """ Called when the action is no longer required. """ if self.view is not None: self._remove_view_listeners(self.view) def perform(self, event): """ Perform the action. """ self._toggle_view_visibility(self.view) return # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ # Trait change handlers ------------------------------------------------ def _view_changed(self, old, new): """ Static trait change handler. """ if old is not None: self._remove_view_listeners(old) if new is not None: self._add_view_listeners(new) self._refresh_checked() return # Methods -------------------------------------------------------------# def _add_view_listeners(self, view): """ Add listeners for trait events on a view. """ view.observe(self._refresh_checked, "visible") view.observe(self._refresh_checked, "window") def _remove_view_listeners(self, view): """ Add listeners for trait events on a view. """ view.observe(self._refresh_checked, "visible", remove=True) view.observe(self._refresh_checked, "window", remove=True) def _refresh_checked(self, event=None): """ Refresh the checked state of the action. """ self.checked = ( self.view is not None and self.view.window is not None and self.view.visible ) def _toggle_view_visibility(self, view): """ Toggle the visibility of a view. """ if view.visible: view.hide() else: view.show() return pyface-7.4.0/pyface/workbench/action/set_active_perspective_action.py0000644000076500000240000000436314176222673027126 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ An action that sets the active perspective. """ from pyface.workbench.api import IPerspective from traits.api import Delegate, Instance, observe from .workbench_action import WorkbenchAction class SetActivePerspectiveAction(WorkbenchAction): """ An action that sets the active perspective. """ # 'Action' interface --------------------------------------------------- # Is the action enabled? enabled = Delegate("perspective") # The action's unique identifier (may be None). id = Delegate("perspective") # The action's name (displayed on menus/tool bar tools etc). name = Delegate("perspective") # The action's style. style = "radio" # 'SetActivePerspectiveAction' interface ------------------------------- # The perspective that we set the active perspective to. perspective = Instance(IPerspective) # ------------------------------------------------------------------------ # 'Action' interface. # ------------------------------------------------------------------------ def destroy(self): """ Destroy the action. """ self.window = None def perform(self, event): """ Perform the action. """ self.window.active_perspective = self.perspective return # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ @observe("perspective,window.active_perspective") def _refresh_checked(self, event): """ Refresh the checked state of the action. """ self.checked = ( self.perspective is not None and self.window is not None and self.window.active_perspective is not None and self.perspective.id is self.window.active_perspective.id ) return pyface-7.4.0/pyface/workbench/action/view_menu_manager.py0000644000076500000240000001121214176222673024511 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 'View' menu """ import logging from pyface.action.api import Group, MenuManager from traits.api import Any, Bool, Instance, List, Str from traits.api import observe from .perspective_menu_manager import PerspectiveMenuManager from .show_view_action import ShowViewAction from .toggle_view_visibility_action import ToggleViewVisibilityAction # Logging. logger = logging.getLogger(__name__) class ViewMenuManager(MenuManager): """ The 'View' menu. By default, this menu is displayed on the main menu bar. """ # 'ActionManager' interface -------------------------------------------- # All of the groups in the manager. groups = List(Group) # The manager's unique identifier (if it has one). id = Str("View") # 'MenuManager' interface ---------------------------------------------# # The menu manager's name (if the manager is a sub-menu, this is what its # label will be). name = Str("&View") # 'ViewMenuManager' interface -----------------------------------------# # Should the perspective menu be shown? show_perspective_menu = Bool(True) # The workbench window that the menu is part of. window = Instance("pyface.workbench.api.WorkbenchWindow") # 'Private' interface -------------------------------------------------# # The group containing the view hide/show actions. _view_group = Any() # ------------------------------------------------------------------------ # 'ActionManager' interface. # ------------------------------------------------------------------------ def _groups_default(self): """ Trait initializer. """ groups = [] # Add a group containing the perspective menu (if requested). if self.show_perspective_menu and len(self.window.perspectives) > 0: groups.append(Group(PerspectiveMenuManager(window=self.window))) # Add a group containing a 'toggler' for all visible views. self._view_group = self._create_view_group(self.window) groups.append(self._view_group) # Add a group containing an 'Other...' item that will launch a dialog # to allow the user to choose a view to show. groups.append(self._create_other_group(self.window)) return groups # ------------------------------------------------------------------------ # 'ViewMenuManager' interface. # ------------------------------------------------------------------------ @observe("window.active_perspective,window.active_part,window.views.items") def refresh(self, event): """ Refreshes the checked state of the actions in the menu. """ logger.debug("refreshing view menu") if self._view_group is not None: self._clear_group(self._view_group) self._initialize_view_group(self.window, self._view_group) self.changed = True return # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _clear_group(self, group): """ Remove all items in a group. """ # fixme: Fix this API in Pyface so there is only one call! group.destroy() group.clear() def _create_other_group(self, window): """ Creates a group containing the 'Other...' action. """ group = Group() group.append(ShowViewAction(name="Other...", window=window)) return group def _create_view_group(self, window): """ Creates a group containing the view 'togglers'. """ group = Group() self._initialize_view_group(window, group) return group def _initialize_view_group(self, window, group): """ Initializes a group containing the view 'togglers'. """ views = window.views[:] views.sort(key=lambda view: view.name) for view in views: # fixme: It seems a little smelly to be reaching in to the window # layout here. Should the 'contains_view' method be part of the # window interface? if window.layout.contains_view(view): group.append( ToggleViewVisibilityAction(view=view, window=window) ) return pyface-7.4.0/pyface/workbench/action/reset_active_perspective_action.py0000644000076500000240000000272014176222673027450 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ An action that resets the active perspective. """ from pyface.api import YES from .workbench_action import WorkbenchAction # The message used when confirming the action. MESSAGE = 'Do you want to reset the current "%s" perspective to its defaults?' class ResetActivePerspectiveAction(WorkbenchAction): """ An action that resets the active perspective. """ # 'Action' interface --------------------------------------------------- # The action's unique identifier (may be None). id = "pyface.workbench.action.reset_active_perspective" # The action's name (displayed on menus/tool bar tools etc). name = "Reset Perspective" # ------------------------------------------------------------------------ # 'Action' interface. # ------------------------------------------------------------------------ def perform(self, event): """ Perform the action. """ window = self.window if window.confirm(MESSAGE % window.active_perspective.name) == YES: window.reset_active_perspective() return pyface-7.4.0/pyface/workbench/action/menu_bar_manager.py0000644000076500000240000000266114176222673024313 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 menu bar manager for Envisage workbench windows. """ from pyface.action.api import MenuBarManager as BaseMenuBarManager from traits.api import Instance from .action_controller import ActionController class MenuBarManager(BaseMenuBarManager): """ The menu bar manager for Envisage workbench windows. """ # 'MenuBarManager' interface ------------------------------------------- # The workbench window that we are the menu bar manager for. window = Instance("pyface.workbench.api.WorkbenchWindow") # ------------------------------------------------------------------------ # 'MenuBarManager' interface. # ------------------------------------------------------------------------ def create_menu_bar(self, parent): """ Creates a menu bar representation of the manager. """ # The controller handles the invocation of every action. controller = ActionController(window=self.window) menu_bar = super().create_menu_bar(parent, controller=controller) return menu_bar pyface-7.4.0/pyface/workbench/action/action_controller.py0000644000076500000240000000306614176222673024551 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 action controller for workbench menu and tool bars. """ from pyface.action.api import ActionController from pyface.workbench.api import WorkbenchWindow from traits.api import Instance class ActionController(ActionController): """ The action controller for workbench menu and tool bars. The controller is used to 'hook' the invocation of every action on the menu and tool bars. This is done so that additional (and workbench specific) information can be added to action events. Currently, we attach a reference to the workbench window. """ # 'ActionController' interface ----------------------------------------- # The workbench window that this is the controller for. window = Instance(WorkbenchWindow) # ------------------------------------------------------------------------ # 'ActionController' interface. # ------------------------------------------------------------------------ def perform(self, action, event): """ Control an action invocation. """ # Add a reference to the window and the application to the event. event.window = self.window return action.perform(event) pyface-7.4.0/pyface/workbench/action/perspective_menu_manager.py0000644000076500000240000001145214176222673026076 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 default perspective menu for a workbench window. """ from pyface.action.api import Group, MenuManager from traits.api import Instance, List, observe from .delete_user_perspective_action import DeleteUserPerspectiveAction from .new_user_perspective_action import NewUserPerspectiveAction from .rename_user_perspective_action import RenameUserPerspectiveAction from .reset_all_perspectives_action import ResetAllPerspectivesAction from .reset_active_perspective_action import ResetActivePerspectiveAction from .save_as_user_perspective_action import SaveAsUserPerspectiveAction from .set_active_perspective_action import SetActivePerspectiveAction class PerspectiveMenuManager(MenuManager): """ The default perspective menu for a workbench window. """ # 'ActionManager' interface -------------------------------------------- # All of the groups in the manager. groups = List(Group) # The manager's unique identifier. id = "PerspectivesMenu" # 'MenuManager' interface ---------------------------------------------# # The menu manager's name. name = "Perspectives" # 'PerspectiveMenuManager' interface ----------------------------------- # The workbench window that the manager is part of. window = Instance("pyface.workbench.api.WorkbenchWindow") # ------------------------------------------------------------------------ # 'ActionManager' interface. # ------------------------------------------------------------------------ def _groups_default(self): """ Trait initializer. """ groups = [ # Create a group containing the actions that switch to specific # perspectives. self._create_perspective_group(self.window), # Create a group containing the user perspective create/save/rename # /delete actions. self._create_user_perspective_group(self.window), # Create a group containing the reset actions. self._create_reset_perspective_group(self.window), ] return groups # ------------------------------------------------------------------------ # 'PerspectiveMenuManager' interface. # ------------------------------------------------------------------------ @observe("window.perspectives.items") def rebuild(self, event): """ Rebuild the menu. This is called when user perspectives have been added or removed. """ # Clear out the old menu. This gives any actions that have trait # listeners (i.e. the rename and delete actions!) a chance to unhook # them. self.destroy() # Resetting the trait allows the initializer to run again (which will # happen just as soon as we fire the 'changed' event). self.reset_traits(["groups"]) # Let the associated menu know that we have changed. self.changed = True return # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _create_perspective_group(self, window): """ Create the actions that switch to specific perspectives. """ # fixme: Not sure if alphabetic sorting is appropriate in all cases, # but it will do for now! perspectives = window.perspectives[:] perspectives.sort(key=lambda x: x.name) # For each perspective, create an action that sets the active # perspective to it. group = Group() for perspective in perspectives: group.append( SetActivePerspectiveAction( perspective=perspective, window=window ) ) return group def _create_user_perspective_group(self, window): """ Create the user perspective create/save/rename/delete actions. """ group = Group( NewUserPerspectiveAction(window=window), SaveAsUserPerspectiveAction(window=window), RenameUserPerspectiveAction(window=window), DeleteUserPerspectiveAction(window=window), ) return group def _create_reset_perspective_group(self, window): """ Create the reset perspective actions. """ group = Group( ResetActivePerspectiveAction(window=window), ResetAllPerspectivesAction(window=window), ) return group pyface-7.4.0/pyface/workbench/workbench_window_memento.py0000644000076500000240000000216414176222673024647 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 memento for a workbench window. """ from traits.api import Any, Dict, HasTraits, Str, Tuple class WorkbenchWindowMemento(HasTraits): """ A memento for a workbench window. """ # The Id of the active perspective. active_perspective_id = Str() # The memento for the editor area. editor_area_memento = Any() # Mementos for each perspective that has been seen. # # The keys are the perspective Ids, the values are the toolkit-specific # mementos. perspective_mementos = Dict(Str, Any) # The position of the window. position = Tuple() # The size of the window. size = Tuple() # Any extra data the toolkit implementation may want to keep. toolkit_data = Any() pyface-7.4.0/pyface/workbench/view.py0000755000076500000240000000111114176222673020516 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 implementation of a workbench view. """ # Import the toolkit specific version. from pyface.toolkit import toolkit_object View = toolkit_object("workbench.view:View") pyface-7.4.0/pyface/workbench/i_view.py0000755000076500000240000000703714176222673021043 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 interface for workbench views. """ import logging from pyface.api import Image from traits.api import Bool, Str, provides from traits.util.camel_case import camel_case_to_words from .i_perspective_item import IPerspectiveItem from .i_workbench_part import IWorkbenchPart, MWorkbenchPart from .perspective_item import PerspectiveItem # Logging. logger = logging.getLogger(__name__) class IView(IWorkbenchPart, IPerspectiveItem): """ The interface for workbench views. """ # Is the view busy? (i.e., should the busy cursor (often an hourglass) be # displayed?). busy = Bool(False) # The category that the view belongs to (this can used to group views when # they are displayed to the user). category = Str("General") # An image used to represent the view to the user (shown in the view tab # and in the view chooser etc). image = Image() # Whether the view is visible or not. visible = Bool(False) # ------------------------------------------------------------------------ # 'IView' interface. # ------------------------------------------------------------------------ def activate(self): """ Activate the view. """ def hide(self): """ Hide the view. """ def show(self): """ Show the view. """ @provides(IView) class MView(MWorkbenchPart, PerspectiveItem): """ Mixin containing common code for toolkit-specific implementations. """ # 'IView' interface ---------------------------------------------------- # Is the view busy? (i.e., should the busy cursor (often an hourglass) be # displayed?). busy = Bool(False) # The category that the view belongs to (this can be used to group views # when they are displayed to the user). category = Str("General") # An image used to represent the view to the user (shown in the view tab # and in the view chooser etc). image = Image() # Whether the view is visible or not. visible = Bool(False) # ------------------------------------------------------------------------ # 'IWorkbenchPart' interface. # ------------------------------------------------------------------------ def _id_default(self): """ Trait initializer. """ id = "%s.%s" % (type(self).__module__, type(self).__name__) logger.warning("view %s has no Id - using <%s>" % (self, id)) # If no Id is specified then use the name. return id def _name_default(self): """ Trait initializer. """ name = camel_case_to_words(type(self).__name__) logger.warning("view %s has no name - using <%s>" % (self, name)) return name # ------------------------------------------------------------------------ # 'IView' interface. # ------------------------------------------------------------------------ def activate(self): """ Activate the view. """ self.window.activate_view(self) def hide(self): """ Hide the view. """ self.window.hide_view(self) def show(self): """ Show the view. """ self.window.show_view(self) return pyface-7.4.0/pyface/workbench/i_workbench.py0000644000076500000240000000665514176222673022055 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 workbench interface. """ from traits.api import Event, Instance, Interface, List, Str from traits.api import VetoableEvent from .user_perspective_manager import UserPerspectiveManager from .window_event import WindowEvent, VetoableWindowEvent from .workbench_window import WorkbenchWindow class IWorkbench(Interface): """ The workbench interface. """ # 'IWorkbench' interface ----------------------------------------------- # The active workbench window (the last one to get focus). active_window = Instance(WorkbenchWindow) # The optional application scripting manager. script_manager = Instance("apptools.appscripting.api.IScriptManager") # A directory on the local file system that we can read and write to at # will. This is used to persist window layout information, etc. state_location = Str() # The optional undo manager. undo_manager = Instance("pyface.undo.api.IUndoManager") # The user defined perspectives manager. user_perspective_manager = Instance(UserPerspectiveManager) # All of the workbench windows created by the workbench. windows = List(WorkbenchWindow) # Workbench lifecycle events ---- # Fired when the workbench is about to exit. # # This can be caused by either:- # # a) The 'exit' method being called. # b) The last open window being closed. exiting = VetoableEvent() # Fired when the workbench has exited. # # This is fired after the last open window has been closed. exited = Event() # Window lifecycle events ---- # Fired when a workbench window has been created. window_created = Event(WindowEvent) # Fired when a workbench window is opening. window_opening = Event(VetoableWindowEvent) # Fired when a workbench window has been opened. window_opened = Event(WindowEvent) # Fired when a workbench window is closing. window_closing = Event(VetoableWindowEvent) # Fired when a workbench window has been closed. window_closed = Event(WindowEvent) # ------------------------------------------------------------------------ # 'IWorkbench' interface. # ------------------------------------------------------------------------ def create_window(self, **kw): """ Factory method that creates a new workbench window. """ def edit(self, obj, kind=None, use_existing=True): """ Edit an object in the active workbench window. """ def exit(self): """ Exit the workbench. This closes all open workbench windows. This method is not called when the user clicks the close icon. Nor when they do an Alt+F4 in Windows. It is only called when the application menu File->Exit item is selected. """ def get_editor(self, obj, kind=None): """ Return the editor that is editing an object. Returns None if no such editor exists. """ def get_editor_by_id(self, id): """ Return the editor with the specified Id. Returns None if no such editor exists. """ pyface-7.4.0/pyface/workbench/i_perspective_item.py0000644000076500000240000000423514176222673023432 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 interface for perspective items. """ from traits.api import Enum, Float, Interface, Str class IPerspectiveItem(Interface): """ The interface for perspective items. """ # The Id of the view to display in the perspective. id = Str() # The position of the view relative to the item specified in the # 'relative_to' trait. # # 'top' puts the view above the 'relative_to' item. # 'bottom' puts the view below the 'relative_to' item. # 'left' puts the view to the left of the 'relative_to' item. # 'right' puts the view to the right of the 'relative_to' item. # 'with' puts the view in the same region as the 'relative_to' item. # # If the position is specified as 'with' you must specify a 'relative_to' # item other than the editor area (i.e., you cannot position a view 'with' # the editor area). position = Enum("left", "top", "bottom", "right", "with") # The Id of the view to position relative to. If this is not specified # (or if no view exists with this Id) then the view will be placed relative # to the editor area. relative_to = Str() # The width of the item (as a fraction of the window width). # # e.g. 0.5 == half the window width. # # Note that this is treated as a suggestion, and it may not be possible # for the workbench to allocate the space requested. width = Float(-1) # The height of the item (as a fraction of the window height). # # e.g. 0.5 == half the window height. # # Note that this is treated as a suggestion, and it may not be possible # for the workbench to allocate the space requested. height = Float(-1) # The style of the dock window. style_hint = Enum("tab", "horizontal", "vertical", "fixed") pyface-7.4.0/pyface/workbench/workbench_window_layout.py0000755000076500000240000000121514176222673024517 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 implementation of a workbench window layout. """ # Import the toolkit specific version. from pyface.toolkit import toolkit_object WorkbenchWindowLayout = toolkit_object( "workbench.workbench_window_layout:WorkbenchWindowLayout" ) pyface-7.4.0/pyface/workbench/editor.py0000755000076500000240000000112114176222673021033 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 implementation of a workbench editor. """ # Import the toolkit specific version. from pyface.toolkit import toolkit_object Editor = toolkit_object("workbench.editor:Editor") pyface-7.4.0/pyface/workbench/i_workbench_window_layout.py0000755000076500000240000002437114176222673025037 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 workbench window layout interface. """ from traits.api import Event, HasTraits, Instance, Interface, Str from traits.api import provides from .i_editor import IEditor from .i_view import IView class IWorkbenchWindowLayout(Interface): """ The workbench window layout interface. Window layouts are responsible for creating and managing the internal structure of a workbench window (it knows how to add and remove views and editors etc). """ # The Id of the editor area. # FIXME v3: This is toolkit specific. editor_area_id = Str() # The workbench window that this is the layout for. window = Instance("pyface.workbench.api.WorkbenchWindow") # Events ---- # Fired when an editor is about to be opened (or restored). editor_opening = Event(IEditor) # Fired when an editor has been opened (or restored). editor_opened = Event(IEditor) # Fired when an editor is about to be closed. editor_closing = Event(IEditor) # Fired when an editor has been closed. editor_closed = Event(IEditor) # Fired when a view is about to be opened (or restored). view_opening = Event(IView) # Fired when a view has been opened (or restored). view_opened = Event(IView) # Fired when a view is about to be closed (*not* hidden!). view_closing = Event(IView) # Fired when a view has been closed (*not* hidden!). view_closed = Event(IView) # FIXME v3: The "just for convenience" returns are a really bad idea. # # Why? They allow the call to be used on the LHS of an expression... # Because they have nothing to do with what the call is supposed to be # doing, they are unlikely to be used (because they are so unexpected and # inconsistently implemented), and only serve to replace two shorter lines # of code with one long one, arguably making code more difficult to read. def activate_editor(self, editor): """ Activate an editor. Returns the editor (just for convenience). """ def activate_view(self, view): """ Activate a view. Returns the view (just for convenience). """ def add_editor(self, editor, title): """ Add an editor. Returns the editor (just for convenience). """ def add_view(self, view, position=None, relative_to=None, size=(-1, -1)): """ Add a view. Returns the view (just for convenience). """ def close_editor(self, editor): """ Close an editor. Returns the editor (just for convenience). """ def close_view(self, view): """ Close a view. FIXME v3: Currently views are never 'closed' in the same sense as an editor is closed. When we close an editor, we destroy its control. When we close a view, we merely hide its control. I'm not sure if this is a good idea or not. It came about after discussion with Dave P. and he mentioned that some views might find it hard to persist enough state that they can be re-created exactly as they were when they are shown again. Returns the view (just for convenience). """ def close(self): """ Close the entire window layout. FIXME v3: Should this be called 'destroy'? """ def create_initial_layout(self, parent): """ Create the initial window layout. Returns the layout. """ def contains_view(self, view): """ Return True if the view exists in the window layout. Note that this returns True even if the view is hidden. """ def hide_editor_area(self): """ Hide the editor area. """ def hide_view(self, view): """ Hide a view. Returns the view (just for convenience). """ def refresh(self): """ Refresh the window layout to reflect any changes. """ def reset_editors(self): """ Activate the first editor in every group. """ def reset_views(self): """ Activate the first view in every region. """ def show_editor_area(self): """ Show the editor area. """ def show_view(self, view): """ Show a view. """ # Methods for saving and restoring the layout -------------------------# def get_view_memento(self): """ Returns the state of the views. """ def set_view_memento(self, memento): """ Restores the state of the views. """ def get_editor_memento(self): """ Returns the state of the editors. """ def set_editor_memento(self, memento): """ Restores the state of the editors. """ def get_toolkit_memento(self): """ Return any toolkit-specific data that should be part of the memento. """ def set_toolkit_memento(self, memento): """ Restores any toolkit-specific data. """ @provides(IWorkbenchWindowLayout) class MWorkbenchWindowLayout(HasTraits): """ Mixin containing common code for toolkit-specific implementations. """ # 'IWorkbenchWindowLayout' interface ----------------------------------- # The Id of the editor area. # FIXME v3: This is toolkit specific. editor_area_id = Str() # The workbench window that this is the layout for. window = Instance("pyface.workbench.api.WorkbenchWindow") # Events ---- # Fired when an editor is about to be opened (or restored). editor_opening = Event(IEditor) # Fired when an editor has been opened (or restored). editor_opened = Event(IEditor) # Fired when an editor is about to be closed. editor_closing = Event(IEditor) # Fired when an editor has been closed. editor_closed = Event(IEditor) # Fired when a view is about to be opened (or restored). view_opening = Event(IView) # Fired when a view has been opened (or restored). view_opened = Event(IView) # Fired when a view is about to be closed (*not* hidden!). view_closing = Event(IView) # Fired when a view has been closed (*not* hidden!). view_closed = Event(IView) # ------------------------------------------------------------------------ # 'IWorkbenchWindowLayout' interface. # ------------------------------------------------------------------------ def activate_editor(self, editor): """ Activate an editor. """ raise NotImplementedError() def activate_view(self, view): """ Activate a view. """ raise NotImplementedError() def add_editor(self, editor, title): """ Add an editor. """ raise NotImplementedError() def add_view(self, view, position=None, relative_to=None, size=(-1, -1)): """ Add a view. """ raise NotImplementedError() def close_editor(self, editor): """ Close an editor. """ raise NotImplementedError() def close_view(self, view): """ Close a view. """ raise NotImplementedError() def close(self): """ Close the entire window layout. """ raise NotImplementedError() def create_initial_layout(self, parent): """ Create the initial window layout. """ raise NotImplementedError() def contains_view(self, view): """ Return True if the view exists in the window layout. """ raise NotImplementedError() def hide_editor_area(self): """ Hide the editor area. """ raise NotImplementedError() def hide_view(self, view): """ Hide a view. """ raise NotImplementedError() def refresh(self): """ Refresh the window layout to reflect any changes. """ raise NotImplementedError() def reset_editors(self): """ Activate the first editor in every group. """ raise NotImplementedError() def reset_views(self): """ Activate the first view in every region. """ raise NotImplementedError() def show_editor_area(self): """ Show the editor area. """ raise NotImplementedError() def show_view(self, view): """ Show a view. """ raise NotImplementedError() # Methods for saving and restoring the layout -------------------------# def get_view_memento(self): """ Returns the state of the views. """ raise NotImplementedError() def set_view_memento(self, memento): """ Restores the state of the views. """ raise NotImplementedError() def get_editor_memento(self): """ Returns the state of the editors. """ raise NotImplementedError() def set_editor_memento(self, memento): """ Restores the state of the editors. """ raise NotImplementedError() def get_toolkit_memento(self): """ Return any toolkit-specific data that should be part of the memento. """ return None def set_toolkit_memento(self, memento): """ Restores any toolkit-specific data. """ return # ------------------------------------------------------------------------ # Protected 'MWorkbenchWindowLayout' interface. # ------------------------------------------------------------------------ def _get_editor_references(self): """ Returns a reference to every editor. """ editor_manager = self.window.editor_manager editor_references = {} for editor in self.window.editors: # Create the editor reference. # # If the editor manager returns 'None' instead of a resource # reference then this editor will not appear the next time the # workbench starts up. This is useful for things like text files # that have an editor but have NEVER been saved. editor_reference = editor_manager.get_editor_memento(editor) if editor_reference is not None: editor_references[editor.id] = editor_reference return editor_references pyface-7.4.0/pyface/workbench/user_perspective_manager.py0000644000076500000240000001536314176222673024640 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Manages a set of user perspectives. """ import logging import os from pyface.workbench.api import Perspective from traits.api import Any, Dict, HasTraits, Int, List, Property from traits.api import Str # Logging. logger = logging.getLogger(__name__) class UserPerspectiveManager(HasTraits): """ Manages a set of user perspectives. """ # 'UserPerspective' interface -----------------------------------------# # A directory on the local file system that we can read and write to at # will. This is used to persist window layout information, etc. state_location = Str() # Next available user perspective id. next_id = Property(Int) # Dictionary mapping perspective id to user defined perspective definition. id_to_perspective = Property(Dict) # The list of user defined perspective definitions. perspectives = Property(List) # The name of the user defined perspectives definition file. file_name = Property(Str) # Private interface ---------------------------------------------------- # Shadow trait for the 'id_to_perspective' property. _id_to_perspective = Any() # ------------------------------------------------------------------------ # 'UserPerspective' interface. # ------------------------------------------------------------------------ # Properties ----------------------------------------------------------- def _get_next_id(self): """ Property getter. """ # Get all of the current perspective ids: ids = list(self.id_to_perspective.keys()) # If there are none: if len(ids) == 0: # Return the starting id: return 1 # Else return the current highest id + 1 as the next id: ids.sort() return int(ids[-1][19:-2]) + 1 def _get_id_to_perspective(self): """ Property getter. """ if self._id_to_perspective is None: self._id_to_perspective = dic = {} try: fh = open(self.file_name, "r") for line in fh: data = line.split(":", 1) if len(data) == 2: id, name = data[0].strip(), data[1].strip() dic[id] = Perspective( id=id, name=name, show_editor_area=False ) fh.close() except: pass return self._id_to_perspective def _get_perspectives(self): """ Property getter. """ return list(self.id_to_perspective.values()) def _get_file_name(self): """ Property getter. """ return os.path.join(self.state_location, "__user_perspective__") # Methods -------------------------------------------------------------# def create_perspective(self, name, show_editor_area=True): """ Create a new (and empty) user-defined perspective. """ perspective = Perspective( id="__user_perspective_%09d__" % self.next_id, name=name, show_editor_area=show_editor_area, ) # Add the perspective to the map. self.id_to_perspective[perspective.id] = perspective # Update the persistent file information. self._update_persistent_data() return perspective def clone_perspective(self, window, perspective, **traits): """ Clone a perspective as a user perspective. """ clone = perspective.clone_traits() # Give the clone a special user perspective Id! clone.id = "__user_perspective_%09d__" % self.next_id # Set any traits specified as keyword arguments. clone.trait_set(**traits) # Add the perspective to the map. self.id_to_perspective[clone.id] = clone # fixme: This needs to be pushed into the window API!!!!!!! window._memento.perspective_mementos[clone.id] = ( window.layout.get_view_memento(), window.active_view and window.active_view.id or None, window.layout.is_editor_area_visible(), ) # Update the persistent file information. self._update_persistent_data() return clone def save(self): """ Persist the current state of the user perspectives. """ self._update_persistent_data() def add(self, perspective, name=None): """ Add a perspective with an optional name. """ # Define the id for the new perspective: perspective.id = id = "__user_perspective_%09d__" % self.next_id # Save the new name (if specified): if name is not None: perspective.name = name # Create the perspective: self.id_to_perspective[id] = perspective # Update the persistent file information: self._update_persistent_data() # Return the new perspective created: return perspective def rename(self, perspective, name): """ Rename the user perspective with the specified id. """ perspective.name = name self.id_to_perspective[perspective.id].name = name # Update the persistent file information: self._update_persistent_data() def remove(self, id): """ Remove the user perspective with the specified id. This method also updates the persistent data. """ if id in self.id_to_perspective: del self.id_to_perspective[id] # Update the persistent file information: self._update_persistent_data() # Try to delete the associated perspective layout file: try: os.remove(os.path.join(self.state_location, id)) except: pass return # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _update_persistent_data(self): """ Update the persistent file information. """ try: fh = open(self.file_name, "w") fh.write( "\n".join( ["%s: %s" % (p.id, p.name) for p in self.perspectives] ) ) fh.close() except: logger.error( "Could not write the user defined perspective " "definition file: " + self.file_name ) return pyface-7.4.0/pyface/workbench/i_editor_manager.py0000755000076500000240000000334514176222673023047 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 editor manager interface. """ from traits.api import Instance, Interface class IEditorManager(Interface): """ The editor manager interface. """ # The workbench window that the editor manager manages editors for ;^) window = Instance("pyface.workbench.api.WorkbenchWindow") def add_editor(self, editor, kind): """ Registers an existing editor. """ def create_editor(self, window, obj, kind): """ Create an editor for an object. 'kind' optionally contains any data required by the specific editor manager implementation to decide what type of editor to create. Returns None if no editor can be created for the resource. """ def get_editor(self, window, obj, kind): """ Get the editor that is currently editing an object. 'kind' optionally contains any data required by the specific editor manager implementation to decide what type of editor to create. Returns None if no such editor exists. """ def get_editor_kind(self, editor): """ Return the 'kind' associated with 'editor'. """ def get_editor_memento(self, editor): """ Return the state of an editor suitable for pickling etc. """ def set_editor_memento(self, memento): """ Restore an editor from a memento and return it. """ pyface-7.4.0/pyface/workbench/editor_manager.py0000755000076500000240000000604214176222673022534 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 default editor manager. """ import weakref from traits.api import HasTraits, Instance, provides from .i_editor_manager import IEditorManager from .traits_ui_editor import TraitsUIEditor @provides(IEditorManager) class EditorManager(HasTraits): """ The default editor manager. """ # 'IEditorManager' interface ------------------------------------------- # The workbench window that the editor manager manages editors for ;^) window = Instance("pyface.workbench.api.WorkbenchWindow") # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, **traits): """ Constructor. """ super().__init__(**traits) # A mapping from editor to editor kind (the factory that created them). self._editor_to_kind_map = weakref.WeakKeyDictionary() return # ------------------------------------------------------------------------ # 'IEditorManager' interface. # ------------------------------------------------------------------------ def add_editor(self, editor, kind): """ Registers an existing editor. """ self._editor_to_kind_map[editor] = kind def create_editor(self, window, obj, kind): """ Create an editor for an object. """ editor = TraitsUIEditor(window=window, obj=obj) self.add_editor(editor, kind) return editor def get_editor(self, window, obj, kind): """ Get the editor that is currently editing an object. """ for editor in window.editors: if self._is_editing(editor, obj, kind): break else: editor = None return editor def get_editor_kind(self, editor): """ Return the 'kind' associated with 'editor'. """ return self._editor_to_kind_map[editor] def get_editor_memento(self, editor): """ Return the state of an editor suitable for pickling etc. By default we don't save the state of editors. """ return None def set_editor_memento(self, memento): """ Restore the state of an editor from a memento. By default we don't try to restore the state of editors. """ return None # ------------------------------------------------------------------------ # 'Protected' 'EditorManager' interface. # ------------------------------------------------------------------------ def _is_editing(self, editor, obj, kind): """ Return True if the editor is editing the object. """ return editor.obj == obj pyface-7.4.0/pyface/workbench/debug/0000755000076500000240000000000014176460551020262 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/workbench/debug/debug_view.py0000644000076500000240000000553714176222673022767 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 view containing a main walter canvas. """ from pyface.workbench.api import View, WorkbenchWindow from traits.api import HasTraits, Instance, Str, observe from traitsui.api import View as TraitsView class DebugViewModel(HasTraits): """ The model for the debug view! """ # 'Model' interface ---------------------------------------------------- active_editor = Str() active_part = Str() active_view = Str() window = Instance(WorkbenchWindow) # ------------------------------------------------------------------------ # 'Model' interface. # ------------------------------------------------------------------------ @observe("window.active_editor,window.active_part,window.active_view") def refresh(self, event): """ Refresh the model. """ self.active_editor = self._get_id(self.window.active_editor) self.active_part = self._get_id(self.window.active_part) self.active_view = self._get_id(self.window.active_view) def _window_changed(self): """ Window changed! """ self.refresh() return # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _get_id(self, obj): """ Return the Id of an object. """ if obj is None: id = "None" else: id = obj.id return id class DebugView(View): """ A view containing a main walter canvas. """ # 'IWorkbenchPart' interface ------------------------------------------- # The part's name (displayed to the user). name = "Debug" # 'DebugView' interface ------------------------------------------------ # The model for the debug view! model = Instance(DebugViewModel) # ------------------------------------------------------------------------ # 'IWorkbenchPart' interface. # ------------------------------------------------------------------------ def create_control(self, parent): """ Creates the toolkit-specific control that represents the view. 'parent' is the toolkit-specific control that is the view's parent. """ self.model = DebugViewModel(window=self.window) ui = self.model.edit_traits( parent=parent, kind="subpanel", view=TraitsView("active_part", "active_editor", "active_view"), ) return ui.control pyface-7.4.0/pyface/workbench/debug/__init__.py0000644000076500000240000000000014176222673022362 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/workbench/debug/api.py0000644000076500000240000000067314176222673021414 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 .debug_view import DebugView pyface-7.4.0/pyface/workbench/traits_ui_editor.py0000644000076500000240000000544214176222673023125 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ An editor whose content is provided by a traits UI. """ import logging from traits.api import Instance, Str from .editor import Editor # Logging. logger = logging.getLogger(__name__) class TraitsUIEditor(Editor): """ An editor whose content is provided by a traits UI. """ # 'TraitsUIEditor' interface ------------------------------------------- # The traits UI that represents the editor. # # The framework sets this to the value returned by 'create_ui'. ui = Instance("traitsui.ui.UI") # The name of the traits UI view used to create the UI (if not specified, # the default traits UI view is used). view = Str() # ------------------------------------------------------------------------ # 'IWorkbenchPart' interface. # ------------------------------------------------------------------------ # Trait initializers --------------------------------------------------- def _name_default(self): """ Trait initializer. """ return str(self.obj) # Methods -------------------------------------------------------------# def create_control(self, parent): """ Creates the toolkit-specific control that represents the editor. 'parent' is the toolkit-specific control that is the editor's parent. Overridden to call 'create_ui' to get the traits UI. """ self.ui = self.create_ui(parent) return self.ui.control def destroy_control(self): """ Destroys the toolkit-specific control that represents the editor. Overridden to call 'dispose' on the traits UI. """ # Give the traits UI a chance to clean itself up. if self.ui is not None: logger.debug("disposing traits UI for editor [%s]", self) self.ui.dispose() self.ui = None return # ------------------------------------------------------------------------ # 'TraitsUIEditor' interface. # ------------------------------------------------------------------------ def create_ui(self, parent): """ Creates the traits UI that represents the editor. By default it calls 'edit_traits' on the editor's 'obj'. If you want more control over the creation of the traits UI then override! """ ui = self.obj.edit_traits( parent=parent, view=self.view, kind="subpanel" ) return ui pyface-7.4.0/pyface/color_dialog.py0000644000076500000240000000252314176222673020224 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 implementation of a dialog that allows the user to select a color. """ from .constant import OK from .toolkit import toolkit_object ColorDialog = toolkit_object("color_dialog:ColorDialog") def get_color(parent, color, show_alpha=False): """ Convenience function that displays a color dialog. Parameters ---------- parent : toolkit control The parent toolkit control for the modal dialog. color : Color or color description The initial Color object, rgb(a) tuple or a string holding a valid color description. show_alpha : bool Whether or not to show alpha channel information. Returns ------- color : Color or None The selected color, or None if the user made no selection. """ dialog = ColorDialog(parent=parent, color=color, show_alpha=show_alpha) result = dialog.open() if result == OK: return dialog.color else: return None pyface-7.4.0/pyface/list_box_model.py0000644000076500000240000000274514176222673020600 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 model for list boxes. """ from traits.api import Event, HasTraits # Classes for event traits. class ListModelEvent(object): """ Information about list model changes. """ class ListBoxModel(HasTraits): """ The model for list boxes. """ # Events ---- #: Fired when the contents of the list have changed. list_changed = Event() def get_item_count(self): """ Get the number of items in the list. Returns ------- item_count : int The number of items in the list. """ raise NotImplementedError() def get_item_at(self, index): """ Returns the item at the specified index. Parameters ---------- index : int The index to return the value of. Returns ------- label, item : str, any The user-visible string and model data of the item. """ raise NotImplementedError() def fire_list_changed(self): """ Invoke this method when the list has changed. """ self.list_changed = ListModelEvent() pyface-7.4.0/pyface/i_layout_widget.py0000644000076500000240000001304214176222673020755 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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, Int, Tuple from pyface.i_layout_item import ILayoutItem, Size, SizePolicy from pyface.i_widget import IWidget class ILayoutWidget(IWidget, ILayoutItem): """ Interface for widgets that can participate in layout. Most widgets implement ILayoutWidget, but widgets like top-level windows, menus, toolbars, etc. do not. """ pass class MLayoutWidget(HasTraits): """ A mixin for Widgets that can participate in layouts. Most widgets implement ILayoutWidget, but widgets like top-level windows, menus, toolbars, etc. do not. """ #: The minimum size that the widget can take. minimum_size = Size #: The maximum size that the widget can take. maximum_size = Size #: Weight factor used to distribute extra space between widgets. stretch = Tuple(Int, Int) #: How the widget should behave when more space is available. size_policy = Tuple(SizePolicy, SizePolicy) def _initialize_control(self): """ Initialize the toolkit control. """ super()._initialize_control() self._set_control_minimum_size(self.minimum_size) self._set_control_maximum_size(self.maximum_size) self._set_control_stretch(self.stretch) self._set_control_size_policy(self.size_policy) def _add_event_listeners(self): """ Add trait observers and toolkit binding. """ super()._add_event_listeners() self.observe( self._minimum_size_updated, "minimum_size", dispatch="ui", ) self.observe( self._maximum_size_updated, "maximum_size", dispatch="ui", ) self.observe( self._stretch_updated, "stretch", dispatch="ui", ) self.observe( self._size_policy_updated, "size_policy", dispatch="ui", ) def _remove_event_listeners(self): """ Remove trait observers and toolkit binding. """ self.observe( self._minimum_size_updated, "minimum_size", dispatch="ui", remove=True, ) self.observe( self._maximum_size_updated, "maximum_size", dispatch="ui", remove=True, ) self.observe( self._stretch_updated, "stretch", dispatch="ui", remove=True, ) self.observe( self._size_policy_updated, "size_policy", dispatch="ui", remove=True, ) super()._remove_event_listeners() def _minimum_size_updated(self, event): """ Trait observer for minimum size. """ if self.control is not None: self._set_control_minimum_size(event.new) def _maximum_size_updated(self, event): """ Trait observer for maximum size. """ if self.control is not None: self._set_control_maximum_size(event.new) def _stretch_updated(self, event): """ Trait observer for stretch. """ if self.control is not None: self._set_control_stretch(event.new) def _size_policy_updated(self, event): """ Trait observer for size policy. """ if self.control is not None: self._set_control_size_policy(event.new) def _set_control_minimum_size(self, size): """ Set the minimum size of the control. Toolkit implementations will need to override this method. """ raise NotImplementedError() def _get_control_minimum_size(self): """ Get the minimum size of the control. Toolkit implementations will need to override this method. This method is only used for testing. """ raise NotImplementedError() def _set_control_maximum_size(self, size): """ Set the maximum size of the control. Toolkit implementations will need to override this method. """ raise NotImplementedError() def _get_control_maximum_size(self): """ Get the maximum size of the control. Toolkit implementations will need to override this method. This method is only used for testing. """ raise NotImplementedError() def _set_control_stretch(self, stretch): """ Set the stretch factor of the control. Toolkit implementations will need to override this method. """ raise NotImplementedError() def _get_control_stretch(self): """ Get the stretch factor of the control. Toolkit implementations will need to override this method. This method is only used for testing. """ raise NotImplementedError() def _set_control_size_policy(self, size_policy): """ Set the size policy of the control. Toolkit implementations will need to override this method. """ raise NotImplementedError() def _get_control_size_policy(self): """ Get the size policy of the control. Toolkit implementations will need to override this method. This method is only used for testing. """ raise NotImplementedError() pyface-7.4.0/pyface/split_widget.py0000644000076500000240000000111114176222673020255 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Mix-in class for split widgets. """ # Import the toolkit specific version. from .toolkit import toolkit_object SplitWidget = toolkit_object("split_widget:SplitWidget") pyface-7.4.0/pyface/key_pressed_event.py0000644000076500000240000000170414176222673021305 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 event that is generated when a key is pressed. """ from traits.api import Bool, HasTraits, Int, Any class KeyPressedEvent(HasTraits): """ The event that is generated when a key is pressed. """ # 'KeyPressedEvent' interface -----------------------------------------# #: Is the alt key down? alt_down = Bool() #: Is the control key down? control_down = Bool() #: Is the shift key down? shift_down = Bool() #: The keycode. key_code = Int() #: The original toolkit specific event. event = Any() pyface-7.4.0/pyface/i_image.py0000644000076500000240000000604214176222673017161 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 base interface for an image. """ from traits.api import Interface class IImage(Interface): """ The base interface for an image. This provides the interface specification that different types of image classes need to provide to be used by Pyface. """ def create_image(self, size=None): """ Creates a toolkit-specific image for this image. An image is a toolkit datastructure that is optimized for I/O, pixel-level access and modification, such as a wx.Image or a QImage. Parameters ---------- size : (int, int) or None The desired size as a width, height tuple, or None if wanting default image size. This is a *preferred* size and concrete implementations may or may not return an image of the precise size requested (indeed, this may be ignored). Returns ------- image : toolkit image The toolkit image corresponding to the image and the specified size. """ def create_bitmap(self, size=None): """ Creates a toolkit-specific bitmap image for this image. A bitmap is a toolkit datastructure that is optimized for rendering to the screen, such as a wx.Bitmap or a QPixmap. Parameters ---------- size : (int, int) or None The desired size as a width, height tuple, or None if wanting default image size. This is a *preferred* size and concrete implementations may or may not return an image of the precise size requested (indeed, this may be ignored). Returns ------- image : toolkit bitmap The toolkit bitmap corresponding to the image and the specified size. """ def create_icon(self, size=None): """ Creates a toolkit-specific icon for this image. An icon is a toolkit datastructure that holds several different variants of an image (eg. selected, disabled, etc.), such as a wx.Icon or a QIcon. Most toolkits can automatically create these from other image classes. Parameters ---------- size : (int, int) or None The desired size as a width, height tuple, or None if wanting default icon size. This is a *preferred* size and concrete implementations may or may not return an icon of the precise size requested (indeed, this may be ignored). Returns ------- image : toolkit icon The toolkit image corresponding to the image and the specified size as an icon. """ pyface-7.4.0/pyface/i_progress_dialog.py0000644000076500000240000000715614176222673021271 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 interface for a dialog that allows the user to display progress of an operation.""" from traits.api import Any, Bool, HasTraits, Int, Str from pyface.i_dialog import IDialog class IProgressDialog(IDialog): """ A simple progress dialog window which allows itself to be updated """ # 'IProgressDialog' interface ---------------------------------# #: The message to display in the dialog message = Str() #: The minimum progress value min = Int() #: The maximum progress value max = Int() #: The margin around the progress bar margin = Int(5) #: Whether the operation can be cancelled can_cancel = Bool(False) #: Whether to show progress times show_time = Bool(False) #: Whether to show progress percent show_percent = Bool(False) #: Label for the 'cancel' button cancel_button_label = Str() # ------------------------------------------------------------------------ # 'IProgressDialog' interface. # ------------------------------------------------------------------------ def update(self, value): """ Update the progress bar to the desired value If the value is >= the maximum and the progress bar is not contained in another panel the parent window will be closed. Parameters ---------- value : The progress value to set. """ def change_message(self, message): """ Change the displayed message in the progress dialog Parameters ---------- message : str or unicode The new message to display. """ class MProgressDialog(HasTraits): """ The mixin class that contains common code for toolkit specific implementations of the IProgressDialog interface. Implements: update() """ #: The progress bar toolkit object # XXX why not the control? progress_bar = Any() # ------------------------------------------------------------------------ # 'IWindow' interface. # ------------------------------------------------------------------------ def open(self): """ Open the dialog """ if self.max < self.min: msg = "Dialog min ({}) is greater than dialog max ({})." raise AttributeError(msg.format(self.min, self.max)) super().open() # ------------------------------------------------------------------------ # 'IProgressDialog' interface. # ------------------------------------------------------------------------ def update(self, value): """ Update the progress bar to the desired value If the value is >= the maximum and the progress bar is not contained in another panel the parent window will be closed. Parameters ---------- value : The progress value to set. """ if self.progress_bar is not None: self.progress_bar.update(value) if value >= self.max: self.close() def change_message(self, message): """ Change the displayed message in the progress dialog Parameters ---------- message : str or unicode The new message to display. """ self.message = message pyface-7.4.0/pyface/splash_screen_log_handler.py0000644000076500000240000000250114176222673022752 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 log handler that emits records to a splash screen. """ from logging import Handler class SplashScreenLogHandler(Handler): """ A log handler that displays log messages on a splash screen. """ def __init__(self, splash_screen): """ Creates a new handler for a splash screen. Parameters ---------- splash_screen : ISplashScreen instance The splash screen being used to display the log messages """ # Base class constructor. super().__init__() # The splash screen that we will display log messages on. self._splash_screen = splash_screen def emit(self, record): """ Emits the log record's message to the splash screen. Parameters ---------- record : logging record instance The log record to be displayed. """ self._splash_screen.text = str(record.getMessage()) + "..." pyface-7.4.0/pyface/i_application_window.py0000644000076500000240000001116114176222673021767 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 interface of a top-level application window. """ from traits.api import HasTraits, Instance, List from pyface.action.api import MenuBarManager, StatusBarManager, ToolBarManager from pyface.i_window import IWindow from pyface.ui_traits import Image class IApplicationWindow(IWindow): """ The interface for a top-level application window. The application window has support for a menu bar, tool bar and a status bar (all of which are optional). Usage ----- Create a sub-class of this class and override the :py:meth:`._create_contents` method. """ # 'IApplicationWindow' interface --------------------------------------- #: The window icon. The default is toolkit specific. icon = Image() #: The menu bar manager (None iff there is no menu bar). menu_bar_manager = Instance(MenuBarManager) #: The status bar manager (None iff there is no status bar). status_bar_manager = Instance(StatusBarManager) #: The tool bar manager (None iff there is no tool bar). tool_bar_manager = Instance(ToolBarManager) #: If the underlying toolkit supports multiple toolbars, you can use this #: list instead of the single ToolBarManager instance above. tool_bar_managers = List(ToolBarManager) # ------------------------------------------------------------------------ # Protected 'IApplicationWindow' interface. # ------------------------------------------------------------------------ def _create_contents(self, parent): """ Create and return the window's contents. Parameters ---------- parent : toolkit control The window's toolkit control to be used as the parent for widgets in the contents. Returns ------- control : toolkit control A control to be used for contents of the window. """ def _create_menu_bar(self, parent): """ Creates the menu bar (if required). Parameters ---------- parent : toolkit control The window's toolkit control. """ def _create_status_bar(self, parent): """ Creates the status bar (if required). Parameters ---------- parent : toolkit control The window's toolkit control. """ def _create_tool_bar(self, parent): """ Creates the tool bar (if required). Parameters ---------- parent : toolkit control The window's toolkit control. """ def _create_trim_widgets(self, parent): """ Creates the 'trim' widgets (the widgets around the window). Parameters ---------- parent : toolkit control The window's toolkit control. """ def _set_window_icon(self): """ Sets the window icon (if required). """ class MApplicationWindow(HasTraits): """ The mixin class that contains common code for toolkit specific implementations of the :py:class:`IApplicationWindow` interface. Implements: destroy(), _create_trim_widgets() """ # ------------------------------------------------------------------------ # 'IWidget' interface. # ------------------------------------------------------------------------ def destroy(self): """ Destroy the control if it exists. """ if self.menu_bar_manager is not None: self.menu_bar_manager.destroy() if self.tool_bar_manager is not None: self.tool_bar_manager.destroy() for tool_bar_manager in self.tool_bar_managers: tool_bar_manager.destroy() super().destroy() # ------------------------------------------------------------------------ # Protected 'IApplicationWindow' interface. # ------------------------------------------------------------------------ def _create_trim_widgets(self, parent): """ Creates the 'trim' widgets (the widgets around the window). Parameters ---------- parent : toolkit control The window's toolkit control. """ # All of these are optional. self._set_window_icon() self._create_menu_bar(parent) self._create_tool_bar(parent) self._create_status_bar(parent) pyface-7.4.0/pyface/split_dialog.py0000644000076500000240000000237114176222673020242 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 dialog that is split in two either horizontally or vertically. """ from pyface.dialog import Dialog from pyface.split_widget import SplitWidget class SplitDialog(Dialog, SplitWidget): """ A dialog that is split in two either horizontally or vertically. """ # ------------------------------------------------------------------------ # Protected 'Dialog' interface. # ------------------------------------------------------------------------ def _create_dialog_area(self, parent): """ Creates the main content of the dialog. Parameters ---------- parent : toolkit control A toolkit control to be used as the parent for the splitter. Returns ------- control : toolkit control The splitter control. """ return self._create_splitter(parent) pyface-7.4.0/pyface/base_toolkit.py0000644000076500000240000002523414176222673020252 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Common toolkit loading utilities and classes This module provides common code for ETS packages that need to do GUI toolkit discovery and loading. The common patterns that ETS has settled on are that where different GUI toolkits require alternative implementations of features the toolkit is expected to provide a callable object which takes a relative module path and an object name, separated by a colon and return the toolkit's implementation of that object (usually this is a class, but it could be anything). The assumption is that this is implemented by objects in sub-modules of the toolkit, but plugin authors are free to use whatever methods they like. Which toolkit to use is specified via the :py:mod:`traits.etsconfig.etsconfig` package, but if this is not explicitly set by an application at startup or via environment variables, there needs to be a way of discovering and loading any available working toolkit implementations. The default mechanism is via the now-standard :py:mod:`importlib_metadata` and :py:mod:`setuptools` "entry point" system. This module provides three things: - a function :py:func:`import_toolkit` that attempts to find and load a toolkit entry point for a specified toolkit name - a function :py:func:`find_toolkit` that attempts to find a toolkit entry point that works - a class :py:class:`Toolkit` class that implements the standard logic for finding toolkit objects. These are done in a library-agnostic way so that the same tools can be used not just for different pyface backends, but also for TraitsUI and ETS libraries where we need to switch between different GUI toolkit implementations. Note that there is no requirement for new toolkit implementations to use this :py:class:`Toolkit` implementation, but they should be compatible with it. Default toolkit loading logic ----------------------------- The :py:func:`find_toolkit` function uses the following logic when attempting to load toolkits: - if ETSConfig.toolkit is set, try to load a plugin with a matching name. If it succeeds, we are good, and if it fails then we error out. - after that, we try every 'pyface.toolkit' plugin we can find. If one succeeds, we consider ourselves good, and set the ETSConfig.toolkit appropriately. The order is configurable, and by default will try to load the `qt4` toolkit first, `wx` next, then all others in arbitrary order, and `null` last. - finally, if all else fails, we try to load the null toolkit. """ import logging import os import sys try: # Starting Python 3.8, importlib.metadata is available in the Python # standard library and starting Python 3.10, the "select" interface is # available on EntryPoints. import importlib.metadata as importlib_metadata except ImportError: import importlib_metadata from traits.api import HasTraits, List, ReadOnly, Str from traits.etsconfig.api import ETSConfig logger = logging.getLogger(__name__) TOOLKIT_PRIORITIES = {"qt4": -2, "wx": -1, "null": float("inf")} default_priorities = lambda plugin: TOOLKIT_PRIORITIES.get(plugin.name, 0) class Toolkit(HasTraits): """ A basic toolkit implementation for use by specific toolkits. This implementation uses pathname mangling to find modules and objects in those modules. If an object can't be found, the toolkit will return a class that raises NotImplementedError when it is instantiated. """ #: The name of the package (eg. pyface) package = ReadOnly #: The name of the toolkit toolkit = ReadOnly #: The packages to look in for implementations. packages = List(Str) def __init__(self, package, toolkit, *packages, **traits): super().__init__( package=package, toolkit=toolkit, packages=list(packages), **traits ) def __call__(self, name): """ Return the toolkit specific object with the given name. Parameters ---------- name : str The name consists of the relative module path and the object name separated by a colon. """ from importlib import import_module mname, oname = name.split(":") if not mname.startswith("."): mname = "." + mname for package in self.packages: try: module = import_module(mname, package) except ImportError as exc: # is the error while trying to import package mname or not? if all( part not in exc.args[0] for part in mname.split(".") if part ): # something else went wrong - let the exception be raised raise # Ignore *ANY* errors unless a debug ENV variable is set. if "ETS_DEBUG" in os.environ: # Attempt to only skip errors in importing the backend modules. # The idea here is that this only happens when the last entry in # the traceback's stack frame mentions the toolkit in question. import traceback frames = traceback.extract_tb(sys.exc_info()[2]) filename, lineno, function, text = frames[-1] if package not in filename: raise else: obj = getattr(module, oname, None) if obj is not None: return obj toolkit = self.toolkit class Unimplemented(object): """ An unimplemented toolkit object This is returned if an object isn't implemented by the selected toolkit. It raises an exception if it is ever instantiated. """ def __init__(self, *args, **kwargs): msg = "the %s %s backend doesn't implement %s" raise NotImplementedError(msg % (toolkit, package, name)) return Unimplemented def import_toolkit(toolkit_name, entry_point="pyface.toolkits"): """ Attempt to import an toolkit specified by an entry point. Parameters ---------- toolkit_name : str The name of the toolkit we would like to load. entry_point : str The name of the entry point that holds our toolkits. Returns ------- toolkit_object : callable A callable object that implements the Toolkit interface. Raises ------ RuntimeError If no toolkit is found, or if the toolkit cannot be loaded for some reason. """ # This compatibility layer can be removed when we drop support for # Python < 3.10. Ref https://github.com/enthought/pyface/issues/999. all_entry_points = importlib_metadata.entry_points() if hasattr(all_entry_points, "select"): entry_point_group = all_entry_points.select(group=entry_point) else: entry_point_group = all_entry_points[entry_point] plugins = [ plugin for plugin in entry_point_group if plugin.name == toolkit_name ] if len(plugins) == 0: msg = "No {} plugin found for toolkit {}" msg = msg.format(entry_point, toolkit_name) logger.debug(msg) raise RuntimeError(msg) elif len(plugins) > 1: msg = "multiple %r plugins found for toolkit %r: %s" module_names = [] for plugin in plugins: module_names.append(plugin.value.split(":")[0]) module_names = ", ".join(module_names) logger.warning(msg, entry_point, toolkit_name, module_names) for plugin in plugins: try: toolkit_object = plugin.load() return toolkit_object except (ImportError, AttributeError) as exc: msg = "Could not load plugin %r from %r" module_name = plugin.value.split(":")[0] logger.info(msg, plugin.name, module_name) logger.debug(exc, exc_info=True) msg = "No {} plugin could be loaded for {}" msg = msg.format(entry_point, toolkit_name) logger.info(msg) raise RuntimeError(msg) def find_toolkit(entry_point, toolkits=None, priorities=default_priorities): """ Find a toolkit that works. If ETSConfig is set, then attempt to find a matching toolkit. Otherwise try every plugin for the entry_point until one works. The ordering of the plugins is supplied via the priorities function which should be suitable for use as a sorting key function. If all else fails, explicitly try to load the "null" toolkit backend. If that fails, give up. Parameters ---------- entry_point : str The name of the entry point that holds our toolkits. toolkits : collection of strings Only consider toolkits which match the given strings, ignore other ones. priorities : callable A callable function that returns an priority for each plugin. Returns ------- toolkit : Toolkit instance A callable object that implements the Toolkit interface. Raises ------ RuntimeError If no ETSConfig.toolkit is set but the toolkit cannot be loaded for some reason. """ if ETSConfig.toolkit: return import_toolkit(ETSConfig.toolkit, entry_point) # This compatibility layer can be removed when we drop support for # Python < 3.10. Ref https://github.com/enthought/pyface/issues/999. all_entry_points = importlib_metadata.entry_points() if hasattr(all_entry_points, "select"): entry_points = [ plugin for plugin in all_entry_points.select(group=entry_point) if toolkits is None or plugin.name in toolkits ] else: entry_points = [ plugin for plugin in all_entry_points[entry_point] if toolkits is None or plugin.name in toolkits ] for plugin in sorted(entry_points, key=priorities): try: with ETSConfig.provisional_toolkit(plugin.name): toolkit = plugin.load() return toolkit except (ImportError, AttributeError, RuntimeError) as exc: msg = "Could not load %s plugin %r from %r" module_name = plugin.value.split(":")[0] logger.info(msg, entry_point, plugin.name, module_name) logger.debug(exc, exc_info=True) # if all else fails, try to import the null toolkit. with ETSConfig.provisional_toolkit("null"): return import_toolkit("null", entry_point) pyface-7.4.0/pyface/filter.py0000644000076500000240000000353414176222673017057 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Base class for all filters. """ from traits.api import HasPrivateTraits class Filter(HasPrivateTraits): """ Abstract base class for all filters. """ # ------------------------------------------------------------------------ # 'Filter' interface. # ------------------------------------------------------------------------ def filter(self, widget, parent, nodes): """ Filters a list of nodes. 'widget'is the widget that we are filtering nodes for. 'parent'is the parent node. 'nodes' is the list of nodes to filter. Returns a list containing only those nodes for which 'select' returns True. """ return [e for e in nodes if self.select(widget, parent, e)] def select(self, widget, parent, node): """ Returns True if the node is 'allowed' (ie. NOT filtered). 'widget' is the widget that we are filtering nodes for. 'parent' is the parent node. 'node' is the node to select. By default we return True. """ return True def is_filter_trait(self, node, trait_name): """ Is the filter affected by changes to a node's trait? 'node' is the node. 'trait_name' is the name of the trait. Returns True if the filter would be affected by changes to the trait named 'trait_name' on the specified node. By default we return False. """ return False pyface-7.4.0/pyface/fields/0000755000076500000240000000000014176460550016457 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/fields/i_combo_field.py0000644000076500000240000000656614176222673021622 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 combo field interface. """ from traits.api import Callable, HasTraits, Enum, List from pyface.fields.i_field import IField class IComboField(IField): """ The combo field interface. This is for comboboxes holding a list of values. """ #: The current value of the combobox. value = Enum(values="values") #: The list of available values for the combobox. values = List() #: Callable that converts a value to text plus an optional icon. #: Should return either a uncode string or a tuple of image resource #: and string. formatter = Callable(str, allow_none=False) class MComboField(HasTraits): #: The current text value of the combobox. value = Enum(values="values") #: The list of available values for the combobox. values = List(minlen=1) #: Callable that converts a value to text plus an optional icon. #: Should return either a uncode string or a tuple of image resource #: and string. formatter = Callable(str, allow_none=False) # ------------------------------------------------------------------------ # object interface # ------------------------------------------------------------------------ def __init__(self, values, **traits): value = traits.pop("value", values[0]) traits["values"] = values super().__init__(**traits) self.value = value # ------------------------------------------------------------------------ # Private interface # ------------------------------------------------------------------------ def _initialize_control(self): super()._initialize_control() self._set_control_values(self.values) self._set_control_value(self.value) def _add_event_listeners(self): """ Set up toolkit-specific bindings for events """ super()._add_event_listeners() self.observe( self._values_updated, "values.items,formatter", dispatch="ui" ) if self.control is not None: self._observe_control_value() def _remove_event_listeners(self): """ Remove toolkit-specific bindings for events """ if self.control is not None: self._observe_control_value(remove=True) self.observe( self._values_updated, "values.items,formatter", dispatch="ui", remove=True, ) super()._remove_event_listeners() # Toolkit control interface --------------------------------------------- def _get_control_text_values(self): """ Toolkit specific method to get the control's text values. """ raise NotImplementedError() def _set_control_values(self, values): """ Toolkit specific method to set the control's values. """ raise NotImplementedError() # Trait change handlers -------------------------------------------------- def _values_updated(self, event): if self.control is not None: self._set_control_values(self.values) pyface-7.4.0/pyface/fields/i_text_field.py0000644000076500000240000001315714176222673021501 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 text field interface. """ from traits.api import Bool, Enum, HasTraits, Str from pyface.fields.i_field import IField class ITextField(IField): """ The text field interface. """ #: The value held by the field. value = Str() #: Should the text trait be updated on user edits, or when done editing. update_text = Enum("auto", "editing_finished") #: Placeholder text for an empty field. placeholder = Str() #: Display typed text, or one of several hidden "password" modes. echo = Enum("normal", "password") #: Whether or not the field is read-only. read_only = Bool() class MTextField(HasTraits): """ The text field mix-in. """ #: The value held by the field. value = Str() #: Should the value be updated on every keystroke, or when done editing. update_text = Enum("auto", "editing_finished") #: Placeholder text for an empty field. placeholder = Str() #: Display typed text, or one of several hidden "password" modes. echo = Enum("normal", "password") #: Whether or not the field is read-only. read_only = Bool() # ------------------------------------------------------------------------ # Private interface # ------------------------------------------------------------------------ def _initialize_control(self): self._set_control_echo(self.echo) self._set_control_value(self.value) self._set_control_placeholder(self.placeholder) self._set_control_read_only(self.read_only) super()._initialize_control() def _add_event_listeners(self): """ Set up toolkit-specific bindings for events """ super()._add_event_listeners() self.observe(self._update_text_updated, "update_text", dispatch="ui") self.observe(self._placeholder_updated, "placeholder", dispatch="ui") self.observe(self._echo_updated, "echo", dispatch="ui") self.observe(self._read_only_updated, "read_only", dispatch="ui") if self.control is not None: if self.update_text == "editing_finished": self._observe_control_editing_finished() else: self._observe_control_value() def _remove_event_listeners(self): """ Remove toolkit-specific bindings for events """ if self.control is not None: if self.update_text == "editing_finished": self._observe_control_editing_finished(remove=True) else: self._observe_control_value(remove=True) self.observe( self._update_text_updated, "update_text", dispatch="ui", remove=True, ) self.observe( self._placeholder_updated, "placeholder", dispatch="ui", remove=True, ) self.observe( self._echo_updated, "echo", dispatch="ui", remove=True ) self.observe( self._read_only_updated, "read_only", dispatch="ui", remove=True ) super()._remove_event_listeners() def _editing_finished(self): if self.control is not None: value = self._get_control_value() self._update_value(value) # Toolkit control interface --------------------------------------------- def _get_control_placeholder(self): """ Toolkit specific method to set the control's placeholder. """ raise NotImplementedError() def _set_control_placeholder(self, placeholder): """ Toolkit specific method to set the control's placeholder. """ raise NotImplementedError() def _get_control_echo(self): """ Toolkit specific method to get the control's echo. """ raise NotImplementedError() def _set_control_echo(self, echo): """ Toolkit specific method to set the control's echo. """ raise NotImplementedError() def _get_control_read_only(self): """ Toolkit specific method to get the control's read_only state. """ raise NotImplementedError() def _set_control_read_only(self, read_only): """ Toolkit specific method to set the control's read_only state. """ raise NotImplementedError() def _observe_control_editing_finished(self, remove=False): """ Change observation of whether editing is finished. """ raise NotImplementedError() # Trait change handlers -------------------------------------------------- def _placeholder_updated(self, event): if self.control is not None: self._set_control_placeholder(self.placeholder) def _echo_updated(self, event): if self.control is not None: self._set_control_echo(self.echo) def _read_only_updated(self, event): if self.control is not None: self._set_control_read_only(self.read_only) def _update_text_updated(self, event): """ Change how we listen to for updates to text value. """ if self.control is not None: if event.new == "editing_finished": self._observe_control_value(remove=True) self._observe_control_editing_finished() else: self._observe_control_editing_finished(remove=True) self._observe_control_value() pyface-7.4.0/pyface/fields/i_field.py0000644000076500000240000000620114176222673020425 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 field interface. """ from traits.api import Any, HasTraits from pyface.i_layout_widget import ILayoutWidget class IField(ILayoutWidget): """ The field interface. A field is a widget that displays a value and (potentially) allows a user to interact with it. """ #: The value held by the field. value = Any() class MField(HasTraits): """ The field mix-in. """ #: The value held by the field. value = Any() # ------------------------------------------------------------------------ # IWidget interface # ------------------------------------------------------------------------ def _add_event_listeners(self): """ Set up toolkit-specific bindings for events """ super()._add_event_listeners() self.observe(self._value_updated, "value", dispatch="ui") def _remove_event_listeners(self): """ Remove toolkit-specific bindings for events """ self.observe( self._value_updated, "value", dispatch="ui", remove=True ) super()._remove_event_listeners() # ------------------------------------------------------------------------ # Private interface # ------------------------------------------------------------------------ def _create(self): """ Creates the toolkit specific control. This method should create the control and assign it to the :py:attr:``control`` trait. """ super()._create() self.show(self.visible) self.enable(self.enabled) def _update_value(self, value): """ Handle a change to the value from user interaction This is a method suitable for calling from a toolkit event handler. """ self.value = self._get_control_value() def _get_control(self): """ If control is not passed directly, get it from the trait. """ control = self.control if control is None: raise RuntimeError("Toolkit control does not exist.") return control # Toolkit control interface --------------------------------------------- def _get_control_value(self): """ Toolkit specific method to get the control's value. """ raise NotImplementedError() def _set_control_value(self, value): """ Toolkit specific method to set the control's value. """ raise NotImplementedError() def _observe_control_value(self, remove=False): """ Toolkit specific method to change the control value observer. """ raise NotImplementedError() # Trait change handlers ------------------------------------------------- def _value_updated(self, event): value = event.new if self.control is not None: self._set_control_value(value) pyface-7.4.0/pyface/fields/toggle_field.py0000644000076500000240000000144714176222673021465 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 toggle field widgets. """ # Import the toolkit specific version. from pyface.toolkit import toolkit_object ToggleField = toolkit_object("fields.toggle_field:ToggleField") CheckBoxField = toolkit_object("fields.toggle_field:CheckBoxField") RadioButtonField = toolkit_object("fields.toggle_field:RadioButtonField") ToggleButtonField = toolkit_object("fields.toggle_field:ToggleButtonField") pyface-7.4.0/pyface/fields/i_time_field.py0000644000076500000240000000343714176222673021453 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 time field interface. """ from datetime import time from traits.api import HasTraits, Time from pyface.fields.i_field import IField class ITimeField(IField): """ The time field interface. This is for a field that edits a datetime.time value. """ #: The current value of the time field value = Time() class MTimeField(HasTraits): """ Mixin class for TimeField implementations """ #: The current value of the time field value = Time() # ------------------------------------------------------------------------ # Private interface # ------------------------------------------------------------------------ def _initialize_control(self): super(MTimeField, self)._initialize_control() self._set_control_value(self.value) def _add_event_listeners(self): """ Set up toolkit-specific bindings for events """ super(MTimeField, self)._add_event_listeners() if self.control is not None: self._observe_control_value() def _remove_event_listeners(self): """ Remove toolkit-specific bindings for events """ if self.control is not None: self._observe_control_value(remove=True) super(MTimeField, self)._remove_event_listeners() # Trait defaults -------------------------------------------------------- def _value_default(self): return time.now() pyface-7.4.0/pyface/fields/tests/0000755000076500000240000000000014176460550017621 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/fields/tests/field_mixin.py0000644000076500000240000000111414176222673022461 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.testing.layout_widget_mixin import LayoutWidgetMixin class FieldMixin(LayoutWidgetMixin): """ Mixin which provides standard methods for all fields. """ pass pyface-7.4.0/pyface/fields/tests/test_spin_field.py0000644000076500000240000000312614176222673023352 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 ..spin_field import SpinField from .field_mixin import FieldMixin class TestSpinField(FieldMixin, unittest.TestCase): def _create_widget(self): return SpinField( parent=self.parent.control, value=1, bounds=(0, 100), tooltip="Dummy", ) # Tests ------------------------------------------------------------------ def test_spin_field(self): self._create_widget_control() self.widget.value = 5 self.gui.process_events() self.assertEqual(self.widget._get_control_value(), 5) def test_spin_field_set(self): self._create_widget_control() with self.assertTraitChanges(self.widget, "value", count=1): self.widget._set_control_value(5) self.gui.process_events() self.assertEqual(self.widget.value, 5) def test_spin_field_bounds(self): self._create_widget_control() self.widget.bounds = (10, 50) self.gui.process_events() self.assertEqual(self.widget._get_control_bounds(), (10, 50)) self.assertEqual(self.widget._get_control_value(), 10) self.assertEqual(self.widget.value, 10) pyface-7.4.0/pyface/fields/tests/test_time_field.py0000644000076500000240000000251114176222673023334 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 time import unittest from ..time_field import TimeField from .field_mixin import FieldMixin class TestTimeField(FieldMixin, unittest.TestCase): def _create_widget(self): return TimeField( parent=self.parent.control, value=time(12, 0, 0), tooltip="Dummy", ) # Tests ------------------------------------------------------------------ def test_time_field(self): self._create_widget_control() self.widget.value = time(13, 1, 1) self.gui.process_events() self.assertEqual(self.widget._get_control_value(), time(13, 1, 1)) def test_time_field_set(self): self._create_widget_control() with self.assertTraitChanges(self.widget, "value", count=1): self.widget._set_control_value(time(13, 1, 1)) self.gui.process_events() self.assertEqual(self.widget.value, time(13, 1, 1)) pyface-7.4.0/pyface/fields/tests/test_combo_field.py0000644000076500000240000000677314176222673023513 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.image_resource import ImageResource from ..combo_field import ComboField from .field_mixin import FieldMixin class TestComboField(FieldMixin, unittest.TestCase): def _create_widget(self): return ComboField( parent=self.parent.control, value="one", values=["one", "two", "three", "four"], tooltip="Dummy", ) # Tests ------------------------------------------------------------------ def test_combo_field(self): self._create_widget_control() self.widget.value = "two" self.gui.process_events() self.assertEqual(self.widget._get_control_value(), "two") self.assertEqual(self.widget._get_control_text(), "two") def test_combo_field_set(self): self._create_widget_control() with self.assertTraitChanges(self.widget, "value", count=1): self.widget._set_control_value("two") self.gui.process_events() self.assertEqual(self.widget.value, "two") def test_combo_field_formatter(self): self.widget.formatter = str self.widget.values = [0, 1, 2, 3] self._create_widget_control() self.widget.value = 2 self.gui.process_events() self.assertEqual(self.widget._get_control_value(), 2) self.assertEqual(self.widget._get_control_text(), "2") def test_combo_field_formatter_changed(self): self.widget.values = [1, 2, 3, 4] self.widget.value = 2 self.widget.formatter = str self._create_widget_control() self.widget.formatter = "Number {}".format self.gui.process_events() self.assertEqual(self.widget._get_control_value(), 2) self.assertEqual(self.widget._get_control_text(), "Number 2") def test_combo_field_formatter_set(self): self.widget.values = [1, 2, 3, 4] self.widget.formatter = str self._create_widget_control() with self.assertTraitChanges(self.widget, "value", count=1): self.widget._set_control_value(2) self.gui.process_events() self.assertEqual(self.widget.value, 2) def test_combo_field_icon_formatter(self): image = ImageResource("question") self.widget.values = [1, 2, 3, 4] self.widget.formatter = lambda x: (image, str(x)) self._create_widget_control() self.widget.value = 2 self.gui.process_events() self.assertEqual(self.widget._get_control_value(), 2) self.assertEqual(self.widget._get_control_text(), "2") def test_combo_field_values(self): self._create_widget_control() self.widget.values = ["four", "five", "one", "six"] self.gui.process_events() # XXX different results in Wx and Qt # As best I can tell, difference is at the Traits level # On Qt setting 'values' sets 'value' to "four" before combofield # handler sees it. On Wx it remains as "one" at point of handler # call. Possibly down to dictionary ordering or something # similar. self.assertIn(self.widget.value, {"one", "four"}) pyface-7.4.0/pyface/fields/tests/test_text_field.py0000644000076500000240000000513614176222673023370 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 from ..text_field import TextField from .field_mixin import FieldMixin is_wx = toolkit.toolkit == "wx" class TestTextField(FieldMixin, unittest.TestCase): def _create_widget(self): return TextField( parent=self.parent.control, value="test", tooltip="Dummy" ) # Tests ------------------------------------------------------------------ def test_text_field(self): self._create_widget_control() self.widget.value = "new value" self.gui.process_events() self.assertEqual(self.widget._get_control_value(), "new value") def test_text_field_set(self): self._create_widget_control() with self.assertTraitChanges(self.widget, "value", count=1): self.widget._set_control_value("new value") self.gui.process_events() self.assertEqual(self.widget.value, "new value") def test_text_field_echo(self): self.widget.echo = "password" self._create_widget_control() self.assertEqual(self.widget._get_control_echo(), "password") @unittest.skipIf( is_wx, "Can't change password mode for wx after control " "creation." ) def test_text_field_echo_change(self): self._create_widget_control() self.widget.echo = "password" self.gui.process_events() self.assertEqual(self.widget._get_control_echo(), "password") def test_text_field_placeholder(self): self._create_widget_control() self.widget.placeholder = "test" self.gui.process_events() self.assertEqual(self.widget._get_control_placeholder(), "test") def test_text_field_readonly(self): self.widget.read_only = True self._create_widget_control() self.gui.process_events() self.assertEqual(self.widget._get_control_read_only(), True) @unittest.skipIf( is_wx, "Can't change read_only mode for wx after control " "creation." ) def test_text_field_readonly_change(self): self._create_widget_control() self.widget.read_only = True self.gui.process_events() self.assertEqual(self.widget._get_control_read_only(), True) pyface-7.4.0/pyface/fields/tests/__init__.py0000644000076500000240000000000014176222673021722 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/fields/tests/test_toggle_field.py0000644000076500000240000000462314176222673023665 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.image_resource import ImageResource from ..toggle_field import ( CheckBoxField, RadioButtonField, ToggleButtonField ) from .field_mixin import FieldMixin class ToggleFieldMixin(FieldMixin): # Tests ------------------------------------------------------------------ def test_toggle_field(self): self._create_widget_control() self.widget.value = True self.gui.process_events() self.assertEqual(self.widget._get_control_value(), True) def test_toggle_field_set(self): self._create_widget_control() with self.assertTraitChanges(self.widget, "value", count=1): self.widget._set_control_value(True) self.gui.process_events() self.assertEqual(self.widget.value, True) def test_text_field_text(self): self._create_widget_control() self.assertEqual(self.widget._get_control_text(), "Toggle") self.widget.text = "Test" self.gui.process_events() self.assertEqual(self.widget._get_control_text(), "Test") def test_text_field_image(self): self._create_widget_control() image = ImageResource("question") # XXX can't validate icon values currently, so just a smoke test self.widget.image = image self.gui.process_events() class TestCheckboxField(ToggleFieldMixin, unittest.TestCase): def _create_widget(self): return CheckBoxField( parent=self.parent.control, text="Toggle", tooltip="Dummy", ) class TestRadioButtonField(ToggleFieldMixin, unittest.TestCase): def _create_widget(self): return RadioButtonField( parent=self.parent.control, text="Toggle", tooltip="Dummy", ) class TestToggleButtonField(ToggleFieldMixin, unittest.TestCase): def _create_widget(self): return ToggleButtonField( parent=self.parent.control, text="Toggle", tooltip="Dummy", ) pyface-7.4.0/pyface/fields/__init__.py0000644000076500000240000000000014176222673020560 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/fields/i_spin_field.py0000644000076500000240000001001314176222673021452 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 spin field interface. """ from traits.api import HasTraits, Int, Property, Range, Tuple from pyface.fields.i_field import IField class ISpinField(IField): """ The spin field interface. This is for spinners holding integer values. """ #: The current value of the spinner value = Range(low="minimum", high="maximum") #: The bounds of the spinner bounds = Tuple(Int, Int) #: The minimum value minimum = Property(Int, observe="bounds") #: The maximum value maximum = Property(Int, observe="bounds") class MSpinField(HasTraits): #: The current value of the spinner value = Range(low="minimum", high="maximum") #: The bounds for the spinner bounds = Tuple(Int, Int) #: The minimum value for the spinner minimum = Property(Int, observe="bounds") #: The maximum value for the spinner maximum = Property(Int, observe="bounds") # ------------------------------------------------------------------------ # object interface # ------------------------------------------------------------------------ def __init__(self, **traits): value = traits.pop("value", None) if "bounds" in traits: traits["value"] = traits["bounds"][0] super().__init__(**traits) if value is not None: self.value = value # ------------------------------------------------------------------------ # Private interface # ------------------------------------------------------------------------ def _initialize_control(self): super()._initialize_control() self._set_control_bounds(self.bounds) self._set_control_value(self.value) def _add_event_listeners(self): """ Set up toolkit-specific bindings for events """ super()._add_event_listeners() self.observe(self._bounds_updated, "bounds", dispatch="ui") if self.control is not None: self._observe_control_value() def _remove_event_listeners(self): """ Remove toolkit-specific bindings for events """ if self.control is not None: self._observe_control_value(remove=True) self.observe( self._bounds_updated, "bounds", dispatch="ui", remove=True ) super()._remove_event_listeners() # Toolkit control interface --------------------------------------------- def _get_control_bounds(self): """ Toolkit specific method to get the control's bounds. """ raise NotImplementedError() def _set_control_bounds(self, bounds): """ Toolkit specific method to set the control's bounds. """ raise NotImplementedError() # Trait property handlers ----------------------------------------------- def _get_minimum(self): return self.bounds[0] def _set_minimum(self, value): if value > self.maximum: self.bounds = (value, value) else: self.bounds = (value, self.maximum) if value > self.value: self.value = value def _get_maximum(self): return self.bounds[1] def _set_maximum(self, value): if value < self.minimum: self.bounds = (value, value) else: self.bounds = (self.minimum, value) if value < self.value: self.value = value # Trait defaults -------------------------------------------------------- def _value_default(self): return self.bounds[0] # Trait change handlers -------------------------------------------------- def _bounds_updated(self, event): if self.control is not None: self._set_control_bounds(self.bounds) pyface-7.4.0/pyface/fields/combo_field.py0000644000076500000240000000123014176222673021271 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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: Enthought, Inc. # Description: """ The combo field widget. """ # Import the toolkit specific version. from pyface.toolkit import toolkit_object ComboField = toolkit_object("fields.combo_field:ComboField") pyface-7.4.0/pyface/fields/api.py0000644000076500000240000000234514176222673017610 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 ``pyface.fields`` subpackage. - :class:`~.CheckBoxField` - :class:`~.ComboField` - :class:`~.RadioButtonField` - :class:`~.SpinField` - :class:`~.TextField` - :class:`~.TimeField` - :class:`~.ToggleButtonField` Interfaces ---------- - :class:`~.IComboField` - :class:`~.IField` - :class:`~.ISpinField` - :class:`~.ITextField` - :class:`~.ITimeField` - :class:`~.IToggleField` """ from .i_combo_field import IComboField from .i_field import IField from .i_spin_field import ISpinField from .i_text_field import ITextField from .i_time_field import ITimeField from .i_toggle_field import IToggleField from .combo_field import ComboField from .spin_field import SpinField from .text_field import TextField from .time_field import TimeField from .toggle_field import ( CheckBoxField, RadioButtonField, ToggleButtonField, ) pyface-7.4.0/pyface/fields/text_field.py0000644000076500000240000000110414176222673021156 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 text field widget. """ # Import the toolkit specific version. from pyface.toolkit import toolkit_object TextField = toolkit_object("fields.text_field:TextField") pyface-7.4.0/pyface/fields/i_toggle_field.py0000644000076500000240000000607714176222673022001 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 toggle field interface. """ from traits.api import Bool, HasTraits, Str from pyface.fields.i_field import IField from pyface.ui_traits import Image class IToggleField(IField): """ The toggle field interface. This is for a toggle between two states, represented by a boolean value. """ #: The current value of the toggle. value = Bool() #: The text to display in the toggle. text = Str() #: The icon to display with the toggle. icon = Image() class MToggleField(HasTraits): """ The toggle field mixin class. This is for a toggle between two states, represented by a boolean value. """ #: The current value of the toggle. value = Bool() #: The text to display in the toggle. text = Str() #: The icon to display with the toggle. icon = Image() # ------------------------------------------------------------------------ # Private interface # ------------------------------------------------------------------------ def _initialize_control(self): super()._initialize_control() self._set_control_text(self.text) self._set_control_icon(self.icon) def _add_event_listeners(self): """ Set up toolkit-specific bindings for events """ super()._add_event_listeners() self.observe(self._text_updated, "text", dispatch="ui") self.observe(self._icon_updated, "icon", dispatch="ui") if self.control is not None: self._observe_control_value() def _remove_event_listeners(self): """ Remove toolkit-specific bindings for events """ if self.control is not None: self._observe_control_value(remove=True) self.observe(self._text_updated, "text", dispatch="ui", remove=True) self.observe(self._icon_updated, "icon", dispatch="ui", remove=True) super()._remove_event_listeners() # Toolkit control interface --------------------------------------------- def _get_control_text(self): """ Toolkit specific method to set the control's text. """ raise NotImplementedError() def _set_control_text(self, text): """ Toolkit specific method to set the control's text. """ raise NotImplementedError() def _set_control_icon(self, icon): """ Toolkit specific method to set the control's icon. """ raise NotImplementedError() # Trait change handlers ------------------------------------------------- def _text_updated(self, event): if self.control is not None: self._set_control_text(self.text) def _icon_updated(self, event): if self.control is not None: self._set_control_icon(self.icon) pyface-7.4.0/pyface/fields/spin_field.py0000644000076500000240000000110414176222673021143 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 spin field widget. """ # Import the toolkit specific version. from pyface.toolkit import toolkit_object SpinField = toolkit_object("fields.spin_field:SpinField") pyface-7.4.0/pyface/fields/time_field.py0000644000076500000240000000110414176222673021130 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 time field widget. """ # Import the toolkit specific version. from pyface.toolkit import toolkit_object TimeField = toolkit_object("fields.time_field:TimeField") pyface-7.4.0/pyface/beep.py0000644000076500000240000000110714176222673016477 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 2012 Philip Chimento """Sound the system bell.""" # Import the toolkit-specific version from .toolkit import toolkit_object beep = toolkit_object("beep:beep") pyface-7.4.0/pyface/i_window.py0000644000076500000240000002555014176222673017413 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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: Enthought, Inc. # Description: """ The abstract interface for all pyface top-level windows. """ from traits.api import Event, HasTraits, Tuple, Str, Vetoable, VetoableEvent from pyface.constant import NO from pyface.key_pressed_event import KeyPressedEvent from pyface.i_widget import IWidget class IWindow(IWidget): """ The abstract interface for all pyface top-level windows. A pyface top-level window has no visual representation until it is opened (ie. its 'control' trait will be None until it is opened). """ # 'IWindow' interface ----------------------------------------------------- #: The position of the window. position = Tuple() #: The size of the window. size = Tuple() #: The window title. title = Str() # Window Events ---------------------------------------------------------- #: The window has been opened. opened = Event() #: The window is about to open. opening = VetoableEvent() #: The window has been activated. activated = Event() #: The window has been closed. closed = Event() #: The window is about to be closed. closing = VetoableEvent() #: The window has been deactivated. deactivated = Event() #: A key was pressed while the window had focus. # FIXME v3: This smells of a hack. What's so special about key presses? # FIXME v3: Str key_pressed = Event(KeyPressedEvent) # ------------------------------------------------------------------------- # 'IWindow' interface. # ------------------------------------------------------------------------- def open(self): """ Opens the window. This fires the :py:attr:`closing` vetoable event, giving listeners the opportunity to veto the opening of the window. If the window is opened, the :py:attr:`opened` event will be fired with the IWindow instance as the event value. Returns ------- opened : bool Whether or not the window was opened. """ def close(self, force=False): """ Closes the window. This fires the :py:attr:`closing` vetoable event, giving listeners the opportunity to veto the closing of the window. If :py:obj:`force` is :py:obj:`True` then the window will close no matter what. If the window is closed, the closed event will be fired with the window object as the event value. Parameters ---------- force : bool Whether the window should close despite vetos. Returns ------- closed : bool Whether or not the window is closed. """ def confirm(self, message, title=None, cancel=False, default=NO): """ Convenience method to show a confirmation dialog. Parameters ---------- message : str The text of the message to display. title : str The text of the dialog title. cancel : bool ``True`` if the dialog should contain a Cancel button. default : NO, YES or CANCEL Which button should be the default button. """ def information( self, message, title="Information", detail="", informative="", text_format="auto" ): """ Convenience method to show an information message dialog. Parameters ---------- message : str The text of the message to display. title : str The text of the dialog title. detail : str Further details about the message. informative : str Explanatory text to display along with the message. text_format : str Specifies what text format to use in the resulting message dialog. One of "auto", "plain", or "rich". """ def warning( self, message, title="Warning", detail="", informative="", text_format="auto" ): """ Convenience method to show a warning message dialog. Parameters ---------- message : str The text of the message to display. title : str The text of the dialog title. detail : str Further details about the message. informative : str Explanatory text to display along with the message. text_format : str Specifies what text format to use in the resulting message dialog. One of "auto", "plain", or "rich". """ def error( self, message, title="Error", detail="", informative="", text_format="auto" ): """ Convenience method to show an error message dialog. Parameters ---------- message : str The text of the message to display. title : str The text of the dialog title. detail : str Further details about the message. informative : str Explanatory text to display along with the message. text_format : str Specifies what text format to use in the resulting message dialog. One of "auto", "plain", or "rich". """ class MWindow(HasTraits): """ The mixin class that contains common code for toolkit specific implementations of the IWindow interface. Implements: close(), confirm(), open() Reimplements: _create() """ # ------------------------------------------------------------------------- # 'IWindow' interface. # ------------------------------------------------------------------------- def open(self): """ Opens the window. This fires the :py:attr:`closing` vetoable event, giving listeners the opportunity to veto the opening of the window. If the window is opened, the :py:attr:`opened` event will be fired with the IWindow instance as the event value. Returns ------- opened : bool Whether or not the window was opened. """ self.opening = event = Vetoable() if not event.veto: # Create the control, if necessary. if self.control is None: self._create() self.show(True) self.opened = self return self.control is not None and not event.veto def close(self, force=False): """ Closes the window. This fires the :py:attr:`closing` vetoable event, giving listeners the opportunity to veto the closing of the window. If :py:obj:`force` is :py:obj:`True` then the window will close no matter what. If the window is closed, the closed event will be fired with the window object as the event value. Parameters ---------- force : bool Whether the window should close despite vetos. Returns ------- closed : bool Whether or not the window is closed. """ if self.control is not None: self.closing = event = Vetoable() if force or not event.veto: self.destroy() self.closed = self return self.control is None def confirm(self, message, title=None, cancel=False, default=NO): """ Convenience method to show a confirmation dialog. Parameters ---------- message : str The text of the message to display. title : str The text of the dialog title. cancel : bool ``True`` if the dialog should contain a Cancel button. default : NO, YES or CANCEL Which button should be the default button. """ from .confirmation_dialog import confirm return confirm(self.control, message, title, cancel, default) def information( self, message, title="Information", detail="", informative="", text_format="auto" ): """ Convenience method to show an information message dialog. Parameters ---------- message : str The text of the message to display. title : str The text of the dialog title. detail : str Further details about the message. informative : str Explanatory text to display along with the message. text_format : str Specifies what text format to use in the resulting message dialog. One of "auto", "plain", or "rich". Only supported on the qt backend. """ from .message_dialog import information information( self.control, message, title, detail, informative, text_format ) def warning( self, message, title="Warning", detail="", informative="", text_format="auto" ): """ Convenience method to show a warning message dialog. Parameters ---------- message : str The text of the message to display. title : str The text of the dialog title. detail : str Further details about the message. informative : str Explanatory text to display along with the message. text_format : str Specifies what text format to use in the resulting message dialog. One of "auto", "plain", or "rich". Only supported on the qt backend. """ from .message_dialog import warning warning( self.control, message, title, detail, informative, text_format ) def error( self, message, title="Error", detail="", informative="", text_format="auto" ): """ Convenience method to show an error message dialog. Parameters ---------- message : str The text of the message to display. title : str The text of the dialog title. detail : str Further details about the message. informative : str Explanatory text to display along with the message. text_format : str Specifies what text format to use in the resulting message dialog. One of "auto", "plain", or "rich". Only supported on the qt backend. """ from .message_dialog import error error(self.control, message, title, detail, informative, text_format) pyface-7.4.0/pyface/constant.py0000644000076500000240000000102214176222673017411 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Common constants. """ # Standard results for Ok/Cancel, Yes/No operations etc. OK = 10 CANCEL = 20 YES = 30 NO = 40 pyface-7.4.0/pyface/i_splash_screen.py0000644000076500000240000000552714176222673020737 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 interface for a splash screen. """ import logging from traits.api import Any, Bool, HasTraits, Int, Tuple, Str from pyface.ui_traits import Image from pyface.splash_screen_log_handler import SplashScreenLogHandler from pyface.i_window import IWindow class ISplashScreen(IWindow): """ The interface for a splash screen. """ # 'ISplashScreen' interface -------------------------------------------- #: The image to display on the splash screen. image = Image() #: If log messages are to be displayed then this is the logging level. See #: the Python documentation for the 'logging' module for more details. log_level = Int(logging.DEBUG) #: Should the splash screen display log messages in the splash text? show_log_messages = Bool(True) #: Optional text to display on top of the splash image. text = Str() #: The text color. # FIXME v3: When TraitsUI supports PyQt then change this to 'Color', # (unless that needs the toolkit to be selected too soon, in which case it # may need to stay as Any - or Str?) # text_color = WxColor('black') text_color = Any() #: The text font. # FIXME v3: When TraitsUI supports PyQt then change this back to # 'Font(None)' with the actual default being toolkit specific. # text_font = Font(None) text_font = Any() #: The x, y location where the text will be drawn. # FIXME v3: Remove this. text_location = Tuple(5, 5) class MSplashScreen(HasTraits): """ The mixin class that contains common code for toolkit specific implementations of the ISplashScreen interface. Reimplements: open(), close() """ # ------------------------------------------------------------------------ # 'IWindow' interface. # ------------------------------------------------------------------------ def open(self): """ Creates the toolkit-specific control for the widget. """ super().open() if self.show_log_messages: self._log_handler = SplashScreenLogHandler(self) self._log_handler.setLevel(self.log_level) # Get the root logger. logger = logging.getLogger() logger.addHandler(self._log_handler) def close(self): """ Close the window. """ if self.show_log_messages: # Get the root logger. logger = logging.getLogger() logger.removeHandler(self._log_handler) super().close() pyface-7.4.0/pyface/about_dialog.py0000644000076500000240000000112614176222673020216 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 implementation of a simple 'About' dialog. """ # Import the toolkit specific version. from .toolkit import toolkit_object AboutDialog = toolkit_object("about_dialog:AboutDialog") pyface-7.4.0/pyface/i_dialog.py0000644000076500000240000001306714176222673017343 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 abstract interface for all pyface dialogs. """ from traits.api import Bool, Enum, HasTraits, Int, Str from pyface.constant import OK from pyface.i_window import IWindow class IDialog(IWindow): """ The abstract interface for all pyface dialogs. Usage: Sub-class this class and either override '_create_contents' or more simply, just override the two methods that do the real work:- 1) '_create_dialog_area' creates the main content of the dialog. 2) '_create_buttons' creates the dialog buttons. """ # 'IDialog' interface -------------------------------------------------# #: The label for the 'cancel' button. The default is toolkit specific. cancel_label = Str() #: The context sensitive help Id (the 'Help' button is only shown iff this #: is set). help_id = Str() #: The label for the 'help' button. The default is toolkit specific. help_label = Str() #: The label for the 'ok' button. The default is toolkit specific. ok_label = Str() #: Is the dialog resizeable? resizeable = Bool(True) #: The return code after the window is closed to indicate whether the dialog #: was closed via 'Ok' or 'Cancel'). return_code = Int(OK) #: The dialog style (is it modal or not). # FIXME v3: It doesn't seem possible to use non-modal dialogs. (How do you # get access to the buttons?) style = Enum("modal", "nonmodal") # ------------------------------------------------------------------------ # 'IDialog' interface. # ------------------------------------------------------------------------ def open(self): """ Opens the dialog. If the dialog is modal then the dialog's event loop is entered and the dialog closed afterwards. The 'return_code' trait is updated according to the button the user pressed and this value is also returned. If the dialog is non-modal the return_code trait is set to 'OK'. Returns ------- return_code : OK or CANCEL The value of the ``return_code`` trait. """ # ------------------------------------------------------------------------ # Protected 'IDialog' interface. # ------------------------------------------------------------------------ def _create_buttons(self, parent): """ Create and return the buttons. Parameters ---------- parent : toolkit control The dialog's toolkit control to be used as the parent for buttons. Returns ------- buttons : toolkit control A control containing the dialog's buttons. """ def _create_contents(self, parent): """ Create and return the dialog's contents. Parameters ---------- parent : toolkit control The window's toolkit control to be used as the parent for widgets in the contents. Returns ------- control : toolkit control A control to be used for contents of the window. """ def _create_dialog_area(self, parent): """ Create and return the main content of the dialog's window. Parameters ---------- parent : toolkit control A toolkit control to be used as the parent for widgets in the contents. Returns ------- control : toolkit control A control to be used for main contents of the dialog. """ def _show_modal(self): """ Opens the dialog as a modal dialog. Returns ------- return_code : OK or CANCEL The return code from the user's interactions. """ class MDialog(HasTraits): """ The mixin class that contains common code for toolkit specific implementations of the IDialog interface. Implements: open() Reimplements: _add_event_listeners(), _create() """ # ------------------------------------------------------------------------ # 'IDialog' interface. # ------------------------------------------------------------------------ def open(self): """ Opens the dialog. If the dialog is modal then the dialog's event loop is entered and the dialog closed afterwards. The 'return_code' trait is updated according to the button the user pressed and this value is also returned. If the dialog is non-modal the return_code trait is set to 'OK'. Returns ------- return_code : OK or CANCEL The value of the ``return_code`` trait. """ if self.control is None: self._create() if self.style == "modal": self.return_code = self._show_modal() self.close() else: self.show(True) self.return_code = OK return self.return_code # ------------------------------------------------------------------------ # Protected 'IWidget' interface. # ------------------------------------------------------------------------ def _create(self): """ Creates the window's widget hierarchy. """ super()._create() self._create_contents(self.control) pyface-7.4.0/pyface/i_directory_dialog.py0000644000076500000240000000275714176222673021433 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 interface for a dialog that allows the user to browse for a directory. """ from traits.api import Bool, HasTraits, Str from pyface.i_dialog import IDialog class IDirectoryDialog(IDialog): """ The interface for a dialog that allows the user to browse for a directory. """ # 'IDirectoryDialog' interface ----------------------------------------- #: The default path. The default (ie. the default default path) is toolkit #: specific. # FIXME v3: The default should be the current directory. (It seems wx is # the problem, although the file dialog does the right thing.) default_path = Str() #: The message to display in the dialog. The default is toolkit specific. message = Str() #: True iff the dialog should include a button that allows the user to #: create a new directory. new_directory = Bool(True) #: The path of the chosen directory. path = Str() class MDirectoryDialog(HasTraits): """ The mixin class that contains common code for toolkit specific implementations of the IDirectoryDialog interface. """ pyface-7.4.0/pyface/python_editor.py0000644000076500000240000000111614176222673020453 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 widget for editing Python code. """ # Import the toolkit specific version. from .toolkit import toolkit_object PythonEditor = toolkit_object("python_editor:PythonEditor") pyface-7.4.0/pyface/i_heading_text.py0000644000076500000240000000714114176222673020543 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Heading text. """ import warnings from traits.api import HasTraits, Int, Interface, Str from pyface.ui_traits import Image class IHeadingText(Interface): """ A widget which shows heading text in a panel. """ # 'IHeadingText' interface --------------------------------------------- #: Heading level. This is currently unused. level = Int(1) #: The heading text. text = Str("Default") #: The background image. image = Image() class MHeadingText(HasTraits): """ The mixin class that contains common code for toolkit specific implementations of the IHeadingText interface. """ # 'IHeadingText' interface --------------------------------------------- #: Heading level. This is currently unused. level = Int(1) #: The heading text. text = Str("Default") def __init__(self, parent=None, **traits): """ Creates the heading text. """ if "image" in traits: warnings.warn( "background images are no-longer supported for Wx and the " "'image' trait will be removed in a future Pyface update", PendingDeprecationWarning, ) create = traits.pop("create", True) # Base class constructor. super().__init__(parent=parent, **traits) if create: # Create the widget's toolkit-specific control. self.create() warnings.warn( "automatic widget creation is deprecated and will be removed " "in a future Pyface version, use create=False and explicitly " "call create() for future behaviour", PendingDeprecationWarning, ) # ------------------------------------------------------------------------ # 'IWidget' interface. # ------------------------------------------------------------------------ def _initialize_control(self): """ Perform any toolkit-specific initialization for the control. """ super()._initialize_control() self._set_control_text(self.text) def _add_event_listeners(self): super()._add_event_listeners() self.observe(self._text_updated, 'text', dispatch="ui") def _remove_event_listeners(self): self.observe(self._text_updated, 'text', dispatch="ui", remove=True) super()._remove_event_listeners() # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _set_control_text(self, text): """ Set the text on the control. Parameters ---------- text : str The text to display. This can contain basic HTML-like markeup. """ raise NotImplementedError() def _get_control_text(self): """ Get the text on the control. Returns ---------- text : str The text to displayed in the widget. """ raise NotImplementedError() # Trait change handlers -------------------------------------------------- def _text_updated(self, event): if self.control is not None: self._set_control_text(self.text) pyface-7.4.0/pyface/clipboard.py0000644000076500000240000000144214176222673017525 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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: Evan Patterson # Date: 06/26/09 # ------------------------------------------------------------------------------ """ The interface for manipulating the toolkit clipboard. """ # Import the toolkit specific version from .toolkit import toolkit_object Clipboard = toolkit_object("clipboard:Clipboard") # Create a singleton clipboard object for convenience clipboard = Clipboard() pyface-7.4.0/pyface/data_view/0000755000076500000240000000000014176460550017154 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/data_view/data_models/0000755000076500000240000000000014176460550021430 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/data_view/data_models/array_data_model.py0000644000076500000240000002266114176222673025302 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 an N-dimensional array data model implementation. This module provides a concrete implementation of a data model for an n-dim numpy array. """ from traits.api import Array, HasRequiredTraits, Instance, observe from pyface.data_view.abstract_data_model import AbstractDataModel from pyface.data_view.data_view_errors import DataViewSetError from pyface.data_view.abstract_value_type import AbstractValueType from pyface.data_view.value_types.api import ( ConstantValue, IntValue, no_value ) from pyface.data_view.index_manager import TupleIndexManager class _AtLeastTwoDArray(Array): """ Trait type that holds an array that at least two dimensional. """ def validate(self, object, name, value): value = super().validate(object, name, value) if value.ndim == 0: value = value.reshape((0, 0)) elif value.ndim == 1: value = value.reshape((-1, 1)) return value class ArrayDataModel(AbstractDataModel, HasRequiredTraits): """ A data model for an n-dim array. This data model presents the data from a multidimensional array hierarchically by dimension. The underlying array must be at least 2 dimensional. Values are adapted by the ``value_type`` trait. This provides sensible default values for integer, float and text dtypes, but other dtypes may need the user of the class to supply an appropriate value type class to adapt values. There are additional value types which provide data sources for row headers, column headers, and the label of the row header column. The defaults are likely suitable for most cases, but can be overriden if required. """ #: The array being displayed. This must have dimension at least 2. data = _AtLeastTwoDArray() #: The index manager that helps convert toolkit indices to data view #: indices. index_manager = Instance(TupleIndexManager, args=()) #: The value type of the row index column header. label_header_type = Instance( AbstractValueType, factory=ConstantValue, kw={'text': "Index"}, allow_none=False, ) #: The value type of the column titles. column_header_type = Instance( AbstractValueType, factory=IntValue, kw={'is_editable': False}, allow_none=False, ) #: The value type of the row titles. row_header_type = Instance( AbstractValueType, factory=IntValue, kw={'is_editable': False}, allow_none=False, ) #: The type of value being displayed in the data model. value_type = Instance(AbstractValueType, allow_none=False, required=True) # Data structure methods def get_column_count(self): """ How many columns in the data view model. The number of columns is the size of the last dimension of the array. Returns ------- column_count : non-negative int The number of columns in the data view model, which is the size of the last dimension of the array. """ return self.data.shape[-1] def can_have_children(self, row): """ Whether or not a row can have child rows. A row is a leaf row if the length of the index is one less than the dimension of the array: the final coordinate for the value will be supplied by the column index. Parameters ---------- row : sequence of int The indices of the row as a sequence from root to leaf. Returns ------- can_have_children : bool Whether or not the row can ever have child rows. """ if len(row) < self.data.ndim - 1: return True return False def get_row_count(self, row): """ Whether or not the row currently has any child rows. The number of rows in a non-leaf row is equal to the size of the next dimension. Parameters ---------- row : sequence of int The indices of the row as a sequence from root to leaf. Returns ------- has_children : bool Whether or not the row currently has child rows. """ if len(row) < self.data.ndim - 1: return self.data.shape[len(row)] return 0 # Data value methods def get_value(self, row, column): """ Return the Python value for the row and column. Parameters ---------- row : sequence of int The indices of the row as a sequence from root to leaf. Returns ------- row_count : non-negative int The number of child rows that the row has. """ if len(row) == 0: if len(column) == 0: return None return column[0] elif len(column) == 0: return row[-1] else: index = tuple(row + column) if len(index) != self.data.ndim: return None return self.data[index] def can_set_value(self, row, column): """ Whether the value in the indicated row and column can be set. This returns False for row and column headers, but True for all array values. Parameters ---------- row : sequence of int The indices of the row as a sequence from root to leaf. column : sequence of int The indices of the column as a sequence of length 0 or 1. Returns ------- can_set_value : bool Whether or not the value can be set. """ # can only set values when we have the full index index = tuple(row + column) return len(index) == self.data.ndim def set_value(self, row, column, value): """ Return the Python value for the row and column. Parameters ---------- row : sequence of int The indices of the row as a sequence from root to leaf. column : sequence of int The indices of the column as a sequence of length 1. Returns ------- value : any The value represented by the given row and column. """ if self.can_set_value(row, column): index = tuple(row + column) self.data[index] = value self.values_changed = (row, column, row, column) else: raise DataViewSetError() def get_value_type(self, row, column): """ Return the value type of the given row and column. This method returns the value of ``column_header_type`` for column headers, the value of ``row_header_type`` for row headers, the value of ``label_header_type`` for the top-left corner value, the value of ``value_type`` for all array values, and ``no_value`` for everything else. Parameters ---------- row : sequence of int The indices of the row as a sequence from root to leaf. column : sequence of int The indices of the column as a sequence of length 0 or 1. Returns ------- value_type : AbstractValueType The value type of the given row and column. """ if len(row) == 0: if len(column) == 0: return self.label_header_type return self.column_header_type elif len(column) == 0: return self.row_header_type elif len(row) < self.data.ndim - 1: return no_value else: return self.value_type # data update methods @observe('data') def data_updated(self, event): """ Handle the array being replaced with a new array. """ if event.new.shape == event.old.shape: if self.data.size > 0: self.values_changed = ( (0,), (0,), (event.old.shape[0] - 1,), (event.old.shape[-1] - 1,) ) else: self.structure_changed = True @observe('value_type.updated') def value_type_updated(self, event): """ Handle the value type being updated. """ if self.data.size > 0: self.values_changed = ( (0,), (0,), (self.data.shape[0] - 1,), (self.data.shape[-1] - 1,) ) @observe('column_header_type.updated') def column_header_type_updated(self, event): """ Handle the column header type being updated. """ if self.data.shape[-1] > 0: self.values_changed = ((), (0,), (), (self.data.shape[-1] - 1,)) @observe('row_header_type.updated') def value_header_type_updated(self, event): """ Handle the value header type being updated. """ if self.data.shape[0] > 0: self.values_changed = ((0,), (), (self.data.shape[0] - 1,), ()) @observe('label_header_type.updated') def label_header_type_updated(self, event): """ Handle the label header type being updated. """ self.values_changed = ((), (), (), ()) # default array value def _data_default(self): from numpy import zeros return zeros(shape=(0, 0)) pyface-7.4.0/pyface/data_view/data_models/tests/0000755000076500000240000000000014176460550022572 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/data_view/data_models/tests/test_row_table_data_model.py0000644000076500000240000003756114176222673030350 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.trait_list_object import TraitList from traits.testing.api import UnittestTools from pyface.data_view.abstract_data_model import DataViewSetError from pyface.data_view.abstract_value_type import AbstractValueType from pyface.data_view.value_types.api import IntValue, TextValue from pyface.data_view.data_models.data_accessors import ( AttributeDataAccessor, IndexDataAccessor, KeyDataAccessor ) from pyface.data_view.data_models.row_table_data_model import RowTableDataModel class DataItem: def __init__(self, a, b, c): self.a = a self.b = b self.c = c class TestRowTableDataModel(UnittestTools, unittest.TestCase): def setUp(self): super().setUp() self.data = [ DataItem(a=i, b=10*i, c=str(i)) for i in range(10) ] self.model = RowTableDataModel( data=self.data, row_header_data=AttributeDataAccessor( attr='a', value_type=IntValue(), ), column_data=[ AttributeDataAccessor( attr='b', value_type=IntValue(), ), AttributeDataAccessor( attr='c', value_type=TextValue(), ) ] ) self.values_changed_event = None self.structure_changed_event = None self.model.observe(self.model_values_changed, 'values_changed') self.model.observe(self.model_structure_changed, 'structure_changed') def tearDown(self): self.model.observe( self.model_values_changed, 'values_changed', remove=True) self.model.observe( self.model_structure_changed, 'structure_changed', remove=True) self.values_changed_event = None self.structure_changed_event = None super().tearDown() def model_values_changed(self, event): self.values_changed_event = event def model_structure_changed(self, event): self.structure_changed_event = event def test_no_data(self): model = RowTableDataModel() self.assertEqual(model.get_column_count(), 0) self.assertTrue(model.can_have_children(())) self.assertEqual(model.get_row_count(()), 0) def test_get_column_count(self): result = self.model.get_column_count() self.assertEqual(result, 2) def test_can_have_children(self): for row in self.model.iter_rows(): with self.subTest(row=row): result = self.model.can_have_children(row) if len(row) == 0: self.assertEqual(result, True) else: self.assertEqual(result, False) def test_get_row_count(self): for row in self.model.iter_rows(): with self.subTest(row=row): result = self.model.get_row_count(row) if len(row) == 0: self.assertEqual(result, 10) else: self.assertEqual(result, 0) def test_get_value(self): for row, column in self.model.iter_items(): with self.subTest(row=row, column=column): result = self.model.get_value(row, column) if len(row) == 0 and len(column) == 0: self.assertEqual(result, 'A') elif len(row) == 0: attr = self.model.column_data[column[0]].attr self.assertEqual(result, attr.title()) elif len(column) == 0: self.assertEqual(result, row[0]) else: attr = self.model.column_data[column[0]].attr self.assertEqual( result, getattr(self.data[row[0]], attr) ) def test_set_value(self): for row, column in self.model.iter_items(): with self.subTest(row=row, column=column): if len(row) == 0 and len(column) == 0: with self.assertRaises(DataViewSetError): self.model.set_value(row, column, 0) elif len(row) == 0: with self.assertRaises(DataViewSetError): self.model.set_value(row, column, 0) elif len(column) == 0: value = 6.0 * row[0] with self.assertTraitChanges(self.model, "values_changed"): self.model.set_value(row, column, value) self.assertEqual(self.data[row[0]].a, value) self.assertEqual( self.values_changed_event.new, (row, column, row, column) ) else: value = 6.0 * row[-1] + 2 * column[0] with self.assertTraitChanges(self.model, "values_changed"): self.model.set_value(row, column, value) attr = self.model.column_data[column[0]].attr self.assertEqual( getattr(self.data[row[0]], attr), value, ) self.assertEqual( self.values_changed_event.new, (row, column, row, column) ) def test_get_value_type(self): for row, column in self.model.iter_items(): with self.subTest(row=row, column=column): result = self.model.get_value_type(row, column) if len(row) == 0 and len(column) == 0: self.assertIsInstance(result, AbstractValueType) self.assertIs( result, self.model.row_header_data.title_type, ) elif len(row) == 0: self.assertIsInstance(result, AbstractValueType) self.assertIs( result, self.model.column_data[column[0]].title_type, ) elif len(column) == 0: self.assertIsInstance(result, AbstractValueType) self.assertIs( result, self.model.row_header_data.value_type, ) else: self.assertIsInstance(result, AbstractValueType) self.assertIs( result, self.model.column_data[column[0]].value_type, ) def test_data_updated(self): with self.assertTraitChanges(self.model, "structure_changed"): self.model.data = [ DataItem(a=i+1, b=20*(i+1), c=str(i)) for i in range(10) ] self.assertTrue(self.structure_changed_event.new) def test_data_items_updated_item_added(self): self.model.data = TraitList([ DataItem(a=i, b=10*i, c=str(i)) for i in range(10) ]) with self.assertTraitChanges(self.model, "structure_changed"): self.model.data += [DataItem(a=100, b=200, c="a string")] self.assertTrue(self.structure_changed_event.new) def test_data_items_updated_item_replaced(self): self.model.data = TraitList([ DataItem(a=i, b=10*i, c=str(i)) for i in range(10) ]) with self.assertTraitChanges(self.model, "values_changed"): self.model.data[1] = DataItem(a=100, b=200, c="a string") self.assertEqual(self.values_changed_event.new, ((1,), (), (1,), ())) def test_data_items_updated_item_replaced_negative(self): self.model.data = TraitList([ DataItem(a=i, b=10*i, c=str(i)) for i in range(10) ]) with self.assertTraitChanges(self.model, "values_changed"): self.model.data[-2] = DataItem(a=100, b=200, c="a string") self.assertEqual(self.values_changed_event.new, ((8,), (), (8,), ())) def test_data_items_updated_items_replaced(self): self.model.data = TraitList([ DataItem(a=i, b=10*i, c=str(i)) for i in range(10) ]) with self.assertTraitChanges(self.model, "values_changed"): self.model.data[1:3] = [ DataItem(a=100, b=200, c="a string"), DataItem(a=200, b=300, c="another string"), ] self.assertEqual(self.values_changed_event.new, ((1,), (), (2,), ())) def test_data_items_updated_slice_replaced(self): self.model.data = TraitList([ DataItem(a=i, b=10*i, c=str(i)) for i in range(10) ]) with self.assertTraitChanges(self.model, "values_changed"): self.model.data[1:4:2] = [ DataItem(a=100, b=200, c="a string"), DataItem(a=200, b=300, c="another string"), ] self.assertEqual(self.values_changed_event.new, ((1,), (), (3,), ())) def test_data_items_updated_reverse_slice_replaced(self): self.model.data = TraitList([ DataItem(a=i, b=10*i, c=str(i)) for i in range(10) ]) with self.assertTraitChanges(self.model, "values_changed"): self.model.data[3:1:-1] = [ DataItem(a=100, b=200, c="a string"), DataItem(a=200, b=300, c="another string"), ] self.assertEqual(self.values_changed_event.new, ((2,), (), (3,), ())) def test_row_header_data_updated(self): with self.assertTraitChanges(self.model, "values_changed"): self.model.row_header_data = AttributeDataAccessor(attr='b') self.assertEqual( self.values_changed_event.new, ((), (), (), ()) ) def test_row_header_data_values_updated(self): with self.assertTraitChanges(self.model, "values_changed"): self.model.row_header_data.updated = (self.model.row_header_data, 'value') self.assertEqual( self.values_changed_event.new, ((0,), (), (9,), ()) ) def test_row_header_data_title_updated(self): with self.assertTraitChanges(self.model, "values_changed"): self.model.row_header_data.updated = (self.model.row_header_data, 'title') self.assertEqual( self.values_changed_event.new, ((), (), (), ()) ) def test_no_data_row_header_data_update(self): model = RowTableDataModel( row_header_data=AttributeDataAccessor( attr='a', value_type=IntValue(), ), column_data=[ AttributeDataAccessor( attr='b', value_type=IntValue(), ), AttributeDataAccessor( attr='c', value_type=TextValue(), ) ] ) # check that updating accessors is safe with empty data with self.assertTraitDoesNotChange(model, 'values_changed'): model.row_header_data.attr = 'b' def test_column_data_updated(self): with self.assertTraitChanges(self.model, "structure_changed"): self.model.column_data = [ AttributeDataAccessor( attr='c', value_type=TextValue(), ), AttributeDataAccessor( attr='b', value_type=IntValue(), ), ] self.assertTrue(self.structure_changed_event.new) def test_column_data_items_updated(self): with self.assertTraitChanges(self.model, "structure_changed"): self.model.column_data.pop() self.assertTrue(self.structure_changed_event.new) def test_column_data_value_updated(self): with self.assertTraitChanges(self.model, "values_changed"): self.model.column_data[0].updated = (self.model.column_data[0], 'value') self.assertEqual( self.values_changed_event.new, ((0,), (0,), (9,), (0,)) ) def test_no_data_column_data_update(self): model = RowTableDataModel( row_header_data=AttributeDataAccessor( attr='a', value_type=IntValue(), ), column_data=[ AttributeDataAccessor( attr='b', value_type=IntValue(), ), AttributeDataAccessor( attr='c', value_type=TextValue(), ) ] ) with self.assertTraitDoesNotChange(model, 'values_changed'): model.column_data[0].attr = 'a' def test_column_data_title_updated(self): with self.assertTraitChanges(self.model, "values_changed"): self.model.column_data[0].updated = (self.model.column_data[0], 'title') self.assertEqual( self.values_changed_event.new, ((), (0,), (), (0,)) ) def test_list_tuple_data(self): data = [ (i, 10*i, str(i)) for i in range(10) ] model = RowTableDataModel( data=data, row_header_data=IndexDataAccessor( index=0, value_type=IntValue(), ), column_data=[ IndexDataAccessor( index=1, value_type=IntValue(), ), IndexDataAccessor( index=2, value_type=TextValue(), ) ] ) for row, column in model.iter_items(): with self.subTest(row=row, column=column): result = model.get_value(row, column) if len(row) == 0 and len(column) == 0: self.assertEqual(result, '0') elif len(row) == 0: index = model.column_data[column[0]].index self.assertEqual(result, str(index)) elif len(column) == 0: self.assertEqual(result, row[0]) else: index = model.column_data[column[0]].index self.assertEqual( result, data[row[0]][index] ) def test_list_dict_data(self): data = [ {'a': i, 'b': 10*i, 'c': str(i)} for i in range(10) ] model = RowTableDataModel( data=data, row_header_data=KeyDataAccessor( key='a', value_type=IntValue(), ), column_data=[ KeyDataAccessor( key='b', value_type=IntValue(), ), KeyDataAccessor( key='c', value_type=TextValue(), ) ] ) for row, column in model.iter_items(): with self.subTest(row=row, column=column): result = model.get_value(row, column) if len(row) == 0 and len(column) == 0: self.assertEqual(result, 'A') elif len(row) == 0: key = model.column_data[column[0]].key self.assertEqual(result, str(key).title()) elif len(column) == 0: self.assertEqual(result, data[row[0]]['a']) else: key = model.column_data[column[0]].key self.assertEqual( result, data[row[0]][key] ) pyface-7.4.0/pyface/data_view/data_models/tests/__init__.py0000644000076500000240000000000014176222673024673 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/data_view/data_models/tests/test_array_data_model.py0000644000076500000240000003670214176222673027504 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.testing.api import UnittestTools from traits.testing.optional_dependencies import numpy as np, requires_numpy from pyface.data_view.data_view_errors import DataViewSetError from pyface.data_view.abstract_value_type import AbstractValueType from pyface.data_view.value_types.api import ( FloatValue, IntValue, no_value ) # This import results in an error without numpy installed # see enthought/pyface#742 if np is not None: from pyface.data_view.data_models.api import ArrayDataModel @requires_numpy class TestArrayDataModel(UnittestTools, TestCase): def setUp(self): super().setUp() self.array = np.arange(30.0).reshape(5, 2, 3) self.model = ArrayDataModel(data=self.array, value_type=FloatValue()) self.values_changed_event = None self.structure_changed_event = None self.model.observe(self.model_values_changed, 'values_changed') self.model.observe(self.model_structure_changed, 'structure_changed') def tearDown(self): self.model.observe( self.model_values_changed, 'values_changed', remove=True) self.model.observe( self.model_structure_changed, 'structure_changed', remove=True) self.values_changed_event = None self.structure_changed_event = None super().tearDown() def model_values_changed(self, event): self.values_changed_event = event def model_structure_changed(self, event): self.structure_changed_event = event def test_no_data(self): model = ArrayDataModel(value_type=FloatValue()) self.assertEqual(model.data.ndim, 2) self.assertEqual(model.data.shape, (0, 0)) self.assertEqual(model.data.dtype, float) self.assertEqual(model.get_column_count(), 0) self.assertTrue(model.can_have_children(())) self.assertEqual(model.get_row_count(()), 0) def test_data_1d(self): array = np.arange(30.0) model = ArrayDataModel(data=array, value_type=FloatValue()) self.assertEqual(model.data.ndim, 2) self.assertEqual(model.data.shape, (30, 1)) def test_data_list(self): data = list(range(30)) model = ArrayDataModel(data=data, value_type=FloatValue()) self.assertEqual(model.data.ndim, 2) self.assertEqual(model.data.shape, (30, 1)) def test_set_data_1d(self): with self.assertTraitChanges(self.model, 'structure_changed'): self.model.data = np.arange(30.0) self.assertEqual(self.model.data.ndim, 2) self.assertEqual(self.model.data.shape, (30, 1)) def test_set_data_list(self): with self.assertTraitChanges(self.model, 'structure_changed'): self.model.data = list(range(30)) self.assertEqual(self.model.data.ndim, 2) self.assertEqual(self.model.data.shape, (30, 1)) def test_get_column_count(self): result = self.model.get_column_count() self.assertEqual(result, 3) def test_can_have_children(self): for row in self.model.iter_rows(): with self.subTest(row=row): result = self.model.can_have_children(row) if len(row) <= 1: self.assertEqual(result, True) else: self.assertEqual(result, False) def test_get_row_count(self): for row in self.model.iter_rows(): with self.subTest(row=row): result = self.model.get_row_count(row) if len(row) == 0: self.assertEqual(result, 5) elif len(row) == 1: self.assertEqual(result, 2) else: self.assertEqual(result, 0) def test_get_value(self): for row, column in self.model.iter_items(): with self.subTest(row=row, column=column): result = self.model.get_value(row, column) if len(row) == 0 and len(column) == 0: self.assertIsNone(result) elif len(row) == 0: self.assertEqual(result, column[0]) elif len(column) == 0: self.assertEqual(result, row[-1]) elif len(row) == 1: self.assertIsNone(result) else: self.assertEqual( result, self.array[row[0], row[1], column[0]] ) def test_set_value(self): for row, column in self.model.iter_items(): with self.subTest(row=row, column=column): if len(row) == 0 and len(column) == 0: with self.assertRaises(DataViewSetError): self.model.set_value(row, column, 0) elif len(row) == 0: with self.assertRaises(DataViewSetError): self.model.set_value(row, column, column[0] + 1) elif len(column) == 0: with self.assertRaises(DataViewSetError): self.model.set_value(row, column, row[-1] + 1) elif len(row) == 1: value = 6.0 * row[-1] + 2 * column[0] with self.assertTraitDoesNotChange( self.model, "values_changed"): with self.assertRaises(DataViewSetError): self.model.set_value(row, column, value) else: value = 6.0 * row[-1] + 2 * column[0] with self.assertTraitChanges(self.model, "values_changed"): self.model.set_value(row, column, value) self.assertEqual( self.array[row[0], row[1], column[0]], value, ) self.assertEqual( self.values_changed_event.new, (row, column, row, column) ) def test_get_value_type(self): for row, column in self.model.iter_items(): with self.subTest(row=row, column=column): result = self.model.get_value_type(row, column) if len(row) == 0 and len(column) == 0: self.assertIsInstance(result, AbstractValueType) self.assertIs(result, self.model.label_header_type) elif len(row) == 0: self.assertIsInstance(result, AbstractValueType) self.assertIs(result, self.model.column_header_type) elif len(column) == 0: self.assertIsInstance(result, AbstractValueType) self.assertIs(result, self.model.row_header_type) elif len(row) == 1: self.assertIs(result, no_value) else: self.assertIsInstance(result, AbstractValueType) self.assertIs(result, self.model.value_type) def test_data_updated(self): with self.assertTraitChanges(self.model, "values_changed"): self.model.data = 2 * self.array self.assertEqual( self.values_changed_event.new, ((0,), (0,), (4,), (2,)) ) def test_data_updated_new_shape(self): with self.assertTraitChanges(self.model, "structure_changed"): self.model.data = 2 * self.array.T self.assertTrue(self.structure_changed_event.new) def test_type_updated(self): with self.assertTraitChanges(self.model, "values_changed"): self.model.value_type = IntValue() self.assertEqual( self.values_changed_event.new, ((0,), (0,), (4,), (2,)) ) def test_type_updated_empty(self): self.model.data = np.empty((0, 0, 0), dtype='int') with self.assertTraitDoesNotChange(self.model, "values_changed"): self.model.value_type = IntValue() def test_type_attribute_updated(self): with self.assertTraitChanges(self.model, "values_changed"): self.model.value_type.is_editable = False self.assertEqual( self.values_changed_event.new, ((0,), (0,), (4,), (2,)) ) def test_type_attribute_updated_empty(self): self.model.data = np.empty((0, 0, 0), dtype='int') with self.assertTraitDoesNotChange(self.model, "values_changed"): self.model.value_type.is_editable = False def test_row_header_type_updated(self): with self.assertTraitChanges(self.model, "values_changed"): self.model.row_header_type = no_value self.assertEqual( self.values_changed_event.new, ((0,), (), (4,), ()) ) def test_row_header_type_updated_empty(self): self.model.data = np.empty((0, 4, 2), dtype='int') with self.assertTraitDoesNotChange(self.model, "values_changed"): self.model.row_header_type = no_value def test_row_header_attribute_updated(self): with self.assertTraitChanges(self.model, "values_changed"): self.model.row_header_type.format = str self.assertEqual( self.values_changed_event.new, ((0,), (), (4,), ()) ) def test_row_header_attribute_updated_empty(self): self.model.data = np.empty((0, 4, 2), dtype='int') with self.assertTraitDoesNotChange(self.model, "values_changed"): self.model.row_header_type.format = str def test_column_header_type_updated(self): with self.assertTraitChanges(self.model, "values_changed"): self.model.column_header_type = no_value self.assertEqual( self.values_changed_event.new, ((), (0,), (), (2,)) ) def test_column_header_type_updated_empty(self): self.model.data = np.empty((2, 4, 0), dtype='int') with self.assertTraitDoesNotChange(self.model, "values_changed"): self.model.column_header_type = no_value def test_column_header_type_attribute_updated(self): with self.assertTraitChanges(self.model, "values_changed"): self.model.column_header_type.format = str self.assertEqual( self.values_changed_event.new, ((), (0,), (), (2,)) ) def test_column_header_attribute_updated_empty(self): self.model.data = np.empty((2, 4, 0), dtype='int') with self.assertTraitDoesNotChange(self.model, "values_changed"): self.model.column_header_type.format = str def test_label_header_type_updated(self): with self.assertTraitChanges(self.model, "values_changed"): self.model.label_header_type = no_value self.assertEqual( self.values_changed_event.new, ((), (), (), ()) ) def test_label_header_type_attribute_updated(self): with self.assertTraitChanges(self.model, "values_changed"): self.model.label_header_type.text = "My Table" self.assertEqual( self.values_changed_event.new, ((), (), (), ()) ) def test_is_row_valid(self): # valid rows are valid for row in self.model.iter_rows(): with self.subTest(row=row): result = self.model.is_row_valid(row) self.assertTrue(result) def test_is_row_valid_big(self): result = self.model.is_row_valid((5,)) self.assertFalse(result) def test_is_row_valid_long(self): result = self.model.is_row_valid((1, 1, 1)) self.assertFalse(result) def test_is_column_valid(self): # valid columns are valid columns = [()] + [(i,) for i in range(3)] for column in columns: with self.subTest(column=column): result = self.model.is_column_valid(column) self.assertTrue(result) def test_is_column_valid_big(self): result = self.model.is_column_valid((3,)) self.assertFalse(result) def test_is_column_valid_long(self): result = self.model.is_column_valid((1, 1)) self.assertFalse(result) def test_iter_rows(self): result = list(self.model.iter_rows()) self.assertEqual( result, [ (), (0,), (0, 0), (0, 1), (1,), (1, 0), (1, 1), (2,), (2, 0), (2, 1), (3,), (3, 0), (3, 1), (4,), (4, 0), (4, 1), ] ) def test_iter_rows_start(self): result = list(self.model.iter_rows((2,))) self.assertEqual( result, [(2,), (2, 0), (2, 1)] ) def test_iter_rows_leaf(self): result = list(self.model.iter_rows([2, 0])) self.assertEqual(result, [(2, 0)]) def test_iter_items(self): result = list(self.model.iter_items()) self.assertEqual( result, [ ((), ()), ((), (0,)), ((), (1,)), ((), (2,)), ((0,), ()), ((0,), (0,)), ((0,), (1,)), ((0,), (2,)), ((0, 0), ()), ((0, 0), (0,)), ((0, 0), (1,)), ((0, 0), (2,)), ((0, 1), ()), ((0, 1), (0,)), ((0, 1), (1,)), ((0, 1), (2,)), ((1,), ()), ((1,), (0,)), ((1,), (1,)), ((1,), (2,)), ((1, 0), ()), ((1, 0), (0,)), ((1, 0), (1,)), ((1, 0), (2,)), ((1, 1), ()), ((1, 1), (0,)), ((1, 1), (1,)), ((1, 1), (2,)), ((2,), ()), ((2,), (0,)), ((2,), (1,)), ((2,), (2,)), ((2, 0), ()), ((2, 0), (0,)), ((2, 0), (1,)), ((2, 0), (2,)), ((2, 1), ()), ((2, 1), (0,)), ((2, 1), (1,)), ((2, 1), (2,)), ((3,), ()), ((3,), (0,)), ((3,), (1,)), ((3,), (2,)), ((3, 0), ()), ((3, 0), (0,)), ((3, 0), (1,)), ((3, 0), (2,)), ((3, 1), ()), ((3, 1), (0,)), ((3, 1), (1,)), ((3, 1), (2,)), ((4,), ()), ((4,), (0,)), ((4,), (1,)), ((4,), (2,)), ((4, 0), ()), ((4, 0), (0,)), ((4, 0), (1,)), ((4, 0), (2,)), ((4, 1), ()), ((4, 1), (0,)), ((4, 1), (1,)), ((4, 1), (2,)), ] ) def test_iter_items_start(self): result = list(self.model.iter_items((2,))) self.assertEqual( result, [ ((2,), ()), ((2,), (0,)), ((2,), (1,)), ((2,), (2,)), ((2, 0), ()), ((2, 0), (0,)), ((2, 0), (1,)), ((2, 0), (2,)), ((2, 1), ()), ((2, 1), (0,)), ((2, 1), (1,)), ((2, 1), (2,)), ] ) def test_iter_items_leaf(self): result = list(self.model.iter_items((2, 0))) self.assertEqual( result, [ ((2, 0), ()), ((2, 0), (0,)), ((2, 0), (1,)), ((2, 0), (2,)), ] ) pyface-7.4.0/pyface/data_view/data_models/tests/test_data_accessors.py0000644000076500000240000003025114176222673027164 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 types import MappingProxyType import unittest from traits.api import TraitError from traits.testing.api import UnittestTools from pyface.data_view.abstract_data_model import DataViewSetError from pyface.data_view.value_types.api import TextValue from pyface.data_view.data_models.data_accessors import ( AttributeDataAccessor, ConstantDataAccessor, IndexDataAccessor, KeyDataAccessor, ) class AttributeDummy: def __init__(self, attr_value): self.attr_value = attr_value class DataAccessorMixin(UnittestTools): def accessor_observer(self, event): self.accessor_event = event def test_title_type_changed(self): accessor = self.create_accessor() accessor.observe(self.accessor_observer, 'updated') with self.assertTraitChanges(accessor, 'updated', count=1): accessor.title_type = TextValue() self.assertEqual(self.accessor_event.new, (accessor, 'title')) def test_title_type_updated(self): accessor = self.create_accessor() accessor.observe(self.accessor_observer, 'updated') with self.assertTraitChanges(accessor, 'updated', count=1): accessor.title_type.updated = True self.assertEqual(self.accessor_event.new, (accessor, 'title')) def test_value_type_changed(self): accessor = self.create_accessor() accessor.observe(self.accessor_observer, 'updated') with self.assertTraitChanges(accessor, 'updated', count=1): accessor.value_type = TextValue() self.assertEqual(self.accessor_event.new, (accessor, 'value')) def test_value_type_updated(self): accessor = self.create_accessor() accessor.observe(self.accessor_observer, 'updated') with self.assertTraitChanges(accessor, 'updated', count=1): accessor.value_type.updated = True self.assertEqual(self.accessor_event.new, (accessor, 'value')) class TestConstantDataAccessor(unittest.TestCase, DataAccessorMixin): def create_accessor(self): return ConstantDataAccessor( title='Test', value='test', value_type=TextValue(), ) def test_defaults(self): accessor = ConstantDataAccessor() self.assertEqual(accessor.value, None) self.assertIsNone(accessor.value_type) self.assertIsInstance(accessor.title_type, TextValue) self.assertEqual(accessor.title, '') def test_typical_defaults(self): accessor = self.create_accessor() self.assertIsInstance(accessor.title_type, TextValue) self.assertEqual(accessor.title, 'Test') def test_get_value(self): accessor = self.create_accessor() obj = object() value = accessor.get_value(obj) self.assertEqual(value, 'test') def test_can_set_value(self): accessor = self.create_accessor() obj = object() can_set = accessor.can_set_value(obj) self.assertFalse(can_set) def test_set_value_error(self): accessor = self.create_accessor() obj = object() with self.assertRaises(DataViewSetError): accessor.set_value(obj, 'new_value') def test_value_updated(self): accessor = self.create_accessor() accessor.observe(self.accessor_observer, 'updated') with self.assertTraitChanges(accessor, 'updated', count=1): accessor.value = 'other_value' self.assertEqual(self.accessor_event.new, (accessor, 'value')) class TestAttributeDataAccessor(unittest.TestCase, DataAccessorMixin): def create_accessor(self): return AttributeDataAccessor( attr='attr_value', value_type=TextValue(), ) def test_defaults(self): accessor = AttributeDataAccessor() self.assertEqual(accessor.attr, '') self.assertIsNone(accessor.value_type) self.assertIsInstance(accessor.title_type, TextValue) self.assertEqual(accessor.title, '') def test_typical_defaults(self): accessor = self.create_accessor() self.assertIsInstance(accessor.title_type, TextValue) self.assertEqual(accessor.title, 'Attr Value') def test_get_value(self): accessor = self.create_accessor() obj = AttributeDummy('test_value') value = accessor.get_value(obj) self.assertEqual(value, 'test_value') def test_get_value_extended(self): accessor = self.create_accessor() accessor.attr = 'attr_value.attr_value' obj = AttributeDummy(AttributeDummy('test_value')) value = accessor.get_value(obj) self.assertEqual(value, 'test_value') def test_get_value_missing(self): accessor = self.create_accessor() accessor.attr = '' obj = AttributeDummy('test_value') with self.assertRaises(AttributeError): accessor.get_value(obj) def test_get_value_error(self): accessor = self.create_accessor() accessor.attr = 'other_attr' obj = AttributeDummy('test_value') with self.assertRaises(AttributeError): accessor.get_value(obj) def test_can_set_value(self): accessor = self.create_accessor() obj = AttributeDummy('test_value') can_set = accessor.can_set_value(obj) self.assertTrue(can_set) def test_can_set_value_false(self): accessor = AttributeDataAccessor() obj = AttributeDummy('test_value') can_set = accessor.can_set_value(obj) self.assertFalse(can_set) def test_set_value(self): accessor = self.create_accessor() obj = AttributeDummy('test_value') accessor.set_value(obj, 'new_value') self.assertEqual(obj.attr_value, 'new_value') def test_set_value_extended(self): accessor = self.create_accessor() accessor.attr = 'attr_value.attr_value' obj = AttributeDummy(AttributeDummy('test_value')) accessor.set_value(obj, 'new_value') self.assertEqual(obj.attr_value.attr_value, 'new_value') def test_set_value_error(self): accessor = AttributeDataAccessor() obj = AttributeDummy('test_value') with self.assertRaises(DataViewSetError): accessor.set_value(obj, 'new_value') def test_attr_updated(self): accessor = self.create_accessor() accessor.observe(self.accessor_observer, 'updated') with self.assertTraitChanges(accessor, 'updated', count=1): accessor.attr = 'other_attr' self.assertEqual(self.accessor_event.new, (accessor, 'value')) class TestIndexDataAccessor(unittest.TestCase, DataAccessorMixin): def create_accessor(self): return IndexDataAccessor( index=1, value_type=TextValue(), ) def test_defaults(self): accessor = IndexDataAccessor() self.assertEqual(accessor.index, 0) self.assertIsNone(accessor.value_type) self.assertIsInstance(accessor.title_type, TextValue) self.assertEqual(accessor.title, '0') def test_typical_defaults(self): accessor = self.create_accessor() self.assertIsInstance(accessor.title_type, TextValue) self.assertEqual(accessor.title, '1') def test_get_value(self): accessor = self.create_accessor() obj = ['zero', 'one', 'two', 'three'] value = accessor.get_value(obj) self.assertEqual(value, 'one') def test_get_value_out_of_bounds(self): accessor = self.create_accessor() accessor.index = 10 obj = ['zero', 'one', 'two', 'three'] with self.assertRaises(IndexError): accessor.get_value(obj) def test_can_set_value(self): accessor = self.create_accessor() obj = ['zero', 'one', 'two', 'three'] can_set = accessor.can_set_value(obj) self.assertTrue(can_set) def test_can_set_value_false(self): accessor = self.create_accessor() obj = ['zero'] can_set = accessor.can_set_value(obj) self.assertFalse(can_set) def test_can_set_value_immuatble(self): accessor = self.create_accessor() obj = ('zero', 'one', 'two', 'three') can_set = accessor.can_set_value(obj) self.assertFalse(can_set) def test_set_value(self): accessor = self.create_accessor() obj = ['zero', 'one', 'two', 'three'] accessor.set_value(obj, 'new_value') self.assertEqual(obj[1], 'new_value') def test_set_value_error(self): accessor = self.create_accessor() obj = ['zero'] with self.assertRaises(DataViewSetError): accessor.set_value(obj, 'new_value') def test_index_updated(self): accessor = self.create_accessor() accessor.observe(self.accessor_observer, 'updated') with self.assertTraitChanges(accessor, 'updated', count=1): accessor.index = 2 self.assertEqual(self.accessor_event.new, (accessor, 'value')) class TestKeyDataAccessor(unittest.TestCase, DataAccessorMixin): def create_accessor(self): return KeyDataAccessor( key='one', value_type=TextValue(), ) def test_defaults(self): accessor = KeyDataAccessor() self.assertIsNone(accessor.key) self.assertIsNone(accessor.value_type) self.assertIsInstance(accessor.title_type, TextValue) self.assertEqual(accessor.title, 'None') def test_typical_defaults(self): accessor = self.create_accessor() self.assertIsInstance(accessor.title_type, TextValue) self.assertEqual(accessor.title, 'One') def test_unhashable_error(self): accessor = self.create_accessor() with self.assertRaises(TraitError): accessor.key = [] def test_get_value(self): accessor = self.create_accessor() obj = {'one': 'a', 'two': 'b'} value = accessor.get_value(obj) self.assertEqual(value, 'a') def test_get_value_missing(self): accessor = self.create_accessor() accessor.key = 'three' obj = {'one': 'a', 'two': 'b'} with self.assertRaises(KeyError): accessor.get_value(obj) def test_can_set_value(self): accessor = self.create_accessor() obj = {'one': 'a', 'two': 'b'} can_set = accessor.can_set_value(obj) self.assertTrue(can_set) def test_can_set_value_new(self): accessor = self.create_accessor() accessor.key = 'three' obj = {'one': 'a', 'two': 'b'} can_set = accessor.can_set_value(obj) self.assertTrue(can_set) def test_can_set_value_immutable(self): accessor = self.create_accessor() # TODO: eventually replace with frozenmap in 3.9 obj = MappingProxyType({'one': 'a', 'two': 'b'}) can_set = accessor.can_set_value(obj) self.assertFalse(can_set) def test_set_value(self): accessor = self.create_accessor() obj = {'one': 'a', 'two': 'b'} accessor.set_value(obj, 'new_value') self.assertEqual(obj['one'], 'new_value') def test_set_value_new(self): accessor = self.create_accessor() accessor.key = 'three' obj = {'one': 'a', 'two': 'b'} accessor.set_value(obj, 'new_value') self.assertEqual(obj['three'], 'new_value') def test_set_value_error(self): accessor = KeyDataAccessor() accessor.key = 'one' obj = MappingProxyType({'one': 'a', 'two': 'b'}) with self.assertRaises(DataViewSetError): accessor.set_value(obj, 'new_value') def test_key_updated(self): accessor = self.create_accessor() accessor.observe(self.accessor_observer, 'updated') with self.assertTraitChanges(accessor, 'updated', count=1): accessor.key = 2 self.assertEqual(self.accessor_event.new, (accessor, 'value')) pyface-7.4.0/pyface/data_view/data_models/tests/test_api.py0000644000076500000240000000335214176222673024761 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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_all_imports_exclude_numpy_dependencies(self): # These objects do not depend on NumPy from pyface.data_view.data_models.api import ( # noqa: F401 AbstractDataAccessor, AttributeDataAccessor, ConstantDataAccessor, IndexDataAccessor, KeyDataAccessor, RowTableDataModel, ) def test_import_with_numpy_dependency(self): # These objects require NumPy. try: import numpy # noqa: F401 except ImportError: self.skipTest("NumPy not available.") from pyface.data_view.data_models.api import ( # noqa: F401 ArrayDataModel, ) def test_api_items_count(self): # This test helps developer to keep the above list # up-to-date. Bump the number when the API content changes. from pyface.data_view.data_models import api expected_count = 6 try: import numpy # noqa: F401 except ImportError: pass else: expected_count += 1 items_in_api = { name for name in dir(api) if not name.startswith("_") } self.assertEqual(len(items_in_api), expected_count) pyface-7.4.0/pyface/data_view/data_models/__init__.py0000644000076500000240000000000014176222673023531 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/data_view/data_models/data_accessors.py0000644000076500000240000002105414176222673024764 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Classes for extracting data from objects This module provides helper classes for the row table data model and related classes for extracting data from an object in a consistent way. """ from abc import abstractmethod from collections.abc import Hashable, MutableMapping, MutableSequence from traits.api import ( ABCHasStrictTraits, Any, Event, Instance, Int, Str, observe ) from traits.trait_base import xgetattr, xsetattr from pyface.data_view.abstract_data_model import DataViewSetError from pyface.data_view.abstract_value_type import AbstractValueType from pyface.data_view.value_types.api import TextValue class AbstractDataAccessor(ABCHasStrictTraits): """ Accessor that gets and sets data on an object. """ #: A human-readable label for the accessor. title = Str() #: The value type of the title of this accessor, suitable for use in a #: header. title_type = Instance(AbstractValueType, factory=TextValue) #: The value type of the data accessed. value_type = Instance(AbstractValueType) #: An event fired when accessor is updated update. The payload is #: a tuple of the accessor info and whether the title or value changed #: (or both). updated = Event @abstractmethod def get_value(self, obj): """ Return a value for the provided object. Parameters ---------- obj : any The object that contains the data. Returns ------- value : any The data value contained in the object. """ raise NotImplementedError() def can_set_value(self, obj): """ Return whether the value can be set on the provided object. Parameters ---------- obj : any The object that contains the data. Returns ------- can_set_value : bool Whether or not the value can be set. """ return False def set_value(self, obj, value): """ Set the value on the provided object. Parameters ---------- obj : any The object that contains the data. value : any The data value to set. Raises ------- DataViewSetError If setting the value fails. """ raise DataViewSetError( "Cannot set {!r} column of {!r}.".format(self.title, obj) ) # trait observers @observe('title,title_type.updated') def _title_updated(self, event): self.updated = (self, 'title') @observe('value_type.updated') def _value_type_updated(self, event): self.updated = (self, 'value') class ConstantDataAccessor(AbstractDataAccessor): """ DataAccessor that returns a constant value. """ #: The value to return. value = Any() def get_value(self, obj): """ Return the value ignoring the provided object. Parameters ---------- obj : any An object. Returns ------- value : any The data value contained in this class' value trait. """ return self.value @observe('value') def _value_updated(self, event): self.updated = (self, 'value') class AttributeDataAccessor(AbstractDataAccessor): """ DataAccessor that presents an extended attribute on an object. This is suitable for use with Python objects, including HasTraits classes. """ #: The extended attribute name of the trait holding the value. attr = Str() def get_value(self, obj): """ Return the attribute value for the provided object. Parameters ---------- obj : any The object that contains the data. Returns ------- value : any The data value contained in the object's attribute. """ return xgetattr(obj, self.attr) def can_set_value(self, obj): """ Return whether the value can be set on the provided object. Parameters ---------- obj : any The object that contains the data. Returns ------- can_set_value : bool Whether or not the value can be set. """ return bool(self.attr) def set_value(self, obj, value): if not self.can_set_value(obj): raise DataViewSetError( "Attribute is not specified for {!r}".format(self) ) xsetattr(obj, self.attr, value) @observe('attr') def _attr_updated(self, event): self.updated = (self, 'value') def _title_default(self): # create human-friendly version of extended attribute attr = self.attr.split('.')[-1] title = attr.replace('_', ' ').title() return title class IndexDataAccessor(AbstractDataAccessor): """ DataAccessor that presents an index on a sequence object. This is suitable for use with a sequence. """ #: The index in a sequence which holds the value. index = Int() def get_value(self, obj): """ Return the indexed value for the provided object. Parameters ---------- obj : sequence The object that contains the data. Returns ------- value : any The data value contained in the object at the index. """ return obj[self.index] def can_set_value(self, obj): """ Return whether the value can be set on the provided object. Parameters ---------- obj : any The object that contains the data. Returns ------- can_set_value : bool Whether or not the value can be set. """ return isinstance(obj, MutableSequence) and 0 <= self.index < len(obj) def set_value(self, obj, value): """ Set the value on the provided object. Parameters ---------- obj : any The object that contains the data. value : any The data value to set. Raises ------- DataViewSetError If setting the value fails. """ if not self.can_set_value(obj): raise DataViewSetError( "Cannot set {!r} index of {!r}.".format(self.index, obj) ) obj[self.index] = value @observe('index') def _index_updated(self, event): self.updated = (self, 'value') def _title_default(self): title = str(self.index) return title class KeyDataAccessor(AbstractDataAccessor): """ DataAccessor that presents an item on a mapping object. This is suitable for use with a mapping, such as a dictionary. """ #: The key in the mapping holding the value. key = Instance(Hashable) def get_value(self, obj): """ Return the key's value for the provided object. Parameters ---------- obj : mapping The object that contains the data. Returns ------- value : any The data value contained in the given key of the object. """ return obj[self.key] def can_set_value(self, obj): """ Set the value on the provided object. Parameters ---------- obj : mapping The object that contains the data. value : any The data value to set. Raises ------- DataViewSetError If setting the value fails. """ return isinstance(obj, MutableMapping) def set_value(self, obj, value): """ Set the value on the provided object. Parameters ---------- obj : any The object that contains the data. value : any The data value to set. Raises ------- DataViewSetError If setting the value fails. """ if not self.can_set_value(obj): raise DataViewSetError( "Cannot set {!r} key of {!r}.".format(self.key, obj) ) obj[self.key] = value @observe('key') def _key_updated(self, event): self.updated = (self, 'value') def _title_default(self): title = str(self.key).title() return title pyface-7.4.0/pyface/data_view/data_models/api.py0000644000076500000240000000230314176222673022553 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 ``pyface.data_view.data_models`` subpackage. Data Accessors -------------- - :class:`~.AbstractDataAccessor` - :class:`~.AttributeDataAccessor` - :class:`~.ConstantDataAccessor` - :class:`~.IndexDataAccessor` - :class:`~.KeyDataAccessor` Data Models ----------- - :class:`~.RowTableDataModel` - :class:`~.ArrayDataModel`. Note that this data model is only available if ``numpy`` is available in the environment. """ try: import numpy # noqa: F401 except ImportError: pass else: del numpy from .array_data_model import ArrayDataModel # noqa: F401 from .data_accessors import ( # noqa: F401 AbstractDataAccessor, AttributeDataAccessor, ConstantDataAccessor, IndexDataAccessor, KeyDataAccessor ) from .row_table_data_model import RowTableDataModel # noqa: F401 pyface-7.4.0/pyface/data_view/data_models/row_table_data_model.py0000644000076500000240000002055514176222673026142 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 row-oriented data model implementation. This module provides a concrete implementation of a data model for the case of non-hierarchical, row-oriented data. """ from collections.abc import Sequence from traits.api import Instance, List, observe from traits.observation.api import trait from pyface.data_view.abstract_data_model import ( AbstractDataModel, DataViewSetError ) from pyface.data_view.index_manager import IntIndexManager from pyface.data_view.data_models.data_accessors import AbstractDataAccessor class RowTableDataModel(AbstractDataModel): """ A data model that presents a sequence of objects as rows. The data is expected to be a sequence of row objects, each object providing values for the columns via an AbstractDataAccessor subclass. Concrete implementations can be found in the data_accessors module that get data from attributes, indices of sequences, and keys of mappings, but for more complex situations, custom accessors can be defined. """ #: A sequence of objects to display as rows. data = Instance(Sequence, allow_none=False) #: An object which describes how to map data for the row headers. row_header_data = Instance(AbstractDataAccessor, allow_none=False) #: An object which describes how to map data for each column. column_data = List(Instance(AbstractDataAccessor, allow_none=False)) #: The index manager that helps convert toolkit indices to data view #: indices. index_manager = Instance(IntIndexManager, args=(), allow_none=False) # Data structure methods def get_column_count(self): """ How many columns in the data view model. Returns ------- column_count : non-negative int The number of columns that the data view provides. """ return len(self.column_data) def can_have_children(self, row): """ Whether or not a row can have child rows. Only the root has children. Parameters ---------- row : sequence of int The indices of the row as a sequence from root to leaf. Returns ------- can_have_children : bool Whether or not the row can ever have child rows. """ return len(row) == 0 def get_row_count(self, row): """ How many child rows the row currently has. Parameters ---------- row : sequence of int The indices of the row as a sequence from root to leaf. Returns ------- row_count : non-negative int The number of child rows that the row has. """ if len(row) == 0: return len(self.data) else: return 0 # Data value methods def get_value(self, row, column): """ Return the Python value for the row and column. This uses the row_header_data and column_data accessors to extract values for the row and column. Parameters ---------- row : sequence of int The indices of the row as a sequence from root to leaf. column : sequence of int The indices of the column as a sequence of length 0 or 1. Returns ------- value : any The value represented by the given row and column. """ if len(column) == 0: column_data = self.row_header_data else: column_data = self.column_data[column[0]] if len(row) == 0: return column_data.title obj = self.data[row[0]] return column_data.get_value(obj) def can_set_value(self, row, column): """ Whether the value in the indicated row and column can be set. This uses the row_header_data and column_data accessors to determine if the value may be changed. Parameters ---------- row : sequence of int The indices of the row as a sequence from root to leaf. column : sequence of int The indices of the column as a sequence of length 0 or 1. Returns ------- can_set_value : bool Whether or not the value can be set. """ if len(row) == 0: return False if len(column) == 0: column_data = self.row_header_data else: column_data = self.column_data[column[0]] obj = self.data[row[0]] return column_data.can_set_value(obj) def set_value(self, row, column, value): """ Set the Python value for the row and column. This uses the row_header_data and column_data accessors to set the value. Parameters ---------- row : sequence of int The indices of the row as a sequence from root to leaf. column : sequence of int The indices of the column as a sequence of length 0 or 1. value : any The new value for the given row and column. Raises ------- DataViewSetError If the value cannot be set. """ if len(row) == 0: raise DataViewSetError("Can't set column titles.") if len(column) == 0: column_data = self.row_header_data else: column_data = self.column_data[column[0]] obj = self.data[row[0]] column_data.set_value(obj, value) self.values_changed = (row, column, row, column) def get_value_type(self, row, column): """ Return the value type of the given row and column. This uses the row_header_data and column_data accessors to get the value type. Parameters ---------- row : sequence of int The indices of the row as a sequence from root to leaf. column : sequence of int The indices of the column as a sequence of length 0 or 1. Returns ------- value_type : AbstractValueType or None The value type of the given row and column, or None if no value should be displayed. """ if len(column) == 0: column_data = self.row_header_data else: column_data = self.column_data[column[0]] if len(row) == 0: return column_data.title_type return column_data.value_type # data update methods @observe("data") def _update_data(self, event): self.structure_changed = True @observe(trait("data", notify=False).list_items(optional=True)) def _update_data_items(self, event): if len(event.added) != len(event.removed): # number of rows has changed self.structure_changed = True else: if isinstance(event.index, int): start = event.index stop = min(event.index + len(event.added), len(self.data)) - 1 else: start = event.index.start stop = min(event.index.stop, len(self.data)) - 1 self.values_changed = ((start,), (), (stop,), ()) @observe('row_header_data') def _update_row_header_data(self, event): self.values_changed = ((), (), (), ()) @observe('row_header_data:updated') def _update_row_header_data_event(self, event): if event.new[1] == 'value': if len(self.data) > 0: self.values_changed = ((0,), (), (len(self.data) - 1,), ()) else: self.values_changed = ((), (), (), ()) @observe('column_data.items') def _update_all_column_data_items(self, event): self.structure_changed = True @observe('column_data:items:updated') def _update_column_data(self, event): index = self.column_data.index(event.new[0]) if event.new[1] == 'value': if len(self.data) > 0: self.values_changed = ( (0,), (index,), (len(self.data) - 1,), (index,) ) else: self.values_changed = ((), (index,), (), (index,)) # default data value def _data_default(self): return [] pyface-7.4.0/pyface/data_view/data_wrapper.py0000644000076500000240000000100614176222673022176 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.toolkit import toolkit_object DataWrapper = toolkit_object('data_view.data_wrapper:DataWrapper') pyface-7.4.0/pyface/data_view/data_view_errors.py0000644000076500000240000000143214176222673023067 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Provide an Exception classes for the the DataView code. """ class DataViewError(ValueError): """ The base exception class for DataView errors. """ pass class DataViewGetError(DataViewError): """ An exception raised when getting a value fails. """ pass class DataViewSetError(DataViewError): """ An exception raised when setting a value fails. """ pass pyface-7.4.0/pyface/data_view/exporters/0000755000076500000240000000000014176460550021207 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/data_view/exporters/item_exporter.py0000644000076500000240000000420014176222673024445 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.data_view.abstract_data_exporter import AbstractDataExporter from pyface.data_view.data_view_errors import DataViewGetError class ItemExporter(AbstractDataExporter): """ Export a single item from a data view. This is suitable for drag and drop or copying of the content of a single item in a data view. If passed an multiple items it will fail by raising ``DataViewGetError``; drag and drop support will then ignore this as an exporter to use. """ def add_data(self, data_wrapper, model, indices): """ Add data to the data wrapper from the model and indices. Parameters ---------- data_wrapper : DataWrapper instance The data wrapper that will be used to export data. model : AbstractDataModel instance The data model holding the data. indices : list of (row, column) index pairs The indices where the data is to be stored. """ # only export single item values if len(indices) == 1: super().add_data(data_wrapper, model, indices) def get_data(self, model, indices): """ Get the data to be exported from the model and indices. Parameters ---------- model : AbstractDataModel instance The data model holding the data. indices : list of (row, column) index pairs The indices where the data is to be stored. Returns ------- data : any The data, of a type that can be serialized by the format. """ if len(indices) != 1: raise DataViewGetError("ItemExporter can only export single values") row, column = indices[0] return self.get_value(model, row, column) pyface-7.4.0/pyface/data_view/exporters/tests/0000755000076500000240000000000014176460550022351 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/data_view/exporters/tests/test_row_exporter.py0000644000076500000240000000444314176222673026530 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 itertools import count from unittest import TestCase from unittest.mock import Mock from pyface.data_view.exporters.row_exporter import RowExporter from pyface.data_view.i_data_wrapper import DataFormat trivial_format = DataFormat( 'null/null', lambda x: str(x).encode('utf-8'), lambda x: x, ) class TestRowExporter(TestCase): def setUp(self): self.value_type = Mock() self.value_type.has_text = Mock(return_value=False) self.value_type.has_editor_value = Mock(return_value=True) self.value_type.get_editor_value = Mock( return_value=1, side_effect=count(), ) self.model = Mock() self.model.get_column_count = Mock(return_value=3) self.model.get_value = Mock(return_value=0) self.model.get_value_type = Mock(return_value=self.value_type) def test_get_data(self): exporter = RowExporter(format=trivial_format) result = exporter.get_data(self.model, [((0,), (0,)), ((1,), ())]) self.assertEqual(result, [[0, 1, 2], [3, 4, 5]]) def test_get_data_deduplicate(self): exporter = RowExporter(format=trivial_format) result = exporter.get_data( self.model, [ ((0,), (0,)), ((0,), (2,)), ((1,), ()), ]) self.assertEqual(result, [[0, 1, 2], [3, 4, 5]]) def test_get_data_row_headers(self): exporter = RowExporter(format=trivial_format, row_headers=True) result = exporter.get_data(self.model, [((0,), (0,)), ((1,), ())]) self.assertEqual(result, [[0, 1, 2, 3], [4, 5, 6, 7]]) def test_get_data_column_headers(self): exporter = RowExporter(format=trivial_format, column_headers=True) result = exporter.get_data(self.model, [((0,), (0,)), ((1,), ())]) self.assertEqual(result, [[0, 1, 2], [3, 4, 5], [6, 7, 8]]) pyface-7.4.0/pyface/data_view/exporters/tests/test_item_exporter.py0000644000076500000240000000436614176222673026663 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 unittest.mock import Mock from pyface.data_view.exporters.item_exporter import ItemExporter from pyface.data_view.data_wrapper import DataWrapper from pyface.data_view.i_data_wrapper import DataFormat trivial_format = DataFormat( 'null/null', lambda x: str(x).encode('utf-8'), lambda x: x, ) class TestItemExporter(TestCase): def setUp(self): self.value_type = Mock() self.value_type.has_text = Mock(return_value=True) self.value_type.get_text = Mock(return_value='text') self.value_type.has_editor_value = Mock(return_value=True) self.value_type.get_editor_value = Mock(return_value=1) self.model = Mock() self.model.get_value = Mock(return_value=0.0) self.model.get_value_type = Mock(return_value=self.value_type) def test_get_data_(self): exporter = ItemExporter(format=trivial_format) result = exporter.get_data(self.model, [((0,), (0,))]) self.assertEqual(result, 1) def test_add_data_(self): exporter = ItemExporter(format=trivial_format) data_wrapper = DataWrapper() exporter.add_data(data_wrapper, self.model, [((0,), (0,))]) self.assertTrue(data_wrapper.has_format(trivial_format)) self.assertEqual(data_wrapper.get_mimedata('null/null'), b'1') def test_add_data_length_0(self): exporter = ItemExporter(format=trivial_format) data_wrapper = DataWrapper() exporter.add_data(data_wrapper, self.model, []) self.assertFalse(data_wrapper.has_format(trivial_format)) def test_add_data_length_2(self): exporter = ItemExporter(format=trivial_format) data_wrapper = DataWrapper() exporter.add_data(data_wrapper, self.model, [((), ()), ((0,), (0,))]) self.assertFalse(data_wrapper.has_format(trivial_format)) pyface-7.4.0/pyface/data_view/exporters/tests/__init__.py0000644000076500000240000000000014176222673024452 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/data_view/exporters/tests/test_api.py0000644000076500000240000000201514176222673024533 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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_all_imports(self): from pyface.data_view.exporters.api import ( # noqa: F401 ItemExporter, RowExporter, ) def test_api_items_count(self): # This test helps developer to keep the above list # up-to-date. Bump the number when the API content changes. from pyface.data_view.exporters import api items_in_api = { name for name in dir(api) if not name.startswith("_") } self.assertEqual(len(items_in_api), 2) pyface-7.4.0/pyface/data_view/exporters/__init__.py0000644000076500000240000000000014176222673023310 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/data_view/exporters/api.py0000644000076500000240000000116114176222673022333 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 ``pyface.data_view.exporters`` subpackage. Exporters --------- - :class:`~.ItemExporter` - :class:`~.RowExporter` """ from .item_exporter import ItemExporter from .row_exporter import RowExporter pyface-7.4.0/pyface/data_view/exporters/row_exporter.py0000644000076500000240000000443014176222673024323 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 from pyface.data_view.abstract_data_exporter import AbstractDataExporter class RowExporter(AbstractDataExporter): """ Export a collection of rows from a data view as a list of lists. This is suitable for drag and drop or copying of the content of multiple selected rows. This exports a list of data associated with each row in the indices. Each row item is itself a list of values extracted from the model. If the format mimetype is a text mimetype, it will use the ``get_text()`` method to extract the values, otherwise it will try to use the editor value if it exists, and failing that the raw value returned from the model. """ #: Whether or not to include row headers. row_headers = Bool() #: Whether or not to include column headers. column_headers = Bool() def get_data(self, model, indices): """ Get the data to be exported from the model and indices. This exports a list of data associated with each row in the indices. Each row item is itself a list of values extracted from the model. Parameters ---------- model : AbstractDataModel instance The data model holding the data. indices : list of (row, column) index pairs The indices where the data is to be stored. Returns ------- data : any The data, of a type that can be serialized by the format. """ rows = sorted({row for row, column in indices}) n_columns = model.get_column_count() columns = [(column,) for column in range(n_columns)] if self.column_headers: rows = [()] + rows if self.row_headers: columns = [()] + columns return [ [self.get_value(model, row, column,) for column in columns] for row in rows ] pyface-7.4.0/pyface/data_view/i_data_view_widget.py0000644000076500000240000003131714176222673023353 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 logging from traits.api import ( Bool, Enum, HasTraits, Instance, List, Property, TraitError, Tuple, cached_property, ) from pyface.data_view.abstract_data_model import AbstractDataModel from pyface.data_view.abstract_data_exporter import AbstractDataExporter from pyface.i_drop_handler import IDropHandler from pyface.i_layout_widget import ILayoutWidget logger = logging.getLogger(__name__) class IDataViewWidget(ILayoutWidget): """ Interface for data view widgets. """ #: The data model for the data view. data_model = Instance(AbstractDataModel, allow_none=False) #: Whether or not the column headers are visible. header_visible = Bool(True) #: The global drop handlers for the data view. These are intended to #: handle drop actions which either affect the whole data view, or where #: the data handler can work out how to change the underlying data without #: additional input. drop_handlers = List(Instance(IDropHandler, allow_none=False)) #: What can be selected. Implementations may optionally allow "column" #: and "item" selection types. selection_type = Enum("row",) #: How selections are modified. Implementations may optionally allow #: "none" for no selection, or possibly other multiple selection modes #: as supported by the toolkit. selection_mode = Enum("extended", "single") #: The selected indices in the view. selection = List(Tuple) #: Exporters available for the DataViewWidget. exporters = List(Instance(AbstractDataExporter)) class MDataViewWidget(HasTraits): """ Mixin class for data view widgets. """ # IDataViewWidget Interface traits -------------------------------------- #: The data model for the data view. data_model = Instance(AbstractDataModel, allow_none=False) #: Whether or not the column headers are visible. header_visible = Bool(True) #: The global drop handlers for the data view. These are intended to #: handle drop actions which either affect the whole data view, or where #: the data handler can work out how to change the underlying data without #: additional input. drop_handlers = List(Instance(IDropHandler, allow_none=False)) #: The selected indices in the view. This should never be mutated, any #: changes should be by replacement of the entire list. selection = Property(observe='_selection.items') #: Exporters available for the DataViewWidget. exporters = List(Instance(AbstractDataExporter)) # Private traits -------------------------------------------------------- #: Whether the selection is currently being updated. _selection_updating_flag = Bool() #: The selected indices in the view. This should never be mutated, any #: changes should be by replacement of the entire list. _selection = List(Tuple) # ------------------------------------------------------------------------ # MDataViewWidget Interface # ------------------------------------------------------------------------ def _header_visible_updated(self, event): """ Observer for header_visible trait. """ if self.control is not None: self._set_control_header_visible(event.new) def _get_control_header_visible(self): """ Toolkit specific method to get the visibility of the header. Returns ------- control_header_visible : bool Whether or not the control's header is visible. """ raise NotImplementedError() def _set_control_header_visible(self, control_header_visible): """ Toolkit specific method to set the visibility of the header. Parameters ---------- control_header_visible : bool Whether or not the control's header is visible. """ raise NotImplementedError() def _selection_type_updated(self, event): """ Observer for selection_type trait. """ if self.control is not None: self._set_control_selection_type(event.new) self.selection = [] def _get_control_selection_type(self): """ Toolkit specific method to get the selection type. Returns ------- selection_type : str The type of selection the control is using. """ raise NotImplementedError() def _set_control_selection_type(self, selection_type): """ Toolkit specific method to change the selection type. Parameters ---------- selection_type : str The type of selection the control is using. """ raise NotImplementedError() def _selection_mode_updated(self, event): """ Observer for selection_mode trait. """ if self.control is not None: self._set_control_selection_mode(event.new) self.selection = [] def _get_control_selection_mode(self): """ Toolkit specific method to get the selection mode. Returns ------- selection_mode : str The selection mode the control is using (eg. single vs. extended selection). """ raise NotImplementedError() def _set_control_selection_mode(self, selection_mode): """ Toolkit specific method to change the selection mode. Parameters ---------- selection_mode : str The selection mode the control is using (eg. single vs. extended selection). """ raise NotImplementedError() def _selection_updated(self, event): """ Observer for selection trait. """ if self.control is not None and not self._selection_updating_flag: with self._selection_updating(): self._set_control_selection(self.selection) def _get_control_selection(self): """ Toolkit specific method to get the selection. Returns ------- selection : list of pairs of row and column indices The selected elements of the control. """ raise NotImplementedError() def _set_control_selection(self, selection): """ Toolkit specific method to change the selection. Parameters ---------- selection : list of pairs of row and column indices The selected elements of the control. """ raise NotImplementedError() def _observe_control_selection(self, remove=False): """ Toolkit specific method to watch for changes in the selection. The _update_selection method is available as a toolkit-independent callback when the selection changes, but particular toolkits may choose to implement their own callbacks with similar functionality if appropriate. """ raise NotImplementedError() def _update_selection(self, *args, **kwargs): """ Handle a toolkit even that changes the selection. This is designed to be usable as a callback for a toolkit event or signal handler, so it accepts any arguments. """ if not self._selection_updating_flag: with self._selection_updating(): self._selection = self._get_control_selection() # ------------------------------------------------------------------------ # Widget Interface # ------------------------------------------------------------------------ def _create(self): """ Creates the toolkit specific control. This method should create the control and assign it to the :py:attr:``control`` trait. """ super()._create() self.show(self.visible) self.enable(self.enabled) def _initialize_control(self): """ Initializes the toolkit specific control. """ logger.debug('Initializing DataViewWidget') super()._initialize_control() self._set_control_header_visible(self.header_visible) self._set_control_selection_mode(self.selection_mode) self._set_control_selection_type(self.selection_type) self._set_control_selection(self.selection) def _add_event_listeners(self): logger.debug('Adding DataViewWidget listeners') super()._add_event_listeners() self.observe( self._header_visible_updated, 'header_visible', dispatch='ui', ) self.observe( self._selection_type_updated, 'selection_type', dispatch='ui', ) self.observe( self._selection_mode_updated, 'selection_mode', dispatch='ui', ) self.observe( self._selection_updated, '_selection.items', dispatch='ui', ) if self.control is not None: self._observe_control_selection() def _remove_event_listeners(self): logger.debug('Removing DataViewWidget listeners') if self.control is not None: self._observe_control_selection(remove=True) self.observe( self._header_visible_updated, 'header_visible', dispatch='ui', remove=True, ) self.observe( self._selection_type_updated, 'selection_type', dispatch='ui', remove=True, ) self.observe( self._selection_mode_updated, 'selection_mode', dispatch='ui', remove=True, ) self.observe( self._selection_updated, '_selection.items', dispatch='ui', remove=True, ) super()._remove_event_listeners() # ------------------------------------------------------------------------ # Private methods # ------------------------------------------------------------------------ @contextmanager def _selection_updating(self): """ Context manager to prevent loopback when updating selections. """ if self._selection_updating_flag: yield else: self._selection_updating_flag = True try: yield finally: self._selection_updating_flag = False # Trait property handlers @cached_property def _get_selection(self): return self._selection def _set_selection(self, selection): if self.selection_mode == 'none' and len(selection) != 0: raise TraitError( "Selection must be empty when selection_mode is 'none', " "got {!r}".format(selection) ) elif self.selection_mode == 'single' and len(selection) > 1: raise TraitError( "Selection must have at most one element when selection_mode " "is 'single', got {!r}".format(selection) ) if self.selection_type == 'row': for row, column in selection: if column != (): raise TraitError( "Column values must be () when selection_type is " "'row', got {!r}".format(column) ) if not self.data_model.is_row_valid(row): raise TraitError( "Invalid row index {!r}".format(row) ) elif self.selection_type == 'column': for row, column in selection: if not (self.data_model.is_row_valid(row) and self.data_model.can_have_children(row) and self.data_model.get_row_count(row) > 0): raise TraitError( "Row values must have children when selection_type " "is 'column', got {!r}".format(column) ) if not self.data_model.is_column_valid(column): raise TraitError( "Invalid column index {!r}".format(column) ) else: for row, column in selection: if not self.data_model.is_row_valid(row): raise TraitError( "Invalid row index {!r}".format(row) ) if not self.data_model.is_column_valid(column): raise TraitError( "Invalid column index {!r}".format(column) ) self._selection = selection pyface-7.4.0/pyface/data_view/abstract_data_exporter.py0000644000076500000240000000625714176222673024266 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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, Instance from pyface.data_view.data_view_errors import DataViewGetError from pyface.data_view.i_data_wrapper import DataFormat class AbstractDataExporter(ABCHasStrictTraits): """ ABC for classes that export data from a data view. Concrete classes should implement the ``get_data`` method so that it produces a value that can be serialized using the provided ``format``. Some convenience methods are provided to get text values, as that is a common use-case. """ #: The DataFormat used to serialize the exported data. format = Instance(DataFormat) #: Whether to get item data from the text channel, if available. is_text = Bool() def add_data(self, data_wrapper, model, indices): """ Add data to the data wrapper from the model and indices. Parameters ---------- data_wrapper : DataWrapper instance The data wrapper that will be used to export data. model : AbstractDataModel instance The data model holding the data. indices : list of (row, column) index pairs The indices where the data is to be stored. """ try: data = self.get_data(model, indices) data_wrapper.set_format(self.format, data) except DataViewGetError: pass @abstractmethod def get_data(self, model, indices): """ Get the data to be exported from the model and indices. Parameters ---------- model : AbstractDataModel instance The data model holding the data. indices : list of (row, column) index pairs The indices where the data is to be stored. Returns ------- data : any The data, of a type that can be serialized by the format. """ raise NotImplementedError() def get_value(self, model, row, column): """ Utility method to extract a value at a given index. If ``is_text`` is True, it will use the ``get_text()`` method to extract the value, otherwise it will try to use the editor value if it exists, and failing that the raw value returned from the model. """ value_type = model.get_value_type(row, column) if self.is_text: if value_type.has_text(model, row, column): value = value_type.get_text(model, row, column) else: value = "" elif value_type.has_editor_value(model, row, column): value = value_type.get_editor_value(model, row, column) else: value = model.get_value(row, column) return value def _is_text_default(self): return self.format.mimetype.startswith('text/') pyface-7.4.0/pyface/data_view/tests/0000755000076500000240000000000014176460550020316 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/data_view/tests/test_data_formats.py0000644000076500000240000001505714176222673024405 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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, Int, Str from traits.testing.optional_dependencies import numpy as np, requires_numpy from traits.version import version from pyface.data_view.data_formats import ( from_csv, from_csv_column, from_csv_row, from_npy, from_json, to_csv, to_csv_column, to_csv_row, to_npy, to_json, ) def default_extractor(obj): return obj.__getstate__() def object_hook(data): obj = ExampleObject() obj.__setstate__(data) return obj class ExampleObject(HasTraits): a = Int(1) b = Str("two") c = Str("three", transient=True) class TestJSON(TestCase): def test_to_json(self): test_data = {'a': 1, 'b': 'two'} raw_data = to_json(test_data) self.assertEqual(raw_data, b'{"a":1,"b":"two"}') def test_to_json_default(self): test_data = ExampleObject() raw_data = to_json(test_data, default=default_extractor) self.assertEqual( raw_data, b'{"a":1,"b":"two","__traits_version__":"' + version.encode('utf-8') + b'"}' ) def test_from_json(self): raw_data = b'{"a":1,"b":"two"}' data = from_json(raw_data) self.assertEqual(data, {'a': 1, 'b': 'two'}) def test_from_json_object_hook(self): raw_data = ( b'{"a":2,"b":"four","__traits_version__":"' + version.encode('utf-8') + b'"}' ) data = from_json(raw_data, object_hook=object_hook) self.assertIsInstance(data, ExampleObject) self.assertEqual(data.a, 2) self.assertEqual(data.b, 'four') self.assertEqual(data.c, 'three') class TestCSV(TestCase): def test_to_csv(self): test_data = [['one', 2], ['three,four', 5]] raw_data = to_csv(test_data) self.assertEqual(raw_data, b'one,2\r\n"three,four",5\r\n') def test_to_csv_delimiter(self): test_data = [['one', 2], ['three,four', 5]] raw_data = to_csv(test_data, delimiter='\t') self.assertEqual(raw_data, b'one\t2\r\nthree,four\t5\r\n') def test_to_csv_encoding(self): test_data = [['øne', 2], ['three,four', 5]] raw_data = to_csv(test_data, encoding='latin-1') self.assertEqual(raw_data, b'\xf8ne,2\r\n"three,four",5\r\n') def test_from_csv(self): raw_data = b'one,2\r\n"three,four",5\r\n' data = from_csv(raw_data) self.assertEqual(data, [['one', '2'], ['three,four', '5']]) def test_from_csv_delimiter(self): raw_data = b'one\t2\r\nthree,four\t5\r\n' data = from_csv(raw_data, delimiter='\t') self.assertEqual(data, [['one', '2'], ['three,four', '5']]) def test_from_csv_encoding(self): raw_data = b'\xf8ne,2\r\n"three,four",5\r\n' data = from_csv(raw_data, encoding='latin-1') self.assertEqual(data, [['øne', '2'], ['three,four', '5']]) def test_to_csv_column(self): test_data = ['one', 2, 'three,four'] raw_data = to_csv_column(test_data) self.assertEqual(raw_data, b'one\r\n2\r\n"three,four"\r\n') def test_to_csv_column_delimiter(self): test_data = ['one', 2, 'three,four'] raw_data = to_csv_column(test_data, delimiter='\t') self.assertEqual(raw_data, b'one\r\n2\r\nthree,four\r\n') def test_to_csv_column_encoding(self): test_data = ['øne', 2, 'three,four'] raw_data = to_csv_column(test_data, encoding='latin-1') self.assertEqual(raw_data, b'\xf8ne\r\n2\r\n"three,four"\r\n') def test_from_csv_column(self): raw_data = b'one\r\n2\r\n"three,four"\r\n' data = from_csv_column(raw_data) self.assertEqual(data, ['one', '2', 'three,four']) def test_from_csv_column_delimiter(self): raw_data = b'one\r\n2\r\nthree,four\r\n' data = from_csv_column(raw_data, delimiter='\t') self.assertEqual(data, ['one', '2', 'three,four']) def test_from_csv_column_encoding(self): raw_data = b'\xf8ne\r\n2\r\n"three,four"\r\n' data = from_csv_column(raw_data, encoding='latin-1') self.assertEqual(data, ['øne', '2', 'three,four']) def test_to_csv_row(self): test_data = ['one', 2, 'three,four'] raw_data = to_csv_row(test_data) self.assertEqual(raw_data, b'one,2,"three,four"\r\n') def test_to_csv_row_delimiter(self): test_data = ['one', 2, 'three,four'] raw_data = to_csv_row(test_data, delimiter='\t') self.assertEqual(raw_data, b'one\t2\tthree,four\r\n') def test_to_csv_row_encoding(self): test_data = ['øne', 2, 'three,four'] raw_data = to_csv_row(test_data, encoding='latin-1') self.assertEqual(raw_data, b'\xf8ne,2,"three,four"\r\n') def test_from_csv_row(self): raw_data = b'one,2,"three,four"\r\n' data = from_csv_row(raw_data) self.assertEqual(data, ['one', '2', 'three,four']) def test_from_csv_row_delimiter(self): raw_data = b'one\t2\tthree,four\r\n' data = from_csv_row(raw_data, delimiter='\t') self.assertEqual(data, ['one', '2', 'three,four']) def test_from_csv_row_encoding(self): raw_data = b'\xf8ne,2,"three,four"\r\n' data = from_csv_row(raw_data, encoding='latin-1') self.assertEqual(data, ['øne', '2', 'three,four']) @requires_numpy class TestNpy(TestCase): def test_to_npy(self): data = np.array([[1, 2, 3], [4, 5, 6]], dtype='uint8') raw_data = to_npy(data) self.assertEqual( raw_data, b"\x93NUMPY\x01\x00v\x00{'descr': '|u1', 'fortran_order': False, 'shape': (2, 3), } \n" # noqa: E501 + b"\x01\x02\x03\x04\x05\x06" ) def test_from_npy(self): raw_data = ( b"\x93NUMPY\x01\x00v\x00{'descr': '|u1', 'fortran_order': False, 'shape': (2, 3), } \n" # noqa: E501 + b"\x01\x02\x03\x04\x05\x06" ) data = from_npy(raw_data) np.testing.assert_array_equal( data, np.array([[1, 2, 3], [4, 5, 6]], dtype='uint8') ) pyface-7.4.0/pyface/data_view/tests/test_data_view_widget.py0000644000076500000240000002712114176222673025242 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 traits.api import TraitError from traits.testing.optional_dependencies import numpy as np, requires_numpy from pyface.toolkit import toolkit from pyface.testing.layout_widget_mixin import LayoutWidgetMixin # This import results in an error without numpy installed # see enthought/pyface#742 if np is not None: from pyface.data_view.data_models.api import ArrayDataModel from pyface.data_view.data_view_widget import DataViewWidget from pyface.data_view.value_types.api import FloatValue is_wx = (toolkit.toolkit == "wx") is_linux = (platform.system() == "Linux") # The available selection modes and types for the toolkit selection_modes = DataViewWidget().trait("selection_mode").trait_type.values selection_types = DataViewWidget().trait("selection_type").trait_type.values @requires_numpy class TestWidget(LayoutWidgetMixin, unittest.TestCase): def _create_widget(self): self.data = np.arange(120.0).reshape(4, 5, 6) self.model = ArrayDataModel(data=self.data, value_type=FloatValue()) return DataViewWidget( parent=self.parent.control, data_model=self.model ) def test_defaults(self): self.assertTrue(self.widget.header_visible) def test_lifecycle(self): self._create_widget_control() def test_header_visible(self): self._create_widget_control() self.assertTrue(self.widget._get_control_header_visible()) self.widget.header_visible = False self.gui.process_events() self.assertFalse(self.widget._get_control_header_visible()) def test_header_visible_before_control(self): self.widget.header_visible = False self._create_widget_control() self.assertFalse(self.widget._get_control_header_visible()) def test_init_selection(self): self.widget.selection = [((1, ), ())] self._create_widget_control() self.assertEqual( self.widget._get_control_selection(), [((1, ), ())] ) def test_selection_mode_change(self): self._create_widget_control() self.widget.selection = [((1, 4), ()), ((2, 0), ())] self.widget.selection_mode = "single" self.assertEqual(self.widget._get_control_selection_mode(), "single") self.assertEqual(self.widget.selection, []) self.widget.selection = [((1, 4), ())] if "none" in selection_modes: self.widget.selection_mode = "none" self.assertEqual(self.widget._get_control_selection_mode(), "none") self.assertEqual(self.widget.selection, []) self.widget.selection_mode = "extended" self.assertEqual(self.widget._get_control_selection_mode(), "extended") self.assertEqual(self.widget.selection, []) @unittest.skipIf( len(selection_types) <= 1, "Changing selection types not supported", ) def test_selection_type_change(self): self._create_widget_control() if "column" in selection_types: self.widget.selection_type = "column" self.assertEqual( self.widget._get_control_selection_type(), "column", ) self.assertEqual(self.widget.selection, []) if "item" in selection_types: self.widget.selection_type = "item" self.assertEqual(self.widget._get_control_selection_type(), "item") self.assertEqual(self.widget.selection, []) if "row" in selection_types: self.widget.selection_type = "row" self.assertEqual(self.widget._get_control_selection_type(), "row") self.assertEqual(self.widget.selection, []) @unittest.skipIf( "none" not in selection_modes, "Selection mode 'none' not supported", ) def test_selection_mode_none(self): self.widget.selection_mode = "none" self._create_widget_control() self.assertEqual(self.widget._get_control_selection_mode(), "none") self.widget.selection = [] self.gui.process_events() self.assertEqual(self.widget.selection, []) self.assertEqual(self.widget._get_control_selection(), []) @unittest.skipIf( "none" not in selection_modes, "Selection mode 'none' not supported", ) def test_selection_mode_none_invalid(self): self.widget.selection_mode = "none" self._create_widget_control() with self.assertRaises(TraitError): self.widget.selection = [((1, 4), ()), (2, 1), ()] def test_selection_mode_single(self): self.widget.selection_mode = "single" self._create_widget_control() self.assertEqual(self.widget._get_control_selection_mode(), "single") self.widget.selection = [((1, 4), ())] self.gui.process_events() self.assertEqual(self.widget.selection, [((1, 4), ())]) self.assertEqual(self.widget._get_control_selection(), [((1, 4), ())]) def test_selection_mode_single_invalid(self): self.widget.selection_mode = "single" self._create_widget_control() with self.assertRaises(TraitError): self.widget.selection = [((1, 4), ()), (2, 1), ()] @unittest.skipIf( is_wx and is_linux, "Selection mode 'extended' not working on Linux", ) def test_selection_mode_extended(self): self._create_widget_control() self.assertEqual(self.widget._get_control_selection_mode(), "extended") self.widget.selection = [((1, 4), ()), ((2, 0), ())] self.gui.process_events() self.assertEqual(self.widget.selection, [((1, 4), ()), ((2, 0), ())]) self.assertEqual( self.widget._get_control_selection(), [((1, 4), ()), ((2, 0), ())], ) @unittest.skipIf( 'column' not in selection_types, "Selection type 'column' not supported", ) def test_selection_type_column(self): self.widget.selection_type = "column" self._create_widget_control() self.assertEqual(self.widget._get_control_selection_type(), "column") self.widget.selection = [((0,), (2,)), ((1,), (4,))] self.gui.process_events() self.assertEqual(self.widget.selection, [((0,), (2,)), ((1,), (4,))]) self.assertEqual( self.widget._get_control_selection(), [((0,), (2,)), ((1,), (4,))] ) @unittest.skipIf( 'item' not in selection_types, "Selection type 'item' not supported", ) def test_selection_type_item(self): self.widget.selection_type = "item" self._create_widget_control() self.assertEqual(self.widget._get_control_selection_type(), "item") self.widget.selection = [((1, 4), (2,)), ((2, 0), (4,))] self.gui.process_events() self.assertEqual( self.widget.selection, [((1, 4), (2,)), ((2, 0), (4,))] ) self.assertEqual( self.widget._get_control_selection(), [((1, 4), (2,)), ((2, 0), (4,))], ) def test_selection_type_row_invalid_row_big(self): self._create_widget_control() with self.assertRaises(TraitError): self.widget.selection = [((10,), ())] def test_selection_type_row_invalid_row_long(self): self._create_widget_control() with self.assertRaises(TraitError): self.widget.selection = [((1, 1, 1), ())] def test_selection_type_row_invalid_column(self): self._create_widget_control() with self.assertRaises(TraitError): self.widget.selection = [((1, 2), (2,))] @unittest.skipIf( 'item' not in selection_types, "Selection type 'column' not supported", ) def test_selection_type_item_invalid_row_too_big(self): self.widget.selection_type = 'item' self._create_widget_control() with self.assertRaises(TraitError): self.widget.selection = [((1, 10), (2,))] @unittest.skipIf( 'item' not in selection_types, "Selection type 'column' not supported", ) def test_selection_type_item_invalid_row_too_long(self): self.widget.selection_type = 'item' self._create_widget_control() with self.assertRaises(TraitError): self.widget.selection = [((1, 4, 5, 6), (2,))] @unittest.skipIf( 'item' not in selection_types, "Selection type 'column' not supported", ) def test_selection_type_item_invalid_column(self): self.widget.selection_type = 'item' self._create_widget_control() with self.assertRaises(TraitError): self.widget.selection = [((1, 2), (10,))] @unittest.skipIf( 'column' not in selection_types, "Selection type 'column' not supported", ) def test_selection_type_column_invalid_row_too_long(self): self.widget.selection_type = 'column' self._create_widget_control() with self.assertRaises(TraitError): self.widget.selection = [((1, 4, 5, 6), (2,))] @unittest.skipIf( 'column' not in selection_types, "Selection type 'column' not supported", ) def test_selection_type_column_invalid_row_too_big(self): self.widget.selection_type = 'column' self._create_widget_control() with self.assertRaises(TraitError): self.widget.selection = [((10,), (2,))] @unittest.skipIf( 'column' not in selection_types, "Selection type 'column' not supported", ) def test_selection_type_column_invalid_row_not_parent(self): self.widget.selection_type = 'column' self._create_widget_control() with self.assertRaises(TraitError): self.widget.selection = [((1, 2), (2,))] @unittest.skipIf( 'column' not in selection_types, "Selection type 'column' not supported", ) def test_selection_type_column_invalid_column(self): self.widget.selection_type = 'column' self._create_widget_control() with self.assertRaises(TraitError): self.widget.selection = [((), (10,))] def test_selection_updated(self): self._create_widget_control() with self.assertTraitChanges(self.widget, 'selection'): self.widget._set_control_selection([((1, 4), ())]) self.gui.process_events() self.assertEqual(self.widget.selection, [((1, 4), ())]) self.assertEqual( self.widget._get_control_selection(), [((1, 4), ())], ) def test_selection_updating_context_manager(self): self.assertFalse(self.widget._selection_updating_flag) with self.widget._selection_updating(): self.assertTrue(self.widget._selection_updating_flag) with self.widget._selection_updating(): self.assertTrue(self.widget._selection_updating_flag) self.assertTrue(self.widget._selection_updating_flag) self.assertFalse(self.widget._selection_updating_flag) def test_selection_updating_context_manager_exception(self): with self.assertRaises(ZeroDivisionError): with self.widget._selection_updating(): with self.widget._selection_updating(): 1/0 self.assertFalse(self.widget._selection_updating_flag) pyface-7.4.0/pyface/data_view/tests/__init__.py0000644000076500000240000000000014176222673022417 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/data_view/tests/test_index_manager.py0000644000076500000240000001461214176222673024536 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 pyface.data_view.index_manager import ( IntIndexManager, Root, TupleIndexManager, ) class IndexManagerMixin: def test_root_has_no_parent(self): with self.assertRaises(IndexError): self.index_manager.get_parent_and_row(Root) def test_root_to_sequence(self): result = self.index_manager.to_sequence(Root) self.assertEqual(result, ()) def test_root_from_sequence(self): result = self.index_manager.from_sequence([]) self.assertIs(result, Root) def test_root_id_round_trip(self): root_id = self.index_manager.id(Root) result = self.index_manager.from_id(root_id) self.assertIs(result, Root) def test_simple_sequence_round_trip(self): sequence = (5,) index = self.index_manager.from_sequence(sequence) result = self.index_manager.to_sequence(index) self.assertEqual(result, sequence) def test_simple_sequence_invalid(self): sequence = (-5,) with self.assertRaises(IndexError): self.index_manager.from_sequence(sequence) def test_simple_sequence_to_parent_row(self): sequence = (5,) index = self.index_manager.from_sequence(sequence) result = self.index_manager.get_parent_and_row(index) self.assertEqual(result, (Root, 5)) def test_simple_row_round_trip(self): index = self.index_manager.create_index(Root, 5) result = self.index_manager.get_parent_and_row(index) self.assertEqual(result, (Root, 5)) def test_simple_row_invalid(self): with self.assertRaises(IndexError): self.index_manager.create_index(Root, -5) def test_simple_row_to_sequence(self): index = self.index_manager.create_index(Root, 5) result = self.index_manager.to_sequence(index) self.assertEqual(result, (5,)) def test_simple_id_round_trip(self): index = self.index_manager.create_index(Root, 5) id = self.index_manager.id(index) result = self.index_manager.from_id(id) self.assertEqual(result, index) class TestIntIndexManager(IndexManagerMixin, TestCase): def setUp(self): super().setUp() self.index_manager = IntIndexManager() def tearDown(self): self.index_manager.reset() def test_create_index_root(self): result = self.index_manager.create_index(Root, 5) self.assertEqual(result, 5) def test_create_index_leaf(self): with self.assertRaises(RuntimeError): self.index_manager.create_index(5, 1) def test_create_index_negative(self): with self.assertRaises(IndexError): self.index_manager.create_index(Root, -5) class TestTupleIndexManager(IndexManagerMixin, TestCase): def setUp(self): super().setUp() self.index_manager = TupleIndexManager() def tearDown(self): self.index_manager.reset() def test_complex_sequence_round_trip(self): sequence = (5, 6, 7, 8, 9, 10) index = self.index_manager.from_sequence(sequence) result = self.index_manager.to_sequence(index) self.assertEqual(result, sequence) def test_complex_sequence_identical_index(self): sequence = (5, 6, 7, 8, 9, 10) index_1 = self.index_manager.from_sequence(sequence[:]) index_2 = self.index_manager.from_sequence(sequence[:]) self.assertIs(index_1, index_2) def test_complex_sequence_to_parent_row(self): sequence = (5, 6, 7, 8, 9, 10) index = self.index_manager.from_sequence(sequence) parent, row = self.index_manager.get_parent_and_row(index) self.assertEqual(row, 10) self.assertIs( parent, self.index_manager.from_sequence((5, 6, 7, 8, 9)) ) def test_complex_index_round_trip(self): sequence = (5, 6, 7, 8, 9, 10) parent = Root for depth, row in enumerate(sequence): with self.subTest(depth=depth): index = self.index_manager.create_index(parent, row) result = self.index_manager.get_parent_and_row(index) self.assertIs(result[0], parent) self.assertEqual(result[1], row) parent = index def test_complex_index_create_index_identical(self): sequence = (5, 6, 7, 8, 9, 10) parent = Root for depth, row in enumerate(sequence): with self.subTest(depth=depth): index_1 = self.index_manager.create_index(parent, row) index_2 = self.index_manager.create_index(parent, row) self.assertIs(index_1, index_2) parent = index_1 def test_complex_index_to_sequence(self): sequence = (5, 6, 7, 8, 9, 10) parent = Root for depth, row in enumerate(sequence): with self.subTest(depth=depth): index = self.index_manager.create_index(parent, row) result = self.index_manager.to_sequence(index) self.assertEqual(result, sequence[:depth+1]) parent = index def test_complex_index_sequence_round_trip(self): parent = Root for depth, row in enumerate([5, 6, 7, 8, 9, 10]): with self.subTest(depth=depth): index = self.index_manager.create_index(parent, row) sequence = self.index_manager.to_sequence(index) result = self.index_manager.from_sequence(sequence) self.assertIs(result, index) parent = index def test_complex_index_id_round_trip(self): sequence = (5, 6, 7, 8, 9, 10) parent = Root for depth, row in enumerate(sequence): with self.subTest(depth=depth): index = self.index_manager.create_index(parent, row) id = self.index_manager.id(index) self.assertIsInstance(id, int) result = self.index_manager.from_id(id) self.assertIs(result, index) parent = index pyface-7.4.0/pyface/data_view/tests/test_abstract_value_type.py0000644000076500000240000001012314176222673025766 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 unittest.mock import Mock from traits.api import Str from traits.testing.api import UnittestTools from pyface.color import Color from pyface.data_view.data_view_errors import DataViewSetError from pyface.data_view.abstract_value_type import AbstractValueType, CheckState class ValueType(AbstractValueType): #: a parameter which should fire the update trait sample_parameter = Str(update_value_type=True) class TestAbstractValueType(UnittestTools, TestCase): def setUp(self): self.model = Mock() self.model.get_value = Mock(return_value=1.0) self.model.can_set_value = Mock(return_value=True) self.model.set_value = Mock() def test_has_editor_value(self): value_type = ValueType() result = value_type.has_editor_value(self.model, [0], [0]) self.assertTrue(result) def test_has_editor_value_can_set_value_false(self): self.model.can_set_value = Mock(return_value=False) value_type = ValueType() result = value_type.has_editor_value(self.model, [0], [0]) self.assertFalse(result) def test_get_editor_value(self): value_type = ValueType() result = value_type.get_editor_value(self.model, [0], [0]) self.assertEqual(result, 1.0) def test_set_editor_value(self): value_type = ValueType() value_type.set_editor_value(self.model, [0], [0], 2.0) self.model.set_value.assert_called_once_with([0], [0], 2.0) def test_set_editor_value_set_value_raises(self): self.model.set_value = Mock(side_effect=DataViewSetError) value_type = ValueType() with self.assertRaises(DataViewSetError): value_type.set_editor_value(self.model, [0], [0], 2.0) self.model.set_value.assert_called_once_with([0], [0], 2.0) def test_has_text(self): value_type = ValueType() result = value_type.has_text(self.model, [0], [0]) self.assertTrue(result) def test_get_text(self): value_type = ValueType() result = value_type.get_text(self.model, [0], [0]) self.assertEqual(result, "1.0") def test_set_text(self): value_type = ValueType() with self.assertRaises(DataViewSetError): value_type.set_text(self.model, [0], [0], "2.0") def test_has_color(self): value_type = ValueType() result = value_type.has_color(self.model, [0], [0]) self.assertFalse(result) def test_get_color(self): value_type = ValueType() result = value_type.get_color(self.model, [0], [0]) self.assertEqual(result, Color(rgb=(1.0, 1.0, 1.0))) def test_has_image(self): value_type = ValueType() result = value_type.has_image(self.model, [0], [0]) self.assertFalse(result) def test_get_image(self): value_type = ValueType() result = value_type.get_image(self.model, [0], [0]) self.assertEqual(result.name, "image_not_found") def test_has_check_state(self): value_type = ValueType() result = value_type.has_check_state(self.model, [0], [0]) self.assertFalse(result) def test_get_check_state(self): value_type = ValueType() result = value_type.get_check_state(self.model, [0], [0]) self.assertEqual(result, CheckState.CHECKED) def test_set_check_state(self): value_type = ValueType() with self.assertRaises(DataViewSetError): value_type.set_check_state(self.model, [0], [0], CheckState.CHECKED) def test_parameter_update(self): value_type = ValueType() with self.assertTraitChanges(value_type, 'updated', count=1): value_type.sample_parameter = "new value" pyface-7.4.0/pyface/data_view/tests/test_api.py0000644000076500000240000000354014176222673022504 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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_all_imports(self): from pyface.data_view.api import ( # noqa: F401 AbstractDataExporter, AbstractDataModel, AbstractValueType, csv_column_format, csv_format, csv_row_format, from_csv, from_csv_column, from_csv_row, from_json, from_npy, html_format, npy_format, standard_text_format, to_csv, to_csv_column, to_csv_row, to_json, to_npy, table_format, text_column_format, text_format, text_row_format, DataViewError, DataViewGetError, DataViewSetError, DataViewWidget, DataWrapper, IDataViewWidget, IDataWrapper, AbstractIndexManager, IntIndexManager, TupleIndexManager, DataFormat, ) def test_api_items_count(self): # This test helps developer to keep the above list # up-to-date. Bump the number when the API content changes. from pyface.data_view import api items_in_api = { name for name in dir(api) if not name.startswith("_") } self.assertEqual(len(items_in_api), 34) pyface-7.4.0/pyface/data_view/tests/test_data_wrapper.py0000644000076500000240000000333314176222673024404 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 pyface.data_view.i_data_wrapper import text_format from pyface.data_view.data_wrapper import DataWrapper class TestDataWrapper(TestCase): def test_instantiate(self): data_wrapper = DataWrapper() self.assertEqual(data_wrapper.mimetypes(), set()) def test_mimedata_roundtrip(self): data_wrapper = DataWrapper() data_wrapper.set_mimedata('text/plain', b'hello world') result = data_wrapper.get_mimedata('text/plain') self.assertEqual(data_wrapper.mimetypes(), {'text/plain'}) self.assertEqual(result, b'hello world') def test_mimedata_overwrite(self): data_wrapper = DataWrapper() data_wrapper.set_mimedata('text/plain', b'hello world') data_wrapper.set_mimedata('text/plain', b'hello mars') result = data_wrapper.get_mimedata('text/plain') self.assertEqual(data_wrapper.mimetypes(), {'text/plain'}) self.assertEqual(result, b'hello mars') def test_set_format(self): data_wrapper = DataWrapper() format = text_format() data_wrapper.set_format(format, 'hëllø wørld') result = data_wrapper.get_mimedata('text/plain') self.assertEqual(data_wrapper.mimetypes(), {'text/plain'}) self.assertEqual(result, 'hëllø wørld'.encode('utf-8')) pyface-7.4.0/pyface/data_view/tests/test_abstract_data_exporter.py0000644000076500000240000000702414176222673026460 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 unittest.mock import Mock from pyface.data_view.abstract_data_exporter import AbstractDataExporter from pyface.data_view.data_view_errors import DataViewGetError from pyface.data_view.data_wrapper import DataWrapper from pyface.data_view.i_data_wrapper import DataFormat trivial_format = DataFormat( 'null/null', lambda x: x, lambda x: x, ) trivial_text_format = DataFormat( 'text/null', lambda x: x, lambda x: x, ) class TrivialExporter(AbstractDataExporter): def get_data(self, model, indices): if len(indices) == 0: raise DataViewGetError('bad data') return b'data' class TestAbstractDataExporter(TestCase): def setUp(self): self.value_type = Mock() self.value_type.has_text = Mock(return_value=True) self.value_type.get_text = Mock(return_value='text') self.value_type.has_editor_value = Mock(return_value=True) self.value_type.get_editor_value = Mock(return_value=1) self.model = Mock() self.model.get_value = Mock(return_value=0.0) self.model.get_value_type = Mock(return_value=self.value_type) def test_is_text_default_false(self): exporter = TrivialExporter(format=trivial_format) self.assertFalse(exporter.is_text) def test_is_text_default_true(self): exporter = TrivialExporter(format=trivial_text_format) self.assertTrue(exporter.is_text) def test_add_data(self): exporter = TrivialExporter(format=trivial_format) data_wrapper = DataWrapper() exporter.add_data(data_wrapper, self.model, [((0,), (0,))]) self.assertTrue(data_wrapper.has_format(trivial_format)) self.assertEqual(data_wrapper.get_mimedata('null/null'), b'data') def test_add_data_fail(self): exporter = TrivialExporter(format=trivial_format) data_wrapper = DataWrapper() exporter.add_data(data_wrapper, self.model, []) self.assertFalse(data_wrapper.has_format(trivial_format)) def test_get_value_is_text(self): exporter = TrivialExporter( format=trivial_format, is_text=True, ) value = exporter.get_value(self.model, (0,), (0,)) self.assertEqual(value, 'text') def test_get_value_is_text_not_has_text(self): self.value_type.has_text = Mock(return_value=False) exporter = TrivialExporter( format=trivial_format, is_text=True, ) value = exporter.get_value(self.model, (0,), (0,)) self.assertEqual(value, '') def test_get_value_is_not_text(self): exporter = TrivialExporter( format=trivial_format, is_text=False, ) value = exporter.get_value(self.model, (0,), (0,)) self.assertEqual(value, 1.0) def test_get_value_is_not_text_not_editor_value(self): self.value_type.has_editor_value = Mock(return_value=False) exporter = TrivialExporter( format=trivial_format, is_text=False, ) value = exporter.get_value(self.model, (0,), (0,)) self.assertEqual(value, 0.0) pyface-7.4.0/pyface/data_view/abstract_data_model.py0000644000076500000240000002655114176222673023515 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 an AbstractDataModel ABC for Pyface data models. This module provides an ABC for all data view data models. This specifies the API that the data view widgets expect, and which the underlying data is adapted to by the concrete implementations. Data models are intended to be toolkit-independent, and be able to adapt any approximately tabular or nested data structure to what the data view system expects. """ from abc import abstractmethod from traits.api import ABCHasStrictTraits, Event, Instance from .data_view_errors import DataViewSetError from .index_manager import AbstractIndexManager class AbstractDataModel(ABCHasStrictTraits): """ Abstract base class for Pyface data models. The data model API is intended to provide a common API for hierarchical and tabular data. This class is concerned with the structure, type and values provided by the data, but not with how the data is presented. Row and column indices are represented by sequences (usually lists) of integers, specifying the index at each level of the hierarchy. The root row and column are represented by empty lists. Subclasses need to implement the ``get_column_count``, ``can_have_children`` and ``get_row_count`` methods to return the number of columns in a particular row, as well as the hierarchical structure of the rows. Appropriate observers should be set up on the underlaying data so that the ``structure_changed`` event is fired when the values returned by these methods would change. Subclasses also have to implement the ``get_value`` and ``get_value_type`` methods. These expect a row and column index, with root values treated specially: the root row corresponds to the values which will be displayed in the column headers of the view, and the root column corresponds to the values which will be displayed in the row headers of the view. The ``get_value`` returns an arbitrary Python object corresponding to the cell being viewed, and the ``get_value_type`` should return an instance of an ``AbstractValueType`` that adapts the raw value to the data channels that the data view expects (eg. text, color, icons, editable value, etc.). Implementations should ensure that the ``values_changed`` event fires whenever the data, or the way the data is presented, is updated. If the data is to be editable then the subclass should override the ``set_value`` method. It should attempt to change the underlying data as a side-effect or raise DataViewSetError on failure (for example, setting an invalid value). If the underlying data structure cannot be listened to internally (such as a numpy array or Pandas data frame), ``set_value`` should also fire the ``values_changed`` event with appropriate values. In the cases where the underlying data structure cannot be observed by the usual traits mechanisms, the end-user of the code may be responsible for ensuring that the ``structure_changed`` and ``values_changed`` events are fired appropriately. """ #: The index manager that helps convert toolkit indices to data view #: indices. This should be an IntIndexManager for non-hierarchical data #: or a TupleIndexManager for hierarchical data. index_manager = Instance(AbstractIndexManager) #: Event fired when the structure of the data changes. structure_changed = Event() #: Event fired when value changes without changes to structure. This #: should be set to a 4-tuple of (start_row_index, start_column_index, #: end_row_index, end_column_index) indicated the subset of data which #: changed. These end values are inclusive, unlike standard Python #: slicing notation. values_changed = Event() # Data structure methods @abstractmethod def get_column_count(self): """ How many columns in the data view model. Returns ------- column_count : non-negative int The number of columns that the data view provides. This count should not include the row header. """ raise NotImplementedError() @abstractmethod def can_have_children(self, row): """ Whether or not a row can have child rows. The root row should always return True. Parameters ---------- row : sequence of int The indices of the row as a sequence from root to leaf. Returns ------- can_have_children : bool Whether or not the row can ever have child rows. """ raise NotImplementedError() @abstractmethod def get_row_count(self, row): """ How many child rows the row currently has. Parameters ---------- row : sequence of int The indices of the row as a sequence from root to leaf. Returns ------- row_count : non-negative int The number of child rows that the row has. """ raise NotImplementedError() # Data value methods @abstractmethod def get_value(self, row, column): """ Return the Python value for the row and column. The values for column headers are returned by calling this method with row equal to (). The values for row headers are returned by calling this method with column equal to (). Parameters ---------- row : sequence of int The indices of the row as a sequence from root to leaf. column : sequence of int The indices of the column as a sequence of length 0 or 1. Returns ------- value : any The value represented by the given row and column. Raises ------- DataViewGetError If the value cannot be accessed in an expected way. If this is raised then the error will be ignored and not logged by the data view infrastructure. """ raise NotImplementedError() def can_set_value(self, row, column): """ Whether the value in the indicated row and column can be set. The default method assumes the data is read-only and always returns False. Whether or a column header can be set is returned by calling this method with row equal to (). Whether or a row header can be set is returned by calling this method with column equal to (). Parameters ---------- row : sequence of int The indices of the row as a sequence from root to leaf. column : sequence of int The indices of the column as a sequence of length 0 or 1. Returns ------- can_set_value : bool Whether or not the value can be set. """ return False def set_value(self, row, column, value): """ Set the Python value for the row and column. The default method assumes the data is read-only and always returns False. The values for column headers can be set by calling this method with row equal to (). The values for row headers can be set by calling this method with column equal to (). Parameters ---------- row : sequence of int The indices of the row as a sequence from root to leaf. column : sequence of int The indices of the column as a sequence of length 0 or 1. value : any The new value for the given row and column. Raises ------- DataViewSetError If the value cannot be set. """ raise DataViewSetError() @abstractmethod def get_value_type(self, row, column): """ Return the value type of the given row and column. The value type for column headers are returned by calling this method with row equal to (). The value types for row headers are returned by calling this method with column equal to (). Parameters ---------- row : sequence of int The indices of the row as a sequence from root to leaf. column : sequence of int The indices of the column as a sequence of length 0 or 1. Returns ------- value_type : AbstractValueType or None The value type of the given row and column, or None if no value should be displayed. """ raise NotImplementedError() # Convenience methods def is_row_valid(self, row): """ Return whether or not the given row index refers to a valid row. A row index is valid if every value in the tuple is between 0 and the number of child rows of the parent. Parameters ---------- row : sequence of int The row to check as indices of the row from root to leaf. Returns ------- valid : bool Whether or not the row index is valid. """ for i, index in enumerate(row): parent = row[:i] if not self.can_have_children(parent): return False if not 0 <= index < self.get_row_count(parent): return False return True def is_column_valid(self, column): """ Return whether or not the given column index refers to a valid column. A column index is valid if it is the root, or the value is between 0 and the number of columns in the model. Parameters ---------- column : sequence of int The column to check. Returns ------- valid : bool Whether or not the column index is valid. """ if len(column) == 1: return 0 <= column[0] < self.get_column_count() return len(column) == 0 def iter_rows(self, start_row=()): """ Iterator that yields rows in preorder. Parameters ---------- start_row : sequence of int The row to start at. The iterator will yeild the row and all descendant rows. Yields ------ row_index : sequence of int The current row index. """ start_row = tuple(start_row) yield start_row if self.can_have_children(start_row): for row in range(self.get_row_count(start_row)): yield from self.iter_rows(start_row + (row,)) def iter_items(self, start_row=()): """ Iterator that yields rows and columns in preorder. This yields pairs of row, column for all rows in preorder and and all column indices for all rows, including (). Columns are iterated in order. Parameters ---------- start_row : sequence of int The row to start iteration from. Yields ------ row_index, column_index The current row and column indices. """ for row in self.iter_rows(start_row): yield row, () for column in range(self.get_column_count()): yield row, (column,) pyface-7.4.0/pyface/data_view/__init__.py0000644000076500000240000000000014176222673021255 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/data_view/api.py0000644000076500000240000000523214176222673020303 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 ``pyface.data_view`` subpackage. Note that this public-facing API is provisional and may change in future minor releases until Pyface 8. - :class:`~.AbstractDataExporter` - :class:`~.AbstractDataModel` - :class:`~.AbstractValueType` - :class:`~.DataViewWidget` - :class:`~.DataWrapper` Data Formats ------------ - :class:`~.DataFormat` - :func:`~.text_format` - :attr:`~.csv_format` - :attr:`~.csv_column_format` - :attr:`~.csv_row_format` - :attr:`~.html_format` - :attr:`~.npy_format` - :attr:`~.standard_text_format` - :attr:`~.table_format` - :attr:`~.text_column_format` - :attr:`~.text_row_format` - :func:`~.from_csv` - :func:`~.from_csv_column` - :func:`~.from_csv_row` - :func:`~.from_json` - :func:`~.from_npy` - :func:`~.to_csv` - :func:`~.to_csv_column` - :func:`~.to_csv_row` - :func:`~.to_json` - :func:`~.to_npy` Index Managers -------------- - :class:`~.AbstractIndexManager` - :class:`~.IntIndexManager` - :class:`~.TupleIndexManager` Exceptions ---------- - :class:`~.DataViewError` - :class:`~.DataViewGetError` - :class:`~.DataViewSetError` Interfaces ---------- - :class:`~.IDataViewWidget` - :class:`~.IDataWrapper` """ from pyface.data_view.abstract_data_exporter import AbstractDataExporter # noqa: 401 from pyface.data_view.abstract_data_model import AbstractDataModel # noqa: 401 from pyface.data_view.abstract_value_type import AbstractValueType # noqa: 401 from pyface.data_view.data_formats import ( # noqa: 401 csv_column_format, csv_format, csv_row_format, from_csv, from_csv_column, from_csv_row, from_json, from_npy, html_format, npy_format, standard_text_format, to_csv, to_csv_column, to_csv_row, to_json, to_npy, table_format, text_column_format, text_row_format ) from pyface.data_view.data_view_errors import ( # noqa: 401 DataViewError, DataViewGetError, DataViewSetError ) from pyface.data_view.data_view_widget import DataViewWidget # noqa: 401 from pyface.data_view.data_wrapper import DataWrapper # noqa: 401 from pyface.data_view.i_data_view_widget import IDataViewWidget # noqa: 401 from pyface.data_view.i_data_wrapper import ( # noqa: 401 DataFormat, IDataWrapper, text_format ) from pyface.data_view.index_manager import ( # noqa: 401 AbstractIndexManager, IntIndexManager, TupleIndexManager ) pyface-7.4.0/pyface/data_view/data_formats.py0000644000076500000240000001657714176222673022214 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 csv from functools import partial from io import BytesIO, StringIO import os import json from pyface.data_view.i_data_wrapper import DataFormat, text_format # Scalar formats def to_json(data, default=None): """ Serialize an object to a JSON bytestring. Parameters ---------- data : any The data to be serialized. default : callable or None Callable that takes a Python object and returns a JSON-serializable data structure. Returns ------- raw_data : bytes The serialized data as a bytestring. """ str_data = json.dumps(data, default=default, separators=(',', ':')) return str_data.encode('utf-8') def from_json(raw_data, object_hook=None): """ Deserialize a JSON bytestring. Parameters ---------- raw_data : bytes The serialized JSON data as a byte string. object_hook : callable Callable that takes a dictionary and returns an corresponding Python object. Returns ------- data : any The data extracted. """ return json.loads(raw_data.decode('utf-8'), object_hook=object_hook) #: The text/plain format with utf-8 encoding. standard_text_format = text_format() #: The text/html format with utf-8 encoding. html_format = text_format(mimetype='text/html') #: A generic JSON format. json_format = DataFormat('application/json', to_json, from_json) # 1D formats def to_csv_row(data, delimiter=',', encoding='utf-8', **kwargs): """ Serialize a list to a single-row CSV bytestring. Parameters ---------- data : list The list to be serialized. Any elements which are not strings will be converted to strings by calling ``str()``. delimiter : str The CSV delimiter. encoding : str The encoding of the bytes **kwargs Additional arguments to csv.writer. Returns ------- raw_data : bytes The serialized data as a bytestring. """ fp = StringIO() writer = csv.writer(fp, delimiter=delimiter, **kwargs) writer.writerow(data) return fp.getvalue().encode(encoding) def from_csv_row(raw_data, delimiter=',', encoding='utf-8', **kwargs): """ Deserialize the first row of a CSV bytestring as a list. Any rows beyond the first are ignored. Parameters ---------- raw_data : bytes The serialized CSV data as a byte string. delimiter : str The CSV delimiter. encoding : str The encoding of the bytes **kwargs Additional arguments to csv.reader. Returns ------- data : list of str The data extracted as a list of strings. """ fp = StringIO(raw_data.decode(encoding)) reader = csv.reader(fp, delimiter=delimiter, **kwargs) return next(reader) def to_csv_column(data, delimiter=',', encoding='utf-8', **kwargs): """ Serialize a list to a single-column CSV bytestring. Parameters ---------- data : list The list to be serialized. Any elements which are not strings will be converted to strings by calling ``str()``. delimiter : str The CSV delimiter. encoding : str The encoding of the bytes **kwargs Additional arguments to csv.writer. Returns ------- raw_data : bytes The serialized data as a bytestring. """ fp = StringIO() writer = csv.writer(fp, delimiter=delimiter, **kwargs) for row in data: writer.writerow([row]) return fp.getvalue().encode(encoding) def from_csv_column(raw_data, delimiter=',', encoding='utf-8', **kwargs): """ Deserialize the first column of a CSV bytestring as a list. Any columns beyond the first are ignored. Parameters ---------- raw_data : bytes The serialized CSV data as a byte string. delimiter : str The CSV delimiter. encoding : str The encoding of the bytes **kwargs Additional arguments to csv.reader. Returns ------- data : list of str The data extracted as a list of strings. """ fp = StringIO(raw_data.decode(encoding)) reader = csv.reader(fp, delimiter=delimiter, **kwargs) return [row[0] for row in reader] text_row_format = DataFormat( 'text/plain', partial(to_csv_row, delimiter='\t', lineterminator=os.linesep), partial(from_csv_row, delimiter='\t'), ) csv_row_format = DataFormat('text/csv', to_csv_row, from_csv_row) text_column_format = DataFormat( 'text/plain', partial(to_csv_column, delimiter='\t', lineterminator=os.linesep), partial(from_csv_column, delimiter='\t'), ) csv_column_format = DataFormat('text/csv', to_csv_column, from_csv_column) # 2D formats def to_csv(data, delimiter=',', encoding='utf-8', **kwargs): """ Serialize a list of lists to a CSV bytestring. Parameters ---------- data : list of lists The data to be serialized. Any elements which are not strings will be converted to strings by calling ``str()``. delimiter : str The CSV delimiter. encoding : str The encoding of the bytes **kwargs Additional arguments to csv.writer. Returns ------- raw_data : bytes The serialized data as a bytestring. """ fp = StringIO() writer = csv.writer(fp, delimiter=delimiter, **kwargs) for row in data: writer.writerow(row) return fp.getvalue().encode(encoding) def from_csv(raw_data, delimiter=',', encoding='utf-8', **kwargs): """ Deserialize a CSV bytestring. Parameters ---------- raw_data : bytes The serialized CSV data as a byte string. delimiter : str The CSV delimiter. encoding : str The encoding of the bytes **kwargs Additional arguments to csv.reader. Returns ------- data : list of list of str The data extracted as a list of lists of strings. """ fp = StringIO(raw_data.decode(encoding)) reader = csv.reader(fp, delimiter=delimiter, **kwargs) return list(reader) def to_npy(data): """ Serialize an array to a bytestring using .npy format. Parameters ---------- data : array-like The array to be serialized. Returns ------- raw_data : bytes The serialized data as a bytestring. """ import numpy as np data = np.atleast_2d(data) fp = BytesIO() np.save(fp, data, allow_pickle=False) return fp.getvalue() def from_npy(raw_data): """ Deserialize a .npy-format bytestring. Parameters ---------- raw_data : bytes The serialized CSV data as a byte string. Returns ------- data : list of list of str The data extracted as a list of lists of strings. """ import numpy as np fp = BytesIO(raw_data) return np.load(fp, allow_pickle=False) table_format = DataFormat( 'text/plain', partial(to_csv, delimiter='\t', lineterminator=os.linesep), partial(from_csv, delimiter='\t'), ) csv_format = DataFormat('text/csv', to_csv, from_csv) npy_format = DataFormat('application/x-npy', to_npy, from_npy) pyface-7.4.0/pyface/data_view/abstract_value_type.py0000644000076500000240000002760314176222673023600 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 an AbstractValueType ABC for Pyface data models. This module provides an ABC for data view value types, which are responsible for adapting raw data values as used by the data model's ``get_value`` and ``set_value`` methods to the data channels that the data view expects, such as text, color, icons, etc. It is up to the data view to take this standardized data and determine what and how to actually display it. """ from enum import IntEnum from traits.api import ABCHasStrictTraits, Event, observe from pyface.color import Color from .data_view_errors import DataViewSetError class CheckState(IntEnum): "Possible checkbox states" # XXX in the future this may need a "partial" state, see Pyface #695 UNCHECKED = 0 CHECKED = 1 class AbstractValueType(ABCHasStrictTraits): """ A value type converts raw data into data channels. The data channels are editor value, text, color, image, and description. The data channels are used by other parts of the code to produce the actual display. Subclasses should mark traits that potentially affect the display of values with ``update_value_type=True`` metdadata, or alternatively fire the ``updated`` event when the state of the value type changes. Each data channel is set up to have a method which returns whether there is a value for the channel, a second method which returns the value, and an optional third method which sets the channel value. These methods should not raise an Exception, eveen when called inappropriately (eg. calling a "get" method after a "has" method has returned False). """ #: Fired when a change occurs that requires updating values. updated = Event def has_editor_value(self, model, row, column): """ Return whether or not the value can be edited. The default implementation is that cells that can be set are editable. Parameters ---------- model : AbstractDataModel The data model holding the data. row : sequence of int The row in the data model being queried. column : sequence of int The column in the data model being queried. Returns ------- has_editor_value : bool Whether or not the value is editable. """ return model.can_set_value(row, column) def get_editor_value(self, model, row, column): """ Return a value suitable for editing. The default implementation is to return the underlying data value directly from the data model. Parameters ---------- model : AbstractDataModel The data model holding the data. row : sequence of int The row in the data model being queried. column : sequence of int The column in the data model being queried. Returns ------- value : any The value to edit. """ return model.get_value(row, column) def set_editor_value(self, model, row, column, value): """ Set a value that is returned from editing. The default implementation is to set the value directly from the data model. Returns True if successful, False if it fails. Parameters ---------- model : AbstractDataModel The data model holding the data. row : sequence of int The row in the data model being queried. column : sequence of int The column in the data model being queried. value : any The value to set. Raises ------- DataViewSetError If the value cannot be set. """ model.set_value(row, column, value) def has_text(self, model, row, column): """ Whether or not the value has a textual representation. The default implementation returns True if ``get_text`` returns a non-empty value. Parameters ---------- model : AbstractDataModel The data model holding the data. row : sequence of int The row in the data model being queried. column : sequence of int The column in the data model being queried. Returns ------- has_text : bool Whether or not the value has a textual representation. """ return self.get_text(model, row, column) != "" def get_text(self, model, row, column): """ The textual representation of the underlying value. The default implementation calls str() on the underlying value. Parameters ---------- model : AbstractDataModel The data model holding the data. row : sequence of int The row in the data model being queried. column : sequence of int The column in the data model being queried. Returns ------- text : str The textual representation of the underlying value. """ return str(model.get_value(row, column)) def set_text(self, model, row, column, text): """ Set the text of the underlying value. This is provided primarily for backends which may not permit non-text editing of values, in which case this provides an alternative route to setting the value. The default implementation does not allow setting the text. Parameters ---------- model : AbstractDataModel The data model holding the data. row : sequence of int The row in the data model being queried. column : sequence of int The column in the data model being queried. text : str The text to set. Raises ------- DataViewSetError If the value cannot be set. """ raise DataViewSetError("Cannot set value.") def has_color(self, model, row, column): """ Whether or not the value has color data. Parameters ---------- model : AbstractDataModel The data model holding the data. row : sequence of int The row in the data model being queried. column : sequence of int The column in the data model being queried. Returns ------- has_color : bool Whether or not the value has data-associated color values. """ return False def get_color(self, model, row, column): """ Get data-associated colour values for the given item. The default implementation returns white. Parameters ---------- model : AbstractDataModel The data model holding the data. row : sequence of int The row in the data model being queried. column : sequence of int The column in the data model being queried. Returns ------- color : Color instance The color associated with the cell. """ return Color(rgba=(1.0, 1.0, 1.0, 1.0)) def has_image(self, model, row, column): """ Whether or not the value has an image associated with it. The default implementation returns True if ``get_image`` returns a non-None value. Parameters ---------- model : AbstractDataModel The data model holding the data. row : sequence of int The row in the data model being queried. column : sequence of int The column in the data model being queried. Returns ------- has_image : bool Whether or not the value has an image associated with it. """ return False def get_image(self, model, row, column): """ An image associated with the underlying value. The default implementation returns None. Parameters ---------- model : AbstractDataModel The data model holding the data. row : sequence of int The row in the data model being queried. column : sequence of int The column in the data model being queried. Returns ------- image : IImage The image associated with the underlying value. """ from pyface.image_resource import ImageResource return ImageResource("image_not_found") def has_check_state(self, model, row, column): """ Whether or not the value has checked state. The default implementation returns False. Parameters ---------- model : AbstractDataModel The data model holding the data. row : sequence of int The row in the data model being queried. column : sequence of int The column in the data model being queried. Returns ------- has_check_state : bool Whether or not the value has a checked state. """ return False def get_check_state(self, model, row, column): """ The state of the item check box. The default implementation returns "checked" if the value is truthy, or "unchecked" if the value is falsey. Parameters ---------- model : AbstractDataModel The data model holding the data. row : sequence of int The row in the data model being queried. column : sequence of int The column in the data model being queried. Returns ------- check_state : CheckState The current checked state. """ return ( CheckState.CHECKED if model.get_value(row, column) else CheckState.UNCHECKED ) def set_check_state(self, model, row, column, check_state): """ Set the checked state of the underlying value. The default implementation does not allow setting the checked state. Parameters ---------- model : AbstractDataModel The data model holding the data. row : sequence of int The row in the data model being queried. column : sequence of int The column in the data model being queried. check_state : CheckState The check state value to set. Raises ------- DataViewSetError If the value cannot be set. """ raise DataViewSetError("Cannot set check state.") def has_tooltip(self, model, row, column): """ Whether or not the value has a tooltip. The default implementation returns True if ``get_tooltip`` returns a non-empty value. Parameters ---------- model : AbstractDataModel The data model holding the data. row : sequence of int The row in the data model being queried. column : sequence of int The column in the data model being queried. Returns ------- has_tooltip : bool Whether or not the value has a textual representation. """ return self.get_tooltip(model, row, column) != "" def get_tooltip(self, model, row, column): """ The tooltip for the underlying value. The default implementation returns an empty string. tooltip : str The textual representation of the underlying value. """ return "" @observe('+update_value_type') def update_value_type(self, event=None): """ Fire update event when marked traits change. """ self.updated = True pyface-7.4.0/pyface/data_view/index_manager.py0000644000076500000240000003100714176222673022332 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Index Managers ============== This module provides a number of classes for efficiently managing the mapping between different ways of representing indices. To do so, each index manager provides an intermediate, opaque index object that is suitable for use in these situations and is guaranteed to have a long enough life that it will not change or be garbage collected while a C++ object has a reference to it. The wx DataView classes expect to be given an integer id value that is stable and can be used to return the parent reference id. And Qt's ModelView system expects to be given a pointer to an object that is long-lived (in particular, it will not be garbage-collected during the lifetime of a QModelIndex) and which can be used to find the parent object of the current object. The default representation of an index from the point of view of the data view infrastructure is a sequence of integers, giving the index at each level of the hierarchy. DataViewModel classes can then use these indices to identify objects in the underlying data model. There are three main classes defined in the module: AbstractIndexManager, IntIndexManager, and TupleIndexManager. AbstractIndexManager An ABC that defines the API IntIndexManager An efficient index manager for non-hierarchical data, such as lists, tables and 2D arrays. TupleIndexManager An index manager that handles non-hierarchical data while trying to be fast and memory efficient. The two concrete subclasses should be sufficient for most cases, but advanced users may create their own if for some reason the provided managers do not work well for a particular situation. Developers who implement this API need to be mindful of the requirements on the lifetime and identity constraints required by the various toolkit APIs. """ from abc import abstractmethod from traits.api import ABCHasStrictTraits, Dict, Int, Tuple #: The singular root object for all index managers. Root = () class AbstractIndexManager(ABCHasStrictTraits): """ Abstract base class for index managers. """ @abstractmethod def create_index(self, parent, row): """ Given a parent index and a row number, create an index. The internal structure of the index should not matter to consuming code. However obejcts returned from this method should persist until the reset method is called. Parameters ---------- parent : index object The parent index object. row : int The position of the resuling index in the parent's children. Returns ------- index : index object The resulting opaque index object. Raises ------ IndexError Negative row values raise an IndexError exception. RuntimeError If asked to create a persistent index for a parent and row where that is not possible, a RuntimeError will be raised. """ raise NotImplementedError() @abstractmethod def get_parent_and_row(self, index): """ Given an index object, return the parent index and row. Parameters ---------- index : index object The opaque index object. Returns ------- parent : index object The parent index object. row : int The position of the resuling index in the parent's children. Raises ------ IndexError If the Root object is passed as the index, this method will raise an IndexError, as it has no parent. """ raise NotImplementedError() def from_sequence(self, indices): """ Given a sequence of indices, return the index object. The default implementation starts at the root and repeatedly calls create_index() to find the index at each level, returning the final value. Parameters ---------- indices : sequence of int The row location at each level of the hierarchy. Returns ------- index : index object The persistent index object associated with this sequence. Raises ------ RuntimeError If asked to create a persistent index for a sequence of indices where that is not possible, a RuntimeError will be raised. """ index = Root for row in indices: index = self.create_index(index, row) return index def to_sequence(self, index): """ Given an index, return the corresponding sequence of row values. The default implementation repeatedly calls get_parent_and_row() to walk up the hierarchy and push the row values into the start of the sequence. Parameters ---------- index : index object The opaque index object. Returns ------- sequence : tuple of int The row location at each level of the hierarchy. """ result = () while index != Root: index, row = self.get_parent_and_row(index) result = (row,) + result return result @abstractmethod def from_id(self, id): """ Given an integer id, return the corresponding index. Parameters ---------- id : int An integer object id value. Returns ------- index : index object The persistent index object associated with this id. """ raise NotImplementedError() @abstractmethod def id(self, index): """ Given an index, return the corresponding id. Parameters ---------- index : index object The persistent index object. Returns ------- id : int The associated integer object id value. """ raise NotImplementedError() def reset(self): """ Reset any caches and other state. Resettable traits in subclasses are indicated by having ``can_reset=True`` metadata. This is provided to allow toolkit code to clear caches to prevent memory leaks when working with very large tables. Care should be taken when calling this method, as Qt may crash if a QModelIndex is referencing an index that no longer has a reference in a cache. For some IndexManagers, particularly for those which are flat or static, reset() may do nothing. """ resettable_traits = self.trait_names(can_reset=True) self.reset_traits(resettable_traits) class IntIndexManager(AbstractIndexManager): """ Efficient IndexManager for non-hierarchical indexes. This is a simple index manager for flat data structures. The index values returned are either the Root, or simple integers that indicate the position of the index as a child of the root. While it cannot handle nested data, this index manager can operate without having to perform any caching, and so is very efficient. """ def create_index(self, parent, row): """ Given a parent index and a row number, create an index. This should only ever be called with Root as the parent. Parameters ---------- parent : index object The parent index object. row : non-negative int The position of the resulting index in the parent's children. Returns ------- index : index object The resulting opaque index object. Raises ------ IndexError Negative row values raise an IndexError exception. RuntimeError If the parent is not the Root, a RuntimeError will be raised """ if row < 0: raise IndexError("Row must be non-negative. Got {}".format(row)) if parent != Root: raise RuntimeError( "{} cannot create persistent index value for {}.".format( self.__class__.__name__, (parent, row) ) ) return row def get_parent_and_row(self, index): """ Given an index object, return the parent index and row. Parameters ---------- index : index object The opaque index object. Returns ------- parent : index object The parent index object. row : int The position of the resuling index in the parent's children. Raises ------ IndexError If the Root object is passed as the index, this method will raise an IndexError, as it has no parent. """ if index == Root: raise IndexError("Root index has no parent.") return Root, int(index) def from_id(self, id): """ Given an integer id, return the corresponding index. Parameters ---------- id : int An integer object id value. Returns ------- index : index object The persistent index object associated with this id. """ if id == 0: return Root return id - 1 def id(self, index): """ Given an index, return the corresponding id. Parameters ---------- index : index object The persistent index object. Returns ------- id : int The associated integer object id value. """ if index == Root: return 0 return index + 1 class TupleIndexManager(AbstractIndexManager): #: A dictionary that maps tuples to the canonical version of the tuple. _cache = Dict(Tuple, Tuple, {Root: Root}, can_reset=True) #: A dictionary that maps ids to the canonical version of the tuple. _id_cache = Dict(Int, Tuple, {0: Root}, can_reset=True) def create_index(self, parent, row): """ Given a parent index and a row number, create an index. Parameters ---------- parent : index object The parent index object. row : non-negative int The position of the resulting index in the parent's children. Returns ------- index : index object The resulting opaque index object. Raises ------ IndexError Negative row values raise an IndexError exception. """ if row < 0: raise IndexError("Row must be non-negative. Got {}".format(row)) index = (parent, row) canonical_index = self._cache.setdefault(index, index) self._id_cache[self.id(canonical_index)] = canonical_index return canonical_index def get_parent_and_row(self, index): """ Given an index object, return the parent index and row. Parameters ---------- index : index object The opaque index object. Returns ------- parent : index object The parent index object. row : int The position of the resuling index in the parent's children. Raises ------ IndexError If the Root object is passed as the index, this method will raise an IndexError, as it has no parent. """ if index == Root: raise IndexError("Root index has no parent.") return index def from_id(self, id): """ Given an integer id, return the corresponding index. Parameters ---------- id : int An integer object id value. Returns ------- index : index object The persistent index object associated with this id. """ return self._id_cache[id] def id(self, index): """ Given an index, return the corresponding id. Parameters ---------- index : index object The persistent index object. Returns ------- id : int The associated integer object id value. """ if index == Root: return 0 canonical_index = self._cache.setdefault(index, index) return id(canonical_index) pyface-7.4.0/pyface/data_view/i_data_wrapper.py0000644000076500000240000001231514176222673022513 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 codecs import decode, encode from functools import partial from typing import Any as TAny, Callable as TCallable, NamedTuple from traits.api import Any, HasStrictTraits, Interface class DataFormat(NamedTuple): """ Information about a mimetype and serializers. Simple namedtuple-based class that stores the mimetype, serializer and deserializer together. """ #: The mimetype of the data. mimetype: str #: A callable that serializes this format. It should take an python #: object of a supported type, and return a bytestring. serialize: TCallable[[TAny], bytes] #: A callable that deserializes this format. It should take a #: bytestring and return the extracted object. deserialize: TCallable[[bytes], TAny] def text_format(encoding='utf-8', mimetype='text/plain'): """ DataFormat factory for text mimetypes. """ return DataFormat( mimetype=mimetype, serialize=partial(encode, encoding=encoding), deserialize=partial(decode, encoding=encoding), ) class IDataWrapper(Interface): """ Wrapper around polymorphic toolkit data object containing mimedata. To support clipboard and drag and drop operations, toolkits need a way of generically representing data in multiple formats. This is a wrapper class that provides a toolkit independent intreface to these classes which allow the exchange of data, with types specified by MIME types. """ #: The toolkit data object. toolkit_data = Any() def mimetypes(self): """ Return a set of mimetypes holding data. Returns ------- mimetypes : set of str The set of mimetypes currently storing data in the toolkit data object. """ pass def has_format(self, format): """ Whether or not a particular format has available data. Parameters ---------- format : DataFormat A data format object. Returns ------- has_format : bool Whether or not there is data associated with that format in the underlying toolkit object. """ raise NotImplementedError() def get_format(self, format): """ The decoded data associted with the format. Parameters ---------- format : DataFormat A data format object. Returns ------- data : any The data decoded for the given format. """ raise NotImplementedError() def set_format(self, format, data): """ Encode and set data for the format. Parameters ---------- format : DataFormat A data format object. data : any The data to be encoded and stored. """ raise NotImplementedError() def get_mimedata(self, mimetype): """ Get raw data for the given media type. Parameters ---------- mimetype : str The mime media type to be extracted. Returns ------- mimedata : bytes The mime media data as bytes. """ raise NotImplementedError() def set_mimedata(self, mimetype, mimedata): """ Set raw data for the given media type. Parameters ---------- mimetype : str The mime media type to be extracted. mimedata : bytes The mime media data encoded as bytes.. """ raise NotImplementedError() class MDataWrapper(HasStrictTraits): """ Mixin class for DataWrappers. This provides standard methods for using DataFormat objects, but not the low-level communication with the underlying toolkit. """ def has_format(self, format): """ Whether or not a particular format has available data. Parameters ---------- format : DataFormat A data format object. Returns ------- has_format : bool Whether or not there is data associated with that format in the underlying toolkit object. """ return format.mimetype in self.mimetypes() def get_format(self, format): """ The decoded data associted with the format. Parameters ---------- format : DataFormat A data format object. Returns ------- data : any The data decoded for the given format. """ return format.deserialize(self.get_mimedata(format.mimetype)) def set_format(self, format, data): """ Encode and set data for the format. Parameters ---------- format : DataFormat A data format object. data : any The data to be encoded and stored. """ self.set_mimedata(format.mimetype, format.serialize(data)) pyface-7.4.0/pyface/data_view/data_view_widget.py0000644000076500000240000000102014176222673023027 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.toolkit import toolkit_object DataViewWidget = toolkit_object('data_view.data_view_widget:DataViewWidget') pyface-7.4.0/pyface/data_view/value_types/0000755000076500000240000000000014176460550021514 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/data_view/value_types/editable_value.py0000644000076500000240000000624314176222673025042 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 from pyface.data_view.data_view_errors import DataViewSetError from pyface.data_view.abstract_value_type import AbstractValueType class EditableValue(AbstractValueType): """ A base class for editable values. This class provides two things beyond the base AbstractValueType: a trait ``is_editable`` which allows toggling editing state on and off, and an ``is_valid`` method that is used for validation before setting a value. """ #: Whether or not the value is editable, assuming the underlying data can #: be set. is_editable = Bool(True, update_value_type=True) def is_valid(self, model, row, column, value): """ Whether or not the value is valid for the data item specified. The default implementation returns True for all values. Parameters ---------- model : AbstractDataModel The data model holding the data. row : sequence of int The row in the data model being queried. column : sequence of int The column in the data model being queried. value : any The value to validate. Returns ------- is_valid : bool Whether or not the value is valid. """ return True # AbstractValueType Interface -------------------------------------------- def has_editor_value(self, model, row, column): """ Return whether or not the value can be edited. A cell is editable if the underlying data can be set, and the ``is_editable`` flag is set to True Parameters ---------- model : AbstractDataModel The data model holding the data. row : sequence of int The row in the data model being queried. column : sequence of int The column in the data model being queried. Returns ------- has_editor_value : bool Whether or not the value is editable. """ return model.can_set_value(row, column) and self.is_editable def set_editor_value(self, model, row, column, value): """ Set the edited value. Parameters ---------- model : AbstractDataModel The data model holding the data. row : sequence of int The row in the data model being set. column : sequence of int The column in the data model being set. value : any The value being set. Raises ------- DataViewSetError If the value cannot be set. """ if self.is_valid(model, row, column, value): model.set_value(row, column, value) else: raise DataViewSetError("Invalid value set: {!r}".format(value)) pyface-7.4.0/pyface/data_view/value_types/enum_value.py0000644000076500000240000001222414176222673024231 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 Callable, List from .editable_value import EditableValue class EnumValue(EditableValue): """ Editable value that takes one of a collection of pre-set values. Each value can be associated with text, colors and images by supplying functions ``format``, ``colors`` and ``images``, respectively. """ #: The list of values which are allowed for the value. values = List() #: A function that converts a value to a string for display. format = Callable(str, update_value_type=True) #: A map from valid values to colors. colors = Callable(None, update_value_type=True) #: A map from valid values to images. images = Callable(None, update_value_type=True) def is_valid(self, model, row, column, value): """ Whether or not the value is valid for the data item specified. Parameters ---------- model : AbstractDataModel The data model holding the data. row : sequence of int The row in the data model being queried. column : sequence of int The column in the data model being queried. value : any The value to validate. Returns ------- is_valid : bool Whether or not the value is valid. """ return value in self.values def has_text(self, model, row, column): """ Get the display text from the underlying value. Parameters ---------- model : AbstractDataModel The data model holding the data. row : sequence of int The row in the data model being queried. column : sequence of int The column in the data model being queried. Returns ------- text : str The text to display. """ return self.format is not None def get_text(self, model, row, column): """ Get the display text from the underlying value. Parameters ---------- model : AbstractDataModel The data model holding the data. row : sequence of int The row in the data model being queried. column : sequence of int The column in the data model being queried. Returns ------- text : str The text to display. """ return self.format(model.get_value(row, column)) def has_color(self, model, row, column): """ Whether or not the value has color data. Parameters ---------- model : AbstractDataModel The data model holding the data. row : sequence of int The row in the data model being queried. column : sequence of int The column in the data model being queried. Returns ------- has_color : bool Whether or not the value has data-associated color values. """ return self.colors is not None def get_color(self, model, row, column): """ Get data-associated colour values for the given item. The default implementation returns white. Parameters ---------- model : AbstractDataModel The data model holding the data. row : sequence of int The row in the data model being queried. column : sequence of int The column in the data model being queried. Returns ------- color : Color instance The color associated with the cell. """ return self.colors(model.get_value(row, column)) def has_image(self, model, row, column): """ Whether or not the value has an image associated with it. Parameters ---------- model : AbstractDataModel The data model holding the data. row : sequence of int The row in the data model being queried. column : sequence of int The column in the data model being queried. Returns ------- has_image : bool Whether or not the value has an image associated with it. """ return self.images is not None def get_image(self, model, row, column): """ An image associated with the underlying value. The default implementation returns None. Parameters ---------- model : AbstractDataModel The data model holding the data. row : sequence of int The row in the data model being queried. column : sequence of int The column in the data model being queried. Returns ------- image : IImage The image associated with the underlying value. """ return self.images(model.get_value(row, column)) pyface-7.4.0/pyface/data_view/value_types/tests/0000755000076500000240000000000014176460550022656 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/data_view/value_types/tests/test_bool_value.py0000644000076500000240000000606514176222673026427 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 unittest.mock import Mock from pyface.data_view.abstract_value_type import CheckState from pyface.data_view.data_view_errors import DataViewSetError from pyface.data_view.value_types.bool_value import BoolValue class TestBoolValue(TestCase): def setUp(self): self.model = Mock() self.model.get_value = Mock(return_value=True) self.model.can_set_value = Mock(return_value=True) self.model.set_value = Mock() def test_defaults(self): value = BoolValue() self.assertEqual(value.true_text, "") self.assertEqual(value.false_text, "") def test_has_text_default(self): value = BoolValue() has_text = value.has_text(self.model, [0], [0]) self.assertFalse(has_text) def test_has_text(self): value = BoolValue(true_text="Yes", false_text="No") has_text = value.has_text(self.model, [0], [0]) self.assertTrue(has_text) def test_get_text_default(self): value = BoolValue() text = value.get_text(self.model, [0], [0]) self.assertEqual(text, "") self.model.get_value = Mock(return_value=False) text = value.get_text(self.model, [0], [0]) self.assertEqual(text, "") def test_get_text(self): value = BoolValue(true_text="Yes", false_text="No") text = value.get_text(self.model, [0], [0]) self.assertEqual(text, "Yes") self.model.get_value = Mock(return_value=False) text = value.get_text(self.model, [0], [0]) self.assertEqual(text, "No") def test_get_check_state(self): value = BoolValue() check_state = value.get_check_state(self.model, [0], [0]) self.assertEqual(check_state, CheckState.CHECKED) def test_get_check_state_false(self): value = BoolValue() self.model.get_value = Mock(return_value=False) check_state = value.get_check_state(self.model, [0], [0]) self.assertEqual(check_state, CheckState.UNCHECKED) def test_set_check_state(self): value = BoolValue() value.set_check_state(self.model, [0], [0], CheckState.CHECKED) self.model.set_value.assert_called_once_with([0], [0], True) def test_set_check_state_unchecked(self): value = BoolValue() value.set_check_state(self.model, [0], [0], CheckState.UNCHECKED) self.model.set_value.assert_called_once_with([0], [0], False) def test_set_check_state_no_set_value(self): self.model.can_set_value = Mock(return_value=False) value = BoolValue() with self.assertRaises(DataViewSetError): value.set_text(self.model, [0], [0], CheckState.CHECKED) pyface-7.4.0/pyface/data_view/value_types/tests/test_constant_value.py0000644000076500000240000001144414176222673027322 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 unittest.mock import Mock from traits.testing.api import UnittestTools from pyface.color import Color from pyface.data_view.value_types.constant_value import ConstantValue from pyface.image_resource import ImageResource class TestConstantValue(UnittestTools, TestCase): def setUp(self): self.model = Mock() def test_defaults(self): value_type = ConstantValue() self.assertEqual(value_type.text, "") self.assertEqual(value_type.tooltip, "") def test_has_editor_value(self): value_type = ConstantValue() self.assertFalse(value_type.has_editor_value(self.model, [0], [0])) def test_has_text(self): value_type = ConstantValue() self.assertFalse(value_type.has_text(self.model, [0], [0])) def test_has_text_true(self): value_type = ConstantValue(text="something") self.assertTrue(value_type.has_text(self.model, [0], [0])) def test_get_text(self): value_type = ConstantValue(text="something") self.assertEqual( value_type.get_text(self.model, [0], [0]), "something" ) def test_text_changed(self): value_type = ConstantValue() with self.assertTraitChanges(value_type, 'updated'): value_type.text = 'something' self.assertEqual(value_type.text, 'something') def test_has_color_default(self): value_type = ConstantValue() self.assertFalse(value_type.has_color(self.model, [0], [0])) def test_has_color(self): value_type = ConstantValue(color=Color(rgba=(0.4, 0.2, 0.6, 0.8))) self.assertTrue(value_type.has_color(self.model, [0], [0])) def test_get_color_default(self): value_type = ConstantValue() self.assertIsNone(value_type.get_color(self.model, [0], [0])) def test_get_color(self): value_type = ConstantValue(color='rebeccapurple') self.assertEqual( value_type.get_color(self.model, [0], [0]), Color(rgba=(0.4, 0.2, 0.6, 1.0)) ) def test_get_color_changed(self): value_type = ConstantValue() with self.assertTraitChanges(value_type, 'updated'): value_type.color = Color(rgba=(0.4, 0.2, 0.6, 0.8)) self.assertEqual( value_type.get_color(self.model, [0], [0]), Color(rgba=(0.4, 0.2, 0.6, 0.8)) ) def test_get_color_rgba_changed(self): value_type = ConstantValue(color=Color()) with self.assertTraitChanges(value_type, 'updated'): value_type.color.rgba = (0.4, 0.2, 0.6, 0.8) self.assertEqual( value_type.get_color(self.model, [0], [0]), Color(rgba=(0.4, 0.2, 0.6, 0.8)) ) def test_has_image(self): value_type = ConstantValue() self.assertFalse(value_type.has_image(self.model, [0], [0])) def test_has_image_true(self): value_type = ConstantValue(image="question") self.assertTrue(value_type.has_image(self.model, [0], [0])) def test_get_image(self): image = ImageResource("question") value_type = ConstantValue(image=image) self.assertEqual( value_type.get_image(self.model, [0], [0]), image ) def test_get_image_none(self): value_type = ConstantValue() image = value_type.get_image(self.model, [0], [0]) self.assertEqual(image.name, "image_not_found") def test_image_changed(self): value_type = ConstantValue() image = ImageResource("question") with self.assertTraitChanges(value_type, 'updated'): value_type.image = image self.assertEqual(value_type.image, image) def test_has_tooltip(self): value_type = ConstantValue() self.assertFalse(value_type.has_tooltip(self.model, [0], [0])) def test_has_tooltip_true(self): value_type = ConstantValue(tooltip="something") self.assertTrue(value_type.has_tooltip(self.model, [0], [0])) def test_get_tooltip(self): value_type = ConstantValue(tooltip="something") self.assertEqual( value_type.get_tooltip(self.model, [0], [0]), "something" ) def test_tooltip_changed(self): value_type = ConstantValue() with self.assertTraitChanges(value_type, 'updated'): value_type.tooltip = 'something' self.assertEqual(value_type.tooltip, 'something') pyface-7.4.0/pyface/data_view/value_types/tests/test_numeric_value.py0000644000076500000240000000665314176222673027141 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 unittest.mock import Mock from pyface.data_view.data_view_errors import DataViewSetError from pyface.data_view.value_types.numeric_value import ( FloatValue, IntValue, NumericValue, format_locale ) class TestNumericValue(TestCase): def setUp(self): self.model = Mock() self.model.get_value = Mock(return_value=1.0) self.model.can_set_value = Mock(return_value=True) self.model.set_value = Mock(return_value=True) def test_defaults(self): value = NumericValue() self.assertIsNone(value.evaluate) def test_is_valid(self): value = NumericValue() self.assertTrue(value.is_valid(None, [0], [0], 0.0)) def test_is_valid_false(self): value = NumericValue(minimum=0.0, maximum=1.0) self.assertFalse(value.is_valid(None, [0], [0], -1.0)) def test_is_valid_error(self): value = NumericValue() self.assertFalse(value.is_valid(None, [0], [0], 'invalid')) def test_get_editor_value(self): value = NumericValue(evaluate=float) editable = value.get_editor_value(self.model, [0], [0]) self.assertEqual(editable, 1.0) def test_set_editor_value(self): value = NumericValue(evaluate=float) value.set_editor_value(self.model, [0], [0], 1.0) self.model.set_value.assert_called_once_with([0], [0], 1.0) def test_set_editor_value_invalid(self): value = NumericValue(minimum=0.0, maximum=1.0) with self.assertRaises(DataViewSetError): value.set_editor_value(self.model, [0], [0], -1.0) self.model.set_value.assert_not_called() def test_set_editor_value_error(self): value = NumericValue(minimum=0.0, maximum=1.0) with self.assertRaises(DataViewSetError): value.set_editor_value(self.model, [0], [0], 'invalid') self.model.set_value.assert_not_called() def test_get_text(self): value = NumericValue() text = value.get_text(self.model, [0], [0]) self.assertEqual(text, format_locale(1.0)) def test_set_text(self): value = NumericValue(evaluate=float) value.set_text(self.model, [0], [0], format_locale(1.1)) self.model.set_value.assert_called_once_with([0], [0], 1.1) def test_set_text_invalid(self): value = NumericValue(evaluate=float, minimum=0.0, maximum=1.0) with self.assertRaises(DataViewSetError): value.set_text(self.model, [0], [0], format_locale(1.1)) self.model.set_value.assert_not_called() def test_set_text_error(self): value = NumericValue(evaluate=float) with self.assertRaises(DataViewSetError): value.set_text(self.model, [0], [0], "invalid") self.model.set_value.assert_not_called() class TestIntValue(TestCase): def test_defaults(self): value = IntValue() self.assertIs(value.evaluate, int) class TestFloatValue(TestCase): def test_defaults(self): value = FloatValue() self.assertIs(value.evaluate, float) pyface-7.4.0/pyface/data_view/value_types/tests/__init__.py0000644000076500000240000000000014176222673024757 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/data_view/value_types/tests/test_text_value.py0000644000076500000240000000364514176222673026461 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 unittest.mock import Mock from pyface.data_view.value_types.text_value import TextValue class TestTextValue(TestCase): def setUp(self): self.model = Mock() self.model.get_value = Mock(return_value="test") self.model.can_set_value = Mock(return_value=True) self.model.set_value = Mock() def test_defaults(self): value = TextValue() self.assertTrue(value.is_editable) def test_is_valid(self): value = TextValue() self.assertTrue(value.is_valid(None, [0], [0], "test")) def test_get_editor_value(self): value = TextValue() editable = value.get_editor_value(self.model, [0], [0]) self.assertEqual(editable, "test") def test_set_editor_value(self): value = TextValue() value.set_editor_value(self.model, [0], [0], "test") self.model.set_value.assert_called_once_with([0], [0], "test") def test_get_text(self): value = TextValue() editable = value.get_text(self.model, [0], [0]) self.assertEqual(editable, "test") def test_set_text(self): value = TextValue() value.set_text(self.model, [0], [0], "test") self.model.set_value.assert_called_once_with([0], [0], "test") def test_set_text_no_set_value(self): self.model.can_set_value = Mock(return_value=False) value = TextValue() value.set_text(self.model, [0], [0], "test") self.model.set_value.assert_called_once_with([0], [0], "test") pyface-7.4.0/pyface/data_view/value_types/tests/test_enum_value.py0000644000076500000240000000755014176222673026440 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 unittest.mock import Mock from pyface.data_view.data_view_errors import DataViewSetError from pyface.data_view.value_types.enum_value import EnumValue class TestEnumValue(TestCase): def setUp(self): self.model = Mock() self.model.get_value = Mock(return_value=1) self.model.can_set_value = Mock(return_value=True) self.model.set_value = Mock() def test_defaults(self): value = EnumValue() self.assertTrue(value.is_editable) def test_is_valid(self): value = EnumValue(values=[1, 2]) self.assertTrue(value.is_valid(None, [0], [0], 1)) def test_is_valid_false(self): value = EnumValue(values=[2]) self.assertFalse(value.is_valid(None, [0], [0], 1)) def test_get_editor_value(self): value = EnumValue() editable = value.get_editor_value(self.model, [0], [0]) self.assertEqual(editable, 1) def test_set_editor_value(self): value = EnumValue(values=[1, 2]) value.set_editor_value(self.model, [0], [0], 2) self.model.set_value.assert_called_once_with([0], [0], 2) def test_set_editor_value_bad(self): value = EnumValue(values=[1]) with self.assertRaises(DataViewSetError): value.set_editor_value(self.model, [0], [0], 2) def test_has_text(self): value = EnumValue(values=[1, 2]) has_text = value.has_text(self.model, [0], [0]) self.assertTrue(has_text) def test_has_text_false(self): value = EnumValue(values=[1, 2], format=None) has_text = value.has_text(self.model, [0], [0]) self.assertFalse(has_text) def test_get_text(self): value = EnumValue(values=[1, 2]) text = value.get_text(self.model, [0], [0]) self.assertEqual(text, "1") def test_has_color_false(self): value = EnumValue(values=[1, 2]) has_color = value.has_color(self.model, [0], [0]) self.assertFalse(has_color) def test_has_color_true(self): value = EnumValue(values=[1, 2], colors=lambda x: "dummy") has_color = value.has_color(self.model, [0], [0]) self.assertTrue(has_color) def test_get_color(self): value = EnumValue(values=[1, 2], colors=lambda x: "dummy") color = value.get_color(self.model, [0], [0]) self.assertEqual(color, "dummy") def test_color_function_none(self): value = EnumValue(values=[1, 2], colors=lambda x: None) has_color = value.has_color(self.model, [0], [0]) self.assertTrue(has_color) color = value.get_color(self.model, [0], [0]) self.assertIsNone(color) def test_has_image_false(self): value = EnumValue(values=[1, 2]) has_image = value.has_image(self.model, [0], [0]) self.assertFalse(has_image) def test_has_image_true(self): value = EnumValue(values=[1, 2], images=lambda x: "dummy") has_image = value.has_image(self.model, [0], [0]) self.assertTrue(has_image) def test_get_image(self): value = EnumValue(values=[1, 2], images=lambda x: "dummy") image = value.get_image(self.model, [0], [0]) self.assertEqual(image, "dummy") def test_image_function_none(self): value = EnumValue(values=[1, 2], images=lambda x: None) has_image = value.has_image(self.model, [0], [0]) self.assertTrue(has_image) image = value.get_image(self.model, [0], [0]) self.assertIsNone(image) pyface-7.4.0/pyface/data_view/value_types/tests/test_color_value.py0000644000076500000240000000523414176222673026607 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 unittest.mock import Mock from pyface.color import Color from pyface.data_view.abstract_data_model import DataViewSetError from pyface.data_view.value_types.color_value import ColorValue class TestColorValue(TestCase): def setUp(self): self.model = Mock() self.model.get_value = Mock( return_value=Color(rgba=(0.4, 0.2, 0.6, 0.8)), ) self.model.can_set_value = Mock(return_value=True) self.model.set_value = Mock() def test_defaults(self): value = ColorValue() self.assertTrue(value.is_editable) def test_is_valid(self): value = ColorValue() self.assertTrue(value.is_valid(None, [0], [0], Color())) def test_get_editor_value(self): value = ColorValue() editable = value.get_editor_value(self.model, [0], [0]) self.assertEqual(editable, "#663399CC") def test_set_editor_value(self): value = ColorValue() value.set_editor_value(self.model, [0], [0], "#3399CC66") self.model.set_value.assert_called_once_with( [0], [0], Color(rgba=(0.2, 0.6, 0.8, 0.4)), ) def test_get_text(self): value = ColorValue() editable = value.get_text(self.model, [0], [0]) self.assertEqual(editable, "#663399CC") def test_set_text(self): value = ColorValue() value.set_text(self.model, [0], [0], "red") self.model.set_value.assert_called_once_with( [0], [0], Color(rgba=(1.0, 0.0, 0.0, 1.0)), ) def test_set_text_error(self): value = ColorValue() with self.assertRaises(DataViewSetError): value.set_text(self.model, [0], [0], "not a real color") def test_set_text_no_set_value(self): self.model.can_set_value = Mock(return_value=False) value = ColorValue() value.set_text(self.model, [0], [0], "red") self.model.set_value.assert_called_once_with( [0], [0], Color(rgba=(1.0, 0.0, 0.0, 1.0)), ) def test_get_color(self): value = ColorValue() editable = value.get_color(self.model, [0], [0]) self.assertEqual(editable, Color(rgba=(0.4, 0.2, 0.6, 0.8))) pyface-7.4.0/pyface/data_view/value_types/tests/test_editable_value.py0000644000076500000240000000520214176222673027235 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 unittest.mock import Mock from traits.testing.api import UnittestTools from pyface.data_view.data_view_errors import DataViewSetError from pyface.data_view.value_types.editable_value import EditableValue class EditableWithValid(EditableValue): def is_valid(self, model, row, column, value): return value >= 0 class TestEditableValue(UnittestTools, TestCase): def setUp(self): self.model = Mock() self.model.get_value = Mock(return_value=1.0) self.model.can_set_value = Mock(return_value=True) self.model.set_value = Mock() def test_default(self): value_type = EditableValue() self.assertTrue(value_type.is_editable) def test_is_valid(self): value_type = EditableValue() result = value_type.is_valid(self.model, [0], [0], 2.0) self.assertTrue(result) def test_has_editor_value(self): value_type = EditableValue() result = value_type.has_editor_value(self.model, [0], [0]) self.assertTrue(result) def test_has_editor_value_not_editable(self): value_type = EditableValue(is_editable=False) result = value_type.has_editor_value(self.model, [0], [0]) self.assertFalse(result) def test_set_editor_value(self): value_type = EditableValue() value_type.set_editor_value(self.model, [0], [0], 2.0) self.model.set_value.assert_called_once_with([0], [0], 2.0) def test_set_editor_value_set_value_raises(self): self.model.set_value = Mock(side_effect=DataViewSetError) value_type = EditableValue(is_editable=False) with self.assertRaises(DataViewSetError): value_type.set_editor_value(self.model, [0], [0], 2.0) self.model.set_value.assert_called_once_with([0], [0], 2.0) def test_set_editor_value_not_valid(self): value_type = EditableWithValid() with self.assertRaises(DataViewSetError): value_type.set_editor_value(self.model, [0], [0], -1.0) self.model.set_value.assert_not_called() def test_is_editable_update(self): value_type = EditableValue() with self.assertTraitChanges(value_type, 'updated', count=1): value_type.is_editable = False pyface-7.4.0/pyface/data_view/value_types/tests/test_no_value.py0000644000076500000240000000217514176222673026106 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 unittest.mock import Mock from pyface.data_view.value_types.no_value import NoValue class TestNoValue(TestCase): def setUp(self): self.model = Mock() def test_has_editor_value(self): value_type = NoValue() self.assertFalse(value_type.has_editor_value(self.model, [0], [0])) def test_has_text(self): value_type = NoValue() self.assertFalse(value_type.has_text(self.model, [0], [0])) def test_has_image(self): value_type = NoValue() self.assertFalse(value_type.has_image(self.model, [0], [0])) def test_has_tooltip(self): value_type = NoValue() self.assertFalse(value_type.has_tooltip(self.model, [0], [0])) pyface-7.4.0/pyface/data_view/value_types/numeric_value.py0000644000076500000240000001030414176222673024724 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 locale from math import inf from traits.api import Callable, Float from pyface.data_view.data_view_errors import DataViewSetError from .editable_value import EditableValue def format_locale(value): return "{:n}".format(value) class NumericValue(EditableValue): """ Data channels for a numeric value. """ #: The minimum value for the numeric value. minimum = Float(-inf) #: The maximum value for the numeric value. maximum = Float(inf) #: A function that converts to the numeric type. evaluate = Callable() #: A function that converts the required type to a string for display. format = Callable(format_locale, update_value_type=True) #: A function that converts the required type from a display string. unformat = Callable(locale.delocalize) def is_valid(self, model, row, column, value): """ Whether or not the value within the specified range. Parameters ---------- model : AbstractDataModel The data model holding the data. row : sequence of int The row in the data model being queried. column : sequence of int The column in the data model being queried. value : any The value to validate. Returns ------- is_valid : bool Whether or not the value is valid. """ try: return self.minimum <= value <= self.maximum except Exception: return False def get_editor_value(self, model, row, column): """ Get the numerical value for the editor to use. This uses the evaluate method to convert the underlying value to a number. Parameters ---------- model : AbstractDataModel The data model holding the data. row : sequence of int The row in the data model being queried. column : sequence of int The column in the data model being queried. Returns ------- editor_value : number Whether or not the value is editable. """ # evaluate is needed to convert numpy types to python types so # Qt recognises them return self.evaluate(model.get_value(row, column)) def get_text(self, model, row, column): """ Get the display text from the underlying value. Parameters ---------- model : AbstractDataModel The data model holding the data. row : sequence of int The row in the data model being queried. column : sequence of int The column in the data model being queried. Returns ------- text : str The text to display. """ return self.format(model.get_value(row, column)) def set_text(self, model, row, column, text): """ Set the text of the underlying value. Parameters ---------- model : AbstractDataModel The data model holding the data. row : sequence of int The row in the data model being queried. column : sequence of int The column in the data model being queried. text : str The text to set. Raises ------- DataViewSetError If the value cannot be set. """ try: value = self.evaluate(self.unformat(text)) except ValueError: raise DataViewSetError( "Can't evaluate value: {!r}".format(text) ) self.set_editor_value(model, row, column, value) class IntValue(NumericValue): """ Data channels for an integer value. """ evaluate = Callable(int) class FloatValue(NumericValue): """ Data channels for a floating point value. """ evaluate = Callable(float) pyface-7.4.0/pyface/data_view/value_types/__init__.py0000644000076500000240000000000014176222673023615 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/data_view/value_types/text_value.py0000644000076500000240000000367714176222673024265 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 Callable from .editable_value import EditableValue class TextValue(EditableValue): """ Editable value that presents a string value. """ #: A function that converts the value to a string for display. format = Callable(str, update_value_type=True) #: A function that converts to a value from a display string. unformat = Callable(str) def get_text(self, model, row, column): """ Get the display text from the underlying value. Parameters ---------- model : AbstractDataModel The data model holding the data. row : sequence of int The row in the data model being queried. column : sequence of int The column in the data model being queried. Returns ------- text : str The text to display. """ return self.format(model.get_value(row, column)) def set_text(self, model, row, column, text): """ Set the text of the underlying value. Parameters ---------- model : AbstractDataModel The data model holding the data. row : sequence of int The row in the data model being queried. column : sequence of int The column in the data model being queried. text : str The text to set. Raises ------- DataViewSetError If the value cannot be set. """ value = self.unformat(text) self.set_editor_value(model, row, column, value) pyface-7.4.0/pyface/data_view/value_types/no_value.py0000644000076500000240000000164514176222673023706 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.data_view.abstract_value_type import AbstractValueType class NoValue(AbstractValueType): """ A ValueType that has no data in any channel. """ def has_editor_value(self, model, row, column): return False def has_text(self, model, row, column): return False def has_image(self, model, row, column): return False def has_tooltip(self, model, row, column): return False #: Standard instance of the NoValue class, since it has no state. no_value = NoValue() pyface-7.4.0/pyface/data_view/value_types/api.py0000644000076500000240000000223014176222673022636 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 ``pyface.data_view.value_types`` subpackage. Value Types ----------- - :class:`~.BoolValue` - :class:`~.ColorValue` - :class:`~.ConstantValue` - :class:`~.EditableValue` - :class:`~.NoValue` - :attr:`~.no_value` - :class:`~.FloatValue` - :class:`~.IntValue` - :class:`~.NumericValue` - :class:`~.TextValue` """ from .bool_value import BoolValue # noqa: F401 from .color_value import ColorValue # noqa: F401 from .constant_value import ConstantValue # noqa: F401 from .editable_value import EditableValue # noqa: F401 from .enum_value import EnumValue # noqa: F401 from .no_value import NoValue, no_value # noqa: F401 from .numeric_value import FloatValue, IntValue, NumericValue # noqa: F401 from .text_value import TextValue # noqa: F401 pyface-7.4.0/pyface/data_view/value_types/color_value.py0000644000076500000240000001234214176222673024404 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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.color import Color from pyface.data_view.abstract_data_model import DataViewSetError from .editable_value import EditableValue class ColorValue(EditableValue): """ Editable value that presents a color value. This is suitable for use where the value returned by an item in the model is a Color object. """ def is_valid(self, model, row, column, value): """ Is the given value valid for this item. The default implementation says the value must be a Color. Parameters ---------- model : AbstractDataModel The data model holding the data. row : sequence of int The row in the data model being queried. column : sequence of int The column in the data model being queried. value : any The value to test. Returns ------- success : bool Whether or not the value is valid. """ return isinstance(value, Color) def get_editor_value(self, model, row, column): """ Get the editable representation of the underlying value. The default uses a text hex representation of the color. Parameters ---------- model : AbstractDataModel The data model holding the data. row : sequence of int The row in the data model being queried. column : sequence of int The column in the data model being queried. Returns ------- text : str A hex string representation of the colour. """ return model.get_value(row, column).hex() def set_editor_value(self, model, row, column, value): """ Set the editable representation of the underlying value. The default expects a string that can be parsed to a color value. Parameters ---------- model : AbstractDataModel The data model holding the data. row : sequence of int The row in the data model being queried. column : sequence of int The column in the data model being queried. value : str A string that can be parsed to a color value. Returns ------- success : bool Whether or not the value was successfully set. """ try: color = Color.from_str(value) except Exception: raise DataViewSetError() return super().set_editor_value(model, row, column, color) def get_text(self, model, row, column): """ Get the textual representation of the underlying value. Parameters ---------- model : AbstractDataModel The data model holding the data. row : sequence of int The row in the data model being queried. column : sequence of int The column in the data model being queried. Returns ------- text : str A hex string representation of the colour. """ return model.get_value(row, column).hex() def set_text(self, model, row, column, text): """ Set the textual representation of the underlying value. Parameters ---------- model : AbstractDataModel The data model holding the data. row : sequence of int The row in the data model being queried. column : sequence of int The column in the data model being queried. text : str The text to set. Returns ------- success : bool Whether or not the value was successfully set. """ return self.set_editor_value(model, row, column, text) def has_color(self, model, row, column): """ Whether or not the value has color data. The default implementation returns False. Parameters ---------- model : AbstractDataModel The data model holding the data. row : sequence of int The row in the data model being queried. column : sequence of int The column in the data model being queried. Returns ------- has_color : bool Whether or not the value has data-associated color values. """ return True def get_color(self, model, row, column): """ Get data-associated colour values for the given item. Parameters ---------- model : AbstractDataModel The data model holding the data. row : sequence of int The row in the data model being queried. column : sequence of int The column in the data model being queried. Returns ------- color : Color instance The color associated with the cell. """ return model.get_value(row, column) pyface-7.4.0/pyface/data_view/value_types/constant_value.py0000644000076500000240000000566614176222673025132 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 Str, Union, observe from pyface.ui_traits import PyfaceColor from pyface.data_view.abstract_value_type import AbstractValueType from pyface.ui_traits import Image class ConstantValue(AbstractValueType): """ A value type that does not depend on the underlying data model. This value type is not editable, but the other data channels it provides can be modified by changing the appropriate trait on the value type. """ #: The text value to display. text = Str(update_value_type=True) #: The color value to display or None if no color. color = Union(None, PyfaceColor) #: The image value to display. image = Image(update_value_type=True) #: The tooltip value to display. tooltip = Str(update_value_type=True) def has_editor_value(self, model, row, column): return False def get_text(self, model, row, column): return self.text def has_color(self, model, row, column): """ Whether or not the value has color data. Returns true if the supplied color is not None. Parameters ---------- model : AbstractDataModel The data model holding the data. row : sequence of int The row in the data model being queried. column : sequence of int The column in the data model being queried. Returns ------- has_color : bool Whether or not the value has data-associated color values. """ return self.color is not None def get_color(self, model, row, column): """ Get data-associated colour values for the given item. The default implementation returns white. Parameters ---------- model : AbstractDataModel The data model holding the data. row : sequence of int The row in the data model being queried. column : sequence of int The column in the data model being queried. Returns ------- color : Color instance The color associated with the cell. """ return self.color def has_image(self, model, row, column): return self.image is not None def get_image(self, model, row, column): if self.image is not None: return self.image return super().get_image(model, row, column) def get_tooltip(self, model, row, column): return self.tooltip @observe('color.rgba') def _color_updated(self, event): self.updated = True pyface-7.4.0/pyface/data_view/value_types/bool_value.py0000644000076500000240000000627714176222673024233 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 Str from pyface.data_view.abstract_value_type import AbstractValueType, CheckState class BoolValue(AbstractValueType): """ Value that presents a boolean value via checked state. """ #: The text to display next to a True value. true_text = Str() #: The text to display next to a False value. false_text = Str() def has_editor_value(self, model, row, column): """ BoolValues don't use editors, but have always-on checkbox. Parameters ---------- model : AbstractDataModel The data model holding the data. row : sequence of int The row in the data model being queried. column : sequence of int The column in the data model being queried. Returns ------- has_editor_value : bool Whether or not the value is editable. """ return False def get_text(self, model, row, column): """ The textual representation of the underlying value. Parameters ---------- model : AbstractDataModel The data model holding the data. row : sequence of int The row in the data model being queried. column : sequence of int The column in the data model being queried. Returns ------- text : str The textual representation of the underlying value. """ return ( self.true_text if model.get_value(row, column) else self.false_text ) def has_check_state(self, model, row, column): """ Whether or not the value has checked state. The default implementation returns True. Parameters ---------- model : AbstractDataModel The data model holding the data. row : sequence of int The row in the data model being queried. column : sequence of int The column in the data model being queried. Returns ------- has_check_state : bool Whether or not the value has a checked state. """ return True def set_check_state(self, model, row, column, check_state): """ Set the boolean value from the check state. Parameters ---------- model : AbstractDataModel The data model holding the data. row : sequence of int The row in the data model being set. column : sequence of int The column in the data model being set. check_state : "checked" or "unchecked" The check state being set. Raises ------- DataViewSetError If the value cannot be set. """ value = (check_state == CheckState.CHECKED) model.set_value(row, column, value) pyface-7.4.0/pyface/resource/0000755000076500000240000000000014176460550017040 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/resource/resource_path.py0000644000076500000240000000253714176222673022266 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Functions to determine resource locations from the call stack. This type of resource location is normally requested from the constructor for an object whose resources are relative to the module constructing the object. """ import sys from traits.trait_base import get_resource_path def resource_module(level=2): """Return a module reference calculated from the caller's stack. Note that what we want is the reference to the package containing the module in the stack. This is because we need a directory to search for our default resource sub-dirs as children. """ module_name = sys._getframe(level).f_globals.get("__name__", "__main__") if "." in module_name: module_name = ".".join(module_name.split(".")[:-1]) module = sys.modules.get(module_name) return module def resource_path(level=2): """Return a resource path calculated from the caller's stack. """ return get_resource_path(level + 1) pyface-7.4.0/pyface/resource/resource_manager.py0000644000076500000240000002757614176222673022756 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 default resource manager. A resource manager locates and loads application resources such as images and sounds etc. """ import collections.abc import glob import inspect import os from os.path import join import types from zipfile import is_zipfile, ZipFile # importlib.resources is new in Python 3.7, and importlib.resources.files is # new in Python 3.9, so for Python < 3.9 we must rely on the 3rd party # importlib_resources package. try: from importlib.resources import files except ImportError: from importlib_resources import files from traits.api import HasTraits, Instance, List from traits.util.resource import get_path from pyface.resource.resource_factory import ResourceFactory from pyface.resource.resource_reference import ImageReference class ResourceManager(HasTraits): """ The default resource manager. A resource manager locates and loads application resources such as images and sounds etc. """ # Allowed extensions for image resources. IMAGE_EXTENSIONS = [".png", ".jpg", ".bmp", ".gif", ".ico"] # A list of additional search paths. These paths are fallbacks, and hence # have lower priority than the paths provided by resource objects. extra_paths = List() # The resource factory is responsible for actually creating resources. # This is used so that (for example) different GUI toolkits can create # a images in the format that they require. resource_factory = Instance(ResourceFactory) # ------------------------------------------------------------------------ # 'ResourceManager' interface. # ------------------------------------------------------------------------ def locate_image(self, image_name, path, size=None): """ Locates an image. Parameters ---------- image_name : str Name of the image file. path : list of (str or ModuleType) Paths from which image files will be searched. Note that for each path, a subdirectory named 'images' will be search first. The first match will be returned. size : tuple of (m: int, n: int), optional Specific size of the image requested. If provided, then the subdirectory ``images/{m}x{n}`` will be searched first, followed by the ``images`` subdirectory and its containing folder. Default is None. Returns ------- image_ref : ImageReference or None ImageReference to the image found, or None if no matching images are found. """ if not isinstance(path, collections.abc.Sequence): path = [path] resource_path = [] for item in list(path) + self.extra_paths: if isinstance(item, str): resource_path.append(item) elif isinstance(item, types.ModuleType): resource_path.append(item) else: resource_path.extend(self._get_resource_path(item)) return self._locate_image(image_name, resource_path, size) def load_image(self, image_name, path, size=None): """ Loads an image. """ reference = self.locate_image(image_name, path, size) if reference is not None: image = reference.load() else: image = None return image # ------------------------------------------------------------------------ # Private interface. # ------------------------------------------------------------------------ def _locate_image(self, image_name, resource_path, size): """ Attempts to locate an image resource. If the image is found, an image resource reference is returned. If the image is NOT found None is returned. """ # If the image name contains a file extension (eg. '.jpg') then we will # only accept an an EXACT filename match. basename, extension = os.path.splitext(image_name) if len(extension) > 0: extensions = [extension] pattern = image_name # Otherwise, we will search for common image suffixes. else: extensions = self.IMAGE_EXTENSIONS pattern = image_name + ".*" # Try the 'images' sub-directory first (since that is commonly # where we put them!). If the image is not found there then look # in the directory itself. if size is None: subdirs = ["images", ""] else: subdirs = ["images/%dx%d" % (size[0], size[1]), "images", ""] # Concrete image filenames to be searched image_filenames = [basename + extension for extension in extensions] for dirname in resource_path: # If we come across a reference to a module, try and find the # image inside of an .egg, .zip, etc. if isinstance(dirname, types.ModuleType): try: data = _find_resource_data( dirname, subdirs, image_filenames ) except OSError: continue else: return ImageReference( self.resource_factory, data=data ) # Is there anything resembling the image name in the directory? for path in subdirs: filenames = glob.glob(join(dirname, path, pattern)) for filename in filenames: not_used, extension = os.path.splitext(filename) if extension in extensions: reference = ImageReference( self.resource_factory, filename=filename ) return reference # Is there an 'images' zip file in the directory? zip_filename = join(dirname, "images.zip") if os.path.isfile(zip_filename): zip_file = ZipFile(zip_filename, "r") # Try the image name itself, and then the image name with # common images suffixes. for extension in extensions: try: image_data = zip_file.read(basename + extension) reference = ImageReference( self.resource_factory, data=image_data ) return reference except: pass # is this a path within a zip file? # first, find the zip file in the path filepath = dirname zippath = "" while ( not is_zipfile(filepath) and os.path.splitdrive(filepath)[1].startswith("\\") and os.path.splitdrive(filepath)[1].startswith("/") ): filepath, tail = os.path.split(filepath) if zippath != "": zippath = tail + "/" + zippath else: zippath = tail # if we found a zipfile, then look inside it for the image! if is_zipfile(filepath): zip_file = ZipFile(filepath) for subpath in ["images", ""]: for extension in extensions: try: # this is a little messy. since zip files don't # recognize a leading slash, we have to be very # particular about how we build this path when # there are empty strings if zippath != "": path = zippath + "/" else: path = "" if subpath != "": path = path + subpath + "/" path = path + basename + extension # now that we have the path we can attempt to load # the image image_data = zip_file.read(path) reference = ImageReference( self.resource_factory, data=image_data ) # if there was no exception then return the result return reference except: pass return None def _get_resource_path(self, object): """ Returns the resource path for an object. """ if hasattr(object, "resource_path"): resource_path = object.resource_path else: resource_path = self._get_default_resource_path(object) return resource_path def _get_default_resource_path(self, object): """ Returns the default resource path for an object. """ resource_path = [] for klass in inspect.getmro(object.__class__): try: resource_path.append(get_path(klass)) # We get an attribute error when we get to a C extension type (in # our case it will most likley be 'CHasTraits'. We simply ignore # everything after this point! except AttributeError: break return resource_path def _get_package_data(module, rel_path): """ Return package data in bytes for the given module and resource path. Parameters ---------- module : ModuleType A module from which package data will be discovered. If the module name does not conform to the package requirement, then its "__file__" attribute is used for locating the directory to search for resource files. rel_path : str "/"-separated path for loading data file. Returns ------- data : bytes Loaded data in bytes. Raises ------ OSError If the path referenced does not resolve to an existing file or the file cannot be read. """ if (module.__spec__ is None or module.__spec__.submodule_search_locations is None): module_dir_path = os.path.dirname(module.__file__) path = os.path.join(module_dir_path, *rel_path.split("/")) with open(path, "rb") as fp: return fp.read() return ( files(module).joinpath(rel_path).read_bytes() ) def _find_resource_data(module, subdirs, filenames): """ For the given module, search directories and names, find a matching resource file and return its content as bytes. Parameters ---------- module : ModuleType A module from which package data will be discovered. If the module name does not conform to the package requirement, then its "__file__" attribute is used for locating the directory to search for resource files. subdirs : list of str Name of subdirectories to try. Each value can be a "/"-separated string to represent more nested subdirectories. filenames : list of str File names to try. Returns ------- data : bytes Loaded data in bytes. Raises ------ OSError If the path referenced does not resolve to an existing file or the file cannot be read. """ for path in subdirs: for filename in filenames: searchpath = "%s/%s" % (path, filename) try: return _get_package_data(module, searchpath) except OSError: pass raise OSError( "Unable to load data for the given module and search paths." ) pyface-7.4.0/pyface/resource/__init__.py0000644000076500000240000000103014176222673021145 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Support for managing resources such as images and sounds. Part of the TraitsUI project of the Enthought Tool Suite. """ pyface-7.4.0/pyface/resource/resource_reference.py0000644000076500000240000000442514176222673023266 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Resource references. """ from traits.api import Any, HasTraits, Instance from pyface.resource.resource_factory import ResourceFactory class ResourceReference(HasTraits): """ Abstract base class for resource references. Resource references are returned from calls to 'locate_reference' on the resource manager. """ # The resource factory that will be used to load the resource. resource_factory = Instance(ResourceFactory) # ReadOnly # ------------------------------------------------------------------------ # 'ResourceReference' interface. # ------------------------------------------------------------------------ def load(self): """ Loads the resource. """ raise NotImplementedError() class ImageReference(ResourceReference): """ A reference to an image resource. """ # Iff the image was found in a file then this is the name of that file. filename = Any # ReadOnly # Iff the image was found in a zip file then this is the image data that # was read from the zip file. data = Any # ReadOnly def __init__(self, resource_factory, filename=None, data=None): """ Creates a new image reference. """ self.resource_factory = resource_factory self.filename = filename self.data = data return # ------------------------------------------------------------------------ # 'ResourceReference' interface. # ------------------------------------------------------------------------ def load(self): """ Loads the resource. """ if self.filename is not None: image = self.resource_factory.image_from_file(self.filename) elif self.data is not None: image = self.resource_factory.image_from_data(self.data) else: raise ValueError("Image reference has no filename OR data") return image pyface-7.4.0/pyface/resource/api.py0000644000076500000240000000125214176222673020165 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 ``pyface.resource`` subpackage. - :class:`~.ResourceFactory` - :class:`~.ResourceManager` - :func:`~.resource_path` """ from .resource_factory import ResourceFactory from .resource_manager import ResourceManager from .resource_path import resource_path pyface-7.4.0/pyface/resource/resource_factory.py0000644000076500000240000000200314176222673022765 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 base-class for resource factories. """ class ResourceFactory(object): """ Default base-class for resource factories. """ # ------------------------------------------------------------------------ # 'ResourceFactory' interface. # ------------------------------------------------------------------------ def image_from_file(self, filename): """ Creates an image from the data in the specified filename. """ raise NotImplementedError() def image_from_data(self, data): """ Creates an image from the specified data. """ raise NotImplementedError() pyface-7.4.0/pyface/mimedata.py0000644000076500000240000000116614176222673017352 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 pyface.toolkit import toolkit_object # WIP: Currently only supports qt4 backend. API might change without # prior notification PyMimeData = toolkit_object("mimedata:PyMimeData") pyface-7.4.0/pyface/i_widget.py0000644000076500000240000001656214176222673017372 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 base interface for all pyface widgets. """ from traits.api import Any, Bool, HasTraits, Interface, Instance, Str class IWidget(Interface): """ The base interface for all pyface widgets. Pyface widgets delegate to a toolkit specific control. """ #: The toolkit specific control that represents the widget. control = Any() #: The control's optional parent control. parent = Any() #: Whether or not the control is visible visible = Bool(True) #: Whether or not the control is enabled enabled = Bool(True) #: A tooltip for the widget. tooltip = Str() #: An optional context menu for the widget. context_menu = Instance("pyface.action.menu_manager.MenuManager") # ------------------------------------------------------------------------ # 'IWidget' interface. # ------------------------------------------------------------------------ def show(self, visible): """ Show or hide the widget. Parameters ---------- visible : bool Visible should be ``True`` if the widget should be shown. """ def enable(self, enabled): """ Enable or disable the widget. Parameters ---------- enabled : bool The enabled state to set the widget to. """ def focus(self): """ Set the keyboard focus to this widget. """ def has_focus(self): """ Does the widget currently have keyboard focus? Returns ------- focus_state : bool Whether or not the widget has keyboard focus. """ def create(self): """ Creates the toolkit specific control. This method should create the control and assign it to the :py:attr:``control`` trait. """ def destroy(self): """ Destroy the control if it exists. """ # ------------------------------------------------------------------------ # Protected 'IWidget' interface. # ------------------------------------------------------------------------ def _create_control(self, parent): """ Create toolkit specific control that represents the widget. Parameters ---------- parent : toolkit control The toolkit control to be used as the parent for the widget's control. Returns ------- control : toolkit control A control for the widget. """ def _add_event_listeners(self): """ Set up toolkit-specific bindings for events """ def _remove_event_listeners(self): """ Remove toolkit-specific bindings for events """ class MWidget(HasTraits): """ The mixin class that contains common code for toolkit specific implementations of the IWidget interface. """ #: A tooltip for the widget. tooltip = Str() #: An optional context menu for the widget. context_menu = Instance("pyface.action.menu_manager.MenuManager") def create(self): """ Creates the toolkit specific control. The default implementation simply calls _create() """ self._create() def destroy(self): """ Call clean-up code and destroy toolkit objects. Subclasses should override to perform any additional clean-up, ensuring that they call super() after that clean-up. """ if self.control is not None: self._remove_event_listeners() self.control = None # ------------------------------------------------------------------------ # Protected 'IWidget' interface. # ------------------------------------------------------------------------ def _create(self): """ Creates the toolkit specific control. This method should create the control and assign it to the :py:attr:``control`` trait. """ self.control = self._create_control(self.parent) self._initialize_control() self._add_event_listeners() def _create_control(self, parent): """ Create toolkit specific control that represents the widget. Parameters ---------- parent : toolkit control The toolkit control to be used as the parent for the widget's control. Returns ------- control : toolkit control A control for the widget. """ raise NotImplementedError() def _initialize_control(self): """ Perform any post-creation initialization for the control. """ self._set_control_tooltip(self.tooltip) def _add_event_listeners(self): """ Set up toolkit-specific bindings for events """ self.observe(self._tooltip_updated, "tooltip", dispatch="ui") self.observe( self._context_menu_updated, "context_menu", dispatch="ui" ) if self.control is not None and self.context_menu is not None: self._observe_control_context_menu() def _remove_event_listeners(self): """ Remove toolkit-specific bindings for events """ if self.control is not None and self.context_menu is not None: self._observe_control_context_menu(remove=True) self.observe( self._context_menu_updated, "context_menu", dispatch="ui", remove=True, ) self.observe( self._tooltip_updated, "tooltip", dispatch="ui", remove=True ) # Toolkit control interface --------------------------------------------- def _get_control_tooltip(self): """ Toolkit specific method to get the control's tooltip. """ raise NotImplementedError() def _set_control_tooltip(self, tooltip): """ Toolkit specific method to set the control's tooltip. """ raise NotImplementedError() def _observe_control_context_menu(self, remove=False): """ Toolkit specific method to change the context menu observer. This should use _handle_control_context_menu as the event handler. Parameters ---------- remove : bool Whether the context menu handler should be removed or added. """ raise NotImplementedError() def _handle_control_context_menu(self, event): """ Handle a context menu event. Implementations should override this with a method suitable to be used as a toolkit event handler that invokes a context menu. The function signature will likely vary from toolkit to toolkit. """ raise NotImplementedError() # Trait change handlers ------------------------------------------------- def _tooltip_updated(self, event): tooltip = event.new if self.control is not None: self._set_control_tooltip(tooltip) def _context_menu_updated(self, event): if self.control is not None: if event.new is None: self._observe_control_context_menu(remove=True) if event.old is None: self._observe_control_context_menu() pyface-7.4.0/pyface/mdi_window_menu.py0000644000076500000240000001206114176222673020751 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 menu that mimics the standard MDI window menu. This is the menu that has the tile/cascade actions etc. """ from traits.api import Str from .action.api import MenuManager, Separator, WindowAction class Cascade(WindowAction): """ Cascades the windows. """ # 'Action' interface --------------------------------------------------- name = Str("Ca&scade") tooltip = Str("Cascade the windows") # ------------------------------------------------------------------------ # 'Action' interface. # ------------------------------------------------------------------------ def perform(self, event): """ Cascades the windows. """ self.window.control.Cascade() class Tile(WindowAction): """ Tiles the windows horizontally. """ # 'Action' interface --------------------------------------------------- name = Str("&Tile") tooltip = Str("Tile the windows") # ------------------------------------------------------------------------ # 'Action' interface. # ------------------------------------------------------------------------ def perform(self, event): """ Tiles the windows horizontally. """ self.window.control.Tile() class ArrangeIcons(WindowAction): """ Arranges the icons. """ # 'Action' interface --------------------------------------------------- name = Str("&Arrange Icons") tooltip = Str("Arrange the icons") # ------------------------------------------------------------------------ # 'Action' interface. # ------------------------------------------------------------------------ def perform(self, event): """ Arranges the icons. """ self.window.control.ArrangeIcons() class Next(WindowAction): """ Activates the next window. """ # 'Action' interface --------------------------------------------------- name = Str("&Next") tooltip = Str("Activate the next window") # ------------------------------------------------------------------------ # 'Action' interface. # ------------------------------------------------------------------------ def perform(self, event): """ Activates the next window. """ self.window.control.ActivateNext() class Previous(WindowAction): """ Activates the previous window. """ # 'Action' interface --------------------------------------------------- name = Str("&Previous") tooltip = Str("Activate the previous window") # ------------------------------------------------------------------------ # 'Action' interface. # ------------------------------------------------------------------------ def perform(self, event): """ Activates the previous window. """ self.window.control.ActivatePrevious() class Close(WindowAction): """ Closes the current window. """ # 'Action' interface --------------------------------------------------- name = Str("&Close") tooltip = Str("Close the current window") # ------------------------------------------------------------------------ # 'Action' interface. # ------------------------------------------------------------------------ def perform(self, event): """ Closes the current window. """ page = self.window.control.GetActiveChild() if page is not None: page.Close() class CloseAll(WindowAction): """ Closes all of the child windows. """ # 'Action' interface --------------------------------------------------- name = Str("Close A&ll") tooltip = Str("Close all of the windows.") # ------------------------------------------------------------------------ # 'Action' interface. # ------------------------------------------------------------------------ def perform(self, event): """ Closes the child windows. """ for page in self.window.control.GetChildren(): page.Close() class MDIWindowMenu(MenuManager): """ A menu that mimics the standard MDI window menus. This is the menu that has the tile/cascade actions etc. """ # ------------------------------------------------------------------------ # 'object' interface. # ------------------------------------------------------------------------ def __init__(self, window): """ Creates a new MDI window menu. """ # Base class constructor. super().__init__( Cascade(window=window), Tile(window=window), Separator(), ArrangeIcons(window=window), Next(window=window), Previous(window=window), Close(window=window), CloseAll(window=window), name="&Window", ) pyface-7.4.0/pyface/i_clipboard.py0000644000076500000240000000563314176222673020043 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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: Evan Patterson # Date: 06/26/09 # ------------------------------------------------------------------------------ """ The interface for manipulating the toolkit clipboard. """ from collections.abc import Sequence from traits.api import HasStrictTraits, Interface, Property class IClipboard(Interface): """ The interface for manipulating the toolkit clipboard. """ #: The type of data in the clipboard (string) data_type = Property #: Arbitrary Python data stored in the clipboard data = Property #: Arbitrary Python data is available in the clipboard has_data = Property #: Name of the class of object in the clipboard object_type = Property #: Python object data object_data = Property #: Python object data is available has_object_data = Property #: Text data text_data = Property #: Text data is available has_text_data = Property #: File name data file_data = Property #: File name data is available has_file_data = Property class BaseClipboard(HasStrictTraits): """ An abstract base class that contains common code for toolkit specific implementations of IClipboard. """ #: The type of data in the clipboard (string) data_type = Property #: Arbitrary Python data stored in the clipboard data = Property #: Arbitrary Python data is available in the clipboard has_data = Property #: Name of the class of object in the clipboard object_type = Property #: Python object data object_data = Property #: Python object data is available has_object_data = Property #: Text data text_data = Property #: Text data is available has_text_data = Property #: File name data file_data = Property #: File name data is available has_file_data = Property def _get_data(self): if self.has_text_data: return self.text_data if self.has_file_data: return self.file_data if self.has_object_data: return self.object_data return None def _set_data(self, data): if isinstance(data, str): self.text_data = data elif isinstance(data, Sequence): self.file_data = data else: self.object_data = data def _get_data_type(self): if self.has_text_data: return "str" if self.has_file_data: return "file" if self.has_object_data: return self.object_type return "" pyface-7.4.0/pyface/gui.py0000644000076500000240000000106414176222673016352 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 implementation of a pyface GUI. """ # Import the toolkit specific version. from .toolkit import toolkit_object GUI = toolkit_object("gui:GUI") pyface-7.4.0/pyface/image_cache.py0000644000076500000240000000111214176222673017765 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 implementation of an image cache. """ # Import the toolkit specific version. from .toolkit import toolkit_object ImageCache = toolkit_object("image_cache:ImageCache") pyface-7.4.0/pyface/file_dialog.py0000644000076500000240000000116214176222673020023 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 implementation of a dialog that allows the user to open/save files etc. """ # Import the toolkit specific version. from .toolkit import toolkit_object FileDialog = toolkit_object("file_dialog:FileDialog") pyface-7.4.0/pyface/dock/0000755000076500000240000000000014176460550016131 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/dock/dock_sizer.py0000644000076500000240000045607314176222673020660 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Pyface 'DockSizer' support. This package provides the sizer associated with a Pyface DockWindow component. The sizer manages the layout of the DockWindow child controls and the notebook tabs and dragbars associated with the DockWindow. """ import sys import wx from traits.api import ( HasPrivateTraits, Instance, Str, Int, List, Enum, Tuple, Any, Range, Property, Callable, Constant, Event, Undefined, Bool, cached_property, observe, ) from traitsui.dock_window_theme import dock_window_theme from traitsui.wx.helper import BufferDC from pyface.api import SystemMetrics from pyface.image_resource import ImageResource from pyface.ui_traits import Image from pyface.wx.drag_and_drop import PythonDropSource from pyface.timer.api import do_later, do_after from .idockable import IDockable from .ifeature_tool import IFeatureTool # Define version dependent values: is_mac = sys.platform == "darwin" # ------------------------------------------------------------------------------- # Constants: # ------------------------------------------------------------------------------- # Standard font text height: text_dy = 13 # Maximum allowed length of a tab label: MaxTabLength = 30 # Size of a drag bar (in pixels): DragBarSize = 14 # Images sizes (in pixels): CloseTabSize = 10 CloseDragSize = 7 # Tab drawing states: TabInactive = 0 TabActive = 1 TabHover = 2 NormalStates = (TabInactive, TabActive) NotActiveStates = (TabInactive, TabHover) # Feature overlay colors: FeatureBrushColor = (255, 255, 255) FeaturePenColor = (92, 92, 92) # Color used to update the screen while dragging a splitter bar: DragColor = (96, 96, 96) # Color used to update the screen while showing a docking operation in progress: DockColorBrush = (255, 0, 0, 96) # Drop Info kinds: DOCK_TOP = 0 DOCK_BOTTOM = 1 DOCK_LEFT = 2 DOCK_RIGHT = 3 DOCK_TAB = 4 DOCK_TABADD = 5 DOCK_BAR = 6 DOCK_NONE = 7 DOCK_SPLITTER = 8 DOCK_EXPORT = 9 # Splitter states: SPLIT_VLEFT = 0 SPLIT_VMIDDLE = 1 SPLIT_VRIGHT = 2 SPLIT_HTOP = 3 SPLIT_HMIDDLE = 4 SPLIT_HBOTTOM = 5 # Empty clipping area: no_clip = (0, 0, 0, 0) # Valid sequence types: SequenceType = (list, tuple) # Tab scrolling directions: SCROLL_LEFT = 1 SCROLL_RIGHT = 2 SCROLL_TO = 3 # Feature modes: FEATURE_NONE = -1 # Has no features FEATURE_NORMAL = 0 # Has normal features FEATURE_CHANGED = 1 # Has changed or new features FEATURE_DROP = 2 # Has drag data compatible drop features FEATURE_DISABLED = 3 # Has feature icon, but is currently disabled FEATURE_VISIBLE = 4 # Has visible features (mouseover mode) FEATURE_DROP_VISIBLE = 5 # Has visible drop features (mouseover mode) FEATURE_PRE_NORMAL = 6 # Has normal features (but has not been drawn yet) FEATURE_EXTERNAL_DRAG = 256 # A drag started in another DockWindow is active # Feature sets: NO_FEATURE_ICON = ( FEATURE_NONE, FEATURE_DISABLED, FEATURE_VISIBLE, FEATURE_DROP_VISIBLE, ) FEATURES_VISIBLE = (FEATURE_VISIBLE, FEATURE_DROP_VISIBLE) FEATURE_END_DROP = (FEATURE_DROP, FEATURE_VISIBLE, FEATURE_DROP_VISIBLE) NORMAL_FEATURES = (FEATURE_NORMAL, FEATURE_DISABLED) # ------------------------------------------------------------------------------- # Global data: # ------------------------------------------------------------------------------- # Standard font used by the DockWindow: standard_font = None # The list of available DockWindowFeatures: features = [] # ------------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------------- # Bounds (i.e. x, y, dx, dy): Bounds = Tuple(Int, Int, Int, Int) # Docking drag bar style: DockStyle = Enum("horizontal", "vertical", "tab", "fixed") # ------------------------------------------------------------------------------- # Adds a new DockWindowFeature class to the list of available features: # ------------------------------------------------------------------------------- def add_feature(feature_class): """ Adds a new DockWindowFeature class to the list of available features. """ global features result = feature_class not in features if result: features.append(feature_class) # Mark the feature class as having been installed: if feature_class.state == 0: feature_class.state = 1 return result # ------------------------------------------------------------------------------- # Sets the standard font to use for a specified device context: # ------------------------------------------------------------------------------- def set_standard_font(dc): """ Sets the standard font to use for a specified device context. """ global standard_font if standard_font is None: standard_font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) dc.SetFont(standard_font) return dc # ------------------------------------------------------------------------------- # Clears a window to the standard background color: # ------------------------------------------------------------------------------- def clear_window(window): """ Clears a window to the standard background color. """ bg_color = SystemMetrics().dialog_background_color bg_color = wx.Colour( int(bg_color[0] * 255), int(bg_color[1] * 255), int(bg_color[2] * 255) ) dx, dy = window.GetSize().Get() dc = wx.PaintDC(window) dc.SetBrush(wx.Brush(bg_color, wx.SOLID)) dc.SetPen(wx.TRANSPARENT_PEN) dc.DrawRectangle(0, 0, dx, dy) # ------------------------------------------------------------------------------- # Gets a temporary device context for a specified window to draw in: # ------------------------------------------------------------------------------- def get_dc(window): """ Gets a temporary device context for a specified window to draw in. """ if is_mac: dc = wx.ClientDC(window) x, y = window.GetPosition().Get() dx, dy = window.GetSize().Get() while True: window = window.GetParent() if window is None: break xw, yw = window.GetPosition().Get() dxw, dyw = window.GetSize().Get() dx, dy = min(dx, dxw - x), min(dy, dyw - y) x += xw y += yw dc.SetClippingRegion(0, 0, dx, dy) return (dc, 0, 0) x, y = window.ClientToScreen(0, 0) return (wx.ScreenDC(), x, y) # ------------------------------------------------------------------------------- # 'DockImages' class: # ------------------------------------------------------------------------------- class DockImages(HasPrivateTraits): # --------------------------------------------------------------------------- # Trait definitions: # --------------------------------------------------------------------------- # Image for closing a tab: close_tab = Image(ImageResource("close_tab")) # Image for closing a drag bar: close_drag = Image(ImageResource("close_drag")) # --------------------------------------------------------------------------- # Initalizes the object: # --------------------------------------------------------------------------- def __init__(self, **traits): """ Initializes the object. """ super().__init__(**traits) self._lazy_init_done = False def init(self): """ Initializes the parts of the object that depend on the toolkit selection. """ # See if it has already been done. if self._lazy_init_done: return self._lazy_init_done = True self._close_tab = self.close_tab.create_image().ConvertToBitmap() self._close_drag = self.close_drag.create_image().ConvertToBitmap() self._splitter_images = [ ImageResource(name).create_image().ConvertToBitmap() for name in [ "sv_left", "sv_middle", "sv_right", "sh_top", "sh_middle", "sh_bottom", ] ] self._tab_scroller_images = [ ImageResource(name).create_image().ConvertToBitmap() for name in ["tab_scroll_l", "tab_scroll_r", "tab_scroll_lr"] ] self._tab_scroller_dx = self._tab_scroller_images[0].GetWidth() self._tab_scroller_dy = self._tab_scroller_images[0].GetHeight() self._feature_images = [ ImageResource(name).create_image().ConvertToBitmap() for name in [ "tab_feature_normal", "tab_feature_changed", "tab_feature_drop", "tab_feature_disabled", "bar_feature_normal", "bar_feature_changed", "bar_feature_drop", "bar_feature_disabled", ] ] self._tab_feature_width = self._feature_images[0].GetWidth() self._tab_feature_height = self._feature_images[0].GetHeight() self._bar_feature_width = self._feature_images[3].GetWidth() self._bar_feature_height = self._feature_images[3].GetHeight() # --------------------------------------------------------------------------- # Returns the splitter image to use for a specified splitter state: # --------------------------------------------------------------------------- def get_splitter_image(self, state): """ Returns the splitter image to use for a specified splitter state. """ return self._splitter_images[state] # --------------------------------------------------------------------------- # Returns the feature image to use for a specified feature state: # --------------------------------------------------------------------------- def get_feature_image(self, state, is_tab=True): """ Returns the feature image to use for a specified feature state. """ if is_tab: return self._feature_images[state] return self._feature_images[state + 3] # Creates a singleton instance of the class: DockImages = DockImages() # ------------------------------------------------------------------------------- # 'DockItem' class: # ------------------------------------------------------------------------------- class DockItem(HasPrivateTraits): # --------------------------------------------------------------------------- # Trait definitions: # --------------------------------------------------------------------------- # The parent of this item: parent = Any() # The control associated with this item (used in subclasses): control = Instance(wx.Control) # The DockWindow that owns this item: owner = Property(observe="parent") # Bounds of the item: bounds = Bounds # The name of this item (used in subclasses): name = Str() # Current width of the item: width = Int(-1) # Current height of the item: height = Int(-1) # Bounds of the item's drag bar or tab: drag_bounds = Bounds # The current tab state: tab_state = Any() # The tab displayable version of the control's UI name: tab_name = Property(observe="name") # Width of the item's tab: tab_width = Property(observe="control, tab_state, tab_name") # The DockWindowTheme for this item's DockWindow: theme = Property # The theme for the current tab state: tab_theme = Property # The current feature mode: feature_mode = Enum( FEATURE_NONE, FEATURE_NORMAL, FEATURE_CHANGED, FEATURE_DROP, FEATURE_VISIBLE, FEATURE_DROP_VISIBLE, FEATURE_DISABLED, FEATURE_PRE_NORMAL, ) # The position where the feature popup should appear: feature_popup_position = Property # The list of features for this item: features = List() # The list of drag data compatible drop features for this item: drop_features = List() # Current active set of features: active_features = Property # --------------------------------------------------------------------------- # Implementation of the 'owner' property: # --------------------------------------------------------------------------- @cached_property def _get_owner(self): if self.parent is None: return None return self.parent.owner # --------------------------------------------------------------------------- # Implementation of the 'tab_name' property: # --------------------------------------------------------------------------- @cached_property def _get_tab_name(self): name = self.name if len(name) > MaxTabLength: name = "%s...%s" % (name[: MaxTabLength - 23], name[-20:]) return name # --------------------------------------------------------------------------- # Implementation of the 'tab_width' property: # --------------------------------------------------------------------------- @cached_property def _get_tab_width(self): if self.control is None: return 0 self._is_tab = True # Calculate the size needed by the theme and margins: theme = self.tab_theme tw = ( theme.image_slice.xleft + theme.image_slice.xright + theme.content.left + theme.content.right ) # Add feature marker width: if self.feature_mode != FEATURE_NONE: tw += DockImages._tab_feature_width + 3 # Add text width: dc = set_standard_font(wx.ClientDC(self.control)) tw += dc.GetTextExtent(self.tab_name)[0] # Add custom image width: image = self.get_image() if image is not None: tw += image.GetWidth() + 3 # Add close button width: if self.closeable: tw += CloseTabSize + 6 # Return the computed width: return tw # --------------------------------------------------------------------------- # Implementation of the 'theme' property: # --------------------------------------------------------------------------- def _get_theme(self): if self.control is None: return dock_window_theme() return self.control.GetParent().owner.theme # --------------------------------------------------------------------------- # Implementation of the 'tab_theme' property: # --------------------------------------------------------------------------- def _get_tab_theme(self): if self.tab_state == TabInactive: return self.theme.tab_inactive if self.tab_state == TabActive: return self.theme.tab_active return self.theme.tab_hover # --------------------------------------------------------------------------- # Implementation of the 'active_features' property: # --------------------------------------------------------------------------- def _get_active_features(self): if len(self.drop_features) > 0: return self.drop_features return self.features # --------------------------------------------------------------------------- # Implementation of the 'feature_popup_position' property: # --------------------------------------------------------------------------- def _get_feature_popup_position(self): x, y, dx, dy = self.drag_bounds return wx.Point(x + 5, y + 3) # --------------------------------------------------------------------------- # Returns whether or not the item is at a specified window position: # --------------------------------------------------------------------------- def is_at(self, x, y, bounds=None): """ Returns whether or not the item is at a specified window position. """ if bounds is None: bounds = self.bounds bx, by, bdx, bdy = bounds return (bx <= x < (bx + bdx)) and (by <= y < (by + bdy)) # --------------------------------------------------------------------------- # Returns whether or not an event is within a specified bounds: # --------------------------------------------------------------------------- def is_in(self, event, x, y, dx, dy): """ Returns whether or not an event is within a specified bounds. """ return (x <= event.GetX() < (x + dx)) and ( y <= event.GetY() < (y + dy) ) # --------------------------------------------------------------------------- # Sets the control's drag bounds: # --------------------------------------------------------------------------- def set_drag_bounds(self, x, y, dx, dy): """ Sets the control's drag bounds. """ bx, by, bdx, bdy = self.bounds if (bx + bdx - x) > 0: self.drag_bounds = (x, y, min(x + dx, bx + bdx) - x, dy) else: self.drag_bounds = (x, y, dx, dy) # --------------------------------------------------------------------------- # Gets the cursor to use when the mouse is over the item: # --------------------------------------------------------------------------- def get_cursor(self, event): """ Gets the cursor to use when the mouse is over the item. """ if self._is_tab and (not self._is_in_close(event)): return wx.CURSOR_ARROW return wx.CURSOR_HAND # --------------------------------------------------------------------------- # Gets the DockInfo object for a specified window position: # --------------------------------------------------------------------------- def dock_info_at(self, x, y, tdx, is_control): """ Gets the DockInfo object for a specified window position. """ if self.is_at(x, y, self.drag_bounds): x, y, dx, dy = self.drag_bounds control = self if self._is_tab: if is_control: kind = DOCK_TABADD tab_bounds = (x, y, dx, dy) else: kind = DOCK_TAB tab_bounds = (x - (tdx // 2), y, tdx, dy) else: if is_control: kind = DOCK_TABADD tab_bounds = (x, y, self.tab_width, dy) else: kind = DOCK_TAB control = None tab_bounds = (x + self.tab_width, y, tdx, dy) return DockInfo( kind=kind, tab_bounds=tab_bounds, region=self.parent, control=control, ) return None # --------------------------------------------------------------------------- # Prepares for drawing into a device context: # --------------------------------------------------------------------------- def begin_draw(self, dc, ox=0, oy=0): """ Prepares for drawing into a device context. """ self._save_clip = dc.GetClippingRect() x, y, dx, dy = self.bounds dc.SetClippingRegion(x + ox, y + oy, dx, dy) # --------------------------------------------------------------------------- # Terminates drawing into a device context: # --------------------------------------------------------------------------- def end_draw(self, dc): """ Terminates drawing into a device context. """ dc.DestroyClippingRegion() if self._save_clip != no_clip: dc.SetClippingRegion(*self._save_clip) self._save_clip = None # --------------------------------------------------------------------------- # Handles the left mouse button being pressed: # --------------------------------------------------------------------------- def mouse_down(self, event): """ Handles the left mouse button being pressed. """ self._xy = (event.GetX(), event.GetY()) self._closing = self._is_in_close(event) self._dragging = False # --------------------------------------------------------------------------- # Handles the left mouse button being released: # --------------------------------------------------------------------------- def mouse_up(self, event): """ Handles the left mouse button being released. """ # Handle the user closing a control: if self._closing: if self._is_in_close(event): self.close() # Handle the completion of a dragging operation: elif self._dragging: window = event.GetEventObject() dock_info, self._dock_info = self._dock_info, None self.mark_bounds(False) control = self # Check to see if the user is attempting to drag an entire notebook # region: if event.AltDown(): control = self.parent # If the parent is not a notebook, then use the parent's parent: if isinstance(control, DockRegion) and ( not control.is_notebook ): control = control.parent # Make sure the target is not contained within the notebook # group we are trying to move: region = dock_info.region while region is not None: if region is control: # If it is, the operation is invalid, abort: return region = region.parent # Check to see if the user is attempting to copy the control: elif event.ControlDown(): owner = window.owner control = owner.handler.dock_control_for( *(owner.handler_args + (window, control)) ) # Complete the docking maneuver: dock_info.dock(control, window) # Handle the user clicking on a notebook tab to select it: elif self._is_tab and self.is_at( event.GetX(), event.GetY(), self.drag_bounds ): self.parent.tab_clicked(self) # --------------------------------------------------------------------------- # Handles the mouse moving while the left mouse button is pressed: # --------------------------------------------------------------------------- def mouse_move(self, event): """ Handles the mouse moving while the left mouse button is pressed. """ # Exit if control is 'fixed' or a 'close' is pending: if self._closing or self.locked or (self.style == "fixed"): return window = event.GetEventObject() # Check to see if we are in 'drag mode' yet: if not self._dragging: x, y = self._xy if (abs(x - event.GetX()) + abs(y - event.GetY())) < 3: return self._dragging = True self._dock_info = no_dock_info self._dock_size = self.tab_width self.mark_bounds(True) # Get the window and DockInfo object associated with the event: cur_dock_info = self._dock_info self._dock_info = dock_info = window.GetSizer().DockInfoAt( event.GetX(), event.GetY(), self._dock_size, event.ShiftDown() ) # If the DockInfo has not changed, then no update is needed: if ( (cur_dock_info.kind == dock_info.kind) and (cur_dock_info.region is dock_info.region) and (cur_dock_info.bounds == dock_info.bounds) and (cur_dock_info.tab_bounds == dock_info.tab_bounds) ): return # Make sure the new DockInfo is legal: region = self.parent if ( (not event.ControlDown()) and (dock_info.region is region) and ( (len(region.contents) <= 1) or (DOCK_TAB <= dock_info.kind <= DOCK_BAR) and (dock_info.control is self) ) ): self._dock_info = no_dock_info window.owner.set_cursor(wx.CURSOR_SIZING) return # Draw the new region: dock_info.draw(window, self._drag_bitmap) # If this is the start of an export (i.e. drag and drop) request: if ( (dock_info.kind == DOCK_EXPORT) and (self.export != "") and (self.dockable is not None) ): # Begin the drag and drop operation: self.mark_bounds(False) window.owner.set_cursor(wx.CURSOR_ARROW) window.owner.release_mouse() try: window._dragging = True if PythonDropSource(window, self).result in ( wx.DragNone, wx.DragCancel, ): window.owner.handler.open_view_for(self) finally: window._dragging = False else: # Update the mouse pointer as required: cursor = wx.CURSOR_SIZING if dock_info.kind == DOCK_BAR: cursor = wx.CURSOR_HAND window.owner.set_cursor(cursor) # --------------------------------------------------------------------------- # Handles the mouse hovering over the item: # --------------------------------------------------------------------------- def hover_enter(self, event): """ Handles the mouse hovering over the item. """ if self._is_tab and (self.tab_state != TabActive): self._redraw_tab(TabHover) # --------------------------------------------------------------------------- # Handles the mouse exiting from hovering over the item: # --------------------------------------------------------------------------- def hover_exit(self, event): """ Handles the mouse exiting from hovering over the item. """ if self._is_tab and (self.tab_state != TabActive): self._redraw_tab(TabInactive) # --------------------------------------------------------------------------- # Marks/Unmarks the bounds of the bounding DockWindow: # --------------------------------------------------------------------------- def mark_bounds(self, begin): """ Marks/Unmarks the bounds of the bounding DockWindow. """ window = self.control.GetParent() if begin: dc, x, y = get_dc(window) dx, dy = window.GetSize().Get() dc2 = wx.MemoryDC() self._drag_bitmap = wx.Bitmap(dx, dy) dc2.SelectObject(self._drag_bitmap) dc2.Blit(0, 0, dx, dy, dc, x, y) try: dc3 = wx.GCDC(dc2) dc3.SetBrush(wx.Brush(wx.Colour(158, 166, 255, 64))) dc3.SetPen(wx.TRANSPARENT_PEN) dc3.DrawRectangle(0, 0, dx, dy) except AttributeError: pass dc.Blit(x, y, dx, dy, dc2, 0, 0) else: self._drag_bitmap = None if is_mac: top_level_window_for(window).Refresh() else: window.Refresh() def get_bg_color(self): """ Gets the background color """ color = SystemMetrics().dialog_background_color return wx.Colour( int(color[0] * 255), int(color[1] * 255), int(color[2] * 255) ) # --------------------------------------------------------------------------- # Fills a specified region with the control's background color: # --------------------------------------------------------------------------- def fill_bg_color(self, dc, x, y, dx, dy): """ Fills a specified region with the control's background color. """ dc.SetPen(wx.TRANSPARENT_PEN) dc.SetBrush(wx.Brush(self.get_bg_color())) dc.DrawRectangle(x, y, dx, dy) # --------------------------------------------------------------------------- # Draws a notebook tab: # --------------------------------------------------------------------------- def draw_tab(self, dc, state): global text_dy """ Draws a notebook tab. """ x0, y0, dx, dy = self.drag_bounds tab_color = self.get_bg_color() if state == TabActive: pass elif state == TabInactive: r, g, b = tab_color.Get()[0:3] tab_color.Set(max(0, r - 20), max(0, g - 20), max(0, b - 20)) else: r, g, b = tab_color.Get()[0:3] tab_color.Set(min(255, r + 20), min(255, g + 20), min(255, b + 20)) self._is_tab = True self.tab_state = state theme = self.tab_theme slice = theme.image_slice bdc = BufferDC(dc, dx, dy) self.fill_bg_color(bdc, 0, 0, dx, dy) if state == TabActive: # fill the tab bg with the desired color brush = wx.Brush(tab_color) bdc.SetBrush(brush) bdc.SetPen(wx.TRANSPARENT_PEN) bdc.DrawRectangle(0, 0, dx, dy) # Draw the left, top, and right side of a rectange around the tab pen = wx.Pen(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNSHADOW)) bdc.SetPen(pen) bdc.DrawLine(0, dy, 0, 0) # up bdc.DrawLine(0, 0, dx, 0) # right bdc.DrawLine(dx - 1, 0, dx - 1, dy) # down pen = wx.Pen( wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNHIGHLIGHT) ) bdc.SetPen(pen) bdc.DrawLine(1, dy, 1, 1) bdc.DrawLine(1, 1, dx - 2, 1) bdc.DrawLine(dx - 2, 1, dx - 2, dy) else: # fill the tab bg with the desired color brush = wx.Brush(tab_color) bdc.SetBrush(brush) bdc.SetPen(wx.TRANSPARENT_PEN) bdc.DrawRectangle(0, 3, dx, dy) # Draw the left, top, and right side of a rectange around the tab pen = wx.Pen(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNSHADOW)) bdc.SetPen(pen) bdc.DrawLine(0, dy, 0, 3) bdc.DrawLine(0, 3, dx - 1, 3) bdc.DrawLine(dx - 1, 3, dx - 1, dy) # Compute the initial drawing position: name = self.tab_name tdx, text_dy = dc.GetTextExtent(name) tc = theme.content ox, oy = theme.label.left, theme.label.top y = oy + ( (dy + slice.xtop + tc.top - slice.xbottom - tc.bottom - text_dy) // 2 ) x = ox + slice.xleft + tc.left mode = self.feature_mode if mode == FEATURE_PRE_NORMAL: mode = self.set_feature_mode(False) # Draw the feature 'trigger' icon (if necessary): if mode != FEATURE_NONE: if mode not in FEATURES_VISIBLE: bdc.DrawBitmap(DockImages.get_feature_image(mode), x, y, True) x += DockImages._tab_feature_width + 3 # Draw the image (if necessary): image = self.get_image() if image is not None: bdc.DrawBitmap(image, x, y, True) x += image.GetWidth() + 3 # Draw the text label: bdc.DrawText(name, x, y + 1) # Draw the close button (if necessary): if self.closeable: bdc.DrawBitmap(DockImages._close_tab, x + tdx + 5, y + 2, True) # Copy the buffer to the display: bdc.copy(x0, y0) # --------------------------------------------------------------------------- # Draws a fixed drag bar: # --------------------------------------------------------------------------- def draw_fixed(self, dc): """ Draws a fixed drag bar. """ pass # --------------------------------------------------------------------------- # Draws a horizontal drag bar: # --------------------------------------------------------------------------- def draw_horizontal(self, dc): """ Draws a horizontal drag bar. """ self._is_tab = False x, y, dx, dy = self.drag_bounds self.fill_bg_color(dc, x, y, dx, dy) pen = wx.Pen(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNHILIGHT)) dc.SetPen(pen) dc.DrawLine(x, y, x + dx, y) dc.DrawLine(x, y + 2, x + dx, y + 2) # --------------------------------------------------------------------------- # Draws a vertical drag bar: # --------------------------------------------------------------------------- def draw_vertical(self, dc): """ Draws a vertical drag bar. """ self._is_tab = False x, y, dx, dy = self.drag_bounds self.fill_bg_color(dc, x, y, dx, dy) pen = wx.Pen(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNHILIGHT)) dc.SetPen(pen) dc.DrawLine(x, y, x, y + dy) dc.DrawLine(x + 2, y, x + 2, y + dy) # --------------------------------------------------------------------------- # Redraws the control's tab: # --------------------------------------------------------------------------- def _redraw_tab(self, state=None): if state is None: state = self.tab_state region = self.parent if region is not None: dc = set_standard_font(wx.ClientDC(self.control.GetParent())) if region.is_notebook: dc.SetClippingRegion(*region._tab_clip_bounds) self.draw_tab(dc, state) dc.DestroyClippingRegion() else: self.draw_tab(dc, state) # --------------------------------------------------------------------------- # Redraws the control's drag bar: # --------------------------------------------------------------------------- def _redraw_bar(self): dc = wx.ClientDC(self.control) getattr(self, "draw_" + self.style)(dc) # --------------------------------------------------------------------------- # Redraws the control's tab or bar: # --------------------------------------------------------------------------- def _redraw_control(self): if self._is_tab: self._redraw_tab() else: self._redraw_bar() # --------------------------------------------------------------------------- # Returns the bounds of the close button (if any): # --------------------------------------------------------------------------- def _close_bounds(self): global text_dy if self.closeable and self._is_tab: x, y, dx, dy = self.drag_bounds theme = self.tab_theme slice = theme.image_slice tc = theme.content ox, oy = theme.label.left, theme.label.top # fixme: x calculation seems to be off by -1... return ( x + dx + ox - slice.xright - tc.right - CloseTabSize, y + oy + ( ( dy + slice.xtop + tc.top - slice.xbottom - tc.bottom - text_dy ) // 2 ) + 3, CloseTabSize, CloseTabSize, ) return (0, 0, 0, 0) # --------------------------------------------------------------------------- # Returns whether a specified window position is over the close button: # --------------------------------------------------------------------------- def _is_in_close(self, event): return self.is_in(event, *self._close_bounds()) # --------------------------------------------------------------------------- # Sets/Returns the 'normal' feature mode for the control based on the # number of currently active features: # --------------------------------------------------------------------------- def set_feature_mode(self, changed=True): if (not changed) or (self.feature_mode != FEATURE_PRE_NORMAL): mode = FEATURE_DROP features = self.drop_features if len(features) == 0: mode = FEATURE_NORMAL features = self.features for feature in features: if feature.bitmap is not None: if changed: self.feature_mode = FEATURE_CHANGED else: self.feature_mode = mode break else: self.feature_mode = FEATURE_DISABLED return self.feature_mode # --------------------------------------------------------------------------- # Returns whether or not a specified window position is over the feature # 'trigger' icon, and if so, triggers display of the feature icons: # --------------------------------------------------------------------------- def feature_activate(self, event, drag_object=Undefined): global text_dy if (self.feature_mode in NO_FEATURE_ICON) or (not self._is_tab): return False # In 'drag' mode, we may get the same coordinate over and over again. # We don't want to restart the timer, so exit now: exy = (event.GetX(), event.GetY()) if self._feature_popup_xy == exy: return True x, y, dx, dy = self.drag_bounds idx = DockImages._tab_feature_width idy = DockImages._tab_feature_height theme = self.tab_theme slice = theme.image_slice tc = theme.content ox, oy = theme.label.left, theme.label.top y += oy + ( (dy + slice.xtop + tc.top - slice.xbottom - tc.bottom - text_dy) // 2 ) x += ox + slice.xleft + tc.left result = self.is_in(event, x, y, idx, idy) # If the pointer is over the feature 'trigger' icon, save the event for # the popup processing: if result: # If this is part of a drag operation, prepare for drag mode: if drag_object is not Undefined: self.pre_drag(drag_object, FEATURE_EXTERNAL_DRAG) # Schedule the popup for later: self._feature_popup_xy = exy do_after(100, self._feature_popup) return result # --------------------------------------------------------------------------- # Resets any pending feature popup: # --------------------------------------------------------------------------- def reset_feature_popup(self): self._feature_popup_xy = None # --------------------------------------------------------------------------- # Pops up the current features if a feature popup is still pending: # --------------------------------------------------------------------------- def _feature_popup(self): if self._feature_popup_xy is not None: # Set the new feature mode: if self.feature_mode == FEATURE_DROP: self.feature_mode = FEATURE_DROP_VISIBLE else: self.feature_mode = FEATURE_VISIBLE self.owner.feature_bar_popup(self) self._feature_popup_xy = None else: self.post_drag(FEATURE_EXTERNAL_DRAG) # --------------------------------------------------------------------------- # Finishes the processing of a feature popup: # --------------------------------------------------------------------------- def feature_bar_closed(self): if self.feature_mode == FEATURE_DROP_VISIBLE: self.feature_mode = FEATURE_DROP else: self.feature_mode = FEATURE_NORMAL do_later(self._redraw_control) # --------------------------------------------------------------------------- # Handles all pre-processing before a feature is dragged: # --------------------------------------------------------------------------- def pre_drag_all(self, object): """ Prepare all DockControls in the associated DockWindow for being dragged over. """ for control in self.dock_controls: control.pre_drag(object) self.pre_drag(object) def pre_drag(self, object, tag=0): """ Prepare this DockControl for being dragged over. """ if ( self.visible and (self.feature_mode != FEATURE_NONE) and (self._feature_mode is None) ): if isinstance(object, IFeatureTool): if object.feature_can_drop_on( self.object ) or object.feature_can_drop_on_dock_control(self): from .feature_tool import FeatureTool self.drop_features = [FeatureTool(dock_control=self)] else: self.drop_features = [ f for f in self.features if f.can_drop(object) and (f.bitmap is not None) ] self._feature_mode = self.feature_mode + tag if len(self.drop_features) > 0: self.feature_mode = FEATURE_DROP else: self.feature_mode = FEATURE_DISABLED self._redraw_control() # --------------------------------------------------------------------------- # Handles all post-processing after a feature has been dragged: # --------------------------------------------------------------------------- def post_drag_all(self): """ Restore all DockControls in the associated DockWindow after a drag operation is completed. """ for control in self.dock_controls: control.post_drag() self.post_drag() def post_drag(self, tag=0): """ Restore this DockControl after a drag operation is completed. """ if ( (self._feature_mode is None) or (tag == 0) or ((self._feature_mode & tag) != 0) ): self.drop_features = [] if self.feature_mode != FEATURE_NONE: if self._feature_mode is not None: self.feature_mode = self._feature_mode & (~tag) self._feature_mode = None else: self.set_feature_mode(False) self._redraw_control() # ------------------------------------------------------------------------------- # 'DockSplitter' class: # ------------------------------------------------------------------------------- class DockSplitter(DockItem): # --------------------------------------------------------------------------- # Trait definitions: # --------------------------------------------------------------------------- # Style of the splitter bar: style = Enum("horizontal", "vertical") # Index of the splitter within its parent: index = Int() # Current state of the splitter (i.e. its position relative to the things # it splits): state = Property # --------------------------------------------------------------------------- # Override the definition of the inherited 'theme' property: # --------------------------------------------------------------------------- def _get_theme(self): return self.parent.control.GetParent().owner.theme # --------------------------------------------------------------------------- # Draws the contents of the splitter: # --------------------------------------------------------------------------- def draw(self, dc): """ Draws the contents of the splitter. """ if (self._live_drag is False) and (self._first_bounds is not None): x, y, dx, dy = self._first_bounds else: x, y, dx, dy = self.bounds image = DockImages.get_splitter_image(self.state) idx, idy = image.GetWidth(), image.GetHeight() self.fill_bg_color(dc, x, y, dx, dy) if self.style == "horizontal": # Draw a line the same color as the system button shadow, which # should be a darkish color in the users color scheme pen = wx.Pen(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNSHADOW)) dc.SetPen(pen) dc.DrawLine(x + idx + 1, y + dy / 2, x + dx - 2, y + dy / 2) iy = y + 2 ix = x # sets the hittable area for changing the cursor to be the height of # the image dx = idx else: # Draw a line the same color as the system button shadow, which # should be a darkish color in the users color scheme pen = wx.Pen(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNSHADOW)) dc.SetPen(pen) dc.DrawLine(x + dx / 2, y + idy + 1, x + dx / 2, y + dy - 2) iy = y ix = x + 2 # sets the hittable area for changing the cursor to be the width of # the image dy = idy dc.DrawBitmap(image, ix, iy, True) self._hot_spot = (x, y, dx, dy) # --------------------------------------------------------------------------- # Gets the cursor to use when the mouse is over the splitter bar: # --------------------------------------------------------------------------- def get_cursor(self, event): """ Gets the cursor to use when the mouse is over the splitter bar. """ if (self._hot_spot is None) or self.is_in(event, *self._hot_spot): return wx.CURSOR_ARROW if self.style == "horizontal": return wx.CURSOR_SIZENS return wx.CURSOR_SIZEWE # --------------------------------------------------------------------------- # Returns a copy of the splitter 'structure', minus the actual content: # --------------------------------------------------------------------------- def get_structure(self): """ Returns a copy of the splitter 'structure', minus the actual content. """ return self.clone_traits(["_last_bounds"]) # --------------------------------------------------------------------------- # Handles the left mouse button being pressed: # --------------------------------------------------------------------------- def mouse_down(self, event): """ Handles the left mouse button being pressed. """ self._live_drag = event.ControlDown() self._click_pending = (self._hot_spot is not None) and self.is_in( event, *self._hot_spot ) if not self._click_pending: self._xy = (event.GetX(), event.GetY()) self._max_bounds = self.parent.get_splitter_bounds(self) self._first_bounds = self.bounds if not self._live_drag: self._draw_bounds(event, self.bounds) # --------------------------------------------------------------------------- # Handles the left mouse button being released: # --------------------------------------------------------------------------- def mouse_up(self, event): """ Handles the left mouse button being released. """ if self._click_pending: hx, hy, hdx, hdy = self._hot_spot if not self.is_in(event, hx, hy, hdx, hdy): return if self.style == "horizontal": if event.GetX() < (hx + (hdx / 2)): self.collapse(True) else: self.collapse(False) else: if event.GetY() < (hy + (hdy / 2)): self.collapse(True) else: self.collapse(False) else: self._last_bounds, self._first_bounds = self._first_bounds, None if not self._live_drag: self._draw_bounds(event) self.parent.update_splitter(self, event.GetEventObject()) # --------------------------------------------------------------------------- # Handles the mouse moving while the left mouse button is pressed: # --------------------------------------------------------------------------- def mouse_move(self, event): """ Handles the mouse moving while the left mouse button is pressed. """ if not self._click_pending: if self._first_bounds is not None: x, y, dx, dy = self._first_bounds mx, my, mdx, mdy = self._max_bounds if self.style == "horizontal": y = y + event.GetY() - self._xy[1] y = min(max(y, my), my + mdy - dy) else: x = x + event.GetX() - self._xy[0] x = min(max(x, mx), mx + mdx - dx) bounds = (x, y, dx, dy) if bounds != self.bounds: self.bounds = bounds if self._live_drag: self.parent.update_splitter(self, event.GetEventObject()) else: self._draw_bounds(event, bounds) # --------------------------------------------------------------------------- # Collapse/expands a splitter # --------------------------------------------------------------------------- def collapse(self, forward): """ Move the splitter has far as possible in one direction. 'forward' is a boolean: True=right/down, False=left/up. If the splitter is already collapsed, restores it to its previous position. """ is_horizontal = self.style == "horizontal" x, y, dx, dy = self.bounds if self._last_bounds is not None: if is_horizontal: y = self._last_bounds[1] else: x = self._last_bounds[0] state = self.state contents = self.parent.visible_contents ix1, iy1, idx1, idy1 = contents[self.index].bounds ix2, iy2, idx2, idy2 = contents[self.index + 1].bounds if is_horizontal: if state != SPLIT_HMIDDLE: if ( (y == self.bounds[1]) or (y < iy1) or ((y + dy) > (iy2 + idy2)) ): y = (iy1 + iy2 + idy2 - dy) // 2 else: self._last_bounds = self.bounds if forward: y = iy1 else: y = iy2 + idy2 - dy elif state != SPLIT_VMIDDLE: if (x == self.bounds[0]) or (x < ix1) or ((x + dx) > (ix2 + idx2)): x = (ix1 + ix2 + idx2 - dx) // 2 else: self._last_bounds = self.bounds if forward: x = ix2 + idx2 - dx else: x = ix1 self.bounds = (x, y, dx, dy) # --------------------------------------------------------------------------- # Handles the mouse hovering over the item: # --------------------------------------------------------------------------- def hover_enter(self, event): """ Handles the mouse hovering over the item. """ pass # --------------------------------------------------------------------------- # Handles the mouse exiting from hovering over the item: # --------------------------------------------------------------------------- def hover_exit(self, event): """ Handles the mouse exiting from hovering over the item. """ pass # --------------------------------------------------------------------------- # Draws the splitter bar in a new position while it is being dragged: # --------------------------------------------------------------------------- def _draw_bounds(self, event, bounds=None): """ Draws the splitter bar in a new position while it is being dragged. """ # Set up the drawing environment: window = event.GetEventObject() dc, x0, y0 = get_dc(window) dc.SetLogicalFunction(wx.XOR) dc.SetPen(wx.TRANSPARENT_PEN) dc.SetBrush(wx.Brush(wx.Colour(*DragColor), wx.SOLID)) is_horizontal = self.style == "horizontal" nx = ox = None # Draw the new bounds (if any): if bounds is not None: ax = ay = adx = ady = 0 nx, ny, ndx, ndy = bounds if is_horizontal: ady = ndy - 6 ay = ady // 2 else: adx = ndx - 6 ax = adx // 2 nx += ax ny += ay ndx -= adx ndy -= ady if self._bounds is not None: ax = ay = adx = ady = 0 ox, oy, odx, ody = self._bounds if is_horizontal: ady = ody - 6 ay = ady // 2 else: adx = odx - 6 ax = adx // 2 ox += ax oy += ay odx -= adx ody -= ady if nx is not None: tx, ty, tdx, tdy = nx, ny, ndx, ndy if ox is not None: if is_horizontal: yoy = oy - ty if 0 <= yoy < tdy: tdy = yoy elif -ody < yoy <= 0: ty = oy + ody tdy = tdy - ody - yoy else: xox = ox - tx if 0 <= xox < tdx: tdx = xox elif -odx < xox <= 0: tx = ox + odx tdx = tdx - odx - xox dc.DrawRectangle(tx + x0, ty + y0, tdx, tdy) # Erase the old bounds (if any): if ox is not None: if nx is not None: if is_horizontal: yoy = ny - oy if 0 <= yoy < ody: ody = yoy elif -ndy < yoy <= 0: oy = ny + ndy ody = ody - ndy - yoy else: xox = nx - ox if 0 <= xox < odx: odx = xox elif -ndx < xox <= 0: ox = nx + ndx odx = odx - ndx - xox dc.DrawRectangle(ox + x0, oy + y0, odx, ody) if is_mac: window.Refresh(rect=wx.Rect(ox + x0, oy + y0, odx, ody)) # Save the new bounds for the next call: self._bounds = bounds # --------------------------------------------------------------------------- # Implementation of the 'state' property: # --------------------------------------------------------------------------- def _get_state(self): contents = self.parent.contents x, y, dx, dy = self.bounds ix1, iy1, idx1, idy1 = contents[self.index].bounds ix2, iy2, idx2, idy2 = contents[self.index + 1].bounds if self.style == "horizontal": if y == iy1: return SPLIT_HTOP if (y + dy) == (iy2 + idy2): return SPLIT_HBOTTOM return SPLIT_HMIDDLE else: if x == ix1: return SPLIT_VLEFT if (x + dx) == (ix2 + idx2): return SPLIT_VRIGHT return SPLIT_VMIDDLE # ------------------------------------------------------------------------------- # 'DockControl' class: # ------------------------------------------------------------------------------- class DockControl(DockItem): # --------------------------------------------------------------------------- # Trait definitions: # --------------------------------------------------------------------------- # The control this object describes: control = Instance(wx.Window, allow_none=True) # The number of global DockWindowFeature's that were available the last # the time the feature set was checked: num_features = Int() # A feature associated with the DockControl has been changed: feature_changed = Event() # The image to display for this control: image = Image() # The UI name of this control: name = Str() # Has the user set the name of the control? user_name = Bool(False) # The object (if any) associated with this control: object = Property # The id of this control: id = Str() # Style of drag bar/tab: style = DockStyle # Has the user set the style for this control: user_style = Bool(False) # Category of control when it is dragged out of the DockWindow: export = Str() # Is the control visible? visible = Bool(True) # Is the control's drag bar locked? locked = Bool(False) # Can the control be resized? resizable = Bool(True) # Can the control be closed? closeable = Bool(False) # Function to call when a DockControl is requesting to be closed: on_close = Callable # (Optional) object that allows the control to be docked with a different # DockWindow: dockable = Instance(IDockable, allow_none=True) # List of all other DockControl's in the same DockWindow: dock_controls = Property # Event fired when the control's notebook tab is activated by the user: activated = Event() # --------------------------------------------------------------------------- # Calculates the minimum size of the control: # --------------------------------------------------------------------------- def calc_min(self, use_size=False): """ Calculates the minimum size of the control. """ self.check_features() dx, dy = self.width, self.height if self.control is not None: size = self.control.GetEffectiveMinSize() dx = size.GetWidth() dy = size.GetHeight() if self.width < 0: self.width, self.height = dx, dy if use_size and (self.width >= 0): return (self.width, self.height) return (dx, dy) # --------------------------------------------------------------------------- # Layout the contents of the control based on the specified bounds: # --------------------------------------------------------------------------- def recalc_sizes(self, x, y, dx, dy): """ Layout the contents of the region based on the specified bounds. """ self.width = dx = max(0, dx) self.height = dy = max(0, dy) self.bounds = (x, y, dx, dy) # Note: All we really want to do is the 'SetSize' call, but the # other code is needed for Linux/GTK which will not correctly process # the SetSize call if the min size is larger than the specified # size. So we temporarily set its min size to (0,0), do the # SetSize, then restore the original min size. The restore is # necessary so that DockWindow itself will correctly draw the 'drag' # box when performing a docking maneuver... control = self.control min_size = control.GetMinSize() control.SetMinSize(wx.Size(0, 0)) control.SetSize(x, y, dx, dy) control.SetMinSize(min_size) # --------------------------------------------------------------------------- # Checks to make sure that all applicable DockWindowFeatures have been # applied: # --------------------------------------------------------------------------- def check_features(self): """ Checks to make sure that all applicable DockWindowFeatures have been applied. """ global features mode = self.feature_mode n = len(features) if ( (self.num_features < n) and (self.control is not None) and isinstance(self.control.GetParent().GetSizer(), DockSizer) ): for i in range(self.num_features, n): feature_class = features[i] feature = feature_class.new_feature_for(self) if feature is not None: if not isinstance(feature, SequenceType): feature = [feature] self.features.extend(list(feature)) if mode == FEATURE_NONE: self.feature_mode = FEATURE_PRE_NORMAL if feature_class.state != 1: for item in feature: item.disable() else: self._tab_width = None if mode in NORMAL_FEATURES: self.set_feature_mode() self.num_features = n # --------------------------------------------------------------------------- # Sets the visibility of the control: # --------------------------------------------------------------------------- def set_visibility(self, visible): """ Sets the visibility of the control. """ if self.control is not None: self.control.Show(visible) # --------------------------------------------------------------------------- # Returns all DockControl objects contained in the control: # --------------------------------------------------------------------------- def get_controls(self, visible_only=True): """ Returns all DockControl objects contained in the control. """ if visible_only and (not self.visible): return [] return [self] # --------------------------------------------------------------------------- # Gets the image (if any) associated with the control: # --------------------------------------------------------------------------- def get_image(self): """ Gets the image (if any) associated with the control. """ if self._image is None: if self.image is not None: self._image = self.image.create_image().ConvertToBitmap() return self._image # --------------------------------------------------------------------------- # Hides or shows the control: # --------------------------------------------------------------------------- def show(self, visible=True, layout=True): """ Hides or shows the control. """ if visible != self.visible: self.visible = visible self._layout(layout) # --------------------------------------------------------------------------- # Activates a control (i.e. makes it the active page within its containing # notebook): # --------------------------------------------------------------------------- def activate(self, layout=True): """ Activates a control (i.e. makes it the active page within its containing notebook). """ if self.parent is not None: self.parent.activate(self, layout) # --------------------------------------------------------------------------- # Closes the control: # --------------------------------------------------------------------------- def close(self, layout=True, force=False): """ Closes the control. """ control = self.control if control is not None: window = control.GetParent() if self.on_close is not None: # Ask the handler if it is OK to close the control: if self.on_close(self, force) is False: # If not OK to close it, we're done: return elif self.dockable is not None: # Ask the IDockable handler if it is OK to close the control: if self.dockable.dockable_close(self, force) is False: # If not OK to close it, we're done: return else: # No close handler, just destroy the widget ourselves: control.Destroy() # Reset all features: self.reset_features() # Remove the DockControl from the sizer: self.parent.remove(self) # Mark the DockControl as closed (i.e. has no associated widget or # parent): self.control = self.parent = None # If a screen update is requested, lay everything out again now: if layout: window.Layout() window.Refresh() # --------------------------------------------------------------------------- # Returns the object at a specified window position: # --------------------------------------------------------------------------- def object_at(self, x, y): """ Returns the object at a specified window position. """ return None # --------------------------------------------------------------------------- # Returns a copy of the control 'structure', minus the actual content: # --------------------------------------------------------------------------- def get_structure(self): """ Returns a copy of the control 'structure', minus the actual content. """ return self.clone_traits( [ "id", "name", "user_name", "style", "user_style", "visible", "locked", "closeable", "resizable", "width", "height", ] ) # --------------------------------------------------------------------------- # Toggles the 'lock' status of the control: # --------------------------------------------------------------------------- def toggle_lock(self): """ Toggles the 'lock' status of the control. """ self.locked = not self.locked # --------------------------------------------------------------------------- # Prints the contents of the control: # --------------------------------------------------------------------------- def dump(self, indent): """ Prints the contents of the control. """ print( ( "%sControl( %08X, name = %s, id = %s,\n%s" "style = %s, locked = %s,\n%s" "closeable = %s, resizable = %s, visible = %s\n%s" "width = %d, height = %d )" % ( " " * indent, id(self), self.name, self.id, " " * (indent + 9), self.style, self.locked, " " * (indent + 9), self.closeable, self.resizable, self.visible, " " * (indent + 9), self.width, self.height, ) ) ) # --------------------------------------------------------------------------- # Draws the contents of the control: # --------------------------------------------------------------------------- def draw(self, dc): """ Draws the contents of the control. """ pass # --------------------------------------------------------------------------- # Sets a new name for the control: # --------------------------------------------------------------------------- def set_name(self, name, layout=True): """ Sets a new name for the control. """ if name != self.name: self.name = name self._layout(layout) # --------------------------------------------------------------------------- # Resets the state of the tab: # --------------------------------------------------------------------------- def reset_tab(self): """ Resets the state of the tab. """ self.reset_features() self._layout() # --------------------------------------------------------------------------- # Resets all currently defined features: # --------------------------------------------------------------------------- def reset_features(self): """ Resets all currently defined features. """ for feature in self.features: feature.dispose() self.features = [] self.num_features = 0 # --------------------------------------------------------------------------- # Forces the containing DockWindow to be laid out: # --------------------------------------------------------------------------- def _layout(self, layout=True): """ Forces the containing DockWindow to be laid out. """ if layout and (self.control is not None): do_later(self.control.GetParent().owner.update_layout) # --------------------------------------------------------------------------- # Handles the 'activated' event being fired: # --------------------------------------------------------------------------- @observe('activated') def _activate_dockable_tab(self, event): """ Notifies the active dockable that the control's tab is being activated. """ if self.dockable is not None: self.dockable.dockable_tab_activated(self, True) # --------------------------------------------------------------------------- # Handles the 'feature_changed' trait being changed: # --------------------------------------------------------------------------- @observe("feature_changed") def _feature_changed_updated(self, event): """ Handles the 'feature_changed' trait being changed """ self.set_feature_mode() # --------------------------------------------------------------------------- # Handles the 'control' trait being changed: # --------------------------------------------------------------------------- @observe("control") def _control_updated(self, event): """ Handles the 'control' trait being changed. """ old, new = event.old, event.new self._tab_width = None if old is not None: old._dock_control = None if new is not None: new._dock_control = self self.reset_tab() # --------------------------------------------------------------------------- # Handles the 'name' trait being changed: # --------------------------------------------------------------------------- @observe("name") def _name_updated(self, event): """ Handles the 'name' trait being changed. """ self._tab_width = self._tab_name = None # --------------------------------------------------------------------------- # Handles the 'style' trait being changed: # --------------------------------------------------------------------------- @observe("style") def _style_updated(self, event): """ Handles the 'style' trait being changed. """ if self.parent is not None: self.parent._is_notebook = None # --------------------------------------------------------------------------- # Handles the 'image' trait being changed: # --------------------------------------------------------------------------- @observe("image") def _image_updated(self, event): """ Handles the 'image' trait being changed. """ self._image = None # --------------------------------------------------------------------------- # Handles the 'visible' trait being changed: # --------------------------------------------------------------------------- @observe("visible") def _visible_updated(self, event): """ Handles the 'visible' trait being changed. """ if self.parent is not None: self.parent.show_hide(self) # --------------------------------------------------------------------------- # Handles the 'dockable' trait being changed: # --------------------------------------------------------------------------- @observe("dockable") def _dockable_updated(self, event): """ Handles the 'dockable' trait being changed. """ dockable = event.new if dockable is not None: dockable.dockable_bind(self) # --------------------------------------------------------------------------- # Implementation of the 'object' property: # --------------------------------------------------------------------------- def _get_object(self): return getattr(self.control, "_object", None) # --------------------------------------------------------------------------- # Implementation of the DockControl's property: # --------------------------------------------------------------------------- def _get_dock_controls(self): # Get all of the DockControls in the parent DockSizer: controls = ( self.control.GetParent() .GetSizer() .GetContents() .get_controls(False) ) # Remove ourself from the list: try: controls.remove(self) except: pass return controls # ------------------------------------------------------------------------------- # 'DockGroup' class: # ------------------------------------------------------------------------------- class DockGroup(DockItem): # --------------------------------------------------------------------------- # Trait definitions: # --------------------------------------------------------------------------- # The contents of the group: contents = List() # The UI name of this group: name = Property # Style of drag bar/tab: style = Property # Are the contents of the group resizable? resizable = Property # Category of control when it is dragged out of the DockWindow: export = Constant("") # Is the group visible? visible = Property # Content items which are visible: visible_contents = Property # Can the control be closed? closeable = Property # The control associated with this group: control = Property # Is the group locked? locked = Property # Has the initial layout been performed? initialized = Bool(False) # --------------------------------------------------------------------------- # Implementation of the 'name' property: # --------------------------------------------------------------------------- def _get_name(self): controls = self.get_controls() n = len(controls) if n == 0: return "" if n == 1: return controls[0].name return "%s [%d]" % (controls[0].name, n) # --------------------------------------------------------------------------- # Implementation of the 'visible' property: # --------------------------------------------------------------------------- def _get_visible(self): for item in self.contents: if item.visible: return True return False # --------------------------------------------------------------------------- # Implementation of the 'visible_contents' property: # --------------------------------------------------------------------------- def _get_visible_contents(self): return [item for item in self.contents if item.visible] # --------------------------------------------------------------------------- # Implementation of the 'closeable' property: # --------------------------------------------------------------------------- def _get_closeable(self): for item in self.contents: if not item.closeable: return False return True # --------------------------------------------------------------------------- # Implementation of the 'style' property: # --------------------------------------------------------------------------- def _get_style(self): # Make sure there is at least one item in the group: if len(self.contents) > 0: # Return the first item's style: return self.contents[0].style # Otherwise, return a default style for an empty group: return "horizontal" # --------------------------------------------------------------------------- # Implementation of the 'resizable' property: # --------------------------------------------------------------------------- def _get_resizable(self): if self._resizable is None: self._resizable = False for control in self.get_controls(): if control.resizable: self._resizable = True break return self._resizable # --------------------------------------------------------------------------- # Implementation of the 'control' property: # --------------------------------------------------------------------------- def _get_control(self): if len(self.contents) == 0: return None return self.contents[0].control # --------------------------------------------------------------------------- # Implementation of the 'locked' property: # --------------------------------------------------------------------------- def _get_locked(self): return self.contents[0].locked # --------------------------------------------------------------------------- # Handles 'initialized' being changed: # --------------------------------------------------------------------------- @observe("initialized") def _initialized_updated(self, event): """ Handles 'initialized' being changed. """ for item in self.contents: if isinstance(item, DockGroup): item.initialized = self.initialized # --------------------------------------------------------------------------- # Hides or shows the contents of the group: # --------------------------------------------------------------------------- def show(self, visible=True, layout=True): """ Hides or shows the contents of the group. """ for item in self.contents: item.show(visible, False) if layout: window = self.control.GetParent() window.Layout() window.Refresh() # --------------------------------------------------------------------------- # Replaces a specified DockControl by another: # --------------------------------------------------------------------------- def replace_control(self, old, new): """ Replaces a specified DockControl by another. """ for i, item in enumerate(self.contents): if isinstance(item, DockControl): if item is old: self.contents[i] = new new.parent = self return True elif item.replace_control(old, new): return True return False # --------------------------------------------------------------------------- # Returns all DockControl objects contained in the group: # --------------------------------------------------------------------------- def get_controls(self, visible_only=True): """ Returns all DockControl objects contained in the group. """ if visible_only: contents = self.visible_contents else: contents = self.contents result = [] for item in contents: result.extend(item.get_controls(visible_only)) return result # --------------------------------------------------------------------------- # Gets the image (if any) associated with the group: # --------------------------------------------------------------------------- def get_image(self): """ Gets the image (if any) associated with the group. """ if len(self.contents) == 0: return None return self.contents[0].get_image() # --------------------------------------------------------------------------- # Gets the cursor to use when the mouse is over the item: # --------------------------------------------------------------------------- def get_cursor(self, event): """ Gets the cursor to use when the mouse is over the item. """ return wx.CURSOR_ARROW # --------------------------------------------------------------------------- # Toggles the 'lock' status of every control in the group: # --------------------------------------------------------------------------- def toggle_lock(self): """ Toggles the 'lock' status of every control in the group. """ for item in self.contents: item.toggle_lock() # --------------------------------------------------------------------------- # Closes the group: # --------------------------------------------------------------------------- def close(self, layout=True, force=False): """ Closes the control. """ window = self.control.control.GetParent() for item in self.contents[:]: item.close(False, force=force) if layout: window.Layout() window.Refresh() # ------------------------------------------------------------------------------- # 'DockRegion' class: # ------------------------------------------------------------------------------- class DockRegion(DockGroup): # --------------------------------------------------------------------------- # Trait definitions: # --------------------------------------------------------------------------- # Index of the currently active 'contents' DockControl: active = Int() # Is the region drawn as a notebook or not: is_notebook = Property # Index of the tab scroll image to use (-1 = No tab scroll): tab_scroll_index = Int(-1) # The index of the current leftmost visible tab: left_tab = Int() # The current maximum value for 'left_tab': max_tab = Int() # Contents have been modified property: modified = Property # --------------------------------------------------------------------------- # Calculates the minimum size of the region: # --------------------------------------------------------------------------- def calc_min(self, use_size=False): """ Calculates the minimum size of the region. """ tab_dx = tdx = tdy = 0 contents = self.visible_contents theme = self.theme if self.is_notebook: for item in contents: dx, dy = item.calc_min(use_size) tdx = max(tdx, dx) tdy = max(tdy, dy) tab_dx += item.tab_width tis = theme.tab.image_slice tc = theme.tab.content tdx = max(tdx, tab_dx) + ( tis.xleft + tis.xright + tc.left + tc.right ) tdy += ( theme.tab_active.image_slice.dy + tis.xtop + tis.xbottom + tc.top + tc.bottom ) elif len(contents) > 0: item = contents[0] tdx, tdy = item.calc_min(use_size) if not item.locked: if item.style == "horizontal": tdy += theme.horizontal_drag.image_slice.dy elif item.style == "vertical": tdx += theme.vertical_drag.image_slice.dx if self.width < 0: self.width = tdx self.height = tdy return (tdx, tdy) # --------------------------------------------------------------------------- # Layout the contents of the region based on the specified bounds: # --------------------------------------------------------------------------- def recalc_sizes(self, x, y, dx, dy): """ Layout the contents of the region based on the specified bounds. """ self.width = dx = max(0, dx) self.height = dy = max(0, dy) self.bounds = (x, y, dx, dy) theme = self.theme contents = self.visible_contents if self.is_notebook: tis = theme.tab.image_slice tc = theme.tab.content th = theme.tab_active.image_slice.dy # Layout the region out as a notebook: x += tis.xleft + tc.left tx0 = tx = x + theme.tab.label.left dx -= tis.xleft + tis.xright + tc.left + tc.right ady = dy - th dy = ady - tis.xtop - tis.xbottom - tc.top - tc.bottom iy = y + tis.xtop + tc.top if theme.tabs_at_top: iy += th else: y += ady for item in contents: item.recalc_sizes(x, iy, dx, dy) tdx = item.tab_width item.set_drag_bounds(tx, y, tdx, th) tx += tdx # Calculate the default tab clipping bounds: cdx = dx + tc.left + tc.right self._tab_clip_bounds = (tx0, y, cdx, th) # Do we need to enable tab scrolling? xr = tx0 + cdx if tx > xr: # Scrolling needed, calculate maximum tab index for scrolling: self.max_tab = 1 n = len(contents) - 1 xr -= DockImages._tab_scroller_dx for i in range(n, -1, -1): xr -= contents[i].tab_width if xr < tx0: self.max_tab = min(i + 1, n) break # Set the new leftmost tab index: self.left_tab = min(self.left_tab, self.max_tab) # Determine which tab scroll image to use: self.tab_scroll_index = ( (self.left_tab < self.max_tab) + (2 * (self.left_tab > 0)) ) - 1 # Now adjust each tab's bounds accordingly: if self.left_tab > 0: adx = contents[self.left_tab].drag_bounds[0] - tx0 for item in contents: dbx, dby, dbdx, dbdy = item.drag_bounds item.set_drag_bounds( dbx - adx, dby, item.tab_width, dbdy ) # Exclude the scroll buttons from the tab clipping region: self._tab_clip_bounds = ( tx0, y, cdx - DockImages._tab_scroller_dx, th, ) else: self.tab_scroll_index = -1 self.left_tab = 0 else: # Lay the region out as a drag bar: item = contents[0] drag_bounds = (0, 0, 0, 0) if not item.locked: if item.style == "horizontal": db_dy = theme.horizontal_drag.image_slice.dy drag_bounds = (x, y, dx, db_dy) y += db_dy dy -= db_dy elif item.style == "vertical": db_dx = theme.vertical_drag.image_slice.dx drag_bounds = (x, y, db_dx, dy) x += db_dx dx -= db_dx item.recalc_sizes(x, y, dx, dy) item.set_drag_bounds(*drag_bounds) # Make sure all of the contained controls have the right visiblity: self._set_visibility() # --------------------------------------------------------------------------- # Adds a new control before or after a specified control: # --------------------------------------------------------------------------- def add(self, control, before=None, after=None, activate=True): """ Adds a new control before a specified control. """ contents = self.contents if control.parent is self: contents.remove(control) if before is None: if after is None: i = len(contents) else: i = contents.index(after) + 1 else: i = contents.index(before) contents.insert(i, control) if activate: self.active = i # --------------------------------------------------------------------------- # Removes a specified item: # --------------------------------------------------------------------------- def remove(self, item): """ Removes a specified item. """ contents = self.contents i = contents.index(item) if isinstance(item, DockGroup) and (len(item.contents) == 1): item = item.contents[0] if isinstance(item, DockRegion): contents[i:i + 1] = item.contents[:] else: contents[i] = item else: del contents[i] # Change the active selection only if 'item' is in closing mode, # or was dragged to a new location. # If this entire dock region is being closed, then all contained # dock items will be removed and we do not want to change 'active' # selection. if item._closing or item._dragging: if (self.active > i) or (self.active >= len(contents)): self.active -= 1 # If the active item was removed, then 'active' stays # unchanged, but it reflects the index of the next page in # the dock region. Since _active_changed won't be fired now, # we fire the 'activated' event on the next page. elif i == self.active: control = self.contents[i] if isinstance(control, DockControl): control.activated = True if self.parent is not None: if len(contents) == 0: self.parent.remove(self) elif (len(contents) == 1) and isinstance(self.parent, DockRegion): self.parent.remove(self) # --------------------------------------------------------------------------- # Returns a copy of the region 'structure', minus the actual content: # --------------------------------------------------------------------------- def get_structure(self): """ Returns a copy of the region 'structure', minus the actual content. """ return self.clone_traits(["active", "width", "height"]).trait_set( contents=[item.get_structure() for item in self.contents] ) # --------------------------------------------------------------------------- # Toggles the 'lock' status of every control in the group: # --------------------------------------------------------------------------- def toggle_lock(self): """ Toggles the 'lock' status of every control in the group. """ super().toggle_lock() self._is_notebook = None # --------------------------------------------------------------------------- # Draws the contents of the region: # --------------------------------------------------------------------------- def draw(self, dc): """ Draws the contents of the region. """ if self._visible is not False: self.begin_draw(dc) if self.is_notebook: # fixme: There seems to be a case where 'draw' is called before # 'recalc_sizes' (which defines '_tab_clip_bounds'), so we need # to check to make sure it is defined. If not, it seems safe to # exit immediately, since in all known cases, the bounds are # ( 0, 0, 0, 0 ), so there is nothing to draw anyways. The # question is why 'recalc_sizes' is not being called first. if self._tab_clip_bounds is None: self.end_draw(dc) return self.fill_bg_color(dc, *self.bounds) if self.active >= len(self.contents): # on some platforms, if the active tab was destroyed # the new active tab may not have been set yet self.active = len(self.contents) - 1 self._draw_notebook(dc) active = self.active # Draw the scroll buttons (if necessary): x, y, dx, dy = self._tab_clip_bounds index = self.tab_scroll_index if index >= 0: dc.DrawBitmap( DockImages._tab_scroller_images[index], x + dx, y + 2, True, ) # Draw all the inactive tabs first: dc.SetClippingRegion(x, y, dx, dy) last_inactive = -1 for i, item in enumerate(self.contents): if (i != active) and item.visible: last_inactive = i state = item.tab_state if state not in NotActiveStates: state = TabInactive item.draw_tab(dc, state) # Draw the active tab last: self.contents[active].draw_tab(dc, TabActive) # If the last inactive tab drawn is also the rightmost tab and # the theme has a 'tab right edge' image, draw the image just # to the right of the last tab: if last_inactive > active: if item.tab_state == TabInactive: bitmap = self.theme.tab_inactive_edge_bitmap else: bitmap = self.theme.tab_hover_edge_bitmap if bitmap is not None: x, y, dx, dy = item.drag_bounds dc.DrawBitmap(bitmap, x + dx, y, True) else: item = self.visible_contents[0] if not item.locked: getattr(item, "draw_" + item.style)(dc) self.end_draw(dc) # Draw each of the items contained in the region: for item in self.contents: if item.visible: item.draw(dc) # --------------------------------------------------------------------------- # Returns the object at a specified window position: # --------------------------------------------------------------------------- def object_at(self, x, y): """ Returns the object at a specified window position. """ if (self._visible is not False) and self.is_at(x, y): if self.is_notebook and (self.tab_scroll_index >= 0): cx, cy, cdx, cdy = self._tab_clip_bounds if self.is_at( x, y, ( cx + cdx, cy + 2, DockImages._tab_scroller_dx, DockImages._tab_scroller_dy, ), ): return self for item in self.visible_contents: if item.is_at(x, y, item.drag_bounds): return item object = item.object_at(x, y) if object is not None: return object return None # --------------------------------------------------------------------------- # Gets the DockInfo object for a specified window position: # --------------------------------------------------------------------------- def dock_info_at(self, x, y, tdx, is_control): """ Gets the DockInfo object for a specified window position. """ # Check to see if the point is in our drag bar: info = super().dock_info_at(x, y, tdx, is_control) if info is not None: return info # If we are not visible, or the point is not contained in us, give up: if (self._visible is False) or (not self.is_at(x, y)): return None # Check to see if the point is in the drag bars of any controls: contents = self.visible_contents for item in contents: object = item.dock_info_at(x, y, tdx, is_control) if object is not None: return object # If we are in 'notebook mode' check to see if the point is in the # empty region outside of any tabs: lx, ty, dx, dy = self.bounds if self.is_notebook: item = contents[-1] ix, iy, idx, idy = item.drag_bounds if (x > (ix + idx)) and (iy <= y < (iy + idy)): return DockInfo( kind=DOCK_TAB, tab_bounds=(ix + idx, iy, tdx, idy), region=self, ) # Otherwise, figure out which edge the point is closest to, and # return a DockInfo object describing that edge: left = x - lx right = lx + dx - 1 - x top = y - ty bottom = ty + dy - 1 - y choice = min(left, right, top, bottom) mdx = dx // 3 mdy = dy // 3 if choice == left: return DockInfo( kind=DOCK_LEFT, bounds=(lx, ty, mdx, dy), region=self ) if choice == right: return DockInfo( kind=DOCK_RIGHT, bounds=(lx + dx - mdx, ty, mdx, dy), region=self, ) if choice == top: return DockInfo( kind=DOCK_TOP, bounds=(lx, ty, dx, mdy), region=self ) return DockInfo( kind=DOCK_BOTTOM, bounds=(lx, ty + dy - mdy, dx, mdy), region=self ) # --------------------------------------------------------------------------- # Handles a contained notebook tab being clicked: # --------------------------------------------------------------------------- def tab_clicked(self, control): """ Handles a contained notebook tab being clicked. """ # Find the page that was clicked and mark it as active: i = self.contents.index(control) if i != self.active: self.active = i # Recalculate the tab layout: self.recalc_sizes(*self.bounds) # Force the notebook to be redrawn: control.control.GetParent().RefreshRect(wx.Rect(*self.bounds)) # Fire the 'activated' event on the control: if isinstance(control, DockControl): control.activated = True # --------------------------------------------------------------------------- # Handles the user clicking an active scroll button: # --------------------------------------------------------------------------- def scroll(self, type, left_tab=0): """ Handles the user clicking an active scroll button. """ if type == SCROLL_LEFT: left_tab = min(self.left_tab + 1, self.max_tab) elif type == SCROLL_RIGHT: left_tab = max(self.left_tab - 1, 0) if left_tab != self.left_tab: # Calculate the amount we need to adjust each tab by: contents = self.visible_contents adx = ( contents[left_tab].drag_bounds[0] - contents[self.left_tab].drag_bounds[0] ) # Set the new leftmost tab index: self.left_tab = left_tab # Determine which tab scroll image to use: self.tab_scroll_index = ( (left_tab < self.max_tab) + (2 * (left_tab > 0)) ) - 1 # Now adjust each tab's bounds accordingly: for item in contents: dbx, dby, dbdx, dbdy = item.drag_bounds item.set_drag_bounds(dbx - adx, dby, item.tab_width, dbdy) # Finally, force a redraw of the affected part of the window: x, y, dx, dy = self._tab_clip_bounds item.control.GetParent().RefreshRect( wx.Rect(x, y, dx + DockImages._tab_scroller_dx, dy) ) # --------------------------------------------------------------------------- # Handles the left mouse button being pressed: # --------------------------------------------------------------------------- def mouse_down(self, event): """ Handles the left mouse button being pressed. """ self._scroll = self._get_scroll_button(event) # --------------------------------------------------------------------------- # Handles the left mouse button being released: # --------------------------------------------------------------------------- def mouse_up(self, event): """ Handles the left mouse button being released. """ if (self._scroll is not None) and ( self._scroll == self._get_scroll_button(event) ): self.scroll(self._scroll) else: super().mouse_up(event) # --------------------------------------------------------------------------- # Handles the mouse moving while the left mouse button is pressed: # --------------------------------------------------------------------------- def mouse_move(self, event): """ Handles the mouse moving while the left mouse button is pressed. """ pass # --------------------------------------------------------------------------- # Sets the visibility of the region: # --------------------------------------------------------------------------- def set_visibility(self, visible): """ Sets the visibility of the region. """ self._visible = visible active = self.active for i, item in enumerate(self.contents): item.set_visibility(visible and (i == active)) # --------------------------------------------------------------------------- # Activates a specified control (i.e. makes it the current notebook tab): # --------------------------------------------------------------------------- def activate(self, control, layout=True): """ Activates a specified control (i.e. makes it the current notebook tab). """ if control.visible and self.is_notebook: active = self.contents.index(control) if active != self.active: self.active = active self.make_active_tab_visible() window = control.control.GetParent() if layout: do_later(window.owner.update_layout) else: window.RefreshRect(wx.Rect(*self.bounds)) else: # Fire the activated event for the control. if isinstance(control, DockControl): control.activated = True # --------------------------------------------------------------------------- # Makes sure the active control's tab is completely visible (if possible): # --------------------------------------------------------------------------- def make_active_tab_visible(self): """ Makes sure the active control's tab is completely visible (if possible). """ active = self.active if active < self.left_tab: self.scroll(SCROLL_TO, active) else: x, y, dx, dy = self.contents[active].drag_bounds if not self.is_at(x + dx - 1, y + dy - 1, self._tab_clip_bounds): self.scroll(SCROLL_TO, min(active, self.max_tab)) # --------------------------------------------------------------------------- # Handles a contained DockControl item being hidden or shown: # --------------------------------------------------------------------------- def show_hide(self, control): """ Handles a contained DockControl item being hidden or shown. """ i = self.contents.index(control) if i == self.active: self._update_active() elif (self.active < 0) and control.visible: self.active = i self._is_notebook = None # --------------------------------------------------------------------------- # Prints the contents of the region: # --------------------------------------------------------------------------- def dump(self, indent): """ Prints the contents of the region. """ print( "%sRegion( %08X, active = %s, width = %d, height = %d )" % (" " * indent, id(self), self.active, self.width, self.height) ) for item in self.contents: item.dump(indent + 3) # --------------------------------------------------------------------------- # Returns which scroll button (if any) the pointer is currently over: # --------------------------------------------------------------------------- def _get_scroll_button(self, event): """ Returns which scroll button (if any) the pointer is currently over. """ x, y, dx, dy = self._tab_clip_bounds if self.is_in( event, x + dx, y + 2, DockImages._tab_scroller_dx, DockImages._tab_scroller_dy, ): if (event.GetX() - (x + dx)) < (DockImages._tab_scroller_dx // 2): return SCROLL_LEFT return SCROLL_RIGHT return None # --------------------------------------------------------------------------- # Updates the currently active page after a change: # --------------------------------------------------------------------------- def _update_active(self, active=None): """ Updates the currently active page after a change. """ if active is None: active = self.active contents = self.contents for i in list(range(active, len(contents))) + list( range(active - 1, -1, -1) ): if contents[i].visible: self.active = i return self.active = -1 # --------------------------------------------------------------------------- # Handles the 'active' trait being changed: # --------------------------------------------------------------------------- @observe("active") def _active_updated(self, event): old, new = event.old, event.new self._set_visibility() # Set the correct tab state for each tab: for i, item in enumerate(self.contents): item.tab_state = NormalStates[i == new] n = len(self.contents) if 0 <= old < n: # Notify the previously active dockable that the control's tab is # being deactivated: control = self.contents[old] if isinstance(control, DockControl) and ( control.dockable is not None ): control.dockable.dockable_tab_activated(control, False) if 0 <= new < n: # Notify the new dockable that the control's tab is being # activated: control = self.contents[new] if isinstance(control, DockControl): control.activated = True # --------------------------------------------------------------------------- # Handles the 'contents' trait being changed: # --------------------------------------------------------------------------- @observe("contents") def _contents_updated(self, event): """ Handles the 'contents' trait being changed. """ self._is_notebook = None for item in self.contents: item.parent = self self.calc_min(True) self.modified = True @observe("contents:items") def _contents_items_updated(self, event): """ Handles the 'contents' trait being changed. """ self._is_notebook = None for item in event.added: item.parent = self self.calc_min(True) self.modified = True # --------------------------------------------------------------------------- # Set the proper visiblity for all contained controls: # --------------------------------------------------------------------------- def _set_visibility(self): """ Set the proper visiblity for all contained controls. """ active = self.active for i, item in enumerate(self.contents): item.set_visibility(i == active) # --------------------------------------------------------------------------- # Implementation of the 'modified' property: # --------------------------------------------------------------------------- def _set_modified(self, value): if self.parent is not None: self.parent.modified = True # --------------------------------------------------------------------------- # Implementation of the 'is_notebook' property: # --------------------------------------------------------------------------- def _get_is_notebook(self): if self._is_notebook is None: contents = self.visible_contents n = len(contents) self._is_notebook = n > 1 if n == 1: self._is_notebook = contents[0].style == "tab" return self._is_notebook # --------------------------------------------------------------------------- # Draws the notebook body: # --------------------------------------------------------------------------- def _draw_notebook(self, dc): """ Draws the notebook body. """ theme = self.theme tab_height = theme.tab_active.image_slice.dy x, y, dx, dy = self.bounds self.fill_bg_color(dc, x, y, dx, dy) # Draws a box around the frame containing the tab contents, starting # below the tab pen = wx.Pen(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNSHADOW)) dc.SetPen(pen) dc.DrawRectangle(x, y + tab_height, dx, dy - tab_height) # draw highlight pen = wx.Pen(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNHIGHLIGHT)) dc.SetPen(pen) dc.DrawLine(x + 1, y + tab_height + 1, x + dx - 1, y + tab_height + 1) # Erases the line under the active tab x0 = x + self.tab_theme.label.left x1 = x0 for i in range(self.active + 1): x0 = x1 + 1 x1 += self.contents[i].tab_width dc.SetPen(wx.Pen(self.get_bg_color())) dc.DrawLine(x0, y + tab_height, x1, y + tab_height) dc.DrawLine(x0, y + tab_height + 1, x1, y + tab_height + 1) # ------------------------------------------------------------------------------- # 'DockSection' class: # ------------------------------------------------------------------------------- class DockSection(DockGroup): # --------------------------------------------------------------------------- # Trait definitions: # --------------------------------------------------------------------------- # Is this a row (or a column)? is_row = Bool(True) # Bounds of any splitter bars associated with the region: splitters = List(DockSplitter) # The DockWindow that owns this section (set on top level section only): dock_window = Instance("pyface.dock.dock_window.DockWindow") # Contents of the section have been modified property: modified = Property # --------------------------------------------------------------------------- # Re-implementation of the 'owner' property: # --------------------------------------------------------------------------- @cached_property def _get_owner(self): if self.dock_window is not None: return self.dock_window if self.parent is None: return None return self.parent.owner # --------------------------------------------------------------------------- # Calculates the minimum size of the section: # --------------------------------------------------------------------------- def calc_min(self, use_size=False): """ Calculates the minimum size of the section. """ tdx = tdy = 0 contents = self.visible_contents n = len(contents) if self.is_row: # allow 10 pixels for the splitter sdx = 10 for item in contents: dx, dy = item.calc_min(use_size) tdx += dx tdy = max(tdy, dy) if self.resizable: tdx += (n - 1) * sdx else: tdx += (n + 1) * 3 tdy += 6 else: # allow 10 pixels for the splitter sdy = 10 for item in contents: dx, dy = item.calc_min(use_size) tdx = max(tdx, dx) tdy += dy if self.resizable: tdy += (n - 1) * sdy else: tdx += 6 tdy += (n + 1) * 3 if self.width < 0: self.width = tdx self.height = tdy return (tdx, tdy) # --------------------------------------------------------------------------- # Perform initial layout of the section based on the specified bounds: # --------------------------------------------------------------------------- def initial_recalc_sizes(self, x, y, dx, dy): """ Layout the contents of the section based on the specified bounds. """ self.width = dx = max(0, dx) self.height = dy = max(0, dy) self.bounds = (x, y, dx, dy) # If none of the contents are resizable, use the fixed layout method if not self.resizable: self.recalc_sizes_fixed(x, y, dx, dy) return contents = self.visible_contents n = len(contents) - 1 splitters = [] # Find out how much space is available. splitter_size = 10 sizes = [] if self.is_row: total = dx - (n * splitter_size) else: total = dy - (n * splitter_size) # Get requested sizes from the items. for item in contents: size = -1.0 for dock_control in item.get_controls(): dockable = dock_control.dockable if dockable is not None and dockable.element is not None: if self.is_row: size = max(size, dockable.element.width) else: size = max(size, dockable.element.height) sizes.append(size) # 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 # Resize contents and add splitters if self.is_row: for i, item in enumerate(contents): idx = int(sizes[i]) item.recalc_sizes(x, y, idx, dy) x += idx if i < n: splitters.append( DockSplitter( bounds=(x, y, splitter_size, dy), style="vertical", parent=self, index=i, ) ) x += splitter_size else: for i, item in enumerate(contents): idy = int(sizes[i]) item.recalc_sizes(x, y, dx, idy) y += idy if i < n: splitters.append( DockSplitter( bounds=(x, y, dx, splitter_size), style="horizontal", parent=self, index=i, ) ) y += splitter_size # Preserve the current internal '_last_bounds' for all splitters if # possible: cur_splitters = self.splitters for i in range(min(len(splitters), len(cur_splitters))): splitters[i]._last_bounds = cur_splitters[i]._last_bounds # Save the new set of splitter bars: self.splitters = splitters # Set the visibility for all contained items: self._set_visibility() # --------------------------------------------------------------------------- # Layout the contents of the section based on the specified bounds: # --------------------------------------------------------------------------- def recalc_sizes(self, x, y, dx, dy): """ Layout the contents of the section based on the specified bounds. """ # Check if we need to perform initial layout if not self.initialized: self.initial_recalc_sizes(x, y, dx, dy) self.initialized = True return self.width = dx = max(0, dx) self.height = dy = max(0, dy) self.bounds = (x, y, dx, dy) # If none of the contents are resizable, use the fixed layout method: if not self.resizable: self.recalc_sizes_fixed(x, y, dx, dy) return contents = self.visible_contents n = len(contents) - 1 splitters = [] # Perform a horizontal layout: if self.is_row: # allow 10 pixels for the splitter sdx = 10 dx -= n * sdx cdx = 0 # Calculate the current and minimum width: for item in contents: cdx += item.width cdx = max(1, cdx) # Calculate the delta between the current and new width: delta = remaining = dx - cdx # Allocate the change (plus or minus) proportionally based on each # item's current size: for i, item in enumerate(contents): if i < n: idx = int(round(float(item.width * delta) / cdx)) else: idx = remaining remaining -= idx idx += item.width item.recalc_sizes(x, y, idx, dy) x += idx # Define the splitter bar between adjacent items: if i < n: splitters.append( DockSplitter( bounds=(x, y, sdx, dy), style="vertical", parent=self, index=i, ) ) x += sdx # Perform a vertical layout: else: # allow 10 pixels for the splitter sdy = 10 dy -= n * sdy cdy = 0 # Calculate the current and minimum height: for item in contents: cdy += item.height cdy = max(1, cdy) # Calculate the delta between the current and new height: delta = remaining = dy - cdy # Allocate the change (plus or minus) proportionally based on each # item's current size: for i, item in enumerate(contents): if i < n: idy = int(round(float(item.height * delta) / cdy)) else: idy = remaining remaining -= idy idy += item.height item.recalc_sizes(x, y, dx, idy) y += idy # Define the splitter bar between adjacent items: if i < n: splitters.append( DockSplitter( bounds=(x, y, dx, sdy), style="horizontal", parent=self, index=i, ) ) y += sdy # Preserve the current internal '_last_bounds' for all splitters if # possible: cur_splitters = self.splitters for i in range(min(len(splitters), len(cur_splitters))): splitters[i]._last_bounds = cur_splitters[i]._last_bounds # Save the new set of splitter bars: self.splitters = splitters # Set the visibility for all contained items: self._set_visibility() # --------------------------------------------------------------------------- # Layout the contents of the section based on the specified bounds using # the minimum requested size for each item: # --------------------------------------------------------------------------- def recalc_sizes_fixed(self, x, y, dx, dy): """ Layout the contents of the section based on the specified bounds using the minimum requested size for each item. """ self.splitters = [] x += 3 y += 3 dx = max(0, dx - 3) dy = max(0, dy - 3) # Perform a horizontal layout: if self.is_row: # Allocate the space for each item based on its minimum size until # the space runs out: for item in self.visible_contents: idx, idy = item.calc_min() idx = min(dx, idx) idy = min(dy, idy) dx = max(0, dx - idx - 3) item.recalc_sizes(x, y, idx, idy) x += idx + 3 # Perform a vertical layout: else: # Allocate the space for each item based on its minimum size until # the space runs out: for item in self.visible_contents: idx, idy = item.calc_min() idx = min(dx, idx) idy = min(dy, idy) dy = max(0, dy - idy - 3) item.recalc_sizes(x, y, idx, idy) y += idy + 3 # Set the visibility for all contained items: self._set_visibility() # --------------------------------------------------------------------------- # Draws the contents of the section: # --------------------------------------------------------------------------- def draw(self, dc): """ Draws the contents of the section. """ if self._visible is not False: contents = self.visible_contents x, y, dx, dy = self.bounds self.fill_bg_color(dc, x, y, dx, dy) for item in contents: item.draw(dc) self.begin_draw(dc) for item in self.splitters: item.draw(dc) self.end_draw(dc) # --------------------------------------------------------------------------- # Returns the object at a specified window position: # --------------------------------------------------------------------------- def object_at(self, x, y, force=False): """ Returns the object at a specified window position. """ if self._visible is not False: for item in self.splitters: if item.is_at(x, y): return item for item in self.visible_contents: object = item.object_at(x, y) if object is not None: return object if force and self.is_at(x, y): return self return None # --------------------------------------------------------------------------- # Gets the DockInfo object for a specified window position: # --------------------------------------------------------------------------- def dock_info_at(self, x, y, tdx, is_control, force=False): """ Gets the DockInfo object for a specified window position. """ # Check to see if the point is in our drag bar: info = super().dock_info_at(x, y, tdx, is_control) if info is not None: return info if self._visible is False: return None for item in self.splitters: if item.is_at(x, y): return DockInfo(kind=DOCK_SPLITTER) for item in self.visible_contents: object = item.dock_info_at(x, y, tdx, is_control) if object is not None: return object # Check to see if we must return a DockInfo object: if not force: return None # Otherwise, figure out which edge the point is closest to, and # return a DockInfo object describing that edge: lx, ty, dx, dy = self.bounds left = lx - x right = x - lx - dx + 1 top = ty - y bottom = y - ty - dy + 1 # If the point is way outside of the section, mark it is a drag and # drop candidate: if max(left, right, top, bottom) > 20: return DockInfo(kind=DOCK_EXPORT) left = abs(left) right = abs(right) top = abs(top) bottom = abs(bottom) choice = min(left, right, top, bottom) mdx = dx // 3 mdy = dy // 3 if choice == left: return DockInfo(kind=DOCK_LEFT, bounds=(lx, ty, mdx, dy)) if choice == right: return DockInfo( kind=DOCK_RIGHT, bounds=(lx + dx - mdx, ty, mdx, dy) ) if choice == top: return DockInfo(kind=DOCK_TOP, bounds=(lx, ty, dx, mdy)) return DockInfo(kind=DOCK_BOTTOM, bounds=(lx, ty + dy - mdy, dx, mdy)) # --------------------------------------------------------------------------- # Adds a control to the section at the edge of the region specified: # --------------------------------------------------------------------------- def add(self, control, region, kind): """ Adds a control to the section at the edge of the region specified. """ contents = self.contents new_region = control if not isinstance(control, DockRegion): new_region = DockRegion(contents=[control]) i = contents.index(region) if self.is_row: if (kind == DOCK_TOP) or (kind == DOCK_BOTTOM): if kind == DOCK_TOP: new_contents = [new_region, region] else: new_contents = [region, new_region] contents[i] = DockSection(is_row=False).trait_set( contents=new_contents ) else: if new_region.parent is self: contents.remove(new_region) i = contents.index(region) if kind == DOCK_RIGHT: i += 1 contents.insert(i, new_region) else: if (kind == DOCK_LEFT) or (kind == DOCK_RIGHT): if kind == DOCK_LEFT: new_contents = [new_region, region] else: new_contents = [region, new_region] contents[i] = DockSection(is_row=True).trait_set( contents=new_contents ) else: if new_region.parent is self: contents.remove(new_region) i = contents.index(region) if kind == DOCK_BOTTOM: i += 1 contents.insert(i, new_region) # --------------------------------------------------------------------------- # Removes a specified region or section from the section: # --------------------------------------------------------------------------- def remove(self, item): """ Removes a specified region or section from the section. """ contents = self.contents if isinstance(item, DockGroup) and (len(item.contents) == 1): contents[contents.index(item)] = item.contents[0] else: contents.remove(item) if self.parent is not None: if len(contents) <= 1: self.parent.remove(self) elif (len(contents) == 0) and (self.dock_window is not None): self.dock_window.dock_window_empty() # --------------------------------------------------------------------------- # Sets the visibility of the group: # --------------------------------------------------------------------------- def set_visibility(self, visible): """ Sets the visibility of the group. """ self._visible = visible for item in self.contents: item.set_visibility(visible) # --------------------------------------------------------------------------- # Returns a copy of the section 'structure', minus the actual content: # --------------------------------------------------------------------------- def get_structure(self): """ Returns a copy of the section 'structure', minus the actual content. """ return self.clone_traits(["is_row", "width", "height"]).trait_set( contents=[item.get_structure() for item in self.contents], splitters=[item.get_structure() for item in self.splitters], ) # --------------------------------------------------------------------------- # Gets the maximum bounds that a splitter bar is allowed to be dragged: # --------------------------------------------------------------------------- def get_splitter_bounds(self, splitter): """ Gets the maximum bounds that a splitter bar is allowed to be dragged. """ x, y, dx, dy = splitter.bounds i = self.splitters.index(splitter) contents = self.visible_contents item1 = contents[i] item2 = contents[i + 1] bx, by, bdx, bdy = item2.bounds if self.is_row: x = item1.bounds[0] dx = bx + bdx - x else: y = item1.bounds[1] dy = by + bdy - y return (x, y, dx, dy) # --------------------------------------------------------------------------- # Updates the affected regions when a splitter bar is released: # --------------------------------------------------------------------------- def update_splitter(self, splitter, window): """ Updates the affected regions when a splitter bar is released. """ x, y, dx, dy = splitter.bounds i = self.splitters.index(splitter) contents = self.visible_contents item1 = contents[i] item2 = contents[i + 1] ix1, iy1, idx1, idy1 = item1.bounds ix2, iy2, idx2, idy2 = item2.bounds window.Freeze() if self.is_row: item1.recalc_sizes(ix1, iy1, x - ix1, idy1) item2.recalc_sizes(x + dx, iy2, ix2 + idx2 - x - dx, idy2) else: item1.recalc_sizes(ix1, iy1, idx1, y - iy1) item2.recalc_sizes(ix2, y + dy, idx2, iy2 + idy2 - y - dy) window.Thaw() if splitter.style == "horizontal": dx = 0 else: dy = 0 window.RefreshRect( wx.Rect( ix1 - dx, iy1 - dy, ix2 + idx2 - ix1 + 2 * dx, iy2 + idy2 - iy1 + 2 * dy, ) ) # --------------------------------------------------------------------------- # Prints the contents of the section: # --------------------------------------------------------------------------- def dump(self, indent=0): """ Prints the contents of the section. """ print( "%sSection( %08X, is_row = %s, width = %d, height = %d )" % (" " * indent, id(self), self.is_row, self.width, self.height) ) for item in self.contents: item.dump(indent + 3) # --------------------------------------------------------------------------- # Sets the correct visiblity for all contained items: # --------------------------------------------------------------------------- def _set_visibility(self): """ Sets the correct visiblity for all contained items. """ for item in self.contents: item.set_visibility(item.visible) # --------------------------------------------------------------------------- # Handles the 'contents' trait being changed: # --------------------------------------------------------------------------- @observe("contents") def _contents_updated(self, event): """ Handles the 'contents' trait being changed. """ for item in self.contents: item.parent = self self.calc_min(True) self.modified = True @observe("contents:items") def _contents_items_updated(self, event): """ Handles the 'contents' trait being changed. """ for item in event.added: item.parent = self self.calc_min(True) self.modified = True # --------------------------------------------------------------------------- # Handles the 'splitters' trait being changed: # --------------------------------------------------------------------------- @observe("splitters") def _splitters_updated(self, event): """ Handles the 'splitters' trait being changed. """ for item in self.splitters: item.parent = self @observe("splitters:items") def _splitters_items_updated(self, event): """ Handles the 'splitters' trait being changed. """ for item in event.added: item.parent = self # --------------------------------------------------------------------------- # Implementation of the 'modified' property: # --------------------------------------------------------------------------- def _set_modified(self, value): self._resizable = None if self.parent is not None: self.parent.modified = True # ------------------------------------------------------------------------------- # 'DockInfo' class: # ------------------------------------------------------------------------------- class DockInfo(HasPrivateTraits): # --------------------------------------------------------------------------- # Trait definitions: # --------------------------------------------------------------------------- # Dock kind: kind = Range(DOCK_TOP, DOCK_EXPORT) # Dock bounds: bounds = Bounds # Tab bounds (if needed): tab_bounds = Bounds # Dock Region: region = Instance(DockRegion) # Dock Control: control = Instance(DockItem) # --------------------------------------------------------------------------- # Draws the DockInfo on the display: # --------------------------------------------------------------------------- def draw(self, window, bitmap=None): """ Draws the DockInfo on the display. """ if DOCK_TOP <= self.kind <= DOCK_TABADD: if bitmap is None: bitmap = self._bitmap if bitmap is None: return else: self._bitmap = bitmap sdc, bx, by = get_dc(window) bdc = wx.MemoryDC() bdc2 = wx.MemoryDC() bdx, bdy = bitmap.GetWidth(), bitmap.GetHeight() bitmap2 = wx.Bitmap(bdx, bdy) bdc.SelectObject(bitmap) bdc2.SelectObject(bitmap2) bdc2.Blit(0, 0, bdx, bdy, bdc, 0, 0) try: bdc3 = wx.GCDC(bdc2) bdc3.SetPen(wx.TRANSPARENT_PEN) bdc3.SetBrush(wx.Brush(wx.Colour(*DockColorBrush))) x, y, dx, dy = self.bounds if DOCK_TAB <= self.kind <= DOCK_TABADD: tx, ty, tdx, tdy = self.tab_bounds bdc3.DrawRoundedRectangle(tx, ty, tdx, tdy, 4) else: bdc3.DrawRoundedRectangle(x, y, dx, dy, 8) except Exception: pass sdc.Blit(bx, by, bdx, bdy, bdc2, 0, 0) # --------------------------------------------------------------------------- # Docks the specified control: # --------------------------------------------------------------------------- def dock(self, control, window): """ Docks the specified control. """ the_control = control kind = self.kind if kind < DOCK_NONE: the_parent = control.parent region = self.region if (kind == DOCK_TAB) or (kind == DOCK_BAR): region.add(control, self.control) elif kind == DOCK_TABADD: item = self.control if isinstance(item, DockControl): if isinstance(control, DockControl): control = DockRegion(contents=[control]) i = region.contents.index(item) region.contents[i] = item = DockSection( contents=[DockRegion(contents=[item]), control], is_row=True, ) elif isinstance(item, DockSection): if isinstance(control, DockSection) and ( item.is_row == control.is_row ): item.contents.extend(control.contents) else: if isinstance(control, DockControl): control = DockRegion(contents=[control]) item.contents.append(control) else: item.contents.append(control) region.active = region.contents.index(item) elif region is not None: region.parent.add(control, region, kind) else: sizer = window.GetSizer() section = sizer._contents if ( section.is_row and ((kind == DOCK_TOP) or (kind == DOCK_BOTTOM)) ) or ( (not section.is_row) and ((kind == DOCK_LEFT) or (kind == DOCK_RIGHT)) ): if len(section.contents) > 0: sizer._contents = section = DockSection( is_row=not section.is_row ).trait_set(contents=[section]) if len(section.contents) > 0: i = 0 if (kind == DOCK_RIGHT) or (kind == DOCK_BOTTOM): i = -1 section.add(control, section.contents[i], kind) else: section.is_row = not section.is_row section.contents = [DockRegion(contents=[control])] section = None if (the_parent is not None) and ( the_parent is not the_control.parent ): the_parent.remove(the_control) # Force the main window to be laid out and redrawn: window.Layout() window.Refresh() # Create a reusable DockInfo indicating no information available: no_dock_info = DockInfo(kind=DOCK_NONE) # ------------------------------------------------------------------------------- # 'SetStructureHandler' class # ------------------------------------------------------------------------------- class SetStructureHandler(object): # --------------------------------------------------------------------------- # Resolves an unresolved DockControl id: # --------------------------------------------------------------------------- def resolve_id(self, id): """ Resolves an unresolved DockControl id. """ return None # --------------------------------------------------------------------------- # Resolves extra, unused DockControls not referenced by the structure: # --------------------------------------------------------------------------- def resolve_extras(self, structure, extras): """ Resolves extra, unused DockControls not referenced by the structure. """ for dock_control in extras: if dock_control.control is not None: dock_control.control.Show(False) # ------------------------------------------------------------------------------- # 'DockSizer' class: # ------------------------------------------------------------------------------- class DockSizer(wx.Sizer): # --------------------------------------------------------------------------- # Initializes the object: # --------------------------------------------------------------------------- def __init__(self, contents=None): super().__init__() # Make sure the DockImages singleton has been initialized: DockImages.init() # Finish initializing the sizer itself: self._contents = self._structure = self._max_structure = None if contents is not None: self.SetContents(contents) # --------------------------------------------------------------------------- # Calculates the minimum size needed by the sizer: # --------------------------------------------------------------------------- def CalcMin(self): if self._contents is None: return wx.Size(20, 20) dx, dy = self._contents.calc_min() return wx.Size(dx, dy) # --------------------------------------------------------------------------- # Layout the contents of the sizer based on the sizer's current size and # position: # --------------------------------------------------------------------------- def RecalcSizes(self): """ Layout the contents of the sizer based on the sizer's current size and position. """ if self._contents is None: return x, y = self.GetPosition().Get() dx, dy = self.GetSize().Get() self._contents.recalc_sizes(x, y, dx, dy) # --------------------------------------------------------------------------- # Returns the current sizer contents: # --------------------------------------------------------------------------- def GetContents(self): """ Returns the current sizer contents. """ return self._contents # --------------------------------------------------------------------------- # Initializes the layout of a DockWindow from a content list: # --------------------------------------------------------------------------- def SetContents(self, contents): """ Initializes the layout of a DockWindow from a content list. """ if isinstance(contents, DockGroup): self._contents = contents elif isinstance(contents, tuple): self._contents = self._set_region(contents) elif isinstance(contents, list): self._contents = self._set_section(contents, True) elif isinstance(contents, DockControl): self._contents = self._set_section([contents], True) else: raise TypeError() # Set the owner DockWindow for the top-level group (if possible) # so that it can notify the owner when the DockWindow becomes empty: control = self._contents.control if control is not None: self._contents.dock_window = control.GetParent().owner # If no saved structure exists yet, save the current one: if self._structure is None: self._structure = self.GetStructure() def _set_region(self, contents): items = [] for item in contents: if isinstance(item, tuple): items.append(self._set_region(item)) elif isinstance(item, list): items.append(self._set_section(item, True)) elif isinstance(item, DockItem): items.append(item) else: raise TypeError() return DockRegion(contents=items) def _set_section(self, contents, is_row): items = [] for item in contents: if isinstance(item, tuple): items.append(self._set_region(item)) elif isinstance(item, list): items.append(self._set_section(item, not is_row)) elif isinstance(item, DockControl): items.append(DockRegion(contents=[item])) else: raise TypeError() return DockSection(is_row=is_row).trait_set(contents=items) # --------------------------------------------------------------------------- # Returns a copy of the layout 'structure', minus the actual content # (i.e. controls, splitters, bounds). This method is intended for use in # persisting the current user layout, so that it can be restored in a # future session: # --------------------------------------------------------------------------- def GetStructure(self): """ Returns a copy of the layout 'structure', minus the actual content (i.e. controls, splitters, bounds). This method is intended for use in persisting the current user layout, so that it can be restored in a future session. """ if self._contents is not None: return self._contents.get_structure() return DockSection() # --------------------------------------------------------------------------- # Takes a previously saved 'GetStructure' result and applies it to the # contents of the sizer in order to restore a previous layout using a # new set of controls: # --------------------------------------------------------------------------- def SetStructure(self, window, structure, handler=None): """ Takes a previously saved 'GetStructure' result and applies it to the contents of the sizer in order to restore a previous layout using a new set of controls. """ section = self._contents if (section is None) or (not isinstance(structure, DockGroup)): return # Make sure that DockSections, which have a separate layout algorithm # for the first layout, are set as initialized. structure.initialized = True # Save the current structure in case a 'ResetStructure' call is made # later: self._structure = self.GetStructure() extras = [] # Create a mapping for all the DockControls in the new structure: map = {} for control in structure.get_controls(False): if control.id in map: control.parent.remove(control) else: map[control.id] = control # Try to map each current item into an equivalent item in the saved # preferences: for control in section.get_controls(False): mapped_control = map.get(control.id) if mapped_control is not None: control.trait_set( **mapped_control.get( "visible", "locked", "closeable", "resizable", "width", "height", ) ) if mapped_control.user_name: control.name = mapped_control.name if mapped_control.user_style: control.style = mapped_control.style structure.replace_control(mapped_control, control) del map[control.id] else: extras.append(control) # Try to resolve all unused saved items: for id, item in map.items(): # If there is a handler, see if it can resolve it: if handler is not None: control = handler.resolve_id(id) if control is not None: item.control = control continue # If nobody knows what it is, just remove it: item.parent.remove(item) # Check if there are any new items that we have never seen before: if len(extras) > 0: if handler is not None: # Allow the handler to decide their fate: handler.resolve_extras(structure, extras) else: # Otherwise, add them to the top level as a new region (let the # user re-arrange them): structure.contents.append(DockRegion(contents=extras)) # Finally, replace the original structure with the updated structure: self.SetContents(structure) # --------------------------------------------------------------------------- # Restores the previously saved structure (if any): # --------------------------------------------------------------------------- def ResetStructure(self, window): """ Restores the previously saved structure (if any). """ if self._structure is not None: self.SetStructure(window, self._structure) # --------------------------------------------------------------------------- # Toggles the current 'lock' setting of the contents: # --------------------------------------------------------------------------- def ToggleLock(self): """ Toggles the current 'lock' setting of the contents. """ if self._contents is not None: self._contents.toggle_lock() # --------------------------------------------------------------------------- # Draws the contents of the sizer: # --------------------------------------------------------------------------- def Draw(self, window): """ Draws the contents of the sizer. """ if self._contents is not None: self._contents.draw(set_standard_font(wx.PaintDC(window))) else: clear_window(window) # --------------------------------------------------------------------------- # Returns the object at a specified x, y position: # --------------------------------------------------------------------------- def ObjectAt(self, x, y, force=False): """ Returns the object at a specified window position. """ if self._contents is not None: return self._contents.object_at(x, y, force) return None # --------------------------------------------------------------------------- # Gets a DockInfo object at a specified x, y position: # --------------------------------------------------------------------------- def DockInfoAt(self, x, y, size, is_control): """ Gets a DockInfo object at a specified x, y position. """ if self._contents is not None: return self._contents.dock_info_at(x, y, size, is_control, True) return no_dock_info # --------------------------------------------------------------------------- # Minimizes/Maximizes a specified DockControl: # --------------------------------------------------------------------------- def MinMax(self, window, dock_control): """ Minimizes/Maximizes a specified DockControl. """ if self._max_structure is None: self._max_structure = self.GetStructure() for control in self.GetContents().get_controls(): control.visible = control is dock_control else: self.Reset(window) # --------------------------------------------------------------------------- # Resets the DockSizer to a known state: # --------------------------------------------------------------------------- def Reset(self, window): """ Resets the DockSizer to a known state. """ if self._max_structure is not None: self.SetStructure(window, self._max_structure) self._max_structure = None # --------------------------------------------------------------------------- # Returns whether the sizer can be maximized now: # --------------------------------------------------------------------------- def IsMaximizable(self): """ Returns whether the sizer can be maximized now. """ return self._max_structure is None 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 pyface-7.4.0/pyface/dock/images/0000755000076500000240000000000014176460550017376 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/dock/images/feature_tool.png0000644000076500000240000000277514176222673022611 0ustar cwebsterstaff00000000000000PNG  IHDR?T/sBIT|d pHYs : :dJtEXtSoftwarewww.inkscape.org<zIDATH[LUg^`łFiZH[k&ꃱO>h/Mև&64i&5b$[ lRB/bҲla/{e+ ɞw0kؘn2y)8/,ۍ\E2p CQʳ9/c 1d``HcY+Ͽ~PU}a)+ o dYlppn&pn(ehDHr>r)>9Q,I*LŘƲ71p۝d>NDqt*rݯ^ tBאN$uAg8$[as%<-yg"D{ztWC}"K~d('2 @@BHn\cIB5p]}A} t!)ҔԈ腎om-vH- [ Df,ҋvXg(WeFKwG94_d$Y /4Vb=Ķ|Ft4tY8S捥0t"<6raQVZMvK#˘-Ҽ[!g-emЅ?u(1v{4^>ax'?=>u֊ei',v ^c}7;N(j]ɖDBtr,?Ad[ڀ`0:veL"_j_2[ IgDX8vل߮-Z£|p"JR*W#8hk)3b$Dv+ {o؝qF];òx_rIC]mQ ZdBێ8/5G@<60P QRU š&b!sɝm5\c ^lˑ&Wsp,ZZj,ƺuՑLjtV4S4 XϤZoo{zBMX,Zp $Is$q'tGW/&'opp,7QAx\QtIN?Ztnl쐧D(P9!S ݊ĆN`l(X9TW)JEć˼+"949_E1 IENDB`pyface-7.4.0/pyface/dock/images/tab_feature_drop.png0000644000076500000240000000071114176222673023412 0ustar cwebsterstaff00000000000000PNG  IHDRa pHYs  ~{IDATxӿKqiP B*1QCa4RPCK5TC=EX?QSDc ZBAd)z|z.+D@ hD#*{Cv_y™7Nԥ4{~~yfƒ@3`(VoO5`m Y {'/p&mmqN=`pO;g%`n1q L 0) а!=ۇO@+IT ;c@] J@rؔ)`h'P H 7hϵ0 ]0V@t˗q[>h4UD&X G :,ZUBM@s΅o*R/IENDB`pyface-7.4.0/pyface/dock/images/tab_feature_disabled.png0000644000076500000240000000075614176222673024226 0ustar cwebsterstaff00000000000000PNG  IHDRa pHYs  ~IDATx?kPGϓ7j~⚎vt ޻9dH1:t`: :c8PJFP:4yuƕ,y"V^ۻ5SW Q|tj`Q$O4=勚l+օo?Ol+Jav!Lg5Ѕ{R:&RVvx`6 7/fٮeY/d۶W? :e۶.P21$Ied2xe@YEpHLYӉFѻ4Mn(z9@sʷXoZϚNTz:N ~?3] Qvʹ0. IM6TA$b'pGi W|HV8DzIENDB`pyface-7.4.0/pyface/dock/images/sh_top.png0000644000076500000240000000021714176222673021402 0ustar cwebsterstaff00000000000000PNG  IHDR× pHYs  ~AIDATx̹ Ƣl@eJʇ%' t/%"0D=,M4*f ؉x%DIENDB`pyface-7.4.0/pyface/dock/images/bar_feature_no_drop.png0000644000076500000240000000041314176222673024103 0ustar cwebsterstaff00000000000000PNG  IHDRRW pHYs  ~IDATx51N@o-f"Fc(,@K d0zmhl/`7P(V۾ԌLSzbgW{ۮ>wIܶs 0uKIZ$-06}~xȲ}-OuO%pIZ{$=p Lȅ1ƀN#EHNsIENDB`pyface-7.4.0/pyface/dock/images/sv_left.png0000644000076500000240000000023314176222673021546 0ustar cwebsterstaff00000000000000PNG  IHDRL pHYs  ~MIDATxѫ C+FAV8`j2^' , W!-.0{`?29}x]yIENDB`pyface-7.4.0/pyface/dock/images/tab_scroll_r.png0000644000076500000240000000035114176222673022552 0ustar cwebsterstaff00000000000000PNG  IHDRkz pHYsodIDATx  BW(t{ljbbAϻ"B4a+yF;?wNp9g"RԾib QpOQ瘨p$Ro`]E#\EqY*YUNZ>WS!W&X 9IENDB`pyface-7.4.0/pyface/dock/images/bar_feature_changed.png0000644000076500000240000000044414176222673024040 0ustar cwebsterstaff00000000000000PNG  IHDRRW pHYsodIDATx4zQ) 7# 0   ƭ] E_V#\VTa-  , 2)z\ ')$22+1d3QIENDB`pyface-7.4.0/pyface/dock/images/sh_bottom.png0000644000076500000240000000021214176222673022077 0ustar cwebsterstaff00000000000000PNG  IHDR× pHYs  ~<\T*kZ\.?MÉ)@>9;+븆38͕/1E ]q^6,Tv& =L*" IENDB`pyface-7.4.0/pyface/dock/images/tab_feature_changed.png0000644000076500000240000000063714176222673024046 0ustar cwebsterstaff00000000000000PNG  IHDRa pHYs  ~QIDATx1OSQ_+նH{$DIqЁ(lG1O@Bsu9 Or>{#20~6U(Tx7 u(OLYKRI}aMQɃ q_'I}Yܗ%S 4ayԟ''X'ܯ</9(%TPW~Fm(([Mz~0^8[s 6+E/&ڨNس_3] ,26aΫM3vy#x4سބ_bUA.I=L|hGEϩu'S(wIENDB`pyface-7.4.0/pyface/dock/images/tab_feature_normal.png0000644000076500000240000000063414176222673023742 0ustar cwebsterstaff00000000000000PNG  IHDRa pHYsodNIDATx=HQгh\J)m, ZJ!!XFDEEB*F#`?i¸A 3yIs"I'+ONYDBjWܬxټ P(?vï-I:{,j?nY\Ţ* dV52=f8d@tn,ޠQ^3Q jЂ8 ħ*L vTߛ5Kv5&tj?_[|4"Pؼ,Z bz>˽x֒SHp-K9t;\x^W) H6ٌP*nb V܅#IENDB`pyface-7.4.0/pyface/dock/images/shell.ico0000644000076500000240000000257614176222673021215 0ustar cwebsterstaff00000000000000h( Ni6Iݻg*m)p;Uj*0Y*u>|C8\*:£zV+AY+liwzKÏPS$n)Wx@Xx@ӭxvhk*aҤznׯHǽBCDzo}Dc*Kt=&zo)h-./m9˙l*JbƏԫyX+yAX+^*Klp:g)rga*"[+Oj)~DSd*66666666666666=RAu:TDOhOI#Ree+HSy!b#FR31`%nYvUR{r cG[%%C?vtRm0 q;/EZ,J$oRa"pM>X&?$oR*BjPwN)dtoRg(K0] |9oWR` 8WWRRR4~2'RRRWizlxx}7.VV5f^<@@@\Q-VklxLxLs _ Vpyface-7.4.0/pyface/dock/images/tab_scroll_l.png0000644000076500000240000000035214176222673022545 0ustar cwebsterstaff00000000000000PNG  IHDRkz pHYsodIDATx Pn!cBW(t cbA?! "MZbuF;?NpDDr-.[=N\ u[6'DqQjkCį8H-)ZW!D+5(\48n9c\',eQZrTkN717v$EIENDB`pyface-7.4.0/pyface/dock/images/close_drag.png0000644000076500000240000000107114176222673022207 0ustar cwebsterstaff00000000000000PNG  IHDR  pHYs+tIME'DptEXtAuthorH tEXtDescription !# tEXtCopyright:tEXtCreation time5 tEXtSoftware]p: tEXtDisclaimertEXtWarningtEXtSourcetEXtComment̖tEXtTitle'IDATM=K`MHZPPl6 )d*3eďA K~ElEA("j6)(T@J$Š7ܝPJk C"jך90˟>u Пvr:&\[F}ll}E!3Ohcm!^ $]&1;|>ķ~J`[S+.&zH-4ZACC@XԺzWR~<5D~;/l\IENDB`pyface-7.4.0/pyface/dock/images/close_tab.png0000644000076500000240000000124214176222673022040 0ustar cwebsterstaff00000000000000PNG  IHDR Vu\ pHYs+tIMEAtEXtAuthorH tEXtDescription !# tEXtCopyright:tEXtCreation time5 tEXtSoftware]p: tEXtDisclaimertEXtWarningtEXtSourcetEXtComment̖tEXtTitle'mIDAT(]=hS'i 5EC)8HZ\+ HV\\{7 IR8%u` BMƘ`gDi#8YdWq$t KT A$U,c#tzz!VXcsizj>}}yJX9xAoy˥)\@z\)MV>?qYw{]gk r0#j&+de^^}$G#OνqsIbspL,֌GwPIKO/+UOn{.S,@DZa)9agٹ~l<R íBZ432xz!h0CIENDB`pyface-7.4.0/pyface/dock/images/bar_feature_normal.png0000644000076500000240000000044414176222673023737 0ustar cwebsterstaff00000000000000PNG  IHDRRW pHYsodIDATx4si c7#=ў]F MՒ]W8V\)Y a|-8=lE~s%X;<`UIENDB`pyface-7.4.0/pyface/dock/images/sh_middle.png0000644000076500000240000000023214176222673022033 0ustar cwebsterstaff00000000000000PNG  IHDR"zL pHYs  ~LIDATxc?q_bBjfj:$9dGP0 7rX'+a[<9H!MNB%b5t8f z,i} n3 "IENDB`pyface-7.4.0/pyface/dock/images/bar_feature_disabled.png0000644000076500000240000000037714176222673024223 0ustar cwebsterstaff00000000000000PNG  IHDRRW pHYs  ~IDATx5̱jP@1!K7'C !s:dRSd d\N.B@ .Ió@.,UY_afwy u]$٪DQt.۶Q@8^Q' `x7u1˲sj4MAyA8X~IENDB`pyface-7.4.0/pyface/dock/tests/0000755000076500000240000000000014176460550017273 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/dock/tests/__init__.py0000644000076500000240000000000014176222673021374 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/dock/tests/test_dock_sizer.py0000644000076500000240000000164714176222673023052 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 MagicMock from pyface.toolkit import toolkit not_wx = toolkit.toolkit != "wx" class TestDockControl(unittest.TestCase): @unittest.skipIf(not_wx, "This test is specific to the wx backend") def test_feature_changed(self): from pyface.dock.dock_sizer import DockControl dock_control = DockControl() DockControl.set_feature_mode = MagicMock() dock_control.feature_changed = True dock_control.set_feature_mode.assert_called_once_with() pyface-7.4.0/pyface/dock/dock_window_shell.py0000644000076500000240000001542114176222673022206 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 DockWindowShell class used to house drag and drag DockWindow items that are dropped on the desktop or on the DockWindowShell window. """ import wx # Fixme: Hack to force 'image_slice' to be added via Category to Theme class: import traitsui.wx # noqa: F401 from traits.api import HasPrivateTraits, Instance from pyface.api import SystemMetrics from pyface.image_resource import ImageResource from .dock_window import DockWindow from .dock_sizer import ( DockSizer, DockSection, DockRegion, DockControl, DOCK_RIGHT, ) # ------------------------------------------------------------------------------- # Constants: # ------------------------------------------------------------------------------- # DockWindowShell frame icon: FrameIcon = ImageResource("shell.ico") # ------------------------------------------------------------------------------- # 'DockWindowShell' class: # ------------------------------------------------------------------------------- class DockWindowShell(HasPrivateTraits): # --------------------------------------------------------------------------- # Trait definitions: # --------------------------------------------------------------------------- # The wx.Frame window which is the actual shell: control = Instance(wx.Frame) # --------------------------------------------------------------------------- # Initializes the object: # --------------------------------------------------------------------------- def __init__(self, dock_control, use_mouse=False, **traits): super().__init__(**traits) old_control = dock_control.control parent = wx.GetTopLevelParent(old_control) while True: next_parent = parent.GetParent() if next_parent is None: break parent = next_parent self.control = shell = wx.Frame( parent, -1, dock_control.name, style=wx.DEFAULT_FRAME_STYLE | wx.FRAME_FLOAT_ON_PARENT | wx.FRAME_NO_TASKBAR, ) shell.SetIcon(FrameIcon.create_icon()) shell.SetBackgroundColour(SystemMetrics().dialog_background_color) shell.Bind(wx.EVT_CLOSE, self._on_close) theme = dock_control.theme dw = DockWindow(shell, auto_close=True, theme=theme) dw.trait_set(style="tab") self._dock_window = dw sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(dw.control, 1, wx.EXPAND) shell.SetSizer(sizer) if use_mouse: x, y = wx.GetMousePosition() else: x, y = old_control.Sizer.GetPosition().Get() x, y = old_control.GetParent().Window.ClientToScreen(x, y) dx, dy = old_control.GetSize().Get() tis = theme.tab.image_slice tc = theme.tab.content tdy = theme.tab_active.image_slice.dy dx += tis.xleft + tc.left + tis.xright + tc.right dy += tis.xtop + tc.top + tis.xbottom + tc.bottom + tdy self.add_control(dock_control) # Set the correct window size and position, accounting for the tab size # and window borders: shell.SetSize(x, y, dx, dy) cdx, cdy = shell.GetClientSize().Get() ex_dx = dx - cdx ex_dy = dy - cdy shell.SetSize( x - (ex_dx // 2) - tis.xleft - tc.left, y - ex_dy + (ex_dx // 2) - tdy - tis.xtop - tc.top, dx + ex_dx, dy + ex_dy, ) shell.Show() # --------------------------------------------------------------------------- # Adds a new DockControl to the shell window: # --------------------------------------------------------------------------- def add_control(self, dock_control): """ Adds a new DockControl to the shell window. """ dw = self._dock_window.control dockable = dock_control.dockable # If the current DockControl should be closed, then do it: close = dockable.dockable_should_close() if close: dock_control.close(force=True) # Create the new control: control = dockable.dockable_get_control(dw) # If the DockControl was closed, then reset it to point to the new # control: if close: dock_control.trait_set(control=control, style="tab") else: # Create a DockControl to describe the new control: dock_control = DockControl( control=control, name=dock_control.name, export=dock_control.export, style="tab", image=dock_control.image, closeable=True, ) # Finish initializing the DockControl: dockable.dockable_init_dockcontrol(dock_control) # Get the current DockSizer: sizer = dw.GetSizer() if sizer is None: # Create the initial sizer: dw.SetSizer( DockSizer( DockSection(contents=[DockRegion(contents=[dock_control])]) ) ) else: # Sizer exists already, try to add the DockControl as a new # notebook tab. If the user has reorganized the layout, then just # dock it on the right side somewhere: section = sizer.GetContents() region = section.contents[0] if isinstance(region, DockRegion): region.add(dock_control) else: section.add(dock_control, region, DOCK_RIGHT) # Force the control to update: dw.Layout() dw.Refresh() # --------------------------------------------------------------------------- # Handles the user attempting to close the window: # --------------------------------------------------------------------------- def _on_close(self, event): """ Handles the user attempting to close the window. """ window = self._dock_window.control section = window.GetSizer().GetContents() n = len(section.contents) # Try to close each individual control: for control in section.get_controls(): control.close(layout=False) # If some, but not all, were closed, make sure the window gets updated: if 0 < len(section.contents) < n: window.Layout() window.Refresh() self.control.Unbind(wx.EVT_CLOSE) pyface-7.4.0/pyface/dock/__init__.py0000644000076500000240000000143514176222673020247 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Pyface 'DockWindow' support. This package provides a Pyface 'dockable' window component that allows child windows to be reorganized within the DockWindow using drag and drop. The component also allows multiple sub-windows to occupy the same sub-region of the DockWindow, in which case each sub-window appears as a separate notebook-like tab within the region. """ pyface-7.4.0/pyface/dock/idock_ui_provider.py0000644000076500000240000000227414176222673022212 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 IDockUIProvider interface which objects which support being dragged and dropped into a DockWindow must implement. """ # ------------------------------------------------------------------------------- # 'IDockUIProvider' class: # ------------------------------------------------------------------------------- class IDockUIProvider(object): # --------------------------------------------------------------------------- # Returns a Traits UI which a DockWindow can imbed: # --------------------------------------------------------------------------- def get_dockable_ui(self, parent): """ Returns a Traits UI which a DockWindow can imbed. """ return self.edit_traits( parent=parent, kind="subpanel", scrollable=True ) pyface-7.4.0/pyface/dock/api.py0000644000076500000240000000243014176222673017255 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Pyface 'DockWindow' support. This package provides a Pyface 'dockable' window component that allows child windows to be reorganized within the DockWindow using drag and drop. The component also allows multiple sub-windows to occupy the same sub-region of the DockWindow, in which case each sub-window appears as a separate notebook-like tab within the region. """ from .dock_window import DockWindow, DockWindowHandler from .dock_sizer import ( DockSizer, DockSection, DockRegion, DockControl, DockStyle, DOCK_LEFT, DOCK_RIGHT, DOCK_TOP, DOCK_BOTTOM, SetStructureHandler, add_feature, DockGroup, ) from .idockable import IDockable from .idock_ui_provider import IDockUIProvider from .ifeature_tool import IFeatureTool from .dock_window_shell import DockWindowShell from .dock_window_feature import DockWindowFeature pyface-7.4.0/pyface/dock/idockable.py0000644000076500000240000001066214176222673020427 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 IDockable interface which objects contained in a DockWindow DockControl can implement in order to allow themselves to be dragged into a different DockWindow. """ # ------------------------------------------------------------------------------- # 'IDockable' class: # ------------------------------------------------------------------------------- class IDockable(object): # --------------------------------------------------------------------------- # Should the current DockControl be closed before creating the new one: # --------------------------------------------------------------------------- def dockable_should_close(self): """ Should the current DockControl be closed before creating the new one. """ return True # --------------------------------------------------------------------------- # Returns whether or not it is OK to close the control, and if it is OK, # then it closes the DockControl itself: # --------------------------------------------------------------------------- def dockable_close(self, dock_control, force): """ Returns whether or not it is OK to close the control. """ return False # --------------------------------------------------------------------------- # Gets a control that can be docked into a DockWindow: # --------------------------------------------------------------------------- def dockable_get_control(self, parent): """ Gets a control that can be docked into a DockWindow. """ raise NotImplementedError( "The 'IDockable.dockable_get_control' method must be overridden" ) # --------------------------------------------------------------------------- # Allows the object to override the default DockControl settings: # --------------------------------------------------------------------------- def dockable_init_dockcontrol(self, dock_control): """ Allows the object to override the default DockControl settings. """ pass # --------------------------------------------------------------------------- # Returns the right-click popup menu for a DockControl (if any): # --------------------------------------------------------------------------- def dockable_menu(self, dock_control, event): """ Returns the right-click popup menu for a DockControl (if any). """ return None # --------------------------------------------------------------------------- # Handles the user double-clicking on the DockControl: # A result of False indicates the event was not handled; all other results # indicate that the event was handled successfully. # --------------------------------------------------------------------------- def dockable_dclick(self, dock_control, event): """ Handles the user double-clicking on the DockControl. A result of False indicates the event was not handled; all other results indicate that the event was handled successfully. """ return False # --------------------------------------------------------------------------- # Handles a notebook tab being activated or deactivated: # --------------------------------------------------------------------------- def dockable_tab_activated(self, dock_control, activated): """ Handles a notebook tab being activated or deactivated. 'dock_control' is the control being activated or deactivated. If 'activated' is True, the control is being activated; otherwise the control is being deactivated. """ pass # --------------------------------------------------------------------------- # Handles the IDockable being bound to a specified DockControl: # --------------------------------------------------------------------------- def dockable_bind(self, dock_control): """ Handles the dockable being bound to a specified DockControl. """ pass pyface-7.4.0/pyface/dock/feature_bar.py0000644000076500000240000004076214176222673020775 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Pyface 'FeatureBar' support. Defines the 'FeatureBar' class which displays and allows the user to interact with a set of DockWindowFeatures for a specified DockControl. """ import wx from traits.api import HasPrivateTraits, Instance, Bool, Event from pyface.wx.drag_and_drop import PythonDropTarget, PythonDropSource from pyface.ui_traits import TraitsUIColor as Color from .dock_sizer import DockControl, FEATURE_EXTERNAL_DRAG from .ifeature_tool import IFeatureTool # ------------------------------------------------------------------------------- # 'FeatureBar' class: # ------------------------------------------------------------------------------- class FeatureBar(HasPrivateTraits): # --------------------------------------------------------------------------- # Trait definitions: # --------------------------------------------------------------------------- # The wx.Window which is the parent for the FeatureBar: parent = Instance(wx.Window) # The DockControl whose features are being displayed: dock_control = Instance(DockControl) # The wx.Window being used for the FeatureBar: control = Instance(wx.Window) # Event posted when the user has completed using the FeatureBar: completed = Event() # The background color for the FeatureBar: bg_color = Color(0xDBEEF7, allow_none=True) # The border color for the FeatureBar: border_color = Color(0x2583AF, allow_none=True) # Should the feature bar display horizontally (or vertically)? horizontal = Bool(True) # --------------------------------------------------------------------------- # Hides the feature bar: # --------------------------------------------------------------------------- def hide(self): """ Hides the feature bar. """ if self.control is not None: self.control.Hide() # --------------------------------------------------------------------------- # Shows the feature bar: # --------------------------------------------------------------------------- def show(self): """ Shows the feature bar. """ # Make sure all prerequisites are met: dock_control, parent = self.dock_control, self.parent if (dock_control is None) or (parent is None): return # Create the actual control (if needed): control = self.control if control is None: self.control = control = wx.Frame( None, -1, "", style=wx.BORDER_NONE ) # Set up the 'erase background' event handler: control.Bind(wx.EVT_ERASE_BACKGROUND, self._erase_background) # Set up the 'paint' event handler: control.Bind(wx.EVT_PAINT, self._paint) # Set up mouse event handlers: control.Bind(wx.EVT_LEFT_DOWN, self._left_down) control.Bind(wx.EVT_LEFT_UP, self._left_up) control.Bind(wx.EVT_RIGHT_DOWN, self._right_down) control.Bind(wx.EVT_RIGHT_UP, self._right_up) control.Bind(wx.EVT_MOTION, self._mouse_move) control.Bind(wx.EVT_ENTER_WINDOW, self._mouse_enter) control.SetDropTarget(PythonDropTarget(self)) # Calculate the best size and position for the feature bar: size = wx.Size(32, 32) width = height = 0 horizontal = self.horizontal for feature in dock_control.active_features: bitmap = feature.bitmap if bitmap is not None: if horizontal: width += bitmap.GetWidth() + 3 height = max(height, bitmap.GetHeight()) else: width = max(width, bitmap.GetWidth()) height += bitmap.GetHeight() + 3 if width > 0: if horizontal: size = wx.Size(width + 5, height + 8) else: size = wx.Size(width + 8, height + 5) control.SetSize(size) px, py = parent.GetScreenPosition() fx, fy = dock_control.feature_popup_position control.SetPosition(wx.Point(px + fx, py + fy)) control.Show() # -- Window Event Handlers -------------------------------------------------- # --------------------------------------------------------------------------- # Handles repainting the window: # --------------------------------------------------------------------------- def _paint(self, event): """ Handles repainting the window. """ window = self.control dx, dy = window.GetSize().Get() dc = wx.PaintDC(window) # Draw the feature container: bg_color = self.bg_color border_color = self.border_color if (bg_color is not None) or (border_color is not None): if border_color is None: dc.SetPen(wx.TRANSPARENT_PEN) else: dc.SetPen(wx.Pen(border_color, 1, wx.SOLID)) if bg_color is None: dc.SetBrush(wx.TRANSPARENT_PEN) else: dc.SetBrush(wx.Brush(bg_color, wx.SOLID)) dc.DrawRectangle(0, 0, dx, dy) # Draw the feature icons: if self.horizontal: x = 4 for feature in self.dock_control.active_features: bitmap = feature.bitmap if bitmap is not None: dc.DrawBitmap(bitmap, x, 4, True) x += bitmap.GetWidth() + 3 else: y = 4 for feature in self.dock_control.active_features: bitmap = feature.bitmap if bitmap is not None: dc.DrawBitmap(bitmap, 4, y, True) y += bitmap.GetHeight() + 3 # --------------------------------------------------------------------------- # Handles erasing the window background: # --------------------------------------------------------------------------- def _erase_background(self, event): """ Handles erasing the window background. """ pass # --------------------------------------------------------------------------- # Handles the left mouse button being pressed: # --------------------------------------------------------------------------- def _left_down(self, event): """ Handles the left mouse button being pressed. """ self._feature = self._feature_at(event) self._dragging = False self._xy = (event.GetX(), event.GetY()) # self.control.CaptureMouse() # --------------------------------------------------------------------------- # Handles the left mouse button being released: # --------------------------------------------------------------------------- def _left_up(self, event): """ Handles the left mouse button being released. """ # self.control.ReleaseMouse() self._dragging = None feature, self._feature = self._feature, None if feature is not None: if feature is self._feature_at(event): self.control.ReleaseMouse() self.completed = True feature._set_event(event) feature.click() # --------------------------------------------------------------------------- # Handles the right mouse button being pressed: # --------------------------------------------------------------------------- def _right_down(self, event): """ Handles the right mouse button being pressed. """ self._feature = self._feature_at(event) self._dragging = False self._xy = (event.GetX(), event.GetY()) # self.control.CaptureMouse() # --------------------------------------------------------------------------- # Handles the right mouse button being released: # --------------------------------------------------------------------------- def _right_up(self, event): """ Handles the right mouse button being released. """ # self.control.ReleaseMouse() self._dragging = None feature, self._feature = self._feature, None if feature is not None: if feature is self._feature_at(event): self.control.ReleaseMouse() self.completed = True feature._set_event(event) feature.right_click() # --------------------------------------------------------------------------- # Handles the mouse moving over the window: # --------------------------------------------------------------------------- def _mouse_move(self, event): """ Handles the mouse moving over the window. """ # Update tooltips if no mouse button is currently pressed: if self._dragging is None: feature = self._feature_at(event) if feature is not self._tooltip_feature: self._tooltip_feature = feature tooltip = "" if feature is not None: tooltip = feature.tooltip wx.ToolTip.Enable(False) wx.ToolTip.Enable(True) self.control.SetToolTip(wx.ToolTip(tooltip)) # Check to see if the mouse has left the window, and mark it # completed if it has: x, y = event.GetX(), event.GetY() dx, dy = self.control.GetSize().Get() if (x < 0) or (y < 0) or (x >= dx) or (y >= dy): self.control.ReleaseMouse() self._tooltip_feature = None self.completed = True return # Check to see if we are in 'drag mode' yet: if not self._dragging: x, y = self._xy if (abs(x - event.GetX()) + abs(y - event.GetY())) < 3: return self._dragging = True # Check to see if user is trying to drag a 'feature': feature = self._feature if feature is not None: feature._set_event(event) prefix = button = "" if event.RightIsDown(): button = "right_" if event.ControlDown(): prefix = "control_" elif event.AltDown(): prefix = "alt_" elif event.ShiftDown(): prefix = "shift_" object = getattr(feature, "%s%sdrag" % (prefix, button))() if object is not None: self.control.ReleaseMouse() self._feature = None self.completed = True self.dock_control.pre_drag_all(object) PythonDropSource(self.control, object) self.dock_control.post_drag_all() self._dragging = None # --------------------------------------------------------------------------- # Handles the mouse entering the window: # --------------------------------------------------------------------------- def _mouse_enter(self, event): """ Handles the mouse entering the window. """ self.control.CaptureMouse() # -- Drag and drop event handlers: ---------------------------------------------- # --------------------------------------------------------------------------- # Handles a Python object being dropped on the control: # --------------------------------------------------------------------------- def wx_dropped_on(self, x, y, data, drag_result): """ Handles a Python object being dropped on the window. """ # Determine what, if any, feature the object was dropped on: feature = self._can_drop_on_feature(x, y, data) # Indicate use of the feature bar is complete: self.completed = True # Reset any drag state information: self.dock_control.post_drag(FEATURE_EXTERNAL_DRAG) # Check to see if the data was dropped on a feature or not: if feature is not None: if isinstance(data, IFeatureTool): # Handle an object implementing IFeatureTool being dropped: dock_control = feature.dock_control data.feature_dropped_on_dock_control(dock_control) data.feature_dropped_on(dock_control.object) else: # Handle a normal object being dropped: wx, wy = self.control.GetScreenPosition() feature.trait_set(x=wx + x, y=wy + y) feature.drop(data) return drag_result return wx.DragNone # --------------------------------------------------------------------------- # Handles a Python object being dragged over the control: # --------------------------------------------------------------------------- def wx_drag_over(self, x, y, data, drag_result): """ Handles a Python object being dragged over the control. """ # Handle the case of dragging a normal object over a 'feature': if self._can_drop_on_feature(x, y, data) is not None: return drag_result return wx.DragNone # --------------------------------------------------------------------------- # Handles a dragged Python object leaving the window: # --------------------------------------------------------------------------- def wx_drag_leave(self, data): """ Handles a dragged Python object leaving the window. """ # Indicate use of the feature bar is complete: self.completed = True # Reset any drag state information: self.dock_control.post_drag(FEATURE_EXTERNAL_DRAG) # -- Private Methods -------------------------------------------------------- # --------------------------------------------------------------------------- # Returns a feature that the pointer is over and which can accept the # specified data: # --------------------------------------------------------------------------- def _can_drop_on_feature(self, x, y, data): """ Returns a feature that the pointer is over and which can accept the specified data. """ feature = self._feature_at(FakeEvent(x, y)) if (feature is not None) and feature.can_drop(data): return feature return None # --------------------------------------------------------------------------- # Returns the DockWindowFeature (if any) at a specified window position: # --------------------------------------------------------------------------- def _feature_at(self, event): """ Returns the DockWindowFeature (if any) at a specified window position. """ if self.horizontal: x = 4 for feature in self.dock_control.active_features: bitmap = feature.bitmap if bitmap is not None: bdx = bitmap.GetWidth() if self._is_in(event, x, 4, bdx, bitmap.GetHeight()): return feature x += bdx + 3 else: y = 4 for feature in self.dock_control.active_features: bitmap = feature.bitmap if bitmap is not None: bdy = bitmap.GetHeight() if self._is_in(event, 4, y, bitmap.GetWidth(), bdy): return feature y += bdy + 3 return None # --------------------------------------------------------------------------- # Returns whether or not an event is within a specified bounds: # --------------------------------------------------------------------------- def _is_in(self, event, x, y, dx, dy): """ Returns whether or not an event is within a specified bounds. """ return (x <= event.GetX() < (x + dx)) and ( y <= event.GetY() < (y + dy) ) # ------------------------------------------------------------------------------- # 'FakeEvent' class: # ------------------------------------------------------------------------------- class FakeEvent(object): def __init__(self, x, y): self.x, self.y = x, y def GetX(self): return self.x def GetY(self): return self.y pyface-7.4.0/pyface/dock/ifeature_tool.py0000644000076500000240000000546614176222673021361 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 IFeatureTool interface which allows objects dragged using the DockWindowFeature API to control the drag target and drop events. Useful for implementing tools which can be dropped onto compatible view objects. """ # ------------------------------------------------------------------------------- # 'IFeatureTool' class: # ------------------------------------------------------------------------------- class IFeatureTool(object): # --------------------------------------------------------------------------- # Returns whether or not the object being dragged (i.e. self) can be # dropped on the specified target object: # --------------------------------------------------------------------------- def feature_can_drop_on(self, object): """ Returns whether or not the object being dragged (i.e. self) can be dropped on the specified target object. """ return False # --------------------------------------------------------------------------- # Returns whether or not the object being dragged (i.e. self) can be # dropped on the specified target object's DockControl: # --------------------------------------------------------------------------- def feature_can_drop_on_dock_control(self, dock_control): """ Returns whether or not the object being dragged (i.e. self) can be dropped on the specified target object's DockControl. """ return False # --------------------------------------------------------------------------- # Allows the dragged object (i.e. self) to handle being dropped on the # specified target object: # --------------------------------------------------------------------------- def feature_dropped_on(self, object): """ Allows the dragged object (i.e. self) to handle being dropped on the specified target object. """ return # --------------------------------------------------------------------------- # Allows the dragged object (i.e. self) to handle being dropped on the # specified target object's DockControl: # --------------------------------------------------------------------------- def feature_dropped_on_dock_control(self, dock_control): """ Allows the dragged object (i.e. self) to handle being dropped on the specified target object's DockControl. """ return pyface-7.4.0/pyface/dock/dock_window.py0000644000076500000240000014160414176222673021022 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Pyface 'DockWindow' support. This package provides a Pyface 'dockable' window component that allows child windows to be reorganized within the DockWindow using drag and drop. The component also allows multiple sub-windows to occupy the same sub-region of the DockWindow, in which case each sub-window appears as a separate notebook-like tab within the region. """ from operator import attrgetter import os import shelve import sys import wx from traits.api import ( HasPrivateTraits, Instance, Tuple, Property, Any, Str, List, Bool, observe, ) from traits.trait_base import traits_home from traitsui.api import View, HGroup, VGroup, Item, Handler, error from traitsui.helper import user_name_for from traitsui.menu import Menu, Action, Separator from traitsui.dockable_view_element import DockableViewElement from traitsui.dock_window_theme import dock_window_theme, DockWindowTheme from pyface.api import SystemMetrics from pyface.wx.drag_and_drop import PythonDropTarget, clipboard from pyface.message_dialog import error as warning from .dock_sizer import ( DockSizer, DockControl, DockRegion, DockStyle, DockSplitter, no_dock_info, clear_window, features, ) from .idockable import IDockable from .idock_ui_provider import IDockUIProvider is_mac = sys.platform == "darwin" # ------------------------------------------------------------------------------- # Global data: # ------------------------------------------------------------------------------- # Dictionary of cursors in use: cursor_map = {} # ------------------------------------------------------------------------------- # DockWindow context menu: # ------------------------------------------------------------------------------- min_max_action = Action(name="Maximize", action="on_min_max") undock_action = Action(name="Undock", action="on_undock") lock_action = Action( name="Lock Layout", action="on_lock_layout", style="toggle" ) layout_action = Action(name="Switch Layout", action="on_switch_layout") save_action = Action(name="Save Layout...", action="on_save_layout") # hide_action = Action( name = 'Hide', # action = 'on_hide' ) # # show_action = Action( name = 'Show', # action = 'on_show' ) edit_action = Action(name="Edit Properties...", action="on_edit") enable_features_action = Action( name="Show All", action="on_enable_all_features" ) disable_features_action = Action( name="Hide All", action="on_disable_all_features" ) # ------------------------------------------------------------------------------- # 'DockWindowHandler' class/interface: # ------------------------------------------------------------------------------- class DockWindowHandler(HasPrivateTraits): # --------------------------------------------------------------------------- # Returns whether or not a specified object can be inserted into the view: # --------------------------------------------------------------------------- def can_drop(self, object): """ Returns whether or not a specified object can be inserted into the view. """ return True # --------------------------------------------------------------------------- # Returns the DockControl object for a specified object: # --------------------------------------------------------------------------- def dock_control_for(self, parent, object): """ Returns the DockControl object for a specified object. """ try: name = object.name except: try: name = object.label except: name = "" if name == "": 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 # --------------------------------------------------------------------------- # Creates a new view of a specified control: # --------------------------------------------------------------------------- def open_view_for(self, control, use_mouse=True): """ Creates a new view of a specified control. """ from .dock_window_shell import DockWindowShell DockWindowShell(control, use_mouse=use_mouse) # --------------------------------------------------------------------------- # Handles the DockWindow becoming empty: # --------------------------------------------------------------------------- def dock_window_empty(self, dock_window): """ Handles the DockWindow becoming empty. """ if dock_window.auto_close: dock_window.control.GetParent().Destroy() # Create a singleton handler: dock_window_handler = DockWindowHandler() # ------------------------------------------------------------------------------- # 'LayoutName' class: # ------------------------------------------------------------------------------- class LayoutName(Handler): # --------------------------------------------------------------------------- # Trait definitions: # --------------------------------------------------------------------------- # Name the user wants to assign to the layout: name = Str() # List of currently assigned names: names = List(Str) # --------------------------------------------------------------------------- # Traits view definitions: # --------------------------------------------------------------------------- view = View( Item("name", label="Layout name"), title="Save Layout", kind="modal", buttons=["OK", "Cancel"], ) # --------------------------------------------------------------------------- # Handles a request to close a dialog-based user interface by the user: # --------------------------------------------------------------------------- def close(self, info, is_ok): if is_ok: name = info.object.name.strip() if name == "": warning( info.ui.control, "No name specified", title="Save Layout Error", ) return False if name in self.names: return error( message="%s is already defined. Replace?" % name, title="Save Layout Warning", parent=info.ui.control, ) return True # ------------------------------------------------------------------------------- # 'DockWindow' class: # ------------------------------------------------------------------------------- class DockWindow(HasPrivateTraits): # --------------------------------------------------------------------------- # Trait definitions: # --------------------------------------------------------------------------- # The wx.Window being used as the DockWindow: control = Instance(wx.Window) # The handler used to determine how certain events should be processed: handler = Any(dock_window_handler) # The 'extra' arguments to be passed to each handler call: handler_args = Tuple() # Close the parent window if the DockWindow becomes empty: auto_close = Bool(False) # The DockWindow graphical theme style information: theme = Instance(DockWindowTheme, allow_none=False) # Default style for external objects dragged into the window: style = DockStyle # Return the sizer associated with the control (i.e. window) sizer = Property # The id used to identify this DockWindow: id = Str() # --------------------------------------------------------------------------- # Initializes the object: # --------------------------------------------------------------------------- def __init__( self, parent, wid=-1, pos=wx.DefaultPosition, size=wx.DefaultSize, style=wx.FULL_REPAINT_ON_RESIZE, **traits ): super().__init__(**traits) # Create the actual window: self.control = control = wx.Window(parent, wid, pos, size, style) control.owner = self # Set up the 'paint' event handler: control.Bind(wx.EVT_PAINT, self._paint) control.Bind(wx.EVT_SIZE, self._size) # Set up mouse event handlers: control.Bind(wx.EVT_LEFT_DOWN, self._left_down) control.Bind(wx.EVT_LEFT_UP, self._left_up) control.Bind(wx.EVT_LEFT_DCLICK, self._left_dclick) control.Bind(wx.EVT_RIGHT_DOWN, self._right_down) control.Bind(wx.EVT_RIGHT_UP, self.right_up) control.Bind(wx.EVT_MOTION, self._mouse_move) control.Bind(wx.EVT_LEAVE_WINDOW, self._mouse_leave) control.SetDropTarget(PythonDropTarget(self)) # Initialize the window background color: if self.theme.use_theme_color: color = self.theme.tab.image_slice.bg_color else: color = SystemMetrics().dialog_background_color color = wx.Colour( int(color[0] * 255), int(color[1] * 255), int(color[2] * 255) ) self.control.SetBackgroundColour(color) # -- Default Trait Values --------------------------------------------------- def _theme_default(self): return dock_window_theme() # -- Trait Event Handlers --------------------------------------------------- @observe("theme") def _update_background_color_and_layout(self, event): theme = event.new if self.control is not None: if theme.use_theme_color: color = theme.tab.image_slice.bg_color else: color = wx.NullColour self.control.SetBackgroundColour(color) self.update_layout() # --------------------------------------------------------------------------- # Notifies the DockWindow that its contents are empty: # --------------------------------------------------------------------------- def dock_window_empty(self): """ Notifies the DockWindow that its contents are empty. """ self.handler.dock_window_empty(self) # --------------------------------------------------------------------------- # Sets the cursor to a specified cursor shape: # --------------------------------------------------------------------------- def set_cursor(self, cursor=None): """ Sets the cursor to a specified cursor shape. """ if cursor is None: self.control.SetCursor(wx.NullCursor) return global cursor_map if cursor not in cursor_map: cursor_map[cursor] = wx.Cursor(cursor) self.control.SetCursor(cursor_map[cursor]) # --------------------------------------------------------------------------- # Releases ownership of the mouse capture: # --------------------------------------------------------------------------- def release_mouse(self): """ Releases ownership of the mouse capture. """ if self._owner is not None: self._owner = None self.control.ReleaseMouse() # --------------------------------------------------------------------------- # Updates the layout of the window: # --------------------------------------------------------------------------- def update_layout(self): """ Updates the layout of the window. """ if self.control: self.control.Layout() self.control.Refresh() # --------------------------------------------------------------------------- # Minimizes/Maximizes a specified DockControl: # --------------------------------------------------------------------------- def min_max(self, dock_control): """ Minimizes/maximizes a specified DockControl. """ sizer = self.sizer if sizer is not None: sizer.MinMax(self.control, dock_control) self.update_layout() # --------------------------------------------------------------------------- # Pops up the feature bar for a specified DockControl: # --------------------------------------------------------------------------- def feature_bar_popup(self, dock_control): """ Pops up the feature bar for a specified DockControl. """ fb = self._feature_bar if fb is None: from .feature_bar import FeatureBar self._feature_bar = fb = FeatureBar(parent=self.control) fb.observe(self._feature_bar_closed, "completed") fb.dock_control = dock_control fb.show() # --------------------------------------------------------------------------- # Handles closing the feature bar: # --------------------------------------------------------------------------- def _feature_bar_closed(self, event): fb = self._feature_bar fb.dock_control.feature_bar_closed() fb.hide() # --------------------------------------------------------------------------- # Perform all operations needed to close the window: # --------------------------------------------------------------------------- def close(self): """ Closes the dock window. In this case, all event handlers are unregistered. Other cleanup operations go here, but at the moment Linux (and other non-Windows platforms?) are less forgiving when things like event handlers arent unregistered. """ self._unregister_event_handlers() # --------------------------------------------------------------------------- # Unregister all event handlers: # --------------------------------------------------------------------------- def _unregister_event_handlers(self): """ Unregister all event handlers setup in the constructor. This is typically done prior to an app shutting down and is needed since Linux (and other non-Windows platforms?) trigger mouse, repaint, etc. events for controls which have already been deleted. """ control = self.control if control is not None: control.Unbind(wx.EVT_PAINT) control.Unbind(wx.EVT_SIZE) control.Unbind(wx.EVT_LEFT_DOWN) control.Unbind(wx.EVT_LEFT_UP) control.Unbind(wx.EVT_LEFT_DCLICK) control.Unbind(wx.EVT_RIGHT_DOWN) control.Unbind(wx.EVT_RIGHT_UP) control.Unbind(wx.EVT_MOTION) control.Unbind(wx.EVT_LEAVE_WINDOW) # --------------------------------------------------------------------------- # Handles repainting the window: # --------------------------------------------------------------------------- def _paint(self, event): """ Handles repainting the window. """ # There is a problem on macs where we get paints when the update # is entirely within children. if is_mac and self._is_child_paint(): return sizer = self.sizer if isinstance(sizer, DockSizer): sizer.Draw(self.control) else: clear_window(self.control) # --------------------------------------------------------------------------- # Uses wx calls to determine if we really need to paint or the children will # do it. # --------------------------------------------------------------------------- def _is_child_paint(self): """ Returns whether or not the current update region is entirely within a child. """ if self.control.Children: update_rect = self.control.UpdateRegion.Box for child in self.control.Children: if not child.HasTransparentBackground() and child.Rect.Contains( update_rect ): return True return False # --------------------------------------------------------------------------- # Handles the window being resized: # --------------------------------------------------------------------------- def _size(self, event): """ Handles the window being resized. """ sizer = self.sizer if sizer is not None: try: dx, dy = self.control.GetSize().Get() sizer.SetDimension(0, 0, dx, dy) except: # fixme: This is temporary code to work around a problem in # ProAVA2 that we are still trying to track down... pass event.Skip() # --------------------------------------------------------------------------- # Handles the left mouse button being pressed: # --------------------------------------------------------------------------- def _left_down(self, event): """ Handles the left mouse button being pressed. """ sizer = self.sizer if sizer is not None: object = sizer.ObjectAt(event.GetX(), event.GetY()) if object is not None: self._owner = object self.control.CaptureMouse() object.mouse_down(event) # --------------------------------------------------------------------------- # Handles the left mouse button being released: # --------------------------------------------------------------------------- def _left_up(self, event): """ Handles the left mouse button being released. """ window = self.control if self._owner is not None: window.ReleaseMouse() self._owner.mouse_up(event) self._owner = None # Check for the user requesting that the layout structure be reset: if event.ShiftDown(): if event.ControlDown(): # Check for the developer requesting a structure dump (DEBUG): if event.AltDown(): contents = self.sizer.GetContents() if contents is not None: contents.dump() sys.stdout.flush() else: self.sizer.ResetStructure(window) self.update_layout() elif event.AltDown(): self.sizer.ToggleLock() self.update_layout() # --------------------------------------------------------------------------- # Handles the left mouse button being double clicked: # --------------------------------------------------------------------------- def _left_dclick(self, event): """ Handles the left mouse button being double clicked. """ sizer = self.sizer if sizer is not None: object = sizer.ObjectAt(event.GetX(), event.GetY(), True) if isinstance(object, DockControl): dockable = object.dockable if ( (dockable is None) or (dockable.dockable_dclick(object, event) is False) ) and (object.style != "fixed"): self.min_max(object) elif isinstance(object, DockRegion): self._owner = object self.control.CaptureMouse() object.mouse_down(event) # --------------------------------------------------------------------------- # Handles the right mouse button being pressed: # --------------------------------------------------------------------------- def _right_down(self, event): """ Handles the right mouse button being pressed. """ pass # --------------------------------------------------------------------------- # Handles the right mouse button being released: # --------------------------------------------------------------------------- def right_up(self, event): """ Handles the right mouse button being released. """ sizer = self.sizer if sizer is not None: object = sizer.ObjectAt(event.GetX(), event.GetY(), True) if object is not None: popup_menu = None window = self.control is_dock_control = isinstance(object, DockControl) if ( is_dock_control and (object.dockable is not None) and ( event.ShiftDown() or event.ControlDown() or event.AltDown() ) ): self._menu_self = object.dockable popup_menu = object.dockable.dockable_menu(object, event) if popup_menu is None: self._menu_self = self section = self.sizer.GetContents() is_splitter = isinstance(object, DockSplitter) self._object = object if is_splitter: self._object = object = object.parent group = object if is_dock_control: group = group.parent if sizer.IsMaximizable(): min_max_action.name = "Maximize" else: min_max_action.name = "Restore" min_max_action.enabled = is_dock_control undock_action.enabled = is_dock_control edit_action.enabled = not is_splitter controls = section.get_controls(False) lock_action.checked = (len(controls) > 0) and controls[ 0 ].locked save_action.enabled = self.id != "" feature_menu = self._get_feature_menu() restore_menu, delete_menu = self._get_layout_menus() popup_menu = Menu( min_max_action, undock_action, Separator(), feature_menu, # Separator(), # hide_action, # show_action, Separator(), lock_action, layout_action, Separator(), save_action, restore_menu, delete_menu, Separator(), edit_action, name="popup", ) window.PopupMenu( popup_menu.create_menu(window, self), event.GetX() - 10, event.GetY() - 10, ) self._object = None # --------------------------------------------------------------------------- # Handles the mouse moving over the window: # --------------------------------------------------------------------------- def _mouse_move(self, event): """ Handles the mouse moving over the window. """ if self._last_dock_control is not None: self._last_dock_control.reset_feature_popup() self._last_dock_control = None if self._owner is not None: self._owner.mouse_move(event) else: sizer = self.sizer if sizer is not None: object = self._object or sizer.ObjectAt( event.GetX(), event.GetY() ) self._set_cursor(event, object) if object is not self._hover: if self._hover is not None: self._hover.hover_exit(event) if object is not None: object.hover_enter(event) self._hover = object # Handle feature related processing: if isinstance(object, DockControl) and object.feature_activate( event ): self._last_dock_control = object # --------------------------------------------------------------------------- # Handles the mouse leaving the window: # --------------------------------------------------------------------------- def _mouse_leave(self, event): """ Handles the mouse leaving the window. """ if self._hover is not None: self._hover.hover_exit(event) self._hover = None self._set_cursor(event) # --------------------------------------------------------------------------- # Sets the cursor for a specified object: # --------------------------------------------------------------------------- def _set_cursor(self, event, object=None): """ Sets the cursor for a specified object. """ if object is None: self.set_cursor() else: self.set_cursor(object.get_cursor(event)) # -- Context menu action handlers ----------------------------------------------- # --------------------------------------------------------------------------- # Handles the user asking for a DockControl to be maximized/restored: # --------------------------------------------------------------------------- def on_min_max(self): """ Handles the user asking for a DockControl to be maximized/restored. """ self.min_max(self._object) # --------------------------------------------------------------------------- # Handles the user requesting an element to be undocked: # --------------------------------------------------------------------------- def on_undock(self): """ Handles the user requesting an element to be undocked. """ self.handler.open_view_for(self._object, use_mouse=False) # --------------------------------------------------------------------------- # Handles the user requesting an element to be hidden: # --------------------------------------------------------------------------- def on_hide(self): """ Handles the user requesting an element to be hidden. """ self._object.show(False) # --------------------------------------------------------------------------- # Handles the user requesting an element to be shown: # --------------------------------------------------------------------------- def on_show(self): """ Handles the user requesting an element to be shown. """ object = self._object if isinstance(object, DockControl): object = object.parent self._hidden_group_for(object).show(True) # --------------------------------------------------------------------------- # Handles the user requesting that the current layout be switched: # --------------------------------------------------------------------------- def on_switch_layout(self): """ Handles the user requesting that the current layout be switched. """ self.sizer.ResetStructure(self.control) self.update_layout() # --------------------------------------------------------------------------- # Handles the user requesting that the layout be locked/unlocked: # --------------------------------------------------------------------------- def on_lock_layout(self): """ Handles the user requesting that the layout be locked/unlocked. """ self.sizer.ToggleLock() self.update_layout() # --------------------------------------------------------------------------- # Handles the user requesting that the layout be saved: # --------------------------------------------------------------------------- def on_save_layout(self): """ Handles the user requesting that the layout be saved. """ layout_name = LayoutName(names=self._get_layout_names()) if layout_name.edit_traits(parent=self.control).result: self._set_layout(layout_name.name, self.sizer.GetStructure()) # --------------------------------------------------------------------------- # Handles the user requesting a specified layout to be restored: # --------------------------------------------------------------------------- def on_restore_layout(self, name): """ Handles the user requesting a specified layout to be restored. """ self.sizer.SetStructure(self.control, self._get_layout(name)) self.update_layout() # --------------------------------------------------------------------------- # Handles the user reqesting a specified layout to be deleted: # --------------------------------------------------------------------------- def on_delete_layout(self, name): """ Handles the user reqesting a specified layout to be deleted. """ if error( message="Delete the '%s' layout?" % name, title="Delete Layout Warning", parent=self.control, ): self._delete_layout(name) # --------------------------------------------------------------------------- # Handles the user requesting to edit an item: # --------------------------------------------------------------------------- def on_edit(self, object=None): """ Handles the user requesting to edit an item. """ if object is None: object = self._object control_info = ControlInfo( **object.get("name", "user_name", "style", "user_style") ) if control_info.edit_traits(parent=self.control).result: name = control_info.name.strip() if name != "": object.name = name object.trait_set( **control_info.get("user_name", "style", "user_style") ) self.update_layout() # --------------------------------------------------------------------------- # Enables all features: # --------------------------------------------------------------------------- def on_enable_all_features(self, action): """ Enables all features. """ for feature in features: if (feature.feature_name != "") and (feature.state != 1): feature.toggle_feature(action) # --------------------------------------------------------------------------- # Disables all features: # --------------------------------------------------------------------------- def on_disable_all_features(self, action): """ Disables all features. """ for feature in features: if (feature.feature_name != "") and (feature.state == 1): feature.toggle_feature(action) # --------------------------------------------------------------------------- # Toggles the enabled/disabled state of the action's associated feature: # --------------------------------------------------------------------------- def on_toggle_feature(self, action): """ Toggles the enabled/disabled state of the action's associated feature. """ action._feature.toggle_feature(action) # -- DockWindow user preference database methods -------------------------------- # --------------------------------------------------------------------------- # Gets the layout dictionary for the DockWindow: # --------------------------------------------------------------------------- def _get_layouts(self): """ Gets the layout dictionary for the DockWindow. """ id = self.id if id != "": db = self._get_dw_db() if db is not None: layouts = db.get(id) db.close() return layouts return None # --------------------------------------------------------------------------- # Gets the names of all current layouts defined for the DockWindow: # --------------------------------------------------------------------------- def _get_layout_names(self): """ Gets the names of all current layouts defined for the DockWindow. """ layouts = self._get_layouts() if layouts is not None: return list(layouts.keys()) return [] # --------------------------------------------------------------------------- # Gets the layout data for a specified layout name: # --------------------------------------------------------------------------- def _get_layout(self, name): """ Gets the layout data for a specified layout name. """ layouts = self._get_layouts() if layouts is not None: return layouts.get(name) return None # --------------------------------------------------------------------------- # Deletes the layout data for a specified layout name: # --------------------------------------------------------------------------- def _delete_layout(self, name): """ Deletes the layout data for a specified layout name. """ id = self.id if id != "": db = self._get_dw_db(mode="c") if db is not None: layouts = db.get(id) if layouts is not None: del layouts[name] db[id] = layouts db.close() # --------------------------------------------------------------------------- # Sets the layout data for a specified layout name: # --------------------------------------------------------------------------- def _set_layout(self, name, layout): """ Sets the layout data for a specified layout name. """ id = self.id if id != "": db = self._get_dw_db(mode="c") if db is not None: layouts = db.get(id) if layouts is None: layouts = {} layouts[name] = layout db[id] = layouts db.close() # --------------------------------------------------------------------------- # Gets a reference to the DockWindow UI preference database: # --------------------------------------------------------------------------- def _get_dw_db(self, mode="r"): try: return shelve.open( os.path.join(traits_home(), "dock_window"), flag=mode, protocol=-1, ) except: return None # --------------------------------------------------------------------------- # Returns the 'Features' sub_menu: # --------------------------------------------------------------------------- def _get_feature_menu(self): """ Returns the 'Features' sub_menu. """ enable_features_action.enabled = ( disable_features_action.enabled ) = False for feature in features: if feature.feature_name != "": if feature.state == 1: disable_features_action.enabled = True if enable_features_action.enabled: break else: enable_features_action.enabled = True if disable_features_action.enabled: break actions = [] for feature in features: if feature.feature_name != "": actions.append( Action( name=feature.feature_name, action="on_toggle_feature", _feature=feature, style="toggle", checked=(feature.state == 1), ) ) if len(actions) > 0: actions.sort(key=attrgetter("name")) actions[0:0] = [Separator()] return Menu( name="Features", *([enable_features_action, disable_features_action] + actions) ) # --------------------------------------------------------------------------- # Gets the sub-menus for the 'Restore layout' and 'Delete layout' menu # options: # --------------------------------------------------------------------------- def _get_layout_menus(self): """ Gets the sub-menus for the 'Restore layout' and 'Delete layout' menu options. """ names = self._get_layout_names() if len(names) == 0: restore_actions = [Action(name="", enabled=False)] delete_actions = [Action(name="", enabled=False)] else: names.sort() restore_actions = [ Action( name=name, action="self.on_restore_layout(%s)" % repr(name) ) for name in names ] delete_actions = [ Action( name=name, action="self.on_delete_layout(%s)" % repr(name) ) for name in names ] return [ Menu(name="Restore Layout", *restore_actions), Menu(name="Delete Layout", *delete_actions), ] # -- Drag and drop event handlers: ---------------------------------------------- # --------------------------------------------------------------------------- # Handles a Python object being dropped on the control: # --------------------------------------------------------------------------- def wx_dropped_on(self, x, y, data, drag_result): """ Handles a Python object being dropped on the window. """ if isinstance(data, (IDockUIProvider, DockControl)): window = self.control dock_info = self._dock_info # See the 'wx_drag_leave' method for an explanation of this code: if dock_info is None: dock_info = self._leave_info dock_info.draw(window) self._dock_info = None try: control = self.handler.dock_control_for( *(self.handler_args + (window, data)) ) # Safely check to see if the object quacks like a Binding binding = getattr(clipboard, "node", None) if ( hasattr(binding, "obj") and (binding.obj is data) and hasattr(binding, "namespace_name") ): control.id = "@@%s" % binding.namespace_name dock_info.dock(control, window) return drag_result except: warning( window, "An error occurred while attempting to add an item of " "type '%s' to the window." % data.__class__.__name__, title="Cannot add item to window", ) return wx.DragNone # --------------------------------------------------------------------------- # Handles a Python object being dragged over the control: # --------------------------------------------------------------------------- def wx_drag_any(self, x, y, data, drag_result): """ Handles a Python object being dragged over the control. """ ui_provider = isinstance(data, IDockUIProvider) if ui_provider or isinstance(data, DockControl): if ui_provider and ( not self.handler.can_drop(*(self.handler_args + (data,))) ): return wx.DragNone # Check to see if we are in 'drag mode' yet: cur_dock_info = self._dock_info if cur_dock_info is None: cur_dock_info = no_dock_info if isinstance(data, DockControl): self._dock_size = data.tab_width else: self._dock_size = 80 # Get the window and DockInfo object associated with the event: window = self.control self._dock_info = dock_info = self.sizer.DockInfoAt( x, y, self._dock_size, False ) # If the DockInfo has changed, then update the screen: if ( (cur_dock_info.kind != dock_info.kind) or (cur_dock_info.region is not dock_info.region) or (cur_dock_info.bounds != dock_info.bounds) or (cur_dock_info.tab_bounds != dock_info.tab_bounds) ): # Erase the old region: cur_dock_info.draw(window) # Draw the new region: dock_info.draw(window) return drag_result # Handle the case of dragging a normal object over a 'feature': if self._can_drop_on_feature(x, y, data) is not None: return drag_result return wx.DragNone # --------------------------------------------------------------------------- # Handles a dragged Python object leaving the window: # --------------------------------------------------------------------------- def wx_drag_leave(self, data): """ Handles a dragged Python object leaving the window. """ if self._dock_info is not None: self._dock_info.draw(self.control) # Save the current '_dock_info' in '_leave_info' because under # Linux the drag and drop code sends a spurious 'drag_leave' event # immediately before a 'dropped_on' event, so we need to remember # the '_dock_info' value just in case the next event is # 'dropped_on': self._leave_info, self._dock_info = self._dock_info, None # -- Pyface menu interface implementation --------------------------------------- # --------------------------------------------------------------------------- # Adds a menu item to the menu bar being constructed: # --------------------------------------------------------------------------- def add_to_menu(self, menu_item): """ Adds a menu item to the menu bar being constructed. """ pass # --------------------------------------------------------------------------- # Adds a tool bar item to the tool bar being constructed: # --------------------------------------------------------------------------- def add_to_toolbar(self, toolbar_item): """ Adds a tool bar item to the tool bar being constructed. """ pass # --------------------------------------------------------------------------- # Returns whether the menu action should be defined in the user interface: # --------------------------------------------------------------------------- def can_add_to_menu(self, action): """ Returns whether the action should be defined in the user interface. """ return True # --------------------------------------------------------------------------- # Returns whether the toolbar action should be defined in the user # interface: # --------------------------------------------------------------------------- def can_add_to_toolbar(self, action): """ Returns whether the toolbar action should be defined in the user interface. """ return True # --------------------------------------------------------------------------- # Performs the action described by a specified Action object: # --------------------------------------------------------------------------- def perform(self, action_object): """ Performs the action described by a specified Action object. """ action = action_object.action if action[:5] == "self.": eval(action, globals(), {"self": self._menu_self}) else: method = getattr(self, action) try: method() except: method(action_object) # -- Property implementations --------------------------------------------------- # --------------------------------------------------------------------------- # Implementation of the 'sizer' property: # --------------------------------------------------------------------------- def _get_sizer(self): if self.control is not None: return self.control.GetSizer() return None def _set_sizer(self, sizer): self.control.SetSizer(sizer) # -- Private methods ------------------------------------------------------------ # --------------------------------------------------------------------------- # Finds the first group with any hidden items (if any): # --------------------------------------------------------------------------- def _hidden_group_for(self, group): """ Finds the first group with any hidden items (if any). """ while True: if group is None: return None if len(group.contents) > len(group.visible_contents): return group group = group.parent # --------------------------------------------------------------------------- # Returns a feature that the pointer is over and which can accept the # specified data: # --------------------------------------------------------------------------- def _can_drop_on_feature(self, x, y, data): """ Returns a feature that the pointer is over and which can accept the specified data. """ if self.sizer is not None: object = self.sizer.ObjectAt(x, y) if isinstance(object, DockControl): event = FakeEvent(x, y, self.control) if object.feature_activate(event, data): ldc = self._last_dock_control if (ldc is not None) and (ldc is not object): ldc.reset_feature_popup() self._last_dock_control = object return None if self._last_dock_control is not None: self._last_dock_control.reset_feature_popup() self._last_dock_control = None return None # ------------------------------------------------------------------------------- # 'FakeEvent' class: # ------------------------------------------------------------------------------- class FakeEvent(object): def __init__(self, x, y, object): self.x, self.y, self.object = x, y, object def GetX(self): return self.x def GetY(self): return self.y def GetEventObject(self): return self.object # ------------------------------------------------------------------------------- # 'ControlInfo' class: # ------------------------------------------------------------------------------- class ControlInfo(HasPrivateTraits): # --------------------------------------------------------------------------- # Trait definitions: # --------------------------------------------------------------------------- # Name to be edited: name = Str() # Has the user set the name of the control? user_name = Bool(False) id = Str() # Style of drag bar/tab: style = DockStyle # Has the user set the style for this control: user_style = Bool(False) # --------------------------------------------------------------------------- # Traits view definition: # --------------------------------------------------------------------------- traits_view = View( VGroup( HGroup( HGroup("name<100>{Label}", "3"), HGroup("user_name{Remember label}", show_left=False), ), HGroup( HGroup("style<101>", "3"), HGroup("user_style{Remember style}", show_left=False), ), ), title="Edit properties", kind="modal", buttons=["OK", "Cancel"], ) pyface-7.4.0/pyface/dock/feature_tool.py0000644000076500000240000000300414176222673021172 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Implements the FeatureTool feature that allows a dragged object implementing the IFeatureTool interface to be dropped onto any compatible object. """ from pyface.image_resource import ImageResource from .dock_window_feature import DockWindowFeature # ------------------------------------------------------------------------------- # 'FeatureTool' class: # ------------------------------------------------------------------------------- class FeatureTool(DockWindowFeature): # --------------------------------------------------------------------------- # Trait definitions: # --------------------------------------------------------------------------- image = ImageResource("feature_tool") # --------------------------------------------------------------------------- # Returns whether a specified object can be dropped on the feature image: # --------------------------------------------------------------------------- def can_drop(self, object): """ Returns whether a specified object can be dropped on the feature image. """ return True pyface-7.4.0/pyface/dock/dock_window_feature.py0000644000076500000240000010522714176222673022536 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Implements the DockWindowFeature base class. A DockWindowFeature is an optional feature of a DockControl that can be dynamically contributed to the package. Whenever a DockControl is added to a DockWindow, each feature will be given the opportunity to add itself to the DockControl. Each feature is manifested as an image that appears on the DockControl tab (or drag bar). The user interacts wth the feature using mouse clicks and drag and drop operations (depending upon how the feature is implemented). """ from weakref import ref from traits.api import ( HasPrivateTraits, Instance, Int, Str, Bool, Property, observe ) from traitsui.menu import Menu, Action from pyface.timer.api import do_later from pyface.ui_traits import Image from .dock_window import DockWindow from .dock_sizer import DockControl, add_feature from .ifeature_tool import IFeatureTool # ------------------------------------------------------------------------------- # 'DockWindowFeature' class: # ------------------------------------------------------------------------------- class DockWindowFeature(HasPrivateTraits): """ Implements "features" on DockWindows. See "The DockWindowFeature Feature of DockWindows" document (.doc or .pdf) in pyface.doc for details on using this class. Traits are defined on each feature instance. One or more feature instances are created for each application component that a feature class applies to. A given feature class might or might not apply to a specific application component. The feature class determines whether it applies to an application component when the feature is activated, or when a new application component is added to the DockWindow (and the feature is already active). """ # --------------------------------------------------------------------------- # Class variables: # --------------------------------------------------------------------------- # A string value that is the user interface name of the feature as it # should appear in the DockWindow Features sub-menu (e.g., 'Connect'). An # empty string (the default) means that the feature does not appear in the # Features sub-menu and cannot be enabled or disabled by the user. Avoid # feature names that conflict with other, known features. feature_name = "" # An integer that specifies th current state of the feature # (0 = uninstalled, 1 = active, 2 = disabled). Usually you do not need to # change this value explicitly; DockWindows normally manages the value # automatically, setting it when the user enables or disables the feature. state = 0 # List of weak references to all current instances. instances = [] # --------------------------------------------------------------------------- # Trait definitions: # --------------------------------------------------------------------------- # -- Public Traits -------------------------------------------------------------- # The DockControl instance associated with this feature. Note that features # not directly associated with application components, and instead are # associated with the DockControl object that manages an application # component. The DockControl object provides the feature with access to # information about the parent DockWindow object, other DockControl objects # contained within the same DockWindow, as well as the application # component. This trait is automatically set by the DockWindow when the # feature instance is created and associated with an application component. dock_control = Instance(DockControl) # -- Public Traits (new defaults can be defined by subclasses) ------------------ # The image (icon) to display on the feature bar. If **None**, no image # is displayed. For images that never change, the value can be declared # statically in the class definition. However, the feature is free to # change the value at any time. Changing the value to a new # **ImageResource** object causes the associated image to be updated on the # feature bar. Setting the value to **None** removes the image from the # feature bar. image = Image() # The tooltip to display when the pointer hovers over the image. The value # can be changed dynamically to reflect changes in the feature's state. tooltip = Str() # The x-coordinate of a pointer event that occurred over the feature's # image. This can be used in cases where the event-handling for a feature is # sensitive to the position of the pointer relative to the feature image. # This is not normally the case, but the information is available if it is # needed. x = Int() # The y-coordinate of a pointer event that occurred over the feature's # image. y = Int() # A boolean value that specifies whether the shift key was being held down # when a mouse event occurred. shift_down = Bool(False) # A boolean value that specifies whether the control key was being held down # when a mouse event occurred. control_down = Bool(False) # A boolean value that specifies whether the alt key was being held down # when a mouse event occurred. alt_down = Bool(False) # -- Private Traits ------------------------------------------------------------- # The current bitmap to display on the feature bar. bitmap = Property # -- Overridable Public Methods ------------------------------------------------- # --------------------------------------------------------------------------- # Handles the user left clicking on the feature image: # --------------------------------------------------------------------------- def click(self): """ Handles the user left-clicking on a feature image. This method is designed to be overridden by subclasses. The default implementation attempts to perform a 'quick drag' operation (see the 'quick_drag' method). Returns nothing. """ self.quick_drag() # --------------------------------------------------------------------------- # Handles the user right clicking on the feature image: # --------------------------------------------------------------------------- def right_click(self): """ Handles the user right-clicking on a feature image. This method is designed to be overridden by subclasses. The default implementation attempts to perform a 'quick drag' operation (see the 'quick_right_drag' method). Returns nothing. Typically, you override this method to display the feature's shortcut menu. """ self.quick_right_drag() # --------------------------------------------------------------------------- # Returns the object to be dragged when the user drags the feature image: # --------------------------------------------------------------------------- def drag(self): """ Returns the object to be dragged when the user drags a feature image. This method can be overridden by subclasses. If dragging is supported by the feature, then the method returns the object to be dragged; otherwise it returns **None**. The default implementation returns **None**. """ return None # --------------------------------------------------------------------------- # Returns the object to be dragged when the user drags the feature image # while holding down the 'Ctrl' key: # --------------------------------------------------------------------------- def control_drag(self): """ Returns the object to be dragged when the user drags a feature image while pressing the 'Ctrl' key. This method is designed to be overridden by subclasses. If control-dragging is supported by the feature, then the method returns the object to be dragged; otherwise it returns **None**. The default implementation returns **None**. """ return None # --------------------------------------------------------------------------- # Returns the object to be dragged when the user drags the feature image # while holding down the 'Shift' key: # --------------------------------------------------------------------------- def shift_drag(self): """ Returns the object to be dragged when the user drags a feature image while pressing the 'Shift' key. This method is designed to be overridden by subclasses. If shift-dragging is supported by the feature, then the method returns the object to be dragged; otherwise it returns **None**. The default implementation returns **None**. """ return None # --------------------------------------------------------------------------- # Returns the object to be dragged when the user drags the feature image # while holding down the 'Alt' key: # --------------------------------------------------------------------------- def alt_drag(self): """ Returns the object to be dragged when the user drags a feature image while pressing the 'Alt' key. This method is designed to be overridden by subclasses. If Alt-dragging is supported by the feature, then the method returns the object to be dragged; otherwise it returns **None**. The default implementation returns **None**. """ return None # --------------------------------------------------------------------------- # Returns the object to be dragged when the user right mouse button drags # the feature image: # --------------------------------------------------------------------------- def right_drag(self): """ Returns the object to be dragged when the user right mouse button drags a feature image. This method can be overridden by subclasses. If right dragging is supported by the feature, then the method returns the object to be dragged; otherwise it returns **None**. The default implementation returns **None**. """ return None # --------------------------------------------------------------------------- # Returns the object to be dragged when the user right mouse button drags # the feature image while holding down the 'Ctrl' key: # --------------------------------------------------------------------------- def control_right_drag(self): """ Returns the object to be dragged when the user right mouse button drags a feature image while pressing the 'Ctrl' key. This method is designed to be overridden by subclasses. If right control-dragging is supported by the feature, then the method returns the object to be dragged; otherwise it returns **None**. The default implementation returns **None**. """ return None # --------------------------------------------------------------------------- # Returns the object to be dragged when the user right mouse button drags # the feature image while holding down the 'Shift' key: # --------------------------------------------------------------------------- def shift_control_drag(self): """ Returns the object to be dragged when the user right mouse button drags a feature image while pressing the 'Shift' key. This method is designed to be overridden by subclasses. If right shift-dragging is supported by the feature, then the method returns the object to be dragged; otherwise it returns **None**. The default implementation returns **None**. """ return None # --------------------------------------------------------------------------- # Returns the object to be dragged when the user right mouse button drags # the feature image while holding down the 'Alt' key: # --------------------------------------------------------------------------- def alt_right_drag(self): """ Returns the object to be dragged when the user right mouse button drags a feature image while pressing the 'Alt' key. This method is designed to be overridden by subclasses. If right Alt-dragging is supported by the feature, then the method returns the object to be dragged; otherwise it returns **None**. The default implementation returns **None**. """ return None # --------------------------------------------------------------------------- # Handles the user dropping a specified object on the feature image: # --------------------------------------------------------------------------- def drop(self, object): """ Handles the user dropping a specified object on a feature image. Parameters ---------- object : any object The object being dropped onto the feature image Returns ------- Nothing. Description ----------- This method is designed to be overridden by subclasses. It is called whenever the user drops an object on the feature's tab or drag bar image. This method can be called only if a previous call to **can_drop()** for the same object returned **True**. The default implementation does nothing. """ return # --------------------------------------------------------------------------- # Returns whether a specified object can be dropped on the feature image: # --------------------------------------------------------------------------- def can_drop(self, object): """ Returns whether a specified object can be dropped on a feature image. Parameters ---------- object : any object The object being dragged onto the feature image Returns ------- **True** if *object* is a valid object for the feature to process; **False** otherwise. Description ----------- This method is designed to be overridden by subclasses. It is called whenever the user drags an icon over the feature's tab or drag bar image. The method does not perform any processing on *object*; it only examines it. Processing of the object occurs in the **drop()** method, which is called when the user release the object over the feature's image, which typically occurs after the **can_drop()** method has indicated that the feature can process the object, by returning **True**. The default implementation returns **False**, indicating that the feature does not accept any objects for dropping. """ return False # --------------------------------------------------------------------------- # Performs any clean-up needed when the feature is being removed: # --------------------------------------------------------------------------- def dispose(self): """ Performs any clean-up needed when the feature is removed from its associated application component (for example, when the user disables the feature). This method is designed to be overridden by subclasses. The method performs any clean-up actions needed by the feature, such as closing files, removing trait listeners, and so on. The method does not return a result. The default implementation does nothing. """ pass # -- Public Methods ------------------------------------------------------------- # --------------------------------------------------------------------------- # Displays a pop-up menu: # --------------------------------------------------------------------------- def popup_menu(self, menu): """ Displays a shortcut menu. Parameters ---------- menu : traitsui.menu.Menu object The menu to be displayed Returns ------- Nothing. Description ----------- This helper method displays the shortcut menu specified by *menu* at a point near the feature object's current (x,y) value, as specified by the **x** and **y** traits. Normally, the (x,y) value contains the screen location where the user clicked on the feature's tab or drag bar image. The effect is that the menu is displayed near the feature's icon, with the pointer directly over the top menu option. """ window = self.dock_control.control.GetParent() wx, wy = window.GetScreenPosition() window.PopupMenu( menu.create_menu(window, self), self.x - wx - 10, self.y - wy - 10 ) # --------------------------------------------------------------------------- # Refreshes the display of the feature image: # --------------------------------------------------------------------------- def refresh(self): """ Refreshes the display of the feature image. Returns ------- Nothing. Description ----------- This helper method requests the containing DockWindow to refresh the feature bar. """ self.dock_control.feature_changed = True # --------------------------------------------------------------------------- # Disables the feature: # --------------------------------------------------------------------------- def disable(self): """ Disables the feature. Returns ------- Nothing. Description ----------- This helper method temporarily disables the feature for the associated application component. The feature can be re-enabled by calling the **enable()** method. Disabling the feature removes the feature's icon from the feature bar, without actually deleting the feature (i.e., the **dispose()** method is not called). """ self._image = self.image self.image = None if self._image is not None: self.dock_control.feature_changed = True # --------------------------------------------------------------------------- # Enables the feature: # --------------------------------------------------------------------------- def enable(self): """ Enables the feature. Returns ------- Nothing. Description ----------- This helper method re-enables a previously disabled feature for its associated application component. Enabling a feature restores the feature bar icon that the feature displayed at the time it was disabled. """ self.image = self._image self._image = None if self.image is not None: self.dock_control.feature_changed = True # --------------------------------------------------------------------------- # Performs a quick drag and drop operation by displaying a pop-up menu # containing all targets that the feature's xxx_drag() method can be # dropped on. Selecting an item drops the item on the selected target. # --------------------------------------------------------------------------- def quick_drag(self): """ Performs a quick drag and drop operation by displaying a pop-up menu containing all targets that the feature's xxx_drag() method can be dropped on. Selecting an item drops the item on the selected target. """ # Get the object that would have been dragged: if self.control_down: object = self.control_drag() elif self.alt_down: object = self.alt_drag() elif self.shift_down: object = self.shift_drag() else: object = self.drag() # If there is an object, pop up the menu: if object is not None: self._quick_drag_menu(object) # --------------------------------------------------------------------------- # Performs a quick drag and drop operation with the right mouse button by # displaying a pop-up menu containing all targets that the feature's # xxx_right_drag() method can be dropped on. Selecting an item drops the # item on the selected target. # --------------------------------------------------------------------------- def quick_right_drag(self): """ Performs a quick drag and drop operation with the right mouse button by displaying a pop-up menu containing all targets that the feature's xxx_right_drag() method can be dropped on. Selecting an item drops the item on the selected target. """ # Get the object that would have been dragged: if self.control_down: object = self.control_right_drag() elif self.alt_down: object = self.alt_right_drag() elif self.shift_down: object = self.shift_right_drag() else: object = self.right_drag() # If there is an object, pop up the menu: if object is not None: self._quick_drag_menu(object) # -- Overridable Class Methods --------------------------------------------------- # --------------------------------------------------------------------------- # Returns a single new feature object or list of new feature objects for a # specified DockControl (or None if the feature does not apply to it): # --------------------------------------------------------------------------- @classmethod def feature_for(cls, dock_control): """ Returns a single new feature object or list of new feature objects for a specified DockControl. Parameters ---------- dock_control : pyface.dock.api.DockControl The DockControl object that corresponds to the application component being added, or for which the feature is being enabled. Returns ------- An instance or list of instances of this class that will be associated with the application component; **None** if the feature does not apply to the application component. Description ----------- This class method is designed to be overridden by subclasses. Normally, a feature class determines whether it applies to an application component by examining the component to see if it is an instance of a certain class, supports a specified interface, or has trait attributes with certain types of metadata. The application component is available through the *dock_control.object* trait attribute. Note that it is possible for *dock_control.object* to be **None**. The default implementation for this method calls the **is_feature_for()** class method to determine whether the feature applies to the specified DockControl. If it does, it calls the **new_feature()** class method to create the feature instances to be returned. If it does not, it simply returns **None**. """ if cls.is_feature_for(dock_control): return cls.new_feature(dock_control) return None # --------------------------------------------------------------------------- # Returns a new feature instance for a specified DockControl: # --------------------------------------------------------------------------- @classmethod def new_feature(cls, dock_control): """ Returns a new feature instance for a specified DockControl. Parameters ---------- dock_control : pyface.dock.api.DockControl The DockControl object that corresponds to the application component being added, or for which the feature is being enabled. Returns ------- An instance or list of instances of this class to be associated with the application component; it can also return **None**. Description ----------- This method is designed to be overridden by subclasses. This method is called by the default implementation of the **feature_for()** class method to create the feature instances to be associated with the application component specified by *dock_control*. The default implementation returns the result of calling the class constructor as follows:: cls( dock_control=dock_control ) """ return cls(dock_control=dock_control) # --------------------------------------------------------------------------- # Returns whether or not the DockWindowFeature is a valid feature for a # specified DockControl: # --------------------------------------------------------------------------- @classmethod def is_feature_for(self, dock_control): """ Returns whether this class is a valid feature for the application object corresponding to a specified DockControl. Parameters ---------- dock_control : pyface.dock.api.DockControl The DockControl object that corresponds to the application component being added, or for which the feature is being enabled. Returns ------- **True** if the feature applies to the application object associated with the *dock_control*; **False** otherwise. Description ----------- This class method is designed to be overridden by subclasses. It is called by the default implementation of the **feature_for()** class method to determine whether the feature applies to the application object specified by *dock_control*. The default implementation always returns **True**. """ return True # -- Private Methods ------------------------------------------------------------ # --------------------------------------------------------------------------- # Sets the feature's 'event' traits for a specified mouse 'event': # --------------------------------------------------------------------------- def _set_event(self, event): """ Sets the feature's 'event' traits for a specified mouse 'event'. """ x, y = event.GetEventObject().GetScreenPosition() self.trait_set( x=event.GetX() + x, y=event.GetY() + y, shift_down=event.ShiftDown(), control_down=event.ControlDown(), alt_down=event.AltDown(), ) # --------------------------------------------------------------------------- # Displays the quick drag menu for a specified drag object: # --------------------------------------------------------------------------- def _quick_drag_menu(self, object): """ Displays the quick drag menu for a specified drag object. """ # Get all the features it could be dropped on: feature_lists = [] if isinstance(object, IFeatureTool): msg = "Apply to" for dc in self.dock_control.dock_controls: if dc.visible and ( object.feature_can_drop_on(dc.object) or object.feature_can_drop_on_dock_control(dc) ): from .feature_tool import FeatureTool feature_lists.append([FeatureTool(dock_control=dc)]) else: msg = "Send to" for dc in self.dock_control.dock_controls: if dc.visible: allowed = [ f for f in dc.features if (f.feature_name != "") and f.can_drop(object) ] if len(allowed) > 0: feature_lists.append(allowed) # If there are any compatible features: if len(feature_lists) > 0: # Create the pop-up menu: features = [] actions = [] for list in feature_lists: if len(list) > 1: sub_actions = [] for feature in list: sub_actions.append( Action( name="%s Feature" % feature.feature_name, action="self._drop_on(%d)" % len(features), ) ) features.append(feature) actions.append( Menu( name="%s the %s" % (msg, feature.dock_control.name), *sub_actions ) ) else: actions.append( Action( name="%s %s" % (msg, list[0].dock_control.name), action="self._drop_on(%d)" % len(features), ) ) features.append(list[0]) # Display the pop-up menu: self._object = object self._features = features self.popup_menu(Menu(name="popup", *actions)) self._object = self._features = None # --------------------------------------------------------------------------- # Drops the current object on the feature selected by the user (used by # the 'quick_drag' method: # --------------------------------------------------------------------------- def _drop_on(self, index): """ Drops the current object on the feature selected by the user. """ object = self._object if isinstance(object, IFeatureTool): dc = self._features[index].dock_control object.feature_dropped_on(dc.object) object.feature_dropped_on_dock_control(dc) else: self._features[index].drop(object) # -- Public Class Methods ------------------------------------------------------- # --------------------------------------------------------------------------- # Returns a feature object for use with the specified DockControl (or None # if the feature does not apply to the DockControl object): # --------------------------------------------------------------------------- @classmethod def new_feature_for(cls, dock_control): """ Returns a feature object for use with the specified DockControl (or **None** if the feature does not apply to the DockControl object). """ result = cls.feature_for(dock_control) if result is not None: cls.instances = [ aref for aref in cls.instances if aref() is not None ] if isinstance(result, DockWindowFeature): result = [result] cls.instances.extend([ref(feature) for feature in result]) return result # --------------------------------------------------------------------------- # Toggles the feature on/off: # --------------------------------------------------------------------------- @classmethod def toggle_feature(cls, event): """ Toggles the feature on or off. """ if cls.state == 0: cls.state = 1 add_feature(cls) for control in event.window.control.GetChildren(): window = getattr(control, "owner", None) if isinstance(window, DockWindow): do_later(window.update_layout) else: method = "disable" cls.state = 3 - cls.state if cls.state == 1: method = "enable" cls.instances = [ aref for aref in cls.instances if aref() is not None ] for aref in cls.instances: feature = aref() if feature is not None: getattr(feature, method)() # -- Event Handlers ------------------------------------------------------------- # --------------------------------------------------------------------------- # Handles the 'image' trait being changed: # --------------------------------------------------------------------------- @observe('image') def _reset_bitmap(self, event): self._bitmap = None # -- Property Implementations --------------------------------------------------- def _get_bitmap(self): if (self._bitmap is None) and (self.image is not None): self._bitmap = self.image.create_image().ConvertToBitmap() return self._bitmap # -- Pyface menu interface implementation --------------------------------------- # --------------------------------------------------------------------------- # Adds a menu item to the menu bar being constructed: # --------------------------------------------------------------------------- def add_to_menu(self, menu_item): """ Adds a menu item to the menu bar being constructed. """ pass # --------------------------------------------------------------------------- # Adds a tool bar item to the tool bar being constructed: # --------------------------------------------------------------------------- def add_to_toolbar(self, toolbar_item): """ Adds a tool bar item to the tool bar being constructed. """ pass # --------------------------------------------------------------------------- # Returns whether the menu action should be defined in the user interface: # --------------------------------------------------------------------------- def can_add_to_menu(self, action): """ Returns whether the action should be defined in the user interface. """ return True # --------------------------------------------------------------------------- # Returns whether the toolbar action should be defined in the user # interface: # --------------------------------------------------------------------------- def can_add_to_toolbar(self, action): """ Returns whether the toolbar action should be defined in the user interface. """ return True # --------------------------------------------------------------------------- # Performs the action described by a specified Action object: # --------------------------------------------------------------------------- def perform(self, action): """ Performs the action described by a specified Action object. """ action = action.action if action[:5] == "self.": eval(action, globals(), {"self": self}) else: getattr(self, action)() pyface-7.4.0/pyface/grid/0000755000076500000240000000000014176460550016136 5ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/grid/composite_grid_model.py0000644000076500000240000000113214176222673022676 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 logger = logging.getLogger(__name__) logger.warning("DEPRECATED: pyface.grid, use pyface.ui.wx.grid instead.") from pyface.ui.wx.grid.composite_grid_model import * # noqa: F401 pyface-7.4.0/pyface/grid/grid.py0000644000076500000240000000111214176222673017432 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 logger = logging.getLogger(__name__) logger.warning("DEPRECATED: pyface.grid, use pyface.ui.wx.grid instead.") from pyface.ui.wx.grid.grid import * # noqa: F401 pyface-7.4.0/pyface/grid/simple_grid_model.py0000644000076500000240000000112714176222673022171 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 logger = logging.getLogger(__name__) logger.warning("DEPRECATED: pyface.grid, use pyface.ui.wx.grid instead.") from pyface.ui.wx.grid.simple_grid_model import * # noqa: F401 pyface-7.4.0/pyface/grid/trait_grid_cell_adapter.py0000644000076500000240000000113514176222673023341 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 logger = logging.getLogger(__name__) logger.warning("DEPRECATED: pyface.grid, use pyface.ui.wx.grid instead.") from pyface.ui.wx.grid.trait_grid_cell_adapter import * # noqa: F401 pyface-7.4.0/pyface/grid/grid_cell_image_renderer.py0000644000076500000240000000113614176222673023467 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 logger = logging.getLogger(__name__) logger.warning("DEPRECATED: pyface.grid, use pyface.ui.wx.grid instead.") from pyface.ui.wx.grid.grid_cell_image_renderer import * # noqa: F401 pyface-7.4.0/pyface/grid/__init__.py0000644000076500000240000000000014176222673020237 0ustar cwebsterstaff00000000000000pyface-7.4.0/pyface/grid/combobox_focus_handler.py0000644000076500000240000000113414176222673023215 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 logger = logging.getLogger(__name__) logger.warning("DEPRECATED: pyface.grid, use pyface.ui.wx.grid instead.") from pyface.ui.wx.grid.combobox_focus_handler import * # noqa: F401 pyface-7.4.0/pyface/grid/grid_cell_renderer.py0000644000076500000240000000113014176222673022317 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 logger = logging.getLogger(__name__) logger.warning("DEPRECATED: pyface.grid, use pyface.ui.wx.grid instead.") from pyface.ui.wx.grid.grid_cell_renderer import * # noqa: F401 pyface-7.4.0/pyface/grid/edit_renderer.py0000644000076500000240000000112314176222673021322 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 logger = logging.getLogger(__name__) logger.warning("DEPRECATED: pyface.grid, use pyface.ui.wx.grid instead.") from pyface.ui.wx.grid.edit_renderer import * # noqa: F401 pyface-7.4.0/pyface/grid/mapped_grid_cell_image_renderer.py0000644000076500000240000000114514176222673025015 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 logger = logging.getLogger(__name__) logger.warning("DEPRECATED: pyface.grid, use pyface.ui.wx.grid instead.") from pyface.ui.wx.grid.mapped_grid_cell_image_renderer import * # noqa: F401 pyface-7.4.0/pyface/grid/checkbox_renderer.py0000644000076500000240000000112714176222673022167 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 logger = logging.getLogger(__name__) logger.warning("DEPRECATED: pyface.grid, use pyface.ui.wx.grid instead.") from pyface.ui.wx.grid.checkbox_renderer import * # noqa: F401 pyface-7.4.0/pyface/grid/api.py0000644000076500000240000000107314176222673017264 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 logger = logging.getLogger(__name__) logger.warning("DEPRECATED: pyface.grid, use pyface.ui.wx.grid instead.") from pyface.ui.wx.grid.api import * pyface-7.4.0/pyface/grid/trait_grid_model.py0000644000076500000240000000112614176222673022022 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 logger = logging.getLogger(__name__) logger.warning("DEPRECATED: pyface.grid, use pyface.ui.wx.grid instead.") from pyface.ui.wx.grid.trait_grid_model import * # noqa: F401 pyface-7.4.0/pyface/grid/grid_model.py0000644000076500000240000000112014176222673020611 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 logger = logging.getLogger(__name__) logger.warning("DEPRECATED: pyface.grid, use pyface.ui.wx.grid instead.") from pyface.ui.wx.grid.grid_model import * # noqa: F401 pyface-7.4.0/pyface/grid/checkbox_image_renderer.py0000644000076500000240000000113514176222673023330 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 logger = logging.getLogger(__name__) logger.warning("DEPRECATED: pyface.grid, use pyface.ui.wx.grid instead.") from pyface.ui.wx.grid.checkbox_image_renderer import * # noqa: F401 pyface-7.4.0/pyface/grid/edit_image_renderer.py0000644000076500000240000000113114176222673022463 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 logger = logging.getLogger(__name__) logger.warning("DEPRECATED: pyface.grid, use pyface.ui.wx.grid instead.") from pyface.ui.wx.grid.edit_image_renderer import * # noqa: F401 pyface-7.4.0/pyface/grid/inverted_grid_model.py0000644000076500000240000000113114176222673022513 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 logger = logging.getLogger(__name__) logger.warning("DEPRECATED: pyface.grid, use pyface.ui.wx.grid instead.") from pyface.ui.wx.grid.inverted_grid_model import * # noqa: F401 pyface-7.4.0/pyface/split_application_window.py0000644000076500000240000000255014176222673022674 0ustar cwebsterstaff00000000000000# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in LICENSE.txt and may be redistributed only under # the conditions described 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 window that is split in two either horizontally or vertically. """ from pyface.application_window import ApplicationWindow from pyface.split_widget import SplitWidget class SplitApplicationWindow(ApplicationWindow, SplitWidget): """ A window that is split in two either horizontally or vertically. """ # ------------------------------------------------------------------------ # Protected 'ApplicationWindow' interface. # ------------------------------------------------------------------------ def _create_contents(self, parent): """ Creates the window contents. Parameters ---------- parent : toolkit control The window's toolkit control to be used as the parent for the splitter control. Returns ------- control : toolkit control The splitter control to be used for contents of the window. """ return self._create_splitter(parent) pyface-7.4.0/LICENSE-CC-BY-3.0.txt0000644000076500000240000004601314176222673016664 0ustar cwebsterstaff00000000000000Creative Commons Legal Code Attribution 3.0 Unported CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM ITS USE. License THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED. BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND CONDITIONS. 1. Definitions a. "Adaptation" means a work based upon the Work, or upon the Work and other pre-existing works, such as a translation, adaptation, derivative work, arrangement of music or other alterations of a literary or artistic work, or phonogram or performance and includes cinematographic adaptations or any other form in which the Work may be recast, transformed, or adapted including in any form recognizably derived from the original, except that a work that constitutes a Collection will not be considered an Adaptation for the purpose of this License. For the avoidance of doubt, where the Work is a musical work, performance or phonogram, the synchronization of the Work in timed-relation with a moving image ("synching") will be considered an Adaptation for the purpose of this License. b. "Collection" means a collection of literary or artistic works, such as encyclopedias and anthologies, or performances, phonograms or broadcasts, or other works or subject matter other than works listed in Section 1(f) below, which, by reason of the selection and arrangement of their contents, constitute intellectual creations, in which the Work is included in its entirety in unmodified form along with one or more other contributions, each constituting separate and independent works in themselves, which together are assembled into a collective whole. A work that constitutes a Collection will not be considered an Adaptation (as defined above) for the purposes of this License. c. "Distribute" means to make available to the public the original and copies of the Work or Adaptation, as appropriate, through sale or other transfer of ownership. d. "Licensor" means the individual, individuals, entity or entities that offer(s) the Work under the terms of this License. e. "Original Author" means, in the case of a literary or artistic work, the individual, individuals, entity or entities who created the Work or if no individual or entity can be identified, the publisher; and in addition (i) in the case of a performance the actors, singers, musicians, dancers, and other persons who act, sing, deliver, declaim, play in, interpret or otherwise perform literary or artistic works or expressions of folklore; (ii) in the case of a phonogram the producer being the person or legal entity who first fixes the sounds of a performance or other sounds; and, (iii) in the case of broadcasts, the organization that transmits the broadcast. f. "Work" means the literary and/or artistic work offered under the terms of this License including without limitation any production in the literary, scientific and artistic domain, whatever may be the mode or form of its expression including digital form, such as a book, pamphlet and other writing; a lecture, address, sermon or other work of the same nature; a dramatic or dramatico-musical work; a choreographic work or entertainment in dumb show; a musical composition with or without words; a cinematographic work to which are assimilated works expressed by a process analogous to cinematography; a work of drawing, painting, architecture, sculpture, engraving or lithography; a photographic work to which are assimilated works expressed by a process analogous to photography; a work of applied art; an illustration, map, plan, sketch or three-dimensional work relative to geography, topography, architecture or science; a performance; a broadcast; a phonogram; a compilation of data to the extent it is protected as a copyrightable work; or a work performed by a variety or circus performer to the extent it is not otherwise considered a literary or artistic work. g. "You" means an individual or entity exercising rights under this License who has not previously violated the terms of this License with respect to the Work, or who has received express permission from the Licensor to exercise rights under this License despite a previous violation. h. "Publicly Perform" means to perform public recitations of the Work and to communicate to the public those public recitations, by any means or process, including by wire or wireless means or public digital performances; to make available to the public Works in such a way that members of the public may access these Works from a place and at a place individually chosen by them; to perform the Work to the public by any means or process and the communication to the public of the performances of the Work, including by public digital performance; to broadcast and rebroadcast the Work by any means including signs, sounds or images. i. "Reproduce" means to make copies of the Work by any means including without limitation by sound or visual recordings and the right of fixation and reproducing fixations of the Work, including storage of a protected performance or phonogram in digital form or other electronic medium. 2. Fair Dealing Rights. Nothing in this License is intended to reduce, limit, or restrict any uses free from copyright or rights arising from limitations or exceptions that are provided for in connection with the copyright protection under copyright law or other applicable laws. 3. License Grant. Subject to the terms and conditions of this License, Licensor hereby grants You a worldwide, royalty-free, non-exclusive, perpetual (for the duration of the applicable copyright) license to exercise the rights in the Work as stated below: a. to Reproduce the Work, to incorporate the Work into one or more Collections, and to Reproduce the Work as incorporated in the Collections; b. to create and Reproduce Adaptations provided that any such Adaptation, including any translation in any medium, takes reasonable steps to clearly label, demarcate or otherwise identify that changes were made to the original Work. For example, a translation could be marked "The original work was translated from English to Spanish," or a modification could indicate "The original work has been modified."; c. to Distribute and Publicly Perform the Work including as incorporated in Collections; and, d. to Distribute and Publicly Perform Adaptations. e. For the avoidance of doubt: i. Non-waivable Compulsory License Schemes. In those jurisdictions in which the right to collect royalties through any statutory or compulsory licensing scheme cannot be waived, the Licensor reserves the exclusive right to collect such royalties for any exercise by You of the rights granted under this License; ii. Waivable Compulsory License Schemes. In those jurisdictions in which the right to collect royalties through any statutory or compulsory licensing scheme can be waived, the Licensor waives the exclusive right to collect such royalties for any exercise by You of the rights granted under this License; and, iii. Voluntary License Schemes. The Licensor waives the right to collect royalties, whether individually or, in the event that the Licensor is a member of a collecting society that administers voluntary licensing schemes, via that society, from any exercise by You of the rights granted under this License. The above rights may be exercised in all media and formats whether now known or hereafter devised. The above rights include the right to make such modifications as are technically necessary to exercise the rights in other media and formats. Subject to Section 8(f), all rights not expressly granted by Licensor are hereby reserved. 4. Restrictions. The license granted in Section 3 above is expressly made subject to and limited by the following restrictions: a. You may Distribute or Publicly Perform the Work only under the terms of this License. You must include a copy of, or the Uniform Resource Identifier (URI) for, this License with every copy of the Work You Distribute or Publicly Perform. You may not offer or impose any terms on the Work that restrict the terms of this License or the ability of the recipient of the Work to exercise the rights granted to that recipient under the terms of the License. You may not sublicense the Work. You must keep intact all notices that refer to this License and to the disclaimer of warranties with every copy of the Work You Distribute or Publicly Perform. When You Distribute or Publicly Perform the Work, You may not impose any effective technological measures on the Work that restrict the ability of a recipient of the Work from You to exercise the rights granted to that recipient under the terms of the License. This Section 4(a) applies to the Work as incorporated in a Collection, but this does not require the Collection apart from the Work itself to be made subject to the terms of this License. If You create a Collection, upon notice from any Licensor You must, to the extent practicable, remove from the Collection any credit as required by Section 4(b), as requested. If You create an Adaptation, upon notice from any Licensor You must, to the extent practicable, remove from the Adaptation any credit as required by Section 4(b), as requested. b. If You Distribute, or Publicly Perform the Work or any Adaptations or Collections, You must, unless a request has been made pursuant to Section 4(a), keep intact all copyright notices for the Work and provide, reasonable to the medium or means You are utilizing: (i) the name of the Original Author (or pseudonym, if applicable) if supplied, and/or if the Original Author and/or Licensor designate another party or parties (e.g., a sponsor institute, publishing entity, journal) for attribution ("Attribution Parties") in Licensor's copyright notice, terms of service or by other reasonable means, the name of such party or parties; (ii) the title of the Work if supplied; (iii) to the extent reasonably practicable, the URI, if any, that Licensor specifies to be associated with the Work, unless such URI does not refer to the copyright notice or licensing information for the Work; and (iv) , consistent with Section 3(b), in the case of an Adaptation, a credit identifying the use of the Work in the Adaptation (e.g., "French translation of the Work by Original Author," or "Screenplay based on original Work by Original Author"). The credit required by this Section 4 (b) may be implemented in any reasonable manner; provided, however, that in the case of a Adaptation or Collection, at a minimum such credit will appear, if a credit for all contributing authors of the Adaptation or Collection appears, then as part of these credits and in a manner at least as prominent as the credits for the other contributing authors. For the avoidance of doubt, You may only use the credit required by this Section for the purpose of attribution in the manner set out above and, by exercising Your rights under this License, You may not implicitly or explicitly assert or imply any connection with, sponsorship or endorsement by the Original Author, Licensor and/or Attribution Parties, as appropriate, of You or Your use of the Work, without the separate, express prior written permission of the Original Author, Licensor and/or Attribution Parties. c. Except as otherwise agreed in writing by the Licensor or as may be otherwise permitted by applicable law, if You Reproduce, Distribute or Publicly Perform the Work either by itself or as part of any Adaptations or Collections, You must not distort, mutilate, modify or take other derogatory action in relation to the Work which would be prejudicial to the Original Author's honor or reputation. Licensor agrees that in those jurisdictions (e.g. Japan), in which any exercise of the right granted in Section 3(b) of this License (the right to make Adaptations) would be deemed to be a distortion, mutilation, modification or other derogatory action prejudicial to the Original Author's honor and reputation, the Licensor will waive or not assert, as appropriate, this Section, to the fullest extent permitted by the applicable national law, to enable You to reasonably exercise Your right under Section 3(b) of this License (right to make Adaptations) but not otherwise. 5. Representations, Warranties and Disclaimer UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY, FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU. 6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 7. Termination a. This License and the rights granted hereunder will terminate automatically upon any breach by You of the terms of this License. Individuals or entities who have received Adaptations or Collections from You under this License, however, will not have their licenses terminated provided such individuals or entities remain in full compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will survive any termination of this License. b. Subject to the above terms and conditions, the license granted here is perpetual (for the duration of the applicable copyright in the Work). Notwithstanding the above, Licensor reserves the right to release the Work under different license terms or to stop distributing the Work at any time; provided, however that any such election will not serve to withdraw this License (or any other license that has been, or is required to be, granted under the terms of this License), and this License will continue in full force and effect unless terminated as stated above. 8. Miscellaneous a. Each time You Distribute or Publicly Perform the Work or a Collection, the Licensor offers to the recipient a license to the Work on the same terms and conditions as the license granted to You under this License. b. Each time You Distribute or Publicly Perform an Adaptation, Licensor offers to the recipient a license to the original Work on the same terms and conditions as the license granted to You under this License. c. If any provision of this License is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this License, and without further action by the parties to this agreement, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. d. No term or provision of this License shall be deemed waived and no breach consented to unless such waiver or consent shall be in writing and signed by the party to be charged with such waiver or consent. e. This License constitutes the entire agreement between the parties with respect to the Work licensed here. There are no understandings, agreements or representations with respect to the Work not specified here. Licensor shall not be bound by any additional provisions that may appear in any communication from You. This License may not be modified without the mutual written agreement of the Licensor and You. f. The rights granted under, and the subject matter referenced, in this License were drafted utilizing the terminology of the Berne Convention for the Protection of Literary and Artistic Works (as amended on September 28, 1979), the Rome Convention of 1961, the WIPO Copyright Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996 and the Universal Copyright Convention (as revised on July 24, 1971). These rights and subject matter take effect in the relevant jurisdiction in which the License terms are sought to be enforced according to the corresponding provisions of the implementation of those treaty provisions in the applicable national law. If the standard suite of rights granted under applicable copyright law includes additional rights not granted under this License, such additional rights are deemed to be included in the License; this License is not intended to restrict the license of any rights under applicable law. Creative Commons Notice Creative Commons is not a party to this License, and makes no warranty whatsoever in connection with the Work. Creative Commons will not be liable to You or any party on any legal theory for any damages whatsoever, including without limitation any general, special, incidental or consequential damages arising in connection to this license. Notwithstanding the foregoing two (2) sentences, if Creative Commons has expressly identified itself as the Licensor hereunder, it shall have all rights and obligations of Licensor. Except for the limited purpose of indicating to the public that the Work is licensed under the CCPL, Creative Commons does not authorize the use by either party of the trademark "Creative Commons" or any related trademark or logo of Creative Commons without the prior written consent of Creative Commons. Any permitted use will be in compliance with Creative Commons' then-current trademark usage guidelines, as may be published on its website or otherwise made available upon request from time to time. For the avoidance of doubt, this trademark restriction does not form part of this License. Creative Commons may be contacted at https://creativecommons.org/. pyface-7.4.0/MANIFEST.in0000644000076500000240000000057714176222673015513 0ustar cwebsterstaff00000000000000include image_LICENSE.txt include image_LICENSE_Eclipse.txt include image_LICENSE_Nuvola.txt include image_LICENSE_OOo.txt include LICENSE-CC-BY-3.0.txt include LICENSE.txt include README.rst include MANIFEST.in recursive-include pyface *.py *.png *.jpg *.gif *.svg *.zip *.txt graft docs prune docs/build recursive-exclude docs *.pyc graft examples recursive-exclude examples *.pyc pyface-7.4.0/docs/0000755000076500000240000000000014176460550014672 5ustar cwebsterstaff00000000000000pyface-7.4.0/docs/DockWindowFeature.pdf0000644000076500000240000537170514176222673020775 0ustar cwebsterstaff00000000000000%PDF-1.4 % 374 0 obj <> endobj xref 374 33 0000000016 00000 n 0000002545 00000 n 0000002667 00000 n 0000003002 00000 n 0000003510 00000 n 0000003658 00000 n 0000003804 00000 n 0000003851 00000 n 0000003887 00000 n 0000004183 00000 n 0000004260 00000 n 0000004539 00000 n 0000004835 00000 n 0000005376 00000 n 0000005948 00000 n 0000006095 00000 n 0000006370 00000 n 0000006686 00000 n 0000009356 00000 n 0000059632 00000 n 0000070502 00000 n 0000070784 00000 n 0000070993 00000 n 0000083351 00000 n 0000083625 00000 n 0000083845 00000 n 0000116066 00000 n 0000127191 00000 n 0000127469 00000 n 0000127676 00000 n 0000128149 00000 n 0000133518 00000 n 0000000956 00000 n trailer <<2ED634FE80EB974EB38396D6DE0AEFFC>]>> startxref 0 %%EOF 406 0 obj<>stream xڴV}Lg( R i)lr‰-35Q& #ZS㈭Aŏ f-X-ꖙ@D6efKמ}~g-e# YIE ɌuxJ, PXIfϠ F+HdD Tї0+[fTq裰?-ödi,'l;ļ}Tz.v[{}Qq$o&inXQ'4F1fЈs1j8RvK,{UfXrǙZ1`=J&NH2KҦm%{G)%Qm'܏w8?R@Mw8rBkTd<זT.w!˺bH{Juzq;b*ubO+k],dn g2nL{|R!.cG:>nQt k;t16!l; 5J5(,UIc|[ Z5eJgd;lY Y"6lE$tB7=MS"82!,6 zPBQDj pQ׹Gq<5*7eI:=k1z+߸Ocw6h-xԘھUyKF"*h6N=^ߊRӃ+( FW9&/&yf8u2 wrfoHXі_ K13ρ8bABUW> endobj 376 0 obj<>/ColorSpace<>/Font<>/ProcSet[/PDF/Text/ImageC/ImageI]/ExtGState<>>>/Type/Page>> endobj 377 0 obj<> endobj 378 0 obj<> endobj 379 0 obj<> endobj 380 0 obj[/Indexed 381 0 R 255 403 0 R] endobj 381 0 obj[/ICCBased 391 0 R] endobj 382 0 obj<>stream HTPN0 A yXEB.x-hп' "8:>?g&7}yԢ]L^þf\0E:Jrʹ8;q saGa-/\0d58?=lބ'հ> endobj 384 0 obj<> endobj 385 0 obj<>stream HTPn =7FHݒIsH M{ݥ.`zL0:o A<43iG5YGYoK¹ce-'D< <+Y$̜Gf5/'X??g^EYq B+4|m4oPlB>stream HM0>:ugl@E‰Z6mDe%~=v>i!Tgwfjkd3d?"/hx<{SCQ1R(ɥ~K^$s{ 8Hfr1MŌPGc4UEw[ůZVe3dU;<6e8ɯxpAU! uTR<L+ky)nvMDŝWS"iFvg(%3f_ջ_. G[ORL,& aY}qb5) .֍']hׯ>*.AsAbK[˚|JJT2eOw(; endstream endobj 387 0 obj<> endobj 388 0 obj<> endobj 389 0 obj<> endobj 390 0 obj<>stream HTPN0[($@,Ѥ!}&X"8E1wW3;/zl[g#0# ֙˼p:ձIymKĩu MCGJ.1lpv - ֍9ϯt?8@J08z|QUM/dyQ0gWr#B9r <4oH Ԃq&I55=&U<{K v_CU4伄^CH-~yȯz endstream endobj 391 0 obj<>stream HyTSwoɞc [5laQIBHADED2mtFOE.c}08׎8GNg9w߽'0 ֠Jb  2y.-;!KZ ^i"L0- @8(r;q7Ly&Qq4j|9 V)gB0iW8#8wթ8_٥ʨQQj@&A)/g>'Kt;\ ӥ$պFZUn(4T%)뫔0C&Zi8bxEB;Pӓ̹A om?W= x-[0}y)7ta>jT7@tܛ`q2ʀ&6ZLĄ?_yxg)˔zçLU*uSkSeO4?׸c. R ߁-25 S>ӣVd`rn~Y&+`;A4 A9=-tl`;~p Gp| [`L`< "A YA+Cb(R,*T2B- ꇆnQt}MA0alSx k&^>0|>_',G!"F$H:R!zFQd?r 9\A&G rQ hE]a4zBgE#H *B=0HIpp0MxJ$D1D, VĭKĻYdE"EI2EBGt4MzNr!YK ?%_&#(0J:EAiQ(()ӔWT6U@P+!~mD eԴ!hӦh/']B/ҏӿ?a0nhF!X8܌kc&5S6lIa2cKMA!E#ƒdV(kel }}Cq9 N')].uJr  wG xR^[oƜchg`>b$*~ :Eb~,m,-ݖ,Y¬*6X[ݱF=3뭷Y~dó ti zf6~`{v.Ng#{}}jc1X6fm;'_9 r:8q:˜O:ϸ8uJqnv=MmR 4 n3ܣkGݯz=[==<=GTB(/S,]6*-W:#7*e^YDY}UjAyT`#D="b{ų+ʯ:!kJ4Gmt}uC%K7YVfFY .=b?SƕƩȺy چ k5%4m7lqlioZlG+Zz͹mzy]?uuw|"űNwW&e֥ﺱ*|j5kyݭǯg^ykEklD_p߶7Dmo꿻1ml{Mś nLl<9O[$h՛BdҞ@iءG&vVǥ8nRĩ7u\ЭD-u`ֲK³8%yhYѹJº;.! zpg_XQKFAǿ=ȼ:ɹ8ʷ6˶5̵5͵6ζ7ϸ9к<Ѿ?DINU\dlvۀ܊ݖޢ)߯6DScs 2F[p(@Xr4Pm8Ww)Km endstream endobj 392 0 obj<>stream HVyPWs.[:@p Ι5Wz:{@fEXFA *AzxlRG6讔aѲگ׿Jjکל[BdmH܀v|w@YB4e^Hbt=+;EƫfF<@Z`۟@J)ͅwּ ^l!Qp>5B(8 D *7#,"O{\񘕴P>u"}8賮Tȶï{dqlx؄)JA]گAE"%ňqơNg$#7}# l (w2?pl=?X:4-=|^p';Kp"+{t>*‡?㉠IHqO [rc8"Gz*ǴVS{€S,1ѤLLbj7\GJyo8x"*F|uPp>oDew,BztH1!]S<-ПΟ:ZZϾu|TQVm7nJ$}TYG1:*n55d^ G 7qX}ҕo7.Xr\[))P "vaцys[7+_.z-%:`nZ爥j*g[jYe:O<visL>YqcLeG;pE|^]oϜpeyiaW'SpiuAQ)xy}ܳe&j|a0~>󍁱Nq%6U9>_1vp%m0=GZhkbDa:#qE?zbzUMfb*:9Ӑc0e6}q$"vhġQrgSpM?୒bb!(dD:bci̋ex{6(3tT~jXwoZ0Ƀ_ m' \:UPEͧNPo6u7Df{fTCN@dkOӌ귏GF&_xnǃf\f{i/}]N'yZ[&MpUMܵf\݉;=NK%8tdWkƦJ%RcMq墰k.MWƋoG/L6 0֯CeuO:g=}Y'({/(VԖ IN쾹KPEԍE˪sW܀H sSZ󫣢fY_vmt㎏qoxBD +H"p3x!&Ly喥 eɈ 1xE|,"vRdoby"wB4|{꿨7ϟ!Q;uCWk d79rd8b֩N pˡ#mg,^oA_F7oWd}bK Suy}>^NI-|]޳wnTg;{/i0?xMzWqڗXvyN[V=F͔ko\ZCnW䞘YҼޞ͞&{q;_Q][w4lYQD%d`I& :_5տ܏&9כ^C;_zY aU>81 PRB$B1W x>K±P<.HLz2,fΧM$Gat_NX>z*b(c59b,&(I@'*l9y&[AiH#˙CCwBS,|9h_>k%*OOo߫u!i>†˯]WV\݉[D[M?|dbWM>.$5cn6h(nNab&٩84́ҒrYo_s^ǗÔ֚lS9Z-d`4OkrVl<[UG[6YTLչRb196]SиqGj-8vH'vR78z=#uu|ɤPlh|p] ?&擾i'`Px>Xy'JP}ѺC1')<5*5[ZiFq߄7U'S/-ڼ{u,{4sp]t;wnFl_{3i kDvHY%L|1n{Suy=mUJ`+_ =/&;{w>yf_G|zcM%zMI?W8<ζ_ 9{a`Y"ph/|;RBFE$o`Z. ՁhQt}T2@6ϊN;3?$R@zs7x so--S lBy#-a qb5Hx ? t%(|[QGp%@Vϡr؄ăK0$0@ 7 S1֦ Z *)ZapY;lwa #٤n:Xp1C0gPT&!f(܄JА:䖕`!zc7fA͗B8&J ;!m Kwh! X |!Ӂ# XYrynkY0<%I58Ja3$ca*\% J%(?*bp䵡L#$ R$a.SR(Öp fBOT~o s7 5 =0LwJĹ(` }=JQAl(\G6(@4=4aėw5ڈ^Ni@NQHg55|#.ƒ |}p!tatZ,܇\X#PƉBCʈqk懈,GWS1⎱IH?T/jB,مrwF9C5i >^y>zbHR-ȁSMdj:GX^ }sx$hS Z'zq;j:ox[^?{#s1VO?lRv"@kO!"IQpq&#qF"ԈLС8vN{KEd-/zMP!8|9.D \o=6gCQ1faBXB]6۬Ѯyt/qDiҒD ش AmQ[E(cU&MR+!RRъ R)i"{gfqoǹ瞙{jCt've_w~Dq "ΫBP \JʾX],^X`ʰϪY(>mꔒ;'O*ą]ژϖ¡M%k4=naٜ=3eZ^NW/Sb-P5P;JZljO1FE! EfSgʮ@sĹvM-xn=1 vJ- Ct3UA t>,$V"&ˆE Ni%U)cT&͔O4#P0̯Mqk6;iP^8j hK(gS $}I" 2}:\0?>Bi61}Jۚa2ftSQ.Aftk~mMtִݺT`O[qax*Ͻ䘏kK{z!x۰Bv< <2,]_x DPFw}fnt~Q= 8/ѮP|b<*J=v, JrcX)퉼ldi֙jd1M˼$I JfقlGgkk=Fr_if"cY SOX+;b!nk3:)_Pee84?Nޗu_/)NS[Y^KMO0K^9`ͮ"G a.ŸǷGyg^OI EHaHmh(V[gfLb]&?DwI9hF}a DɛI>E坴VOwI9jP O®4S6 %[6PCT`_^|o6<~ JU7^gRQ?tG`}z=H5R/u(mlΈJ3i)'ӕ]gS],уeGz1۱ 7I8A&6Gܶxxq3?1 LŽWo6PbPo=w ObN@w 7.fzk[WtCm{̉({1 | sY-ƸGi5!3sy˽XتSl`?\syjN=br?{_}ˁ}AP&Be~P-k+ӡ;E+IAK8#0Z[/P kE |k}:H{^s{INLŋ{V:5^AQ]gιwym"٨ ơٻfihj]XAmڄj: jlm H褻sWhUM!ƴdF0/IJft:f?gWcig9=9/X eLj1N _ʗɑ4JÙ a6}O L+Ul-Y?to1Q(1Fy#KSp .>Y r&%I'֗y&'.9!4c'D|JI^'2'a%C<dN%JII`6C$Hxw q^C YAk&@1IehcA"h$ & ?)'Q e¯"m? EQ$U_#H=${,HCsO@ T*7 6~{L=zYkx3ÙRGjMUXo՟Do+Q݈wcUy?BGpRuyNwՁͣTz9\DLkK\5K  u_;Z]&,|~WnK;{W%f,S:J~q.|Na:JxN~WL-Wu ` J% 2orOݠ $Y%*5}ҥ:1Opi#HgVsk ϕBQu<#`/DxF?!?Ss/^@g"8Dg\l^se|'?u^;/&"}rLN'J~~wʼJ4YX+ߓ; w'27Uܽ>5s7՗hQ92j~8»07oǿkxk}ƨ΀,3!dx -wyT0[|0^ycrxᅞnϐG&2.t1~[u{ DW| ľG}W\}Lȗ3-jW[kgk۶g>]4Lff[k嵛tef]#.^貹xRlqWԥb+9jgi;YNW3Ӧn5?djATBlco#[<KHj5{ oq* 4}Q;!R,#{@4æ\`9i| ݂q : q@xA%OKq7:sc< x8Pak_oh'x$K<O$gX)%=, [`,_!cxbɒxz-gˌ5ڸ} [yVe4"@! 2Jmj() Yar1x;D%E%Ö ëWþB0h@7 RGEwlFW,0r43֬#?d8QRbb Ǩ b:ЉvfGaHmrH?@s#Ru4 Q)e eCdqZ,ПtvLdTt^,g3_9=dǷud/M]!=۲8¸KK`W *Fh)Jh>g$Ǘw 47S|1[@"ͣE*th%T/fBZ] ́b8_,V ΍-[ 'l'෵ )>As\uS > bz1=2H&6l>`(Q*%r"NJ+rpM h΁Ne^ UZ'\6]/n-,tJKyiZg7i:of%VW"F ,wm u1 B1c&Yg"%֮%UjU,^X%Abcp\0qրq;l`czB.>jzF~0FӃA .{PLh=jDqlu l5Kk T>p|2a XlMK%]~[Qڊ5܊iwǰZ1,.zj~βA&bnL,'y*n_VswT@';d;؎^^}\Wilޙ!K-Iiɥhy)RW%0)ZIatڦZġ#V(Ô8m ֦V[H n p/@7ov)K΀}so޼7amK:Tn\ݠ&4 vƄ)z zgG6CZ&Lh3)>3ӄ2 VZaG&Hj ۳A+ Xxmъ#)6&JV~ Wp~6 ФͅD"W$,P5|."F>{ * @ :apxg30r ȡ ` bB eMXX56~[;fJbRu -#0 \#LFF(Hse+A59paXVMRUT櫇)qs̤Oo*ˍjSW/<-btz2~T,y2wr2I, ^#kMf6FR3#̆bONgħJڔ!d +S##I·e&3}C$_Hl MUz׮x <*!7y[lgc'CS&&&B$>)SfMAIA,&AD6Z,F? /4@ oiI{ON*M-~$6>gsP$Fp\D6|aJ.SKCIu硤UQh h0^ɉ곸,T mшT7:>FYsh~Vu';??ևBx?W;PWѿ޾oN})_k._?Ώ}`b #;zjdCN4|ee&#[Fw2zI~$c|Kʍ^Oqc!2*1ϓg: tK*:k"XVB{VBk  &\-1Yuu}D9a,_Y 5`n@,e%VBh@}N J&!3*vHCy31bOb8CX0uL-:E?qD`U¨VEH,G7z=^q/ݍM,j*GMntAM 3AdcKόČ0fN11b1~z̉u/8 ye2aG}o y::':6WT#1N0 W %_8%P(a A C۹رc6;KPe- )l0iѤZF bCjXj((kWmZ,mhV[[eLԮg9{{'|>߯INwgRޞPR^MUJœNPŞbQsGlVXBSfIΟIXT85LM}K4ƣq [1Xf|fTFD)Ȩ9FIPTbSU؇ب5U[kjED;^t-)daH' / =e "6!uG\QxVfFT2SSt*fb'fxmX)&Ƽ9XbVnF,Uj S.ū ~/RIarv V-.L"t<$\+mi/H[)cu4S YȌL|9T?WdųoY~?ݭ|<_tpTejꠀW?{|"[Ri;~ۡmoޱgF",mZV>;B :Η}C[s.%mxiL΄bS~5w#8&%7eġ JVqɭ07SR!4"+\vָvKvٝ%vFy Gyzk۱Ƙs-i $KXIMkMj*4 6VDm0L eӦ,D[p[&*^Ts6/i3y<~߃^V ?^(u -uuΕ5D _c!J{h8)-fed 9ՎNQuB4\#d'9ܡ .:,1g,}>Fww_%R%w&HOzD>x0;:YȤ^׬޽s!{<柧t"Uz,lέ.Z{J%P*UKtL`:Jd::YDeSJg+ee7p%3!GIcc (ʪpsYX 3) ` "Z0AZy}=aAOPu 1[VIb< إqD"@mg2 3 d&W:pA2YVz>x@9b0paFxgM֗Nth4}v&ijQdgonVo7 Ǚ}O7-o3*o$qM9wj;UvlSܓq9lҹxF#: ACT#jN N]K\Ձ5T/IrCCk<j1Tjm,4=`"Ȑr35=0H]]ݻf-s43mO6A^%Ͷt)rSҥ\Ph"œ"BrԼ%"P"% QҐ4%-J+JK{q2@ji FZ{2$T$uf@v~ +K"1Ugpr7k:@i:e>8 &ٝDƹ$$ǕB#>?`fYLX&VcKL*kmg/nz/1X'?:]c3@b{qۛ {ҝwq3=Tl}jf%\i @6frGHix*dJQ``F "}nwY 2A ۝tG& YXC.eǰaSvL%HC> 镌 "c0OKV|}^AX$j柼zie$y|dc hj?};h tĒr'֮J}ot뽦F[}6Lñ17>.i/iyDy,xgOF B{@E; ɧFb*X) $ h i0bưAT 0LeLTEd+9 qXl1l֚T*T+ՙ]gd{Knګg;5δ g,ڤX#>䑭Nr^^^$ *7-B{6^7HY;:^A?_nY].৫  QdAr2Yde66L x TڳmFZCI"(I3|EPr+ku<'4ڠQ>@OLr1Pp{r*;26V-mյ[ 6퓗Y숾Uknuk 69H^OZd䅄S@P ZP$-6${X?sQvԑL]_7i@TL tbbay4_+kM=־BTh(4-VB%BcИ I] >lޞÂjqs=ww=cc{` FI'տ:>k)HGŰ ӽ3sT(¹"ߛ0OX0U 2> 6kP `"D -y 0gb=˝AƇC5=4e:3њ6X}ޯxauV A F`@[ޛ-Hsp )ӯ `;: l~GQ>}a4 B6jhD%pWJhj8OHoT$Ϭܜj]0Z8nxvy~lr܏eWU, Y/`oZyh wPb\<*3L 31'lCTjIafNd5V4VZҘƿX.$S < %月.Nd/eio]=…xJ*b_ʣq +B*mnKOKD4wXhf1_ܭn8y2>CLř{h*;t:2ĦFkitnjYzIhlg0$)sŮ{Ӌ-6a"۹?y nuK=Z{xJ`w7[eC<?+/I|i'Զ?=gomw#.`+nbZskMٟiΥ4tH*b˱`oWMSu<+z!shb xyR,t5ub<8Ҫo~xj^tlHrO-f95O\}.8I6 8 bBpC^3 >+ 7܋zC{9-N{0chV‘LC􄠲DT_ũM}gDD>A٠\8/\( sDJCw ѺJTX8\RQo6TzGu{<$x.Y@;@Ap{cam锒듒UW/f${}3P^/ h'bNwWվ,!+}{@z.(iڵݛ_bg7B+G`mL4i![tŭgwVjbn ZY8ORW/ w"9 ~k\M\A!YE*Ą}i ϊ0-R4"%s6DC)gY0qovcfL~3FgyzE{ս^<{;!Z:X5mƵ'5ܠ Ӟ0ju)~(}?,~Yt2}~_ǺӏG`D$ rL =dgL3D㈁e6u' 4v *AAtFº N G0}IK,8pr8 Ckl},it"ǪQFׯXNsj44i=;)*zEXe8JqPNRƂ0'%1vy)R$}rwxVÁߖt_=ۏOJkE˟w,Pރ/UۛͯpRn=u1^v l$G3,^o5ven8@%rx~i{ Գd#we< 'r:s;sؾ4fYkjjmv!,K.$]@O[Ymji=r(s*PZ@J(#J#AM} c߁xD ׈?90{]Mwsbvc;>;p6ƦmH-/MvYueR(ݴTCڠ6ZTRIS7#d [ŐҦ Bc;6Y 7=m=a)4ÍVk2GFL66ah29`4C8D;QQMqB>"Up$pdmj9,;gv2 \ ~ yV' N K$VN+)ʳ q+B^+1 v: z l.e=iwWk}e>cz+!^Ũe#B 8lS+\hj C!6ɀ;mo;ca3v؏.k=a%C6NX` 7am ( y,K6B.5hGz V4vk "iEmvI3IZXfXCmԃ^[2 u2dO-,ӖGZfy3y30P-7?7Ur7rtPjM-"ɉƦC_D"C=/BL,=}/q-FkL-9J-9J-9BZֵ QE?uzbzxf 8uf V(k4YOVJTͬ'iV[CK&f{Q95U"m< A%;1,(k(WG3P$rTN1ò곖)_#3\NG`HY!ߗڄSw?ޯˈ"cwvq1;:vlxm46O u#~2{]S3;|wE < K sJ5ܯ<(J6{)N䢅s\x6 ^aslxV$ Wn60Y!NX\#LaulnI;lva/;g'w.~1:BO&m1 W>o`1b~RnӭvNeS ;Cq2'>'lxO>9  sς'"am3@fXa Z8'nQdOؐXaoơS5:uV\.ZuޮY}d 9CsFtTE `dQKğgX% ,.ɂ-^! ;!U@\8㱙]X׆kdqwnd7ifzu[Lzj\I[F3TR7DXjuKFבet(bbScҜ{z/q\ LcxD^rKpy&o͟#sY~@]nqM[?J<0^w1vEѨWo,}^V,:@I{Ώ 1].3B;*:Pg}CcqY=#ypƳgJx$\s\B>1fA?0zOOLto Xz;e ] ` vq}X/{@ȝ.D(D"^*/wQ(SP(L>(,0ZmvS}U5c?$1O~_4.-==%˖_+ё._: "ҪYݝtI4imqPbwv%=ICUS;m+?b4]q]lm1 C3JI9#<ttIۗ.gn1?V7I'a).M]|+U|󊵗V~rM>UO )zh_xh^^zS_&Ֆ_1$M Z3$b_43a"݊?d(CHAN!KB'>Dk. *KQwL{'{'S!&hPS袀(ƺ$W*QY,`3cL[4h% /#'R &fl>G.G008 4/xn4;* +*aPF0""7ЂVZ j$SI$yt*ai0(PȸˢQD(ňu EڤɕBoj jMkei%>VjZn&kpt8!R$4*I2(zBSM[D/j_0bE)rZv jev}rގ9R=4te)d.yR 4-dM$I2P hvC BR:G*Ze=X-Vѷ,s몥,vՉo yDyĉ}~Z~w--0w#oû[~.g6r<7Z&c[qæ |D8.[4̱ݽ~=\av5\xn zM 4a2I^)E69tu~'tF2 MNmgxV򻌲X1$p(iFaf<ܞmך7ézwC a"* ,,,0ͫyE'ăI?Q  %YD doD JhC!wʚpN -3dץ$ |brKr&II#rsr>%DŘe9+4eĈSsNm5 q9D"z5NCW|kWA*PVdÞ(yl(R+J<[!2uyiڴXW,H$,pG3 eeI+*J ѵw~^q_u)pQzF; n]EC@!Qd xi'٫@(zu{M =٭<_JH/LdqjJgW l .޼Y{W vwk0`^sr!;oq3$,j\o5dU겊>.BO;Vrv,h<:Vr3woy%J~dGݼ!Ibb̠YXC{ Ǒeqx**AEQ+حh3g]åz]PJ@CK?$ڊ bC11,Th6vK\ت˭k55[ÑˍA"aN[ Tꃼyw~;o[SwTCNj5tԙ:e1W19j6Y-+au ae7oL6aR3Mo3oT6O$3#&͚M:d||llgf9lҔiDcy$SSiOM.a\`L{{~A'#8ߩ>P+=wj &S;^ k+rZިWQD%"GEE^ Kߓ~$%P%@6nN$G.b2As& }HB.kۅ<1g#u!B?L~?! =B&Əo2V3/$-VQQP 'a@x}]!P)TU`Ҩl|BQ.1 oAO+12qYGF\/G\3㚡S ŮF:{o;9go;s`Rt@t^qx xf3ʐ d2>E `EQQ#HDNB.>dT!ٰmFl^mnGS6c`Mg?SeepАX y>$8 cs,fA&)B4 I&[%@‘"p]c2mV4j~N62<_ a&WA΃\|:N*:/H5 m6N/|1bÀ14wclkt7P̶f&3;:N;˭=^C)1R=[kFrtKG`DMCwFq]9sg3k{gvnj񎽻w]36Ú҆DcTG: EDD" $-.PA -U Q㠄D4-{_wuνw=wm; GBURl##W F;KvWʴ9H?>[ʾ=L}7{ʴ4JӁ vGT^t=nle mY[F-VTc{00B< Psa$dQݯߚϻ|QS\.[2ul1L7_~{gǼ}/p+$^j1dzxzhz}cة[S|yi'LNiREpB5 2Ous'wt*)TNZL>VDE`1Q* CPAMZš4 rIZ-9 2 .[TPWT"wTͨЭYqBUH47pX`tFu!ӥD/ %FvtvHlsrN9@zQ.PLӆjej)J8t{..T̮Fj"hcTa_P.TǶS6K߃d\޹GqcUc!Bavyl[Եxjo-HRz|a1͑?ECs^{g5o=m}G$?}xg}#oV* v#/֎mĔX2$2 d85<6M0̚V! UV3.aD&z:dCYJ'+eMk~:Elя[މj!ᇧ cwr5 $1 6:̂u-+$Ot0F&.X_L8J?>.񬔕ҿ% 0* a xXosk]>sN`3 \S4T/}EV={,t+t/D[ERFt2JDz 1@fe7`'zonAa@ߠoщAj֩V!Zero2Y ^3n͍ͳ&Ȅg[=W?=':!PY_ ~Ks \LkI8q⾸W4AMme!YqY#qiG;A^ˬ")Wdk*+diq'| g;3۩av;xBnah?r!^&M ύd s" 2 , 8XԜ8Z/ޤ5--pY##3--uS"HDRbQ<ηJQ+Zšbӡ]Mws{9lmbv|Il$\ '!%‚t"HԊJ #TjSu2V`DE7(hH'@ SRUUYئݴy{>ߒ%Gq%$uBK'a'a'a'a ۍeQ6p4col)!q~N]g]ʕ/"ޯ(Zծ.{QJ I}n~Pz>f'.[%L0ʲ*p$bITe;_YOPA/ūWHَFCG=%xk[[zH;E ‚0!IO糛d::Vkg>Xc#|=Vfnz`rXw'7AA±(}}oO^9V#}pĖ.MM0z,5f&]ake# 6~^Tq\{#4OEK,!Bh]'U+40]. C΂x(0+AtpNo#9`NIDazbN_G@J q sW$DR :̄a:$|$3I 3*. C[]ovKP)#]Gp~C68-8Vu5lcnؑ&ؙ W3(K[rWa! υl8h3*}BՏ u]rzuP4(PEl6u+]KkA9W{')e>)0SGJKէX+_jYljRňQreD^'F&vSoM⍟0Yۛ5JGcRJrTջAnPJsKA0 `hERݖ 5atDC^A/ML[TS(ߖi>;RӪQ隮4[:(bF]#h~Qy*IBK*.[2K</ xߦ:fh@#.ЙE@1 녾[ٔ{O"&w=~3K5\T5觑U ٜK11 Rpe5|a0,^;Ln}0SVdf3\5aڄ!6A,!kjKQjq-qF>4&='$AUVH)D:|>48-3#slɝuaؖn#hP,b$!Xy>y`޳aX(ޛ>`"}DMvFn 8!@0,Rȑz1 '\nme&*yBe38dev5^B$XCmrӅx.>EV{[Wa =Dobeݑ&Id/$8)VF @s+&+~{?rg}Ngc;$n쐀I3dIiL ԭk&N d?tЌN +k!mS:fE`SiLf}ϻֆC0t'eL'01jXkwx L@"0AV8M܇ 0AS 1B@UxL 3\̰2HTDIOOp$RTM\cY䔬2G{ Xʦvr:Y^]bLlj8nak2TnZ'L‰gRx[yBEqh5;>uM3eh\)Ψl1Z UxY.Lt5z4v\k)3t" ͖žMwMݽN?QS_\Ӌݭ_^{~_g:][^BbrkJFpVu3=Hҁ Z%%`cN) ؚ` M j dU:Y(pk B^bWb7`x0=mA^{1pBo!X9|$mΖ^qWeU~KF]2rMYK &Nܼ43}9NpqhCj:&d ց+8]M@ 5iS&!TUM?Zi%- f09s=v=w~?u_0~#~*& X7g4&0`5;N2KF0~2- 0xF7^$|  P=S˪)-@*-c>/p:Dcpc #mf]O;"օ· X:k=Jq DbF #ihIZ6JscO߈yvi>OM$#& &m&6Qbe4ްS`>aN0 Ɍ^cA ~H(:Ӂi~$eNI:N4F4vrE+I&;%l0۝݁#Ω|Rmς+x "S.r͸w.pY_K,S6Mm @@&*P 'OInVl!a[U ](6s{(* ƒ N0ɻI$K s s fliFOzVgfUY{ޞ-0(:X\JCsl*L'繨+$VuW]aƻj:/DTgf\Xd:s+Um2[>6k79-~P[d~}]n7lu"X^,pO 59޹Od2y@nK|e_'wR96؆O=95>Gֹta?>tD"N7GbcHb$Lk/6UM* 7f OP=Cs~e)<9Tځ sP9(fǐE2<߻5' `oUO?ܗQ-tCm+ә%lmM 4VKUED I9YH(gDT (q<`W4xgmV_$@KHhpP@QսID䠪'"QE9O;(Z>,AԒ)ARn/ F aQd`4O4RM(\2\HQùGSDPIP1m4F^!|RN *ɳ2 '$M"@e?#|c Ġo2%hvF(ii A>z@dZRN+B2,+(uw]ȄIJ9r ޱ݁]}:V}D@"% Ӈ]T( 2AJRHwғ41g HuƈȢ,Ũ ]ǎKcdF+0BZ4,}C.h#w6qqsl]/qs!x95IVQ mh ԮHTSJתlQIDKjA%TV2ڲ ]4"=.{3tyƠ rSEfe?L0<H$UaX~ȰL1,3}o4Vr!CH FĎ!ִ$EWe9{DN*Ik(w\QDw^-[rqdu {mi#u$ߚ[x"0? 4&0 pyq$Bjh~TH6c aȯ4?2V4š ]v*M1aLIbٷ f1$RX "ww4a*r_o{d޸i{q{sd͗ͷU3C,0qM\$@$D8bĜEhi{ [yllq@u\µoʔ.XBt2 +N:;{<A Gp~>瀓bl7gOO)ӇȪnP}] {kၚ5K|(WavGxkCZ P<-jNT9Un>Enz ng}Rw+ xb8dF yeB#6(9BV@ǂAɾ}>c䭉OR[F]væRnS}9u\5·ȒtRՃ:֩\izE8 ]D/Bt"s Bd!@>Α0YEg&(e$l2,h!]M]0Yߗ7 Q O~rx0GF̎I$1)TrJt:F|^;(vT\?^_^Y~i4 N?/쵻]Q,@m׿U ysGş1I5b>k]hn=v4jN𫗻wv +TfGQa:A';"VǸJa;J'3Nf; IўY ZD3} Zi JE*H6uW>mD0+A|dҝS7ѳ+=L,`7U'F|{CL:?v2B0(^k6[*ttiHmMD>4Ӿ19$Fw}gߒq>ᒘԱxya`PT^4-&+T`TACESTuݲ ުQM i[Um9Tݒs<Ņ:=4 aFx(P ɞd<`2dsnUOWYc;ܜށzl \ ?hu3*^X3fq7;[ 2_m;ҁIwįxz+r-~vᰠvi&2R#{Ə48ɋS[J`x{GE )/귿l]J]3?m?j/LCQ<\R _E8!d +6o`iA`S 88/H'+ll`mdlOyW^v{nAr˒rGכq͕,c)1쪈]OɆ OJr"4jŅ"GD*jZ*zm|4vp)-OT<ڟڢ'E F_@-/^dg0*m>3Ȳ -t])L4c͖ܞhYYٜ~tyؤE֬2Pp!$?c-f`w -i>τuL&{҃Xx"E}` hq0]ًy`6eOT j!Hwwf>ImK$ drץAB!qtG*T`5L6_8`pӼEfd!ߒQ`GX$&9 Үk Ҧ ] ʷC7YKVRQ.aꃘs,.jhnNM =B?oN笨(^ǬNaR8c8JV> > 1Xnm xmXE8:$K#eQ\*kB]+_.&SJ#~ 3<8˟rkuz8ۚ/j "(C=8u8h &sRbiLn xnpD WŸɕ%1]&{}`jLDC t\^y{?$>].q.88\}NpM2@2h@TjE6"*tZHӴP*ec?Q!jҶ@?G([P'E-d{P$f};[lA˗TXЛmLjQjn$^ۡ0yYPC]޾=22bUUqv;p*~1"A _b!V0tj#=4QPal׊k#PO#1"e"arΔi\-O^p`4~bۃ%9wu qRȩ`sz_: <=%]+5YQ2S9dn5'8dŚ_ pΆvauouf҉9]d3jm 2HQ<l۳s[L49GqQTZv6X<_Ox v'RR('~£΅P[4 {bźNWR-=xᛅW~k_UO/Ɉ=RSpɤm/P[bQӿGn0xlBLd2Ѽa1MCbAR!,DTh1*F"TϭLw Eus*E#Iʧ2=.('K2aX~d)3lRP1d : 8k6hƀ>ƒbxG 1ackjDf48 )1ŀƪ1 7il 0nÌ}&U{žyRQhidKQ&k`/?CmV#m)~y+d󳱶P,F9nVC&z{M,\ gss1W>Epm-b!_M0M C^ԁ{^جQpd 鲩#A^S +ف@ai ԇޝܡ4G./wV1=͉W*T<^Hh^=~ f v LG|(Gzv $w'~-gHgᐖk{&ƺAVM "l]rQȁn2` 6܉U\f}lq 07]Mwslw۝}} ؎c$Ǝ;#kɨQ;ކ6p Z/R 6Mʐ6 Mꐖ-M4*Hvզ= c{y̥wka^?Łyr͹9ˎn(e dTTϼAWvq8>!.էڠbS1TANG4_hF^P ujǴ5֪=/N?}^^ Dy3޲.oq>[Ԗ]YD$~2}}>Vpwt$ч:+zJ:=LYm8yPkV$OJ^LLZIH)D5A)-|r-/Rfct$b&t!RSkskwlq-cP0Age0Pl Gl6ck֖X[ l.6mK˂l1;AtvLBm9#BF5D^v>+ȮxRdF!f'mM4;r#x-5?S%6,6'oᡰ[e;p|cB߆qcph}Yc-o޹unA`Dw DvJ(`O=ޡ]g. 0,#/]ld?f60 z:ziP\NW< qfõQCL_ˍ4y 2(Ȋ^4GZղjYԓVjHղAz>_k_#t !K^VZpNp@ tPibn%QTBo:~xFm SrF4v(MpւZl"x&ӐN &aMQuʪrIĆֳ3${8 C6808`h^휌3B_Q\ aNnw]#1HOv5)08gиa1dz kJ1bӕ̎*m &~S9 =='";17tC'l?@$'"ĨSDW(b3TL~3\I>Tx "̱?T z]/MR6W2,:Ъ&pś5=-s']uXA1;F' 8 t*k^.tt!t-u$XKup*vp-<~ٹ89?D9T&E|zj2u52)95joQl#V \YIy/ /1 z+H MrjfQ^v\d>F^ O1iWꠎNv ɬM]+h]i74ήSG5,BPu*bU-VR̴so,R%VR^K8H"Qƪ.Q|; ъ0so`1.,^i[/LamƸ?Yֱ $/bAL_IFf̕  . C+{"d94o+tYBGU)]~|Bq/59c5ߴ.QAś"'Oc̠1L0)"N7xR]dEu/5 @ՕJzT(vҊ줷|̕CSo?g=Q>~SKw?Z޿<5r?u! e[%oN2Qfs:5X 3,ƥ,xDJTP.XOJ&'t4C~JG:KslEP˧;3/ƘZ#i<}_g606`nj5(&%N@diaMD|t mH:PЖMZmQ&Ӛ2Ni1,6ʶ"mLkgФiɒ}!٦EgBfԘe|+V>'3FOK,{;OdrGCOʔb ]!;2t?ČVj  ?Je?-/eP>atVkr"Ũ|fuqQ˿oS)r _x 9aԣ)QT@ h2 44#hr:ue'$t+آvH=^|tZ)i3iLL73B%#m9EM!"B0"Qnɪ뺋Mgt鍚sӯoS"?'Jf&9RK/ˁk;CEv-!!q}?z~9K.2 >ʠ:ܹjoH(~p㭏;ώdO~G)nڕӎna;}*qpE+uA`7b>J$ʈH J#P]a%6AOD!Y(J1A}I5*O: Ѽ^!V={0)yrfЀUk)_ ̳*|Rts9$8C\7qN>Y2xFثʓ%tT!N`uuKX,EƬѳtđ`m*ZVY8H/|g3N3!B J[Yq?ȋߌX&{dA%fiPX+-)${f*GG'd4h a1k"~ sJyxd(i@cʔB*HT ɱMc| b= C͔.t$.x:'qa6ab2IY.rc``ף-EN, Lϯw6BO6o]`IO)z/g%fSۯvdg4n4=+FZpf^oQ?>VnKSQ0Ha8ԟQziw:*>|Fͭ4|~T[;a/3;)ai'HվP֩oT(T|$DYD(8 dg5vIS k>Kz+dE ^ y%zsҴDI{xNJң*ԨNlM:96# X!HW/ x=ZVB?*w&Dm/KԴ>{rݛ?4} ?"3M/LY[k~8?un<n;?s}.ҵn3kXٌiEhĕ䓻YS 4@|HkP'Hh"a {(,ۑ6Ehp<)rrjcq(IF@x=Vd)uZ%jRmS{TJU%({>ێ{q5C{ӻP7+fEB{X^X؇{}+ʢgI{ 3G+BZY OӶ8M|<ύu3ٝvm׀,{X.ŔGL .<$TH46NMXhƍS)Gu]/ԤRWWޝ{uw;(IUQE|OcmTlN+@G,o ;t&1J=0aa( CA@c.*( d~:as\j@vnuA]ʳɳs_px4iGx#p5 uXo/C:Ouhš%mH[×C° F`xWB4$D 'UQş??a;07zq0sD3xf3Cq9#5Ss Vt8m#@8='HX\982XjX9QQXFg 1=spuGS*M`:ARjF&dmc"IA-S myYb2Td]EE3m̦jV4~'yckKyȏ{e+LA/=q#7J;־ko^|mX-o{+{VnۇL~#YA3 Uq[G'bQ2 >ӨOqmςu;ӹ*&E;7;8?H; .:P3 P4EtRdi,H 6غmfE3i&J fȧAYЋ]zNzޥCfQ`Iҩ*b@ҤN>JE :S;S8qVQ ɓhrPCVTFIKΓPI3ExrfD-6@Hvw5b}yAтՖ,iኹ"ۊL*xC->*1+L9$z!}#ȉ ' ?og#V}TZ{ $a-y,tݏ ]PƐ^ZB)+dK>/bߗ8aɸ` ^C5y?G7~MMWT νycUT3ib1t#EcpF $%rLʬHC2{ 0;/߄&ɸŒ2ZXsƷYZ-Wq]FB`+3'ipZ{zoDi ݃:SDĚA5i }Is 9@VA(E8;OnWd۾ Nw|]x;9gƯNqe4Ύ "Iw:0~n&!S!Xaφx3EBCrpJ8DdY2j :@/S77ȯ =L!e{Ybyﴁ,mLtv"R#"zxyC:SyK3|^$¸r/z/ȫH>I |،EZdopuޟH7r'<7@V%l|cXRIPyQRm]fiDG$. o q%o$.$PQ<6 <5Z221Aڸ+]BBJ,z9u5h+E\c+wR/]>qL"H -v<<S|)ZĄj9h0-ꠊ;PE%LJ;cha?1,,Gl t{>y54:,ҿOWMZa1ݰT,h}'Qj n]ɫe^ʖ׶$ YׯH1Q܄`ǏLH]JiBiL N'Oycg @ MgtxL3%nC'{e'v4?h>ΞZx_(z\8$BtLhYt^*֥.JrQ +ʩHN!si)ؐRPsm<)1ՙHwlqp΀ n(4BCi2kյï{f/T,K^s+3e|#f* Rh4ҵB3hL̪kqٜs23֖bq|ÌuEf2Ύ9U1|*B%[Pl{|l40hJGQ͈d=~G. 7Tfa_ !f8ɯbbApVhv)Ѝ}:[f R<xk`ЦEiP8EqeJY=t/@dV LS2idre6݄})I3MLr^qG`*գpQqqRXPYa Wz"yq(RtZUROt %f RYͼYm5Xøbpjؕ5ø?b8r |q,LPZtn 5X,,%b,y4)!b 㾈1Mfa?>'i@*|*MpLl8l'YͳjG @k¿ VJdkH[%@ w&>RBc2c`ώĐ% M LM\DMW&O4 ?IjC, dgUg36Ef֮z~`ߏ1!}o}{ũMr(mw__JEeop`yQwY ̬!?;nHִۋMFda!mqۂ:Ӷö +6.w$WQyW>!_ʂ$"/:iqxX ^M*GԱͱqqՑH]O\'z 7"Fz/ymw'.^U/JHٮ EڽĦmA~[[_Cz~#qi^CƙU#p_x(oDx*qK%@ڪU_mSשΪ*tЧN:5?\D}D͔o*+0υjw^:$KDzD"0}9/漚m9+sH3;yާ޵^&>/1xpmhs(C*O$Cu T5xGA}[SI)+ = vO&^ƃG uRa*¢J w&U();ѹc$P~GG5-ԳG ,AfTK4{zLSrC9^ \ҀS, 02MebF;l'#qt4kt$˓Mth? ;P]uˡKy̸GcZjڪK'ul4n4sUl;+g9y6.ٺ|՞0WpHztC 4DzZqua8bgK<*y**W[a*z =R%奙RHr?bZ`9:?`Z>5t7@ƟȞ P@] ࣾOʃ[@3UDKbN`7X7I}6tX 1:'xг`K3裪vN"u`}CC#I$7 4Ύq'4V_\CAU+"!H"$H"$H"$@plD'Ff67&)kč[x.Fl»[ܟ{AzH `S @tz`LȂl? I lOد6(vҘJ|Tz\V:Qpu$4 4;WY,3ĉ#(_BDP9qB{T yvEJHRli3~3}&o;.!|')|BP,~`D m5<OSA#4ܣH[Bx°c6#BƬZ`YKr 4kLHlsy*{'*s] Vyhg(v>Ws /Z܍xqD^5 ƬķY,Mj\R]a)gAHf#Y"yl͊\i+q5c^u({5yK.`v*iqṱfٽzqqòhUװ癋O8.0߮OWyT<``\RXQ7 ؑ/CWԸSos$7 "a]f"'1k1 a8Fyʗ>O %4 m/}1O `B6Ʊ O OFR-yU("R$Ф @@ Ɉ(ׂH%4#W =Tp%UD c$+T_<煏5rS=#kt5ovO+ ˌM쮶VOXhM'_a`PP$RST/GBoɻlq]X\p>H[hW/"G!r̜oKoЃ,@_NϜϽp=2ǯ,tkG_o_4קR endstream endobj 393 0 obj<>stream HVy\3{ [nD o[a5e{,GlZvm@EX5AmZCU+4h*U~g{{ *(Y.]6#S)+mgKX2sH/ BSyODR++"'`@2-B q[!2'd5Ӹ!ì/+v-p@tdшJih{ z{w#tx7]&VV>9Zct$t:yB$!q8Ư,(>b4Hhc  dn@b=1oe!YЁ@ք{Vǝ ;`a'#:z y9|'Og%i J}7y%iJKZ$bFA)79q`PXf6Rzz$l4C(0wDB, $Ha,*&!&͗A^7tߘK/v~"5 ]sv]~ejZߜ=yPE咳nsNSXKu Sshacҝ ~v^2A'vϷ}z,?BA7ݱ/O%qqAا홍GKw=<q1xacĽm6j<.p(e>$:46%2HJs^!_Rfвzs1e)$$]BIBch(GO$ijLBPt*5%T;<c\_)Px40Z͑X呚,9gJGrBCDH8i:%9^3Rc m^;0 QXYMMl.FfkT+wI)ٞr8têmQS)Nrg>](K]dn'u5aD{\͔kfi>rDT D -NJ?^1 HWĹJa+|8O& $z~Vƴiи~/{Թj.Pe/kں='[^WOس͙zuBKUG][dE\o_d]ƻ/6ثwlx Qx0STy52UaNc`|^)bbfrdzޝ¶g_w >޼~[031G/$i{NtGg9>ڴ23X)tum"*3fmSLRݟX7ea66u2,tܟuŚU^+ z6_N@DhӆfI:&ۉQRT+ I0DNSF ]EY=Bz4Y[fQtTVhLiHuEZY:=ZTBra[Ji(̠j)AQI(JJsgFR 2J'6 2YT bv7OjJKJhJ's]zH#RY1 Y ₢!,fZr"Y  ##ߎĵT"۹-Wʬ6*BXev+(Wx"ZQ"OG T i4wg#yOu5zAQ!^\v|F`;$ćAf?`S?4~\aDS" /IHMID%%ʼnfWea^~V>&xG]m-*/8Jx}ԽQg:oֵ${j_mz}Pgl/+,Ljc61mkU͉3D77wd?*}?Teطu}37 o-O7;r$NLS($ ՟{4%鿓pJ>FdۿCz^Hǡ춳%gŶ~~B!14%XmPe` -,r`."(@( qe`>'] [b[RntcW;F۬zxH׌+s+3 _?c~:~N֮ Ǐ Xi+hQu|xךI7ߓn 'n[R&"iowL^3J:yuWpN2y7(OYꖵۭ$0c)%qS+GHc<]]}Ze-V? "ԝJdM0R]Jk$XHRLЌtEɃBA׮O`mTcIߍ|{oÑ w]$bIR$!uE+*$k;DOCہSb/f199W+n[KeGܸ3)~m#hL9ƻXW\.,n䖃oO=PձVoM}+x]GǍbNmxbu³ ڤYoTb5^;ˑ\gjZ/0]0j{f'|e^fvg}}R-&mK_K?ZP{_`睟qPo3<˄L* J |8WoD%9`laq,v'V@wQ*Tj b8W8I)$-,4(  X_A`.3ok?Y?]H">e6v n2ux3#JX=m=q &10ox8ϟ2{grM증41؀zm8r Z ;a 0 3-51k͇Jx^8r9}8,7a@L"0İ/Q|#(cC4\Z|q\Iq=v/3Q: n:`H'`Ƨ~Cu`g`"svf̓uU0̆4 C!mbb>Cb.M (~lPK֣<z.Wk[22Qh[O!LB93wP353ٌXXc^@!KF!J @ δUQCqs!g2@ifC,B؉lhDRSC'ˉbYW7{wW;gg@ }23C>+mĄX8&:*R4ibĄTXтQ#xÇ Y$rA2izC0g t)I&K4Y -K@i8|3t( oMBȌB&@y`PQР1qfv' MF:MHxUDSZ,&HIHу$!¡h4@#P㻸:k/7EhEsnx(jZMM\Y/U{&к l߲DZCy!).HHBw]5J::CQ"uZ?*e& RuZD€Pel=wq2=s{7APACU]Jx"5*R{|ͫ 1`%ːq#DjCR0+9,QOP5űHBc]Z5$ bqT+ X66`%斥1%-"1pПȆ %3l5;mo/FҭEt;l,:5΍̹fDJ_ܚJxۗ_ q`s-Z-}fR V2 N  Ce{jBlVؿo Zm;7cp}"HKA<^u܍-h4qH'vWsjPr`FHXK,>ZA4-៥%d/) DcZa;f{AU%O_hkQ9NDè~Ny-ZZViDZ|{i1 T٢n*W@rе|-]K_XAUr'̶J%3^j4~"/SEB>0LFEJek#%NS%+d%D&Vܴ]JlT6v+#|D֦HѮQEYZK[J_9zie,/QHy::o?g̹{z 2Qiλ+o@|?yTȩ6S_M9۲gWhŐ.SOtf}: |fSX[Ix|O=x d&<r;me7ցn0ؑR0OS#h5 8u+fLt  wڄ]M|:ܨl'a OxPЮRXK ܘfg}3ؘ3fʅ1 c>G'au۔| &ڻĞ)Q>Y| o:to $nߏlUI?|M< A8;w7).u( w9ƁEoa?9^4wR9k\5P~UߣݐІEW*{0Xbfd[gR-UVRuR?^< $.6송[pԠkGzr~}~o2ÒT2X$F @>K1(z5 b>Jo蟋n-*+?<dK?bxankFA{HZ*{i~\שw߸BY.L"87r3i:sE8i~I\ԟ9v.:or`aR/ԋubA"a 8lAk?B 5DfJ䛪M"ڌvq YCsD c+ ~ 0IǾ~yonyAq|S<GdΔ]&iY|yTf_N[3P{A5"+N9Gm1U\H8jyg8!_Bɨgl8qJ\̷o"-?6R^zqv4A+X m:y|M8vLȱBBKH!oBF#5nPQG@L2{m 8,qHi6&ӤI>ĘJ+yЪ%LC0M6v0˘*}Ucsyssk.Կ\ Vj"A1|*WJI{~Җ ܏y+:gv|?'nig OӾH\W pZ~xg=l{쐘 ?gC8%}V+~{7fi [[ G~Ap6$^gi/IB !f1hd KAL')XQX6'OAJ=NryMmdAӠ$q6_|(Cܧyɚ=lv>ggxOS;|]3Â'sgJ/jgtq=Iq8-bGtZ fX0|78]&l6q FВc9f5p$@*Qr`n}k}6Roԅ6]hӅ6]hӅ],#1b#1b#1bWPz B`H%^%O'Id\ʿ1Z߈b-,IV(i)ݵ.oLvWJ;\iWYno+nGnKZƲiW|eUH*$B $tg:ZL t[n+`@heuxC+JU)*UHddzdvDli dE+X?'Z0X1X5JX,X`(qt~|~j^Ϋy4,)ŃP0`xQ$x|stx ij NmG/l^5z&S/~Gr1e+\٠*2Ѧ8h& <.d*pY+?P1ܭtw)GQQ #%ygz:P{D{=Q<?p9K "`x 񞰃.,B޴9dh+MNSÔb7 6`Z["erE\X|M\[l7+f,2s/k %d& Kn@';JWY332SRe%.HXB6D)f6Eb(as^$g;Û- 3BBeܲW# :kej^V&9 rbur ##DQbt-pm .'gSyVor#q6ӳouQ]DXeRPwE Z]>FCF4ֈ?FlGD {^\r8Do 8|u@د6 >k;\0<0ۀ 8E ' 4ilw@!U\"EjrhͥR"ѢQsȥꡕ?ʡz!I=/ *Z޼y}U5#J5?TT]xNJjv_oOF(uyf.x7Q.n_2|gRmԼ1CtսI}Of!jBM(]V:t60͡j5x6U0ڌ:k LCnzu2yc<3o4-~|xhKZ3F]T\N$]5@hlBef_3&S`LQ $LL=XŋÆ.r-xGkuxoKP.Ľ3JxɶkVok+UX<ơKr#c1py1yGKF[4gb9_,/tZ[Ng=)_k|lgKtp=^WRǟ[owғh^M%:gC] T[;Gfz1yquw=Ɩ^I/uN̶򍴵Y '-_QDŽO a؉}Ǜ{`؆9{l8es8 >^|:rH?`k MZb%Xb%mK,K,K,*wQہ>86}&uo؉]^-pe0 Llqg؉{bO4P*( B6@H갉!kM (J `<dQ+j3u&zJ8+;G1QH3ͳ:> endobj 395 0 obj<>/DW 1000/Type/Font>> endobj 396 0 obj<>stream HVyTG9 7*^fLfF93xd@9]f܀7,bg 9UDFGǿ:(8j=7!R I)QmrÐݦ.פ$S]K˖uB+٪mMDEΚJ}4h2˃N>>~vNκe?M0_Tr\Vף{'v8.ܽp U~M; f`^~Q4ki٬(b%rP䨀ݺ,_aN˗Qʈ`WT`cلcqQꎢ٭8mmׂD˿?r+osT/ Blɨ?6;WMIJ2݂ȡ RbOr@6`:ŕؙUeh\¨zh-mPO1Ŵ:G!Ez|zL*֦j ӡRd4j 1r;SMs S js<G57.Br#_tm'\B: tȯ*i)vmm6uEdchA㥼ђt44Τ OqI`=](LOůJ|)w(ɡ'UGlդ{ãDp(c7/R|/F2s1vALb% XdQ3SRP~-QPɳW^:v_Q㵵:sNt]u;}piuΪ)ҷ{ܺ.QQ`Ootmp#_cH"vW$r ^Ɉ[NY2Q֣!\E~IYq섈߰oo*%tEbxBP@_>ؽ^_U{ .w.j>|,ٺهpDh"ԖɏA +2reɹC2fN[&L. ]DK ޲޿M+SR_׾8,gˣnT{;{/)8>xr%Z0Č;Y@OD}zt>jPe1,a063vS1hsmb l Id-逳hHU\ݖ+Bzπzhgzb9ּT._ΗglkvՊ=tU=#WO6OrO,i1:{ם=;֌36_ 9ǖziC7ғ[Zݓf4OIZӳYx[ O L5#}+I68]Lj$z6DuQc >zRsAͽv[]7:kb˭W0Jy#1h|K)׎!p˜g0~S8 GԅWI_49f]VPPgI6dRs٨Hi`+0m~Ad G L']p :P 3-/4ݑ.*rР0LlkiMU#tDoztriC6xV /kj(ˮsO/__q-S.UknPt:˯js/>=4{򌰛CMykk-S9p[w'w({oBɗlbF:j9~xmAu[Q;|j#rt;aI.9D=c[*,^ +)vEĹ r_Igm'kt}8So=,\0ה }YWu;|=J#.2 *h/\;)ZQ SCo^UjL uM³ $ \o;yA ࿺3 ANˈk/ Gv\*v{A b^_ w ⨯ڒ0?v 3BɾP /6#` b`!כթkS̄T\ilH`VY{l`CzXp1C0QT:! ❴&8܆r.В\`!zc/fA͗B&J  m KhZ! H g|%Ӂ# X5:"7k`2yFΒJ9p*9SCq. XHD_B1ʰB5H łhbF2u6P)-ȩb= 0CE,9BW@ЧsBdPWd0NxHRBK%5?Fd9}DNWLDJQCA2D=(7|QaGp$PKspgS?l %,8Lv dQǸib/ mBk?^wcQF܉)12a tgLvZ{ʎ:;5F<47dFD=ı "]T/"|S~p|7A$򥰂^D\g#Ć\DtL:^g ?s5 _|ſH/(+y YGǘk/š`:!F]P**"Ri&R*B2iZ !VM=ܳq5&kBP \Jӱd{۽W4YP_Wfꪰϫ[)=k挒;O+ł]ڜϖ¡M%4=naٜ;eZ^N.˚SbmP=P'JlL1ƜE! EfSwʮ@s̹vM-xn=9 vJ= Ct;UA t9*$V "&+FE Ni%5)cT͌O5cH0̯pk;iX]u_)N/R[Y^KMO0K^9`"G {0b\J;z<Gޙ˓b`?<h{Rn[15zfϘ8[da8ZfTM079%ibڭĎKECt[z+f_8C&E:}sxgp!j>5ј0ӗ:"҇c 6g֤G+KבbqZ* WiǑZ xL . 2DW2ͅ\yj퍃7N$b6ܕciDw*WRH!S5(3"v+(]8USN RI"9B1}|4 YW 4s;!)̞0@%=L{6(EPI+ܷ྘ |rKzF:|WdWEnv`PwTңeZ+u;Q'0P#U>DwI9hG}q DU[E>E ݴ^ZHwI9jP{ O4W6. %[6PCT`L_2Qخ=Icp G|؞9sxqV:(@y~skOi_!doū mΡ~g2La;;-]T:ŔM5Z QgUT/DE2j l۽KA6JS#0 ~f1~kA_ "h؜@="5P:k{Uq 0_D*8{cjj_FzF[y޸uO,b-~A&t )zI~ ף/懟VG|E@ RO|Ϲ:~XA1P[!~jA 0h` X AoBĹJЭ)մ_QR} OWVY} {zb MAT&n(.4]u- Tv/(twd!Eh;7yMr/3( ⨟c:e:ϾMlH4$s-=?b?ޤ}Lx6h@/xlDZ0'h_>+v0>=HuvR4;#*ͥV>q na6sG?qP>H>+L6xA&$ϱIۨqB+qx0,0'^ pr/x2x <ĸK`u6gxcwxoq!0[?ĸޚ{e%[σ<(W÷.w(WKs_G30>E83;B̆#'-s>[jl@% > sz '&;(#O:{ߪ}7'9r0/z S CH=N/Jpmfl8sDQA."f*ƚVua"AmڄJ:jlm HNtݹ+[)/Bi;Ɍ`_e3$]vj?\|V;zב$;b*Q&c&&s/p@\|Ls~NI$MK*'&=O/LNJ3]&sB"hOy 1(OdNªK: _ o9ex(S` .qr{%Hf}ᛜ"c<WGFv`$z7.|lBn @E=T~|#P! é>-ԿRܔ8M13OM gJ ]4]}cGU&u3ߍ}V}GNYAkETs25QYB2sb?05mq=ց/*$|0E/|hu[=/B|&]TMsLI16 LŹ;}9I\q2qֶ_ql60',scʕ=8*ԗi8g,ZH*'p?Y_ U([+ C*nbn(.*Ø3daGw7I;*WWq̽Ԋ;~Gk^GU_%zF¼dziᙩ ^J3 ¨0ѐQ/w慩h, ,G5xaCopbs{neus]趹c;![u747`.e =?|׎^x|/3js!K[gi׮iv|$­$_Z|]>7;?c}4_4 8kX?͙rrM?Ś5",g%4 )>V{sP :5[GJn-ȽPp, AK&ۯpdyA/a,^,=a,]j`zH62\.k^ݗ# 5جJBh"[Z.c.&>nl"XDW֬Kk$"#l ;0"sm]l\OQh7sX`:}>{uȄvS!8V^|n`&P#툴. @ k1p|o '[˨Q QdPkXvp ;yibb!&vPLrW5׽MXZXk?>9u+8t#{<%l$Bg $f`cʮy00 7jn(jl@%mB[i1>4:lw#5M#m7MӦHMwӤwι_{S_jbW̓K}H?G(>{*}DA°J=Y~\&>`VBfJ0Y>܂DKIZe;jjlzGwXͲj](/ns:[ Ni9 ;N$MNGD^-ÂzI؞L$Ot԰}x=@6qȸCC+;M«6G@gv !XT PTTɇl.m<+ضL§9#p5fuvaA%lno΀ϖ s^kjr_W/5KkQjj^EYKUEvmQ[zC2Z(K >/ЀpC/< J HGǒ Uފ@Ewő gVrV>YZVg!'gi?XiO-ɕ}' ~L6䞖F:uW jv0s9[L^0h&|j J)˝Z O j=3~ ~@/g`oVd包Q $_S{>Dž>,b(Wb1t7PC[cPk1!FcҲBayX,la$1zϼu~Lo^'|]'⭟} Ou㧿onFkCo<|C^Mb'7(m6J7 =,Z(EƩd)j.x{/|'{4>VJ.ۄ-_Vij*=}<{FN s;lx# ZaB%V*?1FprαN<7RGݷUlcm-bKmbM؄B@GCWַA(1*arr,n!!@D=6 t@BJQj^>w0{'{#-jm:j%%I#<#CY-]t=C>N=d;h#At =Ϣ,Z;WIϳ̓Q:zy'=O1"cqǰd7Axƨo1ȋ=x!x|8HvN>s] O9/ KLc^1!hbȭXoӹF1(i#^F|a v9  o[HvWj݊*8 'qRR|+'7+pb|Ly1kB(Z#.(򽠈IS`9)ﵵgrH%|mŅU\[P5^7%3zwhsY^=emdt pHLLD GWR*`4(ρ@,ay".-$2y B: Ў'<~!C F'U,UR<r eAаՠ+W]HVF FgF5HpSäm8J:ía{0 LuU&0Wu8s/ޠC elթCmJm7i pǣl<[ 5n/V_1LZ' a%fkJ^ˎlu.!lov[^۫p{W!܌]޲]y!c7z;b괯|xZ[z绝g/?yJ#PoUa] endstream endobj 397 0 obj<> endobj 398 0 obj<>/DW 1000/Type/Font>> endobj 399 0 obj<>stream HVyTG9QAn!.Qp Ce jDqA#xE'FOTX5k$`VVLհeصW~UUYJzU{>$yWS6LVqHi0 O^Pg]ooJnA)p=' g,\r &|Rs^E#=. 7g+ix6-=0J4?b/pk7"Ћ<\3IRyf 6BP)<&+d"i7ZK.[I"͚m|fF {w}\D eᑱ  ϶ @f(Rm :۠hQ.RM;K./)]v￴έ //d*do(~V8[cv4 r瞩,gr"?}[~9bj/َ*v^~r¥_5*1qCA[xރ[_9ȬqOL_3-ݷgmȂ Ҳr\ֹ]f ,-v:&Bfw9mgd푍uIeLΊ3'ϝk5~uqrλlN"e>$z4V%RHB+^ }R&вzSe#$]HHBc(COLI%IjLBPt*2FXT58VΜ^)Px( 5XLaXa49gBń pCtJs4d1WB!'ӥ(Gv c8_u7+-gOll5wι6=[? kN8Uq[RIr]Åa W/|gPړw}D)j/8[tŔ=kgҜ~VPTx~Ӈԟ)-]Xt7>>zQDje28품i)ӦG[HE~p蟰sG5؎;{.dʬ537ⷻ>#V^xWovm-?BtcP$tDW p9TNH!AD[9`8ϲ̤_oo) ;B!c:q7> ^J.ױkF~MWiׂžO1ao/6G\8]0Naޱc{yұN{x}|{9yj߲*5+Ps-hsΌ[MR*c_P6]ūO,MYz|맵L8 j:幻sy֞Xs;䬗4 hovl,'>;J*bтj%0'Bd@[DaGHYb& eR'8_ghPM=DL>*,u}G!sLs4-"^5ۯwzֽ.k~9$\3.&{Uk6oir͡W%8ǧ56>ڿōQf}tdLwúQg:gd]LavrTB厲+m3LGىgl0}v6m; p=j8|15жa|[% >ծ)+d`xQl5cGU=!w1ypAD|!^_H<&俪H4;fS a iʥ z$N2\d.If)!b C0,MXcfOXH6$].nhj[,i",Mp})>1lm`$zF8֪2pZ!&YIe,A+!dȣz$$[fVXeRd뵱2r+(|eHcc^2Grpk2'P4::E4]StjP$:Y$)^,PM\;Q1DDBI QLX>~kǶkW jd[=E]]OtoW-8{''ٞ}o}{}{ߛ^ =&R"Pk9mJR pHɆ؊]E R"}Ax[E|3d )\<}yyp#KEqwWM"Tɧ7UB&BBXv`@Ӊ|( $äAW+ n{N(+)/s٣*+ZT";ꛟ_di %Ɵl.!3d GlWt'ك{cGM_xW5gW6|џRׂWL[zוqmy c&"30[q=+x|3y:E<е;k,)`ZKfu >qvxf Bey:\Jİ">aOM~!mtLJr]ȣFuЊBR;ZefWO(gUm>a+=JW.wՋf4S !Bi:.t!(r#"ZBR@N" ,NØbvB.r{iawҧP&G/xN@ѧ@p' qCt': )(1z X) O;R g|(@1nLLXw1eQ ~ԳzUh{p{Qi]ل'4r2'M0oZ8@nz Nu'vzHU~! ȡ%3 4~#dRB>MQ17RuP I5;B&Y)ixNΨPI&:p8OZ.cvxgbLiq&G6[95`3CX2OP$x38T&cji`4 AvB=p"N:t$LG$$u‘A=#-ޥٺA.:sPluuk ᑄ[GBb1! l@Cit7@M HAZ.0 Xvt P [[iA;bôqj-6 {Ϲs_Wv:duKs{=єImKZ^= >'$q| a޵q6櫏>8OCSKSKPlɎ~=.\p6|Imf[; 1d8XNW.qIwKDžɼTjZa=>i_gs֊3Š;+bP;jwDý0ܸOv"OY7Hrd2K`r0 wTxS6!M= 1R:[ X@ ~9ˌ&4P]#6 -nwnEUWZMZ`$LFN$O&NN^09ؘ'wġР>*?ʋj h"-ӛGhq0N\sӧyg8~'#GO lbw&>٭h{\MoU>Rk·YiH u!oʼC]c(l9Z&ߵdǃXڒ19>TW!I~OL<~5R(sN8YVIyemNMp ̓!mokGT-ẓyuiEO9{֓r5N; T_X  i}#uyB%$㧜׎\;(kE6-*9e14\F!ֳ2΁mh+!&i*6D@+N㏢\"lovs%ʑ `s#"9 j# eͫ W防ys~勑tB-ywٰG~e|)<*٨f:zl5A:܈ɸV _Dzc BV (_`/GuЇ\dPs`> 3gG6f͉=鿂H*u6{Cu*IHM" &gblˑoT2!ȵ4 1 ؖC.[9uکĻ:C_.Q c;.1B>;䋐ȗ`B}}Yc/%Q0,o*s[{*a_E3,Fq Tw1?1q ek"`׋߼RCX54.[7DL}Dw 7]PwX.a6w폑xH<" GI C% a3f-X 6(VJ #喫mx_rx@a;XѧdTπwYt$od}2(lއ34@eJ-1nn3:Omކ}{ p1WL3bНfz j^Κ/}/V{:e4*g\N#x ?Wt`>M=34ŬA|BTa@+Io^b_[L/kc_)_F|o+gcj=Gz==Pd"x BĄ͒HEb̠ehiXZעȟ,قFHU;[XڙEk_:`MϢ I؂L;cu,tg~wwuIpv32g%8 Ɖ5@:d oWEC9V'\f;_⬟X:p/g'x}΃W '8;q&c8)xy$;q?aƜ2\f ޘ;kމX 15;̌ݣ0f\ 0Oc\X¼v҇1)59n2o|1g&wnxf {V!'whkXVO)Lw\.(Yw: SL94Fl KvRќә~q7y7wW'Pq@D{g>>9ՙIidw٫dE44jmn̸!e׼޸q+*Ԡw5xϔCu@M '%>ȅr9@\UFLEiԛu`ܖAvq 15+Ǿ^*UG- QrDKAk)]YEc9Y CGT.(;>](#;S Ql4y٨RjP8:}z~攛v?υܼ+<~vwh{g1 .?E{vg`  ;{v Â;UsN_,b~2t5VX!HstxAHond6LmTՆsr+Q[li+6VX) V10 V> Bc4 4d!s׾KMS}e|mw`>*μ5E?0D$SkhtB[-7r|ܜQԎt ˏ [?tm1S]F-zLGs ( 1%_eH?|(^$JOz6WTlΩ)Ƣ^SVOAc2 NyX3v+^&NkS$#dh%敔rrh76_#7HZ jPM(MSV.jXPWp!3U3u3-33Kn*fơY s<(GH$".RZꅧ/[^~f~D!x!?yb5?~3ǂ<!7lP( UBbUVC X.l3#d<$,76Kkj J@$0߂6K rDRyեwtǽ4Lͬ3tOU)z43ܘcE%Ȑ"(lF?2ˏ* P q6CΉ$Nn,ɍmzPn#+Y9sРD=UkdR=ɒLat2?lW}p;IdvЇt%;[894$vd{H'@ RLUn:DžiRҺM&N> tLm\(=]޾{vwAM㡦6mh,=hjx>ÅaxhH2cŧT9!OEf/<gغW"1T zC-d- =9^J-fb;mɮqKqO30#aH D]$p$V"Cc}VLT h$<>&m2B:0Tz{Ps]냂$͚:[zށv`ۡKt1Y[Bz^CMT*VnLIkni՚b` ˝H቎Nקn`^wppwkn~-)V[MXP^bN)} ̄ĄاSʄJ)XeR՛$2$nTҊIv(F# @R:$YN@5W˒p:E?^`E0@E_\T5+T)U^Dk+T.ZZ+ԕ1iF=\ˣNtp&A wfK#HD cuû"xڐ6.I854qigs_Z88N Wx PwR#>9Yluhu4#bvv V%B֘+lAլj`(8, tߝ$;Hr 9$1$iU_3><0g ư9 Pe˾lKܾ:ԥvF|zEз*  ;о^[7Cd;I=OQ{R!Q*6jFKr%HWJ`ne7|=)XK$L n2hXzpШ@ۜ/"턉5!d؀SyРlaϥed~H>iT~nRSpS0٧f?5IE#/yɊ8+K>餵NMȋ2^_1n>Gνi󋽖0ܦ̈́5?KPD⽛zAЃ%xHިUKR$07(954G5PBdЁrhygrB"ʃe\od4夃1-4|rˉ'5iWӞ/fTgf?e1 B+ +k.lLBx*[UUb^`V![>~'cU&̲al/_m7~tc="Lkiˏ]ۚ\)5-e: 4i\b_L|ۡl4pf^On102jߝs$>&N1iZ 06*(eH !14HśnFjVeTu޺6N4wg燳tpwO>&(nR͊Lvg:N'ҚkF>H鸤7FX@.^0m:t, k3jmT!20[[S{j8QH^E5[8I7Ҁ5}FsW͑8^F#1ZA; LДHTΘ&^|,xӞ3}g8rkL~yIP9h3LճfH#Q4x#X'vG~A6D  ![\Ċ0qS$I>tN"$~HK+*: Z97Ou<lOt5 [ [ܞXS@ ]/K02~be'].յ F~+ދDc:d=“~ɦtƽtr[ N f+ӨX`+_3l.2<aDA6-}=^L4v;"#ɣY}%"`̽l%6a1 َrNWF@R+iT>r Ɲ~һ*KvV/E,K xM5@Q9DfaSl; H+3t}'`޷o7lʞPH2 Z<@%:jg >3: 92nuhCʬ' eW1ȏL"EĪ6_|HHUveSMP}-|*Q9.4=dքQ1~ Ý(aOXUj[y9gzZ84slb49ۤ+AL :~RtJZsQE^P8Gjg@a,B\aہ zw,B{l-ػ S΀L?{vA/j퍵Ȭ:fKQiRBN,v d `}?p:p>y0slaﺫQwxDبմr^TҞ({|Qq4 Wyc^ӧlzm2a؊mT|%7~x;MY`ԝj(ś@%^_Tg}6 3q +U_K@]qK1],U*2je5*c)IX!=`eHI眊O!0D2<zUTkaE:j}ỉb[;qcVL Uog /ի~{Pd@RO{ֽvYX֫%Pt:L2~ 1 /aZ*AM=mr0hB1ֶ|B86uG9umڡuԳyL0ɠ-o7 y !;^b>Po$dIY+ iR aţ()ZlÔ)Bbae gql'2kΑU39dGDB9M(6#< ô qE>J0/B(!qz:trip^RӦe/C!PϙcWP:3r@~?W{lS߹u7~ڎ;1ӎ1QIx9 KfJX ^0@@Y'"PFm(6Z:-[' iqvM޺k|/wM~[HXNIUԨ_2 Վ19TLړFN(5ps5vuNҴslv=dE\t%S-,7*m,Kr*z ?4D M`h[eѯw?kXJJR.*Yqisϝ C\b:Ab\Ajۀ3W(zVֈpJrbtz -C?2r]1fX&J\4ȧo0㞪pfm{FW1p%<lnV/} `F PdՆ Ní~ky7EsyXDŽg٣<՚q٬9љˎF'̌ )UW͜0:tC><TrR)2ޜдɘԕŌEluct\Ԛ L 輋Κ2,?z2kBc?)8 a(Ұۿ^M_߾زw-2 6V`!\TjۄqG/0qש' ?.3zc/ zsvZal@nπ_[* ޡۜR</D]`V V#5 3Gf9ٓEzw@pX"ݢ >N`5OYXC8#aa8~ _B$qxXd9bZ4```Z 6H?K@r< Id"[/Iz nI:|odYdKdYe,K&!An5Cl 0Ҥ4i)&5@H/L8釔$e ɤm=NdYZtٝι3d 3ng4Ưi&@NO5\rSI2r9䎮^ w+i:Cs[8+,V3Ąi >ȀC (p 3A{~8b́\:=7]9_Yv9gx1A.ϣNN+`LWn(zHA ,n Y74?qۍJ.-.`rrAr݊;_'}WeO>*.28%$Y탁u~Dx6A(dP1 08WBxXIahM?pfK .4FqR1E#\9_!{M!z,~2"#哆-B0/wLHB_ۺ!3e I0\+jmRGdmr^rZ.n{!- rO&km[ƀ㢜Y/ﰆ63@woOM?,2mG<a LNs썁Abh-h܄{+E+*Ae6* ъ2 ) ͺ *6JT'3vv(p\BƗIdO_KD^T Ch2ҴtFBYҞӂsϒ=Ԭ&PC%IƒȚ&|6;h]NdTrPMP=oU.+&8A-rlVhk*L^zVAuJ)4~$3&plL>S„dk iQIjO -DA(^Bt8yEwB]H+߉s |ΕHgAxP\⺅Xc[(`lkm%JٔU ҳ$8# f*,3w.2$.~X&RNef8=j0?R&Kmo v [Ŗ}7}%Ɋ5ٟ<,v84ę he݆'Ю{W( R~ > Hi'!d70R1∄" EL"[mTڃ~=lwhmh$IY12=Oi1T gY2rd d0ZOxkU$h{8Gy^b9"ǵHc?2 f#.(Iۡ?>{7"u Z-)ŅR*Vr0.)\Fxl.ŶH'1RH2eԋqBˆV=`$=ч6G[;BɩRfͲsp'Qj,/M/v&Xt,۴JX " g7Z5{r,4w:Y8^mh)Q DY|s59;bfqOX%3byK'3%3g= .oW ]G)+U) )O"]uETULw c MOi g=PƠg}ݞ@ )J#Ǻ_ND,N˚^fB_McwHNT {`@Q3]rw|'Ӽv0g },{1-Ot㩓hjӚ k2j: ᄑ׻vSnlbD˦GZ~m2o6^ۖGx{K^ S\?$^]Mm;>;w>ǎKo1K0Nr;8)$h0cvBaZ PKF:l ZlkViA%u[}K'MwwEtA )gB1{*Q끣ȹюbڢvH8nxv74Ըt-'x(q9 m%YdG 4]$B2FȆ URN#H pp "B*jV--!I*YEc4]jvn`&s(!KhG7xLFIU0&SȏwFqzG/Ç SwgOY/ *MGa-EH<;b qpuxu y`Rz a/vu`$I%R*u"IUv8^OR)ePb$WTeSo)J{B#fωwrbN) 5@ ìo5,D*VZilզfϛӀ>@ryun*CZeu0CaO 8l,۷S[YvXCq%2}JBh?6~.msoظX.!e%XgbG@2 dC&މ$l{0=$?ye.0dFYY \6ůS y+)/`lŮfc((D7D(r]baxA/@)vJfX~8 \/p8` llc 7U>+!>Ó7[]~]h5ClKc`PyݺX]y2`E5u-(=yT'1 Y;F P6%FoD K#bԇ葈GUy޻',IK4Hrfr%1 JڠӍcA.(}3 SMd{'bx ݩGZ:z ]s[R8*Wԁ0_:[['ϙ]Ĥ%u>K*W%_ֆzYlf2Ti=fiV-ގ=sռ{v][m:yZugjϽ˟TG,agco fiZzw}z\_7E5 ԺE׋*8zd[+r%-g4 'إ./)-2>뺴5i2̱ۦ ƇY7e=ʀac g7GgjܡKu2Dr'/~Rcip{".\i# jp[b UX r⫙fݖ pzل.M8'n"|1*D Դ]??DÇ5"O!cI[B> }=%[oL=r;3È3?0s\Nk#.P?="fIxeg el|~)Ԃ $^|5-[ua c' K$vm NK|dH\ntPI]EQ x, ^EGp];ᩍM$/6bMb\}}N%  6!<<YD8[q=x*$QK]],fFs Ev/VúuANWA [{VQ>}MEܷ|jADB¯._Mn F9g{=V@J(d.غywvp+yp./@ŐFb_/~NH)2W1ڛX|M N$<<Fb9Nݎd DAJE48$9K[4nc4h Z2Mo'YlnVoREY ?o׳٦24#ǙdS)h!0PK}k~s\ b3-x6qqg/wv_Lp]q.vR&Y(/M! kqɶBF)%Ek'эRhD4#+X JSgec/,5 *<#%K~XƅaJJ 42V滏^w(::UnG[G٪?v5|э؈y㇡pqscs^`kv*5Rc.+]eVy'J>Z&|844܇M'ša 27C 8p0ơrKP:0ϑٹK/}[g8!,P(/ -iZV& TMZ^RMʀeT 1!ҦmT݋HeLj@JHXQ[&+SdUJgZEoU Q5,]W>>vgFVXc׵#,O󝼃+jw!PxF)*UT3A6Һt;6Y֫ߺuB]cW]܇3Je?re FYP}Wfn6I1oq彬ppdl!UëoasËGKOdK\K5&^io|9dW׷L䙆DBT%L˨TCJ\'$ ZKjNɠɒgYWJ$mIy'QmwŻr)T^Bl,_81ŽpZJqV|*X?6fw-;cw-?bV@sPW09 nXj7wo.^{0=q[ 98M?L_\x*՚Gy:Y|p|!v泉 P̈́k͠ɔ&%EK$*{{ij>f(`AF=HA 91N?b 璩DTJuP%a~ PG͜9լ,tE OLC: O,W[̮T353:p0$ $ *MG1Dþ S3rX_hڅhW$tA9iD3Ej_瞸opi4^Up*_ct vuv/$!Z?{m8͵"!Ä`qk_esKY@I@t$XX-Tt/H0ߙ x`! d>iK+S <+|c˸8TyBwj^ƐP{U#˳ԥ`( i+ _wZ(}Qgw5Bm$7EN_~v<*k[a^X1e-F8(6m}aa |mCҦ^ 2+JZm|6 =ن!cn ݤw폨&w25~͎:/ַ|/q}>޾f{a%w9\r!qi؄4D8@A%@J(tJ@gXe@:S Qgbq#NΨNϳ >wsᑗL]u5ISU+NI?$Y .ډ/m14e1D|hr: يt:(j2XY9B1d"GUh>+o('R@gT*]T+ OHm!T#dX]ΐ(=~5^aQMJmS(x42W{dWUJ(jLKU@Au:T=}$* sv8jbJù920sUِT CPް$"EL\1D =PC!߰懐:y*|h,_FI/؏ թC3ƈlPWG+&x dQ£rf3y1\p 3^)@ (F 3lփG3n^* ]Yvw-^>ooBON|gً[wM(,Jq`|Q{lNj^ra{ py\^(Ֆͬ}.ӗ3T7%be{1?ig9+(,Z`GiZ8ƒ-RJ9D--U*we=OۦAwH \ }lWr@u;]զc|Ut- dI>/@6b>oy2M0YS fnf?Hqͪ )c2&C=foxu骤?XӵTCX|/~r{SwI'`3Ce''HLL.ĄuA~5NG9)o4SNI$ fT? ۋR^W'Lt^'qMhwx6uSx#| HVn.{țc~0J[0 'ǏZFqƵѻ[-c57D_Ƕw 鯡hG_/oXu0tEO(q"=}bqsvC= * ^VlOs2n_֡[TD8ʏwB 'K ,f`2&+KȒLoP2 +KmKAEBe(ql#/ h~ цO"y0 MShX8&?pcQâ qb(qg5X0To g׻Gj /r1u蚕lZ tx!Q^9,xzx#KO.Ō֧rn:~J+kޗdÒ%?eYO$keoJDRRᑺ4-$Mh@>2C4@0`dHIgfII12+tfwIg%\IhJ$TJB2n (iD-CoYiqA \ԘMVpbAp.u4G2`x3I\ t D]M\рN:Խ;-U"9b@Yp)b [ҠO4DT 率9stgz)祿&!FFv M.Nz*Mc6%jrP\ яKJΨr]r_ƶj!+=%z'Um(jn + VيI͛qA|GIebxTNۆ71J|'>{$Rq+4/*(HT'N߹2 n%< pvĉ`PEu=~~;x`9fY-G/.*zS#;{o@mympXrTa9GyXBp@ #v8CSra }^`EARG+nGxדD83uDo}XnD:->$Ūjݔ|"K.\PjEZsYS~pv~J$nCwρՁ}?lZeւm^ˤTg xfsA?~x=lojO>|/[݉?wUb{6-s _#%sA*AEFKHe$I"tͪ,?`* gJϰ$)$ cYg84Fn~S89b@XիLc*x:k}N4Dn6t!` eߐLqi6^q`j2 ڢ xqU?2S&%:h`GGGP3$ <)G S?ՑW}]}]hO8BjTp!#DS-BM!&%[E^VvLj>%.??m@dw& cW-d`:*.%Ҵ 6C,}H{s RL*tHKrg e>HF=}$مa ,Y(mr݅)/O\6GHLn ɯ'˙K`~IL'n2xK?|ɫOeҗׯXbU~S:=]5[GZgooy]0lԆ9U=ۃKG~ܿ&E|ĠzSME#PiȞ$=CvѲe^(vPY]H{`QVNU pw尰N`Ѐ>_@Δ",fa{׆`؏Ag<cA c7l^`8Vw0A) cq~GK3$Z+W7%_Q\W;3;ޙ}x6~ ڀk:00M]؋c6T E 呐JRh*PCSx(RDA(RDm=wv6Uj޳=s kyjwK:XVR $/.s`9A[mq.*{sG.M;,۲~yF>?k1G}s7_iR('4)82$,s<߶|G#ViF9OP:m&PIu*589 ^Vh&`x.k mWR!CiRgw ARmuJej$Z_`Oӌ;*˜3*u+S_sKO >7SԵS#+o*NMx}uu4?EC;ifniһETxzhJ{c!rQrNFm-tXtv輓.NK#v%(K$g eI"̑f(nCeB5 ENb-ekvtc+B0XHXVv0M1䮴-ySW?W`M SS ccKyW(ԘM3;F*jV,:`3|) D$ۨӈok'*8ҏ8[0 :{{aD -(0%g3#|'P~yr`Z 8T."-@t06 h1066ˀv|@ 2Kُ.{X*o=`e 2?c+5e__kdAdAdA;@A<ِJNѐlo> }jKh)-or. M/\ T`a/|wDr$]؍=xa/IS8ix#x?O$YR:&d&0 hB3`c)1Y`l%SeB.KЛt~M4:Z4|+Ai9oO}( |wvt~#N1բ'LZS\mHqڶWNqq,=n 6g빬Hk6\ZE_cr zD+ч{c#\ǽ븷syD۾dk1}\$;w& A~}?QxU+-yӚs|=_j<-!Ĉ&O`K1X6gfoͳj1V%S_c;g|^8> zûk[lK m٭z$3I3ɐdxwyp ^<EP|y A%y>y̑n_ER\2(#^g)+FVФ0'K>U@+MQ 8ѐRKM-i|Κ3(v|B9\[SֵYiUc"Z"wN#[Gt&T_Z"^1LݑWEdFunX祑A{Ԭ6]v`($i$L" "^dsޠ4 b+p1\&ұ4-AXs!c䈆( 93m 7T 8 qQbba!PC0#¸lD``h mY%3Re^r7*L*pEDY4 ȵrp V@3N}؂>0YTdL hV>stream HV{TW3y!)o7!(uI  di Hm$ZTjյT|Vm}tGjWVXTx{L7{@ ʀh1<[LcT Xk)[6%EeX*@}4w?h@%\SH' > ^;`P6_a࿶XYVߓ*fsfﵓf[cH7dpƓ߼W *LEw>Uޫ>w܅+g9iizTϸEym+fּZbd!{MAzo3){h~RQZR^oy*8h~hwZl=iu];W{F+/*jǾĉU'Z49xx䟄KΓs"hά?<ժHNF%d H$e'rm>M) q$)N?qS\y˫>yUu*>YI)h"QSL[(hp:9ERv3aN.]gUj6ˤa%2i8LR( 0+ݍ/;KEN%%%bPiuҜ)1dx{N(B-G4[ Ս1).ˢ*?[T8&Vt"\XnOg~3_w~%IjJ=rJmݴd)65-h6𮳍G_kU&8^\pWi!Jцq3cǢʼedЇ׆16]eO=Peum?mVN钮]4oWesLY߽΂?W;w -^x9PQ S,P }PGgқǴ^@HO$l!.ӡG=;@YNUv'՘0ۥy>.?o'u?>)^<$1ﳵ9U 2]ZMW.1kQ"M*ٺmXM[m\:RFolp 6 Uʎ-MԘ4:!z] pG$ǃ=\5#{~-IVzpxv N5=d:^2_:NbcmrKbW<+! ^('`UcD(F|W8aS #"2.=MB|b(Hw.t壗)rX(94;Xb1,V*a]yoQrN9 )<61[8KQv$RiGM/7G ̷"麯+_oeo* ."^9fW|ui~G56֣[~_!+8LiWm%u8lZΟ[=zsg>(|WO-S{u}v-^*yg'l/K6LWxŒe%`O($@cW=VG4^z[yYP/!-wEQVˊLf6TFC@B)Xv`AӅ( $b̈AW+ ݷ=?YJY>w٣*[T"M_xi !&n*!#xt[ǿW\җL슰|߹籫MX<O)~kAk5^Yطljeo X=ꄶGC77N^_McRJ$&c+ZOeUAז쬱 Yk)o//Ɖk ZIExf B(YkvV'*J%a&Dυ=9:7mns귑 19ʖu!3Cr<&B>.*r~'ntC-9{ni8~;o6-j_Nr^tHǞK9IˍӶeIǍώsqiZ^onɪt[KgXn޽Vӕsnq .>j|^9a˛-3->wN4Psdɀ{wO /=,$}HňI Po' QkuS,\zGZX \fALe!|ju{mOn]|0$_g4 &|;œ`Qc{ܻ+tAх -f8ʇt ~JVtM6HQN~6' !h܊`#Crp#~A`2,d*T`.F+8b[dh(gr=l{p {6 P{ )O\),Nƒ&Ͽ :8Dngк@>w9\hZOTiJ?$B )Tuf Ԓ&"8D z#W-}!0 ƟA#qMXh;d{qN'ѹj'55;3_*qa1l&G 69%s91Ti9ӡ N fcqITְ5zHD E; Us9yR756šB ć{$\6o7Dӻ:|G|pjb[_O :M&qRGB =74SK}uDžɼTjZa=>i_'.$3'fw} WvԎGý0ܸWv"O^7HdكA08xUS Am1S*U ؒhbMƖh='hJE5hKBvxQ?(]%|iՔ Fd$DdtW &66& 'q(4ן싥|,H g8O1>5m*̚{瑾EpmE/ϩ#7pY^AQ0%߿n^|^9GyڥNy!M+ufd_e|$k=pVŽ?6[P>8ȼ2Ax_ >siܻڇLb셈lU\Αq.sq0blan:)]jH_!.,_E{q곶";hg1M>w7~\Bmwi11۶A`.}41,kn>e1-gqށA;ʨuz7 *v*&5(" Zb}ki+ )j_^*a_IR,Fq %T w1?1q ek"`׋|CCZ54[7_GL}Xw k7]PwX.%aw폒xHF=* GI C% a3f-SF.-Sc"{'Ά4!هvtzF;Qc> $eI=sQW`ʌy?=jC*?Pr\>y+hb'C\3^s#-4PQ@wQۃ|yS8kUkH!Hy~pVOKIdf1J7zMVkXn}>"g\N#x ?WtZg>M=3Tl >!*07i//-R//#JP>U7\ֲ1 jy0Z%f "vpkg6i6$;v:ܻCd;9y9y{9"/=¯𭟂X=zp/g'x}.&'v>7>YM_ ],C~>Xrkz+sq"\YxsBʺk(3bcPwF\sq~'KR{k×X/5%f=M>dN ^O2>{<95_e B45M+heDɚf€`ȑG Jf[ܙt~>#=vCtte:˺|#]3]];k93M}38p"ОT4~}}ln&Mv[@ &) Ԁޜh}?'7ؖB.D/o--3K6 RܛB'i9|:-[t=8ODs,y9n@`6H F=ĂT?C?3n.]#7Eٲ|-=CN=ͼlݚ!OSujEQgl: UC%E uS#ʊ:9N:r5U X-`X ք=tIFIFAFJg{ вiJ#_# Pct.}ō\5ޚ8oJ9]G4¿F3e@=jhmo]drɥra\lf" E2S՛ `&mBMzDK\PůW\!W>},(}j}__$$t٥ȟe^ҷSn(/\%)ٚ==vEgGׇz'8} 4uꃍG=!^u }}Ao}_kh}z=GO<7@w4e(Ŀbtg%s3 W),O6 N㰝+aA> 12]?#xm$#26qB*B}ZYS3ogEYav\\ʁ`편m:A+ XD"hQð!4 LC0d7i:ovxE cKjȫ[K^41H@H2f& R+G8JwC1j>:j|fʡ+ǨЕQh1??z!/D|!"q _6{( h\RR{J{#* `8QM?iRdY\!+B*eټr4Rf%fx&I^35cUI,5+2-z>/? q uz~u.873wknuI(V@*r(C3!:DJRr71t i2Mc&,55[jkhUg$cIq0q MI"EZy\K֊ S1tSE2Ӷb3OWl}y-F@kbdQJnpQpʧV9VӐTQUutj4=GQEW^uF]C5jyi]Fv:5ŷql0A5 P}1֛CΩ$VCg4v7MG/6e޾>u58̈́g%2Ð 0d*dS~h~ka+LJ4>pߒyJnĶq\Kԟ-)55(S+ҕ]"+i򒻢&w#!5Ci&)`=mTZF.=TN WHP.f7ޛLܶSMm4T]Gs"{4a3jg,ܟo̵4, kgqs/ˮ^L-_\h9>IWk:.sZZ;U];XY TT8!;įD˰ʝx7;x>tAl?z5u+$7.]S>1|jVWcqP=p`XV} ,qK遁'K+ 7$כ=;0<y~gz@x y`$<]ƁJJ\ҷJ7\ LJX0k_[]߮ն8 <^\UGPَ<^Զ)ہTjjvJ.`0;l&]qQ3q.EGcD>: B.;hO P0z|t";r!.:=cԊ: }1B1&+tz#+#+oU񩚱X-_3C"dzcۥ|(prPn;ʳ(R~c<` =0S1^(,xt)~NzFRK.C& a?@ŸwK})W\qW\qŕr+++ wm{qL۞9G `ê4ʁ^I9Cs@w`_"#?`%(:t@ &ǽEA-ck`<8 mD*8z -'LMlb+1eMP ڊ̻}4j=as؍(∈,[GCv= :F#X1-HVM8h#34V11"w߫QgY*YTM9&ձUXDVasܩ*ztsl1oSG{Z!:g mX5SyfL@qYcQl-I3nl:l+,# XM l={OpUdL[g8ʓ{W{ԞxUdְtyu+͞dUڿu?ueMJs"b!&7Naqul7_ -{'{OMG-pgWk}(5޼8󧾵Ozî?~CG}uTiv_ endstream endobj 401 0 obj<> endobj 402 0 obj<>/DW 1000/Type/Font>> endobj 403 0 obj<>stream Hb``4w=Sjvoޯ}\/&A#k^Io6MUH夘>}sfݨ/^t3!%'.^f[>stream Hw۶ϡ,ˢvl37ÖEٖ$cYnڬm/#EH(e|Mq񸠾"H$D"H$D"H$D"Hz!3%1WU0tGԴ23t*G3/!ЊYI:%3W,.%dYZ YVzaN[zd2IDrObjқXK Jmq\?^xҘɶaO 7&MQl^*_"!o'݋$=8_Fsc.?3Y`v[Z"Iw߉ӮWtQ2nrpqygE~37ߝs^/C-`ly%a'cbz0%̖w=R|J53jbûb+njβP}Ƈly8g Ejȋ]S-VÞ3a1 9-M&*w;8 }՞k\_i ׻͎]C $;ipk_YVeYU VaYֱ޲ۦ? eƘ u.'<-wwޙ%==wBE`;;+V1as x;- X$Wsw߃}w^}ۓcuݽsUC.-Fwω‚hE=eƲL b nǿ>[-Mu1CchP d?'ԧ1K,w䇓T=}2y%}nim̺X6Ľ;?z }V"R% e0"o=n0Ľ+# 'nzȼ@b4z)Hr ҺCRk]$pxQ"(1Ea0rd:Ne%;#s/w^w2 |Z ƈ{6ݨ'X23xzӊ:[%靲|8rG6q<ǛIW`<=Mr<'bip'l_pi:z۽$]'< 5yPz.丽SuېwʅwT~w:GinOoH{,)A?:?xnMiGoyz;~w3b0s̻";eĽ;R}+DWOE{elaк;?{zG::0@c|#M;˛]aN#WĖI{;g;J>jo'oc|>oJ\;8,=OScErҿ $FE>h/Һ[$\Y{ތ=Zz)Vӷ͚yj=m5ͻd4Wj]6.QuJT>$yǃX=qy4nfOU H$B]]T[ݸRSՋda1;tW<}uWYNʨXCI֕m2a|(^bˬieѢdF ֕7ij/|S0Nn1{e3yg){bhsxYպky3bc(^PftJ IMuwK9Vݝ5y$^i!Aĩ֚f1X/tcl80H)cYV8MSAJ鎨g]̗JğxoOg),#~4.օX)n]|t2oelV]s}N./?tMuCIwqϼ*ͺog[6tw-+,;rv)][;⫋]lZ ׉skd?ڳ9O72y'c %UG}ͯ&CJO:A1\_(V澜ꐪ>stream HkKwLϸ""!|FL|$5&1$=u^] \ݻ5p=OQ}3L&d2zaQ[7L&d2zͩ5d2L&ӯM5d2L&ɴ}ꩇ"S[cd2L&۲Qd2L&vۿQ ť%U`?-C5L&d2rE}y~~ׯ~kkk[Qd2L&vÇ?~[XXPۂd2L&K j|||bbݻw333~˽^o}}tgg2L&ɴ]666ϟxիWo߾}4Q?fd2L& ޽{Ǐ޼y3555;;KwF}&d2LE@nW^vڍ7FFF=zo~a~~~iiiuu۷o~C53L&d.r;w燇/]w-/^LNN.,,}d2L&688844tԩ3g]rΝ;_ӗ/_{O}`L&d2m9rرc'N8} _/_NMM}uuuըd2L&i :{˗޽ٳw.--L&d2mkݻǏ?yŋo߾=66699933Cgg2L&ɴg:th```hhhxxxdddtttbbH}fV4V3m\"#fJ(paiaJ;SrR+ KʂiJ \mB]dHie: &V9g#n#MFp,3VbW M$O#&I66ɟ%dJ[,4vFdIanCh GBI0M"("H=/U)a?E}v;p@ n޼N}p*GUGGJtst22&"Ŭ+$#tty\4U1jB.rGL7*JJP^ma`MHGh Nc*g@*ÛG7T QaqZ2˜2 IgB|rs2UYK?H``8';L_Pӆ1;Mh(%$9WMKgI yt^5.-Z^_lGbEW K9h;F袬C¤S P]4OJ^-Q~c.Er 'T+ƥfOC2Kd7a:72 LHyV7FOrFai1qidGi`~HF)9vސ9g 6d_zAvEĭ 6:  3b6iP'Le3F/P|r"³jlp,P|VDӖ!˲1.Rf7HJ3ZEh&ǭYA)!\Ѥ9\8bL7RbLǷ =cBu< %d8b4SVDFa艩J؊3]z=eR9ohHJ ^ 6%!D1T`-@LBwB}ӈ |#QN&{+I&V%fQ(E sNy%u|j1)ZȰ:Dzr#+%Ue3Fn Ò|eC9 ?O3 V-?!YDBHC'.Tڗ5jpk 6,Y̻˵elQͮ7Dz"^Z܈I˽rp+})' Ô MYw-O]bu߂j1At~#yoxJa]=K4HZ@\"6#0:!\{=DF{kT5hC"[T@("N5f\zJ;oD~QCC=fdaHp ~[BA5μIVnY jwc.N=Қ[6u:m|Ҵ~x!:vhַ@[O#7C'}ИCl[ a)@@㵓.s%5!Y\KX-mR_$al@m&:sح>+WX? >C|ŠYa_1zTB\T\6UsCr%YlKNSV&QFO~jKF`dΒ) n*c|TULb.|<\%#\rAw ,zj2g ]KbOYFV}Oe̿sez-5".iJ;I"Ȱ^9;3ѫ:kb Xc =UB WE_`kġm}F~̯~f//56x67!Ib^ Z9ZKϖB?Obxu g}i9r^\ lxM٫ 1,|Q?nkrqYT*&WznzjKUm]]-.ф38D$>*X $5rSUr JMhгb[TYѲm|RÌ,3nZ3]Hz.:'Jm4@5yԽlXV֖07ɊMS7)0X>Iϱ7}CGoO&=2Z I;Y7?{=(!kcQV6cb7 -oYwiSc׆#GKX/^$%xXl U_ Q ܟQWX-Uq,Gu*GM$ɖ{V(;J# :aB%: IZ]9e6JwTDt uB%ʟ<[͆ʕ{&BD?O&|B\!1Jr`7KB;g{O |P%)H2c X˹>M8&.Լ>W3Z“({:9rQ?;j$ur'||t:Ő\;ٟ#FՃCcIP`V ̉HM .q1h{qUɊZPBI^Ur"h\)a[#C.( yh`ZzkΫrC Z)qvV2B'G9e(KIL i޲੍n3&GfCfjEYֆ蔅GnXs:I,(öCw]sKH=:;8/r# *Bfo 7BTED)~.މS o\VJuON0ZhaټsrޡRnh84U݁ bܯ1yrZr\$z}\UZk+n펴]t 0~O>}>-\ zav y%RpB1 2D2E=vӍ{II$0IP8_ -ʰV`6$ƛ.1l(BL0 oFrU/&ӳÆЀ /E86 i?Ss%zJ ix|H%q5QN{90_/nH@c=(YlngK$4k]4.r()*A*mUb=ւۛ? nMuBn5'B8ƘWm 7 N|"!GmqBXH|8C:B3fguxK."E·l+1>s|2>w=rsϡmX8|݉=QtvV;y-"w[x[BoSj9FW6ٱLND)2MUőbl7\}?N@G)~$ż=HK48$.;z ZΕjMi'n scޑv EC̰qx;0JbQ?ke.aز11YJ`TK9[^6 Ly\:̆j2z &Vρkd^>8>4EG(zBR-?|ytތR>}/_~cc"xEDNh^+뢾yXHg J!=3VRn(fq&d$bH!bh^ K8@P 5(tr؃;xf#\_]JN~~7˹ :(=\e۞9SZ S0B! %ʚr(jeæmw{O=lo+T-x[j)7"mV\0:ك2ا GUS_P5 o1O- ab K T(C¼A<ː  r޾ȰlA:5( s*4(&2 ob'3bQ7':{/|i嗊=SN;_vT:gD8W-ŅMmuYn2 _\׆xBo?zEP\zKEZ ~THFf33_U}KfH lroo-׷Fȱl&Y.Ç_~Q! 4@D3;E1G{g܉{QoJÉ҇r`8Mf7W&fHџZ>1X#>qJ؝sVzAms ڇ|zȜi6Z[7< Oa ꨏr#!g~`v=s]3r4X z܋-whWi=?sԣj6-& Թݰ4j;Y !eOH,3 3$:ը%MȴNÍ?V49hy C߬v pn v>l-l8&&F^x.n_{km!>xʙ1ѱ8gS nw5*c\"~@惶iM'a&$AHQ}:' ^QGT:\J~@nLRX5:7vyz1Mo4(-Y|Wɷ4|tXyTK>PCΤ3Rr)rr%_)ǖⅦ\Fg~[Q5R*i_oٙ}13̶r 3{&-(Yne)>I}_<.OׅÜ󄻰3+!ñ2'^ SxBO•"]ޕDz\$gvLWPkGNXثxTO(IJ.ՂOU fτ֒BޜS0qʂɹh{`/N,!-Ļ)aq$(V"5lPzwIF_x焉.3@%_=ɬG<&<q4{2/-⎂ hk11Ĭc5Gedt#fWb^߬a'qzcȎ/O00@L QQ5" "J3ńKiB KaS$sMc5ot @şѡ>量Nx|{%879~\;u^D狡L"|IsR X!79zJG,f-LWyjauGQ 5n:g~'}ޕ>rf!nnd۹#nI&77xflCs~cf lvH1*;Hxl~D^HbP A2e-q?d/ {zTyryIԄ?6^%PϮ> VZ.Q.GSi$Fp#ü_ecrҟ+y馱!,_sL).ZctmxFv\bǟҋ">vw\gj'kۍ^`zRJ:ҥk-̓ޡ@dCQggܘScOTk[/S;_Ω#'/E=BC-("YVY+wRH5@%n ]f EGK簐OBfQRo31i1Ή@Q/!he?O4LG'jl"P/l$}|K K]XD oeN@YKZȩJӈ8%TD5 M(J/r_*0aP\iޓ(O>vblͩvH?>KW-αZZݳ 05=vXN6d= W]t=5x龅в',R3VW'$ұs_SX+u2h)Fr4(Z L$8GDGCL 'ro&<[B_  Ȍ;)&%.5 ORĿ(EOAr2r1֐Qx5{4Pw3 .JJ_gŷN x=5z:@/h P˭ .c~\>Ż?OPw.sn+n1S$qõ*ϓpw'lH;"u/nm6?6K/9<l~/]kיwYȦ8{g˷o~?R_Rؐ"фUx!]x.{&R- 9&@lٙW~ңԄ}ZֿUPJ^\Gz kf0yFSGzpE w֚Qˁ`ȶ׬G;Xʴ8鴣:k[UAm3=E$ĊRbŤѭ0fϬb;-& WT"hJDF> bM\i7rx<*ivn@Wߎ $+SÔ庄͚j$Qq,^yr]2Iky嗓Ƀ0s0<>`{_:v˘uZi0(,ibʬفJު509mFژZ6f{NɷfsP'+ oqP-II#Q`("FmB& E$`ޫIʁ֑J#ՆW(v 2MjUj0K}{o]g!}^Twv. 5NM^|t\>B{Uf9r:ց`o}^s;'fGa9mSO=2f&みxў:X  9<(ui[FbN 2 \Dۣ=Ŝ[rL}_~ϟ?so vXYh%D%M@fWl6UDf讃S ?Qsz&MDD_j wl/K='gjؐ{3& JۜÆ)biǮ}tzi61,x ;bKǧNn& R]} B`pXl|c^OR R*8iR#.yjgiQ7t}KvŨVYq7ѷ˼iUEWGSy#~kH\3ذgՕȣ_BpEH+-іSq~Wv]X@fW^$kLs݊=It:^>#+z!ƣ2fɐ8JP:%NNB1` ߩ\>1hHِ:&K/j"3Z!Ԓ|s1lL%zNTam>2{wm윾+o~/\lDt>&VڌW8b|Nce81qpKy`[>h.IY̭Sr !7J9~X ⻋̏H|L[kn˧0b5VvoMoF}~O}!"r&X$KH#<oz^ }s-r-V1Nt3-W`nܺM[!mRn[Dz̷J]b؈7ovNR$&'rLx5>:rew'P1և+O._(i_N+Q{-3]]K^!KcNɁXcO8!v{Lq똉r:LX4P. Ϥz(mX:ZUɢW0@BS(c& /=8;y! WBYVmpP|u B]ǤCط}jhGkޛIjY,iF0}['9׸"auw\z=*_^=86\Xb,Fi2ṵj4u Evz{Y04m%X|BH̽չ5*֝mY @JaH)`m'X_SZG8C>nq-= ԶrmWwS[ņ.R"9 FӉSJޢdZh U!` <~7_'&/W twGuAWb`l$*^{e%K"fDl@(&7زEXIѨ>y.Im$ёyQ:!@&)Qhd͚"c.ǞбW -=`C5c|[][ԮԮkW3xoŋm* uf?^-:,P-0)P}%٣~)gz}Mk*j?4Xd!Rmgsf?{#VU  Thm¾4fS`fFBnRDHGUs^tS_^) KISsZ :Ff @X{RlZKF.%!00\s)76\D/ų+wu$x:NF Ƒ;rww~YlK\I:e -"ed?T9N#Aˬ(65)SK \V#+tWK,́pQu#Mh=b0F;ԗڠٶAR*Lrj"68 :1o~?r|Rfc.4Zc!P5@n:|ĉ_~yq4kr;)=;{ۿP|T7zͷ3: [{QC.seT_nt~rTβKy;P3fQVA$bPЍ72r-)C`hy2welHCuU# tN$Zp&Q6xH"$BjYQjCA)0!($mǨX.U_mWuB;{8&=^Z,KX욵*lwn-5G5u %<_zTLwh|RT%UxqZ*]&\^^v`c/p*;8ˡVh~N5 @K֘Ix-)Ry4Tђ#=㪲 ᝹^xfwSGqƿQߓ'O>}_Q_4KW P<8n㓅x!3_a9/ ӽ$];AhHփݢJV-f lP`,Ϟ={'O&/sOCW9azU7.bY`fF3ֆLUf&C?2BQHR2W"5++3\"Yk[ܨ`ɽah`gQXK, ڂCHmlkоMmjfճ [}ɜY"Y8Cl~ʁ&.I7jŹr" WtgBn^MkZ5fu/P)({}n+:u… .]JOo۷y{\'wyW_}5]~nYbiLK.MK^s/5# .V%U]3ϖn_{3vl-y Iu_O)]kU:c݂L^!ԏ1 {Aꪮ'@%ij| ׁ[P:))J5][U)=Z'1/Ek29tնfmA)?mm?HmQ=a frs %v AhGAy5IauKߡ)桙ȇnZӾN唅YB}FafS4s6V8ΠeXJȿt@3fJmclNL P 5bwwJsnjJn:#׵r60F]`m3}`ln@~ǗA;'RPї5j^Fuq057D:u҆ʁBn crMUj3k};[fFUuwTf*yoag^3nDⷞh]^Jz_u*Yۘ`pvx_brɓ'Ϝ9o^|͛ǏOwÿ=W_=Hwok׮}/>w[oo>^zD2OӘ*;pnsk<\༵D-&*}Lr [Cc_oԵ-Eg*6ۄL=)h[{_(O'mB;R% mOגVOvGl6}wX Ӽm9ժ9.xSEA*wd;hњ嶞}NRaIV*qRQJ-܀Ha x,f`X1)#0rH?>^d,$dذ@f& !!b"D0!3$BL@ 8pZK}{/n?t:}cHW|ž:~{|cxիW,\RyYGG9]PO^ Dc=QQ+Y.|( (ID46,+Fe -b*3靈7Y1j2O.$t3+Ơ2#=4m$J\̇!gXv޳}߂G?94 2 <3m`}]C% N<<$p&%g[MLFyM>}$CW>,li")l;5װ/lwCK *XK- tfB+r3KjX{*zP)\1v@NyD YR+tf` J}HJ`͙y-` A @,v.VKv7+GU1! /EQ '3 +6NS"}u05,QTDA<̖ч*Sc_ 62DH I!|"AB&H s|L^'+Z'4lm+/^ r826{jHɫ"KXR`U;`ji3R\T(H>Zp+Az )BLGP~봒n jT-^w]fƍwiὡ陙>{' \52=5^Mgj3Sӓ7'n = _1xs=};vCw_|>]uׯ]vժU˖-[h+JieI%yW󠲣dEeE*bdxd"LE2ܨ2y5oJԗD 0T\Yo>/ 7qa9z-_ѷpH=.L< gLtn(*uMf B|c'DҦ>$e5%> 5EO}T9B]~"~PP>HlG?Y]ݤ.x ـ䋘BżP* :fZZ"!GB: 3tmrTɴ&4KZN\" Q9bi R/(}t )(2qjC V{^[˭pi̊?ׯ_k!$SJ-,āyISZ p0%4H9IIA\P$dИ!D}9YH(ֶA!uq}kW(g f@!` 7S䞑Ƙ- 4 > i1^F"=S3ѵܺĐ 6,޹@"KK0[VIJN E uZ]E=z֪I/oKFMD8d7:R$>(tiSv-ZX63kC E<6$nC*9w*Ce\4樌x^K˗۰aĉށzǏfffgS&nNNܬMO֧wn~mbPpfz09~kȐ!ơk׮\|s{>uԉ=]_}upw͐'?O;vضmۦM֭[޾r%K,\Ry)ž_MW?ڤC"ָT0њNp6JQXY,e`(:$K $l0U`g3F `?gH( or t reb?᥈QS&T&M).R2ƒ]_f7dN4ۜ?1 @,/ G،PF& %3ϼW1Gg^`&W=UA,}2R< ,P 2A9x||a%2k@eJgNBݦǔsR"Y_|7C6y(?C!jH@>֭[7lذf͚+V,ZȐuƌoLZYaMfLcGۏIR Y @#֕Ȕ rs%pJUlYh}T" 1f[ iG>O1r9K2h)PT*Sը|#q ɍ0l@ X:,AO9dzy"1x8H}hdş3:ꊬ8pXܦ$ -o_ޔH8_eɋ]ŋQЭR:Y=.mw.s HUyWL%U'y3K|v+%IB2'M\$ ]r,: e$J;q}but (ѻBG\?M}GtI} v$X|}6+e~/g.Lc<ՒsQqHf*Q|Ivd\v ]4 R^.c_7+u⁍j96m+wldaDʋ+,򖺹ԙWS"$ep 2EIͣ@Ir Te ?Lj%^ }zlM{Phqs#  =(_UAbv6 /t>L,amaW߱?(N%]Zf 4K8 !"Qafu9xoG2u+xiL? :9ۻu;wn}˺uV\i`Yf&Q֤ GPPm O3f$gO .bp5iڼ2늳*b@P %+G"bTcK["oldy@ '-%̓K1RnU1mŜ8*8 -Bۛ)&u%0pS87P…|9-07<.y18=mêu#QKZ_QI*wT,7ypd$RwqJEޜAr!$Hi9]̔ N#ADs~Ek,bR P AmYH"$.qqdG>}W^TP>YFuIH " xë*"<xl0f${ьڥ5#8 qޗ"y=K"N Ge8(r8$y LՌ)ax QO;&?eZE*d R۔r@:&li+qN{,qF 1S(o4i!{TL)ݬ2z) .؃ -cq"CҔt9[@'tW'C+^AO;ဣ99A5i*>Vo: *,BITeӧO={-[z͛7߿ēSN644455vtvvww >}ٳ~e۠U1QaqPa{c#w;l  !1[#}\1 v޶}VWζvru|sF/_rgΞ>թ_=}=޽nǎ~۶m۸qڵk;/~_/N}R8tF{\ukf%$ą|*G0yIb\6^g<6 @ lHHթ=aeVvy#ЙR?\nƷ).6Rho,qhy&&TU>&*/w d> ~2JךV'5O^w&ׅ-K:5P~t 2i"N^Q'9BP*')wSK9cTKlj!U~,K|+"*$--8%1΅m- Bے/@Ttf3,e%mQ3 tr_oϞ=A}/^xD}#~;xaQ H}L P7YZX2Ogtl#HХH,0oLytHa<C^J] mLٕb ΧZ; i] Yz: KK-g,n$rm*ҾoJgnL!%)L?@y ^ [ pWanJ=&=L:Ro JE T]EuIsUYI?UIƼNHU囍F.|jzzP!0hDY'6ƥSk*3f|9 .\|ϖ-[v8pǏollliiikksgl޽OL?33x3ZefPgN}ÉG'ѣ sĽcB##C#w%Ì@>x~{883kVOwWήζo6\mզo|}b .;3_՟8G8?طowCm__~ժUK.5OfܹӧM.d(^8ͤ&y]r KlQ%^L$,0{ ӥh^C/A'*ĮC"M: xQZ>遠rTFT)|2pӴ{Ęw/qwwXʿ&"3! f΄:d.(" ,G 8l@`_:xNIQ_G$1 :,*"9y*gQ)2%]ZL<ƒ>)R 8i 9 8STqRߌ/G0]KfeVVcBpA9q!q hYI(=%>'~wO.,aWVj@ "ɜrf ef< t5]J"F3C){#b"!]Hԇ8Ac֘M-klCj0gvi޼/^|rƍ 3{=zO>mػsE'OZ[ZO֖Z{C 9jb[o@?Yohao|trb|j.u3Fq80M!Ȱs@_/Fw"Olx>v=Qrgt={wn޹pk/_tGr~{'9vȡC:xWmݺuӦM֭[z5y…~Yf(|{0FNY0CKc dEYSU4AUBP֠0CkZ"A([膗"A :^8PgC@Q$:JȊ5Zl<&.1;+3aʇxsFVE0J"ŒUjG *2=j{dI`hY.K7䲬^"/+>djO⽄eDZ^lsJ8!"if*!˩+FoYfc$Jڸ+3`1←؏'i2Y'Gݕcr6M . /ݻw߿+;ǹpz]/1X !ZY`쇑1mqx>VYW=0If69ꤻ¸ kQ.wP5 gTb-y_I-eܔp.3ހQ;UQ `,T#] cNl掳y<ЅeƼ-.HƄ_̭%KXbTvر{nމ'+W444jsG>>l~\}񼵷{||l`p`'Ƨ_O}vdt3tǿ{zzHk=/_활{=51zi~=z>I8t!FFGFف#ÃÃBklm#l4WT4=x7jMlk׮^z/o;3;y#G<߾]޹KR-[6lذvUV-]ts-J8 90טs o+ukh$X4HbD_ _XSa էːY肶od:y ؏&=#!(kRa_2&ȃ6JxJf˪ӲQʰFj̫ǘq)ks݈^w$Q[4ʾ~ǡ$H1!"ڣo6 .[šX.T\ϐ{G.Ĺ՘NyIJ_FW " XEHg"8asOU(I! >s,\〳 +ҽE"[9렘87)zFBngg @*=՝;wի=9 k.˫6(]PJՉq($Ԩlƪѭ{1m%1A#]ZvK7 T)5'QԹq浙P?%a$3hE袖$mE #嚇Z캰[`t--ʛ ^‡Baen^"nLWw7 ^Mo(ۄDu ,΂:H, BibB@f23l n-+69|"DY]JW})rSCRA2TJӰL@d֖q?4S( Uv Yh!bh'|E1HD\4'l$L"P.RjÉ\ QY`dYqOz¹d…ď+WoٲRػpa\]mCFC}C1זo%‘2bؓFG_<|5=z$[=c]][=<1m$@nChdH+@۠q@{L|tINojcי-ҞoijinhnolVVWVWWU\`^åϞ:{ĩǎ=t~tw]Kc7n\vU-[dɒϞ==MgҦJuӎ+UQH:e񡣌7n]t]G_<ɬZRlOP?1(qRC,:ID1AȬҐ!UMqVh;c̤pp& e"ӮR*rY•`9AI:rّvُb~nnRg$ʹV#4limVOě  S+| &T x 8$M6E"##*m [4la%pZ73"?Ut6(B3o KC̦=>2se4 hds:Dy~ҥ_>sT ualR7 GldzbPLn? <S*7Fx^*:e _m#YWZ $Ս:)ȄéUb2x 393^,Z[ژ^R&2r2!DA5[:Zي@"pWCg(Ed>iC0K;A%ǬtRP%cֆc7Q:ńh ^ : ?@#efOX:Q9v|#:ױW]EnqA(a%z w9eܴ E؛7E}ٟH֭ۺuΝ;G=s aWSS}WWp756PkmItX}=yxhhpt؋_멷oܿ{={{;Z-m֖͍yQ؈ёnϦᱱɉ13 COL4 QpLoQ{woԁmRըϴng^[;[ŁM7s u k5U5*+*.]<x'}wȑyon]_}/ܱ}_6o޼~5k֬Xbҥ$y͚5+d=7I'I' z' q <+yq:|& q?s,>fh!+0DqC2O&E5mGDlnx\PH"4ջl9+#I %8Ӓex)可fOA wʙx?ի7lذm۶}?~ٳ ׮UUVWVUV\VU~w:Wgؐc5www=|G؃HV.2˟^LzS^yq;{:;;:7ruɎ6n9GmbbQGFIv܈'ONP'M0Gc"9pdڳgCԌCÆD4}?|{v`94S 両`:9F\]u]MeMUŵʲe^p?;3'N:~ǎ<|p{ٳ{ww}7G_|diӦbw˗/?.X`Μ9En:J3>H6ȹ5+G>f@3!ՉHآcQB(P|e̩gfIlסW4?U(h3UF2t~"P!"ޗUiui7\$;/ ř,('|=Ź)$I$4M0$++_#3d+ҀI!?+b@Tx̲"7B9#B7r9@ ą GGSI9:P^5i(*i?*)=8=o΅$e~H8 {gƆ^IxmBB/$ :ec{<6m`|m|M$!%Q5֧ZYh",#Pf(}k~ ,V>B e4H4UBV`t9m%lö.׊]TS ~ YNУOSQ-ܴ!w۷ȑ#'OxիW{p8'///-D#D{ű’8{TVYQV]UP_ {7488:221Z0fo+++ ==]ښ*pcUeYYi8V,+-lS-BI*(N7o~u!uH''iRhG[* Q'GaW ylomo{IQ>I}"@aoځU5ϫʫ+J%XaqQAQ4? +r3~|qZzڃGݿz7֭SR׿ʥKΞ={ĉٳgǎ[nݴiS0 } *-7ë 4QM1,A0l`ǂ-1pz.H,e-iRe%I5H8d TBـjO ,5+Ԭ IÀV okl79rͰǟ*T*CxQd~7|_ qvx(&bQ;Ze `~[pQ!>ӅAF ajDM'PDJ,GRCTrs'n]" B6PBڠP_IdRr7O?Bauzn>-[T)/L1 ?;AH.BʢޫAB^CJg25JH4Ӯ%OMè$W=.JWǎ!,J϶Qr&Xvd089|2=0.e/'vB 2m!i yNl,4ō4ʨ1A~V8MRtE| 9F zYLZYjOazpABe;TJQ%^]D;u8O7DNhtbMP ɪzFбFD ~1~ӧO_pڵk)))wyQfff8 srryّp4 +,Jت*{mmm]]@ᡡɉi= QNMN᡾J#`$0 eUUkxoWޮ{|۷oWWVWApt=VU1(:|m@""gi#̴ڦ&yC yS   ;^uvh6hjUlM PINƺfښ t`Iey}V),ȍe煟sde ~׮]۷o߲eˆi~ϋ+CԢQlǘmb:©AǚzB/4H>^0}-MAASXhV8L'n3M݀> R.Kq^Ux&~"X,irkiz֙^k=Kab` ָ{8i솥'j Odz䱳1j'%Bc '! C3(rcm$5%LԖ "TTU*Nò:2!M3DL j|s6=P$@"\ÝC6L!L 8 2Ï?~oWpȇJKK JZp9ጵq N զM85169>:162>6<628:<_m^Bhn$4׿P#w)XPPWSyuEueYeY184^EcE 08iFVFzƓ{޹RRƍ_}+WΝ;wԩcǎ:th޽;wܶm͛Ay]'}&ѩT=e L&}t(/TZ>88yI ң Ⱕ gD3;`(nnD|O0CMh2Mbת01Ý^#WN~{$u'h$35O>r5M f0,ٞYECC;x}^Q>$WURhK+)w84т 1FC5Q%=wʇ'g"i a4=ْ 6:Q(8KQAL4?R,qCsY(36L&80nqC KȀfb0aXlvݫݱ}6m2Y\oyUFQ4Rzr~&|fѐC̔\C'YMwb Î7ND,ɬ?$hY&!B ɺS:3ސ꒬QEZ>[tuRtvlF=xٍա۱9{lZ4zM"A5zi|5ptDZ,dz^-,Xlnp7#AڣLo38PEi$9uiiÍJhi#ۣՐ`[BU⧹kfjg'Fmķ u>ݩ~{Ϝ9ػrʍ7rrr`bx<CPD<Ae%Uej)5mݝx OLOOfN9044}}P*!ڔcтDiIqUUc# [[U0E"BhN//-.+]\_|ʂ*YUPNϪ߹9+'&فC}C}֮Vq+_:Wl񼮱A94^U,*+)()+ʏ$yp< <| ~Ν컷nx#?\̼zҥtHҎ;  {~s))A65W`IF~2rKI-=.N6t t^CEe:3dsv4"" oXŽrLn`ȉܺ5&Yrق[6颸tstZ .y(ȟFhq1p156yt35CS=yB$I@Nck_M@Ngr*O 3؏j儅]/n3e@E>N!o u~WQ'Nl96:+f]0qEB oc>}2ܦZ!YS!- w,#J4M0 L'w:F<W j 5r4&[X"C\G3F"BCU-rb-'7K 6k}e(hMw]>r.%XOm`;Ȥwhi-WMf($MiAPeB>3=x!vn.D%szig>v!P&1 '=y\N+ 2;GZ7G'E*9%i[7)ˡ0Ώ'e8_H޻}͑#GNqÇ<ݻ_~eU= 3uҟz9LR<ِ˛!] .qO֗)/tF1$zrܽ}ǛYYׯeeyK/^LOO?{lZZ:o߾]v5nvre1/7 gw"y GҨ*ZQT@ Hh@ xi`>x^Yl6M.FCo=6"UUGף;g߇ .VnӺή|.]^ɰ$A72SM ~PdŕęYDr_&$F^Lk 'u.5$v0e璻arh Em浴kxdϕj([b؉] WP:+S`$ b˒R$N!Y}sS eP/3GdQsEm pI+b7]`ȍlRQl> ,^50hHMW>xA+`zz[\aɋqa[2 ߣܻ>|I8V9KLVr )@rNjڲ؛o`͚_~˖-w}9++'@TEEyIqQqqaIqa<^\ث(*OT&*Ɔz>lS2Y*mjLZ[[.vuvnO ޻wݻ1DWw__`PQknn򵵵ZJCCBF~g9$7~yǶ]۷gÑCʊ;w |=za EܷZ?׮˄$ŗ/^?}6?t~f@83=dj'©q|<='9 | o֍_\E^qՇ3+ݗA@8yr{ץKm-m]hif7&ɺ x7+˪K񒂒3y|?cnNNNVvV֡C8~xo߾iӦu֭Yw~r_~j,J JTZN"fh7gGSu|(ɠ'U6M$V D@d%ihbdron \EuKzo.V0m]v r 'cikVl7A}oS$̍lX%E쉆u' ޳9#4GpΎbeQ2XtιųS)D#Gbs;ĕx̼8rb<3$1]o-:XnY4Ȫ֠Y#|ˢw^EaQ8&(իWA}$op6JMId9/o\>Lb!lҲD 1duGdzHv%\O, O!렱Y!ɞxxrr83898}B[x|q w7!>B78t8\³gOgNLO|55dN|9981;plxlxw1yZ+kWz{W|=_؏Oro}K.[[ζ6475U5xI8'O<>47';+77G8x(wޝ;wnݺ5==}ڵW^+V,_ܶ"p09R:e6\k(BMߛswӭ<,@l#6%O$ba \!!e^E&N]:]ĖNxlo=a JIK{'bTȐ(!+jGqph-zXnB%͈#|*Ƚ^_% D;dJAgN4mʭ4]=JLtvT8H^eA]@\J-#-eI}G[XXQ_ߜ E;8+L(/E̷ ZC~*ם0HqAi l69CI ӯvS&{T7zؾmOT!>s!sE5?Ľ2TORLzJ)\Qu;"TuQA=QmAq2e$fAqa )̞GBѢ6QDAdodbDnPg?9BdxjA] `-bDW3wanŅOy4lmLᢠ Co۸qSFFƾ}>W\\v‚‚3EE%ҢR^ueYMuym:Y_P#YH'E{=E\fatySRq]4E@pEYAYc/İ*\DfD` rrѼ&UR٪:uOw>}ujYfU;꜎pZ#jnmu{]]]as30p8vUUU555477A&^ |{/'"0W?$%E' O9p(<2>=dVlJVTR֑{##MIg_^{3l.u:z|AFWCpx^wz \D..޾]^}ăFȧBل> hݚggffgSSDAD=W/_ z p?C1vw2aK;jwp~r\m-MmFWs}sc8YS稪e=rK8jZ-wEwсo~{捜kײΆssΝ={6---!!!***((h۷oߺu͛7_Ɵ>=n(W%YCYtп.a %4Qx#BxFB mq O !&TEƀ5Nf6XBhp7Sr|8TJ{F5 sv~ vOş7n@Z'XU}VcᛉTGb D%0i}!\GW/xZG_a%aI81&ᵆ4~/3H-J"S#uӱG),l-A&A1a4H/Jh@}R6Ad}R1"~p @b bLRq ~Z1:4\,C˜ KP}P +|.`)n85%ӳ94(Ҁ&oSAXd6ˢ3ȣ)<8KNmܿd2@p|=^II1`Q/V~ޭ‚E{w{{e^q¼WnTagnXz6Y먯s65;Yjw\?~,nUVV~/(廻{pǞ= c3S3RL̄SMY_edG=mJpw2=zGgx m.L͘\fwKS9:Y~UeyeXjKW_Tx ֭7r~-*,xV0"""8cǎm۶mٲeӦMzVҨ5jI@1~Bd!#2yȍA|,_p9G5+'pqPpwM2PsdZAKJ^@ZS 0(`cZ :]`X曞storބ2Qq*ڧ3:^"dS` 9 =:ic ^ Dw͢"KOoE`1à`mpR#HVdz-$h_#*HD+,zӊ RaцHZ2L *"hB$Iڈ Zx`nݓ ][m2f*lIЏ\ nVRf*>%9!Tul@pHz,FcEF רt`<Tܓs $fegTF |bjViz.\^ԼN\.hh=5V<–Z hT.srry%%%vyNaݢ`ɽ"3zj)mr{Yf)e@=LV^uZGMꫭu8 jv!cG#UWWnkk~=杚zP+)CM'& IP)Cv<&">3".ȱAџؾ+w O;}&,ldBF̉q)Y: ;xɯ23\]_`-c_xF1yz^OMOOLO3-fgVVV3%BG ph0wˋK Пipxu; z~aυ{~G@R_kB5C~r5[(4]چ:p`pVz29/T87Εn711nlAӦ[5(ʮ ""W(ns߼(B*(Pn}9n4MO&o3ggw<}v`eueO,j‚zHMMw-[[n?yݥKl!!meHOH*Pȧo\ 2 lo 3?C=;AJEJ$uT#e3/EBbM Apag۴AD`-X%7='`% yX8a.B xh aZO75|jZ6!Կ6: n-{!4fKE` o" wGTPt#vĬd(Ҁ_UX `# -..҆_Q$YrQ8QR2u+6[X*[jUa&IR*HvMAGD]HGHp@}*^ca- q&j#׳DZhXLR4mo" Rs#LQ*S(HE;}!˼0pNqنSXmbYz$ӧO uxav^U;Ǔccc7nz']2gdI?7#{wzm?H߸3ױދ;)17> awnBrf=1[SoIUGx?lOOڟv() !5/np Y6Rcǎ~ZVknvmhۻS1TB 1 }FL؁/tυ}n4k?F](ٹӳO?"N&';ܺyO7ȁ7? ^*A$Ca?q~iPɁ/Rw=sV:mijy鍣+-Zr aZ}vSrcǎrŇup޽111k׮]jyǡ.jZVG4ӱ"EU%GNS6 Pr %1 $o~I moPF=a?^{%\E0 Jf "/ CSN*Kh٨!FC *i'&a wٵߊ%.q()kIܩ<0PCΠ& ˡ^H%k(pZF8l.!=: aBq:tHO⫼JrR W8|+"2>##-<0`ȍ %88'?DA3ȌV^ΉiZqil@Av0Pd m*vUggbzv@w:LK4D*G!<"Jx QLĕx 3@A%Vuy@W06"Pd~h Ҋ9-dZʇ$0MO%%Џ:nO*G 'P`xUS%u-GTlYNVUVTWns{MM&O44Z__W뮫%y}^[ksgG[wWΎVZIq$!Rߕ+W5Ν;w2^WW5#}~33555>>>22Bm233 ׬lW^^~~~! *̥R'h׾„ԂCۚE%zmٱ[S boox/fo7'r뷯^M;6ʉߝGeWzI9ћ7%FKRs㒲T⓳䤥g9IIYғ/nIvQ*ctw=|p|ģə'3sĿ _}{W^\|N|0;7;t4#8!prB9p !3ͭonukr_ݸ~arÃC~Rz} .P%ُUq0roK]pxlӝgz:{ڻ:Z;|nDkm\_[Uߟ,=v\~9RXTD$77OZZZbbb\\\TTԚ5kVXlٲ"#vj.HpNZ adXfU$V@.9'àu0WH'M"1{j~o!cfL,QmPt(]l'8TMjM 4* :Rtv'si'\!-TP0ϏII+-0Jh"m`*0jkWjcQ쉫tb-/j\n" eUfBL됮D4Lnɕt?Tm8~_Dj*C0̀J04%as<JO9ABZx~bs*e p(QSI#"ihiT0Vfh5B_ ^kc*Ʒwظsٍv5Q6 -HUfԲQ0Z8`D:/, MwɼĜxƐ̸DA" `4tMc7㘨MoѸ D^!w{܄pD8Ma>C33PǏggϞ ~|@˗g铙G3{M@;8o}8r/kpt+._{~躿,o)@W OɁϟ={8LOOthhkjom'B444?v葚l>Sڷoߞ{10 xa6sss333׬YbŊe˖-^%ěLX>&X*AJ/RDW7_w DN:<#d2a3IKQj'nTHPPFd|b%[Q%*R"Q2B(( ,%zL?$cQ0Cn n# MKG!Q4h1D"3jU`bP0b_q%.6 :F j\f&5?#!fQGvtΗ\)*[/ ȑ#>I}J8" Xv$Hhd ^(J,!I HYrdM֥` FDEH7G[ȡ"*?痊K)#l.SdUHjq H-VT'*Mj[;_$SYSCF*JFL:(&%Cn1D9j J(197'jRAr?e6Fy)lҙ#yLA6WexMqz u&4 r"&~`3aMJn$[o.ݔ'":p;ގ=;~Pw < ܉ ? {寐mu}߀`a3,ᵵt5'|^POG 꺻.u%?M888LOO{0:t Xzf{cbqq1~ݮ*2pݖr)NgEj#V*%6綝l%%}{n}U&sk7h.,*u;rwf!uuv٪,˪wSsWݺ{[p ^OUdWvLI,`|*(A#K)0. 3.(l 2"ై,L5sν * Utw{ӽ}^o|>(HLc 񉉱/_(/pA4Do qɉ1 p79r'Hf$htkY~.$&0 f jbq]NӨ#)Q&_C]u=e1b0SPb`T궺!X1o*lt[dLr ݈@R3:rfa@K 2]9;p|lrb^8 owHA4D?|zjsA=޾n^F`GOG=wܿ{n;n~^vlL# |{wlnhnéƆ5 5uUՕ5U.dj Ol'N=vp`FFzzzZZڑ#G䥅%r%K,\p޼y~;~+Y \ kn=$[f0W؃zqr1(˥(B,tOlf\ pN\@y!X/p+X.|#c?]ؐ8f'J MA4?*Ad6))Kl?Rb-9d 9W0c12 *q^g?V~_ L e5szl8f? Vy]MH&!/Cp-sV##M]0Ѫg%Hkө>+T2l<˜X <֝?}41!t 􆺬uk=C0s!CNjed,-?a/+$傂8=J4;zd₲Ңb+//tE9E,=.';0ގ?N{>8~e@K =\p0}gg'G`,Io8޽{Ϟ=,ކ2r\\ի@8 7B})c!Τ-݇k&erʆIQ7%Ec'r"V-]Ƥ}I{RY%]%ٱ,:)<:Okv\,rÊ5wVkl{a/6௟ }CC#CBA)czȁE(oxA(7WSS/&&'FBoz{'vuv=~^'#{ig޸~xd6^ixȇ}Bc5փꠎkjj/U^XSUy ~,2<E@OY I 'K7+KE +S?-r;'@j\EѦͥ鶫NInv )rUf3̦HCY񘋧BM}Q%$O) ,˳.EE3Ԧ3ii2R-ŨC]m`DA QGRi$1VmjHqM|QN9s<9r>7mie(*KZnS閹UlAqV2H5}mB5m "F3&Unjvy 906'DD#`^L%tETr "Z Vp 1PX)H;x5ʾ/WE_'lW,.A7V(LQ]P_vrh-e ]8eⓀ=xلjR[;*qi Wx,b<#zQ`V,Tc-"85OVg,W6A&qX.Ɍ THACw4-*YqXaBs,ځ2kJ 4Vfd1cxm eTF V:5} ?WjWs5ͬ X.ЀpjkkCcGs{:^.5zN9&!]y] PPk 575 47677474alG XzȮ6_y&\ٵkٳ̙3]]]===hoooGGQ8??_Z{eɁvQXX <rRq`IlXd}27z Ksgx2VpR3')ٳ{S21=}ɬߟpuFNʲהf*pyQjfAb{ ^7'པXTr{Z.=y!p`Q_oo/H88?n GG}潻'O<}*ЎwnÃ}Z=}8L ڕk݄+/\t?^8w>S';O<~/@8#$H#Ra=x d ;]( H;ȯƖ͛x5>a#]TTzsrr-Z4s_= qXq  a%"C\R۷BL#SdhcMqySŘ\6ChGJdh2✢A*xTDV.lԚNФdMZ ѦK`nN%FD*l|)KWSV Sx\ gMInِƵ'lo2$?ƊIkAWXQCkIJ\F+_`*߅Df!b/:çF"V:E؋Ҿ2*%u/+Ues$Rudm ҈ƌ%S )jdbw*Q( 0pX֗G%4UR/EEIY%n0WL2x8 qScCSԌ鉶ЎukՖ-l<0ȏ! VQY 47 }Kϟx˗A0.a^{`⒒}j;l#om(\W"SymzlX[T[bO[yٳ>I^77`INɟV\?{q;k,'loY꒴ iKSΟム --X[rKϞ?<2 0( pPxd8xll( x&9wn=oѣ?~9ٳKA"Óh |臇ߏn߹526:42 `uU=W{w_օppx&jMIiщv.OUiy2ڭSlYɣB] {q*ďR>/P8\H-$DH2( [#:RaI,$.!G 5Ѹx0zfgdš)MBs0#D,W6aU,+WQ%Ʈy92 &#ڄ2SKe*Nj-.Q. )u]&iQ "CGT&bIbd,N9<*_֑E8Q[ ی[t*SIi ]dȌQb]4GO!p: X܊4H9J$e?`2O׿zsEJH/6ֶC߿_cV#jA-WE"N JORC># A$P}C0 6BP=v.`z۶ml-Zzxߡ-žU*ls9]r."{fΟ8 "44[Me~TS*r[EeY2FnĠ%"*(. 4Mb\o|9=jYjn==yٹP{Oy_|>x[_XGܔ[) n.,6P.l800%='veGg-Ma^LHX%%fNYV5ً2~5;z̥3^ihVO5n {؁NgǾH*CCРg7y1~ @w mqׯ_xɓCÃAs}=}{{{tA=vuSNl.lkmrZ+!ꥫW/7.455x,l3L:';v#G> Vp k= B9VVfv[,p 9tV ,RztUBoxHNN_r%K̙3}SO&M 1ÁpQDma#Ib"p(ʤ&@Ǟ|I2"4(5 ^&2 6xڌpTfjr@aXޣ<1d4e35 u­pBXj*H!z(;2EF1&3KbBHNF5,F=U9!EJ4L4wf8!bt#ZpH(WtI9AӘq$O4.~V}[}$30(%NA(iH&A PM0ۺSu& 5B1i(*{iT"|E oj)8CƶEAB&xYV {W`--C+2d8XBs$QC! qt+s#LȊ dyBB\̫)BƢbwH@74PMl Uq 4mXvlbHpQ@:ԟ,ƣ82 HSӧ%'%`ۿUUUǏvRY Uhس:l{v+tG簂TpⰕl/;V*r)>ħ*{Ţcv|1l.,,W/k. ,"$ԩ掎x8`#>=\GaCN-\5gm&fI^AIn_mMh.2ӳ ҲWM]2Q9pYiݢM9Kc7FN_(1ÜYY6`uJuK~=wyƌI7caܪ-;wY?Unq<v񧿿]Nx=Cn`X/_ˁS{M[DϞ=} zn|p uu{n-F`[6Fo\o~rE!K՟'Á(`޹3\: WwTjA ,b0ª#9,b+pvkv܁+ҿmۆ,Y XX)xQJMMMHH^`̙3?6O)`;@h7+ ()aN#_!$/HeҕRQ@L>B:uX#6Ҥ 'm nꋃQ<މF:Ŵcy*莍qbʐL"SIȅ(QRAx'Ќb*=1-N/sNvU )ƅIHLp[DMЂ Hƨm~l6G/GIa?d?C4F@' bG u1sNJF'4ɇzI $*gF?i;v$i8 t%Dk m "2H(@C%s l+d-)>gG(izTƒN?!466wuuGa|=FGG`D,7zj&t`t, -p7dv彯9?,X.H$A1 ff9L>,*0ӀtFs :U',L:l`,)K(QeXKz`IZU8E:"Xh @A6C䠔$%L3\dnca ңbUTPsni*K(*Ħud!U_ *+BSfO0~>'}aupСv3{Pl>o+#5UW竲PQX>Ǿ_'Lu׳cJP|+ RF+7GC}- $ 7=0ȇCہPx;(؁ccߎ=z'@kP@(|.@x4>F n  ߀n\$k뽊rj+׾ K2xK/|r<xx4ʹ3'||'N>ydq88 ؑ6F;zz`KkksKKSsKcS 8а=p]ޫ߹]kk}@_/;<JE0xbYTfffzzzjj5k/^<+s} @aב8PG!i4b)i3 r]tI͢si"plE#'\!, 2"R:F<9)n;; :,NfKKI .X>7\`mT&;)֚; y͒r#'(tMrYbcs[!(ªÌbHNn3hK*|IʂR&e:MS} Vy7jv`. 7O. 4)|BoTVKOҗX 6ӝ)`\Y/QpAbTNȊD2$8:Vh"4b1?¹MPFH#Psf*:,-jmfѠR+'PH`z~V+M5e*!BH* Z0QEXeBVZLUU9)sS19amu хy$xL[UxI i7ŜMiuuuX>сev{54+jW{)>.:9Ro5ctTT{`m!(#vXzfA+@wީx(5 4wR{YJAe)W,Z )kʜks_O{+{kV6ƃGOzw`0pgd$04Gr 7482ooX.7FnGDLA`&'NL<&F?9ЋU\Uܾs~pmm˖-XiiiIII .7g~NqE:";eb՘D>J/i%@'pj"I\a 2KT}"*3qXcN)0ϡR.15-r8V\JǪSI 2NcZa\(&I1a,DJ<|33" ZL5 Z*fRs`Ut:Q'5& o(<\K 4j"]ߢ߸4"<-B#q*MDCǏ?y_bpz'qT >ak g(YmFNN B8pO6QF}H/D `,D0аH(0 [21XO;H)@q¦b׸!$i.S WDY*EcI\(+.$lҜ|HA6AXEu dDh 1n$i2W %iR'ܲj&Thz&}* 'O_z伓E+H'{a[g''(I::/$r8V~Hָ5t^(cTN9@A0pȌ-Dc5 ,NAU7ݭ}G']2]z~@;x |;Nuww  {; [=aqkv G8gqh̃, !u< l6vԚ͌%t Qv@%BxNh< wva)ϱgXh_.o~p0:N~㣓cSg'>3p'Oϟ瘂58[*,@O('?~039u   $?== oy: W_|+] ~W%|A9}\י3g::X>a>8PC??B`@ώ;\d;n~XHhp"+E)qF I>pT@& H"*REqH9,cĘmȍaWdBDl RH$bnBIEkQ*EBp%L"1?zf(Pd?v(bBY)^ i_< ȀĞx_"/R|_>x|#CFh_F1|Ŏ| *>LKCNL]b)D0+4d["Ƙ2 -#~8pQo5-`́]ﮃ_|ꥫ}|}2 K_~ދqQg@:uI_;*??~ HDB9#G[Blv757? :Yd֘kɁ5f3lS^XxYYY**!!!&6vժUK.}w. ?apja1r["2 yB3_DhÉ0 &TWP"u A cLF I/G6iC) ҏAE\ S.y1#44I_pyp /EK0- }uf'J+x~+܋6YVy<Az.\H'ZMLFhVƤX)}kZ)+0* 4Ђb23UF8S b7@Q ,jGVQ44[؀T(₈F5*tC,?s_VMM9Sy}K~ Ǜ[]K೷4zz`p`يZ>Iz8_ ǠW^Mk~ ƉG^<X}&W!$G{˻s-xxx 2x/]l~\ۏgϰ[[O,4;yK@:&<8_ ky|(=ypgE޽u}[7o\y׮t_^cv J^ltg~ZZ[OO 65;FygGHZ܀U5@`e;\_Vns`aQ1(uxejqh+EzvڱcGDDĦMBBBVX}׬3U9.X>/aLE2qMvgHٸR dQr HEq8gN%iJ&TQG1 'G:Mjyx¾._%Mݙ%q$rW!#YRW΅I Nr ڠf%,mhdK4z`"O?Qׯ1lS]+Pş !GoM!E*3I K#[=SME>uZbP5 2)هʗHk'[2 pv$nj?ثō… 滻ӧ ʊR^HH#Qfie\y,@~<]6?;1n~y#? e;gWn4ȵ{EEEv@g?oWWƒGUUF`P0AD~iJžK͉ɩCC>?bc~p:6m_`y|*xkvF/~`"OzZzZāi, pw|jRJ"=C?SJJJ͊ۛMlr=wjͶEA۾\%a}TƘ[5c#v MۓClMXkAUF$EJNݯ=zφfh4bDl2B}PptWB~щѱ#/H1>5CO?xP gB+?oBW; @q mmg---O>x飛L: qqqaXXXPpK,X zP;RKs֜p$7߷ؓ2dr!InQ>Ew%UJI|?E]qnPlęKP3f2ZqPQ;([Kb\,ZQtjEiV'G|9=JjRILN}߽nާ=iRiPwoՓ;%UW,TFAY9EWnDk@#o'M<(/Mږ@2J2d**3dn<]@ `XmE /S}  lEe(Ou4w8ܲah 8DY)q Sq!髫BMdI2+j 䎆NQ:z2)_@crG' epE8E iC\6&5Ix2/nM31x3I_1`(r|:MűL9k5E~ Zg c79q kj(Tk3.))E¯erR|k<{zznܸq;>3mG:P[_W T{U^9\WRkG~--),c압WU+)O#+MP73I?(B>^@GUUe}}}MuLIyyY{{[WWWG{{yy9hD mo`gz;){.XcpoUF;p?O(3/ا[Xs> ] ,ْ._6?x+BY!&6!!>7^CΜek6Į|Ły93pͶK5vHJٙqkҚeaCB'nH]k# ,*136gђO~|mxtT\ztr،uѻVnIYpe̲ ?ˊr].޹ͣ##O;΁Ap~(8440<4 Eq ؏ё!݁OO/@??+2 ?M||tlhdic=|!@B﾿w@.@ O$>Cׯ޸vkWz]|}<__;+;|S8q Fq  H}"VGām֦斃M578ǃ+*JāTO--mmm---Ń  3gd 5T9xAD&|@"E$=!Ʈ3K`!Wc&QH2%~$X&!6 3Yʆܕʠ { '^+sr%KϕbQYutxnY~oN}^c >: hQ0`0n x.ff&BJE> y6@YT&ÑPMc$49iye8/IB/&Ain"(" YRw7{JND7'6 h #-JxnR̒NjȪv!Pz!w h죩o!fhv[0Y7 7Eīj45P2处StP985 )%[ovEH ܹsxBw>}̫*,(c*J(FSxEE(@tYqNaϱsN&t:̲{Ɔ]'Cr4K޿#)Qbbbnn|zE,])j{r-:bvO]H+47Sە u3K1ӹo^(!uGR|ܖ])ؼfo@P\b*#egZNZ6@ `}.[}{>M,@`ϥ /Y_Gؙ3]]N3NKH/?Z>w4G)z Ewy&q&d\^Zf A]zzL"*Qr\ ,*Aq!=qr݊"Ԉ$tT)_oC(./~ܤĸ~zoo/ԇ0m6TVWUUVJZVѽf^1bAο8meP.ǥ!QȢ03#@D#T 6FE.U`Fe1<;xl{./ss~FźzoWqHz|&]@gd2=>2:WӡC'Nk9|pCCRS h3(elĐ^gu^?~r` >b߰7 hA/ӛXVi;%-K_\R;5%iSYk!+V/pDaF[^XmZMƏ>LޙkNB`,M mK|bz@ ;pzM`?_03=3jfjr4glb8 #铑'>}hF~XoXw޾7t[C~-ҭxڵ?KWrr%q.]`o^s=v9}\7'wFtKOgNuNQhC8d_>;誫Á Zp_NN=)))֭KHH7/*66vܼyOG3l!KThQɀ(~|#|P_Jy h<Ùř8F{+p/q!03yod0IIN?S*.dD0!ux1a 45F#avꅝ'xc|&b#0m VBn;rH[[[wwR_pB޸ v@?%}ɤAB*[QQN`NHR 5)ěP=qI&S Rbq(q6D%<L=/,A>:̀@cc;ڊ&7j@tإ!'OcE/*=ei|"VEp?޺uRhY ax["gw bXLي?&oŬ& Qܰve|ZUR[647jU"4[mQSY wEE*$jTk6n W-R/+)/(,(8P0TqU{vgOLOckv 'tCE*4ZZBW]evdlKtOAھReTee`ďqrXk+ hVeb_j}zxW޾+_>~:LM̼x^hn{bxqCnPKׁ"f&EBf79IuO<ϟ>}wv{@9kjƏZWN٢2PRR ݭ]6>>>:::11۴iҲG Y(% sJx%Hc}-aFay$RdvԖ*ZhRsnȉgZDzdҌG&ѣ===o7o޼}_`Ì $Y$y=is dl0~d9TyBfC"n"V@B ?I&ɍBeRcz4WR(1yuq&|ucz-΀~0.?mFiPݣ3>5ml,Яl1>?-@UZoA+dWmT몬k؊%U5-e+ʌmwӹK?F?4ccZGOBhlsǏ= odl1#(882; {F _o߽s;;nܾy֍k$]黄{Eg΀xҒO>uɓ'O'BTgϑG (A(<fo?չcN\8Bv\xKliMJ[diyyy +**z=k lR,*o1I{*E|ezOx8SХrᐔb<6jGZ Ns40-#_9Ҩ;Xb4YʯTGK0Ner|2lhzx)Ҩ8MUUˊIHZ= 8]4-YQ%5,8r~p'7{!j{SR?I x a(#6΁qO*OIF+B0@D$~gKQZ2YS $v5)FJ GQ)YJ)fH& }M恽R^I"Z.ez2lUЋSx Uq9+NSbed,JoBWV* .*C Neprv|{2I3So˻/Ϟ=9s'žπݝ}O>޾sG;IQ= ^uDB#!Dp F{N `LAs: 7Qg|^^\hsׯqaXW(Hs~E/=iV{ *fCqo͚9S& 4iY MjrTI؁oM˛YT f /aQ~*bE4Qo0G5?oY :}復e4f^k4.O eeen_b04][@ CCE\g9fiqyNV [ˢk+M(C ! l.o? AA[l:uSiG ku7~+׵U6y+?ty{fۖ&-^[q 5r{fK5c7.\xx'OƟ?%2#Ar b >xAA   {w!@] ![F ׀ׯܺ~Ƶ+ׯ]{/^<{x_ƆשL@ 7$@׼~G=~Gz?H< "ߡWn +$_/n/ϟ;wEVZU^^5f2pC- d9B2҈La%".f!хEF5Ch9 40ՔYNcBׄǐ&a,IH,#,R 2r4"OqOlQw 6Iؙ2e,B:Uv/ g!L2 gk3 ffɤO TV,(%( *#dyf '("✓P*7$LPGP!OI0F|(Gɘd2DYY HGi*.%1V_*]J4*T7>_J&ΆI P'Rdʁ(,T)"CbC#IΊ^J,aQ#,B6byϝU[Ν._ ?S'q_qFuãlEd2 Gch|ęED͇!:tQ0F2QPPyP((@QTY6jhe0FW|VYYI}SS=_ܿ Js xsltltAptdhdxh{JK` x(_-x3O3`?>q)~]GA]_;Z[qu~]74nTZ_P&6."bihhX,_*::666))1++3]HWd-ײtSHhD5/grHJBbh0Z(B@hjb%lCHS$% K|;XH]c^b! }$}ƨ J(ƥ>TB#Od%%FҳM&CSUtt]qB-$*rIRrL}o޼c^oc+ܐFparΎ*n.R7F D"?! ļ-D0hC¡Y9x#w#&,9ǖӆ+Oň<̙7nLMgvf-a!ܥ1[:ΝP~Jy&VFF\VK 9u4RUa);qh_itHQR"t"9Wg;S>vF2vvX<0?:=0odd_ϟ9sȑڽuGIimvXM<ɠ5u&t3qK`k654)!ju{z 55imғ` = Xo6m<,d@G:-l3۱Cκ: y{:qݔ>А% Ж]92tdр||PP(\utp5k;KJgw!:źՖj (,j]2#Jkj ]j4[Mzb}٩1115uMͿByi0t KE|icqr%.JPYWVTרS?&~M09Ь3X4Ma<3Bk CtgjD +  r3JVǤU^: )8.02.){]JA^IuQEfT\)mL٠IUV'fW& S2 6W4h:3|ǓS?xLLcjj8 r?At|=렻g~qɧybOǏG}y`!<@硎jٶh7ikK* SUq".1@`Jeeff~ 7oڶ?lY-LK{%.A;@9&%v|:$֎t o]4d9섞HHPT AM%OIJrɸQgpЇeqd8cJ΢ E[4#чmƅ/͖fBR#*F0A.:uE4``{˗$R)zx#$L XGWr2'2N2W(-8aY$ԴsQ,mHwTSJvq>9BOit euvGp|_ȁ{2ڶĞFCXzpNDoa l6yzQg 4 ;<G{;k<Ldģrh@ M3Xq߱cޟ;v`o8;uc'Cx, Y0tՙ2Ow_y^-Z3oR)=B_`Ew%Kދ]zÆZ ^^ ~RB[6}"*h6䥯_^J OLVoRSS2"daP`䪵jMo9C*Od4[b"C":uAjqТ &ksEJ]T^kjL+,ڨaյ5y9ЯOqEr`ޢZuu vw/k]^_E[\SJ,I+J,YX):iCtbթYTEjcƜ][Z,㤚 u\جܺܪԼuk ZsoGn}Oϟcs{919n)(8E{|pFLMۥM ѓp(X(ޚ/'%>!>=}w6ݾ9v(Ã^F˗.^ŋY xԩSB'O?qh֝jqО}>鎝:dSYn~ajȷvYQQ+WB{}rrFffn^^NNvVfFvvfnn?y󧨮,԰t, cft*7K 6[C4F7&&ѪL,_jʌPIDg~Ws^?t8L}{=xjv[݅O#I@b5d6E TQ\FԅF3j95d1b6؉yIJ=ZYlC%@MF15PЖ%]QQN8ʸA5\Be ZgP/؃0ˍJS((8If|&j(N1MK9NCY%DO8τH}Eqvd!T!tɚ`3^D *~ēHU)5IC(HA7\OpAfa橆d.p' 8!1 ZZFuMDR>9p^,# ֒Wg&Qq' cyAhL@_`iEfGzkpg~E;@{^={zt{=ݾnw@cn|Нu;Qq96k5[p{=H$=3x|xZ[S?de5} ؁.ll8\> ~ ,".vWeC!\_ͦ<2Ƕ՛k4-[Wm\.oY<;ڲl[L6ڰɸ,~eڬ꽇?gg'=3;7w}s ރ9R,!ps\/y {g  ~Ӱggܝ{ԝ۷no~PO_NM_te#pȅFŋ.B97t/ >ϢӧO? 8ww-vGC4/`֬26oٺi󖜜myfsaIiiYyټ\ZRPWmYNOYG&KULR&bjѥ:NT%R&Ee/ (֒:EHnbH4L2h2Kb$I5+Q Q].ON#ғ.\>=)1{{fʢ@5]"0WZy' XGwȑ'O9sWK#Q̛P .g<_C+D]>!#ϳYV7L2G5j",PTHBUOJLJݘO8A'/RSHeA<@"-҈hW*" b9YD ,9|_j3<L`$L$y*kpޜP .(ԍG,%\pFFF&&nܸ1111:::44'cࡁC{}{>=0&<rl3.!;Z{gm65LYὖ#Mcc<#=Ùw`#w}/??>O\zwK@U_EpTeeUi#4aV`H ' \ % Ґ)1;j!%qiؘhqI|,DFgNIMMY]&b=c-XˏDSpT[l~ֵ3Vfonv嬏7vEdmym`,{ӆ&k -TSfWҊ߮+(,)..ڞS[ov9QCQPȁ0^??>p|~tCo7tT7S_ƺ +7b}q^y{GC(^ŁuwKsVg,-xJQܕNG'N8·G@fguf@s "Gޛ~(s<*@̝wNݽs =ue7H: 8 _Azy#/\4zxny/\:,x;{c<|}^WbocL[ܔ-+;'wNӮݸ*k J,u ON]\;p7r &ƌmJ R$(^'3f `F <afDBN$.mJ8_".@b-MJrV[=2^ȧ'RZh*(ys玝+ݽwւ5zI'x=`v@`}s[m]]MmUQYsI}.YSi:Mƶ&` !T<@[ImixQ(tDO/ȁ' x~nn~nV<|hѣyݝ&N~3(( {*NOߦo}YWo\E\ ~ "Νo/Nj` <.ZTQYU\Rh{KJWT2-Ç--榆n-s ܺdTeg3XXVcV>Έ.CE"1!~[ |2fB:, ,` IG4]q/C/O-r$#Pf3My*2ه=#T1dQân\9B(T)kv5w/5~H2ƏGab@%YuBL3יz`R$+]X*! 9 :um:M&"Y/Ĥ40R#]FHӈdwz^pS0( "{dn)e}roRĕ&R<В]PabUT/+MFc ZId!'{ "S'F Y9:h⣆MK]Lg 8ґ6M.ͬ,/{Horr7._O>9{ aG8vYE0Gg8x~+s}>T|>tNglN^'p<$G_cD  O 8qGC#@K =1g=$] X,77t -ȗbhH4 b<[Qѥk=/559SS$RɄ0EvV7n(.qTqN)^9uCYk_Wʪno'K ' Wc6_۾HE#5|d_E]y쌂 @ * 877=7pnvқ !@>W$!wwƧ&Lex1vƭ[7?OmnFP}C\SS^^[EE{m۾iOx[lݹkWiii]}xzp' {l7B  X'WHG*TIK! p.2H8GvJd?}E}rd:P鞍ƬZͦB4%9M*QXT&+Ը@NjkaA5Zϲ&ߴZg3!7FQZx;z!n>ZSH;2IH'v0=}e`M(3 KKLqB\AaؐIxq"""% LzҠ2.e9E0 K%2ƠzR%-, aC2 hTy3MlTfO,1Ezi&d.j6Q,s:%d9;G:eZ!G'POJ-YǼlg۷oOMMMLLwʕ/^ȩS{#$@_?O8 E_gHynzz]T |uyNl6kڱ={a{𞏼={"==HO,;00|!pދ!R|-D"AҾ{r}{BT] R.6.`FZk5ULhЧ~ݞ=:XZR\k_}ƍ^] `e^Ίe&ޠrd88XFboU,1DR`  `@PQI&!32L %Qʦ3yԫ@KQq&7;7p9Y,>FY"E;s{Xr?ݙ޿_mm؏K m#'9}GP|7pWK{%5Mnn5i4MP* qȟ^ nNxGgO^ېESu$-+Yoz;rr >]Gci"q{)7hvju񺺺ʪ o>Z[[o2,VА> :]pl:vn62Gꛝ@H8d<[69`<48;BѣrZp ,, EN:cO2R~b|b^.cqR瑣KL>$Gdfc  XbjE@IxU ,5wSVgrw:7^h4WZFȐ`0;E"ec.D` ;e{'Oލb<쓝,`Rl,RD=Q! dAXhJA(Q@x*bSJ0 B6[%NQ+$A)$YQL5i"8v]*}R?c;DJKSZ{|2ԭK{ׯ_VVV^zʕK.~~y{g?Y80/aəs{Ҍl&4-=~(ި@u{#..aOxp ?=7ު^7A$ '{=!=)큽s--?QZB\Ŀ<Ba4{ᆀ@3`6:Z'*HKI?g>M PwtݖsO`7=n]w׉֖ʊJԫ*=ZsLO-).:߻DHەBT$aj1A+{V}@`@TT*EaaAuu^w]8|ڙDwſ7s r麴ZkhŕVnVCoNmF70hF]Vۭlhj/xGMmmmfla߽x0l\Dozrn:2;gdx9l% J cXhll!1A;&ur c9"őx ]S.+j9d%lCL Td<+ &! VPB3&cџe!gJqD]NWR7<f(Qk9tknTԬGkaJ[\\< ~0 1u`Y$Wp$l( *jRC2QQv LU|uTI δTEKcgʍ7Uxڵk_՗-.~tjܙS3)Ho&2K%><1h@bD\~O-cˍ8G6Q|C6xs7⸰A0L#a 2~p+Л";c_=H Lu56h23v{JE؋$2q{\A#3O!Ox^;՚uC^}G{KӁUzOg0L&Sw0^yiIIIqqQQ=II1~XVTDg5Ǜ1TS[s}>t=y9!|V*I‡X"ߎ !0c<9/79;y8 WI;]9{hu:mVG՛,>]NZJy}F:mɩi/S9cַ;Tl7yPo,Ԕh8mzAP2 ~ (@ꎘp`s j 1MZah8볎m~١p}(_W g zgF?Yp/a޾8\sڎU3"@p !B 7.ڙ:ЮڙN˸k͋ӱg}ɗ~ N۩vfnvog'gk| @ V!y  { BX ַynq;K<Gc3茳^D;[Z2OF$$:CAhLR#Clأ=qEŬćD>RJFu4"UΎ20/F.(<;5n_F*L; zt$6}IңLiXʸn_'hHjK.]|ٳW+;c3"`CB'b92r26?O8DCDl?+ L!"d+>}S}B8JҀ @ha4S|mVE2U&Ԥ+Š엓E3C.3H2һȷ0WXp˭[oܹsƍk׮{}w_X^ױK2M'B\ HF:9@|>W7W񞇼P71aĪ9^4Io6Q +!ti#gNVNwA\By{cf(Na=ZNUgi٩%B>9zA*_ԇKUݮ~fkkS&HX,SCgn?~fY,mfz>\sBueUeUաjUg*8QHM]]mMhnn2q:ڿ'w0%\V&ffm޴qlڸ!+3cbX ;—%*fRv <1 $O$jP࠻]{bH[cѸg?.k+SsLC (DP(MMό}oV6Gs:&&ʊ{b}GXb* ǽ1/7Z-Qw}gw9ݳ=go0;vH+F|B8p$24 'Ν/?|O>|G8{ţqMICyH܃qzl|> z=ÃXnwttd2pDV-ft|px Sι%,t>"\$Vf,/mm1D/rEkDq]Y1C.,HHnT|}HEkD8gITW֙dd_8(+L uN[$utiwrqZy :A1'qNV.E6IϸUg``p*wM3SF-P) M ԗ]Rߓ'O>}Mc]YiL ߈1T%!Z.\etjbniY)Z+jNdS=U" } ~1dsuY-U:HK鎌"['z)2)RC D/)b3kl(e阵(cz$PQSaioLyT@rNc=J0DygN]zcw޼yu}o?xy`oSgN璱d<" BAX?1Gt$&{>TnÚǑ! {G<^بR؛|iLG!(r3pS x;*CB>`/{Ы``0.IsSHMEJTT[:%fGFY^n?><4ԅ ѣVs\nk{WXGDZޞ. knۧ`%9𿺲8rC:to#!AD$j!:@Dwp\.kDDp:VKkMMvc(n޴a [ K\.̪ _ pI_JAcN$M5~ot24s}twTvkcqac6y828!HדtqWlu`ÑJ$^فC3p3zYkiA8"D  ;wkk!([~>?靻p_}@U +++8BkW/_546@zعn䳵Mz  `fcnkh{sK ŋU K=M4s(/L`w6-bV cfC&*V!lӖmTB;c E(#B$CF*PJ*UHE$Ԭ^"GŐTTqVh(rTDϮ#zA}5Gq]qЊ4vEbCpld%f̦M6$@AR߂]ĖSPy3P+O<Wy tjݹ}۷}]$\O!8*2T/ـĆ3E|QTru5s jGz)/4 ]6 I际&}˂^~4lT7naoQ 4XttHhВ9E-LEaҝ NQ%A#'ABsr4i?s7 G*4Qk| ɐ$Lzvɜ`3_`^hiZ4$5Ϙp2ѡSr>Ar]irfIkHt%o̩Z[]zg[/ ݿ޽{޹xػFأ T$N\(^_B @}{;9F'=`  I^4R^,!=D<>]rekkkss?V." ; %'ۏ=`3]Q O_& y<#sZMAST`ljw9vOplnnnooi@7Lv``!,Cأ V&Du5jy3٪ʪJL24Cg-U -8f++sgu5jk5՜o3[7Ba[kp0ER𰽽-**8)))IOK5 N!074J Ӱhm:0YnU=ζw-uřIuw*F}dW[SSK>t:\ŒI_Łsir` cQwp;yJxF N~r*6o?ہL,Ϯ\蓿g{@x|ۍ D#B>EQ02>Z[[QxEDv|[wyck7nݼq^L%Է0?wdi(𩔄`ꈑt+lU5BpjC9FhFL*Ԟ8{<+4RG=GMCBjYõJc9)gl*S9MZ+c񞮗\h"Ib;LH3x>RAjU(6QR5~h!!r# HPQ:2T-2dCq:!aL>rבS'ڤL8qEQc`t{Pq}:v'B+9R@e2=N;וE6K1ɨi&T9T[K,*`|ya3wA R _f:W etB/_>S*ZR/ӁsޣG>}3 0P]cNǨ9=rz<.`O@ ލHk{s[Xc re<ŶvxFuo.-]{YK@}OBH42zءBciI$Q(-2q$cq(P8QI| % v<lv[wweeB5)2x?Lz>WVFYChPfՂ@]u VWc_iTVAj>***'͕f(k*n6j,X('a| > Xj9ydg|R)AAV[^^! 7:qcvᅦ7:P O뮩4Yc 8aڒuP7ţ;4Qx}cS?vĹPѲe'ϷY;F^op*01v []A/ EfGX#kCp|??Pp ̭^'w~|5QST0xFF6f=2lƣڊ^GG^@BH:U67omoܾW|+ťX|~ꛎB&rH!9~H a1 3)+ʋq A;XPȁ|&R0P0[Fpm*#.RLډ;E^DZa4wt*"Pf^t5Z;N e p8/G~2qj];#h0`gOЋEڟ>Y~H}/_|կ>]pB!00!XA$.` J33~X!AĂAM'  Jϥr`:nVzpq_Zc|2hTW|( GYj#_K-S:[҄&:{IMWW횩CXqtg8(%]&*א],U:9WǏwww?ɓϷ񵸵yƥk.{ˌ=TNG'p |~eu+u|~"yy]nF{Nݮq?YWSY/Ў"$ @HHH$L ddNc XZ"(K*}/^c?/{DԪZVukU7s=w!H,K$4 {/b歧*0C ,lo36mAn<_{(kdu$܄6YǤ4֕7/k)EsmהUWKXM٨:6>-MuP(dvizf e񞄔n2fXlSdi%SSSc+&CC}=^mz={tFzL& {F{+ =Sp9b6{ p83X{H>o}ݻ#55% nTȗ6~U"Wxq!hokiy|~8+h_)R ~ TE,}'=`$& )4"Z$䍦E{>j<=pifxf[Ml B 90C[<]#d0͆^0Ɵ?vff6۔:0-FFFE/ .\a>q;qߺG=xp7_6wryZh#>atNRD PbFSJ[>$N!.PxsAX!\5 Vb,xۃ.A@qQ!Q h"Cf!/@ad!.*E*˕#7@BQQ$HT*da5o.ªң{0\1I uEhjJxWψ`a.jә)g:d˗[`zO?_?[ ]`o}5Z^[Y\e'X( Aw 7{*`<$='!z0(8H&=|UVc`o#6wݝPu}&YOlaIؓ>=ׂ酖`colB3Z#A{~tdPwHsn:c@80F2zk K TCl  {}~|5q9v|F9v\g6GGG<͆Z7)ps;Ba[!=P=z=y;C5䨧. @&!p \Dk r* Tp:i%CU,h$ѕDYP-MO`=2c$T@𘎢D2[ӰΥyRХ {Vr YyXYeLJQ(gTP'H,=k\UٶRU珩oggӧϟ?r%L:J>Т~6JIA!)1C8fXNl6IH2?, 7db3(~C̜ͪ:%:TidBFEbڬ͈O4ƞB9 Qx/^rqqai)Օ5 pHߺs]n6{bs!:l>&S od=JxcGh|zukN5UjTStvCcnmUX*FDžr &nW% ɤHշ;Vp˾`"3AOa0N/MMOnj`If11+&sg3{{{{=F 괈. PK,vi`6MgCy=:]KK`@g5::$tt6+9L,-.}811f2OU93@F87|nE'' ~BѶNcߠ0:7uFrX\撐 8A:$ } /lwlf7~Wr%UT*S]MQOg0OcoSi_t[;*^1.u;lCIL=JJ%5"tO=8U uK]:CW+]N;nq %Tޟ{c98pMB`9C)P0$"HHP`ЏYYY9v8?فJp TGr=;9$;=<^n@PLzDvBMDf6&49)늌@ŴSw1q j DնĭEm$=] ]"S-eJ˜rN&w`4Br!Jk v41r4RP:@~ ib57ǃJþT+91"dg[F9pXJ"RWÇ?*Xbr++ 90X-]͹R9iDJj*K+JP8A2A8y(R_M93UykveD%)@m"6$NM/@S(.]QA›qqڪǽr.9\.qaE8B@*b{؄^h0*9ysZa}a\vWC6յUǒc~~~br A0GX-H,렕-V<^F*&=PuZNAj4ZZRnj OL,Cw,>+8& z}sSSMueYiIYY~@3gA\.vjbS &;t:u*,Mʎ>۔md~ptaNۙmdVb#9lfV\\5ah隵چ.sv=.轭fa<;ODM3e!9:ͳ7M#׻{=&ۤo)x"I7L@olD́nq'q+W xp``ȱ"\}- n Xx|Gw{nn{/m5HYW?5%$GWs,7)*m7Z;c W272{I&C7]{ #@SWZ8P)ѢNVy: ],L5l~Stej\4*0G!5ux{,DAd[q##VcDjJ᫺ZdU}T]@HW)dT.!!dUn66ba-4erhE TZ)I W!APĶs ,_d뙻+8XD,i]EjLSOH$;QGj 8/(cԀI] V*drrFPr%N}i&z+^~ ۷O??|^v Vln7^a x/򅐨Oװc> "sďX2*aEzqa&e{LAG''tAu8 ޻"g٦qWKaX⋑İ٨nwTzV6խ,UxeFFgLl1I ڧ,h?ϻZ^/eH(pjjrdtdttC6 2ZɄ ~V`,f1 zTDBhuZd?JjZsUuug\Ɓ2(.vHfC}}幊򲒲eKKJ> ۳~Bo`3OuuUkkKoo/6x<ٵ̱t:=%ɇ sF,֑yo|z)K%XB2B*Oč/[ &^,};-3Xl们6,l0_(L<}r}wO@>_wMmg?@;im ̫cc$^$@K6 ;H$51`g+ϴ.m~o;񕿁o}9Rp.:mgٳh, 9ay8m~ͫ @Q'ƃ̿VaK i-VM5D\m jnVaakcRNj;GqTHdի`0M JZkT70e Xҍ' Y%'[Z%wqv2oTȊSMiR䐰{S۷o*r".PGU^]Dn!UX"R 갸x6SE+ci5ސ$E J崪dhr Ňfk)U)MY"2kTG_>W֊*C \)' - XAW΂-n~-?).ղ& èNw;n^z-Gbׅ{HI$!ؽ+ef2GIQ  Wx,Hā=,y3=c`M3#PwxxX}` BڻKۿ-{.v->d>=lXEᮟ罟h }Vfi~jbn3uk̜z4fJ8SIе#pn4',Q.cσyܮn΁hho"ԖL&Au~k5/SłѰ/"3=$~h ~n'~p7JNehEo\6bs'?V.Ęc fF!@Q6j88[QøfMfsɨ3 zQo6Qb :_Antuv^-`pIEZ yzj]VV--)-9u#O w"۲^<&zL =~)~k ̥Ɩ19znf|/{{}s; $s|$;p6au|t_F *7"yO4;\o]tzKƕzrqk`zZDs=7o&,גIBhhuu|>?rHnu:<b@ x|KM( X>w`m~ݭ|v}-RO@V! h6 A" PGç)!;<*jAx9G= /T\Sp%U/QM8fťH-l?ZfsI/hRQQj3re୉r*Pc0Wc.UiNؕGbK~]4n:ԇׯ_yQ 3"1hEp^M¥JȐ$ڡ 5*PQD;(Q)z= 8Jt'):@ ȑE#,FpSP&F.WLU<Ym9QLi Oze(=R},VN6`f=@pf݁{_~#{ڝ;rlEOw]zrt}aϋGcz-6GrcuZxxBH|̙==ݓP5rLKx9p e)5zJgƧnVÛ,-c7*IH:u`2Zn%Df=9 sXG|эPlDMwx{fy1 k-ikƴ^ 6}_o|pkƛnUN&p0iÑh8 [XXf4qcrfzқ吞!^oMEXB1y^Oa{1RÃ>:>xeY ~ys{,oyAthGfH7 sml!1-a'"ȱ렗j,Gpb%٦Nl?~ɯ>`Bu é9dB0S*J$!:yn"d*d66e`&/Xeacc0`ɋ־2JJɉerK*rk)&$K^=w={K}WW zUGD =V %7iZ2N NćZ"67ѱ@Q XHaeR]2ձZ6-ٯYab|fM ur2/aNr c 1x./´|אi"j}}M'H_}?aѣG_{~緷?ާ[W7>/B6YK1H,)} Q8z'^d 6x7w׋[0؋&d{e{lolަ`oٹٵk=~]g]At6UjߝGzÀ ̚sqnbUf@6f|i@=0@cTP`6v f4qc$V8\.SѰwB0 ^ka~iAD"/`$6{x,y"a/npԣ#.'IJF=nъW, gO[f\0gcOߏlviVuҊ"gNПu3x1dbLXh5:52 {}p`A:yRU'N~>Ǘ~c r,.aS##&[/{.ѣSS(ܥ(v| հPYxo|’{e%{@PȗJˮe/)Ssx _^Iz_]/FS@29or%ir42:h9946>=7 $20d<]x[F}]چa@dKAnpvC>gqit.9Ct:˔vۼfΞ>&̫$ËѢh2\N(H,.>\[~[7w/2xm\X :[tփ=@Q=A|pRkyҶTC8.L/O~OBJژvGNñC5Ru~3br$YERVA@/N](yt:@E+r~eH8S,b KCrɰ4w޾}ݻw_}o޼y`,}Y#?B8qZE\-d"2TY`2 LJS^F\&SM$ƣQwѷmD5*eͬZ kW ,PB2A*eLMOdf Hd-'Cio݇gT#$bkY|Yh:JXy.T5)#S=~'O{gπ/^>|x/;7 {׀π+xsz^JD< -uʱ;{O0@qU\ H% ^*&` cs94; {[[ u{H*{[W6y+e9UG=z==Le~K87r3sa|r 25>5^5Te.iHwpS1XOHYZyD"Gbn7S-ƃfNc$q8:: phd80g4 ")}P`I_%>vh2MlKP( 㲹\677gZѱp~Om Cd[ܵ[u+DI eXYY+-tZ.`(7<4os ߙs@0~9]<^)՜+SXe:zco4KK`+ҙ<\+J>ZT"ygd@4yaѽԲ h ,/炁p<^ߺH4wb }N2PdǽBrr8:0NRb$O~󹭫7~NoGb^(y͍"2jpPCgkSWk{9᰾Mأz !q!^(Ў08s@,4 OXŁ<T!ep#y gb62 JDWY`i" ȃ협Y 1V'gA;'I6>#HRW> ZրVHdhCS*Բr,?RnQ߫W^~?Q҈CD25UP"{k?`FCؾ=5{؁"O@$d452%#>eF^DHMռ*[5d:fy TXFʄT )y4~MTICs 0:2T?joYAx_}NA@^T),"X {BȘiQTGcUU͎){nڋ8ʼnb.ڙg@͌eSz*Y)OŠWB-Vk4sƆ^.SRzؓI%K%K(2"X>Q@Y'aH,=\ȪJCR rl{M|e. C7 #iP#P(433-{x?فt9gccc#/ ~þA$q48  ׮] {{=WoYca/7n̠O<3Gk(XD#u@6#)(` i11X,;?_?P"6o^BCX̗K[J +|QVmkJxL67E JLjFB -1GM`dχ*4{ nW7JzgZ(5S\p>:yk/$=5~*xg˦0u^WL&|JhI7{QPF叵T*| ^́ryv1uf~CA}{d<y<"Ta-r<{p=*c!T*?2>`=BCXri}8f6=lLSP>_B#Þ'|>nqXxN֚NVHJ,^#KxdVWJ"KI2-=X8XPi+.lJiIӐTM7˔fJjFV4`/fňsx~D,b82Ȓ" FPrbLCC: ްwv083?7Z  *Vù7g7_81>6>6191>1)$0! ~^$F||CC^7@]^J]s…ݞApp!aᗀ}{<>xh-X{T8v#|D$ Ξmz!aA@ |>1)?w]8̓>?79]6VaT*ˀ\yg LzOo./ߕVSaNH A+YSyW%4bDIl*˻AR;<_ȁjȕѐ.Zdf4"L&^V:1M$`N4@8N4zX}HI$FCa| *̨N L$I(|b (WJvKUk-εos_<ׂʇtCSd24gO7A}T]A3TΝrŧ(vq`O7aVz 7)F2DR2.EjYa%%b 2@A*/󙇛eQ ;'Yk52غxJerk@ 3pPvw SÝCq; o V3 homV7 ?JU|𚆡ғGQ-U4|ve!# >e%I, ԇeVSU&˸&T{￷q!. nnv6t6 Nλ^.sr(g?%T@,$,r+5fD8St[T9JB! ]nQ{`+(pu\&79gtpA +S0r [yz|ܗG z@9WEXRxͿO}^zD}z|@PB""ԸK?ЊlpP. PHܢ ԤXV4D}5涨)*vRhlQ!1jNj+lJ*uUe}nѮ ȑ9U'{UlJ1g.-U4H604ʶ3~T`o{/^x_/':{?p1 j{t& q3fj&rX`/ H+ʒ\ ̿HD6=('2֕HHk%$gCXvxuffGe!Ô)?)'O|\= 7^V-;{{c<:Ǻe'ّ{w`%iIJmsj39$t哃GZz5U+Vx*i&$oUXK iV&kjӵtURU͌@yڶWsp4Ք jJ&h)[Oӣ2ٰ_$$A}H`@A>G{L(H}*3 2`U~L&Y[]I./+=j5޽鹹KKYֱi]bC\X\|SS$7w?b0Qwxt7D(!;<488007pzn V_/RD8bix{<!HuJgի]]k]gϜ>ਫ?Q]W[}2+9!Jv Qxǎ!XYYqdE%^ yAGys}Mo`;>f-+ͮsB!9LfrrZ#Q[YVyI[[_O%?*+֣F^Gt\S~Xױةd fnc30cH:6nv$& 1eD>({S}ȈB=,:’X6p@tX _E=" l~sꓖf/\+-4u C9 h39&D' u1l@z ɶEDu'$&NƒdPم3EBiܛTN K8cH]8MGk!06uJcbJD9Z a$ 1^5)¿>옇O>/z9XHX%1ޘUA2a- K kbq+6,r%bSdJhtVǤD&A; azW4'fkUqsù`u5JՂYMux81tWOp sV]| "&))O~/Kx﫯ٳ>=(O>{0 c/Nq#f:h( bE+HAM2jjM5yU\Ѳa 2 ȈXRG#c5r;88'{|wpP%Ix69'آ0ob̀YO*ԇ +K2޻x$1]=S=}C"vO{} <(Uy@HOމRNT,spڭA>@jn|kn}W s`Cr+YW{S^=9NC Ҁ~=&6CC"bAz%r)NJP_  u E$( #BL4 sa2nxZ22=:s ;Jr"&\'oǎQ-pkI 6_"PCWQhw]G>Ə'gO>|pggBVdi/6 %,!SE7$1DޖwW^tB pUUW#󬢐,:lmRVTMd,/EŅ4cIh)KXksm\,/E%eلC) sr&ˡ}Օ?_{ϟ籇Ǐ/|O>ܸu{ ?%֖stf*OF`~~7!#Ul؅ =Ct ,VFe.5LD*G&3=҃DzX&a]iu[[ Csw{y&trz*eƣ[(J8EwDkr 申=ξ>:S5>a=gvuuu]]Tщ:@60F+4Fk? FkF4 $)N'@GTN`gkW.9} AgX8#$ W  "_J1Ojjlqu|n_xQ|{ՇI=D+k,չBCɇΝA?ؓn1>|?l&IՕlU!N2_W ~EB܂XZ` pPNrFnLh=bɛe=Z^PIt44?@THt% OVl "]ԁsGeRMp:%1'7j̓No!e[y\B 5@ybJ}%^|իXA%J|l;e$TPgB {,TRPM E܏eLIuԱޱ0aKW e*mYb* IcI Tңh n,%elJTF9õXg!Uvoq籣#O?yɓ眾ŋgϞ|~G[я7ooYX~+2kL0cz{؅'GIzcþQ" }Ɗ2Q82rF|TDw3 i؛g{[{zXP!򭭭BJ=w)Şh{eE^2Mx:u4<>&lf@nm(viGy_G2)/l-T~mISg1G)Dz.5uxCm{ZTJhb$I(G']R:I~=b4pxDl4=rǂYNy ;1R (!/~?Vl& I/><2y#a' 5ъ7(6ɇ 0c(v8%VG'ǎR\|⽥}…c(~71Ϥm7@>0 )xrdKQ,,Ec}52rm9/9Rʜu,Q.tUR&.$B.BM g<+yU|ǹin4s%7ٷP_^7/'j+Ww@wFfQ[?2\Z-S}ĶMGRaKGhVB9QPJ8֯3ЧmߡNx|;usVp Ѭ\c(іmR!ŭ0 QN%BtNWrbPCٜ$3/De1[ȋ"4B9aOj.ɓ'ϟ?˗/!gϞ=}tii75x¥ /;;7;w龎S'Pri܍kWݷx;sT4L>G,f*y~/NXYQvl6srffvx$宄{HƁӌ9`o~n .='ERJP!D]aM;ʒxk?CƣLRd8D!ZCABq{=@H%u 7IhTOqCp! GVopblׇ#g V+h=n<6x6<(hFԐȰ=|$ r9bA3@V.N ]kT0i{81(q> b ܑa8F)I(G4%7>ecs1[GQHj4Q;AS0xG.$ ߏ{wQc{C}ƆƦݍvnGkWjAz~04@1v}zaguզ ֕ir~~̿bDjTPPa*; 5((mXfl255l6[PQ PwBAS_SS7q`{ב!lOxjޛdRأt2:t2hV#cPF3D )xF=TgЗ;4h1GIR90Q_XU7`wKF${JS' ߨOHOMxuPm06%,oܓDS3g,|/\8O;6)w㛫!~ң0DbK'Q5U3jK7oZdj*`20of%RM3&+ذ#K#l ~y&C^Q!_ڐk^ez 2ȍ4af '/X[VXG9)?[ԭ`-cO'Yζne@Vѝ 6ǔn M}o߾ J ` ]O"rӐ|.XYwZWXBMd$(!!VDf[Yk%;pXI5BS_5˘mBeB&2ge؟z&QHs[!˳&QEX<~ׯ߼y|wܺu^x@;s?/~{?~p]CGׯ^!͂c4 !B$+A9؃3SIX(F‘KF$79ao7O; 8~I{ľ{y^{@ ’t񙙙3Bb#) ضmߧ? q43"\@w]H[j@>>!L;&iiG J{8Z/2GF;#Hq{\ǡ1d7 Y:f]n#<@Hp=nA)dCP@ T1Xtę2"I@Xj1M&F$r#.Eva|(rPA+.V!@y:=s9`!JP`yQS1 o1===AɈTQ8I- +љPk͇ZZ>oi޿oߞ]@ݍ; vu{_gOQewQQDD4RN7tv#0 c"nW8L;[<Ø+)I[}~/`9hg`NQW؋zxb;@aNNRf ?RKK_|+7Wn MOMN] =|j{ y|#A+t2# bŝ>`Vg]dKլ$I*T4g) ӈJP 1*|@DtEDM$ɟ' 0I8$ډ(up0#L,&R姣Qu3ʣyf"=1'yxtxf7olm~j:\zevnvjfjr285y|=8|^ 0[\\\^^ƄYYYǦh1a b%F 2\E̺0.~a|Wggg0L#;(0={ ?ᦦ&]63%GU|a5HP)R\ (j3/O8_~+3/]Y@py) A8bV-C~==7 &tЙϠuTMM* ]nnqX~Șf.Nd2jL԰àZttuvt_o괘.a1ѩ5lv;zoӁyNWfA9;'(N'"IaV>zz$/&Ǻ} vmqqw<-࡙Kxzśͷۛ^>.gE IS8:J̖ Xt̜# FL8)9Im ;A41Мa=C<I2Ehit5,Z*,+O KGz0 Qt+xC,'h_b&I/AX;~v,xajX$|ωa[Oe$a'wKLE![I}"(Ht$4̠5x`$+*61#C2E *0bQ2n &]7)~A`y &t} I$u0"D;.|\Std@B\8'_wփ{y~%7W?Oһ|seieyq?>e+ss3Ӵ wiBi|۷oY[[ePlz0D7>>&eET6@p xnn4 cw.M]A=${, F h[N+s{PO3 `Q'F`Ql<(N8צ̏V Hyu2f1!?䅽9Ķ [ dk&tج&_7c0 h;--ͭmqnt9PjX6znŁ-ּ<أz٤j:Tjԝ]T7Lfn1tYkpjMn4}Vb[ yھn h zl hQ"B>72@[kKScc[[kGGGooN5uF҈(ὺse%pZeE92\WVZ;ptW\TPP( Bqq +,,iYYimm d$ ά)?~$5)-%ȡciɀ_:Ξ83mۀEDwEdG ; dY! *>=n4 33S53/Ͼ|sNUVwWS~ܳݛs>w'/'/]/Ld~8&1)HHA8wpl$ãtT ?]ȇGUBEէ὚no#p.D%F69]84X]YC##\]lrvA E 8 !?lql7lv5ցvlkSgϴ &>;z4kzVo0Ms8 >2?# qG{&x=aub<|>jB}[33Ss3s3 ׯ]YY~rKk7_~z!yop=}'+ryONE rNcd?L@#)[l&8G !~|DvJٞĀL ve$Ǩ&ꠄUG b2Eb^V $I\2-;4%%H=s%<){hBr&Y|#b1ys>Vcfa?7-VIRԇ n~~Ç\}?~cXM &N @#v'm0O%2L&!(Nf#!n_hѸm , wIAND[h+)-'埐Rxtr&R yjGu'JcqCӔ UL Qxh<޾}͛w[[}GfћZd ۞xzPb4F8Gv_*5UG0Xl=y#s"z ^d8WUZU*u[[k" }pd(p7hz&Wp\W/tglg7|401~T^{S {-ǘP.ZA0=$s ̔XagO}js{m/. x~'0+:𞴼iJGX"IC"U.T I,ŠP1TY)臑&\&䰔2АF!)DWIԕUdŽJ1.Ʉ6E5a6[>wHYrhJaYW])MO#eopI+nj',J} >wQ0#5]̹iL a?B* ZC Ebш4~ h"KRĵ~KK*`& C }"Fs1WVP$>1.KՐc&hB&dۉd$:OT qe #`^|i~ oscumuy+OW?Xb}<{w,/--.C}33S㬾cgg&Y80?;G+$DA .^ AJ4 >zvuyl>% ~XaaO/Oۼ遝c95]M 55j+#WwW_I'y<"L>%/Cy{ ?],h!rL )V5U4WkJKyf5 ><:<7<=^AhJgЩA>^.hq;]Nl1& j<)؁ f!AИ!@snMrf z;[b}zk~Q~:Ck)y,.anu' hxl.gunukkKyeSScc#iGL]mmͩU^Y1VpJKqVS<(WRRTRzAPQ^^y̽f gOVW=Np%8!q.X"fʁXv5a‰(bA#pGWWAEEYREE K h} [IB*.< R!QԶo眾3t:7wbM(;sfpyϽ7O{j+08jlhPT:&|sQD'Rk^l1>a9;OwpNJ?br ҆qrjRX|hnx}ntΦ8/;mvn1619:6ǜpx~y`!!I2wV+.9|'3ե1900@> צ|;?fcL|H ƒ{``3 6P0EέC>7<> `9G`[o3XTŒ*".dt$&HtAH:͗@Da9V$$+D>c.씰vnea?ԇ\5,"2q„xẬC3GJ`#P\.?H7P@fvJf'>sxT@ "+pw0jOt%Z#Fa/{ Tpp/6Ra,m#\½*l!SD"TCN AQjq>$[d,5_T[.^"wiǍl`5LԹ޾}%}zVhk}ӿ|%ج^6lyrs}mmGߧ?7?w{έ۷n>ʒ*6}k+Kϖ=ydqq/Ǧ;D k0 }zuǩɉ7o0ޝ#)޹(޸~i\ ۡIÿ-Y 30p2ʌܑ*A4JvcKSUUR"WNp="2H,y=A>: od5U #M?/=0/\^SQ/mj=^oSzw6*::/hM^i WXhxSPWW].Ʌޠ7Ztlfti4*ds؝ngx=χl6g6MXp\:RkuH^1-l6 N׎O' ===.@\MMBRP5MuuBT{%r@VTRR e`^aa!}\ KeEE%Ņ%=Y)LvSRkk3\UUKydd%KKJKLMHOO!1Md8GK"ES^1SrLQOn"S&/% xVΝK k9@ NG:xOioċzK@h؄b&B7r&Ó;54iF&"#)ϗ]>4Bi1 v#C&46>qix@jZGr:%[(Db̠/76Dk΋]=w@hAzzx ؙ~H>0H¤9ݻ?_.x-b"jHӟN^vwm`ovVH/=J{L6q_&fffoc EhM_w5TV*N'*JޫT~~L>ytLm{z`og|5FR1/캖jJe8Ҫ)luԗv6(z[a Ҡз:)ۦj,PQ KC>n%&Mh[a?j6MnGe\g,^3r,fc[[ <_t:Fo0MF,s݈]ZVI UUU yBQZ&9!P}R*Wt;@YYY邳^>Dzj--M{m*J >N<~XzHYi ?_Bz2 ~G"TK ŤXH)?@!Q\Z PT5551Ďxq @t\] 瞭9[sFoMoltѱQw !z ?2{vS~qQԪ6ZQ4  e:<:q:lA}IRQZvtAq:iN?S_?quu2sp {OHV& >"R۷n޽o~ޛ;|>x<ٗ .././-̯,,?u}65'xv2C~h'?.vXPQ.^PN :'C+2ܗyeEJD22gbUWLuD/$H_,+Egb3LJ}TW{w/^<jBȢ <k!:NB,5 iv ,ű %!:j#c ^2RRIL4Ρ`yXґ8X0l&<[~8?.AP/sc+IyUr!ϑ@L' ^PVs 0޼yO޼۟Gn}uq4?4 mmm#wݝnyqnanfvgO==&bkkc{ksks_ykkP@8ųo<} mSmmmF1ݍ71K>r2GUr;S}*{>W_Qy:C40jjB zGk׉<t4t-}0Mehjp[+K]\Tk;gaAgtdã1hp8PC&Qo@j8,N fydF48-8ʬ6nq$=w7@ $Y6e x!@3z1 ftJvf#Z=VZմttzt8ݰ~Z-kll>;Q,+//." Ь䕔b&0OʊOOKNKJ;HP#~iɤ԰B#Q /!62>62o ':&|L&q-2Ӓ;[b|XJVagс%&&%$*k3ڦ.Hit$066<~~܅Qyx:CCv9ނal4cC>M;g,n?7U6gGҠ{/_$+^(&8?V,f럩 O`OLμ}@oa s3/=n-;KK%eȏ )d(<Tl9>dȄRp z7I,Z`I# { )Sc(Ն&,UYc HI 6'&V $IU<WA f6E& Mi14 +CcXeчG*j& &B@#Rx=y2XjlTÄxG`(22rc,SP\a\14~Y}LAd?~㏯v兙%oavmea}misc! lBna~>yhf߿=|m@4FO W! z|  }M{vH׮M⏋KW?rx9ܟyXxGaObP@rJؠVz=K?Z ]ͪ\$H+?p+''l 16{tZuyΦݹ^M:6U_;2mrԙ;pۣҝ+n.h*l7 p 1FxF qcZl9V/tIFqjFUqKG |3@b$Q>h}:$I_"5G֧nViY_V7 Ԩj8ժTU^AqEaA2tt~2/OI y9ʜ)@B0=`ŢB+k4{OdH: d2~KAK~1I 6 NCD % (NbpH(ՀgΞ8Sn@¢ "R* $ I $Gvl@QJM6Uua|*7_}e iƙ)fN~u{=nMMMNN#CWOm{*Mu_^{HZ<~6oǹ^Zsǹg6]a:++kL&8OQ\jm|msnצT ofYC!x<>kSð hĘȹ 塡$dǮ_X}C'oMOݞKޛ;0wi1_zۚvKMFH|djBԑq $6LP {%f E -ŠD51}(;$ =PAxDǒ)촽$wBJ7BC1 B\ԔtİhBk™!  xq`T ,7TLĐK!ȷmmNf>,m_O>}\P$eA`|Ka)s'5tEC|1Lhmc퐄n&wɐ%\cx=haaSP4A }( lҼm)<DG&(_-հ+I_߼yqyxeyaa~f~vj~nzqaȷ0G_=ѣVﭮ,A}ˬ> ۓK֞?{מ=},40ᱱ?KLA$s3!wn7 kW4:  Kx'>bO\xO[yD z;::NޠRKJJt:R.Cwfi..?ISrR=OM#xh&SF12`~'{^qFUQ:WըX]' 5 MR_)!="4LEZ&_W-ˮ->SVX_簷xZv/Oɷln6^X-f|3v- ѹ]f!+j3 Nn2- 8,Clnp؍6465vfؚ3eexF}X9SUYYQQRrsra|0 //WTGSҎe'd ^FF:P'˒gfff ,R"ϒv!'K)Jy >1Rv,%Ё {@>(~р_>/:no'"?R_ l"?pV_X/4 if pnI, _x.xBz_ց1,;6> CVa9xF<:8|NoOWGuia6ϯ˪7iKmuv#}=F3~m]c/w8i0: &G?{k6/[CCj=ibp8 ɇ=h4a{zko #'_E2xuԝI 9d/Ç?~&FdhA>{H&% a*")% A(t{bV`"I A<ݸUlEydDJYy@!U0$,dːuGwD_EǗ$ 4{ȖPSR"(HB~73uݻwo߾}=xoif&.[_ʣ0|gxoueyeyqiq MV-|_ ԯ^b3s p\._ ,!\oǛ|.|&ؓn߼y:x Y GbE](A ~7ISQէOjXt _n岙kKT꼣#PCwL8P/B&_!((tmI42ˇ8 5^ә= A}ѳ"GW~Z Xq;<.,X:= a,V gifÁ a_bLrB+෋+~_Tcb" ~Po~?`ۏ'ݙr(ؤ!"nR߶F|~5q#F} d K(&@$AtUPQ@w33'|wUi=ݟϣR.x&WB+ls:7&= 2\}Qb2SCv>6wb17uLۆJU}=i:9tvwǫ'G_#Mݞ^?cd ?K><˙tZrj?>jOYχ6w%o$|zvݽsg.{>|`7`a[]YX{IW3V"uf1ۭf!#)$#)K&r!CSXeJZB06eœ q$F U1y1) H9  ILJtWHTb}yI| KhS3QP HGYC{:F1XFJ>Y (@Bit Ìzo B'!EG%Y09*K3$QpEAlc} ˉޏEmDhA!fbtaR_W 9#M"~ .dΝ0?O$ٽX{*1+}MѼ?q_‡R xwJ o奥ݝ~O?}} 7?~W@>'onn.mN þ%a/W 'ˏ=\`ΝW3_~qD xAbbiŭׁ,_l_uu5 bBrOFM5ŵeٕ a"QG+z9%4^kT\UNZ+Ԗt՗v7{ˠ>"_WzR.o +z\.ZQ6U* [ޑAp'9a+IGQ ='D73q x3['t ,ÉFqMFA~{XGlvVߨn Ɓ^Cެ֐::{zk]]]З.-JTRU@,Tfgfe\ʾT(K BEBT O\.?I|,缜y h BE^vVjfj\FJLFJdD$i2H_DJ|x2E,><)X}ѡS{' 1B"HL!F6kmm Iav7qssZ64j5]koLO XhZzR<60=1!GIB0$&\2MQ)|"E<p!#xC^2 jRDDžKUƬŐꒈOlZL#Ie,CYz V?FKdd8) 0<'nf&}R18k0@i(AO>'e؈ bI@vZ,~7yw=d>yNgݜON\\|w$'[x ;bS"$/ b@ {Ν(Ol6mmm555`}̌0ni6i4s+K2Tj&_|Р<^CE.Ri但RTz=}eDfwD@.ޖ2\ bSSZ[Rla6qjI"i8p: ݐyԧ7 @8~8,ÖPiĄ2y0`08 h' Y@dQ}m-ZM-D577wuu;;;zWUU%"p+T) r/eeeefggEEJHI+W*|<./G(P*~Q| R.j5Zmig`9Yғ2Sc21t\Sbғe'HI@D$'D>/ꋋ f|"¥aAa`NH8 BB qE$./؄7 @@88孊0!qd+I~~f&z~IS~X<`p7KEEjZj1v(]NNW{ 7}CLpvvzn̈́f9{;9':Ư߲{<Ꝁ=2<T-crN&fgޢ18*s M zw jxG/'(ꋓoC+`I!2@}> ᥧOVW^bi%^=_F.EXΖ>pۋ.!&`_THՊ˹f%)؄r-L,+G΁ 9_CQh6y rτP22dp+;=/9!28Sî6C=/]&.J\jJ u i&jӦ9Mӓ5h@k9]Rq D;dB3h䣒IE7"0MYs2Q>/Oqb uvI">˗N F$ #51lP L5g0z(q/CMrbCgOGOĎ.gdBq DAnO?ݎhdc?bmpn'[[`]ᵆEaeek+wv}wo~`Mկ_K>^x.//?gii H^z cIyn: Aƛ5=[^z_C`x.*O 1XOoOc#Q灁xD:li87 5| |K})HY_`۪Z*ڪګvA>R twC7́H`vՀ0$XΙ|߬# ,[an{+0 kĶv;@c`5B}~{:.hWr%7㲒8Q^֨ 5*Ia*PY] ldP9JZ.Ja*CF2$0I OC$k$N9\tϧ9j% ^f)YꒉD cRPVNP'#I"X1g_!'Ƿ3fZ +=˿>lظ~&{|Ec ,cPSǑ#gT\eLC'ȇgДtIfє+ٓi)g刋'>&GAuq>{+~'xߟN4/&~gcN0^ I 7߿ Po2ыX,|eh04@W^ 'X? y\ |W~x"b!7op\y|GOAx}W/ oVȸ]Ď)0A_=!Z\mZ, emŭ7kjݍf` i.k$UmیUr;n 2D*;%gm7mݷpBԠQW^Wdt|^g '@bO9Aˉh128:8\9FЊ2;=n ~?2dlCKDՉ@?~WKK TW_W]}T3LPeN믃s&l.5K3~%dR F(`0|m\__WSSm*5:m9 Ո+lhL/+̣DKjI}C4/+5;BR32C_ZTy il?J!++uT;XN(S Q( 2j@ Bd+JkhEK,0՟~'_Ik.'!9Q|+|چPtG'ږ :aq|@ JR|Vgp89dᖬ\q% e>ϠB[ko7GG5nݘ'Ⱦ  CJIvg`1G.ku}~QZd.ح2ko_ބ~B}3Lz}&ȇAr"ߝw<]yc&ߣǏ֞}+@Nd#6:;y\. C2: ##"Db\~H2J)2CeF/yEB).'%HhIIPȐCq`\)|-gqP8 ׅ $ 0in> T<#:]EN9fUL>e(8SWf۔ H#SqZxCn<0><OQ'3":䃎dT {\KK'ʼnq_%`*e'W`Bk2!+:NB$c\S5*(&f05OzF[GO#m͊r|;WO]+;FGP_p׿IM7tG{~(@>hj'=SRrH'Ag$~-(#PplF۷40LROݽOcy՟_ۑg$յQ [b.-JK7H xc1 nkhh#!{vyb`\6]imu-}3h{Q tCq9<: {P/j:jz: ĉt:?7iYfifF M[?`QvѰ,S3Wmvc%Bp [=cظ~ȐN.v?ߨѨa7QWVTT uiյjTOSY0FU{TUxWhlT+ p%l9wPQ}6_/ԗY(Pf ~JEJcN\OJJgҒA>"rϷb{t:K?{dBBBY 5wyk? %+ ]ހi>l<6>YG@?FMVZ8msfqd K bc8wg3 ۼ@0z\u(8cy}!n6MNNFG[m;P `RHƾ5ljj kdbVzwνkk+ן=y[_>\].y^RMzl}?|$Yv+[^unJyJT觕ƒ7(EJgS (@B BwQ)\qq9d^Banfp -*2?K}޾}ݻ?D}Q©co,+1yrYA>p$ 2d xW4)<> %GO c 10S?)c.>,ԉ#))ig^& Os)؄I4#ҝ&Ǡ ~g^7o=Q@g>>@5@{HkquLS"!ݽ*@x g:lD^_î xkV<,O p醴^Z$<,@|z=cDl6 xM,֭7gMc=:R61]AweY49 m& ܘJ(=3|lIX13$X O" e"{BXQvf_BqJMQȒnl7|xbQvx' Eb{QB}8!F}^ǵ9vT4,'*T <*(z<ǖo`T=^ͩE ;X H، D oCF%ǝ R fѮAqdnx$ 2i 4!bZ`Gy DV4lEY)D m٨Zv -]>s00:'s ⹌t Qfі#ASB},Μ:&:)m* 'W_:t+"ؘ_r)N&2 Qs4 SBCLL#O0!FOя02E܄@؎DH}{2`}"YH]xAMi!.a.. &w{!<H-.@`Q-­޾)(F^rskٳ=yGo` G/mK .Ԥ~֭Qp HoV%karvlo%5GrvDvyxBz- @ȻH5YVZkjLkN|g_XǣG$Ǎi۬V~IK}`acs1|&@ F z*uꚚʊ˥%zxWSm0< @_4lDAz7 {x7oW5Uܼ^`ה*j eP^)*U*42԰ ߕ|RoKT_JW r9>V+5_OEˉ께yY]zh.o x})LnүNԵ4ނzSzC?69~@17\<='n8mc9<6k~rhl@&NL+I#6 gux睒CF${$iphprr4zwsK!^"$ȗPyV A{OdH ?]XI$;Â[YV)`LpcJI:1YtxX 0dv6CD2#)cE0z'}b!@G[# pٌC\>G)Jz4KBƥP.?ChՕy=xp{ϾU!. >y3L% OaKt y_@A4ֵk׮rڰ+6CH-X͍q ;}ŋ |xmnmvv=v~[{{;WU]Y}}GG>Qg|m|l47q@Fޓد5?4G*ZOl/i)l:Seh;GF`p##ÎK>0 t[a;pܹƖV* ga3QrCiiiqqIQQaqQWPpD~~^O35UU6U1X #xĨ?Q7+ʎOV=U++$qe~'HK5/'d]eLʁғI}i TS3RY}tM|FMF=jq)IqIj/1ViR|LbBL* %'2C)h,ZV!=-8C x&g=330;5枰]>iyy{iĤ>kҬɷ؇#Z=Vcv4ew/w\0!Ћ~ n6_sȇޕ :C. a ރ+w.`ÂM[]^]'Kܾ5봘65Y;~S`͵͵u`loǢ Ҫ[(/_t7:24TVRcm`O!e$Ef 1E "r" FICFC?HI-NL1 RCـA 1QsbDciPN YI~$BEEp*3-1J.X\"< 8U~ݩhJ˷唻HVIMZq] T 1nî>R(G,xOA9HY! )` @>!IHGH G~ DNC&PbJ#< >Ӄ{;X尲mcU|wq۷ W_]+3_xg!(A`@sss$@,~Q| )UWW՝?,ss_zg~kk[Z_kX}'f V{eMuōu됣o!p1>N{4\s8VKK9^Ɔ榦ƆH{(plyyyqIIYYdgЗUO+`ʊښl-<&_++&XVi(0 OTL|>ϤGXWQ[Qe,ȄPh~{ZB>`//5xf%f&fdDehS&~j*ԗGSǦ$Œ`Xm|4SRXa7[쭊1 ZO8i,^XoWGWfAiɩTk]+T}0,>!D!9=\.<߭Fཾ>L{.,`-2[Y^]%s"-q;Gj+/tN7/{{?awI,+o 67y#ZvFFhqg3؎hb 4]ٚ~Z(xN:R#9Lv[ASQ9t %%D/Hw8엝bIMd*: ;0A)COMޢriD00H45p>$Rˆ̿IT*2Ғ:gJDnG138c x8%Bܵ\嫒|ٸM}Gb}{~!de_t6VVtxp"RHꐎP%8PCYG;a\`!,CLnq±8c@w"DIB9PlFHSAA2o)wh ;*z3l ^QQDb6eQdeX\PDa H^o7+>yΌVTc{Ow9ݿCêz&tDf"B)OvWJdl4{:-t׎4;;}ll?;~tjm(M^LM97o@}޽wn.g6J 0Z vo]|??OdpU9v @ƀ- D),t[NǦs<:~995>~:480ֽzHRE2{}xCUTV@PhTTT >b3k*+u$=01R%n0~%9J).ȭXT~Igg*l^QByrx/_#tyy9$l5hPN˸䥋ii65&bt^rFڙ MbF+iI$R_i{RK={LLJR̹h&ߧt>|'2IjNOY}JU% N aP䋍 Os/XDwLv4 8p$4%'CX<7n (n=<~==ѧBl] nkgOçLW[Gc4[|jW6׈GhW U_c+lTei=|@_~ג+!7ʊ*Cmm`}k0Lq<+knn "R=j=HJ$D3$QCܠ ̓PrG0odlH1QE q$QS}?Ai$㐔E Иy8\"y& E!Fx`jZaފCIDH}x~F}>|7Q}{x}'ax@ %/sB/ p U&,pKh0 |8&I};z qQD)4hm\B W߃ 51`Ez"F4#E\%ID#xA~zBUC1 //;ӭO΍%;pH#-g)~X__YϺ죁;K= ۦgO vM~a[__H666Dg8GFF ! I؛'O^L,/΍et͗S/&?yd ?~A 2>vji&ffeK|ɄM`kh`*Uvw=8,B"|b!U\,ig/:%(7͚jSc}usSV\$;l2JK x999d<KNkSΝ+K~L>䣒"O %1>"1^xIԧf)C:F#bcBbCOD*ꋉ8MG_G# TGY} _܉Ѥn(_P}R4W{Wvu޵Z;${=}=Vkb7T=CC5c\vT״_o{{n uu ZGgGeA2nUVWVVỦlvzEZZ:&!C'[Q@XĻ/Çĸ ,!E>|NN</]MVT߬Ul7[[NâG}[Zms"4}F>auw[-͖;uP(>`& gu*8w8!28!DX 1v(ِGRv\1-W!TS)(}C @iEę$4!35\Jt$$&D0"!Iϖ1| FsЗBMd oPLSEH֝!PIT~c~^l"pl0B#S$R %z!}HL=VXSReɲ20/]!poBK}JXcsst"2`x Cn"@HvI&VM6XWx1\W81|lt ?|pQCX  h0A07/WcI1Vz S50/쩩,0. mIAVْH !H$!([nAU[{r꩞qW|$C:=35~:sNr^Sj 4fk?nm(5M¦]Yfmwvܮ.WOwgW'd^բ6kkjZF{8TX*+/T*N+%O%+Wr]l&\xX\zy {LEA yԢܔN>Y>)A OG!ev~Ҝ{P_z2ڙ_/ jd&rdӤSqTK~qPOk CH8tDю _u[[[^$/ۻw45[/$ᱱ8!{I>瀯?ܻ%L7KQYQ؅zVg0V{Ox{'lj\6bۻpnؠwQ89q:>F?g{}xG N> hoo [n޼DN |~ozrF*VW{Gf1sk+k+DHt>,ͯ./,-HȰiRo\{s5Ԕg=){ΖD ̒eA}iI"`,B005(h?=BH>cѰ 'r%JI)d? ]Rμ8'$ab|fJ%KE2BeB(.!4AIa;$@ȊXY"rBkd"I2sqK*{$!O d2~1\_L}o߾ku`߮OqE6ىq 4$#(ׯh~ 0?B5a,$Qوa^o$Irۿ[.K!%І>F _rFE!E0>Iy{ia#8ro}븙۸psTXۙfHj)cPӧO_xwy[Y1 -2SōMn^ǻZkR|\0o%@ֿ C3,@@CrX nSS~o?G[{h &d{SS0߁\Ws?F jq{VxXPu:]KK V)?7k7[j=g3i&MTR_bO UFC{+6rtNkGdk5C]cFh4刲RWB+T) Kx@]ZQTȮTjJ4<|:>%1BA+g˳ˊ| eA&?VZRfCߡSI<`r2$0^ȗ!:]H ^ZRFALJg ڏ/>KY$$ ~OB}*Eѹ>^PNW/@L G h=DyUcKkǠx876::񎶹|}^9G>K!+u m ]>YVl[-mGwp8T<'N ڌ|F>@|Op I>ܹF'&>[,<޹;=5jʒ.{3 ȟR"&#q"QXV{s~"Ȗ=|8C1w.5Ax0wj5 %L X .B C#2/ȔL ƃRB'LFƓa$AL(0PJB\"^| #fL!3!ُ h$# *rc]1Rr J !6v$C(l8NGPVǎ$ߨo޼/ 0<': ]   g>T{pE-bØZ4>r߮d9RbEDB  6Abm 0@$E@RȹmowCq:C-1A'ѹ=Ǔ[lyi0VĿ9*@?s;ӷnm\/_K+swh=w\ݜۻ>vi].,]RNm( ^,)@|M _>11\]r?[7o`'_?}ϟ?{?~߇-v~d25L_Q;&]d2\. .v7riփ|VSe1zՙZgW7i-f<Ѭ3 : CN_VUiJT%J*xRũrNUBwyT,JHzjV.g+KY̒ EAT|yiWR_>#?,XT!Ty&$ /B $[H$tV;Z**(X}ʄT|x Eԗ"Gĉ2^)2R'E$~.M8侀UN o޼ /@p@D|e ;H;{AՂ|[5x^7 p:6_-D>o1Z.߁x:~@؆<^S r//j*Z[;L]{ÉW^|]nkjn&tՍ:]ų?{끍5ocm>uWsvI,X?-4?vOMMBz΁^٭ygU \. 4"_"tG~Ҥ'AA$R>Jti4t& GJK(! TKICpӋ&5L89XR }E I]1dDJS@ "Nݠ  8`6:$>Z# ""sB"NREcY4&DgP0c< a2t]*>/rKHP]L;@!FîB#"D!0c !X +IDNv#BE# 3n?AH&LJ! $Yt08#@hǜBk>`u#G1JDOB3x"9_ !:3x=M+A#wg@`"-.67(^H}CoDZ>F30;@`cw3봭 =jaa{xooigOvwSrL#g!`l샿=^XGGGq iX򏽿Oϟ?.`N\!idFBT]JDg={]ξ>KJ ~Yz;Ֆky_Ԕw/{]A_Wʵ"] M~EZW[^ԡQA@t ]֢"ȧѠCӜ!=E T{sj^Ye^c f!1YJ0_ O%ΐg@>E.̐eSd&1P`P )㔲XxOGeeS_RBLzax؋?2 kڍ 1滶C'HiE^mIkA{~1u۝_}6ˠ;Ht}]m2ĩ$@3}USS\ANh'浶⋦Ň#dw] m|0_ #,Hc{g<Pw3 fcI"ߝom-c=[G[67OX,UnKdiAͿ>s`k+"XZ^r%%Z9WViFy׎۵A;/gXzOӤ'T<28e+? A#:JCgf!H"`@Z&`a7%%'x"Hɂ= )#csPN CHO .2HK 1^stNVJ{a0L&FXZ|{/\䳂v3Ak2=H~ Yz6/(ԇX4'Vu|YNqi}dPezMSiZNn7] w|)/.x|XI}>//׽?.{^{7g#:Z&mˑ@d$e#v@ &hh/`=qi`":JZ؄8$/+, !IK@1 ֢ف *'u0DBN%ebtDi$OScRZ0.g# VF_G!-yBHM_R?o߾{&U1v{ |eN7_n a; V !PI<:<[E nl*LHI"ЎFLlFn^r*G`ĝ0#n`S)QO~+ף(vʿg/HӐD$'?`.> <;cGhe?|9޺u y< ?_vergb7Gx5MmwL=;h{S~oyZNNw/`> p\^>oB2feeǂ01=@A5Od5a7o޼~ŋkkϞVTSP}+kq(ǎyՂy!U=R_=/*ߑeE9eEJ ALE~fq^fI$_X} g o|k#GW dvƵarm䘞MOxw/^1X뜈yO7/:4,--}ޔ =+!~++0= A ,˼(l~O<]]}*?}1F\1{!ٖx@uQ>wat&ZPQY*JJm D.]MfhTGN76+l:VԌJMaF%@XР  OT:Qy*2ȫ9"?vD^+aVy|J =E~ȧ߫8B&_nZAn*F!t!L;:˗/ ?>'KKK6lܜW(+9J䫩;mu\! ȹϹ]ZP٭SR >cHm6X-5 d+ 3jl=wzïNU@|C -|fgpaV Z^c2:FPvʃ :Q(a4j5K g|ybAZG*發;NaybǫTVFC{{۩MZMNzEUQu#``n18aNyQ1,ovn~k32~^:MB4 z rEb@@$`F\aDnZR @ &ϑۼʋ?O#MS]Mt?g9;^'{+JٕHl)JOw%N|vY-5% qM J#/aD,"EIJ !Ѡ+",3H '3ل^ۄ^<"1FZQG Rl(.]B8_|Ͱ.mz2IZThV)oĥ!R͐lDrD&h믿>\0XaeŨIZƨs" BD*Wl"urXB[U\髆L|EWX&'&VIPC _&CzOi ʪb^kfNԥBM= ^34wejo{ 5יAj՚ i㏏~įc`ɉq,ƍO0~vx!DFy7Fn}612g\ v(`+KݽK@^Z`vuۢ/9]c3CuxxMɉå'U_DjJU>P_SZ_SRW 1!,>.~goeyAEYAʛpT歪(>@pmw  Mg*?"_aufaOڲEowNU߮R D}i2Ϝ>suuu-g|>")j3S:NB5q֒jVo3|x vv}ؾ=v\UիT]]].̩5'-[QY|w?9 .8ZϹ#|Pܐ7RBvym.tzI})`"2ʀc3fg.)nU̒SNsz{\vDžf){šçO [ /݁䜙;wOj#]LednHޓT7uT 2U}h8Bc6V0 ~¹D<4 {;bxŞљH ,:@nR-I+Sֈp)EI'T nJ,DgdDem]UaiIo3)L*.1xO` hנz~r{|t{(zw{ׁs //B]w`_yB{ CLLM:ܷ|ܧL}`Õ!\{KK5B3Xk? 㚠_T~ƀ=z1N3mL?cjj NLL@#@~ FkFi,OYvYYK/K;GBݗ0^__O/|{+5ޣm\.vw}=pᴽU}v8|].#{@`|RZ-:B>\u]mxh::Cd R/Of&CC'yi^>'g ' {v~Xcږ30.y{ť2rvzfzfvzVgw sGc#Cb}N @ͶT5QAA8Ҁy{7'7UyI$8dKMJWI[4{6KMK"ķʴ~a@.cbI{ғaaN#=aJDHLNj۸6"DabF"K۰KG['%3Qʳq?3f3Pꇣh[0c3VȌkfɼs/!_8lmR)^bNQfSpeQ}sCeuA0E@hNFETD@ .L,ˉJ%_M9M83Jysrs/ܸJ&oP` V0 4jHiJ+?B 91Dڐb8J̴/j1,:e* _b^jb!ToB;Pa/WҨhJ}'J*gp kBVyɺZn9|Ч?_䑑pw}f?R-._B"L;?}* د׮mcgͣ5 c5w]VDO6iMP^42;;}wn540!KkvC4Dfvw||>N˨LCMpr?߃t {"?~LK @J*ii ~3` 6-^]*qH7ѣGϟ? ...Gڨ Lg qfkr9qDiiiQQQqqgeeFFVTS^~Xqdȷ ؃|)ކ{9rT+)*):Xu /s^a{3?/So4f@Aʢ=T24 s uV Hٳ]p[򮜤Ib¬ [3_{B>lHٜ"KM~䔭)[c$'cb v۬9ٜWPPpq>Bz]]`/{WTV;.W;x-t69\-3.<_OB":/MTyMmm;vkjLgTHX,Q{m{N'tj'pB>\gY:©EW QTki0[$"l7iqor$8_~ wݾFn OzTܝ6ky% /*O.+/.WUU#;b\?~7=G(_|*华$Q('xuLHf$O\ɲ tjlxvb$$C}[֪F#-9& & Dhv-g!x #<rdi߆fHM&ņk7Sb:PU)QF.#% ,r ":zܧ5 #'T{ZqKO^ 6E1L[Cl%iũ%/br"ØuAB !ڊ ^_9*(ռņ޼yBG ~B>їr,ZZ>>Aint0*#1\fI0EyZQYZedWwҀeKQipǂVji*.>S˗?24@] ]Fi: ׄD]-&A/GK\tO}_LOo;B& )Wt p$lm*>ww)KXkooWv׮^vݽƁjHuh]èk{cz꬞Z #SSb;37o+Ήf͈f~>Q7{oѱ[#>x@HOx]xP}cӏ>Gh<)-pʇ@?J>B;ȽlMc$ g'/˗/^x,l=@`fTlPK#A -l-Sg+//?z(+**ri*vS_aER?${:X} ?@~&g~)3{jQvN{C4 )+5W߅BwGR*AL wnKޙ=k+9G4%+M2D)0%)691&9aӮ5gp07>4550 } ޹s~{)hu;\m.RޓhcjrW6tyz}'O+ђ,QWTi7]vZpUgOUUWW.T_p0?]Rs ފ[{[lkCRv;ݬV+| V+DlR¦F&æF`jkkx3}}@/] &}? J,SsX[SaTo8e\Uu7KTVb/Mvb--O<{>A>bبO<B)P39lum-F3;]~ e%DB>9QOQOE*ʁN X2Eh+E( 9­E!cerT&2Oq.967M;/ bhA`LTh1&("n d-D(.`ShX ͳ ]^U MrT@kЍ#5!qi|,zʢ\ _gtͩ`=`\y֘OhOrL01j^Svz"U߫W^~?QR$P|0hM{Au `?)(X S݂e-fnEPP ʜ/|Cfu%"EqJkCdUY]P^r?6*[LUMZ}k4;iT E/?ƒ9ccDA 2p^/?vw?rD!>9% 8Z.W/siWTT2qdq H "Ҳ{7 t@#"({4; 0Qc`L*{bCCC 7o޼~:@ÇP 1dpd ׀!h4:}㙙yyyFcqn`O Rg%rIelwd$_$9f1#X^'TC(]{ `GPuֺРeP3[5HDj1WJz a8V׺:\.RHǼ$tDșc#ޮN66Ύ;s\f)1Jˋ*++ /9ߘC)W˸KUD55 &!a qxTIq(r[Y얪'ݲvϖ=m `Lq.t#NOog"3菴vF UPB1ryگ6Ɔ Gaa.1 %T bNjoi)m+F/J/F&Ԅ^Q 7+j_i\6ɵx~2,So0m:ݨh;BYIBf|umʜT}b/PHuʚ#d_Ћ&Pl2 {xk(հiiEV {̋Zۘ(1:ȵ B# +դΑ˗) ߘ:VшcŅŪ5p^ 腛H4̼z.pt>T\qH/b_W4-2>~{*c zޝ;7Hk$:نCCLEFvJ$㗶[m9-~3Pc0U8GG)$=_kMt0~3!R17;= ~?OzwO[0 㢩ɩU4(o|ׁ  F@rx{9A}}}𯻻}6WFٌ|>B<2{/?Sfb"5?*;-َNT\\6}"ѣ999.\G=}z銱t~^x0/(eleiIx;#Y Ҩ9f?L\ҴxTRߛ&۝~`OHoSw.zq@qLMqt.BrS:_XF\WW_8ْeI_gh'/V|x5xW/Mfws8,2KUi=];w[\ Pi,*ųI? 0@eA9KʯdI_p\։ߞ**tbqqIYEEE|E&SW_Wv r*lJvfU6f3&rjk͘.zEvخ~$oxv{[͖[-mn~? Fl*5הQ_˹.>MbadE/??ލ0959AvbcP/o*Wr21196> NN'fBC6ⴋZ՜HAYo8x&(۽y(x5I[N@^l Z}. g@8ЈqT}6  i՘G%( H>hjl%1[!Q-"SVat`[U/*hDYZ_q*EMG9MD(8aD@nGbrZHҶL_A]~ZʮOo@ C;Xhiш?XTąqWǮ-RM~AGJʥ|F</[F rr2ar&e%&\tAL/Li0B1oKRFCq1y/{\J~)OX )|*NE?&B/X=y4w7 N=XoEVMrx>/}Jw}hp [=^ >,z~{3C@`hR.NO]b^lP#p$52fB!777NMCt2xwzE2;+vCMar OѣG|38s -3W(188puőpX1Q`K (l *h4;/n??ŋO=z*)]3.a3WxA t: :~xAaܿY|Y LkjO}r|Oega?ż,'۟a^+% $g\)HݗwxoG0/¬;,xo~/3}[FzJF6moxv곊|4lK6 >zhUU.«!h}rr 8q7V_13&~hp~| @iu?8U[[g3 f9EE<}0OjLLy[p^eeEiiIYYyE噚꺺:BFfBi,x=gKKe ݰhYU:[c]]-nd ҒEksCN|BHm~Ð@%:jD#'D8DӐQjE>Jw٠h'ەוuSc7]y*&Tr5~ *#rHM\,JuQf]'sUBp TT+)ԧ>p^~O M)1w0Ko , TIĩ(<#!}H^\(wV*ERE  #18"˴KW)t18\9~6MmB ^T\)kiL!V̹J*KUn}ub~OgnGԷoR_0dJ vLA.x[ SI/I8{P ӧO</\8r{GǕ;-A5rmll$C=!6f7 OOEZSSHx\83w}:2D TΝ۠i~h:UTxmbBʙ'&2OE.쒴w$ETݿ;zO_GB!Pf]&a"w=Ŧix[\>dA|^1P-xTMhv:!+ˏ6Iwyɩʊ}>YtXCT9ky[ϫ12LQQQ^zFlٸ&r6a\B~vi6\gFhtvS&tu_C}k&+//|(3r n߸-vtV́*ϖ(8PtXOW䣗#G@m;7Ysn߽7::2{CCC2{Bح%, ]MFj:Vcohzf}mʻ X[D%6YDz&a>$r)Mq mpVE8ʷ -MJKT x6EgC.g$E84E $eBlJPlj} 7Z4gHq=Wr@4ZwDd xsīM'D*G,bTyh Q  I} 3,$0SNQ8qڲ,5x4 pqcYZyB/pT[myE9)OTӸrtBhR? `QS}Q[18%:zYJr ÖqD#,V!Fɐ BSMa~7YT_" /+B==׮uvwwkp&o_VOo(ߟభܜxssϞ={|~~ub\.'?`ɥ.yP01872_0̌{~^_sc&QNr|Tb3.2%a£݇k9yر˳Z\?8[?\iOz^ϋM$mnwʫZLJW& ͮ wfeXvPhlȟNb8< u.gZi&Qh>v{(8^lgf0JJ+N-V7%%4jNcFZ"awp*٫-5a5*kuJΆ3宫%K:KKΞ?';{Ӑ\VY9G2L%9G,(ȫPyKVGqqqfffwK߾?,>|t14:2y#>){ox罆NvQSǕVa[0[|D֍+Vl۴26|% _E=cW@ 4pDzAeWlsON!PׅDIBG#5PP5 'ԡL6*ex!ՈD 5B4ظf r obMM53ӎHSPR)bQfd -!YWA5#IeI2:Y>[ؠ752%2vuڕ';-~{ۏ]Ł +P= -Q V/IvZq!"I%FRIdäyh/S8\pEr&MaJhp)uA/\&lRJKՅ]R$S.UQS}ŽBChk 4͋?hO= 6NM193VEyuVf޶9.twwLܽseSgjjO75~7sԡ <`1n]_}^zޞޫ^!{mZX * ^ _s1~P'''0!ȢaS7&zz#^8"땾K@,ܺ5AR^>44nCKμco}j.J%yP_v~:= ߿Ǹ|CC G  ]]<ȃɎɾ#D#~@cF{.ll!^?D-o,Alf󉂂YY{jjjԻId2W23c_^)/%Fy/H#cRw@=fR)53ޓے#:yGKE&G&jGdt.lvWHZn OLO۟J;r8'{jMM֖֦zrj;i'+jj܍G߷i7J??e*p8JMʒ mSQmfp;m wEG-͸[JKM(xd*-+;n:an(΁ ~~ZSfF53lB;[qa1Fe߆ar:Yc2lUP7:22xq|mm<O=smdgr* |EEy9G(.>ͦɇi2s~K†3;XsFFQ\se VW^~Q,=в )ɈD2 cLlƟhB$p&\"$^HI Yi'Й5 L iRքSJ|OlTSmrUXGZ4 V]?+QKT-:.-3?\w0Gf&E%ɿ;Sl\י/oNNk/%у .vv^Jtwut|Ά5_}lՉ5`**{; */o?|NjJϟSx)`ֺjYNj;yRx7z{Ѝ+SiǴ%Ã?Ł]='8L7>1>5ESW^nIwT_ϥ1owfxb4' ?/9s1sEɩݻw=|O|{wT׬߯A Egg`{{;fjb7Kf'e4!gϞ>ap˓HQ9AtKEFMjx}_7&[[nzzO?O?O?\͌H{v*((LO}>WdEOϚ5kIj҃|}UT7(Z^kkSilq+l~bdVRq":RoJK;jw{\յ𯴴bEn }ɾVf=Sth%9:*..*-9CꪊZj5;-d[4^S#GihnR!֛ fGMXLTVjX&ar&)&T/|dnS%>ȇT9Ǎ VXU6k5ꫫ-:k1WU|stV%\hq8yKڦ+=ij_؟"P8 R}` W"_$82fK{pA>r(8lw@>Vi7]8m %oVoʁ 6)IHgP䣜O>ݐ0nlT556/EcLWCj"֯HV-"=ϓ2 0$@dh=& WNTɐ8X+a0I5<-E2upU"[fBMNAqiB¸S$$0~׌Tr͘e"tf*L% %͂CB)A.g'Λ-AlF]W SvBC 1B brg@:s>sbT$%@9Z; AQ+ʨXYl} _BI 97vF@1d\L9lgY-2I1IRGC|Mb%7CW aX$^MllP8{2(XzQ"ހ.]#FcxwvvS~yҁs_iۋ/]|Z՞| 4BwFn?x] نFGG`C[nMB>`m7nC>Z>?v, A)?G!E8݁Ke)S}gO߫W/)3"֢j]Zl E ! mr\C 495U߮&ٓ)ޭj%^--6჻27'+7e r6cMyG 8v85xao-k]mz`āX,j**JJӧߛ}-;LЈf@ A򒪪ڪ$Pjr:m[nhW̫unjjz|pQX\g.t+:f+L*+dbee (_~ _ṽ.^8+tzl'6!Ri5Wp).9z+uxhpc֖̜])(3}ot$ TfClx( pPʻ;-{,@fr.i3}9XM莼u?I!ge,xTXh ̃@i0:h-k[D剚y0ђ:J֪=\* ѲB#-qu?t7i0^iPD.FZ"V-RwXx)_Ѯ4%?Q.(KyU-_8^# :=4su;}SJ-OƔROO9pꖡp_L}߿c%*$)ޠ>3oN yňg 6›MI1.]0OM|%)8CdX*j>TD51eb*?_(hPL6KPݨk&90*YQ!0VM=p0-R8YU@˜iD $+CոM=Uߋ7c +ܸv;ĀK|>+M۷naV+^-xՏ?Gy[/o߾{Jo޼y*iǏ366zϵ!{_vrOWg޾;*~({ Gݝ]bwFþH7z;گG" ޹3pg004>>&P8{` C ӦwJljQ>Vɴ[~+@=TDG 91ӧ/~˗/l4 ȕ UfC#yjxw𛘘Eyww.kFl75xq-eŧ|罹[EwH/;kW6<6yTV[7\gLEKUTTdZٳy0D Cb}YY `2TWFՌSvi{.w}- rj6sCRnV Usl4h, gb u@G&]r~/N^RV{ME֖ Fw8yFJ|a=䫨(|B55EγcČ_&;`#~qЁRCԍ~7:L'D}~=viqj(,Է7}T!՗eڲݩ5S~ $*)Piȕi5QbӓUj[e-9^5Nl(K!"=H8; B *$L,:*ڒ+X%qVNq^bR4˦y*gQiT )+o|iTa11*Y!<4*Y悂"8bަ"!J}?իW_Nԧ|GXLIHOבGXRhe-RrzqV'?>$+E .ZM*R5#4J(*]-ݥ4$VIU]Tkdn>R]1&״  MN_3111=xSSALfg~{{goO>;wFGF %f8 ~#)-dRR+<9p[9ՔD "C8޽?r؃컺 /5;&o7S4)K=qzy*C"Pav;vߜ=:j%ԥ56jvk>#}~x~(g@ɷߓNۿ{{.PAe;v~[_9$%%-X'rNɷZ-p-Az+l&<< |ݤ5(\3O(!mOA3de0hu%՘ʃ op]^qj9m^7 @⊊rd2X-&daC\v@[Wpk:{謮u9PT?n\Yl*Vd6ƕZУ͡9f[,fPwk,cOw7X g!zQxB3ۅ&;.RYTs ;9SMIX]“9g>̩I!OC-cW7?q`B7=wwDzot|dl|4GFdՎNIQWl0Tӝdd\DZ3%8j(ں6=9n8vIR7r>]t`"_KmKXG% 'D!A!h"eE|ڒ *U)}&bT`!KUNq6i'1*2XTY%7$È%9DiP-LDL&1F)J_. QH RUGĵ+9NXjnx;M'ѭEтjf9F: 3Y#'f᪨eQEtzT5fRk[f#mF*w"(Jk(6ZfS]҇*86e(11ILHcyVZ-ͩ~]բBr,QSc\b iV.}s?,+otX`Ku)rk9|>_csS"[nub@ϵO=zct^|(?2}R LbNƮ 8h;/#jzoxv^qx絫0h LMM@V-eDh2 PHovws)|8 Q37; `mT6~ia)>go P"Msv!B PڏBw|ޟz wjhhJ /o7 U$S(HV&#u~Վ˭Z΅}RUJ0.l6:u*C|ȉqبJgrN>aK }BG:2@999"=W\Z.8Tx^4M|xκ2Kc{Mj*W;j$H>߹bV=D{ꊒ 'F=D}x:,^$@\~zׯ&<ozG$QBUQJzEB !r!!p's!!@E.jb}ٵ}[_feiOgs<33sX\V%55JEJ)hju:ȧCZMV[fkҢU :OVgԚdZKAGlFBFSX:9|.].zs;:lY6U`jUJrN+S)%ť%%^|i[ZZz…ܓyENW+Jϖ<~ c NL&:"/ 'CP S.{^ jzL|'rO椝9>y9i?2 >M3(ڡ"ڟ®E=Ĥ.x2S&"oAZ/ M!%gsΐ2蘙FhDܿo'XNyOBS痫۷Z@[88ϐHng! QJGF5'qFhS-#,3Q}Mg\k#mJ/q3Oe#b%吳y~Bˉi)8&ul+'rN~7(Öv806FixSkuY}[3' VWX^`^|0#vAC8{]SýV 4cA3~1fv}}ׁy+G@ο  SekKs}t/@vu!r0lHo*<x'u '"ht&4r {F@R_tf~~6x_Y-<[? _7ٳD83~eZe~㯿z }+KH$ z8_.@~T^{{KLW$4AGJRL>@VC{epTZ%KA>ZѨt:uNck6h5at6| ih0@;\g zѩ5j$j5rͲn7>9쿼އ#H|]0^W'iQ4(R^!T*D>T|\q/T\} e|=y/9F]#!q]bTC OBA̓]841kq"4L^#Fg1֙tzO}YDAcޞ\ ,H/0-pxqj˦d{ oƹqS !Zp P& v2HV(.e$P>dE:N2 Bp P+'3kG\(MHւIhKv f E4.cv5tD5%Ru*cV:8*^~͛8Rogv&l$oGi@ ,JeC=) <l%e ^haIoWVfqÅb l?ſϟ H#8n[@,;G*,#x~󗯟<^Kaehdd  ;Rٚ2w1$!nl;Ex@8^|h0aѬT޼yŋW\)++0v_O7ۚjL&GA0 ޽b7X׏R\\ e/|{ZLlj447 0oKͦ0Iڴf{Qo6d:kvbYXYAn}~\Ht\2jP+pmƀ-=ƿ81<2E~h5j :]A_S_7𮕟(+(+-uOVaa]6H޾/G7?%bB+ШеfJ3թy"CF`@5<ƦKf0TMf&U3C¹PkH 3 NN1HȘTTR"5+~C>˗,Rd ,C> 6$PF@HQ#8+W{aeV`&EcQD%cWmIlotj*ÐfKTx諮H9AwAJ:F|ʫ0Bq9uw5˸pٿ'￝ !36<< 1' P~R,V:Lj. ~?\a: ;_pw/k>դ^ {{o2!'O!e7NeSrŞ+]Wz{ .~Tup܈Ppjj" {qw-HaI}i;~ X>G MNN`x#7 P,,,sK0kC_ G{rgW'oĹ? #[7ej+EEE9x:YYY +++-3xSz|9.ٕ''w5nx LvwEc[BKΘ\]{[KG'#4?z$':fe~i}*ꋋ'F/NX\R\p* GD-/+--.++¿ڪjph4F,g86Id7u:Z|pf"|+-F=3Mg誫k%0*3"n˼,^s| [E _uAXo7VUT<#*"dR7Lٱ?cޚS6O7uLϰ&q7ؓ:w/X牢?2,z`s#3ulO{[ɟMؑ=cQS48mZ<2bdmkh 4,AՋxᖈf%D9%Nqǚ i` Ns=&L5k!d{+E!I# i҄TPf2 1Z1T5r( EAʂ$'R1dJL*"J щA5 ĦBZ'!'FS$=NRDAT4>uI/^ ɼx oZJ>+z9 [E^H9h$Ed?P}9Ұ"YD# (ꅩ1QEϨ*ؤ Ѣ\4hqELєz^SJU"n+cBb^H=N(J}?VqgȈd3U 7r sϱuX͢.`ΐWȧO?-T)¿_Dl0(?|t3?LW1v]pBorO7s/~x<6ccÁ?޼qupB`Ɔ<ޱW}>DMOO|1PCySSSwfoQWhrbAwgݻs]9+~MH g eza]TK& Oٗ{ffxtdWW@bpWw9Eݗ:/B7߇qB^TM"p!ø5_^~S]ɓ999{ݿ_aaa~~] ={w@ ۲NW ?Wm-Mͮ:S[ uzY<Tw[LTTbl0~Y\PFg~~hgQWR511)Gڅ'Yjkk^IimyiU)  &l2XatڡsZ):f lڌ&#V \4uHd5l<`^_W]C"<ɧ4<<ļ4umRE|{dޠ1TjU5g!_cj#c%`R71ӃY6(ޚ|]eɆb{Po+ꍻWT@Aq٬6iMe)D 3 0#0FD208h.ԗƗǶ%/o@W՞YϾ۟3B&obM&sk8;lj|oZr2S|kA/K$'+e$$IlPǰ?l;#=Әꍴ5xoN۱9FRFw)7rp Ƀ%~)JNp 1P vDͦRiluvLˋiAHr6T *@mYˉ< v P3hyS1[F˃F գA`qWc5D}Ϟ=ŬHOYa%br-F1D í8_so45+1:JU%:D `Z$ů0ZSMj,W'Vsr7)~edX"bW|v`LwQ fd/cP| t O0|dh;lw Jl=\`NNjmi!)Q  Լ)GZɓ'0 5X^rsB* P 4???44Du؉f7<o|zzy~cxh({m#QD_?{33`04 Smd40== y> ؘ ð\849`vffvz-uu,cLYSB9T_c5z~?9I|-|G> ^"@.u@`nOWw׭Λ7:گ[xZm.vXR\\w…KlMMMHgJ@lIѱoA|GϜ:as+My$4EVOcm4|K}Қ7r9zxk|M2v8yO /64u6zZ\㧫m YklT\]"7hRyY ޳X**  NW߄܍WM6pSS#s:lhUYo*Z*دNGDN'kQ]U;j{_ %NB%. вl|L%Y/W2[iS_>yxii"||8p=G+wRKWé4qL[Aq762iG!#fdݸt8VU_RYRr2޽!'3]BBے rko;)yo$ED'%.IuG%K6,Pܢc¤-"k8F822i6 4&ȤRP(GTfjDGZi7s/f]f!9*/m2M;5dѠ.mC ybRȷ)VU)\L rn5Vzi900ԇ@l je1P͸ (B51+2hGucB*nR!9)"/Q-_Z)b"TCtg,Cqq.NMх ׈ٖN̼T.q[jQ1U:58V'+8ėJ_CU"rf|ܹ?( X&'B?88xIhC}}lۯq̏.!ŶzP(88|c?A_, ؾ"TK9T1ǢɣP$v5jkWuGF _ľww=90ighp3gg}ѡɩi7;; X  ΅Ess }\#Ps8cDcʼމl#@j"^RR #@\8srƨs}g$2gl;::` lX? ByOA_o6eG9|\ 0:kimz/#G}_}iz/\Ii] I=a]nWyUշ {K^ zmޫDS񉜷ޯk|{۲s9J˕|ڳpD{/6YUL m|bȗb{6欯kp]MNKfva+KD>|ٳgG[MVc9{jUYڠe3)ToeʕMlj/ }]rJٙ?x mP_ ww͖?["{@"\( A@8WX5uZ ɠzQX<\`!0;P"ٿ[r8\v~M,ٻKSY4=7CI,,G#1ve1 IOD\,&i_ )f1<mEDcUBgb&+X hHkX`An6BǢ$J'2_B-J!uMSO3t( 'xo^|>j(Iߺjb:JqHl2R?88%qID oh"3)1I&.rEha[V0ZPRL)h\Y<aKVdddlM`*q,6u#dMB]a: 09oH/ZZY M@}c>qap`:l´3 {X7}6^n,6QFK =m &L lC(m6Nii6&CΑGo_M7p`%1ip~tNM&`ozD"H(t> GŅŅ(෸0?&ԇtRs Z@+ۋY<ڝ58 20vܟ/^~͛7^—S܇Ǐ?|0..6({?rr$./-QoɁKkkwVWQGD<힌 pdd$!@ȍ +b5 q칊'NIԝv{7`' w H&`׫od Eԇ(W(T*~`ZM&CG͂9V"JljHu:AJk0;`3>Z*u:. k;1{8FF1dËMCnWjKUkrLuK{ Xk׮>}z# -=u{U^O%>wWh$ BAG>07sA>$ =l >Y>աOێҏ8';Rc? }^r_2\WZ .+F^Wb@G*M%2@d((0Z!@S{vqNAk$*\tؗd3IBE si; {l/W azc\WdpH"d)- 'I S8@La)a /b6#1eȇ-"&C c$}`(d2bbf9i8p@}y4&tt"+e~R)Hܲ\,876X꤄-Y$@vitzbpo=^=? GBA?>or㘦d|ɶ,K,#S6u&>!LBi3id`/EI3$P;;ov]QSdxv5v*>ζ6 O&wr 1؞@ſhEhF#P P!7paו6Y:;z榆&l::{6;2ty=n0aBloup844bI{||F~n~h( ƆȨא(=ytdP8 `htdDZ0kVÇ=z1#W(VaַnݺvիW!ZU.e( (8*( ԇXEvV[׉jZ,%@PSci>E,9ao8XPp?CQQQ|||bo}GXMl=k޳="N0{yuZ㷼Wfj).6XPZ3g'$hkLevYb 9Gzk9[V=tıC+]͏رsO_#8˗0?/7;)ȗ5yy$%WZZ+/2O_[k{̷F}:,WNj**t$f3NJzd,s&_1:s:lv/)?{䫏"k[gєWҚMyKJ9yN8~fffٳgM6Jz赽dmDzeQ$NmദP,c865I_0 p/|V^kuw9*+DCEP_^Lܑq|ߦRP\ʹ`FisAJ5idYl2g_,H'4b \OFzKQ# ##;ywldY'YHNeJII5l%A(nW,Lli` XeҜ/IT[<)c6pQ]JKo8d%tSDt9H=9uW\KSӧO0 b$ĊIR"`L``墉ݼ$D*dѹ"=H#";kZy͒$;7)Vrq 5ddrqh[/y5>yvo߱Whf thkktk]m~Xn0~|x2v?Oh b}://-v^MMVWW(agCwJ?CpM&NJ* MJ=ԕC>$_Qf0Pͨ-)CѼ7ݔ(vE[x#!_c^C{jy͖GNdTeN>9c<1T)*zCN}!s|<ѣvZ~Ӳ=+^ de|Coڲ2V _`# Bp>7 !I>᠟,iz7xtU+!V괺҂<Ӧ$QtμbQcc:;J}ח9@H(G}|wvll}j3[:8wv{WgJx|W`^&_eP+~' tFn^KsJ}[}>0݉I|f44zn{P0 4|wGoOO(qphClB  KtB.%@1$_ Kdٲ,/ dIeYͲ|@4/KIxd۵UNIڝ={~G>gsƃ@8t ;?࠳%|H$xhdch+h8 cpƍ߹swS% }'|DBڵkWU=[.0Z|Rg9>v#~{( >dIn :kP؋'.ӧ +Vy׏}MܼJx^_hhS򡣊²ƢFcUɖTnIn| E ocƪ[5sⷿ~v֭[ZZ_C=&#PP_WXt&/ Ν9]|wOS0"%M>ckjl xnRᲭb)7KKKKϚˍL +&$bTX˰%Rʌ͗y{YB~^ *펢2r |6w*rXo/.LjQZB!{3mzͭ@HoAos&M>f73{cN0O֨q9*y~anנ{y[*Fgˍ%妒;v`oצvm^픲qJa/jzُ-(=l˒nEĄae9m0O#ea:utsf"bzfE;ri,)AՏl T X2WpJZiL$dpUI,=K e)zeJ(>(87 ,A] ]*{ #I:IAy%*FJ R#Y+ja8JnL$:^"T#[D"mvBbPÖ-yy:JdZ<OUK5RfڙЏ-%^5\2ŭio/"@ӴWrИ.)C&-}@h-C\&Ti 砝*tWjҔud!Kc!] v 5| {.K--̒ :;ٯ^< ]m睭p\_SO?h0!SU[~O=}/}?>3=7cjVڞK~.oknooC|T^F}kϫub"rtko !2LD4.kFBԤ GBh55x,!ydX8E@+`3_~m믿H?O?eo$!@U)xQp^)-@?9 /0W^W D{/@;XvԾ,$}=A9_4Ȃg"qݿ墭;{|8%8jxa! 2q8jYNKMe0/UM'XK5w*^_[w{|Es_{~c}ۚŧ|}] k>y<yA¼0$f2 ^k8:xxV*+$Y&}0<YeFG<2avy=CCAg f1+/;g6-+-~Gzm{JD[W@(lW,|'voy {:K4`U;7m̀pb4,1}<1"h$`q6@CԗaJ+jJ &\\@P՝oPhIz"5K5z۩mX=6@9k0U*CNLCzVJ&41Njt:_~0ANɥ~MdaB4U@4RՈIKYomTJs C1=ʴ;_,Fo| C ՜TѢ j)1KH)`t%e҄g)T?R4QH>Pd4 ~L@Xqqֲj,e,h첗/TZwbggGsok޿j*>G h ~:|N ?N?} |=狣B+_iNFv{~ yݮ[ Dž-X m:;:;0{2?䏺Hpb|bb< !"@4&cY4@~ `4-4( O4WYY#%ۄyP3j*g`0=?,8,؏G< pYt* b5+X_x0"Y/\A N (+pS^` | 8X/(["qA<AF@R]8ʜ ahԬ_`-NT[ .|79։ގނ@eV*fPY]]W /&4$vtc[P֖s#)]]PeS[Եy#%,KfFR[7F~3r}9{17;<6b]1n׀9@_^{x oij3vnfKs:aאtorIvd607 C}Nm|11>Bw OO~,9ϸn߾}Ν?~m}/ ^ۗ/_o4["B|77BD@p+1QBЏ E_"o`'@,>677skXWWŚ KjΎ6c@OekG: xw!pv߯صPjO}WHU;N]FzZ]+Q J፤jD)D<=ȁ5juYgJx[.sg%"*+M0q){SRϫj +***,,;y^~~>Dre@ 9*%Wȧi񼨪*q |XVZ,*)_,YbRV)+ B( Ik$*P$J@#T<--MlzdC4u*GsLG%l$| z5fee}o_TRʶ&29#Y2<;~H,Nqxof,e?v\lÜk1ajF^&KPDR*>eWT#;ByyĶQtr` xC (~36%Fᴋb*<16F# `΁y8VPPh&F>$80J֭^1}9^U7ŋ>.-- ܸpm^zx˳S$ggg=$NNqB\#d?.H;;;!g93tx4Z!@߽]wPajGNIM"*Bhj¼W׀/?y<TMf}hX%(D+qMP@FdAF^iAdkfhAM:/Ҍqf*>X>YNdSe:tbvڱ[ U)dJь}i^,L$ :hwĜ11ZMvv1`:Sx&;. ]  V6&Ni J\ו(E) J)OPOAHe|iqNN.$܌adb^764Z«@atTPQa,ҳ<ɧ1Gb2eɗ9u69)\& m@pࣝQ<&y^ܲ2W/.%Hǧyf!:l6[j fNxw'/ω $d(W A"a"}P߻$Cx #vY!8p*lf`[F އ$OnBb{]0?0jW+Lv] kDnҠ)%SZo .P8X]zK0ɰ)Anߎ>ȶc&eס.&%Յ[GϟqH2$ q{b_#ߚq~]>Xju%m蠣@ml`AG9n/R$ɂBf@ B]H9qls+W\2ZNu 0 ZsGנ|8[df98VI 1PT#1[+ 5^._rb]+mֶu`Ap@#kjnZTCyG쫫%a /nV庙)77oAznUYP[grv?Ͼ7U,#Ps߼k 3wnruޚ5;<C37F]] '7~s_gg wu]l6Wu*Pbh5-=]1)ta47??v޽>y}in"EѣGxhܒR/!p Spsoh":t 8{), j C`yy 6hZlj*`/+@GFF=(Ulr^~LSׁ@Fc9De"Sx^X5T_UU!3/)E'&=y6ϧNS*IQѕI!%%%Ş(Ӛ؎"i.%%%55"deAH+ ')6n4t: ւ|`Q4$TXHrlm%D=3ԧєz*\Vk5OY -F]{800jV3 d2}B}OO|Lzw^s2+X Zы^ddd`ǾO?/:{N%'__$_||Çqc 'a =:e갵comg @|`{EyŅKYS# TBG rk .`A@]Y8䴨PR9f^HJ nIȰ8,HS 騎 `[(LaX.?nN9yZ+"9%t/0X"&xs+9?bQ 22FAG. ҃Bxmm>8E!aPxԸ*Ј6 )&!Qdԇy =~ӧoD} k? j-1jֲH .DC!%>]%D' BDiFp +u\ $޴Gfp5J(pM,RHaBh0n*:\+&ډS9Q48hb E-@)NY~ ²&GGb/^Vk[S觏> Vkւhc577uvt=S=khJ ޽{~w w:{{~DSsz?޿:ޱſݹu{a=64=90qऻⷷf\>acg.N۵fg'c⇣O45- N{wg{J}-XSe,%̰JcCCGG{gmkokjvG^ "7rp؝>~/}=v;VlaQ@bkxhvRP59yc%2f'8 rx}<ܳg0'+077 qĿx󦈨vf=$"ɽ~D𯫫K V ª~B-M 0+ߒ p-v*^xHtB ZE+/DSaq^]Z0eL4f\q,USTJTi~E@זjN:s*_>E4ءΥ\̕`l4Al.999---??32\)a"&LPVZw'Z tGQ-ˋ ~ZOɕ |SRRCk%с^AM%yݭݿvU]薊#^58(2("H  s K.ZGJuQfr::uĖ|R5ڣyq!cSL>Mwowٗpp}@>2$$׺m>v6%qϢ>#S |+:c>*8H}w,tw)_#΀ jLyz<=KON\%aWHkQDFn\i$ EֹxHqH|eDDeBz_EoD3 &/ڰVGWWjZAjj &#Mp5) C̓Ad .2s"Q1V$FH8cyܬ`6` L̃ 3j^<|;0тUTelM}2j4?,$,+ 3 px{*(H_D|K毲&&`Ƙz:IEYdɷ:XnJ658,WE.IAG2Pd * Ws Ld'\.u^G0G) n .Onnlȉ7;Zt==]]6x@ш5lA]UUYK_m6jkkF, 5է+++ F /Uw}ν{߿#!`Փ4ǁ|S7~z~qcߣKw_rsv 5<1=1x}z=?3=95146;%qp\=@zaGшƿ 8z&-͍]m u'+KKO'UXxkϵ6L-m&7|;ڠ֖NXϛA>s€L\V"L 2oܸc|^x!&sVիWϟ?&^3dI(:'@" !}?օo#Sxîl+@ls,~][eO8On%ȷ;,&-=P`.|IiX'f*y" %yE_ADFZب;mߴ1(8""KA[ihⓒ{ry&YPPA2Vj=D0/O#g5"tae|MUgي,B%$ kM]zl'ȼL>|m xƯvK| KL `[UOP̆xA?aGҔw) Wdԇk_NF#:Hyc>cD8TY4xz=Aہ:i}"*䓕΂|8!zzQJdy3RTeTDHr elRѕ !'BtpL@Tp a>\u6K`ͷ R'v%p՛;HZb<< Z sp2 (B/J'W-A%B>Igԇ)Y]]Wߛ7o>^ d4")A>`̧ŜXh-& rG|rd h816-zd;! #!VBV'ml'||k$Òh˘s|E T0h׼h/zANȁR=4'$e\Qtد\.駧:n^ۃݳ`ji|ǚv|v񷶮 a 6@R_W5tim١WRLwsă'OcpG֐MCQq[ ?ؒ"Ok:̭]MrI1R}EyP8U]E2f,皪*8fj3joki35s{  )\ 7'y%z{-٦;w<|{5&u|A/_|2c}aN* ]]G!@jΚa_m+@,YPC Iռ!QYMn|XxwB sO>Y#g!_tlB^0HB!:m1eIDW/U\"讼(8JUJB\o  TȐdE~i<:ċ;xc<3PQ?RZMҗ`***(,00ȇ/'ԗV*jhKVsUlF΁2'/(zᛗ_W:] ux/Cc~e!_&PVp.J>& oY|}?$[|2@wȷ~Uc<1L&` _o8`0gA>l4{ CEV/g3vlaq-e9{I` o7O dZ׮p=8Ab+A!%F̗Y>jԼ:>TǏ_‘~1ȝ,gN 1,U6 wɉCdb :;H蒰wYPeP$'P% 2{Q $63N"dh!!kf_J`L;9L'dR*%B6W{Y?) $HzK4^ XKْ?tsNtC29.s熰g}O ܯ8QKG݉'lPѨK3]z5꼢*qvvR{/>{t￟ܽ{{unƼpK#GN.L_z9_yj$۱[.ү!ˆQB h (U2{{z&@@*@`OKX:} ]DbAArm_#L/~TEbm.-.+YF~)zkx*PW&V!Bed^+TVz,pb7qTR&C){wmDS8F,-8FFDG K*X} ;sDAԏY"`!{=1?;24_9^/]$[*8fp Tde?1IxQKXr!(gdfhqpQ5 ,: _L;7(gwA mf^n\B)ʒPbQcax4vW't_r\@q7DCdT7C4VfB 5_ɬ@9laH;N+%Wo 9/sw!K+CJEIԉ ʑB$ѭZ|b:ڠ~ gW'K%Zj(E)Ln` z#n#NA`dDD7j5Cf/ Gf˨{k@CKnDbj}ͩJA>;ۃ׈IF ?wwwaW*Tfu)l(YS]DYֿ@c͛7k GM 1G P>?Z\eoqчK.815=?w;}-sɛc}7fF޹={|مS׾?s![cqG/oV~^?6W!ā1"-ɪCɣʌ- }}`g2NV0'*ʱǀ*Km]m[ksgg{[[s[kSWg.M0i~o`,t! |ߒzL&XJذpX,$ Z,ڵkS 4Vwb>} LOj,BO=#BY\//($`%tɧF(,!'';3#D@M/##3++#?/;+3=;1ba:> ; #\%%kUrs2#Um ULɧJNփ|;>[ipxc|{AķȗB18Pۿ?3uuL&'Ј Q9BGsK>iҜn4%:mb z`@oD,Jd\T<]DtH΂.\Dp"gq3-(0 ц0֍ِ -G+n( N5#,IX'] mb wdeX9/n 鎺VEh cR8'K8|>0IExoÂ? a\ٺ1G; Xg-,yrCPd*[f RZCC4SּL(4WTc$*$Q@B4 hYeih&K̃U>YݢTN>,r.cD-#},ܤ¹Ęm4E]2(]wэ"yug QQ'7w B%V'!IDX%s c8.@j/w.rr Xz:;[Z>Sm g4Tښ2vM]}{6ǒJK$VtX*ʱcEn>q{|jj-skK܅4e^FUb-&£G?]OLߟcfFGڦZdžc7fna7Nݛ9oQQi0?HԻD}3C[O~5 kXGv7fd\ZVPWf67 WKh59ڼ+Eڒbl7E߯V]#L@zżϱ 봴0{l#v8e#`O7i%2 ( '͍1`B\- Vk? 688cԾfgg>|O[uĥϟ?yknnΝ;nݲ% dGv_/m- @3: JNx-$<XU,`9 ?φ ^{o#gsU!t)*M%mlWuR+UׂO//722##cM ܻ?4Aǂ{ரot@Ϝʂ s4RVE ̍ l 4xPٙje|T2 ͩUtDMdU~v9.6x7 ^u:v=~7h6Qf2S? >~**%(˗a<&ADh򅆆:qV$?xkkk@U _U%Ή|F(P V>.1krI} EAPr {6u!ʎ=;-0fuR)H|JH_8]+eѽE[/La,1Ov_mț/sY8}=_OmXp CTctIYs[c@LtϏQ\T[ ??'RB ١h=YAU!y@_ Z)pg$](I1IռA59Y %q'J)!\*wK(bRְx}pP aTc3kjbDL&ǰiX}%-//UT`Q?tцYPlim)k~Z_m~?GM= :t cSn>i~fj?{s@&ƻg'd-}e:ӏsw{ZM޻;=uHWKK*a%߯uZmpOSg>w`t%dF\PwMWVRfi)t׍z,_C}T5s2?6Dz(1՚P2աjmk1456X^`p#ZG 3Pm||柜߿???A `cYX>>ET'=Gmidġ@+a{l_sA(Ɔ:Q6x,z (+u xyH&/W+jbN-$$zkF?ž=b{YffĤj/k3Ko iҌ 5oLWX?XBys'~t4"1)S`s}*fw( ~{p NΞ= $%%b&5w21؀D!nOoIV59mN6*23ԪtJ C*e2##hRBb&W]]+*s# J||"_v|R𰽡/GĪ"b6E=u%<`2** ߖ}w ĎC_:gSN> MNMMsŋ /((xJ X}XEo;aEֳ U}m FCv޻))>9!6)!/l ImL;V'LDA, d# W0 U`0c< la;BXSCZGJb:F[=7j fo(TI]?+4%tms+5k+xYƎe2τ6Hitmh 6E~ׇЮ; d)&'ޭm;?x9G2Ye@ %- \f!LFsU TBCMtCc_. Da~~\6"էt5:55=774@!~;tL ݞ5^7G^C؈ae쉙ɉqCWsYIԤqfz|'h~UݥӖl+4Roίw}yݻ<}|{~uto@&𢶪E\SUNФd@9$򌼼++*jjj4? _lKWW[ڂ 664VTT T^^kkQ׈CݐW/l(l333bxv /^xɃܹ*׮>DH#Ml!bҖ P𯰰 S=rEMLw!GG,7|<xx/%%)UBA>tCJTO&z{ AQAⓅ|PY``G_ JfҔHEBo^ߝǡ LEJbt0 xXX&Sу011s{N홣NE:ԇxJJN %'BX>L\/V& >(((ȅ2y^͠XKH =緞A &zy7+;+<"a-/-^(g?$CX~(ȚV]u-2Cut-\K. AhPqtn(8>>>;;y{ґc 8:: @^kQ—&O^ &&7؈Y|8Vb+..p- `EpR&mEBx .@_,@|w_{yqUrȇ@K"*P)8*_xnth\PDzDHCWeg=ܶD4Q&5esߩcGWRST"v#<L$2,44!T*1x_ 4¹R؁1{؂xrjvY)ix,ŹsёaI/*2\.g!+..61VFCeh? Bc/r3A L&Vc"`1qsEow&BX$_tt4F._~/<{ 'Տ2wEϿ [=}۠N[ZZ-..-k"8|a> |"fup&!x8nse_A)d*WGKfM=B (R:<EVN<C r$D>Gzt"%Pe#I|>}QƊ +& 9 jGrl05 b\)G 7Q%Dj%*#nDuR ,=u๩Յeo>>;303g_] ǧ'>^15TL,XǍ 5ڊK1iYёUmeٶ4t>7g0G8>ww,Gnm!$c,`腡 ??'uf)jfKo_OmSUeYA_TXPVZ "`*/+#l6s577;Hkulnnljlo$~瞅g鷀R~<b971`zj6?`].(s3Vn?z^by ~Qb_\\Dǩ);GcGZ[, :vy 4!ua?`iiSFTB_gDZ]]YrViau|/Q#o {aG{<^[P/BL+Wk2G{+uWrFo(t̂^b⒋_{|kkb(QqEJe8~%6![Uf^|Q(-R?HFף# AZ ipď"< EEC)y@TtIZTʐhr6r u9JJ#P*PȔ%)d»17LEu|xLxxbF>aL*y*2:1Zs)I';$tij,&...<wPXW|QwBRJz&u䋈Ξ= :ɇ)PP({91M`d(AdC d.Ig0IRSE_m;w$xy'a&x^_&dҡ/c@V%E <^ evxaD;r ? )cdcҰD?Q O ZHAΪpBt9b^.lF M! Ȱ*Q2Uoј-זT<5/ V'VʢlYYLu[L<{x'WWlK&3O[J6 wL,YaY?n;7;moY^b\G۬QvP2=u߻ ~=1! Gnw†$GPܦͽm2: ΎcmY 3`?ǡBZCz: @ .;::P imvL$y8QĜur֑ /<`b?:255_zFz˗/>}jfff&''s5GF ؅`[( SeU&@)@lथS!ruL*3gN]t/:&A橵\.{1#isR @4z!&HT"*_|R MK?\M^S'>/efz#@2`JVŸ$H$ | =7 ";' SktyF-)-bLe|\UFf\&W*j4JJ-Tْgr̔T,޻Ѓ0_ 9g(2cq ixBH] ;YTn J111Ν= :y@ĿD&LzW}ۏ"IFt¢@>x/11ʕ+hnco2! LzdF#>Lκ:5%bYYIzZ/U H@؃Pt=;.- 9*Ḳs߯ )p@96ÖhHd_YϪ;+\ڒB֭͞P鄽3*ofy?at@!C_FN{ wDzI m&hwʫ(+-weVժT7*0c"#"MOOچ- t~ C3ys~pdt_ζ4OC.xdf<6uFPސKS3 QJ㉙Ǔx:}fΜa9lblUgL;j'"{]ɹ5DO\ꪪw f}ϫPVğB<#<8R `JMQ AAߨC76?F@oykk#/qYCmV> {:[݃/uht lCxgի  o߾}% Zl6n???_= h\`sJr  ۏo\!ʀ(H%6GUEK 5N2Q NNI%db$)d2qv,!0QXx-8='Rd seE#:/+RY|TdKgC8cb"O' ŢLjDȿ4$11199[?O TH+Ηe v{%%XTWM.VX!/I݂<%IO>O(fdd sr20x^VCQ0M|E 4=3/"ȰSqQQ Iqїcԕea|DGG~|s2hԗ!p׌|M!||%BԩEtX!VcʮQ2û@IRիkoHy _/)5rJRb铁[Iw{={;1o pzg0㺋 0s oID[E@ 0RVrHa#Q!בl<8ד]| ŏmq+S((:vp [r @!\t9Ǫ 8HtB.a "O@K\ `{ބN!ǾAprKt܄ +a x͛71#МW2\ᫀىYz3~~&KF!8!V8y5od$pRC~!<⢧So&4Pn<ps`#1z01:R=v촔րpN{:pl֭x}P?6w7r_Q õMDmނ~ 9cæ>oN[_ @ĽN@6=N WQ^y諨(~ aNSc&U5F@,BP[X|i෸03oz3/ַ.X_g uPé>˽_,ösZ>}h||gg&@BU(/i-O/ld&&k6u7]vMONz&ԹMz>w_\ma;p}ϑHKM4V45&cM<7YA>\PZ(+-?!:A!6@;Ns7q8'# 9E)$IrYB!cރ)H A%SF~qo0r~q"0`o셸_ST)e2/!!ƃ|`Q%myy%Sa(2/B(,WXɗ+z5-#==++#eN0'T*WTx^89.W|ɰ_ c$jF㧾SR'I?sxq.e򁲸4ȑ#}p(lϱza|wKMLJK_I>8asmdTûVy:MMJ@:4QN&W_j/5rCK_E0vCw_avTd7хo'N$1}ܠ2N,F>f(ˉ{ef#K"Go >[PmZH?BHrCC7? 9?3c;vbL-7 C:dw e;a>tk㐪.] W x 6hGSf3_AM%ygw[<qcwgfj*gAatp9TBG Bp Ąc8L `p$?ZeV ٩٪j~y*#1_}`!@3?0>^/4N#t2SF@k6!j ԡ+ ZK"9w 2r0,:A>pM/'7*E .3LqKCIu$NPFL A,J|0 M;w\ yn=?;1XMOO~gcLݛw8urw rJU&DzԄkLqE%ήϤ3rOƇVǠ+! |-|~w6ZOأOٿIr-O^HO1"uSSvE BV)ԅb8lwUPmh65Z 3ڗ&[mCv aԃR(4cga3noPTj hU[ 766699/^zjj˗/?ԙ422t:9n/s߿D@ . lgl ң66~CUUW2zSJ%益 oQM2ԅT8M( BeQR*T1@},YAr*ER,U|B*j(GY_m,-E 2%'\>R~}8%Jƃ|`h$99 YY %`p&bXT* iaWs!/+/((+ @>a{O&ɓ,O#3!7ed h3 u|ߓDĹ )S_%:OmOٹ}GϯщoU_N^>v@;cè"=sLLL VIyJ`&7W4#](JK`?Opʡ{0m'A5A?ٶvCl :ya=HBvRJ^K۲gQc8 6UA[ m,顄@O E0!Q'2"p+YJ$^n f+.Y)y3$!H 990 ܓ8K#ä3/jrh3*~8vQ* 𝃷OW~!~^DEXPǝtǴAū!5~`< j]_;Ƞ;nZHbTkE9Њ!D 2EP7_q sqSxI,*˖:gdF%lXhԡ+b"דQPrNDC1<)dKD@uE)G=kpCkÞ{CG_xD;v=+؇mZ겒25Vb̄k꠾Zm]EMMFSVS]єbY_jGG0~wo-Z5-CԭnÔ1ġ6=aw/.޿;7=5TK$VԌ{Ko6Mfy,JF'??7!n7ϸ. <8z.7F_2 ".{rA\4Q\K2RޠGqxVxϛ;(Tñ$ \Ac'ހS0xiP%X0/Nfrĕ| e bn]6Ճ膁1HG ,!$ HXt!U-e {qgR=% Wc|TsICehB|W (lhnoAU@v n7mB\+<4 K ۣCB._bF<+B`CqT9=-#;3⁦? ݿc;5wI}-:'M[7ʮ[]λ~pr&ͷ}j+t77757K=1rs{dc|wX8Lf'oǺ[;Zk% C'dY@fgC[sckKc[ M(>2ɀEb6F, Z!eXJɂ:J?Fjs \|477Ǐ =K('>HCEB_.y-85EIx_ @7b)-6 cAa:/$K h4lj5`2f_U$@}VvfQH39"Un2ȗНʒD,]bI ::K.AA ~0R߉HJh֪u:LE~~>v WXTɧ]4lF!k5J*R%EWRI4bQOMK09Y4޽)rgG|aԉmF ^5xG_WߗWEbD u|ͱ9|p\\Kǔo.f^R ff|ĄA3|aSW8& /$ /&&8`ǽ X%sH U# .gc0Y0!odP9`!2`B"']"6I}?*VDpԭxȈ0:@)|1慅# B|!>)MZ6&#I+~#ȡ n{#ycTU}+ּ 48 z; bbV2AH.#1Pe%s.P~W{.؛ r^MOp 々|d:K; 9 ǐ;D{C y%rH7Hd&`l9 5k8Ԭ[\XNSԘT跆t5I ug}XcDs9.Dg=u]f-0Y*:ِzQbbMcqĀĀ "'bsӞ6`_?۷ûwh5梊o#Yo3[qy^Sɡ w7CNXP3+>,}H3XPpĭaz:{\.5K~FN*Js FPL. [+ kUi p cÃ-Sz7nu@ֲ_h_*`+h#qͻ$*R!*dbBMffLD=hWV4;X3ƺvh(.*..B A|XJ@SqaaB G{eJiQKsee9Z]]pk!:`cCziXiccc\>}3,q=yÇXSSSn{dd4/o"@ :SZa+-+o?g`y^Ȕ(@AB,+TTೌ PwZjjT%VӔ9ɲ3IrUNyC;P$%%K8$\^ad YK 5%v{<7xIHɿ8?7V ˪~#?_`hhoY'V|^|^=?`sNpO")˿ #V[W,IجZ |-XI` Q&khhGQWtT's򕗕"ap:0u^zE3_&_6X-ZwwNyaaI QqgYy3?krwȇ$8uñSgx7?O}+&dTϏ//Y›e_KӧO={v|999S~Giɸa/>8 ԇua::;e몯_RUQW(+\0ɔ_}=9t'^?^gmHGBr7`\^bҘy }'0=` m[@!Hȡr DVQx*)$H'322d5KH~ #“Lb`,>H+.j/^|4 B#"vtٶVd z10.b.139T8c؀QT'Il|aqb#IeLqT8#^-b9tx_qa0oȇ <Vл^Fv$ŇcdxG:RK~,ZGN`n,C xGף.EIۄ>f uZ &6OOmC[]imC&$S |ron.[ݠd ϒký0 (E=(/Vi5rJ57i+ՖL;g{nj|u/!dztg~>5qKgx{'ѓ?8~\J[zuo㩧kąE7K{2MKY],A6lnj׭T6+riIE)WT5ri{sZUV5jH)jNdQѨYa PGG7.!OAz@O5F~pp &!ְn w.,,pam=~ӧϟ?Gѣ@ zFt̗8cL&mjY,Cfu؆bZ,ٌFz]6jvHJQ(vUWWi $@u0(XW'Jo!o@t@$//D._ "ID٢b%Ik@z%IO٢PCI,n4 kye7ɲe՘ʲJ"_iiIɍb_YPSYǙ~QPq)zhűG[hEm]}@ ,B l %!5@e Amʪye9@;ݳt95spssNzz@ xlqHB>2WD{LoHSӲî :}&8ː3g%d%΃K׻򌈈rJЩ,[!{Aa}r>@N].$A x"'۷D>:ac.>w^hh° /@*iXq>1VUK2I)IO`6'~Kx1-Ǭr1_t! '%I+ "ÍߙA:bgA#Ƣ~g$b|p+#G 9E5ž'ܦPq8ED cAf9RAԗa!QP^&+ѝR3<ʹpcٸE8Sɇt?Efi8ػ~MNokO}?Rl8Fn^ⴃ8]vR 9HцF~ af Fg Mh#w1 i܇l]v;wˮmmXrF΁M1,D_t@AT]HAw\rDt'"bq(ba:z9Д#b WΘfe*&rVW:: mN]QVi r&:NR,çDTYYQ]U!k NuSe&mp#߭[,#YR~k=k M fec?5i~|aYZ^QVV,sZMy}QPբjibEqeY@gDM% ̃'&zgUuv4=9Z8O%QbJ͐g>K5ځZM2591l]CYMNMF?yov}m_ЕNн>xc΍K ל OHREO\/=-5=qV07'@t))>*X~2P%+K%ehe8P1}?kj?5/ Zm_=:!Ŀ]͆][+5::ȍCVÿ͛mmkkk "lFQt~G ?'4qrtήNԍFZm?PجohjF65uUյDŽ>z~el+e>oR ˮښjmFKDRgT (y_G" 33322[F=|(ٹ9Z,)Jq'y'tT)%BXEu9M!"N 9RH*H32%ɉ$I\B~OIvV@Cdf !\VuuuZxW SCa_@u<ڂq zνG )Ԭ$wc^ׁ؋D"$ChL_A#?v@ϣ|'aQѷF .;wmEGG"#֭[aaa!fnoVR仪rR"z0ZMNONKJMLM,x۔Aד~(܉DA(rGa'!d,zx" ũF{Bj!aaD>J8 0@Pas$toaFRng!?"GLȰ~>\}{(ؗ ~w{F{NM~0: E,au[~/g CØdc axV>xk|}#`-G[ ݙ粛lb:"}AGv\b8@:pb C +pIlS_a_s!mjD5,XGDzlKrcCG(4oY/[\ %Fn;gnhkuMHO]c0ܐLg.CkNP^ 2|>*Uq{[KkᙱchXumʔ0Q0_Dݿ!￱+ZKFcƸn 4e޼074l2,̙F^.-S=SV ([|l3T JEiYnHkR>i-/[^\zbUKSuqzXzb4$Yt?c]\7?U$'EQZ*kjHÒueK33ӣc=C&c'`Yۑᐛ^sN3 _7|3W5]dNiTtvLl 2l_vvP&HMAO Ar !}\)SĥRe!{V.W3Ŀ* O >2Ͷ77"l@2k;uUv1 P@0%P(o"/HV &|YYYِRڍ70QUEV7CI#)o/˨KWƦeJ3AcSb.|IT( iQQQ'N~rC~s,ɨ 9r:d÷W#ξC+WDFF={|2i|׮=vdovuuw?3&&t:]KKX`AX `>ojPFU ï&=59ᗚ#91p^l<챣!D[Ē\#[N#QG(%Bnd˹9 ӎJx¨sqc1jq࢈w yqН1B9&\\|Jx,k}x` a1.?_2QEDZȷ8P:xJhֽ5K,~|k8(y>Z~>. ȋGbkT8ZZb/4<#Ieћ0=}9uu:|o&V"""@wȇc] 蠣N'~P_Lw0ŋ1`xs2$@~O!ԗt߮=`q<;W;Qpf+BAޣB4  GmG(gD+wD|D` C0n ~vTr 7-1 Ytrt~w}h$(Z4Xq\,ϟ ~@5 2痩>Cbn͆Zl?TB(˃ 4 0Wu`aϛuƃ`njI6)].1 ǥlˍAGȓdmL#9s[ԑL$a<>OÛIe>|AD8?8 {T.G'(3鴥C܎{ml47i5=_wg;b榖懀BА7j4tiM+TUK>~.K><<8==m2zB-CoqvzԼ*&Xoɳ6FC߆vlN=]k-&of}2<7֟g q}Rq{qi͛'&!@cےBqVsoAKo_^VP()UZRp;JR\.UW?+lVԤavzغ8 ރ8wwvE!0 nֵ;uk: *  <$ "Eބ%7XwN²n;{sܓ{9$usv'w22:;+l^M^b4f~TcZ_]\]Y@[^zbn ~Pv,.wv o^.=[8yw;v[GdTů ܼπL\c!C"RhKj* UeVsYi)8PܫgֈIDm iqo[ÿ#FxFJɓGmkoTGG{?qr?&=233ވn? |D74V#voxX@?0}ήncmo.F vf VM=|Ԉg qY>̦r 1H qqq)))l$)`ZVvXRVwz^h())ň̕&ZKF5jd6WRR_VT/**04iiZV/da'nWQ.eu#VW9u!*ϣn'gdsӯg}u7\"W64DFFADυ:z>Ÿ#.'BCN}uˉ&gi&Ξ=:v! 䋈8R/O'qƅ  F/QD;6.Gs^ssSwߕg~i)诿|/0Nq4DVZz?(tr:21o g!/s {{ͦrhI5>t}e~ccչ.:B /&O9A?!xmK%zyYa]mQE~J^k͸075bmmq oue~iqvf)F+ Xj5~rB7]; R#k'>uF_KgGBl'nvYiYB=\m&_wJ**͔$J WmZ*Uwj Uf$@"/J[=o" aqqbfn=8Hv ζm] l$X,..tok"V%l#;w![K$9:~DFq \B {I6 m`{ۢ *,`*藾yع2RL ?.59! PNN4oZ& )-/H*`qF>8 +++*bFo"va8:]XV@IXh4frrjҝ;I:]> Inhsgd`r/%ʥN_t'9+3[-KH׋nf7?È>sc'?<ɥ>=E\ة]IQ{>DT|j]{Pv=!!|'?PQzb/^/^\{-<7B~}=7+ojnb&#"贤d'ް`*Pr" Ap27PvxX-} !.%He oCpNHzv8Ind8pXua.R#RZDE% bl )=3!@ES"RIhWRQS'hqГ#~r~tG+J?':ْ8kvŕSy"[I(J' M& \o B½[,ǀ  =\2D;QB!MƔqF(C臝%g"=grFJ GBޣbC RP DTH>d*AKܺ'' %NL8y7U[ *8ז;.}#\@H A@Ar h߱deN׏~ӁTnd-p i2s"28a0foA  R!vS!i"# LN"ZQndXQꗵ5#0c#d-H;@AF:BdCDz<+++v޹=עAoFJzAWܯ6Xk N휜NZsc~mU'%86mi.m2V=^X~sQ鹽ypOA\~ڲXxm2uE/}nUKJ'u6uw44I)ǂ5O9v?ԋP NܮY Ae׋-BEWW=2.Y2sZur~X#i<Xt/Oۦ MJ[7]j 7~Owj?=zg~]ZķIwlJH#^IfIQQQEE9Rby9S[XW+%FmlED,:BT/iBjXH2ٚ,1R1_L@QBѩxr@l|_ˁD@K*,mq  ƳfxƤ! -҂ ˵?̀5~L3h$@>h67Ï  cf3VQNjA JYxJy#l s G><0$|4* x<0ddd"!yKII BRq=B-@}ul@H%4 !~ GWx5TX Y?\@VV67@G")hO1m|wk7R2K r>L˹ ZBBzc\g`Px͛_Ɯ1b)=ds72BO=3?eRRFNA1rVA _bbݻw'l"_?\*:А]{||o >M yx&p蚠:bڰ|;Ŝ‚ܜ\NF Dq{,Ud1c4pġ P, s4<(*<;l `-\ 3qz*l;FړhI:BkFb?"Q$1(sш rQF;4dcl?AGF5Z1`ECl {B|QSݭ05[x!H@֌5C_֝ڀx`<DՇbuTLwD(i9BZq_ąơ{Hf)@;+}|0B89!I ̆hb52+# H{zBTh1. éplDA`h a0}lړ ]`H fhbӎ "({_ 0xޅMN"!,F(f%oP L#r4-HVA?b\]]eǣWWt:r8m̬vL(j/]^UjW _S3ycn*alkT;= v6N r3IAi[NY> 4nW^k{ɦI߶<?,HƝxt_}G+ 8Hm|>/疖P攖pҪʊ:A ._% 5RQU^DxE&#+üDbSMrOމ T{~V1jAfH;i##CX`X/@ [`lCz1>1OƲO!˺/)6/O]A6餝Λ}v=iwﳘ$XC4إPPRRXR$..w4CNd%}(xȗ@&r Έ?4.IH}B $瑠D!7P($\c$J'!#bg8PD縃|]<\'dB$h'$Q4d11!űǤ{hLj,Wǔ!^ P?qT^ .;%&U2XM2^1A>T%)/z؝/,9A2QڜY(ԇV _}ϟ?< |# v0VNq;A!* LYf8S1#.-iPqjD~!1G%Ǔآٳv%P)ǴI(8|=11|}L'9+1"$q4>70 GMf ?.@-VfiEiz|^4:>:<6`aλL}=F ݔG!o>-MI}yP~qvv$0?p8x#4^v-#))ZP ?0h}g~lDh_^p-5,fp97cLu)y1ԵTU4iZۛ]R.yܖ) p ,3SiB~kiuf2Vw57ܮ,~S[YS鰍իW˨4ICՕǔtW7 qwQx}#u{M(lꪊ+4(XRvxc =9BŅi2 JAQk&^{߾;U?9r ݟ~j|+Wm4zZ.S vBiLZuZ.QުSxMЁQ6*MJmJ]#UVh_S#u##Xʺ(@> #@_F`xf{}S^߇þܮ$B.mwv":_9'G@6_W=#ãcHs} @TQJ)f *hӶBtd lm4UPRޮsV%ncNF%(dD>YeU7>rgG^| |/ԧLOVܒ+n_-knG]|]RR&T(J$R?#$a'T*.\p]o>>zsOs?Ry_wӧO|JLQvJjj`Ot"]Rq;?ɹsgA>m11=|H :dZpkˆ/,)A}" qA^~޵Of'?s hO΁Dx(ydNf%' Od#370/ՁQh&f;L#.E0K#eљ8cab6hn^`!\L*!,{"16\G\Bf<5,XQ`CڟZA}tX 1!^Li4d^4O'""| I,S`!.%8 NMj- qB wú{g/`9X"1ż(GDs(d8 %2"ϘrD>421`½qQTgB ꀗ(D@P:V3$2:hPAI#0dhq#x3%!*>fjb4QVil8 h&T绀a{d(ю6 0{lWƴa"_&XS9scѡ!Cz\"fLv+׮mninB/FV٪u045i?Ņ!#>S~jpqE2OO>~Ǔ[m-&OQK.p[0F[Ӗ{Է~гL~7'u9F-fph9^jj]UiAݣ(+oj\3YӮPpc{f@4~r! !IGa׸ATuuwڜZB&QT$50x>Z[# >^8 iyq oҭFh=hԨtwՒy @Z1X~MF17? -:դVWUwf5uXsŮ#"7-EY~۲#"yҏ/iiYqqIQQQAA2,''GP$w0~9YJsKKM]RY䫭U(+ <}[|OirNYnn=bglAl9J|kpJe5},"߃ =\~rM$WܸYU_S/>wM8??@V'> :#r7OE##rܺ@o29ŋ111|޻䋉=Q%v׭!1'G{Ο9s&>>ڂGOZakh Lj{ UTM*<Ԥ$/5v·@!A@]x P*dPA`>Er+/$) &(IycAth"G sKܳcqC8d6Þ$OaL;"z@)? \Np dQAOV⏊+6p=2- URZ#F5L>%1Gm ‡n|9TNOI\W0oro!_z`0J2d^0/KEPOtd%nأzRv. uU͡5b!1ģs6傆#Y?OOyc"LDO#GsY`!tH p/h xzPC"=}]?7+ub0Zh񪕕H1GA>Ąg9ިa8U髇ހ#GN]J}|.\䋋r w-'&&񦥥Aџ=rt`//o.xx8OFm}ҥKct0VhY u]}m}C]KK I1Xve%NKIJIs_|s%toT/ey3Ğ jar0,$p  D[/64; CA<^0 ]`Ocㅒ(O"]AYR$CfC*3qkqhH ]0hCw"koQ? ѡ)u Ho:lŠ ?Sߛ7okxR7'u 4f $; c9$.[J E0'NὝ 0IoON%t9'C7 e2/AP'!FF&IR@c9`H*hQ0@; su>L!(9ZP0hdy=4AXXf)SxM=e∖Hb!L UVmN00 t|-Znڨ~VL06imV)ԲPTjbilKKKKKݔ|H$[]Y݆:^X^{υOP˕gKs_AMmw}3Lu*(l<Y^OQgG>}Uuިu) @ –} PDH,K{Et'g{{n~ib{Ңf^mz$bD|NM!tz̰<\_-V&__]qZm qTq8_SM>}:80*mY:į~vT74< R|g΢u5;ls{ ^/l^BX[6ݺ/6J,DǕi0u{Vp~۹8@ߪr9vsL~/c ~<󳐙ĹqZ+y;y<5=ScKܑe|OP[%y6r-.s+jkMMbkMUB1}F֏!@`~U?جWtqP4Jc-KWQ)/++- `*J?EB| [%,,qsXA{_z?NߓmP f|,d걳i/>ce<:(ԟF&fԕVQ ;{we'2OI8}dᤘ}a!!>q4<2$>)D?44l7,z9!$`b㉀v}}]]P&0yOo/<.UXR\XTX*.S)GRI=Ek4*4x55 C0`醘-1pJ aa4^T:_z`/i_ML@"CKK`v߰adIm x/E O"*82qns1 #.ANƜE#"zK$T7i,d:& ؛vu1IԇbèR02# N?B4L?!6q2\$ $茏 I{۷o7ꋀ4 B?J51* ZGAYBiQ{"Cw@0#="A$ =+ nD0H'u68v"6HA j~G桟A8<43ӌa|U=( =`h'M&dY_af~QlLOB,RI Y=a +3xJ ]1fMOMM#mjUC*T%ɀSОfC:B>1-慑aeyp릹]&T*;-KKKkkk.Ғ˗P߫5\uͤ/]KZH{Tz!p7}0ۋ_|MʳIݪӸb\ qի1z])¼ /Nw-E5 츺 қXs_˓T5W(h9`?mֹs֖458ݫ-ªF_=0 x\sZn]hɹuxv,/SWu L(dwgMC6_+uŚ»*<Z"Ϩ;狮 yz]ŵn.9 6өoc4@PUYYf#8*^ZP܍.J:0z0/CC\.@^4Hv8*h NT1]P(0ޡ!zXQi4<%avD$,$GHV2?7˰ @8oPVj7[A[0[aXh1SZ0GuAщ|DEVũp*+(գ]"k!kmm5˚Z$D$ j-j5uzxN$Wap r -JzBmﵶ4$YJJ4'q@+n޸^@zq/=~2xvoot?+' _}spD|vw˿t'uW^ξp엛|_LO<}(3=)id=:?*;ߥ}q=b ɪT`bcvtac=9;;?⿞n, KYEŅyǿ?[hCɄpN{?5uq2+!\ `E[i3ݶZGwggvZzCA !\B Bnܣ r'r=$@yqܝtϼs=y{Nr<u **`>3rᬽ\Z2OEdX.@Qč4a5?${Ta$T\uy*U,An70r-eCdBlTh'0r18%a d2f+FLT3.a# p2I>F>^i) x'-R%{h0H5Ezh@b-;?_Ƞ=+Cq^/Dc&,\OR}N㑱>A)'SpKl6v"Y\\:Wq\u3s߿_kAֲkhz?08[}sԷ4Z]s 5:;_[G6=S>ѠoY]iw~nk-+<>p@cmuNU7mʊxbo x"<:\=Q+>AS=K.\T,?WYb]ڷ$RВKR],e@k[6W)zOF;ސoj:%R\`шɨ<}BQBˡPu iuoZ|])Q;g~{zY%*a7/OY3[N~8Ο8k=qVyZVޮyiS(+(WViH:mA5tF0[hUb0z^! @ [{=AmmuL7E1aHK,Eu~/,@P<H#h3?hTyr@Vv s|]6:!9&@lxUCSCCC QA 8{Mafs_k{و]Mf|"ZؚI zFUTZ jꆆzxUHX*+W܂6<$l6s)wW{4ʔ5EW(˯ʕ巫U:Ey /*:y7WVVVSSٳڱ x;ױG >Oݝ N|_.wwJWW\\ Azo${KSeI ]w̓_}gHO֯6̙3'O,,,RoCGϱI4Z 8޻~ J\t~;x9#IGsPrR YI"fG#*L}pA@ƼDVODne 4i~h o2q6]Nj؄i,J xHs:%)Lil%Pm,eU1G;|S##Y<͠BAᰍwzVAG&Jpq3rLa:AnD# ;~JQuW۪o2gOPim#qHܳ~8K2\в˨1kth ]݃Zu۠S/^f;vWa(̿S^,{Axg8Eg}Ej(*ZUP306V^]_]R20-pbw<=݂Z ˁE&&'F߾ukeQ̯3 i#sg.OocIpn3_ۯPՕUw@.nݔ+J* zS]Iz}53` C[;^> `nمJm&Sf@o?#JB`$A.@IHēS9˜u氡ة(6;b uUrM "3 7xH\ٕEU`s8F`av8FuST~ &@{z;/Am:M&lji] Bz[[D* jYEKK FYLxhJyBg+'c XP"c${%RJ V~~n^^NZjrNv % 1}{c+#0_Q$5بK7&pE,&ed\_M= sm%4;;ƒ<bF }}?F"Mf).\={ŋpj9X=td_GPOי3'9c0υ˿: ߜ ;RllPk+ԇm ^c#OAÂpM^> p<є;ɔn#ߌN>oJ?G||0P%?'D "x;^_ #9*| )1#y<=4 ^ S؏"bʗAY!  $:v@sA=E^c֌KώbEF ]%y/uwrl̄APj8٘-wS ܕPu'~J8tuf?RB}o޼yJ};?`?{mp uD8p}cv6rD]db.& w p#5]`9~wcvhLsAKW'd@فS<[= j&AI_d bpm\\Р es g9($_NXK=;,Z=52&ԱNv[['`aMy=Ś!jPQ{[ˣ|%F ٠7jىƺ-)jkH} V!>,GG)šA^]Z^SF/?\3_|4γH߇@˗_\__^|YYώ,̪UӒV?=hN>ilEx6V> oemĞCkˋS@#p~F;ӯTɤ5r4+\)˕fde$iwui՝9byh"MsƉ<*7;I^??0RU\q/{wHK*5 CG!t'c:* {HKO[u52ARDpkjkjUuu_#Z uxL#v:!v`Q{+T1͸f\ jtcZ\>[cel9x[Ԍi> @JBwuuëXxШ^DBТI٠T6ʱv @BΟ6] m+OC$% RD"),,$/+3=Q`@.J>CF> K~|8 =N>H]Xx䞠_{5qJFARJVjzޏ.ǦMH>#K.=tF#F;BCN'F:F#K9CCC#""~|cz0?+ h'"}u\\Dۭ}w1*ۈ ;ǿ=>(1_}(0A<77c__ cjkk1I _Rb)g~%<= 9^(ݷ<GEqg֌*b]}Xl4 \G g;_\1N I]h-Hk;>Ra/ K q=z{>Z5QS!LNN͢B߱XY"&N>|;F#d&PNq{I3ׯd:[Y3C~V6x0///?3ZU̜qxjwި^\0 F$IoO/UY%ήfIN^ˣ6b$ӝptyQG rX]=xnFʍԬҲꇙ{Ҍw +ZV,ya(% m: j*K55US3- B+Ҹt+N76.m+-ʠ,! $ր6A1= aK؄~7_}a9 MYNLT:^=ܓxw] euMm@ZnWת+t]EUIa-8bkyuK SS8mt\1/ R3\@(YZvfq겥GS)+.oz(v?zuy^D2D!.yb_Clt3{^|5xxXDFxܝqñcw~JO%|yQQ,O%32Dily~^L&V!bQ#BV%j4uu5OKP ]G[ZuFCixLGGُU `W'yۯ IwwwAs7qx6;V n)6 =g} sKy 3S 6747a H.{$4V^YQR*KTeJUqQI!W*-.-.QʐbHl8S.c@> F 1m!Y+8 X* n܌<t[S#)&ݸ%t1--u+=@>BrӑÎG=1ɠ7'x"Rd!˗###!k׮|C%%{p >rK1QgτKxbT!AGvr߶ׯ!tppD܌k\б@kluE,qX&Le~ p ܎ } \YuA.up_Py*á+6h6RU 4~H~p̈YD~=ȕD;'XdIs|N܄퐝uXڥLK0/epg9 :܋#;6-2;A`}4rmV.vl*Elp3x܄=!\f{Yd3q C;oۄ۝v 6#ZЏTT6tGtbc8x!lc.6dvص:peG2"qajoT< D`9`AB !5X[;i۳`T{%|K?b/cODc/^a;ʰaE? =@z6~&8e4B_O#y=PkiHk (V'$d2f Zv*7+m5=}Ol۴ jX`y0 sι׆Wȳ%9q2 WYVH$"YUYJihҩ%ܒل[P͋@0Y[%Y̳CѾ;ޮRII{EP#Cχ o+ cƿ%657͌-Yf7#vgFQ֨)Je<Ȁ"[vv, m-#xu~+o,-[G_Mf̋0:+<0Q_RUy_o0vU|eIiχf/gg_͌M&<:>3 +=}33G0yh1Ԙ"2&eO wX8!Z"),,P?nHMLs?K*)Tʊ2HF] jNES8FVVVz Dd̆_ h ǽ ϓ'm^cWfI@/~GH `0Wcmu;hWQ f uVG&CR $#,UTby~>.]yR aπξ6L lV|5|eee٫w.e ) /=Crv^`jrcߜ ;p,:aѷ/%{_aqo{6r/^6lV%''Ƃ|ϟǴ|(hȗ,wqqu<'#:ygС"O|yĩ~AA;>&p n޼E鱩!0#Ot _5P_SaL)"不~QB]qo5>d{Lq|AάL7wCOP)?^)[~࢐ LGI^ ! Ȑ]ze?Rdr.qSAdJH!3$np1Yf5{3Z!lFPcu#c|'\|Z0f%Z 9W@AO[!hC޾}ݻ7 cB8mwCc܉mvr0 *Ұ8p(bFR'8 .|$A{8rt#ò% J;1 WN&|8r٠<&yhǭ?Z/'p_!5K/۝N;;mWEA!jժXu;AVǸa$<"@x&wd%H !!Qm_{:9s9r99G1R~G`mŢ˱P)˾xzC}63ACfh=Fuv7Vp ^$yO-& lk"5M/HL!\.׸dy?87;s/VǍzE^+*݋/[>4\ZN+^l)FM&u9uڴLvcb ,Qm։GQ֧PTp+z]vqDR:˸%UEb)䰏y=vt:s2ZңPUV֚]<.NXԖ0]Tu*!nih?'隝F3f L=_|ju萡ūKkjKjf QZjc$Ykяc6m ,yQI 9r;]ipB(u';2d綋k2in ]%)^.;֟[(.+/aq8ٙy\ V3RIED$5d2* D*!!!k p\ P(2AbщŌPvw()4EKK_kq܏ףB:݀^?86fg,?Z#MNM]/1P끱EG7w *k5׊k%MGѷA>l"W-_QUU==7* 7&M5xu'Ϝ[tvr~Q;MN?ox1v %''OH8x=խo?q!a=~_~sI\5| ߵkRRR~B>򥦥FG5?:—q_:O a SgOM8{bE|ow#GV(041fn[WR |dA}q1jH~oq ("=jp8);GX.FO!C4:x C$~r q :BA8aG@B&2Nc.CDg 0[` Dr1{1k ## BD8AY:4ICm$BDJG cZeGNؠ# \FbѲ'|k1DI hdb8wAe<QQmf v.jOXT};)Qr* 6#z"#@uvn!#.m"7|ķkc썳eڂ;O|-21B Lb(2bBL f:8$Rᷣ1ĤsE'&atqDVC\@f14l~Ϳ-N #+Iְ6~2_gDj=8ه[l:}US[)l}"lvV^n+/W jE u !~0AX*'6IrYS+`<.rd+$k\*(-4S`Wծ @7~~6>5Ǡ {R⪟oī~ee3bsdff8lBMJ,'`׉qˤRXQߛՈDUjHT-VQ"|||:t:}޻G|kK*K[39992#OLsm.ra_^^!ӓΝdBlRN^M Ċ<3̾."_nn.wƍ 򥥥eeeeggE{"oڱcK|H g/?rwq': j:N8ŀe'ubwKݐz clYN bdZbXlIرSwW?A=INř3{̽x||lBG?8s@ݣ]a?R٩P@}dOR6Y kneaK N8|L/=^;)bC~~88!6DLP, \c@ :. a1_$FA A d^ !Bؑ?' ?> d9: ga&?t>H͛~/g`n?F>Bl.$=|!@_?O8ga6 %O8 $xf3h[E)q*G]ϲWL2gh2dHT<`gd3/0olnzxNIxkCf:Za22 ׌1'JĠ.EO7K;=IĠ3 F,^umVᡄ*P_SK3ʦz>ძo~TsFDǧVmlqL7;^B~>Xe[qVM#*49 g[#K6ˊX@VŬ ާc|QqqF]]/#[\-NOeꦍmUKK6ӼqrV? U–Nȭ;~nAXR%֩%U93 ɕOՋsS;tcN%fԏBk0A7=m`N XZϪ{{m]ZP*m,-/UCJ<ՍŢ\!X.r_[m2(ƙC4-%G]9X[^RXV__XagԫK>* GnZL:A=9Ofwj^.w\O ۵Wݜ.Vts_hG3r%EEEEEeeeݹz7=-/~iI1 fl ^PJ5ܺ" <92Y#rilhQ:::(64Ia[km0+uw+z{룺Q[2GDQ ?PF 3HBpZ7˗kkkpeAaCX"H%IW!VO(,.BzC> CׇgV ־_-'5ɇ"~PS!ݾWUrj _\Sr?VF?ʍWsrͅ.]pԇċG.$$9R*tsϱ:}4 ='/%|)))/^P'%%퐏o|.><\O\b3QaQQ~^A^~>:~dAA9#3&WT]GrB!'w͔`qbٲ4ȇۂF~uub`/+ԗqI@ם ޜyҠAh2!({Xs7 ψ Oxe\D cT)XGlKI,sב$M 1 n mcca,gQB\I#naۨBAk("NGߨիW_(r΅c @gQx?1NMfzr.dkF'5&7r xhs%Nv W_1;D g~ Y>⽳Vq =XHXC4 I _he`^,eL Ca[aW0bC>oTsmnC;(Rtw*;5cF|Yͨ#,-Z,u2"kBǾbFch4[,QYvm}CBJɝCpy_a￁ߏԷfl@}[[%}Q3 /5VӸvj24G.iYNAnY]xl4ryRvLe95 im[Ei-)nv0x?1٤lsE#a5cVqmXW7_S+jkl*jH*lVNMhwqr.VHÜY̹:m9 *۲#'VSY$.J/9y2Ո*/wt|Lg)q9zn6O %iƩ-*n[k}\U|FWDBlԱpLk٘e"$ t#J?7 fqUcZ({Mï?L'~9}7=tX~?WW癧yqn#[D%hd\$&MV! Ⱦ )ts$/3\ܺunsSӔ))II[7""q1HFzZNvV^nr LmK»EEʒ"T2@uu5X (q[E[]V4F&,'Fk{#urj;ۺ:PFF 7FdK8@ 3ԟׯ^㫷o޼{<::׫vuwiΎ&MA_WXT ΂3ᾬeRRBbBlRR|TT$N1 2،|vnAz|yyw~tsAWn?cӃAaqgܼn!8p䓣Gp?g9}.v?f}6FT~ ={|5%6vRJ,-D?Ocȭb3 cGg8yBj+889: V_u̙3/^5q>z3&$v aX=WW}?3&:RDFܾi&=N%n]Ĕ2t0= !IxM$"-XfI<,A"=X(ϕ'7!壓c>\l9 D[hƖš̅.= @Lpцv(rh]E&z.zAnbc†^qz; ks 2lE4]܎?'%qYBh@m8  D)0~``0 Ӭ#~qgq%aqPqteCإ줦rvKIu=m'۱LJa\xF|-b0?+uv?gpeCp"4Q:`{Hx}0=iY$ t*o6ND'b$f"W}ζv 8ijlnjlR7 |(PhueYтC _|XJYכo~ɓ'KHg';ulN4'K寰o ͧ֋۫+s&fuc#Pںɮفqhhvf 7oiqkG܈I ͎͌=1L,~-87Bs%~5ۅG3c#Ӫ¼{}ɉU}mk&KK :\ybQ=?:>-F~g4{?]0~+ldण+)^ +Q*))wccDG*nݸy34$Nt29^zn\ƿ¢ bXZFJ:4"KDuuu )h ~ԮBR65vOݢQc)-ζ.mVhnY@N|޾ެwv`vww7rG⣒ȗщֆ;ƽ+ȁS0 Wp/&>ZHჟ^ ͥ؄t/.!-8,2Ās KJJCk׀4_~=~ȹP }yc&TWww ElR!BBBN> 򁑁c8rDj#exɟE{>zXJ̍M RSoW7/O9%2A[@}bkoxyA/=M:ZTUYWŶ5 Z__[J]jjE܎ @awk.=?BPq08Iz._.EGl~e8"(ԐwtE^t!UO#ƹz R'RO' 0Z11%gbk;8lKQc<7!YdBa]'9e=W=Sgr3 b0H%;*39!lYc0%-g0)E0%RBH4^1١޿Vf$}&{Р^+f?;XpH0 ‘ c+p4.BسȬhnaf6yrHw=S"r1&ܜ=Yr>?lQJE\wN6f0_JXBzl ~nxAk <֍-a h4MxJ谶 /#^b4ҪCs)moWGY i()OP:8SSX#UU ˰,/vZ時|fg>zFeh`Jֶ۪6Vg7wnnm"~#='67{r{mu߬wP[/IOPL3.Ύ3W|9K2wgs6gUuF"B !TD1p=;}l&\{ow-YkL y\>:m[67knͺ0꙳֡UnǼ0T*=m~sles#ēF9w=%,C\PuEUWCĘV0J5]/]soΔF٭O 2 ^UPyuI=Sիі du P8va~ggckCo3|fv9MX72DOMO꒒Bu.ίWEe|mw_oI+XٙV%/B9l@y!2i>&yWLr6s(iV:[T9bx| Lf%nַ47{dRI\!lwINS~ !DIu!*z9K2H%5\ojuZp|L7>4:l2HZ޾|5/XIÊ~Sގi뛄"L$jljDo`$j]f2YT*" ɐfiiin%{ڛۨS]/n|y._yTYU*+yXyzz 他?sʧrWy*ȼYprvTbZ\'3_]xNE>;]v|E:ޡ؄H !1XB仇~}N&:v6%htXБwGdd|tTjSOGF  Oʺwylt&Ra=?s)pV RYì(/}Txx\R/] !35ES3ǓC3^, ̆Cwh "}H݈ dť8mZr-:4tC<(H 䈕t\~D}^zO>(.##B6r#BpQ4HEGQݷn!& # *!Fa,ﳙ^y|)wDNA|2D|VǂC!'(ϟ"WrS>DmNr)L8?cv&aiE(F!ݤ98)0 }Œr"06(}/OVfݝmZ}nʓ =ԧ&St#eyn'-PߒF 2L".Tp:sKF,෻ón7 FsmE]aII_ [[ݧϞnlx=.#5>u-|6}vѨw:f.uO}s;]-m5~fpf_6ֽe`?iybUWWQ f x&!A(nJq!tw{p =D,&r9 uttH2T!'X*%r诳fk`O P3"Fu{k>4hJ31>:596;;m4f2 Dڋ/n7y♞NzԠqV308%Ih6ED|Ɔab},뒒bC*{#(Z??D&-յw}܏2?qۢҲJ.VPQɹ}7۷ncɇx< dzjzƥ̫)Nɸ^Sy")O?.@;E4q9s|f>vʕw PEK3¢#ὰ!ӒrrI{4qq&Mv6X( *dq]u33븗QgFFr!(r)R-7J9{ЖP}g2ci|>oqɰ}rP+L:/ "E!DߝN9(I9% Xz$ۗ.]Z9N'ѮUmmm--6zT]E: rтܗ}xl JBq \83R%x!wh&͉d 0(AqݝP!řGcbQ( D.D|L}@ h\AQ(Ht1NvD>L$ALqI~ {x !ЅRca 9- v$uLw4O& 秘`P$@Pn{ ďH1a{I4NW8awâ"rsсv)<q֧8jw2-~! L}ѬSf٣Ɠp.@zCDg>[\#'KύG#Ec໦2AJBR"^ELu˱Ue|LBb5yV Qga;SC.; +l7YEgKO4W6'ƇCc#CWԧ-,[,&nib]"Yv,t)^*$ԇp5H4":;ڼw`{sԶ2۰j ׆٫P55缦i~;; r$Cv㴬uk+Fݴ᱙`nf^s.77k+Kj<ʹ#+}Xn667ݘ jf5Z͆e ne]YÜ+4z}Nף0LBg7ږ`ڪy ۽8ohm}ZQR4T|Y]Z쨶{`hTUU8zˎ9sű]f,$].eͳE%l[ѧٲAM?>7n|׳;׮,6dw'e5g(),u<*+4Lژ=x^rV0!Wssr闂7-TVW?+?.((ϗݻ-).|\^Sy55Q@zJ%po&S2jȎlQ EskSfYVS<#꾾n;j4Zm?o CA&Fp@`@ѵg hzׯ~B}G~SS|p`P?666229*I^O^_Ae{$ xOŃ0fUWW[W[ >ԇ[CcB-)W\|//xTT򤴬J[yv_of'N{f/^QS)g7 D`s`<#TSAvP4ĩ ,Y.'\*8BR'FY:D/Cg9/!q$F $I@N> Lp{P;' p);40h$-%E9 L?N}؃r%7o #?` 8#a@tDtr1S4<Ȅ3sPL {GοY/gc| -$$` j  #=jc"*dEDx{킌`tԱYe2X@q?cBURT^aѦK=E[/Yh&in loAPVCyY\}fǍT]#] ?/;!gvیqj1Y+ٞΥR>-?+ocplfo~ A}իyi t8X_-i>jm8qyq|UEUsUCAe_qOř1mGKݐ6g5O* jœV7=0}}}HMM oYYYaQ!Xry(w3` H)G2āR d?<ٯWXuCrLsii9oމI 2JKTd4Nȓ'O Ш/›#7 n~{O~~VSg`s]z566gW$Orp24KsC@}}S;{J,YB6{{{+;D &>ڀ]!O|!cRLӌAz4biP4ᣖ;;)I)n'%ޖ'~k~#]aC ~G:Ff?G93)#: % %"1UGu%mye`Xwm+o,^oO=]('c-8vP0uI(#3R O\w<;ZQr 19K;p(H-d0N 6!{:3iNL) i-0oh= P0#"yt>Q۷o߽{_QgFW6̸!1R "1vBb+{〻d[񳌝hH12=-EI a1\(|J uV6[ݝ%x勉IESdp&`۱)g[ !<^Ö u}2춂мeh풇V+U.oZA>: znRߓn mkg{1hF''ǡёٙEݓښ*gZ65UeH\}O>Ŝ웛ʼn+//>IzĀVk(n'{fiqnueaccuK} [gL3 >-7=eZ_[~MNON.14 ++/^W䧥Wq1w5 KK3Ah"63= MMBVSs#5Y^e|V_ZtO\lj[4&j^n<X#KKs3#Ϟ^|V\\\LL Wqi+YzyȎ:}<8H,"@$6HE.8IPaBKǡa={6<<'+F&^AMewvӎVQDuwj㸸늲ӎe]HD@$IB IIH  Ns.0;mq{=s e˝&,3OfQ܅ysUd^YG7YI̤bwN!rJ\@[!;"@ ԄXGDwc<&c/T]zDWj1kJW*٭kaNnR1V S%a`?;̷h]yިĻEC')$"ji![]]|~/nmmWfy[lI+w3:0 |YLJf`Hx bHz`/<;LjjŢnL mnDP[wCkN^ (:|q\Y5V.zb\='[̘Msz}z9j}I V݋L-0bHiX<2 l (4&̣ }^kί9lgeK~VXjer3NNg{nޟ?QZHrk+؏MʕE_{UmtT7 6XeeOzg#iȆoc;mo'"!Z9Fc0XR` VQ44r;%^n=dR+ehD@>o3r/mSS[yK?3wh{1˿=!'>kRvttZ]z]]PP]B_#!=8Nw/, hPGF+WJr\"@}JO&R#*!5"@; j4*IF@FX΂jX&l S^njZ^ÿiw}ݻ^o߾E@ !_3!R dlT*|N4f.ׂdB uPzko*YUpo~_+*_skxOyҺ{~0!#Zxf՛n]zaIO yrg]Ι+?**#/8յ|w |x:cF}^eĸ#9w>75ѵW.8}=?:JYHΧ]:ˡ=Rҵ 8$I 9CV.={J> #aM=p/$2P\VRޙ@cN2j:E80T9{:;}0#7U|f2ˡˡCFN॓KG ٧Z>Fp" B) DV*ʢ< ;0D\:UY&Bf1>?P>EYFǀVI!CscGZAe2GS?Y'AwZұ(i)D}paPoDs'1Dүq;JF ( t:ltOK@"^"82c~2/` 8=WP@3$$251{& )@h(_ sd>S+ɏ3'&,5ɸPf\~G6P|)TzdKćWzL"cIbn23>F;2C529Y\,,v Z-.,^H?T.'HBo'@}[[D" 4lfCXlf6&&kY5uVݘڻT*b?}~pp3ێEA}iqa{=A2?ѲYބ=.׻2.ke\r,f$Bgeq.29ne͚\}.VRZ[5y]3l #F܅da1AYQ 9!jRWӆwP(D"NFWH( RR!`"zry_BTՄD*zT j pԨCjD4 z\76 :Ōo4,מzlk\ KXNduE.ѱQ.Kq@¾/ Av([HM$"eMTWqϽ0NTṳn];{ޜ4}=}`EfHy{?WXBTr|tæe;G+.G޻-򕖖~s%Zlߜq-ΕJώ};k|ONNNlllTTԩO>=B"^ ~)Ng ]}.=yȹw]I+$$HRSSk׮ N]i-{@d~*f\;|no~.k=ۣ lÎ\ztKA?z*dļO i@}pc0aks3/#-9-%)/5Y1!0A*Lw[;s.+c z=19st`y q Ft b%C y4xe={7^CPRCH;_N>OTg\^u_[ieݗ!@GrfE߅'Ap'?0HN#%֮4Z,,lܬp!HcJ0.D;$7!l@$a~͛79 q\qֈyCC-C`$1T1N}AQu {.жۖ_7!ޯ;=7k:JJ( +p4=$Bw;0hy9pas 4 v%Xيq _l䀅Fäy{cAa#{bD;lim{nXa>8`dxS߀Nۣt~]mZM؈aRo0L zC|HCQjÏg~vocòf^X֠W6Ws cOEdQX3eK-}gO'Q9SѕY nIBXSPת+Q52/+2,l lzMgͦy0k m$? YWs/?:.>--%f\\~'vWsbgfC>L=@AFiմlru"%1FSѠ),|4?bF:&\i-U꟯."f1kRSSEYJ27N KKͺSӴg6,+=UUSK+@g 0؁:' ӫ9yvJeec֭̄Ⲓ6u|8TRXYq[782p˿phvfqh``BOO} žmi3qplбԤ'%r;w 333dYbjrrNv杂rEYBTVUUB5յ5((ր_\TVӲBER)ըj5쇺]V@X#'"--ͰDG#tw'@^moO룆1m-X-zz4h}L=C}?=︈BQ ´4V lT*Ε`$= J`oKz2`}EBYQAnn|ůo]_~{]SW}L\?FF^HNNI\N>$cbb`a_42$⻰ 7/'G%  ݤ~"9оK-(&_RRwوȇ67nOR(:D^B(ps1򜏏;E& Ov]p4HØ576s55|dB4=DAcCzjRZjR&O,KHLu"-<! r!rC,d„PBk ~N4L*F;ȋ*W9 G~GCȼ)B@J3S(;z?ەDE .D"(lJMGw GǓǏ&ɉIhO0|QA}VuzwJf00gfrs~|lhB7:bb7/(ji hU-E% e"n_r߽^q 1Ĭiǘde#/?='W'|x\oiTV|~1uٔgYmm=JKJܼ̌'xBAMH$ndR  j792iSL"Ir9BkJQ PZZf\Ta9MWo5@` C4=vmȰNQQtwvj{ ׯ_{O AeUUEEG_I$ȁuB!'DD$I 466[!!wڕ|7ݔ$pJJk8OxYEӹ78`ȇ^~0RRR҅'x%Ig=H~*G"buNE6e>%u>n߾͐C?C>. !ʺqFzz::8E.rQgerg G۽gWp' 9tKc kǢ"~#" ^Mȇhoo+IEȇ6ZMۯ GwWGGZ؉1P‚܂Ҿ&6!H/T8 G9F GQY.EM>69N&# %GCs4;-I2 L1inEz& =q5 B # #pα(^=P1ȇ(MAODG*^}::ܶVOc@-%ҀCCzALBN0rIhEawefBB-}?aqbb۟Y$ùpV ±IΦQ!ChBJ `f< C`whG}4N A;yxJl<~~PЧ>0٘}f2cNsfgZbrѴq!9mt@zgNLZ2K#C93  3;#yKze}qhy1O8rFssѸwu]=m kf#ߜd2LKQB.Sҿ0JQ\$ijF8NcuSSz4u`m؝j}A ~{{~ƫW~joA{fn_Y˂WU>-(s扡z.AH-L&<yP9h,.YF[UTVRSW[Ӎ #j/UoחKGZ:.Ag4[}s{ue .-\N;n^l /R 7R/Eߦf?LMYn,}s09t0xf&,cNn㇁,CљG9dsN77fK\N[H"Z>8;>(**T= "IX܄}E!oA+mn6 b^T0kmrP@IGYJeMMfY֪T6MʮÊfb`ncKwthZ݀F7؏z͛7KKKXa|||tt@>!]<gw!1#.9"] ,[!g>Em#KW^~䃾|Deqh/$n@d)6eN:f0giraX\,(bd_(bXKЙq^0NL2d^7{^aw^|)))|7I?%&&?=W|y8(Ξ|Op@`ίvciV`a`u0"x/Q>Wqqq.\fz[[Ṧi}RUZ; @ MMM- bi铻wH}wn|䫗 qsFA⎆GPeqD9F& v;qd +4L}$74T`F;2cƂ-1)2 s%T;sQJm~<;TU?:E]FnKn۲%"z8?$Yp2ІfИXctcs%X\uYmC5I!ɘoQR#9 Ɯbku%Ï9w-!޾}ݻf9' ~`>NA{  t*d7hkf,ٍ%3mZr1[ w\+G  @ң0yEփ0C<+fHiBd& ː2|?َ樋]0ċԇr(9^2!tBa _T0-|)})&<C>&Ã݀__O'v+FcqBWga |dO*C4 W߬Nշ<4ۣLړK FVml|exf2mnZ[3,ΏL/; pL1i4PߤjDtЌjhaAiYbœ72tTEW fğ/j433S#%nV7u?+-+=G,}0qaR܍֦qZ7'10Az}N4I2ҲaA3%VK:مŃ-o vHĵ2; į6WKUFpi~V7>3=U+ z-> 3/MOtzb';n]QAnQhIvƭ[§1PKsϰMrõԛciH3Κ~$UyemE۟(3'WRo%ߎUT@7m&#Gq\LdKO3#_Z_o0D-k&'NqAҪaYA`S^n^NȘDhH_w}n /hfFTC#2E_Z}nB33 K/ &|NDNEO)$ ,.}}ee?zt ?L\Z^&**ˈCO?IeiU3teUu` ¤DR桲cY"¿jICc mmm}lok~o=路/xчaOL6?==>P_WWWkk+~**PǏfhhb"_AQG ]wڽsמ<_R2S._K`\";w."Oԙv Q? |]<u$mǏs>%ߵkN> ]xLJJ|II~ 111If‚GG>vKqC=<{|cngcfgoigW {'lvصkoPߩS'AX:`N*eԯi@ $65R)rA}E)Ғ}pB !3+G 0O|MB}GUrḅvh BēP75jPP {<U%X> zX$mqj!@|hݚu`PuՆ4H6 y2y0rْG2qs؊Hp3 3ZpA#r~| !8Ys},tj,s.BXXABh/hlIwvohZpahGt渵`6f7g'qHLcscCÓ7DG_hi%<; rSb>o"i'b/; b;5 YmX(qx8#Z=ThB\ 8E2 ealCG:Ha~p+W*PS_oNNԂq\5>e^T45Jk$l3=+XL&?ԛcWMƛ?m+߆ߛ7oɴa5F[SN(fG''d+sJENZ\P~ElueYvjE)w67&壃})N:zlzr=ƘA s%) |IciY":FrEMCB R9n0k%Z3i5cF,!Q Kyoi2qsC=Q;3Y=ѣԖni=t2 S9_uTo:Nt95@  .aUDEe,uqXmѲ¢% I U@@fe_ž y욮{ik&uyr~ϸ`Rz) IyY?.^TEEޭz]&7hg |~&&Ņ BCL,-ή,S-pѢԒbYVLl93#Y"i-MΓ%=hlxN//L蚳KL4Hb2Nߌk@p" b4?13Чr1 (__P>GI:_ ٳ۱*CyڵV}zu$t_st^}CW6xwV=~Bj;b6<EBZdfKE2TdK32+%+R^UB %L簮]^*Iee yPV<{Vh P]lo5Tw<j__??sޫAH~ |~Sy{nݰa#O]]p~3DD>*##yϘX`کGC| w&ॻqFnu?ɛ/| ts]~=((ӟ/|$."~(bBl=O7w^dԕK9[അo8+{{]\/Yǵm;x|'/\;wի nwwE\^PBȇ]Ku8E` k%!C<Az zznW͟* " 9 ǚI<ʹ9F~wpI?'\g~-KZ':,~48l*j.vN+ccs<@7Р=r;-[ҝBɆ"hf2b3 ?u - IXن 3]V(l!Lû@xkǏ{9kn#H6w؏@"duefN3͙\S| 5. .7C5#rB;7Vg>9oWtv<~3>pcFPǥ73o<0vrc x hc}N\r.vr< ܃%3X@@MӾ^;ޮ:TJ_k^L}z̔atxBCW%I9uFFFVo||tqqCoʘnK긻* D JшQ7Mf ܏K&aZ99M5Sب~qa `|=<4 )`ٙ7]JUDTT'%?MIϩkh{($L~ s7CFu3ӣ[K}yZnț7m .T^,f'dK%'67uw:E b2ŧ"gyK 󆲲"yecA(5&.7M!K{T][_zD̎ h8؈ÙM75v?:V/).*DYY”(#1%%)=59'' /H\VZI%_LZ%+XjXm+e D1DV!jZJ`AmMussRԪ l[eJGGȇm{R>?@[[d+7K3\RNRLdyIQ89] )'>Q(x׬YswY9;e_ qm5b@ZeAh~<{"|. x$,Ξ/bHL1_/11fb䋈u,O_L{_B.^ ;3nތvrF\ }8@=[+o\[NJ˥X;vޖgtصS!'y_np Z`9D]}m]} =vQGB=-%1/>)w$]OKb>|M9&{~nxm9b&ZU 1 Co/"F1s?c"h}@ jENQ4Y6W{B_:ē]GfE@2L&CצB͚8Ѝr3h C]gOI@쇢C$ vV9(v%@D?HpE )ڸlf6r*:)v,DLÀԇGߨݻwXOlTN΂8?&cRJUX( qhXShF,Kb ("0@4KKo,C/tM+@eoKMRS5u瞾wP3ҋ p>Q8mn @}3a)wOzXROMξBc339{ɇދX=y{X3(@FIQ u݉1'8Lfx`)UL,HjJ7`wbԐ5ԉ}P[$èI8s#bB}멸3d%`fھsBӓDf`pMF*ZZQiJ9d5QGB+ztVlo+:0BZOml4#-|Li܊?zk3W0o.ZaUk&ƕ9 %Pݺ @6~9Ϫݶjnnh>|.j}򄷬Q5vbW LAf1:>RImC_mh{!/Ny~D$nis/&t0a[t`[Kz3Ox9sL𴣵a`۽n-4BA^jY4Я{ [UsՆt0/MpraWAaÛRٔbmoW~n]2t2[ 0&Өfٳ"N@5(mmi+-z&u~'~[ۤefh?E6^˵/OV;h}cLGYe>-љJ3vū> ՂaQ#ڬˋS*lʼb]c2ū wr[Zdmv~gss T__<*/++p+Wr=i5[: E§mRAcOgmWRT"b'-᯿Cxhh`t6FeL}X\εW޼y9ׯ_+t> ?݈XoշM>RQ74q/.{XVW6&&>:*җYU5oKr W] .웚@>L[ݻwylFNf>?>^pVqٜ{z~;께0!ȇ̬wWRR>[.C}7n\Fk`BGNK?ׯ/Ʊ#)Gv>6jO||S{8yΟ ܽsBBwڜdL1rƘI 6gφU_ooW!#{XXO潌O:rSIJ0) 7g? \G|x2.4p*Ngd~C`C%0@ӨشHg8SE!hH21Fo&_ "SF{?^4QГ8<EPSEC:8-ap>~(U q)A04loPm 1!v=$Ɔ9Þ11ߞixSq\dHB1GEGෟ5!: 5qQ{QYTkCxXأ=!yԊ}Ɔ(ep$ a'F"C*=4 C҆QQ{f&)0:#-hG Lj@n@%00"04Τ`h=NvpM;s6K)0y^À/LK>#myaS߿QVNL~c/Ơ>L1?zZT+Fn5aJD}=2i! 1 , v=n7j[ښaY|@$Q?}P}?~p^#67D}[[PF%IܪI?g_1L&p4zWu y}*gi^mͲj20?//SVQUTR}[guRBH+6Hj69,b٤jPkn׶?(mhj蓺VC0Ak(&qZZ^ǢzZ/>jZ*dfNMv4Uko <()WLztLGS0^V.,/4T>?(^Ubo{zjpvfv?b?0/Hr9utf>tt6>m _xWܻ396XS{F{CkUCo6Fiqyj{^#Ci fmܫ&͂9qؾԹ߄/|~o15cyg/v+_,^J{.@_URhG5C|UVrK9%b^G{[ EB)' :$b hX`5 "+2 X-]OC{4q&%YhE*Ea0Ap^@KB*@K B[ r/511dFdv}{ܭݗ,6677?T@}=ݝR٫@ x͇{PȨO@&=/ȇD"qGl8+zH,.M8\|vjz^ZbzfEEEb"#y}w[T(*,/~B6˧B#+ ): ߄m~s3_[+Z ?P f!.$'ԡRB !~Dt GH?:PYy:! NCzOuЍ|a2nOLL{nt~Wa8s!T^7.ȷ̒>*^g;j<ypG(Bʇ0zGvᓑߧ>l%Q,߮o<<{[}p߄\ SS 2aDH}Y|G ,gr#@7c0u9#=8ō`t|)"pcCKflJÊ=-YPe ۍu [lplba!ht' P~2)<SDdω+8`o~Ci}GL}={~Cq7>>6:224=575HeijQtߗH5Vl%#Sߢр$ollD'zu6hh6|lMF{|˼_4L,F:vfP?7XX_5?OK**M+}&dwX^Ӫ?Ԋiql^\6{JK+kmWnɛ\ky>m^j(F2a^[3A2FS6PTP(F3Ќ|pyeASjɕe=ZRȴ$Y~>vvfG+*H*@tL F]0<^[]d,D7.s2/Jn>T߸'/SYRUWQh2L=X MlQQ_h :5.'KJ񕂖ƹiFSuj-L| 1;jeYjxRpqI3;tk_pipuŰWknjU# \܉/f\HT,뵛D,6ۍ .'VSns gi+"Mz kB:(i&&FcH[RRUU[Q^QZz rsrsrs/o\}fFZEU.5j=Y=9x7;ZC]]ݽi(l ]=]l^>T߿v/G$Re۱*Ho~u.;Z|22 //^ňDTKOO?sLDQ':z@"<)pp%JmE*ZZq;v,::=c|'&|m7Q>@1-- {caG"¾ >ȍ:=5A'giaz2+‚ '& \Cm-'>ccca~죎kÖ#CnyrpI$NeyA~ԇ6/'3/'̩(=RS>Q;BIئ !*QDB+܇GBqp%qpP0BP %Td=tCJԁVT}ЏB̃qeFp s:Og;\F2aEtpݝl5>y8<8d2mya0zuZLfds,fhPZtnn}*+k č55-siymY#?ۆA0լG-Ή~qѦ/0sE%TNT28>꧄?/x- ˱Q]^QvEnf^@o[4jTV7t=#6Mkm7 ڍ(@2`$$H^yjuK Ҫ)_3JKC/dUӮuBZZϿr,[԰۹25oT>và, E%ֆqZ•sy~o^9xW{\W+ׯ֙2(5Uy|t^=h4=&rfM~g7excQ7\ ;2Av6-oW:cd3 Wg3:6Opu8[[UW(H$>X̼'bLT]%GJ}]]DRS-0Ү"@Lֶ6\pcc\.{hCSNM7}r~P?ySlll ΀27P^s#=|WUKY?ͥkgܺ(#*.\}f,nJJL>PHUZZZ\ܹç/:̘kYg94Ga/께 ww$4hS;Kf07w+;;;11Y, =vpǎm//1H|Ώ=a!~ɇ[?({Y̓O&EĜ?ϟKIIIMMSPtGr`&Q=Psyp N.{-,!w{Tqݙ#A$p(%NE iû!B?TB֣E@(/cTJW$V#8 C#r )nGAbCQ; BЎ$G9(-) Qu  {Α{@:!ȱfР/(@&YT xm4r d%sPjS};o;B^.@eZqw{4k_&'bRq@8u^{"{H- O=;v~dvo::, "xVxPCWn_(dHR u.Ftw a< vAdޡvtÔ!k(L]dIEL@vrZ웯6:zsQ`J֞Z?{olF^8+|~Y},d^A}}E{xPefCӔfMj\^gK.@|k6N ~>ך}b * K.l64ZiGMEI~PB -`חUMA3'(Bn̘PPV|DX#oTKEu2i1O7gyk,Jz8//-u^?n>9>(*(d]w˜}z#^Џ9Ó>PfHv9L+n Zy{\--*P + уjI3sRZ]\ʊ?-mJKX̍c/)L%@6O%@622 /%.3%f=$Xc-UYpumOcC{mmCm-XQZ**..*sybPJk!֖&ʦƆJI%]O;:RnBѭ,hYqopfL"|oƆ/?f42qh5 5\ Uba%#/_IcU𤴨DR"ds  m&H$| oN_.%ʭc)eI٬ˏds";2226 j|,Ì iׯ@} _zYYYL&C: j*K<ݣta MQPiJk m[EY$$,BΛ>4@;S]S3U]:u~9{臘[;DWWb{$.D rwi&ҕ>-[>_8q`>Ҹ"Lw}o4.b?<:d (lpB]0j! :4nI:qc]B`/(; acA7q;͕S; E,AȧL"s,!qtP3rҠ/?O.m;EnϪ>3y|9y~ Cf9x-Xoq F\N&F 3!\w2Bǃlvf>Iw :tkr Y& 8xlsS > Eݎi/XDy3E9G$z 3:` ؋fۂؒ$a1Y$.JH >A;5<@AH/.?~ $d7oVG5*rX!qԣZOV +uOu]͍@>m `FՄS^Gl&aeeemmsV fϬJi8mǥW^gܓӂm?Ͼd[]]~bmuea3ڬV蘦bf'  l:ozNH j$e_.(.wg-<gZ?B feE o\<ֶ~Y̺ 8! բx2 WP^[W\-{\YTRS,>1tz0"Wku[Bgf4fG̦KϬ-ź`i_bO䈯u*Uy‚BӌT.Nl~xKsyL3tfYYX"[ K+teyu_`fGc,İ&8(1H|jU)s;uU63jҠ~G;۷NZ7`C%i {NJΪw ښRU޼UW/~/k31ˋxSII~^Qf_8MqڭgOtnfθ 4)ߦN4\뮨쨯o Ź99YbqN»e5ՕuiDJ֢cMMDR(d-} ҦƇA'ݏ{uNzm+ϹUuÇpZF~^~iԴ+93y|@ T(X %%%9k\wJ4AwC}}!3%X%&&={~@Ԕ I'{|9@4%%\ܷEE.]:/}Sr(7mP-EG%&w|&͋'m?|oN;v 0QZv8tt?`F>q֞}#;3#ux/FzFzʁ!U^~=DPYP~,8*jD tE#/{DՋ gvs $7FP)Hr g wz21M_=:|p&0"aJ9l׃~sLc=rPX ^jJ$aa⊋j*#SC 8ȇ?qr7o4jJ1T}]n9ٯGi?J5Oku}=]-Z[ZA>t8|:nC}tSjG~/^*g4ՖTZkM%lNqy?~̰ꪝoq~nj>'ǬyԄj&5%h?c,lj-PEytOCrnT74ZL}2i ĚYg)LX ;⌫7%Uw˫挴1 VĨ qQ"[7&$$$` F}iiwu 8yo~ȋ_=c?\sW1?큯=v^t)99S=#IR .!)c®0eyź> 2ԉ ёԃ܄NAFz:IRG|c H:%Mj4I |$;"#C< ҃8q+: 0o'(I  `*@xx څPl{(1It<$`ch0\R2ң-D !tH@t4@! b9*4@0s ёo?:A`#b\qx %gŞ7331>>%HOrv'pp!~^ru.(="^2U3F4賓s:ܱ;+C=s.>o6LywEQ)/#*=b#/ O9>6R)c*F=藴ZxDŽ%b|AZt: f|g2X G^Y>%{`N#Өz7&+* 9hQ-3g1U,ց{Ųu6̒=edЮJb>39\ ѭ@hV`/W~3෉h5Ϩ'MIۢ?^vRCMh,'23O<̬n3?/.@/#/6ӜvM2|?vω[|&?uw9i(9w-&7N8~_ypdRRR>u0"<=Ƅ ~ro~}_STW? Vjj&UAa0웬 H(u A8X!** tulht NMWe^ǪL^zw}{y>$2d$Am%0q]Э=;Cx)i Cw8.u:(.0uGi*{4M4֔@X\kpl gbvDk3%RF[O 01: !u<72=-:lxBO9p =WcL$` C-uF>aދdWE9a4m+G`XM}XLa&J^~˿+3ԇ@!|L~;ITP$$n#L0?i߰>ޠs;=#<\z4x61B$b!\ jDѢR؞dz CKZ}K!~! E,/P&kα1o&M{xm?sx7lj3Ǟ(\} P|_Rr{CCJŰzfR!쐴B}T!~XH456+J>4 >el6ˈvfj1;mTK☯1Nw@}K72ڭsq|rfN 7n߼W̦ZZy ~&}ԷY5օ1V0UvFlRM.-95srL^t]V Өpa, FލqKs])<53VѬ4\ FA`#].8j/ ɪ+KKDյY@OgKD@Ҙrݼ pKm&neO9&f94M^-rKTVsNI۾=&S/Zewsf-y+KԔ4U\xub''pit A;0?"?y@1@l0*JZE=#ss-ԕ6=NHru鲛Vm jBYYȉm1M['6m:]d7go8Veg>5\\U$ݿ_[UuO [QQݞ.PUHV__߆acKscq_ӧeG# VGæSs۫W4ZL" _DŠڼ;q lAYyM꼂O}}釂21 N>8󙙙3&Lә.dd_Oިo||999 ߩS˗/_erss>ѓY9ř3_={orΆccǿM L>x #*)"9/yobA}##o;AA_f؟~tVxTCABCp~+77ĉ!YAǶ.b]ijOP]uzz5YG8xAsA=ZO:unЏ40%I@hKMDKK )I4DQ?^%L)úd 0hcnbR8C á<>Meg {D#Q&Ea0Mix˗ 'oF@۶da12a nv7lRv}b8pۡ}6>xx *T#*o]Ikc]*K$VA&i٦f s]:ͬӹ&o6sKWV}~w`AіƇMѵ6=b 0f̛m<fvPݦg4ɁyØ27zbҏNM ,y:Ȃ!UV?5Z?q,-**T tw1Bɥ+WoW^OV0߲iǵQa[tX&R1$MO4VݭU]SZ~==<.L"HyaZk'lV2ԭ?Դ͛ MMŠA{;UsO;U'14;7 VQ?osnìQҦW_ݯ(\p/:H<8&I4( (Q\@L# &QZ AAADPlvlhPE=>a_UC3gf=uԭ|2F܊B4`CmР,GSi熬FTlqݒY她/fEӍ6 >R%k`cf{> ^L2A Q=Pc?2; ѩځ=Q}u%EEȺtRz)3(0&]RjN%5 lƦKK!ϗ!~gK;!o>yp,껞S]]Yh%~+.ƮX&{L{(Jz{⫗d$^!Ϥ ^W~/:qҖ!GO=s他*MHLN8!pyF>5 ٳ'oV0]7<!>>IQZz'wСP/>>KI9챟"݀_āQ{=ѸǏbm;\W =mliWA/c0 ~uXaA̗VP߲ؽ. N:L*Hj$$DWKoy hm%,׳] ߧlٲĺŒc k|Dy `1}J0@_B_7 ;NA|!]P :wAхogK;\pENb./ꛙwI}Z֖S uAX>lEG<(xVRUY&T3A]]uJKoZ^G63QNaofCRǕLJrr *t# ar~8̌ut^ɝ_32/\ͺzM8=jvhS33FgT7~O;5S =R+;[& #IAAѳquSKN!ojk4~2lӏitZUc4K,.*l$cGOTJ45ƃa?qJًqFT&& ¬yiٕ*iܼ#AVLժ@h4j5J MS1ͨ!pbRČ+Ţ۲'$1ÄnhrhVuzw4ˮrRϧ5j^w{cyqyvZR1Ө0s΄qrK UG{>@RqtdD캟|t݆?hIKɉ,oȌSzk@1NDaBz73ٙѮΗ):}i7Wad{LtڨGIFuCrիO4ŢM7~JJ--,NhUOgb6M>1EwgG}- 3_>`hٶFl233 VS-wP_ݺ ή. cVy۷oI}!dmkа#"q8=?= %5+13b0B$J:w|TPP?*۵km[>p !(\pfշuN}Q\|gϞ* !`6<"s̑#bc#~ 6읭w~o`ӉcII߅|h__q:ظhk'ب?/*8xfxv+111x*1H}Rf?D>vHduՒkTwԔK5yؾ 1ob|l|lӪ4!fЛ2 7S&u)@Ѧ$Cuv(a rA}N'< q3zYr1:A 6.+[ڸ Kc?85vqJ]n^!hE>`ꃅbX(4>{9Z>eaY;sNW-=J w)f]SoЈEچ~9Yr[3@`(\[DMkkQ~ᄺ}nFQbp>O`[r*iRΎQ0Fl 0rc00-II;?bFlU;I6*5x< 쓈rYK4XGqǓ'OY}Ϟ=X,CCC >\$P_Zx%ܼz5W<,;2 ^&x&[EEMȗvW_ݳ@>6a^VP&UT>x|[`*|l6;njv6uϜKM̌Oo\<+;Oɹ eddg/77I>g_|qXw̺qS'O^xG2^~.]ܲc?;w0. :##"܃7Sw|D a"GrCrAҁqxxMuLLrtP!Q"m@C#>$#=q ) H&) 3„?u4.P&"ҌyHo}+nj<b-{wOZ3?[PO`jy;QA<1Ë;HYwbd$ 4 ]X\0À<1BvCw(383cRi=dTcR`ca3KK,$K#2j?zdu,&ڻ3~)*ẏO']8 %b‚|>M;`՘ZUkS| BתGT:hЫ{7 uu~ fgg>t:^ (fZ88àVG#a.U;+:z(3V~qcS\,z#EgK/%#q!QU' P>i6,.ݮEwbU|6:pFe"_H&&1Z|^6!_3YԛS:Y*'OO$]tҥ"D>SШ.^gCa@KOOGe}ofwvN֏gwpټ7oCe`W-v|s0=>>.>>qqq '3qIdL}hihM*ɛ-x7g^ϺA%_ߞ:Г)I>G8"2X.g8J0ؗ2KGa 5FAaه<(uۃ<{L%>~ԗv! yKt`VްXX", oλAÉ|v&@zosac# ,aLAcD8m@;Tn1orT{@fl5!~q3 ;Z [aai0%PAG sv!\0Uz"HX;v?aV 桅 P+v'^zI}@8u؇kh#]0N>!1#ks"r!ðC4F t^Y6|o޼jT/z=ϻm]ۺ(z}}/{PA4*O HSTԇ_ښꁁ1DF!栾f Cs3jՓcjFAw49 =IP[KJYeNQeys([=1@G#c`}59iGU] zpo~ EwCEy~UG}8fQŴ6yJ58U:Vp1 M3N῀g\8mQQZQ'¼[;LeP҂2=qxF6L./0mTߌNPbX$Kwŗ#g^j ^? kYZ2gu#iZa8Fz/v~qQ㝔:Mןb%'\j_}m~Z}!1!咙'3V(z2s;»;F=NF=ko)//YXPS],m~Ԁ)ɟv GF"c޾}kL&Cp >"a$,-+0_OθsW|E ӒbbbB :.<uՀI9n$'/**^'F^'_dd$ J|Yl|?%;w\aCBByX y8<zؘ?2EۦB}vuylKx|Tĉݞ/lMm:,찇뱈`;+'[[f !!>11̙3QQQH$ƽV9m|T>kEim%Z%L}Vp17's\ͺz!9\`Z.<} V(afBpr f.:C?)mthn s1ﵗn8 !9D0أSmvp !Z͋·Dj{Qk""zjN"ފ1guŝ=zw 8d9:_ Gaυ>'w&f)([wDE J-ؤEo*1*F–K8ػg耵Nkˡ={h (ˡΙw"-(( *'\zdzۉݩ`kGPTL}ϕʎg}/:m͒ǒ'2JISGkk:GGGӳFQF^Q.LfboϪtU#\ mgUOݽSPR@|g4 4|%RҼA?3_3;NYk\#Tt,VjuPD@-\-b\B$o!!B$+pGY9?}݋̞y<|^2*6&,w{߷c<|>?}G#))iii[ JT}_':v n($ x +j3)>?AP/*=>(=Ѵ= CDr1 Hs J[M0;N!sJpHt8FhQyQOxz1c"3-4<#(p N99Br<=@h၇-8{ :,E`P@;T)9$=ОX/ HxQ({v|;V"1PP~TJ~W+_Pߛ7o޾}QGo{_<]QR 3GOHgI_G̫'+@(ڞyݘ-A8ܙI.` Z'#C ,ļ=M SY`F%Y bIˑu'@OtIu~GG 8ځ@lDzgŞn~h*:P!WbB A२+B/ !gqkHK==#džЮObb|dx_+h:2kkimix4 HB}l6C}Pʨs:ؒ:񐙾5Q2jgtklۯV\A֞ ys9NuuIoX6L%:Rʠ> ]uѠ 0鴩C=/*mƑNvn~%AچAie0f6#gMgZ\[SkB=*mm [_֔hLt {zusŽ =q3`f5T2vvA>Otx%&Z{~]$4dY\H$`s4s{9E\^fn+(.,+0uVm z k2S%<36͍!b*+5,vzR@dVSVvh'@¼b3.LFaB7 mmu5 *Vxnm])Tn-ln~L6WV\Ic4(1,ry{GeL dKa} DelccCT.6f^hOƨ \ۋz!DC>%Q_S#>N)/=#cN|BbZ nHP4ixƀ|/~Ԩcne|{$bS}|_a=㼋|׮]I/##oef>LIOL+`_GoO`g߸yV•w$eff0?`kW| HI>959^jhDxbRb2_OM&yݚکrwtA΄p!\Z:2ZS " 7QTF$ #Ą\r@f7owuwjvlS]O~xqoo~/>۞myy 9sxlaa!j~?bˎ}}{sQQP Jjʫkʲʊ}s:䦬$נNC"c 1.y4͡CxpIdNVR1$KGBhl0z&GlsRɶ)D}{;vG!H$AK,HDlΪq~,у̎Ļ}# 8 bѓ8X\p| pl!!q @&rodV=V -nyR=u\:r("Rh4rQ;8[7"ƥOoHvnM\= ,F9-TdceJ#oAmO>~\o|}ḧ́q}GGHJ2Gy`!v%Ǥv!eN=tK);ZF;d&fĻwLYdPUՂmV"Ƽ1H ՃFʈ@ !|2|G XNtnv¿MrfZ.SMJG/ePؠl|X>9>=%2O HcC(>qOw/A!Ï{G^8,@:!б(Jo-i߸7(}d`HVA~$G:6q8\.fv޼q,:FkղU7;S*Fd_m6b^-(ZX&ZXZrX!j])VK&f٨[ *ϠW FNNZ,t h KT,?xznNpecQ7p؜yJ6aBP`iW=.TXyW66ȿo~: Ȑ]+V`XN;V &gPJGN=7=xT{_zָ-T^{`Ӷza7i,,xyIRŭ1`has7 懒uBbݻ.: Uj֋ͺZ h{1XwN:77|_ZV{ulnV)fiŰMg0|?70^٭~F57U JޖG&,օkzaӨZJ咋nݹp:D&^sllQniih_\z+X?zxɓ'3ڛ= 2O]>ϭQ߁V7;Lĉ _AAOɇs9w3&{rc!! `^Kڟz\t?QXx;~8s>O|vEIJlޟ@SN?D.!7`޾=U׫+WWVc$1Y3$tRah01:--K C$OB(2qbv2U=dBp@\4H L $v5&Y bZMW y~ȶ`!BX9:$tzQK[KVL$ @G>PY4 -Q0Qݑ1\Mz`@=vH6ПC#(h@C)^p/ċG~8GPzGAb7vO3{[!;^x}Tv@b<5hP2[;bb Ž^G /Dz x|8pMw,!sOs foggQhϟ |h٧ aJ<M yU"Sʼ: ^hܵn(s!( pj֨VV@:֦~~efEZVVtL՞Ak݇jj25c~7-AB]A6Z^ri7ָc=_>z]655359AWl.>yۅ. >Bx.K&&EbY*[ɱZݛ7o>"߻wﰮ% utt9ܶ xmZ̫IJJdGcc#C DYؠ )3މV}HOԒG +WƦ|J>`!_Ak7{=;*tg[KWo &]CVVznnn||>+7GW0Hjj*CPprrR, = q { ]b@М߂\cB!d{rPƋw2TϘP"tbB$F\v$#IEG֑!sC}@oRAK/D> |HL Mj'7՝c0 !_/id@>"B?*@ @(]:(R@-^ζ**~QƁ{_ 9;nx)rs9y}2pd_Pׯ/!? <|c.!rs1->2v| =l t~Twq c~DLO{u}C)Ƀ&!&*^=A0.t&5L`f.ģv$Î6ú8 An5E=s9MV_ HJp¶G!=QxKPiꋍP::0g4HǮ|geCNIg$3qB)O26=9.%E23-xP5 #Zr9ԷR0bZJ)_]GF윑#bf,+UPqWf\mov/M3Aku9Ťwa >NG,|P/~ ꫪ9-OO8<.b68(uwO q¢\<=۷al|_kZ<ڸCڬЂ"3x0o Cx#nTzk[E ?ƄE$%lL%V$m( Ѓxߦ>>??_ݰ9 R2ܥOJȼj|6:Njia%i%VTbfH94vp"LA 14sH!#'I$I9X !tMDQ\mIo# Bx{d>V TG;8E4뎔vORr&uWܟ~H&tw|_*?>ם#;w$`aAn9ʼOe_8}[f赊W^|ީqNL4c(Qh5JVm4OtFv|\)CÃ2rxxwwɤ)~wz{DE;`gZ_`kp]T}oydЏk} >69fnmzD)/˞rvOX^ZmoQH' x!KF[3XyFc]^,̛T)*6eៗ=N49j= 4zzz{D|}}2 P/#CHS#^=ow?9Ɍ㐽iWv`L7:2~U+ 0e ,4*bH.U)K"IO~ZG+~dD^ "` SS#ΉN]1o^RE< 甔Y\_r~D}>w(FpnoT flisح{uٗ&ۛ[8m^-Ս=g&? fw4[?pxuu Ž䲚et/+ z{٣U)k uǼAѳRNOOo:}`NR\ci`Zi9ဋy4YTꊒ+cgE%܆Vi3op a?H/w9s`u[N  u^J n`7m+jOܦQdDV pc.qO uYp@.NO:do ox~ \vSSY`zafDY!a l H0: . l"$$Ĉ"{Pv$$@[*gypꙪꇹu8ur\=7ϙ]0oyW̸.@Ea02KҶGRZ[7]?tҺf452k^)8 bqeՌ c[Z[Qd_ΘxS٩)*S6ܑgVvHU ߾M-fRd+ƿv ScOueeZ4VU57ա 4:4Z#GdqȄgHOȤa_Ϗ8/NS^mmum]MCC}&m>vr򕔔@JQQRcҘ3A/.-#_RցS#_97+|EEE/Ƃ|ȇNW/S1caE^B/oD`>g?`,>>;{6>--ԩS EP\p^(HO9.Qr׻l'ٺv{;좏D|ΝPqgΜ@>MivRا{*5Oj4mqQauEuT/a4 lcsI]yiɋzlEC eaYp5ĥxDAB>u!n?y.$P’3XO>t@CiyщDGQpe=c)/!bCɌG8t#1r6iy's9Z8]ASÃ+х0@2>@DB`g#Dq'rd,tCpc1޽{Q/Lh*bbOS$e>)EHód;z-A>{trK}#a!G~P|hц&-` 6^"+oU:D܁_ӖP{$J;(GdLgx 87:dȡĐ ŨY7^\lh6EƅK@f92Ot:oIW_q`~@ϳӨH}OݦFhg]&'' dL&#ԷETxjnT=oj\Z˯.]Zn׵k7WE/`%\m6"w/v?R-ͦQӜ~d8;diуuø4Ys*8_Y^q|j<n?̿y? toj]CZvVyEW EVn\QU[dtǃ?f4G3NKaO"^e킻 2"0fŗ֥U$c[3s2겹ٰ87 WiWu; 54EZqfxaa`& 3uuѲ0 a.2ő0oݏTϟm.,({5v5߻y5C?g6*+WMs! z\ZP8D{R]w3; ڍ,eF_r}%.;l2ADqXYl^˨Ps\} ƱWRFkqZEâq!e73r ,сGã/2]NIRWݬ_:}(7EXMI[K}1YWq/ @i_'܂ax|lD{XZRrؓT߯X54ԶJCFЇhy~||kjbkwH#| ޭ,((@>H;Й'LRˡ |7>!Yy%|>!|SS/'&;v(#HvROP4Jw8p@($<@+ EO={DZ||N4`9='1WQ⢯e[Jg7c`@ʹD?r1,Gv8ۀ:ю?/N> C=Ν>!m  -zy"Ɓړ!ěT/@N хe{́b w o@A;9>J& `~ݶX>ܳm9G̽^B[ҝ+]L6G;@@><+Fq<>u-NzĻG)eks`G@>lsk m1Q@;F;vC( ډ=/TA/8/ЙN_^VpEDݫVo[{eѫVQpYIBV@!!}Y޾E9鋶g~inZ?]Sg7B}fL9>WbDITxȥ߈B*|T6h ՗f]]F9K.Z!lF<}R3;رT! x,_N2rN[>3N}e%-Ytׂi]ktSn#_vX^I-L H?׽ƿh,Fo6kժ\՚1`,M p AMHSR.ĨZ xfGL $SAr2I p0&cu #VS ~L L!M6l!z ĥA'c+%TI*Qgww4u>jX)C  ,p~b<{]qږSoRS#u"4*I@@ vn]9G*f #!g(X#(@z)5txfw1ѤLm38 Q޼<{Ĝ ށO6Wv-Y19vǤ4 i^i15P*ofӾk7~|ҒM"+ُżl&}@A..clKO*<_< |\.Q*JR iơbhZ|]e;Ebt% V \(|.n|8AvO>Bd2oݺU[[{ĉ#'~[vʡ _CU탣kL}Քjoy/&O@9ҹN}S7oal-*؁3GΔWSYZSj%]{J5k/ݽ{ҥKW^Ł`v_Q{F-[r6l?~hsAoՃv.\٠ÜssZ5ƦB )#829L)Tr)aB%r&Ɔ{wT_/lCJBnB9z t{m*됰H Jpr$R0H Bݡ=y ȪTœ[JzRA;}Q@Ƭ}A]h!4"#C (m B-o~,H#z ڲK(xmu>jqq hm,.}57jGo eʨ;w1W n'c; +Goy%[?,̀c `}XUT뢊XSXJp%vUAHoXӇg00g+vUF; qrrFMe!xJ<_EH%p At![$zy̪' zinfd0gkϡC;:V@}JL4" 4"oIpH$F$>1b.ó:#KyddbXҪN~P_*@8~E&E\a1}HȾ0!d"%1ˎy6zIޫa op\65\M$> #)?oyZtD5m_r9CR/rX/9xynևt1R3vʄTrt{uޱɓy*uݦ;R1cfJ 0E^V&'`ӱEsfnPרTDX^d"K-%VBT3+8ʨ\呂5VjO:g4#C`8H/|v Xʤ⑰ƵDnI پ '5JuX/C!wS~~NKg2?%< *𶦾Zn7=}h:ZZzYpn(VʼL:([|o^' 7o!tL.LEm>yv :!`>~ MЈ쳳c>&U,r^8@ Ifl\&MMٜV͉74fwDyӧ48mK[ YhPEǽvATV& aQdB @B $槩shWW::^{ܧ=Ri[[;==::46x }45 _QQQrJj8#4s'.J ?!ߙk/{uJ> |˃@ʏwM|;qWB|EqtQQIdX%FCelHZ /6O xLSy%%E+WfMz y\<;nwpǝ;} CqOC8q";;O>VJ510008SL.J=^Oq_tp)p.-Aw~ԍ%%}QX.C!~Q.`5b)?z" 3 ü%̇q l(Ɇyq1Jt_'2r, !q sb(td1є ~B2dU=I]x|E^("! XȮB,i0#^ r qC(Hc=n=v}<2P Гۖ.f#a]޽{xPLqTjRցD}xzaCx14a r0˨{4јzD{"VS1yXfܠ8W$FWG/a $7Uُm {Kb/A_PD$:^V %=xˈ  7r؉_vRp6P%q ~r@F,qvɑ>A.9$.#q;!%l0/%XZaډ!7 47bJ=>gQAO3U/F ) *.yOW_\I+{e>uzZϩoii Kh}}fpұQMg'Ȩ 7]&과24heֺ6 B}(b4n[fFImݺr j򞖺oxj^fRLFf߂aΤ1/,/-XldM~8Zee^EGCѾ~y㝇ɞ>ǦeuegAY(˸1:z+ݻ~ӵUJ57ԇBв[kf4 !X]}eҍ~{L-{2.]ݩQo7 ~Nȷl݀ 2iXwtLS{NZ{v][F֖Mx|MFexᴅ)G5wM𰱥FŃV,g5VWց̙WFSNQNʼa;zYg~閧j=4Re?;g4|``dl7<:R瀺b]~]||+,>~,@pTPXZ0K $D$F'GfK"2Q$`qxHR?.,77̟\T m8%:$7<ڿ޳g vŮ;wDC|… 'O`ʾ>*xꫪ|(Y 7MKaC"F_*ac9I!1JtQHW:ш v Ő<`H S%(rtډ81y/ h0._Go7Hz| :ׅn)Nz8 hxu ;]E w yo[zY"p/6ɐd!)r/ю繇bWEN}ؘRwR):`z&^S N3 ~1fBUT^>C}OJYB5Џɠ> of3jokkX]A{cVזSfu֤{>trr۬Pt"Y;%¬^;2tCA?.Ϋ̵w:I{=5q;jF۵]Aٷ("8. 4cѶed}MHIX"ka3@V~Y}e@t3UsԩswNB8~32>Y} 0xi\;(h$MYr>.,.-yR`K?ij0f6NbPa>͘~Ͽj5* 1Էш>~Xŷ5 jG@e%-7B.RU_Ff 5>>}P K>D"V}|ꫭ-z ubO%ftrNlB|_w{COW^GlU_QQK'O?gIz%儀p.Dž\JMDy).2txRlĹ΅(۷3 ؔ #cBD\\,!KRyLu)* A>'8K]pދ8 ҃*O*bL4<;~-$9΍@}OGIEYH~>M@"||ퟅt{G;FoO3 < i7aPGNX^M-v{ Eciaȏ9'Bѓq]Al 4(*>b!B*^!Dc[Ghdzc=8GJQ~!/N9Cl{$ 0 7d|GN."~>;s=  c~@;0/ 9X!?LеĠ''b <>b@xqSKCG/QЄnX;2>ntpltp|lmtd`dh@|j%D} X! 5e(#m¾ޞ9T ٌ-vC`c/ڄtNQ/:-s݊1wA}^seY[[q>cqdk2h4Ύ fC}zZ7^Xoq=4#m(m4 :u^?f1MR9rNL?h'yeeuenI%o|Ntǐdցl)c\ ejV5# ishXXU]98+W*ӹ A>e8Ânu@ nqOmf^ue놬|FUg%|aun+cq3v#nWWhӴZ[QEsG wVū5nJT19oa'BkKcbt,ԓbMS/cwo[5ue&Nwm&٨w9LN<?W֘!aU}mem:X-{s:Xjһ]fxl.L uN&@}"M:;D}Ju__FSQ0;7˂`tqJ%KD4BaP{PED/6c_w)-Nش\BLH} _㢒H줤Ml[@r#+ZƝ۷HJr/:9>J\qQ)NE\Ϲx*q0DًA̫q\/  8埚zqs·o;Cŝ {/JwNOOyk*S)e2䓒)!M*=P!'(쒈%>{^I;I\nRgQHdǂW % 1u(`'h}?Ij1a!V# v D~؏޿< ؊1,%(ʪҭH3zƞnqB㈈lBC , $DhvȾ$3õe9Ǚsf uTM]SDžk9vlJf$tau3I{H~Og3pxH@S%uĹ4!G@xɌK d]ĹPxh bp"xG#H@& /@ܑH}9_۷޽ԇ'?'{  "fPHt6S9p'~~H 9XT iyd`bQ!Xz,G@/?3D"[x9e H(W숔qg%8' pސx4BÑ|s| y>> "rY59DJ1 ²”KgSHDKE)t(ԫ/IA|N}s9g5 KPaQ &i OcW 1ͨF5>\=,Q*=Z rOQa~n8áÂ~aqϖϹvw[}ݒM2~>M =.a+RB}^eCanPƊ~R9:>:96 TBBAbƾes㶂亠ײ~'ăl~!NϛMPWV0жf*[^lk]ݳǧgڞTVomyCUfT5+\>h%m5s r[kjkV_߯Q h$Dm1nw7 Q tkmώIrf4-ښU7kӢzhzjtj|b;{GAjܮ x5-p612qF\: nl{~v܆K4{ќ FNZw_"4lÚg Ӛj|\T DV鴯,8 !گ4yaѽ; G ΍-Ɔ,7ovfaqc'&d?'~#|n\[RL*t @} L.W)_2M̔V]Y]1Lxy-Vl1[,x8 ok:K{==}R)ׇ>=] e)ȿZUyռWߵ͢ɇi_^RRqm==uek.}/.qEş/J/ԩre٥ٟ K %y}TI?/#^2oTU݁3!*x훂c _fnjn~vbRl"/6.1R/Ȋ%;wq x 0>:XyTaR߰jE4PRP )55@>jhs9<a簇Մ=xFyf]щ4q3إDh0]*ȌˡhqP(-r0  ,VKcC1&@M$FRiO!r]:_Th̀JU@tHxl`r "q`:ЎL I"I)$:LQA2l^όgCNr°w )r;ĘGIy,@%R"}@S}=ӃJs# "rXd r($ywd&<  P{<tɄ/B,QBŀ4qaJt.Oba?,SJcIg,QЊeB!-]:qeq  F1rG$QtN9c'0$syryp]ilL* dtA;1v߁gp(A8+>”3O>^>âNiZovqaְZꛚ(UT+2rON}##CrH![__V{8ysmM>gstmҲ,e:l3S~Dm/ = ɟۀ۹ Lp9 ^Ϻݲj\Nm0-d/5qmaiJ-)V{$6„gf\]1.K$]RUSѓ/.Ǧi[ 2!&!1>q+kFO"m)nڟ|_񰧩-, all٭`ea6HZwn>ljMiduK՝yY7 yprA iMEU,c@GsTFZlVSd2D;Uy0ܯEnKtAq[{sӺ.z<:j*K hZ[ ,ZmTF@ieKزivD a NoHO0s/ЎXSS֭;޻ߺ}s|jl ËK[Cy鋱~Ȣ,ohhάX&%g@+ G.YS/VmLzE# Vڜ_4n%$lCmK MSS~njre2;V֝}]{ Vl׌v0:mZ|S[Y3<~VTͽ{@+U1ժTGFֶ~Әi||3}xm߮}K[|nvttu*euEaY҇>2_ՆʋKJ^ C>nHV(RϝJN>#bA}QQ HDR"Q2x22ȐaR{)xhBBtRb<1VH E|_^"[(vI |āťӧOoC}y|]3 `O? ר{ ZCFרk23"#ԧH׋Pr!) LL)PBHCD!ۢ#>^ez΁>TO/\I<$0Dx=PG=4J?\' wx#hi =B;A<@Q{?\%~ " v{< \}< 7ǝ .7M踃Ɯ17%wKLd?X L}tj pGE{AB;Ie,--TzrW fWU(ǧ>~߫eqqovzmڢ )`qrwb|xfzdLpX9:+5 vޭ{E7oLQ2<ُwc|򨳷$zNZvAUuƯ̔yw]TMOɿ-2 -Xm١nu#Uaiѭ{55i=}3f1^4m2rpsf5hVR\~2RK4j޵KcfhjyaқDzSc2^КilYYVx9¢k99:ck}*' )W~~s65Iޖ熆W+ /]R\,;ʵ猽CHc[5{i~W^ڌ-} ?Ζʵ/3m4ybmnqtCnaWo2<2v_uL[{VTS #)Uʕ.\szɢ}%.B'|껐u %%%EDDƂ||8}M.?w8vb\oO IőA'B= wqHmbgL̑8tvddd9,J%Jb@$u9m ȗ[Y'?>=MCk7juNk0huzAנu:WW.+/P{?EZZjҹ3R\'vF a`_G5gRcIF!E#1xP92rﱒJC%EWp~ *TO} 9zzPP͉{{?PZq ⪈ Cx#'0TĀ9t*(F\N;ә-^Aw'qi>:/ Dө';twf`<]v;}Ds;88qt'dHz@, 篶9١J}Qߛ7o޽{?Q GÝ${c6M;6PbU OG\bBgSOd!E# 2l(H?ժܴe΢uӔu 8gZ ^a:д6>dmqc5ImT*D E Cn4?Ǽur 3qXWޢ7T6Q&RiUQ&B Ax ls̯/&s;Z[56:¯ >nik&e4L ʞ;ܜK/3.[^Z| :֨V݀5CAw,I/&p3n a{Gc]O͑c(rfqF|ߖ"b2Lk_R>.nn6YEXVѤ7NO.E?oDX '[LAY_k%VIH#;Dҡ!%>un\7f@>|%w{/)UbI!jutڡ,PnRva3 ̺xn|k_%͂5WTT>|hܸqcM!VZw߮pyj9ҥGٛء39s2*5AM>ZH0Gԧ)RLA}CƑHzt:-gZ0@qGQ~`kG#a(٩8-ZSun`෦"p,Eb1MOj)7myfM )bƱpȿ7uvn3ǜb 8yY-H<1< jZd*Am9auEuݜybL9F0-}`#ع д;gh,/kmkܻojU7;+5diF ? Fmض܌KVU QiM)qiyU%O l}\Zϫn #?IϼntڍX祕J i.wUݸ}SeޤW QGO]^" p>;0aMwG#2mT=^jL=U?ȿeyшbe , p.qXZZ%fEF"s}bATHvH%WXco0P^^ z1@}藴 H%]}RW"*jodtT=F8Ym6j9򋗿o߿{7?zWolvf俴WY{<"{ !v` ;DDmAE@fFQEdĐ=,"E4a 9W8˱/77 񪪪>Ci'NpaR_^̔ЈmH.<].Ld$DFF762Z> <T;v0/7#EEWXX'˶ "`@W;P⛶wz ͻɗs&%H(GTb3|ZAo0k0ȇVA>K\}dpڢQy T(~Q J4 q/U4lq2% J&@ 9<ʤsuF(DAB'o(֣ăp`;hH yз@A̯~I|>~߅4q0oxĞx xCoűGAP`40h{){ a+&@.:.!kN+&qT9"!ȷ[F>T0yZz8u9̈3 ^r#N`# @K t Q_ ]X_<:# Շ62=qpdp`xQolvfZQfd1uj.sArq|N?:j]=]ͺ-oܬvj^-~~Tߪs>t.C}o޼'&',ZGGqK?<39i);:Hz}:ޘ0W_}̸D7}6>u>3\ZrNݹG~}ѫu5Wk˯0ό-.١ [s҂X>#Ź lkW OW/0`P_xYдupA y_zj A^'Z[[aqJlkmjUujl6z ꛚF{ wo߾?6wnsCCSKV_ScBvH"Rb K⎔ _u|999 ߁@3g|N>0 [>0… i _~~csseg EKȈΈK&~Qi /.,].=,ߟ+޻b#R@c~~\}АW;`pDtAtP&q@ݩSIIE q E9G NPd4t]]fѬ՛t=d  D>VSwB}竪xlqmbehT%A4;]\ F "-*AJk>RPMP_>1Lzr܄g3AVquD5 C{8}oJrI`7@xO@ mF8@D D C/7y.섮kRci!Q7p"_XH {wCC9?oE:|2ߖo0޿Çpd0?UiC  g/IQLt,т -qd>HQD5*ȋA0>08l×04 6DZaR$FT"EoM d!+b~$!4!4İ_t+98HwY\`61e&Jd<<]h7qVrPV5ξx2e%I~q;W}qn2xOr >G'c! =t]Ne֛:KPMCj4 *f[ς\@ 6MZ MY[{zmյm2\p?s\L}+v[^,6 `k: [zm/-g޿8)uݲ2/i]Jtf646c$G5 nSW;߽}ӳHhl~0 >*9/NJGTseOrY͝ECyvItVۏ%T3kM4RðuJkt6q޿ҘwwYlX;n0o^s.٭U*ÏA8h*-͎ʢ&P:@/ GL;QNWvU4nuՍ *6(^$҂6V"k&8}fg'uw32@tZJ9){׻7ޞA`@$ zzF A}&V;y?D"=>{~a? 8\nu!-;6 !Wnϻ`1>Lz|DZ\䘌kWғQK|+P^~yLLcII9w s~{QeeeaPR٤Ž]x:5*M>edd`XTXy41c1kr$3:#DQsJG8p.@IAN@X%$qNଁ:6$PM1 <&8 Œ? Q"P9Epm/ wjmȐX12DB:YTI"uS;St ]Gm&q]Ab9/w?\f ?L̹9/fߌWIiaCZ2h)͠\U">͂&AZ̫*T2,~KG>&q ~c##Cbop@fžt>HZ:i&ډިOyCkoՇoX_?xkPkfztաq9V}e~IĽZs_p[u҉I vVtIγTIKc7544-ʇs#g###St ,vۮ.Q{inB.W< m?CZu-qۀU֛7 w4VEX3"c &n?Z0p7Dn$(y\V\rq}bU+lWIMoK+0 EuUn>n+ RdTæ`?j;PQRͪKZ74`3H}UTLo9,A2msqզ`{>(HHkfeqɠqA_l2zӧA u&Kc յl!(ܢ*KUʯ^ߐ///"2n^[^(*yZ1繹##oGܼ|~VT=6+ҽiqiyw2R"Sbτt'K^<INNF᫋ϢO8z# {ȗr/̲)eY}^aa?x,ʌ**^HiUC@A$#) iHCex:Q .:VQsQ[XÚT9# ƴx^оpbb(%6|3K#W@ 1aLB)  a/F`O(߮d@C73m DI#ȱ`SN:@5̇\8ෝb 7FoCoأC\mS o$d~{:t˜Iw[x2 98Ċc=H}َ F>6 bl b=?#ؓo ){waCzx9_a𪲾u {q$:PJ& #h-i0 y xGL#E|F S lY3EK = #0X-I/ lC={I DtPdsG~|=8 Okެ|1=5iЪ 0vlB=<>:8V1M4F8͘ ]J[+!L uAA?=="277p8΅sFgF55Z qPτ~0w뀓NMv-1q:-ym3~V}s-)+{XysP bBF3۬FׁL䈦qmCU}n]XH,0Oy2YPs(faoP!=\]-˶"Udz˿U4Ȟ=yXWXY'i%<$/z@/.Y0ۂüs1y?W60 |P-K$7/_r}zd6}N8?|7 f{6,Ge6nE&,NpeV[Rlnb>Z8fery␳w\QRV ɟT k/Nz\ _nyVUoy\pȚ*j1(iihn-F*ںϕ&v_3ʋE{3j/W 7ŊoOf_QT7+x鵵k=VI((h%RԧQ*TajhP50<3a[z[{rWd?u;?Y),D"ѳ'OEj2GCNú?&;|.\JJJ>%ݻwB!~P.d껜s֥g84%HĔS)}r2=ѤIg2}8DjoӓO|p 6|=cnNASΝ r~:s_̮胇.^vHw\>+>/''~)zLAz)=4,GS k*H8  т, 4*jD>C} D:A9*4JZ45@ S̆k@ FEp峀2mhT9wS=@L1([`x8 #Fǀ[(x|8d7YsNRܦho怉0s>vlر1ױ82Y$aNuaIwtUo4 u \zaXp|w] ~޾}ݻpqjV#DأUރpNc]lh~lrz7M/NeqlSa Ib1re$ Fw2N=#ƻ،yixpXyQ q =HxJĉT/kIcTIt9y ƃa<\A'7b$䳁|2s8{2 zdƜ9#!XewDK/Y̚ ڠ@wfdpB3<OujV)O\)*:PRէKeNI;FL&>.>|,[Ǫ1 U{IC-[gK_U^_US?L+3:4h 3lX4puRA2xkpx}6 [vѰ[VuFS7ᰱ*yyqX(s:˺ttzo6GEKF[&aI>OưQvvfTl2E>ǩUͮzX;,0ecSegЬ,)޷p}w"aHЇC(Ka*OLL|455ŨoE^[Ӯnv8o_]Z^SJ$R> qy\^?i=}\.h/U{N}~i'Bn |;eee|PWsx999yyy aܜ^Ӝ$&G_u+16JL*+VBXZbx:+oIa/|ѩ@VDprTDJT/a?WF,~Vz\tHdŤ+Pȗe~|Ź%RK]+-,|ZLB kiiQrL OV!ʤr,w]o{ʏ.*S630rډ 3$cV)>qɤ_5fuZU:̛M@>ղ6!_ZǮz}]yfn͈ n] ౥Uy=_*o(氛/II(3%)QL*U*x{]++ijzie7M-;Q < : )Y|qZYSUy*ifr W*ֿ#͵i'!3BlLJʑƆ7獯j1J$uv[5K6BԳugs'f^=}Pǩ,7_< V^ Z7AC@SF}٬7xLs9fZ7V]AcTMݮ۞m.+m((4ڛw]yi{byٰːwimVMe疙ζլVd^yV5ZxήYjEeq~mmMM}O0$!%cJ%ssskPO^Y4ziۍ5y[oML<@/_;~mGWW绮NnOwwWW@]K×E}7 ^7vtvl233a'|;"_ÁU`Xiü<)|t//7䔄+)QQiI7/߽ IO̼ |-?eV/߃JKK>(+Ī*+My5&*(!!QIIInn7 U,ilLy\ij{@' |8RSS)PGG3+=H'DBzDW[Eų_T1~}h #u ]Px#CBbO {LCDz&q HL%D>%"EeZK N 4I8E#w@R ޼hJ"`9/*:4LGHw:$~?0PHwGy*o8{ 4qȍ@ p=y49;E,xwӏ|N;iӧ&<Ԏ*AD@ouV)ud@5!9B pJ;"yrDWoeMln~Ow?tR$/_P(_}>|V/\{p5^&ђA"!-i;h(qIGlLr48D98`ry`#ZsI96-[-E.v9ӊ&c9{OS#]GpK;$w=C%TUxAyPQ$\.u]##+H@d]ҭ.Ϋ'f'( I%GGC#Ò jB}6t2D>y/͘fٲv`0DrX:*ᵽJ%]ͭj5/ ~H`Y I(9veH90/O5rgc3WІJbf(JAρEIX(S䷵x M>amMԬ& KazX.ݦp`] 9'MOEB.i&fM%z0U 0Nr\=?ò}ktN05׿OU&x[ ?9. nlē^k4~  @}I )~XgN}(!{bXKrCW2_]]sSݺ}A%XjvHndGbb :3-Qy4~{KlۂQøc70R꓈%a8adљ-iffu>.Q_<&78yeB9%(nC}BhpP,"!SMh|䃬>'DHꫨ`Շɓ'ܻWvJrĝ;.ݼqNR|rѫE/Z}͒CW SuQ _s0s2ҥ;Ȼe_xݻxH-=۷U{啧OUWݩ"<(1"WMLQj|\5 ħ _1$? GTB~WWU=|XUrdruC$w*;p\"F H i%I3h=k7F";'Cjޅ|"k7IGN@R>BS6v%M+!G3vIߙC 8F.-D},̦9@w^a,څ ܑ>|_8A KN/SlC\&nC()!?A}{XP &ܷڏha)T"FJ$ LD@(+{/#J$ט o-|bbҰ&#kRdS=C6@6g qGv{p@%7lg:66h>9b}Hb"e^BA2r)QAhgsSΝLЮ$7H;COLT,^*.KDW]~*M)=˧.0s⮕!tp w̋ \"0wC C;dHƗf~s&|4ghQ}ܖFŨ5dn[YTꛙTLToRPoЈlD2*ɏUq96NyWl`dP6 E¡ŕUAw@&dA3_ mFAF$Ţuv9kǠ  (BbnfYq']FeBW)n3Z s|Tz>k6liluy7g5}-z^*x44"}XX)h_ƌWV3&n1HAC~Ŭ]}V[u;um U͵ӚY̔Šv1 ;jezLƢcA~ LŢzIί΢_wv -ODfjyը_Tā,&vk &XHAVq;wI n76/\nP^ v<]Y3Fُ&S.=Dhob>8uu’% E IQ@$D $$M4tݴEtfw{lk{TuYuWmu݇u%⨨vҽ_v7U0-'Oǀ[Gjxuig e]h<5prr|I?Kax)XHSɉ1R߫m&UKcvJccpWmn..-mll*''Fs՝n(cPъS%bO"IĤ>q+ԗ_;|QWUg䋉`9X?#_N,͛ӹJJJRbssB}II~7|#{ rwM u]oExD8O }}D? 1335;;;""r+(( NMKM9bccKNIύ|{6~/nzzTTct' C:;ߠ=.*(/,̇egf6H{X61:Ru ri  M BsSJ_fBA4_g3xE|*h&T[@c19'&bŚ,G}/C#DX;@H=GtRs5&VĶ/H0p"p!" - J>?:܀u ! ~ C a;k g&}PȰ'&?s fuphE&2aH>A+>sݱfntCG`D9#P`|=;˿q]=1*5n8 7JOБ=5z 9>{lAe>(l[Htx;""}sxC=-Cz^!:lHO }5!Ҁ0okqtq]w+ s\L ukrȏGaԇ!R> C0AݝH0p'/1DNbAL}'2ovY63?31~ MA}+ 3+sPfdӲIptW:7A{z;;XBPWJæR2OǦferSZ!?kl;gjv>TRnlvpo{~yqB=X]C(yuB8Tn2as@MS+wwVf +DU+rY]쇏j_MKQu!=.&W+{G?z^YMyqP_GWclw}>v嚣o7@=2\o}Ssy󢊆{Y)RSdƿ}'cWR{`xF5V?+_#)EI:Z+EݱGZGժ]f?hM%}?ՋD KJ''JJ'u}O}?͑skcnog}=CPX\[{w'5!ѫgފ:8\=j?:-T6&I?D& 3|qϭw*}USW]]]UUuN>(:::,, W`0#>}qQ~QQb=aV\8;+TM}0%==.9)2>>-'>=!-!56>5..-1#>#6-&Z겝Eq/?!;>̆+=b[^^^ZZ.W «|Srn'܊MVҳcrE̽z{\/.S[tttv }"R_F`w )3. yXPUJ"qnJɌWF L;QP8 xTv f4h›Y7FwL8Dĝ!ÔUvqcOk(g tfN!`Bn?|8s ΂6ctA u,)D0;[9(aPܜfCq 9lMIt |[_DfM.Y_i_w/|dϟ?W?ﶕ#6Vg xxn~L|@ ..L_~=1k/_+hX~.ɾe̐e߅@7 ?7kxxqA? .nx[([pE qZ/4hÆB8'G=D  :Bj %Mq%9&p<jYxjpHđS=R߆|qeqof៴SSY?eiƍvCFElmEU"  l C6fB[ѯ~߹7:NtWMUr{=uoUolx¬jnF>VUʩqjJ+岘aNfFӴ {0p85uVZ/nNkYLt;Qi0׍۟O_噡h?.jE;VTPe44}ՠFv˲mf n8:?֚c 1NdHPҺ_ 7ն6X.A=$>7v2O״}va@0Nh-d?*ƿ(qwDR,IMeY<:_=6>4L1,bGVbVN[K/SG%㳫˚ڦWJixb4\!XmTXF!eB$Tt6׎O.tu 4n7pǪ/&@ Luє1w. q u4 x5@l[W^Mȅ=' c%Nm~>O[;Z{VgFVUOגISquq F i=%O@0 CLWkce}CSR_2܋W vCGS`I>}bkf;ÃQN}P~F&%jfZ3}Pg ,rJ_x~ pC28>S˽A/ԗS{_WQQQ vC+U>-<[݅Z ꃻ><-.{NEǏ >̽/FƕIS /!}Bη粯]FϭrO9NIN9wzrGD}EJK zZ]SnN:)gI#KK~J߁7,hE㤗|*cZ( ܕ9Mp]8ݡ(>BzgB,љtP)^J A|HBOu q4N&(9H":h2DK:sDy`K.*i/VzNڑ@?:{WE.s@D+@x`ῂsq>hsˑèI}{Kp>T;C4ٷׇ7tXb퓽qw.@E6Av6'13Jِ8)|$ݱȹbȇ=3RNrJK:ţA7|t CljR(r |, ^gAxƃ'@wX]?u4(.=F!!i 9da x2<:x8ٻgggw3i`w&L(ɿ3K>MY;x5N}K[dX3ZͼoF)SɥjzZTȦSJlB"^Z҂|(PDi^w$A5efaS\-ƈrnV)Vʬp>`7>x#/BEm}yFua[2̇~m^h1-J X ?DZm[kN .կ/j)u_8;-x?$xA/bagYYt[LY\^vN^ ^ϗOO *4jpq`ॏ)&\]>/m7kgщyp &YpBdm+sD7EskqKo J$s##NXɬ!OM[}nJgM&]kk[WUh1v5^vn-+43<1aa.þYa6sE׹#nnP kWٍf4mA˿X7FPf7p^tw7D9!"yRڌ7g7@<7}^oib]J0> E !?-i^w4 ^TuV:̪+*f KVf{ӧ(8` &vt쬺S94!dd|BѨg t:gY># A!/y@@ Q|B!}V?AHQ# ry _OX({x3vb}}=sÄ55O sKK y0 3˷M!r `/1/^OM&iک)o/ 7(r*Pda88<B8!rAq~_t㖵USSo5]>ow|:Np>'g\|;fC}E\ W.@.##y&8WZ*̹v/"L|1A_ ,-/qq)_E\ߺ0nr̓ bцjRHO"@w}RBB~o~bQL\!q[,ю+p(ExC]Q$ahÁwAV] 'P 0V@V tD2`W'= (P ?o<2rΓG̃qn|OPic5#1O(,h`Gs&(eJD/5X͍y0 RÑLCvΧsWDWW6I}~=64 '%{*c"6>6þ9pc{}}E[vqG63dV\!`ڮl3N oan@ a =x {D{ѰH8rc!sfBq#79.#5"~x" ,zrH~ tR|~qPbV/"xNAj!4x(x3MPr5FZ4,L ^۶ ~[C)g7´jiArNIs3GƇFQ0'c}9&Ud d058vvhlP߻STO>k$}<.ojylz9~fywglX&$IQXYҮN[vnUi45I'?Fz{Zere$iV+#~]@Gw]3'ӓcY~\ʤCЯh < XZ5>8YD[:{LV`&O:gXnBmkU@8 5vu4ԵUo5i&=FR9-&i՛ג~iln*(*플(fw,lk :͂NlƼ Ym-ͪfiZVy^{{wwEՓW-j>Vv~83ͦ5vac:ۺNxXS^~j~6AM~Cq!7X^4ջMI.C~'={n5=n=mߤ/Jض&gmx2C6sŁ*}pcSY]vjYuU O[d}_@z;ڏ'dX#Oj_l}~F䲱)ɩɅ% ʪJS4Jvhq]VUb S_/̇k_}~~g|)))yyyߓ|iP_N΅0>''G,W\555+EE҂۷s⢫I!ó"]:Yp9ڥKq鱱S"RcNuQ0`6 Ys?G/o#=q J8DwO&~'0^.GɊ'1#҃xC/r(]G=0xGP\NϩA~W#yۓ { jF! hYÛHM'0ݏ'8PWcl 6?8X7vpf;50!=9l<;ve b[7)1@o >V :DRcP,|@;X+q(@L^sŁTuy.;Gˁj@ͻFn^R"pd?*JK .(WL˹vm+e 3,oY9G[٥2oblH>?:$eCL}2·at:.Wf3&lR(kW=G[+媮E߁76- 8 9 %T3! պvvq~ 3tƍŕYe\kTU=-5>IȐlZb2h5aC[Ny/ ,Rt㺆^_MdYc [Ⱦ(*bQA {A(lHH! I $d!">Wq93us^J8S|V{~jc:r.fw;ז<^p'Om{x/oj V͌Nj-)x/ZJ ʚM\"1*⟃otMFOfS*vAӮžnH{P|v޾y?Ϩr9MT{ⅯW[tNM8pUNKd"oG7AB|{X:H hh|{E0;Ye 4Kyyy&=]taqI.WPyVL½ԘS Sӓ3%g NoDd$>(y=?= %9eeD}fQQQUU> wrTTVd0ҫXU?_Gfne،vq1؅eR)'I;hO,<A*)oh?W|7aSR.  p =앷b k<]21<=ODШ=79 ah25Fc\Շm:)¼+4eR$E'޲c^ "9E;' ]>NPG$8AI*Q(̆YQO_8 q9ğ BcqCh& 9~ @ˇ;\N%!ߙcaǐH̱p. a9\}w;=t=? OP.`EPV_ԇ?~>ZW)]OB}1 {xy(iIx|7Xl8Áhu m+͘sI9\(f1ϧ%Łp98qtRH:Ix JD"7 N Va?O Gt0H%Gh0奅2"ǂtN*H#yxZ~F$sP97jafTqv4;;g@1=+ʊO'Ct3& P莨O\]P-h>͚VU"tUoqNV6؛O fiL"J% PI&L&,g^/ Ǟa_/o73*lTiu;mrN۶P7lVŬfV,jY^LFikZŦvѼ,4. &&;~Z,}Z٭-dY nF?v6c֚WOkj&D~6!l;FeqYK+rL!;LuwkOʜV#1~&6P2k_{v/[1q646 $Rݤ=۲iI8&QmV @۶-M涹)NblCQ-[\/qQ$`s4i)TF}ǭomo{9* blrlq,QpwUѼg]/b}/5n>\G9l[0;ڞ>immYYY5o;[ v6(JFFGtGy=þeQ,ZφQ2t}SXv@W[ oG]_[uuuvFG_z91!V*^]Z1;źwV|.#~jzfFl4FAf,/"39*F$399Pß~y+-/,)) 76 w2A%_ii).ſUep3bu72LG#NyMI$DwIH!=Z}89Q<@PONNMm-!Ώ2ΦŇƑ=.5lm<¿h08vA)Š-{(5=4%,]qK8LKG%dΟʲ8)SӶ `@ق"ݭ* `+4, M $@!d!$/ vW_=ʲǪI]ow%'v(=r -SE!CC[ CALHCPL p _q&<N8ZbHz ǎD.Т#h@!u;CҎZp.:BxN>|O7nhSюR@uvqُwȰ/H r" #0}Þ oi'8w&&T"ܱSt8وɀ_&MH})RSD.B8z,BݱУSsv囨̔\zN';.'U!~55:r,90;Uqaմh(j(h!.`8")Ȉ 29ȭ(+q@c~z'^~d'ݼz$[T?yYPAE~ ZRyY/&&N&UQ٨bhR>%O:+6L;s:.W:8yуjSoMR\Ԡ3!9YkdqfvoK,SkUc`8+Cgq/OkVά9vzPm[]1,߄yseeװnQˤʮ.].ެz6 =_'Ril>QӓgfP};8l/Ѕlff_280isG!j~:V s)YF{p~myD1,y_6I2-3SU0ى؇m=ctfcV?^XZQqRTP^2kv PY~+`f-J,$ >RDg]V'es \؍s1-.5}wdKcU;mϥ*V֡XӦmymmn+Dy67=8X~s~^ <>0+675656Hz_IC" Ől`HFVOLLNNMG4Im]^^^^΢ }%0 c'W/mmjQGUI>sC gA}%YqGW!PSUUՎ n޺3Rs/u_(J%;_u>/#0' +wP_|fJų቉B+)kho❗nNY)XXQv"Ѓw3 fV%]-#jD$0"S $L,<}.TMgo7 _QN#E(=_{E/``?z$N~B`V@2 B0>#Ő i ESھhNz'qHV9{ QEh_ԑ:jD90lC9ÉvD>b!Èx%"H#(f̰3}߿=p :4ƣg9#q̃ysrBc*[3k~=O#ba/"/%3ųh6 ߈H^}@ .'( q)/ ~; ~h t`o9 ,a4|x"DGC); oqO#a\p% AkX~4$Ȍ9+0+!!NAĬ_~"! w/893Wcg&鑆Ya ԧRTc#cJR>"x``xY Qߚku{.#ϾQ߿l+]c@"LlXLKFt*R9g PsW8̠C}nyΠ>ت->ѠvAý沬: f{d|bӈ=Ւ֦涮5#ʉ~yވ I,YsE{jUAM>~Xl HVWMrgԌI]xjOTS"m~~] lG:EÃu\,Y˺̏6#mNlޚ׃nqʛw ZjES3NJ8+=cOc+ K%,ad1$,6*" YE a [?{>g){nZzVo2Y9p7֦ٺs0-A}Fû8fD!scAf=~ |.V$˿sI`Gk]iUٽӪ/Ǐ؏7XY]^^-VNo -OMiUU5U-ϟ6+MF==YA077OjFdZ?|T*q7ٳ_*>F[%L|_!Jzz:D$@{%RIRO"ψfyfz2SSS% -$7'55KAI>!>1gΥ]OJͽ sf&~BΡk 7ŸU$%'*ԇr\}٬|!$#?'7(O*^;WS%'\}SRRޱ\}X,*T*J:*u~{(0 X___He2'Kۢfsv 0?(RCpُHPH!"]'pБp>p9PShج"\dh u`}hc;>hΑ !:%p6`q't1K8! @h]4Jp;.p&|JDWrctqas? Ns'2lC a#vvb$X is܇leIסxZSRTUcL#b?~bueƺiy㰛,/jh:8bWYYw'#1'/(K+d%@}@` d/..κ|ֵ쫒+A aȠ𤈈Dkq޵Kқ,!Fq~yyjyuVv ls1MD`!P t)K\nUSef,14~?% tN?AbNzJy3ϟ9/}}̿H򸌘GY1!`8\lL ?,]i7kBa0~?ƛy뒹7~nb<{g?1BJ!%fƚ ,~L*?Z36oo^6]ecɮֱQ1cX [ډh&~>\1}b6LMM67 Nh-.--A}bD&+JTR)WWW1#=;>+}XtMг% T)S#M4t`6?G%]: 0~nQf$Pqߎ?+mΑ"8yZgiGП~@HA@1H)@3P0B<ۘ40/dPx Ipav=Hf d21?IS: 9C'@OQ,h4@> 'h.Vv\ѿw[3~;l3j&W೐ׅ)v~Q>riAj2m7Mq9 m6r,,QGUZL"Q'T^{{jRYL"8fntphx`jaxtJRP ^u0@fi<.s8ݡQQ~{jTGgfTJYo[#P#\  ml3H4`&x/Lvp_Ȅ{684[.)lm0]Ӿ\\ߔω9F'h~ɭh붙>%A?# E3^}12yY]{eMʫ++M4\&KYv~~N$ Wݭ k4iԲVdiYC;_to Sv!71!609Ω}VW:mvf̈v- Rud'E$feES?rڕyEe2x) z&ƆNˀ r!0##Ga0>!'G'W<>TjX꿘ϧ5uC $HM$DYZa]Qk!XbAZ +~˺w39<=1~U9jNLL >"K~xa;]z馋 WJJJ '(*uYѥ*_Vd H}¦Ɔjkb ûT~)/SIr LY6VudɈTGw$LL)Se XTk^*@Kh+SOԈ3cNKOgi4%%jnP_nd6|pG3L|r `Z^mumMuU.5R$~c0e}AA1GRR3tN>.KD`/t^|$̃"yt,Rjx7, Ȅ^AqXdzM H=](Q) 9̼"I &QM-Aq @`tBw0$)@wցX(Ñd<?L!C 1?At bG~up21}3:r ƥ\1FP (x#yn|M}ϟ//_Ex0I™"xB/{s݁8pbҝL b~`"DIx'O K81" F c H ~˒dጂ'# =y.7M@K$sR"Ä9GHKgT(|ntJH!a@؇rXUI0FR3i7݉Xera)QT *Q%+g~g-/-ɹiN}/f`30Ɵ?{-l\tֺsf]YZX[67V6CSc}өK3gꫴ;wLo;w7\$qnk95ܧ-.o>kwݹ^hY^/2rc1M,n/,-?nkLS憚uqu nw7uf[[]G `Vjmq٠ʅ3WZ_|j4;7w\].MqX3m|昵L6n_ֲvO:׭fؠ5춿'ugj7Z.wO/jk9w@znsudn8זӰnsGVm_mo3Wwtt9`?{{;[+ ~s$A˸uy+7nuFGb|>]/v8g4{{w޹~:kߐ*jrD|88Cއ{z TՔ}S$|.-g%;&s @x`r pX,'iaϏmXdST(ևrA~ayGwr\BQlނF'{6$e/7my=N0<..wM+%N ٸrف@USO. ʷsK҅ѥ|S1n*TE]h.˾f1.un ~^amYXR- ;_>+^S;&Қ,>>7At0 ^iizQ)|RY&쨩<*Cg?t7m4,tju=2Q@[yEļfZfSp{d~Y\W+tg"W+ ČB@Y\1'7.!pPfU0XU4G/[&~V@0Ĉ i;kF77ߪW6X7mK꼼_Pڰ# vjؙXa?붼>C3(Q{n1gPva{#|~_˱nhWI^ 54ԋ=]nوtffg>OR||T@x@bE7jEjqqq|\>44(Cczqˮvᔁ@',oO yض|u0`Q&/;xv10/KM˹CnL.W| ݿܠg>((x7zaVVjb; o2.y3~ҕsyh+/Ix|aaf~^z֣ǩ _1^pJJ }Oӧ}%Kx,+*ι8JGalҠaoxX:,AJ6򡍍=%%e2JҬ7G^?;a~*K?p=!118\]>b!GH!PfFX"mNG`2`/17. MO$0NcGK8E6R|]VdY8b62ɦbI> f}x'CDa!FG 7QxH6K˱XɓQ;9˝ ty r@ QVG> 8EEz#v)#D#ؿ'>~DDIFp O ƃ!96x1_{ҨsQ0fp|觙sQ GC ܵ(~w~EJ< ]O@a%cw'Qڝ ;·\t u !UBt^4qYB@%3xN#@v,jnrlbI1Ƙ컱)gG;1Y䤜KB-I=KL{yH >Xp?H^>򭯩6tjt YRK&ϨVMzZ [UqZ1CWˠ>JNAӴ>K+ElTSM- ¦&Ѻ~qr aeCΎ¢Wm-- $ w {C `㦍>>;=jB-ɭc8rZLjun7nm,?꽂TRF.Ӯ1ㆊv} Vl\!Wʥ꒪ aum~t. uyjMT8(j w ) srCԔdpJkpmyEªC_P]U3*pzw=r ;p?bB^ \crd #u {[+E>m}Eo6 @埖aQ tP3|"EWbvI񪲥q>7 $] lejf6!\[Z6o|??8l:xG/?@.zVR:esjEBMז(OPQg[_ۂBqX@{Ocxڋ,Y_R +>p?g6jYQC_oZn07PNNOPAԷO6\ Fjnn_x~ήɗB>*z{rhvǼcٷN?I܊N>\~9Od$A}e8)ܩFSC|AGTԬԚqSMӤi7|EMEESSQG@mvگV43gw=ܯ}đjjtW.֖֖#).Q굊"$G(RVkS>.GV~&,P R(sS鲋e:<#꺿?OCpN}S|7n(o˛/^n+WQXXXTTpop`b2Fc~y ׯ7`&]o(Qd3qux {J8MRv!$C^h<<;9ԉaJ)xKtOzEliR;EPS{ Y`u (Q)!IE[\(<ܠA9 ω8181.B@u6A~B0LH  A;I O?bxCQCE)9"\ 8hfCE@ m2!g;HP'DdB.XO$$#_@#8|9tǂX < -? :@ xTNGkmm{xxwR8<0K JxR@eB/1sh |1g$E 9U_)D~ MV'F@}$`/ WdBU\J@)hC9h(Ryq v!C +qy)0-Pʹ_|Xf63h'IzX@h%BE`u*+Xϥ+?=E r-P"NUj%5aN}Go K 3Y, H6b{nMY-lr:2vvv-ǩoowv^ޫkGlgq@wolcͷG#C7O=s954=5ݏƞgͭǿ¿UwyC+yʺö9隝=ŋM Ă:wg;"Yóos9-v=^Ϛ{{fs,Oo޽-1GMvM]]nѨɊcu &wp]^ L>hnhlO잛0;~x̶z6D{cz6~_v8C+!wossc׻\kޞѪ+555[FLV8@,{w<6az N٧ٶ겶~lt8|GG,:ex ]Q/Q}>P%l ¡H[c~{w˵vn,w=vչ84wpg:C?9>ԷQ}A>\$5cJ[KKKf7\S:::|oBJ|oݪ-Ϫ̎̎թQYCH)ԦDx9𲌘|vi2NrkaRuI-^}Zի:UZUM2ȋQ)/Tz ]v:0 A*5jizbګ5JUF"7ՇWso$|7o |i%7 /5eJ|1jg2iMf===A>?'޻wCD\&T <Lv3A;ys`*:l, ̬4,"UGDd2$q :9gBrYȄ NP#XrHw@h->$&P`2EN`X#OsD#NCwسB3iIC!@сh"Ė( IT t#?7 @eG8E| 0%eϞ`6I!g~#;h4'xB7KSCeᔟǏPh&'1d|eB8A;SpaV%'犚D<GXxNo0r]/d $@!D:׀ !)NOJ%pb8Ѓ^# o~S%!1~ A4ӠA+PciF@}x8qC:!ka.!i ^KAlUj,ʓO>ʢs][~V6KE ۼ}bvzlz||pjrZF̖~SuG\.9p~$= Wu*d:gGr^MMY?mUk; Р(. ض4"*,BBHXB d_HȞ$jw_K;ӭNuMI.rح&]skˈ`nI"RIܞA-O)mM1Q,JMM"cŢ qjﳘMmVVyKsMr~qaJ x$؁l6hX;Vẗ́p-1JrOH.n%Vtq-y9Y׶kNajp z%ݦ7f8-2X<4lo X, ='og`x30|2kjk`O2݆Ҽcccр('#7g RYj2nH'f1xagpЊE(ϧ?VX; 3ӓ =Q~{r/)H'yw7Y$|.o%gE5j,d<I] FƁ!lYpCh0+{.Jo_Yۘ%ps=Q޶榊JTps yi%!gÛy̠ g6{}~"^a]f{{zx1(h6?? ,HS2|Q&He b}}]v>8oO}VdBrH$ygt'qor,ZZZ knnnԕSBWQy(ұGҾ=׬']iWVUUm@>?mAÝ`މ£܌e$5ŢWn,{fccݿQF |L>F} _݇5嗶RrBesIM;OdUܺURb!b -'pggfffB3i }G{ӧ>yG I.N.:\ZMZp+-L/9KPaKgҠ8X]|L:RpS/OAQHKIX.M Đu(s$60PrGpPC,88}L;գrSȐH/sOzXnXN#C9~ \nIddx~i W$$p߾@yGR ‡_U0KBChiH_ = ɗ3IG|,w}pܟWk:"=xa@ !.WF^) @QZ݁y*r:pX#0^ =K׋Ct׊1Jfm0ڕbW~> BwrvUI.c(#} `I.2 [ āt{ri!ka [ń@ FXs$$&k~_C[x kH'z 0&$V83P\3 a;YM?æQnoXM[P=i!zvSQ zuI,SȥK1$m>vEQ{_($%r`N˛`4kZbv-zx^TIFك,Pe "3щ֊R4+Fb쇭O$L.[') enPpǩռnլR*DK2q/djF޽7:ÿ{ U}ܴܔvl,fi ޒnI´Rk{,ID.\zPq[6a!0δjEpؼޮ=iaæޗn1R6?'[ڻ/9/[;ՋI\() Ð;lT r[)ܶz:ۗ7ۛp?DHlh"NM1l^us_-ԗ8/o .B16whBA7cG?ml8! KĨn$ %CSM">1)s"݀VeϺ٭,c=zn7g}z5.LޡmE}ēo>V'NӢT*iDG9\x~XVҒB*~W6tm zѸeLNN2,pb|o| r8#  V[[[ZZZQQ::|=5qWRZ v0!!!da3NX@ N; #6(*  Ҭ#H !|}\j֯NιHIST!&IT& E+X]eJT M&eԇrA:wʘU,I*}4 Ĥ&hMzV8;J Ɯ*X% Q}_WyJ(]|Ʌi%i QfYIێ-~r !RYmCV^ u5u5/_,OsH㨤) .p T&\u|Dzȉ'@DH@a*0$%w ЏQ#xy$,=dD88 : K(x/!oՇoǏ>}/A)9HMY`/Iȉy e\(0HOxHƃ D!=5t)3QfH<]GiÇr"$Dz06e hĴ*!ٛ",ЊTb?aQ҃ "#cZ`3@--_!NDј10@(.ϖ|UC,WA1{U% яzI ~Qoޭ+ 0B}98p\w{uoye\XZ=yj ໄWښk~cz0lvӵ7`t: yk}k~ukp,QB~8ku߇Ǿ?!-ܽ>s->D>1>u}mͶ8t.-;} e`h`xptpl7:azkcvg](x?փ;?l掶 =Szn'هF̏~vXV𜵯vCMӭ[++s>ކu-mbla̒e[Rc߳kD?Zw{7#Muw3v>sΏ-uU }޼|crѱ45]>OYo6hq_GqiuQ_tyޭy`sl'7_x=E"C"]Vߛg'[pFKΊ,Âkex۳OwA~=ӷrl}nLoo6ݯ^LMw #ԇ͠W򃘊8vn#ǿ5EqG;nK-"iWpW[[ضe^rFMriepd3AlK su :JKˋ5Fgʪ״ao_0ȸ]V¦EtE)Vn郤UE|S)en0CegHX&=Ctvz^Yy΁',(w4-mBҴ1$}Յihlx#yYSi~ް>uRs̺/o{vɮAk1אDCO5OHcʒαYHkV0*O ~-z|=*w$ġ#%}ᤔ 72Jˮt9&nyTVUUꫯ[VW>.){(T>}7njn;[r%9+51)BNfE KJ KPS^S]&l|jq/;s!#; 56WT'=snb]g'71]!=6f>̲S!4~i_]VP+梦0‰CW$qL; %"`9LqK<'4T.J !kYL]ĈH8w6-!yRDΤųoyvl^;K~&].=aSZLkvAX[ꟲU=z]__KQtg*܈Km],͊.MKk᫡a_gscd$/@)îHS_o3iU*xV KF%gu쐪~sqA^/QŶli]QߋD7j3 BsE|``2wS[# #La[+AyCY5׋ŭMyȸ`oy(bg'/=?#eQ{#If,e7+^=SԵKۚDݽjE,Ez|iI,p͊!q݋y%6X'O7mkŏ,Ў]8bS+TiSܿh󧦲,ʴNB@@vP$MFP(jPA6@$$۱W"LLL<=ܛG>k'`Nfvvaaqz~Z7}DԷf4"@}P_oooOOOwW3yo/Ottt`rd*))#F7m&͋,ωO y~q,(7,r+(T&p|=a i|AsS#2]-pu7U*Uc5B&f'_VS!. g ᐞ$3&[,rOc'JqgV}\ O4`oAY(%.O\ؒT$)ڔhʮPתqC3rdPqaa!/,Ohǩ'ƴ^ (a(7O!VwQT*o*V~0S@5` I/ rӄl80 Rxs)ةqɱcDzL[:q ʜЀ8ɱ,@w q%<p(O[pvd)ʏ{<_L,vD]D"uQA)Sq/i!O4 63^d!"y Թ{4<`B x!"~_A a@p΁$|?R>˗D|! '!įJ*S2ń|΅fC(B2DȉRx?E StR ")"l|`8ADu( C1S4 ދ`߳mu#ݕ=Ζް< M K37[zBݭfwl"z iZ~=1hX]:>M٬_^3,nͬhus QMPnZ4n~?ugbƏ~x`cP$NmkݪYY"o579\w۩/:mkjfifg;\ 67W&?nVj۵5[]/\N;?:= ;s9=ힾ'76V{:/-RN8zNDO뽅uCUUm}3|69hl1;u;fkn5i}a~GGŋUWSSS3o>¾%ݚ{۸nX[CGSCgP_g}~P,W^^򕖖VUUBg7"0ZQ,Wef% /~Ŏg˹e+ByIJRw2]Cmii ^ ݨuSިiTU7(+e28Z!ϯ()s|$aAA,;hq:3 #/'_=` kQR,e}Y~l{3phmz᭬zl 'hb9z/ Bn&[ 6_Zz›{ӏ/.Zn3aJ.}fWް  u-uīؾo+xÍp,_(k4/{6kz;-+xj5Z".Yx==p ;.ӳwp~f~':>{ͯ|@\?mooim-n s}英E`O({om>8Oc׺m׸ 7]/z'PgPwuUłuV>izVT68QazEGö'< ~!nir%+zavkҒG좣̼XJ~%ųGaIg޽N \~Ns-LYŦ*B夼ֹKy 3oެh_{гs-Wu]KN1י";s1ȗ_zCmM'~.LNc]U^^^]]0[77 CQ:;A}vb\MA_ʎ-K`ygpߌ- sⱄ"Xn0eyr:9Q)Qp/4#aD31H9(.68#@L150j"$\0/AO>sq{TSv}S_z >./ q?<nj#I@ͬWuD}vSf15!6mKIOd2tM fsp8`>U}| @;\7p}&nsˣ<* ޏ7ўwۻ>}&PvppB=˞y="E}yA'*y=fM)_fjWy\-|AtVJY2lo^_s[WJVb 3BM7?Dw\6'{&&O[um~3=;77 %o9lZϡӪ'\;;AW.l{PstmǞO l } Q^n :fs[KkhͳhIǜJOjCOBsDdIH$!}׷}y:uk?a=OO/rΎ8haEIH916p:ha IojhA\.O ƽio}/j,rO Σy/E>'VQ̃OvEG61 >S4A 77/ *&Lonjn9ö{5Q):ں;EC}ߏ@}2'Ϡ_XԷJ1Otz=ȇO.QJmln/,###CCCP_O @}Ax|>CE/G܈lKֲ2s2RܼnzxJ>nx{RO񊊊p/@D֚jC糹?C}Y yldtnNZR+6Q[[UWAl|W} k^6'|ׯ_/qKG/~@Zl)d / ^@&!}Xq= ؄C1 rHfG`6nJh=+ {G`UaX2( bL } i2* 8ĄMG13C|;Va^f$%+pڟu`F-`?"CPMz1*0J`ȲcRߏw[z U4JOЩ{z- 6{jjCR,/W yl^>%&%Q(&jNOOimP8wPG'GH`kk˔Tvxh(oo4fF-|'wOND}p߅rhwLiPё 껸pFӬdjkӮXmKٶY6"Y3MŊRh6*PYT[rEm~oߨTle#k+փsс͢6v;emV[.+)rlL6+@i.tk:0@&:ޤj-KK?zh;(Y MjLX̛ۊ~mI~}?"j~-蘗57Jgi]wBOo"ƿL8,fΠ|UQAfhŰ]}A%u&6ti[Ɗ7d>:ljnwiF ;W CBf@ _|Snaտ_ؾ hTX,lx?]}333sP $FɇfW=5նU*Z}}==o|W[WWW>ʎ|RwNU1IYwnzE<3\qe)"3*UEP_AAAaa!Eժ* !I䱃dA}Orٌ̤ڲor%%%4W|Շ2/PVQmɤ">99)5Y*lxU K*j¢Bf=+))MOO$d2pN:6MJ'{hbB:|cSSx23=/-Ɨ Q`#ދL dPc? 2?2M %~F8a(>q`7MD(.vb)  cȍ? ITŠ'c!09-&T C6 +4F܆)((w "&lbۻ#hݤ[dH!0__M^i 6.kZElTYtZl,BA@RHXFF":3?ޗmq4g=}sN^y?aǜi z1 FD3'6 mrIQ'"I ÒI! ;yTrLX" 7Ƅ%P+2ALH)H3G#NQQ';smԍ'XG(/H/+=DZ>>&I6X.ZH 7+̧8,̌Gut!0-D={5G1nT%dQ9ܬ8fu0^yL) C p2LBN-7 rî$P Aku\ .&ⴚSi,IGa u kP^}]KEwX)Tyi4O80-*]MfR="u`i:ũ/n+vԲ]\PuD}Rݎ䳘v,mF QK>bO=-* ^Py338ݻ}642}ӣjF_?w`8 ‰? -: 2`k;_x 90>O>@1}vzj=N⹭->>f%Lt%ɤ{?˱g]?8m]Zî> ?-37f͞NJUf۶}w6/]_nQm!ષT7110sPߢxn]*=F}>ZTnHkk"H zjP__rpp!ёa>>,A_!,ʐ_nBKUԭvv^?u.4Z,>!/κYͪ\P|=u>:;-SpZ\@͖xx\aҚ 99++ʢ W?xp< >>t>q^Y;w +6{n{uW/kr~7;ۮs^d=.а,fg^3G~KH7%dBE888򸭵cm^#= ]3j,]ߢѓUt7$9y!4HZE L%<ц h9CCwWpf/3,#_/q)Ρp2݂b9b \Gミav>>mdEh1ʭMjɨ0d[;[y+ΕW`Hf6~o7vlfAdjw`?}flfasw{myqraF-'!РdӸ陕??YH"?Ůdu:ZFdGە9l2ݢUA^߬WSIFa\ݨb"vAI 7P¾! nBVV7_}oU@鞙3LU{'ꡎ'e](m{q] ݝMMd-M>{ih_w{Za2j)-`mw*4.{+PS2=3_\?<9l-?@؏Xnofݳ[ =q`Auעi4wο3QmAqKӋV62uϲzb7'VR"N0˦ƈ*Ť_ooԼ6XmGvKk뾡,ל򆆊~hA&hjVzχEԶ Qn6*vLHWf3 FR477556tvB#C">Od:~CWS9ںM~ޤR8<|B}_~E}^d @O_q3pI C7#b?( :GNz$%|ӄpo؏pA!M&KP2#ATDlP-TK~C ȗNǨLIBvq]"7 x@Ā8h v:?@7N~,߲ȞvOAg4JtlBΒtX!Ila7aUSEC}_|*ԊUr[Sʖժ5W! : ohd*X)],͈DK♹鉙)pR MOMӦL&j4fv6ѬsD2Q)V## Cj;˶M06:?k_-I6fprN3~VZ>Nmkо`1amˬ-o:xxJ]=?,k)̶7Sq?>l;ɩdVVgMTI<2} :g+s3f:M 5B)ʿ!) (x4+=i-ͪ2KZZ׮jVo߷,erum^Й˜X7!46`~E9V$p\x.*հnZ~j?E$߮ɠ]UwW"gU}h~|`yDz䟙&wO=fmazh<>>_x/ûqbB݋k`7_TUZ6ΊF7Si5'D}40paZ:[i(Q ɯTassSs>j=T} \&I$E@nkmuvwuvvvuuutt+0NYI~EɌ_Cj2in ǵn\ν⪴~~rz常GW]bbY ߿y+(`%&1Ky SAAw\xB}~WVaV^^JXȠdNR.+++!!!;;;.BsfǜЄФԊgrxŁ8%\ϋ3r/]r gG?_\OC E"`Scq DrNyZJ4 q/rAr\(Οʲ8!S5U#K A ;! AVh KdQ6A\]G[$EB"! Vu?/=NMOLWͫ[= W'B}ȣ Cn"اrD>, ŰJg'QLLgÎx9)Gv(Fىp Ce$IGrd0@.Ep(5!@OQv8VB^Xx, @CDq(i4\&YJ؋S(ډ@$It!!兑$1_  KHO sXb@zl"|@AD0A oH?@.w petx@/GA,1x\  @dxS_GGG__\.?}GHDQdNg_ |=ŵA= +-$ _+ĥ85g f` 39xuX<\<8WB @AM ʊ%h (cgU 0RVBQBl. .Q'&^J0Kb$1pdVFoo/Uvvj~~fÁƥ9Ȃnܺ6v6 ں ^?7qJ3[a-Z^q~v8o);ӻJJ;nGY2]N΍fԪqjYzp^OϽmgݣ#Ch"fdۼ +:l%XĹMl xuMV[__fDT^unwݹeYm{o{rYNKO\CD}ͨUi4z,>։zoϿ0Y2W[=oӅ n~<{m':=x׵a-v^7,36.ǜ%I-Y彮ǫٳeʙŽUf0Y3am:?awn?`qQ/=~YdzޞWL}cL}I(N`4{6}eee|&bR~z\94Z o@}==yWW}}'_UiMPtPSwWt9-1^郣YMTr ;K"N ;Rp5b47!<:.Q7+Ⱦ7U744HҪillmjkn\56n]+ %O_pp/++dj|>`KwR-iwҋZ*eMFͦk keuMK*jc9AkhT\FV^yɲ _mmmj{9ؕJGo=BBP 5UWђβW@A]->li0P "\ m= 6>2HvP#!cx a@t Nfa 1L< C̉i x "ю̆ C` tQ8A).DJ;ĔOC`aXKx6̃A`1 "-#{@ T`Ds(*;f؏UbWp|Dm!!P> qK\x?oLBȰI~}9uvv+^}>}ԗtd2Md^Z%ˢ}J{vx{*Jy9,\,}a>'eÍόfH{9ƶsٱ40` @$/l@2tqx,D cKrL_'`W)XJABZe ?$7Y<R. yn) 58 `YY kNnOZ*a3w,WOyF]rmu&oy^ -0yonY~fuu [5ӆ)ɱjը͈jkK4\*BwZ|>{?~V!@+yg{G=]--w'&4hÜqj5,.=t΃Uyww7X܁Yˡ75g bA 3t :eU{=Vvk_X4s[TY5UsֹwV ovĮN͌6.s} J]RiyjfSZ,m]v!\-֭jՒdZ2Rx׭v;JQR-_ -.&7BȗC/GFlMi敎 g_m3jn N Dl;zڶq>?dz:իW֞.Q|T\.LT%i:lK>:QB1Ycxx;,> }}⽡H˨?IMaJuN'TSQsGC]&yS{WDrEv&gN㲳?'%']9|ⴳej.><klxil(-.R2oRAV止¬ /??Db;_Wœ */6\HmL%sʪ ^qEmQy夬o~}c£̿t?aaJJJnnn By)=\|1O<66ZUETq+܊ :Bc7#B}/D`"0 X=o$Fs7/Ebʍuf Tc+<k W C = y,L"P<2;3@a΢h1 ~;-hx6bx̱H~2c(c2T⒝raa4=x,`h&BHD88q毨ݻw߿ 'cX_GD)_c8?F޺KցdA6 ]]:~+ۖ(0rr9.5ȧ&ćOO"#A G1,DNzB{$@n7rf?n$1YcIք$A̖}#w!5]y)g 1TQt7H l+L]$so#qO)YF ܋Ⱦ -FʤS4RA}zlxv?58Pg֯5K奙9܌tnF23-&F)Qju8v;YAen+I708uW44×V>.NɥƵc{gN& u?zTmF(úD;A{htY}X@VaA7a]' "5[S.ڌ>YpӷYӂ$8hP5/v6'Oj#]KTpӃ_n!4ȍaZz=vB..t+UIG8͜ږM?J~lg&[["W؏og#uz4g̶i?h4VGG_+P@6 N~Ox=|-l+C!<2jf&Mo5Glo۞Y솗| cwtN4+@-:x{|ld2tzj[XR Fvzs@}kZ4XQՋsssCP@{zz;H>nWXI?MO͊U|ޝ޻wZfFş۾"N'%:҈Ҥsq?:Rttyf|i鲌Z+-- $cW[[TWWP_וVq AҒ‚ʊ3*+|v5]qꪈ bPCr!D(CU@ʌ 800\0rA@nOyUW_̫ƽ>p9x~W(YYYD&s\m<9P FB֘ȗ{72TYZwZQVQ_^P^u\*5IoV4ɠaբc`8aΥ}h s4*~b?'@bqbLOdD$qb$0E| {,!IQx ѤJC/Zl(03a 2!xcBxޑLw g?q#O0 zBfch'$q9}0{,-<8KtP/PPΒ+FqA{1~`?(% X^HC9`*~_sPZɕ̈)K >eFx9IA?Nq`[UvV*I )d6hu2#1!9ԃ1%vHsE9j oW<1z @Yn4&I\$׀qkN}Eo!i1 &#r[[;t&܊nvq%7~T F4󳟪rnn߀tշ^+ -.q]VǷZl0 Ž]G;<,,YmO|?~po.:8~ИZ==Ռ*}359lnp 7r9-?>7n{ܶ`KuK{[[\T[zX~Q>'`!Nz`r#9̶5@ݰ`cv߸F| nHrMa7l;l:훛½YzqR>[ >ljhnz9556mgw4*+~׿<3zwp9>^QTeIo~T`ĨzN.m6}bVLFӪ [Ei%"Zf@'O<}#_ﳾCg'w|-8Ea|cZ*~kq>E2{dz xW}q>~!YrRY`$-5R\{# ͉Q*r9>R ݹ-{p[wu--TR[sIQ%W44Ԁ| _AAT*jyVVszZKL Tޱ1-vu|wS ; - ť5%e 29XUߜQթ?wM(H,+.),//OxLEST2qCґL#M Bވ޸s9E&:ܰ ,Vu=KNKp-f^q l|O b[:ۇ E:iL䷨bЈSą#|669DH2A.#Ņ@a<=&\OS0bB}C/:Ty'q' AP_$)1azx̄D;&@&FDdPW8'G {qya<.4 G!0\wC!=&| h }jux< %qpƄID>b3v\,9tu\ u!Tfp±`C$ӪD/fØ0+H ߰ |=)kdXiiE'}I2BQ<@4VaTSœ+$b"x/<`KE xGL;zIV$W]+*W1Is9U\#P(Bm0GD}A hnkyUoͤ3E^_X[]&Y {-$٠1-i5ɩ_I}N &M쵶rr:\[Ƶ݃K/faq؝n}r>784K^OQeWTeBtDEE@Q4"( ;4""Y[emnz}ߐTs=%3XIU^uyM_~3aMYLp(mPC-=sb&=H26E"59JNU9u "5eصirea`)AxNnȨo-VwޝΠ< ӪV"h6\@N])r{uyb6l!&2JWؗiߔ3I$7lNlr#^i?  x 1)<^kQHӎfga*{B1\AB(C~huZ:{_MD-Wqb_ťQ#ȱhӿ>.'M4,Hj}*YZI?eR넬Ujdey ؃PdmʹX{ $0'$ExPN)ߠ;x[G4x8n\2ܒGP%pW&y0ZAq?oEY /F8 x+Kn (+ps٧iuewS&& b3z@D4AIw hid:DJQc%#[6bT,:VU 5QʃyK܅9,*Z}6v;]IE 4c{ -]m?,8<ӯ=}#VJ "\g֕B H:AF-r$[S+CwZ:ElM$^6 FêypuJYOшgSV??T+E+ja)*ŲDPC x"YEӶ 8;6~b0z{{i1\;[6?\u^byj*RE7J}4^뷥F3?q`oIJLz'PIɅ̔c?]+wTnWP]|:@]544>mloj:kjrIf.n.. KI }_Dt\Fr(PD!,!%!!$aYjKx3WS֩~Nҩ[MPJU\P_\V5#˫ryu2)E#UI%*y ynyJqKK2MNQ^TPTS\R[\Z_ZT](_{ NY7/Hu*(6>77R*;T*xiC`@|&@$ ]RU*JP)-M]R ʳL1c5ZЧ4u6CLJ*W<D RpI1L%dm,dV~[R PQH@ ⰐCr1?2]bLP$x&BCbt,`Q! :10>*@ cX%Ƽ@*RadQd@,.d0I5HG^ ea,^\LO bA>/|0KG!gH~~,E ;w HY0JC@N)yѡ;@ߦ>OZI}䗢#gp p98<>0r%eE&㤑E.&G\s=FnX AnRy_^Jd^/')<qD'#od )šs7RrW%E"ï+2e<o0,2؋e H!!Ub =!iolا:GV%@/\1 a?0$rrE-E-'ۆLE#ԷlWWl5N:f \p%ӌ8q~dblQnXfhb@}VsݨsԷ aM&g-UzǞ>tp85c;7 oG5;]ݏ^yzwo$]N}| q:1u47f͋陉qm~h.yw:Voi;8\6t<|p4u>Q^sm䄱 f0>̎-/MU̼aY0n6W7=+npZ3Mvä뗆ىý-g}a3YgL+s?> (F G { ;ۖV쫦M'ݦǵn;m{#?=-,i2,.'rn""魯-kv"-Hoggcc{kjl³IGڶcþwg>.5 C;?ò:<m˕*Eݣ/vvowT,:ïYv]>׺nGۭV}99wm{?iAgό61Sο<{I{ĜOG=nϲŴ`c4.@* mh2~zu~{z|:[^'l6nL #?ףa`$R=BPߋ_< =}qww;nV\Q2H.+ [JtT'v4b$o4֖"|}3+Q Vp1:zl a|QuF[[TVV5|jZ qSuSySuCu % zxœJp--ri:=55MuQǨbT*Y2MR&VQRX\RWXR_V\ZX^ȓU'$z!Z-+H$cc!~:7<3R A@`ZV+>E)o,F}JbRI#ԇpa?Ў9Tw?!a>.;ӃYxw1eGksA;qzSiÍ^^8 ٵ\IRWLIGw7vl,̊n)iA>Opg4j^Z63z=5q7afjQufYFDnuFZjEQPIPdDdSY B $! dߓfOH}}^kjzjn:uᒜs^v>^QJTrT,] [S<Ʉsensh4ʪz=>=j G'_(T)o<ijB?g.s R%&9uEy~/ Qdؠh$ ]{(B8Ћi4<^L&q# uFRh2~X,f z">;3;?<>2;ƠUBzϳܮeR^+НêЮiWDϣl_MGG?E)x&^_[()Vg2 Lixw 64Ƣ ʼnwGOo]jUn(">XImR|9<45mmnԢi^0@Y&V |i0r'}鰛(LWƮ7 תf2YicئS5 XlU-h~*ps-O ++~ s H'>1GG[4bA{Sƀ655ulkÆC`h:2F1!z1]sYo~f1J!8Z{otlW}*~Sy`C)S~zvԣ%?gzOʾz|/-{ 7xU_ss3Ύ6N{VNSテwjkko|`[ sUUUo _un5O߸νSC՝x}y㩫g7Vr]rJʪ^~xjO%72s/r %]OYVV_p09>޹9H4+N6=5+%"% ۷*Sq9\NuE *#[8Tó=To`?" 2eO/F="'l'dߣ]̔lTvDPQֱ- &X|.adi?k<=w,w 'cz!kBġcy_APGSJ-Ǡnway{c{@xYSD84B>9f;rA|DB>.ܵIAqh ]p q]Z ƹiYޠ>8_'Bq1lF$Nۙ5^>`_N}---E}߿E}pI%5WBP-=!P1)"N,91L1"p8H`Fd#Ddh : M#;UDMHFV s@2d9ʙ1x6lVx9Dn.v 8uH#z$ÊMBoo\,.9 UbU܅K@ ,!=tNY|aX{EuE*H#`+C#iΌwCe3vf::n)+Mgg)sc5fJ. Qd~V"zf\.E"ا+ Lל(!ՅȶM; @Ӟֶ/*Hǂi_[ ųy?v6HhW/t4 UFȥB8m*Qj,":JmIEmh`?C+eݪmyݺpJģn5:VͭH,!N|X5f"p4uY pxrksZt2ldeN#E^c8 (G# fL"- 8jF-Ggۏ_ |v/$}ЮUKG/վ*fx >8\>Uh<~zIKZ"x;>˛D,Xl05 {x%b<T*ަ$D}1WU(on1~,x f~-}2-C]{ߌVsp:J3IGO[5UxxAGtT(bL2MRⅢo1f~ufwk*?m[]]O?KAW=^s3SF< ¢ybMfijDh0tׯ_?^ɝ[7ZJJ!GqrꄂJae`a!95=ݢYq RYYYʨþ>on*7$=6IpUUUX,T(*|M}F_SX[|EpI*.ol54K׻֮Ҳ2q`|PsytqS3Y$cc%ZfxDFApB>Z=LScF铟N`OBxLB*f$)P%\9p n(?x=)LE..v#SDMxEg-#kH2+G$I\jyc c|gF0*S* ?LeО(B1 &dCzuDzP& dean*Err(A>C%{ȃBGf2&L LK 2PT^`2ҕqALB6uQDw%b K8GGO8YD@b? 8$9/U}߇Àré/कdR\F.s# G4X,(%y2S^UMLy^W+ KPŠglPYU! ZEԕAnR"%\WE)~"J8ୱ4t" `β( {页Jh- 1),ƮtL1T E V́$[m6,k$5\i*O?E ,lW%5YRaAYiJsV+.>lD}yeoX[1,sVaViG&o'!Ћ9=9W+++N@}h';8jFOM.چv6ݪ:C\Gho~uiafR;i6v6f%q955k֒Փ;,+C;oy{Ӷ3kݱߊZ=mGx?|mмۇ{Uߦ*eJw{pӣ}qcI&6^nq[(Nl[9Yk}TV<`JٳaIQ14O";t¸vL?;h[^r8VlOmyyyɺF wobbB"pa0Ӎ Q|qϟ=}/Q5(lj3볡>i}n@R# jnq5A5uכ e)EHم? ,K_UT!HVel ]R٦P(arvGqs@50U*\ޅjoo[yNNOUesKȇIIG'o{E]֮ƦVy}Zg}A^+f:q/: $)(,_ 7DbiGXã獆N{pRu).Rec(1(v(LQfQ%(9$D]bG4qJi1EbEbq,*uaFxnJMFM N>$t#b.qsR6"4"=x߄0FOM.0!>qZf\GԷ Y_Yov~nrf%>6 yrXts+H tUz>22uP(7E"wT0ۡѪƘx֌r6U₯JEB!On0dzP? *f<^0*jM;>{ӳ3zQ=Zz:}s]}h7.4*Iqj>ui9VHQ^ɥ^F#NQRLˢS_PD߲6c"y`dfU!LjήǫsOz[/<({L,|^æu4:m~.!"_;r5. m=.d=C^k^xqZλ/%ŕ 0Ub~fQZͲPow>X_`ϿP_д4VX`H-΀nݼBxamiii[GKw_;CZvMkeee1 w [+iWL}MEŽU*W7WZŭo^N<_{K[\͡Ĭ켊򂂂ZZ$z74Mqvty4a"LomiF4S5=&>4AnRܪ@݊wz&\10Ǐ/ȯ ¿ ϦG l_"ds]JÑ'G;O$K>!T~XvjńSs")HKJs$~lWsvX @ZOi5 \U hGkgdڢ H1UGVS5^=i"nP%b{%iNiIHJ{9,,9 ~ ݕ@wj,lHxU"ڹ8wY@>z8`D}V:^ɘ5va5{%u[6YHE)Q!'^M089\1! y}~Vh,'\\%O_L<{6z#aB?߿$U{xteo<'_h >ٜP"5BH$ıVmԨeW.մ)-4--NN8dz~otssenKΒܣ6F ն޻=R-f۩ xtxX5?lX7h>|69=jxxT$Gt>qHVɽLM_ȟ3FZ)R)& ֢guXszU29?7UKaN"wPZ| itsJ9K'R_T_ Gx[nvF oܚu =]GGfj4T<l W ba|zEa?;AQ=G =nwgM{}v5q3K:6,"-zdHr$Ԅ^+]( b %RH#b>qwy3kM^<$;y2hUk~,f nT<4*W+3>hyEx~b?Y~lV-)s|.̢#<`P]UcߣnI+;,M-@¿Onbո=FzN8:nML+حerF:mq[2+Xnp|=/_|ο}@o@n3I>:;:blthiqmmURi:B&ZdɲFB}[[[JƆ ['a IJ}R@@i Jm+Js D^1uí[Z\j:*C⺋sRN^ VXu=/r2?(7#616+՘w!Րlo`YYY0>GoA}=@Q-"_EEWTTu׵&­niB}PmmmUU>d8M5-얎"b<^/*oe՟0o0_b<Wz5773w$. I0/_z7'\ć?d,,tuu@.ĮJB7PLah!(2 `/# dL ?lػ1[|Mj O }tBXH H)b˴1$2T"A1G@xHtǺq56233bѠ͈KYPv9.2IGʮ_h ~q($dxqyh @N@4pDspgŨh ~υ@3$"yB"ɸrP/,Tɐ+("?_ +r>]2Y1+rKg"22(H`D?VeA!҆f G v '9eԑ>4&/%"'%"7%"@S""ٻv xVx:>^m.50z)u\ V0 D5@ȇSaQLU&Å2bE  :TmPlfca"!lI2`ꇄ5t Vq~K1 j.f)i}=du*a3lvFyw̳[t63BkGkځ^#ZW>ȥUҲX( >ỷ,XZb@}VR/t*:9'A!|N[o_6?Jo;*îYLOs&dž &ͤw|i?}9=JSUIT"ϠSP߮^r9:qW x3lmo "x2[#\`4kQnV۬l[nWVM,M-X:zr=fgC8`${2=Ռn{uS6dv-ߊEou-_6:mjQVohLkjXyFJ7)D? Z鴹:lKNI#yfU&^]^y5^F0Ov;-ߡi3n 4RI@l}}Gz\];ɑ񉧽?wY1taH\a3ls媍i(O#7l!^3l{Щ_[E9b[N{uZWC uIvYD(x]ZQ;}}cbnۅ=~ߦ&?ZrڲXMjRTX^"`/o*"ꛞ>B>orbb|td< q bJX")6Nf풲'[|vcv(> +n .35$-/?+**AxJy64F71?''.;>7~hutttvv>v0jm---MOO/((hq8-%|nEyk3G| }6vC-~E5wҊ~N-:uq^8q/0$.Nq^aMffF~~>χE :[(- '\\Z"!\ŷ077 x-m\ȶ\SF GM {( -6Ȁܨ1vѿ"2Ķ あhb4RƠpH@H..Q1 pN2ٿXӧ&<־ADA0 G PȀ\P AݒUTTP $rDBB'ɓqy[_f[{[U{ ҡ? ݑQaT&?<\v }ȍD6A F=EL` ˢ{Sn`9/39p|hzb:BE E~Ą Ea@X1@. i=BT^0! -`$~cŽ,V^l {=ݖEoj`xss lzaN=_R `c] z9֖g=,\n?ϲ{[[eحlc Ò [8a~zXF:WJœvo| Kk3Thށ[o +?j?ݤfs xb4in36x림gD5Pb説뎬I6ƭAc]+6뢓1x=v͒amcuVԢ t6>Wx48AG?c#FXjs);wvj l/@g]djSFPjgͭת$U=]xg1îCBnovbևO: ݻZzۭ Ir TNNܿy^ϝ}O_nF#sxZ 05h?799R57PbB1<΁p?jkk(Ǐxbᑸ\^XIAzSer#=8U~1"C!$`DG}@ R G$+NNԇjޅ1;bI"r"`6P-1 6n2tZzm+19]Y' 0|` qPH/~lBJ8HTBp F\GƧ8DB7"pbB(9~HAȋͱ0,ދ܅#.' E= 1r;x[$_ߨÇ?~^CDn"Ѹ\p;,QCʄQx"3l&/zl!Է^ԇKC5.t:n VW&_k|75^Rx67h8beXس۽w;:nޚՙ=,ooHpmp m9d\sl> J~ax܎a_4^V:T0ÐNj-cl ioV!*ɼz>?/w<~f}E+uTc~Ҹ/-"{@DqP@ $b*" ; d7{E3sNQoU =F=N-ۭPɗ &ÒӦ?1*{M:ϳ aa( #f 9!ܴnGCmmw[Bj3h*^9w8l*lvnzhIj!B$ nQ>fH$h?{)ȅ6#aϛ]T]χܸ%~6G.|Ȼ⒂\[ {zU-yA?25T~=~H(nm7F/i-+FXAeݻwKF6ȥWRD"Y! HVU*B>T 摩E BIcll!B>W/=]"grj۫Քc7u_:_Y^w !=\umFlj:FzvRL]NzyjK%SvLnx5Tz@d\ >~oG􊊊F(`(*`/%6ApUFmōw[~hlmt\TMҞc狪ʺXYR͉;ӟjo|b++>R`nfz /711>Kof ܣr/ ½8g^8Qs848PA~9x).`-brXSG66xȐCMNr<}A(xb?#b\Ypx(!&e{ὤh(H$!M#q0 opj@ (*$#Dnjsԃ|p FR;Y.#b TBɧ=K11`^1I< *w \W$ZPu 1BhphQ"ԓ o>}!S`nC ^˗c¶׻:R _z7%&p/̍x.~ռjZiǶ*ө )J4C~ Akz<3§M [-;|ӳ7V,eD._H?ˮ ke(ޭѼ .׬6k2vjN.[/mCI S6bs ٴ v9>[ynϡ86bY<1;5-͵PuBA%ךzmbysdik\5%ۚUDQ*Tu[z]ZEf6VLRs%6Q+itZx7Bw~hj>|vŬO~]Mgj~;#ï\}$;'fWsv%nn ݫ ))z]gഀ@aS VGѨZհ;UO<𬧯_?FRÝi2=~xcrziSxO$y+  9FIhjJ"7:Q}C8zpp!6Wέg7_>ʾVǩds;ۊW*o#󮴗*=FqΩ<Ҵە43wXLd}|>}߆LSSSIIIyy9ŢzNe^Y@f$FH'J7,nuFS;qD_w0%!)}C^OMfwn_;mr HBBTe]Q[HDn !$$! !W@v}[_spluڙN9s{~wN ρɮ_:_6:6=PQEsQz^#iZ"!G0D7t]~u=_|h҃pDTѠ,$) OR|ЎȐMfv΢Zcs8)Rz!I08E=L,"7>=bѠ!-OQaD/ʌrH$o/]h+02íGW%tRO 2eBD~nz3,J291"ݠ 9\6͐@At@>?yw0c%"į 8nL#>|=2فf'0 O VLk0 ['i*݋T0L/`[҃ C%pa #*SmѮ⢓Q.^ ^_`(ds S/v^-  A KO<{}ڧ/1whY\vZn\>o#/˞p[̅CQspn!ܮiP8}ޞﻯ |$ZsCAJv^-Hx2&Gus/iGoić27o5WXS-/AVz<.|4-󶥰03<Gd{0t,?+۬c3jƵ`\^~᠋I8{UNBo"Eν83k5OZi9ggi7 cҬi[ǜ SeJ0{-_& />SOܻq&S㼓Qv Q<-A¹.M9ǣ{}G6zMs=u[_ OvuccmuiL]gDž6eε !ԇfF+xѣiw0;iZbl '5===i"@0>>Q481aG__Gm)COC}wnFuJͥbUGS]fEYw-W gY;2dғ%Je28\\Ro)%V!t[S5WYYT*;F}/^$Ai|`^ A>ШXGIaG㩶ϓ׬jTVWT*\<[URZЪj((=zgr|(.Γ}_}dDb}_|XBi&PQ gÇt z =f^4jW`K]|ֳM8"lݖrR rS88Jex)'gh** ( l}W.Kd@HNr>sX>*tgp4S%fwDHJ621X[Q PQ%)I9o{@\^Ԉb(hcOEqb`??^!U# -GL'ϔEB~ r8VN1k*5Cq٠ŕDz"JV"IR,dT"wRr@g**j64A`zd9xQ ϜCM4 R1b埤V^'=_ !cbcI=WJ˄9m&LN>;mU:B>f31zldpT301'!:Zl('g]Tz/`$_wwlU6zLPph-#x5 Ff-.>@(UEb.&=Ok:5<}+ĕn,BCrN@eu6/#?- .hU& Z)vPBb̆ik#kRQQ28%Bdh~}x -@su|:liAW8z0^:>Tߖhx\#ez=Jo5k`-Q\ 8E"\ޫ[X_Ad^7?:"s3ZGJ%|Zlmc*-vo@P9 NJ PdW6|?F47:)Z5?wo^;T*6utJrR<:-}J>BTvz`0?8P]>r^&W_?Q}}"Pb#w;2˂+e 3鷟](}ٔ۞ ֹS ,~kkqijS@>v _1kK}wẂ*43Ը˚7?bȗ!=Bb?U>;eYaaaMM ԇ^ȯQIϭo<{gKGO_Txڣ'3#~@bWTT2"ŃnAɉ 1GSOIB"ف~ cFAUߑ8G785".YK|x+X\vDr_NC+\?i;,D#'$V1ex #AK]LVҷL/ !`ˬج#M1&-!:5e2=b f$"n)1[1|LVr,zɲɱd X@`Dwg0lKA,Hhdo&:˝:"*1 g|Iq_a<C0N#u-=/( LA?';8ud?3\~ĉ NDGH'K$w=.Ԍq ƃP 1ƻu+_M W}yx 8 y:s:i 1x+𮝃P_9 ۖ1NϺl; ȁ 1"%cq@ d=oՅ\*1l^`ōDҕO0y|І0f)X ^Iw28P0l@k-}Zm B`4~YF5BL> SJ1n3qFX(|_6#ݔfZ@(R{(sZnrZ *jF97$ǡC=#ڟ}:`[(Q_C# NGՃS{ܬf.'Ftw?xvj4W|Y yGa? IJmyrævq;:6m[)ezg2p7 S?l\49@5ly`?Ê olnC.a{ggO[^u /uԐ$tjv* (lycWPpwuu!*(Eju5hy1] ˅.L7t1/ᷛ>N#c9^<:>l2kvlmoj{҆S5l?&K~qW'_HТVE?@} ~[[!Rs`ՠfzk1@Wݝ{|>FV(2brrffO=[7hyHw<7=11>88<Z|;RsyzsEF&nBS9 ɓΫ=n瘝 7D$;lNney%9Ѹ Pl0 DAb!a/˜ CA7qN}e9Ln`$ˁLqB[!#HKCM4 A1 9`S)`'%>$1clSb984O I 2AeCwIPǎ".QpaB&@$lL2'#IPRT=@.>1&Fc0Q3I/x {l|D;pM"he0`OBQlI2 e[7ݽ{w```hh͛w.|Zp=Nˏ],p vD79!сq=3Ŗ&uXxp-iq`$ >DeMa"7 !J`_f.ϒNZ?!|JȸjXJ_l_շ*Ei{g?Շqٽ^Ϸ+YAm9W/tXL:hnTg~wlsznp1Õ9Pi׭ͮmVq鰁j险NiwktJqplnnLF͌JBOϻu6{%zXSG҉1S6|),ȗm>۟9:\f80?PAnyzwbĪn\^5F͢K%^74?O{0=P.Os{ Vdž׻NilT;߆{rdL6x;֚cYuߪ{yx?~ӌmi^ɺ ~l{}׻MwxR<6g݀3PW /HQW^CMUջΊ}? î/mQs&wРloaA T*R99jVL&`ٙUFq@}C=|8@&NZǏ4 ڊK{k$o]vU^rOTJ-fac.ŵҮ;  ֊de'4t^mʠ>HS_GGGOOf#ߕҒ],iEeg[qMJ)%٩MKKK&@MCcRGCsgjd/0$7NdN=yW◿I=%?S")Tz^" :`ox䓏h#yd? b ]{Iu]%>N~Q 3ÄiEs/䇗Da q.y9\a "⌰3H ,eP aK= K,1Tjhvʧ\ e'QC PAqA(8aQh@@ (C&7.T&Fa9DǓOQ Ը`=$ ce\q/fbÎLJ90& 99A\DqXЇa,ݸq-{ׯ/ÏafWȏ8˼WH'@Qp hUJscraY\ ΏLˋL5b&ǡN*: ul }Q ݥҸQ}0Ck钲T,/PJ%d#ǹڢD$ikNB@FSe"!Iʼn$*}~.YФ$SȣƉ<ߖ3!CUQ&Y5|4 ̎Z pK( a8q9e҃,nrHn۪b5.t9fj\ltr~`p#K2]6U{.ϪwͻԷ543vkeFoo;?ܾe ?ncg[2߻w*'Fݻ؀"|meyb~YgNvvVvE&!dݬ-n4c)^/d,zKQLx1D! 5EU {q֛Iʝ3}Ͻp~:i7i~U1[yA@ }IƷf2`)v ^1[ Ӽl[N7>es8zS\Z~~ݪAO/p8v<G~ o4M_ӟ˙R,ɌX⌭;ʨF-V v˪ӡ::PY r)%[pa]4)4+K*Te[yGqߠ7,H|B_uv9NͿ ͢eSe#~v3V ?He  y =/eoqk_vt-huoZZ:*> ?b??7v=n x=!ݣmxx.OiKͺzll6`7 [YQq "pvvt'ssRJZ_~D}JR٧%1O >CP@G5?+$!$rE]ZKs6VMI6sE9LF#or#^T男kЪaլ5Q5yP7;ˊ(UDWd#*adi뛚Ӌ 4>lHӘ|P_[ڿUB}Y7oGPػJKΠB{/}BNՂNar:1EAR ')dz6IbB|xwq ?_8JGrA_+\9O"BcpO=Lņt?k. i:gb0t1 B1Jeb08GBu@Kc f߻ʈ({uNJQǜ',21#"*PMTVcfDEvLYf$E aܘn^N˳9P1#!fEBq1s˩@(X6$F[.eXQMGD4faͬ%#\6icu}EX3 s|P0. MM$oUߍMB9y"Di7[nRsܼi34yjjZ,z?059:FPfSbq۬ZͪA{{9߽,x}~Wn}ݥyL$_dG-#wZipyG ZdX[qoc|o,7?k[)þ@3 jZ4FMc6] Gfj%Md2:*Q^sV?*{žߌ? )t,'t4.ZͲk)l ^i2Pۢշ%&qۢlmh޿{폭-r-Or۞6նgb'v1B@ðxv~_?;n*͚HQuun' "T"#x33{4jJ}r\(YDQ11AoP}?W_o;j*MxVTT47},M{yQU ';Z6+y eE/+:' T> /V_Pf'PV_#gjĦ5/Z=*HLDdO/!4j-Tp8NWGOHqg΅=\|I)ie.pis5Yf<5pwbŁ@]$ Аu2 ;.E9@@)'"S,ES -!CvICB\!a^⑤r1`V@ Ңȓ`Z4%P)C(Xya$u(MeeEK,)YhG =ABL @ tj<C/J&h@8TR D>D0o7!VSi,A7)4̍v#Qǀ!DqBtljH@D$'!&>|s]Y1[ 7bBBx^^T-KC)xp] "aɩ ;F<FՔU1%Iݩrԇia>vaAe@ZSU"x`٧Pf#ڠR NKf^0M!;h d67֗ ߹QFs9:6޹qw>:h?Bm\MӀzXز e-,/M~sq٠}Vg9ܒZ?vt޽{wܽfwawٱ>uk mc 7Bn}f]z ZD}K=;vons-:mk YQv-nx g\7W/S{-n 01wtt+oy˅JQY6c2?Y` gVm&˲fJ5v9Vw^x5,9Fլ3aSݛѥ7*~j?kk{nn[[\A}_g`m 7_PGm+]XH{̫ 򹭳s*bsCík=cZ%ߦik?_ ~@|XLO ʡQF=h4 `j| T==8Lj[1YW-6b4t:^;E?NbR9Y}ό(?Sk쮺䓊좳gc{v^,L?V]sWU:׈+vJZ.WK۪P_{j`3gΔ* !⃗]e򵵵aCOg0+U575:tkiiAS뛲{ّߋ~wvBjo~+(n8XXRyRRSr)BLV[[{>ff4Dz`J0jB5MG0=W`Kh/vkkO"~b'dsƨqWRzU#y@K403$@! H0#Ydx$1E))2"&9Xbo""G(ŋ"S8<a)(0i+͊x(dx`^= 1ev}+޽{*|ny_I !ɘxX-fy)hy\ȍ/PZ e[4 ªd ce\} &ctGEp`Mj 0?DYz$*ptT P(KNW J~2'Pe0f$ NW]Z-"Ā0(cr%aܮ#uHAJ1#fREǫk+~N!ANzX6H^#9K*i# ={Q`ϹLgq> yVU׳mݛ.c`ZkS3c!ؐ~aZ;3_e[Q[3gِRRG &b6ton4SYe98c> ~p+ |^Ϸ9CA/z]j+sf:z t;Nʲ8nڥFiPY hK ! [(8*Bc#2 4 `H [IHBڞ18=3Usԩ; G{sKs-_RH$Ow<~N [@Kݰ fgg=~ٮR6˪iׁ(nh7he2o鶲o8+wvȦNgܚۭ\RKif?}yco|>vwq6 -U<׫f>珿Y; nY(x +B(7,OykNیt5ſ%E  fdv9nNæ2E _#q:5m\K:pQ D_؏o]wڬ:Aw-ϗ*ur(|p]j`(%^7,cP =~t:DždjfCD"br Sbf 2nnnyfZ=)RɥD}dPB>ofoޞ֪fnڃҘձ˛bUet? ]BgsSχ];gGױb EP_}aN|=Ϩ/`^W}p*,,hc h?ߪkA[YfRFjQy.-*|ҟC#EhC`9")d * ѐzL;C'pIuALHp.- F=!q]2T'dIœH1y?";G8BHPsQC>KF]dDG|T ~@؏->( O&=y:s' ?:N$DP`=0Cnrgl@&s8/2ԇ"HwHtQ^'< t>BE𡰠aD`N(xϟd&x0N MXWPg^&؃Pvm83H(+u`s+Gf,qeqV 0.cHdXnN :]x۸q0!Au1T@xb#c16^9DIv,;, QϰNd㡟w(H)64_$V4&&^& Q|F-VD7Y54@DjNK@$ 8[Ʒ2?:F"=O2jW Kffz22D}fzfqF>-eS)obT( Gz&'i|Rj*d^'HR7G0dkrJkI$ٍu?\44JśW|ӒQKĄr>dz氛:٭0do1A}ZnV2|e_]YaU<uK_;;;d2jN'/K,Y~?~t|ێ֑na2mnxv^^5ZڗbzlVf2]!9 ` %Fܳ͢kN |1~'mm/_w#w]+6pmu0 sWT|xQ>Rpϐe𬭀nNŴ8?36!\KAٝ׾X[T:JR@N7@'onJa֠1 fI8f-g@A>hŨlP~QEf ?ݸIm8˝m|^HOWW]MS[ ˠVA}U"muj^ZϋLƷÃ/ #C )e>RP4619!J\.*u2Q&+jV1Ib2{ctt2ЗySƕ?kJ||(|VrדCb22C9quycύE) D}p ~IuN|53 XV0VQQowՇذ c&'%\//.xL&S_iyu ]TPhPA1NW|F0/^)Uͻq;[q "`eee\n˼R >7N@&B߸p أ7N}w`Ξ8#U"{őM@iAAhٻ[V֡GQ@B a5$dB}ɗgbI;\ȅ122򴷧۾kKsXF5x3,Pu23 V$%e"F e@eLw% ~hx,[BI bPV$1HX\$@:%O1L}`>6̈a @Lg,QlA8VJ\Yf#ZQBhh.BxO Q`6 Q1 %rXq Ԅ2. cPL\ .^r Gƃ!TL%! #%@tP~̡|]bAbmrMoswӗuSwPW +͉G7 Ɠ`qBݙ `"VWw0  :VT]RSb$%aU>l8 `,KJ e(#6!pTvD~֪ @ﴈ%($)@BfbtEWES*qS ȸxcU/kl,FYoC6\GSUDJ$M\HT#@qٝ5ҾFZx+o~ոj3jfݤ5ѵ[`޺Äe3:kcukWs ٗrŋ|rdrr|iLNe1-LEA>oUzMQl`BGLTj׬j޽n? ߿~PZ+7^xxuw`}_l> }nacQ*&A;mj^R/Mks.n}e1{n$P e ZUO߽sǥV9io28Sww?zxi6K ޽}R1겫HS WJlVqmvSΩVuۑ8Ur #߼1M KjKΫjl\ f00pr{OFn_lu`[gmw۶ksYԳ8LWծL [Y7vߺԸ?qqV1|jXY ^uͲ4]Wn-}3n e[{sAGŨw7^l?Ej2͠AeƆ񧡁?5OA NMKX3 C?gX[<2Cu-]J֍k._k+9 ]kjVƿ=Էuel|ͿpѧcNO S)733|bjzZT./ԯ syܼq~^RiT*"?ч7[<{6N]~\#=z+/7fA}e ЋD7oΞ?zE9غ|qaę|/0Jp"LzoZUI# Pʴoݭ}MMMSGqC8"O챶<7D&lnڠ>~bwK{oNAaa#DŽ_;%'ː֟,i,njpsx۷oA xLLO&@|[!$>9D{~ޚR{2-ˍc~Q_^|g,7aU{5@<$Hȇ9X%x)ЎukN Hh%i]-?t 8uiYTa Bϕtȭ$FNkFȊ†S Bn"rZjB*p 6W]L:fBZ)"U9uTbTFPCZ)*/UK!tRBAǙ 4u%mtŵ` ]M;IkZplX4ve5Fm[sیm;VI^P,^L)^LGH}JĬbTSΌ>ervU+VӒղ_.&T\@/|FӢwgl2}6uf^ދ}gcO󺑠vif^4^XL 3 6v"":kJ޿c:3KVK !!X /A:Aq"lA’@ސE!=<6 =''<{>mб[3Swn߼sT*eH?{9yNiԔryWWdzJ8Ӹyflb~S mmlW')6ժ_xn\2jA)gqZe@`c=++ήow>|F8m[ہo5ݖnvu>2('h94`V%F!~Ҭ,'m(WoZ5 +w%oOߓ;mZ.êMlT#~ۏuج4vfyvN`sQMGSڼEz}dY)l>uƀC;; ?hik)Ω[sG=hU5w wCXlogm\N^zn@߳ᡑ 8KW >tzrzV&4EF# Mt:ƩoqqaUA!{#qO{ba[/g 5T]:{63໚SiG&F<%5枮ώn* MxvyƼ|aCˉˍk(=/벲***>G?: 5Wϋ眏kYAF2? ZaV յU7+jo'bv`CO?ۿ/(˯N-xꕜKk/?I_?$'XI&$I: {zo|7o`bDE^H'tAwld2(ő684Bdr^!Pq@: }xr0.("X(" ᜻`dEy!`QtPl(P }/R<e)cO9 i 1Dq,YAN; MⅠ/9}Xy(7$!P d")D2)6 \>80cwd 0D">2~ǁC|˜Ɲ &"D%r ;yldH& :QQ$ p΍bO!xXЉ teDaA@tȰ#5P> \H T~(8}4 <.2bK/0yPbtP}%KG',aWx +`Kdײ8Ⱥ04 D\tb9:rxJ ZaX`zV2UB5ևpV@`$"s-O \4 P AA2⣬&OX$s% p 2$tK"BQsiacH%vwj,&ʱ̀s ŤX^W-PSa:mKVR-O+R̫ؔdؐ|FR.(&jN33'U۪ݸbVBK&fQg.X<}!od7<zׂZ3/IF^f^ixVּD}fUO %6ˬ+*Ӓ0y&hPT\]6jسf3-)^ok?z*MJiOSckk/O{_k8 ӉCGajz8ݕMz=y 3`^DnqQ,=?A8t8*J-gy\ NTj?~PSP|{lp`m37[Zt żSOiU%Ay5N(ln[نj3dhtZITrqgD;iv4{3Z؏Cl9^bV٨X1\3@h kv~a[deG]>Xyl6 GukuloQje=m/56D6~?~caq?iwwg30&J_IߩO6;.Z1DFiFrエoGG30  A}Ozi-+RnuvRQkY%i1qA\j|xzb]I őm̉i5 EMxR&sbsc u z pUYYpD/'E"" 33555 _UUfU5T6T /(@0oWA2$r8=,_O[wpiT*I ,c ;-@@ dJ4d)m$a3bjcx`j :}k0e&ՌV3ss+ϭY^^^Ss1/.NZW[YYͼHsA|2a342 T޾UG5;]mmKⱳ AnY,%CIh0!üPb4+&bO%Ӌ8s"!=P@5\1eaKKC<0SŰbؓD;i(yR.{aLAXq$P$aYQ0px<;d12'$Iww9 "OƋ9(E1uDZXhG#%'EQ\&EpC1j3$Izaa6* F am Vԇ#C(_`HA = $ā޾}ۯGV~rDWm+‡H9KӮN}?l`?y3,֖f{촛@ i_Eu۳^7LezSrP<Q>|pBi5*~IbIՋ5Zִl/.Tjд[Os4Z5 ۵r9^iJڼcݸ0?j3CZ  jց=m =qifѠnEb Kgg5:6191|p/:uSNk˷:ݜHomY-˹vڝxݬ?Nyo{n@<^N$XvW1aҚm޾SlI}?R;újpkCu`3/NOйuۢf~Lx273v9^]KEaN>V,>4tĿ =r1cyqlҸ] ?֦ùaBvY%Uy<~Lo%n߼v[V``@ΎϠQk[[ګ{ woW/q۳?U[6ggg>ownv <{;440 00 C}/Tzʊ̷Bgvaa7;3R^zyIox_t^^!sElN][ѷnɸQT j+ɐS$AR2Ik8XxBZVŷZJtƶHK$ME¦bI˅k͹ To$v* Sqp$ǂ|"yz"jN}nCSk]Kǹ܊>O >$#?}p%")(tA [)Q$4iد0a yL}( @!i2 0@XF6;s[u+VEC=&Tdـˌ[!BhW]Kr PW-Ү5Klr u٩pݥB LO[SOAh}ƹ+_D*B%-P V@,~FZ̑+dZ=;:*!7K86zVk+^]@33fc0[Vl&g6jmeZ_s@#i[rۼmMoXf^C}/JPH\]CʾE͋ڪf8kZ]5̼R* N}f0ۭUb7Qy_z͑9ܒcnN \t-6_f?h,_`>COMش="ĥeЬҀ٢ m aO eȞ ~fʪyuqޛ^pr{.˾6@}.*ePncnv\۹&|vaH/ĞS_:z{r:YɃΞΎq̴R&/ A }]c,&ϣpA6% جfF0#N45=oxo~$i4QeʨXhdGfCC} FNOcAq_Q'8׃> ?e_W)piV6ݝ7ސ[z eQJH'Yݛ]52OOT]{+Vvmf%^%@bhZ<&y\Hhn) c/^3>1>wf$-, zdFckjx=*F45-hmks(v= BeSbOϊ'f%2"n9Hמ&Vh5*5U5}~+JT*{cBp\8xIY~)aaEJUc^W~_rEQV[1:pňO=s$y-s-E*8eҌR6?ZK2?PuįS{mBZ. }7t߿__[St%2.e12S_^SSSX̿s,#?%&q%5./|w(#i*(k*lᕗ|0^ee Bz]yY5XWWPiB /f4b1:K(8+kֺۚJn|4Nc@Z.V\N"7F"lIq18 I(*`qGoe'B$yssX 1^bqq2 lz 5"cx0 @c]Lw1OH qA#+5|h_'68!$lfBydsH#>u,SPOHxh0${SNGH2'mtd0ND@A:y8ag~8Jl8ÁO`֡$Ã{ N$~b %=eO˧;/ EW]IFo뒁7b6Zk?];x}!PP\Tt16V !h4W@(aI v۩ui!(Š Lde FT~)dz}8t X4PaJWHWOB23? @k)XO4]TX*`!=F&&qPh)cc ^! ˢNDžBwob{{.9(E>`/u7={-!}36 J!] 榦ůfDfn$gERTzY:i6 ͼb+ҬiN;FNIK6\O.iedf%"Qփ+Fj7ei ;lz\zte"R%UȢ,CAv{mUoP>{joFKn'FI|8d~{TVk4JyH_wwڞռCC%Ѱ Fvwv{BAoϣ!v_[W.,jwytAf;bG74?P߿2xvVY~oѰ _G*lrq~llz7#5R:eSSAagzS/j ?O[:7N^.VNC$/la5CNtM;:@^ +c~7e S]O~JMclX-w؋o'C˨~١|T~fHS켇Hz=ID}ݿHįFǡɏ̈$sRL,-ag}&tz GbeE8_DQ`T =܁<&l:U>(k]T/M0SJ&W%_b&Ma( HX@.Y Fp4, CP?gyG!C G/ҠQ$M}8qd2ٗ_~/@.7Ɖ.79\Sȡ˧1΢|IfDazhD]'J2ʳ~h" 2Ea4X:l8@3PztV`K4c U,3[%W15HABQP t< d9.{`9,2x~`Lm~4Dw B @rIlTZP{$㉰:k.7\'xN8>`I} pZ|vk෱h3tZՌZ1U+cÃ'+qq$SŹ1ͲH3͘ b{a_g0hOx??so]nv\sW;3 1r#(:nݺف3[\Xhk7 iBv9;ߟ_tߙ׎BY4zIXff̶45=}SZ`oojM/,>k}ɳi8}ʿ..ivU}hGM/#*[m;.۩_YP<٬=hg˶ז̊Gb|`oovζgݳat/Zx+/w?>S=ij295_Z]" f4J]w[21PVqzcJ=;]~})>gmYP޼6[C]Ҩy"vпK{lmZ]NJ5}n׏wgO<6pS(FG򑑑_|R~_ZN}R9 <{?xaCZYuiuiT{Ytkj$4Z_ΫΎ(J / oW [$P_"\*I}&I|K[`< %X} gAKN' 2TWW^PUԷ_}z+_+PQjfynBiCiy]UUbTZYQZX+Ϊs|\{2(GF/8 `DZooO3wC%Y^ (#`8d1>6c x"INr"+zΊNf'a Y4 8LDHhtR1d&`$C) Hv# > ȗ (܇fA>nJK @g"Q~|`̗m 2Bt$~Oae=aVv6m őpxVS )*TGB5Bn z_8lÆ[$R15I^"FPD@qU@ٺ!0ic9 Z̎@O}Qph]"@wOa ŭQXIaS2 A;3jbvz_-+6fp HH6cͼl:-N+Է@}u+cñj,//qR߄|r\>6<4,{.{ܜfr|Ab1/;:ҬvzX;-7kv۲>7V^0y}>V#-Ce oD }ߖ~ @ӆN4?S!/Z)6 ;hT*4Y.(v'%I_ @iPv۹kQ/2Ԗ!9>A'=R m7R-U <anDAˡSGYMY f8KڴmUV+6/PݶAA} =/zMN4;400>EpI*ɧh %1%X,P <7 I s~r~D\z.H,OEB+Iog%& b$gxh.$63xZтҲd؏/} 0L***ߥ0A>\_Kc~ԗt3#!7-: ꫯ}pqD|_t +۬b{jҜU]]]Q^s96O䫬 _Ż)>?~gCL|{SP79ʗ/z[=vk+k~Tp)&L8= eD_:[ ! u .ɛYD9iؙV8J,Q! QT}\4M6<{O_O; ]"f; fF]YP@H'v9Fo5VdkyX3 #8<^z\d*1t2@+ɧ$!~f`.23F\K,"hLR'0rM]2vsBzĊu8a~(2GP~.#1Gq* ;Ehu<-#Vg!SYjYIg?Ժfh1kbO$ fߍ \URX|-C]M-})%?31/NaTU1̰5׶E.~uWQ5p5v͝"VmuuMIIIvv?xק&;E;vj*U!@  (bT@A*( j#Z]Wr w\r# $.|+_{gvfg:L̏3<{a¼“YNW#`hr cw˟5656?e-ge\A]t~C989y)x6;V^A8g!&1-IwYȥ62GDzS4$1IBσ<Ln,kӢ!_< ӧCj$ۗ̓S3;`NJ(Pq ?!CuL%>f5jX{Biʝp:\0ݓN9ťmsbTDŽH ed*&D.sݑh!wC)NLJ;4DQ\'BZaJ p;DDq Q[0!^f ! " |E I*犈qR=ڪQD9&lSLmDÇlEម|^AY gX\GP 2dK`2P\2<_؜Ԡ28P L"bwRv lUV[TAŪHtL`: I$>:)1/YB[aDʘ](D~Q›dbl<RT+Ӏ3tf H{Ў[TrW^~LKϓ**eZ\b!㺋U=us5?>K˞Wf1 zQ_oW`'ͭSu߼a[|-`N_YjrvVύu=YF=n9X0+DO_׽s8jLY&aďZߘ_ʂ˿[__[~_p%X qL9Ɲynu >{0Coj|U'GFqM(hˍׯ_2ƑFzz֥W/_yݖgX떝X_ ficMO7ƙ㵕'y/nNӷׯ]ɩAGyWv0rM,#b'юɯ{g"gfLKWnnY^~l͵VG\I;v&w߃$OkXN>LZp9Scg.~rݻww t  F0M[%1- iLO|{zz G}4zؓG˕W*rBIKeRusYJ&hF~8,4/Jh,Q7U7jP_CP_C"BMAsWSSs@u)wק&W܌< ; _ANZ>]UWwdB>GgUݦ_}鷛M1UEeT=Q][QQ]Yi~}mzꩲ >~S`FyIAR_mm a|bSScsSc}_mUYE9 P8 9dah0FB(ER*`,E x,[d4o\M@]@5yuvxO"_4D%K Izp.[Z:L&Vj/%'0#14#q'Ä{բO&SLW0ʸEHj=YrYlѩ1oʃ`HƄLSNʲ8YzkFqPA@  "d(TlKG d3 $,/ / D"=SS5N];wKR* ?a>iANƨۭ*3M6b6&CU&&@<_L >mzEwl|QpC?YCH$P6^.4>|78εZ:}#Hdgg' xX?s;uA-y pfGCN~=[X4:RcbVT jfއ[[X*@?YiR04*4SbnrwE ":L΢(x> |f;#hǣb#AL\$9;w%ǀ VG<%WLwcğ3(x0HLq˔GbRu_P_^3QQ بgNK$S_ww7<['ݤO 4 9 GˌhTn^s b+=Ì:3e%5$%rVp0\>:<7^O4y?SJAq`*@8#`3nzBIuEIu ~n\-_5 (N$ذOp+ 9 K 2?L Ѕ%୅ @;Ȑ-. *[<ܣGע((NL\b \G8WN&z({S)!',Nm.㹭:N}68X 4km>5y]  ^'kcu:[ղ¬b^N$sS,ƈ'bnJeg糑A;٤g~CX`m-OhTsV3dC*+beetvTZz^8tBVӪ)KEbx [z/YF@%mB/ ذ,mͬV-*H7YkQ֌8m˱*liNx_ L۱8hF{VaYq5p94nF>?T*5L>+j~ص\).*=}137 SV7#ߺ@۩iawdsvk j,k~7/zQ]<' L.#ۻ`P* J׸ڊ.#b+KӪ @|Ȑ^8`A^_ky N>/]T̸ݝm`}Ȇ!CϬF1oOv&UUv 5lCPt0AkaKৗ=&qchf=gDzd{_/Gy\&?%4}Eӧ]z{$3⹹Y>ta~~oA!fW@>NR)pA}T ǡ> [R yRNzGuV&uAg 2cb\k/O&KZJV~u`RKeNȇURR,}|S!#)^R^ܭ$QtS_R^FB^zKڵk&~]˹¯~^ʫ(h*n.ւ:~Y}Q}=J>bCizzsGH MدAiX4 MOWMTOw*aM $,%B# :((ҍ *a$!ph\y7|陱ڮIu|{#I".,/ ҎJٚ%oEلĄ\ 9$#+2z9{&KXiuGLH/fأ͏dB@%`[qyY0AxQ,pGf4,Ɍ%(LfvQb>|H.ƤD{dHȇ$zĄ{Ttk<Ş %Ar$4O܅L:1)]nB@Gp @~]\Rc#Rb#]HE=R"0G{=o^}߿-K.Mr&rڕp @2.2(%7IЮJZrZ4&{,qJaZ<]Q u_I/I?vW]A~c)u] *a9t0NU &Be[yH6g")Q cy QTd-X!LTYATIqk k9BІtv,"*"fz *0V/8G3SN YY7[9clp6p:x ·4`OP&a vvq<̲Q,+ q-}NOWF_ҨRh8T?g?cu᛾׮t)EwXSy]F_`6P=}}fro/w黻? vkmimtq`ׯ _[5T*?oY-~:1؂>o`3mnݼ|;o޼f+cX3r /vB/v5f~ʿ_ό2 i[mln~[!&t*#>ʒA3~_ ^~b'(n l]]\V__^g~ N/B/=['iǛ\_[wu[6Nŋb`#ߏCHe`\ 1dK(Px^YpB>utW]Yi ih-KxҒNZ0ԧR>z4Īo»s cɷYze~JAOw,K!Kk28P_RQ qNS!RPk_G T*+++**X}L}?K>,DZR.VdWEe]TJ3P)_Q$U'OB}9r=&AOɧ(顪rEss5uN!T*AN5~H>V}W\@{jjtCV Pgx1__- ,r*SU)aX$+gChZ &vPbG0qZZXrH]@xFKlpZ,fP!cfF bEq@,+`@p rC~:z)hGP1(=(!I 9A Cʹt-BabBru( #CA8a N?4݀* v|.q`&~yQɑ0m ,D,-J)DÄXȃ#R㾄;~ތH`! 5!<^=ar?O7ŋo߾{>܊ "(VNu[̫.H_`XJ`^KBWy(ƫ)L`<'c /%6%W„D)48K\w[{W[#",@ .~@#a^-Y˭'PI mLxM Me!OT# :dCKL&0 ++Xގ^!n hN.P;R&A 1Z @w|C΅or:$'ؿ,D?s)^Nx#~OfwN۹Yz;!->^aпuY057֌M5 깙'AԶjxP_( [cJC;O5>ϳg;[^]H}|С6T1q¹fp3fAst ɨ lx}^;E}RRiÏfsP56M n~??ymCv*u:mj*KkT9U:t,#@H(QAKʒBBvbl@@|uq쮙WsɩSQɸB9ZTi0-W hJ wdpx%hσ'D}yC|ǽ=w/eݮII:i$2:%y  ǾW{"[eѹɭ\vV{uf+E\

8t"#?\Qܥ la]&|>[#-(r;mfNЪJxB.2A=<ۗju39}WI \?'y B>aC~6=%ъe4-g%۴0 Ome\2h9FU=kk#KzU ߪl:`ؖ=DY_aeZ_˃YDz/O*EeY5Ӝ2:)?Q\qP93)I?jjnjh]>?* yD:2=#OMIAO?@ :[{jl6GrHp΃aD(P 7d{i8vi浐w#Mnc{kkwgj>yƭGV "/\3f&ǥ_\,u>bx=B߄_C]ֵ>m߷X"7b)o3>ɤ'd dRj2D"HebT8.aYF|tlL | @oǃߪ/?e߽U&Nd@>P0CZ|TуU'VYvfZ[Pʾz5݉1yD"!5Do\O)Ta!N(*-ހiD0Řq)Q%MӲ7F W|ܫIM2eTrW~w~[p~lޓwr61}JrW\ݹZW]qo= 8*BAݑXn_Yr|yCe2CA2 37#bCć:Nf' ѐ%.ʒD}$%ʤ!:0!oXI۶Ee-eQVb7۷ ǟ/` k ԙQh GYЎ׵ 3D-Qp TDܳq±8xVmxR܊wR/B9 (i@FxR8pgafTA![(CKv); ry`OS:A^@±D# G %\%*xKRZcuh"T*q =Ա_HVq))wP rÙdzH@A"9_&?^: ">i9Wb߬.[Y!=7s[g]q.oA?s''&Ft#mKdYb 55z;g2-gNs-..[st^õw.WUNO٬Gx[ ݟ옚кnjcfd:<>OS s^m31M3n_j~Fɱ >yf{G<eaީvY:l~cI4>Ï6X0ևo/]pfǷ:[њ7>k0yPb2 pSُNe2߻qb]SSԤl7j*zaUEk&ƇS&kuu+++Zo77==c`sI ,Y^^t:lv)矅/WWwtji~|ow޾zV߻wo޾͍OMN>_៷z:ud˴~=! 'bna~ ?ixE3 >3/Nd@rX߼y͋K7l6U8{%O;~;3gx;hzku ,.9[ysVߗf;#twέGM56H}Z>muuhti`wO{Gדֶ'P#4mwOknnF}ߗ QC}{ZiWPpY{$rqbCqRCqrtXb 'rb /% =}(b^R_ϔB>WPPe}A}~|! Օ=0+"hW%k*ooR9̤hI}O8z(;-]UNJ<-"-%`V^ꓵ啀.;;HM=}𩲲%jgGPutwIkvH ujQ#NuA {i Mxi|R7:NwEBBni(I,Ą Y0!@pD'rCe!TncdF}$-y$!4PLFYOf AtQB2b6DFꉒ-r( x;$nrx 1Wl FÇ"Y$bibhS+qcd!uDȚ-_ ի_P?|?Q_*"@ j> y #z҃|bD2+1!Z`r :uU$7+YrSV#8B3T#GeJ\$). }OH%I؋u9l)? GRq(5UIU xȅ#*%@IIsj SĨf %NvR9mUtDwLDž2niu*F|-YNVU 4s:q㠾i'ԙ/؝?vJRn bH r\JB! C.=Wꎝ3y}>9|jrʶ|&âF5co/&9Hߏ?n% ?/v;tVZש^!\ćٳ{]m]w%1Y5|;Gx`6Ǐw&yvj#M>1x3VwVtRA;촦iꯩg^Nig!ژ--WX\VFlr l6 |>Ö2`"d%>9P}>we/;e&d$쒏DZɬ b/e+-`z}MMMqqqQQA^^~#%fUV5E|#OBz"hbBDG`O(1ӧ#mM _KsS+IZ]%<-͉c`Vbx%Sb9ƗQM8HÖd%5泌(iq@8ԘO1"O Sc( 0.hBB{b%;J;uNȮ|AFJG ghhh||>_rc02b+x)&c#cc{G:JzhE`<8ǹJ^XSʏ#X.N*JX Wx a Ae!S:HK4"YFmIɀ4 `GDYv6RS_FIM*0h5UderǠJ2FŌzb4nU6j"6r(!pnuWpiײ;϶R5@&ݓ[V2#RE6â趛 B~pqQfM 0|iQ3gi+~܌@&3,k|.a4fcV:uhyŠ]EF0%sqfYtu%ռ`[LNa7-%z" Yavqw:uUVBLs~hqfJC hoƛR<=.Uzбr,8JY{iT TĵdpVMP̔TL(ǕC[zzz.}qE=3-u/>}nQۭ%ϻr䜷z34?sx 9fA͹9^N" faL5zwN=G8sup{ʹ7+#j7~>[Xc1 IαqMz$v+Pd,vc06-9f*{ 췈)ښ[\lil:ͽӼ~sNQk&{\Z~ o:.\h0erzc%.~JWZp/O HwozHA:Z3~> XT 1=zjǒAg-j#7g؜$mHtϴ5Ӷszl<\ ~k[{LP'~X{o߾uF{SeU@z3PJSCŨb``Ν;7{{ot3M[ޛ|^e3qgҎ*QgJ|JZ/tGȮ R"[J6RyJ;;5|Qp{&_ss3FϟD"/2?*!vo붽_}^}exoA9#S{AAƫ)@~0$XySY5P^^+'Nde:PYY_j^ ><s|X=R=|kW~lֳֶͭ̎O!}E#  R#s$4TdWbaZC+"GQbQD# Іa4yX̌.HKrdy)4[sa9:F2óC;qUH` f6"U‘ y%~HR9נ(L]h'JQb0XG DS= PダP8>(f0j'/@pHҋ& `= Q;c? :*O3>q[C;i4bG,pgU"v` ` mB2Ig~* DssV%X*.|uzK97p6NX+4hhLi"gJmSQ'-C:gt _R?v|4?19]p6ӂ0LPbB9 fh4jK]_}uZwn8]k˦-,hMnμ_[۲+XdaQ νQo?yvqN1bXxahjB5`WnEj0Q-L7 ܒGp4ͪX-:ǂ} J+9]k2֠B'o\vlcBg(ƕxi^8lvd? {;.vu]1__uT̔fjzqZGiTvE&*%dƾN0@@PA$d',AHF6B D¢Eŵ~9>!={IG}/}2}m򮻷5Պo/;epص+[.ך٨-|ͽ}xxVq1Okcg{k옭ֹz -m٨[6jV/5* ˡXxǹz`2*3;WoozW]xFV76߸jO54&)MjiNn=:WVӬ4X5޸~ix=++Zq0ޫ\[4cc>xO* '&|TP(@gLȆ'|H,d2\,%x'] <"ZhcQ\[I\kqls4Mւ`Ѐa{{[u"fRqz2'37S#ǿ);vᢣ a}G~=rN aԤWUUa'b rj8ȗ=|̴$˫nVWR}4ʺE"!ڱQ>O$P%F Fyx^㏺474757546k[{-$+  xFvP|jr{+WC1h^gƟCE.%8u o &\ ஌s,ʍH,Bt@類F$g%yć `vhIPt &L%!9b!C/%,T EM;O,%_$ r1.KC` g(ݡ,$FcٵjY&%B} JrZ*NLJ'BoTR`Z :P$WV=xrvoUdXyy&dAX-*ö^z3}n?:$qj,&%>JX8zNp؝ h6 *ɸ`5YTk)ͪPif.`0rZaΎ_ C&ô2O5o%ݜzAL~ [i5fNAQ*dqSt9mզQ?֦AZfZ|_GM wwgZS*O^<F]/GGGyr\ٸAŌKm%1M-ŗj_[[[mmmvvvaa!A}sGGG)@2NTݑc+.>zQxPgY'dDQņd{wm%s5l@}rҭ5U5%T}e1<< !&!@G>66:676Etooinljomin{ 姆A$hYIsdp`^j:ѐxFu 8/\O w%~(a5-MSj(V>Dxdc%2EzvJaRFHABmBQ XaTFP_J? "Hs\Tz|w~tζ3₰EH BDDQnE]xtq$!ALBr/^GWYh;ty{ys$w2AW^Ǫȭ-ZۼX<"eS5pʍTp*I(m-hݙQAw9(@YG-+B2l 9uv%`mמBojyȀv4&hxAŁOl2($YbUq1H~艆u<xQE"8e1: `}eЮ +ױB# B(v]K 8Yfìpn#eDdx\H1P<tΈ]Ϫßq[Œك^{q&# XEؗyNQYL Ӣl^o F^[\<$W‰EKz{%޽v.Cx]PЕLWWWWVWӮֻE9c{pϣ[jT9CA2dlP%XHTgZ4hk^R'adlx, ^\ۅ!\J/7yݦeֲv;u{o4JܝoNݸrnKI,8;å9.= Pڅ6ۼl^I= cz~!ͫ'p~l#6\}Y9mw#䴝I N[" YMjvN?{jқڂi_r0)?{@j3'(1z\Zg)cG?~o+L4rڭJrrjkߏ޸11~P*D}*%x1559ְݓ/f Ua2 J)@.qbrU>,n݄nUȥc;.tη ε K.W"M^Qi'mJxgjAV@Tcccww=; k d{Yۮ/~'>~J_7}?wعN՛(konp>8xۻsoaIk(XWWW[[yh׎#Bn*/?\]}`ku{K(d驙iA LCz>ĸzOzOzW*=%M*&ѭxlԁ՜=9ʳa3ʂodm3 1lAhc[vQ{Hn2X!*`ADn;ijJ 8ZU+2a!,kK6v$YD+̫ݚhh'@ ȇbTVodl|x#\J~2' =qAj ʋHT[FvLa[PB$@Q~Z)o2b:WMoR{`L 9i\GeLB""=*C'OH Y 7G`f8Z//yGjR>H}Qߋ/^|?QnK0 kF=y CUIPWb-ny-5\ vC}u| C+dG>TET\=/ 2ܙSG ĸHjnZJ B=pQ7;(z%"V\[1$; (Cb9#ZX{ %V/ 1e,qDbR>Grm([ =f! r8&m?XG(:s 8C^+Iz>SĐb S\gg>{c  a7pB>/y?kA2/ ͬZ93qN![Ԫ _L ø\<ExέVVVNF}luo9 =լ{~{dTu3&߻ :ϭV+gxG{-c0XZ]q8LD+CGQvyIc[߶hg4e#bKn^dN-߁!d 4-?9t̉+CC_Z\/Joݑ:+ 0[\kWOQ#^OMeY驲_md,@*;eij*vYd ,D@nDzKGj^zu޹8Қ&0MOFaD%QZ1Uj@Ϧ6B>=]H:Qjo_%H ĔB;% \K|"$-I\m/+=t煇}__3鯄H~lB"QRX R)Vf_K@E"W^^~劔ϯ%E|) O슒W\\ ]| je} ԓ H􃂰G}__e}]]Ojݸ~8YbP70"W*&UKQw63q)qYyyrp ңCD19@ `Vؖ a((JB$|]1hC1T  qH9i dTB?r!4GH/-)qILPK%`-$ g! ] ^¹Sc`[8aĨ7T;,D$9$Lذo96\bL}<{<*O@Gv"" =Y' ¿ez<:k/6D !"~yH"ad0`407ag c| RevEӈoI @f#( IڹKL$ba8"C=x4gBeq`!Vz1RL]}M]S@:`Z+ʈ!.]K8˂HزL/By"GFa^>&@b$Cgf VUnL+o`E\ CLػN[y!G HaPg1 H/!CMpFwJyw+D7h I8Ky8;*h^+'~m} ,6oT}?fi겙܎%0/MnR uƃ9Nnv\75:9gxL]s.6Mg6韍({F5CNum 9m~yɮQ4hj0ʒ6Eu5 6>ڨ&(Yn\oGäWY (  z,X853~v],fGpXn|x>4g6vvݾ{Ζ&iƇ:3qˢ,jۣ!pkc+˖wu[˞W&YY^-u[V]byww5zs؜9~{7n^lmmpQ#~ak VHo}tk+v#Oͦ-g}ٍ̱:R{;7W: ߛ3ѣscw憇rƺdOoGZ|Jէj|H6ԧMz wUf&#`B B S 㻥I;u#z:;+S7Hs|Rv2!O? ªˋ/`̋͋#8JQ!tj?|u T}4>ABCUG^=zPzԯЁ#~%G/?ͯ'q>E"DLjjjJʯJ+*ȇ!NRT)K9k4)'J(';a 5\.Ȇ=!¯w`#K%_+Nѹ\A8]"Cp|/P3C.BxˁRߪ/a4Hub93xhNppX:oΒAG|}z"9z Ϥܨ')Q 3xg)0`TH@a|t>.B"`83vpjB(l/4h{GQw!q8L:}"4cԇ<960%>G@ M: j*K>oU>/SeMM:Ш a@B(b- "MMۂkD [B1 Wo,.`W|9Wqngjn}u,oudB;oYZMc!2Y|z* !:xchH޴o`!"0!&J܂/Յ:||^~gUᵄ XUUVy!Y܅ 8 w`pf(15&kKқ6$2D~t2, rb6Dq5%rDV\uhk*36`,RrsVpl$ "1`0X de6~(2ǥy-p9ԨJzzM$~Y9.q94)-@8em6?mP`V}}~vP7DP͢OqCQg6*^փs\S?߱ѣ6i) ^$Vc`CX$h;pJoݨ)?ԧ:^~pDc~J` B==Ž ؎pZg)ujʊjDŽ+[<_,//}BΨSkǜD8hy2Fg#vJog~rH. Yf\s3HHb3+k>_\2[Q4,N5Kt"X,-I2ʿ1όXV!%;wP˗/_zQ>jʢ|M%V(‹oߥ#m_> UOŮKBpj(e`!2:$w",LnlA2TLb-b1vBsNC5,`xYMel;Y* vl?u~c QOza 9D@YZks F 69N `9`C)8!=SsTEv0`#"GClA}tZk'rݸNt[am CLd6y.:'waAN6**$^K?]N5׎&h@ }fN!c<`ֹz^σ:Y!TraTV N\(lTt2&yHX?ݻ^oX㢇wbkxڔLC_Ϟ=[XX.FM&SWO?]mWO 3do? q?&vR>../-;;& e_Q4u{G:Q;<> x&ӌ=/ۂ:/a:/봩,)˞ꙩZE !!!'" }1U@pi{l-Qv$a K !,! l*-1|1{{ԤNݺsO,m']?rg1hpUf.˾zgP5I7@׆X-<{}4-[v׶נ6aKscˣ懰tg=.uW_!>OѪ5gHu 1%_G{;@B)GqkˣG "o0]U[^Ja%2k9033޽{Zz{T}=*Uoe!_gGGV54@j+W*J3fhVRhv;Gɑ0$TI] "SBPJKdi @1Adzl0. Dx> N5`? q#Ap(DY}H=s I2eԇꐄH$}'1/^p@FE&AL 2e@g,~0ϋ \o1}?@̥\a*1zd 4Bsa!_AҐ)L1 )IBH,=KPRLVAK+2QF5ȧF(;@CF$- $lヅ#a<ڜwE.>GȈNU@< 0t[ytȠ䅌RI^/Sx ,QMquE."#\Uz33x;+ݼd]s9,N2ykKeLΥecd,f- èAu}:MN3eU(c&Flnnnj~xq6@q0|em??g Cb dr,@wȃya}ww#X1Bۜiu >-+3cSヮC}|ßfKttva}[h`?ubzraԁH.Zuzmw)*yxft]mO X 6\绛v ˵sիU{MMM/:~67f2\p[K/w^<[^MCx?a6ׯ{?EZ|tov+ oe\Ǎir\<toz½^tWsIh2g<5')^C)gW^p|ѽn][^,'] ܔyʺa0 6wP_kKSZS}wi:v`Oݯ2~##EPJk 0h@SDG{G[uP57P$/ nQ*ED8YqչQuy}A[VV&ɀrp)))%+W(JaR}#ၗ, R\.ok{j `#ཞttQsxkU*J @$V|p g.ʏzD;$a KxL z&AYp s"I9?%GHI`9 }`TI`F #<8-<$Oʿ D$4{2%Q)o{|X>8 E[po F18ćXJ^!9 j3,'"t%K4qysKz('Q/0"4!4|`>遂$#5 { 0.7dX's"@ }|Ob21͛.GtJd$u1>$-t;"5`!!l^g_Mdy9g~ynmiq[]:v sU2F|Ӯ'ߩ>7W81h,&ShtZV*e}c/ nلGeG|÷MmJR w,-|{zZ6+nllrn˔`kc~a1Z.!p#En%I}m(qF]{Ϳ`hQg)RU gX/GѥȢͪ7$mb_!v~=Jhp1dK߂5'> ?ʡώCbee ,6Ѕ|X,jWiY6ƕo#aggʺE ¼׭\M(H!ߖ皝w?{ MţbѴxfJ$PB.yLe9\"F72"Rߓ>D؏̔JbbuA2yɔyjp ζ4:VͥҊi!pRse1_O;n~qg;x_|tӏ ;/ORXYPMNBawpԇ#C*. ZQqFZR ^yŵyTy{>< $33@{cx-!G#p LWHCmT Ӭyܔ`?hN:vptFQ^J.^J5[|h7% hmMCowەO[py"3nCն=V5t[ |fr r_GhJ|):Q/T|$\x"a*0M!6B*E|Ű}_\vQg-%:SnN Eb6ƶD"^OMyN֙N۩'*9 p#& 97HW`ڊWabP$p' B&rC,ԭGgttuf3/H}7^ Eg ὘ cb1PPCjQ'>]s 5ViA>lQVV,So@X'HV{M}4Iu::Or^?|cR KjZߐ.0d (0yCF$Te :-*$2%2SejgA,Aq3GT$3Bw1'ERb[WJ`$ݒ,:F-#`0T2H0xCP@7y\J5ʼf<1J h"z* dMPINCRUW5$]H>mX^ @{q#"Hp8JUKjmtSPgZM(\Uڿ;h7:@sNouuoini{̹\s̄s66b쳘ozL#+kNT?_ۇqB=?7xסk-W6]<= S195@ `t]_ZZv{Sݻs? Հ[`oqaď/du O.B}s.]< ~&Szu|z=晝_\Zn cm=6S8ĸp5^>cF}ΝCǍε'O^?q~s y޵½̴~co0ᙻ{)sMO;nqmT\TYwXRVkp=[ߞ>xsgگ7κF>tmmon>^s++KO藭Kߝkmml.?V/dz f\Q6WgL(omOyŃL `=~4xcac}n6V+ӓO.|f=íoA+Nhtͬ:n=y?o`7x?=oK@k.qaw@ 偁57\fz ~}KOϝ>kgFmCCCNww7awfÇF"]5g 3h,W-p'#WxZMT@>(--MVVVzwdИѬ%'kk߯7XRH=2AWKM( +㪯>rGϾ&vܡ|lj1$b.???''p+<5P+3JK>АViJ^QQo{@Gcaqtث9QM9I MdB5 Ø) ɜ<BA$e& J/'hJdXðԓZ :&LZAd&P׊ D/g @'rG{5i`O4# -Ze)*Y(%|=#ʄ~P, K:" N )~h 9P8>Q2QF/EޣLB$#|Q,e(?$}=OL?QvC}B~ ]^a9,ы:^^ݜ( aD 珩?FwO-iCM XA[h!AkE30z2$(S,'޼BP^&LB-E pN/H#SyHC1u!FBSGWk/)@W29`adv"+uDؚ٪Y_8:S:""GD':܇E !$N:7 W~]]q멮_?$ݟ2v:\^BMTv!S\+PRy@up "BJ^ `kIdJYe,豉9 2/_:!# 2$e`2d H{% .@>r4@9P<^GQLHS6qxvl5?Q"pR$ ꓈ftv|^:<ܺv @R4"l6(i|-ǓPrij13z=gwv-XwY^ Oq=nf"@P+5ˡ%3o`  sҲҩK"xdGDPzYUY]Amobh|xFם c*ĨSX,ӂ%XV!C{ nި5^H-yy SG{Vgz {޽Q ".nXp\?߽.<:^C$^]YDf< 5ܑ~N1Doě fS+z o KQhIpE 7GGdԉwG2F#7'y ZP8D>/ zlm1v&k0YjKeL1q6>ۜo B+T$h z!>|Q_9~|ٿ'OM73X*JR&/  wxO.Wȃ?ި#IS{*ћrWRvdj.NȺ^ɺ^w䃅r lK3?fa!}ԇDӥ9yߞ@♼˻? >jqOq_>ڝVÒ?~pߝ2S YC5A>8٪ϯgkkN+_aՇb&s"fq@DK<#;.}>>5!Ľ{wZ[ZZ4՗J`%.7iJCeOM2d}{kh bRG2#sd1XHL";IMN}z.m(HtbB}v;ؑo;m@I!'q8/^|>| }+H)9L@WٴS)RN&H~(KO%`)Hd0F&|>BAX BB!5PڵD .+"C j` eAk8l ̾V$`B#0x2fߓZ2~Fc9 g 6X !Whfp,0xÌgLLADW}? +$^e 39 |2:ҭVKv yZd3Ͻc?UȄ; nit]kkk8 >z6 zAS:fQ5^[GSZڣݺt~f3/̝WD%3qZ@#7F moqiiJ]onn zL)-9r5i5 ⳺yӆZ #KQH}dNں88$d,^H X$$&@2 Y؆6` `K`b@f<~9{:o:/wtD}Yh&qH$b\j5ȧhEUBx[}8$&,eBVrrr5[xx^o&MIR@Y^_xqZFeE+rcDiV^jpM FE@h(ʲ!(Pz=D\ Q/̈!Ѐ' JS S>' @%uf.snǃi9FŮqTSUiO[~}}։s g o0\3c㷧'ݎ7[y<ٳuἧVoi囖KWޥ'4_oZ:_Nrό.~Lx=PqGuZZvg6xs64[kJGly77@5] s .76)=]v9WV;;;ۀp[5]N|wݝͭ-׬>6:55 ./Mk+[ZZZpNߎx֚ǵO?Z]a"r*#69|wg{ OKC79^ S ^9wm;/]C/wҥ-ߵ]vʕV¨jf,ݹCp8<|~aPF}D}_Z}?ԕ|_&74RKzˠ`3@ڂDAl55gdLY~AV҄^c1C]w\){tOɍkWAGz?ާx'=e'ĂjOR1cW^OJ+ȷW|oPTȤE\RBֽv8#X Tzƛ==Ff$ ţA~NpJTrB.eɳ5f@hMfgD~ř"J  " ҋ@~"~x>X@ zx :!TC!"o/22?5Y%L e@ Dpgɏ`D%D0Li6M 4$!u莑^0%u{1|v =Ƌet~n=vD:a*:hFa!ˏM@GwD^M|¿ ,dAxQAGbP21ŠCABի\$`&?ï䲬(p(9\LIUyxN6୔ $媋:/V[H) Ns+ 7|`v*y'ت\*{,J9G[F W$c`V#MJ$\ '<] A*:sU9f z>5q TmNmmMծSS3"B4 YD9 C$ r@W7;!u|[_YGwݝz맟~tHGw6QgNPz&)ŃM3$+#n,A,\:[]L&kx"b̋kxa>˗?A}9lXƏ XgGxY;ǹJY>0[PggrB6.B}3Gi&><|?Ŷ--w b!q\,y! Ïg2iz]̒UU r ?]t>0F jviRPn3855?0FGz >8W3yW>XVW Uy/z+*Gck۪HF:82.4o+(Cx9k0|臛R[Uqnh.?GXhP`A'=v~9טZY[kS}}Xh/>ȣP&kZx\pb튜沬+JR.h{"R>+fyYcIISA^._\YYï IV_=[pp\X~ԇ1wqǦ/>wuP/?+'E= {'edBz[\}Ѫܪݥ@>c 9ss` ) KKKA8p!&|q B2.NI1 e|6w.8:K;J & ;$ x TKӐȇrvcSCISK7d"z$L)&! jb2|焂":8-/kDZ]蜗 Nfl *&aQAwl.lR =b +|@I^:XLCHt rN{P}"1XJIF{MsXV >Od Vsb@ X,h1:4\/6 Ya8m>z>v)qx $h NFY eZyҙ)ɠjlOJZ%!5QzfX,ʠכf8e}h}^yvſ?x=K>/krmm ۦ&qs/kre՘ *='ݽs{vv)^^UmFCp88nm,oQrw>j%x^<Fє31-vN+fcGu:ˡ#{A?y[!6ZL*W~|X?\դ{bP .~p]8LyZLJlX=+z\z4fvrNjH7>Oi~1khP`__/8T@}o791155P}*oaaQ&h4u0o1S5O3 _бӟ"*$A@-@kASOUVqC(Y0@X_ɔ JKK\eeeY$cUjr| (?"_yy9@yq90|O+]Vq +~R棜W}/سq0*BG/Pj!EVGJL()2&/EvYddeeeۗæb +koqFx ;: ^scc\ r͍--M\| b \PԥDJ%5'3V #`3@~3#FNP%z(uJ@ @eP XeodTh!zĄ- q ÇrQw :#h!\/DE@zz7<(n 0ߝo p'V@W-L/붆귇B i8Ԯfzu WFDz|6zmҫ\Dfѩ rozK ױ܄|e],T.뵻6w@q_ǥEs:ھwxI}Yb)^XuzCq>DV:K!e iQ\f^ɗF20tWp%L>("I]Yr1S+ר,aKt埛-I"|TK(0R .$%".33ƚ#RVWE.:l11,SLT9+%ZeΞ# ۤʤ!(k  7 >L;P*)2+x)(n$ )dOiq}qc9t8睏'::d}xNoOgWgGͶ;moj0Ŧ23St Xo6{7Gmw|G4]>u. eZm6]vᘙ|viinz=3S#33{6؅wۏ@tsSч}]oozw};6buN_ߍsgz}ess3n\a?R߯lhYqѳז\Y>; MO,;瞿xnoi=gx}l6o'L8)LHL;O,0 Gk[zDŽ4[wL ~"g mG=`Ո/>?Rw70hvYsj4=1vݙ-Toh;W遥Oc-./\PmVv~Xv:HJ߫W^|i30pwf`&睘vx/<{Ox4`zkzI x84, /͏ik[\Z{_>}s6<6lq>ynnk6Ν?w7n AֶN}/PCCC60EB}wkj}KMg/4V]kj| kˆS~F}Ǐ@>f6Q:tB>ĄYU |V_|UUU;z衢ȇEVȗ{`_QߺOުڏ|dYa>[raۆ >|X&ӜsF-/ ު-XӐjdɦ~zzz\\]]mmB}q^+讽zG{ss[-Bnmc왯+(/+/+1 ej,qZg-%B)ya6sSaBi<6~vM $c ޠ -K6`[L d'>E"ҟ N#NJ;„qd=F P- DCTNa9?7W% q]̞] oGt-2=w[Dn{pn {oߣq{M/ߝlP $=W j]ad>IMrk7]أ4BwO)bĄHOO7t~k5PZȧ޵SkjjZ[[^~@ˌQ& _!1 m.Ǩ͉K-eo1'*Bǜ, EQV:(txϨOt„f=S +_47$&%zD[D,s2'?i7 Jd&zL3|ʨT?!=@ .10^Wq|e|9> To8WDg ƳYKLOgly@`v. [_(+,Hqva?3 UatLBM85Xí[7yqZ'Jy҃L&|f+ȍ:ǻ&r^<7ba)]j |~Im2Bϟ5~Lkp 7[tD~nmmoY*5*DӊetP)~E=pDb ~xa?ql^ԽK$L(EfYWLmx4VvmF1cccw>hBz7MLNCr\MMϋD)ܼ?=?䶷uu|D}C;|[Q+EY'(%%U|.**%.;fMMmNN]mmn{=|S}Xr6Hcx*9!?/nW?M՞+B>-'1]O/"ɗ}&HQ>ΗXʒs. zK}_]a {+(/).2V]U-pc`?0}r9ܱQa|ƒ?MGWUYU*ȌaΧ@0|@W,<<~=M(>( oťBwā)"iO L ϢGWf$eT8\eLD1E2=!J3(}z2@Frx&Y< <&Cb0xȂ  u*! 0Dќ;Pøda'CSwHPwD\(ԗKXW, ;g ~lZ\hʱ\B}ItXޘ!QBK!bB̈E;HpSKL ; чKC ˆN}6x˗Ak`؅"sOF^&Ҝ8s!5 :TdȬs|4m9X9X  Y s,@Rz{vѤl>:[b(dd@_-(%ւ⡦0 e$T =l)z>MjVɈ'DXJ\J&߀@KX].JQ#+2W)ĊJ5I#1E Q jplfYV݊]vӂ@fKh KJ\K,fzLpg&/&:;ua(jkr'!qw)dƆpT*-,( ---F9 vv36bT,/H&(C=?nA[^oW/3uw6ޗ@@ Ts;|>ȷO`6A72mÃ6U*A (N~TAkiN]{ ~VB;Ni1)JBOi{Q/~^SsE"?bFƦ{Yk+ Wɕ|ccGqhu ncSb|<5hfΆ{{:Moź-6ٲ./nxVSw"] !\Ƕ}IVʅ~ϵk?u8aJ(k軝Ucvš+N۩DB`bd [[8nš2TrNet*pvC#Gz Z}|=|D}BP&)*fW}r8L'%\̼H0=3ʝ @|C;:n$߾rTBĺR Ҳ iٵg^cT^ekjޯW&@*++l6R.!.IaB}\׾jg7!חlyeq6hF&RP ;xmv0 ٷ`MX! $lIh-c06^Y8K}JRR^ƺ:ssyߗ#HN=>*Q_FK&@}rSTUYu2I]y/-1ÏRJK++ʱSCd2z蠽a"G~QQ뗸#|sx --Ͱ_mZRF*3X^afJF Z7d R")c]n t<TԳa68郲(& HF). \d%PbP2 M!C%AR1@e C@;a]($+-#Bq8)KC(C !$Ja BpdAI48. JSDŇ05rh13Sb~2ɌJgdE X = r[|tv$X_B(@qFŲ cY_2Cbff0sBC$4"wZS$a#Q%2Na&Dʆ0ʳ?JX8b)P8 5(ZqvDty9&hWp!i ~`e66HNa fxPHO!KAsi zx] T%@^[@F a edS,jM '| i[ܰuqY̭.LN=̒9,/fhj׵4l7  vifM3˃+Z<`4up\nq}v˹vV OG J ~ktf`<P qk/47:W>=wܺjGW?} K;0ն{ov{ux+Ksޞ^DOn׺69\Ԡ\1Lح]2xޗό+ӓ v}}scCmjr;~ipofҤq }{s.U\ն6@#&irf@wM]Í7:oNMξx "}%|ioF7 6]˻稴V̂Iov Dy?|Ww?۵aL2Lܛx[|>>;h;ppO|ʿR#QC1!'Q!IV Z&d2`///w&Av$>Vީ(jih45pmRu ޣ#ԇN7醆20߉VK.eFlj X+rU,د4_,˹{ Thp.79~1Xi,!}""Y/b)G-BzZ>)#L8$+h GC0OJh ! m:'$ExHćqD4I'Ði꺄dH2ǃAA`P1_ G(($ DKh8TL՗ H `Sć@1T +b*N qN,&296J9%y@`}[d Ȁ!9@ 3=(Ƌ ; ;΋CCLE} ; L}o߾gZ@&eCqRIP,VSMaY Ӫs8¡CU.<[íW DQAS6ё%pĜNPn(C,QWpj"GUr9FVS\Q<N P 70 0C 0tOп&L!IMx(2 2 %"lJ"=o:P" ڡX?xljbуG-w‚d2Y,t:V7`IrX̋ܤZ1&o?U//K*n;.m U+S{ DW"66xHUDMHI$*j#bgg <2f| <V/Ԓa_IǺ;x[aejd7j8lwx~/gwZZ9}~d65vuc=C׫fg4>{&+tP .gqxuA[[[hOvJeSӣ榻Y!|$;;d.YY^Y&R}=踇7ޜJMΠkogu޹;uiqȲS'직`3yg{}wwa ZHX> {^9V@qHϽjXmx~@ LllW6-ZvsG&$\n{[swWG_ow{{Ub!(jZO2}^R}ݝ?<_ȼ-^>NeN]v;///555++%nWUߎt7~QhWG5Nni򊊊$eê䜔1Q$.cG?**O+}~fտH;Oqf$Fp3JB4p,)ݷeE%EEWGLeff\Hg3sr ƲX,!?4_Bqr0: d e=8F12|A`-i>!~Ο_QJ@wGШY:`B&544={ g3bd!\e'.$RYyvRI&+9^1c Q2찯AfO>A<n6@2ۄBNv2woGv[fU#6=&N'{aqk8r[q'eQQ3C?CaLybd}ͼV '6+BSC ? uuuv ^oR`'JdzA"Mi*,oH>9)Gg>KQ$~~[% #*8u8A޸qKeu_o(.}|osZ9#&$|Ae(Nb$+>"#Ɏ)%}P~G>8q >8t0>ychAΥ0 sq۸Dff&l qإ_k l6Jz)(G)p8111E" ?nttT(x4cߴiSSYOS35f{jnGPY4,! IK aͰ)("a,6 KBB !$ضkkSjN=uyι$MN3@LLTW*+++r/ ~F3و8 hGHXXM@ IP,xP&O? l3håԇ@1= sD٨ $P/SG460)MXHaj %ŁyKu YA "{ﺓT'@A! X=< 8d{"Oܾ!n 9 =hCf!;"/s@ S_GGC'p&Koũ9 `w%Opx>O@kEKlns@$4!hK$b 􀂅)KdR.<4RB=b)Px-Pbj*$)%0$6)f dD_PSE>^Š0Ȫ1:"@dpZu#g\32/g^r,f&Q|42GI܈]"n.E@h< C B,:o_}OMc趯&}9lmMշat ˋYJؾe] {woim7L{{]Ӷe0T 5==]uSC]}433yxﯕ^o4-nq\vv4kGvM.Pռ2G5mYKUզeѾ-Oo&ur:Kc#]}CtoddjtP6 ~kyP9T(Fce9tdZF|cr{NM݊z4wd?XܛuZ[kzn+cK3=J{9b^^[ZLs4#j4aZumZ_\l!g|Ϟ={sft4ժgH׎޽c8؞Ƕ-ŋ3ۣP}_)?ayͫ?ڟ{6%۫nǽ~toBq#1-M ZCÓ̞uc]3(a0 u >#TT}jzbj\9=LSSiIhPͯT43Gh` 288@7L7D7Hs_ݦʦeuYJJJvv6#D۠&ȇ]oCEY8YyyNKMM-++;R_\'K͓D$eG^n|k_C\'/>zGp(~q H$RC'J&fdJIYE6IHH(,,Tg 4t7:2 y3zIoBaA%Ip=1oF'/߬y=G…2₲Dz%Dޥx6.1"K RHp_T h<Lډh?q8.bf` u@DlOfQ%E1hP,@ryoR}c(H9-D$>?Qi9##❎EP8FFGdHp@@8ㅲCw@Q!ǸgIqC1ȋx?66ݻ/xɼC @pP @T< r%!dfIHؤ->,Iyg| u"0(rc@(KdCk2,IP ®эx|8"/{quT4yeYaWQΜ2B/!Ȥp zpڮR1q M Ůt4)"=D0c#s c(}0د*PBH L/N80Jg3D菮)}9Q%ffo߼}tַFݼwoX[{]a,.&rXkE҂f~VU)IQh\+ZWo݇-Uw:ϞͶ|nznUYq_>w{Z͆Y UjY 4׵nh4+++: V+Tp8N~/^AMmwڿsk;ԑs@Hy D } (ȾAB,$@IHB؄b}{UZڙ͝s߽9Lڴ`ր^;(`H3Gͦ>+Bq֤_ yˌբQ*I߶=57& .ݪ, Rۻ\7*{E-zg?znnn/7nЍ2ZMwinoo1e{k=62;9Y7{JV" (tZn Jkn=.̹R^WZT'95g׻?e쵐a/9jQKK߯1֖ gF n#3 0w6<#$v6]؄%h!LcՇ[#?я&bGwvvז]nyػ4vk+[kϟ︗&H|w>=9:#o8*y 5{\S]]]5?z:8ؿaG].UF3S3:Q?Mi4Z0mO}qd=ݐ^`kW˗0-777))I K!/%j5IqQ{{O}LË88<-8<='*,,D|>Wv96||# }f?\|pA/H/r8z+S`N /=%1'n;66XOJLJJ]1XFcA^l>|; ғـjkKKKK%78  pk?~O<!zr"  7p.;Q1˱*$2L“,Hqo~@ /6v`2aDK` {x!,}@Q tEbY?p ? C8ҁ#cpHQ\/x,?.c2sX+"X(c<"#g=a`߯a}8;>AޞL;: !:rH2po`@=$12O8^x''իׯ__ԗW?!ƃҢ|]cf\vb0.Bz1ifx?uXQqDPПљD2 .c'&9LM2*90+!$R` 2KDd!pcm&"O':97Raa\ +Qv>)p.(+\N%2٠8h qd7?@xa F^uceH4zb :(LaUXd 0*+ TVFn23a<ച܋seumy6Y VdLk8QU#1吁Z ~2Ve6mqakZ#۷owFx; fJ5P]WUsL$4<,uޓv=37UOk͊'O~:l6/,,v^ZpG67ݥt#. Ќ) !6˘Rح$W;&L S6>֝u GeʶiRe`\_m{^_r:~StC=]y65o Xd*ԠO1`zr4fk?~E-L*:j$ׅ"qMebXpPeSmyw"t`t)UN;C V4CwTP,5YVS7omvÁ]뛛-&=H`'`rm\ޢVaɵ8m[^s=~ƳbjχVVVmoܚ1_8../Y.C; |f޲FJ57ZrPoz/~~$H(0!#b|`bQVuvvoiz$g)== ŰB>7e3?==hl&Y(G“x WW]\))R#+)W$WE /$`"4"HZhdFt|ҩ{M|)`M$|ܠpa,cՇ</11/! > {d^_oi>zAqQaQax36.9 3􁺋RyTB0S;E[/V@H ! " @PP.rXA . -$&!!!r! e/=秮S팯:3g9y~'$C>!*3Z.Vq P$̦NC`l HAd*9he@W!A#BVN&1 |0[vvy*`%ڡ2`O ăp(- \QO&P1$ 02Y Ẅ#K&Cqr)pq"b.Ή]$%~;L+F8TXJ'GcGiw&vv<<}'j/'f?vAzH2G$Х}?W"/Ό-(ƀ.  +;}cfQ +>aY=xa6 ~#V)8e9l'.GA_pn@PNlvGbʴwz˨o;ЙΥXul\kks}ʪǁ@\^Zϙ %œZ=56ڮndYo46ju@`=tOx~fNu{YqzxOO/ֵ֖߱hƇޜy~mZ^wG[Zl6 g3h >~y]|x%8Ne6C}~[59iv1d-:-MZDz8}V;UgWgݾq/ֺ{2YxMCGnhy9s7t244}#7*-z v^t_o'XR!V=N;<6k2N8^o{?ašQ?SW[Vv*h_; <$L4Hj Ӱ#5EF47^֦VϘ,Vy].>0FdgggwwCYwwWǍY0ݗ[n3]E+}0K׬[ٙ;;o߿GwUч*jbb 3I}p4͔jj(ȇb{Q~!B(((2ЮIYxgU"|ᓋ! NH8 JUȣsqqquu5 +͖eRB$o =]_=N?}#wi&na,+sDƏ˗]PURR"JJ%8)>Uߧc*bEe^^\O$'eeJJP}Lrpqxhdd 81N>Ra)u^}-^rucC]iD) rdFl48 gdQ; ŝH;) HuTH~#LR cQ'(((&3Y"V65\- ,B>267H8tD,ICLa xƋ@s AQYcCd``#~BR nA;0 t*A1DLI*[SLb(TP@JI(@"=1:pÑ((dA*0O  2L /9ЎK+yT!EC$D~ԁQ{g{?83L6BQ r <.EBTKr6SpQ UFįMϊ4 sk6=~ Uu\حFawXݜF=if|aqpdA vX[G1ܰ.YMi,fí 9fnFzG9l\j^7x(>1C7jVtشX4h 'V 59i_p9 iA R/?}&鿩We@s̀?0Ͱ~~Ż 56ZBkCo4'TSZ7udvюgNOVY""[6 K-ZeqA)KdI! d@@ +{y"؏mQ73h4 9͏,FUW}7ݍW\nh64v6b6tmmm(&U#-v-ٌ&bEežܯ^qM*:~ ;ƯzSr&IZVִ4IʣgnG|m{١ O~6SYOw?wv_lmlO%?9:6Xh^lmo{笳V/ Y4!d}]mC}m <EW ƔJU_rjzLa2po~VKIghx].ok#{mmRy;|T@>X*2)Z$IU_TIUhߣʌn\q87/82K =C %xR_"qY Atϡs> >9/߄~Ѩ _{D+*3D"H>: ??5UfGpQ)) I!r/qP dee5k!穆`?=TTC./+Vqw>W r=P =@3ؓ"^c$@$@t ̳Dk|$o)4>:Oa Yq0K-.< H(gb0'Hh2,yHC?z@A!/R"xPzxBGh_љD!ޣ-H07)<4PN@/FFB 1!/D~1: )qÎY>1G!>hiG;!ܰb~< ApF}0E:D @!hd ΅ x$"b(>@LԇQߛ7o޾}Q_a +;.(9A9񁨳 x&DXĉcQ"x,L ~LRXȤp.r+t]bQEJbDSXp#-لyW)"fTuNĢ1FӋn`pTҼH94౲hz D3h80UT@VbF˙1 kр(.HTAbx4hL(b<ϯ)WsЃ(6T]¯Qә謠ݔp+%[`O* XEpU@D -zm[ nš{\ v.96}ml4LtsnfL31P;r,gǍF^W5uw&t~JǵjǴkNG7ڬ%U)Fy h1[gןZ-^EQ?~s Mwjjp~z=bh3vv׭V..4kԍ`mtgqazyɄeݨ\ώaC}C֔*먩,V7o}eb=4Υ> ~!P^ X67lxWƗ/8pf\8<Ў#ue?]o1U}͏ ډrOz]T{'0s րDꊪuK.%n! 8yve[V&m^ܣuRA YP9cW;v_w꼣N{ etm\qCjp{+GǚgenrRS_WW_wG.kZ;;ڻ: Z}ccc#*e߀J92:7p{(xpOYhԈ 2QR(r>O*ʤ-B&ki-5iiiis!?[,"r8q1LAء/y؟opd Np` "+J1^tI")NKKM?ˋ<Ɗg'aȦW"|~nn.܋E7T (W)SӞ|*zy)NOyU)ASLAPWf\P08WId⤐b (cb/Fh6G H}gŀD Ja>pJk_2fpC|QGL6/;d{/FcXDqqp&@~.^MMeiԼ3Se7ӭ $]d Ь0 谅Fb`"$$ IȾ܄ wϹ؃SUsԭ>ϹS`!lʁCqONA0dTNhFɴ0'5XGأ`"-`<.,\d  fr"CbO"BSN\D Ì9}̉AAuR>,؋^s4'$; H2Cto/D!@Mo$UN4\WN)A͏qUYz( |>}k:ZOZ'd@zBz\j90F,WIBk-JMo%ߡ6CO\ (`?'N Y+%͜iEcZYR)3rJ[&W_LzɴjXg'jiYfw>ߚo ؘR1;P? 5ut}wciv}䑸Gx~l0Y$^'^``Xpb<$A׽y(OɞMsnfjlT,¤V# z]mՇoߺ݌.(!m9}_'Y:=Ͻҋ慫cc7f16\ۂA@<%#zvCXi(ab/ 4ǎ^ȃC2nͲ*t)Evj߇;a?EvCWW>MF0(Nx,o//q@uAgg{˼r1.-nQ:x %2p#fcCiYW(W M'$Wnh^낣g%Y/P0}asbkg,9"yWqnN'twu bdP(`rpӑff蘘HPАX == }@>Q[sNOɇ֖ R/6oZ0/77UeEfqvJafRAz"fq_w_Q?67g? Nj O;Ͽ mYYa򽯾w +.xܸDn̯ˋ h9_U%FOTRdȣ##7*"h-}6n6ԖeGC}\Z8~tFX ԑ 2TnIhˉ+0p όd3ڕ4#yd&SF\"#~3!:80/LjKpVY P(Q SYUrR8jȅğp 9J#KOÊt1L焂s)ȟt*|8c!bxhA,S; Le#>Nws|y޹ciy Pr1g)sw<%h%" }Ą踑X=s1r>DQELDGHG02S_OO~?Wߛ7o~^_`x%ZCP_8Wϩa"W_-<˳810tʁajWPS$H&?o dnQ=`".- Bcu4Ho }p @ VNU/I`)ƒDȪA٩C;iSmw:;;Q񋀗fcMHAw>XV U3F`3t,yFq#Pxu"a?mj(*@>\h|?I|xǺͭMdA$7*p ~0[>m5C|:/9'-[̋Z ' kعjTBV1;~oP=4\{jgkG<μuUT킀@XBB` %@Ž@T@@WQǶ]Q76qGe Aݣ̓+]U0~:w~so0}-siw;C/~~za~nfICɁY9DfΞPWw%w;:Q_&`oo/Y}߿s;wqz{k[+_K|ǏYyyy 8p@&_鮃IɇEꫪ"ϛ"HަФk3blIKl*Ĝ8>KtX>,mꬕ˳V)srˊeI+m}kk?sZR2Lc>'Wfff$'- U)qE;vqAbJ.Z65GJkk3x oIz0o-CVU{د0;'!:-I @Z3>X IYtcoB 890.(Y%̳ J)s>PO D/l TKI<&d[-9t)>r3K- :HaB{x/ͱZ8~Ij`1 2C7C= $}ezIq[^Z \U{:ml&#:](rC6FltG 9pԢWySL2 mY9BOK!vYBZB/bӪ@տK}տo߾{XXLc@1f[sN#vZd)pO8ebkRpY'aEܲ+M&!UU%jQBdEv8gH  ekK$nqNi: ݙ=y{V4ˠdo%ij+ЄPkʨN ^n4*HZ-BPnӈdE.۬D<{y`4')QP'fg"77|bFSSӣ'Zۮ?_oc$޻}QǴC.375N.}EB}^pXMួ)xtjå=;osobda.<6?~]ݓKc,,,;`|SNko90?v>v7 ܺu{PCCC###c].?P~+t|;Ss:](p hrN׋ݽ^\x?Zf+Z= y7/oƾ[|~U,tO:.s^>_\\ oSߠxS]8|tvʥ7*d跣GB}woߺv_ыb4Ndqo=_RR UL}ƚ*uTDw3ϖP3+//F~)ˈ@}֭:5+eYˌ_Q/|.Zq2e a^{T } (p:*/2QJOaB6p$@(gDG>iiG*7{ȮӐ S+:wuϪfՈNAA5 ٲLރ~$6RJ&[)J\P-9لBI+`d \^b06)l"ZjT| _AAy rHSFh|`^{q[ SCmH-#4] ޸ Ǻ,E%Q%i2"JdU[)`. GM d}Ig8542U"-% z^V"9B_K렄J!I|nʥ\LH^MMeiBLTMMMY=3Xݎ D#K M"m&., a [Z[ ldY!"~ _ܫ4Jf.s.Id.H? v)[cG<]ڴ / c''1kcWeѹ9ykwۼnۼZsm>E] g2hTrNzUUM$r)2xVwJn,QN/8NlJ^W]rPdم}| < ԩ5n~g]| 1/p[&tp ht={8FrRԨ5:`ZMM vZ*uRAj6&Ѐf"LT0^+6 Yh|?h㵣;@co2o_RIc<++|yLfM=|Ź&AЭZyo򕨳qD؉ߝ*$XLDNQ6%Mu|兵[* u𞎍>uQCg21DNS!a*Eˣ;[[[A?q&"yp8\.&AbӆϞ*# q|־W|Nf>b`B!6V}ͲZ b=n /.Pjj&_۩?>eF&x͍ P_g{otO4(RR5jRϾAO,D#0ɯk^W_3WcȷW}L|F}&B򅞻v>''ѣGӗxQ^^%}BTVN=ˍ Oei/9w;q _p; "RRR |{շK>4.7=ĉ:^Nq57%J܍~lڵ䌌*(NUVH  ~W.G*/rKҢhH@y)9BR DЎ2B`NR a6.$f'Ry*"+3.ˢFiDɧU32TFeN@yd-kF\0]2\Db(_,=aF<+=EI8y<%`s2A bYKFs8aG``KQ QœIQ -.zIQ`a˞@^dB߅cFl?~.[~1!caG]E&HwQ4ϱ@q0[X!. :CY֑G11D^~x3Iźf۝7[[ ݬuSFs[R>7ȇtY&Vkow8qxOO)2L-jմNg2į$uV͔OqVN+txh`f ф8k @4)Sv*KtQ4Ns?n(i|X]^vnzXP-F&Q) sԔmƠ+?pvc]Z+?>&^~Ѩm'O}?\M4 QA͝{BPHl2vo> ^NjO^ͭM惃V6,/y66ַ?"Uk%]|aKˍp[PU^ܻvmЖ"kE<7Vvzs{+˞KK YV-fmYuP XuwuZ!':::P(tB1%>H4><< x/<ߣ'.>䫭EMBSA>Nµ8/^8pҲ҂ xW}Kcsbr>/F1?WTWῥc@C1STER%@g baA@$E F#%>oktWp9ϹoziнyYVj!TA>2H߾}Ͷ3#cb6u.%%)gp},B}f9++J`'|M--VU}--|7#C5WҒ8w[4vp٩mfrPvjʬ ք p l@U IA0P.QCL"YfI 'j"={Jƒ /5[|B8OIj io HSGAY ?![Et社K%N2 KH#ľEb6 z0pĨ/H6"0ۢ7Fl H2q}OLкؐP&%DlH p`\8 }U@&~#' 7#A^($+뢂?~QA2U꿆[@OBsk󺿤Ǐ~wOԇ/5d`, 3m ѓ J$l:"(q)B2%GJ*ȈPPb?^JT\~eU]W"$AQ| ѦX:,`l1*Ƅmb9ylD5 v|퍚憫]pW էN^o?s;ߜG'3DCϝP}U )+=Z~Z=P}bO϶Ttׁ_عv-Nup\(xFfGc/[zd1x)3!_/PN\XUVqnrJumK[kΡ%mypgg 93yo=Ck^Ң_wUԉ~{9y\3SCCCݳ3sL|SCϟo||rrrfzp9\.GcyyO[ʟD|XSsGVg˯^zzӾc?ܹu+X]훗. v\؏~iaׯVGcc=<{{<ꫯZoo|T}] #nQ_7o䫿^%Aߕː/ꫬ<{wן|}XO٣ EGΞ=[)(XƧnܹСC1(ϱg[測dQ_FbL1j ޔ`3Sb):mNVzQQfKMMd>5nAۓMlNښfVPu{U_NNh|pHn5650nyk M|5zb5+ q.>Dw)Z2)feH Yp`{%C"`!vfg2f6SSRBYHMqw!=rۑH`أ7*9#ImFr6|k[a?1Pa1q ӈt F1SPК(DXL8 mu`=3 MRi3EKrj(bJ&C1RϤ/YG8Q-ƨ zx899qi}ai<>Z>R6:hN akIyූiJ͟kd[_IW]]]WWǿ'AlK=N)*ʌV ؈m{a{9Պ鑌 E\{.)d@KB#rN9hra0_I'HVQMph,$YPjd"qIv,d|TR @cYFB񲩒v`g4FBXQu־ޥS$*kDVl`[]v$ 2` M}Sl=HHs6Į)W #'Seʧ*"'*XV_YWVW_c<Ԉˁ&܏Xt*s./8<Ϲ418g'F{U=1>u8ӓ @s9vh]{vK':QQ7LLOM46~Wz֭ƙiEsb?3sw::g3y*??9?039cӜSq\T]s) u63 \rĝQD%d $[G\S39ss?vÅd}21:6"X[Q|*ۛ B ujRd1oϫd;Qew]U74New{ɸqv ۿT8cf}{h4~S} xtώugP1{Sy^{-Qs͠r{)1~?ٱYlj޶zz<;VÖ|;<<veRTTP655vy*6c hT1w'dfi?puN;)ncngkkiLwn1@g4Zf n\3,Dh&+Y^^jio<.0 _ 5VZ+J?OD;/di6yڤ6>뫗A0n3tOaQk=ECC"L:T*ff)̩g |!e[]]C ZMoaORL+@q$5<48?'>([ʯօTR ehc#ѹ|>\_}BF3488GO:~0$)څD+]2ɄAw@_!q'-j"#D$ ?~ 9Hl O=!%bN ObÇ߿VEQPJ]ł[دE ςaa;f5gkI$uȣsh D|`,DW p@CcuFB74azHύIXE2亏% BĐ3U3' Cy nSC[ipĜP}˅W5 DlBIl5DhbDÑQQzc%Q+&DSK ^Ujh,|.=jafV IZ6#wJrp=܌Z^޿{ nintv و_~:VB-foWpD֦V.>?sq~n}_(ݽ=}~>Ӫf%"JJ/Ŀ&R!y0[Tqn3r>6us3.m׸Q9fy"ʾ?*G0,tTmݰ<>ݔΪ[3kP+bg{Sy<KFFgz}ހq;Ekp: ..YC̳,&^T2,l6ʄo<Í? ( &'p_N0ݒ^;g1i6pTL7jkk~b1L{{{6p mW_Hdd2Y{=Xռqy;o޼>;Lׯ^H'&}ݼ.xqݻwoׇoo˞ˣs 󺷃כ/`4k9XOH,F$"aOP*!f!:HjhV|_(Ϊ |B>6:X$ BĂΎTQa^=յ7b # iI%!%WYw[ pzRcccAAA}f,I/I8 #17-¹8:LIJp8 _II W[7U_͑" WPT]RZZ 03ٌ\ 4rU5_*..fׯ7山;55I$'߄\&'E&I-榦FدR8x+ˉRf48^&0 ,rZ@lf (Ak[ETD!ſFTa&RZ<̜Ψh7@ @!Qd;}T4*,"",B B! Y.!!vC_|}Ua9=90u~Nݪ_{b*L#s(!00_Қt : eI\$V̤r!Peja ]="&9t ;" @M=Q(ȗ&$ C`6TE8,LPF )C HBx8/:BX"Bw~q'DɈI%'LJHcmw@ A; Bb ]'4# P|H( 9SST/_|>"+9:AwD@tGxYK*:\%B~44jX^:CwDb9䰤DT 'XAz`tH|NK(|5ȭ4fV)~7huB".AZ"h b4B>R$q.̆*؂ "B!^P֕RIX$fQ Wn*=K:L0xV~|r2_,*uo%N&ןNi<#p{`ґ<eo(ȃH0pxpq`|޾YwLߪۺo@>oűc`?? >/fVi۝^7F콖Agce&&5U_`Xzf6[|KvzFogq%N`Zju;z{4S3fo>:mOw/ m}5:{Eݪ3&gg^#`,3#͢}pm{}=]TVԇڱ=ڌM;i7L.]a}s}Cכ{^˜˻[bisYNh ~0Ook&˲ͨP[7aKӲӅ[}Cg. wv7㵸N<Ӹ3̎/'ybBHcXKr>lҨPqVeiv~nlR8_K 6^n#\4fZv;v?1ۛxJ ,Y4n 뛭9rkminmizӍi0QQotxllhz^hiLOCzccD}MJ>}cGmK mD ͛7_""'x‚X">8%]|`/{H΀rssvՇ:JE>EZB%`Ҹxn>K*8}رc#;=)J/;;HQ\.r'N䔗WVWU+P_yfffaa!ȧR]GP}->$  ^RsrummumͥښS%P4n,NR8ā lYxa:O%b2?-[ߒisXͧأ#Y0JA"p]aF4E Tj\Z0Ma DNAz4f@88Ǫ/77H&ʦF]YxRD^*7WEB:A](x>'% DF$c{d[b) ;q8& >iKH#i$0Ґ?KcJcj^*q& M?¥bD$"p PIwȷc`)DOt; 摁w pE=>r0rg(h xrb!xBT@Bg?A,8%7dOLOfǢ||iudlqY|*`74YR, Vaeq|\o v._@+ĠBuXb"I\9,3Q0O@V, -~S@# 8jUJ'i {!:\^7O 7Fɕ0 0# RTU2S}I.cW)!{QzeҍdVWCIeCZ;ǖ" p(R֐<>Ԛ"ٺrET NǮb1,%zxzEsrw}|ƽlvLX׽u{ט!^f8fa }0ncVӮMזnУ;QzvzbR;=(Syf~zJuZ}}S;]}FӼ崩s=]0ίVڏ[v-D}nyt on# o݀-iw^Xv~?{7;pV[kQRTh?:WG[=+3><݀siqfv𠧧noOoOWO݇#CP~|/WVV䛝?|P9)ċ&@ >}}0) uuwuc. > ȧ4$  8D.!WPZ^-..(PC}-Ri։SE`]a|YDf D)h\/Oa{SK*Wo4.HW},1x99ʊ򲢢T")bX$g*+ԕeUUeejOPTws]wvww(^~D ګ :7+3Ai|$ 2c$W cr3Σ("C#2CzW܈ED*irK.8x若 *ii_%w|{sn{nw>lڭ5 Uո'`>E7=~v}fk  ;Pk[㇃;pޓGߞ>Z.e4ͨ7s-my-~ߪq~`w=ݰ QS<4 fh4 333y&I<>9>>>xthh,~AO w:5Ҁil/R1X"_Bw:'-$.79"|euUmmX,Ăh4ZmCyX)38h$9~ټ҂Brk. L,.VQT՗y"Qz-lD&OV7y͟GPHtY O=.ثGC+f)E1( <ދ+HNSQ $D+0!*A|b} { PSI@J" 71-_{1Nel#%szf[onpscBġCrsb^|8dxLԏD7؏!~p~E}>|D}%bdXz>@5Ȋh0? CC ńK,_1"^C@Ȫ#ˬ:~2B[q H!w!BOF>FhW _q2Sa,%Hb! #Q#dxEQ>A}XSZ8mU3Zk㝢w^k22r>CqPlR 4r P!bMrGEc1ʴVHCHCB3n~UT}o޽˦8~u{{9e7[-ׯ3Y6YUyT*ق'Zxzuz7bq:[;Awp>GRL7]}erLhL&p+wtάkx~~eIƵUْVlԭMzҔQbb E TX,oz؟Гށچ"(@7i֡]if}жY)IҨm}?I} kC$Ɂ+V-33;{KGsGæ3̳fF%U)^2M&to>vp'{Q+ῃCw4N^#W)M?z64/{[>}ht ?Vn]o%^z"L'}U2uk.ǥ lv}7h} }ɦ}W6F߬d&|3^heyneE2"#&fA>oooB&i4ZZ+b+(dF$F%]K.J攜M/ߺUw3")+:6*!UUS q\޽ e^rrSfaw930= Kso޼YTTbMNk7jj>l,--`+)).JNff\WTW]*ǮʊrH HڂV쟘NcÝV] xv9; 0+9_\ܥLh 9Y x09KD`ClscDCNX0N= %"HAIKi& 奞uP奬hupv!5sa\6>°uX!F?seR#89)XB哀ȗqXN ² a@<=\vX'!Y:>t/<}EYHl@xiNo*O'rtBeUt -^#SmB 0 2DdLZ9'] W!AmƃL$ʠ=jE,D>S-(4Q &NgVeBέM >^vзlvpe-63*C}j}@n E.ް ^ F'wtnu~-Lյ ϟ>1 yn^jIժ5g_̏C}VL PKmVA+k.q҄Ba[g$hg?Ϧgɴ.?_*B[ۈ|Zؽ~7 AD7ceO6h%$&-ktӮ[Lp+0HqAwzJ25n$r"8diͪSc%=ǻ~#DSPIMo{՛SMSki:{Y6u=\~=~A7wGW꫌'(_OU{rpF`X2<4822 ١>Gck󃶶vvtw|}l'OSSS~Aom`_mmmFh~_cFAhzXSS_jH6꼒ÊύȈD:Qr@c|7/CeJV_EEŕ"EV.A}ԗ%LP ʔ䘬4~Y`0dܒcQ.7KfR> 3rL&%%<\+$ԜEMZj :P\X5Kx?ެ%⫡ԐhQŻAE /jbWQa6_j*aJ  70/'-*]p&pz]B+_J($102K@H(42@,Ȟ|EvL~ȧ"pN\|p0!FEJZDdIp'҃ɦTAtP : Q;GPPACQ&& #DE@i+)IÈ8gA5IR4 A8ؓRa#݅rCP/~K`N:9' ȃ8ߡ'G3$:Ea@1,p xD a88z8ȓ!M}uuuݻ?˔PTB9[#rPxfP24Xa%e*\'PĔ}TPvq41b 9f#DdD( hB:F?*.߹,yID(XTP<327>$)fO0ZGJ2|-^ {潞MowkCnl_lzK!>|E,{+Kc#Nsvvvjڵv.|G6nق061xp99֧;ԭg#mۯ*{5~`܆ocH[vO6zg&G[*kjkL  ?^Y]T}^oqatٽ0: 6Vz[Qt/OP؛/--Nz\S=|ukqCÏ~ۄ9:=۵| _o<\NFZg:W7˓4>w}&'@ģj?¿õɅƎݭU!=g0hX^Xw͹\3,H.EcFl2?lKJt 96Ą|N%-F(i"  0sTWQn󰐙΋-b?0O#pN p K¤"9pN=(NŝK冒 7TDQ{- `)p.~Cuh bC{P­3X%~X<ɉ̓]$ȉ c sc' dA=0C(.>|D}o޼yE}lm:r䢌$C/Vei(cXc̉94H9@@Xq&j!eB+GwH?yWSsWס]RQ -RX0I`A-FB¤0!d$xr0$1*9Zծ{vw''' /04fb ÈFAu&|pjiA;A5It fCgU걜+(2kR8-ͥ qW%Ђp.*HʹcY7-ͤ~&9 0Vr{ݵ :쇽HQν ]!ً;&-*\s emsJú<$Hʳ:km3PWA  $i`o# 0ѽp"y&CT"gx=> [[]ڬ}W-o]RS ' w_̮[!A }Ff`r}\;\KjmpoO^^X 4r{MF\vm= v\9^Wt `0Bh0v@;Ǿ87P~ǬZzaR. b_n5ɤ 1>13}Xٽz_(E悔}O޽?>="oyMF~2<4"TgY *| %3HgX`1M: 6-099P(>},d#QS FYJO'-'gR.L*qr*/Ujj$LTsgs`FV}0dcmMyAv2׼,V}E 5UנBFA~ӕ _D,0Gkɇ^Vv\?O+WR8TWDUB-vAx`6&y$p'%`E|o䞤N1KwWR`<8 6CZub$)~zFYAZ CH Q0,, K~Jńx`2K@c C TUEZ ) LBtHDS@ F䰻 {Ug&$AzÇop "׀OWyčߥoDȁy`I$#駠;0`,Sp:I 71X%|Pu"KdtU>9j/uvבU%sDVם܎ μ)"7^^`Sw 3ё ^EHY]~(&舳"dŸK%7KvQ$C^߾턡0> 6u[[[gTQASt0Սӆήe|w0+ǟ/V )Q"ثT=I#*#߯wuut IZĪIƆi괚Wͳ?:%k-]y& =k]<.AO9mkF2p"O7h侸Ѱn>~_S߾$E b6<Md_fcGGv=fu;LWǶnkI{~:JC.c4 ~>(m;-&OX:ƯP"A#,ϏfգYH|nŴn/π`k)N'/0vNM*[ MY]ɰlgC|nj⦾^VkcpȣDGH'p6|Ԃ4e ӶX~ߴw/z@Ϧ ʞw/iCQ|jjʈ0&l6F߼SSYgٳl  dc HPi@,-b b* *MOO0sZ]vTwԼs9}>78e"n\~]G[[k #.؅g+8In|ROf)lI*B[f3f5Kpos:Q2 ӧ="e|>Fj$Z-'=n4N'Gթ.DWvgC~\baԗmj|URթ)TP,4d[-ܢBtw׳ ~t Jc;Q g̫f۩j𯶶f⍴f>]vJ\n9!v{gB#:b3c@;NiH%z9Nq@n8sL+ $ bů:WU`8.n$ 0Ksaڤ &<  r\Atr>J&L A\% W CN?i #_8D;$V"O2~*=$BwH ?1 #&kF8;G41ɍq 2 C QXd.N0b]Cz d,I$¤@tH$VɬXl 1IAg?(Dʪ]uE*Dp!@QBD}=D&8qCIa)PpUCJ ` h'7f*.4Nu^"(8$1xDSJj]=QH+Rl)HI hT|-"_[r 1p)2AVl p3㉨>(:_9zr{V'Wg6llm/o?XyFZy~ WəG>ȹV!a]snbf2ãcT# sYÑ. (pc}2lijirξnf_G]F櫗k_uwӥف`o~.0tOfN=3}"_mm-x:U|IJ\R ^;x… &HL4EQUUe6a82 àgRPb*//zf<ZYa|"c@ԇŜũl֧)ԊXV˵dORH|?+.;VtKk<ުT8s' k`̈1⣫jj>nTVtd Mb͞eFV@lK7 .L$Soœt CmF!L Mh Y2!\6G&>̤ 2dB@,M\:Kd:jkNI%-,Kj"`Ң &M%SID:h!"aB@Q/9p :c%R&3IiyT8 a{:@Xr2C%pֈԢ08@( ӈ#`K,*UH\DEdBb!ӖFs TF; qㅀ@Iᤄ L!D1_{&P#I!bC1"`_R{w'LR:؏JC8KN#YT1!3aNƳɁb$K8/OZɸ+3j1dJBY bȐAVt*θ5%*LE*2+%\!Uv(r 44HPRӬΠ vZFGL|}"Ma  nbb>F9&h{ލ 它Ld7i S*@&$~w #K4 oAnYDw9u z!Z'ip҇>1dQN#B+ԁs$cP݈ӐGJw23sm\zTIN#oS%wuٲ0 jz&(a^< O4dU5Y#b?JqHr;mLq9&aY: 3Zr 5$hE.~(nûwQ}[p m#h`;s;w7!7}!{ Xۭ6irI][[Ivq6sZZ:zw~9崙z6[[#C#(Q~, L+ttܹ٢Pz%5&Aji@}vnA44MѰյb6XMڍ9J}c#OD"Q_n~8Uhjߓ6$_kG¡h$-ΪpLbhva@+Hճۍ}x0Ve@$gݽ]Zw?sQQI&u+⟛`1TFIeHҩ{w7m%2ted?!4x,&~MDm,&CxOlKd;Nv%*gF"x rL&~ėq\wdEovQ&Fi 4D$|9)rHSXF-h4R M!ЪTNg{X`LDdHK"%'=>5U5 "%M;zZ1g`Hý'ԇ[JY? ;0f#j0 s}MGȇKTWWcu*MR)|&+N`22,=+3VUUfʮWuw??}@AtV ? ?$C(wvvwf^Ym90r򌺲+3 DJ]U S?_SI?gkCHB B ʡr!̸xHH9 BB$C(Ѳu'oSS[t?O OP*YAH&d-ꨐ8IZj"&"cSqzz ^B"Kulp,a3^z8Qs:,A>%?@% DD%+! g$P!~ "LD1 A=:?"@"¿XiBѼ#ph_%_| 8xBb.y̒0Q~"<?%Dr!!' 2<#~BK~!, =vS_CCCSS]}?~.i" =x!] LSday*:pY's!:K VzZ#u(GG!,G FZ@#$VBjt4,xY/]@ȧp*TX-%ej0FGdZ ERglV+E͗3C2h RSw.(\m@ʗG]'< 4%DGxJݎ^yynQK\%˕]=+^"E|E}u v^nzӺq<͵uX 췾X?ݗ[[o@;;K+a66tۦJ{<g뵌[n~0Yxb{gO{vz_1~Nǣ:_?lzsqhd4vwwmSnZ\&ScpS٭C.Ǹewjr:1r<E\N;KKZ._VMņw{zz}]d=egnx^pUmo3y[Hǵ;afkojxkѶO fQ Lz0:;}M~ijE7a|a|M-//{ov?irs~ϻ-Xf}Y|3+(U1z=]Oڠtc~9%ܤym3ġHYh?_ ^-RaZn;׌vkwvv :d޺uZVHr}\g$-3skۭ]ד6hn[Z4:;;Ln>}>WTܬ'ӷAë!Cn򕗗$gLlQ&WFeK e^@\)eqԧi;q/~N,..򕢕g$h9j:ԧQɲTy ^JJJBBBnn.1_afVqޙ?!ZAAAQQؕ%&&RRcjLʗJ2P,Gr"cU12<:۸q*Z:će]^'Hm5 GzueU5WQYUaRzVƳ5l:b#rgyup &NqlC\PI@8vflI礟dFKC DԄ3yh]()EFLa`( pdPA\ۓ)/q5^ā`l "v$u1B0ދGAy(F ƋA&*I bRŀvr~Á26*1L2EA   !'&(H{b0&HsɄOGv`En4O@8 Y9^AOba!"f!!sO"„9!D}aa/N@>ޱ|֡R^L޽{Ç,`/7D $xZ-HA_K`%.$H#D9\҈8$4b *%Y"cR-`JEĊZ6EJ>T4"BYi\ֈNK`0VkIK@fP   d jC>E,ӊq "%#aG{؈9>KH(cVCNPD>>;%P"K2_ a%ɿ^+ 򄖁O*Wv6Sm,-:&U\[X7V};k߽}ޫW/߼yҘv8mQenXOyaKh8ۭ{rQNm~۷ \<|]1k fcmډ1g6?6tZFcYЭL0{׭Q`}jhii0NY-Ho{w|>wniϜs'F=o[ZY|:>[bs?3_OM&y}1U;k͠#  @A8B (A\.9tF$@8 r$5Vmbݏ/vu=׿~'G]{s6!ӮuWӳ NL|z}k{ZF;77w}j?/5m^UUu -QpLwr6KG`ei93:>:o_!8|A\8nch=a';?fH\7Xĉ;쇛ioriuiڜN‚ƗUg}} -j<,n^Hlv)e3 WW^}Xfwlox=hkir@}=cccCep{ɓ^4HwubWJ_RNW__G1gш = *JSrV+bϋ2:Ѡzp ]$ ~@555fsMuMuUzUZ UV qiQ̨(3 X$ }T ᠝A%W5:9AYt-!SY0 K! 6GD054@ ut+M|2Pdam6$M401UmӉ-Yx vWH$ؓNa9R'"rC%a'sd!'0Q,9 :)rO, ѱ2SR ~v@ 'Ē~O`1K'8y$& q OJ8b_%G9Ȉ9'"x"6/?*N'$OP+ YG?"Gr?՗c󺨂4p)0Ϙ0EЕ.0rIỴ]6Āj z^n1[B` v;n:JDY)p冘l֢sik-ȑ;TRRD! "ȭ>n?00hljwt/ocm񣿻抲 tGx@]7ί!x7k{W`oƶ׾t-{;C mm/pڜ_m&}#=&16; "2=ͳH7vMMN >s ]6? B[_]vo޼'N!~q}r:a{>?r=V}uK/vu:].}⭸A 2xo۷ȷTW?켏{X].O'V0lvv֦֖&F}Chm]]T}=݃ bDwG=GѣAGGzk{[sKsӍ+@* /eYJ>aLX+ORRh4IJw)txOG34 %]tBqJGקHAtu A.~sEʒTuəEE_Pjp-,*Uxd2!jZLVh4 i\8ŏ`KBm2M#ml7Z\E-ā|H|÷WyW ȯS qN 7;)"WI\W(`0VJЫ8c(`X~C~Tg$ȁy 2dF AĜ^3h NIK2L1Yn&0[8H"JIH k@.. MrJK-"])p#기S4 wKS&G#` P0L%v0)FЅL}dU с^((1:QA ~ oѠ@DZlx/CO23G/AA20% P|(%mı 6D* x%pRO;N"+ C4^4`$x+HF'P/4zah ٰ.pW 0".C>y}#ϸТsA}Nc\쭹6s;ngxSǛ|>/^U)#Cx |0;h6p$;mVGMftuyeƉbjbb>]`Me}OǃƦy:n^ouJd //˃1V9'  WԳJz)aupt);,ՓJղ[wK{d]);F+eV 4gsZEixg~@M/gqggnoA2,F wUM^[Ϫ<j0OQ) s#ƕ0}UrXׇoss̔m-oG_6uIW J3ϮZP\./h:`0L888w8 4xQؕJ5~/@>\MMwZި{OPrxO&  >>G ; J}$_YYBKNOagGљ'bb/'Nf\֔DŽ}~˲%P_qq_kߗYYY)))!Y {))QS}o %VT#^\\Ј*ɠE> !-t2!rXlVpXhpH3!Wyׯ+ZspښZHmUWUTWWUUrOGr|P@t;@xK?0!`Bk[jL Hv:qN"r&f~ThD 'AόLLy2ɐ$Hl;c@z?8L B q`ĘH (LHq1 1{2+&sD`56?T*<NҫPEf @@¼(dgǏ"C/ 4HQ  qۆ@BLA@rX#C,̋pB"~l>6c$ ; =v1~hGQF}x :'>A1#>H}/y RAq!`?ā?zP^qSP߳g^zQDW" 'JbyWl԰9IAIL{!"lCBJrS I&DVFaBCvp *!` ځp* 0$l/L|긘v1e*t+:Nbf\TeGBP<8Z}#ha'Pp&kQb$AIAJ= . 5s aÑ ˕N`rjH`Zkh^¥%BvuN4HA9eTvU JWA*zs Pt?}֦jX Kvޱjln0ZAzU8Ͽ|>b_}z`A0?1>655P xM1:X,`޲F74_TX`Р_jnl&Qj1+l&J`}Vvuz}G/,5*Z7iz۠>~nHލͫffq\53EZ}ͶwReQ-Pdɇ t-WIԱf6jɲYp:?~3YBP=#~Ŷp{ּY!1{]oG*ޔ550g635.Cm, n|^}UKcϱfwghj]&T#؉CZ4vihhPT߻jWVVFl٦O|& &}szf_4jnjlmmԀS%& w:{j*?Ly܇`o`qٱh_X8EϹd;ٙ ںFq/LqGjjΎGޞY7 re{19Wz!ѹン{\]z1?46ĵݵ}>Vka˃ʲNeo7)~ åYx't{}cV]w&ǞVy}rzb`v?pkciSoo.yV6넽J\Y~\ָvf6gG{*oބG/?N/?R|x%<:IW||^66oxԇ)ۛ`oAv ,tْ eq]c0, \׵q,>S̖S?I9:b󳳳a>AiF6Vi:L*˄b/FLJOv{>Vo85h1WUM}eeʊrF5:bTf|d6 Cd"Z). X.#Y,0G^ L1!KӆCF2p3xgr Z؆^1"SCХ NfK ͤA~0&&֨ tsj{IQcQ`\jo,B :*t2}>:i @1 c;O |@}tP% dW }!:9E$p{#]+= Fb OK΀|rZ|N)0GecC, {3 :.:`ACo)LS !IItY3ƶ_o`D^NBD^&8GF䱺0EDFw qC ;qmIw&dueQ( # EfYqF4Cv5U m38U2?!‹7_ !!Vȅh`6ة<7]=%J$8q##+ *CZ4*2vƒLP~;7l! vYH$YȌ )a%e%Y9 %62,p(ni.u91bw{ݵ{ˇ;/^{LJ;{ۻ'?'+NN>}9]Nottx|||ʌe˿\w[?Mw25;722ySssunjq˖sӾax?޿y\]&\+ #_Tԕ677޹r]Aч?L y;_[_[Xl_ɪoeϱ[^[Mno?׾sNML;߇{x15==r<C yy<Νx ^jtڵ.1w۷o R B}@nި[uڱ+`ɇj-*}1D>NkSj4z]EO>Ғee;w #L_|FUVV\x֪KLYF$;YZGjd2AtjkeɇҲ=\"tDEEEPL4ZJU%gU ri*)Y)$bB!צF}} MV}54WxMu{5b+i++De9Tn*ڠ9~a:ExϤ 8@ "[c eL4*"I{A,!qA`/S Qt {Г."0 TIZ:p!@>=Q0* xu6!%BQ`E` C g`Hp)x؋%C"/XESD; YH”sX,SA*i8))5#X$xAȇȑu!(b "I @ԁvnr7ƋS7P6fLA0FqTf?rН8 2 P%Dbk `p It?H}ߨ/_@- ӢK 1Ō6̓IÌUB"A8m5\<%(RmH@ Lb 2NYŠ,-Dݖm(َS]i&uҕtK"ʗ20!IAEX F|E.JIYb|o2sQ|Sa.!{c 9Sm29W{Q"4R>PW[,'љ$ pjH@V{JC+\E:I|Է䧿?}:H}sk~ϳuURʒomuiO[ ߋ/~&'Ou63`tt;7wwbD`7){xv9'`moo氵wG*͍CCC۪o[/wlsK[^\] V_+Kvܭ[gz-Nݾ_Ͽ\d=1陼6]a 6\7'X][%}Xj89N3x_x6`+.޵c#wUUv;>SΙYg3MεGDGV}6[*uy7^OO/"|CANV} T_{.dH_%:cAKh|84Pt@rP)ief ~g@ (HLHB WU^V~bĚuϯyHx>K<V6RFbr:ԁR齣iL Q'gC:ᨕ?pI_!hP\AeXUK'D-X(AYP"̈l9wP/om}mK}9<'=ip`d2u=ߗYf0z{^Z}oS1 =_Z_S;gpO|~~S^79v~=.(t a<7>fmOv?:nݺ|wsOg_ن }~!j֥M-=w#/fq[8f[>F;Ji?;? C}a2ǿrC3:j׵56_1 v1#mymSdLr9Cu77;uPVfG潳\>i?nq#Sۦ>~LY[747Ka;PNĄ{~rsS[ީ^{;90`6QLb$?w?Anֶvתk#vee%j:ɗŐ/d$._&+';'(15OOtxa|eYxX/v%˓JD"RH*9@xԩ\3Kbx<~vmWCg~yP_#A>\"KOOgԇi xX(``q9VlltBB\jjrcmh(.i>C@m-- ph }Ο*W*@t^IߴS.3ZM! (Y\ n \PQ!!@ dcx:9:wzS[z<99 3D#i*A%K̂etɤCv`cCidV~j4SN%9+L 5c[!Ő q?դl-2hXII<@jHԓx# @ȍSK,'`.SgS5O 5Zk,$ ]ɒxĹ(8NO@D#rtCϪꑤPX$EM j,WCVUg ZcI UӂbYm+aKa NAD!7ɀ@ Et̎ӹmC}>&r;m>}ui53wzWW+K۷o_~盛/O%Y&MD߀Qp(7m1 2ofvv}fJ9|~xpwrgOs斋FnY̦i@77޼~`y0|<1d6w'dѽAڪ&]S}}}[pM{_mp+yiw{f[܎ ^RmO]CFsb?`4Z g}}áK-7.O[GQ;g{\'^ ?f&׼ezctSs9'LÆk jb4[&-##ZO9c]mWКhg3 n {T}O7twtv_xރd+Y _^!OÉʢTUiZoͷ㟾IKK,/-T'76~wJujiP$Q}%eFRve L]VVyi'ȇ>=XB>?ll Z+DBe9,vt$ǑJ$:ĵk$#;u ̨l>EMP\CD&66ih8Q_OWwA]mM~qAFlN_rPZzPZզsS9iCܼTNvrTVb$e%D 2bMB(„E%1^, D- %R(K4 X"~퓇Cz * E[$@L vU,ɡAGNݨ5X4.0 QHbl =JU1SI˜J!xe*-$C(,IiH!\ 1'yPJy@`wa+&$&#r{lΘp ;l ҢT/)Jj؊84>ilP,r` cЅ$T#D"ZH@ TK  +PB.W 0b2 XvrцUHB*+QC;$AP+@AeN\VhQqaI PY򄚹"e]L|}F !_whbYsI<>*:vƑIƉ8o R%~] Wx43ď\G *)/__v?Xyó%Ϭ}r9[.67חV^X_]Y=[駗o޼^|/~66!gf6L&S[m;۽^hsN{mcO,s?{@t&F큛O~jf]O h> XG O>'~'s ˚*zWAOḯ<|4`з"~0;ZΞ;g{j㜟ꯜr }|~߭!:_ ?jϢy|677;{gxcV-%V`͞=l͍6`f::흝YgWa>r& fGGssZ7 BnavÖ/_+-=( /Fb T|MbaS~;xh#&mq~}J\h =AcfWDGAA^W([V5?ϪP[jWÔ<çvdJ#><ɥIdB|\Z g <4ZVf_qjz?:l;GH\[[xYBS򫨨JɅə,yYܪd' L5WłI$)4Ƞ 5(CA@11ܠd~r-ʃcze(zik(RlHĒ =`|/N'`6<*>b?7FPM,FArlP,SFTBh!$J PGXTt9-B$ưټo9C >鮯CWN>Qypa3O)C1q`Tɔ r#e+&+ +:(j\wRzRr ^XDHpuu @>1Fg2䲰yuP"/jB}? ;J : {4~ߩڵk^}^z͢OU!4a-D^27;1,HK J''1̬Xd@R (j8 :*QPVNEVbWpp ńIȭ0\@Az54 r -:[JB2 IUHyY2 Q{ɪs$8\<lx 8W)d43q LN Q f X ԓ~oytƌK%H0DQ!)AJ>V,1 !lV)$ z-gӠ{^s}Ý{nx;[x;=y^| ~xӧOosxwajʅpgg.dUU#+++WV_W]\sqٙ {g{Kŵ={7ﺈ̭Ņޯ0?6;tM^[dCTq|9ݔv߿W}[nsideE[0 nݽfyupww6ל//6]T֡fg~oTȝ}{-aLN6lo i?'O?<8zBaQNvbmڮԏ e?Ѐ}WZ杓N7\uoa}밻4=5\]pOm)ۘr nLֶ/>7Ct_sA71::64|9xI7B788s>Gw*wwwuZ/67Wb$7!*1733/--[Hɲ @\XnX)&>;'%/#*5ܼ\FZJJJ`Q&t:lXlONPy`njQIVX<A>_8c$量_PQ/GG-gJ`Bf c|>.(C AY4Ƈ$%BDk)>4@a({pP+!"X ,I eD2* (,9 z3"ˈ '#t!"t)"KGh .(/cLO3#X"5m-lFHPE 2Q %Y!ڟ_^fZ{f9<>>^s?%##"%`D m(PREO,$h$쫻hd!(Ҡ?؅bHdK"7*8$!deL``!?4)x`pSBC~2(ނ&+v/~ײBs#M;Olo8lEӱ@ou1`wל׊/^<^sn=zӝGDzlW ӍΎ ?ml\sWx8@WYmf&w}}j%C?h?Ӭn_aJ1cP(C|=>yV?5ퟷ &xo_j٭[Hd2lނfYح9fgzTꏄP˱0[+\Y 9!\i!&2t'3=P9%CߋC !;a0ЍГYg y :S.3}Nx!\A>I_OWӮ#?U_W*R9@u63;D{GAFz#Kffqq@G•#賘 @5Ye0. -hVQ4b^!4Ȉ=g(#ńU8A_tΏṚ 742~y,ڇQ[#*+3<8“y"A z\g+/Q/Gn2ل* lw. B~ okQ  $bSRVbGc;ۛO%ˣu}{cukxmu}}ms{{s{<ׯ_zcuz>FFQ'j(h3>g49ii=pod1g@G %|Ө2`i?oj :ajX=1:>֧ ΩI)P͛.hm}[}u5IVpP? {n )4MM{'C Ht#zFQ\$6mFo:Wz~86!ws얓G\7k熗W~vi46-K^!65H9㘍YEMRN EUQݓ6KƆ[4MUa\~avtMeѦǩwo^`A?o֍9Mʑ;Һ]ɠ>82[[boQ V>B}Ξ#|J%ߠ\Tu oP'?~.ރ1yWDR4){![^VOKNLKJ%ye|}ݷ?H `EPƉ? NEW?vw-((\.\G0Z|>M}RȇK3 ~{֯:Gc8‘~̰РDndHH0;8𯑡Yi=Ri Pu0WUG#IEQI kkR:_DrQ0SLg;iWE]A‘BH 3\9Q@PED!DYQ v * @ @Gk+O|;;k3|ʋ|xdazx*4'+ŲU8FHI}c2(_4iSz?#F}ةQљaʊu!FP>,,'wKeSi1@쉅PAv#!"n((@z$^;$h-C(I%,/:Hb@&Bn %#f^ H y/ "_!0,rd =G RI@PPDX?Qׁ|Bb<!/?IŠh#cxܣ{=~p `b?>E`k/r|~l/9_"M}޽gHeH`sa9?E *rn0s9p +T3hDleܕz!i%hl| iS)$DK)!fe !S`2,u@PP@8mhȁx-??l*C'YQ*+u6!} tt `Q<$أ bU#`9y%)R{3&D?:+rĸy^!@ hk%(ik7V\Pߢӱ8|MԷz7o޼}?c>L-/0Ko:9==ebf٧I{su%mMMȈ}brjomm}dv8>~Nj?k~bd<_^{ڣ!9Q Y1Q71n21wYƭwuTuPw@*uf3%O皛ֽ+?1?wMwJ=(k1fF]revҾ#225kg<{ɵ>~kk&Sܘ}gXp-66LYf]6Zo4 ~ Sve1$6=idܿojǷ1bO2>3w[^}~Qsn ?__[c7EOԃ|n߻oMa g4PtC8?#= }QtF}C}m\r>C>aDjNyyT*e3ӳr2/HS>O?'$+2<د +3IJ,?Q/)))--3T+X;C|_x}ORd2B IB2Yl'LGHUd">F^__| c8Gȧh|j5 GXI2*/VW(jt:ի+(R}L`H$\Lfęt1vXt/ά[=[X\b~)^~~x+R.b8S 7讖kk'Nb)DG{WyZ}~̃&0co.]PQUw:fu~sP kkC3`Uh ?z<_վVs n-#}R#N^p׵5W{zɣ_gxDo]C[--N,ЛC]M; 3 ՕKA++ @MB[DY֢Lc͖XPL&sF Cvvˡ,Wn|N>Z} ܁qo)}p3ȷU}nKohڌ ܻwoR.RR4tqY6.|:[uUE9~1!={-!WMBeBLn74#V^v葃p_Ԛ)+1K 0] nhFai@; ?PHJ'fJ  U|(1R̄t* @HŲbو!'I#׆ ##2cLQxdREP4"4ea5[GK2@AlA<}2p ѡB. ރ qP"Q ʙxIe8L/Ee$|Nʘ|jTSӈCi&ICfG@8/ ;h"#Pl#B! b2&VqTSaHbP(I`},Qv&ҋ! U#0D:~d ?v؏6^81 &d n/S_]]x<'ip]62 AeagǴf:Ą8h,s 180#Avt J(.VS, `sjRVQ`A8kszXauVVqm8|$;H3޿4cp"" "N,/N90@t!%HڊZ1bjDt5E*l/#,jHiHBx70+Gx[f09tpzE_UpUe#W|9.$QB>MhBm/|^xKܛ{X[yG/^<}뿿5޼yݻ߿}v}xVړK33Ӿ1~ڜK64&'#>0քYR|rr?:iwmkh_'U6 z-]cnS!njrnw/kߺ6k#gGǮ'|g=-mmΎT'kzsa@uƒ?ߞh-,s]oo>{n`;=LOM̭,?h~r_FF;O8{AakOLoZ]\^X{eV.9[/?z ~3jLM:Z;\uhobn6>wg&m?W.u4UyY=([]_Z?\]X]!o Kx၁nGUEYsSQ?jt_8qQc@>K{{{GV⽆FmqI9G8IXS-hkZZYqԤDJ׿ܴAA[rRXb;b3gf"7`3u)Bʄ蒒,K 5: M?m Ay?AKضuwF1(y^E"c9Ĵ4mnbVķQSc\G& W|rSel6٪0O>c:unmh#ꫨ&,eYQ!۟d1.b^`R䡩0H 2lT% Sc Mt JfZ|9)3R<9UX "pCA 1ISGkȤRaZIVt  Fr&u$SPg&qM̆&)dmXCJP JC1u,:ѝAEv&cj؁IgV# IBwq '8.v'@qa8yʊYZ̋V .a +QHDC2A ,GO xš&WB)'Xeu(7@ $2ܟMaT(&X)f3Pn(F{"wpYۀ=0M"@N#eꫯw:===W߇6~%9/Ksno*&\nr>;- ٞT>#10[qt`-ԅ,dxx $s$`l0cЧu쟴SSYa懞gjqV %ZYþ,a J 8&l!!$!$dQ\~}OǞu}{zU@EGW*e<ILOԔOA4 SoP8F@8K[7 Ƽ0)a[i*@>T" wݢȚ5L@24ȭ! 5&Tp+JRAQ+Na#rCPSOE-`TVb;_s|u^07$. ] p~L}azkcbuinjZ]m>ؾɫW^~Mػwyf}͎_/_B}; Vˬ9GZ li\-UiFTj 63M>2icC^"6L6b?v=AEeШ2s6+Goi(o~68|PURt@<3mP{&ToW뤮<-FR|d  b=0P&B}-fqsSs3H XEC}]h|@H󽇫cy|~aNaB/|PDחHKvvqJJM+8lI xsٟU_FFy)N?~ }/t䥤p8D%$$x3<Q#orʱmj*aB1|eeeJHD~~vrfF2'/1(2;=MO]ʉpJ{I Rs](&'E#I]⸮ŀ\WPf} bQ¡m$ƸP\:B&F5F)#$m!7P wќ 'EADL4s0w_ 0hw). "!"ا±!=t"OF(Jd0N>q `?dX'4 ?w$va~uc+ׅ?@tc8# @z9LљЎu E;`^XG={g`y8sc4%x C^hX쏻<6zIAF0T l(7͵'Ϟlo,X߆dsݧ?~o߾lgg6ywww˖Yf`mԘfLNMM3 H/NEBާT@8˜uh_&W *`M;0,[ Ke-Ͷ@GoR^:mRѨf͆nnluwJiz\ӫ*F-f@Q5vaeO&L*(-Y_ &te;fb[~s NSܡWvt,6}sGj=[SP}Pu`YQ ?+.ږk>~)7=zְ0k_[aJE#[W65 RIcjLC G1?3ǵc1yf6֕$wǂ"f!Ѿjշ 6ʲ\Z fT[c}Myꓴ4~-6IU*D}ry!%QZRÌ_B!W+@ގvHZZ[ĭf~ՓhE5[BZ^.?8&o2=w_ 9^xBXդxb/Dci>^nn=t%==KKK8>|su /f',_0I&d8ɤmBJ(V Xed)@v*@e)Yeb_hq~2џ8db:ɼ\{ye#ߟ)H}.7It/==LHā̌;s?j-ac3WT)fVz+ZByiIV<'Aq:^M̰4m~RHV<*)\b@H$2ѥF^@N% " ̋ 6 2 "ĀV !dI6@J>p^.5yJraCQapQRJľ~>Ã钑e"##1s}Ξ=#U~RvQ~S}Ҕh2Hc`h&'ŵ&R]Sm?Yd?FJf3~K+I{J++*(Vՠ /)"l+66{9J4M։X)FL#qZВ8܆td>K x'މ:_ep8-fX,? #\w E\IOzЃdE*W "Zux:i+G(-f`M,z>KmAX5=W°KƋE"gZlg?~pfo^RSjm^hok={|?X,TL90=3*mA0BIX"ʮ` bdٱm@T$" hSY/S3=]]S=U*|:;6> ܬhyivfAEG+F$cAP ao'kW_]]Gk ?|G @v|7)~`յ)й S4i1oiZZY>9666>}yTW4z [C8lY˄oag{tZ\9HL~Smf,Z3(\muUeCSmkloeɨ{8`,3!~U0q@__Kɐ P_˅&BscSSC]]{'۽O[=K", K_xusv޹=..2X(HLPPFH!CL$%'kЃUPh ͫ/77HuwC;zOm5m?iTXJ8WjkkJF5HB~]CH(HU` ‚āHbiYq؀1w,Mv,UZ./8$9*MIcG4A`[ME viQC:DžߥP߼yIԗx4H/#~:?)9h*'^pDˎ@YߦZX~bPVG,{ABG@ZG=)h(q!D# ˕/"N #`; %^(W.6MˁC.┺8D 2x kHfEx"gsYӴ3 W-?uxP ą<.#7ԀauEdʨs2%eG$SdifXUC/D+jEx( 9^UVhU>ZBPJb<'Z[|}yѶȾxӵ/oB}^zw^~p_<KٳgL3>6?1=ߕ敜<ߺ`٦;t_jlݸ>33-!|a3$tZ{/_:W[[pT^g ^,Vf'F͌ÏO?YZYu #kǰ}n^3mfȎ'Ƈ->kz]7~y65}g<ޏo!ܼa3ߙj SW?0[LFpM\mMUe"C!סxjrd"@jpW(ZD}-T}͍ Ņ[}Ŏ/|5gΞ)MIIrrq(ʓNNܶ+//ovmu޵=*J" VxM,JyGjeMINN>7ff!ߗ|voo[3NHH`1F(|p& ժ k0@?VUV0#t{0,ؒdyфМ4yQ$Sq"S5W!?*N)HpxY|Z"O~JN#ju P-{i1 ҄KYC>@k^! TqF;\'Sr;V&OTu-Ug c?AOe> O$F.Wp1RD+IB/@q1B(CA2RIXDʐWzKb;'Id+k1ߍȰAE@HSƓS7,PP.`\7@O$|]<ԇ XPb0gO.#r Ȼp|ziӧ&<;ά(L2%09 pEEPDA!w!A.r$ጠ"2|e/|~euEkz|r`ӎbr*N@;0A~8 ?~TAs Hl-p4t8iMGnؠ p! ܓLT'~٫ 1F`P$^sgع项B e å!Db.>(9d`¾LIn)ʌ K!躞r%7IЛ0`XDw9!D@]v +BCr ] 9a[|a4%<O.X@*`]Z^beyq~zrq >_ylyc|ޥW^-.Zϟ/>\0MO8ܦL\Rt:ݰdvQ9툻{cZpN?+Ufeq[%rPT3NM5˻F71;h,jtwCqg1OAtڑ~p\>AvX!R+{jf@*05} aNG[H4&]x;!/T0SÁ)IYIA9g8?{'Fv@9Xc'hqtǠGŲ],:(yuY\ "Q `:G( Bk'D209@XR.\&T !-K.p taQ,W} 'a9E\0x:P"L }[:s(FE0CJD< sdyeyA)Od&htHPw!kN?wP9L`*Tv)KJ\4HQ1L̈H5a3JCH"ɔЀ A`ϏdV~>cިׯ_dUZ}朜mȤ2R!4Fa\2$*ĥUP҂a^ Y(l#"ayz^|&SߠW[/rWoQ) ?{`=Zlk;#1BکQ*^'U+@}ZM}m@ ugN+h J% 7^[ݔ0;7CLAui^ai,lnID#RtοiVEeok`0xӌ=ȸwIѭ3t [ "Ⱥ=T3Y:]}dyrxg,Nk.3|:jK;z,&4:9Bhڪ&abR1~AH%=!nP:,J  uO5E"QG}hF?B.PBQjk,Ak@(0/V\\XwƵK'x8rstܵvm.^ tqqm8kOk4ȑ#r}o 6o_#_̝[~v߿ 7udojvt͠YZ%!@XТD K 1/!c+ *S5=Z<|c٣=3թ|9ZZ,5 Z6))I,yCnmmm e|NaJmb?&(QRR\t(+..*8x , ň$ă>(~J$?G b\ FIلp0u {Kaua>{Py1'h;17)g؂J y.hsBdbːAl JA8J8 rgńxd38 q3|1ރU$`!`$ft>~8I)

PG]9AA؃낰 @ sF Č ["gP l;#':08qGĸ7#/_Oԗ&NˊeE1X.]Hܡr0k(6W(2o@ K@vi@&NQ9F&,8L2 2 \aj@lDiWE,X LBP\zD!$IK$DwJOurDk-ehr@=Z%u"el5 _!iA؅"=j<M0^l:GAkHs)8*Mc$T !MJGIZpc 0JWRkK=HO'X %d:KMȌJA T#S y(,B¥Bv A}b6fB>g+vm,+ r-BqG.x *h8P@Hd|((ڋ{d2 I#I"q v𰅯y"og>ׁI'q# O%زc*b(bWŸ ~ԍS&S.CM+ztc컿rwe y㿿0{9.~'gO>]_|onʂSos].UW[[ܲMOM oӣc;+n\Y[!i0SѾ̋mF twcnle_e{qy'*67IbsklkmRm֋뭦YӽѦs_=10pn%E~ wFk-'+;?Ni?7oUoclqoj_M3 \^[s[CF1c'yb{50m7+nD kiiFc__/n6C|z?A6#zBzCs ^tLjrZѥ q4/Og=<ܝ>.GhnQޞ>n4;Wӎx1X^g P(!/QWXX|s=<d8CngٌtDP_R:!>Vסkxo%:TW+{Z-E;a FsS= 7OU\J|wQ~-S^!+  2 .Exiʰ\5O% Rcy^T2 0 a'%_% NA"@xV"oScYM@D| zo<K`DŽd!hr"$T1s2& hbB;BnPИ(e%ePS-ǶqP !I `#i~l_<#`30‡RpJ) !,IU@/IO`\\4 Q~1Srn(Yt9H81@q7Eh,ʸ,h$=ă߄Ċ0:7ԓG8G0RA82(&'#DG ~'zEJ a?/M}`9 ˊ˦tl" |U%(zN`S17WGB;XUH)`D5BJd+¤LBOq/SB RX \mQ/"Q"qXN eir!R X0$)8@K =d!(u $C; .ɕ(q=+erꞲBVa-9F"b6P˓uee+čԵDbBb`gXS'-%2$1i'|6awz͍W/w^m}{Ǐ{Ç;>߽soƺ);d2Y֩ɑG#z]eq#&Jq}=_o4+cxSSK^F M~h쇬6c_ME9>3W~I}63Of-636nAjdkqazjbbxj Ʈ.mO/Ng&ǯM}n]nlenLUVW>{3tpmAÓ_UO @LWT4t U7*f&L%G!n<\]燞- ~޶,ZLKމ+s~?߫W۽FW^!~ƙ&CSCqS I#anԾdZF; 2e k]f2T u|Z(򵷷vwwtuGF`|4<`h}bCg3j wjTS+]qveuUe_9e,Q ])˽p=y۟~6Qr+v,0?œ㊑~㘗0-(Hoٱ* =՗Tn^>=0zq~|]8ܸN|>N3]i?L?_IOGΝū[[[R=E>sv}]rb-e?H J%ZW2x$9j^~r UR*&.Ls|0Ue;MUeGr)_( \Ec9iP&" ƅP5)gB`+ dLղ&dd5SEr7`j,L:pV#$\JaLLb,@`Cz[QX ?xZ{ S eQ273<808 =N#c(c LؾӨ!D  <&lbǐql?ĢHo2FxÁpoA]HP(> z{p 0%<,鱙4L{b)wXâCzl'/̛-o$;{T ( x 7 r8"܇ʝrS 9 @BoPd*Q-o^}<^ߢڝ:bw1vV$vW/++k=˗%9%9&\qɒQ9|ڭPPG$I,p+>BĘtq*H^uNtE'!Hwt!Ƣ\ Tt `7in;'C%!t?bKEN$d_amF{f)vEfoiei`j|kKU2rH? v'VkUSC}Q(/;76׶l}4vӧOSl,/|Z=9.UӚ`ZBN+kn(5W'}JSRTXSUQW[Mk U_X\54Ox 0/7cȗKG @FA^VlKe+pvrgr')J\Άo/ ʲkoavlC ^ B!{N&xZ@/s "9zDw!Dt.h|tyݭ|,@q( 8Xx:A@_t:|0cgٚ7Yv=hj ~q<~X )hggu\4w>k`},xژgES[#g?q+**H$'|!+Dhw+=(*}t 0 %_ud!FJ;p(0`*(I*Ie"'h0=iq\ rˈb%O:F1afx؈}t{FM9M$5nˑb9EfgD1$`(vʉ@\T,|x²0c&b-̆85{͛'P [WjˊGf&4 P?y26V O~ܴzJ?V:u0SC–fB!C}TW=䫭׊Ϗ^"{;lmm뚚߯vK]R$,|O$(5+Dr@$Dd~琉_hpvQעz5yq3Qd 8KXd#l  *JXdiQ& UQ0BXK.ujaǟe}ig:=}s7>H$Bv~yo;75@!Q {)))0hWXXKJJT NubnSc##{[?;k/{njUlmkZvֶmpN$grLק &`^?9He?G;\UѾJZLC(E8@M%H/yH-"|ifBTk,H~8! 8I~2!X྅EK^[<{!nA.`PhNDs rBF] r}kll~yU ̶OO#0U) HZx@ X1=t1 \ ]DqNzhH kE XU_٫lȱbz-UUXxesd 1ʐ T}* 6$`LU{@=*jP܅hаjUH!!ՔWk ;+^U3ġ4_q] #Re =.RscJT6=H.TÁ}g<Ϟ>/}{G/^<{^z/x π+l,../\r`(֗kknFG::OVK544cj`/-?utUmzζ5w>;gF'Ʈ^4biY68pftb~<8n?P u55ܣxwo*E &L^?Y',-}~~hmt{gWΆj}i͡/ޚZ10<0౩7;O۫/,7͌ap:OݖgF,#?n<|ν֡ѻ_-Z?|bĩ3K?n TW|}fh/__@_G{[kQF&hsSkKS{ksGѶV mFZ{5N*5CS^Vզ |ۍȈ0+ μ@wo'Nvn>~.A"yn(###)));;+dk3w:K9|+'G|;km=zk>Ue՟={mmfzzbbFܹ'N#~F x"8M\ Fr)QA바V갪Qd ӐR {ʐT9ZKϢy)1d$? KXN؅J[!!?U4*'I1*g<(XU|<2WV<Asx rR aj2f?BV)$"g+%ao%̓dszo ܘ\A@ `8<"% I½/UVV/؃8W#ܕ'-r& l)ԇ (X!)!ڍQ*rc!rLjOfFj.ˉAB@\a/Uc`'\q&H!P ՀCZ-*d83!<&FA0!|^E lV-ƀd4)(* f݃b]WsY@i-ӫO9[Y V6_NN+R#8V3$^¹L}=ȋtY0{|2 MI`3 ˗yï=}`yG__fZBܬs25)ԝrL*!_UF&75qqv-roʼn Yl7q1Nn '3wlgx/###""%s{wD})[WcnGYu<8nͰ>z}'~8`ݞ~Ϟʂ1eoDz JLr许+% ++F" ,)%+-)*)ӕLݫi< ngL"ɜs 7'h]O FajOf^Ȋ Ea,2KLD8L ҅l@Y\&{!MD+*FBwe!F0-& 1&p=.BܚEyjV&P_E :/6' f*Kj)}dL*=/!;Vfn@% C&0"ܠG\'zD3 ݐb^d==2 W,L˻ɸ0{^0R+i`_NۭUMC}U%2WSj*A3JW{=3=;y֬R5ʪ>hZ6n&q좾q~qeymm7-eոP[]36:tM7407 6 Ʃ^J2UT 6~mvg?8qK?T$ 궶UҞ'e=զ7?7o޽}kZ|13C{fgV /773?uJ:dK~ev45c@}]e%5UOP)SrLJTH^ J"Gm5%_A )dFEBV7< ;}'`w(*u%1&.6)ᔝSg=[[:3OI| "ڵk0nԇ{}~taXXz0m-:v{|滽G#>'=@ %ie%I$IYVVUTS"a"2 ;,VTa³/eJHq2 dgr9Ky\17-p`z ;66eyL B_ѕIv)Q>I,NDg0~K]!u&@![პ^ex N}=21`B"@0<,v+`~ :c9uƄ}ԕuM>tL\"Tߛ7o6@t@bqLD ( _9s# NQiNf QcBIlH0/)I43KA2"KLA³Le% ][p0 #L@20"C6#CO9s <"P \mDZ_AW9b1bA$xʯq2lS|G8Wp)&VĂR!:pNr/Ɍ)B0܎+BP l/䋯8+KTpz ŤԈ2aT8ivl՘eѲueէOI/^cFf21è^g ²ƒAmm0:;z#_,.*de . {f~pM%ʹ%UYVvM 76a" D84xZ2dMF6 ck`i""5E20/x /Y L0p- 1h:b?.X*N(z6Ff$]E +%@)NG0 B0 x0LxBhfxJpƅngESeB~0*zN:"+14P9K-""EK/Z,(+L@ @ń,@7F"` ڀD@EiAvREt.jMT^/?T*UT~{u}onꛘ?o:O~d>tOgbރϞ98w,w}ŋGG/b><|N{xG|]-=}0 o)_X_(*ϒel4 LAr$ l(C Idj8m ɑqpص E ꑇЌ`m . .4eH9BFV@'DpR:.#KR Α ,KpTE0@:a, 6D:,MdYFpž"8EEe8>!8(#?F8Q%~zv-AA(CP@% ꤠ_'A\j|8ރ$I $RBR !DTD,EPppC؏p?.]gʑ{Y%Mqe%ۣA.%Fb2dJxu25u`8VDM8,18K(f&,Tb^ "2 ZPbC7B,25#)spDu>@t C Na41%8\~(oaɐmXozj6=A}6 'Z[1-8{{zܽgO \ݟ^S{NNn,..>X\rm>7o/-p6;39u️g$:7 ݽ3e>;np#ݾ53jO^koozz:Ohi;fOEe˗-wNtZZZ^^-fv}rjjl4\ =~әόܺ6?exf/[y Çӝ}~PIȀXG1#[ŏٱ'uW}ŧ^r}akŏ#˗ vw\wǫ/W zjuu;{;A}֑#M Nvf]}}=]Pŀepp7Dӓ7nئ&'5z"KB uv>i{%3 uc𥛋\*M;32R4elsqpBpw  x|]7n~)Dƪd 3Le6I6ɜ=^..PН,p;;mqvvbgkbZsGcRmZ#--他-~Pq7x(]ODD#QH„Ph 4sF.jOP[eThjuFά4q~Jd^RDNШ+H*Hoa BDIz44i4P #!D ɡb ɜx( ̰ET̤I6k'v!"ȍz!M, iqt8:*ItÓȣ?9P<̆t&L.B[$*S_0IOMݠmRV!: <@$yNp uMR%Ym$Y ( ],_ }UQj 1Q}[yP'8&X 1`:yV%OEV+"}I";@h'{-2DP IBx|H*% 1_ xEy1桡OW߻w>ٮ̘t1W[$1 }:W A,W8%ZeV,ʠ0+TvNaM PEB˗Í!bO-r:"1:7SFB^R*?0Ih,PgijƝF&h[ewN$i`<¹%*&v4Pqȇ$b<;J"w:j:]R ɊZoTU(\eE (* #@ \#B%[i8|tuwg}N *1`Ά(N 3y=9@ <:D ]Q \fN|}#YDz'3^԰}z>=>n5O _ }~7o0?w8RwWuwuu1A R_Q^q->VVˁVFo(5ZMsS y]kni-U *776`֖=[BZݥYG9sd n*Z1$W hg2!OOEIl0ԫSMzX?UnY\AYtZGGuRFkF'lTǞ΀mK8h-]RtG:7;5eokT']giJ?XۄS]se؏)B]*S߳M?單tL*CrX*S^.xRml.V6=_{ FF".(/+UܭK+|2Y4J4hll RVWJkJ>j?$KKڅ޺Nr8N,G.]X۶mpu۾kfxo:ֶ< &F'$$xW |P}!AgӦ 6ntweXNYG7|Y>tЙ3ldοvX곭 e~rElNNBRVv_$*H$&"!!EB.?_X +\T1(&<&e^I̋N;}-ͅ23ヲC=`/+!mz\P%+|oLKp")gޱgyTG!\ -ֆsQ0a+8?$x<}p ZAd ;_Wt1`!ཛp׮orO  Fs'MdQB%ҼnΟp/x#ŴqZ}sqR_nnO?o߾(P}͊もI .eK@ڌ˄/u t@qPMʳl$cOA~V6!"Si0$D"}0  q0[T'L#WO D7RBeB`(%N E aN2!trGhh]Hl fҸ8LT)Fc9,JĘV<'/N?C4{ ]I0$#4֖fc @HLJ K3{37i)s_?;]K82lm*,#}OX>ΛyRiT*'i1U{ɕIOg̴7{wm2H7ҿ$Qa>;un7ldbQWb@k7ڌ*ςߢ랿Ѝc^.<_[c«퇷JZ]URUdյ52Н^׭ѨTN::;:ڔh^Ux|*vÞPPD,ڱs*gF; )A~~ܷkؾu˖MlӮ=[vںmzZM[ݎq"bSRR|~TTTrrRǨ/33Go%ہ99z8<׳ײ,Wkk+\>]aUl\k>YfդDR {%\0+.7 C0D|j?y>JA|o@M^=v< .JRO&ENI紎=؇DA0Vbq>NLs" G@zh"b^@/Pn+x^nv 3E`}7=x0=ñql?G;瓛(KXRA\byT$0L"cBIlX!PAP"dAsBE&xLA)3,]Q? yDTi"LK0<ƂG %L!]8!Y4!jb41^B=(KiB/Jm!@UWY JQt c3WN(hĿl`K^b[m~DCHCJId졗HRG_fIPzJBb̔2̅Ew.f'G c͆Gϟx>xo! ׯ_|_ݻwx?\kճ&4Je_WoW{6J5<kv?}TCÍפE7n5\a&'JE[ۍs]=89!eٲ*rhzzl6of\Fu/t;5 *ڵ>B5i4++.v?0 YzrɬҢ򪚶VIY,9ܿ= ֛FzmM]wޚ4pOѬmvOL\c\Sb,SHO{qaf[OX{Sd+u7յ~`u`PcyÏoɓvSݦ2oh^zdydn29eig}=CY/2:ioUW76ַʚ[m0mljP*TAaohzA Sގlh:2xmV_[kvmom3v6:8ٹsmֻmv|}M۶ov:`#ps2AtLnϏK@|ֻN[ݷnq%wfwk+͛8[XYYo_o֟m|VO~mÆ;wuvuW DKԓ*YTZZVVQYҪJ"_U%؇9ɒ8~NDHQZh^8?U"`I`P0L=.N J<pJ:q0/%;1\r\Khɑ\`/!Kq0Hh,آGEA]aO &vDpOVz PH5JxfY+q7v) u@#j y&Fp+_TXZu#A Q5NE\E8c Qd5u(P?'E. ,aB]A8T t E,A}L <Ljc 9b@  ƒBlqz;@n 2% "%[-\ 7O;GPƐ!p  mx;=@[HNu{? r;} ls)H;&?FA,/8Vxߜ_6 H(uȡxHH#nQ}J0q`H#dc 1ɐg2İY ̒"u0f g?8 DAS 'VJ~pQ" .SP7:$<%~^ =䆃pRXEW}& 3qlASX 0Y:"*Uз\~ltCadCQT=cKgBYU2-F-'x|8Y4VV_}f%>y_o^}oޫϞB}3ӣAEߝjV+:55e0f>z~33:ѳ*/Zffs=$}ӆv+Yg [zӓĹiiiVi|`o?_P|i<;c=1Ľn^oxT=齧"gDe4NM+/Uz?|6HN+E%e2 \S[?)Oih򞾼lCttXPb};}X1M σAovzeIS}0~+W]_\zT^7s͎V; $`Ⱦ([X"Z-  U%XEd vzw9z8w9<9pss6Y})LkV[US*t55p`UeVDG+w^E%7)'**1ؗ@lBq]Cr"ރ: >@.C}Nם"BNPq^;u[/_OW,H&J_Tp]! * ԁ%$@3` eA;聣-"KX\"e.\:3Xj%gH!i.DTul=J$2Vk\;BW$ <`x6C2̩)FQ, qkʎQJcAX%s3HedܐjFل= ʊ2ƙDPm dGAn+WA;m~(2=#5GGsuM3rRq=+ˈNc Rjv⇹ +_/Ϟ' /^s<Ƈg`WZZo^F۷GGG'&&^SߟW`鲒~[ÐbʳDw䔮|nrmzvnaa~\sC}A^aqaI]sF[]MH&ǯ7LG 驉뵺o<^ ̺ZmBRjݹ7oib/T12;K3|[\" =<_kxp1e;Fޛ~RGoŚu g+K'n {Ʈ}YQ|ylp{ wv~592`X1-ޟY[Ck?GOhusS熗W~'g 1-/@k iS G{w̌5],mllh77?_;]jkEx`78wΫ87oKZĖDk1 zEtllvrmNKI be3els̶ne*/u!FE,?{+N0m{xn<іͬ\g=`G|ٰqF?dnu^p d;@ԫ&jD*Z@+/+S} *+`y)=*ǯ%Uv4?E'+s%ˊCҕyA_Ρce~ITyAZpvT)RG"E3T'wzuIa^ׁd^hC"I #%x#wayp,2J愫`D,W-Nj c&|!c<Hd|0/$ O1y<<8@B`wL!#ځHc7\Gs,pMʇBtuH`< B0~-9q >T:TJo!Pxℐ@w(dPp9P xlHԼ_!}L#:%FEFztDN9J~{Ch/\v]w .,}=vlQ"ԧO@=A>Š5+B5)aH ]Z $N%%@5­8_\4as|@?(+p&h$L 6&8K=댯@V<ֹ.ewr BRà2 yQ5F}0! 1H̃ތ 7$ 3$C@&9Np'ވE6 ~9D hCA|\.R=?`ڴ8?O~ayȷ0lyO^z|~ЀAۮlɥEuՌzzz4`¶Ξ72D"xtB AgK蘘0m-\>\a݆49962S:Uoq5P__\oQwaO^?(%|+H(H'oMUwM&Lҫz~U}̠3 M"B˩RuSQ7=~~d^L?lu5{{F^[ذvXX~ՖڮfwĶq|sBV'V˸CG/;/gX~BIEAw'KP,L0!?.Dog8w _MM&){O;r0ۙ7x }}u9s Ƀs,=' %qj3'O8w8c0$&@ Xg!^;;f#aa`OGlHgw~NPry/9 <CM\ނ=("^ : q |_H`ඃ:ЁϝE;"0'4~0oqpWNE:x4 <]z K(u ~IyE/EdC:}' %IUU͛?,Io,y^>鞖 bhO\!F{r7oNZ"ݒbr0G(*?$&ą@;$nf-+7% adv""9 EX3Lo&ya`nJh[j&C 䋁wd+`I^+a'C4D}l]=6nT3&OM_^^]7?Y{?W_ׯ_ƌ]ĨGlW(*5 w3]E[|+=-BRL16Ay!yDb.Vd޺]\*@h2m_󐡿 i;Z=mF}/u4mU')qCsyX}~ۻy]gv_=0_ss{@Ww}d ѷ#|m &A6MjPM\fT.-˫Dyx_MaQ={d]&'wUa@o@XQ MuJuwQfg䅭*C[jMFcdw?~xsRUL4{u #.q{deE5K)y:+,0aJYxiArF, 5UR)d5(x8Ms(MX ?12p8l Ҁp3]OCDGx֘u Q!(|Lmq 00Vud "Yx/Snz_cIdpc!!%H>T| V1mȐ*H= aBl.ԉAHϛ6$|Rj;,}=t`OI(1d+hX-T#]E>$q,h4`, oU0ԁ ~<" ,ȇs Zy,>@> ܈)I;ԇ`]WW^Oԗ/ʋAt(^73d0Bb9 :/V'5XEy=i+ڬ;|kXsiE># _6_h::;3`xwjrRʺZ홸آGVރ}]M#v*)cEѩ.kCmmC&P} xsL]5ŵ'-eŵ ,q[tkqq{sr>30?vgq:%/-8-1/r5v }{XX{65pl-wWOՌO~58aÑY{yo,q273j6Y}cC]}Y[ScᬭiwtwaΎ/|҅~&glM uMkϝB~URP( v߶y?}k׮qvvZunN]\DDݻSRRJ]^JR׹+ཱུzlq^M~_rṬygNj͊W\_nOD]l֙u]eUgܢ Mޥ'0ĐUzt!ɻ4|P85@Р>x,Hq*QO-Fɣ}r/0{h[ZHh_dKAWI,xD#wIl$@ԁ(NAރA,W /ZD@Oiê(9jRN |abX.LLz2T;5"yVtC6I4BL)B@G f@PP-DÔaX$fەyO䎌wys8'?cEW Bp}rzܻd0L?Ҁu9M&a~nb|f>L.os:4tI9owݺJ'UsݪӈVW'.j|,hbfgtiraJwE^z>~hkmlYY]>~zW5~?6Lʊ"` 䫺ZRUjꮁm !Ɔ6C݅ ]O;>OXO᧮WVxYff6!B_{[lm~kϭYM ˟*((\%'+x܀<~WS(GYtcx 9(9{L(V+%p. KS"ac1œ$飡iG)Ӣ gƉAh EiZXnr18a_>?b[++zr b^Sā!^0X$5!qᾀ"tAhۨ Hă=J<@dDd[HuxDp.BnxxL )#I"DaA~<2&2#ǎv@=VzSQa =mT˗/,'3~)bD\o0]2"h--/쁠eORe8~?5CAaMGA2?.>$D!K' '=sTȨRb,?Im2TXE2>}); (Ӊ!8yRfH+dp0e0r2%+=!eN/L+uC}Sc<}Jٳ={o|ߞ!'m}^`afjlzjtlt`hodȈq3h}zvv&^wozjjdt40P[SW[G+5' ?Luss;o )zuZ&ڥn6뵷nj=o5CTKť---x?hp^mueōM`?Lu7L#]5P{[[^ZF]93jlfW[Z1} _B<gffdش40o_x6/ 'Xt*-cԗr, C}v=di[l㶍wYn֭/O"*Vg)ڊ$@PC@e- r`@RG( "0!!{vYv׺30g3cYd )@> 1vZ].|d3 +$\z)$YFB3!qԐ ҂dNj!`wgp+%qY6:K@4^R0G؎Gu- ȢGaD,!?+ĒRlxFx VP/GDD}((<dBny<Ā|J>@L@mIr]c}t .#ODI;)ӏ ?~sHoa/ϡ_igO F}/_׫Iĸqltd7۷}nG{{[vnEŇ |rΞ=(>-U_*m26.i͇> vv urܽam~}7_5XZ+W}wz67# =N"+*r"?!E"a!:1?!?a!'Cu!( /d'{˝Ofųh_N6jxXL9͏e%{ bt%O 7и9T&8QA};vQawNPq@1`6?r$sb)`)p:Pp^%M&˫*1D}a:7otrZvZ;;Nz*@ oaaaIIIԗDDd&|KՇ-(NII0ǣnue9ٶn~ 9dku28qp^ar޵yw-6ˣ77\\PX y{ERT* Ex =2GuPXIR$ÂxN֙o0p09㟕ijq,ˊgÁg#b}zFEyޞ'MIK&P "nMİZ$DMD.\ bŁA,00FMK/k=\>8?wFyJ޴!yBs6 uKu }A>AppEK8I<1ag< sG{|7:8ILctu _Hs`VxmZ81^*:1 4Ji,9QgThE< pf8Fz #qLjC}W>-\{4rOIN8hthDW~'&JB58dn"% h䃤d YYd ⪉a)m l{~==%G_ݛ{So~/ayjyC?M'ߝmp̝[b$}53s2:~πk0T'Okinf?J<€\cewgRs=}g2lo;7`4t5N'H())ILLP } 'N5m63}cށ!Iw뗳vw>o?u׆[,=_oNLLVsG]g,;^\_wٺT'> +ׯYXzy2ez0? 7 mWjj?j.;thdlvH_2;kk1?؏lΪ7=}'&ĝ۠NWK_T_ZRT[])aZZMmuMuU]mi,XU;wt\+mZRKVQQTYnn[z% x;xז-[d/.]pWwT)ü(ԗ 233vDgGdg[!=ȗAqo;j~y.[ګ ~./Yʢ,~酟/?yş*V6UWUA<4LΣp=*? Qcafɛ}].'> {{b]fbpZ&e2=^7Q8VJ~7AI㘀M޻?ߵ;y7۹31ݹ?gЎ$7H'CFلBw#֓sֽU`|'[֠5V!H&+.̦o$:aI Մ`ZLH**cd scpuPn CQAۂݢEyH nV  ۬Q rGe PGp3Mu11 UC|):,uXGN)'У-~%Bn:P :Ij#joG|"QIҝ]+Vx<Tx;qۉySz`?@'@ >u[X߅m%ԗ _'0dF)N)cT -uovkcb.(YK} orYٓG$F,)h*.?cahAtpI0XHVC5(qicoQF/^IsA99IX&A5eBA``sYPtTNAdA;"܄%{$c3\/>$)!?j峤k3ĢaP|U%Y% y $< O%gr7kJ!( Z?a6޶[߻PÇ(Z=e9iOs3xSijrnl[SVm}C23ڜܻ;c?(yO@ߑϴwv8Yѩ^_Cq:ݍ 娯[t_t58o\[eeiqA܎آ2\mlPП؝| M,b?94wn#׺Gɴ3Y}7zsf nk|bL]mY-]13q~IqdbSGMur{ͣcֱgOM!42X]ً9iMsw6;DiKťh7٧lgΟ8ǨhF3$ " /wCwt)[MӨ4{FA4H-pYLM_MRҤjj.W{y*@ ~X ;t7}mqbno;t~}==}][oa{ܣs9K0u:F/^N:qq X{_e(ZMn>6mrwp5^Dg0JerrZzVc eeXTʰh4YYY?/+ܼg-nnr뗿[~XծkV[-s[ƫTRdJ9l.(l\W @uj4Xuj.=ZzeJvBnm*Ah$J /LR%gJ)9tQ0ئTiӢPL$Cs`nap ˁԊDRv:PG˘vTfCrdy ,+C=-922,]ʒ*'ђÀx3=Z ߵ[JiTp6)4pē|8Hb$~ɼ h #ldy@P It'̇BCK 9!pT$"0!9{$_/'Cz?&xPDH c P8DI‡ȣW2lXQ DljmAOAAzDw.0OU4;$3(#z_n< JNn6p A5FCHh U<ȰVr3cJ@zdFtTkZnAtL-UiwatXm֤!X,2AMD0L.PH B a~⥺Ba$GcYAh`xNH{(Vp7 *L4$c3LBbX[TO5SOJnD}++~_ގ0W3l.%e*@]^gkk!⊆ FE &'kyrŹ{/?{|-<}4;3qO^8gK@'Aϖ.A~~no/~իWԏa?nbsnߺ69XSekk]wM_vo\+o?wWG9+c'W'/loozz{333~̚~va;޺m`\~m;Pi?WnM9 >33c\?Z@Pzd޿ 1Cٻ'/L]?15b߽~o/ 2>=PP7} ^}frr'o]tn?wyG3ᇵ47?ᯇ/B}m[mGtں;tuvt!N;~ɓ''&NWg'N?szKc#D}\8w%?KFs:3m۶zxlذaۖ7ErXJu6ȗP(T*^Jm TwQ_ee%ȗ1è-^-[ü7n fךuZfG\j]|ŪeXk VkcCC}}ssγ[[[ZP g6L&#HHl6 fK$Q %Z,&]&g_R#*@\Tke]yXO5:yZ_'?GQS(+x\J'#+U|ĞEHp.[`3&?9;seߒN,21@'!ޣ& Vp LȡV ?8UHL(X Q>h_<Ѿ(~'dL42[ WMhNjƑQ"mqEk"1ʗˋ.H$Aoñpda^ a^4TyU~NrĤ>3+^RϚMm'0{nƧ dnw P Y d5ٝזZwMߔ|NQh㘶ܼ<{pIyGeh-..*U?]8(5JdR^no˗$]OJ`W`gJ*o&65J=I}C]MC!x2**j+ [l7;;ﳵݺiFfӑ]̤d/v0-;!Q>xXipppDDG" 8|w/b_ljo֮۶zgWX~5Pߪ5+?î;zRaaX\".).KKʊr wD0G!0( ?.#rmu^zTJ(/3>/;L;' ~^>1'}Ri>i_4'E{%Gp*C8K\x D$uWL < A;v @>A, N9xq#QQ ]`ED8)=t}<`98X)LB#u h }sCY|'Уx졙}9y!'2D[c0S(9q`Bj9'1[a=Pr`K{{QlEБp 灝~}߸@xq *_QI_xQPW*Hd <"݁@Q'"ezs0 3>  ư +Jp9d":+@ed^ 3M,ꗟ,@}O.3ꛟz<-01>4 g ,'Kˏ`ʨoqxZY#߲%O}{~4*vdW~w?麇]i4jejp8%,ÇWr "K.{~P wQxS׆ǐk]RG}uM}?D9:o*MWMJ[7 LS1QɻFMǙcU"ypjČ{~dlH 4_3C;0pWݒw]vpF1~544ꉛS)iz;+.tI&AHiQ&He YCmc]k7ߧhtZ-^OS)*%=m T*JZ!SAW+6X߼j~kk+jxD"h-$$$,,,11;yPsKOx@>, #JhlmbmϿO6ZZW-߮^6l\ɧ׭;+L"oC}%bqqq!W&.=$4e21WrW3Aȯ ˊ2>EAYq~Hv3b";;)`f'p96:!=!M!&pK8~$>p!bbN`rd a Qfq!r!s# ;> ^‡\m? J c c;u| vc9sp  BEUD02ܟ xu?9Զ{VgJ@¦UARWdQP#bdqBH& % a 2?_{Kc83<>OrNG鐮&#:dBtl1Ƀ\!:H/ (zxB3(E.'Dc'qxylpa?G]~แyB":!Kx%CFE$ ]D~؂\<̙"=5'-K^? ۸@ߑy;xH q`=2y?'Ano8\].w#ǩݻ555'dOIMl@Р삀J?56a4'a02QeJc\Gdϻ퇀 CR$. K"̠9ىBH`<#!3Dz2 -&`P:' ^=E?p $[ p (/dgaXKg8A}''&/j2Z>$E0|.vEE$X~#\$ 0y"D!+s+5VjX4$?)$'@EHE1Md:9krfXZ4/Z̯_ZYYYZ0&G'L{|ƹآu?mѼd7M[fW-6ܒŴda׶5l֩ xȻ14Kmnjint=wkAn}M{3Cygѱ澒‚tYnI¥4um86rNdu ?h?f (.*w#7RMoC}g]S}Ω~pj`iK{u:Gcl߳~}o ~pd^zC4C[<O3Quh\*m;9n04m|9:(׽-+{_ =.X#!AѮmQ+n+J;eJURUR55UZmgggGw7`QVۢiiֵku&u{*OQP+IeJ5+b%0϶l67Q7|6XIT!w2Y*"w fԇo'KͶݬ:;>۱ܴa_'6O~iƍv_޸Q_,=<+[.ϖefU.ˀ٘//ȃɐ#00% $,37/iT}d#7Of'p/)9&(4̃$.G &\;z\Ǘ„+q8AA(n /E^< ńF|"rDž?\ 4QyR.b+ZCȁ ܨ)#c1'p(P/AMtAk;}ui҉aB:du|TdOĉ`/<6"Â@D[Ȏ$E<O4(s`d#bpE%vl:{`y %8)%¸l:r\]]f us}XA>"Q :3w1w`S,jW3&wwr1YL㖹 BU򙦍6OPudZ^2۬ˋa%Pժm˶IC{~ wh5͵ݝ{b?*}'xն]nma)uSQߚ2.iw:>614G76N'%M @)qNSAI[}qCBW7m0Ouݏ?v[W_ԐϬ~=ڧ#/eEOMǤ+x.uxdUx3~ <ͩ}lx1< l? ܻc{~_]Z:<=67o˿Bo*JUUTTWҨBVvu;0VmVminlx|RYYTHTʊJU9!>ev[ؾ~?;8| m۶}/$@n111qqqȗtYzhJxDKMIJG%K$B!3V9׻}춲ﵳas|E}qtj&5Iڙ}߻. r-.ʱr),\ޭx Ш ,(}ޟI4;q~ֽv7Ś5ozuy66OtC|uuA[[k%q^ GFPZǩ`mh`> ^scssɏdi~)G\++˕R✸"WZ/h@Q RȰD+F4YFrKWeHa` K""WLpdhKdK!Ҡ$h$O쐇#3X ;F@<< MN,cdC9;9;6p7tpȀc!(7mwz*/]k-Vy~0s9];j;5Z^\vW:oij=z;qRߩN^vf fc\?w4}q ;ཎZg&7__>oO2wT*V | mi,/ϸ* {Ӥ6!\MB` Ff\&BPYp&几b~K''|wNG$ }>8ou e(q22 4z,z["Gd XEK?xbR]u${<bDxIyD`ۂ!Ca8r@GOQָ02;IL;}~(ogj ,r Ӣ! :rN*!:UIߔ|$0C312"CP P!B ҳf`iʊ5f ot*Y5,MaoK nU #q`CfzvX"L(. 7ib*ԇjbqPTKr՚;;~ЃχGߙX\pL8]Nle p㥅yxo鑓@Ag9N}ϟ|S}i?ƿ'ipG;#7ϟmawo;z:w$xv֍+ܽ{Pwso@/0UTtttLLLINtu5[?˥v}1wu_e=}᳾Kf7^vx0ig⛚: 8*FLɄKd',ل( "KCDFAH4P.R<̼)?5cet*Lzsm*~b~~SWn];9vM4|ݯf;?EKwN|9wzgݹsshޓ{:N4|{j7U͌ߺ{ӱk{aOBfw.<4t_'of飇wr); 5N+ɮ:!;١Oz{.VgwWG=]{;;v_yq髋^|q{o'Enܸb Vپ.F/ȇ7c*6Xssscbb IOxg/Zhޘ, _tWy z9̝xQG[ޚ핕^uQ種r:jY߀o>QCj;gcSRQV(X^if]}q!@sbmzDgˌeK K)6omJӰCZ%Ɉ JTksCMO^xK]oZHKf\ +;>bh? Y2 YgBVԠD]'޴cHKoFDg"z^t^dYĒqէ[Y"CL~y#oQJE~$+E1//6|B.s}ޢİUj?|Ly)"r9_S Oa,!3HXWtIa Yp1dCkİ &![^drh[a~nNf0X^\ 5A˨pWI,<`Ydro!~0o駟~YiaLMyV蕣x 4C95Tš0 e(Fvq11b! Zh,Kgό[aVB=rQ( i73f`"u1-7T 0L_,A8BzQ&#Zcх]GS}s+W_[hWe-%@ BnUŐPJ+pjnMj\L.x}{E*F[m64נ-kG:kP͉莥h<(ZvA yi)NZ|wkW.ܻ{kfzg? 0,qٿ_܃y_}9˩fޟ|f%\Ç??OOO=~~]8z|qgYǎvC/}~vKύ4WۧmWkNzV=}O9zrrO;z#_}~O|x~moOOcuՅKWNޝm-݃N\Fn\/l9uyd 퉉;w?pc#w义-=Ǯ?JO_?ٱw;wxH|\C{7\1t$W?/΍}GsO{C=Z9{ٳg~'O?~^Qo_O׹_]{^w'koiiz7^Y_RSlfofƘ d^Mbay[V8[^ZK[uU;jƆzD✢>~9_}NנGxk\Z6}ټ^'6={,7ڪ{DN"?0,/ƞcbY3 RŠ6蘰)-2#>Кۘf6lI&|g r)8-䓬J12$WIDYAMIqZ/Yqy^F_)U6Kz,oPh.W,A#6|;:Agfu\jD\',$ar¹>ew5bB,K9}K%꼓8/Gҝ;s:d/AEq%xFxD=y!֛J4.]\*>K%WH/@Z5"Cm0V2YE 1muWBVB PQU0/L~-GߤJߒ?\=nJ35dʲdʳu7gsb!B#,h+.^1 A5AV.\T.R-z%@ -$Z2.hψ؞h5n^WZ e+#l5(%ݐ@wW ("ަSF9[J--% *bkY;lEe2w-J(`9ćQ䛬"S){8f6XSIfV),wgvtD gkVG] @ ! 7NS9G0A @HDP9"ꮊ5~}qJ qڮNO?to~Yx^%`d9Ri PqwLZmoz&6:{BԷxպj}HwD/?3F{|噙b?ĈieU Mܹ}]e3{ơowQSB~a;JeEk?NNNG}ss3Sw7nޟ0[,--^yښNYq{ڇיZuf]\ *t6͐cX;3`i'M{4>=\gе ^S 'n-=ZQt[uZOWK׮7_h:FK+OO<* Riu%sKk[[^ ? O6ffnL Yώo n5{<ҪSUWKrMiFOMFZۢii!3MHwЇ/ӭ︩ӵhh*YjR7+͊f\ԨT\(ݿk77';N𐐐dpTKOK>H>$(F0xELBuRSS\w~nms揇~Νޱ3[)PTXPVZZ%%U2)(GνqjF+d``uu% VW˪%9Kqs)9[OK~$/#u1IŰ҅>U(Q 1^I`eDM 9OxF0vRO|K3$p }awwÔ7򖖖l($)Ȍv'|(_A Q`/ ^V'8B"E] f2 W b&xgz.AM<.$XE<#KhɠN)"7'VTN@MN V4P #0OLhw%+_q J|/pV)d\+  !C w&M`ɣ͡2 r|ٖ`T. n^' µq5l@r0e˂yx xPe *2)T'x# :YJl!G8p T5u<=near<`ég_xW^nnz|c"5+b}Q#_Pok+#75uwĨ5lWNɈadwL5#FV꺺kS(@?ec|bbbvvv,2YkqI 5ecwh-N>0 j Ipa~j]g5-YFZ G,}yiVGM֚&QAg-">~++FzVSq}m~vcU}(Ïqm?bFej+[Wv[40;|{MZ%oW65(*QnjR5Mj%j2LFXoK}0Ѐx?[=ݝ7>B>JnP]R{M뀟A"ɶǎvp: $X.;;G,= KO9Bn#-033z ha) '?ik7{lb?}]_|`h-Z-WZR\A'E'ʠB>o{IRR&J)1a'C+B<1˿ڥ /s2cAA6N"$- 777Ң2bO'Gx`rtL~hMDxr&t%Q/-Er!,H.#Ɵ {BRcD"4Q0! z81wB5r(Bh\$@q 1OA9p.L2RN!G>ACd6!w!䒂(KuB<fS] BP_ķ`9B>_Hہ0ϟ B|Oq|ϓ?SW?`Vά[wǮ]"H$y. ,ŀ`Q@A@ m8B*՟ej]=}s'tcy{$\QXr7f&3 9$!"( .8:'W,xA^#zD @$#yxĄA = ,Dm=>L}ihhxz:JC岰R:0j9.v@A"# #aU_ar8[ OpF.%zw$@LTI>fPPa@et"Vb:H))(QֈrD ?lVG`" qА+ȏ CwxMbρ!YAn¿Sfgq_l-KHh =KJT#i@E>[?_| <-edT(Ȗ$FOf'?3eXxa|:?x 친oo?~Ofݮ{7X!}hpOG{7ln뷶ZzߏlYn߲\6+W}\cT0vct= l*:͖vcFnZXXt0w|xs@pcwڔU#7{u~М9j7UY{)S}Nk}0iY}@Ɩ;]Aӭ{7:n:)nk9F_ ~xhcZ::81168`0^5]5k^C8dz6~<ڬsOWlڡzQ_QߕkWkj."̓7~[RATDi@ZVV>_P(&WoY} _AAAf[VZU>Lkx*RTU*FVkd3@8>:Z)4fc4 AktXYL`VW”dI|Oa3?_!>{R\.'3\.4( y2~Nc3ܔRn4,XHRLTY%J.$^L9AwCL 8A_{eH¿YL oĄ[X͡w!L B'v¢p9A lco'I#U^ҧhr4ќD#|?G.2{}p`$Vc@5&k8hXRWS % ;>xvDc r[tom'}0[pЋ9%슉 g!@p) /ȓ &^.Q(MH5;H# y1v"!n$ivH.! % ̈́X <)am!T_II;ԇ—/_*+NWԧ$E0V8'?!&"//儝N *JHp#$ 5+@]t9bVnxhBSgAAux TNFaHFJi ,IYdҿd$eGEP+d# `:YC♩,4~d~qo ⧟^,̻g\S3y¬1皜&vp9D<4oj@}gn=#G͈\*,9<<$BzL'ɇ(yn 9VA;HB xh4\^:?G(Bv۠uDmB}A[ъd&\Gc4^!lY{Y$\K%xU2` m.(D'\S}ɂEq$n._B;%Y(E{8S#`QG"`v*Y 5##$W؏ux k/^x8*D%"LYa-RE]`y6,!+*g d#+-3p SUzJ-$'WEѽmL$(bSh$9-M 8f9cRiE=.1fKc|DPA/ 9ol2cF|*[,Ro>2׀?^~x7w?!֛_Y?|xMef]h}waݺb{wflvۃ;'W{dN}~Bϟ=t+~ͱѾ=]- FG: obyh}rr|rg__ JսvUWWH|o^wO̿_OzCўU%/f]6_訽53`Ϗ?6uydA{yXWɩھ/;묬ꯛN/~v@wKs o;]ul[cb #֡Ss6qd09~fmtm/W.5<^P,ܨD+i1M܉ng&{P_{ ^|ӍߒdmemMeHUQ||./77&Ψssޤ>.w@C"]`H Wt㧮d~>O>|vdnjh8| wH1Q,%%%0;t ,-+:'%? ,-%UdKQ%SUi=8WiڂH/?UxoΘ~Ŭ.̈ˊˈM ̎NJAީ۾E]gV,=[S#DlM4+rCz8m{jrVD\C CܑvV9#80l,5*` ׮]s Rl*x83v (GvsdZDIx!_1"1 v=4w-ATHŋWQ: K |ٻ}āْ >d/'ޗ4A}b 2Ԩ 'S?9\9HCH JeM #'H, P4l4KEe pK߄4&џ2jQyWl8*ۇʰSq0` B”7]^?F5P(-D\>4qD7D*rœ*|XS^o,2oJrP IrѪ%SAfU~"C:/%99riE(r*F<Y_Uw0*33y$yz˳w9e~v݅ɫcS7&ּ:oYo-Z[oi~zy΃ŇKO<}=o{eNM]6ףkWGMgOI]4 _2\iq}|闇^W_)q-9Va2kOrne67\W]S}j_2cstjJE}@ ?6)rRrڡiPTWS)k6dfTL͗ԌADQh6Ydmٔnh MЀ, -[Hq==^ů.^/qXC[k;WK]ծ*u 㧲xȫq=W U1^m-?=q cH03Ӽ#H+ͲB>Gݺ#Xn*KC6g"F#H7Q`[aJIg Bn`/3f6P&%?=JR6#m1AGt)ɏ%h0RSGȈ1|V'F$3eWT."5"6٪3  nr/A|(@S%d3$XXV <.%b:jat|̜-)5 ր@6OnEF:hEIO E)*-/t~K>U}~Զ7774x-s3}=SkyF{w3ˣ7o< ~7oO4LONkn~,u 3Wg{n_sgoy%~Ŏ3uãm{Η8{ގswڝsPߕ+Q_u܍J}⽦F5lV}MΟ8Aoo|3Owc G9|CJ}FZsNgzzfChQ䈍-W QTi+))sB>oSJEUaeΜb5&&K<_x~]Y[u]Y[2^Unb\.Ef`OUuu tJFnw-KnU3/i#c S^½vKV#XH&XOeoِߙVͺe|h*+~CAJXNbhZd@Jd Kֱ Ԉ)dc)F  _v\0 ¥F!mM+͈Z=!$;~}FtP ʟ0AeKl"f#`xd[q/䗤61n,ɛ56%qf?z#CѦt5TF|Xپ2G1:?k-8$XL4{ԓI`ra;ӪX(ۼ-x6=\ySI {C›Ivy9kW&ojA2M7lެЈeRE)3K :p ǭ_V= rLt-][POt)P)E,iJ$h8XbVdׁbyI swgD)˶P dSF^3`6LHXߋ&Ɗ^\ޢ*ϲEK1{ggBJE}u E/"Q6.K>Jo۷aw>6oыcC?Gɱ[7q_>Ӆ~y![6omȖ?`S~&WLLLԵ0kjj&&f 66766>⻶6ho#d@yjUr^d3L2dJI fPU<-bPZX b -&e„=:%;Lˇ qtey\KLn̈GA gK"!4=}GdI".,_w؈X2CzXɎi:ZO d#E]O0 -8fz 8CCJG,`9 R-kmZ\ElFоRFQ@#p&Yml4II*Tw؅ BnPv)4"Ш0uJ֩0yr(i&R%€@ UH홒 ;D!/,594JG =g;G0PȓÉ]RF&@^ `< cR(Cäp@w?s!ev!|?uvv gttŋw߫Rǫ:6=O>V -,'4'ɚl7:J;i,SIjB-nkȧLHC1v=iQx.*`eC 5iLˮaQJH5F2k?&& 4ԐÖiSvW3LBP+2 w [cNˈV;m~FNhB_xD6#0c&wڕ}x6C/TKL5&[ ;[*ԟ_}2'_b>o*{=|m{(nYI1!]9S2mwvuޗ. ^9yb|vf*xjft`~ڥX'Oʻw~p{w.^\Y:@H=x}Ca߈ }c@`jjrrrCf3i pD}Aǃ?^Lw{ܽhU__WQQhZ-`9vjWo&VYiy5,^OIwrNvr6_Lj¨x<>%d˯6mO>|#MHknj$kFh T?m{}m-)tt|_d )u4 H%Xы7n:bD YQ[VW)RmK(-+i-ץ*t\)3ќX\ rJrGԝgI4k8٨ⴊh Ic<MEq,ZG]Yh̑FeF! Ld -Е#c: Y ڌ]l D%֫@Ih0A&D(Nӂy1E"B$Ws 5bNJK͑ e`dLW"7QĢFCYҝA&C/), ~pdϠT^xJvav! B̂<1 '0[(A`r8'I H/]`'Q{X#b ~Kޢ>|=*^YnR%^J5DWf 25A5~VMa t D@hL4,xpUҙ f1E lSOU{%>o& N|L)uB C*F4jXSVb~ +UbbA""讔 E spq/'7J(/%p pط ] C5P*0[OMfg٩նnwNg:ө+]P5!!TsD I%" "Bꎳ:;̙s9y:|QU-T*FmIYSDJUDc}||[0!1Gpr]r;*s[ YMei+"<sRd(،SCK㓣nם>9918pmճ}uClow{`7/Ὥ}J :6wn=;؁~ yd5=}8:9n۱wqȐc9=56x_sclXW9336yn*ŵyK-,.{~ܣv[p+2}eϢ}6<;<3;rݲgiFZ8Ya7rs|nrm{_}gѮUV[j?Z'Յ r`h x#u'ڻR{IӠEljZ|;~?~Pԃ5UX-v2Zm-mmNG_\wA>M4Azwzzutjk77546bXnXob455Y- B$%&&f\.W*lv{C}EEELoƗ9|BDuLRdd)))d^^+C>OC>:|ÿ|xsuGG`wㆦ<:mN[}zJK:V-WA} |z#A&f,Js<71\.^S dET矻Vɸ8U6=lAAĕ\ D:Y̎CrԘiEL!#O&"#8d 2 @T~`:'^4Ҙ4ϟAN7XEa %#:0סp1H NCޫDtl$Z*R~pZ $.JII@QO|9u& C5Aܠ^p x|Y@9A#tGG*[v@|4 pf3()"}2`B,p7"@p# H8T|g#GB ';x8J~̋9')ND[|`,sDtǢCSCSp<G& I08$8;X.;Jłp eTh$C~qV QŠ@LP kD3YL%.QW N^$C&"ڼF>" r 4AxIИCh4d =A6,A#s\ b< sUF$Z*3rY{`!YJpK`Ր&?]B||C$#H9MΉE} !:G ql@GBCVdr'+hcl e5qHį0џd%N&Ӝ]{,(פT)n^[ԉE x\VU85|45<678;1~potv̊gac}ɖ`gϿ=}}o|;OO72Dڿ={`og;㣽=!slDdpzۧ&Ɔ}'ݝ,7WW~b"}GS[ 0ߒk涏LznqfS=|%ڏg,7 &Z,**+nMLVaZt,8r b c Xi1/iw5=0fs"/D+8WYР"#ei Fa:JJDA*!ߕ$RrD <@+([&K8i@WF<0}>,OE$\v R^J/PF}Aw81PO&sRA( [.qrPJ&>~2 @AHF|nLS%s<$ 9H#r˟T CӄI$9- p,?R` ;[ $^l//&DH3 HW{bÜN$}7 H  |qA..#ABQ?DmdwJẅT.naȷM8ﭮ}uuz۬MW=e_p}OVhL_BS| ?~@Dohl5ߘg6?~Pߔcr6\WSPal4w20c'ޞÃ^ 3̦>Rl1 X̽=7ȧklԷ4׷: -mmX KJJޒ/!9ޣ>dJe9BNJ\ )v Wx)E"ŧ\>ⱱc|فOw`/Ϲ7JJe?T%jP,Q+J rzV[_ʊ ,-g&g\ Ҁ7{p7;4.,'K/,O !@i1^ x)´ i72Lw^ ?-++ ;%$"13._H_}]柁B.) 1 = KLH Bwt1$ (v 5brU rpIP;r>FhiOL/`*YN@Kс P9xCEpc9hYIX.D(xX8. {#88 ,Ca[[>oH0/vzB_Z @^.o )3a)!P'`@s:Up鄿s.L>UqD$HF<4 zϝL*#\yp |Xeä,`EQ*1H,"b2 Ɂ(@G> /+^*DP쇣!rD B?GqBZFhU9Y!v;3AEu૚ 1|Ue`/,2# **^,ɖQ(1/C4ReM~D"_BtBz8SY#w_TAC17ȩ Fhڂƫ|DB`$2QS)2z\DX j {AڂMتN4(bb#+B5L 'ه;6˄mpjr1123E[3tyu'k7[l9=Y?{c/@ޙw8cɱfDŽy1㍍tf1a93{aV%23g6 Z}7̦U_S#W!Z CTWW#% 9yR)“  ܣbJѹkSnP_TTH$|C<'??s٩R)UZM2h0R5*6,Qjh@r|RPR!*cy]N- S @.79P&‷^MQfW%y7Ie2ʫT2qaDhiDeUMDhDªh ";"h&o}Jj&Stjs9MsP%dM~x8(50'YKJ{hH>LsPu&#.w!",U2r4ke;u7AΏ.{ -F @(ez9pwzC2jC_1Š/^耟b<Ȝd.PHlhͪ~#olfd3ɴsCX'9` ("1\"Z5>Qa^1_%;`E |zS|"#c& @B搧wuর]nRFA$Ft ?jAb^oi?/ր0l>ɯD!Z^RhvbHPN0HrCe'5(S.S0.FzB 'ՊyH2tX@B*Gtq.-Jv5Ȱ*OHG5IA= ʳX *j5r $tD;"A\,ϊG(%UW (U;S02IF\2+>Eð i~rͫu^>/>b~ܸ>0w?[;~{d=99zedwbv@pMOՊ%0kjn..,x<_~`xfj .,t \t:Ra O\,6:j42Kˏ7h˻~<^YXZ1uen4LO\:pq]OwDslQ7;Zs _:;3bgql+IcohpvZGsvEK])u͛7@_}:Os\LMMKjlަدBsux OIEw8V䭇||2멽TO5P;ѣf999YQgן'qqt8k3Wipa:U_Ee9q guuUs:k*)@B Rt%Mp!T!ԈCdua$3 %EiaqAfL%4CM`qzdpi졾=Stq6X$m]Rei&þr c)TkKC_V8hYZ6N2&' BY奴I@@2@8ʹ A>6 BbJ2Vi1)ݒpybd50&U+"( eNjqB~Bn.IP_ϢRsv̆)1ߪeђv/%h12zUۭE4_1Jj4!=/ La*&OpZD6tT>ܦW' t!v{Du$!=x0ȉ-QY$%9Xw!{8??;r29{g?GmuMm.Ro~呫N,,\Yvhxx(::rlRvMI?UsggϝL=(ڃ|#OPݤ8=Fm,*nn|t=>*k:K[u9D?)ߟϟVft>?=MZ\QQX}eϽgy?3|d߾@'|ra<;]a]x/tvur!H({/'ԗ[qiXpT~¾2Zۀ4!Vw!ZRoU4R~Qf_B:5&F1*1|UP"0mB,3  +PC+:F:haw=a0րǠ("l&MA_1r\D> p\GH ]P-$~c\$= K$EVL)}d ٙRX?1 +tф!Z( ȎO:GxkN'IHx#ʎ6ʟM6D)yA]YsȧÙ. >5O7<^ovƎ=/8[\#G[<g_c zaޑΒmgN.-[ZY4mҥ ˗W]yk}M}hw{=wC}C2>ӻ򋯿?I}R/>pշV-Ό^]^p}S]pۿGap7Fٹ`Wdlt…‰UEk7{ݺW'OO\V)췺pllol.5zƭ'j?yk|GlTn}r?~?Ͽ{嵥7O-ņ/zcw᧱K`ek{,N >x}+‘᥿[gޞ%·T͏ߏu.FF_991I>M};rqxK.[Hr WϾ 555vC饌I猝/S6$[j(*3A"? xJ}PL* WhcaI~#m H np<ҍTb`}Pm(Cج[a.W6jtS$lSkʑxvW0;h#Ґ䠲])&f!ȄpNd!7>ƨ3ven>9VGnLJ|o"_ii)rɖi >D 뎀|Hx6FM*%_?g BF6_棏}߃.U`5@vh?j?*@COouux@~\Oh &cHS'WA;>$V`fW*W,@ x xMe"li&u&\*8&d 4'i 5HZec]kRd2 *BPA8/: mznQٮ72ɅT0$>7eO#ot`3t\)mզ~i5i]-noxȠML-Ϯ,/m'{'{{~{T[ @Ͼ?I@ʯאַ,..NߘpMNg;/^o`lhj4&$MZWWVޡ>-ޚm:m;meeO5{c>^x1nx疗mksn}NW'nڰpiƏڏokeЯ+qffz} x??~skgg{獆5R~ʻ:b5ٛсffm혷:!1q{&=Pj+Ρ~G@q8ggwvۻ=NytkE>F}h5Yկ'҇%:VOVh*Y_O?'9+[V%I~~>wi:z,g߷DՇ _z:V[]fs fB=r&b= N/di[Y-x5|MRq5 5-!#2ဂUBtl<+fsH\Ex@ttAbcyhp 7D'9JBDrg;<`DB;60RA0]ď cB0bqlQ bxxEc~01; trPO%:oxyeq}w 7fW+p[ {?ouimv})XY ꜹ8wQks}q>;UóP_lz؈4Z"𽍍յ~en6dvn7z;>g`ffo|< ~けagh?̐haA@`>wdQUVVpm|wLH'P bu\q'?nÏ~ D(Wۿ?_]w"޳6t =n58dLF iXazfHbhMFcg2* ٧s.l YUG'uJ^ίg^(ut<]#MS] y)B)bjll[u\rV)=T,N,$),O2Oe& !" SAhPqL@DG 7~|>+(@DrǠA$'"hrs8ځKrӏ%bc!A)HyW&3 I<@chw \O D/#ZfJ0%}`|A IH=LBB&Q\!䳎 Oe2Rd0@2 ɇ3L"1Ўwdb>zX80`DLO>!F=7 9H>d&I~fhITZ3d PL%YZD{ {YD{'wyd 3qZx6~OS_cc#~bI} OS(?e䌾KX@e @)L@bPp2K°V zA#X)^ΆbAk.#[%s LPq@2em&uFkЩ Hs{Ҷ- U*>`=ab؆)K2aQ,;;T{Hȉ\rN4kh+ѧ1hrZ 6'7'H< ὖ!Zڽi05Њ6AqHCECةdy*lFUɋ%4U;Bp$4\TGK/*)xSGn6Vb% ;wbH#J, 5 *GK}M>g񠣰(iןsj@>*I umԋ;ȬFs{P"tQ9]Tx{'#ӓK!یnFW6V} ok#vs=`kۓW{NWWwK=}ɳ'?z|k|mY::::_+͍2Ԃ0\XXX˻/foo.E)ymvW,ܾK^eoHh(,ky/޺997v~'^zw;v ݼ?#?֣kk״˷p%zq-~gnMM.Gߤj] C?4i7]n"i)ivA}@@߀ w~z &FGGAoA G|{C~z=V}PW[[[VQ}G||Bx1]4M/~ȇ~O>}J2++K${1T]~{.|^bkZ,VfL YL f@> X(h`d 3‚U͔$T$C}^IVMM] 0hȩy<] B`,![#|u! ޫ!ۍ9S{kΟ)JT姀dy @8I&u}piszڞIq\pYFaaD " 00 3þqKQ#@6If=}Wy=jMSMs̼`>F +'#Rю=s^Lhll*S"S:B:ҜM (2mŵZqzmA&+a5xүBTrD ꄔ FA`F 3ߴ㼌@%0QעtP8+"?=F]I<%!+A^"n%lѯ'gBnY&06 汈 ( ~hu)R sqTRt(K|j!C(M(1DipOnZ 2)UrVxODuK!cIpsDpFʁE¿ qkll?u(s 4` \ysAPZY E<0ℚTWIBKHOSJB= *ol\qZz;_sDZ(EAxu|m<S;ס1QP>TYMer7dw՞z@'{ovfyY\,-ߛ^,,ޟ_;pg~Wk3ߗ/_|}s^=WWYmyM.\zzJySӷe c]]]]t~kf-nɳ=Q;w~ f~cʭQ53O"{Dqw;;{-gN#g]^6swUߑc7fl&׷lT}^ty;؎ophpdxNwM=36:2VZp[є~f{l9ZƑr0%%!q ͇$s\C4imH;,){rVY#U|)F'BKń:\tf-pIQ4:M1atx@ dH*6 G.Ή@2pqER&CRiCpPfH)K(q}Ҹ`Z&񑢾ĨPj0"" V2|Kt!_p%/>oY'+[ rc!PP_INѸjGDH;|*U(qJܥWj2T(FLxq@Nj|5:HZQ^iڟ O#Ґ:RX MꔈdhQ`U(Ez m_eKMȊSctIYlEƃF>_ŰʩD'}=V VQFO5"[EhN!CQDޖb[ls62YjdRpD͍C)6?;^,Jrj7S/vUJ/imՙ=Mr垰)[slmQcKSi'uZ*۪֪̣mG "㖬-G4a x{|npP[Ư\zKw;S w..<߽;00W_~cwO>.^}r~Z4UV[ZywyGu]n'_tUrsҵNݭIo6ikwk?v`P{s;j]]twgN'.6|蕎_^| LNz|͇]<mU|]ӧGGF1Po706:4~ft@ϩGTmUQQ,A>fsy5P2j֥ vBկ._U/wb}DqQQFFFvvF>J;nt:]f9{ziA>Qgs=gc;lDy_vl6iڴZ,PT1W8}a PD-޸+(RzZwD"%%ay[ݦMvy<{xM:Fp߹p]Նm/{҃>GԂ Me⺆ЮD&Xi{Mz|0†IhفbՎRQU|5;F򪂴z5ֺ})y)[]W&yy[ *0G,k33)\zЪyLʔw:vdēUNҙrٛ@ LW+Rƃ^hE KG1&)J[p61$LPD'9͘=i̛rLN3r&ԗZeĖԞp+1g yЊ|rU\g6Sѝ͖&Vy\x!Np#JHY'6CCzPUI+A`cub1!%Z mЎ#L4iN`CN]k&)Un0ͯױDaW]_jM@Dt rZ!F}W,uiCVphO /&;EXKMa~>"TzO Z*VrL ,B:ڡ j.I' vT[IjdCQ?̜A%r2c@ ,"ՑxQkƮsb]O| S}ɭpw+sCD54ȝsY0Ve~N#{;v,dM}:FVL/NaOdd{2V)q X7h)0;;[v8>v-+?os"7P7Qt$T7Gۋ) h(8uuk΅\( -}C[gNLM0GO¹K~ ~?~<|ӕ>dP[Utѳ?oۚ?Xog}7_=/>{zGcS6n޸emuz~kc~~~z&̞?tj̍d g͆&N]Az?wkХ .L\=qe?4=YyrmڅfM_S_񖖗txbƃٹSVV>Ym8בּ*ZzРsr+i s }-RT0C&h¤)ot5'AQ$@W#ٴV@=\l؆%-RmFz6$IZ]-Z$S&q@H+e FdFKY8m7rbjo0h-Nwz5jQ1)52|!?g81)tģp-)@0Hg@AS=} sW?y;7~~q*do?x>Mg|rB$rȫݝno` $Y`g8 vA^l=y~H}va'6fUZ 4[׵V+5jwYl;+创͘0^E#T_hВ\QV_~&BlوRN`)̓0Bt\<X]]S$j?2.)G0>A ϩ22RIq6h1"2EFgZE W~,S\FK+'A8\af-X̥Bb { җDnu o!8 P,d䥇W7ZaE젬Z1|@J䑄c1㢈C퇒TgW䁇ΘqE&Žzدń〺0Q 7ɀn")k=NMoP.B}h~C xXj0[ T8ԝV%hd1dHj#_HA 2x"4ZuW4붩@@loX':+oE{,@ wU*^ 7*ÆhX?e0 ]3\.pyˆ._w8w; aBQ@SC[7L&--X[_/ߘ_YgOQ{Ho}]=~svoww͵9n4?|4xp0>w|;7h~C}CDtfffiiqeVV76Vߋݻ ~cwB}=R8>x/:6<:2 S}MMMNѹed7UMEP帴qsyO}tB_᱌c)9N4:ql799z\ /ށG@?f?utB>< 4Y%o(kVu-UJ[6b~} Hvw٢kj*a6Hŀ$-kKm&dR%w6C>d[Jr`3.˪X/J WUe 3y3UEBd[,#:}h3)Ȱ$PDor0Pe&YFLV*O.'܈-2yԕd5 `JT%mj(~&z2앲zƼ4FdKަלA.?5 PiԊ68HB tAY64R" yyzC@9 ӈ^DLIR 08$Z:E1QI AR_8 +KbңD'Ξdt>lJI09itĬ%T/8dx9#"`NT'ΌeO:<qp_M/D͆s@ 4jg:I$RF5YɈ،SztVUIZ+%c ;b*lT>+Hd$˪PFt CO 1q *ɑ (TJkPHITu5(h1(("5(!4N *f 44yxCvsMgъ,tMWoQ %N`[ў2V`V{dWZA) zy`ޗav*dš&, Dm➖|y lɠY}z}q<džg{-/.,o>lۛΖ6~0EzN⛧|a{/_| d?ڒà6p|eWNj}9xW7_jT7|~?m}uqݰqecho iK^4'oy9Td[_x ~)v G΅;Y{ = {w~atA}D}}vqx/ssssgd)3|>ipXvk6UoLQ:>%K)DLH8}$}_o~@P"\.W*XG{;&<]t:$:^kzJ"UzCk[{|)2FR) Rjer/2.ϴTr!7h$qPΨ/c4K3eU5@<\d!PDWu>V5H³` ^LLX_NғRS^*`)KD,ҼTDyn 3𤄗\sJʲT"?$/d!8f%b^A28PX!>Aq4N"05K!?'UyOi ˣE8́=xU-Q@b[fQ Qlpp,\LrcȐ?S?/3؝NLwHU* ɕ;! wPILH B$!. !DB@aq].mQپq=meێ/7g9y0}rR&Cd`џHa3+9S]Q &3ٱ,*-fyf9:4b)LHH&9,1Dt K:'q8T%3Rb$x13CH9OF!Crڊ2@hnr4O1' #ԇϛ7o>4{Kj98U`+y2VQ>D&~Xk-d VAbPaI-d$4|@ * qK Gt w؟lΦZU"E%:&zC>I&pN$0ā8Rvjp.܅-V6a5< Wj$ d!jTCN 6'V<DPXDpKt|)YhUrHgB-U;)*q#|XmȳkCw[mdžCcx~x+޹=`q{pog`gogswkmgseokuC.]x=~?G~xo?_|Ͼ}O{^ AgW;WƭneÈyGÏ~𫟿Lvup nwFf"E⺟m^w߇wWÖPGduÕI?`ni}ުG߿>W5vz0׀ZÄoz:23335ycvjr. Lǂ7BJ*j5E )6/24щDStR' Ʊԏ%iǎ_S-OJ"TZ[[괶_{C~Vj1[-XͦV&gnX(mo5L&cS/FF[-ꂔKe|c5&U+ ݠfRBhʄRr*᫕z9W%"AΫWe%:GS/8s-+c%l,"'dL|@]11p0.'*/\\:{!f%/`9XPpR&>M5.DYJ_oKL/^&@ÍaE!zJ%H<#$^)$ PaB LJ%8v9$K",( qb' sdg(#Cs¿I )8 6;>c] :|# _841=pg~]ocme'@͕ݍ5#zo?˓W/Sz׈aGjơ Fܮ!istC܉w,,-KD׎@zSk6թD8X췾`y53ꉌzBs#յ[^{ Pڂ77Ik?m7K1LJ9;;{{~[Έ17z{W>kR`[ NoS[l8rk"&CPp"2:8~zȡzԦ$7l m2o:=ٌMB6K0ak8@SʖlREM#$^8{\ZEM==~f.3@ދ<F7=5>UX8>~٩ݻw`(d/Cuo@`|,c}b :[An[gq[{6vy;CwZ34Oi!8= 6nDnHcenCy.h5ov5[i!$ۑ,Dt9YQřQV@W*/dvNhҜRaNjQ9S , VE NS##%$CMtZjTLTu(%2p(IjOUGpy\YN IbճB]JlmQTJX1Ic0e:80a%̼%JĘ լ9-3)Ed 1le6m1m=Oy>9:!_ZKv,iȮs3I\ӦXFTz"Iقɨ@+ȴI8iΎ3n{k84gdz4dѤeahW:mCV,[YpyӧY̾Y"1Zeyh!`i >1Q 6 TBJ clѐ*Th)i 7;km[s;zd̒>#p (G>rЩrc `FUn 65yz-|Tofxk5R7.C!MLKB6DZ @kO{~łx65wi|rgK9q 07Qݎ­Q3ѩFB2,S9nn!`j ɏK!ԙE1WN-C}rV@g58W鯞p|_[C0pp=sc-sc3#n['ۦkBC*7%EGE}{eHꐯ␯cP}4Pnd;aLygOc+]𓅏ݽ_,_\\Xn/~v{;KCg_=˷'OY xѣŻw{f߻)J=n2:wiv}pS 7n}82|o^v8 1_1?~w|oύ&ΎN??5{ x_>pO(8yjc.ܸsǂåĕgF..™GfCc;6ODΝpŋϟ;=;sٹ'̝>uıGh2Odz*259!ދQj{>vʻLD/fz-aʘIW%Ȍʈِ, [V,/wk^Oill,,,ꄠ3oxxx(I/: C~"P||zut3X6UʳLFOӉL@{i:mN⭷xLuk iq;LO-zPg$CbB:Nb3۹UVc$,rsZcpg-PkҸrdJ6֋2iaGk ӕRj@,\o] hREZQΠ4vT(ǜ bEi,'S"=ҨBUy"Cc2HQak0J|]y}i}Ap:WZSjˌ38>"]Re=8US,xKEbM 8אRCh$"jc8q ȇm]Q#Pώ(ەǬ(ʡ늄Ѽ)~ T 5Z"=g>첛`FdK%&3S, ͉Ґ W2c9Q0 ~mQ́& 8 ڵ;4;*s*6y@HWi$@)&nr K>p"wu&1aw+Uyy۩6"]OҴX%lԂ1Xz^#|#B"Q+)-NHl`HUqQJ\D7=vĄn9)YlXTMIxIO.T/Au;De Ća%75$NDtFF=~.]P-}Z~1+AGaVގ7m`Q(l2X0E"9?6FjF}==X;=TV95X;9uiLyyfL6ىqu=9 rRJ[ZnJ[rh9 g9Zz2ȡ8,E3s/|v&3?y~?H>7[:[~I9n+#&6(4 ^2ʀ@AګtfveN zTuvFh :U/176]N* i]u}ݐZnK}ӓ3W&o|3sַub?nݿpoikm%껷s܅W\'_<Jߤ^~0GO?|~ڥQ^]zSp4o[Ǻ&kl!z#\-;mk:̐Gϊoenab؍k..:3y~K -=˖յo~O~};;_4^h7w׃}lw7¯\7x\aC8~d18V1591991==5}ozz@7xd™XBvP&OãR8z|Alq"EF=8EPOwF}E܎/w}Ow!eeeRt{{iV>cAYh66=kF +SZժaÓ I$ՂslU _L |NuBM-L/W  8urNu~bm`}!^u[U " )BX!M[*$ x01H1KE*s 3 2˥ ̘bALqVtIvlYq \T|@]NaR~Z$Qdǔb9 ? D` .Rp4iJxNR,lp \e$"p GV!2/9B`d8"Ș&-8)L@rC2YAY`,c`0F4("YHpCҙ`(qQC(KHHs H-El@ [ZiAA"CQb|CE<0(.08B Ĕ&"A&@u %DB<P2y`жxԛ!ޥMqr@j0hYGNG&-@"tёL]-: r`s9k #jd-&ad41 @&ȍE!WEQ`(Adlp =m%,ڇŰxtSګRp*[QyZ}LS] 8N 3/@"賰* ;jҴE Xf vXuZE0PRҁbGs"L8oRv*1Ke[չBNjii lzxOYQѭUg"S}fGmzЮ7Aܭ]Mҁ\^ :^X2`m?1ty~p;244suŅ{w׶67 ; זXڀVq ֽ6ы_| ͛o⍏^z={lYy~ ~̷@7[^Z[?}N?}ڂe2j?b_o̲?A 5Mݾqkv[zgwηBx2gs޷[nZ'_ݞjlywfff]1u NNawm6kwA}SSSS@8v"7:ratd[U;U̯GyR4&.uG7u N4$T\L%8,o(0 ł輴Y19мԈŒHL"@> (!Ђ@[n*D !ad:!A(GO4*KEYH䤤3"B`p-qD>PPVB::Bm1!BNԹ|0(".)b]gg3&#-!EB1* I =l>A 1 _`DJ-b;G&,mHB\4O@_ <=r`P/'2R"{*!1~(q ?N}MMMv۸JsJcOgFuUpW\-z(3DW(cJЗE_W(C&VŘ'*ˉJsbuYQOZ/F`ՠR(XK!%}l*r`4,*"r]C& 65 1 dF&pH85qrU,vESb ̃w9ǢAx [W?lМרq[T}V*ӐUoU XUΚL!s*RKD)弄۬2e-v}r>Øj)Nj׺Mk>G1s (@a=y2ܸ軿>|ݻמ[yij`/_'yt&^~>r1/m_[MOOC} nWڮΎ~I8o \FG=ϐgxG熿D>$_u8 ]><&Pf Rx? =3/|opCwwD-t=KՊtsUUeu Щ2j:(+LR ZyZZ@eZfȓWǗKTR]9D!8G%CwejqI8Gϕ:%aRI+d8K|8?-{:KI>0ձC1*)\C#pT["Ъ1PM!NRDD&vR(̈́R0,MOg$7ròuR%`Bl9L"b0L񓅼@ZR @.9;1i3 ,1/G! aE1vƋ'сle{Ei~I>MJ#$%Q? RHGHTh|ٳD}E%JGd*UK@56k8/C}dqZ" @ tT%\pd%rH2aX'Jsb 8 P6 O y@hJ2n`=T(‰ZX#Hs 8x *@nt&Π%jRzbz$Z( odJeJ1[1Dc'O]p$ hJ GXur޺D94 xC2q2e4˜+B]u˜;–Td! PMF))0 Cr[IBW}ۢ1oL7i!{LfMocnMۃ5Ϫq@ڮz*Y{\mVGMmu&16עt?G Xf}JS4gu*NX>I8bh+;nH|X[_8y/^>_h? >}:=7`ofvfk}[ka;ŞKLk^Gbal3 Tiןy8w] >L- ş~oy}sɁW[ZA}SSS3NY^O+9{mPo199]/72<v.?.:]]׹ Cyqq^///yD>?/xQޑv> ߱{Bڼ=ݭm ܽ_w>x[~֦>'7bb [ZmVBX-a4,1[?mWŦ֪j]#%mJ @HH `l ~31`!  !%MejK{.'T}y~M{QU۴',>с(\F! 0^(t&uֺҦLHUGY4Mr$NDlx{&GE~!wk y ` 9ԓ];1ÕQjwb;kuE}Ӑ(-0`d)43n~, ٻɌM8Z=-!KR:#(tlf!MxL{ fCAW Brce}d)G3Nj"hw)f3?\8f'|d*uO+pl}Muqz.XFWٔd,XT)!پsq෺ziWo}?{w?~{;ow?<~w{dv7xxosoW~3}~e$S7}Aӱx|е^|kja}nh]S׆?wonxmomiw7>}kcS6owGu.L}owO֭Y<76;_Z\t<ٹs sna~e2߻j껲vekk+KKoAO𛽰03;39?;ۻΔBdKC GB} x5I?p}'^ا{wϿ&)?s?'vsPtdd8b`o$zvxGCCoh0e?P;vn%=^8[w"a<82u^S{0Ԙ)3ux:AK4DCs>COά*EnXy:S͙ N۳);]k4Wd计u-"AX~sD} SyKO"4\(A8-eK>EǝTELAAk4jjd~j)̐ NixL{ DSfFQp)n L+3ܔ)"CxIG%JHHVOBA2iA/cP& uEĂ2^k*#BB;2Ya3دĐ֧ĘUH)Qh'Á ? \-H.'sס>*-; x ":YPe"pyg8qIQkVL}}ݏڝ-U13[VI͙70&S Dh *uyt2QYDl0RYh`4`].\c :hSlbTƩ>$ iS_#&`yt/jCSoA|*^,]+J.j@#W$AT{ Sd 8eBN#4fa>UO+i&Tm!yg@p>sk,?jn!; F6ɨS6ơG^ȳ; ϶˷{/g*qow{{{?;~docoÝ&D_?ٯ zj*=?tK;z!$\UPIȅpDBHB"CkI@0ծ^v:K{vfgN;{s>W1{Co7/76ַ췼<58 NMN'V`K7W'Tv+GoZY:8kʃ{>~kկ:SZzU_~?~OӍkn֝ q7 /^,,_]937<7?7 >w WsLOMN.B~Pߨ0C~!_`3440鯫3L:a82k\5xHȘќC]ؙ͍OEviOĽ_z[?f9:fgg Nvw9活AWˉpڬfX losnl&Â&Kk3gi5ۭ5iYCkЦ%jAFX* eTtC@ĆR1 ZRf @T$v5$ kkb>dGRP_<0R%-L(]^^*p&30LH#3.c3iyG ` 1N>V$B6O7hp0F~X-PYq]q TJ̀H*(N*c;!Qh&*E/+&5|.j(I1TE5(}0-HV-n*%}p"ځdȟτ?:)8ZIPd S 1CF2QG7-,lcix 5jru2 `XEBYf P /#&'̦61$Fl\& ej8l:>EmLHĉJ ^ o+ LHϒax씗١nCP|Q앍s`]ά` Շ{(CKpiHv[=Ҧ4~k=]5֮ ^X=B{L^SapEW- a[`kQ!)6צpVz.CG¡ 3o췴pӍϟL؍ml{ps1W*'ݿ _x.4N߹vG7??8? z{<0wnt zx}<ޡpV+Gbʬ,+G?g{bw8s_~V}Q?Gr"vl`1ؒ~~O41JMԥKP<~Xgvᱣrm;Naڭjs\m0lnn6fslj1,bXJ&L 5$ZQ{N ,T7cETLѬDN-Qk`^2JxZ->sW"<s Z1dX^TU 1$NS*~WA+bm8 jS4",v!`T*3QMHiB64H E4 SJ%&Eč**:{Q a#l1H2$a ((Sɩ0?l`''V!G%ZYiDwydN`Q%jSw p&% t3nBQ!:+φBe), 8<4,#ddtST`ԑWW42+L'UI@0Ac)oФ֩RN)ÁKh%tH]<S Z$>$Cٿӯ&<C_̼V{Z7abQ!!a ;H !l/AZj"Ng|OSgZϜ3Ϲ**L̄PG,ąX+{ӿmWݽ~g~8xmcnW~Vum++wn(5K+F1T{u66>[gC?YZ^[Y]pz] x]ѡbߥ :16162>62囀F}.v9azmv^.gScgtQq"b ߵrh#C;2fމܶ=x?$vwhz7&|Suuu2h7[ͭ&nd׬6|MF`gVKQT*>J )U FTieҬb/+$R[_A}=^j%u\PPG̓FYar 瞮""J( CQ^ !JKبK~ 3O&lAVqևf/9{iř(#Z%GSBF,$@{ GDE Lx̂Xa*.q(5:?Z>32OD aB-8<qT$.XVyL6yr#=Gh-r rIe=#@K?!#hJB`ԉΰ\*1<Qd#@;EkAHy'NSaĿ DKO:JH9ƄDb;P0&`GF8l.F` 68Qz36#Ճdt :6k)PD$p"pJAuHZxVt Жq uRN$+$ ~CAiS :BQP+b/Cbldmux$72xZj:`r4%IBC`'a&%Ai-ԡ#0>#MS:eCABH}5sBo!~8cTK ym3*6 Hc .KcRHآ|*B10iZoa:`I)3Ucӻ}jq^V BkST4U6Y:I޻huWLXʧUrxo tyVC5@>AlZѐhT8d.D]l y[zM# U Dې ,!V-Ǯu}MZ22&Ff}W.M۟ݼ_돶˫ss_n.{6p m־j_0mhr=o<~`mqpI--./y.xUKS w_~xݳz{~ܘh-^~67sv:? /ofzNzZ\=pczI}ŋ/|M G>AO尻 =WlVkGb%w;;­}0X]!9x {/};nulÿ1s_o{gO莽a7ubim5xm@9`j1ϠzOdhj2L-4j\nP)T= PBS*@)A9% %@IBk \>v!IF>DD0`*2XӐyr!5B#W.Y)c;*0[^2':Tj%AH M#I*br0sxB NԹ OVzlG}!%@G C^c*;Hg(Dt>+OC0H>HIqVAJb@QqʁS@ܴC(iH)JTpHJ3q!L(I `)Ѹe~\.w5޼yi̪ ui:A^Pc,ׁyuf3UݐaS0B&PQG.՗eԗe{H1N` 3aK`(QtB`H8!,d!L3ȊLXiH @9q'tN*`xCY#!o$`Tޚ~Ga=C.*'BR(p##.TJqFLsm&y^U#j}PHdY ̮j (U+T 4DmKNlU>pߠTWN,}ފ)5Vw#]}^K41ۍ~!1a`[)h- wÞ)oyl6k&;#-%FsIE+v =5`!>=]fոtI/hPա6-7^,]3~٩3KWb7]l[79췱6zos惝͕=}hmgk÷ݗ_=?^p~L/_}ϟmo=ZY>~B&o/"W?i{ݽ'vfD):qu(zk'o~>:+Ů!&F@pq]}qt3uSԳ O!bB 1x'EFcp(]^X]%]]N|sѹHd"46F? |Ш?2nIGZx-I-|r0~ф8^}q>;O?{{&}^oޏů'vgB(nmuww{}n_qޣgBs7z@s8] MM 6\I:4\ЦzQ..U9R> Z6.Vi3e gMgNs i(А 1V2+K2ri6BNÒJMYMYu:WVx'S 2U^|\v3 e u0pYX|Y SZI0Eq ++8n*:b#j•g&$ɇ6/ ~S˰L>f$ρgV4lF'=J"M@o$f7򞒩RIBQK Al ü$FD% rF;yWgrDy2QS `T,aj*1p]rN $gQH@ 9PHK>k`#D 1tgLa4uMJ$C)Ӊv(8JK.3+R v $}Rp.'fz lTЫ*l=l)4F4ecl)&RbӜ%@ +M1cg `РA$KC-"_15`h)(3:]89 &5|I @5SQL*4!G8ئ,8ɐ<-5T> 5p';ӪRUt2Dyz}u>{~[%a6}uTo g+@b!acP|hw(jҎ*$*bL3|1{$#UJ@>ttp}Ip+LP؂b4 #Y8M}]rgtfzPn ڤ:i*FX.v[.0xig !`; CHq:HqS6ܢ Eb9تl7(]*?tkM @Kߩr=z,7a<,[#t?8)xХnT:کP$sS3śܺ_>X㭍' m =}dk5og:k>6a_=n~Gիo^xO6!|/^xᷲ:ݽo뫑ԍ_671_=m^Y?hfTuRdk-;x+6$``clfۀmvlBŽ 2MIHАΦDo0sͨKj4{n< o@ۂ'~g_X_ E'QX$85>85Ɩ+qx[YYY\9--ϜMF"hx$4293:ծdKdSvLڝ75齃ɻ&y0{oߓu{wk[eo&l/W/˗^~m2ur|evn`^_#@wsw0@Wg| _{t]nwrC~XN`$i4&"i!oX(Z*r,F3((kV;,R &K&I-P-f HGFϷ%"VLYVϱrbeZ=6@,3^e1ɣ396q|*NŀQДI6i%r,TY41J o,KF BkTYƊs 19-CCЬB{zE&|߄"tD4R$q>ۨ  ئgh{A :E`a$qte+YI9ˑRS`qC#3a AP ۤ&.SNUj%kD))v5G,*Ab0HM1Hh\6 ~g'*IXD>D9 @<;AGYP-j@z[YSBOnIxL}?]IkJ7`^3,7dۉXfV z`i8[PYlud_+k@5DPBl. b~T2?dl呺ف Hov `@%3AcocA8+NLa1(9tfb&֙h' | tvCzvݨ ЏaN"8Άۑǩ]Ө ӓ#g~.}^'ׯ^ 񣇏7ywޭ7㣵?o췾Ճ6=bcc|7dٳb?s='>7Z[qݾ.t/FcS\/h紻nR\ʵ>ݸ}5|q͏/~-}~/\],͓ Ht"21"a1:bh4tr|)L<>?7{~z ꛟc&'FÑHx4 {`p?5Tt7S,]b] $E yIE3倱E,L_K)!24 !E( r4)*YQM$^N"'[+0k6M[RUBZKpAM(ٕKS %iu#l7Mh`B~@}Rk򳨯$՝l7߉R~iI[<#KY(o4@ݐd3}c #`ve]2!f3É! p倅Ha@I`V6x*8 z7Wt:DDDChjg!QYkyWۚY ⪒8dZCnGCcDp+*<7 2@lRF`=ICDNg=\&{ 9?y+43CۙL\O*dDZB% 6(`B B!!@!#פv`̎Yfi~9q|.{l-"K]+$YJfU9h,ЩBQshW&z*}3oT 92G*qȨY`޳cbv_WIAѭ> @nRWvMXU[}ڐӫ;~}nh׊]ylSm&dlʰ${g?N [sxvO~ׯ|{oݝ}:s93zazbl?SS'|^o;2<88rڝ}ty6+a^̫,=?Ss // ?`8 pdKrĮG=/];].i4ay6/4$CărrZlAq g7@W (g㶣ǥ l "TP.`dgc\vʱTBZNBX BP~\*L#BQJIb8-@8Hr$@lGY1I,D~qDK9N:" aք2$#I5- 5)T @ 8Q+Bw D! $DzqD>:QZ03I\!Vi;rP+ yuASA1!#bCs1$ %tX5;G53CixRFl0p}mݍ0Sd8m "4T`<ZFr(Z%@NsK a0"@8A,!,=ڮG#@ -I,3^T&F2^itL/M@s" nᖐ 4VA(*@jd%!"h z "B tp>LJD>R}5`뙪A$ 3!1Nzn}(\DNOf7&cq,j,QaHZa$芀C·q6-MJK7צDRJԫӝvh\ӝ|wݦ7N TqoZ6j)Y+ =E\@hAI[帥=g| &{*&(8ئte.}g#b ͦ5;mI:b*hш΃sv@hYer$`!In-kʷ$` s5KI:1V{R{{gF&F&Ǯ߾~77~;-]_]\6ז6v6?Ypg}2LN|`ugsG'˟a'Oc#}Woʵwgσyswfg1m_4>vcX;+_sfܾˣ7_~ϲ3~x._۬Foqm|R& Ĉg3F}>Ȉ :P_c,& Ã]c98Y+z}`؁"xa ч|1`v|~g^ ?wTV/xGGtmFmZqi2Vbз66Ѯ|Z :VԍMʼ:eb]T-ME"8W[P,TD0aWI4ɜa*ybuF4̣ՖR"Z%'q,~eh,ŀw,\|2/bC[aq|Q%oID>؆|h )1:"e)phCeA>boL8vBG JS# 7*c#X"R0W'[!9/_nѕ,[p ! p9p0$ 7!DVq wOf\ 8sǹj ̥dd>cXDĬ`n""a$c(M"0a~9$3B!@ 1 1si!PO%dvSgv33nl]" J]K Dn $%!@B\HB!\E\e[kk% WlwUXjw9y3߇9<.MM!f"X> HzT䣈r9'z옜_M}===\}/|ݼS/N.<-/RXzuR1qՅ5H T4ȃ@#uu8{ G*"H1#A! a6[T Q̓BiLA0 ke1 "=E6ǃ&ѤsX|1(' i+OM|tD0NFTԌ 2+p@4΍Ā Q'BeX,mg%n:lCy bdX2cTC DdTDpL4$ dKZ / wAsŴ Uz+1Fz* K+ΆikͤCڹF՗BP6[>l,wt2dS˃#8fbUAznxV;P`QlShS ` ?Ogi>oUBt!mm>dH+H TMlKs`M2x#>D`xj<0;3q,w'}zn.޽ZX_݆V`ח =XO mCl0q{'O?XޑH}-v췺=W].+ZiW`f{1o۹.,\`0h^ohs x3?=q7848uA|>ϐ Y٬ r=x\?ʼn{0w8`d/b3¢cG7=~po׾uivvbj`0sFSh4>> 5mjKԧh2iuvOniR6PVuŊ qE &i* P %7BzFYFW#LB ZT-Պtb)|ȇ!p['N9vQ]FR"&~,UYʯ** Q,4'Պ@܅8a dV\8  4iɊ dy,5,/W<T%OJY$Ҝ~HC>L!FqQftim'!I . Q 8AGG) -ΉeĂb l*F/uh.;{ #7'1 -dX\rآ(= zD2~$|{<2Y([`:1iL4F7+,WD҃Ȅ/p iPN,(ȏ0`Ȏff$RJ:XA,EJcq" :䫤IM%Qd$ #)nQiP9z2azN[Ujd g54abT%(UL<\ĿL IfF,Td280(sc6<{1 ftQ38f$4$Zsy}M9XEJa9[HmXU&$ZzBʱ4fr{砾vnq4hdKOs>We`ح-FzʝBg(@x6fA8WA@uBfe})^+.mq"[գU"A:fev`Nj +}ݥSˎZMvYE>UM]bb^8CyuVh/D)^a?pȢFإ~O{ wn,/lZ__YV6V~ھ`sesmi=B \ mm~Ϳ|ï<ӧO_-~_~=7_ނ8--SgW /y6 ̙g,%uŕLo{ܹ3X `ٽA  qlCCN>?͏F/F|3פya۬\~a`sQq)RC+^P?,⃨N8;p$g_xxQgzg_ػ??7,b׏]?ycQ^`bzi |X,}̽!A}ޠՑt(h2vusKkJjiW[[+G׎zATсj^KYHN,IQg(i D5[Sp3[gQ_WjU)M<%}i$**Ucbl=nje6{<^fl}cPl @)iPa1fIHBSBehL] CI vَ WI U{˂~.tGx vy y;P1h17.䃽vs2buSh5 {!s;vx c=g#ocG wϟ=x+n\[]^\^<}'kW㵫kPڕkWo2Uw?~g__=~jy⢲N;}asٳ'OyӁ1791|2=96񱓓G;7OccC}=AzBSgw KY&GZ=/49utWmR4<ﱈ.BlUEҬv Z^*+'ڜ6\dG޶*SLG}2$lR6Dmω4&::@H-;DsHe+T Xj2"3$+K'JGaY4a 91$5IER܆c1&bѲl,[M$B>l] I&DFihȍ| l0c @FH.%, Lц+#PAoH&j9(Ƥ8+ "X.\B.T-QP* -Њ *L)]*GpN RE4EpZH'm5_f#-uhj#~O\S&Y(b81RҒ@,^% q(.OrTMo(~u Ү|H^҄|L}O>}jB}P\1~WIҮDNc|a$sՂ8ɞG;Z&咚NWZAr^C</#n'DJ3^/)EcMj %l:o2\4 } =y:i ҿDr2@62ۭ`LV1d1臮ݥGr~_kcizbRt\[$M5TPD!:lS֙N\op%buF.tВ@#OJHl =Șqs<7j~û_'A\f)+{s@lW]VWmk{́؁},>x©rԙ?o.\[rcm|y+n\tkuڕ7W3X[ݾyO}{ѓ'O7Ǐ>|m)߽{_~q{?:wUϝ.?:o^, smf2tڙNLK[J Y`ƀ1X˖bٲlɒ%m}lK0i$0-I@iLs ^LC6:]Ev_~pK߳|߮_y}e):55 Oχ^t~&<7 ꛉȘ^ ^OOE"-_\^0:9FF# h7551>6Ĝ"|dva>]/J@8STڃ!䓟Q7/1=PW]=d> ZdQ_+-)O\F\2BR}u1$;^[wP\Kf|Y5%5ZD__f=ҦxUixRhՖ`4ppٝ'U5v&WZ E볥UӉ}Lic` <+dɣdyg&l !1V`ubD+6ECʞp,9Do.(F [70uʑgNr% {Gnns$[(ʐR$KU(řV`FF), 5s)$;ēI%$JBP"P!]NT'JĨM%)4yJ_V>q*NIc [uvf$F4)TyQєPԔhl@GX UT+խtn-_Sn8,$̙#CGu>OJlϟ8]D] }ˉZτ"x Txi˃oX;?VEIVc+Cƪ]V5S2{\4z!fk~~ynmdҠ{tI=NT_^tǃ@T+m)@w"8wN;Ӝ?t* FBvx1Z0̟rOᥳW._~o[{-wƽ͍?b>|Ǜwl޼Wc;m˃O3=~?|~޿\jyվ3Twf/uM\Y7#W#OT_~b?w޽vum`m/2fBHx g3볳oG^;]G.D.-E ɩ驉ޛ\g(7wyUsd{[_o}W [~G/cز}ߖvm{9o淿?Gu.]pPWW_@OOO|H QS߰7ԭ"__ooP( *|ֶ@{G[G{Tk?8UΫD448ru}Cehr ؚ&,MD_04N[r̀\4ԗ7:Mn3^sQm Td UWU泝@S EdU})[Ո\v#sf'ɫf+F#Ry ƫ,Z= oGC+6se5նT(mRo?^ER쩨L#cS ҄[]9v4tLȜ~H >ArC-#"a4Q|\D嶿<|['gzkuv6a>idRfijB< bed06eO [SƆ8kb~ڧjooɉ5LAVnJzmkm!`_]FcpiR_?iשּfE^ g~Wb wWsG* :/qhڢ^m%.O+8dhN}qMyGڤގ? [WgN~ 7.-̜_z¥;7fޜ}𛹾0ƵYnݘ{wo?>|Oo3?Y[VvgQgk~YL~ς߹K'/.O9|77SNPqy}P |n\g$</:E"an D#u#"=N} 8A_0,wo֬۲vӇl]nkڰ~G z/WKx֬Uo/1{\nbYm6kg{&3|h@;dj4-]Vlnշ贐рݠ7$e9M 1IF &e9Hxbv(#8ډL`^"KP*\JIPAx9PQ dEAlY^bJT´RV*CQDHfb9KRQDz#G5qIZD@E$8|?=Xxxqѣ wCe:x4f16T! []GπO0[HrHi2+xHg,`".E:k3۫3$Lg =J5Tto T!!{e|dԴgC K]YI;AOZȁ4m;)BCtP'X Rôy>5fL pi5qXђg2A6ʢ,JOۃqwq"K]5ÑD!O>!.4`?Y1uY)W Ec,r ͹؀ *ɇM9A #VyO=hSثUCb먳mfQ~_3qBLĦSy|][CRw$lUV Zb{p݄5zգxz[?実6v)]J Z#Vep2 %Rwi(qZ48^]ёb!"!(r0O,4, yz?GW  DG#G'?tS'` ^x~~t+^XZrř[go^fWY{ko=xo`o-OG=~gΝyůOM4ڎ>v o{@+3NE?0 SK3ς BqN=qǚ)=f`eߌW{/nUkֽ˿}7b蘜:4L@b-V66A}`G#=ޡN3ȇf2A>8Uo8յPt#NwA^TWO_)Щ"< IZ)ƲT$CkMl`, HHSVe**"}P\81r_cy̆r80\D.,O-M4T%hW.xP_cY%Tc!Ŝ IН4'>^]$ ؁DS)܍=`?0SOJYH*”+WyD՘XO%7/GA4g'3O.5 8bLCRʊ4H&AD l 'Az" qoKzm<}>LMZ6(An &ı~sn ɍ;ʼnvbC$$4:h ;کeBޣJV aΓy 6XmX {)%S&ٳgD}pGj3rՕj@8Zr@#Z<a/V'y{8],H%@Yp5!Viz7$U}{$\!=NNS(HTeҽU,ɇU(ҰG*4 Lp8G2 'Ɔ88O3>̍"0@q]C&r)2̮zR(..thsDe 's-Mbl@8jf5B, ɟXЀh*ڢ|'=jA3L̙=Z}1/{}蛿<|Elu;O/0o~ܭs#fM.~++՘AЏLO>q 4{{\rH|_px!o&&"Ϗexojb||4p(&>sΝ CA;^IoK+}/vƯ(ݱ+g;l+'??޶=9ٱшf~ƙ-&9} ~x==FC/ ]3::z]}{4GU"`+AkBle!|#S |4d2%bVA";(&rPO4$ښN6eɸ F;mPђj+pk Z կ3 ؘ6]/ pD0'j"C#Ii`E}ą,̙YoQgyjf!_u*Ϫ.&*,s( 2Oo0xaiO=bjFu{]2S1b-r>::c|ZVŁveʐsJgkV|%˃pK[! =F yGcd7{aū_ygosۈmVŖlZ|m67֖6ז60Ɨɓ'`Yǜ߿7/ "uo .+EiX^Z~i-,-\WA;~~~Pߥs\|zA s D({E" GƩQ`plt |>sy|?D8&{oiؿkOʶ%~sޤŸnm۹W.Xtrr‡ξ>2hs8?aV+PG=NoVkh0)>lojg::UH}]:̴NU|OʅLk`5 IҤu4*5C IHHbL01`ls1n 8bl1_d$[jYnU+@.MuZ~9oM0u[<9+ǿ,:Uccpz1(L4pZMﱧZ[r)]PWo(N'[3TxƚTAuHҝ֦#ҼC%X4秜`G>Mz{4oȕٞ>yt5BǬ-#7BOP\%֫}W]xk? 66#1,N](G+ *m j-H0=,ޢD+c= XD٫m ȧMaɯmjRl@]dEYd8IG³H1rj=^v⠐ʏǟL <U0զjtPZ VQfZx9Me0})Lt57fs1@[U6 U^.)GkcJ%`O&1+9T&I((rn70ܜUʕ6vy2OOw8[Qm5UBwb"%_/ĠS҅2<Ң~d6:]N>iݱ__ FۊOEp'6l {ܓᲉvHuufJe ="eݮ}<`~#Bgߗ7l # ?_.ḱ}H}6{\= }Feメɱ܌oyi7`/7_֛׮rƭ;kk+׮_u޹};7ٸzmcMGvcCrÿ|Ѝ~ߕ?_:~~p9p5;=u=- . BQFGё!78?;30;'䛚@}CC?58?7;3 hodx4ꍎDG&efl,:59U`umvzᥘ/eʦm|?ً۳/yvӳ["gA$koH7 ymh{pG('-@@CF׋}0멯o5Ѽu}c{ɞ"𦯵'W&1U_YxDZ\+cOr5EGwZuE R꘴UיB Թ uV]" K@cIN<.a\laHmKs8~`47[_lĄqF8jf˩X񽀊PYݰ.D]J@J"Yk7Qyp8ۊ,*u(Y2^!\aAhQ=?e+3%= l7귱˞gWgZU*ha _Xv {HҔ9M2 q!,LY p0RH t[J،Vt|YDP25(JX(7MUKon/WJ1,!=œS Dy); .J0aմ^$͔WP0hI*#O+/mߨ=zTԇsBޕ$1A<$g9tDSi8ǯtG 0tcCGL Q$KlF-4͊ % ,a'Sx]S01<+-Ư)\粪W#!$P4vIIBV崀@X+YN/J9KijS# $5 WjUԔX G'S;.XکvQneh9DU]nbxQ E{UjJS{~GwWοG~pk]rԹ##xr[o !]KW7.?s߁~ n?w疧cL*Drz<1=>8ZX8>3JMMNCŅCj#Ijxpzp%SrѣKe_[7V[󻲕ٴgn|#cO?yjOW5:}fB$P{|H7;00 lj|/ B/nko¡PWgo.oU4oۦeDVGNKmx{]y-0mpcl3w4]Eaތ`aCUvP 3z-m@w4 &se4˖&d+Bފ-+PCY*pBeNK߾0 (pN<&1Ul2'vZtv*ڴd vHvIU#~NivP,O:7 t:dE QSFR(m&Ie*L T7jɰ^i)ISgT4*" xj؃l+y$/i<$TA;TWQ J/dUrZsNCWNUa+⌝%7)ܬ]h>H}M"jNvaHPF ÜEmN%]{qN}?oWYxm[9Xm iUr j^޾} K3+b~n=@ɠhS!1 fF sYP'S$U]9#® uR##?KY+rCG,ݡjF\ӺX"XTh7@k`@U_GdNxS {M(%{,#X^*1)%-keN\g%B"\ YIDxeݎ\ЊY5HazM )jWPϙ0i5SX{%l-;l)@]CuS=T=9Ss+zQ]17JDs}\HϕvNE\}n7Y9;6ڄ}` 7Bq~~o"RFݵPC3ݧ&S7W7sy},wh`pD.:sM"+FU#V@8LvUF=^85G}Eo:,h)l)CoṣKǏ.=}go*]z{koa/o] ~_}7ݺ~[b?k{~)~޽O>+G;q{/˗/\z.*v{ Ͽ:7,- ١D*16O)%R𛜚F yOz{H7nzalm Cjga: z )HC}m\ZvLXzL!鵸5-ǴU >{. 8rj Z\]__M )59wjʁ8@]S8Qϖ {j ̚?` 1[ .T[ʲʹ2YHHY X35"j~p@KPQb%R# fJP&0C8.1 +<68,BJ-ʼ19k*e d:c ie\,)NȦUP0dEO$EM &F 0N Vh[WJ4jޫ6lom*EiJ4+/O]fYujY$ 7UΙ7jv\J[Q?1;Cԋ#y6**ٲA-[/1~~}}g_-+.kvlkrlmٶf-{wtr&t9~>2 \!JnmioeVr` ,T#aFrH|4bt( |e|Ybs18M$餥2&s5`>#!$ەz 2=z oL>e*Yvw50[C, "co@fŚ QyMTȌ2RcF& FrpHh4,R`~kXUe0@! jf, Ghg.ŇڎʴU P &>f=2 m2zj@N Rj/uu&iixn'M%!v&A, ,B@z@ acu7Mu<Nݎ:͝}{y, 9nm_[Nuln0snܡ T`aةH9DݚCC:@F~ C}n,Q0گEUn1vhU*tgF(SInz땍tU-P=9ݯ [kq{PtvUU\OŰt4 AcHgTG6XM`ޠQ2b*vvy;ˆiI9T&u/xF;guw}\vkK͵m.nݸq死w6֗no,~7VUw?O>׿| | >|_l??q|qmom~Ny'mǍ3.,×إ.] -/] ~=~۱ gNHo,gBA |^p3#X,s4DX085>;|O .{~n&ߙ og˟"~Yv'x9~ + +Ҿ="s>PbN υ5fs/v]N޳;qNX!GGaj7 mF&dmv&U)@> JlPe72R6U-J2O!* [b Zk#"?Y !L?!]x8kJn ,j!mA[ڲCp ꃂChCW~RUw0ҀCD E!e!Y~"? O ͍'30 yPHPuBqQ]$"#`KrH b#oPe``nACɡ9DΕ9DDdl> 1vr=؎w&x jVY"qD<TCȦ}!F} ~?O LL,e(p r Y؛16Z  M$p!؆'cIĩ ٔI&Ҏ9T!bz@7;XhE*lb.T9tr}Uu!- z¥'Ẩ[qjnH9ӯ{ 6'ZoOuVEPvKʙe  Id-C5f"NUġ)@~HtXl)ܮ/p(tb^t(J=%ޓe\qPA Ю75"E |CIfӱ3 s~+K7זַ7Wזo-]Xxg[wח^'mlݽ[~}$p|{9 ? 5l?0|+~~,qs뭕.=c> ~g߿ ].`8n3 y8|spN|L}x?"~_Z F|^pgΞKJ#?ޛoG?O{gϿs>ͯ|[ٹzz~al| 9\V?-}jsYf"~~;;McЂf:MmfX_ng4ISe†TЮ6M-˄U]F dVeA)"2@5 kQ:4:kjˏ*9 5J34eǚdfY:P%dA\ F5y}E J W~pN#I FiԪe[< EA<_!)B ?PB*a,$~%1 ,#=&\n\LNHE bC<dIdlST4 AY~[4_$yV1j%N#&x,` 5I26`a0.X @{26p C>qIPμG`` /X+S%8FDm+YWBJ|XH򤸈"gbھ?a/]:m.jx e{2ے[K(kgA 2:ka]btR)PIJhG((Т#d-;m"%^(f@-P+|&(YneaY.Й@ 4vՕr()AQVgs|e"{hzަ2t(fj9e|,Kam#dCesXռr$fOd٧Ϊv﬜Ā@U"\kv@>6mq"mVcRJ̇ꨯ f^~>.3]qVLtZxkޚPl,\~xWt8bYO>*;쬈zM}G C%anWAK๢V}D񮆣PD䬾]D|!?<\aOSav{t 862096~\| qշzc;+Kk˛nnZy}i_ׯ_wX~m\_qk pscm`ō;m|Gw{O~_|=p_~䣻}kF).'""5#__}wyw\uKo|B ~o~ziq?_ OkcQa&7J$ $D}sӓo||bpM36:2<4 AGї^t)~;Oy=Ol߳}GlKz|䱟>mzSbl,z>|دCyx0  4{uu}yS'd9d9\wR0/B]#g3}铩┚J[1GkKROr)IN|*'zicPKyH#U- 5QB,M=\r"I9dQubKvְp5)̣>O+>mW U}5/UMmۄ2{P-nh5uZD95b,duTƺLAte_W;eN:0$} kчNEZǀvr?<r ]{ [Q>v5իvGt0(B"v ҕ˯֕eu~kחVz^\_usm-sqyև??Յ ݿSw7oolُͿ>Ŷ+37}?u7.]^ [g,KP~i~ǔD<{#c#Q78&Cn#Pdq-Xlnnvvffzjrb|<~S3S o "=$##v߱?i:iS{~}O*co۾{~̮B" :O܇`'TCZ!_'wzwjjjlt1g*SSeȣ*ٖ 籩*m=SYoʬ-7up؛ij7uڋjUIӠ$؎c'sq.qI$D)(Еp)jS5UӴӘuxty1fѦ ԜcB> by \'Ċ(hU1jw7Bޢ.|Zum2V@\KϕՉxg Q.f_5:w_CDKJJޫ<3͎\RxM9gStȦYW#WNP+܎`9rlNCQ$&^UTP,Y$਱Y86/>ūQ_ِ[GB!:H0qO7E0O61QI:XB0N0Y91R\7jr`OxWB8V̫n$xiT1#1a|'"'q.t"ĈH̅mb屢ᢞ0QXc%` HNa8HxUEl4*3 YV!ɠ"6q($"c UF6,Xtl[cIN*҆ <"+ %x Fcmkq|Ծ2Z"@\Ղ.$q'] dԾ.OP_z iK1/Oxx\m^"=Pm6u&\\{Z܇ٖq>IrnN4lKoǖlN6wWH>Q52M7'lKÒ ӝDpqș$A}v2QIꪞULvUMvUOf\qלW]a8Z6T2Z:0 ye)Ix<_+[Ww`[7[ws~d=G~/=~O~O>yD#P~_}?} ~/i??|~ݽ۝wo>S{Yv{1پxƝU7oN/J/'SsO/HKm\][^I.N]^H_]]NnnnOdrnaaȖYKVqÙGs(Co[>oΧv_'@_|bXl46: Dxx/ǀ p>$v!wÝ\#HOw$ͅmmpWԵη9WA+̈́zt4hH<}0(4Jwa0h[,\]ޢNOa$\1a2H<`~":MH - _Ih=77@Po `8J[}x$%Y$³c.5Kr3ET"$15JA9r!K5dZE4ܔQ~\,T.)`c!`PTBɊA `ŽR$3dV#d#|aVW0CVIe+vD0JDˀԈpHdd YEaR;,*JIҧ4r+Q17j=|it,[.hanr>nkNhż|tƣ~!,WtE:=FBᖆcQ?C$D,PzzxK4.":4ŕ)4@Q@NyMyj2K0 5 p^TV`x`pera= ǚo)Q<"<,bͲP sq&6u,tPd!dPػY`ZDN>Uk+VeP#T P@OD"T1^JrnIΊd9뫍eTWezر36ZJo7׮kWV7 QW#0~3SbWf.M, , Xlnfnv&67 KRo,:o@tI%?̋? ?~6{rKEO^s޷vimaq;-_%;yt]$( G]o8R#FꅀH8FGQU98 ~*"ٮY,Ow\GahA9[Yk& lz[+lz4!a:.y*ݥ']4BI8 `/8fV$H`:l,9\[Nd*i?8b6a1%"r%=$p A0X.1I^4HNPT۬BTZX)!1(R"2yrG},q´αsF )^$'vO%I)CUb̢&)IH *+ZH#MtZ {Idb\dPdگ(j% T}X(d#Z*QSh {$ˍv:zMpitZ:8WoNmLSQHO%k쁋P R5;1jJxUð FCY%:Zdhù3n 7'RߑN',4#+!B)=  nOTfspS̺uTj=Z#b 72xFV5j 7"vTk{,g)W9OKXU Xη(' >:LlS=F#H|I {{&^8hWey}x_Լ2P7OؠCw'x}Tx6"S2̝Z.D\SAdX3FtU apgk] 9ɀl:y_?:] R#˃!UmÝ;J'~eqsͅa_@۱ѓҁcC"+r(qXؗw>76L bɅť勗_to\rڛo᭛܎oݾu滿߸ow7ooܹ}:P{}ѷoj?ߣO~wVWXt^ߌ]67έyڏ]o_ur|dzE޴&'Ƣщ 767 |'~gOsW ҍ. zPa@OLwOO0${C}P׷ްt &mt:;[<5)JVmlw >ђQ_~ YvCU`[Ek}5ajMkvYc Μd3)еT3\orH'g]y*OEZ3kN]vFbdw$WI<0f'Y.x{5^iڡYL1Q)Pu8Di%F@IRQRʁX f< !*9"=gq veGj8n"P$gK$,bGrh(L 0 %OǏ+k5VAͶ4DW:W%MMJDYVCdlMrL7[T MXygK' CG:FXb 5kDeJqL & *jf(IHZ$2>kJ*aj&^s (Z)[KlT2aiwOݪV@lTv&{ K8ij> D e3n_;RNE~01k`ycs,K늎J{!Meũ ALo9k)-K^$@wY.<"9f8MvADH0inD P! .^Ry٬W.㣒m淴y&![` Pmm>s#ޓMň#<>=$mv=guri°ۜILٙ|>?+לּ{ۛk[ۛkZs޸yvVAU՝5ogw|٧?Noi?ًO?Gk~e[Z݊^~}[^|\\r"N%©$/>3ٳ4??sfӹ\&=(ԇH8L@2쟉N$"ALwUT׿ɫo?q޷?pY~Krsn#sQ{qsy C@nb9k6j%Ys# &yu_^AY}EU]g6@VP\oW;{DD8ڢUU7mxuVI0W 尮7H`<3VeUr>( ,=" ]P6P$pdIVFk ?89l`H)]7*D=KTGfX 0 ڢ ; |o"fcr2 v^ÄA*LFb<]DlYs0 4JQ('R}#3HвC ^v!Y .P{`R =I2HC9FAtøI~,r2ĝj%5#O,`HЕœf2.PF[䤡QR/=71I] A~iUYeEQ\d@9lrтoe F44Z~AbńdP^`I,x >D鑈H҃g`' iH Ȩjbj0 DI1 iQ%Z~h o&P)Eb:JՁj\_֍)rƆD48)czlt:61c@#E&9&F(#dm Z:nR1hf?XݡψKAfMޯkR"9ޅ4RքvqRTqgw!uFG$RcI&2*;9/z51QYftD (ƝĘ|aR6\s8Pzf֣x5+~q1dRYR.UIEȇ A>22rNH9e1i$6"IK+PMC6)`iNfyQrLO,a+|376q SÝQ`2r\>xa+{nom~[߻~͇n3~q@> p\w>xxwOœg_>狧׋or+,=ϟ>ѣ5w͞خݼYރoݹsµ++X86JƦ/¥өe7GO>?3L:N%`T%q4@l¾T$04𛃇^}lNq_<|W.ҙqcss /1t. z}. aqx|x~cxjۆlar>4ԯN4uU݀$TĕzQ9KVi ̪,ueV7 A%@X;d| JkO LU32bIOQcVbY3 CUSi6O;A)|oVHf혺H腥ZF;r j* @鐸|)+>Hx*୩ϊ%#Q jSK㪶c @f`EŴ KYQ,n+]$Ln-dRHG|XzfIa;Lg!NJAUQ=J9x%Ps`a5!I+űra:=7<dL'wa.ka3;#~p[gqVW_+UVHE- * mCMB%8=gg'gdzu$B NBq@([y扇BT*oι{?!{Q=&>Hs kyw NՋ %L̮yj1x~r\yԉTxű#V_<^Qk&+xO,#GW_Sg}ku7ovcТpBh<+fRP(H$Dh<͍v+`-(5DzM4_(%pw9/908^6~`lETG>x; ɣQْ%B(Ps0e-v(&upDQ_T(l+ϘۧErW>K6ZDa3yBkE;,"t Ob2m_ʥћ-v$pߡQA:$r"r(4dbnQ+i需r_''\kQ5fxfߔr.äpJ*0ft73^Sk 4c9l. H]¥gU6h6SÆyJq졲i&LiQ\Мi%Rآ uF95~zs9N0a++*5ϩU]nS-b ňu)l] DZOE-+QKc11MX2ng8^;qT"8[1b? c܈MCC#CpATr LgٹSK/Y^zk\8勛W/߸7߼vqKؾvЮ[xڕwo}wv޻޹'w?~‡ [; ?~g/^]y1+O-,&c~*5KL2|J}(nffzuuea!]!Y&P_:J'1x$5J? $b#ɉd<-=}<{~|?EN Kny^_<^IɡCϥL(c}~Gv{|䫧&nzݍyΖRe3z|h*7،8n.Xr-GrmnC 3TP]c~73JZcQ,T&mDyn⥎#ϴɠ?㕂hȑ\PS`+Uis4 %E =W5 9zaNR420"5pE̥ɩjiQ]#xT\1-Z)wަRB5x'[y3C!*̇ f|-ʰefhG6d '0`- #33أ=6,ڦMHuYtӘeDUQgǪ#*!_W9Ni.&&dT>:$C2t*sˋՕ3~g_y^~қ6ܾ^zs[7ߺ~˷o^ues 6d@ěWoܹ֭w? j}~_ 1]uwxhteWϜ]~_V߹s^<}jmu)996ILx,>rD;?;;4e|20“c#鉑x0x4Hc u?{xw݋˫ΌF].?)yd t^ovm6;s;8vp:}.=+kQLjAmFuZ_QRlce!x6>w/= NV(((PȦM(\ȋH}Qttu<9jU8cY=Ҭ:T1jmf T߯Ơ]%6tW1C8~:R)DtzŊ&$㮚vsVk6$3BN1{e49;Ƙ@?7w A ~O>ױ:|uS 5s0tT[a:ݍ""Q;leYĈ@BRe+& 4* &Y!RRK=w"lw<[*Z).d𛀰Lc6n=MشX^9GѬA2Ukxc]Y*nA2Ыˊn:h۩X[BB*CH -F<K6[>{eD_ˉPI/QU'$JbGA)rs(JսlWXk~U@xl=敻-' Gqf| k6/Z'H}cOx62Д`%yJH2 s!3@J E C)u:+6i#BD#k hr͂x 4L$b3bl Q XjbpQr8RU6gz1P4lJD=95C#QBz R %,,Nx73vu1乺ctxuܳ1^ m΄Wٞbۙ0؃WoWĵ\btw.O#'q[R82g67&\mcZ~Nt6b-xoϖjlnZ<;<0Զ:7R*䋅[~W.oÆwuލ߼ލ~}?`n߹[>s/~۴Go?w[yv;ܥ?1/>{ճ_>{?=f=}ɓ tݼλ+wvo|pOo?_sp}r~yQW\([/_mc>ZT( |n%Y_W2Kԗ_L/gˋ$痷6 &+?>ʏgG^=~~BqR 7|Q{Z殁a:t׏|n?(bdks4D?j;kn$"*lF|?B.ꨉ9Ag54āÚu(0ֱM |f, q'# !j?% "uTQ%ƣq'aFbQC\g#'Ҕ: iYlp\rɈ!GS!ru$ +<PʧC!? jIG)KJek(X5i &_\N˹Ԧ.̫h2„3G쵇 #"vD{6I(U=i'5)N͡Z)˕> >[Ner?.>$&;Yrދi5|B5 #@n+|e xN+{ cȲDppbB_طSZJ7WϿ^*Nz1ϹWB 2&ʀ Db5x n孆b?E$f?pT LM(]GP16Ećd+ ?,cb^CBk)A{zhRq#ދ׃-Q ac3!' }rTu̢ !ss&^TOR+LO#1IƓlZ?Ò鮆Tge̢@5}:FRBec43"7#r&!gNf{$W1)U6'-@BnknTS]g6Cp+3= &J)zJAT ۝אai__ /ZJ\]lѝLhs{.|d_laȱ1XpPmw>!q?:Hrʘ7?]Oٴ4ڝӮ/lxg.۝B;$ۙ./D߾}ڙvR|t$ebBMZ%ܜ aa{vaΆ/kky/ ¸x|5;&0 Dd 9uupjZuڦU7lcۘmvfellf{` ;lbb;븎0;q9;I_Fԇt8t~{ȈWPU({E=Ѿqοŷzʻo_{naWoo-,//\њdkcqsꂊ뫷2xݵOcGo?zуOϾdWf?6pkwΗۧB'[_|w>{~!&Ɔx`w|hhLOͽkxObjRԇG!a|`0 ñ`;v&EߕK+?? ?o[!T΅BaK Nu|..@(=P/ ^>y=;x;<n֒ј m9A4iZKvUQki:-\ ]lxmZoUۦm2kLYEflfƚ؅AQ[״X,DZ Ƭ6MF,4ե80 $il1keKI*E6Z>uEV"%`Ub=J2`}UvPw e Ihf!=!xc* r@qEE{SG 8!a2֖6dR`E{6lTG1ϛZ4|v x"8a쿕Z#ӒBbб B[Ĩn$U+OKlub<''vd jB>!1rTR Fǚ4:09ēJMU xHw>(K1Wr,OJ^0V[ổշ EXQ_R%gʧ9%;<7_|D}h t,DuNMC6Ө?X5 2k53a!BCJ2Vpr `,GY]Pr%KB5#b48Кd$0v\rB=|CJ~J>P-ԴW Wk圖 FzLm5:c/)n(j`x"\ $`*-ԳhpWQeP98 qT䆻Pk5@j#E5#eIo3qS5;j`1]T"H96 }!Wڒaװמɀ2)Er)/%$L3?Ҥy+"ב`SDrLtg2S1eƁĨ2[΅m骩+LHB661~Hx24ɐ}g㧬 F{aXƒɐ M>Ez5؏dFk"V#Z0e\K @( o@zpU*(!MwHpqt`%9VϸxESQ6tbD 9 ޒ^wDGxgpg9w`OKa3?, ; ͜O0s̛s}W/{W>7o,b-ɍśwV?psw6W/m/ݻrosmƧ[wӻ۟?xϞm?};=}ɓ?{pue.] \zm9q }}]rP٩QB&P\t셙s|&'@HPn vz0EF{/>Xltl/;v7/W^~yůϽqin. / '~qB@(wC=`+uG"rn;nv4g;.[޸Y]Z#[V#eσj ,QlyސhԴZ[9L*s̹ I`!>l6g:à 3E6 LK1Q9@hٔhH.;P[~X2_>%RTJ5BqXTԖ.I.0l rwU_^-Q*a,E# g;73CEM 7A;<ā/$ DSbN&K*Xf`E'H usMC&k1 b9x^ )46֦&kTo !MؿnJXLWGqr-+sr<* R' 0S T5 (b' !zpNLGJf3'aBܵy^(3%Brb?ER)4ˡ0h2KnaB,*J{Wԕ6ԗ5T쇈fO Ҕ =P >bxuسs5 w>߬(n=`0`U |yAӴIӤi^Ҵ]lڴM=eI`B1cl`6&6i&n.48BBIB/}7h554zݲ! E5g Fվz'YZnrU69q4:R'[ݚ!3<3iZh 9oGzL:״[O#Ciyr̜ 惨D|+j ufҐwKajcy1dt !62ƭ{%[ŁmiB]1 ǣ,lUOt)ZS1t@q%f\0ć WPczTu23֚SύkMx5 0^{c\O|=_0 x14FF#{r9]*z}>.f=R :4:jX?խ*nQKȵ@XZˠ_WM%6uYbHKAJE J bB /h+LJYUjTh+#QG[nR* !-Cf5_P=t%Wc3/8+-v҇5⾼Es;&±pNYlc .ihꅙpqGgوx <3}O %843.A Va&ϟ9r+xw`Z=c:-Gg9sUX!r^ cHyX'X$߈`jvuU&K)XZE!:vԁ OBk0FvU2X yGhIg#(IqJ9S/.t'DP%b61cӲWޯ'l  M=s8 c`pnB&areS3@TE aCA]M [_[%$ <{QmזW못U&eUSil.5-%5d Ξھ?3!}C66f1a&@ @6$b1b߱c2I[w2ԍ:q,NƓW?{/qg22)sqsϽ?}J\ŪA1OÈ<BfC*GƜJЎ!},ĸC٦Kg,35_VtZ,ZyyaMh׌Th8Bŭ $ȡO [ %^$Ӊm rI|Dגrwg1DqlEeyExr f37}C0*LTLg4Xlov;n>x 8bAru\Lqj(y;p>1y SiwSq?hUBX^!e/Ru ㅯ o="F4,/RPv. `ake`(QUX٪l Mm\~a[xg"N373֟1ԝaܔϳ7J}HG/^433L5WZʃ݌JqboSIuPt&L/VU핬vö тCȧ4@Y˅8YE'lPlÜ=*,!rR:] t  M*!r㰓""I1^TQ%Җc]D:8¨JAf((TԄUZʘI xumRYH܀H`RRq Vr)0HHɁFFˆK7ָ(z=PPs#8;?LcAjǦCz 6R0j\jsqK*|a]I|,Pfĝimں6e9H 2^^ʳ1kYڔP#}8;ݘj3LqSnڂ4{fq4ilfzRӥ> X (#t<o \9^=kӶ6Ei.^.n@OD7Oy.w{ۉ;v愂J6&\{d<7Vt+܊ٗ# Y._2\2._4<:hvm_tXP?loNZI 4gG/d#bCM5GLltw{9noxwn}w>G~ÿ~އ|O<6O~_~'PO?{um9za~?=zt;pw}m%|xO෱S[YZr~ed7wY䖒+re|2ȷ Lf!H__篼kPOXl6& ~I MKW!_<ŏh\#_,1LEpxEF£c~H$|1vԏ.z5 A*WݘQw]7 ۫Îae+?dxC@w*hA5Ak*b& !A{ݠo,G,T XkNWҁJ ٠{5C6&{ $ i R6~\,*Jpw+<( &hYIbI#@ ZXNxC lyND)Ou罆bTT Dm/PJv*!`v"Et{PҔ'l/Vf*JBUIȊب@mQ\T^PъHeK8lp*sHx;Al6,HpoFȩa""{ckP9YǦtq^%G‚Xy}mJxDuʫW%tq?&i팡VnOOpLnuO|P[M yfDcC,olvssdUF.RZ˘EnjYhb ,7toPV S rW_DZ7ؼ5kOb6xƬ @#r3 3LOzp[%=;I'im+&ڎَ)O.jIv<wyڶp!E(>͊V&uԿpNZ) ?x?_o' UWǪ5T0pSuJJ툫 vZ:0J]Y$h$Q:^2:ZNy,:$.%Emxχ*-"FmV~0bT\'qp/)wa* ya Ubš`y8C_p b:bUnec$ш^'!_jM iF #adUdҙWHNyrcƬS`S0$hr@ȦLh,xK8SɺfaX8JDwȵ苜Ua᱂'386ߥ/r7*?ssLVhUصUp]z/PuE%Hs Zȩ-TγBLp3BQ:)7P.|!xZlIUJBn .B|Kx/Cuokijb8PPg8tzffoT+ 4Pcŝ& RY+%H@.@^>v-*oGST8i2k:0XJB`4\8WD<Ժl\̙f62N.ocNp.&ɸ$70g^4M g`1对Q_﫝녂긧r\|LvWƻΦ|ٱ7οpw?| d;o|eޝkncn^q``g bo'>~Ͼ|_߳?Ϟ~hc} Gݿwݏ?\z[wn6;-.h632=51596=s<dRىdR%/ch$ fã#L*NxOwt;Z.vԗ.6S1< &4@U$k3rmevo沜":VAaZ뼎S4ƒ\!OYGɶ6s)k׵heVzcQԝEŠ|Y7. 5X cةCJ 0&8{٫ ܆n}z| rDqK|e?ᜣx%?fT1oSc{ SST {a$B;sX';Gwp`E<@+dH$Yɖ(5<cn AF9]1;{ Q/N|I&#ԭJ[63<ծćP](Q#&~~&GfrMHƉ o1+줙|[tW[VA,)]m!ǘ^B+5 J}~ߨ]D}^C7m7 N?bK a !$,!jվ/M`S;Ďql Ď#]9%O9s~}X G>(Vy*sDalz 6PkQݡ' +3'а.6FjmxPcl#&d¡ HcM-3k[#rH~ 6 BA]#R5KK- 9]0s/Rj P"\4صi1RS0O։Kt7zgYV0jo#ޔK1_χS"=aXw9 (^%*{}eEwȜ@HRUy"G(9ޝ艍N8eIlXf%Qqѕ$#6ab+v@tՀjpjցJV!ᡶh࿆]^H!B>3;=YZ,,/\]sOw?{?Ǐ>dN 췽񨼱^xW~z`g;:^Q <{q>?ʷ|޽}i{?<~@i{e_E}˥_L?|vf2Nfd&KOd2B"A$ܬϕObp&L%bX80`8Ă|X8JaDGկۏޘ+ ᘟ`>z{=C_ |ڭ6as؇*ZU65$$Yg2d/q :E{@w= gՐ, E7Z 4dufyC+ uF)ŇA%)zD+X8hblwNT2@tZsO#d@D 4*6$%D}!eyP!ѓH5*"+B Xy'T(EVQw(IIeib,g p"CӘDY`-yM'dĪSw⟐t(^(B3c9(E?!m[NpiA, 2Uܖ9(0rA>e-`?Ѫ؉\JKh#Yjs,FqRz[ d'USzrw wT}4Zx\OaUG>ZHuEh_y"7u:Nj8""m99I'0!&m>1 *߳=@(n:)jNw_9-zZHA?2g_4uW|(C;MCx`WuYsK!2Ixv\,ȵYU+e9wO!J2,zAMtTS>Ò'5&:ºRD f!>BG9]W*a96- IˁP ƴO`rL!=0VּO8\MWbz[S`\P ,b@; XtU{WJ()tF¸c}ӌ2[>c['{12]=hiXv545Mm2G7=),.oZ统}yW>^x 7n~pɃgOv|v|ѫ탽MB *[}qx{˽w_~||O㽽]Hgo'gbuFXw&ǢXK`abQ 0˦)w˵kk%0aAy>АaC@>_|}j劍6j Y?Ϭi7s6eV:{j8 f11;hHM JHB B LڦI:n0g>O~wnC'v:a~s9JOav<4E}aPM#_s݈Rܔ"0oo9Ge?Ä1hՌ *Sm !݉qaf}RG[\iTkG > z P @0lAL0ṬH@t͸*Cm<]O `s{=8B qZ[J(HGT&*ud ]:~QEV[SFS̩d(T]'TSltѣ V[ ᮠ( 4"[B!4 {dJ|\Peҩ`\nk.rԃmq W4W2/q`KV ޺uW;UrHL砣!0|u:3v5+r8Ihn._)W` yRFy-e:˰ 4pWS߇P6 Ӹ2=`ogv*^+m }EPpXdk!%b ~Z}2qZ\#j GkXPw)!F+ܥ0ٰ/!!&5R29)MN˼Sn&,aՑG>؀QbERC j(QAKZi,7f sO&4P)pӂ*ȕ[BAh­Q6'ígFINƯ!!PeRJTc)KbbdʊZ|vcv,Zv^_rd'=ٽjZt|ԴjFY 60o7Yq}G)Fd}h{Ѷ J 9 QnƬ+gouçL.S~u'7Aq7>oO7O #w7BfҿKy~y#BH=V 44{b9;06lc^( Hۋ{X$;=J-@8]HOO&[JdO 9yKnHES,؋r3h_qio}b|zc=҉^yM#|M;vevvwn졎w>oO?g_>'_a?ӿ<_=}/ϟ}^a'?/z߼|߼~u5ͫ}߿}oW??=m=ѧK_߯l?wvvv|Pڞh:Wwcw;)FȤ[ݭfvg3s*f̻V,p]O'SkT>{AAx}s|S{ׅJӓjʿZ2? ȷ$oeY_ē4V)!_" Y`'Ȥ䖟p5h*d㮆1~ĦG(8Ez1\]e@u+ QkݘaVO~ [AaV7nhY{v鲷z\+sC␩&h5 Y ip%d ;'"I؍` $p;h ?FS?4hs+!P <&,.)[J)N"E' !G9F2KnBAУ#,%T!ZqFXf!|` G/.5{*} LO9&-mIG&EJa#fQ/aT,Ip ZDTs9 V;1?cx4h6Ws2DP{F!&d8Bis,Cy9$/i<}*E{WՎ=9{E:XF5RVw2R,\ b/e 8c&)5 eBooz.5 ~$CA_/ݻw߿^h'B*kc0W+6HoҭGeZ*l!&t^(RhXi1:LFn P"@P#z B5%:U2jBtxfe"Je-ZCVtj EEFT`g QF΂y1(BJ@Ъ!j-WJ!ϘL"]-Y۬tdh !*ifV=nƓɱdgfRM`uŁ1َ QڨW>+6Q ʂCrdۋ͘=]PqpM#&`KΣ(Bl 9jR¿F܇ogO2jwWq );HxD냷ҔrLwcջcwQV75O w!V1ު 2WZ@Td7T𣱷l]l]5of8T&NMt$yć*3֕p?]EȘB*Q$Qbc*$ &e8q;c0l։'2:ww߹wчL-e3+V/_<?rCͻǏl?{bG{[>xG!pn|NݒboWk"7˷{?~i'd?,w7o_/yf?®\t 镳Y-gslf*653 Rsɕlzi!Itrf.9&Dbz>O!H8&|S@o.=ɤP\*J3g\4>> D!_lb"N?B0DG/<9MD_$bSN~_'G!ox$ 7Ch>>;m:GՌ Xu~͘5*y,zbZ z: !Wqgl)T5{ަ6:[b90[J#7"m>Atg*k HzH0D! ɀ \ hmR >$xuK fX廕|xeHQ6V)bQF&9ە[Z2YUrEˑEB|@͇NAr,2blJ6{So0ِhIxVĵPl,@m~[ET 3Q" Y 82|=t*cz[A`qgsLBJqB>\' =zHa5W(,e?uq d4Ro9lD]Zt]Fܲx¼n6FIf=gO7W㽃0O59my߬P(H$OyPg*rx5<$sH)dD!Pg <[J41*UCJFekB T/ M"=jǬSi:".;a_;bεXWu5)"RIR97f];3]0g ȹ?x/%z@r9{p%#SPM #ѭm๘]鸖Ą]ϛ?J6Z8&{>J5^p{aF4 [ _g:'a坋OΏϘs'~qѸXt~r zb xor6GG'E5٥k+ɍo~孛wOtks}'<}xgOo={AW;믷_sx٫mT~K*/_뛗~>`矒߻ww;;ۚ~og/ ~iϲoYo9N$3IECB&<MD<5;SX4F#Y-̧C:~sW8b8 p,!ǝ؃ҁ|/2ą|3X(,V B/ @`H=Φ"'sUz]'RV1W+o[}^s\ITaZ*:{Zu*0nrnFAaZ~QO:NJ{Zo2`V+"V9 wc1<|^NUbNl~B^SI@1 Z!]:l8) QJS!/ױ%KRR"z3.fڏ狣<60-Jo*ʞ28}ktbD<,%k)\H(l[DɳC|鱈>۫}.o'y{Lq9&aywEcdkA]U5c8/<ń,עbxhwǦj"k>|d_Oac%i/Li3:\NnD,QQ\ѯ'Zr2#EvҶ0uxl%G̥zd܌ of̼hgK 8Rv7cy#,F`QFLkʒJfQ߰p:4x{Y ߔ toz3f+ W[ ށL/(0 ٞdtw|j\^\IOw 'aEOGx=;9~# :[њVW*Vsk͍7n~?|}>xpу{b/?{;ww??9yvGrpǻO@>q~GO'_=?}/o?/pw_/wΝ{Tk~k +R1VʣRq_lLHS+N(fJYJ'_:\Ea2HL&f0[WWr&Sre}+P8 ·hP>@y>?_(Y#@ď+|ṇQWksc9{y҄=&NPJY{&F ,~K1[xkv4X ^s@u]ZGLA[ʎ-bB}S2^=k}eWλޡՉ5k Htq(:bWqs6pWHmYRJ0s xE?Iw` a<>L LA<~Suv5_x%FRhsmkHǰ~0&eř Su3)R" BʳbN k9.x1ճsͭ8Mf({ֆmpU972%NS?:qZΎ0! k S˚ Ψ*w >2`γ^DzX`rEfFvз<@8:WFD-nC/ /6jh.@;6 @Q hUo~w p)p^E}&EsH}?>l~1󵳜Wmso.J9)U])ho+>=c&s',)7dA6dO _G:3ۻ<ۛ9,=דvuD=WgxT9DץecW8~ki%U,bysc{|?xݏ?z_wwۻO`G;=9=N<ON }x|ӣgO}!7߽GM\`;R%ng歭O>~mr[ |..Rl&ST\r&Tr1I˥BY,C}VP,JZ X, @8e-8\ {e[lD!@[0l+1Jї2FZ.qE}Oep^qZuv]`:C rb7D^<`6 B8"Jf`a:o.VwZUԠ.C=*Kσ8A Ul,L;[u0d&`ceҷli<(+#G7!\„6 MT޶RpXpXLGv)WKnYQICLH`J~Ñ5K1vp9 Q"N+,i-vn^[-j~ަ8 23E|*.]UWoB1'iH eXpWV2P`S! I:&oc8M`6vC~eRrk ?x0dwY&bLE z Yj^jhEͨi:YJC~WNtG>3{n fvFY+h*U)Ӊ|.N'^{!0j"j&;d=(84"TT\mB2QAܨhQhĬ}tx锪Ԗ@j'oNĬt C235ZWVF9|`sE lK M g)-EڙpbwgQr~Q}cMN:lPj/ t;Ĺ? l'9A]݉߄[$OĢCZ]N| KCC!_酉?]_·@ lP~ȓ@}wdq CtwCQX;Li a-߾,d1k[VG:k#+ksskk}}i&˗Zׇ\ȴ,̤!37RL۰4\V><{~_|o~>?ӣ/珿G.οˋ'.|}O=`ٳ_#)r_\z~~^߹N'?JGmgwO?݇{G&mo{wgkggs}}emmyggx[ncmեŅ9ķ"ߦ",ml3`yeh:gٜٙon07|Wo|*7SѦ3[E}Y|`vh8;8?WF޹ߜ XCue<5YyoW Y5=9ܼ> 3ǬgKؠ]ХE5`"{5(h)y4ёGyt=ؒ*-u3|5Y^Qr3$I1F BR.<, \U )AG%* *RS{#sLe:?pԱE zw3[ Ym-="RMxuKyZ$_"/r&+nWXs@Gqru*1:tK"L}o޼J9* }:3ee~&*CA8S ѐ> v530P_S-G~!h1D1(ʸR##x~$(U~pФ ZqHY1* p[Xd`Հ:7ÁP9^jms9&+Arj^ĒK10dUDSP5#2C^fo2PTgF%m\kKMFTb3nnb>E10RADԪ)FEz&P;CULsC&t#CTl; }-F$AS{97̶>-'I7rHwÄ7@(H|ADI F":cI\dwɣzNNBTV)V{hܛO{+_oޙ]* c8,FHb wBh?ݝPm-Fyg,icQ"Cj vi?ɶ7ݛ&,H-:yclOG{ Ꮅ]ZXͦ[ }s۝k|[T.lȧ@em1A{>5FD@@ !9 BU!r'"xZ:}:NU~nΖSTOW~ӟl,ܔLLL ĥ^yڷܺ?g!Yȩ:kUZԇtUk2j[,]HBKdJ+r?:[-j Ca' sBF&8XIB8!#Y[鶊 1!xx:iTdpz"]09`.O#4L'ICU61CYo+E^h#Ǵz|Ttn(=P]aEG' g,O)H}>|>|u2xjw:OUaؙcCmce=|Ʌaoorz<9Bsə˗-^yonݻso=Wo=x{Ƴ[{˯_I]KcͫW{;[k؏ 7V6;[¿ۯ_?߳߿e'߶Vc7bW{ܺ|ٳg7^d"_HNSMMN%71>E8 O.^u #}]]=g1_8NnÐ7D{ _ԩVH/yد=l!\YlWlG{.._aXCu~!`qٜ5򼐣0R4 A׌ U9͖#CKq5*  L:)Z0k4hFm'K|&\* fBKhKv d> .BM!%EnqEU@b4Q\M$#FR=@1@ MDi0$W$C -1j+aH'ҌI.©DVNAQr`A8 }~4 jr1E"D"R$V>\%53ā).:!j6 үsL2^UG0,ZM!%Vqtft*D'hTM- jiU0DD 3yptEiVv͐nHc] ʴSB^Mmegǿ7 Hw)B"$F3VC$]Dbz׻{`[&Mϑ_d2nfr+q~+K!'Щ8FPXJq*-JD`kЮCү}hhhG$/ M/)Qv^U  RdS. .;E΅QAe! :M`UsQv^Y$;@.ņq7Ydfa5L{&1,+hfjyjĂ SfДox3MnhFҝg/#v"`ȅfS5DsNdsC:bQJ8Lz2KlԁƃXY-2 4' !D4b^I87jhb!ˊgT6fi +o\4QhHyآZFLpͅ@]6֚suꄰ33ИؘKk޳ɞڸ\{z,x>8>;38LtyS5੦/[ϯ`Џ {rY@27?7-\Y]^_[yUw?x'+=#l=zǝwO~ }6{zbWybw_`?/{o^*˟?~>_ޚYJ03]㟞noopcegݞMNMM&timRx"@}X|r2=3LjI82tFȧGߙҡ EbW, C! PR8v)hv}xu v52:e:'#*K; օ̺)9ι_žG\M%aQ퍅sXʣ?Gv XIbCړw)Ru@ !QBcf4cD>W]1{jy:kk&p/:j~WAn=lQ6KK1 -q Ҩ1h.h`#>eK|rYVKa<Ts% I3(n1*kTQ :9j:ڭ<.AZc^*r9"ASH&ɔH[%0# !k`~Ԧ1V=;Yΐ:" <@!RB1m5r:F! JwDAkaLQa$r5   tHa!΍Nk}ِ(!uru<ɸ@ h7_essaG.Flو}tq{ΫIr}͎\z0NXG['\(.[gG2æR"#( 6A(sR(o_ej^zxv赒pMo$]M+cMs]OV&qa-1!BԶsf9c-PpmܹfV [[wnھ׭<ʆ-=5n!d91;\3ɾ@6`|,- Qj1;5;=?N${Χ{kbq |7Q|U {s 3 $Kk7p֏փ{w߳l?yh'w?v7'{y!{^ǽ~OԗϹoE^yv?~x_ϟ?/OT=ڝʀ-|lu/w7n--Σ<7d&ĤҴt*LSiMKRBDLKOdaA}ifs{X_( J/E}/\HTl'( J'9>=0~Xn(mk,ݙЗIʀx9J;M¿.fS *qw4;%~cqoK%& ]2yX[1B6MmvTyM:zU^v|?\˒m`.tI BB܇8Fcƻry+]_3&l%g]IIC7ݯL1zz+<uS7kV,~uA(#6dXC( ;:*(K[`J Ddx(hT!)::)]&NBcE( 3FuT8ldm-d]|RZ?x|RRggs)ƦI3+^boz_ *WMNo!Q~4% A{5C2eЬcX@IOYN~BgPɞAgXJtYV*׉U1V&^)m``9B_ȇAr J&S*&,FqKB[G1Ms#!x|uݵ?#Mt{.pj_/}Gׁ.^q`eԢW0҄ei@Nh,WQ@` 4R1"RjФSJ +da< r\"8S+M|E'*P9,U jjHʐwđu# jbjD%~j~ָ#jg}3'q zk5ج)h3:r)lLJESЎlqLBj{/XVʮlfBdZ<ŇѠT8CC*.3LKFg]t03>k\$ ` 60bEG RN);ij̘qn\'k^l]3mMې0ỹ>Y*={ -4?ڝ2/EĺE0yr) 9xxw6V@nމl> `Y|r% Dz7hnܔ_r)PZM{І8hŜQ3Hlj;7;l7~'03chɕŁɾݸEԽ=eXZvǯLǺk.OPPk8h5#7#եB ֭ÃY~7'n~rΏ}g~ӟ|q_}_?yѓ_>/?}_z޼x߼xc:Wg}wozq?x٣gŁ^>߼y~W??~gFw6f& ӍGO~~oySs܍=ෟfFzcuoodv2;[[ 0^X_!&#ku-i;fAz3=/xK%rLđfg3BKhK-rL.,..766Dxlr2&'G'ƆGE}Ã2~$k͠ "v&X f=qC26Fs1ې.hҋб&dFN=ƣz :u4@;|5djت'bF¿C)g}'pN`*&x,OpbY7h DJ<Z)p\-CknVC`?ȯ\WD|bfi)0B&16 'c?½p5lԾTzZ3jřD1|Af6oBAEh*HKK_JAH;ہ s>ܺ=m&”v9ets,[qzxw<o{I;L ZĭQ @Ix4@Y§%b:":I9Nt#i'-b!Jι2qf2\_OXΝWױ)TE֪7ޟ:oH,-.[nuƟE$lˮEM޼fڡ=CJ>޷6nؙ6 7Kfv xѲ7`?Xp.I9^͡[luw὞>z{=W4kmk%D[Z"Y]|MBPЫFTE'2(S)h!4%}YFƘCuF\[,^Qb6oI(ZW0L:_O,Z):w}8V/~:CԡEnzcMnȚT&K|?JC!( БХܡPEPGC(\-NaM keʸ\j:CP:iN u MĞN {J0(,efka œ=_%õ)7>ߥ^\gQ0IVScL%Gjo'0`ZśF;tT]W_x XB :U\u*"=C:W7_9}[jZXo}0n%JdEL"R3SB"1OtT;CdS`im4-Seq^mWg-mՉ7g|G!"sqx݅a/n)h9 8|~kW/wƭ}?߽aʃՇw6M\d/+/vVv֞?Y{t}Ƌgv/mzd)[IomIhSdk&{KDž߾~b{{'K߾0pu1~|™ߨ?߿UO?|{ꥳlrr&)Mgx7=-MLOK530or1 ⢢AQPWwoOO_7yOI.{ ?|D$kooh,mCh ,5B6qU%dCqEM0.cYuN+õf(hFQ9d, 5ڐ)7쀔eEkt5Z ^#FVgqku![AH|d,'(EưCJeԉKtNLC\eN+mU-XH"lT` 1H|.2AJA,@a0KC'T)D0RʊL!:k篽H\W "W;5_,Ak~P(ex|55M"=}Uj䆴%jp`Hܨou#NodU[rܲ^2,vIiȪ 35l##l+`${TDq). +oelH/--V̖Bs,Sa:@U1C9;YNA9c D%R);i;("Fs qRYSQS"D.)C/R@}>|F}b~ hɻtWU+2A-WEhWE (׉(`@>NQi63_-Y_c`c kЅ8tnq{}wY k3:"wƩٔFow3R?S1B/iE}2qJj=J >qȐaU ,p^>- U(=V"%LkW6T U G#rDh=d j7ZT/1BV*%T.zTIzDwMi/1{&z4tYdT\Q@V.#Tc*F/!Uet"U1V%2TXp!g#x&@5HꑀX,:OG50muL.Eb`[}d⤍1{G fݭԸx2̄17Ը.2 3K-eqҒȇ,T܎;V-"V$AbΞs; 㭸ci+7'0+iۍ+b.618aFB'rYm CBH}?Z}֯/ Dnk3^VB؄@ڲ(ײ#O ;fRVcaҞV,rI,>C1dZ YϬO]bOļgּRV;;.gB}gp Ҍk:~؏~~k~*~~*|pߚ[oo!Q՗ϥ39̖Lbbˡ 6EKsLj^Q\%8&)Rsb!_(0P(,^`^} rT?8I &#Ȩv]qwt32٠FuC& Q%sv .GG5biFg\fM^:O2K ÌH:~}W*ہe00ןf:]ͰYL];djx9Cy6}q[40թ04QTzPˆ}B;EZen)~-9l:1n"?t oƖ#ÝMgczr@"c1C#za9=D0c%hxE(FmOXDV.GYn#fmAU ꪔ%Tw/YZ`ސRTE ."^P!IѠՊ;[#۱dWp)@\OUWNѦM, P ]8 M # _m'Xd>rУUݡK= _vRqLg'X.lk>Da37(]dOv`3\-GEbzݡCMŞ(hT:$ܸcR8@KNᏗ?.G`0Ϝ}=exNv|j?eqVe~mŠ<*Ld ڡ8gW 0M 5HYAm%M DeϠ2&t_:!ƻt\'e u:+QYY&]~i>%y&)ZjD+( Xr$DVCC+si G#&tgViDP ^&jk(nl|@3ˁY'Std[GCDkB:r&ɁfN E2UeRl<P8 EbweF 4E] D¿Ե+< 0.a*m]3ΘXZ*M0 F0cAb;cvD Aڌfgedl1yc~pi+S/MZ`d?,zE\H=1wg&9r!9/5IРK?h]ZB>PX[ݽs}6Sn[vKw6*;ۤ|SO[%/W.H.oJ?nnP*mrig{ >/?w/o^~}>~y2 (kO2l:O]"|!'ϥy7%D,KљD"J|\mWV l4o"`/4>c} '&/$ GD}>ߘzax]aN5`oPDЧkY[6vk\:W߯uVvpOKJ - /ϻY  a DUJGS%$,m3騪:5?~}4Vu|9.fɄ3^oMlk7yuetV:`Mcē/Pߣ1W|~K*\G*XKe@~B>R&U=~ ɀ:_r :(%0Sf3ZY C0cU3D#%NtFgvC$m+H ( ?C=jJ8Ga Fj)M8a,q6^_jD<HψU 'G  p|Ƥ?509:% # 򹪡]H5zkM(1Ĩ&Dd,:UXpP a0[}aKY\p( 41aabq{x{\MS[' g3E׺ɍ+"63 R U蹾,de9K[} _0zu4"QAhK0Yz8/^l\ߩ/-.._?ajD^ maiaKu"]!D_q[Y2=Kה:& Ug@ʶDDJ,)ycjiTf%#,c uUJ2C' TTZ$[V=g=?c0*^qE 1.mNh[BMJFЗ`NmmĻE3^ÈRAm_M6%OH$#%BI:`bHL:\ju|fqW\wT%\&WG`p l6CJMnKO&b-xݸBoNE2%(37672T+[1}u)ƾ QwgyMTaA۽961dwӝaaE6Ҹ;pBO;|}yȹ.fNr4`h!+3iv&C xҏpTa ` rēi *`B>HϦ9k#.$ f;vʠ뢸2_OM&i I7H.rb.rc!!#*(oݝکQqFk+tک_6vo}l&?(8:xϐ?>w;~~{[z&+,V-旮 o skk&WV\.0_C}l6"sO8E}Bfz:L&&&H"=ŽD"1H$''Rh, ƋHT #H =-=-qWcn mG=򙴞^- ~UO Z߬'aCM(LTVotѝQ7AG0Ӊ^Eq4\qW3]\7D$)>s5$zYMGR1l"N`r8wx)}}3DI ZPlNؤ:lxe`񆥣@z bT~!qqyz!9".].CcT3ց:ls/m:H. Y YMy 0{q/0dL-Zzk2uR4]^') H@5(xTK@‡Cji5ԧ23,=8'dbYK1`2tBbq,5D^ICJS[E_kU}-@Iz@R6 0!2;ӄ?JgW4v+u\#* \y @l3SCRShnyϰ0RX(mMjjʴ`+ )Gbt8[=DR6t9OG| JN`RKC-es2 R )4 ]zhB`uDR"lP>R$&|`f:)Ȥ vb9a`]2}1K:SLc ͩL3[L'efBP;z =Ny9o)nL\WB`[*ЂI?bV/ZLqucԤ2P ZrfO"'2aBy#NÝ^pQve̜.05@ sAizdʔ0ƴQ3jEJg95]= %&ozVgrj-ޘld8IHhT)cǭ7% 7s)NLD^mF֧Ǎԁa&9dY8aά# qodcyvk{?;UiѵiGavw/ tç F/'m;hձ|.=|tivEe5ghWb0:w S|S2+Sk+++ח/nnݽs_7Ǐ<~t#wTzq]:y]*.[ſ_ޖ>{u{{Owm<{tAW~o~ORi~~G}~fa}Za][Y^\rы< $7?7i,岹L6}뛛S)ȇ.%&`矐RD"N d2cP8 Ecx<&"(ƣ##QрF]TCP (x`)diwE5DMe<kΙ/h3@Pz>WCIu!lBX#M~5M? b OH?QG#hu.xZ"ԍz[k}}DЪUsA& ’b0(l셅5^Q;` EuVz(i Sr5Ww$Tv s"/hK⨅=r"Is=. PbaDX8t1#F\j3\118 mZAw/FL/͑נ"_i>cOH)$Ě2I"7[ML/e1}뵸;mޭsݢK^]h P13؉ods0 gpZ1qQډ wJgu HHCRI"INM3G;E5*C_HNN"(tCp|k`LIOpHG1+!Ϩ 0! 0G_qKF#!s繤UMçGKp.'ĂjKԍlRsκPo=K5L0/ý&gƸ9 x4 cPR33̈o˜> n'-> 4NdQvzy.l^wOl#gb +s6dMLsK4@;J/ey#7< 4s %06cf1 xW6` pRp3"bĆ2\m.a[&`$gg,ͫ %` %au uԨ>5ږb>1= :Zy^BR fHQ!Y"慰oV'Ya>dڗ'eQ;[N8i0ȅ[c>ZQj:K!h}9Fw4k.L3齖0[;7gnMlI_x!oH 6M 4Wn\My/~>Tol ^[tq`O-pf'دPliy]7w<{yvvlGo^>yɛߞ{opgwlmomnlmn>9}x#ͫGG~Սgg\&33}Lk䛡܁|T2>db2bSS)`rrR711gՕR) C@ǂ!ƂJ~_`,ptO( x$k9F;kНS%JjUy64C0C'{R]Z 8dqx/X+@>fʨlB8VdLKtaBfOBw~K/]*n35\:Q)r 5F SMe\_I/3Q8h@z_CYF-uᄩ̨.(}1O/uL5 'y?Q߇>~Yz-mV{'+eXxfDU5!.vAdvRC;%P<%LK2UIHxK! r3Fz ;UECdcd fpY3.mĄV`Yp!BidTQ"Z 4k"HTqIFr%Bb:DfY+ŭjx6576@*2Ӆ;a^7հ9nkReX6W?!iH6=zRё.~EP 'N _oQ;2W3PtF܍%䪛Zプ ĝ$ai% Yj^%L'ǽ`쇅IGv[.`LPj!jSRkQdI٠.f}04䂖5U¶BFŨ^gY@H0^HbɏQa'խDjaF uiBrFb؊"[ivw{)f'lv{!؁`B[zro鳾vc.j3c wKnld)(eSIٙ)d$ <@q%ٍt1lY2Km#WW]RtijBkX\yWݍA^xsD_ުl:6OsC۝Zz20?'g+%~ᄑo=\_~{[?m?`~/=|_<~Go}n=ȷhcccsc}kk r =>~޾uD}gw޾z껻Z\8ϡ,{U.Ґe23l:ִg%' ȇG[ȗ'xp6YZ^^X\{p |1b^MQ]iǿT *; 4Mmhi@n4:cLMXƉF%2$Se3fϹy+Ucu9y>[..VZRRu)xL*Mest&ҙ}$NH̙ JetІ]XoQ/<ΆpElR g[gH/NLI BĠbN,Hripڔcm& 11bF8GmmIhV=(uhdm>j\`L9PИYL+ E{  ˃Y'۠5P l$ pqs_̣C>ᔽ5= V3BP1E!6Q۬%OHݳG3)N`$I\e `sM ڡFi$S>s6h\۩!W-l,&mBڬoӜ+c1:<׸*xȉie0d; J~a 5i¶V]ctrtkՌ͈ HT~GQ;sKg#Ckܣx_x9 _i (8o=4yρ@GI> ΋zA0xΏX?aHzͿm+Jkkkz(e]m1ia>Ez|}YY"7q r7˟^ɰ6X (o GX&};>4fs+!xJyӊs OH9Ѕ9!mNhщH.. I7,z L KQH! M?*Jg)+RvB)vOVuƻtb?2zZL'u/RmsHZc#x;疥x5l&~K5K ΜZObl V9Mv8-)(z#g4sa\kKJR^^Qj}!^:۞w.A&>AwV'+}wl^+_#WL >`ijR#ܺ\ x訂Y'q}l۝w!z QS5Bz1)z Tǎ”5ٜF]U9jΪvw0%<~z]uHYbW17rs޳W0O;m 򣪶(Z( ˚2' P<Hˍ50@LN:7fWWQ~zvŋ߼z??ӛ7o^*}N}p~k~W{lmظ67ol׷ ([]i2A[]V˥ZV>:998<{b+ W^,UxMx"Les\6rŁ9:rȈMeߩϜ ,Y95Mq3c)x6N5[A4I|{)~Hl mH;bǔR60 waǒ~S&ht e/i]P^.ldZwdx -eül؆XؿAV;*bA x25\mI Zpx#e3 6`##@2,4ƒsX= 4h!WEbeI3΅:v$rw0-l+dnNxcS.~Pg,+gl>\3أZdcC|޾}c|,)1RB0ʚ$`Ɍb3u"jʋf\\p|W7 X?Mh@"LIFs_@ˊ:ThxR$p$&0 X@ ?~Ewf^3 l2" d*R*p ֒:#%8Xb搉FdT½2"+W9B5$ MhVf; ֒?4(fMz)Kwf/̅Ạ$9?8>!quzg8 :NQ^gxMЧB晩̍40?yܹ}ך~w??6v?6Wߣm>^{go^WOIdw߽w>~&iu}}ukk}kkv<{j|x.}kymŹy7gB w%?>=]hbrrNIƳJ~l,r|\7&䋧R#L*JX^c_Plb$è/MgttB0BH7Ez{zw1!HEs7LeM﷖6Dg: ~6AOUVW諢ݬpVVS)]v'fiv#nŀBjh|o-๫X^.ÐFE !!XR R22=xDRj)Ch~n j C|R楪ͪ-Ԃ [.U7*Q((pfpIr `(mRJ v!WG\͗NDSqPFոƨWNe.Tl FV::*P]  ‚IL lffYWy,~q@m_P= aYo' B:쵇|G& MږB8!$r=/r&9jr*<Sz G)0!Cv0 .}!:=b=}HlTs|EaD3=^ BH{Ea7#h>~EC}?~E}3#M$$Ϛ99:C:?ns ;]xȫXpGMRIC.5Vvp°HڇeAlCB_ݿbLq;%lv4 v!Gc% cda ) DbVh˓5R*-2GAg:O֐ޣYT xr_rzMޭpr[̘*>y۸Q\M//B>X.B"Lz4ƁF ϤTӨ0d>MRs)`rݗ"c Xd? 2T%XDXw}@ \L͌S0nlA+Eq_7k0hv1X& l^H>pڐ򅴗e M,XsĝWz֩~یZUpkT 21$,{.nqꠝ}>8`cw-tn71T ԀisI2(*Bν/CKm(DtNdž02r 8]H)a%|ε Sސa]:<;|#u«8+ MXse\q5a>r.眝]Z>7#[~;}{WWl>m!\a{㧝l=xxӭWWOI^{oawvw66W66 î)n)=|-wy ~Q;} 4?[\6umjhfjj 1'f(-9>(f_*1L&ɑxb$o-x"a/N$F҉p"LeD"!ȇ"6j*cVk98x~񛞒}a0ߪo17@\"=8tT@aIn:.+ j@Z a6-CsUBncY{YINGUSOP c &yM5zO4lUv_PşpAE}Ic-"R;W[. 0γGHM¹ҀR{!(^$(_knPSaIE^qH iUP(BGYhTB]l *.2< #z}fLFԕiSpi/g3;dPkqR+1m`OvK,:pU;5"Of uKSI>= It\uR S *]L/!_r`ovNG郚"c]h !"Jd 1:(ޫ9.kGр* #_YWI9m׸ED%(*;('>1Y4r1bjo挩p^Z- Wr͒s+T}UUӕTts`35dh3a:o@tj/-~I ڒҐ,,C\iRs-FjvӋa)ҩ~M IRRk.wV@dr]gb*[JAfC2cߘ5!t| L2eҭL⥅14ei J{d(.2h_DfΧ`?݂)y)nj~z@;;aF$y~SG2X 6(j<öy-ba}§x ~=[ )>"5Ƅ&1<TL V)&^bl,ۖ-h-N&'Fq-Bրy/3*AJJE/;)"ҠInd%!B62LP2dv]'4s}%;:*MղQGh{ wUF+ÝRXx,qWTߍJ_sue+ YE, j'M 'R/O.&Rkwý6`sogO>:{v7{lӓg޿էw/~y_zQ^|v{3W;¿mdxxˣN11~||GcԷb9)Dd {Ñ0?h8llt$8 yaO(4pprjyey;4*ǂB8Ntw*?Wg'mf@ # /rY0b:Ozwz>sC[mN]CsjsF1d]Eio(a`K Vˡ>GC>Y4MХ˨ yN}QkPW+7{,W[p@(dA< $0Lk]6N&A R~ DXR}AWz&Kq<합M @ONZ bDM E:8C>{ylp=|v(TFvî!Og<ӚzՅic"[$jSۖS B\ɭ(͔MYᲳ!O–OUn\e)jv6 |b6kjʓ)ELԅdO9Q)IKMLI6,k%uMôTlVCk Mϴ ZDegؚL_y讞Cqs3IҨph{eFcw;=H ̀|uW-93 ׊Z )Z#\A\;ʾ˗/?C; _#fI+‰ل,0A& s\KA !G,'~CZ|P!A8`+TF]_.0(Qi}2yGo}=M=XJ8lE4(C1!~BzAbXզyznTΐ8S!( {گ0%<G8Ѧ)_)Vn[K@6*@e|E_P|e/ :Az)4J7&@o4Q2%7QxpHhCp4r}f/K1uFK>iuDZ g7'{)$VqOC|p`Ge!wҭ2fY Y]*h C\,79+ݜ)gkP6ag&a~XŮe=ڼtN~'lۘlR#"И3o2`"gNz4B6j# x/4܌1 B[s^nʭEzS7:*;oDzj/BgŨZjΣLWOސ/dr>HW1 s5s^Sl6J-csTrauu{wogg`'wm>;zѓGՒM̑S!bGl5p)-$'ixeH<{3lȖY&ړ^sۘ$`1@%:ʄҔ=q3Z[]% 8iOKD#CszbH =b@t taEB8!q~U22XD~ :`@T/Ja(b>9@r#c ppA-&y>B <2`uj]>|JC'aapz+䃂sWzKw;Y\iup10 77NR~ ¿Q߻w޿?Q_^+C=7(ǐ\(siRdيq ;-t7tqCՎ J}z|EcӥcPh!00`c.A ?# #<4(وY,|(srt`M"zqW៙PhXK mƬ"^ ,]_S^e cZQy-$-*V*JH@m9<4Ox`-Ԧ i3؂#Cw;٫&)j=j#wItT΢][MF,_ Uk}MS,<‹b f5ao݅;l`M Q)p%?<㒴 -H?O=lub9rG%!9m|:mcX^wZyq'-CA <.aݦxw6.܈9涚g[jA+zͨ{M* W[N~vwɋ񺎶āLA;Ƞv^;s *2\ܭcQ9xc ~3|=Q/DZc۵ 3? vewHnaqOw3ՙVfd4V&9Mpyzu__~~WOٗ~oV{}߿_}gO~{4..>8B+,c?yop?~GO>{z-xh`Nk!FhTn[jQ[vjKJjZ޻GvxjUmtjM$t:f#fsB!Z>,#%qIDFdEam\psy7,kQ[1bX?1BX.CeBؚkIyLRY$L;3l*l"+fbGi̳PkH/3B$" =S!a0ZIqEIp.23P[vPҎSape;Aτ qHjo>H#W֎ybʄ׈=uB\$ . $+e|HQj d $^ KBn>hĚ *CJn<PxJ:,80Ӑ>TVғ@db.*GIn@Y4d%Ё:*Aqa۵>ǫBT#Uvhғ9ϕw.ޛ֎!5X2A5Sq耜F pF=^շF~3B;;a*~暀p=Eeqᷟ*O( cܓ>(s? Pc>~n8Ο46s(:҈Ʋ%9 3XćA#6Պg$c :5ѭy[AmQ6[ZRLdzlCJYF"FnOAQQ䆬FAKG`UIgeDYn̏nDžsB>iK<(̇P1ӹ$:K1ڕFKfUkc&L0bpબM(o,!ף/Fp Xff&* xc\e +uFsI<^Yr=!݊"O@ndheVSCTƙjQ>^߲~#jSRSt/5G'7$N=]b<6I-|8&9?:t' k&E߅Tokej XAbئ: wc̥{QL̥")L3SE7;;33Sp߻׿X[x,xwkw{uw핧{koxs|O#}xso߿9Gk+oE?c|ۧ/ߧ*^|?~wwyly~4WLOM/KrX<E/&`y#l:ICD"GRYr \.-,-.,.T*ɦ!_x5::}C??=}K=~i\֪i+׬򉍔͟MX0U&֦kk<3)]G7+;Ou a2$@qlAnI n`ޠуOݫWv"E$ՠě챨efu J5u O>} IO*!(!>BL8 c{_/d5hiJz>QO@qI᥯#-"`*#j$ I)wFQ2$@` nSN}ZqR4'̮r'<Z6QNX:KsswќtFr]>$Y 0~IIa7԰ ,3ǜOYϋVDujOPNX1M_5 ɘjq.bH|.E2U=<#vqK'fiƶ#f{q#&P7 ൷R%ڳTO}TX,/Wϟ/IZX.) w?W,Q%?mU *U3n.БY"DX"h*J\y, 6jF]Mh㤤&(;0U#25)0LpzUa*2+P aOՀp肔xL%`ZU[L h VI)uĔߏ#}ȹ ZR}it+F#!HXd !AG%<j 4*fbT1#$dV]Pu )676:x.?Zxf$|hiaj:nXtQ)l !LVsn_u- u:nZ)a%!LD! iI Uv̶\p"S慼}.eUwMaQMDŽ65u1pg9i+#Fv&@C"&ĺ/mKiHfs)fֺb$+Ӌ NDt儞&݈!nf:0G 2,XPSNq]Y&cR ZݢfZ#AG,ʔCdˍ(5L'±VȗFFD3e{m?Y{і:<᫃wxN_}Ez|ݫ;[~?\[[X_~w=޾OOhrxx޿vvvPJirvfjRKF2SbgT"F#X44@zP$O {B!?ϐ/P۹h  x/-x<ޠ3 gctN#ȇkmR3P$gmc^K*M~O@AgIݯ">RXKÞ #U}5ro'uDPtvH>cSZ6@ȓT,F3ٝ-@,Fr$ @h$I*!5AI6Y j frMRלJܝRZ߁'#\p&I"SJ@NYa;9700>L">SbpPH"@$Ȫ!8@LEGT<ʐ1'mQqu{Kga]>os] !FKҗ2}f99}Wݻw߿=;`MxgfosL ̾}$;NŇBk_Tjdspo|&44>allc[Bc<# oAT͍^j oI%CADĕ(HQco{Hgƒ!j,$(sky| =DŽ ֜ \ N(R!rm<ƔP\.L¨쉸nH6+CXHFX1rfL#12"7M擥1.2iiVt p*hyr*7!)EpOnE3rwfFOtҴ̧6Z+Bj|dzV.;iO>˳h)\io7 rfEnZ;T7OKDYBkB~=Ib K(RwQ~w?G<{rgӓWϿ7/IsʛWo_x'޿ ؃|_uW~}{Oӷow?~{OyɿG~ɝ?Q}QUjZu}}mcV.76֓$bLtzj9 l>eVVEWW*+뵣;w:]VWV+2++|mRIQ(\df.216`Ӥf MaAZۄ3l{+s5se{;]qW"ʏraTky-|->~om⾎sZLi Ш[iĦ]^\EVH9!B@w.^v:$B=3jQb4!G3Nf<ְ])LFՏ1#O c6sN˭PΏ-b;n٭)?.lB6" W$9% ۉ`G2I$ Abd@RꤑTɛIWIpM3;LE/b$rXL͈22!ABg=u-$V4!jQIy5i]R'8ªzqSĆY8 p ~jy)3h;E7 |q4v v\SS_'ԯB_]d!ˡ;psYy{#Tlm @*a?r]t9Tʙ+aP)]&t >rV}l6 cEh(6_6$ۀ=4bc`#[Mť)<|;mFB7n|WW0l&](ɿ، R3$u($dUD_6i2n15f[ Z 9[%oەי }V&;S΄_DGQ=^CĖ_4`e8ؖN/;&qXcępys/u0VRW kqQiS[㱝8\{Yqc'_-*nJEn؉jfwX"ttR@UFuÜaJQfbˎ X *:!q)K=T$nq<)fy\4,}hsR4VԹ{ЎcnLH/(&LٷRέeZ\[L5O쩍#*W[ɜFBH a% q€9muws&z>a[˪fz3Ӛg,rq!4-čF,=ᡴ_s\rCa t~0jD o*X7qLzkX Olaai:TՎ:Lk^nԭC?ҲQ煕˗/]zիoǭ?ڸo <~?oob;{rOQB}O_Rׇo_b{Ɲw7b{g>zۿj>|7Q볳ˋNgɉꛞ_X Ch$>oA}بRxbjJȗHss3kkk׮{t2HI䛜J_82Q A0mА{MxFrK\C5"o* iE"%@5b*3^ёTv@׀2!?Vbݕ*NQYS@> `Ϧc-J uLۮ=HW%1#LJ"=j-$Tqo/Lސ k!g&1xP;OzCb<wWe6U-!W;^*eYPɫTЯFMN+"+A٭,NP@Ī T"Ӑz@`8HR\u r9a8-UBcIOeزG!%٘K^nzD}WBIT1$AUn11gn>;5{PȰf#F8=T~gpHE`P|5V!rJhaKgzYLU qeA>[}¼F1MRrX ܘm΢Xḳ`va$k8-Q5լ?'su9Xe6,RZ!2䐖 گoieS&_RpG ƾ"ߗ~f'Ik~i!"5,hg9vRo(.?Ep!"E35ŏ9 (1qx}UgzcNלz(,r\G* Έ `P$LYTX(hiO;9d;MN~gm  ]t SyPav]?k˂$CMYG'XwfKoŅpȭ1(VpCK{( sVa b$Jk#NI=ȔLMz@MZ uhN M%j^|]t`4C{E ^{]No.U6lseAsU PY YaX3BGLJUA<\]SR9B2 Y:ʕxSb6C1d[*F f§r0<-D +q\H+ 踗o lB~ʯ'\z ȋ\@rn@,grbEJAί-fMtQ ˄t`uu1.p2Cffxs:d TKQBļSNa_;2 ɇ+q W-8t pگ{ܔ[,Fm<52[PF1!ـ+8gM)M8䨉9G5kZ6ƨ_M0n*aDگ$kK0VtՄ+x-D} t6|gq1#wOdsc{o^lcGo>{oOgGN>_9~AX췾nmnloÿ/z>~p췽ϟ޽(ͤggF1_f~vvfbfz21 E#CC@``{{{\vrPK$Shzf2}p;+ّ8KGɱ8 ;]=>{z]zn vwm7:] ȕ:=l)r+N-X*A_\Tl7i <o"ܥ l}fy$ɞc E٠~)Ď5`le>ly)婟  E3BԂ|F=[s!jiQP-'3k@ˌ_7މr15O5D ۖPzL:{a=ɁO.X E6iH+6bG!͈Th q#unY,I.JLԀvi,@%l^bֻIGӥC1 >Tcb$%atǼeP_Ĵ8dZI_d&ƥ7t!^ J送J5ل )j|0Nd=3K&d" C`00uƑpo۵ Ѝ!tH:Ѕ$b`lڎw}fQe'q_UR]S_ L*峙ɉD41^*J ꫓go^߻|˳ ՙٹbmnfҹ|&ό3~dd8T"l&߂R>#"9FKoNL,gAhBg0sja[k֐DڣEk]"2X!s!«U$23;vHrd)0U^Ӱs$f lKՄ~S&h̙dYq>j{hbDÂC “x$)3~C!b0N\J/ˇL^OO;DllCt$ 2ه0Ł>wi깉yQk~ɡ Z(2_UPH޼LI#yVZOqE25w1(2lSՐ!).()L<:ez!ILR|Ak}2XPLCQ C(k!&) <$: ^ &x ƣD-/t "2:FqKgGaC#:ӌ=5(Iq`CДu A; t u) 6N5 գNwqWc=da;$N5zhAxyIh~Hvc#BJo 7d~BٝY)ybЗ.L1p}̹6Xt45|b)}RTk]Q\oZ7&'bKŝ [[v^t7ͭ=ۃO=|<|g}ݣ?~7˟xˏ/_| %z?}=wp7ߺu۷޽s݇?o߾O{񝃛/}U;?Z\ڹuԷrVUQRuq^~rq|QxeL}nvja~R[]mn\_]~չťbmiyT- d2H3h6D*tz$I;{'mT 4=Gz4H PepKⱌG/ 2s.`8'尥4p]Gph,߀!N-WA[B&znm.dD`P8gR`HLq%Ps݂7L G#4=Ϡ:̀U v9u/酎Hw a^ZBS u2hLʆLx/j/Fl9[ШP\Ս ٩>щx뒪ܬ v:Z.F$eHBt 2)&Ÿ5 *.lSUe7R`&R$WnD2~ zȊq~A Py@ʹLBV訉f 6 ({սTWT$ztu^k^%:1'@GMa8}#4wD ԅ .MqQKQAb*f<,N< ?bB٨4եF fE$O6mim9e'@P0 ÿK}>_ԇYc|P"v"ɚn"9οmEvl@Vs[8OjŽPLbe&Rgq5HP)HG" B; fHX6GȄ^LAA/! Qb1b&aٹmfD9I"ńpt $ @8T`&ioBĈIcӤ E*ЗttsR+_V3RǬL[Zi\ίg]Ǣ)A-]!qxdUj*V-!ϓUf,dgSTY϶["4Me@ MgH#*àʩ]kVݗK{8>czMdoh,[I@{)mA#ycF'"BG~ яBrD85L ?ւOJ&chLi P83\ RgzH77`rBpdLޖE&D4.)=]u.A 2BS1h6nR@nyv3!]2ei>me.fؕpy1},xf?MvC-m:5d"`r!:$b2fO;⦌-{aמ]] w #ĵd߅R9&-8iv7[ \*.ZŴl+VUPw\=t-..Zf~?t{??Xpkcӗz+x{rgƫO_=;9:x7_?|8_~z74x>z􀃓͍Ǐ7}O~?''(}zoѡ`x2(f'&'FGY1_(yq_:' 9b ~xo$ʤTVbZz`u+ws|qt0Z|r6AwLJusxqwHr7=fM<Ex-j)yMmUOwamA{'yFi[7Ug&ߪ XjthepV |aX!B`^(s4GZ(iz~}uc&`aوPsg#~ѪH~g\p!@gVo;S `T&z)\W*PY@afD|״a\tϑuJ#l![_I,VyJ'i5 >T^mH\k )9H0vPG"W;*uEDQ``b08O1q9 r^ H?\7i{QvU[O:F2.?PngVZ!I*EkO[N~C,lY.}O]w@Nx](_)pCZΘ/.5=楨)m ׮Kg ``Qx< 8:TGnb{d`eAsȖAG,~Xuu /hTpM:ȏzWUTQ'(Fvl7 {7]rSQs4]"@v[hw< 'cB2Z N[kϠN98LIxF^.MV[١ 3ꪋvk +4#K픮1aP(%?қХ#]^PҤ1^>U75(AEs@5u*:'T`Oju!@^[CZeZ4 Ѡ0V Q$-9ߌBƶ0&iv_(pJT(eH@{: ~Cc:$6?@za-dbPpu)k]Xow璸Lzv޽}_}>~o8ǛѣՇW[w><mnSʨU#.)^}5r5HLff^sm?5iB6 0|5!ù)l| -jd56  }Y5W఻:0 O`^gOQewQ@ٚf7 J/F7;hř,eI8!jM̤*IeW|w[RIU*Un>99p4 &.ц[`Vbtpȴ#1IWF"Iw#);- )+e&!4rY$Yogj/"4y 3k3DŽ⤹)>G%@K@WNs2! 7MZ(n72rӬXt!2ʰ$_ʕl@P,*0Lٌ*ϒ\Ɓ+<ܩSF *< ǥ;^lB5s#4z BX 7:M\VB@{G.._j!K3aQ=*%2CH}R#2g|&dG@wXTGbD ;>FJ}f0`24}(;/7rUՈ6`lqV~B fԁ7-j$Hf> FIqAmij }!YDFbP?MӪtm'ȴ/1'<=]9LR  Ce,ڪb*:;BARW[@S eqH{ &)SΈt1f+uI31!xP &X; *lqUK4sc12X\U#>IT^ Xqۑ0 qspDD-`FV+gbò'3;^ɍK}o\ֿE&rV.&62VQ 4"- "+%nd;1B8Fw5xXԔm8l7K mYEV]([v)jRƇ휕.{%a5i+Zkj3TY-)T_iRilX.#Zm[G~>_|W7W7׶6|\l'yx,roI MiVAU1lkܙF\L?N:;"OkO@>=`_W1K.l"$.".QOqſ*G%?JI"`(btzRi& OZaXXAsrᐏʰD#w7rAlT$RYjD̈́l`@A1\" JOx> B@ǐ˨iL ̶uZ ģ'NAX8g*E*s#2s/(J"s^ePhp4p/N^Nq"Ct=ܨ `Be|x>T#"@2:㒃CH)-)h4>Ԡ+bF'&TԛQ_߹s:i)hԗH$޾}ݻ--,-O6h9`3,,]!{1yyEBцVE[ ͭ >fYIj9/;u[F9̈ $. G|⫴Д%F2vs{3Ai4-|:$?QfELݓCD1+ƣ ?mEkGHj[J\!>e2O0:d N޶R ɽäʵ/v#?[ ! d[Y CGzYzֆ ,7 nފ ~ɶ]Nm+lYRc"/D0vP睵anfyMѻ[mY{i P{eξaVL:nt*` \(ej vrڎB|2Jκ䰨huTT~еCbWۋ㪟y'zV"ղ`޽Srm睫JʺߜwmI(n츔 #@pxg)w볩$ Q i1HPDnicXÚ55/|%ŽVO?.#z@O^ >LQ^R'r(:Kd {g%o21t.m=g&t{:!b5ޚ3ҩ-Ol[3W| ";c.ӌev>!s&R /,<~KK bŅO?|X~{7{'GgGg/N?|8=ǫy/sXf}kk׿wm|fz6~3To352>6:44^*ѕLD;tV}a0[/dpJ˹ɩGOҦkۼ}/B[6"cA[ O,h;l,% =Q?l^s Q kQTD Nsa_*nȢ 6l% ޺>g,0 L+"Jh/ 7!=('?9:Jg9t3~hP \E?),%v&(6W .P[O7MmqWeRbJZIwe2,%'\CdH'$]#%D2Y#E-"(`$\P(Ȓ/4ˈD, UH,Q!*db5r:D>?+*1RIqI?A3V 5pT]#Gs&~?Y 0[o;*qPuM_M}5A}kķ4k2ӓNgggOxxo.]>$Mv\dPD0b?^aFlpT,n%FHNǾ>7 $!128˄4FDG֨ S'Rr9vb~AC5p3ȍ'BARCɸ:K%sFezmK^[C.#Lz0(L,)PrJHwTJ]M6_g̓{1$7eTrE*YgcZ B>:Ffx3q6`vCOCTwu}m4 89TJFwh0P1TA &[.=y0G`:{){a ӝh}xAgE5s= Xe"&2;Ђ|Z,:?J'ctn4R?RAbǴ 㑺鬗lK6H#[~b3oPjZHiveؾ2x"݃| ͼ8`yBd 9+o''MBxIaeΆ>Kֺc)3Mx"%Ҡ042=T)@e-3]yzQ2!>XkQ>3 )32=Zz6ۦ4%\u;g$9R?pz.qhxLK۹vcH^an&9qo4xwX!аf4PZ)ǝE2Ծ&9L*f i$33鮞-.<]^^B}$ʳg?xׯ^~~?nl[ǻ]\ N/Ϗ~tqy~O>moٟ^zc}} mmo~|~uׯ`SP?B=ccc##/LbP3AXzd AL)[pHd2 fgf( A@  GhG$ y6]QvX"Uk7W&UX쵔%^qtP0deśi7Co.OTEŴ{n! }e^Pt,ز8ʳ\ߪ ;,:?h  61jQꄻ:6&C)E8)l3F)šR 6B4TaƚJ$>]UT4tUhFBP2A,\vTȓCbcM5b&E%^c!NcA&HD>G%h-) *)ɰB> >SaخUԂ1:|YaWo~Ri7H&L`8@6RFm H' wnA%]S-UݢzvvVYoï %ZTk 7%/0c|Jj:}8먀;Ȑ6B5DdRJD$V \EસFUUC$gZR"eNL|Hmkt6aFq\'άGnY<~>9t:E#ѩ\|fi!\ʬۅ sGw p?; H0 l@096֏[ߩף#8kb$W#4-7 {Cleq-` Yk.~܂:Q9RfFj 9*qM9'͓f`Pbl<bMo놻k:,pCk&cps]E!f/d;firh3U8GŐYj&ע޶ifٶn#m||DsTL^3C-؆ghbؘu JjCJcjjCўMjϺVW-/K4Ȧ9l & jKLje3g8@p bf7Q)E%/`X5r4+nu(U{h%caYaq]J4U>Bqa_-nG7$ͪ,Ѡ#&@/, :xYJ 8w(/2dP8ШfŘ i.*r՗ 0Y T+JYji/.@K>5WwJtv 9ˀ<ʙ@N 8i fv [,oۻpUGhl,l-%~P߇>~QƄ?C c%4ǣ0l*Uӵʫ&iDWOw˄9 +A+ U*0CR0<&{;&1TkUE023]KQKzqrHc<UX+u*TV$ h7JȐNWUfB:ᚙ={(L4&CQ9\yF/^0ѥ4[9iAΐ/ c-c`#5^QJ?1ڑA8<5cQ^lpF`qF0 |ڱ;+[LۚѲIOۏ=341 $䰢r"=dw lLk$uB(.n͠k۳"Pnp9mRr$"2ԶgM}5,]=ƭTΜGz"[a#bH# Rr0u;fM:V, o*ybN$}#KV *\fqAi@ٷ-?/\jjNݺ97wb[vvƻݭw؏tyv}~~?;^IO?Zqsm-[^^ZZZ\Y]^{@ׯ땭^/{oVv w+KottddO=]'O' Y|g̋"<,H$ BĉH%x_< xl&ҵXmbkQj $G=z/e\;E0,HSq`O]BvmЪu.gq^Gvgd)%+ʑ kxCv{]y0.EmKJz1pKҵ[Ǖ1۵4vX5amg#-dFaWqWT@`o7PNiLLlaepZ̭H(!Wjr$`Zq0hH#TCXT|U]AJ G_*n(F-u@KB$.c#.[)xѢDCQ1*Ԑ$dlVݔ$6j2F"tn/AgVaaU@j<Qb` & ̣& fW@IIrԬZH9#!GiwBb1WQu G")̕cKDS7ץ0ysKӹ+ɫ< N$fHOX}žue; T\G7X䌍E9pV:fId""&[,'IعAUVurTń 2%?(b4RE@m%=LMc1#|Bq\TZӽAxmJYPdz^3x8d:mq<^; j㱚')`62iA}LX%py4?09h Ȥ3AI0QhթF %M(a Nvrcz01!/T+cŃ23a{02Qm*LY=GCb)TKEܚ0>;t J!ˑ#DKӎ'=_3hxBhԀg:]"~ZCIqiZH.}" d³hcf~v; O{y1(RTSY&nVVDw?|^@0VK ݲQ.wEo w.,d29W^`_~zzX̓ݣM췽fgo{o_]|~Gǻ'Gopgggk}}Օ_WV677~8_^w߬./xާͭuՕ_</! =z8>trZ̒2\{3sBg^.NbxG8;"`/qE#h4E.'9v*H/`4 B)1\ ڄϐ`ɮI5W@PW]nq]ژXqj{;: sieH/u7: GҸs rt89eMrsDDE (jb.4„'B'/hE\C4I` #Q)Vi}_mTgaq5~Z*(iSūyy?Hh;S_2[0#ā-a7ȧ#i^fWB$S!.H`^P8TjILW4H+x@/ݘ [1R *-WES VpuH' }20 u'\e]VqnEzR:. )N,  vT!$JIICzk#jWL+4bs٦꒟Hx2W߿}3wUv0 k77uTpV¾ &!$BdE@V[l K@q랞g_}dfjT޳9tUfJSۼ&yzdG? ikrcc!MFwKЯt(&@(r "\IjK=K ͊h %e-`;P  o!F; Zq^.Ux4Ա|p{ H"LxxEFHkq;uW^@fCwbFhaq@i`2i$/ ?!>-k*yn/ `"z\.\؈H-x<bp~4͆u AV3f.ZFborº2 ,qF~#Wryȸ6 Bٰ~WF-s}w!!@Z5C;i%V`<F~ C9c(ܲ-KT?=H 5܈0;)*6R.>jԂ]ӓv9sO:smf {*fBtwAyq)ED//my;gOGLJ;o_fmhEɗGO׏>_?9g34s~+|Q6{J:;;E}ow{˟O2{{mC}ϟ??33X,6191>>62Y[Z_~VUԗJ%ԣt*HVzOcCp!{>/.c5Ap^s[4v`(:**d+ʕEX!Wuȁj^vZ0qY5pb ,%>k\ qD X&-˯/N+%CT8c.0V[楺XK|vXSP E^NiDB:tQ>^'+$H[5s .Dc®J:^:Yi@~_(6a7߭@kUX!m ZwlVBĀCu4c- 6ڗlsƎ:e^-E9^汔 䖋LEܢG4#lDkZVAn.FRUoq'#A|Yoz Y[.:6Zl.Z.hص Ws^jWNC10gSZu0,Z)篫@uz,eK,ڴE"%kvfo 9]Xw$Qܺj72!T,{x:__Wsښ 󸢫R>_۷߿_ץdͻc6.琱 `^U s˄GM*nuԴ UΔx|"r`1)bPRUWq][UV)Ʋ: uyGfr15Y4A9jif hlr@r9Y "V 8g<U* 9pufa g9JW, 5C"YfYjC>Gi SyEbT6&`P%\>xՆրu2%2[A<k\iX52FaSbMC1o\ǽc]5hG|X;rz4F$+jԶ8dԍXQ;AmFMC%5>1 CDw[ 5OS[F;jǻ&{oNHc5)-@8{c*79LEi¡ԆJںѓ~|髗/;wsr|=`if }<;t>ϯr|=' gϲG'G{[6_zA(svwo~ew/eO2N,WPPDFMvD@TUDKtLwMz2EM4$&(y3琙]]5U^:=>~?~vv[z]x]ѐ݃~ݣ+\&est6)s\ffmL!iS&IKcn=uFacmyR O Æ@\: :  F53UO)‡)wuYb$:߀퍈n*ؑtNo 2 0ЎES޶\<ufdPl4Gj(grae !R\w"g9u6[:dly $s*b' -gK1V#0!  Beګ=ё8s%;+C&l)y'9TK.1RISv?\y9N9{NດNك pT@r۰Qt9C6K'Sc89ۘ6nK^¼GK.acڹRx:\ IVd$dAݩ +bEH57$n $&!N܇M-PkQypĢەb( Z1|pVV?XDh QGKAn˝1tjͫf1ٽ-7ۓW27ؐŋkx[yyrxxnyPg[x`Cߣ~?g'^ao=;9;yz~*{gOߝ~| ?_狷>~7ߝ>9>~勓c\gOOOOvO>}xׯO/$Ãݝ͍uVՍ5!NS=K8x<\]]YX\XX |^'fr\H+(D1Z.lT %b隍#pGm1q :s!soA~tў 3f+vVL8q ?'!dZmH0"+iJx cCң($[ $&}|A}؀L b2(}U@5sˁL0Vu.1*|ZItBr5'ԗ@; ]*I& hԬ8%̫+DLΥP5 $4rNEm<` ˱Mz*Y ZƽI)4:a+ QR $# Ȗm$B!1p]@1%[_u6@ȠJ+ђVA#9#vxߖ\ZC-jD 6lny8 d dz)b#8Wc9:L5:: xU+c7lk5[dPYs1 I{湺e:~ MKC+^1L6,zA#c*P.oS_R{Wߗ/_~\!A]#oPx<P:W˙VLũh a%Lpju1:AMӆaF;+,;^1yMZxk8KVϴZU]YXɯz7T!%7.2p6&C0)o7X\W"6& 5&B) lcbqEįt6'MܥEiQ)v\CbFFƃjiYX1l,Z^zv5t3^A ,! ,BWv!xd't39uTWW?U]]H<~`4PHf >(\lK}r8Axe%bYmYLٜ SM 6IY$:N3SOO$2+QC#H‰aΧhp6cT8дu5j RHK0)7ŌHBdªԈM7x6zNS7ȧY럻|mPf`B5M9om9d ز+QR@,4+qr='&$Pqq!N'! 3!7&*'Q 5 & a*E/&m <Ƹu% "2$ f 8Hub<K+ayL̸2 ֔]Q5G jܲ5GKƐ-S7*v^%>78f*}&H=M7n^ gF~c& jjqc6i~&LS&:h̸G>Ksl^L3cL:[ϊj{˷oLJ?~;rpvzr~8;흑_q~u*~ܞ9`˳/ /.ON#>~ۋӓÓ@qwΫzfeeiyeiey%RKl:$sܫM* Է^@>{_ DBh(t462\tWޣ)yazw}0luU(0l甆~0iwa0-5~jH_ѧ.׭l0!פ[4Y&+;Q\H,5`h@_9,FzF`pI,*(<6U!Gzق]5/U\s>NFAM|r7ߚM|b<]5IϴBĤ?ms9[+AڀN *wT MIz|a >BὈR"T͘!ae+[inl*ҜO̓x̥Aq&.R̡:T 4xڼZ~M4?:gg1[ jWRN =lk$I% +}-N=mb#?D;:$̒QB@ PGbv)IFtK!*a,pdݭxo- 0$ZMRXj&a:S#ds1cJ:pZt*G}vԲDq+QsOW1. P Fuil0&Mq`{l[B(PJpq\1\Pv)b cmNXQ7j`&0dR `fX&E ԁ Dh=7&]k#&L.B%T -ZoԲ>jޘnNy܈$E7<'̊hO=iiw˔y2oCӮGCL޺D=2L7@eSw1aF6M2qqea2]eEi ޽{O׳џoǷ ~gGw~%JD\}z˭L6ʦW^&%eV7s\v5MAQW;/7_VCA P >K#XLx4f%@r[$[5xX1[-iP ɆU@cj=[-sQ6+"@b;Z[ 6xe1c2qJ9L͌w3=?nܫֽ}~ݟYQ7 e5{F}ŐEJX!d6EYMWtRYWlȑ #Hq@>NpBDZ^DLqjg\3Ոx#5\72ey mdǸDG63z\U[&1>l-`<+A+Dԗ"7mk1QY, y ; u V\@bh8SZ)*J ebTpj']9Ӗ3^QXt4FL#L@hh0HfNp[FA砯/d(Bܞ~j1SF%GbRMX.9GZAr!7G/Pb|ּ0zI C2&֛sfKGɐȓh 3G H(V$`(~ǦL4Ysl ##DoNI5=4֡OE^KL.ibQKf0I}p۽WCW}Ð3xeu㢥ͧ(ct>$R(S} ɛ{.|IPXvV KHZ'&f`(1h]aQ17eP0'e ɲYL ug[ 9W+//l\6leb7lnbREml(x9 YHwKpń# [P>dyMPJt57 WJiedJ|Wp&}I[a-+4rRL[Y3Ȭ-2$= l(ƀ"UGIL9I'W5nŐix 0Pv4"3)ͤ0^L U2PqsudRA1Oi~Pq:x`<_QʿMX[[1N ! 1xPvd_C[f[zp'"P"saw*>G Aі߫g }3Ç3L Վɘt _iG_Jk~|TLӞlOv9G}R #̓ 4VۑJ 8:<.S#R(wUP5f?E;:FeV@]=h胦qu h!*fzVavqj822%8Na7];+c79YvﮊYw;9~|Eggxٳ<{ŏ_ͫw^~wo|r^ϟ.><Nj>^€/ ;~F}o߼o>ႀ7/_*=|K|}'N<:~rxz|TOOdz8=~|jjMoVi4ַ׶F٬om6K1CX+3fxoatgiU3#cNɊJWan( ؖCYI 9a{59"t ;h)~0q]iA> Cդ,]Y"=z\L)9MW_t磎B l&#X/Wズh-9Xr;Ǔ|=5\n,ʱ{'\Tyw5V<*Z\X5̌r0-D㉝Q%b튺TÜ _,98-I2`H5zѢRbD}@/ K֤rBmRہ(3/e\P2`3b-3u_QqW.h3(jw.pVl bTiVXҧ࢔Z KRg64HN''#9H ?!^2L td] D2NG':޸hI*xUZIiàʐ/0i{Ɂ]%a&hd2fݔn+W0*4@4eEF|nȏq&Y)|ߏ5%{ݲ4y.2ގg?u% ,,NּpۭOv&;3]\ev!-$ Kz5A$VLyLlVbŒ+W >A+beY3%%JҔA rv51+m”ض!]^B< g2פ~ TC!" "]BP!#Vb+IQ/^M" Q+Rj !F$-~FOϙ3/ɩ}jU'h{(SEt J92}53VEr cМ;in&Hbpw4O6r+*1nqr|;&!8U> כ[Fvy:?[wׂNai1r9Ы4@vwjZuܫJ0xP 0ڃvU`^<MWi8߬iSSYO7(! *H7{N $d_HD@aj-gz[ۅ]V\ nS5USu*usYsrs!ň[IM3`] MhjS3R>Hie=iA\pJq za@YSLww9!t>,taι`[>k` EP'* IDg6-hڱ;ڝvnO7j.]s)ֽiG.e{nDh ͬR֎]ٔ3_b&FMcgEYڰ:"5.h'+HoGV4o[q+8_Ww-\MZF٢Մ`{. E,-EHsvaj)3tw8; вl$zk#ڐjbXDL-'i0b{ <~/xg'G?zE8y}u)ۧZ÷/?^^^\]^;?=<;y?]_~ 翢gO=m9M}[+;kN~1F=j&E#h8 t"Nd'L*&S3PoKaH5$Rά֍zPǧQ`ސbwׅ@Έ͂vè498r㡻HC. :{T6^̵6b6Pݍa()D/"F{t]`RޙlH6{jjJ[rPpBꍩq^SfbT1w'Sl:!38fumd1kznrJe`D[eث%kR0"axiH+$'c! |Z (&Ej *U\b( f),i Bbj4q\i)\n!B/hBVmI0+LG/@ۆI6U'+TEw Kx%3S0 4+ T=`VԨVvKHݕ<8.+6xmm W/2l+ʩGt^S\t"ym-.q]CT զjcQ5h-)RUBoX*4(@Yզb^/ܲ - EfkY wĕ~\\HV hP#\OR߽$U%-`_៵IÏB\`zlʦ,; bYd!@4Ա1me d!q6Rb-eTu%帅=dl)ff.,E:{.m'帲%UT4 ˑxW^${nElPd3#ͬu+kݙvlO9')vxVdm‚.D> U7jUI`Q[Oa@wV {$~~rU㟠MUxdfe׬y:3r>}m{*I NHe(\s74԰\^ŋy1>*q#Qu(5 V rJaDb#*fv0jB? Qt:"2יRx\e^:*#^Mm\g?Y_l# ,辒KAȠFĭxҴNb8/27?Gm_g2ә<9[̶t'uDm֡cZqL)5J03,6PU: kodHy =G8Ŕ-ipR$! C2 !:X8.M2$\܁!`8Jg.~>9scd/fN(uԅan$@q 臈x3BWAഎx؆$9PCx& h(Jtx,jm귑}ĻwtWݖ+nkoS_R9<~غn]\ ~O˧_;|?xu\>{o_}{߿_o|w߇7޾~^_{.}/?޽ǟ^{zxv_<뗏OrcZkbT(rj^nlTfckY(VW[(n֢i4wYflkQҌ/΍/٨9 ¿;w||De>SJNkldKX%`ϷtIV -bVpg\Y֌lZ# uyQjYJscKv},'8B9CJӌ_Rkl,wӕ0[_ќ ޅ[L% n%?hF5X}:'TCStAS4,#,|b!b!+D3+!F@ƄE-<<3blH8 1mqT.-zcYh"Â+"$c/L(]c0IBLh7 M##' ;0lhp!L0@l7bI ӢJYG8YeyEzIOmg?JfTzÜcR6,F0HAN!@t'1~8G !vDŽLjܴ.PئT2 ֮Ä8 ȡ8Fr!Cp #]?בPaR8sB}"@0IKxVACAx7s^ZJ0I*;ƺK Pcݎ0Zz}ǪUxKȣw'uEzHcL^kQ5Y39 >2=![WdȈ+8?ZT1@n91'?/ [SAqSa| sJaAa!zA-6DJ^BW!f:e~nT~ dNXY)vRTM:Z!~Ŕ[^ipYQ9y+fY^A$&^̣m' >'|aaEhwވQ#`]Y*9p\7uVp%HPw*"h1K;t\cҩTu<5_jԭù;rϽs23Ži+I42fSK5A.x4//a֌/pZcym.Ԝ7Lղ=+M:V/OEʫK{,]a;Ϧvj`cewwga>}hO/^`ӓGWgǯ߼8;:<}wG{8#;_>_~z_ }|~v|W/_v[ۛ딝-F~ws/Rf:5?;ͤfRSd#̻;Rɤ9NL5>bG@# YmUla>]Ϊ瘫:]F-KuR t [Gm:R€QubZ718 ZkFF]ZsP%A>10#8QOώlI^}@sߜ pM|N6|MɁ)3/^JP "A}3P}6d%$Ww(7 fK05ĝTB 7vQYh)MPFZ4Q"Iئa\a1b9xQT` } h#^aN Y$Das-ӒgT!+mL`o)n,[mv T!(aC~s%Cq@Ԫ#V-T-ۅ-<[0aSaeҁ#mqj fiRCA20CKXG'Aݛ`BcEFQ^6'cF&QF{<7&7b9sܸ+rr5( 9YVx,?#jicykPpG,;ӶL?|6KlO֓}tQ-dX3x$9M0s;'{ִmUk^aW:-0;W>sʥ؏\)qȬ}[1ʎB&v?hdde<mswGXw,EږmօV105X3@Cv>5ke;iEu٘{cm9?G>}?vÃo^{s ;y{pv](}:8x˓K_/w3ԷsxowkJnw!mq\[NHuST*L&2ݹ,[./.gV@P O}] V4:rج ꬺ*o/#6}": 11 캸a[GKR4/JFtя9a6` ozě:% oZK0gp)5|͓C<583rW7h2`l]6ݵ̖<0onm(Ipczn jV*[͘CAɃ} |[4իLAUjxl}e~-xn +اBV6kفQĨQ_U`h NTX\. 36J]``.F`V,9ҥ291Vv@4 aH* 19lQ$^l9Cӥ)`2C>Q.P"s%YC١H-u⊝PFj0sIt#^l/ކYE曖"Psvj uommJt/lJX"[|6l^%$[ qS2Nv]^a;T1"}WP]SMO̴f~Z,wLmmZo[o 4oqѫ?U_H l-hG}Ԣe= `*Bz# ~}(x,XB**D,D%CNeOt%R 4D`d}LT#tSUd@RC!hWw0!Jt<{T Ӽ  IaLznn5aƺQ a|lX@9CipJZj1/G\8x FY,luN Ps/95r5ӂqE;fHkՙk1mmsm) : bфiSazm%p3lˇ6c9ڌf&K|ӁhǃQQLʌa9<@;D-2$nO @ԭD^Cm$E>Z1ܼ5o [1i(m:'0,cQ睘I "q\mV!s-lG #`8+q (X~ھnjDW%1ɒHT,pb8 N %+pVDCP3F VLY KXgv`-"2Eףa@ mxdOGb!)iYAG?N߉&,v+O9?}yq볗/~z?1s=D}ヽуg\6+,WV Vȭ.g֗6W7VkD"؞Ntcx=3֝ES#]mćxvQ9nBbMt6O5+BĨ%mr4LXk!Sdyr4L;qZM; *ieǜT0n|qQgxyf:&R#­YOap'Å԰]Pofs[S\i'Pq#q^ny!_"ؖ鈸_QPHdZª&l+y{FZ*ؚnx02k`9w#32#fiʹnZVKPC)9VTU+!p Hk{|I^H@UgwX)1>HGWuiVTʇV-1`L4b %<6Z"8X{ *|{*ڄyQP7b!w@ʰ1&-<9R( jr5@a3x<)8auuݔp2 VCQ%:r͘y}ָYg\I𖅚ͨ s[ m'.?5 A*{IKQ{\h~T,l[v'6-Ui bҌDcG;I;eC*xQʂ$>-nʆN;9.Y'-Aޓgץ'̎dҼGN{YdTB;'8K/7 Vs.β81ՙDQV̰=o؝%-]S=eoO9H朮0ՍXɮTEG{GLJ'><>z{ǟ募_~o߿y~8_:>]S|>puI_]yӧ?zo봫 wkl ! $!%-ZbJP;I4M4u6uӦ~?#r紧=g=sgyfrnsyqi=~1렾eu}v9V٨WJjiw:l6;GRv *N~f6ͤ}-j)Ŭ8j9 6RrҶ[W#֕96? Cb`_Ǭd.=ùHx`!d%mo8YaXYLße}1jA7G/!}}i8VMWp#7 eعz2͸&[KZZN0UMٰ^'R_ b*3\c.ģ[(mA fY?g͠h!C B u+^8@ay2[$/cҘQ]Vr Zg JQ]4'H#׉-#kj14v".ep5,DשBu#r]^vHl܅0 稬3CgL3{f%rhI߅|YIӽ!6/ fimxZ3%\t)WhZmُӔ`>< M&5,@= .n/IqOv#'Ek00Ջ4N|t靠WK'nϙ5gڑc0zaa<JOa͌~05B[rܿQ߻w޿?Q_hJ×$Peor]Y'aۈB|x8ǕQs3z!\Dk-6j ?ANJ*tiU 50YX(>+@ Z7NY2A; .\~hķ&T讟Sp;KiSޤRyST~S$ے#!=V+JpnDM帩YF@OKEO%+Gk9?#3pcbN ([H;{7L+Bf ՒjA2:9=7ذMQt9QZגrOCKbBZDNӊd r`g4Cji|96 8('?N} 'xp#zVcgՅ Wgs""r 4bIN#tVq|q ]od9!iDw=*@UE fĴӲ^DД~M3 T{ؑ[00fEh}",d*F-/މ]v% +zi0{M;;<  ݜѾ⣕yoyhu 6zW*'KgO_[WW}}}~g}Ջ/W߽ͫ޾y͏/xW߽˿+~O?㯯/?=Y:GNNZOvP ֫fcގ7Fck+ݮ% Rx=sځL$Vr0k1br}c#~,g| F| McBxBPcH?(b%`BzňLڲAFeѾ- {_G,,=towec@{VK۪PZTC~ B]Vg9D0h !_YQ DSHxl;3I  jm=fƩmx J!Bp(\$ 30b_rVWXO14L2Gw ϖ !(?+ea2ds-{K@Rq`eZqؒp? j")٢SNN^K'B- .#I$6# :T Qhs`:\3B7;0aE%lO. jx?c=@ p5`l_t/4&*&{!bLe&!B#E ѦhaTZD߁y]!7v.%F IpB휶8x.0EwB2n&8T 8(:=U-j<==?w%氖hxOxo5R4@Q{ zq { EnZUZ%_إ?~# u7eE224*^ Du72WG/)'TDYTK0t$/t%IF-)ChLvAqWL@ǏbPE,->ܵnJP1Ru!G* @ynqP`cr^MmdYN-^r6 B9dr(# !$=8k{l03[?_=-~ڪ[]{9K}i'ZEtl"[YIDfNdHIl^kﶩ $)mWVU3X' rsgFnC٭k0?/ȱL(B8llH|d|e*LOKgo`-IEʆLjQՈ*I%bjԌ3S3E^z媡v9&~unG]ܙ!Dj},睅e< ƗG`^rm嶦^~Aly$uf06[I ] ėGWF#*Y~ϟ=}?_~/w߾}|Û~w_ͻ޽zS8yZ˥\>z({^"J#lOJ9{tV bXHs)(X*t>Lǃ ܪtlejهw>j1 C!n5Ӡ=wr cA dmEj>lzaކy5=} BV3sIa x@1r4(-// T`0HSőT'ܘIMa x3 dѰ9.MsMaD k !:LͲbR}~ p +pu2D\ޠPD8ٶ؆/R66Mbo[%D$5!dZ F+ Kej+z{RU ޴ `TZ3t9f^]l[B\{@ ̼P _tR*3)% 3 B8F:g[$=ȭ9Ω5ߎЖ=j@([5-zh(N9v؄"҃U1 CǰNA 8zN@i4b#x 'Zl3m,IC׽ߴS(hy\i}6TΔ28*b>bZ Ppӏ'[-3VHZi#?5b|Zy}υ߬S-33=: C>D|{ s섈:[SŇw(mX pKCM;~%(igIծ +-r-Q5խ jSf"sWuSRI8IDtݕ# pΩ+繨#SFVX ͗ @Qqǐr]%<.ި<§* ]"cq#x咕@=%YD}\qJ /?7JKI:fY ST.QǷu8'v-ʒmʥuʇIdM:HT.C;&rӄQ.39P(k'8/ðY:q*4 njޥ4,-S~|N16bpq1jʻ~׳vǽ!اO87ơM3A,mrRRo@%.M6ܙs񑢝8 A>7Q"SD6G#3gL0sL*&ցÜh)wN|K3'v*ynNK.@G.693^(LB) H# ځveaUSjFbI AemtҮUR M<\&I b-QO]PShI؈aKLVnt1l,~j`6" (r-)BZXUIHK´OgAä1CٜgvP잕m<N(I7r\qqsNuɬ7wi32aXaoU\kt~,}ZfAGֺ> ' 2;eѹ xka1n$hxB:#zQ Ä7uI2!- ۍ0DM>T6 boDmA:yQ'D .hirY <=Km""Vl8o"nNJmb.tDOǭ qZϥoN㯇ɗ@EEwϒY`:Fb]vSy%~o߾{?yӯo?ݷw߽w_Ǐ/G.GwNzM7vG'irjƷA۫HׯFMqz_7[ W /Cڄ_;XE)K%fZ.`R=i]`i$L]s+X"(a4E{9۰mtr}+,RY&Qu6%.BF yw'ED6G)hE |uX9.7EXhe֔0ǩRK# f+竟ED&YYz+ R-$s@Wʋ6":ą --JBh7x?4aao*4H#""<|>F #o)fˑ*8X}~+xUAEϸ˪hHY"D.%^G`5Gi.6b `e#D_ibN x6OCVIb?GN0 BYivQ"11H5ܘΑ-f 4FyOZz0;CHҔӨ `! @i@&(xr;"Or:L6;Ȏ.*MV0\Ƣ6MNm6:NxǁriT}[tԦI~VXƍvV0g ᤹TA,^n]lk*q3vyn"+9γ0X%W>Ϡz95#L"= GbN|9єS 9,+.A}就SHk2.-F*6 08W U* @JU<3O2&a[PP1_TBY8/OyAq]Z̒SS( k~THW'F@J"[Җ?W/t9N)Fa-1ʀ^G'&cAS xH))źXdYe`oNf'A)/O(cY9y FU'i.k@wp{!v(pRt|si/˺P.P z, -HϤvBGGD*yD{.ud1ٌ$$HBЂ}W'3vMٮ8S5U.0̧qujOwZ2VUx@fZy7R,c.8/NZi;n9{SQ9fE7_¹Y n$)V~sdmjhcE3s n]) O'/n6&5Į *^St'&\`TrPM;Kd/$ub-e> BcG %Aq g*ق9]|K{muC)ݴNwΏ f vX;)c^xh9-e?%%-=n7}_3uOA_A39:Ɵ.^xVu |ӝ~bQPmyꨂrcOXַjc*&8d~+ַ%Gj?c :(#ɕ!(&\R]v t1UV-1l(JT3[.TBڙ)g$2d~%PJ 2'md{WҢ;% &@*dZ3QUKx5Io[lȓ3n[=#7f0ICBJM{//Ys8i cy;ĝ"ݠl^|2C§I; kꇱ";*1LA # .V{e(am$mXAbl/hYy։Kuk (DUPY@P 0ޫos53!$pm&Izs\{n9Xp\ 7 B@;&[6sbDR4xŊarے--BSrS:~[ʇ̉qBidNə;%"dvaKbBduleluSekmڑBcC ,<0-= ]ss2Ä8ĄsduUE,v#7%/-V͞ia:s2?weΦG&+w{{syyy}}˟~͛}ϟ+?w/ׯno/Ϛk5~+2o?/TGRhY?mje"2jX-wB)%]>4F= váGpݶ n ^[wi7u6.- T#RIGsɰC.V#&>֟pFpWvi4'0[1\q&Oy)b2!#"&TV^*OA ˳c p%9ZKz EVfU> )̑ pNL4(/X' x J%lij(ѣ4LHO9mH,ǁxA01 x9Dyʙk cf`^/"2|S ^9"a0Ń 7򏺇#wnBZ(6CT- L2?p1 }r!k:T.b{Nc3&dBbx"K~r"C"e0~/urpl0- o1l݀FmFJJԻVܞ\RV}yy̺%O@8A Mjީs<:2o#!"TXGFv%SLàq'u Y Q:cX8]0VLa5{q9c@PaϫYkJwx,]2:3 u2epQ&\.oO?}T؂tP48+CYy1*r摡E2 x*̃jy/4Zu H̘Ԕ蔹 o)%@GxҤ L)A)csLj ֊yƬu b[tB{,.A&*:k&euWCe2BMa^ZbV=E^A"L$~T3"1 X͸g 3|WzN!nhH%$0+ɕd[>0`ÄBp&23bH=BKPc 7j"dUejʛ^-}w|#+XM]_ LvJ|XH$@\Yd-N_T}َ]/.,Nf˝8΋N . *2wLrH8 Հ",BfB4z5/Bg?,wUt`7ǑG~3*kbz/< /% 8#c]vIaN{`:25>7J\!]W%_î8{1]3 5}LX!"RP Ѱ94'ZIDo=P)id#C؏>/+~6M6`eƟJX5Bᢗ^ "oOoO CnV%+4ҊEIJPH[-{!.9jx*}amD-/V+u &൲+8ݶ^7{(9?-I u sRl{ʁ?ɛڹÜU^xy/?|Ǐ~ݧ?~>_}ûwo뗝e{ss}q~,ǵI{}z9u.n;gW'HD8>js @)mnnS3\3NdµN Btn7J%tZ,1{:d`x^N3YԊF56,VH2B#j1#<NK@R`[o +:M7BI&jYWŇr|m$(d#n6}bUG6^ijd5= )n̋`_,67V"3$gYJ3k\WE;v7b<D''rq (ɶ[*wm@Az]s O0b0)yي`eFPpć{N;'}n{c \K+U=r PuJ(Bbss[z} ܫT5XBP0oYۓq"vٍU+ẍxW8dz&1-o>p,xC"1 o`׸5oŴQ'I[d@DZV|ۑ_&TBbٺ[IVx<)oYĮea?Y=ܴ-3' q:{J,8Mw\PqPA}GpT@EimW\{L&5L֪lT&L&KS;$󘚤RuwwιT!: il`?Qv1[Y1X0$aG c:XO Gv =<!ׯ>O?'}?}_C>ۇKwy}uq~vrj4Z=&:?>?m/aZC^UȄfNs?5lfֽcy#< X& H98Vc|x"DG.&R PY#c?ń~?5]+3{)nXIIf4?lj\H==W˩ʚjL AnMuuͱc Ǝd+ӻπFcST7O3iU)wUmZe͂5 5܊L2Vd"8}+Ʀfo7\m=tvtsq_p TA*`q"aCaj`Dh r!杤Z60ӒN-ܐ!L 5u[ 0$ Y{i73{G f퓀X64{.WıYSvό^ݚw4g2{~ ΅mꆚ#T!mqv-z\cg<8T@\qQB%@\׿8 !_')j?W]e@8B/`i}ʦd$/ z=\/`*u@x)Q9`*aj1?ā~\}@~l]C gIZЬqP_fyqqݻ>j/4>>c,h醂SD@RȄ!fP0qXH)K#~D T'ȑ,YN#'L!%IAHH'ԡ5' U4U?Y!""p"QviE =X/M9EzIAA)b ) :4v|;!kAp /XnDM1ŕ!AV% P%'>hp;+D(vV[( b-*-<,(eGGea@ƃ9VHbUdnF $$~睞Wӱ .& klQnӢ_PKoa+f|w)[0;vV\AbaQs+7go*f[gG(o^s_U<Pӂ"Wⴳ&Zÿ%@7  Qծ!V~A|eɄWe\p/a4–F㾕K?)8N6. \}"hGe/Zhfo=+B*YɍB1z//iE6֌-}ܝTNKK]a `u%xEGH[P 2e3ƾ@sۃMU}-7٣*~S)_|y7Ss YYA#ck 2\CnY+47/Jp} )ΙAvd|?eo~߾/_}~?xq}yzqvr{uX9O[gIBDw}yZM|X/#rT]B̘ |x*}Pg2-ͨa3莟H/#@Z&0 џKʫ°BtH"@qEVs oB23MmdȒWa,pil0:4hs}R/'C]^eES7jHK9aTH##0 Ajog{~vmʭ[&]#ż\o@+ӑlRCq ()OU^"GעY28f]Fska|l?&aMAO!]SlWKA}o޼yE}-b\ұsO?yfA>g[ *nuz]qq~rO%L g~Qr]O7@(ؓPT[j0"IW 9:cg(.J?޴,_.r[} o76l؀W0&NyieZtRaUQԩ@ 4π^Hyg!mM(4 L7\x)>A/JH#y٨7 Ž(/ex,R0f%tC@ zKt4\ eCbZS '.1FlGj- ҁ5K,Qx"bwQ_+C(1!E_:8Ɋ.VGL t^>2 38pR+[q#k͹"gA#fqeηP~qsMZU? t,x|W!" @d1F٩T׹- I)zEzzEͤ|1THPI qc#nfm:5jv|ghQC`vnAWtG+U+C}~Lż\3[SUs[0&]z|vww={=Uߗ/??ۯ|/_^~y=޷ڭFh+@5*fts]n֊7Ww׮ݴWEZ_B>G/q(H=KCczfߦ ; H5^:^;  5Atbd֟̈:~G s/W|H/"6t:?yu' f?c^!JngQD`V#iCtYӊlv4Z˅H)AݼkEȄ Pq`H}`ǀKuD*@AİlLv80MJ}:ߘpĭ;MbȈ]+8ah:&N! 1\Hq|$rD8"+(ǨhhM Ge:7-,g݅$kZdxX Ġf IK4)b3Ө0Bqڵٙ#7 uEQ:贈 967ClK- i{N=)eٗ7!aת›~PohN.{C>(Ż>ZV+(׶%%Ro6НwWs3i~4?4-fW{2G'CF']K=s":*DS$ދ-#;" f!7[]EAE Qx=ύva@Q":4*@{tNJa5hh3B8h /;UH;e_(窱$XXP:|W x# sRg,S0iX&{OT&#¼!5"-v:Tq@ L@D"h?R?Kb'$U4A&q΂SI9#+hh]c":Źi@h>z"C 9qdhs9GRDODhnZζgkQAV82#k'z|\lW viS&z=a9O 3Fq߯xE$j q3Tt`Iɻթ,d(n,Y>Ȑ|r@+ijc!5\ rvyV=Y89HS=^NlNM嫸-f3T!@K~!*x`_qb{ZJv:sv;OO>ׯ?_ݶ>FvѨ*+.RݶjjV(劅sJ!E_ d?7J;i9L(bݝxgSaD1ozg9ڣR\? \voA.˜e `Ƌ^xR2;ֈ5.W{[x -Bh ZWW1aC.sޢb-U ΁/} ̈H#5!=$ AGkt8qO"9-VXe$ %ޚʲ"" x";pA "+hbviai&{:kxdLۘ[ sTN(:N2-cRSG'X_ؘP߆6Ԧچ8\38R3Yw# Mlj N,sS2Yvfo5UMӀpC"I ͱ jby"cJhj ouײSNȎC6P/x`bXVM4s_GKVDVjvQRRQ %mQYN$IyyyM5 Vf!̦} (Ns6$|4-fdb9I[>dg ~K3L%}-`ӂcҘA1$/C2k%2MF ~`L5۷o^xi9b+,YEhѥ Y>g}Co(R~AJtLBoa>`d=4tHgU!+0e DP\,mF`88L j.-$?̎JВs*%!!L YRLD4ETߤ`U<"5i&D E dDJtVb+(k$+Z')'닪D2XU*|K J[I(dEf΅H)\sB)LA&Fi ˖)LeJ<$v@RyC$?u?[sɹJodY3FI-p">(I%$H6 A_]mP~YHp/Z ǞƂN|3H_cD!bB#\wN ⧼a2Cl{=I^#pkЎRe+>[1Lxp!:Ez1vaPE_oG_a6L<2v'j [i uV۩NRƓ n ;TI*^Cq1V{m'ܘЗ}{"xwGyvRr[+豗> u^%v:{xyj]<2/ʲw]Pg|:)޼^77ק|wp7]McTCn[Hb[OtUCtЮYo^4rbVņ>SNfXyxT[8Y/jMYdA{\[bWdU`[xK{[ݝ4Ə>N!;k,o/6e+i#T@ngynowU*M9hG ]#,aNiS`s Y6.qDh AS9q! ۬\d Cw 9`RɲUs6riP6M8g a t따xC-} V~%J8A&n.мg)iCɐ ^&,ϴMEc1$Cb -GJJ̾r֠fz=bEQG%j*UW9i ёI!Xal}TEaJ 2=JU pZ~iTCVUnJ3^IDBӔc55$g96^i+Sݔ ! 12F !;#a^?Ḥ! 'h/EǑI;l# 6F?8e|~84'h7 Lh j1ᵤ ТR_ׯ_/ܳ]0e3gC)Rt9#Kma12g@QD/p5no(!%Zb yc#) J?#+,Y69F*K_g3dҗ(Yt: !Бq5" O2V1!SF%R< 2̜ܔ1QP@DA9omֶ{퉘鵻cvd؈"{YP}3xKBcر2Bpw SJѲi|TExYsּhS554x?|j((rO}:0ˊ1 oB}`&g `J{L ,D?OmC3ĒCg!lS@qG}ScY;{u qpϋ v3v̀!'! Ol  uc=>q+xKGݻw>|?ϨO\iO.OG\4oNΎ[ӣN(yƺ d6`UkOՍҚMצw֭l)b)x1C{)jA};Qkyfm/!p,8 ghil@bh1Nn߰GΊJx539'Y;b35fXsi ,bņʉ!L$3fʂpSR\r,Lk3“tmA/`(e{bبX%tTxG9ӅOB<&Ǧƨ y͐9nuy&J@q`N3l[DG#VL+qȆUFo[*FhLn I *C^8$H6j)8@`Wh%-M$VG.  Td- 7CGʑR>\_ HXrv *Z_VܔcB$`08d9S~7'L]uB/k움S4/ӎ߁zeJ>8X,·KWKW=́xNIO"^Z0Zv:Ls[vH/0'(vo]0 TQ崠 qA%o`gHIPԵY[u:bL?D-IUVr`CfMud@|d(t1a];Y%`aĒ$ϽaJ)[Qʟp Au5U@K ބ[0) K275{0"لd~Vp]ˡ(̄|y5X)4^ ꁗTQUGp1ĿQ.8H_O%ЎdZDaR;D92IN U%(Z=a#RiL=1TECv?] a s;X1=>b6 rQq0:ń PQB]*|~l9[N?HFa/.ϏO;GQ )]O&|e vb\Mk!Mؔp <0 -f-ȰjiDʪJ\{ .?r--f2ƦBxI&VȠ,/;{uup[(<z%3G5h)qYX`lׇ=\c E0-ĸL^%- @Psl=ŵwbIs@BpB8$1C51})a03~53+FN9fxSlfT*f+iy% !aSڹp7=)1@#`|jTpځhdM8;LE)srt_,ne"CtQy"8 Z~gLTqX+ `K%t@_98{9;Vm(} 4,8 `;8/ڇGvJ#x,c0@}mUϹ${'2Azg؏FAІ'!)bh6:$?ZH@ߠԼuZ A/UҠR>kYuY?oռòwpU@ 5@0V#' gK-aHJ[ǜ2.$,5=_uH(y@Anl`0|g[`w ˾E7#a nnZd3@~Z tzK~ߛ&2dQâWNF %|Ώo@n(ωe#OwE쭵R{k'93rG`qfң6Y<=Hsd{f)$G R^o}|ӧ?Ǐ߽_rq>xIj5JjS'*<ԚJUk7J)\: ža3g3{؆L`>)bG BDa[-vL| {([Z,x~)JxHyk$ }I7FTYv2-*I})A rjƗ[ĨTW|.!@=&0Ic-N۫DBP29/RBl \B\ 5adˆwj;EL||"Y c]-I2uD8Eїh34 >U @~ZՙapK3 E̖=Y٦vH*io;| ң}aE2iAx1XL@jG+MMѨe A)ei bq?xӢ)*-bDRDqǴgTB4HA5{:gs&ny/l DF$t64e 0li0'J!MΉ|":7\'G1}F'2ĨLRG+wt#pq QܨGQ3!TsǠKwWd((RܺQ !}b a̩qJZzmK6,vINЎՑJ}d?_|ౠQ8ܫ<\Bk#!7 (QsD:ZK?85@FL#G6&S L؏`*a2 =eB cю͘g'*/ݨ̓H{O%vI 6u4dVȓ015WG$"TCI* Ȑ}[˯bBAQdr,tNDP١u\iIM"όd1`RTf?g`.'0#m IdCl2+4O~X5ҒR"[B",?Om/Or;Tz*:fuCĬ6t3#X BԡLH/^ւɵFyݷÓiuZR@7 򶳢 pLp=\T-eM!y#3lL rumeO Nȧd循z0ݐ yA켩{ʎ35n+m,(58r>-AMU2+Ck|N.bNX;pe 6M3pJNւ/[Z`Xd9/כ fNE+|Qn2_6/H^u@/K!'_CoW -]V.|َ\2)wqH>({-c՗\U>fbiy}{Vn:aYK)8wm̠fVRN:{ յSޡpYJyka\gҮivx|j�w_z=x¼ǭVwV(Bҕ$" <t= S#@XN1|j)0a~,KwB1RTw\l @tFhN92VP7|-5\~i@lZJB{BB;bln08< '}+#2*N-qe$ze7O:E7 AD\ 0Tgsm.U=^$,նP͑>$y5Uf%@ zv& [c91[BEH=PjބCVӳ8y縈R@9qݬ@>P`Rx)sit\' ;mhJƖg%=X-V ڍRQT^.)m.( & gR2'e'fq8CCw$Ί0/3[K xF@,r"1':r 9Ar`,o@&FК XQ֖'lȦ8*#%f[.ō{9tpEIOQ^.,R<1pRkSQ.ctn4Ė0"ig!EĀM8nDN$|bMh0{pۢhoѸwOiFW\>lX@H{G3$2,K%8q`E鵱wD)m+ca.6]Pdz@(PDn; QA\OkJL=T#P05evjR:Ӝ$Ee"|tGx@ajpbg\%8f(fہodȞb@'L31W#Oz9D|W z%7zD$=zsqdMKK"qiOfv["@ \0s[cQԶna}ϕ'v=ޚcSWjbx M(E~+dz].+h&/ +McM^K.Ip ֺ=ֆU?|ߍ_Uy:ٵ8c->|}W5qm3$Vi x SswX0sM&^2&&<{hnk'ajLVшAZiMLc+3kG V䥟"" P*uB.2 3f֢n )N'vE\-(,p8t0uU&ʰNGes8FP_+?_v6w x&>R{^ 9m^ #H*/{0X{E[O=O: v̯ TȪ:.֍%D 4HnW!QKpg{lEy,DjւڅUTvYD5a\&zveĄ^LXvK"eZN~Z9a+L0hr<&]%=Ӄʈ+b t JwADΝԃQ%#E+ 2V9o<kmw$, 14HfY>Oz}'!pN`OB>`ĥsD*`BJ*E; xd`r\2ԩ ZJ0ˇ%Ƽ0laUٶ5;̆"@XȊ6Ed(8t v2\I;j x柔! i[Njә щ-@XxuZp`zc~$;O$ק'>i1:I',og?oc<$<#x3^.Џ6q8&>K ^c*cQfJ)1( '"rK|(za*"2%H++wDI5o(I!%.&] .N(>TfbRYK>8Q&wc3Ȑ*ajI7&`)g%@z4 iG1 ˒\|%Klgȱ>v+aAM]Ɛdd`gx6 cQesPAbFLmhW QGfcck 9LrfgxE?[Xndfŕl.;w ,ojAJfg%v5bWz&a(P[}h=l]D8 KzD6Z;QG{߆|ZDAs'҉Tps=6f]- s &A &B#H!r hE -dzIf#LHt*d-RR7owu$؆!SI]HaK`ɯ4U"vݿi4,mFADQQ!\DEP\(xE Ohԓ<7k翋YkfͬՋ]wծ}@ YPwߢc;vߎ|nk iT;z= v M5c^\o@S7IIƮs/K^\>q S|< ⷍ;M1SG(8R/(Yc^Ay$iƤ66XKORP2 鱟*LE [//,~دSZb`QA;' H(CÀ}mÄ?K 2lm-k-xMJI_߾}}y~|~^ߩ_]4_qo_^]Aʶ;N[dj6S,./(m;μ8Mv8}TױkZWECqW^7 ˍ8;jţBoDUZn'jTvu4`".5YdjX:3R8d-jEYirXdˑ9帬j ƀ,J 2r(=%%B+RE# n&w#/OJ)9s<ƽq &vcG0U!MPp/D&'C>1 e}SSYara'<'~#*梢Bͩڇ*0Đ Ͳ?E6Q93>fJah'#JA-[rYЙQl&0'zЅNnY-+OCsB.epf[6@Gc O0&4X g&.T|@w:3)̖ ćnV;MK5BK^48NoN+!=JT)>%ePNքU7~?lws>O·\_cq/uo| uvvv~6&xx,M?fNB/ט )pX% (F)Fiah>vfRo2&6%Ʒ7tYdFzuMyiJ+eʔR*36҆ңWyf*iVhk4-n E"H+~68ͫ+gx.;4r^(1jڏ͒c4uj"k/"g(.djlVAo֫N-q$ H4ڷ<_ %rzXAJ<J5gf(F֦(E$xh-Ekݒ$(75-prRK[4]AW׵Ay#dڀ1(#_9y|RÇ 6i]KTf\Xu%G>K,n GqcXǚ,zjz}7]C$,h9CJIAi-Pk%GЅ VG|> H챭50OVtQôߝK9+o~Yqh{H}AY=ŭ~esv&Ay2c'q݋cl'cQ62N>6Hy 7}8A {B8O^쮾?*VхU_YVI /72n#emw͝j)7d}/O>zꟿ7׺wq#BXj a,BڶO`Pm̋['V흵VW[oyoYY;k'{k v&svWzeFXaYϊZg#kŁviƚ\3gf1q=~`fYYz[ߖF+q^W1ש%\gq,p'[!v J77 #7ZZ,͸AtLHkd"(0 b0-hf8a BI,dm+U0\Ks># .4U NC "1#b܋S aE 7٢ M2)!~^A8M/qX8u葔אp!(OMEU'm^d5E|&h!>Obg!= +b9j.sYتbB<6 htObت^Mai(4ZlK{t8a\7EMɤ#|h|4uN6N08˜>XZ;'eq3*?#]:((CSX!Ә<ƠRV'Xs6-M|CtMAMfN/e8 `JEHDUґ)ps$9hBqYN͖I & NՕT_Π\V 6Գ|EYsE(kb״亞2VF\.jo(KTYZT?5rUę'Uҝq_γ$tpmGnIMlUB_ƳxAqϔvy )iq`dES_?~eJ#)AD qGvXZ "񙏼sPsZA K(r%y3f3>)ucn5s'DxuAhP'T⺤#n&?# 9$̻3#z6{D ΁Fwkϻ=#sqOʧO(8up5ԧ`0N]}~c)̈́ۿ~Rp_p΅1I.mcq; 1~" ĊFb6(5 F&|^g94eFR܈܌.H?aeab9#?# JRQ:ɸ?Q)1 aWΔ=9x $ыxёHURiœ,^TfR3@npmH8xHӠX< BY)+lKe,fUK Fbrzh]HO!sg2Z 2XQ\|XV !")$W|{[{ tTD℅Rf= 89)!L"/$3o9o+o5< Z^4̤BG$spsʿtu/M⽛3ᇋ Iiw5OC\AF1qC T!OFĠ8~1X@a1FFUG Aa '&MS;Gezh2Sxl!f[>L8p 4|&HLb]ԄGq#"b*ZA&$_Ł/3P_*,$_*ɃTbf^=ܾ|`NRxMXYFjޮ* J^ҝ~ L"y]v$3 x5(EǶD}i>4gV21{ pݜ`!)I9`\s}SyWWG}Lf0A XL[xFW[ienPqSEeU ZX.~ Ʒf֦ Ya0u4tِZ ꊅZfv.2 ̸E[ebx&lV ܨ{pexēm@sF|@&ZJovD{ P&ċf Fz[@cc <7XB p2Uy(B/b-@-BN^ I-6vFB:Ll!+S5amw[8yl eU8`[l5C8ZÕr4,m( [M( (!F5q9*y9ja6^kniXo$@ e ,+b|BjUƊfyAB.ee?r҆l!3_JRYҦY+sN-RJcxob3i2!$I11]Pf2dC͐m sƧʺKup1IW*ƅj Ptj2n|-@Lf&PEgɎZ˭o JeU~'`fct>#w u,(qO;*[st!ě~;{ A {:I ؈IxF5Ϙ?cͦl?I rN'8En˅k ]cleIJEܛQEqU`/% S_T:[~YgX[}XRBwo8㖿,Ո`W`K[,SN XbD# É;˙(D Eb$'Iª^8L|B Mr%6-3Ui7UTa )BqT6xf)HbhNNd": }1ot[TͰ-a-P e_HE_$?UK!-j '@>LaBN2SHi$q9K|%5*"=IB4&SXJ:Q;UԦā=e ;zfG?괁sx-6r8Ry^컁a ֯#1]u48 v+gŧ8 >iWA^0m\ףZaeB$"~kȫ*Ayxڛ†աrtBLJt A%$+:ƷIm<< vRt^s=vY oc3!<]>E@]#Q(cK=|!S舭 1rO ]V%o{-\3]MV}kX~86gZmDzx7|w;]Ey_w|/񢝭z~EYh%E2K ^{/Bty=ĥ0=0DHE~r2>ZX]_c z{^_~>+zQk{s6_V5 T0sZ|kW{e?><ͳU8#Q6j 9kkrfA#-|KHO"\ax2o2sG9 5W DԚU:bߜ!+r"=S$E"F+'YHЪh<$e L*>ku|DV"4 OyX EMD$*#7KLƱXbsB?wbSu.L1 JP.ҌG} Rq\ OƧi[.HagC¨sQ7SMs>\`*<˅{ǒѴo&s9{oK&)'1kM:o3vX2!c(Sa a6,+ {*=])u G⫣|HOfc2.0Y4^Cn,DVF/6-V,˙Bf,1<pBqp('lc^Hd ֡{E&YS%[ b21Eqa;0˨~BG60NZ46 HŒRS%?K"xd@En%?b_Yi I[> h F$~[I2KiG鮤]EA{\bQGYd')vVe) U(N%⺚9 Jm&ճBzu;HD>NԪNWM29[OTI~^)> Y|z'Jf~St38^g$ϗzim^+|%E%rx/𒒙F~饗=QwG/~I}h&`՟*ZStP ķUX/oPDBzW\Wa6aR95GwLޜ$'QDR - i$0褅@cBQoZ:koH#+@& "!uo 6UnŽVc•G+%,b̐}SgCKS=m6෎Ť ^.?8il 1ii/!"L޷4 p&]}qcM P 3h e_c<Ц[t Tx]j&iyCI3ga-A|2BModR߹iJ?+~SNy5"B&6^?N;IV /H}XMqu:0q MgoTMy78 `V9.kfO"`fkCX8z1Ov=LA-,) Ύ9lrwY߽}f m,93blvtQl  Wl\]i7* uZbȩѱ.=F8Yϙ#Koc0%r"ە%)>:bo"J4gu_WgvBJ(ȞbUG3#A#TMtUUe"*'yɕ. -,-,1[p!,{!J=Bb\`)*o0$21ÔӀSSv5+d6bȼJ.g]F*C,eCʾ\Z?s$YCx*#b%>U[6*F:WY*Be'T &8i ؖ۩*Q/U'#$b2E&XvńG)+f9(y d!TR8IHM"E IInR-5ކ*}1ͬ֊<7[39NG hpqh197_owX)|ߏ8J9KRU3$1`8jW [vm/` f3xLfaR=2$uӅW8g8R|x>}|#hW "4b|{~<$>dXC, M=44q!}*z( #%)b~㨉N ?OxMRJ|?Ia]e$'0ha6TРB{f'<58%+59.-rA4o^ ImqPa9b05͠MW9L)x6IMw09c. AUz/@'> Ix%eS|=\+K1ɚt4Gn|wŤbiROpx?:1z1|uȩ}CS E86yӏ=fG~a&|~"i}gք+;9O'宛vMỪ6/kIT4v +Y+ ]O<uY u+$wyG@hd~usw`f?ls( Q jݴY4«m'ý <9kl^n:ctDa-6Uf`*~ ܮ!"jC}$FE6ip2`K͍[Ǡ:76Mvq^6{"CY; J?-n8<ɯ(z[}c | b%0#T@a| q+"fqyu+ѹ8$'7zFr h3ÄM%q"U<[LJeJ=.lplkBM)5ȄbK0@5dzPEF0B6QcҋB؞Kj oIW1\.z#nhLq͡:bQfEnߢmG1b*&B6XX9$gA9oI^A+#fCv'odÁ:0oAk>,1(>=i.q'Da잘P!!@D@hZ8L^$ J}nǏ?[Tαg)0Ī%|$l L*I}ZMUpN$ۤ3qu]e1x3uT$>0`m~R1 EM|e84}F$qQD<ڙ:?6UX"/ap<_bE0is?qOc)StBt9d }=aZ>_1ѣj@2亴X=øTMAx;6NuT\@p(lHmBa _k$=d !XQ8,\Nvo>^;ALTaV✒[@B ?y>w^~>yN q]v0(7s/IoSsqwU!Nf*YnN뢝ްd*OqY)k29U팹ba-G[d{}!+Wl4zI+oOMC3{Ug~h%a!]م$%{?J( 9fU^VuTudu-Yo7uh4L~NJoNy(1=DR∅2eomt˨S$R["1P+ht6qKWD|J!e f̊|Ӫ囘u%6Ţ0BRwL)=Ҙ4QIrv&"-(E`A>T10hS9 Yhu T³<3J³!:!E@L%+L|fF! @rACnbKBA;QQ dYP+tA43#+4C1"ْ S"䓖B4b6S|î@;@wاȓ"P[} Rpweu[-;s0G 3!vOZ~nh(9,{y6Mv%7ȭw٩qT5fiaU~d{"쉲#~ 0f{E9?U]iM\5Cnh﷭a'SiU͡ 擐2' ( Kǯ'0 7 AǞ0- 0!r `_NMA?)h$$u\iUVd._#LZTdvvO@Km'AQ@ Է40ͽ,܉>A(}r(,Қҋw4NuG0؏HL!qDbj4ChQ=fVITRdHb&HP#x qE}PMN4ْ@YȟS}MdE̔ĎA4f˓aS ;ǃ<-{yr8ܺ2)@ rVDqRO5qފAѸ˂2zP Dw k48鷬yϋ76;ۼCᗀU/njW3h9H7"7x%=װ7%g'hL̢kL̋s*PLuoq_\ЎAYh_!8U⼓Nf^EW-ꩵN bfY+Ѩb=a_\U(oA'm츚.G\W.&V%u0Z )XnI!Px @ aO-h*K5DRdv*7&Ż8TB rMջ61IȬ1 BX3OLvDkՖ(t#pIҒ xU(h]9C_Y?%Z`Yʞۄwp.'99K>_*ĕ ZʓbŠ$\ 3% E^,8`QhfE+DÖF3a1%qL£8焋0OZzbŇ43o` ބSDمg펤dhIOeB ɑ*4E=kHtA}sF$gfWPߏ?~Q_zk:RNTɗp ^HN8MjK C򡡼xLV1{*Eʖ2L NM8f+Rksx3vi KAn{6H)IzW~>&-JqN\03S ; ~Hq1 w{SI&1j5@mt)(6`:{%򂪝ǐ/ b6T`) BA᫙"O)CV@@FރVz5KH#D m&B_ˑ2kTqc#n&{ͬB)ڷv}ђٯq%uUY\;x)p}lAN4Ò(^7Ppq)W$W#0n_<8S];?8ַ:฾=:sx"m\VlT@`1hw \Tjs(B?B NvgaOW'%dWl8I!:B#״V(AY1euu½0}q|:Nܴt V-0i=H"e90߈Iςdq˄C1d]OlNtl:ӝE`jfp0jGQ8fȦҾ&p1:XJ W򘁮tZv|=Rȇ WMjvuWJv`C^~#C+?7rvmp!2a]S)06Kݹ''gg"Q17:=` / e`[^ AˆQ_`,ׅpaQ-A5x}-΂>i\ۘyx79s%YxoՂmb,ЫZCr\ ,E$2a 0\ Wl+2X0#E+& }!D,GZӗ}ZlţjKm,I\5іgl"BBaMǏlhC68G%ѤBV~4g5ַK 6ILѦ"OŚ!e̍CG4{( f҅x }q6n].rHyIl;qCM<ٽ{"[)O1)$󼀸:m+_mf ZK\mA̓cYղVY_N6_Nw"ۣQqc8r3 Iq.fX?+dgJ)3^Uap4j3F{3eՌRLQo-dhD)#Ws \/(o .fIQzݩNs2s>.sfa^`' .*ӄ]o@ܘ8Yfh`y}3Mh7 A 4,:kS @NS :>W μjFob|و Oõ]V\<$R?UGejWpc-׽?Z ><ŝBE-~,HHGpWDV "2#D ]RJP=\Z {@G# G4|̂RRNT4rQȼ4y7qTSl GP gqۋ2^~b=_aGZ='#FǚqR]ASGL9>o m ,$@&"b$;_%ɟNs\7VBڲUI`RYA)IHy6 !E &Y>Ļtr #d(.qQPFPJb/Eصm&-D>ݩ8|S67c<(C#& ׫&k)o2sV?,M]o[l;7j1u݊_4V?oDyG] x]b x]ݘES/\Y zv%nSuY$Hkc'^i%0w۽7)% 3۱SY/q8ss'($hƨ ?V ܷChLQ->ǥc Jay˼.<"` }{TìferP'LB}FfԘ@󤞨XCO,kTE@6k(ѱt.<f_8 LYUl&s'ڙ(%qEvk rZxm}2$-ۄQ-!wr/F};3']ͬj\)֦`';ME-&riuWܓ$?A玾J? K'iwV Q{ɃiӦ44m+.TvU@AQD%$.1'S3*ozәSuf(<9,ץqs; sjȓiKIQ'E3prwᏇa+ u^2WHo7=W WMyh;: ސaϋF[%\vuşu-?b۞2wB(דw[&2x>T0cƉJ(F2FU`aUE`rZu0YHkXYg.!a~Ir]Ǫ1ZSʑ`{ gͰw;tMEVF\'6tmXaX+Q"'K@V%.H?ƃzR/#UF]D9!t;zpe'8$p5.&YvV^ VcQg Ɲ{IvRFB L@rKignrUrH`"Dl21NWf ,G;! oeqr c[1m$yzXٶߢz=fVxU:2~Kjc:/wy3ÀlB?J9B lx& =q Kfx'afkh꫘g+Pߏ?~?Q_d/]X q 뿛M2NNp.ꮉ@.06}mN"(WPPBZ@Nc3yS6JU=2z#b)f ``c@5jƬ5uQ%JQJU"pсDA[Bζ 4S 5CR]:wg`-jt"ԖNft p).]Y3ԜFYY"Sq{ra΂Zx3u䯮HR6j7qa& ta7뒇Нu;)$}95?.7F;8z`Sv\lF1l9PzA =c4HnFPظloAvgvQm#hS^.s_/U፫V*PJTp $(}P!z?3%G^ ei^ Gl{Q Ƃ3zu;im~9~fyTc?u3> zG{ ]VܔFOڇlKaϺȔCa KX{4xԥV% ;0۹/ce c gLB#k -Yx8]|Lx"'+<yҀ5U~|gMgbޜ/Wm'þ>)4yHB7G]ű׀Tr*Noona)&Pݴ67@is?zpecS oM|;O00AnY~$=hq]"eز < Vpfe[gGJ-;jJs/bκIBDq+w5EAYYQTKKKkTZO2i㜞9sN<yƍȄe/AZP~:Ĵ~+|v}\A`ڷaf' $WRu'E˘)j&QIF xϬXC {7]v,tz~@_ئ}9˰-ϲV?I@E!DZ>ʜ)EiGaK{9Mq @ZVg50@aYI.Ԑnml{@p\\ާ)ӐA=;1jnDzpJrS %OBM!m'naOX4Bq6Td^x7V=\1.~4.ў꼨};[K\5Ϋn(GZVw P;CVL Y&]V]_uVv\oe8}?{~!CJ1u3 렎E/~8=8{2֍$ʱJkUԽɯ JbC[s3 8̯6r+:j;"AzeOa[[v)7y \୙Y@<ރk1( l܎wV"i@BiIJ=8y9o7Dz8tUi$,Em;FZX x=b$icksZtr N{*L9c -Wҹ%WfQ+BYq@QUsgksQ"O32Z0o%!vX,G )) kDl Nb"NƢdXwv?މH~&-D, !fS)DmN蜦eFfp ݁st ՒαcQ@.cO|(B{29nI+E X\ AG Me6WfV#&~GtE%vo#n}%1!kM}nw4u} ѝ&Z%ʯ84%700|I bտqi"QPs?,T־6 N~171<g!ji}G)wʁ_CY Q$cEhV`jڭ2},çA6Vo$14j{Fjj(#NQ|Rq4(DgQX9 x2'5U[ʨb` +bϲ=֨X&922ྊRz _7}{w5/|@ ˿hEE=5%S3ʍċ/9d?z}DVv,ή}ur+l.V'&c}<`7^'Ҝ'ACld;H `FQaW}:(c?_+n>6\)\aR?) ΈKOB;#=`}ۆ:L!"!;%]ks:A30a*T^貌%2b*WPGb%pAkVLZpA ꡺koBԼ;`QJ6MnRس_.Xc7o"nql:.i)B,,5I796<}p7 ϓ2[B>=45b}k[ rXu *ھ2K7.ܽ*.^,as伬l(g%'6keVYɭ!G԰Y+8hd$W>)lWp^_Գ9d`ָN ٞo&N@=3ɝIĈܢnA懰s<*]Q B7qZ1gJyS9\oKĶv9?1{wk|cM ~OAOl-ev>fwd0K">pvp]Rb.]@Y3f@:Bj|جq="$Hj1q>ŝ[=MU5 8P0z%I?ȗ&zO#-2!dY6n&U0,eNpEyO4Ej!V%lY c%k-5͂Jn1`L-vjFt02ێvFFˇƮӌ#@ }(h:5dj P"QF|HO2KدWV#axz]~㥌z/8Nqu@6EEd ;/<;C%ظ뾛Fk4+}h^.Dmev8q7x##؋aՏ^ l40  =mDVNe ʼg1? KԃW9B6hr~^UөEjq)vuy_L˿eF"?O'^Y45)ol#<ّhxM ^HF}bplV 4U}"h!l&:`D0 A_C  b,C/4oS7l~?E }%IXjP)lG8*svW_ſvES"M䫹L^BʑܞALpdhL&eh`I4-)KēI4H`yL q+/֫K)YԂ=+0<=yA@x禱9Vq2-= WiaC<'v͈h-:+8QR݂R6J_nvέ4" ULy^q;~ *c}gPq۝D4 jC,+yEU}y偫_rW =>Cffx 6x{CA{6}lYzr M7MYNb<[?heW)+0 0\<( yʡYGv@HjX `dٰ0LXO-ʮK51a?CnM- [VvM|$!j*\ʒKTHgYJNcY(DWY4"0FQbV5Dga^Jm0(Ba/B-RE/ 5f2 oQFu|_69iysZ+<^ 6 zle&D7oCG4xW\+zHR\l9c4_eZ]9; U)K+Khfn2EraH:hfT˨{w.FTsB} `ʀt_JSdUƎ<K\(]x"Sƫ,-aBOY,a&Łтj :?Vosssׂg΢{bP(q'.?!P)Ő[ppےyȁ7~Y2bJy98TT΢$ xe_("7qk:<2 0 h` s‰ =ruÆJ?to&c0 XXjD b9[~v"(Q+.aGX1B,Tn͞a3M*\|r9=4NX^3B Xo%=f[]D}#=Ep.-G}TJf#<[jMD#i.s'kPcDXj33X0Kt}yM<%sGڊ֩ pN90#Z8•1рܵ`ρsL9E>n8'e .E\"nN>[귡c1A88qxXˑΜLbٹK s'; T򝊔 i(6 D!私qٰ/Bɟ#Ap1SnUz oL"/C ןlWaߑ(\=5S]U]UxrV8j 7ӓ}{}<9qo.g]s9~rrnX Em޽2Uȗ+KC.&tǢtr~ߝ4NߞPc~w<:f0ɍ'dqr 5{ ܼ޼.aL.Q23x3Fb9$je ~F``,6\Գ;ms4YMUi)psnӸt]߻Ag7wFy\k_ Ll-6h::kN5j39ődzi)>3pn˻  U-,?e8rmP$ Yw* epʾڬ&ɼ-q'3 㱹í`[Y c1CϘ[PYW`k¤ʼ䢥byeM4MӴ?CP}_viivBn@` 4M4MӾ>U_$Wii}c}Ʌoccˮ_4M4MNo5UiiWO4M4T]ii[ VO?t7nuUiiWrTii}!`0F/]=z4M4MӴP(400fff9riẅ́677>}իWϞ={۷^zѣ0rdd0Zݚii=<F?~̙˗/ߺu/_\__Gw[ݻw>sεk.\G">իii}F0 ᱡѩ'N={}'O@}kkk_~JwA#{ ν~ }yfeeӧ߿u/$KKK~[_-aXDXہFEFl&233- qwfPY(<}U7{1CCCCcccSSS ;;;T*j5\Vū/ ;cYޛ\4 cooouuuvv6H/~a###=Uo}}=L^\\ zj'sz=U_ڶRIWVVgffԓP"P|abh4t:oV}eYj4L&svvƆoiiII9ZckkkGGG\Rضy^UݽY}>ea}^/|^ݨӵd2TmmmmBJ2a777bQj|Wש^T_~v4( RSqrS읞*Ϯ|ZMyrN]ϐR϶jtlVdҡ+K:F=*Sr94Kϲ,[9uu~><)>aaaaaaaaaw˳(Nl6|N>xmVL K͐LT>/Font<>/ProcSet[/PDF/Text]/ExtGState<>>>/Type/Page>> endobj 2 0 obj[3 0 R 4 0 R] endobj 3 0 obj<>/A 6 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 4 0 obj<>/A 7 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 5 0 obj<>stream HWs_j&]m3o0B$N5r}J6=0}eH\h]ַߺ?hV~ׂx疰ڴmKQ+\z|/㊿@m?A*T\E n_XH_=iOz< ضۈ7e<'(9UQj;<,;XzfY>c_pbQ s; _΋rS|}) p7"f9 v͡1<[-4cml{,5G>5'lfVae~N,fCiYeh%+S"^6z&**f 7|5̀h^S]}ӔG̔3p4 P{0'$[.?bknvAA69fxGz?u/sis$ӡB"OIoF57@r&! kǔP9CPFqKnU<^oGY3>iҕg @uTE[|ƁG_YnCyע:o\A \6~y$3e^,Wu䛅(f4A-um,yStpSG\pH&7ҍC3 Aw맟0M ^tHRhHVė^AY_lRwDhyA ~(ƻG㌭UEj@IySF3 ŊB*GU99S@(S:~G(E5A)v. Z,%ꚺ-g?5 !|Ռ&݀* `8\+ךR*8C9=*Z0bt 'au>muI,bw Dͅ!x Nk㈆#XI{S[ &i!ųb 18& 5xϻ ۊpn3l!'؏BƎvU=cvJ<Vw[Ӧl`ScSjIQ4 سx~nqNUQOxȦ6ДFlY"'7L/s`W9T=KiCRAnH >韦;RyA}|> 04%:8w1(` g`Ta8;Yz1Gj[cvCof|S "do>g_Q˷=w-h;o:X_g Dͱ(gh ^]Rx$7JqؗNuy?62 ׽Udaz8^yp?4"\(#M(Z:AzeE,{kP3ƭ,P<I4B sT_/!նG,ɀr>i`q-CnUU}d]>ف;#O*o>Ĥ؂v5;ÞFEt-Or-]fCx:R(90/1BqCpnsl)K$r-A'O "&5hhݧ$32x@"x`ttRH*<mb@9~K`pHEj \jK) T@S6Q= kTИx>'S& ˀ;VXa _rh BiX^2qX75+ea1Iv-?>HdݕLI=!@'O:T1<aA0(?AJ\IȎw]ޱe_(^oʥ@AA=FFxNoE>y'e|*nY6d%=d|1iNEt] -Vʫ1v# 7EI_g$K*3Ĺ0/"vyyl#'X\F=]5ԚE>X?kv)9y6ѪA6~hj&]m s:T#Wa'+wý2Pj> :ё&x1K7XH8Ix<<5t'Y6m`2&7ITX0ZJC.5rH\CUR^}.g~2Рк =mG~+e aջ|qb@GЗn)psz';<4khlz4E Qs OJ6##P%l-{@z4DT=*!sQ˙h}^1|ȆKr#Wv/_iNw~EROI/=Ag:\#ߕML ]oGj2 9wsW_ϨMq[1%rNMTO??{2'sClFh]SCÒz̉K[sKP(!i̬%$9UXE%T8(il7;8wBtS KfVKUG7u;,l-1닗!j >J:#ZBUq_)g3DP:EIIj!ȏdp{hI2v~HK#ea2À7C VP$s~lU-hrjTf`:11Rs@L:܈:S|jr:006?} +m(jKaQ5 *Hc\)uxww>f1^*n.דl," AQN#'~ym4M׻6 bxIS[{>ll R1v=!lx6D- endstream endobj 6 0 obj<> endobj 7 0 obj<> endobj 8 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>>/Type/Page>> endobj 9 0 obj[10 0 R 11 0 R 12 0 R 13 0 R 14 0 R 15 0 R 16 0 R 17 0 R 18 0 R 19 0 R 20 0 R 21 0 R 22 0 R 23 0 R 24 0 R 25 0 R 26 0 R 27 0 R 28 0 R 29 0 R 30 0 R 31 0 R 32 0 R 33 0 R 34 0 R 35 0 R 36 0 R 37 0 R 38 0 R 39 0 R 40 0 R 41 0 R 42 0 R 43 0 R 44 0 R 45 0 R 46 0 R 47 0 R 48 0 R 49 0 R 50 0 R 51 0 R 52 0 R 53 0 R 54 0 R 55 0 R 56 0 R 57 0 R 58 0 R 59 0 R 60 0 R 61 0 R 62 0 R 63 0 R 64 0 R 65 0 R] endobj 10 0 obj<>/A 68 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 11 0 obj<>/A 69 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 12 0 obj<>/A 70 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 13 0 obj<>/A 71 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 14 0 obj<>/A 72 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 15 0 obj<>/A 73 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 16 0 obj<>/A 74 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 17 0 obj<>/A 75 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 18 0 obj<>/A 76 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 19 0 obj<>/A 77 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 20 0 obj<>/A 78 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 21 0 obj<>/A 79 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 22 0 obj<>/A 80 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 23 0 obj<>/A 81 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 24 0 obj<>/A 82 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 25 0 obj<>/A 83 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 26 0 obj<>/A 84 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 27 0 obj<>/A 85 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 28 0 obj<>/A 86 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 29 0 obj<>/A 87 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 30 0 obj<>/A 88 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 31 0 obj<>/A 89 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 32 0 obj<>/A 90 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 33 0 obj<>/A 91 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 34 0 obj<>/A 92 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 35 0 obj<>/A 93 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 36 0 obj<>/A 94 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 37 0 obj<>/A 95 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 38 0 obj<>/A 96 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 39 0 obj<>/A 97 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 40 0 obj<>/A 98 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 41 0 obj<>/A 99 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 42 0 obj<>/A 100 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 43 0 obj<>/A 101 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 44 0 obj<>/A 102 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 45 0 obj<>/A 103 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 46 0 obj<>/A 104 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 47 0 obj<>/A 105 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 48 0 obj<>/A 106 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 49 0 obj<>/A 107 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 50 0 obj<>/A 108 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 51 0 obj<>/A 109 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 52 0 obj<>/A 110 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 53 0 obj<>/A 111 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 54 0 obj<>/A 112 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 55 0 obj<>/A 113 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 56 0 obj<>/A 114 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 57 0 obj<>/A 115 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 58 0 obj<>/A 116 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 59 0 obj<>/A 117 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 60 0 obj<>/A 118 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 61 0 obj<>/A 119 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 62 0 obj<>/A 120 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 63 0 obj<>/A 121 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 64 0 obj<>/A 122 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 65 0 obj<>/A 123 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 66 0 obj<>stream HWێ6}7У`wR@"n@TaSZq [N~}+%&E`cq4<mfzg0-=[⿡ap婯/ ??=/~ꅟf?CL/ٳ0,yb~ g8 D,OŠʈ#q^A&1e !;}ŀy N{0CwBWB_\.~w_y$ _QP\3\&y =*te8=|zh 3r]h^y|.y>JTp &J3,nV1~Ł@7ԧ#0z;rǷ>|!>r44`iDp~hߋukE#bN\,I챠 <?xcN;+x@U:3!,PF !-,#Jq{QJ=o`Ԇ%r ui6O(qW\dw?P8 =Q183Lp/ 2"f4byT9:ցﭩ!@ |ta8@I߼̄*BY:3ĉAG9#z'4yJ4s@u'<aU6\"B椽vk֜Y.!,yz*81IUKG58 d4q-FԲx\njJs\ζέd<(&߯Z;W$DJ1-j%ĶY7/EŬBMN*yڔvH[־]6>W=ifit'Tu77F\#t"'Dy\,T"!༖07sQej}PQ[\H,.I$;F%՚*6jOԣ4hO+tZ6nQ͘@A]0a@48(QR0AhMSXеJ^k{ı%r̈sLV$ #D ⾝Ȥ3I 7h٫}'E:c[͕qi!h+l~Ak 9L926[?T%mgݡW j&H$& l䡆vNIVpa9^u #90.Q#]Sl2U RɆg ,}i`1 Uj}=f _*8աFD3ytAPL's<>7mhSR&kMo( c lX @d&[uz˴vWujpHT ClB8DdIvG :hnTsuؤ HhwRPoP@b*RG,42/L'M"&1H0Wv϶/F "6 b(lnAODՆ_7GVLTP% fd[U`>.8'vs*krV}tzP>U?AJ J-4 SWavUu*,:l&`VRY endstream endobj 67 0 obj<> endobj 68 0 obj<> endobj 69 0 obj<> endobj 70 0 obj<> endobj 71 0 obj<> endobj 72 0 obj<> endobj 73 0 obj<> endobj 74 0 obj<> endobj 75 0 obj<> endobj 76 0 obj<> endobj 77 0 obj<> endobj 78 0 obj<> endobj 79 0 obj<> endobj 80 0 obj<> endobj 81 0 obj<> endobj 82 0 obj<> endobj 83 0 obj<> endobj 84 0 obj<> endobj 85 0 obj<> endobj 86 0 obj<> endobj 87 0 obj<> endobj 88 0 obj<> endobj 89 0 obj<> endobj 90 0 obj<> endobj 91 0 obj<> endobj 92 0 obj<> endobj 93 0 obj<> endobj 94 0 obj<> endobj 95 0 obj<> endobj 96 0 obj<> endobj 97 0 obj<> endobj 98 0 obj<> endobj 99 0 obj<> endobj 100 0 obj<> endobj 101 0 obj<> endobj 102 0 obj<> endobj 103 0 obj<> endobj 104 0 obj<> endobj 105 0 obj<> endobj 106 0 obj<> endobj 107 0 obj<> endobj 108 0 obj<> endobj 109 0 obj<> endobj 110 0 obj<> endobj 111 0 obj<> endobj 112 0 obj<> endobj 113 0 obj<> endobj 114 0 obj<> endobj 115 0 obj<> endobj 116 0 obj<> endobj 117 0 obj<> endobj 118 0 obj<> endobj 119 0 obj<> endobj 120 0 obj<> endobj 121 0 obj<> endobj 122 0 obj<> endobj 123 0 obj<> endobj 124 0 obj<>/Type/Page>> endobj 125 0 obj<>stream H0 endstream endobj 126 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>>/Type/Page>> endobj 127 0 obj<>stream HWێF}Wu/I({uY$0vg1) Tw{fe,yLQSrZšcmٛh-۱mf%9+,4WE|:u]+p9g?8fgf[7ϱsqwJpߥ'%^iB _z>$m߲\!8*10ET8@hF]_{0^\LgS—MCi ɩ+Io]>I:=c Fӓfa9 m1v.?GIx£8js ZO~n2cM ®4%{ݿ^UR8͎ʳ;ppY"ԦӓN*/3(}H9NkI0L&w^T`}C)K?c>ICZL J"ᡠ}S $Y 4½5(D\E`w@|*sp~I]8.0pk뢁:/E%=i`uɀNJq- i#.)ԭ 8flyUZ⫾t4ivĉ؜ '~ 'B&xyn1Y8(h&9>dJXS/&GIA|,+>B]D+K.{\S 7@!.8*/HTKfrmdYw*g +H^@_YoXD' Ԇ@P|Tn4 uG|N$~gArka*Uo5G#3dkX8 ]2g*+=u?UW}F]\뷢aq~h̾4+Lrisg^KiYow96YVx>CS HҮpA='(AQ1:ZթCJXB wv%W[z@`- sjXa1U;0@1q%L4lqI?R- w5)*jYZ{"/!IyC7J'+u?_vg0Wvӎ*˸VxߎZR|]:nC_N.0wrP6ţB |Fȕ4z9F䜎J{O] lǦW}kVkLou<_xRP-冖Hܽ:e$ 9Pd&Y~堏&37⌰=v+}> n4|vK#RYxSdYqT<3M64A2xzDGDji.FB*LîP`D$REZmExӢ29 nxkxҤ@@SS*^Iv&+rmlBcfbIKp.RNB5!-+Q! RzjW vQ9XԸ+Iɒw2P\:g$0ɯq'" _a{}V|QL gAnR JM5_d+U"cw=2l˄\Ԝ[RK\o)Yp">5l*0ҤV;9Xpd;(q0V4Pcx}( @Fdoеd^5Dž;?I?ZgPhHcx;h1! endstream endobj 128 0 obj<>/ColorSpace<>/Font<>/ProcSet[/PDF/Text/ImageC]/ExtGState<>>>/Type/Page>> endobj 129 0 obj<>stream HMO1sHL^RCzDBDB_߱$az;w/Zܤ2wgDLnV S( wRgo79F^ng/5b[V ;XN\=<._nF Cwh5I4 h0m2`aߊ{rLy@$F/p:G J1 !Y4Yveji9$y@jk"}:o/v =T7ڹhMQߺP3\( 1NWq35HMu0-@ }dg#7bϻ'eD.q:u Q8]RhGRnׁc?iy5GQɛe7/[ endstream endobj 130 0 obj<>stream Adobed     $$''$$53335;;;;;;;;;;  %% ## ((%%((22022;;;;;;;;;;|"?   3!1AQa"q2B#$Rb34rC%Scs5&DTdE£t6UeuF'Vfv7GWgw5!1AQaq"2B#R3$brCScs4%&5DTdEU6teuFVfv'7GWgw ? r֞}09hmu-ޮZ4UU~ZQ'xSGw?Fu7\22KE8n3x2y7)چ1u=7sx)e#%}W?"SZhZ`wx7е0w_ͩegvm}l> -T<~gR}]M:5jЉLe%0( Owx7З;u慻GA;}^'/sCֹGC+c\f`u/!`t ^FÚ)P@,_.S OӋ4iB_okt*^Γck-9-K=C҄JOZNf/r>>)G=?DܭNkw ~ӻ[h[. Ǿs]`T*mm D;u7_h[k\6E1lw&oЊ6c\7\yLccӕNo/w~ S/>0kYlM||¦ÓQx&,ٵNo/w~ –¤im~ӻ[hKߺB?t+NkߺB_]-.Zw[_wЪ)l)p ӿwЗ;uWaKaK~V;u愿i4* [ \Bi4%NoURRŵNo/ڗ~ –—?t+Nk 4%R]-/l~VKhK 4*J[J^Э;-ڗx7ЗKhUvZw[_.o/ڗx7Ъ)m){cBj]B_.oURRim~Ի愿j]B ӿw ~ԻWiKiKŵRw Җҗ?t+Nk 4%R]-/l~VKhK 4*J[J^Э;-ڗx7ЗKhUvZw[_.o/ڗx7Ъ)l(cGح;-wx7З;hU6Vw ~ӻSaKaK>iwx7З;hU6Vw ~ӻSaKaK>iwx7З;hU6Vw ~ӻSaKaK>iwx7З;hU6Vw ~ӻSaKaK>iwx7З;hU6Vw ~ӻSaKaK>iN/dCK4@h*,? YqU|Η]sME{*Yp2"h #1㉇oSHђ^-==ҬYʒwx [uq~]o5q}{̇ʋ$\1D hn/z+Fw14KG$KA~v^Sne8n!G\/6߰5:s:{wY\bbr1@jo!9e1#bFU<; h'7qݥgϮ;Wn7kϗ u]G>k4'O#oL?3sK姌c4[y~0cǯQ}6]7ѱ?vX5zײa^8qǪ~S첯9oŏD륿+ȶN$.MJ8 <enGK^܏?ڽ7E?#i~ȻM/3oU|^܏?ڗzo싿~Gw_g?/={+r?jA\ko> o9i, u)\؏Ŏ\R|^܏?ڗz{D"y?c>q9 dqV h?`1/={+r?^3mܗ ?r?c?%nGFxYmgK?0׿#Ŀ`1z ?r^<,x^ޓzV/OgKg/1oU|^܏׿#<,z%?xzO~[b_zV?g/QoW/I={+r?K^܏%3mܗ7*E>anG~[bQoFxYmO?_?'?`1/={+r?^3mܗ ?r_c?%nGFxYmgK?0׿#Ŀ`1z ?r^<,x^ޓzVz ?r^<,x^ޓzVdԿ`3=FxYmgK?/׿#fG^3mܗ ?r_c?/={+2?jQoFxYmO?_?'`3~YgWz%3mܗ7*E>_fGK^̏?ڽCg/QoW/I={+2?j_zVd<,z%?xzOYgR׿#PgKg/1oU|^̏?ڗz ?r^<,x^ޓzVdԿ`3=FxYmgK?/׿#fG^3mܗ ?r_c?/H딹/$s O gI ~Ƹ^0QoFxYmsi8~r,a%*h[ 32ѣ7{[qNWN7V`!w<,z ~/:_p6Yki,u'n 6='q1EL\#"85Ѳ7TgB0Vm٣LѫPI 8XV6c+^1Y}bmYYk K=>jV?VѻղWFovZZy%>g]7T۫s.c8h,|UK߅aO]+/36_[`ai6XZK4DAՌJɭcah WmϭWRmZō69<xO8:}6l5Wup~mǸ;lɟT& [msii'Ci~鍵7:^,}jιx=:֑_۩[.q,}5ैjnŗ6Iza6& 4Bme \Dpؘ֑:vVN@5/% {`ATq>6괺a[,...؀<E/4.::fQ%#Wt`6Mc?Dߪ9A{^5d@0:owyuzY"ָ1inytS̼WunK[\9^BG/V Bm<]/^n_-}vXX^7F49s3⣑j leꃚe\G))=KYȬ>6ǂ!.إOR¹1;k\@ 6~[zWX%6c" j~K[YHmN>`i}/tSwPækkZָ9`Qʍ8'<9![.fγk@tޯsnCc\"rmcm-jv)?5!S3?cmVoqu_~#LޝZ< ^^gƹW]5??)} Sm")/ꌵEn{2Ʒc6À5k+|n]~͞. 'KHבSÉ{!+!ٹ3hydl'^ eݮ&F:܍k /ݶ~%簾[66jޛ}Gtohcm< sXERwԺs2)f\\7>'7ڰ͂Vdf%O7֐% SclkIi,!Ӻ RI$I$$I)I))u*X_W~V(*X_W~V+?(mIFm+ST3_IE,$Q?$I(,_I)J1?%Jd_IE,$Q?$I(,_I)J1?%Jd_IE,$Q?$I(,_I)J1?%Jd_IE5ILQk]_$%ƿIE5ILQk]_$%ƿIE5ILQk]_$%ƿIE5ILQk]_$%ƿIE5ILQk]_$8B.v'[%+a$qw%*$]kg Opph]16 :3^O'Cbr;F)p~OGOٟa-z`%?Z ?3Iyn١րg ǂ"Ӡ⫲5nm-uMs^2@OEYȮZX:dp63,i!AЬ}/ΦqZ9ᬩ!tilogt|l].em=xx2Aϧ՚Xrz-5< {tuZUQoHYaɻ4imGPDx& \],Nab9UM-7ka 9P^:T_UvT˘\` h8=[_su?/=;CG3ۨA]].WMh;h(ҙT.6?uvEm6~q{,vEޟyTӞf5_N2C~֖5PmS{+4Mk?y:yRi{TMݹy]sϻc]{wsOn^MAP_an=hq Qʲ߬]TTS%{85͖6GA'}]>][nY[=6ݍ;w;8ӳ0s.ɲCUs|qS\zvK OYCwG)|]I)$IJI$R)$RSXa~w]XXa~w]Xdg[4<"}<)'9_Ty7RowP-i{lܕ6,Y.Cp:3[KZ\c,kϐY}'8dڛuVXSM9s0Hq?OC7]}4&Dt^YO5;1L1{t:TO⥉:5qjی]C3שy YfMmtMVe,e`{~,mqSQ%K䑲\~vSkLz$zl'dϚhv[Բ's?5WNϲ(2KppyyGVZw]i+_;?eTnٻe=9:;huZ_s;CSMUx4dw<>Ӵmihbk(q"0-0;EJ\s v@^SwֆNk(k#&( $2ƚe$%3 ]KL>phkeT}Bۉ|ZQ>9%0:mޓ ˶t{}&wFwL9Y7esiu[%E~Q'y#fh|\-&X75Z 6ZzNۖ0^Ƥ,Jq}cZ$ Tp?6=K?xz~RALK?xz~RILK?xz~RILK?xz~RILK?xz~RILK?xz~RILK?xz~RILK?xz~RILK?xz~RILK?xz~RILK?xz~RILK?xz~RIMyGыer;F+X?q ss_SĶgo^oX/?l0Rs[6jSBhO;]Qh}La]cCN:-giΦ5ǽZwVT״:s΢Vah[ '|N:Gf][he-@<^[ĵph#u.=SKcć x*#껜ie Y_y/ DŬ7B [kid8X}O] ixl40nh~ EVc]AU;k^qݡ;)OZ7\˛-.io] oSk5;󛮭 5U1&zVmm/{sat\d6ڡTtS[=nA?Eȅe~6E0kج&N epw:n,,m2gLYyȦ=w Xi5E= 3]DhZ0ZX< hcL"&u@tkϒDosr4[=Ly[JkF0ps^÷+kiɟZ)9x^nRp` L e͹\48VgǕOګ9v9~nCG;xKE;nٿl{cs<~)K[n{]oV]m`$NM5ALI$I*X_W~V(*X_W~V+?(m 2MFmOѭKo_Zu_7lU1rDf;׳]M|m{kZgQ.&7ǻgMec=l.ɩYXQ[ ٶ c\݅/,ƧpڡYN?܀#gF! q1=s?i㉍qX=vZX^6=lv߂ScG!8 2Zsd>ǽw7ӯX'Y! =,wm>^omb^Z׸4x8X_ne2~ӄmosIcZxS9(ߩ#)ˬbjO2c'Lcl~lϠTTYjenm4|&dzk??++"\Z=]uT 1KyU̾s,mp۬>ɚ H*g_7<]F>8o$ D]{s=cvY-.k-FӐGc%5e&8] y9 [11]NƹI9~9ː;~*^z]73BA%H}yƷ4ex|/4v[,7)[k/7Ӱ{ߣf> rB1>7~t{,/ɾ^}hp6=0n%.1:uvv=w]| 3\Ήq Es,_`}| \qF$qKrKzsK]{75=_6aUc$Yy;8Q1ہ{k]K˛Y1=kַqѼyq {HdW}US]rw=ݡa }Ykq)so'OX=MlEOuq78aWmVǸ o$'^ԧpd83蓦9g\Zi}]:l6,;$8 cj[>QbWn㑒.fHvew)C\(mCWi=[>W[Xy .#7}x,J/dּ~DeV2Mp׺3c=.cZ`^/f5 7Ykw(1G?rQ#yoDx۫Zi®%ql?ץ}Fv_|7q+#&ch;4nEt%(UkAcݩHHDrׇyϮu 5m.}Y"Dz+!·_X @הSyZ@͈k-#z zB)3ňbœ "lހ?r_e~t+:?r_e~tR?r_e~tR?r_e~tR?r_e~tR?r_e~tR?r_e~tR?r_e~tR?r_e~tR?r_e~tR?r_e~tR?r_e~tR9?/'1Z濻c%?Z 3y?KfguGCq"w#x(O #[=ӬhAB:ͭ5c-"Khi[]e sie.k5]sEZ-;ggbd} qlChxЙg1*)cՎ_{^],t nsuA_Xz/T/=9ۤϭZj}8ҳs;e~ 6;GY` u2)8Ksm-pc+6Fs5Զ㏎UgnK7Wicab]N;חZsd>bsZF=.k0!JΏ[_֬3.k+aacr[E;g kH46Ьk~i<¶m}dfzGS`n'n<:?_x߁{ﮟYcTqi Hr>Ga2MKo"GO{{i|_ ;\a`{z_4j6@>M/*~ߤ}z59E~?Tk}gTz\X[Un R}?G߁{Ċ?mfq6LLo]?nY m0@hKS2`Ɍ8&] K K mB[x;|[*WV7Rt,0?ş9)itn? OEV hJ۞IJ獜r_yEM5PGs+ ͬ۴Λj}?Zq@_h?ȥotVAYy ̀4]6.q[9?G%gmc[e {ei-:?yP?R3_|]Rnn>%nMi/"ÿEU cu ?zN쯰=YsYn0OnP@4P'y_lp/ݥ޶ Ϛ58_co]OB3>RV[pѬNxudTKKXQ7: 9P^YaSXk]A7MN~oj. V籤XAt滧Ǻ>R[2׷cs`bRI_ZևW CckAϴ p@;U>RI,`]d,$y4XZE,Uus7 z;ϔ>'΍cG. p69ԑf͍ӸN3f[\}:lzŀ5!j96]C>͹7bSҬ2_@4N#kN'NG| Q?>6'V+=G~o¿k,g=ADᚃnTǖqz`(իS+0eִ4}ؤ~'}z0j/5]7583ld޺>r]ScYp.k!:Ϯz<M=?QIho#Ϟ#sp+=GWY& qx`8HgC]~8eLf}hfUEWyc8͐χQ`hOaE2I$UI$RI$I%)$IJI$RI$I%)$IJI$RI$I%.O wV?'l0^Oٟa-v97 43nT%'?طQn7v of֓Ʊ*bEs#sq+?框ٞ2mVKXw& 7Ur*/e Ie,OEwNV>Y{K[sGn?:>32\Z{ wAqs4ͤ@Us&Qх3_udS]b1oE?3ƐiK"ﯚpm-k-ߣ/x%|a/Հawl#iSǙcĈMpē+86uwzfUekw:cN(z1ܽpc Al9苃.ҙf-+wmFp uާWu44[/k+}?{W=b7|RGp k{H9Ah5iqٯufb岪#s^;C<ε{_V;Ů#{\v&+԰}7Ғ>nptO 94yx.?g^cҲ cZޛ$h;OEV2ֹ! f6+C}*#>iF?W]]o>I l0yE}?fdֆl4Z 킽$91/⯻ƫ_|n35}-ܟ ~nyMsc9v HsCQwr:{бXn?;&mX{-=7.9^kv1p$>/K/>f1zncҲS(8v~n%4:+n :^[650Aq5n{&w_"6l둶Η-t`tWv ftShʧ2v=d[a,0:o$Tys "o˩+@$Z=gUߕ7=gUߕ?-jF}ZΉ9+O;ʗVtӑϼr[<\`?2,V5rlw?{]-3ĦZ:_[+mͨQ509ަ6٬,n߳Ջ$}/Θ}g>dmyKMo=")aԳFLu>@r;7KW?WN;U/u=8=_u.Yۿy%Z׍(:IOsNxhsob=&cX] i -X=&sC/"vC|w(Rr=C~S\cmszC좡P=z𑫉jV/qn9Qiq靾h]z&xg)Ɏr?`Ƨ9vUevX+\kqkڲMVWM'n0-K=7:{}#3)V<^7O W;5]?yYUK>˒ڜ..4[-~ E6bAI$$I)I$JRI$I$$I)I$JRI$I$$I)I$JRI$^O'Cbr;F+X?q ss_SĶgo]T9q.0/ k-'??dֵ58@SҺ}6Zt.n~F y ;VP2&;Xɔ6$[_S4 U>kϨdTeyI\GDՂK^pOwYUfEf]%4LyR,],ޫk*ԗ4k /{ѯtgRc Ü:U7#+ [NChkv=5qc'c4UWk| k@x{~ ?`Zr*3#{gaxL: ;4\斴t?T{w i͛{DwZeW60{G `/hp5Spq(/mC繭< 0=?c^2iK,lߤAʶoHU^C]v1e`4ZmbJWՖM_k,#nڃX60h>hׯ/9[$Ht\;k75& U^.N=v}f׾~okoۖm粝ۧ& O)=G=*ʈm66Otax fq0~]E4ӅvVƊ>G骺ނ?۞*[QfZ[:]ܿpM53n=BuyR=G{ {Ь~:`%8WVLnN^Evk>EGXa~w]XTw]Xdg[Ԍ@8%oUUg9RΘZ[1Mcst܎!E5:m}U65D1j3Hfm/v(upkLn椣uCc%c*/*gڮ5tJsw!sg ^̛m읧!l!ߢiu=+?}^e参L3yƽ֓C\8i5))d=+m ]{sLQ=Åc(t㚋\\὾PxF[Y}_M)7~-##CuyWZžC=W~ RwkZuN]~p?;U)7PdKk}&xat2(,K[$tԾэ?TnE`Ȝ7Ks-/n=.K^:umבMku7[I!"#MkN<Ϧ?V7[Ƞc7&7*ᶼv[IഴW;zMgXٗѾy[Tg;z`k1@vki* ̨?Ԧ)-W,y.w>ʹ_l$LJI$I%)$IJI$RI$I%)$IJI$RI$I%)$IK'1z9?~k99/[3 7E@?SĶgo^oX)9^-~47-7{N0?*lc=֘0AЬMH5X50s!YZ}E1Iu %luj6iͥ0 mv=mC't}w1f5؄c+nms\]> Ic%tU]{k,YHȕձ0meW sK˙[0 ZѪ!כZKas[I=}tME`kuݷ~_ߚ!/ޠ*`kl -.Ѝ>z$ ~ap.nEn`{^րΘI +MMu7Sk2o@ww,OZgAP9T=%t_m ޽a(v3_w\s vδ9C:xIO=oΤmĵ,$lps]cZOgbu lZh̶cKAe% ʴ2:IǶhm9@I%GY{wڷvOd Nߴ^M?`{MCu{O 82> 7ZC<P?MƋIRI$I%)$IJHIN>GRwbGXa~w]Xdg[ԉ"ڈw;O9+RcPK;3.k41h6@\2r1inU_u 5ճ>loL̺Nazhvǘ -}<|>)\n+,s~>?ʾ.Tz~ 4reյlvݥdž9H kKq8>igڶAhKx\%d:WL6=Hppz[K{{Mչ;ݛw~؍JDoFG@d lO2Pcqָ0tǂx\F5Tbݐl9i5zv~RuZ[?I2}|ǡu`Z< 1~. 0Dݘ,[5AK<\d xGz{/sol-ߕw&Սu]nsG2ZBGKR2[ZRCy&j,u{^l\ҷt=P`X.}Y1U[-KF%9T5n1z~ګ=lVo t|7JX/fK[Lʧ׀-Ƽ춯Shi,?FgTZ [o@kH›_f#Yl7?2m/qqӦq3[vks09) JZ a8=(okap@ p3so=-{lNuė4qiw"W{[vݰ)hum'RHwUղwk>k㪐h1fvy,voqomMǵK[?F|P.Ⱦr.v=o^ʶU9 eZ%[v=zǤʨpmh _M]̪ 8{,݉^]XTSd$LJI$I%)$IJI$RI$I%)$IJI$RI$I%)$IK'1z9?~k99/[3 7E@?SĶgo^oX)9^-~474:OxH@:|®5V -{KOwYeّYXsh1c{GY Z` EVQu k\5ceI Kewťm9QsM{c'c4YWk| YeoǨ l? O7^̷'nA$l5NU*OjIiCML:}?]y\6:ڃu{w;w|~v fs[ -x> W/eaz zu 3s^KScU\=-c݃vGӺSɶ]] .om%B$I)I$JRI$I"{K K c'?@-WV&-L89MoUCugG^V>ڰY],{k9Kqtm8M69Ŝl'|=guNm/˵e9zٖ?֛Cc6zFlq~QvsAk \@])zFf7]qEw䶛1mt$́Zb>W7_siאUwJΥ.ǵRn{^A:4x)x00OdYVK uKxo1m63vC)a@݆N[:}b]evXQ`.9w9JX}7[Ryme.{L "AQy!@2f;*z0Evj\[Ŏ-oa3TniCTcdՄn;\5׺\"0W']̪+ȫ-:- qkhulk_U(2I$I%)$IJI$RI$I%)$IJI$RI$I%)$IJI$Rw^Ok?ac z? k-T-נY Nןp5@ D\`o}n줒H)I$JRI$I$$I)I))pY<2*X_W~V+9?(m91mG]x95WVr՝5651afݸTtޕ'P:Σԩëݒn+nְݴ:R;On0{ֿn>JJ:*¤gQq`thvVQ4V^5nǐ ;HI PRc.FmNxc,&Z_Ԭ~}6P1/k_a 0=ֿp߼rDGCu'nMh7 #}뢕 ӣUU+"SnUk~]1z.]? *ײmu6wDyFGV?q}+ZmN{Z0sN %D4Cg57u3F'kUߏK^eގyрs܈WKz*c }O[o,2C_G& ֨eU+f.z*\̭kH )*F̷7X='eGiٙFE&6C/65> Za1 65ŭi~jYXY+áT^FeZC-d55@ƻ~fm8d:kǗ@}MG.c6Y`Y-4'Ѻ]Ժ kH!6+͍ ,I(j0YeyPNquۓs l0Xmh?gZ\[q[ v.qZz8ioԐُ۪GOM_tcv00<=yK@t]Y?d~ml~s[xhӔNx \[{Z> i.׺$5wt_^=U9 I>fWOkKla mF=JҾوcjǹ .xe_{cD3e M΃&Fr|:xxUz8抧v0{2.UnUse6k]1m~;y5lT$IRI$I$$I)I$JRI$I$$I)I$JRI$I$$I)ur;F/X^O'Cbw0?1=?Kfg_̂k}-e׷NI̮?%?Zﮮ1$u[6rnc}W3B܊E9nGhU='en %pcz@lBQ?D9WYW{*n "O--xa= >߬scOMƫ);X,wdUSXO*956MH85) qy#uX.p趖Mՠ8|UVg8 K][qx7bʯKۼsƂ $;t5~] mnMu)|+ȲA5qNSm:Zlm-u>hWOfnL-ٯh:8,cŬ,pp4 .iD R|{k6 M 1.#~u?_n{sZkwkOQ.ɤ|n:=OS]Y$<Ϸ蒤ik W .#*z~iyg6vF[v6:lV'\|]R_Pu.}@=c(sK,nh UҪY@y75Rku3Sf&(0k\%Fު7]Kju2oŤXuZY9Xߑm^=WfN kJmWNΛlA5IcG>:#sK^5.ϢrZd/:_ί'y*temᶐL:wL6깳VXjUU+LR=q~R1)W^45 '[gMUx&:qvȑs7VFN'O^Ǹ46 HE?>{D?f݇Bvf&:P­cl>;Z&t*Yw;>PQ]s[s9G Q:X85X5` sA'Ļ_ewڝm ,kݦUE6w2I$I%)$IJI$RI$I%)$IJI$RI$I%)$IJI$Rw^Ok?ac z? k,T-נ[ Nןp5cYЫR|ls՜H[(n$Z n;Nw #Kb>ޞZk@1]]u쮚kfhUʋZ Vwj:Z톇1 2%4Yq\Ft #V][]#/:́XX 8!e;K.}6Ϥx`xWiO73ⲋM,.A gP2]<5c.ͳeNX7l:sœm_-Un5xL5X˪ݴ9{x}p?aU>!a@Zf81u:[M5R׋b qe-.8}d5V0gڭ>羽k4*;+Ԭ<*8X99뫾m5X Ѧu|]Ru.>νznX^S66]Kk"O:P6{*8 }T]stm{F\7w$Tu׳INk-s@->ae~RTTbj0 >Hκ48y쪩/e}AFyŽF~U]W 7^] c?))keLq^f-{y ѣsRAfK 'icC״k(`geeU#~kk{K4we.Fk$mm~_m {p C@aWͺ:\o\|ȶߋ"3q[v="鲶X\c^9qhBT`Y~E7]~07m? 6F?eh {sFjղmϳ`yɦ'ƀ14Z'k~%Js #eeun}nvT:~{6{9ki-~fm^s[9 mV3.TYX@3uE6ݔ4m#{%UյuXWۋkms$nkc=2}֛yk%HKgeS~݋m~Ō}inֶ:OnTղ>MWRڱ,vIxy.o-hoF96;}ZX t*,.tg}^u [[9STSg}T$IRI$I$$I)I$JRI$I$$I)I$JRI$I$$I)ur;F/X^O'Cbw0?1=?Kfg k,'??f?)րx(UV.6*kYs=F8k}klh{\uhkE.+;2+"lk^?Nﳣ0o ۘ浻w<ߤgBaSkAudn_Ƒ@)د7E5[, 挱>7 *[~&װ5ޣ^uI$$I)I$JRE$Jq?+oz,/+~tKo_Zj$:95WV*]Yn==̋f3ixps1z'Jv9c8:q̵_x?`eaPku$qDe.wXIvx#PTlnӨPf&H"Ysa{;>I%%5zUǜ18V'tWF2өEeA@J\EL3÷lm{xk=;zFeC,(,5ݽXݠ~P<S*j<zVfUZvW>Ɨݷ?*x%T\60\9ۜLKr(}~1y#ݎx @Kkš>r/Y. fLZO9ֿph$88V`%.$]5 CDQ˯.uc\v,pxZP<(*ZM^ muzcbeD6v7!J\JCc]nCb7׷~onx>!`uE:xk,Ar@J\ICXvR ֍ d[?4t1hc몓Kg<x%PatΙS]v\7OO dj w*P<HJI$)$IJI$RI$I%)$IJI$RI$I%)$IJI$RI$9?/'1Z濻c%?Z 3y?KfguGCq"Ѵn;NwSBy 0`΅g}kkhsp+O u $uXAT]4f˫ -'=ZWY"*ʳ {wZ v^ZɎ9gbא]ge5\Y9C}[J71e[dK ߤoa)SZ]S} h5>'>i#;NHd5Լa\ XCi1߱VN]wYapԵ;Qo_5E-!֖ƒH%+: TeԾWEYkwj>=cS{k0Cs^mѽmQ3}|ZmO`,dηtg!oz]sUM{m \u:} ~G*k1e۱vs_gCouY74z~c}!h.WR9鍊ZfAcl; ִ<;q: w+gRˆEla5h;-ӕ?ڽ/6LQAv=!ؿX:fC\ݕ.uYXpq}UU!?J^*w\GapdvBI$I%)$IJHIN=YbwbYbwbbΖm+T2 񶖙лK $={)}v" UABGI/.4q@vE֚ZACZR\ ߽J?I}w$gK͕1B !V?I}w$ c_h?]h)$w$?JI$?h?]h%I)?I}w$Jc_h?]h)$w$?JI$?h?]h%I)?I}w$Jc_h?]h)$w$?JI$?h?]h%I)?I}w$Jc_h?]h)$w$?JI$?h?]h%I)?I}w$Jc_h?]h)$^060n̯,1z9?~k99/[3 7E@?SĶgo^gX'[6hnhu;QPh7N;]k-}+;2+"lk^?银xx-,ʜ:9g}kk=XI$/nf_vEyXfcYs,kATiYPlSHkVS=Ij1~}s%ս @ݤ [/{ h=m`v#>*G[YF*mTFmM{ud }!_ً]4e1XY{> uK:;nV4 HMbZwGhS`mXX71ۆHd񬱏}v;%>#mE4F7>_GfuޕvtmsBZئ~P,/}ͰK{1(Ud=kkZdk+/YȬYaq'c ~*㴋 Qy݃iI=+YSZƒdD SCEv=s<%U$H)I$JRI$I"{K K c'?@-WV7"oUzt?ꊦ*ͳ9VvF5Ue f3-kmpO_cU nU]uEa aݣ`p|F䗭g3$/5je}^㺋2)׺V6e`wmj}m$W1l]D}gֳ_^ҕG0ez:Ѻ=_cdZ9Fꔽk?џ(ZMe$5E4:OϿ9E%k,٥ 4b}Q:*[VM夸춽&&.K{*><w#C+W200NA~}}6B?O=ޗO=ޗ*8+F5Տ{trgcSfEeֽ 'm=igO7ӆ1̲lکa{:a)U߿*C֖}O5 ȯ+# k.,5q[#C*c[^R͍zM4L]:L.cF]ֻecl7]LןnK"65^=WVs{nW]=Gz{tklΪcō_I(XptSzK" ovIU067>*h ߫G[c*_ۉa.nUOmX6;6?Igly%b6q7k7}t; _ReSa׶Ɲo 0}[v ˭R׊k1"z9]zD nA15KE6,:b>ŕ]lVZ?IwSomk{g; :,ϪsLk5=y[Ϫͫ:@v\G~( Qc˽'lc~k_IS_3mU;eA%)$IJI$R)$RSXa~w]X*X_W~V+?(mNoUt4LufroYûfC)['uf`Vu}\xa3[^EL/yk7}H0htE ͮm=t.--:4~ ы:NN~Ml,nas9؎ZkynYY~XZi_Lū L<8s͓q:̠7tnwײ9.p.q2I-**D~c6)skNER^אg֬F6 @pq1I>շt^][2Z=igنovot5=X:ZLAk{ g˘][#>ǵU~Mà_QX$Gejv5RG0kK4L%'bcᵅ95G2di&bU1}=DUO[R.qXUX k,F,UhkEEac@dX,8<}GI(tNTt:VVK\[\αTg`RUeF&#EwMŻ -|9U:,~G51k"7 ejcvU7l{̀S?Y*d)!~GҎibsw~6UևegԅuZ9_X2qrk7} ma{o-5([[Ӳ4]unh9to}<۾c689+eofjHw}-4ڴsϋFe؏}ksO/lиֱvu7nhs,=k? xmo:ZmP{KXUaۇˍޣ\%Du݅{meS5ʪ۾(4}bc[_ck Mg'U_Tl]/u=phW}^w89ո{i vpDWTw֊N%C~=:lm|okr{#S׾ZYɸаSf{9̲7;UMC2O(N}Yj)1\v˝׿x$APۅדS\-ά90@dnw2pZ knG@6=PZ-x{\\ѡC?oYk(xz4GpUG}b˶ri˞=F ]gY$j~vבc?Q]ZKke 'tݛXF+{K7H:W+(˲\V0\Z^A.ph YُsZ;b,i{+p3Mſg5js潅mӔzi85c]+f_Y}ׇc{ mVR`RM2C\(c9 c=Ѩߤ<:p}Omnk *k`QNcE{^e{h! sѼ$|iLytۊvceiq Zyi%nߣx/s{[=W!H@|C颱X m@nwkP9Qէ&nk1un/.Qg|i=_Xaq9{bװ:{^X5&_:ce c\}S7:D+K﯒~j7_S*ufya<0ÀZ ?Iǹll=4;~&'$I)I$JRI$I$$x)$x))zoPnA ,-{ih1yWu*~0n3inn*uʰP5Xڈ>^Ǽ ޫFEXήmV@h!I#R:Aen {F\xΒw^_ֺb2$D|1Un5bf׿ӳ _/9n>+Iw,címރi{}ŁloӪs6\lh!:jQHqUXkpuK^c[a۴H9+CKX %@~ohְ1}mZj>. sCn0QjX6V˘S\؃_8:(Tc͢4Օ[XY׻ZaܯaǯS˯ y/{vN\$I<+u<[xx&c_W}\ʟ׹`.f(¯\ >V4 bV-̫qVXXp/s]ed+hth7ٖ1~KXpl.-%6TԖ_Z`]EV;ݼ`h5U].uc2K /nۛ΅ T.K}fd@kv;sz~eUf5MwQ4 AnZGtU:),NVOP~}m'{0gݼCR=r][Y?xܵ%:I,Ϭѓk{]EO5ϱ9ö ϭ c},uW 3TwX9Y;e9/ 5. 6aZÇlWniQ k*S`*Ev2[5=osE[Uv6Emfߣg4/>y%}l p]nv햲[;sOۺ%'N>-,w91-i_>'DxIdtnuW#__"_YXa6^= swgI/IeUJ)u,k$~lekon-v@$];v$WIcUcx:UnptHXUuy}h;6 WSȻ&5xY[%k pkHfO@?Z[Yȳ"Og6V7![3 W~Q`ыux 2}͂FZfKX -s\X昑Sa%o\aVxi iyr#qAbfmvQ`c,e8t.wXY}3ve[A5V`{\Dnݍ^-(e.Cth ]%Ԍ]eC^ۻ,߽ j;sP29cL肅!IdC>w$znۆ8xZ*nl[.=u5X9m [jvX;~}Y8MײR[-ۤs~Mc͕WYzvn:׍;,k~Snu-/.kqh5 >`b\*{\I{2X`s7]i%K [KUٳ%-զ7Bg{)ua6om]pR}%WZ'QMNnpotCV nIj[:ӈMuRXM~|7~V/gI`^mNx{K ^zE۾䈝O4߶Xi?oŠ`) I%)#I#IN5?&ꚡx_I?N_ww^WWk0>?'l0^Oٟa-z`y?fր#i ѴItUF5Հ65gcSnE{I{1:fco59-iMwQs9  2[-]KreMu-,m-jn6lEt3 T\Z~k\}MuLJz[;jv{kP=ݎs_.t(.}R]eUoplwL%OV494_X3d</x o;vW[_Y 28]S-9myI:܇95x(W'ò'+muA{ůnF)Za6m56nNu5mX]\'{"}OFGPWkIѵM˶`bjakRZ2> ^(xhs+;[g6r+{X_~4l $ʶio]zV{ۿPw#⒛6Yc-VZ_dWmd}ASZjsdZ=;"2/Ҷ=USyue>sR65Vc C}[nuo-MAPWhm$:~cYa{C3,9,ވ02 cZw M$lI$ RE$Jq?Uߕ7=gUߕO~?:[Sƿ[-j75ESYp nKUڑ,aqhכ ^ǹǰ4=G +' 9CeWvڍgi~>ֲ݃v̧m s^d2'Gưϥֿ#kֹ嗓к.>ٓ[v; ZhUSyu[{;D>슲潍 fM`.:h0ݒl-a˝etNV=s.k}m;Þcc<5K׮﯃kky/vEϫkk--enѱinl;G \~u~̎(lz7]fAΡQ5-q&s7v Wa;c:Ѵ{Kө:UbInш@ϴyuv$o`5_˻G,K=VK~.:ׇO,k6;PRM_OEf9{,i7;Av:'=Kssu@ul=W>ꪳ{m-!~E WR+!8mK FC7Dz%!ACkE$zqm>yF3\[{pe2W۟,mVf?!6l#K7c]6|vCl։eVנT~ץc8uP4Xx@hp?%uܞ|[,cf{Vn§:,Uwڍt4zm{nm@b궻e8> HBct޵GQ7oe5WS}VSIҳeӞ $!ƿ?*o;gSm5O?;*OUI5yoщ?漱 z? k-T-נY ?6jSBZ]Qk_Ul 4KuPmuQKmcmnY>uW:{(km{Kj9yƧ&ܚP5sHm=igK7QV3mip@h!KFkgsƼ9qהL̼i[5K d`?-]2ݾ 1sp}da~uhb n2a{,ɮVckku媅V,*XՎMv45Gh"dz8fToiA͵4QwHk]*sϧ^P[,pk\vkC8.Ǹxp/1nhgυNϪ ed6)}#iᴍUtu:K726@h0I׉VjmY[s- A Vi]ŖTZ߱TƗ K,Fc K總p $JRI$I$$I)I))ǿ?T;Qz,/+~tKo_ZGMQP[:okꊦ:!PʹX0Kvvtg{o߆Zv7 l3wY5,szV:"LJ ?kCKx}=2YOQY=HӘ:ȰYiah `q: T7F-vR ?MQc퇸}.ڣf]k2rmͱy!—#vN=\l:+R׹Ϭ6zv4jQ-ϏUM8;`owYpxݻHlAř9|Z}kZdzLʯ .m}.ۮGYkZKEv]`^NwmP8?VzcY9XӺZ >eiW[ֺ\9nZ4tY:m괇C,8 hp= :_Z9X2hճ'!0mYxŸ;'aw_p2ZSкe%ݼ9{aq.qe^eSuo We]I߱qᦰyZc/ۋri+{ϲSktݬg_UX,*ҝQu/; .ݧDN8F%ȴ8>緻u>`%<_ѩk{&itwjWMůO}^ m,}ppF?V~.`τ.pq={m`:"ek:vǘ0"m -xcsVѲ)>GfZfé-7n%0C:VWTbn{l$5մSsAL.GU^;4VZ̬˧VxV:jIe0umct> ?#%6]c8opa;M{۸4蔍tVԨ鞥b۳k{nڝp *2W {ku~hL7tzq1Љs}2t~mM5mmeX ͯcC\FEiCUf]}M/49̪}ֹz>U뭹uh/73v7ehdtL*Ƹ3Ԗ={KAh!!9P?۳>nFH▚s0~Xnf;a[cE\bwvVz7]wUɰWAc,.i%}t!MVCks[%[fK=Q:fK KM[׽CjMkN~Gl\`۲,mƆZCz]n_}a mdIVs3z1,.,zn}n ﲷ8ni~Ee66E6n 0OiCERbe $H ܾe9lux캍}7Wsw{;][GcԨ$gWm5e֗l W=z}szg}c7бkCEc{f@3[꒜>1"{{sj=;߾ϔ<߭cZ,VFKv [Aݠ_B+on:Y澷tk6\+8kd nGKN'S,,te 9s4x5VX]6sC{-sG wtlx[{Ɨ{kݴ8l~̓|VM-k` d(gjX^qn0/;`R2vݻXK*fV;8C(>;v@$"^ `<m4:ǽq+qwhֻ~*n߆F>{3զ ߶@?D*F?Ɔ趺l'6}W&pMv.$Oh[:N~a...ǰkZvDKO3 }vsֱkLj."tmI- T,]6q{ 8Xc%ѹ`t59X$"I(tCx_I?d<GOR+UQ'/;r;F/RN_wwM5]?_SĶgo^oX/?l0NyGCqpmv8]59-igG,󨹁5c&VQk_Ul 4KuPmtѝKm[[keX-*,^->h涝;Nֲl5Rځ9lֽ/n獣ݧ<-}Ts^ZXHژuk6Mm 9Ɛ ls,f}Q6Ѵ:VRwEwTҰTy.7u^,T)nX^ktt*P70?&X)Xb*rj]ήWiZ}?ծ.#hVvuok-cˮxZf}GQP I۩ XzUUz̓-h5;Ѧᤥ{)ǣ_8W4`mscon VzSP2Kock{\ sZւ6Wl8{ߑ.gÎ7wJ:Ih*{k]Q-x{B_Ewo$:uXm~MCn':`ֺc&F^ǽvr*tF]$XIXstkΪJRI$I$$I)1*y${)KlW+J? dLjBږKo_ZGMQP[:?ok` ߫O2koA=ݴX=min\[܋$_~SVKv=En g [0M]nZ@K{y(+}*_^.5u_}CB|Vu?uXƻs2}Qюɴ=Y>$V}0Ǵ\[LᵷSprKa 4> _j?c[uضc M^kma]dtNcK> ð5Zf]w>n, ^B?ZV]X}Jߵ-ip=Z&(52:_23\nXDZs\޶UDk5bWuX2jknSuO&&% ˾UӋcayٸ :"U12Y,T.|<Ǻ:bg ~mkYh H6l|~b[ӺRVޭ+-Ul/HxtuuYM4>繍?D?ź"Vp"u1s0:ņ{+[piz?E5s:ɴE+.Zxaq1EVm.Bd2koɷmW> I{ZyZ=G tK^o{^ZTUzt)7/hio:Rm6{{#w wt^[u5Mx:·]uW]cC8$|TkEOGͮcn]\em {?OI#VOm8fvDV=Kip@~T/)3-̦,ǯWS2$5;{Μ ~ʲ2lȢHIkǬ,x.{uؤQ?<[]BikcuVn!7~7]Kk3VH)%zj;_CU?eFzͰ"oXO*^\e/E.k AGHui ^.6PMuX܊.mXs7 $EZΡ^2bڏ6R_{h|;zi> Mf5Qz }]h_Uu^]x}sݰvd&i~̪ X5GKI U3.߫u]Wmveͳd0i/?ZލfCnA6d{P &Υu)#g}R]5c\ᵭp/yAM]m8֊]UCXA#pL{"WR@"S1?pn5e~iX[}#=#[Kl_k5˚]JѬH]^oߴoDŽFCfc:6g[ p!̝w-ԒAJHRHRSxI?_*o_(|j9?U'/;r;F&gL.Ϗ/[3 7E@?SĶgo^oX'sF١u8H(DCqpmv&;]ԫC]Xk]. Qn596״cF;hDz{ʜZΎY)QsCkH99iln>eDU[^>?8}W%uah,ps컱s1]cޝŔ{op 5Ğ굽[35U~ kK2|}>OL^qv( =ǗYdK8;>1jmV0;FZ L*5}bQN̒jc_c{]l}jo =&zVCvmBo'mfS-kk45C'6ƒS=0K.23('"oU6Z\t8ީծU yQkNSN6]L`61"8nOwկYkZWZ,uh#ku|^s_,`@ t^և uΩic*k鼗`4;HewOJ6cSEƋ1 UfƻM:jҨ}R1ຶ74O*k11lc,{g6 cP)as {]T9q 7h*}VksEcNXHZI$$I)I$JCZW*ҿ d-NoUt4J=;"܇TKmk'k\5 JC潎mn`cXc^ WR"tk驵<7?4: obUn[ꢽ5L0TOe$t;5vs-ciF*YcmkyI*]2Y5 Q= }2ǠtX`cFر,nuZ)$i2?cNOsYhv7DZ$LI%)$IJI$RI$I%)$IJI$RI$I%)$IJI$RI$I%)$IJI$RI$I%)$IJI$R<<]?O T SM5X_r^['Cb*O^X=?Kfg k,ן~5M kFBȇv 3mkq-c }*Re u` it7MG. wY[^:fﳱne5ZitmNlid鶷K\\:N,nV@CMNlǐt'N>,mynf#vgygE[}Ǩ ZXaۼ5^cX:N̆R,pl5䵺rJWCin[k-cV=lׂ܋7\L座T5{M?ɵ4IvwHD lngfE{}CUn.lXCZ]~(t,\nwMci8743^;>2쁕/'퍭iyC]ͭ͡"Ƙh0IZꯩReV=A!d%5YIk~֘{SH- 1pais=088蒛 $JRI$I$$I)I$JCZW*ҿ d-NoUt4NORwu_^ ȮUzh c`iX}B{[m &R$"g&xZ7͹B}`s*f+a?׻/s:=m M.whsKÈ2^~i׋72 xzW>7Nռň){ci N+?NsL̶ 7ԴsV`Xac k^3V9h|;:o]K6d: ,N;IG?Y*Tq߿/4npDF-\K^do-fnMp 49>.ߊwL~c]U\X*p$tλfKiʇ`]$oLn؅sAv9Ĺ۽My;.I@wE[6Ylx斸8kkKJVZnlm8u^%8ln!kꢋ>m:-nh9%YkEÊ=/Q涹&%OK=[GX=~|R5~ݕe#+<.{hGF=k(vܽ79hk!"}S}_fW,mO75uVzum“uË\ s?P])~W(ZȾ)QUͶAqsl'xQv%]KqsmEMMO Z=/*Z˟VNZ׋iÄ oIkrh{c۸ptKJ 4?ӂsc$폣[5iH:lcHtl1p$U|~(m.ƪ\춸acZjΓf /E.l=|Q= ,1.ymwvtVW[/b\-iK,WtEպ9v'ӝјfA{y{N?v驧,>eVZn->kɬX:93sHӿچ}k 4lp*9v=n {+mCwNw>+C;uXcjZ0״99Lʶ6|\AP'F:{wWȺ߄ljvmC-Za&oR*;NSI@,9Gxә_`Q~+VV~ysEf^}tNԨˎ9{ ,Gttڭ?6ч5@g鷞lgup}O5IlD>D OGth7VA nנ1':]Yst{8HXCEXiQOw2yqYhnnƨwʼ:/N[88;F04@T?k)[+s͆Qqh=o:)i^)=ku_[Wՙ8!_;s}7HU~5R\ǵC$)u2[hݧ`1Xi][{pxnw=6"1u{KK}Fc3SPƨSn95@:ʹOF1x!EOToˋZn4t܍hS̯_lk{aUޛimf1wfx)ce $7u/ɬ14n0TwJ 5Z۽p\\ٽţ0З:  ISx_I?#cNuտSn0NO1Թ2D1"V9Ư-1z_r^['Cb8?漱 Ϗ/[3 7E@?SĶgo^oX'sF١Z6;N"xTP5沧9E#YS/΢294pi괺Zc;h۴:sO6ΥV.l'۹ҟiCE;ڪcq aѽՆV>:P}d7!vUU"nk]W]Ƃn I*vnIU[ Z Ie3yzVg؞d]F9'"lul$E o'eޫrk[Yex*hl{+xts=Pݻ#T.ʯkZ6.{O晀R(qi]bq˟7Xav1cݪNL"̋Z(ڱZnyc߱s]oLY:xlu~I.ZQgΈdZ̢Z^r[Ekڜ52e]_DV+q}.;W>WW˫X/f Zp<jguΎֵ͡๲<%jqUtq.5i Oɪ?X-Xϳ]5mϐw2? 5S}c}v]P\xu\ik Xcipv1yo@I$$I)I$JRI$֕G(J?#Bi-KSƿ[-j75E4uSfQmAm7+unӨ4>C|uY.kcl`k2]if)4. ܸ.{-gmZmtV5g;ێl ƿ%eni M{ou(ҏ+^Ycʫ쬲+k[;CÁ1Qc!>/v.ȋenp+WuʺN+2_[6W[OP'}P1ۏCn$$<^my Oí쓏Z yw:;> c26P/_m<kʷվ}ߒ^fc=Rֻs[Q-ׇF]VZ}]k^+v1MJ@-Kg{׺o}=5fa4KrZ6ne;d]Vj}Ȥp1885OZ~m/f.[r\[Z6/yur]:)Xan.ޙ1:ȲZ̊ٻenV&9Uux׌/̏KZ}a֪7ەgP͸::Ijmqvw ]}tիoHueeeVYu$\X[hhlsU(]eZ[k?idXS.=qd)"{\kƽ7;wiߍeMkwz5}FKҿ{he^~Uv;qm.kfm GU:7TA÷tиsE]?k. סF]f`3lk 'A|XÜd?k}vp`*gt[mk3]kZ/64<ijs}a#>s;Sk`e>HYuﲺGdٲo/hus#Q#zKn&s~Emտk^e6%P_Ӻߌq5CHwW Ku5;FׇAhpO w_Pqo-swn.GB|?@ܣѺxϬc\ 1'kmwn3BM4/9xecz.y:Dz^^E5׍ciŰFS[gP]~'Ţ&p}l .;$~ SwWI ʪeOm]Ýk5YƷ}v*zma2tntοOTɷCw-{Hsw4f;*t}d~f,V{1i-s էNR=|jx[.V%: ?H奮ᤏVwݧ0Stޗ*ͣ/#6z6DW{6Yk[[6Sh2}ə^[ˬbVw;ԵnFJx/zOYVP殬m~ sLgcd^^KZ70m,GYumk4VP>vzf#y2cZN9 ۔ϫ#7b{]v17>$5+{dg[u@ [sl" -¿}u[[fփ0.~i4ڪ5tVw\v;gayQc̟*~E=#?fĩ嬶cYX k`jvf?Ea}.c=Gcfل +sŻ :ɭ2H>R}L{}7%粦[w}cMledZ,m;]+9HW.ܚqɬ^7M̼:s0@)7b5>26*s][mg ٗt:lֺOeէP4VluXzsm~Hٳ^*LLXY?IcF4X IRC8d_YǮu};ih+:ޟ{11`4jXX98;ZN3efl -髟z=81j 7m[W B=}kz@܋_}>^r2+.iE}ӼD*w̷VPWf8c\ٱnqꕛQO_ؓagu NJ^ Yk9l{Hз FOP ;[9us6O"_^"8Lo2t1NM1[Z9Ư-1z_r^['Cb<ז?ad|~OGOٟa-z`%?Z 3;u05 kFB"h1*n͔5Հ65fӑnM{O9c:fO:59-ig,e7#:s-c`99ilm/fMY cVt*?VgQB܇[k^^%vou98c:nl-1Ь:F5Yc1 Wu8׆Vh4 9 ^+el ]a.n'H)I$JRI$I$$E%5MTaoY,>ަr\[޷h֑úR\8 tRdʛKo_ZGMQP[eb1oeV6ǒ:kͥԂoEk߹hCtOc5s`aڱ΍>HE(e%vjr TtlڭR2q-kx<~ 9+lUe s=ñ͖+aeW]W0J9oc#B%g}f7_[qg|R',3}wVJ.%'c$ٻswgf;(a?i}{ ,\ƾlq-lK>*5Y魵ՒƲ{'Wɮ^e8Nݚ V5\-s=} hT[9KLS][*mߘև9^:nk@$Ƴ__\LT*RU1o5݀}-}5K~ eabu XۚCs\DHs \ 'Q]SK[]6rP{mh Y2[p`?{v-#(~v q~*lWѺ{znxQﰇC/q0Ä:nmn/ykӹ] x*B?__z^3qoKj,{KIKq k \부]8ȏN2\{6<[JUI$_A*lf]S(7׸vvJuo7tֵm p%Am}4=,*!=ᅡn]ۻ+/k2Kvn#AA 59M WG摑S=5fK[kZFЫ鶿Mnamik]pnFوUꫡvSuUF;Zy򥳢댉7 Ikt+*x]_өn]0j.廵{G-$4ƊR~ݽ~hiK~.Ft8;Pw{A{@_ٝVdElc=cHs\ =ʍtwXrX\˼}1B:jy}f 5[ |*Iӱ~eYX}TW0~'f! ⰓP & Lf6h WEV׿ȍOU;9>-\C M yٷKM̲ƍ]iy\Yqi6'57"E^6Kӫt*vt̖6@~ K"ΕXr2YgwKI[iVjf=u'ð ~韴og{=-7mݳINV*!wh#PyMsz5{ꆋ=~G\^ #"}sA%ޞW;z+Efsڍ i/C=Zov0tfPvWfsĝRVcUms˜ [ SLLv!i'x{{q~3CKIpMfR>nejٱ>^W qpCK'gޗt<"^DV݌В$4Dtz%ڜǽh׵p"7ߴ1?k>hb}q)]R]t\uvr{n*}_YD=x}mhq hb}~W&?Iuo/[c"[{!5de2ڷ^N9pkdą5z_1?k>}0WkWvkh4y\D[h5z_1?k>v7}^w*\/pxssHBaYYa"`k[A'F7 B OϽ/5z\JKq,,sP\Ihԥ+~GGѼ. hb}~RJzFXֵoYipnpV OϽ/1z\Jr:=M.~+KZ UӘS/W [sۏ kV sms&#^ngdwx}O 5h!KDi׵-C&p`]$GܺkA{ _gRV{?ͳ"5$?v~~>_rV{?ͳ"sm%3+smկlȯ LwgRV{?ͳ"1$v~~>_sV{?ͳ"sm%3+smկlȯ LwgRV{?ͳ"5$v~~>smկlȯ LwgRV{?ͳ"5$v~~>c}Rq Z i?^$'UCݽ>}Y=G&lpcJg/Lv>>_nUs?A/_3$ۘa}yW1o^$_wncW/?9? sc ĒK} x}_3Us?Axdw_nUs?A/[3ۘa}yV1o^"/;1+osc ?9? I2_wncW?[3Us?Axtw_mUs?A/[3$ۘa g%<$sl?]a}yV1^$_wncW?_3Us?AxIݹ_<_g/I/;1+oKyV1I%v>~>_mUs?A/[3$ۘa g%<$sl?]a}yV1o^$_wncW?[3Us?AxIݹ_oKyW1I%v>~>_mUs?A/[3&K} wx}<_g/Nۘa}yV1o^$_wncW/?9? sc ĒK} x}[3Us?AxIݹ_<_g/I/;1+_^uC }G>cfۢ'ˤbgqٍ{p ִ? endstream endobj 131 0 obj<>stream Adobed     $$''$$53335;;;;;;;;;;  %% ## ((%%((22022;;;;;;;;;;|"?   3!1AQa"q2B#$Rb34rC%Scs5&DTdE£t6UeuF'Vfv7GWgw5!1AQaq"2B#R3$brCScs4%&5DTdEU6teuFVfv'7GWgw ? r֞}09hmu-ޮZ4UU~ZQ'xSGw?Fu7\22KE8n3x2y7)چ1u=7sx)e#%}W?"SZhZ`wx7е0w_ͩegvm}l> -T<~gR}]M:5jЉLe%0( Owx7З;u慻GA;}^'/sCֹGC+c\f`u/!`t ^FÚ)P@,_.S OӋ4iB_okt*^Γck-9-K=C҄JOZNf/r>>)G=?DܭNkw ~ӻ[h[. Ǿs]`T*mm D;u7_h[k\6E1lw&oЊ6c\7\yLccӕNo/w~ S/>0kYlM||¦ÓQx&,ٵNo/w~ –¤im~ӻ[hKߺB?t+NkߺB_]-.Zw[_wЪ)l)p ӿwЗ;uWaKaK~V;u愿i4* [ \Bi4%NoURRŵNo/ڗ~ –—?t+Nk 4%R]-/l~VKhK 4*J[J^Э;-ڗx7ЗKhUvZw[_.o/ڗx7Ъ)m){cBj]B_.oURRim~Ի愿j]B ӿw ~ԻWiKiKŵRw Җҗ?t+Nk 4%R]-/l~VKhK 4*J[J^Э;-ڗx7ЗKhUvZw[_.o/ڗx7Ъ)l(cGح;-wx7З;hU6Vw ~ӻSaKaK>iwx7З;hU6Vw ~ӻSaKaK>iwx7З;hU6Vw ~ӻSaKaK>iwx7З;hU6Vw ~ӻSaKaK>iwx7З;hU6Vw ~ӻSaKaK>iN/dCK4@h*,? YqU|Η]sME{*Yp2"h #1㉇oSHђ^-==ҬYʒwx [uq~]o5q}{̇ʋ$\1D hn/z+Fw14KG$KA~v^Sne8n!G\/6߰5:s:{wY\bbr1@jo!9e1#bFU<; h'7qݥgϮ;Wn7kϗ u]G>k4'O#oL?3sK姌c4[y~0cǯQ}6]7ѱ?vX5zײa^8qǪ~S첯9oŏD륿+ȶN$.MJ8 <$6\ko6 o9i, u)\؏Ŏ\R|^܏?ڗz{@0Gp|/h0wXyc d+4LzVԿ`3FxYmgG?0׿#nG^3mܗ ?r_c?/={+r?jQoFxYmO?_?'?`1/={+r?^3mܗ ?r_c?%nGFxYmgK?0׿#Ŀ`1z ?r^<,x^ޓzV/OgKg/1oU|^܏׿#<,z%?xzO~[b_zV?g/QoW/I={+r?K^܏%3mܗ7*E>anG~[bQoFxYmO?_?'?`1/={+r?^m`ņ?@b| 0R>ZcnG~[b5M3m܏7(E>anG~[bQoFxYmO?_?'?`1/={+r?jQoFxYmO?_?'`3~YgWz%3mܗ7*E>_fGK^̏?ڽCg/QoW/I={+2?j_zVd<,z%?xzOYgR׿#PgKg/1oU|^̏?ڗz ?r^<,x^ޓzVdԿ`3=FxYmgK?/׿#fG^3mܗ ?r_c?/={+2?jQoFxYmO?_?'`3~YgWz%3mܗ7*E>_fGK^̏?ڽCg/QoW/Il^)s^I8AΓ!Z=?#Սq"aw<,z(6qY##JUOzfec75Fng㪝]?Xn[ nBFxYmg@_)ygL!ϹBPv@i&OsXmq`q3g/Qo1G|sT_x̎ZDf=zy\rk}wڿ#߾clxncˀ p-'ONkgۜ?QYdcYf(\15ҩ>_3 7Egd]ETcJC[N> l0,[[UW^֖ItwW/ hȑSGm17Oƻkd}nO?EQB }vu5X%=MO\9W3l{Nc^FEpkdn-Aϋa/۳F2'ǣWꡢuNqϬm.%>:V1: 8bŵ"鴳n{{|ԭ~/ꭣwelφK4}`%oWv]eǴ0pY0 ( uV^>glkZl䴗h?J9XQ[( cA>wG 9Z],,Akm.sApy"pumhk,S q٢wٓ>cMmx9$Ou{tisemkgY /xDLA r*S s/O7- *5d[ks6>iNZ'`Sh'Mk,tߣsx%?S sTe}֒ML5Ad7CJL).~IhȀ*Y>oPj}KzY1kKX h{`lVyx+}k cMz#m?E+}Kzϫ@p6eu44n;rvoQ9Ywc3N܇4| {~>*)9ޗO',V5Z!v8}g~M3ұZZg/4\ $%gz_e??xyaW[ë`Gn;%{-pApn1d T-ف3{Jzutv,nPf^1isuk:/ǚo5G%`n|qN҈ԩm >P_cWjYFzaqz- 9mubke4x@ƣMdRP_cW%__d?տ{Hvl *zxzvs;4Rx~5_XoJg< f8Z6W&e2XݴEWam>l]Ps^lZ` %:Gtc9\1E{XW:6ևc-kו/RVv6[]pdZa Ro"4ikk; m/>⎚ vtmupkZ1q,#QÜ$X/40 e֖Cyt{-`[^H@.:-Y]yF_K`溽:-r~Jt^z^^Aacѵ:,t+;E d1a{^YcFXwDpi}h}=631\ޛ2Gy'M]6]o~59c^?A@E9ߒzOC}F;Yaf7Lk8, c.&6SOcԵ![@w7ADk#WoCֵugkF5GOsSӼ+ c^]ul{51w&kˢqMuDzڞܦk{\Qhnȵ#B Ro}vwL~>N;}<9׍R=U=ֵ^l7p0x"t_7{- $qyh.scī-*zF%90kAmasliv$X`NN"S4y[X{/*a>lh[WilS'sC[OwI VF۱zX/cmv旖x+x_mDžqGT]3 hD4F#0̧tUF >$;ծ}:~i [x_mDžqGTᾞc]YV K Z`L97g۫05_Cid9k Sm"5??)qf&\iy.%5}nGSh m6wlchg[LV5??)} Sm": ڋښ ` YYxB]ewYeLV݌oh-/jRƧgE/qTӿf?_:wYV>DžqKx_m8M;c۬%cu%cx_mDžqKTӿf?_:wYV>DžqKx_mM;c۬%cu%cx_mDžqKTӿf?_:wYV>DžqKx_mM;c۬%cu%cx_mDžqKTӿe?I~3gIXƧgE/jRU5ltn$펝s1䕏jRƧgE.?S_N?I~3gIH&إM]6M/h0Hڗ펝s1ӿf?=eƔ&&$28Je,eH &'VkcmaQ_v9?UU 1f?*1XQ z? k,T-נ[ ?6hn$Z {NבSÉ{!+!ٹ3hydl'^ eݮ&F:܍k /ݶ~%簾[66jޛ}Gtohcm< sXERwԺs2)f\\7>'7ڰ͂Vdf%O7֐% SclkIi,!Ӻ RI$|U9Ye,/+~tKo_ZUJm+T;Xmپu4U#1o_cmdsL$˪cꝶq~D4֗Ѭ"t[c\֙,;\<  JdI$$%kTRP/<TuM[`.c Ԇ$IJIB<<5a-3i!BܼZl[kYc0N$IJIE2 CGĘ I)JQi[U:m)x[88ߊOcR 9]O~]QQd[>VUQYI(şS$bJ/g$%şYI%2IF/g$ILQYI(şS$bJ/g$%şYI%2IF/g$ILQYI(şS$bJ/g$%şYI%2IF/g$ILT"Ndc!ʾk^Hku1j?o?-W*;8emhJsR^[cv=1;jpTyeZZMulk's_$ll;ė5.?ƨSYw% )=cév7y?ы濻[?1=?Kfg k,'??fZx$wn$Z {N K=ӬhAB:qǣ`skp` kCѥݽYvLH57Viak\Php.D~j5W=F]lK6N#gS&L\WXӚkemB1/qt:XW4ݭ\7{Boz]Q}WPk_.cq6ynIϵjK4w7nuvkܻ]6cwwCK[JgS享>\K\QxQg`_C첺=z~zGSN{SE~:wXVZX C|eO쮣kc6cM\緧9Hm;Uok]S}77vuv>"33k۹tF3Q9v{9?Yy6[_A~E\17MG*~uR}SO 1{6Zdۡ t?IwMmkfl4qV/ON̻&e 9T`s7{_iVKp#Ms,.>g nGtI$=g;Qz)a~w_Xdg[_~.3iiqfU6818-jwWIYmWZ6Oa2{*ܫgZ컰MۑfQf1ٺsʷX2_6k D_mVص5Xւ*g]AUY}v 0 }opj{ԛnrl`WcƢlM4܆kϘ*՝+Yc=;洁[~ ko[[W]Vښ_rR8[\dL9;]\tU`?jfCou4ZbѼ[W}d<}j jsF:,.cHCOW5YKl iv&*y]zь5\[h76(ug v2WhƝ57e]+#o7mw:?KcZ-x+jًΣsWYX㱾fcH 0`u`y,s] @hy-Q׭gQ/Ʊ*=˚>9jLϲ^[m [,yNK__nXن2 X=]Y`5z! v{NC>Uc,nIeр.+G;YV 2.0Y`s>ư4FρE5Z궷088:dtobQa긖6l5ŵ|8*}Yeg8Zl7=ۦ[~c]+snk~yB:~n0ǵVjqqGx+ɩ%ogمm6+Ct9­Ġ~E2̃E[]>ΰAD,nn?>3! PHiղմ{ w1ycp?"C(Ƈջ.{\XwH@v$f5c[1$W\ XNGdەUjX}]$sI~.cai9Rwscase׸ Mĺ˲_]XfE޽s! īu6\^m#.nȤTUeմXכ]?Fwvs~6Fc9NZ׸~!gY"Yc!Z:]]qkkf\["OJn?Y[ NIa7Vel^wdeJK*[|ۜ- !3kĒc]ԞX,sl@׼~`3~aC\ZּYs4X[yMշ=4|KR1vE`㚎1sXӷsgteۋX7b}B`e^.n1!8&͢uZ۾[i8 ]uY &ݏ_[˷dl7*S[=6XGv}2肖~݁t>1i-Jϫu_q"P[xlgB]CquV5Ye?7V }@c}Θ}/45ׇ[TC"y].rlcOKE`ǐ)CsRM>k5V]UaK?j5;ռ>9څ{%ֵ:7L`|*!ֲou}E{?k↾EpڿM[f9ΓsgR=F]{[HqC$to$K_&+:p}`/h%his x}v*Ǣ[Ṧ"Bx9 |fh;c:+xD=@z1L-2^Q'Cbvx/(1,~kT-נ[ [3 7E@?^-~5M ǂȇ>0.qduU4S]s vc}x?+sikqkL@OgQVc+0X-> u}\{qe4A2F G/ VmmZ84UHn{iΥ1XC|<]4YҬկ㼗`b`ebHCp u!CZ\o^\2wLN,k>n<674Um~d?cnOk+k뮠*Y koΉڧt.e͌4O.ETvS\\Vޓ=+66׹ lL{S.kZqymPy]l:)ѿڞ`K 7n dB?HZSO"Zcv5Z@V~~;[YMT6˙e3r&_լSe,4ڞo뮢dS_S{-`h, ysCm1&F^Hh\L 5a"7 ~Xe[w&<ԭC˵#89kn-휼AK7V)aٸm]0qE,1C\<{:+faPG1s`}a|yY}Mnu\?p-h}0ӷ4Sw#?bGXa~w_X1GKoUf`bc^m5-R[Ctm472Z u\!TtݗO"QKj.lT=MG`SQsM{_5dvoܱg\Cq,ֆl_S[:n(W059R]%w,Ӆ%jE[Ѽއ+RecCX$4#Wm՗H~zM| //745vvfI3Uc6d_̭kY!:zRo5Y}+cAPtlfuy\-5dQӺyᏻeUv?]cxw GP+̆_s(~=.Qmw?[8[q9H _zXȲ{\}OUk&J;0WM_C?,r1\f{,SMvXVF+bݕvdQwmq!}7PԇA?Α]mm>5qkgi!ĩ4cm5^X}3&=V|l61˚-#:y0]v66EYpkKŬ/\jAv*kQ뭽>GLfS cӹxi:[uA{/vI9 xG>nifZC>=fݼ,E *ʭ칬nqsq'PQ_u< >;cG*MƳ/ 55sݎ65Ͽޢu-ѵnv6Z0_F=WVuO=fe~ȭFޖ,Ʊ59 m sk 0qZk'gseS+/^_U_ujrͮhחš: /<^T?7jQm0ǚ%s6? `Bܝ~]QVGNC,mV_+T*JmMkPul7TEA6YCtù!$ጉ2S'ݟ_W׺aT׳Wp߯KcKۤB9\JSf=s 8Đ+? H|3 ^wgauW*o0,q*_X~C2͖6m5¢USZַGԠ>,0j(xnC.s}>*!K&Iϩ';CuH]"v6lnR޹]0keQ[`wU}xUS57\+snaA~ꬳa]]x~={\t0 9h*d_IՙpS]MU}sٸm'+(/%n\d|gpD"׈e)DoZAQK܎eBt̼B4??!U`4< '7[ w^Ϣ߀^S'Cbf?5%?Z 3y?Kfg??fEiGDP?vF{ǧYkIg ~u7Q+[k\85ZSDXӸuTU4\k ZwWZu<m2cU?.Sǫ-XF־^_G{ksA'WI+5[==qofvFSAlwD'_"?GTeRq ZVmse':Pkm V–opRӼNJû:ĺw45/ߡ@}w洌z\טaXC}#{?ٻYf.4g\V3X-Ti %+sfvIDz׃\imXiPyv>;mk[7tv7f.x~W9{S䲽bˈQ{[{ivZX2ssDzKiqXqt7E3"@&yi]U Ơ4X`mkh`5=gI/?g4:zv6.T=k[Q}+Nݳ9Kyst.]>Ůƨ>}4?hRl5$x?GOzlU7:,/?,[άΝK&2Yee[dWVwroUZV Fzoe[k,»;iGNTɉ<[[-h 86gdBvE`e9[]n=7]x`2 z_Gơ4ztovcoqƭRtMls7:eq ,n}B+þ-9m1֍=T֖UVN[/c*[k^+}G?Íӝ5kS`2#tʇ\V4cۧ*-u޾Kw<o{lkIp H`Gi0fM4R[k./cHJ|NnFE]`S #edt%obcY]6Ym[l=\Of5[}}[# ^ꅬ:vK f,?uV /o鷈OK-fasf:SUl {o|ݘ?N^ s[W\nukGިu<VSk g.՜Ɗ_Klq!<װ5Ii_IXU63ӫJE??⇪}ae7ԹՀmsQm.~8~8kN;(΍V]ܒQ: O# `;Xj;ݽ,!ږgT4э_ٷQ`9.ຠٿٴٹ?ʽ3mٗUY{\DP?J[>j٧q:%O7 K^^]k}"OxRb]ic/ccSuy{vP&+Yik=TCIicYlA"7nF}فXk+&{z'M,k]Yl׹纷Ak\9@&[BP6WK?ȮݕMܲs\nhq wg-HWPHsn;.eQf;1*f5l H%U$Ԭ$r#kvXԵsSnfU&}܋r,7tĹȎ࣊R*xv X~,q2qϵEBEcdZWV=uхI}!eZ|t]e\;\,mj/j_ 7XE67ut_Rm׼dnmN׵썣$~'ΐA* W z'/j_卿7v]ֺlc\jr6soD|SG+ǯ`,q2]Z@c\UX0STW3N=9& {-/c}dL(ff@GVoD;cd#3fn~дlnu]gGfC0q>q[;5Y͗\}UK A}evH|KxHG ý Zm&?jdld޻gG\xMy42|oͱۮEO=ZlXLX77M~Tԭc5j^*ߓk- @ݹV1m6<8mߢGr::G=W4qxXu Llo$D:rN;i[ qz蕹C,].7#PDZFϬO{˯}覦 ۿ|ُh$ᆢƣ ,mw~ӳi: Co[nF N }yTkɴn-Cc.0dLw(9#޻+'M#>-v;Ånc@3|WTkY[r FITy%frEЈRI%[O*g#Cxw#a |*澅'7Y[ w^Ϣ߀^S'Cbf?5%?Z 3y?Kfߔ܂ڪҷͺr}R&|^Ÿ?t,ô $U{0 o5!SmUdZ,hs8Y5 5׾o:Xvý1QB|Q{,-05O)edz.p2Zݬp=k;vAq[ Ѧm"0N'֌.|+"1s4-PX)47`sHX}g}|g^mt=@k]Ymn{/  VX04S?`KJ<dž$Gbo#$\^_ɳc2/]dՓv!FUZ a?D\wuuΔ1o]]n7VCo#<G2ϴ{YX50Q1{?l[GI kDhsyD!|n)v.:ƞK #G}/p6x-y.S1H^|CWޖ_J1UV}$=Ia-Y3$43eOl$ȳٌ>}5\R?O[p陭mN 0/nndukݿGM<$2 DaK;_qz,/?G#?b?KoUUS`mtk1h9µoU?d]e_p{U{AϦ{4Cgf _mv7!Yh}=0\~GWmWn5 `Ӑֲl}-}_KtG֬|f}Ne׾X=ΰ7)/o/S_/ Ynk.cٰN>[n>F.Xk7vǂ~akjPrc\Z+"4:$~4x>{7{=_Rw=gDV0]C6e:C] :>q8h/ a0*nic&t5{tq4vn96jxsm6d6j^f9Zsw[$.o=/.΁Ժ{e[Za.5s{? *WWe"-Ȳ;s7>y{»g֊k8n .'|:Ig? 5YIv/ٮɶD,̙ܗăҺ7NϷQļX]Mu{voT0z7m"a 㽅O}jγg=X,y~50Zs\[wU6H, KX]2 _ҺئWmڽ{ˎ3skL>7,%u7LKinxKwunsx2j:p1^}GW`i{u6Z7ks" G[ .*[VV=AkmKht IuC~K/i-Կ` $8Xa{^V.۾Zo̶ڝ[é{Aaxs-s a"{d;*W8 ̺m'_sT׆z7ݠ5::6@dmχNr|??"ˮeiŷ֗81{EpG:P ul7l9~'v㹭-uC&Ei,(]+%>Ց3&ʛ,{ݎp<4k֠w]/v/9xS'#?ܹzfns\/!"t#{Q7#ۯ@$*afUF?XO9Sg\R6=v#i!3M1g7nNI%ub[0L!)q¬pqmrRLCe$IaF}򂪀KxVh'iEm +8Æm|9?r;F'`s_L*>?'l0۪Ůs\$ap_T-נ[ ?j?_͓Zրր OJ)iйu7t74:OxYB2 co&PHؑlr}Ln0nOTQ>S^ݕU'iqOVV -{KcQ>gQVe{,at2aKK tz R\V4`;Fџ[=K/3sOeWt܌m9 4׽ŏ ӧpUV}^)ߏQ6&.t݂ iȨT흇0$6 sÚZwC+Pkʠ1;G?6mgUjm\,.upIN_Y8Xscns(èxɧc,pC~{*ٽ gQUyhuu9lfk\]9*_VY7Y}܍j `@!D^[Xuoc#s@"H>j7gavspv̘T+Ux85K^ujU_n[Evn`6>hhdC*!8i<S۝HV85O}uoNۓ[k{+tz  n{\pmEkil)wSrq7dͻ7k @IIG{ {Ь~:`%8WVLn}Vm9mȼd+%@lu$QU)/<׆._鷾r;z5{ vt)cΓ^1ݎ_÷!ş(׿毯B]zV8bß^N]={7a!Γ^?g=sz,+:_*WEb pa]Bkq>PGN8ɭ8Wc=]eCSGL1=@۵o'A}Y׏s]{Xsd Ike7k *Ku\c>ՖY4{h|c~r6\[}b+ -oP_ث g;jATis!:Qia}-ivHm#XV7059$y},~8}-;bH1$r鸹ͭ~s8aV:'IC\<8{j8?Bu}LXX\lW=Kn+ns_S_6Ӎ_:K`JmmfHﰸh`7D:OHcA6׽qNwlEvl ,}g.-obAUcrY hsq ;_K >9۞[[s2݁L5qXݿWSk[Vn$Nэ?aYfAE-=Cpk5<[W*$W^}[uη|uxʲKY@cvׁʥm]AzoZjcXehg>^>N뎞?SƟU>v%==̵Sݱ}HNG`f Koދiư7dZk;wkQWGу;|7\tb_@YERmEZڜg\CNw붻c*%YKc/u<%h6 {}m-}CpZѽ[D~wOuzgG\[`5K-nU] Cb0hr}د6+/я~m۴Y=Ct01ip ŝ>}UʚY<ǒ5W ދb!ucs}"3^4^ek>4hn#Q;K2>zs :;KQvnyH|7+7zS7WkKmvcN.>cOQVϵu/.hʰ]zV&v7y$j7_ l2[sͬ$(lI5)*~ \cOIZayGы9??q '?'l0^Oٟa-z`yQlH@:|"xTP5:͌Z`BE6gSm c\1 ei%449uY6\]5|jњ b(psCYqt5'oUwe8z}dm"`"VoV^,%/.en{X@/yh;F'^ni.}nI$L^M]5=[iv~{~hScvzxq}4B4rj0XF>iQ ={KZ:`he$456O٬ɾmhw}ܳq>k}cnK@熁S7x} 6z@'}$#pE-:7-t}%<[:@8_鲭muh{chu>1i2ŏ,kKˉ8nsc3*=&i]\$ fFwj=S7;~uy47]u5 k<+X:Vc1jm6>E@z7no.$oJI$SRG#?bbΖm+T:X`"͌% 퓦jT>OI5Was m=aTS=7 }mkCwn8D`uLq2}g״PfǰgHEwBnu;K;+C)a1Ƨ{kos=s@q/ypTߊ\>Ջ^EH"=xk3@l/^XX&,p.wUy >iq}K {_hv@"{)zl ]T9=̱/-pNX/f5OXcYg[CݸA&$jֺ^ Um${CZgY.R+YSík ȸ98nY?-}KlرZ? I#}\>ceR7muOulѶxnp΋C3 0TkpXsǂ_M]k6/ec< v k᡾jxTlnsɉg^m;3m4bvzۃ:~]czud-\Yvkftl }{t,7wvqѺ)}x?SMŻdYgL,c [aXs`cNKjkȭyvvh2v&Y'!˪}R?Gaix=9ey{]csZ kZ!w֜fzUan;WOUn>fCÛ^tKv|?gtL=@e6kAuӳߒ>7L·nK k{&@DOi cN¿ ԃY,mMeqv[` 슝]m!mwvwҐNeubVlcnmltc5l<׹@5&;G ?hֻ5Uͷ emUMgeE;XQznU8 =; 5&w"7n+{ KK8I;Mecek 8*]?=}2\,o`l9aXD+() ߤu/T-{u(Ca0UL¨y d4(lOJlEX xc*EHP?U3 N_Y}n,pkS]6;E΁pMzN[_Q}G㕅wSeH}9>^geK7u K]ocoF9]#sC7j}xg#s-?7% Io/$)f?F~暺b9'B%NLAfN>osh ,>AN,w>l䚧>wosy=G Y}, _I_@tm k*wHjXCLu-: M҂ZAtagbu}w7fn\~oNWm􋉨c59v9sFiٱX~mlaaf+Zk\ 8umy{Ûu, k,~ƀ=5#p;:>͓͟wq=nqŌ:kEN>]bӏzYgv%S|>U?EK> &[0onF57jp6Ay~n;m}nNs-gp;k^jt}?F~``̞uFT~EnM]W!glGa:@/w8ANuLe;ihNh\3-:d-uΰl~3oqu,qy0uV_8E&1-ÃH)Vܫ:v{ItrD8{;% >eum^lїCyȿUu6<3~5[/XSP/JӹmVQ>5MNuXk, `dן[L]uέks`;>J;k Kd?'pbZ`@]-G2dPRI$rU?Jܚu9c~ݠ έF˜;# xS % & ?w^ |Ww09/[3 7E@?SĶgo^oX)9^-~474:OxH@:|®5V -{KOwYeّYXsh1c{GY Z` EVQu k\5ceI Kewťm9QsM{c'c4YWk| YeoǨ l? O7^̷'nA$l5NU*OjIiCML:}?]y\6:ڃu{w;w|~v fs[ -x> W/eaz zu 3s^KScU\=-c݃vGӺSɶ]] .om%B$I)z)a~w_X-~iO;LߕO~?:kUvS+UY8<(/ʱoUo*@I>,-V~ aeVavE8RfNNf5Yͤ8uv?}lWM#K~G܉f]+œw]/-n75@_hVLk]hTC١htJX볝kh/Oѓ_gƆmhh>?.?p17Uk[kdX~v'V1WV3Eu"w҇ Hߑ%#KJMxX]MW/ƼZܒ}&1/OuR֬jZk\+ ;/&KɈ!ѭv?K Ҍx@Y(mD<^Q'Cb cv`O9??qϏ/[3 7E@?SĶgo^gX'[6jSB!iW,zCcƆ6*kYs -9h{\fPc4ksU>Q{,-0Ű]$2֝u23[sHae!#D j{n} ad艝ӫ̳8Y͍X|35apV7/!NИ{rQ{AؗyEn~ ]lɩϸn%ÙhVNGLk;Y3`L?tb!UiZ&{ww%e$AJI$RI$R+oz)a~wO~?:[UykOVm=TVvl>L,W^ H [0~ȹdc2SâjnéX axuˠYAo=\ з+Sv>;vk[˧e4r.2@\rƈQ6}B&8OoDzMpo[:K }ʅnf$+Zee74}QrK*%}הb7VwT=VlE63vtBe9 h?[/J?ޏ5APMmҏ {/J?ޖҏ$(0z[/J?ވJGl(0z"I)ҏ${/J?ޖҏ$(0z[/J?ވJGl(0z"I)ҏ${/J?ޖҏ$(0z[/J?ވJGl(0z"I)ҏ${/J?ޖҏ$(0z[/J?ވJGl(0z"I)ҏ${/J?ޖҏ$(0z[/J?ވJGl(0z"I)ҏ${/J?ޖҏ$(0z[/J?ވJGl(0z"I)asK>k?w^Og?ac z? kο)"Z+n\?Kf]]n-.cI.HE'?طQm3kI0f;MYUy9sB79> `D .zN=8AsKK7L4͐ ?J؄~rU@EscϦ6ZZ{}X32ƟRVS@w?{Y[5+ȫ]sU2skXmCl$9Đ!qjS#)jFF뻒>uCC2]U5Sy-nm,-3@qq]L9.%ֺ _4]oV^,y{X17u4IDwTk鼛^?4S}9m9xW[S}'e5j{9Yg.cjuջִ[)eu}䮞:[}u_{40u'qXNjXYcKh\:AO-:8>m5=sc^\G?dbd(7ѱۃl7f灴n\H5Fu{(5ES{vlHyo=%I1.7{:5An]FUh>NP7lxeCmOկai fI$$I)z)aw_X*XWV+?(mXe^m+Tcs>LyU0hbtn+5}M =6xTz{/vK1m;hcw0"5Ű}FpSیO=Գt ͘tQ䱺ꋚpkA'ac~یKWųbWUM1rY}n9[\<jϫf&MXѾǿ-@6ʱ_Uϱ8uޥG7۬Haˌ k*eaOOQU=KgG7۬ du?blC3cKUh/uޗpnO3cKUh/uޗpnW3cKUh/uޗpnW3cKUh/uޗpnW3cKUh/uޗpnW3cKUh/uޗpnW3cKUh/uޗpnW3cKUh/uޗpnW3cKUh/uޗpnW3cKUh/uޗpnW3cKUh/uޗpnW3cKUh/uޗpnW3cKUh/uޗpnW3cKUh/uޗpnW3cKUh/uޗpnW3cKUh/uޗpnW3cKUh/uޗpnW3cKUh/uޗpnW3cKUh/uޗpnW3cKUh/uޗpnW3cKUh/uޗpnW3cKUh/uޗpnW3cKUh/uޓzSqaO-ޗ}BqűȥȪGzkk Rwe$bF,ci A^S'Cb*OcG4?Y?_SĶgo^gX/?l0OyQl5 !@Kk}m~8YąEp4#mot UNgP >魮kָX`]y 0`΅g}sjkhsp*XLel GMpb8Eoտ12.`S_NMgkAN׀6vXs1+(Pt,`zn !Y];26Ty5p&h<)j5_*W^%캭C׼ۧ C_iyy4 Ivk#S%CYQ-xߦ Rӈ>'N]c8/{ce ?ꖻ}PFqasüK 'H`CgBonPk0U&s;],uh5;F6NcNŶd%U3bVEn68}`.Zmnk^ ̩!} Idz澼rlksp=87MFͮK.tB"E }!pp`kCѩ ɺ?gܲWq۝q5lSۘ$X]0Gyv! \ڜC[!ΟىfH۸L$Go}?Wo1x>(_r_Y5:$iv}j۞slsv67k%KなgOƧ׺ȐA.'achsF4H$"&;YVNO*_4ϲZYW>Wޗj#⯹eI\-^z_eK܏kM%sy}?/r>*_4ϲZYW>Wޗj#⯹eI\-^z_eK܏kM%sy}?/r>*_4ϲZYW>Wޗj#⯹eI\-^z_eK܏kM%sy}?/r>*_4ϲZYW>Wޗj#⯹eI\-^z_eK܏kM%sy}?/r>*_4ϲZYW>Wޗj#⯹eRVWޙ4 e#@gYvGW1!'[cuG`1L6?jG=[s][`D9RQiչ_r^['Cb*O03yc?_SĶgo^oX/?l0NyGP?@λ- dqѴiY0PoۻoiE_mOt ýO[V`U[{{Hf5dnxV?iq]1Xi%=S~C6cS[<4S~>Uy 6cj 7FGM,K%puqQ9Y8ŏ=`DbO[>I)&㇓_q,Vl.6@!V_nzP˰T߷VWky1:W:?O^O$+v{ Ֆ-d2+ۿj|:ޝq{GwDx}Rͷ+ jɴȐv'$%]K+21Xr\ʏۜκjb}Wk1q˩{,{ں~YvklK"qH-~K^ۃtOn7Oռ!؛\8V74o҃E{pSִn0ny#̣s6hem[ukheT]vy k*^JoI)I$JRI$I$$I)I$JRI$I$$I)I$JRI$I$$I)I$JRI$I$$I)J66kpJp< tWoKR97E"'p`FPwY+-zOUI5yoшgL*>?'l0^Oٟa-z`y?fEhv8]Q{ Z` MWgQu@k\9&VQk_Ul Is`묰6hͥW1[[!O={TֲEYTUfsY !NCkh}s 0z8nc.-ISӀ&Þk}@O|G]v6kxäϴ.m5*c c{龬sI53ck$wZ߱PkZC,'h!$JVuˋ} 6K .|+{7E9Ʀ`c>ͷ[{ڣ?fU{^ڞYc\o9B߫\ i`uFvUbwcHLW8Ϭ=R nix7ǐC}= ]^(sM7d̂ǚv5hxvtWMȥ `kv[*z_me!6Z&{C=m~uͪ+o\GԲ5E?X mCݱ~TFvét#z$H)z)a~w_XYK V23 Q-jgBKo_ZЬSaD-%?mD)lK_][J2YWݍu.jp.^F.OYmmJRF&K~E5]=ŎltFI ke9_UZ6kc[`.ku:b^pCKk\^G{wҚ٪W?ߒݒܷQYhۋF<{$Hr1s*reH;,hsdpaVsketXFښC[+i 8[ ZEs:c4(CtQ[zLHbUSL~5ƳkMmwqXFXc'BNݞh߷wD2I)I$JRI$I$$I)I$JRI$I$$I)I$JRI$I$$I)I$JRI$I$$I)I$JR@( JcZLX~DJc&uI5yoыԪ|j9?^X9%?Z ?3y?Kf~gw?`j?_͚hm?"!Z6iӎWdkuhkE.Ƨ*̊țױ:d K92=0`ΎYi.ko99eI 4[ݑ^V&GٲV\ 8AlPFU}ZynT>1TճROB'Z^NU8NcoMk =cHݲzޕm{lnco?xt_c2|\ǺƉ}ovd~7iV?_+k[kimm"97HϊFQʛU,S^dY%dwW|bMLx5^w6@FitGwRNە755};bƵ XvV:m8T+v;eV vṒ'Yv70Gm sJ/YlֹŰ kkhk)+.k.X\Iŀ_"C>^k`m`anl5u{mOGE-dTֱ0T]ak\-$Ii"$wAI$ qz)a~w9Ye,?+~tKo_ZЬ=WVw+?oT8X)oFAO["2 Tɢ0m5 t|B^C{ }lyk&aMƢ[rʱ6B8:srõϮpDγnݦ UzUYђng?`$C S{3+ȩΚCvh),~w~[C} =C ;<|.6H-2Ӵmh!u.ֹtWOhsg\eN){ujo.0v욷 V7=&Xp]{7V݂=B- -|QѱٓfEV[W8nx8{ʋ:&3*}^eu -~{84o\Y]Ջ%`l c YI :^+yas1 8 aa" ^dęM#dqjx! 2~:mq>-, ck{cwhl@xU W s7p鸽%=E}\X|ې톻,,K5Ūu_̛͢/s^[>P5Ԟ]Uѵ̻\׽9w6VYe/k6Xy=7RØ,q׼eѳygD&wMد/m6o/q{\] 3OJaYE?%0Scc֘pNγf}9T潦Ho .dlZN;l* 8vk,8CZh6yILZ-uW:HV8k3rWl5 jؑ S(o_"ϰ1ѡݪWt,[Mʆi!ٺde76 j.%%X̽ni9hů:bIII$I%)$IJI$RI$I%)$IJI$RI$I%)$IJI$RI$I%)$IJQ%>ILkCȉZLX~DӲCN9Ư-1z_r^['Cbk0?_SĶgo^gX/?l0NyGP?@viWdCZ[ƀIp 3ieqmkrkC]X/iw7MG/pVv59VdVZMk2v>;Vtz8nc.-k~v^ah9iawIP2>͐ƺc\-%42͖:eu,GK:\tD:#4e]kV=F˜ Cuʝy6*as_0Yۺg:^Uu݃wFFϠ >Xѹmw~KGE:,?l)}FA07kS acjsptpPն2aEV\?z_T&[cncvǒZ)O.nؽm*ysvwp'Cu.nVU6Im{li :շm,۬-x -17 ,e޽gNl43_Sbαӫv#Y[eag~:f7nzܷ?{LC_ck^%JLڽ=n4 ͭh%7NX;=̻v68;k8(?{ޱ,e5C6Yq`;~\S*XWV(;gUO~?:[LVZm+S;ַ}ٜ,?ŷ# D-H,þ4 ɦuqU|qcm;\h)??^Tz??^IJY[&|ޜks+.,790cHѺ:vF[Un2c<zޙ$SֺŸɲ=nXx465f[+:X+p1G*ULmuF5y#_ kΞ׭wNcl072{XCnF*5BVVESnE-qEC@pVH{n#{E9t(=r+ Z[Ai0jul#Z455vWw]96sc{X͍p"9)tpu2/h}%{qv>g5^{!%)"D͵g2<o,cF7A[$fwVr۪uY{q kUOPa=坕VXuWS8\ك0ȁi;-~ng๜vYO222} 7e7V]`t+I8S+ԥض9uM8?y$90=8rWS35 ս ~{KU?Mnۓ.i;K-v0t)9x<SNfmaN_ȱkF[/&-wi[x\yBJvUV۪55c۸i:{گ22dhk]fh;F8GLKW5=ENOwΟ[7}§Z[vΊXaa}60Ew{K+}$?;:lsnkދW IgVgĴ\cwPepv릶\[9#V[C kD`J:}nm/Ȫoٍuݺk67̧;+ m29{K^i{C9+'1ȱV@>~۝ -:(V=l+hkD5"$I)J6}g))hw}1a+CȚvHi'/;r;F/RN_wwV03yc z? k,T-נY ?6hn.Nӧ"xTP5C*sZΎY&kk%85L>־@;h:-?6[_a }uOWڴ京q=ŗ7kKvwU=sU}暁E=momfZ0*uvm.A Iʗn +pc6&tΟ˺ԭŶ/!`uĴh5"#才:Ncq6kw7ZLqcknPsK,sgFSξ]@+cA %iRC:PV[M7ZxݥG)UϷ]V_W\, גs}ElUo!_ipcqWZ\aE§Vm16JƸνvn6=x>]xc{۴zuI"I]`̂ܫǧS0 J5Tqs5AENX徰m8uny1]k+uΧne[Ʋ{+%[@s~KAw̱F׵4(7'H>JWz*}5KJ?~GV.;.`s76Xִs GYKX_WV+?(m3Yk{LVZfpSȌDe"u^&LJTEs2ɩ:x. ):??^\J>]m 1p9W]rv+l0Y}m;MD9Ee;I.k#ս42Y`lc2߆c!Ա>+!c~qFIb&~+p\j6xhP'"C]?0MmL^A=hG[q$IJI$RI$I%)$IJI$RI$I%)$IJI$RI$I%)$IJI$RlIFϠRSc"Vw4ӫN_ww^WW`gL1T-נY [3 7E@?^#Qlֺ$m?"!Z6;Nw #[ײ״;jrmȯi65cF84Llqള沧=>3qɮ.`55c&TKektsN[lm \-FӢZ讆dʙKX ^okB_ɺ_~Cq C[b޿qNmbkTeΒ\%EYU>~K5 鄩Ƈ:Ƌl׹t!Uank$W`_X"7peѧ-67[<~QcTXvC~]n=xCH8L0&ROu4I1Xƽ=kxV}OTi>_6IYvLWW3.nv;}*Fƴ sZ rg[nuo-ǹH`PW%-SR5{y v~i=qp Cr[x)H{U}AeoU [S-jgBS+Dd?ŷ#)UK՟zwnݬoT*zpeEs~i )vN+_Ew9vXƸgBVqsmLm@qw%U?y=-;w9S\`+X:`"ީngO拉ީ KHۨw3Djh۰pq~=Nk,iym4I-kj44pt R]{]y=_l cDϹgulYUnMv_`}šopq-Ӽ'BvU]S`Ƒ dT2@ } w<z'R8d]flff˄=?7Iݙ]!.>[=-C! |gk+mtc(7mE6 > cbG㱔3=dŅg]ac[8jqDX$0x>K:{0&0ސ%ާ!:K1޸=Pѿo8RjaYhڙXu'u<.Ӑ-ֳkL7x٤W̖ IHd4.ۺ$):N5RHki,4$:%Ϭ}}FLpUo,ix76SKkqnT2qߚև uUwOV-,,ckhk.uNv=E)%%qOly,:# oJ295-w`H1ߖIijXSu6m\@Tq%V&= E.GҁMTgݐhٺۭ2潏[O5H2nm6ڝ^#D+OǴSM6 kƵteazGK8ޑ,6c^CvưzWSmv;gs*C觤uh}];\ iDu77(豯ka !$ޙG8y8]EjKsCnJzOO6 }[[[XfK1 ?Ks揇ޱu66lKɍ))I$RI$I%)$IJI$RI$I%)$IJI$RI$I%)FϠTl%1??"%hw}1aN :OUI5yoъy,|~OGOٟa-z`%?Z ?3;u05@ڍd/iNLUoҪԮdkEt9qv7 %+N=X:¼vsl{D vO?ֺ81^k{kִ_gYcݶF9s=Fw9Q:MEyUn{[Xk+y%VީӪkj wi>H56Z=xӔIT[2"L"^uVRҿJ?#Nj[-jgBKo_ZЬ7,?ŷ# D-HU=<%" 'mviQ@3NC17X淐?r561n7J_L*i퍧a{_GRϯך1aQe Wk5atwmx?Y[N7,v:w%n<܌潷ǷhwAj2(DZ[H;F`e1!By"\j~~ebTW[(js-l.B ]l5b {|8Z0\͢\FY^Ѫ4e*U$6n֓&{p˱.>Mvc\`clkv&GU~m0P6@ Zk9h]߅~ޱikneךּƀu Q:nFu^eQNv0^Yպm9845մާ׳'mǖcml2k}?3VSK`n\7umukimut2a~zrƓ!~c:nMwRS acO2"7OP*qNF#m9@X[me:s~p!{clupA-7 lಛe"@s smdSG\om{lm{N$ o*u*n̿eَ.X{Ӫ_گr>]tEӏlԶ*{5GfumsAƗ?-aef$ö7uڝqΣc[,i0`(ߴpm{us@\5|RZck*]Aas] F=C>ž5%=G1MBZ_m{ .pRͬه}y/bZnWMn i }ceu/]`q7X<0+Y[2m u`3 H)uWG+'E"+xwm-itgQƭr+wak۳۽^㌊_͂q_"apzͬ<5l!XpelV4=BpɷƢXH4:[eTmofṁyJUuv!ͯ.b뷍>:JKkt^xq묔Ar54yIII$I%)$IJI$RI$I%)$IJI$RI$I%)$IJQ%>ILkCȉZLX~DӲCN9Ư-1z_r^['Cbk0?_SĶgo^gX/?l0NyGCsZ@pPh$:Lw #[W״\;jrmɯi^?v3 {e59툵S/΢29,ssHU$/n})BʉҪ0;p|~q!SVKK[ofXg& 'wbbNj;(k9=kzYngk.> ׆Z:,Qm%6fM]ֆ7%mBY+K̖k:gQ⹹cu,q{ND:Zi$3E޳ܘuSWeqF{1n}5Kaߦajh-h.<=I.6ߐkZYxsvٸZ$}EeKKöLrpƮ3M 0ژC5Ftm$l]}s[C'~ pec=0kK ej${:`k2}&s=]$1?ֽ~{C<I/JI$RI$I%)$IJI$RI$I%)$IJI$RI$I%)FϠTl%1??"%hw}1aN :OUI5yoъy,|~OGOٟa-z`%?Z ?3;u05@`u,m884:K?[. S1 %SϤ^[}ahkHݻ.drVfa~uk4v f׻XEo!6Nj y-nRŕ[XՏ[&5h"38n%xd:mpM)fv] E[ّ^P[ָ_ 8 ]Xo7 ׅNϪ ed +{ckgFPWGEk=s3hski{ȱ uVmԼYU9iAY/Ik wRZꟵTƒ#_憋G ab8Z\q{%:$I$I$5i_~G&Է[g)3Ykou8X)oFAO["2,ǮdmIO @gPϮql2!m:C]U9'};,Xza#uN$ z| \LfU>692$̿[fK1ȯKŶ9l7moeZw_X";k˪acC[>)1u%d}XVWF4mͮ:œ{d'VC>Qixf9c {Zteb@"=_@̯f6ƐP}??%Z:}vcPY2ukH:izuF@{}V<>`:<4SX=2ᏎFvKD=&lP?'y{KE.hhkd~wd}vZ\I :<4\GMmƺ܌jdSm6qr= 5x=bڨ.Yu̹}ɞЗUtztV/Jv7W2z܂_e6kZXolOSMۅW²?s7;l[΅%:i,,{}LJ t46kX+;yF 4~W\{Mƽh{HYh[ />>Ijֽ!I`7]\.ǫ{@4u**Us`7%4R짢L5.qI'U)F;e2ݸ;p8+!+ۇ]́VƋ7oz\@K%YƖTLe7{ ۶7qޏur2\\l#pw|>k}lk;ki0\@._]>ֶA5Hpo;Gx\W:xc̪C/ِ-k ̡HhRꪇVvI$I$$I)I$JRI$I$$I)I$JRI$g*J6}ևwIVߤ 6\2 iAWWJ9Ư-1X5Y?_SĶgo^oX/?l0NyGCqpmv&;EWdkg=eNs@ZF:9g_EesYh iuVwX huPmUQKl][k Os_c>5_j҇sjv)UQý{ >6} ?u nnC=쪪Eݱ>W2kk\ MT2kmʪP@9gA^.ϱ=ȺrNEV=Ip0O%}J1V? $T y[:V_z%wUu!unfeSic^ @ffߏu V>7\X!GMUora eFK{zQݎnWn.Mxzlk$tN4_C?/n};UL㍓O7*YM;\9cGpޫ;2_Kgw~hXG;n]c>Fc\6SW=GX֚/[ZEglCL"#UG'{YnwmϬ8:ۜ[>r;F }.&| K]l)=W%q8&VƗ5-6U'dKkd^ͧ{wܫÌYZp}k6neMav`81 I Z^nu S1d?ҨFSnb͏Zq<65Yh$a({ AWWJ9Ư-1;<ז?a=?Kf[H Z?%}SľgoZJz5uRӴ;mw汉R*wCvN+YyNQc7Ю/ǵmOkxsTn8ey {\Zwp@0РJ /QBdamԳmn5ռ WGgQBܳmyxLjLvF ! 5:$h?60 ś&6%[dֹ-kq%XK Y:T^Uu_c{ƶ͍s=G1(}/ .e:chikZ;FꗨrsnҰ 2(;ߎַ[=?D;ycDz#*݅rv˯K'Gگkŧ9ސ/})ޮ} %/_O^ߋGmC?Iޥ9ޮ} %/_ޏ^Ks\v˯K_./jz^/۰]_nu }Uxpyv+e@$huK_.%/_y2֕K ޖ~Җ~exLޖ~Җ~exJMg)gQ~̯ } WTޖ~Җ~exJMg)gQ~̯ } WTޖ~Җ~exJMg)gQ~̯ } WTޖ~Җ~exJMg)gQ~̯ } WTޖ~Җ~exJMg)gQ~̯ } WTޖ~Җ~exJMg)gQ~̯ } WTޖ~Җ~exJMg)gQ~̯ } WTޖ~Җ~exJMg)gQ~̯ } WTޖ~Җ~exJMN-תVcCZlDi:tFPj^ʶ\{И C_./aܺ9JYdߢ}W݂HAKU'/;r;F/PƱەencp}^_'Cbk0?_G,>v'Sc oe7@~}Sq>w/?*9x~>/Wc~.?n4aíqo_Uњ<:OxSxJ]<~ܗs[r4ۚags[r_o KnkW{}xS/Oi% 5+<~ܗs[r4B} o|a/57"?V>W$Q C?/g"sY^_;s?l?]a/YꭟO5<=Q~Ri>q ">Zj_pUmK{}VklȯI7}w_j\>\`681%?_&C} wx}<_g/I/;1+KyV1I%v>~>_mUs?A/[3&K} wx}<_g/I/;1+osc ?9? I$w_mUs?A/_3$ۘa g%<$ݹ_<_g/I/;1+wsc ?9? E:_wncW?[3Us?Axdwg%<ݹ_<_g/I/;1+oKyV1I%v>~>osc ?9? I$wg%<$sl?]a}yV1o^$_wncW/?9? sc ĒK} wx}<_g/I/;1+oKyV1I%v>~>osc ?9? I$wg%<$sl?]a/?9? sc ĒK} wx}<_g/Lۘa}yV1o^"/;1+osc ?9? I$w_mUs?A/[3$ۘa g%<$sl?_a}yV1o^$_wncW/O="ꏲ}`DO IX/g?P!\?Uxfh endstream endobj 132 0 obj<>/ColorSpace<>/Font<>/ProcSet[/PDF/Text/ImageC]/ExtGState<>>>/Type/Page>> endobj 133 0 obj<>stream HWnF}W,5wW h:nZD좠X"~}g 20 ytf̙/ ;C$vBIl'v-b]ѷ獕pB4eq`|4 Ц',} P<$Wo?@,juumjOf$'M}ڦ^`,LcxceϏdG@rnYC8c?rx) 'QlaL}C ('KD|BL RsF;K𔄁Lb;y记#mct) ~BQ,wk[nJ1 A|TD]&h& ZRс$)fׁ~ҡObYE}} #EyVbHgLTmҴE.bpөE @f'@9+ҊHDA2]U#On4 )!PPAgj":Urw|t}*clmG>]gm+58w3`' endstream endobj 134 0 obj<>/ColorSpace<>/Font<>/ProcSet[/PDF/Text/ImageC]/ExtGState<>>>/Type/Page>> endobj 135 0 obj<>stream HWr}WH-ǘ n[79Z?hS)E$@d 8$ƵIE%їu g/}ixZuV'G*s|g?UP_~Kv(O_~P?v"Yj{.޼cx 0 X/3N$q~[2ڽ7USQǟ=hԘ%n5DXVm@>9й\1B$J߯_<ıS" T|N _$ " IBux  bߏ7sS->B  -$Ya{z؁k(hGE}ˈCޱC޵J% yBѡP(=DOHw ``X:`呂O<KsT1Mm@r[g:8p}5Qh` ɳ2(N`g!+Ef 'is PZk8 dĞbfzY ߊoe/2φ[[zQ {Ůa-CQ Ih0>KPvUX qGג~ləh\NīUCUe.V@-*q({D=*ě=Q0xȡDvi$XA$[GGW$ʭ6XQLqeR'Ұnmo>YN1oI=Mu0:" tChR&$#WF[v*>^=ȻMBNp|jZEh(WՉ!-vNC9KU2NG78 st#^7|Z F|\L=03TנԺI"8xt Gr .ӥ_S^-,h4 leAlltjCpUeMĊŀ23&#QfIwnå12"Kme^ G;4-$pU5 bJw;itZ:o / !T5 XolW)9i\ȈJ;õ=qyy&]qkzF{9+B-zVl͎u$zvQb+U&H9aע9}CuVyF5U&ϴʁxuc*MeLYlj-C72 fbܩV?V]n(-Tv43[_T{|}PvKk>(H+ㆳ/MUE(3H'޺#o}?: nxQ=×bt$ܺ@#mZ˂RNX$IwN #߉S5h_gMi5~cгVa{02"<=O3M) *tb;$^,J\ tlr Pl#NEE&4^96/$)O(E[F;-^NwurJ $$h%y0#^xpc00o!8uNYpX50xI}Ċr?$Dߤ1`uS)ݮ>WPhn~һ~s/.!2fyM=^gC/݋*Z@ Tn2пS{DL N"_+HNv)j#| .(E{ IO <Eԍ yBR2bI߾ ]fw!\fv'.#-R~~½dIَV cZOu(hj#J5p>O'mgӆX]3Ϩs[r*z&NI 5%Iū<طʛ:?~Y|7r5D}p.dhk=!04X5 51E|Byk ubA&.I^-6:f;I.TT'^u3w+LN, 1&dyF>stream H TSWǟ֎uN8qt:.SVj;ΌgvΜZ; EQqA[0 VEv$;s!,e! &@|nw}{ #0#0 k%lG[D Gp U۬7+n4 k e^x6{[Dc2]4s4i夥S8/V($JI~En^+w9XM}xŦjyM b<7iOP(o2eJ@Kr @ z\~+.WȫgYf"@v*pdUUU{;&$$޽{[>NR?X`%m߇0)>YN,pe` % RK&dddj z5>B!O/,+R}:Y˿i57.ı|,27D Q΂s^} jT8jJgT*UpG۬!{RRA5{f@lJ*嶷7sk] 8` Y!DJlQ=ctvv2¢@UL&GzeBqHؕu?>>%*R?x|^m=k/]돋шN145au~P4P{uCfN@TPlak":%z33 {<))YxJwww 3gUTT,J {:4A_?vCϰ*lf(־$8[2 E"SM*jڸm|EҷE*cvwqqy-/&#xnJF6I AwNƅk5>A"9TT&QꚏY!SZↂŨ^kܘ ޏ.;q:{_?`Ǒ#_) /}iu m-Qqq °N22*NkĹ')cw&Q+g5N62j.{ e 7 ?o={tu 5zQkvCJRbl|I q,AAZ3b3j3f{u7a+bP{܈S" fܶ/? 7Ep`Au7ʠQz*^GW)J*=rS5Lua0@'`vMtJ,{`-\5q!sf\gXS;St,pLݽRS DBf D@@D+ @E_7F dO" ZV M>X \# ZV` fy3a¦q_^X=X/q rôO@4 ļL&ɞDKkB pHn~58ViW["("xxHL<9'" |0JDm*pN%' !]YF\L,(5x[}A!/1w!M+.nǶV 0XI@4UC !3abyHC H|@T~& 17pL8cK0e8xX(@@ . z=qvl- &R3@ȶ_ֳm`%2"VQ>u @ǧжW<l<āP LK/Q,Yrqz p=1aw;njNqǢE*dLWooN96BOA&h^-袬nL^;5\a bd"@h^ -+mj#^y9voxa׶. 15X{s.̊;Ÿia+0f|[+ЩEc+9  ?| Du/B%sG~UcYJ<¢hr#1/Ir'DMV$bXA|3hLjtʮavuIZ*%BBQH9.vB .Maf4R+{i?՘2 ֮CD;gS[T\,?Keh1N"&!jjL~8!*\Ը     ;Fv*>$l %,12#5~ pA +U?0EL;F5ȗ2gM (C@+ y=='"tR$ 2郵E (AA £ 8 ,;>eW{((,`ӖF_2ߋ>\}zC[bF"(-N8H>ӭM W֪lg:C,?_6# Bb{7=jbY ]G4'nn`86SlC-ÿk{ۚW`G #\[<)Lk6TqhQHē0[^¸F" @8 hG3v1r( " y endstream endobj 137 0 obj<>/ColorSpace<>/Font<>/ProcSet[/PDF/Text/ImageC]/ExtGState<>>>/Type/Page>> endobj 138 0 obj[139 0 R] endobj 139 0 obj<>/A 141 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 140 0 obj<>stream HW[o~ׯ#YD E7E "Mln$KRv_3R3g8ٻ(DF"w9Nh8q$qJLiJxbN5u}籛$0MY8>04M$th K|B(~ h-}箞 ~ Ȅ:Gߡl>Йz9Ρ!u([> e1<|ϟfLfۈ>}?!)2L1q8,=O W3o_ʪ_>_XF)c5O3=Ɯ8qxe@N~͇m) v죞<݌p^XtP~<na=) M=!~]§lEDm: @i&tݍk:ZVQ[TߎaLp=ݬieMc7˺dj܀ȾqezԕhH}}S')A4]XJb3HiI$$xb)*"ʪ[G[ ug4NPij l,ȈWz$M0V'8P6=dcJ}EIb^WMua̛FnQB4"T2mT@CBT DF ך9mD`i$h38ӠpŢ~1zî嚵 _2J3z0-Qjrx?M43L"AOZSФ[;P ++bUPXjc|)'/ל] 3/^Yy[g6 "l96Y-{*p8yl rgO%*R@8R +I>-ijLvLX [\|X8lSgMP.ʾ">Pa?`2dSM՞ <6lb p;\a׫GG ߲~s+ib^Ve/p;&òjXi-!8&ߏ* E)ND%љ92EIz68p4COOZw5)H!Y'PkJnF#j.f0]x T*{f{V3*P YirI軶n#;oVF7e TR,FCkj<ڠ Yvd7 ݦ bXMj6쿎W$.u,hp;`l4k1F,օuJ7}{7~pR[WV|ƧIhW r}cS}3 Hh44@m Pw|ER`i A -m+$kU2\ʅ@4 ԍ_.IV$v` LA-LWygPsiѓ-r׈9nS Z滝Y"޾JFtF^a*{Px0pz4y[y@lWCURa|zYXmצsъ*ǩ)z++SX#ڈkHxt5Ic\zb+y)d0Qsݬ :Z!CT+6SB(K2\tʮYdߍ3iY ZDT˰׾n ߴ4Heym36S,mv iٳAJKkRܽR\V5EY:.)vqe#)J劬* lB1Y nQne8D<.^fCdMIuD]o*GhPߔп̫=>]iJIry%9IBoA2밐{ګDchP1Ƹwn@1 yU1Fr-'H0iZ{ b 1B1iﺵWZuf*J*ST5j)9 V❉qNZ(X@IX$#sMsGrRO'( /Q "Sd~&!]eMU H/sR,tJŊJ݉#"SPr3iTSU'@ZtبMu{_{$X|9&6ZghwX&o5›Fk,t띅XbQ7 p ov-Ȣ6\~f{lׯƲE[edޛWݱrO\ʛJ]2B7qhyTPxy*'8̗OdA \z+^ FڼN6=kwQHCraWg揫TId _|$y y˫ v z@-h-: 1GBlk-kD^/kr w|Zf/7o'z O60 endstream endobj 141 0 obj<> endobj 142 0 obj<>/ColorSpace<>/Font<>/ProcSet[/PDF/Text/ImageC]/ExtGState<>>>/Type/Page>> endobj 143 0 obj<>stream HWn}W*&s(pK> hȤ*vӯ&)RQԴ/ckS~Y_s_-^YÜvR:ԡ q֕hWGqawrś 0 ?l\?ֽu/#5]~CEZp}׀& `]ID[ ty铘_/T$;+->)K'a~(qJnj3GD!Aã4 c'}X@w7X^<%"AD쑘qM\:r^ 9"]ڄSNY(b{ g/q:<4* Nblj&B @g̉焣bG[ H_._·vN`w*/\Ht0anAXЕZ}<稘@H/1I]+!j>LUI#܌T4Hj-+SP9W>γ E}S%Qh-Vpc^6hT &k3ʼn 0+ZFy@09T;l6h|"W5χ&G4bmD+{`Z6G1p,?~7KW5BĊ: Ƈ3r'R,qQ ~- M n`qqvAQ]?ADwomR[( *e*\Mu&?tSRYx6u.'Ɖs_m%MG6b2\/4ǻy%w xvWʝ"doCVR:=*B˾)͡D 7]R+`g=2ZYUM=VJUW`Ċ^ADRіD灒bsf5e*v}k1_ J WP5TeES ԂC"~MZy(2 #^[vHzMS_~6iW!O{)nLY4]bk*M[W92blAr,tY!F.ĸo[ 2ʫVd[pHMtKdHmUJ6(+"&?fi [GrUCqae-6P(Qb12!1>+]jrMҌl`ӑgy,$rۦXn_ZJ”54o*tw[04ԗK=`PK6HGe + }q1uǓ;~ 6 [99'U.9۾S uJԮc?Q nƗ>⻟D̶\U榻wfsK_1L𩽩zj~]Ջ^W/wUdR9b:7-@[d*uQ`,~6q{_ҍ@0HpS}zO(9}d)rږocV7U \~HLIG2m':*)t^i1&p5Q+/ .@T5MUUfP2jIY;u8?${Dx" Tk ͜t&0DƄID1/ ?B-eu|\@g-gbQ?ps2˕Q {]9DJM͢~Hlи͓\nv:Sz3~n:@U4J6FȚj~Vټ5gnzϪ$꽴@:QԦЗ9>:1$tt{|V#%FBd+QA&Po>j[?5zsu1.y"'ڷyhh3 KZ]f26UvԨT@=4:xn郚_քF,VEnV.G*;w3/5{vda:]~ٙ ȶNeS0yjWA)+H`Y³jU͝ΔofXaHJrE endstream endobj 144 0 obj<>/ColorSpace<>/Font<>/ProcSet[/PDF/Text/ImageC]/ExtGState<>>>/Type/Page>> endobj 145 0 obj<>stream HWnH}W4nލ3;0h{a$es-55"e뷚lVW 1l)Y<]uԩ?q4Hɒ>]|?nbؔa}X-0t0Ǧ<,l:a.l-9_~ Ȃu_[zy.YzȪ ̒ ssW"mŏߋUѢIFd g[S^=ْpZE?C-e:EysD?~IO ,r@CO |=[udU^ON-+eu +bgOHR>Oc -w'Bu,O?mE]i!w:ZK)J@'(2^*  =R ǚ4uʧo1@n7'%Ǭ˧',CK2oZއw9϶zD;!CKy# +˺q=."A.KnQ P¦Pt83NyNw!t TQ-Tw 4b]|Y ~U{d"o2Bgoglfw%MA1) #Y. ;f]mWkٞNu&2tuB0g2gsͧ噔;a2RyN%)83s, 2Us3'Gj;3fgHVZ':GĩrLo@bP|5t(wi\e5 N_nN(uޥ(@4PL+ 52&ՠI`#cײy͛FbޟЪe7

    #gG=%eqF4k rsi|wD|`5O>/ColorSpace<>/Font<>/ProcSet[/PDF/Text/ImageC]/ExtGState<>>>/Type/Page>> endobj 147 0 obj<>stream HWn6}W؇ȋE7m6]@."`Ȳ6\In)ٖ(JJux朙3䐠EQwm+кC( 7֒`B>?VD+/vQYvK [`DPެ) FU0"SITJ#j]p'8&(J 4bc"g@'Poet w<uqb%-ᡀa_\L]!(̀wřusFma}sz [\8XP!&X6?*sxtuQ"9JB.(Z)~n 1*|, ua8?U6G:!x 3chSTBn@e 8ʟ7r[~nh| o=4-ra]m?FUʣ,A77hShrU׻xe ^CkZCUd&IEB(IiOE$@g1Qe۾N 2%l ~/=ʒpY9{g%*R~'_XQ}aUgU8R$%5R./9_a{F!\>1xHJά^PEUXb~l8BeR nj6U')8rc֮CmP۱БGzi=N7 9T/7M;{>!SSQZ rtLnu2ل!-ҙD@LQ_f NDQ:N\6&^ϢNJLћS_ \bY(t_8\cQefysx'=L.0JLEB^x=u+W]b#zL_vRޥ$cBuIDm=J=1>I%N fݨ;K`]{q5A(G4:/+lmepTxfl.g7phO07vF5ԩ65.0zvA;AK㝹kP1r䜙 /fj`__'aKSX]z6Lc8ҵs`1qv]d"D5B?R endstream endobj 148 0 obj<>/ColorSpace<>/Font<>/ProcSet[/PDF/Text/ImageC]/ExtGState<>>>/Type/Page>> endobj 149 0 obj<>stream HWo.פ怢) |AS+gTHjw(j9+ي[μy ܉Ð$P™3) /pɻwI)擐iaS?9 |=yq;?PpM޽gCbߋBgC~8ơUPֽ> eqz*qlvHIF)GO>vz/a$45|Ž>TӗW_Dnk`FJ?wx#H8Aȸ\p|Zv*HIe.w=Z}"Dz'bݩ5Ӕqf a&Y(4i*:$Ro(O H?N I-+Dz,Q:fYL@>Ibma8eoX,F"nJaL0\pw|-Egc},|Z$҂Z坛ylVEEMC$؜4,i ]@wۈbJԇe+E %J@/4_ų([$R^ۚhh\lZQ8ۓheZ͆@LnŃk H'ժ*;sfJ3J K!.!Q?G%5ed̫|񑂃^EQ΅Լi.=VeK6R%A>DaS^ IF, 3]#;vQVn`Q S:dJqy0_>x,m$ؑD]BUbgQ|.!"E*jz/׊yNVu`RO)Mʫ=RaVct`M9, 9]q\:j㷠 K7-[WbG}mF^StY UZf|(v$ڰylF}[wPݮ4XHה:ݱjoQov1XҋݗE4iދEs)"qvҿa">ދQ3@ =?#Aznػl_?W"#oo5*|O'fJh/0܁QI.cAƦgdYVs4^@lhX۔9:VTn< r,nr%thQzQ׮.FKh=m⹓Ep0&SGP:su )5MU۲hB8.apd^ͣnLO<Ex =w|_ycU n:عQ-aUNt{P˲`s[f3NY j؎)B[4U* ]\ FΉѴ`uNA9^BR[t 2 u;G Ά2YAH\oA.a J㠝9F:Yƀ6nvmfaPk| g;[KFCR㻰m/hcCtxh|bzAOY q۾u]aJ1[T薅۔e0m5BeܝRL-؅]dy^bbP -\hvŶE7~VhmFCg*nz9s ˷£LC\*';x+bQwQX ld-tԐ52]H =Q+t{k۔%%CǗGmɏjvxjeoU6tm0,Sס!OGZo-*C$0i$,d9N' ~=?Íw ETcA $ڏ Ek$%$jKFVXk(S,. endstream endobj 150 0 obj<>/ColorSpace<>/Font<>/ProcSet[/PDF/Text/ImageC]/ExtGState<>>>/Type/Page>> endobj 151 0 obj<>stream HWێ}W V ym@5.֒-OӬfK3@^Vk{TS]uΩs`^->{j\/>czXQB)uQo ֍hjqz;\\e3 'ee_.GgcVMH;F݉jZHjj$Ӆ m!I?-D!I/a$ފZyPIini) TЏ`P#4S//>' @Yʃi@R O; iN_W:A4] ;D$QھKABF!2.`dqcҵ8;Mben!R}Y;0r‘i7h\ #Ng&/|^Q_Z/h!|➝J6X%2EK&+@FcؓToǢWa5;5ZcQjgCY.r_뿍 A1>95U5.jy5&qsx Z;2v.,oLa{/Ŧp&3]a\V<&@IڠLsy8R !k/QP1{`0UZᳯ!u }p@KG}VZ,ǯK3bڡVZl季|uɇJv=kq&o5ͱH("HzXRg i(;e]< }v\ 2%QzTK6g"G~2&$@: %NLĄ3CzYEܤ(,Ȝv;΃csa;i5J⠋/qW!쪭氺!.Vi廑^;M\ u]+5Vu\E~g0xZmOb-+DDċX#[3PFn=$͈Fc$v$$EYgK_HxJ[cVOlI՗Q*(ز4:yFنOLx(Bp߯iĢd Ѓ.܅YPvv-u]$`,xt"694%;;SqZ,sNK͠.8Uk1!Ҳ$ F7h; ۬g_> f_9C8)З빾W_ w Y.o'-`_BZr? bKLxۢN`N􊈈Su$XfGי%qP3I.7`劍_@gapBI6PeNR$I/mUd T)[\@w%+Ǎ:0ߖQ;MGefOyQdYSX4"k<-$qaL21FleDh$ nZ]s@Q& v/6O0QmT53V0Wi؇ P}QO k؈Dņ eg@ˑ֞Q;L]t0SMz :~Iz:52 /uTCpք죪GsF)U;Hc#.4~N3J.(d#NegP(ΎK^n]Zs 0.duӥG+k}ʹr@TXۖ;Lf\:,Yn se=O9^)b6\x[=$+-(=8~drRv]B_^/8R/HFGp:*8O/nljCO/fw^[QȵhB`dU܊bnㄸ` ֧|ܝ6n/ufr(Z \Hoknp ٍ]CDUJ`#?C)f`_PG1Xq * `S&L@7\Qq`KH*@NRԘp)$v%1fUY tD&}eWvuQΐٙqr{ ,K7,|+:n*b(+w|l"NÊMI-@K>QM{T%![$eKPVG Ŀ7HB̵FoGP(8 /(+Sǧ&,XF"K} DQx_%HeUR.ӕ8FcߋܙMWV{4sNtee.}xo?"q"7yBZ\wc{_pYmMq]#=CV;eT{.S9{`F ihP~ר(wSE% >[X6v4~ 7o @ikթ[b&V˝XSTa VԔA(.Әo p;-JpE&TiޘŇݫ.|]y^#. IBt*u瑓i2(d9rr.GڶBv⦂OCnt!dUՠJTda؏c+wܛ>"'W+*  9ihRL{6 E!dK[$ ^x5 `΃x lʕAF7J՛驼%J/1Fa \R} (P|G6N endstream endobj 152 0 obj<>/ColorSpace<>/Font<>/ProcSet[/PDF/Text/ImageC]/ExtGState<>>>/Type/Page>> endobj 153 0 obj<>stream H]o0+| NHl !7MW6ٚu@BU?Tُ>Q$X$sb1hW/Pwm0$yN3yQD$8TO *7&h^R|]K @20A{߿_;p4J3bc\_w(I`"?dbPEز: vՔ߮Uho+vU# Ma|7'(b)\n]_|9Gm3rprH3,8A/=>tbƎҧX%#@RgOsICoAB13Sut`ynHNDmƘ4k^8 pT62&q]+uدJ˅1Z\)f7|5OP7>l̮L,z&sw`\jRﷻ>y-l*͈YRj' b3J=0N*/ ?NE1QLmY1+92B=6EA6EZ*oRg*) ]mh(r0p2o^ԵxK`\~OMsɡֹT7 +xBƿESejUذ=.βUvKYV9- oU endstream endobj 154 0 obj<>/ColorSpace<>/Font<>/ProcSet[/PDF/Text/ImageC]/ExtGState<>>>/Type/Page>> endobj 155 0 obj<>stream HWMsWHg*!:ٽm*T[)JI@ 9̐lxk)ࡔ$˲(8%,2Y R!!FHidAeC7aYH2e)z pUlo=NtU; D,VV@=GH(dh"cjh ZȨGxH\xbF:xcބ8VР;u @ckF"N8-q|nYJ g//?|u0C:!G7D [c?[, @ -"_CP~H2_.ˮx$hj<4@'gX5BPԍ%sV&f$\Kq֒bY:o۟XЃ dxtp^gej~/zOU^s $v{-JfG٩)+B_bƔ`Lxw"J/+'ԙ7||H\Dy]I fYurEItrH0C&L( PeŒwaF0) ]Hjimƥ9v4+;5N5[Z1g~9vJyLzP-h2L\9Ms]嶺]Ut@u}G77"q9v&BѶZJ.{NP!8b)t G]ݮBe* f(BD:z+ 1?8dvaػcYB-AH#9j/ZY(]'Pf~ňG :|.OdZՈ; ySNfMlМ.*|5vy.ݺP]as,v5;^WȍIF͆;~*!̗{6 ](0?ȱ$殢N0)wo?QA؛YcM'R)Ie,%ף{yoz،lOSw /GF3ɦQ9ZhEvwv7'S5e{E, _+pXx޷#C⎽:\?ԳU4"/f :AH]f 2H<:˛k ?ٚĢ/ Ǥ]JR䩡ɫv% Un̮}ٵ<x\huUHt+X_v,rfS 1[׬nw,vKxІa8C&ث%<-~KGuWoYRi$9%n{Wj d qpfZ4lzʵe5<yMm&/q 粳,EAwerd zYLb8̀f^ 1 wqW2ڡTRu[x۾F]p^uP-&X)OS 0};9;p`nt GϴHd&5kTss,ZWJ'uÇhcwp*iSVr>uDA14d'Ԁ >/ColorSpace<>/Font<>/ProcSet[/PDF/Text/ImageC/ImageI]/ExtGState<>>>/Type/Page>> endobj 157 0 obj<>stream HWIWH rm ccOd0rEHLRt~}zUj=F`mm>OAE(M NKFLSPM>O^wܵ!eMxQD,Jph1X((8x_O~d ;H0_O(2Aۀ`_& 4R&,FQϏ1S$HYM߻T=yy9VOQDCoh>3D/"hx qu.;Ne (I3~_xW͈e(ؐ&rhEKPDP8־ScBZ\2(LJkK "]L";ARQMF@2!ɮiCu* RU`/A1bYwP,RXrjHq(Z1U32g8}aum_S^e] (q+KVr"d h_=lZU=.py#9xC_:'[8YX9< UqD>@)Slvl3X>9#*b';QR e,+0DcRoG84ǫsw]YWf(LXzaxScQojPMG}}[<=Yʷ(pƤ o*eFQP]F!ՆqN R̈Etܦq$b p.wWſ$p H4NmqɭNnpU6әt %sіZuϴ8=Y ̆wڏzSMwɫv-@.w!ӬL|kq۔Eޏt*Y.͘(,3 ] r,\̘:G\sQW@q@:򮹶֍":a)`R]( 8@~oY.9IT8Fvz# vuHOWFȎ.v%Gt_A@ETP1-wNqF"19 )TlEבoDR xLxD%s?j\kQKM'uZѱ il@\gC!)/ R4f'pYd Σ ΢}8#8[`W6ǣR6IvLDQ0sL 6kz>gyQJ{6=QS¢î(h}-n͈T 6=UJDQ%#1"_5*߂$\}ug/`p_—WVhA:g(ƉX.RpkJNχrKid'iDI/BWRiY)8yJ}9eҡR{/0'Evv-"PHGV $]z" >}Akx5@çRzᮩ(2\ z8J,#0+`8×aJy|XTryufEy%nT~ K{j]rXYφKW46_Yv{r2i)!f1?A[IrHP6AY)'F2!ugHň |ve観$TTq~$4EZXdkxujjK$+1š`%o r'5=PQ7hwuޒ¾v02y=)Jr&>+ǾI=,5 S k$m:TKb:|W;+TT!8{Lj'*TR`I>c`A*0&wt];#;o-y=ֈ.!3zu4>5&gP"}ƅg.Obݾ-c y 걙>ָ7`M`Q'5^ !zs1l]m7B;9G kLK_<Ѳ^^ׯXiaE,+I ,D!n "`4K^ˁv}1?>!:FYIX[;9fG0 d$FGV{'hBZXF4;! KJK5Zn Aw3eEk$'jiaSC=S%'gQnjQΣz=МAY!_Pmx뷢41r%=WjgQk/tw endstream endobj 158 0 obj<>stream Hb``Ϸn>qΓ'O}ԩ3{(,,*))p={x7,\۷o/^޵ko޼ׯ?/s>}͛۷}܊= ٽq㦻۶WStY˗**]\\322stv \6&&6""<88ݻt\1=h(O!3kĸ̛fuߺ0 70Y6@ endstream endobj 159 0 obj<>stream Hbd&fV6(pD%% !1)9%51=#3+;'WbS^~AaQq IF endstream endobj 160 0 obj<>stream HJp`CbgASi M1K ^=hI Itt;"8Y} n7~Jkժ٤kW SeL rXRQn+E »w²|3+2hofE"z}FbyApiZEFD#TDv0"?Ǣ}L$juL&3 l6Wu`j=0\.iL&Tj"aWǑ6 0IZl[xv:f>A$I.Kÿ?G endstream endobj 161 0 obj<>stream   !"#$%&' (!)*+,-./0!123456789:;<=>?&@ABC&@DE9FGHIJKLMN*9OPQRST/UVWXYZ[\]^_`abcGde endstream endobj 162 0 obj<>/ColorSpace<>/Font<>/ProcSet[/PDF/Text/ImageC/ImageI]/ExtGState<>>>/Type/Page>> endobj 163 0 obj<>stream HW]H}WѬBV̮Z RZv6cL_6`Sn7sϥjĜׯYF?OGS0gM(tPgza_a>ih'3݌O?ߏh?O U/c?~Ϳ}IEv}F XzZܞhPjh DI˕zwXHDT菑0#a脌HLXD \IK!'AE^ qJE3GC>B!4 "gl@XzO-I( yTaq~|Տ@G-be ZU% ED}T';/ƿM *WgԨ` v_2O7O<7Ed$,yg| ЋQ肜QGgodIb!A,f_L-Գ!nCLS. Fj-Em 3蟦%h6ZufBPL=!Zt.2PYsSB3pr<LR 4 'Ws) ARMO 6EZx`NnP𞟟אvKЬ(&\e2 5ў>)mz,N0^w8⼵P:wJ ֽS=^'cZg}"_p(|^kQQ:#)p"m nM[!Ozf+CVؼ.,I sh|7rꢆsQA` Wؼ%o.u#Կv3  #-J3C ~bM] wUÂķͪU§kΛNC>qu_j1RkEdXP%|4 {@d\ 7.G7nIe 8o#3СdR3e}xR(îvq7u :rVN&"rd{p;|[s٬^l_`^L tˏ9@fcwLc-j;a8BGc8F }L:<,nu_x;?jy66SD.#u| Ϸ小:YK^3`OV%+YmczzsX%4eO_s9N{ .-.AVS>`9^#uj=ey1ߛ mҽᄰG V?'fpmyiS^̫)6 x}̚&x6U;t^P*,^1Y&B@"&BTJBt>_atR|Yn;VܷOO0E݇ NyRPR/L; Gdtc[%&߆m3:+DV#I8I@Oz_dVQ/9ް^:IPx/XzVd6ɲYfuQ $W%eB٢,W7CfcdUd(TV˥ӰA;=A!Dox]}WA÷!~[(Sf4޴OxHx{/ /ovn;qfŜDfNa^d-LG)(`Qu 8hQ(#ETO|FG?O/[?@)˧+mN?D>>xm$ME: ˠM:b -?ޜh3UjbKm7ʰ~- [RM`\CMȓپP!$#[z}FpnyxӰyވ8]cwb>4+DUO<5D5"¡dWT~f8{,$p 4˶nuǁr۔V75/BCp6Y1b/lcmtPwEg1!Z&Ar<.NT-}{^%~ ~pG'U{*Kyt(f[WhMIoV n@%+RzZ:AGc8c=?^li0X 'fS~U@viWNG#EϹښNC>|cB}}D)53]94iu냓3 &fZ3U NoNTA>fC>D0>R:Sc.KaUt8:)KSKF:o)&^( -A3AnUĖ*.#!^gSbV܍ba'GaE[-T;C8s~ ~-a筡Oz(Dam\$5QQ!)pl~c VL07Q, 6 Du+xhќN tPh#RW7"H{[5iUJහ`94}ۚX-)a+"R7'Ja} j1:<Ǎif իa8-a_iN?$= 0!ͿBm]mL…H}}^QxLKp0Wc1؟(=trcCz!;wpe1).$2#LJm 9jV=|\D͉2n꭭ ʹCkgg/ùX(ZWBfY;c)K W]WTB G~Xkt0iܼg(Dn:j87U] ֯%H t*fC[3rheѦSY=G{8gZ.ޔM]m_4K 8%.cMdTz Njj jbg?SPTp z:%7HmK㙣01c-F[I] -2eE,*ѮJ9HAölũDI=ˇGh+#O|s,STm^ /V;:Lq[M5&$eXfҭVh6Sl@‘F-](>w܈=6şAV&vQB/'C~&/g | dV"b6jțB9ۏ*ʼ-_P"f,{}wИjȬ+q#B`rxdZ98A5D,Kd"iP"tO?iOSe%:aM^6__ kJUsvbF7sW2 ؘ]s։G_bبYt2?G׮y-d!P;dw5@~_7&5MbӜk{_01 endstream endobj 164 0 obj<>stream HO1n0ӗ{NZo(p#CAf^I@4dHJry>stream H{60-c wXh&Mv/llK`HNO'{cQ=`d}C !|D+fU6iNخ:+]5rsF-)7̥#o>u1=aq,KxvޖoW r|ݰ,ʆ./ J[f_݊ڢH ,R3uHg~vv3%W98dg%4;GՅhFr\=S,P 7OyPňv(0{wF+8dX@;U]YB"S%.7a޻D+D+s̻?Χ?'e hcKqyHȖl-)xp@QK=/DSvWV.%aeS|7Ve[b쒴?S m-ψihyƱBTǝwyM"* VIش8aC2g> ?4%s29v endstream endobj 166 0 obj<>/ColorSpace<>/Font<>/ProcSet[/PDF/Text/ImageC]/ExtGState<>>>/Type/Page>> endobj 167 0 obj<>stream HW[o6~ E{0هng cl2@D3Jr!.b16Abǡ>w. WL\nSx}m?D\ Qbxu.O 8 $H8W{"ȓQϕZ<\w:|+\F~Q$eq׮#ſ.!8C:GG "YR7 qj=@LaІU ` CebU%A0ţvcP *lC[n&<*ɳ fإQU^|\.1+A0_wVU)vN7 Obcq}V`ԠNk)@+ cp<K0^i tI6x|Cp(Xe}~^Zqs5lW w"w%ŏ# +GI2A (B8筓_ y|Ɏ֭"jܶNȝt 8V$=US};n7,jviR˯ga۠:)`15m_ +ޒȷϢGnչ0@ELoǸζǑTH/wXǖ3%A&L>!ei!u/Z,=g6gZdZ_I")Z k!cǓ2H77r _U?s=Չ~ dU9壐:rxwtww;& Q틌LE>EO>CO׾Mkml_3 0V6 ] endstream endobj 168 0 obj<>/ColorSpace<>/Font<>/ProcSet[/PDF/Text/ImageC/ImageI]/ExtGState<>>>/Type/Page>> endobj 169 0 obj<>stream HWnF}W Pj6;-RC$鐔;\"\RIh`(93gf6ٯRγșEڙRB) Z@O hޙD[usfm FK?bL`O;Oca׷Mn_d>! z !m@|(mT^TZ՛6K]! yc#)b &{)(exE0qrw(&Bes/ ;kquh\uu؃#7ө{8;pse'goCywqvCll.0Ɠ^D%60Nh LmZؕg>V;QT9nK.DY yj 1=2gO*ԛ`Xr ՈA>VCyiLyH(kqS[+|`5v-@1pzd`'7Qk/"ݨb'6@AkUYBS9to!ڮc&@]1T}("m]Eb~oMf{?nE3,H3F @t|ffXa~q]>krֶު)~t*(u#z8!1/`8|M /,sjpl#] t[p듔]SkFSbaaEa`z endstream endobj 170 0 obj<>stream Hb``Ϸl6a¤'O}{(,,*..p¹s={Q_pu/\۷o.\ڵko޼wׯ?/?}[>stream HbdYXPD98yxEDD%%eDdJ*j)XI , OHLJNIMKږ䢂¢2@j endstream endobj 172 0 obj<>stream H̑KSaX( QA`( n,vE#2ζk5tRwApHtnE;ک9C]N;sNջcDߋy>}bQR,d~̲(&vwwEc40m]]dryyys3TE?09j``^2GBZz06",'A&ѴBN7S/l6NbK^/{xx(FPT >Xkjt=-.Ft܊==$H&AAv( }]m^ǽ/+0(f3ݨ-.-/$^V&Y@A!`ߘOn&:h[uu7ZZ#VFFW [.ϟMO75576ޱCCC[[ysJ n|6ŮsV+ө?GGoYӅw}j,KZ2ccPFe)ިH6]lz!Kg 6@xȚPr x(G.CMudq endstream endobj 173 0 obj<>stream HD.{T6`Yl07nieCY-Q{C[[Hڂ-=Y)It-;շ'$I<@6"aaQ43J$!rm43H^y/ƞx$1tR9ZevÏBcr1$Xs%{~>3H(ӈ!x,gor<1B#kCC;<֓D}NtkRaAemu=bH{1K ]vT*C<#!I'ri 9~&ByyYu9Y8s|y?ހBĀٜ6l*Ph82X. ˞)bHh{\xŗ\zW\yUy23°3}+l#ya%3UC)r~"2(JyfioLΡʟ*8qiL!r3 VQlӣa#˴T#; -!B^`y'm,O]8_0& >K vڴ6')u.W8GymÜ/kY:of봘ch8Qsd]˃)q\u;=09Nms;Üϰ}qNdGܜ \IV9sn:}0<&u 켎fIZ#6͚6MlWatҴ7y#G>/ColorSpace<>/Font<>/ProcSet[/PDF/Text/ImageC/ImageI]/ExtGState<>>>/Type/Page>> endobj 175 0 obj<>stream HWYo~ׯR} 6u Xyph3(_j ]mih`11Y;KR"Ќkٿ~mg?>{RNhe,^XH"eQyTp0GDF^}qzw̨Wy^>|z[<8^7oh(OWQ}.`,&<&f]8_\|}X,NC!!iB8dsd|Nth!a`-Д{,'B3H;$c. Ǯ#3,ga{AK(,asc`!C 2&c{߂fk1KDF]hv <ycD]C˥?ϩZ:X[ zvڽSAa;lD{Qc,{j*JD_F. Pbq5V]bܞViQNqL:)9(er]L!OCۧBI\ks(~8u+S)t&]S?W˞E2jɭo2q7ejKRSH;Ji*o%.]J\I V]b,C $uaBzok,RHR[4b.DXm h9 Gӯ>EkHh]ԴE'CFiꍡSfM *D/v۪EiE%/sF!pGLuyS(g"8\ħأaW=.}&֚jD}S9&\Xa8Akʢ;Ep S{Ol$dƌ+?Le2 ܵ* Y^@&Bdr܊ת%G<آx U@ Ufy8C4T91`@R|}@ w=)ph Qٗ}d.ۤ!f&U`+uׄ6AF*rC".<C~pDlb@{JLp M0DB8`Edj5^R8SyνXml y4QO"b79||/m3OqlE^JNθ! g5suPcS%!Ћzz0a#[w#`!x.'{жil&\B;ed[0dkQ؟YhՕFRʾKy_Sٴ׺MP|b#-Wjt^8Ӻ$Pʗ]A !38KȊ~pe/6WN:?c I¿fjCԜ|_~kXVeU}d>Uz(-w9Z y,apJ}jɻ,ȾࢫW(:|΃e2#3"yqAx?W`/:Tk'1úL.s A:(+c] !e, endstream endobj 176 0 obj<>stream Hԑ]KPGM/&P%jV鄤ލB3nLCL 좦Mt]5]dVS/̨qݧsqs{9k`tFp{{y~-zikM+)'}633;54  T*gg Mvw7KKq=i~987""ԏy~u:qiz\ {eVm HAKv]}=:*~N? bM{jVTjHIn]\xss1]fTfT*[j4:.kj~²,vl67ߞ6'&soF6W,I6!cwyWӳOt]$Lf}]唝ilPhtH2noo%IáP(fWX6n LՒ(FHNi"^H@.^$ ii` 8pE,)C&-4% 2MH*m"CG1K K?_h endstream endobj 177 0 obj<>stream H엉ELPQHIiSE4Z[Y(- }beZ)B5ͱM6id7f~~;;39Uz^R xj*{J٢OQqh@=f>xS; H=rj'j(tMTcs|= {JբOT\N{ɧ:ͺ[B.(UpU3>QquO;3:{+O1\Ղt슐ae'*9.8/T^ЅFjϼ WQ|/rx]UK]Je[ \a+r[2 :tq=\ z"pgaygzE9\m$J{K}"[u^w 7rܛ [c$׽,B$j Ǭ.{"l\oŒ6<wL" yEW_#fpG%>QKoYum߱Υw}4C|[w[aåOs yEW|qTR i]Yս<ÏW;J87%յ:?+ ff9Bc”"v+|s]R#2]f|Re^ۯDbY̽2j%K%O#u5bp`NsTgؒ(Y)HW}VfsM~STkYN\i$8ļx,3K~h͙9t{<5ʘmnyؖcà:08<2v;~UKlx;X ǬlO+!84i>$I\Bʗ'li0/ y [ 0bׇ3kwC.{džf~'mr/cl./GNM pOZ!F-5KE0;*L濍9ޝy8Fk"mw pwQD>Z3pw@nt b84~6=q2dPIh9Zyh 9Zȗ,ϒ` >Ew+L9Ч`k#b.-Hfstu c '7)3\Ki*r³jK[4s酪74f|̛O,s-w74Sg#ȉj:0oW> Dg endstream endobj 178 0 obj<>/ColorSpace<>/Font<>/ProcSet[/PDF/Text/ImageC/ImageI]/ExtGState<>>>/Type/Page>> endobj 179 0 obj<>stream HWnF}W-"aw $q`yA%HBQcx~.f5ec6d.9; urg߿b;q6y7ufO|ߏY8C?UeoQ_81Hw̼ɻ؟`~>_Ok?F1L?Lֻ *H[Z܁h1X.h'1t,7׻B$JMp!̙҄@U6N F$I˜Cc>aPF. 2|xC8qwn{a?|Gt'Ih`Mi_[p :A'uQc!oj z89 yB E!Z)mQi"h~.,#x{q~lBn?)32nv֧Fmoz]{v`]Q&/LRi@$0^ݹʛƁ+ۯ9|nI#sv /Q$H"|dzP;+!5u_ u΄rY婬 8u1Kk,њ^E+(L/5bN9PYQƾ| o#őRAh2ݭvo^, Yz9 uDKSAnf(HM#"Sfǥ1V/z\̲A׳]k%o<p8a,xvq4FI'(1 UP`YGڽv%|Sf޽b:S6tfR3w:ZrY} "TZMV :3v1L &ܶ\ӷ<߶\>u-EZ>y{wg jx@V z6BuEj5V4 `g,UDR;5My9/w/J6{kf֣jvտTX̛ʣnqcm=<[M^ `F;JCa[EI,^9?¥+K&]uDU2(ڨR~ݦ'fT#YOﮦD'$"gm  @@V~ Ol<7QԦ:`$tJ]|f9u,f%2tc׏&AdkX:yhq#0md z g '>/XHίfB3ZU }ص:} n&VNؾ"wlVyڠӛnaM>stream Hb``߿>tmY?ſ~yy nUSR3Li8T Z_gmuV+>stream HTG0C߸ƶBoLKs1 +>J xp)~J hw CP88 EQ,Wrg^;_#zl+9h endstream endobj 182 0 obj<>stream Hb``/OY{׾{v疙Erq|a5Uj,ƿ 3tk̙0=᎜:ېcnRYͦ"Ϲ. XW.Q۳c(+. endstream endobj 183 0 obj<>stream H$G Cg0TCz!chV""X1bNEYɢRB4I~Y š]=AbּZ~  endstream endobj 184 0 obj<>stream Hb``Ëw/ڲ~=%*ܪ mF̙hv~ze$Q0 20k*v endstream endobj 185 0 obj<>stream HTI C(v% Xc"JY 9jfv[y̵4XL |vq>Fŋ9nv\ 9^n@ endstream endobj 186 0 obj<>/ColorSpace<>/Font<>/ProcSet[/PDF/Text/ImageC]/ExtGState<>>>/Type/Page>> endobj 187 0 obj<>stream HW[o6~ׯ ЇEk ؒv^Bbӱ;Yr%w%Sw~$B~<;Emy} h2a%WO=1uy6"a;4APMZWݦ :ZW7 •\  sD|!sc.[-aʟ/UֈBܑ0KkHOp"W1Lz>ڬ5`ET?|IK{1[UF+7HuW\n] \Upyvj ط(~2eL :SK$S$ښl*X:sT*'كc@jhىQ_!l; (j'#Tgb!"޿1Շ.pփ@MN5,-ceuq۠T-.@J}\|~NS]|>b[̭}G8st[IF|'qh>}ӂG0ůЇ4^+B #_[BSC=0A,YL"ECZs!=*s88X ݤImUBBj#_i=/Q!Ikjmk]uPk\WO DՓXT_E&e{Xrg:}þQ s *h]tlY\}2u+t_-_\"^]6kXĕ8V`a3 4`RuÞj?{>MЍ ]Ub['3$4gCbc60꫉Z V>iԔ| - dOE[ym3t^+ t8Sw"=t_Yy^>k;홢U!2=9y4]G BcdXɤPr.yAۥ&6uS6!$8k@Ғ!4^E|iWcwd7Zh'~g0-"G7B`yDԽ\h uŜU\XTج $wt쏏Ne)L$:GAdlARlX6q3.:Lt$T0EhCP|/ W endstream endobj 188 0 obj<>/ColorSpace<>/Font<>/ProcSet[/PDF/Text/ImageC/ImageI]/ExtGState<>>>/Type/Page>> endobj 189 0 obj[190 0 R 191 0 R 192 0 R 193 0 R] endobj 190 0 obj<>/A 197 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 191 0 obj<>/A 198 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 192 0 obj<>/A 199 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 193 0 obj<>/A 200 0 R/H/I/Border[0 0 0]/Type/Annot>> endobj 194 0 obj<>stream HW]}ׯ[@׍i`yKQ+:ܬ_KR"i-oP/W{.ugɝ}5brsG 4tC#ph{_qb7sl0{= F0 fR._ook_ڗ~L9/V,hԈ%5D`mKDD+X JBnԗ+VXHTf"|D1"㱘@lqq쇢q8sD9#$ bHJ0vu^ ?P.) %bIb[ 4ΏZp  Б:m3w@j Z"&lQȩDƚ5@5L. 8)oBR !FН@/t F'Ǒ ^qOF qǻgFmYAI@hpЀ# Ge#2YB,$~' vYȎIkln5R@aNY9N׊ _=bs,zouyquV(ع <7vN?䄩 041K,q ڴ%yy QqbjPY`w69IQ[ӊ RupaPLАhV*)V,KV/ } c%C( } 񸁲׭O#-~yY60xĥivn6eO6ގJ*8Ҥ҄cjpJӢ(vpI00 Ů5!zPf*6Dqf͵vGQk ^mzBH ]͋uy%u^K'AI>ɘL.d8(`7V >i%lj*Lajڼ:x؜g\˻Y.''-hfc6JFv K3ؔ`Kw}(硻Cxn ~H!k4=I!Z)5&o[>&iE e=,yMxx[I{ }?xYtu@=6P:Y%u{`{Q·Vv-m2j?m>XD䎌bLds9(xBFBf2{^_Tq$&M3OU 䘟[}  FL͂&ހYq$TuIsi i._f5Rjj]N0(|Ԧ@o) dp|il<×'Cwwq|Π5uvCU#hXV4dɷC^}3QZ 'lsʜ8oAϤgw*%T!/Q;gm9f/CI:cca_7\6= <-t yu)+4Z8]:xO]:+D׽ZxsTAسKgi3l:4 6s3rO&hL7Oߨj}D$''~7Gwя4#Vڲm^iP}F0B^S9=P_Mk! Guٯݹɽ~LLidrkx<"\wWo:I6,O]R#>,UI*4u,-5~F k#g?]NhAj63Xe^83>o_qW΍| )=A<k#c)֓|diYd%!HɫCQ_iC֨U`JA70:!id8WB%\`0 vvFy `pn>٘MOG+649@`GVY;|oP\~4peBj˧q{,jN&/J-@jmQ| hfQb+˘s-*CvB̭`]x 9Ƕ! tӺ9N& BmT`3*fuo+ɏ]T']':(,'2k7M-=Z$nG>i*W$K|zfuJD@{#Mi*ךQL WYM,ī<)ƛdo`3d7Q}sKжlks`>QGb}NhԜLxy ^g/;S[sbxҠ#!hCOPj (8tm7k&[N^QfFR*TSj䱤Д65MQIf8ya1imnUޒ[|Z: 5Ai"4&&+{ws/r?̽Y͏/q8z/9r|Q[YP}w fpkT5'06ya .^oF`b흠!o((zɴU>stream H1K1OC!@ ..qr)tў =z(. 'DBڜ_P໼}=}׳HF4l>׫y|fajpr38&e,mB`x;[X}Dr8!nPnx!VEbc>\T IѕR]8KQ>stream HW1X-"X[+VEkܙLPWD˨}M6; Lff0hUq[ VtoOo_i}=0I'<<8k 3&>@ !Fm{x xQYGb2΋s#o-ܡ%^K8W~ҠWjjv^BoFHnjȷ*T=EyGXd/ w <)$RG[4U V>I3Xu;n4/4/4/4/4/4/4/4/4/4/4/4/4/4/ږ}yyv22m *uE endstream endobj 197 0 obj<> endobj 198 0 obj<> endobj 199 0 obj<> endobj 200 0 obj<> endobj 201 0 obj<>/ColorSpace<>/Font<>/ProcSet[/PDF/Text/ImageC]/ExtGState<>>>/Type/Page>> endobj 202 0 obj<>stream HWm_A _ Zs7#iu@S|8:ѡH|v}g)D-gw;A}F">|Pν$Hz)%y309{|&IeK?, Ͳ(W HL HYLOy>Mq\m֗}={+Emd za NBF Pj~#_xWQa}h~y[`odKIDRA}?%@>t{|Yx? bQ(ejnt[)Ϙ "ȼúb uw(bC_'sC.k58T)K T?79%}D:qYr4?]> m)t<`HŒ=Vo0tP+sZ-U[~̼ӆ:`$4t*buy4z%Q}yăeڍ̋e_H՛>$ꀤkF l\ S XN,D' d}@u44dw3o[p,֛R/ґ<U($3X2/Ec::uj)#9E67n:CKZ WV"C_>QTuU~yg9gXJ"(rTݶqdp/WbˮUT}Q#2p}:+ڶ,p=d Kv̮ &}h g:8#El43 `n_RTdu e h\PB]r01=Dsk؋] _E72cEP^FZ =vPXJmtйXoUUt )zq݉Ȉhr!W4mӓQ)'t#!˦&ڄ K9:܀X|`HRGoPw*Y]LwgP0Wmq_9]v&rH*^cǢ`G -> rOu3Cj;ϭ/6ȅ1sp8 J G_+9oRt+<*-v _6螪`+zJƻb&,n I%Nx(y47\.uk|{,A?pi FȖDG'/uu$`8Eє, @?@w2[pEL usd:u߶[Q:Y/۱ѿ튰 &DSЎўMQ\lvE[ecGS~P|v-fgηK/:wM~dyMdJJ%1U@ڝ~>,% ְhڰ̬Oφ{ӓ'J ذ}aw\U ((0Se$99C6:Gt[ڢ\\~dq=Ⱥ3S^QBd6v‚Ύ,Y'Qn va&[~6p_V|#*Zvb!ߌc_7J־vy9%/WaksblC|mL=Y<uԟ)e6*NV >Xr\3/|xrf+Li-((Ivє+ē$%|wG7Rn۠5C 4ٓo2|kwgYSU~VOm {ϿWk,Cr\P_};ojuju=>~~NsMSB6odM1Mc_ ]šܱtLg?'KF$W]m"h$XMyc0rdzS="H б|4wZQ%@Rfg0"ΰ̗n0}rJV=* DU߾vu>+l~3G׭ mdo C̯_g~lrm2V ζ:7[cIz' EN{ os;%+uGTԄKqְ( ˍăS>SDVP_eH7QI:RlH~[&+0_(1j@f4F2s:_rzmA,)_g˜`L/BYq- A5UpW1~`@@^KS6OJYEYP%`Ax xSE(/7$ΐ{C6L%,Uګj%{XuVF88MWv }Tc-XS)`.{C0Nh\fAȜ5J5+Krψ^}lh9azcm7M+KOPqoke@8gpc'$/i%=T endstream endobj 203 0 obj<>/ColorSpace<>/Font<>/ProcSet[/PDF/Text/ImageC/ImageI]/ExtGState<>>>/Type/Page>> endobj 204 0 obj<>stream HW]o} Ї+_\sE{-{Sl_5pbش-l;d[d$m}4̜9OF 2ydL8}|5'GA aUMO_~0?%Iӈ̷o ow?}0_ag|x:(>UVFF4Jq#-%Z@L$Hh ӄ 6XȈf/?&A DMS2r2cbۏRN,ˢ$PM>eQ1HBd Bh"}?M2N}S5@XCO f,R) p{WSB':ü/pqJ%ZAFPsɦI @F>C =1y7- @ ;u h8HM6*~CT$g~<2= juqH:λw=CUcPrM͡4:_ >)RU^J2c\ubd 刳/Grȧߢ7q@GG>nHVEcEfu[fQX!] 2bOOæ1`)ua)!τ$p\NOsz+PnPz"8“}+ku-N,)ڄ l?+Ɖ\FZ z,ݵO# ڕaׄOi&^薣0|(iHdp 49D̀Ar]ܡ -L4b#Tayzh7hQsv.*oq].`䮕sE(,j&Nѝj"lPo[]%hh fk{ = hŌha9ޞZTGw֍i YHӐIJeӊ|U}=,B$c* !a%_tբY|tORח)ڢ*1ň/,䘙ĩl*\JĎfBo2O%*ha;ioxJ2 fI{yuAH|oELkGUo _ n+,ggw:ʠ@NK'ح{,sLrW/W`ze2'.fw@5/۱p9L& 럳vmVȬ&Wz'a U[jmƲᘁ77<QC.imw iqs:58ӫu^p-֗;]#F ːaNezu/HU'a=l>@6ySm]$fk7i$y&"H"d|,d#DvBX=H_?rJVuފNQvnƷ">< ){QǢ}(J4s>+xn-_lȝ״y-A>ݔ|$7{Fu;O>7p'REoK2S!/Mq?57M(V,Uh\4J|ן;Gp3d%Do[&suRtz)䴡P22C$j<9sKGڀ7*%T_"6~F7CzŶ=lɽ<wԾ鍻/Vk>stream H[OmUaK!Zx2EZ>ͦFB.Uydu>}PHߒ?H-?l-Mۼ1E`@e)A|YwTom4>C&ֽD8/'nsrZfV2\ ׋k{{OX{W'4.۪$R[n9gʎ5O^>2*QP`ts??y+N{Oqn|nGP D'CNT9qoK{fH[.b# ~gC#IKbblOxʗrX3wqm9~SO ?C`$ endstream endobj 206 0 obj<>stream     !"#$%&&'()*+,-./0&1234567890:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]Q^_`abcdefghijklmnopqrstuvwxyz{|}~ ' endstream endobj 207 0 obj<>stream HRAE 0B^ v` X•Šr ptO:?γ[uNT/pFy[H\~, 5?(,~TkPion~* d :5 /s U Qu`+Uҟ^KS3RJht4¢ӿi2V{ĥS2JfReʅTFm.J03+jWfg-/:)}9|NXbJ?))6yS^D7sspbV}^n[㷞יloZWV]%3 0Ny endstream endobj 208 0 obj<>stream     !"#$%&'()* +, -./0123 45677668%9:; <. ==>?-@ABCDE F=GH7IJKLMN OPQRSTUVWXL5FSYZ[\]^_%`abcdefghijklmnopqrstukvAwhxyz{|}~a endstream endobj 209 0 obj<>/ColorSpace<>/Font<>/ProcSet[/PDF/Text/ImageC]/ExtGState<>>>/Type/Page>> endobj 210 0 obj<>stream HW]o6}ׯ Ї؅͈waIuЭ0dNʒ+_KR%6"=m9}lS4'|3(5>翼7}a&NPF&|R;$AĶ0M`V[Dי'|APjC.LDP1`p"&/UJgvlW&S8 ĦiRI^u l `s\t{,<)v YAp%TRjŦ$ A6п #-b\O uTY #+2RSIR#-?UC#@R[c |2Tc{#(ǧ$9 ˮk S}tU6rT3$D]3`2>As#䈻d4cG\j gOӚmⴓ5zϴBT!!?xYX,Kjhg>E wiVFz6rG(RG63>N"+- ew{" 7zr:l=aQB;󠩮<Y.alLFolT/M+64sp=vM 7BSNԘ"?; \U4u{"K|5ZTzIv߾.$ʵ\[e6AӮHrɍԬr[Tpnx>tۢI}SA'mN39Հ_Z-0$o=UEn:'L'z >js.WQs'̴ΨU Y>DZ,=Nυ tx`posy:Q:Ν>@wno>DtiUzbDڧWxy AN'Q㓌*ǘiq:t&RUyoԀ0G觇g8aLqG[Џ+*i*Ũ xzbs8&Po\|'nt &϶,(. btw'ə'|]ר=. xrߋ>ԛN'Zv ۇcCBgݔxɯT̹n(Se$˨R[w%b1M )3T]WFv4r,K1*jkG]h8++K,hɒCo4j喙\"M{B9` mH.\WʸlɛOJu endstream endobj 211 0 obj<>/ColorSpace<>/Font<>/ProcSet[/PDF/Text/ImageC/ImageI]/ExtGState<>>>/Type/Page>> endobj 212 0 obj<>stream HWrH+p;ڰTGG20"|&: & |$`! %Ҵ6mx˗Խw??QrMSʙSB) 8u?_n\6k~T'm>Pwlܼ`akr~mqx[2־k޻"9-߲~(QԈp'-4bQiCroADQ=T$܈9SB-3Oq*PM0_)ƙ+"@DFrӭsaKw~AlD?P(TUޤy8ؒБ:2PgCqGD[&)QOY(eB."24!⠈epI}_x#1X7!R@ ;:V/V郦M/f`SƆ|XM1@Ix` 0 4f.Y[ ϝ8x3ygC\e;3q,N>ϝ peЏ0D=k  EvnS8d셤}/8kI_3ؗŷ|sAԄQXʷ{\jVumt=`aw&ܪ2adñDi|KN9 \ 3l0eNo02JNOYo w2lr>N".2UґHJB»W(133kDB[/'ጱhrǐ9g e?֖ӻDXۭ/G狒)-jIFBڳZ 7Ǎ̪|?.k6zƼ7Zm˒Fܙ>AT]er ,}]rJ{'i9^g:˸y_:S/-J_ y-4l0PKң ZKOKF 5x̪(`%|H(pYM?x}>* QL`MGte&>stream Adobed     $$''$$53335;;;;;;;;;;  %% ## ((%%((22022;;;;;;;;;;,S"?   3!1AQa"q2B#$Rb34rC%Scs5&DTdE£t6UeuF'Vfv7GWgw5!1AQaq"2B#R3$brCScs4%&5DTdEU6teuFVfv'7GWgw ?!_2ֺ8mVb"<5-p N:ul }> :tfڌr7_<֮[:V&y66D { y= ΋Y4W99 -c$Zvtȇ6S G7U;$ճc[K@o"7{?~_k,t|wcךּ Gֱ˞\﫵[]Nђ_OP5mQ8½F>63niǭ95॑)8#BPQN^k!^M=rZϫ!"7x;ѝ;ۘ׶ǿkDS9:oϮlO G<{m_h{k kָֆDi_tCP{'Sr\kws!Y~Ci/`Vׂ>o;!5Y]uc+ 8{y| \`8Lvڐ9~(Q͓bP68r ilOqP*V(K0-22ԛ>*%J PRR n?B[%+qP()AIJ%J PRR n?B[%+qP()AIJ%J PRR n?B[%+qP()AIJ%J PRR n?B[%+qP()AIJ%J PRR n?B[%+qP()AIJ%J PRR n?B[%+qP()AIJ%J PRR n?B[%+qP()AIJ%J PRR n?B[%/I4RJ5'm_jwM?ڝ/;BIqjw %{ߺSt $O~KNЀ\G 5ų"yNRyK@߼I+*[@߼coS-coR7)Jʖ7)zX%eKzX,o t,o ^7:IYRޖ7/K~$oK~򗥍yNVTyK@߼I+*[@߼coS-coR7)Jʖ7)zX%eKzX,o t,o ^7:IYRޖ7/K~$oK~򗥍yNVTyK@߼I+*[@߼coS-coR7)Jʖ7)zX%eKzX,o t,o ^7:IYRޖ7/K~$oK~򗥍yNVTyK@߼I+*[@߼coS-coR7)Jʖ7)zX%eKzX,o u[:C !i$JVT7)zX_Qʲ,sĈ@QsY $`pGU37)XSUmu=6ܘ![q}q1.$)Ҍ=Qcd>~҉?C߷TO脓5OCקӏVW@UfP0XI#z~z}zrq_mۮH%7OKק+nI%ܯg$z~z}zrq_mۮI)7OKק+nI%ܯg$z~z}zrq_mۮI)7OKק+nI%ܯg$z~z}zrq_mۮI)7OKק+nI%ܯg$z~z}zrq_mۮI)7OKק+nI%ܯg$z~z}zrq_mۮI)7OKק+nI%ܯg$z~z}zrq_mۮI)7OKק+nI%ܯg$z~z}zrq_mۮI)7OKק+nI%ܯg$z~z}zrq_mۮI)7OKק+nI%ܯg$z~z}zrq_mۮI)7OKק+nI%ܯg$z~z}zrq_mۮI)7OKק+nI%ܯg$z~z}zrq:SÏmvDIM^oޗOAnW3wK_uI%&z~~?$ە]Ro^oޗOAnW3wK_uI%&z~~?$ە]Ro^oޗOAnW3wK_uI%&z~~?$ە]Ro^oޗOAnW3wQd[l #~I)?mۮI}+nI$޽?߽/^oޫe8zlk0i?)}+nI$޽?߽/^oރܯg$rqJM[SXZۋ&@ܯg$==2> ~% Xptl3M.D_uI/~?$T\9VpWֻk\_vVUd`kf"`*rq[Iݥ-#v-th`#)@ dISO脒?K+STIMU O՘rjO-A*H?lk/c5܇ 6$c5ܗ1xK]ثL1xKw%.UIw%?c*$?f?]p˱iAf?]}e ُr\2~Zd~ُr_lk.v?b2H?lk/c5ܗ Vewe/ ca Uށ;`w½?NF!{^u{$5Ŏ]C`?Rr\Qd;14wK:{οvN5E]c} lT ^մ3fn֝FU&uϪhH 4rSuj/s?WcMmt~۬NQĵi `UDN8'Nqx3Do>=W?6 pkvbqFju+q^K*]qm+Z6R0a&}Lf#y l woZsK-Ƹ%kO4D˯)Wt} i~..'`$4:jos#zFr[mD[=_QȨ9L9ƌ(xcmS!$O脒*J~5xv/LynMvxa/{khp^)>TV5wl6Mze֊m.q.mo'sCk,1Y#>y^|sxt'}*(ZuG0[l;kl{ 9ZXU~=ϧ.DĎǠyk`piv΀.rxy5Ӌ$X k }=V`"sw(6L?k7= *۲=}nmna7 YV>RifǒT Xw~WtO.{ }M={&)|*u?#OL?/ɿXՊ~kSf,- 1}Sͤ_eT{{gZo*hs 6gVN[{x~=_cԇ<8L9xY}/߫8Crr[CZ?vAݨUռNgm 8ۤ1BٷxeTNwE :=e4}@Fw6X*wc@0y5<pR[q܆βګ2N $}VS[M}lݺgfulS:ױZK^ڎ8/sZ]c]|*V]׌ʫwTKYhk\&y!ܢ3 $d_@oO[_zl;CVEX@;@p5Ic&T~Ziun/`h vmݩÜXv?798$,-ʉԺh 0(\`ƭszVW~uUMn504X SwDQݺ펙l7+Q;_cq`㰓\K鋇-o%?_PWc釺,gvW~ qq}lnou.-ZITAhޱyu%SO~6 iP ofgՠnTB2_ I3૿S\SNk*<^s -q:SBLI =*6!Qr ^:9}XhmȦ)e4jűgP﫴R6ʜ h۵k{?i{=Bec~6%A􎪕WYu6 ~CֳC=ۛVn[#N^mo ܛcH}6zb VFm6ن+ƩԽ6ǺsQ3*^={n5Aeϥ(hKCg-hBϫ]PԴm.yHNh_5⬑ߥxwW%9}\c1$W*vYUmdK6eW/i~UHߦ5İo}{~ h7VW_|T>f=6z f}"OC@(|a/N.nN+ݱŖه[2;OƧ_n j.K =Wn, o:4]cm}5oG?E߻•Ł*q{˩.itngod4OosN%705ӵ kao1M4A6[Mmuͱq֍go^cf=zTqh2о좭ͮަqWT IGAґYo<7vk9-kvM%@[CfeݱC2륏kDZށ 4$Ro@ۮѭ >CK6&61pu]KunlFzLٳۦ֟΍tGչXizᆦxj67#ilMޔdASY}:?oՀWo}T!9m"͍kݵvG"TB1fU]a4rŝ뽬{lǩ'qu%.hc3> );u{1hZ}Ot8:X?pR[tNsr>4>% 5FXe9 ױIQ w<kSgV:m5f䰰: mbKdnan}^׏nSi}Wk:&lقk~(7 )mlmmyخc TAd~cnt?o_I/ݯ$+1+=|zȢZz1v7k!Sbgk+[}b6 ,j/o.6ZfZK\992ɏό'qeON-Wr_u&a̮]&o-w5Eo՛deGhm.}4 欲f ۚ@n?ٺ>v/- s\e-a~Up}2M:o^y3=yvP ^3p2rٛ]}M`>ow4Fš_P/̷Cv<%ӑ.f-EьƑs.:9YnqbGHUhvK UW.ha>ZLUl}RFZ%AXoJ_^{79Sfk2ͻgʝ5s׳V93v5%e(gQ)Nm׏{,3\\ vٔ@<OO|ˋZk%ղl<nTcz}mmw$ݨM 05ǧc+,}"5Ho^񇦼q~6^uxsD OO+fbsMf8DD4[\Jr:M!hsoTqM0Ns96>pS{ jj?cΤ_kVeO2~1yy6;'yvEX4z;E@ZmXc¼vû{HDl@l`_p1#xyKKm}aܟl6nOTy{+xeImݔYN Ձ })T#pF# s)<~KZ9Zս:ǻkT}fwO}5.&IPόJDsq7O)%6nOR_X'*o=xc9En%iq_+r[Lf7ogF'>g ³cݗDejfUpq,kC ܞU;d.uc:00Rόq؝btjOCkVezJn>k_[9 #q! ddf6ѻj?D$翋e+SZ/e[FqY[ ǵwh3?__;yȱ{e2ڬ{>؞`Qꮌp{ZP)u1%BzmXn,mq;llL5oc^ H8U:;+e{˶Nג8J^6lpkXAaa}ņneYžX׵5ikIԩ3۷/Cj8tN̫-[V5յ;chkMtVʹޕNn+.]6Ōsu Lϭ=2[\49zc%B%]+UcUͬo4XȐhzN3jymmYkF։aMwiKuA=W9h?Mc[gT^TۮJ=R:^ N 6 s?S7t6熰As=A{i\FvImULd OO֚-Uisj4<0d2>\n/XvM66Do#h&6%U_os}'i}%lN3▚MYyU:[E7PҝKn#]4(kMB6jjc~{7쮟ZqEcicl8 B>*5^]81߼s^#+e6Ysx-vkiB?XPԖ6eV\`n`nESe]`/q$84 2۬U 5R__]vCg$]poksiK43摏}j`eu~eڀ搈ҝ37ۻ{s 47I_Pm#ҰG>I:Wjm.Ȯ'k~TS+,aw6 M5 (no~4=GJϯ`۴AR I֨>R mǦU#ԪǚHcNMǚi{qX'Bʫt|˘NUvv:&4~*=+-k-vlax s W7 Z[sk,ZgP,GSl~.5eI}c@h ">4Vуs=qcoeM0_aqsaA>|B,W]fƽMN6n-psNLM_ge6և=dTΚ(tc{>I`{?Gis+\t/Ibnk6E2h 5{EI'+ycZw43k[>iXR6}eOh߳usշZvb3Z^繬Ң[OtInMIA4*ǙӱQe[@mm v8teǂ$R`^&6CY8̧{[+q::t3ՑD04PcH@OJtz,Qhx,EVmo-5#s};n7~i XK76=UcSsEVe5n?Μc)- 1~ʯb=Uunc?k֘`myD/m= i"C#+˲4{mS]'~Hch"} .ccDrCuq?G:RRSxK֧#~𕩜K#We]ctvև8H5k֧#~dbν[UuWR׍ t#%.^cv=պXd9s?vdki\.xlV)clOONOFEӻ`ۼ8ݻgyC{^Ht}mĀYoNӷQ䕅te]ʳ츺`s۽ѕxZ cba. -'FG*O9e 0H es,YU<5pni +s[UM cg E5%C֧#~OF+S9(FXӰKh8K֧#~km@T ;.}-aQnsfsAt YfMMz|DWloZVrUz?o d B]I/S!$_?۳ʱۋh] /sцnʰ1~Vtڟ뙙e5Z~=nhktonU$Ӎc t6hyivc>*:h^*2ʞ׀\YS§-d'm KH-#ݕF}^̦>,`>->ƀ2JJM_n͎lxcZjxqT:;=۸:cM2O6?x/kͲ斴ftlۍqh \ RqOބ΃[ZgZ2+)&2:7n6"Q5Z$ѓ{En4291JI?Q!sZƊ]fV#!5= n*U .i-u!k;Dޙk mMOvֹhs.2cg֌RI,csrIod⭷ǚC^놐}7C7ia/ihCY4a,OAI f̗kYl׸čyƹ wWweBkP=s#^J.GK*}Tq,t- 7*NXlYKJ {g[@[sZ^\L 6^ D9ơCnmu*{}suhsH)l?yimOq'ۮAhfCqXEaK$+uw wYxuVfSC}/o@Ӻεlf`=K hw;*``fCX iA!(Pr?4߭s-9q1ì֪k[cbʽp9Sg{sq{Peugan*};\2ŹzubۋX5c7۬{ [oqc^ } l>Ii___g^oy`{<֗. yg ʞhauֶM!][5UUpߵ)s3װ~1^;+]N6akmq{p{d]=s^KXĴzo\=,w=sK׹s=nncؔ,.+n.]m`hq-d{cD؟Wn&=X. kHmLsjƂwJZ)vcvUYu}ЅakKaq-h$rϫ2wVRMg=07Qv>e2 Z\H)lNVN>5d mnƺ!Θytxt솚->l~6Ǹ@stDŦ]]l >-iΒWվU^}5$ "̉5,dS]w9X+ 16ߢvQ]:hmD=Ns۽8wUcZ*4jcb k( p 5gq:1i27k%e_f?Ig3KM'X:c54ʞk}F55qo<5aӚ^_FCMls?5d.cipk{Nu5{K]-Snv&.G[xpZK6$ iQuW[5uk a Akӵi2> ,zzS*8k+v]k\ ?5W d_ z~*JMZ5ƹqk7 ]+P;_ڞj-01퇇 BJF`c6^n۬smaccs\-q xc{U8EzR8+>X]o~D׸>mG13&PV]k]Je+;7sClXnn6;+e\Z}'9 Tsqz.}ndX*X2H[a?e k8XXƅ-4Rg]M5y5zo mus;1 UuKe{m-i.p~3k"%ak15\MơSa!s@ Y_WnKX`.9ͪ/v5k)fVN!(U { 7ְ044)+nq` L\=۬k+ nH] xt_!J>߼%WId>wWc*6FAq A\WWk:赽J}xY=/?Ҳ˝V֚k b=7BZH (~_ )~6c1dd*XH`>Kd| aQ:ĎrjmV)3nq#ՍV5.Xp&#TGt}J}xKԯ 0>R+ʱ2d6 _>܌[1 oq 7{{t*Eywe% #z~*3*7_5^Hz_?CBIڟ %T)Uu{1Z(\YYl97{q~V?[Fc_CsZnUtgn~6<\],깰k8m: VZmkYPs\\Y\G|4>,np-FHܪ7>u!,@Z qq a2<}kk[F\[hY1Yk]ppa\Ksvf ΁܆nsZ֖@Ie Vc3]v6m!O&yDI8.ok-eOl3!zFk\sak?t 6KËLBՏEJId\ִ<h_>\ZǸOZ){>zMXc[$5D}UnRr@5{L%q]lmsP=igV;.kgk+O>!-?&vm;,6e6Zֵ-<Z]"̇͌}7> 粫gAewc]fUcl"Cb "t߫:M^MZԹ>{J6qOe=/ai7v@C:0Z&V !ŒFu]C9z~:==h5]Y@;]Usl𖟏wZuT-`c^ KC K ÈL<6Kwl}ƽ2C(9Nuո\is_oa,o܀ϫF[loҶo%}t=UNYl-̖쇸6%Xz,*l@en0mqc]DZA!߫8g d:08@ӻO{&]גTAuD W^Y6>eEκ+,]*Ha8S$ȗU?A\(lT0_!ų;HQbZ~yk+qk8'DRkVyXzEP+sn"W` p׸7 3tn5ck.}HfX o摨T:wCv[,.1Sq[EV:٭[{%{UK/uY cl{z%}bfue0ϱn@%a޽nX]}0ޛkcY[w:tL.9.vMd7i~Vyϡ9NJs[-{]ap+ۯtK+9KF$>DZm:n74nkcpWb9m acI5sad?8P?k[sqA$RF웪+kpkuan|[2:Isc-c{C i !l6H nNa.VBkoOsٻ_҃6'D~ñֽ@Z^ /3#TX+h7 w괽'N9PwSKMUdVֵ`N&ͤFTgjVE7M`%Ճty)MM5cT" 5sw!cEfiqpw'ݴV-c>ŃݱVwK3&'m6?-*nƶ@iu>IhkwCH`2_$[)Ue-m moͤZ67t*?U1ڪcPt1/ |] q^[~@eŮni-tsj$UYi: SKMgZ@ne6, =@44m:+ÖP,h6  vS,E}496 ]>\%+K.cdRX%۶G8OZ80[Nݍ/0[)f^5mv5hrR5;4&5BoEn54 N'5͟|Ls01,5c!64HlK0%5]sC./練unۣU}[ƪXHM,s?nFєs[+ctv,n,fuNk?3&ʨk_S*cci]o sl c=/ hAK$Yn}42IkG{apknv]۱x6AZg%; CX;p v Dģ7XAu~i뾪Sux5Xh4qmԺ6oZlqc 7X]gA]^sev5μ_[.{J+CIf`:v ITW1[VhTځk .t7NtwIʻ}+1snwtW/ѧ# 5/4o.fvO۲vs7iqqkw@ kmOef-W>oVKIOY-$i${Uq5XZ@n~5eWYSnudhYꖛ6bf5Pw\--4X-6Zlǰ8ݢڤ~K݉M;Z 7Q.G,Rs}^浍{@'ͻ,sIYS[pbdS=g6ZѺK@.I"^o̦ΝkMVfRtp4Z\$T2z6VUmɿm>S4ױC՗^-~0;뭎$n[L[:G~ 2Cu>ke.`&4 v|eϯ(א묻V* jѷNcݴ[x,eOx{nCn +%%k13jxu8fWk宩6RNZ8V7&ǖWcZmA:R:6rH/k}s[Xډi s(>K [XvN5g?~MG5鸰Ҳ{4Dm~["]amfE!Z_Z~-su5;Kو$KgQљu6 CK@.Āf'*: JU>XG =eN66eb\"D{]zߍeVͮ}$6$nszp}67\vC W+NKYؖv{v egvzƑ{@*$[:Xl-JBi)qa2.S[cgw;kCOec s1)c[{`sp?Ewm7v͆ZHkV11ŧMt01CDIHU563[3[o[M~a!W%Wcc$@BJEܬܼCF' 7{ߕsc?upAߖK[ 4nGR @4+g0T%HO脒?K+UZ;5juT){6^⿤q쬿_?鹬ɱعBl[uY@x$F7e`2׶W]fs7hvS3tg}/|h$nBn,cw=ƒX\Ǐ[ Zf~ ۱1e.ppMcwĥJn3t;^Ek*ij7i?Ahѭx^krYqZƵC`h8әBe^ʬcCGqmPB$.cRc갃8hzD "ڜaUeŞYCX:;8U58ݼc[$XzuwcJ#氲P7#?wp#Ulgtr.(nxjm{ K5.{÷_+-kr1 5\lvn %Cko?7WWm{ZFג;DhtG>RU-w<6F·ʲ+YWʩY[[K #O}[:Nq챁hs߶Awnunr*md ~/]@ eXHlH߫;~. p6{@ '{&}\j4hMw: 8}-А]ueSɽ-kG 2Ue ppNyۘʽ;\7  y#)icF}e qT?TkSŔ[kj0{ny/4Q4R9m j:wEλ2d5т)e>mwk }_}I:EgCex;' P:ETu1Hqvߎ>*oS]P5Vlȴ_cY%Tj,ldlz]wV@4亪y t:cc?D+n?=1e xڙ.ǵE/,;aTWzpc)ksZܝ9Mߧϊ՟Oeug ]3wcn6Y&Cٌ5G9:zu Kogo.1kcڛ5t!|OP衶FmOi&,Eme'k~"}U]E/c.kCqxKN׵VfW8`~5cō294)7ZYU!}-Mu{KtE=O㑎>lZ|UJӍulYheѯަ#V}b e:73ېr>.itRUv5N#Ӫ}'cu~;os95~nd>ީ0UvECY&P)mT=ikp 2A,I|DCMz!u-E4Ih##Uʚ[k)6_a=Y;햵ַкrm72FdH~~׻>V ^l}7N:h'.YqNX죍:.UQEswװS_#NoƿĚݵwJFZ["_3gbGSm`C>;8L_Ge!x"Ǹ3>iP}ޯ/Yiumh{"\KZ"Fmxk]eYuBSFm(#,[;ZYCߚCmTZ [k-{RPY_z i[zֺ03;Pn5V%sv 5{ cnk{>tTj9XWmvWm4ާ'Oiogy;=;#([qv[M>cUkr:p.hv]!sni5r9M_g2*[[Ys?#Tjs:CF+.iqo{Luel6f=nGagL;$aYE1uX,v_ۭVN*e>Kf?G+~-uNM,c7tGXlkN3+:Wb+7>Z.;כ hoqi߫F24_fK,V^=AϦzVZeSWo*}ZMN5zUdL_Ut.y6PCuwZNy!z%[wdRwׇE}iU \p7jcm9 w8䚿WSgUe[R H5tu_]t1sNV;ڧkql7{gn4=pkA;XA:>VkRk2~7cL>f۩ #k̇eE>^~)PRz=j|ZlM16{wh{-zEy" 'NYjQ)huYa9ݲ5~+ȤYee PtwL{[.a {E$vF :*NVy{WY<'d3[uuhnu1}Yw@umj*D-%J}QwL.Kcfw^VZ]o[Y|;}_^ֶcx4:~.=պvT{K/61<߽* oMV,qf;vF6m}u-%&X'X}[άޮ9v;in8(;t|.c kzK pI㔈Y6ڱ4e5n`{KliMӺJWeE74V^kΚ*?Wv#.c88_TKhӲ?k龇s+xm-C];9"z)=GXȥA ivӵϷﶦ&gEk>򮾫,k.l:6jveWrbz `c].f8ߜ}Q%A_u (6כ ޣay?6ZǺ8 ,ׇF&Rn;gmosFvAGA0_[kwis0܉6Hȡ+p=;+s/SP=Fg>K*Y:Ml^,G;?`gװBi t4"̇3;(ca{[75yփc߼՟,u[\^,hkËNT2l ]]~=ݸs滋>̆`k`A6a 'Gk6,;2m40=']6祐mbj602 ­j΋[/kQmǷ7nalmUVr6U{&i64ZYH#跨tGKsK^ K; Gss^*k- /$Dïckȭe{{Me;WAdF?W2o96e][lfCI;r-kC{'N]jRLLJ3ϸ5Vt4Ulxw\h9xRv~]v q`icV沚4f+_iyY~=j{CK\ _c]{?Q᤻9X"c+e|IAdjũ=??O$N~]m.4,WM58t hV1&V=Y4j&A U226ي3'm^%fmׇ @mf& 5>O/DI*R?PmlcGU^_epX8$vJ//++8 X4w'FJzp~(Ub>\ rS3ܬAHO X`|Օ^aKڟ %j?D$RW1W`Փr۝˚jcXnua)U/TʸaF>Uaw 4V:vNWF_Y!gNlF`c9`,,$GTzL2+em;lʋZ>=o;Ϸݯ`c}Wmv+:=őPZUs."5Kj⑷Ө0'ϣ[֗94 .6 -ue-k/ KN`7UwJ¦M)4YclmN6].nau]k\2X^Fҡ]?%mXc- ie4EV8n:k{X~ }88澷gY4x(lSušcbk>uoqk lwv}Ǘ6ks$;|m{et|imUы`.SY>cmH,Ro1,y5^OW}OἄW] S-/r:};}K6{tw%d]s-,icǦ6x}aoRW{3w:E4}w>:vvBUwgk#*Tݹ奻Ii7i0f9H Cx?Ko՛c}1Apl!8d6ô- xBv_,zF*]P~)=uu2p1M}+O{9L쾜۽Wuֹ+'^ !ٙoXR/{=cUϰ#ia}^ǯ"u evCeVYc0#WG cZ a$w`37%;`;eD?GK7fd^9低uZRk6ejϫٖ7"dUY]]u9H $8JlF4ofPˇ}f#^ol6]豺gA3#kXkNl:ӾeQs]c}xSfCKU{wL|ҠXd1OSXükK5v Kİo2FW=WռΫbdz6>oi;Z] +ʜN9c~#QD SVfcZ83,TÚ\vMK.awsL䖬8xN]mėzDNi%VRϴ>Vn9Jz)~GOccnj ]GsfxMM x}Yϣ*K,[[ʹ?'M\w7XՋvmqxC'?+w@{6PO|lZX^K[ZKv엀ec_.-k.]`tQxz/\h@Eg[Cb@$p؜MylѧvBm+:v27, ~u 1]P5XcCV楅o? l[QŰ:K79c ?TX6'p*9 8>ηX{\[@ioqUZ>]~i NHƫds$Zѽ'Qt'hdʅPlm:~ahJţꫩWN;=F>Wlx;{IBu]ήa, %4^wze_>{`+nyisqc˵,#k;6ܧ'6@-w&Q-lr648l_PQQ)nk긑SB7`˵a\a { r>uq1+ղὰ ިv8K`*YyVz8\E{fױ?1gy<5OsetB:K'! BѯzvN&-fnUƺX0q twMencڽ6hx%%Fq6 =VqQOFIyM:\G~n\5 mms1cOpkB(n>?'9*Άs\8;CU8ֵbfȉ}5&@zo!a=Զo ijYkS8.5dGCMey eI1wd3~6d6 `]d,v?_`s_X5f8⅃o3/"[S!}즡3#_JᶷZmodGirs[mSG8WvԪmŴq\b>e- {Ot.u y T\1ݎCq:~GiJE=`aeޫC.ip"f1PaMxܰ`uu ,7=EdO9n:#S23:M/h=eGgw:D𪣠$x/F?3;M|bZRH&V~~; pL.\K7oYH L,^ҲK_]K7 Y7t" $ju wsYM} 9qnkw Ql+ȤUkCծ;C&˱6;#ki-cY#G N8X@HQQѯz^~;IHw6z 7GU:ƶ( nAIH~^NN.;ee Nj5AVQ#ӽ^r#k9އPϺaKڟ %j?D$RW1ޣNHŲC5xkk;%)UOTʾjj!ƚ\}(Ѻ=#qև?D"zCqYSݓP=A̶j ٹ iw _qq_SZ }w6}V5اl.u5oD~+[z^63(cK1FE {@.{ ZGT6}^8]^q뮺@Vst4T?;y8kL1GcNO]Ye5)h}; qpo>; VRlz]%c[Y6 _WrV\ [rOu>w1ῗDW/CPmv=̰0:Vz5mms7V"^V;>Լ \kcm68K]V2j3 uF.ap]{.;CH媻=3ʥc[ID|/?}ͳe5{+휚[c~~)3uj+el Ґ-҆Ւ<<ݽ+7ZlmL/&}صzƖAC't\OoU,tZ\=rkⴰ~!f=m)ä{BT*/z_Pܶ`n 0;W3spT1YȰKd b}lŽ2YUg X[9ֺ7kH;Mw[=/?};=*[_c:7GKyTUֲdm` ppѿ]7,j/>='T ڜ\m;5x0\rT_QX--~%CPKߦHfAv-&Knh~k@Cq\Oj˫'#uVzMs=Wmv=[i1|nl~+s*`ս_/`A_!8}T77 Rf>CoY >\ǹ4Z,ʵgF?bƬWSkxXU&F%$N?}*_EkVƸ .fs^i-}mOt3٧Äj~ՋM_gEu2@omx!)PWwG◧W;w1C ;[uk. E57.[Qs\7v1Ѝ\7PYc.Fu-k[ZA|l[2\F3]FK&ֲTgky~HyVM99ns]cN;y!h)R=Kkks^$)!8zΑ]*eU;Ӱh@24Jٔu,KY  [X^Cd0{C}^W=w6?ZZ=CcX#ݽ5souvdg긏CR]_H~)!8$CqP6{:gKr]1vƻu{:{e%FU5ms+{@ ~ 1|u#IȐZ{Tt,ܳjcic̾1YkgY=>8j[!zk#oi#CW?}?w,=ֻmXŷ CjcwA#'˕cݏqMNpe; "2?4* K2s)}8qr㣣"[Z-n<.!2W3o̫m6 2.暍mV]cMsfdYCz{k ;w}t'Mbz=W?yސW=:A[-yn4ֹ]7ݙVV<4WQoFPQߢ?}!8ck3:V|v^n/!ƳSZ+ֻ᧊m_l9[cO`cO ⣧7!8−JaQ; &I w?ll$Ls]ֹmmzG@tHaNϤ?}ސQ I)-5ŠZ.(zWT˪ϮƇ HU>FUF62;`x5f#|3};9V8 XHGGoH~)zCqDI%#ڇgWluCa/kZ@F%0+ '"j2 bO??}_ ,ֶHfw3#JD#PSʰaIHO脒?K+UZn~ 9Mķ,Oٵ݁:?_??'"ۓV^Co0;iflDnm4_굡ŚNӠ?GOwȬ}ĭ/mcֿk9Ώ'eΑ>5[c=Bְ zm.q.zOE"7Ȯz]0on;Kl;.-.>O(UtmƵ?\ok]ѸLp*z"y ȝwvW?:;r,٥[kO?HI'CĆe]Q`i`kwH> Whd;т6wd_I"|ٲC᛭dPi;Hfj]YcuE[CYs`x.QkFDwI?O,|(munE6mn-'3m=C]Ԙjm1c.-i<(NakˬuM@.KJ]KT%^㴃ĴPB'\zFm}+Y&=R湦Ks]pY[ǮmHkc@DJ~wCK4;Z~sLc{h}-i ҲrfUtLgS3[=ͬZ˘KibUjGUnk.}O6fO".[%B] z:-WkDx?E'3n\5DJ=_7aoaq[!EvAgR:aI\"jICȩQsv!wGU+yml#'p6t[[[jeҴ %|]'!*ws) 7\ -[!䵻dLvۤb?6n{oǽ<`}TFC^w삕 /E?QE ܖbck}BC`|1VIĿ?~#k9Ye,e԰ m[5Egum2CZ=JͣlR+wP~ƾ׵֝> ΩC~E[mm}t;h`qn[}-M$%Jwa㋝ zNKCD58MUѺ}[KrN K-{wC]µzWP,-c.mh )ٮ,3'ݧ>5c*[ 74Obtq}+mȭk4:}ÙU:Vÿ 09P87s' PON Mt ^}Er= r2\+;ӹH%RG߯"ݯחۤ?5sTn1ok {îY*YB /R8qʦK\KnϹ-_4=7KwȮrՅUiaw#=oW8PbMjC.fOWR˔#;ḛdc3_s7÷Wbus^ElYnlAicq :R]?+kmlK c7c'۷(t^mqEouspZב\|үz?IPmO~#n{`5KƾS0F;s!UzoP99yY9>-ʲRk*ur?M[}+Jf5Āa?㯨:#):U宬]g8gNY3EVvXz⺏K>4)PUvz/E߾"}GѺK/!CX}f=ζ}3 4d:"Wkiin`~ۦB[NH"UN&{y</k*S5OG1aR)%Wg]C P_GowG~:d_vutX},CwNW;|粛0:)bڟ]n19sM!iRզƇTwjwG܅M^kl bK`,&Ωc0]wE9V+oxXt.p_uu.Yc_]ZvJ+ԽCs9$??+['3"qKsTE 36YjeXuo{n65@Z^?ȥVMXfv;}M_YY#Īޣa6c̋nk6[v۵#j4?M9&^{٦N-,'Fz~nKq[C^o76Qۈ5CWowGzg#ȇSN|6[~M7rI-ZIzlcl=p .VKVF5G!CKZSRtk1:n.-lā$6>j=zg#zg#ȉ$~Hrk>u^^X1XKPDj#s~wc}dݏcH%ķvEGLL#oeMӴHR~HrBDXu<*3*?CBIڟ %Tb*Q~f5y,ij1ɳRvHR8 +U ѕC2Y}wl0Mv5l#$.;aӳ)3c9o}vag1{pG=Axl`6;{eT{l'ln?^OOQo-wӷ։o$:z`PW`8?vSaB$WU= <0qlU?No.{*FMcnmeMַݷw@ߐ[\snqD}=dѭOӳ)˖'}Van6vhjUf3l%,m/?Fƶ=_{۩JzH yp<֑ZA{m/` }+ɮE9 ^[LL$ My; ̈́ommFko+0%qliGn^O\xtZ"JcLu#\Z\=&buc,,ǭZꁹb7GxCeջ _h㵳sL 2Ccs "N+7*J.=Y'{ m̺u6ՑeV\cƾY2c6i*_"ޝOLk\Ca@.o,}kE.e7l;~CC!HRg>ӝ˗ l37Rz6Z1|Ap[unf]c9e 2}8-TN,cSْpCnXl,.`ieӺTXeL05nnw[tnM2^nsy9l]]zK^ D;?ҟܹ\,feђ)cccw2ֺ@s1jG\~-y ʪ^KbYZN=ү z/No(`sFd5N/}Xq~Z[VllC+q}>ZwsAiRNo&ks[[jɩm~טiJ!¿{r%>f'W6nFk)mz>㸑OGqm5>Mphhh SS [e/c᪗[&:-?G]%kb?}۸axIW"RtK;x%L*9F$)zp]5OcMՋ~˝[Dvֿz?Dv^*zP_ݺ8kO K6v h`c4~C̱l,p?ġ,pI72Q{Z֟X˜}kׂ+gS7gS7ֆHhNljN)zoHo"$ ?"uپcuڳ,SssF}ޝ>'/No.o#u7#Z-s2 }`>;;M6\\Knk+ds]x}8^$4sƤ64~@of#p,;#h xC12d,f -pcFG!]5.$|tKӳ)˚_}ܽloٽ/[gM??c6<^XgXKH5_ޑϽq?Bs5dd{>kwV}y|Cu&dbYe`eUQapd{ Z|үΕ?s-?s$eOH6#kuIHv?sV94cߔ]{*kO |,E.lEo76!9 So%o"RIMKo_(~5;#49mpA˷SU/")4='^vS5$Oo%o"$) e?پu^]gٝ[_,k\tkJ]k}- ݫwcE=?sRW]]"[K{=MBIHY: >*3*?CBIڟ %Tb*N˛}YQ)YdՓf]UEd` M3-:WG[}VKvJ}eFe4zFUm//Q`Ǝ!QcH#mt3.{=F1#se*Sm 7m 7/vKﳗ9ѣ=hU`=WgbaTRwz~Ϣ#IDSџP D1}s8I'+%;5bp%]@߬בmSr nz4} iR5CD '%>ۿڹJ:'^s ڬ{^ck\OޭT,m+icj]cn9&# jZIn?Yyci'8[-`5, ~k(-ᅶ7[t%Um]_%d9@0c\;zZ-i-@4H'TnH?j)?c MoqcklCoLq)yvSx/;{mp5ΰRh#p^=Pydmwtͳ|ct`j$üȳד{nv[k.evJM}\us/R "N~ZZ}i] z`,pm!>μu1+pᶝKh5A7YpZ0@_^/{Cs^ÚFr }s8gVÒli}hV>@t+2:;;Hkr]@ѷvʕ~t7wExXjw{ό$Q։0%IrmN3q/荛Fhu';&쁐Blkm4oR#}B`OɓY:umm.`V@pasx}Zʪ쨯׶XEQM]f#V5t|m>uVzzd[, 1kF]?z1]Ahvuo'14T6X;~iƪ=u~ge Y{[ oT7xLO έ23]5Xw4 6:pῪ0{ p.~i;n٘"4S,lwoTY4X֒XpX7UӫfS-Ye `!6>T:6ñs_ N}d?zqCzXM;#J}ۣc8hy]],a#[nH?jPg[[F55ql?D< XlVd>ёV $d~vmKm\3>22~m45e6ͻZNY t}f;9{orH%an@%WsX% {~ÂE`Ѯ0~װ߅]01盛gd6zFn=mScC5e~c4: wz-s[r]S]-hƯ-奇x7z8 2\4Vz g7l- 0{ ݿ-JTtvvԶafugcY.{h]n[+1:A)t=46[.aܵ;nH?jmmU2Qŕ 'pV:.EgCE.cKwz~>@j 7m 7SY {)x hѼ8i-A?guv87_M![ -59bwCM+k{N@5-$mPkml >+6K$Z\5ILsK DO-F,fKhq,߫> M*R=d ; Lʄ)_CBI% +Q  6VZn73dl9 Ų2X7.fD5;p?F]LndA%>Y}M~c˭]v0; Qf\v$nm\IsO\> Rg$_Is_׽zu<VI8\7P]Q[bӕR]72`h'HSDx䒋yq.w UVb\] tyxyXrS/-fT\lֈ*SXr H<_IruXEl| >ƍCnc2._6{+ֻgmMk-ׄ:Q;8˨ӔʯK,f#1cEUƴ@vuKYS^꭬6{,.[:'`Ӯ m.X[[@7VipyJMo72G"I(7cYM߉Smq{l<:+vӵn,rF5y9s#[*c@ik|\Oa^*+z(_HC\=G!K =A;Y W?)Sm̲sq/l;ԨĻhnc%JtgIiG2r.xu]v95q- "m$lpn-ڧUpʣpܝVgK'OM4Y齻Z+LDvA J$Rϸ|.2 63t%edu :ո8[P5쵄Ao"H/30[fALcT:!Jٻ]VT_0{gH$q3?$gH$VYQenn.!M[]~ֵ EI%#ybߦgC<* O@>0 Rg {D:A>>>ogM}-&am. 1 ν]qvCAm~@&d Zo7obىEp]]{If/}$_e²m3' 8=-H!h^ n?0yq*'*_'tiƲٝ3rȱvCFL5;$nn)y>KDmoA)WTti??$gH$q3?$gH$}futH3s,_4IHg$SmlnCCd=ګs]vm_u:2֗m@SOI(i"RIMpla{>{"Eϸ_[m{j~Ef:Zgs p[\_Iï s)`xw H F_4䒋y"I)_4.l6};!_,FC_BJIi̟?$g$~ ]NƇ{qlys:,4AJ/HuN.A8 +VE az7g;twg#{}Y=S"Bri7Ue^lw:Vv&:)q5U=$#rS˞_V;9Ŗ9 j @/k)s9Z}׵Q:ꉉESNG>%9ܹ,cYy-]Ǹ g^]mVu%Do*觧}Jr?r,yom.c6C[G5ײWZfY`8$oK_c3*rY]/A)S驔Uck.v26P32r(ϵn}^%W^v#Cjl>iQUO>ϽNG>.jn̝ebT_h[Sl]$jgYomk[/}. 1xJ4=$#rS˝W=،eOUsõ-$haVoPk7Vit'GϽNG>.l^H6TK=q㮈_z͟dvFQ{ʃ6>VO@_Jr?)GaL.WP~E9SEiX5*"ߊ7U_Iu@\QǮC$^ԯmd k-TUMZ7x\߄ܱz-c?05}\Ƃ\5J}Jr?;Pv/23ƖRj[4}9:(`uNS@?cuW:9TEc;ܕ)g'{kճ]nQʇ+yuƐt%.}_6hfVCq9[c\o*G>%9ܹ6dU5] I=7_S- FnkxmokMZ7͎4JzG#{%ֺnn>74?ժ?d75󪵍:ciֵ}/Ieсx+z9Fn@zY,k;:3Vk"[DXn%; =)WA _ZS˕m}-ݕzCuupmxWmQа-mŶ)e]׏ZUW3kr׆[O UzV6!>Q_ /Uv?]n͠S.~3ko ͲUthC+}u*I)~($G$_G−JG~*GO4{dHl}b~.{ Vk\͞$|O=[uT0OqsW#{)8:_61<4XIņ#Gkq~r[oX]m{&WogL5~Mm{6-nsTq>.-;0eN|Ԯfɸm$|[)qiNUεns$k+elmu5@*H)~($G$_G−JG~*z۲5(s=~}Zn4?ҽf_GRIHk'pt)~릁]_lgnIwmUޏF[fJcCTz6?O/#DI%#??B}ς:mVM>9ЍL%$c??4w` --DHOwDcU^BJCj?D$_I_7¯?^؍Ac|*N4^?RwbR׷^߸?TRJʙz;1K׷IYS/^)z;1QI+*GXˬUs 5s-}nG*k*mcCRI.#L{pE$o{p2wbo^?RwbVTħ }is{h$7~V{p@q8myqs[kN5Ԓ6ױk@ htjd=׻^?TRBʙz~;1K׻IYS/^)z~;1QI+*e/^*)%eL{pE$F܇䊀ƵvX\Z8p~AޓŵȇRI#L6\Lܢ;1QI"^?RwbVTַ^?TRJʙz;1KַIYS/Z)z;1QI+*e[E{]I$Cu^RӼ <xP6*vw$=Ԏ6m_=G:;F<7o*e[/Z*)%eLkp[E$okp2wbo^?Tk07h1?gRI+*e[/Z*)%eJmv3%/Z*)%eLkp[E$oev7xe$^?RwbVTַ@h\%$M ?KI;1nqen6 J?nzlKh-iI@d_CNN/ ZwRt?)RӺd_K?▝ԝ$ _Au Ru'I?-;:HAu)iI@d_KNN/ ZwRt?)RӺd_K?▝ԝ$ _Au Ru'I?-;:HAu)iI@d_KNN/ ZwRt?)RӺd_K?▝ԝ$ _Au Ru'I?-;:HAu)iI@d_KNN/ ZwRt?)RӺd_K?▝ԝ$ _Au Ru'I?-;:HAu)iI@d_KNN/ ZwRt?)RӺoo*Au)iKڟ *o[ǫ~M'? endstream endobj 214 0 obj<>stream HKqH)A" f%2z*Z,"$M'̹9)Y-ݏyw߻mnkll[}aOd6 gyRxmK_.>7*%rJtQK*Q C˦[X8"JyJJn,C6,l퇆!!_*W6?5'jv;!h>|O0 |R)K͍(F;O \Ƒ ds,oFh.|-rtܜ;ku8`k %< J*&TI [݃}ڒ7(^roʉ0]*zl_J2*  >gu=@ZOxWF$Сn6$"kqrt]DNXl6y&“u]L`xǰ Ă`, d|g+_LĎĮTk=$M$߿0V endstream endobj 215 0 obj<>stream   !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYYZ[\]^_`abcdefghijklmnopqrstujvwxyz{|}~jS endstream endobj 216 0 obj<>/ColorSpace<>/Font<>/ProcSet[/PDF/Text/ImageC/ImageI]/ExtGState<>>>/Type/Page>> endobj 217 0 obj<>stream HWk6_"bć^@wiY`13@#kJ,ɕ%$< OE|$ ">h|_;s.jgwz\YeZ@4!O|B՗XGQG@Y/O]}]o/ŷK?ռOȩدy훼QĘ\GcG(vhHE9&~9pMPe]I/wU :.FAJ Pz>F'`8!_%A@1p8`^>I`y%DTRQWd/!dHpuǬ`3q[M~O EQ2 |G/wP1ѸKW{VDɎ܇Qn!ZΆ}äi(9L Rc7-bB82p!j~.փM%kN[-x%uhi\̳MZm ވw|"F> T |ZtY j) Q>CtKu04&DP%zBmӿ`fSb&M.ۃPd`&R2ZJ58IҌ8|vgcըmɄ J@ n8PAvDntg]?lc|rtLoSUAc ζB[bAXۤ`- e #l(4|$%cA]NCX}qtRˌ Ng^%UD.m@ >4xb>j,<&Ըn.?x%kc+^vQark:|+dD6"eQ 7w㿬.!hT@#$qQd;VbBKC_;Rje|?P>?m18+{$? &Y ``!= ig6 B4nHlGT>룄oMKדbpDX2CڀgK$&r)t %֜q~DmuRf[2nlhB%eYfT:ٝΗ^>XzԭhAM!͞tY/<#6+x׮^M9V |5W0=7JW]4xZ["pM]H ? vgZ _4kX tu4$N[M2 ,Hu -E*Η!f{ODHWݦ?m:5iѵh_ >ҭӋCN47 ;e ;K[]zA}v-, ۤʣyb)E m'ebFĦH&1‡ryФ\d5U|6O#B;Jlw ӛj%- ľZ %,KAA73>Vri $x,SU7%$02\^klA}H*RأfQ w;w/bn;;ڨqHY ,mňvI|Vi_u9Ȧ7l).Ϻ i eo5ٶgΰ &?ssRAŶ(pC嵡+JM){BݩUt?tб# g"Ss!_5)ȉB4/dze +"f|]~g<%˞WduIp8=}1\t"B #.Xu@t=fBR5I(&Ÿ>7O3X_>]RK;M1b;jN'MxE#(LCdr2Ol\ v k_Ⱦ˴+2]o;< ʴJEE.^>ՓƗ %!gҎ\[]hi-äS& W;[=LՌGx-ZcEIsmWԬoQ>Z1_ ؖrM.T`8=Dcn]z\ӯ(Ѫlֆ҃b3{U-J\y:0(ӽ< MP,ͻ\[Fb$`HBky Ө*-U^q-mIׇy)d$u[Py jKpls)~J CeSd,&ϟRnb嘱Vo d+|:'Eפ rS)&n0ڥ̭fO(6.ϊBKΦ7 Ox7 ]B2qGQ?ն( )fw;'! V!"5vƷߟ"K:!3Y2 Kδ"~BP-4io#i1 ˶~O'ޅTywe tޑnSB vf RlSFjx4(dӶEwoS @J!>stream Hn`qnh2uHbe`'bp^œ.1-S&Ee`X`(Zh}\B`笵y*֕w |.?/*6.2׵N5F1&4Ɨq8tz~^Y]CᏓYU)>:,U=Idf4O*Hj!.eػ;Dq"Gmq endstream endobj 219 0 obj<>stream   !"#$%&'()*+,-./0123456789:;<0=>?@ABCDEFGHIJKLMNOPQRSTUVWIXYZ[\]^_`abcdefYg[hijklmnloWpqrstuvwxxyzW{|}~IGH endstream endobj 220 0 obj<>/ColorSpace<>/Font<>/ProcSet[/PDF/Text/ImageC/ImageI]/ExtGState<>>>/Type/Page>> endobj 221 0 obj<>stream HWnH}W\D7`gf2Yd_^ԲHlx~ f5[gYG,9|aB̙[g3Q19F6Rޱ"7T8JOJt]gΊ/31r"" ~U6L5eu,@V')cBcl8:=}xTܽcUh8at[NXDہ=F̈oX"SoĐ-9Q1[4l<[>+ mDeg iNA[,y1 C[Z MU׮\eU>xEFC1k40WҭKP5P$|TQM7o&[yi$4( 4`[P'Ur0@2,1"|M)WXN)ȋ"N^؅/άC+uh1#:&; S>³u2Q2fܘDi÷&:#8ϴnko?0uwB=gfW` G6#qAB-"EAЕvѦ<4vB5 lpp:R0㦤..~0qbZ1O׸ Ɉ,&4G} 㥬s⏘۵'a/mGPRl1.k EE5@Gat6 *ߖ_9r ð6jX6 &j5 yGD/YCr8^(BۍEOy|cWK1߭>9&zʷLZ'}~u-0qܖcm`KRC^e-~1XrGGVmpa5}` þQ \#ϸWsQYwg˕WuD!zY齫M-w=+_$8K-:?#T}*EIܿݺb-V Od> Y*czȭ 6:B&n6R8>KC$nWVG҄5?Ա(pţbgrbWHxѲ^Dǡ3pbPݦުƁwU Km s?vsӿ&'U`3%I;hvVVjBp1f [>ȴԸkeXn/SifE]CI'ֶ,=/!%fqyҿ"J)wkӆe`P07Y]%K6sP:xC6G`(o'N/\hR{=PX꥞{ٞqvH{G}K/G>U~lF¥s#tAIFďɔ%$l|XW7c/B RvjaU3j@gMS }^2'Ot$4a $_Uv c1)tf.u~# 6l%jW쫙ځ@U|;6V-̓Sh+4·?N8>yXdL8>0I9͟Bb\= C,DBu]RJD’;8f{q5 F骶7`Fa6oF322oІl)_Ă4x-ouփ~7q%[n\eE>7)eH$}ӡxyx^M.UaU["=եAhvspj[}1\ *3y,41@9v½x".SҘ"F  McAƢ§D2W;BnGV(FҪ `TQDsp.3QiЦYl{ghX~}>i ƫ 4Quޔ›2qa% .Z;|Tη}C>=X㜳o>stream HRO[[++X=3K-KeDaiV"Gt]ΚWRQ@+ D0P^?D`,/7lx<57; \8p/x3={Uo B3ӧP$EdJq詜js#@؅%_*vx@կR);{?+zJ kwpL |`e+,0ibꭆ烈&ŒL~gt _@/^44=zhxu+Z归.IsJ_̭SS1Le;}Lm&}2CuQ?>i0U%'VpƁoy+έR;J"iC. > U?:oá짣3 :k ֱSf џ ~ǟ4U~!DQ#kiwQ|i `s5 ֟L2.ׄpDQy_|Z (l#ݦ$ё\NSa!*mSՒvta8z5{˒-ID$|fQkh'^j/d3ximIM 렸I,4n?6ZYZ 5Xt%UynA}{0Je\o4;pQGQ q־CBjnV|rzDZspLz,#n^ endstream endobj 223 0 obj<>stream   !"#$%&'()*+,-./0123456789:;<=>?@ABCDE&FGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopaqrstuvwxyz{|}~pAA1p&& endstream endobj 224 0 obj<>/ColorSpace<>/Font<>/ProcSet[/PDF/Text/ImageC/ImageI]/ExtGState<>>>/Type/Page>> endobj 225 0 obj<>stream HWnH}WZDm X`gdG,(eq" Iٛ-R7r>lF,ԩS_FT/C$^B`ބ'^-G╣/x(ᄆib4 У(? zztsjdD;nޱ<4M?=7.ҘGX_n=b@ `$ a3Uy}#4荎6h!aenk`x⽟BsFDWE_ Ov=) EbeNuP[cRơD 9)`Wߧ?J2lEuHQWhP@U@:lax@eq$=+MJubAiq/z^#_U@%Pm4<|5,Jα@& +^kju*8sȶj#hG?Z m%HuNk]k`U t$6W6k%Ŭ|L<8AdFšFǬ.:z}j+C})'',"[“]|*b[%s摽R& )EJJ= I(H~C wVIWUJ/`U5sU<9mP25Ҁ-3Ѿ _ahfA9s&<VZa4p W3*ꓗ.M"簄WF֟ƸfS8[i fJBͪȋvfa 8^iZ $@۞U9`Y&?&uꀨ8qߋ%jk:ӝ:N=~Ʊz*Js,|< ǢWO L_`6%l<3̨V5šكfD,QRbc*?R]{/蚇[V͵\md6W`Z;ײ ](POvaAC=If#aBn {x0 P׮#^TEi!o]K$w np):] '\nn]PRueCr;_wp'_t+;wI{p3>g/32?=,rm%J:⛠ΦVhP1 `}qݱ[t3~WEN 5u8~j۶.J4MAb+=?"geA3erD%.ӥn˶X4zNځd>i jTgrN&W0WrjB&7Y-"@,Ε$¶P)sx.<|dZww^b\ Qjmp =~:2J{חz,$4Pp/_nԻFFoO D>>4lzX66܋O4CET īFoYFKl4B`n>RDLQ^7U)q.ָLܿ$$0CHH̰ߍI238ORmԉHC"k&Qf_׌$gz3@DqXB0dz__@7T 1m=`D;лe*~Y&-S?5?ڢmpE#3AEbnv2a'hteYxì=CNr1ئD>ptS*iV,k 0p>߰ArhILd|C}?@xpOhơb,|4}0= }mIkO&NZPa6bԂ44qYR IYid9/JT@6B}5cCzRuV~s{Ddd-n2 }:_ Ň9GjQ )q,+n9CuhDyoSn6 x_wiM/MC"7ۇٴ*AemAt-HHY,pcIJQ %ԵvItSޢ\ȶu_pW73mν5KױBf6o?LW aF<' sXx`\9\A8Ueͪȳԭ~KGG $Ԩ!s /{?kjyr To ؽw$ҒݵdjqOMb lQ#)~ paF`^~:2J/hDI {.CnMjavuX_4x7$id +x`BF,1F?CZɥd者+TCq]JMv辮]#/+.wr^=3m9ַ4!5F/*xuVJ eӔX{D5{\j}U/1,eTrT 5= '%X~;qмcZ1IXT gת8 %, V̳ꔲrnps E%Vu(J4Un;й6Y 3 `,ЯCKs ߏ'<@5˪n-Z/N"ɖ(eE5菂/8"bCvf ~\T$$F-L5>stream Hb``/__hysf]p_~x'Oϟjkcѣ?|0)1:!.40(NGD(>6޽{0!]NUT^A=S`NE\8#&5%,:54s޵vs}W3ps)á^m1?*;>g|[N`ʝniiy̙ gZkqkÝ f3paWٓ.\ݛKr2ΰaG+ÞN &zbԎͦ;ZY63"&ziӦgvZIg]ٗZfͿ_e˖߿'O<ϟ?wu Q0@@< endstream endobj 227 0 obj<>stream   !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQGRSTUVWXYZ[\]^_ `abcdefghijklmnopqr3stuvwxyz{|}s endstream endobj 228 0 obj<>stream Hb``/_.]4Ysgt_~x'O̟jmmѣ?|0.1:86$4(@@T(*.޽{slkhnjemdka,/, k6{k>E?GC=U޶W=jg{ w&}-wI.ٗ\)/ߺ۷o-{6M?k(e ]^gΜ9|p)+l"lV;X=`3.\ݛNynQ[#{HrL^t_Ojr r 욧5gӦM99{.<\zq=wggY߿ׯ_߲e?~ٓOϟvq(h`Q endstream endobj 229 0 obj<>stream   !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_` abcdefghijklmnopqrs3tuvwxyz{|}~t endstream endobj 230 0 obj<>stream Hb``/_n]dyg]9w_~x'Oϝjcm=GϟÇ&U'iZ뷗%DGݻwo9GlL-m lTLdԲvꮞ3cR,حnVS\%r߀}dӺ!j,0z"[9' =ѱ埛o\eL̙3eϒ#L^1>stream   !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstu4vwxyz{|}~v endstream endobj 232 0 obj<>stream Hb``/_.]:Y9_~x'O̝lmmѣ?|0.>38"6,(@OW!*&޽{s/wekejjkbd,+'j`V;kc.vK<yT^U{8t߿7N/yi˅f/+ٷto߾9::z$dZ?q6;-gyΖwxgZZZ9sɳkNbsު}7n=Np›oV[Ow)Y[d֨.=$ӿ%s/_uLغXe>uM&_7ϟ?6m}RæMkjV-طŬ5k[lO<}?s׮]7n`  -[ߎ endstream endobj 233 0 obj<>stream   !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_` abcdefghijklmnopqrs3tuvwxyz{|}~t endstream endobj 234 0 obj<>stream H 1 )IR܉sdW=CC3QJsfLش7% @頤Wiz6~TBc;CUl+d. GGW]E endstream endobj 235 0 obj<>stream H엋0s6!aZ1 خz: mٞcd`f zQ= E)"cX-"zP:lBQFfdŤNLț;UbdqzB lSCj>6fRB;VR*7֝WݱYRB+{'k 1㿷gx4o}rnJkp1èp5;4 P-/j~X۱WhaqwWfGG(\e`y[ endstream endobj 236 0 obj<>stream HVyTG9YnlπP3#e2293 12 \t7 qˌx@P#>1A]q5*c7<ܕ&n5,"|?vz_w#A9\V=q$ wg6\Vlӊdކ=r JXBwlI| W`3-A7X#pr3 06T.>̆ms(F @h4Phf'B<6ՠa/&4?hϷ9:6',z3y=#lfeا̩6'5dX{7z{;tx7 ]&W$U<9O0H&tB!18Ư (yN2zcPh?A` XN${\[6l֌mw:쪾SЁ'@ֈ{:b 7`xT ݅,’dЛW8y+|H񢎧u5!8{LksjNT7}q瑭54u2i,t<=>65V$~1_O@ƳDH鳆Iz~VJa"H AxdMTD&u.z'zB17q3V8 ' oZApt2DtA+оvE\u{3]N۱nuF3;򤬗)8G^mocħtK "bK]33)ƹ'S]މ[97 xN(OO2A=f߾r\?7`x Qx5cGU w1prAD|!Èx$SR_U$evjwv )04KP=KTrN'.ziIzK>A Q C0,MXS){4kl!I 3\fzu)ԶXLZX"1 ! /S&}c2=;UqC 4YTL2,0gE:^ !G Q?WHj-z*"K%\r!*VFSBeHd"4AA#B&hi:uP:߼ PSW0 "͢(BH^IHb48IG$GQڪEmQY]~q֙UۭߑZG;kg;޻s{9hJpѱ,c1eMcb1"{~?kIjPA.|ٜU%`B[}FF@\MLGW4V1ڝ˘و`"Z֢ŵQ6t*1晌3d&"b~ g?^{5%O:$g͹4krSȅ9vzu#)1/]+)=?{A$I>fǟt}pYdՋw?G(ᦗM|z&bY&PɅx|CyŘ~A2XXN_q6~g_nX>OY(o֮ЀlPJ\@@ Ud!VvXlK1-Cu˯W^XwxGuezI#aOذ2cGC=LnGҶeo'T~=b ƒ'vs]ẽB&^붅K!E=sc]>X*unsxkBؑw+'& OHbb'KHcS  R4lWQD8:[*k0Ů}`% <jAcOY&ZHĨJf|H͐$@㝻vtlNY,C<*_>С|!3H_ ejrbrrWw-HAeˮ{}uF}s8w3LS˽;7/Ɔ-w<3s)>ۛ  ,ӷ)֊ed u qKnpdӛ3^Jx&:.{z]tGp;6K6i3Oyh~x>ilNza_}Zo.cMwX?T5qvV{V#N̼%`Zv,ai>PaU?&_jw\7SA;(D# xyx)!-"1-U!Xd@܏E_<9D8ijGUVD EsA+Db}׀p4ϻx| G C:K>q6| PJ/ec;KA{*pMVu*nV.Ȇ.f`e g)pP _ dC=xC ꎠPr5yۊ JX/;۰ z?4% ]K«:Xi ToCP m*zl{ ѯSȁ߂}?a:Ld219)QH0>z(1Z:jdIXhHpP`p?_0o/)p)F 8\#$V77`L‰b8B6Lb * rQɧ\*>7Jmɤ$و 4bLr`}%SeRLV.v]_`AA6ꩦ6H6^ g LcЌ@PU8 zB8*, :tTKpxge4AIzmj8.ľ"Rmv h-Fp`:/\$SݝkԜ_Re8XCb;ֶ¼?nm6 QLcM b¯SkDb҄8[\ïv{@A0uSb?8g߯Dk7Iw]@ J~IJ& !q7-tl:$@F()4())E?, P7 :TtQ-:;(U ,NgϽs{}gmSЈ?WMs?8_, 1 qG܂8D?XYkWEӛ_px[[[ j-?T;aC8º<Z ք4 聶@l64"[EC9}. T Z*+Fmݡ͸%67 Xg\$R VYLjbȫ . V]:̪<@U8f81K4VޠQXh>uAٗD tQx <_Qa&Ɉ:EL]0݋%ׁ/f/ dz Fqȫѡ{'+*$ܕO$#VONAĸ@)4:-IXRX2Xp⟖#xAD)QT.g}2'' ovB/͌xJQlդB֫\$uG碪.yВG;~+} e?ÙN72?M[i4Ntq^Df)6'ȅ'ğih=& ҴzZ~;2SQWEmZOZ}瞋LL3{]CEjG0/ .zU0SiǤHA{) Ƃh7.1Dr x)I.7*({$Rv+h!XƋ_hu*\ݤ(.-6*5Ԩ vѶɐ'N s;ucՠ,6)d6#tX=K[pFXP>Z_@7u6mͧaih#n QB%ɐwM\CnL[K| 2HQ^<Ձ 5}@'Q;0xFF!Dyi5b7(W]I1To@rAs_s\~C !׽'CVUjdrݥavU ;}(/kq+ݠE! S7h=X/_mW1/3ġ 6@Cz=ZI}Qw~5z?X?=OBX@SfZ1OFdaыx_G<ɦ=J͗5joj(rL(o4?G\k(C6hFy'覑F'Us{%qM{ǶuU{}<qr ujN@IΜj}<^6_MsyU|eEbМ @: h 8Fcgp^}_#ԉP0PiD5ʽG~ɟ  |PDٳ1W-6 =nrr~12Rf Bjo1C)>>NΤSR;ʦϾ`*SL$hkh`h|]7 !{Х!J\.!x y<^OH_Z^&kgLaMqW2x}>Eg&;aj@P: i.~2]x2/G7g3o"[W^E«]?OIva#SDxiJ%LID px4&MI`[/\ސ~"ZbcVW"z- 16b9;%|VNg2쮷96[f6]ȦhSHІ6J=H:}&yN74B+ЩIȃh 1c#!J%z'Wؐ;f֪7qܸxH%#}o<6Yz? yH ^tgxa'r3/Z988L}>,Ql&d&#v[izf1>9|f2sb2{b2'waKY> :+{۟푧Kz)E3HUO|1azU^B+i,S`]idi&?xsf5!- ZƮ>oC# ]y\#@P=o!5JVn<݂gіM؂P`XX@S gAՠ"U “}cvt}C_UXoTX8vt4sFkSüe $9bq47JՁh6P㨮fM55\ A5/`c9ʷtB+]P жFu{ЖVQ3: /cA1gDkEN~q!O.€+H;,B|qk_%/Rc2;i26쐢cGL>G#|yfumVN"uQ?S^FyEyY.:`;Au lV L Vbf 7jiX.jsV1pj_(pvhoSKVr[է<W76fb͋m;=#RGitestQZVٽ(lGJJ6*\lk*V?kNjj_V >9; ԆQ֊ZqGkCKivIVp$\Hoh8iu"Č۽Y|-)&i` Q'diV2 8m õί %%5 5gʳ+V+VpJ+zەV3; Jms3Nd5j:5Au*u}.RԒM u`j>CuA*-*=Sd2q7;5Xsת_)_*%9cKL0Ϲj*LKRk%O׳߽|=[fZv I1kPkRV;vWUB#[~hgP/a72l {ɀl\i{T$wg6(|=ͭ']ZnQԵy)\WufzR|q_fCj9CEIYū|ox%*k|.w* gWw^azH=ϸ kCvƒFGCCc-fRupqs;CĘv_02 "TpuleohjiFTN? 7Ued>uɜ;|ŗgؘ>o>rxБO3/|/^=ÝF*3𱕷M,MLyOBPPiKl}}8G(WNIjZ<#MlO y@8,t kSחKK]2SéW y1yQbh+n71e߶=Y:C7PݥkfݦOAUh+[X$s풤ELnkk9Qm6XPW^e~}!Y| }NR?dg%ReE%7fvlxݤ_;5K۾kb`+Ƅcp&ᭉ _l !["],HA R ȭ)HA R )HA /rG4vl1mͶ[,eP~]zxŲ$d }v ڈ+WtVnW <âueb1_Y: i<-]D`3b^&1E,s0*?d<꿰tF-) 0  `BfaR@hi*x[-pcDAKRQ,0$QC8ƱuK2I;k^,#s[u'QhaU+t##jآa1j10DT` Xdkm)+i5 GYXaIB蕏3KѼ HFc_u0BXF)^EH\G9=hNw aM,!'Qf¬=44z4{4iIڳV:Q@l(i:[xfn:mPLx:O,lQ;AKz976M݆s) "R|<>E1ϟ<{w^_#`FgRJGc#OG @@2G#z,y-2  P2jfTOz0I9¸hHረL\PCe40)eOԒI](S"ʸ1Jʘnk0SQ#T!7R'aH3)ea& S#.yM]Gnbx玉zō՜6vD3~/> endobj 238 0 obj<>stream HVyTG9YnT,π3G\&3 r Y C H2u= QPJC@W}x$w婉[ >_]^_}Qjqrҳ>杙d*|$0|Z]#kBswm3Z z@4!~Q}}%Zh֛x-&   lb{}xq?]ͭQ8ffy|^gsq #\ DP4K1[4D*!!ŧl) 0,zfs񝯯6=nw|1 kyaU^8ϙ'#:zpv.j3KfznH2.)-g@ "e*,&I(j-l&ajQbI 916"&"cDJɂB"B"~# Ϻ  :R[k=[~XTg/-+_u'Okk(f}GP 0&;H1֖dϙ~PuӞ{S9! Jvz/X(^v|'߼9%>~CA[x_|KW9:ˊhg3p,"z|W&!ͳҕ ry`oJI﯐w>.)GYêb̛9hSbQ֥'/7*Rx=bx1GߺW{3>/IMNH *7n̊+@}u*CY77=D%ٚڒVi.ԩ#znq[^tsYo3oOwK\zY@vm.%"\Nw$DB{Tq;= !?4\gcڏ['<71n;:BNY0Mq߿As/O:1infݾ.%+Psw<ݴpj6|7s,Ug.:z.&y%lZsQOzLbzus/i(?xR"Xaذ;YB&(zʨ6rLF[ԳWm4c7!3idX&f4Bt8ڽ( Bգ~./& RdfUEH^Q(]+GRg[4""sʍF * dNT4::Y4Wȓuj*ZE\RByrE&QFNV'EB] kU0%Uk̩c{5ue{!,$d$&A.$|E\ڨ:*tV3uqv::Sj;R[/{^hglg{s9s={*!-&ʢ'漬B,ND01iޥǯ5ѤTY*尃\FoA!fsGyl u*iuĘͤoVhJzk޹Ih, &Za%1*QGUNi"<`f}tDotO@eЛyVG֢#ӫge"jerc@ #扮C ΌV1 㒹}Ua'rέ.j5@G-{Amj2A H:U[aaMew:ݕe,NȁiUN>uE˄yo{= h.i!"Xa <4R? ?6'?9id9aO~'\[LARǑTEBZZ|`6 ?;ϧaVjrE ^rW raEs^GT0-ۘ1G/l'[xIx? /u֚siN ssGR3d/+)9?k]gMv'a_=j&x$k/U4p˦ek{ NF|hj JCx; Š}^>OOZN_q6( 6ݖ/7iV6(R .(7@հ*-Y!-r.(DLP⼣9z~>郞qT/p$:5(\I| [jo'V~9b FW\\^tzuؓ vܥ̃"&^붉v%1vC,;=NL5>Ȼ⎕]o%47ҹ}W4ғ)?+Fˣ@l-5zGbw^ zC0j >IW/CU1fer)nn/F*c @o%: ܵ#> osbQrQ zPO(|Ew9RiI))iOC_=ߵ{kL^F Ƴc_vmcv0#͇cƣ-{-gbɣ6ϐ/5-ycÖQBf"w', ,d~RҼl?]b’.Č!)Kcj$ιM w$кS7v0g^@e έT-OX(h7֏*N8St;-+xjfJoLQ.;&{{{ro>TX~||,@gO9޷%"::) :{yX?vN{yQ`)>[x}m^Dw&l oL,y,8}c?ov a90| -5)LA7BbTJJ#UVDұitFl[,\i^I4IA{hxczڪR +,DqLwӀl ERX*rQsRϞH h/n+JxLְd}ڶ TRC BVJ3JCPU8Õ6sMR5 )}6o$@^(XL kHk|gm[(/ۥv+˳e-llAnUվ[#oR5ŎiCTU1 Ӑmҫbz<4ڟ*j\ja8vƔdqػ>>Uu~&ZʚtAKR®0LٴdӁicQ))E?, P7 :TtQ-:$0$vPr=Y"&tZgϽs{޽gD "x}E^&'3cxF&XybklBH6DS +њFA633@b^_$[EC9Y ֓[*-njyy![ew(g3n 23.XRGy]&4a[Ei`ǟ]s*bW!V ?ł~_0E:D} bO>*oƃM'B .a}+"7>ǟIBNs75BO% ݋ &ہ?.^R'!FV;AX٧T 14vQ*2 o;eg ri[Ꝗy؏C)((83ꛞ-ieW Y%իq)5 ˬ,Ѽ}\a1wdōY!' ],>~ aijkDZpMm+ EmyJCqyYװ]UP(uNIr5*(mWݲۙGT/[uP-G-<*x[k)4}$7vJa8)fNqz7=(5g.-]b"mv?EJ`7\S뵴 u;NdL0'բy 9r5 '}djldo'WÖt|79$瑌< [FA=s i 9Gmg4^#1i"~P(St1>EyN4Ms ?N^`ӀLv, [~dIʰ}ȧ}Q9$,hO:e)dAsHƙ7A>׎s 3%Zf\!%8sϧ:>*tBs Σ[~%̔Rg\|0?QƔHc19[ ]lTO~pjLOǷn.ӖCIf*pI@.@!XrA5X^Q Kp0Q%m'ɠ~u7(<rUR x 6k@+ٯϡ&s]ehv=a_gK|(.g`BCc7=fb:Yma3RBG geˡ%dyP'nok O@}<$^ ?koi;uv`ꍴ8ʇ=Y @q ܆P^@* t ; X)4Z(,ӨA0(0Q&8j(f!a>+9(o뤴Xjbت{e_O$GRbdyrUѱwAhQ ھG{!(/kqL@f=݌CZ,D4Cy+L zxQI4MKF bo%/98|Љ >+~d~.c3e32}TĆ޼k`ߨ5F6.S>;ow(`!EEq~-~59s#lSXL9G:JS!.Gik sZ YѭCN~0qOݤ `U"c`||_ AZRLZY'5WGcq-tFE4)#%|}m) VqD3Z;5&OSi^^i)ǔvqF3x1;h3FwNiSy}~s5HLH*y ٷ ޜSwZp"'h!OTM첤2olS}Il?〝?M"?&͋4)"D$^m3F 61Z^Rm UhiȴO&"D>;Uk>sϹsϳ|7=ٹyE;ȇHiF>AN13|^TU!7)-ѿYh>.9߹vWs|~f[x\U4(|n@?{}WA5ճV:m+=-޿+a=dc{+2i'X > ڻqVfw]ɿ?Qp΁qE*H= ~z 0d^? endstream endobj 239 0 obj<> endobj 240 0 obj<>/DW 1000/Type/Font>> endobj 241 0 obj<>stream HTPN0 91$ U%t,8--п')"޳}n'ol Oq [+M{e;2uN84k!pN]kuN _!{3?>3-1~ㄔ@AӀA3!_گAo}s4ЈP+P5o` c[=F/ra.vfXwbE`wj endstream endobj 242 0 obj<>/ColorSpace<>/Font<>/ProcSet[/PDF/Text/ImageC]/ExtGState<>>>/Type/Page>> endobj 243 0 obj<>stream HWMW|+*ډS8F[)IxIZ+> .Z]Iz_z Jo?Ro-21/[LPJc/y˞/{U{ÓF/ l3UdL޽?hO 0FxW*"oj4jB7TZ B#c$"QU ,%1ҕz{,dHRfm6u"<4 Ix #{Sj]L~N4MXtM㔰0MgH 2 M$Nf {JDjTiMsv\NБ:m3pJ&89 EJB١'JkqTD2+\(g7M0#NhdRT dZJ՝I<;/I ?xG~c"9GgΉU#OI|Cw  biȵHj:s+|m~̶o?3/r:+4Vl &, D`*Uv: Ab@űL櫁|VzY5"i*J4`{1}ݮA{8D߈b]9z}mj^ nGր: W*[d 01-& k̞P Y@ԅS%iQx]U\DKլ &.8E,v &4B{g4sNb(mw\lV =)=g:9B5c]m^aSf#`tl&U,rRNP5qU.LKqS1qW5jƥ&{~ Yjal+kr9j٩`U5z>{#M{jxV…U˿o8đQAb ϭ ) 5м6hQy! ?PvH$\nճ%iouPⴽJIx dYcԘ>ܮ>Ua[^# _lVyۀ2$MvVY;[ۭYޔU0m3r53XR"t`h*-B>{Qfּٝt2P=BU*GsF&iK׆H-n ]|1): 5p44vz׺89nLTjGP77ׇk^$BiVɫ" x)򃩛5j4?}$~t-5ZW(> 4 ut$X &E-Ph,5ю%TFݵk/kXh#9hϳe^;Fl7Z[|7*!f 9`agt O F*CfzMT;hN0\gaqk͕䋅fp)HbiFxh(MfZž)M&"½4fAXK91Eǡ=զˇ'IEQ[1mKFEB3墍|-_%u`/>:cm6_6 endstream endobj 244 0 obj<>stream H{PSW/2XfvmGRbŢ# Ihx AU]|T]Vjw]- h"VL yPbMx$߹7܄: t͝{w8v}2bXɨ])H@QcD |aǿ@ ):3e䅔&a1)Cqi[}%"ÈUe5F {4»fRsYgB>Of<`r-L1\z&Y>e ڰ1ǀiOa٤l*e -A4V+Yxm6E;#‚UAL,`ck8A .6B1zژإ#95)RSAX`yHl(5:C$TiZBc;bf Xd+(%AF $OaH4A6& jD.N Ha}P "vn|!x[rk)h؄&3t0)cBac.YAecFCy%oş.|dX"!Nzԟ 3C4Jt8{JF8!HtZ&UO=ͤm;FzKi(PCZ= d?<W IgRCn>B]B;[rӵ[9Q%şTxuae0ߚ{iVEݢnC嚙$+ |zKUe|L$Eq0F^ȓse9a}inIVaGcL7rT ]>!ZpzyUù!PmpաO9jH?$EqAQo8Lj3D$!i8J`}m]tx,Cq(E^eC(}rq,LG둮uOӺ ]%onYjh䥋=$cʲԧ gAoP$+% Ix Ͼ& u v{z덉5\v ͠a78Dm*>/>o\PF^lB`Hǻv2.^8~ #S4(L?cDz9{esMhcY^W@HrA3C떦]'к u5Aj2h$/:ɘ}lLձPΤm(̓=Ԣ4ȶn!I .||YMMߨ<0k)'>t9(aOyR/$vLs2& wI]x묯,ejhdLC);A- xtII8b5"0!{$tk;UCZ@8ewn>*c Qӽ5gnl<]P [Xdb8Jxʢ%Վ,3Q]k{@ESwbfK t W螣 <+ԮKÝd|eMѬMƴ֐N$O0ʋb蜷GB{;A"JIYڿIw&֐#B+nÊCw[ӌw 5d۫FV 75[-4Tc*s:z=B=9똍Q 2*<,sO&c,:#BAvo\s X9!YRC݅clMGncRz?G=JDEGЮ0-}^ygIW+Iq{OR @IS$P bZ< Z/v}YҴCF*1k;]YC 4" G2$d)TMCZOI]ݾVZ0x&?==h inŠtSK qSL7]ں^(d6DfXR NN]YnR}fp#"{oi ~y{:TN&<I.;GH~ eԝi,U z.((bp(!!6E3r twlW:=umr! d{/huVw2}?O !MCK³ C Åh "#+ZKB`bCgn}qR"Պ%_;nǝ8SdImBL046ͧQs!s ~,'R^'/Isۢ$3XϠ$n$E],pD88̮Yh p*Zך`8ixX9 ̭n{?}\ub^`ԡMW>r{'w_jZ%fF2>?S[&T9AaLˊV4n4MeA0mCdpj oAW`%i!Jk5n+Z![.T2֐i4o c=A`ܷcaT1BH*,''CRV{t-tH9M5 (9 0RVOy\;Zc! 9i!Eҏ (:$h}-Qc~Y`ۜӔBSũ{8ke%RM)#?%'pva5xp5Wo= 57_>;zfzUnc,#Cr-İ 62b]N}# ,KXC ,4COU<yg6dg.W ٩]sڱTm[CنYf LBvXո6&Toh8OviV,W-O_i 1t4`]osgB8Dva2x"MQ*MNN?Nt w&劎n?~Qgѣjmm q $cXwö\;r B \*xxij\ofÐ~V 3}:{a*dp4g6N4`}pQ<2QM4PK#nH lmd$ IxYf\mJpYziץw sճbhr=enA!=>v6U_i4WΒ&-х?J|fD~k%1IJEH|1lTaB3rPGP/Mu")0lu}m}OYjm#^._4I'1ޣ:ɅV^؂H[x}Z"=vH~+yD>l_@-U:an1|Ub*պR{p.%JE\UbR U/Ksi@|o K_'Mv%98)(+1 Zcy+0tWz f׌r zMf(PR!YksV(V)P8P:)` $g*-FVbL%{~wZ[J[&vs$$lkY}-oFZ $CPj!I⌬q& t*Қif>)(4W{Mr߅+P BaAK$>W/=9$tZke&]&/ዘ|#_d"Ks%HߊZpDH?8&NHLz7w0;;]: ^r;y'б3оϦǺ5kv[gqcrj4L05ITZfh 9$}ObyV{jyL}"yǕ~c_+ /- ?W LUG R(DlDi W 57n6i%P(n} >h V \V6u#M4[0 B9{Μ9sνr/_NΝ?soJGP?MoF- ԯ[a߱#N;=._R,>zv]̋b看~>6Ac.+NoX2H~ݺC 2d=lbȤ_x1rSSv&'L.Nz48moPZ/M0s^X9pڶWx73o5b} 58-mXr7dle9`};cHU{OZq*Kv5TօɆN._ Qc,cr5-Gp;%[ 6A4b$b,D}fǎ; uS2\0&*Qɥ3󋻓!}7g\t-Xn(=tx \$)(&|B. 6l%eͥhO;BA>!C؜xVux$hvCA/|BZ;-|(p`G4*;2'>#lqbŽZ¡/\2lD p62u W̏/L2Y27tXCIpYMeԁC/-4YMMw_ٞלJHrP$#j1;CKq A#m> &b/a7䤊j ؎Иl;p䓼Ei?{ﶯMoYZto̲'FPH6X<͕V ـǯ4cMvf?b_2 K痣XA¿+`O."Th̿+\;' ⎌]zE}i w\եMfCs=ē2`DZ!AyY_'e(Ƙ5.K$*Ll׎fG)\B3);EiH|q^.C .B]XpG {SvYe:aVò ,]"TƔO .Ⱟ\|h]qY]S$BԔXKjP~!o.CM2l~ }})=o"*h5*1SM..pr Q{69Qv,RJIxd3>!e%}}] CUJW'OKxGS(6ibW'R`ib&U,AZ8JHF K/͈@yN dL;#C}ҊSX)祲=}_ f3zX&,¾3=ɟTH#PҾD`5cYD N89"B$~h$,Jv 2dGf*QKؙI@f{ף6] `.Q\ brI w2^Bep֡Y?~=2eqʵ?p U(| ՐC U2 g>Ztj*@ȁE3 C OȐbbC gxw6Q8a`ejöXR\#2Ԛ ͉7lUGf^?'drˇ2g. b,̖qpQieP١^+C8UpT1Q:`{ƙ^kjS9,p V @Lc]23-?Ne6ۜuMTZIx 5C:0bpՑ2)i+󚳶5/*+GpZ-$@~H-`бtF"L%B ?"P`bB~~BA@LȏN)T1PV?t{%yd^{=gk ID#C&Ugt Îyީ%[q2fkGHB}q 9rd"Ƈ#yC23 |q\ߜ5),%?,8SbMK܇9pMP+8c *ѻ&D!NEӃΫ;gyKPHF=9ݫ"D;˓9/„mW°wbcm_jNvnŭgƨK{o}eCi"/J3<n4C]+ټML;ܜ sA1Knˑ-,Dv4SsQ: $vZ0 ) g,XK4Sq3v?wS0QJ72(Ixlf5D憑suc`doWzTW|]lHb Jp 9̆?ӫU3θc+o2Xc+tN#,JDP.^C26ү6RT:O,k 9p'k -Ѧ<1/[?RҨOJqOI]kc@,x~Вc/➲]1CQkxoG2&-XM7D)ŰL%2sl!~26lkAR,#c-%D-@}2U+ v\Y;T l\igΞx*[7_QqIy%,PV1\Yp:kq2KDSw{Gzn]c#d@Ro/!6[`-[#UT<1xWO)=/_-O`0QyC_[φ[й ҖRo"HEIY-d@ 94Nƌ^d8E7K{'0t !m1<5X^ pb ,ݟg{  `|1(q~xHh*3052arX`L,RwdD# )]!9 I,'Rp%Y 8nanlވuU> qLƱ$v5o6Er<>ƥy9 2.D lH- *+!Jb;txon @G͔ .6`(iY+} vuXU%Zcg2b ,?IzA ii"H 282 MO!|_.uZ#ns0-½Em/v^ziBLt)j7ec'904_Cc%3Oʼn?XŞ&wvg~0&> CI+c;@[֙m-Cm4ҙʯ^й my XpQ1ԗgĊ CqFf:K!4ac?L"2fͷanlٌ^Ǖq+8{.XKKv3+5Xې3r!$1iIVBb`TpM.`ߋ.KL!5"݂й up65J-ÕKrMпRڍ?Eܜc{ժS8\aTJd|1nG}IqXgy7n8p&Rz!K4bvi) Bo5qŰ+ hjH`*x _Ov}9)xuR\KI 0W{C_ O.i&x\ftQ[b<EyC_[φ[Э`.Z YYQՑwz;_XyCPoeus3S?3N>T!&I°VodìmU :N9R'Qiֹ-ۺ8Is@ ! ]e(a1Ԇ=1BfƋ3SOO>bMW3Ʋt`GC>^P"cF샜-6 ^2Az 贈UƝ̆БAZHDbW"+y0c?qX~ ^ǹpU,%dž G## 1&rJ8~Wdb0OџzI!ݑOfisd>~8s~"-B/>ZCb ]I sߦd7^~ [px?? 5$ d6NXpޭ b֬MHayA6?7$tXRd:V7Y`Xک1-`tֲNcnQl+.l,u$n]pKf#eT_6 =̙qt{~ Ùg}9yN䳤퀞qKl7T"܃O\*+wyvp'3-m A}IP"̡DtCXߨ(JهObaFpY őO> 3NG &u㤏44hcC-ok.M:{N<-eWen}Po( W ^DWqeF#ވ5X(j1@KYӭF6Vϔ/qK0npr[@i\5MU lx=2,|F!$5 m%d_PY@_GIQU-&ցn)D +nj VzM×aYC-.)3H;#  6Ҡ!1"2iH:̚)ɅXb6zN0:nXtgcs8Ε K80ތHnLdfRxNTx¤,F%c(Į١DLJoʢ,'ZE7rY=) 7fnUmٖN44k=9*Щ $ >i:#BaHƁճTt¢|l._D<7Ckl7h\(]ytɦт#y+IlK"58\{BcڍXto6 v8ͯ^_1XqVD%.Xo44 #0\r)ї* 39ͳ4ŝ$p0۰%,AC67,AXwC6|"NWm-#6@ :|2u>}6a,,Ay7aHil0ޯk/cNY~>&,ACSMMPڂ}mj0ތ?%FLuS">UM y&s0rA5J=-ATSB )-h thim^_~̑7{7}xYxba!7Kh :~$%!ڭ?GKOn> gAlglcw.t;<|kؙܖȶ3 ?B 3^f%^4a;@CR>0Xs:ɰ@ ( };Çᣀ%hh p@镏Z\xS Vt'xZ"ͥˮ&auCJbHCEAyT-켟))]ހ *S u !#>CKIc%NV[5|܉CXtCCJ]:?iR`]qFf h4i+!{Sxd@Ce}3"4XjS 44{ŭ3UMS{Jo=is27Xe)lsACTS tt8F.; \ wâ;qd`]v_ZnO}fQ L&?:PPUJ)<@[dxy:) :/8,?- ACSТ o9ZάUNg{3\prlK {?*'JtZ(HĐ{+ϣ @MƜ H#tD305m= 4dWXfG$D9̣uO6Վ[Nb[O&+A{WX#3,AXcuw7PVp^SR_sWq8MCXh5zHh`ɥG_zngt4wzcnF mXܰ c mXXX NWm-#6l44 C_>ku͜c44o~;?X7sݡ08B{_h7"ۍ[ Ki} mAhz]u5DG,(\33ل>#^IJ99y6ATub{:㙆]ϙ?92Szo:4GGɹB":*Њ* c3X: ACAlglcw.t;<|kؙܖh+?1+ '&W߃F졭 Jd527=QC.yQtȳG&$>2u0W\Bofbᎁ+:7tO9DK]M! N 9yP`|܈*S4T4tA$wh {qtbGRd0:&X$X:@)虩[*DAuj9@8!o.~p4U)p.8Wp#0`.d,#rfH&6^iUimDVR$@&7=aIuCcTL{(PqD_<j_&a0|[gz`{rw$eYJDEUSg#!^1pB4 ύPDJE2=6 ICԬIcɽ /Ptgcs8Ε K80 I exN-J]aGoJWO/%!Ljͦ+&i]fǺwQ}1bu,*?\ +l'w[֠3'EӠY-79"yEKTS>gc(8b?l5O Rl "JX0b˦PbB9F'?K%9 (C-$fv~ffg۝f3>yt,H "<|?ΈYyZL CgńX.B(1Q|_ F9CKM&5` s;Ȩ[A'm H׼45j CrCH pJITl#0Z -A_O6~#,+oe77`M8N O ֚,Ք`9qȈ@?miځ"GMq #*x ߆m*\ ߿_e`nEe&Q4}9CKЪN )@At; 6 g#+W+W-G`j%+-:BIdO=>qQpy:ג~FmPPV{-9^﮷˻sYCGjI1prhjߨZc_W~j)^3ҦePN4|ρs38DG7 "9= Gx,(u}/s 3ȿdž5T:a /XETy=Q[LႽfQMb>qM#Uf"iz3 %rvVCK߂Z({u =wǓ/|14[Y@J{ҳiJI+"~qc,d2BU2,&|7<l)Fch¨/1ysOnY׼l_眲NOb7Fcq ?A$K$MhzJr"b<S 0 0wÉoL9:.qDr9j+G7ZL*v?QؙJ0.18X{l{%yNa?t<^JIYAFͻa`hp30@no|0ւug➌7z= YOJ3hhW wyB:P{D.Y~bxX_J6Cg`h[5 I?cPo ['\Ȝ>i HC֞38)#'n VY\{mxsΥeO}C} s ʻq(bm_pT0V|bƑ ϨP Q>xob`0ܕ+IЕVjU Io߁raWCoVX] _:5:BQU9k>K=Zg>Aq;{_G)LYuVbl1 pyrTCaXP<,>1p%`n)YYCQȯ̠F壞Qh :NJ!?eڢ`DG`hp34g%|Ҙ^ӳ'A +,z 6 '0]8*a`,-+>VO#A}/Kj^ Ľ,kqr}w]5m*_Z5=p̂ӁeL%~3,h*jDK͠قOꆐnOc2= (:8-*F !b1Ga`8W`Ӄݹ:\"Cvu!UCb<&&;`Z?ڀ5,JP }HHzHɋcL+PZQ'*Y .N vdj^֕#0lOxBd8#-jѫ'hNCec-f,Dfiػ6r Q/&o-WѝSѝSI2_wbD`BD~7]uQ7])t&,REYdjڪFEME ]V(^u9{9g]{qyg}3_B]C K?wj&QuRGṇ0Ȱ WA4Ax`X20IKtFPs{\ϴK쌳iFISZOt#N~7jںSC^̟N`jb-? i5Ѕ$uX&L"{qNwIΞqj= iqa,'!sy,A4|0bF5,i%e44@IG.SRyaA7o{4i4-yLoR>o_蝕.y3 Nކj[L4N'0XR֝z [>5Z[,͏e'Y^p }1b>[jdA<*_b( ۓ(F` ۶X,W+;ZX|fY,kaA 7VNV΀acVWx&\rۙ`!)0 \K !E` [X|BP !H!)WjauA50a8rCVn0ae0°rCVn`p/ĺ~a'1oyg`Ǐgڶmn,a[@r QU|S.g_X>/ColorSpace<>/Font<>/ProcSet[/PDF/Text/ImageC]/ExtGState<>>>/Type/Page>> endobj 246 0 obj<>stream HW[H~VBUq]ͮ4I6هFciՈqpfcpqNv+‡:][Q8bJ|f4!<|_reyS9AȷZ ?PdEº|^~,j+^X J_n,-ʺc8iC8~ןVsExDIQqeVgG<9O1BC?ͫ]9ڝj~1pGs4)+`]qilP,oA=e!K:-Vwjd78*k4:5akHCg]n JEcLTa*1r<;!Key٥M(ӆHNdChŅ9kTʘD^ntF uG1piM4Do%YU>e\V -ȎP-h|n?OMڢGBC},QNdI,7x&qm8.]O<߰=@_LY ?~7 quL1>+29L)-JAl.EE G \8%-ζhnTxi0Ϣo!I|c-Btx)]uBl{:nh?VtXW*RqAQH,!oL)369D^pOn,'&4$=s4ɇL̸)l@7?9ndW&~B__)$"6,/))mi#p%F?W4U# 5L} OW"YBTcLFz$v@5S 5/q=Ȕ٪r #uʾz)ɟ a0 t "܉SYUݦek`W z3OegBwizHbD| ܟvʨ4x ?j'fc)+\aR#(L#(h>9y"2?=G5Db20E0^Etˋ,܋7J4mw)oբiw3ULMtvRie :€)-fs?F Ay"ϟ0i3~&'My*w]ʰ}XN\#}ˤ&Vb.ٽ]֝$jYXجj)!*)RTEgĽDEXDS8)ov,[URFert[1N)ك5 ttxo[3.|5,4UB4FnRf V|䌏J^BL_dX~"o\!2?nAi q LU*P{8%x":V5¹mBpI?p~y`Dž endstream endobj 247 0 obj<>/ColorSpace<>/Font<>/ProcSet[/PDF/Text/ImageC]/ExtGState<>>>/Type/Page>> endobj 248 0 obj<>stream HW]o6}ׯ  [mC "Ӗ[t%:AGR-Q4 vH{ k8}B`];$E Ar"BdQR0H~` 3Kr:O8WCme80HڟޱsQ.4ʈA?54 \F[ 8\ohomatK'WǍE|F0X u>QD`DZ*hA1&@& (Au.cG `"zA7 oc4q 4TtE+ZffQzF-aљաnSd)/.]-لaOgKtgic٘\I_*lQ+8\ s/DCU`2B=lG"tsR%sʤNܒpdw&t4jj/50A,&' F1^N>0X4"JT{WS@wE42_3*2ƍꛞ.ǙE{]>M#Ԑ:,TDxb.hs";g{+0n TM;<73`F7g]* Yж痻fݏF.H/^Css&d/NދX.#k@ []6Ss]t;.]bª*v0^3z`.eV9It,<CݝhXȵ`<2*sD%69Y5&bqpIWEY=A?J{VA  l֖Aum}(-Ba<P=Szu-Ry׶5oX4+ ٸ nf*RN*-x n@h Պ]Q9390y-Hr }Uђb) ,zIV!&eW$hK6ouY̨`y-5W=<| '_>q n:ץ5Koiwlbӆk VEyŭO|)j[ +p]+U+ֻo݆.2CyGln)`js)vOeR+21jg>*ֹ`a8SF<^DBgXm8^^ "f̚ĺ=:/VCG 1fZ:d1QFgfx-+AZZ(\:YٽO%lԤ endstream endobj 249 0 obj<>/ColorSpace<>/Font<>/ProcSet[/PDF/Text/ImageC/ImageI]/ExtGState<>>>/Type/Page>> endobj 250 0 obj<>stream HWnH}W); of0"D"'[MB5%[ڈ%DQwթsN}P!8 Ix %y>M OZNx啓/7zx$ᄆib4 У(O |=ysww |-xԛ/'(9@7o<ǿec:i#XO~?&ob@ ` ƄP,&mwT Z}Y#W/oY{yVz:RSD(c-j'6տ ؽ}[w/ـQgABOWht~mj#5dI5|5 JX-(J PG(^qӅ{=&JrkM}婽L+&`7{z48Ud=e;8ik0Qwy j8ivJV'\C`׼su܁駋uQ&6>Bj !^,o ?]]V]'7A2~9p]J1C?$1mfM[ߘ]ihE9y4$\Z- _e(9K5|K, EO]T:.0dGy}N5'dͪ3sPH/>]S5X?0Uf{vW(P,&"pv+g8Mӑ яaVqslP8&{2粩'CfL#"ʀBk(Hav"ŜV uLj,smz,s֘$McruU:q-d55ݚ".T+B=eG-0,B{ '4 ~ӑ(/< Q ? p5 7wL?ȶۢjBmvu5m )Z6\l9re> OgNA{4Q> Tya!aUA] zDl$Ks A+;w"J+QPn?" hAaEpW$|͑BK%h[vKN+E xgy8Q3t$$n rk@jQ, xx˄ Z|+uCErVݹ(6+sgx?o$/K%Yf* Z^>rVn:Ms,Ϣ!\8wO0Dy /BNPL?9՟vS%d5,_fwa1)T Ik%x-8k{kl1Pzײ 6鹸_Da-P)^nʅ!vE/|VJeB DqOqu6[>l7wc3\ZLR*4lV0CmQ)!) tsUFgwrwAР@IFLAr*YZ\ݖjU=u5ʫz8Xl&[oV4cbئ |J^$ӌTl= ?\_29%BmP='1p?@jxj# S]h<+2%| 4f.a˓877;5|2Pwĩڭl1(U"N>B`۶z%/sQ?=гP,u%bhN36"7æHѠ b( ruC /i$L> >Ce+kC]Bȇ꽐ˢ%}DUApet0pڏk49i>=0gh>FMv="bWsz#z93lw ҇(WdwKo{H êXphR:kۺ۶5 `CaKsԍ$ pU: c 3?3NO@טrevU B@򌧝 TYU@pVǞ$hoҦ!9kSf)b]43[=ln:W =I 2嬛Om#WKK%Q7\'^ElV7G-,Dܺ#c)l5ɯpkD K;t8{#E7R:,ZP Ow=4Nyu0% m O}dMUޯY3ڻ)IG'HeJRsQ9hT%&e&{OEɩ8,) )0%OC#` Y56b/%?9[/ R3{`+FCZyj endstream endobj 251 0 obj<>stream Hb``6q~|8s?ժߺ]_׽O]6iĕՊ<~m׷7Z2dO1,;2gw.t 9<=PJZ-?v]I3?O{2"QkԋO7`вiJy'71踫vW&_Jٌ+_5T)_sot!y/H곜g F^U<<.e#ͦr endstream endobj 252 0 obj<>stream Hbbf&`ec29yxL~A!aQ1 S\BRJZFVN^IQIYEUM]CSK[GWOb a{@؁A!avxDdTt `8 endstream endobj 253 0 obj<>stream Hb``8xzb@*r# ˟+T!SuwVɂo lRL6},tYǿ/Wk})Oϗ/4(?6nOs_z1J.ͼa]N}עrѿG~TnuW_-xrȸاYqЯ_rH_!i廀*yǞsOl'r_͟&g_WJw~<ӺE~4'Q0|@+ endstream endobj 254 0 obj<>stream Hbbbbfaec`N.n^>_@PHXed E%&eU5u &M-m]=}C#cS3s K+k[;{G'&gW7wO/o_?9aQ3cb` >Lj3 endstream endobj 255 0 obj<>/ColorSpace<>/Font<>/ProcSet[/PDF/Text/ImageC/ImageI]/ExtGState<>>>/Type/Page>> endobj 256 0 obj<>stream HWKs6+j3Yz8p*9lBee463ͯOK`p!}_'Mcwk;O wF 4$  Q}GU8ީ1*!! {Au`T0|[Nc#*/bSTis 43!B+. J"2!{{ W_PaD쓘ű8v{#tp !/F`$o "&Tv,Ա ELߨPp+i!萌}#X7!ĪlfC ;:p+^'-6<>v3Ofc|ix5駻|+KJ"JmەǼw/j *\`IMb{hef*_dʧXbrFIuͲyXKpMS41̷>stream H,z/x΄GE>UߜHHHߜttHHH)ȕDAZ@o0wVݍSNH$kZ VD~HttHtt'sˁ;95W8ӊy`ZUR@3tߜ"ns0.)T)xӉwlgbP$ntHhg&#~|N*{ב}vd mgK}xL4ߛy$rHtttLQ J IHE;}8˭򬨙bj̄AAk<Ϗ<bرjbkbA>stream HuLQcZm1a`0s1wg.@Q`+c0 4}K{G((RBxiIV6] uLdͦTn9s#ț/-\h%J.S\ VTJjkԬeު]n D'Bs0EfnF4mּEVۘڶkߡc'hu4Ynݺ٫wlL6lz(W<ΓhuP"},!*V'mDI ӦϘ9k/X3}9"oZ,p=i'fX'e%K-_ҴZzu7lܴyVGA}˾\ O0w;w6/ݷC6yAU2r8y.^|ż007q}u˽v[Ew޻hC=ȸƾdc/g_z-⠿3$km\ rT $1`[]r~ @B.dȅ IA̿5I\ , !.Q!2>F%J2%FLC"X>sB_ P7s)C1RA e!N \#l߅˾J)fG76Z \䇁 C,d6_=ȸƾeoQc~L Ň\Ȑ r!C.dȅ !2B\Ȑ r!C.dȅ !2B\Ȑ r!C.dȅ !2B\Ȑ r!C.dȅ +nB.dek@#^@ endstream endobj 259 0 obj<> endobj 260 0 obj<> endobj 261 0 obj<>stream HV{T3D s6*kx, j+,HlZvmXĈ hP z|ETh|# JED'u̙sw{]Vo[my\*;P/DMSwP^h68E5PE'wР<|8P۠ո9ɨu!@r V``¾86{af#윰 u$DSq'ҌzsW'\ 'h"o|3 %B܋, AYF⏋X3H-Ydc*1JKXτAe$&Lk-dJI)xY<]p 1q8ٻ}#dq=pV\υgBtϯHG* qx E(f=):g_ʂ}8AYA/$ YܲZ1sqǖW=Q܋l79|xG2Q㱢2:<;=B@ҽ;Wd+ ΚTx]D~M>dÉutISS@5NA=\޻oPw;{/i0?c,Q%xYp"3$g '؉K#H.J&=Mc1X$I)+ ) ӛ=CZ-XT⎻>hV ف`.倱Z$޸QhR G_ѐyh_k^j`dY'6֬s@.ǖ~q)fTޢTdui7ziw۱fEZW]>n=?t9U|K3r͝9DKLo˓In$ϙՂ"N.y'S şO2^eGױry_?Ab%h| Q|ʲª}qb8t.;| Ǎ#`:aP5#pAkiɔ)]IJJ&mtUBnRR5 ¤PFsjRaL:`@Li)A0 %!RkB],hь(ٔ3,n!PFdVf J Ui.*[4(ұۭ@hօ¶QFY-7PzA3*Lfj5ՙJJ7B5*(<-WG!63pաV*MOWc(R)p鉮⦑D(-c(% y 0CجTV6r<Vn^[Q ^E?(Sy6*ߎSV7Tn~bwz>B$1I΄_Eyyx wbg!X8Sa5o13O*& )W؛lE1XJE&HS2Y6~u:^\Fq:hgmoQ_+Pa%뼊 +:КԽagD 6oCڹkK_7ix}Poc3;^R_RPt%ǚ]Ƕ6%M o\?=Qd).(ñ_՝O?#GQ֢}T=GZMMo*.?/8zy{BxP$J8Ҿ;9W=}^ދȕ^0.i?z6謤@bmHvN=Br(V9,RmT!cOs%Pm%OtDqe[R[9"&cA-o[_ӱ3st^8~pO hQ7'G{#JV'ޕ|O.ؑwKqΚ!>qjGe*:I|+܋w>lh;Rtg[5EKZMNơ$TT} ,guMe h|n_`x&"z1 yO֔#Օ v{4Aku}"SP(Q4q@йsW}'8C+MQ" F>=>^3Óu],N &b˕va_M'׳sS#7]crRJƼR_rI͍;ozu8MHj׆| a3<֢^iԵ_?w Ko=0XEpldA2=󍐪ܿ&|9|#oK?W&3,+*wg5ͳte../?غx%/'?ZP̽Ԣ{]`fv0:<ˆL* I$y<׀oD9\npV@WQZ@?$z6 z,4pĨ =8_/B8,`/ok?4] ?]H">e7tg)iO<J:xm<7 NJOg^n9.sC7`Mp #P )נUV s )i)YkT2,Afބ) 2@d C3H`$_+PsG Th$1߹JLv "Iv-(u`-|k0GaD0d3~cD``!sv9̆YuUU0zBJ_H"!mbb>C`. (>lPK֣0+`4S”N7Y8t5feuC S2"@B85)9촹i!z\5۔6VwIkb{DX,qٵ@- .Q%goMeDljWeu~NeSe(&?OG]qVUh]/ TWS]!og>'khK wv ->Sw0=V m3/~ }o'8qHǍG:Q-k_3d ]T+va}g'ǣ> ت>a,m.bshA 0hY4[p?ʻ'W1AoN(9|K-/n`'[#>f}% 8WUV _A|pxaZAsv8/Z-\@c՗O/|),`=f'O|O;{Ǿ0ڕ6[ e0}AM#94RVh/ <:f)?Me=lNmJRMT,I9 }9-Q>y甐~7M>btbZDە+ԥ?1@+Yož~r 6ㇰ'l5I?v!Rr)ebu8$? 6}1&o(S#}jtcqfq([&>܊q6N$՟o3[9"'d\{?_TIAj< @8;$)nyH3w9ƅ\FSD$r\*k\Qz^?g=ІS?Ңh{h,1W3~v[wS:[~p^<qp\mٍBB w`?B9{ken߭zd!ilSZHm z*y,Tt78^c}= [T 9V~y:ɲQw4gσC74S#xgmvҷj>^>ګx#呩wRL?IEo1eM'D 3QjWgr+~{EPN>*CJ< u = >)*@@4&s~}IiinܜkHjEg}FjӧH Li0$2rLt|Ba%T<3e/$h8nb>^nD\|d!p| 'd֓]'E,Qko/h =禎ؙl6c8 ВQr%S*T` 2J @^X{a텵A72DNDNĿD111Q"H%^%eǝ|7>0 YSSs>’TeOCz2mCm 6WJZbo܌ݔ. ;S(c PDB"T@BkpC5D@ DXV6oh%RDUJY MM CC]CC|wqz֊[@c;c; ,6Xl$ύM 9e.2?'Z# p|@P|st 39okl``LPvW弲jhrN6#Yɏ}R'r/' oU4"F{^ZDr1 x4K}CۥWCį=?2&]Mt#Y]Ű:Gb d&h9-# m=nX_wkR0/KH&/ΏL싎 [>o}լݼ|Fq|Jkh ~vwG;hGGWG a8TH)jb10 D {FxF].j?mYbYJKےa!k-J\( , RVnY"Zk̪޶n#X.#J@ L IiR;vb1ɓKN&3Iə4$[]ɁHrIC.TW&tˑ;n&eff-LKdF.Fl&Rfe0F4s,).*.IΠ nZ^aXTAe-YȧOe-7eD7Y&ќ9q7C[ j n mt3fd*OH]ܺ ?V/NI7Oh]LXaӘiٶ.M/*˷% 1wX[;OS.Xc# JvǶmqG]>>+/g}v(>Z>YN΅NJSS&HYh w][hrv| .LOw}[vvye 8$lL37=55yb;95InrS9*uĉ3MMR;Uu,~܀W$eLd&'cQ'9R( ~Ɔ#K)JETJq&ULz  TE;lZՄvp2-2P)(ʴ 7ÌS[Od7u BnJPJ^PVKyS=īU;gx|dp^1\1ph. ҧ\-x@pC7j>YK>XpO{:9 բi,eѳ[ZabZ$5AK:<;;\šH=ʅe;]4}FR}T?6FzrnM$9]s j0Z#3L+PuQ Y]HoAVժD"Nō88& Hz<0`cG l C hYs*ʣ`b8A4Bȇ"=D l_BIaFt$JXIJI-ݥ"x8+I⼻$a?\[wea(,iQ~0 D[ 82?V&A H̊A2*'XqhMjkO%EVSGL]^촢12c00y -f_Hac0C1w@n1ibwǢGi[gN^0Ghؿu 2|O.%͂-XJG r PSyWyzd{og-tS75(MDŵ/梶9 !V Q0MFEbeQEYU}2 ma:f%FZ,ʩla7X3 _/vl'h|ƒ3ϔruyVtHBy.N=`3U&lpVQP!AimҒ(ay5 @F" 4`h7$Hx`\$7LC}<cMye˳}Db Ԃ}`쥔&6ǣo]r1N :ަI5|籞~or?z7}[`2bضD]aѤǁ?8?c`\ E0#i8Fq_8^qPۆr ^ߔߑ|S\sx I`$'=Z$L;.z %CUYFpiuB%FfDH1>P:g8yt*ǢZs3@dljmh9k(kfl(ެk釰QA’c[U*;ǎ#d.3+¿&;~lwWǎ$iKH$Q t,`1F[,-QU۠uRFB; TjVUUc+Tؤ*DqsZl_^dԟksm 5{)j.+" D׎f6Ӯ(iĎ "dvaD jɄ3Qo԰tou{cGmTs+[xߥSK=?*lls׿3o>q->rb1 U:}oE,mI4$`Qx JZB!L %TJaOڂ V?oK vyO`'(azޡ>:B >Jg BL+@k)ia[rqv9ۜF^(A.-R婅PIQC_6:2jS C1 _XGRބE ޔX]#I>Ya/| qX.HCiTe]}py?G+MuX4xӀg\D/t|zD@wFQ]L @X h9֡UJA*Am: Ѷ O_J7n]DQj~ {j삷}Dvgk/>[)US%9W˟<_ޮgo 9LxR.5bOʩǁ1.3> 0yxU͉HlcKx<)8:K\ LU nAjaTn:n/@ hn?.BhTX~cڨ>V&OE%[$(; K|@_OxrA*rIJCc,q9X\*㯣"I*,Gc|k[Ռd#3R&vY{^@q5]hJ )0E,93BҶOW_{b4kNY[X#<+ 2u(_ŊƩtC4U,#;~mGGaKmpL/cUٲH˧u7#o_>FYGZP\8<&Py_>2f\GaL)(?y;zbyΗ`Qہ2ɱov1&2?MF F&t1L;z1_=nP,WLnl0+gGc}ًYI*oV?OֿZDzDۮm5Y15Q,k, k#~;"Kϭp 3aP~]ZbEb Kb ; @X2wv|'>"^O"{Z! ``Tw%| 6W J/A-cC$`exXuo4z6jgqa;_/ډH|{aaWWJ<ܙp;v-*9\Jr)fndngjn<)|];e mu6#|Ơ0pq܇JT=l1m&/3܄lԻRiNkӴNt%#K~[/FWiP+GҠR!h^]#5yӄ=nzQS]фJtmW1BmKh:VޟKݬIwXv6ᣟ+.\ϫrk`ƶC*myx}g[{K;Bs|93:l2^d4ӢIGi;H+t_6i<#ˉdǖdN"ű&Xv7VJ -%)Bh^\YʖȮe/wnnmctۭnmew;nDz=rׂU,8X+j}DO#S՘9A,#wr+CmU4'9S04P tpvB$w)Wa5JqEb2T-9Eb8qn;9ܘq;cch7ԡU P bj~a@bn(Q "E%&JNKZq\%:,Zͩлgi/ueil4 ` _.94JѨQ);2n9UGŦ,P`;!XՉ T:2I6{g9vx{rID*[h?~l0G-YgȇacR$I Gk'%7sR0qtN *}ۿrkS581=Ѿ槈xߓ3/}"zo:O' S;DZG6䮿S;~vgȥ{ wM *5Wv{-iD uUp.RaRjLj>u-:ڤ9 s&1"<?.#bFg#8/.y&.F_`Y ),j;d4AyK7N&㇜Vif!Sz4ܯ]ъώ^T~qFR,jLi䲫ӍJ;kTG=ˣ_Rz:m`!1cn5W^=[g/ˮ ZSٹ6mwoigQq M 0ң5x~AG͍"xzýH`WVOpu!RQSY>sr[jp D\Ih(RnN# (]=7τ`>Nb9%%LQEc `I{3LnzXӫ(t92Ny@s s2Gdd*Z  :.N'R@2U~z"Sy7XUM2ެn[ROcj=zCvDkmmL.L)(f(=Z!p=,UD& ;6Wg+=ji*; KԐRYe](Q`'"`OmlN[A}䧊ga@/HğpxT\ %ϖ}^W fY?,f5{MT$97u\XyŢ?rgV\g{a˄#HFlxӽKw1?ChqIF%s.݅x.LJ#!mQ7S0 NﴋNu˜h1/lU!?uȝ>jO[_:Iz"mYѺjJ-UΑ_;c}> ֻS)n O竴dLjcwo1SdSVd&gk]?B>`jѕ#c68^9w>{ŹϜ/9'vLsqNB Z)mV" UKWmdb/ iѺvAJcфPUB6{߅1<~p| ?0BozqP۸6n 0=v LΩ߽s(a] KUGء1l1by\t$$GxFd'86 P8EQ\9p 8W=|YPZ6 Vѣqk"DCZKJ(Xb!ɻFBa뤶Jݖ U{ TvS5)" VK b Xi;4G__o< Ɓ_7YÕʔգAlEp;1'I3wxDXTΚ4 2 qD$-˝Շo扟cZmN{WΞ>MsK'_v.Ǖ![^A3ۡ ??'{vȺG?ZnNɍvwaM}srn@+C塥!$ 3DQi4`.SiXנYXBȘ79ޞoxߵJ,pLyAWt Y2bX5C#(eB X@__]4Jlo1TJWzpg(P z<\ta0\#?ݿqm0I-w#SFu_7l\{vŶ@»HH1()D=1Zn~>J#È[0'h%,v6102; T:+t%to.${6\חutMy_YVPPoҦEb|`вqX4>'-J% nHHJ7$tCmT~'[%/F'IQft#d(K:v}RQ_Y=;`;)K|BЊIgRnTB;T"ćnKy'뚍ZvT8|o^#%{ʾzA5%\'jbR QZLj`|II$D|3Ui-VpTz+Q'L7%D$}~6ۋk˹e}{ՒLʨs-io xgyxɱNyz%xgGu&. ,]D.rZ)‹.n.L0-L_ވ*Q2H]p@W {͂'Ûy8&ߔ5Iψ[2OSyp$ooqDGAQ#LJ;}v¾d?-ml\fʹ{C!4xG-*vƏy&x56(qn̬s!`'7ė Sג IF@ɤ2v*sUJHZ ?fNIdH|*=c #k+o@X=7f ͊xhthv{clY&uz򰸅? Fv؆MMN$i/\$_ގ"uGϳq;q.8?.!d$M`l YVka`R5B*RhcѭB:MСv~hHޝI!A\WPVһc1rpډ]"ӵR2/ eLKЦ݆ᵑfG"z7:OSu5JǗ>\ك[ΖO]󤯭=_Tگ=ttꝴ`0a2@ͻGiޕVY0U?=)p["qmT.7#*!ĸ'%^N k"!Q'ra'<`Γ=)exYL `YȋAA)e#R,;Wߢp></ ŒY³_q.BbK""؜< V8mO(`!~!8y ,,yB֎"ɇy"AL.rXSIAZу4͵Q?+عJh C^8Zk3}ȇ#JQfY=HJDC{*< pzLс(2`1qh#5 an'NMIDpMp4)j__ѥ@n+R(*sn堂8EUU D14)vF9uNs1\vpb\N鱆9W.C1,r7ب6rLߵSGklTt3xto9~'yߠ~t7!Y kFW&C$skD0,ӡ^KAL \3;צIvNC*WtUF`h4 :T.aڠ|j4Į\0+^rG1KK̗#i1JՉU'LUtkކ|žLq >hݵ2TǂaO~VzkT8<]spc?G:C|w~nK^>HMmG;= w_^B͹ދ_ZTOYm`}އ6+ZccL oAufD֒Ip '0sWe;izjHA0A~TH'z=v) f?[f*Uj/f K֍T7]Wn>Gu=%B:ӌX;٬d1lGlQ`'/n'ҬtTB5tW pAz]B oƑ,s|*;>g̍M|T}qO>,@<G"Ej!m2[) ,^-f/꽼:.Ƶh g&}a[0Xc^JX ,܋*Y ,`B{Y :8NCgUctB: -*Q/f4U.N#+N_;j U,Q r`9ئ*[(Ǖ3 VCDs wP L>~Pfb*^Y Rt6Vm7#5}&وƨ[AZ'XɤtF(= *EljI϶Rn3UJFbq\αOl6)d "2J#U"m H"JQ_-X5TU6UhZds6M{w|ߋ51㛻}I)},6'ZVR% cMvb(2D.!>x(Jc(?Z)Z$Ҵqf &}max壼LwōNr@U~V(I i<;1CjEs"/7sHOR>}z/Ad ʗ~k+]<65O̺9_QhZb *T " Qyʨ_`'͞^^6xd%lb14@8-mh?~32;2BrT?:mmι~Qg̞ Ӡ'>n$2}m) {%^n!_G0HXzwtG'F0Ӭ-/6]0S`eF@O},âYpK6Ed}E.#lEhB6Y\ʬzە?zCSQU!IexrBAU׷AQ+D݉fZB7ЧT_?߻yhQ`OIvXtxPt9-0E(X|X>%A(5S9#jVu͠dlSX1qYGҙʠA_pfF0ϱ>"SSh5osE&Xp*aSd$i`inܨ1ҽ911_iClvޓNǿ12p8[~md3GzssyrOB>uZmT[l "COŕ;>;s)}9ܻph4yktJ%ii !4WN*4_#U3IVN^O2ڥ**’KAͱ7Ȋ(yX=}K/x" :E֠ K-~r<) O;h?B cFcՄ~aA!Avt9""<3~;׮cm  wYXss4~&:]ဿrY3GkN\pxDLl>њHT55j,\5GLg~,LQDD} dHPE".NGr'XGܙs#-Ë-CS{'c8}ΖW9{+r3T/~"dezꆟ$X֐HiX=V>ģx8é̹ jHS2Ѣ d!JX $+"{&qV `:\˚Cql4x'KxZ͙w)hjOງ6Q)phjnRĖH3Iw#-OBHBkjR\ʎѱk*#44~3i4*mi}1qGj0') i6 UQ QX FeW$oٗIIɸ-#t[B:&e?tn}YPg8p0ɺ>I!dIrR]\06<\㫋6M5ʇq6[h|H~$9 G('ANgIkB,qrLG=&A^#0AH)||K6I,Ւ)g;Ь .fFc&dáY2e2 qC,ƁN@B1*L٭&~ƺE:G'ڂEs|lgq컋?َ%%㒸mH]¶viP#JױI%*-emjlj4hl i 4 RLg aswx/<6۞aOl1ڙmk /(zt\o'ۧ=Z2 a/P 03¾ZNQT=E%CGvT^?lq?f,Uv i dMݵt-n_uc鸑6aS H!]/w]"qسhMCz˲/L22Mh)$tSIsQ4^,R(x5ڔ1w:p&p!yA/qly z5׫͕0 x֟GxʷxØt{cx8Vc%nX4 yA8Z,yRPռb)?m -FQ^CT_5xK% Ћ(%r/.ϋE"%l3`^iUrZsɅmSlͺZw0gRM).n2yHݨiS_u(1lkOG'k?>@ÄNj?d1[ʙلvj5hjE0#- ~}͊pX|Ryp HgddNw)Q۴#ME}rzg:2b7Ha|aз$4&.1Fێ^`SϩkN)\98'm`Pw..ݺEw4 %#rZfD6Y3 82('KUN8J!3>e 2Y8qp$]I[Lv$pˉ d<.h2WT[vw6#V{lsu\-c! GySvqJ(!~`jr趩 KbxR8]dL7ŅyX3(8H{ *TO/EY6dr:dg_]{|W{/~_:)\}O U.V~_G !M?9je H?|6+,pYOYPA桉ӁS!3`kw%\ tsi#ίbUЇ@Z)4N"d>n]m`59;¡SoTw1hGvA [U~kaܘ4vu}9*~4i΃N2ޮC|jfv)0t3n\w s``.+83N>QZ(8lr =zngV8CЅXkvUo8ާ_+Hu82 u)MrXf C3Iv_ ˺lx$܈n\ EJnIH3ˠÛvHI v"~aLtpy0!`0 TF1A _ }Jkl!jD)h8Me4I϶ =Rͤ4mo.ҹH&rz}N0r򁸮z& ɚK6[PHUB iVMP` V!ɵX{b bekrzZU) ufϿ2ēZbVq7f򔔝/o}S#`]̏6:n|O>;=/MR K@QXyo;~z.UnrA@~ `")1炃2hr`7F8._8`*pi-겏m<컳lߝ38%N29 -QuĤ@HABl߅]% tVX]GӤi+Z4MbLlJ${s"X;y&6@DMDà7DZfܦ>ц[CwiO<6()b(4icf `uob`ggcٺ,+صJ5tJS4+WQZ5p\kh'8+bF7~(S?Y.Sd QȌ@?_1ϩFG(I^#**NYn qY4=x584sd pM <6M*&Es4yQ[may#YH*?~S+ -aY{l{ ?6-?>'V3緎J <0r;0+*4;2dg1^x(eu0!)fA774Fbx-'8AeNCp2 h4 W 0Z/v{%dVN q L WZڦ )a]4:AaNN":1\4>0[L^ Q</b )!"լ6_">gu0x{IJk?^}ZFNo+3;x> KMZ>8S뮞'%E>ۭ s+:f}Q51"SËaDöŮb압pu5[’Gˮ'M3$WSM:R)6 8$yK,K,=n_"/tw|?s@!r3;o H@ t QM+Ťq++2QdԜGl d5?nsSNx>ѡ>d߆ufc];z}x9!ٻ}7ٷ8r_xq;>^Kũo:t2|vkr,SL(=~_ $ h.I]p $Ѥ'Y(,e X9xWYQ^ohWI=Àx<0A=<]҃*}/3'z㼮% JGO:yXJxy>,UD"!G)K^zNbG--P PQHG](0Ռ"5ku~(\\̃=oj) =?QNv=f=r"\I١e[VzZjL|JC瀋#UJ\,wv^CjJqm)|5 {+f^]_ٴOvwPbwI+x;E3~Gmx> ,B h0'q[ĺ[8xL G`aEUb硚*A#6/kS龴y~77b@bG1VYe>)k7aQH5LXџPbte:SzЯ+Ao~%W 9aKr%R>CV|j:RTEpf;Y6X<X+tS<+7aۍH0 E#Vv/隉210Q,O\S<3Jq1Qc,L)M'KT@S,a&Nj dɜlh=h*FQR"-s]w.<":摾]l]:=WkؾڴuiH.^)3 om%j]WKDH XHVxI^“l1[#hW_j׉:䵧 XE*F"K%tWc}T^+ҼвZt'z0TjPhP,{s"K7UGl9aVk!H[Eg6ڡZY?G(R9VN d ԒVnDŽCb{y}f)Sv\[dml؆ VcgZ5d(hXјdyr/oU;n]%kydݎ­w3&˲k&Z 8FtEjEw+y7s*h}ͫMdajT-<6w'-sWF.G#>ܡtO>s$Qޣ{nn:r)>Զ}woƎ:cyoƞ|#t/7,^ncMk7RRg%|:cQ>+8S,6^k@6(=J*JR[p8|F}]ȥ8nK:鋁7XW٩zewu-HcTce)+* x\frg>2hNjtxϫv.G{[۱g^vOu*/=fI7$mɁcrP]57yB<ؠ UP͙t-9Jgxf5IW}P].QnfFYxRVMWy7^k\FVpT Fb4VX BMz*Gq ڐSBnk~au!WX賐^˰XrjK\XUu'{1!Ʒ>*ߏ>X\i}C(!7֪^ZVqzJRY-__ ln:OePϷkF"{[ .Mԍ%o4 Į(O Է ŷ7otjac*]k,Zt:xl8c\RF KZPz5Q=?llz>#mgotvtЋ ^g%+_y`r+C!=l{`߲/_JVd%+YJVN( ո*95d˃&d!{>(+*z0|n Xϫ?m9'$yxm/w{}8 gEx ~gp^~pZ  Z£0 c$٥%oD{7 2 S;!~I5ϧ`K\aS<yqԸU@NVu-TGaהZ#T@qsg<h3Pu-RuLU|L,"0bno3k TkJ1BF!h+ 2!xj8vB/4s}ؕgg^ ^Sl,h *6'3f"#bQfI`v F` ҈t+6EbgM6X4VeRټDL),Fr(Y8֥fQ1Ly:U4+(qD&i!2L%4 leF".S_EL>^<7F]sEIҘt1Yvي;0 *O3,ɐئ4-ltJ97) |2)")y0Vظ {cxM?лaK"9+G“nǦI->)IK-11ĥ tF d)%3RA! G1x0BBLdQ $$JB"  YHʉP:C & 1(h$(SB%օIIVf( CG0T:"RNL#IS+70+ ΈJ@Y7s d",Rv ^wF%D,B$lПH".P H%&aKBN&dd 7Ý;rQ's߆HF_w|^2O,.I-賔Ģl#$Fjb @WkcT6JjHԫ M !Y-; PYBb[8_[ǐ~&cZzU6_9!]F)'A6[ˑ rzy endstream endobj 262 0 obj<> endobj 263 0 obj<>stream HW TgD"Ȑx({X o8 4~ ga* סm o1ˉ RJ*HAف .ݨ?^ U?dqQ6. B!s;@aA禟>H%xe{2ܘ.Ud0^{ A\^fF3Z5i7 -G{nb@HĎXG(xgik-eպE]&8BBAE㉅}udaǨK 6D: }Jgu8a֘sl@KPT)@0Q"A}f+40r'FvJ: E)U %`)v 4"QFCbFW@FI#(2#,o38˹{DNI]lXU9=Xdӏ<uF3mmZ{TiA9>Y_uxR1-6f8sf9.̋jkcwtZʚ&&Ѹ)LTc& &k)b};y̖ 8]m yC^LQjCo]都{OVt7(o|T '!fqf{ \&}<0[%k|,xQ++]f;ֆ/9pzlCU++>rɧ0[f!_>/Й192p_o@{4^Ɲ[_znwF Pg/Rz:W&W^hf+1Tr-?N:V8}aì[O > aCt=zI9QVm(߶xߴ{J:K˩;}P9c@ڇ\r=LW:s?5qf&m;B1`?2u[Yn{أ.]]}}Q:ȣAstT҂7YO`Qv= 75e۲CQZCvy׫Dޝb0ʓ7g"*}Q[]7!>ckխ_lEWGd޳efpΪsQMs\>U5V,;_-+b_%1r RlI>*mjJPlnl_ޙ~DyUewYÊnik\pw|̹/w#lY{do9ZPX-un"&zͺ"d֔ǺB\D]s=y[fʧ_lS:ivť>E;!6?-GkPH.~46=_ߗ@ӭцBãM] VR:DQ26<_zkjԚ$0e6=C'NUC8یmYL 0a6RBg)@E2^%)!DVFI$iTEf`o@O޴8Bln 9UA 1HF=쐲|*Ry6JYxČBGE52sE7IiLLkY,@M#HCDžgI\0%oL)B1D(Ód`:!3gd)oV^3IXu6:&J樾ܚCr\#1 ]AֿP)!a-1XA#IT$7ܫBfuuZ%v2ӳw!6Ff+&a.:/w*9g `Os{oZ`SҢ u/w:Rajٰ\OMKm?b/X,L)kfOsM}soۄWwGusݭZEP8aw,XmxTWr_gjmzpTǓr@o}%oH<{f{wc2G(rv|*/=;1;9g JH85ĂQIc465є"w4s&疵Qxrģ YUR!nQy.v}Xpi֏s}jџW&]inr)º&߲-Ct/7lTàai9cYeY KMܴf5S9Znpܶ/z˄e)|ڲQۘSR;jQsiO?ޟmΫMc];UkEV*󙍪{AMc=C8DOv9`Ѹ>]6Yup)9 -w.5kg^?x2K z|!ӒSuBdk_)XhkY9v.|<(Wˇ )0~_=!2i Ӷ‚~*Wol3Tk4q!qSScKӁ^2kh?ħ7HG*7WlSZ#33_cMb>f.ʚfPBVS쐴;7D󲭅ͷ=;ד;ѷecy)/'a/QԸ/yBTBy)|j.Fd`H4O`{H` y{`[&޴2_\*%`s4pFᰀ~c>3p\!( bca-霙XA S&x =i [a'0 gW% `pʠ[b l!OlRw.R O4?a8Z_k,$Ӄ,MJˑh3Zh|! AR~@u! Xƣ}XVC6lBEPG,%cڏjh /Vt}aZ;C*jn-5Kq?C'đxO֓OYf&`2X3{iwv讈b*rIszq΃EC ZR앣T 5(-!edŁ&sH =&d9Fir't2ө tMVzV` >b嬎gWYNpVkp:x [^ث҅cE1L$<@G `xѫḣD#% [nN11Jпp-h3%EQRHѐ ' ?-"! g{h㉒Pr;WPsF\A^rX ytyGL1b2a&pfpNNKjCkGH58J%l:a`.40"mb_mM\g?ݵ|AƖ-V,c˲ڒ%c[jXf($@ǥ %Lf 4aɴG:r&~k_Č)}piJd?+)3];˞ݏq½5̽τ{ X" 9 ғrxנqQDv/^=OH51bۓM݀=H;S{'^x̕~ sd%OWgG{۶[Z[677] 9ԍvŶvRUbܴX$KAVh }Q:Tp;wP[RU}}ݔ՞^<OoӻIL+LL*J}`,苦0k8ٸ3Z3Raw*I54C -R{ձԡ(MM}; 5A%5QǪjL.W{wcTLٚSRI.tNwg\hxyd H}WU VQYlTFQqZ䏦LLaRg+@՚XA#:Lc:Y^kh_E8Xc.os7r>uҤ Aٌۘo >Bzj([g(e%˺=rjɲUq%_ `5ʵNJ*9jhh_D e[k8V\)GR;fˠla2ikܵJ^U^Iǂ'ٗJWڀ \۽O{$ՇquMOUv}j;+( 3C쀝iD@#:בˡ~D#"Pq؍b0C@a/lDjBpD+:'b(P!ղܟQ-:X>v2J} +|'r.Cu{ {B! .nGD:D#'<< y 8 ` ;p<}X )Gڌ*ą½Ja Qip$CȴZʽ%0ɒSS{ɝ9o-PM#!:P\jAeP{ 5 4k=swyG_Mк^+48!1E^loHLFmZצ6k-B(3meLV#}W'{@T &Qz+J{cƒ PP20`fH$s›  hs0@♆U龿nH_{̒8벁e~Fif`Yiq;g {Ih0WGb#NUjz+Mu-X?x{])>p0S{+,t٭w &^ps}z]` n]wL~fR!۶nZX›8_.cO,|;֠ BJOO`)x ^@aagL%ʻXR82qUɉbG'(MJ v4YgJx1'Tϓ0EO4a?`v[16 7X"Z _ Ma 9r\Bgic Cua$j%;~?]|VEg&Hw\<]֎}W2>oVYV/GV,2 endstream endobj 264 0 obj<> endobj 265 0 obj<> endobj 266 0 obj<> endobj 267 0 obj<> endobj 268 0 obj<> endobj 269 0 obj<>/DW 1000/Type/Font>> endobj 270 0 obj<>stream HTP=O0 +<$.=/q"Q'rӡ1ؖ>>w'8-GOPipަ۲L:'ZԵy8'^;u 1C+#7NH 4 8> endobj 272 0 obj<>/Height 57/Type/XObject>>stream ʳ 8N pAA-j"Aj˙#>#pp..~U?KH_yEF| 05_E0ɏaopy.1p/ a4aj )o  vAAV5 (aX(j†@ endstream endobj 273 0 obj<>stream HkWb R"2"<=`<Ő *2y{ WihDh mg ufrke.?a~ݏN̗zxu|wjIIKW?}}'z9IIeZ<9%%`uhp`.hm'Xe&%qϞVޤ%rY bu^VCl**wnջ};GSҢ bӽ|U/iQ \[7gk`>h"~ۑǟ^Fd:'-T=ݻ>`|ª_7PʺIM*bqH\Ϟ9YRٵ#rRԔT |E)@h@񣇝klZ$_]9#ѢlfՋk.TC$ikЖ.I@D!栥X]۹&fo9~BT$ܭAcS0h)kcI%i.qI,ϟ(z7LN6HV% ~ݭx1 טR\G ZqZ522r(ǟ0cxgU Nϟ=mh))06rn15S-<2O8w-7҆V9_8aOa.lƹ 59|6#u Z|Q}ݹدS9֬-h57>QbVR/''}%ZqDF+!vTn\O?16 -,@< ˠr;&2hq6 U@빅X-WǠռ VK9̡e N$l\"_>~{wqD4 d?r7a>ȹڗ/;VapܘD+e@r\kN~ίeaGV|E _(=I5`вT ɘwJI,zJ6<Y=sZ&yb㖯^# kВX1[6j@L ZUPdQ~=tY"]XKXżAل@<̓~ ]Xds,Yh6vڶF.@ H.Rw(;cPVJ  s{o[y&*7s-(-kB Arf-ǰ;ȈFkfx[zBBW.6;7hq 7dOe5ֺ[`DBg׎72LᭇABKc͕-Tco0: ZߠZ$hwnŐ(_ Mm'>xzՒvaU;"hmZQeJ*BGU -R~ YTӆ=Z%];bJy2vUZ ZYS,Z6-ڢ,UA,Huh`-ɏWzS,eR5|b57B 㬩 C-/K2bs Z,ҶޭV̼d{ѧ__0oy}֮1b1wk ޑܹ=5}uQ]p <Uf cQ{ }*=S[wh^|H.o#)T[Y~%; ^7ȂMZM.zrr- "\pV6\!ۖ}5-SMWmmSĪhdmQ6%m5A[E*o9% ڠMyh_}8*bnM m+{ڻL/Ƚ}C~[OZȕ3*n*ױjܑd χ4fѰIIF- `nk> *jDCٵ#W.}*I1Za&hHZU%KVu:RU$iaHB Wri$uk;pmsW`uibC҂ ;F5 z#/_uU;ia hUXᛜsZto'zIG@ 3g߹=zIP@ +N? Zq[Βt-gNNVV"?%vϟ=łW%*\o0'~n}p Zq[“V ڱ{wZR/_۪כJbU1))G ڤ"h^WRRRR endstream endobj 274 0 obj<>stream +(&@=; endstream endobj 275 0 obj<>stream HkWb DDe$L0L6O1+xޞF҃}Fш mg *殹E{Y]aw/ ~]*ִ~E'*fffξ.zhQQ)jlt6:yh$IE7j(X׻:V7]kGGGMb҆nԒG}ĺn]߻;a.zNQY7G 7F.E6A|gS0SUZ=2boK oomg7XeltD&g.zQ+[/_<|pE짟|믿`zҦ֐lV*ިz xxhYkă}<0ӆzeN֓ͅV铩-o֛6WTD-k]q]Ҹ^|ZOn {i|*dAV֜Ӹp6j *e5[[۷ӏZ 6nqHss?7ojk*Fn\RB|⅐sչcv5IBTrXzg;LUm!9-N|BwQU.ݝK3zp/}Y! iO>5S[؋5W=|!K{;)ÁkQjG{=uttP`WW'N- Ô)W$ntƓƓEaƬ,+W?o@r[qbkSU6Whk0,3RR!Ğ:yB+oR@[/h=n/er͢"6W{Vb!MCW9ǭ5nm>ƭIn]t5m[Ư $+ni}ґNmĚ-obLպ~ܶE#NA'BڼQv ,z]Йa[,lh1~UC9v(lYEV)a?s:pguؕW/J`j6 T?wV&bo<6M)- HR*"Cۋ7Yu [nݘ^nlq 譭{n1,+h zMrK =9ܶTsxí Iuyu2dp1 '`omq3*B+CeyQт:j[ow5 sk{B,ɃVƸu&|.UދMn8[ݹXUYŨqﶪuNvp/S sK$uPIn)ȣy2sl:5ntCF[~"u̪+iZ[xsyvyy4nTn '$U[1J-v7QU\nTѰ5eLgy;m !l %v3km8l"/[nd舜Zk=X[m{Hܺjrc!ʑj:d2n߶ToA,:Րz VWI滊z(ewYFi*ڃXnBf2Њ[Ru[*y^˥nЮhnݷRzը.n `p](106)éUh[a=s#ŭ6ˊr: d0n&qME>AIn$9irEV;QJː TnPJpk`l=c;Yb!с:x`7?kRmh1O+ʭHݝIn[P{ {V}Ao$FT^꿺0fֵ cm6'`W&R.uQV#u)(إf_-2In=&KZ,n%pJZnnj]AíUɭ̣m&m˗Yfumn]ЪmV.Ye6:,٨('ޞ@2t̄v@_8Gn%nܒ Tyoy/2W.mjk '\E/IJXi x[۷ 7f+WEAJ¯Ҁޞ@yp`u"CyGxL3~<uBE~B[Ѻײ[l֐\ɇv/_<-.zQkT_Eʚf=-zQkZhom(b~ٹ[x ,nqJ}ӏKBPZZ[[jDEEE-/O z endstream endobj 276 0 obj<>/Height 58/Type/XObject>>stream ʳ'8@8A8pA |KA;Ar|s>χ> endobj 278 0 obj<> endobj 279 0 obj<> endobj 280 0 obj<> endobj 281 0 obj[/Indexed 381 0 R 255 160 0 R] endobj 282 0 obj[/Indexed 381 0 R 255 158 0 R] endobj 283 0 obj[/Indexed 381 0 R 255 164 0 R] endobj 284 0 obj[/Indexed 381 0 R 255 172 0 R] endobj 285 0 obj[/Indexed 381 0 R 255 170 0 R] endobj 286 0 obj[/Indexed 381 0 R 255 176 0 R] endobj 287 0 obj[/Indexed 381 0 R 255 180 0 R] endobj 288 0 obj[/Indexed 381 0 R 255 182 0 R] endobj 289 0 obj[/Indexed 381 0 R 255 184 0 R] endobj 290 0 obj[/Indexed 381 0 R 255 195 0 R] endobj 291 0 obj[/Indexed 381 0 R 255 207 0 R] endobj 292 0 obj[/Indexed 381 0 R 255 205 0 R] endobj 293 0 obj[/Indexed 381 0 R 255 214 0 R] endobj 294 0 obj[/Indexed 381 0 R 255 218 0 R] endobj 295 0 obj[/Indexed 381 0 R 255 222 0 R] endobj 296 0 obj<> endobj 297 0 obj[/Indexed 381 0 R 255 232 0 R] endobj 298 0 obj[/Indexed 381 0 R 255 230 0 R] endobj 299 0 obj[/Indexed 381 0 R 255 228 0 R] endobj 300 0 obj[/Indexed 381 0 R 255 226 0 R] endobj 301 0 obj<> endobj 302 0 obj[/Indexed 381 0 R 255 234 0 R] endobj 303 0 obj[/Indexed 381 0 R 255 253 0 R] endobj 304 0 obj[/Indexed 381 0 R 255 251 0 R] endobj 305 0 obj[/Indexed 381 0 R 255 257 0 R] endobj 306 0 obj<> endobj 307 0 obj<> endobj 308 0 obj<> endobj 309 0 obj<> endobj 310 0 obj<> endobj 311 0 obj<> endobj 312 0 obj<> endobj 313 0 obj<> endobj 314 0 obj<> endobj 315 0 obj<> endobj 316 0 obj<> endobj 317 0 obj<> endobj 318 0 obj<> endobj 319 0 obj<> endobj 320 0 obj<> endobj 321 0 obj<> endobj 322 0 obj<> endobj 323 0 obj<> endobj 324 0 obj<> endobj 325 0 obj<> endobj 326 0 obj<> endobj 327 0 obj<> endobj 328 0 obj<> endobj 329 0 obj<> endobj 330 0 obj<> endobj 331 0 obj<> endobj 332 0 obj<> endobj 333 0 obj<> endobj 334 0 obj<> endobj 335 0 obj<> endobj 336 0 obj<> endobj 337 0 obj<> endobj 338 0 obj<> endobj 339 0 obj<> endobj 340 0 obj<> endobj 341 0 obj<> endobj 342 0 obj<> endobj 343 0 obj<> endobj 344 0 obj<> endobj 345 0 obj<> endobj 346 0 obj<> endobj 347 0 obj<> endobj 348 0 obj<> endobj 349 0 obj<> endobj 350 0 obj<> endobj 351 0 obj<> endobj 352 0 obj<> endobj 353 0 obj<> endobj 354 0 obj<> endobj 355 0 obj<> endobj 356 0 obj<> endobj 357 0 obj<> endobj 358 0 obj<> endobj 359 0 obj<> endobj 360 0 obj<> endobj 361 0 obj<> endobj 362 0 obj<> endobj 363 0 obj<> endobj 364 0 obj<> endobj 365 0 obj<> endobj 366 0 obj<> endobj 367 0 obj<> endobj 368 0 obj<> endobj 369 0 obj<> endobj 370 0 obj<> endobj 371 0 obj<> endobj 372 0 obj<>stream Acrobat Distiller 7.0.5 (Windows) Acrobat PDFMaker 7.0.7 for Word 2006-12-27T17:56:36-06:00 2006-12-27T17:56:27-06:00 2006-12-27T17:56:36-06:00 application/pdf The DockWindowFeature Feature of DockWindows David C. Morrill uuid:57003269-3d3e-4b07-b937-a5d8502777f1 uuid:a13b4b70-93f9-4a6c-b2b5-3eb3e7b1b370 1 D:20061221 Enthought, Inc. endstream endobj 373 0 obj<> endobj xref 0 374 0000000000 65535 f 0001006957 00000 n 0001007214 00000 n 0001007242 00000 n 0001007373 00000 n 0001007504 00000 n 0001011121 00000 n 0001011177 00000 n 0001011233 00000 n 0001011513 00000 n 0001011921 00000 n 0001012053 00000 n 0001012183 00000 n 0001012315 00000 n 0001012445 00000 n 0001012577 00000 n 0001012707 00000 n 0001012840 00000 n 0001012970 00000 n 0001013102 00000 n 0001013232 00000 n 0001013364 00000 n 0001013494 00000 n 0001013626 00000 n 0001013756 00000 n 0001013889 00000 n 0001014019 00000 n 0001014151 00000 n 0001014281 00000 n 0001014414 00000 n 0001014545 00000 n 0001014678 00000 n 0001014809 00000 n 0001014943 00000 n 0001015074 00000 n 0001015208 00000 n 0001015339 00000 n 0001015471 00000 n 0001015602 00000 n 0001015735 00000 n 0001015866 00000 n 0001015999 00000 n 0001016130 00000 n 0001016264 00000 n 0001016396 00000 n 0001016531 00000 n 0001016663 00000 n 0001016797 00000 n 0001016929 00000 n 0001017063 00000 n 0001017195 00000 n 0001017329 00000 n 0001017461 00000 n 0001017596 00000 n 0001017728 00000 n 0001017863 00000 n 0001017995 00000 n 0001018130 00000 n 0001018262 00000 n 0001018397 00000 n 0001018529 00000 n 0001018664 00000 n 0001018796 00000 n 0001018930 00000 n 0001019062 00000 n 0001019197 00000 n 0001019329 00000 n 0001021135 00000 n 0001021377 00000 n 0001021430 00000 n 0001021483 00000 n 0001021536 00000 n 0001021589 00000 n 0001021642 00000 n 0001021695 00000 n 0001021748 00000 n 0001021801 00000 n 0001021854 00000 n 0001021907 00000 n 0001021960 00000 n 0001022013 00000 n 0001022066 00000 n 0001022119 00000 n 0001022172 00000 n 0001022225 00000 n 0001022278 00000 n 0001022331 00000 n 0001022384 00000 n 0001022437 00000 n 0001022490 00000 n 0001022543 00000 n 0001022596 00000 n 0001022649 00000 n 0001022702 00000 n 0001022755 00000 n 0001022808 00000 n 0001022861 00000 n 0001022914 00000 n 0001022967 00000 n 0001023020 00000 n 0001023073 00000 n 0001023127 00000 n 0001023181 00000 n 0001023235 00000 n 0001023289 00000 n 0001023343 00000 n 0001023397 00000 n 0001023451 00000 n 0001023505 00000 n 0001023559 00000 n 0001023613 00000 n 0001023667 00000 n 0001023721 00000 n 0001023775 00000 n 0001023829 00000 n 0001023883 00000 n 0001023937 00000 n 0001023991 00000 n 0001024045 00000 n 0001024099 00000 n 0001024153 00000 n 0001024207 00000 n 0001024261 00000 n 0001024315 00000 n 0001024369 00000 n 0001024498 00000 n 0001024577 00000 n 0001024872 00000 n 0001026782 00000 n 0001027097 00000 n 0001027609 00000 n 0001093195 00000 n 0001156805 00000 n 0001157108 00000 n 0001158907 00000 n 0001159284 00000 n 0001162755 00000 n 0001165781 00000 n 0001166137 00000 n 0001166163 00000 n 0001166298 00000 n 0001169281 00000 n 0001169335 00000 n 0001169712 00000 n 0001172542 00000 n 0001172894 00000 n 0001175455 00000 n 0001175807 00000 n 0001177110 00000 n 0001177463 00000 n 0001180475 00000 n 0001180840 00000 n 0001184941 00000 n 0001185232 00000 n 0001186094 00000 n 0001186458 00000 n 0001189607 00000 n 0001190002 00000 n 0001193192 00000 n 0001193654 00000 n 0001193967 00000 n 0001194372 00000 n 0001194676 00000 n 0001195060 00000 n 0001198959 00000 n 0001199237 00000 n 0001200722 00000 n 0001201037 00000 n 0001203316 00000 n 0001203711 00000 n 0001205247 00000 n 0001205716 00000 n 0001206036 00000 n 0001206716 00000 n 0001208299 00000 n 0001208695 00000 n 0001212041 00000 n 0001212679 00000 n 0001214747 00000 n 0001215203 00000 n 0001218249 00000 n 0001218515 00000 n 0001218805 00000 n 0001219003 00000 n 0001219247 00000 n 0001219400 00000 n 0001219656 00000 n 0001219996 00000 n 0001221686 00000 n 0001222109 00000 n 0001222159 00000 n 0001222292 00000 n 0001222426 00000 n 0001222560 00000 n 0001222694 00000 n 0001225978 00000 n 0001226333 00000 n 0001227466 00000 n 0001227520 00000 n 0001227574 00000 n 0001227628 00000 n 0001227682 00000 n 0001228022 00000 n 0001231524 00000 n 0001231956 00000 n 0001234751 00000 n 0001235357 00000 n 0001235689 00000 n 0001236146 00000 n 0001236478 00000 n 0001236818 00000 n 0001238540 00000 n 0001238935 00000 n 0001241139 00000 n 0001289516 00000 n 0001290125 00000 n 0001290429 00000 n 0001290800 00000 n 0001294616 00000 n 0001295150 00000 n 0001295482 00000 n 0001295865 00000 n 0001299327 00000 n 0001300132 00000 n 0001300524 00000 n 0001301017 00000 n 0001304297 00000 n 0001304787 00000 n 0001305067 00000 n 0001305557 00000 n 0001305837 00000 n 0001306334 00000 n 0001306614 00000 n 0001307109 00000 n 0001307389 00000 n 0001307574 00000 n 0001308418 00000 n 0001319973 00000 n 0001320260 00000 n 0001330191 00000 n 0001330478 00000 n 0001330683 00000 n 0001330969 00000 n 0001331358 00000 n 0001333713 00000 n 0001352822 00000 n 0001353174 00000 n 0001356711 00000 n 0001357050 00000 n 0001358863 00000 n 0001359233 00000 n 0001362306 00000 n 0001362691 00000 n 0001362971 00000 n 0001363369 00000 n 0001363655 00000 n 0001364013 00000 n 0001365241 00000 n 0001366094 00000 n 0001367071 00000 n 0001367471 00000 n 0001367630 00000 n 0001397000 00000 n 0001397282 00000 n 0001404359 00000 n 0001404581 00000 n 0001404809 00000 n 0001404964 00000 n 0001405097 00000 n 0001405247 00000 n 0001405433 00000 n 0001405721 00000 n 0001405952 00000 n 0001406353 00000 n 0001409453 00000 n 0001409598 00000 n 0001412714 00000 n 0001413117 00000 n 0001413351 00000 n 0001413827 00000 n 0001414074 00000 n 0001414311 00000 n 0001414358 00000 n 0001414405 00000 n 0001414452 00000 n 0001414499 00000 n 0001414546 00000 n 0001414593 00000 n 0001414640 00000 n 0001414687 00000 n 0001414734 00000 n 0001414781 00000 n 0001414828 00000 n 0001414875 00000 n 0001414922 00000 n 0001414969 00000 n 0001415016 00000 n 0001415168 00000 n 0001415215 00000 n 0001415262 00000 n 0001415309 00000 n 0001415356 00000 n 0001415593 00000 n 0001415640 00000 n 0001415687 00000 n 0001415734 00000 n 0001415781 00000 n 0001415852 00000 n 0001416009 00000 n 0001416146 00000 n 0001416261 00000 n 0001416315 00000 n 0001416446 00000 n 0001416615 00000 n 0001416759 00000 n 0001416813 00000 n 0001416961 00000 n 0001417103 00000 n 0001417157 00000 n 0001417285 00000 n 0001417339 00000 n 0001417488 00000 n 0001417542 00000 n 0001417685 00000 n 0001417739 00000 n 0001417793 00000 n 0001417956 00000 n 0001418010 00000 n 0001418187 00000 n 0001418241 00000 n 0001418413 00000 n 0001418467 00000 n 0001418521 00000 n 0001418575 00000 n 0001418711 00000 n 0001418765 00000 n 0001418889 00000 n 0001418943 00000 n 0001419065 00000 n 0001419119 00000 n 0001419253 00000 n 0001419307 00000 n 0001419441 00000 n 0001419495 00000 n 0001419623 00000 n 0001419677 00000 n 0001419844 00000 n 0001419957 00000 n 0001420011 00000 n 0001420106 00000 n 0001420281 00000 n 0001420392 00000 n 0001420446 00000 n 0001420541 00000 n 0001420595 00000 n 0001420713 00000 n 0001420767 00000 n 0001420891 00000 n 0001420945 00000 n 0001420999 00000 n 0001421053 00000 n 0001421187 00000 n 0001421241 00000 n 0001421295 00000 n 0001421349 00000 n 0001421403 00000 n 0001421440 00000 n 0001421465 00000 n 0001421544 00000 n 0001421682 00000 n 0001421824 00000 n 0001421966 00000 n 0001422075 00000 n 0001426082 00000 n trailer <> startxref 116 %%EOF 373 0 obj<> endobj 375 0 obj<> endobj 407 0 obj<>stream Acrobat Distiller 7.0.5 (Windows) Acrobat PDFMaker 7.0.7 for Word 2006-12-27T17:57:04-06:00 2006-12-27T17:56:27-06:00 2006-12-27T17:57:04-06:00 application/pdf The DockWindowFeature Feature of DockWindows David C. Morrill uuid:57003269-3d3e-4b07-b937-a5d8502777f1 uuid:bf8c2344-1301-454b-ad2a-2851a39691b2 1 D:20061221 Enthought, Inc. endstream endobj xref 373 1 0001433943 00000 n 375 1 0001434264 00000 n 407 1 0001434407 00000 n trailer <<17CBDCE2856891409766E5C965F14AF5>]/Prev 116 >> startxref 1438414 %%EOF pyface-7.4.0/docs/DockWindow.ppt0000644000076500000240000615100014176222673017474 0ustar cwebsterstaff00000000000000ࡱ>                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ɾ.|nC_^M1)yO4?PNG  IHDRxsRGB pHYs+IDATx^G.{DdȪ:Uu;H#4f7u_眚X$Ab{C d/Z1Gdd>?nݿ_G[T,vT8Cn̨]}HWG>i+Pn#;|7?_g,JhlT':E_Z?Il} `?$2hU-Ӑnb?֔L=+9EHwX85H$-rq&8;N d YԼZ-U_ʲ(ʟgEaGpw?=R;;fa7q/J `e%p;ڐ)@[,DqXt/AnD$]!hp,jl,ٮla&c0"bEpݡ! @7N+$Rr75??cg;6):Ԋwp˺M?V{_)ȽI {YAtjY7kT2 PP5A,_ؽ{7w$yno8*xxȲ,E߻W7w!MAֻ`XpM;YAC0 g~Chtyg%gg}/"*?x~q0QvdX4FQ2NaYiHYSv"Z߉C2Q2ܠ?R &ytr뜽ȵM'|4]6eoRb;U֣B.Yְ;,% C Po)EA > kG#icO<4 YW c94Ǒ$M)˥oedbMq(ԪnFh*% P~f)>xlҏEx: ,}tţζnDG9djs8=oAfk#/ ©G,? }!ÁMB~0]Bё~tT;^@jheB;~#jM9VCFd1{є/cYD׹:2[*T\gp6 P[Ѓ41VLn ~uQFjEMd5|HlM:_~j\G#yul~$H)̗>Ƭ"a{71\ 5b[Cju"DC HxuM9&{T %Zl jno,Pi G}ܰ+&\Gb!(c2!DF&(gW*W0XzE{%-tLRiH kN*Ȫ%LZ5c#=MdE4_XbU.DZ3wâ)GsX slmXc7 =ӵ+6<|TCPmF+È @0 ʫwvR-RăN5cEZ/Gš3ʁZ-#&r.L0Ew GzyLKd@fb9 L-J,ܬ~5k,\X ,tR2"U蜡^B~Ԥ)XQ_RXOB>&D*"fV8EFB-nR;Gڊ;ZwȌ6KC eEͿa%p 8'h[O Ybp ϱۣK~ne`ǯkD9ZE8TlV\8 !1 , {K;f)]* (UQ%aL~k6"vQq '"0F!`k4յ )q)%y=dVKy&-s?@QYcCԃ 'h򡀞?qI?4@vy7LVK3\':yYf8݁/Ar1kw#0BH8-Lb/9xzVRF%h)qC QG{3yu6a+j9Nh$C9#C3z+- {'8*>Ki.:dȪ_/V6 p w\/FO3At;R([lVR3LeH, 4rFId`D]fICmIo&49b&JLFHp8RyZ[N$~Bw=qe煷3]3rp9:iO! EJ?mH&swMExqj;wQF@WpV,`/H] HVIU[$&g1nIyruqYގ37C m&X;Hw9w${s9/[J"p^Qs W{0Y9((Pk!D9/YĈFCݠ3[z@\Wf qeǤ[feeqۄE6ýY~?"'^ ޽`يALU I09$m&\`ӡ5Lq7<{n'CGn$̳I(reó SgDs=jա1ьy-#s#6t3PZdx7 aX1̃?I?ZhSV:cU K7ai3,PiKK` GM3ߛ]*OlN>,iU?\bDv[B6"TI @0:4M8+oȰfll=gL&L<"0S2O#F‪ˑ]afIX2-|D5NعgtZ0`ahC:s"zim2^;op΍ 2T`IN3ޚB8xh=5P`!;St\ՁAd1JE]"6Gu< 6kmIPu $:9FψH51om *Ր): 9x[o9<ޣ&KRm#~%})$4Ip1]az77 = kOU4~.C_]؀I@3C?A߸8|ȉh/5_hG uvvIR 1ک'djwD⌹T7䳏> eN(0@C)eK8>΢R,K䏒&Cdh>Ҁ-@2 Z$YRvє*} =@dc:\6"6&6Y9hS`3~_mhQXF7W^ԸHA}oU~)$obrϰRuȦ$v.X|(Q dIdp&VplPۘ P2t+m@1G,drA!|~#B IeY+ YR,^!Y>gXrdβz#M wlVilF D9|1A/1ȶY*qq=]Peo=+JjbP "NqlO:aPJlN}$8{dxZ%jSmjPMRAb%ޡ9gmqԂ%"=j`;J\yܑ˫ZQjDr10x-YnU?:3lxaA@AY@̽~f [UO;>+?EPRV6 dZn5,8a\ Y!huh X\ܬ5(3穁 Ug9AŅbkM*8SGnl'_5)plxM0l l ;YR1}*Si$9Dl>J5j]}Z~ʱ804Uc#"{TU@vrTf[n.Xa"5&_F&VCtj0n" )q WeG09~Ju$&!l~FbƬ?t+py,ւ9(_:#( /tWdvW}48n,ohW}n(7Y# mP PgIPm:/$a|D>hdL'75Ih3_:d _~eh@; cD.aiMpB3*$Ԣq'i:}vqZџG4bc(/:"{/O1`\T8d 4\G`Xa t 0{R^!xbD]3*_{T%*6bBdN;K ʳ! ( .p tr2o3gImkR aJ0?bw?)lE=u(I`&RjѢSKtZS@7_tiFUѢNӖkkmTk>Q Ԏhƞ$iuDNiVbg97tfq=[((+ Z 68_{{U1Hj /܌usP+RP8n#{;OxƑbr)\!a_-W`?=+aQfPTllhpd&,RTɉ\ >@,#@a㨄)P D,lT z cc{D'H3vr_#Tc><م~Z\hQ43&N;ֈ/vr*~ T$twVXkJ(ǖ7p$ $R5#j- f*3.<uZ<iHMNXn52{^t4)rSSu=ˈ60!orϭN*K>(S##;߸'%4lŁijKQ-׭fe\ mmC Vۣ+MAl A+4xwh֌IXA;UFA<㢬8qT^:) pUw7:u0(rQԓwY݀Azl`1d / pz@aFN>6axrEŅnՠBm:v5z+2;)H1ǭbKtJVNw>CtJ >?ƓWZmNamHCkDB1 ]*09V>RHq,e/Od#z}@v:F,q`U%3n[@(|nM).matKzv&f&bP{R tQ^r1Z]1%=0򔁀&5@PVQԚ Gn:B"Yx5\EV0QU;Ζ܋C-? {a|*[4Z݇J/YS.pWki>UuH znʐKW֐j,R !pPQzLDs qſ2މ G{yaD[߁oy[DfmN <7tCPG`& x9e%HO 9p\b@C+/G\'AUuOBS5#FE%oxD\_[cnveM񼉡U6kQv>Yh#*C\E`6Hwttz: +zMQ,Fs3WqoYVzA ~yƭ~4 "Bw& F()"a%pziY-/ښ~*5S3Yߜˡg QXQVrwim*lMˡ5UI~O{G5yEْ4rҹ'P֤DIT 93*Hc휋moZ]eO1Taejx{H˝p?IhS*xg #}-NZ*qBYB~6F]h~oȷ*(\[<~C20%vRu` @G2+v"8NPuH&8(`:\?3'e@;k]!w+ O4꧉3ﴇj;/bm7P"g]FpقM{<ϖK[,Wj6n;d N:2NS gh-8j&(9dz~}^]oLǺ-0-_}I5z 2Σ1[>uH9@bjcP&)b&*v)+VTC0 TiyNC(coTRa&u e!ts0L4aOh`4%8 kG!ӛfͨKΣ0b>Tӹ:Ph"T1SgܬUnֆ>huu:x8O= Jw7noXLX l&6T\Ѽ5єȕ,S6V; O_X3 X̦M^_:Jo8\ B]dkC *xw])!I qDkh6@pCSG6F>O(;],-%Т4whWGH./7FU@Z^iQRU2H̉I[kW"/7'P\n9uvOlWa`"؉MK|m0'2E5Z,a NnT(Q ©Foy[ƔNYW; XrN1~HhD"|ؾsϞv#Sbh3M >m"VV״G˜`U+'w1oqdrW7A ՌlYAh͔MKK)d&eۮzSG-/B7jP]p$p;m~_WvbƶXzUʵmp y1WKd8NД7<3ޮ48yE;/-o(&[kBX˄2[ y.F8n$0Zd ,'h#গ0#R M[էQuƍM=[,Rd* *! R@5Mh!(b6y̳?.Bm"\tN r뫥FJALGOiYEQK9q fxI|7*)Ű<{cڙZ}P "UR4bsʆѝK-oe0ȴL9%=a&Gd"_A Y`G!l)L#3gpiŚȵxƁMQ99uHedҌky%)xձ@DMb BOJr;RˣCK 0LaD̈́GJ/УBC5-t)QmGl#:=!xEl e8ƭrNfv ,ì%'";\U̠%vk]dyC>fqFn2!oy˜P=z LioP5p Xc:\R41x5nP☒2x7/[[]%8D_ 5@u(bK]lK]ļEOݘd;6N,[E)GU fij @cB-VXr5Doq9WDd[ w<'_9 51ɀgm8|)clGN%%w~t5];(;'i蘊!b^*I?SN]R|/% Gr3W-y]hPJ/rP2nAvMe",:\.:ΒN񰫍ᩂ`# [4.`YKH,%;V%D6ylZf4⹇f_*,=2EfqXi!,2x5+,"o)À5|4C> R[9 $pEs, B3jI!,SϚ5%q3Fdp:J < (F@C2@JWR_E<+ /t\G&S"K/7GPQ!RqZَߕZ9ʤ=tLZ8 Cim7Ɲ @SRU43"ngc)cA6B5kUZv@dNA'<pG6'N蚌Ҏft/v+)0"/UDmQ{hڱk46 P4 e m HS2A yD2Wgʩ UF̝hd/*Nz0:WCtQ՟;BZVpq-O嗟q*EYifXJ8`qpM%s08 խTr"_g;H]bk%'"v'Y-S ]І!EZyj{P|rU8yl9܋ {sfKo&Cf7maCJ4gOEgF)q'EMƷ=\I?~W`ZY[ yF$}K9e mAǴ0rxVgwFhIĠ$A;E]!\Tx ƹ$ "CNraMčK!Ad}2zjZ@ .g+G"QJH:1ƜÌ]BK:1 #,Kw/CK1gyCԬ%nà٧`r D񦏔rVB@-gP %7![إf0ڃ*ݳѱRTdnDYm}p֢n*ra}xt;,Z`B-Ts8,S#E} pm%VJ,.9ZL:0k94Bެ[ $TPDU&g$Px$|Vq?d?"e18qp ]EΒsC:Y E^koH;[Hc?L0Mw6^ :x3\ߓ \T ;C'@iiA*+} 9RC$gΕÑ6zzĨrl{|qL!(x `Tjn( `?$ ΧH2` M=}{IPt!ڒAx36g!."rE֒.nb\9t\{')RCY`pBrӵXb'dF +@YG6 >B:"+XdѕBc,LfÖB<>jEcNT|AԱC-$1lX(,hi5n L#s.7(^ᭀSr /< .a2T.݁HCmM&_[1s4ZKXGyKn(vЂl;Bo y̠蝊[$dIhLX- ef 1k7J0o [YGMSEN9ѧ:90kD;s% b1.Lj9 vtLG_ykBcX|CcfXLFJX\ 7i:s9膘#83jo&[6.@D9PҩR{h2puԳQ?˹,. KDCg.t(P" ކe4E;@r1(2וr-&_:G*oTWwzΈ!Ӵ&S3Cpne{ 568q&ltm:!1[[=]CJ >-# Fߩk0N>@dm눬Nm^6*}d:x]塪Cp*b ` 2$g-\?t.N gN)j E?.QeթM0@'B⯙ WGř 5m;3:r*5PM9B:õ(N#" \$6?kzнp%|UE AJЎoc&<;6^_A\aԖO4/cfc/?H\d'f'G؋QEdGltjpx,`>\?{R51얄p]GƑ,glk0/܇9Vr'80D!:b|N9F|`L05o`ZD[X ~GI[4 TC";ӗVTךzTb/@ @ťȕդb}rMg̚RbE,k ֧F[&<INQzOݹ\-Hl]Z ʣ mx.HB &ce5yJqcrNvF'E]ӕ?N_ЀU E+aO~kPqC\WϝRz@KCG#R\gDf?C?eOšl_Hj=O?d8 1P![~ N%!2qZ*C:EjٲmcX(ðuM8(U-d*&= Q 2t޹bζC a(WzLuee?)0gO`DWN~k!o@TVJ5a4. 堞gBAհ'Sj./ԝ0v4޲_̨ѡ_aBRTM(")^ lftJqodƗ~QL P$] R{ JF7ʪG]iYnMb"To=<hL&*I?BP]}c.g t@5^\P^n) WԎ@+H:>}Yn.p#I]ыjib~j(NyB*lnn󴎞}vd7ݷGd:6OPg_ ]V /lҧjoGzTA#dnـ #IV#^<՝58bXC)bGe0{c St-*RϽ5xܒa"^eo-n/M)e9M9+iBǘ)SdOo{0[&冕2I)N@V=-~;H2t~==ut LKߋ'6zBs^G{f:%|NG=FU±-µL=%FJ&rc\!ȌNv4.3z vn8mNtc;^b(A~|2AN{Z2y϶ >| ^͏ r>h ٌ́]ш QρVi9H!U@g<عYc%ՉAӣhkq2NdCMmP, h'ǿr֨a$F4AzT(%Ab=*84z~孰j)t#Z8N=8%)MRƛ7ЬV.6W.&?-)Å9)z4 t4vt)h!#lJ6:[`&g: G+bTeiacn|p`Q?ο{yW@qHp:;W2Gޯȱ-\PF$qPVAi kB)1(P8Wr]m\Bb 9kF=D'Dl Q#(N |\"&z zғ;r\'WAdEҘB2h r*-oo`T<#kd"=)ߴp67D41)P Z@Ӽ*+QA!AGB,^5(HEA!Y0Ul!`/FCҍc{BzaC$RskFǽ7ac/w<1AJ$g ZFQ&A_žx9 X6 a}},5*(O$4̏fP3:.k,91d-RG/h+"B'E. :?`gY@ܧuL9&68ܗ<@ȯʱ m7%vD=4 %%Fø UN\S#dSxl3Q]&#I˧bf]Vp6 NA3SyC?+VrRV!wSF7{, wbISCG&.(H[ 9j+\u`D1‹E6ʲ0rmr<"5vѵ1:VK,P4Bك G M$Hi]JlzCF)l}N hty8n $36?i4݋2C;Q~ jspUgRV'en\9P6(n"ܼyʕ-F#4.^U)tQےC 5{Be>Y߮ 9a&&ǁcаڦzn1LyAM$`-5P7&w(h᭤ qm)S(I27z9m% >Sa /dߢZ}4,S곂75I;̺TVa$8}TdMg6ICz(2.O qppi ;2C-1@4lBC%SLWkV_"ϊ>I&1f-Mg8\Mxꥐd,MP\S^{@e+s-qtf ITG:װlpFXZjzfpJ :371Tp >I:81ҼDh@6&C̪3RAJSL2ZX#xf!ɋT{0ET|,˭J]ʣQPpNڣ|Z7KAY>p R~뼗{~gggZЅR E[gvAV.(:8&TpRB["= v^)f;yr @):(UU7OD)[P3^c74Y=߁}AY3=leGPӿehyE 7hlLH(YOZ[)sMtb=K? &#=?N?c!̫D=~A#pa":x  TغdJ ?IC\ Fa5&RE+E@PtKn2%* ΰ HRrIm%WiS(=q[ WpN5۰`d(7O/kc" 3(ҎfCVw9=ma q.mPq;iEpj'c_A ,!cDt XÂ}L 8>|lSxB7yt2G ,c?;MTQf8ff#QM~\u5ɫ6%[ aW O7gU"1C$UdV;&#ّSڳ@52vf;$5Y&>ե msuTz(pE4i$1]:!'~?=kNb9h8+ćGhPG 0piR9\22;'0ET,R̛[,/ c" @bWߪt^BꭤkAqCfDgȗ$[.d mP. ~Dp:zd," ) %@tUa'%ه;lY xt+˔wNM.)QQIo<1y/B T7s~Jh#O"@IKHMGGwAl=NS].J^@=&S=MUpبIbc͕lSf\z{ ;WexoxVyO(lx@yN6}i-=ըA_rae%-/14O%(e.jBa54m#x+΅lPsu+ hֱiDW Ot$n+2Vz0Pb̟OH 8QBj BKLc&,kӀfI,ՓaW|l^J+uZck첼83X 2;@lPq$Ysxvn||sN15N~4eI8I՘P tzĮj"TAN&EAj1J p9-͟JHFIsI~ryPM ʖId 89njFBmrx0]lkH1eqXnN! ;0śBRe[nb/A9N-'WДB QP < h,Y KPxp!1qK@%-c$mRBu(<Zz"VbHԜ3OUp6]Fh,66HB9fXӆ {4B.D\LwZF ,?)ِԕqND?{\i6R׫9b`Uk1m]TTU5 3gn md<Vh… y4L m.@rk%Tm9_4Tfs99 5XyﭵHbQVj|%(z躳*@=: Ado׎ؓP0: Yx̉)iwTpc:%ͿfRQ- pcrؠfl` )I/|iC#$3@Ld\ߑz6棆<rmbk(p{WOOz}lB#˝R"F&a=1 /a-^lᔡQ_]Cj:MЋVa bV"qOE@r'\"'m/wko6Ue6*JYڪea-b[32 RWJzKgQ԰{ m+DBͷpԺ6s5m c%Jr()p퐫rUP4\.3 ۺdd&1Ͼ(xÞgF|.6M_Y^ u_òƑ$:_$GX/89YD3Y$@[Y>!+cCО"kCHtdCe0p9⋊ J.jn{$guH yUh%>KFÈA` kKR\3kN'di!9^= 61j}㌴'J){7-&P*_}0AR>-K8jZ l@&q$Odh % !Ex:˱FՙE+K?WfY͋bs{v~W">B4o[ AuAl TldH/z4QKzɴs` 2V Q6Zj{V $w-/`?X뒰 @b8&ljMømhU.=2ƙ>T޶8!IW8QG t2_Xq=qXCMfp.a1 2_w<Fqǫ'Xag"7AhGB5:>ih^N&e!D&5eh#uJuYKASV8\PG h!'jRXZq;NݴY8XSz4c$& QIc8u4f2X.FCFZ :uTι69'P#AxلWQ?Gk ӌM]" =^dL Z^!نDo'84FW?2u30JYJHh(g1¸<1[0jR1\-˘#y cj nNtIY;4߳ۛyJdI#*qWN ̡\ 7rJ4ƿ=QGxUI-4]6ySR03LiHAOJ#"0^+ڑ4y {!?bSCѺf~S`]{ڄ.8IZӆ]7[?jZ*tU3c2!Lqtа\tH#&M܂kvcL*V3#I!,E ( ŒB馃 2\,3+@]^1h̆iTc㻦l!|(^4n+OfG(ՌV`ʼeJk" ҤSk66%,UHrnؿu[_$RNPK..ȑ\pM,g.7c ([cnY\bwi&9ѳILt~ÜO#Lc&l69gNmʥd˲(n˜Wܖ$g3rbtUU7OrKo4<@7{ M (˱x3P%D@ѱ]7 = M #Bh1*s?N#0-HxXef}.TD[se>@WQCZbY!g݊ s#uPgE|%n*p$o>-~$ֺW>~> "tydlRd"/4?Xۆl-%&uF Sniy QOqaPQ,%ܸ?17'mSdx Fi)ddʱ `qP9j2VcĠ5fm1X>.TIÿ"lzn )2[&N 8<58M! kNVP/^1u›+5~8V;`34^ g_M)ێJy %/,iRIw(Q>/(+:&,*ԹS:vìR6# -;r𯱚%1ʲd.㞘nhS_„.aaV)IŃ{hCM=̃s@\dɌ1h^c S(bXJ9) [Ueo~f{hYxv "Dckk}|ZSi_nנ'XV46՚I,(blOqy ; Ѿ "2д_ ܼfPuP2o786Ƙ]dnkJi2#uIiyi<#m>G15XQ>]y?kfò>Oed h䮳 kf.7$g1+_riEwvzN]3 %I "ˮ#[d]ܗ9eQeޡLFK04ɩd8}a`jn#YJFPǩ#rH63pc2y<>P ;y#"2m,ӹwN], W;Q;hQD?<(@U Uއ"8h|"[ݹMy*7Mh.I$]9c<ᤃXt[ ƻ{ڂ;'=OR'K+3,(؉p!M7 oɈT/3ϭ +y7$*씫/]^=OnLt?ggI͇(Nrw (.oē? zr / 1zi x0'=zDAv5yzr*J > \XYBNAѳ1WuR)DLeC-3u0U|!;LѩJT4߀ur7GGݨf  rY4ːɂl&+ڣ5DC!ʊX6g[?Tb5g6Ygı”9)58ۨ;\1S@I{Hն4PBrCaĞ?]S 'Cx/W['R:a2qɄ"5`I+hr'!gΘV1s,!,(NG,n{+zN:PdR.,AIc?յm+Jif$石g^jz n%me6|r[3~eBԅɽtb-a,'"}O"Tt|?c^gx58A(4xi= L;OO=c"ȵ:Qd~98$Z*_d,^'1 Ĭ 5k0ogFTzBk}GS!m6筺 ƞJtS _ˍZـ5}ÝwJ1tvu)]2 OxO r~)k[f"iYX;@V&@nf& -6Eu/˩W\3S7^t P!YNj" Կ޾K%+Hh<k3*J˺ :ĉHr^"&;z.Pgb2W[Vve@>ɌgKM#n:ZÓʨm]FVaqC.{8ȤV<5̂@TvP "'l]F[ C3͜ż)wȭ;$I0 km#Z(191k43oQ>Zq3k% ] HVjfa|*ӷi+ hںnγYGS)BRV[[мswNM?025֔Lhc;ۡs@,50렒$:}|LʡsHWKK+*?!oQA T"Rqʼ^OչQC+t0 ._r 輋OMA9הIZkyF-^&y<@o)j*e&  K/m?X8”ʙa@j9^ LGH08MGygH(,+.RN`KxxQ雼]xԆY$m|$eGLU`H4"dCY{35kr>_+9 5Q/[4͖NА8ģ XT P" ϺZy҅3&'7sfC[ _U~ 2gzx`J\R>F8 4֘-]HrDO)Vp.s֛RkW9!}n`N~i\%ɏzb> ]|v<;HH'?Ew7c7@`FˇǗ/_z}}&}0>]>|ٓǏW.ϹBCR*=uĽ %^<"Aq;Jt 0lܹb@-r0|te87"rX' MCR2x:*,ġl52Tӳ'W tt(W`~p0W?6YES?IV%W/Q6˳Q⑊H$fBF֑jTDELu6{PFL4]XꈧaȩV?#&onl#<0^[_ԡpGRأEG1^UvƬLgTmBXl1G&,ix ,ғ2)y !my,4LѠ 6kwPzsdJLއ:Ze8Ž e(9͗|Y#FV6sLҐ׮ 3`hBuwb%kyXO:Ydxg8BR)hny5}C 0pƴ9cI}):8E?` .Ctxqv9b7c[0L{7W^y ^ _}ٓ}@|W_}gl\0 ,ICHu^}`IE<ٚrXS(O5l1PP4-':% 0gT>ȠLYKYzkQeŒV+EK\1'̑#ʄeEi 8Nfcs,HK-~mL;rh5vG A;DуZ\aD~# vnmZnS䉇`(F^Ώ(RUi8meL/J OӊvLvlga CtjGö*BCt-j%F=o ;?wƝaj•1hefA(9RQ$Dj_;6 M !taEmNaՄEZÆoL:` >G=jbzF@X ,Si#OCC͟6uy,&9o*K.VFc 28<{L܋Á1p[ .5ng)_W -p['QJH܏%Ǻ觇y4+yPBd*"^IJL̶CQt^3,8d[Ч$몜hZ`ΙX?$ϗqwex+55j+-f9y?UiXATf`ڨ)>_60Er'p%=, F߽}U57Y܌k ΅[5|epM+b"/V~Xś|}s>}p~y <v~umC!O$k nE/]\+{Q;pL2xq%xm}SSRgU!%KH,c*,<m]80h%V90juk1[M}td`V@Cmzgx1` sWȻvUFr25uCysWr~)`Դ1mLW[skɼa'/ii*8zEkf@k=FyVf]\DU6$J!ɬ.f#,ᰃz?Ѽ')4h@P "7 U ɦWsVꊟJ$lLny J%$BȘ0jf<>^ Ľ"a3p`x}1{,D5E ?m[8?4u4cɉ%}I&ƙLr: s:|k؏^՛^|}h SG\^2'\I. |c!Yans~7ory4_޿ᓯ"xcēG~_} ClE.%\^#L^5Um#^oYy݇g0,ΫX0Hb^%j6,"$ &ftH{-I][p]1* DE\(Yn2nO:fBJ(@cB=pE']IkBm` MHK\ źjַ=*JXR_LeIw7T-J/{9+p%6R8nzpߠe%$S]ɫN`粐$\ɛDͩplCpw~||?^~ËoO/gfNpqy_p? byq+r8?)^ydY:?/h7o޼خ>{qzA,+ݏuxLBPRy>DM4wOo]~ͻc,#DVk I77WnP?\<Aim 5 73! h%-,r{FʐNG /t!5,׃Q4`jEܬГ(1cr?{w'pρυ8@ZQ-*buS''tɌfyWӡ :muTZƸX-[,UIM(L*Sˮri,`?Я"yXn bzM5[(~w$#O/<$zõPKqIzɝKD90 BsܒWL,}vLiabY6a#=a\icmaƁr".v=/sXPEvY^Knq$+Y؉>E`L &KN3)N2P6wCpO5$B]mϝ(n$Xd]4h"-hFaDG'`]Q*7@5G5 d\nbR"`i lԵUQyH~fR=q* a;Yw1D큑4?&\Qgn=l iktK0bJa ;>QnS$ /~Lf@:*):їp"R$pJ#{\ 7e!g2ҼZ]4n6$+%RyTڹZ#1jXD8ƕK2XZociTu hvĕ 2F1'}}ٱ̌cAsKU-v -ȏ@Zim4IMtBayR[iEʙJ(fĨA_ℾp@,KJ)@Z8߿ez7~͛w9|7*<{&^]s?|ٓrO.3[]=;"3y~$<}=W/cObA7_Wo>8{tyXՈu{1վyNsR+qu{QnKS%ncI7x#J*nH*12#c+qkȜX8jx<'qcK|@~֦lf2ihszRJy #dU!^r AqkHo0bp_LTHuFrJn&"qlX*C+^4C8:e}F "Ph&6:Eyv1e J2.IQ}G[rdV'%׺P0[n:Pɕ*MwV)/,³]谩^)hӊ gvs׷v `F:\X WQ\ 8b}k aSE5b@ UQFNϙ~ޖRU>ѨFi~i`Oj9!0ͽVPR xzHQ9 LEH>t'ͬM(8aفI*|zV8OY6qO`2n$56צò]o4[q ~tqv(0*-"2^5$8v֎ns r (XaR |W mcژ:@-^ /'olΥYʮeZ)0I8Ko3*Ko0ag ra |n*gVR^Dĭy[ʙY8F(MP,"p%$VEsZQC<ĥ6V0yGEt?޿}.n]x /^7p}v{{y,f[ 6x=ī>'_vGxbEld,0ˇE~{ gܵCL> bxDl,YyVZŃ /߽z^t݇{O\<%Xڈ9J"|̟A7^6-Dw?yœ'O9{}9WZĊǛwWWw^}& 훷|FH[1os6kϩ W)@@dFp8o_Ug,*dzW9@8lA'}XYXC9#~1WGe6Cqx6Z+ù)ٿ&ØdOXrFNku [ 5$\S45qj gP\1(!|]c Yq6c!VMآb]+@x34LR*;|hikq¢m"# lA cN/Q%+B;E16j(f_0u M"Y2gpga,nא1;U me*Tj2ӑh}-;T!zNIfN%+A[Q%!ȭUG(0Y-ߧV!K9"pHFALȆ)VoB5:XzH"e?'FcFR") iZN@y+ 0ߑ&o޽ `wo>{勫7o>ͽM"3OUȷ?͆œ޽G6<<;g{;7SXvl1Jnh*V_⮅~|W \Ggri<6 y pύk&[#jŋ'tW 6{)޼iޅ?{]<Ň^<ǽaq6M#!޽~՛Wz}4'ⶍ\Ans>C.Ģ1[N(QY< 9ˑ!@.(:X cA1WjVsvO)}*D.qχFtbS B$qy  XRD02H~U fm*Ƹ6ZfOYpԠd&BUPD,St?77 2 UD Vn 8sejYLVNJؾbްǪKeaOWzP:'D09^Mw\-@|ԑ3-%0J&'tPNvY?i+ioj# 6iz1;_lUg\Rd]`B~Di+jѭO|TG%d;SfWt5%!c-]H(z, 5j ZAH4Km<l0K}?=~?x|dOޅ=,0_+j{ɹ͕خٽI:raѐ&6ٶ$01y S&ņj̚"\lfؽKFO v}]%:̳V~;U+kC~jsD[3!lȵz˚+^WJ @ w,3#(u !ż\=\ξ`!͋O 1eʰu2_B vA/@C^qߞ( @DḌP WW?>e^\]")w\݋Q\Q+w6,x?)y2hxgϞ<}G0.XJ~q = 񔆘Ǯ{;2%=o铏泧DZ!#ba>!1ó"뙛h.6eOBPm|xqGG=Ĵ"wa侃[<1qw/^o޿ /=U X-M\H~& Xhcxmx\t΂|!֜:H܁XvWcD> v>J<O܄!lr4yi,{c 7*B9ɉ ?H QzpRQFKR~(Xب^G'Ifd6U][f[DUxX݂گ|xPSE;PVM:W ʝd7Sj5JvvӊT{C(e[w廒-9~qe[i$CCZ,q; nKŵ/=b1l^@ u³S[a0%i ^KbH:9p,-A^]+$G0BkOg'&[nTW6!Ic6[P6ӻڦb h{O,|+4C%j8i.ehZO&QLF?K:&b$KcV!X=mAMR8Mz>.Xw?\>~rѽxA<篞x.䦚cqŃGB3O>_?z  gOb#n<4")q~D<-X^y.p_~Y &S#cq$o(rI"pw^q|y凛}LsqF4r /#@A>H\c\Z"W3ueK+s9W"8CVlNJ^Jt/*HY .RӲ%y[pT8}K0Aԅ>F;G*%6hOQ@DIn١)Sg(Uq=虅q{}5 _CrA|*!Vᶭ6O:4.LQ1DBw8øc(T~0"FV4~reV}m0 TTU3$3 E.a"2IGcUS?/ !Y1FU4d]@륤 ޾EVѩ/ɕ&+q5%G4s`^ןYaU#<;HDżpZ ᄧ/&3~[Z"*÷%K0GBB)PcQݸoweyTݨBנdy)>Yx`PJ@$ pZ. //\7tcDʏlJI!uF7GsHh0*u n,U[ʢk5#w⿑|*&3Z&おjl5z $2HLAnXAccJ2 G@ikmζq9AƈI١n}0VRpFX샩 V$ӛE%1kX,Vq0iV6w_>\]=x30M,4C%{ggc6A 1 g-"nca<9Q<՛w=~ŗ_$cUp?X| ~],YkPZ2㳸&8b J 82z9h'5Nɀď1aKyR|(q_~b0L?lPߗK AI+"6dIJ|@'K58JU1Y8Il$fӛn D W}&s0_0 T+ܢ`BmN3 Qc bi|"vs ͸4o j=F)A.UH'2ZT+eeIKa1Σl[2BIQ`OϜj |hqVD};z@_*+BG2CSv@>qhZnm 3sOQk1X&WQ\J=wUMUx艠3¨26ox" 3͘|F,$-TQ'u R y KU^#FoqpO^xm/>~wg@,1s&U cbC{?zx|wwK\> ͢t9;D VErW\\xld~xp(nYܼ}>lb="by">{x滐8oo9թ|m4$ww[rWIc'Ľ"bD;{VLY5E9[A=MOSE1i ucOšNNno=BZESv6[*&4*\vSI-p۹{ߜBeUiˁ! 8L0IqAӁ]V~ѐ+֤ (o-MoK FtC|YcwXcUWꚀIJZqJڞJ!͔Dm*c[l.ZbPmF0E+*P $O52]! -}u棎K:cLIV|κ' NM&iVpbJm31Tr^BPc)hP+)bZ nPbXocAUMim5)OۓHQvka HnG _90$DjU<8Դgq-;x|ycLL+ȃ3ȸ{iRU8,7ZI6*;=ɮceV*dC̀Kf@?1T0?\}tQH9\cOnLamg 8G`UfbqXXqZ SYnx`Xe[eN&ذ3CٸcVyrYq 3)PNJ gv и57S;jf0s6젼7;phJ4Ȧ| Y҅%l~=H$E0X85Zix՜lpwƙF\?T@Ͱ^!tE J,+3yO5a &<^?b)FIjx"hq0ox{wBKJ,jE"2ㇷs_xK2S$mLxIn\Q޾~&b}oxB2f S\GWTl:7){yUȇ7c)!` 3= !^(;c!W׿+جM9[僸7#^p?_o߽WoO,0ܝk!b@HHj-n՛أ|6@~_gOIͻ|xv*^y],J᷿_>'4ē Ex V`< |Ã7Aă?x_Fܨa&D[e=ywT<{1wnb"/p7 I=vī/nrY :o8<ny]ӭ|7Ϥå=P_2j1N yKfrYR Sa+&;cH.7?Jyop*7L~ٮ1s"02]Aah5IƲq!\-/I4xE:n"Wւ";qX–K,NhG~A@iz34pq!-zHe)))bK 9"!lz1r%sWɖESٺp/1h u.'$XP6&`$22VXkŚ2R*$"41ƴ`$Jd(,$yyݕl!jJ"T8^@gCaH#Jz.AI$(t3?_s߽|uǘ~WE$:fq=>,"?~ѣTlU["6ؼ {6ܝ?| O?bILWbc^/fﯿ/㻷cBY<_~79{{8GO?;|ͻ븍"f(KO~gC#\ĂH6]!jl,/e !_zË}?w}ޅ Lj6|_f>!aBŋ6.aQ,BĒȻo߽O^>CxEYױ$31޽7e}uܶYf@:Äb )NEϴ"I]~"\"C*^1c!0[=Qk^Ji14yf;hR #qD>'!@@zLFacs mHOɩ >n%9t^P&QD-"R~b؉* Œ594;w__#POq5-]̭J17=*{ o8|*Q+{ǝg[rc-uڴot :2i4rŪISUWt+bO(ť0k0DZe`&"Ȑ25.sxn̵rEҌzlx.*t[c3Ic6%vxU9;<w$+)"w42ӑ #fqh'84rW zyeXZ}s̼6 m;{ ̡\1nd XcR7vb[@ItvS˧6l5l_\U"ԣs%u)Wl9/*(KPyM;ɨyWi'Z, !TV B7ºD$"#|rpl 8c~ZԛcOkJ0N^ao͛:.Ju[^?E&45ZϘSHp@_>NU{w@!V 1}O ?OlU)15nj@z5 j? Ȝq_~곧Ͼb q ?5IT\8=~\ܽz&V/~)=O?ⷿÿcO/b*i\?@>՗7^|YU2oM!bEs!74$> hz4ػ*-5t)6}ж*clp;{  e2,Rm1  0H& j*ޘgGiN o?;j}naVzMwe,eNOfjPςL=>ll:]bq=*ԏQWwvd5QM({[B:w,wS+! eL@{\fcs{&eљBT"1sSg$V/Gׅ-M})IC95lcEGBQRWJ;HIVd$p ڃ0PtYE&,i E|8WA(ضMI7Vj=h2M4GijD<9W8Ѭ!'0VktK@buJQ .&9GD$.1 ܀S. _3iyonNzDS#!HD-$GY.yݟ޼񪅸9"f۱ V5 qM<&y? n.o~ŭ㝑9-6M y ?E ,'ޘC'w?npwx2Gy?E̫saA/i뫿ozut_|_~_ܝ+onߘZ>zw_wnxwxDĶL,FxD< ~aOoOn_={~i<JUɓxfnIܘAq2# oܽ7/^//{(n{=vF2P+4K#7"r="$omy>b+I 씫zMWk1 Ǎ$MDΰ3H܋?`ET bDxPC2rhBك20ntD[m{;N,ڍF)ێNWo10^[n&uʠ@&/[0*ϸ IJ]V&P *t;OS"X[awgm//LS6T$(Qq%VԷ/TVjE z&ΫO8{ 6Dܟ8D]MMm=~ pT(-Kf;lbsOq<6rTo9'h\ Jo]$ɲnSCH&`u_XgE)abCv K>z/ xoeNlYv Uv d ՎqL)'{ E7fo(9R+D:=W?+qwCnX+A*?li=\\ޜ]݋?q+AϏ<.m C\.? R|.Yu^|6:yϞ=?$ة;E+?|w?~_Ͽ*$\z #ϟ>_7!?yW_}ٳg܃]_ŭOPB4?w_/=٣#x/HFBT= o'6ģ-./yw?}ocDV{a4J<2m ?+2hn!Vn?~ &N`& /\U kt =\mSʛV554SWBH;;g[$$c !qsˈNDGפ"r(DI eCp`m%ԀXMl4r6Z=OH 9ؖYz(S`Bb\݊V !:vx/TɘSH%rũĐ2OhӸj3wKh@QzX11_(gS#~̐W}lМ\c}Ѣ)֔}A0 mhV[ettБjJNz(N200xJ2K UaXԲnBa:)3V4+- |}L[P皫j-;lQaz\%a+WLOF^a%zM2l(9lڂ/;򩁓~u,4ą lك?x???G_< }xwS?[ r$e<8D~oۻ_?/_<,vjC]=|W_>~0&ǃG9Ս;S//?WG>]!g>36rjʇwsكǷ.b/?{Nj^o?|Ǜ"[/ngOJG+zpww*b(\bNl|ȟq+KeXGGS--p.qL/)xdv+)H*a^iF׆qE tDwd${c0k[=($6M#npv[ ؐhB0ԫz4'jS  Z1ךg OI 4D=UoR+Iq U,mZ-4 %|zdwE}ÆdbF\\LW^evT}>l-:Pdlk#nahP@dBN!SC$Ԋl=Y<О2Z qd6k.χݻ9dxXُ|y)^ 7oǃK^~x6Oym<2/3"q~+ x@ċ!&1bU (뻫7^n@[!c'籘N|C_ȇǓ~z6,}1% XFOU?_?//=G*fxc,xOu(b~o/?,<_w}'W_/ɓIد4.~~ۏ/>|/]]- gb%vj\X /ų,I^|.]^|w!7y>z1€h# ;k,y$쎕#5bG"ȧQBbGo!"0o<Ֆ4#ȒM+zAWE{#c+HB:)؆#"`rVE SFNP$5S4 hѓX?vSkJljd( PRתhUwɏk*%N,m:"2{Ƽ=hv؝Nظ:xDxՇҴPhؒ1*[/+nݘ Qc?&wP#tFx_ICcR-AIc3p,TF4((pYa" UKXR lKVV[I9Yqr iĨ.9 %*^r6!EPD5+6~E(-')jmv Kiɰ@2+<4/8N6I4o/U\lWNtҔHuAc4oY{G Cň2j*2Ig[e ŚEE{ِ@H9-*f ]eS6?PEQF1jַI Y \B4Jyd*Ur#nUaR球rx#+HFаEտ\)I 5ͣn>RO.|m=(UgUmRGDVIGܻ9` `DpQBVbPN#zgQ"y3sbl9&'9M\wl&!GÆBvE <.ϕ2|쾷(*}_)quXzUG~h?2Z;zcx{>Nk](4ň>;0q5E/^}q^ՌV:"3QXE҅J1~mOjDe߽Yq7$TcJ*HХi);,mpCdD9(UPv5KB#uBlqպb`3 5UZ2f gʯ6cʁs\U ,P. b:HUX3t*[;QTHTv14j 6fcr#5+>2hP>AHJFF%"H*fz-4^!}jh_Z j$*N;ZzLtt Y&7X&y܆4On`egf;r/78䩩1A扸u}x{e\>'?N'|`~3 ?"_ċ#.}.np?w/r+CLkx2I)݋q>b="ȝxJaRc~zG~A<2'{(_y׿}ի~UW_Œu,jDqA?>ˇ_s3\E{vxhLf{24g5N *W%G ]Y|XnM!-eΔOq UfI#

    .>ld*¬̀?"+1Kf<>ʦƫt:!3: ;r*>^lmzhls .RD:a2vfVpsϼQmDRYlhAyb'YLb).ޗ{; 'FZR~`@t:^rbo.Y ٗ%? 6={}۷:C$$R!2//R0!  Q ],` xvxzǴ~̪9osMUVVV T2M/U}=(md(3{Tvd೙*令H,j)gq!+)dVtLDE[ J[1qP$6GƔ"q&ٔEt/TdDI|_T|VP30˳NEwyyua$莔 |q3Xigҋ 5 } ,d΂MdN*^x B4݅-uje둨⺶زr:`F0'4*#[|JN, DXpR[;xRUan| STbȶwY}k10% cG#`xdlxxЈϥފ1*_9PR?;GPR u;lnm,lo l*Ÿ~;sC㣕Z g(<:j&"[>hѐCY`֤hP\ Oqn!ӊokq(e+n1!e ۪SJ%FNC'fc򷕪˝%dxINg }٠pEُ)KPe3NqUUn)A|PPئ9M]I9MB-cgG9͏ dpF; 1eY|. 4@x9^"!A1ntQoP w(a|3m%AͨQmrt;WN'WXK k g0_:.(U;OI`i 1Eiaŏr11J7,1 )n0x稳H(nHRu!d,JGс#Gb9N2$vytHx'm% ."R/*Rjz/y?>:M~ A_fpt oΰ2v.̐L2$:qa)g&1]]4Hʋ_U2&H)An$S/''H%Qgs>úS48a-Xdsn` LE 85![o2T>8Rk#Kg5 H1z*yy `o X7,0 )^)whl=e&)]ǔҿ6a,~?$_.{FQʎP]bq̄0 IP$On^رb쉠df֧cԉhsB JcaEr?2ش“վQ(ELpyЄvaյ/67elH ކcFx@/ډ(f)/ `ד<< Z[:xؽ*ՑpX=D?*Gl58SA 7v(v%a [[y䔔,}8Q)W8 C'\SrBJkPF|8V^>om脞,ã3WG'[;;͓PmphGHQk\i# '&sHb _ϠժUfhg!D9_h%+4Z+ϗV׷vwvq [wn޽;61U 1 h!|3r_P@(dulm@] ]"B>Ƕ`\-g8ڂqD4D xE"RhDІ:):UYIb@߮ML3ЊtMdldwA(#1I@//4;y-aIlpΛ IOj΀ԁ3ja]S,-0ǯ&tdji9uDfe70>v $Tl"Bӏ8Okv~#$Cc%BOyteZtbN$0d\"{eȫ?kFt||W @ 0C.564ښx#oY/Crh0LGx*ʈ֔^ FeDŽ"xwNjIDyUr+R4~<:x-K,H(o~/fYBְӡ=FKl]WQ{qBT5H"lˆ6A \zANw`.ewv@ȱ17[]:' i G)1, wˆ).Rb.]]=uEtH}ΊK6РIUg?c&"̭L#Ao#Kק+)y$\c2vbHi։jМ~1-?o>AiH"yN hYɌ@\#fJLFB½"q: @Q949q: T/] AQ&M]18Cgx KpQi_qqRzنsh<̓c%lR%f:ĝu!wx(dGfO܎|AIDATj YCyIZFۅ9tmmrs9 AwFj  YiOXcR. dҤO8C ,zo$V.fHQ xp$qbE/'Gǰd5))oЄͮsH%%e|ŧ:Qŀ&~v6ٰ.3آG1ce8GGGϖW_OjZ?,t%F=lȾ6R* Vq4EI(& )1n$uD  织:,B_::pmʕ!ت0{O< #CC##\E#8j4[7}&!p۬ \ KDKի׮VOȠ€!{g(̀ T+׆+x^RLM!Cvw1W-WF9!'4G͓".Dh4v;Ս 3Ǧ_dnq:<,'L6LQx`eNNMRE<@"0'23A ?YQR{ܟ+10611633 &x|X ^Q2BK3q$H]K7N\dZ\aخ~`_7⢰ GLaPهlOy$g!<'w$E1d|X= ^\5Pr`>bLB2.é\2 }dqD7J,ǵJJdyšl`8erEm"]F#Y< mU $mŎ@UMU+9;ƜH;<(S 1%=HFzCRʙn ]h;3: c/h^e*b"2!L^nΌ-&ĕ`sFlOJݮKzV"ttaHỉ 3 2~#z]7HDDZcϝEtdB;i#w'd5+^ʒ &:# oK"yfh;Oʔw̼.Ţ~iQacofckǸv&ҠD5>>iוªdlRΘF;ʀG;J< eyv7̔[DًB!"7%WBt/(B5jv aWtߕU]FU$aﴨSMiÈI/$Xa! ֍^պbN/I[C~þqxt^8h-hO#Cc8>={&|A r4_VGF&F*%D 38Pu^:>d5/yZo {BehdjjydgrHخolmmrm$8@^b!qRel-| /lsQGDxӭRoajzu1Ϛ0%A-IaAyԪoPobec 4Ffgqr^<<;lQյuCtF 0k^Y+nLf gF% 9#4$ccJNG$L vb$Vh Ax|NmJNBR'C+?Mb.b^Wu6 6 uyx']rNV\,6>΁@/(Uerd[A2"7$PB^7 YH:АCHK'F8$I}RHAD\[y>Ô<[k%zO4Q!"H)2'_F&C] }4QAq8:D,(ڬ)@0/ZXf [wMe Aq8eVL[bg ącB_6Z{)ws+WN v-pN2d$ CWHY'Z6B+5!:Q($bw,q~c|;Jȯu͈ȏhjd3! Ȗ/VeGu@ R(gq/؄j_$sA~Ǧwh$'zpȁ9D`GE;WׁU PR@ttD>:;iۅ= (pl[s()C0d_  PaT+Tf2,NFJ@B$W;<=8lc{po׏v\X^^@ <S W4@>lX;MD4eHP=jQ:bx FdžFєrqX^>>܇fBrzs?4`1:_km56w^mm!{C=z[޺9>3]8;n oXѳ/3#!"|"+Aꛀsⴉ"N0PPs(h[!B\-1B 2bET%E/ghn ;@J_\1ZO;@4E6NCV,o.W,߉zu@W((肛 CAxvLpvB`P"~Ɍ !K6B|>^XxUbpܧg_93#L~C3} k".!,g&CzTn!ڵ LMcO$)9D4G1#=6pEb2>d~j LǑ2uqh>MÐvI֪Z`^$S2ej蠹CDx0UQ <>hR lB: m8eʪQNE`v*PLtZdؗM P+ع$Sg^tȣ(|ܓ"1R;ydF6FKc2Mx"|zqF M>Oc0#2;X?;ĖcaE(+NR򿝛6&#I1ہ q]xO=4`OQOU堮ZGx89AuXd?lXwQO nyciHγ20GZl:T>`{lK[)EvLT: 'Krzw6^\L"$y搽i1uNƭb/,sʁG5GsA99= ?UՆLDZҁqN$*((+q _\3Y&I:LH^FpAQ[n'"N1dAJy,i-*_X&7S)xg$f(b1CHl&''ҹ@ۿ(z$-ȩXw㾧'KssR66ɁbspV4 NS3lՐuu6H:꛻/?}Q+* b†-VDA00XC% /#61hQJ$ıGMDKCb?7–7NTPrέ7`hr0M+X:!)/T pGGkՑb!/KuS$_'Q}*t(-9pZat+W3k/[(oٔ xP+++G!g4}8k''g+/V66QC JA흽n}y,61p;]y*#< Vp3Dxš1E`c8b% ./7sm=av%Π;x|quٙYB]D҃1rBƩ/iH~IIH 14&&RUn7:/ ŕ"g\_\`D6~&:8#q%.f/]35A&wT שkm:>d^1\y$ւ1h {{hSQ|/} TS/# ѥn3XN/ \<N1 Ub$l41@RKn4h h0G~1`.7{Կҕ Yҝ̍Rur;d=gIi4ۊd3%m&'Ll@bUQ ,s7po\;jݷ'J+AN3-i#_WڢmcE/F3_`@"@JTO"PE %SH3Te.L6ptp=u %D oKgt8Lr{8/ F/leq:&aIE\t6u/q}-o߾I><<m/s)dƷҊ`B`eƽ[9򧏞ڛؐW&/ 27 u[wff&8#:5ɥ͏?}!Ǻ9% D]))BKCI/tVTD$*7H/6S# ͯ&_ R } jy \+;EK1ÞS3n+$ e!3 š;PtyCux-oa9D!8H:vR>.8k너4: AO PNh]ZS;L$ZKƢp_$'E>u|J}bB>AG a[n.0R -Ch(Po2 v#Pgt`4GH)JM'5I|BxC+K2p:lm~tikXHRED4D%I4}œBudm~tvW{s *a#I"B$:`*A9H 9DN>FB"Pࢆ+#0jIJ?:i5(}'}xTbP)]oGGOo4TzkS󋣓sվG/^|  [W*| ;׶n}kRj#0Z贝;h"C#Z@,H "RJFXmpd`t|驡 z=ŨbprB20d:QBR*Uj8ӲуJ 0sgI΁S԰H@pGCN ηs81[sӠDL `A0&jQH\ P#rBDgY9@npF+ ﲘ?.t͝KY^⸧9 `ph \- 2D,p-քks^mR){B( vr~w%Jy`ud0_X{2ʤiH[KvF Ad+Z;Գ *ϯ9(~ENr+i_iA*u_ܝPh=U!6}#:4)6&< VXAd`Nk/o3ԕB.zV[">M"Bmn"y)h)mL!eb%\l$)Wg 0E\MUn92Y8(Y#BxYЧ$vҋϙm[(!#&rauH*jZDwyVEp/\͐`f1d{n=suf4sGgҩQ`\ LD4|1Ai4ލ/f-pRfm_D_qςr20boDO޷'''`Jp$`w &GKeD[,wk+qL>P63@g"C>md.;{(N靈ф1$JѨYͰgϖ2: \;1tׯNqA(^v7n.SN1О>}IEĘ6CA ?$]J!}}P>|!𥜙 Һ*vvC9s͕8I$jǵA:9N+mʬsЪ^[[OGOҕMa)$8$LJ016Sq|D91#?0ph{6\)68@@!B y], ]a9zr%%T(CNh._`M9c.R>ۚ2J@Bv γ2KN,esa.H^apxf#ï[ QqKYRlQ bV+=\Dﭭ!d7ZgWIbEpb^,I\}Qy쬅(#yvZ)pO?90 }IS^RukW'm)(!Q)A d0uYԵɀr)GGFa=;ikc& 2GѨ|up:ZAKD"jEbQ \> Vԯ.-{9QP*sxbʵs˕%_F@c0~b]{o^US=Ҏq_GWsa1 ]fI?&.ډ!5 B'ͽ &eTgL]"!@oՙ;~ƯrMwztR.w_J>CAXz HP@ %"0uJ U.4B 628{LH@N: y%c1ڠX*-H]t`3֪էAY-LPIUmJo-.Kę]3|],.@Ռf-Ɓd)Z@o6Qcavfnf_$OJ9PGfjM I `p$DPbo/DA/yKڥm ˚H4D Q")+j<oh(!PA#w6&mؔD #B2^qFmkZlD5a*YVㅀGrY`[E|C4u9b/ä(H{$5|' F ﹀U(q08CKvhiҹ@X|j?rv D֌ݡ x Xt#Mg0=; v]ކL IQQmD. yQ((Eĸ}% iġ҄ UnW'&b䮈'|\:eb'/Ll 6<ϑ)CwA T:4 4HT0tLC6y0rOm2mGҡ= ݴooҕfFrI{7yC +UGhP_f2DE\Xl'afRG As=qI%%6iji"&4,o>/mBE!ӎ!gҖ='FIJx* Yp$EL]1]f)p]p5X\f^0!<4j: s'btlT Hx&j*U^<3; "/l%z~p**lB15Ӻ fB\ z3(9%*L9d=%NK"CaF1RE? ]O`(T6(™g}&Yq鋙6dw{/.(HΌ7茐bRMRn蔝p7Pu8d"^G$TH ^S.t 3!8c4lHAzh&^jSoLs\8M)roa[sS3c(Q8mn6Gp% SH(FRA0XAɤZ"6rg*pڏ*@F̒$T<ٕ]ᓍK\9>mN $~Oca9.afCc>Z?wȟ5 E>F26nORx}R/N!oWsW2C9);;-d;\ml%|U?acQ<׍ЈpQ[\0TdNPlP⟸}"mN&+qzDO:]CUpzAՅ֒31PF5[9a8.NE.?!A\:'1~M~:fJ\YaPA{%cFɣSU^ܦKwdk$(R·q.f\ϴEnXUֿ_7۪œ,?c*Mmv!TF[UtǸo{ͻPCDEʦhf_z CәU)1_.8iq/ y6U{>O)'  kLT  A648Qj5H&3"m4R̼DQ{ ''F_ggg^LI?CC_xdP=]4bheTI s;HA_˾2)鎙) bэ(cD'2plhЂ2z *4s`ɩ@:*\Q³ Jgo71Z ^mP^s5Q}P_d>_AIpY֩s-=PI8|']DI֕VMEہWJT_ /_N|E;Y㛍c{G4,9M ־RB2{ːTd*Si#un<\kj>jR t*Nr,9!!pz ضƆ|V;6y m2eY`~!Ne% O4#Ne4-$RBRLתH Yt2466OpE I}Y'K v7ky͛7nBjJ{!sg`vw;nPACn61fd!ls9сGfKDЀY`9qm6[;RؗU0bd~D@806<#ׯ}so߻{wtd!T\Y[?HU<b#Cr`P("fRh-0Q|rURAM{xE!1#%9ߊurTŸC(W2"oh%Љ55%DT VU‘?7|nųյ !6}l7<.1>PQet53Eˮή'-J[HIr%F !piLEf, bXq䢕{0L梭IzMG(Oti<@s  D|ʂ؇o<$bZ1Ŗ%qd1Fq_Î8)pQT(.c5ە +vEI֎Fl#:WVALC068w_;0J֧@@&葉p+k/pD_NK>e ʥ|RԐm 䴜>;Ѧl|-x:wLU^8W WNpv#'Zc$Zü%[qv6#a sY.;xq E-x:RS jJ3HҩPMFM+H(=?@IM3v6A܎:N8]7ڹt@8d|I{@1ou٦Vk|}jLhm4vxCBv׆%"tyciq3 gM0 AqTqO_ 0XUFb(mlm##p4HnNDidxISǦj<4T:zA-7SJdrJ hiTzF:0+*lD֓LtuV/4ŪΛcC 9y /JS#J CS^',R#I97_iw2^^2{}i'h#7fxPLp%jQ עگA\Hrsaˋo&:>ȀAFK e6(^O>f1A'L%,Cm"+4C$ )Pk2a=Z$˰IIz==0 (M*i [g[:D@pʐp4|_I2=Xu{pLXQ0 @:\{X( f ms% DD="$%qO}ԡ( Iq0:b$0xOOjLLsQ*M0sU& QcA*A@ttj T3ӳs@jjKYlqhxcme{}u >b)|ʼndREOג2 D B ySXzA&ZšdD@#G8ptP(#C G'|+W`}{cyuy{[d zFj$a1 -R  rB{ã'z!dth)j}x:580S,U*z%_3:\MM7QzG+"DIA(,?XJ A P)R@wcs ;{>Rxm[c/N1-Q?ԹoY@Z}ȗy|Fd٢s{LK ?™w$_W &umbU}Y2>G%J!q1EF5(PN[|hK+7J<jAz&k\V#V@gDҎ#< ebڐm.J鴭DڑKV^.=p 1E V!iaJ>PI0d*I†Υ\u$Sc::6J;5uI0XGtrDHXz͢9@dPJDЗdQ}X@ӂTӕz8dh #I3Ӂ,]ef!`̞kv')ue[t( Q=Sjb5辷viiG2]@CyE111יh~mA_q8V}V:0|WE4m?ڤ \H%X߹6asj (ω֙]ЛK8~K\~4M[Iu2OR> f:y4dQάTI4о'93. (ˑ(T.5[p~c- e-Z;/e:9NRQK[a23_DGz2T4/ÀT 8! II`<0n0p=cJW5#%yCpq2OJ]`w4ji*mtrc8sBAO:S,+uQrePfƵ+\:`F`IoA7g1gVۃ, $Wqg 5%ѢG!q6QB2GT*%:qUIg:̀NB2_۪hYw3 sj+a$B5,4n:;* b_$)et Pbaq|# 9YYȶұX^΅RCq\x/NqWKl C9Hw]rL_MV[DjB$?)şrv:jφ4y21Vt"ėGYV2{%Y5I\N~l\AFP$abb<[uu20<<'u]O'!+ )tqkǾ2]9d*(ĉo\C͢nb5@9A  /(9B6aK7APM|l|d hD?}?BUؖ>. [x~b뱁 /o,//\^_Z^[z2 [:7C#1Ѩru g'Dٔa P+ccBNQF{Vw߳*Cb >n5]>REQ*MvW2TeqY<-K-.Juȕ-}zQU@ٺцc@¤+'8i71.{NLN5 6lMJ`pZJ #G*$8{6 FGXC PqEo'.MKTXVFO+Ś1T27ev?\R5sq x'hcj#}Q}G&b6(p6@ S?hH(Z&Řlf-^?8>:_Bb}m;`+FFܸ177mn9 I|P@DBp'YpK-4;Y{skSUܾ )T~BpϗY=2%6…P(la…W`#U c?'k[pOh_ʁC8進X)N 6}/7Y_UQsW]Y+Wq^ElC WuB?/JcQXC9ٗ^qw_[0%XHSBnxx;?"i0*t*:N /$~pϛ(z;(s}a(n9붥aJz[Τ}2k5Q#M5-gi Mwix:*%mHG(+O0J*™!q\S&ȱjD (>la M@Rh$[$!uaa0t#_Y G s63#2DDڡg|h0H5vwLzB>w9u{4^ + k7%em8lGw4<&ԧSg?ek!:dIb^:)x4u Qw]BP/h8VwvF3+7yT1znQac{kT Z uƢ ד!qz%!NO!K(&WQX|P {￿I7/^|s<[z|./wvx(ZJ񻐃xKksw ȉ3+Ex_dSf͸dKC6C"uQ Wz ;# (ԁ" P/(d ߌ#j$[V[2L$tWzFTT)V4R]q,${}REWBo1W4Dw?kfGzÕ 8nzV2|9ENw6%j$GѰF+֕2 P%j<+L ;>4BB$ ׊' (w1iӪBHwzC C}{Yhr9n,LE9d+Qp j_f ai"ER$5@.9KR-0Ȱ|0Ȕ'0+v_i40qgSHvF;_o#Lkׇњ1@E\(ăŀk * p:ഽ3D\[s&5)h'FFGҏc `# A HxJL$`eL^}}u AT,Wgj͗k/~_9:llnnnU-bq|palh&&>9k'ͻ7QA@f\@ڹN};yxPx``pMr'/Ј£,s(2}-:$V*|w0<9 'j *k4ID~H\Ue""m<5kP=vv6wv7wg)ArR'5jH(q͕L@8,Vi U%"PQ`xjd0l2K[)w2r}!ߍGrngcR(x ;PvL]ҙz% ~oѱl(քrU֯^Ԁ`0&6D 5ci8(cW>,5J EX"#W ֐~5^H fUҜufSpǘ#Q.%4.ma+Ʊ.e!0''&8?CCW3cx:D{}=8 D|k4s$((cS6MéU=67eQu~ rD_Lۦ9o%!0lqс z$B>`~$t/u}cZnhNW- 'NrcؖML-HA%;+RfXY+>ǔKJ8w4G.?tǿ Xl@%H`$ I$I|4-W|G~{~{Zs|Hyes';^2`vfpgH*6P+qYRͦ8vNٔOWhCk)哇Ixgp@j2hR|[=bnz|hpP:Sg@P*D`bgPNנؠߩ-/mm(-~bv>|txP?=;;M/N O VJX.ojC#ޙCNIA)$)~?P~ 5\@kp^mT %L'"?T@ d8:[Hrc)6Q.^`ZA3Um!,i7[[[[;{P}pp{ />Hu ãU)';KRIvq>>][\@̦OŌ/0IwbYXւd趹Wt1qvΝRn{N G9qT QٖxZI=g0wB,ӮK00 ~3(]KQŐB',MIDK@,0DN!ș:d+a}e]|cfzILq~yѝ #hV/s%w%e! t'&} Żɢelo&PpV !9oJ؎ZSə!?/}^[75h‰3? 4^6Y7{5D_F-p^.D=;..18+.!"L6I!gk~rj ̟Fmͭm@wGF0ah/7yg^Mo͕JQgQec3%[)ɨ\)$ȋ}%cz$KڧビmZgU.)`(;+ݦ ^0usYe֎N gNQ.UnI=rG䪑wc IZF "H󸱷+J])Z)#_XJ}ԃEiIOL cuXy8>nlcBH)-f*X ADZcsuXkԁ=ɕvO_.n Ha|:;318<zG&]#{$S  >;&G@<9 FiO<|f}}|tso޻{ P!HE7>>x޺qU.T%JH$鋵TGjuxvzl|jtx^nD\W:eo=y`KcI ߝy}! R 501vvg3 W~O[71FTy̢\8PN kO +: F#̶G$-l XXP$ %v`~WDgH05C{ޭ7Qy*؄Hd !Aj$XEL.h&%;F!jD`r%9#>ϣ-ۜ@8N?k5R|JZU1,|Rs3PmI .zN{NV:9hK56GNd:gsFBI\###)̃BPggN&s>ك|Go)VQU$fL݄G Q$vՇ6^k.!Oy8ٳc+,L%bv G@ ưuLHpAi[V³ü\)ɸ$4Q`ֻ6lJ-˔PS\?\gd:o>?`d9"6V:z؅?.%F #8bjedIBU#Qѯd7 7;>擙gtѐeɺ'g˚۞܉rILXJU5jrK'/qOn XEv^ 2,}~KT̜#P )_JS$}.c0{`Ooa52mSޥ `".`kĒ[|/~3 CCYIa2A{5~WYW~*9ōs>s`Z2)Y p#g1=a80E\\y.O @'H)A' <\yիacɹv6ܹci|[f~%ABûW̽9r3j\e$-CԸ,-/덷ߜD8~&'&2?/_[2&lm!LE`aq2ydFT'B8?C|V7h4(x)pEr75.ff؀9>UPsA ]'Q0ׅ>K*cEjmلuQhX J>l BuvRb_ Ugz%g"`vJD&BE l#V}um{{Rb^'m~ooGBO p(ǧp6Τ@rV(²ܭ#C$ DEl RؒYQ'fИAC~xlwt g'a44; Y[Q#ڙoQgmH¬'ӐKL@2Gc5aԁ!IQl.5DRA0r4o}J, KŶ~Wv5vǷ PyO|"a/|x'·P9AHSi"ɟqG}=.Edgw{`G|k߻s}rrWqg<1!䟾ٹiĕt}ÿ @ɟKq^!<=IfZA2k} ojޭaƯ\;}7}"AΝy>4= |K~"b<̀>-}9w,/GAXo]҅yFOb˿|soB!.G"Hɴ 3=m"2x|f<P0U)*QLr !E [XJǣ` "9p$i,TNOL4%43(QFE *{ǍJWJT}b;yPrc$H/-)"X†C;hlop{ p͈U8U,%Vq FIs8\yDȡQN!e^N΀LwhJ]{ J¥qQ0QJ9'2( +)a^ nfd=m'džoܸvcb"RL$!|1/_"(*kNL刮${r֏H,DI(iq 82{iM7ai%Gl#5@OkW'OX*wjl4D99 9+pț)e::!r/sSCI* 7R5Q=.*v(,O?pb%-0`[a(q%Deǝ MѮ/sji; ]'Χ1;#6use [CN| onZz8/x0ജu&tFJ Wk3&'&MnSMC]TG,rE *D=[2Nhvk7}'IBF>2r,LtifhΈ 3mV&`?9D\<$;},}dkHқ2/m_v +s(1eNcϚ O}G8+{]Y{t )rԭ+Dzww؃{{v 2M;**z2Th8듩~t#l޼w SӣqWS] ;wo @WgxZrqs8rIMX?ٟe Jή0PVZ/kR $';o~S"Ե GH݌ԘA7WP3=;h}m2Ppf~~q2@#/Wd[R%r#s s=}TOaeDYDFW-}]X?C+sGbӢ K| I+ٷQQTpZO I5g$ՌQP9DEg3B,@^z;Fq-D`SCE=q9TF --+ONVR"#\_SI9t+b ?{&*# †:82:4:>0' Cs`C^`$@@ HNLƺpF&o)jkVUµ/Ase(@{In[Xƽ}W (5]?\fyjp~5hOtg:`7J=t]Df8܉S_!WdyrŻ_k0L+yBI33EPh58C-i:h@eyCήjR$ 83u:9Na[ Ͱdь8H,lrSM0[֯6kqd֊a4y!nf)'0' Gy:9YH8DwCŔLr2>ZڂS-m'55) 0ݼ l&eŢUK2 ;#}g?{t" :ClfDSX⪳ŕxCB6D+ Fn( L=\))Dp_Ͽu1u3Ac͠=$o0PzCK0"Wo\^QR֤ x[-ϟ/c*5M[ú]0PwO `YAQ|Rԟѓ~MNՊ<ćbO2 UR1s UMWVhaDD7]MM rЅu{Օ]hԘs*\4XGGMm#|AҲL88HYđY)n(7S## rm6TiH(|ިÓ) Kd|1^l> !CyGc[>:Q6G-m zIBC9 GFR6SqrF8GJW." hF-ak&󕥥!3:~P ;8P GD0f8@-*ֈI3 }5<>D^9$eP$lldZf}88>k/o7Ȃs+W¶Gt L06"<dkv?clXI?}g^OVtl/A1#o|zX"h]:.?8g16t] Ȯ_()G}H&U 㣗oG/h(h,`ccDҶvN#ר@:ןy"K}tiA;׷ĕKD{9v|M/#l}osO?998;g.C.`h!7v=y ӪҭH(.uUrQ@GX`R1z㖾gƷLip#EH]#O`o2e~F<|MQFN -ϧBⶊ#(p k/$Aj@A2hlAvd/7P/TA 9#r#ű&FEݯ4q8F˃3SeFZ Y vRA־ɩ8 y Ay~7rnn7`9a=Vݚ_q}qrrV+#>Fq65*+5MTh.c`%TUd#(*Ox푱Q<0Qi@hHCi2 l֐iY(a)L8^zGǑ1sptxAG s~d.J H'fk% ;k|gh< x} << ~R!e+*e uT#"H>q~T E0s@N"f E3GKϞl.89T\rmkoܘ9xKx8j!dp+7!^ Q|ZT|%}FrvH! oI\ *!ӂD'+rT}WAL8q|ۻ8#XU[Е#tb C3 /6+Ueʚ,4;YyI\M5)I\YΚ5,ngnY'H|5D=1'*E,)DfmP:rV@u8p1%jb:X% lL7'"E\xTcUMkw昙 x B[*w"q_ꔆNjV5@/C(ˑHMIfYs}k_|eRlzoaП&Coy ^9RlNdE7Юl+3 3{z%]rRG_\E3fO/ )HWM>qat>sq8$;Gj lO .ܸ,Xީ # mll "nt}~~qOc!H^ӳ>-ԋlkJےV/.ui%ҳcGAW  S`/ I Dw]!A.*;MBIeEy`b84O8"'7U*LrhA^ztĖ#zNLrkICV+bksg['׫q Vr cae,łdc0{ ݷWowa~K5|ed|7GƇdž+lO8NqcR1Ci_ƛ71<1 ~Տ<~:-0,GFXɓCCHrzwoܾ{ͻ޸zՉi}cٓ'?x'?}ry}{rh)Heg^" E줱bcmF)kFX $/yH׈C(8!&e$0 N^? |vk338 rHX䑙1787 +xXdPoNqBX'ُ&R@WxÏkkHoqfݫWn le 6j bf>/} 7n\=-plA_\ ׯuCN39dJQ!? ~<N@!$+Bwx FJ>vM$'~JHCJY0p{?yúT CrSirH ̴=XAϞs&f__UPpG kMf#l _dXia5r?5l*Y)ʵd>B!2ibq] 4¸\qsfK[;"' uKɾLy P4d4um-:D\rޤukKpzSՑXp0O$d}\&᪍DGkCz4v~i܀K_gP_.Df+ tI-hc c9{=%_@|(7 #;kPŇERi$.rdj>mT; ,ȖsD+gx6TEv ':Q_VZ#-.o=MŃ6YZqN PsJ dNLZS :p\]F%x ߅zؒ% G'^'޷u$dּ ̏ VV7d- n]sG'zĀ)>$all7w޿Xf9,&w;0q`L]v^ mzP詠bʂewa#~ym8葭E2ƶʃ 3Y(W3NehTRxc%zӺ93FjJ>9T Pv~ȁ+b!iC5dߕOÎçkdHN1&6=_^^f?bs,PNLAurmkR`Dy|Z(Af#N !}~~|p1HPq'&ؑD#CC5ȓPpvg[ǵB{b66X@ɳvNjFfF'FFo8?UO$J]! -Ƴsd5+9%^rru}ֵ7įIAzB#åq*;],FSPW0#MB@*90X:lRG$: c}d^}gX&F)4FgH̲d@&'[;G'9ypQjAKv*hOI()Q[GfਤD&><]E |4lnmn|lciiݵex:j}H¹řE WH p? efgfܹwUD!8%(?:={D4ፑlb/,תʼ: -t!$J"gf1^ER^h,j_?%$-q%2Ʀ'%C3#a#È#"* U& Hm_v9(T/gk0U8-3` '@? {bhPCl j!҇Xr,Ht ~j֪m1 )4,h?4ĢiyNi=HClĤtm6}Zm31V~֏҅,W!Eeoֳ!Fth=Nza"65l3c. c ߜ%疔o :ꊩ;Ϳ2ЖSeMw"k&|9܁}:|4q;yi)Qj(UKFђ-LKiXDh0ƔcvdAtd>ޢCØr47@ $l~<^G. _şh ?}!zi ;oui堻āyq>'/%J(+P BbUA̹yhgCϿsM-g\[}olqɈ'O,g?{ﲢQ )/_ *nݺڣ'b1<|iI*sp4Zo뷿-(w hCD ( G/g'7#ǻ} `L$*@\ k?b;_%ٚdh#AKo}/ ?x Ԓ1a1 ] ?{n˟{LyaVw 0m([?U`xfoGÃOIJ*yt9cD L^>Нwjf:`L2HOk{n/Ҧ=BO }dG#-*`zWDx$Pas!GG6D\ B<)¢;;!`%J 1"R#_+3N$ bva G'0e2[`_m9!YŤ溠jRABv8kC|Y8iqX@9X<7:890AبRZ&b_pdv,J@(v()y_??/#21@R '`Tҋ괾qws3%TSFObajuLwQi QɵZC.Y$ V1 &a RP˕-@^ j_ad>;ϟEYLK`CJbE)`q{L ()<4%@ b钫%(!h?6>6PHoI٫ cm񧟮xvTipy@^{unjrferT1νJ|6q*Q伏8%CaB_ ϵj =)z#OR0~vPKm 7ai 2x(ʖkk|tGͲ-ƁYGM lFK'95%U·=|虄dtS'£/zt455ٙKOD<'56w|G>G`'8`w|;Nj?hOD޹}힥"oqPA=4aRB?ԔȤ=Rl^ i>X}={aW!Cyd5_lS*)J@'1PYB:Xqʡ\*  R6YOϖ?5J $lE7 Y {rA= dbx=Ba ŋ$ "qJ\3D~W/~A ]]>+;;u=&+Rg<4LBBeTG>ܘaU nI BcunSx@8i .Dς!+ ~8ЈHwsvm!L%ZQ"M(ס8|gg=ٯ3'.)5?aTAX@ŤmNۅ\ &9N<^(d|opvha{F&\q1hĴd%\PlB7`a`FQ fm| L(ЏЊk3SKNMM VK[/76MD+4NM\089|֕7޺q}nbf矼ޓG_mmD~a;7_@E\0 > g@))!.b\ ã&`ċQGD=aR w) -d@ _" @vh`cgok` aNpbyxԨ=1=ˈ /I V_t+P\C!ù2<2})sX}r("Ԡ8Z[?{m,ON[צܽv֕v7a8;ƙ|ݷ:ocCEQ3@H}CB"rHP8;C ƈwݳ|_3Wjǐ/PCj}IiJ~S.Sʝ!g /Xp p>~*x:;?ȝ>A gx1wj3W pťyϨbTSW e uS]A\MΦSy1> vyokb ]pc0$I[zMTv9P/Pg9qŸ6ec idOL#&&УF<%I 5!VHӂ =s"EJfj>>1Nm0)c4Ih|h;a(C0M{R)-q#<mM!?dų4qFugZdNp6YNU6;ꄒ:wS8.o9Iuy1rS FedH!!3ĔWDDi?δ#hẏ="8/5ۡJx4z< h{VEND![w??~~pgh074+q,X` y&ʗ碅$v{h=/d&7>eN^ګ"KC_P|9JePs_h@iUhxW5|ѥ9&d%"W֌6{` 6}1,2a sa~bK[o+*wyDgģe$PxRە :Azr2DL$vON/K9?T%Hkpݗ䋢S># wg!\O>4m)uQfJ*oRD0f]U iNu҂}.,InOP+Nh(QTҥwIVK<ڮ#ᰈ%5~T8*A`~t$A(%l%!2K /@g-Jd]~LtwZ ({̋9qRJd|`yń ֆ|ęɉZ_~ssgkcd-fL`9%CAL` u GMvw[ /(Yo5OėZ@㰪s!3\q.y/GxR W)|sGO>xs}<#<џy{jB'(Q͠p&8L֑s<|+U+QT=\34\DKڋ+e=> 2ၜ5f=Bb<.Y6x Y]f"|9什DɀB4N~U6 X=wf@J#M lζLOWD"A"#Jen"3tK)!8Mm&X4 4̎1i?@"%>$@a\n|4d(\Gg!!D޸ Y# jETvɠQ8cO>dv d)pLc)B1ACSڈ :Hhur1 NtŇ㌾(uP_dWޡ)[k>]͋bQO̕ĝM|'tkN: K("49fZ$ˈ~O eK? FtɤkYyjU}r G5ug6n`JjBp&Otya@'61?*2j=ь%?~ZXV:E05Š֨ սE@Ȗ ?$?7Wm̫%˔[Ab2'b4VW70P%=_lWc e",2#֭l.BU[DyA;&Z@IR9*~~Isdzx`›צn̏̎ c IBd"8>jmGFFggQa+eT7UFKlQQ$ݐ8R(~-߁VAҝ(V hsf,@ T1ZRBZLO__s8kk * P-Kd=qΘ)sF?*Ǒ;L:|57P-eBfDSGJҫ@@ 4L cA ԦXCeb D0pȆ8]o UU׏vÑֿJIM2I̦#֓SOh5@e1MxEANV(߃} rc8NL564"˙(z0ȌV&Q O\"%dNzS@i&Xkf d#mq)8z%ddq eDũNnzˀ ׶HJ)ɿ.sS"h k"UO=@o `Fi@t瑝;6w֨0h1ཡZ]۴!?lgLz #TRgѸB2#:JJmTJ(>`bI0> =[իP1 ' CF_C%˨%LV-j#Ag ru~h:FD6] {hlhbf^x/N7Fܛ{Ƶk7D@ Fı >cd-IF g&|sӳ3H]@ 9 s'R,"/$萔KH#',p*'BB=7B89FjE#L[>dķb X2if3="'B%o$W59,bFJ]:Cg ҹ@FW*y|.hᢰ7x4e!/f! aJ!LQ4NJ7l׋7?5Y<:ٴԎүTvyv&R3Y1X BJӌ8E]`2#WgN7u)J1WO y{F Q*o!4ޅ;xs\ "j:ҟB4d(<$2#:y~ڠ!*%\sYlbOji^>V?>a޾/ ҡ+$P۠Lewz\p9JKaEcW`3mt.O =a|Cz҉:cw荓l+,hvi׸H0uՑ=q}t'*}B bХi|rIf9;˝wˑ|zk.Y&$J6}#1#gN|~t $pF5s-BUE3gEձǻ_ 2h1=8Q?Ck.OJzCu-X:j#Rh?V)_/ ɯc$'8NU>yl@n,Z4 {}tC@Cvy6@2JzՁ^יA CG U.6غr)5#Pq_P4?0턍RB֋ ҡ heP/ A͈]@B'W.2J" |/NAr  a*iC6JXȏ$ʽdRM6LV,̓a% ҜCJo$#hk+{tdCGQl*FF%]L l5):1O:؇Yޏ4U6lqck^6 "`2x$4ȷW*# U7 'La\5sZFμ _)L{)c|†W@5!6Ű}Xñd] VK,i_^22:ګ#FV0bDlbͷy{oxX'#sSCcHx$&D( /x[dB42Z#n8#gRhV-FmR#JqL3;v@aB4_Gvp) áC%q- TZk`lprZq>x*Z䄄@@آ܉-<p#Y(rhF1W+s=sefN|MIWW *=t iRrQh.<R@r6KHӑ:}z6Aiy 'Tx$|Gbu*%*H_,gvr+C~*x'/䰉 m1faAbHZ.Nѓm䈐p1pQy18Օ.o:lpJݨWU!hAmF`1`ks@v+&7-0S5 c=pm I#-+2&02D j1T~TXѹEI2fq_RLVqat"pԪ@arW]Q(tS#{!PSvD5G!~ޞ[k&TSN:YγIľ=f< 崧 dY‰+'(Ot~qs5tfSkF)Rf^~!V UZlut$|uhBC:N/y ^(R1EL!3Faۄ#E}^!$ZorPU= ޿i_-X)h1 ^ӊ&QFz G]Iy (ĩ{lx?Z?S^ ގtbu )֟P+G0GܿzoWo\7v{c?Ꞥؕ(bq1Bz-B0>|>;  2:;I2JF:rK}:A+lywph3'c/DYs*ig;xfF'x4qrcGQ[m *s_h5L*) !'Yeb-&eK9DNYM:lvnU}^=WgZUwjB7%'}z@Ĕ,ba^I2%uaG"N\( [Xr lx]f}U /"Zkl#}90960xڍ[A}ʼn!=402cEHp|@ (pgN=H=JІGqA8<+}!Ⱦ:=jԏ7˛;/6?yzѳ|0;xg*E P.h;Hvxtx|ty|/*;}(S*v]ôFfJY*YҠ+!BDHBJx ;̆}9cÈ򨝟N Tn^[?,ܻ^X "8Є-\!_h>Kqsg8ss"',kRߔM)rȬi0\MCB 8t5JMQe,⭑ԑ,i x9*< 7E)_-[}ELYݩoȉ x\VP:DĎ!WT7go)bw o> 1` ·ywN-W5m8@6y+63~'Q2H*,tBg9Zfɷ2:I9k@XMY7"Ε3#s\ٴsp ND6cL~1#Yr !q INdoIdc^!ξa)&GID60 ́Ph)%)oݖYeD…K)̸UkAppK(KQo=QZp>vg_}%.,..7(Vx;*|@4 %TdTYhcd14 t.D,%!GrT+zVj)j4Fٻ˞wa_%#u5֨{:Ӏ TqY^^G=Bw݈/㶃x/o\r9ZT'/VV Yz?\|7G.lH2`Q< 6Qbu>i|;xwo,nj[?) ]ߺzu5 +@%eGN9#=)MEjN]pd;ȪAB~MN/bnHi2J %ԅ!T7>:ӛUO#tAiRO>x 1;w~hk3Z{#angӕZhiue=[]ڬp])hc(LFmLp:!AF3ZL P iT,NOܹͻwff0F!M"V@Jq=#*7*6. aQH"X3Ԧ?.rrQb*zOx4i%L b@7=JlP߷3J xNqlh{iP88GŌ2 N7ŰwxP? Cd^Rpn0[spyL#Eԛ$2at戣5$jD]> I@,ьܦgը3BO]06-Y)G ذ8%G sRH)Š4< _F2h`Z=;)ӆP%&"gLʿ< G+[cRTK'Un "JvɁFEdǹiXzqjA%rOTH5ox^̱bM/pVK\KXW|PX\:=g' ; tkB::R|]F|FNhkg/{1\SltM&f  ehhn6ήSc? g){611攤~2I!A% PwfB'-x%*Rau7|(ETU(ʎ:mT ;; R{B?ɳ%:g<啑a|ChYi0;a=.=^Q(ݒŒ4z(6PAJ|grZnpG_{{?kQ]rx# QrW\^ZCx,h@ʋٯVw*$ i1|ǤjC!lVbqAkUR%;i:Y mSh:$!@ɢ/(0@W)<((ké>(,biX47&* ܗp2:xUN&fi._x%JXrY?hizAFhuzZ &*#|Db~P9/ޟb48A$.'HNGksS֣g+;EX%(Th^ϋf;jO'pjQ5*G+Fg@م=L,JYDC@[*R# R:W6ڛ(Otytx8<܇_oc]}?~K>?^ztѳ'/_W6GvG[c"(T(#jMJUGӦ6L):mVjNk=7W-?_|;\;"Ory,xmV_OQ۱&{8g!N!DN^>}zXWK 61ЈTA0 3gRfsu`ie3 SS'f). 4rL)d'kAiO~cO$. IiBTif$i*V8!gGcJJJ}3T ʃǹ~QXIr"w*Z__ lq'&btp\j5PG =R58fRub[2KL GJylggZ|P Ն3^`[ePdYT ԗXc\`$>ب2OЬD^(<<,3lBnj8ۏXDI4a"Kˊ rOc4y)bA  Íb^c% ꀙVM\IZbT\ V)DTc'":Z(t5m A,ysͫ8g.QK<N8bBHRe'0m Q:h'a%jQ1nfGq_+y*ud_05c6Q0i;JK^K<0i0^cHbߤL IiΎM2 ;DsiR+`N"&s z:8RrO.kVb6Ny);'e^Ғ=bS܃û2bjz?qolx>0fࣘe/[zrEvo SlrOFvbٰŷ?{?Oo?'K{O1抔|~J_1dM.u/٢hFL(b}u<U/mݗ (4?ܢJ^GV{Ϗ5$f*Md߼G7&;T)4q@H/\:b(x W8JfO3ץ[ tAiffwcZ_<_9'Ysov^˘l.|t᭮IPrrbZv ~_˿~~/_˿ʟߐiui؈8TٵLEpo*Y(T Pd3ma:?$z>]I+q"Ud0TUj_))9l"OR*aV)]3 ( i6>ޫ#% yuzx%('F,LNLN ʰjѨ%t غw[{GhrY$^ LOG Y{'C`ҠVTI *QذB\27h~p%崑r-sopS _:PAdpliis{4cd?@DW)JU N7ޮ ON~&;'9&!'XԨHvM{GÁs# j5 ;<(UC}}('Y6Q;?|SN)NCŁJqVU럯b<'Lj!r@󳽃O?|ɓg9[EFC$H- :pvdNT1@ GhTD% vk׆1QR2#<]ԋw>#$;b1؎dKz,OS7"i'ȗۅDOv5 (֪Ýy2Y yGz.(gUhL {8m1Mf!#T&nJafLҽ >@7촡y19!(37!7<׷#L{ʛ4WgYѰ %b6W1RV԰D] ~Lsaƍ h댩:)-C dfIXx"7gSIfQ)4FH#r}/(i}_fw>`+#-k3{c.y&;ҙ1s_.pb Z̼LWiǓuFZ@[D b|b?@7zu5j @8)tM~Ό2,k,;͓U5ЩkD% kBSq֍*4CآT**p+R;~O_tB?H ojʾW?/gܽ{E!,.(g(|yXScx@yL|ԍ×Oʯo,oяzFO C+0ukJU`CC.>>yRBuDIi;DnČp4 q' 7{jJju +"lp};inX "^UX$`bnZFYYܮ#9  0G5 0gEDG]Dt>cgfW.z9-  מ>{;'~O>у{ųZa~<3V/]-߽:+}k{7Gnsk&nܚuo͹AT X}9lWCLO!Cd୫w^řTmv2Q+Q8>Fo}g/6vwXN4[{;X#c(nc". 3- !Jʊ0;b mNLN^vmxb )M Me# cGC*XZ((1Ȭ R6A;*r)I9 -$4PJtb_8nm4Ww{!^($hU*Scóӓcp$nK`ex cHc>GRTP LfH؂-q umZ VOPV d\Tl M8.mFثvIJrMpڨqdkU#0C0d\7CM򂲩%$[|*SG0L}##Ĉi_y.Qj16/ }D^WYo[U(Cc7!c. mDHh }DZҎ|3'xP̽o䊭ٷn'nTp$)̒[~>7R~[aѧAKid{sFOat7e. %Y_@z"Ҁ}UoF#ZN:a[kDZdB.q!܀k;SH<3edzC-`/Hg K (H%$`4APSsaEꝾ_rBt5Q/p/] ۺ@<TdIY #Dt;;GK(_W|K_gבfSѲl077_|ƷoQR o~~+M]2w>dJŖ(ϿuehQX^6ʏ!Q[7y_=n3lI,-s@jq'iP4F{p"][F xJ?/so2лq}Ǥ`4D%~FNվS_걔@-Թ2,ƹyJXZZw {[]OI\l ?؆4f_TAI 0!GM*De I3 =9*Vw_Ccam9=>m#%^rxh\%"䘽TgPg B{:@>c|*[y줾5ؗK>e0F 1 %ފZ}$[)!d<;YXXz?~rk}h~|tx''b{n|d~fxf67141S#xuzr`,+ccb*}C}hqBKvԯL]~<4ّ[gnޛvkan~hr<;;233(Z;ngD4@Y γ[K:phln-LefX' <@/"%Z̹# O\^Y_Fߵ!y'nYA!xZȡX" #>@=bqQfwh֠DLx Хp30f1Γ|Ysu⥛^25iCf|@׬Y)dj٭jPU 62/?dWLJvFIA+:ꅯ$P]5usxZCb)3 id TGV!Ci2}KYrEP_Mqdd}^cb^/.qi[x)!A2/$%>d#%_"= dž) IZH866ڕ!`4إx%gqKAFV;T)@ڬfPܦũ@>O4/#JӬH L]Lg#}%XWx6ʹd :A\/@6lO8"m[,K2gwu!A8h+;]Y~L|4C L==߃&ʙ d`?s]id_W#Gd訐Vŷ?FiCGpT#=K3`VEJZbUM~|-O3$h0ϬBE 6!MeuJovA'up_\z~O˿3wx goܸb>6r /LLVZcE&\DWބ%!Ջ?5}SsuP ȡ M" v",ѩ:a_7ނ>xMe0t++L;%{e WZlS!S95iboΨ8)ME]%Q6cPTz*؁~1uX˜" r7Pe(!Ֆ hWF ƑR0240758?qlj.NAV}:pyH1~ Y)pRͣchR#%qj=b22\jͱ<[O@}pqtU+bĉ@$6.;Z8:\Vķۨ gc]:=?i[Hid/(PP*RyXi2Qqko>}?C*x&'g'#%\͌~޵xr9n\yE rG?%%gGjKaR󁒝S$ aDN@J"1 &-j- W`UjXc j8BId@s$q5I%nЀ:%H R'y}`ͥiHG Qros{'Ho)dNOHUƒUpT<= @A]RC\7TјJ"Gt%{BKWޫ_2GDO GRã4s]2p7JfOpQ^0̚شlc8Ys 7+$A>(3W;K̒.ː8J%p& [ևn;:E>j*,6~vaV^ɛ0*tRM>nЛV)EdoyS\=coM3=CP7 (C@}P+ȤMwUЮW0q+K:PtÔ;_.˵w⟶@z.}0NXeVF ?} ~xiPL)u!B}Y'#3]{m6K< JHv3i3dT-v5(I`7cmRTA F҉Rmw:@^4d0wb*d1|uD;Ϟ/NFvme+o}c_g|W/|o>Ǿo&6qi+ 7ݼu~㧿%Dbm^?r?s8ļM5 um ,{26:aIv[꨻~ҘOo!fRSSL4|N~|Ve@fSSbh>wb .C">S~s  zE4l'Zh#hD c?dk@,Uh1ŚѰoX~?ikFr\,.Fv~{Wʪ:?qdսoyVe<'#ūW;{?{]ȡ#Fg#`ndрiO_\sY꧈`W &s٤%1Z4\A<]0gCt8^ۿ տ=8ȥ%S}d}R~`,_~-#68:/| S~·Bٚm۽Gde& ;x3_JدP Z[ٚ TI"`[K:{ǂ#:gKLp1m5l&g֨ٲ[/}ALk^_:kTS$9%P$*DR=3V)}!V|1IdPGE^ y'PAtQǸ`P *9@/jϾmq99h2tMdtSn1V?Bg ÁCM2r+@bD E\QlR(Y8.o )fAps3^餼ZNxتv[{4P{)k;z I"RKPRY/ x tsu%jaʱ?"C[`:._ҫw26(׉` 4rqڜ?{O˝\/O7~85w?:r:GH7BB C~ Z 4  xZA1>xY\Ws^VA7$DzG6nn"PTꏰ,(Tzn9 3%a+A,B6Lmv{N=-O+a&\u⇌OX},=ݕAֽ6#C@S 9tT} Z=r%[^W@e8'WGo]KG77fd~a8tqim49Il[{ DP#c9hưDi_J͑aV Cw7^/-& ֢2qPHmA/GztWp5:I*9re^^puvԠFIs],N^Tz4Fl%Ӈ+ycOfc1f4§/-j8WaZ 4A7٢5uv#SB_1F Ӧ'-Xtj_YX0kvĴ fHs'Y2<]iF-`V6e{;ENg0ű'>Un~YX(>J-ٙ0_h;eF`2秪3'QE(u?OEK =wg:|GB>^F>†xI+&R2>W4>tWNNуTdc+퇱ù7Nf@ j9huڥb2O7nDeԏ /GE/nOo:>BF;A5~+84H3U+4/9;u_Tn+䎽|I?@nP5gS]/LR$4pR8Q;tRڢd]*\TVyNƆ\6 |_G8NpfzHy| n>,6;2r]QS1_`.a0_|d9!S!z@G8yaivjJ/!j@T+!C+$TIM"uh$I\(դ!e7 ^v1Eh݄M-4|Du94tv?6N(e\V-  .2ȕ,rL{޻-N^PaUkN|򽏑~m={ O~~7~~oO_~\~~>o~{uy;?vTE &REݙ z%%=z7;PW=Hr:)Ef\+wʫ7wռ5:RC @96(r.xKRE{(Je6{{_/rҋbTy0YoW:DHg7FZQmUHԠ\ X1+bZl0Oﴚ(T({mfөd3.dMcAW ~gFѫv"$朖OmW[}lC3?/A eGU{]`?tջKd[ѿɿswdra%XT=˿~M2H/0ra>Nq_~+nT uO/ _T +5mfjj,&8}N |Q.6g/ .s9|^>\vKv\'!-_~Gŷj^T\:}ԾÂsQ,=4d #jlB߈N l!#n gS6} Á0s$ѼZP o$@u=ayD+%AsQa|H}'fً=J:d w˄C*Z!>0E7-oWqNN7R/0֣z0/ Tzl~tz yp*953I㿀b"P:WLJ/P:5Q&|! a4Cpd Rs/|%L;n- n.o/ y#!XMJO7x#.({QAb䚙¼?}_186 @Bd԰ x4JGn>L~wy},rr}Qll7 s~?}g?G|)g\5WBQ4f{Tfy&%b@RȝiL2e6rvSRN6B Od$*ꢝ"IntX&u)N6ix5J: !0:iѦ(&IԆ6zcn@&>e2Fn!gUQ {kjBz|->^ŖM`˞Ah"5tx%FI(+Ωv`=F:l rw2[|RӔ-l'imd~N61p0Qmyyn?F'6Bcs's ǭm8$b-tW0J [fJYR~D,5pa8XIocq/> S*ujO=W,J@W wr><Or!e7 p??O5`'ߠQ}ĜA7߼Amh7+DZ};jf#ʿƗḵC\kAHS0 C$V0H?Mx-ST4ϸK#Kil8G?ɇ1#L e??#꼀 Z@>+? 'JT"kM`xZ6M|?KCS h >M\(t=7Kqa'D)L-AFDo(FṮ -3wb5w rq:Ԓ#3D'ĎXksԝD^\AuH@CS>SVW](nNYT <~hj h>)G~p: 'Ј zVW5P2I{7=T*CEɬ U-h t!d-a(Uk<d^||߬w;FDJ]uDO`R@du@XJr>ppȯ6kw@6h?_sү<vvku +jmQoU93G^XG$;e#hӰHh(Ub-_eDg`-.6y94`U6Wwcr7brW_ T,/Ke3/2טo氌 9qAUSk+B \!o>D @!FDO?tiԛ#ۯ}_|52._n{C}/x 7=ă|QՄE(1dyLgur+壓}]QLW~{շ=ߜ"'L 2jVq)n`jr2+Ԓqox/^ş|3oƅ aշHOE@EVϖvϊӓTT4C/Z77*WGLpKj1@<"}]t#fɓ0#S0_/^.\em>YLĴպ6GAiUh~2 q(vO8TS1[QkSM(*)|U P+@J8~BqH4a8,A-@W~G.9\2%5gJXo\Usu{HgFXҋ~40b#}&RԲU,418~ |z [lXZ?_u#p|pva> O24ʩ޺' wH7T)d'['LfBcI5, ;R)%.^\! d6`E4hUӒ=I ,=I;&S- NډPhJߴ=5_h&.u8qb(G,Ƣ5 f'uh1nIvs$wØ{M$h@һV.N}/?&t)ŭy~p¯w_20>o8??mPm 41h@g<77=ݺ x?{{OM?Tmۋ? i_|cmбn `92?L 8ORW`O)?l>Á>D~SU>?ks5ISs4;br/iwh@Չ5.)@䧀? ?}Ks M>wh?P?េ:z| t;}u}wq~ >_4n dPԉNЃ`gWHb}Zr7>YW3r@wYlsYߑ@s4 _{n'm?+"h^&$ט?[l ],U'r *&ti7aK8xZqV>o7ɴ_LG8'1~/gV|}xg]qC=$q­89 y`(֪j l3hKC+#c]O_zȋ+TjzY)Uo9CyL(rQb A4OcD.L9I#令f:}˟⛟|xq^\LM lfx6MoX#L#!6nՋݝ&A:;t‰fEh~+VJ̏ pTm•ńm k_:89` O6RWaa}IJC0+i(8.RfHhVbэ_xr USmX*W0%d)'Cf/k\IVdL&.N8R7(qRxCO=;L  |Uu.>gX϶*$5bpȷ[𨪣 D]%(Ƞ;I rWqñu \U?P8 ^n`r05x¾na!R?2}=>Ȍa7/49/32f&vk >5F ؈u(l ̕p&DG 5`̍T^ LrNnA` N@a : [!pyj@ʅ(8 ůlV AoKzxbֆp@Or!w"f鲼B1_o?]jTtGX@('Eqv/~8Nuq*:^*4c {54|? `H.D5^ِi@_承˞ނx,-'@N9L*!Vf?߼? !T5l26k̬׫nTK(miώ> 泿R Q ,h}§=0?P|Z)evYh 0l|#wAgCh;2w??è; !0 qC?_,z; ֕j8SgNM(ґgĤA}-c!?o/Ҽ<=әpw~׿@NU~!M}vv2֊-BUV0q?,Nj-qE\mmxg Th{& Y"anW RG_=#eX[_vSY?#N'F>bwf5hĪ\]UY ;1@TR#Ba=7Ntɨ4Rx}T\!I@>V"Uc^ ?ן]w6)G63 Ȥ @Œ.R,SG9a8FҪߞzp["l?s)V5NҭTxH P1-6tsXtmP8Z*jk-O2TjEۇvxv~w zCOWlOs' ?<.zz1:폦SW->𮇖9>4;Ђh$95˷wպԩCeW<`Y.NrtǮէwTը¡&n8dw=MVJHSoHiX?%L O`@e ,4?Ѡwt 12HT9(rk<Wo޼9۷_|~A&J}U4,..@8?X7d,kV_>;²EK@ Oe(Grj8,Jb"$g$*8#ɛWn $ˠ:\غłS6ӵR!VI\=(/^h3փ_bN3LH!Mmo+$B$Nqȭ,w+YX0ADٚ«oT154$5HbqЄ;DTs0D:V,վPӱGWF^^ɶ[7!n+C,UkBqU4yP h$' ^G $7J0kdLJO.8:\]WZVqˆxr!:l/blF`mMfi|1QG7MOAYKrȭ1Z WQ,adar92SF@bWu=qpiK)lbK6B/N} ?/n!׆lGQAD$F~W>s٧h +ar<\ K-&iP|6i/,H3_g8{عA" _a*LL `|?pg %? C6CXXRx;oVwR98dDp('/. 3 HeC#"r/[4IDATgꗱ0$Ґhbaԁ-ɾxv,Ow׶ڹ[r"6O%)|]r=DrNZmo{ )a'O$2LH hb9 j5AQC< [h6,7ϻ헇7A!Y& RQ%ՐpI9pǯaǽAzz(7gQ>'eXvʘż|+6vknըtbFY$!`(L{:>{7̆~f`}PKd4CQ5~\wWwDh¦]B uvڍE1˵ *]@'ZnU΋ŗoݨ%@!QBd) =&p|nr 9yd` H)L cA߄i=Ϧʙ`~[f h"D*c)"U"FE e/]xl_~\*W%̧y`kn_x{p8cUBRUv&u0Qt8Gy)jIϦ!/o:M 13CA"=ц6$ FD}x8KOUmbPPs+l"Bhf'p*d$4y>NLJjP-+R5mlT0U1 m򊶱6nYMX@2%XR$t'/Xj L[ކ.4ț}"-f驆&j~E!l+g$J储Qն]A.(vK_Clɐ}2 :5dزjђY9ظҕזFC27>}؃D1)AYxVv$l`_CSAL4$6:8ǩ!?1C)n2EW.a)9B{b6E;5x eGfvyUagNn_x %?H;U`aTeJ|PX`~"t$Qy37 Nd [êmt}ձ`_2 6lEٗDЌ#[30m-/4f\Y ;=e)DiE ~) Q cQut$?GrJx x$G&SgCD lIJ >"ѩl#Zfe'xeÑ E9ԸGf<5鎨>t{R'CdCfl6U=^ha'A !!jZIt6t'0@6yr B xҘNVk4@p, GjrR-[ *)=_aJNI>q- <Ҏ$WN.QDv UtT>:bA%o+K hW  {26~[/u\]f[A 0p!o#5XFb5iJ=ս` }ʵ*aK,(zZ+ AÇ$ƣܑi<{Wj˶JKˠ 5bE--ng%NqPONwN$YB*&X$u?fPhyKtNB62!OP"QcĬRt\51:7ad ( EB<,4DpK?WZ)T51af9ںI":q8do涖d2}Chk,įx87߶DDPpDuB$^l4F"^H8P&\葙\4k^YfY Cч& #렰dT@z8cN@ /Z EIV v~7!iC UQΠj*U5~PGC}O8W kG!"܌(`ca) -R,"CLYT^,VpɌ+x]_ Y@EopEHI~V,6D $Ą܂ QJ؞,J_3؁ZG! 3cI̐@VLkPeQLY+ r+l r Q9EI 7Y!/P,0qqt\@jmVvn#YA;z D,bX ~'EXs,,vO>FTXƗ믿bл9|:T 0sl@Ýkǘ@`_|ۛ~#a_z֨Z6fDH W̏0p1p8joUu/g&/ \Xj˖UOD=D&-DmniV+r)Y7gghAc`AB*Ԙ(|ѬJbq۠D{GTR";@ˢP)"9`z((p?B lJhnNP6"&TtQ;>>hVB*:5bszjp|TM Ɣ5p! ?6ytD#J/~H4tP!y>v*7}] oO[Ě6rmwT9MHÀ*. z[ܳ=Y޹-gT5wr|*EtK1*QTmoG.H ?7wH3 \K%<3RMsI&]c@@ucر!-$YX"gUEUTLrLB*ʷi/rTQTLO1L(* (=hjw0`(XHl>,yV#@w{1t6FP7bA4&xpJ1h0(I-^c_C?duҋ_ɱ\xo Ӑ𸪱Daq`Tz@F0ү0uR#7nRu"Ay^ T95Wvr<*0ZGmrS/"uy$9W;vLJ)DY@SrUe-BpZ 4Ճ `LzHQ7ݨՋFDzL]L$B ^ fՏ:(p`C26&2[>G$ e6#[/|fC*V.MYיľ7N1(X7|\ax̤l9iC &np̌s9x<;4se) ;9U@kRq].^ì(J'QB/5Sh('x/ 6D!BQ;9:|''vi< 4BؾpKDCnoAb4wwuY@ن[*r†)J6Q,@/5)J至Y.?7P̳vdzt9=v:\ "94WU2Zɖ0v; LPlR(B3q.v֩ڴ9 YKРZֆWh.=?c \l=%[slD>x8,/&odTts?)u*JEx;Uxp*A-Oe-Xӡ\tTJQ̗AP[aj+"YVrU1VG DC`~U%Bu_db{P]-%BB)e]8. Hh!nle9v?{. x֯* bHxT 6bb|I2X5;izmzԞKQmʅcݔt:ƵuM"˯ĐTG-h0FNL?e4 U$떐vX2TP Go2UĈX-)Lݔ.p6)254_P~_ڿ(w ځmHn0ElMQ5amlvA؝?Bf,R aPIn⛫ _Ă7熉 |%`{1¬'hUƁ"銣43GɒTCSh[ws61ªoc\o!|l`DWWG0@v6֞1=8zSzFc"KOV ;4cݣqN{*eX}9%ž;s)mstd@UB9Um>2)(aؔ5ތӄŇa2Вg̑]y)4avg㉦d[u1|ƷY0͑$otOFlSKfh^h6`@TSĖZlPMf&ep̈́hԳdL&.]\֬ Jʏh|Ƞ 盔 աpf |sauk[n9hY+DjI!gAʸ2AH9<:uA@0Ἢ90y͵JEՍj|QPc؇/BsefTm7QU1ea::D*1%AA5S5٫=QzԼbN@.~|B˅)A8b*yH RUPF A8TWK0Cr&ߨTj^G>ؓg/Q4F>6Xҥcf|ٔ('tz<1 X4sZeMo*6TH@%6Me!r()5QrLi,YdJO:#IyI"Rj%RE o01sY߰Yg0ѠCs rlV>Ў뒟04Dd`v갚eՐ<^*I ))Y߄9 eAe . ) 5f6Z$|.+`Ę! 5B\rA³CF [4%D.%K,;b:A 6ǧϺz9<h1E*% _v AHIa?{bz[?TQgF5h }jyQR^j7]Z\q+ 'B:6p;^wT h; Z؃=*.H}TYb2*iJR. %G4/NnSdi/`,Jت`hXM87~44eC$KL;PP(ѦuL i @bSC>[ oEG7mpP=GR2!)_rA(w ehQdЎ%Fda›6ge* !*V"''}IMwE99RCX$oDݴyc-v5F( %Áqp3UJ.$|L$Raip_a 0"Yҕä2&'E>{7Auqm܏L}>dD?" )$  n FGYBov"+>캲)CRU nmlʒ\I>pΌ">End-s)BFcLڥokglb{6C\@ jNQ`hNtUgHFRkjFi=s`\țeՕhZ\\VKU/:`&\͜_S =Tqak~.$s5ءnУuT&DwLHlY9W7_uENWO;4^'Ŵt]4j^K]-zlѪc Hwz01A5O? 5S<6$fU2^4ϼhaK2u(|h| H2KjXPEC9TϷSyXv2|iBX+4݄ Ĉ<$ Nr l̆p/jJ%TTGnGQAԋĬ;b$ qy7ytiܐkn? 8+Ö\Edre ‡#$PV;Rɮ,lhͪHxD1,ʒj" EeILGSh1Qx +D!e6 1-4IYynȍB{@$Q`-Iy Z4]z٨Ɏ<%Bl-Gf䐉HE')7p<] XJ+# nkOOOpzxgEJr.H⽰T WH7 =)Er UMR tAa`8k8P@$#tj M 젨j]\#T" ,&kRXG2@DM- J%Le.nRa/.~)΋IȀ"e0j )"zno>lHX(aVQYA8򋯾Rk?zT @NQ+ K0՘/'8y{4VSv^2+"l6zKyAu$@iN2Od`]NDe&y/' 60;D#+ *qy݁ & lmva^M Z4m|"l'lhW{R4ݪEAwܼTm;A]75gS xDS  Z.N7K@C3j 8L֍4]Wj]JYyV| c ņ [LyJPƒx]Jf(M]?*-^aqpT9 ҷTV(+<;Sp`8}0? U~ 7gA6A9Z佌FF 7669B$#ⶨc×i&p\h<) qZ7T߈|LS10O2"U4)"HQDŽM4#c%%m dT[upp~Cl$9wO3&GuOvJL549H H&D.Mm. sU>n:h_PoUEe/- JȳZx3BM5L,!89 QlZj*c VZ}@zi?ޑdH*XAAj;s1ǁ`"ѫ*yVWzS-K&~TvCb }Qk%M J\QDUt+@BN#.Xa.2D^"y6)\FZ'A վcٷR zGTMs7ߨ)j~R liU\t2r&MJ.SmR²78GI̭iAsG4o&u`!Cz9Ïǹ'ѹHMHt}sU8E3e ?cHb> 2BfhL,6#1 hsb T||b"oA>&+bzNhNj5d9 Z+"Jc5G0P]œdIZD-Y\BoiC$N \dGˏJ/M$Iv4gnʃJlf}5гhSFahGDA.u 8aYgw͗ϐƴdd':Ap-!"p RQr+rچ%;|9ТlŐE67|9AGhpUJ@n nϿdg{;27]gߡF}or7A>Nado'Օ<|=!qŒT}-4|z,l\ i8E%HF !|% zbP,c"-Xx&0 ˀb)!Glc!V=ஏ~/"VϖџO~73-['/v&{88wUd?=:i:up^ߡn3O TAH7Tڠ0'R(]%gyy+@>h!F]& gя8Ghݜ՛Q8|)V?`Bc+8OM`j9@NP`}^^1&ICjT!TB2 O`"p&X,.H L]!~p+5V%a2@V\ΗkF{CV!% ,OZ2w418YyFYAJ1s4mi E*H`AfKm)!nAM0P7! I&OLȷ,tle$5̄,V"L*J3I< S\ d3 vkW]tMYlL ^b>lܘuЦl)tMpCU^ğWiڡ*x]b(Ɣ(ܑ)ORb_=`h1s_J04PJUS&[Y@Wы85mJ$ej$6 JB^G%ȋc})Qs8y_}uv|q^fh_zpxuJEy+4P.:a> ` Z u?F.@TdS _[GGL`d@ ( J&au̧[562o O9,Djɹp4\O?9:z__/hMuka4ou_}rBPbLPӉp:'P *R"3[a똄&`ܠu MjwZ6trݗ_~[dT\OQt#vr~]a+Wjiq@zЯ1Z^1 /ΌXL*`Px8[go/0^AG;|Y`W7_ywNs* C.[C8JQG9x5 HHk>@~I$ȑxD$YLwt=b#5~|0'Aj0|{LwaVKP*B!y>݌u*jRY{+2荧S>N_(~iO)T2柸l ,>Jb+ycޡSPb"A(.ICWS+|\l}AŏƣvAڈQa9}c8truGH S=Up\P`7w#+%:Cn|LZ'{vџ:)@8 8M L6fZP ֒ [ r[oh0Ev͈7y-lT7SDr#M`JC2qH4Nѻum٩qm !ț`kuGl.` )Y:N3:I i6eOrNJGZDl| R Eۓܓ *bqFva=5Dfk/=吚2_N[B&2U^ūAήTYUEݔVm[Zz*T5TG+Uz-c@br~Tu/PoMb_VUGiA'rzըOI!ٸuѴa@{ikqESVeTӖ/xFaܥ Zi3MAӜ>~6O DjrUFlo|-)dYȦ ]Am_1 E-Nd&-ʥ6 Z rDB mjY,4) M8ߤ1p:{)ɬ[j=gN LBN?F-E \tصwȢG9Npt~O$2wZU`T-7 JN A`9"QPPs 3p^}q6ݣdۄ(E` ??)$*8AT!iza xH#j5ч!A ༿ZHsgG;5 GD+A IJ "zI<yhou63f`hM$-~1qtwjc@$,gfl~Qo,NG2"' ҡ oJu Dר:7Hz gaF-I)c %4C92djF";2As5ET|Q|_֓z>̔JK-\Ζs$\|{{u?\\(e< rz.lff fCd"D!R>&. >dD|{{~wL0.GB! 4A)!An?/``~pe1)HAa[ɡɊ)Na yNuQP E~dF۳ae40Td%)=RNfrJA'"\R$vmVGc[S5C ʠ>"tI9s =mQ IxEB=Ε~SW'7A&z M3 ;Fh!^Gt!{%Sf~& Ѩ@KV6$0I̩m&I(_s3t 0$&RN^,  Əht%y'JBO 60ĠMV6az"lDb&Qdh1 ' V"بe\0nS#;s5txyƞ؀z`Oa_SՍ؄/}`ܠrFsWZ6JB&JX6Ț iN \RpǤ]$.*NL}RS5KaQD`gˈd[er1 flOR;{W:;# Ljp"p}`2\)ʓ161Zh2zȽ;;U 7۩; GF|3lN^==ݥJܘ b\TJJ)c꙯4q)\o !/2hG`FqPv;4f gљ#bm W:^ &A1ԝM-\؈N:]4\D l$|:*mRw\4O!JOBJSC< T!Iԣ<,D n! 5V4U|DH0[ MJAA)cjRJj*֗ JN"Zv PIFMkj"f׉G2$l,ņlUNm3ھL)Iu$LXVyɷTv39j!nDyu@SڛEj̓ja賤8-zKSuoZhlYI;oR!;v[#E[M:G+_O62ҭnmRKK*Kc4:9}%jp$%aѝL@Ip4V |v$>9qL_>~24qVTH= vB"RBB=X'$‍ fD ;#R2Sa?O6ދϐ5ɑM,Y)r$aDV#"$\bT5Xl%̀kLX|/7k$F@U vvM\j2ƨ%, U" <#zxx#28TZfˬXF6+nCdqWpi>+;$oXAx,)V75ZԛZ&}Ifj/2~޾^{ꓝ>S{jp?\{G{ǝd=43PYTW~h5n),2*3.ɋ8֦? C\_ꫯ/F!zԄea@ʕbF+8 o|a5 GA}jtE=1РwauWZU(" /vw:Zer# "` &c(k4~0ÛBZ:{D3Wg?Lp5zKʡ"f"4 F Hv)R`40F~$F0>܌7ԆIN%$;OFZ~괻V-%Ҕ4& B}%}cvnٚG^q]}7ܜ}ɈH~^! Vg%J79(-RC/ab[V ahWWd^5 <U̚6'p!5"+ELT޿ѡ,*Dv ֺ[O:o{cp1ijkuC6D Tfס(Ey!߷EHvL;o#t."O8\jmONJgM= |HV fMHj*`) . 90aT3l%Չ =vA$sا- H2XKYmLIn;hmNLҶ#<$0*^JC$սK־)fLH?ّ0=B'?o )t5(BoE{K6ZL$*EIFj7f.[g&Zm^551j ` P^vcYwk]XPCWĖ#ݐ!=ȑޞ%1^$~mC1/RW >9l'*+8bњ9ç aI:3~ RR%)QAGlCyBcsiP!ZV -*fG4:Nd %+PPJM;(Q0m5D* 8gDuJ P]F.ϛ"!Cq3 DJ}yl- ]닽4ŬIƽEsQGrjRC%AmC ĥ4m0**e-𑆽 EYʹEΉâ Ž,i"Bm@|+.%Qla%%7s#by6+56_ʝ]|e1T mR R]@A^a= [xN$FȨl"2Gp?ikŪ"OpeB^ƊFFT5@YuX;#I$E-kB ᨸP׎v+@x{{{uuߓ> 9Bb0Ճ~P.3),+ѐ& 3!yeU({ÿ?6b;nWWL:@c}c32[p߾y{vq;D K Y$Z+оٴ2B/g%#jIJ&bT:>}C FEd !#"%hodR@ p0Rh}}O_={ emg̾8lǃi]smOs]wSE7:¯V[.`sL? '/_^R˓(!Oss`F/?}mT! +W&{ A:Y({o0g9Z-گ~ ֋.L%2(R]ֹz4|'@pAVFb{@*iPQ;FZ|/Rk76YNL.өHX!t-''CN81Gc9IO8%'y, [-wa= ׃m"mo#ֶ݉")AclŎ$d3fB#cq`fUDvo~ߺ?h/}xijnMSP%DqNtti<5lsE;TL7n-w`Km@ ::6j-A"]krui<0{iI92h#[4P˟ȱn[NU-a!&d^՚<." V{ QaD T.Bd:$x/P϶A xUSйO$M3HhP|x رܩ'B($P_'1] fªҴ:e仇?÷HdHqTBf,vj(}bRuB2Z!954a*;TDT)2d )1+j=ihW+ZU <7wHf)Gʄ'1UJRMT(p0__ǯ7#P ,MrBppi˰D<;Gc1jZ8w)~:v} EM D𦽳 h$ԛzLHaW``< @SP4uUil C GȀIЅgb )CjwDyd޹u#!gBYLX[\eM(u6fOKu'Ļ^oLGNmmN[5vg_yU$o>jЮ"Ö覸FƕHuɑ)e``(X5IE_hkn.Ux;CñG;@vH䕶6īYtPjcچu+â=L-6,}<6K h:t\')|X[ЩwȀD ҶFcS}jN$92P7i: <טW-q)–#)ܨAm89u~7^%he0aе#o%ah43>ȳ(򓞢EI!n_ 9 ڣ&#gd a Ҧ2+4}h[GC4/uWȒOG1C=yB򞈣Hoj;lp둾QI.NR/AksG*eKng7N5ƒUQh*UԄE;2;ǡSٍ?FWPSu8\B(;B i(udA<@/bP %yyƬT29FAi|^|ĆV-â˞iSE;d1=Z!!vݞՅyU~k4!I$H+ETw9{/ml>ij tYa!TsI2*MY- GHNESd*d:MzCD#]P)p\_8XBNgoɳ'0+!8xfV;H $#}S[#s[}[3EJ2yBDd+0u%:D* xҗ7g777ZR\qwr9q=]!/f|5^܍ή/`l )ꎲ%"7G"dղuKAv:udD2j=FSd1ilfWF$"EغCxѽaXBf7Jlj ,UɁH,9dP:o..D< \<HZ0ai;ZFE>?ٰT*sľj oQ@Z$2^dY&3|~WwWIJ5SjZ5(IPF :2!N&b_MN?;>0P1.RKPѦ<`y 59;]sDXIH9CF]Hg]:[fFpz {8>yH$HL\)j`[6r)h AjLF:d2;Na#Tk=rV"0 𵨡'fH,Om6ȌY5HC!%$O)aڐ\KT~`A.t纻]%8 DhGg'UU3 +w*)TpyEΥ Ih١!.h#M4qѦ0-z*SU?&ȥ;fTnGWJXO Ii m uӟqaI3&Y|k}sylER R͋'bPבr"z/H{@Yo41ud[Uf@m G45T<%XLAJvF9sl2a^$yQtɸ9)ݎ! bR'Sp`']:r?'9G+`؄ V4EJ|ϏF_+`kPi*(;pu5ҘʕKs h6j /)FxCoSW5z2,7XjIے&`' S?I_9Dvúu%'XT xƬr#sMzL@wkQb)qԶ&iҟ'Q#C#6[)(Afguc^hYaMk`3Mζ;[ќ~I)8am;l]Cgoj o u6UJpI-1=PGKA`,38$S-!xFū$1vI_5f50~1?DpKm ٜe<7P,JغT. 5&L4=h&΀tPV)m!+4TiŒi79l "3štwD~8<>=>>]zhZjѐ\v7+k%Wq3VKAK5 “TA5 'u{,<1zw}~d9fHjV&˅r![uq0Z^ގޜ_ݢp1)YjP­4f1ӭn$u|%j C|I02B[5?Ś'ZP%B5NZd먥ر3GjdF9.o{_7 ' Fl~2#, yП'(~Έ B ؎`hDot;,AR8?aY~~>g|fUPu(1vXpj\x . q' ]Š\rRB݆.e%a=BƏp Y"nWlg v\I,N IjH=E΅6)#E I0Enat7lh&!;^ӣfZdBc s:Wu{zz ⌑1!~('IQWxYihPoQõӪ@"͐b o`G^l+Hl"R}jE}R,:nkGIER-'pwF?*!4N=bOWw*E1S .TNdqQ5=?N:*f̑f` Nt½BX}) l_+n8$i)Һm.$B裼0<-/aO.FE|AܑŎhчu2f$ji k*ggW. 6(#KG.6W0&=|N~A{Wih nd$cy{(K."iڰų8%&#ҏF_:䔤Arr١uE?X{LjbpomRXOdHZ)/T i 7TQ0V 6c>Lf]GhmBd6_y1 }bx= oERgQfn2ڼ0I`QѠ♙[Q6W' ]2iCRpڭdDwVDifNaD^gr Qfe6!vQ-P QyJYH$,4%?46q/ߝ/~'/N^[vԬ$:orO~Ȕ׻A!v;үj-ti<Y@9|4 U MѤ "!9-go/no!"R*zyg택b D qc+MUf^KW˯xwvO)9*;x {ߩdvjveg z  QyAfbd [k-.0F +uQ*2]4?n#r:ppw}{}uywu} /s0#e,7w>fLhި4[P9`{@3Ԙ5A؃$L!./N/Qx{t- `>RpUKHYo!jY@PΕ5i)RH5Yr<ɩ$ CvR2j:7gAn4Q ()×a0Zpq+ËB~Qyvf Vs@T@Upz W0h?;ocQ%:@2#= )`0g $x@P\@9:pT.ØVW:j&RZbzJ [:bXli^WhxOhNʜ Un=zL"MsTIeFE셌""g.h)>}ɔ|0"ģAJPZ<7ɭOR}[62A]ij>FޕCaˁX(.WE +0dܱFneo E5Cȓ 6&84c3HķavgjUd#CU]~0k qgLz4# Q7f2 m|`L|-! $dTZO.0,%6{%缐pj@l@~Mr9ym׳5 w=Dy#t[*C+P,P^1ۮ[* ?3@m[#*SHbc(V$#Zڠb/xh6"te%rbuCa*T8< 7Ia͆wӿ=~M? ׷6sw=P40pPY)YGh.e=9)UUd7LɯSd'p0 4R~Y\$BԤ.fgHq{ n[)Eoz<pY-RdjG*(6sdD , Y0. 'XAOQHu9ul/I\k4v'T?<[FFbs#pbjOP_e[$e"EC$LL :aӂӈf ( BȜޏP\BZV $62q'D$0 D1@`R: Ӂ ft{}]-ڕbNvs%,$i8SڥVU7%"gB_bQM[P?WCJ=O;KL ;ڝ2* LUkjJo{ـAbRnpY*qa;[yG@; ~Qr!m%fV d ]$lǬ1c7ufk|S `.=)ǠvdXNsI͍D:P48c? <(Lbַݒd{o7%`-E XHBJg9'?e)`iv#vw2XuF[pLlLh5@(}ZElI)DɷхA%XClhJ,/x?e ?:k,|☌\Z$|jȐPG4?EIu$hPO,Հ%_h"N 8{aeOJxH4(% /ʄ d>n aMV/Ɣ*yQ]L wCp 2 tmtb誛jgqZX)lda(c)O0LC $ 6 Ƥo$ ʉ<$HieS5[C-"hmʘG7wV|rf;|QOytAH&LW̼8r/5(XppB0@F ۯS)WzB'\zWCsh#N f'RYǠD)A5.'(Xe;^Ix[,=WDb1koOP(KȚo_ #"?2II#]e,O~:.!kk)e`>Y2s-IM&q;BƼg°6:ie4]P)w;HWV0R)XFa6UfWoo{gW7Pq6ŠH(y7Jri{~mwfH}F! =)ᑥ'B u>::FD6bvtwps\^_7WWs$'LcXS(U%ºV08Fe(Y#(^L:*{n +C`5ʃPD]/zggg_}pp/E $޾}]Vy̅P5#bf\ZO)(_Jϵr {|9 T 6;Dԧb+" 5pi"Sf(րl"faj@Dh-VԚbW5^oWAxQ,*Aw6';^>߯w[йxBm0*E7rn<[6NxAuq4S@4["ΪʮVm֙9Ss]Ԋz[Ĭ@ vdc fd{ &$bQT cL“HˮKcB-j9iϬj *8[s.b.NF W'r[Ť44+D(NX!Jx6Z~+D _; .ژyO"m HI%~o*Pv\}r,{+QkaѢ{4iB~`Om`}4eNÑ&|U<_+ǔ2N0[c͖qoO BE;&uۊR<5ml!ⱤX[Rd: lʌi u{ffqKn,pxdFs9yg [@gm۞5v/ ,ec[Bf0ҳ'qK6pZX%hj~ӌo|Bm%ԯ x~|3/hڗc&ZPٟsQ'EAK}|/u-UFWiE1{3y 8M`7!@k0o/O@ȖK x{R##rb&&8S=[{YmEh3tA`:]@"IFCX0߮>>ˆMp=fX4A0cv۩+$ƇpJtCAYa" 0oG;ڋk/iDnRF \2 A(||]6w}:%^.%yqkmL[vo iHTο`-7=Si|w 2UR6@q!KkT Pw-fdNThZ <-m9#rO|;D|h%,SD# ~ x0jon2 H<ᇠ &X`Rd j>iwv.Ip<4XGv D[焥`O(d4q/, ~ph= ;(XBve=Zt4y+W G?O_<n]*|o|k6^{w|iHrprnnnfW_|yqy 7F,je ^¦Rr^,7ْVP奈a?AZ؄ǁ|04 Պ Ȝ bӀdC &*y/ Z^%P ½&$1E;fB_N>ߩW_|uwWBRCu\4Z"Sdn}K=kp:pA%}aJH1fϳojɱK&6![ -Z?dDZlf ZYb2hft__#(4nSW#%UڶFS+6~2JHl5>PNDWЫQ cH!#td kCj+CEt%;$Y[Mt9*Fh^xi+-4tzE7NDS޾ !^`B(WX8S69S#r WPmmqc:q5k,KH8JZL%ֺQ8WA~Pso" \H 5L0Ҙ0\f)rQfWIЗXp* 0Bm9ƹ1^oM hGV}w{p߄%4̅4[x@Gf/9q5o#ڃHp,0,gD"52'*Xb*fnoS{d3 I|4&5%<6@is)f}Oah|Bm /4]ӆV?k``hw~s~%9#!H= @C:'w)i-kT< 4[nF\Q 1GJ!([nۆ# nZBPuhӲJeCeA$붞JjY.@ib55ulu:ȓ S{hJqCZYql=MUOb㑪8(Rb8 NիNw)Ki@Gl Gl\Ft(HC5]ϥuKNm;ۍ  j <RA+dp09^nzu~ ޛo.N=-qS-wZ;mP:V)JfVKjA."U^2+fxk(!k^Vz8'/y[W*("gc4vZZnxP*lq#vw= =b-hg*mF2-Z|Y*dJ|}j$!'(@!Tb)W-Xi ;* Ana ~P̥$Lfp%>)%P#I8J 7K6N \|SlNզT )=;>8yu>+jb @MG`Lb&<)wN u@ ɟN]̫ {Nuՙ!L+5 # xpj791`>AT$ IQ9.hd2\"RI> p>9˺8,%7"s!(&@/LšQ)AVRg@&b|ڈJ6ܤQM[F ZH_E֌v^o0ʴJLB|P9\&S4O a@FW6|\Sf+I/7ݢOZԊ_?>6_] X鉟p:ļ4a5*kyz`F ^Gm=9HU| 5jJсgpk ׌#);ٚF~<sRJ6|^Ndr31DM0o Tt"YT3^5G?zJ]!1Yo7pbNSl04.xOlQ64p6ZBIΗVcJ\5} {H R,dx(4=0`Jg Yl9xEIM>uẌRzSD&.Fw.J'"J E#ak2)5HcI3V[M4dFrKDŚ}N$~p(oQ?4dm2l1CvBMcU6 |+021(NXNhNG؟&%RƄ#m #C5i>aP 6 ͋g I"f'sW63*2{Ex4(z $|ϑ8 [10BfVr^$EZm"<{îqȶŦٚt2>BJp.u\,[(HrkfhDzr -YtqިBb}!l$ܛ+,tڏh ?i@(wpq\ FC]W⠐OFde /;;]NUtɁR&T o<Z`j)*! zǧGǻJP3K.>9uU n{`si*,FϒFu!! f[h} zDH jO#ffru-sP[=a8yMFV#5DQBs>K, (vpЪan`!BU.P:Ŕ  GՈM@ GMdB]^l.B6zpzbo.zwO_<q$eKZ| xPtvPWp:{v) ƣ!2 "f,PgK$eA F2% ô C6*X@gjy I@*i/M9b%܀H(TR9$xF-Eb@sh4XY6uAA\iTOҀJf  !KX6 ףJv T?٣nnvږ C/%q4F8d< 44qA Q2|% T&"+ 9ҜQd4F & iyyPH(Rl]I$\}Ы}℠\'=nbFGqs֯vlеAq"U=q5@RȐIh}Q`tլyӧ1uoR U"S-6(t`#-p' !;ɰR˼7!{)d)mC׀0b+OMlF sxd ʇf_Dm$-=qrW[7ROAL FW*&yl-A*X`-xzVu3j?$5b*6HH1 '=?^ QřKDŽ3ȜkkFZb%HJ0}7 0_S'&Avcr[kC#>M*=4rtQ98ds bhH,󇄍wdJf @G3uJ$yt]|LM#e[[{~zAT8Q뎞quD'bR&ި߫L"+W6l`nġ=:)AI~1t97L׸CPyºx ^=HDX lC*[ҫFSbH1!b9'7 B7!D*)f2̺dNr:ܫH e&*m"g2(eīV %"S׽q7X>#qއ[~ @L5 :͓aqmwݥV)l,i4a(DĄ9JbrKGD̯z7 J2Jng۳ϏOayVJ@pd@G1Z=/B *w:uحv݃C@6Lǟ|!E$R&({̳"-5ĄCu΃4%M#5pʨX5z Q$夤(€UƑY6:`:@+A5gKh("7DHGXtր$+|+F^ yY*hb6XI) 1d{cLI# Q<89FueI!V=`RK{ vTtՊ':?U&(2-ZԾW{8-ځ] 8-qm" ŠCMv?M )'kX`tv*`d(1莴ЋfV5~_ Ƣ̧'O ;Ixa̚@Hvʅ NUe3Gpھr %HDtn8J Onҏ$eoJBIfG)~런܌mfR&6h8#FM`7QTc1b 靡Q59h #EҞ#Z<Rs#@Jj9wkR"j f IkҐMv\9ۺӇ m@zl#_Xs{H=xt)@#+5: S\h`2`x\1b$1Ť(`N{il˧܋8l]T  ZF.v܌~Cg7RŃM~~֜' Lj1h|5 D#$mx/5yc QČiYT#oq&;e[ӴORv4z% P۪oVPnP7%c 2h vC9;ׅx&?&[dm ;$(}(/wR)UPDw-@..3|F Ԗf}E%MPۂNgu, 'y>A@{-)SOx4xUp(d~X(DRR6:C@ %ب(Y('ΰ2&B7G>, AAxFM=0v٬|NZ.:x* ZA7onPcb1I =X %t2kfy6Zŝn\E#PqI/q.*_D"l ןCX"G$|nKI1Ip`N z92Qe!J[,!b3`nIP~@<5fw5^pų_7߼W>iAS`?@d`V5^O!gB2VDY9DQzB(ǵzԩ5`9@G=* #:ZƩF(N) xLAo8؂[$˅ KR۬g;0HӸ~3bUZR Pa)!e2tG;Az 8͈Ae\Ԙ@9FMVJV`ŤXLZm`)T=hir>l 04 QEw2t"*&umP{4hף Uĝ!8aٰ,4vOV2=$Oh`к&e &A/X)ȑۡhЏ4Lmaq ƨ=DSnBE\-l l̩6#x|_6o|TlLZ-je"3!ȌĪټbmx\x<`rtR2LF!&uVYƈPOg#c:Ht{\ a:Mq/>8i܅b$Ty,=**92~13 6?EbjErhQUDV7=Lj怃J+c'6L%n 4ӓ̨sPICRpa#jV8Gs&u"(x̄#_Ge:=)&6gԷH U%XQ$Kā9:yO0"k?lEwC2?wN]rf ^ASJP#5S_#d[65N2r`ᐤRKGXJgY>99 , r*$"WbUKP%`^9 C|%EJEq/,‰I$|\ˊ`}{sݻ$ݙu9-VʡN,"!PeIr ]΋xht  h7+݃\Bl9Ϟ덛/>澏@O^gu vK $l H_zF̱]o;G.0P3WDЅQ:U>R| =N1J pr )^Qa0Đ%5%Fh PLJÛ7ggg ] 9ؒNѫÃr.723!&]p ̈R{ !pJGh :;w*Kd B70]]Ҫv$ Ck4U^U0񌐬bB5%Pn"0HMM `_sJJ0l_.Kɯ pU[J8|O6Cb҈ ؊P>벋"ªJ).5M%>%?فB;ijhfP&wݶ:&nJeJi[S&ߕT́I /Q5W-pv[&Ta@Cc.ʝُoSGs3wEY|FJ|("F ] I)Axdk[F=Ag qm":bht.HPJ 1p襖@gZVy3:@.6)6'GL*/xUH l0"S8ҙE&HnDh:ֶY<}tg'}sa1 {uɖ)Ѣ ؍XߺGRvp:!JlۍRR x kbgb PWSDF((EUNeHIZ؂}N~:Yݏ/C (^>T*ο2?B,%_smQ 5$IT |3%=҄I-P"8 pd 3Ar%5╡o$[] ASabAs p[AB`&|-1$.욽OIt]dn|%wf ȗKоٸmhp4Mi+AXScvx0(cD к)\ 6tWr)Ӈ]\ _tSv&Fp D!>.adL=uoo'HK lq_rckXJ.@mIHaƉ6):JI'3HSGwل4ESc|@ SzK*G4 DV6 /7|X"5Y=qW\O$#1G=I\qq2$'0v >2y~iv{0[qFjJ$Urxum:3 0mlX|,#\\rp*&hO򐒆P0#82KgEA?, /ɒM8HG՞hocṚ7WЧDԅ8c{r^|"HKz|P^*v/B% Ls(ذ+B8^L=Hf(`\xVLN;՗.m&^t>G؀/ѦSQB߁8/ } ڡJ}9f))|k#ehsF?WA9"Is x.H/ŶF2r=PM1gr#.{ xh(PDB/ ׈mGTTD):v$ )|5(w=4mǸ| TeK2f`BIK3p@' $*-szXV(`>zbe-v0IMIDyTjӣW/vPšөV_{zf,pL 9 q#ڠ8fHQ cD̋C A)qwe~p0g0t;_˳ןvӏPw `qS41H]x߀HY |\#GVjn}xpӅ 28}d%DàM[h,B\ cI|޻]$l.h`B@:#RT )ptvy^+7w &Wel[BnTnARB2&꜖ #YΖ7g|&;dX3zbZǒ 0*Y C(:7Y^Ea<"nBm07bg+;] Lq z`ʃDolU 1 $–L'#P1A- Z}"T\wZì5^@GvRSE- &e"%d3 %EWףCu9j5rS<(GUȰ7 Fm!*.)`X03%BREJRhB`U&ԆI]tO~U[DЅx n6]h%[Lu";ey Bi_L 1;di4;GXLY#W°SjDO̘VbKf*ir rȽ{nLT8R !l#Hч^=;4MhUaK] ܾ ;Bgnч_!` BEc% Gԙ *]GQlUxSr:lǒl1jc7lk ~S#I= cx(N}jn0!P<)H%d4 8#TLcؚTtF}빘LpQbN‘\L?9RƠ hHfz7rb?ѝ-xO~a5FO K9cLֶق.ῧ\`8p,"&cm6謴c"bk SD$+ KD %ӇL5f=Gw-`&9 ~Pmc>M-6H8K:WIvO+Rńd&e*aO<l`Җr$ŒѰi^iOri{Z7}oD tG/AasFvϩń;PtAOrchٞF@ڄRެnL#R7ڌ5VpړDxIGs#FHx ^<^63#g8$u# %S+niwe(a<r-u(5\}B 'SI06-x—ŏ P8/xO%ttot:NryekxۣrrTFCH=_87@Wj F9+ٰeD6$J&H=-~|V8]Vhq&B WX@h*0q.A5w6Ew֗/a[D4v{} A$6l 3~-mZ) ڸ_'1w `pNdA2@QF[- (@ M@gûZQ2CG;RU@ߘMC(иZ~B3'-DBXb%g) 8؅|}|#AC$//)A1ٹJ ΰ7 Obq}4{zF%J=Rk <.//߽;áW/xJTӁ JJ5ps]]Q*`aϏk9I׏.KB!EZ1LǓd8A?5*G]$y<9>: K5ۗs"FPÄlx5Xo*RXZj1G&HM3H 7Xf)AGl2OySf`W&a*"hp4Lh Y}q +`dob/0ݥ AV/9iG8n#.ԫ HTwwcg54D5a(\0QD 6M֧榋>O j?VEb<zg*lSU4`h2-X!h_JgR9q;݇RP+ N%&@wozV@!3uLdnvݻs3r@>< T앃3j**t2M3ɕeThwz";G0ޔ*Pྰ{, >tWn6 CNN`;@{1tVx}.8@?,qqq2]TܑIU*@4XBaE3a@=qq`elnuOϧKU/̢b{kC(97h4*;,},XX0B ȫt}Wn}h PL 6`ڪ#iJ P1t# /#8kF/!slYJ\θ93XƪuZbT3]Q`xafciY)#O'gD /z;-D[W+R(f ʎ%ӳP-UR x1zat ;YH5n|_lR >GUpgksmFu" UaB/S!tdlw>rw{HG)P+"=d\!Q`XQA ~ `FR#JAʈ y lHƒ3gTWe =my b q>--Ü&xI-2EY}606,ׁmaYt;K; Ɉj۞>X0MO;vds-/{o6)hɽ:H|c= h ?IĶ("Y kmh)@h Ⓛ,c[&mrJJ;V\Hi*P],%f3p l9}֭]M^T'iNpp&Qc57ƉA#ȵ\n(ű=c׶D2Ի1`8mDdrgYJ5a9,n*3s g:- .<6xIsvIQI'!Br#ќ}JvテkfC}%3 J!a+f7WF"?a^1\97k@[Z4./uRC֠[ Kn9GNC ;Ex4uL˾35;_v rRX,>8[Q=ب޺g]]Z#J1HKa/gG #ߖƑCָ3B< @ڊۡC0r& -y90gG%at,łQcjP#Gcx!PaB{bzW"(zڷt`4Y,t(`W(暳 gRUg?ΰ5Ac 2:~sb~Ź2ƆV++N-g ȪZ/CƎ۷WT|c5e׃bNk3UFF_(6QWbvj~eazn)'M ҐY`Y;UX;Nq?MN&jj8 OR@Ev[r܋/Z-WD%,ߔkșY. 2Tyī}!0I![Nx" gFILGYzˆXPn("%TXyBLP083AՀ\bZaR\a1)97Y!5 .pތG~;9:)@ 0~@!GD ^wd5gff+Q9A+XeV҃$sX ٫*) eHD;;i6<0h3V*Q,xl w |#8nuP(VťeJNPa˕Н!Q?1sqZ#f!+_IY WzylGEھ`7j3].cfj9zPl`%&&WKN !Ez|3V72(fN' mo q$ޏ7胰$?$8+uO+bߧՕ q__|J |rlzj#v(g8ðfoC$gEEx$~|#$7܏4hڏ$0@;$ +k&;22;?iS6 )En[)vBOٓ^_¯F"4`n@Qh @Cqߚl- A"O&x[ކ& E"Iz76P"d.C̸J 4dfuz:# ?ө9`Pti)—&I\ڑhqg[z)]Fs}e|}ɐ=XԥB؉br~dq'sz/S{?ple-ǟaݛٲyiakNI e MͰE+d+q]t"ɪX2%<}Gi%Z_rԚo׺C Nzr'FRCFbF08|>&$90p̗S ̛NgUBFV2u7spd[Lɘ" J2qpk.,!D"|˿=0KAfxR+pYDKLfhoV0E,vW]72#?Nπ'aŁ*ȋwԡCB .:tp`s`08XBihK 2(773Yű8O85`Fyh@ǛՅ[Wo-L7"FΏetH̛\e`& ~&kC Q}tuh`H`A,ЯlYi[ Q<)v:]X2"%y8;llф>"JXN~1dCgKA N5gnݹOP~n#`(<'X}(tb(9R%,0.Rye6Q>$ s)[I $uT%NR6-hG#w$8M܈##6!u!YL#U<#3ya8%F=5SCjDB3w5 ]!<&+O{D|i:a @ "I}1f+#,I6b@9:8ޯW!LP*LL"-A:|89>^ݿ@k$S6ZYjo`Y,"M7@_7{nY^l.A%<}r!r[...ܹ{kjfˢ4#q(Ǩx ]aZJP FИO50xT.IbgP| 2űS8%پ-25<3woqzYא<6QFgю@ړh~Z Wnw:AF+Ύ;;`dZR h:*)|rFK /bg2_u;Sq?/ D 5KcUO֨L⸨ACIt, nRD>(6)?XXhJD Fc& D{-5L<7&JĄ(,QN6&'PV+)8y$[!k@PU2){qPJ4r^ܐI!s?~g"h$p+kR9<VDRJ^^\[{QZÃCx#>9@K,q@R:T6Ӌvxg{}83T(56`d10HwN6J*zfu;WWfkSS0('|F{h` V\X0{d%_iB)Ka;$]-c6#M*ö%}@<K)-.7AK2vWp^N@b*nmsa H!d1Ƈߒ}K'$lK#N0P"m& ؾ!}ةÎx`8ؘQI$a0i+ Ue0J mJ@(r5IZKo5중(;*P~$meo~EYnEC}aR|WQn71q9Mb[, AjՍb!D5g}q&M ?[j;܅?Edr||INÔAdtb1D7.eYvpd*-/w7 @Sh!%fuڻAy"&0gWJR*6D6 kFU⭲`8>FVh=O1[$7FO~l%OXYerf Cyz' Gq&i%J7QqʁBǿ>U_m!w2sR '֖ . +ˤͭqmX@roh2>+°̃Pb͡kG |cEһ\'=\ tǭ!։eZ 4%™ǗH)) #6CNS 8hޤ1;@D2[J}:60s8: ɘK ._-Yݫxiۍ3]о%x#]Sxz|=8 I᭥hjǦ 8D(Cx14zs>ΫU,z==><[>BYZ0_kԕQP <٦IR?NgwwզgI̛7RWW&c?PJ6("1h`sck}}9o/-޹w1۬4j(*]&M"Ҕ("QqfhI1P-5q ?e!.l d؋ GkGrɅ~ frea0[ r#aEor;`~ #XG1ҥE bDt.y>j _'NsizH_5E&lZAddӃf@2̓}^DY,Ϡ#fOE=bpz v Hv 41-'H."P͖+E%FG?}d *?2I6A1GHox|cC~{xmmd8:EDW`b(hscccf(Qlob~ƅ} 0[wܹ|{9?t 6IPRrN`䋰6h59 $$z*o/ CX+c~6 /4TLe`y7.rb#9cmF&W}v ,aGbK*o׻>k$„g앍xHD3hxFA5Lɀr"QfZM5iU[Q,>HrS˲L ||A#lI'BG E O:.ODq^w} ?SHhyhsܝk)ĕj-*p 5[o%>b#Ҕ5g8Xʉ$ ~Wf 2D!ɴ$;lb^uMD 4I6!|rG֫6 %ǫ)yrt %^]I&{_:Eg{zC$h:0;btonL3T ɛmzڐaa@"R7͜|a}]Q-'ͭ!fgxVFDѡ1{Q< pr6`pЁb4n]Eܠl.]a|"Fzy;h[xz)cfdBӗ,aEs3ewdow8R,bd'9nZ5QI^nodck4W"M@BmH"]PX,(XhpN_tscpn`tzzfa~LG.NyOng8o=9]9hnmom=}?xIZwc (: 8T*8*!wpxUsPL-MCG밽x&\H[3ν qEw n߻}:@C伀}& Dӕ1J uwI52!#a7ՠ.Fc8O&`58ۍ⳼NCƠ6cf6!b7@4uvzjq&COйS'FP ̣G3&HFO:8Gم{=טla@k3V[֢ )68N‡ 2"ӀK84pF,+AKd2DKGeHB !;[H2 ! lKL aN!H1D&C \_Q2ZB2l#QX wimo 睽Vk0z'Výݑz8CTNBN1p_Nz{b8:8la?ff=矛_Y k AÔ\ @wC 2AR wbzINeR0f`+sR7U$㶱 ̎eV˥i4D2դ I m{;6CmCF|<=,.6/L=hw LބD2qgrYtϰf{X68Hk@qKȽ;4v7!J9O{I)s"dT22a™!nNȌ 4m,o/M"}b6C5yspBtR ȂHu 4 H.f17 ec]q׵v%..-ߘE\5C]1R*Eqk`skBr%6*\T1=UAҿ1DR w-D/u]T)evPp>|ܡ^ 8F  _)(aӿ9i][nh󠵱u }cc飝'G[V12:;V-U|N %q\{޻٥y9ϟQ $0@VonlwLbJOW€ʨ(PFKpHZEÆR }?M R`uuX. _Q,)v \?:Z}gebjEKEl1QT T8=[Xu,d=>re~ Tqzo,eLQ G]l1.m(M*FY!J@HPf ~2O6#@DRD qzi<[p9h7Q%6$bJA A .ri~ iܻrBfgfwf~n~9ӜD͍2&0x9PE >8kgwcV!ꂢ8G\BEYˌwaVcD P9{omTmTf@Ƚ =pA`n-#Zd'Ѩ/EŐ d;l<,<~˅&wc:aة}cD5Kˠ)U|L+pl+\rŐw 1?SH]~#e;IͿ l;heYAs ~+~m7&F/6T-+wPu_WaNQ9KJ 9fH60]68`TmG:Y61k=!fF3"k#Mܧben(.\MF.!w[lpbИȚEf2h˓d<#.#F\M~W5 ;<@\p#WG#DhfJ Wn>IѠ`I YűomfՉ_j\)j5i3]vYǑr[+y&yYh+Gݟqp١ fZvA_;<glsAZ wk%tلldQ4a6sv3̆gfk/zSr2N =7%yjGa@U46e{E 9őhǀCq1I79|ݖ54'Fm$5g 6kdE|Hitp}!^#))t> 0Hq$CG#vݜ-=Y;:om!89U07ׄZ;!4QTQqO|N{g]1tQ U =%UihhupNvs:=cm}ӧ;O`l=j[vsAjo&0d>ۘ.K~oc! _^|入%z k,yFL6˰Lxf˔NEfN@uǽ@a~kV c8Fť入x@K=c%RXb 0Uʀd)Iq\dJ Edj-n.z';@[UJ*8ai3:X,Ɂ NlN/ƪ[s͙Uh0> oמcBMdl71@س}!kdٽN80lw+Ġ8ro#l.,a0DÃA͚gCޔ;dK_FOT0ЄÔ/u|:2vȅW,3- 'QHK2}^|P&A`[C,k$ѶcU$n~#-%|\,TG` ?ABQ` `Vl!kNA5^#u u|uX:5_}6"^J\Lr&Rb !H=ip@L̨fu?/Hts GF|8l~2."w8-pf\y0E QtcTQ$g*Y<ꜜdy>QAk bvv^3N/ 8Cn,d Gp0W26aPk33pY?9pϿ;^Dxi2HS?rWoq(8Q֪(q"!:uaafinnрzȶk਽Vk^o'摫VEA^{~ZDHbKVr~Ζi $7>~?.b/_R(ۧ' )"#<0\P @O疖34@x/>)īr.x GN6had ,tl&ņD#R&NO{Гdsjo dU?0' !%  SI*EF4UOIDuzg3R>i7[`_]K3 1M{2y,qrH5B{9,fgQBIB8JR֫U1z^pbKȠ*:SƘR1-ړ'08OKS2t;:0e>uT8_$[!Ӆfv~+k#']w_AE5Qay&NGcͶo0 [;8x #aå}%6DI./{L Ii&6Twb|I! ܄+aovx8ެd CH$"Bv$3 bA]S.Ǻ}u%HeaS'o8&@ץe}ġ$8`qsPi.sJ`$"kHN&2&rBOt 00 \V,BNB }9?FhW\J96qhW':0L|:+l bfKc(g䪓Z9]3aؚgp4k5u%"!ݽwk>>׃??$dL{|,ʖr7.=7gXrv/J SNfhAFgs$s=5^~Udw\ 8Hv ckbasCqeT/~߼GY"'ʳw] r;778cbgR$)AFx@p;wB!UsqQV>Kp, ͂g YO>y㑃ĮI;QJK> qlAiCtKsl$-ig__ L{e\mk |B @Im[wL>+lav;x̱Ŏ%2 r|]TjQ@Ȭ%;{0# a:e2 Qss}4Yܽ:73 @r Ï 4iY{@qg "HcЬaPUAhᓍcGϑp '|?:VE N9&P&pupu 3g>(G889om<܂r@G Ņٗ^J p#Y;#l kVDrF(Y*nשּׂ:l#|<8?xqG'KS(LQ*np3T(&2NvB*),gw ;=fjVI|r3gf58 <|ǧgWWyLN(pIu QYՒă7/3pX-PQ?ᓵ7lx/?׿q^~m/<7nP>J@s4K,M>"  =9l=!>1,@.`0<RE$xE[7U9!4fVL>#8 t  "c84b3yĈwaNSUQBo\oaDaJF`# oz\3IdgI>N}A{YdbXpj6^1lÞcihh'&&N'' ~\20pI0Lp N}n\nm.K@Dlvmr5&g!Hv[n,5`fP/Tѹt$ v= !yKK|2Ch3Fj˰f2\FmhlJ!ww\-hF"S3A4!b;ndlbJh1=s֐2;kg.|bXQ D%N܌ BvӢ%qu{yPj~[ C 5i7f,;-6#46ɩWϼTͮe@Chh4jwnBN܆dNjzΆA~0K:4dlըcƾK,8=vgLC{(2P/d[mfL+hq@LN4?Tгδ52׼Z-C 2Bw ޿'Ep2uk8܏-rRзb9TNOOŒusS~t_f 4cG"AGevF+( v;i:_‘ }}cy]Q^x x Bt5(*SÉٓПmcDJ-OWCӦ)?2B3UaӖZv)|'WkZPPiO6?dXC𶈋4ŭ=ݨ) XJijO![s>\+D sqހg 8N.  ٚ Nv6DA&fFPK!搟nYTQ#GIאovvjavj~)AS0ӄ 8w$J u=jpkKP*V''Qל}ٳŠp~Q/^aev,6[>|tL@VJř奩fgcdS t:܂6ۜ\hY'q XȔς/SQrW@TxP%d>Bjb<Dlk$`㣓㓉ZQ6Ft*G-:%kyrZ{ȭɥ &L55D \QQrvv1}x3%-oQ9=E J]yJaCir z<ئm1D6eqb4ՃZ3ra^0%( B[LaCL`#t4U-5  F"~5P AҠ߁6"0B3SrH̊QA? ~ղo-`xV޻{kn֭F}KJxPm,g</Ǥb\=f I42J=u-OWGW8LP0neyA.76) 5_ö=OsL>u&9 9+7cu$OQ•D3QmL֚雛A09p0HǮvT"k I ]Zaxp_%YlX]uk/w1rS=~a\Ӕӷ: B܅Q1"פ# Y EȄ$7ֶN s}kkc-y3䗮< K]Μ ؑuDH.] y@$Or}Ds2O4Q#ah)9qq'|EJaވr/T:gk{H[έefgF{kʯV.hSgS PJSԑ*y (e ;.! 6tqVPafnFd Ȁ U82f>òCbRk =Lp"j5֧Q{(iĈԪU,0Iڿi(AB=N 4A\ue ƦYAd@P4IDAT* u$(aggq`3P F5+5':tz4"#T6v0SjFi| k}rr1 BvQL Ad789|ez}daBjn! (MK aŮibHwf/>RR  X z"E$!k Z%{QUsQo>V釽5,G/ru[My³>CbVj?^whݸh7Vo\Q7R {CaDMo9qv&J+vnw[Nœ̵GbkM5|i-9RC#;^ 6çfwH4sMrzGazϣ&QB2$wrO(Y+PaetKՍ= a2i%+ wHoGfxFr } hy|qTnhG[DN$[º[MQZo*֨i)CvL J#SDB +-U߫dgL:%8cpg7Đm0JXu'-; 8S'tQRBJ8uGn8_2Fيwr>b1#fMF6]B`'W9Yȋ1 !0eJ )̶f@zy {Wp D|f:Dc$e?m@ ^[4_`Â)ېPb0@XgZ^DG7h vB Ge雛z=XD ]0<1)- k ]ǤHw: |)zU̺dCՅx^ԁ4XBq5vQC=]{BJVy)(Ȅ8՜DW(q,EjzX@.18[1lhZÈ {xAC N2Yt&"J͔c9fha+OY)33g ANQ!wtOE7H#lY2F[ǻ'^Q0b~i{#=#,eYdYz=+ `ie 0KQp ' 8>>Fr`D6QB ]p}qdK4 S! [\SDuR+(A8> csufSSW9UD Qn9UQK,Oa8Ah#\L7J27y1(⺶v3܋je$zs4L p@)'Eo-f0.lq!2+(:tpv,*;TB48ATgedE~S{} 4y'5G2(VdBaFOmQ eăR3fkC'P)&Lh^f|9L/L̹-"ala0q`j'+4 wW|vA^=g1:vE]4NԀ~m:8K_]͛Fm[d ⶟6l4FD"#oCóe {uMNR+=TX0 0MKa\mFc9hdJ!ۜg8҆Jc} %nI0{vV=< nZ./F 0xB kF2IyWĜ2U=q0&} ζGQ/dg[S8F=rhe{ paP: 4g+׿A.U'wn]܅[JsJGO`#8 `N@ -wlD&|snvR%(`mxsyέ[wL6iPT<$  ([:0  AwL"˘TܴT{@ݨZ075լWa螏Gz@vD33M"*GE-28]h fM<1̠Q s[}@^\ZUSj5'C Vtuz8ii3Ej.-!aš0k!bx''vPNWx -xT+W"dt{#N rP $<_Ej:62!繥+3ӓ̊KTNA{kLa$2.'qr Н2$c2褳O>y~H1[.7Qel`P22,ZF@|)\8yYFde/Y$a.ANM`˕TDGJ hh;=ul{YeR1 3%M҆l.62XpJxE E`M|6kFX<[x:իGaW p|QA6* aCB^6#+_Cox1dcS77߫v.+߀*xv,J*]˳ لEcErRHXEhIDrD.&LAvʣHӤ^#;]q7`XTqGK:ubb72X.g̤ 2@DMhb%3B;RD9d*+ΖX (uM'e%aM/ 4g3VY@8sZx<24n@cAv(L. e{&%ʉvТKd__s%mqd4`R0T\޵Ӳh |_ A^6aηSS,uܵH0iZw_qz|Xga+A©+#HP@uڽM H9;lp% XyϓI4V-ٕPaCt>~ %=:(VJ pۆ;RC 7dc}ܨҀ~63 hXÅ8s/ +12?~}wRN,/޹!SS"Fv`l'0׊> 2D Lg3G`}Y&yBwfUQI^w0(~Ö-Wz{fz&+4"ǻ$)pzMLQt]Η-՝[|$lՅ6sX36Lq ziF&4%!tQ1{ec;M\ jRY8ңi#I{zԛ'1,u Gj $@}/nr JW T#iQ/ݚJ{`\ ^qړm&ge!#-2x1HP"Ioc=FB`4pYz!k2<~VtuUo]z; b.YwW/&cChK)-a.Rt uMGTЛӋIFķqC_F{m]ոHcbci0b7tkL o1#XG FVfKB'F'q_C+$*=%Pt G1 _WgԳ `K|6+gU'.MhOE֓Qf|G3RgB*f##rc y\}O9) QiB.p@.HeGO5rik.Ha68kA|GXv/ܾr3}6=(;b >?OpZҘZQ@hۍ{B1[mMd4 ^刱ax?[Fr o^FbD?;h}c/,̡uj,z!⽫O&d3lD'6'iih"؊d* LȂR!¯,{?|xHiX0cu'ȯAdF |8Jl[Sd)g[__tI*y-H':<Œ2_/&*SG vZ(,N#X`P,Bb[*h4.P8gލIJǃQ)+)s95čP[ӿE KHuFĜn0GkX*2:2@{*M@S{t><ݛmT4;:r i5$kTjЦ2YH813=20J5/ 1\٢n, u\j e 3<%Cf.Q=>| Yr ^3<7iGm.Y3mjPWɲ6("y/:xІY?;CZZPClKmwP m0&wv6Qd{smpګEm;x2=?[01D.c}dq'. 0xۨĊII̡]eqC܆thn}QOTZVoxv,a07w權FǑgoakdiAyč[_sЦhIK=uTwX6Ggoȏ5(ڽ~s\fVMhKl}|Yz绀g$rnmڜ&ؽMD?A([vn`Fp_H9(m!ft/L_)4<gGiJ#wArcS$ab$˗hDd]#'G@m&MfĘJi2̀<㌌>&Ü9j qhZ%'_ 7[_O!H"1-GJUf_89RNÞe`Y>(Wa;4dB?טQQ()Yo y,yXƅi%}/e>g_d {_O8ewȐcH}@.YI3aUzQI~LsI3<=OH"ӎ883`#]}7;IFT绩;c73HƔ>؂IЖ%ښ ;woIyC5Hg# Gry3Ed);;04Mu*őYQT {{#x\[Xax:5P[/Mq }$z p?kHum&wXݻn(iO.{k;8 %J-D_OnܩhJ>$3L爂-aR"y]; Ӆhdqk[\/V_%ʉ3Ռg>~ AjTfз.`:,;7 v Gwv$d-"!c9筷lr,H_XAj@Ne|9wV4r^)Tr29QnÁť۫ˈFB t1B$/>LPK\ N Ȧ4;{pEoey ŢIWv$PP0Z؆=z*}XǦ(dP>X@KS+sROA(hFVY*UINQ t}ruuuvia^fP$=pB/yXu؜&<Lj4f+ 6vFlvs^5tl{CxQ)S{8k)+=?=o;8S(ç,~apA9r"u4y#8B:O$\F~ 12gÞ2=?WkΌItNpos$dlyXfB۲r).Ia!JFNcڔ !՝^`( '*D&'d9!g@!mn`"-(AW0@RM -a~ ";-nJ28 [ҋJAKM*DZ $]G:raQjEPQFt@18!cܗoVĊ6 \ʕ4|~vnam xnY, 55l6~I_x/J06&O)T3e;hk0&}ŨMLBgmXFmQ+ÑtJ#θ]dnp+؎ ɼ8Gu:BJ<^X;:jHsn:94284 CYP:֧g{njz:uΝ{.Y.\vA㉲y]lvm>a-m-w{~Y; STuͦ\BtsD0٢h I.6[V8,_l*_ R~*]x& *OFO(dQ3E^2  ruPG`3iSy2wac\_kjY..,-,LjT xn gۻkhu`1{{{{ʃ/:ṕ]]dM"Wccb}^uluB;GxE>S*%X? Rtz.')M 3l5Sqy֒lV,T,FRFZ3Mh`3-)p:9y%p~mG=^]0hۿ Gdmh6/\VmXR}cLJQØcTpk` = &04Ѐ^~ &:t!!)0 btg[MP1@@jNXFm0(gk6`)|Z&5wg=a+`.XO2h/sQN|V?\`6۫0 J&ǥb,-Dq½[.0~Ƹ}uA]t%'`6Fh4,qRaL⚇XS,.=w/~>]f( |x;f)fw5i۩]n +31 ?HbSeQ|;vbdhua `a- v~+IYX^" xеM`HvH߱3bmF}bq'mU\GУc`^,AAmeik*3 ?yv >Pjȿ2ց9Pd&C̓+9˔U ,=/CG8ZpiX#\*}m3Y&i͚eWUqPkoc2jN5DEFě'pT`"z!aB#_A&%qh0|J'iHbt M) Vmm7;MHp/9BɊ#Q G4y.= qln2.%xƏ&wø}&N4&x.5 "kf[70&s@7{\+{  *PZ6P4;<߄ I0*W ǢEzYwvf9=>9tGp.0|m(8SQ 5TVq u{vh *:Niɣb:9 1Χ'P){67Go4 X_`kk8X]cTLjVר$RڡoS+n5E.zŅohfx0/z4ìS SJ1Ic=Kd}39;/ $V, p qJm}=cZ f"dk|O^{룧O!،WjM'P{?ӑÏ?~x9]]J_*n6&p`mg2 h9Ɨ 5U2C lY!}) c(u>A* (,-:i =%$dV4>25AFB#Ba LGgp8(67_BI*ʌ`7dH) hg{{UapuR/ggggf`1P I1wu9e$F9`vZcvf9b~_$+a)~7xXL)!4|i e/.P:aDp;CtsbF>k36. Yd$ hgO*3pbgaGRWAwުu˾3mG2siM2c4O3wxv.9>ba0lhޠn.Gn-U߂: y4iÁFnf!x7>pAg~R㲱"mP1epC_GT҅kr]*RTHn:D4]p!7YآɌ&7{۶L=>dsΫP?\4B6H;3`&;sTVl績 Y$4C0&M&ӎ;ѡnXTrp7W;ܐ Y><4\J0&SqGj`;Xn  h<`85"Х,,+i'ʬ!ƕHӠ_lfo?_Nm-/}\:qf9ed&gMÕG߇@h㻱% ]#K8% .܎ ֎N8 a'S6r F-=܆ _qT hCzۭtVdÅ{VP~+ i/>> ޟoޠCc?!|wMg!R]oj`v: '&JW7{ aQϮLUH㥉B@9g}/0 )k@0x{wWH&Y7!D^lf`(s}}  b9&؂4)2 %R*' 1Gj1"'zA=^y7cj4f'~Pisfrey1ڛIWBĺA5lըa2Q ڨ#yb";w^$-{3/^,`bdD:F#!g F pjNdp8=[e ˕'g;{?hUGF@}g=Cf|XOVY?Xo W>+})4_r`bDd=8>ª[^4({rns -yY\T„QɛF7 )f&(TzpB٭y6dAd-*g 'm-ξ0]=89=#CGc`;(=./ X{[<⟫L|fy _`;o{o}_*nI gtrÇon>^Vk""ncn7-v0 ?p.ʀ4cp@~a[sϔ8G=+lcBa2dDlxKlZn[;u6|}F!66Kh6ˈLf:tڪ9_vZ$ekx nc8"Њb\Q5cJZhG_d0K+1r7Y_W?>󠾉l_74SHܒ,82 I[6~B' .'dԣ ,2dpփ*?_];Q력svW_}ieeDu󓫅MrWqW_C#8C|;;;/u6~7!(GNĐl8Apt ~^{ߺG#D.#5vtr?C,!6P*97L _5^ =79]H*Ͽ|sS_`2P&k=G?Y~ wW^y(}~}T=P 0ao/.n}k_G \L ~_\ &*E _+旯k  OLf <⇛ҧX4 AS\ tIF =@Z_3/5OOl|ȢN[C|각ĜV[ض&Ҩ7~!}90x:7.yň>qUqzDöcRf1nﴵ(VKg`jɣ'{ ݝN|:xj߰8{v:A -|ZfKZhOԽ{ttz>ܑaiiQV+$*ȵO;ftqlkkkh6|g>3 F7ZuwOQFдbJĽ+m!jLwA ǩ-dm' sӃYfDk f`] ĮRGI929͐Kd+OE<]I(&X,MՉWשּׁLQF"SG otu;-4e=űWW{Oz6]/W'܆7B1pqln{W?ٹYl<%e!/!%*؊bFь!ܭ ɝe2:h}6e"xa w66QYK:8-* ǧmvf'GOS\ B³13Dpw/WjtPV'˷nO4z@ Wut:H5Jϙ&jVIl@U*CJ2 }@ GX3SM 3ʝ/F-|2P3c׫3h 6U%dUGj "`3XGu͍d酻Ӎ>\%`;<9:FKJ_YLbTY6պ~rrAhEmVo42 14-"^3cE9e0|WN5 c$Ltqrsz3}6K>.g#fWŽE3QxHPl}3; -mw~v'`R1qYjzIIi!Ve`TS kMM3–(&]l9".uq*]=38!xGwdy'бޥGOG/# c_EF {h`uAb_m/\FxOԖAE(͏/xy:_oj}歷~jq 'ggo6\.B7[ZZ@5oDjc(֜ܺ-a<ܝ/}7ǡ691l1 ;[1/Uf<| gu{hǦ2lt f:l`Q@˕R 0ݽw-x`?) x_LMO..]7`e~P>& ̽랂~{g_}͍ (/IairC No.G8lPo>`;YbEG^c NM()5>e/t~:7XpnC;oGP"V#`I3phNֳΙ4 n|ib~q9l(U:ڿbbahL=<ه { :), wd\̀u`wxtnp>wxqX*XfIy;mHETt*Nrq܏XL<ۅ7,(rJw}wkNpp2#8ޝk ~py*EӁ\뜬esE/~`5 zj`x BxJEnGRffI0$2- ?TrF{@(D[8J[NTDC71z=<:\ 1ҩ1(#qp҃<,- 8 625q0==*U*tO=A^ P^9l!ˑ(JtzR}XsWiͱUX|V de0ppSX9jp IS* U$hޱq KI#, ý3"Qi2O"h"T!fhG0Pn"&Bhr@l:g(IEVRxm$xi0gȕ%x43ɔ*JࢯJX?Yz0ݻwo߽rb>NbVQ$er V<ףEcHAc$H?jtl2&(NNvzXBbP4Y'ac| |ǩN<6I:);@72{؉ 8/"s/ōML:uT׷Ɛ 3#lL6kN>r}Ӳc3lpn .D6ؘo0N փ ?Ͱ=7vx}wy@Þ{ 1T+Nܾ /.=y¤6p1ASnAg <4 (7~~//BRii"ZU^:p%=j u}D*|C+>t/2G(qXgmJKNua.q?f2R*]:؃A݊mHFUE0j9lNXWUft)) I v┳'?7PTw[ ۱WyV5ԓ D`WcoդsiJ#֯LUq)އC|qiZ0_ 7}KxŠ^Fi$$^f@6>zGD*1+98L'$7ч"$Se/SY $ڋ{? m8RynSEb̟ih@8\ /uHDFxs h@<&L1< ֭5=54|UfԗahxyZin:/lUL~ԁ( !1sN^gjC@̄u] YtQ.0njSRIh1/0qW4$·|/!3u ]"oL~겎Zzր7ntPs%k4ǫMPO;4۫m;S]4f48@ 5p{nb[Ȩ؆Sҭ[n^Yr i*W4[`eUd@Ƌ8:I_-$QW>{pƆǕľӧdT,WPWBمMnxm{ "-|X.-6ʃӱ>R.`CCmxoAś_.яzPk/+D7~m۾g3)", ? 10ap H̴DD0g(" *:X0Hq8(=<8;PiBa}>>q:EM# fd2rj$>vNWW*(o w|4fuY<|;wV*$;α9]x3foѺu !2)FXlǤo p[9P]oW`g67;{Mc5FSW鶭tzXxÏh%aޖ:3y:=hw!KV_j.|ˆLLo<Di7#w> =A9E/I6ϚMƸď)k1!}b;ϴLN {O;pɵ=EX*2X]e<5?hFKۇ'{)RPw(' Dbd{s1Ж ܝ޾9$4%ďW dkL69`t0xLZ|vfH5 vdg ; 9ɏ:VQ)a{sǣչ[ӍB% {8:ztr;d-.)Gf*:L"I#)q: DQ$GuR//- 5jI$QYBKnnu3w+EJ߈f,6]%H12z4 lN$V,?" P%QS.(Y8 hctuzQa;:=:]TGZ" tp̓v G%dSlNW+ P ˑ2,oVEȇWTL-޻MVmˉG ($'!0<쇖Y" t R>e6{+h06i DLL S)&e4AR0|)v أ@ 6PFЖ5WfNdhK3 LR;H:KKsS3U7h ь4u  & [{o0g^{ȮNͩ>ЖDW@pVŚ韁aG b!b@+';[OQ䘔5o^ E5&KpU!UF_Z i}h²|}Xl1(FRSaMm׉tp8WyF|sP o3 ^zx[" vI;f$,p*؈;sy@W\4{>950 }aJɝL7(#/!9B3,3ҒBaqtE48TLYfv5#óAiv2{7bcM4U9q}aiky\ƸjS\̀|# SOh2EQXh&%4Kth()J Z%LIlƄKֆ  K'xOǚCr=>f-HJ10m4O CC2#m&#%2a"0ƬfíM#WO?z͊ipNP!<t!Z*KW62ahr:CʀC԰wVoP>1^wyqS I08z\g_ pazS'/Ƀ'$"=x`f x^%=ͣo :,C÷ I> p->%`xzM~`( Bie7CG(8n04w?_S.}+޼ћ*hd 8*ќwy5 39X]]9`{{yCmbO|5frlF?8eWؒmкԮk$;7CRw\ְS:?9m[;vȶ=F]> Ơ9>^CJҜE&pj.D{'P%:[Y]iy=Z6Jn85 ֐\%Ys `"4lZPMY8j9X8p^dOہ-jA˵s3ȘP:`Өi"ZqlaҜ\lU K8wruH!jL7O #eKM(-ѫ(MW,lW=aX,@87b1fӢEDFNw S4pAd?`NoNc467QWPSVU{A]H "e3sFq3tWz4r[h6ʝ;A ¥jR)iɪC4*'ŪhkFu}|WD G ,"}0!+#`QLr >U jEд`*Muh R`|ɛaLNX fLiz1`2eAoln'{^1h)X N%1# 5w&CTaA~%LBAذDݝ2^[o2+#xsd܀;MlVDa[|3g(&h. |WңMq_23>dw-ZƦX`ΝFZ[F3A5s;@oq8okcJ @r#"׎+q\ޣ N5uuSBm.gZ?_=3gbyL"3-SG -UݫG'PݜT'$vĢ.ݳ^}'p}ֽS˖³4d~t?cFD9y>D3oԏV()8m+~s7 K7#~iFI\(mݣ⺧ JY-=?sC@oa,/&o~wkT  ZC1,y:@߇-04sljޤ0^3k!me];iAHW!"δALt8EƸq䠜}v1=]@Rj`mG8ŗUCم;+s YT'5'ћIX^@YD}2+ĉDݠ \t0E|D{q~?J,WAįR!i&x!&iw[6;i)J2LβL,$mʤ[4 58_+Z> ߆AP-ݝnxpvBq15@@T T?)T``0Z0>zͷz'o~??ypΑQxsՎ0MӦ v3 (?4jgty- ,iɮ a1q.sxF((F<'I8z b!arx0cQ cL# (`yWRLJEZ@lB/BGctϺ=`\x=l4qܾwX/ j59SVٜOFF`%Skk'459zӧO>y'dߓʼnř;n{2TL jqT/UKFX+Gʅ2 2H+7&1_ȷp`=ـ9љ\ VI23DCy$b}NG5."rB1S.K?n+AdNK"@=CvPy O9r#Sp[uLv-ܢn5&jm`J*-> .6PMJ4roEj0BLid:c37Pǟ)l\͵ 7;}]1} "W jH] BCx@l;(5`)(>2!a.hA|"QD% =Ԛǁz0ʆl"KP4!4GazF)Yʳ jCq#]AG)+;q+!E.7/t uO~ejf %Q^Cy! ] !^ <).;w=;QL'(sG!#9$.6ơq?EJ|Uz^z:X"A[8~Y:$?/_{[3?!]+/?/Oz7|;}mz3lрYFEܣdF7oy(seynFr4( Hb^w"ԗ }G0gЫ!%>ڢ[DQ:U3T(#ue{RH]U$ۭ=x7RVbag}k'ָҘs. DU˷xoq(F&P<%[GN;9͡mL fiRX3VJƮeX /wޞ,.ϣAiUzՌdHFX(2ncr!0fat^F)A)<$ `\M0?YZJCCqw-$g||C=dxz {-[SL2;J@Iw\hǥZ 9=h ڢOkʠD-l}",Gx Iz0|DZz6r(HKNʹ6*+\K$$d6 lrVgLcpRzXC{D_xS e;53iGB^͈ά Zf/cIW2H .m+.. BFOٸ(̮ ]E'l&ÛQx Y6ͰRҦ|ю_sR> 95DuVπ[Q/pF6l?[4䥕gQE$#r@|ȑdlFi'䠕EG ;~} &_C/ccl(qߜ:}zިDu#ȩ#6fL ɢvw7Q?~'B?!OYIR8}DHW?,׀x'O7~ 0u9|ݻ7<Ǐa Qn2R /ܹj2Z j: 釷o/X b0Ʋ24Ht)(0!8ƻ@(R9؉$oOch@ C_ ?)VB1BolXP^`H/f9!;RJ`h@(i}J'\ #i;({Iuk9Û\UU(͠#aKiuVjE3ÁQgM"+[|F3aD(Xt:by a QN$ v!@s==+n^X\~J7Mdo  4:v:R,.̳jUVkN&Ft:cBH$ڃńN4]u0D}о,6袩BtCۮEC%a"ӼԠmETkV{`oi KX1>E|絛8@\) /e&={xÀs+n&zN-Lc05 K&Ey^V)^釥fj-^mI*Z7Y<}粨oVLK&30~)iE}+"KF& a⧝R U٥2}A࠱}xstYt1SjA"f!JiVI& pf+,`&cEǥ<)8`rrdӺb!hsbP (c^ldʝ#S0/PսRChN8>FGO?zGl>~Jht4a\<<7wk~[ /nZ]Y‰4LXfp:T"ȗ9SXnLQcLkIi\~Ν=}W?ʗ_׿dE_KkoFMXAH`G.R19BGzr!c"' ȉos+JHZWM:sl <#KrGӃa3 X2D '+.: I0! -G $AiQkww2Bj"`!#2(Z/2;9(^[D`>6nb63LI\8|(U xS  Ss^[`$ D(3B'*0) 7 \DBk4!`\@ăq-qɤ1qIVb oz:>/|"FqOCC$ 蠱6˜h2ȇs"|eL:۟ac#t1Ϙ)se$-O)dL&;yM Ebfo.: %Lhr.9Lf;4Ck>'$m4rnlM7*1K0)ž1ӧhэzPKԖb ]l>8ZGL8$Ss#c;8Đ -#xY@26`7s0H$%x)F vMAx١^!%¹XNx&RlΚ1A7ކi $ZLTIx~b\YOGra7'@JA'm?۹y$ u$|]|dDƪ>uk[IV_+o_B[_YyIP xxkgOLf5r8CJtvc7l4Ԝ=W p(*_lG"eG ~a&%{N27pxlgL?.p^sBИ5Jg-oUqvrM`䨽DO>}h`te{euir *LeuK3F*Hx%\,UhF3 f8JMNMcz8iobelht:͑AoYE;fOT<T*#9%3"?| <P.kbZїv>_8G%<; -Αn C?hPTY;R+d!2d$€XgHa&ҫ8T/o\7aCK xm>G>DJo (|ﹻp:PL:cg#}t|{キ3ƅ3榖gssra}`o3#D 5أR-#bW>˿KO׿Ͼ*|P5q{}|P$Rr7wkmT@)r8Lz;Cty_Y0J;{곥 G‚tZCYc1;GRqXP"'+h+|G5VP;Ӑ F!!A {85gu:88tp*Q.C0OtXbS?c P?m>, i)|+X/U3Վd̴_2B ZkǴW>.?>[ %MdRiOn_AYڵ͆;BKǚ krYWJ<ƚa:$x 6Dnى1ЦEbv°KG(]wڱK1 R3ʀiy4n8\LA ;27Cs&1iEoC|~4Yvs#3X_c0 ㎤5@u#P<<Fl&orPc AjA m ]] (I'q vEe?*&;?v C{9py6̬u%RB9d}oJX95'P\Qƣ*F:HP"mGX) iKmvtSx:\""`ߺ1 $Hq< ~fi\0,3!T I D"BfՕŕAD9")|T"(GkW"ta/pv,p0w,D۶ݙ,Hb#DDEkD)Ʃ[\5!~c@Qb36~>jU肼pݏQISWZ4Q:tʠ|8P C8Q /Je$C@ FJx{'g:ON#9v$ҫ^!Ud}ze&@j,Rh>jȢ\7b׿eܔW;:>T~GIJ}bk- wלּ=D,8Gsr%Ēq8#gY?`}c Jpq>ULg2Y,ηQl~ gH}$V&X;_3O+_wN\9oOotXCea.( wVܝm1CGk6"?t%P$vJ҂sscv|I4Zf,.PGZ]T@XD #։ij}FːbwƵ9SCe\p}dMTgPMBsffr4&PQ08+  1.Z,UWa<'JmJAG0#Dl6G^D9|=$ L$GL1'); 䕎[y.CHuȌ"YH \6,P| 6[r)ڻdJ##D2\k)0}BjF&ڂލ4xF*sWU`Sp0..]ₓ\.rcGif_(9^[)E#, [U68\i]n\))nٿtLD#%*?}hp $rϚuӵvQd u>P)  , gP"%~Kܶ!kKy## 6ڙ Oפ+3MPƊUpbtPJW<{WqKUh7N.A٢0cE@ 'Dt]>1+Xʳ^ȝnYcω φ \ +KQvқnC^z"-9/߳"YBd7oO,t\/}^~~R(<($6~a??>kdݹ)$OGH < DeXⴺ{'Ow;&ySgo2 y_7~_ʫ}oǯ=~mR6h*YDEǑkwe OS{Ålv %1%QBF"1hcVBٔVJ4nO`S8zLV(KMcLp2CU-raz.FGQTroN }I62d :>CNqs=ÝXEJb-qмyⲡMRp88#&Bc*뀕)~5N)b%X 2lxNIX@L*bS0y4`[335cXP |+qg ߛX<ظyy4Za;Ӯljf0 3zXfD&yUֲӳxxNI:ӂ%NI]&F`3EeR,=N¾Nm-Ϯ1\5~_5|Sح,=5f-&Hyl1F& AFsv=So3 PWQq9=@ʭۋSSeeOzjbmF12%* D(S[Th|L֭YtR,ȇ>P#8oon~kOi0{{wc(9ڇK=m4Uqk{Ï>yq4<ɹ)$z7"V']B8I2#?~moo!R_cJa εY3|_/=繓1~{0'O[GmIS$x 7Ah"q7^{I_ |q4.mgfC7E`|-ZV77V]#|bTAdydE7PXXPL]E\0G=pa@ R4*N蠅(;F%lWA)Pc b='"_h7`PZÖg6Ϟ\q 0jZUnJQ(yLꭕSӈ@sL )7AFbV.+{ZҪ0e}u1G_8޻ 5:+$3aϥHOvF S&`Q|^Pb>Q ~OۄJGApКRݑGOnTk?!Zn>&V N:]U sNb2l@]Dn" @(9,qo|u{ _AR5sg`988SR߇lQ#0 ߧoR&ƚ'MAQ ֞MMjxhPͪ@"|+Óq?a+e[qDش%8B`ùMZM,OO_g'6dm*vG)ڳ~@ ξ0!'d}&ȈD ) m${:E4DrD'OB_>(V0TJz5l^tPw/i7> y\>& pfCG[Y^WgӳfZ׿\2YJ3yjqJѮES{D* ȻôZSPS] 1 1tۊ[L],ZMw#K$ȜvGj H8ڻ9y[8vefϖÛgbm40H(3aWPeto@$h{24j|aO/<7\dP0H'Sk9W<쿾mDCE ~k} QLXGF0FЩ_ѻY(3Lj32}V⎔gJZHvZiʿ6OL<0VB|\R!o~.ɛ;> p^ h:L":nWd+ *J s?wzsV̝{N ,Fb#CKÝULv>z(,'krqQlMκpKx_riRbpΛo"~-#Phص=^ _/|_y/תU$x "mMAmqQ J}=0㓬خ-[k$5$10!_ e$`ωph',CdE,SЌMhM8"4썓5PSuba:|8\\*@ sj+m/)%3[#U؀w!fk#\ J[1jf1qe0E CPc)}·h'Cĩvy5?א)́,].ͺH' %9t4]˳~&rGU+/~>>;蝄; Үmh g$3"'p3mW=Pd6Ipa 7c@o7^գZuA{Cx$N;)˾܂qX ̀j7c2_*!d@UUg&|jbULO|Us`̢ -r@P 7(60.%GJA`Tr H _s/A^QTX.rnR Zgpqv-&5 Te#ar*&PB.,veó(1jXV1'y@(`0;5C %:F“4>DewQ3ձzA@9OFpjuh<ʤL =qF҈1i2qoZ/ d͚o\Ὦ.ӁN2#3Rl'$&l96+sS0daaa-qC=bSKhMD'DO_*q_dk >06Li_2^3ӕlm5~?’w1M7&Im%ÍBqf>1jBKMn%o6)#ONOk9]N;$0Z]qtm{P{W:) sY# #d/5~v= 12HFɊ :g_'$a+%'0'Uq)]u !v=Et`H¯!tŶ3Et%h1ӫ23N]OڛKP.ŗt/@&Eoʪ -L6"А7f &?3ܿ w&V%0io&c4w]pnk_\tu* Szˡ`yӾtĴ6T="l6AipgrrLd "IkA$ pU+8)ӓL22@J~'/py IW\/(Ғ,'Hz݋ք_, O ᒀswՂ2Bn(6+0bFߢxe-Nll`L0mVV1C4O  4 g8ʯ{]bx; )C{;06ʹz>4aL@ߎࡍS\$!(!bг swelV H/9V3nf$ V!|h B 30%ѩD6`Ƕ> &j&NĬ7nfTN-4I9!i1e$.@EL(􇇧g/ϮJ~㷿Q[\D@dD@iي}r8.[ wv]#_X)fP+!͡4$')/`}e^ks~E(Yd'&q>aa`}{guyDszr3s9'`L[&sdqeg8YOLmӻiN jL%e *\^jU[Uf9[XED(0.֊s(ZbtmSۭ& [I!&djeq\\ :䬦fZ?܀ZWSD^fY00PVP%OY/\v#^,8!n0 Lo|Q|)𡂩TB^3 ff;tvN@lȁ8 u/cO dҰP_[dՕ lk-K6M“;R~ L7xEGnFrBbÝ%)rpGSIrWHB=lOLү{ ,!ȆtJ#˦@mZ~Jw / n\+{D|ǝ)`幆$78O,g!=Fт&}j#&*՜?kUyJđRH?;vō A sr֮4n''Qq!$5ȄH#Ev8]ǁf FؖNf+{'#kb"00\I}&"ڋ"hJl2 a|u %Ԏs6-EYFt2ED8Sx1&ۙV}+bVC8.h1#cCQ+1rWPҢ F?ƘYľ1z@0c$<>mu0|Km|uwtzrV5!~_ɟZwlJՙY2n8S \ û+:ãSdl@E|67R*4jk|Pz [8`G*ZeQ[-sh~Ѣ,)s\i8Pa9TޙByl=}4S??>9B6#֥&;%_\^ ԄEO)k|ɮ{n%*B ý82.KMImGՙEH(dzyP z"?0a!R\\wHi(`hfX y$6.S320g''/^#PR [3gz9?,8 P-R 4(/EbT؞gmcäh 8nGg8ia`4*\9+W@ѷ6Rr}-H@?f|Zz U'N3"f\[Bs4c!yI ֝bיjtm@f0$f+P>dY`j<|x ֣l%auUљ̅6ǿ20r:vT&wʯdtA-N;yJ`@:s';4$)##݆qm RJ ($c 2Ln6iW 9ߊWMc*eϔ):/5t87~ UG/?GPí{7~ܷܟOoj@Xz)Yt2e2ХpZ㳃CPB҄~Z8iJHaMǺ_(+_i)[&C/H^HWqq=lPN~ړ|cyyMV/O,(MLcS,?Y)x,ҔpkK07ML+$-7 3NfWJHx 织3",b=XYi"m*2Cҭd6$;{A.rkB+-6l[vOhW\ɄXoԄZ1K(A˵b0ݜ(jБ?Z`Q * PonX"#$rT-R ~H;[|K~3@ZɒP; ]lZ(jЌoFL 18Plǚaw;vmݺn[@ Yܪޟ b'''gΫSTpnj(.HjrꞜ]^wqAثX-,Pϗ*<`"]U+W*-4 PIiR93#*gȮ9bF皷%3\h&^/δyaA(b/#ja={/?<Zܺ\ches_N;haCޗ_"g>X-Ld~oK7H;9`!鍠{Ѯת[*–g{g__uۋiwtz'qw`|6AF N:N_㏟=bjCYKڲpEk*% ZnC#4A/Wy?kXx_7V*HFXR39`1 l&EJVTNkwWI$s3B-;d$Fcˣ ̃u  #}|go Grieu^cp zP LF9cWCؙ*|5.M˸8źHQZ'nLksz1_# n>̙•K"Z $!4GRF\(gWza.|:`r)hCL$]V$}"0@. kpƕľpb vylpܕԂD!Qڳ& ɇXov-)7a3hvdtvK]ʍȳq w#oh;:r_A-!д!/ғSeJk牼zK#X>R%Xkj>4Ӑ^Sf'?~|oV習1xkCz6_P 0:}ÊU=/w,ɨHD< 6g5uz:u\Vs5b<}{֋٫\*O}5@ :ByC7.䞯z?GPGgYo< nS8<}|~k߈_"AAqai hq":o.Bo po A'[kk+ZUj_@wu[2t`A5SA0aq`fse8^?a5i}C";@34 :\.6W=HBq(:K^. ο'ַٟ޷~⽯k-d\@Ns"5iP0<:5O܇MHя>CVi#MÀ:X t\b.1g]QT#w&{VA9jE]Tۑx]]n%C5+8BuП;|gj-_A nEl'Pjیf\#9Wu p6o+iϴHn /./a=!=8߿>?EL;F}7LFYs[u82js"a<\\qzp|~݃= R9R_Z熖N#B-ۙ`YV76Պ\lH~@ӖGw&E-JbgdcC)H07 kk htn7/|rzq~zyF}aaͧgp|+euu}Ui/Ώ/NP׹lz K+ݏc!ї (`ׁ%r uNx;Xm6חW{Cnpxrϒ.5 C;)v`'nA q7kt}q/NP"1B43B" LiQ~9%㛰K`W_[{2RHʯAZfgPjn A;Xn6[V u'̺ uz0ƞCEJh LiS왰 rvf(Y w5N: '?M]%UMU8;BP|_J,Z&g R!'t @dx<"fM٦ r;cЍ$Q$Ƙ߀!] 8 ]MV"&Ȁ " 4@*`wo k-@"D%}h/ÿQ[xܶ>4aTHȉ&M3P,Pq&B2L0b$5Vض_@ /o]b$HPF`30>ݹ/Tû;T'/D"bO|JʸEE00|hhȠքcx<'@ 7~7!%xu`h89ҙn 8;[[O]hpԊuBX=SM?x=|yqs%ON;p^ N%2PXL eE0dzeJ2^4ar%|@HRxY^@r$ooYO\zXƐ\zD=鱊CFHJ-aGDg21Ve?QĢF F2k4( uiuemlVȈTC;XhA.! n[=Zjyf˗`׼Z U\ 8qۺ<8A*nxJE@X2m;r6 [/ hMhtKŹ<*#(JhƗuXuXJKp GfLjky.KǏ+/MhXP؆Q: z| i\=9~^܀m7Y CJ QI}dz{<3QacC7k da&( ی * n9 '4qYi!CM*`Qˤ^">L81e0*v}j(|0@!KpOws.I")$X OGN=h'!ӯ=EY' MU% }5#lLH`9W&Μ8;uE\!nE#]MįԜ;@4ņvk55m! id\pqhvә7$%F[F}A/S~𓦂3 Ƨ?a 7n9 %br$ )!V#3cCw&z#<ᇟujhz?Bǿdp }yMOrC0V6z$ٿw`uu鍳5Ů|nO |\o,d*я>}}Tlakek)bMnhIl/o>AAC?w~៘w(#A%TYyR$q_l;գdovxţX [`YarFgkXȚҟ/?/pw3I& 4أ7aR&W)aߥs x Fr3ŌU; #SUӄ۬eY67@8(pu=hG\nZ¹(1="5hST[B)"qlLG5, !M7{rKZ8V1mǬp+")~u# O{r˳+Xvw^x?~ᇟ}1j`·"W]F |3WX/vE xg!XVEr &cŁ<--;yHb3xmRACuLz3vo:ݰgܘl\1Y|.]a`zѢ:t.suWeBlthGC0pެV0y3lC)$;d䬂*區X(vk4-5/m,(FWWitADy}~z}z>=A@$WNkm)s}wlLCLwЇU F1K;J0E !LUʕ,+6GǗ"@ h@$E>7s0 (!rS o˳6)p?B )|JjV N)a*g00ą 0D2]>Ks!{T_%' 'e/X@ǎGA.% @Go V3+@A&wGC CqD<1`c#TX@^2zH g#0r̓_r7`4X4OR!sH.fw{wz筧o=mesqqM|syg&'^a,"P^͕74'fKJT sVo4F% ?g~__]\)W?L*'t3{p$iPcuqݶʔ9T,nT-W*ե~jk啥R,/5KE^kJ5_*c^Q)q /g&L E! ]敢jAlRSV@FmePFC3ho=y'wཷz6[oo=œ?Ï_La:h9;\=~ddcsZPy%2"VG .X!pc}}ii^|˝H}bU]nr, ªv理R D0#"gw MۨaihTnE, #y^x%ezsBZKdIe|*fk(EDowl"=;5ޡ& 5,p8(e:;7 6t(EP*>1fn:sBpM x?7LlޔG_@&_cw] .z.@HO;T&&,K4rbLז+`D+Ic.@Rީ Z\kS]JupW3$'\/O$FqfU/F|EiBsʲ:aœEAC4Ab9) ;!ۜ>j 8;@Z-@+G}5 Zv ް5\ L.x-Ǐ=.dN5; k܁QXbAN(2TN1s2i0F&We"@Hk74>hx \ь71̀t'njдsQW|6hDsi_֚_ _e[A(~6G3$ђ?<9!5 xBaV`ح ?fA:^_5M ?U\&3@N_HM3Y5l_C'G]鐋ഹx^h<{2$1^Rzf$\Äx t8^F޹ჭ7?&-!nBƆ\8lc5 9tb( `9z0,[tzcyhr4]*hdL5j."GB',G#;bn qcO-_5)?',Ru6*loal }_=})$pڀ,XA4}d ˙.m6hyuNtW=6D[>dd ek~Zln]|绿?;*QD3}GҼ=[kUt fgAѦ5;?/_s⊤ֱ|2n7O($sO4~?Μ +?''0|A>ū}6+8zćd|/^Knq{r??ݽ#3XBMK`<ڈр_Hdɱ0`򙸣q<"1&/K~-pPN 2{D~8ͿdAjB^?u<8{kn8swB{jK~#,"^o8^Lv0߶pUN;eCNbtm;?pcߘR I@].4ܷ*%> 1f piʩ–V^-W Mu"5}r7t˷UԌdS|[YrBGDEdg }ip4 $t OVY\joo?zkOy[G[o6!Lb͎͙ˍ.b:7:%2ȜJ1eW[IRݍǨx]VEaƟwd yX57J%u\~iH͛ 1)!i@4W| r8>EG։/Tt]w $,?JppCt2L%9l[ S=!F0 9O^fxqMQ}];ݿ1r#zXA@G*s{8[a&Ee:pgfjFȰzmk²:Äŋ?s$ @_/C ϵh/D+KA{HXqϑ0RͿ{G } Ms')=w& l?v\gGQmo(: ~pE#)EldX'2߿sҏ$|#)//lp;=Çըk_t"7w9!t¬32}J' ePT ZaZ+ˁ.+waED# /otp\ 3[D/~Pqo×/<E܋;_Y'\ԎDɭ^Ag<ӕ86PfO3_t-"8'eB@Z_{^u4@^P:඀tZ\` J ӧo6j0@E' <{`B x+Y rG)9XPp @O}Tnz7sL{<۽'a(0n ;7Ej@~hƋ}}i0 ̋BP#mmjdpAm&.$i \vc O= Y r>_,cӃep X kX-Vƅh6j9iO4c@H߰ڨXZ+ř1ДDFvrPh+ST\BAmxb/>rwe?NQâtGggⓏ?{Y[*|-͒`7=R̥U[ɌA9]ٲN/ 0MydiE'llԫM͒+ ǗdMDyRcӥ 2LV'vhVK?13AZlgBXFԦF;ݡ}aU&&`Z$W_Hgv3GxU$lzd[fhHn2fc"*W,MfbmHf3wrs4eJxϮ-q0-@Rī@MOt'sh<em)ö04웈0 ?Dqd_H :b`# 2 '4w:M/h'P ϓzo _ᰈ R#ӦB"Z(}V56+=oYvGuxz@5_/r9oF8\Spw;C1AَB]^(z4ٰ?B~o|k[_[׫_JkPn4R{0܃:rɪۋCT'|{(RT.!<#/ 1([8͹[A??N8Tj??ZͪNIpjoG~J-b . .y8kGgp:?>V"FɊ>`a| 7/-6GK( G|{D2}m\!~thG3-Yth7tKsS0cSXWoHq # Dȣ*_."z _i# ]D蟆8*P`vo%o! ?Q9G IKeGGV.d#byU_qpxK.2͑>>@dm.% Et= R)U+x*0 ]\Rؤ6Gt]&Wѫ3M]45uG"?OJJ5_Wb  s%Alv?t^z7 ao6ZQ?׉1GNuo0rU@W  1j ~0<8C%B.qvcX'YѐT<#Ngc{kb}xBԏWkX4/^4A >R ƽΌh=EE_{\X+;tl"i"Ês0룭<֥iVD&'4OxbԎ-W,>!q5+D1>>zu[T>"{|ѓ{joY&3v0Bn=P4kt4δί>㓃ݕrj:e:% $ aʡ&q=Yɉ/V_ֽR.jw^t>|yQ?GX[ \_Q XfZdkabH1R?ݫVP5BsG[u*F`p$IE'|{X#| }$ݒ{dzSR) |TXgze -w@:>!k \| V`:"AmhFcP}}GB_$6$X zLh `Ǜ5t Ns B@mQ5v39@FӇ0|1z eKԪS hl3~)9]P?E=`S 6#drL> HX} LsSes!P)( !Zd1 "SHF:2(xW佈(-T^@ĢPjʫ ]LXgA,_RdXHaӯ Z@ׇTA(&t||)ʤSRJ ,søGA[aEӸL6XD) BG0PTM wpNp̖K NOj.h|y?53w,B]w>ƥA˛/1lCchV"~tnf mUz4cDXz8tT!ׂq- N JFN K 2qFPժCKxK܁OuSH.ҵLwjvS8 >wX@;F 0[23oʿ7R%!H& %+" +4,;D!Wi4DFCDdLv'pmM')݂ܙ戀+ӓҸTqn wjy>kJ?yZultXN J . Xfo0MzP5x=Ӧ$Jʀv^593 rFM/܁-QX@ѓ /l=~F90GSavKAl:%\`h8z񧧇 PGE4E` X;8'B C5׾>;9˗nsyǏZ[iTHtO>g~{u77wgs`&eW̨O~p7;~kcGՀ|qyh6}ln,pq@JYYt"<(o)$[lxkhlZM]kbgm's`qv{ݘyF, +?E4O /EzE55RMxjFo%Eɋ|Gؔ])!M/JW,-:LDpRR5.Dp3x` dZae">_}';/>(7Ţ7;l[s1! xڇG U&]Tҧ4LgUKX2^\ܼI %:q# ,n&jc͓Z<1lqÃ10Q_Xϡ_v(7'{A@z:;4)bى0 G Ö+wr*v5IJ>l"`C $؉o:7Mh$@2݄C`IӓC.R~dB+l5hڡ ,=&d/N8 < $)whrHF ''@&8c:bk}bB4nk"]wsr9=N1.Xd@MZ^8"uMqC?=Rn_~;.A= R I-)KVftm !WONqv}݆fTE@ϡ" Pr۰c6S|82DL-A=xv Y[=$;t=*4`xKRt!ʺ@?e:S5>N8D4d fkgA(oKk<))|mq,}gu?%F-fwD f `"s&k`:&DV<bELv ^Am="l)c#[/KʸބrƱ8IHكql+?9l)\A&OCcMfug҉9V =;޼Q:~./FH*:$)`3J' hx/]96ɾRoh&uVMBM8.xD&p c 4Qr%*X6Dz} mӔDBS=ZZr1[iD< kuJq>a -zw n<0AK5 YqZQ5P,e1ANc9; >RE dCvhdMNWe3N]|1,*"C#+ $J\Z*qUnoP*8 匿ˋJ AFAJi11C*iLΏ;fvj/H4lR54\Xsdk# Kߓf]v/c=)WyElHBZ,. tKLd.e8t,aL)ƂyK5rz*[ ff~Փ/O[N2Z>qk{̞b ô [Ps$'S [ Й1(b;P HӱlE_`|e1akrSG\ ZyϱI6 SgT+z45$rRe! 0#s昊kwcRX% Ȳޗ3 18IXJtbQ.[;GםfX)mV5';hv&i@nR(8. sA:u{w{r~b!jqfep}e}892a똰eP+P9Q\9T.R BÈ4xk+9&fp|]wbnZ-x:n~vneiqie\G\m߽h#IÉ5XĔhĢN47F@lH9^X[yxk0 Y^a0(LrXܶ4WY2˝L> =(LyTf\\]P&htKoOģ[;A,jc \C.QnKqQ>Q`~I8b?pTàȵ]~I@[kwi~}׈ܸ#qUOh$ ]>{H:}MԼ|$`8:d-:#-"H伱/t:~,ҁHu=IwqWtpJ+ ND&J%oba)'sCI,qAjȐDqO}x$[_g#cM G6 .u( j J2"{q/"5D@ K`jėq];8jszJqW#qPhΡ[-StdwsI{aEjLAP}p-4G &4WF1Q%B15L! -R 7'3Rm)y9z2]uZ#6c^i6;:3HSu\ɓ/;u5 gf,@6!}r G(˝{qK剳i9w/0!"XM3 9i_\X jErUFXbj+x7D4_nHn-.ϒB_$66iuC Bij5#Cg @ftB-F1Yج_G:L㤢q OfX6 $MPEXm)4޽BӄmB(ѥiɫj #2 [N@K {neh?qLЈO&x5G;ih$yv(266`=Y=ʖ:)u;9wOOGTtO5)G-Z9=%oDF7:HQ%k_D?S,tP/#UK}_sԱdNτT5yIT$Z)z%=^*eh#EҘbb%9{oO+\.q89=ȠMvZˋN~($Xm41]hQzB&@編hu[7b'Ѫf[o3p)WTJդz\@BhecR" 2D"#@ԺqލS3 h,mnԪ(gwY c:OON6)_@lVR )nx2q۵x 7Š-X“2nB k2Lc# GNHa6!czڂMɦgbQ>t}>?<99\l-{*~a7W5Ԑ7R Ɍ .'GއgTwXdۃo3ofzrty}f[oǸi Q"y {Z+HOTG΁fNReq /^^!iGru SRT,3`z"FeYyEvYN %>Ih}G(JY TEw2b#QT߉8jp%Wy[Pdb:NH{yuT} c acO8 X"$~>=>?8h`QΎO/.'2 <6L@Z/MjC勘ELDÔ`Ib%ۺB>y_!/e``%Ie6K̴_BSL0GBpvafˊZ bÎUЪ]嶚X@40=ߊK$۷qߡ]ɶDoȦe(6܆sPW4 ]vR{cQ6@" ;) DkY&BI84YdQ F8Mjp_|ܹ:NUGO<Ξ O|k!O0l9fRl boXDQdҩ9`/;5EQ )vb*b.>8uEl_ϔX2Q АO@DZG@ֳj":MS?^q8 3OqYݥ lv2GC"4OS!S;թezAh*;S<8rHve]g>M@z;UK?2AMD`,Lv%9$~+ R԰-rB z>ŰÉ7hHMr?:Te^f!5o}k]|jHھR2Y8P2Gb 5r92CxAG4w9ɊD8|BbHow/ft x^ٸ,5]>@(R4FG.wEp 4O{A4 6l*.3!Ɛ'H$DD65G{, ՞@d?{8d?ta$<a8?nwȨ$抿{G^ $,M @n`B# Y2ުD Buqs[nW]؊R Ȕ7:}Bn7.KOP)6~gnN.?P0[s7ό +;n,@Tq-j/B F_^0o}'*}u}sia&e+qk|DzbC!09xHsi /L~6Kt$k3΢=d8;=AR 75SڅRSyrPZ`<8Kέ@L,H[ N=̺IX`(2=&5SvIk-෢uAS0@kVr~ሁHdzc%NA R|QT1''mg4o.=' \PAWxia KK+/4x5P:sP-N Gku;;/_=EfǕOoblp3b L&p[qsЯ//G';{'ǰz)bIӟ,'׻/^v.Կ\+#b543{T-mӃ剧ʉ z:^j&8SR Hƭ -|ɇA @#wNEzqWGJPpf@]ULv>Qk RlGpR-j vn,FY?(E ́Eoqbr246"읋jh- Z D.0|X fwku/tvG';]d88;<><"m_!$Ҡ Ii~^U :/̧]*\!?){uUݿl8z\W;Ӕ+>*鳴|8>i #uHیF[)v9oI2q2ǬV 5ڞnͅ0.v ӼӖ13U 3>Dj䭉>Mi7MhKhfNb. ]Gs;cBwFQeA-BFҀ_i319i[AL2f=NN2C201 >,"4;Hh?ƱfNxk*u$Ȭ'=mK] -:F7ٮb^x1Ï9iJK]/-14 &L^1@MxaG;< &hO82NUY٢z a\)'kاz Fmd-1Txti*Pnb`h8^) 3ȏRn'fb`B??{~oJR208 (ts^FP[Zl.BN%+rLH>ZQ7S2 |D~MEZ&YklY(l ԌQ͓9aN";uw` ]d3ҥ j8gUg|Ä-:JOZs` L";Y,z.uC IuT:{h⩿QȕFNeؤcܩMv0IW݊HA+ 0`’I"'EM1)iʼnpER\ݒp9AS͂A'28h_,؇PB^ Hѧ5HݥȜS _mF:;qTiQ"gq$+ qgpD$,7qC5d^_}%Xi;ƿBpÕtz.~LA6ǁ"p\( ǡ-6ӫXi~8:?>>8:Ԧ=2c):-C+\Z mԨaFyY'A`A9nM| `hw@ x %s ظjw/Iʲ iF& }qxΓ̇ɏ\rx-G&LZZH.h:&dLIeKs VC >/R% B>bfҼI6 cl>iO:d% 0PkۀvJ'[04%hqcTx ,: v *6a<ZKtnch+?ʆ;jI(ZuQZ; 56HW6(3 #a >M~eNaGD2# .gP*ْ,51d1 |4G%w -C6l2IA|"݉ ;沁gBHSsZn^ޡ)FQMrч1uf踕 0Pr5d]Ϟ`@]7wWcʼ@gĒ]03"JҦx *5KGĜ84Əqr͂A ̀u![rlvN_Z(@f܂R >!#MӟfGYzruFsxs|0$՚,Acy[3|d95.1 p(CͅcQӒ$)E/[8aw8\;zn4P`    lQ5$ n5( Y dN Nn l"mVinQ>i#0dv2+H \m:L!+z"5p_7l4H!X-KyK!5!/dx0ѐ$#o{YR'3ش? " 7ɧQ4Mfh ʠA^^_]Z[[reGVAaPv8F^Vy{r:%QZ湛sk_wnM7 |yX( G>OTnrXBdxV0sIsA'.P4~,0Ywq8`I!0RAvvAEiX [ 8. n_~sLۈ51@?0A0 [  0!&6 ҝ\\/oV!cdq<6 sbm}Ҭ/+ g:`~Cp/\8 sYfP(jKpʁF -L|rXՠ`,q*y YKΗ_/C+pa6Jrsks;H݅=p=FZ1} .DH 3-pqL cZN_h㌪.h /!cz?CDej/ڠqY'I ~=7 '8U+Gpӱ;'*|/0"X@F{yJ~OFtL GE*aPT*e _ '"h> 5c.OPsJ ``0vO9+'vk8t8RL1 whY4m8V5$Ȝ"Òa;rZ0DgfTi!.o;[|z:*#qEp0k(9}HV=}`nr2XAƃ!?Wb<Ψ3R< `HiBl~搃Jt9!: bXSqrɢ,e E$E`7Qp&>޸u)B`K<v~ga>0TgfᛁTshVjpU(-UX. 2K(YS 6( vdj.t3 `Ԅ!ciFƂ% {` Y-hRf``Ri>,9ļ&M[AE!)/BUA;IwOiGhN4tBG08zS+rAیd(.4¼GtMqdў_3JsŮn@v 0#dƀ ri,|5g/6*C}R^ER6b3DiOS F!w'Gx 9Q;u&,\ 'eStj ^x@ty;x1 $eSNy7pG^] {"&= V+-HgjM!f'Ɠѐ7sš^Q.OtZ2FHiE:J >kG0OѦ)a-;؍SL_W|R]\Ǡ&E{ujFjO8FX~i{J8@\GIF ⒣*؀rZt*EVVY@*LVz)8PwMU⻙E|lYP/+Iؤ^dxoxOyٳENG΀h Z~8<>呂2OVLkBQ>$Vč ,fCi^8KykF²@/V-vs*AClp>\{N@Eė#e偤%ڣ6G!>"xbȾKǡ!W J6bBQ|ȥu*lXl9 jķ{/2fsJ0kH+}!1F(.qx%;30cp-Z5}7(w"PDjM7oϰ?\-"NqM_^OO{n,!/[FK@pt\FJ<LxT8MFpL(.FQCz EQn&N͹zh}Ɠ, cLi~0#40ToIQcjZc݄F@}qt((Vᘔxh>0\ŃdhF<).q<6'd*0YWJP@AZDjhR´2@^ )NPAHp@Տ-[u c,@zm7p@y P@vӻtofPaaaJB𵴕.ֆѕp/%W:r7[0uT Ll4%XD "bl+e`2l! d"eBv`8;>bUwܹDIwy~3D|6Xp A pWBfH//_-xnᚏf0 qn>L (قfCqrH1S]h|H-!#D=,b5:YSx(# cG|~6^.!d2ZP#5f\1ǃѠn ŵZQD乜#})t@(EjCxߛ+./ी3.^"/Oڭ p%2*>   b[e2V!K/aD1VBK̻bZ>lfi`"D1tk5[B5FUIq? ȯF3Os[7).!Xt FTEmN'Ekam9\oq)!-tByPbL\u哊 M̍Ok&yw{)a)'H7}V&A[W(,%i #ڬd1(5'@pa֖a rFX ۄ>|l6)*i;VkI)HEU@csGMH sS 2.wqq |̗S‘8LړP ($= ThŒQ!M֏&Z|; B1\yfO$Qؑ~X¡-V'SGf*ApUO%La$$tḂ$7R"Ƈ<9w#Q%'\ʒ4{oLr}/FC^(.Q NMy `Ҡғة 3J6v OH9r%K558PP|0>DfwmRޮ44SdHSx,"n APR5ɚ2(/p}&kC4PTʼn=zsF7 eCO6ؒ29:ڐdE3\pNp\l1فz ORrqt$fbpGI2Q6PNGK @ӤH\Vfei? #Y 9Ҏъ:E8\:y|@|X9|H;q,kh8'/>2cha89؅;~' zθzP=cm*Q|fr9^|ѻZ_Y~pka1>ߍեյŕ~-TyP_,OUggJ&zHSBa **%<ΑTY)pB> Y MВ5 U", p>GO V5 <9;!'dB.H c 7 a^^_`P}1PVM>G}tvXSf(B T`)JAbڴs&!ZfGDCx)*%pC?p@PyCP~9AK]b̍K&g#f:#F[c0,}Dv!ifXAUZ}>W76a@) &aiI{ <XW1l x/V| QZ)t3hTu,Ke8 (J`q;mPd֚BR@Q 1@)}z1P~CBI  r ԛ"::b s鴆hh8h} S\S@3@ƕ!Vuf+3>#+CZy'ذվKT/ UP2,%V*9ow}Z4O}p7ޚVv_P\ѝNPGVe!aţt|OC'):Zw˼w+`R]? 8kp&1i?~{mA`ILH Rx'\uk@e>fڷK^29&@~Kyc \+'BT͉W\᫸SOL>?],YF#apiW'cu,40@BA2`2YơFC1=o)] #NCjNP[/;Iķ?0Sn/s1&hW1g" Ǝ;XJwcr9` ֙-GYyc+sJ; ߄96z 2\:9h azSsVڈSyi+<%|L4=HP'Fm~<0@YU0q\b$g]SU$zsC9bGN6Q{i6@!|֪K\og[^(U^Þ}K2sX xlm]0 4: 0d>R776h݅eqs۽noUsF;2Rd>}t{(V.b@y톼u?|)=qJo#D##5rIvF= Q}4y!z`\6Ĵr( ˜IX5>PۭA݅njðaZp4{>";2AϻO/g 奕gc{ ^up@6Agzi'};- KE:04HExԨՠ޿^Yph`$hH`FjXfP3b߁o?c&@?x &;;. $u7?%փ {KܬH1bS9LMǫvo"l}΅MJOZGPun "K.4HmrRTEנfjfǗx%ؼd쑃ɸ`e|:ҡ~tHj?Yuj̕~<35i]Yw#)S<G(~||MzFÓ_>i]vsi.`'k6_v!x-O#D\6 eGFnm \IĖ3h ̼!0ׄ\sdpt$+CJ ҈E,jBX2)ѻ"N~ u,aRP}9qnރ#e|1Eϐ_N q҆xI Hprpg&׸A;'9X дZb;sLi8p'aҝ#Rp%Õ&E 2"e6.9Ӹ8e&)"}>Ќ8&ޣ[Dd^pNNOqf w*D`iJZ/1DG rUu+XAJXֽ{k띙HdPlrp2j9 ,4|Ӂn\ .<9G5ӳ3 'g'Vz~WwqytxQ^ 8  :FDgv9L&lY*Z#H?#6NLM0u,2ʥZd ^q}q >;9wX:IxP@ӄ>in({w3۽/!b bR}."2K@M !Ţ`suqʑAʥ تMnGJZchX'۰F(il@uH|f)smmG 6dZ)[tw}FAM?|@H(ذ" >r1sRFHb/ZEKD*g4zf/t11(UV+U(#0S?*z"U$~OOP;EwvvW8dKKp]XB E@K e"LXH uH8Su ~Xi@yUC[("sudq E~q̊6yL3dE1pu$mtyd@PDB ^KZ^XDXb bh|7;D5 !k9=^ZG@ xXPj$dpIaԾ9*"yfܷp+ʹR[N.gE#'bB{Bh&Ƌ`zdϸ[x<;1(wao.5Ԣ9܇2:gMٹ|[zpzJ.j<^ ;6|Xgqax=<7Â2c!zM/e>Lְ&&F& >1-͇EQ'B +aN.i66P)6"ϒ*A#舰 oR+.LNkɟpS&' %u!QՆڮG h:yMݒQ$۔"#bz%vFn 2N}$S |$O CsAHrjЦ/Q޹PEpǶji > tV$mnQJEgZcTL@l֌!)E z\37ŦUrĄ}?_z =Vil[.F=oR(|'5tF͠!N(2]"(ˑ8Ek>qL^q9h!PT$I%tu* Ju:T8[!e\)в``vyQ% i ɎA`nc8y3V߳o5`F0qD8t+)cvffIV)BxŘ[R5}zP-BE4 G^hm? S8&W]% kթ vgnr8w3XjZ; w0O˗/ahXC&TZ3YG!6F\m?@+9Q#@ZZDTڿ8<>F.J_nn,-׫bG*, )TA/6tpPw||??O`x@ $ >ȚC_CGP3ZYPiJy,FP7P#R% 1 Cnn3m#tOZr  4Wth[sHoFDl'MoJxXGXdcKi1KxPRH\ cGj K?LJ0s3 4V‡):} рީo} ubഢ 8Ye툶3fXD;]Ḣ>|ޔc Êc|)s\)ٴMW>Ќ@D0N"xS&s/a 3՜Gwap0 ,u$YUT58S* _'߱3prAX1L4<5H#0ɉu3i[lw爋_d$A aFo%9*_su%:Ofe FOZItt7N:nbZ`—$>lSt]'p&&i3 (yHx8%'-f6A|aڜ`g=`!dvцӄƴT64Y9v&,)K>i'Yd^{"'uE)>DVFc)`]o]M1Wl{\ȿr_#[/}zOȝ喱q|`^|i7+A%A+B#ޫ_N @\__)LnK mv#8:%uρ h(:#0g9 op7C++mB hX _@q?}(O|k9S4 0TYoYf( Tye-Jbe8=8<ϾtVZ\@k6̡oB-+wYRý|t vn3wJ6 97K ܸtV)Vr B%A< -4@ϟ/a`}s[ˍ+"R!Zje}vg0;:kGGȤg1=lڶХ%!a]SӃSI|\ *5*O쇖 -KPa/,$#4 KgG] rZ|,^ 9afpR˾`)Lt[`9KkZqֶ6˕jD v%r'L v`g*P%쓉`9 ?h77;G׭vVX[@E>c?!0җ]^z0XXwnF+Wk%K)wpLI.!{/^WCn 'YbEc+Ɏ #+-a Sy{bgEQ!iy>\^[h*[9>*NhڱIK&G5{ r<tos6J[ i6>U Q'^|(\߰#8>-0<&l\=^~$GŞ ^P@ b.m$Oƅ( Ļ0 R++IjzK&` Gv(Du D+ fadЈDž%{ķm]mIE0d!~ODE$ pnd4~g:6ISSO+a/Ie8%#a΢D;!hQnrL)51R{wf;> ;.R *8+Ȫ Kk<Rt&^1K_Q`D̿)P?{{xЦA =$CRSctdk&@вҶ_hH9EH' ݞvG-؊jX91ayBq|6;B^ l >%oRJvdELӨp7l=ЪCD" K\at5>h8DHa37y+z7!')aB~! &|$T7u݇Ǟ$#YF`Ⓧ+>lSp+KMY)Ƈ)O,0A@IJ~m=~<<JfyB>tIX1==Sx|^arMjTg'ȥÿe$@NqmJBP0(?B{0};gfJ5䴫[R5"B!m>hp'B1/wnU$z}`9'WwcD#"P, _tpREX[j`F 8f}cs{#YJP-HSMQwc0B BI; `@TODmMr ?J*ʰ4c PG:}0i]YFChuG}à-ܠ j1hnp@捃N!9gaLJ ~ƶ"RWG7LHci| L0~\ j0(3zy\9p8A,A+ (dOٷ`HVcFɳ">&8i6[l3Be䘡0m[>QII&R9m-@;F  3 Q_ɞ{%):R"m$irDu;t)dzɽr#qؚEfeȺ(I-fSx psbI^dS@ $N׉?y '%wf$騌R,oDEJ +D$Rp18DBF7aNAeA|IUk v TQΨeH pxPB7.6 ?q[xׁ :;"WX)&8l%=>0Go-@d)ջ1I) &9.A{r ! |06YV>u`1"hM>"|nj2FH/\2ӆی mj@>鰜DI9cX3  &wf!H@52(,:58 {}Ha݇\@ X5TJ"B$=-H-Lɩ܎PvA'@'qdl d6 n'*_ ֙7(#e`XPH87Dah؀gf0QłuJA2mY`8/"v}/xzL6Be2c<;By5V~8|QY^;L "*B:Q(1PՐ?q}sy `T%筎/W$?FZk3L9R>P %p' hJڙ&#ແ$a?rX) 6^9ѬWs{?8$o.#[A >9(#rjhog5i܍ amDv79;BU p;֖/f:>l Pij E Śf鲄 yX$P8 U,ydfh8"=)崁blݝ/_ oeuG+t(ˤLXGP_ԗD0(0C,d*nmY?dt^z7Ex4-ʿ 6@HdiE-%CRB$cMܚQ2E 1&c&}?}spΩm565^%NAbS7I\OQkv/8awH\IEKf|@UUoy fͦȕ f.SDЅ,H%,;ֽ9nOnC@'bl߆S yI 9ld[Q lľby@X#t` )] hpb )9e\1sh`At4 9A)ΰ#n|eǗ\d}*+l}fw9]J ƅ0QdÑSMNN#WIq9/$틆!E ܡ Eڃ7PT,U]m%Lt/x2JHC: 0kک=1AJZa /D9dw&לWb+aˌ܍OܗrߪM`>KZRrNN snaF |fbCrz MŁO]˘F6i)]@؄pc=^4)GH>-Ou!X $h35YnW $Lɫz'?%S֐4}Wj't)xfY9#&ЌЗ$̉| $pƧv[Jl&'N#& ASI$Ԋ4 ̴O!6dhpb֞I)p6IDv*"vPL&]:M>k7 UMLajT+P- 8@4c) *$4=+7VZd%jU tW-M$W -E9,@޹6THMygO6StW"\gt R* \n"B0 ՐbIƣae +J)x UBگ60i;cyԉ؄}}kck}m*G@VV~kkv/b&\p&2-XDƅ]Zj%iG"]R?A$Q[X4trrjg~g<~4i89s*(L?.|0M@ ӧXé.ƼVRģzi%-kӀI=6 B6MX b.!7 gYZm4PF ID W7j2شR%J&pVd:lrbLpI2 rn&4 ȗ_0y"/oǞ(1m2lƊE*9Dˆ 'W(p \V0q3.Ul)W5Hh [__gKŵ{Qg0Y! { "=3H OU7THn/qQYQaFGu.4֍u01m Q"p3h sE7ݗQSORDEE4D{ `b;ߜ}GGyVL! Oiƫ̀`ImI@6/ZP 14a{FQVmt/٧r~7օeL}KwXП֛R`8zp]l+>5: \dKnXfg \PAN583Pi;I9uLD%/bKF=9A:D_оK7w@@}Uػ_N] rPH>#Jx I:=:`:u&];8^=297(d7EfMn_V":~ Sʐ RO[h(n`q"W lڙS DDmz΍RԒ@JUzBlSpV T0u9S^j< ;ӾSܓؒhsGfBQctWȋti$ Kvw˚z =b)%R$WcDM"$pʉөE 7Ja|e=gMd`m&Ca+WT`-ji8J!:d}Hq ×6T2/E"H6 ISGw b!1W]}vpFQH/1(h [KlNCdy61X4>Ι[reWK"M)5q N4eLxm0a"лEcRC)Hfh@e :B\Zh ,L`%5Έm&h0cܤ{SQ43WjŋgϾVF% ro}Ud$Paj>15Aj'Q@I B\hb(D`KDa4x,QZݽϿx<_Zv;XZXCrǏ<}?  HNO>](cq#:#_' ez4jό:H P,G(n0! ,PP1 ;7cr)ZVzfvqm҄ f; qC6${@zC1"N~i7m9!Eͻ}d,0ɟ}%ؠ" _G<% CVPwiֶ >1%/sb9.09I<2 SH$%rpB^JO8[qA#vI}ne0* rFGyOMbWq8V8ùQFvq~cjXBL\ 5'NN_NLK\B){/Gg8l3R ĚFKSSx'nr䀾~%sKw8Հ8R~" ft- y"F,OpAQrw61s9gE"7j s>Ts^qx CC?gnT䠹 >'xdD?vy@y\ 'To5ϯ?;~i׿<ӟ &/47?6f𚿝G% zcaߠF []a gr-  6Kk+"|0PҜۙwyRYϣ<Lo,QRB)yluQ" qBf##d[$qt"" p>Ju<. z( ѹBTbQ 1NpݳKez[e]_Y3y H@8ӳ/kk+[[H{FOIw 6r?")=82`1Suotu!ҰѤҲg~ MN:I8\Piq NK4t5X#j%D{`ͥ%m h `(ߖL!EP)[Yb%ܖ9$4bF0=#o<`)a "d+.-:ybNjO4q$v+O2"O ZKIF*0vKB{}.اG4sSKDN >! n+;şP;H&k1|mh96A* pJI,!dc4f:W1(&s2SHuw|t)cQ:EHS;hv'KT}0:ٻ5: k Ga|}6I| )β->ZGɟbԁxA<P~wP03 yQα`(8exQ[YZF}aga.#!2,_s07D|g/:{o=l,6d-)߂3g"r>_䮈ၑidk88:@|A'ӈB,vLǑ<89 h^%L?M3>UveG.tDEnM L'6/;ma9hB?4Ca)ʅh@}\F: ̸,78QB6 _ "kr  s)k9C"}ls y7[aapcn?."_ R?*%-FHE#oyy0㎼}BJl>%=aM0ILghl&#e f'F*hyy:[$9 l4/!R^ {ZHOZҺNNT[Md&nN9;][YVHT(~IqT IRmX,jx<ΝhĮ=2.)Pм}Ҫi4L'' nOHwG=9,O$-_qJ͏:9xHn0X¤`)dD()cLr1ㇺŃ[%Gǟ|>jU=?Z߂ZiNۛb>?S?q}hW\6뚊>/./D&xFR,l$^B@R1?scE%St :rgdARW2M%h<_)07_d&ee&&CKؐ U h6vLvo("ͭh}*4 Akؖ Tn6druwTìsy{BR}Z],7?WXaOn56NNmVbs {o|/3+I`tHÖC2ȐYae))R(A6-P1 FuWWWUΙoɿa}eVu瞳5^;4mhVW,< =TGRm7i a *}X%&Z+g #kAn`#uU^^Vr}(̀q_9m)`%yclEѣ~HzظXA@sC#K6 l@UBAm2$p4r@GZčqH16 UyNBOC>&c!;=BAM7ic3rUlAY[_zUc4Rz/jIC֩|#rٸ̿\N I cº \ۚpfP(()LaJ+6_HMHLΦZܦ]FNB1H9R+(QOUabG %(*ۺ-;hcҥOH6zBkeRq1e8J~HQ :|#w׷Ty1yV:2ɒS yT h/W$*aJ316]^G5lAP̮=owC-&Gҟvpbo!bɌ "t[y' v$,Xd\ꌂm+¶ȡ69^ispC(* !dAΫ?w035\ll4۠z1܀"ϣJ ㇈gX͏0F+?vh#AF`CL`:(M{HgP_r%@x9taFP~}}u| wctbr5.-/wPQ!M;dQǫ|凫CG=6n^\ _uίS(蝁Aڳb@σPL!"רEQ?EBt5pDYkE< k{7sB}l*C 4bX7&b}a Q m1z%T )IF]XEP,Ljd?ĽV$Q9Ea>|.SiCXm[hE e8!S^|œ[;GӋLΎ!UB"b$/ 3IV.󫭃mtBnh# Տح`*A"s:X"dd sV*xZ|u$#7%,aCɐ.GAB +XVn])^Đ19) zmڦsٶ/)I${ VfYVX[CiV6 w|i_TFB$xDp>WAל6bu1quCҩt#a ds{b;_*ȷ` J(Km P5U8ԎR0 Fœ0A74Eg4ZKY tq_h7E5"pM,S[X Q@mPz[d4QsH.T()XnUc8@-V]L vm7526L۲4 -dHL$?g%3HEd*,)Ď,%?E+aL9Qpbǩ}"k:,ݐ?ٓwd[!瀃pv/Ugc5b)|M5-T,-"ɞYF9",RG`-aNepnñ䚎2M{ B?e)i!+<Ĩ2~6 Zy1 \D*אEn3"`D YezM!'| U5gk͵%MMf$dWca -zQX0%ۧтeL*tlLiq4:ҧɆSUUP]Gt""W9Hr\x-r;ڝ_q47[kzoNһAӘO\A_{"ϟ>>yr >ƒNj h UNoP"Ax#gtto}O_:{r1h*Zb"硃< gkI|!,C 8s3,guxPP?Z? 76661>xMg?brДP=K=H!N伏P;PmB0Snh"p E#ŀfP.ڸ%8JGyN]SHUP筚G'N;}9ZSc6Mي:O+Vy[Ϊ1#XkJpYtc ɤ~"ij(XSv jOW<(rlf&Lp-RVZfZ(|k%=$Gk(ht?#ac4 NF"PNFkE2.,jH@S`%؛VD~)4ZʙSl,k b % $$F c&Tc}7))[̛b9!讯]o,Q/2ڎ|nr-jw- !Lu{Gw@:ZA^ऒkۯvv]S# 0F8'OY2ILn!r>V,&, a(dIw# _ίQ+A5AAq75D7|6BiֈBECYW!e,pтD&pj ~>X ?}QbbhP)cDƇF#az }|Oi,;",_^sPv^ol]rqzqkwXɚ8ܦS@wAjn(S>_Ò1|Q!ymkb`PerB]5]1`Bݢ4o@*mK ﹪#T<&IQn_9MWDz55=E7D $K"+F&0يbcBc "[(0j20tAI)Ok2*S.Mq[Bu@rg ט]ޔih!Q hx09WFL0FcP RE͈%8ESOߒ.,y1D"[$s r9!(+v;Q?C yČjNIO [4N>G]F6[RZK3c[Dbw!@nM = ^pW*s誮 $ N WV@jE0ex([ #niR;*>t3$W͸-H$*AZ:n7n&]RRIBKLd"w* f5,Ǎ޴cUPrh\kyׂOY fBL̺a6U4^m eOQkFkI"%, ~Ѕ$)8SAa?1_IO +BfF; kr22L=ɳctݥ 7lCDS Kˋ<)ӏQZT悦NOx)5Qi o YTM_\yEѸj˪ф6Y|Hh6;yҲCA)L<aαi勩Vr$J)a0!ĔW 65d$)Zr/R?(xfl+V ٟh&k-sK6hEw|áҽ>pP%Ԍrg4[sA'W?Ʌ^$zGn^`j  I|]6艠>OZzńzJ |¸g.QҙIiWE#,I!sl;FW ui+\ Tk~A7Q#T|<;?<~pƎm)Y00C1x8wutոW,T߇?333>9:@$-9[|+ƄFЩ@9;>vMHPOW"ެ9p@kqx^0=*Q!2x\4 OO8zD\GH In@ pᵢxg '&^^oo8ݪ2܍@o:w tqbWU) 8rz򂍵-ITDac@F$RLL]nퟝ_a@^z G#Q_EYVViPLB%Սw@[qR"$?++̡R0P4u$-ѼRJfV@AZ)Ae!8oĪd7*̎su㭂Z5'4_Z%|)hCQG3eԓzҰFs1]BX%_L@7DQ(J%ԊI#Q02Vmx4 nAn]7Uh`41X50wh@t6_J(,y,[G0 NuG"tO"ܴh<{,BIިO@ ůhE鏅Ld= Jo))+HGIZfEzy{= %rIゥb_\Yd:C-yx H*.%]V„] veQ'Gp` 9Shr+e$"31V))6ZDnЂ'\Op3[!,R( 3ɷa$kƓ.7dl1G ^RǨD*2 Ue$ɒ O]"}IC K;:"!BJh컴W99nV+L\|Zˊ4DB: LՐ=}p\=<4puo~}Gf^j}m%h1̧Cy!4==]YG`S$La bE+x!PQ5ʍ xi>LhI<&dm;toؿW@9g Zgp% 8pstHpJq.NNM aÐByUJ !}S[ړ\1Zn+qy( GVnn#Bp?lΆ\٘ҷTHH~7 wLwFAHzw` 4+A0k@ePϫ>{ϯYE:^Y8xG&T&UV?Н$9 B;YBNq5^V@>G{{Gz7?7M6RBe9y捨  /_G_ fWcQPPp9>U&Q)BGp%fGN.7p4%Qq %VX7(g聧`I Y)Q/*~ŤG _ďw"DZ5aY[;]M65+!-֛*|Յ;L!sKDm~ w9Dm6AIr2?CnEWw7,Zy@|gޟ$@T*>k\SVc3pDg- cbUMk&.BI}ZZ[MAJ A%e9ŷ>)nzK4 XA2m븠ΘLM IE*3h86(/\ۨmTظic3 RJt4V[Nlb5讙}sA HZAj;c n@4C0zcDY m|?D!xr=ȶwpԫhߟj_Udm-׼{LJ\9 pKLQ&\r6:_wg~fT.IXT0o)ZVB|NR" C ՏmfLfd\<'լ[&p׃+ w߿>őyC8p<%6#pyy둞`5T?9yڔR{?p'g^bWp@ߋ v8nNgG'Ic93:"d[Ċd HLY\,DL'^^^;>ѩq u\ =O\ i^4`<<V& 6)9Lȋp0L|F#X_[?>:x4>X99;<9¼Jػ랧0T%pq) AuK85@VOqڙy% j[F#Bk{sz}u|hZL;9tb'!1S_0 n #x,*BKIv 4bp6Dr *K2ƂM0yae4.AI©LR !C8 4ֱd;NQ6sgϞ::,.:8_1hD !1+$R@%-R1Q4ScF:1;8qtѷ{rz9a" O{:W}C=x\c}S}s==W78bK*0N Y!u+H@Ŀ29J{t"a.麬ZҺ"tãpaZy[ "BZh'5 $ӈؑǭLAc,"$3捑ˆsjn]E[vkP@iU(dP:6`D畖 3m]ߧ>=&z M ˴kǶi: I[vUtq__ZIJQg,1/Z[uGzLNYP`,; QI,IĊ+BGo` Қ/O 3h\6=5Њؘh #? ͇_~KaȢ[ś] "hDzx!ָO,j$Ԑd2H.J֩k{V|c>~\Ս w &jC)M0+2"-!=rMh3Ѡil=oI#h6W:eUiE6)%w CSl'{OVP/\(Mhl5|\F@B>jhvW)nn@f,rt(PW)%A}ZjPX^IHX0 XsNdC `v̨O%|&|%TEp2W;=-xʄ5\Rn6h,XfPASX^p!t KcciݫC>eƹsٻrqvw^xyv|4?" (;Yp퇷7gՇ=z z><R&;:bބ&~dhgxdn\b}ڦ>Ϯ.Q|f:{sGp'1ٙl1QdbZ+JLsIz3!cV"سj&ѥŤǃJ| Z5re6{GfW-.8SffR@Rۈ3,-G:q*q0528c+9l\OG b32Џ,8?;>D>9)U{nC7#4p*&bI~XX#A)|8T Q~H@I_"Xӷ$vvpb?ˇlkslQߊ*Q.?vS f]r 6O6^4ye4ƴ ԉ!JK:8d3]^=xO,܊oe 7Pkߟ44eyJ,5ب~QTH U*5NoAoQT`µx϶ aߌ9H7%ZdbX -͇.5 7Z.I0cj@h 䎲0yѝzAIN^R-Yp|zkB i.HorA(5 BHrWצx^N.QR}"ւì&,aiNzߐs V5vϺ!mg)GCYHeDL}fL=s+~![L\` wxPIEǸ0)rf [Q#uz 4G #Y<-&t\"{QG?U+o) bw15v;m!ºMqݽmG% %T- mRI߫)!QޢJ*4V[, 'D*c3+iTThqO pr*9X4ls/zf~:q7m$Q.㮍7oFGܿ?2jz󪂗4 A rH^ < ?88>:~{sssp"q\ 1ģF CQPQ? 'kkcW9?^U5 ;0|q{C3F FGpw%E!erhYI:E1ZS 2 `bHJT쁃fym>  '8܍06Iۈ$l^CC|Fic>67=<HOhb!Ḙ@~$4?6,1fGQzaX!B R27Z,j,JR9aCYNR{Dfd'~cE7:՝K1VĀO@ta}FW~.Ť&C}4z<ԯd"T oh1wb!q\`'B}߅ C ]ͫvW([0EH#i (0{{ t%yՈ#Sؿє!3Y76o 5ѷQְ-ָtBiGRHL ݦ;DOެ@Ciޚ[Ƚn6=̾=Tn$nM"FuWSNEbAfXhϦL[MHO1 (̣溳)U# P6I) HI]rD8UsL÷d1e`/Nb៍,! .đ;BZ7)LpDMBc^*hrBOT)?-O$D_YEx.l],˽ePlз(4+dX}IⳞ[P8fH(JH W_ Gh6>ՏЬr!ﯽ^CùY@>ksGۯ^a &~~&c<2UզZ'[㺼.9f@bZVdԲF:܋XnvϯN/bj|cx 5ҔFx.LJLJ777.{N0244/=4+/{q2SSc砣_8)3v4J4vls~ m9\/rs{ydd?@3p}E4nVVV:-[3~42 0zcHUϳhEn3 'ǧG>P#Pqtt:"v+XXN܌E}`@fzj+ӓ#pa5 b2.*-PPcQb qj%try0*tFGPB >&[f6d!0@)< @q68>::=@f R$ !pcY~78+;8ZuP?UőXS O :87fQs`G 8%< ɐ4?-;?<?k*B9 ԡ4D89<81cPZYC;YT2NBF@Bz8@D%Q37?1 :VF4'b!8}RCqB=q[9p;R/Q_ kh}VQ)utdH Ͽ6p.sBK<3aC_5PqsJxSabg )Ր&x4e]2`A( ]Xͤvk髦j e460녚'^7 L ݕ o 0LC5pĄUEu3c;5K6 JQւj 0B وQ3}[X Oj:z6Gjgf5kx` Wʷb.T\[CMfw >iwy/M QmUׄ$o5U#X" ,'rk2=h#۬껡G9ȺEJQ&`-C2;Y>\??B~峧 \$$K=W0ʹMb|3t*xʜǶ~T(OL \Jex84?=d?I08 ow&=X]XyC.JW5iމPZU BɊUH p=U̜1\{O>yf~>7[,rsc& /3@!񾄫|4*8#K)bj:q 2tp8'S6>|02L&&?1??4z8.J,ُyd,O<}2?<U\V=`z"BS"' 9BQ +_n]}scW'fdB/TiQ@GV6,IH:x" }ӨA";pӃ ^omb*BP?j1/ g@; MOȀ0TAYL/vpXUpGʙ52wvم,.tF 1@[`7+>:mbr$r% ЄfA"na vzl勫Sl댢U6ALC8g;ALjL1-.@gxp|jlY%(F/,Cl~[-9>twǩ՜ bjx J+~5dD"ȕQ2@0ΕPS"gRwbv>otA*Vit_\ݎjBuAP[8n9(#J r 8z V- 9SS@,FSd)1Bsƪ_%#5&X251DYe Z6I|%"L56r֎M& n^Lh$Sxqk I cB1Rk ݁RQ[ iz7!tQNK>Ub#ZMb?#xe xdYQzu;;Uĝ g+H {6I%a.#Ԕ45 ts 7fLX"?W?`v&CJiWdM-2`MG`$`?su}#ص,~aƧľJyD FӜ@i*&Q ?Z_W9Ld,q}|xrnn#${Uom#.&N4\ֱjgt}#N\ ٹWxJģrx=Y ipI;;W'g#\eo0cC% (O:"q~1G  1DY{|b7 }7\A*9v#;؏`zpz~x+TB]dBPG$E0֩1:fTyyFL,ֿ&F,R8H;_ny88#*ZboW!ߴd[n&*"͎/V)"'84sGrD"vX\EPG<>y%7{Ϟ>E{:2\"c'ǁ <4w3#Wvv667/Yƞz&Ǘ;cpyc"us 4 eb> /AzH^|wiT q>jDɁa)(*B"1ᦇzy& Nɕ{p vՌISSCx[Y]<=330~֠qI'G(z{D еV0`eX;c|p2*ibaY#`M|Ņ鹹ygfpL vE$'ǏA:x/.4Bd8 co2t|^u%væbe,sW \kP, u5=ER0:L$0%bXvL2,k,i7Xw%4!*\ 2r +4>d} iѹ-]Ay7r #JIJЭTiFыiչ %* P2Qm=':qȉzt,X !4R4͕ZX 5(m\|DD*fp&bk㲪΄֘ !4clXP q+Us*~X"^Z4?pL]U{hf49;Y82AQᖖ w@5o*ϵzen 3ʼnb]yk҅rPS&ՐƕؚS}l:AX`-O*!g ʳAdvaB ¼9[A̿^P;ѭ'g HlY︷D-9 !$Hw[!h[o h-p*b) &f}+g1򀇖$Z)92ڬY j1Y@Pmnžs -U{9__P1d-3YI yT3hᾡ@MV0uB‹ RC;FcI439[ң]G!)=D/t_^ՌLe o]V9\3abG*á9 !'qMb:qa:(~11:?731=^' {[r/̭^$Q*<,zfٛ0, &@nDg38A nPTd@:# F{uFqfp/vIJ,ɡ"ӳ㓸$1t\ _NpDyOȫ34#II!l7:ʣ$ T8*Ba%'7[Z^@a!V wcPlT8@fgÔ @R*H6U[fFD??8r-NUnn(퇖)rEðڲ`pl@h𖠂V0h-H[c%hpyiEW*u!J C!A1tꡇ2~u)C34.W7b$%Ƹ/Gl#w5"?c-j-z1z,O3?GB|T o2J]PbE'HM{i,"n-'Mȷv0ջpj>)+XlHX!k7#&k!,.іDKHhZDj5dqTxU6QPߒ86bF_Q_+ 5۶0( ׀.0٤)ΡQvHpMs ᇠF0,$,!V C9qJBzDl=|tf4l*.T B^z/ߕ|^g6T pi]"ftϔE*oߢY%`ʬ"B%%R&>8cN5Vݢ/)]Mδ@QgҳqECO$r$A8n3{SOO)s?"-4k Xnx=M[O1 YE Ԧ__j& t]:xlI1XLRG%֌|.> Y+l_6* _UgsIB~qW^>ul912==:==)$sVnxҲW -.[Zka(p2tgAF0J a:QSi< Zm4 ˗8``sO6?=:\xy;ߛ[Ʋʼnѓ{xyaffGf'f'FF&NOvQFKr ÏCW)C XNUDj7Ng+*SL"UN2yb1;S\=+ 1~adNV=;9=7nس~vu>4$ձ9ӏ^K(iT<=3PBƥ ( dxj#Ai`s2lyEIk  hau$)KHʫӋ7o? 3aċ 'mD89 ּb1h 7qY@G!:dJwHM4H-ݧ+Fܨ8E(\T0C[8Gpalr'bw ytˁѡqA$E@NC 9}ƐXP;0Y5];6u'p)gPȃ)2MYfIn1|Ri D2p.) k[_aҽ{ȮXBnNe@Q 0/#d7Ȓ:;=A"vS4]cVH9<.,EdPJ&2 T qoBPT*Bق:tfrǤ =ӳeMj:zE bD4m54PVe ~g4k`Cun]uJ`>ʹ~Qm(6]V4gej؅`,Rn9/ ~f5&`1Tݴ۸ =o+s,y:q \Ͱ{ms?]YtlUFB״V&k5][zx) RfǴ\z5!] C# *lh&ÿrP`Տ%ہ4l :kWDcPx:,iRtdg0|JtKW : GDd|NѢV̉Jbm

    9>:9C 'u F(CҒ؇_b6o4-D¡ey$/eM L/nYGР1=G㳣C G(O06Bq6֪P.5䲰LQAgyS~]s&&#XL I4-ms?r(P폏!46PrZGOR:4⼄~C2RdTYQ@a)*_ĎD{Q?iވX\(XIdS!T;vzn+0V@tf)Wb9l;Ĩ+mtH]OߓRpH,#/b-a& ǔn9<@7\^Uh+9gBU-f<e{V3) ǵ~J~Jj՟ZmjȡF4Vjl8w3[$THul]dkN!%;,9e KK?3zу=OT=/?JLi\21ScEC`eBjqDV%mT{FHI1 ! (nD\2RHB뎝6-̥:]o8WL]F'p+a{4K& >#}<rֶzHRcqtzMKA[}]+d+qG^ N]8 *  kFBqlΉ)5av2za;;ʧ7lX.2YG3av2Zkhܶ-/U9چC,`q`p`dpheu 8Y;NyO{/Ngg>xpiePO7=C>Ruۻ(&ѹ&P}vnp` -OI/fc &j@#󆏵z@8zG]bPcAh"ӆ )&C AZ~34=%dž} rv dhb` wD>s @0Zl_XXijmqԠ5EJVt[(zD2Jouyiytvxh% F | hw pJ]w\O`ψ˘aB#*ݫ!lqyyxL3`?3{Xy* jOÓc3Ĺ77cӳ Vs1xuvf{FGqrʝZ+2hDݥ*H{ b Nd3֒ĉZĚx\5A=df,,pbJ(Ωʹ+ !PLǁD9QczsPJ^p{c!jh$Rؔhy! A<4 Br&VڲQ?G}pF"Qw+T^+ ތL`ZMIۮa/x5:FE:A႘Vt_M4ݷ#5,CJJ̔BG@D!nƄپHB3l"2}R^Tq2[ Һ*L}C똉ph|Hp#d)C(r-g mZ.|2֬6j})>ccUTw+<F 6IWxwC ÐZlXSR,^l.2i  ]Wh=!YbVx?-hRz*0+&7g'qEU#*JN2^{+K?Dz<&ZF5$ 0~RzCjt}Uɮ:?ocù9偅We9v:3rUғţBa@?9` 5*|!{Bt"^z}#Y>X3@!}uJ;~AH0v#frtC}zsxKWFgFQ?I(Op` R80B!׊ 3DZ5m-07{pQ cjGFP06L`3V[S$bG45mG p\繎ىG+-/b/$6ln뗯 Bvc@77P99E,'Xr>@ \:EfeD]fY8Ɂ2 : gO~;(Rri `K 2U_`X,g}(V6q GwNΞx{ @_]L[TPJ/Ư]*x*0-e &5ࡖ-)Haŵ0x[ AFl0\a7+n.[ \],L 0(A,& X]K,Rs(’`T*DKB{tJ,v \%T]\ޜm Pa :B qAKl b5"WQ5\ 'gjNxuU3-6ܐא(#k3F mQl6E)u -괷S *2LE֘cIBBE`I*leD\ў@SRLX\Κh6˴X @I$xbi5^@(@FWk3[M"T˭Y^H2{jR 77?X="B\EvCKPi]ec )"5zmS'‚5RCWI_5{ W42ѤZx芑Fz9Z7Ub{:\BFO7V/T%#)kd{6]axC:aEᵯv@ѱwSArI3tF }E94-i0;P-CĢ6_lŝx!^ɷ/ĚPM X_<.ʘZnVm&8`bt>O Ou~O?^2@[/va ?g?_\^GԠ7)+Sxa!n&{ x9jY "-+Z0DzM>?7'WG/|rTgzW$s8$$;ZaG8  m,./vAǽrQ}8,CU[\!3n}R2qdcFJ[4t*{JEҎа^[#źv*9CL^Ei0)%GYO4f(Lb Ȋ%`m D[(zgնuXOsբlFY$J(^?J(qwp-4x=-%۴)k8DFg @;kM< P?ꞟ#HRV͸X4b7c!wrZd-tTp5 ,nLIzhlŀ&3$?7)8IܸIe4#C |.P!R,s j >)Zm \$I˰lsty [Tλh"gߖsnUfs* Bk%#WBM<nDiRg%b:礡.QdS:in-vq[sg 1YbȞC??P)X- zlBƋKMmH*溂,x, ȫ`fCribe8%BC!RİaM8>'Qi*&xśV/̄0mT[ IџP~0^R-pVT*Di7#˔XZ\s#D' ffv'~㇏V"[@K ' 3)d[zb{]0aaᦔxe{Z^N4UwhJ1k|s-2bɔMBvoҨ$=@v:Q)bv).x|eps/ #`.C m a)A뙬"^5sp  :` DTLQ6xϿ^߸<Țþz,;(lxg=Ggo}bbefk̙HfwfLNL^@* x̡2zlMTǝDS j﹄?p5dZK87'bNRE*"T!L#!*<tgyf(G ~˗/!m"jT)`ypy$6_ `wihו+A.oL>@-9K8dee'\ 9CF=xqYNǾ/ΰd *S#>BXpt'AK7ưv57}o6ྀ>@%a&Xz! J_A)@_BWhU0сsf]v+HWPa !>%K<τdϼǮNꇋt#`_CIXX! (f'=/Kڛ&""4ə幩E:8z囓cdSX:Cr8LMᴑ5wBW2'JK:Ŗcd9`3q,{xi)(#B^Ť36BjB"NN;0m蔔ަp!R:Rul R$K[ۡCLͩ@ Veh3{1]L6 kY$"@zv<4\D*u?ghc.a_ГmC]F̀jc`PpƄKШpeK1@XezJ^Vp^0H3)FK' !rԀwZ8Sy&3zhTcJ z$$Jmf a^`.IL,,I@)i|{4|6uZ"hb`T"EYG92`*H_T^6kZ[ԞsưiƞB 98 ^ϺךrDe2. xC!% Zh1]wP8S[ $/l䒼ZF+5 ; g6\xս^\717]i&jQaօL,MZ`.J9fNVirLB"[D҄΃"}-Թ3r[wBq B-k͒QV)5|Ã᧟m^??:AUAU V5d(!0=B#z`u6>s'>~R/_SY];=B l3w:\1~'ZpS9MS)s5c*\f{n!Ȕ#&au^@&²E`%eM vQD>"{]]^MN,ĽP>xg/vwQ)Ro3T9,9? uG' ./Q@a}}˗kk8cDvW'Ǩ Aɔ50;J 5D/;#ԃXZZ@~1Hĸ01P<;[۠8דA*FGQ@FX8b@޾!|=>y[W(9=98w"ږg7&x:8LW~ Z x[ l S>BZ?2$ DD@=ELGD 3/IOp B p In,H,)#XP Adpjnzqe"#9ivv:C}e:1$-Ip^ Z9 @h#'EՋ8Dq={w1~⻮Rl;%2(.%3aݔ1(4Ɩq$1Ess_c}q/.^Tbb@YWM(>^nR/ U4B Iq<<WPy<&n[Fs˸+`Srk(4AR%t ApC$XלpizpiKWT..{w:s뀤f\R c#c7vvv] NC'Z()8M xȔǯgۇV}3Ks#\>I&C௎CMT`mM્͝x7hE&L LA ->sw_ 1\P`pDćxI_&JM/fS$b mm\OtG yYECJ`+.}'E%lj@<$8<}?'O0TAWNOl$d X)nC93DjPVw`ӣsF`C ӝa>S1mSRT)a~E7:=RK0P@x h@@Q >r@Z;ozexӈ2(v$GV#6;#GZHt719(!ꖔ{2D z,乸*MŘ OlaG1bdP*dҿfD3 PKb5qɺ- SPa0˸$DomC$ =1~ۣ z,6LGaXXa#LQ!dH,RxmdPs&r܌ɔ<tOkܓhFM5mKi(6/=~c!DJ. RoY VxX1}ف e` _kdY n&LA"ƋǞ "oZR񣈱exßigu19_ Uss n?"1}ІowC̿kH78VK&ut]ݼ"#z240H[W0[WO 5-Dۻ\.wXFc!:@pړw=kRJmg0W<܈f"V! ѥC1cUoXtGlx]^1 *A-β:;Dz6NZOi^RjE n8~bVǵSgxπOw[9$5[`'o ɵDEIX 4}9/:$SBD+hG{8Ac1Z F65-eb)Z=Ђu;ߊZ=ߵx]` eIH(y+u=p'mL6&cLU»$jR7|"*t΃ʗ*^- |;!J\er7jґa#/rRu^%W}8q RtŃ;hYg'K[72|c.Z#~zv8#r{k1aR !_N8ENU덽'_y5r}ʣ{ PU4.nG`b])`ZZtD)29pL;G)78O~rmJaXZ\\>:;ƙr/5?O?Ͽ@v>w/_BZ r<$ĐZbC0hY{62\./`0_?koo;~{Hx͛ݽV%|1#3G;c\C|a{{Qbcx}gL|xG/-./[72~Fp– 3xV")"|~5ECH.kR.eQJћG]4)wKZ!I/c1KvNEڥo O'$V`V@WLJHj:Œ#gL>B(HO({fgfPr GY{F@)ί!Ā3&5֫#az*1G/݄ Bwےdp<ġo"D '~%ǩ=bIg@#%JH7˒YaQkgIXs`7LJRNodخT+7G!S04h8aqjCv3Q09tC4ˢM7V1ͫ7rhd~&R 2zX.a_?%KT=CԑpHbTcI̅PhZ P| #D;l(u>ka&ߔ4at_40 z$ԦwXG Oɔp˨,.AoYrnT_RqwϚoiEi;(" LwܝzD2rTEden.Vwa?zއ}059 ٓ/_x,w 3A֣U|om/6^O\'gHӏ@+}ܜ7R$ NT ^5EpyPc0*u0G hE'apˉq[3t xJmIU rV(o( K=3X<- Y{K 6gOPkk-ˣӓ#՟>}W{+ht~7Sh]nF\LBUT'goo [?_/`wox `xh{N/{/wO^lm:;=Ʋxgnynf+,%ji(1ȱZT[+wFYmzNPє6޶ bR b4a/Qh^ew'R"Y`>u ,ac"ťtpՊf Y yxު͞bpdtz~niy' śا 0:;<%267677q01 u,QK:G.B8]UN%Ҭ.Ryi1=YB˩Z+aDF+Ms)m",IqJ 0C\QϹQ`oT5Y7 #(FQlEC3\k[b|K@CYH!dpj $ƉвJF GAvcL|hGƂBi񹧡;1=$~Z Ө "Ԝ^i#MBAe0TZ~ABSζ{[`xHҿn.o2\aqI AGLr/HVZ̨5󠧤;Q&~wДWNe4CтTA%[n ڜ~ dk5f8}_ 'd) H31cRH,Oz5@cѯ@ش\ɟA~5|tĻb),ZY^N;Lhʶ hj!Fghr:Grх9"~{5r-/-n}}\TR4&B) #; d\Fn/Wr:/N” 2xO:Աa$khJ6+%nebw?_[ׅ3s\(.<&3*+"ف e S$ABmWH(P*V/ȧeBpW4ϯhȣzYs<=BTgp~rtqzbA×/_>{|ksJsq'޼^C`Bij,<ׯ_xyIJ{P쫞G`#p.5.oalggp~Fssk[[G<䦷B/^|x1탃/7lnfp}hmpxcxG$̍ܛ_y2>3 "!'Ǘ瑄Aq3 c$֒b$iFBJ$$]R6)rjonR MZ(//S<"FC[6-ϋ@Yoz3L{tg!G#xFF 5PLřɉ˓7o>?@F bGl`NN^ʤ 8&ǐ-3'@-)GXi+6-h4ҙ7zec,LŢ M$cюCEn__, &8'@Xe7/KQHѫ~R XOxJg+8V2dW6FbZ!m4rB7!͂x4g!tXL݁Mg8Ҩ{E MdK@9. 5ǃ)q9$ 歇LزKݮ$z&(XWZ$R(ɥ`yhpӞ G$gvp0V͜4l 3]lZ1GиGVȢR$:ِN5@!ZkǨ[b!K?Mn^L U.5dcHg,|Z̲5h=|W@]f>ABaAAʏ3izsf)u_Op46AjF4v) v w7(r0tUMZ9ĆlT7f(6@vWѸqcA!H.(]/(t>5Ѿ,uxWZYpy|uEd!0a^'qioa 1)YJJ[_(_c#UMMRa427sD048,@5eVeWt6 ~IG|,6:1Bwv` UBͫm,8n>T=,n@LT~v´Nn̨T🿩mt_jOAHÍUWZl殯[X+1C='q2&{%lRUP/R2uؓ x%)ʽw?B~d{M'U- Hx0TÇ+Ȝ|~xu%+-8mgf'f+_=8:1g(HfHʎn=TGK++xÇ8mqnn'T6Ga‡fgfƱPolddHgc$>D.,mb|lr #|ŧHg.RFwtfvyw bff٩ɉ#A^RHufpw|!ɚ;%VZ=5m_msLq!.J>+fAEq!mH̾!1gW`~Dq8ڰ-:?:90c8neVbRF(/ܪPKfc}#( 9F;=9Dܸ8=@҇>Ѿ$Gh'͢HCkCqGdzK+ &BH* ghJTX~%P?T!RꇍT  ª4)=Pb)]B?=y 2!/6ނ$LXeD.HOs 'sjկ/$S o]xéܞpˋٚ.PXYfOr%jD6Ccg_- WC$Кz%7W":F?jCn!:Y+3_ZqJd,6'غOˠp(mxUH*17\'*.TSC%H4CpR>0% PW AED׳݆<$TZ)&R}7{e(b# (3WqKd/̭ߞm .FkO-)9:IAnqHʷ Bӥ2;܉Ƿ`;(_hSDw Ylv-QJfrrgoz}b٭LS(Ъ]f(XtYXɟ: % pa/|`kڦ󣽬DpGfgAfadp)[_{pC sَZm2) Oc_ڣ>|83356BMxB?~@8?99=1208-RYݥHJ4:914

    33 $mS(c"m;|}"!L?(/.QŧVIcp5B 2# {AO(742| 66壳AeGKae`l,10<:%q2&qx a }p;+yX"D8I4vI;U&~˗o]D?~u$)>Y 2¯Davz.x4+D  e2{N@[{DL>89~# B6)FJUBd#`:<&P: ^upp'ZyvԂiOw/Չn!"h"DA])'I?;y nFNw{x][%Q2")Ei&/q +cA[dDe8h䜒<$H4A77E Bۍ"ȅb\\\vhđ'(܋п>kP_:V [)WBȵ E$WS1(/2T"*yaժȡSv_X*ME؄Ï[qs-5x˱'ƚ-glkȼrShtՆq&"F6@]Q3 njB50P%2i4C#@ )\F K!)ߛ-e\AV!c &m2<4dw-ߝEVAyHN0EЎH 7I:PrAH4z6rznܩ|tWE'^ifR[k* +"FE"鯋lmx|! 'jb8ߊ3 # n7Wq$G|L(1Fow3n`oFΥ6HoF!GB 㴂ʷU ׻\~ge:_ _Mg8gԾK ¯MrI+o}?ٿ[wmO*xw˫?^kۍ#5(22zu39c~/Q *ڰ^h(6_"RG#R^>S\HJ,QbԷJ#,ǰ__729ӈ+s5)z-l)#/, 찎 e\hһtIuƣh'Np2AG޳TX@9s8 ^lgQiXNCE%G_65|Ox/ӽOnwgF>~,x~ruyR}&>* p7wr% |!:L V|  /kZt"9T'x}df3`ƻp'h V`.CF/a5 &S0G D'3ң}E W78(pցW㴅=@r xǺ=}xؚÃkU&Fp!ۯPCKlY{\< utKitN8X>?Ef E`/~ V\_#܌:J䱚\]׾G+H!҉0X$J~+7ɡ5 A5{Lx44,)\^S6Иl[hhS;bW8uS/b]7_mǵւG}jPj4Ln~X|{ %"b؈knLXq^}|qb:&o ?e3L7(6 k D\p0NJE_ee Ÿx2a5eb&(0phmzjP؈X͆D7h]udS5Irgoq[Zz\[  +]}[]D$4 fLǷfYg3eJr;KTFADJ(6yu[W4Dۮ0k:"p_pEղnm-?s2 (!/Y)G 5[4mɟ|_Mw݁ F PAU#Pp| "1UaG:[.=5t=wQbŚ?S_`ݹw/./,o](t>W`~%ͣ0'/*  0Бo/Z%1$(a3]|ʴQ##jBJv檙$St.j&u":4TCZF/al6mm%0Ib5fQH̑y*JJy5frXkEIk:0>@su  Yoj[_ t:25>cbu0>285:8=;֙¦I,US70|7ikaެbYL>#rrr0E#vO/6vi;FnxS3NNφ+RM^Mu8TW ^&g;\# I=7!&DW 96F@WgC5‹~V@#;O,Sf:$[mG G˙$JĴ.^3 EmwQe2ϢHfUeСs)@YR!$k-N@,~rFw^AaA\_{X# i>RF I(!:2,/xd%83Sc8ymthpia~=1uI:dK`RGʫɧXX)}ED<@Gj 3 -rrɷRN6Z,. (&"΀Uؑ3bt1|f*jL82 H(Gv$%=0]Xz18Ue挕DԦ^q4[(~Yȡl\WpApn=n5}#%<Ԝrssؚk\+Wųi<҈3X3f}ǤNsN^"N0lLqVla6SԁPvu@FļԑJ-Eߤﰧ"sI ;tRso)Y$BL<]״@| 1X)"Dn" Dd-C-R_nͩ+=%,C" *ڱ7f 1"̇qLΧLaA{Z;ڹ+eBFh*B;VZb>٨NE`)ʒ@=B957.WCU!1mSU}_ KnF[o҆hr_ `za;zj]ѥd͚oQ x_ُqJ|'>F@Y 1ϿxGyabr(hhɺx/ȭX5Z#+~t4ca#֘nE s.>c6;5XoDt^ .WXZQN+KK:̗Jl粒 `wjV+/XOz"wb+TdQ$ca +/bJ ^ 석k;Z1,V֭ ͸= uXrmz}\Q[-ëպP 07t¯p"XK?<y.xx|U.w+tmb -Ey>7>^h臏V1xu^F:}(xCo|> 1HH0.{Q>f5hK` DlY窿J&LOȹA`ŝ)<(uW0-6ekZgTGda%;nqњ "d-I]/P|ɉq[@?`xGa-:iat3NN,H0j8jhLRBlP 8!1no(HaAФ GC4' 43F`Tρ:{gص0+< z p66ܐ.#=׈;([P:4Pjm!8+r@2QԁSBw51f|dU;C|r %FyigH0Ǧ!rTOe~+Ma^R= 7JHO.̢1(f8ICI^Be2=|FJ t1p 4cZֳ.8\I#y\<5T+/K~Zpe 3gb*isg4lboQ-}&24n r74HS B.ݦöBfj@P/UCѵ6sSؾ[q"04bv%x1(4"|lyVCl˲mj 7Z[uK3f2$&pjR'PFC"^T.ZAn[5 )R䭙xɨ UD,69ݺ֓o?!>wtz -f+ÐxS~~zk@T ~~~y [nJkItc:T9rCR/麓_ن%l%ZgJdWRWcKSIKgmjƌ qsí9eUCYD}<EEG*Id D^ ֿ0"=)ց3O`5B_ZW_$kG͵~Nr[￷SX7:80G^{(ǘjUo._*uHS< DN/bo JA\a%k7?}nXڧ76aJ-L 9 u$N 6#҉J/4 k6 9j\| {XG"v "Ѐk#Ja"rOعb,A)xïV/ttFQSN0+[e"EB34<9ԙA}Y/tD+4G),tH@Ŵ?m^~3+?zHW1Pm9dR|en0"y!=Fi4!@8h`xNa)U;fE D~HTW$XFPP5!Ph05) PA 3hLaϸ ?!{z@WŀtzF^$ sgcTtjKd YbC0b:>`>| u @|c;i2t $ 2F%S%u9neGE, ^&[J/Q-5e~ɏԸl6jxu,>J3Gg*gy-S ,`$)_|`,6AK+-%\)b5 gPrZ^pl/o*D1]ok0FQ I8,eSV 6sFxgO g'JMu9_D0HnSV@>qL<hPIRSt#ЌEN6B*1K(ncZvk3\qVypizk[wUֈ9!LF3/ǚj>񶨴E&d`d퓺ƊZk^Dn=BlSmB<_u5kk -DD#%E4U_oP5t:(H?C$oN'_HT}"ƬxWZAF"d`§Jڢ噙idݾ/^?/?O?'(߈u}e/+ɓ?0"t``mO=u)$@hҶreY(p @|u7a4iI^ J8P-adhc*_kS)yН?rt-A;EDs& 735Nc=2;Ƥ 5H @7 1\MFʍ)ۉi 'hG=h:@%UDLu07ծZr ˇj.ĂMHe(ERKRGoGsgs`8Z܃gKG铥;E賚3ᰠ$p)=F_<*81j `YHStP5\ﻆdžnt' ]%g&5>,/}CoF{'{ƯnD6DufMve`p,E%\ 0܌z.љv;/bM+vpmyN[קz[?!ׄC;<0zB8xR$e[0D)@\֓00`elr>#//?o~gpx dM~ځ~yvt8%-n1mL_pG@5_/\!F!Fp.P7nR#Xd@f?<҃+8a C$X %=yP dё&NwZFf ~na xP޵D;^<2FwNÑ͊&9(yx|U@yymtY.z.{.{s!Jg~* $o3IBYil~BdIA1;GP_qӇRg8C!$b0b RUO4/XBqQ Bn@d2>YkF#rAtĜ:.VS9M7ye}7A.bĂ k Pi:E"喰XS fLQ#xef<(z(5 O10M/3Ԕ Cj)@! wtkηjrI$&BtvٸuO׃dl죻&N$!?-Sbj%%ůB-JԔpBOA}L»d!(Tm27i\i *Rl`CwPahH^& .qɈH 4QV;Ua:-5P](-[[hBT;zyro,[%xf۞嗬1wm+~! sDރR.CҪ="ȫoާ(H9q?)G{o{ PSH7V8bb|mOmm~}U GomXgqÞr[aZ"I$b[-ݘU|#դFB]eQy`fKEK\^ qw3i89h z'*,rz5_5WւCprv$Q DE+q9aSz;;9`Wq<˗KtA?fؙ2v]ze/Z ,_WaYW9=peza3Fλ*Oj9N*VCvpms x\8|ar{:{mC33g7k{ jJzpXC8eu&n!&ggp,WiTE mӶVφL7lD^HVƩXnzvvv6w 'p$NTA;quZ2m('ggPa u+Ќ*9:9FeȥAT8:L/{>"3S5EuwHdʖ 1E?=Cnzٗ/^? (m~~ÇKvN_o ļj(E{Dqgt*  Q+v7NI0%X\> ep8% hƚ$fefB;ň0WeG)PqIY`sg1*f haDܼk"Dw]mn힜#@䧚gPmsg`'8cCd*Pě{)v %0 F m*CY! T 67߬게\#Jr{ }!W28]'(NZi"G8T_7;o8='| xɣp|Q8䂿pfa\{D~-VNE*. Uťe )G!4F%/_$d'DQ.9"TZؔ 5 '`*j۬ 1?bFy@Ijsw+r bEo=* Ra ; A+_24B F_Jr99X1aQ(N4T\d[CGJ jd;)%tIheT0  @Ֆi>rAqGY%ž3(`0JBf'v%KRג&~g~oXXfSF!qSL%W;*t_ϳ W5;,x7Rq۞0E4)-TJm42H oV訋njI)`ºpBtnUR(9uqH+q}T~O7މs{` P}”5FU;59Ձ[xÎ +\R W'ol?{ve䄢&'r >T슃Q3 :C^|cB[pb#'pn֯گ~Q-.}[E7?X/.V-"YnMDh@Rݯ?phfR6PmOݽW6_yG9~;?_|Wn+^ŕw*-ye14E7P )I&!  7-Ny(rwJZЗ#b/t`[l#Uh7CDop˜½>-@b)) x12Զ=7cxUbAI xuX^. @r}kpwgtayqtjJVmұ?w$;eG\ r* 傕ߴˤp}r(Y86114:YE>{3E NϐS#$ +cHT91O^$qB&qzJ'zsM60$(@"%\E}—.*$cZߓ,TiJkS:g={;{ A&(Jx_/;vPG$$qJX rk:C*zy;ۧ;c gN..vw uF&fg;cͱ>#O;!Ʌ)OmGχAT7}ۻ8wbrpq*&bk;ϟb?px"ND u=l@|ir uPb#]3'kgGB8׷3:dS׽FKLX}g5JRmnnlno#DF&'A#v:>ٔ;.* v1x?oGM^5 O2g_}wfn|;;;;AӃmE % z<؉ +{'G'[ p/8=>?;>>c4v 8{GG{|xp!bt+g< hwogs{óC"?x|pggpo{wwv7͝}6v7wqe{ksmџC-"1A{(߭uAlKq Ca/F 4rJ&U0P )-QSOmBT!teWSAyV%}!VS96YƟgP$8t2s]EևŨ %%0Z6?B˭Ղx*^[5=4BJ3Ly)϶1a nIYw%"$#HW ֊QH4 vdNDj)/NsAn*yR2 ~(]^sf8 z”zB扊zQSŕ3 mQT_qfd"XnQB?:,-Rt\.% XM"mnh+(&eB ożT=x-X"}BTHUц[#s?}_3$p{|AS[V>5S/?GCHJ^3o ?l 8kBj0~` \,:U˙dxq5 PXZ_p{#hgSg~[o;cZ'tXͿ>fUb.wv;~^80_{x;S{(P?ϝoyaƯ $o/^Fg ~羵r?K}JDк(F W~oDi,a(hA'Qb?$3E!|=FJ7B|v6ˍ u]^kI% ڼ-r31/I"Xm)d+ u8K;]q\ӽ>5%)4ɼl# Un/80<6_!1]{`"o>K036N>k[̨5)&MS(`QT$p>֫7>`{XB-VAQr:WDI%enD͖;'Wo^>y\31pa8ݽ8^EJ<А%nF'f&;<ЕPQ|ҽL"~N?K9!'x+0B] )>֧BHba"~8ŒHrGEƔ_lz( 3҂lrHWnQ@@Paq>4tٕ[9[YZLeQ8^#ck?G?S~-?zpnna콖HJ\GJ>eiCDpl*d,vмZ[>Qpgkw~{?y767~޻4>4=rpx_uo}s3>Շq{$  b_=yώ__poM?Ft69Q0" htGPYn+J6p+LF l&bB d,@W3J;ZA\dV }3:n ͩ PH˓K(A 28PHz(2܃&;>=?>;C`RRx6b-Ɍa?pyugCY sQ'.dd}JcCtK#$|~}~e.Z0K^0pP%R(3m.+aTAT ?ӳ9R=AB+,=%LIZ{^^e: Tڤ`#?R_i|c¹h"QT,NpUŒ^ѵ d Н‚I.2n r>N2xQ_Cĭ6FH3MJʤx*WÉ+4OV^{+G־Y\!L&+'ʻ۪jk8w6ygw4L%w]ϴf[TQ]zy8ZctMh]6cFJ<@CTڿa6%Z,!z'B'oa9mM(SS\#3DWN؆̬ImPh.^\8?(7p2WʱY)'~>oLUi]qUG@(2>`\؀:(lL*V87aɒ_YY?'>W{F%h=5+_FۨessXYE; =_;h(ܟv'_| F%?>!Q{^l~_=U$O/.J!'(\bBZ! Q+.C*fP| Ϙh7Y' ަ]+}϶U'}ЗEx02DC2Xe-g[G :8{i Z ],`flX9ßy|E0*0~x2qiwmhU@=zm/̌^v`KgBAJG8V/(HH@)2&fF@cpx.ϸve\)^X6c99=Q}W<^p0{F04ĿAчogR:|{L'2\\aC: i}Ə q'y,àGz'M.L\32" S ypb^paHPqG.)P!V1qKqg<Ζ@9 ed3ԋ3W[`?|1YHcTg.0ld|-OM D@^/G榐0s"'G/ªPuЀ=}{W/_oϣ/~ŗld??S#8s?_:==^\XDhy|͗mB`ljgHcE A@o3a&&b}C PxaV8*XZQ0i_VںK}4Άoa Lp[9Bvx0z#S_v0{PzNɉ奥Y.5t0>#67|񑾙 xX\9|ƆBEBXn;hPn]]a(PcBE@5EN@N"hkS8mnį/gf',!H *AŴ/wggN hASR^u![)%׽"47>XyX~cqhnpRœ!uCb҂|G@CDbh_iBÖ)LoiΦg+eIm~Nm4vpAA')5T&RywTM*ՊF{Y5XAntwm٭܄lK 8&è٭uݪ̯xq 0L-cF@nFEoI~XvwgC,юDP.Hm8A7Zc5nd1XL$=&aUqѴ^^ ҙR7[t9NF(ᅚ ߚs߅vr<-46BSno?4j m X E=cU7 w8'P2! p+GhѽD]03Kh&yWSbwJEzcT"DŨxjW FQa##0ߑ_ҊX? k}~?wY+5C4tz;Z~ M?'Qw_[dz0|tտQ }#O??y6~?3WKStŸg?쉋r`|=a ۂTQׂIrAtC6@YE !b VUrR"TЕ3խ#՗FoW,ӥ:TC%{3ܠq[xi6uۈ3$:lT %}׫\x5dږЃ`m tL\<<>, 8J-rrUVv8YK:VBե34|Rkz2G| XpP nLsGR3e^ Ɔ5^?CFbſy^`X1:|}ĎtS<T4 ,aDz"m&YpdŃ*8cDv1݋[Θ}4(I0#&oS3` ,ke %K cu1j'X1?#w8B(z"!οO?2g-.տ79>Gp'Z:iV.tm졦O=y|dtrvjG[NgGmeM{~~F!b>"ھa47;" gQUzN_|g[kק/) ?b{U|P06/(32UA|BRjo .`-W7³-kP+6X*su856>;=G"imowp{cCF6 |~VVW<|`lD@hL;;O>`~~b~qft tn,$[&+>X~ȲmcVnAK}8;VVVQ\c55$}tm*də[F٥ݮGtEa9C4ehbzS,]!UIBeg7hjn-So~/~pg~F/এMvF'G'FGq)0ٱ?p}œ/ګWϱ!;14FƎ{=3x?zO4#С롪(7}c}_o;8a~r޽lAI#X]Q^: 33VV;cRu-E(njӆ.g.V0#:4˭dg|y9{zB76^o0)Ç!J*1(~|p3\774;,%E$'(5S#No0Cz,'@B~8W|k{+GTERHf,AX"> 4D)`=d`߽i^F8?*WGםi.5_ĸt+(*Q&d/Ҁ"YJE_[nuBg髣VǪA921f h5MOSr(rd}7J}']T](7kn1ҾaE]ef%۽*So.a{ '!/=zYuӶߏ[!&jĘϻd+Z $" $$hgD||{w`s+K֊Ri6&I %t\b'i^1Ä7v4w6Tusۇσt[0f`f Ei VXr(C`8VDHEH!$d 0X8fޛy{w̪:+tn;NUVVVVfVfVg.wSkFA~}I<#&stD/ūxhhXఁRͷEWQK7Zi}p3ꟼ?ߡ/gE(S?c7z l7h,7;" a ? V/ gP?uCJ_[O Z <9,#7(Y%uT;cRoc ̶l@E(M<ms7n6ܹ{))_A[P!Eu1k=XWQWa,K32hg%VnA\$!vc f/O>{oa9X\D&EQT$Y/JEnoͽ3L/Ӡ\57c稠iDl#'.c1e%[4W'{,,޹f/1,|O0xw㙕<[o|S^O%D8',fRY9QMhTBqXm[|_>oGߛIo f<dPX?*-l}68>|򵻫K7NώS`\Nb#ykjbdN̈́5@>q$ E'i%$Hp n(|D݀HEr'Q%6l c'?ǙZV!-q0z1,6rúw%C^C>%bw:JvVO~b w2%*!9cTMzށ)=,R%sC $wC6lӗaJbP?<PVJ+dz w|hNzs&ia@xKDM {q # {[-ti@ UTy.`{zJM%L2?j=J%j#)XXҾߵ~UYzubv,;?ZѻYh4عLog4=-4 ϝJvpB͹fz5YOĺV gV߾5{mU]`WV]y|'Q+wokNz݇VKp}km3V{Vq*~ՕYc Vr{>tk~ڝŕAŕՅŵ[uQطp"~VWVfVgoZXYsB3q|禱Q1;{>7{Ao`K&|fqN,-,-,b0MM I|S0087ض~1x`}v@p1>3Yx>$fg8%OOo}7>}kOEđ;WgǷ槾ѽ'~ϼ=y|!`3{B]kU80HB(XhڌWU,N6a|XQ9;EJdᄳ&.y:rKx BRy*9 }wˠ|"/0HM9cɈX܎n1h21y'WGg01ҾQb%Daă ybB,'8|F2l)dAĿh~x!L9`jr|ttĞퟝퟟ\ttl0pxv|uvpu~pu cڏ#,vsA`<'  F'c&mv x6HOItsЧY"N\a&(zO"DhH ʡr[ssbA&^U@)|)6I\k$Iw##3fH_%Vz;07|\KC.$jI,j܋b )EW) ^_Dѻ)ۆe Fsy"Y?_%Huv'#A6*xнKWY `HVR.4P@2SMΰ _2wF`>%")3H}'}Lk .YR2m&n)]b/T + 1&Jz%QKFT{;1^i/ߑ-B`JI=F+d1|1DJ:V.pvƧڋT^ NTMYu |pr2\w.eT=e4DdL@V3bC{s IgxA󀆾FW˅ :7t(nTk)W#L1+MxtEe`?eh\u #c>bBߵի7ӞFdH?MLU(E,5y|o)x?__$ցݔ$dN {u~ǧ?zt]/s % -7 G (:CApCE׉s}tl^KK)Ѝ0zZ+\.^qIj|ЅҀ*D˻|O O &OrG>ֵ=ry(A/]m-2;+Hp!9d46z¹!h 7'e% Y`s^[[;)~P\ a>+{x|'X$ uY';ƿ\aeRД-lZ.LNғg9 =cyLm @!l!8La) -.0Gjn^u|lk8s-1yFԠs*ǗX.N\]\)qy>b19 ez/a}b2P`q= ~8gzT2 "sĶi"UZc<%f\#LT[@Lx A:ՖZ9pz5q.`8 :J7)瘫10,)H *gG5| {F@7*IHfY+iRёՑәvu@aآ)6;\hT0d!BWun6S"(%&V  E9‡lVc>Rpa,TyТ y ǑSl G2)z?-QRuU+$RB8K=ֺ"jCc51-ݫS(Cii]lٸLjXNE91kg#3Maf(^xia`oop`~ l~vX,E e@q:&=ݐ͘!>A&--Z"XN s۹p IlVW+ HTqU맂1#DT&COw;VcM-zt:T e{LĠBG:9x m5xFU^ރv2zZb> kYV$z A|s>Ģ j̊Fmjf020i^Rj`D 0L a7niK"EX=o'9: u"3=|d/l^P7dw\!4+L =OSqܜ@ޙ+2TR® !_?7*4{b k׽aq# cLkN,Km,{C "3PW*򕕥;rs[]B6Ƹ`3T_7IN/يMssADl#raV_ # <1ܦdeyd~+`ATU )Y/RFcGM( .v _REEk=wlV:"xʬ\$xbY*DJc2r1Y;PxS@V|Y֢rP?;¾/h(p63m!"3Cس>FPd 1QP\L] [07^b`S8k=1a8d ;Ñ.OOIp 8A- cl{Bf~ː5 A2 o-<.CyCp'P-CAh'(|1*%$er3h>OT[S>k3lv cWt;.Ig2 Q+ ̂8p^?4)Sty85kiNT/# 7ksV~z7~0(|__O^oϾ OxxϜ^|Mسg &| "lH2M'p4 Jx"; q(3˱S0 5-_2:#)$%c=G1ؽa葹$6ŕ7uN<7_fq <`mAL M`$`Bc`./@#po!yP ccSӈ+Ay:905ELfzazfiznqz[s0>5@nx p'M_G(Tl Vs𺙜gs /2bˀYY1N9Xe á&h 3Aa~`|9dPIFڒ\y]R/hDgb2]s6Gk6/ Z*劑Erl|J [ա|lOeW~2*Iw4g1+F˳I}ΒЇhE*r(!B(IU#v+iS` eOw"S[(MqF,Bxodka~#Z?e"%Taik7!pmq&Qah!JT=h )4bEPHc-Xް Z©*D qlAvT|WsKz]@-SG-M ]mTR I8BYn 9m%D9 M@C, Ku*݅okAhc(j@^_1J]-,jf>')AP*"Ju"%7n&1FѰ /+tpC&X3w)!J#, R9L7s ڐ o_pC++?O?WߓCMiyVB[bO?:N-_},%I'72 /eјJ$yË0a }Κֵ\D 63k"azrԜ0w>~ XA)k rR"7ox%qŲɇɚŒ)-yQ>,J^iK0\lM,GH>bp+[h‹=Y寫b+>z" ihylJfS%3l~{H V,顄aТ2Ɓ:#3i9ϙ=dyçB&gvlBGJ+hEЦy\2$I(ܦU}^rbYڱ9X28 ٍ8sv O"B<WB,4k"ϯ8q|r|p|1{zE%xrd#A#P]ඁ\/]åL9;{:3w8\kBqd.<١N^L)lGO\MSY1Ƚ858/ggbgZNL!lb|b* 3pSxkÿ{HsDM.cs.&'Ϧf8c:%汏 H@g  zK3/?QhF@mgr)Cuf@։&eP:B :>G Z`ptovd)I&xpa"L*X|TyRy*TtP&&`n^az4ɅS LL &X&'KKfg6c[x1?15(%* F,%6 5!mČ&G'ӧWg8rt]̌ͯ2F,$+rcUԬB.3{ Vˏy(cΕ=;V#fd^^d[KZtPRXW\Ԗ\T 詡/U(Y5knGD^@C5ΙRq" X-gSYA^ Ro[LG P/Y]/WDhe ;֪2hϝWdBlY(D p1xj͎oERc7;qOQˁ97^d.Qw(sV@I+tZS[unubwH :=ry*^[gP&U5IK!7]M:7s2<D!rX.6iבvH"m0~h;ϧ' ّsНp%`#:#2 țOE.5my؜; !BqN67&DXA)sDO,|?dWwNS6Ķ>L%P͹GA< $r{SJ=(ƴLOtCI3عE.F|Be gN䘚6?c,`n N "P&>N/LNTEl/2[E<%p00ms 'gz͂$ s=71;7> ]Jw@Ev$g, :%ׂO`b}ʱD罹Abdn z>+0tG0(Mg`HL)tȫIB>;b8԰268>{ɩe(<454 ƕ9*x|ae__pEJQH *o쑏D R @(PWt ]q>;g5!K) |7$4+l".|BA1{>5{obj1=L!0w&,kzskg{):ރy ,M'gGpN:FcL_O_`& clP{2AI](0THة`b]`'p87%wˊE*GBdL /c^y,6q14=?i5Ma !FgDPq89 <|5uWG֤U<\R3ӕ=veⴧ <[BhLWԪ$}<+2QElݓ |d]PNUs)&QyXOeoEtEi*oӠG^3/'"=Cq#!AҲ>5!1)k`VgĻєW99>Ok Oq!Tfk FМyNfEf"b5 0vϦvХЍڨ=ף2LfWgK4MՆ:p 3Q px,>EB!6e ,`Jt\:CץlM e`r~ R;^zAvorF"I{u^pQM{@q+t)w;-gEFy Z2k3#`3[zKS\h\HH+ZY^VȋxƉ GE&'=~pݻ74+7 NSǠ&\" ~W{^'P[\.>ڗ/`J}{\Pko^C|tLo b[}@9 @`8|>9ğՕş|ڃ-4xߊYR.'>o{վPgs4m K* ̤כ/)uqacCLy6>K4ձy hCa,hf"TNp,0ıF efʂ>誃LA84a6H) .*;Rzug^pqpö9Mf TZُ$CUH?4 = X;Ôz̃ tMر~[++kksiɃ*OjnaSN62 m܎E?^p=v:|.&Ӌӓ}@(&@]AeIuOL[Rg⨋q8`\rvƉ9 \g|bbb Oix6(? s s"EE`"ذv w "?BL.,/HDcJ@N/tG'G;@Ӱ,//2J Urj n#SnH(x0`E^BD[lB 0C1fXL 5y>t8ӧ/6w@Fwo/Z_EmlUO^n#˭{'r8"7m>&.6w6^\cZS2p (#'{*4YJ#9}9gwzzr_T n1 Cj{8UxX8n QHGÃC7M§]9{';[{x[8dscckwkcϞ=;<<Hj'㬜mmyz5KjBH9.aȲ>ӂ^*IT8cc-}y9]yg%KK*?}3*{:ʢg'( d LC eⰞĊ@D3XӾ#b.[ҢG^rU s݈tL"VUr V P UO&FrL`k})v dPKK/CPe&M~0T됇Şn?S8 rH܀rGݒd8$USrRX0o{@ * ύy/ߊ~h@Rȓ2~i`v0/4z4D玴(/ٲn{PcjLd`- lWuȨҥޯx.1#kgF|D*{E3S? P?QZd02A؁T_wvo>uMNddLTM ?ǿ66{? !">dq/|歏?ܐڏdPaaao8̟ Ѕ~AK(x ۨ?C;C3?K?=IX98f1GǠtK%{_{O+v?KK:|;_9 Ǿ02ǣ'Vz&@)OLI18b`[wiF)f($*)9&*We!tD0(̼XTY|^#A As?n DC^s U{Q?-2RE\ ~SacrE/D[zo,cxIsgcWz.-o{3W4~xgg <]\6OP8G^py1w{f޸AyyȠP'J){XpQI{=S~m}r)"ǠQ=;}pUv ZZ|7 Dr>[aVy1 {HPJ7K}*Cczewqd&pB a*L@Z^}ofY2*SD ( B8ãGWW];!l@;|{8%4Ru~ mBMܾf76wq`mu~ihއ܄7cMOt{?/gg?9 -N-8d2`~C7zɏom<`ѣ޻ )s\`sd;uX")ƱlA{Etڭ[wnY\@n&%ѐj<Ӹ`qoܲjRhKB%ezr׷ ҂oono,M.``=ydG/ <( {3=tr9:Swc^@g,d":^w͙,zp^=h96 a<`q1~jĘ̈ԃ˖G T܊k 얄 $;^4)')E0O~ťᚵ+֪!(b,#%倆pA"ϯ:aU)X<[WR F*Q$cIgyD`J5+AreV.zTu~ q.Zs> C )qY@hi5D/PrCGk} M*IEB}`?4JNH=p)~7~ gA+vhÇ9$O(~?ôSE,A6)jq+Li5)z%uN[6B@8() EPzEsf#ñ&#aH`V!+ nJ7VD楬Z\̧Q}J?$BI|G%s`Nl˓' d9?+>zxg0L)," v1fn<߾/ yY} &b+>y|s{fj}i43X;,A6lRD%ȆcgJl6i&NN{ㅩ+8! 8 =?0tcbʃw}dyfOw_nln"27Tæy;ɸ^p?`cԕ#C8qx~Of܍`8Ƞ 751Y軏}icY5m̏!ţyX8GB=P S./}pk 0m^Psu87~ku(cWe`s0nZ7H,.Ay0^]͒ۡ(cU9X2H }8>1(J@'F t`ִ=@0u1TW-]E% S$X;ܡN(#PlQL}q|x0 `ÈiiNp9[A W p< ZfG!H#l 8G0'[! X C2bZ# ri((DE ]`HtR(<1#3ʪi)UàIgҤzsu ͇yAaYҰ$hUN5IPr&Mڹ段ʒq{KoQ<`6qˁnQ } Xqg{&x}q>u>!$H &PB;@=zGy«=9+q3^|+,Q|yZ%C,\gV(bD3L} IeC^VOaŨ'UBiFe"W@( 7jw{7-*g.* Fw[TYUo!%x} r&ecN;CD̽Iӹ{ 3T}+J&?"x#< *6IAu!WC؉u9pyAbWR}[sTussi쭂fG6oqust"Jl{4z@Y< c߷]7~7߾3?q_l©k <_yj ӡA ,`kp%.*'g槿|{}sߦ)V |9TȕBG_{Uie͏;G'|O|b8ү¸hhpKH{>sCHH흽~b| BIu?v.)MaQ$B˔'[n)k'fٗŠq3"E< Ohi [E@AЊwDrynzuI@ Ywr`z;vIYa?eEFMqEjʙw`u7?; %7I`l(fvrJ2.t%Lvv YMO(T\┱϶@[R ?9'0'Xy P z'I`b~?5+%jQ!д 2a25ơdDg0 R0vc` ?yb&4jEGBz+V%hJf 4N"騨q(48,Z3=CiTQ,t)"KST@Y[MUD}aKEi J ?p>LK ?*-,/ g#r{(,\ȣiR`p"G۹o-s<]"iòHQG89!W>(-C4~-- QqǓӥ:S_$ cD+N5h/A_]@z}+ZgƮ8@3cʯ3FQ+l1\7{׺1axԡH_&҈emg,/2Gu}~Co S~](Dp9KGZ筷M6ԹuNE@2OZyWoXg^. gosJ)!xlvO}͛3h׿kMk_%{64?|iiؠh60XV%3*dy/KɌyx@1\酸+s~%`vzSoY1<<F #xIgIxfB[šf|LM9(s/E~s1HNAޏN~h{^%~r57i.8p\H3(w-L͋9?eL:Ϛ+3gG l s<Ñg:d |\[[S[͌Dp3i!'*$x'-T/Q<p%y=`$ 6!;Bc /򐃐 hT*Ǚ/CylqF,g+7@I PI^A "2C Q wCd!H=D'HH;! Px&1Ł#}|<6=%n EE-pAh #\H_ G xNugF #~kn'Lb嗙=ietzfq0P`?z...VAoJc.D C h:D,j n9?8aHVSN`6VܠK+HML^yFHK[&< 8va\B1aJ"gZlAI< 2.EUG>NSDډFc=Оrxn1g02ʑ0`AP h_rgF1?q04]m*B=B\ -#փz*@QD~%Ml2}JKV(n:OԺ謌6]7ŪNHCh5vWDX?_nQϣ9=E%T;b-V#)5R&J\Pk3`y ʠ׶(*ߊAٛ.$& UrGCrZZ)h=U"HEӿho,hnMJSj+j KRبwCټ)9~|TTϝBIvx2E-O+4S`(/ d2>D?zM# 5bu#jhlLaC;m^WbPB/p q wLqLEPHK-cB kc9wihЙYH+ϣJ|0"x}$kr8]ȯ~ H<()ÓVHz/c4Az*c9!l|߂SI ?{{> dk#c/w}gKב RE{kv}!9t+m^-o4*%D Wy؊X+qm;O^ AlWa=Ӽ,<)5PO?lM7Y{ ^=ېcpT\uxH9C'I4^ɡU- "! | ʓu(Q*$;R[:xAiHFaǛ؟>P S @+TsPy^R Z[2)^=V\?P65<\ ?#+iTc(RV-#sATp-`qe D#LeU*=A :,swȈ 2Y.so#H\T1R<ͰE0}6pلjg#Āz,HϚO+8#JkKaPԆ"ۄTDh'@:>avYs :Rm8cb*Tu,0 I^GѮM"0Є*\3^ O΅ZO8Es[H`79??o]ig1rq 2kouc&2if4@#L4&Pc/tq}4& 5]k}'&rc(GMQ 1~\Y #T$ bC6:-py8+`4(rM W 9Vea”8"4 m<ۺ*v>*LrM-φBwzuѦ1h#xJ.R#̩$}.ڦo1tW}}c/| _/߅P9pdjHHww~秿=hw%b@.7(uizZ}y|p>=A}% s!ʑ#嗿oԊp__w|IAx){)|_};!՘J$\a[C9:Ӡ?37\-D|.*TRq5՝+gei;3oQeM9LE>vϢO @6^6`6|teXሹk꥜׉N/aQcGx@Eȡc;_v>`X%ƌhRP%.Tr3] o#e\CTn:Ku`"Xn6^_V) B&м)*WmSہo>*p7-l)*EK*QYgh"SZ~4؟X\dC` p$*PBL^h @gr;]SmZ:GDNiz(mBbE+t0~ 8S쓁t (i'BK|E0 XLЮ369٤aqzB'i~'JȌ4I@\d+VV,F꺨Nt:9̭Cl=&pt =xtWFs(,m&` Q qDf`E!" @b8A3(k{%U a-W\C8I`ɒOO=&ЈlLlGMs@?ڒЗ&ʀ ~HA8j4$!Ҹ$ wFG(Es$K.ϰލa0a[\RB_S!m@A2kKsN>C.}yy@+ (}3[Z[^F4 yEB; Squ-k!(@-_y|άir2yJUWȪӪXz$ĜS{~GAYOFWs `D:d\@v-1pcys) CۋwS𬪛ӿxgHBXop2bE %ᜏ|;Ɓ!ZwQ =0^S&n7aH*6-/q,Z)*x!+6TzSD5ѕJF˙ N1RYiDLTKV[q1Ҩy*4U bNSHmT"((2zsJ򺉤H л5$#hT)vğ8aV!7'mG+4+&(pڦ2MTeul  wQ/}|wrW[ O#w7ƌdE^Ƽ.$_QBS12G]ݕAﲜMRA~ .h}~ #Tapm汢, 5QI?P-M<k?v}(SD)"2~3y|~_Bj+u1Ywq_k)k:}cS ko p ˒U$q[|8s6فq#I7&ޑ)(eMLH^rks01. " 4)sHa|XGŃpS83txrgwV+ta@SP)04?8<i[#mVLGZA8L1Vx U̖Ļ>/_z\Y"jJDz"f]M 'KAzb𝍗[y^t40XO~^B-x(HKc3UߔW-AʣaؐF6,_,0?&hpŅg, s< AʅƼ}oS?)XBY{J|ӯW>< _z˄AXz4(U/p~gҋo!@vƅAQx2 =~9ܩנCp{x{+]n4Y@-8 F:;z0К#ppfKƨJh v0X! K$W'5ZUD;q[)5JLb"V$^g[bJҨ(ϥ $8d9j#^ar}bSݞU?[DF?@R[Y+LPpB IV4;Йk/aVB $> vVAmZ:X.R(5[~AidHɔЄq<&>,\^*6CbjP p*AQ O [soLAr @#{C/EaFSyrN*40 ~o DĉuTO[zcT%&# giLC~EJ-x$΀Q|nHB?u 0;Xn$`YKB |HuMN8C3(KGoP"}vtpw 5t8 uaqC/?u8b1M;B2.F^ A i" !’qQQmh6ǠU r`p|"`ˀYZ, ,@U4 /1Z@K<)AKw ri IDjr͖rE#PkHJ6%.H<H C:ڪbFT+T}^_n"e}mymeN Y٬⾅i99IE͍<_ ZPgWYЕFB;7.e9Mx`ue>LSfB:L >ͨLHBVv *7IڋI|]d 0&s1.SEՀ4ySDI'&LV8+D `ʹAr_ kc!\kΟ+WuDGke,(m&{SpѡZRKo`c>"UwњˈJ[̦~./5 m.F|b8(o7ri29LZru$wrĸ c3@XT8d>#C{V5vU/6,?TIܵUElrp|3 Zvz^{nK+ jךʚ1֐tPB ioh((Fl;[{+팦troez5%גoF3Ǜ=;ԑa.&>^$!scN1/.(݁¡a2T(H[͡N6pvYm۠onnQ`6i03W UEP;^kl8(wQ3 )iRXc:~z!d;:3n|AKExF8=1_5M)c| 1$e )=XDn+땖[M&OihZ$tVMfAVL)Y˝Ҷ2ŷZp:5"O7[Aϑ1yv0e")szF6Ź b`J67A*H! 3i̓B#[n0Me:9@? ϛSx MG"p}zbqvrqi0C$#$1SS3Wq!DP/NvwT;X\@Լ H5?<=فl$s728x0q' e9i;RT{2˺tRv@H,6vp(9VHZ4(6D@f 4Ap2>dwGXgd9T 6xU12,u: ;LrH,d(nW!I@*+&rb#zg< E@/<;F q6"RZ<`7$OT^Ie@*20h=9D4w<*0&Ҥ<`. I,c`j^Zx+4%(F o .X(!bSxiӓ\1Pg29ο#Xx>7:c[P倓S03sQ'A\St{k6uזWi`^{Ursߜz#7 0 0|dq6v`YwQFis&kmU>`qq.ϟ< ~xu>aKl|0!^\dޟM~[d\rnTk3N&CԬK *rk;Zd14$l 6<{ L,^3|&kH|f4`LZNāA,f '16\9>M)f#_+Kdbu 5:ɊbmKH.{BPJTڍ[ QS(,E!BѿGyj` 4A 3lk d-jZ,޵ڎKm9Π Z~SM}[U:v+w 1?m =©m,`75\ ,*k6< 0X*AN12vP]C L̽ohȶ%WV>.|*(,PB_W AxHtУ4^X^2{{*TsHF`$ߕ!خtݥ.m"^~s}CU)VJ{ڛW1r G1BhT%K}?Wh:[J'G%<zu>_ gsb8tv %)g| Zfh_ ;g>IKr!w\t*K3IaIy ,wX#\t wz%΍(f :!3̄OwM)c#{EnJeߤ]*vfa׊Hh/fTUh{'+q/1 <^Җ_T.iD,*oy"IѴ[L[l/(Q Z;/u3+T!P[,}je4 )қ߅~ovֺNtg@#4c3{>gSg;;GHտ01>[ eڦ8ޚ\1 {gs>2$ e_`$eމyl@.ѣݣ̝wqa{GO>PA-3Mhc&29ph5A,wOXϽe(BbܹI4Np0bj4J\P|{2) ĕ`ő&1#L@"*y< 8aSJ@ lv_)t,_Tp`&9gGoO671s;!#X`RJNW `}C<ar^`0Z:NA Ѐ!ϒ@ :qUR&к*hݹe'!ϵ̾[J\<4bf=ɸYyI-"lĦp  nuJ6Mk> WڍGNsQ:>?ܻwoeuEY>o9dqlww΢ ;4m]rdpiC4sMDJM5sN ND]`BC\={M ^{w b N BNu_`pvDZ\=d{gܾoaiɾ_t 3Jm;Q0S|WqӾd7͂D QulIĖW, V4`NZxFTuLz$=QN*%!rDW>r2\\C)w3{ݮQcyYυ"W?}=-~"ec4?:[i]hYF~ #|-zⵘj_[_ Wu^FfŇ>fud F*x탶:JO\x!wf.wkLF(zvӡkH4>;@P$L<*X黥jVERi:Kճu:ˈd}]nH CU+-H_kLvaɉoח+H?ťwZȐ{͔>W uⳁ7:8A"C@\g ҁYa{lqR\$'.H! +O \qb Մn(ۮ_zhڪO*Q~"{ja@ RLꫫO~ro~o1u6UhںO[yI#ԇDU w)l`v5:$rzWǹ;/ G#["ѮĕRrWByn;Dج؇[ZU$/\c/!0)]X6!ur̪/ϧ@KdB wYe'P9,Y8@@R 翲,H$0E .(xPz!B;l,G!~癧~:\rGuUoc#<*@"R9RSMVZd6jȳʱO[ K5֭.cKyrf\ϐ+if*r A/tT8gtéY<\DxY76sxL WfpR9' *W"h%H3JM~hKYr!CՏ̅ fOe1^3b@A)xxŵT}8rx]AGy֩!Ԓ`Yk[keH*E]ļMQlG~ uFCx{Nj~+WѨ(Iz2-5$a$&BIj[_EZ {;$i$ԡhR(41D;PGfYvl ۱w uRҪ1,Wq<.! - ̉>AQ望>6_y7$ *n}*$n()GCzG.;wn޹};'=ۑ(~מ={iΘi w,Jfa =RZRf?lKa) Ԉ {MA@H3D~0 ?v)B9hϓ@*ękSqHj5-I{Gp? ŕE Ӭ`bm2GYWF9!#PMQg*-:\NsO#R0 uo$K9 ȈGٝg/}2JLD^J?)wtcCc{.8*FyjmTPꊧ l?ebbi#طQo #h^z8<?KXt8N{O RfB9u'P. D&Q#M[kKs+K 2,Hks\YC, 3eq4 vK9;c{JQU]e&qЉ`E>~Bc]o&)L tzBxmvh) fgiNQ B34'LV^\hg"fpSI4ƘVվ/0}d4+\NtR# ٺסb *j Dǡq] GLɦ2c8;a U&9&`0!lYPDŽ-n4ˆzƕؚ]Pevvʀ4DRcW[KT/*JYHBQ`\2d!IβoTN Ӄ9Jwz"j&&1 RO/疹 5B|]Tber8iY,ަ(QITJ Ҕ!"&b6BA!2D?!<߰[d'–ΧJb@' eci_< }l $-Ͼg_sggwV"P~b3Fe-%N?QwᆊE/;+Ww3o=\_ DUuZS4e_b$~ɟp3>L^<0wyӡ0|G)LșlDAqmVTgs=EHYcKTSMK |aKw(U>|7A !1tg0%eBe&KT*eޥ\;c_B&'qfSTqVY$- (@` < ދ͝]zIω>~dcwyb0gw0=0hqi בpya)!i6m g6aZZPEu8U)th&f+O62qMB=J1#0{gMvr #%=)RFl @ER">bMĺCGHo~ ZDT gjƁ۷aXa) 2EJ+>=>؆gii&eRf.|"Mɞ NdI@ 0sKCygvwg$flgs *edՎG%hFvQ~ yMFV6<ܐ&F&WEC$ G6'(gI 5em?v]Hf:}#'ʕOǩŕJTk4륀nǭQZQ 6+(CA|+39IKJV"QM9ܧ ;ZRϕh*yHi]vGlU^D1Q~f ^;ׂs}XdWk:Vx=aRGN6S6:5 T۝RM~fJ}T#)_V/6Q/McajSCFj* q}X*A[ ߲|a$tKf Apg S^O,ċڕȫNrS>zwn AմO} vwoU֙,&r/CRRr `Ӻe+w r{D@NE]r e ޕ;ytS:+$??!"_wDch-y,ܖ%s!b@?PDXmAI$J'jwB8bl y3(! ȑL; LʝB>hsC)!u ~!€`% ɢvjOκ?ƨ`Kt& mf$"׮ 7yTQӜ%G Q)U$9fT|˨1U2}1ަh!d_%[EM5;ms-7[Ci-@dva(vHbܠUY "jҸ]M|T*^><^Yw\`V)jX"6TF)S9>=|cs`ufi_e3l @GpHbcY9h[553ara`CiZgF:#Ҽ2r Iv7(s'D2428 &+,ϬѲ@w_mD679yP'HnkXVaya6 0sPz>Yų2H:q pǃi%G\x<ÍA sr@LrȘuBN$@Mfrb}R;e~1*LD=@x#f٬)&.O\,AjpVࣂ6Ġz;/L(Ymi\:`35$H6l"6&#yd CF e‰ZeNk5-k,B$چ%nU[` fթfbF  (m(FCCמQ7ӶR߈@K6ł+յRx1u!fDUcѱԬPg@X9a^ @6%4RfgB䷢U x5hK]F\>?A*Т۝2nOTkuYLhV);*"%+*kj3x&oNEFOX2&yb(3;)#&> [ZiRRV}Qj^exmI!âTHb{҆߼<71˩s'ųKW|w+֖? HD䌭u_͑bjq %0~ۯ[Lԡgk}eHǶ;;5 clrs k8Wo~:;PcCC8<J' d_ ,`S|]XZ\ZBYlER4  JRc5 jx0D(NCy5jgg[|ٳc@<8[nllt>}<4ŝ[~KO=X F"&AQ3kh9Á`04 "3o}ȋpd21$̓T-MDv]0bv "\F:&}X cJk^/wѿft{p~: h/P$#,]S}zgjz$$Kwgv rb 8n˝x4̿ڽ[wJ @Ёpb8ǽ i+ d]_~Lgs{+qɋ 9 n2jLMSl &3 {9 3 '(⯎ A:bS^# #MM2<ژrw9l֞sZ&yL 5`@pP_hܧGwVK*Zn@;8j{Y,nB!5vcPb3 7^nBPa.N1?|̓29խ-.}ZX833 ˋ0srX4'0i! &72@ʳS8Bcn~,Tw&yF:~r|rppHc<X* ak5c@> ZuRN5)DTk F3qfH F%m|@-#qp2!pSIJs<tupx[prMI@2J9J | > X$G;pYyKȤ`,JxSpZ6yy\9;D<#{%@ 3WWaTAdVo6lae\#Gݖƥ$;  &;T/^U׫ qhxi)('Sv &DceAm f)IdWƚ[(2{e)W#j)=j:WaHZtȒ6F/JʦO*hRUÍ-ݾe"ϦxTa({|[&=t#.}ʺ<[^-*)ow i^*;pQY(7=, $a7{XYɰN6bH$Сr <s_xDRf( 0h _j8π놾yw|p\ nDf Y|3dۤߊ 3~nQ,Q4=IڛB(>bS *FjtHf~'o=RgL-34 M,R4y(:  i&WvG i$Ǫ"NWVZ֎ŀl(YJL'kw` ľ Reo cFx.:4NJ=`w0b=s5l+c (Wn8m#j9WHH3dY1rs3|q*~|xm'mi mr8h)H[~ܾyiďܰp;;)W#u#f@=y $0b0̬'TFG` 18,C8 1TICh?ԣyc&GI'{ )'O}5ih}WNb`vC" (}rˠ'Z3g#AiG/ǰK4\W<ƃ85@х" lO#9:Þ6_l>{&\, '\-={giy@オDsf;%gZu8 p|(5ZWW;ux"Z b\onqÒ,gW~@so:'V,`YUKrYe 21$=r+0u^K%T\pWtKf[W68&ˍ##KI8̫ Mn[Y +' }90Ѽ8Ʋd-aYZi`.N Ϛښ>hfN hm bbs8l2RR5!&`i w!˸SFH\!16VZfCuR~[TGb:_âCkRUpPRG I^[Gxy=C':WHn<~K(Y'F+|ޭcأ;d+稴Eoht# yU59u@R#ε4yj+Q>L߆wdcFl͖E[zy*ej(X:T9nI7x``Ï~v&ጌe J_<9ZBZb ~؁!PLMexf1֊dY׻ [Qx!ڼ&zMltMjyIcEt(ܞo2T4&e!nӝr7v.O:b7dJL7OcP^C26W6*ȶ,ÖFUP.Ҁxqae")Ѳdt1'`gz5PJ(I24&jٗJU'][_X&?Tf"DQ q<;D*81h7~O77gZY]@[&O!de9{M ݤAa3 1'{88x۫+ܙ[w/AT KXB65xo\N/.P+ P.pkq${Αя8bG1S 5B)da,a{qh7݁XUNa0rCRvO|2:)Y3XblVPM\+`w˜Bן\VyRY Qd@L7z<}bb9=CN[K=oΝu, {%l>`(p_Z^ N `09hAO@: DE kPDJ, B|POn(. $0%u|TCr AL |:_=ghd鷼X5{hCx)Da|r R#081N$c80< >z`̧݃C88( Eagxӫ&ĸ2T84L fl{IDATz #;eVlxɒ>R/i9(v5p@#ĺp0~NdR`_6)I.ci69^%e5S2e /o<&iܡI؎M%B)bj_0`nXLX]^- S^jsJ[^ENTky1 O߬0K Vkk!+ۜMMAnwzLe4Aps$rU7:谫l)xKNZc'Me$ L.ջPLJTAVQ*FtWT6PZ*]T Zy0 ش3#Թ.e5j}"3w+T KP\t+HBrX\ mFu3Ժ٨Ks-AYWnף!0 J i7V^n`9`dz!xb?'&6^}k3Ciٮ<^ٕ9'lH<  x`s C =.I8J?|XڐbJ 8, O]+MY]%(Z DhH\aNL<{XY]JK;#Q+Iunj&$ ?f|G.q\&p ^\m O/&fӟ`̟1`dz5 ! qxh]Qt٘ ]\RG4@'bV{HjNm>ƭ_ʢF] TCdBog(Xux/omZo#F"=oǍVʄگźU8^M+ZӔk4ĭk\Q'TI_~׶xYGzVJW:~ Y+;-c95$8V8 a׻%&RXy *tjSגf{k.S{x}CP7}c>F4T߁}%b–bH16!k0TްԄOElu\Di2cqh(7NHg<N2W?%c: p_4u:cu $Sg+{_kN](VW_jg%*r8Hxa敷 4aTwZm.)@ v\KG#^eǶz|;y%[:"U+@7QM`pDH|( m.PE ԞZUr'L ?nڸA ]ȧ&k|**ã(Vk9iL蒧;bHN.v_PҠDuG}ΎJa (k$瞭tb5);r&#܈0trVˎPugԀӎ@$F6.w@yJBK} OK@QQ(hGSZxd<[*3B"~9$S*t)pAηhR'sكT8HkTj&#)B6#$om&`Npik/нJ?Xofmq:-n.W/^lBNr GGqG>ӗ[hovf/|[Z]grr0?0 fMvقhD`K8j:mI5.NwS Rkb*"3R#\I1 Cc#9rq0BUK0M5j4ap!nbh~H6UGPQ2q< xXdmD1Fi?̎_."3ehG!N*{o#&Cn[l2gזϫ:+G9C|<|Ko囩| E `P}vL#{o.,ܽ} qXO<1p\A73<^-o)Ax &g 46+:Y+/N K\e[4EE˗qXtˣolFJW1Q"uw0C+o_2(7(4MnV ,RxDVkIۦDLcwsvJ0]$?(ɸm5_ұ銢kѭ0U NU~NKv^dC$! DN#iڒb C:\F;]R!?z7n,lrդQ0ɮ䀌_vKБGrBemِa mFKna"pt\xh%ծ8Iifմ Y|mzPͨ"箫EcISiSC/ϩ!dJ%S]K1͐)OHuב@TXzCv;T0\ҡ[<Ʈ2]bjܶOa0ׄMj'^\E1#YD|"2z`&#fp'>|R >>x+M./|ǾEd@#lfc@I0"#Ľrc"J8 ThzKnpz(揎,kVU,dëٽ6uJ&KyD蕢J%͸M\؝c3S0d"GJ[:g 9(ƻĊcXrzuQd6J:&ޯIdI^4$/އp &2|5 ~f,N.\O;4 k9\62sta߻8MCC캢/4ȸL@}Be$-Q1?1 msqT iJȬ'iY8YX((%D'M>Ng/8ͺl_y>QK5zL u/ma4,- q"oX$FcǁU%eqx{jK6 tksCNj}ۂet4nQ9Us'@"a4K\׬pSG#D}^hBl GW/ mzJ˽@h·cD4^ac6Y@Ɓvo{W7RgLmہe/jV]v4#Ta]laC5Q}ֈ+ ]]GE'F֣A'w wn7s07Wpꛗ]OVE/wjoiVP[{#<JP]m@>ڪ06سV>tg{5w d̗dtǥ_̧ۮ݉$ျͼ 61k ȦP[ tRi8Eb&QYل1ʻ`qzCoI0]>=Q?Pe Ց_hGA֒WUiN7XCR袾!Sin{XNO*NyC4iHˡ~ Ē9M8-Kj;doW O¤!1e 0˞NL# {~n^[^^~8>8j6B@̊BU)gpCCPxp:ݽ̬% ˱Ufe"$8ۇ3"&xvw#u(#P=Vo2chśT^x~8@ͳN $F2Yihsx=K0CKbbN `>HQVCn00Ar1R@H+!%[uN M9y( 5^"I,- nA gg@.7&6[wxM@}6ͩ4ґvsW% 'L7 GmPEq)bܮ.bs+J8X8=>ٟ8sb8;;c.N*I7I mɊҶmӣť8O tOw1m{$]RxAd|!2.G- OymPIEZU0ds[9)\<Y\`fbI$E "x鎏̙CmmՐ)/a(?[\_x@H`IjEyTYtcٓS.<^7dj{۶zDPRK"-鵭c ro4X(؀`ܹR]:F\$.iN]1kLD_gYMbE7 R{(!H3ݏe@ | ac?LrE .N&YdJPم-+1y 3@^@[)*&]PLBR%(S/jʱEѠC׿f%M(V}6ʯ4pg Ę؅#N4Gf>?C©Ws/AOE#*4#-*;,~Cݷ+l D2覱&~({h7B¨,yH@kP@XL>:{!2Bpv2A`Vl+L! ljimEY QsXJQKC'- ccM7E˵1Dpʃ .sqP/)-)TtKBr"3*vI]5_A;I XITe%h YC-ЗVl 4PPa~@8T_(0 ƭRC8 Qa xz2i)>M  ;&V:kRǨUgDܫ;^6+Xfs#LɪT4vppv:q/x _(HoQv]aB-X+ е$j2>&^ahW>GTPQ'uT,"ApE]᫅婢v[6Fޠ<𦽎@ҕNPKggøaLH\d`GWeJ.s9}.bp#V;Z%-@^1NBn8[K="y'ݱu* )dg0e TgEoѣhx NQJO2Ϊ}1>) tLPИ=vȰ(NP5>k~!Z'$h9~wvŤ<On'#mLesS5!0( vpAEyK?a\)s! V! H\WkO71~ب>?B658:FZdTH{x&^G%cmh0eH\cIPF![GU>CnA}H*iAi04bht4J^#hϵTAWNܐ(kT`Qf=<(:V2&$+1OU6X# eo&x†$\£n- ѭ3ĥc5ҝ%΋koCB7ƁhE8Z:{*Us6W8]W~&ض&,X(4Z B zt -Ceh#١|YbBfN0)1Eb gBƧs{ tY&bo2;9 6 (NFS[,)% =O2XR`Nf&&/xЭv<;[\^v-:rSB5`HtvSurUB x|o++CH{Ze16=Lj. im4Ih y,8<fড{bqXVNED*EO49F`v8{6Tob"_;DBD|3*LGHjK+!CC$5K* -iYLQ$wk%#Θq)yA;/|fA^֚@`^ &O=?8p1A6#SwI|<x I baG4r4#(Ђi}pq1nbpK閽Й[v |ZFWr«ݽ]1mĨGePr PlqT&kl 8[rpvr,THC! {$99ڽ  ;7$ pR3o(OQE0COl eT BIvu)~b4+8\I]TkI3L]( 1"G66(j( 71w AKNS W}P!o>x;9n(lE]TiBhiG=Ԑ;n^3H͂VQ X Kie"@";!Օ;]Qڲ= uܳ] E|ve}iM١vzm{V[DTJPQd"AlqRfGehd7VXno.eQBWjkȭӄ.|mGD^7Fj9()WѼ[ T!vD%2?+aDwa64mWC"AULcCZUL۰ 3yǴ,^tr^GTC Gn*Ĵm u]"\P*NY!w}T-gFٰlїH(`>>jִ%Q L ^M<"PI_,Foڰ_r̐MhA,%`,Wx6/8gyL&pwlxl:|BҡՊY@mA6S 8%P6OΨ&;Fr<ܾ}{vnlTȺS^8~mB! 6PȅFB]DM b}jo #9EeR @oq1LBd)҅$W@ses]Nb_f̶vCQmBKQì3.`?x aȁ'{uc{`f qc}48h6) ɂ0sanif05U"XC'q" p Lɳg/7/.˧<3X 7!@ ] &+E4 LU(lfz7>W@l!,^\?s HUƆ˃_m%#X}G2P4xā0ɖD!9xJpAE>Gt*LxdAI呬ɉe 'Jh;x&ZC6Y肃h&: RҢ2rVҍ@QT쒗,ROR85viz]z9bKu(ŭLg5偨Nol#`NJ4@8)W͋MVMhx5j8(_7+Mc1iY!f@P1'ncZ/a@Yfcvm;W¶P;wWfۍ[AM`'TEr}Tج BA_Ue!VGn1pOЌRYŰ"QbSǵ!@ -WSc\ ] I5jdX1tZgNu~̹_JGKE7VFkH׍j^TZ{󮽧;FPD|(=r=m*5U*A&iuȤȔmatmK]̇m C턡hI^Pwt&2"9ԓ\u7It;)1<^XKlW\\)M  rgpyy8|c<ΒBzFFu R9Pewqq&ܥr/Y= MTv#vJAZxT8\=~%Hl.HmהN H5Ð*[ BzNYoKʴPPuv tN{A"7mï//zzcoC6աkiaK'_nʱ0FT.Z/fg6ֱ~|upz:1~k}mm :Å[ V {]7$o>cB %B>&P;B9J`QTmд"8݌ HwN8XDЃ i0n")YH&PD {8FA:sVOh SxbƓb Z^o&A2cS{H=>38L#aϏIt7PlkX\!~LAsB7 {2@Tq*6(3&Y`4@ZLQ* Lݽoۧg na+Ktw)Ou4$;Ōyaa!xyMw0"Q1UӐC"@]`zAej@XP_Zb: Jc0kݶ+Nښ^dGН#cێ[3iެ&D8_nt3ᚺ*[WqW •Ftھ%kf8`] &+ 7zTgA)Y̑T'rv=2%F`)f(3cQ}62$"4}T4Qj+@a M󃃅Yv5;$eN›[}{uu=XGQBU`a!+v$%4^/ ^НxvcIgiPrmBs Cԡ9ϴp }J@WJCN(oAi(M|õS SEc9K\a)6+龡f{[Xk*gشP"]{bwQG5A? _ܐ:CrHM\PH8OUayqj~֡z)O2 ipoko};ځ8,Y<[eElpsax !"qc4ƠF<ѰV*{1ZCȼ* ӽc :+ 6e" fh;s LK> .Ӄ#N(| NBTr4`(32:ih888i2 8`,wB](rbaJ(>lnD[;;>}^@!HƇϟpLA~}OF;pePPP#9i"}8r ( %d/g4`0mr~eS3C ⇱:8bN0VTDB1NGx(i۵]k q5 S03?Z,2jHNz bi2Ӝ4^fsa%BK kw읲؂4<_}t3/M]]Tvs#7~KD`,U~+j:5PƘ3Y@{uv XL\~t?*K@r.V5@i!k<,Z4v# NZCڙֵ^+8LE-B*Iԝ-IAh _`5 nHb$|R޿Р)0Ô o kEYC8{ɒGTR(4'Bgu׹ R%px).TMN*ga`^)hd7.̢;KmC ? =`"*AhS);7 u04R*00?^INXumљPm!7 $q>&цV)e,'~WM5 !oCo:pWdcq\ FC.+\CKW;?`Q4p.iCugR2_dƄe=Jx:4Cچ$?Y/ee`4f2!g4Q lP1m"hL**ՆR4 (: OLGp9HegU@L", aJX&=.WY=X|gˣ %ܻ܊ǙWp88:amcHʂvo|ttJ 9RbZ' %QޓF`x/]HĔ*C3MڣAIn}.7086L `L7!{߮4@3{Q5Ǩlvv  WuV>ƨ ЙOز{z2N7Xxŵ%&` „ 90_8pJ K=s*AÇ5 iڞll .[U\f#="t`d,Wʚv\bGsb\ D8H@A9KҾFkcSn_a>H4>v|| #!iM#qL]ArPDO`h89D}uOIWg N}=y˗ϟ  x,-/- dF9n>+wڜJb +J^OA#.B׏$fF |ET!7BPnn#<*t%AK%!s*)h,^o]#r'wݎ)~^$%[aw <|;kp#~ۭe ?¦ťa (9q#i;\BlGIUL l;fi;VQ=@_9u|ܤ3<1ƉѢIDԧ^b)hI5AV7ST֥f>yWP "닽wfHRK=bjۇ Rah2ƨū= i4qaC̩4 N$ן^3Ly.+ewXoLfS$Ze=bN = Ŏoj9^MP&o-S8bKVW}ji!8Z}7S*jE;J^j"X7b?))/v FjhrF SYa": ǵNNOF̀>ɕjI\xqHˏ+r՝> 4†X6 FpgV`CђDLυ2u$<_\顣(H!l yvS3WNG*η\^MmpؼEf)枤.J0q xa'}ӧ01<{bg{d{ۻ8 uķ4tG_wQL#/0I>jم]S:[ lck C%AeҺ95Z&l51T#^>ɎLCВTӰfd+{$R8x G:yg\L0 X?eЩ ^7̲PX\Sl^GL1~=vvpITj/` K1eNϞOO=u텹=z7fg`A+sݫhq`}¬9MhXwC_B"8B3vvb#+gF^7#p=b"OztJi2Fˌl 0tA$h6P)S?N0YN\M\2;y. 'g{3s Ta*!$dnpVp<8o|ϑDXY^y{qA;wYZf:9R`lP$5M! Qr`}Ỵ2S!flnyAS"AZ!m8u/y\ CAa^c-y56&ǸN,fyLlR#$8kjU 6 1YQDDԵս wHxzUjO^s}5*DcnMxb&`F 5"徐RhGuE(a1ɏh鶼UZI2n(m~kH6DvDrKf. [exZ#lHz a!MO]m^eZqGޮiWĶ lft`cPGP~[ƤW`T!J2d9"eBH|w_)N\0tE3My^+EQ,[AaՅy\7_]P:u =#&bXDbL2,pڀ$:^(4x:|!؁svuJ>v5J̚^(!trw&)/XQY,$R 7I~ZijDK\d4dPul';ji01044{[ 7kL}؉)~ h!ϡ<݃rr|z697ʉQlln#!vȱ IA"$BgFOSm  jq05TH7doTr4|F56wBs_ й2`[GzJf$OSl_AR:ڒ桘/<)JkHL_*R6v`_9uK^NL!dWa97SmDt 险8 )rm(E "hem{8mbm|'^[XA$CAA,BE9(i m <J{ Jn EDB(n4 KZY8^&ŚdV85&A7mepbtp EJG ;B9/xE[J_$B]s9;7aVt98889OLoVH[h3Ja"͗[-, ?xp{7N*]%p aiee֭[]AF>nh\bۑl$20!l ;-K:ssrr~.<|.%-ATfC]EYm@xe\Ҙ}^Kd;6J몣z~XaJݼIlU#S%~ yo9"cTENlbyyr?u쩉 ) OMа{|16=-B}g%hpPӮxdnp|xdA,hz-Ӈ48ڿ:\'_`OC[>:;b?ɡc^b*ҳƻI&L6,C`p* VBweZBZX[ΎṄ K_Q3^0EN[0WA27*@kI82Ȱ.IO p@N'4S0Ё2B=6;ED'\ X*Pf1 (>%o& ӱT\:VC\R6661>z[pi=ILjYɹ 1>)3 Uh:4>`̸E4 |0;KJB9 `>g+'Qko]=~~~X2*E.aRP-` pTԧ|;]RHGu"K+Sj[u *H!o*taB1Y쫰6qߕJ?,"ͧ52(݆5X~ld!vJ W $V~ T,ҩFϑsU,ÞkPۥ*)tL5 H/&.z8yI_~wSkveN7m|kj 㻡n͓!dn\h_}m<Xܱ6C[JT : D}V w+9ǣΜ.s1Է\Ɔ+q!4ƛ =) hnz%V+C`CDASj]cEB 09eK&҈Im>h>! \ 5BP?8f Mz[܏\0(s#E<$"3++&0:6`?*1"TDp[|2PMAFlJ6z-"NZ^.c6R k^)/""x 9Nƀ3kj YpX(5¼D5$ehJh5i ܴ*l g<_?}Ϟomolmc{ M |݂(h`s@l+?1Bo{"pb%&]N|S,sKYus.B6̼7#!{$0 _ۂ1H~Q4Θ|cVO0d}(V*RC`/cCcVUV}[1xk:@4]٘P vK "t^s)*I&LN-30H$M ۩U\x:lj;F =V*<>xʢjzj5QH%~ef涀zP*b/'\ 4H#saUʸ[XpN&xxƥԀ {D%HYTc0pM\0qdyWz|fi, ɽ&E!>=bˈs?|P.kEVi `O[. ߜunb 01u%kwuw+hL*2BaɊWb9- qbW`䟿,dTuob<|_"Ʉp2/+v}_Oap-w*ز+B؀1.̌]R*| $# 3G{lA%b%[vmOg''wW8[}sm^> xN/Q6eUp8WnP' 0"12?.z^b s۱z `<[ Wn; 2..!H2Mdj@Pf\Iulb0j)5pG_-`BUhPV w/{D#]fZejieͧ%͓1/NNNϖVX89"~~G뛛H=(֗a"n.x`D2P K`1Y?c=(֞ Dm<za9{j1~J!c@̗"v+PȖ`;6Zt0ACxVpH,h-B$⑱ u2Փt0=#B~f,Y1ˈjZ0vעyxk J"QhoUl݌+sIױml1vgCj= :U7<rVM:@G76Sbo?|kC{KJx֗O]Q-) tUJCj}QDK@jt ,dWR-DE]W[ 2dZu5&oVD&^#QWcJw p\P#RRc0eh|@[['"v8 (ĵ!Zr'zL0OuQ'ѻ4 G>F4(aU\ZuѢ>:Vv ';;#ֺx:X0PB#йj Xh4Lq$60\?1s97T _HQ7߰YU!NG17zK 76Qr$eǺ|DFÑ+q(T])>},V7L8L%h@YlHl7e >󑨖LzƘ6fSb>{6g Pֺ:n=#8;sD<!ܰ`h"{{8d3 I 0w^Xwȼ{Cߝ|w=u k=6cSp_A,1{!1k},lh;-cJ.CYhjǓ$<-{lL<Z*PkP!:^t ԅf2$2YM̸??Ue,sRl-ADZ~n3g7߿;9W2I}ʲ9b0] %H%PdF=y[;Qc`2~v :<8LV7cϯp%mQH}0l-+ ؼ<ų40$fN-ɍ􌍧c}Q qG״J[Krn&m:@Z=y&!4Ư%욈 Im8j-1VH vdXfdk-fФ3kP7޵;v;Lss5\ ԓ'v*96m7cm]_ y>x u2 $6 !h>13aN.,~1uĮu2', v q m>Z0 ]|KіiTo:zNKִ !# 2 ޵#*#s[SXoԺfqi Evpʇm10qdHje~YZ^Fp4ˀ #BҮֈ/!{ Ԅ'#+-qBnnNk#l{Rio߀Um2.|cp`f1.\hEg+WF \w85nl>^NJ]EPis%}z$#sEo bѕ2))Qܦ$9 M6IqgxzbA.Ġ{kb~yWg}Y{W5u [>VhƝ@ES \4huTķ$lj$hnIPܢ&1j kzM8KUBd]zl)Jp[;w`#Bx8*@.ԍj磰Մ>MH& ti(DÛA6Ykww}r]_˕xRLFYCnPI®"NbZ5y,]@W <͔VճIaS g:m 4qIQ,P%:*H#ٟioɘ;1%zoUt K ŜS&cЇ,(c5@BumYj͉A4.*ӳf,`P=h8HƳK$B cVђ(U#6?y&5 f~c!0?(ګ>_ 4R gҺtvjV$>d*a%7#uY"N.n[s4l`9LMzD=tquk\_1V;ezа[!lnlҼ -u%jmߤ+Y ݁e+bvyg97Xb6X"vZlJ885`Kg;\-|A0`_s ˆMEt=p+>OA>7+DŠ mӕ ryI\D &s_pTT|ټfIwH=`l[L툖qOO dJ?vpxDXw?g^ү|DENbԡ@\g6Fs 4=YuG}OL8oLBGg'轲&+6lU L%ٛ zi%lZ&g͂|+xh}Byآ/@|r:Oh,uW5jن"C UT3ǦLJtN 7آ6Éմ&snXiJFUAI`coI Oo?# Ӻ IuVaA_u(7 'urZ]콕6[Y{:sv#=Cmrv8t)`p{@kv9"^<{E.uBɢREV {3 s"w.-rB+{捝Fhf{-ؔ&ubemE.l) mp4l;7}3o˩p^>X;o;uaVslƤ+Q$wR=K"nH Uk;ֹYHeR[ g%dާ$jɨrR OFtǂ` /C601 d]EƣK"i@p4ɟ᏿'+.$v!mt >4XBZȯ̲)q92hkB.szC mZ(eƂnV $kXP`//ox  2&''X]> [inV{ss{|c.ano (|<Ċ٬5h4WV]%;C ;mL`P::+K><^at4Pl:Ʀ.Mܼ,떂&p6!,gGˀ&!4 hJŚ Cpz Lڨ2hڐôl\½/dW<]ԣtZBlD"{qpi`hiý 1cNLD1Aoepn»W;~{$q ,A ީ`UỌ7,&";6BO+ A]` Z '’;#6ogw8go> `"Oig+fGqB&4Uei)-lXNDv78&oxݶ`g2W\[jpa\ m0%O~ LZDKL9&uRvLn,wzv4pIafQ%xf~&6~*  G~)0`gv?>ٰ`y p{ -J(egΉ8nnCRd"/~Ȥ2}R]g4Lf*a",ca2Vɘ2Npt6em ,љDMj4m^ꗿ܏h ;~k7MQ1L w)9R[gLe0!ެ&p,JRiG_"[Џ1SwMNI85t{͋/98 b|Rg9y0GJ +z#֑y3WǾNogJjv2E Tʅd0jԌb'v-g5FW] Gf6:VD?Zdyg9/85 ggR*A""A䉌:GG4!{ k I+]xB | Y=R0u ߉k)wC'B T8>։K޹AI h*׎!$D4@Ъ™%=_/Ǖ 6rR}g7ig)9'u_sZJ]֌p/qUkkj OD5{nJpexO-)안)9NW R ֘ȟgtȼ9'zo@|.'5]L&%ǵaogJ(oC*8۷5Ql3l۷X=m5r,BЬ+lұHmn֊*l!-=m·L 85,ӺL 4OOHp} !vw,>vô)W7 ֕a[a=C ݰ w[IP.am*ÆvHu1Geo}E%0G-hڴ\:!H|f3a߼#X0 4ɇΘ;[8"\+ B̬Fw"_?ٻ_w ث 2eةҳp/J;G ]FfDOGϳ`'Qr lqtGPnn߾}~yP %Pq ;qQ ̃`ܩ-8$ݐWĄɷ e'de8z;&$q1'2˼>t&y'fhk8-n d̛fb ai;g|jpݻјLˎAG G F+ylKqSCF Bm[f]8-Z m+bx04[87e!ag[z_."pJ"ڏDo@!P>\krH%3hcd?BS2m>TA/!YQΊ\]R\*wӴ!A`(p\zG|y7L9|["Kfn߇{/BVY b42׫nЧx;w5ˈt~Jc[hl $9sT_1$rIݍ6GMimjxT>%Jb::}u$+cztR7ɋoHݚK0)2`\dY`'X6IsB ~mڗ$8w0*x}$YO \uK%9 TV5YM$vҿ_Ԩ-:}(rDGDWdI)'Yՠ8q zqNU/YS1m2"zd`uk_suص!~EKSBy+2ngBsKল>89? I`cݶ։@ses g3xZ=}[0#ZS^kJ& Z獖#;$V|5؂ O޼ÒlZD|[[7[ mfvqvK[Hl #seY`1y>4gY𳽍[sg!H@L07vv-¢tͶd ٢ GЄUP:&|\}Ŝ`qt@<cr! 6f> <;M7ۉ4b^ {{+`{l2/CSsx)t-Wz3.k|G>?oE$!iae2~n8F(k`|O\`F"qآDY;ݨ0a-3F6`lcRw4P0QjF6 G ߈D@-% ~D}p_d(vw>r$L,OL"x^%D7l#QvXw`tJ dCxbC&3ЪĦޝN,ЬEw]Ĕ.?ìs߷8-Dr}b9̛"ԩPN|(&0eTeɘr_M}lL fX؁FSXYnS풍)\4r|p[iHex68؅9iI'D|?_-TX̡\IlMnq&(!9\dQ"#$  |s>L"ΙI(w+0{V\ny9I=E @'Bਫ਼ZIn JYkى_PYM0t( ,OѿyDm2Ut~•&*eaD8jgj ]J"}ȟN=*e#8={w^pW%{b!`(\vOupU;a&tH,.#dS7@pvT#G$PȮ vA39 &s&xT 7zQ۱c? 1Oz0EMقvW@R(:VspWuHދIg8'9̈`uUJ j~mu)MCh(|jP2S$?$я~/cVŸkGBAD3B ]2egOMpG} f$&5iYZ |s}''' 6 ϷpC&|__hge֪!˛ %M+X׍N=KZ+ERL^崼ރվձ핚vWZOL/9!nD#v,k'GӂPOӝ  j.1@kc {㰟T_ClV}YRGE(8JhΆ4-h_6pi`8B޺~Q(_Ĉ¯Nxħ+u:xmCł\i=}4Nɯ{^ݑ^pe-M>Ek-i(=/ ,ݺ{aKJ&q44uA fjE*(aJ4|8"yK$}8*?-ƥ<_zT,Ttt>a] WrDYHP= ⽱kD^WJKիP#L U")=k'*{sm,~}i(c_>%! .s}/z*~([{+3;꓂y<&dND#qDɆb1q4b3(4I&{}WۛХ'J; [fB*DלNuDd pyFވ=`>'$y#Q^< 3 &g0?a ԆŏNNQ!R%>l^qi@HE/=~_;'"JIofJ{iC)悉ٖ){/~.kP ^wW氼#Ö:7ƏwHgAayR-"l&+a}FFuiްsi |LTvSh;!|/;D0m*@9~ǟ;/ ͘}W=/R` ,Lj}X^[EmB3I*KgK%Yg1^EVs4XH|@ bnco|oߞ|u7/^~|i N\mkcQ1= Q]*+#WQˇ'Y nj. { ;8nkaw} (%/0Y3 [xˎ pˌKJ᩹{Dۓۇ%_zi) + QTc OU=|wu8zB&'(8:"s ;* @HW}szrp} p2AfdoGL*=1Lx?K6^w(g3~/6Ӿw|5a[Џ3w|΅I?z|*Wj?1G,BDqp4̯`E\tU*m EonMqPkbT1+ ?Х_|ngMwk4$inkV}ES'I9l(DwZջf[P,tg"+b}O߂ӘYQ"tz.͆#y:g[Ǣ̇ Ab!B^Z{agyhrHt,?X) V ڶ? (hb}xt~gkw^cwGBmf4{B8\D!a#/<>9l{ 8ɒv,2lxz7X;,gqe!KX6Gw Vkb; 2%2_U|ku:̌2as3XΉ #pat.µaĒ5Z~ic/&훷?:?~@ħ;C1&܀;BeZz $)ktbaKQ A ȁ`$ klgPώS(S4 m!VaF'/|ɧb8.iSPJpGsv~{fG] M& ;Ysޖ-\]\.٩ 3Wv&5SE~z~u{姟{2 yRԶtX3r<#''_3ij|'DkR3-&{O/޾ Bz@ە=q'ε;VJkr0P[ t22:XQqk-_`ԉ9Ҩ^P. b {\P2+mlR62&O|`Df@PHXYDWazKL(3Ϯ@!FD a+MJZ]яU jih̭ #ziZZeZh;=X[''h:(⢋ D}Jzs bF /Ӝh9c4#+*MR&kݮM0+ZlE5s9D ң5XuD[㶴ջ'ohrZYF?MT{RGƂ]7 J|4uȫ:eծXybbP.s<`W&B~Gpų9nKg’0o>CCR7}PU %쒗/_bi{$F@ Fs\4;K͌0~iz磴Ы'bf(Tx54FyvVUG=aEk {-']z93F52u? ԇ.³GMJs/"Y.n nR?1t'n UvoT-{x< * IS|'NqWY`ĵfJ",'$QpC_sIXpҊ.0:$ ]Fbg 8XHٹpVDgxqhľ&51^>CS9pQM6*#FdsT`U\EL%)}=4k#c¦d Fz$RN3dCEHj(FpjHت60!)$gh [EfшK/=}kDل< ZSQ(z\43ߒgqzWCRc%ӈlbx.{:7`a 98nܫMa'4_jR!E72L>;*QHh: 3`z<@";'2H86իwwx̽3v_|`}gϞq_s ,7k~Cr 0hµjTª2cjk+-ؾ /#W1{zwwKhs}x C6Nk9 `^?.!־8D625<h@@TtGǐΏ~lE3r(ikX#S>R `_]!Fȥu3U}6$`*TQLFSWnacg ؋dpIFГ*>,+}$/= RFl1౅w?}wk/>d ,GMXDXr$]&dH5 (N/ab) taW;YVmȋzP>4*u +n4Z|VM@Qv\e(wdﹴE5sYhwY7h\h= oEk@)ZXu|񈴎ɼlVKOo䴧cTm1qDMVIVm2w8J'5f 7=( w4L}PP+:hnGH`=囨(ѡv Ho%EoErjXr196ߘ% Xdhǥɫ!I^ N?2L\*<QhX]HtŠعX״3 1h쵞>7T'"f[faZ]Wwhh,+ No筚Ia=ɮjxax p)WjRZY[w<7fu3ӑZ#7^M|oh/?d@l1Wx>3) <1 T'_2^|7?,hi` ~N,,=fW =3ܜ=KЗX6m!#Nz+ ̕g;/^r.!A=Hx^^^ѐ:#-恻&,=NDFM8;Ҝ [9roE.rσ!-0aaZ؞ᕰ0/w t(YJ fTF[Ԉ8Rr,q@얔ʯlTpB}RѶo’yHtp}yww/|u7 ݰ):AGNo9`xBZĻ V2Rb(Q̖3D1sF < zRB-jXk|oسH`3"i "Ϸx([g샔ϻuhsTV25j)/'FGp Kꇮs ⬳5l:42̊+@q6.K,}*gSa^-kP #,"Ʊ/^^{Q&ܜ^ceV֗חiͽ=d%ΘHŃ "MHuD;mIy=+}zpuo]^^a/=0Y<\\-!TR̬ud;Gώ>w6֑`ouowegwe{ge{+[vW 9;<<8H'J|Be utT݇Xo71Nmi; n(ƕGB΅Xۊ^[NxM344̑u3EˤZ%]aE[ƾuVXtwz\FoThk JNG4 K<2Вd6@ؔ숔NJ{Q@N H5G[C?2/T;lnf"|uŠ7_ 0> s5S>Z$ %Xh 7?6fJ{toTQeq8j=,'OGa@ T= L¡nPnJOOI!Q2Q%a]Gۛbtuvvu~\i99 }R\4ALOCF-5۷o};C+ '>sSY456VwgV|1N=Z)'B\+-e34|7ty_TB *63'>]T@m%2[]v,b^ S"l2k`˙X:cGd`|^zMQ?k%$dx]x NAc|qGAAU?G߶h6kr|N,mw/ma@蚣@cW؅vU5eE\4| Su4LZBxbnu'y7;O*oւQJҬ%q%^4h*)vhͺGrrI\ _ZafU*'A֠(iY?Fo$Hm5pns@7dq3˅ͤܭ?.m]#mlڒ|y}(Y>'OD`BTg),>z .΅Rl>1irWDΩ7ȋ;)B.4טl s S&퀃_?__]#.-&}¬#2dQ 2 saGcŐQ-f'xe3q ('SZC YÕAT$t C˞'6z= ƛR@uww 0K_^XF @n_3],k䆀Ca."H'~?` -Bn'`ʉprrryq@D!*zEO|uP }q" iOUTHELŮ)Tۼ.aFm*AeIܑKFȧ%fIMHUU#\m\Qn^UQ\E )faY|M4n)T%jݳ䜖YwwA%hEFWCjlĐD}U&7CTq g@lo13HLD4|P#΋6x5]E;ć/L$GL"ܢ 2xA8PK}iۺ K7#in2|< 2Oo'J9Ȇr)&m`/bz=_;uD;PhY[Q'VuZ%ÊZZF=.[Z+-`yXӭ/=!A~3g ӿ8<@ <=0m۽݈ M!an.o>|sZ{_"!N@> ;7wcF&z\a1=$0xn'< o\M.YPv6E-p"׾ ?_Ŧkeȸӄ$ Ɬ{ :#֘ y`_X/$;-bp#MGN 1RO\j ?ˑy;4͠}鬄dJAR(3Tq :?9D?CXRM7g@rSD.<5CMخKVY[y^=zslynb:!{&!-G:P=~ȴEY J;|!> 8qqNVƂ^< 2QxHsi)#  D(_\) h3Q+LG[و,րs Xr։g E:$:#Iʘ 6 y"/K4}%d>t7(مH`@^ ꔁ]j~HeX,NԪ!yQ蕷Fr[˴ч?}DHv "k Q@_ јoFM80F@v%)^Sis#o~yzdR٥=\j?! RGٜ@Ћ8 *lr6Dc5Y>Q[Ma*ZA'EJbPաr$ƬԃugZwqⱲm,bhs 3^[nt޶<043x<'0В6 >IgV+5p6A`ac{kc(WoK0|bXcN7NG9hM${NB?#t(vKHV1\F.#Ar@,hYȡAU/T_rZГ,ߨQ81[vWB(3Nt|Fw;"}ݣc oin1WIAK77.ؓ%i{Ա-GM2P)끣;dx5jF`>6}W oWx"IgOa)h2,2.no)ke Nr 2&ŤtXD|gBTaRnK&}'|tTxss}s~p>sGH=ob}Y#7b6R'#'`(W>޲4zãݴ3)*/qlX>b&_!"K gZcmYp0R- !wI}Jabi'2(;GF4}m.E򼹹8=˯onvpNX;Wq|NVrі5B3{ y-eiBB؄aSjtf{h֒" X!-@H0+AE&CFa0pîej>?[WvI.D UD$ <_3 7ܨ67JPXIu;o /s.ВIҋƌI+UihWUqʒZ&rw$5BrA{aR6+ ?gnqF r"զhO`$  cG|D0-KLPeŀ߶eXº/nnחvwz OU-·_ي2Ⱥruaw-`;hyV2?" 0Ύ039yʽ.+D:GRO G_WyYj6Һ}`,.dm oG} tut3WKJ!B6H -$|qor[$V"")tG-EWݕv i?2G p40{\A;]־eb,ZXض ]?B&k}nf*a"^s܎u#F*<2\Q}tpE! Og [~>i+T:卨lsS%l=AآiR9νv G2X2J] 7DzKtRsGVߜGLzU?Wmq9Ju}3D2 ]~yH8I@Z ,]r.nt/WQNhq+D u*K5j2'ǂ&SEE`UU Qb i?p8; Ů#P]l% x(ಜ, ;s }oֿ&Lqn b^Q7Eeu7 Aejk&/R֪qUBEMTyJ=ڊatFE4ʾ)> }*/>9BlY;J5ԮjX Ahԝ(~xׇA)N&+qDТXUjV 3wTi6쇊'NY/ߋpUH4" RHh+>u 仺ߓQfaQom@.W>vyO0Z9DX@,KLAO޼;>>F¸#MVOSdvٻ1O;;[gAD-=:̧,x] tT/d65ZV)$BA|g-Yf SOYzmflڳj4~ψ_o~?/˟wvw?g8"s>)xv^`~<-"\pSg`:mn'qJV)0ؿ)υ Xj6Kp6gQ<䉧/SWJ]싴ϕi PW'S(:nA%gcBNig?X=Y\AJu^sZ򒹂 ~x  f^ϙZep\\nJJSȝZd*^zrke昊y{ԧG+ H?rB]& =눡2}J|5lNGwl)FK=g_h7Ptӵ`&K&YL2qoh@)xej= XFЬ3FPԥQCP_W [oc^JOM[8L XBf*çQDUY7>LJ-QiSa%&B~lh--@`H֨3E:;٠oI^tKD_}B6/Vsn5\ Ԇqvvnhv.(~vkR!z;1f(Av vγOd~q7|[S0s !o5RJ4OW'oT/@DfMp]Ѹ&0]mn7d! 2N]D4Z a` fcm+PƦAV퓞mFmj ^]_bqY3VAXw3|o޿>=]{xۃb y`[sE@'[VfY@K;kX(GkHHs[ ۀ.w|XuK{ hP#,!)?Eߎ$MGy[x^05| N%5@zrR>OfZb!i/qSvZb |vv||糏^\}р7pn6PiŚRYm! e_t%e}RJci|i(12PߜsKkꩬ ƭ"kMs6o_MΛKEZX]{٨)0 YafJMnS0'4m+>:L+tpndӘq5B4NM%#:„Ց @l0k]4v2tr6:iJ'fkJ&n(b5u⡹G6ށ6)'`>9(\kξPdl5< $T5 5wX/ a^D O߄$4Dx>]0)U^I0\!Lp8r6Pf` uT4LDUmjz*"v!.6or "M4_=}@`ڣqDVR|w&b&o|*NK5t Bk@*X 0sg)lJo9iɎUbQh>(Q=-<06iޫ;4\R3޴ϲ7&JIGm3_'1Uob͒aH8l=Pk]|m.n+ܣ\H}ޅ^t] `[" lj@,:,7l>?;;E16m?nbwo X-9 Y"Ϩ>h< ;[[u'ċݿA@mO? u4P;n )ێM: ;1ewK5uן03*Ăt3f-]{PG$8k9E- lCx=ߝ6Vww6wַt$# 8bpG.KhkGà _u1|`~7e@c#_GL\xLhq q" zr Z?( ؇b6!Cyǻ+vG0YZG:beV 망Soten=H:K Lja o ^ĩ18/?I^]&WFwq(+9ruGp &fgNx}[m\:t߆x |Z .5Te6OAn2<ВDNv|̹h5s%pnSFQb+*5+WXTXԽPT>X{m8Z Nj#PϊͼjLKa.7+Njdڭ_BQ]3 Hm=/\|4)wyC`T&>6.J-Rlh'rgEELoJ;g& S sh+7=K`$&([išM ][sx`rk A@&WsY|:Vio1HY7XֱRH`_p0 c9W铂k7Aڏy*^xb;j <1G."om4gqEH*-mbСA]GӘ mhLS#_c;טA-7f @b42~ g[=AcϏO-J:7} 5=+;Z5LrN`#]f3zQ+DwPVؘ6Wiǁ"T{X`z(bpN}8z9 ຟ KCު*[~ 6!~i ?WWXѠ:ȍ߀U 9 [T5*T)9? *% ?Q5SY I44V072['7VWN"Ib97F4t|j]ψJBrWb)XWoWa_A s CƵB~kXq`.~ā|1}C\FQ㪩Y poЕzxӇѓ)@[ՓY75I-{^Ye˨ֻ8, ďsj)8 dpl\1|6+nE9&O#4~hҀKX2]x)99!%Ykqƒ*~24 ڎg# ˸[:1--c{*4 PrU?2,nlnlbqa"n ecyigu |~x)67 ADvo6iB{ !tc[vm v &## 6l0%SDr;92q3,pP:L|  k*8CHu4qs<7Rb8RL5u(!, $>i~}qr)n"&խ-ئKkLG`$[R; 9h$h>DT]*FHu :IJ[[)M#Ie 67w>20@! ,y:(P: ~`nYC )p(mG- rr,p)#R]+@;|흭d꓏|]y3vʬiCA<hڇ.mh/r"&fvBפA,wԃ¬?ޘP{S(f~ &Z98-7,* dUȓԚPbQ:9N|( )x?5mNȱ dN- )MR^PHXl&Q Gؾc*AN?k vONlk7VQ&Hկ{%XLbyЙR#vXM^bQezqC֛+!Ztƙ.g QI|RH^dR-/dR-ӾnqZqȐ؁(P7Kow#a=V&8}~@Hpu5ʲ9DX 83M:3G JUf=UNTg4E'(ZKB UharxF"XjwjmjmM8 CNRG&9 ,F{#M姵AR2zpR"*`̷XEjFc= ɆJn AVTnJm|=s'kP@Z¢mLwl`Fs6P#TnDk'A 0D@~¶gmMU욖yWO4Lvp::^fKyfO4㋼!-k-fdQMWNLSb^Zq6``,׫K*ɻ2 ;\6U?7e.>֞wG49?<@Ƈ-tIؕ tp)wd!)"{$~!6Ww,d xUq|];=;&bD63/@\HAf)#yA, r*SImJh97Կ{KvIqt|ZP$ =A<|G9?\]G_ǯ߾Ƕzo~{y} (GNu<{*8fst`[]CnpdN/JGXlnOy!yPP y4{^$E 4 3s0~/mMd OF%ljX)Kel@dY_]ڃO’E2='A gNXb9dR &7PYbvlӲe_yx\88<1Mj6Uo RbT}hsE7F-f}qKlZ<ehWB]WQmT,4T8׆pM3U\s)'IAN/"ZU%؞zF#0AƾZHUU.1zHq=$fQuUe*E .*) Pjbio`nsMg#4|W ڝ)uz6zNzSoX#SxC_@JpflNqK*I{U W9QK*$u7N;+W0E4]3I~NL@2_BC~@I1%/W4:jJ&xӉL(X4rN*fm6OD鴸=h 3C9m|%LB;,䤄JډAڱTc7='P-I ->'HشыFᭈ'xmJJL՗3J%?В/cc5{MTkD+;_jEly3D;CT3!hFo@|X D]୘I՚6ᔑ'l1x[bjv፜,NO,_8izŵƤOuA;^ [x}Otthqϐ 8y:/}B7ૄGc6 )7Vz!!3Um.J|rzS=I7XK!FMDr#<5Xd9b!JStܚd#7_t/E1 7 7}`Aډyl@|7".nS})"Rx8"JU2W]/C$D;4;/:pU ^[ (Tg$|Zzu֪Eh"Xe@˓<+-x.FB`e1S 05vH8͙% r 7#ܬpY046\o-o0v=mq2?; JP&A"̧r + bc} -v ,,B"IgiBY)f$dA ޱV Hт1^a, Un>ˏ|&*!5jjk(uBqZQI 34H\MXd7?m/oOoq%N4iNqTs3K or2n|HX[9d)h)nydueܤ {ٰm18kᶮة|+a`XGPÍ$1# ?x5)ᣜ.0! yߒ=Жޡcm D7z@ n1h(c6cTJ2.U/]竈p"qhɀ``I0 @ȝs9,;rv lvS Fd刚' oNc\&QHw8͡Q_<0%Sk#:v sYfb' EJ/9kb|G/A5ip[8r3ёw cH3,/`!TӃ6bnu!Kuvp;uF\l[?=ˈg;\ߚ%E8JaA2B^E0]6d$w4U:\*qhp|ə(V! K,D핂@1~J I9ݐ`!D,&b +TUw.>p9:Zw3dD80y-$]Oxtqw?6 sYe!Gׁmcc8OKa.O7@_R#UERj§#S Jq0]HtHkRd .j UWHmi?)>1E\Y\'*r}6+ZAn6W1@V/{ׂmG1 $9)yNX_p-@* .Ė 6Ö^F}q[Qq,ӫTs lt!y B(#%øqX5:R8/STs XbW!uAhs OØ)tc "5(*f'^ym Ɣ_1H]~?ɗ;O˿77~ן?uz~ۥ[3Yytˎ/f=z=Aйv19P!w`ȏС+6‹5Xh©Z0~m5hu.I!K8t\Ax2t]OuoY uECg(@=(lgZ֐Ʊ!SHlm|!N#A@G:n!2@=Ts}1;8uFT=Qa^BQFX *vr7^d4*S;S͠U,%]W]4HcԚC^u9HIwi#`INF=8G|@MNsN5V1)ﴗ]L[ղ|(0T˚#L498ANn 4BW! K751 co{V^5 '={ԅ|Z&?wßF U .zBWOP#L̂1\z[ujuΧ =&3|3F Bp7dbNFLKn䱤=Q3VtW!wkݍkMǹ#uv1Bsڋ>u2R,\J2:8@7)Zn$'} "5]Ce;;RgkS ).s6^;O x0KW:vؾ(}C߫YOGkL^'Ew-}Q54\M@J\cS[aavY4ʼM q3kEOyPnm)&Wvo.Ȑ.zvDs[l/<!fU7A2/ >'qD:vS ~DŽ=Q_\D WV ȶ{sKYv73A[pCx*KiUЍɿfe˥(? ȿ@cMAKb ۳'kw<Μ8o#*/hXC5>le64䴰&P4Iؿ>pCl-/o.-m<-o><oL;gT(RW!xˀ4%mX04)dRF'0jh q)߳kx_N^̅@=)V2%3bE-ެmQxQߕ? g9 朩!UEEk!]wkaeJ7Fr) >㣁PBJz#ċg>7 f)foZR̫fUqtݮΑFxjHO 6ZerH"" XZ!;ghZM^aE [ܰ _S/ѷ u]P5he6|B3 2 jYPQށ'9@Û> d}N'ޒ. Gz/M_AŢ#`t%P|uQ@hzF-+C_?+`-Z>{MhO $IrFU]|]˳ ѕw23_lQ"FgJy֓R[HPd]5mr/^\`Z[izTKWވY :S]KĂRFMkxG?-&&BYXz9x8l}}WR v=p"p7~;,?-u_/?="giuiV6WWvbeau~_wm_z92޲;+[ۏk+8Y9Vlʮ9k / ÂDb "rȵO*-LAXT5l,-?'.·FT%IKF,|&|FI{`dЄ,(iL d 5_)bx7\,A4JY:MlWHVk s8 ֹzg0X?$Yihɾ7ʑUTnrMOKD4@ǂ6C,fM  ,&W=wIDAT=Zf;WDvF6u÷TR x7 Qjw(1 DdpnDyȯwu5&bGС)xhEJ.zٚljFf__a1ha/{s챤S1h ;ʐ|ɡ)N4a[֢4zh`NIcOh˦ FOP:G"J C7P% (82O%LڀHrEͼuwp:c)c6-bڱ  N/NOow}jICf Έgks ?| 3:j;ÒEˉPJ 9kWf:;N{yw|ӻwOo__<|}vW+xj7WKbo.p-x}zwhrv}uws}w{8@~fWvgT8kp4}2vffD7@i)͋}Ui [@H~šn>wV*U0*ϣP$IU'inRn$Uip.ر4Aa͟VZ V`-&y-yNQLt4^Z~h-`2oW,1MEROxd>=Hq]jEWFĔdxe'6o!8AjC=u*?Y/K $> :G/Jq 4z2W9aRk .|;+6_ _JNН86`ɮŲbW%q^iLԄh'iQaR/#FC0 P큊`|)q1na{.RDzQ:Gz0/&{#!`N9|<Th 0&&rns"_׊x)x!%'.hWh暚hH*roe4Zg~Эf,ݢb8]*F,~XHgW-qpb/ސ4 = ՕZ]~۪|FGLZ̑];t˷2E)ʌ0PfDZtBz0<A!իk,U9_c֪Q]=ZQCcv,/C83XL@ݕ˫ׯ_}g| ߰_i4M8ӳKz%X+V?cxL4= GW6MX&$ Hivg"17p&"L 892Wpyly>mWPLܻ;{kSŇn?h#wXB ړbm lQñCAJgk#`ǭ*Kfή]oWw/x!n={vsIRicdg@chQdN.A;w.dvCvDȌjIniv?dV֩?U4M]e+˂Pftx'0'ٍ}p67vr=N/۞;rxwAL#q.ha#0g\Ep8y0@5Q/x&m*n8Z2R2x|X"D.l>;:8 YkχKW/B1ucΫz4@Pj ]qDb]87a18\JFvM=isHW6T0jP1ap"RQx7 .Z]e q#F׼hV:^ ݿ$V9 0']'Xuz -"7UK-V[ -zgm`\4L@!9 xrQQ<p/z`/%aj%sԋlg"88;{SwAA D|1D_scSMu¥64YYAGr˄CdIXJ`dϊo٫qgDSEB4Q8gqIIW]]gORX>-dI+2'07:8:YjhdÇƤ0oǚ x6P׼ V0FSU"Vk٬C7aހD;!Tsl㻮t4bѻ  ݀E+e~s>H[7 iڪ61 dlB=xQpb6͈pXve7FyG|9m4_>&u^͸PXӳl~!,5f7 Efn\"m]b躒Lh{@Oϯ/hxutmM?ܡK_]vyqyu~`;d`~acmzqvn ^|㡆0b42SL^H'1rKRYq,Ј~XVE QfQ  XSqtȈb:,Ma׃$ܤgp!pHHh neHR8mRvp p4VM`m_A<ӜjVl'բba`oOQ(x[g;I$ur] W.{s^Mp_1T1"=ZzfSlۯY6\!U7EB5uȮ7.H>HO.Y/mSo/ j'qv[[3h;pV7}[9N= Ch)3y竩M8]Khh Db B I`0m #0(AfyLh SȂ}U::MLW"DB둹.u-ފ%"vS”~ <zӋG45w$i(*=8@rf ڑC;:-ބ+T@(j&4bxY3kM}:;2FnJ# 'Dz'|Od4} Եڐ^{?Q~wZ*!:lvU}  ('lZ cz*,ifⲝ:C; R39r==kaQ۬bI VRvc4 Es&h#pHtuH8f=¹͍ܰ8_Zm[FSXϞpisiD92mZZp~|vys `vn̴ZheQ| Y^S:_oomaݵ]Dv3Bl*֡oonnoV-}ݚyD7p_;B( P|ehqXJmL) ?}t4>bܦcX# h_XnU3 CCi,毬!G<;P Xe Ffe*Dt>n [x%lZŖ~8$5Mގqpss :;>>|_|{k}!-/^n _ [kΫW_zqxtQW67W6mG~`gjD؟]2-o tVXoe/QX|,|BCFgS>欢QjpB 7jdJ <Fl& pn3Lzp~sury?:G_a uL~d+^0zn۽͵ex2{7ة33{KBE}ERŻ"C7]\XS{J5-:X&jUϽV@ ]mmVOl(T[/5Ox$X*jOgզ)q4B@P{aؑޙ}WgrTkx( vi@NI{wE\3jBRF, Qǯi!\w?L4 y!GPE< TڑAtwwN ׁS rg) >ij+{ Flz(7 ??6Gn+D+̶8yTmiY[_M;lQْ۬\ս_ jwҪ! Eea֭ 2l h^,@4"Rсͥ+9 F|(If7Moxb6Y 01u`u=bmM/5;d)1)u;{t;Ut/B+ŭ +ؽZX%O_pHl%#~9S'mh]/G;Zc sH(- ިذ@Ɖ,YfNڂ5n>ITEuA5#"l'أ3lA{ 1}mK01qNrnn8T""G# 'òMDm!ve) l:r'H0Jج(K e냹$l[)aTZ6-HC+Bi98xÔJ̾k]4; Oh90&@v߆B^~DT0%?Y&ͅʊltmPٺf0"^)VQ(hE:*( vΤaa;G̎t? .է͌>8&ۛ닫ɻ7/WVowW>:@ݽgώvwwcM3"M`Z.'_MvOx`4I,ٙ킬|{3涑}`PMرv~ Lgh|XCL+k ?yD}ziI-WAgmĒi)@p| @g[ۛ ;eN׌+?6gܴpѳcbbxGB~R~CeD1?ԥ1+Hbu~@ ƠJ26F6? :˻+; A/3vHT,Geǵ#A1rE$ y%,S6P4AfQ3%cPl[.{= T0jNpZQT9@PxLnށc&ϰ7s.ndŒ&K&1)xys"*UEJ\NK  }# Idi&aY_V8/3cMUw\=u_& .mZs:*7_RcBZݫuxZ+*/Y(`YdŦ鏳]HhS[ODuN*5=-5Ex0ET> rh/|Ӻ7N_w@Qw V\B%ZDk@dɀLTԚvkV[s(Ze6"ӳomvK;zY~Ĕ~+GsDLOM;]`+ELԄ,:mEy|ጒ|,1n-YUd޲_'o2 E4jGAr;䊴F7ikEl6} ;rK/o8zaf[חrao˿xf <Β#Xʥ%$Y5W:aõ,uZػ2Y`4mE jpCʆͭՍ r d1hL W fksunYPk޷  8hH;/ TZ3r+ё>{Ĭ }dnA-|vֱĶ =mhP"=`ۛF~,㉥5v\N..Oӕvo/~phw;خ@* fqֽ%T~qrzyy n@N ԅ1"F\~Q:JƃەXM$!}r ߣ~2rKGpTY(aAwIbh!*ALJM `s`mfkY)@J@tpx˭M$FuX%O8yyu@]Ufw2P){{}>R-)$=tB(VR1'zşGc<_z]dž7W[,Vh $",J\:"0i W>Hb:28b0'UW'ZV$̬у1hFQ|񯺧 I|D6{^COV* <#$TA7H/7\ =->X]V oumhyMTpޑ({--`"}F4_ ЪaYo 5r>P@zBu,h0wK76p"&CE̞&쨚t^W0T ST/Iǯ@?Jƭ,j8ip{ =.ٗױo9iˆ[[o{kww 5ii( 24Ab? <y*{w}˯O.Ol{ G9"G`E5b bkOOyWHF?uRyIݝÃ-^]ðypr#P<3L\N0Ų\oZ/>?quu}=|,ƾzX6??8D?]#(=Ⱥ&{5"B&ezˢ,FXs\յ݃8BOO/nT(1m0V8ŋO>t}sØPw) ۻ cU\Zv~3 =/O'?'CfRy_"u s!7nFl{zz?w?}mRۇ/>ܦjQMjRG$“ˣBGj9jcݑT ŬQWtNɘwLctODeV7ܤU]6M2%5jTkt:Z$<7 hDwXC~p.}CYެ^(%zQ:MqV;imvನRONϩSQ"|oB>Y;.,hś^QeT{kӞ I/L:qE|F9zNd&L  2ZVMdUkySa "w=LL-Nl kjψ;bi,b4r !GE TsA}V(A71ܸxh]7'T)ݘ!&Y@CP@@[;Q,lSRUDb &S M^ CQ.&ńwj5Hg:T&GE)DtN ǭKUZ* , !}.R6F- WΪ/fpڋUvQdz1;1S|FF4WQ -y{,S3ȓ,%*E!uqv>Z6v 7f˯z?C}ǟ~MLì|P=llK%»@:Y]'_>ˣ ], '4!adbډά6} k 3gwdy٧Şn `s .N~> m~t? -;{bks3{o` 6aY[F=e7_8= rT4Bhۮb=֮~vzzzr;V zo|y9$Q d0:؆ ֬yY4I'O4 v-Ƚ  7`c#o Y 2Mp@OG{\]"7{vC-/oo{ەO~G߿_<+k=xI8ȅkxw 9qlhMXP)S@qp+\ x<` N/{@.\@'_y@X>zrlnmX癠8 w$yn7j(s(+D\%᡻X߽9;=¡$8ہa ΄Oc-ө4?# 6y:|xtpg$[&*+!ƅz@/W|-Ǚ<>AWJFplq2ȟ??яn}{+HjQ88gs|kP]&)FLk6e R/M10H},apHYE_ J9&`660h]j2<*z\e$\ ^wS} PN}㉨.ǪCImU Pr|^Rc⼢d~e%.=sӠ/9٩'g]VM3q4R]WBk2f^!P& cGZy"" U1Υu`@@~/DsTFuhֹ#1өv_E4TDJC߾r,"q|IbWrpf#٤8O s{WMeۤP|D2{Y',>sJE+b%Ee( g@W 2j~zţ LLAJUˍ,tU^V"¿ΐ{IdRăPA o_&^SɇDڤAMUA Ut/55* yٖ..u#PX(#+BˤthF:!_ԕ z7!~yͥYKg65{6M`c_lS9בrv0;U[نkc9 t( T]sA3=w:.Ƈ6 !dd^]}c'!facc:.EO?yˍ-K La@IW>7[̷=mg6X ?l>1ZxfV~%vgCziԲXOgl.!c]ߴ{ع)>D+q b5R?ƥO77'gpzqv|ۯz^w_ͻ/߿yuvw~q `B*=v<G G`C6X lf8=@=n9M dvTUb-Ԁ8 7Fsx:ٲ䐖X{8, 4Xdo/(;sB/f 30ޢO+kM_*F1Y^YN$y"Y(oIoNNO7tGO]?Qi3*{S.o"C|V"5^z{{@e h]à"DrAT2u'+V1w(OOW;[Mj% oRvvco3*fSWQΈʓ[k?4:iY[C),r#A(K'`}=ۚx;y٤I[ b8`ޕ6]?skchgdS.<&m>j˴m| . :y]w$ gU@d?c5b FGC0s4{MW?N \ϐ(!sPu;7 <8fKuTJ7i$yQ8‚1:t_Ѐ~98=CH=m8P8/oS.c^I%d߇:>υrdV%i,ZbtS_FDtWmCzcj6Ԟ˃s QeqB/'RQp_%}x_ŻϋGE'C`/Y>Qmݴ2$j". x\|E}-4(9:-[$ {u&ʦE枭pwm9uAX]zU,:wnnNv/S [zM#JbCZsޫTr,9nTSڞ~ҙ3@y,{w`)ĕ< ^$ܗr%ˀ1J:uOYe͓SK;8DEs\gprsG!0ԱZ*: gf(;Gm_A%-\]&>Flxq\ }}C-mOl+`SD?=2=N˶{ omw6A[Ih!Yԁ$̖te\2`]2ާ"\b,W@߸C~c NP ) Bgq%=&@iDe=L60_ 4~~D=?յ/`/i0"t;8 gxĀ n0P 4J?^^g߾}_?ww~?^՗oኸ>@;H,Y2hxpBr TzuIl;,]M zsʛ)Rk\FJK>uhs֒24bb1+W+T|fmrQ{-ܤ9jB5$=*bUxmٱ|7u674- oV8(⤢}$ˮd>HФCsB_ *75nQvA\_ ߊ6]{aL?[T) vS3hbQ#;WFD?6GDv s D^ #K=+YrQ&QLNeOY\P5缧fk$NAV8v27տTOeUP,8ߔ!+koJ޿8T Ea,$ԣ&L0~Z;,\WQ0de٭9b1Q`U6$$NGgco4EdlM+*GҾrR_S:FG(O/U7riY(WT0<(9kM1s~B`]_lH\,ث隆-SlFn3od1u94.vͦe а9Q%_z}oBj,Oq\\v˳g;r'xp?cŊ-3 ]N.O|x{x <%F*J a;&9E?X fz`[a6g7rZgIp@*Zz @k%AdAߨŰ |`ƮDry~3QG~1b,y`NDf~:L ̠aR uXaG8,Y;FQ1/˜p}56} _;?|[@wW8 99~p<{D(\lm`g<%T R<.Q# {IP0ClC{ :tR_%0IF5wyYM 4S7H_9b5Oa•9=i~:,r_ZGp.#~:JHHnhG H'qt̒3"^/\<5ٌ\wq~lR\RPVܳiN"{(1QrOSaMwъvn җ{vu 7XX`vCvLml!H TZRylPwT2\$(z ǏF0^qt.ɩ hF_i GmȐs28DJ]U(cq”蠑3rVcǧ 5F(:-NAi.z#v;@9iGW7oo 椛s&]]meHDiM,JO&w,U̩OVzH$D}cϛ|3D.u”9{A7w V+AYaNJGE n+~ q^oeEeR_pnDCP"༏YqYA-ZF4$*;B;CS V J >-Cu 6Ģ)cB (\~-F*J7X%*S N{-O*Cwx}@5]W?`x^hRJ,UUI4:mI)1ʎn };Huu7%n# MH5S>Gґ$?nI>-0B%;=NPZ)Sݰ6_o9;JuX(|lsHF׏BC? ~p(!Q Gn/avu\sM7 d9ؔo#4 |rKr`}"My'-Q\MNiED#])%>Edu`rPW2ZPr9Cp8ݤXEȁna^\Anh<,b JP8V~'۸=\|5p\mL]Ӵakx|)B̪}J= t WtkހPN&9> ae*moV(HΆ-xX4k;@Tֱq,|@uIsP@Ƅ?1d ڋ=~Т nPz<7}K.Wq-4vn4ٵ4։LRe\j\^ԡX1? 5fxnViX ۄTF|U!t81\QyŇ#nD+z<ͲX.A5zSVY A2:_uVjzǁ# N+ Y*0Eh^N<`Uǽ9O>0^0׵%zG}AQsR='aY-|&f)+bt=}i+@+S⩨iFydI;@';;0[bWe͕P>|vmViD/ P(bQ !Ѱj%`6 620E EXfhUe93v䪵U۲lG1 CS 왁1 .PuŎخ_[Ǧ =0\}o#3" pf&6ÿxޅ=^5~|{87'9>=^B=eqZSnlx ڰ,. HeJiK&N  Bf_<Һԏ]$D–c$ʃ5[tΪǾߠaq~iQ_@VH5cUBP*5eMQ/µqsun"nyCPSV X;M 'S]w6rXN8a#q5Mquisˑ2j"$PAtKysRaOH:pPl0=6h4ۤXtEnXS|eyu}`yu6)kgW-v\73Qk}J$}v W%/E\ykG݋%@ kYjywB⺇,p!Glwy]gdJؠ r89H+^( ,/Hi N,wƶ}-ҾZ5@A}5Zjgb{"b#frqGUYrYsy zGHQg_F7S1,ئ elr؟fkHdlw9¾/. nX)9q*+t[<`뛲Aۻ7O/O1o?:@oPlGWٶVv{$*8zfƹ EqHp~|~DOHPgIh{#Gd3di ,-< idI"dQo=ofmT'1CNʑUx:%}DKsOaH-KF`] v0BludZC4 д[Y7R3lnWæ,0#iš`HͥUd|{~{{壡qWv6,'my<99`kXFn.߾ o,!Lc;b <'Cdn#1d,=hHO6s6 ;h,*¸Mb5u`"ĞPxNwI%2FSrpմK)R\-5nmlz_S>4sRM(Ʋ~P|cLL AFxռ'`V-fLI.vOV 3[ }#O>Ɛjo`u˛ Og  ,$򳄱kT.[{;t /Om&(](BzV| #  amj#D V7CBM6N9P.xSJ'0݋ @o'Ea q%}t^aICDXE%ٔ3KA"~P9;Aލُ؄t65~'g'Qyަ.]}^DsM̖GAUERBsA\>s`d -2섽/p(Ly^ߨxS& IBLsGؘU;J-o:t 1y)/r4m'. I;N`&m/bc"B.{$ip߇N88ǎݝ}l*2F;8=6Uw^}_ k +kw+K[뇇{{Y+&"r9 6Alm_ڵsV\2~3n ^=l^ d!!ΌIfeN e 0lS2x -C,6ꡑLNc9"!Oq`ow1gY؞0ߪE8qmӯMzXã01y2;. !'?_ᇿ{܃e0V:ѯ!m*\cvI '%vJ`w'ؘ.xˏ>ޅiuڟ=Y$XfVfVY^w}Awnl7I&Hh6F=Lz? L$=HFj5 H4Ht7h޾֭rE="nxY|"<| wl%p x+M m&8cY؋*| ׎nPBN,s&%%#(G!!0AvzL4%29ccm+.t"=>H  h¬jJ m0$+wD!n-39"MSS_Ex_:2XãCJ1|z0iڮXH E$4  hBRvbUtgF꿞 9.a;$:խ$Ca"b!W>:Rm ѣI9:%2]D)Nq¯Ɔ+J9Qp9$RVcHc3QU>7 v5%IG(bS&ZVIˑgG%SGfƜI&pP ktN@ϬM\N֝yנ$PJ^3.5suDJ^7VH h( fD%eDݒ Kyu1u1X! FR++R Vdvaؾؤ$num:HߊK߰xO`XaxA>՞u埒Ѡ'6Uhl seuK5jP!ˀݎ=_9J:+t.սRղ O6z59YUrls@N 6S!IgHtt'O<aV †]Hʑ1  VsNÍo9|.}E\i eNgkb!Wo^ڹ+mPJMY's#2eq=Rx?ǽo">8RF+\$'+ethE94j0ٶ\p4WB4 2xgspeF>ug..Rr18+[7^KGFw*H,p/i23~IlCvxA MNX˱3{1ZqkNDށ2 T<Λ >% x;1KT@d;(O a`@0ɡɫrU-bWCq>܍ܼqp,Ⳙ!*Ep2j1`N.в#\}(rxr(xG%zb1<{[;^, d_r/\վxJ 8AD ㈣pN_/ X]e!5_bE+H3KCMP4+_`r08q@b$JP0"ʯ%Z#t.HR6eEŽC=( |Z e#Oaaqe%2(n"T_&+-Y(v*ELRs$cULA4ocm}eeE^=;972._Bڨjj&ʯLL#@j00l'GRJGGzpr:=?(^DH{"Q4x__$ ^8:\'9#(D\ :AAL>^oL`+RA+w hyiam-ק/ݟoeou#"Qn,cf'-[V;tm*c|ONڽ!~3ň$l,[P_C$-ߕ0wΠf~ozNypt-O ?l(/1hL,t)M -X#5j.]Dmg l&Zu4PT*!> @QՎq.{ _q։J`f8=Z)8z,krK]AAغ05~?KHƸpC[ 2\G=n+89]u|{{4#O=]Jz7VU0M WoלT8SJo]du)pZ4P ?3dUQ-C$cl$5;v^fRF}P]fݎm!gsOt2gZWvQVzM5'6ܙ9-لmF[ }_ff>A9ۨ oJ5AlpE?ta ~IfI? n$g̘̓l!uo&tla(VqAـx J#``ǚ,u.G=\lC$$<pu7w!+ X/聙As^3kxPFΙBo,-DFDTAq2qx8 ~k-4+қU脭FOȔ$:W $SF?; =-94d|)rG05uu@󨕠 ؀X^fO儸>mm~`k`wm K -xRN۷oߺ} k/@\ %*fl0?+|;y gqPNĐAj q GJ8E:qSgX د,mGuiuY8ۆ]H#"JA{DλdphbZa_ HU#䀵NƧ&NGnJ;"R1GBmc+++kGQ tnҍJkC^ Nä.БHNB~7BA"e{B@K.{y LkB(!=BUQ 8QfE) ".!#`&@o'Nd>:8bx=<D賌Id3BsGO@UMf E֗.wAIs;FKifftr M X> %. ~/=Yf3"9+z+&Z'ӊ1֓91iXCutFmqW^- f !ךKN;zi pq'm ?]XM.R7> ?}袅+7 > w1DG7<b5xgFvR9K/S0E?&j MuGR@ _Z0@%ؼ}:fʌ,^ݾJa>? ԕ~ 99&6&oE؎3FUv1lhY>cxy,]0tg2-G^nw9G0gbgtѢ"1w $Zt+θ 63 uzȫ6N|]rh-T].:Wl*^„(Af1zGvB\WK@S!.KTըçcvP~˂S'XB-ʮbn<-Y88`g.!`1i3)fϦ{zk]rM餍i "j=ž1ѵ# N@ Yttǁ8sgkde]<fK!1o웠K5jK&PR}fI_ߜp  {p^~@C zq5xA kc?jh r@#D*E i'_8zyhIDF.]W )e&Y*D,UjvB  O_-}~#'g F#xn%cp>ǧ,AZ\123;X(~8`;jd x˯g] 'U3H"4Q| صt^7j*|@6AX;pR"TV#,dѼ@GxKp!8 `wY+U\!PX z~:# w2{9G11d@f>)`[nfD(<?rG"t6qyOBi (0>8>dA"K:|=;`8ȟ9Bt >`b8~f~ j09w "$)dߓֺȞK [ƨ&խ]lErmT u"45NflR7@ PGo՟'ȎgD](Ahgqc~4%BQ@'⣉O#EIZ$LPocD6 )xEϖh>GrOGNyS_rFʫ7a-bC{%BS|RH$kG&bI_E`-}C7r =O{T8z/oHG R+w`[¯~gu25El\'N<>shqj-2ۥ.+ `P2[ma%ѫ@v!ɩ(σ=B 7@Նpv TGa!ݠKH 0P 77]}p(2olz]Ğpv:0&4Pjؼ Y:5SKCE7X#FxriRPxب6Mp!lY¼yȶ]at莖'\uH#=m oj.Gr¢s>=rЧPpdF'bL19@@L7,,1, D(þxׯ݀wy{UHs Ԯ.W43D#@P 贉8CW6?yMNL]vgVa̙'ƏX:MCf8`t{w~㭥: j#=FQ~_ xNƑtGPpq>{MT/ a2M@Jx[ 1r-# HJW} LAQ1ChW9d=s$?|)ܚtKp|8J8 h?9"Az/6P<]Q=exz 6A㈄ ƲD_'YJbj)K2m^yի(Ycc(tCBɛTȔo- < QQ;ԝ819`4lŚD`;Nj'Nu;G?h*ͣAGXEk![0-C+*Ĝrg.ea)KҔM {(-ٷVh9(n(?FwÁcG 2]X9hIjͅPe4xAkbtE_|O,{+/x_L>yGЃAC ?Z!͈Ӯkaց"LlnC4/qIPrz _vm.> ex[]@;FC8߁ Ga8 E08pH:$ɔ.Ft7#2 tGeJC2M KHk0S*!~[oh@Oiޞ"P aeHj ,ڇ̈́jRvACT:طEx{ m&L EEFːc84Dhצ P-ǀMA|H۱=v >.85[Gq>3CzڒaKFܰi/lE/)LҼc90K98S8)+WI# Gx+fvjH =d \(!W_V\)IjŠ 3ˋLW?I2L[ϰ6vg n?:Y]S,V<DZҪb,T|:nX|׋0o޸;Q1}z0:LbĮ,r)y<4q A/̢dm}HY .5G:>|RʎQ߈(0<;)"@Xb ++$r!D qK.XyQ]::(@1 ,>DR$8D! yn9:B@F8;a kXg` @76prDꄼ4U!r2hSG6rs㎄\Nv U!<:9y^/8YlXD =XUhE,fL+v_AǴlo#= 2T2D;r蚸VT'VOM-$ G+| >Wb?)ƣ$^sbDb<9j'O%{r?arДM (L2Z1} D#R$S`5r&$>sū⑔Wf\rW`l[u ?i.Qc`6t/!޾7i<t~C'U}ř4;pn5ij/%(̒ɠdwFlcDD7:P4PgJJ0$m[ڧ윸xX<.%RU}Y% 1;.DZhtai>MvJm qsXWhRy40L\Yx Clѷ 顉$Esw AE=^E96(<Įpɋ~3YZǶa(p؜An=!`CdӢeVʓUiD9jCJ g#$Є3*cz|ј~a,F*ZՃQ]Y!{˴vSj@\e4_-ͼOwEF=ױA|6sDc ! Es ;̏JШ*XhʸmZ!pgfS_(ܐv@J#`Bĕ3ɏc338<8zVՖV<q~z$\/^0ݸ}ރw^dƙid<(v86r oˎp$p_ EVPz쑠/e ݣ#$kc})@M_VA=9X>87zFQ8XŠe{tVy$:N`A zp0h`مET3'msN B&DH ?jc:x״N|XpP5;>zx֭܁eJчCdږ<B@7+Fv_/m.-1m#53Ka HdC`W|靷ݿwEz~}'>(ʀ.YpQ;Mu-'B|Rؒ*"&˜KGZQEt,6{g }saՇڀ9꺎cE prc鰔hl/}@BENs}K ,*3pjg^Cʚu @ f8J))"!%y38[T" VWזWVP]"a;0Hbw^v"AE7TaSo5*c2CiFTk: iN#'G8 o)b <$"眏#̳nu:Gƈ*œFC9V J[)e[K!iQP4Q'n?;Rݕi#A$#J$5J(I|$#$lL)B,bm`!y5e!RC 4}RB]%mB Bj^jxNQ;#_/hʈo b9>@i9X;90{ibl^GV0_ȦI-CǒaYaJ#)8|RgB7,wإ Uvr@'Dٶn_,Q=0FlPKphA27un vgh E$L`K[[GXW޵0z 2fƱu;OQg!?a4{U!ު=³c";!(ZY .3@eI1ЕI.\erָhR& M#0?:}i/_-}۷nb.,6c(p3x/X,:S2+WǮ~c@bxtdlG(Be/|G_g?yea**rN㠔KԳ555=??;5= HP&!gU$_9'U-o,&eZAf">rj/kC@dT QrɕXRV1IV 3gNiAC5?z n l>:D4ɊnTQG2(_H `}Cꔬ|?dYiJǕ;8,ah'kk[`+W]CJ8 9.B~YZ9/8}Xmnڷ=CALYR*?VwQT8EuC8(ފ#m98ZHR|ЈH8T FI 1)x/)ŕHHL'ܮ_A1 Cۯ/ s jҿa#ԇ>:pt=8;7U_<G: djh| 麢 S -19'#f"o}puX L3#'G<ߧP-N $ӿCJ593 >X D5Xڟ  @'7,oe֒v>p􎦴};*fbTpD {W\g:G* I"&3p\G\\ PxxǛ[;ex/ =9D#lo J!8B˘ W;KE`굝ݧ+`L`>/ c Tx@G`2Pt{{{`aa~2QDd:E7%:( א$z,eaIM-Q3s !h9nK8qp|en ؛>gg)D% _eC& h{musguU #V!8]fUny։#DC.*1,[ kaC))$"ʀ+sW^cqNO̚HS3'' ,Kq(ꄾ50xwogHfP= GGp`5`>!{ߓq-b eGyqve?9ipBm znLH &/77GLsI߻^rrnuc*(Qϛr|*SYL1+r:'YC`WLFuYFAko9-s"sJTHx tz+̫8",VgA]-]Fmq6~n}+?;;*`K]ڣBU0,.TͨzWRdyD[Σ~@EC aĢCѧ*  c7ग`ބHKao(nDὦSeٳK!P՟+@;| Ky$ϋ4feǬ@]н( |O tHB/f-\o`m|p<]LttrvѨр09~5pJ˓JVН F>VaXg7F` '^\ F؜6r]9Q_Ycd곔Җ}S4 $wƢ/A#^}s}+  g[^:qc?H>(b.JUQ{"j d$b01>p#pyjC]L]c#YZGr?PK2L,CQ,R =siy<? A; 6ˀ/gf~ŅEE0րƒ8E=\F+l#eFR7b ˧_/M1ހ@ØzoƯ} E%N8]EquεkgFY `l} X)p@dfH6Bܜt-"+ vBDnƸ>1@A;p~Kt",Թ?6&&|rּ<,#Nz}p4 u "mG ?$JOT(OG֧eboG8TyPug<\d2+m:E;,B]$b > $%w3I$8eg 2)V P'#677!zc$jG1QjOZ[W1!ĂF}0HCBڝZY:Hc1vsO%OF%/vge응%&HP'~1?'9,Vd|<>8VFB\&A| `4Psk&,ط؋A)l1KN~7RN51$l.fk&Mb.98.حiZߺ@ub-j) xo",0]Ϛ} }ru: e\?ΖjG Snk9a#NzyWgn޾}.$Տ(ά:, 7,w91piu1N8RQ:Hhl޻;S'&v!.&OE~K#Q[!J#N|߄Q5i3E.},pAX9xa IiDJ)y/BW@5Ul1wPnMaEK%ȶvQݝ>z =z* ƅxA`h~h^s[EzW!4jA8e<u+ }>WOZ6Pe8?2;¾ Ìr)"saw1&U2we^GzZj~V5 瑑G$J4'>!cˣ\vapMQתB2xzoVttV\IusGZ 4sijLGʩa2FY20t^^ѼP(e;Bh"yoЃcqML:k X˜QC) A)]]u]@E@T+A3Aĩ Wl9 C <1 пAn[yjU0KR^X`xRY}Ӡ4ef !et B2L3BEhF*\@K0t;6zڡl;|`]E)؁ڎ.)/ T ٹ/  tGpQW:m MC.G[5N3꾰vQ:Ufຒ?\4[crH}\fLJ}=ZNLveh[JεCQo1S'|.;(LJMoO%iC![؄!fG)Ă؊瘥Y8 KZipaAVDDŽVx,2'2vޔ>v, 3̛Y[8+lirɮsQ;^ȷ؏Œ|xI  صcr^_[;7Ye+6ejH ZL\<$Mc?b.Nyk,֪#XW-ǑjbDs9 Jȴk:R:PDՓL{^ف;(X+҆w&2\vL0gf}컧[xi >)5:5m,J,vR8L}xN$J;49}ybvזw7E>9;AG_[rH:%a%{r73 /O"|.`e{ϥrGS!#ցImHs9wa=q.쒐̳<*'¹=y3,o᝞LkRG^#{ؤbЯ2OS|l_є|CZm) ;llj(ybu"e$EP4LfUf{sJ=? 7NM}Ag$v`΅΋AL&@$ KEeaGgRJ#m Moq7Vu4IrCh_fnn*QP4)plҊy?Uʼn]YL7㸊2U6>J<`(Bω9}Ɣf#C2r#'Av) pP;X2x̊@YCCf|T)%0o*<'tmJ|s9t;BKJ*vܘ.}0L4?&qV -9/{#WP uw}&BXPZ؉TGQ”T}g =[X(hhvKTCP fuo%Oj!;~|oR}Hoo3~2' ū( oG\F.+!vQ90pW૥W|y )A'/\z4£FC([KpF,ϝEAŐ#mK^iaO_A5|,3O#&y@St)[l%bFJNS3:mVBd*Ҏo +*3OZ4|dcG^tjZBYb1U$EX[lGBTp+gخ2RgąbV֐:D,xfvfw*tW\̎s.77w ???(AE0X Gs׃_8Rxί2$GͦYU'@I%t\"#S$N 8{[ayFX6DmH5Agh4@\)Rj^ $hܲ@; #xW&+ .=V)OAݗ͍Vڻ [Į aN ct8 ,}gޕhk-+!{ ]ʤ}*hYKQa yǛ3 *]F7n1G$7K|+^AѶ7r~nJRBQ#ANCe0J~Um wFaU:ˇ8W,}<^|K[JpĒCHnYƮIz*4m,7zNH̕mF=hX \lوI*0suv[j. {C^DAOI0l+TU"6i)!s AXUmgAps=WPxCS55x 8![J ?u8mP}kg g.b$l{z;'A=>^ZYY]Y=ߟwwPO%2qaKe R<I(we9#CAqU{r >Ns(]H nmckpj(FMX$4QՂDhb&U]%dep4&W0G/PeBj8R|\]N6Â|{Q1;xE C$R*:."H2۹#d xݻw-_0V[@Jop HI@APiumsmu p >Y擧?x>_})J?89ɘKkkdމ؅^1WnߺzDP܆ nXgQP6F$9[^RC7`'[qRR0-NOC^Pm6j#T)id9H _@9l08=:@sMFʒ@%K `# FAC}ApSi5^E8Q-Q<"lHa%ŴEѲd ⋗''_ P0-[ zsG9aPWC-Y#gFv1\LZ^0, v)`.+^ώ(d$͡5퇨RN:Vdd\ PI Tྛ8o:!eY&Xƣ ZG1$pIP@f{x*5H|Z<Gpr^ڹ7THB]D$, uIԤM͘ |"Mc ݘ۞C$=4JKtK*],w[0+ZXAAo`0FVCiT2.D*|(m*\[ci|s;q\_p6Zm$ܓX-حÏ+IA(ua7ljyF`]Ϳh k2V;J2D\lXM ~g*'SEz<{"ῙcV쌱R/hPm?tU T<ѐ?طEιGmemȟAnxc=~iV* uQV}.[*8 @99(i^b젎N iB> @>SG􇉓 M6VcOXH_gIsg2E =557wa s]*h 0̪2[:;1Yo+{EѽGC 2ܝڔPptmzM^SZ=c)]G Z-&mh:(Ωty至12`g3Ee?7c,o.(xVa{7դ4.󱝽SԥDB?*$}X g>FG'q1A^ -*>!Qc:vD!'wLF d=4 3<`Kd4 U=,+H :4UZׄ+G;&PV.'ʖwp6`6 o(/r}pӯ!HH!|pw __?GΝ;\722w4/ol.Eܭݽُg{/?KzCe NgO?XچJH0E+c VA1H돲L־*'E̦4 *pK] fF +!e|C&qa:ǡc?Xg3SFRC9I2fx(G^ ݂̓`<5ƞ.+5!$׹IN7kɉ7{,^m=Yү LUpl9!_p1M# 1k_tхK B\ʐބ\prilV/=+enGde|۫lN*/.r,je O͞r&e(>?bξo5uz MrLAm ؒ3/p;x2(vh@rH* +kDqOݩJE#VXx7K5Cر"~]XiU$-t5Ob$ 3pS≻: /fS$Sm&8E$m㼲ȚrV?jb6!g. #'88OpEpoeeykk{"x A :FwnߞBeS֘`@ "̍Ql_FH_(",k.5|&/ҩX)*eJ}p=jkpz(rU|/v_"`-HO OF=;7RxX @yb23;`Z`6Gf9 |cs @bM*)^Ύ#uޞ}A 1O&&Qft&ՈFl|?%ыw4dxV_r3f ‰=0只V7h)WE,%U d6iUMPC٤‘#a s5۹) PUmΕ @& ɖ 2~IT6Qv% fi:5N[;b "ƕZtc 48LMy=/][̔FKҗ鋸Kk%U]7g$ $iP>3'YQr Vq+?WBu\1 9 }WuPAOď'o^Jur)r&]Ã;#(C 2uXOzU`AVsR|Ma3>A-)尥VG6545^36ՁooHd>7߭\pTN\!y3+HWRF/n|y%VZ~vbpWN&.M yϵ76$sl>E{xa~ a˔nءjp_/9Wn߽sM3zKh\6໪-DE\8H[UA 7h{`weM7P*顚M`M1mjaaic# .μ"FDLq)?MM|mT<{p?GІ ^ǝRdT2TM*ݍgy_\|LQ-~PYLh&8xdT#~5D2JK֪ c ܕˠvE3TGWW6ױa^.^E*B`W^o8!8h  1Gb屑If6O);==F @Eilu+Ksܼy@TDl XS!c OѪ™7) _C}8YKԃE *ӬHYy H8襸pHF-8 9& Lm9F Ƅ dF0xJ+(JصDD-B=]򓩕݀Q.@e2+ \87Ko!E^q,r_[mDPBTD@l9`Xa  M|A]fxwp8HP8g+s[D5WvRR8%C"[&6/#̚3H2l_bE]eyŵ*kQE|:޸ҙmҒLKDIP(+}0ڧ-]yotM><3FBfn&6ZJHƝ+X (;?.<&tptjSRYf?b7ࢄ m?ߚF4&Fy^;vßԅiuϩv*v*DZ1B)v&S.DMIGa4jo))1r< *ېdP #&RiCAY_? x)GQ &JewS&$!)d:xgtԍb|) |m6lW%ޭؽT 5:$\{ʷkIJ{nMZ -s;l vaej{(CAoLj%"Vt0<L#-!`'ž dE#d:/9fBғʕo@9FHҀ@R! 3TgtU1zJ*G a$/Gzw`sя 3ba ,&C N\ fh8H[\: s0x!&GۻZDL--`t nZXJv̔dx?g`d[ e 3`ȦWz(*j6wvV6=,9L# \Mt}گWW`#N0p;&ݼ9uyn!7țخi߾DgƶbK͑3,b xPHVx*2ұ[Y (|s% )Ѻ3u R?]$Ѣ ё~/^[D~BW`f uG-Z)>"ɛyֱG{ /E{E0}r;)ӣᯰ(/L@~CpB$ !~ 67Xpa<X2:YxTDۘC LJ hW+CFu'WAK+&񶆈PQEN9LOfMg+!oc>_-BAOD(vf0E OX~R2PVB d1| ZΦGv>Nv͉ILv2C1F7P <ÖZKhLaF*5xHoXh AhH^Zz:Q74=b$k$E\i̺p+M͏/vzT<5ظ骕V}Vaf5-‹hUNx13Y0pEP|UН^jvEt(C //?6+:(C҉Vz!` 88!uD<Y2-}q5Đ}Br p0 |1b.)0F+` n(18#(pe搒|xxug.]fQVdo}Q ΨNm:\mܫu+a=A/OمO̜ 3q ľIށ$XdD>"k۸>=REdVmR.E3>60Xl@j1|]x tЦzM,\} =`P=;ʫU+WxǓgO7"by^";pC8$W)WS'Ȥvk  req~ lfߜ**Vr'6D? ;7^ =s (a'ܐ~PFԧl;XKv6еzo'p xD gf8DY<!68ȵQb:!(J"J3"J%`̍Ƽ8^U"b,mMMv3ز"ߐçREDi4{-^(E-6u#wIu%.LI.d႕W/_!$Np3357^ !ai6TzNk?xt7h'Je.*_7׮٦ʍ1j dk6+ 1[k"EVm}y 8ZW$"N)MLpc촲HJh@UJS2PٞANBMtݶNbJxHb@Ww.?c: qi-xW)-B u8[f6Pz2b%Uh$t̢'fUwz o2XH$ L|QZa ̖r{Ry?#1lQ9UJKT˾Vϣi_ t_κ քht\qE! '^&z ҹ``?#PBOaQ!e a@/#P/۷oܱI!`+|xC`a[7tD }Ē\^D*uVRoZ ;r险ٹ fr$1RߟJʟste͸sM@'YigncUҡUMKZ@fh&d&C l5̴ihQ9jca"@lr .?`c%Zޥ]Pyƽw\ĉloG|-i[`A4(ES% v>y#Q*,剀`91:rm``@-L _tz#Ȟ fb:OԋfOeGu"O#Iv0\"Dr6[ lX5{9l}3 UftN\D+cEHvȠWo8׊9O9EZ1<5:phc2|T^!V~*ݻp.'?^~r{Qq `mp9"Mb:Rq `a&Qc -^~=?\zF`cq׾[nzt Hpv7vTshL39ג|lG Kg&N1͋$t&0w\7#Y/!蘧S VdpT]H,J^Lh-$گ >Kdռh_:^S_w7"&[J]Bbҥ}x8*q"<̢qsg|ak wǢ??E`Eu'W?w@?|K5(/K4AtnqݔH.,"ZQ"UҋK$##ݽqZqd B?}|*ɓL'bZ'ES0v(3.[5J-4 ˜c&CeDUﲐL8`esF`j5{†i2"WKt!GBꙙ9܁OG_`IkpY u !6plFҾn?|9|::/JB^y9Cei8Й> ߰F`0j{;ҁF3P Dᨣ5V;ĀPB,7>y^w:deT0IlRR  OxQzNrA9 #y8֕smZ$nP=TcAGq;1Smks 2ĊQ@VG:&Wqg}?p:&VWWXǙXh (ɵ=3.O_&;w}k|˥=F^?Ы,yid_W,a>M M@ax=JTF`hHܒX%~-1!zN_ȐT&GNlm#%̐l"Qi +/U'˝0 Ѝ  ' ̝ccc(O𣣃?9 6mNL߽死8?n`Hziy{wqPE=ԁ>5T[)JJ&!"a5yy5UzybUB :C/5nzb gMܿw4N1g85h{mŻ[UN*VC4Q3RFynvW+|.2AZYûR*w*p}Q*Dn5ě^~],yj̴0~nVf׆|2&ۥn_=S3 "ͩҘfCB"1e`/k6Rw \Bۊ)yaR`vDrk<զgI3g&đ}ljX~ rq2|" {AlJCV ;vS#iFTF]w([]`i6|y>Z M>zhb^y+k4m> ^hsa~+4.aK5@е~erL U]/̚ss3E،܏dٲ?{n{H]ԫ̓Fѱ}>~ccS3zBwkAz'Fa\9vmK𙅅p,߈\^]x53M(͛"wgm>@DŽ҃NW&~ԅ$EM6ƿwݚ}kSLz48K6 !!{ -4句qvfo[ `-y^A;RzcC n  (MHc$*(xpihcF*Ev" AKc;UAJ|_WGVy_B::}X>EUπ"zis792dskg9q?9.$`!wdp5eBeoe- LJaHC/_z̽ɩ9C(}S.#Y=#m5\k2ͻ*<@'VZGjc+BE?m,t 'xYI1{yzqa 8A'nAH H'!1vJmQՄJmS~}JeEƘ x"1D) tr9"g< +h p4>5=8WH3;p* j \:?z!gXG(" %{pIh6Db / 'k;kׯl 2"G'  CDˤrq2:5c!J a҂_#@:ͬڇBOy 6&aN;s70\EY`JJuw1z8FGg+ӋgfP vz">@ϻw,x@w&>CΊR#aDBhXtPu QHilF44#5ĴBZ'= >]*0yB0/QrOpܕ(Kwy.HNLΣFCxAR')UȢ<Ҙ,ahr #%dsIelTJZIlnoU9+_?leh6R"JhP%IcU&*?1԰YV? { e zBwXFVpX/+dW ԮNh b&}[PD|o4 Ƙٗ`bD%&PR(8)^ӷu%b 5%U4-9N[/E{'[/CYUDK 6|kVDb.Jn3tص?ނ2m;ёTP2C)^Ƞ"!nv}?dv Bj8stFlieLt!5PL{I">xpwpg#Ѐ?z,&&K^fI`YZ`KZa"E rO]9@Ï?o#0!IT"2h ޲J 5lBṪ* w^Cg⹽azzƍkO+nʬhGČC1А:?qM4d'lwůT͜($cR]PI[8ƤB2!4dض):"[X2h Sd@s@*2//xEݤ&E 1$N&[;8:HMNP Vg1;0i7 {[؇CpQvL!|O2!3`x.J5\cgR'T3's|455yn"ʚ,/dITϣC&-Kxv h y!CoM(L0^?ݽŭ#ԃ)-+D^+\鉠!'J*VaV霅ˌlIagB!(s o&QO"Ȓ'=bq:/ 4Y C;&koy@wVOq9qRxvg[PՊSQb) Z\vhã`N$ե;;.Ko` Fȍd)kSh@ o%`)7)DPE [$,crs~ ? 0_a/-8 HkAP2QwRk"(i>%R,~VFC0Kfΰ=7s.APu]{-u .4&w` _s"pyL_GenoxTf*PR5:"~r 6(d4|SnaDm-cIj05-.y8rII vcT-DHG'3Tq<]'?v_~ +|z\ru|P6 /7YՓl1␌͵d`(VVi!hw6[zt1|_=QԗP}!UIHLYQhu?Y0qWSΜʘ8]Y 5u,݉V֙B{o܀ⰽ{t(xūXGg ˛8{ߺ}ūtQn"., 5ZC7 c|V ˉrB+,G <|L8bwgK^2*rU/W `L596rE$9SS[ZcE@H=FAK*zr/'BvNa#V6p;ώg'f/_D U#gdKpt:r2Šo*GXشXHVm83H3)7'AҒ_CõUdʬ n@"2Lqq6.@7"0; G8'6G|oe]f̋aNAOHƑϩ-VUpڐƩsĪ1|OBo>pv&eCzOŅ+2E놰4[]9]z!J]LCx9]Evs!Q3fg}^?p=2_wLx?UnL= vIY҄J;6u.]. 2HH$ay H>C}YIvЋk-DIC7 ;*;r/}Ed}޿˗̤SU[rP*m3lW}E Qiggo^҆?z% 5F;_Z^YHʻW)U4aNUXK C<}Ɯ4`Y\W n '%{0h~ C` +E7]BH4EfdY;PvmRX\жFc%DɄZhvMJ&Y*&BjsGhz[er!u.:x*6,ӉӣWg]ÎpD0"".7!{71uP,/Ғ4'8:!$ Limh&,\Vcb㱕]n qetP?B+^V,'㸏ł[pL<38Vzwoߺ \\\Z@e8ih׆ #mXf͛R_Y.y2J+*ǜ7ԹQd 8Ge4Aj5bn|Q/` +NO?Լ;@BKj0t\]hBr;"c1/tw|t\3-, Ikf0$@!cJp# QF,| ؤĪ $DQdksS)R0ZSQDЈlN9V_޸bn`ccR8$' F,gXxI԰q! +ܰ~#t|t#LLAIm@L HװTUsu2W['.bi\HYW-Jݩ6)tE}.ŋMH;^?]Bc@8H3a&uH ~cV3+g}7dcnǪHOmi(󶢾r*[=J Kl{H˜& ^W"FD>16RZKΓg'Y(R 愂dvo6* F#Bݑ#Zq?OU6S.0fpU3#0[tԎ$ ߄ b exCaMTA߆M:m ׌k5XG~ӏ 6` c/̯ՑXu"^XS\wFs-fR7hkfzx? ?  p?M(ya<4ˌ 4'QOjzT<+p·G|WΟ{?y0+LL'p *0Yh1޸y}{GWAmT(Xw.OuԕXYZdv sF]Q$}z1Mʄ\Ƈk摒 KJu1+[[H:xz˥ׯXz5HMOzH9,!a8zq>}m;*N<6g ڟ[hSC_3Ct[L=<CI@ls^)x޾6qCM7S\ p-NNLMNaG2@||d99&) +LNz0έZ×{"0 Xv.0;u]D:|2B~p>@BBl"a :M{4=pv_] V:D(b@Y2:(WF,*N_~uw6uK5HGP޷UfUDp|(A =|[Rrnt&5:=Ot nM1A㡔}ASp#hlX4'q"CG,ꇈ  ggF'@嗯_,-$2`U%V@:`q(uV?9$);WP$}UCP*USy泽 '7:=Ne;sQpJ EAD-, 1F?U6-/"YxC%);C8:{ P^bU4pl{ F :q'89MOL#l6M?9B !~h;5xpL pFq'luV&% 1pCfpxil+8Ӛm_Z pD@@%5J%)^Bhv/5R HWzCf{C7&[ΖRFc`皦K yw).>#p4폟<Ns7*X'0L[;]O˯~(.%z 6(P lG8,kffB F7_z%~v63v:2 ű9r i6qR15U)x+SI~ZU.c: vS\+@V<$'SFv9V)LO'η77^"ʰG=Ž ,Oܮ3@S O)[`+3zat PX$aeQ` ( dkBt7_LB1)hi+Y,nLuwf9}H@w$'̓ffY#}?d ,#Ȍ#Ce4 ^FL԰񟠅YxDet|88aM)( Fe|@F >7.j,^Y Yi KVR4`wr@Hi c <:4ƭU謆jFsG:VJdDJHZ2CK0w\Owt]'I+Uzh|"QɃ73n]1 t΍1z|nW ez4.* }b#՞-S3o@OǢ!k-1S-E5"^ ꏈb/$ 5)EX _)w/s"zlboEx t `!Wl4N?V 8dasEM xZhzh-ƀ5Sg>hJmj$$G?_.o&{@s7 M":S҃. LpO=5‚+CnҌd;Lr)>4$*0'2SaQ+ e==U[BdIu)Q0/Ƽ3҉HbiV=%"xg e᭷h3;}0/ٟ%ny}0S|竿w~p:R#񞛝8xpD9__|7!}]f/~փߺ=m7_'ԧg>/|_~<8-T5!P1{n!`eip&|\$K?-9!1?j6ʏ!lcE AËK?G6 >;j3GZ +3s%Ju!|l O^zWv8O\9bdɄ^R:ģ>60 9,3.>7oQDUG—?{-}xvscӟMD܌|8_ ܆|c ՟{8 (h'>'>w>y=0 $^쭛CBo+Hba3`r 'cTQ ^a\,ujoU7&y8 = y4sC}t̓?EAJF^j~a>bT0 (A(s9|U4[xr!B 8QW yCN#\gj*N\ M: zi,nހo8^pn7^>}6=>(e~mBD|TWDTm!X]Bwka ,qtrl#XSN|sX0-,HO "tEEjh&~2fV1Q\vh( 1a$[vӡls&!}!7&0L z~clÈ N#b>}sx&"Tva I5 `SD nSW?O~s_[w̏() VԠ8zG}_@@F}{3vwx)0+T: Jz- -nIqcŌ.W519O1w~@;Fƽ@^2fOxtWLN50*,b42?3XR9.;HX\&4 ZK<:k፳**SBN_X WR5C BFD]U(OQ,Z0vۅ%)bIk9EK#SȶБ7ţ(!AP$L<&s¦iae~e'TPکg,#1z12Ts|؋wҤ. [B)&3.%czCܼW_v>O(in@QR)o\Gs+S L>s[6BItf_:c(z7l?qXFfd+fh}1=ւBklœL T2=S9x `=I-H\nͪmR~粷( 5#l WrCMwݢƔ!S4WU|ݢp9vkLd1S-˅n0A>V.-FCwn*%J`δnoS]{?Y$kCb9J`Wr՝8#- Pl X;}}O?ѷK&bǠe2 n-ژOW\- `} ޹s7 dՙX8T}Kgt_4\'wA^z DuơѽAoGXs2R^)8̧& *S[(Pb82U/^67?[]أ6.J̞aפ@㳴ZL*fH^ g)1/aA f2}J1P*%^kѺYbcg|D{:WKc::kG}ϕqHryk7zreeTC;9?9t,ˎNɑn'm3-TYD,{W^.dɵX^mR{rVҧOZYB,ϧ85nD!793395,·q 5J- i(5R?dM*HDz`F(eEL y zA+K:3a2KF}yQ9'RP8I9(Tr 8_oQRo'r|e`Pi 0A mMeu9Vٕ&G5[{0X,‰lđN@q|Pj˄>`xld!\cV2΂=IP^lY1Xfb;w89)7-HG3n0:ʫxR"44׋H Ue^Ow3aB a4s/s quP>vpV+|)*Gw oJ{2p8Ȼ#35u /4 FzA~qJej2]dDkq#5*rd$WD$G}!pX2!!j~J}l!\GAi =FN8*#j<虊FZy_ֈIPP(o?yP"R6?Tx艱*Մ78~1\zWC|h=)l*:>(8=Oē -$u|cj(JxN~"Q7!!9exWY '=Rx!dӻ]9''wv@ҳ[\K 땟⤒~ o>DS0@  a]+Y0Ìd8dhu! %hߋˆb MɪŠPQUi*@0ZL)Uz9#(!1DK,bFv45],YOziFJ4o/s!6^ 2u_|P.Pڱ%D71h"h<(^le{3ϵ 7]G|\pAcHC fW8-p ˏ?^} q,bM ȊjEX.SyL+7E(=& C,r9;xGww<S\SA& ]Ō:.~*9Yd"YHV1"AEPCQ'GhoČGe'&" 4H ^YIz^$F48bJtͨyݺK+ݓ+t-ĘeBe#!sS+E'nC6# Fa8 y‡CojSh™T$nntzU7i)9d [n tġ 4tc*g:9.Z:. êj@Aa%:`@aY녢++(C}$?mev z5|cck`s3^ {o ރ++lڇ:xû>.yO~ǃawж)rrN?8h :cM .hUA*0y ,bEnhk_/hwR}Y}J*r]tiΰ!?N,,eqas-za#6^_^^ BGae~~0?O3vi :O3S=COzljm܇bs{1ʠq7:C{I dQ6!mH%tܐ:' xyK^tc]4BNR+?y"j`Pիpፌ^>FɓQ\:&NۛqqtO^,tkeG޿7{QC PB˩ g| Oxi5q9đ1u&׺˺އgk )'f4Q'!em!mLW>^Mc d̗Ҽ$4gh) )9;ӛ@ Jр}-_*c$T?d1mPaD;[u$L% Q- zYIt j&NPɩ@ MȂCB( m̦O0uփci )c;$CݤPEkhK5oM3N*Ehl٤ orEꢴՕ*3#>^dVnPl俚+ 'b $=RW='*yũ&_Xdw_??w.}a*PPF~ٳ׽0MzS6Jyri 5aZ`:', 9p\?v^D$R:,;7fH`;fSN ikل5B+W ܪȆy}o#3?)\hTަ^)Hq;29W??p"ldvDp.vo>| 3i܏R#\M)BPB&Hqx.]!ؘJKJOa UsG%e6 @mC\uyd΅ΧU촚HV3A_Z!,iP:@Lh\))JT:JDɾ\pnnS"M&=&0ZHx,mw&[k# 1pD!Jݼ6֞}tQɑ1&yC$_:i#cpCF(ó[[ 33CbUg2`p]%1(ᰂRW) ?QYPv2puL0s4l T>:F=or1Z"4A"}E<s iTH 8j2E4^PR6}LDGw2:s<#@w5@iwx[(afnڵk J g0+H,SBV#hHAGc\zv rb5)kZ>)f;; С?jxVC@c0ŭw33_1!ȂxJyW鬈d yL@g2L{8>C3)D*8_"R稧(Ht HŅ㣃%6xbD (DC3lax9u-3-HdT=3ueM($eKvģeFI9Q[$N[|[ YžE1t3OX&iHX3=|{zQP:Fr̵YBv 1=/5iC+2b9Tܖc!c AdfCn'/X6<[I֐_Qu_e`UHNڛG MWuΡ5D 50$V_0> =MVX6ߪIHvO /Ͽ׿~^xSp&X;~1 صJA)7ܻ;0U Q!S00?O?Q|(nY9)aKEr̿fkZ(^0^Xϋt`iݹ?_᷾gH[g 2t~w? Em:0ٻAGaXp.ն6S]uwIK\6BʼSu&4%츈c/-˅\L˃i#qƎ]H)<4^uF2K2NKo4$*F'Gx3gruKQ yD3h2w Bo9aw8d|lb}uœ˯WP/D{y8p`cygXv>zdFLCM|5i qvUNFmtGsy5򨚏tԔ@e͜]|aDgK5`wbL 5{[fG72-xE_ X`-7$^<6h-Fxjaax)OC}"yQ9|O4EnIч[2\#M=J"(* .e80Xn܂ zԔy=2+c+w3\ay= ?)HӼñ]oJY2:ə6wHjF Ȃq%wP=y2AzcҪ{煪8 ī6D0`GMHXӢCڇ>I-O翃u{QMNT jriA9{#;/_mzyr Dql/Vq&1 Eۈ+I㤆.'$|шVsr!^CQ_M24Pȥ|_ Jص]8.P MD"\`sdcqlg˦ʗ].fs5.yI~nNTh+GB6zZtkDvB =J>(E|d Ia qOВi_CѩL9h43$ک7,bYhwo|?~_IXݑ3]|Oػ&[o=@B[xY`.9= xP0!,^K\n)Yz(}/evc\7\xTuz<} _~m /ŲXlRFi2-%CUa8NWFѝȱ~{Q"bNx }(#~zCn?z󶷶"P5&0V5Q[ūEpHVWVOo.\yÛn!<ҕC"R>V~Ƥw[/p}-r.؆#Kz(ƀ$ #i8FG2HU4L U"u A" E$v/{,{T*hGj7VD̚R`',]J[ei\Q).C"*^a@όng\&5?Np0[Nӂ90 ?VWYXgv- ѪZ;Nc a\EBJg6b څf-ԻWj iGʉrWvĔVb;pLQ>uqx,t ,lƄbQxZhXmмk jLPQ&{ųO77_Zz26vܸvέNL"؞J6 o-_G&2+Z2IJWe7HD*M,A`GiQRRh4^I16HQ)9 9f0sMF `PLx\Mv'|lU0؛ \ֆĘιXYkQ+0fh R9gm8uz D 䄇jDG{#W[CRKg-0U D'\,a(4?G`qUJyr tV Fh][/YQ?(xRt_ag[[4iab3xV}bu,5hc1pfL& \甭,l;n!S7nݼp;k/<]z|go)D"!6_oN2RiIS%,,v&22 QG9@ wC\*z+\z/ (O * ҥ}46&xGP )ZBւjYHisDŽ'Jn#/ԯpO.8{b2+Kn hzm"@h]M\A kv\FUK bO3:W>D)'OWWWp|%2>;GEq^pB "Y_k#Yat<7}a)38 e{& $? ڛp_/߻{#xâ4`62#kxٳ/s YB5,7`kB< MŌ$qPCPAY>t,]% ӌSĭȤhE^ݏR@OL}Bld%HԦgãt(! d'.!i־.[_yGxPOR-_{7{a?}.WN5gd\%q8]W4:18,UuF߿-}dpUQ#.]t;wn|˟+?,6-.>$q,&-21ykSEWa0ZO?=7t>r\Xy^zD(~6xO>1^`Yʮw6>n卽/_M\XwudEh'U +.j y"/C㗱Kjz|jcXe!aPLZ^fmXG x)u  DܮKm^=I'8Uy:g}r"Gn#:Kc 1 X3xHGR0O@m3营Mln ` ewo*&CA{S7w-^'d'R٩UlXStbo/W׾v# rkg[Oͱ$uz̪>|ӈzBX.K%$Hm~1iJ@͢ \SYen^I/>`:Tc(ax\ppj!-(z8%l@e&pK$<'vPVwt>ҌY}O7X\\|;,\CÜO %s7ڜ#!զ}`SH ܙc*9yUU!3 =)Ifwmbrʕ+cHHP _wP!w<*Hܪd1 ̨]O9=CȄM^!2Nx\zK(\"i3Fj[Ơ@aFy5VtAlZj+jBx⪅3>سͰPAdJZ $ L R7dAn#}u!@ iiV*r#nm 't1s7</yVaSsQn+̺ CX:{h>y3>9ݚ^YO<"P*w%P=,1\Cc8E%V0Yf W7tj2HURm?jVE#+I>:Ģ T _oMnL^N 8A !<*u vD#238ЭUp:AxAXIu,'A ;*B02>@'4hY9qp9Ҁ7U2[Xš8(SWܻ{i3yZ,%] @>$e4!6˿2s7*iux"'ݳl]x&ME$}_3=2(>ZkOcUoyW: d!~_-p$9, i34ڎ-^g #8=uD|=W@c/i^̦3 QHW&fv_b4.*"0ڴnm8 OypLT"ZW*]9b6ABk߶U͝JQPa9ƲwybY B<]UH#eٔ{<&x!$a Vߛ 05^aa @ϟ4;@ȹe dú_C#uXM~k jmMV4 yVyrN]łO pZ@M?3 =dqU !U2@DbJUi7c0(fm_5S.”w_ <㌎~'>-a߿W>X3w_7C,p}0 G`W'N/rM񊱄/[{bOF`cXS]el6NNgs쟵fg7( bP@L+Gg>C@:__|嗾%K÷y)\UKCKjͭ!Fl)l.Q }p4l`H<ƳB0-,藝TЦB}^({dd_c:nM3̃FsJgdԂtԏ^ꉹ-nZ?B:q)O{Dr9iI~-OIaYQ!/ff.~گ/ַbV %sdc~091fHnBfq,!3 & v.L{x{8uhsso}`g߸ѵ78Qi:j5%n5Zn}Rj& T7q:%q6!صjEy<B5lIB4oaR|dyvU<ҽ e^2tcD.bD͠4uSIXW8p;CI)KrSpׯ &3.45E;@뗯WVUlxl{sS'g(@?|:_x[V5zڸppg?O'BӳD`VD?KK(!JFL&Q S e39L^FXȵ B RDX|,}KbÙ"?:pT'zBrM$l빛 LOdc7 ]  $;OEl힐Fl 1!~JPUksP;Casw'>+C;j{-~Z B)Ik c0tAZ۫Y?@{rc(F3ϤvҰ:BQ2Xicr cڤ H+4F={hEk=*6ԘK AtHХ}/z,:P{ ĶՉb6/eQ r^Jtp-5Y(L,|<%)FŘ cMN=Y>1NCWu\]ķ$jL IQlZN81AM։[!TXUѤj&fF4DhsU4Vn$:cc5:=憴fLE冤Hjɂ}1UFL-[Ex-a_/_W_~W2uNJNB#pޏp%*k'suqHʃTHԵoyDwiy?n\_MioEn""V)aX ~<_7_]q] h͝]䃨]#>`n;Fi=G[EQTD!z2GOL-5t飏'e&Y  ů^j04 O_܁"M _^צx zV֘{APH=cVU dEFqϱZ$,0cٔ-xٞ}̛As$ح×Hu>BݨKt(j ~%:.z_O9 PHm ` @2"58dU55E i+kaU\an|Ņ~޽y T<{ۏ޾qfApd!Mݹ 4|z2`? y8bsBd.!ʀ}\{>FfsR7VUiu:iS83դ TPCe"d3B(`Z@ M@V±FO2C]YIJNRϒ-Ծ%VZJMpߠ@X;&#O+aHg}M5nab yL|C"k6h'z(]AC}Mv50JJ qx qncCEi2؂BP @D,['[R`FuI5Mtdwe\b}= Wkvlןl4c^⥲B88mhk8?OG?[~WLA}/$u \eLع*BsMǺLj͉.,NFPFW[8 ,1G\K:]y$Ԋ+0dBE{5|̃߿5;C%ܺpe.N~FcnU45 '1#vR:PFGH#yJRLO%\/'V5PdLirXOd\ |+՚֢۸=z 58qE{°̔B!PZd&W< 2:( jt4g{e^$ ,@3AwwavD\< =CHm/_BM lAB]~[ο?EЦXm&1=geq7᷾w>Qg?.[~VUYs<ޞ8vS4)Q"h@O~%e@φ1 R y-&iiҜVws|Nթ!2+ʬ*߷wֹ>Y{"V)Zb_0N/+si(B@bp؝^cHxղi"W)z&és8Y}pri1u\>Jl aqouzpt|m1Mzcgs:-*G:sL} ɼl/< ~Iȱi!W|e-)yG(`L*`Pe*C|9X!zi|[sc @hiiǞ|d&&]#"TQ<%kTWQzqY@g[,Pq'p8Ppxp}, {l:+VQxyup D!|)t$d'xwϞ>A6~ehKrQ6K) 8р?_/ſDE+ sJ)7 DOQgxoE :(·xΥX%Ur6!< m2:(F"MtMC ^I(h:5%?fe&Y2CZl8&HۙG.7J㤦K+ي~V>bC[J0Pz!^B%/š s%EECB`6eĮL,`I i<#ޜvVr1i&&<}1']͙iIW+nyfv" ΁p*A@_0}" )Q VTZa$:½&XA{nAD2 l6JĚRLBPa.C2А:eeY y]+VF=:e6vBtaX >pv F -oh C7½hhN#n>l$*+KǨGbM9%M:0Kð[Iaa%=;^e}0zkRWҚw_=4['zw҆r}0r:1@7g\wt:xvޏ{?o:6`e\'s"u/,SDsƝ}W6菾X ϱ9^i^yDGOރX$JK 5~ͷ zbO!휐ؔV3#duhLci44*S͒2 _|m"04`j/ᦿw"G;?RZ1Sst4Dk1df~.׎ܘ Ӈ4α$ғ@Eg{>Þ%Y* S*gp sg|h@7#qΨ\Ūߘ h`p*gƩgKe1޵<8PQ__UVD~GB9ϥ4e< .d)sB#mΗyCf2|#xm@!4770*PWީB#phʚ^JFഎrdĝفv B7`rEy\`۬POE+GhE>TlvJG?tZ\t6S3tL!VW6#Psu<=m"$ a!n_dLcp%5l% H LPM:J/0hG|RC(wQ H$0=A ̳GviXDKn,A`?<;[0X0A&[l \Z6&Abr(8Q%ݨ-#<c*XE??k$E>8#u8b %Mp$&SP.šCkU@qr~ WJ82E q`/ZR#‘\No`x|;'B Z,Ai9+@**kLe(eufM @jlv+<&گٵϵѭ5JT4qסF4}a!O,Z ZְG:̘2dJ3æ.JÕ 2"sa~5il3%Ug$qwE[W7}98L2*άK^O?ll @pa45LN5rkHU2z1QiQwv(0 H&\ c&zvv;ND1)dtF8 NڶD"~=Xܧ(zqI83%^/ΐG+V`AM_d4LxhKwRB?i Q9,9i!+͈A#B[%Y4RW)T\:-^}*娡G.~\ H4LDpZ4cdm| h%u-ov ?W쬹 MeVW87}4L\E' (=/V`B'2 *LWTʼnU'jIX1 `X"٨ :Ɂ"4B! FUL| [G;2gϟ>{r%8b (ۀ5y^.@RUJu1,E Ɲ vG KG8`-F2/"Q˳ЮXcc7&q*ԡ(\LE&i& #I'G N`֓"Jᨘ6I6Yl6D=ưfVTA`vCjž-~$PY0c)[D`JxBNE h$+beQ9ե$s˨=cJ;ˋl klejm+AN].D3{fUlӖUWAULe)^C(g+CCZke~p2M5̏jЌxqɷXH]{"`z {Z 2G!F?@6tz\*M~jov3 ojj,=vYD@p5.W j #U\qJYxcI]cFcd3%l[pZݏ2iT_g1^<jbMGE̮i9o3VVXfɗ5{i+]_3SMbM=2,ɣ >F$$PO&, 1EE f;Ql}' ځu/vA~BbW8Wwq忟=EI?oy|ȷwlȣq;kY\X% ql!{;SNwW%+XX]FW,}2 o՚"B? E?+O;pa! ܟNQU" O{@IAf]V%s-.|8}7~ӏ>|[[F&/?<:~]i^ca956V>s'=`(iܚfܱffxxu@/rp/|W==<ɧ~xui]0ȊGͿ_bX*RR.^Kos?%Lf'ǠEӏ1k (j,εeopk׈ -sorO>Çw g֯~ ˗]Y{?Cλ=zG?AIm7q!fcu#v͟Q,ۢGVV5Zok/aE퟇_5UOGaquhQ  92;7qC`1;.={v{+|^kk8N<8'E.ދfYG$(2W_=;o~[k͓\,u[ʛN$1E.! 7޼ɝX /գ7{G'g\uRm "A.9j]/_ņ COewћ] 4p hOĦ.r^0U7XPGNyTU>/CՂ_ Yh(C𻯾.^^_o~.;e,n<8@E4lml@ T|r@3»|jZgHjcoDK3mf~ɊhbXݿ~_k%/m#g`>Aw%KèOfihF+#=n1xfrkgT1N2+l:eb&42[y:jrZƸA MD T\ӗF!O/ xhbaQ:3sK} \y(_?;jAh;/ 8^GpSE}nZWG\wNCe >+AA:sʲYWsSB Q]6h4l+;XZ!nH a:uIie#u6Ƅ)L\}cu|#ؘo4hmJ7S(bkikkxkJԺ  MNy0 ml^aAy^qU)䕗3ݦp> /&#$Hb~eO  A! Q(,8O/Y[Ik ;Yf5< 2\9 |K2]66c`Jq`h n#bECY*wa =zi1qB1BdGs4@3[_GΏ&c \(;gw#Ѐ<8V/y~4|RUdH%r~l,+tJۻN?W{7ɥïwo'olzQ_k®PIQ#L+W?$JyZ>&\eyրYsYtҕː- ߱<*ӳgrٷw^A@jy$5l"Ixq¾l䫃C|)1|Őx}r;fpKU@Ygקƨ"Xf.o#AA-83QI䰕ObijDOl…;7~G>T;Woܼɒ*p?Z q;m :na1[*]#MyZexݛoqJ*.Ӄf>pp?vwL IfӌpYx_%sФ Q kH˝WqI+:G?o? CADO|"!h .6;LAU̐K:+A u)IS=`*:U++g2_Zqyk /3MD@?|BB0l.X9JA ܾ{}WO+?9`|A )XbuRQ # ^::<q:SЪCȶ`[һWٯヾ-X<:]5C_hIHTN#Y٢HY@B\HDcde+8 5mK<`_QPB\tO3>?z zoT2cl/giia0c^65Dui=ZnN {"$Ge]QLvbtsŲ/4̱n2ɔOs>W8Y-Ķd`ٕyWS,^ $۔ʗ .[%-:΋$&249F1Qu`+P m%5㶽^8J% NJ0 w?p)ZqRا?r Іx.a iFt$Iή1T͜9FƖ0 v"<~c1|hi6GDf^(G7)$QBHUD0Oy]6}cto]7Y^o\ QyֺZ˸TƟ~,ÐfK5my}fZ"jpz}}ʼnuE6{_tƍׯ]D$FP-6-8Egeޣ4FvϞ="՝7WT@X%R=j(\ ¿D?9mc!X"E XVeC19%jKBxе5ǠR3Q8+ΰ &k30H=0O Lj|ܣ_( ')[~E?y_CyJ䱾',)F򖬫cATWf4=?F* P2pBN୯ܰ[y>|p;<@=Mf-H\4Mg$Teͭ|__䓏oߺL&;iArL|,Ѧ3i\Gň:F"дŗx(b ᚊ4ehȈ t)940M 0&^VKrz먳%N \GaV 9EE]l@p?h$EVΌ@˽8'b)2vwgؤbp$$paJsPѠ`:;e`Oz:FMA"p(Ҧi]6gbَwvSn8VUzh;( i. =DhNϼ8+ڰ;OB͏ ɴS+ +ݦQؠđ7-$Wo!:? ^ CD)q._G}`p3^wk%Ҡgfa[ mW>t.Y?č$C]BaDx-0yp+ א gEgMb>Oi~Y (-rnԧ&CXbFPTZD=:*w Ѥ}Qi +Vk4l7U !l4a)IRv)N%M^% LC}]Iw9tɣaFc2#<ޖ26MCju+f(ZPo)a-J¿8,OKZ!|єSu(.e_Oh7XpTk 0ĪR0vWPih"0+5"zǝ4jWo< #cjN ǮP#d4! M/yHL_P9)eˋrYo[7h:qt%rѲbN^GG켴T f&- zMC:3giN51ZQaz1 rn ,1_RKCg' .15ISvX:cmVĒOȐ)bd!4F}GOO/^@N(TKAW6Ĭ};eig=|ӟonWL zsm jJ IQV2 4ғO_>׮mmý=]+@xn!".ImqwO4% SӶ7) rSʦp3PJެVt+Kp7jFF؆ɩ B1J4p E[2hը6$0%-׮T dHgݓ#\wM p"JQؠaeP ?g̟n2 #^>kWm p@SEJ `ʣ&*ʀ+Jڎ/n# 5:3 Jʠgl+Q*B, H@mkcB SbCőف?f~m= i(4&I5I9˿bP\^B2A[;;;[iU!SBYh>xMs'&W`$"YOPtDDw@6m[ץm7qY偶>K@G;߳γYdBהiRflHk_~^^.롸!J 3KNu'#sT>e{ SWeԟZldGy(jc2fXV!_SPIn\߆0nAw^~q8Vmf44f]o1XLh(67O da%FZ8Q3t֖5 06k79|Q͌ ֘FɅKL]ť]LBi8DsNVôn 4 a;5*_/'1 pӑȏ,뵄 ?c’ΉWΡC.}$2I9cw`DlOm /w=DyoE4\_k5p.dL!0yǥ(:܁6ŵAVOB?x N@|WBtϪeO U:`z< +L2p=Ѐx6bfkE7dq\V|-k=#b(jeٞhcBPYDXJM3ɸu6l'hFWƛ9e^ڭbcU~Cㅂ[>+a%Vo sE-+nA2iA9UDRѽ);{p-xAݳ,$kaASxIuhQJsj)H*ຊe"v>k&|-n"DYj(KO0Y^74) B"=LcM L 7$l*@\) }hY:P~ Z౭]{uplFa!$2,=&*^a\y3̖g_WhÜ-S󙱑0Fp9iXp<{?l.Nx[ p {_ۘ#^A&h=8HG+/H-^uH]a@UySTߋqe1UOXc\{{5 4% x" cRRHN"rIÒ"{/_}(7ߢabNaAn`1F$CWlnn\*9KML'kbw~1 pYy V Y- nݾ@%4_~(67lj[U p! 3zw?#Thp?ݻʂ"H M=,z VDrn`CؐRZW|¹Bgk1w͛QZ.d~u,0F8`2C)jw2֠8f -z ! $lct+FsXq>Rad\bzk&cΣ4#(TkhPzf22c#UtiędP}yQap b$L:漴wݒ&HhJKZ dHL<㧚X38ſkƲN&өr+%]~~,  䋸5keDPA~,e֛:NiP`e%Pq*ґ-k$48>j ~FlMp6WdM=LNM!lQqtz- #oxjrTC٨1/H"H;yr]W(Fp6HD{S;%Vccr@93IF<2 ?bS lQEFiċo:ŽvѤLw=ItJ!6hݛ`^:0\1yA 4@CKc_x[E# <_uHzcҒŀ ֢W )ё'Q sB(xEFE3)pGy3ݘ~l#VhrkYeRE-*K,ې,EUI( 䎉FJ.m0Wo|%eBZ^`h$wѳ0V9냕 h&{xm>ȯ#t-dUW]yzaR ք#y `[`!pBCNfIY͈]"FAN($XwHm`5Az M V`(MJ'XMH)lm։K+@W8<Θxq uTaJ:՜7񌌧6E/t*v H.NA]/>}Bj@Q!F BD`sbZbq ~`Ў x+9 7/j(r%/xǽ}(Oڹn_/_c`J*A|cT8zp@ƀͥi|AV[ye'=O>wކ %edi" hJEtL|n(C$3LtIM r(ZAyE&9;5z B~818M2CZЯT0gLpek60fYj{4.A aqfG]#X(, r$۷l ,U2ˍ7Y{ɸ.qLdS~ ;2c vbīg胙W0^Y(V{rҕwoBLd~QuqF&-`6dxf Ү$qt_ᆚRfm6lY:&5Vpys22-H3$LK5^ފ&EcD:d(BI}#@3{*؟oPP-g6b!P|gYƽϰ:@LƘh΀6Z¿<mѕ#|: nyDycc#מ \d~ 7L`/_k'> x0Q5k~>IUw:?S(2':6eSf${cΖٯ rpCx&0Z*K7ySM3,eph@І Z|y }i*ǻ8:1'Bb0H18"DV unIVa\՚a1Q6l֊a ҡp*658x[LLmE !8K"#Nb_μî0,*E%xd;ƛBïf{xC@A!r' %{GIxbpr zėJ[/E[ VJUD4AU$ WϔPS53*]whodv=Խt8xvs1I:aSees+díE8^!ܰœg(J/"\_|1p*!Fh%gī` 'ap2iuwxc?nEM"a-8+Pj=~Dž\@' 3:m,R}|dw8rflWɺxpO~޿{cr 6e0'.7c K3P=2QS0j'i XHTKa#et cf'gHZu0 p(Bl7nYCZS(ޓo`o+ KX95%H_s}zi!(+(5<|xHB:LygY :K VlHI|wՋa [;^W8 { ͚> 1@qJ~H1'YHӷeP38G%nJw(WFHٳO(3UI%at3}4+ws) 8&nl]+>y064>/&; a{^I#a쬘R,ONhhRe7 &HP,|/><ZO Vr`ji;+ -%|Vp9+# :-vFJOFЫKW,@mO S{3y`uL;h@lbEa9{³k,52 pC&hFO%4UYBjێTmP{/-1G)0Ռ <+C9M49 pG WIIN7^e$Lj" l]$7H"宵 ߡC~:2‰x<|mW6AM&ci`!a352O[>p-eOz Ś!F{qy=plCnI>Q/XT!ʏBOdBGugL*Z".R2`bmYR52s4|/b\ӧ{/^"7tڏi’""ᵾ:Ϟ>CeŭΊ9ȹ@ hy̞r[#q2'u }欱*.2+i|K[COssg|0:ppsahpbvhr8a)ݚrV RC7,>莡8fY`6nO='P#|s>fsE^%>'颮f+*UjS{zboC]\Q*h'A|I]v,7:s+x52')eDvfYsj5 ̔[(Q\"frhY9};6r؃9i(bGQpViD SQ7c~*XTQzT7Ck``IjxcB8d8[^Ñyg`QT)DrH~/)a҈Za_F5sM\`jW[ЅpQӪŇ< J$+G j6/K Q#/f5k'+yܻzeV|!n8X+@6lyBo|DNLa ,c^W/^}>ݸ~(EbTC?EhPZ9"5?_!oܻuÇ;׮p7ut[UL)p7} 7])W<Ɵ]BޫnwE$ MŠTJpu 76PJc}.&7PAǵHih0\ql\pDH,YMaQ|- ]*%ڛY׹=-)|{pewX'ӯûI|dM`a\ٿϟ U]9$D(]:(A}p8kT~x 5ulsF.8pjLPR.qRBPx TO6ɿ퓷H aHX (pj @S!2w_+/B );ۛd큿z t͕ׯ""#鞨8)/$+.4rIaY A&8Ru 8W0dpX+qj$+s ̲zݝ;U7aǍ8 g̀?֙R&WfոY&R1@ Ł&8tF yn.5$Pjړ;w{1u#-/K3ԧj7yF;d 90W]Xš ,Q 'E#8*P s% ^;Y*rK5qFb`Pc%%՚V>#r,zJJ]:AN] r-`ШHU|B%s],ӯpP\ 2)w>gh8~}@H *ȫ,GޅFh/=Ïhzzn{cu{ kWQ[`?޹ (GKwËBL^![z\?ډSZ5ODmՆ &Fgoq 877) ߜl*Cȃv5`O!kII7U;GcLw R#OK\MՇqSˣ4t YMuW~ ]v)KF@,y|iK <FoGthL9ҽ mz 6{̰\ w5bc xzG^K1{4Gjy6<(zoD腳 |'k.[M"6 &5Q1`=şQ"ژZoOl mzíI'+k~ciFnY`!]mOCTZlZ6EX $Z;]{lJqe;Z>F /Z#ire?-O^f9/^=g9v@R訸 $nmH- `͋2y :* \-lI|4`m5X?ǧbX %s;# ~@eh>}S ֚@OeHF\,n?)+rL9"8Cc__[Q WKQ ~7m6 KJ [6CPs=S\eh&u^=#ЍQڿt-Tvb3X^Q+&o{'>i5¢٩  Įd^_|rKGO={7XgHևtKFZ)cˈG` 9nB]^_]Fhc;ˎLfR;? MSrg6xgߢf+{"\D8Cq'kTJz;M:LY<$O02< G[6(- p%X8 –!M%]L ;> PA,-A]zUL(P]'6!'*ڼ, dEHgp2x":*ojLAbe_h+O'#081w=waQl)1O"Th&`CŅK8 V؂X(ٸsym^Ʊ)~eEnP]Qn ԢOcFddH-*\ t} z蓄˱9'b,61 ~TQjN< c't$$e>e;t7w}= SK']@wUx {dqg("՘TKD[ ,-?Liru1Awup'tf)3<4N'r)IB޺[,}fi }.V~DKǢ`4H K;^tU46r@j%@,̳F[:ƈ}Q3N;]!a.#xزh]2sHR.feT\&+w3N0y^imE./(Pƫ/S[g|,XVܣ}B[>fն:g~C„:tȱxU}R.-+}xYCw>IB9({=c!'nx俷wsJ:7dGG|vB5*&4g[o˳T&:'5Buk {̸*ZRƖ9`tn/*!'-y9?B}od"CoHp,&ZʁAɲ {VB=92rj0Zm8q9Ԇξ]-!I lvhjL#` 6 δJlaq\9`7Dp%7/%/BD`FAHi98t+0BKFI<+4w^ w'ք;iY${Ҩ bjҺiiFΡT.<whCk^R=]AafÈH*ۢz(Bd`N9Fo=ab wȀ*gPeAT>K dZ (kZ+pUM[c%$ZH?'H`jf e>js&џN*6oȰVc맳!"dui*mc.VTPhO'?6-p{a?Ul_1$֥rYBBz=rO$o[sw'~S<,lBsGw ~2߲d'ɵZ(D-j|taopjcw t4da(k}8UrUԷJu0~)4%{Km7N=]Bfd 縨[Q)6 3!jnx6JEy̏VzWU`b#$bZs7QlHfk苎հEТ'A)]J갎frZA?z#,q OPzިd)"NTC6@LpvU#S#D{Q-F=~'Y*D&ϰk 6KsN  X{+66`zG`}bOڍ7v]CQOA[Ť K燐j Ĕ\wr޽?{WVq E5EAp?nN|M85XYwD8ы۬ Td`r|!K4"7Ga=UF&2H u@'dU#osr̵g2p^&﹛NER*h҄I[1El vCk>bͩHbPF5ރwՐ<ƀ8OHr0QKUJDzU,W) /s3GOD@TeHi0%0@sw" bd] Yp*TT _22L

    u2~;2v R#ijDJdՌp<4kHQ6A=D8 IGCaHDpk9B'TCr->rU\Pz `.>x:!מbFOt'^ ~ijnW=&`xg*'Ed(߄ #TsA1Ѐ2XW/ܣ.gljHZj%J"Ó5Qhw@ElǏxDgA"TD}ʛ jX*FHwa3l.b ;"G7qdBFz1V|46KGEjλrt-ѦQS#P bؓ[Smt9$5bz3OȡD c2ONZ(D,\>/uO/@HOH,Q),_܍of(M=}`ڸ"_&ޝ jB{qK-96}۞CLF7@f4F-鈘-qfb=Ad^iџqVhگ]HxySu@P̗IcJQ$S_\N: |qmtQՏDyB*>86d2% * 'k^G *HHr@C9hIÅuTutF+$TrYĬ.Ģ4&vldppusitɅq̝K@l ȔКHNmJAVꖎ+ ŀ[5v+K-Ɛ%ZL# Ӝ(.26,N.I8h*< PG\FPq!! *j%GRV{s6mkf8!RfEI)Ƹ=Bv[ j,5%vay U} GL{[`K͠WAn .8(dy<)u#,C ["stylTP.b2yzyT$ě3'r ZQ!xsV`1jJ#烳=.Ps  OzGUOtx&vAD^vBCj$;B'ҲY)s1O|/ҴzUpR> lo C4%#w؂3i<.qa8L(?(2USM@|E!aUdP}`>}_{l?:>FHhp l' ўRrt2l\&jqfZP$sfc<*I)^PND8Rtv Y8 8ClHR 4H+H9!R":J@]60oZwR!(5o H.v=EÎ QNa ]RqPۋHx }}#'  2^D+.m[_`7??ы/yko^>:9i):N XB?| EFB*x!Jfj G(?ɓ/ "A ⴁq;f`(,,\Ag"MN-7]%e/HG*yh%&:5@VKEq|XjE ٲ#[БdzR")Hԋa8k0OI9o7r nU0P1YoVawхvܸv-ݿ ً'O}_}e6n(u*D_'P+bF2"ERA2RXهUtt"/OZ)øAErj$u䬁ÔΎtٔ+X+) YS"D_E&+FןL֒E95O6lk:ȚզFu tS-IAiaw|6A 6&P>hZ/]Q %YʀKFWu{( Y8nMedhTSly,"mվU`pRc {@n) 1Z_4ԍQ0Xe[o6 ɇ}V:mdl@zG0@Ӥ'L3c1r2cl0}ҎK#AM>b|k /b1vLҽ votF6 aN⧊ T'z,!w=aT -Eɕ{AQF4[pFOxN3:,j;}6Tl:m'nu qc%=Sl9r4o .ܤ4ּ]+ܝ}& $jO~#tV+"'Zn'/jf2ۙ5అF*ȨʬHG[KB1Q.7yA?ֶ!tJEXK*fjW)lyu_e里U[ނ!u `P'm2b@nf2Pae1ob} $Cpc Y@,>Krx]1CP@ւ 8#:A!h ^gQO ʲVke*1_N,n4I| UKHnx^·bl3%zCT:r;5||9b/qիw޽wÇ|ǟ'O>?G}<~:ZxHp/v#&6w{^.-׋ꒉ|'|3.V~s|spPF}ǒ][tfBiΟ{ YW,6%)Ђ2)JT SyN"(\ I.I񞙩Y;S}~Gk? 58uq\͛HSMXt>[.wLO\~]7nQ4Gko9x w m2ElˡeGdH1i~c'o# %>vj02X ց2ݼ=8c%F~"e:-vd]ͶO@8Pjl~W՝ʊ_)Nch'^vZ/(j4օ=덀8Ep8WUڦ҄qݰㆰSVmv,=є |xBl0k;j^vQ#,SUb*dZʝqM#ϳ15 M+5 Dt EgҶ=uX;;b} -;$Vk <=ؔ%¶8EliF`j J@%}b \ҿ Kki!w o*_:)cDbZRT<%wyt?dsY:N }" L'B(n-]23lrI-Y',= $6wRsiڸe.r p?g /py?j"޷|>g_<~,+l ~Mbd}#i <2|ksw|L~GuܷNrd#9JIOr &ANdE9']B'g|thI.W1*_(V!Ʌ)FMB͌ppd|pO_>1jՇr7BmMns@4Vfay[YF\o㖂oQ[A^DMc>1j(LJ76n|RYWVdAE/54*Pƒ7q6Lh(LmO#3;5XKAL[Qg94BXM&60uw5ޙmyheγL [dJ8&BY w0'Q5C8|ڈn֭2D F $` )B'!a5 8=%?2 Zn[7~l8tdzvTԚMcE+/gՉPfW]7&lL,Z ޑ1gl2eQ-|4Ȱg>`-q!$;gu6aw7L8B]H6:^0_u)'cf.<ME7)Aˁ.(CS,&S~]*Gе%#0O7`JOXkeUƨZL_Q~DDѦPM"HA`?QY4)46l޸%k KӚq!=^ByQ:5H0%|`Le4IZ.S2cTsb4,FK^ F=0|e)KXƛ8Ѡ *"9$}:'mOY|PXblKZn%ãǂ[|x~ǟ~O>"l8:do&Ŕ^;'N̞t=Mo-"Uk&<ǎ@TOQj$kp%$^6S,N䳊]w p9Zc r@\(TT%f򳺄!̋hw@g%$W.T0Wj09oY B%Hp. 8PY5`KqI\@|a{c6w67xPS"}a L,ZQQ7nݻwƵ 1I`hW@!e +8${}t}!w et`ĵ*&po30"*atȢm!ҳ\[4̖(f43w8((bdW_.K8߷֔`ENa CfI~#pt腚F'LӶXUSXFd/'SxM ZjmZV(Vc(lbg>TwƬjڃ#yр9AmZ?[E'ߢ/C]٠Q(skqr GLҠ,޲ú( ]3-{rή-˼8n9>{7؂FBel545`Dtg:.Bȿky# GH~6rzXv %yl-`oL;2 ~qLyƃiH4dK;'Å4szq>D2#ZG3߹w 2颼M쫿&ӼX3Z} b>Ҩ~5v exzn ۱]P(,;2ySsg}h^`$5 98M:2Bk&+vutYWWXݣz%$hE^.:blTaQWnTP\|\UeptB| Ոt1-#e`ʄs'HbH|M(#!?g:`:?M">M|  ӆ~lq:ۼEؐ .C1q1B@e]R(jk2:MS-|_Etz(Oꫯo߼{?|᧟|ɇrέ[Xlw%y ph*Ńx kMʂȁ*L\9\pxd lM7W5Cp@Y޼.Q(H ڌG_pc1AH:7ƒr4z7ciWG@1c{{2򠲁4Yt]kR]<fU l"8>(49c4#hoQf]-}-$onn(%7P\_Gj&p@wous@87tbZk:@4~d샧O"y< X}' AubkʻL{̑"i/.@)ay0=-橙jX3v[0t4 LjSz0ŅʑZ!6PxUijBi8Qb'PSTU@t9}G歛x!{9Ě) Iia"vvud?|Ƿo~?<9>: h{PQq̬by 3Mh )&DlDun&X֤`4*|-`=:5 IF[vur,'uP4z-JbRQJfAS:^v3dQ蓝6aQǔ|$&dcsb.o(>S}FσiD75zZ d<TDݭ}4n hp=7#IWfQj(T kl'-RRL*F2\6} p;[T\ k\(J 8?1LS7 S}+ Qp ۚy;t#Ϥ%0O60! x 8{\ Ū_Sv.[Bjޗ0 PcD϶pIdZi?Ĝ܍?ٿ8@e" B'a˵>JhmS$."YAdї_Bm"Ge!F7hK(.U08`6&ґ5<°bq&u!i*Ą3P \q%3[B( Ѧ0O4sMFNV&e1vTȣ<F\6NM&~&W?(_\ʠ<1gdBD}.Vh~Mre2>{6:InHbHkanl dLp&}P,7 &',2!=wbUw %Cx6`\cbڥW|8zpyN3}<w4 [~''?yxk6ݮ\`tskDG֝X|b~ך,pY!- 3aCXa*CA?;Z+X_;*XFffvK€u#xT6GLw :}fIE9J\Y8Ǿ/AM|4dVKP։RlqǪ:sE*4fCm/Bi؂kI[.e%<ֻ QI*"0YfX}*^ lg ‘(X辄uL m+=?9Q Igx3|0=[W&fx,bHcQVFIJָ$YP泱9 o2?ۇ$d2wxjӒ冐 3Չ(-\k:ɲi (+ۨ_dȱhj&4YkPe=KvMi\>LhȄ?Pv g%{E#V5wXj +FOj"/_D3{?৿cK*N@);U<SxԀWڕ+W`Kwr|/^ Y2-OvE˺HsN?G;\ҖIV?D >R{UGo12qv4 (+R&:AQ7rԖ]}9r3E ӳj%Z?ч/8x~ XxCj#oi%^ XrpegH RrL,MԩhWPqcsc}u[3**zB'!K`4LdrcV4Qɿtjd"+ 慿%VfV8æ,XVj{?ITjAO9Rdw Q;5[c8X\6dS[Ԩ5/)5MJ1"I n,cv5IKW4O{RGٚ\Xzt|A &e*f*HhZ"Ƹckc>q?4ifkxl-J)Qs} -a^E5o~nS8*$ DԚЂ hLЂbӓv6خ+ݦW-&h5OtX<ӹ̼ 1?*92i‚DbVS0˽#lI{5f֗I8!.c:Qj]i tc+Iܙݻ˹Níys  8]ujq=aw \ˎ,V[slݎn'D%q?|X vji߆j{Cn)Υ痠GYFgCtk_i^èۀtr'ʻfQ3=6 ̤bņp, Et2_%=m%:@|TXiE׏<Dy sPh[b]YgTz szQ(('9Zȩ͘ |1OoYE>TtmE><о[٭^MIgk تSA"nRh> Yd 9AJFÎB|4%  ]\d.cAU qC0\vgB3*H|h3+Ϟll}`pb$ Dv;6`wq8!@B%pέ?xx9CT"d&!8ܹ{_@&JujH}A"Ƹ5SO3ŊBCx/_okFE//nS*u.Daem[ǰ sS:v˞;b~qc( Hk6L=k"T׸2gU9o"@[LT1լb?j ŲJp_6M`̒o4H}so|Oä%z!hm̍F6̨Q;*N[[/%u760k5[ɐ'Yz{0y&?mjGቫ $u^Yo1uB!ёҒb(JQ- f)tӄ3f̝dn`#V]լRy hpJ"RQKBZȯWXQXcp i?J McMOJYK oD~ I# Q&nԺRCY"0cnPi 떎V{h¿KYa66iqKƏ0 ۄ~3ʃ ;oivj +:kQ'tj]TU-£bHE[:jE z6𐳯Ƚ*/ lfeXhQEK4]pAaq,h=<|}Wǧ8ezB9Y'([qf|*!QxFmˉ{4N# X.2͐djd ݔ9Йj4cԣ5AFML5,ӗ%v %ɅhSSFZ\H uЦI_I"A^432gle"txR&̈́Lbգ-o˾[vi1fd<4sYBŮmipTnl2ӼNS1ezSM7U8h-Od <6d=ja9G{2@v9+^x">:9Z*\ @QGfeDE.PO~@>67`"kOx$P+82|aWwo޺^}MO12d ЂZ'zbyr(c>dS"%HnhsٔjqK\s`Zr&H7նq9{4FWډ"jLdw;pŅ1 3fNxǀ[SW\SZchU/E^!pB`'m@E4'oP Ry^9xŋݓcEpQA8q_ `ED6 ) o`WH|b U_E WA.I/ Ƃ$|wPNn ,i."$^enQO51%iT|+|#}q\A\~3XVP*Bp;k`ë͌v"ArA [H\_sbEp#~stthWո[T`itzO9CI-Dnࡃ9Nm.d̮%m8sW,(\ ݺ0?iJ,I:FDNhPb}OeWB`m>b9npcz0 D񛵓4`pX8(t\jЊ LC> ^=/Z|7V3_ +϶}e?O,JA:$c/v0KЉoxb"(c=ݷ1uYjdtڥ2뺒gƨsk`f@PLǫc%hf&. 6|AˈfQYzv Fu:?U3ýn&PrαYWڈ=hOnɯm`fgq,6Ag6Da ߔP?)^KO{Kfr *OD?Fo%Yml#8HH=HGEs_9C;(1KZ/OKdofq䓮j\c kOk٘ed)R\: MКkk3齫3D6^E$ rm֚MT)s)ϙBWi|8^4EnY`$ wp@$- ,*{1x8X]|N4,9ҧ rS/g_c#V70a+-$1Nz=\ $Xހs!|(Z󐼬% &A!'U/.^\ e9i(y^cPDծ 5e*;%{*Ck_jH/Ҧ7r1%v#I&?=ȟ/<(s**J=X|JNS7S( ڢ.Wpb8$!k‰$ǯOpc) Jy5eX eUYfOȕsL~rĐ*zD$%-ѝMཽWO_+)@]|ު F1 C\as5L^aV( qt-BZyw}3x cxjޒL6 <*J:c]݁wX;X[W @w _NjoqZ%~A(Hj@ *5X/eJ,¬A)415J8e (8NwSIfa\%%-A3ӆW#'U(@EWՙA]KU%v.O6b;٘󿧠>$ԣkW_4O<6X{G^+Fz׌< ҍ#gk5,e {d4}U$+S._HCa=@a봯LJfUaʻjqiUR9ꕁ1YIgFR[Oݗ܄dVpNm4Kݸ*m5pGR)r!5x%k;s=Hi$/7fƚ43ܶb2 lU%w4ר:02DX[ nMSnz`/<`]*f)H? /SMuJ*hKYtk ⦎+Aӑ?u^JK#rԐdhKVjEjɌ֐.n+Ġ^zj'5GP<߸}ra.hv~/Weu?at w , eCJg᱒!cCGUg-/ " &}>&N0Md3METN;Kt}4T8NguR0R@SO A ,31/8})3u:AzF|4={_| 9~} PA:j#*L/2GGpB gTº%7IDATGW9ʁS}j_-@ɻW/=}=l9 /DDbț ئhm#}7zOLE2}P,](#oF׎(=89=W6I sˆFnsĚؙNUS` 2qàBS1Z9EAČe4qIƵ !Tgz W<--:#Mϴ<0khpY7dfz6IWU,ϭj-uO.iy.)hA|T6R7փp%҆BP莄PJ?sLzR5K->ށ\n=E_ F{zxhFuY78GdOW 4*-/Vh8%I"^SX+hϱll:450o5RMM{1l*12k ZHm ]%TC}Y(rt/e8o 4lf~yJ ?8Qz9̫-+Q' ReZ[6DWziY Hڸd:R g$,QlY7aQG X{ph Yo.$ʠ5i>7YBJԳhf_&wAh $w n?l{0g6vq:luޫOPzsmsss}s h^qeu jD1%}(l_۹sױL+>!?GZl!{qTELp4Fpqk{ p۳ׯ N?X_*0=a,j%<.{9N[X]lownHXn*,-$k1쬈 9y^d8xqSDN@,O=Fho0?˯oo?W_|/,B g^|SO24  a@(nX_FTbׯ2AdTvRfP׃G|"Dp'#q`Z$5pbу@ͻp EpO SqWqʳ@&j!=:8 ,͓'?V?!Xj =;kҜ1!3.GJi~~Yf"L]<>Oiޡ !YrшIYu{4Q>gyW׏_o^x&׊Q+1ӳWGgǧoV7wvo8IWӂ%`$3fqt#lܺs%$e$V { lU W]8~} *ۢ"lINsH`#x%[Wxp%| s:KQkk<DZeŋ[ X.^U#VFH:Hq>1z rCʴ%N'E![n@ & I/$:\b05B[ʎq 't#r _ur`9]8bMGiR3SioЪIeDx'0q g}g|})@voܼ#'f2Uz /Ai^qWs,,~Ph7| XH J_ڜ.0CH&ӓ1(!*`GBxf+HC y8Hsg,&?•ݓbuE"Έm6`Ӑr!8@ 6! b^_xmg:uP5Q)ǧQY$30& #r1&,X3Eyٝ \?پxM҉|w>5wȩlm )S1b }88pz%uF!5dk6_&hS7y24[a@w-ars: შa!hFξIu,Gj*0?bڨk+$HEӾ4f03YD[EշlZ6F ZRX+0Ў&GĂ+^F̐R6XsF9qr|cܨ9sCd$F=$DJH%GL"D({i"N7cguipp`½ nUo}BGg|>ړBdޢ`0'm6HuX-1顛J"3=Gutlp㹁k`Ԅ]5 Z,#aZL7$͖ 1|Ya[4+m?_&0iüOɠ& \H:2$Z )mգ,8e}%KL69=K5JrM2>ц!p WQf۔GBL1̞P aӦDBA@gNyn'WkلO :{ɘKR1Xāǃ[^t` >\I>A 0;7n hE'+lHLT9𽕉Y%+ $xH_#.!.ۃ}n<9:-RtiĸDPXIj2Rt E @ggR$EVHb;8WJ@#\!EoXt=8:B˜߀<b%Z2uksuxZKhQE[ JxqYccUi3y.Jj {쫞au i^8TVķ*)TFZ _m)t<ĝrx=1wYrZXsP 0Gs_3 5_aCCEy/0 O5ه[t!a?<hSv7۸y}Ux2mu4e1 + EpBjm Ãei6q;-j83.u<%rnVE3?+GeZp}kWxdUe>ߠ v;b,t3T'UpYkX?*R GN#RxBac1{]!9OaSaj,KbD"851q>']e`Ǽkx;c l]YGŕ++8ƍ8D΃7\ჯH@3p8 1G,b{cA)n2H0xsҩqA{vcm-axHuן[7ݺqʥ s5WWAp!U-F@+}xM,CV5LPV5oH[6B5e6zMRj.Cҡdu)7d dgM$4 dB4\NbTO&4Cym6lKfRKلk\lIg=B/t&z(2LV786A:Y̢WgH͔ـ8߃qn+8n+GqiӝKxkfZV'P+shsZMáFlY.<ȁ8{-Cex;DN؁['ܞJF4iajZ=߰+b GXƢPam~'c\YkVɛSh7SX\. &whD gJ͵ͨr0xNrDP"nxEXHo'q}"PNj>2-$xd6o-_GLHP^a{{s}]z}ܺ}w?|'䓏?W?c5WA2\Xrm8Hc ~ c`#S\Xz 7m\dAH֘`;ug߮ml }ccGXI(k(rɠނ5vY'2Kt&~ @x͛>w 9o?ooW~W~W~?՟~|G};9<'~>򸱹)ťpZjuO*Zؔ:Z.k%{4_;`^(.2[[(<'hNܾ:+<4 Ð$[{q*OHu ^}JrQOxYk$%j)hDMe;SJx E$+P\ 8;]Ki%k~eή$`;ZQ‹M9֊> ulDLWRku?zmϧgX?SQSu?x92ql `ի7n ЉN0dF-2 MB SyǮ' H?G>x dWh;١sYK(9?sPU<א(쇬Ttvu߾u~xo={zx F>}vs.Q#E;ָyktL%0?qV)]<&[| .EdbaYR  ³| YqƣG4:W ;0dEb__Edh?si wo߾y֭7߽s nBi8+}7[LyBsE-I"JcIt慪D8xh͑QEKX#G),!i xݬ2NG`N;z"C\ -N8ǁ7 ? cTYa2hyh @綐ڠ O'rÊ"w^mr8@3=bf:^tk׮߽{y6e 'nmT Af_E5HrgV 黃78;W95</ >jskcRlyܷdm:o=ߦf1s30)_¶z8Ifarܱ̈́yxY>_gǂk,>Lwj[S%=x2˺)@4ϐW&iE g2??k-57F[wqzOͯdtZ5FnCas 燆2hMg` kl>9G*ZV#ȷF89)UՖ-PTo^|$isҧsA .G0jk&|V˙X0"]b4]XS'塬Q3A{t`*./,%M ia^LH'\֛ul8SC4I˿ٗ&mm-ca4ηL`<𦣛%|o˳!,f. dgC=h>vNq52c\w^ !^1--0Š#`οqfȧҏ\t/cVk'7 옞s݄Yr* 3r5]=] U2ü BS=/^VY0hn0L+w4y'$!Lzd65]!4Ұw6޻^o1AxiEx>؃JZץ GƊWא]$kL 8.>}xV:ϐ319HpP>ְq[Y*V+Y4W|#:yOyp yxo}Q Q8zpx#*j8{U+@aZ*kkti ȌlzaTORn09 AE0ak]KtiuH!mG[;CFu Fty;_T,!b^K~3"ayp]K /!|QLB?XёX&^[XCM+(Vpw}7]$N 7"8=>tʭ<-B:P^JJFpPk;(bLRyvkDn%E[cP?HGs1 M _`p #Qދ3Б (\0^rΝ?=0?#H\U4!وhxe%ւb/8=2⇚)Bf˦R^b:vUôEE:ZN b&éSW~?8ARGp!@)_qz |rn 5PPQ?QP[Nxh󄹪&.#ǙgIĎYeÅHSIT3"5H>^h0~쩉;ȈVlMaTu/0e@%08T1ee va"a&y8N07X(Àq59Ҵd]Q=3l!r.eK빰yD4jJt8}EXZ{dƟ>E YP]4TO*yhk7"c%}Xke @מ)pZB5iFq~H7tNQJF}Hh>K:T(eM6. CrX*+-Đ0GYP͛ZHm:V`F<{ L8 Wڧ ˇg{n߹k(pWn`yXDN2\# lqĒpHPH<teͩ`;h _.kWz -Hz67c> 'WebU6*F|x-uP+~DmdvV@dl8ܡ\2\zdS jsV>,8[;?]R.VJ|bu{&o\U{$qe~(a2`IhXS5pt/˓w`@݌wpgQS!PQ(x CȴcA^.=ލWqC,A X] @L :^}' 4@A(bԬ-IE _m(=if:k'U_,#}3[%%Ӂ'xQŋj|Q$`6f$&D5)o (̌(2jFԓVFR{e tߺCZLMFeg:!Ck"ÈrOul[ i!uw mQ@SqF&ѤRXx1{s_&94Y5ѰT5 Ri'pHA mm铭pCQ䳂rL3mM76H߻c֐''uW(ƛ"g3kQ{ynHi_"xGemu$CcdDRM:&=H=l:!U 'l4qTɨo4ČXFWm4ph[ ͽQI[^^<}k1=o<(3sEYsmǴ(u̝NJ{(҆Ni3LaUVo!^ YR7>zqb";lE/G%^ #WI&?fKa(8E^%VÀ֒(ų8tj4`, yCFûWH[]W^CϱxuW7X!G:}|:\eFe vU`xDUIQinx/?C?7UKج!<% ϰsc{ X&A:NHxyݥ+o\DFe :g@!t6 9G\}EuLЄ&1/(o3) albM[&j$aƬ 6K9/\dF5,U6 ¿L⿊0(.pd#?~z\ಳ"'DWFD6q@J?`Bey:@O׶֯l1ҥO>ut[Wm4ҵKPqe՜!q'[; ;0Aa#^kwJ $.m`Ԫ۷n}tދݧO 'n3n~$Ȏ`Pϟ>id'0 rƵ?pQ?Yq)dĚDYg?dp<ܤ 骒 >q p^S >o@LIOv_Ħ(Hb#VdHiH`RF4Ck2ŭue :o",3T[cJ> ~Xa-orX r&4!Q4_qR*~ܢ^*4 Gu?,;F?ϗs. 41aZxI Q,7%)PB1i;hj4˷6 w;eй0'qKLHښe2LUTazM^`isQiˀ|uc@imӬ"Vpwclh:a}q  5vn-W8ɀt?C@gz`RR2{ZgLa N^^9ʀ>D>@37'³C1fuHE(JXе9'U䂾/40})vtD5c䓅G'ǀql0bC(dVRWP bX {AHn]]]~so_5tpW$crŚLҖBw:2^#!rϴQBdWhJ*k Js7-3H ˳7ީ ittit=g"K\ J$YK/݄= qIivO,Q{5Ons9jf}r&˼@эvBx^^r4QS1lz^9 &49-d &Ea-[ij5Z٪~\EyaX-5m#1Oo+P̔zZZ=iwI/=Ն?pD=7ͧc2DM,'@ JmA}#iQ"z-iehm|@ҹ^b*-|^ifxeku5gFB-% Kxb·³<nml dM[xWhMJ%`.fdIFG#bVL++s¢"1‹0;nUމ^WHE I-fOKpw^Xky]SԷՕп|f"܌">;GS8EPUիM7Jt8⥬JD/Wzöo!:Z]5zh|՘14x=kyi:B&#[wݵL&*Fi<ҟmf#F ys:zZqJ lN?B;/[\u\h7~ j{ܞ/Oӫ(ɘUz׺ٚd P2iW&.CESh!5[/p Hۏ+@yl܌ p-\_ch| ^/Jq>Q$@Խ RK.ൡWw޼=]pG`f<{Qw7Tۉ&e$6!U!e ?ҋNڌ3- Q6uAevO*)%[BDIA +?$G=CwH4Gآ/+%ː֔ߊ#j#@ P*2)530 L uG*aay|f;nn/IvK4GAԵgVX -G+;sd7?yk<4 B^5R0kSO=j.ao$p%n3o[>GWCY^*BrnշML3 Q36yB/vf'̙)KqUǿO!ռV*R̡X@T8 ؙ>~Tiwq:a9VH5\}XcqN j9: 8ONqOϰM; 'Q0]~̽;y-A䈵k3J'gg\]>;=B/!լ{@i@"ná=oQ l{#a⠾Vfrz@ĉsQ$8l"#C^rrp~6.k2|f[+tVgůg?՗_obP~ĝD֐ӁIcڼfEV ?z#|W_bo~cdX z{wG:p"D r|{hs»C%)b-/^(t($gC?ӟ.'G =pY` ^Η;P-ptx#UQ\tJhzy)7Ȕf(;#_kvOpgd֣nC7U[3n:.yL¸!qtW5 7wOB [f+Vf2vnF\SH >Rf]0 ^] ?TRJW ?"(>pUo…^q!rKk$F`QT#5cnrPf 5Evz%`ͪYd;QKg&Fh]涞= VI*5̠X)M $/4wc!ߺߛ^ĊV)dBp@lkxi03^:dXscKCmĵ ln X6YЬjM0'Pzi6PXjA(#Yϖ:<79B2ä3'z,)s*D6溱_LcZ(]a"yPo͊=UUGLL. [nD˒ Lm ײ0].>Gezܨt{lAm6FSbօBJ64 :!1p|`4WFV9D8GYJ硚Z@:(bR95Ma]r JC-^X0c&a)p5P5Wp;=puJ\"=dݡ*{2&6Գo_2`b?)颸w )tf/x]1m5]o1D0/D(?{-UnW}%s݂w'e=6coZQpU)0< sC~]T]T 00%`”e[R5 w"n9;)6ELx`賓cYsxxqvqtx曗HpBώOi:Gt~_]9#HJ+qN/9;E׎w6=bczD{HrQ66݀0Rql{܄wBChC Tbqi#V%)JDzQʂJZnţw)Xg6A6~a]8Ps .\4i;^>BZE-H O)&LBi?^# 1bީ72 -rI{f|^g͆u 8o@0<6dl"st}C%4в 泼t{rʘ|,01쮺~44>ebaK!S2m6 tF[BI͇#zt;ZP?Zìĭ-;vJ6jg< j8`ƏYjAMܹY Õgr,j%AL?Y7ij&ĘFs3;/u]z=CEBvނZD7 vh-/5DB;3)=Jdnw1s \6p<HMoJ/_Uw,HCﱏQ{E0aKPߞYA.e8Qh'LY#+.f1jr~4g!sQȫUoSoFےh/-= 'Fe4tjMM*|bUbIs&{vU@MQrOm$2=0?1774'$&nӪb5덎Hh c\fI',xWSib{ QuUZ3Ɏ`q- 9Y?yE̡e(,Ocgyxb0j]U{)O1#fRpKQd֍ѨW0.f`s²]i~=},ђi3b̈uRYY"{ذ 5 ffp08C(Ͻ~믎޼> p+ Yods-xy5ve)xvw[y=.eDr)qtuwwjPZG3,a<7qEh.d/ r+Sz9N_C8"Ϫn/{'(i=`p4p! hu$cXyx~/oW?k?x9Fm3wnc+ +X3v;I -tΤ|}=f8*p۸'Up~b{|[ A5Xܧl0 "C6,%]!+P*%kY) Bnו߂M9Uށ0VBJFt_+IÏPC1L.zoȝ# Ƣ6uspefĊ`u%ƖD_I QGP7 T ۸9aLA;B>:9>ysmT4 @EyZW[~o+r&!<<8+ <8bqGt[Dml@P ^% X;B\q+Ǟ&'].ưzS[5xy6&&3?vo%汢*k~j (-=Yzb^D6PcV>!2YpjvUS?l}X.\z/| xCWjBX8Jfۙ#7:obV u%J=/&'&,jyPw>xpf"ctE*6ҐF3GsRX&6:4QK lY{&3$ahHp.]Ѿc$;o+uM"K׭tvfcH[zZʍh,S$Zɫ.mمUVr4*(B({(* vg͊@z5m[  %fKgJR,&4 (8g5<D^a&(qzΕ@l,lQ$L]%|&4 W1%WcU8*$kK4649S&IֻhPdor=QFc b]g)jFoh52vWQwJ\sY$ִh)_l3Rۈ3鋹ܪM8(i"''bj5/Wzi~[j4O 3 WASo&[u^2ฯj.O-"޳GH%2OX1օ&ypo=/7@]!ńC`Z(L14gOK4C Q 8tM-|'U&< p?̑+xSipp|CfSSz1b XPSm>EO*WNz?y?i :4}D'y{DlbkZ7 6 ]|al{3A[Ɩ)qs{ ./m䀀w 9]^j޸);)n34û=z}x pjQ 3i .\\΁ ~ `7`#8߃?#dD>zn+±p޽;wpc/g(f| &ONt. TL .C|W/_/fDB큦QT6XWRt6Ŋ0 eUMZZG%/qê>-F9Ax@1fi*T/co1 .=%] }2+'%HeHЃݣHLyrz΀A]h,Ƌ*1™=\rp<{ŠJ0-Ѿ4/>My鏙TSED.a $4{MX(G/@cT'ɑO@(/;a`DӪOUhS'^*Zc+,:-rjf hE(Eɷ{19r@KLx0հ$綦W"Y.3+uQ 7g RIL@-cf& ݲZ.`в&F% .U@azU[bY}=YgH6_dl%v1OZs9/fNBdV %}W\MoKVt34->@`kHgea|KKU*_(GJzVOZ"/y6 4*FVmfaTo"w_Ifh?,:T tM[+,P=y |y V,[2wuڂTr޵OSz+Y|z| {Z6 [ VϚr",CTv^Fbvw^pqZXJ̀ߠ6o1JMg tG|n R'=)(V dlXO`s, 30F?zXx+# 3# ^&evV7D<ћ7p`! D]iRL#hL;2\M% .moy¦dM%.lcώYd f}D7<1شCDGEQB?qN63Va8#W~:u_-K6%HӰq&WxhI$uQ5«yVβoO%CKD޸?ǧ+GLH` ? (ݽtpb+㏟>>A9SS2u^i(3nt"1d cTJ^19Gn d7@̝;_w?7_7 W_|?Oos8˱w +1ꫯx僇?sj rI1Pz Hf3FS+$::,RƴxЊqIÔ@gZ  i^ѾKA8Zb o,0< 5*3(^AM9# 23 o@ҍw8p Hw ɊHJM/%S7]\ 0Fܕwv/>윿syխHrkݽDC 3e6_lMYSCϠ5lԌ*naW?^CBdyuc R.=2w1E- jb!ݣZzi! .\^RsLDdWM6$?]M"c)h|5u5&z_._"pݶHOAh1sVxB2H!1L)"*<.Y.yK壟7`;8~{/__|ͫÿ~ӟ7pW+ =wgm\йA)F.֫dW!' W6b~9#u}Qx1QJ3X{[ GĨ_K^+y.@r&T)1 eR@Osd<ȑrwUA:ӄ+h#OFU$g Owd9)cgDpD<{[8dsukG?w.s}^{)[T,ijvHf_Yp!\P62LkOӹZ:Gh𚩵.Mw/]> OHS;pDM(XZ7Ԑ׵`:Rn1}ѹzNzDUI̫/|YbI1`p Q0bI\ TZV9z}6"Ileꗿޣ;HgIds8>PEGd9R{*.E\?'(-ttvzw>Ӄ]X ./1N<'{.< }r]!S(bJyCđU'(>2Jb4f)<{ɫ/|u׸(?~#ܷi_ׯ=~gm&ɜ*xmz$KdqPCe#27s x,3J3 J цW( VG.DwT \(מӠ "`h2nǞ{y68RbYҧP{z 2Ҡ bR\VX^MR.Ynj#5X-CjpV)W.|CDêc)Jѳ۪M`A0*k7!G.n-zM˦6yi^qxf9tMђdǶ1d%ic ,XHf-׬==P /m~t<,kys!֏Bwk.j`Iq`x>Xb˜Z-Ӭ_XX+~v3"Ҿ~0׻t` "VmqӹdA!qJmUy4Vx5qd!T!7scAI\3R贻&J2ErFK2`s/̻Y16z/`-M"dY'Xp\\ nrԆ*1a=n7|nh~ԚK𞘗[ LU{BL*7H2[Gdki-)ڥZTD\kC?)|A EZdZpѠ{m R 0΍z2=lzE"W 0CdQ+Ŧ N)UA2Mʖ2-ulww@xOVy hʁ#|*+!!\YD2*\ {Bǃ=YΓ~l*R1x}\ABsCS.U%xޜZO%;\ȁ Ns%r4 ( 0<i sA _׌\\~x"ρ yWMv5Slp7$ug?^}իw@}{?/SB3kRG JUeǣFJ#MrnNǤ e{st/SjƨImOK5?lD i$ql4ƵŴ=Iq^xDӢS72}5v!U5FhR沼F]c>Fz$mOLP׬(e,PDHGQC%Ơg4Xg1+[ BFtꮐ^C'l{Q9!K-@:X 367 \jw&ƜS\ Vp97:٣dTo] OsƪjjC&@|㠭™ܫڭe@+SDCe$ ֒ab7LoC@l9f1e|O; WQa'u5¶57nP!9o`C xT]X~e%T 3gpۼiekH|u!U%CjA*CeL,1x"(fPM1* +Dneђ悘m{mXtt434eًe"M7bT^֠ \^\ x곥K#҆mtKDBXk(\ ?ȴp vYA䠙Z`l$Cs,j̥?"5"Clt_^ڹNXkc<3KԎm^7yy12i N 0u 2$òup{ څCtӋImQŸM^XȱQ#)~%:`-W1RW෷slņظ\xn{LkqRB0in]#/&Bc.0l % }~2*"֚B31H(v3.*)^FQaK +Rysu((߄ks;l 4t o<-2SifsB ?CPڢ˚_̨+Wa,y?U10…nJ3'/Ϸ7zy&\.FXRKx%ּc3tp42iD+d0) -(MHa%2_my|e%k\KU5 8B\U1F=mXȋvi6<$ihZ[$Gaa3כ>[e_3Y~ޖ&-Z_VJŐ7z1 ߐLQMeAѴb0}Ŷ%&~GF#ڶup`ޤJj@KPzɺǍdzŬA;2"i/4r@CmeXFɪ$o9S3y+./ZQY5y7K-5=6hr[! PJJFsI8BZcypO?}{#_WH̀8wn+Y$/%68 ff`R b\j"#,rd( մ:%ڵsQ> qZUt 7-+#{5wed@Ee=UY挑щ3\G ,=t!r$N0NNSs`?O(-D <{OG?^<~ YWN0 xpF ])h;E`N֊Qndn}sŔ(;lAWȧ5Ɂ.'tF\^HRf$GY3BϒlK)DXÂ[ Fid"LŦ`q2K4Cb6B}H*d +>vZ$7 RwꚥsJ<z~nٳ/0okn`J JoB[  u@FKq ZɖR1S/q`0o6KK,6H`-Ť5QkmCoF?'|_|w/νEu.؅l48یhEv2e bmB\kɣyO{Ӎ Ռ)BFp_ӤG+kD;<`KK^i4j=MZ DSW̵yr|q̊ qS}6S:8$vI\.}4@0~k/y)L9F0;0[S\o| n!d# m3$J0SsIqsTٻ'Bt:d/~HVG(FgڒZzxѓwO~?zFJJPOK҂։[@zqXB$PLS'BLz2L*惤ĂaTq OXRG#,*v^Һ3ao|bbu@ev,8@%=f@L@ςWJ;hh{~ ^3}`-_t7(I$` LQj*8"⠲'א) :g'lfdr'4ƽ+RT>c=X{q&$a÷oqwL~6w=[]"#< 0:!/y5I0 *pTޝ5??O?'щϟJ<`~/_=zd:;;D*B&Sp >ex:#%Z}{b;R(4µT( =v5SȴV>oKQpXx<R J-Ba”A l}pbN=5~яrbqjE[UT e;ЂD,Y`XލYK!f= F e(]m%LZ%ը;єY J5)yg@2oGvTݜH &Y ǡF,ڴɨpK7wӞt_L=לZ\^Cm+2qV0\_.ȯ٤7/SqSZِ,p,}H,ˑjDl:Ӫt0z:˲]-BlsFztA\&ҩŹ*?cd%e$ g!j֊,24*SkhH"߹d!3VuCvb0nlZCCObŐim3-hxf:u= &fݹa.MV1Nob*\y-Uє-P&a6z`lbږ^:g)DK'+lVbB%s6 KU-ؙfAƂg-ʍTXe߸9%l=o/U_V+cטZ& gK$czm|*ά)ijgŮYrgOI'ux-4%ܹ.Q!h)|K Vl-6ezy_^uV1bĶ|o8 2Ģ\̋%QGx>'+WeP FVHucN5AeY7/yNs#5q6sܽŴp>m|叮u m W ;|`{ +܇|LE|oʪRp^ *C t\\LJoqbgֻwǯy GyYa1T=qUffWB\p_9|}1tN'Ii L x  sU.KCK}:n' q Hq$<;e Y,cD?\?cDQdұ4%~hc纁~?/]~?{{~w>2C X/|ͣ>9pnE ʩ0v1Ut5o(!,is&~uǧ0=+'9GHsK(V ce Xphb4呸O@d ͠ӊ40 i(s?\^ ]R<$",Jvlt"LJc?L_;tZXSL e$& Fdۄ{F_vJ]P<1^ 45,C8 bU5^u^>ibr'RSJY5A95T֝ z>|FMn]2߂m}WciBɣM \ W k0S nFҲ/qz(☂v7.%mPg Tp).W6c.Ck 3m 鐔ь4_n#kPuK~.kB^]qmYrq@l;OiQhj ZI9L=vP'+ǁ ܌1oC&Z=`Ͱ ۫RÅm-`n i#yCԳul3K9͍ yūXߧ A*闍RL/`-[9F+ n1ԫ3ZIb *S!KzUH6dIŵ7OBk/"AnT GXPgV_MEhZ^S$ gB~]HWʤkf ,F{B'Q|5^!뛘􂡑1Z˜X>ہb-)=s|G<oIS+ۇ{fV|6&fl,Rwaq}?N 6oyc?{cVJ/t˫9xElAδ҉)IhԆȅ\L@X"" &8Vn'E8# yě;ѐq^"u'c""bw YB*P +,2 %?< ig_(>vKiyn |4 4햃&q1|;8f $l@&#dꤹH,Eg g}`_OW qAk;1`\VKQR5# - vԕ;4ɦ(`"p v0oy 1w'|tݛ9(#~} \X:[=w*ex)#j̑D ?G?o~o~'yK@/\%Fkׯ_n>U?sj d]8HN" [ŕcJD̏Ch#d!2#Rt|$e cq! JSCqb˺Kg" {>2U* Zw1A ('Gd32;-x$dDL|/FmxsD$:aҕNkmOILsYN5i8894oL1ߖ%H/)׍!Kl8'eλwPJЊIEb7c+ s4lCtZ/*9+ Uh1J ucw^PwӓE(х'{>6PܜGT,U\ dZ0}:>\\A:D@pGR}j-)(e Ǜ$0M?ox+e\T#0̚"[6nuCݎD% w,ӈGUnBVťqE3L{5ԼoyZ+h 8F&e+soq MVc*fՏO*T<یjWIm<h{2"d/uPV2["b4~Z:4iOzk k$-~=ZIizJ<伌4QeMx 3feDNHX*@`.>).m}a~kfBY/J{>MÙUzGύPhA(ə8+'bt#sg @lH/lċZ9ʠX o\psG炓h5x MPtvwpa4'w@aN1@=;)s!o4ba'BA LZER)pI2mN5E;bQ;B>9P/^NŴ^^kuPkmZ>9o@|ilU wxo5pԫ*fE)yl)z;TIxYckJ1̣Cط* ũRf6rk-orW\D4.m!!.!IRS^~1ȹ!#tD+!`I8/O&Հf)Ϗc* b?a5p7zH5y|v '=Ux o܉=//OO./Nβ˓ N1(_>‚ R([en,|N6QL$z8tw#/>$`k첊M!磸,\(ĐҸnߣu!/ai;Zpn#EPZST֭"/. I^z9~*M×R`)EEhȣ/gh"s$dNJJ"=ɓ6A*J&QVT7Rʫp6(:!h.,0K+hchI}r%n5c+&R6SBVE=QW̞txfÚjܺJrp[/w`rIs;h\gAQO k_<&4 UlPk!2c;i3 W %Sw[QLnPxiȬ@,巻+pF[t@ּ MIi`xl:cM6 3h:oyB)iPX׊t,-F1cg˚:QC2o">G֗O:l*0#Z3'`w3ÉRn`h{~0FBJC,5 !Ku_TZ8'-_K{)lâ:`OKc6Di7r}s-pM-5smf3?+(ˢ jJ)Z /|xvkV =M11SF#]ғ%i,<ӊ>U#Vj25?K؋Uuݮ'=pH[Xݭ5|pEA8B6V|`x)uOhkPfG2zEǤlgctjWx([QW0! hYι,W-^xb!(ɊUKnӸR V6SRgQ23 kEpiۂ+pGP΋r,>cJNOf0/^A4 ^hS4C&99^[i+뼞`pƞkiv :ە% Ɩӹw`1$_j_)f_7jl;a@D a=b82 lQ`Pz)îBQk !bz;^d۷*wsJJ1 h>C@ %Dg +'|>\N(Pƍ~ځ QHD"ELOJঀ ޭH̀cwd8gjFp% ׀$ ~Ӳ4 `AW0vEs~qtSpvKGzl#u$bPƿd.~fΉ$A?ȁd@C?~ĬjMnAiHbr15)5p0w%s<3tB9(E0 >|j,LJm^ h JchM3 rIc۵F#ҖW4$8;r:XzmFA QƵ̯5x^2K);i;](){])botl}E%-u;.Wr&n U;JԐWIl} bS$Ua9EQs_ ΅( 3 5E0[-R1c1m;7i!f!Z&{T}4R1 GZ }hSN_z!Xݓ4 5,j;vMLQBN x݊cNh}MA.mI=ea-՝sxZ-1ܤVGRY] Th^F~hM, &|kH0cռjtbM*HFոڽn< lgtsq1?\"zF- 9G.fj6"LlkJi]+j 2'be/-<-\=JBS }C%ā0irne@KK`.=Ɔ`>ƃ"ޕbUj2d5 ч^3S{vr1'w"O9A)ż+lrɏ܂֥ <` ~FEDKA`'kАہuZjBOڗ7BgIN0|Yxy,*bdx2s<ɳ\%~fQBMYd[0eVkPIHQaMZD&drAn vs+X 5z=ϷȠtb"hˊUxdZ`rjB-;/7Nҷg-#q2X+q+aZcH͘PF蛎F:Q9=yě` UtK`E׺ ؚtB&W4&'Z: ZWZnG_ sRbeVV7'2t&i¯" -6(S xp!Pz7d@*yZ>U"F\92Rz1: NXxwibC k+V/UXZ76 [Vv!˗P럱֣Q1}z/4Zr+9* iԆ=FYCo3 9 ,$B j߈UDׄD3חgeq6 !K?z=d`= ܳB,yN`fG`?O2ta=۹!3 0DwV0b#be% Rmh5Y,">ljW^8pxpM)A#\%59B'@8lpvN m "B !QUAUAIE1j֏L 5v鈔f[-I$kE!w~#<#]mZgbDİG-7 CA(]%!_k=qm)w$c\>,i'r\[oȕvzv$J,36P޸,7P1uzyՓ^Ɍ S9>8TlyȠ8$|%165yirW1Wgn,9HjqnU(3:CrAEr')QhFi\.WT 9L@{.&wf!QNHص Qc.KgOͦS `l}UE-lZcUA2Kt|2^3TͭFCHLrIBƈX w1ۊl'SUl8 Ӻ.\"f,Nl$*1L4\AV ep?vLM46 ߉m(5'Lo#y7E9l-W2lXfkM*Qi&if,Ixh|E{יj.TINMNKCjʘXu6d -)d5ZZ?b8(bERoe4颋&4o}h^ڷjHZBcTGQkx%K,?c"KtWifA>Pڰ Hng{Ub)Yn4.SB%x?X2ITr1ek7O+ A U.,j6Zr`:mΝ^ݻ%(u@HFC#n[V PB&4P‚'\s6M4-!{GF" sihg>"qdd6=Z|v)r BeU MIʐ՟i_}@J`PjDYmC 4 jPA|6hw`7a;ڹ)r⃧B./M.3JCb>inh:74h s>~! h]7O .5K@hJl8>UAMN21Q[qY1YxJJCA2l\"yEqv S$Xќ*X<fLSդ3jv&^e-ל\<:1M"*fBMq fvɲ"M@Kx6jf^h'`EejvIE(:KN԰E4觖,7gי?!GŮ*2Vtl{h8s-{(w$0*)q|)02?SCM$ݎPKR$' |7O`RBc o}' K,% ޽bbՋo^y7{=Bp|<,J濅/:ZW=JXz}X.;}dd [=NkBK< vM/Z x u6uV[A?߹i+[pތi(E^*-ݝ&,7G(hxBo]cA`zNP|?lEhV@6/lΌbĔz΃ nbuBq}Znymo6jfLŢf+S;pN2h pݙĘ %RhZ7͕Z-ٌ248w=*L֮ޕ 2}2 Zj~m y$t;VVlHHB1gr-I̚aE̴bX7E~,d.&MR7r'fN B wg|XG7'N;Ӄ?-%Kj*>0:ZJp #e:P"l8vl;Ү#+AWSm^ coŝhLbmniͺ `i3Z>{cOS;/ج>E;{va%Mh:ak4Xef(kAq$?92ۃ]|fZ$6o,*bBHFǏGX6;縕/J.S9 JaF0C8@i^‰ذW x7oϣ'O>]/J9#o~/3O?̰8v lѼۋ"YsOjg-Hxg/_7_)S8=^\H<<X_(ցk'`ׅ/C 㭊H7ICW3n/-@r&"AHw}`Y(l5oW$&f2h8:z¾p2h{)-y8qڀDH;wiݻbCy//~ xm Dirq ܍hW":Y #8~{IEa+n}q%U;mH8F@{ yCGemQT!P>prV?;;OG(}ogw~̜\?>E<ӳG\\=x_8Cߠpѣ=xكI#<(PPOБo*P ̀̍Ao%?""/;|D :ж?OO5h~Q5YDpj'1Td<2&[yE"-hCD-񶲒R N0JD 6r2/zM@("'VvzdMe\KL! ++pxI 0ymۙ!y2LjAo5 a_è'h0GŚ:a[Xa0-p?f\,43yWu%VМ7Pˍ ,E+Mܒnqv/xe]c7W5 E ´ ޻].K*GؽTT.:  zdU9J]]HcdA1Dp5# Qya?tѠ n~)с&^쫠MM$hU갽&`=dw1;LVzHk\祁.,40W4cxðk͖- qU@.A?卌m-O{{ $I  8 ".x*;NZ la`>Hn?u {Wp`%toqJ lD\Rb[Bi[z t^LywgEwO1;JܲΔ{nXՏ&61s [d#x/[(/?wq 4MӃp ( }qG`ߓ82FM0rag uɳ'_l,=z`p`\ y0u9Y!PpΤYt8 Ncʒwr!66(o!KhmI. tBhB"aɟ7Lb=>`'Y|S8{?O|럽K_CnaBbX7vu4 ~%lV6&8L CHM hUXDZ N0 O(Rųܲƅ`O΄K% /+0tl`N;w={'Oͯ?}E .\ۣ×/_~~>|h> ?y-j·~ɧ}G}$>_-0b@V'иB |~A>Xc [@!!9gNBW,\]46,Q[ l(̔hoxwo?{'fNN.޼}cHIA)6:􃠈,42sC^/ ~̊}ʬʛ-m "',ve&  03H3+;  (G^Ԟ&]@-`KC$xlSu=c|ߦ&Yek @5/'] zYU:u+?fiڄjF} T45dIh"Tg9긮pJ{~3//)a1kT¾.o1i<9L:Bbr6*C%88[8]s(ܣ 8鵭 , g}7*N4[g VZxNrh1i=.܌\3~ #jv U@gĿ/8 nrڶXnI]5[Hf (;PoQyE-dx!V):^GB&Hs25X{kZĎR,+_)PԨ%j^ W,6Jd!ns/y5Ƙ,1Up+r.rP`bt& p u:']/UX`ã7GGW샊+}_n2ȾJ|(l9r,+!]#DbS ~aSW{0uٯ!0}_/m=nl}0# = {O 0a {!rO2 lcL.pǟ|[0Ǐ"M ©lo LW° {.$gB n z˻{|a+sE7?Œ|p!F'=;SnKxGx%yQszu׿Óӏ?zϾ_S0@K9wyP@Wr|4a*@W 2 ~?9?9cI~_O??}\cL޾~ӿ_?O>/ey1M T @xOzlL gUiy@p?; A; |:pqD^<G/B6ó/ovz|G\f?ml翎bq;L2EV/VW,D6]4Ũ,> p>؆O~JV;*4C,5|$? 6GD:o(ю2cHd:rX}x|tx<}tyڙL4\] k^N91B3G~`²UD5xA ȑ;0&E{&/~CHN* +B=v*A/H_ÛcoA9#|8|H$""tf1uE\m\qCd{ `Y>ymkƹuF1OGϸ89 >2v;<| Y+Wկ~ DKAxʘ"JoW=HBnhsNeK NR D `t Lȫ>`bN,ZV2<)Hfy |{0z/0RxSQr OJVp8&\-LrVF*B(g~v"؅vZFWz#9ްIpnHP+%O$)o%t n| Gk^[C;z:6Wkk!|!*V,0P,{E-|XnIfiuoO7[=MejWY~@,7QHdvK{ߟkHv #юFD^6 rY1x_B*Hiu:Z4v:rX9Sr-ϏE~ ϴz"W7Icې"% o|x۸q2I[w,0[nk6cngB//(ڋZ-u&|Q9{2۶̡ 6_%4fiQ{j\rPq猣.d 6r4N"P׼),\!,Ą־VPr@ 6PщWlf=IՒ4F2V̂wweG PiP]`P>|@7[Kf9-hDQH;r4[dDNHq0ZD >^I_G4{[ р:Aw::wt"L? J$pF6 آ){]?A$0-tXyCswF Ȃ;38=W89`1 q>>RQ(=!ɇu K댫7"ٻ?z gr!x|dG^|_;<&05pxl+")#Yvqp~/=ۻ՗)+m hP;hv6Iԡ!p {"aG$f0:lFEYx$HBw|(*Tƫb\¡qO܃EsX02OPc]4 =mPXW d ;?G0xN[=>Ǐ08 ."U<A.-7V\kHfh ^heF{^^;5 X<;x{;/|Gz;I`%ڤE,b&SƤ8Pk&.A,pT I%eh,'hT^KaI&Pe S dm넚}x\PڿlѦPd[~;x(ƳBT9>\b)R貄qH$|%ͷ='njV@bhRV `_@VkKPtaR%EpQs)8L#xU*Z7jW8^GՇ` ݶ&YxB^`7VkeV> P`9> FVI?EE*5`,moPuᴄ༩ߊKc `5W,ؠqYY4h/H6Yjo|U 3PF\Y!8w Y̗j9@*T7Xѐ"GÀkFX^=j46oa׈6Ԉ8v[^٨cjoz ˜Ȅ+&ڜvP =Q#>jdZ mա!\KKᎺezád]~ -qnfetAG*)(1 uriT[7mN1T-Cꅓ=UZ,-"], '$vsaƝDQ̮i̸Ay>jE$ُ5 ӜSӉs#C!SAh/`.v򒬅E#oeMN [1;pF.}h#OZE|s :2վ.Jx|~oJ``}+!sqX@?Z.sZg:j.Kը q&6 (`uD`n$ʝ1Y 3tc[օ4nAGmi۽g$Dh)GHulL6D4̌A0%S3C^q< VJu%lZ  ^Jt`Lg(yutxgmXnLp >_`5<8hJA;"+ 21\>=EpAЄF5Xߔ uN#BN reEcCd;4Ċ) Es#;z3+Wgt'ŘU$15ty;D^dx9$9`n&0&p|T2(؁&2ݽ'k[liVI:n0"d>^Hc'(Q<I-ytwϤUDiwp OR]qYY)W:ޥL;Q Nép.3 }T0;.< -X vG+iƂd*\S<[ܕeUUE~Ze^4lx%B%=Kxػbil{meW߅>' r8Ցo"K-&ʳF9RVxB# ŗ@VWT[`B 8l|NH?`Ų $Nh9<g˹f{>!ؐofn vEd?jZ$W kn1L>4{DoL\x#ؑeB~t~ N/R6W#f'@DdjA6OW+h^p3 (M0[(&f{Tn]BN&@ݘPyif0qOXjLJ1b v,D-yBմE`:i[:C<&Z7ƇIK՗3hIWY5AU[a] 7ƹL&^%1tC4 A0-k73rMy0M!UpgL33EuXQ|zd#51'8VWUJԁkW n&XLQ"ˊKGà&zڰ,'X6 q(yTjbӨJVUm,\[Dg\ Ѥs؈G[#ʹ5-D|^M1mCn/ X}yR> f!:K dmlz%f*U(+rjcj8È(t) 4|2C3s>7EN݂f ML*lXX1"DiQ# bY}K蒬 dC T "V&ik=]}9Cj\et v+׫q57!7cY` V_ :j縃 -i#8ݓxLpD# WQ1v@oD<[K@&'i|\lM'a1[p A8+d(RF: 6l!0KX48.+6;E!2U_ʺatPGv1=BܚRuӝůR`ĥwq! >͌S <5]J9͌)=C }lLI3<.QA$55MERC[`XkײE_kHr$zqt " ?Vb%,7zr5̢MDk]4yJ2  2RQ#kM0hFo+6 Wԕݞ5Z7jxK4YK&g$`V<D"Y2^E Lgm<Х?DtP0*YvZR׃պ jDj>ܔ̘<ߺ|`K C|%7QA =)i4͏ii¼Wrܙi `)#"[TSR[4Tf|?Ң!ohG}0\E[hzt0 Y|U cQíυȦ8 M">;hRӍJZz6DJXa (2A{'HBXNV1air;Vlz N}7Oe x{s] 0kEEhѐeqi"R\TY׶A?4Ź+MTXW6Ȑ#te1D9f^Yql u) $}|ȅj4t"S oB B #S3FP CbT 1d;ZHb 48ZZN< \y@Ae[4u"Ghp*`ҹ+:뺡gUIG! Ӡ$}*\io*JV V5+O Ak9"J|u`:7w;[+x5888~V>lJD05-9lHq`Ǘ\:7M *A~ɷ')T.Cphycq;kl롍|F*@ XX:92C9bq&}'V4R=z;:MnwOkzM=1xKAȩLPy F-g\ L8򜢣J+Fqxb"lhdZ0K\3B-:(9_9tJ Z-nۜ VTYyl66N,fiJ#{=Fen{1PG3ucy(CAYr1H@=p)Zt/9N}Zh|Vgj/J&T07)fX~;iהςQ l9U["LZ%(-UjwD%M1t5yغ-eu{M~lN}`̓,VBbCKGC"T>wŷV¹b\b%tضת-m&5\P]:c 3OSkUXѲT_(YOR|) U!i<+KEoԴ8 yqٵ|rP=Ius%7Q!R&ؾjgo_9ЬEXIKz/ҢqIJS4S/n4/+Zh;yԙ`l+.IM,)C_XEؒj5ZTj?! šB~M[h+l[&C,虪 (0se扇Qh/őU!0Q^9,) 0ƹ˦1bw1N,9,4W]UaBp`-;|xy6BJ~\*,S4f=yg'v9.qBdvL9ICZ л ֲvkә:RV6;G0ݻc8hmhh;LI!s:G`$Ř`H{o1mfPp0IÉpq Y"#Qϴ,/Z!l!bf_&` 3*15#?R8D_C~q.>*-//BG%Ovw56,!x\QwPαΏX𭔜dURxT;n*a9GxHD# |W~RNݢ$?' T޻~M@ώkDSPIw_~g}UA}jX\rOkx;Xϓh0!w& c.e牨 ,g"ϐR,σg/hX}XrJLQHcwCCW}Ru7Fнxbo[pG@P`o6!Om|!̠LKBOWYf E&CQk"i٪okMfTLe=(AP0 n" @A zCUw/G,֊([61BxHJhbn۲=iTү"ΫcDbPXK$D uL=--[x(P\ocBJjqb~&`o` 8<^AoIRƢ9%9Ë[1Z`EC}M$6˗3:.9C:NӣwJgM=?f:o0tH* ,Ai&Z-^d"qas^ibd?φn-j j1 WKÄ[x;6uZ|"D`]A 1CzF-a`#Pm8ۿTvL,   f{ Ǡ%v)FL,Ι@[U>F vb7f4i@KQx! PZ7@%&毖,HQn0.l {E7eur^L-ni/Ͽ+b5]')'(29V7ܭ2*7 .-6 ?nwp^S{p#3Mͺ{\6Ybh]׷@8S2(1qK w?0 _`*^<NkSRo5:j%/K,zhkunˆl Ed_q`j3d7AZV_۴Y,.:ip3X"*uZF_7JJnj!3Op": s9['.p)ST9J8XHkRet9D4XR'W'g4<9xwrG<LPpq,)˼x8p|</P/lWwX&5M8JZfsCaܸʀB> a^gzӌH`r[X5&-|xYՋj`azzx [t `8ʚղr$++sÁj9ݚoF^2_58Oc#4<(d"Ot |.YҖ;O &7 /u h)k';dB>4,5L11pݮYPW:9,k 2.qhtb\~ĀDN.Ĩ+\l wFBx0"&`:ouhʼZX)#/%W\ʃ{`#@1#bR'Je.&TUxwz"eL7JӠrƨ.qDX7zdCRFB~Vs7zɪ}Vf`k=1oRxr[e7%Rf^Ml7PXl6cX3q=e[\!sj#0* FVV6t]jTXpH^J4]lkx M$ӛ(,1,V.ַk뒛%J,T#wWh4M?fd)1q̒?6;perYt9c0BжbӶ@;9h ϩ{r񨡙f]e&vqIJr/,qRnī^jpzȢcx2d7FWZ1ًP7o{|yƏ8l1@_J֥lg*w}XsN 7>'㚔c샶e;707bV7.VO7^㣗5'YTz ȻcD[. +e Eq;Nz!]Ť69/cyc*`Mg63 jqgXԪyN|ų=@[u$'Y"nHM ܑXÉD#vϴNc%$W@qQطPf2?dO?xjI >;Gl 1(:1승p _#2yZ2h5NS\[u2! g?IA}8$}`dvy x?0ɂ 9°ӎnW0e(CpNg0_^ 0q\}ŲXֿ~%u!5>ɀGp%-h,My6hIDATMXؠZU#8~I0=`/2ܾ4"ՁrS9XPF@K tqt$%FF(!iXJ#PNE<H]iAJ!ASgV/tJ79|ȯGGXZPr6ZJ)G*mmdҚO,*f< S\ lIQ ֺ_$Rbb"noեZx3zK3TjkItu#|Ȓi+ K:EuBwTwfO05,T7!gP%lVhŝEQ}W@FDX3W jsCÑ7fwKgfj̦)5 dN'mR۰(ܭ[CE c"3+-/BoAm`SpsaG3dsxy .PoBֱ0d303hIqtnB{P"T0g9H:3>'2GՋ8:=Vrn/NhX!ZP|YP0%E*%Ww?-36Xp xߖjL)-7q=D|\_.SrV(ӷKXuwMB/sdWHYfwIa\E4TYl Byb }*ky$phfBarьT+mFK226TR)I=^7ntg}\ܝ%^K4 3A\yZ^ oEkK#qIa}6}vN'Y:"B(rƛ rmmLQVئnW ^W Q:Q/p]s|J-]&k" CS[6RU6,h\6 F4UK! B:jf\;ksZ[ک\!h;ĺǑf.^c uwf '),NFȧT9ޡ`0ѓ'zg¸/dCdtO eg4h[  Ë6o6y- Y;Л39 Ïq8 ;((pq+`^:~"8xB΀Pak0骠RDb%C@CC H1=s sXX!VϚT0l'z{2{ < CXERݹVp/UD'@eboj"IIKIU\E"2<V@@vK b"2Hӫ > G\"=b7Z֚-)(HJRWѠ a(w}=!=,A!o(E0 \ϡ zȋ׀]0,X Gcj+җ~kLU""JҺe^jf#afSѵ0(Elya7K6 hg\=bD.e QiY= Pnjs4q岟5#h*p=-6h֗<%a}nDVdLӚ:[PlltlDm +%EMTu) FAn?#[ˆLN/* - 07i:OD} !l2|O8O}X2Cm[r gč{X4[e#Cb-,C.KRiń4ITu<+<6xeA,܈^7O 8Ig Em *tI|2e;9Xyj!3fֲ.{7);|?¡DЖ?g[Q1[Uˆy]i1o-LDB1F/gĬb<or.8j妖JltR RSmMQy! gL2e͵fbȬ֒ZBieJT,ce٪">&TV0C8_;`GfjN(gKgyNXbGh@y"*4H)28 ]UV"ΔƅrhCD=9'Ŧ:(U1x0w^EU[I3W7k%L"9س YQFw&Zݮn(.2hs.,I5+8MuaIҬ"'|Ez91.)q|Լy(&XtZ^#N.;wp0B(3{ˆ+MR bd@p"|K| 9[ᚓOPwxHQxXr4nG`<s/Q1вUj/s1r@MfaAֵf)͡Ll=f5zS9,Ү1T 2WoJـ+HlUN2<1P_‚!k S0$rMlb X/{݁ %3<^i Bo]86ڀ$1R4&Px5ܡlG z';LS~/R?irE3 2'ee D`BuFRܳ@):̺LQcyRl!y EAHaj;' VX1:Bn!A|=B jpt-k*,ɋ"~ (p4RxXLw0Dr@.WU af*Ҡ{8O M<8\R:4+c]~[BG5FCb*]o VVL%/FP H?s nKLeqX `w"u(&]-A"I[Nm ԚMtW4Q\EV\ZD?f3nj賁SP'MQ</uf,Zz潩1Lqkuցˀ'ʹB(;3^R2ZNTKfmiM/=KBvaFAEM, ju}g#s&} OĠ7]M~lAAG;8sPÇ0A?p)HҎ*R65§}Cs 0]>G|3TV&BL\#Q8V# $y= w-<VOt#2~%etD<-R@7'Vu>WQ ![14~)[uEt#[R2iZG'^l-!lDYT(P Ǐ3C'#Ao^S`Aܻwpp+[\ nP0{%u#O=yD\eC0bQ<%3ā~tpn/RsH"_ 8Z C4VAU&oMxs +¿m;yyh-Җ!dqyhNe1m#Mxh,gWa@?)Ab&mGZ!p6oOj T /qA:}0o?[NJ)ѰZcڊ[b,tAr+Ь2JuC MxBֱ +LnѰEG}2="r)eϞvz+.YVPR I?vϩ8}L D{ې9\[P`lТ@w4'G (4o $oI/!&6#HAP(G)Zr1KZd+5s&MV~swS!p5qY6wް 7/_~lr4"d{4m_gmYh^\]`+!(ey(JƵ;-x. ZD)!L-#=zLپr8i1XW"~С ϯ~uͫ7";ЂVu^.h$jDeLiṗ;#88萸[NhsW ֈpֵœ|ª}: ->qۺu-6aQ:%Il+ڜ`z4Ni!3ǝϞ>pY* *FJ!Z"cۜi!x77_|yr2u0;Nx io( /M];~@w?<pϱ "!n]~`> 0jF

    xo_oO޼x^^zu y qЭAyiE6e6WEuK U؎f^T}sptA\j4tH:|e= 3o( \P3(7._=~OO(BZ Ou}G߅ag0nv`Jth$ fL("q}r|d>|w~wN&I]j?$ux&1~e@]goAk"iC9tz&rWnw+GK|P:Pӫ E) s@H ft"81<'(%AmL?yx-lajN%$:&FauaўݙiwCjuW7S AYF }}cg./2ɲ D< C4XS:7y6d~a,=kXK-Q4ʘX]QAiAk-)J( tz%Yr7Yƒ[ͥUf' pZCFވ&0ҦZ+|^N̗̈́¶tU[XBdCb7nE یKL#ojlkD6fo[k)c!Vmg`)@ QO} %ޅRuLtR&Q(" fB}~?h7䎰Lj̪KCڛo7bC텐MaJv\ţk`jģ.hB.'y)@| vl~S#Ў&rK )݂EI0IWz&gvd`҈Ɇ>%HH8C^󠥟;W۷oqdU :Xc{'_loÄ%<t=W 2{{ [“۴^wBo W&HSIE}G]Ҟeȑի/ oǴbw2C]lVYD2*jbWYrhIJ0 SE%F|!9! GG8.s?S V ȈU;";n!q ;<>=;@g_`}ы>Çt0?mFA_~Z<}/$k.NA ,կx]FC@&.0/` 0M^`J@ 0 _{?-d{8q./]@nHp){/?˓W}?_/sB.!1`@F̒kHdrM eLQ:Y5hn>~wz~rx';!Z2b\G]BG%bܹc~din3/'W?ϿGO?{q9@NɢHpdŷ({C~_'0x\J ؄oq qhzƻW}3g`{qen}ԙ*mnPIc<$kE8Rq(©_<s<ٜYTkjǵЬ)|.1 *( 7[-ذA&F0DSL| 'f-L+B b0(]LmżBAWoDM)ޕP9ʭEsJ!ZҪ`i9sdqb{4յ]:dt+OKеqI-= 7:DHEH+hĈϜk'xzD#s"Obk\O>ŋ!g#_hȊŋ? "8 <1iED6î`>-w`=Ĺ ϟN>'?=w=OO<ǟ<~ɋO>ٳ'm#6gW??я~_1 P1I(~ RIgv*=&Yee%vux֤,;VFݐqD<|` w|A v*G ?s|@tbGl?A???=&4a0 \ .B~v}lƷKK8Në,X?7I^Ak$h8:< } ݽ'ȝ`?ƚvQatۣC#Ch'ǁ=2ܾu E{LtUΌc8Bh怴#{z%Am;BAbY_i)NΈ2X$SeK/3IW!yvq?o3$mi4&'c G&It'&^QC|\菠{LQa<`DT-连 K"GfRY-{2g1 x$E4XN3R򯈓J0j,R+6!uUN5!3& MϾ^ L8H+-P$(*kNEB{:Bs _WWJ*S4*90ۙڸohJ<$>*hCY h , LdeKo͇{9 ^sb>y^Wtl{}5MBO"ؐ_aT^z:aV2WG ̍!7f/T29a.=S ݑdp.g±'Eyr[ &fY֎=% jl|&/CzؖH }~5ktaX5휘P[i;k'e:.o}MؓlsQVK{]H=ө߫?V-xRa6oloihG Lf8gv,`ŊVE IC1 x S3 :N˷0E} "ytY@f9[c)W%[k}=G)L4C:eTZ"Z "a^o5f s:7Bz#YjG+YkYdbHHh'Է|4tjq@v@\򝨂gHC9U8@q'\=b ldMiSEӱٻ$Ŭ~/p)_W5gOi@cw[ڝ_sǰ?'YvQG0#a!_F@R`3! >B)BCf4Fì.LM(:Kk[#"hF>Y;Χo=;xǟ? dww~TNrѓݧ|>~O>y{~'Ͽ'xw?}|C0DD|_R ՌdGR-Pju! 6qEweTX|m#Ǣ&1MG~W f>:]2l7%^rD @Hjxߠ5 &5ᢾcYvYiWسm]T([gDOT"GCr!q.;Gݷ\56EfMzݢ&j5ndo)18դ4 7jVD^⻅5{q,)+9#mko!NomÒL5 U#&Scۜ;xO38WDVSJ6A8żIF"^[_gx롬ݼ #jyfD8$L l!UT`UtYS84@[w9 -g4!Y/ԪcQ,~I{hZeMR\ i-@lY rʅynq5cW:@xm4kS /'y 읫EeniYLq2c tԺZzZHhtSYڠ)ń WZZˈKfy " G˫ɂ|/HF(jf ׬OPyJ19h\%*ֽ'! LTAN8xS(w؇:RZz! -lHimsX.fٯojʐРklac{xqԧW8@w'y5IgH񜷲فx9هsp[wn]ppV{ѓ\/0i22I$F\+M'3~7߼|WyrgLD}:f$#$RruoL)R~p.?E\ #v ݻ0&PK>eb%iZ(DKB*UU5m1mrHtʭtD#5.)K}/;F0{XdT@q'y=HiLa]Kc~Xta8"<5ɭVB4153Ƨ`]dPdmM[ױ^9HMyֲGV;mf8^Q4ö8QGٟ=Y\`ndԾ/(@H&v7c3c4>/nja^F6c28=fj6 R RUY{ҿU͛7>܏9~ܽZjִٮbpɣ!emfaK'Ԃ5/d`,cƴ h2ZfȵM[Z<3>5ZU"z#s~.jJNL|yh݃f4ϲaʦ|?2} fcg[2͊WЧ.r`#fΔUڨ^ G,9w2aP9ֱ-:Ω?Ԣ =jJ5q5Cɓ޲Uefc| h.1~/v7OLYc(qՂ"Y\"-NR_Kxƃ.J<ᰥw$<| Nt{!]4D=دZPFF-v 5a^k 0 s8d%5 sz\i xz".LhtXU 슥"t'fM#v ׉!c(IV,j݄_Idyy^38o.HX]YCrWC=u8&&&=Irds+qʥׯbSwy w8'T3VYXW=XsqǑX,8$ܜRFrsWw$ԁUn/euMjEE;T!FcmPJ)"܊2քD}H,NJxѰp[.iEkK=.uk H.7Qqsֈ.L"R%t.ղ }4iUzv-R :LL3`{.hfؚ lΌӼ/L;vI3ypeDy#:] cq2k4KMz,DJN ;ݳkOXϺиKOln 4 xyH hbv[=1ΐ6"-m&r٢dX gz <Ȱϫj2D&|m/ oÍc[S]lpa^J]-0-_r_Vky U?QF+PxO B,b+([&W2KkZx\kx~B^?*+jf@$2H* 9q'z;w`'Tr9T6Y :n % i6s.v𢰌#`dT ,qTD|Ԭ.B@6}ZRO4 (X,NeӃZ 5$*jU/t;@t~ьRpL0/`E4"'\%%dQI8ŨRxhؚoVXS2ul=UfWG-KFO^wg2H`Z(6p>c!s{xh^$:Uf!NLÿ, rՆ+"t[H>sPx_7>xdE9D_lƴ\RfBCMXK"Cb|OE@)zɼ eq:MKD:aM,RÑ}e/b~1AIKU-i1Q '4dw1~*o^ӎL3冎n ܕuo$6ig .tl)ehb}EV=u C8nn+'?3 V=`r>"1d4Oxs[l,(/+Eўezb8Pa :ї ٌ\OqrzPVu ']ű٘FM S. W,HbXBB=(]*4Njx2Y&({.v 夁@JghvCrI6O1Xc&qjyV$/F#.G9]֕V3[T&.W,dIOhSzIةժl[QB_Q<6|m/es _ㄆ419f@#\id+5Ę nVd`1栥tQamz)e3~wj̞scS0mπT ޕ/xaPH/y@h",=3 JnvT4thQ>өHs Oy ,x|6qj,GcӤb kÂ˗/c=822ZF:;n)~xš t,}!4#BOZƒp# WVY=m+WN:G{?3=UX}w|xGrY=2!}pmn8W;TGhGF#ˍi*7KJ\IqSCGԁgcq8dx>FO@W`C\RbX[P!C;4a̝:ā5ܼ[9hI8-EH5c4pfmE58:[ucr375oJ۫Fb8hwp6} ր*ussxaet0(icGNN9FC RBrpCJQ _ˢP:W%=($]T.i#26Hi?RRlUx a 0b<^ 8T:k R zdrSW M E@U6N7Lc۰"^u6ŠD*7m#:vM4،p`J;LQѵ Ep.TZy7DޗcH2&7:L ?ddʤif~݋ 'Z-,i!#MxZZy5hj^2 =^<><".娗K$ӹޓ]3. 4Qpi{lJ}7VLT/0dA e)`-M5eYȘU}Kx -yWtBfdckC`jn16wL-/}mdtwx,*Fz}/c6'vRpD4)yWY-Ylwi- \0tU&Z׭m8hĕYjf𲰹S iCuXY8Z ި2pTje7`Af nMkHqGk9s >;E#Q֩7";VJVdT9B=ceuf6^/0.UPЈ:ĦI/uՆ0 9pNý`-T9v؏P` baC`IAhdi)S<e+%x# pxʖς 5êF )֕`&~XtSO?~ڕSWϜv箝:w']?}ƗN]?u;Gg.==+k+;ok;ZTzWO8{6\!}pͼMxr!).D(OOG!2W l"F, =maPz|Xx)S'!t޽ugs3{~=ZlEyՀME^l8 -FaP/W`wFgAs!>#/P l$H>4Ҡ XD׉芦`HIJ}N܂CT 0Xis1Vo~DsgtuKi)BHPQIh;2|P s< H-u*/n)VM9ɮiF1ĘN=p;0ܠq(9ɺp@&޽jN=s5ιJ* DZ-&!&Qz[M"ϰ fM% (`lCW8t[[Z@mf 5km0ۈT7!eNoO=sC +D\Az!qvjK'~'K۰7P/Q!>=_$)6/8mN+2<ލlg(2 hBLigOn<@ #Hj83̋y Vx!s7SJ4w/)踬ܑX)Hb}$f oQOR+k6GI7oy{s[uNE(( :W5loksks0TMh OuJm`*A'q;Aq{;?nyBo\H8Ghkp^r/x -pP6'.хV!hVI8 >UudyF.cl`ؼެ;ܼ9p\]ɂ寴@ª,"fcuX k ZJlDGr($qhj(ͳBٽ,,yDٳ*C?O46V ¡)FY Wogwe(j̅?]%}w4ZM5ep\ܑ۳Eey ؎%s4|c !bDZ_\ 1յcʧQ^wFt9{ZASGP}+ $m1e(w㿽"n!3.i)6ZFڼ;=[Q5>衢Vs1?wB R _Y&hJݣ*Ӱj2m$*Ig~q/]Q(c*΁'P-3SS慪Zx h_0'_2-D;&cS6(r2A9ޓߦ5.£种M ꁏe!Z MD* 5vA&نocM\_p&r9l^X0/ (ђl<՞ + )+gml 8%>ۛK7n\x)d$φ4{xb!pݻo#$7H/rwo~+篜ǁwٽsu}k{so[7o޺[7׾}-$5lҟџٻY!^TnpHf `์83*"tHRv#WÇP舎?|~$ɋ؉cogg}rwo7 > !=p #;;S3bwoA^vzMF0_ckKQɝEcumlHdY*2 A~nʔQ'Q A=#TlP>r\[AhiAi{9R$=.C?2EBT!N|LJRmP ~g1Nze{*R]N3f!ZR}ִ%(IPn-mp-΄԰E)1ⒾGXa6 '2uS(ZR'M}(1&"SE2ֆOLj$ 4U`:-iUkƈ ⛓GlGiFpZ#̡\ NoۤcM`!1BlszZr b˽@ v&i?:18".ȗpI`ˆ^Pb>;t2s,) ^Vω[j :*2ZVLZ Pm Eoӧ<#E7}&,2EEiRZSuyqi wd2@-)5FIC|Y!E ൷YAbqNf+h5li`eu*Y3qܘɈ~iT;wތV[K1- ;j[[{H[oHV2O܎3žZ!Lأj5m_W&]UҦ%ְ;`*quٍK/xyc,0W =:x;oܼm^ŕBId޼1Ah*h_ͥf<V){hsD$@.8mqVk䄳P E5޽W7~G3<<| x<M Z8.>wN8:q~[;>sO^}ۛb4 ;ؼu=rrpc){R|$nW^-bm WG#xߜa-N8B.OmLy@Gf6"|}lpb@$8x /]x Uu*6x#8ޒ7pmM`@n: +\!0ݽݍgy=:q<.E)bbHAjƙ됯 \׳]?@gr-0pspPQ'9w$tXڦmyKjgjSdpJH@PQh6RxZm쾎(,_~@׃:կ8NP-bvnwz+r s ~q :VKV0~S_ZzxʒkwT*=-EG8zhU7\ҴȼL:ګ Ha$P4P-{`F?3`˲"^ ɭp|g%Tn0(LVbd78plm.h l7DOOcVi &v̑}іzŶ^p8UKp%Kq59Gh9kE];#]oKr[Vղݤ29v 4Կt 9;YLwaw>( CA U]8~u jnJ'nPJ8 ]sW:?A>1 `k@lz*3t!OQ9>{x!}j_EN%kkUZ/x*m4.D ̧PPg:]tX`6cNR>xOaG?e(QLjS˼ZAnUtU+RP`7uZg]#1rk4Ei|ͯ0d% l{ӱ<5E^~*kVWfG7 BmVǿv bV+ȽkIar{ZX}Aιr ihC2i̢‚pD< y phl6:R ^Gp/,3٩3{Y8>37<6z 7\ J»?Pgt1g83,mA5bmߗT5^6[IHxxlb+A1/e^YM=Q[=2-aԍ، k 3bBςHuAk*-8nj:FF6 H7Wj $e+XM%Ai):mBS'ۚ*9Aӟa3g}{ i gS\@ߜDuo.jtL8xA%\T$zQK mmma7 hSBOuxjT[Aj`*MGXZ Sn)Hh58: vc(Uz۵ 0AVgWӆω[s ?:@aw{ނSϾ؋c cYy:I<[T-X;,5r30ɍ{Ol|o+_/гGy?O}gp_#(P"F`2b+ *֭4xQӦߦzT29 GpS3܀E8bYx5=~^};s?W[[ $ʡsg<ēϽҥUN}c7ߺ|ݧ^z՗>#ww AC6 xILNU3kg!8Љe:H^N 8UNBBWOaQ.p@tě,>סXM,nom;c*E2ڃxHOMlBa\?O G;wwE nA˯t!<'>p˰nfAr;Јw-%֮[T\oU"QT0hlZ`puߕJ/d4 -}bң5efkUUYwFB\p4`z"Hw҅W/"٤bjUC]ÔrNhl*$NG=d [#&TU6& V_R* N[r͆ 55譇UAts2 r/6Q sǓ4DՉ23]bFfLȎr+>6m?\̈́K}j@-bUX䆅'UhX^_/D` žGVbKjFLG8KMlW$MHTʶ9%Fct"^2 7HdIe`zxb&b}0 ou:nm8 s_vit͏Sl2'S4KXFآLX/]֊ߧ*DW3u=j5FU? OP^ln9Fbw @Xyt˽e;tDZ>{',{7,3D:œ,Qщ7"uV m_2-bBc:Yj0=Ōhh`aħ:ڂ3׊z㹂wpN(C.Xc(OiSS62p-v3Z]G,s=D68z ۻ?|w8\ 5aV~)Ѧ'}-2zzA nGKwUP\9z>8?[w0G`.XRŅ&/cSD98_ڣ>yO7 .zW 0!}<2 Wo.8Xl(s01h&@C;4S'x w@ )Χd$X ɯ1<>D<|˟?7n?`N߅_;?)_Woټ}owQ 'v UEzuC7`~umw 6`*\v+ \AnGO<N>oΟ~O_铫+t?/|yAӧwկ>!аq=E5WePE&kX~%#ZiPq-vv^;)ArsO=b 0b.ܼoɃ"I'.\أ`Νm,ARGZ`EEM쐭X=jEdiZXsLJ%au0Ҫ]K>68'E/R|w ߒæG|d|V&׃T.@V؞LG|19#-S6H`q{^sTwJ*h `0NR}oiFp?:V&5;4ח e•15zEU#cbJ靡⎻oQY@ީϐI:)J 'x2OQyU Hkje  bC"fF>EУcfQSRFFZœ8ql2t XXQ8 JRb CۭgE2J }&, >gӾ.CbE9.^XLUf4]76&ߋ=X̎|pȍKZ\,B4!Gx:2ղfc|9:FZ:”]1BFP\TςqBd񱕚 3A#G%/U{ \3` |my\A==;mQ>LZ9[F#Yf;G<˵j1hy]cP6޸Y, },α0"3 =gۮUޏz<ۈ5hƾeD(D P4i0,VbeJx R!Qkן|W` )>Xg&O= a xpQ/kemk#,ȱʧ%u7X|Is$1s/"q"};>;PU$DDS @N@2><8{[󕯾}+:Ab;N@^?O>U<}_x/W?3Ϡ'؞" )#@6Zy v{ 4uwN`;sfU2_`gױs< o?O]{fͷ~ k8?܇ΣZ+IXuWoi@"-*u[rws%f($0V->"{B{И^c2qCd4`IlfI*3h|hi 0hQR zl(KPsY=mV"Og +ˮ[W~%L`3uhJSUe@V@[([m5|PQ@xUo_b~bȋyDD5:V 8], m6J/iuvG6«$9$|ؚfuѡ̓6Woq^OvzQU=g4ޛ`hk|Z2 h4r좑UϤl0+zP7K Qq{_ژgia\"![ږ<)+#iSSg2)S-6D4/JK s >ϳ,>Cj; n^sXǼ6s3^o*HNQZ3]1. 3rtfVd"~ߗ-"풋Xm[\ @A7:e;K0x=<d&Lkj=P2A8[ۺVF>T!=*te,#LSC7I32rK&%w٠Q&$F @;HE,BOf}Hd&6~{e6]OՊfb(]%P6LZ>pbeq Rހ] ۍaSX;1^]R}YY&ZJr:WDuqEp`AK0zc"؄PC_.$ PvC9-1^?ӸW4F찛o4hGRhH3k΀ cbKe?k2wO|ʅL%h(lqX8Fԥci1%vI pg0JtdwWS g{1r>sϡ Դj4i14^cG-IиQ"IYg} @X*lC|T^(jQ<$'FlUḬULxsFW6#pv 5paRn_έ?ҮJ8ޡ}qD&R?z ~oxG?ݾepaڙ"ptOK`C@u>`ɃWNIN`3 dp LGS_~#/G8N88ubG?T T`,?:wn㥗_^$YxSN8Bn*?( ,9 B_3(~|3Ah@6ɚGN69T<K$N?&@)ɯSpH|pt`p9'AvDw+C$>"n*s>2A9@S81u.%QU*܎C1@ɿ` H)0D$~nh|jVeLɲ³ vX* m4@J$%,,1h ]!kZc @GEa,'g,)35yՂ/Rt.rsTiqǵ"r`r莶TQeלd!tTA@Dzީ y b+\ck05SjѐeDx40 ?"257=eNk\âTtɘ!ɰO?ǦZ$Yro/yNCI<  W-y WLSVjԂ:Y !FдNO~n]1= 2%sFP|NDh=k?[ @p|w\68K#bt[Ѳra جZ$L9z­_]7-9̣0t!/A0H M & LxsZHѿ(M{W'\,#:J;#]+%5W8a23Bb0&"0I v7mlye䈮m/ԀO`+Mܟ%L|Gx3s]ظwoP!;(.C:$çzx ΟyĄp*s!mklIME[+NP'~'.iid M0YZC8N4e3 ƂbP7,Ac0H*bۥphe'1^ W:Sܫ-L +%UHTc9fX)αzŃ2q=P=IٳzzNnC[)4Eї<:df  BZ.|ot,Bc 8?ܷn\O{p5)nPPӍimnC!@hj%KY"ABNtu BMBz Oc7pwmNԐ︓a{E簅gwʐGܗ9 q_CHF ‰"Sq`!RbPN쉇؈og>sU\іO>뗑C9}1sn?|]+ s?Ol`*t"Fs/?ܳcg{Q!M3©W.]y$<.[]pMÁvlHӹ֍xx=A{s^\J(:pm DJH7$d>sJt9]\Pj P Ƒfl3(76U$4uHqg'DH9å\R+П!JT{9>_BUVcѱxCd,)nGAT x>r;w)сyF&ت\fuq̫[Zq^{)SXk\*37"(F+sl,@vSD! x+裮agc7.|G~>goa\,QKrb}fA%9s~ao&ցHkk>ׯ_~'\w78AU8׾sgsK[];/|3\G~WlBgsGK QR_5 QC!]x_|C?K0s*`AS#q(w@D]R}/rn;'ݾsO>B-AHm!c[Ul:DU+"ҙ2OGe2(Xoo(ϛTC-UDo)9_eQ(+ g2%+Sy*O- nwQ4raif ʄCwpRy HI yO~i"}?B+ŐiWFHw܌4$_ e$ %nZ@VxBsɑTu/R:v7k.bBzIs_1/耼6Y{!OpT fp_:jκPH~UX(teFT3hVö!5*5hKyT`[v^eQ\ϔq؄fIɌܸ(D?l <% ! ]G!fA/tsNv|,lPDP><qj!5 nܠ~!@N^z>_Yt窲^ /!I\@hnAz!GE`gڈs'|LB5:R'ߩS!-y I5DUequ+j}nacM(SX3}yWi.GU2XdΟdzĒO^'y:?بYm'eQ7h1=݂a'TvX6P'3dIJ4胴7Vބa)%T#E( pR,Qe)GcAY:Z]¦북-FIqKeӻ]|q G̻İR>S]p^AAz 9=B1S-Rfb?drY.>[WF4)8l '~ˇ4,H0PԀ,'Ȅ .<#"w0UBMuQ^ON//a˜A~Rž+9xG{SۯCD{HR gb8%-V&eAJ,r*K٧`Dc9C I92bYPm(AtyRCbO\sglxM7wBᗐ5a0Sꘅ'EmH~*22h]%`%%A<Ķ-IxAz~&c=jR#iIoџŞfRw5Zt&* XFmѻ۴Gtmt7-˴?9m ,}R@EAWKVMw!vʖnدY>j@-'Ʀa0s>u뎅}!sԖ /|L0ytSFji`@uԯW[@uk4YhMqH%8N4 eQ6.AKspqTN{3|Zk)A)_vmD'X.qЬeSk;)>=:~jFsqAڅ@cDŽ5tO]|Éo~"" o?r4NQ w6YP#+:|G>__yW^?q.7 x<8rxfQ&c{ZWV Q#Jc(ߞz|h]}ìY7`Ҝf dæMA Ȥ9~Љ'QN(^:`~: pQ{4 Eɒ,Qε%Dtب4I3B%R1QftȳV7ۚ)Xڈ$LNAc,-khR?jX`K( 6ETRݨj[UBF)ѯqE`*:a> A—m j6"D)i˝UwP_@xr<$iE]HE- yאTl,xTYPUr:7[H&>]e8fA مa/ []xp 8I0Jutn`iE~4Yfٜq)[LF\V{ t22$3`Bj i3d kLOAo.z+s7acڅ'm$be4M *3]?Dmr SEP ]: 4CLĂ֤| tYË ̽]^:BEɵ^4me<֞\BM  zb0I G9ZP[YX*:.֫Jjz ; 7;ۅJ^8V0K=x>MPeD?K0~XAw4hHx%+{7EBUc9z*32)pOUHc"/Da~CL* ]zG4ڤbOwpĖ]*ZxYZ{h;CͯC1E]S-lO%$ (4Cd,_T-|>ao~dir.CÈ ,#cT[ۅ,/Ȗ3М'ܹ/]pX.m8'`!z$\r2@͙j1=`CG_!x ~@F6l]pK6`Ax!}̝;_?{'MF'Geͣ2%!FQ NBfٳk/^:dž܀*ߺ}?޽6O\!p{{{Yz1wi`K.XAkXGz]ry%zA∃M4z;`U>`&z }S\vegg~'>zyrx[+o"`|} $DKg$0FE"bq2"@ wFfsz* Nržh*%!bޞT `Wy˽}d=a"8s%&C^S#$pPj;:Z2/{^)m{讲ƒ klV\c**0 C|Fi]UfWXPլ1%[<>Iҝ܆cc*QhN] Ϋd6TPkVnx֧NZ՞ q2#+@x!!",BC|7D 82/}F=m;qCS_pI,ں%, rfg4^|칍j4Sy|]<:ŋbVI(OvLT*f:!hrXbC[[{{8a,Q33YAn7'x5gNS)  dj !3:tfOy  GoyVAl6~B0][j^_ā XpmO>R=1{=vҺfp`}c'xg??ɟ&/\ӮsP\|ouk/2|BZ^asܗX\R< \2M42C΀ Iġ#@pd% Y!ܹE=)C!H F9ę843uBa GAvyXI_*$[dpNΔ0*2k%;솗4.-#PE5U %dgDUJBN0sY$S 3)qGb3F' mLn'5QG#B{CDS20^Tx_H .և HNQ M ŝH`;Q?PgH.D| غ"d0hDSRXJ\žY]dt-F l+"s.)'cL$q7b =KrcGԐ J%/bEl6 ;;[0Ul_{ɠ#4un6zˍExq# 7atfqG3fNhfӼBp$6-؂ⶊXzfHٺGa0鍵׮Spj"us$/=ML x{S]/(60&Z+jss0޹{w I k:pF7m8l?ܓO<'RQ@"aԚ. Dj@][b">=[Z ;??~ƍ0=$Ze GɉO>3>w%"qA՘b̓yUĎf'ʬbKř{p?'zr:2P!\nV puXl>GF 'Kg,hG|8縮<|𹵅us'w}tuWسH8!}{H 5}@YB eUŞBb&p nȐÃk ғp]_P}FpC쎃߅vxp=i9G2@@T񌿳MQ%R$p|G 8zGF_d՗1_(>2TɂnJTGR5n(j!\sv.$zf'%?(ƨe{au-W5+:Fd/hqP L{|Vx,Y 3uRw3̩,1Svy;?mfejtL8q+uZU1!nFm8Ee'rSR.큃 sVM/[32w=:໨e:b+i{,j(*eAdqlw]VTi=1/y- $.- ?<єbj[6[@]2n.ōJaV}v3IvE@ÈjiR.Z_<9]XIH|Gm:n2ߚ*KXK,Di,U} K(aK1gayN^XdT}mzQ9YVkequ94z^v0 Oo~F["lC]mhgXY $ogɱ!Ȯ/KTz9O9>ӶY ѝrPB/. #%J42U:̕Pa \AL B[Wj9[n7.8}fIb'M&6-dVN5A$m#ވ:̟vJ"x?r34,! d} PX R5WOz8#nZ* ә׀:-˧I{.rCix^XXjGgY`[y__{*:KtR$UhC0y:E!]n6I Z7x뭷aǩd ) 2j-Wd$`m}{DdF8Ȁ'$Gm|]=X1 Ie zX90(!8C^0R U%3HZn{$q (`ANcWXy\ y$G4O o+ X'*e5ՋGc@ȧWHLK"{2Hv!Ǒi cv4c](|9"$KRcM<ih4 pO ɖ]Z? PsFV Xo )2[{{׵9#´#@B>޿}6NA]7/{xA;wؖ1?.'{%ᧇl8AxZ4,JGH_X .  j `A!.B0AL5D^)$ `7W]GwS2b|D3a̓F^"YQ]P˱8ݏ SmZ4 EpQڕڰH_u]G~=j+Vm+1y܅RzAr/14[W86V;@h{u¾r{ZaqK̰pFBp0^[ώ&l{*/br*A]bX%\R(n"i?0{_EaZ՜ %!{`.[3sV0)LTkG/˵ˊYuǐpdzP+ciPwv{br].sRvNlwZ˹'5hGn+ YBo}0_*ڳ@c၊Ky5e(!4/!UxR3,M;UqfWa xA4u~h5oYe ˬo'ا}ȭqLgb׵%㐇Ǻ+dҭb@]s«(XP\M9N]/0 8ˊfU͒M\߰HtRWay@٢ m)5דv0mxۤ#JBOjWijb˛NcHO HwKwkc4) ;l ~ = ԯEO`*}*8;5dz]mr Ga-<&t պY1cS=`a>W`}{n;`A MԶCDqz5oMCKd/8r@h$ O݁{ׯ!c_%<Ĭ% H^X9SO/~77>˟48v{)Ya~ޘքx1&xb)pgwwg" *IHQ5lOy *)  Yb1" 㸽BPM57-Z#qE^Ǡ*(CE$&9ױyX78%8jp9"8iRi։zvx(ªnBYicj\r'*/eD 6:WͺF&[vK~+' d= ]jej%%fwV2 tfH 2~~4t4IF`!zWI9ԂG J)|raJRu@8 +\< V1HrrH\lq9[kg7nM+gVmC8!+b<bu=z49Ují/cl5dz b$Gabj'pV}uqhr-j8xeAVڙ(~óilA\U`F!Ș8Yޓ3 F<%nR`W\a[3e@5׾]1_SJ"5Bf$rBS]V1RaDD%ǽS(M=8C[vEWjʴ.H-rdcw+e=u婺0iؓ:׳S:ڪASwS&Aۏ͂plTMх u, \0D)) z' Nh$BAz:֔QݕENYxa:ʧ#Z3l42 B6,ȉFx {u,y _~ WBSi2L @Dj NyV|_?ϼs|>민+;0vqùs71yԈ?rc֞^0[N“;s 6dDFO>uaղɵ10޵ZJ+vːEAdȁ 9.KtS ޝ=]I떈[ݻ5JV#t UOz $C]њ{pƸq4_h0e 6T]-ԠYbe1$B|GWjT sc]^xruAzOߺ{ygossw[' .OX㠵XU;;7wÅ XI}Cdvc ?JB܂Pq% F *Ĝ6 0 _zv"G{ATBAjߵBwGi=BveX4؁[\Mt=gr2(LF:@5hx{F\ցo҆J6"TtVؤ3h7vk\Ij?N5[qfuؼCS" lZKW UìTS\igDaAJ' ~R.[_ꕖ6Mf%9(ӥ0CA}rI DgV\P03S"SV1cNlaIXhJKdnˑJdx)m:4*R)BiY"mྨZ%GX$!&6l\W/\} }Qiy)R$8+EC7.gF>,` %piϟ۸tYDOtP01ô1Eiu@p}nj:rMflMA%i3PZzæ)2a_pkS-Я3/c8uvGH0A5gQ6l(0ɩB~m PĪ T@K/?s?|W#C4v|c1v?_gg´7Gݼ19xpcPƳm*Dr N~/^CwƴK1G_OgPO73+([ C؇ȁNi 9?x7/_‹/b9g x’r XIS|$\{ 9l#\§Q:"{D1,fvSh N"d;LΝojHSqsf2{j넽ww!;޵n9 ^+ϓvWyQX1>b>/OMEΪ$HW<9N8Us M(kɻ7W媓 \szcBl5~u+]c\zGqeQDC}CDortASlqn@ȥeK=ʜ&V~B/QLtHߟk^EJ-$Yhyq%bd4%q~L{*4/hV`n?VwFDJ)1`LOiDT`ddB|2RXguLIfC iD} cSL]+f0慆aP+e{߸Jx 4 MtXb jPyWZyYis4 ;زkc. ݗzTW&b?瓱h7\&htFp&:4Fm!?*d\W; u7P[Zd0|O+hh?LQ6ذ_tCIC!3˗&Ň7tH !Aos`ͣ|x!6M* Թ+^_I*1t_f]jS]\'6?~T;2"Vm\ dRok^46Px\ah Ӕg;qd-.+n--0K>qqZ. 7x-\NHa!6WоbU4$1asN*(;uqNzj磾OD(v1 kGK01{^"+^Y\$ 'Ŗ! $ l5e?9\ǒQ혞:mc~Ugrb?3ΫcAUO9yEK ] {jA U{>Z=fY/Ljض;t&p*S0ڼ _xG} Y|\]ǥ/=D̜3Wru4qxHIDy+_k_6 9vܾsiyG!f#FN?裟kKK~P^xs?ŏ}죯_y镗)!Δm(™:> Unu)6WjTgd:7~| /<@uyY2dQ7UAJr,hwz,g`"ϝ?!g=tEͰPsl({Οǹjyp-c=3n4χȂ8;B* 3sRqR28ʬ?1fT-PآAjU")Ptno3 &WMC!]( icFu5Y@)Iv(ȡ\`aY4t Xب%52ʀG`gl?YJ7P?"faIV*)U-qj02?C)|ÿX0 OjLU#8*$:L=?#뚂 pF<ȨdBA u ;H7(ўW@A m 1Sh'v#?JI( +ED(Z/5H>474~XR-`9".apsBJ>u4#&LhĥnhJzRvRpaRei!WQ,R >iϖ!`Lo\H 3(i;F;ӚHn9|(=g[|aX۰q\|Piue/T6i{y0j<-1Mр{bhSk QhQ<`yOegQzi|=Qe6̚xj0=Fx( Ի0=QzxS{Ell%-"ʎ#Cvrm ~%nD=5ϭ4["qB]Zp{,Hys0St+c?v-)ڪ-íHע+:xÌ5X {ĉ'`^t ˄ ttw8f} X+` mL@xV1k2e?xh-^sXqӧa{&UۀwxᜂRۛo"{G&:_FstL{Ksэ<=ӓό{Gf^'rE,LVʙsg>7o ]ck~U1YecSO>SlѴ'6tL8wxDEN vj[Ӷ_~W_'_}U-޼q}7^{}'?O+"V,CcR"Sp*&xʽѦ~[o&xŗ."]* Z g|G#3Ń 0"NZp y8&:6CRؚ{O<+\: ol ReO-Q[~=$L/'+@,?_)ʸcOM12zY[kRLd5Hyg[Xg9UBrX'1<\XM-(N~ T[0JՕe ,6#A'HWYWPj6R2ݻ{X$?ڃ/=PgPB>!Y))W4f$7R% L̳Z eqpm eBD vnd3zYt]]N?V4a fD_ơ* 4/\i(ITqBqOއVw% {f5Nf(n, ==sfTrG )=rD;*ң%UGUT$a,~u0êi*E@)o; mquX}aoͣ|2GPF-Vp{!6mGuUO\X#끪+x=@4`+dW m >Wb':N{*hXXutLg)EBDťa"Shjg̰~'X:":ӌCC;e!}(8m:nXOus s"$}*C󒅧R/x\ª#5ql3s<U H x?b}O>Z~=$^!w~yk.շ`X0Ԟ[#DM\ͱ=27i]}UZl}3Vg S ! bosإ֙wjhPTs|z U:fT-OkZ'"`51^q0q@/*e$M,)QExPMsT~YgTG9)EC((3)(=B4 1zY΂vKi1nzٶ2hiޤO0%@W])ײPʈ78 4 d ) G CMp-3"V a(F8Bl,>J;'<AaHsG=y7VN 7Y)N:*XƘjlXyM7jPi[ ]!Gh3/i/2`"O[N0i"6 J`tp9L` إ|euRA.J,>I'B9 5#NY jqTA,ŭ|*dO=u%޾} B8syX16*훈pZԿVh= 䈕6,$ܻSrʧ??B2?%U>C~|;|!AGrU,ˣLDj2&ٟͿ?}g~?s?{=勗Is㸬ql!X*[|a ҁyթЋbf]V]$C53KK WlCJ!*WI~ nXt Z'3NnQꚁYN(滱8LG%0ǚy͵B_w,cóa\1'5FLn*Rc\Jz~67fnD7.nVJRkz耋z>x28飝,#I"N(̀9v DI?9,i S\_]s",~Wߋ&1LU5 Wqpty1 1,+2 U9Z?Micy?%yq~ÏLoW4Rּ֦^am#nd߼ڛ9,Hǁ \1!wVRqqdM2F(852&l0F%9bBN q'hoyFh,\ck)4{bEO^gzx4RD]30E!wnjӣ2%ӯ[A9VHM}#50pUU"n2Ya@no9Fc"j!y[Y F?NuQ=y_RB J35e@QˌӦ۵K(AՅ.I !ӔǐJ꩕ԆJJMiȠxs8 ǒq1p jЙA*`NCBFPؿJ<\3r)k#SHYkE]hr>{6TmԢ s*Rv[ V;}z'v#\M X6%m A.F=)U*B/)ru!51PZ.'bdYȺb\1je5ܗN، ;|Sufˆ O}ZZdDu]#vᖈ}<8liǦ|6⋹s;rEHE@~)!Qĵ.tqㅇ\YW4?a,^6;EyeW@/'n?gVuz3hȟ\CԸTG9 ;X\`14:)<ófSQ O *<67Yt OEp)aXXy3ruMե~&e27ÚBrEQ4X(TKG}ܠج.v e k&ˢ1uZ$5/!$8Y: m&i$J! sE+D#!7b.NT1T{)]rDc9fXLόM[ڴ]@ԥEuc]ІUթV"Uq/cD1f-ՆY,$z>t aC/2g[ Lk-@DQꀄ, Dtd͋h*U,B~ zO\d+naD0a<(Y8aƽ %جDC-wyd M ]o}GԇU`f[w|߽y F~=쳘BVx7\4oD  ` M@ĵמ{^OO.x]xDGx^M0a}% dCw_d*qLHuCd>G}+f|<]gL0YcKi >$X)]O0冻w޽7 `pʱ|PmxE -/J: #ZzmtxĶ^lr"tKMeRlH6G[vef^Hi|n#OӨɑJ1fArE8{tWpQ",f n$_;rkԕ) 5+ bPrA!wEQU#iǝȇs%HE7)IXr6=NDل 5R<;LR*OMq[:K_u=vf"bͼ'%Rw|Y;lT6J;uUo=xc"&{²<rw]F3S{I `"f@XOB~.LkiG3|Hͷ*:-Ew61)n'4bQ2K w6PZj!ՖΚ1\3L<i UǠ9^n6qRtbˎӓ۸ i'S7rJ5g5HKGDv䓥̸gGexd[<`=;FzUSuz_*c;P3diZYJnK}5t2_]`{dȩIm%ě6! F8,*#|A7Ω~=Oۀ"o6o)8Gws2[?Izzr҈x!HpBWœꤸJ 2:-p?r)&'*@aa'um== /bKx Y"&7J §KKt fMIk$E Sk39QUQ,aŴ73x VH2uUYGD!T*Q4j.%XR0<(37 5Z9-SH1j'Ӗmdf"B ~ F@nfiVc[ku&E6[qy]{e**N;_%lIg^J%@`6,`wY˯) @WL%`$T^НYW[\5Ōh&7201g&.ϞϩQ1Pgn-p̔sæ}`Ali2KN5\XVbf8 5,R $ЖUϗ<;to&  W+M+Ѩ̠YϏJϤ-hHǠ1nX`"q lKMO;\%<^mId$?_9QaVvɓQ kbԕ6bbїPl_n& $ ZyPVF0(! wٵufX4-qNK][]gIL8=2j? ^U`0qf&UL0sSz4!zc˦RJaH&f8@Y֦Tq'=ྸ'Zef8Eba6d^t,fc`35&>X9!u,sQ b)5nWҮiݖ ՝OQ.{HFpQ a?m[Ie*ҍVc[fkoSׯ-`\V 0)ܚgŸty0o i`ZuEckd32{'RhtR hD !#co/j,!/AfcNV2rS{yD#3Lee6<#V&hti7>…<u^("K֕AAeFa;Z-g "E'\#w4=#Q0<g)[{ou6X[8m4l>a">U/ɟw9á8]qvk.m[X*ld|p/gy3exP{7_©]TvCͶ-m H%*j9b 5Q]`2赤p?#n -mWbQ{a _rfnWeJ,~!ri-JVPXjaXQG^*,AVt)R .::JAR܂l Ff`+1ljiŨF C2<^8q/R|ٍE;a.]B,lGu@h$RwvYS$, lu&=Ջp0k,?e,{^2:5NÓ5AP/I5f}?wuŞyLE#;? 840Ce`O)uQLk?;>Wv! ;7HTXL&K{%!)[UuaTp=p"Pc;Ѿ 4Oj!9fMnz-aV>w!A\d: d+ڈHtЕ槜䢉&4 klfoLD* &`RbӞ['6+F{<,؃GBSCAwTw7>@͝;;Y؄Sx7ܣ}ipރ#vp1p"Uq-g8!Ui:_@-Eq*]q-x;Ru⅋/}्1'Y""`O^ Jļ$$bRV=z@?0̌l ol ր)+`$c KN:@l,B܈F' 1\$" ѦB0|"yޞըwO)ҫبFD7siݐ"l3Rr8 MK<(} !ݥ\F{V{լ6-dNI=@(5waL,-FσhTXdıGw vPU >B wiuZw"|aZ+Y8ҒGT1uI@[ke/ؙC*w ,'V +X,W 0͵ń,M#wVۍƼW ۲rY?XEtl9L͎7N + > Et(.:i+<:R_]Sքz8ZRVs\G2SdJD0VPCZ7 ~ bTfՐ ҖҰTV[|ڌ:8lƕobݼ7ak7Ʌ /{>s #3H7& f[L10Ě魏xWުPF]alSt(D $h(̱1iDy#5d6j4tfNX7R fX],^RNF6S^X]w"] pk;عTdfl9g,Srq18GN`ӑ]OCy ,*>.CgQ@{p+9_tm4<8E2u5?:F3 f66u6G\!b=N+6`3РwچYVxͤYGȝUY;f`)AזstRtl4q '3ʖ%D?THhc{GkXakP4&GF>V-7q|}Rj/ ]]PjFГ)xz}t~=sS96T(AtP5QVPrY2iX_?{*b{< z<Iz=ɧ|Օ5xZ 9.uLhR)D3ژրׇE&d^بLtz7/gm Dj)BZW6X* @Ad;w#אIrv}]d9 DΦS`?逌kjPsS'ҵebΝqQwJvºdA"n{H=Qr.O_TF!& %A#P_:Jm9=ȫ [j.] RI NґU2lXҒ%iQDZ][e, !pULx'+=NB 6!/FPwfY4X nRH*lJz bH;ࠁauNJtՊiJWyQQ=!&k.R=}Bj/5R}&ߊ ~T9KKuԌ0)}bH5+NUj5UmSlsGwt[zaY5n*4a2Z{0 A`}='`/OapE#BT 㑴GnY0~ 0̅i`0|0_x;kgװo5o\]C2^ڃ]9iF ptTR>w$00IܤH"mL!ad[4drcii5*< GÓhdxiQ rTf5#=7mSG:eܱ 4ȭ6t fmG yƨ:Xߋj77W )]]CoUwˈBFPRzvcB= fT5]γFdmpW a$BTNf>o(*UǷ,HSuBv(=лƅj$q ݭsm=YE3a΋](4n d56ODwKV%Cd F!.A)y!GXib(p>؄޳ײ斔hK=KdJM1:%엹^Ped`;=, .ٵgnBx2ʘD Z8~@Xp;[oq7os Q]p2Nc6H"/~>rE1 3X4ci~Xd:x3j7j˃dz !h=Q #11ʰ׋rSpE,+/~XwX[YY,+vO91i@.8W# pLƴU˻L 'BJ{Q1Gb92>VXdIHj -,IFOm9R1fe,p*"E<|bsά edKݨ9MỉTN't+;=)>vuzHQZPcEDȄ؋A2 6yveҵ4BǤzi]"wDIm{B[gT6+;`9V󫪚6a%5Jd;SMtƛ?NHIDATP2h!Anc?O@)PN A\{;Z(Q&Z ! H/.mU)S.c6t6YFe v r>eHH't0(@,"л|_z㯽_~U(q &5}CPs>(_ŗ_~w1V.G~_~僯"W?/?B$0tf]( δ7zȚ6|HCklj }+((\WZs{$wH(+VV yrX J3]pTHA1̵"чq@EU!;jvZ#XQ!UMmI+v֩It1جum#1 Ⱦ؈u7SK&Y&I{oscbE UTUj` K]hy}N! t9D+bpJ[gByr8դcx1k7>}41e"3ag'͕ӈ"[zەLUF"i}0Uŧj9*Ŷ$2?iprzšz$X&`hgoᴯ[䍧D^AKAPP sv1v?GUe#0HCVG84:N1OrIܦN '`XŐ;c7GA!Z850\CBOt4*zИ\v̕1TNS`Hbpdr BQ̹-Vtܻ^ua 6dcYP^30%B r(:TRv-_gV2xLK[{eLe$3\Mo*"-dà(q6#ʊTYCpW~X 07)s~EJx@?8BqimBP'[Jbj '!2㛵|Lm#W=%`fz.Ȩਲ਼GpOqT U5u3 ppS#^J ul۲0QUuJF7[^o5`b^t=vܒ((hoy4}~h7YBܳ=/֕y?R!ƉJW<%*Yg0Id.vǘ)412q?QES]FlκaPDp5&U_ JpRh4)n@j1?7J :Z1E_Y@fWӤ|7ad}s,?H)[\W\(D(C~Β;z%6~QPmNVv&Ҥua (qx&\XyZPktx- VбΝ֏^nنoM[7oO3hOq'g3#sC;9%M%( \z2XlhDq̉ +hao8W4K΢)>lnDC,i28+d@"z<I>%pތ3DO'vТf9Q %MX"%,35>gJ/UZTBpzydJL@"l!zY,!K<!@+Wew9z`h:^5eYYJ\bP l=r|tAP#LZLA"hMtkbKdTT{CbAT hS0Tv֞xFQBF)gb@hiuMYc/4\l*Y>‹=ou꜂C#+ "NN!1w% 25Z8Hmri.CLFR d|Go%.`b[UI7rbSrY,>閭1m'νuWO %s/|c|1؛?h`= ǦtWjrzӀ*btőwU/=ЅַKov:̃z3sHkv2cDQs.ٲoTĬR,%vavPPK'AM$!AYoaG`%Kڸ)Y{zd"+n&;G6!4#j P1Bt62T/$68&l): D+F$)qP7Jc2XEE=Ċa+QƐ mth7]hgx+e8z{|9\hB94$vG'!~a}*vy_<刂LL)|諰!s58F ±-2zR(yHl- bY2Nľtց)MK]gĠ%d|!'Y_"Rڐ{l49ې@ (jtZ7/u:BR>T0iw_v'>W'*!.. ЗRb:_s|={ ~r̩5Ԅ!.b!أ?~5VsϾ>9ݭ// rȭ@i찃 sc7/ј/gIWǑ3*JoFh k])1Kod+)? PHqyqȐ؄,;K_\E'O;Y@A"H<25l!X/36=*#:N)ڐf&c$3Mſčd=c(u*Mh7~h:V6e|#U'}cnQ_b0Դ;BB1/ö&A|Mp2YO-{jzjկ J/zNh/'۷`%kz*J]KH l<Ļ[j(Y,El3D^hLD&[޿B rV c9F Q=@DLJ$5fEKa$$B- [VxT M he5?[oFV 9|G,9(`D,8Y,o)c茌Fy~if/)"xyq GI̓l3b3:;#%QaW-6tRJ5HBq5 u3nR?i_jGHPw$<{ô *EUkfpD]N;F΀xs_{S"fS}p ~yc10vuT{Y#gba TBo|wi׹Ub}uo KP=bd,I4?YkU3j*%.΃߭*QPTKͨV$D8vM8OD4aSV҇a #K tCnPB ; B XŐhzRטAD[`/h.T |:( b]q!6;2m2 dp1 3iO2r){zq {bj 9R]V|3+FLBţ\Yب:yT<8Ԣhxv%5v l$L)LHVG@^/2`˲B.;)B{ac}QR1L'H[`VF*VgkeLVV sa}]Pzp<TK&q\L(a"0G3j]w걸Je"ΗaCgQUM}e~yT8l@V L4!LeЂ9@Dո)Qo/.R>GACuA \RéT%_rnpkװ h_>;/~G✋Oԧ>Ww毿}^BEs{Y=SRjlm^%. ]K(:˭fwP]jM@Hyo+w"m{\M(̓\c_Gc&LAÀY]3VOGȫ1¼=8ڶ=2CtlMhVI35Hd<*3 DkpWt|5BRrj͖FH)r*!|94hE}Qx1:ޠ N&aѠ_Jσ'w~vT}S䵗܆*M;*Tb Mj45G aJc@vAĈ䳚pA4* ci?= 1v_ S4[ݳ c_9pº8ѵ2w\Tb>Z;L/A$&`LV}dm<Q" :t'KTX񈶎~ Ux URyDC `q"1B3,_tqzX qfBɃJgE\SJOHO=&}XyqNPnPw?HJC354޺vP%2T  JZYKHK38[HI8V$ćTjɦW <|O >51r6,%dbI$$ #MKAsxZĪkH"D4wٲ:bqcy$& Yk[fe]jYdd&M- YPf]U]bApD DN)2#Fܘ-wܛ'ook^˗1{!ucv]WS'WR\RXaZWf+ٞY4BH@?)A6)F}w[ȼ_0v0t&]hfO1HOBGlAW~K_׾7+[ͯ7ɗJ?ݿW_*w᧏?xV yTG\8H)ƾf)!F~s[SPM*tBA?U]yZ4̺ZyhGsQw hBJZ5/p(jP Pǎ {lyV7a-v zӁ$8+`j؇D`F2ϩbKԜyJ*&p<:l"a gMH٤{B ;^а1jI4Bh+'k~척Tflj&8m`vb2כ=kP܃qZe7jTveIA}qZg%M =񴵭I& pr(!NX1OߪGg:AK 6DWzChK'Kʿ#1*awh!A4Y 57MKuPuFa ba%j{eNc%vƭrxFL3Q"3;_ّ bdѲ(WRofЉ9U pՎd++ƣƚ3Lh7@v@Q-Zؒǔ% |Cؖw$@rmNa4d B2Z xW.+sЯl?W!i@ܩd6qUgh=i3g &!.OoC#҈u\MI*JHȳRpSuf#g7|@r'Bշ(;h޿A~T:/Q+OraR=ص*D1bx]O#»sL <sЉ޾}tc{/P ¥&b%aĠCRe tBV  s+ɏUXo}[س? /X&*M(.IPqx8ԙwoߺSO}zxSL=oZYߚO>Oş^z#Թ5^!ʥg~֝W^;s+<̕kW lL;-aQ"_`U k̨HUڽy7 ?gkG2\ji]P  Y8rЍ:c.VH%`;Suq|n|p#XȮ85nr2WG˜[-w퍄Q [/j~ _{-)mPZgKNak}aV lD})J!X 0@{Y`&rLN vZ}7ZH]ߩD-U$qMmoFM=^%Jb47lBbz*ꔖY:^fcI\ QW$"k:ٌ Pc;L~2<5rN!^:v3cnAoR1s=P;y WpP3zZm>VlԾ"C9,| LڱF`#bWS%l&gΕRyƍO|!X&nk5i"ژi T_шB X+~3 ~7?◿C6=bJM A?tO>կ~_|㻷ɼ/ǟ| 5}k~b/NEx_u{Gwn_Xe]j4:%X),d43&Ztz3{!o^;G=^?:*}n4*[! fE.ʂ>n5kͨR:>صڵtZ^nXH#6L*ھwz,S^E!T?:ѹlx̞%"aM>P>,7S0B.t"$ y->vVm"zU!d{Mt" Rs @jg l rT3fUX=uV hZv8aA%@%myL7o̻<3%?3T䀽u(0Yk|]`_ r 7<*K#BQ>&p =vǃ{#RnQ(6*BMnic``K8l B)3@MT)!edu]4=:7G!n } |@w'ߵf HCuu MtZBurx^$6ͤK_FHYvLW@hLt7=8S2m$$XgQ' "3Bv4*([+Zj ~^ɠO -0(Hla:ˠLk5:CtBrf4#aʦrn of4R;xCJGԣ^:h-ZaU_}5=!RF2 "hru[W-!-KXUl[EIK5/"t:ܿc7FcXAW݁Taz0D,lBMdv /}zKٵ>~fP'qPO}8VwM>“0è#f!bS<~fa/}X\K_U\szxtšs6ڸ_IR `3I 1u&E4Mαs%P:F?3Av\|OH/ryD!-;eVіWSeVOZ ZM2q+>AJȫkzXOW/oj^xw;_j؜s#Yl,TDmzOmxO|ŏ?o|><3?Ϳ_**Ut(M[g?'Op?_:D6= C*8'R0yvTIi)`KQ0G~fqiy}(&Յ6ݺUt5m+8[Xj{?*7_~]۷kw_5Ƣ[鈗8X}K;Y #%x֏yh#)!X Rf鍍EՏbF40#["{W p>nq$O z(܃u ` 8WuvfoD<0f*Y$&! o.6~fP)5 ;-f`kG@ށ/T}^%q_éFIx} ߭J嫘`=.C%BV&6Ws 9St8To%\ \᠅) PnF{ >)2he<j 14=V]*P͌ )\Xҷ O{6qY/ " )E2}C#8]$UDnˀv k(L,3LXXohކhZw٪]`;xwZȖmR&x +:QZE=86Y̷&>FVo}ҫUU41נ} '([b|`ڳ:sW`$3mǃ̼!ɸ<[f6pHrЇuS_Ybc> 0|wcC,fS\ja1b=qAqwV0f1й}*c"Y bB/;`HafQ6!ӛib?a#d`b V]+#^{N%/z԰6Jc2¨ CR[/3ɸ\>dep;I"` w>M\UXNk8FKTO" jH#ȷ "Ro;N@o9٭ק0gvsvxD_&VqDN_Gs`a2Q2"2qK.5 )'Q`ܫ_1޴[+Nj2&岡E!"\{5LtonQv6?+2?=| I.E)?P Mdk-GE& KگD2Op1oo~^|e+ Eļ+NtN:JwиBTz7;}+_מzuiG?PVqeΩ;xJ 3+QTg>/} /§>??~?O)5A1d׮_W>ϖ|?7'L8_ĥ% :334y`:F!jOSª,VMf-BaJBo VN?%|F҄aBfjpf J.f@#UOgy/ܳUȧkMC>6):Y'l;6~B!J5ޭ;M|eriKf [D~uG! R0蒊orYU mm6SƎ@orA5xy @MlB%0xk!u ^p~4N<;Ӻ"JdfcPttVd^ΙZeR؎t+(t״Allɥ#Θ5hW0ʹNl>nIu705K,G'#:'JV/ԿZa3 .gg{}s=HhvOB8^&kucevDaniNVV1~ UMϽ{ȥqa=Lڅ%DPs3̭|n€=XNnP ꁬ=N#cA嶟e2'Wf#MگhN Nڎ20/rQן-4?n'Ky'o w91W@Im Vz:51OVY4KJcmU .l*[Laݱb 6X, clOg}eKwιj`dxDVz12,ZeϷc"U$/zs-4'.,8ۚ βp9%_6kaunNitFi wX=23`bWҳG hrnV9ϧ3K.ZK,* ѮX e*Z6sTI' t<(w3X;~kZ&RbF0 tGrp &bsIL6חNYuI?sC(BI8zQ Ҙ: '$X 5jTZM"*RNz#aX85ij:9s 5LBg?P@nCb A0Cj5H}QIn=ڗ^#ZFߵ(O'>:n2tʂ!Dm<U\wys_~~?2(@ U!:tU=.^x|ǞzҹW_zsW]pQnXIa3g?_Wo>sw/_/s(9.jCMDҖYmpNUN5*MQD-KjAꡬN\ZOw(PJ.x1fI65ͼZu#[!UyJTmCq*0"ڑμԽ͞U+VGC:+NǠ,-!-G%qm/q1Amܖ ۿ^:+?ۿ:MçHzLlAs!L ?k;cf @P܅0 ɕ'FO ѐHIpk5$iy%SGQh˃[.iNPqu+cYiG"{4SY2}' ]*GٍBW{B'~dVIdE1!BLc3kYȏc6۲c|UPEnaIi Vfd@n8WhdNp6Ij8n+xTk\3\9{j#H W=+Z2`xG P%%IY0g9@ ؛|=Pc4 ;M%GKF rC>{"ln\I&k/GM0Z7-{/Jvn묅}[  ՂvՂjoCSX_aspz52 >GN4ڃiF@oDghP_5 wEhN˰Fl1H+8nn!E: ?JcޜpbHW&pN)zf|TD#綝|m;7'!in/>+4E*ksnؼ olVeɳȟz3z$mbwhE?Ln Vv#*v?jB0wD/C91 ,h1%|#nlk Nd7&9U> YyIG'\ܪ4e6 =1>T'qY>M,c CBiD00Wx"̺E8o8n5idl1aa= X @ȀgUBzyUҡ/[hYO dIL!,.4x/̤넆aEYBnZfEɑ,; T#ɞ`. Mciuc-Ut+: wi:&&+RЧi \7#m\G,#"1tVj88}H@[Լe'A3#ѐ #>Y{&[9GCn2@LtDsz1O?Pr:[BI8!D jF4Lí[G?'7F͏D(~yEyepU79e/~Zc$.Uo~rx _o~?W^j)^pv 9GjU< \|sg=/8Uf-)u0hjv]Lsѳ.i Jq Q.R@jq.>&p~G,TMG8͗>"T}&k]?88>,і/|1j\Žzv*%pXo5ߴ\BA7< ¸"3_(6>Wsq 3 %^g@ '3u#>8?Oz-egJ[ "M_rVJ5B B;Й. d]8ܨ26'~BPQ1V\#g f-_0v$[x.*HF?3 6:W"<ʳ\G Ney90\5L#\EDKa_ɇt^̉e4MrpF4#mb 0{Vfa@:IOM~Yڶ"OI.fK3ƢyNq'JTE G[5Qa0h7/-#{l MasgƘ`7['|NA`!Il ۇzE\qkeTKڏENAWScHݼ\ha)"<(x^h_q7+,ѭ+~0nr"xIcr" OܢScHl9e2)e`k (gֵMŮGP0Ήx Å@(N&H`1Qm#!ㄻ QC1g|kTsBƒrg@T(<+s|jC=[ȷ*6 =`ɬ[niRQ+&@!tkIAeTR^vz:GfۃXlUbsG5s CX椿|XO+2"F_LU.5r"A PQk &g3 YDf{J$.[zI 'G$/Gx-\A/gy,>74Ff0Fg!zeC|ܺ8cƜH6FYL!w&OWeq<VQX"m NbOB]5ZΉI HtyQqkVx7|3_?쳵AGPZ3Ԟ&(n: ׷ڒpBѤ\,2Tj_꺊UdomoN]˒Y.?9^yk_WgutG?߯*"K۫|&5t;B"!>"doIU!j:7 --wg$rm0ia£kȠSZxG%gs99:E[f ~{w9F1-Va^ QJݱX+C ~GJQ!pSHL& qQ`(u`_ Ա~OBʴ^vF.PaZRʂv,z9[ԧxx2!MȄDpeMjf\vԐfɍTŜOGCeٶ.̽Q -JT4sBeDy2zG{3StKH).ݚ.crk̃\]7uJ"`w 8Si2eFȜ+=/eL am0kòvxm,d*TiAC|Եf[)hN{.I <itҐN;W &ܯyuҘbq*6G`cStb#퉾Ob=2MUrc u&cHmt-L[lҸ9:e;3o2np%dvOZli_Yz$'sb&kȽ lˈ` 81$gqPQ뗫 ]QZG̱b k@!ދNIh:Ўx;Rb7؞$.5GYicQSnKOr< fèhwv Zk.4;ifi^ߓg'Tdq y:I1jۿrhj\[> OZYq3I  eЄ:U6`%dvIBXq'n" <Zp_|ڟ HNfۅL@%!(:wO{+ tSk|k$PϏcVEs;O:!bidfHqwik΍@PdIK+'yQ<$&a R^- ^LUryܷq~l ng\cf/` ٯ%tt& G 'Ua=/a Q;öq"8Vb m֙3jԌ y!aކQ+#hS0<>_1$ _1+~- l¹ebk߸j5Zn= st"K~#tM#5A8&7FFMt lz ^.loۏ:iJJܙXǶKxq{ Ks+ZNT4`+Ix1vvaMhsE vгkfmF+$ِݬO lQvt9dc/`j)iٖ&BnE,A *XhU7Fs1 @iNecE3[ӆp]l DgJtq9S\X۶ P V= l2ͣ"ǔ=>j*'04p]I}&oZ ,^`pohiN1LfgM gbjg93f2 >'=ūGЅ88{p7 xӯ-z^LI纛R1n[}8hIkB{mAq\)c%KƵfnܦ& oSV4JT%`Xs~RWa˛ZF4!ȣּ#>J xq !{ǰFs>\؇;Λn? 'j, yIiYc &u< ȏgp\w|nO<[o|k^+0IbxV)crmҙb5EK[g8UFwGT UE Ո:ӟ? _;y*xfbNġ pno{ܠ[O- |Mr+ܴh``;"L-8- h!gHu$JZ(E 5(ܧmJj^IkRI9 <[:KRG3)|Yfqbͺދ-)vSdhE1}2Bd)t[2Bh ͍Q8AGTl| /|kG .dIUE QB|g|}-8lr.ĥ1` | `[5w׹ ([%RzR * 9wۊʑ@zJ;[/L,FK =S,7_+pR|nΨ }L|"Ë5Sqj4+5.FN7{LRpM抧4)V]ϝ'>8825f)CV8 my (5Apw-j$`]>1¬Mpұ ܊߲5+X_=M*M!B f"D03aiX`Jb3ѽv;|oYkpϴr֕ iD[Dr.kI~3d{"t a3>&H.d=#cCNF3: j|,Ilw# {mP9 uOhaC,yjF݅k./X} 62NB#]YoZtoqcl"Lyd۷ʐx>~G2ӊghWk3̏Sr- >x[i)}Uk=Dz`C\ ?ؕF^6HF?@)h ҥRpxѥD2+B4Ŗ.'x[CDlzPk&z`C͵Yy]L>钕Y[ʌ7ew|1.@킴0 e4VTP3լbng1KyGGgz>NMןBIpZ@~w.TCHK-\H8+_vIU׬K5bvs,?2hS4\l*^Us`U3U hW̻N%z T; >2# 6 q0NyxOzo}xx>/~׮]| ]G4mkKE*h %H1Sb8f$DuDt k:aQ&V/o?[o{o`r95<4D"RQbY9ĝol|`hl$ \S&yN'ɢ5TP: z6DB(Vr?fע4ii2npo.]5_K}O[13nP_&I(I+ VGAZ(PmŨ8A+)#TRZD弬ƆI[$̉n[sQLۋwApHG9! A:h:j|̊C[g+E23Diҽ)lbuxhQDd8C?ۘb?ɭ NR!E!l0!A'Q/ Z|=s oWc|O )W5:b5)yF_f'7z`wtvjlW0lUeYnLdRHYmM9bF5xcr:Vb?6F̉$E텍1|<#-+=LF;Odj%qg00voѱ Լ_ 4I*k#y=y4a}}{fU$(V@fEÚkÆc8ec}h[ |Ӈߙ0 CY-([ܲգFtȂL1+$#;1+8W`=1͓1еr^gPܜJl8g'LSiNT T-i9 fY:D%ر[du#k^XH{ iAH\rzG>MPupb_`Y zL3*C-QOd  +@\1Ϋ>:o?OV1 x?kX} Eh1 d/^2F:O=z~~=”f,mPjO=^~տۿZNDՍ5/ c ,̽g m)ήn?m#dDIZYs+22d2<֯ Z;ol)YX*SG$JN`;tC& E9"pX|%yAO|PQ7(=,7 ָ܏ku6,laΝJ8dsZآN>,^Paa4&۔GEYE%bd I|G[m܅S!#õ* ZD]0x`{?c.Vpv]S̈M> 7b3s*D6)ZMCF\XGlv Q.I~hD‘[zr1ρNP` &|E2IU.UYVQ̝2DPVL˿4H\ِgDk0/@-~cƱ,ӳG[.l"CfwdԘsһQ)5Sn_qEl1'tC:dit%uC-]((grvCb .Ƞ6w_\2uo׆r;q/IQgxy yfJU$K6]Ȍ|ѻd8>;Vim捪2j[NO`ɐ50U'2oܼq' Ĵg3Lۇ+[v`aˆQ ^: Ã0"5IdQ}=UmE!1gۯ'iJuQ~}ƫzҮ#EZCO<򵧟qG7TX-ܿX8-kB[ _J9YkM+:BQDd'`P-צ8s92Iu)Q#I*3ubFţ)A|[ T+A&;POt~QbĶu|k|Tp#M6Ó LIԭfaZ.լ.TE_9iY:TB@&_2vR&L47)Ai.v;$\ W"vNiϬٞcx;[/l)d8 v<76v縷6~/{3 b1uX#ه vt:=VuV*;q {jePBaV$Fv4:l^U5 5aSHS]{-V ANT/DّHB^ _VA)?Eo2R Tq_ف)zUNFM'82M{Thg~筷.?O~ _>)6uq=$-zdq)oub"ʦg{U@ua7IT֐TZEW_}3t=>iEྵ1}rFbsV=깛Uitx@,,8߀/D!LPYH8d h:͌XISUGfh|ڛ*Sy4APL`ӿN\c?~ 5MSOvRzXJVGAU {&V'WBŤ"Y } 'FZ !l\  ٤}@b75C ;*YdRi}l)B)vm2 5cKR JGۜ̋^3z_Yr $p't/Vd'2G:nԉT,.!v؁.C4Bd–y#(FM9]X~2M6N 59冀R8*=A!q;0r"3As3Ҩ=} B;}8o'kP l cm#=&} "EmG?vVnhl $^ Qd@j0Y;]r)#yaWK*ӊ=B6L8P~J:xX姂'Q+^utH8>زeukK +~OTݼ+T"DUxJO%PZG>*WuAnlI^f>\`#J[ex1X&i0KdI$N2B@O<>o(lC50#!1gFEzq  .Wt>DP6g1H-I6u|l #h4nR7\s&ܼa|^"Uz<ʥoӤsn&U&휶C#n{by3 g[Hw6V| OPdW)L<0QG$E{o5p2f (Zя} ^hNtl%Lfh7\A88}0[[,fj4T^IUga`l2;zNV@Vtz& ) dEv@ FtI-i:6`RQ0( īKDšA|^7'-47tYҜII h 6MrOh `,,=g ]BuKKޘ 9b17v$0^a?Ԡ}%=ju'LZc kBEZwĝm,?gG$g@i-6'? aglg`j`?1d4u`P?J 1)3&_<~nnDb ]̇_5Ѱ,M7b̈́#N`A?|FoZUԚo"nGiWnYhO1G۰ [|XŬܨ YM`} u4vX)#YBP6uQ}xfwJUdFk24SjP)hZW*>DlcΠqN7s!#}鳍q5qa= +/MKn[7 Ep0ɸ+afZ,D&MlGsj. O+\]KL phrP|f !9H#6wyb$܃ס>oxRnAoC`[r"Z,aK:gql(TZ?+7/%c6/u4bڹ1֖}_Phuѱ3GR2  蠯뺢lVJYUI]ѻBµ.iYs{}|]G=_W }P׻"3uxz_꿀)BTxtv|hvi(v7*M +/ԧ};o_;Gܧ?;>]miKxB~J;`Msa ~:\ơVD`5VbZ } UxxPب>_ƭr*e@6Ojx˿|{?8{|%Qzj cR\Tt>Tj\,<:slxd|}8[QܹswU9Nh"fSO8+qř6}q."#ԿY%|7(fqɋ]qo* R<X0g<|zzE4P"Cƈ1=vNi)bc<158%[8|4RFt(Y+]35LT h ٖP6z8$:ܶP5f+^d3&jfTqsXrM\]MX{M⡬ۂZU%D 2:UwwVf͓c3lso@ ^OW3f.aB9|ej~ؤZV8?qZRŁiBAָ`V2p{Vz hTdQf -b7[Fj-" gߌv YɬbnPx" ae ?zv){B8ifŀ[ 2"ܑ?VfN!zL=?L=jk2N2)j/z $|ph$f3$ofXtA=ʗ"^vQo^ 8Ο2u3öi%N}2^K.]jE7)֖͚ME7s')2CMg5(,joB|{߿#dY%9} {KX8M%0^ ZːO(H*/`F1 .dpSsg.(^A2+pN2jC*}t[գf4[WP:hBOf5BNuBvVe>Smޫ}624XU;{+jQNV+-@*=tyVW>rriD_xHy*VX|SA l͒vc%ڼYfiΊ}E2޺u_W^)h_/]: u뀃8@ Ԟ6OٔѣwoW_{Ǯ_W} hx:g"?[fLsөNP@RyO,bG Ux P1HzwZmNX\XZk~?Eo9{KWuםݽst|{7{^yz\ūo+o|_~~g._6 )OA!S<49 >*bn$ *[G)%|֖{^<^}w~r2 UnwtSSl˻[IS O\rX{|T 8(CGaґR\L֕2Iat$A: K zH_ZbUĎ|EȜ 5c>wQ. @lsQ L~-`3 KGGu^YRbm{6C{xXQB 9|zhѧUY(r;>F/``TN^%4 :4Jz#1N'OƄtXFUu`=L~\m dJV:{}`TdtwEppc !`Gʹ&?T#h$8l&Un1;м6wN׮]|#sp\@*wSȈ7K+ɎV{؝2&WkT.4 3&dx,nksU`i Tftag`rI6T_aS9]Sxsn @APZpԤe#<`-? ._.kB2ˁsARDZY3E(qaHFaH of<;\OG瞒{׎L3얇д za: KE 6xV =3h ZT?:~/+TEx}¤tf(ѼtKs8`y縘:4MVQg-A+<]fL8i."õ|긫ia?D%*13̞7t:%G`[w򝛯;`Baoub紝 xboЉ `n@'j?Un=~+xDP3n+%x[{ۻ.L$"ѵCη-+P/{xxx֝7^Ν;L"G._y/"͜y":UX7ka~(hbĠ)eU3?9H o?Ï˷O?OYh U;qH ItGG&)tJ[>z]9&PqrJ?jӥR*(7lc T}q G TC8jt 8 ]Tz6IΠutm֥H0+ .]<\x>O^tjz"1(6CsWO_\MQ*S >s>IB!d~cqHz KaiE؀ObQ鍪Ta :pP='^\wDY3 SAW_׫QU?[c c\W2`o{&e"{s[v;OC vjsK:Q2z?&S2H;#ޥMLIXl nĩObִY[ݯ=iz!W.p Ywrwj!fQ`Q2y - ??+k ~cWz;.YqTLkٙ.'-rE)r2p8|me?crZaWN`],4C$OI$geFJܬ 6XQW?}_s!E#XH r1ley9:1Z{MIfǸGk5Zf* Q/]*kR>LYf,l3y/_k\e\dǰRKuI+h`Ow*T'kf&F ǣwS`VDd]|hGg˿x{V䧴 73=N$ ls!4 ]Xt\>C>!qZqFio.QpC`kV.2kMB?qX )=0Z?X?JT:']JCὒn`&]ɫ@eG.2gI& 4\Tb ) Qɏܩ³a\ @X$%AVUpPO`u!t-nn-^hG^[[[ywW kgkW/_kUU/>c_?}RR JZx, RjCjf, zQ1:X:u|@R.w @ɒkP/0tG5346Mɖ-ţ۷/~T3(5R KW E;$ypP;U*8s( ɱ*/\~Ja-kbcAvMJX/c 01a^òJ+vkJ]T՝yQg@5~"*T8juM0Jn&.{L0NN{F]A>|82Lf@ . 86Yػ|7eo9$F^imD2OZ֥LXN]YWhqdꕬ@!/Ng teΞTY QiqtY$oOOxEfLm@hh pu BU6OV BXlĝik;>ljB0֩ R$FR泯s˯k& ul,(ðp@(w5rMTlt7 ؙlRUkյӡwuxxIoLFYX,R,W0n `H8-٠@IZ=+WT0&R'oI`!qYA@3!PNF$7'~n;-#:yޙ77ny0NCE(};Y #-x-utq9^xԢ%%6U?w?^;ԕǵb? "ІpQm@8vh pϳX"urUWVd5_$7_|Z<tޣ޻W^;>:A4 k>"U=SePL껐YQ-^I@e:Env4] eqp! oP`VdntP"4VW2T ^iKCM}pJ(IQ ZW_Ĩ>N PV]s8Cܾs"K|jPw9DZXȶϊij/Rp~ 5m]:7NX^ SM%uge.#'sDTbSKʈ>Ugu\vg>/|(;}>IsS(\m̩[oh3TqBbQwt3*6DDL *)/#gޢ%,+0"Ek4 S)8{tW_?~EYhfҬ.*-UՅ*@d~*k3*Tkwǹ`+^~XdPE C3uʼnԈKmA(%S4'Pu{wȼpa'k?dzէZ?=*M#Q4ʡ^(U S6"·Y+qZ[S'EuH^8&Y&6:£GVJ ܀"t5Rm#P(P؛Xn?ɵQį.V* 'wK}ٕPe"Ax ڲƹpp*yg<{ η_2RnФ{ U_$!(nH{ -F20!*g|:O.@ "l@G54VJTMѷ$b%EȲ׮^yO=5!#bGBn?P+?j80]lB*ymB9>!R=l}b?;L_Y& |BJ\AUQ>h@DT&~E8(e uO 5ݕ [BXۯyT[߽q7o߾C2B:vY\<*h誗wNp&iC]Z׈ 28?g3}bSϭXWQz훨֪MBH8ZөLkוOh6Rqɹ^*#"z$ /$d]hpX EQ\xƍ|;O~\ĺ'=ӵ_UڤҾIH ig*US>TѝG7޻[k7y;_~_\>D'AI\"!*`hYC0t'L+˭٥  kBڅ5:Jz6<^̧~JԞB[Ѩ٢͛n=+x+sx:DXu'Sz#T]5 N ?G(-Q^%y_cք@aկF^SQRNXwWϠ3։0DΩ+90t`؉-0A̙PNZB>mP`sn@LDE9ĨXG^:DO?)fNFSbTQ%jzN((s!Ϳ7(y9epM:r9l-td#"̂͹%2e',H$nGrCV/NK^ F|8=Oyd'S杹(8+tLN'hmxŧ}4x! Ōn[pW7==o6ޚxR`˼wodnLQ߂~6W<7rDz&;7A֙iiGxWø'463w(ټ͝S`ek-S=ӎbx`c?uH)LsFN5 3deVh"#Dnnr{900<]G=+ _;Crk~8 Ӱ4ogz؆ͯt\l4>Д03ovn;;7~W X<6n"EM+6ytќUQAY͚73uDj?P e`j4zɩ09NMYՃ,"apXEE#u.=ᕪ ՙ*W2P-tط+RFkT@şV"ѝ~WO_֟ מzŗ^|'hY: Ւ`UQU)V5)'Ł]卣7nV\_'~LJI@2gvȆX˕-" 8^%ڄdALd.zgC $ɍgҎ9 I$+~ohT竜??_>O||OEħz(jG:3e#N@$NmXy/~6|]VPmlZFb+vj«FC++$?gUc(r.z|cXژTG9?’n׎k?}JK{V #X9YEIjUd@2}J8#vAZSOLP=?sBкkX:Ab8Jhp'4H&h-j5=Bliȃ:u Rp?*Fo6~_{>Uݽ O^VŅӬל6Ȧ)fo\ "2iS`{(n FrƼeT_ ee&5(]t溋T2ڬ`<ƮLWH;'*oȂFQ} ΣwIe |q{^0>Mr/Ñ061 Qef/Ji8u ]@CLG+N`~y㝷ܿ[I-(Eٻ#qXBc:Қ}^ &\w-;-g$O" 5oH4J|u&"uZ yz$ho/uC ޾7?19gU~^#>R8/3 ;9*ْ- %R6i(BIae3YZt;`2("3'}c;_ֵ;EQk K_z/_nqg9zk'q:.7ɏR'2ҋKrrTB޸A>P Lfxje!€xx,@_z巿}Q雿_O=Y, WH\*M@7G?mE?? ueP"${q!kH/VUKu%)Si#[nJ@SRC%} $Ƶd0miembت8S|W_?oo_S%hFQOYI.Vg<ƍӿoYiR[   }Yw<ٖg2|54~ \o<[ }[Og>SǮ]/ %g\VuehS}K_zُݺ]~xͷnJJ x2T"V+5&EY$Ӭ'l9 {Po8CιFir:8 d!)l'"dUҩ>(&HKjfcN!$D1a>. KouH~-~G +yƕ>lO>ܳOTb;&_4/7 ?(݇x0HݪII@/F]"cJ6t)cJ*e-hocVm ! Q9-%<õNAolL*MMi2r,=fo\liaT7l+`r˖JK|@ҟUO#'hӈ V P"Jap}6~K'[(kan\*!Xn# Cy x׉sUCno4 1æxCzþCْ>֓Di2 lAQc%1i9!\c tq ;9:l8-aÜ[r|P d!11Vĝك-5ڶ^DMx+* m&0f0YLH133]X{-;}owuɷ1o/.W4rW/ fyOE4N~GMvlyzxF~t^ڭX.f*q l" ~vj|f%1(uYg̊/1 c%h3-bY٥P ;ҝF0c[֮R4Zp mw8{<ND ]o5Wդ> 5f^ߴǏANyRx氢CQ&/g~/"`-y6H5S$L;=0^c+'b xE#-p[LT^JB5(%.Tj#5KRMY10ȯq܍־d9@AˉRGP֯NagDsu*ᵫW똇JWGiQ!bOcuĥڗ !0?@ ]pMZ&z4^ ٳO 5|s` u\O?ORtV<ǫ~K_ꗿ/} \|lFPπP|ZJ*WNP{H{%*T|_e= VD*}Vd*Ub_Lٓw*s~'ShyܥnkBܩWG/ 1ekܠ3/~g=Y\a*Y[:Zd]2LZ=sUTBǯԣ=*Opm4mDcnzН;ZI>Gh?GGuJ6IUY_@JhT ugY[mU]P5Rw#e$3"K/trgݻW?b-fHZddZ JUԮxagެ&QSNѽs^]${/ڒI5kb3OInYnG['! 5F%#oމ1J8&1tն}jۈ=mv۬ᢣuFTiLXքL*vl4 EC"l%kE*bYt]YL?rɭ"64ؖxKd,Xj62us3 VQ+!ˉX[|@Ia(O !tL:aTNQIDƇl_4-L?KG;5XnF7t#ƠS8]?)NpTA> 7SpllDzC\DHEZMĠeb͚- ZNuy K}Oo1qIf޳;}`xo}$\Og҄u( D "N=4G_7Vl|mXZܝ@֦;^?a5 Fм&,c vpg46CObp{e(< $Z ȑEL w^hu6x/۹٫}4Vb  0vj XZl!<7p8Eh mR Uzm56IGyƙ e7 %ڱZxjp 8hv7T_EdQ'9 =@K=TV9㻴wx(k?>)ݻt }Bf@;sk\ $㝌S/dun?{h]yg U&%.*%~{u_[bVAHZh_8RQy*?~x9ɢJꌃDf:Q,]okgÈr=yT^S>vw3.?P\\~zFmaqQ%wkUVpO>v$n*cT0)މ-4" p8p ݶPN3GC"mɀl()0na?JSc& -*P+U',H+/KIJbRr1hW -ϔ h eS"FzIիsδ#&h;aIsY+-=66N2jhԨR jC+7%/G5`@ fjP@RUX'imYDWH'I}KIwz$"7n[֕vu1TX@ :S @c\+ K[, `6iJ%S8^Ha cHZYx RMf]VMڵh<8$}91#3nA8n15S~Xm//ʹצ^l"kuLC a9.6í>zsyAHT^p?YpKaQXADEuLiL)[*MwPv-`lhG#6t&Ѱ?S9dvG^FZI9F|k=rtey{@9MjwfhNK:8ʅ= QM7\u 8^8k #v%77m;3;j:]bM9;Kr3JE ",xf`剝dGML<|-\q&&1!ǐPHe %K>OO{ӽa=NU{RY ?5|ᯕ7vŘ˾g{3Prڐ&3QY*yi6{X)m-WJmu=(*f lWnQg= *>OXvfUD:.֕+Ӣ <i{kfEjQvl^IJ.n(L=ٔP߅#37r,75 ~X]( nlQ*<@Ym7WγDCuz5Z}:R\aL '|OzUB@P"\T 9q-T}^zzeRͻNP>$*Lo_WH]zuj >ۚwA\|} >PU͏=w^k}tÛ}@ѭg}zfDZ?x\դz\_=6BJX[e W}BAG:r6zQ?Ss& Q+TP{:GL%+YQB:$&,i%:(W ^&٩Qq<MDcufbB97=JŤW (ȪGTV̅{g.9_{?AesWUswďBoPé{KLex IW$Ly\5b5z&oc-<(&W~=`(zD}әzn(,}tu)ƞ?=W^UuGw*hE(Ks)S.TǙ dԟK (Yj-65;gi7w:p Ya 'Q{4sUU۾ԩ7J> 6;,Mr?(tl2bS8AqxMuqcnwiNy(.yhj>0N gSM YEöjc,tRG/ֹT)86o&+h Uay^yctOORjPOt.3^X~ W#0mCD _q'2b\Yak.(s#bk>cPG=4 . ipͿ֘l6L'&5f޾E~M;/ffBJV޳3h2 s{s!<=  Zؕh;=cl-aGq V>!Ѱu#ce.8 )Vcvg0+HE,4a mS,HW/{eWĎHpH&V˪ P#X8NdSaGs7ϋD ahD;0$G*m  "Ej "2PET| DơEk[ 70uD擦;bGxMJ$ua/G%]3"*]s$ TymڬrTXS-0 dST\\=7hEc!5عNAZ9NwVj _TbT,@xH6lJmuafx)⦯0($n&SCd< 4P%xAuŐTP嚙Ľ =R)7TRqr! 6es,-~8*7 Z=FP~anRtGz}i}ZFt<:F#V.^RQX[!5q 65fUAkQ- 3klm6Լ2EDT46'S`[&*V\SM: o (G~hzQQvќ8 Mxy2B4C60N]1yIe })]f[ToCvbܡyҲ=, B0\%@O9Cɇ+0"WN!BE*|-ܧ1b~ˇBu%2~P({FFjl:Yd7^\^(k4!cj+`0̏懀cYqP21p-: &ߜaM8S,f`s*<ㆥcdyjt607{1ώefMƹX 1I1Nh.5|/1}m`[[4 B c+t`Un ukg"kׂu+֝F 50A?OSÊDm<ڼTAI9<9YOG5‚Snmв{ ȯ]Hzwb]?ڤA(bj%8yֶ -MqQ[ z*SˢcCu/EC]sQ0N@/%A tCh'I&+%_CLX%:hU0 G6;P­ȯCsWD_璾"2! F^򠤠h(xC=1idOjmmGЄ6`]RE$ M@la&}[ z8c3]P2,$=,=*L>Qf ŬA+FnMpPq#ejj !6j nx (EBNvwF3Ϡ-09[RF߼;YXY:'ld>4 >U y!VH?{嬸['Z-H IT'\hs JZcukz8ąA2X9CψqjӥΧyZz\Wg4&:"C3i`N@*u-PaRa-5+em9?$J/ @A1; G[>@g65JG0|xRI<!,e7 Ċn`Fh~0X,-nM`X9'Oq#syCdvbe`^5 ]ͦ({b# ZƄ;jF՗Wn+oD3dՊQrTHbyQu؈ $n,w'pیo r2?ĤD\d ; E0l;r;%eҥeDZylah;sߨZl Wظ[{6B]3j Ν|_£82 Pl4^+x_nn]x:i\$N1!=./>!L:>VaHp 8V6{rDG~-j` fF#9 yB?E\PjݡbZ9; ol}E+3];pc0ED>>0\O/4?%y=mr1%aOqmyՠ_W#8uU)LP"6;M?}/sTO\ x.5Xͺr͈4 'queԬo=y(9OƻiNNy=QF oU3 T,sZA֯@Q#bpS{JZ k$U4Zsa s 9i`2ےb! }04 V}u\C{PKNyDqwLE,`({)P`'I +ncJ'K,,&/Qޭ, NԨlGugN'& )f#G[p!bG؁h p`M^|Y?4lyA3,A( `IBqrKG; Կ4'_^b)dMufBY،n gVMFe/Z4rh-EY/R@r(I"\m*W'HsjViP'?VNAĨtvF'g$S!9WTr$90"fdxP-Q㚣i*F!mPE^Gl(hݹ2EϤ`?$ǠS!cz|MJ"HTOc F X5fJ>z(\#']fbcP 5 tTUd;Sb't&ȕRb <@D]"%a8lzX@3)+io{gKl\ l)Dܳ&Hc"y}!Fh/5]=6SyP#4$XK6M2辱D0Pb[,6"ER,mɹDy+f!Ht Ѵl?#9gĤ`ƶD`AsKM$!NJ ,=as#wl;4"4' +^n֠sq>ƻaVۿMf8^IOb} 1&a-y`z;4 xm Mx絁gbD & NZㆄg\-2B> hڷ[čXy#ƈ\}X( oqq#}ܑoʎ #28d+d>L[)U?U9[j Z/5Q`@iu# /9V>\ Eb}BS(B4;tI0eӽ(L<"Ik \2X0+شgu _5_˲6f͆F$7r|}:W0<0*aiKMB+ 1e6YJdE U*P?{8ԑ`N @(t`v<&?U%|M`=T %S:=nܚ<͑xhu$?^I%h2 Sq𺽇S<2ad VE&gF%u'G\WGu2eu_8:8l#y_Y߷^mk <>`w;~ aNd$PeOM>)i@L=RΗWH!OZh=!`zB&-&_)Z,8Qf]kéHSZ1xIa ]oUUc:15,O:Z] / ?pJp$=L#0&PRTu.dTz*tܣJԣ*QZMjWg:}" Ѯp+k 1{~؞әXPI(+Kʘhd(1B)$5~1Dy[n<^ UTtX0obs.L 0X+ OWzJ"z5}=V%fK9QWzl:D`ģs-;,d]5ޝع_:VA60c1fR DV6y JE_0"X=qrb}9_|v)ҕ~Ƈ4!DrxN+Ij g [њ9"Ŷ8?{R3] h󧧉&6{5]807zPm2CpWo㨞Aǀs@lDmȕ?{6rk._HyhvƘmG:vsf+ m^x㺕@xNb]if78CX1nDtw/iѻ 7o70+R,ՅJd3u6RiKt]eq-9eN{ټtS['v7,'b{ -KpzuTڢg!+QCl[`gLemNrd'S$Ldqo Z-ep$1t&Aߗ *_m|o'l}/+''Y͌3^8\cQ;}`oE=ܾ>)0:UM̌_Muu,X",zV?T5E8zʀgZ_AYldGOd Br8S7 FGuK0Yw< N. ft0}Ywh袩\{5 =rxSeȂk{vaꫩv]1U.T]D}e\GL$,ak1xL -FcEćPO*jȒFRaC+O& A~*ȸɬ-ݸCBᕺNE~`ߘi4$ ^;b][ \S6 JMgcB#6i%Ν;\ʜ\# Be8.恲 TɐҔyF ̳N+U.@?r Cqȍ(jj5T_c("Q>uQ]lvi|ECFaVi<t[hZgV6NfQf*`i0kp{?ZBd- Ѩ5SÈsm3Y6au;r:; кozuuRcz<`rA Lgbpg=j"RމmO?{L\0QVΨ<bC5yLk kwb4Zô|aluCÎ>3]bv_f-?LK^<G5v_.F~A> ba@j>ɰp`(0Y(@-6,D Y44>N5Nf /AcC$|?aNdL0 j4wؒXwԇdnd1 SJ~oFyvDlk**L Z͢I+k} ZW1&j ᥾юm' 늧HL~60bV;jR6~NQT]< '(^G%{W!W5G.|;_wA3^H¼~%ca xf-|'%zBxIF(NSn\gk: gU Ņ1wm1YKLHaNǢI{@p EwCuܛ+P1AG]i(iY5wZ;ݘ('YAE^/#Nr] Bх.Juui3?qYTiHWj4B+=7c$〽fFe)^Ɛ'[=7>^BƲhuhRbcw+QvEXt=/+,Zif*Wԫ  )C 5bZ4M") W͍&R. De+v &0 %6 Pwjq#B͌aQWֲナ3ύSFzXa4ݘF_1 '| Fkh=nw 28܍}-qPsw>ƒyDwV[;-Yei^ X:hD\/Pz n!=e\ݨ==Fma<Nf/F}fJJC c#u6 ~z:2`nQ߶R9y{;C}|6;__.Y k+(FyDhV)J:Q6𤶍OsCĩ@iu>ntJzI?(zM.ĺl;@Ø#\BEM4.x1+992$ļv-QCAcTUB0%k*/Yヾ*wG5اpֳN{@)o54)l:DøN a S}*yՁGw}p[1gen&V8KLUSPS8Ι'97LKJ o'ҹ1W.qR3FaLj)l=|q<wko U4*׹z_ёX`ujuf>ӢR2}u<Ti1'c NbWT5PR6CG )Ltzl:I)l@4 `[0woMC3#؊YT![Ly4bt-3% ݺ 0'YGnf s]Z?T~4ǎ)zT,h1鞜"~+V(,R+Mq\*ItjVfE_vG Ni|3 `u!(Dl1am5ԙC2;63k ސa*9Vol) ^.S 큧a6K?:,I~6Q(csފ}+hi['"&:0m{e ;ޖlȍ(}YO~4wIy%̖巿 6һ# QaPz*I'Ki׷kj hkv䔮!JgVdpW]slmRkJ] Q$5C2X܂)}#RV"{*XJpAy<۩Ō:2SElEo%Þ`5eCռ;'WO.zwZ[AHHV*gSLt43uZaӞxg wy%`5 h>TĶ/K '2hS껂3gj prMO[OB];-f:e+ͷ9޵v+j! XL$KI 7DqWo65a,g:ۢ?QX`e>Dzv׽~! zD~XϰTHWGLxv߻7{sաTm>xTXJj)MS*Z6GZ=nqH^ VB!4T8lSRIvL%iZXU/C=5O,gg&_&MªFGx޻'5NA}n21Q!/%zkOQFGr%(#5gsrk5d|h|szO.2m -Z1qpR=U<[d_RܯCOj *݀Q9ʃ,} ;gCgSTTl奔q'"BKT/E yKBF՘h!ϦsLͿNR4#}jM^H9Uۣ/S0aM *) = f+pŭW` CSbV0Bk2o(v{IL(Џn';x ;ڐcPi*! J,Zai+O-`aLyeIwU6/wiwiY]LE|`+D$_вɖ4Mp1+̚C"a [t"ɡ|ҖRL6[VqkwiôDkso*Th<YYЊ70`Zcn[Mяjz{8{ӝ~ hA >F=Gc53YLSCT`n|Wf_H惦:~.j]g~UkC &*0ڋtYJq5E;zɜĈ!fލnq꿖6˷ Ӄ ! wOB!g!;x \SԣtwInfyy3Ź3S?D8!X:,S!ԕIP:î&d` êh`uu~F(P-(⌤(xK(j`320h"omHK+i9Ad&ʽ)jQv>E@KO2 w.UMcEywDB&Aۄ:#Q:4Ktw6~Ƞxk'mD-^mMv2L9"eD1I|̽u n&3cr;{Yθ J|Kp0QN|3C9 YuO}d]~vx֋5ש zAǨAe`Կ2mb!fwՀ,hȴUod7vԉ{[Á~i^,{6H'  La <[>H@M6>cX.>|\3bhyĺd]'lOOn6Phb^Q,;hk';%t(Q61`vO6m&ȼ56]Ynh:5 JP=.gֶyMqX0Z67zqxPuW{68JQ +}@rv[[.7B$Yc!3I nU|7G̟91LVNy )gLhO ?%K@VhtlQkPNt&-"ȑ˃ AnD8&+r"lL%RM׎mхMl3@jϤ\? Ъ55Z,<mO:ߓrp\T[N0gEɳƌfyV2Z7\AӞDFMD6 :f~urgBV;f:oX28mdO0,a -c0rdm' vwCt,xO2n!F@)m9z1h7r3 niomqp4S Now/NB.њd00?JNV 'Xme Wwp9RkKdxjnՠ ,huuuzhؑ}Z h{0~ړP6AYBE@JkkަWQ|4V =Gxk,/`k(de V䧙)8;ƳbWVDpC"n5 \c79hW0p0!@^P[eA5c[×?ıev] c}5!8 l_˖-XQ{6Xw eMI3-]Ҫl[5^t{/=s׮]+?y?*qGƬd9p*0`IDAT~fvW ݌'0tKW++P*'ZwS^f>\c$\J!)xF=QApԁh,]<<5g-h㜋1zğߜ}ع̢ޯuы/gIV򫸷ٹR` U0]cWbIL0IyI!* @}d.zc=_?NN4G#2k FE҃ZAt6o* h*kkG wÙ(^-_Z;UsPS^8([ޯ",P8- \9IjTYzJDZ:`8JfTz+rm6ǚوH97AF%锁}xBAʓ鍊17X-qKDe41W^}ђr3fC&2r˩cpC) 4!Hͧ؄hXssLх. 񬢸EX"01$6h6X?3 *r #Ll!blp< vtiF4J-<ȣغ I6JlF6as/Iɰb۾Ynռ XF4^]o]hl4s%a{3= u2f5|)9} +d(GIbw` چޔ;`߀gi'7v!.}EލgcG;[N;˰h0 -Odfɳ'2w.FN`1C PatL v;v[&,O:l+I>$Ztޠsa(U[t7pgb Aq۴XDF˖y?'㗶NӚw'7`,o`Y}.vs@;$@=Pԅ@|pC&/!H["TX~3$X2B^rw ;BP$(qf]S!4`VO<PN1ia,j! _ q n21 ½F:!;d)P^0aZ(GŌN}XnǨA[ݫ!ƈ"i㙩ZZ9A 2y&Wa~|_;4l̮yVs(!sAKaN`]JNǼ0A(V8dЅfG4 5No3aڻw+>RjA/֔ZfOܿ|xʕ/◾~s|=O<~n~pHSK4Ǡ]C"=eHwg_aeC4*lUŇzT^j]i:n<8_=4[ mUPEt&6l [HJ6WqYtEe߻zCJ%1 $HJm71A"O-0\8Žl}iA6RPk F L,E0t.iTD6Lsbf_ph1 YFrQbV8`mD#UZ@4syp9X0fv{)T>^%.Cpsh0P&̲"4Pڑ~ CP xpڥbuMbK"vXeY#XҬ|$pFK%(-,]`ICQ,/CÀLlzfr=THz#8tZy6?G N隰.aJw3PԷ紂 z:[9FgڋHg" po߳3Aܽ l|mA972)݊O3Mk˂[[X ` LZ &30Fe1!73L&1n[xBAzec*[N!H(wCƠْk"O\k NZ%,nntN0~%\ c9ΌId! F C=uݬ͐]?͠ΆaaA}_8ͺ1[vZ*eDPqfV}LDWZ%z=d/-Tv.F"錌vwql]g*Nq],^ljTSkPbveiþVyٿI.jE%NRZшQlZɂE?Y"A(c?)l8Vf˩ܼj07w0>AX,Q` ё9,!r7cEq;!3$f6[O5pR&&W/¿Fd5I\wV,6Ǣ=*:NɆ*Y)ȧ gVu|+`U vҵמyO~_㷾կ߫S+,M4!ovƪCݞ c Mv a;9 "HWT /9 ϢvsfK|RUȅD*'2j:P!6 m1x_C9yt2g[)>FT5sD<'tTл#`tiٴFԌ <7mrWtEи- [Zc\vB }O LlJ"kĝJ"P{Tm W. &_0ٙ~)5!k@¿$ELnI ( ыʃ,A`ITeH% SQAm=OI.`)ň/!~ѤҐWDת9sA{f0k&ۊ36 Ez VI59f"˝=L z-WfZ$:Df%(R*sއqfm21(Y>5}JB\/j_NV=kݭwO<_?LɎw(@DbVW._zJޛ? ["`!3gPd@02D8jREP{ .\p v )\_Pb}w5X "UuQ#[%QN'Ri%-rKSuZ÷8PgfF qF5GgDJ*i_nY]Mkhh1U'SQ/pAuQTT|d:VTs9C^EwBf5xIWPYP'.'UWPDP@E T zSp,#W: Q/ΠQQKa%u]8JiW,G:)y$aؕGSۣYIb{%(>8VGI4,\$6X퀂HB .=3P\fiTV KƐƲ6=5sUnEIE+,$s| =Z`qap^,sfL{8^KA25f~yqf>F?a0nX(qY۷`lmBe(߲k;mƢqo.IDNm0t`nsocsV uF6) &WP`E^},wیCϱ.Ova=m~mnͼKm6ZT~O3񍡐dX2Xg2W) fE;/xt遺_z~Da{D4gs=o=灢wqx=!0ɺduLRDz]`d樏.d.1I;-qف0\q'+a'^;ܗ\wB9w8GwjOwOʃ:4C.^t?pJL. (Hnh"d2JPƤ2s&OGnù ǽHt`y U**7 n9PڣJk.Ds%_C58G%赠@.^ps^(=쌯hZ L '?(=-^.ѡy^C ĵ-0AX)5a%ÊbO6l?:G8>{laG/?'1A16j_7-?,10xM Ohnl"3Y㣦ƨ#a$)KH#7vV}5j#̛s.Xˋs>"#3H:P/tfY=Tnpq?Uob"$i E(MnuStfn]E!vN"oYc#7?hDGደQ,e(:ɷx9\TrI/;&IM0hʈr \sOV+2XDKh1ZE3wNz-\􂄠$\{9&߉Qg]Q'D"^a(?Yħ +AY3v *p!c S'{i?ʈT _tc>Z-珑/FE:г2#Y"yזk g8 '=](cl5gI 7gPEQZlOs;lors~.hdj [5ԳOV M58N!)brqbEb}XA O /cVfESE)E$4b۸-,UUJe"Ķƶ3#vD"Afnon/..O?}[\v?կ~/?ן^v[ݿ'/ff66R=D遾}[Q1dUXT?EA,8LӍ)Qh5q"Kc l"$ zl +jߡv52J ՝GBÚAR5@+Ozb}m'=z2l~aFa``Plll\&ӱ(1S жQ5 x-1,˦+UPBJHk%pj&GJњJ#xUWɏQzB/*Ev2U8Tz81XHY"B$qiY& R?+٪[TiApeX_O(U#j6l4AN1c"Sݾ(_SrīF{PtXܭkR'eN-e`@T\ =N.@Tv9MksnZ/]u9 iF $YwHFNMHLBAsN&QWf皶߼Jʏɱh8bBExG4bddWM{ϰP4dk+~11jo#ׯAzR%%\D1p{Ƹթx)F4sLl1F%B^$kpy@NlfvRg>W [5 $uCWq{t+؜kS'1'9V;n84JU0cYlHO6_G4GZ`YoE: I)K5 pu4R !0S]Ѓ%:p`#bhm0M?$UJAB#+U%_.V(C@DW2yG=3DGGU,:;!=.^=DP+B5ngcF0mqE@sxT4(c ev|k"E N HW!>B(t]-͢o4[SzL(;y.h8L c vOV?QwRqiֿcB Y`Ck'g4gjr4-3ja(Aٍ@E^Qdo'U+F4 )t6&WUXS\W.6fWM`[c&6,1y/f6o?`E pxxpp+'|OujB\)?t)>q$̶iY:br|f_|/?_OO/??___ݿ??ˏ>͛O?Y]Y(/,B|vDؖ4.*Q<)WR;d^~̝)Jgڴ me@$*8!.aV/qLM{0CyP$"E)iP]%ulp:2 Vpnt{#7R͚u͙ED} !M1ʥimz['!mDR`Tx$Z#ᏅomZbc/1)/pܓF %>}2-nVW1(~]8;U r1QْW k֕K^/n[ śR6r)\>5e li2OK|9O~Jԋtk(Ʀ 󍵣j!_մsmrSj V#DM IE1V٪O~8@U ^UDZ \|ԒĚo"Wa1\Am_sZDp#edݺ̖ؖ_[{ `N]}>&,nLAG*YcQ;(B~U{2*&Rp|fT}1T_-]~ٿ9(zQfD;Tdg閧tÃ4d\Ȁ' =cxAo_.n0T~$:84$tR8:_CM?[ZZN'N< C _tϜ()Ou|&I֋ \q߿wsgx#c6-`xǵ7CЮF`y{}SNeD1#++(Xw^O>ǿϮ^_[M ?y2ŋٙ/fNO߻w;o߾y ?]W_~3ALOOnlomnRz̟fPr LHBnm`~o ,(Bʝ*CS$`pw=|&.}o p ŖC[ńOh G233:tx uR>2ߢ:ϟcAh ۔?w$g} %3oW.R J x , +v@Hˣ I^ӂO(m*C˃J&]k^*МM6f&*mYIL2ڲ@BeiШR‘aT1n:. ,@BhŅelϲwRjBbu\E1q3~::nb#-bzRvB6Fdqd(^`i 7 zR"jn>^RBT4Q)f~v{H OlAu#ZTV9d 1#L;KM[(Z7닛f H-6%9D?#*x-^$ \Ȫ݁^B@w!L`^zs/lrUӣ4 Z"MiYY4`:Ь)°);Hx Er<{ѧpek-7(lˣї2{@xEBM漽8!pD T`dp0jzݤ+P~!bsC`Hn(#QR)xцPHqVkyc Of9ѯ Qc̗eZ?*X/˷ٴJ;J.k]sPK(RG h,!T>XNs&_# FTL2ďf5lh \d)q0 L/ĭ.L<ϯۖ'ēC#C˵FěEmXF 8iSY\|~ zR|\R Gl";<$y"T4К4w!$PIr8]SJTegUJ@3&OcqI H[sn X*Bdx},x4#@@ЇDA/ ؓ$rms9؁}& %x \pSD>2{2FV& ?F#e+.1dM>:Y +A>>K wukf.Q7"+ى|^I:}lF= 9Z IjjQVVęRRm>I?8*2mpMt7W „UHKZ|V#<|\CEZfmRZl gV{ZEPG6 7$zj~إ\5 7(@GEf?E#tSî\* *k;. GJK*zC(-F+0H?]yksUS18!$&Rn\Z F2۝B[ \2pEHHO=r~us{umSO.'fG e !@v&9] We Pu'ZQ馫DdS49[-=Ka+Al0GF 3!H|eRboEgϺp;*ohcX-t%":0tBSG0H5,7^v} "%Bhg?ܠ@'n&K= HXeQBEFmAİ,V/RU&+{۟O(P0y7 ׂE>nՋVhsNl5su(Fgj+[0C\4\fqyr|U h6y&xVC7Il4400֬RjE_3@A__ 3}Xw}zll|ee>O>ɓ3 ߿KyW;QޏD =y1:9v㓬yNZjhhTثݻw{z7ύ#IA2ULP\+IIa,6 uJ)+d! 69u!BrX2I ԙSC= | H I Ghe/}KrbA )>3v!#_kBI'gaBl$1딈>a4D|98Ǫ8 ?!#tXa_Lnt&-Mdn)NN"caV!2^O!raiԧp~xfKԵP"TtXY \ kVO)P֗hD5 LVj@e.wY${wj~7ͷSN1h.W{Jc{]CQ cI('&3e#^3011<:t)A$gT=-=4K+ԈP# LI9h+MYV^6 \F=֨FmA6)AL5-)Gfw!v&Aح-QTPSS7 UXI @atpGRU >U杚ʰBh&BČǺ;5kBeU A76 POt#L@*mz>-7c|kl3=Ы[SxZWI+a==4C#͟ÎqCֲSOc+1{kda !qa |y\Cz#*IY4|sMl65>44\!TKa,ƚu kYG r!ki`/H=69TY&"`OZs^Q;LmxDd2]tl7z(Z܄hˠZ[/,UVݹGjt ~>YhZZsKE_a+Hjd "Cޫ[elFJcO-*IEE4K/iN6>T v ü9JϗǓ0:5RhcN5+$~d}uOhRF!#9/)Z@MpA0;6\ϭ yZ%SJC&q^d~v-l/0mo RgŲU1*tJXk1?qf9ڕ\+~?'ޏ~[.А8 "U>#<6͸?g???o_<:6??~/G?3sHT%2Cp9Ƥbmsj2f1a,jyKEJFt|h M)f1ߡ^r0gXZeGԗʈYQ`hNJ6|mQ ]"1[yB4-ZFcI6+&d):T&WJ~+&|R*d">aT8SF*sHoKlV* ݡ'u}X Ĕ9 0 Ro`_3.$~B: 3ӱdFrncbk l쯯b'#2_*#bcOTޤERQ[&M]kծ4Yo֤@%<$/ߨ=(5zHob !*}4\vf<6D2%`ᦡn2l6Vr+ƴ9P|*$"fʆ V%XaoWŴ!I@\xu:d8"31)( N4cxz~/duJ;ց=4WDnO(zIfFkfKAD\E%>=0Ss\K9^wpzS -ٓCTaCWZ&Ӕ j^$e G"^n8d\02Qɢ:Acs,f qȁnpt2nR!"CDEX1;-kq6̍,5|/B 4 0 (9|m Sv64P^hT./=a%'P)u~ z F=ƷlGs&ՌP7 ÿ FK EljMPH=>ч&Õx-^<2BZbV@4TISʿЬ&q5 }# .Qj8QAfo#),O2h ,(zpsdNi`kq6ĬO( !KOhJ35-%ƚ0cFڑRSn+ALEJ xjF®}^j88tp?n ?r8#>dcc,*T܏B'¹مO?~qBÇ9C,8v(By&>kbnc?~1)Hp^]38jvC1O?ࣉ qO'}'Va@$~ $sG[^RIhݰo¼x)qBLh.xj',$թ82D 5~3l[B _!TRkZZ #tHԉ~|ع۰ AB]b{~rT0wǍbf<'ȇ@!K}xĩZ>UʀOXAic adWo2M>b4X.Aծ]JZc]L NMxg.x`(ޢܸyMTH[ibDx*^?23tRb{6O~R߾S~׏(%Kpt` ov ll$3'%n@ w"NX&r9hSģFG˨JJ a`M( ȏ%A%CQȫ-mRf'ڃj5 MRERRV+cOhWyl潢Fyrm B ju^(/7 Oa~H)&Pش27/4`wU[\ EJx_KFbk X.[h]8i5%!d#9޳ǯ^w|ta`ͼ#,-M aF$&]Y8aDq[M-O-ķ&7o.9١h:1QCo։z81^ƞaf &o/ZM.ݬ,eKFhX—Z~C'S$ƴ3 mTS03{X*g*ܔrqMmۨ Ӑ0zrn,_9Ԓg!FZE*{Mf`bHh˓ {} ZH-CȤ]9P0sYXLĞvb/F^tØZ9cBHGKus &Se(fԇ+ZBH@0fn_:hd>A}XƢ`@|S3xpH̓!б=,^f2W^g`?WؚSC<0wp`ľN7}/ 3װl `.ܼ9qgwin/Oo? ,Xo??G?߹p≓':ă 4gNZ;"Z|-qyF>R 54>u$3NBRSZ9/{|:"FQوQ$c܄E2?1f4U 8-f:fY­ : }t-¡1uG~ IAИZn' 9nrq0( a0nSkU̟D|qLx6  #Vc[0ÁN\kv9s߹borG죈6%8&71>zS$w/oɳBz졉iHPp *BI@mbM̨ ;'Č_14xFep oN<977~/>$!;Pw7щų hbM!DzQYl*ip",A?HTիNFaAl3,_ 679TDf$R"<'ĈaG/N感ºA I `k@<"N^7QنQL VKRTѺ۲L Kc[il0Ĺ*!Dh*&w*NLc 4'KؑaM|iCܷM-B1!J-?Iazf`LmjwL ^~ڷp ?`ctxLP`h5BX-b@b'!F rlM)+n}Vm`* 5ٳ|>^@k{ZaeDQY!7olKbU\[Z>Xb%OT7͛)F BקP*xcWi5(5@PK|يw,brepP:7ӓ9RQ%vtB= !&:f3CmvNDټ`qYB'\CHZ-{-ssVzKZ׌JTA!0E<p'|MHRD f4Sdxvk rN3FPTm-LœQAXCf0\oP>TG.V k-[*fO_D?dąޓfWKש#?XSŤ^oTʧӖS.R62Խ_iD-tH*$k`ℑg3MTq yJCDM'W9F Wr?9{,tҧjЦ:!ĞjՊ8bT'xD(WۻqrPL2vZ8(Cĕr m -I X)Orr9KMH`$Q(XN$h}%u:P9/x76 !5Kly_I2r .DC#gVVױqh#&?L[lA3ϸcv'[T'cDq]K Io+ ^p9,}[\\1 OҁE]#._y?엿7/`o}t䩡fUP"'6bmBz e$&z>"AAc>:䪱fO9u4 `~ie'ZoH kMO?vOC;mnmP{\g_u\_ZXs ڙ{#>Ϝ>['&'D!DNOX›50~4yR X6&`k@sCޙȠYRQ:@~iueua~vccu˾#jf"6M n%w^B$H7 WlYY橢#҈FbpBWkTԀ>iR_hL7~лIvB TMכI<:&VcgdO 8jm@y&s/J l QCM2arJjxߑww3r/觊131":Z4NV9a*1 ^Cړ^fPZy{M+0Gb=6LHzϲJc<{eQ]q7Va*2%ArL9~609ʔ@=kp Eۛn(ēX6;%87 6mPT|32"1.bPB >֣cŖP"b:B]Ò0hd;nf퀣zْCOw]~ kbxBdP 즒Be=;8Lqkٴu[{ .xBM UR%%Hhvhv G s K1XzyrcE;?zk˛Ŀ{Nt8gOtj"ԝl}{쁊pj§z}Ro+sKAK;yM 7DLXKwX[77as V`GpQvS& osjmD k_D# Qp~#M$ҁ_ WQ?Yܓ5Tk(&MQ,}pMT2{V znƆyeIvsQ/]0D]/J :@ʾi]vBG\'db6Z_!vx6D6|yaR`En%D%xV sJ/ɝ춬6YϏfM( hyӈD|'MH-;B:~ő`@> Hf%e08U@y+J{eGc!@pp83by C)䮒e=,M4'~#Đ[E՘Š RA_='7eQOm&:#=c2NOChgmmH/:cG/s<62D(sCC'O#]=@aIXp⾊fGK}8S`ҝf^ єlؿ<"7<q|i#c\gVq},u%$Ne _(<9r RbM^g†K_2ϢJ^|2qWM9;/Lkh|HXMGeclfCe9zd <@t8k _bR,rY|) zc_jPq\2ht["֜%I{o({湴FI"`bAKh>v+A{ lXŵyDB^D҇2ZL_F fEc:zOT)U )&5h)p"v,ɸ/мXnvs"Mc[:aXUsG\A佊J D[cU28{W"@Sf ~|@ZT Vv1Ur5H5PɄ[/ܐD}˝ (`Aޭ*#<'+PqV:oTx-,UO'=z6XpA6n 4tܤoiN ^o^ M>ۺ ImD'DݡIW#†y.@vQ;V$֫z jJHkJeIhjuSqIw#eGRxxK8izn_Kb[;9v[XZh0'>,H)aZCw2bhY pMc6run* jpцg(Xn(Pbh4A:F4|];gт>ֳ%?}6펥ï R LR[='\ıA @:M.5#@xx;)Uȗ5OV4F&׺FtXB \<-"͐]Lh 3pUaYWzB,\gHDV0.nxKLr&SP$&P)}<H,1Aז܍쏸M3">jaq?a H>@, gOhXD4DvWnv%ɦc$K9{ydV hӿ~$oݾ333D V,,-/ǙϞ--//"^ZXX}˫X Z\^B~>?%\VQ҄H~1D.~„9f*~!{@L'qFJXI@T3Kfkd (\4>0W#0Ν={1lhZ 0xo>{޽>|;E @9zEgqen~an٥E~d44)&8gxxR4wk( W)qTl#sMb>r:RC /XZb Pzksb<"e`e-O>نQP䙓'=y@ ;?6)xu^Pb  I؅ժdݔr];n8 K#hVƬJ/6MH cdd݋U[5j@~ 0ReFà!Z|ZEPM0(BFAG^V"mx1 },OdN6p/] mS(o N3 "- **l rb{'ؘo#$` ZZE"*8D M;qxěiu,LP@̩trHS @{B! )u(SؔqwGCh/&GJy@ hבF89߇@kfQ ;p#pFӆW4x 0狪?o<l!dpSQ]`ez5N@mr~&`~uuytxx|bc1ÇǏ:4?9z4ωcǎ=|:+#A&/`S߇|s  x Lj*D9~0lln G%Kyhr \18*i<u_"VeVN_ӪrPvs̀3a=γhX¨ť3Kc((-9,bvv(ZN866Ҙ.ɻ 甁樂Av=Eq3- w܌f R~"r&,N,G12B{M2֣1mݨd`q R:@og X˝[?oc6~ eЉzU,]eoQ5B5c~4li{5̔%lomhbW&0FHP [5(a PZd[6a]ޕIh|~K'ȵ{xѣ;wmPӸME~ ߦѲSl^ fݕL*e[[vq+NC2KP:")hU9$9+,w!=AekVތ[4(hqyL, DaóCWꋋK</uɄqic;=_E,vq4 C&=?)ݰոZY:z"NuyPDx `z/(zr@ȣ3"{o/T؂:ёgn`6h;p,8NP&Ko'0˙IOK2LǓ/N.$e%WDȝ$&k90>ֈZZl!-} wIh[[_[:$[&܋Pi%4{bB58n.]Hr ?-X}l_qΫ+.]xߗ;[Q4Ǐ}ٓk\mhC[Mg\L7a F%OP'~+oc[ w@[L2o܈ !G/ g_~͛7WVW@7ολ3gLSV{L"ք>,Z1=&p*P s*ΰlĆ|$b99  ⽝텥ׯzT(`x ')|QλEԲ0$K@mXǏW~Btǎ}|;q,HRfQ>ȡs"XN8,]w mlIOy1$Җ?ko<$ E+d'6׿#l߱ `F޺SGTKp\vc~'^jFPa%$r*rT_APq(IՂ&Q-꼴BJ8 $8( !VT>䎒Tv-6s< p%Ķq)S x)Q#s/ ]`F Nɕa't؉H [uX;-v)NزsjqyOb%I'hDiU0ѠSw"ۗ+05DC-=XT9PMځG&_'׸ b-l R<mHV<0֠vI6 =ej}oT';jZ _'9a02JG*=p`lxyzǵ!n$nSţOKhx֋5jy/ qF^ FPOϋ9+cR pp:t_+ŷu;=F0^G.Emʧn) j駃浒n/f-ëXPGB\X-@ }y#Z?hAn (A ǻ^b|:)My姠b]hO; ffiJDJP!&_RMxAE3y-Pn靐.V]% žxE[1Ũ \qWuTԳIaԸ6g"Q"a6{@Q:lK'J᷃Sͨ)7EB+X87Sx2q.SQCIm]Zaɺ-][^;[JvQ")LA`ַvgZXWK`/: !Fll{ -@ `4DҪocle6唚//I#jfS^8r K޷s^l$RΓk5tv4qQ`7씑~ X4#5*C_gL|CxpY3C.^̙^!Tk^=_H bj},8`TC]tẮou p&!#}SSGc4Z  zy[>x(G=M``ltͪ9ECH l+g " u+nξ/^jii.ο{._:thbcqdx_'㕱Q5>6911?99ٹ'Ollw.DJȎlT4䘰kFqCCt$ -AkTp1D$3[I"3w(N=,"T?^o|SYΌ_?V4 <̓CP+@8+ٷoy;n."ӁT LMN pʙjzۘfH%SJZeP`U[ŽBXO%e)Xs{X݃P왙fft)B :|#8#ه%Xvr& xܬ W\C;!=sup p*ep+"ï1sl=.ԧ(M fP 3;m)JKz\#i;aJ~}\KTlL gg ojLڑGbB;22*[6<J tfN,--SG^-l-$M /C~9f[$fԛցvR13EV4BV?~sDNx(T|@FEG&CPn!:vM*ݐ9$D@5LMbVuka(*IأiV |D2N`Bi |+&Nneʜ<=v@ƛeׯ!-K`-Hɣښn%2`: l(WH+T|=$Gܾ׍{-B4gkbkQ]UζӦ(`̆[ F&{F'h.4FXa-$kތ.mԏ04ˀF -# ԴkH9#lu} e|\Ox¢}4ۡ' FjH녝cJ JtƊَ$a@+hކR)Q.oLoH"雘b|V&D+[ȪD̙#:s_<]}0*`8ݠI(m K"$;u, ҤR!fD .P^xhI5uV*й 6:Ȕaa67 䛣Ye~8~4fKG,aH2`r) 1/f? B\c߳6'$-i<%N$׮_ö191~3g`r$VΫ;5jCۜvnvp5`uvLD.[o8N~dr<Q:9Rndș-b>h,$R-le1M4<tB֛oy#G`s3<1_bY6tZ)k[kۛ;ۨ Z][w$) CC#\yG?o4⠊GO߼qi@7Ez 2VO%4EQj/$ʨ)"Ž ~U1Gb  {Ε޺@IloF/49M)y),Ȫ X $k\衙`}O9*^;2ն/ImViA6UvJ?"Z,.i)ҖIqi5kUT>d41pj阾Wܩ*ɮ-1+"2sB^ݎ;VElF%n߅ 9Ud$LZ!_vDuP!-BY'NA1R:$lK(`2³Ktr%wf:]Ab"`Fu!o=a^QބRRt W[CZ KlH# lLݰ.t!j8Xw-4Fz+ۡRC(Өr?D22rrhf?~ \KrU!0 |ȿ9ȼUy9"v+XU?*_ -c^t܊ZG~Az*4ʤVLXÄ}"5.pa :er Gթc(۰2㸮q1]Hn/J55mYsDh3u#VGX*P)0%|kZ PQ UZ1\J2|ijxDXO1-f8QLMK:@=P 7v˗޹?0vmƍ՝mr-Tݭ͕W[ )q??v*ahQ6(r ?8lwFкa%<,#JV&|Ks6$i2c^WUǮRl0S;jC5 @W(E¦D~@=:@]/q'Ͼ\'rI7~}:s5QEGΑdᑍ scś~f6aF=p8#VҐ-0UIJV/\Cbw{ýHaȲIݤtsQQYA\Щ'\~,ηgO~Ha q?c#Il !݀c_Q/:\j/ZbͻR(:wNrSIė8H(7ceI%ReM(, T'-}"7+{@NysNP."2P;UX, S:P-,堉nqH[Ǘ*%{a\`dqODQǒS]cexT#x k;?g=߮, =Ss[GLӔǑ6_uFgj|O(n@R3cŽmMy8ZrHY2bU_ UtxYl'WhfC.,0[0z%:rOS2QSEcɉQ=R\M{[m9$yP^ Ӯ}X:}\b(jұO=H.i+5F9FoLb+`r1ehBJ`/Mlh4{ ~հOtTH yF+kEol.lВFfI7vTo|hpQ 41oɈMBWLNV}׼bqOmb# %l'w~c,I8qaSXЏDҫS 8IS ud5mo*=(-ih~ >Aphf"tmT\PBV&^c~MfR? gE:A C|A)3Q2~xc#%ێL0hnZm5eBŷz IeKυ{PIѵ7`+mtdz-{8WFX,lM=ɖJ<#DR*(~rKSk !nCN [D1&dCFg(~Z̓bAaf_@fZLj} 5ӉEK: ,a>Utϐ[@3(Zae'bB0.t=Iؒ 6VȎB2goԁ7CՕ;޽wmu /Nx3gb{QsD(ʩM,t< @w@;qg~׷^_|G@ ,1NrH|%H&؁I9_#/Rf<3+f_߼+~oͿ;߻z'OqXXS ~eA-bpv^̎|[S ;>x< O)e8Xp|5.[5#8BG"'PR  i {D$)*LF Yx (],tj)揉w*dpE5"*4|kAOC`vv5tKRkFdh}romߊY8Xr-m s[~ 4A[J*.Dd,G#2vL+l*)9q8]:( 2IIpTaB7߭<2*)d2GqQ߷r?L&"ӾY؁6lT?(b M~ 慛cg֫OZR`'-"gI^nXuJ (MV0t; . _>7ٰO0A;n!-AgGsu/ٻY`9U-<Ĥr,-qV5: ]9.7`PX!(JFpӬS1,p|GJ4 $CAhs)Jb썮l$mgPzQ(XߔFD`DPZȞAe$m^3DKd-y$V^S_by=o@7аOa[z) v*NC;͓%cZ, k.dJFڻDCr˻R!|*\}aYHͱ JJfĿ.1n]V`P5L(/qY_8) 5SB?.ȍΰ6$fj!SӟxF/4qXtC>[Vto<}r>oF>G CP»/=pFy-7H^ӿ1z \dݚvlOj?@/q|0~9a X6CWθ UCnI eɛȈ9#\?uؓ$r"/T,&G^@'zswyd)dYT Dv6oJ"Í~Jh.kդrKa6;11Pܺ}&=F;7zͷjj0f$3eeo_a8ׯ]pͿ7~чO?/o޼ "J* d(L;;skOT,clTH"~Z'Rawk(/ ľ/3hs% `~~ɭ[>ޏw}ﻫ[nܸ~3+bq"-xHe,0BC?yޕ+GGǷ6w>~Wܹ#B֥%p>?6Gg ሩ),+ƺJ:>஖8$Dтq>밗x Z \ؾbcQdeDRlocM+h+{ʠQiI3 (BsLTחŀ ey4(>LUUA}3%9 sTY%=Jj˔"w+ͩ=P<6c3#b J"(#Qѳ4D7[츓C3P(no>BEw* KbBI3)D0oWidcKRYAMx5JkW:!R?"%EѻHk(ɣbͧhZ s R>ʬ6e Ы4_x)W/%&B#U=\6|.UX)df7etS RZkϢ̿t|o˅S@&2AD"}<1#uU(V(yy5p`rTiu^gG";IJ󝆁=Z:9JoEn('ՙh( KɏG]5\۞)5iWB^E:aBЂr5FP zq fP%:1"k|魶پ>=t-u)l]R]jĊ-UZ!l))(BQiw]Eq~ӗY{W*`Ax@#t5%eᶐ_)dָ6N ǕX1*.H ,޿Kˠ#/^x!< O+7#q\'OSǏ>յOB3ٗx^sK|A_|͛7G4pR[xZ1z-2V˦kF͸}$2ľznXի׮ݘ;|l;u긗{ts/^ɓw--h kH8"c8 1<]dd.]:vv|13wn^\XCmA*U9j2Zh YWtgief!&UM9&9(s[N N,(W>bP,d@R"ɀdWaCAmn҇U$hAWEV}%pMdC`@ 2CeqGE":ߨ(ٚ 9rR,bwbŰfE+~2L&xx:E=H(]tUis /y{7r4QD[f/҂-mȴ,)]̯?cm_"c;O4Iτ+ްk)ҩc=wdVUPBHWC;)&jTSq'2 -mb|<$XS[-G /(SĊFبA{=^!֬[S)z KոyJQDE I]#E៚^;*,\d ?fKiA~H6@ {4,%Բabeq C+z6MI8 Ait]'^y+a RRUx"Ѣ?JC9>ȳ5ihjT@p/m|pYk*#SXfh@媱 fMK۴KkS+`kJ[[VWЭeUi >3xK˫kk\ZMcwpu ڋD- U?6鸧Ulb$%.T---Q^02xA?phbdhh /a=3s"\Ȥ'BJSi0MKؓmZg O#:=-rHܨsh9c. w@s4{VB~bahapSEDpP7tB$j;$7WCthx-Y3ixEJLK @˿ebMMБYปq:5 !Rjq,Tf눁,y=逵r~Ev[0tTa<؃(kLg(VR N2 ;pfE<Ďss~ȡ˗/},}G~#DxjVsV쇷EzrrKo?q)< m y YFLKqѓ{Wywb|9uOq"&Y\k!Yi0"IibH⇥8xWp۲yړjN?*I0 9uU_+SZVFϕY:~]"D.UsZnR;(/Z:?%%/!Zm#'DҐb!N/O[륜4R5Xo{>o_|){rI,-b„W'4:G,.nn`cӧN=rNFhĩ~, @n97nz:/Њd33_Of.K8">Jz_GR1`̋o|ʋOh|ktlh7QsisCCCH`X_YEA'Qٵdp{A 6vP FF| o]F  ~ZϠ/|et\. d;ik:93$p+jWUU[ 6$湹A8OqT؇J M>}i>@Us0s܈ i[ ;UKe٥ ,P2Lƌ/eQe_F˔|>!ه4aʡ5G߸Dh1a2R0?ŐJ~ިsAÿ 1ҕ\4Uz+DD-fWR9CD|gҥ 3 H엑A :gdd{=9W1%8DxYBubtIQ A1oW=/$&+{һbSrVFHȴAN; +#C5ְFU+\&l~>Wإ݀;7E wTB`5hJt9&kT[`?U~)[=2w"'^76zK( ^7mQ&WuiFF t:M&ޤe߄ջ9nٜdqi',< >Z`6|z晽І<4x[ h\7d.`^dk@>)EODAh׾xǐ5BIvq?lӉLBR EAڡ>zb<(ȇVki< `dMm ;t.!l)5-2R cmr"Hض|ʖ'p{|GEp 7֖6x(cy`k3cR7A٪9-:"BvQc~ƿmG60 h}Qn2%“`[P҆l\VNFhj nѩ`=t9Ѩ}ŧN>Tp={F;k%HB!Tܵ`6%B\d*T6"}AYc!ExFb\ HG3ܣ+/re#hWcLPkCY fCOv>N: Y| FN (za' \+р$ݫsZ #VW` mo<~&񎍌\t勓|ivh9E:Kٚ T[o,\}LqTc X4ä YLdSm% r+EANN&mRzPb&P{Ђn@u#T9g0~lA29G%}2cMۈS%B=o$ I5NAZEǗ.ZY/ $;J JՉEn)(7_S S*5}^)3^ɽŀi*G*T0-di0[J{eI]Y"ԏǴ UY1r 4fs+R6bV6`srw7zCS`%p'pJqhԼ{5Ho&Zr4}"!MfB-z;/tZ#n {dA ).F $Mp-qj- V(H~8o%-#LmbNCAFm#۳lU{D9U*κ ҖЫ LZLAFOHuFG"Z<&j{P{2^*LShEswҡӭo⁞Nr u4q7N_ewv R³-~\ݩ5/!Wȣ˘e'Qu)Ң+HDPOBDPn2f%w ׵zzXεK#}TX.o*bd^|LvsNɕ/9>'(E>AFs<ɐAQbGkJTߝdb #f¶(MX#nxY su&q I0 'QkvߓENgf[oWy:nS9d%Ys;oƬ)*l*-sBqANCDEײy:!e~nNw6fұ35cFEpC9|^[B9_1XzT}5D ;gppF\^>8GaF>ةT3<oo:V# }e%$-JoaJlO=u ]]0EcN<ƹw޽<11LGɉfeNqٙO?WWWW?O?/tzNŽ f={]Dh=w&&#姟~Ǐl0?~a!Ih|+Ԯ]] %DܧShK aƿ$ ?d".m$s3׀=rb/q-,fwTNA <zq~8zDžJn0VeA$JN  ]_ N%g+qDD"&̠֓ʾ񓊋kFrMEV_Fd!-l1. NZ'vhj&OTYn?:?2b}<#ލEh{pU ~'S}ɶj YPw٬Zp]!Vހt|½fo(P?+d{@wcLOs7,~ٰ%_p ݕ7$z D(AФ"Bl_b|#j=wנԱ.(p8>VtW< M'hV4H3(5-u|4>~=uXk/G;54\I+;$’C>%S6@f\w=y:f_O>}mgfqV__ٟ/1bos>r}N=>@ݻwk_]}t,)Y'&.C}"U1NQ1̜(+iS& A5V= 7#3P՗_!)s}ﻨe8vE ~807`2v]]C+###VnݿY/~ٗ_|'0Hu*enJ@2;61zKCS@ʃGݿ0RpoZ,C))|W6 ]gy)!E3FbW%T'䅲aO$p@M̯ aoA[i9 $*t `ܩS$ۤ(*)}:FyNj3Tご:2i>vP؊WSX* q,~e޷wk5l@V?f,]HZic"S[QTZeRnWӝG+x5m^H$$22Zy{Yl\ *W^L]ru4G,W1UK5arYGzF,@-#VJ#9?zSZľw )2L5KNhc+Y1 l|U1& y '_d%7'6=`iRZ Y}L {<&]$wCξ,B|)^b( цƓ1+׃WZSlC&lI"^70-[Zo Q?Vt Y+gqw>x]o 5oUlVX[uڈh(q\ 6ܻ= *Gm.b"~! .NFYTtet[`L-Bmj%&;ŌY~ /OaR~3 7ʨfPbm+{r:|C͆Ml,h@&4~qИH)ym\yfdM˘B T KV968_} q{Oc3ˈ;gJE@h38_Ks s K 2 ?08::~=yXpرAzZO!1;bnvSH S #'[Dˆ[Ȳn5D[<$kgT@DR=~`^ZZ⇾ՍG|}&%؞rmd6?A1?w{ԩS1 ,Aϒ5OSx7 HvԾ] PE@m?hz33g%EL_ApA{@eueգG8vdT>T@g,/<~hii ='GΞ>ua=2ɥ%agϑ#bqC5bc7pPzQ3eDx`S4x MyPlf,9V\Ȯ&)8v̋HXz`?TЇ f&G9Bim@Y$0mA$ .MTLq (jYx̟Nihox2Yzt?gOf Z { И`dvcFoeER:H􍣫Zh: aZ^$wrhDCJJ.ETLN,6m*ΑzZq.ՙ:Q= U'zR#-oqfݽԽ6ZAjtBk&No1EL*Ќ݀sZNœFl [ T(. 3¡ Kzh6,D$yyW>N&s /urZYCQc\7.ژ 7qpqq}Zu"Dq1vbm)}dսZs6/O \9 IS%8.2DK tF(NL&''nE )]t@Tcy\38@UfXi¹mV;g9!;ASV1N:s˰RW6VW1-bRȈKN/y 892x5@ {~DDSd't%{ X0"Va XF󱮯65LY`F v.&hl8 sa߾OGCi1xWV֟=FY'OON0'WB#k"82PŸQ)$Z4Lc[^zΝKxo=wA6q20 z c25I8u``mccnaq j!}KK=zс3֑LOLMMDϵ( BZl#ɳJEIMS֎9,a4,$HqV u-) [oӧ$w\>r04vMC{D>{ן~_Ƶ2ƈ޻bv;쇁\^Y[]:Ϟ9|⥋j# N7ebA7 6Tg؏~q E"FeadIs1=ކ (iS=; 74kTvZkL"k cL>[x%|TJDkLbd9=;/|,A6N5_搄AoPatkW*`-mD K!z!_J .a14[Qw)tY\ "[+މݦ m\g GȬ1#QX\fAq ( 4i,# ]sZ2LԬZfGB;0Դ,$W) =F Vf$Nu4O~#2F4gf6j㶪ʏuVu=,8=Xwh/o-Ԁ MpSF?m+"r#{v9bMJHiBE߬OY{uRݒU fLДqqP=فRh†5? ߤ.$e, ]$*8 ͯdԠwrk# BK4xi,)c5} *nl*eWPHZ{lLEfPzo[+DB8\1񲾾K D얲PLpM\О|-o5=F?'BQ +C⻧022#DYE2Kc8Ș䊉' ^։֤zi_h0HɄC6 &%&6wf1ˇ)9yawҩY!~Sa=Z*x.Y8&d $U֏DZ#(мȥB9b7ǘXUp{C I1>% b3L7r'nV+حL lzR* 4P_dno1Dw  6@6d~t:ߨaN:rыiA:~&%5 8=`u}<G=1zB[ KԙؑQ)tټ̥H.x @pݯ 0W 8{o\13sȑ|Wy5r.cdF+hp}hY.u69@z8/pW9?0{iy @+:ǡr8}hT{a*q?82:vK++آC#;^渴w[&R 427.؂W^bÅ?eysg?o_4>9N .Qr?֭ XsR Gp9]Ǘ6~{$DV,Rh(Xo "126sBdBv:mn^x T4,Ͱ j;,/eJ-Z mDtӌH|N66I*Mw+0O6(%Px4Uu4eΩ7=NҐBy'_/`nS6^`pܛyL%O\Úݔ=#!y5x_Ը|oqЩ6UD?p 僃wљXf\R}MM)V Kp˶ m8OޡXgӛp%{if9y 8|GĢ@C\;X:ŝlii=Iɹ=6 $tZ=LpqB\4\qw\XYS($؇;D{#9Y0UƝ-o 'ut ?Ly%,=q;ZE 0zϦGXa2"UTv ]2b>ZK`b"ӳc(r7+}'Μ>1yA(j]b>;}$?|Çgfpcrj Dd #m J3T;u2  UREAa` ,sa /-|"bieٳ =R$VC?~U'O:{W(V`cCam2ݦLD*<-^>}!!`ˇ$Bq.{6A A2 )eWVز+}新e~aiH.,/,<~pxp+"mM󱹅Pu1L4dp69 3<aY?dy7*PXlƊ xg{jA iPHc"SH:XaǴي%Q7FiףIiPSЬ\PVBYV1P "yŪ6 aT0¨*v-o(1y連RƚVLgI!,zpYTKǴ- `hth A<=UTf¥FEmZAr H03V]x8iVd9OT,QRDeup[~ÐGz[*DŞeLC;FF kH><߆b$]izX̒kFa`2+ 4ۦȜ5MH h.TJD2Mi3pHmF%V|_(C/}Eie kjzulېe^*T] E_Gǀ Ƽϧ}G"vV R0)fb`iq&'N&7!̈?Ϧ.}%r*kEbw^R{ |2F6{p":>~ƭP߾ځ /~ч.]fuHƣtxD$huxϼDd~?GP88DO/PX,/_uG}һGVA:ڀ_u?Oo[~Xv_n`7-MAŰXbl ;~FHooܢCbƁ!jρA)0gׯ~O~~7`!H^Q|fAl򗟬-~Lk0KP__rc#o};W9Qt0~8ˆڱ>R1LA/~;0Ͽ+D@-`k#MlddB!4(.;=;f!@iS\.,1<62 =~i!|0cJY?`fjyrb*CC#H]e `-!XC<(Vm Pe l܀/ 3Lʃ!OήtBzW>_ nΦt7QԠ3_7_J.3!ShEv OQCRfN|`=i8E>E^9K6" TaĉcG "G"LM^<4?+3`Eg?xJRmr$]֤9T*, яrCwn^ڱ8Ð,-?X̃;THm9;;l$:*]K bxIB5. 2!V YƉ WDl. 1 P"UUP0<bjr# G:<ޟƫmő&3wpm?yHӷ1XwQĻ%b -Lٺ"{lm+HN\O[s_y/ħF l/±|` bVrpgۛ-_OZFPV2ݟZOy #YNE7F[kHbkjD7K4b/ts/גGBޝkwȝXX[ y'gר &iR C{:SRb(7-F%5CN4qUG3^C56փ@J:X\^ ow#r@)FF`쉗ڳ.]ЋOfRe\m̍r;#On2#Ò Uێv $p*C=OrZp||SaqGdۄm'ZM08j*ϕqk{ӈ m!$㷉\'rr; [6M|xNLAyGSHGJJRS|5{L0p c#c# & FV5*C('Og^׼!e3]4cpYsMs_zs ?.'d>xPhTa&$q*=zD DM>J'4$k/f}bʓ'(H8ƛ8…7yy{cB.ff翾qݻ3/{OK~n@rG'& N8cK\X+#qQd6kN[\[V}A je%ɄEmTF'hQ3*T- ,P7(ΖM31Hr!ˌŅꃲXX~:.@pL TX"QˀWU$Rp ;_Nd{htKbB,܃IXePY<⇿|L.5 9'6%;UQJtrL%@Ņj"IL$8#Ѷփ1xź܅CR DQJC*N-Ki覿{?6yOTMJ l[REνGs?F `[k .K*،R#yAE+h\J%YXOYn^46lӥS&,rAgM կ蕨i~@dvJ"?k EPBƢ2pĘ932~(Sc e?lT8Wshߤ0\o);*;71~DIKK["PǞ-=e!v wڊm#Fr1=Ł(&UEW˅$cc3,ɇxSin g|>/^&;aLvGg+<Ǯ\481ϏI9tΰJ(x-BGšYE_F(Ax\8mQE'rMqZR3Hϋ"xxe,x)gs="&AP~t:61]aSQX)ý-j <)wd( !7Aѩɩg<@smPl/~"0$" Ga\iy ske<~=uw/<* [bP =ز 2fQk21qk;WVq: v[b6jon]:w~ѕ`8?Spen^O8ݻ87+|u'`M`,$d+D+Z@6%I) +!F'q([ۏ/>G{_ヒ .] )C`ys5Xv8ue[!c .xQj#3295Icl7,as}}kmPf&rciueq-sD%:v[]9wͱ1dW#YaPqt:߄P"2υTgvni=195~̙'NKy,BΧ&8۫K+*v}8zfv~yyq#ȇ < )b]`@XN+\%*>g`(jRXR0/ ooSLm;-pk y^j6K]i]@XvW+3Gһ!\%6#t4\AͩBP2MT!tELLN&1ik)Fݰ[x{?z4{c{ ; y{EӶDCOhxBP-aFG<"EFa6૬f򠸞 ^g8<eЊ>J"+Qh7i{k%"_He|3MԖ#0kH*^n($VWKSɱ9{. '0(M;qg1H^|?SSlm"a@qSZD|MCvn9 c"`=y:} N~q\% :P)m,6+v<~ٯ?} K(Kp$/=szuess/VV^jUn )hѝ$T\X@]BE 6F!CF@PͲp9.ch4TN\Y-D)w,JȖA`> lzٽTQ>F5L% ǻaH$_xX `Ui|GF6VfxQaQm!㾍͗޽ w&g[9gRdYĢ^:Qy *T#3[>HY ʈ ،k5"k=8"uׅIkHڔv*u)$G92y87ĘFEL$<B{Gz*񙾐*tg5̝yl;03ϙ&qfkJL c+1U BV#\3[|f 4֧׻=[rqw~:n iL4?k:&ԉkZa bTw>jb| N2*)LCH D(FtWHE2&PU9x*nl˗92IĜ5F.%vTO݈6{5y1_g0ӪϼQ@0؞>?GƐ`Mށז\Nh64ji"սaG˪a ūߜ2CI+ڤ{/tEH+hb2xF#bs֬4M;[H<\x %=ԐF{n.ϖryb )yl Hgkf" !d@A_L;"}rd77 h59=za&7y{@W J2>\Kh"ڻWd Z]  O,نp\q[084]?wФz*VcБ3 L -EU\ k1N7CdzK@Ci{prxGW !A1*ӿw7V ;¥KǎG?Q>1,p ]ZXX__=vo}sn5KA_~ƏMMN:wQ">91xէNG C>u؉'1a} O<|ʓNb>c>1fffxʕw޾|̙8avʒv#^ӄbHSc##ϝEOزt5N(ᑡQD){Obajrjx. ۰ʃGOnܾ?/>ڗWoߺ}޽;w#w8t޾w2_y_9vۨw.^x *$MRjY"fgfzh<bH>a.W9'O\zӧ ΂H|)9zdg"cCHO/Kx!B:c|A=`mR9v8:aF8 #hv`{.P#/;a ̣4БNv# 7W6Ch\r bS0#)  fpv𾣘bChQC?ׯ :W%'y\[Ft2N)N>i^R~t0ad"B٩ jDL+Kfd ee;Z9@j-5x-6U_!ke B5#û{r4> iΤ+%$P$CmBy>22D#{n6zJۘgiXIw `d/cQj#23z/S>AIDHƆ̔,4 F\c#tʺbk$_L4 '~7fL뿰j 9Y1W(G{OE(ƤSU9'O;>Om<߲+^+͎Ź(< I|]iOv'QKͱ2 +qsݥA>( 2 z.DTB໺K8~$5 1렗'8^YVmk#^42,~hnJƉ bs{>Wxۃܵ|u׺`򤿈]d115Cazkaja=uZ⤺[T,!$ތ"Zőj㹅 F:}=.PLsA~|3P&*[/2IBuDl9Z (@d-y9vu~_)рJoD ,)މDR;U#_tTdsQuV"&UIGV9>^U^؂E 4UZI CRq㳰8ޢÆKt0phER-J  NƼT $iX̓4M#=b]u|ĘBO9JxfaUB~_J$稁=LHqG6-XN*)Y~1Ć0D3Db ng$YtbI]x#b.˒0I554f4色!9$j4y͛7K@Y^,?288>6467:iqT#:oikk~uc~a7~ yw޽r1xnye;nP`oH:[\&3H-,gn~ɳ珞v؉C bHp?j8$*~6)*:xm^Ig6VְJx`+KGG'qε\A6?xK^d8BS* _0 8bNDX΂ƁҠR ,?q _6YӁ!dQ~o}mcEcH8! &c7j"T^.,NHdpۡ"AuET^j  G8LJlm?j+{|AhNVa-[6[=Y Lp\^56LStZUp66܏'8ckN)x7v-́8"ZWS,\Va[<|=Uiwь{N_L ?$4Êx]7ˌSse"G3abm{m #bn}m&̎[veXh X޼0x[:a/|ѥCe_uH'~/ ( `[jY{pΫ-E%>hF ?VŅJ("TDG<~\]AxS'YΪ6kFqiAZPPxFH[4 I_^ju.DP6 )>(T֊)FvGʷa.`j>4z+6'RniI*FՆhcE=$$o`;~c5g@Ҋ1)S>{-TJܐƞ\蓌3Į1V|?60G?wq$%, A+P);ht+D؃3}H N`X!!R2nr2 j" 򈃵 p.m5#!/<9d^B΄x Tj^2TڭƜNw_W6Z΄A2/-= )y69 16@lnϳOaƚT Gv ހ^[0 H =<ʆ"*.=ZZ'v [=])VʺdG;njř&9Y=FGksbN\:Y0pqid`8># :&58!mMPX|/ů.7h5Wrj8h`w)z 5\|wuR`%ߺ]Ge:^^Tbhw~u-7ϪƊhon+ U/^ԉ.d#=kbu)Y̖*N2ljfU1_e{qq<26zM:-FKSo4vA/ 4w8NB65MVͨCKM \d# ^ZQc- T,B蜊i񃻷ꗟ|iZLR;LT?W'kWahsՌ4L3<5RN.m9 rnHT Vu|>*jE:z5>އ88 9J.#PpBuV۽Tpү KCfy :gUo. 4j F`=:w7 Q+{j9)[ …"uX[k(^h|v*^pUZ 2TsOXD48k<ڥmrzlmWwx/2%| ~5x2jtnR0.lЌME,| 쭔!uPRS /r!#[Kfiou5ȣ`W'm~KEh6jbVA"͐}$}Ѓf {fY|gJ 4AjVlБ^Bo+h" &u0"PfLT=#S1?xaWQٕ-9>4v&uWҷnsGA]$Ӽ-b  >?}FJ9VTϷm,s.,DN!T63l *=aX~X5lhjc JaYz}f0Jwi؈+X)kʇ7Oh2/_umBX{ r\La9BՉ#A1 3,P*J|,U;6jo/׿U.gpt*1:LQ;!Z&n(xCϿo,։T|%{/VX"'h- QZ, _ZAd0OVK yJk UPMjج*$X0B:Ĕ֑bIF3|:Dhj&[KɕǕam?Q)%^ˋ# sQV.ԯTB,a}5Yy/o`@+8&b ({[nd*PZ&WYUlS)(PI!=56*Z -"Tb(' ȥ+Q)6UVoy>{ %R R)Rr "QRH$KFaP(z+-Z[|("jQBQ| E͊$A~m< V=-JhN06~xnW~NsKծЊ=XoK?9}:՛řRSe. |z?HRaH'q5k+ښ&JH'6RCm90tsipեY֝cK.,@}YnxRK0!gYԊbnQsk(͜z/LF *qA.2WЊv{NHSŴX:С2ꫲDԱ4XF}5ZͰ|~APSO:pŕ[QE%e6]>k-(i/VZ4>)yEuǏ֋7cE /M$Y$ ДO(OA(6 dW/^~j1@m?w}%QTĈ jhܬ58s n +ߕQ,ZR)rAQJj2 :U}rDնn@%9Ld7(P\\TPxڻJ[שSUp:"lTzVJdXTׅ.S"l-|dbB{:nCaD<: "wp<,?>Jcʼn{#\/ԌګA>DT3hHF?.G<䃥^q6NL- :@Z]9kҧYxIe0~ cFmi ^U)N xT %@ʍ%a+W*&P $wʀPVQBrpRHJk!hRf; u#TD<lԒǵxԹQi%/I1DB|,Ԝ孋lh9py2WuԹ YO%[Վ%UY߱m,@O:ѣ:CĦDt?h4mdL8VK!Mv;M-$'dSBJћ* #@1|i `ԲicUG:Rއ%llaJ*I L_*$󆵇s}-ޯ;堔ئ ixowPxGbO⋵.&ZŽeˊY$+ݶB٦Um~ͪ˖IFHvaWΟӍHFLt 6L@A}c|96"$)\sz , 5|%K<7yj?P[#L\.$]KCf~knvn=9=}( "BUБAQ2)v6"@t8>ݑJB9C p7R~K20D?ZݿSUVߴd= >ŹW<:cޣ=.FSuP4P|`Ɛ7.ס gǕ 2sVË3䍱c|GINd~CHf״l$r`nfZ_Vm[Zhfy^P7K 2dJb$ń2m.i++Vd~sdp3b_IY'{ueotST:^m[u2"&3F4F8w93H(h֖C"0bS_yb J.H)LT}4!&h>Cy>RtIeʋV"ɪy_m \G`j&_hj>  mcL:XM֦ j,ܺXjdobmCW/k‚OT_R0XO:ۡ:0G0Tآ~ך^br@SD o J!vYֺ /AWAEM=**$ Xڭn5<8V>dIzNr%']pQ=q|Ժ^gSB:abUI'y0^;!qtGE .@_A#-XP@d-fSz nXj ^f Dm\BjΧOORW=|aQǢVP[ɮTWXA1 V'8ZR@%8=ni){QO9zW. m`_-!Jp>IŬtʝTR)6( NJ ¥;pR(eɆArPZV.V$aîa&V+FP &:lhP|TSv˪:=T{@.l5Ad2ޓ7n B݃w|%c}џoƲ͞;5Tj'RS`FIXP6 tgd2m>$4m7l7D-{\xL5!Վn:jF`^\Áƈ (Mr @ڬ5+yTd3|F>lZx&T36P0N3:̓0M6n?m,a|ZsIn|#uUnm i"daT[{ QP]}Zh}w__730/_O`Clr^ ))ƞɶo*w8E h]qf=]*z\F6vJ0(]UUXpK-DTgAUD2qyO֏m1w{%GRr;(<V4IhW) ؽ'6͸<֫!q$u69 dT;C!vA8KbCTp죌BD4%iQ|2*U 1ԢZ" Pg֟ )Ck?#"壘![5ܧehItد >(k9;Jȉ:!{@AN<"l]A(㬆MݤtBqrl~l zPg"oojv6 m%N8 GGaL vj gT5mo'|b2_gl:C͉ӑƾoo_`u#dv z(١uaSl E_UN֓qEN۾~V<[Zrev'lCT3 \;VWAK+ 4-h!A,6%~HzQ嬭,^Bhwrb g@tHqnp>IJoK (M;fBwJtێJCx.n$bO\/w9iŎ-ʼۼni@eɋܲ#gZ .vQYе3(>;v+"E:a:&@hC*{~.\ѧK> 5̃WCͺLAځlC3cZ8zH0t{igA jg*NTm1Og^t0TDƙK#D!& ]-H~u;c:08"'?&ʠ\wb\A~I 3+ Hq=BZ=q NSݨm2kQ^}D߫*P+yDar_W[N^&eZRpl;D6EVsQSZTE"Ru3^DwDЅ N:>1lA%UQ|T㸎aދOj%}Z~ Th(gS`@UhDe4<)'Xcو U~ PiӍ@ qed]FY3ѱH0,]!Rl`tiۉih<O,KDg;c]"Ǧϝ!\S>MsYP?/OUZ!{;5$f2O| |ccg,#G)4a z[Ó:@S'޼:c^˿:2i+*8a/T ̘" 5gxBouV2l:av}mBS!Jڸ7?'vW:Dm- ŚϿo:]9ǽ7R7dh׹,7 $/e$kB TLULվ' z}ƱV+# mm9;P}d:D1x- j}J#Mt|kO_*Z󄎯h]W֓؁mD;Iqwû"A|kc+m?&^}_0Zjoڣ|2D&w]u(-6JIWhoڂ}k( ߏC ׯ8.:~H u-hh0sF<:! ._bc9,3CerBկgO vmLq1J|]tȸ!t WRg ; g[D$D"B-΀$wMnP0'ʕQ6_:*;$ pd0!:ؒ}Ϳ/SeaBat7^j8$B8 ɢwDT ЄYAd<Ѻ9v!:@fsן~G"b?B,8dy|FIZwCגsP]nX*a趢b1^>|ya}ALVpֹ>])YcPafC:5Uœk]h$}t[\r/aHCG4 {5v8Z 0gyӶύy]8޲xkw΃WPz֏~3 x CG!IZ̳D#=o5dPRm~3.Ծ26XY`PilnË7kfk7~JHհ'#v$`c:^8 yq Ql>96՘`eu=5j -[ㇷes\КqP:N; ( g LqU/a'PGe17ݩm.t '辷)jн0bs쒩8@?|1ecس_{cmQ,&c9nS gO#M*`G5Xw &s f 4z8d5%es]{Fj'/SR֤F겢 'ދfgl}y#YSohJ/?-@/CĦ\ 4T8 Nlw>o*.y՚28:)6p}SJ>")9xT|REwex:TՆap!"Üne)U[:r N~?!d؅e36PU_t=+ʫHz,3(k~צNsl`p- q7Ee8uBIƋU^n!Ld֝'DNȱ2 L @^P춧<#tpե39# n/@ mږ=.KR;ƛ37d{TLS7)[jǺ)g$) # *&DcsjN_>EG}XII QZ{|ʛdm>RC\SM(! _:&Bt"$}"I1 x˕qt .>FGo.4,4W!t@xVʉ 0f^=?r() V>@GۆPf~ 3r,i>(^MV4ןWvqM3l,𱌧UpwuCyX"dUIpYHc7Ѐy; !&N pR'X(@*Y]j,6}3PgFnd]JTĖcJmj&KKI8c,F  h:ܳ؊JuFՃ@)c})g 1$; 0awR=Nx6ژQf\E6.q܁#w`p5Ƴ=C[W3x>FRz "8LSzvԸ֍n cz}f6~"8p$YvwHV7ns0݋,^92V]V7m8?tЫ|Uigd07Mk3\_ۃssmxo;ږ֑q'04_0:.X wߨ/cb:sj Ưha8ֶh_}[a^jɞ&2֭3;.9䰥QZg_>S^6iõW- }%bFU+}+`R思 )0|>ɽ 1_Zxaܭ%å'tfe"g6 wlŁw^DӘ=yQjƟh2Od$Ycz0=AZ>3:\f0>ި2jkDģ*.T[tm0uY1ll%sk"Ϟ_h"8LQq H 4aLjD!YBWfZLq AAVW-"|{ xx5Cݻs'wo˯PktV4EkG5skb[Wys`Db d@*GއSogJ LfZ;n%j)mKyRe^ly'0aF [,6{vƸتb E@GU HYcڊŐ*Z. DD1/oO] [ >&k΂'.POED1Mw)oɣSKfSv1"#4.,AWAU r۷o+mUpۜvTUe \$R;̩UN`khVK-MAfs! Ҟ3nEν{ 0IMZ"C% Gjm*(KGfU[JU׿o~#;4j+f|v5Su}M[YxՈuCyCX:Ho}&. <|ANq`K+-!t.EC4eRi^5^f@$ 3\WH bوIxv*p쳮lETVy/9~W^ІY| Z#4mWL(ۜc "f4xƵZ4qCrjxlHץ;W#uz+ oo/^?ӿE˘6Q`V0*`Ŏ00ܬ[OKp EѲWUlN͖Q"RHB9nr*c,Mz%m^x[wn=:BN`ikCB|6C|~_FK9? a=Yi= CtJsм(Z{OU!p0U3 j&V[|/a98AQjɱ|P)Ќ?]j;n__҉>OLfKCkė_~+T?QC) :y -+fyFU_ x>?3fO/wdU:ŽgX{K]ѻyA%N5Emut}uZIp-U4d~/X-fovbu0Esj*kXVĆ3PlvNe|8kUvBw/t^m]3B&;PƖI=0B|o?DJla6m(2 Xo®fEgx6Wz;@K.,j-5,0"vcֲg{f_2#_-Y/21`zk?<*Go*BAlGl~;V[hՈh&>*Uys u2_Tԥ# Ĝ!e;Ώ-:ʂG=6Zqo>~>Cb+vi2$# "6NXIݡznEm+U7|Zt6T2cqWD\>H#=Vԫ~XQmBnB~ѓaObcZ$BW641 L"yd\ i tvax)%FE%cI3nx=v%/^.ŒzڞviqHd0P͒VGnk\sY𡌿S NU8鑭t4n)0b|Fr۰a@${ { @b-3Qţ~*!43{%6AЌAHв fǬfL:2&elV8C܈ b=gDsci4 飺ksG?+cvK|>Zm %-!#}4C+TG٭+x358~@sWӷnIB1~ 6C_~f e*'MꟅS]|u*9 ;]W_q~rPD'62xޯ^XߩbN4^xr%l14/5Ov J(aYUsشeJm5 i~ $k Iz*U EaܳB8ы־z"sAձ^p.MSi5pzP^?>ѩ~kBYlz d+2W!^RݚM5p sm(ԅ6Z ]VP{+-HC5=l& z@^gS,Ï+9jbc2zCpEbpV G463H@Xˡqm~^f 21(Ng:qͅ!:bobtq, 4/gLr?"ftŅ1e^ "a8J|NRғ=z34)B*鐱Y $5m;ʪ`y>)L"YB]vyϭDU3r{Pa=w&4'a/BTZ݀kNOBwXDlȣ&%CE 'Id0pLzc'{ 4IϟW,?{GOuByFyc&K(ZQ.+"XV$\.FI7:+jQAzS*PQZSTOٓw겂 M" u]-*/GzlV|}c # {';r"g~UwTp_])"Z7jyT!fU8ιV^oeN7QP93 J̽#ܩ9uTLIdF́PYWtx5KF125X c6p2;g|T8Ey ݮm-Jo_}s>W֛XãfsU3W3,!,gq1}"f^4[Є+as ]j@WF(65n ,?nJuzT, .mkj`!ڍ:Fccdh*qwƭ:XrkohvJP;9R6R7^̍w4ppK{z)v!sNa%}#]c ;$J"?!?[j]t8Ny1¹.`Uv6q ( 7PtO_+ksje__xU?= F65sb.²0Aaδ .ť S)Lʳw ?y Q 6ԧ2jwAS*i qޥqăZ*:k` Z'3Ac 78u.jGJ QՁ Tv7oݪ}$J߼en%TA@8K, xB] :G(q+$xkdD g[We(j SxzK_x߃5Vrʸ?o=xpvHߍRa)nlt+E2~2Dp3YddkU#֘!ĬKyRB0U?Ja0DP_ {#D{4 L8 H18rX c,!YG7ҏ xBb%O$rcQq:PmR{,ݽwbIH0ͤp[64&|^0_c}o&q^@Ҕ0oL 76 8&sMʭS<` 7쌆AA!l8jVP{x$N2\K%5cYv5Jnt,j^ç)B&#vْ`-O-WK n\!)_j9OfK}=-o\sTlXad"fUwo_ܾslbR5/&Uwau?BPe/:Wux#nr5=mȻz9(kpc{N02Z{5K0 ֳgkx+<> q-Ҏos?3aC/I ١Wk/p -`X# `R:$0p]T@(h+rIӸ#$4V=zgQhr_~! *`z,HލΌը)hx,ߵWK3Vj)DsV\ *۵jE*P[BEiD.s*}3kSLy^ ?ա XdS#-+xj4ΰnu%9F d0r[~rM]3p/j(;hxvn%S븂ֈhPLvʅv{l"H Q[a=e HTJʦ4-5ڃm`3}Wm{m9ziTc|8A&_ m1+şnfq(mۀKZKk>_ n=NC)Wp̻ k QְI. lWzC/NjJ`O-[.U܁w?Y Z<?~opӳ( ʆ CjRf@ܖ0:m+0ON'+0tg;=AWxdet(.~G-7. #&vg(l[ŒggWRyNœNoh;'?cáNjk+A;wz㽥2̾Zͣ'/Vx+{ՃXtZ7vq6]Вoc$Ui(b zCΊh)Q=pu}!кVHj{>;kajnԴ8gs͑UyS|y0/Cɋ+u 6y׫6Apj1E=B4Ɛ,*DmdՔ}\wW}dԌ6 ( fy"-}P^Iau,7Z9VfjR=ڄ< i&tŚ犚ڵ_@ȎQ!$p^@="P95e\a_|>}U2!Gڥos+ y2  }1TCW>pgCxݸoo~gĺmۭWJ0~!Qa5T– ^tLD֖ HI!IJoةhhP gʳuW#N2Q+b.q]QedС!U!jG!Ɍ irias75^{,hac} 0p `agV%߾Y,wRBE[Ov-sGjD@(4zfXN1>hqI:\TDB*J]00U`jx^h6t{6* EUݛ͛5\ 2_6E SʤNym3"`}0g쥗Ya{*xaX(FOz bb++w5Đ:xNV:ll0Fxfjwi1ԤC'`0@5P#HUluzҝ;6 bChߌ&0-?矹qH]xY٠W;ayDC?j[F[#2~~tJۗij<2kfk  hTv1d\⤭Bc?AtMjLG0~{:n9b{*j4KLv's鞆= `pڽ lB'kt1HC>ZQދK̵7z;^UO4tI-mbb1>-9z~¡=r=|fj/M+̥wpGUYiO푪QSƢH_˟MuG2R AOU`^Ma`l53XysyyeQt*36*w)|/_<RcmڪS/Ɩ/shw 밤-QՖG5cN%mi% ӱ=Ì pvG`ҙ=Dg|qKm_|edj{^ΈcR[@)lOAK [K'^g\կ~駟WSl̤OUk"ցUD`pLԕ V8.EgK-Yn`히 =(ծ.弸|m:m[KIo w5=>-8xzXޚu\:gr>@ҍ;~iǪPr@vz͛pz-'f頂ZOpш0ӥ3dDr ?WU]7ԑ<\YiLML4e(|\qJ=s7ˣwA-mం~6Vz&'OtD*D58kt2lvΌ!팗L ɻ'@zCѣ K0"FMEk-Q_wDNhᕵ<\ K>B08; ">0|HbܞPI=݌2׻HS6N5Ry6 DgfaM_qG~[ }rIB!m#l)Reɏ49(U;='r^Ql(!qU]{ooo_*G ,ܦl.GfOpq4+ӯrl6godhFN9ӯr~ks:ק\G>y/O?}P>VA]p[&E+Zw:2)o#C.$kj}/?ث0ZWCyWzMՕ%ɱ(;b'6<71(y[WD=/)Up[B5n̄h/Gx{ LTh i^Hw qƛpơ }O,touɰnHE-\wԭyJE0͡ -簌ICa\OJE[$` pZs F y#[gZ*[k൘ij6[KD3HhpҶLDkd=F xEdkI,9Apӭ0S5e+WU+SK8tGZIH2Z:Al֘bD:fbWT>vFR-RጪBQ+ _;5Zc3DIS<1s}U1;㷺sέ_7_|]nId[!Gro []ij7ue\&-\;׆kô,4("yGyRH q_R@+͡R >V,%~\̖7ʔb=O=8f077@w֜bD+>h$ux>Yk&G~9kGݪә4jZٵcܸ 8d,z%#u-iP; <T0 IaSfNmAZQʣg7uԉ)svJ@7Ef8Z A$DD,2tAK`(>bda,ʆXP9\ FmrN~8/pu@0׀ٓ4R[jN1[XBlk.,~4 G{ Mz*|utJq8 FZ2GQ 󁞭ݚۛL~xQ:t)@jgLzWx {%naD:^P`.o]ӿO=}R!jSwaS9U@ïv5U|߬ 5.XkXܠl$Ofr+?}E2B6b.ʊ3nꏧpư+-sk:Ǡw۴I{36H{ABTh({ XFQY' (?(PBfg/,LLG].ǝ>Z!wwDĸt~{&CzOj s.F&?DHmGp7Q:oئn5&`:W#IDCݭ .hAĻ0r \N㫦\Lf۪5Vr=8DvG]2Gμ<&|`~i! Or6/E Us%m%7LHWVKK39BYB3onZ ᣇBQrYtpӬxE ˊitJF.mGczJB(bO#$9T"GsQ?CSAIe!C("A=oqK|׵ +mYO]]R ]TD@Ίb=J=30 p$QܩҫKJ%E]]#"6RbOPMҢ_qƅVSX_ɐ`y >,.{T&Z_4B .(L`3ŏ9W=|³#wV1рI߶hG3;hD-_,nR5R j#m e3cP^C*4~bPvoZ7T OQȩEN9$eɭS5)niSF^z w2О#q׹?Ьb;P{24ƒ岵9 WvcL;UN[Gݴa+S=;E[l*k֥ݨ1rx2$:#F ; wF_y͍4܄jWrqtyO80Ptw},UutБONԯ\D-sZ]0\w[Y/G:1-l*;|`pA[Cˠ\l 2~cc2]MşGy!ؕHϔ5:#WklNmA!3dkLR̬|f^~ ckKO0 Nɛ2vGz e~a (F!F# rMUsQwED>0 yCt~z[9 ÿ8S^l;!.Lk4im6-cDZ ysHFfks˄rNՖ~0)ΚiM56-eBpCaBB-s>^aL#;m!/-\k,LHx ƶSBy֖! \B]G]c剽7u}SX4húy}qT@: q\դeeHT࣏U6A @p< "y57Q[<*6Q.QWV ]oT "1@XW'"~N$@*P,ɄxPf"s83Z{H[\8utgYJ% .Ĥz+SK-K([$5\/.kjKؤN q3k9=ݶ>6S]Pi:]Tޔ; Vąs7<GuGL =H'8,ʱ*/VC4h!!|!=j4@$dJ+?~][ x886IQH0ͨG!U&/{J#6uL$1u)y$G6 5=<ޣ,janFyu_:&!G85hC9q @cLXǎ/?D;h OT>޶̚l 3 7Tˉ5NIV ~+/g-fey[{Ȳ`R2M5fh1'>paoB5M >kL㙹W|T ;gB`'U2 C N09޽- h(=[GD? & sb>=Pj󲋦Rn|ȿCWb< aciLp_^.,.jGv;P@_C^IcκP2]. XsG$=?*qtP! xf%xyZm-:S+#]#(|LC:RlS* PiM,'8WlQQΐw\BDva=q_jYT3ԵفEjS_xm82`URAVԪ*pJ)~QFpG؍^v+fvNRkH}Q{GO=z~c V^r]n Y O~Lჹ̸9A$뽭)G(t&hџ-CǶf+?Ʋ+MjU"]|ȁPK%a$b|aq_SgRف5 U"699~EFj(y>|֝f o$&6RS|jU~&a7plS#PuH:&]5DY9-qp咽x3L&ע`*j[cU[}==1 0u);?&3&vn= /lc.rt^$[$TxR/RfFƦ[c)]|uD?-Ix]3 ?,6L:ۺ0raWa}QkLVA2zaZ^m]=; |=zzCeZWӢsXh PkV ;:16lE={ TЭG̀/#/i-&\4cKJR&X EOisj{mUpIV޸.dVnHE.W\uwsQԢ?X+`;E_ϱ$YÁ +!Hq;Ƣ k%ǥtƎ=qF{vLLM[}W{ 8U+M[[ADN}/"o]曕\#c1DN♄9 $|3ɒ0 GK/>t?OaU$^`t; (FV6À~'%&/xxd0'=[<ÊӌDlIU dYYHuM-BE絒׵MVׯ O_H{V%hT m $Jڑ辠J _z[g<"f䮨߈6l4qٯ6 O1jT 7l+yM3bS ˻G?~ĈE*H=Hv 2Gޠn/PP&22bXk$@ stPphIDBX1:Eg%ʴ(M<#Tj뛺|'zLSjFs!`~xdu4B QN6zyw^4477}b_:ww9f|spď)B-wF '7 _ #G#C{eՆ'1~&IPjlb4 .ъbi$QJYtp5x[-O?"5+kvAumSW| oZABB1=lu,S /3L# m( J讓l_=zӣCgz.';zԴfFY!YQbݰz-qFYeUyyPZ"Ksga7{2 "RXvrdU̓*76׼^p;^ ) ^эZoǞ0EY`):rXD#N,ZE9pU֬*_Q/Q0]EP;dV}u`BոU!]U{Q:YĿz_,{$¦q 3kZ2d^]FX"W+F[|G[d,Jo9T@̢= KsgZ}gHe s|ppFap6-Ԭjf (B u b>-]R$˲,* x@U׬T49 e⠆i´]8ptJ8821o]:Mt |0BC΃\D8];Β𦰣}g*@PZq{U[F_P'2&~WcW3v? COP~ LgNP;ApC\EYlg fN KFj{O3L/btߘ/Diˮs#p^ӊJV#! ]] %x+q`GA=;bX?t]Ni>M2.L JەQE21_La^(ޏZ̀1aT 7;\/ c6E1BRN GzuG,QnWRW@!Auq L)ʬ(Jw`ʈgZ4;oJ #"k_ ,춎%2qTm+D6`<İ+C@Ǟ3$}s(e9[7񵝑ޞ4Yevt xn?'u$c\,˨c.k݆vX]]}0m\=sF?B" pjm!T+28$񩝀fkUmƏS=^׃7P0Ed=w(p;Q#fUac"@@0dG ~ܩ |> YH,Iq!0޸qd3bPFZtB0V.SGmwּ- &t@7ѵJЧ~Z K K$]/W`,Qض7ytERpW"͐aYE  Pۉ{TbOV33ܞP5پC8g=*E3]Fr`=7<]Y7w_2Rns?3&tX ώ6Yynpuq,#Ҹ{,4EpjR:w%t$מwŢ*/:TZ#ΒqvpGa+(8?5k2%:ApMݦq4V9Ln6mN55,m@_n,AZ &¡&ݥ(U.W2n;gtD {\ &@&Ё|m=Q*4D͟"rVSMmAx?6d٨վ"ĭk/yl{` +Z87Mp<}<2lܿ0/VNm 3>\&xMxF6 Y*qRsq8UdtR[NkxcoJ%qT{3sf5N„̆>b^̸xGMX67ԉ/skeT wގec3۫C5Dz"x#6[hh3S&}Qi@r*T {ŀUS5OɡX=رٍiƜbTgwTV0V +.!KHd %bk&lCHҝOQ#(u\buDrC׍Z۬=0 O~wZyj Y).>''M鸶 :mYHָCRv '@!?AoՌWR"=w^]-WCS煳jG<>3qh' :;6.K:EuX( ˔ ɭ"T0z]YVڻC7\`9ۣKy4TkpۭX~L$`!op&0[ڇvkI̡RteJY Ϻ\t1 u܌ }S/Yq=J,&h666b-' PhVĎpLUIq&Ta?kcR)HOy1ʲ(N贎ZZQxw`BPq2'Xu \^[,^w"M8 7XN8Vl>X͙eaZC)BDp[`D%X֪A S4'rQaDS ^Ui 5Y!s!-cuP0J€-mk,{ 匂 N4b͘GI').ڊ@A 'I%VG[#hBXV|JsLJDMFK:qv>P֙@Aols}:6$X[\>07ֆ ܊ w0?7<9/7~o4tBq:хqdzݼd4,CrXmg %prߙ[M xjYתfY 6{+q~Jwg,~fXt,¹MU (Ņ;vW)"-=ak*R nF cN"XAjIN'X()*^8X60=M\'h؇#i:x{]!)'z?:ѡd%Y]:)T1s' Eg)TŽ&-qcK0fxl%3Tk׆2~ "D^$I1%YT܁~K4{z`)r58a8h]%ѿ+UL82wh5&>iCmL O`z)] 5bh68%@HlBԢId ԣ!L*#*5 !遆nGE:w *t;sߴ "㹇mꋛ mA((hT𞸡;L0c2ߕvd]6uryVﹹ"9PBS h0AL'\Հ_-OcO3s b1T|"1h%2n.-ӟ ^:Ciz~ݴhإYzlE,sU4E%AwNz~ZBdu'; #-EJcwDNXZvB /P CC_4Otӏ_ 'Z`@^=WG Al{1vv/!y 'Ru`ɟWdUP*d^mؤG*?j.*U2.bb[+Tj aܙ w"S>bb?VH؁Rګb })纮l֒jUH"bA}QP3BnKNŋV;{[Ǹ 3@7ftblNЗ(V`ZAv_2\oYB7(k"xaxbY~cQl9#L(&XKd].juHh"WcF:ae@}VdgM( ެ!25U9Deu h^w3ݽ˩(4իOQxX]@P!*ȃx?Пv<+`\>]܌p~ù^0ĉ&KjfZ凿V=IvW6Dsʣq#n1o8V~v#Rgw S~cB62N=4{*IVdJw8bPO2'&ȱs)mL4УBTk~)(_*+ZsgMU5o=5n^wN3(TĈ1@wO_ػU%ڝM[f?OZ]pUJZZ]4PDJ7v2dtoȽBp2+^~_H ]#l+=!٦3VX*c 'Uآ .AʵMWBTl7@G7}?" ?%<R~~[NmθUFS LVRK=[mSH\*"q^$2bXB6&G%t& XMGE؀Cu BOmn|r0j7Wp=Ƅ+ƻcCPalDq>N]{bfկ ̡`h@G:1-rr״n0B9Dj; M|0)QA12IPm?D ߆ :!"ebkVĠ6JBڡDqOkJ0qipݠKI*@>Qt7X%1Ê::_GIrm\~r_ʾe$N(ڼmc4nA+/^t߁ V$Zѓ8g2)p$^ĢgT46^<#fppL j0@@at]JOPґMN3ڇk=وvlU$4*O,򸪧t~.s)@4N}A+ .G U(྇FQ[Hȭc}Ѡn܊HIѪۨ?E.`y$FZ3Mn) v}l", T8mam[i(51 "׌WL`xw6iQYkh*a 3ZhGXA<2fblV]."6\G}+B' zE5-tAp.[-fLU[}e5#=WR S|\?`C!b&0no5Pv[/ BUǎ Ap66F"zTtxBTB73 \[O OgO!H_qӘ{!)xFƘD5?۞ZK,-b㬱nػU'R? Yaty`Ke fi0vV<_ 7XkQlnS1-&W蘟+Hؘ|pWybp4E+f%~ $kZcь[x!9GQeR8]ϓN=eLsSsYEv$Z趫M1ĶzKfbs"7qC,nE>'osQȿ]_ݻwVoIH  $d%k2ڐK,N`R*<5;y0!(q?ԫ_ՙށCe_cd_XHzowfI72VBl\e}ˀّ [8pEEb&045[jq R$<-+ Dp~eDAMg ˢ/d`QL&뷴=(}QuC`KyVĪyf~NX5:pV BMJXʈCI3-x̲.nՉ;}88] ; rFGٻqۯYGm) 2K7·qez b6'n4A'o>Naf]O9Y'BN6xEINA;JYC>G;KR7y!]$HL5ˌ Ai aIp,c4BlJ?xUTxoݺ]HyۡЍ[%Ecxۑ Y[6ܫ_=@9A89 ^}U`5B.!CRjchohV^*C'7PD҈G[2bJXݵapPffi>t#'}Ni1JZ+26J;7|}o;Fܵ"ƪQ{2ׂXpWC+`}Ft_{Uo85ͶN.A ;P_ H n8{K'EiQdoC>Y]mdnSlr_n7*R1KK0йے0ҐKy^mg1e %C5cJʆdX#c~( y)hOm5T)XC]&ߖ>Cjg -}rϸ5 Kefռk̏:L_-m3k׼Ǭ̢h1")Q '2/(%o~1f7=k'pVAw&1Z?>\trjϰq& ^x4m:a;8UZy\i I K;μ5% UPѴmYuJW- |(ZW={IR)i1$~Fm Rl'V_0--S UZ*ӞVqT\lvO,K qjq5Q*6F"U=u 6Q[P(b#[ 23C[JAxB;˅x5w^fCkXD:"Jf-'l ِېf>crhZU1kP%1T2oPmv7>;#BZڒJԮd0h9|2v0 7D t~R ЍmM6s8kS ְTUD5Iʼnxt& a}?Z1\+eůuiXB3$4j,'?**(D2 ]`\Q8h> #ZuYyYY!s^6 Щlp1+6[ح7 q47c:``! @WV[V4"NA"|F$\&ܽlc O1W~*#'` TX9BaOc lU-5ĶEh[t ߎh[}rIuQW'՝'?5e=IE3A=0HAq}FtetRV26]%T2KZRf{@" 9<-U O-t(l]33V=wcGVzm=~#ڟ,u9VǕo ^f0i%ܸE09g9svj]lUE|z QFH`{f-쫊Ľ+3J4 bխ];Vk[uZu)XAL5?xTA LipņX: =_._yÇ00_Z-/Q)5{i{{T`׻va%duEUy6S"w`yVph)# 2 WkŶ,㫛?>HK#ڌH$َDձk k=8?4U!p zPyފ7DVШynfy<ץ fXOR R٧5jCѦV CL#i:,ND2eT-UBa„\z>zE`AǪGE;hOQb0) x`{:JзW1(@TW^3U5o z0 Q+m: R#~lsڎJ@k&-"+f2:ꉚh^2GMH2dq#B6T{0CV-uOwSD}p4iKCѳlA|}]a}TOF/tkAhg1`{3+U3s0AUGnM"Gar$'=4Y T㦇~j\ [L1cAiF2kC/ F+0DP dQ{G#Whlyy/Xn.P8|ɐd}m>k^C`Vm5ڍEьyP7>IV[,gXr-en&`S?õS\,rSvRn (.d_Ŷ:*ueLem/Sq8!pTԤbe:Fv?q<r[ F,vdҞb| `=[wؼ.s1'еdc)lÝ3ۖ*ϥ; Û}6b-olWIוs3޳&f9F)'Ijl|[Ei8\Тxz2p|b~ D7 ؃I +-530RN\ǩmXdԺd҈iV߾z3w!SClƐشmf (4? $ء%"8:t tW|ATE)Cr)?m ;b-O>Os%Q_ot̎` į7|Ud> zS}@-?0)!䆱 ۩`ЫEgOXj\+X(m`C{C+Hy[^6 =y뗵`E["/ 6wOȾkV^ikL!QsC)9ZgTdYsmPݏlco{/zLa+.?EXxG\Vck(;2+4ji6lPa<ة )#2rm_jz t4g3Vv`eW}8$UL|wM u$3&HdN klS 4( 6IQĦq`x"cJ"S78kAI YEަ0]/x@J|F`V=WCQ7 ڠs`ar:tr+/ֳV^nEyU&goS>Bi9uȎ–){Nɭs7f>U4:^s{H7rVU # c((++kמx=&{%[z}YiMG6"L06Pk׼ 2=/kQpxOdhofEf,-*qc܎u56=sFKQM4tz"CnqQ_xqM׹•GŎ5p[ˉekpg m(bY!%Nך4 !v"',c%CJA-wѮUGtbxX*N7oak!pmJ۽>N| ?"A(XXΊKPgEZ$#EMŻ,I ݴD{dyȞT8gO?+ Y%/\ ?,{{ͪNաS A_kw-U?,l8A Ro@ta.y«)YQN8MQwi r5FFuJH_q'w|ru B˃%]>T>I={ѣϞ>-L1kF-,"@ƜL6磼2߼ TD +TH Iskq` WF,90^A~`Z;9I=Qp~b#t뺡KjTCf;oݻ{ݩL1vݦYJafu=Yo{T{*W8ò׍t ?8Kg]`g}#0~!Vq"zlT(*kJDsW?)GJq&sXluMmkppNp ugShw\A4Su5DMe؟V۽dž^T܃?'|ƠbB+4go(;JR\J(%9R1/)#ue|Ҥ4ffvGzx)܊_Nw᫡yZȡӪ'j-2B/P 9n8Fivr/M[LMjo e  ބ\p_G;Qއ2 [BTwӌڏWd&ڤ+Ӡʽt4˵4/`g.owUX`/E(.7J+fAi?@ (e'{awI/Oum֪֙8U7C@{P n1,ʌˈO?B-ཱྀlTwM0%iaf8#3{X+lrdqtg#IM'#?O[9r-N'?O 0K|0D$ JSE ")j©8FBW@Rm[žNT67D$Yg. RKՍcy`?0 rK6P=kq<2ČQaU*168SW $PíDFcg&\GX ]c(3~R臬<;\<;PR˵$dETl.H':_L#i(\ 9d+CXG  KME҆ I#f#%.$($[&]AvR(y:Uz)GN`An8. xj 2W>儠l<,q.5/`l]i[ַTG:+ t9[%_K>QV>8&MR/Z"rY]Ob73o\`7KmRa3Om!?[)@I*Х*ڎ3KmJYޭ,pGO21)#i\ĹtG.`m `mQ5EP)O|eW(B{Ge/PD X(־َ/W"y 8#B&ыːa@{I*ݱr̟Gt|d 8o1c $-aM8)O̘C 7$.mw961l奲 ^Y 7?UsmׄƵAWFCsVPdԵ-X3B]nq!!ɟ̘A\[W xv"A&!vp};={ {_".VD(*o1wZu sGzTA.mdڳO:|UmAA,!xE4қA!8PF\(U7p@WE*$CMLjZY;#K^EFy('/ N[P9R}Xۋ~3*ːkB'1FOWqġenPjZT ^yk#(A^LˢX@UB"D1q`8%H0cFh7esp׻hu3Y[\b° N֫9 O*e<SׯՎ 4cx茝Óa%Kvɸ /bmM(ֆ/& 8`[ghq\v9I-"Sp3Η52Ж FgUY/ u_,ۜx5؏`9y{o]yS+uV?7_ 壼>yE[G57㰞MC|zLBV6ܥJ[ޓ_,YyN%׸kvVkd>,#XշW0%ЩI @ ~ p`G >n'_--s$aLf_![ zCiz+ )$^C` ]6${wJԞӧqzC篳C#tb"p )td0jN{?4h 9X.6v 飀9?0.lzGQfYXZ_/ƿ.i›Jw{cXB yQ 5yQubq֨ R%v>aQ|}'8Rlt!Xkk'ɤfx.Ȥuބ;iEGBA:?ϊfi7:IfI`䐺65%Tɮo`J 1[{XD),[RަiP=,Gvq/Y+޽;yҦgTX3tWqp&| (Z~iKڑy+sG"PL+B`۫GV$Cg#[%cpI%NaՊ_i%wF4@#@R{b`b!elJW*<Q+7k@h/VP96i@Zܾ郻~z7n2 K]{]dxD?**H?8{i* ~`!MS@vtviHiRA˵Ȁ%R/&TI><9A -` U]H0\&b~\tsNUUcH۝ .r{ZlME`pǶ?.:rg(>|x;fhvavxo!~mB e{1> 贳6 49̬h{ees'T9Oh+k7 䀋`D`Kd..;  f5]Vgmj ;{JJǶH &@XU]P Ey:%JtAf̛E\lɳo?Kf&?ɪ[7k@.m6Afߪhxix,T4}䰱mgK\ 90?!Bs\p9 Cj9JA$f{$xgn9?l8b#tUJfsp,Glr35rmed|%jSBA#)B|,%DZJu30NU/tۚA\=6#<ojWH>3aԥ[ql%A}艾&޻ðzeۍ-Í8*>BIN[R-)1 GOQ}8ş kr6ÀJ>-(PºHu{کrJj/gϟ8fJP"dmY[kȈAwtZBJ;lq]\ki"H'+YWU U-]ġ􈑁<(hتZvrAdq-A$8O0ft𲂴ԉO>}!K Q#" [Q R/}j`uyYFB!Ut rA;2a;pM %ؽM}I @.(_K)7LT#'AuƃO/OܹZC0y7@z_+ͅzSKj .tkSەv%K4 <08S+-j X']?n%4{kEFSY>1̦~֙jGS٧NwJu1cw û s@.~ܻ}(j-BʹT!,?&zt=ڃ2 su;b*ޏҥZ[EukZNvSb&8c +QP+K! Ydn6Ij^[>w*:sgڶoO#K•KzU3wipeP҉%6%mz8-:`b6KЃ́]Xl?+;F^Q  ̗]\9)&cT @NeiuEO(e˞ؕچ'% G2rڄPӬ|odNQ& ' daCO&Kr7_u4d2%M0{/0-V-hl|^\zO8ۥOo N0P!%dak`?2#iE( lF^di(:x5O-oG w¿ oIƨ 3IXoX;`uFA_[Bk)EZ a6n'h xF.V\V~W$Щ%ra.ꖣS p9Y2A &ȊD\jBNsχ$Q}v-u=~$OxGvx:՞yu1~r@gB0 S1; ,j5 ,`1! ǀ>Ӗ<3By72qLhS TQa`x8 %, }VI>ɂ*VHΩS&)cKןQUR|?E3e~āNJ "h %, =9Ҳ޼`S fEg]:3c!8hRk1BN5}"1'%Fp1fD{,1(xS7чzCc"X@BQDdTKi\xZU& ܹqnm+<۸>82N?*DIۛyu,췶s)؃hXqN>j8`rx$wGF0\L w`U_E}qďܺ RdQJr oya\DkTl~ v5'I`ʽC3*<Ĝ S~CTL;eb\0,LefXv<43عdQ-g=arZ4IA%8Ycsck"̦Ex7UeASZqԔ?Uÿ {Mmo,x2ͤZ`>^,]&|.m)]a;s:&%7ndkv @ u}<gZܶClU(ȓx-8$j]kZ״E*NߎAc_qezBۮnz yȤkKLk$B1vi {aTM*_}1]| *2bR #Uju8cNpȻE &B8"G1c,׶%["~?/gm'fc3p|Z(-ǮզOQaaЬY(bM*zqmU, uv,"@9$f` YuG=X8ׂ8_fwuhI=*.o߾mo j h׆ZWdX5N HS)f7 c`Ă !BZoEhu5c 6^p;VM[%SEMy=׏Wj 65<[B#!}IUWyX=\xFԦ>h:Τ뛝.+{As‰-Ae-#ij!?hNO:G΂7zе堌9݊fޚHޑIX7j]g5Yq|yIa@So7E/_|o+"7oa.@|~cM[>f9Qlvbw\G ;Qj"2Zٓ ֪FJЫDǽbTL)uz}JGZ cyj#]G>Kh7h&;o~D\6=<}2WCmt"I< )4צOكq?1jg=;".}EgK3Ţ>#Ëkո֬bnqrk^a_`gĚ,dCQd8FBmTQ`2Kz=qtWZ1{s8v1GՒVHBv,. ]4$oh=W.Mez%q_n٣D*k3%,@b fTpMH3<8ftD _d0a6"@b;]vyU=ИvwnD}_]\ IGjALW 0R삉7n IvXǥElKO-L\?OaUĠIvz94S&Fcm- Yrh#𲣾-FxZl۾}ECHȢm ̥-Im#0?l6r>ޫ,._3N$jͥ^xH(P} G5^;yۂb$92PtaD1`k,GIkQc@D]8['탨ę aUk`Dq{u=tA&v4Ej+.PqPVbxOtJ5v@J5JΨTU:3bbǪ1s̨yBݨ&ZOiES]GQ1 =!2k Ί礛𪱏jWU>=(-dH(4V Ay8Li7 m2.ih䌖%hq\ڮxz!hfhǰp byޗ _順lތMU}w{C|3JLbo735z1C.oHLDNI3@(F[ukDLYc@ډ@uQ‡O.~Oݲgh7X ExsDuݝ,;6ep׬F{ur{Rq">nj0=ðYpa bE]bQMHn9NVRĻG#]P~U17ރ"H~7A(L5W}J@΀k:2\M|h6:T{ZAOusW]HU]86{^ fo˹/.'( ?A+`;"-e7>tJm}=x.S6^{4~GJr:݁l\ E-3|Kᰉ_G jĈI. ~Wh+_Gr8TۜV bzn#HLb *Ψig f1b7E1g-"ɍʻU3=;b247)œ Ce?|gv7'u(I~:<ɡ`,|CWU:sݨFf&]~ymXop}UBoP_E2K{+OŒk`Hɤ+d DRi+AUہk62F眳/"^!sWՖ˥.,,/C[pecޕT,pQ}e^},>{oo˗X$Kk $yx ھ~bzrN ȏ2zR`^y)[e!bCuKhFlQցFĀ&';"!2:ڽYܼ^KlkoPԍ8@>~mG此jmtS0fuխ~/x`lTsxb8aTɕ1<^.〉ltF(ߔ [FYO~&` ԞvIxnZ{s~p Zy˶a=LqqQZ0텂)3S/^ݗ}1 mM3w:1ttf"-1]7gL媶zܭޝwniQE /Y,|hXMriV"j.WCe[`0<;o 7ցণ6 b21fQ X[ x b aՋ_uOMC $o i2B RYCw uԄ+- $dKU9,eǡ(6TK2h"+l]df\LCʂ[-I=bbN$p !#ASF_gm|P;A/>[ϴ/95:Ik+/UQq"7T!eWY%I25%ȣ{wI Ԡ=W,Tgiʇ=/tT}Ŋ&eB`\E% cD;%TyXKБ4aIt2Yu:r\Y6sj[/fC6 "a6@WʢF>[ÚWG)|z%K'4Ppz跹\E{3{7&v "6#n wwST[Ac0+Bk4V&hfZ Q~l=Sh/o(نa#"3l}|-]!"GNP]rDQ/.p@xГ>o]գ =.ԃV#|:`\˸BH])vYpift]FW~/)2.3%6<3L ٛl!T'y+S^ #~[&O o^MkbV`*Λ+r'ԅP|͖ݓkfQ wOխ[ 4\{۷>8 4@%\MpD@TKc[1fQ??<L{i ,/%:@%~Q/濺$w(xn9uv҆DluXGzy;gv#{ܸ~޽;N F-Mx4ƪ vqً_O=!$0,:.ؓJX7\c^z45ZBrvӱVhߺuΝ;c{`v&Q TTHꠀdhzȾfw =HFFeY2mU9؂0MK':BW<֚os3lryd$c*E?YކhEd-X $C~( cut͵b zvmKFpL}4T oZjc <gw"\ [cki'W[|4m,^S?(T=wv~#Z3¦xY,O'ez ASf#ab_?͟#*JO&˧ʡ-IUⅽ^Ac=*z“/_Eݯc/?dI}NFX#Mu^b /sj3sEU?d+uPޫy}y3Flly 0h_}_|V!|zzX?W23bj$Ze})*<ґ~ҫ^V^/ X#TimZ9f@τ+Ȗ"=LYDN@82]B_k[!/]-S\5%FKhmkIDATP{h"g\Я}S~GD2~5iyCoչd@IE6d7̱_1N4{/[Upô]V֧Cw6'vȞ.&ycx ӐA΍Fl5>|`jk$J U# $7 #kV9rĻɛn˿ [-9<8 c[_~eac{;^Aǝ⮭Ch.A|rڶ-~(7m/ HG(=}nҜ{h).4n(C첎_ g Zm~"  {Qq܎T!"{ XX`ۦ96:;lkS a7ȩZ%*uQlRiGCN@i:ゞyk4MIԖU-OR66k5%` >Ha݉ݍ!qK pzSb m>Ois~) )[@-uOLP!.5NlqGpq[w]Pݾu?ybGrsⶋGgפw*&%hdžȃ6)R="hw/%F(ɠ^*|%msѲvxonHr CWLI}S'-*xbץ8H .OWHL>TԎꉟ` 7c #m\?-@z'BW\_43ό>EߊT4Y3Ho$IZf>DVD]_ ]D.h!FU&\\z}3AASL 'L\j6,3OjFF$QtZ,e}vGS]KuwXΣ'Y,7O)?-t?\D7OWn?v,f0II!]*&ř8S~qPҰǎ7(iHVv af*1Yd9` H?9*8z @UiAvW`F -aB 69>Ϧ-|O- ܿUnxrM-Ƙt7ښ!.W:pҿ2@` v+G f; `Cc<l115ۼ\Ը8efp!ԃ̣¬{J&QNp3Fg\d4}8 RFv.Rz3cZۛW> QXKC@-(} &%\/TcĄ_O7 h !>3IWۚM@}Eۤ-k13H|B:ҋH'+NppwW:~_>s1m ^,(*eֺ3IH^ޓ~BaƵ)C@}p3OSԢq;c6TOfVUi*.3ڊFh B.{^W-V]f7le7:Or0^[bkEw.^:]I`*Ux9+,0&̮ @J(suظ65+JQ9kpWG.W_1+uIx[49MղTB:Tӵ[Y-Ҭddۘb5XPJ2s@<]w\cVvAEo- 5|cr_єQUpoeE* F;Vrf'tz _ ɫ #Non6M8ؔXP¯}k{ (7T"~{ D #d4vqmH3DGWbߒg3ހD\NG~Vx*/ed}>}{r_#VԘRo6r.KTq&PV3QAwTigВQ(sU&ahDZ]O-FIsAhum[ 5S)t1iI% XG ŋ&S匚*Gl}E{HjWV2NRt ˋ7o؞P28EU~߻W3T%e>jD`ŔՂT6B& "/mXUA,P*µ0\%< - }?)$T)R kATG*Gϟ=)45[tBNL}|-։|zիܽ $"ݺf׺)J/Tb$Xx-e ;r{ɘcmV;V6ۏfbUiJ4Z)3̡F1*vTX[ksՏ8(bhL75Z0xhXVZۡѴXE*{}:ݝOj$y##Ԕ1hS.hiw0=Q6K3,w mNf 7lǖ:ǘBa?QE^puGnVoe&qcY:&! >WA݊ڐ$dYW/솱 _3sSp۠ifd\g c+X QCwbǠ!n*8~iA Ӆ{v[hMD?Ž 1~/3 ɵ_D(;VnXˣh?7a92;<`̈9I80M՛7 ±^X` ]7ՎƢxmI($&ּ-jZ=UXPRf͎[i\B^ҬgQyG(|%h˹eС?6o dk8msay<"!E>_M9Bٴe@ǹc*HUC6&UU 2oݾ/I[s]+RCJ{m>/҃wԉozzMHo߿%o0EĬQn6iq2N@Hm24+1ABj)ڐ '+[h5ft!emMuQyy/wC2,P\..|yDx8:cOkWw[5f: 6ۨ'j= :])隞Ustjf4Mjq`!anS"}^U;xx5VqK#)Mn]q0asЛ`e|>\8&̓EV|/iu` A+蠏6$@ ej^7@մ|$E=,!y OLq/9|Bρ(+< /QEJ}Ny^(MNPGY|'^S^諀d-auyЉQV׌m񩶇V)USUWz*nJ[R$jr!GdP1 u;bMYwf"R:kx*- ے+32,v-/+|wMnu ~e&L^φxEL[! A\-o!| cCkK Uof63h\ڙ>$6#;D1m2;U(8JCB;m<ʚ₧hGG`VƘvSa5!!h:1[m$x=X%RGoz}z];QE0eٶX+նt Ta=Я7=gOTV Eb'ެ5_v@gB]4D?#Ps6*Lvnu(K}Ūo*K;ٷtN4Zc\g1PF&Dzq^,'V>ce/+CEj]1'x w=$J4c_6g^}oq]~ix:|N k[0FQ$0t6k{= '_Zi|^;!#>B7j:a@AuVGNh,Ixzvu/nWQvtU[&Y>8uAU Q8_Dis{әL;p?2n\^M8 &lKYΨNDkS[U lBc@8v)I)ʿ'ga"'N7uW}R";V8wcvʨ{)`1Y.]3hwFxqF$UEx[gAGcПNl쑓"phv+IaiϠq:Y-}Cܞ=ԖC#Nx݀NÔȾcj+Pc?7p~Y1r{ Q#꤃/TZ#,LW ETUSߵsUQî@u4Vh*0Ǎ$2A| g#ңͳGqU'"iVVӾ&Z5P tIe4<\wrΨfWB7۟V;J^](%i.FH ِYXb[ޠ6SyRdگB4aUEGo2 -|S_ 748+]E+:y{a @ "H8 OTs7V PZ+ى#etwhLz t_ց0k$4XS73 3E.f? x?a_1(09m'A-7xjsvxs$6"AހP(Q֘YZjNGnπaSɆ>֮ks`«o|$ޔa0uO w Y^J̨7C#ݍMs7idqQ-`{ze#D n;Ytth~y#?Gw ϖ-l" 8*9zU`3ާL0eP,m'ѵnBBJJ~(z0 U{[v!%J9H18oJ_cj|(A@vMSn.#BG?;;Zp4lSbR@ăW?oĪ= o?UF˗yeӲ.ĆSW3EXKV-muUBWRwgeӣQ iJ2LIg,0uYc4̇—z{ڶm:t- +N'AwO l5' (5.jL& @NxV઴LZ8 eoZsB5]I‰NC_5ۚȇRH'&(O)B(h8k!-U u958T&i gmdf3{g2RB;rmF0awQ@Dže%G>ΧD ucxϷ7E mtxSVZ%}^qa ̶|Jd3lE5@sA DB,<G-O" iF|o+HICa-GgX>97U/ei3$nz36Y-Dj98C y6uTtEv~ysqv~~UVi7J=%j^%pY(”`½AӦc -HAL (]Z X#y$Vy+" lc0BŤp 2N-:og .zϞ=~:>ԕa<3)yO߰GCxYxxV,| s_*l|d>Ck-BBwPrM ^Nϒ3fAkD:aG LR}kpeG9Q ؚB$gH"NޑVT_}ѣGC7n #|CfBؼ1L&m"ߠYG&vmEuzb$8D6G-K?<04{d*뱧=dtmܦԳ>"[½5cpvG7 8'pApg'BMm=A5[+V\$!BOӣ9'0Ŧ Ѓ-9ʹ2Y?ր]_dר抛&xH7%Q!p\T0)~~Dm7M`lcn8̗z w}01~`!2c""""Dߜ0w]]^_aMA#>#li Ie4ԍ,cBGIPјS﯌Y= 5RK7KpqM^I`N9Ϝzcl^KCYT3{wY \ !, ġuS6[ujm̿} ,\:()q|uo^Y7W׿~wMyrP"3 g$hǺ2RKC&m\y-O?3CHRvJƶ]({_;>v/99yP_ jG 7-=tmpٲ3:nyswˏM2d5lg䁉w٩/j:q,N|2+r|뤉g?zRc{pRkq=ݫRl p콪tčw#JF/NoiЬrc[k۽NZܦԒ}$]D" O˕]&@C(v"pzz|WIחW BdEJ\5Xچ6xdԗRvXڑ$eѸ!QsǾ m7۱!8wOM:ٶLyeʮ=0u~V*Ba.!)L}6D]OjV6|Q{4Ա픒QA^#Xh6-6=z1E`ZݐRK'd+N4R<֞F'tk4ʴ徟7@YPYŝVvݻO/^Mf5C?|qڋC `ѓX5Vu*WwcՈ~~U ĿKk;a\!wN<8ПLuwÞUr@|t:{`*3*?aSIL |^::^DS^hyC(&.kxtg.'5U#gy٫A FkgrVr"!Y .[ `7L3U0`gnnREmQk޾z{)g<PbW;Vn~k^3}1'k]D]ײz'7tGú.V{w~TUkTT~j{֫Ui&=Oy뺪uu+$@'G]{~XpT;}d#6"#9Ϣ-MS2,B|V+Jp%;ͦ}{]4Tb G`7HԓP*9WSKT׊jhzTD޿[|uXubUVQHeUqbI8jaaBq^qՆEAFǪ{UGx;*g!"'zDXȂ2#d!iֶDқaMjCh8sVuFi~>VfPML!\$#Ln٘>-:nї*%G㽕ϥ#nzaIE͐%X#meN%*XYڨCeE[R+l 2U@R9{Wϟj!jR{4Bw&Ҷ.ʍ$lMQ!IOPڽͣeaVG|&G(pWH/2ugAC&>z1;POR!7} ,-PbU=f_mHI O3#xk6lg<َh/8RSMK٦ N$8pa E`hH;5,fZD]C~w`J ]L>^azsqrga>L4Vb:nt[ cP2D0Jׯ\^<-,ܸ [Β OW}>h%n_վXpx,ۇ `d",AԺZJ6{|e5$ߌ}j=w mCxW~t5ƃ2Mc8ݶ vJ>Lwin5k\͕?J>_|U.fG"|̛v \3ı> ` j=t6> ,4sĭ[ģ8ʽʟn%9Bw1e FY:@!%2yc;TU!8O!pphBi&6< gٞ4rd#0P;V AouɎl>)1D#%_XjHm{9| P npY+R.]7}QYah~QIvsOg;avG*'뫛n~8'qhܙ0 VCJ =y,pġ# (0\H@Bs5ru6^PUE mȚ 1MX3yHñ6vbBZC2KzkKlv`WJoNɻ> b[Qk%kWE\(~R.PTycV""4(XtTx$OLea/( dEKHᏀF aww%nARrf%Ow즭 F`ɖW%4mMQR¬:̸:u(R67?bWYh"HEd?ZaAd&(z5T.2#<#E,| .}b3VGk̆A&uF_l&=3ܝ!Ǻ +JT1BJ1ء$ZS`cd_s6?d mtFzmph~  hIJI1u ɵ˱ QNԹ}CdoPl4\݇thf(pUKg-d?o{;7&[l"π,6ahPͥyDm_@P;s4a%e_ ;l 7nۃ Ƈ: Ծ-7.x|,ݗ0I`ܢvdzk@oV#e1&̝coȱ* =tLa͑世&vȎ8LŁeĜdz m$1mfiY0``}uM1S'1՟ : Nc=CSҦ,)yYS<膑Qq֏6 {;†{j^Pg72/miXM)@~DTr=5fGSۣ4::MPi"E889C-DMHn|+%V9 >^~qG S6nVNN_/%Pd B5y5;㦰CB'y;Cd5Vrk/ܮh/m@`l\GTq*|\rbn ?߁A?|:!I7 &^'p:2v8tb>h@4!˺{lKtXGifY;ܣD+0X7 ׄ\hʰ:>R"UrEIz_ ?s^1ZZlČu^ $ḚG|(Ҍcfb賥bù;9gIRv.")UF]Y=7؊8pZIy.''Nt:çQdK>ReS$c'PDG 0SR6{_ה/["uT;0V)v2Z2?x]pz6. 4Thj(VȪF/kMD{) YY>ӡ[%Z4;muuZ(M9邗S&wdC M8'qIKaK 6 l ͊hnw(Ӧȧo.g|cyp7lLawxrmvІX ~aΫ 6 6 lX \fؕYq! =Wސ A@/y8@\7|R M.dxlPwC9N-71}}8"![v3ݳe$|CAYEn w$v~ np4bE!j7*Xd8RgIa7փ6r" 2cc*!>FҎx#9='x ۖu!c/ʴj 2YWH2h #(~ڧ%`5?E,ޜHGW}?.Ca( %TF|eH$>>sc$3%MrNGrdv~%ǻK PbMoҙ]N4/ pbAyL1GAS\!Q-z4]y" gJɯN_ o?#'ʳlW)୆c!At:`Q1o%**Q3b8PxU[@*Xj}!DP-X%-F3NX6نV 8]Q 'CB O# Q0b6ީ: 4dd[ϐ'ZcxS=~Vɝ}6і1 v։*% #`> uVi}3,@8C:0"hIG*B-&VTmWt2/lQн[zj,J!ʓ=7{js:+$rԛu֐~u(1< &ּ 3l̔qkY5&ѬuqDYWd4|>pDj$= \1hXQæ#6? #?|S MX-2D!D]ϰ$O PҪ|3Ja+;ZvuCs1Obǀ#Fֿ[:>* @sø*Y-v1#zYéjD"6C5JD?=WzBO0+]Fj4` =p>w}gU(weQWJSU$ э- !JeԂݑa 4``҂a Mӑlbʀyt_VI D B-j|{cBŏA` N/OׅgBk>~@҆h,$z6SJlnb0'8 낝F҆񍒹lal-VIo@R Ѳi$u 3yb8 x;g_qwZ1%n,]`\Le Ӽ&(;\RP/;<ĕk9 *WGxWˋ7秧:iyggguy*] mk oR]R:~UԲ汕wU|[X.zSP"Bj#LIXPdUlɎ^&MIX&@`zZV3b|2u#dɺ;~@ɾ&}݊L@3 C![qϓ'$` ~YXj gvqv.%HDR8T?ELi̤hOkJf,H % u`pՊz͖T'ꊀJ:bNdҶ9%` ِj' lEtiϋJQ xl%> ˵t B &Lוn M-Zdž0l$CdWWҪ_^4uYL57a( zw m0N׷u|;j'(  Dfہї7/&212O`/^ ݱ^87%ۖ 5Oe4snK.s>5D t0LGn^(oןQZC;@`y7 :_vr!D- t6xT_[Ks 8n!A&i337μ5TaDBaz0 vv\]՜/ޜ+B+W ,jЯ 麮{%-_׌'$~]?N7iݩ怜 k_q5l,`h %(Gߩ@'j4[ꆒLd/ !tÑK],P0X-}8>Xh!(lo'^Fg8]N 5cZ_G8zv¨,#y*j{ )3XAep.=T.fF_au֏GZm:iy7"&uke# kVof&&lS_IB $451KPH`11Mv$ڶ_[Mb#@0TA2םИCC,2=t(hH{r 0U4wB-.0G\sTBmL‹(!K)%|J_}E)Є_ı43%hfޞ43ft,U*"sw=B*H0ãJܟ @3|_6| ;@+O:n򆍑㡍!Fؠi}Km9v7MoZDo*w6F't]$  w[kH.;\g JHFc+l٥4Quqit#7m#4:,:hxp LCHE$8_l~H^]~jmY{4̗Ê{T%cē೥kV,# bĘ0a1̠ ٟW,r!BPwb6^ 8zMnwqMT XҤ.4d7 m=`L@{|n}Pu'~N(t;rbG.A- S>ty\GGzDLً,i 3̠V.3f^Nߔ=SO+ҋZ6y>ZG hsأI@IA.w.t37EgȈ3T݁ֈ30s*t'^{G,ƹtbGχ:cG8rLSj.-*s1(lR2/>8=W R:ǣ ۡ|@S9UQhɩ[rbWh|tV9hДkis-!df&+g(L]n)U:J8"6t8N֩ U΂v~+_UU5Ԏw^-;j?X)eYy;iq-'C2 q`5Gڤ%,{[#vC 0uggwfAͫTf"fGmۉkmCm\&RW:8Q`Wr6'K/Nq ?x sd4փ6am)ө [*w8 w;)<%& 0QcxCo2>-D^#_{S[)@b];wg8 칚=L-6l\kqޜ)Yx<xS[-췶oh#.CMxWjP ArPDwAcatnΡxpRɾ ;*1A@NJbyEk׆SJ8TEypW JrPT*[.eӘƇE) mAji=o"cFB%iF騊_ܿJN4i1FF6l<:{BB-f)LGa躰犊]4 =3ZIF/m+P&Rjc5U]}%:>+3pjR{ 2 A0BSysU3\ArlDävKXkU !D>9SVgF{2>W[vSe>L ^z 04X]:4k][FP\Mǰ*hmd: d`h (DWQ 4КDG\k~4е%7(eArxUf>]U-.׵Gx4&XCP$D#n!Ctᆾy7E8vՍɃ=;@FXi>< Q? Í6&d,Q֟1ɂj ҿYӐ"ĦA0k޸Df6lr\G0tų&:vz{}=py_ jMTYII1~ Ҵn.*Uk0*.mq*7LAcфl7rOl7!WD;8=؍ (10&-FL$ojhUە;Tl1?ݗn:ߤ_xqdƨ1C 2e#A=ڶC8oZ9`ЂBܴaI Ehx|ƭrPr[" 6AEu \Pv\ӡ6j)+o=sfǽKSPCv,ӰYb*b^?Ƅ´!3,"3g/Irݘ~Z̯9ßlg7aVxnc\AXpbߞ_W3iwy8ݚ3o08Ч:_{P&$ hp6s(7kוmfd+xy5ht;LJiԡH)q XmWp~+a 6!^+PхZ5_+py]zM D)`r *0)|A^![wkUVjڂ ypE/sA?~bu%Qz1I#Pb:B Dxjr~?~G:)R߄np9IgSAڜD7UX"WFÝ'k򺬧D6GA?mG١F#<ֹ^$E: ي}+;8(TI b[Qem=6Μw,K-Dۀ%IFahr:o]gYc(=av+9-j"kB6>&Z TyO>bZ M_3>G㦃Ǯ1(g+ d <1we.og ފR,<[ax:L,u^ b3\-A0aԭoJص̤_ O~,FNc᲻w1>BvKf]^isTfw_*όrt `5V5ŔCʫ$΍Ǫ胳.Pz4?BUtX|z3.V\pdں-ظ{IPM bN7 DeSqgK)QΫ QHɏ0 Є6czw@ 6me안A1Jٝl(m]kO>~VJ. rbr:z'!Zl4i2 p! ȗqsyBж&@kVI~}|aFI/_d7v/ ѸEƶ{o5J-:R-/_)y3bFf)qcSI= j=]7ߣrV:12Ӽa'뢁F3_ #k@Q*qhk[ld9M`+lX6ѷX93urDFEĬ)w n| !20~~S J/v4R+[xo{V3b6MKf@ &fc̠Lay)%>.6-Luzymo %ۖ#Zk{j^9]%x%'(<G2jE}ݭ4^ZƧ,;}q̫ĹB{׈Z7f!&nBbʱLMe?o?}Y 3:, xk14 DMOqɂP2 勥 ukēWu^)к fH8{{p"(rC/ɱD к~zO_f|h "kّpPB %^_00s7OBG4mPLNSRݪGP-XՈ,cwV U'=;3sl*~ԊCuOu3<|hhfayɃySJLl:BcPtФP:0 t|qh^ 0LѹPǛd-g,VP 1C \b, &5?' Z8L!f=TM9fʹ-|R.j}aD!ZLd*ߣ=ԃ [$(5!RCvndth !a~Tv~!L!Nµ6HJN2$D]}EQlP0ۈV 2MIT9N+x6.h0DKX'<l'"w8ޕ +&ޝ<*P*ɓ' k-*ދ_~֮|D 75ܡЧ\q:nÂ[ዋPm]0Qd1p0$ RfQ§M_=ghixNegvP<,s Aoή} =bW`ГΌZR>kNࠃ ִ[D9iz츢#72+DE`w+M!D&P-xs4JI", CKT'#}D] =Zx01װc{`;pPBݟA{W« "+ =kf4Ae~pI&pX:XL A':q]<#"S z__"2jּ݁K`_ofƝ':Bjc:_x%)1zЭRr@A-f5j48p?[h,h("i VwH?3䜏 -$rj@*v EYJx~oy\ZyyWo _WWHם5Du|_ւ ]̝.^R F1:Mlj!ɘ|v{]UB&v,#s"=$hꕫo!inȝʟv*YZL }՗ϟ?_ӧO$ "QES‹6Ya1ԉN(Q*? ?-E9t$y{Ӑ7Sl֞܍*$HZlXGZX8VSAF%N{NݳrC6 G& O()|di挆 t+P_?IȌ˂鉆ai& wϐFkIMM9 PtJ|ͅ6YX`31SѾ'i44gl HFt9hmt={2_ 2ĸ7W)J<II4VdFaqc4titX:k2=cCE:ZNF[q:|y`gMF{víVƛ AMJ4BԖ9=!@Dk~o-u%)DM.k =8jh?WM7ЂY:Q-QE~uյRli)HX[ZG G&Y] -5M_0:=$QWͦ=$BΰQ( 8Gkݮ)(˚g=j{OC*:v 嚄B$3C6izeh-4[O1]\{Nl+#]rq>dj͆A6l٤W{@UuR&: 7 Ԛa ,(݅wrQ=u{C (n&w2Uҫ5uw~~˗o//mu{Fu_vOϳ^ldylg am~xܯD:lnm )rg0<4{2@'FjJdKB" 8#jKSf&hs[`#Ifs2ClDhh]iTCEQrgpnBXC@47'[}4-!!u!vFQ3fV"c @ݖ BJQDn j,3_%CX[^lbS"8F_xw;Hf2s>Hx"C\~,4J tlPgD/ . jL 1qfNbB} cx` bbA ǏkħOg8CԦJ;y [+&Ĕ0,]/B6P^(?r{bܳқ\kF\6L^QHFZNTVh0ylr)H@=6~=8M*uY;3~ױτ;K>>=N ܛtXFfXp8%) \t*w,gQ`BoBVBzla\P?JgFoz8at/-}Rwd nwԸ{r~UuZ&w"=-h\P9vE`#0HYDH?6Oc~M?G2뮦Y=:1 -v߬ o,iR1Ke1w\<]D%T(Bn;iSuMZpDv&%+ĶBmhEA+%?~\WϾ2Ͽ|QE<+ ZtL1AەXgjT,񛞕(Q*fX2Ԏ0#!Щø3ڪ bJ$%K6^Q)r}R O=N>> aEcX/cA=oA٬J v/_n8F:Bǒ''Xat-k(KI&1H Q[ahgz2 kld5 %`c w4naL|fgl] BH>j6SGzg?yTN7Cż~.;vygߜJ-ڴ($BZ m!QLLZzsb~; 31_]z6lzj#8d5!m֑ZSX,&(^Yh!pTtu\_F5F---l3f uHi]t :NFM'&56<P[ImOM,PeT2Z*'F܀B 7waҗz@g\F|a^F1`ձCV*i%^R1qhx~H-6 ulYu~,&MgJ#I OmUmι2Uv7v z+@4!s7QAF~HӡGGXa`AaVoGwVMi}QrFk; se OLц>bdaLL*v|/S%NjrB j(`+S涬6f XfMl[[C<=S'jszCuBO6+suꤦTA[?~X55)WS \*PuP+lT]P}bޥl|/D~&DZNf:aQPDf/N 1{[qzƄyeV wiV7L6k@,YhIpceC(~ɪ%nUCFΌ`9cj3~۲K#0RDX+FυfUw1JwGG:rW IJA \XDEd%k^HuX%+B\fD$8 ך#%Bԝ"ڢ6$X9 W.//*NxSk}_}HբڋVP$#D)DWEI$_2ُYuX< (D==SU2*9*R¤~h{ ΀b `5`) (jBPJp,E^G~&"%֛CUm7瞩˵쇏uZQmh`@xb`ڶb- o~'2ݚK8Lii)IPuZ$c7jH{XT9 (f949eMKs$T1l0I&`vjQ|%P3ݩ!EW"΃X5]!v#cw!ES逈 o}WwhfInt{ Mr|sl[xxclM Z}n&63ހC၅#3n4 BF:+(|kͶQ Y#{4DK:Cu"4GMo.]N-B6@ In)G+n-v_t k@/<2C,MLI QO!bSXǧ#C3)h%THl ulJ@S:TyEpg^vtCAFula@߬Qgգ}bH'ݷm,WF9;&186UNe%T.ۑF/TyZųqSʄTei9{I~6PV:#lMMߑ;-)w-aM5SKyl){_n[)@a~$&&0N<0Y=ϙv =yBHvH`y$$Ǻ&S6R*+;3Rw?5d3k2{VW RFZk- JLލE.lQ3"kœ BWΨpR{Zf^3IOm)wu0ZpqZ ɚm$Zw^E NB{|E%aj.DqUR|\,|H9; 9E3缌k YCB`(dxUϞhg" 5_a Z^TKJhAAGv1A?nv( m ,4l$ " -FqYSD(Y o5]6Im N<~tbzVwxm7^OY_UNzsqvv.kyGġl$׏ag/۬I,KEBao!,k}lM.VU-^A˾g| UB~;H~DldBQ]~N|Vh^hB'Ǫ|>< KxpQjWCiH] h*6@iBHu&*i4lZTׯ}nmXkH6*nɰ2< @ ma4)Ш|vQ;DXq95͂s h#_&G\@;;39k_f9dhh=l>_0~hZ]Z&WeCG]ˊIq` ,94X^5!n:qe UGՍW̲L~Ba x}+Ȋdx"wTDMyĐn/hL1X`MͻKg֎ 8VI, u8)eh{G 6dj:DsRT:m1!k"-=Bɱ)20fc8Q1umzj pԖy>& tmfв. աXȩxPV}9lqK4/ŗ6ef ;eEdܽJ$fܭ|Wp6IUbp6aziHM1 LA:ڃP;hݾsjΌ\KǁD :َEmаf4^^ +T";#V[ky6^{\)[5KSΕP{QIƔME3^oZR&ȝPmj97 X -ƲƓ);VLL_m&r rSMi\5mu,HA}i[fMV^X*HRy{y/fr@l*d\P+y4 r.d/OUwH< C-V{`%{4tU$l" ;^."GWМ|zǚǑ:ԣkIfAǀ2y/o$8&5(?4N8E.pv8L/G2.QV3A Z-%LPp guvp #-s%*C֍UPsXhZ'~҈f1 }y.ʦ%$-i҂h?W2Y[#3T@ mrg ƠKD=hG-7j9衆sM%:IP[դ8FvW<\Q}#prbzf՛P?nJUj67j 44QX?1a2v7PHFMj{$m2tshm7{268HTې 15vq-LM˅A'#5l;!IK\Bbq6oi` ?~OÏ?ݵ彌AQ,{b90oCPAKCTfofBy. Wqh{iV('_Z(\mQ/Phjl:B}?z[4\wQNM/>}믿|IPB0M 9Xgo4eyԺ֡h רX iY!ZZI s\q,ڑ^=@o^'q4BG^2کŜ Nҥ:PS o$@?D T O$+h51ds[éb''*OU2<1 ,m?\dO`bUqdb1䩓_>s(8L2 5( {*b JʶK84u%tB}t{_[:jCͼDZ0Q+(Qǀ Ǚ*qM'k0̍+a7_%2@z/v#U ⭰'EX82XJb"uRhsf"Eϒt,tWtf| lt;響/^^]v5OB/>QbVH(d7Z}`:s7|h ZU0)UDR=Q%P+~?w2~+)@ fN!*\q:YO~m*T`6O(W҆< S_u0$'~u<%SC| h hZ–vR*` 8g`zLnN} ^@7LWJ8t재QwՀ26:<,= bF6.˧J7 t7ho-Pie!qĪ ĩ٭;5Z- "R4u715[W(P37iǢ.x$A +æ lĵnabwO2©zȆI1GQ{@*D0G7dV|QG\@a!DL`5mn]4Z:w6w_xg[{4YDMl#G+df{2g#f흆*6 G#ti s0#mK4O1V?x7cMhԻ~zÏ/j'׻dL,X TEJigFʔS[nKi78:k'yͧe&%F%ў^5)9h(f"/vd__mYLF'<1vVчc,9 ~  w-sʉH&Doe㕵\!':iKv* )) $!1EC״"gᰋ@ssXPw&zd"*a--m׭?~aYR0 Kh|F ad ^ Гp]4c)Z;Ni!*LVؑĸ\BNZ-?*KI,Oא9Fl:PC,4pi3DBEБ_W_KTj >j%Z%uNZRh_06!xȿLB8bsġ:ū:cmo}Q . ԵoYP+=PI+ Ze9Y`nBX\M3 /*ko@ᦜ֜62!~Ok߃[n_]_N)48qҘtD+G-&ă-måĻ5!X bKF]ɀn0L3-[DTQ10@XW%;zXvlQ=p6YLQpև- :i:ƌa#Bn D0UW@Ք^1q~,u>ү2՘6NjQ_Ct|u+ ?D:(ٞ{ZaMe蓴=v1C%ou'6 &$4 B9 $y&tˍjp¶XHg_Hs[G^_!JqWfbX, nMy!eJ[/MpcVIO=n ߽u۫: Y+ L:$rggo.u2:r<+ )˧= a2!XM ;o:Q*QKbx)#j3UI$*Dw-_kU-uc], X )߽wR?c}|77jr,ϩo`f7߁ :Yب  ҂'Qtz6︂sD9αo{K: d[;NӜy0k ;G?XL ,w5EB˷ޜ~U5^2'!t8d8a va!LيHFQ93,6)!>lΩ]YueLᏑI,ddLQ3"h%JvYڛG= ɂ,JebBt,'>dJtb/2~݉(~F\FCA j󇢽@cMo~0 oc__A6Ij^ڏ'̈3 hR˧m/C|x۴}=IPT?Sn ggv͘I(G\|ceGnIWmP/&p뾡#3]4F|: L[܀9AC3LnH%'gY4LfZM)z"=y2̰yc,[In?:+UW{JqJ+ss/_UtL+{8+S|V(` ?˝r&G~S/HHCŽ2䙾,x\hi[ vFϻ KӇI]F[N|֊d 8yvt1 {@w xٺׄwbV3GL+1K02дA** 2hqcY%_&(oΖI'1O 33F!Azxꚴ[ =u٩) ɷ1%D(=5QӝTcWd9sDz.aD@ XVB--8lXXQuhK z6X1'Zjr| æEJz*6[As\vqۡ)`^Y L7grszѳSDm}ŋu^tWT׵~˭.Z+;-zP'\UX GB:=T_~BkzYP: SsIF@$ ))B| Y&8@x]K}DlV M$VfV/V B !a*RiCVBXѝDhYIwm42?5U&c&; | _TTGQ $iB5( RG AJV-PՐ`&Hj#O6p|;d*./=|Tya:fnޟ4ۂa۷*Vۍj3HKs-yzI QbΉ$HVC %:S혀%:{hE $40n kڔBR 8uDM>Eyf􂵠A&Ȋ n+qU;Vus . `i$PG~VځGJrƗ.l &wh7kc)1I5]Ӽ7-BC@1wT!4׏I5RU4?>k}&<2vׁuE!XF f`f;gֈBUmƗ"l Х9ܳ;xMFϊ{j~~C }?$Vٚ&dWrRGaP`7r]כ~0 Io"^0!CYn2XfwQa˨*wFMܓ6;wzu~Wb07#.{#xp˥WD2ѓ7$xVCDaȼ=ur)M<ņpa232λy<`<[] (KM\X[-+pѰ 6 ZfGe7AƿXIDltUX&#n{fƤ|Ʀ9﷍J{dn|LcP3}р5 fL NpTK'JV6vX S;FdځLVڷ۫/߼zϟ>o8d̪AsS;4 ,S>23`"lX&ZgzKM`'@hO_2X}9+d.E8@2$yOQO2EdRŝooLar&phX-{Ul-CƓEM[4yFN \@?T9,'M*܉r(C5|\ɰF7;׵HqR8rM!8S"=5#{LP*zs;O/)drGc٫+f.:TaׯO_|u}5=|qy~yqն *mdA'LF)&g_zRNLFċ؛Q97vLP;>= Eu]{*TG ?{:B:*P۶ *q32SQP +:%]ƵtKvsݓ 걂42j¼,t _ ;@- A0X5gpuU m}-(ުNBYYE1N\V 11DMn_Жu6:`:$ EN[Z_7Oa[&A_^;P,N z(nG=3@VBh|z*>'R5hKxB٦׼0n ZjU#y 㞼(鮋aSwA'cgϊ_`&heвm&ޣAoKPmzj5eYaХbœ6`sB#40mKi;2h(XSI#Jwl5F;/٘ՙq7\ϧ9pӣDHsw"L2P6#Sx{-TG[$q׌= 7'/ mV:ڳl=ZTkS!0i1j!I&@6|v=c܍ڮő/H>qly}nTx,7jݡ#6ƆŒX9J r/Oks.U%Ȭ4Ұu.>l: s⭂tY[ W*'mɤ`{ӕTv>e٪kDJ2PQ~@루FT,ne~vP9zx`;=sKm77@5NƃS6a ZL}|HJO^}zQ.D-ƖIz@6ԩ (l)NJ,$KmAF3;7I΅ZT,o .` fܑ f=vE^-kw|7_ՁL\9=w}\do(@GVF6Hdϔ3dL4Ը!3s^3{cƴoƎٹ뮄oz,La ނPฒdb$1I^opB9ZNoE*&lBq `&vFqlOͱ 󑈁 0c݉{8wUbCHg=8gC+*P?+T:x Oue?Ó.@룈. T%kz9-|鯝^cFtBz,xq Z͆ꦎ۫B1p^4,@?`~+&o^.St9}ype@ 3(J}`:C) 6f!2'=)Z=x[./׮ҋ #v⦠Q,uD`ֻ2!%/u`>䌘- "-( 4hZOYXPWviZ3F=tmMZ[%ܤa I<]Yu63S0hQZw?ĕ*;&STq XhH/OVY`º2fhýKΓ 'SJ"uvq[jh,,2ythxzܤ7o+3˧o*36ЏYchlʎ$ؼ%km*~ͱΊ`>3)eU`{uw#lNf#;Ӥ 0*QC(*BSٳH>}N<ͅ3&r^" m}&ρ$$Åioq3/+.[dʘ"ZjU~a8ISOV:iWs'57ޏ#]廦Q{$鐴R'NVq::V*'VR6!$g*rjPG ꌄ[gPج:X"*pQ u_M: =5- `4AS| ) /BNd9))2jMdX⻣N5 }c}QM+ɳO+P / #0ۇ,!v!r"bm3TMp2Hp3 ,hG^Db vе8bp2 L鸍ҧ?%gyYUp;4ؠ=EoC`$cD5YPZw[v &"ToY 0Ed6-xZdRn :?Ԛ:ڹN2UKr$  x .2_r-CB&|(iD"T ٨HBQN ˧5Ѐ,2洍  0(Ae\K՚86@xe @0SW*Y߬-r'A+UJ|o7oޙ&B9J 0$y@ݬIFJyRXGtˤ6U`OsGm2 KZY{Vnl|2b !A8R77qL|oYĆ\Ukm{qZBX7 k:3[T#[ 4=ZCԵ8(RMПؑφ*?G n@;VK@q;|;BD*jj'ݸ9*o}rڕg9IGݣFbϾ!7,?,껸׿WU3%&Ո98rx-=hR؄0P5 Bٱ^%rL'fԕtbn l>XfX;5J'[Bڜ;"7>fɛ`etkҽ=q3XfPT ,g}@m m /z@6vR 0Uʉ2vqwT i'_̳>LjEKO(iwJT9RNdͳAÃlK䛎e(ra4H,>c,jBpNF>;2Zh 6CֽriXx/͐nn  (跦H;c` >I2Ϭf,TӮ X 7ȹ' *~}ӄ=4N@Uk0Uz(|'7rsDC3tT|u1`Y̥F#$[!Iڔq x :8%^<rD/~'#$NTx}zv\ 1hI<9CPP_]؍n:f4apPHZ1x!'_-,\Mj~Ç@—NXxb kKG*Jx}Ly, -"+RQEh!~aHKW+SFD!{$N}oEOmqV[4Wrn}H!I0vLEi  aTM@,XlzxZmT@7@.zo4P6 :gU'd-XGw-[# Np5W_k_R^i+M T&򯌯 EYM9J!xMfGDMs-zվP(b*GJ!-I4P[H$aܶwn Zٵ'֝qAՈǫ7n)O? +Fx=oZ0#=MC+xHm/}ua!9[ TZtmҝƠsr(n$ 'Bi g [, G34dTog>H}{k[ZA.h{N2{~,g|{@8"T:h1uuuejEŅU!W?#L&:ťLje Եe Ġ)C!Y-sx1;`n8l^^;m]mԾu.-؜<37,B:j""./j̆yZ^ W{Sx.XxƖ6V*weizbQ9kk/R_Ns 7 o"$xnt[84A)k(7N~kqi[9Nf*y:/eD XsNcTU'!8݉x5Z]4Q3swɏcN(cc~{˱k5 b q ȓE. e^zFS(\k. Cğp8qEx|7WB0TJ&L ` z>PBXg1V%"_$@vؕ-Q((v`0|(yߵ_KWgOoMw_??Ң.Sk)2rT{ v@ydぴ=5Tb.ͦ8.x)z)0ثAS{Xt=W,&LjLڐ*5# -0hUqU`͡HD84=yja ׌1F Qs;=so`oY~#='NuE}k53DhmCGbĦd7Q1^MAEHցvG"] wOk@ eO` )ee0%Mrofo&AVq xF3M7 5# ʬop,t2@& t@Ϭ )l%dOUUڎOgr1AWHEZz`.5܄Qn'k:g2917"X:][hi0Ё::M- 4hFVxyznD3 dkrKC-ŚәpdвVV {e;PݵCOt[^˫{yZJ0-U$C pʀ~Yx쭖VoԺۜ&)izm0=+Ov5 F/w ᧩V7dc-4$DH樸 rhA!i $ Ѱ䦝%ѠwjhL#00曍LK6$ (wF)ݕYD(Y;*TrV[+dG 6+]YuLM.";s~2*}ũX曢 ]Ϧ}ɹu&x!"8fsT:AeO )Rg sLj;n$<[v фLdV1%uXa;MrB\C&(`g&&H#T4mqk%LFN fC uчըl~*J閰AL 8|G|ǎoDV{5Tg쫭"In3&ׅj^[>d,`g!1C؀.%dj DA1}mVHs h7'L8N9?JВ>u`ZTQv6M?J T_}!M,D79'*y#V~[ۯ oEUCf'SWr"ukj쁊!W Qrkvۡ*:dcι\r)O1@,6CzVΰ*˵ԪN P5>{H >+P-~+V'Y*cReӶBgZ"pS'E;a"rlnXuv7".P\V^%=h-yVLEUpPgr;N胰U9L'rjTԻhư7h JA&C7[œ'?S!FϭvxO5j%nȯ^%okVcC@,-ߢ@[@Trf m0 =J:HC/]k D+utIy:1H'BkS#z&5F7F3%o MM <~GtNfAA1&sg﹃(ZΧkJ k.x*4~'݅q9oK3tO LWo3ul.%g[m6-Nj "|1lkźe[Vju WYsյ3ǛUx-,K*3]kCF H_˗FHcf}`]Xm͎ꎋn΀l/T/t 뽞ݔ!>_M zM5"5JO |<:gKwX^ݿǿ׫+mgހI5RbR¦dj0w&:V4~ke';ъY6-s@?Qrz9P&ƨ4wTk /'* [ͧ~eڡ۵L><`vqJCGuJ%!&&`?kd̨q*&fMe?w?]_=cdf{T3v/:S\RC$UǵGfJ=\sf>TAܮ{kR•C7[!þS;ay`MjQ>̳vxhD^&~xS+0m`/+QXn4;tË{kVڋ/9.^LJ jZp@A2a //"(GrGn?<P#2ʣ h|hJ3%aV5q+lζ%+|FBtp=\K:;̳{XBjޖ5l;zR d+آs/_t fIx2 Ki5 D@F0 1I=kRXpqY ItCq QZc5l26t!e0iGIX0JkM59O*vlm+MQ7NTZ ~7a1 [3ClT *pEt i>D,A6$C%+r@@ S֯__~SRjKlk=QkJIDLᄲ_]cB^8Ld5yvtJ0XBf&?g[G4 Q܌}c<_uX|?8w?%t+! ~CǂPPJ{|-Wʣ3ODYT[Umm"C⮺mS1r%1A۶J!}\ݨRdTЄmeQ΀NVOD(#M&Hc~V,+ m t' m%F1j0,WmxLؘֈ;I Vvitt 2D,`YuzLjgX X@cVܡw?C~+P=jSSsAC~3؊ZjFL6_ ׽r?K~{wFyMfY`2_ 5Ζȭэ֛>: /Ҝ#e{ -4n72r64޽Tu6Y8)oR{Gd<!69%14gZvd?@%2h/1ͼʠ1eFA=8.>}wpuw5b}{SBhB!oWMB: ,PŁf|P ><'kRT j|믿KZ>*.]A>Ae~| jIiQ '+!LZtF2L4H-WfqnkcҶ`!jЗ']3 $"L谍XLR9Eⵣimyq^m*T)%Q*i] @ϪrVqBj㛒3/?Sb5"x fAp} #Fk&#TRD +BѺNrt_!;k4 @C2\IՄrR*PokL{:%QP)N^#r7źs`'-j?. \RulKe4nj{+9؜_{x`Z&c5=LX֯tu2u˟eп@.;pQh cmS LkW-q߮SƁ˜w,]*xbȦCmfo."a"SsX߅ip1R\3gø;9k̎η2ֶ n -f @?FɰK fE8'Afm;^\)< -5riUٯޏe,:=,[tT4%/(n܇%ƒR($Rm.c助*fOW9Uo9Ձt2[~ih O XX ^P7 ))lQag 2`.^Low?Ou} 4+B|e*O*ډ4o8>p͢E;yxm*W35CSMSÙvzm=zO=uAz&5*qO &lUc@^aʂDw,@f٫]iL>]/ w.a){%Q_b7Q ߤ3Hj[{-O :,+Р:ӇI4r-āgxTZc}& s'}ZդSmfe:4(׀@Z=nUU|uͅh6u #%$c$qtWon29"˚P5b 3CĮՌ]!6K <"4 m^e06*#kv#)*|IGۢy5=|J=h(p$i}͂Pr), >+P.qoc>hVDtL5H4g`I Bػp+7duax&6#C%,2t(Ud  F_/?|OR`sPz\{yTVK^fDZTe XIBԯV턜he IR? D!U ֏=v}*bw^RĨME SY??iHY.R?ڪ>P\VTBV(%?fHd1g1@jU5%2N*ǂ"\#=fvhHeH :eniuH" "A.63vU2* 1'QqZ:;Z_xFDE=1ufgg})"3Vdypyl nol@2KV;omoD;ZLY DKDhoYXf]MҞMml̴78=;:>@ JiT[Ǘ7_2K]Ǚ>VLm֨叵\ մ|xxtrk~t a*r4 iPIr(KAg4hl+L+G/'xl؉dC:r{f0#6t4?RQ<3Ѐo5: lbMmܽ+jX6 kmq-.)" J-ƩP5ZHeq #42zuFL^4XV0=p7g '==qU;KM*g ./VJVi@r!Bat#kOtf:wS'0{爺&Zm?Xl;]zQ5BV?mlD9쳀_n#db8<9s|2iĢY*ѷ09 }[YY`9 ZfSlt7U}|uXޗ_Eli4K[ Tyju:iuGGT~qxV B Ж&_T^4 ~o~(Zݔ.rMAuR fʑmIJ h,.djX MxbC`BX0箔UsRF~& ^hlC *wv dT|Qk#A"2?-PnBt dH| ״:p9|qaE 7DM4$  {]G]mVDVQì6mp1L7ac) /cа)Ԥ@g* sJ.;즺6~]Y `Ġ:h|V;6--,t' L G͟ ~m9[&G bb}ڑʊM޾ϧ_xU~8#b+98*RohtB7$0__ϧhuhH8WS \gTw|]h+_ 3,,F3AFTaΎo\{{jd{7Ftd#HP{n֕`y*$ \n*㟾 Ofx):#%̠H$º./),(6HTB3 t֛|F->C8t@CFk{f]$*MՖ5#{Ws, 4L+>y =il5(9s'h=O; S˞ bC^l@ .؜fW[_󶒒l:gC6ǪK7}e,b8†e{8i27Mh5̴QGBUt]>K > ő%HI0)"#mr283Id}*{a2h!/I7- ; $+>2u&wL&F$2S$2ЫN rB5T;R{|;ACan?pן=sd(Nhng`\*)׃@W˦QM/yK:!dUNu̠3ڢ]Π]?7{#EX랄b{ɲҚcϓqpgtW4 S:?ۋҟh@ sIv0FA:TEZ 4Ԕ[C9 *z)pLMď%ԛ#]|}\pr=A t &7 Yȗ#V6$| Thփ>oxjce SW5a+Sj3 t )}M b钐-?iV Rcrᆰ@7_?%#(X,8zrn|U\o.jKQ0ӡ|Id-g[\lm~>ZPX䏪aQ0pe"?U{Jvޯ>୨% f0+c]$6[b:8 "txw |_qJU E"~IF\DʌO>_?V6Fcd҉8PB ܑlb \-ʰuE蹤\3wՃF?󟦁b s'tn"-_ox]c4Nfa3;n?G쐍1 o _HȦ"x I[K'hzŋC{̬{g˔{K]&I\{ ?MPrۥ1V莌R1mx!>ĦMi]EЄ9hBi%|8_10bSDߺDE_^ F &mQ ;=Q>- ka/e}K ԈMxMfh7\rIc{7*>C}E͚IԂM '(VTrIM64GLh|0ܛ\gybD HK`|P+eYĊ_u䨕2tB\7*cF7+ 8&r qn(vǖKꂶ330;*jgp_*[ٖOiu M4?%k ڋy[y;M\gy!M<=|P(l ˰</u%_v\+f§^sҮ)2{a붍&pUC0CL9=%ZvLgXm׵AҐtYc x Rp@}0G3k;vmJٕ (}2XvSN!(ztnj\@Br-=Z}>Z'N9 _~tjv 쪶v>&汽]2CToB)9TkYpL|]Mf@7(y}A;R_ҷt+'K'0Tқ1h{$LLqKk!sRW6  ۰ǎ#10д^ wO&&KdC_$fpʕ۵grZ^0;[\ZXZܬXS>Z\VUOK;C2KWi.}ӫ 4zõnxÞSY,D𱋢a4{zpW]U+\ #cvY=x :ឨohzAVl2d~ջ۷pDQj-F&m.{я|G2"Rc)Lgؠ\{t\;ZxUgk0 $a{rծWy[YdT.%EK -ާ 0axֆ"t.c8| 49b] kXĵXxD"ޚkX8*ו)4=bg>}b4 syӪM"B$BD'jU)qJ4M BӚ~78ڢNW!D'0I![%f:6x*yC 􂗑jb<)D?DhTyH v!U" M,, xQ^eȀ([5y/?Eܭ<$*Z)PZ׌2GXn ,-TK#'"+I~(U,@q[rYQ)|ڃw?o<[ﵳ.b6I\b|퇆jݷWV[~ch X #HIl f##j9RW3i$ aV԰3:>1d膤HXM}]-B ŎMD#j:ûS *m"f&9 Ch"tCd}ݪ7\bf@#6(P>|X}@G>!{$/A I j"|8ds˂cu.hCczw+r.Lv]OƣkѢzTk YXAHϖ> +d)?B1SC?kc6v2[{S&aX9fB k4@ gUZFn߻{mF%ozsGXo#pX#Lc`ؤw*B$FBv;z.WeKk#Da0[/B/ch oȏfV{ 9Tx*9{#R@8S+TX|rd:͐ʻyuݍK'*vW:S%2M8Wք1I& #~|F=Xd`9+q7B+JP+J> ~lhXrv 53et&h6Kx=c_lb:2sqLP3OWr4֦!%Gb/83=Lke[/&&,5@iw Vrv!ѡfZQX6# "-e@<OEh}=E*NnZ٠P#(y$ A֙Yk 7rЁD 4xQbaeFl1Nn1/ 4Z0!*"⿣'ɉ:GFZY˫ot˴P;!n;^]~;kѤHF;sC{@cIdPq{8ӳ`c4llNANS@U޼>ᇿ姿BSJΞP(GN|QWSQ`$pQf儣UgHp-V]e«[NmЈT2lZ1ló[ h !3֦!Ũ^Lr$`4) 4.y8n٣0Dͬ四J!.ofhP4̧۴_,q~O>XQCR̒?B1Q|lN.6,еv':(lz@eV*,^oS=/àŠR:1L NA$g#l| z^o[Z h`E?K⣒^vSEb!p4n]~_Zs6kXU,H 1[n kсl;eySkmnݪ E5̺s>8Қ v䱴a}狓G'5I؁hm%Ͳ^]}8Gu(fe42b> _ȓoXb  ڵBS|lei֐l10 [ B7uB{bx@ݠ)j Ha㜴a?zhX~nĵ_D< mhDعjc2m4yK Rm1duuA&v˝-JrK$S8Ճp^݄Flq8G1EP ?R29}.@f+"wJ%4`?Ht y3FA5JLGЙz TՎ$`9#}h7Q}!ZjYkD8ۯ;V9`&^:Fbښy[g :@FNpL' h)Sxt3xE:Vc6S&ܩ&eȻRm V.铊NԮ.,z=@d#uz,G-uV^Ekٰ xM9d鰝D1FPv(di:Ya|+p(րzqwL I}`Y.Q&P0!Ί*ۅ)so}Pd+_~SSoG!^Vœh5'%Bm4b-; i:`Ddx'@ƷńK$0LGH 4)8TÅk3FU V ʂט32Pps-LPb83?ϔyE\8B|`B/:" )'K3>D^G Wpyɴ>UU-Ea_ev5!^o-Whil^[Fݺ1+BTmr]u4jFu_SuvnU-E$374Ѐ'AkcM*Xh; 51x-H,5$$j{O=<٩VAbzlǾ "  e VpdXӯdd#NTrn)ՊSھPZ'O*lH<cIW2a=/Tb|Z˃h4fv_w G:6iUfSV`ny5 ik=p4Ơ]ZMoqfV0Hjאy1*mۺIoa="$=;wD aѥ_""33d} @@!qp֏0Ryd70 cM4Z8dyf|d CCP~ |b4) -ž㐸iPA"S[%C+! ?*L>ڔ2e]\e{[w.JT)h\Z{q dcj`b˦\ִVcmt} sJh "`jWUZpހ D0.!V-2緿C y[)J=xCbXDŕhxSlagpCmb80ә"8r>̿]4Ɏv"*m2*{$WQfY~*e'vyQCL}6VB3M <鵆;iޠ2g걼UØ>Udmt*ݯ3>M` O87/3aXg5W_4" ?Ʃ!7"~A# oyЋ  0_,;/csefɸ{FŐ?A[^ aYrȷ@0%na@8.s M*p}H}l^/[^SSzg/MzVSLL?o IdgY<f xݝˋgd$IQS37j++LMUT9I~n9 x&'8)I !k +I|:'E4#P{Bu}X^JDTf:ᐣu~\ϫ /QG[Y HF*/RQd7A6"ׅx]uJ'J1qҊr'caI2vPhW'Oێ8).k*Y|=|?FY"a]*i,+f`͇Uˋ=}Qܢ,ίJM?P80p>gyvYX՗d%P:y QGn(h _tWV3o?&K0Jcaޙt0A_k}[ᛉ\ZUBz&'0$`vVIYk*}ݱ2W=&`LG}(𲇡W}5+UŻi+l y"0%Ѐ @X{9%{vDk@EHDs%ZΚH*Q3,aD, lOÎpT4OwKF.<3]᳣i-ԏ;mѲ Ɯ' :S@AV-\0@/g<LH#U+mWIYlTmpKڥj0.dQb+SA%GRE\B-*u7שuzWeȍS(I% -,Զ4rF(`fuox5C^ޣ7mJD헹+h( =USO>~P`r:؄';-'??y8i60{+;uSCnFAF@V=W{CʞXAVrX)8y=g0B X'il .~mÁ%C4@x&Ejl}uN1DCa3^Lhav?CᛩF)5tfy\_~%ЀD5Hw>`8B]f#2ݳji ؋Ȋ -Km3Mi `+ 7,p.3ztGr UM錆!X=󊃶,A4V2WYw#rk>!GV~c֏uF,ڼs4:`Û ^{7ș0Htȍxz2+y9y(-B˧n E=F_&X62{M *L5;RGj\(@aD\5'9;H\nIԫDR9!qXv[0[໚iDͬs+\|}?oʥbUҕ]Uk>pZ/rWugӅ<[clbc\maZBlÑfVtsU 1p^VL/].ȰY+*2'+,c19%%l7NH'o_} K,Ybˎ7q7s*OyW^01@+; }Gq٨3:%Ԧm`< ;Y֔wxDDzj8By\tد#moz l׍PƬ7F VKS`N@o#(%c#O' 0{Ӟ)B=+5#1T*7?"h&r '>BX2pl=!9o(ISqj˴ Ӕ03cG<tf&W#6Cjy gU#nOepEڪ5~P5@Y7!jrV-+D X9B҆G'^]}$$8k -eCiS+9?8y믾'}_.^T:QԣT5{Ym雀Mj::RS.!qEYh(,=ݨ=xsu⑔b,sb1?@5D4Soߟ_֖*ť{1'Gh~p%8[D2 >b8@w!OʀtHSq{HzrZ^90%䣸$DlQ)ƤćN B#ISwfz[<.%DAcC>G_|Gu挆ALTRS&ϤjD -UQv@blHv?¨ G Dm{'mzXMG57,ӎ%\D.4xf6ps /LnX `39[;>'%b oWmx,cY[OM45 .֭&'E<%-Yl}^/TLߙ%PL(WءH73b3cI`HZ1b#'v2:M Bi6[eqIJNDO^n˃ EBCe$n o~NJ,E?X3nvXofaz]Y:mԆ35`[D߿oޞ(_/3Q.Qc٢\i\gXZ]mJ4P_XǞVTF o:{n-:ޡvś ?Ú5?09|uJB#wZ5]D(לFf_P05'UPj(P_Y^tVUX)2.9̊v`)OHjf4zi]E腏3vYp--Sogn~#ejMa6QU튯\:r%V.ZrL2xd~gP5l 6T ]!w)Ơ ŘUx$wOz D'5uD |*<!ᢧ< YM\hhz~X4kPo(=P',FP9E(械u͑)8y9b{8c w>f*$b(Pm4 |r])Pb8bBF88|b0da0tՏQZ)Ic\],tAuͨ$姾yn96>9)ii]MgI9YTԾy!/1S%`)Da27[R]lv QKcȀiN ,kA1ae#:?xx﷿DoCNKW/gX7:ߞ/_U} #YA] 赇`Mײxz(q=9ɫN 7絉1CrNQ30k=4 |G-WWI{Wفnr 1tDy0ojry&օ9Y:㇏o?O- $6&a3Lq?;ukJ9r=Tq 31NFLŃ@6i8Mg(h5nl 2B6 W)3~ 2¾SqkYȝiH N<&W##pWV21Ӊɐn[:x'Nlq0CIY/٣ _9|Vb׬Hq4l_Y}NѡfI -io9a98 -j[_ s{`3".1؀q Z[Ph9Mp^ʰI@%+Cy0AL{v 2"&9DڧS#4or2»q^MvނcO(QV.j!C:]ʆLΰG=8c4m =犆!zHCfڍ@ծd3{wskBD]ƾb#Vs牵p~,g4PDͰ15x [A ",w[~f V\-/~w$ڢI;D|Y-҄جeJ'p_fMI@w˞AXuv9=l]{gU/G.ނߧLEdQ:a(HϪ~A jy{*`n 1`alf 豸>gуK;¦5d&J4׿E0`Ϟ2fɠEWV\NBNYB/Tʓ= ÉC!ߋXD޻J4>!HARjQUG3ԩ"/臮TC¶L#"̏FW.xf֦H.bd]F4'M@W\qIrɚ΍ؑe0bK!iX ) : t=eWz2ƚǢtK`edZƓ1//&OPVb/M[!::c&/ddKuơ+z*&W~9z秹Zh&_0bXn|i(7"|432O:z5r @Л~Nw /(-?PRkwiݳ2ᅕV!+Ox~v^2T@Eo] ~qlE Bꭴ ki%ܛTmDF  fCB6M$ =?w<.qzlQxPn&j3ℜ?U {% j 9ӒQӫGy)I玊%3 )k$#.⢒zy!Ν{2:"g$TFfILVԏzoHT\ډuiu^y)['R/I$ ٕY'sMbرH`}(ܫwNܿ_@~O;е2PVӚ%&E.'xr( c~fel;gpcb9X>=M <`&Q.5SiST?П S?%gSl G5EfizhcL5pTgӿlg2PϯĦN@1 ?xbevX e0`͂=nMPWi~v5-i't۰20]q&V[.UoF#\Yk\YَɮDU/O^hf0Ʋ(S,@S).Bo%VG^mrñ+/!/9ăQh^)bv5Jw aGM=ֽOh5ʳM7,j()Tϕy㋗/^4(v-[xls1,uvXH˦M,T3 VSWC?=:9u+^\0TP9b6eɚs+QNUPKQ(\LYFas!hPc@Q62se=*aN43!yOڍPBrqˈ:1j`rO-UVW|Z癷tTF2vh\ћUn ?oa zžWPl|.pэ-֌#o  Y{!19Wf2c7,>;NhFlx8O4hS槧=h52$1h8#9c+%r<"^e 3[7VQj׳T 3C2P'h!%6T4:xM2WEOH%#5z|𧣼d)J(ű\ӰϚlQ܂F!(U䀴]8z#}p=Sؗ4St LNi8ެe4g"76fG8N0YuկuH D%Ԫ)=᧏R#ERt8?T4 4+2R^#+Aps iΈЅn U_m`|Qbxb~mr[ߏ[lt `nB0=3WE3 ٯAP if{TjUq~ |$sm3|FAbL-&CFL>ANq^S98}湼`OZۼ{9|ZW "y]HUO2$JBF"9 {SJ_P鍌zě'N ZYDrD:E 2}ts8ҩ$mV/,2#oҧ h^o#5XC'v8'6Y={)M픽IB͜hwEHm#Ѝ^doUl\%M98KEa7LhIw1NUi1`Pv2<aݚ\b̝q$ŝpYźPI>kW>~TN4ݣtwuAO\,fimY:=zjN MUsMѮBMhx cl: ̢gd2Θ7fia6N݌v,`8${&2l{̜ o~<6P x;i ydLC&u+g$c mEB\*4L,v`\v\m 0'uKN:WIZȆy CpGw{ld|d:6[:5;yAaYT>\\W?}_qS> sO"Pˆ\EjiF;b Ҝ X-e}syKI~=ߒwP2V_Nاɂ"URؓԉ>>~P-zK:-Mt6g B48N\_։~yKveqIA>aWm!IG(( u鷨Twxo\CQ,^L:hr?z| `N{vy6f ]=v\>eq@1_y5ՆC͟v+}?n #G|9@"Z&h*x&+a603*l`EMy.VEeȾ;.r; w?ԝQ`ٯ(Xk 5? ǯ"U V٪#Vts:cwB3+:)HN=X |X[.' biV46#ptjjs?VEZ>P i&eF'/ EZGb9MjD8yo'u@C?[}x넏sNmiI :VPBz 2dft*ueaqN|g3lO~v߭N։RkVpd~j_umWuB9U^ў>%$X":B,lWQG֥ 9i=q!\"[ԛNjO{?OHVS$+@eT*ؚ"@"Q(ѤPNyp M ue_)MW ?"pRx-O>1ZKgn߼_?MA%,? AVm-aCXvӦmGPECG=X,RM7Wl̨;| \X@# d^&Xò~(6ZcѦ[68DZ4W$Gz8S`Dc]]hh~ 6p9i^ ao[#^W@h u.Nm2cL|!ε.ѡS} !ޢ/oo'v~i2a[_G3%oշ?yVByΛkuU엄{h" AgҊoɄs< /⵼{[g4UZvM%X<+<4IķY/NjdZAWsv2Pʼ6 ᨪva͕3$xFVaX3;AvbחdDTA x~ Gm;X$89zbx]GzEuŎi= _\0ru "8P9k 4 =ms&_DlV-%8hfMʏ6^ơS?r01V'; $vs$<:L!>ZpoN^&{@C$\c 3G\na$'E!9B}җt*}6+kSl-E<{x(ꍖ[ugq)m!YF$sGФUDh/e^l L wG<`A&/5[Ě泺$h6 8eUv7S@ѧk %&ۮHGr$w0xrhjC_:-d_ p'̲:I!"Xu^ir E}Q잹^4xx;`3 ܏՚8cѶ-|`|l`2*@d`2xr-M^žꇭ*ns)nxϜuzo{av0*PRG?GKbX>1ckYs ϰ7Suyv;PMμa]cg'n`eeqY0D,3UiLlpMYM,LGW;.)IOvXt?%La:{Ҝ(1bm Ma˸num#",Q|bm.o+ፒWf5=u~V_glU;VFHdk7~W_}'?̊:H4Ht*Dmq,r39Je#ۺ0qc m1qR6O]2 f]rudŇ7=D+-)-=<k0K猆M/=qqZ\M8V̫7Oh I%fBH&5IH)^1`b:^^?E &/`FQoSe} q~xwo޿:gPᄐYG)?_#% j^U^M*Wj@p%2pꉏ|[ǣF$Qd.=YP9ћ<e@])Z(ؑ9mU[/V9SFJ̯j*PP PWhxיt%V#=2&a0H B>(:NLE\fI~&Ja@DENsïN`W\y{^[z? ,C@Yr0x{\UYS Lϩ= HEJ/ kKP$:P n&jNInIY+ڌ1f>˖udX+%SDgsIJJeeQIMݛd֘aB3ry#wr!g9P )+Bh;  bh msh>CMJeiSMPߙ)G} BIR1Κ_CP޽yx{!ZTqC1Va@)rGZ9BB5'@/~E6i D^X+&uI3MPL8Ć 3wIS FG:2i^|`׆/:wo/`)cZ[y|K:yYp-o呒+-EjLMy$$BXI؎KxU1(d!0=B" Ro A]Π?ۨdP3YPQgVV|?z\ĭY>zU#eX,<4$Be+0j#,?Ƹno#q#5 s3l)~V9G17w>/L/^#-Z~l&tgfzK&j~1SӗAo)6@6DÆ?d`:R.!Ӄ.]2kݻ.$sgyfEVZY M ,/E6ZcȬtJb?|Qخ^rx<8s|Lخ#6ΖɅzCym]ڭĂh9=N^ 6 UL9ʆNxW 0*ujݿ`0Rв]@ К͗LmFD~)AFC 5!l ZѨ= uG/HIv;*z|5 (%nW\w߫E* |g.홵^$ vc~;f7/',jxZ=Wد-pHRTDBy]P_ekT tITr35!!Y !Gto@GJ b~Hu``*$rT@1C(85`a9.:2 <*U~0/HAx |{lRʭxB&k@+ҁE1@?jߩqWM\*-8KWs-_o=gM0PD$^fhD9WȬ:GF&}IcF$JS'PxC(Q+ N5Nי_܅Y5,Q U^6V-bK^~kwՁ !^a\ ƀdDi9˂sE R VQH_zRb!q3Į$(_|7ߔu~^!k%j7*n7Y~pMsv_o0oܨwK _fVkKRx8!?}(,6k>|Q^oih0a")"sy?ۯ]zAqfQĴ5JKv ,!\E^=38jC :oY$`Qn*e9NG3[%c mh W9K̬!Z*Ϩzcխro[s?:FXiqR;w?9o>ꊆ!X!%?ҐO4ڪYHfv&SoqfPq ~lxŠ~4`бg]+uc]pT&"ך4`MiAf7[oFX (9a. zs #vFj<۽}vzZwv*,fC[Ic列c?:hUUÊъ.  mհ:ycU ]o{`j`G6 kv\Mq®%/3FIqDye͸t=9@#唷J>ٲ{W[[; =[a C⍻@jɪ\LA/3oqQ\7cF2;QᚤqKa ;XS#yf B LD`J`<@4Lh` QXTQ@K4JI6^;cf<5VWH>@F@I뤴 D$6nIx]tk+L2j=~c=-u=MA:su4]S2B-9(cif6%#8f5d&Y 43,ߘE,1O4҅$D7$*R;:~ILTJSsc1>E?%j/ۻI-VƱc.hԒT|OO~]KA5t5K#pR<%-R,rK ⫝̸!( PE)2 ohJa5+budw?A xS/TtDtʑ0Ӑo֬0?W_}C+RlX $IQE^W ~MLflmK4o%*_Ŝo޽)_.ly8LRZaɢHQVVsRS-N5ء4Mo SZ~ -WfgWY %]PK9w}gO? /mԊ`[Kݦ6 ։<҃Qvg-r:z9 _nf$  >.λ9GC;J{׏fYl ",:LiX1X- xv] m?=͘<#u ݇xph؏1% -IG9(Xyi tۅ{Av_P L+Qąw'9MDz0D(ȯ h~ӔhuA&ƀxR~E *v(XސZ˓86{+#W*sNԖk a>&u^/e5ߜ]UԎS^Qk7ڼ]eMr7 |p+Ujm#a¹?R?FRi\O R Țx6ko˯<ڪMN8HpHâД8˳Cjڸ$v;EoԠ DJS45RhZvWX-ֻNE̺yJov~~փ{=*$FG!&H`kR NH']O6! ^A&ZPI^lB #aᙇMO ga(u˾C J<>8Jl'qr&|2!T,@xx7~wweX-Mb}E*#ACs",hś.T!z5+qud e3XIMS8"(&x*w40UdǪt ¢>Y5Y+I35#qZ9WsZ'[s~`|e8a0dXW l#s 8$LNxqaj9Lc9ս &a2 LU S@ufJkD3%4A+G8/ ܣ:M$Lĉ(ϞͷÛz{/ Z'3RʵHTP龎-QJuC9d/ {Ls;-EX-#yYF0, yʸ=|D?ǜRwI< ,3UFx\a3ǁi0]{ :4N4X`ڊ7[+ m+CtxaaG~ bԯ ]ÎJ7UZ(xq՟ ,d Q,)LNY$6^_ȯ5O e e7dNN|k3ʎ.:jw s3a[M6Ev87dZ }d#=O0VYՠ+ < [;F(ͪZa0##p]|u+9)+ޝf>04˩/v';gbE]\]z f=%"btr[TM?N.͂o$i:}+r6q l4^]z$ ((4\s2H fNt a!_',54}HJO8`r] M 4$*5ͪe kl`Gn'1Th#AoOJTC,"-޻?yt=e{D^qL`3DwN)$M-m&MA[x=P4xYݬ \n‘O kֿ-}5$Ω t WE-_T 8f] H9 JDN=  !&`2Y%@e0l`93 'ĨO pBw$WWD(<]%,¨6a9"Ӄ3e`qd8w K6Q Vf"5"(yuQ)GM-/t%_+P#XրӃүbZ~{NGʽB(ۀla)~ԡuPk,-$VuP$/f+]d@M2`sg+8Nv@mLEcyoj=̐Vl^bcZaU=͸(UD ZDN mИNz$Ӑʱ}00N
6(y&p̛m-bZT@y`jǨK@bqupD̜ ϰw6t,[y/woVUi2FpL濣#g$ ΍\WYMf t,h%qgf[66ny~{`LR;aRwtp`!澒۳hx%O+++n?rؤ L&Ǽ%-WKC0qh7TQZ2q]͟+4NJxӳ+DQ/M 2w'vԓBOj'hg]o88^.G_`\/_]ww[xyWzFMrur ,u5/SFr`t6<&;"@q` NщaT`ֈajo 4,#6spV jʿDEl]-_O` 5u5h^'KZA&I:n3ZD5iѳjvf\o_sU @@ %a; >ar%qIXÚEX#AK&xFҿGiSd ˃wMM"62(vaDoယ4J&t{' 4Xj:g{th'DSe3Xvw uo[. ^R-Lا1 AvmK݈YHT[sKW!Y/;+&I7 8ƙ<#z,R F]p8 B&>/(.-'|m$X)Oj1i]. [dmDxx$ïhi \ !:Wmb$ԥO&H["C!VV|_v0QXX"Ps9CF ѻ/^|U~J4j !v\gC*Q{ϻVy]~Hx[A\{+?0[EF</SP8- y(#rE?[~DӀ"SlFc }qPKV\^)w_~Y3x8{"*jPNF袎W㎼w$I/P&  b=4;g*c^+wPyʝV. <1[Dθ«)=`DdEck%`L/7N.UUn^̣Ms%\\e˯֊:O?wzgWb/FLa/nj\YFnx&m7i|Hؔrv(zf0xK_EL=*:(|-i~gc{̪ lg}_AferǚzKĖ2376R2v F']3[K byk+ +uYdpeݶAiP|p\ q_BSeCT4пi01Vz&Ns i/xC p:EЭ6Yc-6dG:"; B;3`$W&Y!¸تhקws{HsYiivC.ȇgO/Z 8iv6PQs(WW&+khDi:VqdX .U1v+U8,Xzarh/O_Wi'tNC m`l?0OxM+O)ᖇ5纑z(H1+&A a$-Au]o/֢2ζ,!-BUpTLeDꖔ}S/,O|E[ERz00fE*ZHX_X)g-|moTM=\&n6҉h5nwFGcℋ7o~Vx]rp<6~Q-A К̑CT"EeM'^s Q X_Q-A$۵{=MGp 6ųŞ^huؿ7sGG5N08~* 1RK lӗßs 5dǾIW07pl.FRe N<>_ߴ1nbHߛF\`D^+~ [,<ȱt`e}P)[W(^H('6eӅ 53iG`Whn,wk[/;9 ݰ0Fwnv>tǀ>ԿVXKϢ(?BbvxHygxh"YO/^R,SFFTW>#X_>KoUVen@;5:_&Æh~bww\;ް+ Y b {c%̡݊t-Xwp?dyxhdr N2vYeӼE?/yYOy,\` Kh13*T2CdBj)5 l%$!I4> ^P;(p_Dճ:ϙ50\Yb` 㚘jls>XI\T})Z\ 4wj5W' oRh/j ~iJ#QEK('u:`nsI-yƒJ+r?MsD15ia9!H(Bߤ7jERkȠn޹sGGJ4T:IB:엑{}(ܾe Pa_M|*NF<kN:i*P.?%58>L/#9^:$P0R ί.+ Sُ ۝79lz%D"qp0nYT*/2F)X@TPRtoǎbc'; uVKY5WEܩFv9mq:tj1LH̫f]gLT}tYXrHKH58C3Bs/JЉToJ:_H^3%M9 SZz0ƽ "oV=\65I:m]5A&a803x = f@᳐I@!a %hlr8n <N 0+1,FA+HlȝE߹0V|l&:2uMun*rTm>B Aۥhz)jV\Y6 SGluyp1 @)g8CD7Pѓ7 -GZt1=]TJw?<}HVVGS\-bA(EZ/R,ժڇT:0BBEm`$AbƚV[̇K9( $;®uѝ[u@?7։iygMwsg1;#z ^y7C\`SL99N%|aM"Y һ\ijN/PC˵ġ` &^Buo-ʳ0`1 scLq]^e ^uj({ Aj4 izD8)PțQsŎ3vfPշybcn#Yoכ>?~'hccCh o4fÙZ]43726aGO*+T!"Ck-ZTA`S0P 8FYx3䓮t[paFk1/X+7+.)h**3Mt=FI9Tz{H9¤vԩܷܦ YbDmՈ Hl4N>fic32&ZhY1cr D UV;|ŐgM]sH3xK\TF|+ƈ!]mI62q戆XA!T`ܹ}M|NˤBјxSFz@hp Y\8yur+i]rhGDM#ˬ w9u ,@ńv,Hȴ/EnE T ta5"Vi*?cFn$Zt!L6ʔe\2ijcߵphBðt+'<:Q#!BTߣeQÃb얞&^&$>;3|/B $G|ǂi6e~p#rE$+|LUcfkboXJ[buu߶H5{[Y1ި5[V/HbBqPhf -[X R"{l:/_'TBF8B`D6:>wܮ9Qŷv+P qIn|^LVح=> B$''.cIVВRZrxJy }RVgZ8g^jMAs`XODyLOV^1fs&<8=VR L_s eFgq^LCl+d6qa 5k0iȀn͵.χχEyz-3EV&?Zt@a t Y7s}ݼOb_(aX33 i"g `D=∩qKf$?GsZ[za(3)}d^5g?:XIU 3i XC^0"10P]&ҧV,QpAW+0:r/V؀L13a@[O.׌R3%_uʧ P"n(5iV.S"JtzpS:# ?K`!H"^ @`wKPR\U`_'+߿mTf~ra|3Ի}qrҔ*3Kb*<6X!]';9G_:j*`jmԒl1A)UcsR*2܅$l1H ~},xd+)Hv, SjIKNܫ\ÝJ4Ԣ@40l$8innpLD~ugF1NxŻS?ah\ptՍ^ ,A>♃R |zdO_>%WM*aI@Lv]fY]Z`ۡ¤=&%@&v \܃>;v>e_3 y|dž{y,\dl,amްVlTMqx~]%]I"ɘ_ly6caYpg7(DbFѐK=P<9*!|&ާmvړS?" *e<꺏=Vo^ի-O.j>g\9MPʎzD6;A[I{^\@qV8RrA1DaPC#^ȴbX%ċ0N4rQZ6xG1O^3\'gmiXasQ/Ħ,0pQtt%SC~?g_\T3 r@&RyݥD#=54no21da&G |ք )g}VLތ`P6rɮ;5LO.1Ewgsq@vެ1fiy;JsxvP-Bɯ|CaåhKOij+eI?_{eBe c,JUt3n*@MܵMbmcjǑ]x (&MXB`)^k[hk;`+N|}ʸ*>Q{e{]`X:kƀiva8vզ; ̗rX4cA.͕[@cA:ao?v@O)+SB NmԑDRt7f@ҩ~vfi3>B+&}uu߇-ػ뫙ogD!uM31i)÷"b;$:t fVHert m'}4MFlL*(F,o޽35XRE9R:Q +,7׾mXPi]Ya6N:~Ԣg'3`c_=_0JA.?YuZp ݽjC'Jdފtp@zzЎƑ.δXM`"˓TU}@!:o`tDY.@glVTgB~V ẍ́=J)H`]/j.pūGβRz<4Oh s 3 ٔYGG"kw=9n2+lN*hsluLf;j"p9ޥ@KB<ć{-^pGV[ݜKpgYJ92"`pLvu͞3.e6:S-NIDFǐ tMa8}1Dz#ݓ23d(hshԒ s/$PPӁ%e~~b[cԤ*Xo$4%N:"1%՘f~"ƊDѡeઢ' [IƪE94J”/M L=o,QSݍjrC1K5b'^Q 67`7?,\oTEC%cjB \a[cPy-NzáF/Gs̑> ,g\C^ldRfTM7spCr#:ǠXz`PKS]bL[w9uĕ=͢2'oT A>lfޗAW_X :~,ShϚj\dn9e<|fGvLcf_MFZHB>^ ՛ 4ʥ3w(ds(sY?4}#d0`2dLk&C4I^ kLьm1HxX/U@jg~7G1; 悝oʠiJ!g;E V_o߾;yy?8y}V@}ΠC"/#FJ: t*[5Gj̚!f8եR8$_#P D?7=, 4o0Z{:Pzu=ǗInelNTR#/kXTRi2g ֶ+!f L-gܻNT~fyK`%Ю봙[tK\.T]QEj$`}a!VΐPeayqŴU]xGgn荏>^%|>ƜI CVh&`Q{X x@"q1Bwlgw8q}ٝJi"K\!p "?+'[쒧Mtq0F@Of]x &Х0'DMJr&Q !P'(W"69$nC* IZ ]i LqLymM;ۄLNV4IctyjLDٚgG­f~gB (/E$h9.#Tc@}ں_7ןPǎA*}n{̂fG|C 8 jDBV[;XK z"ea^I)VA^p$r^F}^O[}t{-ϰ]F^/ CrtBeT-9G-XW ??Y=f]doGd`82 XGoSķfԉ| VZ1ixK h D^o%Uyxpu~؃Dq..瞤41q"5 MjY2D%CofD9']"ΧzƋbhbvAɲ/>>뵗&X=U 1u Ʈ6%*T\GpƵnꙗM( Cܾ0`m cPY4D$e< N6L ~e7 Ps*qktC9Llu]OuZt,2i -kDƍ쓩^O%x965X=9/)nQkx Islm G[卤S/ UD")p ii39dg]tVR)V՘WHA {@DX >F?JgEn"|l{RRqܝs&bDa1i'ԐrXe;LP f8z2t0ra22@/b.(nO^OV0XIQu[8*fu'JFU}1\?鉁~ OokwF9x-,w/T֖*EqQg=E@NmI8S⼄MTotXS=!VEI;i!r΢A}e p=/pRHa=I>9eVzN%Huܲz'~ZGͣO>O6{qа.r0 e 2LO_"T:lR]\ȴ,+8BG'/5'.W0oа;|1ˆHYp]Ji\Ԫܠ{tvhw&4n]֋4Tm6Ю0n[7Sywj:Ah5Xl{Xcgc3,cZ~|6[5؏в6ój,@MqQ7ld p:11܊v8x=,Ri 8{c cDtiB 8$t d7뵍Io;m8uz^rT3'qvar@+K҅á|;wZjOg=e^t ?^#VȦnrizVgPW[zɩ V&y1)$96aħ+)b4J U{S -1^ЏsEZ E} R fGKI-/W$d<H8PVފh \$4%/sdBiMc 2]2L˞zaK߂NS1PH!o[j{8"Pkd 9h"qC9!M|Ru C)5RoW%b?7kCD>x胵;pQ pZ(dҚc3 ,aG~ݒ#t~m:_YjS ;w 7CD/Ar@9֯[X?Т`[9senS*m 5o2}ɰ z}_+Gˁy/L@U"~QoęAI_E-xVb)bx:S,e jAN&$͖[:T[ND6-Q5߳6-똆W^^s҄V*دsNя(5288ppWށ-{SNdR(lu`BVDN+xT W.k1L 7o?{JM %|\D1g ClW;-VfYp4(˰J50Z(`FLV1N֯E6WyC`nHi-,<aZY Υ-G}Ո=c)Jk1࣏>zܽlhϱ{{N4‰X8e77 02mF閕v._zRӐ-qCJC z^\.7h`YfiC֞e 4MiXf4!7@"kvl0zlZ- fsb9- Y}Z=QQ%.WHz{~GnA3bFgN ^`lm~o:Zz6k5#Co~[|3CӽWC\,}5}3 )zUBd3zvzvZW=|XuxiVԬ}=\' S'vTE+7Ԏ˅_K$:*QkǬ nWW4dqbqZ_ru1ʄqh7=fc˶orI_k"њíO Q'˽bzL[&4ɬJ]ME$@;:|8?Ѻ_3tD# Xz&bժx%Alur"/xRnOݤWu0s H;(M:^\OphYox.`_Q` ` u:Wo֠$H6|RwӾJb!'b[;!kz鏯;Ire ܫ@iK~L"'(Aq~Ď0pG Ը",rCwE8 PM^)p* OnDdrqݍ6J:Cr145R7u5PkW1l8B=g/8`TJy2~R݌8_&(KM޿DYքJ*(U}RGPʴ:ʁ 7F,z9fu%Yh?*Lʴ"wu`6(i hM頩/^?#"qeѪ OZ;2-ѰA{a EAW $U앳7ը2wo&%20IjmYb$i9fؑaIl̑%o%2j_U|V߭Y+WuO??MEqB\c}}bkY ],W-ذ%]*mP =?>  :Uس3dte)y?mW|b*`=R\.i2Hh-NB`Xˀ6̌ I~pDId3!Mb0Vq{%q+2CS J7sE `YvsFS1{~5D=ps:6j-NHZr`U_Ex: Jp.*,'?-u2ɽuʰBl}+tl[Jh,)#`n"4u}cmg.`f2G7Rsz%+^R:ƹ,)V/Er} F'S3&1lL핞T s ]Nc(?Y[/䗊ߟ<|ТTQmd]!U\5 fP?ĪYrv@Bh)t`1YIA1KQ&*aqmpG5pBj$ ?樿(ǑERz?sǶFZ 06T]VŎ'2U}Ea(?)(IS }p7Ѱ8.סa4i+zaDqB5r }fR{㋱e-1_ںA~(65w&=NS7a4Q U2Mtq~E w5hr ל \A -8jۋhŦ(3f2S"@B}mKc n2.2Q3 AF#6rՄZlqQUo=x56"EG>PԺ ~^0&`v+`P]Y&/޼ՋWk(%I 6};8;>/VpI&m3*HOgT4]|q}YM2Cn {g9=̈@AS4\!KD$顟6cC!Kz#"*/.Tߧ9{Tn ;6k6M7QyHJ!̪G>RD,D/:鐁oYf8?r[>ÓC3'r4k; jɻ8Ru;_]rj)v%Hl#a[sP;u+;I z[ L䩱yi!+!}"2Dz:lzj(6{*JO&q:S'GFDP&vZV* Ț-v#^*hO.T: i%BYBIh6"tI8[I xiبfA69A;E`q!mZϛ xc\pJvspӤl rg#_i^$h>Frj!%9Tؔ4ԭJ5+jauH,Z!#w8ʩW,hl_g+3P#vi}ـ0 6 |iR/?N_KQHrml^@D_# QZh]*1mIIE_x Iń!dmՖuSYq%!IlWP,ffM8|G^yP8 !Pڅcɣ{/Z 8oƝ?;p|`qz - ?7Q˭Ԓkcp?w?9>R7?2&\0i͙F%B@1QjS^Ϙ[ io ƞks$o1[2r-tJUߚi&vX`![@7"X^>2E/u16gǤm)Y4zva▱'̛ 6h&Pp#Kiq>&i;MD1cc]b>*TX]g'x_. !OsB&Ah@|t&dYV$kTeke0#3c]Gw)]{kl},d:V1=G`#d7e:6RPWW+gijrhO&ERLON '3+UZ~Agle8AڲRN'B "8Bth5pr;#i̼"-](*:0hVcrCd8ʀqZ[XdU# O`8]$)E7UF.D:)2OdwPT7 MCO5 u+)rPƔo]CrDc޾r&0Q9;wkNYnS sRH6j֛`8Z,EVgi&$KL赋RAB껜ǣdpjTyX%5PtDM<ٰW)!4VaV3̜-zu:% Du-yAwGF |76L-TI(#Om, ]*R2H8xEy0Z ֱ8HدШP*F%Jf`Ԛ|J B.b\ P3a/"MMVգ3ZUެ>ֵbv ;tt`dp(̇xлqFΊ^ G6@vgZq?dmJB;tu 6:-~#Ǭv<ӋIBI_`\Z#П5s/'{f, UW#F ,00|`i >Ѵ JT-VU*!i/J7k3a6$0>{['ƴ֏:~=[mt)52k[GƲGyhg!&Fo!r_<".|CŞu7fl/JNx Ykܖ}\M#\4!js_o[#?P724&NJ3W_\{N8w,S6%'-l{ sM;xAbJ@ܵg5FWxgnC1k_i]-w/hBA K l _Dma3W /QIoEk=7B0I}UMY m`,Sa3D' M0:sEL1r䆂0|̴W 4d=sBDGq2 I0$&!2QkNDF0LANAkr&3цY+UFQx_j+ ZB=MsK*iogý"}`þL^9owbҧ/6KDUZt%up{=/_c3G y_Éɛ^ЬAc{dűьGbf5O;/ȐEjoI5'ϚY&Fΐb:VNowj53cu"ꎷS?х$l9\P.҆e?';~l~.;?b)5 Fn*x8GޝA} k>I*Av !z,Z[NiPVS\[Msse90WMXU| a w4QO4 0Sglc3HsxѼMD2|[Ӭ%|i2bq bv2T=aKRaP1R-97'8?dd#ph꼞G|amZDC;ZWd?ooxOϋ57-zC\b^5!^Po0DyL10ρ:uµٳd[-FD\g6(O)ˇ,[aTC͚=4D-Jk.\jcc"(%ômv&"C=.v0rs8A#hsY&=mx䑶Si=[cs;8X b.<F2As#w }T RXU߲O4x\. N΢,S%":"(lh}%9en8 o 'yu!Ծ|^tK 1CC_Yq^wñqNzs31qOZR3u]ÌIwc 'WPTm+\WQYts7#vE>p@{hd`GG kӤ(jd Zס\ ! ʭ۞yMlJBliT1L&FRSgggguV_-w,r{u!D%. xx0 M*X؜cM%8BY{, (V (@۝mqo[=@R=>m!*08J0MހjR׈a= &%K}ՓOmF F^ ]'NxRk!骭qj<I9`'Č;FIlS ,ӝbF?IЬsB1nƄ{'4uKt5.4lkl uvsX\d3`C>ݑ Cmq ]y/_e~3;`5N۹aS<0K|kI} Ճuώ;^Ȳ'&Lf_I%TECd,>YY%4(ej;s0\ Zĥ =j68Y$]ITF]QFP=7XlRrظ+]O+z:/ W_G+t =e5Fd_ %XL+V66J[1yppHaP.! )4;}YoןBڈ0eᓑsh瞡 (Sԩpt[*h tcְu't{IK2J)v"ÑFxi <3Z'FG Q;,+q d(/)edȹP#8Q ,;PDwv09PAUӶ&ԬMRW87&xSS>1i $|Xi1![fTOҎbڑtL碿8r2oJ| ]:DSWZw$Y#SX"דH`3C!wt<9뛑{'$ܠ3L4l-|y՛?ǚ#4SLDM|ӡo'5/޳c2m^aq_Ք2XqYH0qX@w `A,T[3PYhsYX8kw3 =͓KT? |XW'=r ܳO0E8ѫi4U[Q+3!O$+ZәD朕4X` Whpfo]> LI|QC,YQgɪq)v 7YEA(ut#ر~E&mn~|AR޴p.-UTfOh(JvFd_w#\g(53o #rzIo#xsۙrb%PH`!\Μ 64Qԕ !)~ zmp|.4w}RCۅ{H0V4CY1rk<̂k\dGgvt;d*ONN5be,YX6r!'a{x &"S#$إ@!Tg=S#hhOeAAoi+7\NHUĢHH22{jL.e@5"{cFbHVzЕ2@lt BUw39i`}RӾ^5 nBt9dy.}?q/{j_Ǐ $jStvVkjn3qyV tqw;]1~5{s ?Ӹh d?~eiz g =7}ix,bl}) { 1m˔!:ukK.5-&P'+G⷟YOC*gnR#5|EGoZP"p7d]=b3χɳ7XMl5.>\AZ09Çf) 89ұnN56F㴙i[$ oK 8}y̓VfVPtt C d^VVW4ogŝ*"|D im g]~8N? ( Pmϸ26?tz8I۴EH_uV\Q{!0C4L,}B[!xY#/9-f.G4ǚaA_Ǿ/DA&Cܥ*pT&l:2'^\j0{@& Hɺ:a ;#GU|jQ:Q%s8Db(:lv}ze%^˟KD?Vhp37LKK?2af G䱐P>=xUEdg˜&O$nJVE(HT1~:ׁd5"(^ "Ή8̩uj%Eޞ)@쏥ѳRx/ӎڰjSEej '7v8 #(Azp BY"*)ԃ7zܥb^b騯v% ([Qn=zK0T$w'߄AimGD&ɫ'Z)]v8ֲgb53.oG!__v٤`>ܐO1VhN2,V>j.gl:kU E"vh>бظoUUmhYyltD_~FTI٥RG=K>)|XzX_eĩh1znB;ر<7}0_n "w0ت9VhdfaILU ۈhfagIhDC}&Q/5u+x*$gUajl*P_T𮎄CsKK95AM%f/KcNAmpy3tdb1ejc([fcvEѰ =*O1om#ǝI/$ĸٜ[G9Sùй: c+D<[!+FX7;\h(KթD }),8Q/X$څU˵#틙 ǛNg>Mח6sx^,$MR5Y=XnY=m--C g"$ l>hX:ܣ:_0zt| nM4^c .=\YٶA`Z3ږM.dLW_#s!끗5/d*|l=q۳#1iGO+dʖ6ixyTi dn4_AqHE]N\ڐ{\̊x.~Ux9{ߝfy^\mZ5L"_׌YGˏ:*Ë8_ oK愐zC>E 7uCLw =x)DDu *&@)9֮[jzar)MpoĆ"o I\Y;x- rjsE[fxT3$vĭmJ+ݙ]\p7_T"Ȏn[56ujcmn3jbr##8,`S{8#$cg Ib"/Ǿѹ&1qW4<I3 =-i6x)7F1ה%ql2RE\(E`:/Y/Mx^Z`my_Bq0{5%lƅIuoK\a"Q`k8 )@r)PwO,m'I#Tًg'%⅝R+W@S X bbX`q9^!)e2-*Pg4SyAcꯤ&h0*٫8fuDqZCW@n`0{SѤUA>>h]vtfŁ0H;7{Go, C\X~=a20Ӹ2Y"3ϱϺϰ㐈g;oxz2_' G^=$E7\=% 2#^=܀0!Z w{4+/9-:TwTLm Ii:DWf;_JzͯճNJ-3ˁ֡_\Kif,^!. CԌI{ `2! 7V9)CEg9&ihp ,k;]nLmx Bf|tXAV4;G沝>2YX+SI-b,j"[k|U2%Gey+ 6g sa"yݝ-E&Qa1_D9ָ~6CFk.w#YO)ZT/^vX'&h@@WjS{u].wN4ExU{xC)UEBf[<'38ֽivtԌ'4bh@ݎ󵱦/p.XrA58Iss,-|˲zgRLn-dAl'fF$b Mef&V)oJS\-!*V>F;z{zӼj쀋B M3 s A_ݭ l! Mr*:}'$WH oԠ D(D>a=1RR;h_hf԰oPLgKc,>ܳV DËx"~tӢ96;A18$"qZDqBGh~|y~Z x٘i~^i(1g+,ohZGO-P_u)|к꫶MiQA@fAHZJפ]}gueT#7lWt&:zꡜ53l1&JazDs}lGhmN*|?|'ʬ907ɽ=hVㇷ;oQ4:A<iviGEx2beh=>9:J~f3k_옮GXV|YRq5 Y>z0̹8;߭׋{L+~~wug\㕍̹~gFnYOaP6(uj+k~reFuM4<֡2BHWRn <Lv܂ ~!n|kr.ORpHVA_OONjLB >-見ћ8nXkDuGڋKb8d_y97*PJmWtIL5cP˅Lx=hP*:Dx Oq:J?H sq+BuC{o8Frs#bkdk1x_;1}Poɓ/^U'x ^J \.'肌5li^^VnoAQv%s,bu۵ެkWN \7oX_ |{¾2H@2BnlydI)E)ht&Ӎȭ'RfIw l֓PBA~%,ߑ^2d\j}9X^B(|\X-֫#%,Fs+WEꃡME5I"Q|k%·h©Y̑f;4%Tz%NW_..Ku\KWo!%aW;@QBp^ $Ծ= ]YIUUYT-ђ#BC3{j(f=5{gJAkEZpi*59eEGLnΆYc9M&@H؄0roZ0 }PݗtY@1s@D.}F[T^HBPT 2S)V)]kDU1p3h&xOB]/>[ z}OO1 žumaM|+aCQ 1,;NTtRi]fU 8ģ Yf6p(@f"0bZvzܲ,nz_|OXVi̬#h%oK ƍSwׯN_D\GTR"o+Nꁊ.MFҊ sJ88(Jݒd҅뵼4#t0^^fDCeG!^ 4s3dJ@^lQ,2lP`zW#/zŦ,o~-CpфEqraxG???|W$C˕4=߿y^)B6hߛrlH21c._XNOe |r..F\x~ҕQ򊪾OQ+NrYqsǻ@ 0uCN]6>ZiQ|qVVҌbÚqg2=I~~~/:}V|mg. @.(~v"'5JEjO~DÛrZ./8+ ppS#q#<FY#vpdI*YxZTR?^42sQ1W(8mg yh~ul )edNk*Ա?dtDT'w^?V@II4ʉ&l\,QE ".G^`u8/B6!HN7H_3;d(ްuփ/:Tŏt aRL䩘,#:&l#R3 Q; t :V@33RmˑFI8{̕n_(=KB5Q/^IfZfQDsI;sÑ{׿xzR=S[y?`*7N~vY%&WC!.; ҂NicFij.Ag5żN,֬"0%o592Qh{ %gvFyjӂJ`:mcjEb`9*0HґK ê2fF]>}Bͣgx*Dp`MaS]w$`T qBYYMZn;>Q Ak\e*sTEMP ++*0ܑKpKT]ϲ/2Wet́tIP* u|d$6& ȭZީgdcJjf^m6̦280ݻg??˯cXu U A}oTQ9WU;ŤE9QQLчme>yP!5_33;|donͳҷybvv=.:tG-yG#ju>)߬#́9215iuLZeY:9Gآ . [ü幆5wshwPZ®~Aqsf&x7i6agQR||¶vZ.g7:&G>&I^i(4ɳyRYF%~CA:aբ?my-m̰=P\E;#].ho1tpC<4yuWXSWEnW^F~d10+ y ԰_O대xJsTݛ:;y1:ff'Κ|f K4{AJ]bxAJJ4|/_և#|cPE,+t_iEXI8D) Q^$D}n9ݨ޽rNA]5Y>b9 V"ޤox_.HւߐͽvY%"h4Jo<˂K#| ȫ苃+('}P=Al#NE/rT+:\ Xٶ!H%OKty6W%#dg\`-1H! _YF>BA x9P%5EfCIEsP|aBk.^"jԬEhՏlqmQ!BK<ϧ.Vd'* sCCYg0192F"w·)a ff#B4ԚxPĩQ5X@2^VK JH)nQ&ѣGT:4{?'B.Eg+qY_߾ӟ:yN "$حGp^>[AzaZ>yiU|J=I6'[_o-P.ZR#U: \D ֈ)VN ZXg svɪO@"tY?/j:ӛe:G7U2|)@ʏ~  Bݺs C0WNo/q"[++me(:~ Zu_#CZ*#Mc?~.31(+gw"ZӽDϘ_Q0  a֟#CFXw"gN2_ kH1Jk'^8hLvtt>A; 6w$iүyi3QQ̽GwWH.I]&W!ˉyRYXTP7ۨ}!K܂]3{SJi+V["ʰV͛l8 mL˧WիzAm?de4BK *̩`&"$*. Z{qfc,lҲNX <{\R,PBуCg tpV5[d+`ETT.c#)0\ RLWdum'ͩAiy 0<"А=fRc~ᗹ;1뺗蝕iw%"ύ$:r./&.qAd-!Q$ad}gJxǠfXSPt$+rP֠іH0`z|;tȷ0IR cxoh.쫙qKnT;`ݏ?׿'W_aY'9ھT@oEӺSjp+3r]U8AE !ʕ-CY?%J4WjN)G2ZGH[\B:}_h K2y*>􌶎,a#),߿WϟVJD* ^oy v9 : 45:w4<:pj5_އ-grl|5D,.ڱ!I+zco2AmfG$ddVsnvi2co=bȫG2O>̃1ә| IFd(Td> Nv)RRZ~S%zJJKUe}uzAsq4O,d6s#i0y9K{96wh/O..nܸ@(mR?;t+,#fmx ~慦z#Q y]֛x2K]2a9$^v9k] J:23UzIkI#h [S)/t}Ð.k݌ dGs;0#YQ`8އ4g*dҹ_'CJ fΩu8Q!SMYgA{D(ABub'0Z{"M7yF% ^%1A>4my Q&`^- =+rUӇjYk3L 4n-k ׻ɖKl}^-PX;Lw]pjJ?҅dt=keiǃF2W(I;kb}2 V; ƴv]~,:YϾ9#oXO)6x+rIvdzt D*Xk45B|.2:flF?U3lưD>yqO+x ͂s%C,ׄLaX6]z%vWBZGzH53x1(\:Z`M̲)Zܽq^oYۻ҂y۶)gAX\'\Փ~>?;%,A*r9(~l=0 ڸ/$0,4~OY0M՟qR8-/"lzYdzw z<BP-H9&i(73GE ɩ2 G"9 M<wם7;xVƔda'1F3ˡ3OE6ЉPØFp 3XbJ.'|\Oҗ Py'7LXiY,{]h\i2FWD[Tf^7yipvk7.]' r9lP?UC`LXu×G uҎ櫆ygW؏fP $] ~6 FVtOI@Ån/.O_(/(Yua3 <l+ &ڠ>h*)'|R} _PGL wbaR&ǵ'nWofy 7#Qf`ә~gvRvt4iwF>=5 5qMdkxa -[^88 7Qs.AD<|(W >Dv/yqQYyXe]̰3h:Z{Cr.Y b =~J{pK]3Ș EڃF))bjhLwRM&Y@@h8u/upJeG:j -0,b<Ъ ]4fSqΛY`o%S**D:%A"\2+=ƢZe4rL5J d"e-#Fl:ؕTX&Tb np Mz!jnn7#5dnaCFS_8x&sHFAcB2o -hEVlvIY}$<8Y(Xz, 9Y:4ǘ&m !\zeXM7f­dai2,DvV Zkvzg/jI1n#m^]7Y8Uq$|B=q]?E_NY˩^ce j`P6w'څxޒBd/$ѰBqcnxJFSԉ3d;=Xо& ҬG[a #o8y}z3hDr|ޝ͈@|+k /vFN/$G]^U7cnAd*5VyUxoKr%I&8{D.USwf~4PM4DMCY{X6|sX\իjj`P "зl# rm$.'0Y@E"  8H|C]-5SiL<-;NY^cflTfI %%Pd>fE& bG5aA}ZWdf./:YHQv15]g\g}f2FȅDV5kM`1T]4HB}{u ~RXp95&lM(`@RΑMIr [X U&VF`h5N3Xi"cgV)EQK+iUB";**MBe{ҳgyv'<|ۃZ:YAIYBTNI/Q~ZK ($3UV#jQ=_IcHkc#0oo߾y7h䶫X+-!ew ѣٳ:],dt+ E,3nwgQmðҋAQ6u']% Z~Q,46=6J4&b+%Af\ V 7Ȑ} fAsr9w^#~/]&*R ,09F|4j TH1xx  #7'ER7F3..$\!.LͧMӋ?F2ؔ3GlHG,kE[s3Agw4X]$6o#WWM-=p:]nk Zi}0sʭC<^ɿ/?VΈ7-κD6Faswa6#jcpO|aіDF>GG"+^ЯLZDT]eӪ6Ro^WYX0b/ʨ|o$78"HAי[ a\<nc4{ /QE=Kf?sz_/^xFTt`g,@Ob{+PeՈ&.u bV|iLr#E_j+5 Vˆʴ=)L;!&uAO@+;I5/ބHd =ꛚ{dy r `ԖehA@uG/d#ggqڀXV[00^꬐SKrڍ{o3>_tF`dAyB9V;~ W香9H6Q94ӌFdbܿ·Ed;eNRB3u17=%烕`CPKbQ~'t1B +\!C@-aevcKiWQd SHƧz3g ~ߪK@JX)( 4.]XR[9#Bgl֨Zes0!7Aqg M,ڬ 78*[T=تD3osrL,FQDZ qr_Jf9&hb]w\Iq]?1|1S+A0M>kʮ+JЪ8Dr"%x[7>?:AJFȳk?$7,Q8W7 k/~r4xe_Qy>.:[ƑN1a @lM & !KW>Nں;ZB+*ifƼ5ٌG%w_4AaلAGV# 4FS66P2}%,Mr~rv *Ѡ}-N/rw}WUL^*e/Pg>F3Sitҽ<[Q]pyʍC!?4 5[.Ejؼ%׀4ݦ&-NwIDAT*%2G7/Y|O) ԜpnO=γ,6tQ^>/_mIVBHAS]ImyjHmbVvMh|D=AIjpW&& lyffkPQ8qD_klRg=A> q7cÎ[mk)5Cӹ?L}dYd=B*S1Fa;*\=̰,I4Ȁ{9dkj <%ۿ5 i\M0(!}!\3+ ѱpYc$0E&t R ^=\jaq*{֪z*T"X3,HCf\e &3ƥ)7:ǹkߩ]IE]Vn>Gs¾XF;7sk%.RYM{P(UHRvU&jhyti^Fr JW,Оt6#tA.H( ϸUOέ7?oa|,~(ڬ 12Ͷ&Qk舨 NO_|o_qWZ$Z%KJ*"`(qzPvPuվRFڠӓǧ/^.W zCGaBe4JgH(Ǽ!{.OE1v1 U< b3 TTM=#Fс(vmУGWݸv yvMaif~7r`h ^֝ӯM{@1Xpb j=n=}*qkJ ;f7GE:;Hn$G}> ߙm<;HZce&nL~Fᦡͯ*cPvEC-4z3x/1 <3 }EG,}뼾k0 M&6g"'_hX 7ÄMaLZ3NAd.Z O_ןeRa9av=:店BrhPj'd~6¨ٶDضOQnE;q&L._'f!Ca`T(7*uO;JUW<`;%0O\C Hh%4mW>Pf/GubKk4o8NDY>]&0 ?MG!{Ov:Zb)CX&E ?hJәYdotF3#asa- HeEi#O#)NaA(OuM2 嘯^6WȂՀwtMd5{79!>4%ΐ8! صۿ|3W^DKOYbXZӼqm\>Ң*V@! 1 +|#hJ֝YxggN׵-Cb4PΪơU!W_+E^pU}=&&hqz~) kyp48GU%khW:-QfEuC/ZqFP죌2պhX}Mz A˵ ʚ1"IOvRI#ױZ`WL J h!UVŵ형d-R904y珫ų*~Z.fcGBe^ķ A7m*(~Z/<XP$#'CT؉XR5w RriDz7nܼYgN\\ag]@;yߚ?V~/h:Q-4jx_"Hђ@d %w?M^=<VcQtQo^WYx \z|n[kM5 Dhx6CO݊)?}?E`ef t8 Z8ԕx9ekq{ly5`j % ,k|l%7ۅ)QPebؗ)\f_W+O5nd]5 dRyUp r:?rOc% MatZi=25Es2rM&L'+e~{oJ4Tsw2E9Ϩ ;IL\(Ze1/V::<~;:E{N8`:e$ AgƟ%՛F77l7P_VRp Hr 3: ylI  E KvS!if5e: 9qHX'",C8H᫏@ 4Kt'{1LᣋY@sC \#p ɉPN|wxr/O?i#Ҳ+0PҝGZ p߿Ajd8AA@e n 1x )J;T)W)橲zjH:a@VPJ;&kݘHCI0eNj $yDo,k.W^F e-4 hB?AVxroV@ĤXBgrd+bN@oA3nV([H"ae ^̊_<ϟ7m@vkiPqK r҈Nqx\z3ɌKmcXguF^cT8Jr ' Vޝ۷?O?N872~B8ƨsj^CL:˯27lUZP,R*'ia!󖽂T`S,3‰& kǧ={"/y ;r LsS|]<.SD)ZMҙn0Hpst:VAY;WݺY2TƥׯOk]̻Z:?$2Os ϳG5٨{,$ X5D|x nur͍WOBcduʌv84n-~\ :ZjzVk46B` ۅK+֑խ-ͮC Z0?c8Z8ƝjK6QK櫛w feiFMܡK$kԨ8}3=͂cxmw B F~ΝQX=Èv >>=!6,;`8q!HZb+G3\kۯt8m3 }WD(/!>a9sDuZQӉ P?tfi*[G kfLʫ*H944eͼZ!^ׯifWԞzf^SN!eDcI]Ӹg da5'x|Q_L/>hFMϴfd`2Kǜ3IMk6|vA)gF$_$ÕufPN90lnOٝ2>` V l]Wy!3Z=H]\VQ7g;A֛Z)0"eq8 JK|^ZB*P!Ǐ>>9yq?yz^xTQ=ZzS P jofdH ,""Ah5ZN sLƾN{HϩmHU ,3G*T2fPWQݻ7oެ-cH*wb̊?kb7H: ;ÇOa)h@*<,RЎs%Sn5Ƶz`BA&ƠR%`lX_:"'lDz0!>Zmn޿rfP`C&$}F_nM c- 6rq͛uD]&tBp;@.>J;W<\ K* Wvn!6kn yم1KeƂvbvne<:Fg>{u\όwPf q޷29oAQm=sȠ/Ark+V*fd-myB61Rg4>FM>aoa]ŒO\S~~v_k!fCZL#Q <~arE* `a3yexcn~,íxitfu^ԭ.VFwphLz݅]>zEՋ渉=k^=b .~zvDyo1z@|klb0c8B= 6ɏ1wfUS+_hLV^ n;>?k+}vBSk|P=!Dt죌к|ZYu\!e6qcx'ɩ%4,Rm_~[*yu1*QOwh jšg.YvIK`ܪ^Y7E,j:EM`:іgiʢR =Mx܃\@[8ոeK*^b%πG*A61!r{;7 g„l0b +̍5b/Fx`7S!d#: P؉U OJU@%@f1P(m ZvD*q)_V:2@6s u:Ҏ5KNGPIubZrjA9k=< I$$ĽV>%U Q~%˸(dkU/NzKު@۟|WEC . fdUS:9QEßUՇh2Ia&(.5y{Tà =p*  6A'ʾ_mmyRAnRʔb.I~PXnvU?fHC\O>R#Pgl2Uw8/^׵ڞd]uƇ͹a,No([_?5'$/s_=$a\ozOjc=p~ j4LM]C h{|"mX`E3?Woi=>`iwѝG~M? >;f돍^ y`~`>hc!MAd'Ż8˭:M[97y= x☁}>r;B5G잎?-xh+5ZqӬ~[i@4t@C7.ɳ^(_3i`JTיڄfۺGk'kW.9FSq3|u9 \`$w#Y6c&Q!.!,8,Z]?p9r dK[ {: Ēp.h:{]CX.d}"lV$]秼jv_5 \)wԅҡbY, v@[+?8Qn=}>UN\Q݋q ,'g=61|MHnRhZ˞C;:jQk{v!<|/`2~0h"P/W1 dE%}\l͙~6ffF+IB!R,Mvb iZ݊숶$\4IT*d1C# w IK N}G>>?4nA*%#Q^T;<(ɮ\]^*$ c(LB a`+Ҹ#h֔NR̭0]"LmjEk/߾u[7_*T]oW8faAm10f/^?9y^=xYsRnDdjq5e Y*.PR7$ЯmOQ3.d]N,d.( *;HweqcpbӶ+\m>7ӆ@} ` gSEVceeCg\l}} O@u+Y}+緢x%%ɷ6f.ӡH;Aӡ+H[iGgGؘkwށ&@E˧N \-[-ZL5lO0#d!Y9TCefOL'}=-꼜eQfqR` 蟜Dhzqp#$;9H3gN#b,6==ӈApGaatj- Cк<`4fb;DunEL tQABTET2̄gpR ld29 sxXUp}teDX["Qk\/!E+moi:O7if\tnT(ʢN%3|Z$(ě9KIHbpp8Vv''fi3KFŘ^!KT)VҨTSF ʢ'g/HlOi(o"u6XC*r*}Yrq +: *# gVií;7j]̍t)(Yqh"~_*{7ƻhqdɛkP=j+0V,EfXSZ B3hVÝSHX%m͹[dĀ&8~+ J5?@qV*˰KVƽP ZrRFwzm~|հwm4X>zxZ 콷w)v Z@Tk6n j_CEf6TKe65ak׋ UY5]a fmP)gT wVk Qxf.3 xLە&ER NUcQPwEK:v !t]uџ+,`U+:&hڣI"AWGEp˜wLYA*#Y Yưh5ӳҲp347BV@TҨWc<:2L[ J |x dK`y/ki0uBMa CyeHa:c1 ף>Qt|RmZ/Jmr]%g݁>,Eѡz=xXQ;1 0 p[:o{]'ON?y㇏=|XIˢ>{vR/kԎR, @>ኲ V;6h*>juA!=pV B jzQ\}B[3T({ǂv좰^]Ka8guwI>8 w$ڄ2eܮXvU4hzSK;3JQ, ]74hWk^|$] k3 rA ZFs;fYGLr`hM!]Yj ۖsY|M'sDr[-ica‰#)fG4_dI1-cUjYkׯwސY( #tg:r]FP K 3 mDt~8}6}4c@a=< &< ;h0#>~o|enlې$x!2)Vj2yF/ J¸\bt H[F)c-=WΣ# - eLHX͔3oN;Cߟ'>l;<@E$jo}>9"Ki8mFs-SN1Ԛ^voX uVɘ>~M*Qql:Wh2uvpb+ʡ䃙%r)3h#S;2fMQve3ڃ9H>/>'e܁Pjp w/yb8*u: ׁ0K$!fg^U;-БLcZw)Վ iBbfm- A7Sk@є|&p3Ibv)Dcó5A.^݀`iS%eeߴmBlH~ށ ;f.ܠ Ӱj+Ҩ^or~4BN(zku3^x]sQEedPk^pҗE#S,p E'fN(E(*8Z݊t*Cmc9*j AZxRk;!Nk$m[խ޹{C+'nh3HiW-`kha`UEhX."l%h#_~bֈi zq O jZ›(ڕQu83Cjr 4kBJ8boaAriv˗!nvk]NQf҉ 2DhnLʶb/T0"ɥ[n΋;n~Mϱ7{;wi57t'ms݅ڝn.>MˤDÏ>㢱?}wYK[oEc(lHK;{q~Y>͇tDúC p`%hkt}V;q6 =ɚxO<~*5XXh].-`/JH%-΋Z }* _+:z{PrZUR@̲%l#ђoOXJ6ڈ%1ike4Ę@*3~dl/W'dzbYo y=6rk\s MThȖ Dv~{+ׂ7߸yC9ڟv~Bwn\^8:O{P)y-u-)B}584z2vBq&3̚LoN[K1O=W; * k;.Mb"35 "K~›}*x!&mHC:ǃ-2 K3+*2c9%i \KbG0A![[wC Vs ,HRhȑ BzHEO#QB#1TDpdOP/%c wD7ceQ Fz;E&؁Kv(#I}Y`6퓎 ̌! q(l'CA{<Θ e+`-# hot'Ьd~Rk/<=y&:%VDxV /]@WSXHxC4}) PhjQ$4MY< _ wp>DL;PzU{e*p;oݻw>{N];>ѷrݻ{ww:j-pPX"`"p3F3p8aެ 0>k=KL&खQwr. dmrӘu6]kv+XCmʩ:0hXm }<.qn0%jQa vY^v\5FKrΕ\*ΨIX SGt|2wݗ4D88&|%:z0_-3x3,Ԋ h{]U|y 3%9u6q~oZ+tamE׍DC|&Dl!0ݙa[z+hcmjֲ0W:1aũs~WL#1H&–a`!=`Cfcb@C}pǷ8jm_BΚ!bb0Jx*G4סͺH p&TiW\l:r9LX`=9vvISdO@G]pZVu,s #8AYxht}QGC/ V̍ɡ8#;2wo_J"mZ7UMO ȤpXRViq-H>{Ym]vYaB:mwild]g!;TwzʝkvLU *Z|/v ]5TDn.9`+([xp-8B*Eî{V0 1&t C7^9-,2#鏱 U:$gFA(ME[ ̞© 8eࠎ= śƨv'ণ 1qѠpupЙ ) ![/W"5ޞ0Nm3ЩA E= !v Wp[sU$g!=RgIL?YC J,38Ax>}8uhXaҎ55;rL]z O&õ7q>۝ȝqJrNk-D,Y-Gjϭڛܹbۼ9VxuqQoqZhhHJ(^8`jnmVb׷4}o@bUa2[2N]T 'Og_ltc{Z&GXxA[HZcowPܖam6=dkoFnYK+P#D֦lJՎʞ1"'I0)Wũ$* Ҋ^T.q狗*Oٱj:r NXs/V9+P^okBhnL9ej}jھ|5X+WF-)vhK"?Șn@`M]k@9?ۿ?RN ^(LQ,tJZ[@fR y´/q#Ihuًj(d.$){*> Dީ$BmuViUpmYԶ\+t܆A-/W=l qa@Z8r.Y g⋾uO;֞gNѷqռE+R~*rdp6F`C(0)VЅ$\z; @Pfշ[ڡ$T#i33p.pmHCe0/T th+bAtMӐDadKbX& &FHiVLJjQLWV90Jg$v^uTY1d{Uw*׉Cb\s>LTI8VZIP(4W)S Ԇfcsfj$N>{oO?7MNwfHg luh6៓ U e~BE|G"NUUGoϔh W/ItS6<㦬DX:mIvQN)+W׹Z6h!笙AuKʩ+7_;޸~RԤ/75e\>S -e!S)R^{ܩ!xw3:z@Q-ԎTE]oP/_;F99&I髯/pFJ{x-odfx]7JF9Uq]TRU+L TquNTDM\V)Tjh@n7_44`Gxe$Ib]q8N$iohUJYs >ҧ!HIJ[Pz+y!rD.J j7Z"^t+fS &`N: B&QrKDPKT #'.} Rp4F2AtzJֳWr]`NDDL1'&ⴄ]ym aD yP!f_F͜ˆdZ̙ls8QI\֗A4BcU8!/bLQmtH:ZTB`kb|$M2('ي0؋}f[[rj*]ikתlJ~k:XK !&@ݕ|A_A_T] ̃|̞)kے*'_|k1`9RoB bs]X3p46ɪ6:DEWҫRFeo޼{Oг)3d^x5a #fZ —wfۃlcvAیfnT'`B.¿sttlmm[VoZ7{<(MmZN4 A&Xӫ[>W16#g780oĶ/6$Rh^UF1 1<.:?D ΗcM4[[j1`{Uc2(_|cU4\c$ s*F(&7fz/L"kWHHNC(Q^(ebN(>ʚ",T{ $,@wNUdo28qݶ30"DdM"F9>rY =E߇@J)֤{0ɴ4 D"5jIr b0\tHC)y/Gw|􂧥G ($KTpހy +t2f8q੨.:W|CY]9ίZ42 eBNkLJBOW[IQ%-G )-.@v_攁, Et8u76zBډ@){] `٬$'.j#Eh@Wed.J^Үo\[7*lGW[:RoFmqnȚ=>(7NBa5Rd~zUVF6A` j}jW,ϟק_}3 XU4R>Bs)牦yGWh[#mIN*tfTny'3~F1246NL\k_FijN_RȢMBW)OP *uL@>!SdI'[a4|Uro?hء ]__. VON_GG8v-8_36;~.HˇSVmq3|Q ̟~ŽbQ3354WqhmQ6e%s6uzQì; V1CfëMxyml`vU{gN ,MN#:^<<ƙ&e1G;Xq2£2do48S$l2S?oO*k%0 h`d`K|o嘲>3㺨erꤓ-K':r>Y QC-nݫIv9=g2nsI$A䠹^>|_=aq:c_]"WuϝDžָ 6Ѡka(WJ4WW&X9hW2Xnad.rh SjK&(㒒4PlP.Ak\.dGs.y>BjYqJ%Hk-G[3 XHVp"H8y̹Q,Q9'&:qg'gvڼJ󊡩[KXz8P9@Ʊc69EaΣqtaJ0Jx4̚O8L8- #Niв;Ï˞}`~BhԻsB]sC>3n@+4F;~ Ռ6(I0, yb!R ޔSN*d/\LلUc ġTdqj@H$zBg ; @/BBt&i#>}%1 q #5&Ve_tˊaIcQT:SBwM&hTTXeqgu$+ ^371/j>5C}XuMBnњsw}N+*KϘbȂ@5pg WI$O0E[3Hh8R]vQ%~6#"P" µ+Ҿ;ړ@Po,RYxqr*mWpDt`?dE,FsB}}o~_/(f%uUp%Y!k' =B\^؅#ܲdÄߡƨh8}r:DotDF12+"v}Sp+S!oՀNQ͊oE12|J30 kMʈ"r5t[w޻w>$Di^5l\jzVSh‰YFp>xHyDkLfwC}v4i36])#'k6=|<3ZTPѥ< c jP`sՔwftU⧚P%ؾJ΀uڊ"ZHk+Uεr@:NFwn3!v,PA旦|[MjC=ZPpS _~W5ڱlBiC$T;Ri/V@'^:Q.kx* ٢;忪^$[=! ȁ3F 8D!͉7'"iF(LD =5o7(l1Q۽"%򟽺־DXKBSNHD~]D|JپxsO@&u`%cvqeL[`5WV}!/f"𸡾cnM ErX+ԌZʠzaeBP6IPl`Wqn z!2: sZbVmL96)b\y#9j 3&6"=n1Ck/gfuƶYL,DfNˠ]`G yE֯g+942$*s@cr)ctТT$ԪggOx |̦nr4ex-4` 5R%yDNӂ k 8DŽ|z"ɜӌ_\%$m<u6Y`#$Ly2%\TB-#d$iaTB MI֪ˑf 7ȒŲ/?}vhBX(KDZèq }_}]m)AB91Kkj-MM#JsHj15x$%LSjZGԫ''OKeT f.XgS" M}Il:זYP"NqΊ`8D3V: ˲;KdeW=_~{gb3tf Ajznڪc| jx'aMh6E⻷2& -|~ ~/=ڝw[}LޅDR edN,5kܣoq J4<l'ִ$ށtg Lpɡ,-vʘlXjz2CibA׍~wg]=E=+#  oSxzHT}tm{3kQꯏO˿h?4}d.^h=)qL5#B;! ʝ12*PMWEkš$[m_jZ*v֪eK- ㏶"d{$js+ >U)+H\(3Y핋xI#,{ֲ(l7>= 3y8OOc҆z6aOmfМr*"*N,7GxVBKGPXe :t]"|9r8)&uQkμ@|1rA"B. ~}|gU9aKsn3qG蠡k}EM6{"<靳ԝvAd'voLg%4-D$J,&18PyN8S7EGªѯ1BO {7?߿\^tl 4N)*~-񴂔SD_oGc)yG'ϙu`I-] ZYfAySS,#^ឍNdž) U!)9\0tՌ=1S_fjJzi&P u{wݿ{ݻ7ٽ>SGCH"υ޺<غkY&oXoz >{mwm=2Li7c`}56n6:.,DѿYtF y{l6A= 3]V2;A?N4~9~BSk(k%v.fdڛ@eLBb՚QG6 z;>$rH⡢5wwAXXMi:\Mw>[jcnj( VYPČr_OϿ/u[d2Pze5G?_;d;BAx- .6J=TZ9Wh~WB+|kyQuVWj!X[U\*W+ ~VxG{!.޸"$Rh-vߊ)qYQفa0vxOϿpeZ@UWjMa5ys_2+JshIsf8@ѭh8[|`6/pq45t|si8&x }HvSG,>l4&LٹܲDS+0F,3s/^w'֕2_3$@H/.'oEfTD[,s-OȆ+q/" R:e"~e^U~ijȑ>9SDa)aeZyeЖ>/`T'!P {g,y쐵+nkّ!^SPi RIR4޻T Z!R1i*ѠrŷRblª"dL^.%#!.JjvѽQ;j?n 'ypd b&GQ'_&nKcghA-|Bj_O=vX _Npԝ9\XG 0L"Ѡ5 d!"nJwF2@JQ'Ȕ7H2 ;.nݠ dF^w= y gjSl 1c)p{S4VI?93Z3lI4YMVQskM8Q@E N:=*(8ުlVT]}JnU^+ <\Rj,SԈ/9J&*7C _@; 76 1 /FL^YT(ӓgN^~^6+pFg|l[Qj4qzz  UMQE"}L1{.w,ܴZM6RIsUgO+pzRj͊LEqGgނk[xEb)x/֤Hn v!$n@7i[1daA>dH939Nbw߻'~9`E`v~"z䧵YYmjQzcvFB3x ZFq-C„N|^__TC- zfĂ.&Re\" E9 $:XHwFݠ=Ohx嘗!zk K/ih{{h5G[/?遑xiMCߊVMX>-3mîȣ=~wzЃO*ajluV-Uq Zy_~e"_j'3^ tQ wJwM)32r0E[oI4lWjo9HUYNGTEëZfkaG Sl.>Jh/3O3eAr(2iNT\`Y•R["Kk5li {DCU4<;}VfK1 =!' +Q'Iܾq?O{~7E|&= լ  @Ք˅"=lx wMg vůKBaQ\8Ε7q^(Ե16 i tQrW vԆJC㝻2lWxŐ&"7㤆L' ZsCLN `*.k+$1j)`p, ujrbjI4#$¥ jkHjcDB3,B%LW`#ڳbspoY1 =rtcQ!Jf:Gr,i+XșMuc;wv!w$GDz>|?oԩFvSfؼ- 4Y=#ЀTyk/ #с>8 h%\ɳeӪNe}*kJ}. ;*֔q#A뱉j+;֑Ka]WOM.s&s +XyYB3h֦wxc{;6>/oh4ϧ&B<+4^ M!ݻwT.ydrck>gE7pqեT7s #l%@+dJIi_ š%Hh?px)S!'h`J(Gf&)AR{Bm]ə<\YexSu4`#聂B ɇ -Pc#v\CjUVj~a >?AeىA0 zs(vXd9(R_c&?srCVԀ/1e+m +Pr GaEpt6/uh#mJ''h> >bJrѕhk祈)%M z Ks$fHp%-ez[*菺Km3eTP ںN"8-.iu6P.G'B<ެV+xh@%|]\b:5L( V=O>JhAQ#0Bӆҝ#` & DȝԞDsO2f}dJi_pjm~+wQX!d+O +1&sn/:0omq6HaQ Xɼ<;}gx1;||}Q0s|UK'b+'F1YҸ4jQ!8c.$ 䍮+D2ҙ.p9CEV#JVRlvk4duG:Z7E[a%?ªc G=W` SaeUZY?|G~uH [}d뫇~4w#ƴm5^ܹa7#!h&mɺsO4!kj~F}Z!S _V!Aw9=-8G;Vk#04 -!yr%_6:1I!PLK屎' hn%;I 8M@Ubfȓ{h[2\M* Hr|HA t0VA5f0za(Q +jx˿ݘ&1a+ z f 7x\TL"Gg̛ -enXKX_A*qz9U]>NִhWV`*'qY:sϬHj3ikrДRxD ڸח.ߛ׵Z.#7KQâ !2KCMD[d"&n?`qhUP?Wi:/xB_ _w^" Iu>9X~iE@:rhfl{b1J,8 pj!j#tn_aCw Y2} ámg9HԢ!6ElEO=^W8YbcZfM:u*;wSy5DHҤLm)QPFдe bt5JwLpz ŸSB0l9.O>۷ko(Rˇzġysu07_EC:εXO`n7z}g2+5\.Α?;0,CYov\`mETb̺el`AF>QRyyj1᜵ TkP`]]pb۰r$ zݞMn ځ4nUk;x~#Q1vwV#3SWQ9t7B٩'Ͼj~}bYw';vh#Ȕr;T ZH:9 >UHܫNT_ 5eS](*Ug~qZQA ZdꁺyӼ}Y07^].ܸԁIOI*gXs L<]{~;թN?h}ΥD#4z-uR̔8'MY{_ի>5,R)kⓜV* ӿ~U[z.[5IXؕ7G̠׋x>vh+Qýڕ@t;3Z\͸7 =TZO ?:VkCt-_17QP %O$Wl: Ä0Dpӆ|%9hc %\֤HL eDp mWLt!Juno['/{?H@KphH23!pH_e2%^O< rAh3@$& X*vЃvw”[ 5 1f6= -%B?nH*  ~@ DE[]`6/:? cF ZK>FH*gkrUJ&[Xf;_2)H?JQ1lDNGZtWb e]Ǝ`2j،]uue-ܑ-L<:9&&.B-&^FXMJm̆v1S݂`6zf$v 2~1OX(yO>vRbaoz*şN|Ou Mb67$+Y@cδp;1i_bo9@YbWzWՓeFdBeB>yS„8W<)XU^$18++ Um KZba#+jk)~\BʄS{@nֆYnI 5U6iF?eT|\F"1?)ES}g `Á-^[{`[,ްûiF[8ZmbfY[Bkim3WcM\绛2ꘂZpZ5"Dۯy+͊\,⭦ m%6}_홓VԷ yhE%X ڱz;- .n@~sI #b eȃ d.,đ5A!=>:Wg//Uč/%oTQ;J*4|{P eRP dVe䀞__n u2Z̼,jh(gYXSqbNGuRE캭C'U'g;ԭW|$.2MT:M]ji-`[TC_ܜ]b5vTm|Y.<Ͽ=껟hu򂵭Z!/QCʌ}*|n;w?g֚iB t=x94!" md´&f\ư"<ؾ}84LUX該 Cz9@!3]iw>2)~-D9l'!ay й7b\s0a:_Mgvʽ#wIq4]Bm:ُޔjN2o̤\E*AX(8M3H &zeRîJ*+!eW`*%A`\/!)&%9*j} bN<}.<~"V@p把P:XqI6\r#ń!K|[i΂~2uR4g 3L/*TGI"dFcAƚ򵲙Y2`(#^EWk#GIYGE(PoGb%[`#BLh $P}_QH0k&r$/pΝSA/ʡ%#Yat٘LOJ oeH8L`nThDdHPD=l MbzˆJHos1RSϬe"vd9j= C,_7AUc(p7zfϡ#?b;=<~:`jfv#QΝPZJ8DϤab-/ECĹnvJb.*~MZ4 ,M}L sVcav.c7bu4UO^7s݁hxhhoCh*\<º-Y B㥫+ɓZU U`[ZVNWvFa:5/Š6fz؛y$+\%9ɡ]7hqB vZIOփQY)2"ӂ*t>DcelUǠLp{RkD)3k۪54re,&jRy˗# E,Y%FK4˻vT|W뜬rHTV#4̔DʚZLȽ2I{@hX$In&r$MEڡjR١7+׊u'}UpUd7[ʸ җӷr_ |g8w>[dB Í},3?[//y#'?XkA20ehjsk}98y8nFvqݜsv}-)* enܶVZ:ԅ+v㾥nv {܇ݦÚkImF רZ!ߊx6k5XQ$7%Λz˸ +5x*҉IŜhK r}m:]Tt=TO!]}ɼD_ǟkZ'J64-W;aU ޽OU6I~! 8Pp_ĀDFf$pJtM% (w^-g8x26bb hfȭ OcY d5И$l*k#2(KjF#'hC'( 13޻fthf Kccx.Рބ,6ѺV;2,@ ӱ_˛u"",-L{t`.qyu:.@yN` xoĠ<(R<gSA`;kN'4DV25ZIcƺ x6Ww) fQ :oIWYե*68JtEg*Xz%[/ݟ^2 chnH"yua#`n4 ) Jx4jlĹ7A̼~h8U#W+#Wkk?XlMho"2I hSȑb]p?آA5=րu |.bbip~xEۏ}9HaeiF4pBT^i%Bvq!<ف0,t03nO4 8d϶zܒ.mqX)0.b| fV&*gBx JA{%R=)RT=.{r:ވD*R6x?l5yFtj:/cڭɔNmūuC@Y4oCI{t7ylViMT/+g!L6]xuޣ!L,g¾ C[}&Uܬcak41 Vhpw?W/EwTmҨkZMB+߹{>??~GUO t<f80K)>aPlhhqc oP`w9qI4Ҁ`mF_Qgqo)LN|oȃd<g:st`[ r$OϕU]װ H0~ZdK'K(w|Ƈ cT?AAJc! \h,~dJJ'J[Efd(''GUK+HMa8 `n50ܨBzzNmc|B^Xm@b>05ͩS.LyEH ~Sf^#AO Jf93~³'U ^ %j޷BGbʊ-)p /" J"lK=[u!A4MО:@Zp֬Sv4+%4DB6RifZ!jeЮXV.WTPJ-TE[7jxet .(e_1Ģ{AwJeUû U>`eȨ[_'f̀?t5e")>yR V!V^A :AGXJI$EdĥWw΀kB{hfoݯՋOO_}}4CdRP\Ib"ޫ1WBF~Şv$W2w9Bgԕk7kAf2 j%y^F`ҨKڥ58>S%dC.k[飽4dD\d`ў:|ThWLb?|O?y׉ݡVY`n.~ Uڹ1~ӮCE1nX,Occ}6{=X?TPh`*<1w퓭i]fgf/ΐ%`t%V&ޭ1htLDx*>nJzchqf-j`icl}"i.*goA7z7ri?j*~==6Z!ؤZ} yk{b{a_ !ڣѺ! <Ar멵N4|GET6{#?|~-"% ziD`H5N-(⬺O]Ak}d2 kAׯ'yr؀ N[oJu"5Pެ"WZqZybfZ y̨ǭ~`α)-L8$xKErϿR>uhAfőҵz׮}'O͛7Y;6l9OM\fg\//¼kJ 8ha}@M1Yи7"O|%nqIJJ:d*YĀψ(UY 2 U9,zUϩ^iTO%kT K{JsG~P K>0 !<.ْ̂D-6K,R(Y TP])UPe,x} ftU(xNYe 1S-V]/wdG|9D<}j< KV ڣ,zP.JbqUZD($mQ%XDCn޸UIȰq" | $u#6{^f]eAn74Vr@6KgA hXK.Ö,=IX*|roV|JwKmV4ҭ TNwl[- Yf#X9o#dV(YQb36|-=t73;}l 4:AGX-f=t7*B[{4 ; _E~W"u~B;>k#Q^[M#m 4}m<ʴSgs3Ef0 C)ݡ&↾Bs.ʷ^kEO?h/tAK|RN4$2M^5N/l$`>U2ȕhx˙x_6 *I.WhdB]$x{k UR:x\:uBG]W3,N!gE9s[l1MH4ӣv~+ζ6id)t|G"=mRnPIx qI;<+$h&bߪ%"^;*3-:FTI$^ۦ} eLH@ ܞ/$NGQLAzčXVg~# UlϢ6C ]c X` D} (a8rE|c@x T8%Bf1ȦgUmjB&4=hkOTBM'VIBC9~3D=]ns%nQcv.\$7;CIKOQ& Skhftub[WgSfБaˡg)Co2()!z9 P9& 7S@Kqf P*;?ꫀN`|:ֿiTt\9T@KiP"9k*C\ UiZ;Y#tw~x^5\̷ [.bdL''g/UpJ1Y%8%uJ4`~kFe|j{"+H&$f]D * yO(NXG(t0@Ex ɼC[rLDMs=^j*YZ;?! /Y-'o]D>_ 7o]ˢn"KTL ֜Njȭ͎649-6b]7~`Ϗ-cc~p=g\1hgiٌ {-6{0V^_p.C=!q~&FȜhhܞ}Gv@#Z.\^>[!f̭zHS? ,X>Nb}fn#MB~-%<ż=zs"%y (J4|YV\bsc$qx] ;vryK]]$BpخՍJ*Pk/\ri9P*e+pƵZ+Ԁ"9]B͂hN5a) F9I{ıF[ڴjyV2*4?һtX~Zr]dGb/˯Vq!Ql5+s+yO>kđ-z+f@vP /wquGTQ0D*ZRj[pvjչ (urAb`Wf8׉]M# izYrd@Ҋ2$F@-Gi;L5G<~ΓƏP!v.l> s(%X!dwG\9AxEW9J qU^ɝ)1:2 *ήDӁ5 svC~,H( !`)0T%ancoй" ax˲4oJRxɔ5O<~X'G9yܭёfKCk=LSxM8\ fWGU<G!AOs"4JK3"!HM ef7)A7]QGFԉ fV`! [f9Gд늗F1.hª*7w=I)0 $Z( nd}6_lT)_<"ko,Yo p +`` R%%om2IQE`֩ Z%+U2ltX#+p,0Z(tZ9 ,wS4\6I~?;ѹsfbq18r \NJm֠DȰ&Βg?$ t%rhxUnܪ#6yZ:CLHRZ~*0T(J4`-nJXM>$N[&K4kFgk7h4 ~y645nt5t+|n !bTwڙ[-[^L QL0f^Yˎ;6{1~M]3E6! Ѻ Xl32crk84$ROv]K,( Qg:fQGJK})fM% Iz>ash Rnں'$E=G>@~$1߈B*R/tF+4sn@9IOWB)'E8.#mb^# =Ëdb( 9-`^I ?;=}GTCП)\_j{{( V5 [_?VC>l,2hBnmPҀF!$cn<v7'ЦL<'|fkσ)P >o .Omtx!=(;W`9OJ=`zb2 ۣ < 9U$"\D))R{C_mkIl\$kܣgL`L?k_mDßDC:'VpMZ&s޿wZ" #*OG P"1CAw*h!PN!p7p !oKrtTAhu+65RWn2I(7| -%`p7Ȧ&[S3[ I x4fQWKDOyD5kBNw|YC!3kgEzOѩ% ]&i,(RiLȋPMCv䓊"w=Ӗ"FL9✈TS ab !q!#fq1=Zف*2"os;[]@&eף\`Ӭ0M9xǨ6}6{Ƀi\a%v^4L~ZW6K,Fֹ &-V7J75 NBAIݣqUD {L8CYT Hb@] G3Q:6+E OY C;6QVl#HXuhI~__ P 5ƢZE,ի7.]Z5<~YŮynKg k{Km鳗_~QtILPY!:J' f h@B-v)$ZONOʻqV@,h-LqE&` .i/`AQUZ`U#Cʛh·DCHj8X+ $u ؤp>=jäb΂[r).OS29ƣjYPMlHH 6-5@b;TΠ>{YɍߘT2m-nAީ %]eptq0@Uձ]MRi @h9N N.r8ւG|{#XAPg}/_}}:1Nq(yP8в:>:\'T?N¼#j\(,eۧ7ƺLCS@ʭ\wh 2%,u8HzWȕPOϞWt-ikDri9:qa Fߡt3fW![x hvЧq\ CP"sB ! ?,*!tznP"W;46RIp0 xkfZe  @7CQ?> ҋ@ѳHlJ>9QK2є~ػ7՟%kK X=cSw丿j1Z*dxXɆZUFrf!#A\*AsO ` m8]bT#KElNkXDL5Q)fB!Je=up갢˵$z%Yؔ}zc'$Y=SǴ8bYXГћP;|:+1Yx;P_L@b3^VeJ*id~^FkTČI&ELWLO}a)[d@| фQ\pIYJ-FP:euӵ[O??{t+Wq%J<6 P}zo~߾9=ŷ9 V'i%WpG̋mBM^:^5j''O+SW[UPFhT6i`\ 4`~ e-_/KIxk`5RK 85i!O .G*r'0 ni Gx3` ?˯mj/z@$^97tt2jK߽w?~ɝ;w s7I1t޲]5hGfnEޔ_$.֔)CxtFF1A2W/Q-۔R+7~PaD\:T/qkx֡^$YXChVe$yLXTdbn+d_"' q̌cG0%5p]TjE*ѠzO}^*% Ljqxͫm>5d<7>3?Om A|"-= =P-cM9I7Mm{ۍr GS%Zk$_?`kǙqÉv  NK'7bzuWt{GX E>Pn&9"<7Sl;7#D1wDCL'THMڹҰ[/8rn?5FiRdQH4~Ãɩ:sݚ kAB6VY22yuZ>#G` q9lb1$=$P"rHkJ äIeCM4TL>TSLdʠ`,L^r=Hq(GؒgDwaY;մ (yg Lۛia8B/In ]͹%3hZ9Q#o3l#( zו3٢uutVɓax| C%{UNp"@̥" 0p G2ۆDJ`9((7pe jXvqR.iIWzج!@"\~m…T7u(eR'@/DZWq/E|h!*uƠ8T#TD8'?Rd>XJ#zĢmO[m2s(<(}"VP8 R LK0:;`_N^1LO㻁٫ӧ[/ѽ>uQ ;s (Y粆d?t@hP#(2zN9}vRK߼ ٣\NGûԸIqAYU=.xyP\O6),PSi(vFj%c|yN}G}JHNM;gG2XȰ՗9Xu?{tb?)+RDˣS&yaf/|cԅGCM иkrV9A``BzkÀb8ӻ5620{8~3VmN8n>,Zv8Lb]X M~ îB>hkפ8w@k,Ώ-i ms!o 3r V{,ذۧ25{nClxyREkMy[olgytZݙ5_yO>_|wuz!\-;i 7exL*dB[Ț*r3 0u>҉; &3㹔=־/U-Հ]W`Y2NM]YZΥr EU +Ԯ /VECꊽg1}mW {j /7Z=CLf{R_hg_گs&"NWi+sO>U͈y{kU NfvAo{v^:+ h3btQ$$7X}ȇ{"24U.p[0aXʭɹgq&ѸWc)e:}f AZk4x4?5Q)|Dɓ #Ci#:YZҪc-?y_~}i)Q7pg5b0 DAK -Q/BXZ%IP>ۙYHn?Lɗ U7E" ;Qn+WI3pMWAU$tpr 7*PN_ 씨9H{ ]aF?֊! D#:|,:&=MҐF^hA#+OkrDϤٰ˖nlTxTwx5RzI:rЃ lC)Kv 9U PjAjW{@c@t/zW;dTA(mUhkW ~hV<sVuInPAhc0 ̨ƭM#)AQd\JJ^u͇HT^Z앍$$D?pC%?Ww>6ՙ)Oݽ>ěA4q->~nh鍭k=+CpD2FwAcjib~˂m9y?umz'Mro,!_Gs35ֲjKmpO;p S U,fia P3sX 8oY+3aЁ=7Ek|ޮ7V mXv`:$u>]&΅mC0A UXIמ[)CesjOT66F[{R ˊwf=k`^L :prJX Q2YgL;O')ⳫKL^NpZ*;#jd:(1|d@5 8·&ۣ࢒vvn`bu_n ҙjъ;b^*+1=ѓn80@f*fOʋrv5w EW1m[#V.4uzq'}^b)1y˩LuNЮ06,Jbq̗w2@IzS)kVIݲ+ 'M>&]uޣN5y"M9]WE<sjFZ4'>dlNkQU*} yKő r}uԩnb;CyUN\%Rɸuܥ}byJPGfJWcFy@cZ^V"wY6!P~܄#B=?;B,Q'K<#+|N+t\ށ?rp,j c,ѽci*=i.$0JʝJ uEJ*N oݸs{w߻nsݚ}[ }Ӱh _)3d $($\;س&N6$g8OL/ #H/ ԅ s#¤2[+i*6ĺn#юNYei|=W2&8 DIJ^bFU#щ.U OQm`Vfr^0#$'LĬ&ܖsA6OX~S%: ]]e(U!yfjfBXod4PtMz<ԋlSi/%Qmti^/wj۷k;wJVV[a=瓷~zqzǟw|MBo/Ylgp}: 42Y,m~tX{#/x݉We;}z0_2 o;eXhyyWv>k5?N 6;aDhPiL3<8m pmrf(Y:&Ԙ12eGnފ76"hdCO;(Ml][3,5w*)i`'' 2&M~uǡfLK*# 0oOޔCzjaq%,q. `C݉+WvwydYhh6}j юCaàk"J7x+/Y2 4F3pz]έMX.A}΋LP kFXu?||>|,<^.u,ܑo}{/(H o9ދu\KQjxd=G~Z( RޞeA&.x^@"]FpE'B+dg=E<-iDGn` ]j1lQkHٙ$:U;+:IX /^?|_$Fr-.#o׍̓&4aЖ\mՈge6DNr 2 ":Ǭgd͚-hj &x5Ҹn4 f$"i[y4Ɉ 9cfӪU7ܮJY=>=fD}7[Z{)Z2qSZ ]39s ;W'-}yn|6+BH٤[[>>=|,y V3j$}eS7Vhn$c 3]! =DDlgYm21kVފM!Ӎ::L= L9H66>ka;cO̼-O/Zٷa_Diga(r[132*m,ӳ3;+\܉Wip%ԏ~_jem%waqR` ӂ-5]ChƏԶ6xL6l]f.gkZ[%߼vV٦zJ487bʁh9]^>\)# 0_9l2&Uh/~巧Oh PP+VݿSGU}G $3 .!6v3cF\kqrxJL!(gI)Sd.YYy ;mI""zС6?q~H3n@]k 0wi;!r>fR}3>M B 4PVsgDӃbѥ`tpN2yΌ[.<_{ ~>2QQEYlܽq?1w $ūHis8ZKKoĜNp6H54uhPW Thk~PB^գg]ԗH8%5  E(fxepW{Kct;ȓbfDHқi9's WPx R_;[TF<ά Ӓl&a)n~zǧ 3i4ih{Fv >^ " d1d2/CB#&AKZ iMX`RqAրq5hd =YW_8+Bg[WD./|Wܤ( Q{e vOH`-̜ɱX[b3Bʎ(a X](_#ͻ#rsӾ~B A^F"O>-zT]=6VdޜUN4XƩJEM/cv85W!2I5+PEHȨK(EP_~DL˪%Z3%BJ.?x~pe{dZFTEXVMο[98!6tMi~w߽DÍ:.̣,0h__o6{iccpijǑvW7;F&~w~uȘys2M?NCb:soeGZ=v ͚[x= U+?ou(DiWhf+_ޓ;ŵ(< s?4!'zHvMbG)[3}9<"|[=ڢ8nѺ1Հ-1pP TWUlX3xuY[~:Swrl։+kR4 ܘ$qňOHqp6Gf뚠K;W"nu-֑.dѥ)Mѕx"j[9Ğp+?83кJ1!%%w CXPh6`ƶD3J)ʎ]=!Ln:9~G(tGE͒ xڐdtQO1lFvdJ+ZAeB+vZ{[K$jGͲ5 IxZ4,ceV0jF>cX|{гI<_4*>_%MR-I%M4GU׶8GMs\l ;ݏydtT?ʁZ{wR >eZ`t|QuRV@+y B:roBbUz.݋#Qm^~v|ѓjE-`8A1)kqsOur'<3^E)f#G{( lQS6aF"3' 8&Dsf%4Ual)c?<0N5d{ ڇvztG޲vzp3^tRMU@ ؃<}e؏fu[v?̶F_.fM7K;mܶg _@n#4^gh!WzԇO an!t.l2 ##,=Cet;{o$!9I3)15$-{dfMx۴"e하3طpʺ?|_=.9lknQuk###+EjP:jJ"Nȅ}G)Vڛ+,_U+Xr\ $~zU.g.J-!ʿׯW?:yrR_tTn}U{#|KYnKc?~мo:nW_֐U2"AeVj߻޻Ua\XdKzW+J{5)Z@A%'Gn0ٸo7m5)6kﰿgj3D CR];Rl[aM\13֛VP+N ۑ-UPJ<޴"%)9!Z% lr&(ji4z$NC 2))g4 `R-dZzLee&tP@Sf_OK с~1j@[Mf .c# bLdK/"#E:&[[E}d?aYz2ذa6kKI'Ő5/B%R/KDȻs%a<=4jJfMa2O>nSʮN2 I'ivGx [lNtz~V>KGW>~&g>_|O?VXHI؈xQJqڋDb Ҳ CZ$)$/+pܪSoݪE[Vx860%k;':{#5aCXjTt57EEDۺr#~Iw$Tm &._Z;~}X F- e׸1?62_#˃BhA^^)K0X71!y;sϽ0u'vgk~0Xfnrn$w1n0QZEy^ -yX2rX.hwq6uY^;>b,{ݣGꢏHk`>j 5eWj nݭI;uCq 5|cf Dh{i4]Ĥ)M|7e7oN4q_ay!r! W7\O>޻u64avpg8ǎ.fVD72QKvNzDks͠d?{4X:O=)Uwrc 6zl=:T)3?09GRkxcW3)t0hd r +0Gsp ՓˮZ#%tɕ8V=`tN8,Obb"[Sbmd, mI zZʪ>\7J-jCk¸"ڙiT() D(gׯ#r]ZppjuM3 )<9ڪOqQi7ָ.BTKs=Lh `IG l}_|?9:x"teˇ~g%k)숎Tި4F=#}XFdbC$V8իgUpn֢6[V╜ x9od JS'TV,$UQ-Y/0nTJD8)H Bϐ@I׊޿WYZY#fǘ7j1>{1!Ys k`.nkӍ3p&kwyLCъ\֤jqZ DZM/4lP~o͏靁l m[o-HaN!J[7#YKr?hhEgMv 3d-;I 9r;~TN62wXR􋕹;&ݏ֖^8>`. K$e_ѿ黇=9*g .xO}bm<9BS@v-8֏lJJsEѬ*quI4`UѠzMK Q-Ct2>I^vުjj+j\8y~zrrZǝ[?;w)uU|z}д?Ie|=-&៵fn?}Y.d=$CN7SW^YUKkPZkUuK{s L>k}ω`V9{`n_?TB6gH*겉Gowe=j!fCs,=5c|= c!oRve *,@*N /abBf@! вimgmzq2 Fhh4G^i3v&-@BFD6,D0І9q 0 mgvG5Iz_cX.]iܭlh ڬʺgHQ#IߚWV tԷ`>:oW/$/?//}}e44] ?Ť_~O~^XŤm+r*C&∥K7ʲ;Ya%n̤'8DR */6}<{~Qi{ʹXC`0r B: D",*wWb5z<]}zbGBX%E똽ި__#SLO:YWmkKL}<ԻlFtfã!}G>A; "~UK>zdǁm,6c3.O}㋿~^HFzSIif+Z<5)7H:FRӹy1M[ ;x?[O`{RXL;0d[AȪ>\l )hƞcmQHQȈp0q%wӖl :K֮c[lio~?_r zW{1ls`>k/\yU]&{e) ܂9Kΰ2sxtm_FYUV /Xu 2j S:rPmyzN]xcFsmG5jm-P&ka͋ow=x4W/u{ 1l(2yfxJ_%61t0T*/ Cu>o_) ȜUo/~V&lrp21Gsq3JqlT GzYaﰨE ]I1!7+jFa`Ŧ0.nbE~H-7$q;u:C7F v+ )EHCwY&\ )(0mlPY8 d,w9X3,Ӧ 2o,{ i&w ۚ%Gcӛ/p} i@N5Z$\BS 9YoWB&&}iҿy,TXU(3D/JnÏ~!8B:mD@)H(!(`XVJL="KW5 Ӷƥ~Y/7 8P jYB\ x4P _!cP7#[&՜QMvlEt`NfF!jDr IOZ&ԡ' Q>Ll wFLħ@:&GQ+kaL L h># f|ی҃#C/saK'co/="RO 6e_͇ـ$7eSҴ0][=E Dz.6dia6 YjLX+Р52˿)]:THũNJ}dL)/^!}[mU.[:NMu0uMo]WȚq֙ߺ@A@cco֋RCU 3mJ1wcR0{Qr/_WQ۷;{l:J^ulK/-콱J+GJN/Z~YVOˈZ@[bɺ1XZrmLb\nh@(zͼ]s8e|U+d:ʔi\?% $k#qvWeHFV5lOtoNᛁJOނ8e $p仼!Vz&q k\u (IH-9G!v7nQvbP,|8[`k3?".q &S=t Nwnf 8Ox8J>i0Ҵ{ 3!]j^% ¤wߡ>"v54O (y JRrSfiQ<z`0W&Ղ 9R3YE%$i"a[Ċâ(W _SGi_'5Q٩<̆")I|J)@[o=3߯DM*U*i:@—zIKwӅ e5t!K-fjqvb Uvm*S/5w~ӻ@8xFk.}7).GQytĝE֗RYY/WUx--Ex_׎*j+vX2*5>UVCՈzP+gIDATT‚{s=S.Ж:N:|j^uS&勺kwEiA2ٞRkja-XaR)ʚEʢ|ٗ˿|/b-[;w*ZKHI+pW{! 9< rĵUz'wβ$Ev|"NttEb\ : hL#Z=SSocO^a&͖zi{PUux3Cf̆ hA.nuQ4NMT mGխΆ=t]x h $_g$&XZo5eLV'I-$QDK<üUOgYUYIUpH;> #,enC;Ј)?Қ"T :'%?@1e=f2@{Fc1.:! y(IDp} NQucA6$QWTԣ75k[9K %'mEE<9t4 /{zQ5קy0U_|_ mz:F{ *%RJJ̪4Jռ] TMB<<1*ыD틗O/.*Pu~Oa% _dyD:a!@C#3M+!?g-y,1il2M hHݻwWKFC5=XKu%~irY1Q򆁆A32=3 .ğxJby8 -(S{@zk&1)xH𔩙 ޖ2Ǡpњt053jxoeX)᥀ +e:|7]='O^s͖vO!DyVk*2ҲHm +k+S,;Lf%iY̋JvzMt# 󖁦|BU%,tHBYVq9{UҰn&zZ,r'?q}aԺF&,1{9s%Wnꆑ/O+Rc~կ~Oʙ|d( . pN gr=΅#X.$e9tC{5R[q?2 C|'4j(e2˙op"GvUJ =qt>û(oeyYu\h/ZwSƾ xy)|h`pOݭJPi@]kn[6|d&4e.a&e2|/|X a Fzk)h,鵰H]oq<PG=!2Y@  *d h uY*Il^Z3>G'7q1VhL v[h#Hlm~ ?Rۻ8擰F&x =e #Ȏr8qb2## 7AsY>!"Ȝ(>~cucV[;EP9J!e06R6rJ*8iA/Dug$DANW ʗBmBs_[Ua .$KEGz#N: %V 48͑%u(scKlӋ/y9G\T SD 'D6}]EPIolH!XiS$u]wϔ"q2Rts|brt?!܆'j%Bq[d05֜Gv˅<&(Yޓ<]i T޽okZ(A3f[CW0ת 2>@_Ȑ'MZ\4Ш/k[G8#_w=аB{##5!!QZ_V5(:n! 3shk՚eZn%ܐs ¾mAE X?ؼ^Qg!ecZ >U#5(iKu<~2{]{ތ/Ŭ-,CJÍx8{DA"Aza`4Xye`(.&Q^]\uez=#6|?I),3SMlglXW}nd`Ňjٖv^asz.N@/R3-Sӽ{K/uB&PѼz"Y,Bl ug44ױ]n(Pomvt A5Zy믿JN食7?ym(kR*C[\fb, Q`zJXhܨ0C$5dAʆ[fL}bХzo0"慁S06Z1ܮFl>ۿ/!H8[Xv/>:wWå*a".b#1Ct!21Gb8@ȡcil ѶeY/ѓS )C:mBc?LP"-;(l9%%>vFQBWp7(вN%`vGi$$-(&0 "*m -&1ڦT6h)`^ .ەѱ1QzGB3ԠpFCwC}5㧞o*3҉̝gvi&_W{|ꏤ\_p,d bm^ɊnG4Cnfr ]C _3ᆳ[1cZ2xGg4 5yxVIIR6#F?4.LGZ:D?XVG71߄*g/焟-,p5?۰UtJ>u\xLJAS꼫ߵMoZSxC&] mlqJ)!`T`#b_ѵxYuaSKfVFUP׺`k2gt\]8 ,@l(kEm#!z,;޺Fp2endΤ\ɨɺ;XtXzԜ rq_F>O?;gw X1}$'+U [cc@ )4A4,UYoeKJzgrEɾ^@'iYʦI fs^Xl8e|utQ8H,{$'BdEJGZu `D;GDuz O[rmb 0p`ض {[~! O$ugX 3 -.| 6L> T!A17ww4&1E婊cQSm;l_Cvrm`k9(\+0-R8^RjGr6qe+.e-`#=rܐk!D/ VVUH}8" %FFQУ*ҊD/M>$c=? A ju 6JK2zCǐ:Tkݨ~twZ&: ܔ^$Z!=M0i\>nN6:mճśWkwC0ӕW/o//jOϕA"Tmzh.F 3JӉԤy}Hrk)^aKxy: { 6CcR_}^Df.:ǁ l>l9Y4]Z31޿ /"'pE_K"oĦݧ2???wóe¢c~ǬkV',3Xxζ)7릛!T7|`H]RO2=V ΚP @-`(Fpdң apqpQlCC&Gzk( BmcAZf> ħaQJl Ll{S- dmbsPoXfV\b)m!f.DгyEtosSlhQ46fף"xRXwmI=!}k_#. |$gIs-6 pLHjC2T ^=!C^Mudc jrP#omٯⰦ,Ծ)%,'ARі>k\M-ܽ 2rnz{^H}\N[Đ3ɝsu_~W_|}[yo_9>6c|mU.ŷMi'-aQEE:hA:i=K&A,ɔƗ`[.IM?1n${ǶQ@P&<45E3ì ܑ^spb&1x> E{8mśJfO7#HI\G(w_Va "GQFN4X1aaHMwX!7sSf2t `Mg`xcPT /^(s@LՈ4@x!p?3PAZ$1g8Av]7YbCY@^2V8#$9~G/b2+YaTג^ E ^:,k7 G v" 55&4iHx.4<t/@ ӎB s'!%TDŽLRj?NҿQVvQ{gZ ;]脞;J`#m |3wKGc2}4??߬_-Ԫ'33 *m0cP.hˉBh )ŕ:ΊS̞PzUBu F~z?wy,.Eac |· 0b%USX 6aN~. D=5!WKUҹ r`P/ oNHА13úY4:T'E+&V}>(r]Yd_i,+V▕CxI9/%]UCT W \rɈ1]qxG0&`PFc['C1n /RZm :Q˘39w ) I r~ˊ.C/E˂rг#Ir3+ީ`VhpM vLo.,T$ mg)E٘z۴2_YYOG(TAb4iuQRj,~jB#7a5|qLOv ǢF58.1 (:6zNE3Ѕw'?5ܤ)R6KKEh 3S5?U0;Dc#57ݒr&#M_+&3=fLT}A߬g*@e4۞ovJZˢ`됕Z" 䐷&:/8~u?|tHqՊO~:%8V$:J]׼:J}al 4˺G7ԕM+ЪMiLI*ee4<&Y41c LOpID)ݽ[sOW!ց U1x;tX䓝_}]wC?حw= kDOBT浟S5Ӈ)_[#QF2?[{fq265R8̜DFS9uƌiר)>zs#X? D/-U; x@k?6K> tܑؖi?|65Duu!Dr _DvA./ٛ\RQ{i)-U_yz$@rbs` Sos b)w&:|ӺiZ|Uk5afV,zaHoXwuk, :MMdirVZzEa"l^(oh0beYfr{bnn?XFUv։Ǐ~_/~BpNtnNg"n#Pv  9SU-+ǝTRoq*4GY%nxeuv741lkJv|ʲx-6*Y F6Iv FV[':"b<8S_UFÇnкREMI&j$H혨*Q2e;wM 2xuг2KGؼUW4i@H(c>l(ex TLFMY2 +֊Qsډ07GpEpj$Go/h-+Ѐ” c& Ö1&}sR?et^Pm# Ư_>=N.R&U%2 Q/LBx5ssfJnMGnkpZ@Z ɛHRvf[%aTEwX∐z:~ɳ ޔ#6z]JQOo[d]x iM{uy3ck(!H@=dRFDRzꞘAltg ѷ2h7S. '$f+᧯rãw0T@]TNLRillT#sbCvoAQƗm[uT^e4‚T5j:Z3?f#GKX=ZDS$6QI Yslr76Uײᔧ'aj7͖WO_\>/*?};n߿wL*!XuJ99k<,:)\eD,Y1au4m86U9Ͻ *!k9D6%d?i6Dl6e,}(*XbTtdV م0L~L'{qVl+Lx`Y˺Jd h(B һA<0KhnQA#lЃK % 07&ĤB 2Dc}pml=١`ax~#:n=_CJkm Pؓ-"[4Բ5fi> OQ4̛5Ls;"CjFG9t,%Vi0A8*{v܆rC&l1Z] L<6?qP[ T- }}qٯ5< X5|sNuN A,d wM/J^<{vyuYO4F;)nԁ1W"+ 7C#K!(aL{FفNCgF6huxуRW Y_Dž$2 ɩ1rȀR =R 8b(n J)"Egݎda=WEGu]T(uFKym: 4bl*Ԡ?$uKvS|3|F$cjIͣA&UNJ@V W/j m6V"J $ʑ'+_ϣ?o)4B<͢IԃluP&I dRBr>Z!*{ /~+pI#E-+]Yt>بlE9h5Nf8*.H"Md6IGmjCLߥ,q:Аg`074$Lc-?X.i52ZqbQ r̕ʠd{N@FݑsELߍR÷FA1: &>Bi݅a)hڛwu^bXCcDi5,߮ޫ'z\G}Cl蝗1KmtDG-DK[e")'62L{X2tʴP](}A 5VڠQ=豞TNG(,hۯkN=0(E 5'!yv/mpjϲ.^|jZ_|y%N2ž]I/N?a̭/-n1fbV[6_9ژUH$y^*b3 /H)e?VEo ;l\o`q-͠a1!%r@% *¦ ;S`9PPQñEs H #w7nAG24r?0 U2 \h|vVj(XS\"HQ9C"(LT{X5@_%Ȓ5W'.jB-U|YڸUTdSF.]h ,ΜW[7Pk`̥!b2S m<Vؿ/P31Y7Z@ 9*-Hݑ0p"'9NV턓3%dR0Sǵ``l`Do|+ҋN/m*bIi(%٥䠞4<quuY^;X+\-ל\UA7$M T,FHU