pyface-6.1.2/0000755000076500000240000000000013515277240013736 5ustar cwebsterstaff00000000000000pyface-6.1.2/PKG-INFO0000644000076500000240000000605613515277240015042 0ustar cwebsterstaff00000000000000Metadata-Version: 1.1 Name: pyface Version: 6.1.2 Summary: traits-capable windowing framework Home-page: http://docs.enthought.com/pyface Author: ETS Developers Author-email: enthought-dev@enthought.com License: BSD Download-URL: https://github.com/enthought/pyface Description-Content-Type: UNKNOWN Description: ========================================== pyface: traits-capable windowing framework ========================================== .. image:: https://travis-ci.org/enthought/pyface.svg?branch=master :target: https://travis-ci.org/enthought/pyface .. image:: https://ci.appveyor.com/api/projects/status/68nfb049cdq9wqd1/branch/master?svg=true :target: https://ci.appveyor.com/project/EnthoughtOSS/pyface/branch/master .. image:: https://codecov.io/github/enthought/pyface/coverage.svg?branch=master :target: https://codecov.io/github/enthought/pyface?branch=master 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: - wxPython - PyQt - PySide **Warning:** The default toolkit if none is supplied is ``qt4``. This changed from ``wx`` in Pyface 5.0.. Documentation ------------- * `Online Documentation `_. * `API Documentation `_. Prerequisites ------------- Pyface depends on: * a GUI toolkit: one of PySide, PyQt or WxPython * `Traits `_ * Pygments for syntax highlighting in the Qt code editor widget. * some widgets may have additional optional dependencies. 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 :: 2.7 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Topic :: Scientific/Engineering Classifier: Topic :: Software Development Classifier: Topic :: Software Development :: Libraries pyface-6.1.2/appveyor-clean-cache.txt0000644000076500000240000000000213462774551020465 0ustar cwebsterstaff000000000000001 pyface-6.1.2/TODO.txt0000644000076500000240000000112013462774551015246 0ustar cwebsterstaff00000000000000* Remove the import of pyface.resource for core functionality in resource_manager.py and i_image_resource.py. * Remove the dependency on etsdevtools.developer AND apptools.io by refactoring dock features modules. * Remove the dock dependence on TraitsBackendWX by removing the direct imports of traitsui.wx. * Need to make use of drag and drop features optional in this project's code (see dock, grid, sheet, tree, and viewer/tree_viewer) or help make traits.util's drag_and_drop module backend agnostic. Right now, it has a runtime dependency that wx be installed. pyface-6.1.2/pyface/0000755000076500000240000000000013515277235015211 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/tree/0000755000076500000240000000000013515277236016151 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/tree/node_monitor.py0000644000076500000240000001042013462774551021217 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A monitor for appearance and structural changes to a node. """ # Standard library imports. import logging # Enthought library imports. from traits.api import Any, Event, HasTraits # Local imports. 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) return 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) return def fire_nodes_changed(self, children=[]): """ Fires the nodes changed event. """ self.nodes_changed = NodeEvent(node=self.node, children=children) return 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 ) return def fire_nodes_removed(self, children): """ Fires the nodes removed event. """ self.nodes_removed = NodeEvent(node=self.node, children=children) return 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 ) return 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 #### EOF ###################################################################### pyface-6.1.2/pyface/tree/tree.py0000644000076500000240000000117513462774551017471 0ustar cwebsterstaff00000000000000# Copyright (c) 2017, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! """ The implementation of a tree control with a model/ui architecture. """ # Import the toolkit specific version. from __future__ import absolute_import from pyface.toolkit import toolkit_object Tree = toolkit_object('tree.tree:Tree') pyface-6.1.2/pyface/tree/images/0000755000076500000240000000000013515277236017416 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/tree/images/document.png0000644000076500000240000000051513462774551021746 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-6.1.2/pyface/tree/images/closed_folder.png0000644000076500000240000000102513462774551022731 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-6.1.2/pyface/tree/images/image_LICENSE.txt0000644000076500000240000000136013462774551022406 0ustar cwebsterstaff00000000000000The icons are mostly derived work from other icons. As such they are licensed accordingly to the original license: Project License File ---------------------------------------------------------------------------- GV (Gael Varoquaux) Public Domain N/A Nuvola LGPL image_LICENSE_Nuvola.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: ---------------------------------------------------------------------------- enthought/pyface/tree/images: closed_folder.png | Nuvola document.png | GV open_folder.png | Nuvola pyface-6.1.2/pyface/tree/node_tree_model.py0000644000076500000240000002470613462774551021663 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The model for a tree control with extensible node types. """ # Enthought library imports. from traits.api import Dict, Instance # Local imports. 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) return 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 return 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.on_trait_change( self._on_nodes_changed, 'nodes_changed' ) monitor.on_trait_change( self._on_nodes_inserted, 'nodes_inserted' ) monitor.on_trait_change( self._on_nodes_removed, 'nodes_removed' ) monitor.on_trait_change( self._on_nodes_replaced, 'nodes_replaced' ) monitor.on_trait_change( self._on_structure_changed, 'structure_changed' ) monitor.start() return def _stop_monitor(self, monitor): """ Stops a monitor. """ monitor.on_trait_change( self._on_nodes_changed, 'nodes_changed', remove=True ) monitor.on_trait_change( self._on_nodes_inserted, 'nodes_inserted', remove=True ) monitor.on_trait_change( self._on_nodes_removed, 'nodes_removed', remove=True ) monitor.on_trait_change( self._on_nodes_replaced, 'nodes_replaced', remove=True ) monitor.on_trait_change( 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, monitor, trait_name, event): """ Called when nodes have changed. """ self.nodes_changed = event return def _on_nodes_inserted(self, monitor, trait_name, event): """ Called when nodes have been inserted. """ self.nodes_inserted = event return def _on_nodes_removed(self, monitor, trait_name, event): """ Called when nodes have been removed. """ self.nodes_removed = event return def _on_nodes_replaced(self, monitor, trait_name, event): """ Called when nodes have been replaced. """ self.nodes_replaced = event return def _on_structure_changed(self, monitor, trait_name, event): """ Called when the structure of a node has changed drastically. """ self.structure_changed = event return #### EOF ###################################################################### pyface-6.1.2/pyface/tree/trait_list_node_type.py0000644000076500000240000000304213462774551022751 0ustar cwebsterstaff00000000000000""" The node type for a trait list. """ # Enthought library imports. from traits.api import Any, Str # Local imports. 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 ##### EOF ##################################################################### pyface-6.1.2/pyface/tree/__init__.py0000644000076500000240000000000013462774551020253 0ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/tree/trait_dict_node_type.py0000644000076500000240000000307413462774551022726 0ustar cwebsterstaff00000000000000""" The node type for a trait dictionary. """ # Enthought library imports. from traits.api import Any, Str # Local imports. 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 ##### EOF ##################################################################### pyface-6.1.2/pyface/tree/api.py0000644000076500000240000000211613462774551017277 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005-2017, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ from __future__ import absolute_import 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-6.1.2/pyface/tree/node_tree.py0000644000076500000240000001357213462774551020502 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A tree control with extensible node types. """ # Standard library imports. import six if six.PY2: from inspect import getargspec else: # avoid deprecation warning from inspect import getfullargspec as getargspec # Enthought library imports. from pyface.action.api import ActionEvent from traits.api import Instance, List, Property # Local imports. 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 ################################################# def _node_activated_changed(self, obj): """ Called when a node has been activated (i.e., double-clicked). """ default_action = self.model.get_default_action(obj) if default_action is not None: self._perform_default_action(default_action, obj) return def _node_right_clicked_changed(self, obj, point): """ Called when the right mouse button is clicked on the tree. """ # 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 = getargspec(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)) return 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 #### EOF ###################################################################### pyface-6.1.2/pyface/tree/node_event.py0000644000076500000240000000237313462774551020661 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The event fired by the tree models/node monitors etc. """ # Enthought library imports. 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 #### EOF ###################################################################### pyface-6.1.2/pyface/tree/tree_model.py0000644000076500000240000001266313462774551020655 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Model for tree views. """ # Enthought library imports. from traits.api import Any, HasTraits, Event # Local imports. 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) return def fire_nodes_inserted(self, node, children): """ Fires the nodes inserted event. """ self.nodes_inserted = NodeEvent(node=node, children=children) return def fire_nodes_removed(self, parent, children): """ Fires the nodes removed event. """ self.nodes_removed = NodeEvent(node=node, children=children) return 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 ) return def fire_structure_changed(self, node): """ Fires the structure changed event. """ self.structure_changed = NodeEvent(node=node) return #### EOF #################################################################### pyface-6.1.2/pyface/tree/node_manager.py0000644000076500000240000001111313462774551021142 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The node manager looks after a collection of node types. """ # Standard library imports. import logging # Enthought library imports. from traits.api import HasPrivateTraits, List # Local imports 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(NodeManager, self).__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) return 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.warn('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. ########################################################################### def _node_types_changed(self, new): """ Called when the entire list of node types has been changed. """ for node_type in new: node_type.node_manager = self return #### EOF ###################################################################### pyface-6.1.2/pyface/tree/node_type.py0000644000076500000240000002001113462774551020506 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The base class for all node types. """ # Enthought library imports. from traits.api import Any, HasPrivateTraits, Instance, List from pyface.api import ImageResource from pyface.action.api import Action, ActionManagerItem, 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 = Instance(ImageResource) # The image used to represent nodes that allow children and are NOT # expanded. closed_image = Instance(ImageResource) # The image used to represent nodes that allow children and ARE expanded. open_image = Instance(ImageResource) # 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 #### EOF ###################################################################### pyface-6.1.2/pyface/python_shell.py0000644000076500000240000000167213462774551020305 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The implementation of an interactive Python shell. """ # Import the toolkit specific version. from __future__ import absolute_import from .toolkit import toolkit_object PythonShell = toolkit_object('python_shell:PythonShell') #### EOF ###################################################################### pyface-6.1.2/pyface/ipython_widget.py0000644000076500000240000000217113462774551020625 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The implementation of an IPython shell. """ from __future__ import absolute_import # Import the toolkit specific version. try: import IPython.frontend 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-6.1.2/pyface/ui/0000755000076500000240000000000013515277236015627 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/__init__.py0000644000076500000240000000000013462774551017731 0ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/wx/0000755000076500000240000000000013515277237016266 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/wx/tree/0000755000076500000240000000000013515277237017225 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/wx/tree/tree.py0000644000076500000240000012417713462774552020555 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A tree control with a model/ui architecture. """ # Standard library imports. import logging import os # Major package imports. import wx # Enthought library imports. from traits.api import Any, Bool, Callable, Enum, Event, Instance from traits.api import List, Property, Callable, Str, Trait, Tuple # Local imports. 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.widget import Widget 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. wx.TreeCtrl.__init__(self, parent, wxid, style=style) # The tree that we are the toolkit-specific delegate for. self._tree = tree return def __del__(self): """ Destructor. """ # Stop listenting to the model! self._tree._remove_model_listeners(self._tree.model) return class Tree(Widget): """ 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(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 # 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(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 = Trait(None, Callable, None) # Flag for allowing selection events to be ignored _ignore_selection_events = Bool(False) ########################################################################### # '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. """ # Base class constructors. super(Tree, self).__init__(**traits) # Get our wx Id. wxid = wx.NewId() # Create the toolkit-specific control. self.control = tree = _Tree(self, parent, wxid,style=self._get_style()) # Wire up the wx tree events. wx.EVT_CHAR(tree, self._on_char) wx.EVT_LEFT_DOWN(tree, 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! wx.EVT_RIGHT_DOWN(tree, 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 ;^) wx.EVT_LEFT_DCLICK(tree, self._on_tree_item_activated) #wx.EVT_TREE_ITEM_ACTIVATED(tree, wxid, self._on_tree_item_activated) wx.EVT_TREE_ITEM_COLLAPSING(tree, wxid, self._on_tree_item_collapsing) wx.EVT_TREE_ITEM_COLLAPSED(tree, wxid, self._on_tree_item_collapsed) wx.EVT_TREE_ITEM_EXPANDING(tree, wxid, self._on_tree_item_expanding) wx.EVT_TREE_ITEM_EXPANDED(tree, wxid, self._on_tree_item_expanded) wx.EVT_TREE_BEGIN_LABEL_EDIT(tree, wxid,self._on_tree_begin_label_edit) wx.EVT_TREE_END_LABEL_EDIT(tree, wxid, self._on_tree_end_label_edit) wx.EVT_TREE_BEGIN_DRAG(tree, wxid, self._on_tree_begin_drag) wx.EVT_TREE_SEL_CHANGED(tree, wxid, self._on_tree_sel_changed) wx.EVT_TREE_DELETE_ITEM(tree, wxid, self._on_tree_delete_item) # Enable the tree as a drag and drop target. self.control.SetDropTarget(PythonDropTarget(self)) # The image list is a wxPython-ism that caches all images used in the # control. self._image_list = ImageList(image_size[0], image_size[1]) 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' 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) return 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) return def expand(self, node): """ Expands the specified node. """ wxid = self._get_wxid(node) if wxid is not None: self.control.Expand(wxid) return 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)) return 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.GetPyData(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.SetPyData(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) return def select(self, node): """ Selects the specified node. """ wxid = self._get_wxid(node) if wxid is not None: self.control.SelectItem(wxid) return 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 return 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.warn('removing node: %s' % str(node)) return 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.on_trait_change(self._on_root_changed, 'root') model.on_trait_change(self._on_nodes_changed, 'nodes_changed') model.on_trait_change(self._on_nodes_inserted, 'nodes_inserted') model.on_trait_change(self._on_nodes_removed, 'nodes_removed') model.on_trait_change(self._on_nodes_replaced, 'nodes_replaced') model.on_trait_change(self._on_structure_changed, 'structure_changed') return def _remove_model_listeners(self, model): """ Removes listeners for model changes. """ # Unhook the model event listeners. model.on_trait_change( self._on_root_changed, 'root', remove=True ) model.on_trait_change( self._on_nodes_changed, 'nodes_changed', remove=True ) model.on_trait_change( self._on_nodes_inserted, 'nodes_inserted', remove=True ) model.on_trait_change( self._on_nodes_removed, 'nodes_removed', remove=True ) model.on_trait_change( self._on_nodes_replaced, 'nodes_replaced', remove=True ) model.on_trait_change( self._on_structure_changed, 'structure_changed', remove=True ) return 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.SetPyData(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) return 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.SetPyData(wxid, (False, node)) # Make sure that we can find the node's Id. self._set_wxid(node, wxid) return 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.InsertItemBefore( 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.SetPyData(wxid, (False, node)) # Make sure that we can find the node's Id. self._set_wxid(node, wxid) return 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.SetPyData(wxid, None) return 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) return 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 'GetPyData' # 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.GetPyData(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.GetPyData(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, root): """ Called when the root of the model has changed. """ # 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) return def _on_nodes_changed(self, event): """ Called when nodes have been changed. """ self._update_node(self._get_wxid(event.node), event.node) for child in event.children: cid = self._get_wxid(child) if cid is not None: self._update_node(cid, child) return def _on_nodes_inserted(self, event): """ Called when nodes have been inserted. """ parent = event.node children = event.children index = 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.GetPyData(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.SetPyData(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) return def _on_nodes_removed(self, event): """ Called when nodes have been removed. """ parent = event.node children = event.children # Has the node actually appeared in the tree yet? pid = self._get_wxid(parent) if pid is not None: for child in 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) return def _on_nodes_replaced(self, event): """ Called when nodes have been replaced. """ for old_child, new_child in zip(event.old_children, event.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.SetPyData(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() return def _on_structure_changed(self, event): """ Called when the structure of a node has changed drastically. """ self.refresh(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.m_altDown == 1, control_down = event.m_controlDown == 1, shift_down = event.m_shiftDown == 1, key_code = event.m_keyCode ) event.Skip() return 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() return 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() return 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.GetPyData(wxid) # Trait event notiification. self.node_activated = node return 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.GetPyData(wxid) # Give the model a chance to veto the collapse. if not self.model.is_collapsible(node): event.Veto() return 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.GetPyData(wxid) # Make sure that the item's 'closed' icon is displayed etc. self._update_node(wxid, node) # Trait event notification. self.node_collapsed = node return 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.GetPyData(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.SetPyData(wxid, (True, node)) else: event.Veto() return 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.GetPyData(wxid) # Make sure that the node's 'open' icon is displayed etc. self._update_node(wxid, node) # Trait event notification. self.node_expanded = node return 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.GetPyData(wxid) # Give the model a chance to veto the edit. if not self.model.is_editable(node): event.Veto() return 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.GetPyData(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) return 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.GetPyData(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 return 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() return 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 GetPyData() 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?!? data = self.control.GetPyData(wxid) 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) return #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/tree/__init__.py0000644000076500000240000000000013462774552021327 0ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/wx/python_shell.py0000644000076500000240000002437713462774552021370 0ustar cwebsterstaff00000000000000# Copyright (c) 2005-18, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: Enthought, Inc. """ Enthought pyface package component """ # Standard library imports. import six.moves.builtins import os import sys import types # Major package imports. from wx.py.shell import Shell as PyShellBase import wx # Enthought library imports. from traits.api import Event, provides # Private Enthought library imports. from traits.util.clean_strings import python_name from pyface.wx.drag_and_drop import PythonDropTarget # Local imports. from pyface.i_python_shell import IPythonShell, MPythonShell from pyface.key_pressed_event import KeyPressedEvent from .widget import Widget import six @provides(IPythonShell) class PythonShell(MPythonShell, 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) ########################################################################### # '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. """ # Base class constructor. super(PythonShell, self).__init__(**traits) # Create the toolkit-specific control that represents the widget. self.control = self._create_control(parent) # Set up to be notified whenever a Python statement is executed: self.control.handlers.append(self._on_command_executed) ########################################################################### # '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) execfile(path, 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. wx.EVT_CHAR(shell, 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 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, six.string_types) 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 = six.moves.builtins.raw_input super(PyShell,self).__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) six.moves.builtins.raw_input = self.raw_input self.destroy() super(PyShellBase, self).Destroy() class _NullIO: """ 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-6.1.2/pyface/ui/wx/ipython_widget.py0000644000076500000240000004241213462774552021703 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: Enthought, Inc. # #------------------------------------------------------------------------------ """ The wx-backend Pyface widget for an embedded IPython shell. """ # Standard library imports. from __future__ import print_function import six.moves.builtins import codeop import re import sys # System library imports import IPython from IPython.frontend.wx.wx_frontend import WxController from IPython.kernel.core.interpreter import Interpreter import wx # Enthought library imports. 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 # Local imports. from .widget import Widget import six # 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(IPython010Controller, self).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 = six.moves.builtins.raw_input six.moves.builtins.raw_input = my_rawinput IPythonController.__init__(self, *args, **kwargs) six.moves.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(IPython09Controller, self).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 as e: # 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 (self.AutoCompActive() and line and not line[-1] == '.') \ or create==True: 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 = '\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. """ # Base class constructor. super(IPythonWidget, self).__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. wx.EVT_CHAR(shell, 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, six.string_types) 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-6.1.2/pyface/ui/wx/mdi_application_window.py0000644000076500000240000001357013462774552023374 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ An MDI top-level application window. """ from __future__ import absolute_import # Major package imports. import wx # Enthought library imports. from traits.api import Bool, Instance, Int, Tuple # Local imports. from .application_window import ApplicationWindow from .image_resource import ImageResource try: import wx.aui AUI = True except: 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 = Instance(ImageResource, 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. wx.EVT_SIZE(self.control, self._on_size) # Client window events. client_window = self.control.GetClientWindow() wx.EVT_ERASE_BACKGROUND(client_window, self._on_erase_background) self._wx_offset = client_window.GetPositionTuple() if AUI: # Let the AUI manager look after the frame. self._aui_manager.SetManagedWindow(self.control) contents = super(MDIApplicationWindow, self)._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 return 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.LayoutAlgorithm().LayoutMDIFrame(self.control) return 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-6.1.2/pyface/ui/wx/image_widget.py0000644000076500000240000001647513462774552021305 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A clickable/draggable widget containing an image. """ from __future__ import absolute_import # Major package imports. import wx # Enthought library imports. from traits.api import Any, Bool, Event # Local imports. from .widget import Widget class ImageWidget(Widget): """ 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. super(ImageWidget, self).__init__(**traits) # 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.PENSTYLE_SOLID ) self._selectedPenLight = wx.Pen( wx.SystemSettings.GetColour(wx.SYS_COLOUR_3DHIGHLIGHT), 1, wx.PENSTYLE_SOLID ) return ########################################################################### # 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() return 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() return def _on_leave_window(self, event): """ Called when the mouse leaves the widget. """ if self._mouse_over: self._mouse_over = False self.Refresh() return def _on_left_dclick(self, event): """ Called when the left mouse button is double-clicked. """ #print 'left dclick' event.Skip() return 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() return 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.GetClientSizeTuple() 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() return def _on_paint ( self, event = None ): """ Called when the widget needs repainting. """ wdc = wx.PaintDC( self.control ) wdx, wdy = self.control.GetClientSizeTuple() 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 == True: wdc.SetBrush( wx.TRANSPARENT_BRUSH ) wdc.SetPen( pens[ bd ] ) wdc.DrawLine( 1, 1, wdx - 1, 1 ) wdc.DrawLine( 1, 1, 1, wdy - 1 ) wdc.DrawLine( 2, 2, wdx - 2, 2 ) wdc.DrawLine( 2, 2, 2, wdy - 2 ) wdc.SetPen( pens[ 1 - bd ] ) wdc.DrawLine( wdx - 2, 2, wdx - 2, wdy - 1 ) wdc.DrawLine( 2, wdy - 2, wdx - 2, wdy - 2 ) wdc.DrawLine( wdx - 3, 3, wdx - 3, wdy - 2 ) wdc.DrawLine( 3, wdy - 3, wdx - 3, wdy - 3 ) return #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/expandable_panel.py0000644000076500000240000001145413462774552022132 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A Layered panel. """ from __future__ import absolute_import # Major package imports. import wx from traits.api import Instance # Local imports. from .expandable_header import ExpandableHeader from .image_resource import ImageResource from .widget import Widget class ExpandablePanel(Widget): """ An expandable panel. """ # The default style. STYLE = wx.CLIP_CHILDREN collapsed_image = Instance(ImageResource, ImageResource('mycarat1')) expanded_image = Instance(ImageResource, ImageResource('mycarat2')) ########################################################################### # 'object' interface. ########################################################################### def __init__(self, parent, **traits): """ Creates a new LayeredPanel. """ # Base class constructor. super(ExpandablePanel, self).__init__(**traits) # Create the toolkit-specific control that represents the widget. self.control = self._create_control(parent) # The layers in the panel. # # { str name : wx.Window layer } self._layers = {} self._headers = {} return ########################################################################### # '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() return ########################################################################### # 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, self, title = text) sizer.Add(heading.control, 1, wx.EXPAND) heading.on_trait_change(self._on_button, 'panel_expanded') # 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 #### wx event handlers #################################################### def _on_button(self, event): """ called when one of the expand/contract buttons is pressed. """ header = event 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() self.control.SetSize((w+1, h+1)) self.control.SetSize((w, h)) pyface-6.1.2/pyface/ui/wx/tasks/0000755000076500000240000000000013515277237017413 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/wx/tasks/advanced_editor_area_pane.py0000644000076500000240000000136013462774552025076 0ustar cwebsterstaff00000000000000# Standard library imports. import sys # Enthought library imports. from traits.api import provides # Local imports. 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-6.1.2/pyface/ui/wx/tasks/task_window_backend.py0000644000076500000240000001622313462774552023774 0ustar cwebsterstaff00000000000000# Standard library imports. import logging # System library imports. import wx from pyface.wx.aui import aui # Enthought library imports. from traits.api import Instance, List, Str # Local imports. 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-6.1.2/pyface/ui/wx/tasks/editor_area_pane.py0000644000076500000240000001340413462774552023253 0ustar cwebsterstaff00000000000000# Standard library imports. import sys import logging # Enthought library imports. from pyface.tasks.i_editor_area_pane import IEditorAreaPane, \ MEditorAreaPane from traits.api import on_trait_change, provides # System library imports. import wx from pyface.wx.aui import aui, PyfaceAuiNotebook # Local imports. 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(EditorAreaPane, self).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() # 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() 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 ################################################ @on_trait_change('editors:[dirty, name]') def _update_label(self, editor, name, new): index = self.control.GetPageIndex(editor.control) self.control.SetPageText(index, self._get_label(editor)) @on_trait_change('editors:tooltip') def _update_tooltip(self, editor, name, new): 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) @on_trait_change('hide_tab_bar') def _update_tab_bar(self): if self.control is not None: visible = self.control.GetPageCount() > 1 if self.hide_tab_bar else True pass # Can't actually hide the tab bar on wx.aui pyface-6.1.2/pyface/ui/wx/tasks/split_editor_area_pane.py0000644000076500000240000000132413462774552024464 0ustar cwebsterstaff00000000000000# Standard library imports. import sys # Enthought library imports. from traits.api import provides # Local imports. 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-6.1.2/pyface/ui/wx/tasks/__init__.py0000644000076500000240000000000013462774552021515 0ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/wx/tasks/task_pane.py0000644000076500000240000000245013462774552021736 0ustar cwebsterstaff00000000000000# Enthought library imports. from pyface.tasks.i_task_pane import ITaskPane, MTaskPane from traits.api import provides # System library imports. 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-6.1.2/pyface/ui/wx/tasks/dock_pane.py0000644000076500000240000001775513462774552021732 0ustar cwebsterstaff00000000000000# Standard library imports from contextlib import contextmanager import logging # Enthought library imports. from pyface.tasks.i_dock_pane import IDockPane, MDockPane from traits.api import Bool, on_trait_change, Property, provides, Tuple, Str, Int # System library imports. import wx from pyface.wx.aui import aui # Local imports. 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.GetSizeTuple() 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)) @on_trait_change('dock_area') def _set_dock_area(self): 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) @on_trait_change('closable,floatable,movable,caption_visible,dock_layer') def _set_dock_features(self): 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) @on_trait_change('name') def _set_dock_title(self): 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() @on_trait_change('floating') def _set_floating(self): 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() @on_trait_change('visible') def _set_visible(self): 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-6.1.2/pyface/ui/wx/tasks/main_window_layout.py0000644000076500000240000001510213462774552023677 0ustar cwebsterstaff00000000000000# Standard library imports. from itertools import combinations import logging # Enthought library imports. from traits.api import Any, HasTraits # Local imports. from dock_pane import AREA_MAP, INVERSE_AREA_MAP from pyface.tasks.task_layout import LayoutContainer, PaneItem, Tabbed, \ Splitter, HSplitter, VSplitter # 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-6.1.2/pyface/ui/wx/tasks/editor.py0000644000076500000240000000253513462774552021263 0ustar cwebsterstaff00000000000000# Enthought library imports. from pyface.tasks.i_editor import IEditor, MEditor from traits.api import Bool, Property, provides # System library imports. 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-6.1.2/pyface/ui/wx/confirmation_dialog.py0000644000076500000240000001043513462774552022655 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: Enthought, Inc. # #------------------------------------------------------------------------------ """ Enthought pyface package component """ # Major package imports. import wx # Enthought library imports. from traits.api import Bool, Enum, Instance, provides, Unicode # Local imports. from pyface.i_confirmation_dialog import IConfirmationDialog, MConfirmationDialog from pyface.constant import CANCEL, YES, NO from pyface.image_resource import ImageResource from .dialog import Dialog @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 = Instance(ImageResource) message = Unicode informative = Unicode detail = Unicode no_label = Unicode yes_label = Unicode ########################################################################### # 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() wx.EVT_BUTTON(parent, wx.ID_YES, self._on_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() wx.EVT_BUTTON(parent, wx.ID_NO, self._on_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() wx.EVT_BUTTON(parent, wx.ID_CANCEL, self._wx_on_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) #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/single_choice_dialog.py0000644000076500000240000000605713462774552022765 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # # Copyright (c) 2016, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: Enthought, Inc. # #------------------------------------------------------------------------------ """ 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(SingleChoiceDialog, self).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-6.1.2/pyface/ui/wx/widget.py0000644000076500000240000000434513462774552020134 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: Enthought, Inc. # #------------------------------------------------------------------------------ """ Enthought pyface package component """ # Enthought library imports. from traits.api import Any, Bool, HasTraits, provides # Local imports. 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) # ------------------------------------------------------------------------ # '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 destroy(self): if self.control is not None: self.control.Destroy() self.control = None pyface-6.1.2/pyface/ui/wx/directory_dialog.py0000644000076500000240000000534213462774552022172 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: Enthought, Inc. # #------------------------------------------------------------------------------ """ Enthought pyface package component """ # Major package imports. import wx # Enthought library imports. from traits.api import Bool, provides, Unicode # Local imports. from pyface.i_directory_dialog import IDirectoryDialog, MDirectoryDialog from .dialog import Dialog import six @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 = Unicode message = Unicode new_directory = Bool(True) path = Unicode ########################################################################### # 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 = six.text_type(self.control.GetPath()) # Let the window close as normal. super(DirectoryDialog, self).close() ########################################################################### # Protected 'IWidget' interface. ########################################################################### def _create_control(self, parent): # The default style. style = wx.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) #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/message_dialog.py0000644000076500000240000000475213462774552021616 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: Enthought, Inc. # #------------------------------------------------------------------------------ """ Enthought pyface package component """ # Major package imports. import wx # Enthought library imports. from traits.api import Enum, provides, Unicode # Local imports. 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 = Unicode informative = Unicode detail = Unicode severity = Enum('information', 'warning', 'error') ########################################################################### # 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-6.1.2/pyface/ui/wx/window.py0000644000076500000240000001507213500704207020136 0ustar cwebsterstaff00000000000000# Copyright (c) 2005-18, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ Enthought pyface package component """ # Major package imports. import wx # Enthought library imports. from traits.api import Event, Property, Tuple, Unicode, VetoableEvent, provides # Local imports. 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 = Unicode # 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: Unicode key_pressed = Event(KeyPressedEvent) # 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): 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. self._position = event.GetEventObject().GetPositionTuple() 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.m_altDown == 1, control_down=event.m_controlDown == 1, shift_down=event.m_shiftDown == 1, key_code=event.m_keyCode, event=event ) event.Skip() pyface-6.1.2/pyface/ui/wx/image_resource.py0000644000076500000240000000646413462774552021646 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: Enthought, Inc. # #------------------------------------------------------------------------------ """ Enthought pyface package component """ # Standard library imports. import os # Major package imports. import wx # Enthought library imports. from traits.api import Any, HasTraits, List, Property, provides from traits.api import Unicode # Local imports. from pyface.i_image_resource import IImageResource, MImageResource @provides(IImageResource) class ImageResource(MImageResource, HasTraits): """ The toolkit specific implementation of an ImageResource. See the IImageResource interface for the API documentation. """ #### Private interface #################################################### # The resource manager reference for the image. _ref = Any #### 'ImageResource' interface ############################################ absolute_path = Property(Unicode) name = Unicode search_path = List ########################################################################### # 'ImageResource' 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_ICO) 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.EmptyIcon() 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 #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/multi_toolbar_window.py0000644000076500000240000001316713462774552023116 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A top-level application window that supports multiple toolbars. """ from __future__ import absolute_import # Major package imports. import wx # Enthought library imports from pyface.action.api import ToolBarManager from traits.api import Trait, TraitDict, TraitEnum, TraitList # Local imports 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 = Trait([], TraitList(Trait(ToolBarManager))) # Map of toolbar to screen location. _tool_bar_locations = Trait({}, TraitDict(Trait(ToolBarManager), TraitEnum('top', 'bottom', 'left', 'right'))) ########################################################################### # Protected 'Window' interface. ########################################################################### def _create_contents(self, parent): panel = super(MultiToolbarWindow, self)._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) return 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 #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/list_box.py0000644000076500000240000000772413462774552020500 0ustar cwebsterstaff00000000000000""" A simple list box widget with a model-view architecture. """ from __future__ import absolute_import # Major package imports. import wx # Enthought library imports. from traits.api import Event, Instance, Int # Local imports. from pyface.list_box_model import ListBoxModel from .widget import Widget class ListBox(Widget): """ 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, **traits): """ Creates a new list box. """ # Base-class constructors. super(ListBox, self).__init__(**traits) # Create the widget! self._create_control(parent) # Listen for changes to the model. self.model.on_trait_change(self._on_model_changed, "list_changed") return def dispose(self): self.model.on_trait_change(self._on_model_changed, "list_changed", remove = True) self.model.dispose() return ########################################################################### # '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() return ########################################################################### # 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() return 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 return ########################################################################### # Trait handlers. ########################################################################### #### Static ############################################################### def _selection_changed(self, index): """ Called when the selected item is changed. """ if index != -1: self.control.SetSelection(index) return #### Dynamic ############################################################## def _on_model_changed(self, event): """ Called when the model has changed. """ # For now we just clear out the entire list. self.refresh() return ########################################################################### # Private interface. ########################################################################### def _create_control(self, parent): """ Creates the widget. """ self.control = wx.ListBox(parent, -1, style = self.STYLE) # Wire it up! wx.EVT_LISTBOX(self.control, self.control.GetId(), self._on_item_selected) wx.EVT_LISTBOX_DCLICK(self.control, self.control.GetId(), self._on_item_activated) # Populate the list. self._populate() return 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) return #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/system_metrics.py0000644000076500000240000000403013462774552021712 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: Enthought, Inc. # #------------------------------------------------------------------------------ """ Enthought pyface package component """ # Standard library imports. import sys # Major package imports. import wx # Enthought library imports. from traits.api import HasTraits, Int, Property, provides, Tuple # Local imports. 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., color[1]/255., color[2]/255.) #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/dialog.py0000644000076500000240000001232713462774552020107 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: Enthought, Inc. # #------------------------------------------------------------------------------ """ Enthought pyface package component """ # Major package imports. import sys import wx # Enthought library imports. from traits.api import Bool, Enum, Int, provides, Str, Unicode # Local imports. 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 = Unicode help_id = Str help_label = Unicode ok_label = Unicode resizeable = Bool(True) return_code = Int(OK) style = Enum('modal', 'nonmodal') #### 'IWindow' interface ################################################## title = Unicode("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() wx.EVT_BUTTON(parent, wx.ID_OK, self._wx_on_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) wx.EVT_BUTTON(parent, wx.ID_CANCEL, self._wx_on_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) wx.EVT_BUTTON(parent, wx.ID_HELP, self._wx_on_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-6.1.2/pyface/ui/wx/images/0000755000076500000240000000000013515277237017533 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/wx/images/heading_level_1.png0000644000076500000240000000424013462774552023252 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-6.1.2/pyface/ui/wx/images/warning.png0000644000076500000240000000257713462774552021724 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?9uF{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-6.1.2/pyface/ui/wx/heading_text.py0000644000076500000240000001114313462774552021306 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: Enthought, Inc. # #------------------------------------------------------------------------------ """ Enthought pyface package component """ # Major package imports. import wx # Enthought library imports. from traits.api import Instance, Int, provides, Unicode # Local imports. from pyface.i_heading_text import IHeadingText, MHeadingText from pyface.image_resource import ImageResource from pyface.wx.util.font_helper import new_font_like from .widget import Widget @provides(IHeadingText) class HeadingText(MHeadingText, Widget): """ The toolkit specific implementation of a HeadingText. See the IHeadingText interface for the API documentation. """ #### 'IHeadingText' interface ############################################# level = Int(1) text = Unicode('Default') image = Instance(ImageResource, ImageResource('heading_level_1')) ########################################################################### # 'object' interface. ########################################################################### def __init__(self, parent, **traits): """ Creates the panel. """ # Base class constructor. super(HeadingText, self).__init__(**traits) # Create the toolkit-specific control that represents the widget. self.control = self._create_control(parent) return ########################################################################### # Private interface. ########################################################################### def _create_control(self, parent): """ Create the toolkit-specific control that represents the widget. """ # The background image (it is tiled). image = self.image.create_image() self._bmp = image.ConvertToBitmap() sizer = wx.BoxSizer(wx.VERTICAL) panel = wx.Panel(parent, -1, style=wx.CLIP_CHILDREN | wx.SIMPLE_BORDER) panel.SetSizer(sizer) panel.SetAutoLayout(True) # Create a suitable font. self._font = new_font_like(wx.NORMAL_FONT, family=wx.SWISS) width, height = self._get_preferred_size(self.text, self._font) panel.SetMinSize((width, height)) wx.EVT_PAINT(panel, self._on_paint_background) wx.EVT_ERASE_BACKGROUND(panel, self._on_erase_background) return panel def _get_preferred_size(self, text, font): """ Calculates the preferred size of the widget. """ dc = wx.ScreenDC() dc.SetFont(font) width, height = dc.GetTextExtent(text) return (width + 10, height + 10) 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 return #### Trait event handlers ################################################# def _text_changed(self, new): """ Called when the text is changed. """ if self.control is not None: self.control.Refresh() return #### wx event handlers #################################################### def _on_paint_background(self, event): """ Called when the background of the panel is painted. """ dc = wx.PaintDC(self.control) size = self.control.GetClientSize() # Tile the background image. self._tile_background_image(dc, size.width, size.height) # Render the text. dc.SetFont(self._font) dc.DrawText(self.text, 5, 4) return def _on_erase_background(self, event): """ Called when the background of the panel is erased. """ dc = event.GetDC() size = self.control.GetClientSize() # Tile the background image. self._tile_background_image(dc, size.width, size.height) # Render the text. dc.SetFont(self._font) dc.DrawText(self.text, 5, 4) return #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/timer/0000755000076500000240000000000013515277237017406 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/wx/timer/timer.py0000644000076500000240000000231113462774552021100 0ustar cwebsterstaff00000000000000# Copyright (c) 2006-18, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: Prabhu Ramachandran """A `wx.Timer` subclass that invokes a specified callback periodically. """ import wx from traits.api import Bool, Instance, Property from pyface.timer.i_timer import BaseTimer class CallbackTimer(wx.Timer): def __init__(self, timer): super(CallbackTimer, self).__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-6.1.2/pyface/ui/wx/timer/__init__.py0000644000076500000240000000034513462774552021524 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # # Copyright (c) 2007 by Enthought, Inc. # All rights reserved. # #------------------------------------------------------------------------------ pyface-6.1.2/pyface/ui/wx/timer/do_later.py0000644000076500000240000000101113462774552021545 0ustar cwebsterstaff00000000000000# Copyright (c) 2018, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ DoLaterTimer class Provided for backward compatibility. """ from pyface.timer.do_later import DoLaterTimerpyface-6.1.2/pyface/ui/wx/tests/0000755000076500000240000000000013515277237017430 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/wx/tests/__init__.py0000644000076500000240000000000013462774552021532 0ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/wx/tests/bad_import.py0000644000076500000240000000024013462774552022121 0ustar cwebsterstaff00000000000000# 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-6.1.2/pyface/ui/wx/wizard/0000755000076500000240000000000013515277240017560 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/wx/wizard/wizard_page.py0000644000076500000240000000550413462774552022443 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A page in a wizard. """ # Major package imports. import wx # Enthought library imports. from traits.api import Bool, HasTraits, provides, Str, Tuple, Unicode 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 = Unicode subheading = Unicode 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 #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/wizard/__init__.py0000644000076500000240000000034513462774552021704 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # # Copyright (c) 2007 by Enthought, Inc. # All rights reserved. # #------------------------------------------------------------------------------ pyface-6.1.2/pyface/ui/wx/wizard/wizard.py0000644000076500000240000001362613462774552021453 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The base class for all pyface wizards. """ # Major package imports. import wx # Enthought library imports. from traits.api import Bool, Instance, List, Property, provides, Unicode 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 = Unicode('Wizard') ########################################################################### # Protected 'IDialog' interface. ########################################################################### def _create_dialog_area(self, parent): """ Creates the main content of the dialog. """ self._layered_panel = panel = LayeredPanel(parent) # 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") wx.EVT_BUTTON(parent, back.GetId(), self._on_back) sizer.Add(back, 0) # 'Next' button. self._next = next = wx.Button(parent, -1, "Next") wx.EVT_BUTTON(parent, next.GetId(), self._on_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) wx.EVT_BUTTON(parent, wx.ID_OK, self._wx_on_ok) sizer.Add(finish, 0, wx.LEFT, 5) # 'Cancel' button. if self.show_cancel: self._cancel = cancel = wx.Button(parent, wx.ID_CANCEL, "Cancel") wx.EVT_BUTTON(parent, wx.ID_CANCEL, self._wx_on_cancel) sizer.Add(cancel, 0, wx.LEFT, 10) # 'Help' button. if len(self.help_id) > 0: help = wx.Button(parent, wx.ID_HELP, "Help") wx.EVT_BUTTON(parent, wx.ID_HELP, self._wx_on_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 return def _update(self): """ 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() return #### 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() return def _on_back(self, event): """ Called when the 'Back' button is pressed. """ self.previous() return #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/resource_manager.py0000644000076500000240000000471613462774552022174 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: Enthought, Inc. # #------------------------------------------------------------------------------ """ Enthought pyface package component """ # Standard library imports. import os import tempfile from six.moves import cStringIO as StringIO # Major package imports. import wx # Enthought library imports. from pyface.resource.api import ResourceFactory from traits.api import Undefined 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. """ try: return wx.ImageFromStream(StringIO(data)) except: # wx.ImageFromStream is only in wx 2.8 or later(?) if filename is Undefined: return None 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 #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/__init__.py0000644000076500000240000000034513462774552020404 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # # Copyright (c) 2007 by Enthought, Inc. # All rights reserved. # #------------------------------------------------------------------------------ pyface-6.1.2/pyface/ui/wx/splash_screen.py0000644000076500000240000001041313462774552021473 0ustar cwebsterstaff00000000000000 #------------------------------------------------------------------------------ # # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: Enthought, Inc. # #------------------------------------------------------------------------------ """ Enthought pyface package component """ # Standard library imports. from logging import DEBUG # Major package imports. import wx # Enthought library imports. from traits.api import Any, Bool, Font, Instance, Int, provides from traits.api import Tuple, Unicode # Local imports. from pyface.i_splash_screen import ISplashScreen, MSplashScreen from pyface.image_resource import ImageResource from pyface.wx.util.font_helper import new_font_like 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 = Instance(ImageResource, ImageResource('splash')) log_level = Int(DEBUG) show_log_messages = Bool(True) text = Unicode 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.SplashScreen( # The bitmap to display on the splash screen. image.ConvertToBitmap(), # Splash Style. wx.SPLASH_NO_TIMEOUT | wx.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. wx.EVT_PAINT(splash_screen, 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() #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/expandable_header.py0000644000076500000240000001753713462774552022273 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ 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. """ from __future__ import absolute_import # Major package imports. import wx # Enthought library imports. from traits.api import Instance, Event, Str, Bool # local imports from pyface.wx.util.font_helper import new_font_like 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 = Instance(ImageResource, ImageResource('carat_closed')) # The carat image to show when the panel is expanded. expanded_carat_image = Instance(ImageResource, ImageResource('carat_open')) # The backing header image when the mouse is elsewhere header_bar_image = Instance(ImageResource, ImageResource('panel_gradient')) # The backing header image when the mouse is over header_mouseover_image = Instance(ImageResource, ImageResource('panel_gradient_over')) # The carat image to show when the panel is expanded. remove_image = Instance(ImageResource, ImageResource('close')) # Represents the current state of the button. True means pressed. state = Bool(False) #### Events #### # The panel has been expanded or collapsed panel_expanded = Event _CARAT_X = 4 _CARAT_Y = 2 _TEXT_Y = 0 _TEXT_X_OFFSET = 10 ########################################################################### # 'object' interface. ########################################################################### def __init__(self, parent, container, **traits): """ Creates the panel. """ # Base class constructor. super(ExpandableHeader, self).__init__(**traits) # Create the toolkit-specific control that represents the widget. self.control = self._create_control(parent) self._container = container return ########################################################################### # 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() header_bar = self.header_bar_image.create_image() self._header_bmp = header_bar.ConvertToBitmap() header_bar_over = self.header_mouseover_image.create_image() self._header_mouseover_bmp = header_bar_over.ConvertToBitmap() self._background_bmp = self._header_bmp close_image = self.remove_image.create_image() self._remove_bmp = close_image.ConvertToBitmap() # create our panel and initialize it appropriately sizer = wx.BoxSizer(wx.VERTICAL) panel = wx.Panel(parent, -1, style=wx.CLIP_CHILDREN) panel.SetSizer(sizer) panel.SetAutoLayout(True) # needed on GTK systems for EVT_ERASE_BACKGROUND to work panel.SetBackgroundStyle(wx.BG_STYLE_CUSTOM) # create the remove button remove = wx.BitmapButton(panel, -1, self._remove_bmp, style=0, pos=(-1, 3)) 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.SetSize((-1, height)) wx.EVT_ERASE_BACKGROUND(panel, self._on_erase_background) wx.EVT_ENTER_WINDOW(panel, self._on_enter_leave) wx.EVT_LEAVE_WINDOW(panel, self._on_enter_leave) wx.EVT_LEFT_DOWN(panel, self._on_down) wx.EVT_RIGHT_DOWN(panel, self._on_down) wx.EVT_BUTTON(panel, remove.GetId(), 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) text_w, text_h = dc.GetTextExtent(text) text_h = text_h + self._TEXT_Y # add in width of buttons carat_h = self._collapsed_bmp.GetHeight() + 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) return def _tile_background_image(self, dc, width, height): """ Tiles the background image. """ w = self._background_bmp.GetWidth() h = self._background_bmp.GetHeight() x = 0 while x < width: y = 0 while y < height: dc.DrawBitmap(self._background_bmp, x, y) y = y + h x = x + w return 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. """ size = self.control.GetClientSize() # Tile the background image. self._tile_background_image(dc, size.width, size.height) self._draw_title(dc) # Draw the carat button self._draw_carat_button(dc) return ########################################################################### # wx event handlers. ########################################################################### def _on_erase_background(self, event): """ Called when the background of the panel is erased. """ #print 'ImageButton._on_erase_background' dc = event.GetDC() self._draw(dc) return def _on_enter_leave(self, event): """ Called when button is pressed. """ #print 'ExpandableHeader._on_enter_leave' if event.Entering(): self._background_bmp = self._header_mouseover_bmp else: self._background_bmp = self._header_bmp self.control.Refresh() event.Skip() def _on_down(self, event): """ Called when button is pressed. """ #print 'ImageButton._on_down' 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._container.remove_panel(self.title) pyface-6.1.2/pyface/ui/wx/preference/0000755000076500000240000000000013515277237020404 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/wx/preference/__init__.py0000644000076500000240000000000013462774552022506 0ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/wx/preference/preference_dialog.py0000644000076500000240000001635413462774552024427 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005-2015, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The preference dialog. """ # Major package imports. import wx # Enthought library imports. from traits.api import Any, Dict, Float, Instance, Str # Local imports. 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 from pyface.wx.util.font_helper import new_font_like 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() wx.EVT_BUTTON(parent, wx.ID_OK, self._wx_on_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(panel) 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(panel, min_width=-1, min_height=-1) 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.on_trait_change(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") wx.EVT_BUTTON(parent, help.GetId(), self._on_help) sizer.Add(help, 0, wx.RIGHT, 5) # 'Restore Defaults' button. restore = wx.Button(parent, -1, "Restore Defaults") wx.EVT_BUTTON(parent, restore.GetId(), self._on_restore_defaults) 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() return 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, selection): """ Called when a node in the tree is selected. """ 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 #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/application_window.py0000644000076500000240000002035613500704207022522 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: Enthought, Inc. # #------------------------------------------------------------------------------ """ Enthought pyface package component """ # Standard library imports. import sys # Major package imports. import wx from pyface.wx.aui import aui, PyfaceAuiManager # Enthought library imports. from pyface.action.api import MenuBarManager, StatusBarManager from pyface.action.api import ToolBarManager from traits.api import Instance, List, on_trait_change, provides, Unicode from pyface.i_application_window import IApplicationWindow from pyface.i_application_window import MApplicationWindow from pyface.image_resource import ImageResource # Local imports. 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 = Instance(ImageResource) 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 = Unicode("Pyface") ########################################################################### # Protected 'IApplicationWindow' interface. ########################################################################### def _create_contents(self, parent): panel = wx.Panel(parent, -1, name="ApplicationWindow") panel.SetSize((500, 400)) 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) return 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()) return ########################################################################### # 'Window' interface. ########################################################################### def _size_default(self): """ Trait initialiser. """ return (800, 600) ########################################################################### # Protected 'IWidget' interface. ########################################################################### def _create(self): super(ApplicationWindow, self)._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._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() return 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(ApplicationWindow, self).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) return 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. return 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) @on_trait_change('tool_bar_manager, tool_bar_managers') def _update_tool_bar_managers(self): if self.control is not None: self._create_tool_bar(self.control) def _icon_changed(self): self._set_window_icon() #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/progress_dialog.py0000644000076500000240000003025513462774552022033 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: Enthought, Inc. # #------------------------------------------------------------------------------ """ A simple progress bar intended to run in the UI thread """ import wx import time # Enthought library imports from traits.api import Bool, Enum, Instance, Int, Property, provides, Str # Local imports from pyface.i_progress_dialog import IProgressDialog, MProgressDialog 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 = Enum('horizontal', 'horizontal', 'vertical') #: The maximum value for the progress bar. _max = Int def __init__(self, parent, minimum=0, maximum=100, direction='horizontal', size=(200, -1)): """ Constructs a progress bar which can be put into a panel, or optionaly, its own window """ self._max = maximum self.parent = parent style = wx.GA_HORIZONTAL if direction == "vertical": style = wx.GA_VERTICAL self.control = wx.Gauge(parent, -1, maximum, style=style, size=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(ProgressDialog, self).__init__( *args, **kw) #------------------------------------------------------------------------- # IWindow Interface #------------------------------------------------------------------------- def open(self): """ Opens the window. """ super(ProgressDialog, self).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(ProgressDialog, self).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 == True: # 'Cancel' button. self._cancel = cancel = wx.Button(dialog, wx.ID_CANCEL, self.cancel_button_label) wx.EVT_BUTTON(dialog, wx.ID_CANCEL, self._on_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) 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) dialog.SetBackgroundColour(wx.NullColour) 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-6.1.2/pyface/ui/wx/layered_panel.py0000644000076500000240000001326113462774552021452 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A Layered panel. """ from __future__ import absolute_import # Major package imports. import wx from wx.lib.scrolledpanel import ScrolledPanel # Enthought library imports. from traits.api import Any, Str, Int # Local imports. from .widget import Widget class LayeredPanel(Widget): """ 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 #### "Layered Panel' interface ############################################ # The toolkit-specific control of the currently displayed layer. current_layer = Any # The name of the currently displayed layer. current_layer_name = Str # The minimum for the panel, which is the maximum of the minimum # sizes of the layers min_width = Int(0) min_height = Int(0) ########################################################################### # 'object' interface. ########################################################################### def __init__(self, parent, **traits): """ Creates a new LayeredPanel. """ # Base class constructor. super(LayeredPanel, self).__init__(**traits) # Create the toolkit-specific control that represents the widget. self.control = self._create_control(parent) # The layers in the panel. # # { str name : wx.Window layer } self._layers = {} return ########################################################################### # '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 def has_layer(self, name): """ Does the panel contain a layer with the specified name? """ return name in self._layers ########################################################################### # 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() return 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 #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/action/0000755000076500000240000000000013515277237017543 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/wx/action/tool_bar_manager.py0000644000076500000240000003372713462774552023427 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: Enthought, Inc. # #------------------------------------------------------------------------------ """ The wx specific implementation of the tool bar manager. """ # Major package imports. import wx # Enthought library imports. from traits.api import Bool, Enum, Instance, Str, Tuple # Local imports. from pyface.wx.aui import 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(ToolBarManager, self).__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. ########################################################################### #### Trait change handlers ################################################ #### Methods ############################################################## 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.TB_FLAT | wx.CLIP_CHILDREN 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 # Create the control. if aui: tool_bar = _AuiToolBar(self, parent, -1, style=style) else: 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 ) return 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 return 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.on_trait_change( self._on_tool_bar_manager_enabled_changed, 'enabled' ) self.tool_bar_manager.on_trait_change( self._on_tool_bar_manager_visible_changed, 'visible' ) return ########################################################################### # Trait change handlers. ########################################################################### def _on_tool_bar_manager_enabled_changed(self, obj, trait_name, old, new): """ Dynamic trait change handler. """ obj.window._wx_enable_tool_bar(self, new) return def _on_tool_bar_manager_visible_changed(self, obj, trait_name, old, new): """ Dynamic trait change handler. """ obj.window._wx_show_tool_bar(self, new) return class _AuiToolBar(aui.AuiToolBar): """ The toolkit-specific tool bar implementation for AUI windows. """ ########################################################################### # 'object' interface. ########################################################################### def __init__(self, tool_bar_manager, parent, id, style): """ Constructor. """ aui.AuiToolBar.__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.on_trait_change( self._on_tool_bar_manager_enabled_changed, 'enabled' ) self.tool_bar_manager.on_trait_change( 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 = {} return 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 wx.CallAfter(self.tool_bar_manager.controller.task.window._aui_manager.Update) elif not state and tool is not None: self.RemoveTool(tool_id) # Update the toolbar in the AUI manager to force toolbar resize wx.CallAfter(self.tool_bar_manager.controller.task.window._aui_manager.Update) def InsertToolInOrder(self, tool_id): orig_pos, tool = self.tool_map[tool_id] for pos in range(self.GetToolsCount()): existing_tool = self.GetToolByPos(pos) existing_id = existing_tool.GetId() 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, obj, trait_name, old, new): """ Dynamic trait change handler. """ obj.controller.task.window._wx_enable_tool_bar(self, new) return def _on_tool_bar_manager_visible_changed(self, obj, trait_name, old, new): """ Dynamic trait change handler. """ obj.controller.task.window._wx_show_tool_bar(self, new) return #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/action/menu_manager.py0000644000076500000240000001553313462774552022565 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: Enthought, Inc. # #------------------------------------------------------------------------------ """ The wx specific implementation of a menu manager. """ # Major package imports. import wx # Enthought library imports. from traits.api import Unicode, Bool # Local imports. 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 = Unicode # 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. """ id = wx.NewId() sub = self.create_menu(parent, controller) # fixme: Nasty hack to allow enabling/disabling of menus. sub._id = id sub._menu = menu menu.AppendMenu(id, self.name, sub) return 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.on_trait_change(self.refresh, 'changed') self._manager.on_trait_change(self._on_enabled_changed, 'enabled') return ########################################################################### # '_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 = [] return def is_empty(self): """ Is the menu empty? """ return self.GetMenuItemCount() == 0 def refresh(self): """ 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 ) return 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.PopupMenuXY(self, x, y) return ########################################################################### # Private interface. ########################################################################### def _on_enabled_changed(self, obj, trait_name, old, new): """ 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, new) return 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 #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/action/tool_palette_manager.py0000644000076500000240000001166513462774552024316 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: Enthought, Inc. # #------------------------------------------------------------------------------ """ A tool bar manager realizes itself in a tool palette control. """ # Major package imports. import wx # Enthought library imports. from traits.api import Any, Bool, Enum, Instance, Tuple # Local imports. 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(ToolPaletteManager, self).__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() return 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 #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/action/status_bar_manager.py0000644000076500000240000000717613462774552023774 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: Enthought, Inc. # #------------------------------------------------------------------------------ """ A status bar manager realizes itself in a status bar control. """ # Major package imports. import wx # Enthought library imports. from traits.api import Any, HasTraits, List, Property, Str, Unicode 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(Unicode) # 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) return 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 #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/action/__init__.py0000644000076500000240000000034513462774552021661 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # # Copyright (c) 2007 by Enthought, Inc. # All rights reserved. # #------------------------------------------------------------------------------ pyface-6.1.2/pyface/ui/wx/action/tool_palette.py0000644000076500000240000001370613462774552022622 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: Enthought, Inc. # #------------------------------------------------------------------------------ """ 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(ToolPalette, self).__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.NewId() 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) return 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) return 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) return def realize(self): """ Realize the control so that it can be displayed. """ self.is_realized = True self._reflow() return 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) return 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 wx.EVT_BUTTON(panel, button.GetId(), self._on_button) button.SetBitmapLabel(bmp) button.SetToolTipString(label) sizer.Add(button, 0, wx.EXPAND) return 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 #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/action/action_item.py0000644000076500000240000005500313462774552022416 0ustar cwebsterstaff00000000000000# Copyright (c) 2005-19, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: Enthought, Inc. """ The wx specific implementations the action manager internal classes. """ # Standard libary imports. import six if six.PY2: from inspect import getargspec else: # avoid deprecation warning from inspect import getfullargspec as getargspec # Major package imports. import wx # Enthought library imports. from traits.api import Any, Bool, HasTraits # Local imports. 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.NewId() 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: # Some platforms don't allow radio buttons to have # bitmaps, so just ignore the exception if it happens pass menu.AppendItem(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 wx.EVT_MENU(parent, self.control_id, self._on_menu) # Listen for trait changes on the action (so that we can update its # enabled/disabled/checked state etc). action.on_trait_change(self._on_action_enabled_changed, 'enabled') action.on_trait_change(self._on_action_visible_changed, 'visible') action.on_trait_change(self._on_action_checked_changed, 'checked') action.on_trait_change(self._on_action_name_changed, 'name') if controller is not None: self.controller = controller controller.add_to_menu(self) return def dispose(self): action = self.item.action action.on_trait_change(self._on_action_enabled_changed, 'enabled', remove=True) action.on_trait_change(self._on_action_visible_changed, 'visible', remove=True) action.on_trait_change(self._on_action_checked_changed, 'checked', remove=True) action.on_trait_change(self._on_action_name_changed, 'name', 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) return def _visible_changed(self): """ Called when our 'visible' trait is changed. """ self.control.Enable(self.visible and self.enabled) return 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) return def _on_action_enabled_changed(self, action, trait_name, old, new): """ Called when the enabled trait is changed on an action. """ self.control.Enable(action.enabled and action.visible) return def _on_action_visible_changed(self, action, trait_name, old, new): """ Called when the visible trait is changed on an action. """ self.control.Enable(action.visible and action.enabled) return def _on_action_checked_changed(self, action, trait_name, old, new): """ Called when the checked trait is changed on an action. """ 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 return def _on_action_name_changed(self, action, trait_name, old, new): """ Called when the name trait is changed on an action. """ label = action.name if len(action.accelerator) > 0: label = label + '\t' + action.accelerator self.control.SetText(label) 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 = getargspec(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 = getargspec(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) return 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('foo') 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_id = wx.NewId() self.control = tool_bar.AddLabelTool( self.control_id, label, bmp, wx.NullBitmap, kind, tooltip, longtip, None ) # 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. wx.EVT_TOOL(parent, self.control_id, self._on_tool) # Listen for trait changes on the action (so that we can update its # enabled/disabled/checked state etc). action.on_trait_change(self._on_action_enabled_changed, 'enabled') action.on_trait_change(self._on_action_visible_changed, 'visible') action.on_trait_change(self._on_action_checked_changed, 'checked') if controller is not None: self.controller = controller controller.add_to_toolbar(self) ########################################################################### # 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) return def _on_action_enabled_changed(self, action, trait_name, old, new): """ Called when the enabled trait is changed on an action. """ 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, action, trait_name, old, new): """ Called when the visible trait is changed on an action. """ 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, action, trait_name, old, new): """ Called when the checked trait is changed on an action. """ 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 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, 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() is_checkable = (action.style == 'radio' or action.style == 'check') # 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 = getargspec(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 = getargspec(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) return 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('&', '') image = action.image.create_image() 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.on_trait_change(self._on_action_enabled_changed, 'enabled') action.on_trait_change(self._on_action_checked_changed, 'checked') return ########################################################################### # Private interface. ########################################################################### #### Trait event handlers ################################################# def _on_action_enabled_changed(self, action, trait_name, old, new): """ Called when the enabled trait is changed on an action. """ self.tool_palette.enable_tool(self.tool_id, action.enabled) return def _on_action_checked_changed(self, action, trait_name, old, new): """ Called when the checked trait is changed on an action. """ 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 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, 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() is_checkable = (action.style == 'radio' or action.style == 'check') # Perform the action! action.checked = self.tool_palette.get_tool_state(self.tool_id) == 1 action.perform(action_event) return #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/action/menu_bar_manager.py0000644000076500000240000000357113462774552023410 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: Enthought, Inc. # #------------------------------------------------------------------------------ """ The wx specific implementation of a menu bar manager. """ # Major package imports. import wx # Local imports. 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 #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/viewer/0000755000076500000240000000000013515277237017567 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/wx/viewer/table_viewer.py0000644000076500000240000002775713462774552022636 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A viewer for tabular data. """ # Major package imports. import wx # Enthought library imports. from traits.api import Color, Event, Instance, Trait # Local imports. from pyface.ui.wx.image_list import ImageList 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 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, factory = TableLabelProvider) # The column provider provides information about the columns in the table # (column headers, width etc). column_provider=Trait(TableColumnProvider(),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 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. """ # Base-class constructor. super(TableViewer, self).__init__(**traits) # Create the toolkit-specific control. self.control = table = _Table(parent, image_size, self) # Get our actual id. wxid = table.GetId() # Table events. wx.EVT_LIST_ITEM_SELECTED(table, wxid, self._on_item_selected) wx.EVT_LIST_ITEM_ACTIVATED(table, wxid, self._on_item_activated) wx.EVT_LIST_BEGIN_DRAG(table, wxid, self._on_list_begin_drag) wx.EVT_LIST_BEGIN_RDRAG(table, wxid, self._on_list_begin_rdrag) wx.EVT_LIST_BEGIN_LABEL_EDIT( table, wxid, self._on_list_begin_label_edit ) wx.EVT_LIST_END_LABEL_EDIT( table, wxid, 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^(). wx.EVT_LIST_ITEM_DESELECTED(table, wxid, 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.on_trait_change(self._on_input_changed, 'input') return ########################################################################### # '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, obj, trait_name, old, new): """ Called when the input is changed. """ # Update the table contents. self._update_contents() if 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.m_itemIndex # 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 return 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.m_itemIndex # Trait event notification. self.row_activated = row return 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() return 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) return def _on_list_begin_label_edit(self, event=None): """ Called when a label edit is started. """ event.Veto() return 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.InsertColumnInfo(index, info) # Update the table contents and the column widths. self._update_contents() self._update_column_widths() return 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)) return 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) return 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): """ 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. self._even_row_attribute = wx.ListItemAttr() self._even_row_attribute.SetBackgroundColour( self._viewer.even_row_background ) self._odd_row_attribute = wx.ListItemAttr() 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 #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/viewer/__init__.py0000644000076500000240000000000013462774552021671 0ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/wx/viewer/tree_viewer.py0000644000076500000240000005134213462774552022471 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A viewer based on a tree control. """ # Major package imports. from __future__ import print_function import wx # Enthought library imports. from traits.api import Any, Bool, Enum, Event, Instance, List # Local imports. 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 ########################################################################### # '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. """ # Base class constructor. super(TreeViewer, self).__init__(**traits) # Create the toolkit-specific control. self.control = tree = wx.TreeCtrl(parent, -1, style=self._get_style()) # Get our actual Id. wxid = tree.GetId() # Wire up the wx tree events. wx.EVT_CHAR(tree, self._on_char) wx.EVT_LEFT_DOWN(tree, self._on_left_down) wx.EVT_RIGHT_DOWN(tree, self._on_right_down) wx.EVT_TREE_ITEM_ACTIVATED(tree, wxid, self._on_tree_item_activated) wx.EVT_TREE_ITEM_COLLAPSED(tree, wxid, self._on_tree_item_collapsed) wx.EVT_TREE_ITEM_COLLAPSING(tree, wxid, self._on_tree_item_collapsing) wx.EVT_TREE_ITEM_EXPANDED(tree, wxid, self._on_tree_item_expanded) wx.EVT_TREE_ITEM_EXPANDING(tree, wxid, self._on_tree_item_expanding) wx.EVT_TREE_BEGIN_LABEL_EDIT(tree, wxid,self._on_tree_begin_label_edit) wx.EVT_TREE_END_LABEL_EDIT(tree, wxid, self._on_tree_end_label_edit) wx.EVT_TREE_BEGIN_DRAG(tree, wxid, self._on_tree_begin_drag) wx.EVT_TREE_SEL_CHANGED(tree, wxid, self._on_tree_sel_changed) # The image list is a wxPython-ism that caches all images used in the # control. self._image_list = ImageList(image_size[0], image_size[1]) 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 ########################################################################### # '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.GetPyData(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.SetPyData(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!!! ****') return 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.SetPyData(wxid, (False, element)) else: self.control.SetPyData(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) return 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) return 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 'GetPyData' # 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.GetPyData(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.GetPyData(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.GetPyData(cid) del self._element_to_id_map[self._get_key(element)] self.control.SetPyData(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) return 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() return 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() return 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.GetPyData(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.SetPyData(wxid, (True, element)) else: event.Veto() return 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.GetPyData(wxid) # Make sure that the element's 'open' icon is displayed etc. self._refresh_element(wxid, element) # Trait notification. self.element_expanded = element return 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.GetPyData(wxid) # Give the label provider a chance to veto the collapse. if not self.label_provider.is_collapsible(self, element): event.Veto() return 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.GetPyData(wxid) # Make sure that the element's 'closed' icon is displayed etc. self._refresh_element(wxid, element) # Trait notification. self.element_collapsed = element return 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.GetPyData(wxid) # Trait notification. self.element_activated = element return def _on_tree_sel_changed(self, event): """ Called when the selection is changed. """ # Trait notification. self.selection = self._get_selection() return 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.GetPyData(wxid) if data is not None: populated, element = data # Trait notification. self.element_begin_drag = element return 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.GetPyData(wxid) # Give the label provider a chance to veto the edit. if not self.label_provider.is_editable(self, element): event.Veto() return 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.GetPyData(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() return def _on_char(self, event): """ Called when a key is pressed when the tree has focus. """ # Trait notification. self.key_pressed = event.GetKeyCode() return #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/workbench/0000755000076500000240000000000013515277240020242 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/wx/workbench/editor_set_structure_handler.py0000755000076500000240000000634013462774552026611 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: Enthought, Inc. # #------------------------------------------------------------------------------ """ The handler used to restore editors. """ # Standard library imports. import logging # Enthought library imports. 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.warn('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 #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/workbench/view_set_structure_handler.py0000755000076500000240000000440013462774552026270 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: Enthought, Inc. # #------------------------------------------------------------------------------ """ The handler used to restore views. """ # Standard library imports. import logging # Enthought library imports. 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.warn('could not restore view [%s]', id) control = None return control #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/workbench/__init__.py0000644000076500000240000000034513462774552022366 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # # Copyright (c) 2007 by Enthought, Inc. # All rights reserved. # #------------------------------------------------------------------------------ pyface-6.1.2/pyface/ui/wx/workbench/view.py0000644000076500000240000000411113462774552021574 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: Enthought, Inc. # #------------------------------------------------------------------------------ """ Enthought pyface package component """ # Enthought library imports. 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 return def set_focus(self): """ Set the focus to the appropriate control in the part. """ if self.control is not None: self.control.SetFocus() return #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/workbench/workbench_window_layout.py0000644000076500000240000006774013462774552025611 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: Enthought, Inc. # #------------------------------------------------------------------------------ """ The wx implementation of the workbench window layout interface. """ # Standard library imports. import six.moves.cPickle import logging # Major package imports. import wx # Enthought library imports. 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 # Mixin class imports. from pyface.workbench.i_workbench_window_layout import \ MWorkbenchWindowLayout # Local imports. 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() return 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) return 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() return def reset_editors(self): """ Activate the first editor in every group. """ self._wx_editor_dock_window.reset_regions() return def reset_views(self): """ Activate the first view in every group. """ self._wx_view_dock_window.reset_regions() return 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) return 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 return 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 six.moves.cPickle.loads(six.moves.cPickle.dumps(structure)) def set_view_memento(self, memento): # We always use a clone. memento = six.moves.cPickle.loads(six.moves.cPickle.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 return 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() return 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]) return 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) return def _wx_set_item_size(self, dock_control, size): """ Sets the size of a dock control. """ window_width, window_height = self.window.control.GetSize() width, height = size if width != -1: dock_control.width = int(window_width * width) if height != -1: dock_control.height = int(window_height * height) return 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() return 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) return 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() return 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) return 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) wx.EVT_SET_FOCUS(control, on_set_focus) if on_kill_focus is not None: #control.Bind(wx.EVT_KILL_FOCUS, on_kill_focus) wx.EVT_KILL_FOCUS(control, on_kill_focus) for child in control.GetChildren(): self._wx_add_focus_listeners(child, on_set_focus, on_kill_focus) return 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(editor, trait_name, old, new): editor_dock_control.id = editor.id return editor.on_trait_change(on_id_changed, 'id') def on_name_changed(editor, trait_name, old, new): editor_dock_control.set_name(editor.name) return editor.on_trait_change(on_name_changed, 'name') def on_activated_changed(editor_dock_control, trait_name, old, new): if editor_dock_control._editor is not None: editor_dock_control._editor.set_focus() return editor_dock_control.on_trait_change(on_activated_changed, 'activated') return 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(view, trait_name, old, new): view_dock_control.id = view.id return view.on_trait_change(on_id_changed, 'id') def on_name_changed(view, trait_name, old, new): view_dock_control.set_name(view.name) return view.on_trait_change(on_name_changed, 'name') def on_activated_changed(view_dock_control, trait_name, old, new): if view_dock_control._view is not None: view_dock_control._view.set_focus() return view_dock_control.on_trait_change(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.on_trait_change( self._wx_on_editor_area_size_changed, 'editor_area_size', remove=True ) if new is not None: new.on_trait_change( self._wx_on_editor_area_size_changed, 'editor_area_size', ) #### Dynamic #### def _wx_on_editor_area_size_changed(self, new): """ Dynamic trait change handler. """ window_width, window_height = self.window.control.GetSize() # 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(new[0] * window_width) region.height = int(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 #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/workbench/editor.py0000644000076500000240000000353313462774552022117 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: Enthought, Inc. # #------------------------------------------------------------------------------ """ Enthought pyface package component """ # Local imports. 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 return def set_focus(self): """ Set the focus to the appropriate control in the part. """ if self.control is not None: self.control.SetFocus() return #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/workbench/workbench_dock_window.py0000755000076500000240000001052513462774552025204 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: Enthought, Inc. # #------------------------------------------------------------------------------ """ Base class for workbench dock windows. """ # Standard library imports. import logging # Enthought library imports. 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) return 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) return 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) return def set_structure(self, structure, handler=None): """ Sets the window structure. """ sizer = self.control.GetSizer() sizer.SetStructure(self.control.GetParent(), structure, handler) return #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/split_widget.py0000644000076500000240000001200313462774552021335 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: Enthought, Inc. # #------------------------------------------------------------------------------ """ Mix-in class for split widgets. """ # Major package imports. import wx # Enthought library imports. from traits.api import Callable, Enum, Float, HasTraits, provides # Local imports. from pyface.i_split_widget import ISplitWidget, MSplitWidget @provides(ISplitWidget) class SplitWidget(MSplitWidget, HasTraits): """ The toolkit specific implementation of a SplitWidget. See the ISPlitWidget interface for the API documentation. """ #### 'ISplitWidget' interface ############################################# direction = Enum('vertical', 'vertical', 'horizontal') ratio = Float(0.5) lhs = Callable rhs = Callable ########################################################################### # 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. wx.EVT_SIZE(splitter, 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 hasattr(wx, 'WindowPtr'): if not isinstance(lhs, (wx.Window, wx.WindowPtr)): lhs = lhs.control else: # wx 2.8 did away with WindowPtr 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 hasattr(wx, 'WindowPtr'): if not isinstance(rhs, (wx.Window, wx.WindowPtr)): rhs = rhs.control else: # wx 2.8 did away with WindowPtr 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() # 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. #wx.EVT_SIZE(splitter, None) return #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/image_button.py0000644000076500000240000002354513462774552021331 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: David C. Morrill # # 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. """ from __future__ import absolute_import import wx from numpy import array, fromstring, reshape, ravel, dtype from traits.api import Str, Range, Enum, Instance, Event, false from .widget import Widget from .image_resource import ImageResource #------------------------------------------------------------------------------- # Constants: #------------------------------------------------------------------------------- # Text color used when a button is disabled: DisabledTextColor = wx.Colour( 128, 128, 128 ) #------------------------------------------------------------------------------- # 'ImageButton' class: #------------------------------------------------------------------------------- class ImageButton ( Widget ): """ 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.PENSTYLE_SOLID ) _selectedPenLight = wx.Pen( wx.SystemSettings.GetColour( wx.SYS_COLOUR_3DHIGHLIGHT ), 1, wx.PENSTYLE_SOLID ) #--------------------------------------------------------------------------- # Trait definitions: #--------------------------------------------------------------------------- # The image: image = Instance( ImageResource, allow_none = True ) # 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 = Enum( 'vertical', 'horizontal' ) # Is the control selected ('radio' or 'checkbox' style)? selected = false # Fired when a 'button' or 'toolbar' style control is clicked: clicked = Event #--------------------------------------------------------------------------- # Initializes the object: #--------------------------------------------------------------------------- def __init__ ( self, parent, **traits ): """ Creates a new image control. """ self._image = None super( ImageButton, self ).__init__( **traits ) # 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 self.control = wx.Window( parent, -1, size = wx.Size( self._dx, self._dy ) ) self.control._owner = self self._mouse_over = self._button_down = False # Set up mouse event handlers: wx.EVT_ENTER_WINDOW( self.control, self._on_enter_window ) wx.EVT_LEAVE_WINDOW( self.control, self._on_leave_window ) wx.EVT_LEFT_DOWN( self.control, self._on_left_down ) wx.EVT_LEFT_UP( self.control, self._on_left_up ) wx.EVT_PAINT( self.control, self._on_paint ) #--------------------------------------------------------------------------- # 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() 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.GetClientSizeTuple() 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.GetClientSizeTuple() 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(fromstring(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-6.1.2/pyface/ui/wx/fields/0000755000076500000240000000000013515277237017534 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/wx/fields/field.py0000644000076500000240000000635713462774552021207 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2017-19, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The Wx-specific implementation of the text field class """ from __future__ import absolute_import, print_function, unicode_literals from traits.api import Any, Instance, Unicode, provides import wx from pyface.fields.i_field import IField, MField from pyface.ui.wx.widget import Widget @provides(IField) class Field(MField, Widget): """ The Wxspecific 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 #: A tooltip for the field. tooltip = Unicode #: An optional context menu for the field. context_menu = Instance('pyface.action.menu_manager.MenuManager') # ------------------------------------------------------------------------ # IField interface # ------------------------------------------------------------------------ def _initialize_control(self): """ Perform any toolkit-specific initialization for the control. """ self.control.SetToolTipString(self.tooltip) self.control.Enable(self.enabled) self.control.Show(self.visible) # ------------------------------------------------------------------------ # IWidget interface # ------------------------------------------------------------------------ def _create(self): super(Field, self)._create() self._add_event_listeners() def destroy(self): self._remove_event_listeners() super(Field, self).destroy() # ------------------------------------------------------------------------ # Private interface # ------------------------------------------------------------------------ def _get_control_tooltip(self): """ Toolkit specific method to get the control's tooltip. """ return self.control.GetToolTipString() def _set_control_tooltip(self, tooltip): """ Toolkit specific method to set the control's tooltip. """ self.control.SetToolTipString(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_context_menu) else: self.control.Bind(wx.EVT_CONTEXT_MENU, self._handle_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-6.1.2/pyface/ui/wx/fields/__init__.py0000644000076500000240000000000013462774552021636 0ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/wx/fields/combo_field.py0000644000076500000240000000774113462774552022364 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2017-19, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The Wx-specific implementation of the combo field class """ from __future__ import ( absolute_import, division, print_function, unicode_literals ) 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-6.1.2/pyface/ui/wx/fields/text_field.py0000644000076500000240000000735313462774552022250 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2017-19, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The Wx-specific implementation of the text field class """ from __future__ import print_function, absolute_import 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-6.1.2/pyface/ui/wx/fields/spin_field.py0000644000076500000240000000477013462774552022235 0ustar cwebsterstaff00000000000000# Copyright (c) 2017-19, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: """ The Wx-specific implementation of the spin field class """ from __future__ import ( absolute_import, division, print_function, unicode_literals ) 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-6.1.2/pyface/ui/wx/beep.py0000644000076500000240000000023213462774552017553 0ustar cwebsterstaff00000000000000# Copyright 2012 Philip Chimento """Sound the system bell, Wx implementation.""" import wx def beep(): """Sound the system bell.""" wx.Bell() pyface-6.1.2/pyface/ui/wx/image_list.py0000644000076500000240000000701113462774552020757 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A cached image list. """ from __future__ import absolute_import # Major package imports. import wx # Local imports from .image_resource import ImageResource import six # 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, six.string_types): # 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.EmptyBitmap(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 #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/xrc_dialog.py0000644000076500000240000000750413462774552020764 0ustar cwebsterstaff00000000000000#----------------------------------------------------------------------------- # # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # Author: Scott Swarts # #----------------------------------------------------------------------------- """A dialog that is loaded from an XRC resource file. """ from __future__ import absolute_import # Standard library imports. import os.path # Major packages. import wx import wx.xrc # Enthought library imports from traits.api import Instance, Str import traits.util.resource # Local imports. 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) wx.EVT_BUTTON(self.control, 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) wx.EVT_BUTTON(self.control, cancelbutton.GetId(), self._on_cancel) helpbutton = self.XRCCTRL("HELP") if helpbutton is not None: wx.EVT_BUTTON(self.control, helpbutton.GetId(), self._on_help) 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.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 #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/about_dialog.py0000644000076500000240000001002613462774552021273 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: Enthought, Inc. # #------------------------------------------------------------------------------ """ Enthought pyface package component """ # Standard library imports. import sys # Major package imports. import wx import wx.html import wx.lib.wxpTag # Enthought library imports. from traits.api import Instance, List, provides, Unicode # Local imports. from pyface.i_about_dialog import IAboutDialog, MAboutDialog from pyface.image_resource import ImageResource from .dialog import Dialog _DIALOG_TEXT = '''

%s

Python %s
wxPython %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(Unicode) image = Instance(ImageResource, 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() path = self.image.absolute_path # The additional strings. additions = '
'.join(self.additions) # 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)) # Get the version numbers. py_version = sys.version[0:sys.version.find("(")] wx_version = wx.VERSION_STRING # Get the text of the OK button. if self.ok_label is None: ok = "OK" else: ok = self.ok_label # Set the page contents. html.SetPage( _DIALOG_TEXT % (path, additions, py_version, wx_version, ok) ) # Make the 'OK' button the default button. ok_button = html.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() parent.SetClientSize((width, height + 10)) ### EOF ####################################################################### pyface-6.1.2/pyface/ui/wx/python_editor.py0000644000076500000240000002164113462774552021536 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: Enthought, Inc. # #------------------------------------------------------------------------------ """ Enthought pyface package component """ # Major package imports. import wx.stc # Enthought library imports. from traits.api import Bool, Event, provides, Unicode # Local imports. from pyface.i_python_editor import IPythonEditor, MPythonEditor from pyface.key_pressed_event import KeyPressedEvent from pyface.wx.python_stc import PythonSTC, faces from .widget import Widget @provides(IPythonEditor) class PythonEditor(MPythonEditor, Widget): """ The toolkit specific implementation of a PythonEditor. See the IPythonEditor interface for the API documentation. """ #### 'IPythonEditor' interface ############################################ dirty = Bool(False) path = Unicode show_line_numbers = Bool(True) #### Events #### changed = Event key_pressed = Event(KeyPressedEvent) ########################################################################### # 'object' interface. ########################################################################### def __init__(self, parent, **traits): """ Creates a new pager. """ # Base class constructor. super(PythonEditor, self).__init__(**traits) # Create the toolkit-specific control that represents the widget. self.control = self._create_control(parent) 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 return 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 return def set_style(self, n, fore, back): self.control.StyleSetForeground(n, fore) #self.StyleSetBackground(n, '#c0c0c0') #self.StyleSetBackground(n, '#ffffff') self.control.StyleSetBackground(n, back) self.control.StyleSetFaceName(n, "courier new") self.control.StyleSetSize(n, faces['size']) #self.StyleSetForeground(n, "#f0f0f0") ##self.StyleSetBackground(n, "#000000") #self.StyleSetFaceName(n, "courier new") #self.StyleSetSize(n, 20) #self.StyleSetUnderline(n, 1) #self.StyleSetItalic(n, 1) #self.StyleSetBold(n, 1) #StyleClearAll #StyleResetDefault #StyleSetCase #StyleSetChangeable #StyleSetCharacterSet #StyleSetEOLFilled #StyleSetFont #StyleSetFontAttr #StyleSetHotSpot #StyleSetSpec --- batch #StyleSetVisible return 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. wx.stc.EVT_STC_CHANGE(stc, stc.GetId(), self._on_stc_changed) # Listen for key press events. wx.EVT_CHAR(stc, self._on_char) # Load the editor's contents. self.load() return stc #### 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() return def _on_char(self, event): """ Called whenever a change is made to the text of the document. """ self.key_pressed = KeyPressedEvent( alt_down = event.m_altDown == 1, control_down = event.m_controlDown == 1, shift_down = event.m_shiftDown == 1, key_code = event.m_keyCode, event = event ) # Give other event handlers a chance. event.Skip() return #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/init.py0000644000076500000240000000265113462774552017612 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # Copyright (c) 2017, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt #------------------------------------------------------------------------------ import wx from traits.trait_notifiers import set_ui_handler, ui_handler from pyface.base_toolkit import Toolkit from .gui import GUI # Check the version number is late enough. if wx.VERSION < (2, 8): raise RuntimeError( "Need wx version 2.8 or higher, but got %s" % str(wx.VERSION) ) # 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-6.1.2/pyface/ui/wx/clipboard.py0000644000076500000240000001246013462774552020605 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2009, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! #------------------------------------------------------------------------------ # Standard library imports from six.moves import cStringIO as StringIO from six.moves.cPickle import dumps, load, loads # System library imports import wx # ETS imports from traits.api import provides from pyface.i_clipboard import IClipboard, BaseClipboard import six # Data formats PythonObjectFormat = wx.CustomDataFormat('PythonObject') TextFormat = wx.DataFormat(wx.DF_TEXT) FileFormat = wx.DataFormat(wx.DF_FILENAME) # Shortcuts cb = wx.TheClipboard @provides(IClipboard) class Clipboard(BaseClipboard): #--------------------------------------------------------------------------- # 'data' property methods: #--------------------------------------------------------------------------- def _get_has_data(self): result = False if cb.Open(): result = (cb.IsSupported(TextFormat) or cb.IsSupported(FileFormat) or cb.IsSupported(PythonObjectFormat)) cb.Close() return result #--------------------------------------------------------------------------- # 'object_data' property methods: #--------------------------------------------------------------------------- def _get_object_data(self): result = None if cb.Open(): try: if cb.IsSupported(PythonObjectFormat): cdo = wx.CustomDataObject(PythonObjectFormat) if cb.GetData(cdo): file = StringIO(cdo.GetData()) klass = load(file) result = load(file) finally: cb.Close() return result def _set_object_data(self, data): if cb.Open(): try: 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) finally: cb.Close() cb.Flush() def _get_has_object_data(self): return self._has_this_data(PythonObjectFormat) def _get_object_type(self): result = '' if cb.Open(): try: 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: pass finally: cb.Close() return result #--------------------------------------------------------------------------- # 'text_data' property methods: #--------------------------------------------------------------------------- def _get_text_data(self): result = '' if cb.Open(): if cb.IsSupported(TextFormat): tdo = wx.TextDataObject() if cb.GetData(tdo): result = tdo.GetText() cb.Close() return result def _set_text_data(self, data): if cb.Open(): cb.SetData(wx.TextDataObject(str(data))) cb.Close() cb.Flush() def _get_has_text_data(self): return self._has_this_data(TextFormat) #--------------------------------------------------------------------------- # 'file_data' property methods: #--------------------------------------------------------------------------- def _get_file_data(self): result = [] if cb.Open(): if cb.IsSupported(FileFormat): tfo = wx.FileDataObject() if cb.GetData(tfo): result = tfo.GetFilenames() cb.Close() return result def _set_file_data(self, data): if cb.Open(): tfo = wx.FileDataObject() if isinstance(data, six.string_types): tfo.AddFile(data) else: for filename in data: tfo.AddFile(filename) cb.SetData(tfo) cb.Close() cb.Flush() def _get_has_file_data(self): return self._has_this_data(FileFormat) #--------------------------------------------------------------------------- # Private helper methods: #--------------------------------------------------------------------------- def _has_this_data(self, format): result = False if cb.Open(): result = cb.IsSupported(format) cb.Close() return result pyface-6.1.2/pyface/ui/wx/gui.py0000644000076500000240000001037113514577463017431 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: Enthought, Inc. # #------------------------------------------------------------------------------ """ Enthought pyface package component """ # Standard library imports. import logging import sys # Major package imports. import wx # Enthought library imports. from traits.api import Bool, HasTraits, provides, Unicode from pyface.util.guisupport import start_event_loop_wx # Local imports. 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 = Unicode ########################################################################### # '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 #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/image_cache.py0000644000076500000240000000566713462774552021066 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: Enthought, Inc. # #------------------------------------------------------------------------------ """ Enthought pyface package component """ # Major package imports. import wx # Enthought library imports. from traits.api import HasTraits, provides # Local imports. 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 #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/file_dialog.py0000644000076500000240000001024013462774552021076 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: Enthought, Inc. # #------------------------------------------------------------------------------ """ Enthought pyface package component """ # Standard library imports. import os # Major package imports. import wx # Enthought library imports. from traits.api import Enum, Int, List, provides, Unicode # Local imports. from pyface.i_file_dialog import IFileDialog, MFileDialog from .dialog import Dialog import six @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 = Unicode default_filename = Unicode default_path = Unicode directory = Unicode filename = Unicode path = Unicode paths = List(Unicode) wildcard = Unicode 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 = six.text_type(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(FileDialog, self).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 #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/grid/0000755000076500000240000000000013515277237017213 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/wx/grid/composite_grid_model.py0000644000076500000240000003154713462774552023771 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ # Enthought library imports. from traits.api import Dict, List, Trait # local imports 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(GridModel) # The rows in the model. rows = Trait(None, None, List(GridRow)) # The cached data indexes. _data_index = Dict ######################################################################### # 'object' interface. ######################################################################### def __init__(self, **traits): """ Create a CompositeGridModel object. """ # Base class constructor super(CompositeGridModel, self).__init__(**traits) self._row_count = None return ######################################################################### # '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() return 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() return #### EOF #################################################################### pyface-6.1.2/pyface/ui/wx/grid/grid.py0000644000076500000240000020313513462774552020521 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A grid control with a model/ui architecture. """ # Major package imports import sys import wx import wx.lib.gridmovers as grid_movers from os.path import abspath, exists from wx.grid import Grid as wxGrid from wx.grid import GridCellAttr, GridCellBoolRenderer, PyGridTableBase from wx.grid import 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 wx import TheClipboard # Enthought library imports from pyface.api import Widget from pyface.timer.api import do_later from traits.api import Bool, Color, Enum, Event, Font, Instance, Int, \ Trait, Undefined from pyface.wx.drag_and_drop import PythonDropSource, \ PythonDropTarget, PythonObject from pyface.wx.drag_and_drop import clipboard as enClipboard, FileDropSource # local imports from .grid_model import GridModel from .combobox_focus_handler import ComboboxFocusHandler import six # Is this code running on MS Windows? is_win32 = (sys.platform == 'win32') ASCII_C = 67 class Grid(Widget): """ 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 = Trait(wx.Colour(49, 106, 197), None, Color) # The color to use for a selection foreground/text selection_text_color = Trait(wx.Colour(255, 255, 255), None, Color) # 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 = Trait(Color("linen"), None, Color) 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. """ # Base class constructors. super(Grid, self).__init__(**traits) # Flag set when columns are resizing: self._user_col_size = False # Create the toolkit-specific control. self.control = self._grid = grid = wxGrid(parent, -1) grid.grid = self # 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) grid.Bind(grid_movers.EVT_GRID_ROW_MOVE, self._on_row_move, grid) smotc = self.model.on_trait_change otc = self.on_trait_change smotc(self._on_model_content_changed, 'content_changed') smotc(self._on_model_structure_changed, 'structure_changed') smotc(self._on_row_sort, 'row_sorted') smotc(self._on_column_sort, 'column_sorted') otc(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. otc(self._on_enable_lines_changed, 'enable_lines') otc(self._on_grid_line_color_changed, 'grid_line_color') otc(self._on_default_label_font_changed, 'default_label_font') otc(self._on_default_label_bg_color_changed, 'default_label_bg_color') otc(self._on_default_label_text_color_changed, 'default_label_text_color') otc(self._on_selection_bg_color_changed, 'selection_bg_color') otc(self._on_selection_text_color_changed, 'selection_text_color') otc(self._on_default_cell_font_changed, 'default_cell_font') otc(self._on_default_cell_text_color_changed, 'default_cell_text_color') otc(self._on_default_cell_bg_color_changed, 'default_cell_bg_color') otc(self._on_read_only_changed, 'read_only_changed') otc(self._on_selection_mode_changed, 'selection_mode') otc(self._on_column_label_height_changed, 'column_label_height') otc(self._on_row_label_width_changed, 'row_label_width') otc(self._on_show_column_headers_changed, 'show_column_headers') otc(self._on_show_row_headers_changed, 'show_row_headers') # Initialize wx handlers: self._notify_select = True wx.grid.EVT_GRID_SELECT_CELL(grid, self._on_select_cell) wx.grid.EVT_GRID_RANGE_SELECT(grid, self._on_range_select) wx.grid.EVT_GRID_COL_SIZE(grid, self._on_col_size) wx.grid.EVT_GRID_ROW_SIZE(grid, self._on_row_size) # This starts the cell editor on a double-click as well as on a second # click: wx.grid.EVT_GRID_CELL_LEFT_DCLICK(grid, self._on_cell_left_dclick) # Notify when cells are clicked on: wx.grid.EVT_GRID_CELL_RIGHT_CLICK(grid, self._on_cell_right_click) wx.grid.EVT_GRID_CELL_RIGHT_DCLICK(grid, self._on_cell_right_dclick) wx.grid.EVT_GRID_LABEL_RIGHT_CLICK(grid, self._on_label_right_click) wx.grid.EVT_GRID_LABEL_LEFT_CLICK(grid, self._on_label_left_click) #wx.grid.EVT_GRID_EDITOR_CREATED(grid, self._on_editor_created) if is_win32: wx.grid.EVT_GRID_EDITOR_HIDDEN(grid, self._on_editor_hidden) # We handle key presses to change the behavior of the and # keys to make manual data entry smoother. wx.EVT_KEY_DOWN(grid, self._on_key_down) # We handle control resize events to adjust column widths wx.EVT_SIZE(grid, 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 ): wx.EVT_MOTION( window, self._on_grid_motion ) wx.EVT_LEFT_DOWN( window, self._on_left_down ) wx.EVT_LEFT_UP( window, self._on_left_up ) wx.EVT_PAINT(self._grid_window, self._on_grid_window_paint) # 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 wx.EVT_IDLE(grid, self._on_idle) def dispose(self): # Remove all wx handlers: grid = self._grid wx.grid.EVT_GRID_SELECT_CELL( grid, None ) wx.grid.EVT_GRID_RANGE_SELECT( grid, None ) wx.grid.EVT_GRID_COL_SIZE( grid, None ) wx.grid.EVT_GRID_ROW_SIZE( grid, None ) wx.grid.EVT_GRID_CELL_LEFT_DCLICK( grid, None ) wx.grid.EVT_GRID_CELL_RIGHT_CLICK( grid, None ) wx.grid.EVT_GRID_CELL_RIGHT_DCLICK( grid, None ) wx.grid.EVT_GRID_LABEL_RIGHT_CLICK( grid, None ) wx.grid.EVT_GRID_LABEL_LEFT_CLICK( grid, None ) wx.grid.EVT_GRID_EDITOR_CREATED( grid, None ) if is_win32: wx.grid.EVT_GRID_EDITOR_HIDDEN( grid, None ) wx.EVT_KEY_DOWN( grid, None ) wx.EVT_SIZE( grid, None ) wx.EVT_PAINT( self._grid_window, None ) for window in ( self._grid_window , self._row_window , self._col_window ): wx.EVT_MOTION( window, None ) wx.EVT_LEFT_DOWN( window, None ) wx.EVT_LEFT_UP( window, None ) otc = self.on_trait_change otc(self._on_enable_lines_changed, 'enable_lines', remove = True) otc(self._on_grid_line_color_changed, 'grid_line_color', remove = True) otc(self._on_default_label_font_changed, 'default_label_font', remove = True) otc(self._on_default_label_bg_color_changed, 'default_label_bg_color', remove = True) otc(self._on_default_label_text_color_changed, 'default_label_text_color', remove = True) otc(self._on_selection_bg_color_changed, 'selection_bg_color', remove = True) otc(self._on_selection_text_color_changed, 'selection_text_color', remove = True) otc(self._on_default_cell_font_changed, 'default_cell_font', remove = True) otc(self._on_default_cell_text_color_changed, 'default_cell_text_color', remove = True) otc(self._on_default_cell_bg_color_changed, 'default_cell_bg_color', remove = True) otc(self._on_read_only_changed, 'read_only_changed', remove = True) otc(self._on_selection_mode_changed, 'selection_mode', remove = True) otc(self._on_column_label_height_changed, 'column_label_height', remove = True) otc(self._on_row_label_width_changed, 'row_label_width', remove = True) otc(self._on_show_column_headers_changed, 'show_column_headers', remove = True) otc(self._on_show_row_headers_changed, 'show_row_headers', remove = True) # It seems that the grid must be destroyed before disposing of # _grid_table_base: otherwise, the grid can apparently generate an # extra _GridTableBase.GetAttr call after _GridTableBase.dispose() # has been called, leading to headaches and segfaults. grid.Destroy() self._grid_table_base.dispose() ########################################################################### # Trait event handlers. ########################################################################### def _on_new_model(self): """ 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) return def _on_model_content_changed(self): """ A notification method called when the data in the underlying model changes. """ self._grid.ForceRefresh() def _on_model_structure_changed(self, *arg, **kw): """ 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, evt): """ Handles a row_sorted event from the underlying model. """ # First grab the new data out of the event: if evt.index < 0: self._current_sorted_row = None else: self._current_sorted_row = evt.index self._row_sort_reversed = evt.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, evt): """ Handles a column_sorted event from the underlying model. """ # first grab the new data out of the event if evt.index < 0: self._current_sorted_col = None else: self._current_sorted_col = evt.index self._col_sort_reversed = evt.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): """ Handle a change to the enable_lines trait. """ self._grid.EnableGridLines(self.enable_lines) def _on_grid_line_color_changed(self): """ Handle a change to the enable_lines trait. """ self._grid.SetGridLineColour(self.grid_line_color) def _on_default_label_font_changed(self): """ 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.FONTWEIGHT_BOLD) self._grid.SetLabelFont(font) def _on_default_label_text_color_changed(self): """ 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): """ 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): """ 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): """ 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): """ 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): """ 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): """ 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): """ 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): """ 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.wxGridSelectCells) elif self.selection_mode == 'rows': self._grid.SetSelectionMode(wxGrid.wxGridSelectRows) elif self.selection_mode == 'cols': self._grid.SetSelectionMode(wxGrid.wxGridSelectColumns) def _on_column_label_height_changed(self): """ 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): """ 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): """ 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): """ 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() # needed to handle problem in wx 2.6 with combobox cell editors def _on_editor_created(self, evt): editor = evt.GetControl() if editor is not None: editor.PushEventHandler(ComboboxFocusHandler(self._grid)) 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_grid_window_paint(self, evt): # fixme: this is a total h*ck to get rid of the scrollbars that appear # on a grid under wx2.6 when it starts up. these appear whether or # not needed, and disappear as soon as the grid is resized. hopefully # we will be able to remove this egregious code on some future version # of wx. self._grid.SetColSize(0, self._grid.GetColSize(0) + 1) self._grid.SetColSize(0, self._grid.GetColSize(0) - 1) evt.Skip() 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, six.string_types): 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()) 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.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) # 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 == True 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() self._grid.PopupMenuXY(menu, x - 10, y - 10 ) self.cell_right_clicked = (row, col) evt.Skip() return 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()) 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. 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(): # fixme: in a split window the shift tab is being eaten # by tabbing between the splits self._move_to_previous_cell() else: self._move_to_next_cell() elif key_code == ASCII_C: data = self.__get_drag_value() # deposit the data in our singleton clipboard enClipboard.data = data # build a wxCustomDataObject to notify the system clipboard # that some in-process data is available data_object = wx.CustomDataObject(PythonObject) data_object.SetData('dummy') if TheClipboard.Open(): TheClipboard.SetData(data_object) TheClipboard.Close() 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, event=None): # 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. """ # fixme: The following line seems like a more useful implementation than # the previous commented out version below, but I am leaving the old # version in the code for now just in case. If anyone sees this comment # after 1/1/2009, it should be safe to delete this comment and the # commented out code below... return self.model.get_cell_drag_value(self._grid.GetGridCursorRow(), self._grid.GetGridCursorCol()) ###rows, cols = self.__get_selected_rows_and_cols() ### ###if len(rows) > 0: ### rows.sort() ### value = self.model.get_rows_drag_value(rows) ### if len(rows) == 1 and len(value) == 1: ### value = value[0] ###elif len(cols) > 0: ### cols.sort() ### value = self.model.get_cols_drag_value(cols) ### if len(cols) == 1 and len(value) == 1: ### value = value[0] ###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_drag_value(row, col) ### ###return value 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 wx is retarded we have to check this as well -- why # the blazes can't they come up with one Q#$%@$#% API to manage # selections??? makes me want to put the smack on somebody. # 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.wxGridSelectRows: # 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.wxGridSelectColumns: # 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.wxGridSelectCells: # 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() 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 <= 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(PyGridTableBase): """ A private adapter for the underlying wx grid implementation. """ ########################################################################### # 'object' interface. ########################################################################### def __init__(self, model, grid): """ Creates a new table base. """ # Base class constructor. PyGridTableBase.__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 = {} ########################################################################### # 'wxPyGridTableBase' 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 = six.text_type(label, 'ascii') + u' \u00ab' label = ulabel.encode('latin-1') else: label += ' <<' elif is_win32: ulabel = six.text_type(label, 'ascii') + u' \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 = six.text_type(label, 'ascii') + u' \u00ab' label = ulabel.encode('latin-1') else: label += ' <<' elif is_win32: ulabel = six.text_type(label, 'ascii') + u' \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 == 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 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 = {} return def _editor_dispose(self, editors): for editor in editors: editor.dispose() from wx.grid import PyGridCellEditor class DummyGridCellEditor(PyGridCellEditor): def Show(self, show, attr): return #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/grid/simple_grid_model.py0000644000076500000240000002303413462774552023250 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ 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.""" # Enthought library imports from pyface.action.api import Action, Group, MenuManager, Separator from traits.api import HasTraits, Any, List, Str, Bool, Trait from pyface.wx.drag_and_drop import clipboard as enClipboard # local imports 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 = Trait(None, None, List(GridRow)) # The columns in the model. columns = Trait(None, None, List(GridColumn)) ######################################################################### # 'object' interface. ######################################################################### def __init__(self, **traits): """ Create a SimpleGridModel object. """ # Base class constructor super(SimpleGridModel, self).__init__(**traits) return ######################################################################### # '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(_CopyAction, self).__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 #### EOF #################################################################### pyface-6.1.2/pyface/ui/wx/grid/images/0000755000076500000240000000000013515277237020460 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/wx/grid/images/table_edit.png0000644000076500000240000000612113462774552023265 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-6.1.2/pyface/ui/wx/grid/images/checked.png0000644000076500000240000000063413462774552022562 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-6.1.2/pyface/ui/wx/grid/images/image_LICENSE.txt0000644000076500000240000000153613462774552023455 0ustar cwebsterstaff00000000000000The icons are mostly derived work from other icons. As such they are licensed accordingly to the original license: Project License File ---------------------------------------------------------------------------- GV (Gael Varoquaux) Public Domain N/A Nuvola LGPL image_LICENSE_Nuvola.txt OOo LGPL image_LICENSE_OOo.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: ---------------------------------------------------------------------------- enthought/pyface/grid/images: checked.png | GV image_not_found.png | Nuvola table_edit.png | OOo unchecked.png | GV pyface-6.1.2/pyface/ui/wx/grid/images/unchecked.png0000644000076500000240000000034513462774552023124 0ustar cwebsterstaff00000000000000PNG  IHDRabKGDC pHYs  tIME (7rIDAT8 0 E-n!%A{z'AQQ j.b-.x,{ʙsF8f$7Rvg6afvi%愝P7NCxwEtIENDB`pyface-6.1.2/pyface/ui/wx/grid/images/image_not_found.png0000644000076500000240000000136713462774552024335 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-6.1.2/pyface/ui/wx/grid/trait_grid_cell_adapter.py0000644000076500000240000002330513462774552024422 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Adapter class to make trait editor controls work inside of a grid. """ # Major package imports import wx from wx.grid import PyGridCellEditor from wx import SIZE_ALLOW_MINUS_ONE # Local imports: from combobox_focus_handler import ComboboxFocusHandler wx_28 = (float( wx.__version__[:3] ) >= 2.8) 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 def push_control(control, grid): control.PushEventHandler(ComboboxFocusHandler(grid)) for child_control in control.GetChildren(): push_control(child_control, grid) class TraitGridCellAdapter(PyGridCellEditor): """ 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. """ PyGridCellEditor.__init__(self) 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()[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()[0] ) self._edit_width, self._edit_height = width, height # Set up the event handler for each window in the cell editor: push_control(control, grid) # 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.SetDimensions(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(): if wx_28: super(TraitGridCellAdapter, self).Show(show, attr) else: self.base_Show(show, attr) return 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. """ return 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, *args): """ Validate the input data. """ return True # Pass on all data to ApplyEdit def ApplyEdit(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? return 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. """ return 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. """ return def Destroy(self): """ Final cleanup. """ self._editor.dispose() return 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() #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/grid/grid_cell_image_renderer.py0000644000076500000240000001332113462774552024544 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ 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. """ # Major package imports import wx from wx.grid import PyGridCellRenderer from wx.grid import GridCellStringRenderer from wx import SOLID, Brush, Rect, TRANSPARENT_PEN class GridCellImageRenderer(PyGridCellRenderer): """ 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. """ PyGridCellRenderer.__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() return 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 #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/grid/tests/0000755000076500000240000000000013515277237020355 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/wx/grid/tests/__init__.py0000644000076500000240000000000013462774552022457 0ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/wx/grid/tests/composite_grid_model_test_case.py0000644000076500000240000000716413462774552027163 0ustar cwebsterstaff00000000000000import 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]) return 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) return def test_get_row_count(self): self.assertEqual(self.model.get_row_count(), 2) return 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') return 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') return 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) return 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 #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/grid/tests/simple_grid_model_test_case.py0000644000076500000240000000416713462774552026452 0ustar cwebsterstaff00000000000000import 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')] ) return def test_get_column_count(self): self.assertEqual(self.model.get_column_count(), 2) return def test_get_row_count(self): self.assertEqual(self.model.get_row_count(), 2) return 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') return def test_get_column_name(self): self.assertEqual(self.model.get_column_name(0), 'cfoo') self.assertEqual(self.model.get_column_name(1), 'cbar') return 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) return 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(10,10), True, "Cell outside the range of values should be empty.") return #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/grid/__init__.py0000644000076500000240000000120213462774552021322 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ pyface-6.1.2/pyface/ui/wx/grid/combobox_focus_handler.py0000644000076500000240000001027513462774552024301 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Workaround for combobox focus problem in wx 2.6. """ # Major package imports import wx #------------------------------------------------------------------------------- # Constants: #------------------------------------------------------------------------------- # Mapping from key code to key event handler names: Handlers = { wx.WXK_LEFT: '_left_key', wx.WXK_RIGHT: '_right_key', wx.WXK_UP: '_up_key', wx.WXK_DOWN: '_down_key', wx.WXK_ESCAPE: '_escape_key' } #------------------------------------------------------------------------------- # 'ComboboxFocusHandler' class: #------------------------------------------------------------------------------- class ComboboxFocusHandler(wx.EvtHandler): def __init__(self, grid): wx.EvtHandler.__init__(self) self._grid = grid wx.EVT_KEY_DOWN(self, self._on_key) def _on_key(self, evt): """ Called when a key is pressed. """ getattr( self, Handlers.get( evt.GetKeyCode(), '_ignore_key' ))( evt ) #-- Key Event Handlers -------------------------------------------------------- def _ignore_key ( self, evt ): evt.Skip() def _escape_key ( self, evt ): self._grid.DisableCellEditControl() def _left_key ( self, evt ): if not (evt.ControlDown() or evt.AltDown()): evt.Skip() return grid, row, col, rows, cols = self._grid_info() grid._no_reset_row = True first = True while first or (not self._edit_cell( row, col )): col -= 1 if col < 0: col = cols - 1 row -= 1 if row < 0: if not first: break row = rows - 1 first = False def _right_key ( self, evt ): if not (evt.ControlDown() or evt.AltDown()): evt.Skip() return grid, row, col, rows, cols = self._grid_info() grid._no_reset_row = True first = True while first or (not self._edit_cell( row, col )): col += 1 if col >= cols: col = 0 row += 1 if row >= rows: if not first: break row = 0 first = False def _up_key ( self, evt ): if not (evt.ControlDown() or evt.AltDown()): evt.Skip() return grid, row, col, rows, cols = self._grid_info() grid._no_reset_col = True row -= 1 if row < 0: row = rows - 1 self._edit_cell( row, col ) def _down_key ( self, evt ): if not (evt.ControlDown() or evt.AltDown()): evt.Skip() return grid, row, col, rows, cols = self._grid_info() grid._no_reset_col = True row += 1 if row >= rows: row = 0 self._edit_cell( row, col ) #-- Private Methods ----------------------------------------------------------- def _grid_info ( self ): g = self._grid return ( g, g.GetGridCursorRow(), g.GetGridCursorCol(), g.GetNumberRows(), g.GetNumberCols() ) def _edit_cell ( self, row, col ): grid = self._grid grid.DisableCellEditControl() grid.SetGridCursor( row, col ) if not grid.CanEnableCellControl(): return False grid.EnableCellEditControl() grid.MakeCellVisible( row, col ) return True #### EOF #################################################################### pyface-6.1.2/pyface/ui/wx/grid/grid_cell_renderer.py0000644000076500000240000000301313462774552023377 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2006, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ # Enthought library imports 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 #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/grid/edit_renderer.py0000644000076500000240000000266513462774552022414 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ from .edit_image_renderer import EditImageRenderer from .grid_cell_renderer import GridCellRenderer class EditRenderer(GridCellRenderer): def __init__(self, **traits): # base-class constructor super(EditRenderer, self).__init__(**traits) # initialize the renderer, if it hasn't already been initialized if self.renderer is None: self.renderer = EditImageRenderer() return 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-6.1.2/pyface/ui/wx/grid/mapped_grid_cell_image_renderer.py0000644000076500000240000000437513462774552026103 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2006, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ 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(MappedGridCellImageRenderer, self).__init__(self) self.image_map = image_map self.text_map = text_map return 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) #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/grid/checkbox_renderer.py0000644000076500000240000000265413462774552023253 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2006, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ # local imports from .checkbox_image_renderer import CheckboxImageRenderer from .grid_cell_renderer import GridCellRenderer class CheckboxRenderer(GridCellRenderer): def __init__(self, **traits): # base-class constructor super(CheckboxRenderer, self).__init__(**traits) # initialize the renderer, if it hasn't already been initialized if self.renderer is None: self.renderer = CheckboxImageRenderer() return 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: pass return True #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/grid/api.py0000644000076500000240000000216713462774552020347 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ from __future__ import absolute_import 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 #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/grid/trait_grid_model.py0000644000076500000240000006011513474227520023072 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ 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.""" # Enthought library imports from __future__ import print_function from traits.api import Any, Bool, Callable, Dict, Function, HasTraits, \ Int, List, Str, Trait, TraitError, Type # local imports 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 = Trait(None, None, Str) # A method name to call to get the value for this column method = Trait(None, None, Str) # A method to be used to sort on this column sorter = Trait(None, None, 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(key_trait = Type, value_trait=Trait('', Str, Callable)) # A name to designate the type of this column typename = Trait(None, 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 = Trait(HasTraits) # The specific trait selected on the object trait_name = Trait(None, 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()#HasTraits) # The column definitions columns = Trait(None, None, List(Trait(None, Str, TraitGridColumn))) # The trait to look at to get the row name row_name_trait = Trait(None, 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 = Trait(None, None, Function) ######################################################################### # 'object' interface. ######################################################################### def __init__(self, **traits): """ Create a TraitGridModel object. """ # Base class constructor super(TraitGridModel, self).__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.on_trait_event(self._on_data_changed, 'data') self.on_trait_event(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.on_trait_change(self._on_columns_changed, 'columns') self.on_trait_event(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.on_trait_change(self._on_row_name_trait_changed, 'row_name_trait') return ######################################################################### # '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 sorter = cmp if isinstance(column, TraitGridColumn) and \ column.sorter is not None: sorter = column.sorter except IndexError: return # now sort the data appropriately def sort_function(a, b): if hasattr(a, name): a_trait = getattr(a, name) else: a_trait = None if hasattr(b, name): b_trait = getattr(b, name) else: b_trait = None return sorter(a_trait, b_trait) self.data.sort(sort_function) if reverse: self.data.reverse() # now fire an event to tell the grid we're sorted print('firing sort event') self.column_sorted = GridSortEvent(index = col, reversed = reverse) return 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)() return 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 return 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, new): """ Force the grid to refresh when any underlying trait changes. """ self.fire_content_changed() return def _on_columns_changed(self, object, name, old, new): """ Force the grid to refresh when any underlying trait changes. """ self.__manage_column_listeners(old, remove=True) self.__manage_column_listeners(self.columns) self._auto_columns = self.columns self.fire_structure_changed() return 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() return def _on_contained_trait_changed(self, new): """ Force the grid to refresh when any underlying trait changes. """ self.fire_content_changed() return def _on_data_changed(self, object, name, old, new): """ Force the grid to refresh when the underlying list changes. """ self.__manage_data_listeners(old, remove=True) self.__manage_data_listeners(self.data) self.fire_structure_changed() return 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() return ########################################################################### # 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_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): name = 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.on_trait_change(self._on_contained_trait_changed, remove = remove) return def __manage_column_listeners(self, collist, remove=False): if collist is not None: for col in collist: if isinstance(col, TraitGridColumn): col.on_trait_change(self._on_columns_changed, remove = remove) return #### EOF #################################################################### pyface-6.1.2/pyface/ui/wx/grid/grid_model.py0000644000076500000240000003512313462774552021701 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Model for grid views. """ # Major package imports # Enthought library imports. from traits.api import Any, 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 ######################################################################### # 'object' interface. ######################################################################### def __init__(self, **traits): """ Creates a new grid model. """ # Base class constructors. super(GridModel, self).__init__(**traits) return ######################################################################### # '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. """ #print 'GridModel.set_value row: ', row, ' col: ', col, ' value: ', value rows_appended = self._set_value(row, col, value) self.fire_content_changed() return 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' return def fire_structure_changed(self): """ Fires the appearance changed event. """ self.structure_changed = 'changed' return 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 #### EOF #################################################################### pyface-6.1.2/pyface/ui/wx/grid/checkbox_image_renderer.py0000644000076500000240000000276613462774552024421 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2006, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A renderer which displays a checked-box for a True value and an unchecked box for a false value. """ # Enthought-library imports from pyface.image_resource import ImageResource # local imports 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(CheckboxImageRenderer, self).__init__(checked_image_map, text_map) return #### EOF ###################################################################### pyface-6.1.2/pyface/ui/wx/grid/edit_image_renderer.py0000644000076500000240000000262513462774552023552 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ # ETS imports 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(EditImageRenderer, self).__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-6.1.2/pyface/ui/wx/grid/inverted_grid_model.py0000644000076500000240000000635113462774552023602 0ustar cwebsterstaff00000000000000""" 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. """ # Enthought library imports from traits.api import Instance # local imports 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) #### EOF ###################################################################### pyface-6.1.2/pyface/ui/null/0000755000076500000240000000000013515277236016601 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/null/widget.py0000644000076500000240000000257013462774552020446 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ # Enthought library imports. from traits.api import Any, HasTraits, provides # Local imports. 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 #### EOF ###################################################################### pyface-6.1.2/pyface/ui/null/window.py0000644000076500000240000000565613462774552020502 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ # Enthought library imports. from traits.api import Any, Event, Property, provides, Unicode from traits.api import Tuple # Local imports. 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 = Unicode #### 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 ########################################################################### # Protected 'IWindow' interface. ########################################################################### def _add_event_listeners(self): 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) #### EOF ###################################################################### pyface-6.1.2/pyface/ui/null/image_resource.py0000644000076500000240000000475413462774552022162 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ # Standard library imports. import os # Enthought library imports. from traits.api import Any, HasTraits, List, Property, provides from traits.api import Unicode # Local imports. from pyface.i_image_resource import IImageResource, MImageResource @provides(IImageResource) class ImageResource(MImageResource, HasTraits): """ The '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(Unicode) name = Unicode search_path = List ########################################################################### # 'ImageResource' 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 #### EOF ###################################################################### pyface-6.1.2/pyface/ui/null/tests/0000755000076500000240000000000013515277236017743 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/null/tests/__init__.py0000644000076500000240000000000013462774552022046 0ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/null/tests/bad_import.py0000644000076500000240000000024013462774552022435 0ustar cwebsterstaff00000000000000# 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-6.1.2/pyface/ui/null/resource_manager.py0000644000076500000240000000272513462774552022506 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ # Enthought library imports. 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 #### EOF ###################################################################### pyface-6.1.2/pyface/ui/null/__init__.py0000644000076500000240000000000013462774551020703 0ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/null/action/0000755000076500000240000000000013515277236020056 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/null/action/tool_bar_manager.py0000644000076500000240000000557713462774552023745 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The 'null' backend specific implementation of the tool bar manager. """ # Enthought library imports. from traits.api import Bool, Enum, Instance, Tuple # Local imports. 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(ToolBarManager, self).__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 #### EOF ###################################################################### pyface-6.1.2/pyface/ui/null/action/menu_manager.py0000644000076500000240000000477313462774551023104 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The 'null' backend specific implementation of a menu manager. """ # Enthought library imports. from traits.api import Unicode # Local imports. 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 = Unicode ########################################################################### # '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. """ return 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.") #### EOF ###################################################################### pyface-6.1.2/pyface/ui/null/action/tool_palette_manager.py0000644000076500000240000000471113462774552024624 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A tool bar manager realizes itself in a tool palette control. """ # Enthought library imports. from traits.api import Any, Bool, Enum, Instance, Tuple # Local imports. 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(ToolPaletteManager, self).__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 #### EOF ###################################################################### pyface-6.1.2/pyface/ui/null/action/status_bar_manager.py0000644000076500000240000000522213462774552024276 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A status bar manager realizes itself in a status bar control. """ # Enthought library imports. from traits.api import Any, HasTraits, List, Property, Str, Unicode 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(Unicode) # 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. """ return def _messages_items_changed(self): """ Sets the text displayed on the status bar. """ return #### EOF ###################################################################### pyface-6.1.2/pyface/ui/null/action/__init__.py0000644000076500000240000000033713462774551022175 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007 by Enthought, Inc. # All rights reserved. #------------------------------------------------------------------------------ pyface-6.1.2/pyface/ui/null/action/tool_palette.py0000644000076500000240000000615713462774552023140 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ 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(ToolPalette, self).__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. """ return def enable_tool(self, id, enabled): """ Enable or disable the tool identified by 'id'. """ return def on_tool_event(self, id, callback): """ Register a callback for events on the tool identified by 'id'. """ return def realize(self): """ Realize the control so that it can be displayed. """ return 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 #### EOF ###################################################################### pyface-6.1.2/pyface/ui/null/action/action_item.py0000644000076500000240000001030713462774551022727 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enth373ought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The 'null' specific implementations of the action manager internal classes. """ # Enthought library imports. 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) return 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 # 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() path = action.image.absolute_path bmp = image_cache.get_bitmap(path) else: from pyface.api import ImageResource image = ImageResource('foo') bmp = image.create_bitmap() self.control_id = 1 self.control = None if controller is not None: self.controller = controller controller.add_to_toolbar(self) return 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 #### EOF ###################################################################### pyface-6.1.2/pyface/ui/null/action/menu_bar_manager.py0000644000076500000240000000316113462774551023716 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The 'null' backend specific implementation of a menu bar manager. """ # Local imports. 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 #### EOF ###################################################################### pyface-6.1.2/pyface/ui/null/init.py0000644000076500000240000000123213462774552020120 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # 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 #------------------------------------------------------------------------------ """ Initialize this backend. """ from pyface.base_toolkit import Toolkit toolkit_object = Toolkit('pyface', 'null', 'pyface.ui.null') pyface-6.1.2/pyface/ui/null/clipboard.py0000644000076500000240000000422113462774552021115 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2009, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Evan Patterson # Date: 06/29/09 #------------------------------------------------------------------------------ # ETS imports 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-6.1.2/pyface/ui/qt4/0000755000076500000240000000000013515277236016337 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/qt4/python_shell.py0000644000076500000240000005463413514577463021441 0ustar cwebsterstaff00000000000000# Copyright (c) 2011, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Evan Patterson # Standard library imports. import six.moves.builtins from code import compile_command, InteractiveInterpreter from six.moves import cStringIO as StringIO import sys from time import time # System package imports. from pyface.qt import QtCore, QtGui from pygments.lexers import PythonLexer # Enthought library imports. from traits.api import Event, provides from traits.util.clean_strings import python_name # Local imports. 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 .widget import Widget import six @provides(IPythonShell) class PythonShell(MPythonShell, 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) #-------------------------------------------------------------------------- # '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): super(PythonShell, self).__init__(parent=parent, **traits) # Create the toolkit-specific control that represents the widget. self._create() #-------------------------------------------------------------------------- # '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(PythonShell, self)._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.connect(self._on_command_executed) self._event_filter.signal.disconnect(self._on_obj_drop) super(PythonShell, self)._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, six.string_types) 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(PythonWidget, self).__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() #-------------------------------------------------------------------------- # 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.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: more = 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_Backspace and \ not event.modifiers() & QtCore.Qt.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.Left, QtGui.QTextCursor.KeepAnchor, 4) cursor.removeSelectedText() return True return super(PythonWidget, self)._event_filter_console_keypress(event) def _insert_continuation_prompt(self, cursor): """ Reimplemented for auto-indentation. """ super(PythonWidget, self)._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.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(six.moves.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.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.StartOfBlock, QtGui.QTextCursor.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 = six.moves.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 document = self._control.document() 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(PythonWidgetHighlighter, self).__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(PythonWidgetHighlighter, self).highlightBlock(string) def rehighlightBlock(self, block): """ Reimplemented to temporarily enable highlighting if disabled. """ old = self.highlighting_on self.highlighting_on = True super(PythonWidgetHighlighter, self).rehighlightBlock(block) self.highlighting_on = old def setFormat(self, start, count, format): """ Reimplemented to highlight selectively. """ start += self._current_offset super(PythonWidgetHighlighter, self).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(PyfacePythonWidget, self).__init__(*args, **kw) #--------------------------------------------------------------------------- # 'QWidget' interface #--------------------------------------------------------------------------- def keyPressEvent(self, event): """ Reimplemented to generate Pyface key press events. """ # Pyface doesn't seem to be Unicode 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.AltModifier) == QtCore.Qt.AltModifier), control_down = ((mods & QtCore.Qt.ControlModifier) == QtCore.Qt.ControlModifier), shift_down = ((mods & QtCore.Qt.ShiftModifier) == QtCore.Qt.ShiftModifier), key_code = kcode, event = event) super(PyfacePythonWidget, self).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.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.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-6.1.2/pyface/ui/qt4/tasks/0000755000076500000240000000000013515277237017465 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/qt4/tasks/advanced_editor_area_pane.py0000644000076500000240000005541113462774552025156 0ustar cwebsterstaff00000000000000# Standard library imports. import sys # System library imports. from pyface.qt import QtCore, QtGui # Enthought library imports. from traits.api import DelegatesTo, Instance, on_trait_change, provides # Local imports. 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) ########################################################################### # 'TaskPane' interface. ########################################################################### def create(self, parent): """ Create and set the toolkit-specific control that represents the pane. """ self.control = 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) shortcut = QtGui.QShortcut(QtGui.QKeySequence(prev_seq), self.control) shortcut.activated.connect(self._previous_tab) # Add shortcuts for switching to a specific tab. mod = 'Ctrl+' if sys.platform == 'darwin' else 'Alt+' mapper = QtCore.QSignalMapper(self.control) mapper.mapped.connect(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) 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: editor_widget = editor.control.parent() self.control.destroy_editor_widget(editor_widget) editor.editor_area = None super(AdvancedEditorAreaPane, self).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.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.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 ################################################ @on_trait_change('editors:[dirty, name]') def _update_label(self, editor, name, new): editor.control.parent().update_title() @on_trait_change('editors:tooltip') def _update_tooltip(self, editor, name, new): 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(EditorAreaWidget, self).__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._focus_changed) # 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.StrongFocus) self.setTabPosition(QtCore.Qt.AllDockWidgetAreas, QtGui.QTabWidget.North) def add_editor_widget(self, editor_widget): """ Adds a dock widget to the editor area. """ editor_widget.installEventFilter(self) self.addDockWidget(QtCore.Qt.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.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(EditorAreaWidget, self).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._tab_index_changed, QtCore.Qt.UniqueConnection) child.setTabsClosable(True) child.setUsesScrollButtons(True) child.tabCloseRequested.connect(self._tab_close_requested, QtCore.Qt.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.Resize: if widget.geometry() == self._rubber_band.geometry(): self.set_hover_widget(widget) elif self._drag_widget == widget and event.type() == QtCore.QEvent.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.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.Resize, QtCore.QEvent.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.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.MouseButtonPress, pos, widget.mapToGlobal(pos), QtCore.Qt.LeftButton, QtCore.Qt.LeftButton, event.modifiers()) QtCore.QCoreApplication.sendEvent(widget, press_event) return True event = QtGui.QMouseEvent( QtCore.QEvent.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.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 _focus_changed(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 = focused = editor break else: if not self.editor_area.editors: self.editor_area.active_editor = None def _tab_index_changed(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(EditorWidget, self).__init__(parent) self.editor = editor self.editor.create(self) self.setAllowedAreas(QtCore.Qt.LeftDockWidgetArea) self.setFeatures(QtGui.QDockWidget.DockWidgetClosable | QtGui.QDockWidget.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 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(EditorTitleBarWidget, self).__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-6.1.2/pyface/ui/qt4/tasks/task_window_backend.py0000644000076500000240000001533713462774552024053 0ustar cwebsterstaff00000000000000# System library imports. from pyface.qt import QtCore, QtGui # Enthought library imports. from traits.api import Instance, List # Local imports. 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.TopLeftCorner, 'top_right': QtCore.Qt.TopRightCorner, 'bottom_left': QtCore.Qt.BottomLeftCorner, 'bottom_right': QtCore.Qt.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(TaskWindowLayout, self).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-6.1.2/pyface/ui/qt4/tasks/editor_area_pane.py0000644000076500000240000002054413462774552023330 0ustar cwebsterstaff00000000000000# Standard library imports. import sys # Enthought library imports. from pyface.tasks.i_editor_area_pane import IEditorAreaPane, \ MEditorAreaPane from traits.api import on_trait_change, provides # System library imports. from pyface.qt import QtCore, QtGui # Local imports. 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. """ ########################################################################### # '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) control.tabCloseRequested.connect(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) shortcut = QtGui.QShortcut(QtGui.QKeySequence(prev_seq), self.control) shortcut.activated.connect(self._previous_tab) # Add shortcuts for switching to a specific tab. mod = 'Ctrl+' if sys.platform == 'darwin' else 'Alt+' mapper = QtCore.QSignalMapper(self.control) mapper.mapped.connect(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) 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) super(EditorAreaPane, self).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() # 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() 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 ################################################ @on_trait_change('editors:[dirty, name]') def _update_label(self, editor, name, new): index = self.control.indexOf(editor.control) self.control.setTabText(index, self._get_label(editor)) @on_trait_change('editors:tooltip') def _update_tooltip(self, editor, name, new): 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) @on_trait_change('hide_tab_bar') def _update_tab_bar(self): 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(EditorAreaWidget, self).__init__(parent) self.editor_area = editor_area # Configure the QTabWidget. self.setAcceptDrops(True) self.setDocumentMode(True) self.setFocusPolicy(QtCore.Qt.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(EditorAreaDropFilter, self).__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.DragEnter, QtCore.QEvent.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.DragEnter: if file_paths: event.acceptProposedAction() # Dispatch the events. elif event.type() == QtCore.QEvent.Drop: for file_path in file_paths: self.editor_area.file_dropped = file_path return True return super(EditorAreaDropFilter, self).eventFilter(object, event) pyface-6.1.2/pyface/ui/qt4/tasks/split_editor_area_pane.py0000644000076500000240000011421713514577463024544 0ustar cwebsterstaff00000000000000# Standard library imports. import sys # Enthought library imports. from pyface.tasks.i_editor_area_pane import IEditorAreaPane, \ MEditorAreaPane from traits.api import Bool, cached_property, Callable, Dict, Instance, List, \ on_trait_change, Property, provides, Str from pyface.qt import is_qt4, QtCore, QtGui 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 # Local imports. 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 ################################################### _private_drop_handlers = List(IDropHandler) _all_drop_handlers = Property( List(IDropHandler), depends_on=['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) super(SplitEditorAreaPane, self).destroy() ########################################################################### # 'IEditorAreaPane' interface. ########################################################################### def activate_editor(self, editor): """ Activates the specified editor in the pane. """ active_tabwidget = editor.control.parent().parent() 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 = editor.control.parent().parent() tabwidget.removeTab(tabwidget.indexOf(editor.control)) 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): """ Returns a context menu containing split/collapse actions pos : position (in global coordinates) where the context menu was requested """ menu = MenuManager() splitter = None splitter = None for tabwidget in self.tabwidgets(): # obtain tabwidget's bounding rectangle in global coordinates global_rect = QtCore.QRect(tabwidget.mapToGlobal(QtCore.QPoint(0, 0)), tabwidget.size()) if global_rect.contains(pos): splitter = tabwidget.parent() # no split/collapse context menu for positions outside any tabwidget # region if not splitter: return # add split actions (only show for non-empty tabwidgets) if not splitter.is_empty(): actions = [Action(id='split_hor', name='Create new pane to the right', on_perform=lambda : splitter.split(orientation= QtCore.Qt.Horizontal)), Action(id='split_ver', name='Create new pane to the bottom', on_perform=lambda : splitter.split(orientation= QtCore.Qt.Vertical))] splitgroup = Group(*actions, id='split') menu.append(splitgroup) # add collapse action (only show for collapsible splitters) if splitter.is_collapsible(): if splitter is splitter.parent().leftchild: if splitter.parent().orientation() == QtCore.Qt.Horizontal: text = 'Merge with right pane' else: text = 'Merge with bottom pane' else: if splitter.parent().orientation() == QtCore.Qt.Horizontal: text = 'Merge with left pane' else: text = 'Merge with top pane' actions = [Action(id='merge', name=text, on_perform=lambda : splitter.collapse())] collapsegroup = Group(*actions, id='collapse') menu.append(collapsegroup) # return QMenu object 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) shortcut = QtGui.QShortcut(QtGui.QKeySequence(prev_seq), self.control) shortcut.activated.connect(self._previous_tab) # Add shortcuts for switching to a specific tab. mod = 'Ctrl+' if sys.platform == 'darwin' else 'Alt+' mapper = QtCore.QSignalMapper(self.control) mapper.mapped.connect(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) 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() #### Trait change handlers ################################################ @on_trait_change('editors:[dirty, name]') def _update_label(self, editor, name, new): index = self.active_tabwidget.indexOf(editor.control) self.active_tabwidget.setTabText(index, self._get_label(editor)) @on_trait_change('editors:tooltip') def _update_tooltip(self, editor, name, new): index = self.active_tabwidget.indexOf(editor.control) self.active_tabwidget.setTabToolTip(index, self._get_label(editor)) #### 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(EditorAreaWidget, self).__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.Horizontal: 'horizontal', QtCore.Qt.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.Horizontal, 'vertical': QtCore.Qt.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.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(DraggableTabWidget, self).__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.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.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.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.AlignHCenter) # generate label label = QtGui.QLabel(parent=frame) label.setText(""" or """) layout.addWidget(label, alignment=QtCore.Qt.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.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.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 """ 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) 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.Highlight) event.acceptProposedAction() return super(DraggableTabWidget, self).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.Window) event.acceptProposedAction() break def dragLeaveEvent(self, event): """ Clear widget highlight on leaving """ self.setBackgroundRole(QtGui.QPalette.Window) return super(DraggableTabWidget, self).dragLeaveEvent(event) class DraggableTabBar(QtGui.QTabBar): """ Implements a QTabBar with event filters for tab drag """ def __init__(self, editor_area, parent): super(DraggableTabBar, self).__init__(parent) self.editor_area = editor_area self.setContextMenuPolicy(QtCore.Qt.DefaultContextMenu) self.drag_obj = None def mousePressEvent(self, event): if event.button()==QtCore.Qt.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(DraggableTabBar, self).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.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(DraggableTabBar, self).mouseMoveEvent(event) def mouseReleaseEvent(self, event): """ Re-implemented to deactivate the drag when mouse button is released """ self.drag_obj = None return super(DraggableTabBar, self).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.lightGray) painter.setCompositionMode(QtGui.QPainter.CompositionMode_SourceOver) optTabBase = QtGui.QStyleOptionTabBarBase() optTabBase.initFrom(tabBar) painter.drawPrimitive(QtGui.QStyle.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-6.1.2/pyface/ui/qt4/tasks/util.py0000644000076500000240000000272413462774552021024 0ustar cwebsterstaff00000000000000# System library imports. 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.NoFocus: control.setFocus() else: widget = control.focusWidget() if widget: widget.setFocus() else: flags = control.windowFlags() control.setWindowFlags(flags | QtCore.Qt.SubWindow) try: control.focusNextChild() finally: control.setWindowFlags(flags) pyface-6.1.2/pyface/ui/qt4/tasks/tests/0000755000076500000240000000000013515277237020627 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/qt4/tasks/tests/__init__.py0000644000076500000240000000000013462774552022731 0ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/qt4/tasks/tests/test_split_editor_area_pane.py0000644000076500000240000004023013514577463026736 0ustar cwebsterstaff00000000000000""" Tests for the SplitEditorAreaPane class. """ import os import tempfile import unittest from traits.api import HasTraits, 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.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.assertEquals(root.leftchild.count(), 1) self.assertEquals(root.rightchild.count(), 1) # are the tabwidgets laid out correctly? self.assertEquals(root.leftchild.tabwidget(), tabwidget) self.assertIsNotNone(root.rightchild.tabwidget().empty_widget) # are the contents of the left tabwidget correct? self.assertEquals(root.leftchild.tabwidget().count(), 2) self.assertEquals(root.leftchild.tabwidget().widget(0), btn0) self.assertEquals(root.leftchild.tabwidget().widget(1), btn1) self.assertEquals(root.leftchild.tabwidget().currentWidget(), btn1) # does the right tabwidget contain nothing but the empty widget? self.assertEquals(root.rightchild.tabwidget().count(), 1) self.assertEquals(root.rightchild.tabwidget().widget(0), root.rightchild.tabwidget().empty_widget) # do we have an equally sized split? self.assertEquals(root.leftchild.width(), root.rightchild.width()) # is the rightchild active? self.assertEquals(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, left, right = self._setUp_collapse() btn0 = left.tabwidget().widget(0) btn1 = left.tabwidget().widget(1) btn2 = right.tabwidget().widget(0) btn3 = right.tabwidget().widget(1) # perform collapse on rightchild root.rightchild.collapse() # test # has the root now become the leaf? self.assertEquals(root.count(), 1) self.assertIsInstance(root.widget(0), QtGui.QTabWidget) # how does the combined list look? self.assertEquals(root.tabwidget().count(), 4) self.assertEquals(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.assertEquals(root.count(), 2) self.assertEquals(root.leftchild, left_left) self.assertEquals(root.rightchild, left_right) # are the contents of left_left and left_right preserved self.assertEquals(root.leftchild.tabwidget().count(), 2) self.assertEquals(root.rightchild.tabwidget().count(), 2) self.assertEquals(root.leftchild.tabwidget().currentIndex(), 1) self.assertEquals(root.rightchild.tabwidget().currentIndex(), 0) # what is the current active_tabwidget? self.assertEquals(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.assertEquals(editor_area.active_tabwidget.tabToolTip(0), "test_tooltip0") self.assertEquals(editor_area.active_tabwidget.tabToolTip(1), "test_tooltip1") self.assertEquals(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.assertEquals(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.assertEquals(right.orientation(), QtCore.Qt.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.assertEquals(editor.obj, file1) # file2 goes second on bottom pane? editor = editor_area._get_editor(right_bottom.tabwidget().widget(1)) self.assertEquals(editor.obj, file2) # file1 tab is active? self.assertEquals(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.assertEquals(layout_new.orientation, 'horizontal') # tests on left child left = layout_new.items[0] self.assertIsInstance(left, Tabbed) self.assertEquals(left.items[0].id, 0) # tests on right child right = layout_new.items[1] self.assertIsInstance(right, Splitter) self.assertEquals(right.orientation, 'vertical') # tests on top pane of right child right_top = right.items[0] self.assertIsInstance(right_top, Tabbed) self.assertEquals(right_top.items[0].id, -1) # tests on bottom pane of right child right_bottom = right.items[1] self.assertIsInstance(right_bottom, Tabbed) self.assertEquals(right_bottom.items[0].id, 1) self.assertEquals(right_bottom.items[1].id, 2) 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.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.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_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.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() if __name__ == '__main__': unittest.main() pyface-6.1.2/pyface/ui/qt4/tasks/__init__.py0000644000076500000240000000000013462774552021567 0ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/qt4/tasks/task_pane.py0000644000076500000240000000225313462774552022011 0ustar cwebsterstaff00000000000000# Enthought library imports. from pyface.tasks.i_task_pane import ITaskPane, MTaskPane from traits.api import provides # System library imports. from pyface.qt import QtGui # Local imports. 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-6.1.2/pyface/ui/qt4/tasks/dock_pane.py0000644000076500000240000001416713462774552021776 0ustar cwebsterstaff00000000000000# Standard library imports from contextlib import contextmanager # Enthought library imports. from pyface.tasks.i_dock_pane import IDockPane, MDockPane from traits.api import Bool, on_trait_change, Property, provides, Tuple # System library imports. from pyface.qt import QtCore, QtGui # Local imports. from .task_pane import TaskPane from .util import set_focus # Constants. AREA_MAP = { 'left' : QtCore.Qt.LeftDockWidgetArea, 'right' : QtCore.Qt.RightDockWidgetArea, 'top' : QtCore.Qt.TopDockWidgetArea, 'bottom' : QtCore.Qt.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) # Configure the dock widget according to the DockPane settings. self._set_dock_features() self._set_dock_title() self._set_floating() self._set_visible() # 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 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 ################################################ @on_trait_change('dock_area') def _set_dock_area(self): 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) @on_trait_change('closable,floatable,movable') def _set_dock_features(self): if self.control is not None: features = QtGui.QDockWidget.NoDockWidgetFeatures if self.closable: features |= QtGui.QDockWidget.DockWidgetClosable if self.floatable: features |= QtGui.QDockWidget.DockWidgetFloatable if self.movable: features |= QtGui.QDockWidget.DockWidgetMovable self.control.setFeatures(features) @on_trait_change('name') def _set_dock_title(self): if self.control is not None: self.control.setWindowTitle(self.name) @on_trait_change('floating') def _set_floating(self): if self.control is not None and not self._receiving: self.control.setFloating(self.floating) @on_trait_change('visible') def _set_visible(self): 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(): 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-6.1.2/pyface/ui/qt4/tasks/main_window_layout.py0000644000076500000240000003003713462774552023755 0ustar cwebsterstaff00000000000000# Standard library imports. from itertools import combinations import logging # System library imports. from pyface.qt import QtCore, QtGui # Enthought library imports. from traits.api import Any, HasTraits # Local imports. from pyface.tasks.task_layout import LayoutContainer, PaneItem, Tabbed, \ Splitter, HSplitter, VSplitter from .dock_pane import AREA_MAP # Contants. ORIENTATION_MAP = { 'horizontal' : QtCore.Qt.Horizontal, 'vertical': QtCore.Qt.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.Horizontal: if rect1.y() < rect2.y(): item = VSplitter(item1, item2) else: item = VSplitter(item2, item1) elif orient == QtCore.Qt.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) # 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) # 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.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.Horizontal elif one.y() == two.y() and one.height() == two.height() and \ united.width() == one.width() + two.width(): return QtCore.Qt.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 layout.width > 0: dock_widget.widget().setFixedWidth(layout.width) if layout.height > 0: dock_widget.widget().setFixedHeight(layout.height) 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-6.1.2/pyface/ui/qt4/tasks/editor.py0000644000076500000240000000256713462774552021342 0ustar cwebsterstaff00000000000000# Enthought library imports. from pyface.tasks.i_editor import IEditor, MEditor from traits.api import Bool, Property, provides # System library imports. 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-6.1.2/pyface/ui/qt4/confirmation_dialog.py0000644000076500000240000001075413462774552022733 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Major package imports. from pyface.qt import QtCore, QtGui # Enthought library imports. from traits.api import Bool, Dict, Enum, Instance, provides, Unicode # Local imports. from pyface.i_confirmation_dialog import IConfirmationDialog, MConfirmationDialog from pyface.constant import CANCEL, YES, NO from pyface.image_resource import ImageResource 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 = Instance(ImageResource) message = Unicode informative = Unicode detail = Unicode no_label = Unicode yes_label = Unicode # 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.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.Warning) else: dlg.setIconPixmap(self.image.create_image()) # The 'Yes' button. if self.yes_label: btn = dlg.addButton(self.yes_label, QtGui.QMessageBox.YesRole) else: btn = dlg.addButton(QtGui.QMessageBox.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.NoRole) else: btn = dlg.addButton(QtGui.QMessageBox.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.RejectRole) else: btn = dlg.addButton(QtGui.QMessageBox.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.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 #### EOF ###################################################################### pyface-6.1.2/pyface/ui/qt4/single_choice_dialog.py0000644000076500000240000000773313462774552023041 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # # Copyright (c) 2016, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: Enthought, Inc. # #------------------------------------------------------------------------------ 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.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 # Let the window close as normal. super(SingleChoiceDialog, self).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.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-6.1.2/pyface/ui/qt4/widget.py0000644000076500000240000000726513462774552020212 0ustar cwebsterstaff00000000000000# Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # Copyright (c) 2017, Enthought, Inc # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # Description: # Major package imports. from pyface.qt import QtCore, QtGui # Enthought library imports. from traits.api import Any, Bool, HasTraits, Instance, provides # Local imports. 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) # 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 destroy(self): self._remove_event_listeners() if self.control is not None: self.control.hide() self.control.deleteLater() self.control = None def _add_event_listeners(self): 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 # 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. """ 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.Show, QtCore.QEvent.Hide}: widget.visible = widget.control.isVisible() return False pyface-6.1.2/pyface/ui/qt4/directory_dialog.py0000644000076500000240000000520313462774552022240 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Major package imports. from pyface.qt import QtGui # Enthought library imports. from traits.api import Bool, provides, Unicode # Local imports. from pyface.i_directory_dialog import IDirectoryDialog, MDirectoryDialog from .dialog import Dialog import six @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 = Unicode message = Unicode new_directory = Bool(True) path = Unicode ########################################################################### # 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 = six.text_type(files[0]) else: self.path = '' # Let the window close as normal. super(DirectoryDialog, self).close() ########################################################################### # Protected 'IWidget' interface. ########################################################################### def _create_control(self, parent): dlg = QtGui.QFileDialog(parent, self.title, self.default_path) dlg.setViewMode(QtGui.QFileDialog.Detail) dlg.setFileMode(QtGui.QFileDialog.DirectoryOnly) if not self.new_directory: dlg.setOptions(QtGui.QFileDialog.ReadOnly) if self.message: dlg.setLabelText(QtGui.QFileDialog.LookIn, self.message) return dlg #### EOF ###################################################################### pyface-6.1.2/pyface/ui/qt4/message_dialog.py0000644000076500000240000000466113462774552021667 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Major package imports. from pyface.qt import QtGui # Enthought library imports. from traits.api import Enum, provides, Unicode # Local imports. 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.Information, 'warning': QtGui.QMessageBox.Warning, 'error': QtGui.QMessageBox.Critical } @provides(IMessageDialog) class MessageDialog(MMessageDialog, Dialog): """ The toolkit specific implementation of a MessageDialog. See the IMessageDialog interface for the API documentation. """ #### 'IMessageDialog' interface ########################################### message = Unicode informative = Unicode detail = Unicode severity = Enum('information', 'warning', 'error') ########################################################################### # 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.Ok, parent) message_box.setInformativeText(self.informative) message_box.setDetailedText(self.detail) if self.size != (-1, -1): message_box.resize(*self.size) if self.position != (-1, -1): message_box.move(*self.position) return message_box pyface-6.1.2/pyface/ui/qt4/window.py0000644000076500000240000002025713462774552020232 0ustar cwebsterstaff00000000000000# Copyright (c) 2007, Riverbank Computing Limited # Copyright (c) 2017-18, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license. However, when used with the GPL version of PyQt the additional # terms described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # Author: Enhtought, Inc. # Description: # Major package imports. from pyface.qt import QtCore, QtGui # Enthought library imports. from traits.api import ( Enum, Event, Property, Tuple, Unicode, VetoableEvent, provides ) # Local imports. 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 = Unicode # 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): self._remove_event_listeners() 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(Window, self).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.WindowMaximized ) elif state == 'normal': control.setWindowState( control.windowState() & ~QtCore.Qt.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) self._window = window def eventFilter(self, obj, e): """ Adds any event listeners required by the window. """ window = self._window # Sanity check. if obj is not window.control: return False typ = e.type() if typ == QtCore.QEvent.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.WindowActivate: window.activated = window elif typ == QtCore.QEvent.WindowDeactivate: window.deactivated = window elif typ in {QtCore.QEvent.Show, QtCore.QEvent.Hide}: window.visible = window.control.isVisible() elif typ == QtCore.QEvent.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.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.KeyPress: # Pyface doesn't seem to be Unicode 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.AltModifier) == QtCore.Qt.AltModifier), control_down=((mods & QtCore.Qt.ControlModifier ) == QtCore.Qt.ControlModifier), shift_down=((mods & QtCore.Qt.ShiftModifier ) == QtCore.Qt.ShiftModifier), key_code=kcode, event=e ) elif typ == QtCore.QEvent.WindowStateChange: # set the size_state of the window. state = obj.windowState() if state & QtCore.Qt.WindowMaximized: window.size_state = 'maximized' else: window.size_state = 'normal' return False pyface-6.1.2/pyface/ui/qt4/image_resource.py0000644000076500000240000000600413462774552021706 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Standard library imports. import os # Major package imports. from pyface.qt import QtGui # Enthought library imports. from traits.api import Any, HasTraits, List, Property, provides from traits.api import Unicode # Local imports. from pyface.i_image_resource import IImageResource, MImageResource @provides(IImageResource) class ImageResource(MImageResource, HasTraits): """ The toolkit specific implementation of an ImageResource. See the IImageResource interface for the API documentation. """ #### Private interface #################################################### # The resource manager reference for the image. _ref = Any #### 'ImageResource' interface ############################################ absolute_path = Property(Unicode) name = Unicode search_path = List ########################################################################### # 'ImageResource' 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 #### EOF ###################################################################### pyface-6.1.2/pyface/ui/qt4/util/0000755000076500000240000000000013515277237017315 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/qt4/util/modal_dialog_tester.py0000644000076500000240000002731713462774552023705 0ustar cwebsterstaff00000000000000# (C) Copyright 2014-2017 Enthought, Inc., Austin, TX # All right 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! """ A class to facilitate testing components that use TraitsUI or Qt Dialogs. """ import contextlib import platform import sys import traceback from pyface.api import GUI, OK, CANCEL, YES, NO from pyface.qt import QtCore, QtGui, qt_api from traits.api import Undefined from .event_loop_helper import EventLoopHelper from .testing import find_qt_widget BUTTON_TEXT = { OK: u'OK', CANCEL: u'Cancel', YES: u'Yes', NO: u'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-6.1.2/pyface/ui/qt4/util/event_loop_helper.py0000644000076500000240000001204413462774552023404 0ustar cwebsterstaff00000000000000# (C) Copyright 2014-15 Enthought, Inc., Austin, TX # All right 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 contextlib import threading from pyface.gui import GUI 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(GUI) 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. """ 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. """ 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(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. """ timer = QtCore.QTimer() timer.setSingleShot(True) timer.setInterval(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-6.1.2/pyface/ui/qt4/util/tests/0000755000076500000240000000000013515277237020457 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/qt4/util/tests/test_modal_dialog_tester.py0000644000076500000240000001416113462774552026077 0ustar cwebsterstaff00000000000000# Copyright (c) 2013-2015 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! """ Tests for the tabular editor tester. """ from __future__ import absolute_import import unittest from six.moves import cStringIO as StringIO import platform from pyface.qt import QtGui from pyface.api import Dialog, MessageDialog, OK, CANCEL from pyface.toolkit import toolkit_object 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 is_qt = toolkit_object.toolkit == 'qt4' if is_qt: from pyface.qt import qt_api is_pyqt5 = (is_qt and qt_api == 'pyqt5') 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 @unittest.skipIf(is_pyqt5, "ModalDialogTester not working on pyqt5. Issue #302") class TestModalDialogTester(unittest.TestCase, GuiTestAssistant): """ 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) 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) 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) if __name__ == '__main__': unittest.main() pyface-6.1.2/pyface/ui/qt4/util/tests/__init__.py0000644000076500000240000000000013462774552022561 0ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/qt4/util/tests/test_gui_test_assistant.py0000644000076500000240000000443413514571010025772 0ustar cwebsterstaff00000000000000import unittest 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 pyface-6.1.2/pyface/ui/qt4/util/__init__.py0000644000076500000240000000000013462774552021417 0ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/qt4/util/gui_test_assistant.py0000644000076500000240000002357013514571010023573 0ustar cwebsterstaff00000000000000# Copyright (c) 2013-2017 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 contextlib import gc import threading import six if six.PY2: import mock else: import unittest.mock as mock from pyface.qt.QtGui import QApplication from pyface.ui.qt4.gui import GUI from traits.testing.unittest_tools import UnittestTools from traits.testing.unittest_tools import _TraitsChangeCollector as \ TraitsChangeCollector from .testing import find_qt_widget, print_qt_widget_tree 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 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.qt_app.flush() 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. 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') @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 should accept no arguments. 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=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(): set_event(trait) return handler handlers = {trait: make_handler(trait) for trait in traits} for trait, handler in handlers.items(): traits_object.on_trait_change(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.on_trait_change(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-6.1.2/pyface/ui/qt4/util/testing.py0000644000076500000240000000735013462774552021354 0ustar cwebsterstaff00000000000000# Copyright (c) 2013 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! """ Tools for testing. """ from __future__ import print_function 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(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-6.1.2/pyface/ui/qt4/system_metrics.py0000644000076500000240000000350713462774552021774 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Major package imports. from pyface.qt import QtGui # Enthought library imports. from traits.api import HasTraits, Int, Property, provides, Tuple # Local imports. 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 QtGui.QApplication.instance().desktop().screenGeometry().width() def _get_screen_height(self): return QtGui.QApplication.instance().desktop().screenGeometry().height() def _get_dialog_background_color(self): color = QtGui.QApplication.instance().palette().color(QtGui.QPalette.Window) return (color.redF(), color.greenF(), color.blueF()) #### EOF ###################################################################### pyface-6.1.2/pyface/ui/qt4/dialog.py0000644000076500000240000001176413462774552020165 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Major package imports. from pyface.qt import QtCore, QtGui # Enthought library imports. from traits.api import Bool, Enum, Int, provides, Str, Unicode # Local imports. 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.Accepted): OK, int(QtGui.QDialog.Rejected): CANCEL, int(QtGui.QMessageBox.Ok): OK, int(QtGui.QMessageBox.Cancel): CANCEL, int(QtGui.QMessageBox.Yes): YES, int(QtGui.QMessageBox.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 = Unicode help_id = Str help_label = Unicode ok_label = Unicode resizeable = Bool(True) return_code = Int(OK) style = Enum('modal', 'nonmodal') #### 'IWindow' interface ################################################## title = Unicode("Dialog") ########################################################################### # 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.AcceptRole) else: btn = buttons.addButton(QtGui.QDialogButtonBox.Ok) btn.setDefault(True) btn.clicked.connect(self.control.accept) # 'Cancel' button. if self.cancel_label: btn = buttons.addButton(self.cancel_label, QtGui.QDialogButtonBox.RejectRole) else: btn = buttons.addButton(QtGui.QDialogButtonBox.Cancel) btn.clicked.connect(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.HelpRole) else: buttons.addButton(QtGui.QDialogButtonBox.Help) return buttons def _create_contents(self, parent): layout = QtGui.QVBoxLayout() if not self.resizeable: layout.setSizeConstraint(QtGui.QLayout.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.Window, QtGui.QColor('red')) panel.setPalette(palette) panel.setAutoFillBackground(True) return panel def _show_modal(self): self.control.setWindowModality(QtCore.Qt.ApplicationModal) retval = self.control.exec_() return _RESULT_MAP[retval] ########################################################################### # 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) 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-6.1.2/pyface/ui/qt4/images/0000755000076500000240000000000013515277237017605 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/qt4/images/heading_level_1.png0000644000076500000240000000424013462774552023324 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-6.1.2/pyface/ui/qt4/images/application.png0000644000076500000240000000651613462774552022631 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-6.1.2/pyface/ui/qt4/images/image_LICENSE.txt0000644000076500000240000000121413462774552022573 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: ---------------------------------------------------------------------------- enthought/pyface/ui/qt4/images: application.png | Enthought heading_level_1.png | Enthought pyface-6.1.2/pyface/ui/qt4/heading_text.py0000644000076500000240000000530513462774552021363 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Major package imports. from pyface.qt import QtCore, QtGui # Enthought library imports. from traits.api import Int, provides, Unicode # Local imports. from pyface.i_heading_text import IHeadingText, MHeadingText from .widget import Widget @provides(IHeadingText) class HeadingText(MHeadingText, Widget): """ The toolkit specific implementation of a HeadingText. See the IHeadingText interface for the API documentation. """ #### 'IHeadingText' interface ############################################# level = Int(1) text = Unicode('Default') ########################################################################### # 'object' interface. ########################################################################### def __init__(self, parent, **traits): """ Creates the panel. """ # Base class constructor. super(HeadingText, self).__init__(**traits) # Create the toolkit-specific control that represents the widget. self._create_control(parent) ########################################################################### # Private interface. ########################################################################### def _create_control(self, parent): """ Create the toolkit-specific control that represents the widget. """ self.control = QtGui.QLabel(parent) self._set_text(self.text) self.control.setFrameShape(QtGui.QFrame.StyledPanel) self.control.setFrameShadow(QtGui.QFrame.Raised) self.control.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed) def _set_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 = "" + text + "" self.control.setText(text) #### Trait event handlers ################################################# def _text_changed(self, new): """ Called when the text is changed. """ if self.control is not None: self._set_text(new) #### EOF ###################################################################### pyface-6.1.2/pyface/ui/qt4/timer/0000755000076500000240000000000013515277237017460 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/qt4/timer/timer.py0000644000076500000240000000162113462774552021155 0ustar cwebsterstaff00000000000000# Copyright (c) 2007, Riverbank Computing Limited # Copyright (c) 2018, Enthought Inc # 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 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(PyfaceTimer, self).__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-6.1.2/pyface/ui/qt4/timer/__init__.py0000644000076500000240000000010413462774552021567 0ustar cwebsterstaff00000000000000# Copyright (c) 2007-2011 by Enthought, Inc. # All rights reserved. pyface-6.1.2/pyface/ui/qt4/timer/do_later.py0000644000076500000240000000101113462774552021617 0ustar cwebsterstaff00000000000000# Copyright (c) 2018, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! """ DoLaterTimer class Provided for backward compatibility. """ from pyface.timer.do_later import DoLaterTimerpyface-6.1.2/pyface/ui/qt4/tests/0000755000076500000240000000000013515277237017502 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/qt4/tests/test_qt_imports.py0000644000076500000240000000062013502141171023271 0ustar cwebsterstaff00000000000000import unittest class TestPyfaceQtImports(unittest.TestCase): def test_imports(self): # check that all Qt API imports work import pyface.qt.QtCore import pyface.qt.QtGui import pyface.qt.QtNetwork import pyface.qt.QtOpenGL import pyface.qt.QtScript import pyface.qt.QtSvg import pyface.qt.QtTest import pyface.qt.QtWebKitpyface-6.1.2/pyface/ui/qt4/tests/test_progress_dialog.py0000644000076500000240000001047413500710640024264 0ustar cwebsterstaff00000000000000from __future__ import absolute_import 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-6.1.2/pyface/ui/qt4/tests/__init__.py0000644000076500000240000000000013462774552021604 0ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/qt4/tests/test_mimedata.py0000644000076500000240000001305713462774552022705 0ustar cwebsterstaff00000000000000# # (C) Copyright 2013 Enthought, Inc., Austin, TX # All rights reserved. # # This file is open source software distributed according to the terms in # LICENSE.txt # import unittest from six.moves.cPickle 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) if __name__ == '__main__': unittest.main() pyface-6.1.2/pyface/ui/qt4/tests/test_gui.py0000644000076500000240000000430713462774552021706 0ustar cwebsterstaff00000000000000""" Qt-specific tests for the Qt GUI implementation. """ from __future__ import absolute_import 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(HasStrictTraits, self).__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(): # Record whether the event loop is running or not, then exit. application_running.append( is_event_loop_running_qt4() ) application.stop() application.on_trait_change(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() qt_app.flush() self.assertTrue(application_running[0]) pyface-6.1.2/pyface/ui/qt4/tests/bad_import.py0000644000076500000240000000024013462774552022173 0ustar cwebsterstaff00000000000000# 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-6.1.2/pyface/ui/qt4/wizard/0000755000076500000240000000000013515277237017640 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/qt4/wizard/wizard_page.py0000644000076500000240000000775013462774552022522 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ """ A page in a wizard. """ # Major package imports. from pyface.qt import QtCore, QtGui # Enthought library imports. from traits.api import Bool, HasTraits, provides, Str, Tuple, Unicode 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 = Unicode subheading = Unicode 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.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.on_trait_change(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): """ The trait handler for when the page's completion state changes. """ self.completeChanged.emit() #### EOF ###################################################################### pyface-6.1.2/pyface/ui/qt4/wizard/__init__.py0000644000076500000240000000010413462774552021747 0ustar cwebsterstaff00000000000000# Copyright (c) 2007-2011 by Enthought, Inc. # All rights reserved. pyface-6.1.2/pyface/ui/qt4/wizard/wizard.py0000644000076500000240000001537213462774552021525 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ """ The base class for all pyface wizards. """ # Major package imports. from __future__ import print_function from pyface.qt import QtCore, QtGui # Enthought library imports. from traits.api import Bool, Instance, List, Property, provides, Unicode 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 = Unicode('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.NoDefaultButton | QtGui.QWizard.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.NoCancelButton) if self.help_id: control.setOption(QtGui.QWizard.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 #### EOF ###################################################################### pyface-6.1.2/pyface/ui/qt4/resource_manager.py0000644000076500000240000000355013462774552022241 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Major package imports. from pyface.qt import QtCore, QtGui, QtSvg # Enthought library imports. 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.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 #### EOF ###################################################################### pyface-6.1.2/pyface/ui/qt4/__init__.py0000644000076500000240000000010413462774552020447 0ustar cwebsterstaff00000000000000# Copyright (c) 2007-2011 by Enthought, Inc. # All rights reserved. pyface-6.1.2/pyface/ui/qt4/splash_screen.py0000644000076500000240000000542513462774552021554 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Standard library imports. from logging import DEBUG # Major package imports. from pyface.qt import QtCore, QtGui # Enthought library imports. from traits.api import Any, Bool, Font, Instance, Int, provides from traits.api import Tuple, Unicode # Local imports. from pyface.i_splash_screen import ISplashScreen, MSplashScreen from pyface.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 = Instance(ImageResource, ImageResource('splash')) log_level = Int(DEBUG) show_log_messages = Bool(True) text = Unicode 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.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.AlignLeft, text_color) #### EOF ###################################################################### pyface-6.1.2/pyface/ui/qt4/application_window.py0000644000076500000240000001535013462774552022613 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms described # in the PyQt GPL exception also apply. # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ import sys # Major package imports. from pyface.qt import QtGui # Enthought library imports. from pyface.action.api import MenuBarManager, StatusBarManager from pyface.action.api import ToolBarManager from traits.api import Instance, List, on_trait_change, provides, Unicode # Local imports. from pyface.i_application_window import ( IApplicationWindow, MApplicationWindow ) from pyface.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 = Instance(ImageResource) #: 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 = Unicode("Pyface") ########################################################################### # Protected 'IApplicationWindow' interface. ########################################################################### def _create_contents(self, parent): panel = QtGui.QWidget(parent) palette = QtGui.QPalette(panel.palette()) palette.setColor(QtGui.QPalette.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(ApplicationWindow, self)._create() contents = self._create_contents(self.control) self.control.setCentralWidget(contents) self._create_trim_widgets(self.control) def _create_control(self, parent): control = super(ApplicationWindow, self)._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. 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: old.destroy_status_bar() self._create_status_bar(self.control) @on_trait_change('tool_bar_manager, tool_bar_managers') def _update_tool_bar_managers(self): 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) def _icon_changed(self): self._set_window_icon() pyface-6.1.2/pyface/ui/qt4/progress_dialog.py0000644000076500000240000002116213462774552022102 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A simple progress bar intended to run in the UI thread """ import time from pyface.qt import QtGui, QtCore from traits.api import Bool, Instance, Int, Unicode, provides 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 = Unicode #: The text message to display in the dialog message = Unicode #: 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 = Unicode('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) #------------------------------------------------------------------------- # IWindow Interface #------------------------------------------------------------------------- def open(self): """ Opens the window. """ super(ProgressDialog, self).open() self._start_time = time.time() def close(self): """ Closes the window. """ self.progress_bar.destroy() self.progress_bar = None super(ProgressDialog, self).close() #------------------------------------------------------------------------- # 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.RejectRole) if self.can_ok: buttons.addButton(QtGui.QDialogButtonBox.Ok) # TODO: hookup the buttons to our methods, this may involve subclassing from QDialog if self.can_cancel: buttons.rejected.connect(dialog.reject) if self.can_ok: buttons.accepted.connect(dialog.accept) layout.addWidget(buttons) def _create_label(self, dialog, layout, text): dummy = QtGui.QLabel(text, dialog) dummy.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft) label = QtGui.QLabel("unknown", dialog) label.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft | QtCore.Qt.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.AlignTop | QtCore.Qt.AlignLeft) layout.addWidget(label) self._message_control = label return 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(ProgressDialog, self)._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-6.1.2/pyface/ui/qt4/action/0000755000076500000240000000000013515277237017615 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/qt4/action/tool_bar_manager.py0000644000076500000240000001437013462774552023472 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms described in the PyQt GPL exception also apply #------------------------------------------------------------------------------ # Major package imports. from pyface.qt import QtCore, QtGui # Enthought library imports. from traits.api import Bool, Enum, Instance, Str, Tuple # Local imports. 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) ########################################################################### # 'object' interface. ########################################################################### def __init__(self, *args, **traits): """ Creates a new tool bar manager. """ # Base class constructor. super(ToolBarManager, self).__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) tool_bar.setObjectName(self.id) tool_bar.setWindowTitle(self.name) if self.show_tool_names: tool_bar.setToolButtonStyle(QtCore.Qt.ToolButtonTextUnderIcon) if self.orientation == 'horizontal': tool_bar.setOrientation(QtCore.Qt.Horizontal) else: tool_bar.setOrientation(QtCore.Qt.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 ########################################################################### # 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.on_trait_change(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 ) return def _separator_visibility_method(self, separator): """ Method to return closure to set visibility of group separators. """ return lambda visible: separator.setVisible(visible) 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) # Listen for changes to the tool bar manager's enablement and # visibility. self.tool_bar_manager = tool_bar_manager self.tool_bar_manager.on_trait_change( self._on_tool_bar_manager_enabled_changed, 'enabled' ) self.tool_bar_manager.on_trait_change( self._on_tool_bar_manager_visible_changed, 'visible' ) return ########################################################################### # Trait change handlers. ########################################################################### def _on_tool_bar_manager_enabled_changed(self, obj, trait_name, old, new): """ Dynamic trait change handler. """ self.setEnabled(new) return def _on_tool_bar_manager_visible_changed(self, obj, trait_name, old, new): """ Dynamic trait change handler. """ self.setVisible(new) return #### EOF ###################################################################### pyface-6.1.2/pyface/ui/qt4/action/menu_manager.py0000644000076500000240000001643013462774552022634 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ """ The PyQt specific implementation of a menu manager. """ # Major package imports. from pyface.qt import QtCore, QtGui # Enthought library imports. from traits.api import Instance, Unicode # Local imports. 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 = Unicode # The default action for tool button when shown in a toolbar (Qt only) action = Instance(Action) ########################################################################### # 'MenuManager' interface. ########################################################################### def create_menu(self, parent, controller=None): """ Creates a menu representation of the manager. """ # 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. """ 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.on_trait_change(self.refresh, 'changed') self._manager.on_trait_change(self._on_enabled_changed, 'enabled') self._manager.on_trait_change(self._on_visible_changed, 'visible') self._manager.on_trait_change(self._on_name_changed, 'name') self.setEnabled(self._manager.enabled) self.menuAction().setVisible(self._manager.visible) return ########################################################################### # '_Menu' interface. ########################################################################### def clear(self): """ Clears the items from the menu. """ for item in self.menu_items: item.dispose() self.menu_items = [] super(_Menu, self).clear() def is_empty(self): """ Is the menu empty? """ return self.isEmpty() def refresh(self): """ 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, obj, trait_name, old, new): """ Dynamic trait change handler. """ self.setEnabled(new) def _on_visible_changed(self, obj, trait_name, old, new): """ Dynamic trait change handler. """ self.menuAction().setVisible(new) return def _on_name_changed(self, obj, trait_name, old, new): """ Dynamic trait change handler. """ self.menuAction().setText(new) return 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 #### EOF ###################################################################### pyface-6.1.2/pyface/ui/qt4/action/status_bar_manager.py0000644000076500000240000001021213462774552024027 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms described in the PyQt GPL exception also apply #------------------------------------------------------------------------------ # Major package imports. from pyface.qt import QtGui # Enthought library imports. from traits.api import Any, Bool, HasTraits, List, Property, Str, \ Unicode 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(Unicode) # 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)) #### EOF ###################################################################### pyface-6.1.2/pyface/ui/qt4/action/__init__.py0000644000076500000240000000010413462774552021724 0ustar cwebsterstaff00000000000000# Copyright (c) 2007-2011 by Enthought, Inc. # All rights reserved. pyface-6.1.2/pyface/ui/qt4/action/action_item.py0000644000076500000240000004610213502141171022444 0ustar cwebsterstaff00000000000000# Copyright (c) 2007, Riverbank Computing Limited # Copyright (c) 2019, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms described # in the PyQt GPL exception also apply. # # Author: Riverbank Computing Limited # Description: """ The PyQt specific implementations the action manager internal classes. """ # Standard library imports. import six if six.PY2: from inspect import getargspec else: # avoid deprecation warning from inspect import getfullargspec as getargspec # Major package imports. from pyface.qt import QtGui, QtCore # Enthought library imports. from traits.api import Any, Bool, HasTraits # Local imports. from pyface.action.action_event import ActionEvent class PyfaceWidgetAction(QtGui.QWidgetAction): def __init__(self, parent, action): super(PyfaceWidgetAction, self).__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.AboutRole) elif action.menu_role == "Preferences": self.control.setMenuRole(QtGui.QAction.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.on_trait_change(self._on_action_enabled_changed, 'enabled') action.on_trait_change(self._on_action_visible_changed, 'visible') action.on_trait_change(self._on_action_checked_changed, 'checked') action.on_trait_change(self._on_action_name_changed, 'name') action.on_trait_change(self._on_action_accelerator_changed, 'accelerator') # 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.on_trait_change(self._on_action_enabled_changed, 'enabled', remove=True) action.on_trait_change(self._on_action_visible_changed, 'visible', remove=True) action.on_trait_change(self._on_action_checked_changed, 'checked', remove=True) action.on_trait_change(self._on_action_name_changed, 'name', remove=True) action.on_trait_change(self._on_action_accelerator_changed, 'accelerator', 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 = getargspec(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 = getargspec(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, action, trait_name, old, new): """ Called when the enabled trait is changed on an action. """ if self.control is not None: self.control.setEnabled(action.enabled) def _on_action_visible_changed(self, action, trait_name, old, new): """ Called when the visible trait is changed on an action. """ if self.control is not None: self.control.setVisible(action.visible) def _on_action_checked_changed(self, action, trait_name, old, new): """ Called when the checked trait is changed on an action. """ if self.control is not None: self.control.setChecked(action.checked) def _on_action_name_changed(self, action, trait_name, old, new): """ Called when the name trait is changed on an action. """ if self.control is not None: self.control.setText(action.name) def _on_action_accelerator_changed(self, action, trait_name, old, new): """ Called when the accelerator trait is changed on an action. """ if self.control is not None: self.control.setShortcut(action.accelerator) 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) 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.on_trait_change(self._on_action_enabled_changed, 'enabled') action.on_trait_change(self._on_action_visible_changed, 'visible') action.on_trait_change(self._on_action_checked_changed, 'checked') action.on_trait_change(self._on_action_name_changed, 'name') action.on_trait_change(self._on_action_accelerator_changed, 'accelerator') # 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) ########################################################################### # 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 = getargspec(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 = getargspec(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, action, trait_name, old, new): """ Called when the enabled trait is changed on an action. """ if self.control is not None: self.control.setEnabled(action.enabled) def _on_action_visible_changed(self, action, trait_name, old, new): """ Called when the visible trait is changed on an action. """ if self.control is not None: self.control.setVisible(action.visible) def _on_action_checked_changed(self, action, trait_name, old, new): """ Called when the checked trait is changed on an action. """ if self.control is not None: self.control.setChecked(action.checked) def _on_action_name_changed(self, action, trait_name, old, new): """ Called when the name trait is changed on an action. """ if self.control is not None: self.control.setText(action.name) def _on_action_accelerator_changed(self, action, trait_name, old, new): """ Called when the accelerator trait is changed on an action. """ if self.control is not None: self.control.setShortcut(action.accelerator) 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('&', '') image = action.image.create_image() 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.on_trait_change(self._on_action_enabled_changed, 'enabled') action.on_trait_change(self._on_action_checked_changed, 'checked') return ########################################################################### # Private interface. ########################################################################### #### Trait event handlers ################################################# def _on_action_enabled_changed(self, action, trait_name, old, new): """ Called when the enabled trait is changed on an action. """ self.tool_palette.enable_tool(self.tool_id, action.enabled) return def _on_action_checked_changed(self, action, trait_name, old, new): """ Called when the checked trait is changed on an action. """ 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 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, 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() is_checkable = (action.style == 'radio' or action.style == 'check') # Perform the action! action.checked = self.tool_palette.get_tool_state(self.tool_id) == 1 action.perform(action_event) return pyface-6.1.2/pyface/ui/qt4/action/menu_bar_manager.py0000644000076500000240000000435713462774552023465 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ """ The PyQt specific implementation of a menu bar manager. """ # Standard library imports. import sys # Major package imports. from pyface.qt import QtGui # Local imports. 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 #### EOF ###################################################################### pyface-6.1.2/pyface/ui/qt4/workbench/0000755000076500000240000000000013515277237020322 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/qt4/workbench/images/0000755000076500000240000000000013515277237021567 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/qt4/workbench/images/spinner.gif0000644000076500000240000000331113462774552023735 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-6.1.2/pyface/ui/qt4/workbench/tests/0000755000076500000240000000000013515277237021464 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/qt4/workbench/tests/__init__.py0000644000076500000240000000000013462774552023566 0ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/qt4/workbench/tests/test_workbench_window_layout.py0000644000076500000240000000155613500710640030032 0ustar cwebsterstaff00000000000000from __future__ import absolute_import import mock import unittest from pyface.ui.qt4.workbench.split_tab_widget import SplitTabWidget from pyface.ui.qt4.workbench.workbench_window_layout import \ WorkbenchWindowLayout class TestWorkbenchWindowLayout(unittest.TestCase): def test_change_of_active_qt_editor(self): # Test error condition for enthought/mayavi#321 mock_split_tab_widget = mock.Mock(spec=SplitTabWidget) layout = WorkbenchWindowLayout(_qt4_editor_area=mock_split_tab_widget) # This should not throw layout._qt4_active_editor_changed(None, None) self.assertEqual(mock_split_tab_widget.setTabTextColor.called, False) mock_active_editor = mock.Mock() layout._qt4_active_editor_changed(None, mock_active_editor) self.assertEqual(mock_split_tab_widget.setTabTextColor.called, True) pyface-6.1.2/pyface/ui/qt4/workbench/__init__.py0000644000076500000240000000010413462774552022431 0ustar cwebsterstaff00000000000000# Copyright (c) 2007-2011 by Enthought, Inc. # All rights reserved. pyface-6.1.2/pyface/ui/qt4/workbench/view.py0000644000076500000240000000352413462774552021655 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Enthought library imports. 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.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 return def set_focus(self): """ Set the focus to the appropriate control in the part. """ if self.control is not None: self.control.setFocus() return #### EOF ###################################################################### pyface-6.1.2/pyface/ui/qt4/workbench/workbench_window_layout.py0000755000076500000240000005257713462774552025670 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Standard library imports. import logging # Major package imports. from pyface.qt import QtCore, QtGui # Enthought library imports. from traits.api import Instance, on_trait_change # Local imports. from pyface.message_dialog import error from pyface.workbench.i_workbench_window_layout import \ MWorkbenchWindowLayout from .split_tab_widget import SplitTabWidget import six # Logging. logger = logging.getLogger(__name__) # For mapping positions relative to the editor area. _EDIT_AREA_MAP = { 'left': QtCore.Qt.LeftDockWidgetArea, 'right': QtCore.Qt.RightDockWidgetArea, 'top': QtCore.Qt.TopDockWidgetArea, 'bottom': QtCore.Qt.BottomDockWidgetArea } # For mapping positions relative to another view. _VIEW_AREA_MAP = { 'left': (QtCore.Qt.Horizontal, True), 'right': (QtCore.Qt.Horizontal, False), 'top': (QtCore.Qt.Vertical, True), 'bottom': (QtCore.Qt.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 = six.text_type(title) def _qt4_editor_tab_spinner(self, editor, name, new): # Do we need to do this verification? tw, tidx = self._qt4_editor_area._tab_widget(editor.control) if new: tw.show_button(tidx) else: tw.hide_button(tidx) if not new and not editor == self.window.active_editor: self._qt4_editor_area.setTabTextColor(editor.control, QtCore.Qt.red) @on_trait_change('window:active_editor') def _qt4_active_editor_changed(self, old, new): """ Handle change of active editor """ # Reset tab title to foreground color if new is not None: self._qt4_editor_area.setTabTextColor(new.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.on_trait_change(self._qt4_editor_tab_spinner, '_loading') self.editor_opened = editor def on_name_changed(editor, trait_name, old, new): self._qt4_editor_area.setWidgetTitle(editor.control, editor.name) editor.on_trait_change(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(): view._qt4_dock.setWindowTitle(view.name) view.on_trait_change(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) #### EOF ###################################################################### pyface-6.1.2/pyface/ui/qt4/workbench/editor.py0000644000076500000240000000465113462774552022173 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Local imports. 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.Window, QtCore.Qt.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() return def set_focus(self): """ Set the focus to the appropriate control in the part. """ if self.control is not None: self.control.setFocus() return #### EOF ###################################################################### pyface-6.1.2/pyface/ui/qt4/workbench/split_tab_widget.py0000644000076500000240000010526313462774552024232 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2008, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms described in the PyQt GPL exception also apply #------------------------------------------------------------------------------ # Standard library imports. import sys import six # Major library imports. 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, six.text_type) _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 = six.text_type(ch.widget(t).objectName()) title = six.text_type(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.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.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.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.Vertical) if hs == self._HS_SOUTH: idx = -1 else: new_spl = QtGui.QSplitter(QtCore.Qt.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.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.Horizontal) if hs == self._HS_EAST: idx = -1 else: new_spl = QtGui.QSplitter(QtCore.Qt.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.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.North, QtGui.QTabWidget.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.CacheAll) movie.setScaledSize(QtCore.QSize(16, 16)) lbl.setMovie(movie) movie.start() self.tabBar().setTabButton(index, QtGui.QTabBar.LeftSide, lbl) def hide_button(self, index): curr = self.tabBar().tabButton(index, QtGui.QTabBar.LeftSide) if curr: curr.close() self.tabBar().setTabButton(index, QtGui.QTabBar.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.NoPen) p.setRenderHint(QtGui.QPainter.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_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_Left: self._root._move_left(self.parent(), self.currentIndex()) elif e.key() == QtCore.Qt.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.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.OpenHandCursor) def mouseReleaseEvent(self, e): """ Reimplemented to handle mouse release events. """ QtGui.QTabBar.mouseReleaseEvent(self, e) if e.button() != QtCore.Qt.LeftButton: if e.button() == QtCore.Qt.MidButton: 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, u'\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.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.WA_TransparentForMouseEvents) ctb.setWindowFlags(QtCore.Qt.FramelessWindowHint | QtCore.Qt.Tool | QtCore.Qt.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-6.1.2/pyface/ui/qt4/split_widget.py0000644000076500000240000000613313462774552021416 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ """ Mix-in class for split widgets. """ # Major package imports. from pyface.qt import QtCore, QtGui # Enthought library imports. from traits.api import Callable, Enum, Float, HasTraits, provides # Local imports. from pyface.i_split_widget import ISplitWidget, MSplitWidget @provides(ISplitWidget) class SplitWidget(MSplitWidget, HasTraits): """ The toolkit specific implementation of a SplitWidget. See the ISPlitWidget interface for the API documentation. """ #### 'ISplitWidget' interface ############################################# direction = Enum('vertical', 'vertical', 'horizontal') ratio = Float(0.5) lhs = Callable rhs = Callable ########################################################################### # 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.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 #### EOF ###################################################################### pyface-6.1.2/pyface/ui/qt4/code_editor/0000755000076500000240000000000013515277237020620 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/qt4/code_editor/pygments_highlighter.py0000644000076500000240000002141713462774552025426 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2010, Enthought Inc # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # # Author: Enthought Inc # Description: #------------------------------------------------------------------------------ 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 import six 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, u'\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(PygmentsHighlighter, self).__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 = six.text_type(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 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.Bold) elif key == 'italic': result.setFontItalic(True) elif key == 'underline': result.setUnderlineStyle( QtGui.QTextCharFormat.SingleUnderline) elif key == 'sans': result.setFontStyleHint(QtGui.QFont.SansSerif) elif key == 'roman': result.setFontStyleHint(QtGui.QFont.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.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-6.1.2/pyface/ui/qt4/code_editor/tests/0000755000076500000240000000000013515277237021762 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/qt4/code_editor/tests/test_code_widget.py0000644000076500000240000000645613462774552025666 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2016, Enthought Inc # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # # Author: Enthought Inc # Description: #------------------------------------------------------------------------------ # Standard library imports. import unittest import mock # System library imports. from pyface.qt import QtCore, QtGui from pyface.qt.QtTest import QTest # Enthought library imports. # Local imports. 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_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.Find', FindKey) patcher_replace = mock.patch('pyface.qt.QtGui.QKeySequence.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.KeyboardModifierMask) modifier = QtCore.Qt.KeyboardModifier( first_key & QtCore.Qt.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()) if __name__ == '__main__': unittest.main() pyface-6.1.2/pyface/ui/qt4/code_editor/tests/__init__.py0000644000076500000240000000000013462774552024064 0ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/qt4/code_editor/__init__.py0000644000076500000240000000056713462774552022744 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2010, Enthought Inc # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # # Author: Enthought Inc # Description: #------------------------------------------------------------------------------ pyface-6.1.2/pyface/ui/qt4/code_editor/code_widget.py0000644000076500000240000006576113462774552023471 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2010, Enthought Inc # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # # Author: Enthought Inc # Description: #------------------------------------------------------------------------------ # Standard library imports import math import sys # System library imports from pyface.qt import QtCore, QtGui # Local imports from .find_widget import FindWidget from .gutters import LineNumberWidget, StatusGutterWidget from .replace_widget import ReplaceWidget from .pygments_highlighter import PygmentsHighlighter import six 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=True, font=None, lexer=None): super(CodeWidget, self).__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 = QtGui.QColor(QtCore.Qt.yellow).lighter(160) # 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.NoWrap) # Key bindings self.indent_key = QtGui.QKeySequence(QtCore.Qt.Key_Tab) self.unindent_key = QtGui.QKeySequence(QtCore.Qt.SHIFT + QtCore.Qt.Key_Backtab) self.comment_key = QtGui.QKeySequence(QtCore.Qt.CTRL + QtCore.Qt.Key_Slash) self.backspace_key = QtGui.QKeySequence(QtCore.Qt.Key_Backspace) 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 six.text_type(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.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.StartOfBlock) cursor.movePosition(QtGui.QTextCursor.Right, QtGui.QTextCursor.MoveAnchor, position) cursor.insertText(self.comment_character) def line_uncomment(self, cursor, position=0): cursor.movePosition(QtGui.QTextCursor.StartOfBlock) text = cursor.block().text() new_text = text[:position] + text[position+1:] cursor.movePosition(QtGui.QTextCursor.EndOfBlock, QtGui.QTextCursor.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.StartOfBlock) if cursor.block().text().startswith(tab): new_text = cursor.block().text()[len(tab):] cursor.movePosition(QtGui.QTextCursor.EndOfBlock, QtGui.QTextCursor.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.WordUnderCursor) return six.text_type(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(CodeWidget, self).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_Up)): cursor.movePosition(QtGui.QTextCursor.StartOfLine) if cursor.atStart(): self.setTextCursor(cursor) event.accept() elif key_sequence.matches(QtGui.QKeySequence(QtCore.Qt.Key_Down)): cursor.movePosition(QtGui.QTextCursor.EndOfLine) if cursor.atEnd(): self.setTextCursor(cursor) event.accept() elif self.auto_indent and \ key_sequence.matches(QtGui.QKeySequence(QtCore.Qt.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(CodeWidget, self).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()) width = font_metrics.width(' ') * 80 width += self.line_number_widget.sizeHint().width() width += self.status_widget.sizeHint().width() width += style.pixelMetric(QtGui.QStyle.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.StartOfBlock) cursor.movePosition(QtGui.QTextCursor.NextBlock, QtGui.QTextCursor.KeepAnchor, len(selected_blocks)) cursor.movePosition(QtGui.QTextCursor.EndOfBlock, QtGui.QTextCursor.KeepAnchor) self.setTextCursor(cursor) def _get_selected_blocks(self): cursor = self.textCursor() if cursor.position() > cursor.anchor(): move_op = QtGui.QTextCursor.PreviousBlock start_pos = cursor.anchor() end_pos = cursor.position() else: move_op = QtGui.QTextCursor.NextBlock start_pos = cursor.position() end_pos = cursor.anchor() cursor.setPosition(start_pos) cursor.movePosition(QtGui.QTextCursor.StartOfBlock) blocks = [cursor.block()] while cursor.movePosition(QtGui.QTextCursor.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(AdvancedCodeWidget, self).__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 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.FindCaseSensitively if self.active_find_widget.word_action.isChecked(): flags |= QtGui.QTextDocument.FindWholeWords if direction == 'backward': flags |= QtGui.QTextDocument.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.Left, QtGui.QTextCursor.MoveAnchor,len(replace)) find_cursor.movePosition( QtGui.QTextCursor.Right, QtGui.QTextCursor.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 = six.text_type(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 = six.text_type(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 = six.text_type(self.replace.line_edit.text()) replace_text = six.text_type(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) != 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.Find): self.enable_find() elif key_sequence.matches(QtGui.QKeySequence.Replace): if not self.code.isReadOnly(): self.enable_replace() elif key_sequence.matches(QtGui.QKeySequence(QtCore.Qt.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(AdvancedCodeWidget, self).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) import sys 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-6.1.2/pyface/ui/qt4/code_editor/replace_widget.py0000644000076500000240000000346413462774552024162 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2010, Enthought Inc # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # # Author: Enthought Inc # Description: #------------------------------------------------------------------------------ import weakref from pyface.qt import QtGui, QtCore from .find_widget import FindWidget class ReplaceWidget(FindWidget): def __init__(self, parent): super(FindWidget, self).__init__(parent) self.adv_code_widget = weakref.ref(parent) self.button_size = self.fontMetrics().width(u'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.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-6.1.2/pyface/ui/qt4/code_editor/find_widget.py0000644000076500000240000000475113462774552023467 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2010, Enthought Inc # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # # Author: Enthought Inc # Description: #------------------------------------------------------------------------------ import weakref from pyface.qt import QtGui, QtCore class FindWidget(QtGui.QWidget): def __init__(self, parent): super(FindWidget, self).__init__(parent) self.adv_code_widget = weakref.ref(parent) self.button_size = self.fontMetrics().width(u'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.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-6.1.2/pyface/ui/qt4/code_editor/gutters.py0000644000076500000240000000735413462774552022703 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2010, Enthought Inc # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # # Author: Enthought Inc # Description: #------------------------------------------------------------------------------ import math from pyface.qt import QtCore, QtGui class GutterWidget(QtGui.QWidget): min_width = 5 background_color = QtGui.QColor(220, 220, 220) 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(), QtCore.Qt.lightGray) 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(StatusGutterWidget, self).__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.background_color) 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.green) for line in self.warn_lines: painter.fillRect(QtCore.QRect(0, line*pixels_per_block, self.width(), 3), QtCore.Qt.yellow) for line in self.error_lines: painter.fillRect(QtCore.QRect(0, line*pixels_per_block, self.width(), 3), QtCore.Qt.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))) width = max(self.fontMetrics().width(u'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.background_color) cw = self.parent() block = cw.firstVisibleBlock() blocknum = block.blockNumber() top = cw.blockBoundingGeometry(block).translated( cw.contentOffset()).top() bottom = top + int(cw.blockBoundingRect(block).height()) while block.isValid() and top <= event.rect().bottom(): if block.isVisible() and bottom >= event.rect().top(): painter.setPen(QtCore.Qt.black) painter.drawText(0, top, self.width() - 2, self.fontMetrics().height(), QtCore.Qt.AlignRight, str(blocknum + 1)) block = block.next() top = bottom bottom = top + int(cw.blockBoundingRect(block).height()) blocknum += 1 pyface-6.1.2/pyface/ui/qt4/fields/0000755000076500000240000000000013515277237017606 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/qt4/fields/field.py0000644000076500000240000000520313462774552021246 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2017-19, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The Qt-specific implementation of the text field class """ from __future__ import ( absolute_import, division, print_function, unicode_literals ) from traits.api import Any, Instance, Unicode, provides from pyface.qt.QtCore import Qt from pyface.fields.i_field import IField, MField from pyface.ui.qt4.widget import Widget @provides(IField) class Field(MField, Widget): """ 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 #: A tooltip for the field. tooltip = Unicode #: An optional context menu for the field. context_menu = Instance('pyface.action.menu_manager.MenuManager') # ------------------------------------------------------------------------ # 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.DefaultContextMenu) self.control.customContextMenuRequested.disconnect( self._handle_control_context_menu) else: self.control.customContextMenuRequested.connect( self._handle_control_context_menu) self.control.setContextMenuPolicy(Qt.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()) pyface-6.1.2/pyface/ui/qt4/fields/__init__.py0000644000076500000240000000000013462774552021710 0ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/qt4/fields/combo_field.py0000644000076500000240000000744513462774552022437 0ustar cwebsterstaff00000000000000# Copyright (c) 2019, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: """ The Qt-specific implementation of the combo field class """ from __future__ import ( absolute_import, division, print_function, unicode_literals ) 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.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.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-6.1.2/pyface/ui/qt4/fields/text_field.py0000644000076500000240000001010613462774552022310 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2017-19, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The Qt-specific implementation of the text field class """ from __future__ import ( absolute_import, division, print_function, unicode_literals ) from traits.api import Trait, 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.Normal, 'password': QLineEdit.Password, 'none': QLineEdit.NoEcho, 'when_editing': QLineEdit.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 = Trait( 'normal', ECHO_TO_QT_ECHO_MODE, ) @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-6.1.2/pyface/ui/qt4/fields/spin_field.py0000644000076500000240000000452113462774552022301 0ustar cwebsterstaff00000000000000# Copyright (c) 2017-19, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: """ The Qt-specific implementation of the spin field class """ from __future__ import ( absolute_import, division, print_function, unicode_literals ) 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-6.1.2/pyface/ui/qt4/beep.py0000644000076500000240000000027413462774552017633 0ustar cwebsterstaff00000000000000# 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-6.1.2/pyface/ui/qt4/about_dialog.py0000644000076500000240000000620513462774552021351 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Standard library imports. import platform import sys # Major package imports. from pyface.qt import QtCore, QtGui # Enthought library imports. from traits.api import Instance, List, provides, Unicode # Local imports. from pyface.i_about_dialog import IAboutDialog, MAboutDialog from pyface.image_resource import ImageResource from .dialog import Dialog # The HTML displayed in the QLabel. _DIALOG_TEXT = '''

%s

Python %s
Qt %s

Copyright © 2003-2010 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(Unicode) image = Instance(ImageResource, ImageResource('about')) ########################################################################### # 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 # Load the image to be displayed in the about box. image = self.image.create_image() 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__ # Set the page contents. label.setText(_DIALOG_TEXT % (path, additions, py_version, qt_version)) # Create the button. buttons = QtGui.QDialogButtonBox() if self.ok_label: buttons.addButton(self.ok_label, QtGui.QDialogButtonBox.AcceptRole) else: buttons.addButton(QtGui.QDialogButtonBox.Ok) buttons.accepted.connect(parent.accept) lay = QtGui.QVBoxLayout() lay.addWidget(label) lay.addWidget(buttons) parent.setLayout(lay) pyface-6.1.2/pyface/ui/qt4/python_editor.py0000644000076500000240000001541613462774552021613 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Standard library imports. import sys # Major package imports. from pyface.qt import QtCore, QtGui # Enthought library imports. from traits.api import Bool, Event, provides, Unicode # Local imports. from pyface.i_python_editor import IPythonEditor, MPythonEditor from pyface.key_pressed_event import KeyPressedEvent from pyface.widget import Widget from pyface.ui.qt4.code_editor.code_widget import AdvancedCodeWidget @provides(IPythonEditor) class PythonEditor(MPythonEditor, Widget): """ The toolkit specific implementation of a PythonEditor. See the IPythonEditor interface for the API documentation. """ #### 'IPythonEditor' interface ############################################ dirty = Bool(False) path = Unicode show_line_numbers = Bool(True) #### Events #### changed = Event key_pressed = Event(KeyPressedEvent) ########################################################################### # 'object' interface. ########################################################################### def __init__(self, parent, **traits): super(PythonEditor, self).__init__(parent=parent, **traits) self._create() ########################################################################### # '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.EndOfLine, QtGui.QTextCursor.KeepAnchor) ########################################################################### # 'Widget' interface. ########################################################################### def _add_event_listeners(self): super(PythonEditor, self)._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) if self._event_filter is not None: self.control.code.removeEventFilter(self._event_filter) super(PythonEditor, self)._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(PythonEditorEventFilter, self).__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.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.KeyPress: # Pyface doesn't seem to be Unicode 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.AltModifier) == QtCore.Qt.AltModifier), control_down = ((mods & QtCore.Qt.ControlModifier) == QtCore.Qt.ControlModifier), shift_down = ((mods & QtCore.Qt.ShiftModifier) == QtCore.Qt.ShiftModifier), key_code = kcode, event = event) return super(PythonEditorEventFilter, self).eventFilter(obj, event) pyface-6.1.2/pyface/ui/qt4/init.py0000644000076500000240000000302713462774552017662 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # Copyright (c) 2017, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license. However, when used with the GPL version of PyQt the additional # terms described in the PyQt GPL exception also apply. # # Author: Riverbank Computing Limited and Enthought developers #------------------------------------------------------------------------------ import sys from traits.trait_notifiers import set_ui_handler, ui_handler from pyface.qt import QtCore, QtGui, qt_api from pyface.base_toolkit import Toolkit from .gui import GUI if qt_api == 'pyqt': # Check the version numbers are late enough. if QtCore.QT_VERSION < 0x040200: raise RuntimeError( "Need Qt v4.2 or higher, but got v%s" % QtCore.QT_VERSION_STR ) if QtCore.PYQT_VERSION < 0x040100: raise RuntimeError( "Need PyQt v4.1 or higher, but got v%s" % QtCore.PYQT_VERSION_STR ) # It's possible that it has already been initialised. _app = QtGui.QApplication.instance() if _app is None: _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-6.1.2/pyface/ui/qt4/clipboard.py0000644000076500000240000000712613462774552020662 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2009, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Evan Patterson # Date: 06/26/09 #------------------------------------------------------------------------------ # Standard library imports from io import BytesIO from six.moves.cPickle import dumps, load, loads # System library imports from pyface.qt import QtCore, QtGui # ETS imports from traits.api import provides from pyface.i_clipboard import IClipboard, BaseClipboard import six # 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()) klass = load(serialized_data) 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, six.string_types): 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-6.1.2/pyface/ui/qt4/mimedata.py0000644000076500000240000001121213462774552020473 0ustar cwebsterstaff00000000000000 from six.moves.cPickle import dumps, load, loads, PickleError import warnings import io import sys from pyface.qt import QtCore #------------------------------------------------------------------------------- # 'PyMimeData' class: #------------------------------------------------------------------------------- if sys.version_info[0] < 3: def str2bytes(s): return s else: 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 = u'application/x-ets-qt4-instance' NOPICKLE_MIME_TYPE = u'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-6.1.2/pyface/ui/qt4/gui.py0000644000076500000240000001472413514577463017511 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Standard library imports. import logging # Major package imports. from pyface.qt import QtCore, QtGui # Enthought library imports. from traits.api import Bool, HasTraits, provides, Unicode from pyface.util.guisupport import start_event_loop_qt4 # Local imports. 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 = Unicode ########################################################################### # '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.AllEvents else: events = QtCore.QEventLoop.ExcludeUserInputEvents QtCore.QCoreApplication.processEvents(events) @staticmethod def set_busy(busy=True): if busy: QtGui.QApplication.setOverrideCursor(QtCore.Qt.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() def _busy_changed(self, new): """ The busy trait change handler. """ if new: QtGui.QApplication.setOverrideCursor(QtCore.Qt.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(_FutureCall, self).__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(_FutureCall, self).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-6.1.2/pyface/ui/qt4/image_cache.py0000644000076500000240000000537113462774552021130 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Major package imports. from pyface.qt import QtGui # Enthought library imports. from traits.api import HasTraits, provides # Local imports. 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 #### EOF ###################################################################### pyface-6.1.2/pyface/ui/qt4/file_dialog.py0000644000076500000240000001257013462774552021160 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # However, when used with the GPL version of PyQt the additional terms described in the PyQt GPL exception also apply # # Author: Riverbank Computing Limited # Description: #------------------------------------------------------------------------------ # Standard library imports. import os # Major package imports. from pyface.qt import QtCore, QtGui # Enthought library imports. from traits.api import Enum, Int, List, provides, Unicode # Local imports. from pyface.i_file_dialog import IFileDialog, MFileDialog from .dialog import Dialog import six @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 = Unicode default_filename = Unicode default_path = Unicode directory = Unicode filename = Unicode path = Unicode paths = List(Unicode) wildcard = Unicode 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, six.string_types): 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 = six.text_type(files[0]) self.paths = [six.text_type(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(FileDialog, self).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.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.AcceptOpen) dlg.setFileMode(QtGui.QFileDialog.ExistingFile) elif self.action == 'open files': dlg.setAcceptMode(QtGui.QFileDialog.AcceptOpen) dlg.setFileMode(QtGui.QFileDialog.ExistingFiles) else: dlg.setAcceptMode(QtGui.QFileDialog.AcceptSave) dlg.setFileMode(QtGui.QFileDialog.AnyFile) return dlg ########################################################################### # Trait handlers. ########################################################################### def _wildcard_default(self): """ Return the default wildcard. """ return self.WILDCARD_ALL #### EOF ###################################################################### pyface-6.1.2/pyface/ui/qt4/console/0000755000076500000240000000000013515277237020002 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/qt4/console/bracket_matcher.py0000644000076500000240000000723413462774552023503 0ustar cwebsterstaff00000000000000""" Provides bracket matching for Q[Plain]TextEdit widgets. """ # System library imports 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(BracketMatcher, self).__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) #-------------------------------------------------------------------------- # 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.NextCharacter, QtGui.QTextCursor.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-6.1.2/pyface/ui/qt4/console/history_console_widget.py0000644000076500000240000001502713462774552025152 0ustar cwebsterstaff00000000000000# Copyright (c) 2011-18, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: Enthought, Inc. # System library imports from pyface.qt import QtGui # Local imports 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(HistoryConsoleWidget, self).__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(HistoryConsoleWidget, self).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.Right, n=len(self._history_prefix)) else: cursor.movePosition(QtGui.QTextCursor.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.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-6.1.2/pyface/ui/qt4/console/__init__.py0000644000076500000240000000000013462774552022104 0ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/ui/qt4/console/api.py0000644000076500000240000000035013462774552021126 0ustar cwebsterstaff00000000000000from .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-6.1.2/pyface/ui/qt4/console/call_tip_widget.py0000644000076500000240000002130713462774552023514 0ustar cwebsterstaff00000000000000# Standard library imports import re from textwrap import dedent from unicodedata import category # System library imports 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(CallTipWidget, self).__init__(None, QtCore.Qt.ToolTip) self._hide_timer = QtCore.QBasicTimer() self._text_edit = text_edit self.setFont(text_edit.document().defaultFont()) self.setForegroundRole(QtGui.QPalette.ToolTipText) self.setBackgroundRole(QtGui.QPalette.ToolTipBase) self.setPalette(QtGui.QToolTip.palette()) self.setAlignment(QtCore.Qt.AlignLeft) self.setIndent(1) self.setFrameStyle(QtGui.QFrame.NoFrame) self.setMargin(1 + self.style().pixelMetric( QtGui.QStyle.PM_ToolTipLabelFrameWidth, None, self)) self.setWindowOpacity(self.style().styleHint( QtGui.QStyle.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.KeyPress: key = event.key() if key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return): self.hide() elif key == QtCore.Qt.Key_Escape: self.hide() return True elif etype == QtCore.QEvent.FocusOut: self.hide() elif etype == QtCore.QEvent.Enter: self._hide_timer.stop() elif etype == QtCore.QEvent.Leave: self._leave_event_hide() return super(CallTipWidget, self).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(CallTipWidget, self).enterEvent(event) self._hide_timer.stop() def hideEvent(self, event): """ Reimplemented to disconnect signal handlers and event filter. """ super(CallTipWidget, self).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(CallTipWidget, self).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.PE_PanelTipLabel, option) painter.end() super(CallTipWidget, self).paintEvent(event) def setFont(self, font): """ Reimplemented to allow use of this method as a slot. """ super(CallTipWidget, self).setFont(font) def showEvent(self, event): """ Reimplemented to connect signal handlers and event filter. """ super(CallTipWidget, self).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 document = text_edit.document() 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) 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-6.1.2/pyface/ui/qt4/console/console_widget.py0000644000076500000240000022135413462774552023373 0ustar cwebsterstaff00000000000000""" 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 #----------------------------------------------------------------------------- # Standard library imports import os from os.path import commonprefix import re import sys from textwrap import dedent from unicodedata import category # System library imports 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_B : QtCore.Qt.Key_Left, QtCore.Qt.Key_F : QtCore.Qt.Key_Right, QtCore.Qt.Key_A : QtCore.Qt.Key_Home, QtCore.Qt.Key_P : QtCore.Qt.Key_Up, QtCore.Qt.Key_N : QtCore.Qt.Key_Down, QtCore.Qt.Key_D : QtCore.Qt.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_E] = QtCore.Qt.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_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O, QtCore.Qt.Key_V]) #--------------------------------------------------------------------------- # 'QObject' interface #--------------------------------------------------------------------------- def __init__(self, parent=None): """ Create a ConsoleWidget. Parameters: ----------- parent : QWidget, optional [default None] The parent for this widget. """ super(ConsoleWidget, self).__init__(parent) # 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.Horizontal) else: self._splitter.setOrientation(QtCore.Qt.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.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.addAction(action) self._print_action = action action = QtGui.QAction('Save as HTML/XML', None) action.setEnabled(self.can_export()) action.setShortcut(QtGui.QKeySequence.Save) action.triggered.connect(self.export) self.addAction(action) self._export_action = action action = QtGui.QAction('Select All', None) action.setEnabled(True) action.setShortcut(QtGui.QKeySequence.SelectAll) action.triggered.connect(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.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.KeyPress, self._ctrl_down_remap[key], QtCore.Qt.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.MouseButtonRelease and \ event.button() == QtCore.Qt.MidButton and \ obj == self._control.viewport(): cursor = self._control.cursorForPosition(event.pos()) self._control.setTextCursor(cursor) self.paste(QtGui.QClipboard.Selection) return True # Manually adjust the scrollbars *after* a resize event is dispatched. elif etype == QtCore.QEvent.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.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.DragEnter and \ obj == self._control.viewport() and \ event.source() == self._control.viewport(): self._filter_drag = True elif etype == QtCore.QEvent.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.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(ConsoleWidget, self).eventFilter(obj, event) #--------------------------------------------------------------------------- # '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.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. width = font_metrics.width(' ') * 81 + margin width += style.pixelMetric(QtGui.QStyle.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(width, 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.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.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.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.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.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) self._control.setTabStopWidth(self.tab_width * font_metrics.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.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.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.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.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.Ok, QtGui.QMessageBox.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.Question, title, msg) box.setInformativeText(info) box.addButton(ib,QtGui.QMessageBox.NoRole) box.addButton(eb,QtGui.QMessageBox.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) self._control.setTabStopWidth(tab_width * font_metrics.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.NextBlock): temp_cursor = QtGui.QTextCursor(cursor) temp_cursor.select(QtGui.QTextCursor.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.Left) # Grab the newline. cursor.movePosition(QtGui.QTextCursor.End, QtGui.QTextCursor.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.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.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.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.Cut) copy_action = menu.addAction('Copy', self.copy) copy_action.setEnabled(self.can_copy()) copy_action.setShortcut(QtGui.QKeySequence.Copy) paste_action = menu.addAction('Paste', self.paste) paste_action.setEnabled(self.can_paste()) paste_action.setShortcut(QtGui.QKeySequence.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.ControlModifier) return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier) else: return bool(modifiers & QtCore.Qt.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) control.customContextMenuRequested.connect( self._custom_context_menu_requested) control.copyAvailable.connect(self.copy_available) control.redoAvailable.connect(self.redo_available) control.undoAvailable.connect(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) # Configure the control. control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) control.setReadOnly(True) control.setUndoRedoEnabled(False) control.setVerticalScrollBarPolicy(QtCore.Qt.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.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.AltModifier shift_down = event.modifiers() & QtCore.Qt.ShiftModifier #------ Special sequences ---------------------------------------------- if event.matches(QtGui.QKeySequence.Copy): self.copy() intercepted = True elif event.matches(QtGui.QKeySequence.Cut): self.cut() intercepted = True elif event.matches(QtGui.QKeySequence.Paste): self.paste() intercepted = True #------ Special modifier logic ----------------------------------------- elif key in (QtCore.Qt.Key_Return, QtCore.Qt.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.End, QtGui.QTextCursor.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.End) self._control.setTextCursor(cursor) #------ Control/Cmd modifier ------------------------------------------- elif ctrl_down: if key == QtCore.Qt.Key_G: self._keyboard_quit() intercepted = True elif key == QtCore.Qt.Key_K: if self._in_buffer(position): cursor.movePosition(QtGui.QTextCursor.EndOfLine, QtGui.QTextCursor.KeepAnchor) if not cursor.hasSelection(): # Line deletion (remove continuation prompt) cursor.movePosition(QtGui.QTextCursor.NextBlock, QtGui.QTextCursor.KeepAnchor) cursor.movePosition(QtGui.QTextCursor.Right, QtGui.QTextCursor.KeepAnchor, len(self._continuation_prompt)) cursor.removeSelectedText() intercepted = True elif key == QtCore.Qt.Key_L: self.prompt_to_top() intercepted = True elif key == QtCore.Qt.Key_O: if self._page_control and self._page_control.isVisible(): self._page_control.setFocus() intercepted = True elif key == QtCore.Qt.Key_Y: self.paste() intercepted = True elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete): intercepted = True elif key == QtCore.Qt.Key_Plus: self.change_font_size(1) intercepted = True elif key == QtCore.Qt.Key_Minus: self.change_font_size(-1) intercepted = True #------ Alt modifier --------------------------------------------------- elif alt_down: if key == QtCore.Qt.Key_B: self._set_cursor(self._get_word_start_cursor(position)) intercepted = True elif key == QtCore.Qt.Key_F: self._set_cursor(self._get_word_end_cursor(position)) intercepted = True elif key == QtCore.Qt.Key_Backspace: cursor = self._get_word_start_cursor(position) cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor) cursor.removeSelectedText() intercepted = True elif key == QtCore.Qt.Key_D: cursor = self._get_word_end_cursor(position) cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor) cursor.removeSelectedText() intercepted = True elif key == QtCore.Qt.Key_Delete: intercepted = True elif key == QtCore.Qt.Key_Greater: self._control.moveCursor(QtGui.QTextCursor.End) intercepted = True elif key == QtCore.Qt.Key_Less: self._control.setTextCursor(self._get_prompt_cursor()) intercepted = True #------ No modifiers --------------------------------------------------- else: if shift_down: anchormode=QtGui.QTextCursor.KeepAnchor else: anchormode=QtGui.QTextCursor.MoveAnchor if key == QtCore.Qt.Key_Escape: self._keyboard_quit() intercepted = True elif key == QtCore.Qt.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_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_Tab: if not self._reading: intercepted = not self._tab_pressed() elif key == QtCore.Qt.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.PreviousBlock, mode=anchormode) self._control.moveCursor(QtGui.QTextCursor.EndOfBlock, mode=anchormode) intercepted = True # Regular left movement else: intercepted = not self._in_buffer(position - 1) elif key == QtCore.Qt.Key_Right: original_block_number = cursor.blockNumber() cursor.movePosition(QtGui.QTextCursor.Right, mode=anchormode) if cursor.blockNumber() != original_block_number: cursor.movePosition(QtGui.QTextCursor.Right, n=len(self._continuation_prompt), mode=anchormode) self._set_cursor(cursor) intercepted = True elif key == QtCore.Qt.Key_Home: start_line = cursor.blockNumber() if start_line == self._get_prompt_cursor().blockNumber(): start_pos = self._prompt_pos else: cursor.movePosition(QtGui.QTextCursor.StartOfBlock, QtGui.QTextCursor.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.KeepAnchor) else: cursor.setPosition(start_pos) self._set_cursor(cursor) intercepted = True elif key == QtCore.Qt.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.StartOfBlock, QtGui.QTextCursor.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_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.NextBlock, QtGui.QTextCursor.KeepAnchor) cursor.movePosition(QtGui.QTextCursor.Right, QtGui.QTextCursor.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.AltModifier if ctrl_down: if key == QtCore.Qt.Key_O: self._control.setFocus() return True elif alt_down: if key == QtCore.Qt.Key_Greater: self._page_control.moveCursor(QtGui.QTextCursor.End) return True elif key == QtCore.Qt.Key_Less: self._page_control.moveCursor(QtGui.QTextCursor.Start) return True elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape): if self._splitter: self._page_control.hide() else: self.layout().setCurrentWidget(self._control) return True elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return): new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, QtCore.Qt.Key_PageDown, QtCore.Qt.NoModifier) QtGui.QApplication.sendEvent(self._page_control, new_event) return True elif key == QtCore.Qt.Key_Backspace: new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, QtCore.Qt.Key_PageUp, QtCore.Qt.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.StartOfBlock) cursor.movePosition(QtGui.QTextCursor.EndOfBlock, QtGui.QTextCursor.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.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.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.Left, QtGui.QTextCursor.KeepAnchor) if cursor.selection().toPlainText() == ' ': cursor.removeSelectedText() else: cursor.movePosition(QtGui.QTextCursor.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.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.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.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.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.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.Left, QtGui.QTextCursor.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(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-6.1.2/pyface/ui/qt4/console/completion_lexer.py0000644000076500000240000000471413462774552023735 0ustar cwebsterstaff00000000000000# System library imports 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-6.1.2/pyface/i_drop_handler.py0000644000076500000240000000231413462774551020540 0ustar cwebsterstaff00000000000000from 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-6.1.2/pyface/mdi_application_window.py0000644000076500000240000000122413462774551022311 0ustar cwebsterstaff00000000000000# Copyright (c) 2017, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # Note: The MDIApplicationWindow is currently wx-specific # Import the toolkit specific version. from __future__ import absolute_import from .toolkit import toolkit_object MDIApplicationWindow = toolkit_object('mdi_application_window:MDIApplicationWindow') pyface-6.1.2/pyface/image_widget.py0000644000076500000240000000115713462774551020220 0ustar cwebsterstaff00000000000000# Copyright (c) 2017, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # Note: The ImageWidget is currently wx-specific # Import the toolkit specific version. from __future__ import absolute_import from .toolkit import toolkit_object ImageWidget = toolkit_object('image_widget:ImageWidget') pyface-6.1.2/pyface/expandable_panel.py0000644000076500000240000000117713462774551021057 0ustar cwebsterstaff00000000000000# Copyright (c) 2017, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # Note: The ExpandablePanel is currently wx-specific # Import the toolkit specific version. from __future__ import absolute_import from .toolkit import toolkit_object ExpandablePanel = toolkit_object('expandable_panel:ExpandablePanel') pyface-6.1.2/pyface/tasks/0000755000076500000240000000000013515277236016337 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/tasks/i_advanced_editor_area_pane.py0000644000076500000240000000313713462774551024336 0ustar cwebsterstaff00000000000000# Enthought library imports. from traits.api import Interface # Local imports. 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-6.1.2/pyface/tasks/topological_sort.py0000644000076500000240000000635413462774551022307 0ustar cwebsterstaff00000000000000# Standard library imports. 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-6.1.2/pyface/tasks/task.py0000644000076500000240000000574013462774551017664 0ustar cwebsterstaff00000000000000# Enthought library imports. from pyface.action.api import StatusBarManager from traits.api import Callable, HasTraits, Instance, List, Str, \ Unicode # Local imports. 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 = Unicode # 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-6.1.2/pyface/tasks/traits_editor.py0000644000076500000240000000311013462774551021563 0ustar cwebsterstaff00000000000000# Enthought library imports. from traits.api import HasTraits, Instance # Local imports. 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(TraitsEditor, self).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-6.1.2/pyface/tasks/advanced_editor_area_pane.py0000644000076500000240000000033613462774551024024 0ustar cwebsterstaff00000000000000# Import the toolkit specific version. from pyface.toolkit import toolkit_object AdvancedEditorAreaPane = toolkit_object('tasks.advanced_editor_area_pane:' 'AdvancedEditorAreaPane') pyface-6.1.2/pyface/tasks/task_window_backend.py0000644000076500000240000000025013462774551022711 0ustar cwebsterstaff00000000000000# Import the toolkit specific version. from pyface.toolkit import toolkit_object TaskWindowBackend = toolkit_object( 'tasks.task_window_backend:TaskWindowBackend') pyface-6.1.2/pyface/tasks/task_layout.py0000644000076500000240000001231013462774551021250 0ustar cwebsterstaff00000000000000# Standard library imports. from six.moves import cStringIO as StringIO import sys # Enthought library imports. from traits.api import Either, Enum, HasStrictTraits, Int, Instance, List, Str 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(LayoutContainer, self).__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 = Either(Str, Int, default='', 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(PaneItem, self).__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 = Either(Str, Int, default='') 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(Either( PaneItem, 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 = Either(PaneItem, Tabbed, Splitter) right = Either(PaneItem, Tabbed, Splitter) top = Either(PaneItem, Tabbed, Splitter) bottom = Either(PaneItem, Tabbed, 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-6.1.2/pyface/tasks/editor_area_pane.py0000644000076500000240000000023213462774551022172 0ustar cwebsterstaff00000000000000# Import the toolkit specific version. from pyface.toolkit import toolkit_object EditorAreaPane = toolkit_object('tasks.editor_area_pane:EditorAreaPane') pyface-6.1.2/pyface/tasks/split_editor_area_pane.py0000644000076500000240000000051513462774551023411 0ustar cwebsterstaff00000000000000# Import the toolkit specific version. 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-6.1.2/pyface/tasks/i_editor.py0000644000076500000240000000553713514577463020525 0ustar cwebsterstaff00000000000000# Enthought library imports. from traits.api import Any, Bool, Event, HasTraits, Interface, \ Instance, Property, Unicode, 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 = Unicode #: The tooltip to show for the editor's tab, if any. tooltip = Unicode #: 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 = Unicode tooltip = Unicode control = Any obj = Any dirty = Bool(False) editor_area = Instance( 'pyface.tasks.i_editor_area_pane.IEditorAreaPane') is_active = Property(Bool, depends_on='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-6.1.2/pyface/tasks/enaml_editor.py0000644000076500000240000000030013462774551021347 0ustar cwebsterstaff00000000000000# local imports from pyface.tasks.editor import Editor from pyface.tasks.enaml_pane import EnamlPane class EnamlEditor(EnamlPane, Editor): """ Create an Editor for Enaml Components. """ pyface-6.1.2/pyface/tasks/i_task_pane.py0000644000076500000240000000271613514577463021200 0ustar cwebsterstaff00000000000000# Enthought library imports. from traits.api import Any, Bool, HasTraits, Interface, Instance, \ Str, Unicode # Local imports. 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 = Unicode #: 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 = Unicode control = Any has_focus = Bool(False) task = Instance(Task) pyface-6.1.2/pyface/tasks/task_window.py0000644000076500000240000004006213462774551021247 0ustar cwebsterstaff00000000000000# Standard library imports. import logging # Enthought library imports. from pyface.action.api import MenuBarManager, StatusBarManager, ToolBarManager from pyface.api import ApplicationWindow from traits.api import Bool, Callable, HasTraits, HasStrictTraits, Instance, \ List, Property, Unicode, Vetoable, on_trait_change # Local imports. 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 import six # 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(Unicode, depends_on=['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 = Unicode _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(TaskWindow, self).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.warn( "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.warn( "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, six.string_types): continue state = self._get_state(layout.id) if state: state.layout = layout else: logger.warn( "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 ################################################ def __active_state_changed(self, state): 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 @on_trait_change('central_pane.has_focus, dock_panes.has_focus') def _focus_updated(self, obj, name, old, new): if name == 'has_focus' and new: self.active_pane = obj @on_trait_change('_states[]') def _states_updated(self): 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-6.1.2/pyface/tasks/enaml_dock_pane.py0000644000076500000240000000373413462774551022022 0ustar cwebsterstaff00000000000000# Enthought library imports. from traits.api import Instance # Local imports. 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-6.1.2/pyface/tasks/tests/0000755000076500000240000000000013515277236017501 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/tasks/tests/test_task_window.py0000644000076500000240000001272413462774551023454 0ustar cwebsterstaff00000000000000import unittest from traits.testing.unittest_tools 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-6.1.2/pyface/tasks/tests/test_tasks_application.py0000644000076500000240000000730413462774551024631 0ustar cwebsterstaff00000000000000from __future__ import ( absolute_import, division, print_function, unicode_literals ) import unittest from traits.api import Bool 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(TestingApp, self).start() window = self.windows[0] window.on_trait_change(self._on_window_closing, 'closing') return True def stop(self): super(TestingApp, self).stop() return self.stop_cleanly def _on_window_closing(self, window, trait, old, new): if self.veto_close_window and not self.exit_vetoed: new.veto = True self.exit_vetoed = True def _exiting_fired(self, event): 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(TestingApp, self)._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): self.application_events.append(event) def connect_listeners(self, app): for event in EVENTS: app.on_trait_change(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.on_trait_change(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-6.1.2/pyface/tasks/tests/test_enaml_task_pane.py0000644000076500000240000000402313500710640024213 0ustar cwebsterstaff00000000000000import 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 = "Enaml not installed" if SKIP_REASON is not None: # Dummy class so that the TestEnamlTaskPane class definition below # doesn't fail. class GuiTestAssistant(object): 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-6.1.2/pyface/tasks/tests/__init__.py0000644000076500000240000000000013462774551021603 0ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/tasks/tests/test_task_layout.py0000644000076500000240000000301213462774551023450 0ustar cwebsterstaff00000000000000# Standard library imports. import unittest # Enthought library imports. 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) if __name__ == '__main__': unittest.main() pyface-6.1.2/pyface/tasks/tests/test_enaml_editor.py0000644000076500000240000000417013500710640023537 0ustar cwebsterstaff00000000000000import 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 = "Enaml not installed" if SKIP_REASON is not None: # Dummy class so that the TestEnamlTaskPane class definition below # doesn't fail. class GuiTestAssistant(object): 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-6.1.2/pyface/tasks/tests/test_topological_sort.py0000644000076500000240000000476113462774551024510 0ustar cwebsterstaff00000000000000# Standard library imports. import unittest # Local imports. from traits.api import HasTraits, Int from pyface.tasks.topological_sort import before_after_sort, \ topological_sort class TestItem(HasTraits): id = Int before = Int after = Int def __init__(self, id, **traits): super(TestItem, self).__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.assertEquals(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.assertEquals(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.assertEquals(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.assert_(not has_cycles) self.assertEquals(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.assert_(not has_cycles) self.assertEquals(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.assert_(has_cycles) if __name__ == '__main__': unittest.main() pyface-6.1.2/pyface/tasks/tests/test_dock_pane_toggle_group.py0000644000076500000240000000772413462774551025627 0ustar cwebsterstaff00000000000000# Standard library imports. import unittest # Enthought library imports. from pyface.tasks.action.api import SMenu, SMenuBar, SGroup, \ 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))) if __name__ == '__main__': unittest.main() pyface-6.1.2/pyface/tasks/tests/test_enaml_dock_pane.py0000644000076500000240000000406313500710640024175 0ustar cwebsterstaff00000000000000import 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 = "Enaml not installed" if SKIP_REASON is not None: # Dummy class so that the TestEnamlTaskPane class definition below # doesn't fail. class GuiTestAssistant(object): 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-6.1.2/pyface/tasks/tests/test_editor_area_pane.py0000644000076500000240000000216013462774551024375 0ustar cwebsterstaff00000000000000# Standard library imports. import unittest # Enthought library imports. from traits.etsconfig.api import ETSConfig from pyface.tasks.api import Editor, EditorAreaPane 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.assert_(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) if __name__ == '__main__': unittest.main() pyface-6.1.2/pyface/tasks/tests/test_action_manager_builder.py0000644000076500000240000003341113462774551025574 0ustar cwebsterstaff00000000000000# Standard library imports. import unittest # Enthought library imports. from pyface.action.api import Action, ActionItem, ActionManager, Group, \ MenuManager, MenuBarManager from pyface.tasks.action.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.assertEquals(type(first), type(second)) self.assertEquals(first.id, second.id) if isinstance(first, ActionItem): self.assertEquals(first.action.name, second.action.name) elif isinstance(first, ActionManager): if not isinstance(first, MenuBarManager): self.assertEquals(first.name, second.name) children1, children2 = first.groups, second.groups elif isinstance(first, Group): self.assertEquals(first.separator, second.separator) children1, children2 = first.items, second.items self.assertEquals(len(children1), len(children2)) for i in range(len(children1)): self.assertActionElementsEqual(children1[i], children2[i]) #### 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. """ # 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) if __name__ == '__main__': unittest.main() pyface-6.1.2/pyface/tasks/traits_dock_pane.py0000644000076500000240000000356213462774551022233 0ustar cwebsterstaff00000000000000# Enthought library imports. from traits.api import HasTraits, Instance # Local imports. 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(TraitsDockPane, self).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(TraitsDockPane, self).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-6.1.2/pyface/tasks/__init__.py0000644000076500000240000000000013462774551020441 0ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/tasks/task_pane.py0000644000076500000240000000020713462774551020660 0ustar cwebsterstaff00000000000000# Import the toolkit specific version. from pyface.toolkit import toolkit_object TaskPane = toolkit_object('tasks.task_pane:TaskPane') pyface-6.1.2/pyface/tasks/contrib/0000755000076500000240000000000013515277236017777 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/tasks/contrib/python_shell.py0000644000076500000240000000654113462774551023072 0ustar cwebsterstaff00000000000000""" 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. """ # Std lib imports import logging # Enthought library imports. from traits.api import Str, List, Dict, Instance from pyface.api import PythonShell, FileDialog, OK from pyface.tasks.api import Task, TaskPane from pyface.tasks.action.api import SMenuBar, SMenu, 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-6.1.2/pyface/tasks/contrib/__init__.py0000644000076500000240000000000013462774551022101 0ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/tasks/contrib/README.txt0000644000076500000240000000034713462774551021504 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-6.1.2/pyface/tasks/enaml_task_pane.py0000644000076500000240000000031413462774551022033 0ustar cwebsterstaff00000000000000# Local imports. 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-6.1.2/pyface/tasks/i_task_window_backend.py0000644000076500000240000000502013514577463023222 0ustar cwebsterstaff00000000000000# Enthought library imports. 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-6.1.2/pyface/tasks/api.py0000644000076500000240000000254213462774551017470 0ustar cwebsterstaff00000000000000# Copyright (c) 2010-18, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! from __future__ import absolute_import # Local imports. 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-6.1.2/pyface/tasks/enaml_pane.py0000644000076500000240000000454713462774551021025 0ustar cwebsterstaff00000000000000""" Base class defining common code for EnamlTaskPane and EnamlEditor. """ # Enthought library imports. 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-6.1.2/pyface/tasks/action/0000755000076500000240000000000013515277236017614 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/tasks/action/task_action.py0000644000076500000240000001154713462774551022500 0ustar cwebsterstaff00000000000000# Copyright (c) 2010-18, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # Enthought library imports. from traits.api import Bool, Instance, Property, Str, cached_property # Local imports. 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(depends_on='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(TaskAction, self).destroy() class TaskWindowAction(TaskAction): """ An Action that makes a callback to a Task's window. """ #### ListeningAction interface ############################################ object = Property(depends_on='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(depends_on='central_pane') #### CentralPaneAction interface ########################################## # The central pane with which the action is associated. central_pane = Property(Instance(TaskPane), depends_on='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(depends_on='dock_pane') #### DockPaneAction interface ############################################# # The dock pane with which the action is associated. Set by the framework. dock_pane = Property(Instance(TaskPane), depends_on='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(depends_on='active_editor') #### EditorAction interface ############################################### # The active editor in the central pane with which the action is associated. active_editor = Property( Instance(Editor), depends_on='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-6.1.2/pyface/tasks/action/task_action_manager_builder.py0000644000076500000240000002231213462774551025670 0ustar cwebsterstaff00000000000000# Standard library imports. from collections import defaultdict import logging # Enthought library imports. from pyface.action.api import ActionController, ActionManager from traits.api import HasTraits, Instance # Local imports. from pyface.tasks.task import Task from pyface.tasks.topological_sort import before_after_sort from pyface.tasks.action.schema import Schema, ToolBarSchema from pyface.tasks.action.schema_addition import SchemaAddition # Logging. logger = logging.getLogger(__name__) class TaskActionManagerBuilder(HasTraits): """ Builds menu bars and tool bars from menu bar and tool bar schema, along with any additions provided by the task. """ # The controller to assign to the menubar and toolbars. controller = Instance(ActionController) # The Task to build menubars and toolbars for. task = Instance(Task) ########################################################################### # 'TaskActionManagerBuilder' interface. ########################################################################### def create_action_manager(self, schema): """ Create a manager for the given schema using the task's additions. """ additions_map = defaultdict(list) for addition in self.task.extra_actions: 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 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[:] for addition in self.task.extra_actions: 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 [ self.create_action_manager(schema) for schema in self._get_ordered_schemas(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. """ 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. """ # 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 their Tasks. item.controller = self.controller children.append(item) # Finally, create the pyface.action instance for this schema. return self.prepare_item(schema.create(children), path) #### Trait initializers ################################################### def _controller_default(self): from .task_action_controller import TaskActionController return TaskActionController(task=self.task) pyface-6.1.2/pyface/tasks/action/task_action_controller.py0000644000076500000240000000277113462774551024742 0ustar cwebsterstaff00000000000000# Enthought library imports. from pyface.action.api import ActionController from traits.api import Instance # Local imports. 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-6.1.2/pyface/tasks/action/listening_action.py0000644000076500000240000000013313462774551023517 0ustar cwebsterstaff00000000000000# Backward compatibility import from pyface.action.listening_action import ListeningActionpyface-6.1.2/pyface/tasks/action/schema_addition.py0000644000076500000240000000376513462774551023317 0ustar cwebsterstaff00000000000000# Enthought library imports. from traits.api import Callable, HasTraits, Str, Enum 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-6.1.2/pyface/tasks/action/__init__.py0000644000076500000240000000000013462774551021716 0ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/tasks/action/dock_pane_toggle_group.py0000644000076500000240000000747313462774551024704 0ustar cwebsterstaff00000000000000""" A Group for toggling the visibility of a task's dock panes. """ # Enthought library imports. from pyface.action.api import Action, ActionItem, Group from traits.api import cached_property, Instance, List, on_trait_change, \ Property, Unicode # Local imports. 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(Unicode, depends_on='dock_pane.name') style = 'toggle' tooltip = Property(Unicode, depends_on='name') ########################################################################### # 'Action' interface. ########################################################################### def destroy(self): super(DockPaneToggleAction, self).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 u'Toggles the visibility of the %s pane.' % self.name @on_trait_change('dock_pane.visible') def _update_checked(self): if self.dock_pane: self.checked = self.dock_pane.visible @on_trait_change('dock_pane.closable') def _update_visible(self): 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(depends_on='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(depends_on='task.window._states.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 #################################################### @on_trait_change('dock_panes[]') def _dock_panes_updated(self): """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-6.1.2/pyface/tasks/action/api.py0000644000076500000240000000215613462774551020746 0ustar cwebsterstaff00000000000000# Copyright (c) 2010-18, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! from __future__ import absolute_import # Local imports. 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-6.1.2/pyface/tasks/action/tasks_application_action.py0000644000076500000240000000212313462774551025234 0ustar cwebsterstaff00000000000000from 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 = u'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-6.1.2/pyface/tasks/action/task_toggle_group.py0000644000076500000240000001004013462774551023703 0ustar cwebsterstaff00000000000000# Copyright (c) 2010-18, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # Enthought library imports. from pyface.action.api import Action, ActionItem, Group from traits.api import Any, List, Instance, Property, Unicode, on_trait_change # Local imports. 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(Unicode, depends_on='task.name') #: The action is a toggle menu item. style = 'toggle' #: The tooltip to display for the menu item. tooltip = Property(Unicode, depends_on='name') #### 'TaskActivateAction' interface ####################################### task = Instance(Task) ########################################################################### # 'Action' interface. ########################################################################### def destroy(self): super(TaskToggleAction, self).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 u'Switch to the %s task.' % self.name @on_trait_change('task.window.active_task') def _update_checked(self): 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 #### 'TaskChangeMenuManager' 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): # 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.on_trait_change(self._rebuild, 'tasks[]') 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.windowpyface-6.1.2/pyface/tasks/action/schema.py0000644000076500000240000001146113514577463021435 0ustar cwebsterstaff00000000000000# Enthought library imports. from pyface.action.api import Action, ActionItem, Group, \ MenuManager, MenuBarManager, ToolBarManager from pyface.util.id_helper import get_unique_id from traits.api import Bool, Callable, Enum, HasTraits, Instance, \ List, Property, Str, Trait, Tuple, Unicode # Trait definitions. SubSchema = Trait(None, Action, ActionItem, Group, MenuManager, Instance('pyface.tasks.action.schema.GroupSchema'), Instance('pyface.tasks.action.schema.MenuSchema'), Instance('pyface.tasks.action.schema.Schema')) class Schema(HasTraits): """ The abstract base class for all Tasks action schemas. """ # The schema's identifier (unique within its parent schema). id = Str def _id_default(self): return get_unique_id(self) # 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(Schema, self).__init__(**traits) self.items.extend(items) def create(self, children): """ Create the appropriate pyface.action instance with the specified child items. """ raise NotImplementedError 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(Action) #: 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) class GroupSchema(Schema): """ A schema for a Pyface Group. """ # A factory for instantiating a pyface Group. group_factory = Callable(Group) # 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) class MenuSchema(Schema): """ A schema for a Pyface MenuManager. """ # The menu's user visible name. name = Unicode # 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(Action) # A factory for instantiating a pyface MenuManager. menu_manager_factory = Callable(MenuManager) 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) 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(MenuBarManager) def create(self, children): traits = dict(id=self.id) return self.menu_bar_manager_factory(*children, **traits) 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 = Unicode('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(ToolBarManager) 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) # Convenience abbreviations. SGroup = GroupSchema SMenu = MenuSchema SMenuBar = MenuBarSchema SToolBar = ToolBarSchema pyface-6.1.2/pyface/tasks/action/task_window_toggle_group.py0000644000076500000240000001002213462774551025272 0ustar cwebsterstaff00000000000000# Copyright (c) 2010-18, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # Enthought library imports. from pyface.action.api import Action, ActionItem, Group from traits.api import Any, Instance, List, Property, Unicode, on_trait_change class TaskWindowToggleAction(Action): """ An action for activating an application window. """ # 'Action' interface ----------------------------------------------------- #: The name of the action for the window. name = Property(Unicode, depends_on='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 u'' @on_trait_change('window:activated') def _window_activated(self): self.checked = True @on_trait_change('window:deactivated') def _window_deactivated(self): 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(TaskWindowToggleGroup, self).destroy() if self.application: self.application.on_trait_change( self._rebuild, 'windows[]', 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): # 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.on_trait_change(self._rebuild, 'windows[]') return self._get_items() def _manager_default(self): manager = self while isinstance(manager, Group): manager = manager.parent return manager pyface-6.1.2/pyface/tasks/tasks_application.py0000644000076500000240000002125313462774551022427 0ustar cwebsterstaff00000000000000# Copyright (c) 2014-2016 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! """ Define a base Task application class to create the event loop, and launch the creation of tasks and corresponding windows. """ from __future__ import ( absolute_import, division, print_function, unicode_literals ) from functools import partial import logging from traits.api import ( Callable, HasStrictTraits, List, Instance, Property, Str, Unicode, cached_property, on_trait_change ) 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 = Unicode #: 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(depends_on='active_window.active_task') #: 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.tasks.action.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(TasksApplication, self).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 --------------------------------------------------- @on_trait_change('windows:closed') def _on_window_closed(self, window, trait, old, new): """ Listener that ensures window handles are released when closed. """ if getattr(window, 'active_task', None) in self.tasks: self.tasks.remove(window.active_task) super(TasksApplication, self)._on_window_closed(window, trait, old, new) # 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.tasks.action.api import ( CreateTaskWindowAction, SchemaAddition, SMenu, 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-6.1.2/pyface/tasks/i_dock_pane.py0000644000076500000240000000502313462774551021147 0ustar cwebsterstaff00000000000000# Enthought library imports. from traits.api import Bool, Enum, HasTraits, Str, Tuple # Local imports. 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-6.1.2/pyface/tasks/task_window_layout.py0000644000076500000240000000312513462774551022643 0ustar cwebsterstaff00000000000000# Enthought library imports. from traits.api import Either, List, Str, Tuple, Enum # Local imports. from pyface.tasks.task_layout import LayoutContainer, TaskLayout import six 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(Either(Str, TaskLayout), pretty_skip=True) # The position of the window. position = Tuple(-1, -1) # The size of the window. size = Tuple(800, 600) 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, six.string_types) else first.id return None def get_tasks(self): """ Returns the IDs of the tasks in the layout. """ return [ (item if isinstance(item, six.string_types) 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-6.1.2/pyface/tasks/dock_pane.py0000644000076500000240000000020713462774551020636 0ustar cwebsterstaff00000000000000# Import the toolkit specific version. from pyface.toolkit import toolkit_object DockPane = toolkit_object('tasks.dock_pane:DockPane') pyface-6.1.2/pyface/tasks/traits_task_pane.py0000644000076500000240000000305313462774551022250 0ustar cwebsterstaff00000000000000# Enthought library imports. from traits.api import HasTraits, Instance # Local imports. 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(TraitsTaskPane, self).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-6.1.2/pyface/tasks/editor.py0000644000076500000240000000020013462774551020172 0ustar cwebsterstaff00000000000000# Import the toolkit specific version. from pyface.toolkit import toolkit_object Editor = toolkit_object('tasks.editor:Editor') pyface-6.1.2/pyface/tasks/i_editor_area_pane.py0000644000076500000240000001452613462774551022515 0ustar cwebsterstaff00000000000000# Standard library imports. import logging # Enthought library imports. from traits.api import Bool, Callable, Dict, Event, File, HasTraits, Instance, \ List, Str # Local imports. 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.warn('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-6.1.2/pyface/confirmation_dialog.py0000644000076500000240000000342513462774551021602 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The implementation of a dialog that prompts the user for confirmation. """ from __future__ import absolute_import # Local imports. from .constant import NO 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() # Import the toolkit specific version. from .toolkit import toolkit_object ConfirmationDialog = toolkit_object('confirmation_dialog:ConfirmationDialog') #### EOF ###################################################################### pyface-6.1.2/pyface/single_choice_dialog.py0000644000076500000240000000346313462774551021707 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A dialog that allows the user to chose a single item from a list. """ from __future__ import absolute_import 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-6.1.2/pyface/widget.py0000644000076500000240000000164613462774552017062 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The base implementation of all pyface widgets. """ # Import the toolkit specific version. from __future__ import absolute_import from .toolkit import toolkit_object Widget = toolkit_object('widget:Widget') #### EOF ###################################################################### pyface-6.1.2/pyface/toolkit.py0000644000076500000240000000165013462774551017256 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2007, Riverbank Computing Limited # All rights reserved. # # Copyright (c) 2015-2017, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # #------------------------------------------------------------------------------ """ 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-6.1.2/pyface/directory_dialog.py0000644000076500000240000000174213462774551021116 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The implementation of a dialog that allows the user to browse for a directory. """ # Import the toolkit specific version. from __future__ import absolute_import from .toolkit import toolkit_object DirectoryDialog = toolkit_object('directory_dialog:DirectoryDialog') #### EOF ###################################################################### pyface-6.1.2/pyface/message_dialog.py0000644000076500000240000000637713462774551020547 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The implementation of a dialog that displays a message. """ from __future__ import absolute_import # Convenience functions. def information(parent, message, title='Information', detail='', informative=''): """ 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. """ dialog = MessageDialog( parent=parent, message=message, title=title, severity='information', detail=detail, informative=informative ) dialog.open() def warning(parent, message, title='Warning', detail='', informative=''): """ 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. """ dialog = MessageDialog( parent=parent, message=message, title=title, severity='warning', detail=detail, informative=informative ) dialog.open() def error(parent, message, title='Error', detail='', informative=''): """ 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. """ dialog = MessageDialog( parent=parent, message=message, title=title, severity='error', detail=detail, informative=informative ) dialog.open() # Import the toolkit specific version. from .toolkit import toolkit_object MessageDialog = toolkit_object('message_dialog:MessageDialog') #### EOF ###################################################################### pyface-6.1.2/pyface/window.py0000644000076500000240000000166413462774552017106 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The abstract implementation of all pyface top-level windows. """ # Import the toolkit specific version. from __future__ import absolute_import from .toolkit import toolkit_object Window = toolkit_object('window:Window') #### EOF ###################################################################### pyface-6.1.2/pyface/image_resource.py0000644000076500000240000000166613462774551020571 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The implementation of an image resource. """ # Import the toolkit specific version. from __future__ import absolute_import from .toolkit import toolkit_object ImageResource = toolkit_object('image_resource:ImageResource') #### EOF ###################################################################### pyface-6.1.2/pyface/multi_toolbar_window.py0000644000076500000240000000121413462774551022030 0ustar cwebsterstaff00000000000000# Copyright (c) 2017, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # Note: The MultiToolbarWindow is currently wx-specific # Import the toolkit specific version. from __future__ import absolute_import from .toolkit import toolkit_object MultiToolbarWindow = toolkit_object('multi_toolbar_window:MultiToolbarWindow') pyface-6.1.2/pyface/list_box.py0000644000076500000240000000113713462774551017414 0ustar cwebsterstaff00000000000000# Copyright (c) 2017, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # Note: The ListBox is currently wx-specific # Import the toolkit specific version. from __future__ import absolute_import from .toolkit import toolkit_object ListBox = toolkit_object('list_box:ListBox') pyface-6.1.2/pyface/util/0000755000076500000240000000000013515277240016162 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/util/id_helper.py0000644000076500000240000000351513462774552020504 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005-2013, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ 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-6.1.2/pyface/util/guisupport.py0000644000076500000240000001376713462774552021004 0ustar cwebsterstaff00000000000000""" 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. from __future__ import absolute_import #----------------------------------------------------------------------------- # 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.PySimpleApp(*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-6.1.2/pyface/util/tests/0000755000076500000240000000000013515277240017324 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/util/tests/__init__.py0000644000076500000240000000000013462774552021434 0ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/util/tests/test_id_helper.py0000644000076500000240000000260413462774552022703 0ustar cwebsterstaff00000000000000# # Enthought product code # # (C) Copyright 2012 Enthought, Inc., Austin, TX # All right reserved. # # This file is confidential and NOT open source. Do not distribute. # """ 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') if __name__ == '__main__': unittest.main() #### EOF ###################################################################### pyface-6.1.2/pyface/util/__init__.py0000644000076500000240000000120013462774552020275 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ pyface-6.1.2/pyface/util/fix_introspect_bug.py0000644000076500000240000001353413502141171022424 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Prabhu Ramachandran # Description: #------------------------------------------------------------------------------ """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 import types import six # 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, six.string_types)] 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(object): """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(object).__name__ except: key = 'anonymous' # Wake up sleepy objects - a hack for ZODB objects in "ghost" state. wakeupcall = dir(object) del wakeupcall # Get attributes available through the normal convention. attributes = dir(object) attrdict[(key, 'dir', len(attributes))] = attributes # Get attributes from the object's dictionary, if it has one. try: attributes = list(object.__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 = object.__class__ except: # Must catch all because object might have __getattr__. pass else: if klass is object: # 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 = object.__bases__ except: # Must catch all because object might have __getattr__. pass else: if isinstance(bases, types.TupleType): for base in bases: if type(base) is types.TypeType: # Break a circular reference. Happens in Python 2.2. pass else: attrdict.update(getAllAttributeNames(base)) return attrdict pyface-6.1.2/pyface/util/python_stc.py0000644000076500000240000000115313462774552020737 0ustar cwebsterstaff00000000000000# Copyright (c) 2017, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! import logging logger = logging.getLogger(__name__) logger.warning( 'DEPRECATED: pyface.util.python_stc, use pyface.wx.python_stc instead. ' 'Will be removed in Pyface 7.') from pyface.wx.python_stc import * pyface-6.1.2/pyface/util/font_helper.py0000644000076500000240000000117113462774552021052 0ustar cwebsterstaff00000000000000# Copyright (c) 2017, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! import logging logger = logging.getLogger(__name__) logger.warning( 'DEPRECATED: pyface.util.font_helper, use pyface.wx.utils.font_helper instead. ' 'Will be removed in Pyface 7.') from pyface.wx.util.font_helper import * pyface-6.1.2/pyface/util/testing.py0000644000076500000240000000070513462774552020224 0ustar cwebsterstaff00000000000000from functools import wraps def has_traitsui(): """ Is traitsui installed? """ try: import traitsui except ImportError: return False 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 pyface-6.1.2/pyface/util/grid/0000755000076500000240000000000013515277240017107 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/util/grid/grid.py0000644000076500000240000000115013462774552020414 0ustar cwebsterstaff00000000000000# Copyright (c) 2017, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! import logging logger = logging.getLogger(__name__) logger.warning( 'DEPRECATED: pyface.util.grid.grid, use pyface.wx.grid.grid instead. ' 'Will be removed in Pyface 7.') from pyface.wx.grid.grid import * pyface-6.1.2/pyface/util/grid/__init__.py0000644000076500000240000000113113462774552021225 0ustar cwebsterstaff00000000000000# Copyright (c) 2017, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! import logging logger = logging.getLogger(__name__) logger.warning( 'DEPRECATED: pyface.util.grid, use pyface.wx.grid instead. ' 'Will be removed in Pyface 7.') from pyface.wx.grid import * pyface-6.1.2/pyface/util/grid/api.py0000644000076500000240000000114513462774552020244 0ustar cwebsterstaff00000000000000# Copyright (c) 2017, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! import logging logger = logging.getLogger(__name__) logger.warning( 'DEPRECATED: pyface.util.grid.api, use pyface.wx.grid.api instead. ' 'Will be removed in Pyface 7.') from pyface.wx.grid.api import * pyface-6.1.2/pyface/util/grid/grid_model.py0000644000076500000240000000117213462774552021600 0ustar cwebsterstaff00000000000000# Copyright (c) 2017, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! import logging logger = logging.getLogger(__name__) logger.warning( 'DEPRECATED: pyface.util.grid.grid_model, use pyface.wx.grid.grid_model instead. ' 'Will be removed in Pyface 7.') from pyface.wx.grid.grid_model import * pyface-6.1.2/pyface/util/grid/grid_column.py0000644000076500000240000000117513462774552022000 0ustar cwebsterstaff00000000000000# Copyright (c) 2017, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! import logging logger = logging.getLogger(__name__) logger.warning( 'DEPRECATED: pyface.util.grid.grid_column, use pyface.wx.grid.grid_column instead. ' 'Will be removed in Pyface 7.') from pyface.wx.grid.grid_column import * pyface-6.1.2/pyface/util/grid/grid_row.py0000644000076500000240000000116413462774552021310 0ustar cwebsterstaff00000000000000# Copyright (c) 2017, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! import logging logger = logging.getLogger(__name__) logger.warning( 'DEPRECATED: pyface.util.grid.grid_row, use pyface.wx.grid.grid_row instead. ' 'Will be removed in Pyface 7.') from pyface.wx.grid.grid_row import * pyface-6.1.2/pyface/system_metrics.py0000644000076500000240000000173313462774551020645 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The implementation of access to system metrics (screen width and height etc). """ # Import the toolkit specific version. from __future__ import absolute_import from .toolkit import toolkit_object SystemMetrics = toolkit_object('system_metrics:SystemMetrics') #### EOF ###################################################################### pyface-6.1.2/pyface/i_image_resource.py0000644000076500000240000001641213462774551021074 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005-2009 by Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! #------------------------------------------------------------------------------ """ The interface for an image resource. """ try: from collections.abc import Sequence except ImportError: # Python 3.8 deprecation from collections import Sequence from pyface.resource_manager import resource_manager from pyface.resource.resource_path import resource_module, resource_path from traits.api import Interface, List, Unicode import six class IImageResource(Interface): """ 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 = Unicode #: The name of the image. name = Unicode #: 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 ########################################################################### # 'object' interface. ########################################################################### def __init__(self, name, search_path=None): """ Creates a new image resource. """ ########################################################################### # '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. """ # 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 create_bitmap(self, size=None): """ Creates a toolkit specific bitmap 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 as a bitmap. """ def create_icon(self, size=None): """ Creates a toolkit-specific icon 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 as an icon. """ @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(object): """ 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, six.string_types): _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-6.1.2/pyface/_version.py0000644000076500000240000000026313515277230017403 0ustar cwebsterstaff00000000000000# THIS FILE IS GENERATED FROM PYFACE SETUP.PY version = '6.1.2' full_version = '6.1.2' git_revision = 'Unknown' is_released = True if not is_released: version = full_version pyface-6.1.2/pyface/i_message_dialog.py0000644000076500000240000000273413462774551021050 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The interface for a dialog that displays a message. """ # Enthought library imports. from traits.api import Enum, Unicode # Local imports. 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 = Unicode #: More information about the message to be displayed. informative = Unicode #: More detail about the message to be displayed in the dialog. detail = Unicode #: The severity of the message. severity = Enum('information', 'warning', 'error') class MMessageDialog(object): """ The mixin class that contains common code for toolkit specific implementations of the IMessageDialog interface. """ pyface-6.1.2/pyface/dialog.py0000644000076500000240000000165213462774551017032 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The abstract implementation of all pyface dialogs. """ # Import the toolkit specific version. from __future__ import absolute_import from .toolkit import toolkit_object Dialog = toolkit_object('dialog:Dialog') #### EOF ###################################################################### pyface-6.1.2/pyface/i_split_widget.py0000644000076500000240000000572013462774551020601 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Mix-in class for split widgets. """ # Enthought library imports. from traits.api import Callable, Enum, Float, Interface 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 = Enum('vertical', 'vertical', 'horizontal') #: 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(object): """ The mixin class that contains common code for toolkit specific implementations of the ISplitWidget interface. """ pyface-6.1.2/pyface/i_system_metrics.py0000644000076500000240000000267213462774551021160 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The interface to system metrics (screen width and height etc). """ # Enthought library imports. from traits.api import 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(object): """ The mixin class that contains common code for toolkit specific implementations of the ISystemMetrics interface. """ pyface-6.1.2/pyface/images/0000755000076500000240000000000013515277235016456 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/images/panel_gradient.png0000644000076500000240000000314413462774551022146 0ustar cwebsterstaff00000000000000PNG  IHDRbgAMA7tEXtSoftwareAdobe ImageReadyqe<IDATxl 'sw[ *Zc@R&Fd咁'gNϘ)ls&A8rA$h)c@Ɔ[|8БtjMhZi2h|$5?ƷuX[ndIh],YSI9L>p+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-6.1.2/pyface/images/question.png0000644000076500000240000000347113462774551021044 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-6.1.2/pyface/images/carat_closed.png0000644000076500000240000000034313462774551021613 0ustar cwebsterstaff00000000000000PNG  IHDR gAMA7tEXtSoftwareAdobe ImageReadyqe<uIDATxb? ``-X\ss8yBBQiy( "vs+ \\=QL 7V` P| @ p}0EȾIENDB`pyface-6.1.2/pyface/images/background.jpg0000644000076500000240000007052413462774551021313 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-6.1.2/pyface/images/image_LICENSE.txt0000644000076500000240000000216513462774551021453 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 GV (Gael Varoquaux) Public Domain N/A Nuvola LGPL image_LICENSE_Nuvola.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: ---------------------------------------------------------------------------- enthought/pyface/images: about.jpg | Enthought background.jpg | Enthought caret_close.png | Enthought caret_open.png | Enthought close.png | GV image_not_found.png | Nuvola panel_gradient.png | Enthought panel_gradient_over.png | Enthought question.png | GV splash.png | Enthought pyface-6.1.2/pyface/images/splash.jpg0000644000076500000240000007022413462774551020463 0ustar cwebsterstaff00000000000000JFIFC    #%$""!&+7/&)4)!"0A149;>>>%.DIC;C  ;("(;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;("=!1A"Qaq2#B3RbCr$4s$!1AQ"a2qB ?Ŧ8MOR:UjxZڼwrH1Z*@d J1 sV+w'XLwPKj$Y ySZi>.zHV~ y9m GQ_$AF;c88+'~M`F:jL#"@#TϽVF`ԗqXd1gBī?Q֌ЬRPrDd7 sz[eu%,lA_xi⹉%YG'Ϛ)$#C]pHGfu),7EmNu>RpBQ@s&ܮDGC[nxm Gce.M@=L4)؟^Ѧh˂=9𕞋@J\[%!ZM<lvM5I8tuӥnۏd $vY>ęk6"W@{_hϤYseUMv4-h%T#!Rn9Hp)bj/2ˁQEiH9553ikq\f9$W8ZK7`.4y/}Ǘ(= Z3XsQha1F701jq.k 򘬋Cuq!{`C3ځ+&mb M=j;gzGdZx2 %{-nr߉,jZrԚFlnqT rګJR1Eᰒܱ ֑ZJmSRUmj2pw8K${ugz ',1FɦVɈd2c:Q2E$06}Y)AJ玼œYʧ #8sߥ-3WE3iW>Z#ʀI3FzT=+c# 3 ?ƾuzjڔ'XO8*!~ة<}k*Ec qm#2Dlּ qdQ6;#炭8棴 L!Y_lcYx9+ cr/CZ'6:12H;dގ~Iӥ^y.v~u.0G^ע,l2yǰX/" D*X1w8.Ǡ$ӰEψc!LFSGR8B*%l@TBƇ}6ESd!'cjxK RnLD8X$B%Uc3 ls晴/Se%G=;ĜuYe4+/Kg ޡu,=cQжp>$ ,7lnѰ5X.+2GҫʃqȪe1.rݫ>KK =( '2z#?TWȌ9n+T MHt"|g'ְk(vJ3)m=h#WٚGaU_WJ&K}5fMCyH{fp{Ajsu]E$`a*uRI߸I9_t'5t4jrJjW[.k9"D>|nI:VAdF+uu?Jms\ŗ<5Dቅ7ku#OszIC\h:x3jz͝7P_Qcovm-\7n<˙o5䥼a@J`ӥ$1,ЌZt`y3Ki')4jV=#sɵ=5՝Q2S`}\_IVUX?rLqNwI[ܩvqqc3qg@? @5OZ iY Ao6g|ێ;BE”'QJ7M*<*oC4 :9S+fOߌGZy߄Pp7O.ּw|"hƴ0^ZG 3&+z})F#0:H b7Җi!rcHwS%a@2/|dԿQg lYQVXZ]^YEiͮ"BѰP O)E}UFoMT`V?UZzv7Ḩ[ȿy$<փLE(c|tKYo)e`=RTWi7 ђTҽ#DP!{T1?1(85/-3bpqȭ߇f70FfAF+Z'\sl8!#YKܱ ?j5)`fKdc y=鉷ʂ3;$斃b+=GZqFO9=9QP_jWnKfq6%P{*LhRubd\ Xn!ODgMj<19aѝ6{Zʼg c;Mz76ְ .ZFa*g抍zM<>PcmIg.c+Ck٠Eô0+=F)UIaӜR)GV4ELo?8?SMb wLct,( !`#>#PmBU@WːqiR\tak+]][ks?T>>)hNN1'jS,3B$]ht+m ;- o@c4hQyn34KQiw+z8j*COHI76r mqJѨBİ!q֬5;tŎA("1d2dZMi 9Y ab[-%X'Pk4++]akRr7,ۉ?)u17 j[7T-k@ј\tKțZf3!T9` jӥK0L 1h]OoRvF&Z- N1g#h<߾{W<2VeĬ3"8ݒ(Xb(!|{sylfx?WF AT$wsDIG"u<*;Wӵ(cw2O:$(+7K&s@]'SkqqW\hwY2jٙ7]s~բկ]`) i]/$ sGּP#_*F_, 'fY7P}2D?#}q)|CsڪƇ*D0?L)5P@ Ur4*Y7Y@ +[Jx =JH9Z9k)*6p]ބɔ 4sF5!Hta}: HLáN[Rܵgw(.)YgxlK+x"HI%T\z2_xl2B-.|9F|!f-zVVtk5+E&xj ̓ сLv4kSQ֪KQ i%r2o Wwqޙph(҆=+)F= uم mh犻UԎZEIe `6n$ֺTnkb#̍GcSHn;歳9 gx*4LHm=# px=*9lϑ~ Ѓ⾍(ne2qې|I  9$@X= `{A@OsҲ}0;zGJ}08cYfrN#%<#5X]HpUIڱZ˨jClO$Wy`z>Z FA1nKT[cl삘6=4Jrqjcl*\j[Ghy;j•=R\HU@PYwr UM s,*݀E/52 UAd%p=I=(%@\CB0 *qy,!,}DҺll< brsT\$@z:3tsOJœ%Rx ]!MVe;ՃB]RO̤)bP>R~$V m.qZ˅fb*3(3׌"MA9!v\8QgDYI`vgc{d{gwirгHXĀ?{PeԮV1! xSY m9%m,%Zv<~jYj0@0&y{F;,[^EGN+Xڵ++ HxPW|wyu؏~PijnޫЂMQGm-[r+ ';Z KetXTiKb#pIQ;r:לco"rKHRyNJ I SI- )ǡ=0$Ki⻶e t"ȕḂr9J @}Cr@WvmI}RcW{rq44dB24go?ڂ~K5-R[(#?ǏXx@jZbkIlG$vϽ& #][jQғZ% ZdSD4rƭGUy4K,NdqX>As-02;0ZE&и 7S/"'.ș Pe=)] <)wtamzx#Hw4Xk6SOpJM5e]9)q%DbzkcFr1ip`r?Z< CЂ3?Jcf}`-~~(33\}6Ϟ9@Id`=*_&@=;SY/7ZM`LtjA N]YϰslnA+Aqz 6I"G¢3 z =i 6OQU  2Tƥy&}Hr9A\+ȡ@$'8"{F' u'ہ&$=|YRpHZ!cyK5 Ӡ{)NqGHUν/YXc^jA0Nyo{fa,glb"laBek J5Z [vGnI* XLLa'<8vn[-[os)AD-G~k\96'MuaRn$/?4[Y+& sK ѰYFGei^p4dKY-P4)`6 m4%bNI^MDDaLZZ|$K{ina#y恸ůo\OaZ$1a VW&KfN]oQv˙7[Fvaq$R^fK ʂ AL=g/!ѧ}ͼvp]h˛?P=#[dt|OIIdr0G_ݿZ3hH%4_m*MG] ӻY !>j?sȫ<БJ (=)&ZTAT=ARsӭQ!pr3*4Bїg*=ѓާ6X2\t}No v8# &x>Ҹ dIsx@]6-F8p{۰?J8:1{,I,NsTu1'jlڃF[+vROjk_-c'Vٿ7ڮ07r; _4 /mld): .*=՜sP˹U03aY%$'''ll,6;+×vojT`lN\d]]B*b8#?N s"lu'X6XU6Jd).؟a*Zg]lHhad+cW=s%|̫֒@:BUrLTIbe1w5}}+YFcj(I4]gV͵m,8gVH72[Kٰl㡢+uvNzɡoM40@88)RqکTC!VXփȗ"%mQMv6<s܎zXQ9SZYcADVn,#Tszڱ'Z/ sHݏamn4p biHqK`ƯR f*8=a:tѫze~*pUFXUHDFI=*+}з@B:B01`o01ym+v1a=Z"pv3)qlW$ A4:` y'#SKWS^~PmmMm>ReB$7^b~zz>fB QҳM<`a8=V{jm';ժT=$'<+CWČ>T6Q9@=c$_f 5fFQCќ[Yn|ߜR BO_I^NqQފяAU,ޢ ZYm*}kag_*81FDl$( .NFM/ 4ꊪN(K.IS+m VLua"OvmPryM8@?Kկ5+iR6qz;I fեӬ#W%真`84mb[qL/oFQMEEG0sfE[`u=Buh텱|2-oqzOfEDRrZ_{spܸ!7Eta9|E)@OJ6*99c5-Ih[FdˍޮwcYȭ&]D {gS70h`ux.yU1 3'edkȕzaRnT`~Ny旴XؿԎ24sGXQ!aG>)tTѴIWv槧4w9 ~@:/ۊYC`$Q13FnRm $  G;ISiJsw1=if@ac*9q4vZC@;բ.AĠ~e\=Y;ۅTbKGZIkW6>%e|sqj+%Pv-}=٤W60!9}.Jv-[SQ udFФz)բ2^hU}={A$֓,*LֲUKh4 Ksr6@㟵7v?4teK v$ 飶JvŤrPTZ6 ̥ҲCSD??JZ#=]L#AbFw*OaA}O]q69jمbIT6e:_[iwmC~W-.Z6$w_՗F92eQb/wNΈz:[=;Nn›%򷡈ݞVSODW{Xm`:{R}~mWMH O|o)f̒3*$\jM*v@+ ا⭹Lok @,QZ ombNA]G֓l$rNFR@Ü:3\-zzRV y"TH*xbZiL.!X@*mZ}6x{Ty`%&\ k=Ҩ>[lW52"`N p{A*񥦻$6 ۨIJ=g-e݋xe;]_=5d9\^RnDWC~J,/K;.)14gaY* @0 4G+"NN-ڋS3I[⬧TZt"P28?|x- iB?eRKyR?sRHucIeS<`, 8-f ,NKU1e4̎p =+P֍u;k]L#҉xv 2##`=!?hՃ;r; -Ȣy;s3T]X4g xgZ&G^jb4c!-MMA'XW'~Pe"H)nS2Y%!W'hhT8HSl,l'-3 RZ:I#>Qa,eK|VJgR""rlCwZs3vn)ͅp@D=9 WJ6B{;]NJdȥwZe핱̣רSdęsb.@~5zfE5I#^=Unn2љ Fisnvrwgr9R^DVjcrRrFz5/m";ROsQ`83M h0\eIf?+<ɥXΙ'Dd#{*0_k$^qlDmϽFMd2I{}h-0 fwV$`mV^Ĭ 7oL&( S$ӕ,;5v?:ҲW$dqNdeC v%&MwycCzԥ&MՔ-d:nUTJ[MHpOC% )+9A #bYO?)$ڹQ ]waI]ivע9n'5GtAtInnJbx۽갂FrQ8r16zQSNiӤ7LFH#ھcNڡǔ۷#GnMc-O].i^'c 0={{Jꣿҩ$g䐿Ὲ,Ҷ1U6)߷g98Vx&V(hY\NH#OZ7d }3e gPRec'ڪ.I#S+ݍʐ1!j[py>63֪%۞Bd;IfsE&F$.r%Hsf:7 /[d%'c1yX1wU0A`J۔&o4Xc2&=ȭޜC3ck@;E*vj<gOVjnI k|#{UQ<7v65F8a X3Ҧ뀲땗S[gUfbI9lg)]52N`@ۣsJ0 zcBY 8?]ցs-–i  q{R9)\. 69js&6Ka7{,2V"¶]t +5b7/ >)ƻRM$$HJJиC9R)gvf|`~^ P3z7. q^pq A{WxI-cOK-:chlq Q݄W&x J(-QFڤ]YŪ4ߪ9 h1wmo+BI# ݕV2$ji4\,l/ |R- .[5r=Mh]Y@fY܉'POpi.3ިD$[vVo[[+YA-pqe73H#pWۼҿ˯k-fͼdUZ13^NJcl[^OxvDӉ,o+'YUb:es-d5}MWdI-{C zWQqn:0uUR;vT8~ᚱ/ojҧ4Eo,dg:WVI>Trp9:W֭#E*sЊj0 [,(5gm0zXS1zRł{~ERlt :s]\HQ.IpxqAϵZiW=Gp۷%~GJ0FppGڅEI#q:UeT''k5iV1[4{-.: L Iezmnm 绍cN R|Ƨ,O4ʦиR } TRH៊1Ïcֆ, rE\']$& ۖ]JxVv%>;[hOJY^MsR4ʼn>ֵӕQ@<89X @Kw"DoRz+o]nV9[Z{F}"?3'MquyBnTcGCiSJ4Q(Lo4F^UjxC=~Z~ݤ[N帏E2RaTA@ֆ($6SH|; y`qǖZȢv.'U#ypa#ƂI ?h=Pt]qGVqjd3IM {B#'?qdprjfh)Y0qj]vU4LR8E#KCJq@s׃ԺdA+:f%hQbGtA1BHEN QS?)ܫ%w U#~԰uu-y,Hy5zEő"up  ua`AjZu‰$GfO}x<7iw7əWQ:gިZ0g4et |:WWw3Ϟ&K2IvG jQF/o_Jy`I,}z%ReaҖZm%QHw08j7Mm4PmbBwa[_ Y鷯qg Qi#l1SW)Mlp(aI2ER>)mJ㴵&$ɘwlzIq"HPс VCOA7R}ڦl L=0^·y8 Z 62z|9.# MXlmnXH$M>e|CucW4K!HAۦOȯF庉D p_Ȩ Z ;RFUaPm*,LJcnL+`*:ſYF铟'?Vas7RU2_ަC37mɑ&ƱGvNܪ_M|_^0?f@MQnRD}ʡ8X-#KcAE$G1MO—-DIg}jN7ڃϧ=Zq Fz <⺍# c_Bry?'|QF~w(m01lLWБvRbI2En7] O`ы)*2xR)'8aA=TL2u|)wm Vz~y$ V\#ب:dd'( J)hmV?z!Igg~6#U< h5 6I㊽GK,?v2#F'q"<@$gҰ8˰bGk%5L)9 j$hQE Mh.Kk(#)|Q4(nJA^4oeLZ?7ʦYYhIOeYlXYĞ )7-`~D1ȍ|fhjk-)A])K +r:_*gkb˟j-F{-lo!љ RK%v|LWyfW6sIgw08Ps?U&n-84[_4hnj噋vg^H=f*'TVƳโ9H泯qyvXs^<mk-z$?i:{^S07ۥR9߸C-ty%A9S!{EɝQ4B_ֺkb#:UR"vM-m!&E$(O,f""<$[Z?GYd+zQ3ѷ6#ɷ vއY.5 cQmn{_t : v~.J xFfס,D7c5؃FWxf R8tƪ+n O/10FIJlLB5w#xATb{HABa@Ǟ(OKK4gX^n+ XB.'(,s֭UTc9"4nU]qU"s#2OOAz;*NzUYl-l^3)$QH̡excҖίy;cv+ ǵ4>Yߓҧ%+gVt%gpL}i)P9SG<∔~r Tr=%܃#gbcWh%>dHIi%.I?qI$ۢ0M#[HpJ&J @SWN-ּC;N8^?4QkV'BhYVf9wrsڽš ԣ_&Qfv RY0Gv`*0fFnKiGLV5 [WK2M, p$耜gڈ񎡧Wi2Oo}~&XI=I[E}6c446𕍺0iN6 ԍha66%HL*P^ ҆D Fv2Fx GMsITӥFB\_z[ Sy;eQ6,7<Kpc#/C,p894z9d_[Csy0sUݼ6D[) fF{si[(#9#ǯUSrn:A(.# r:M! 6I$9~A<'qt! y5HI$ZE=nQ#{ԄswyB 9<5}[Lr:d* i1\he76qI$D.cXmߚR-$?'+7s8cqjvO2)&p ;^1M K!XƐK2Z(<ќGnv)d+]\7YI}MkTbt:^Ku$g#=3Ix_2I rtUN(^*SSyefi9.O'jS0i,H895Eg9$r~}#F]ok,Qf|:ڼ~r3қB倹ܪ9j$ sWM&Oz& .<e8.=psR1MEbkKi ) 2u11"FOnEj4udjSC[K c'|Z`GRkkvH}<&X O,S4>ݱҗ%$#AV^EE'HT[H0%}ʒِ73]FW^I>ތFV, 4'R*FsL 1E&IX33.}~xs8g WCŸ#) -ޙk܄u1'\ExrK[y\IJ Ίkl#s̊< 3Kkxʀ+(ݑAvsiReF4MTc t%Yof+k3FqqLM_\w5k-Sz?™"~i`P}ۯY}CڨFs1P=K4 x兪Xڅg ~QH<5=L*X;?ku+Db*H(9Zࣺ+@']Qh{{;-|2y֩reźR2sG&D=DZn.;F ތƈ ^H:~! {RÔ OڷpZ݀g0#J> FN1F|go+zy-."\cS[?[& )p ˏX`2d%DZ?tY+ p0zQS0 W @LduWQ#\wp1ހChr?Ʃ*9T *ghjjg޹R MȢ*B@-y/ +HklI6rk>K&fr CDy4U bogt3#d!< ZdJuVZ^@pnނV 9 vh'9';Qʆ &]yt1pꈥҨAp3zs4QN #c:A[Eh3ލwc}*K g &Pּ{=3cGYL 锾D p0)M`VlRF)W+#ڳr9P ֪bA}1H3>ǂha*blcZi)#t3+ypޛkQ\Iy,#SK`aX?]֝3M3\FGֵ[ %QԀ9@M1渹.U'Ѐt%ܗ7M,ZFx< cOu=F?KI8F.mZXΦA6:m=PoZ[Z+޲,Q7Zi}jxf~:"[`$@8ݩƛf-j>2m-YJ6SY_0SY4PXwZ+F%<¨I7mau zLr;(ھ˥K p{Xjؼg-RdYbn)A.E.xfcF(KI4,aۍA-t7ȨA%W!AQ׽AtcM7hr cZ!1F,> )f;20~(͜,9m^#2K,QUNZCBWۖ7Xc?s҄[a#xE=}Ѵ%X*۽<Ӵ^IjuƧv#qi3tVLmFQ=Ğ"O\'A4g]A73CP3~yEUsUK>IK< cF\Wޗxl*@ hg!X@*#^큤JTxX(A.K``~5]+)n҃xAkJ% -ze,_YX'LdD>P(Ev֑킺ΧlCE%]HQ^MK  5RX ;jof q}aLXgr@]]Ze?ڎg]]HaQ)N&,0471{U[@%]]Cb>`3GjW+xq&B;{ބDwv+Z$ xz xɮdXdwzsU3Ńמ՘Kme0v9'ѫ|$00ic]]Af6VsCi yGϘ{ ꞑD(L;&: )XjFKKIIJ~VvPӦEv6%G6l Һ~K9VFxBz֫IT28'uu<6sE+ [j&:6&1[tLd*e`\Vi'$6W^ 2dȈ0xf<Ϋ:'uu\lG䢷 \-`c)?uuw6 5o. fojm4y.?n+)j:ď+6;ڭӭg7MG$1O񮮧H ~+~=I?8 ۉH=uu0gxh""!Bx0̏ 8 /g" (1JҺEơAy_5۲4Vb?>LkʗՃ m7:.l7KOS>մ!#iveK3Tq#+E{*7-~WWVpyface-6.1.2/pyface/images/panel_gradient_over.png0000644000076500000240000000405413462774551023202 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-6.1.2/pyface/images/carat_open.png0000644000076500000240000000035213462774551021303 0ustar cwebsterstaff00000000000000PNG  IHDR gAMA7tEXtSoftwareAdobe ImageReadyqe<|IDATxb?!@L Dbpva-T*@ b9yP @V(@arW.`iyEyG Fb 1tĞIENDB`pyface-6.1.2/pyface/images/image_not_found.png0000644000076500000240000000136713462774551022334 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-6.1.2/pyface/images/about.jpg0000644000076500000240000007022413462774551020303 0ustar cwebsterstaff00000000000000JFIFC    #%$""!&+7/&)4)!"0A149;>>>%.DIC;C  ;("(;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;("=!1A"Qaq2#B3RbCr$4s$!1AQ"a2qB ?Ŧ8MOR:UjxZڼwrH1Z*@d J1 sV+w'XLwPKj$Y ySZi>.zHV~ y9m GQ_$AF;c88+'~M`F:jL#"@#TϽVF`ԗqXd1gBī?Q֌ЬRPrDd7 sz[eu%,lA_xi⹉%YG'Ϛ)$#C]pHGfu),7EmNu>RpBQ@s&ܮDGC[nxm Gce.M@=L4)؟^Ѧh˂=9𕞋@J\[%!ZM<lvM5I8tuӥnۏd $vY>ęk6"W@{_hϤYseUMv4-h%T#!Rn9Hp)bj/2ˁQEiH9553ikq\f9$W8ZK7`.4y/}Ǘ(= Z3XsQha1F701jq.k 򘬋Cuq!{`C3ځ+&mb M=j;gzGdZx2 %{-nr߉,jZrԚFlnqT rګJR1Eᰒܱ ֑ZJmSRUmj2pw8K${ugz ',1FɦVɈd2c:Q2E$06}Y)AJ玼œYʧ #8sߥ-3WE3iW>Z#ʀI3FzT=+c# 3 ?ƾuzjڔ'XO8*!~ة<}k*Ec qm#2Dlּ qdQ6;#炭8棴 L!Y_lcYx9+ cr/CZ'6:12H;dގ~Iӥ^y.v~u.0G^ע,l2yǰX/" D*X1w8.Ǡ$ӰEψc!LFSGR8B*%l@TBƇ}6ESd!'cjxK RnLD8X$B%Uc3 ls晴/Se%G=;ĜuYe4+/Kg ޡu,=cQжp>$ ,7lnѰ5X.+2GҫʃqȪe1.rݫ>KK =( '2z#?TWȌ9n+T MHt"|g'ְk(vJ3)m=h#WٚGaU_WJ&K}5fMCyH{fp{Ajsu]E$`a*uRI߸I9_t'5t4jrJjW[.k9"D>|nI:VAdF+uu?Jms\ŗ<5Dቅ7ku#OszIC\h:x3jz͝7P_Qcovm-\7n<˙o5䥼a@J`ӥ$1,ЌZt`y3Ki')4jV=#sɵ=5՝Q2S`}\_IVUX?rLqNwI[ܩvqqc3qg@? @5OZ iY Ao6g|ێ;BE”'QJ7M*<*oC4 :9S+fOߌGZy߄Pp7O.ּw|"hƴ0^ZG 3&+z})F#0:H b7Җi!rcHwS%a@2/|dԿQg lYQVXZ]^YEiͮ"BѰP O)E}UFoMT`V?UZzv7Ḩ[ȿy$<փLE(c|tKYo)e`=RTWi7 ђTҽ#DP!{T1?1(85/-3bpqȭ߇f70FfAF+Z'\sl8!#YKܱ ?j5)`fKdc y=鉷ʂ3;$斃b+=GZqFO9=9QP_jWnKfq6%P{*LhRubd\ Xn!ODgMj<19aѝ6{Zʼg c;Mz76ְ .ZFa*g抍zM<>PcmIg.c+Ck٠Eô0+=F)UIaӜR)GV4ELo?8?SMb wLct,( !`#>#PmBU@WːqiR\tak+]][ks?T>>)hNN1'jS,3B$]ht+m ;- o@c4hQyn34KQiw+z8j*COHI76r mqJѨBİ!q֬5;tŎA("1d2dZMi 9Y ab[-%X'Pk4++]akRr7,ۉ?)u17 j[7T-k@ј\tKțZf3!T9` jӥK0L 1h]OoRvF&Z- N1g#h<߾{W<2VeĬ3"8ݒ(Xb(!|{sylfx?WF AT$wsDIG"u<*;Wӵ(cw2O:$(+7K&s@]'SkqqW\hwY2jٙ7]s~բկ]`) i]/$ sGּP#_*F_, 'fY7P}2D?#}q)|CsڪƇ*D0?L)5P@ Ur4*Y7Y@ +[Jx =JH9Z9k)*6p]ބɔ 4sF5!Hta}: HLáN[Rܵgw(.)YgxlK+x"HI%T\z2_xl2B-.|9F|!f-zVVtk5+E&xj ̓ сLv4kSQ֪KQ i%r2o Wwqޙph(҆=+)F= uم mh犻UԎZEIe `6n$ֺTnkb#̍GcSHn;歳9 gx*4LHm=# px=*9lϑ~ Ѓ⾍(ne2qې|I  9$@X= `{A@OsҲ}0;zGJ}08cYfrN#%<#5X]HpUIڱZ˨jClO$Wy`z>Z FA1nKT[cl삘6=4Jrqjcl*\j[Ghy;j•=R\HU@PYwr UM s,*݀E/52 UAd%p=I=(%@\CB0 *qy,!,}DҺll< brsT\$@z:3tsOJœ%Rx ]!MVe;ՃB]RO̤)bP>R~$V m.qZ˅fb*3(3׌"MA9!v\8QgDYI`vgc{d{gwirгHXĀ?{PeԮV1! xSY m9%m,%Zv<~jYj0@0&y{F;,[^EGN+Xڵ++ HxPW|wyu؏~PijnޫЂMQGm-[r+ ';Z KetXTiKb#pIQ;r:לco"rKHRyNJ I SI- )ǡ=0$Ki⻶e t"ȕḂr9J @}Cr@WvmI}RcW{rq44dB24go?ڂ~K5-R[(#?ǏXx@jZbkIlG$vϽ& #][jQғZ% ZdSD4rƭGUy4K,NdqX>As-02;0ZE&и 7S/"'.ș Pe=)] <)wtamzx#Hw4Xk6SOpJM5e]9)q%DbzkcFr1ip`r?Z< CЂ3?Jcf}`-~~(33\}6Ϟ9@Id`=*_&@=;SY/7ZM`LtjA N]YϰslnA+Aqz 6I"G¢3 z =i 6OQU  2Tƥy&}Hr9A\+ȡ@$'8"{F' u'ہ&$=|YRpHZ!cyK5 Ӡ{)NqGHUν/YXc^jA0Nyo{fa,glb"laBek J5Z [vGnI* XLLa'<8vn[-[os)AD-G~k\96'MuaRn$/?4[Y+& sK ѰYFGei^p4dKY-P4)`6 m4%bNI^MDDaLZZ|$K{ina#y恸ůo\OaZ$1a VW&KfN]oQv˙7[Fvaq$R^fK ʂ AL=g/!ѧ}ͼvp]h˛?P=#[dt|OIIdr0G_ݿZ3hH%4_m*MG] ӻY !>j?sȫ<БJ (=)&ZTAT=ARsӭQ!pr3*4Bїg*=ѓާ6X2\t}No v8# &x>Ҹ dIsx@]6-F8p{۰?J8:1{,I,NsTu1'jlڃF[+vROjk_-c'Vٿ7ڮ07r; _4 /mld): .*=՜sP˹U03aY%$'''ll,6;+×vojT`lN\d]]B*b8#?N s"lu'X6XU6Jd).؟a*Zg]lHhad+cW=s%|̫֒@:BUrLTIbe1w5}}+YFcj(I4]gV͵m,8gVH72[Kٰl㡢+uvNzɡoM40@88)RqکTC!VXփȗ"%mQMv6<s܎zXQ9SZYcADVn,#Tszڱ'Z/ sHݏamn4p biHqK`ƯR f*8=a:tѫze~*pUFXUHDFI=*+}з@B:B01`o01ym+v1a=Z"pv3)qlW$ A4:` y'#SKWS^~PmmMm>ReB$7^b~zz>fB QҳM<`a8=V{jm';ժT=$'<+CWČ>T6Q9@=c$_f 5fFQCќ[Yn|ߜR BO_I^NqQފяAU,ޢ ZYm*}kag_*81FDl$( .NFM/ 4ꊪN(K.IS+m VLua"OvmPryM8@?Kկ5+iR6qz;I fեӬ#W%真`84mb[qL/oFQMEEG0sfE[`u=Buh텱|2-oqzOfEDRrZ_{spܸ!7Eta9|E)@OJ6*99c5-Ih[FdˍޮwcYȭ&]D {gS70h`ux.yU1 3'edkȕzaRnT`~Ny旴XؿԎ24sGXQ!aG>)tTѴIWv槧4w9 ~@:/ۊYC`$Q13FnRm $  G;ISiJsw1=if@ac*9q4vZC@;բ.AĠ~e\=Y;ۅTbKGZIkW6>%e|sqj+%Pv-}=٤W60!9}.Jv-[SQ udFФz)բ2^hU}={A$֓,*LֲUKh4 Ksr6@㟵7v?4teK v$ 飶JvŤrPTZ6 ̥ҲCSD??JZ#=]L#AbFw*OaA}O]q69jمbIT6e:_[iwmC~W-.Z6$w_՗F92eQb/wNΈz:[=;Nn›%򷡈ݞVSODW{Xm`:{R}~mWMH O|o)f̒3*$\jM*v@+ ا⭹Lok @,QZ ombNA]G֓l$rNFR@Ü:3\-zzRV y"TH*xbZiL.!X@*mZ}6x{Ty`%&\ k=Ҩ>[lW52"`N p{A*񥦻$6 ۨIJ=g-e݋xe;]_=5d9\^RnDWC~J,/K;.)14gaY* @0 4G+"NN-ڋS3I[⬧TZt"P28?|x- iB?eRKyR?sRHucIeS<`, 8-f ,NKU1e4̎p =+P֍u;k]L#҉xv 2##`=!?hՃ;r; -Ȣy;s3T]X4g xgZ&G^jb4c!-MMA'XW'~Pe"H)nS2Y%!W'hhT8HSl,l'-3 RZ:I#>Qa,eK|VJgR""rlCwZs3vn)ͅp@D=9 WJ6B{;]NJdȥwZe핱̣רSdęsb.@~5zfE5I#^=Unn2љ Fisnvrwgr9R^DVjcrRrFz5/m";ROsQ`83M h0\eIf?+<ɥXΙ'Dd#{*0_k$^qlDmϽFMd2I{}h-0 fwV$`mV^Ĭ 7oL&( S$ӕ,;5v?:ҲW$dqNdeC v%&MwycCzԥ&MՔ-d:nUTJ[MHpOC% )+9A #bYO?)$ڹQ ]waI]ivע9n'5GtAtInnJbx۽갂FrQ8r16zQSNiӤ7LFH#ھcNڡǔ۷#GnMc-O].i^'c 0={{Jꣿҩ$g䐿Ὲ,Ҷ1U6)߷g98Vx&V(hY\NH#OZ7d }3e gPRec'ڪ.I#S+ݍʐ1!j[py>63֪%۞Bd;IfsE&F$.r%Hsf:7 /[d%'c1yX1wU0A`J۔&o4Xc2&=ȭޜC3ck@;E*vj<gOVjnI k|#{UQ<7v65F8a X3Ҧ뀲땗S[gUfbI9lg)]52N`@ۣsJ0 zcBY 8?]ցs-–i  q{R9)\. 69js&6Ka7{,2V"¶]t +5b7/ >)ƻRM$$HJJиC9R)gvf|`~^ P3z7. q^pq A{WxI-cOK-:chlq Q݄W&x J(-QFڤ]YŪ4ߪ9 h1wmo+BI# ݕV2$ji4\,l/ |R- .[5r=Mh]Y@fY܉'POpi.3ިD$[vVo[[+YA-pqe73H#pWۼҿ˯k-fͼdUZ13^NJcl[^OxvDӉ,o+'YUb:es-d5}MWdI-{C zWQqn:0uUR;vT8~ᚱ/ojҧ4Eo,dg:WVI>Trp9:W֭#E*sЊj0 [,(5gm0zXS1zRł{~ERlt :s]\HQ.IpxqAϵZiW=Gp۷%~GJ0FppGڅEI#q:UeT''k5iV1[4{-.: L Iezmnm 绍cN R|Ƨ,O4ʦиR } TRH៊1Ïcֆ, rE\']$& ۖ]JxVv%>;[hOJY^MsR4ʼn>ֵӕQ@<89X @Kw"DoRz+o]nV9[Z{F}"?3'MquyBnTcGCiSJ4Q(Lo4F^UjxC=~Z~ݤ[N帏E2RaTA@ֆ($6SH|; y`qǖZȢv.'U#ypa#ƂI ?h=Pt]qGVqjd3IM {B#'?qdprjfh)Y0qj]vU4LR8E#KCJq@s׃ԺdA+:f%hQbGtA1BHEN QS?)ܫ%w U#~԰uu-y,Hy5zEő"up  ua`AjZu‰$GfO}x<7iw7əWQ:gިZ0g4et |:WWw3Ϟ&K2IvG jQF/o_Jy`I,}z%ReaҖZm%QHw08j7Mm4PmbBwa[_ Y鷯qg Qi#l1SW)Mlp(aI2ER>)mJ㴵&$ɘwlzIq"HPс VCOA7R}ڦl L=0^·y8 Z 62z|9.# MXlmnXH$M>e|CucW4K!HAۦOȯF庉D p_Ȩ Z ;RFUaPm*,LJcnL+`*:ſYF铟'?Vas7RU2_ަC37mɑ&ƱGvNܪ_M|_^0?f@MQnRD}ʡ8X-#KcAE$G1MO—-DIg}jN7ڃϧ=Zq Fz <⺍# c_Bry?'|QF~w(m01lLWБvRbI2En7] O`ы)*2xR)'8aA=TL2u|)wm Vz~y$ V\#ب:dd'( J)hmV?z!Igg~6#U< h5 6I㊽GK,?v2#F'q"<@$gҰ8˰bGk%5L)9 j$hQE Mh.Kk(#)|Q4(nJA^4oeLZ?7ʦYYhIOeYlXYĞ )7-`~D1ȍ|fhjk-)A])K +r:_*gkb˟j-F{-lo!љ RK%v|LWyfW6sIgw08Ps?U&n-84[_4hnj噋vg^H=f*'TVƳโ9H泯qyvXs^<mk-z$?i:{^S07ۥR9߸C-ty%A9S!{EɝQ4B_ֺkb#:UR"vM-m!&E$(O,f""<$[Z?GYd+zQ3ѷ6#ɷ vއY.5 cQmn{_t : v~.J xFfס,D7c5؃FWxf R8tƪ+n O/10FIJlLB5w#xATb{HABa@Ǟ(OKK4gX^n+ XB.'(,s֭UTc9"4nU]qU"s#2OOAz;*NzUYl-l^3)$QH̡excҖίy;cv+ ǵ4>Yߓҧ%+gVt%gpL}i)P9SG<∔~r Tr=%܃#gbcWh%>dHIi%.I?qI$ۢ0M#[HpJ&J @SWN-ּC;N8^?4QkV'BhYVf9wrsڽš ԣ_&Qfv RY0Gv`*0fFnKiGLV5 [WK2M, p$耜gڈ񎡧Wi2Oo}~&XI=I[E}6c446𕍺0iN6 ԍha66%HL*P^ ҆D Fv2Fx GMsITӥFB\_z[ Sy;eQ6,7<Kpc#/C,p894z9d_[Csy0sUݼ6D[) fF{si[(#9#ǯUSrn:A(.# r:M! 6I$9~A<'qt! y5HI$ZE=nQ#{ԄswyB 9<5}[Lr:d* i1\he76qI$D.cXmߚR-$?'+7s8cqjvO2)&p ;^1M K!XƐK2Z(<ќGnv)d+]\7YI}MkTbt:^Ku$g#=3Ix_2I rtUN(^*SSyefi9.O'jS0i,H895Eg9$r~}#F]ok,Qf|:ڼ~r3қB倹ܪ9j$ sWM&Oz& .<e8.=psR1MEbkKi ) 2u11"FOnEj4udjSC[K c'|Z`GRkkvH}<&X O,S4>ݱҗ%$#AV^EE'HT[H0%}ʒِ73]FW^I>ތFV, 4'R*FsL 1E&IX33.}~xs8g WCŸ#) -ޙk܄u1'\ExrK[y\IJ Ίkl#s̊< 3Kkxʀ+(ݑAvsiReF4MTc t%Yof+k3FqqLM_\w5k-Sz?™"~i`P}ۯY}CڨFs1P=K4 x兪Xڅg ~QH<5=L*X;?ku+Db*H(9Zࣺ+@']Qh{{;-|2y֩reźR2sG&D=DZn.;F ތƈ ^H:~! {RÔ OڷpZ݀g0#J> FN1F|go+zy-."\cS[?[& )p ˏX`2d%DZ?tY+ p0zQS0 W @LduWQ#\wp1ހChr?Ʃ*9T *ghjjg޹R MȢ*B@-y/ +HklI6rk>K&fr CDy4U bogt3#d!< ZdJuVZ^@pnނV 9 vh'9';Qʆ &]yt1pꈥҨAp3zs4QN #c:A[Eh3ލwc}*K g &Pּ{=3cGYL 锾D p0)M`VlRF)W+#ڳr9P ֪bA}1H3>ǂha*blcZi)#t3+ypޛkQ\Iy,#SK`aX?]֝3M3\FGֵ[ %QԀ9@M1渹.U'Ѐt%ܗ7M,ZFx< cOu=F?KI8F.mZXΦA6:m=PoZ[Z+޲,Q7Zi}jxf~:"[`$@8ݩƛf-j>2m-YJ6SY_0SY4PXwZ+F%<¨I7mau zLr;(ھ˥K p{Xjؼg-RdYbn)A.E.xfcF(KI4,aۍA-t7ȨA%W!AQ׽AtcM7hr cZ!1F,> )f;20~(͜,9m^#2K,QUNZCBWۖ7Xc?s҄[a#xE=}Ѵ%X*۽<Ӵ^IjuƧv#qi3tVLmFQ=Ğ"O\'A4g]A73CP3~yEUsUK>IK< cF\Wޗxl*@ hg!X@*#^큤JTxX(A.K``~5]+)n҃xAkJ% -ze,_YX'LdD>P(Ev֑킺ΧlCE%]HQ^MK  5RX ;jof q}aLXgr@]]Ze?ڎg]]HaQ)N&,0471{U[@%]]Cb>`3GjW+xq&B;{ބDwv+Z$ xz xɮdXdwzsU3Ńמ՘Kme0v9'ѫ|$00ic]]Af6VsCi yGϘ{ ꞑD(L;&: )XjFKKIIJ~VvPӦEv6%G6l Һ~K9VFxBz֫IT28'uu<6sE+ [j&:6&1[tLd*e`\Vi'$6W^ 2dȈ0xf<Ϋ:'uu\lG䢷 \-`c)?uuw6 5o. fojm4y.?n+)j:ď+6;ڭӭg7MG$1O񮮧H ~+~=I?8 ۉH=uu0gxh""!Bx0̏ 8 /g" (1JҺEơAy_5۲4Vb?>LkʗՃ m7:.l7KOS>մ!#iveK3Tq#+E{*7-~WWVpyface-6.1.2/pyface/images/close.png0000644000076500000240000000055413462774551020301 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-6.1.2/pyface/ui_traits.py0000644000076500000240000002030713502146023017552 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # # Copyright (c) 2016, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: Enthought Developers # #------------------------------------------------------------------------------ """ Defines common traits used within the pyface library. """ import logging from traits.api import ABCHasStrictTraits, Enum, Range, TraitError, TraitType from traits.trait_base import get_resource_path try: from traits.trait_handlers import CALLABLE_AND_ARGS_DEFAULT_VALUE except ImportError: CALLABLE_AND_ARGS_DEFAULT_VALUE = 7 import six logger = logging.getLogger(__name__) #------------------------------------------------------------------------------- # Images #------------------------------------------------------------------------------- image_resource_cache = {} image_bitmap_cache = {} def convert_image(value, level=3): """ Converts a specified value to an ImageResource if possible. """ if not isinstance( value, six.string_types ): 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_resource): """ Converts an ImageResource to a bitmap using a cache. """ bitmap = image_bitmap_cache.get(image_resource) if (bitmap is None) and (image_resource is not None): image_bitmap_cache[image_resource] = bitmap = \ image_resource.create_bitmap() return bitmap class Image(TraitType): """ Defines a trait whose value must be a ImageResource or a string that can be converted to one. """ # Define the default value for the trait: default_value = None # A description of the type of value this trait accepts: info_text = 'an ImageResource or string that can be used to define one' def __init__(self, value=None, **metadata): """ Creates an Image trait. Parameters ---------- value : string or ImageResource The default value for the Image, either an ImageResource object, or a string from which an ImageResource object can be derived. """ super(Image, self).__init__(convert_image(value), **metadata) def validate(self, object, name, value): """ Validates that a specified value is valid for this trait. """ from pyface.i_image_resource import IImageResource if value is None: return None new_value = convert_image(value, 4) if isinstance(new_value, IImageResource): 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() #------------------------------------------------------------------------------- # 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(BaseMB, self).__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(HasMargin, self).get_default_value() self.default_value_type = dvt = CALLABLE_AND_ARGS_DEFAULT_VALUE 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') pyface-6.1.2/pyface/heading_text.py0000644000076500000240000000162513462774551020236 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Heading text. """ # Import the toolkit specific version. from __future__ import absolute_import from .toolkit import toolkit_object HeadingText = toolkit_object('heading_text:HeadingText') #### EOF ###################################################################### pyface-6.1.2/pyface/sizers/0000755000076500000240000000000013515277235016530 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/sizers/__init__.py0000644000076500000240000000000113462774551020634 0ustar cwebsterstaff00000000000000 pyface-6.1.2/pyface/sizers/flow.py0000644000076500000240000001377713462774551020074 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------- # # Defines a horizontal or vertical flow layout sizer for wxPython # # Written by: David C. Morrill # # Date: 01/12/2006 # # (c) Copyright 2006 by Enthought, Inc. # #------------------------------------------------------------------------------- #------------------------------------------------------------------------------- # Imports: #------------------------------------------------------------------------------- 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( FlowSizer, self ).__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() 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-6.1.2/pyface/timer/0000755000076500000240000000000013515277236016332 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/timer/i_timer.py0000644000076500000240000002024713514577463020345 0ustar cwebsterstaff00000000000000# Copyright (c) 2018, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: Corran Webster """ 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 sys import time from traits.api import ( ABCHasTraits, Bool, Callable, Dict, Either, Event, Float, HasTraits, Int, Interface, Property, Range, Tuple, provides ) if sys.version_info[:2] < (3, 3): import timeit perf_counter = timeit.default_timer else: 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 = Either(None, Int) #: The maximum length of time to run in seconds, or None if no limit. expire = Either(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 = Either(None, Int) #: The maximum length of time to run in seconds, or None if no limit. expire = Either(None, Float) #: Property that controls the state of the timer. active = Property(Bool, depends_on='_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-6.1.2/pyface/timer/timer.py0000644000076500000240000000337613514577463020041 0ustar cwebsterstaff00000000000000# Author: Prabhu Ramachandran # Copyright (c) 2006-2018, Enthought, Inc. # License: BSD Style. """ 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(Timer, self).__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-6.1.2/pyface/timer/tests/0000755000076500000240000000000013515277236017474 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/timer/tests/__init__.py0000644000076500000240000000000013462774551021576 0ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/timer/tests/test_timer.py0000644000076500000240000002777413514577463022252 0ustar cwebsterstaff00000000000000from __future__ import print_function 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): 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.on_trait_change(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.on_trait_change(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.on_trait_change(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.on_trait_change(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.on_trait_change(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()) self.assertEqual(handler.count, 4) expected_times = [start_time + 0.2 * i + 0.2 for i in range(4)] 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-6.1.2/pyface/timer/tests/test_do_later.py0000644000076500000240000000675413502644114022677 0ustar cwebsterstaff00000000000000from __future__ import print_function import time 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-6.1.2/pyface/timer/__init__.py0000644000076500000240000000000013462774551020434 0ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/timer/api.py0000644000076500000240000000151513462774551017462 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ from __future__ import absolute_import from .do_later import do_later, do_after, DoLaterTimer from .i_timer import ICallbackTimer, IEventTimer, ITimer from .timer import CallbackTimer, EventTimer, Timer pyface-6.1.2/pyface/timer/do_later.py0000644000076500000240000000362113475176547020507 0ustar cwebsterstaff00000000000000# Copyright (c) 2005-18, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: 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(DoLaterTimer, self).__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 : 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, **kwargs : Arguments to be passed through to the callable. """ return CallbackTimer.single_shot( interval=interval / 1000.0, callback=callable, args=args, kwargs=kwargs, ) pyface-6.1.2/pyface/split_panel.py0000644000076500000240000000255413462774551020107 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A panel that is split in two either horizontally or vertically. """ # Local imports. 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, **traits): """ Creates a new panel. """ # Base class constructor. super(SplitPanel, self).__init__(**traits) # Create the widget's toolkit-specific control. self.control = self._create_splitter(parent) pyface-6.1.2/pyface/i_about_dialog.py0000644000076500000240000000255013462774551020532 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The interface for a simple 'About' dialog. """ # Enthought library imports. from traits.api import Instance, List, Unicode # Local imports. from pyface.i_dialog import IDialog from pyface.image_resource import ImageResource class IAboutDialog(IDialog): """ The interface for a simple 'About' dialog. """ #### 'IAboutDialog' interface ############################################# #: Additional strings to be added to the dialog. additions = List(Unicode) #: The image displayed in the dialog. image = Instance(ImageResource, ImageResource('about')) class MAboutDialog(object): """ The mixin class that contains common code for toolkit specific implementations of the IAboutDialog interface. """ pyface-6.1.2/pyface/tests/0000755000076500000240000000000013515277236016354 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/tests/test_split_application_window.py0000644000076500000240000000622413500710640025057 0ustar cwebsterstaff00000000000000from __future__ import absolute_import 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-6.1.2/pyface/tests/test_toolkit.py0000644000076500000240000000447013500710640021440 0ustar cwebsterstaff00000000000000import pkg_resources import unittest 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): obj = cls() def test_bad_import(self): # test that we don't filter unrelated import errors with self.assertRaises(ImportError): cls = pyface.toolkit.toolkit_object('tests.bad_import:Missing') def test_core_plugins(self): # test that we can see appropriate core entrypoints plugins = set(entry_point.name for entry_point in pkg_resources.iter_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-6.1.2/pyface/tests/test_splash_screen_log_handler.py0000644000076500000240000000165613462774551025167 0ustar cwebsterstaff00000000000000from __future__ import absolute_import import unittest from traits.api import Any, HasTraits, Unicode from ..splash_screen_log_handler import SplashScreenLogHandler class DummySplashScreen(HasTraits): text = Unicode(u'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, u'original') message = u'G\u00f6khan' self.sslh.emit(DummyRecord(message)) self.assertEqual(self.ss.text, message + u'...') def test_ascii_message(self): message = 'Goekhan' self.sslh.emit(DummyRecord(message)) self.assertEqual(self.ss.text, message + u'...') pyface-6.1.2/pyface/tests/python_shell_script.py0000644000076500000240000000016613462774551023030 0ustar cwebsterstaff00000000000000# dummy script for testing python shell and python editor widgets # simple import import sys # set a variable x = 1 pyface-6.1.2/pyface/tests/test_about_dialog.py0000644000076500000240000000611313500710640022400 0ustar cwebsterstaff00000000000000from __future__ import absolute_import import unittest from ..about_dialog import AboutDialog from ..constant import OK, CANCEL from ..gui import GUI 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) pyface-6.1.2/pyface/tests/test_split_panel.py0000644000076500000240000000372513500710640022267 0ustar cwebsterstaff00000000000000from __future__ import absolute_import 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(): self.widget = SplitPanel(self.window.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-6.1.2/pyface/tests/test_progress_dialog.py0000644000076500000240000001111113500710640023124 0ustar cwebsterstaff00000000000000from __future__ import absolute_import 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_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-6.1.2/pyface/tests/test_gui_application.py0000644000076500000240000002250013462774551023136 0ustar cwebsterstaff00000000000000# Copyright (c) 2014-2018 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! from __future__ import ( absolute_import, division, print_function, unicode_literals ) import os from shutil import rmtree from tempfile import mkdtemp import unittest from traits.api import Bool, on_trait_change 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(TestingApp, self).start() window = self.windows[0] window.on_trait_change(self._on_window_closing, 'closing') return True def stop(self): super(TestingApp, self).stop() return self.stop_cleanly def _on_window_closing(self, window, trait, old, new): if self.veto_close_window and not self.exit_vetoed: new.veto = True self.exit_vetoed = True def _application_initialized_fired(self): self.window_open_vetoed = ( len(self.windows) > 0 and self.windows[0].control is None ) def _exiting_fired(self, event): event.veto = self.veto_exit self.exit_vetoed = self.veto_exit def _prepare_exit(self): super(TestingApp, self)._prepare_exit() if not self.exit_vetoed: self.exit_prepared = True if self.exit_prepared_error: raise Exception("Exit preparation failed") @on_trait_change('windows:opening') def _on_activate_window(self, event): if self.veto_open_window: event.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): self.application_events.append(event) def connect_listeners(self, app): for event in EVENTS: app.on_trait_change(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.on_trait_change(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-6.1.2/pyface/tests/images/0000755000076500000240000000000013515277236017621 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/tests/images/image_LICENSE.txt0000644000076500000240000000113313462774551022607 0ustar cwebsterstaff00000000000000The icons are mostly derived work from other icons. As such they are licensed accordingly to the original license: Project License File ---------------------------------------------------------------------------- Nuvola LGPL image_LICENSE_Nuvola.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/tests/images: core.png | Nuvola pyface-6.1.2/pyface/tests/images/core.png0000644000076500000240000000736013462774551021270 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-6.1.2/pyface/tests/test_application_window.py0000644000076500000240000001724013500710640023644 0ustar cwebsterstaff00000000000000from __future__ import absolute_import 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_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-6.1.2/pyface/tests/test_splash_screen.py0000644000076500000240000000706413500710640022606 0ustar cwebsterstaff00000000000000from __future__ import absolute_import import unittest from ..gui import GUI 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-6.1.2/pyface/tests/test_clipboard.py0000644000076500000240000000477513500710640021722 0ustar cwebsterstaff00000000000000from __future__ import absolute_import import unittest from ..clipboard import clipboard 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.assertEquals(self.clipboard.data_type, 'str') self.assertEquals(self.clipboard.data, 'test') self.assertTrue(self.clipboard.has_text_data) self.assertEquals(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 = u'test' self.assertTrue(self.clipboard.has_data) self.assertEquals(self.clipboard.data_type, 'str') self.assertEquals(self.clipboard.data, u'test') self.assertTrue(self.clipboard.has_text_data) self.assertEquals(self.clipboard.text_data, u'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.assertEquals(self.clipboard.data_type, 'file') self.assertEquals(self.clipboard.data, ['/images']) self.assertTrue(self.clipboard.has_file_data) self.assertEquals(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.assertEquals(self.clipboard.data_type, TestObject) self.assertEquals(self.clipboard.data, data) self.assertTrue(self.clipboard.has_object_data) self.assertEquals(self.clipboard.object_type, TestObject) self.assertEquals(self.clipboard.object_data, data) self.assertFalse(self.clipboard.has_text_data) self.assertFalse(self.clipboard.has_file_data) pyface-6.1.2/pyface/tests/test_split_dialog.py0000644000076500000240000000353713500710640022430 0ustar cwebsterstaff00000000000000from __future__ import absolute_import 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-6.1.2/pyface/tests/test_base_toolkit.py0000644000076500000240000000362513500710640022433 0ustar cwebsterstaff00000000000000import 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-6.1.2/pyface/tests/__init__.py0000644000076500000240000000000013462774551020456 0ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/tests/test_confirmation_dialog.py0000644000076500000240000003412113500710640023756 0ustar cwebsterstaff00000000000000from __future__ import absolute_import 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 = u"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 = u"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 = u"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(u"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 = u"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(u"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 = u"Back" # test that Cancel works as expected tester = ModalDialogTester(self.dialog.open) tester.open_and_wait(when_opened=lambda x: x.click_widget(u"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-6.1.2/pyface/tests/test_single_choice_dialog.py0000644000076500000240000002070513500710640024064 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # # Copyright (c) 2016, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: Enthought, Inc. # #------------------------------------------------------------------------------ from __future__ import absolute_import import unittest from traits.etsconfig.api import ETSConfig from ..single_choice_dialog import SingleChoiceDialog from ..constant import OK, CANCEL from ..gui import GUI 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 = u"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-6.1.2/pyface/tests/test_heading_text.py0000644000076500000240000000431513500710640022414 0ustar cwebsterstaff00000000000000from __future__ import absolute_import 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') @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(): self.widget = HeadingText(self.window.control) with self.event_loop(): self.widget.destroy() def test_message(self): # test that create works with message with self.event_loop(): self.widget = HeadingText(self.window.control, text="Hello") with self.event_loop(): self.widget.destroy() def test_image(self): # test that image works # XXX this image doesn't make sense here, but that's fine # XXX this isn't implemented in qt4 backend, but shouldn't fail with self.event_loop(): self.widget = HeadingText( self.window.control, image=ImageResource('core.png') ) with self.event_loop(): self.widget.destroy() def test_level(self): # test that create works with level # XXX this image doesn't make sense here, but that's fine # XXX this isn't implemented in qt4 backend, but shouldn't fail with self.event_loop(): self.widget = HeadingText(self.window.control, level=2) with self.event_loop(): self.widget.destroy() pyface-6.1.2/pyface/tests/test_resource_manager.py0000644000076500000240000000113013500710640023262 0ustar cwebsterstaff00000000000000from __future__ import absolute_import import os import unittest from ..resource_manager import PyfaceResourceFactory 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): image = self.resource_factory.image_from_file(IMAGE_PATH) def test_image_from_data(self): with open(IMAGE_PATH, 'rb') as fp: data = fp.read() image = self.resource_factory.image_from_data(data) pyface-6.1.2/pyface/tests/test_image_cache.py0000644000076500000240000000241113500710640022151 0ustar cwebsterstaff00000000000000from __future__ import absolute_import 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): image = self.image_cache.get_image(IMAGE_PATH) def test_get_bitmap(self): bitmap = self.image_cache.get_bitmap(IMAGE_PATH) def test_get_image_twice(self): image1 = self.image_cache.get_image(IMAGE_PATH) image2 = self.image_cache.get_image(IMAGE_PATH) def test_get_bitmap_twice(self): bitmap1 = self.image_cache.get_bitmap(IMAGE_PATH) bitmap2 = 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-6.1.2/pyface/tests/test_widget.py0000644000076500000240000001025113500710640021230 0ustar cwebsterstaff00000000000000from __future__ import absolute_import import unittest from traits.testing.unittest_tools import UnittestTools from ..toolkit import toolkit_object from ..widget import Widget GuiTestAssistant = toolkit_object('util.gui_test_assistant:GuiTestAssistant') no_gui_test_assistant = (GuiTestAssistant.__name__ == 'Unimplemented') 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 == 'qt4': from pyface.qt import QtGui control = QtGui.QWidget(parent) control.setEnabled(self.enabled) control.setVisible(self.visible) else: control = None return 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_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_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()) pyface-6.1.2/pyface/tests/test_file_dialog.py0000644000076500000240000000566113500710640022214 0ustar cwebsterstaff00000000000000from __future__ import absolute_import import os import unittest from ..file_dialog import FileDialog from ..gui import GUI 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-6.1.2/pyface/tests/test_window.py0000644000076500000240000002247613500710640021270 0ustar cwebsterstaff00000000000000from __future__ import absolute_import 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-6.1.2/pyface/tests/test_dialog.py0000644000076500000240000002210413500710640021204 0ustar cwebsterstaff00000000000000from __future__ import absolute_import 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 = u"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 = u"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 = u"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 = u"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(u"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 = u"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(u"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(u"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(u"Assistance") tester.close(accept=True) self.dialog.help_id = "help_test" self.dialog.help_label = u"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-6.1.2/pyface/tests/test_application.py0000644000076500000240000001472113462774551022300 0ustar cwebsterstaff00000000000000# Copyright (c) 2014-2018 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! from __future__ import ( absolute_import, division, print_function, unicode_literals ) import os from shutil import rmtree from tempfile import mkdtemp from unittest import TestCase from traits.api import Bool from traits.testing.unittest_tools 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(TestingApp, self).start() return self.start_cleanly def stop(self): super(TestingApp, self).stop() return self.stop_cleanly def _run(self): super(TestingApp, self)._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 def _exiting_fired(self, event): 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): self.application_events.append(event) def connect_listeners(self, app): for event in EVENTS: app.on_trait_change(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-6.1.2/pyface/tests/test_new_toolkit/0000755000076500000240000000000013515277236021751 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/tests/test_new_toolkit/widget.py0000644000076500000240000000026113462774551023610 0ustar cwebsterstaff00000000000000# 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-6.1.2/pyface/tests/test_new_toolkit/__init__.py0000644000076500000240000000000013462774551024053 0ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/tests/test_new_toolkit/init.py0000644000076500000240000000023613462774551023272 0ustar cwebsterstaff00000000000000# Dummy toolkit for testing entrypoints from pyface.base_toolkit import Toolkit toolkit_object = Toolkit('pyface', 'test', 'pyface.tests.test_new_toolkit') pyface-6.1.2/pyface/tests/test_python_shell.py0000644000076500000240000001141213500710640022455 0ustar cwebsterstaff00000000000000from __future__ import absolute_import 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(): self.widget = PythonShell(self.window.control) with self.event_loop(): self.widget.destroy() def test_bind(self): # test that binding a variable works with self.event_loop(): self.widget = PythonShell(self.window.control) 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 with self.event_loop(): self.widget = PythonShell(self.window.control) 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 with self.event_loop(): self.widget = PythonShell(self.window.control) 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 with self.event_loop(): self.widget = PythonShell(self.window.control) # 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 with self.event_loop(): self.widget = PythonShell(self.window.control) # 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 executing a command works with self.event_loop(): self.widget = PythonShell(self.window.control) 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 executing a command works with self.event_loop(): self.widget = PythonShell(self.window.control) 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-6.1.2/pyface/tests/test_message_dialog.py0000644000076500000240000002057713500710640022724 0ustar cwebsterstaff00000000000000from __future__ import absolute_import 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 = u"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 = u"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 = u"This is the message" self.dialog.informative = u"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 = u"This is the message" self.dialog.informative = u"This is the additional message" self.dialog.detail = u"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 = u"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(u"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-6.1.2/pyface/tests/test_image_resource.py0000644000076500000240000000665013500710640022746 0ustar cwebsterstaff00000000000000from __future__ import absolute_import import os import platform import pkg_resources import unittest 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 = pkg_resources.resource_filename('pyface', 'images') IMAGE_DIR = pkg_resources.resource_filename(__name__, 'images') IMAGE_PATH = os.path.join(IMAGE_DIR, '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.jpg', [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.jpg')) def test_create_image_search_path_string(self): image_resource = ImageResource('splash.jpg', 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.jpg')) 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.jpg', [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.jpg')) self.assertEqual(size, (450, 296)) pyface-6.1.2/pyface/tests/test_system_metrics.py0000644000076500000240000000117313500710640023022 0ustar cwebsterstaff00000000000000from __future__ import absolute_import 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-6.1.2/pyface/tests/test_directory_dialog.py0000644000076500000240000000450013500710640023270 0ustar cwebsterstaff00000000000000from __future__ import absolute_import import os import unittest from ..directory_dialog import DirectoryDialog from ..gui import GUI 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-6.1.2/pyface/tests/test_beep.py0000644000076500000240000000042213500710640020657 0ustar cwebsterstaff00000000000000from __future__ import absolute_import import unittest from traits.testing.unittest_tools import UnittestTools from ..beep import beep class TestBeep(unittest.TestCase): def test_beep(self): # does it call without error - the best we can do beep() pyface-6.1.2/pyface/tests/test_ui_traits.py0000644000076500000240000004054013502146023021754 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # # Copyright (c) 2016, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: Enthought Developers # #------------------------------------------------------------------------------ import os import unittest from traits.api import HasTraits, TraitError from traits.testing.unittest_tools import UnittestTools try: from traits.trait_handlers import ( CALLABLE_AND_ARGS_DEFAULT_VALUE, UNSPECIFIED_DEFAULT_VALUE ) except ImportError: UNSPECIFIED_DEFAULT_VALUE = -1 CALLABLE_AND_ARGS_DEFAULT_VALUE = 7 from ..i_image_resource import IImageResource from ..image_resource import ImageResource from ..ui_traits import (Border, HasBorder, HasMargin, Image, Margin, image_resource_cache, image_bitmap_cache) IMAGE_PATH = os.path.join(os.path.dirname(__file__), 'images', 'core.png') class ImageClass(HasTraits): image = Image 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') im = 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') 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 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 = UNSPECIFIED_DEFAULT_VALUE (dvt, dv) = trait.get_default_value() self.assertEqual(dvt, CALLABLE_AND_ARGS_DEFAULT_VALUE) 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 = UNSPECIFIED_DEFAULT_VALUE (dvt, dv) = trait.get_default_value() self.assertEqual(dvt, CALLABLE_AND_ARGS_DEFAULT_VALUE) 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-6.1.2/pyface/tests/test_python_editor.py0000644000076500000240000000472513500710640022645 0ustar cwebsterstaff00000000000000from __future__ import absolute_import import os import sys 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(): self.widget = PythonEditor(self.window.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( self.window.control, show_line_numbers=False ) 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(self.window.control) 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(self.window.control, path=PYTHON_SCRIPT) with self.event_loop(): self.widget.select_line(3) with self.event_loop(): self.widget.destroy() pyface-6.1.2/pyface/wizard/0000755000076500000240000000000013515277240016505 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/wizard/chained_wizard.py0000644000076500000240000000533313462774552022047 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A wizard model that can be chained with other wizards. """ # Enthought library imports. from traits.api import Instance # Local imports. 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 #### def _next_wizard_changed(self, old, new): """ Handle the next wizard being changed. """ if new is not None: self.controller.next_controller = 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() return def _controller_changed(self, old, new): """ handle the controller being changed. """ if 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 #### EOF ###################################################################### pyface-6.1.2/pyface/wizard/simple_wizard.py0000644000076500000240000000141613462774552021743 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ SimpleWizard is deprecated. Use Wizard instead. """ # Local imports. from .wizard import Wizard class SimpleWizard(Wizard): pass pyface-6.1.2/pyface/wizard/i_wizard_page.py0000644000076500000240000000546013462774552021701 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The interface for a page in a wizard. """ # Enthought library imports. from traits.api import Bool, Interface, Str, Tuple, Unicode 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 = Unicode # The page sub-heading. subheading = Unicode # 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(object): """ 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 #### EOF ###################################################################### pyface-6.1.2/pyface/wizard/wizard_page.py0000644000076500000240000000162313462774552021366 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ 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') #### EOF ###################################################################### pyface-6.1.2/pyface/wizard/__init__.py0000644000076500000240000000120113462774552020621 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ pyface-6.1.2/pyface/wizard/simple_wizard_controller.py0000644000076500000240000000150713462774552024207 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ SimpleWizardController is deprecated. Use WizardController instead. """ # Local imports. from .wizard_controller import WizardController class SimpleWizardController(WizardController): pass pyface-6.1.2/pyface/wizard/chained_wizard_controller.py0000644000076500000240000001501613462774552024311 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A wizard controller that can be chained with others. """ # Enthought library imports. from traits.api import Instance # Local imports. 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 not page is 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 #### def _current_page_changed(self, old, new): """ Called when the current page is changed. """ if old is not None: old.on_trait_change( self._on_page_complete, 'complete',remove=True ) if new is not None: new.on_trait_change(self._on_page_complete, 'complete') if self.next_controller is not None: self.next_controller.current_page = new self._update() return def _next_controller_changed(self, old, new): """ Called when the next controller is changed. """ if old is not None: old.on_trait_change( self._on_controller_complete, 'complete', remove=True ) if new is not None: new.on_trait_change( self._on_controller_complete, 'complete' ) self._update() return #### Dynamic #### def _on_controller_complete(self, obj, trait_name, old, new): """ Called when the next controller's complete state changes. """ self._update() return def _on_page_complete(self, obj, trait_name, old, new): """ Called when the current page is complete. """ self._update() return #### EOF ###################################################################### pyface-6.1.2/pyface/wizard/wizard_controller.py0000644000076500000240000001205113462774552022632 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A wizard controller that has a static list of pages. """ # Enthought library imports. from traits.api import Bool, HasTraits, Instance, List, Property, provides # Local imports. 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 #### def _current_page_changed(self, old, new): """ Called when the current page is changed. """ if old is not None: old.on_trait_change(self._on_page_complete, 'complete',remove=True) if new is not None: new.on_trait_change(self._on_page_complete, 'complete') self._update() return #### Dynamic #### def _on_page_complete(self, obj, trait_name, old, new): """ Called when the current page is complete. """ self._update() return #### EOF ###################################################################### pyface-6.1.2/pyface/wizard/i_wizard.py0000644000076500000240000001170113462774552020700 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The interface for all pyface wizards. """ # Enthought library imports. from traits.api import Bool, Instance, List, Unicode from pyface.i_dialog import IDialog # Local imports. 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 = Unicode('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(object): """ 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) return 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(MWizard, self)._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): """ Enables/disables buttons depending on the state of the wizard. """ pass ########################################################################### # Private interface. ########################################################################### def _initialize_controller(self, controller): """ Initializes the wizard controller. """ controller.on_trait_change(self._update, 'complete') controller.on_trait_change( self._on_current_page_changed, 'current_page' ) return #### Trait event handlers ################################################# def _on_current_page_changed(self, obj, trait_name, old, new): """ Called when the current page is changed. """ if old is not None: old.on_trait_change(self._update, 'complete', remove=True) if new is not None: new.on_trait_change(self._update, 'complete') self._update() return def _on_closed_changed(self): """ Called when the wizard is closed. """ self.controller.dispose_pages() return #### EOF ###################################################################### pyface-6.1.2/pyface/wizard/api.py0000644000076500000240000000217313462774552017644 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ 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-6.1.2/pyface/wizard/wizard.py0000644000076500000240000000157313462774552020376 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The implementation of a wizard. """ # Import the toolkit specific version. from pyface.toolkit import toolkit_object Wizard = toolkit_object('wizard.wizard:Wizard') #### EOF ###################################################################### pyface-6.1.2/pyface/wizard/i_wizard_controller.py0000644000076500000240000000367513462774552023156 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The interface for all pyface wizard controllers. """ # Enthought library imports. from traits.api import Bool, Interface, Instance, List # Local imports. 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. """ #### EOF ###################################################################### pyface-6.1.2/pyface/resource_manager.py0000644000076500000240000000220213462774551021104 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The implementation of a shared resource manager. """ from __future__ import absolute_import # Enthought library imports. 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()) #### EOF ###################################################################### pyface-6.1.2/pyface/qt/0000755000076500000240000000000013515277235015635 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/qt/QtGui.py0000644000076500000240000000177613462774551017257 0ustar cwebsterstaff00000000000000from . import qt_api if qt_api == 'pyqt': from PyQt4.Qt import QKeySequence, QTextCursor from PyQt4.QtGui import * 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 == 'pyside2': 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 else: from PySide.QtGui import * pyface-6.1.2/pyface/qt/QtOpenGL.py0000644000076500000240000000035513462774551017647 0ustar cwebsterstaff00000000000000from . import qt_api if qt_api == 'pyqt': from PyQt4.QtOpenGL import * elif qt_api == 'pyqt5': from PyQt5.QtOpenGL import * elif qt_api == 'pyside2': from PySide2.QtOpenGL import * else: from PySide.QtOpenGL import * pyface-6.1.2/pyface/qt/QtCore.py0000644000076500000240000000200513462774551017405 0ustar cwebsterstaff00000000000000from . 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 == 'pyside2': from PySide2.QtCore import * from PySide2 import __version__, __version_info__ else: try: from PySide import __version__, __version_info__ except ImportError: pass from PySide.QtCore import * pyface-6.1.2/pyface/qt/QtTest.py0000644000076500000240000000034513462774551017441 0ustar cwebsterstaff00000000000000from . import qt_api if qt_api == 'pyqt': from PyQt4.QtTest import * elif qt_api == 'pyqt5': from PyQt5.QtTest import * elif qt_api == 'pyside2': from PySide2.QtTest import * else: from PySide.QtTest import * pyface-6.1.2/pyface/qt/QtWebKit.py0000644000076500000240000000215113502141171017661 0ustar cwebsterstaff00000000000000from . 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 == 'pyside2': 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 * else: from PySide.QtWebKit import * pyface-6.1.2/pyface/qt/QtScript.py0000644000076500000240000000055113502141171017742 0ustar cwebsterstaff00000000000000from . import qt_api if qt_api == 'pyqt': from PyQt4.QtScript import * elif qt_api == 'pyqt5': import warnings warnings.warn(DeprecationWarning("QtScript is not supported in PyQt5")) elif qt_api == 'pyside2': import warnings warnings.warn(DeprecationWarning("QtScript is not supported in PyQt5")) else: from PySide.QtScript import * pyface-6.1.2/pyface/qt/__init__.py0000644000076500000240000000601013464525303017736 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2010, Enthought Inc # All rights reserved. # # This software is provided without warranty under the terms of the BSD license. # # Author: Enthought Inc # Description: Qt API selector. Can be used to switch between pyQt and PySide #------------------------------------------------------------------------------ import importlib import os import sys QtAPIs = [ ('pyside', 'PySide'), ('pyside2', 'PySide2'), ('pyqt5', 'PyQt5'), ('pyqt', 'PyQt4'), ] def prepare_pyqt4(): """ Set PySide compatible APIs. """ # This is not needed for Python 3 and can be removed when we no longer # support Python 2. import sip try: sip.setapi('QDate', 2) sip.setapi('QDateTime', 2) sip.setapi('QString', 2) sip.setapi('QTextStream', 2) sip.setapi('QTime', 2) sip.setapi('QUrl', 2) sip.setapi('QVariant', 2) except ValueError as exc: if sys.version_info[0] <= 2: # most likely caused by something else setting the API version # before us: try to give a better error message to direct the user # how to fix. msg = exc.args[0] msg += (". Pyface expects PyQt API 2 under Python 2. " "Either import Pyface before any other Qt-using packages, " "or explicitly set the API before importing any other " "Qt-using packages.") raise ValueError(msg) else: # don't expect the above on Python 3, so just re-raise raise 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 if qt_api == 'pyqt': # set the PyQt4 APIs # this is a likely place for failure - pyface really wants to be # imported first, before eg. matplotlib prepare_pyqt4() break else: # does our environment give us a preferred API? qt_api = os.environ.get('QT_API') if qt_api == 'pyqt': # set the PyQt4 APIs prepare_pyqt4() # 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: if api_name == 'pyqt': # set the PyQt4 APIs prepare_pyqt4() importlib.import_module(module) importlib.import_module('.QtCore', module) qt_api = api_name break except ImportError: continue else: raise ImportError('Cannot import PySide, PySide2, PyQt5 or PyQt4') # otherwise check QT_API value is valid elif qt_api not in {api_name for api_name, module in QtAPIs}: msg = ("Invalid Qt API %r, valid values are: " + "'pyside, 'pyside2', 'pyqt' or 'pyqt5'") % qt_api raise RuntimeError(msg) # useful constants is_qt4 = (qt_api in {'pyqt', 'pyside'}) is_qt5 = (qt_api in {'pyqt5', 'pyside2'}) pyface-6.1.2/pyface/qt/QtSvg.py0000644000076500000240000000034113462774551017255 0ustar cwebsterstaff00000000000000from . import qt_api if qt_api == 'pyqt': from PyQt4.QtSvg import * elif qt_api == 'pyqt5': from PyQt5.QtSvg import * elif qt_api == 'pyside2': from PySide2.QtSvg import * else: from PySide.QtSvg import * pyface-6.1.2/pyface/qt/QtNetwork.py0000644000076500000240000000036113462774551020151 0ustar cwebsterstaff00000000000000from . import qt_api if qt_api == 'pyqt': from PyQt4.QtNetwork import * elif qt_api == 'pyqt5': from PyQt5.QtNetwork import * elif qt_api == 'pyside2': from PySide2.QtNetwork import * else: from PySide.QtNetwork import * pyface-6.1.2/pyface/__init__.py0000644000076500000240000000212413462774551017325 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005-2013, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Reusable MVC-based components for Traits-based applications. Part of the TraitsGUI project of the Enthought Tool Suite. """ try: from pyface._version import full_version as __version__ except ImportError: __version__ = 'not-built' __requires__ = ['traits'] __extras_require__ = { 'wx': ['wxpython>=2.8.10', 'numpy'], 'pyqt': ['pyqt>=4.10', 'pygments'], 'pyqt5': ['pyqt>=5', 'pygments'], 'pyside': ['pyside>=1.2', 'pygments'], } pyface-6.1.2/pyface/i_python_editor.py0000644000076500000240000000532213462774551020770 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A widget for editing Python code. """ # Enthought library imports. from traits.api import Bool, Event, Interface, Unicode # Local imports. from pyface.key_pressed_event import KeyPressedEvent class IPythonEditor(Interface): """ 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 = Unicode #: 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(object): """ 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-6.1.2/pyface/wx/0000755000076500000240000000000013515277240015643 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/wx/scrolled_message_dialog.py0000644000076500000240000000313513462774552023062 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ 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-6.1.2/pyface/wx/color.py0000644000076500000240000000201713462774552017344 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ 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. return tuple(enable_color) #### EOF ###################################################################### pyface-6.1.2/pyface/wx/util/0000755000076500000240000000000013515277240016620 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/wx/util/__init__.py0000644000076500000240000000000013462774552020730 0ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/wx/util/font_helper.py0000644000076500000240000000245513462774552021516 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Utility functions for working with wx Fonts. """ # Major package imports. 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) ### EOF ####################################################################### pyface-6.1.2/pyface/wx/dialog.py0000644000076500000240000000770613462774552017477 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Dialog utilities. """ # Major package imports. 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) return 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) return 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) return 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() return 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() return 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() return 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 #### EOF ###################################################################### pyface-6.1.2/pyface/wx/spreadsheet/0000755000076500000240000000000013515277240020152 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/wx/spreadsheet/abstract_grid_view.py0000644000076500000240000001774613462774552024416 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ from numpy import arange import wx from wx.grid import Grid, PyGridTableBase from wx.grid import PyGridCellRenderer from wx.grid import GridCellTextEditor, GridCellStringRenderer from wx.grid import GridCellFloatRenderer, GridCellFloatEditor from wx.lib.mixins.grid import GridAutoEditMixin class ComboboxFocusHandler(wx.EvtHandler): """ Workaround for combobox focus problems in wx 2.6.""" # This is copied from enthought/pyface/grid.combobox_focus_handler.py. # Since this was the only thing that pyface.wx needed from pyface, # and it's a temporary workaround for an outdated version of wx, we're just # copying it here instead of introducing a dependency on a large package. def __init__(self): wx.EvtHandler.__init__(self) wx.EVT_KILL_FOCUS(self, self._on_kill_focus) return def _on_kill_focus(self, evt): # this is pretty egregious. somehow the combobox gives up focus # as soon as it starts up, causing the grid to remove the editor. # so we just don't let it give up focus. i suspect this will cause # some other problem down the road, but it seems to work for now. # fixme: remove this h*ck once the bug is fixed in wx. editor = evt.GetEventObject() if isinstance(editor, wx._controls.ComboBox) and \ evt.GetWindow() is None: return evt.Skip() return 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 ... wx.EVT_IDLE(self, self.OnIdle) # Enthought specific display controls ... self.init_labels() self.init_data_types() self.init_handlers() wx.grid.EVT_GRID_EDITOR_CREATED(self, self._on_editor_created) return # needed to handle problem in wx 2.6 with combobox cell editors def _on_editor_created(self, evt): editor = evt.GetControl() editor.PushEventHandler(ComboboxFocusHandler()) evt.Skip() return 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) return 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()) return def init_handlers(self): wx.grid.EVT_GRID_CELL_LEFT_CLICK(self, self.OnCellLeftClick) wx.grid.EVT_GRID_CELL_RIGHT_CLICK(self, self.OnCellRightClick) wx.grid.EVT_GRID_CELL_LEFT_DCLICK(self, self.OnCellLeftDClick) wx.grid.EVT_GRID_CELL_RIGHT_DCLICK(self, self.OnCellRightDClick) wx.grid.EVT_GRID_LABEL_LEFT_CLICK(self, self.OnLabelLeftClick) wx.grid.EVT_GRID_LABEL_RIGHT_CLICK(self, self.OnLabelRightClick) wx.grid.EVT_GRID_LABEL_LEFT_DCLICK(self, self.OnLabelLeftDClick) wx.grid.EVT_GRID_LABEL_RIGHT_DCLICK(self, self.OnLabelRightDClick) wx.grid.EVT_GRID_ROW_SIZE(self, self.OnRowSize) wx.grid.EVT_GRID_COL_SIZE(self, self.OnColSize) wx.grid.EVT_GRID_RANGE_SELECT(self, self.OnRangeSelect) wx.grid.EVT_GRID_CELL_CHANGE(self, self.OnCellChange) wx.grid.EVT_GRID_SELECT_CELL(self, self.OnSelectCell) wx.grid.EVT_GRID_EDITOR_SHOWN(self, self.OnEditorShown) wx.grid.EVT_GRID_EDITOR_HIDDEN(self, self.OnEditorHidden) wx.grid.EVT_GRID_EDITOR_CREATED(self, self.OnEditorCreated) return 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) return 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) return 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 == True: if self.CanEnableCellControl(): self.EnableCellEditControl() self.edit = False if self.moveTo != 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-6.1.2/pyface/wx/spreadsheet/unit_renderer.py0000644000076500000240000001032513462774552023403 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ 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) return 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.SetClippingRect(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) return 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.SetClippingRect(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-6.1.2/pyface/wx/spreadsheet/font_renderer.py0000644000076500000240000000645613462774552023404 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ 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) return 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.SetClippingRect(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() return class FontRendererFactory88: """ 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-6.1.2/pyface/wx/spreadsheet/virtual_model.py0000644000076500000240000002204513462774552023406 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ from __future__ import print_function from wx.grid import Grid, PyGridTableBase, GridCellAttr, GridTableMessage, GridCellFloatRenderer 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 from wx.grid import GRID_VALUE_BOOL from wx import ALIGN_LEFT, ALIGN_CENTRE, Colour from .default_renderer import DefaultRenderer 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 """ ##print 'Initializing virtual model' 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 """ ##print 'VirtualModel.reset_view' 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-6.1.2/pyface/wx/spreadsheet/__init__.py0000644000076500000240000000117613462774552022301 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ pyface-6.1.2/pyface/wx/spreadsheet/default_renderer.py0000644000076500000240000001064413462774552024054 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ #------------------------------------------------------------------------------- # #------------------------------------------------------------------------------- from __future__ import print_function import types from string import atof import wx from wx.grid import PyGridCellRenderer import six #------------------------------------------------------------------------------- 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.DEFAULT, wx.NORMAL, wx.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() return 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.SetClippingRect(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) return 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); return 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, six.string_types): 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) return 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-6.1.2/pyface/wx/progress_meter.py0000644000076500000240000000174413462774552021274 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ 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.FindWindowById(wx.ID_CANCEL) button.SetLabel(title) return pyface-6.1.2/pyface/wx/spacer.py0000644000076500000240000000304413462774552017504 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ 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). """ # Major package imports. 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 #### EOF ###################################################################### pyface-6.1.2/pyface/wx/divider.py0000644000076500000240000000261513462774552017660 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A thin visual divider. """ # Major package imports. 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 #### EOF ###################################################################### pyface-6.1.2/pyface/wx/shell.py0000644000076500000240000015210713462774552017343 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """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.""" from __future__ import print_function __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] from wx.wx import * from wx.stc import * import keyword import os import sys from wx.py.pseudo import PseudoFileIn, PseudoFileOut, PseudoFileErr from wx.py.version import VERSION # local imports from .drag_and_drop import PythonObject from .drag_and_drop import clipboard as enClipboard sys.ps3 = '<-- ' # Input prompt. NAVKEYS = (WXK_END, WXK_LEFT, WXK_RIGHT, WXK_UP, WXK_DOWN, WXK_PRIOR, WXK_NEXT) 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: """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(wxStyledTextCtrl): """PyCrust Shell based on wxStyledTextCtrl.""" name = 'PyCrust Shell' revision = __revision__ def __init__(self, parent, id=-1, pos=wxDefaultPosition, \ size=wxDefaultSize, style=wxCLIP_CHILDREN, introText='', \ locals=None, InterpClass=None, *args, **kwds): """Create a PyCrust Shell instance.""" wxStyledTextCtrl.__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 == None: from PyCrust.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. EVT_KEY_DOWN(self, self.OnKeyDown) EVT_CHAR(self, self.OnChar) # Assign handlers for wxSTC events. EVT_STC_UPDATEUI(self, id, self.OnUpdateUI) EVT_STC_USERLISTSELECTION(self, id, 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, wxSTC_MARGIN_NUMBER) self.SetMarginWidth(1, 40) self.SetLexer(wxSTC_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 six.moves.builtins six.moves.builtins.close = six.moves.builtins.exit = six.moves.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;execfile(%s)' % \ ('startupText', '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 == wxSTC_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() endpos = self.GetTextLength() 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: wxYield() 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 = wxTextEntryDialog(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 \ (wxStyledTextCtrl.CanPaste(self) or \ wxTheClipboard.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 wxTheClipboard.Open(): wxTheClipboard.SetData(data) wxTheClipboard.Close() def CopyWithPrompts(self): """Copy selection, including prompts, and place it on the clipboard.""" if self.CanCopy(): command = self.GetSelectedText() data = wxTextDataObject(command) if wxTheClipboard.Open(): wxTheClipboard.SetData(data) wxTheClipboard.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 wxTheClipboard.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 wxTheClipboard.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: wxTheClipboard.Close() return def PasteAndRun(self): """Replace selection with clipboard contents, run commands.""" if wxTheClipboard.Open(): if wxTheClipboard.IsSupported(wxDataFormat(wxDF_TEXT)): data = wxTextDataObject() if wxTheClipboard.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() wxTheClipboard.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 = wxNewId() ID_AUTOCOMP = wxNewId() ID_AUTOCOMP_SHOW = wxNewId() ID_AUTOCOMP_INCLUDE_MAGIC = wxNewId() ID_AUTOCOMP_INCLUDE_SINGLE = wxNewId() ID_AUTOCOMP_INCLUDE_DOUBLE = wxNewId() ID_CALLTIPS = wxNewId() ID_CALLTIPS_SHOW = wxNewId() ID_FILLING = wxNewId() ID_FILLING_AUTO_UPDATE = wxNewId() ID_FILLING_SHOW_METHODS = wxNewId() ID_FILLING_SHOW_CLASS = wxNewId() ID_FILLING_SHOW_DICT = wxNewId() ID_FILLING_SHOW_DOC = wxNewId() ID_FILLING_SHOW_MODULE = wxNewId() class ShellMenu: """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) EVT_MENU(self, wxID_EXIT, self.OnExit) EVT_MENU(self, wxID_UNDO, self.OnUndo) EVT_MENU(self, wxID_REDO, self.OnRedo) EVT_MENU(self, wxID_CUT, self.OnCut) EVT_MENU(self, wxID_COPY, self.OnCopy) EVT_MENU(self, wxID_PASTE, self.OnPaste) EVT_MENU(self, wxID_CLEAR, self.OnClear) EVT_MENU(self, wxID_SELECTALL, self.OnSelectAll) EVT_MENU(self, wxID_ABOUT, self.OnAbout) EVT_MENU(self, ID_AUTOCOMP_SHOW, \ self.OnAutoCompleteShow) EVT_MENU(self, ID_AUTOCOMP_INCLUDE_MAGIC, \ self.OnAutoCompleteIncludeMagic) EVT_MENU(self, ID_AUTOCOMP_INCLUDE_SINGLE, \ self.OnAutoCompleteIncludeSingle) EVT_MENU(self, ID_AUTOCOMP_INCLUDE_DOUBLE, \ self.OnAutoCompleteIncludeDouble) EVT_MENU(self, ID_CALLTIPS_SHOW, \ self.OnCallTipsShow) EVT_UPDATE_UI(self, wxID_UNDO, self.OnUpdateMenu) EVT_UPDATE_UI(self, wxID_REDO, self.OnUpdateMenu) EVT_UPDATE_UI(self, wxID_CUT, self.OnUpdateMenu) EVT_UPDATE_UI(self, wxID_COPY, self.OnUpdateMenu) EVT_UPDATE_UI(self, wxID_PASTE, self.OnUpdateMenu) EVT_UPDATE_UI(self, wxID_CLEAR, self.OnUpdateMenu) EVT_UPDATE_UI(self, ID_AUTOCOMP_SHOW, self.OnUpdateMenu) EVT_UPDATE_UI(self, ID_AUTOCOMP_INCLUDE_MAGIC, self.OnUpdateMenu) EVT_UPDATE_UI(self, ID_AUTOCOMP_INCLUDE_SINGLE, self.OnUpdateMenu) EVT_UPDATE_UI(self, ID_AUTOCOMP_INCLUDE_DOUBLE, self.OnUpdateMenu) EVT_UPDATE_UI(self, ID_CALLTIPS_SHOW, self.OnUpdateMenu) if hasattr( self, 'crust' ): EVT_MENU(self, ID_FILLING_AUTO_UPDATE, self.OnFillingAutoUpdate) EVT_MENU(self, ID_FILLING_SHOW_METHODS, self.OnFillingShowMethods) EVT_MENU(self, ID_FILLING_SHOW_CLASS, self.OnFillingShowClass) EVT_MENU(self, ID_FILLING_SHOW_DICT, self.OnFillingShowDict) EVT_MENU(self, ID_FILLING_SHOW_DOC, self.OnFillingShowDoc) EVT_MENU(self, ID_FILLING_SHOW_MODULE, self.OnFillingShowModule) EVT_UPDATE_UI(self, ID_FILLING_AUTO_UPDATE, self.OnUpdateMenu) EVT_UPDATE_UI(self, ID_FILLING_SHOW_METHODS, self.OnUpdateMenu) EVT_UPDATE_UI(self, ID_FILLING_SHOW_CLASS, self.OnUpdateMenu) EVT_UPDATE_UI(self, ID_FILLING_SHOW_DICT, self.OnUpdateMenu) EVT_UPDATE_UI(self, ID_FILLING_SHOW_DOC, self.OnUpdateMenu) EVT_UPDATE_UI(self, ID_FILLING_SHOW_MODULE, self.OnUpdateMenu) 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-6.1.2/pyface/wx/__init__.py0000644000076500000240000000117713462774552017773 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ pyface-6.1.2/pyface/wx/font.py0000644000076500000240000000444113462774552017177 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Font utilities. """ # Major package imports. 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) return 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) return def decrease_font_size(window, delta=2): """ Recursively decreases the font size starting from 'window'. """ increase_font_size(window, delta=-2) return def set_bold_font(window): """ Set 'window's font to be bold. """ font = window.GetFont() font.SetWeight(wx.BOLD) window.SetFont(font) return #### EOF ###################################################################### pyface-6.1.2/pyface/wx/switcher.py0000644000076500000240000002070013462774552020055 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ 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 # Major package imports. import wx from wx.lib.scrolledpanel import ScrolledPanel as wxScrolledPanel # Enthought library imports. 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.on_trait_change(self._on_selected_changed, 'selected') return ########################################################################### # Trait event handlers. ########################################################################### def _on_selected_changed(self, selected): """ Called when the selected item in the model is changed. """ 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) ##self.SetBackgroundColour("light grey") # 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) return 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. wx.EVT_COMBOBOX(self, combo.GetId(), 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.on_trait_change(self._on_selected_changed, 'selected') return ########################################################################### # Trait event handlers. ########################################################################### def _on_selected_changed(self, selected): """ Called when the selected item in the model is changed. """ 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) return 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() return 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 #### EOF ###################################################################### pyface-6.1.2/pyface/wx/python_stc.py0000644000076500000240000003522213462774552020424 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ 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") stc.EVT_STC_UPDATEUI(self, ID, self.OnUpdateUI) stc.EVT_STC_MARGINCLICK(self, ID, 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") wx.EVT_KEY_DOWN(self, self.OnKeyPressed) def OnKeyPressed(self, event): if self.CallTipActive(): self.CallTipCancel() # KeyCode used to be a method. Now it is an integer. # Handle either case. if type(event.KeyCode) is int: # wx2.8+ key = event.KeyCode else: # wx2.6 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. """ if __name__ == '__main__': # fixme: This has been re-factored into not working. No run module. import sys,os import run run.main(['', os.path.basename(sys.argv[0])]) #---------------------------------------------------------------------- pyface-6.1.2/pyface/wx/sized_panel.py0000644000076500000240000000333013462774552020522 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A panel sized by a sizer. """ # Major package imports. 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) return def Layout(self): """ Lays out the sizer without changing the panel geometry. """ self.sizer.Layout() return #### EOF ###################################################################### pyface-6.1.2/pyface/wx/pager.py0000644000076500000240000000640313462774552017327 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A pager contains a set of pages, but only shows one at a time. """ # Major package imports. 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) return def _hide_page(self, page): """ Hides the specified page. """ page.Show(False) self._sizer.Remove(page) return 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 #### EOF ###################################################################### pyface-6.1.2/pyface/wx/lazy_switcher.py0000644000076500000240000002154413462774552021123 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Classes to provide a switcher. """ # Major package imports. import wx from wx.lib.scrolledpanel import ScrolledPanel as wxScrolledPanel # Enthought library imports. 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.on_trait_change(self._on_selected_changed, 'selected') return ########################################################################### # Trait event handlers. ########################################################################### def _on_selected_changed(self, selected): """ Called when the selected item in the model is changed. """ 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) ##self.SetBackgroundColour("light grey") # 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) return 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. wx.EVT_COMBOBOX(self, combo.GetId(), 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.on_trait_change(self._on_selected_changed, 'selected') return ########################################################################### # 'SwitcherPanel' interface. ########################################################################### def show_page(self, index): """ Shows the page at the specified index. """ self._show_page(index) return ########################################################################### # Trait event handlers. ########################################################################### def _on_selected_changed(self, selected): """ Called when the selected item in the model is changed. """ 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) ##self.SetBackgroundColour('red') #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) return 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: current_size = self.current.GetSize() 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 #if self.current is not None: # page.SetSize(current_size) # 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() #self.sizer.Fit(self) #self.SetupScrolling() return 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 #### EOF ###################################################################### pyface-6.1.2/pyface/wx/aui.py0000644000076500000240000002514313462774552017011 0ustar cwebsterstaff00000000000000# Standard library imports. import logging import os # Major package imports. import wx # Logger. logger = logging.getLogger(__name__) # 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 # 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) caption_size = self._art.GetMetric(aui.AUI_DOCKART_CAPTION_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.RectPS(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-6.1.2/pyface/wx/image_list.py0000644000076500000240000000724013462774552020346 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A cached image list. """ # Major package imports. import wx import six # 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, six.string_types): # 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.EmptyBitmap(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 #### EOF ###################################################################### pyface-6.1.2/pyface/wx/clipboard.py0000644000076500000240000000136113462774552020166 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2009, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # #------------------------------------------------------------------------------ import logging logger = logging.getLogger() logger.warning('DEPRECATED: pyface.wx.clipboard, use pyface.api instead.') from pyface.ui.wx.clipboard import Clipboard clipboard = Clipboard() pyface-6.1.2/pyface/wx/drag_and_drop.py0000644000076500000240000002552413462774552021021 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Drag and drop utilities. """ # Standard library imports. import six if six.PY2: from inspect import getargspec else: # avoid deprecation warning from inspect import getfullargspec as getargspec # Major package imports. import wx import six class Clipboard: """ 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, six.text_type): files = [files] for file in files: data_object.AddFile(file) # Create the drop source and begin the drag and drop operation: super(FileDropSource, self).__init__(source) self.SetData(data_object) self.result = self.DoDragDrop(True) def on_dropped(self, drag_result): """ Called when the data has been dropped. """ return class FileDropTarget(wx.FileDropTarget): """ Drop target for files. """ def __init__(self, handler): """ Constructor. """ # Base-class constructor. wx.FileDropTarget.__init__(self) self.handler = handler return 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.CustomDataFormat('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('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) return 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 = getargspec(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 = getargspec(self.handler)[0] if len(args)==2: self.handler(clipboard.data, drag_result) else: self.handler() return class PythonDropTarget(wx.PyDropTarget): """ 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(PythonDropTarget, self).__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) return 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) return def OnDrop(self, x, y): """ Called when the user drops a data object on the target. Return 'False' to veto the operation. """ return True #### EOF ##################################################################### pyface-6.1.2/pyface/wx/image.py0000644000076500000240000000172513462774552017315 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ import os, 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-6.1.2/pyface/wx/image_cache.py0000644000076500000240000000461513462774552020441 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ An image cache. """ # Major package imports. import wx class ImageCache: """ 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 #### EOF ###################################################################### pyface-6.1.2/pyface/wx/grid/0000755000076500000240000000000013515277240016570 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/wx/grid/grid.py0000644000076500000240000002636313462774552020112 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A grid (spreadsheet) widget. """ # Major package imports. from __future__ import print_function import wx from wx.grid import Grid as wxGrid # Local imports. from .grid_model import GridModel 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 # Automatically size columns and rows to fit their content. # # fixme: wx seems sensitive to the location of these two lines. Put # them *before* the call to 'SetTable', otherwise the grid takes # forever to initialize! ##self.AutoSizeColumns() ##self.AutoSizeRows() # 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.on_trait_change(self._on_model_changed, 'model_changed') wx.grid.EVT_GRID_CELL_CHANGE(self, self._on_cell_change) wx.grid.EVT_GRID_SELECT_CELL(self, self._on_select_cell) # This starts the cell editor on a double-click as well as on a second # click. wx.grid.EVT_GRID_CELL_LEFT_DCLICK(self, 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. wx.EVT_KEY_DOWN(self, self._on_key_down) # Initialize the row and column models. self._initialize_rows(model) self._initialize_columns(model) self._initialize_fonts() return 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) return 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) return 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. """ row = evt.GetRow() col = evt.GetCol() ##print 'Cell changed at', row, col value = self.GetTable().GetValue(row, col) ##print 'New value', value ##print 'Type', type(value) evt.Skip() return def _on_select_cell(self, evt): """ Called when the user has moved to another cell. """ ##row = evt.GetRow() ##col = evt.GetCol() ##print 'Cell selected at', row, col evt.Skip() return 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() return 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") wx.EVT_MENU(self, 101, self._on_delete_row) self.PopupMenu(menu, evt.GetPosition()) return 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() return 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, message): """ Called when the model has changed. """ 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() return 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 #### EOF ###################################################################### pyface-6.1.2/pyface/wx/grid/__init__.py0000644000076500000240000000120013462774552020703 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ pyface-6.1.2/pyface/wx/grid/api.py0000644000076500000240000000144313462774552017726 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ from __future__ import absolute_import from .grid import Grid from .grid_column import GridColumn from .grid_model import GridModel from .grid_row import GridRow pyface-6.1.2/pyface/wx/grid/grid_model.py0000644000076500000240000002035513462774552021265 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A model that provides data for a grid. """ # Major package imports. from __future__ import print_function import wx from wx.grid import PyGridTableBase, GridTableMessage, GRIDTABLE_NOTIFY_ROWS_APPENDED # Enthought library imports. from traits.api import Any, Bool, HasTraits, Trait, Event, List # Local imports. 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 ########################################################################### # 'wxPyGridTableBase' 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. """ label = self.GetColLabelValue(col) 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 return 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(PyGridTableBase): """ A model that provides data for a grid. """ ########################################################################### # 'object' interface. ########################################################################### def __init__(self, model): """ Creates a new table base. """ # Base class constructor. PyGridTableBase.__init__(self) # The Pyface model that provides the data. self.model = model return ########################################################################### # 'wxPyGridTableBase' 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) #### EOF ###################################################################### pyface-6.1.2/pyface/wx/grid/grid_column.py0000644000076500000240000000207113462774552021455 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A description of a column in a grid. """ # Enthought library imports. 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) #### EOF ###################################################################### pyface-6.1.2/pyface/wx/grid/grid_row.py0000644000076500000240000000200213462774552020761 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A description of a row in a grid. """ # Enthought library imports. 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 #### EOF ###################################################################### pyface-6.1.2/pyface/splash_screen.py0000644000076500000240000000166113462774551020424 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The implementation of a splash screen. """ # Import the toolkit specific version. from __future__ import absolute_import from .toolkit import toolkit_object SplashScreen = toolkit_object('splash_screen:SplashScreen') #### EOF ###################################################################### pyface-6.1.2/pyface/i_single_choice_dialog.py0000644000076500000240000000415013462774551022211 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2016, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The interface for a dialog that prompts for a choice from a list. """ # Enthought library imports. from traits.api import Any, List, Str # Local imports. 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(object): """ 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-6.1.2/pyface/expandable_header.py0000644000076500000240000000120113462774551021174 0ustar cwebsterstaff00000000000000# Copyright (c) 2017, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! import logging logger = logging.getLogger(__name__) logger.warning( 'DEPRECATED: pyface.expandable_header, use pyface.ui.wx.expandable_header instead. ' 'Will be removed in Pyface 7.') from pyface.ui.wx.expandable_header import * pyface-6.1.2/pyface/image/0000755000076500000240000000000013515277235016273 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/image/__init__.py0000644000076500000240000000010113462774551020400 0ustar cwebsterstaff00000000000000# Copyright (c) 2007 by Enthought, Inc. # All rights reserved. pyface-6.1.2/pyface/image/library/0000755000076500000240000000000013515277235017737 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/image/library/icons.zip0000644000076500000240000004736213462774551021616 0ustar cwebsterstaff00000000000000PK(hHw 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=|7kJuPK(hHz\;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`PK(hH7* ! 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]ZPK(hH$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βPK(hHjP{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`PK(hH~Ƿ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`PK(hHސ~$ 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?PK(hHkF  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`PK(hH- 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:&PK(hHobject_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/PK(hHkf 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`PK(hH 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`PK(hH{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?PK(hH{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`PK(hHtuple_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?iPK(hHVHimage_volume.pyP0  BP$2XoAY[k(ɁBNg=r*7`pM\Y!(>k(•w` aˡ: Е+p>_~ "t"טz]V~vہT<,tM1hDtyb*z4< þޒ$'%t%+/ZKabPK(hHsH  ! image_info.pyA@›[ȖmPJ/e">uq̌n6< ePH}:lUgݫv~@i64Gᨋ5\*$>|OI{gɪL9aռq1cMWhW6WNAͳ Т9ZAmFס˿<-@] S[קFo*-ѭd54j4!EXrPC&nMlθw'uӑ_(cl(7`O' (2nڶHwF_!_*\\''l (K6Lfwd60P0)h"\hj5ԙL >J넒P:c0R8 E 1\'_=/r\& &tP2|d*qFDHzU2@Rm,R8 g˵~z3RAsKsL"60PK(hHMNZh license.txtsI-N.,(ϳR|TBqAjrfZfjs~AeQfzF \C2LD!3/-(7COfrj^q*\GScAPIbRNgnbzj1HcNPK(hHw array_node.pngPK(hH.J2B 7bool_node.pngPK(hH lgbottom_left_origin.pngPK(hHmc^4bottom_right_origin.pngPK(hH class_node.pngPK(hH1! complex_node.pngPK(hHztraits_node.pngPK(hH9Atuple_node.pngPK(hHVHmDimage_volume.pyPK(hHsH  ! iEimage_info.pyPK(hHMNZh Glicense.txtPK1Hpyface-6.1.2/pyface/image/library/image_LICENSE.txt0000644000076500000240000000112213462774551022724 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-6.1.2/pyface/image/library/std.zip0000644000076500000240000016326313462774551021274 0ustar cwebsterstaff00000000000000PK*hHץ 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`PK*hHBE5.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%4PK*hHzqABG5.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&2xsJhPK*hH|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?X2xsJhPK*hHyBH5.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&FPK*hH ې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`PK*hH43 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ښzAPK*hH"ś)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`PK*hHPAhorizontal_drag.png sb``p @$;8 <"bB xn`cHŜ6r:($~WG愨sb$_JϬ?N5Sq5v>֦53sZ2lWԔxv fw\f(i>0xsJhPK*hHm˾horizontal_splitter.png sb``p 3 H6R@1!ftP01b+y8X?W1AwƍÑ ښRaou}Ye۟c⒠`5Y/N?fwN~py+rMgtsYPK*hHT~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`PK*hH3|{JJ07.png sb``p I $W/eR B xnOǐ9o/ 2(P~(*Ľ^@ROҡxxas}p`tsYPK*hHKIJ08.png sb``p ) $ M R B xnoOǐ9o/*600ފ a9Yަ6-އ7n2SRNA!e7 CZCVQG]F``tsYPK*hHfEoTJ0A.png sb``p ) $ M R B xnF6Oǐ9o/60=~c*K`?//n~[7ܶE x\v~1=Z"A0<]\9%4PK*hHԲ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`PK*hH5 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`PK*hHldWRLT.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`PK*hHQר'"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`PK*hHT"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 ~.PK*hHqgLtab_active.png sb``p  @,$R B xn`cHŜ y829qAK9\nž5.jҳ03|ggX^%$7j>|ޝIg0)3T05K?;밡wQ/% ~.PK*hH5_tab_background.png sb``p m@,$R B xn`cHŜ 68z?Å&~Tv׿  1?ja?!,'nW0t ;ןy/FܥJ[].) PK*hH`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ܮ*`rbl]0vIK]LB%ʐxI3VHL 1z0x4P|f+!ѺH~":85xy(+feͳ0}lqp|r21nvZ_ zTRYP#\'{SB)-ye8thЧ9'96ϿB=CsH}<8NJ"y,C'*Α9r,75 ={|<'YFPA<‚Č7$)89h[o=2=$4^ fŗ ^MsOoRAR~}Ng$3pBtǝ&-)f =P=@{{Nz VߖrH% >}\5rn]QX/*19Cz]o/#-ʛ.cEkCp0r/Gn9o$q+2mv 0tcCv GY&`]RmKw8T =;èї#/KzH  VㄆHn,d1%6F@jeѰdw~dq.w<%V;X3M29gtvul'!H6tf:pB89k: ېl{CJjz~g w)e~ ?d:8,ͩWTuf|X Vn.͗X.!Sm(kJN 3V(a8{gkRrNv=gdmHZI(J$ګ&U7Q{NCàz3Rm^4ޝ: g#A_+\u)'l)GhsM8螵#]xKjf.tM^ér&hTaYdJk jTeI%[署 UZ/PK*hH}< 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( depends_on = '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 data 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, depends_on = 'volumes.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-6.1.2/pyface/i_gui.py0000644000076500000240000001375413462774551016675 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The interface of a pyface GUI. """ # Standard library imports. import logging import os # Enthought library imports. from traits.etsconfig.api import ETSConfig from traits.api import Bool, Interface, Unicode # 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 = Unicode ########################################################################### # '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(object): """ 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-6.1.2/pyface/application.py0000644000076500000240000002511613462774551020077 0ustar cwebsterstaff00000000000000# Copyright (c) 2014-2018 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! """ 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:method:`Application._run` method, but usually the :py:method:`Application.start` and :py:method:`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(): print("Hello world") app = Application() app.on_trait_change(do_work, 'application_initialized') """ from __future__ import ( absolute_import, division, print_function, unicode_literals ) import logging import os from traits.api import ( Directory, Event, HasStrictTraits, Instance, ReadOnly, Unicode, 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 = Unicode('Pyface Application') #: Human-readable company name company = Unicode #: Human-readable description of the application description = Unicode # Infrastructure --------------------------------------------------------- #: The application's globally unique identifier. id = Unicode #: 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.etsconfig import ETSConfig return ETSConfig._get_application_dirname() def _home_default(self): """ Default home comes from ETSConfig. """ from traits.etsconfig.etsconfig 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.etsconfig import ETSConfig return ETSConfig.user_data def _company_default(self): """ Default company comes from ETSConfig. """ from traits.etsconfig.etsconfig 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-6.1.2/pyface/preference/0000755000076500000240000000000013515277235017327 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/preference/preference_node.py0000644000076500000240000000463213462774551023035 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ 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). """ # Enthought library imports. 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 #### EOF ###################################################################### pyface-6.1.2/pyface/preference/__init__.py0000644000076500000240000000120113462774551021436 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ pyface-6.1.2/pyface/preference/api.py0000644000076500000240000000146613462774551020465 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005-2015, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ from __future__ import absolute_import from .preference_page import PreferencePage from .preference_dialog import PreferenceDialog from .preference_node import PreferenceNode pyface-6.1.2/pyface/preference/preference_dialog.py0000644000076500000240000000116713462774551023347 0ustar cwebsterstaff00000000000000# Copyright (c) 2017, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! """ The preference dialog. """ # Import the toolkit specific version. from __future__ import absolute_import from pyface.toolkit import toolkit_object TreeViewer = toolkit_object('preference.preference_dialog:PreferenceDialog') pyface-6.1.2/pyface/preference/preference_page.py0000644000076500000240000000301113462774551023012 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Abstract base class for all preference pages. """ # Enthought library imports. 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 #### EOF ###################################################################### pyface-6.1.2/pyface/i_file_dialog.py0000644000076500000240000000737413462774551020350 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The interface for a dialog that allows the user to open/save files etc. """ # Standard library imports. import sys # Enthought library imports. from traits.api import Enum, Unicode, Int # Local imports. from pyface.i_dialog import IDialog import six 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. action = Enum('open', 'save as') #: The default directory. default_directory = Unicode #: The default filename. default_filename = Unicode #: 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 = Unicode #: The directory containing the chosen file. directory = Unicode #: The name of the chosen file. filename = Unicode #: The path (directory and filename) of the chosen file. path = Unicode #: The wildcard used to restrict the set of files. wildcard = Unicode #: The index of the selected wildcard. wildcard_index = Int(0) class MFileDialog(object): """ 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, six.string_types): pattern = extension else: pattern = ';'.join(extension) return "%s (%s)|%s|" % (description, pattern, pattern) pyface-6.1.2/pyface/i_confirmation_dialog.py0000644000076500000240000000406413462774551022112 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The interface for a dialog that prompts the user for confirmation. """ # Enthought library imports. from traits.api import Bool, Enum, Instance, Unicode # Local imports. from pyface.constant import CANCEL, NO, YES from pyface.i_dialog import IDialog from pyface.i_image_resource import IImageResource 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 = Instance(IImageResource) #: The message displayed in the body of the dialog (use the inherited #: 'title' trait to set the title of the dialog itself). message = Unicode #: Some informative text to display below the main message informative = Unicode #: Some additional details that can be exposed by the user detail = Unicode #: The label for the 'no' button. The default is toolkit specific. no_label = Unicode #: The label for the 'yes' button. The default is toolkit specific. yes_label = Unicode class MConfirmationDialog(object): """ The mixin class that contains common code for toolkit specific implementations of the IConfirmationDialog interface. """ pyface-6.1.2/pyface/api.py0000644000076500000240000000540713462774551016346 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ from __future__ import absolute_import 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 .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 .message_dialog import error, information, warning, MessageDialog from .progress_dialog import ProgressDialog from .python_editor import PythonEditor 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 .layered_panel import LayeredPanel 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-6.1.2/pyface/application_window.py0000644000076500000240000000171613462774551021466 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The implementation of a top-level application window. """ from __future__ import absolute_import # Import the toolkit specific version. from .toolkit import toolkit_object ApplicationWindow = toolkit_object('application_window:ApplicationWindow') #### EOF ###################################################################### pyface-6.1.2/pyface/sorter.py0000644000076500000240000000673513474227520017110 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Base class for all sorters. """ # Enthought library imports. 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 comparator(node_a, node_b): """ Comparator. """ return self.compare(widget, parent, node_a, node_b) nodes.sort(comparator) return nodes def compare(self, widget, parent, node_a, node_b): """ Returns the result of comparing two nodes. 'widget' is the widget that we are sorting nodes for. 'parent' is the parent node. 'node_a' is the the first node to compare. 'node_b' is the the second node to compare. """ # Get the category for each node. category_a = self.category(widget, parent, node_a) category_b = self.category(widget, parent, node_b) # If they are not in the same category then return the result of # comparing the categories. if category_a != category_b: result = cmp(category_a, category_b) else: label_a = widget.model.get_text(node_a) label_b = widget.model.get_text(node_b) # Compare the label text. result = cmp(label_a, label_b) return result 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 #### EOF ###################################################################### pyface-6.1.2/pyface/progress_dialog.py0000644000076500000240000000172213462774551020754 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A simple progress dialog window which allows itself to be updated """ # Import the toolkit specific version. from __future__ import absolute_import from .toolkit import toolkit_object ProgressDialog = toolkit_object('progress_dialog:ProgressDialog') #### EOF ###################################################################### pyface-6.1.2/pyface/layered_panel.py0000644000076500000240000000117113462774551020373 0ustar cwebsterstaff00000000000000# Copyright (c) 2017, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # Note: The MultiToolbarWindow is currently wx-specific # Import the toolkit specific version. from __future__ import absolute_import from .toolkit import toolkit_object LayeredPanel = toolkit_object('layered_panel:LayeredPanel') pyface-6.1.2/pyface/action/0000755000076500000240000000000013515277235016466 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/action/tool_bar_manager.py0000644000076500000240000000166013462774551022342 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A tool bar manager realizes itself in a tool bar control. """ # Import the toolkit specific version. from pyface.toolkit import toolkit_object ToolBarManager = toolkit_object('action.tool_bar_manager:ToolBarManager') ### EOF ####################################################################### pyface-6.1.2/pyface/action/action_event.py0000644000076500000240000000164613462774551021531 0ustar cwebsterstaff00000000000000""" The event passed to an action's 'perform' method. """ # Standard library imports. import time # Enthought library imports. from traits.api import Float, HasTraits, Int 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 becoames a public attribute of the event. """ # Base-class constructor. super(ActionEvent, self).__init__(**traits) # When the action was performed. self.when = time.time() pyface-6.1.2/pyface/action/menu_manager.py0000644000076500000240000000163613462774551021510 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A menu manager realizes itself in a menu control. """ # Import the toolkit specific version. from pyface.toolkit import toolkit_object MenuManager = toolkit_object('action.menu_manager:MenuManager') ### EOF ####################################################################### pyface-6.1.2/pyface/action/field_action.py0000644000076500000240000000440513462774551021467 0ustar cwebsterstaff00000000000000# Copyright (c) 2019, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! 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_peform` 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.on_trait_change(self.value_updated, 'value') field.control._field = field return field.control def value_updated(self, value): """ Handle changes to the field value by calling perform. The event passed to `perform` has the `value` as an attribute. """ 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-6.1.2/pyface/action/tool_palette_manager.py0000644000076500000240000000170013462774551023227 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A tool bar manager realizes itself in a tool palette control. """ # Import the toolkit specific version. from pyface.toolkit import toolkit_object ToolPaletteManager = toolkit_object('action.tool_palette_manager:ToolPaletteManager') ### EOF ####################################################################### pyface-6.1.2/pyface/action/window_action.py0000644000076500000240000000316113462774551021711 0ustar cwebsterstaff00000000000000# Copyright (c) 2005-2017, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: """ Abstract base class for all window actions. """ # Enthought library imports. from pyface.window import Window from traits.api import Instance, Property # Local imports. from pyface.action.listening_action import ListeningAction class WindowAction(ListeningAction): """ Abstract base class for window actions. """ # 'ListeningAction' interface -------------------------------------------- object = Property(depends_on='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(WindowAction, self).destroy() class CloseWindowAction(WindowAction): """ Close the specified window """ name = u'Close' accelerator = 'Ctrl+W' method = 'close'pyface-6.1.2/pyface/action/gui_application_action.py0000644000076500000240000000627113462774551023556 0ustar cwebsterstaff00000000000000# Copyright (c) 2005-2018, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: """ Abstract base class for all application actions. """ import platform # Enthought library imports. from traits.api import Instance, Property, cached_property # Local imports. 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(depends_on='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(GUIApplicationAction, self).destroy() class ActiveWindowAction(GUIApplicationAction): """ Abstract base class for application active window actions. """ # 'ListeningAction' interface -------------------------------------------- object = Property(depends_on='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 = u'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 (u'Exit ' if IS_WINDOWS else u'Quit ') + self.application.name class AboutAction(GUIApplicationAction): """ A standard 'About' dialog menu action. """ method = 'do_about' def _name_default(self): return u"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 = u'Close Window' accelerator = 'Ctrl+W' method = 'close' pyface-6.1.2/pyface/action/images/0000755000076500000240000000000013515277235017733 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/action/images/action.png0000644000076500000240000000116313462774551021723 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-6.1.2/pyface/action/images/image_LICENSE.txt0000644000076500000240000000115013462774551022721 0ustar cwebsterstaff00000000000000The icons are mostly derived work from other icons. As such they are licensed accordingly to the original license: Project License File ---------------------------------------------------------------------------- Eclipse Eclipse Public License image_LICENSE_Eclipse.txt Unless stated in this file, icons are the work of Enthought, and are released under a 3 clause BSD license. Files and orginal authors: ---------------------------------------------------------------------------- enthought/pyface/action/images: action.png | Eclipse pyface-6.1.2/pyface/action/listening_action.py0000644000076500000240000001200513462774551022373 0ustar cwebsterstaff00000000000000# Copyright (c) 2005-2018, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: # Standard library imports. import logging # Enthought library imports. from pyface.action.action import Action from traits.api import Any, Str # 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: self.object.on_trait_change( self._enabled_update, self.enabled_name, remove=True ) self.object.on_trait_change( 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(ListeningAction, self).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 -------------------------------------------------- def _enabled_name_changed(self, old, new): obj = self.object if obj is not None: if old: obj.on_trait_change(self._enabled_update, old, remove=True) if new: obj.on_trait_change(self._enabled_update, new) self._enabled_update() def _visible_name_changed(self, old, new): obj = self.object if obj is not None: if old: obj.on_trait_change(self._visible_update, old, remove=True) if new: obj.on_trait_change(self._visible_update, new) self._visible_update() def _object_changed(self, old, new): for kind in ('enabled', 'visible'): method = getattr(self, '_%s_update' % kind) name = getattr(self, '%s_name' % kind) if name: if old: old.on_trait_change(method, name, remove=True) if new: new.on_trait_change(method, name) method() def _enabled_update(self): 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): 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-6.1.2/pyface/action/tests/0000755000076500000240000000000013515277235017630 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/action/tests/test_group.py0000644000076500000240000001524113500710640022362 0ustar cwebsterstaff00000000000000from __future__ import absolute_import import unittest from traits.testing.unittest_tools 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) action2 = Action(name='Action 2') # 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) action2 = Action(name='Action 2') # 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-6.1.2/pyface/action/tests/__init__.py0000644000076500000240000000000013462774551021733 0ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/action/tests/test_traitsui_widget_action.py0000644000076500000240000001301013500710640025762 0ustar cwebsterstaff00000000000000# Copyright (c) 2019, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! from __future__ import ( absolute_import, division, print_function, unicode_literals ) import unittest from traits.api import Enum, HasTraits from traits.testing.unittest_tools 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") 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-6.1.2/pyface/action/tests/test_action_controller.py0000644000076500000240000000301213500710640024737 0ustar cwebsterstaff00000000000000from __future__ import absolute_import 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-6.1.2/pyface/action/tests/test_gui_application_action.py0000644000076500000240000000312713500710640025732 0ustar cwebsterstaff00000000000000from __future__ import absolute_import import unittest from traits.testing.unittest_tools 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-6.1.2/pyface/action/tests/test_action_item.py0000644000076500000240000001462613500710640023527 0ustar cwebsterstaff00000000000000from __future__ import absolute_import import unittest from traits.testing.unittest_tools import UnittestTools from pyface.image_cache import ImageCache from pyface.toolkit import toolkit_object from pyface.widget import Widget 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-6.1.2/pyface/action/tests/test_listening_action.py0000644000076500000240000001523013500710640024555 0ustar cwebsterstaff00000000000000from __future__ import absolute_import import unittest from traits.api import Any, Bool, HasTraits from traits.testing.unittest_tools 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-6.1.2/pyface/action/tests/test_action_manager.py0000644000076500000240000002235013500710640024174 0ustar cwebsterstaff00000000000000from __future__ import absolute_import import unittest from traits.testing.unittest_tools 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-6.1.2/pyface/action/tests/test_field_action.py0000644000076500000240000000630213500710640023644 0ustar cwebsterstaff00000000000000# Copyright (c) 2019, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! from __future__ import ( absolute_import, division, print_function, unicode_literals ) 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-6.1.2/pyface/action/tests/test_action_event.py0000644000076500000240000000053713500710640023706 0ustar cwebsterstaff00000000000000from __future__ import absolute_import 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-6.1.2/pyface/action/tests/test_action.py0000644000076500000240000000301313500710640022475 0ustar cwebsterstaff00000000000000from __future__ import absolute_import 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-6.1.2/pyface/action/status_bar_manager.py0000644000076500000240000000167213462774551022713 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A status bar manager realizes itself in a status bar control. """ # Import the toolkit specific version. from pyface.toolkit import toolkit_object StatusBarManager = toolkit_object('action.status_bar_manager:StatusBarManager') ### EOF ####################################################################### pyface-6.1.2/pyface/action/action_manager.py0000644000076500000240000003042113462774551022013 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Abstract base class for all action managers. """ # Enthought library imports. from __future__ import print_function from traits.api import Bool, Constant, Event, HasTraits, Instance from traits.api import List, Property, Str # Local imports. from pyface.action.action_controller import ActionController from pyface.action.group import Group import six 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(ActionManager, self).__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, six.string_types): # 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: ## # If no group has been created then add one. This is only ## # relevant when using the 'shorthand' way to define menus. ## if group is None: ## group = Group(id='__first__') ## self.insert(-1, group) group.append(arg) ########################################################################### # 'ActionManager' interface. ########################################################################### #### Trait properties ##################################################### def _get_groups(self): return self._groups[:] #### Trait change handlers ################################################ def _enabled_changed(self, trait_name, old, new): for group in self._groups: group.enabled = new def _visible_changed(self, trait_name, old, new): for group in self._groups: group.visible = 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 defualt 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, six.string_types): # 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-6.1.2/pyface/action/__init__.py0000644000076500000240000000010213462774551020574 0ustar cwebsterstaff00000000000000# Copyright (c) 2005-2011, Enthought, Inc. # All rights reserved. pyface-6.1.2/pyface/action/action.py0000644000076500000240000001120313462774551020316 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The base class for all actions. """ from functools import partial # Enthought library imports. from traits.api import Bool, Callable, Enum, HasTraits, Str from traits.api import Unicode 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 = Unicode #: 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 = Unicode #: 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 = Unicode #: 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 = Unicode #: 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-6.1.2/pyface/action/api.py0000644000076500000240000000301313462774551017612 0ustar cwebsterstaff00000000000000# Copyright (c) 2005-18, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! from __future__ import absolute_import 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-6.1.2/pyface/action/action_manager_item.py0000644000076500000240000000544713462774551023043 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Abstract base class for all action manager items. """ # Enthought library imports. 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-6.1.2/pyface/action/group.py0000644000076500000240000002025413462774551020203 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A group of action manager items. """ from functools import partial # Enthought library imports. from traits.api import Any, Bool, HasTraits, Instance, List, Property from traits.api import Str from traits.trait_base import user_name_for # Local imports. from pyface.action.action import Action from pyface.action.action_item import ActionItem from pyface.action.action_manager_item import ActionManagerItem 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(Group, self).__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 ################################################ def _enabled_changed(self, trait_name, old, new): for item in self.items: item.enabled = 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-6.1.2/pyface/action/traitsui_widget_action.py0000644000076500000240000000450613462774551023615 0ustar cwebsterstaff00000000000000# Copyright (c) 2019, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! 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(TraitsUIWidgetAction, self).trait_context() pyface-6.1.2/pyface/action/action_item.py0000644000076500000240000001353213462774551021343 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enth373ought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ An action manager item that represents an actual action. """ # Enthought library imports. from traits.api import Any, Instance, List, Property, Str, on_trait_change # Local imports. 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 ################################################ def _enabled_changed(self, trait_name, old, new): self.action.enabled = new def _visible_changed(self, trait_name, old, new): self.action.visible = new @on_trait_change('_wrappers.control') def _on_destroy(self, object, name, old, new): """ Handle the destruction of the wrapper. """ if name == 'control' and new is None: self._wrappers.remove(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-6.1.2/pyface/action/menu_bar_manager.py0000644000076500000240000000166613462774551022337 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A menu bar manager realizes itself in errr, a menu bar control. """ # Import the toolkit specific version. from pyface.toolkit import toolkit_object MenuBarManager = toolkit_object('action.menu_bar_manager:MenuBarManager') #### EOF ###################################################################### pyface-6.1.2/pyface/action/action_controller.py0000644000076500000240000000405013462774551022563 0ustar cwebsterstaff00000000000000""" The default action controller for menus, menu bars and tool bars. """ # Enthought library imports. 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-6.1.2/pyface/viewer/0000755000076500000240000000000013515277240016506 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/viewer/table_column_provider.py0000644000076500000240000000305413462774552023451 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Base class for all table column providers. """ # Enthought library imports. from traits.api import Int # Local imports. 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 #### EOF ###################################################################### pyface-6.1.2/pyface/viewer/label_provider.py0000644000076500000240000000340213462774552022061 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Abstract base class for label providers. """ # Enthought library imports. 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 #### EOF ###################################################################### pyface-6.1.2/pyface/viewer/viewer_filter.py0000644000076500000240000000440613462774552021743 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Abstract base class for all viewer filters. """ # Enthought library imports. 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 #### EOF ###################################################################### pyface-6.1.2/pyface/viewer/tree_item.py0000644000076500000240000001010113462774552021037 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ 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) """ # Enthought library imports. 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) #### EOF ###################################################################### pyface-6.1.2/pyface/viewer/viewer.py0000644000076500000240000000175513462774552020402 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Abstract base class for all viewers. """ # Enthought library imports. from pyface.widget import Widget class Viewer(Widget): """ Abstract base class for all viewers. A viewer is a model-based adapter on some underlying toolkit-specific widget. """ pass #### EOF ###################################################################### pyface-6.1.2/pyface/viewer/viewer_sorter.py0000644000076500000240000001005213474227520021755 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Abstract base class for all viewer sorters. """ # Enthought library imports. 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 comparator(element_a, element_b): """ Comparator. """ return self.compare(viewer, parent, element_a, element_b) elements.sort(comparator) return elements def compare(self, viewer, parent, element_a, element_b): """ Returns the result of comparing two elements. 'viewer' is the viewer that we are sorting elements for. 'parent' is the parent element. 'element_a' is the the first element to compare. 'element_b' is the the second element to compare. """ # Get the category for each element. category_a = self.category(viewer, parent, element_a) category_b = self.category(viewer, parent, element_b) # If they are not in the same category then return the result of # comparing the categories. if category_a != category_b: result = cmp(category_a, category_b) else: # Get the label text for each element. # # fixme: This is a hack until we decide whethwe we like the # JFace(ish) or Swing(ish) models! if hasattr(viewer, 'label_provider'): label_a = viewer.label_provider.get_text(viewer, element_a) label_b = viewer.label_provider.get_text(viewer, element_b) else: label_a = viewer.node_model.get_text(viewer, element_a) label_b = viewer.node_model.get_text(viewer, element_b) # Compare the label text. result = cmp(label_a, label_b) return result 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 #### EOF ###################################################################### pyface-6.1.2/pyface/viewer/table_content_provider.py0000644000076500000240000000300113462774552023616 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Abstract base class for table content providers. """ # Local imports. 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 #### EOF #################################################################### pyface-6.1.2/pyface/viewer/content_viewer.py0000644000076500000240000000421513462774552022126 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Abstract base class for all content viewers. """ # Enthought library imports. from traits.api import Any, Instance, List # Local imports. 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) #### EOF ###################################################################### pyface-6.1.2/pyface/viewer/table_viewer.py0000644000076500000240000000116613462774552021545 0ustar cwebsterstaff00000000000000# Copyright (c) 2017, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! """ A viewer based on a table control. """ # Import the toolkit specific version. from __future__ import absolute_import from pyface.toolkit import toolkit_object TableViewer = toolkit_object('viewer.table_viewer:TableViewer') pyface-6.1.2/pyface/viewer/column_provider.py0000644000076500000240000000356313462774552022307 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Base class for all column providers. """ # Enthought library imports. 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' #### EOF ###################################################################### pyface-6.1.2/pyface/viewer/__init__.py0000644000076500000240000000120113462774552020622 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ pyface-6.1.2/pyface/viewer/tree_viewer.py0000644000076500000240000000116213462774552021411 0ustar cwebsterstaff00000000000000# Copyright (c) 2017, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! """ A viewer based on a tree control. """ # Import the toolkit specific version. from __future__ import absolute_import from pyface.toolkit import toolkit_object TreeViewer = toolkit_object('viewer.tree_viewer:TreeViewer') pyface-6.1.2/pyface/viewer/tree_label_provider.py0000644000076500000240000000411713462774552023104 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Base class for all tree label providers. """ # Local imports. 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 #### EOF ###################################################################### pyface-6.1.2/pyface/viewer/api.py0000644000076500000240000000266213462774552017650 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ from __future__ import absolute_import 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-6.1.2/pyface/viewer/default_tree_content_provider.py0000644000076500000240000000561013462774552025202 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The default tree content provider. """ # Local imports. from .tree_content_provider import TreeContentProvider 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) # Trait notification. #self.items_inserted(parent, [index], [child]) return (index, child) def insert(self, parent, index, child): """ Inserts 'child' into the 'parent' item at 'index'. """ parent.insert(index, child) # Trait notification. #self.items_inserted(parent, [index], [child]) return child def remove(self, parent, child): """ Removes 'child' from the 'parent' item. """ index = parent.children.index(child) parent.remove(child) # Trait notification. #self.items_removed(parent, [index], [child]) return child ######################################################################### # Protected interface. ######################################################################### def _create_item(self, **kw): """ Creates a new item. """ return TreeItem(**kw) #### EOF #################################################################### pyface-6.1.2/pyface/viewer/table_label_provider.py0000644000076500000240000000300213462774552023224 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Base class for all table label providers. """ # Local imports. 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) #### EOF ###################################################################### pyface-6.1.2/pyface/viewer/tree_content_provider.py0000644000076500000240000000436413462774552023503 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Abstract base class for tree content providers. """ # Local imports. 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 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 #### EOF #################################################################### pyface-6.1.2/pyface/viewer/content_provider.py0000644000076500000240000000267113462774552022463 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Abstract base class for content providers. """ # Enthought library imports. 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 #### EOF #################################################################### pyface-6.1.2/pyface/i_image_cache.py0000644000076500000240000000511413462774551020305 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The interface for an image cache. """ # Enthought library imports. from traits.api import 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(object): """ The mixin class that contains common code for toolkit specific implementations of the IImageCache interface. """ pyface-6.1.2/pyface/gui_application.py0000644000076500000240000002366713462774551020754 0ustar cwebsterstaff00000000000000# Copyright (c) 2014-2017 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! """ 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`. """ from __future__ import ( absolute_import, division, print_function, unicode_literals ) import logging from traits.api import ( Bool, Callable, Instance, List, ReadOnly, Tuple, Undefined, Vetoable, on_trait_change ) 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(GUIApplication, self).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(GUIApplication, self)._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 sys import version_info if (version_info.major, version_info.minor) >= (3, 2): from html import escape else: from cgi import escape from pyface.about_dialog import AboutDialog additions = [ u"

{}

".format(escape(self.name)), u"Copyright © 2018 {}, all rights reserved".format( escape(self.company), ), u"", ] additions += [escape(line) for line in self.description.split('\n\n')] dialog = AboutDialog( title=u"About {}".format(self.name), additions=additions, ) if self.logo: dialog.image = self.logo return dialog # Trait listeners -------------------------------------------------------- @on_trait_change('windows:activated') def _on_activate_window(self, window, trait, old, new): """ Listener that tracks currently active window. """ if window in self.windows: self.active_window = window @on_trait_change('windows:deactivated') def _on_deactivate_window(self, window, trait, old, new): """ Listener that tracks currently active window. """ self.active_window = None @on_trait_change('windows:closed') def _on_window_closed(self, window, trait, old, new): """ Listener that ensures window handles are released when closed. """ if window in self.windows: self.windows.remove(window) pyface-6.1.2/pyface/i_python_shell.py0000644000076500000240000001043413462774551020611 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The interface for an interactive Python shell. """ # Enthought library imports. from traits.api import Event # Local imports. from pyface.key_pressed_event import KeyPressedEvent from pyface.i_widget import IWidget class IPythonShell(IWidget): """ 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(object): """ 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-6.1.2/pyface/drop_handler.py0000644000076500000240000000342213462774551020231 0ustar cwebsterstaff00000000000000from 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-6.1.2/pyface/workbench/0000755000076500000240000000000013515277240017167 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/workbench/perspective.py0000755000076500000240000001421513462774552022111 0ustar cwebsterstaff00000000000000""" The default perspective. """ # Standard library imports. import logging # Enthought library imports. from traits.api import Bool, HasTraits, List, provides, Str, Tuple # Local imports. 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() return 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) return 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) return 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) return 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 #### EOF ###################################################################### pyface-6.1.2/pyface/workbench/window_event.py0000644000076500000240000000112513462774552022261 0ustar cwebsterstaff00000000000000""" Window events. """ # Enthought library imports. from traits.api import HasTraits, Instance, Vetoable # Local imports. 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 #### EOF ###################################################################### pyface-6.1.2/pyface/workbench/i_editor.py0000755000076500000240000001103113462774552021347 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The interface of a workbench editor. """ # standard library imports import uuid # Enthought library imports. from traits.api import Any, Bool, Event, VetoableEvent, Vetoable, \ HasTraits, Instance, Interface from traits.api import provides # Local imports. from .i_workbench_part import IWorkbenchPart, MWorkbenchPart class IEditor(IWorkbenchPart): """ The interface of a workbench editor. """ # The optional command stack. command_stack = Instance('apptools.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('apptools.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 apptools.undo.api import CommandStack except ImportError: return None return CommandStack(undo_manager=self.window.workbench.undo_manager) #### EOF ###################################################################### pyface-6.1.2/pyface/workbench/i_perspective.py0000755000076500000240000000227713462774552022426 0ustar cwebsterstaff00000000000000""" The perspective interface. """ # Enthought library imports. from traits.api import Bool, Interface, List, Str, Tuple # Local imports. 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. """ #### EOF ###################################################################### pyface-6.1.2/pyface/workbench/perspective_item.py0000755000076500000240000000374613462774552023136 0ustar cwebsterstaff00000000000000""" An item in a Perspective contents list. """ # Enthought library imports. from traits.api import Enum, Float, HasTraits, provides, Str # Local imports. 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') #### EOF ###################################################################### pyface-6.1.2/pyface/workbench/tests/0000755000076500000240000000000013515277240020331 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/workbench/tests/__init__.py0000644000076500000240000000000013462774552022441 0ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/workbench/tests/test_workbench_window.py0000644000076500000240000001564313500710640025312 0ustar cwebsterstaff00000000000000import mock import tempfile import shutil import os import unittest from traits.testing.unittest_tools import UnittestTools from pyface.workbench.perspective import Perspective from pyface.workbench.api import Workbench from pyface.workbench.user_perspective_manager import UserPerspectiveManager 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-6.1.2/pyface/workbench/workbench_window.py0000755000076500000240000006673213462774552023144 0ustar cwebsterstaff00000000000000""" A workbench window. """ # Standard library imports. import logging # Enthought library imports. from pyface.api import ApplicationWindow, GUI from traits.api import Callable, Constant, Delegate, Event, Instance from traits.api import List, Str, Tuple, Unicode, Vetoable, Undefined from traits.api import on_trait_change, provides # Local imports. 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) return def activate_view(self, view): """ Activates a view. """ self.layout.activate_view(view) return 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) return 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 not view in self.views: self.views.append(view) return def close_editor(self, editor): """ Closes an editor. """ self.layout.close_editor(editor) return 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) return 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() return def destroy_views(self, views): """ Destroy a list of views. """ for view in views: if view.control is not None: view.destroy_control() return 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.warn('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() return def hide_view(self, view): """ Hide a view. """ self.layout.hide_view(view) return def refresh(self): """ Refresh the window to reflect any changes. """ self.layout.refresh() return 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) return 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) return def reset_editors(self): """ Activate the first editor in every tab. """ self.layout.reset_editors() return def reset_views(self): """ Activate the first view in every tab. """ self.layout.reset_views() return def show_editor_area(self): """ Show the editor area. """ self.layout.show_editor_area() return 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) return 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.warn('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.warn('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.warn('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() ) return 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() return 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) return 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 return 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) return 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 return 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 return 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 #### @on_trait_change('layout.editor_closed') def _on_editor_closed(self, editor): """ Dynamic trait change handler. """ if editor is None or editor is Undefined: return index = self.editors.index(editor) del self.editors[index] if editor 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 @on_trait_change('editors.has_focus') def _on_editor_has_focus_changed(self, obj, trait_name, old, new): """ Dynamic trait change handler. """ if trait_name == 'has_focus' and new: self.active_editor = obj return @on_trait_change('views.has_focus') def _has_focus_changed_for_view(self, obj, trait_name, old, new): """ Dynamic trait change handler. """ if trait_name == 'has_focus' and new: self.active_view = obj return @on_trait_change('views.visible') def _visible_changed_for_view(self, obj, trait_name, old, new): """ Dynamic trait change handler. """ if trait_name == 'visible': if not new: if obj is self.active_view: self.active_view = None return #### EOF ###################################################################### pyface-6.1.2/pyface/workbench/__init__.py0000644000076500000240000000027613462774552021316 0ustar cwebsterstaff00000000000000import warnings warnings.warn( PendingDeprecationWarning( "Workbench will be moved from pyface.workbench to apptools.workbench " "in Pyface 7.0.0" ), stacklevel=2 ) pyface-6.1.2/pyface/workbench/traits_ui_view.py0000644000076500000240000000602013462774552022605 0ustar cwebsterstaff00000000000000""" A view whose content is provided by a traits UI. """ # Standard library imports. import logging # Enthought library imports. from traits.api import Any, Instance, Str # Local imports. 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 #### EOF ###################################################################### pyface-6.1.2/pyface/workbench/workbench.py0000755000076500000240000003272213462774552021545 0ustar cwebsterstaff00000000000000""" A workbench. """ # Standard library imports. import six.moves.cPickle import logging import os # Enthought library imports. 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, Unicode, Vetoable from traits.api import VetoableEvent # Local imports. 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 = Unicode # The optional undo manager. undo_manager = Instance('apptools.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.on_trait_change(self._on_window_activated, 'activated') window.on_trait_change(self._on_window_opening, 'opening') window.on_trait_change(self._on_window_opened, 'opened') window.on_trait_change(self._on_window_closing, 'closing') window.on_trait_change(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 apptools.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 = six.moves.cPickle.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) return 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') six.moves.cPickle.dump(window.get_memento(), f) f.close() return #### Trait change handlers ################################################ def _on_window_activated(self, window, trait_name, event): """ Dynamic trait change handler. """ logger.debug('window %s activated', window) self.active_window = window return def _on_window_opening(self, window, trait_name, event): """ Dynamic trait change handler. """ # Event notification. self.window_opening = window_event = VetoableWindowEvent(window=window) if window_event.veto: event.veto = True return def _on_window_opened(self, window, trait_name, event): """ Dynamic trait change handler. """ # 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) return def _on_window_closing(self, window, trait_name, event): """ Dynamic trait change handler. """ # Event notification. self.window_closing = window_event = VetoableWindowEvent(window=window) if window_event.veto: event.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.veto = True if not event.veto: # Save the window size, position and layout. self._save_window_layout(window) return def _on_window_closed(self, window, trait_name, event): """ Dynamic trait change handler. """ 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 #### EOF ###################################################################### pyface-6.1.2/pyface/workbench/api.py0000755000076500000240000000106313462774552020326 0ustar cwebsterstaff00000000000000from __future__ import absolute_import 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-6.1.2/pyface/workbench/i_workbench_part.py0000644000076500000240000000712113462774552023073 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The interface for workbench parts. """ # Enthought library imports. from traits.api import Any, Bool, HasTraits, Instance, Interface from traits.api import List, provides, Str, Unicode 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 = Unicode # 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 = Unicode # 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 #### EOF ###################################################################### pyface-6.1.2/pyface/workbench/action/0000755000076500000240000000000013515277240020444 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/workbench/action/delete_user_perspective_action.py0000644000076500000240000000524013462774552027276 0ustar cwebsterstaff00000000000000#----------------------------------------------------------------------------- # # Copyright (c) 2005-2006 by Enthought, Inc. # All rights reserved. # # Author: David C. Morrill # #----------------------------------------------------------------------------- """ An action that deletes a user perspective. """ # Enthought library imports. from pyface.api import YES # Local imports. 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] #### EOF ##################################################################### pyface-6.1.2/pyface/workbench/action/tool_bar_manager.py0000644000076500000240000000234413462774552024325 0ustar cwebsterstaff00000000000000""" The tool bar manager for the Envisage workbench window. """ # Enthought library imports. import pyface.action.api as pyface from traits.api import Instance # Local imports. 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): """ 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(ToolBarManager, self).create_tool_bar( parent, controller=controller ) return tool_bar #### EOF ###################################################################### pyface-6.1.2/pyface/workbench/action/workbench_action.py0000644000076500000240000000111613462774552024345 0ustar cwebsterstaff00000000000000""" Abstract base class for all workbench actions. """ # Enthought library imports. 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) #### EOF ###################################################################### pyface-6.1.2/pyface/workbench/action/view_chooser.py0000644000076500000240000001435113514577463023527 0ustar cwebsterstaff00000000000000""" A UI that allows the user to choose a view. """ # Enthought library imports. 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 = .2, height = .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 #### EOF ###################################################################### pyface-6.1.2/pyface/workbench/action/user_perspective_action.py0000644000076500000240000000364313462774552025761 0ustar cwebsterstaff00000000000000#----------------------------------------------------------------------------- # # Copyright (c) 2005-2006 by Enthought, Inc. # All rights reserved. # # Author: David C. Morrill # #----------------------------------------------------------------------------- """ The base class for user perspective actions. """ # Enthought library imports. from traits.api import on_trait_change # Local imports. 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:] == '__')) @on_trait_change('window.active_perspective') def _refresh_enabled(self): """ 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 #### EOF ##################################################################### pyface-6.1.2/pyface/workbench/action/rename_user_perspective_action.py0000644000076500000240000000302513462774552027302 0ustar cwebsterstaff00000000000000#----------------------------------------------------------------------------- # # Copyright (c) 2005-2006 by Enthought, Inc. # All rights reserved. # # Author: David C. Morrill # #----------------------------------------------------------------------------- """ An action that renames a user perspective. """ # Local imports. 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 #### EOF ##################################################################### pyface-6.1.2/pyface/workbench/action/reset_all_perspectives_action.py0000644000076500000240000000221013462774552027125 0ustar cwebsterstaff00000000000000""" An action that resets *all* perspectives. """ # Enthought library imports. from pyface.api import YES # Local imports. 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 #### EOF ###################################################################### pyface-6.1.2/pyface/workbench/action/setattr_action.py0000644000076500000240000000166013462774552024055 0ustar cwebsterstaff00000000000000""" An action that sets an attribute. """ # Enthought library imports. from traits.api import Any, Str # Local imports. 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 #### EOF ###################################################################### pyface-6.1.2/pyface/workbench/action/__init__.py0000755000076500000240000000000013462774552022557 0ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/workbench/action/show_view_action.py0000644000076500000240000000300513462774552024374 0ustar cwebsterstaff00000000000000""" An action that shows a dialog to allow the user to choose a view. """ # Local imports. 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 #### EOF ###################################################################### pyface-6.1.2/pyface/workbench/action/api.py0000644000076500000240000000026113462774552021577 0ustar cwebsterstaff00000000000000from __future__ import absolute_import from .menu_bar_manager import MenuBarManager from .tool_bar_manager import ToolBarManager from .view_menu_manager import ViewMenuManager pyface-6.1.2/pyface/workbench/action/user_perspective_name.py0000644000076500000240000000554313462774552025425 0ustar cwebsterstaff00000000000000#----------------------------------------------------------------------------- # # Copyright (c) 2005-2006 by Enthought, Inc. # All rights reserved. # # Author: David C. Morrill # #----------------------------------------------------------------------------- """ Object with views for naming or renaming a user perspective. """ # Enthought library imports. from traits.api import Bool, HasTraits, Trait, TraitError, Constant from traitsui.api import View, Item, VGroup import six #### Trait definitions ######################################################## def not_empty_string(object, name, value): """a not-empty string""" if isinstance(value, six.string_types) and (value.strip() != ''): return value raise TraitError # Define a trait which can not be the empty string: NotEmptyString = Trait('', not_empty_string) 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 ) #### EOF ##################################################################### pyface-6.1.2/pyface/workbench/action/new_user_perspective_action.py0000644000076500000240000000345713462774552026635 0ustar cwebsterstaff00000000000000#----------------------------------------------------------------------------- # # Copyright (c) 2005-2006 by Enthought, Inc. # All rights reserved. # # Author: David C. Morrill # #----------------------------------------------------------------------------- """ An action that creates a new (and empty) user perspective. """ # Local imports. 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 #### EOF ##################################################################### pyface-6.1.2/pyface/workbench/action/save_as_user_perspective_action.py0000644000076500000240000000362413462774552027461 0ustar cwebsterstaff00000000000000#----------------------------------------------------------------------------- # # Copyright (c) 2005-2006 by Enthought, Inc. # All rights reserved. # # Author: David C. Morrill # #----------------------------------------------------------------------------- """ An action that saves the active perspective as a user perspective. """ # Local imports. 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 #### EOF ##################################################################### pyface-6.1.2/pyface/workbench/action/toggle_view_visibility_action.py0000644000076500000240000000572113462774552027153 0ustar cwebsterstaff00000000000000""" An action that toggles a view's visibility (ie. hides/shows it). """ # Enthought library imports. from pyface.workbench.api import IView from traits.api import Delegate, Instance # Local imports. 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) return 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.on_trait_change(self._refresh_checked, 'visible') view.on_trait_change(self._refresh_checked, 'window') return def _remove_view_listeners(self, view): """ Add listeners for trait events on a view. """ view.on_trait_change(self._refresh_checked, 'visible', remove=True) view.on_trait_change(self._refresh_checked, 'window', remove=True) return def _refresh_checked(self): """ 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 return def _toggle_view_visibility(self, view): """ Toggle the visibility of a view. """ if view.visible: view.hide() else: view.show() return #### EOF ###################################################################### pyface-6.1.2/pyface/workbench/action/set_active_perspective_action.py0000644000076500000240000000374513462774552027134 0ustar cwebsterstaff00000000000000""" An action that sets the active perspective. """ # Enthought library imports. from pyface.workbench.api import IPerspective from traits.api import Delegate, Instance, on_trait_change # Local imports. 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 return def perform(self, event): """ Perform the action. """ self.window.active_perspective = self.perspective return ########################################################################### # Private interface. ########################################################################### @on_trait_change('perspective,window.active_perspective') def _refresh_checked(self): """ 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 #### EOF ###################################################################### pyface-6.1.2/pyface/workbench/action/view_menu_manager.py0000644000076500000240000001075013514577463024522 0ustar cwebsterstaff00000000000000""" The 'View' menu """ # Standard library imports. import logging # Enthought library imports. from pyface.action.api import Group, MenuManager from traits.api import Any, Bool, Instance, List, Str, Unicode from traits.api import on_trait_change # Local imports. 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 = Unicode('&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. ########################################################################### @on_trait_change( 'window.active_perspective,window.active_part,' 'window.views,window.views_items' ) def refresh(self): """ 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() return 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 #### EOF ###################################################################### pyface-6.1.2/pyface/workbench/action/reset_active_perspective_action.py0000644000076500000240000000227413462774552027457 0ustar cwebsterstaff00000000000000""" An action that resets the active perspective. """ # Enthought library imports. from pyface.api import YES # Local imports. 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 #### EOF ###################################################################### pyface-6.1.2/pyface/workbench/action/menu_bar_manager.py0000644000076500000240000000230713462774552024313 0ustar cwebsterstaff00000000000000""" The menu bar manager for Envisage workbench windows. """ # Enthought library imports. from pyface.action.api import MenuBarManager as BaseMenuBarManager from traits.api import Instance # Local imports. 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(MenuBarManager, self).create_menu_bar( parent, controller=controller ) return menu_bar #### EOF ###################################################################### pyface-6.1.2/pyface/workbench/action/action_controller.py0000644000076500000240000000243513462774552024553 0ustar cwebsterstaff00000000000000""" The action controller for workbench menu and tool bars. """ # Enthought library imports. from pyface.action.api import ActionController from pyface.workbench.api import WorkbenchWindow from traits.api import HasTraits, 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) #### EOF ###################################################################### pyface-6.1.2/pyface/workbench/action/perspective_menu_manager.py0000644000076500000240000001112413462774552026075 0ustar cwebsterstaff00000000000000""" The default perspective menu for a workbench window. """ # Enthought library imports. from pyface.action.api import Group, MenuManager from traits.api import Instance, List, on_trait_change # Local imports. 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. ########################################################################### @on_trait_change('window.perspectives') @on_trait_change('window.perspectives_items') def rebuild(self): """ 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 #### EOF ###################################################################### pyface-6.1.2/pyface/workbench/workbench_window_memento.py0000644000076500000240000000150413462774552024647 0ustar cwebsterstaff00000000000000""" A memento for a workbench window. """ # Enthought library imports. 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() #### EOF ###################################################################### pyface-6.1.2/pyface/workbench/view.py0000755000076500000240000000160113462774552020525 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The implementation of a workbench view. """ # Import the toolkit specific version. from pyface.toolkit import toolkit_object View = toolkit_object('workbench.view:View') ### EOF ####################################################################### pyface-6.1.2/pyface/workbench/i_view.py0000755000076500000240000001000713462774552021035 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The interface for workbench views. """ # Standard library imports. import logging # Enthought library imports. from pyface.api import ImageResource from traits.api import Bool, Enum, Float, Instance, List, provides, Str from traits.util.camel_case import camel_case_to_words # Local imports. 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 = Instance(ImageResource) # 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 = Instance(ImageResource) # 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.warn('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.warn('view %s has no name - using <%s>' % (self, name)) return name ########################################################################### # 'IView' interface. ########################################################################### def activate(self): """ Activate the view. """ self.window.activate_view(self) return def hide(self): """ Hide the view. """ self.window.hide_view(self) return def show(self): """ Show the view. """ self.window.show_view(self) return #### EOF ###################################################################### pyface-6.1.2/pyface/workbench/i_workbench.py0000644000076500000240000000624513462774552022053 0ustar cwebsterstaff00000000000000""" The workbench interface. """ # Enthought library imports. from traits.api import Event, Instance, Interface, List, Str from traits.api import provides, VetoableEvent # Local imports. 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('apptools.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. """ #### EOF ###################################################################### pyface-6.1.2/pyface/workbench/i_perspective_item.py0000644000076500000240000000356013462774552023435 0ustar cwebsterstaff00000000000000""" The interface for perspective items. """ # Enthought library imports. 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') #### EOF ###################################################################### pyface-6.1.2/pyface/workbench/workbench_window_layout.py0000755000076500000240000000170613462774552024527 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ 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' ) ### EOF ####################################################################### pyface-6.1.2/pyface/workbench/editor.py0000755000076500000240000000161113462774552021042 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The implementation of a workbench editor. """ # Import the toolkit specific version. from pyface.toolkit import toolkit_object Editor = toolkit_object('workbench.editor:Editor') ### EOF ####################################################################### pyface-6.1.2/pyface/workbench/i_workbench_window_layout.py0000755000076500000240000002510513462774552025036 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The workbench window layout interface. """ # Enthought library imports. from traits.api import Event, HasTraits, Instance, Interface, Str from traits.api import provides # Local imports. 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 #### EOF ###################################################################### pyface-6.1.2/pyface/workbench/user_perspective_manager.py0000644000076500000240000001520213462774552024633 0ustar cwebsterstaff00000000000000""" Manages a set of user perspectives. """ # Standard library imports. import logging import os # Enthought library imports. from pyface.workbench.api import Perspective from traits.api import Any, Dict, HasTraits, Int, List, Property from traits.api import Unicode # 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 = Unicode # 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(Unicode) #### 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() return 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() return 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 #### EOF ###################################################################### pyface-6.1.2/pyface/workbench/i_editor_manager.py0000755000076500000240000000267413462774552023056 0ustar cwebsterstaff00000000000000""" The editor manager interface. """ # Enthought library imports. 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. """ #### EOF ###################################################################### pyface-6.1.2/pyface/workbench/editor_manager.py0000755000076500000240000000547613462774552022551 0ustar cwebsterstaff00000000000000""" The default editor manager. """ # Standard library imports. import weakref # Enthought library imports. from traits.api import HasTraits, Instance, provides # Local imports. 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(EditorManager, self).__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 #### EOF ###################################################################### pyface-6.1.2/pyface/workbench/debug/0000755000076500000240000000000013515277240020255 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/workbench/debug/debug_view.py0000644000076500000240000000517613462774552022771 0ustar cwebsterstaff00000000000000""" A view containing a main walter canvas. """ # Enthought library imports. from pyface.workbench.api import View, WorkbenchWindow from traits.api import HasTraits, Instance, Str, on_trait_change 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. ########################################################################### @on_trait_change( 'window.active_editor', 'window.active_part', 'window.active_view' ) def refresh(self): """ 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) return 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 #### EOF ###################################################################### pyface-6.1.2/pyface/workbench/debug/__init__.py0000644000076500000240000000000013462774552022365 0ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/workbench/debug/api.py0000644000076500000240000000011313462774552021404 0ustar cwebsterstaff00000000000000from __future__ import absolute_import from .debug_view import DebugView pyface-6.1.2/pyface/workbench/traits_ui_editor.py0000644000076500000240000000505713462774552023132 0ustar cwebsterstaff00000000000000""" An editor whose content is provided by a traits UI. """ # Standard library imports. import logging # Enthought library imports. from traits.api import Instance, Str # Local imports. 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 #### EOF ###################################################################### pyface-6.1.2/pyface/list_box_model.py0000644000076500000240000000214613462774551020575 0ustar cwebsterstaff00000000000000""" The model for list boxes. """ # Enthought library imports 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-6.1.2/pyface/split_widget.py0000644000076500000240000000164713462774551020275 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Mix-in class for split widgets. """ # Import the toolkit specific version. from __future__ import absolute_import from .toolkit import toolkit_object SplitWidget = toolkit_object('split_widget:SplitWidget') #### EOF ###################################################################### pyface-6.1.2/pyface/key_pressed_event.py0000644000076500000240000000122413462774551021304 0ustar cwebsterstaff00000000000000""" The event that is generated when a key is pressed. """ # Enthought library imports. 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 #### EOF ###################################################################### pyface-6.1.2/pyface/image_button.py0000644000076500000240000000116213462774551020244 0ustar cwebsterstaff00000000000000# Copyright (c) 2017, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! import logging logger = logging.getLogger(__name__) logger.warning( 'DEPRECATED: pyface.image_button, use pyface.ui.wx.image_button instead. ' 'Will be removed in Pyface 7.') from pyface.ui.wx.image_button import * pyface-6.1.2/pyface/i_progress_dialog.py0000644000076500000240000000757513462774551021300 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The interface for a dialog that allows the user to open/save files etc. """ # Enthought library imports. from traits.api import Any, Bool, Int, Str # Local imports. 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(object): """ 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(MProgressDialog, self).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-6.1.2/pyface/splash_screen_log_handler.py0000644000076500000240000000317413462774551022763 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005-2015, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A log handler that emits records to a splash screen. """ # Standard library imports. from logging import Handler import six 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(SplashScreenLogHandler, self).__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 = six.text_type(record.getMessage()) + u'...' pyface-6.1.2/pyface/i_application_window.py0000644000076500000240000001167113462774551021777 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The interface of a top-level application window. """ # Enthought library imports. from traits.api import Instance, List # Local imports. from pyface.action.api import MenuBarManager, StatusBarManager, ToolBarManager from pyface.i_image_resource import IImageResource from pyface.i_window import IWindow 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 = Instance(IImageResource) #: 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(object): """ 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(MApplicationWindow, self).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-6.1.2/pyface/split_dialog.py0000644000076500000240000000276413462774551020252 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A dialog that is split in two either horizontally or vertically. """ # Local imports. 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-6.1.2/pyface/base_toolkit.py0000644000076500000240000002361213462774551020252 0ustar cwebsterstaff00000000000000""" 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:`pkg_resources` 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 pkg_resources import sys from traits.api import HasTraits, List, ReadOnly, Str, TraitError from traits.etsconfig.api import ETSConfig try: provisional_toolkit = ETSConfig.provisional_toolkit except AttributeError: from contextlib import contextmanager # for backward compatibility @contextmanager def provisional_toolkit(toolkit_name): """ Perform an operation with toolkit provisionally set This sets the toolkit attribute of the ETSConfig object set to the provided value. If the operation fails with an exception, the toolkit is reset to nothing. """ if ETSConfig.toolkit: raise AttributeError("ETSConfig toolkit is already set") ETSConfig.toolkit = toolkit_name try: yield except: # reset the toolkit state ETSConfig._toolkit = '' raise 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(Toolkit, self).__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_traceback) filename, lineno, function, text = frames[-1] if not package 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. """ plugins = list(pkg_resources.iter_entry_points(entry_point, 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") modules = ', '.join(plugin.module_name for plugin in plugins) logger.warning(msg, entry_point, toolkit_name, modules) 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" logger.info(msg, plugin.name, plugin.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 ------ TraitError If no working toolkit is found. 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) entry_points = [ plugin for plugin in pkg_resources.iter_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" logger.info(msg, entry_point, plugin.name, plugin.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) raise TraitError("Could not import any {} toolkit.".format(entry_point)) pyface-6.1.2/pyface/filter.py0000644000076500000240000000426413462774551017062 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Base class for all filters. """ # Enthought library imports. 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 #### EOF ###################################################################### pyface-6.1.2/pyface/fields/0000755000076500000240000000000013515277235016457 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/fields/i_combo_field.py0000644000076500000240000000740713462774551021617 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2019, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The text field interface. """ from __future__ import ( absolute_import, division, print_function, unicode_literals ) from six import text_type 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(text_type, 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(text_type, allow_none=False) # ------------------------------------------------------------------------ # object interface # ------------------------------------------------------------------------ def __init__(self, values, **traits): value = traits.pop('value', values[0]) traits['values'] = values super(MComboField, self).__init__(**traits) self.value = value # ------------------------------------------------------------------------ # Private interface # ------------------------------------------------------------------------ def _initialize_control(self): super(MComboField, self)._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(MComboField, self)._add_event_listeners() self.on_trait_change(self._values_updated, 'values[],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.on_trait_change(self._values_updated, 'values[],formatter', dispatch='ui', remove=True) super(MComboField, self)._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): if self.control is not None: self._set_control_values(self.values) pyface-6.1.2/pyface/fields/i_text_field.py0000644000076500000240000001405313462774551021477 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2017-19, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The text field interface. """ from __future__ import ( absolute_import, division, print_function, unicode_literals ) from traits.api import Bool, Enum, HasTraits, Unicode from pyface.fields.i_field import IField class ITextField(IField): """ The text field interface. """ #: The value held by the field. value = Unicode #: 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 = Unicode #: 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 = Unicode #: 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 = Unicode #: 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(MTextField, self)._initialize_control() def _add_event_listeners(self): """ Set up toolkit-specific bindings for events """ super(MTextField, self)._add_event_listeners() self.on_trait_change(self._update_text_updated, 'update_text', dispatch='ui') self.on_trait_change(self._placeholder_updated, 'placeholder', dispatch='ui') self.on_trait_change(self._echo_updated, 'echo', dispatch='ui') self.on_trait_change(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.on_trait_change(self._update_text_updated, 'update_text', dispatch='ui', remove=True) self.on_trait_change(self._placeholder_updated, 'placeholder', dispatch='ui', remove=True) self.on_trait_change(self._echo_updated, 'echo', dispatch='ui', remove=True) self.on_trait_change(self._read_only_updated, 'read_only', dispatch='ui', remove=True) super(MTextField, self)._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): if self.control is not None: self._set_control_placeholder(self.placeholder) def _echo_updated(self): if self.control is not None: self._set_control_echo(self.echo) def _read_only_updated(self): if self.control is not None: self._set_control_read_only(self.read_only) def _update_text_updated(self, new): """ Change how we listen to for updates to text value. """ if self.control is not None: if 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-6.1.2/pyface/fields/i_field.py0000644000076500000240000001425213462774551020434 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2017-19, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The text field interface. """ from __future__ import ( absolute_import, division, print_function, unicode_literals ) from traits.api import Any, HasTraits, Instance, Unicode from pyface.i_widget import IWidget class IField(IWidget): """ 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 #: A tooltip for the field. tooltip = Unicode #: An optional context menu for the field. context_menu = Instance('pyface.action.menu_manager.MenuManager') def show_context_menu(self, x, y): """ Create and show the context menu at a position. """ def _initialize_control(self, control): """ Perform any toolkit-specific initialization for the control. """ class MField(HasTraits): """ The field mix-in. """ #: The value held by the field. value = Any #: A tooltip for the field. tooltip = Unicode #: An optional context menu for the field. context_menu = Instance('pyface.action.menu_manager.MenuManager') # ------------------------------------------------------------------------ # IWidget interface # ------------------------------------------------------------------------ def _add_event_listeners(self): """ Set up toolkit-specific bindings for events """ super(MField, self)._add_event_listeners() self.on_trait_change(self._value_updated, 'value', dispatch='ui') self.on_trait_change(self._tooltip_updated, 'tooltip', dispatch='ui') self.on_trait_change(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.on_trait_change(self._value_updated, 'value', dispatch='ui', remove=True) self.on_trait_change(self._tooltip_updated, 'tooltip', dispatch='ui', remove=True) self.on_trait_change(self._context_menu_updated, 'context_menu', dispatch='ui', remove=True) super(MField, self)._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. """ self.control = self._create_control(self.parent) self._initialize_control() self._add_event_listeners() self.show(self.visible) self.enable(self.enabled) def _initialize_control(self): """ Perform any toolkit-specific initialization for the control. """ self._set_control_tooltip(self.tooltip) 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 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 control menu observer. This should use _handle_control_context_menu as the event handler. """ raise NotImplementedError def _handle_control_context_menu(self, event): """ Handle a context menu event. This should call show_context_menu with appropriate position x and y arguments. The function signature will likely vary from toolkit to toolkit. """ raise NotImplementedError # Trait change handlers ------------------------------------------------- def _value_updated(self, value): if self.control is not None: self._set_control_value(value) def _tooltip_updated(self, tooltip): if self.control is not None: self._set_control_tooltip(tooltip) def _context_menu_updated(self, old, new): if self.control is not None: if new is None: self._observe_control_context_menu(remove=True) if old is None: self._observe_control_context_menu() pyface-6.1.2/pyface/fields/tests/0000755000076500000240000000000013515277235017621 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/fields/tests/field_mixin.py0000644000076500000240000000376513462774551022501 0ustar cwebsterstaff00000000000000# Copyright (c) 2019, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! from __future__ import ( absolute_import, division, print_function, unicode_literals ) from traits.testing.unittest_tools import UnittestTools from pyface.action.api import Action, MenuManager from pyface.gui import GUI from pyface.window import Window class FieldMixin(UnittestTools): """ Mixin which provides standard methods for all fields. """ def setUp(self): self.gui = GUI() self.parent = Window() 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_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 # Tests ------------------------------------------------------------------ def test_field_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_field_menu(self): self._create_widget_control() self.widget.menu = MenuManager(Action(name='Test'), name='Test') self.gui.process_events() pyface-6.1.2/pyface/fields/tests/test_spin_field.py0000644000076500000240000000325313500710640023333 0ustar cwebsterstaff00000000000000# Copyright (c) 2019, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! from __future__ import ( absolute_import, division, print_function, unicode_literals ) 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-6.1.2/pyface/fields/tests/test_combo_field.py0000644000076500000240000000717413500710640023467 0ustar cwebsterstaff00000000000000# Copyright (c) 2019, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! from __future__ import ( absolute_import, division, print_function, unicode_literals ) import unittest from six import text_type 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 = text_type 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 = text_type 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 = text_type 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-6.1.2/pyface/fields/tests/test_text_field.py0000644000076500000240000000533613500710640023352 0ustar cwebsterstaff00000000000000# Copyright (c) 2019, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! from __future__ import ( absolute_import, division, print_function, unicode_literals ) 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-6.1.2/pyface/fields/tests/__init__.py0000644000076500000240000000000013462774551021724 0ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/fields/__init__.py0000644000076500000240000000000013462774551020562 0ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/fields/i_spin_field.py0000644000076500000240000001071213462774551021462 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2017-19, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The spin field interface. """ from __future__ import ( absolute_import, division, print_function, unicode_literals ) 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, depends_on='bounds') #: The maximum value maximum = Property(Int, depends_on='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, depends_on='bounds') #: The maximum value for the spinner maximum = Property(Int, depends_on='bounds') # ------------------------------------------------------------------------ # object interface # ------------------------------------------------------------------------ def __init__(self, **traits): value = traits.pop('value', None) if 'bounds' in traits: traits['value'] = traits['bounds'][0] super(MSpinField, self).__init__(**traits) if value is not None: self.value = value # ------------------------------------------------------------------------ # Private interface # ------------------------------------------------------------------------ def _initialize_control(self): super(MSpinField, self)._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(MSpinField, self)._add_event_listeners() self.on_trait_change(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.on_trait_change(self._bounds_updated, 'bounds', dispatch='ui', remove=True) super(MSpinField, self)._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): if self.control is not None: self._set_control_bounds(self.bounds) pyface-6.1.2/pyface/fields/combo_field.py0000644000076500000240000000122113462774551021273 0ustar cwebsterstaff00000000000000# Copyright (c) 2019, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: 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-6.1.2/pyface/fields/api.py0000644000076500000240000000132613462774551017610 0ustar cwebsterstaff00000000000000# Copyright (c) 2019, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: from .i_combo_field import IComboField from .i_field import IField from .i_spin_field import ISpinField from .i_text_field import ITextField from .combo_field import ComboField from .spin_field import SpinField from .text_field import TextFieldpyface-6.1.2/pyface/fields/text_field.py0000644000076500000240000000145413462774551021170 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2017, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The text field widget. """ # Import the toolkit specific version. from pyface.toolkit import toolkit_object TextField = toolkit_object('fields.text_field:TextField') pyface-6.1.2/pyface/fields/spin_field.py0000644000076500000240000000145413462774551021155 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2019, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The spin field widget. """ # Import the toolkit specific version. from pyface.toolkit import toolkit_object SpinField = toolkit_object('fields.spin_field:SpinField') pyface-6.1.2/pyface/beep.py0000644000076500000240000000032513462774551016502 0ustar cwebsterstaff00000000000000# Copyright 2012 Philip Chimento """Sound the system bell.""" # Import the toolkit-specific version from __future__ import absolute_import from .toolkit import toolkit_object beep = toolkit_object('beep:beep') pyface-6.1.2/pyface/image_list.py0000644000076500000240000000115413462774551017705 0ustar cwebsterstaff00000000000000# Copyright (c) 2017, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! import logging logger = logging.getLogger(__name__) logger.warning( 'DEPRECATED: pyface.image_list, use pyface.ui.wx.image_list instead. ' 'Will be removed in Pyface 7.') from pyface.ui.wx.image_list import * pyface-6.1.2/pyface/i_window.py0000644000076500000240000002255713462774551017421 0ustar cwebsterstaff00000000000000# Copyright (c) 2005-18, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: """ The abstract interface for all pyface top-level windows. """ # Enthought library imports. from traits.api import Event, Tuple, Unicode, Vetoable, VetoableEvent # Local imports. 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 = Unicode # 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: Unicode 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='' ): """ 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. """ def warning(self, message, title='Warning', detail='', informative=''): """ 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. """ def error(self, message, title='Error', detail='', informative=''): """ 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. """ class MWindow(object): """ 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='' ): """ 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. """ from .message_dialog import information information(self.control, message, title, detail, informative) def warning(self, message, title='Warning', detail='', informative=''): """ 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. """ from .message_dialog import warning warning(self.control, message, title, detail, informative) def error(self, message, title='Error', detail='', informative=''): """ 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. """ from .message_dialog import error error(self.control, message, title, detail, informative) pyface-6.1.2/pyface/xrc_dialog.py0000644000076500000240000000115413462774552017704 0ustar cwebsterstaff00000000000000# Copyright (c) 2017, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! import logging logger = logging.getLogger(__name__) logger.warning( 'DEPRECATED: pyface.xrc_dialog, use pyface.ui.wx.xrc_dialog instead. ' 'Will be removed in Pyface 7.') from pyface.ui.wx.xrc_dialog import * pyface-6.1.2/pyface/constant.py0000644000076500000240000000153213462774551017421 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Common constants. """ # Standard results for Ok/Cancel, Yes/No operations etc. OK = 10 CANCEL = 20 YES = 30 NO = 40 #### EOF ###################################################################### pyface-6.1.2/pyface/i_splash_screen.py0000644000076500000240000000634513462774551020740 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The interface for a splash screen. """ # Standard library imports. import logging # Enthought library imports. from traits.api import Any, Bool, Instance, Int, Tuple, Unicode # Local imports. from pyface.image_resource import ImageResource 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 = Instance(ImageResource, ImageResource('splash')) #: 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 = Unicode #: 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(object): """ 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(MSplashScreen, self).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(MSplashScreen, self).close() pyface-6.1.2/pyface/about_dialog.py0000644000076500000240000000166513462774551020230 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The implementation of a simple 'About' dialog. """ from __future__ import absolute_import # Import the toolkit specific version. from .toolkit import toolkit_object AboutDialog = toolkit_object('about_dialog:AboutDialog') ### EOF ####################################################################### pyface-6.1.2/pyface/i_dialog.py0000644000076500000240000001430413462774551017340 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The abstract interface for all pyface dialogs. """ # Enthought library imports. from traits.api import Bool, Enum, Int, Str, Unicode # Local imports. 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 = Unicode #: 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 = Unicode #: The label for the 'ok' button. The default is toolkit specific. ok_label = Unicode #: 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(object): """ 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(MDialog, self)._create() self._create_contents(self.control) ########################################################################### # Protected 'IWindow' interface. ########################################################################### def _add_event_listeners(self): """ Adds any event listeners required by the window. """ # We don't bother for dialogs. pass pyface-6.1.2/pyface/i_directory_dialog.py0000644000076500000240000000340313462774551021422 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The interface for a dialog that allows the user to browse for a directory. """ # Enthought library imports. from traits.api import Bool, Unicode # Local imports. 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 = Unicode #: The message to display in the dialog. The default is toolkit specific. message = Unicode #: 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 = Unicode class MDirectoryDialog(object): """ The mixin class that contains common code for toolkit specific implementations of the IDirectoryDialog interface. """ pyface-6.1.2/pyface/python_editor.py0000644000076500000240000000165413462774551020464 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A widget for editing Python code. """ # Import the toolkit specific version. from __future__ import absolute_import from .toolkit import toolkit_object PythonEditor = toolkit_object('python_editor:PythonEditor') #### EOF ###################################################################### pyface-6.1.2/pyface/i_heading_text.py0000644000076500000240000000261413462774551020545 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Heading text. """ # Enthought library imports. from traits.api import Instance, Int, Interface, Unicode # Local imports. from pyface.i_image_resource import IImageResource class IHeadingText(Interface): """ Heading text. """ #### 'IHeadingText' interface ############################################# #: Heading level. # # fixme: Currently we ignore anything but one, but in future we could # have different visualizations based on the level. level = Int(1) #: The heading text. text = Unicode('Default') #: The background image. image = Instance(IImageResource) class MHeadingText(object): """ The mixin class that contains common code for toolkit specific implementations of the IHeadingText interface. """ pyface-6.1.2/pyface/clipboard.py0000644000076500000240000000161713462774551017533 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2009, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Evan Patterson # Date: 06/26/09 #------------------------------------------------------------------------------ """ The interface for manipulating the toolkit clipboard. """ # Import the toolkit specific version from __future__ import absolute_import from .toolkit import toolkit_object Clipboard = toolkit_object('clipboard:Clipboard') # Create a singleton clipboard object for convenience clipboard = Clipboard() pyface-6.1.2/pyface/resource/0000755000076500000240000000000013515277235017040 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/resource/resource_path.py0000644000076500000240000000312113462774551022256 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005-2009 Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! #------------------------------------------------------------------------------ """ 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) #### EOF ###################################################################### pyface-6.1.2/pyface/resource/resource_manager.py0000644000076500000240000002166213462774551022746 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005-2009, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! #------------------------------------------------------------------------------ """ The default resource manager. A resource manager locates and loads application resources such as images and sounds etc. """ # Standard library imports. import collections, glob, inspect, os, sys, types from os.path import join from zipfile import is_zipfile, ZipFile # Enthought library imports. from traits.api import HasTraits, Instance, List from traits.util.resource import get_path # Local imports. from pyface.resource.resource_factory import ResourceFactory from pyface.resource.resource_reference import ImageReference import six 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. """ if not isinstance(path, collections.Sequence): path = [path] resource_path = [] for item in list(path) + self.extra_paths: if isinstance(item, six.string_types): 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', ''] for dirname in resource_path: # If we come across a reference to a module, use pkg_resources # to try and find the image inside of an .egg, .zip, etc. if isinstance(dirname, types.ModuleType): from pkg_resources import resource_string for path in subdirs: for extension in extensions: searchpath = '%s/%s%s' % (path, basename, extension) try: data = resource_string(dirname.__name__, searchpath) return ImageReference(self.resource_factory, data = data) except IOError: pass else: continue # 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 #### EOF ###################################################################### pyface-6.1.2/pyface/resource/__init__.py0000644000076500000240000000030313462774551021151 0ustar cwebsterstaff00000000000000# Copyright (c) 2005-2011, Enthought, Inc. # All rights reserved. """ Support for managing resources such as images and sounds. Part of the TraitsGUI project of the Enthought Tool Suite. """ pyface-6.1.2/pyface/resource/resource_reference.py0000644000076500000240000000517513462774551023273 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Resource references. """ # Enthought library imports. from traits.api import Any, HasTraits, Instance # Local imports. 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 #### EOF ###################################################################### pyface-6.1.2/pyface/resource/api.py0000644000076500000240000000146713462774551020177 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005-2015, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ from __future__ import absolute_import from .resource_factory import ResourceFactory from .resource_manager import ResourceManager from .resource_path import resource_path pyface-6.1.2/pyface/resource/resource_factory.py0000644000076500000240000000245213462774551022777 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ Default base-class for resource factories. """ class ResourceFactory: """ 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 NotImplemented def image_from_data(self, data): """ Creates an image from the specified data. """ raise NotImplemented #### EOF ###################################################################### pyface-6.1.2/pyface/mimedata.py0000644000076500000240000000033713462774551017353 0ustar cwebsterstaff00000000000000# 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-6.1.2/pyface/i_widget.py0000644000076500000240000001005513462774551017363 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The base interface for all pyface widgets. """ # Enthought library imports. from traits.api import Any, Bool, HasTraits, Interface 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) # ------------------------------------------------------------------------ # 'IWidget' interface. # ------------------------------------------------------------------------ def show(self, visible): """ Show or hide the widget. Parameter --------- visible : bool Visible should be ``True`` if the widget should be shown. """ def enable(self, enabled): """ Enable or disable the widget. Parameter --------- enabled : bool The enabled state to set the widget to. """ def destroy(self): """ Destroy the control if it exists. """ # ------------------------------------------------------------------------ # 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. """ 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(object): """ The mixin class that contains common code for toolkit specific implementations of the IWidget interface. """ # ------------------------------------------------------------------------ # 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._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 _add_event_listeners(self): """ Set up toolkit-specific bindings for events """ pass def _remove_event_listeners(self): """ Remove toolkit-specific bindings for events """ pass pyface-6.1.2/pyface/mdi_window_menu.py0000644000076500000240000001311613462774551020755 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A menu that mimics the standard MDI window menu. This is the menu that has the tile/cascade actions etc. """ # Enthought library imports. from traits.api import Str # Local imports. 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() return 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() return 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() return 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() return 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() return 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() return 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() return 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(MDIWindowMenu, self).__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' ) #### EOF ###################################################################### pyface-6.1.2/pyface/i_clipboard.py0000644000076500000240000000614713462774551020046 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2009, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Evan Patterson # Date: 06/26/09 #------------------------------------------------------------------------------ """ The interface for manipulating the toolkit clipboard. """ try: from collections.abc import Sequence except ImportError: # Python 3.8 deprecation from collections import Sequence # ETS imports from traits.api import HasStrictTraits, Interface, Property import six 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, six.string_types): 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-6.1.2/pyface/gui.py0000644000076500000240000000162213462774551016354 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The implementation of a pyface GUI. """ # Import the toolkit specific version. from __future__ import absolute_import from .toolkit import toolkit_object GUI = toolkit_object('gui:GUI') #### EOF ###################################################################### pyface-6.1.2/pyface/image_cache.py0000644000076500000240000000165113462774551017777 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The implementation of an image cache. """ from __future__ import absolute_import # Import the toolkit specific version. from .toolkit import toolkit_object ImageCache = toolkit_object('image_cache:ImageCache') #### EOF ###################################################################### pyface-6.1.2/pyface/file_dialog.py0000644000076500000240000000172013462774551020025 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ The implementation of a dialog that allows the user to open/save files etc. """ # Import the toolkit specific version. from __future__ import absolute_import from .toolkit import toolkit_object FileDialog = toolkit_object('file_dialog:FileDialog') #### EOF ###################################################################### pyface-6.1.2/pyface/dock/0000755000076500000240000000000013515277235016131 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/dock/dock_sizer.py0000644000076500000240000045702113500704207020634 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------- # # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: David C. Morrill # Date: 10/18/2005 # #------------------------------------------------------------------------------- """ 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. """ #------------------------------------------------------------------------------- # Imports: #------------------------------------------------------------------------------- from __future__ import print_function import wx, sys from traits.api import HasPrivateTraits, Instance, Str, Int, List, Enum, \ Tuple, Any, Range, Property, Callable, Constant, Event, Undefined, Bool, \ cached_property 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.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: wx_26 = (wx.__version__[:3] == '2.6') 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(bg_color[0]*255, bg_color[1]*255, bg_color[2]*255) dx, dy = window.GetSizeTuple() 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.GetPositionTuple() dx, dy = window.GetSizeTuple() while True: window = window.GetParent() if window is None: break xw, yw = window.GetPositionTuple() dxw, dyw = window.GetSizeTuple() 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.ClientToScreenXY( 0, 0 ) return ( wx.ScreenDC(), x, y ) #------------------------------------------------------------------------------- # 'DockImages' class: #------------------------------------------------------------------------------- class DockImages ( HasPrivateTraits ): #--------------------------------------------------------------------------- # Trait definitions: #--------------------------------------------------------------------------- # Image for closing a tab: close_tab = Instance( ImageResource, ImageResource( 'close_tab' ) ) # Image for closing a drag bar: close_drag = Instance( ImageResource, ImageResource( 'close_drag' ) ) #--------------------------------------------------------------------------- # Initalizes the object: #--------------------------------------------------------------------------- def __init__ ( self, **traits ): """ Initializes the object. """ super( DockImages, self ).__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 DockWindow that owns this item: owner = Property( depends_on = 'parent' ) # Bounds of the item: bounds = Bounds # 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( depends_on = 'name' ) # Width of the item's tab: tab_width = Property( depends_on = '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 # The name of this item (implemented in subclasses): # name = Str # The control associated with this item (implemented in subclasses): # control = Instance( wx.Control ) #--------------------------------------------------------------------------- # Implementation of the 'owner' property: #--------------------------------------------------------------------------- def __init__(self, **kw): super(DockItem, self).__init__(**kw) @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 self.drag_bounds = ( x, y, min( x + dx, bx + bdx ) - x, 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.GetClippingBox() 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() dc2 = wx.MemoryDC() self._drag_bitmap = wx.EmptyBitmap( 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( color[0]*255, color[1]*255, 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() tab_color.Set(max(0, r-20), max(0, g-20), max(0, b-20)) else: r,g,b = tab_color.Get() 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: 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 = Instance( ImageResource, allow_none = True ) # 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: if wx_26: size = self.control.GetBestFittingSize() else: 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 'SetDimensions' call, but the # other code is needed for Linux/GTK which will not correctly process # the SetDimensions call if the min size is larger than the specified # size. So we temporarily set its min size to (0,0), do the # SetDimensions, 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.SetDimensions( 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: #--------------------------------------------------------------------------- def _activated_fired(self): """ 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: #--------------------------------------------------------------------------- def _feature_changed ( self ): """ Handles the 'feature_changed' trait being changed """ self.set_feature_mode() #--------------------------------------------------------------------------- # Handles the 'control' trait being changed: #--------------------------------------------------------------------------- def _control_changed ( self, old, new ): """ Handles the 'control' trait being changed. """ 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: #--------------------------------------------------------------------------- def _name_changed ( self ): """ Handles the 'name' trait being changed. """ self._tab_width = self._tab_name = None #--------------------------------------------------------------------------- # Handles the 'style' trait being changed: #--------------------------------------------------------------------------- def _style_changed ( self ): """ Handles the 'style' trait being changed. """ if self.parent is not None: self.parent._is_notebook = None #--------------------------------------------------------------------------- # Handles the 'image' trait being changed: #--------------------------------------------------------------------------- def _image_changed ( self ): """ Handles the 'image' trait being changed. """ self._image = None #--------------------------------------------------------------------------- # Handles the 'visible' trait being changed: #--------------------------------------------------------------------------- def _visible_changed ( self ): """ Handles the 'visible' trait being changed. """ if self.parent is not None: self.parent.show_hide( self ) #--------------------------------------------------------------------------- # Handles the 'dockable' trait being changed: #--------------------------------------------------------------------------- def _dockable_changed ( self, dockable ): """ Handles the 'dockable' trait being changed. """ 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: #--------------------------------------------------------------------------- def _initialized_changed( self ): """ 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( DockRegion, self ).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( DockRegion, self ).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( DockRegion, self ).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: #--------------------------------------------------------------------------- def _active_changed ( self, old, 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: #--------------------------------------------------------------------------- def _contents_changed ( self ): """ 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 def _contents_items_changed ( 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( DockSection, self ).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: #--------------------------------------------------------------------------- def _contents_changed ( self ): """ Handles the 'contents' trait being changed. """ for item in self.contents: item.parent = self self.calc_min( True ) self.modified = True def _contents_items_changed ( 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: #--------------------------------------------------------------------------- def _splitters_changed ( self ): """ Handles the 'splitters' trait being changed. """ for item in self.splitters: item.parent = self def _splitters_items_changed ( 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 ) def __init__(self, **kw): super(DockInfo, self).__init__(**kw) #--------------------------------------------------------------------------- # 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.EmptyBitmap( 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.PySizer ): #--------------------------------------------------------------------------- # Initializes the object: #--------------------------------------------------------------------------- def __init__ ( self, contents = None ): super( DockSizer, self ).__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.GetPositionTuple() dx, dy = self.GetSizeTuple() 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-6.1.2/pyface/dock/images/0000755000076500000240000000000013515277235017376 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/dock/images/feature_tool.png0000644000076500000240000000277513462774551022613 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-6.1.2/pyface/dock/images/tab_feature_drop.png0000644000076500000240000000071113462774551023414 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-6.1.2/pyface/dock/images/tab_feature_disabled.png0000644000076500000240000000075613462774551024230 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-6.1.2/pyface/dock/images/sh_top.png0000644000076500000240000000021713462774551021404 0ustar cwebsterstaff00000000000000PNG  IHDR× pHYs  ~AIDATx̹ Ƣl@eJʇ%' t/%"0D=,M4*f ؉x%DIENDB`pyface-6.1.2/pyface/dock/images/bar_feature_no_drop.png0000644000076500000240000000041313462774551024105 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-6.1.2/pyface/dock/images/sv_left.png0000644000076500000240000000023313462774551021550 0ustar cwebsterstaff00000000000000PNG  IHDRL pHYs  ~MIDATxѫ C+FAV8`j2^' , W!-.0{`?29}x]yIENDB`pyface-6.1.2/pyface/dock/images/tab_scroll_r.png0000644000076500000240000000035113462774551022554 0ustar cwebsterstaff00000000000000PNG  IHDRkz pHYsodIDATx  BW(t{ljbbAϻ"B4a+yF;?wNp9g"RԾib QpOQ瘨p$Ro`]E#\EqY*YUNZ>WS!W&X 9IENDB`pyface-6.1.2/pyface/dock/images/bar_feature_changed.png0000644000076500000240000000044413462774551024042 0ustar cwebsterstaff00000000000000PNG  IHDRRW pHYsodIDATx4zQ) 7# 0   ƭ] E_V#\VTa-  , 2)z\ ')$22+1d3QIENDB`pyface-6.1.2/pyface/dock/images/sh_bottom.png0000644000076500000240000000021213462774551022101 0ustar cwebsterstaff00000000000000PNG  IHDR× pHYs  ~<\T*kZ\.?MÉ)@>9;+븆38͕/1E ]q^6,Tv& =L*" IENDB`pyface-6.1.2/pyface/dock/images/tab_feature_changed.png0000644000076500000240000000063713462774551024050 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-6.1.2/pyface/dock/images/tab_feature_normal.png0000644000076500000240000000063413462774551023744 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-6.1.2/pyface/dock/images/shell.ico0000644000076500000240000000257613462774551021217 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-6.1.2/pyface/dock/images/tab_scroll_l.png0000644000076500000240000000035213462774551022547 0ustar cwebsterstaff00000000000000PNG  IHDRkz pHYsodIDATx Pn!cBW(t cbA?! "MZbuF;?NpDDr-.[=N\ u[6'DqQjkCį8H-)ZW!D+5(\48n9c\',eQZrTkN717v$EIENDB`pyface-6.1.2/pyface/dock/images/close_drag.png0000644000076500000240000000107113462774551022211 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-6.1.2/pyface/dock/images/close_tab.png0000644000076500000240000000124213462774551022042 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-6.1.2/pyface/dock/images/bar_feature_normal.png0000644000076500000240000000044413462774551023741 0ustar cwebsterstaff00000000000000PNG  IHDRRW pHYsodIDATx4si c7#=ў]F MՒ]W8V\)Y a|-8=lE~s%X;<`UIENDB`pyface-6.1.2/pyface/dock/images/sh_middle.png0000644000076500000240000000023213462774551022035 0ustar cwebsterstaff00000000000000PNG  IHDR"zL pHYs  ~LIDATxc?q_bBjfj:$9dGP0 7rX'+a[<9H!MNB%b5t8f z,i} n3 "IENDB`pyface-6.1.2/pyface/dock/images/bar_feature_disabled.png0000644000076500000240000000037713462774551024225 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-6.1.2/pyface/dock/dock_window_shell.py0000644000076500000240000001662613500704207022200 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------- # # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: David C. Morrill # Date: 11/04/2005 # #------------------------------------------------------------------------------- """ Defines the DockWindowShell class used to house drag and drag DockWindow items that are dropped on the desktop or on the DockWindowShell window. """ #------------------------------------------------------------------------------- # Imports: #------------------------------------------------------------------------------- import wx # Fixme: Hack to force 'image_slice' to be added via Category to Theme class: import traitsui.wx from traits.api import HasPrivateTraits, Instance from traitsui.api import View, Group 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( DockWindowShell, self ).__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 ) wx.EVT_CLOSE( shell, 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.GetPositionTuple() x, y = old_control.GetParent().ClientToScreenXY( x, y ) dx, dy = old_control.GetSize() 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.SetDimensions( x, y, dx, dy ) cdx, cdy = shell.GetClientSizeTuple() ex_dx = dx - cdx ex_dy = dy - cdy shell.SetDimensions( 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() pyface-6.1.2/pyface/dock/__init__.py0000644000076500000240000000177213462774551020255 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------- # # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: David C. Morrill # Date: 10/18/2005 # #------------------------------------------------------------------------------- """ 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-6.1.2/pyface/dock/idock_ui_provider.py0000644000076500000240000000272713462774551022217 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------- # # Copyright (c) 2006, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: David C. Morrill # Date: 06/17/2006 # #------------------------------------------------------------------------------- """ 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-6.1.2/pyface/dock/api.py0000644000076500000240000000317213500704207017243 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------- # # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: David C. Morrill # Date: 10/18/2005 # #------------------------------------------------------------------------------- """ 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. """ #------------------------------------------------------------------------------- # Imports: #------------------------------------------------------------------------------- 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-6.1.2/pyface/dock/idockable.py0000644000076500000240000001161513462774551020430 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------- # # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: David C. Morrill # Date: 12/14/2005 # #------------------------------------------------------------------------------- """ 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. """ #------------------------------------------------------------------------------- # Imports: #------------------------------------------------------------------------------- import wx #------------------------------------------------------------------------------- # '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. """ print("The 'IDockable.dockable_get_control' method must be overridden") panel = wx.Panel( parent, -1 ) panel.SetBackgroundColour( wx.RED ) return panel #--------------------------------------------------------------------------- # 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-6.1.2/pyface/dock/feature_bar.py0000644000076500000240000004203413500704207020751 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------- # # Copyright (c) 2007, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: David C. Morrill # Date: 02/05/2007 # #------------------------------------------------------------------------------- """ Pyface 'FeatureBar' support. Defines the 'FeatureBar' class which displays and allows the user to interact with a set of DockWindowFeatures for a specified DockControl. """ #------------------------------------------------------------------------------- # Imports: #------------------------------------------------------------------------------- import wx from traits.api import HasPrivateTraits, Instance, Bool, Event, Color from pyface.wx.drag_and_drop import PythonDropTarget, PythonDropSource 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: wx.EVT_ERASE_BACKGROUND( control, self._erase_background ) # Set up the 'paint' event handler: wx.EVT_PAINT( control, self._paint ) # Set up mouse event handlers: wx.EVT_LEFT_DOWN( control, self._left_down ) wx.EVT_LEFT_UP( control, self._left_up ) wx.EVT_RIGHT_DOWN( control, self._right_down ) wx.EVT_RIGHT_UP( control, self._right_up ) wx.EVT_MOTION( control, self._mouse_move ) wx.EVT_ENTER_WINDOW( control, 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.GetSizeTuple() 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.GetSizeTuple() 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-6.1.2/pyface/dock/ifeature_tool.py0000644000076500000240000000602613462774551021354 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------- # # Copyright (c) 2007, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: David C. Morrill # Date: 02/07/2007 # #------------------------------------------------------------------------------- """ 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-6.1.2/pyface/dock/dock_window.py0000644000076500000240000014473413500704207021013 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------- # # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: David C. Morrill # Date: 10/18/2005 # #------------------------------------------------------------------------------- """ 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. """ #------------------------------------------------------------------------------- # Imports: #------------------------------------------------------------------------------- import shelve import os import wx import sys from pyface.api import SystemMetrics from traits.api \ import HasPrivateTraits, Instance, Tuple, Property, Any, Str, List, false 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.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 = 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( DockWindow, self ).__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: wx.EVT_PAINT( control, self._paint ) wx.EVT_SIZE( control, self._size ) # Set up mouse event handlers: wx.EVT_LEFT_DOWN( control, self._left_down ) wx.EVT_LEFT_UP( control, self._left_up ) wx.EVT_LEFT_DCLICK( control, self._left_dclick ) wx.EVT_RIGHT_DOWN( control, self._right_down ) wx.EVT_RIGHT_UP( control, self.right_up ) wx.EVT_MOTION( control, self._mouse_move ) wx.EVT_LEAVE_WINDOW( control, 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(color[0]*255, color[1]*255, color[2]*255) self.control.SetBackgroundColour( color ) #-- Default Trait Values --------------------------------------------------- def _theme_default ( self ): return dock_window_theme() #-- Trait Event Handlers --------------------------------------------------- def _theme_changed ( self, theme ): 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.StockCursor( 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. """ # There are cases where a layout has been scheduled for a DockWindow, # but then the DockWindow is destroyed, which will cause the calls # below to fail. So we catch the 'PyDeadObjectError' exception and # ignore it: try: self.control.Layout() self.control.Refresh() except wx.PyDeadObjectError: pass #--------------------------------------------------------------------------- # 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.on_trait_change( self._feature_bar_closed, 'completed' ) fb.dock_control = dock_control fb.show() #--------------------------------------------------------------------------- # Handles closing the feature bar: #--------------------------------------------------------------------------- def _feature_bar_closed ( self ): 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: wx.EVT_PAINT( control, None ) wx.EVT_SIZE( control, None ) wx.EVT_LEFT_DOWN( control, None ) wx.EVT_LEFT_UP( control, None ) wx.EVT_LEFT_DCLICK( control, None ) wx.EVT_RIGHT_DOWN( control, None ) wx.EVT_RIGHT_UP( control, None ) wx.EVT_MOTION( control, None ) wx.EVT_LEAVE_WINDOW( control, None ) #--------------------------------------------------------------------------- # 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.ContainsRect(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.GetSizeTuple() 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 #--------------------------------------------------------------------------- # 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.PopupMenuXY( 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( lambda l, r: cmp( l.name, r.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 = false id = Str # Style of drag bar/tab: style = DockStyle # Has the user set the style for this control: user_style = 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-6.1.2/pyface/dock/feature_tool.py0000644000076500000240000000361713500704207021166 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------- # # Copyright (c) 2006, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: David C. Morrill # Date: 07/04/2006 # #------------------------------------------------------------------------------- """ Implements the FeatureTool feature that allows a dragged object implementing the IFeatureTool interface to be dropped onto any compatible object. """ #------------------------------------------------------------------------------- # Imports: #------------------------------------------------------------------------------- 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-6.1.2/pyface/dock/dock_window_feature.py0000644000076500000240000010575213514577463022544 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------- # # Copyright (c) 2006, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # # Thanks for using Enthought open source! # # Author: David C. Morrill # Date: 07/03/2006 # #------------------------------------------------------------------------------- """ 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). """ #------------------------------------------------------------------------------- # Imports: #------------------------------------------------------------------------------- from weakref import ref from traits.api import HasPrivateTraits, Instance, Int, Str, Bool, Property from traitsui.menu import Menu, Action from pyface.timer.api import do_later from pyface.image_resource import ImageResource 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 = Instance( ImageResource, allow_none = True ) # 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.PopupMenuXY( 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: #--------------------------------------------------------------------------- def _image_changed ( self ): 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-6.1.2/pyface/grid/0000755000076500000240000000000013515277235016136 5ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/grid/composite_grid_model.py0000644000076500000240000000026513462774551022706 0ustar cwebsterstaff00000000000000import 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 * pyface-6.1.2/pyface/grid/grid.py0000644000076500000240000000024513462774551017442 0ustar cwebsterstaff00000000000000import logging logger = logging.getLogger(__name__) logger.warning('DEPRECATED: pyface.grid, use pyface.ui.wx.grid instead.') from pyface.ui.wx.grid.grid import * pyface-6.1.2/pyface/grid/simple_grid_model.py0000644000076500000240000000026213462774551022172 0ustar cwebsterstaff00000000000000import 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 * pyface-6.1.2/pyface/grid/trait_grid_cell_adapter.py0000644000076500000240000000027013462774551023342 0ustar cwebsterstaff00000000000000import 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 * pyface-6.1.2/pyface/grid/grid_cell_image_renderer.py0000644000076500000240000000027113462774551023470 0ustar cwebsterstaff00000000000000import 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 * pyface-6.1.2/pyface/grid/__init__.py0000644000076500000240000000000013462774551020241 0ustar cwebsterstaff00000000000000pyface-6.1.2/pyface/grid/combobox_focus_handler.py0000644000076500000240000000026713462774551023225 0ustar cwebsterstaff00000000000000import 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 * pyface-6.1.2/pyface/grid/grid_cell_renderer.py0000644000076500000240000000026313462774551022327 0ustar cwebsterstaff00000000000000import 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 * pyface-6.1.2/pyface/grid/edit_renderer.py0000644000076500000240000000025613462774551021332 0ustar cwebsterstaff00000000000000import logging logger = logging.getLogger(__name__) logger.warning('DEPRECATED: pyface.grid, use pyface.ui.wx.grid instead.') from pyface.ui.wx.grid.edit_renderer import * pyface-6.1.2/pyface/grid/mapped_grid_cell_image_renderer.py0000644000076500000240000000030013462774551025007 0ustar cwebsterstaff00000000000000import 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 * pyface-6.1.2/pyface/grid/checkbox_renderer.py0000644000076500000240000000026213462774551022170 0ustar cwebsterstaff00000000000000import logging logger = logging.getLogger(__name__) logger.warning('DEPRECATED: pyface.grid, use pyface.ui.wx.grid instead.') from pyface.ui.wx.grid.checkbox_renderer import * pyface-6.1.2/pyface/grid/api.py0000644000076500000240000000024413462774551017265 0ustar cwebsterstaff00000000000000import logging logger = logging.getLogger(__name__) logger.warning('DEPRECATED: pyface.grid, use pyface.ui.wx.grid instead.') from pyface.ui.wx.grid.api import * pyface-6.1.2/pyface/grid/trait_grid_model.py0000644000076500000240000000026113462774551022023 0ustar cwebsterstaff00000000000000import 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 * pyface-6.1.2/pyface/grid/grid_model.py0000644000076500000240000000025313462774551020621 0ustar cwebsterstaff00000000000000import logging logger = logging.getLogger(__name__) logger.warning('DEPRECATED: pyface.grid, use pyface.ui.wx.grid instead.') from pyface.ui.wx.grid.grid_model import * pyface-6.1.2/pyface/grid/checkbox_image_renderer.py0000644000076500000240000000027013462774551023331 0ustar cwebsterstaff00000000000000import 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 * pyface-6.1.2/pyface/grid/edit_image_renderer.py0000644000076500000240000000026413462774551022473 0ustar cwebsterstaff00000000000000import 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 * pyface-6.1.2/pyface/grid/inverted_grid_model.py0000644000076500000240000000026413462774551022523 0ustar cwebsterstaff00000000000000import 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 * pyface-6.1.2/pyface/split_application_window.py0000644000076500000240000000314313462774551022675 0ustar cwebsterstaff00000000000000#------------------------------------------------------------------------------ # Copyright (c) 2005, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD # license included in enthought/LICENSE.txt and may be redistributed only # under the conditions described in the aforementioned license. The license # is also available online at http://www.enthought.com/licenses/BSD.txt # Thanks for using Enthought open source! # # Author: Enthought, Inc. # Description: #------------------------------------------------------------------------------ """ A window that is split in two either horizontally or vertically. """ # Local imports. 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-6.1.2/doc-src-requirements.txt0000644000076500000240000000012613463043737020555 0ustar cwebsterstaff00000000000000git+http://github.com/enthought/enthought-sphinx-theme.git#egg=enthought_sphinx_theme pyface-6.1.2/tox-requirements.txt0000644000076500000240000000024113462774552020040 0ustar cwebsterstaff00000000000000mock nose numpy pygments traits git+http://github.com/enthought/traitsui.git#egg=traitsui traits_enaml ; python_version == '2.7' enaml ; python_version == '2.7' pyface-6.1.2/MANIFEST.in0000644000076500000240000000034013462774551015501 0ustar cwebsterstaff00000000000000include *.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-6.1.2/docs/0000755000076500000240000000000013515277231014666 5ustar cwebsterstaff00000000000000pyface-6.1.2/docs/DockWindowFeature.pdf0000644000076500000240000537170513462774551020777 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-6.1.2/docs/DockWindow.ppt0000644000076500000240000615100013462774551017476 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