From 1f780e48af479e7bf9a07eaaa1ab6b41f1ffb17b Mon Sep 17 00:00:00 2001 From: Robin Dunn <robin@alldunn.com> Date: Fri, 8 Apr 2005 22:54:02 +0000 Subject: [PATCH] Added the ActiveGrid IDE as a sample application git-svn-id: https://svn.wxwidgets.org/svn/wx/wxWidgets/trunk@33440 c3d73ce0-8a6f-49c7-b76d-6d57e0e08775 --- wxPython/distrib/DIRLIST | 10 +- wxPython/distrib/make_installer.py | 15 +- wxPython/docs/CHANGES.txt | 1 + wxPython/samples/ide/ActiveGridIDE.py | 33 + wxPython/samples/ide/activegrid/__init__.py | 0 .../ide/activegrid/tool/AboutDialog.py | 119 + .../ide/activegrid/tool/AbstractEditor.py | 641 +++++ .../samples/ide/activegrid/tool/CodeEditor.py | 1014 +++++++ .../ide/activegrid/tool/DebuggerHarness.py | 690 +++++ .../ide/activegrid/tool/DebuggerService.py | 2275 ++++++++++++++++ .../ide/activegrid/tool/FindInDirService.py | 446 ++++ .../ide/activegrid/tool/FindService.py | 521 ++++ .../samples/ide/activegrid/tool/HtmlEditor.py | 222 ++ wxPython/samples/ide/activegrid/tool/IDE.py | 2056 ++++++++++++++ .../ide/activegrid/tool/IDEFindService.py | 445 ++++ .../ide/activegrid/tool/ImageEditor.py | 93 + .../ide/activegrid/tool/MarkerService.py | 91 + .../ide/activegrid/tool/MessageService.py | 143 + .../ide/activegrid/tool/OutlineService.py | 520 ++++ .../samples/ide/activegrid/tool/PHPEditor.py | 297 +++ .../samples/ide/activegrid/tool/PerlEditor.py | 425 +++ .../ide/activegrid/tool/ProjectEditor.py | 1847 +++++++++++++ .../ide/activegrid/tool/PythonEditor.py | 614 +++++ .../ide/activegrid/tool/STCTextEditor.py | 1293 +++++++++ .../samples/ide/activegrid/tool/Service.py | 327 +++ .../samples/ide/activegrid/tool/TabbedView.py | 48 + .../samples/ide/activegrid/tool/UICommon.py | 115 + .../samples/ide/activegrid/tool/Wizard.py | 967 +++++++ .../samples/ide/activegrid/tool/XmlEditor.py | 165 ++ .../samples/ide/activegrid/tool/__init__.py | 0 .../samples/ide/activegrid/tool/checker.py | 896 +++++++ .../samples/ide/activegrid/tool/data/tips.txt | 7 + .../samples/ide/activegrid/tool/process.py | 2364 +++++++++++++++++ .../samples/ide/activegrid/util/__init__.py | 72 + .../samples/ide/activegrid/util/aglogging.py | 84 + .../ide/activegrid/util/cachedloader.py | 87 + .../ide/activegrid/util/dependencymgr.py | 145 + .../samples/ide/activegrid/util/fileutils.py | 39 + .../ide/activegrid/util/gettersetter.py | 36 + .../ide/activegrid/util/xmlmarshaller.py | 649 +++++ .../ide/activegrid/util/xmlmarshallertests.py | 183 ++ .../ide/activegrid/util/xmlprettyprinter.py | 63 + 42 files changed, 20049 insertions(+), 9 deletions(-) create mode 100644 wxPython/samples/ide/ActiveGridIDE.py create mode 100644 wxPython/samples/ide/activegrid/__init__.py create mode 100644 wxPython/samples/ide/activegrid/tool/AboutDialog.py create mode 100644 wxPython/samples/ide/activegrid/tool/AbstractEditor.py create mode 100644 wxPython/samples/ide/activegrid/tool/CodeEditor.py create mode 100644 wxPython/samples/ide/activegrid/tool/DebuggerHarness.py create mode 100644 wxPython/samples/ide/activegrid/tool/DebuggerService.py create mode 100644 wxPython/samples/ide/activegrid/tool/FindInDirService.py create mode 100644 wxPython/samples/ide/activegrid/tool/FindService.py create mode 100644 wxPython/samples/ide/activegrid/tool/HtmlEditor.py create mode 100644 wxPython/samples/ide/activegrid/tool/IDE.py create mode 100644 wxPython/samples/ide/activegrid/tool/IDEFindService.py create mode 100644 wxPython/samples/ide/activegrid/tool/ImageEditor.py create mode 100644 wxPython/samples/ide/activegrid/tool/MarkerService.py create mode 100644 wxPython/samples/ide/activegrid/tool/MessageService.py create mode 100644 wxPython/samples/ide/activegrid/tool/OutlineService.py create mode 100644 wxPython/samples/ide/activegrid/tool/PHPEditor.py create mode 100644 wxPython/samples/ide/activegrid/tool/PerlEditor.py create mode 100644 wxPython/samples/ide/activegrid/tool/ProjectEditor.py create mode 100644 wxPython/samples/ide/activegrid/tool/PythonEditor.py create mode 100644 wxPython/samples/ide/activegrid/tool/STCTextEditor.py create mode 100644 wxPython/samples/ide/activegrid/tool/Service.py create mode 100644 wxPython/samples/ide/activegrid/tool/TabbedView.py create mode 100644 wxPython/samples/ide/activegrid/tool/UICommon.py create mode 100644 wxPython/samples/ide/activegrid/tool/Wizard.py create mode 100644 wxPython/samples/ide/activegrid/tool/XmlEditor.py create mode 100644 wxPython/samples/ide/activegrid/tool/__init__.py create mode 100644 wxPython/samples/ide/activegrid/tool/checker.py create mode 100644 wxPython/samples/ide/activegrid/tool/data/tips.txt create mode 100644 wxPython/samples/ide/activegrid/tool/process.py create mode 100644 wxPython/samples/ide/activegrid/util/__init__.py create mode 100644 wxPython/samples/ide/activegrid/util/aglogging.py create mode 100644 wxPython/samples/ide/activegrid/util/cachedloader.py create mode 100644 wxPython/samples/ide/activegrid/util/dependencymgr.py create mode 100644 wxPython/samples/ide/activegrid/util/fileutils.py create mode 100644 wxPython/samples/ide/activegrid/util/gettersetter.py create mode 100644 wxPython/samples/ide/activegrid/util/xmlmarshaller.py create mode 100644 wxPython/samples/ide/activegrid/util/xmlmarshallertests.py create mode 100644 wxPython/samples/ide/activegrid/util/xmlprettyprinter.py diff --git a/wxPython/distrib/DIRLIST b/wxPython/distrib/DIRLIST index a4a065fc38..8a749638f0 100644 --- a/wxPython/distrib/DIRLIST +++ b/wxPython/distrib/DIRLIST @@ -61,10 +61,12 @@ wxPython/licence wxPython/samples wxPython/samples/StyleEditor wxPython/samples/docview -wxPython/samples/docview/activegrid -wxPython/samples/docview/activegrid/tool -wxPython/samples/docview/activegrid/tool/data -wxPython/samples/docview/activegrid/tool/images +wxPython/samples/pydocview +wxPython/samples/ide +wxPython/samples/ide/activegrid +wxPython/samples/ide/activegrid/tool +wxPython/samples/ide/activegrid/tool/data +wxPython/samples/ide/activegrid/util wxPython/samples/doodle wxPython/samples/embedded wxPython/samples/frogedit diff --git a/wxPython/distrib/make_installer.py b/wxPython/distrib/make_installer.py index ed0e57dfd4..96672b8ddf 100644 --- a/wxPython/distrib/make_installer.py +++ b/wxPython/distrib/make_installer.py @@ -409,11 +409,16 @@ Source: "samples\doodle\*.bat"; DestDir: "{app}\samples\doodle"; Source: "samples\doodle\sample.ddl"; DestDir: "{app}\samples\doodle"; Source: "samples\doodle\superdoodle.iss"; DestDir: "{app}\samples\doodle"; -Source: "samples\docview\*.py"; DestDir: "{app}\samples\docview"; -Source: "samples\docview\activegrid\*.py"; DestDir: "{app}\samples\docview\activegrid"; -Source: "samples\docview\activegrid\tool\*.py"; DestDir: "{app}\samples\docview\activegrid\tool"; -Source: "samples\docview\activegrid\tool\data\*.txt"; DestDir: "{app}\samples\docview\activegrid\tool\data"; -Source: "samples\docview\activegrid\tool\images\*.jpg"; DestDir: "{app}\samples\docview\activegrid\tool\images"; +Source: "samples\docview\*.py"; DestDir: "{app}\samples\docview"; +Source: "samples\pydocview\*.py"; DestDir: "{app}\samples\pydocview"; +Source: "samples\pydocview\*.jpg"; DestDir: "{app}\samples\pydocview"; +Source: "samples\pydocview\*.txt"; DestDir: "{app}\samples\pydocview"; + +Source: "samples\ide\*.py"; DestDir: "{app}\samples\ide"; +Source: "samples\ide\activegrid\*.py"; DestDir: "{app}\samples\ide\activegrid"; +Source: "samples\ide\activegrid\tool\*.py"; DestDir: "{app}\samples\ide\activegrid\tool"; +Source: "samples\ide\activegrid\tool\data\*.txt"; DestDir: "{app}\samples\ide\activegrid\tool\data"; +Source: "samples\ide\activegrid\util\*.py"; DestDir: "{app}\samples\ide\activegrid\util"; Source: "samples\embedded\*.py"; DestDir: "{app}\samples\embedded"; Source: "samples\embedded\*.cpp"; DestDir: "{app}\samples\embedded"; diff --git a/wxPython/docs/CHANGES.txt b/wxPython/docs/CHANGES.txt index 454eb6dbd2..426c834fc6 100644 --- a/wxPython/docs/CHANGES.txt +++ b/wxPython/docs/CHANGES.txt @@ -85,6 +85,7 @@ list of top-level windows that currently exist in the application. Updated docview library modules and sample apps from the ActiveGrid folks. +Added the ActiveGrid IDE as a sample application. diff --git a/wxPython/samples/ide/ActiveGridIDE.py b/wxPython/samples/ide/ActiveGridIDE.py new file mode 100644 index 0000000000..868acca0e7 --- /dev/null +++ b/wxPython/samples/ide/ActiveGridIDE.py @@ -0,0 +1,33 @@ +#---------------------------------------------------------------------------- +# Name: ActiveGridIDE.py +# Purpose: +# +# Author: Lawrence Bruhmuller +# +# Created: 3/30/05 +# CVS-ID: $Id$ +# Copyright: (c) 2004-2005 ActiveGrid, Inc. +# License: wxWindows License +#---------------------------------------------------------------------------- +import wx.lib.pydocview +import activegrid.tool.IDE + +import os +import sys +sys.stdout = sys.stderr + +# This is here as the base IDE entry point. Only difference is that -baseide is passed. + +sys.argv.append('-baseide'); + +# Put activegrid dir in path so python files can be found from py2exe +# This code should never do anything when run from the python interpreter +execDir = os.path.dirname(sys.executable) +try: + sys.path.index(execDir) +except ValueError: + sys.path.append(execDir) +app = activegrid.tool.IDE.IDEApplication(redirect = False) +app.GetTopWindow().Raise() # sometimes it shows up beneath other windows. e.g. running self in debugger +app.MainLoop() + diff --git a/wxPython/samples/ide/activegrid/__init__.py b/wxPython/samples/ide/activegrid/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/wxPython/samples/ide/activegrid/tool/AboutDialog.py b/wxPython/samples/ide/activegrid/tool/AboutDialog.py new file mode 100644 index 0000000000..ce1e0cdfa5 --- /dev/null +++ b/wxPython/samples/ide/activegrid/tool/AboutDialog.py @@ -0,0 +1,119 @@ +#---------------------------------------------------------------------------- +# Name: AboutDialog.py +# Purpose: AboutBox which has copyright notice, license information, and credits +# +# Author: Morgan Hua +# +# Created: 3/22/05 +# Copyright: (c) 2005 ActiveGrid, Inc. +# CVS-ID: $Id$ +# License: wxWindows License +#---------------------------------------------------------------------------- + +import wx +from IDE import ACTIVEGRID_BASE_IDE, getSplashBitmap +_ = wx.GetTranslation + +#---------------------------------------------------------------------------- +# Package License Data for AboutDialog +# Package, License, URL +# If no information is available, put a None as a place holder. +#---------------------------------------------------------------------------- + + +licenseData = [ + ("ActiveGrid", "ASL 2.0", "http://apache.org/licenses/LICENSE-2.0"), + ("Python 2.3", "Python Software Foundation License", "http://www.python.org/2.3/license.html"), + ("wxPython 2.5", "wxWidgets 2 - LGPL", "http://wxwidgets.org/newlicen.htm"), + ("wxWidgets", "wxWindows Library License 3", "http://www.wxwidgets.org/manuals/2.5.4/wx_wxlicense.html"), + ("pychecker", "MetaSlash - BSD", "http://pychecker.sourceforge.net/COPYRIGHT"), + ("process.py", "See file", "http://starship.python.net/~tmick/"), +] + +if not ACTIVEGRID_BASE_IDE: # add licenses for database connections only if not the base IDE + licenseData += [ + ("pydb2", "LGPL", "http://sourceforge.net/projects/pydb2"), + ("pysqlite", "Python License (CNRI)", "http://sourceforge.net/projects/pysqlite"), + ("mysql-python", "GPL, Python License (CNRI), Zope Public License", "http://sourceforge.net/projects/mysql-python"), + ("cx_Oracle", "Computronix", "http://http://www.computronix.com/download/License(cxOracle).txt"), + ("SQLite", "Public Domain", "http://www.sqlite.org/copyright.html"), + ("PyGreSQL", "BSD", "http://www.pygresql.org"), + ] + +if wx.Platform == '__WXMSW__': + licenseData += [("pywin32", "Python Software Foundation License", "http://sourceforge.net/projects/pywin32/")] + +class AboutDialog(wx.Dialog): + + def __init__(self, parent): + """ + Initializes the about dialog. + """ + wx.Dialog.__init__(self, parent, -1, _("About ") + wx.GetApp().GetAppName(), style = wx.DEFAULT_DIALOG_STYLE) + + nb = wx.Notebook(self, -1) + + aboutPage = wx.Panel(nb, -1) + sizer = wx.BoxSizer(wx.VERTICAL) + splash_bmp = getSplashBitmap() + image = wx.StaticBitmap(aboutPage, -1, splash_bmp, (0,0), (splash_bmp.GetWidth(), splash_bmp.GetHeight())) + sizer.Add(image, 0, wx.ALIGN_CENTER|wx.ALL, 0) + sizer.Add(wx.StaticText(aboutPage, -1, wx.GetApp().GetAppName() + _("\nVersion 0.6 Early Access\n\nCopyright (c) 2003-2005 ActiveGrid Incorporated and Contributors. All rights reserved.")), 0, wx.ALIGN_LEFT|wx.ALL, 10) + sizer.Add(wx.StaticText(aboutPage, -1, _("http://www.activegrid.com")), 0, wx.ALIGN_LEFT|wx.LEFT|wx.BOTTOM, 10) + aboutPage.SetSizer(sizer) + nb.AddPage(aboutPage, _("Copyright")) + + licensePage = wx.Panel(nb, -1) + grid = wx.grid.Grid(licensePage, -1) + grid.CreateGrid(len(licenseData), 2) + + dc = wx.ClientDC(grid) + dc.SetFont(grid.GetLabelFont()) + grid.SetColLabelValue(0, _("License")) + grid.SetColLabelValue(1, _("URL")) + w, maxHeight = dc.GetTextExtent(_("License")) + w, h = dc.GetTextExtent(_("URL")) + if h > maxHeight: + maxHeight = h + grid.SetColLabelSize(maxHeight + 6) # add a 6 pixel margin + + for row, data in enumerate(licenseData): + package = data[0] + license = data[1] + url = data[2] + if package: + grid.SetRowLabelValue(row, package) + if license: + grid.SetCellValue(row, 0, license) + if url: + grid.SetCellValue(row, 1, url) + + grid.EnableEditing(False) + grid.EnableDragGridSize(False) + grid.EnableDragColSize(False) + grid.EnableDragRowSize(False) + grid.SetRowLabelAlignment(wx.ALIGN_LEFT, wx.ALIGN_CENTRE) + grid.SetLabelBackgroundColour(wx.WHITE) + grid.AutoSizeColumn(0, 100) + grid.AutoSizeColumn(1, 100) + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(grid, 1, wx.EXPAND|wx.ALL, 10) + licensePage.SetSizer(sizer) + nb.AddPage(licensePage, _("Licenses")) + + creditsPage = wx.Panel(nb, -1) + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(wx.StaticText(creditsPage, -1, _("ActiveGrid Development Team:\n\nLawrence Bruhmuller\nEric Chu\nMatt Fryer\nJoel Hare\nMorgan Hua\nAlan Mullendore\nJeff Norton\nKevin Wang\nPeter Yared")), 0, wx.ALIGN_LEFT|wx.ALL, 10) + creditsPage.SetSizer(sizer) + nb.AddPage(creditsPage, _("Credits")) + + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(nb, 0, wx.ALIGN_CENTRE|wx.ALL, 5) + btn = wx.Button(self, wx.ID_OK) + sizer.Add(btn, 0, wx.ALIGN_CENTRE|wx.ALL, 5) + + self.SetSizer(sizer) + self.SetAutoLayout(True) + sizer.Fit(self) + + diff --git a/wxPython/samples/ide/activegrid/tool/AbstractEditor.py b/wxPython/samples/ide/activegrid/tool/AbstractEditor.py new file mode 100644 index 0000000000..8bda44696d --- /dev/null +++ b/wxPython/samples/ide/activegrid/tool/AbstractEditor.py @@ -0,0 +1,641 @@ +#---------------------------------------------------------------------------- +# Name: AbstractEditor.py +# Purpose: Non-text editor for DataModel and Process +# +# Author: Peter Yared, Morgan Hua +# +# Created: 7/28/04 +# CVS-ID: $Id$ +# Copyright: (c) 2004-2005 ActiveGrid, Inc. +# License: wxWindows License +#---------------------------------------------------------------------------- + + +import wx +import wx.lib.docview +import wx.lib.ogl as ogl +import PropertyService +_ = wx.GetTranslation + + +SELECT_BRUSH = wx.Brush("BLUE", wx.SOLID) +SHAPE_BRUSH = wx.Brush("WHEAT", wx.SOLID) +LINE_BRUSH = wx.BLACK_BRUSH + + +def GetRawModel(model): + if hasattr(model, "GetRawModel"): + rawModel = model.GetRawModel() + else: + rawModel = model + return rawModel + + +class CanvasView(wx.lib.docview.View): + + + #---------------------------------------------------------------------------- + # Overridden methods + #---------------------------------------------------------------------------- + + + def __init__(self, brush = SHAPE_BRUSH): + wx.lib.docview.View.__init__(self) + self._brush = brush + self._canvas = None + self._pt1 = None + self._pt2 = None + self._needEraseLasso = False + self._propShape = None + + + def OnCreate(self, doc, flags): + frame = wx.GetApp().CreateDocumentFrame(self, doc, flags) + frame.Show() + sizer = wx.BoxSizer() + self._CreateCanvas(frame) + sizer.Add(self._canvas, 1, wx.EXPAND, 0) + frame.SetSizer(sizer) + frame.Layout() + self.Activate() + return True + + + def OnActivateView(self, activate, activeView, deactiveView): + if activate and self._canvas: + # In MDI mode just calling set focus doesn't work and in SDI mode using CallAfter causes an endless loop + if self.GetDocumentManager().GetFlags() & wx.lib.docview.DOC_SDI: + self._canvas.SetFocus() + else: + wx.CallAfter(self._canvas.SetFocus) + + + def OnClose(self, deleteWindow = True): + statusC = wx.GetApp().CloseChildDocuments(self.GetDocument()) + statusP = wx.lib.docview.View.OnClose(self, deleteWindow = deleteWindow) + if hasattr(self, "ClearOutline"): + wx.CallAfter(self.ClearOutline) # need CallAfter because when closing the document, it is Activated and then Close, so need to match OnActivateView's CallAfter + if not (statusC and statusP): + return False + self.Activate(False) + if deleteWindow and self.GetFrame(): + self.GetFrame().Destroy() + return True + + + def _CreateCanvas(self, parent): + self._canvas = ogl.ShapeCanvas(parent) + wx.EVT_LEFT_DOWN(self._canvas, self.OnLeftClick) + wx.EVT_LEFT_UP(self._canvas, self.OnLeftUp) + wx.EVT_MOTION(self._canvas, self.OnLeftDrag) + wx.EVT_LEFT_DCLICK(self._canvas, self.OnLeftDoubleClick) + wx.EVT_KEY_DOWN(self._canvas, self.OnKeyPressed) + + maxWidth = 2000 + maxHeight = 16000 + self._canvas.SetScrollbars(20, 20, maxWidth / 20, maxHeight / 20) + + self._canvas.SetBackgroundColour(wx.WHITE) + self._diagram = ogl.Diagram() + self._canvas.SetDiagram(self._diagram) + self._diagram.SetCanvas(self._canvas) + + + def OnKeyPressed(self, event): + key = event.KeyCode() + if key == wx.WXK_DELETE: + self.OnClear(event) + else: + event.Skip() + + + def OnLeftClick(self, event): + self.EraseRubberBand() + + dc = wx.ClientDC(self._canvas) + self._canvas.PrepareDC(dc) + + # keep track of mouse down for group select + self._pt1 = event.GetLogicalPosition(dc) # this takes into account scrollbar offset + self._pt2 = None + + shape = self._canvas.FindShape(self._pt1[0], self._pt1[1])[0] + if shape: + self.BringToFront(shape) + + self._pt1 = None + event.Skip() # pass on event to shape handler to take care of selection + + return + elif event.ControlDown() or event.ShiftDown(): # extend select, don't deselect + pass + else: + # click on empty part of canvas, deselect everything + needRefresh = False + for shape in self._diagram.GetShapeList(): + if hasattr(shape, "GetModel"): + if shape.Selected(): + needRefresh = True + shape.Select(False, dc) + if needRefresh: + self._canvas.Redraw(dc) + + self.SetPropertyModel(None) + + if len(self.GetSelection()) == 0: + self.SetPropertyShape(None) + + + + def OnLeftDoubleClick(self, event): + propertyService = wx.GetApp().GetService(PropertyService.PropertyService) + if propertyService: + propertyService.ShowWindow() + + + def OnLeftDrag(self, event): + # draw lasso for group select + if self._pt1 and event.LeftIsDown(): # we are in middle of lasso selection + self.EraseRubberBand() + + dc = wx.ClientDC(self._canvas) + self._canvas.PrepareDC(dc) + self._pt2 = event.GetLogicalPosition(dc) # this takes into account scrollbar offset + self.DrawRubberBand() + else: + event.Skip() + + + def OnLeftUp(self, event): + # do group select + if self._needEraseLasso: + self.EraseRubberBand() + + dc = wx.ClientDC(self._canvas) + self._canvas.PrepareDC(dc) + x1, y1 = self._pt1 + x2, y2 = event.GetLogicalPosition(dc) # this takes into account scrollbar offset + + tol = self._diagram.GetMouseTolerance() + if abs(x1 - x2) > tol or abs(y1 - y2) > tol: + # make sure x1 < x2 and y1 < y2 to make comparison test easier + if x1 > x2: + temp = x1 + x1 = x2 + x2 = temp + if y1 > y2: + temp = y1 + y1 = y2 + y2 = temp + + for shape in self._diagram.GetShapeList(): + if not shape.GetParent() and hasattr(shape, "GetModel"): # if part of a composite, don't select it + x, y = shape.GetX(), shape.GetY() + width, height = shape.GetBoundingBoxMax() + selected = x1 < x - width/2 and x2 > x + width/2 and y1 < y - height/2 and y2 > y + height/2 + if event.ControlDown() or event.ShiftDown(): # extend select, don't deselect + if selected: + shape.Select(selected, dc) + else: # select items in lasso and deselect items out of lasso + shape.Select(selected, dc) + self._canvas.Redraw(dc) + else: + event.Skip() + else: + event.Skip() + + + def EraseRubberBand(self): + if self._needEraseLasso: + self._needEraseLasso = False + + dc = wx.ClientDC(self._canvas) + self._canvas.PrepareDC(dc) + dc.SetLogicalFunction(wx.XOR) + pen = wx.Pen(wx.Colour(200, 200, 200), 1, wx.SHORT_DASH) + dc.SetPen(pen) + brush = wx.Brush(wx.Colour(255, 255, 255), wx.TRANSPARENT) + dc.SetBrush(brush) + dc.ResetBoundingBox() + dc.BeginDrawing() + + x1, y1 = self._pt1 + x2, y2 = self._pt2 + + # make sure x1 < x2 and y1 < y2 + # this will make (x1, y1) = upper left corner + if x1 > x2: + temp = x1 + x1 = x2 + x2 = temp + if y1 > y2: + temp = y1 + y1 = y2 + y2 = temp + + # erase previous outline + dc.SetClippingRegion(x1, y1, x2 - x1, y2 - y1) + dc.DrawRectangle(x1, y1, x2 - x1, y2 - y1) + dc.EndDrawing() + + + def DrawRubberBand(self): + self._needEraseLasso = True + + dc = wx.ClientDC(self._canvas) + self._canvas.PrepareDC(dc) + dc.SetLogicalFunction(wx.XOR) + pen = wx.Pen(wx.Colour(200, 200, 200), 1, wx.SHORT_DASH) + dc.SetPen(pen) + brush = wx.Brush(wx.Colour(255, 255, 255), wx.TRANSPARENT) + dc.SetBrush(brush) + dc.ResetBoundingBox() + dc.BeginDrawing() + + x1, y1 = self._pt1 + x2, y2 = self._pt2 + + # make sure x1 < x2 and y1 < y2 + # this will make (x1, y1) = upper left corner + if x1 > x2: + temp = x1 + x1 = x2 + x2 = temp + if y1 > y2: + temp = y1 + y1 = y2 + y2 = temp + + # draw outline + dc.SetClippingRegion(x1, y1, x2 - x1, y2 - y1) + dc.DrawRectangle(x1, y1, x2 - x1, y2 - y1) + dc.EndDrawing() + + + def FindParkingSpot(self, width, height): + """ given a width and height, find a upper left corner where shape can be parked without overlapping other shape """ + offset = 30 # space between shapes + x = offset + y = offset + maxX = 700 # max distance to the right where we'll place tables + noParkingSpot = True + + while noParkingSpot: + point = self.isSpotOccupied(x, y, width, height) + if point: + x = point[0] + offset + if x > maxX: + x = offset + y = point[1] + offset + else: + noParkingSpot = False + + return x, y + + + def isSpotOccupied(self, x, y, width, height): + """ returns None if at x,y,width,height no object occupies that rectangle, + otherwise returns lower right corner of object that occupies given x,y position + """ + x2 = x + width + y2 = y + height + + for shape in self._diagram.GetShapeList(): + if isinstance(shape, ogl.RectangleShape) or isinstance(shape, ogl.EllipseShape): + if shape.GetParent() and isinstance(shape.GetParent(), ogl.CompositeShape): + # skip, part of a composite shape + continue + + if hasattr(shape, "GetModel"): + other_x, other_y, other_width, other_height = shape.GetModel().getEditorBounds() + other_x2 = other_x + other_width + other_y2 = other_y + other_height + else: + # shapes x,y are at the center of the shape, need to transform to upper left coordinate + other_width, other_height = shape.GetBoundingBoxMax() + other_x = shape.GetX() - other_width/2 + other_y = shape.GetY() - other_height/2 + + other_x2 = other_x + other_width + other_y2 = other_y + other_height + # intersection check + if ((other_x2 < other_x or other_x2 > x) and + (other_y2 < other_y or other_y2 > y) and + (x2 < x or x2 > other_x) and + (y2 < y or y2 > other_y)): + return (other_x2, other_y2) + return None + + + #---------------------------------------------------------------------------- + # Canvas methods + #---------------------------------------------------------------------------- + + def AddShape(self, shape, x = None, y = None, pen = None, brush = None, text = None, eventHandler = None): + if isinstance(shape, ogl.CompositeShape): + dc = wx.ClientDC(self._canvas) + self._canvas.PrepareDC(dc) + shape.Move(dc, x, y) + else: + shape.SetDraggable(True, True) + shape.SetCanvas(self._canvas) + + if x: + shape.SetX(x) + if y: + shape.SetY(y) + shape.SetCentreResize(False) + if pen: + shape.SetPen(pen) + if brush: + shape.SetBrush(brush) + if text: + shape.AddText(text) + shape.SetShadowMode(ogl.SHADOW_RIGHT) + self._diagram.AddShape(shape) + shape.Show(True) + if not eventHandler: + eventHandler = EditorCanvasShapeEvtHandler(self) + eventHandler.SetShape(shape) + eventHandler.SetPreviousHandler(shape.GetEventHandler()) + shape.SetEventHandler(eventHandler) + return shape + + + def RemoveShape(self, model = None, shape = None): + if not model and not shape: + return + + if not shape: + shape = self.GetShape(model) + + if shape: + shape.Select(False) + self._diagram.RemoveShape(shape) + if isinstance(shape, ogl.CompositeShape): + shape.RemoveFromCanvas(self._canvas) + + + def UpdateShape(self, model): + for shape in self._diagram.GetShapeList(): + if hasattr(shape, "GetModel") and shape.GetModel() == model: + x, y, w, h = model.getEditorBounds() + newX = x + w / 2 + newY = y + h / 2 + changed = False + if isinstance(shape, ogl.CompositeShape): + if shape.GetX() != newX or shape.GetY() != newY: + dc = wx.ClientDC(self._canvas) + self._canvas.PrepareDC(dc) + shape.SetSize(w, h, True) # wxBug: SetSize must be before Move because links won't go to the right place + shape.Move(dc, newX, newY) # wxBug: Move must be before SetSize because links won't go to the right place + changed = True + else: + oldw, oldh = shape.GetBoundingBoxMax() + oldx = shape.GetX() + oldy = shape.GetY() + if oldw != w or oldh != h or oldx != newX or oldy != newY: + shape.SetSize(w, h) + shape.SetX(newX) + shape.SetY(newY) + changed = True + if changed: + shape.ResetControlPoints() + self._canvas.Refresh() + break + + + def GetShape(self, model): + for shape in self._diagram.GetShapeList(): + if hasattr(shape, "GetModel") and shape.GetModel() == model: + return shape + return None + + + def GetSelection(self): + return filter(lambda shape: shape.Selected(), self._diagram.GetShapeList()) + + + def SetSelection(self, models, extendSelect = False): + dc = wx.ClientDC(self._canvas) + self._canvas.PrepareDC(dc) + update = False + if not isinstance(models, type([])) and not isinstance(models, type(())): + models = [models] + for shape in self._diagram.GetShapeList(): + if hasattr(shape, "GetModel"): + if shape.Selected() and not shape.GetModel() in models: # was selected, but not in new list, so deselect, unless extend select + if not extendSelect: + shape.Select(False, dc) + update = True + elif not shape.Selected() and shape.GetModel() in models: # was not selected and in new list, so select + shape.Select(True, dc) + update = True + elif extendSelect and shape.Selected() and shape.GetModel() in models: # was selected, but extend select means to deselect + shape.Select(False, dc) + update = True + if update: + self._canvas.Redraw(dc) + + + def BringToFront(self, shape): + if shape.GetParent() and isinstance(shape.GetParent(), ogl.CompositeShape): + self._diagram.RemoveShape(shape.GetParent()) + self._diagram.AddShape(shape.GetParent()) + else: + self._diagram.RemoveShape(shape) + self._diagram.AddShape(shape) + + + def SendToBack(self, shape): + if shape.GetParent() and isinstance(shape.GetParent(), ogl.CompositeShape): + self._diagram.RemoveShape(shape.GetParent()) + self._diagram.InsertShape(shape.GetParent()) + else: + self._diagram.RemoveShape(shape) + self._diagram.InsertShape(shape) + + + def ScrollVisible(self, shape): + xUnit, yUnit = shape._canvas.GetScrollPixelsPerUnit() + scrollX, scrollY = self._canvas.GetViewStart() # in scroll units + scrollW, scrollH = self._canvas.GetSize() # in pixels + w, h = shape.GetBoundingBoxMax() # in pixels + x = shape.GetX() - w/2 # convert to upper left coordinate from center + y = shape.GetY() - h/2 # convert to upper left coordinate from center + + if x >= scrollX*xUnit and x <= scrollX*xUnit + scrollW: # don't scroll if already visible + x = -1 + else: + x = x/xUnit + + if y >= scrollY*yUnit and y <= scrollY*yUnit + scrollH: # don't scroll if already visible + y = -1 + else: + y = y/yUnit + + self._canvas.Scroll(x, y) # in scroll units + + + def SetPropertyShape(self, shape): + # no need to highlight if no PropertyService is running + propertyService = wx.GetApp().GetService(PropertyService.PropertyService) + if not propertyService: + return + + if shape == self._propShape: + return + + if hasattr(shape, "GetPropertyShape"): + shape = shape.GetPropertyShape() + + dc = wx.ClientDC(self._canvas) + self._canvas.PrepareDC(dc) + dc.BeginDrawing() + + # erase old selection if it still exists + if self._propShape and self._propShape in self._diagram.GetShapeList(): + self._propShape.SetBrush(self._brush) + if (self._propShape._textColourName in ["BLACK", "WHITE"]): # Would use GetTextColour() but it is broken + self._propShape.SetTextColour("BLACK", 0) + self._propShape.Draw(dc) + + # set new selection + self._propShape = shape + + # draw new selection + if self._propShape and self._propShape in self._diagram.GetShapeList(): + self._propShape.SetBrush(SELECT_BRUSH) + if (self._propShape._textColourName in ["BLACK", "WHITE"]): # Would use GetTextColour() but it is broken + self._propShape.SetTextColour("WHITE", 0) + self._propShape.Draw(dc) + + dc.EndDrawing() + + + #---------------------------------------------------------------------------- + # Property Service methods + #---------------------------------------------------------------------------- + + def GetPropertyModel(self): + if hasattr(self, "_propModel"): + return self._propModel + return None + + + def SetPropertyModel(self, model): + # no need to set the model if no PropertyService is running + propertyService = wx.GetApp().GetService(PropertyService.PropertyService) + if not propertyService: + return + + if hasattr(self, "_propModel") and model == self._propModel: + return + + self._propModel = model + propertyService.LoadProperties(self._propModel, self.GetDocument()) + + +class EditorCanvasShapeMixin: + + def GetModel(self): + return self._model + + + def SetModel(self, model): + self._model = model + + +class EditorCanvasShapeEvtHandler(ogl.ShapeEvtHandler): + + """ wxBug: Bug in OLG package. With wxShape.SetShadowMode() turned on, when we set the size, + the width/height is larger by 6 pixels. Need to subtract this value from width and height when we + resize the object. + """ + SHIFT_KEY = 1 + CONTROL_KEY = 2 + + def __init__(self, view): + ogl.ShapeEvtHandler.__init__(self) + self._view = view + + + def OnLeftClick(self, x, y, keys = 0, attachment = 0): + shape = self.GetShape() + if hasattr(shape, "GetModel"): # Workaround, on drag, we should deselect all other objects and select the clicked on object + model = shape.GetModel() + else: + shape = shape.GetParent() + if shape: + model = shape.GetModel() + + self._view.SetSelection(model, keys == self.SHIFT_KEY or keys == self.CONTROL_KEY) + self._view.SetPropertyShape(shape) + self._view.SetPropertyModel(model) + + + def OnEndDragLeft(self, x, y, keys = 0, attachment = 0): + ogl.ShapeEvtHandler.OnEndDragLeft(self, x, y, keys, attachment) + shape = self.GetShape() + if hasattr(shape, "GetModel"): # Workaround, on drag, we should deselect all other objects and select the clicked on object + model = shape.GetModel() + else: + parentShape = shape.GetParent() + if parentShape: + model = parentShape.GetModel() + self._view.SetSelection(model, keys == self.SHIFT_KEY or keys == self.CONTROL_KEY) + + + def OnMovePost(self, dc, x, y, oldX, oldY, display): + if x == oldX and y == oldY: + return + if not self._view.GetDocument(): + return + shape = self.GetShape() + if isinstance(shape, EditorCanvasShapeMixin) and shape.Draggable(): + model = shape.GetModel() + if hasattr(model, "getEditorBounds") and model.getEditorBounds(): + x, y, w, h = model.getEditorBounds() + newX = shape.GetX() - shape.GetBoundingBoxMax()[0] / 2 + newY = shape.GetY() - shape.GetBoundingBoxMax()[1] / 2 + newWidth = shape.GetBoundingBoxMax()[0] + newHeight = shape.GetBoundingBoxMax()[1] + if shape._shadowMode != ogl.SHADOW_NONE: + newWidth -= shape._shadowOffsetX + newHeight -= shape._shadowOffsetY + newbounds = (newX, newY, newWidth, newHeight) + + if x != newX or y != newY or w != newWidth or h != newHeight: + self._view.GetDocument().GetCommandProcessor().Submit(EditorCanvasUpdateShapeBoundariesCommand(self._view.GetDocument(), model, newbounds)) + + + def Draw(self, dc): + pass + + +class EditorCanvasUpdateShapeBoundariesCommand(wx.lib.docview.Command): + + + def __init__(self, canvasDocument, model, newbounds): + wx.lib.docview.Command.__init__(self, canUndo = True) + self._canvasDocument = canvasDocument + self._model = model + self._oldbounds = model.getEditorBounds() + self._newbounds = newbounds + + + def GetName(self): + name = self._canvasDocument.GetNameForObject(self._model) + if not name: + name = "" + print "ERROR: AbstractEditor.EditorCanvasUpdateShapeBoundariesCommand.GetName: unable to get name for ", self._model + return _("Move/Resize %s") % name + + + def Do(self): + return self._canvasDocument.UpdateEditorBoundaries(self._model, self._newbounds) + + + def Undo(self): + return self._canvasDocument.UpdateEditorBoundaries(self._model, self._oldbounds) + diff --git a/wxPython/samples/ide/activegrid/tool/CodeEditor.py b/wxPython/samples/ide/activegrid/tool/CodeEditor.py new file mode 100644 index 0000000000..09a96abf53 --- /dev/null +++ b/wxPython/samples/ide/activegrid/tool/CodeEditor.py @@ -0,0 +1,1014 @@ +#---------------------------------------------------------------------------- +# Name: CodeEditor.py +# Purpose: Abstract Code Editor for pydocview tbat uses the Styled Text Control +# +# Author: Peter Yared +# +# Created: 8/10/03 +# CVS-ID: $Id$ +# Copyright: (c) 2004-2005 ActiveGrid, Inc. +# License: wxWindows License +#---------------------------------------------------------------------------- + + +import STCTextEditor +import wx +import wx.lib.docview +import OutlineService +import os +import re +import string +import sys +import DebuggerService +import MarkerService +_ = wx.GetTranslation +if wx.Platform == '__WXMSW__': + _WINDOWS = True +else: + _WINDOWS = False + + +EXPAND_TEXT_ID = wx.NewId() +COLLAPSE_TEXT_ID = wx.NewId() +EXPAND_TOP_ID = wx.NewId() +COLLAPSE_TOP_ID = wx.NewId() +EXPAND_ALL_ID = wx.NewId() +COLLAPSE_ALL_ID = wx.NewId() +CHECK_CODE_ID = wx.NewId() +AUTO_COMPLETE_ID = wx.NewId() +CLEAN_WHITESPACE = wx.NewId() +COMMENT_LINES_ID = wx.NewId() +UNCOMMENT_LINES_ID = wx.NewId() +INDENT_LINES_ID = wx.NewId() +DEDENT_LINES_ID = wx.NewId() +USE_TABS_ID = wx.NewId() +SET_INDENT_WIDTH_ID = wx.NewId() +FOLDING_ID = wx.NewId() + + +class CodeDocument(STCTextEditor.TextDocument): + pass + + +class CodeView(STCTextEditor.TextView): + + + #---------------------------------------------------------------------------- + # Overridden methods + #---------------------------------------------------------------------------- + + + def GetCtrlClass(self): + """ Used in split window to instantiate new instances """ + return CodeCtrl + + + def ProcessEvent(self, event): + id = event.GetId() + if id == EXPAND_TEXT_ID: + self.GetCtrl().ToggleFold(self.GetCtrl().GetCurrentLine()) + return True + elif id == COLLAPSE_TEXT_ID: + self.GetCtrl().ToggleFold(self.GetCtrl().GetCurrentLine()) + return True + elif id == EXPAND_TOP_ID: + self.GetCtrl().ToggleFoldAll(expand = True, topLevelOnly = True) + return True + elif id == COLLAPSE_TOP_ID: + self.GetCtrl().ToggleFoldAll(expand = False, topLevelOnly = True) + return True + elif id == EXPAND_ALL_ID: + self.GetCtrl().ToggleFoldAll(expand = True) + return True + elif id == COLLAPSE_ALL_ID: + self.GetCtrl().ToggleFoldAll(expand = False) + return True + elif id == CHECK_CODE_ID: + self.OnCheckCode() + return True + elif id == AUTO_COMPLETE_ID: + self.OnAutoComplete() + return True + elif id == CLEAN_WHITESPACE: + self.OnCleanWhiteSpace() + return True + elif id == SET_INDENT_WIDTH_ID: + self.OnSetIndentWidth() + return True + elif id == USE_TABS_ID: + self.GetCtrl().SetUseTabs(not self.GetCtrl().GetUseTabs()) + return True + elif id == INDENT_LINES_ID: + self.GetCtrl().CmdKeyExecute(wx.stc.STC_CMD_TAB) + return True + elif id == DEDENT_LINES_ID: + self.GetCtrl().CmdKeyExecute(wx.stc.STC_CMD_BACKTAB) + return True + elif id == COMMENT_LINES_ID: + self.OnCommentLines() + return True + elif id == UNCOMMENT_LINES_ID: + self.OnUncommentLines() + return True + else: + return STCTextEditor.TextView.ProcessEvent(self, event) + + + def ProcessUpdateUIEvent(self, event): + if not self.GetCtrl(): + return False + hasText = self.GetCtrl().GetTextLength() > 0 + id = event.GetId() + if id == EXPAND_TEXT_ID: + event.Enable(self.GetCtrl().CanLineExpand(self.GetCtrl().GetCurrentLine())) + return True + elif id == COLLAPSE_TEXT_ID: + event.Enable(self.GetCtrl().CanLineCollapse(self.GetCtrl().GetCurrentLine())) + return True + elif id == EXPAND_TOP_ID: + event.Enable(hasText) + return True + elif id == COLLAPSE_TOP_ID: + event.Enable(hasText) + return True + elif id == EXPAND_ALL_ID: + event.Enable(hasText) + return True + elif id == COLLAPSE_ALL_ID: + event.Enable(hasText) + return True + elif id == CHECK_CODE_ID: + event.Enable(False) + return True + elif id == AUTO_COMPLETE_ID: + event.Enable(hasText) + return True + elif id == CLEAN_WHITESPACE: + event.Enable(hasText) + return True + elif id == SET_INDENT_WIDTH_ID: + event.Enable(True) + return True + elif id == USE_TABS_ID: + event.Enable(True) + event.Check(self.GetCtrl().GetUseTabs()) + return True + elif id == INDENT_LINES_ID: + event.Enable(hasText) + return True + elif id == DEDENT_LINES_ID: + event.Enable(hasText) + return True + elif id == COMMENT_LINES_ID: + event.Enable(hasText) + return True + elif id == UNCOMMENT_LINES_ID: + event.Enable(hasText) + return True + elif id == FOLDING_ID: + event.Enable(True) + return True + else: + return STCTextEditor.TextView.ProcessUpdateUIEvent(self, event) + + + #---------------------------------------------------------------------------- + # Methods for OutlineService + #---------------------------------------------------------------------------- + + def OnChangeFilename(self): + wx.lib.docview.View.OnChangeFilename(self) + self.LoadOutline(force=True) + + + def ClearOutline(self): + outlineService = wx.GetApp().GetService(OutlineService.OutlineService) + if not outlineService: + return + + outlineView = outlineService.GetView() + if not outlineView: + return + + outlineView.ClearTreeCtrl() + + + def LoadOutline(self, force=False): + outlineService = wx.GetApp().GetService(OutlineService.OutlineService) + if not outlineService: + return + outlineService.LoadOutline(self, force=force) + + + def DoLoadOutlineCallback(self, force=False): + outlineService = wx.GetApp().GetService(OutlineService.OutlineService) + if not outlineService: + return False + + outlineView = outlineService.GetView() + if not outlineView: + return False + + treeCtrl = outlineView.GetTreeCtrl() + if not treeCtrl: + return False + + view = treeCtrl.GetCallbackView() + newCheckSum = self.GenCheckSum() + if not force: + if view and view is self: + if self._checkSum == newCheckSum: + return False + self._checkSum = newCheckSum + + treeCtrl.DeleteAllItems() + + document = self.GetDocument() + if not document: + return True + + filename = document.GetFilename() + if filename: + rootItem = treeCtrl.AddRoot(os.path.basename(filename)) + treeCtrl.SetDoSelectCallback(rootItem, self, None) + else: + return True + + text = self.GetValue() + if not text: + return True + + CLASS_PATTERN = 'class[ \t]+\w+.*?:' + DEF_PATTERN = 'def[ \t]+\w+\(.*?\)' + classPat = re.compile(CLASS_PATTERN, re.M|re.S) + defPat= re.compile(DEF_PATTERN, re.M|re.S) + pattern = re.compile('^[ \t]*((' + CLASS_PATTERN + ')|('+ DEF_PATTERN +'.*?:)).*?$', re.M|re.S) + + iter = pattern.finditer(text) + indentStack = [(0, rootItem)] + for pattern in iter: + line = pattern.string[pattern.start(0):pattern.end(0)] + classLine = classPat.search(line) + if classLine: + indent = classLine.start(0) + itemStr = classLine.string[classLine.start(0):classLine.end(0)-1] # don't take the closing ':' + else: + defLine = defPat.search(line) + if defLine: + indent = defLine.start(0) + itemStr = defLine.string[defLine.start(0):defLine.end(0)] + + if indent == 0: + parentItem = rootItem + else: + lastItem = indentStack.pop() + while lastItem[0] >= indent: + lastItem = indentStack.pop() + indentStack.append(lastItem) + parentItem = lastItem[1] + + item = treeCtrl.AppendItem(parentItem, itemStr) + treeCtrl.SetDoSelectCallback(item, self, (pattern.end(0), pattern.start(0) + indent)) # select in reverse order because we want the cursor to be at the start of the line so it wouldn't scroll to the right + indentStack.append((indent, item)) + + treeCtrl.Expand(rootItem) + + return True + + + def DoSelectCallback(self, data): + if data: + self.EnsureVisibleEnforcePolicy(self.LineFromPosition(data[0])) + # wxBug: need to select in reverse order (end, start) to place cursor at begining of line, + # otherwise, display is scrolled over to the right hard and is hard to view + self.SetSelection(data[1], data[0]) + + +## def checksum(self, bytes): +## def rotate_right(c): +## if c&1: +## return (c>>1)|0x8000 +## else: +## return c>>1 +## +## result = 0 +## for ch in bytes: +## ch = ord(ch) & 0xFF +## result = (rotate_right(result)+ch) & 0xFFFF +## return result +## + + def GenCheckSum(self): + """ Poor man's checksum. We'll assume most changes will change the length of the file. + """ + text = self.GetValue() + if text: + return len(text) + else: + return 0 + + + #---------------------------------------------------------------------------- + # Format methods + #---------------------------------------------------------------------------- + + def OnCheckCode(self): + """ Need to overshadow this for each specific subclass """ + if 0: + try: + code = self.GetCtrl().GetText() + codeObj = compile(code, self.GetDocument().GetFilename(), 'exec') + self._GetParentFrame().SetStatusText(_("The file successfully compiled")) + except SyntaxError, (message, (fileName, line, col, text)): + pos = self.GetCtrl().PositionFromLine(line - 1) + col - 1 + self.GetCtrl().SetSelection(pos, pos) + self._GetParentFrame().SetStatusText(_("Syntax Error: %s") % message) + except: + self._GetParentFrame().SetStatusText(sys.exc_info()[0]) + + + def OnAutoComplete(self): + self.GetCtrl().AutoCompCancel() + self.GetCtrl().AutoCompSetAutoHide(0) + self.GetCtrl().AutoCompSetChooseSingle(True) + self.GetCtrl().AutoCompSetIgnoreCase(True) + context, hint = self.GetAutoCompleteHint() + replaceList, replaceLen = self.GetAutoCompleteKeywordList(context, hint) + if replaceList and len(replaceList) != 0: + self.GetCtrl().AutoCompShow(replaceLen, replaceList) + + + def GetAutoCompleteHint(self): + """ Replace this method with Editor specific method """ + pos = self.GetCtrl().GetCurrentPos() + if pos == 0: + return None, None + if chr(self.GetCtrl().GetCharAt(pos - 1)) == '.': + pos = pos - 1 + hint = None + else: + hint = '' + + validLetters = string.letters + string.digits + '_.' + word = '' + while (True): + pos = pos - 1 + if pos < 0: + break + char = chr(self.GetCtrl().GetCharAt(pos)) + if char not in validLetters: + break + word = char + word + + context = word + if hint is not None: + lastDot = word.rfind('.') + if lastDot != -1: + context = word[0:lastDot] + hint = word[lastDot+1:] + + return context, hint + + + def GetAutoCompleteDefaultKeywords(self): + """ Replace this method with Editor specific keywords """ + return ['Put', 'Editor Specific', 'Keywords', 'Here'] + + + def CaseInsensitiveCompare(self, s1, s2): + """ GetAutoCompleteKeywordList() method used to show keywords in case insensitive order """ + s1L = s1.lower() + s2L = s2.lower() + if s1L == s2L: + return 0 + elif s1L < s2L: + return -1 + else: + return 1 + + + def GetAutoCompleteKeywordList(self, context, hint): + """ Replace this method with Editor specific keywords """ + kw = self.GetAutoCompleteDefaultKeywords() + + if hint and len(hint): + lowerHint = hint.lower() + filterkw = filter(lambda item: item.lower().startswith(lowerHint), kw) # remove variables and methods that don't match hint + kw = filterkw + + if hint: + replaceLen = len(hint) + else: + replaceLen = 0 + + kw.sort(self.CaseInsensitiveCompare) + return " ".join(kw), replaceLen + + + def OnCleanWhiteSpace(self): + newText = "" + for lineNo in self._GetSelectedLineNumbers(): + lineText = string.rstrip(self.GetCtrl().GetLine(lineNo)) + indent = 0 + lstrip = 0 + for char in lineText: + if char == '\t': + indent = indent + self.GetCtrl().GetIndent() + lstrip = lstrip + 1 + elif char in string.whitespace: + indent = indent + 1 + lstrip = lstrip + 1 + else: + break + if self.GetCtrl().GetUseTabs(): + indentText = (indent / self.GetCtrl().GetIndent()) * '\t' + (indent % self.GetCtrl().GetIndent()) * ' ' + else: + indentText = indent * ' ' + lineText = indentText + lineText[lstrip:] + '\n' + newText = newText + lineText + self._ReplaceSelectedLines(newText) + + + def OnSetIndentWidth(self): + dialog = wx.TextEntryDialog(self._GetParentFrame(), _("Enter new indent width (2-10):"), _("Set Indent Width"), "%i" % self.GetCtrl().GetIndent()) + if dialog.ShowModal() == wx.ID_OK: + try: + indent = int(dialog.GetValue()) + if indent >= 2 and indent <= 10: + self.GetCtrl().SetIndent(indent) + self.GetCtrl().SetTabWidth(indent) + except: + pass + dialog.Destroy() + + + def GetIndentWidth(self): + return self.GetCtrl().GetIndent() + + + def OnCommentLines(self): + newText = "" + for lineNo in self._GetSelectedLineNumbers(): + lineText = self.GetCtrl().GetLine(lineNo) + if (len(lineText) > 1 and lineText[0] == '#') or (len(lineText) > 2 and lineText[:2] == '##'): + newText = newText + lineText + else: + newText = newText + "##" + lineText + self._ReplaceSelectedLines(newText) + + + def OnUncommentLines(self): + newText = "" + for lineNo in self._GetSelectedLineNumbers(): + lineText = self.GetCtrl().GetLine(lineNo) + if len(lineText) >= 2 and lineText[:2] == "##": + lineText = lineText[2:] + elif len(lineText) >= 1 and lineText[:1] == "#": + lineText = lineText[1:] + newText = newText + lineText + self._ReplaceSelectedLines(newText) + + + def _GetSelectedLineNumbers(self): + selStart, selEnd = self._GetPositionsBoundingSelectedLines() + return range(self.GetCtrl().LineFromPosition(selStart), self.GetCtrl().LineFromPosition(selEnd)) + + + def _GetPositionsBoundingSelectedLines(self): + startPos = self.GetCtrl().GetCurrentPos() + endPos = self.GetCtrl().GetAnchor() + if startPos > endPos: + temp = endPos + endPos = startPos + startPos = temp + if endPos == self.GetCtrl().PositionFromLine(self.GetCtrl().LineFromPosition(endPos)): + endPos = endPos - 1 # If it's at the very beginning of a line, use the line above it as the ending line + selStart = self.GetCtrl().PositionFromLine(self.GetCtrl().LineFromPosition(startPos)) + selEnd = self.GetCtrl().PositionFromLine(self.GetCtrl().LineFromPosition(endPos) + 1) + return selStart, selEnd + + + def _ReplaceSelectedLines(self, text): + if len(text) == 0: + return + selStart, selEnd = self._GetPositionsBoundingSelectedLines() + self.GetCtrl().SetSelection(selStart, selEnd) + self.GetCtrl().ReplaceSelection(text) + self.GetCtrl().SetSelection(selStart + len(text), selStart) + + + def OnUpdate(self, sender = None, hint = None): + if hint == "ViewStuff": + self.GetCtrl().SetViewDefaults() + elif hint == "Font": + font, color = self.GetFontAndColorFromConfig() + self.GetCtrl().SetFont(font) + self.GetCtrl().SetFontColor(color) + else: + dbg_service = wx.GetApp().GetService(DebuggerService.DebuggerService) + if dbg_service: + dbg_service.SetCurrentBreakpointMarkers(self) + + +class CodeService(STCTextEditor.TextService): + + + def __init__(self): + STCTextEditor.TextService.__init__(self) + + + def InstallControls(self, frame, menuBar = None, toolBar = None, statusBar = None, document = None): + # TODO NEED TO DO INSTANCEOF CHECK HERE FOR SDI + #if document and document.GetDocumentTemplate().GetDocumentType() != TextDocument: + # return + if not document and wx.GetApp().GetDocumentManager().GetFlags() & wx.lib.docview.DOC_SDI: + return + + viewMenu = menuBar.GetMenu(menuBar.FindMenu(_("&View"))) + isWindows = (wx.Platform == '__WXMSW__') + + if not menuBar.FindItemById(EXPAND_TEXT_ID): # check if below menu items have been already been installed + foldingMenu = wx.Menu() + if isWindows: + foldingMenu.Append(EXPAND_TEXT_ID, _("&Expand\tNumpad-Plus"), _("Expands a collapsed block of text")) + else: + foldingMenu.Append(EXPAND_TEXT_ID, _("&Expand"), _("Expands a collapsed block of text")) + + wx.EVT_MENU(frame, EXPAND_TEXT_ID, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, EXPAND_TEXT_ID, frame.ProcessUpdateUIEvent) + + if isWindows: + foldingMenu.Append(COLLAPSE_TEXT_ID, _("&Collapse\tNumpad+Minus"), _("Collapse a block of text")) + else: + foldingMenu.Append(COLLAPSE_TEXT_ID, _("&Collapse"), _("Collapse a block of text")) + wx.EVT_MENU(frame, COLLAPSE_TEXT_ID, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, COLLAPSE_TEXT_ID, frame.ProcessUpdateUIEvent) + + if isWindows: + foldingMenu.Append(EXPAND_TOP_ID, _("Expand &Top Level\tCtrl+Numpad+Plus"), _("Expands the top fold levels in the document")) + else: + foldingMenu.Append(EXPAND_TOP_ID, _("Expand &Top Level"), _("Expands the top fold levels in the document")) + wx.EVT_MENU(frame, EXPAND_TOP_ID, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, EXPAND_TOP_ID, frame.ProcessUpdateUIEvent) + + if isWindows: + foldingMenu.Append(COLLAPSE_TOP_ID, _("Collapse Top &Level\tCtrl+Numpad+Minus"), _("Collapses the top fold levels in the document")) + else: + foldingMenu.Append(COLLAPSE_TOP_ID, _("Collapse Top &Level"), _("Collapses the top fold levels in the document")) + wx.EVT_MENU(frame, COLLAPSE_TOP_ID, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, COLLAPSE_TOP_ID, frame.ProcessUpdateUIEvent) + + if isWindows: + foldingMenu.Append(EXPAND_ALL_ID, _("Expand &All\tShift+Numpad+Plus"), _("Expands all of the fold levels in the document")) + else: + foldingMenu.Append(EXPAND_ALL_ID, _("Expand &All"), _("Expands all of the fold levels in the document")) + wx.EVT_MENU(frame, EXPAND_ALL_ID, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, EXPAND_ALL_ID, frame.ProcessUpdateUIEvent) + + if isWindows: + foldingMenu.Append(COLLAPSE_ALL_ID, _("Colla&pse All\tShift+Numpad+Minus"), _("Collapses all of the fold levels in the document")) + else: + foldingMenu.Append(COLLAPSE_ALL_ID, _("Colla&pse All"), _("Collapses all of the fold levels in the document")) + wx.EVT_MENU(frame, COLLAPSE_ALL_ID, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, COLLAPSE_ALL_ID, frame.ProcessUpdateUIEvent) + + viewMenu.AppendMenu(FOLDING_ID, _("&Folding"), foldingMenu) + wx.EVT_UPDATE_UI(frame, FOLDING_ID, frame.ProcessUpdateUIEvent) + + formatMenuIndex = menuBar.FindMenu(_("&Format")) + if formatMenuIndex > -1: + formatMenu = menuBar.GetMenu(formatMenuIndex) + else: + formatMenu = wx.Menu() + if not menuBar.FindItemById(CHECK_CODE_ID): # check if below menu items have been already been installed + formatMenu.AppendSeparator() + formatMenu.Append(CHECK_CODE_ID, _("&Check Code"), _("Checks the document for syntax and indentation errors")) + wx.EVT_MENU(frame, CHECK_CODE_ID, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, CHECK_CODE_ID, frame.ProcessUpdateUIEvent) + formatMenu.Append(AUTO_COMPLETE_ID, _("&Auto Complete\tCtrl+Space"), _("Provides suggestions on how to complete the current statement")) + wx.EVT_MENU(frame, AUTO_COMPLETE_ID, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, AUTO_COMPLETE_ID, frame.ProcessUpdateUIEvent) + formatMenu.Append(CLEAN_WHITESPACE, _("Clean &Whitespace"), _("Converts leading spaces to tabs or vice versa per 'use tabs' and clears trailing spaces")) + wx.EVT_MENU(frame, CLEAN_WHITESPACE, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, CLEAN_WHITESPACE, frame.ProcessUpdateUIEvent) + formatMenu.AppendSeparator() + formatMenu.Append(INDENT_LINES_ID, _("&Indent Lines\tTab"), _("Indents the selected lines one indent width")) + wx.EVT_MENU(frame, INDENT_LINES_ID, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, INDENT_LINES_ID, frame.ProcessUpdateUIEvent) + formatMenu.Append(DEDENT_LINES_ID, _("&Dedent Lines\tShift+Tab"), _("Dedents the selected lines one indent width")) + wx.EVT_MENU(frame, DEDENT_LINES_ID, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, DEDENT_LINES_ID, frame.ProcessUpdateUIEvent) + formatMenu.Append(COMMENT_LINES_ID, _("Comment &Lines\tCtrl+Q"), _("Comments out the selected lines be prefixing each one with a comment indicator")) + wx.EVT_MENU(frame, COMMENT_LINES_ID, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, COMMENT_LINES_ID, frame.ProcessUpdateUIEvent) + formatMenu.Append(UNCOMMENT_LINES_ID, _("&Uncomment Lines\tCtrl+Shift+Q"), _("Removes comment prefixes from each of the selected lines")) + wx.EVT_MENU(frame, UNCOMMENT_LINES_ID, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, UNCOMMENT_LINES_ID, frame.ProcessUpdateUIEvent) + formatMenu.AppendSeparator() + formatMenu.AppendCheckItem(USE_TABS_ID, _("Use &Tabs"), _("Toggles use of tabs or whitespaces for indents")) + wx.EVT_MENU(frame, USE_TABS_ID, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, USE_TABS_ID, frame.ProcessUpdateUIEvent) + formatMenu.Append(SET_INDENT_WIDTH_ID, _("&Set Indent Width..."), _("Sets the indent width")) + wx.EVT_MENU(frame, SET_INDENT_WIDTH_ID, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, SET_INDENT_WIDTH_ID, frame.ProcessUpdateUIEvent) + if formatMenuIndex == -1: + viewMenuIndex = menuBar.FindMenu(_("&View")) + menuBar.Insert(viewMenuIndex + 1, formatMenu, _("&Format")) + +## accelTable = wx.AcceleratorTable([ +## (wx.ACCEL_NORMAL, wx.WXK_TAB, INDENT_LINES_ID), +## (wx.ACCEL_SHIFT, wx.WXK_TAB, DEDENT_LINES_ID), +## eval(_("wx.ACCEL_CTRL, ord('Q'), COMMENT_LINES_ID")), +## eval(_("wx.ACCEL_CTRL | wx.ACCEL_SHIFT, ord('Q'), UNCOMMENT_LINES_ID")) +## ]) +## frame.SetAcceleratorTable(accelTable) + + def ProcessUpdateUIEvent(self, event): + id = event.GetId() + if id == EXPAND_TEXT_ID: + event.Enable(False) + return True + elif id == COLLAPSE_TEXT_ID: + event.Enable(False) + return True + elif id == EXPAND_TOP_ID: + event.Enable(False) + return True + elif id == COLLAPSE_TOP_ID: + event.Enable(False) + return True + elif id == EXPAND_ALL_ID: + event.Enable(False) + return True + elif id == COLLAPSE_ALL_ID: + event.Enable(False) + return True + elif id == CHECK_CODE_ID: + event.Enable(False) + return True + elif id == AUTO_COMPLETE_ID: + event.Enable(False) + return True + elif id == CLEAN_WHITESPACE: + event.Enable(False) + return True + elif id == SET_INDENT_WIDTH_ID: + event.Enable(False) + return True + elif id == USE_TABS_ID: + event.Enable(False) + return True + elif id == INDENT_LINES_ID: + event.Enable(False) + return True + elif id == DEDENT_LINES_ID: + event.Enable(False) + return True + elif id == COMMENT_LINES_ID: + event.Enable(False) + return True + elif id == UNCOMMENT_LINES_ID: + event.Enable(False) + return True + elif id == FOLDING_ID: + event.Enable(False) + return True + else: + return STCTextEditor.TextService.ProcessUpdateUIEvent(self, event) + + +class CodeCtrl(STCTextEditor.TextCtrl): + CURRENT_LINE_MARKER_NUM = 2 + BREAKPOINT_MARKER_NUM = 1 + CURRENT_LINE_MARKER_MASK = 0x4 + BREAKPOINT_MARKER_MASK = 0x2 + + + def __init__(self, parent, ID = -1, style = wx.NO_FULL_REPAINT_ON_RESIZE): + if ID == -1: + ID = wx.NewId() + STCTextEditor.TextCtrl.__init__(self, parent, ID, style) + + self.UsePopUp(False) + self.Bind(wx.EVT_RIGHT_UP, self.OnRightUp) + self.SetProperty("fold", "1") + + # Setup a margin to hold fold markers + #self.SetFoldFlags(16) ### WHAT IS THIS VALUE? WHAT ARE THE OTHER FLAGS? DOES IT MATTER? + self.SetMarginType(2, wx.stc.STC_MARGIN_SYMBOL) + self.SetMarginMask(2, wx.stc.STC_MASK_FOLDERS) + self.SetMarginSensitive(2, True) + self.SetMarginWidth(2, 12) + + self.SetMarginSensitive(1, False) + self.SetMarginMask(1, 0x4) + + self.SetMarginSensitive(0, True) + self.SetMarginType(0, wx.stc.STC_MARGIN_SYMBOL) + self.SetMarginMask(0, 0x3) + self.SetMarginWidth(0, 12) + + self.MarkerDefine(wx.stc.STC_MARKNUM_FOLDEREND, wx.stc.STC_MARK_BOXPLUSCONNECTED, "white", "black") + self.MarkerDefine(wx.stc.STC_MARKNUM_FOLDEROPENMID, wx.stc.STC_MARK_BOXMINUSCONNECTED, "white", "black") + self.MarkerDefine(wx.stc.STC_MARKNUM_FOLDERMIDTAIL, wx.stc.STC_MARK_TCORNER, "white", "black") + self.MarkerDefine(wx.stc.STC_MARKNUM_FOLDERTAIL, wx.stc.STC_MARK_LCORNER, "white", "black") + self.MarkerDefine(wx.stc.STC_MARKNUM_FOLDERSUB, wx.stc.STC_MARK_VLINE, "white", "black") + self.MarkerDefine(wx.stc.STC_MARKNUM_FOLDER, wx.stc.STC_MARK_BOXPLUS, "white", "black") + self.MarkerDefine(wx.stc.STC_MARKNUM_FOLDEROPEN, wx.stc.STC_MARK_BOXMINUS, "white", "black") + # Define the current line marker + self.MarkerDefine(CodeCtrl.CURRENT_LINE_MARKER_NUM, wx.stc.STC_MARK_SHORTARROW, wx.BLACK, (255,255,128)) + # Define the breakpoint marker + self.MarkerDefine(CodeCtrl.BREAKPOINT_MARKER_NUM, wx.stc.STC_MARK_CIRCLE, wx.BLACK, (255,0,0)) + + if _WINDOWS: # should test to see if menu item exists, if it does, add this workaround + self.CmdKeyClear(wx.stc.STC_KEY_TAB, 0) # menu item "Indent Lines" from CodeService.InstallControls() generates another INDENT_LINES_ID event, so we'll explicitly disable the tab processing in the editor + + wx.stc.EVT_STC_MARGINCLICK(self, ID, self.OnMarginClick) + wx.EVT_KEY_DOWN(self, self.OnKeyPressed) + if self.GetMatchingBraces(): + wx.stc.EVT_STC_UPDATEUI(self, ID, self.OnUpdateUI) + + self.StyleClearAll() + self.UpdateStyles() + + + def OnRightUp(self, event): + #Hold onto the current line number, no way to get it later. + self._rightClickPosition = self.PositionFromPoint(event.GetPosition()) + self._rightClickLine = self.LineFromPosition(self._rightClickPosition) + self.PopupMenu(self.CreatePopupMenu(), event.GetPosition()) + self._rightClickLine = -1 + self._rightClickPosition = -1 + + + def CreatePopupMenu(self): + TOGGLEBREAKPOINT_ID = wx.NewId() + TOGGLEMARKER_ID = wx.NewId() + SYNCTREE_ID = wx.NewId() + + menu = wx.Menu() + + self.Bind(wx.EVT_MENU, self.OnPopSyncOutline, id=SYNCTREE_ID) + item = wx.MenuItem(menu, SYNCTREE_ID, _("Find in Outline View")) + menu.AppendItem(item) + menu.AppendSeparator() + self.Bind(wx.EVT_MENU, self.OnPopToggleBP, id=TOGGLEBREAKPOINT_ID) + item = wx.MenuItem(menu, TOGGLEBREAKPOINT_ID, _("Toggle Breakpoint")) + menu.AppendItem(item) + self.Bind(wx.EVT_MENU, self.OnPopToggleMarker, id=TOGGLEMARKER_ID) + item = wx.MenuItem(menu, TOGGLEMARKER_ID, _("Toggle Marker")) + menu.AppendItem(item) + menu.AppendSeparator() + + itemIDs = [wx.ID_UNDO, wx.ID_REDO, None, + wx.ID_CUT, wx.ID_COPY, wx.ID_PASTE, wx.ID_CLEAR, None, wx.ID_SELECTALL] + + menuBar = wx.GetApp().GetTopWindow().GetMenuBar() + for itemID in itemIDs: + if not itemID: + menu.AppendSeparator() + else: + item = menuBar.FindItemById(itemID) + if item: + menu.Append(itemID, item.GetLabel()) + + return menu + + + def OnPopToggleBP(self, event): + """ Toggle break point on right click line, not current line """ + wx.GetApp().GetService(DebuggerService.DebuggerService).OnToggleBreakpoint(event, line=self._rightClickLine) + + + def OnPopToggleMarker(self, event): + """ Toggle marker on right click line, not current line """ + wx.GetApp().GetDocumentManager().GetCurrentView().MarkerToggle(lineNum = self._rightClickLine) + + + def OnPopSyncOutline(self, event): + wx.GetApp().GetService(OutlineService.OutlineService).LoadOutline(wx.GetApp().GetDocumentManager().GetCurrentView(), position=self._rightClickPosition) + + + def HasSelection(self): + return self.GetSelectionStart() - self.GetSelectionEnd() != 0 + + + def ClearCurrentLineMarkers(self): + self.MarkerDeleteAll(CodeCtrl.CURRENT_LINE_MARKER_NUM) + + + def ClearCurrentBreakpoinMarkers(self): + self.MarkerDeleteAll(CodeCtrl.BREAKPOINT_MARKER_NUM) + + + def GetDefaultFont(self): + if wx.Platform == '__WXMSW__': + font = "Courier New" + else: + font = "Courier" + return wx.Font(10, wx.DEFAULT, wx.NORMAL, wx.NORMAL, faceName = font) + + + def GetMatchingBraces(self): + """ Overwrite this method for language specific braces """ + return "[]{}()" + + + def CanWordWrap(self): + return False + + + def SetFont(self, font): + self._font = font + + + def SetFontColor(self, fontColor): + self._fontColor = fontColor + + + def UpdateStyles(self): + + if not self.GetFont(): + return + + faces = { 'font' : self.GetFont().GetFaceName(), + 'size' : self.GetFont().GetPointSize(), + 'size2': self.GetFont().GetPointSize() - 2, + 'color' : "%02x%02x%02x" % (self.GetFontColor().Red(), self.GetFontColor().Green(), self.GetFontColor().Blue()) + } + + # Global default styles for all languages + self.StyleSetSpec(wx.stc.STC_STYLE_DEFAULT, "face:%(font)s,fore:#FFFFFF,size:%(size)d" % faces) + self.StyleSetSpec(wx.stc.STC_STYLE_LINENUMBER, "face:%(font)s,back:#C0C0C0,face:%(font)s,size:%(size2)d" % faces) + self.StyleSetSpec(wx.stc.STC_STYLE_CONTROLCHAR, "face:%(font)s" % faces) + self.StyleSetSpec(wx.stc.STC_STYLE_BRACELIGHT, "face:%(font)s,fore:#000000,back:#70FFFF,size:%(size)d" % faces) + self.StyleSetSpec(wx.stc.STC_STYLE_BRACEBAD, "face:%(font)s,fore:#000000,back:#FF0000,size:%(size)d" % faces) + + + def OnKeyPressed(self, event): + if self.CallTipActive(): + self.CallTipCancel() + key = event.KeyCode() + if False: # key == wx.WXK_SPACE and event.ControlDown(): + pos = self.GetCurrentPos() + # Tips + if event.ShiftDown(): + self.CallTipSetBackground("yellow") + self.CallTipShow(pos, 'param1, param2') + # Code completion + else: + #lst = [] + #for x in range(50000): + # lst.append('%05d' % x) + #st = string.join(lst) + #print len(st) + #self.AutoCompShow(0, st) + + 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_much_much_much_longer_value") + + kw.sort() # Python sorts are case sensitive + self.AutoCompSetIgnoreCase(False) # so this needs to match + + self.AutoCompShow(0, string.join(kw)) + elif key == wx.WXK_RETURN: + self.DoIndent() + else: + STCTextEditor.TextCtrl.OnKeyPressed(self, event) + + + def DoIndent(self): + self.AddText('\n') + # Need to do a default one for all languges + + + def OnMarginClick(self, evt): + # fold and unfold as needed + if evt.GetMargin() == 2: + if evt.GetShift() and evt.GetControl(): + lineCount = self.GetLineCount() + expanding = True + + # find out if we are folding or unfolding + for lineNum in range(lineCount): + if self.GetFoldLevel(lineNum) & wx.stc.STC_FOLDLEVELHEADERFLAG: + expanding = not self.GetFoldExpanded(lineNum) + break; + + self.ToggleFoldAll(expanding) + else: + lineClicked = self.LineFromPosition(evt.GetPosition()) + if self.GetFoldLevel(lineClicked) & wx.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) + + elif evt.GetMargin() == 0: + #This is used to toggle breakpoints via the debugger service. + db_service = wx.GetApp().GetService(DebuggerService.DebuggerService) + if db_service: + db_service.OnToggleBreakpoint(evt, line=self.LineFromPosition(evt.GetPosition())) + + + def OnUpdateUI(self, evt): + braces = self.GetMatchingBraces() + + # 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 braces: + braceAtCaret = caretPos - 1 + + # check after + if braceAtCaret < 0: + charAfter = self.GetCharAt(caretPos) + styleAfter = self.GetStyleAt(caretPos) + if charAfter and chr(charAfter) in braces: + braceAtCaret = caretPos + + if braceAtCaret >= 0: + braceOpposite = self.BraceMatch(braceAtCaret) + + if braceAtCaret != -1 and braceOpposite == -1: + self.BraceBadLight(braceAtCaret) + else: + self.BraceHighlight(braceAtCaret, braceOpposite) + + evt.Skip() + + + def ToggleFoldAll(self, expand = True, topLevelOnly = False): + i = 0 + lineCount = self.GetLineCount() + while i < lineCount: + if not topLevelOnly or (topLevelOnly and self.GetFoldLevel(i) & wx.stc.STC_FOLDLEVELNUMBERMASK == wx.stc.STC_FOLDLEVELBASE): + if (expand and self.CanLineExpand(i)) or (not expand and self.CanLineCollapse(i)): + self.ToggleFold(i) + i = i + 1 + + + def CanLineExpand(self, line): + return not self.GetFoldExpanded(line) + + + def CanLineCollapse(self, line): + return self.GetFoldExpanded(line) and self.GetFoldLevel(line) & wx.stc.STC_FOLDLEVELHEADERFLAG + + + 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 & wx.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 + + diff --git a/wxPython/samples/ide/activegrid/tool/DebuggerHarness.py b/wxPython/samples/ide/activegrid/tool/DebuggerHarness.py new file mode 100644 index 0000000000..a12b8e579b --- /dev/null +++ b/wxPython/samples/ide/activegrid/tool/DebuggerHarness.py @@ -0,0 +1,690 @@ +#---------------------------------------------------------------------------- +# Name: DebuggerHarness.py +# Purpose: +# +# Author: Matt Fryer +# +# Created: 7/28/04 +# CVS-ID: $Id$ +# Copyright: (c) 2005 ActiveGrid, Inc. +# License: wxWindows License +#---------------------------------------------------------------------------- +import bdb +import sys +import SimpleXMLRPCServer +import threading +import xmlrpclib +import os +import types +import Queue +import traceback +import inspect +from xml.dom.minidom import getDOMImplementation +import atexit +import pickle + +if sys.platform.startswith("win"): + import win32api + _WINDOWS = True +else: + _WINDOWS = False + +_VERBOSE = False +_DEBUG_DEBUGGER = False + +class Adb(bdb.Bdb): + + def __init__(self, harness, queue): + bdb.Bdb.__init__(self) + self._harness = harness + self._userBreak = False + self._queue = queue + self._knownCantExpandFiles = {} + self._knownExpandedFiles = {} + + def getLongName(self, filename): + if not _WINDOWS: + return filename + if self._knownCantExpandFiles.get(filename): + return filename + if self._knownExpandedFiles.get(filename): + return self._knownExpandedFiles.get(filename) + try: + newname = win32api.GetLongPathName(filename) + self._knownExpandedFiles[filename] = newname + return newname + except: + self._knownCantExpandFiles[filename] = filename + return filename + + def canonic(self, orig_filename): + if orig_filename == "<" + orig_filename[1:-1] + ">": + return orig_filename + filename = self.getLongName(orig_filename) + + canonic = self.fncache.get(filename) + if not canonic: + canonic = os.path.abspath(filename) + canonic = os.path.normcase(canonic) + self.fncache[filename] = canonic + return canonic + + + # Overriding this so that we continue to trace even if no breakpoints are set. + def set_continue(self): + self.stopframe = self.botframe + self.returnframe = None + self.quitting = 0 + + def do_clear(self, arg): + bdb.Breakpoint.bpbynumber[int(arg)].deleteMe() + + def user_line(self, frame): + if self.in_debugger_code(frame): + self.set_step() + return + message = self.__frame2message(frame) + self._harness.interaction(message, frame, "") + + def user_call(self, frame, argument_list): + if self.in_debugger_code(frame): + self.set_step() + return + if self.stop_here(frame): + message = self.__frame2message(frame) + self._harness.interaction(message, frame, "") + + def user_return(self, frame, return_value): + if self.in_debugger_code(frame): + self.set_step() + return + message = self.__frame2message(frame) + self._harness.interaction(message, frame, "") + + def user_exception(self, frame, (exc_type, exc_value, exc_traceback)): + frame.f_locals['__exception__'] = exc_type, exc_value + if type(exc_type) == type(''): + exc_type_name = exc_type + else: + exc_type_name = exc_type.__name__ + message = "Exception occured: " + repr(exc_type_name) + " See locals.__exception__ for details." + traceback.print_exception(exc_type, exc_value, exc_traceback) + self._harness.interaction(message, frame, message) + + def in_debugger_code(self, frame): + if _DEBUG_DEBUGGER: return False + message = self.__frame2message(frame) + return message.count('DebuggerHarness') > 0 + + def frame2message(self, frame): + return self.__frame2message(frame) + + def __frame2message(self, frame): + code = frame.f_code + filename = code.co_filename + lineno = frame.f_lineno + basename = os.path.basename(filename) + message = "%s:%s" % (basename, lineno) + if code.co_name != "?": + message = "%s: %s()" % (message, code.co_name) + return message + + def runFile(self, fileName): + self.reset() + #global_dict = {} + #global_dict['__name__'] = '__main__' + try: + fileToRun = open(fileName, mode='r') + if _VERBOSE: print "Running file ", fileName + sys.settrace(self.trace_dispatch) + import __main__ + exec fileToRun in __main__.__dict__,__main__.__dict__ + except SystemExit: + pass + except: + tp, val, tb = sys.exc_info() + traceback.print_exception(tp, val, tb) + + sys.settrace(None) + self.quitting = 1 + #global_dict.clear() + + def trace_dispatch(self, frame, event, arg): + if self.quitting: + return # None + # Check for ui events + self.readQueue() + if event == 'line': + return self.dispatch_line(frame) + if event == 'call': + return self.dispatch_call(frame, arg) + if event == 'return': + return self.dispatch_return(frame, arg) + if event == 'exception': + return self.dispatch_exception(frame, arg) + print 'Adb.dispatch: unknown debugging event:', `event` + return self.trace_dispatch + + def readQueue(self): + while self._queue.qsize(): + try: + item = self._queue.get_nowait() + if item.kill(): + self._harness.do_exit(kill=True) + elif item.breakHere(): + self._userBreak = True + elif item.hasBreakpoints(): + self.set_all_breakpoints(item.getBreakpoints()) + except Queue.Empty: + pass + + def set_all_breakpoints(self, dict): + self.clear_all_breaks() + for fileName in dict.keys(): + lineList = dict[fileName] + for lineNumber in lineList: + + if _VERBOSE: print "Setting break at line ", str(lineNumber), " in file ", self.canonic(fileName) + self.set_break(fileName, int(lineNumber)) + return "" + + def stop_here(self, frame): + if( self._userBreak ): + return True + + + # (CT) stopframe may now also be None, see dispatch_call. + # (CT) the former test for None is therefore removed from here. + if frame is self.stopframe: + return True + while frame is not None and frame is not self.stopframe: + if frame is self.botframe: + return True + frame = frame.f_back + return False + +class BreakNotify(object): + def __init__(self, bps=None, break_here=False, kill=False): + self._bps = bps + self._break_here = break_here + self._kill = kill + + def breakHere(self): + return self._break_here + + def kill(self): + return self._kill + + def getBreakpoints(self): + return self._bps + + def hasBreakpoints(self): + return (self._bps != None) + +class AGXMLRPCServer(SimpleXMLRPCServer.SimpleXMLRPCServer): + def __init__(self, address, logRequests=0): + SimpleXMLRPCServer.SimpleXMLRPCServer.__init__(self, address, logRequests=logRequests) + +class BreakListenerThread(threading.Thread): + def __init__(self, host, port, queue): + threading.Thread.__init__(self) + self._host = host + self._port = int(port) + self._keepGoing = True + self._queue = queue + self._server = AGXMLRPCServer((self._host, self._port), logRequests=0) + self._server.register_function(self.update_breakpoints) + self._server.register_function(self.break_requested) + self._server.register_function(self.die) + + def break_requested(self): + bn = BreakNotify(break_here=True) + self._queue.put(bn) + return "" + + def update_breakpoints(self, pickled_Binary_bpts): + dict = pickle.loads(pickled_Binary_bpts.data) + bn = BreakNotify(bps=dict) + self._queue.put(bn) + return "" + + def die(self): + bn = BreakNotify(kill=True) + self._queue.put(bn) + return "" + + def run(self): + while self._keepGoing: + try: + self._server.handle_request() + except: + if _VERBOSE: + tp, val, tb = sys.exc_info() + print "Exception in BreakListenerThread.run():", str(tp), str(val) + self._keepGoing = False + + def AskToStop(self): + self._keepGoing = False + if type(self._server) is not types.NoneType: + if _VERBOSE: print "Before calling server close on breakpoint server" + self._server.server_close() + if _VERBOSE: print "Calling server close on breakpoint server" + + +class DebuggerHarness(object): + + def __init__(self): + # Host and port for debugger-side RPC server + self._hostname = sys.argv[1] + self._portNumber = int(sys.argv[2]) + # Name the gui proxy object is registered under + self._breakPortNumber = int(sys.argv[3]) + # Host and port where the gui proxy can be found. + self._guiHost = sys.argv[4] + self._guiPort = int(sys.argv[5]) + # Command to debug. + self._command = sys.argv[6] + # Strip out the harness' arguments so that the process we run will see argv as if + # it was called directly. + sys.argv = sys.argv[6:] + self._currentFrame = None + self._wait = False + # Connect to the gui-side RPC server. + self._guiServerUrl = 'http://' + self._guiHost + ':' + str(self._guiPort) + '/' + if _VERBOSE: print "Connecting to gui server at ", self._guiServerUrl + self._guiServer = xmlrpclib.ServerProxy(self._guiServerUrl,allow_none=1) + + # Start the break listener + self._breakQueue = Queue.Queue(50) + self._breakListener = BreakListenerThread(self._hostname, self._breakPortNumber, self._breakQueue) + self._breakListener.start() + # Create the debugger. + self._adb = Adb(self, self._breakQueue) + + # Create the debugger-side RPC Server and register functions for remote calls. + self._server = AGXMLRPCServer((self._hostname, self._portNumber), logRequests=0) + self._server.register_function(self.set_step) + self._server.register_function(self.set_continue) + self._server.register_function(self.set_next) + self._server.register_function(self.set_return) + self._server.register_function(self.set_breakpoint) + self._server.register_function(self.clear_breakpoint) + self._server.register_function(self.set_all_breakpoints) + self._server.register_function(self.attempt_introspection) + self._server.register_function(self.add_watch) + + self.message_frame_dict = {} + self.introspection_list = [] + atexit.register(self.do_exit) + + def run(self): + self._adb.runFile(self._command) + self.do_exit(kill=True) + + + def do_exit(self, kill=False): + self._adb.set_quit() + self._breakListener.AskToStop() + self._server.server_close() + try: + self._guiServer.quit() + except: + pass + if kill: + try: + sys.exit() + except: + pass + + def set_breakpoint(self, fileName, lineNo): + self._adb.set_break(fileName, lineNo) + return "" + + def set_all_breakpoints(self, dict): + self._adb.clear_all_breaks() + for fileName in dict.keys(): + lineList = dict[fileName] + for lineNumber in lineList: + self._adb.set_break(fileName, int(lineNumber)) + if _VERBOSE: print "Setting break at ", str(lineNumber), " in file ", fileName + return "" + + def clear_breakpoint(self, fileName, lineNo): + self._adb.clear_break(fileName, lineNo) + return "" + + def add_watch(self, name, text, frame_message, run_once): + if len(frame_message) > 0: + frame = self.message_frame_dict[frame_message] + try: + item = eval(text, frame.f_globals, frame.f_locals) + return self.get_watch_document(item, name) + except: + tp, val, tb = sys.exc_info() + return self.get_exception_document(tp, val, tb) + return "" + + def attempt_introspection(self, frame_message, chain): + try: + frame = self.message_frame_dict[frame_message] + if frame: + name = chain.pop(0) + if name == 'globals': + item = frame.f_globals + elif name == 'locals': + item = frame.f_locals + + for name in chain: + item = self.getNextItem(item, name) + return self.get_introspection_document(item, name) + except: + tp, val, tb = sys.exc_info() + traceback.print_exception(tp, val, tb) + return self.get_empty_introspection_document() + + def getNextItem(self, link, identifier): + tp = type(link) + if self.isTupleized(identifier): + return self.deTupleize(link, identifier) + else: + if tp == types.DictType or tp == types.DictProxyType: + return link[identifier] + else: + if hasattr(link, identifier): + return getattr(link, identifier) + if _VERBOSE or True: print "Failed to find link ", identifier, " on thing: ", self.saferepr(link), " of type ", repr(type(link)) + return None + + def isPrimitive(self, item): + tp = type(item) + return tp is types.IntType or tp is types.LongType or tp is types.FloatType \ + or tp is types.BooleanType or tp is types.ComplexType \ + or tp is types.StringType + + def isTupleized(self, value): + return value.count('[') + + def deTupleize(self, link, string1): + try: + start = string1.find('[') + end = string1.find(']') + num = int(string1[start+1:end]) + return link[num] + except: + tp,val,tb = sys.exc_info() + if _VERBOSE: print "Got exception in deTupleize: ", val + return None + + def wrapAndCompress(self, stringDoc): + import bz2 + return xmlrpclib.Binary(bz2.compress(stringDoc)) + + def get_empty_introspection_document(self): + doc = getDOMImplementation().createDocument(None, "replacement", None) + return self.wrapAndCompress(doc.toxml()) + + def get_watch_document(self, item, identifier): + doc = getDOMImplementation().createDocument(None, "watch", None) + top_element = doc.documentElement + self.addAny(top_element, identifier, item, doc, 2) + return self.wrapAndCompress(doc.toxml()) + + def get_introspection_document(self, item, identifier): + doc = getDOMImplementation().createDocument(None, "replacement", None) + top_element = doc.documentElement + self.addAny(top_element, identifier, item, doc, 2) + return self.wrapAndCompress(doc.toxml()) + + def get_exception_document(self, name, tp, val, tb): + stack = traceback.format_exception(tp, val, tb) + wholeStack = "" + for line in stack: + wholeStack += line + doc = getDOMImplementation().createDocument(None, "watch", None) + top_element = doc.documentElement + item_node = doc.createElement("dict_nv_element") + item_node.setAttribute('value', wholeStack) + item_node.setAttribute('name', str(name)) + top_element.appendChild(item_node) + + def addAny(self, top_element, name, item, doc, ply): + tp = type(item) + if ply < 1: + self.addNode(top_element,name, self.saferepr(item), doc) + elif tp is types.TupleType or tp is types.ListType: + self.addTupleOrList(top_element, name, item, doc, ply - 1) + elif tp is types.DictType or tp is types.DictProxyType: + self.addDict(top_element, name, item, doc, ply -1) + elif inspect.ismodule(item): + self.addModule(top_element, name, item, doc, ply -1) + elif inspect.isclass(item) or tp is types.InstanceType: + self.addClass(top_element, name, item, doc, ply -1) + #elif hasattr(item, '__dict__'): + # self.addDictAttr(top_element, name, item, doc, ply -1) + elif hasattr(item, '__dict__'): + self.addDict(top_element, name, item.__dict__, doc, ply -1) + else: + self.addNode(top_element,name, self.saferepr(item), doc) + + def addTupleOrList(self, top_node, name, tupple, doc, ply): + tupleNode = doc.createElement('tuple') + tupleNode.setAttribute('name', str(name)) + tupleNode.setAttribute('value', str(type(tupple))) + top_node.appendChild(tupleNode) + count = 0 + for item in tupple: + self.addAny(tupleNode, name +'[' + str(count) + ']',item, doc, ply -1) + count += 1 + + + def getFrameXML(self, base_frame): + doc = getDOMImplementation().createDocument(None, "stack", None) + top_element = doc.documentElement + + stack = [] + frame = base_frame + while frame is not None: + if((frame.f_code.co_filename.count('DebuggerHarness.py') == 0) or _DEBUG_DEBUGGER): + stack.append(frame) + frame = frame.f_back + stack.reverse() + self.message_frame_dict = {} + for f in stack: + self.addFrame(f,top_element, doc) + return doc.toxml() + + def addFrame(self, frame, root_element, document): + frameNode = document.createElement('frame') + root_element.appendChild(frameNode) + + code = frame.f_code + filename = code.co_filename + frameNode.setAttribute('file', str(filename)) + frameNode.setAttribute('line', str(frame.f_lineno)) + message = self._adb.frame2message(frame) + frameNode.setAttribute('message', message) + #print "Frame: %s %s %s" %(message, frame.f_lineno, filename) + self.message_frame_dict[message] = frame + self.addDict(frameNode, "locals", frame.f_locals, document, 2) + self.addNode(frameNode, "globals", "", document) + + def getRepr(self, varName, globals, locals): + try: + return repr(eval(varName, globals, locals)) + except: + return 'Error: Could not recover value.' + + def addNode(self, parent_node, name, value, document): + item_node = document.createElement("dict_nv_element") + item_node.setAttribute('value', self.saferepr(value)) + item_node.setAttribute('name', str(name)) + parent_node.appendChild(item_node) + + def addDictAttr(self, root_node, name, thing, document, ply): + dict_node = document.createElement('thing') + root_node.setAttribute('name', name) + root_node.setAttribute('value', str(type(dict)) + " add attr") + self.addDict(root_node, name, thing.__dict__, document, ply) # Not decreminting ply + + def saferepr(self, thing): + try: + return repr(thing) + except: + tp, val, tb = sys.exc_info() + return repr(val) + + def addDict(self, root_node, name, dict, document, ply): + dict_node = document.createElement('dict') + dict_node.setAttribute('name', name) + dict_node.setAttribute('value', str(type(dict)) + " add dict") + root_node.appendChild(dict_node) + for key in dict.keys(): + strkey = str(key) + try: + self.addAny(dict_node, strkey, dict[key], document, ply-1) + except: + tp,val,tb=sys.exc_info() + if _VERBOSE: + print "Error recovering key: ", str(key), " from node ", str(name), " Val = ", str(val) + traceback.print_exception(tp, val, tb) + self.addAny(dict_node, strkey, "Exception getting " + str(name) + "[" + strkey + "]: " + str(val), document, ply -1) + + def addClass(self, root_node, name, class_item, document, ply): + item_node = document.createElement('class') + item_node.setAttribute('name', str(name)) + root_node.appendChild(item_node) + try: + if hasattr(class_item, '__dict__'): + self.addAny(item_node, '__dict__', class_item.__dict__, document, ply -1) + except: + tp,val,tb=sys.exc_info() + if _VERBOSE: + traceback.print_exception(tp, val, tb) + self.addAny(item_node, '__dict__', "Exception getting __dict__: " + str(val), document, ply -1) + try: + if hasattr(class_item, '__name__'): + self.addAny(item_node,'__name__',class_item.__name__, document, ply -1) + except: + tp,val,tb=sys.exc_info() + if _VERBOSE: + traceback.print_exception(tp, val, tb) + self.addAny(item_node,'__name__',"Exception getting class.__name__: " + val, document, ply -1) + try: + if hasattr(class_item, '__module__'): + self.addAny(item_node, '__module__', class_item.__module__, document, ply -1) + except: + tp,val,tb=sys.exc_info() + if _VERBOSE: + traceback.print_exception(tp, val, tb) + self.addAny(item_node, '__module__', "Exception getting class.__module__: " + val, document, ply -1) + try: + if hasattr(class_item, '__doc__'): + self.addAny(item_node, '__doc__', class_item.__doc__, document, ply -1) + except: + tp,val,tb=sys.exc_info() + if _VERBOSE: + traceback.print_exception(tp, val, tb) + self.addAny(item_node, '__doc__', "Exception getting class.__doc__: " + val, document, ply -1) + try: + if hasattr(class_item, '__bases__'): + self.addAny(item_node, '__bases__', class_item.__bases__, document, ply -1) + except: + tp,val,tb=sys.exc_info() + if _VERBOSE: + traceback.print_exception(tp, val, tb) + self.addAny(item_node, '__bases__', "Exception getting class.__bases__: " + val, document, ply -1) + + def addModule(self, root_node, name, module_item, document, ply): + item_node = document.createElement('module') + item_node.setAttribute('name', str(name)) + root_node.appendChild(item_node) + try: + if hasattr(module_item, '__file__'): + self.addAny(item_node, '__file__', module_item.__file__, document, ply -1) + except: + pass + try: + if hasattr(module_item, '__doc__'): + self.addAny(item_node,'__doc__', module_item.__doc__, document, ply -1) + except: + pass + + # The debugger calls this method when it reaches a breakpoint. + def interaction(self, message, frame, info): + if _VERBOSE: + print 'hit debug side interaction' + self._userBreak = False + + self._currentFrame = frame + done = False + while not done: + try: + import bz2 + xml = self.getFrameXML(frame) + arg = xmlrpclib.Binary(bz2.compress(xml)) + if _VERBOSE: + print '============== calling gui side interaction============' + self._guiServer.interaction(xmlrpclib.Binary(message), arg, info) + if _VERBOSE: + print 'after interaction' + done = True + except: + tp, val, tb = sys.exc_info() + if True or _VERBOSE: + print 'Error contacting GUI server!: ' + try: + traceback.print_exception(tp, val, tb) + except: + print "Exception printing traceback", + tp, val, tb = sys.exc_info() + traceback.print_exception(tp, val, tb) + done = False + # Block while waiting to be called back from the GUI. Eventually, self._wait will + # be set false by a function on this side. Seems pretty lame--I'm surprised it works. + self.waitForRPC() + + + def waitForRPC(self): + self._wait = True + while self._wait : + try: + if _VERBOSE: + print "+++ in harness wait for rpc, before handle_request" + self._server.handle_request() + if _VERBOSE: + print "+++ in harness wait for rpc, after handle_request" + except: + if _VERBOSE: + tp, val, tb = sys.exc_info() + print "Got waitForRpc exception : ", repr(tp), ": ", val + #time.sleep(0.1) + + def set_step(self): + self._adb.set_step() + self._wait = False + return "" + + def set_continue(self): + self._adb.set_continue() + self._wait = False + return "" + + def set_next(self): + self._adb.set_next(self._currentFrame) + self._wait = False + return "" + + def set_return(self): + self._adb.set_return(self._currentFrame) + self._wait = False + return "" + +if __name__ == '__main__': + try: + harness = DebuggerHarness() + harness.run() + except SystemExit: + print "Exiting..." + except: + tp, val, tb = sys.exc_info() + traceback.print_exception(tp, val, tb) diff --git a/wxPython/samples/ide/activegrid/tool/DebuggerService.py b/wxPython/samples/ide/activegrid/tool/DebuggerService.py new file mode 100644 index 0000000000..ef4a37a879 --- /dev/null +++ b/wxPython/samples/ide/activegrid/tool/DebuggerService.py @@ -0,0 +1,2275 @@ +#---------------------------------------------------------------------------- +# Name: DebuggerService.py +# Purpose: Debugger Service for Python. +# +# Author: Matt Fryer +# +# Created: 12/9/04 +# CVS-ID: $Id$ +# Copyright: (c) 2004-2005 ActiveGrid, Inc. +# License: wxWindows License +#---------------------------------------------------------------------------- + +import wx +import wx.lib.intctrl +import wx.lib.docview +import wx.lib.dialogs +import wx.gizmos +import wx._core +import wx.lib.pydocview +import Service +import STCTextEditor +import CodeEditor +import PythonEditor +from IDE import ACTIVEGRID_BASE_IDE +if not ACTIVEGRID_BASE_IDE: + import ProcessModelEditor +import wx.lib.scrolledpanel as scrolled +import sys +import time +import SimpleXMLRPCServer +import xmlrpclib +import os +import threading +import process +import Queue +import SocketServer +import ProjectEditor +import types +from xml.dom.minidom import parse, parseString +import bz2 +import pickle +import DebuggerHarness +import traceback +import StringIO +if wx.Platform == '__WXMSW__': + import win32api + _WINDOWS = True +else: + _WINDOWS = False +_ = wx.GetTranslation + +_VERBOSE = False +_WATCHES_ON = False + +# Class to read from stdout or stderr and write the result to a text control. +# Args: file=file-like object +# callback_function= function that takes a single argument, the line of text +# read. +class OutputReaderThread(threading.Thread): + def __init__(self, file, callback_function, callbackOnExit=None, accumulate=True): + threading.Thread.__init__(self) + self._file = file + self._callback_function = callback_function + self._keepGoing = True + self._lineCount = 0 + self._accumulate = accumulate + self._callbackOnExit = callbackOnExit + + def run(self): + file = self._file + start = time.time() + output = "" + while self._keepGoing: + try: + # This could block--how to handle that? + text = file.readline() + if text == '' or text == None: + self._keepGoing = False + elif not self._accumulate: + self._callback_function(text) + else: + # Should use a buffer? StringIO? + output += text + # Seems as though the read blocks if we got an error, so, to be + # sure that at least some of the exception gets printed, always + # send the first hundred lines back as they come in. + if self._lineCount < 100: + self._callback_function(output) + self._lineCount += 1 + output = "" + elif time.time() - start > 0.25: + try: + self._callback_function(output) + except wx._core.PyDeadObjectError: + # GUI was killed while we were blocked. + self._keepGoing = False + start = time.time() + output = "" + except: + tp, val, tb = sys.exc_info() + print "Exception in OutputReaderThread.run():", tp, val + self._keepGoing = False + if self._callbackOnExit: + try: + self._callbackOnExit() + except wx._core.PyDeadObjectError: + pass + if _VERBOSE: print "Exiting OutputReaderThread" + + def AskToStop(self): + self._keepGoing = False + +import wx.lib.newevent +(UpdateTextEvent, EVT_UPDATE_STDTEXT) = wx.lib.newevent.NewEvent() +(UpdateErrorEvent, EVT_UPDATE_ERRTEXT) = wx.lib.newevent.NewEvent() + +class Executor: + + def GetPythonExecutablePath(): + config = wx.ConfigBase_Get() + path = config.Read("ActiveGridPythonLocation") + if path: + return path + wx.MessageBox(_("To proceed I need to know the location of the python.exe you would like to use.\nTo set this, go to Tools-->Options and use the 'Python' tab to enter a value.\n"), _("Python Executable Location Unknown")) + return None + GetPythonExecutablePath = staticmethod(GetPythonExecutablePath) + + def __init__(self, fileName, wxComponent, arg1=None, arg2=None, arg3=None, arg4=None, arg5=None, arg6=None, arg7=None, arg8=None, arg9=None, callbackOnExit=None): + self._fileName = fileName + self._stdOutCallback = self.OutCall + self._stdErrCallback = self.ErrCall + self._callbackOnExit = callbackOnExit + self._wxComponent = wxComponent + path = Executor.GetPythonExecutablePath() + self._cmd = '"' + path + '" -u \"' + fileName + '\"' + #Better way to do this? Quotes needed for windows file paths. + if(arg1 != None): + self._cmd += ' \"' + arg1 + '\"' + if(arg2 != None): + self._cmd += ' \"' + arg2 + '\"' + if(arg3 != None): + self._cmd += ' \"' + arg3 + '\"' + if(arg4 != None): + self._cmd += ' \"' + arg4 + '\"' + if(arg5 != None): + self._cmd += ' \"' + arg5 + '\"' + if(arg6 != None): + self._cmd += ' \"' + arg6 + '\"' + if(arg7 != None): + self._cmd += ' \"' + arg7 + '\"' + if(arg8 != None): + self._cmd += ' \"' + arg8 + '\"' + if(arg9 != None): + self._cmd += ' \"' + arg9 + '\"' + + self._stdOutReader = None + self._stdErrReader = None + self._process = None + + def OutCall(self, text): + evt = UpdateTextEvent(value = text) + wx.PostEvent(self._wxComponent, evt) + + def ErrCall(self, text): + evt = UpdateErrorEvent(value = text) + wx.PostEvent(self._wxComponent, evt) + + def Execute(self, arguments, startIn=None, environment=None): + if not startIn: + startIn = str(os.getcwd()) + startIn = os.path.abspath(startIn) + command = self._cmd + ' ' + arguments + #stdinput = process.IOBuffer() + #self._process = process.ProcessProxy(command, mode='b', cwd=startIn, stdin=stdinput) + self._process = process.ProcessOpen(command, mode='b', cwd=startIn, env=environment) + # Kick off threads to read stdout and stderr and write them + # to our text control. + self._stdOutReader = OutputReaderThread(self._process.stdout, self._stdOutCallback, callbackOnExit=self._callbackOnExit) + self._stdOutReader.start() + self._stdErrReader = OutputReaderThread(self._process.stderr, self._stdErrCallback, accumulate=False) + self._stdErrReader.start() + + + def DoStopExecution(self): + if(self._process != None): + self._process.kill() + self._process.close() + self._process = None + if(self._stdOutReader != None): + self._stdOutReader.AskToStop() + if(self._stdErrReader != None): + self._stdErrReader.AskToStop() + +class RunCommandUI(wx.Panel): + + def __init__(self, parent, id, fileName): + wx.Panel.__init__(self, parent, id) + self._noteBook = parent + + self.KILL_PROCESS_ID = wx.NewId() + self.CLOSE_TAB_ID = wx.NewId() + + self.Bind(wx.EVT_END_PROCESS, self.OnProcessEnded) + + # GUI Initialization follows + sizer = wx.BoxSizer(wx.HORIZONTAL) + self._tb = tb = wx.ToolBar(self, -1, wx.DefaultPosition, (30,1000), wx.TB_VERTICAL| wx.TB_FLAT, "Runner" ) + tb.SetToolBitmapSize((16,16)) + sizer.Add(tb, 0, wx.EXPAND |wx.ALIGN_LEFT|wx.ALL, 1) + + close_bmp = getCloseBitmap() + tb.AddSimpleTool( self.CLOSE_TAB_ID, close_bmp, _('Close Window')) + wx.EVT_TOOL(self, self.CLOSE_TAB_ID, self.OnToolClicked) + + stop_bmp = getStopBitmap() + tb.AddSimpleTool(self.KILL_PROCESS_ID, stop_bmp, _("Stop the Run.")) + wx.EVT_TOOL(self, self.KILL_PROCESS_ID, self.OnToolClicked) + + tb.Realize() + self._textCtrl = STCTextEditor.TextCtrl(self, wx.NewId()) #id) + sizer.Add(self._textCtrl, 1, wx.ALIGN_LEFT|wx.ALL|wx.EXPAND, 1) + self._textCtrl.SetViewLineNumbers(False) + self._textCtrl.SetReadOnly(True) + if wx.Platform == '__WXMSW__': + font = "Courier New" + else: + font = "Courier" + self._textCtrl.SetFont(wx.Font(9, wx.DEFAULT, wx.NORMAL, wx.NORMAL, faceName = font)) + self._textCtrl.SetFontColor(wx.BLACK) + self._textCtrl.StyleClearAll() + + #Disabling for now...may interfere with file open. wx.stc.EVT_STC_DOUBLECLICK(self._textCtrl, self._textCtrl.GetId(), self.OnDoubleClick) + + self.SetSizer(sizer) + sizer.Fit(self) + + # Executor initialization + self._executor = Executor(fileName, self, callbackOnExit=self.ExecutorFinished) + self.Bind(EVT_UPDATE_STDTEXT, self.AppendText) + self.Bind(EVT_UPDATE_ERRTEXT, self.AppendErrorText) + + def __del__(self): + self._executor.DoStopExecution() + + def Execute(self, initialArgs, startIn, environment): + self._executor.Execute(initialArgs, startIn, environment) + + def ExecutorFinished(self): + self._tb.EnableTool(self.KILL_PROCESS_ID, False) + nb = self.GetParent() + for i in range(0,nb.GetPageCount()): + if self == nb.GetPage(i): + text = nb.GetPageText(i) + newText = text.replace("Running", "Finished") + nb.SetPageText(i, newText) + break + + def StopExecution(self): + self.Unbind(EVT_UPDATE_STDTEXT) + self.Unbind(EVT_UPDATE_ERRTEXT) + self._executor.DoStopExecution() + + def AppendText(self, event): + self._textCtrl.SetReadOnly(False) + self._textCtrl.AddText(event.value) + self._textCtrl.ScrollToLine(self._textCtrl.GetLineCount()) + self._textCtrl.SetReadOnly(True) + + def AppendErrorText(self, event): + self._textCtrl.SetReadOnly(False) + self._textCtrl.SetFontColor(wx.RED) + self._textCtrl.StyleClearAll() + self._textCtrl.AddText(event.value) + self._textCtrl.ScrollToLine(self._textCtrl.GetLineCount()) + self._textCtrl.SetFontColor(wx.BLACK) + self._textCtrl.StyleClearAll() + self._textCtrl.SetReadOnly(True) + + #------------------------------------------------------------------------------ + # Event handling + #----------------------------------------------------------------------------- + + def OnToolClicked(self, event): + id = event.GetId() + + if id == self.KILL_PROCESS_ID: + self._executor.DoStopExecution() + + elif id == self.CLOSE_TAB_ID: + self._executor.DoStopExecution() + index = self._noteBook.GetSelection() + self._noteBook.GetPage(index).Show(False) + self._noteBook.RemovePage(index) + + def OnDoubleClick(self, event): + # Looking for a stack trace line. + lineText, pos = self._textCtrl.GetCurLine() + fileBegin = lineText.find("File \"") + fileEnd = lineText.find("\", line ") + lineEnd = lineText.find(", in ") + if lineText == "\n" or fileBegin == -1 or fileEnd == -1 or lineEnd == -1: + # Check the line before the one that was clicked on + lineNumber = self._textCtrl.GetCurrentLine() + if(lineNumber == 0): + return + lineText = self._textCtrl.GetLine(lineNumber - 1) + fileBegin = lineText.find("File \"") + fileEnd = lineText.find("\", line ") + lineEnd = lineText.find(", in ") + if lineText == "\n" or fileBegin == -1 or fileEnd == -1 or lineEnd == -1: + return + + filename = lineText[fileBegin + 6:fileEnd] + lineNum = int(lineText[fileEnd + 8:lineEnd]) + + foundView = None + openDocs = wx.GetApp().GetDocumentManager().GetDocuments() + for openDoc in openDocs: + if openDoc.GetFilename() == filename: + foundView = openDoc.GetFirstView() + break + + if not foundView: + doc = wx.GetApp().GetDocumentManager().CreateDocument(filename, wx.lib.docview.DOC_SILENT) + foundView = doc.GetFirstView() + + if foundView: + foundView.GetFrame().SetFocus() + foundView.Activate() + foundView.GotoLine(lineNum) + startPos = foundView.PositionFromLine(lineNum) + + # FACTOR THIS INTO DocManager + openDocs = wx.GetApp().GetDocumentManager().GetDocuments() + for openDoc in openDocs: + if(isinstance(openDoc.GetFirstView(), CodeEditor.CodeView)): + openDoc.GetFirstView().GetCtrl().ClearCurrentLineMarkers() + + foundView.GetCtrl().MarkerAdd(lineNum -1, CodeEditor.CodeCtrl.CURRENT_LINE_MARKER_NUM) + + def OnProcessEnded(self, evt): + self._executor.DoStopExecution() + +DEFAULT_PORT = 32032 +DEFAULT_HOST = 'localhost' +PORT_COUNT = 21 + +class DebugCommandUI(wx.Panel): + debuggerPortList = None + debuggers = [] + + def NotifyDebuggersOfBreakpointChange(): + for debugger in DebugCommandUI.debuggers: + debugger.BreakPointChange() + + NotifyDebuggersOfBreakpointChange = staticmethod(NotifyDebuggersOfBreakpointChange) + + def DebuggerRunning(): + for debugger in DebugCommandUI.debuggers: + if debugger._executor: + return True + return False + DebuggerRunning = staticmethod(DebuggerRunning) + + def ShutdownAllDebuggers(): + for debugger in DebugCommandUI.debuggers: + debugger.StopExecution() + + ShutdownAllDebuggers = staticmethod(ShutdownAllDebuggers) + + def GetAvailablePort(): + for index in range( 0, len(DebugCommandUI.debuggerPortList)): + port = DebugCommandUI.debuggerPortList[index] + if DebugCommandUI.PortAvailable(port): + DebugCommandUI.debuggerPortList.pop(index) + return port + wx.MessageBox(_("Out of ports for debugging! Please restart the application builder.\nIf that does not work, check for and remove running instances of python."), _("Out of Ports")) + assert False, "Out of ports for debugger." + + GetAvailablePort = staticmethod(GetAvailablePort) + + def ReturnPortToPool(port): + config = wx.ConfigBase_Get() + startingPort = config.ReadInt("DebuggerStartingPort", DEFAULT_PORT) + if port in range(startingPort, startingPort + PORT_COUNT): + DebugCommandUI.debuggerPortList.append(port) + + ReturnPortToPool = staticmethod(ReturnPortToPool) + + def PortAvailable(port): + config = wx.ConfigBase_Get() + hostname = config.Read("DebuggerHostName", DEFAULT_HOST) + try: + server = AGXMLRPCServer((hostname, port)) + server.server_close() + if _VERBOSE: print "Port ", str(port), " available." + return True + except: + tp,val,tb = sys.exc_info() + if _VERBOSE: traceback.print_exception(tp, val, tb) + if _VERBOSE: print "Port ", str(port), " unavailable." + return False + + PortAvailable = staticmethod(PortAvailable) + + def NewPortRange(): + config = wx.ConfigBase_Get() + startingPort = config.ReadInt("DebuggerStartingPort", DEFAULT_PORT) + DebugCommandUI.debuggerPortList = range(startingPort, startingPort + PORT_COUNT) + NewPortRange = staticmethod(NewPortRange) + + def __init__(self, parent, id, command, service): + # Check for ports before creating the panel. + if not DebugCommandUI.debuggerPortList: + DebugCommandUI.NewPortRange() + self._debuggerPort = str(DebugCommandUI.GetAvailablePort()) + self._guiPort = str(DebugCommandUI.GetAvailablePort()) + self._debuggerBreakPort = str(DebugCommandUI.GetAvailablePort()) + + wx.Panel.__init__(self, parent, id) + + self._parentNoteBook = parent + self._command = command + self._textCtrl = None + self._service = service + self._executor = None + self.STEP_ID = wx.NewId() + self.CONTINUE_ID = wx.NewId() + self.STEP_OUT_ID = wx.NewId() + self.NEXT_ID = wx.NewId() + self.KILL_PROCESS_ID = wx.NewId() + self.CLOSE_WINDOW_ID = wx.NewId() + self.BREAK_INTO_DEBUGGER_ID = wx.NewId() + self.CLEAR_ID = wx.NewId() + self.ADD_WATCH_ID = wx.NewId() + sizer = wx.BoxSizer(wx.VERTICAL) + self._tb = tb = wx.ToolBar(self, -1, wx.DefaultPosition, (1000,30), wx.TB_HORIZONTAL| wx.NO_BORDER| wx.TB_FLAT| wx.TB_TEXT, "Debugger" ) + sizer.Add(tb, 0, wx.EXPAND |wx.ALIGN_LEFT|wx.ALL, 1) + tb.SetToolBitmapSize((16,16)) + + close_bmp = getCloseBitmap() + tb.AddSimpleTool( self.CLOSE_WINDOW_ID, close_bmp, _('Close Window')) + wx.EVT_TOOL(self, self.CLOSE_WINDOW_ID, self.StopAndRemoveUI) + + stop_bmp = getStopBitmap() + tb.AddSimpleTool( self.KILL_PROCESS_ID, stop_bmp, _("Stop Debugging")) + wx.EVT_TOOL(self, self.KILL_PROCESS_ID, self.StopExecution) + + tb.AddSeparator() + + break_bmp = getBreakBitmap() + tb.AddSimpleTool( self.BREAK_INTO_DEBUGGER_ID, break_bmp, _("Break into Debugger")) + wx.EVT_TOOL(self, self.BREAK_INTO_DEBUGGER_ID, self.BreakExecution) + + tb.AddSeparator() + + continue_bmp = getContinueBitmap() + tb.AddSimpleTool( self.CONTINUE_ID, continue_bmp, _("Continue Execution")) + wx.EVT_TOOL(self, self.CONTINUE_ID, self.OnContinue) + + tb.AddSeparator() + + next_bmp = getNextBitmap() + tb.AddSimpleTool( self.NEXT_ID, next_bmp, _("Step to next line")) + wx.EVT_TOOL(self, self.NEXT_ID, self.OnNext) + + step_bmp = getStepInBitmap() + tb.AddSimpleTool( self.STEP_ID, step_bmp, _("Step in")) + wx.EVT_TOOL(self, self.STEP_ID, self.OnSingleStep) + + stepOut_bmp = getStepReturnBitmap() + tb.AddSimpleTool(self.STEP_OUT_ID, stepOut_bmp, _("Stop at function return")) + wx.EVT_TOOL(self, self.STEP_OUT_ID, self.OnStepOut) + + tb.AddSeparator() + if _WATCHES_ON: + watch_bmp = getAddWatchBitmap() + tb.AddSimpleTool(self.ADD_WATCH_ID, watch_bmp, _("Add a watch")) + wx.EVT_TOOL(self, self.ADD_WATCH_ID, self.OnAddWatch) + tb.AddSeparator() + + clear_bmp = getClearOutputBitmap() + tb.AddSimpleTool(self.CLEAR_ID, clear_bmp, _("Clear output pane")) + wx.EVT_TOOL(self, self.CLEAR_ID, self.OnClearOutput) + + tb.Realize() + self.framesTab = None + self.DisableWhileDebuggerRunning() + self._notebook = wx.Notebook(self, -1, wx.DefaultPosition, wx.DefaultSize, wx.LB_DEFAULT, "Debugger") + sizer.Add(self._notebook, 1, wx.ALIGN_LEFT|wx.ALL|wx.EXPAND, 1) + self.consoleTab = self.MakeConsoleTab(self._notebook, wx.NewId(), None) + self.framesTab = self.MakeFramesTab(self._notebook, wx.NewId(), None) + self.breakPointsTab = self.MakeBreakPointsTab(self._notebook, wx.NewId(), None) + self._notebook.AddPage(self.consoleTab, "Output") + self._notebook.AddPage(self.framesTab, "Frames") + self._notebook.AddPage(self.breakPointsTab, "Break Points") + + self._statusBar = wx.StatusBar( self, -1) + self._statusBar.SetFieldsCount(1) + sizer.Add(self._statusBar, 0, wx.EXPAND |wx.ALIGN_LEFT|wx.ALL, 1) + + self.SetStatusText("Starting debug...") + + self.SetSizer(sizer) + sizer.Fit(self) + config = wx.ConfigBase_Get() + self._debuggerHost = self._guiHost = config.Read("DebuggerHostName", DEFAULT_HOST) + url = 'http://' + self._debuggerHost + ':' + self._debuggerPort + '/' + self._breakURL = 'http://' + self._debuggerHost + ':' + self._debuggerBreakPort + '/' + self._callback = DebuggerCallback(self._guiHost, self._guiPort, url, self._breakURL, self) + if DebuggerHarness.__file__.find('library.zip') > 0: + try: + fname = DebuggerHarness.__file__ + parts = fname.split('library.zip') + path = os.path.join(parts[0],'activegrid', 'tool', 'DebuggerHarness.py') + except: + tp, val, tb = sys.exc_info() + traceback.print_exception(tp, val, tb) + + else: + print "Starting debugger on these ports: %s, %s, %s" % (str(self._debuggerPort) , str(self._guiPort) , str(self._debuggerBreakPort)) + path = DebuggerService.ExpandPath(DebuggerHarness.__file__) + self._executor = Executor(path, self, self._debuggerHost, \ + self._debuggerPort, self._debuggerBreakPort, self._guiHost, self._guiPort, self._command, callbackOnExit=self.ExecutorFinished) + + self.Bind(EVT_UPDATE_STDTEXT, self.AppendText) + self.Bind(EVT_UPDATE_ERRTEXT, self.AppendErrorText) + DebugCommandUI.debuggers.append(self) + self._stopped = False + + def OnSingleStep(self, event): + self._callback.SingleStep() + + def OnContinue(self, event): + self._callback.Continue() + + def OnStepOut(self, event): + self._callback.Return() + + def OnNext(self, event): + self._callback.Next() + + def BreakPointChange(self): + if not self._stopped: + self._callback.pushBreakpoints() + self.breakPointsTab.PopulateBPList() + + def __del__(self): + if self in DebugCommandUI.debuggers: + DebugCommandUI.debuggers.remove(self) + + def SwitchToOutputTab(self): + self._notebook.SetSelection(0) + + def DisableWhileDebuggerRunning(self): + self._tb.EnableTool(self.STEP_ID, False) + self._tb.EnableTool(self.CONTINUE_ID, False) + self._tb.EnableTool(self.STEP_OUT_ID, False) + self._tb.EnableTool(self.NEXT_ID, False) + self._tb.EnableTool(self.BREAK_INTO_DEBUGGER_ID, True) + if _WATCHES_ON: + self._tb.EnableTool(self.ADD_WATCH_ID, False) + openDocs = wx.GetApp().GetDocumentManager().GetDocuments() + for openDoc in openDocs: + if(isinstance(openDoc.GetFirstView(), CodeEditor.CodeView)): + openDoc.GetFirstView().GetCtrl().ClearCurrentLineMarkers() + if self.framesTab: + self.framesTab.ClearWhileRunning() + #wx.GetApp().ProcessPendingEvents() #Yield(True) + + def EnableWhileDebuggerStopped(self): + self._tb.EnableTool(self.STEP_ID, True) + self._tb.EnableTool(self.CONTINUE_ID, True) + self._tb.EnableTool(self.STEP_OUT_ID, True) + self._tb.EnableTool(self.NEXT_ID, True) + if _WATCHES_ON: + self._tb.EnableTool(self.ADD_WATCH_ID, True) + self._tb.EnableTool(self.BREAK_INTO_DEBUGGER_ID, False) + #if _WINDOWS: + # wx.GetApp().GetTopWindow().RequestUserAttention() + + def ExecutorFinished(self): + if _VERBOSE: print "In ExectorFinished" + try: + self.DisableAfterStop() + except wx._core.PyDeadObjectError: + pass + try: + nb = self.GetParent() + for i in range(0, nb.GetPageCount()): + if self == nb.GetPage(i): + text = nb.GetPageText(i) + newText = text.replace("Debugging", "Finished") + nb.SetPageText(i, newText) + if _VERBOSE: print "In ExectorFinished, changed tab title." + break + except: + if _VERBOSE: print "In ExectorFinished, got exception" + + def DisableAfterStop(self): + self.DisableWhileDebuggerRunning() + self._tb.EnableTool(self.BREAK_INTO_DEBUGGER_ID, False) + self._tb.EnableTool(self.KILL_PROCESS_ID, False) + + def SynchCurrentLine(self, filename, lineNum): + # FACTOR THIS INTO DocManager + self.DeleteCurrentLineMarkers() + + # Filename will be <string> if we're in a bit of code that was executed from + # a string (rather than a file). I haven't been able to get the original string + # for display. + if filename == '<string>': + return + foundView = None + openDocs = wx.GetApp().GetDocumentManager().GetDocuments() + for openDoc in openDocs: + # This ugliness to prevent comparison failing because the drive letter + # gets lowercased occasionally. Don't know why that happens or why it + # only happens occasionally. + if DebuggerService.ComparePaths(openDoc.GetFilename(),filename): + foundView = openDoc.GetFirstView() + break + + if not foundView: + if _VERBOSE: + print "filename=", filename + doc = wx.GetApp().GetDocumentManager().CreateDocument(DebuggerService.ExpandPath(filename), wx.lib.docview.DOC_SILENT) + foundView = doc.GetFirstView() + + if foundView: + foundView.GetFrame().SetFocus() + foundView.Activate() + foundView.GotoLine(lineNum) + startPos = foundView.PositionFromLine(lineNum) + + foundView.GetCtrl().MarkerAdd(lineNum -1, CodeEditor.CodeCtrl.CURRENT_LINE_MARKER_NUM) + + def DeleteCurrentLineMarkers(self): + openDocs = wx.GetApp().GetDocumentManager().GetDocuments() + for openDoc in openDocs: + if(isinstance(openDoc.GetFirstView(), CodeEditor.CodeView)): + openDoc.GetFirstView().GetCtrl().ClearCurrentLineMarkers() + + def LoadFramesListXML(self, framesXML): + self.framesTab.LoadFramesListXML(framesXML) + + def SetStatusText(self, text): + self._statusBar.SetStatusText(text,0) + + def Execute(self, initialArgs, startIn, environment): + self._callback.start() + self._executor.Execute(initialArgs, startIn, environment) + self._callback.waitForRPC() + + def BreakExecution(self, event): + self._callback.BreakExecution() + + + def StopExecution(self, event): + self._stopped = True + self.DisableAfterStop() + try: + self._callback.ServerClose() + except: + pass + try: + if self._executor: + self._executor.DoStopExecution() + self._executor = None + except: + pass + self.DeleteCurrentLineMarkers() + DebugCommandUI.ReturnPortToPool(self._debuggerPort) + DebugCommandUI.ReturnPortToPool(self._guiPort) + DebugCommandUI.ReturnPortToPool(self._debuggerBreakPort) + + def StopAndRemoveUI(self, event): + self.StopExecution(None) + if self in DebugCommandUI.debuggers: + DebugCommandUI.debuggers.remove(self) + index = self._parentNoteBook.GetSelection() + self._parentNoteBook.GetPage(index).Show(False) + self._parentNoteBook.RemovePage(index) + + def GetConsoleTextControl(self): + return self._textCtrl + + def OnClearOutput(self, event): + self._textCtrl.SetReadOnly(False) + self._textCtrl.ClearAll() + self._textCtrl.SetReadOnly(True) + + def OnAddWatch(self, event): + if self.framesTab: + self.framesTab.OnWatch(event) + + def MakeConsoleTab(self, parent, id, debugger): + panel = wx.Panel(parent, id) + sizer = wx.BoxSizer(wx.HORIZONTAL) + self._textCtrl = STCTextEditor.TextCtrl(panel, wx.NewId()) + sizer.Add(self._textCtrl, 1, wx.ALIGN_LEFT|wx.ALL|wx.EXPAND, 1) + self._textCtrl.SetViewLineNumbers(False) + self._textCtrl.SetReadOnly(True) + if wx.Platform == '__WXMSW__': + font = "Courier New" + else: + font = "Courier" + self._textCtrl.SetFont(wx.Font(9, wx.DEFAULT, wx.NORMAL, wx.NORMAL, faceName = font)) + self._textCtrl.SetFontColor(wx.BLACK) + self._textCtrl.StyleClearAll() + panel.SetSizer(sizer) + sizer.Fit(panel) + + return panel + + def MakeFramesTab(self, parent, id, debugger): + panel = FramesUI(parent, id, self) + return panel + + def MakeBreakPointsTab(self, parent, id, debugger): + panel = BreakpointsUI(parent, id, self) + return panel + + def AppendText(self, event): + self._textCtrl.SetReadOnly(False) + self._textCtrl.AddText(event.value) + self._textCtrl.ScrollToLine(self._textCtrl.GetLineCount()) + self._textCtrl.SetReadOnly(True) + + def AppendErrorText(self, event): + self._textCtrl.SetReadOnly(False) + self._textCtrl.SetFontColor(wx.RED) + self._textCtrl.StyleClearAll() + self._textCtrl.AddText(event.value) + self._textCtrl.ScrollToLine(self._textCtrl.GetLineCount()) + self._textCtrl.SetFontColor(wx.BLACK) + self._textCtrl.StyleClearAll() + self._textCtrl.SetReadOnly(True) + +class BreakpointsUI(wx.Panel): + def __init__(self, parent, id, ui): + wx.Panel.__init__(self, parent, id) + self._ui = ui + self.currentItem = None + self.clearBPID = wx.NewId() + self.Bind(wx.EVT_MENU, self.ClearBreakPoint, id=self.clearBPID) + self.syncLineID = wx.NewId() + self.Bind(wx.EVT_MENU, self.SyncBPLine, id=self.syncLineID) + + sizer = wx.BoxSizer(wx.VERTICAL) + p1 = self + self._bpListCtrl = wx.ListCtrl(p1, -1, pos=wx.DefaultPosition, size=(1000,1000), style=wx.LC_REPORT) + sizer.Add(self._bpListCtrl, 1, wx.ALIGN_LEFT|wx.ALL|wx.EXPAND, 1) + self._bpListCtrl.InsertColumn(0, "File") + self._bpListCtrl.InsertColumn(1, "Line") + self._bpListCtrl.InsertColumn(2, "Path") + self._bpListCtrl.SetColumnWidth(0, 150) + self._bpListCtrl.SetColumnWidth(1, 50) + self._bpListCtrl.SetColumnWidth(2, 450) + self._bpListCtrl.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self.OnListRightClick) + self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.ListItemSelected, self._bpListCtrl) + self.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.ListItemDeselected, self._bpListCtrl) + + self.PopulateBPList() + + p1.SetSizer(sizer) + sizer.Fit(p1) + p1.Layout() + + def PopulateBPList(self): + list = self._bpListCtrl + list.DeleteAllItems() + + bps = wx.GetApp().GetService(DebuggerService).GetMasterBreakpointDict() + index = 0 + for fileName in bps.keys(): + shortFile = os.path.basename(fileName) + lines = bps[fileName] + if lines: + for line in lines: + list.InsertStringItem(index, shortFile) + list.SetStringItem(index, 1, str(line)) + list.SetStringItem(index, 2, fileName) + + def OnListRightClick(self, event): + menu = wx.Menu() + item = wx.MenuItem(menu, self.clearBPID, "Clear Breakpoint") + menu.AppendItem(item) + item = wx.MenuItem(menu, self.syncLineID, "Goto Source Line") + menu.AppendItem(item) + self.PopupMenu(menu, event.GetPosition()) + menu.Destroy() + + def SyncBPLine(self, event): + if self.currentItem != -1: + list = self._bpListCtrl + fileName = list.GetItem(self.currentItem, 2).GetText() + lineNumber = list.GetItem(self.currentItem, 1).GetText() + self._ui.SynchCurrentLine( fileName, int(lineNumber) ) + + def ClearBreakPoint(self, event): + if self.currentItem >= 0: + list = self._bpListCtrl + fileName = list.GetItem(self.currentItem, 2).GetText() + lineNumber = list.GetItem(self.currentItem, 1).GetText() + wx.GetApp().GetService(DebuggerService).OnToggleBreakpoint(None, line=int(lineNumber) -1, fileName=fileName ) + + def ListItemSelected(self, event): + self.currentItem = event.m_itemIndex + + def ListItemDeselected(self, event): + self.currentItem = -1 + +class Watch: + CODE_ALL_FRAMES = 1 + CODE_THIS_BLOCK = 2 + CODE_THIS_LINE = 4 + CODE_RUN_ONCE = 8 + + def __init__(self, name, command, show_code=CODE_ALL_FRAMES): + self._name = name + self._command = command + self._show_code = show_code + +class WatchDialog(wx.Dialog): + WATCH_ALL_FRAMES = "Watch in all frames" + WATCH_THIS_FRAME = "Watch in this frame only" + WATCH_ONCE = "Watch once and delete" + def __init__(self, parent, title, chain): + wx.Dialog.__init__(self, parent, -1, title, style=wx.DEFAULT_DIALOG_STYLE) + self._chain = chain + self.label_2 = wx.StaticText(self, -1, "Watch Name:") + self._watchNameTextCtrl = wx.TextCtrl(self, -1, "") + self.label_3 = wx.StaticText(self, -1, "eval(", style=wx.ALIGN_RIGHT) + self._watchValueTextCtrl = wx.TextCtrl(self, -1, "") + self.label_4 = wx.StaticText(self, -1, ",frame.f_globals, frame.f_locals)") + self.radio_box_1 = wx.RadioBox(self, -1, "Watch Information", choices=[WatchDialog.WATCH_ALL_FRAMES, WatchDialog.WATCH_THIS_FRAME, WatchDialog.WATCH_ONCE], majorDimension=0, style=wx.RA_SPECIFY_ROWS) + + self._okButton = wx.Button(self, wx.ID_OK, "OK", size=(75,-1)) + self._okButton.SetDefault() + self._okButton.SetHelpText(_("The OK button completes the dialog")) + def OnOkClick(event): + if self._watchNameTextCtrl.GetValue() == "": + wx.MessageBox(_("You must enter a name for the watch."), _("Add Watch")) + return + if self._watchValueTextCtrl.GetValue() == "": + wx.MessageBox(_("You must enter some code to run for the watch."), _("Add Watch")) + return + self.EndModal(wx.ID_OK) + self.Bind(wx.EVT_BUTTON, OnOkClick, self._okButton) + + self._cancelButton = wx.Button(self, wx.ID_CANCEL, _("Cancel"), size=(75,-1)) + self._cancelButton.SetHelpText(_("The Cancel button cancels the dialog.")) + + self.__set_properties() + self.__do_layout() + + def GetSettings(self): + return self._watchNameTextCtrl.GetValue(), self._watchValueTextCtrl.GetValue(), self.GetSendFrame(), self.GetRunOnce() + + def GetSendFrame(self): + return (WatchDialog.WATCH_ALL_FRAMES != self.radio_box_1.GetStringSelection()) + + def GetRunOnce(self): + return (WatchDialog.WATCH_ONCE == self.radio_box_1.GetStringSelection()) + + def __set_properties(self): + self.SetTitle("Add a Watch") + #self.SetSize((400, 250)) + self.radio_box_1.SetSelection(0) + + def __do_layout(self): + sizer_1 = wx.BoxSizer(wx.VERTICAL) + grid_sizer_4 = wx.FlexGridSizer(1, 3, 5, 5) + grid_sizer_2 = wx.FlexGridSizer(1, 2, 5, 5) + grid_sizer_2.Add(self.label_2, 0, wx.ALIGN_CENTER_VERTICAL|wx.FIXED_MINSIZE, 0) + grid_sizer_2.Add(self._watchNameTextCtrl, 0, wx.EXPAND, 0) + grid_sizer_2.AddGrowableCol(1) + sizer_1.Add(grid_sizer_2, 1, wx.EXPAND, 0) + grid_sizer_4.Add(self.label_3, 0, wx.ALIGN_CENTER_VERTICAL|wx.FIXED_MINSIZE, 0) + grid_sizer_4.Add(self._watchValueTextCtrl, 0, wx.EXPAND, 0) + grid_sizer_4.AddGrowableCol(1) + grid_sizer_4.Add(self.label_4, 0, wx.ALIGN_CENTER_VERTICAL|wx.FIXED_MINSIZE, 0) + sizer_1.Add(grid_sizer_4, 0, wx.EXPAND, 0) + sizer_1.Add(self.radio_box_1, 0, wx.EXPAND, 0) + + box = wx.BoxSizer(wx.HORIZONTAL) + box.Add(self._okButton, 0, wx.ALIGN_RIGHT|wx.ALL, 5) + box.Add(self._cancelButton, 0, wx.ALIGN_RIGHT|wx.ALL, 5) + sizer_1.Add(box, 1, wx.EXPAND, 0) + self.SetAutoLayout(True) + self.SetSizer(sizer_1) + self.Layout() + +class FramesUI(wx.SplitterWindow): + def __init__(self, parent, id, ui): + wx.SplitterWindow.__init__(self, parent, id, style = wx.SP_3D) + self._ui = ui + sizer = wx.BoxSizer(wx.VERTICAL) + self._p1 = p1 = wx.ScrolledWindow(self, -1) + p1.Bind(wx.EVT_SIZE, self.OnSize) + + self._framesListCtrl = wx.ListCtrl(p1, -1, pos=wx.DefaultPosition, size=(250,150), style=wx.LC_REPORT) + sizer.Add(self._framesListCtrl, 1, wx.ALIGN_LEFT|wx.ALL|wx.EXPAND, 1) + self._framesListCtrl.InsertColumn(0, "Frame") + self._framesListCtrl.SetColumnWidth(0, 250) + self._framesListCtrl.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self.OnListRightClick) + self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.ListItemSelected, self._framesListCtrl) + self.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.ListItemDeselected, self._framesListCtrl) + + sizer2 = wx.BoxSizer(wx.VERTICAL) + self._p2 = p2 = wx.ScrolledWindow(self, -1) + p2.Bind(wx.EVT_SIZE, self.OnSize) + + self._treeCtrl = wx.gizmos.TreeListCtrl(p2, -1, size=(530,250), style=wx.TR_DEFAULT_STYLE| wx.TR_FULL_ROW_HIGHLIGHT) + self._treeCtrl.Bind(wx.EVT_TREE_ITEM_RIGHT_CLICK, self.OnRightClick) + sizer2.Add(self._framesListCtrl, 1, wx.ALIGN_LEFT|wx.ALL|wx.EXPAND, 1) + tree = self._treeCtrl + tree.AddColumn("Thing") + tree.AddColumn("Value") + tree.SetMainColumn(0) # the one with the tree in it... + tree.SetColumnWidth(0, 175) + tree.SetColumnWidth(1, 355) + self._root = tree.AddRoot("Frame") + tree.SetItemText(self._root, "", 1) + + self.SetMinimumPaneSize(20) + self.SplitVertically(p1, p2, 250) + self.currentItem = None + self.Layout() + + def OnRightClick(self, event): + #Refactor this... + self._introspectItem = event.GetItem() + self._parentChain = self.GetItemChain(event.GetItem()) + watchOnly = len(self._parentChain) < 1 + if not _WATCHES_ON and watchOnly: + return + menu = wx.Menu() + if not watchOnly: + if not hasattr(self, "introspectID"): + self.introspectID = wx.NewId() + self.Bind(wx.EVT_MENU, self.OnIntrospect, id=self.introspectID) + item = wx.MenuItem(menu, self.introspectID, "Attempt Introspection") + menu.AppendItem(item) + menu.AppendSeparator() + if _WATCHES_ON: + if not hasattr(self, "watchID"): + self.watchID = wx.NewId() + self.Bind(wx.EVT_MENU, self.OnWatch, id=self.watchID) + item = wx.MenuItem(menu, self.watchID, "Create a Watch") + menu.AppendItem(item) + menu.AppendSeparator() + if not watchOnly: + if not hasattr(self, "viewID"): + self.viewID = wx.NewId() + self.Bind(wx.EVT_MENU, self.OnView, id=self.viewID) + item = wx.MenuItem(menu, self.viewID, "View in Dialog") + menu.AppendItem(item) + offset = wx.Point(x=0, y=20) + menuSpot = event.GetPoint() + offset + self._treeCtrl.PopupMenu(menu, menuSpot) + menu.Destroy() + self._parentChain = None + self._introspectItem = None + + def GetItemChain(self, item): + parentChain = [] + if item: + if _VERBOSE: print 'Exploding: %s' % self._treeCtrl.GetItemText(item, 0) + while item != self._root: + text = self._treeCtrl.GetItemText(item, 0) + if _VERBOSE: print "Appending ", text + parentChain.append(text) + item = self._treeCtrl.GetItemParent(item) + parentChain.reverse() + return parentChain + + def OnView(self, event): + title = self._treeCtrl.GetItemText(self._introspectItem,0) + value = self._treeCtrl.GetItemText(self._introspectItem,1) + dlg = wx.lib.dialogs.ScrolledMessageDialog(self, value, title, style=wx.DD_DEFAULT_STYLE | wx.RESIZE_BORDER) + dlg.Show() + + def OnWatch(self, event): + try: + if hasattr(self, '_parentChain'): + wd = WatchDialog(wx.GetApp().GetTopWindow(), "Add a Watch", self._parentChain) + else: + wd = WatchDialog(wx.GetApp().GetTopWindow(), "Add a Watch", None) + if wd.ShowModal() == wx.ID_OK: + name, text, send_frame, run_once = wd.GetSettings() + if send_frame: + frameNode = self._stack[int(self.currentItem)] + message = frameNode.getAttribute("message") + else: + message = "" + binType = self._ui._callback._debuggerServer.add_watch(name, text, message, run_once) + xmldoc = bz2.decompress(binType.data) + domDoc = parseString(xmldoc) + nodeList = domDoc.getElementsByTagName('watch') + if len(nodeList) == 1: + watchValue = nodeList.item(0).getAttribute("message") + except: + tp, val, tb = sys.exc_info() + traceback.print_exception(tp, val, tb) + + def OnIntrospect(self, event): + wx.GetApp().GetTopWindow().SetCursor(wx.StockCursor(wx.CURSOR_WAIT)) + + try: + list = self._framesListCtrl + frameNode = self._stack[int(self.currentItem)] + message = frameNode.getAttribute("message") + binType = self._ui._callback._debuggerServer.attempt_introspection(message, self._parentChain) + xmldoc = bz2.decompress(binType.data) + + domDoc = parseString(xmldoc) + nodeList = domDoc.getElementsByTagName('replacement') + replacementNode = nodeList.item(0) + if len(replacementNode.childNodes): + thingToWalk = replacementNode.childNodes.item(0) + tree = self._treeCtrl + parent = tree.GetItemParent(self._introspectItem) + treeNode = self.AppendSubTreeFromNode(thingToWalk, thingToWalk.getAttribute('name'), parent, insertBefore=self._introspectItem) + tree.Delete(self._introspectItem) + except: + tp,val,tb = sys.exc_info() + traceback.print_exception(tp, val, tb) + + wx.GetApp().GetTopWindow().SetCursor(wx.StockCursor(wx.CURSOR_DEFAULT)) + + def OnSize(self, event): + self._treeCtrl.SetSize(self._p2.GetSize()) + w,h = self._p1.GetClientSizeTuple() + self._framesListCtrl.SetDimensions(0, 0, w, h) + + def ClearWhileRunning(self): + list = self._framesListCtrl + list.DeleteAllItems() + tree = self._treeCtrl + tree.Hide() + + def OnListRightClick(self, event): + if not hasattr(self, "syncFrameID"): + self.syncFrameID = wx.NewId() + self.Bind(wx.EVT_MENU, self.OnSyncFrame, id=self.syncFrameID) + menu = wx.Menu() + item = wx.MenuItem(menu, self.syncFrameID, "Goto Source Line") + menu.AppendItem(item) + self.PopupMenu(menu, event.GetPosition()) + menu.Destroy() + + def OnSyncFrame(self, event): + list = self._framesListCtrl + frameNode = self._stack[int(self.currentItem)] + file = frameNode.getAttribute("file") + line = frameNode.getAttribute("line") + self._ui.SynchCurrentLine( file, int(line) ) + + def LoadFramesListXML(self, framesXML): + wx.GetApp().GetTopWindow().SetCursor(wx.StockCursor(wx.CURSOR_WAIT)) + try: + domDoc = parseString(framesXML) + list = self._framesListCtrl + list.DeleteAllItems() + self._stack = [] + nodeList = domDoc.getElementsByTagName('frame') + frame_count = -1 + for index in range(0, nodeList.length): + frameNode = nodeList.item(index) + message = frameNode.getAttribute("message") + list.InsertStringItem(index, message) + self._stack.append(frameNode) + frame_count += 1 + list.Select(frame_count) + self._p1.FitInside() + frameNode = nodeList.item(index) + file = frameNode.getAttribute("file") + line = frameNode.getAttribute("line") + self._ui.SynchCurrentLine( file, int(line) ) + except: + tp,val,tb=sys.exc_info() + traceback.print_exception(tp, val, tb) + + wx.GetApp().GetTopWindow().SetCursor(wx.StockCursor(wx.CURSOR_DEFAULT)) + + def ListItemDeselected(self, event): + pass + + def ListItemSelected(self, event): + self.currentItem = event.m_itemIndex + frameNode = self._stack[int(self.currentItem)] + self.PopulateTreeFromFrameNode(frameNode) + # Temporarily doing this to test out automatically swicting to source line. + self.OnSyncFrame(None) + + def PopulateTreeFromFrameNode(self, frameNode): + tree = self._treeCtrl + tree.Show(True) + root = self._root + tree.DeleteChildren(root) + children = frameNode.childNodes + firstChild = None + for index in range(0, children.length): + subNode = children.item(index) + treeNode = self.AppendSubTreeFromNode(subNode, subNode.getAttribute('name'), root) + if not firstChild: + firstChild = treeNode + tree.Expand(root) + tree.Expand(firstChild) + self._p2.FitInside() + + def AppendSubTreeFromNode(self, node, name, parent, insertBefore=None): + tree = self._treeCtrl + if insertBefore != None: + treeNode = tree.InsertItem(parent, insertBefore, name) + else: + treeNode = tree.AppendItem(parent, name) + children = node.childNodes + if children.length == 0: + tree.SetItemText(treeNode, self.StripOuterSingleQuotes(node.getAttribute("value")), 1) + for index in range(0, children.length): + subNode = children.item(index) + if self.HasChildren(subNode): + self.AppendSubTreeFromNode(subNode, subNode.getAttribute("name"), treeNode) + else: + name = subNode.getAttribute("name") + value = self.StripOuterSingleQuotes(subNode.getAttribute("value")) + n = tree.AppendItem(treeNode, name) + tree.SetItemText(n, value, 1) + return treeNode + + def StripOuterSingleQuotes(self, string): + if string.startswith("'") and string.endswith("'"): + return string[1:-1] + elif type(string) == types.UnicodeType: + return string[1:-1] + else: + return string + + def HasChildren(self, node): + try: + return node.childNodes.length > 0 + except: + tp,val,tb=sys.exc_info() + return False + +class DebuggerView(Service.ServiceView): + + #---------------------------------------------------------------------------- + # Overridden methods + #---------------------------------------------------------------------------- + + def __init__(self, service): + Service.ServiceView.__init__(self, service) + + def _CreateControl(self, parent, id): + return None + + #------------------------------------------------------------------------------ + # Event handling + #----------------------------------------------------------------------------- + + def OnToolClicked(self, event): + self.GetFrame().ProcessEvent(event) + + #------------------------------------------------------------------------------ + # Class methods + #----------------------------------------------------------------------------- + +class Interaction: + def __init__(self, message, framesXML, info=None, quit=False): + self._framesXML = framesXML + self._message = message + self._info = info + self._quit = quit + + def getFramesXML(self): + return self._framesXML + + def getMessage(self): + return self._message + + def getInfo(self): + return self._info + + def getQuit(self): + return self._quit + +class AGXMLRPCServer(SimpleXMLRPCServer.SimpleXMLRPCServer): + def __init__(self, address, logRequests=0): + SimpleXMLRPCServer.SimpleXMLRPCServer.__init__(self, address, logRequests=logRequests) + +class RequestHandlerThread(threading.Thread): + def __init__(self, queue, address): + threading.Thread.__init__(self) + self._keepGoing = True + self._queue = queue + self._address = address + self._server = AGXMLRPCServer(self._address,logRequests=0) + self._server.register_function(self.interaction) + self._server.register_function(self.quit) + self._server.register_function(self.dummyOperation) + if _VERBOSE: print "RequestHandlerThread on fileno %s" % str(self._server.fileno()) + + def run(self): + while self._keepGoing: + try: + self._server.handle_request() + except: + tp, val, tb = sys.exc_info() + traceback.print_exception(tp, val, tb) + self._keepGoing = False + if _VERBOSE: print "Exiting Request Handler Thread." + + def interaction(self, message, frameXML, info): + if _VERBOSE: print "In RequestHandlerThread.interaction -- adding to queue" + interaction = Interaction(message, frameXML, info) + self._queue.put(interaction) + return "" + + def quit(self): + interaction = Interaction(None, None, info=None, quit=True) + self._queue.put(interaction) + return "" + + def dummyOperation(self): + return "" + + def AskToStop(self): + self._keepGoing = False + if type(self._server) is not types.NoneType: + try: + # This is a really ugly way to make sure this thread isn't blocked in + # handle_request. + url = 'http://' + self._address[0] + ':' + str(self._address[1]) + '/' + tempServer = xmlrpclib.ServerProxy(url, allow_none=1) + tempServer.dummyOperation() + except: + tp, val, tb = sys.exc_info() + traceback.print_exception(tp, val, tb) + self._server.server_close() + + +class RequestBreakThread(threading.Thread): + def __init__(self, server, interrupt=False, pushBreakpoints=False, breakDict=None, kill=False): + threading.Thread.__init__(self) + self._server = server + + self._interrupt = interrupt + self._pushBreakpoints = pushBreakpoints + self._breakDict = breakDict + self._kill = kill + + def run(self): + try: + if _VERBOSE: print "RequestBreakThread, before call" + if self._interrupt: + self._server.break_requested() + if self._pushBreakpoints: + self._server.update_breakpoints(xmlrpclib.Binary(pickle.dumps(self._breakDict))) + if self._kill: + try: + self._server.die() + except: + pass + if _VERBOSE: print "RequestBreakThread, after call" + except: + tp,val,tb = sys.exc_info() + traceback.print_exception(tp, val, tb) + +class DebuggerOperationThread(threading.Thread): + def __init__(self, function): + threading.Thread.__init__(self) + self._function = function + + def run(self): + if _VERBOSE: print "In DOT, before call" + try: + self._function() + except: + tp,val,tb = sys.exc_info() + traceback.print_exception(tp, val, tb) + if _VERBOSE: print "In DOT, after call" + +class DebuggerCallback: + + def __init__(self, host, port, debugger_url, break_url, debuggerUI): + if _VERBOSE: print "+++++++ Creating server on port, ", str(port) + + self._queue = Queue.Queue(50) + self._host = host + self._port = int(port) + threading._VERBOSE = _VERBOSE + self._serverHandlerThread = RequestHandlerThread(self._queue, (self._host, self._port)) + + self._debugger_url = debugger_url + self._debuggerServer = None + self._waiting = False + self._service = wx.GetApp().GetService(DebuggerService) + self._debuggerUI = debuggerUI + self._break_url = break_url + self._breakServer = None + self._firstInteraction = True + self._pendingBreak = False + + def start(self): + self._serverHandlerThread.start() + + def ServerClose(self): + rbt = RequestBreakThread(self._breakServer, kill=True) + rbt.start() + self.setWaiting(False) + if self._serverHandlerThread: + self._serverHandlerThread.AskToStop() + self._serverHandlerThread = None + + def BreakExecution(self): + rbt = RequestBreakThread(self._breakServer, interrupt=True) + rbt.start() + + def SingleStep(self): + self._debuggerUI.DisableWhileDebuggerRunning() + #dot = DebuggerOperationThread(self._debuggerServer.set_step) + #dot.start() + self._debuggerServer.set_step() # Figure out where to set allowNone + self.waitForRPC() + + def Next(self): + self._debuggerUI.DisableWhileDebuggerRunning() + #dot = DebuggerOperationThread(self._debuggerServer.set_next) + #dot.start() + self._debuggerServer.set_next() + self.waitForRPC() + + def Continue(self): + self._debuggerUI.DisableWhileDebuggerRunning() + #dot = DebuggerOperationThread(self._debuggerServer.set_continue) + #dot.start() + self._debuggerServer.set_continue() + self.waitForRPC() + + def Return(self): + self._debuggerUI.DisableWhileDebuggerRunning() + #dot = DebuggerOperationThread(self._debuggerServer.set_return) + #dot.start() + self._debuggerServer.set_return() + self.waitForRPC() + + def setWaiting(self, value): + self._waiting = value + + def getWaiting(self): + return self._waiting + + def readQueue(self): + if self._queue.qsize(): + try: + item = self._queue.get_nowait() + if item.getQuit(): + self.interaction(None, None, None, True) + else: + data = bz2.decompress(item.getFramesXML().data) + self.interaction(item.getMessage().data, data, item.getInfo(), False) + except Queue.Empty: + pass + + def pushBreakpoints(self): + rbt = RequestBreakThread(self._breakServer, pushBreakpoints=True, breakDict=self._service.GetMasterBreakpointDict()) + rbt.start() + + + def waitForRPC(self): + self.setWaiting(True) + while self.getWaiting(): + try: + self.readQueue() + import time + time.sleep(0.02) + except: + tp, val, tb = sys.exc_info() + traceback.print_exception(tp, val, tb) + wx.GetApp().Yield(True) + if _VERBOSE: print "Exiting waitForRPC." + + def interaction(self, message, frameXML, info, quit): + + #This method should be hit as the debugger starts. + if self._firstInteraction: + self._firstInteraction = False + self._debuggerServer = xmlrpclib.ServerProxy(self._debugger_url, allow_none=1) + self._breakServer = xmlrpclib.ServerProxy(self._break_url, allow_none=1) + self.pushBreakpoints() + self.setWaiting(False) + if _VERBOSE: print "+"*40 + if(quit): + self._debuggerUI.StopExecution(None) + return "" + if(info != ""): + if _VERBOSE: print "Hit interaction with exception" + #self._debuggerUI.StopExecution(None) + #self._debuggerUI.SetStatusText("Got exception: " + str(info)) + self._debuggerUI.SwitchToOutputTab() + else: + if _VERBOSE: print "Hit interaction no exception" + self._debuggerUI.SetStatusText(message) + self._debuggerUI.LoadFramesListXML(frameXML) + self._debuggerUI.EnableWhileDebuggerStopped() + if _VERBOSE: print "+"*40 + +class DebuggerService(Service.Service): + + #---------------------------------------------------------------------------- + # Constants + #---------------------------------------------------------------------------- + TOGGLE_BREAKPOINT_ID = wx.NewId() + CLEAR_ALL_BREAKPOINTS = wx.NewId() + RUN_ID = wx.NewId() + DEBUG_ID = wx.NewId() + + def ComparePaths(first, second): + one = DebuggerService.ExpandPath(first) + two = DebuggerService.ExpandPath(second) + if _WINDOWS: + return one.lower() == two.lower() + else: + return one == two + ComparePaths = staticmethod(ComparePaths) + + # Make sure we're using an expanded path on windows. + def ExpandPath(path): + if _WINDOWS: + try: + return win32api.GetLongPathName(path) + except: + print "Cannot get long path for %s" % path + + return path + + ExpandPath = staticmethod(ExpandPath) + + #---------------------------------------------------------------------------- + # Overridden methods + #---------------------------------------------------------------------------- + + def __init__(self, serviceName, embeddedWindowLocation = wx.lib.pydocview.EMBEDDED_WINDOW_LEFT): + Service.Service.__init__(self, serviceName, embeddedWindowLocation) + self.BREAKPOINT_DICT_STRING = "MasterBreakpointDict" + config = wx.ConfigBase_Get() + pickledbps = config.Read(self.BREAKPOINT_DICT_STRING) + if pickledbps: + try: + self._masterBPDict = pickle.loads(pickledbps.encode('ascii')) + except: + tp, val, tb = sys.exc_info() + traceback.print_exception(tp,val,tb) + self._masterBPDict = {} + else: + self._masterBPDict = {} + + def OnCloseFrame(self, event): + # IS THIS THE RIGHT PLACE? + try: + config = wx.ConfigBase_Get() + config.Write(self.BREAKPOINT_DICT_STRING, pickle.dumps(self._masterBPDict)) + except: + tp,val,tb = sys.exc_info() + traceback.print_exception(tp, val, tb) + return True + + def _CreateView(self): + return DebuggerView(self) + + + #---------------------------------------------------------------------------- + # Service specific methods + #---------------------------------------------------------------------------- + + def InstallControls(self, frame, menuBar = None, toolBar = None, statusBar = None, document = None): + #Service.Service.InstallControls(self, frame, menuBar, toolBar, statusBar, document) + + config = wx.ConfigBase_Get() + + debuggerMenu = wx.Menu() + if not menuBar.FindItemById(DebuggerService.CLEAR_ALL_BREAKPOINTS): + + debuggerMenu.Append(DebuggerService.RUN_ID, _("&Run...\tCtrl+R"), _("Runs a file")) + wx.EVT_MENU(frame, DebuggerService.RUN_ID, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, DebuggerService.RUN_ID, frame.ProcessUpdateUIEvent) + + debuggerMenu.Append(DebuggerService.DEBUG_ID, _("&Debug...\tCtrl+D"), _("Debugs a file")) + wx.EVT_MENU(frame, DebuggerService.DEBUG_ID, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, DebuggerService.DEBUG_ID, frame.ProcessUpdateUIEvent) + + debuggerMenu.AppendSeparator() + + debuggerMenu.Append(DebuggerService.TOGGLE_BREAKPOINT_ID, _("&Toggle Breakpoint...\tCtrl+B"), _("Toggle a breakpoint")) + wx.EVT_MENU(frame, DebuggerService.TOGGLE_BREAKPOINT_ID, self.ProcessEvent) + wx.EVT_UPDATE_UI(frame, DebuggerService.TOGGLE_BREAKPOINT_ID, self.ProcessUpdateUIEvent) + + debuggerMenu.Append(DebuggerService.CLEAR_ALL_BREAKPOINTS, _("&Clear All Breakpoints"), _("Clear All Breakpoints")) + wx.EVT_MENU(frame, DebuggerService.CLEAR_ALL_BREAKPOINTS, self.ProcessEvent) + wx.EVT_UPDATE_UI(frame, DebuggerService.CLEAR_ALL_BREAKPOINTS, self.ProcessUpdateUIEvent) + + + viewMenuIndex = menuBar.FindMenu(_("&Project")) + menuBar.Insert(viewMenuIndex + 1, debuggerMenu, _("&Run")) + + return True + + + + #---------------------------------------------------------------------------- + # Event Processing Methods + #---------------------------------------------------------------------------- + + def ProcessEventBeforeWindows(self, event): + return False + + + def ProcessEvent(self, event): + if Service.Service.ProcessEvent(self, event): + return True + + an_id = event.GetId() + if an_id == DebuggerService.TOGGLE_BREAKPOINT_ID: + self.OnToggleBreakpoint(event) + return True + elif an_id == DebuggerService.CLEAR_ALL_BREAKPOINTS: + self.ClearAllBreakpoints() + return True + elif an_id == DebuggerService.RUN_ID: + self.OnRunProject(event) + return True + elif an_id == DebuggerService.DEBUG_ID: + self.OnDebugProject(event) + return True + return False + + def ProcessUpdateUIEvent(self, event): + if Service.Service.ProcessUpdateUIEvent(self, event): + return True + + an_id = event.GetId() + if an_id == DebuggerService.TOGGLE_BREAKPOINT_ID: + currentView = self.GetDocumentManager().GetCurrentView() + event.Enable(isinstance(currentView, PythonEditor.PythonView)) + return True + elif an_id == DebuggerService.CLEAR_ALL_BREAKPOINTS: + event.Enable(self.HasBreakpointsSet()) + return True + elif an_id == DebuggerService.RUN_ID: + event.Enable(self.HasAnyFiles()) + return True + elif an_id == DebuggerService.DEBUG_ID: + event.Enable(self.HasAnyFiles()) + return True + else: + return False + + #---------------------------------------------------------------------------- + # Class Methods + #---------------------------------------------------------------------------- + + def OnDebugProject(self, event): + if not Executor.GetPythonExecutablePath(): + return + if DebugCommandUI.DebuggerRunning(): + wx.MessageBox(_("A debugger is already running. Please shut down the other debugger first."), _("Debugger Running")) + return + self.ShowWindow(True) + projectService = wx.GetApp().GetService(ProjectEditor.ProjectService) + project = projectService.GetView().GetDocument() + dlg = CommandPropertiesDialog(self.GetView().GetFrame(), 'Debug Python File', projectService, None, pythonOnly=True, okButtonName="Debug", debugging=True) + if dlg.ShowModal() == wx.ID_OK: + fileToDebug, initialArgs, startIn, isPython, environment = dlg.GetSettings() + dlg.Destroy() + else: + dlg.Destroy() + return + self.PromptToSaveFiles() + + shortFile = os.path.basename(fileToDebug) + fileToDebug = DebuggerService.ExpandPath(fileToDebug) + try: + page = DebugCommandUI(Service.ServiceView.bottomTab, -1, str(fileToDebug), self) + count = Service.ServiceView.bottomTab.GetPageCount() + Service.ServiceView.bottomTab.AddPage(page, "Debugging: " + shortFile) + Service.ServiceView.bottomTab.SetSelection(count) + page.Execute(initialArgs, startIn, environment) + except: + pass + + def HasAnyFiles(self): + docs = wx.GetApp().GetDocumentManager().GetDocuments() + return len(docs) > 0 + + def PromptToSaveFiles(self, running=True): + filesModified = False + docs = wx.GetApp().GetDocumentManager().GetDocuments() + for doc in docs: + if doc.IsModified(): + filesModified = True + break + if filesModified: + frame = self.GetView().GetFrame() + if running: + yesNoMsg = wx.MessageDialog(frame, + _("Files have been modified.\nWould you like to save all files before running?"), + _("Run"), + wx.YES_NO + ) + else: + yesNoMsg = wx.MessageDialog(frame, + _("Files have been modified.\nWould you like to save all files before debugging?"), + _("Debug"), + wx.YES_NO + ) + if yesNoMsg.ShowModal() == wx.ID_YES: + docs = wx.GetApp().GetDocumentManager().GetDocuments() + for doc in docs: + doc.Save() + + def OnExit(self): + DebugCommandUI.ShutdownAllDebuggers() + + def OnRunProject(self, event): + if not Executor.GetPythonExecutablePath(): + return + projectService = wx.GetApp().GetService(ProjectEditor.ProjectService) + project = projectService.GetView().GetDocument() + dlg = CommandPropertiesDialog(self.GetView().GetFrame(), 'Run', projectService, None) + if dlg.ShowModal() == wx.ID_OK: + fileToRun, initialArgs, startIn, isPython, environment = dlg.GetSettings() + + + dlg.Destroy() + else: + dlg.Destroy() + return + self.PromptToSaveFiles() + # This will need to change when we can run more than .py and .bpel files. + if not isPython: + projectService.RunProcessModel(fileToRun) + return + + self.ShowWindow(True) + shortFile = os.path.basename(fileToRun) + page = RunCommandUI(Service.ServiceView.bottomTab, -1, str(fileToRun)) + count = Service.ServiceView.bottomTab.GetPageCount() + Service.ServiceView.bottomTab.AddPage(page, "Running: " + shortFile) + Service.ServiceView.bottomTab.SetSelection(count) + page.Execute(initialArgs, startIn, environment) + + def OnToggleBreakpoint(self, event, line=-1, fileName=None): + if not fileName: + view = wx.GetApp().GetDocumentManager().GetCurrentView() + # Test to make sure we aren't the project view. + if not hasattr(view, 'MarkerExists'): + return + fileName = wx.GetApp().GetDocumentManager().GetCurrentDocument().GetFilename() + if line < 0: + line = view.GetCtrl().GetCurrentLine() + if self.BreakpointSet(fileName, line + 1): + self.ClearBreak(fileName, line + 1) + else: + self.SetBreak(fileName, line + 1) + # Now refresh all the markers icons in all the open views. + self.ClearAllBreakpointMarkers() + self.SetAllBreakpointMarkers() + + def SilentToggleBreakpoint(self, fileName, line): + found = False + for lineNumber in self.GetBreakpointList(fileName): + if int(lineNumber) == int(line): + found = True + break + if found: + self.SetBreak(fileName, line) + else: + self.ClearBreak(fileName, line) + + def SetBreak(self, fileName, line): + expandedName = DebuggerService.ExpandPath(fileName) + if not self._masterBPDict.has_key(expandedName): + self._masterBPDict[expandedName] = [line] + else: + self._masterBPDict[expandedName] += [line] + # If we're already debugging, pass this bp off to the DebuggerCallback + self.NotifyDebuggersOfBreakpointChange() + + def NotifyDebuggersOfBreakpointChange(self): + DebugCommandUI.NotifyDebuggersOfBreakpointChange() + + def GetBreakpointList(self, fileName): + expandedName = DebuggerService.ExpandPath(fileName) + if not self._masterBPDict.has_key(expandedName): + return [] + else: + return self._masterBPDict[expandedName] + + def BreakpointSet(self, fileName, line): + expandedName = DebuggerService.ExpandPath(fileName) + if not self._masterBPDict.has_key(expandedName): + return False + else: + newList = [] + for number in self._masterBPDict[expandedName]: + if(int(number) == int(line)): + return True + return False + + def ClearBreak(self, fileName, line): + expandedName = DebuggerService.ExpandPath(fileName) + if not self._masterBPDict.has_key(expandedName): + print "In ClearBreak: no key" + return + else: + newList = [] + for number in self._masterBPDict[expandedName]: + if(int(number) != int(line)): + newList.append(number) + self._masterBPDict[expandedName] = newList + self.NotifyDebuggersOfBreakpointChange() + + def HasBreakpointsSet(self): + for key, value in self._masterBPDict.items(): + if len(value) > 0: + return True + return False + + def ClearAllBreakpoints(self): + self._masterBPDict = {} + self.NotifyDebuggersOfBreakpointChange() + self.ClearAllBreakpointMarkers() + + def ClearAllBreakpointMarkers(self): + openDocs = wx.GetApp().GetDocumentManager().GetDocuments() + for openDoc in openDocs: + if(isinstance(openDoc.GetFirstView(), CodeEditor.CodeView)): + openDoc.GetFirstView().MarkerDeleteAll(CodeEditor.CodeCtrl.BREAKPOINT_MARKER_NUM) + + def GetMasterBreakpointDict(self): + return self._masterBPDict + + def SetAllBreakpointMarkers(self): + openDocs = wx.GetApp().GetDocumentManager().GetDocuments() + for openDoc in openDocs: + if(isinstance(openDoc.GetFirstView(), CodeEditor.CodeView)): + self.SetCurrentBreakpointMarkers(openDoc.GetFirstView()) + + def SetCurrentBreakpointMarkers(self, view): + if isinstance(view, CodeEditor.CodeView) and hasattr(view, 'GetDocument'): + view.MarkerDeleteAll(CodeEditor.CodeCtrl.BREAKPOINT_MARKER_NUM) + for linenum in self.GetBreakpointList(view.GetDocument().GetFilename()): + view.MarkerAdd(lineNum=int(linenum) - 1, marker_index=CodeEditor.CodeCtrl.BREAKPOINT_MARKER_NUM) + +class DebuggerOptionsPanel(wx.Panel): + + + def __init__(self, parent, id): + wx.Panel.__init__(self, parent, id) + SPACE = 10 + config = wx.ConfigBase_Get() + localHostStaticText = wx.StaticText(self, -1, _("Local Host Name:")) + self._LocalHostTextCtrl = wx.TextCtrl(self, -1, config.Read("DebuggerHostName", DEFAULT_HOST), size = (150, -1)) + portNumberStaticText = wx.StaticText(self, -1, _("Port Range:")) + dashStaticText = wx.StaticText(self, -1, _("through to")) + startingPort=config.ReadInt("DebuggerStartingPort", DEFAULT_PORT) + self._PortNumberTextCtrl = wx.lib.intctrl.IntCtrl(self, -1, startingPort, size = (50, -1)) + self._PortNumberTextCtrl.SetMin(1)#What are real values? + self._PortNumberTextCtrl.SetMax(65514) #What are real values? + self.Bind(wx.lib.intctrl.EVT_INT, self.MinPortChange, self._PortNumberTextCtrl) + + self._EndPortNumberTextCtrl = wx.lib.intctrl.IntCtrl(self, -1, startingPort + PORT_COUNT, size = (50, -1)) + self._EndPortNumberTextCtrl.SetMin(22)#What are real values? + self._EndPortNumberTextCtrl.SetMax(65535)#What are real values? + self._EndPortNumberTextCtrl.Enable( False ) + debuggerPanelBorderSizer = wx.BoxSizer(wx.VERTICAL) + debuggerPanelSizer = wx.GridBagSizer(hgap = 5, vgap = 5) + debuggerPanelSizer.Add( localHostStaticText, (0,0), flag=wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_LEFT) + debuggerPanelSizer.Add( self._LocalHostTextCtrl, (0,1), (1,3), flag=wx.EXPAND|wx.ALIGN_CENTER) + debuggerPanelSizer.Add( portNumberStaticText, (1,0), flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL) + debuggerPanelSizer.Add( self._PortNumberTextCtrl, (1,1), flag=wx.ALIGN_CENTER) + debuggerPanelSizer.Add( dashStaticText, (1,2), flag=wx.ALIGN_CENTER) + debuggerPanelSizer.Add( self._EndPortNumberTextCtrl, (1,3), flag=wx.ALIGN_CENTER) + FLUSH_PORTS_ID = wx.NewId() + self._flushPortsButton = wx.Button(self, FLUSH_PORTS_ID, "Reset Port List") + wx.EVT_BUTTON(parent, FLUSH_PORTS_ID, self.FlushPorts) + debuggerPanelSizer.Add(self._flushPortsButton, (2,2), (1,2), flag=wx.ALIGN_RIGHT) + + debuggerPanelBorderSizer.Add(debuggerPanelSizer, 0, wx.ALL, SPACE) + self.SetSizer(debuggerPanelBorderSizer) + self.Layout() + parent.AddPage(self, _("Debugger")) + + def FlushPorts(self, event): + if self._PortNumberTextCtrl.IsInBounds(): + config = wx.ConfigBase_Get() + config.WriteInt("DebuggerStartingPort", self._PortNumberTextCtrl.GetValue()) + DebugCommandUI.NewPortRange() + else: + wx.MessageBox(_("The starting port is not valid. Please change the value and try again.", "Invalid Starting Port Number")) + + def MinPortChange(self, event): + self._EndPortNumberTextCtrl.Enable( True ) + self._EndPortNumberTextCtrl.SetValue( self._PortNumberTextCtrl.GetValue() + PORT_COUNT) + self._EndPortNumberTextCtrl.Enable( False ) + + def OnOK(self, optionsDialog): + config = wx.ConfigBase_Get() + config.Write("DebuggerHostName", self._LocalHostTextCtrl.GetValue()) + if self._PortNumberTextCtrl.IsInBounds(): + config.WriteInt("DebuggerStartingPort", self._PortNumberTextCtrl.GetValue()) + +class CommandPropertiesDialog(wx.Dialog): + + def __init__(self, parent, title, projectService, currentProjectDocument, pythonOnly=False, okButtonName="Run", debugging=False): + if _WINDOWS: + wx.Dialog.__init__(self, parent, -1, title) + else: + wx.Dialog.__init__(self, parent, -1, title, size=(390,270)) + self._projService = projectService + self._pmext = None + self._pyext = None + for template in self._projService.GetDocumentManager().GetTemplates(): + if not ACTIVEGRID_BASE_IDE and template.GetDocumentType() == ProcessModelEditor.ProcessModelDocument: + self._pmext = template.GetDefaultExtension() + if template.GetDocumentType() == PythonEditor.PythonDocument: + self._pyext = template.GetDefaultExtension() + self._pythonOnly = pythonOnly + + self._currentProj = currentProjectDocument + projStaticText = wx.StaticText(self, -1, _("Project:")) + fileStaticText = wx.StaticText(self, -1, _("File:")) + argsStaticText = wx.StaticText(self, -1, _("Arguments:")) + startInStaticText = wx.StaticText(self, -1, _("Start in:")) + pythonPathStaticText = wx.StaticText(self, -1, _("PYTHONPATH:")) + postpendStaticText = _("Postpend win32api path") + cpPanelBorderSizer = wx.BoxSizer(wx.VERTICAL) + self._projectNameList, self._projectDocumentList, selectedIndex = self.GetProjectList() + self._projList = wx.Choice(self, -1, (200,-1), choices=self._projectNameList) + self.Bind(wx.EVT_CHOICE, self.EvtListBox, self._projList) + HALF_SPACE = 5 + flexGridSizer = wx.FlexGridSizer(cols = 3, vgap = 10, hgap = 10) + + flexGridSizer.Add(projStaticText, 0, flag=wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_LEFT) + flexGridSizer.Add(self._projList, 1, flag=wx.EXPAND) + flexGridSizer.Add(wx.StaticText(parent, -1, ""), 0) + + flexGridSizer.Add(fileStaticText, 0, flag=wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_LEFT) + self._fileList = wx.Choice(self, -1, (200,-1)) + self.Bind(wx.EVT_CHOICE, self.OnFileSelected, self._fileList) + flexGridSizer.Add(self._fileList, 1, flag=wx.EXPAND) + flexGridSizer.Add(wx.StaticText(parent, -1, ""), 0) + + config = wx.ConfigBase_Get() + self._lastArguments = config.Read("LastRunArguments") + self._argsEntry = wx.TextCtrl(self, -1, str(self._lastArguments)) + self._argsEntry.SetToolTipString(str(self._lastArguments)) + def TextChanged(event): + self._argsEntry.SetToolTipString(event.GetString()) + self.Bind(wx.EVT_TEXT, TextChanged, self._argsEntry) + + flexGridSizer.Add(argsStaticText, 0, flag=wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_LEFT) + flexGridSizer.Add(self._argsEntry, 1, flag=wx.EXPAND) + flexGridSizer.Add(wx.StaticText(parent, -1, ""), 0) + + flexGridSizer.Add(startInStaticText, 0, flag=wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_LEFT) + self._lastStartIn = config.Read("LastRunStartIn") + if not self._lastStartIn: + self._lastStartIn = str(os.getcwd()) + self._startEntry = wx.TextCtrl(self, -1, self._lastStartIn) + self._startEntry.SetToolTipString(self._lastStartIn) + def TextChanged2(event): + self._startEntry.SetToolTipString(event.GetString()) + self.Bind(wx.EVT_TEXT, TextChanged2, self._startEntry) + + flexGridSizer.Add(self._startEntry, 1, wx.EXPAND) + self._findDir = wx.Button(self, -1, _("Browse..."), size=(60,-1)) + self.Bind(wx.EVT_BUTTON, self.OnFindDirClick, self._findDir) + flexGridSizer.Add(self._findDir, 0, wx.RIGHT, 10) + + flexGridSizer.Add(pythonPathStaticText, 0, flag=wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_LEFT) + if os.environ.has_key('PYTHONPATH'): + startval = os.environ['PYTHONPATH'] + else: + startval = "" + self._lastPythonPath = config.Read("LastPythonPath", startval) + self._pythonPathEntry = wx.TextCtrl(self, -1, self._lastPythonPath) + self._pythonPathEntry.SetToolTipString(self._lastPythonPath) + flexGridSizer.Add(self._pythonPathEntry, 1, wx.EXPAND) + flexGridSizer.Add(wx.StaticText(parent, -1, ""), 0) + flexGridSizer.Add(wx.StaticText(parent, -1, ""), 0) + if debugging: + self._postpendCheckBox = wx.CheckBox(self, -1, postpendStaticText) + checked = bool(config.ReadInt("PythonPathPostpend", 1)) + self._postpendCheckBox.SetValue(checked) + flexGridSizer.Add(self._postpendCheckBox, 1, wx.EXPAND) + flexGridSizer.Add(wx.StaticText(parent, -1, ""), 0) + cpPanelBorderSizer.Add(flexGridSizer, 0, wx.ALL, 10) + + box = wx.BoxSizer(wx.HORIZONTAL) + self._okButton = wx.Button(self, wx.ID_OK, okButtonName, size=(75,-1)) + self._okButton.SetDefault() + self._okButton.SetHelpText(_("The ") + okButtonName + _(" button completes the dialog")) + box.Add(self._okButton, 0, wx.ALIGN_RIGHT|wx.ALL, 5) + self.Bind(wx.EVT_BUTTON, self.OnOKClick, self._okButton) + btn = wx.Button(self, wx.ID_CANCEL, _("Cancel"), size=(75,-1)) + btn.SetHelpText(_("The Cancel button cancels the dialog.")) + box.Add(btn, 0, wx.ALIGN_RIGHT|wx.ALL, 5) + cpPanelBorderSizer.Add(box, 0, wx.ALIGN_RIGHT|wx.BOTTOM, 5) + + self.SetSizer(cpPanelBorderSizer) + if _WINDOWS: + self.GetSizer().Fit(self) + + self.Layout() + + # Set up selections based on last values used. + self._fileNameList = None + self._selectedFileIndex = 0 + lastProject = config.Read("LastRunProject") + lastFile = config.Read("LastRunFile") + + if lastProject in self._projectNameList: + selectedIndex = self._projectNameList.index(lastProject) + elif selectedIndex < 0: + selectedIndex = 0 + self._projList.Select(selectedIndex) + self._selectedProjectIndex = selectedIndex + self._selectedProjectDocument = self._projectDocumentList[selectedIndex] + self.PopulateFileList(self._selectedProjectDocument, lastFile) + + def OnOKClick(self, event): + startIn = self._startEntry.GetValue() + fileToRun = self._fileList.GetStringSelection() + if not fileToRun: + wx.MessageBox(_("You must select a file to proceed. Note that not all projects have files that can be run or debugged.")) + return + isPython = fileToRun.endswith(self._pyext) + if isPython and not os.path.exists(startIn): + wx.MessageBox(_("Starting directory does not exist. Please change this value.")) + return + config = wx.ConfigBase_Get() + config.Write("LastRunProject", self._projectNameList[self._selectedProjectIndex]) + config.Write("LastRunFile", fileToRun) + # Don't update the arguments or starting directory unless we're runing python. + if isPython: + config.Write("LastRunArguments", self._argsEntry.GetValue()) + config.Write("LastRunStartIn", self._startEntry.GetValue()) + config.Write("LastPythonPath",self._pythonPathEntry.GetValue()) + if hasattr(self, "_postpendCheckBox"): + config.WriteInt("PythonPathPostpend", int(self._postpendCheckBox.GetValue())) + + self.EndModal(wx.ID_OK) + + def GetSettings(self): + filename = self._fileNameList[self._selectedFileIndex] + args = self._argsEntry.GetValue() + startIn = self._startEntry.GetValue() + isPython = filename.endswith(self._pyext) + env = os.environ + if hasattr(self, "_postpendCheckBox"): + postpend = self._postpendCheckBox.GetValue() + else: + postpend = False + if postpend: + env['PYTHONPATH'] = self._pythonPathEntry.GetValue() + os.pathsep + os.path.join(os.getcwd(), "3rdparty", "pywin32") + else: + env['PYTHONPATH'] = self._pythonPathEntry.GetValue() + + return filename, args, startIn, isPython, env + + def OnFileSelected(self, event): + self._selectedFileIndex = self._fileList.GetSelection() + self.EnableForFileType(event.GetString()) + + def EnableForFileType(self, fileName): + show = fileName.endswith(self._pyext) + self._startEntry.Enable(show) + self._findDir.Enable(show) + self._argsEntry.Enable(show) + + if not show: + self._lastStartIn = self._startEntry.GetValue() + self._startEntry.SetValue("") + self._lastArguments = self._argsEntry.GetValue() + self._argsEntry.SetValue("") + else: + self._startEntry.SetValue(self._lastStartIn) + self._argsEntry.SetValue(self._lastArguments) + + + + def OnFindDirClick(self, event): + dlg = wx.DirDialog(self, "Choose a starting directory:", self._startEntry.GetValue(), + style=wx.DD_DEFAULT_STYLE|wx.DD_NEW_DIR_BUTTON) + + if dlg.ShowModal() == wx.ID_OK: + self._startEntry.SetValue(dlg.GetPath()) + + dlg.Destroy() + + def EvtListBox(self, event): + if event.GetString(): + index = self._projectNameList.index(event.GetString()) + self._selectedProjectDocument = self._projectDocumentList[index] + self._selectedProjectIndex = index + self.PopulateFileList(self._selectedProjectDocument) + + def FilterFileList(self, list): + if self._pythonOnly: + files = filter(lambda f: f.endswith(self._pyext), list) + else: + files = filter(lambda f: (self._pmext and f.endswith(self._pmext)) or f.endswith(self._pyext), list) + return files + + def PopulateFileList(self, project, shortNameToSelect=None): + self._fileNameList = self.FilterFileList(project.GetFiles()[:]) + self._fileList.Clear() + if not self._fileNameList: + return + self._fileNameList.sort(lambda a, b: cmp(os.path.basename(a).lower(), os.path.basename(b).lower())) + strings = map(lambda file: os.path.basename(file), self._fileNameList) + for index in range(0, len(strings)): + if shortNameToSelect == strings[index]: + self._selectedFileIndex = index + break + self._fileList.Hide() + self._fileList.AppendItems(strings) + self._fileList.Show() + if self._selectedFileIndex not in range(0, len(strings)) : self._selectedFileIndex = 0 + self._fileList.SetSelection(self._selectedFileIndex) + self.EnableForFileType(strings[self._selectedFileIndex]) + + def GetProjectList(self): + docList = [] + nameList = [] + found = False + index = -1 + count = 0 + for document in self._projService.GetDocumentManager().GetDocuments(): + if document.GetDocumentTemplate().GetDocumentType() == ProjectEditor.ProjectDocument and len(document.GetFiles()): + docList.append(document) + nameList.append(os.path.basename(document.GetFilename())) + if document == self._currentProj: + found = True + index = count + count += 1 + return nameList, docList, index +#---------------------------------------------------------------------- +from wx import ImageFromStream, BitmapFromImage +from wx import EmptyIcon +import cStringIO +#---------------------------------------------------------------------- +def getBreakData(): + return \ +'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x10\x00\x00\x00\x11\x08\x06\ +\x00\x00\x00\xd4\xaf,\xc4\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\ +\x00\x00\x97IDAT8\x8d\xbdSA\x0e\xc3 \x0c\x8b\x13\xfe=\x1e^\xe2\x1dF\xbb\x8c\ +\xd2\x0c\xa9\xda,q\x88\x05\x8e\x1d\x00P\x93;\xd0[\xa7W\x04\xe8\x8d\x0f\xdfxU\ +c%\x02\xbd\xbd\x05HQ+Xv\xb0\xa3VN\xf9\xd4\x01\xbd\x11j\x18\x1d\x00\x10\xa8AD\ +\xa4\xa4\xd6_\x9b\x19\xbb\x03\xd8c|\x8f\x00\xe0\x93\xa8g>\x15 C\xee:\xe7\x8f\ +\x08\xdesj\xcf\xe6\xde(\xddn\xec\x18f0w\xe0m;\x8d\x9b\xe4\xb1\xd4\n\xa6\x0e2\ +\xc4{\x1f\xeb\xdf?\xe5\xff\t\xa8\x1a$\x0cg\xac\xaf\xb0\xf4\x992<\x01\xec\xa0\ +U9V\xf9\x18\xc8\x00\x00\x00\x00IEND\xaeB`\x82' + +def getBreakBitmap(): + return BitmapFromImage(getBreakImage()) + +def getBreakImage(): + stream = cStringIO.StringIO(getBreakData()) + return ImageFromStream(stream) + +def getBreakIcon(): + icon = EmptyIcon() + icon.CopyFromBitmap(getBreakBitmap()) + return icon + +#---------------------------------------------------------------------- +def getClearOutputData(): + return \ +'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x10\x00\x00\x00\x10\x08\x06\ +\x00\x00\x00\x1f\xf3\xffa\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\ +\x00\x00\xb4IDAT8\x8d\xa5\x92\xdd\r\x03!\x0c\x83\xbf\xa0n\xd4\x9d\xda5\xb81\ +\xbaS\xbb\x12\xee\x03?\xe5\x08\xe5N\xba\xbc Db\xec\xd8p\xb1l\xb8\xa7\x83\xfe\ +\xb0\x02H\x92F\xc0_\xa3\x99$\x99\x99\xedznc\xe36\x81\x88\x98"\xb2\x02\xa2\ +\x1e\xc4Q\x9aUD\x161\xcd\xde\x1c\x83\x15\x084)\x8d\xc5)\x06\xab\xaaZ\x92\xee\ +\xce\x11W\xdbGD\x0cIT\x06\xe7\x00\xdeY\xfe\xcc\x89\x06\xf0\xf2\x99\x00\xe0\ +\x91\x7f\xab\x83\xed\xa4\xc8\xafK\x0c\xcf\x92\x83\x99\x8d\xe3p\xef\xe4\xa1\ +\x0b\xe57j\xc8:\x06\t\x08\x87.H\xb2n\xa8\xc9\xa9\x12vQ\xfeG"\xe3\xacw\x00\ +\x10$M\xd3\x86_\xf0\xe5\xfc\xb4\xfa\x02\xcb\x13j\x10\xc5\xd7\x92D\x00\x00\ +\x00\x00IEND\xaeB`\x82' + +def getClearOutputBitmap(): + return BitmapFromImage(getClearOutputImage()) + +def getClearOutputImage(): + stream = cStringIO.StringIO(getClearOutputData()) + return ImageFromStream(stream) + +def getClearOutputIcon(): + icon = EmptyIcon() + icon.CopyFromBitmap(getClearOutputBitmap()) + return icon + +#---------------------------------------------------------------------- +def getCloseData(): + return \ +'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x12\x00\x00\x00\x12\x08\x06\ +\x00\x00\x00V\xce\x8eW\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\x00\ +\x00\x86IDAT8\x8d\xed\x90\xb1\r\x80 \x10E_"c\xd80\x02-\x138\x87;8\x8f\x8d\ +\x8b\xb0\x02\xa5\xad\rS\x88\xcd\x11) \x82\xb6\xbe\xea\xf2\xc9\xbd\xfc\x03~\ +\xdeb\x81\xb9\x90\xabJ^%\x00N\x849\x0e\xf0\x85\xbc\x8a\x12YZR2\xc7\x1eIB\xcb\ +\xb2\xcb\x9at\x9d\x95c\xa5Y|\x92\x0c\x0f\xa2\r8e\x1e\x81\x1d8z\xdb8\xee?Ig\ +\xfa\xb7\x92)\xcb\xacd\x01XZ$QD\xba\xf0\xa6\x80\xd5\x18cZ\x1b\x95$?\x1f\xb9\ +\x00\x1d\x94\x1e*e_\x8a~\x00\x00\x00\x00IEND\xaeB`\x82' + +def getCloseBitmap(): + return BitmapFromImage(getCloseImage()) + +def getCloseImage(): + stream = cStringIO.StringIO(getCloseData()) + return ImageFromStream(stream) + +def getCloseIcon(): + icon = EmptyIcon() + icon.CopyFromBitmap(getCloseBitmap()) + return icon + +#---------------------------------------------------------------------- +def getContinueData(): + return \ +'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x10\x00\x00\x00\x10\x08\x06\ +\x00\x00\x00\x1f\xf3\xffa\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\ +\x00\x00\x9eIDAT8\x8d\xbd\x92\xd1\r\x83@\x0cC\xed\x8a\x11X\xac\x1b\xddF,\xd6\ +\x11\x90\xdc\x0f\x9a\x93s)\x14Z\xa9\x91\x10\n\x97\xbc\xd89\x80?\x84\x1a\xa4\ +\x83\xfc\x1c$\x1e)7\xdf<Y0\xaf\x0b\xe6\xf5\x1d\xa1\xb5\x13C\x03 !\xaa\xfd\ +\xed\n:mr\xc0\x1d\x8f\xc9\x9a!\t$\xe5\xd3I\xe2\xe5B$\x99\x00[\x01\xe8\xc5\ +\xd9G\xfaN`\xd8\x81I\xed\x8c\xb19\x94\x8d\xcbL\x00;t\xcf\x9fwPh\xdb\x0e\xe8\ +\xd3,\x17\x8b\xc7\x9d\xbb>\x8a \xec5\x94\tc\xc4\x12\xab\x94\xeb\x7fkWr\xc9B%\ +\xfc\xd2\xfcM<\x01\xf6tn\x12O3c\xe6\x00\x00\x00\x00IEND\xaeB`\x82' + +def getContinueBitmap(): + return BitmapFromImage(getContinueImage()) + +def getContinueImage(): + stream = cStringIO.StringIO(getContinueData()) + return ImageFromStream(stream) + +def getContinueIcon(): + icon = EmptyIcon() + icon.CopyFromBitmap(getContinueBitmap()) + return icon + +#---------------------------------------------------------------------- +def getNextData(): + return \ +'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x10\x00\x00\x00\x10\x08\x06\ +\x00\x00\x00\x1f\xf3\xffa\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\ +\x00\x00\x8eIDAT8\x8d\xa5SA\x12\xc4 \x08K\xb0\xff\xde\xe9\xbf\xb7\xa6\x87\ +\x1d:\xba\xa2tZn(\x84`"i\x05obk\x13\xd5CmN+\xcc\x00l\xd6\x0c\x00\xf5\xf8\x0e\ +gK\x06\x00 \xa5=k\x00\x00\xb0\xb2]\xd4?5f\xb1\xdb\xaf\xc6\xa2\xcb\xa8\xf0?\ +\x1c\x98\xae\x82\xbf\x81\xa4\x8eA\x16\xe1\n\xd1\xa4\x19\xb3\xe9\n\xce\xe8\ +\xf1\n\x9eg^\x18\x18\x90\xec<\x11\xf9#\x04XMZ\x19\xaac@+\x94\xd4\x99)SeP\xa1\ +)\xd6\x1dI\xe7*\xdc\xf4\x03\xdf~\xe7\x13T^Q?:X\x19d\x00\x00\x00\x00IEND\xaeB\ +`\x82' + +def getNextBitmap(): + return BitmapFromImage(getNextImage()) + +def getNextImage(): + stream = cStringIO.StringIO(getNextData()) + return ImageFromStream(stream) + +def getNextIcon(): + icon = EmptyIcon() + icon.CopyFromBitmap(getNextBitmap()) + return icon + +#---------------------------------------------------------------------- +def getStepInData(): + return \ +'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x10\x00\x00\x00\x10\x08\x06\ +\x00\x00\x00\x1f\xf3\xffa\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\ +\x00\x00\x87IDAT8\x8d\xadSA\x12\x84 \x0ck\x8a\xffv\xfc\xb74{X\xeb0P@\x07s\ +\x84\xa4$\x01\x00M\xb2\x02]R\x8b\xc86\xda\xdc\xedd\xb4~\xe8\x86\xc6\x01-\x93\ +\x96\xd9#\xf6\x06\xc3;p1I\xd1\x14\x0b#|\x17aF\xec\r\xeeF\xa0eB\xd34\xca\xd0A\ +]j\x84\xa6\x03\x00""\xb7\xb0tRZ\xf7x\xb7\x83\x91]\xcb\x7fa\xd9\x89\x0fC\xfd\ +\x94\x9d|9\x99^k\x13\xa1 \xb3\x16\x0f#\xd4\x88N~\x14\xe1-\x96\x7f\xe3\x0f\ +\x11\x91UC\x0cX\'\x1e\x00\x00\x00\x00IEND\xaeB`\x82' + +def getStepInBitmap(): + return BitmapFromImage(getStepInImage()) + +def getStepInImage(): + stream = cStringIO.StringIO(getStepInData()) + return ImageFromStream(stream) + +def getStepInIcon(): + icon = EmptyIcon() + icon.CopyFromBitmap(getStepInBitmap()) + return icon + +#---------------------------------------------------------------------- +def getStopData(): + return \ +"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x10\x00\x00\x00\x10\x08\x06\ +\x00\x00\x00\x1f\xf3\xffa\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\ +\x00\x00FIDAT8\x8d\xed\x91\xc1\t\xc00\x0c\x03O!\x0b\xa9\xfb\xef\xa6\xfcB\xa1\ +N\t\xf4Wr\xa0\x8f\xb1\x0f\x81\xe1\x97\xe4-\xb6}_V%\xc8\xc2, \t\x92\xe6]\xfbZ\ +\xf7\x08\xa0W\xc3\xea5\xdb\rl_IX\xe5\xf0d\x00\xfa\x8d#\x7f\xc4\xf7'\xab\x00\ +\x00\x00\x00IEND\xaeB`\x82" + +def getStopBitmap(): + return BitmapFromImage(getStopImage()) + +def getStopImage(): + stream = cStringIO.StringIO(getStopData()) + return ImageFromStream(stream) + +def getStopIcon(): + icon = EmptyIcon() + icon.CopyFromBitmap(getStopBitmap()) + return icon + +#---------------------------------------------------------------------- +def getStepReturnData(): + return \ +"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x10\x00\x00\x00\x10\x08\x06\ +\x00\x00\x00\x1f\xf3\xffa\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\ +\x00\x00\x8dIDAT8\x8d\xa5S\xd1\x0e\xc4 \x08\xa3\xb0\xff\xbe\xdc\x7fO\xba'6\ +\xf1\xf44\xb3O$Phk\x04\xd4d\x07\xba\xc5\x16\x91#\nza\xdb\x84\x1a\xa2\xfe\xf8\ +\x99\xfa_=p+\xe8\x91ED\xbc<\xa4 \xb4\x0b\x01\xb5{\x01\xf9\xbbG-\x13\x87\x16f\ +\x84\xbf\x16V\xb0l\x01@\no\x86\xae\x82Q\xa8=\xa4\x0c\x80\xe70\xbd\x10jh\xbd\ +\x07R\x06#\xc9^N\xb6\xde\x03)\x83\x18\xaeU\x90\x9c>a\xb2P\r\xb3&/Y\xa8\xd1^^\ +\xb6\xf0\x16\xdb\xbf\xf1\x02\x81\xa5TK\x1d\x07\xde\x92\x00\x00\x00\x00IEND\ +\xaeB`\x82" + +def getStepReturnBitmap(): + return BitmapFromImage(getStepReturnImage()) + +def getStepReturnImage(): + stream = cStringIO.StringIO(getStepReturnData()) + return ImageFromStream(stream) + +def getStepReturnIcon(): + icon = EmptyIcon() + icon.CopyFromBitmap(getStepReturnBitmap()) + return icon + +def getAddWatchData(): + return \ +'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x10\x00\x00\x00\x10\x08\x06\ +\x00\x00\x00\x1f\xf3\xffa\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\ +\x00\x00\x85IDAT8\x8dc`\x18h\xc0\x88.\xd0\xc0\xf0\xff?*\x9f\x11C\rN\x80\xae\ +\x19\x97\x18\xd1\x9a\x896\x84\x18[p\xa9aA\xe6\xfc7f\xc0P\xc4x\x163\x9cp\x1a0\ +\xeb,!w\x100 \x1dK\xac\x10\r\x08\x05".yFL\x85\x8c\x18b\xa8|Ty\xa2\x13\x92\'\ +\xc3\xe4\xff\x9f\x18\x1e3\xb82t\xa2\x88\x13\xedg.\x06aa&\x06VV\x7f\x86\xb9\ +\xcfU\x19\xbc\xb0\xba\x86h\xe0\xc8\xd0\xfc\xbf\x80\xe1>q)\x94\xe6\x00\x00\ +\x85\x923_\xd22\xa4\xcd\x00\x00\x00\x00IEND\xaeB`\x82' + +def getAddWatchBitmap(): + return BitmapFromImage(getAddWatchImage()) + +def getAddWatchImage(): + stream = cStringIO.StringIO(getAddWatchData()) + return ImageFromStream(stream) + +def getAddWatchIcon(): + icon = EmptyIcon() + icon.CopyFromBitmap(getAddWatchBitmap()) + return icon diff --git a/wxPython/samples/ide/activegrid/tool/FindInDirService.py b/wxPython/samples/ide/activegrid/tool/FindInDirService.py new file mode 100644 index 0000000000..f3e9da16c2 --- /dev/null +++ b/wxPython/samples/ide/activegrid/tool/FindInDirService.py @@ -0,0 +1,446 @@ +#---------------------------------------------------------------------------- +# Name: IDEFindService.py +# Purpose: Find Service for pydocview +# +# Author: Morgan Hua +# +# Created: 8/15/03 +# CVS-ID: $Id$ +# Copyright: (c) 2004-2005 ActiveGrid, Inc. +# License: wxWindows License +#---------------------------------------------------------------------------- + +import wx +import wx.lib.docview +import os +from os.path import join +import re +import ProjectEditor +import MessageService +import FindService +import OutlineService +_ = wx.GetTranslation + + +#---------------------------------------------------------------------------- +# Constants +#---------------------------------------------------------------------------- +FILENAME_MARKER = _("Found in file: ") +PROJECT_MARKER = _("Searching project: ") +FIND_MATCHDIR = "FindMatchDir" +FIND_MATCHDIRSUBFOLDERS = "FindMatchDirSubfolders" + +SPACE = 10 +HALF_SPACE = 5 + + +class FindInDirService(FindService.FindService): + + #---------------------------------------------------------------------------- + # Constants + #---------------------------------------------------------------------------- + FINDALL_ID = wx.NewId() # for bringing up Find All dialog box + FINDDIR_ID = wx.NewId() # for bringing up Find Dir dialog box + + + def InstallControls(self, frame, menuBar = None, toolBar = None, statusBar = None, document = None): + FindService.FindService.InstallControls(self, frame, menuBar, toolBar, statusBar, document) + + editMenu = menuBar.GetMenu(menuBar.FindMenu(_("&Edit"))) + wx.EVT_MENU(frame, FindInDirService.FINDALL_ID, self.ProcessEvent) + wx.EVT_UPDATE_UI(frame, FindInDirService.FINDALL_ID, self.ProcessUpdateUIEvent) + editMenu.Append(FindInDirService.FINDALL_ID, _("Find in Project...\tCtrl+Shift+F"), _("Searches for the specified text in all the files in the project")) + wx.EVT_MENU(frame, FindInDirService.FINDDIR_ID, self.ProcessEvent) + wx.EVT_UPDATE_UI(frame, FindInDirService.FINDDIR_ID, self.ProcessUpdateUIEvent) + editMenu.Append(FindInDirService.FINDDIR_ID, _("Find in Directory..."), _("Searches for the specified text in all the files in the directory")) + + + def ProcessEvent(self, event): + id = event.GetId() + if id == FindInDirService.FINDALL_ID: + self.ShowFindAllDialog() + return True + elif id == FindInDirService.FINDDIR_ID: + self.ShowFindDirDialog() + return True + else: + return FindService.FindService.ProcessEvent(self, event) + + + def ProcessUpdateUIEvent(self, event): + id = event.GetId() + if id == FindInDirService.FINDALL_ID: + projectService = wx.GetApp().GetService(ProjectEditor.ProjectService) + view = projectService.GetView() + if view and view.GetDocument() and view.GetDocument().GetFiles(): + event.Enable(True) + else: + event.Enable(False) + return True + elif id == FindInDirService.FINDDIR_ID: + event.Enable(True) + else: + return FindService.FindService.ProcessUpdateUIEvent(self, event) + + + def ShowFindDirDialog(self): + config = wx.ConfigBase_Get() + + frame = wx.Dialog(None, -1, _("Find in Directory"), size= (320,200)) + borderSizer = wx.BoxSizer(wx.HORIZONTAL) + + contentSizer = wx.BoxSizer(wx.VERTICAL) + lineSizer = wx.BoxSizer(wx.HORIZONTAL) + lineSizer.Add(wx.StaticText(frame, -1, _("Directory:")), 0, wx.ALIGN_CENTER | wx.RIGHT, HALF_SPACE) + dirCtrl = wx.TextCtrl(frame, -1, config.Read(FIND_MATCHDIR, ""), size=(200,-1)) + dirCtrl.SetToolTipString(dirCtrl.GetValue()) + lineSizer.Add(dirCtrl, 0, wx.LEFT, HALF_SPACE) + findDirButton = wx.Button(frame, -1, "Browse...") + lineSizer.Add(findDirButton, 0, wx.LEFT, HALF_SPACE) + contentSizer.Add(lineSizer, 0, wx.BOTTOM, SPACE) + + def OnBrowseButton(event): + dlg = wx.DirDialog(frame, _("Choose a directory:"), style=wx.DD_DEFAULT_STYLE) + dir = dirCtrl.GetValue() + if len(dir): + dlg.SetPath(dir) + if dlg.ShowModal() == wx.ID_OK: + dirCtrl.SetValue(dlg.GetPath()) + dirCtrl.SetToolTipString(dirCtrl.GetValue()) + dirCtrl.SetInsertionPointEnd() + + dlg.Destroy() + wx.EVT_BUTTON(findDirButton, -1, OnBrowseButton) + + subfolderCtrl = wx.CheckBox(frame, -1, _("Search in subfolders")) + subfolderCtrl.SetValue(config.ReadInt(FIND_MATCHDIRSUBFOLDERS, True)) + contentSizer.Add(subfolderCtrl, 0, wx.BOTTOM, SPACE) + + lineSizer = wx.BoxSizer(wx.VERTICAL) # let the line expand horizontally without vertical expansion + lineSizer.Add(wx.StaticLine(frame, -1, size = (10,-1)), 0, flag=wx.EXPAND) + contentSizer.Add(lineSizer, flag=wx.EXPAND|wx.ALIGN_CENTER_VERTICAL|wx.BOTTOM, border=HALF_SPACE) + + lineSizer = wx.BoxSizer(wx.HORIZONTAL) + lineSizer.Add(wx.StaticText(frame, -1, _("Find what:")), 0, wx.ALIGN_CENTER | wx.RIGHT, HALF_SPACE) + findCtrl = wx.TextCtrl(frame, -1, config.Read(FindService.FIND_MATCHPATTERN, ""), size=(200,-1)) + lineSizer.Add(findCtrl, 0, wx.LEFT, HALF_SPACE) + contentSizer.Add(lineSizer, 0, wx.BOTTOM, SPACE) + wholeWordCtrl = wx.CheckBox(frame, -1, _("Match whole word only")) + wholeWordCtrl.SetValue(config.ReadInt(FindService.FIND_MATCHWHOLEWORD, False)) + matchCaseCtrl = wx.CheckBox(frame, -1, _("Match case")) + matchCaseCtrl.SetValue(config.ReadInt(FindService.FIND_MATCHCASE, False)) + regExprCtrl = wx.CheckBox(frame, -1, _("Regular expression")) + regExprCtrl.SetValue(config.ReadInt(FindService.FIND_MATCHREGEXPR, False)) + contentSizer.Add(wholeWordCtrl, 0, wx.BOTTOM, SPACE) + contentSizer.Add(matchCaseCtrl, 0, wx.BOTTOM, SPACE) + contentSizer.Add(regExprCtrl, 0, wx.BOTTOM, SPACE) + borderSizer.Add(contentSizer, 0, wx.TOP | wx.BOTTOM | wx.LEFT, SPACE) + + buttonSizer = wx.BoxSizer(wx.VERTICAL) + findBtn = wx.Button(frame, wx.ID_OK, _("Find")) + findBtn.SetDefault() + buttonSizer.Add(findBtn, 0, wx.BOTTOM, HALF_SPACE) + buttonSizer.Add(wx.Button(frame, wx.ID_CANCEL, _("Cancel")), 0) + borderSizer.Add(buttonSizer, 0, wx.ALL, SPACE) + + frame.SetSizer(borderSizer) + frame.Fit() + + status = frame.ShowModal() + + passedCheck = False + while status == wx.ID_OK and not passedCheck: + if not os.path.exists(dirCtrl.GetValue()): + dlg = wx.MessageDialog(frame, + _("'%s' does not exist.") % dirCtrl.GetValue(), + _("Find in Directory"), + wx.OK | wx.ICON_EXCLAMATION + ) + dlg.ShowModal() + dlg.Destroy() + + status = frame.ShowModal() + elif len(findCtrl.GetValue()) == 0: + dlg = wx.MessageDialog(frame, + _("'Find what:' cannot be empty."), + _("Find in Directory"), + wx.OK | wx.ICON_EXCLAMATION + ) + dlg.ShowModal() + dlg.Destroy() + + status = frame.ShowModal() + else: + passedCheck = True + + + # save user choice state for this and other Find Dialog Boxes + dirString = dirCtrl.GetValue() + searchSubfolders = subfolderCtrl.IsChecked() + self.SaveFindDirConfig(dirString, searchSubfolders) + + findString = findCtrl.GetValue() + matchCase = matchCaseCtrl.IsChecked() + wholeWord = wholeWordCtrl.IsChecked() + regExpr = regExprCtrl.IsChecked() + self.SaveFindConfig(findString, wholeWord, matchCase, regExpr) + + + if status == wx.ID_OK: + frame.Destroy() + + messageService = wx.GetApp().GetService(MessageService.MessageService) + messageService.ShowWindow() + + view = messageService.GetView() + if view: + wx.GetApp().GetTopWindow().SetCursor(wx.StockCursor(wx.CURSOR_WAIT)) + view.ClearLines() + view.SetCallback(self.OnJumpToFoundLine) + + view.AddLines(_("Searching for '%s' in '%s'\n\n") % (findString, dirString)) + + if os.path.isfile(dirString): + try: + docFile = file(dirString, 'r') + lineNum = 1 + needToDisplayFilename = True + line = docFile.readline() + while line: + count, foundStart, foundEnd, newText = self.DoFind(findString, None, line, 0, 0, True, matchCase, wholeWord, regExpr) + if count != -1: + if needToDisplayFilename: + view.AddLines(FILENAME_MARKER + dirString + "\n") + needToDisplayFilename = False + line = repr(lineNum).zfill(4) + ":" + line + view.AddLines(line) + line = docFile.readline() + lineNum += 1 + if not needToDisplayFilename: + view.AddLines("\n") + except IOError, (code, message): + print _("Warning, unable to read file: '%s'. %s") % (dirString, message) + else: + # do search in files on disk + for root, dirs, files in os.walk(dirString): + if not searchSubfolders and root != dirString: + break + + for name in files: + filename = os.path.join(root, name) + try: + docFile = file(filename, 'r') + except IOError, (code, message): + print _("Warning, unable to read file: '%s'. %s") % (filename, message) + continue + + lineNum = 1 + needToDisplayFilename = True + line = docFile.readline() + while line: + count, foundStart, foundEnd, newText = self.DoFind(findString, None, line, 0, 0, True, matchCase, wholeWord, regExpr) + if count != -1: + if needToDisplayFilename: + view.AddLines(FILENAME_MARKER + filename + "\n") + needToDisplayFilename = False + line = repr(lineNum).zfill(4) + ":" + line + view.AddLines(line) + line = docFile.readline() + lineNum += 1 + if not needToDisplayFilename: + view.AddLines("\n") + + view.AddLines(_("Search completed.")) + wx.GetApp().GetTopWindow().SetCursor(wx.StockCursor(wx.CURSOR_DEFAULT)) + + return True + else: + frame.Destroy() + return False + + + def SaveFindDirConfig(self, dirString, searchSubfolders): + """ Save search dir patterns and flags to registry. + + dirString = search directory + searchSubfolders = Search subfolders + """ + config = wx.ConfigBase_Get() + config.Write(FIND_MATCHDIR, dirString) + config.WriteInt(FIND_MATCHDIRSUBFOLDERS, searchSubfolders) + + + def ShowFindAllDialog(self): + config = wx.ConfigBase_Get() + + frame = wx.Dialog(None, -1, _("Find in Project"), size= (320,200)) + borderSizer = wx.BoxSizer(wx.HORIZONTAL) + + contentSizer = wx.BoxSizer(wx.VERTICAL) + lineSizer = wx.BoxSizer(wx.HORIZONTAL) + lineSizer.Add(wx.StaticText(frame, -1, _("Find what:")), 0, wx.ALIGN_CENTER | wx.RIGHT, HALF_SPACE) + findCtrl = wx.TextCtrl(frame, -1, config.Read(FindService.FIND_MATCHPATTERN, ""), size=(200,-1)) + lineSizer.Add(findCtrl, 0, wx.LEFT, HALF_SPACE) + contentSizer.Add(lineSizer, 0, wx.BOTTOM, SPACE) + wholeWordCtrl = wx.CheckBox(frame, -1, _("Match whole word only")) + wholeWordCtrl.SetValue(config.ReadInt(FindService.FIND_MATCHWHOLEWORD, False)) + matchCaseCtrl = wx.CheckBox(frame, -1, _("Match case")) + matchCaseCtrl.SetValue(config.ReadInt(FindService.FIND_MATCHCASE, False)) + regExprCtrl = wx.CheckBox(frame, -1, _("Regular expression")) + regExprCtrl.SetValue(config.ReadInt(FindService.FIND_MATCHREGEXPR, False)) + contentSizer.Add(wholeWordCtrl, 0, wx.BOTTOM, SPACE) + contentSizer.Add(matchCaseCtrl, 0, wx.BOTTOM, SPACE) + contentSizer.Add(regExprCtrl, 0, wx.BOTTOM, SPACE) + borderSizer.Add(contentSizer, 0, wx.TOP | wx.BOTTOM | wx.LEFT, SPACE) + + buttonSizer = wx.BoxSizer(wx.VERTICAL) + findBtn = wx.Button(frame, wx.ID_OK, _("Find")) + findBtn.SetDefault() + buttonSizer.Add(findBtn, 0, wx.BOTTOM, HALF_SPACE) + buttonSizer.Add(wx.Button(frame, wx.ID_CANCEL, _("Cancel")), 0) + borderSizer.Add(buttonSizer, 0, wx.ALL, SPACE) + + frame.SetSizer(borderSizer) + frame.Fit() + + status = frame.ShowModal() + + # save user choice state for this and other Find Dialog Boxes + findString = findCtrl.GetValue() + matchCase = matchCaseCtrl.IsChecked() + wholeWord = wholeWordCtrl.IsChecked() + regExpr = regExprCtrl.IsChecked() + self.SaveFindConfig(findString, wholeWord, matchCase, regExpr) + + if status == wx.ID_OK: + frame.Destroy() + + messageService = wx.GetApp().GetService(MessageService.MessageService) + messageService.ShowWindow() + + view = messageService.GetView() + if view: + view.ClearLines() + view.SetCallback(self.OnJumpToFoundLine) + + projectService = wx.GetApp().GetService(ProjectEditor.ProjectService) + projectFilenames = projectService.GetFilesFromCurrentProject() + + projView = projectService.GetView() + if projView: + projName = wx.lib.docview.FileNameFromPath(projView.GetDocument().GetFilename()) + view.AddLines(PROJECT_MARKER + projName + "\n\n") + + # do search in open files first, open files may have been modified and different from disk because it hasn't been saved + openDocs = wx.GetApp().GetDocumentManager().GetDocuments() + openDocsInProject = filter(lambda openDoc: openDoc.GetFilename() in projectFilenames, openDocs) + for openDoc in openDocsInProject: + if isinstance(openDoc, ProjectEditor.ProjectDocument): # don't search project model + continue + + openDocView = openDoc.GetFirstView() + # some views don't have a in memory text object to search through such as the PM and the DM + # even if they do have a non-text searchable object, how do we display it in the message window? + if not hasattr(openDocView, "GetValue"): + continue + text = openDocView.GetValue() + + lineNum = 1 + needToDisplayFilename = True + start = 0 + end = 0 + count = 0 + while count != -1: + count, foundStart, foundEnd, newText = self.DoFind(findString, None, text, start, end, True, matchCase, wholeWord, regExpr) + if count != -1: + if needToDisplayFilename: + view.AddLines(FILENAME_MARKER + openDoc.GetFilename() + "\n") + needToDisplayFilename = False + + lineNum = openDocView.LineFromPosition(foundStart) + line = repr(lineNum).zfill(4) + ":" + openDocView.GetLine(lineNum) + view.AddLines(line) + + start = text.find("\n", foundStart) + if start == -1: + break + end = start + if not needToDisplayFilename: + view.AddLines("\n") + openDocNames = map(lambda openDoc: openDoc.GetFilename(), openDocs) + + # do search in closed files, skipping the open ones we already searched + filenames = filter(lambda filename: filename not in openDocNames, projectFilenames) + for filename in filenames: + try: + docFile = file(filename, 'r') + except IOError, (code, message): + print _("Warning, unable to read file: '%s'. %s") % (filename, message) + continue + + lineNum = 1 + needToDisplayFilename = True + line = docFile.readline() + while line: + count, foundStart, foundEnd, newText = self.DoFind(findString, None, line, 0, 0, True, matchCase, wholeWord, regExpr) + if count != -1: + if needToDisplayFilename: + view.AddLines(FILENAME_MARKER + filename + "\n") + needToDisplayFilename = False + line = repr(lineNum).zfill(4) + ":" + line + view.AddLines(line) + line = docFile.readline() + lineNum += 1 + if not needToDisplayFilename: + view.AddLines("\n") + + view.AddLines(_("Search for '%s' completed.") % findString) + + return True + else: + frame.Destroy() + return False + + + def OnJumpToFoundLine(self, event): + messageService = wx.GetApp().GetService(MessageService.MessageService) + lineText, pos = messageService.GetView().GetCurrLine() + if lineText == "\n" or lineText.find(FILENAME_MARKER) != -1 or lineText.find(PROJECT_MARKER) != -1: + return + lineEnd = lineText.find(":") + if lineEnd == -1: + return + else: + lineNum = int(lineText[0:lineEnd]) + + text = messageService.GetView().GetText() + curPos = messageService.GetView().GetCurrentPos() + + startPos = text.rfind(FILENAME_MARKER, 0, curPos) + endPos = text.find("\n", startPos) + filename = text[startPos + len(FILENAME_MARKER):endPos] + + foundView = None + openDocs = wx.GetApp().GetDocumentManager().GetDocuments() + for openDoc in openDocs: + if openDoc.GetFilename() == filename: + foundView = openDoc.GetFirstView() + break + + if not foundView: + doc = wx.GetApp().GetDocumentManager().CreateDocument(filename, wx.lib.docview.DOC_SILENT) + foundView = doc.GetFirstView() + + if foundView: + foundView.GetFrame().SetFocus() + foundView.Activate() + if hasattr(foundView, "GotoLine"): + foundView.GotoLine(lineNum) + startPos = foundView.PositionFromLine(lineNum) + # wxBug: Need to select in reverse order, (end, start) to put cursor at head of line so positioning is correct + # Also, if we use the correct positioning order (start, end), somehow, when we open a edit window for the first + # time, we don't see the selection, it is scrolled off screen + foundView.SetSelection(startPos - 1 + len(lineText[lineEnd:].rstrip("\n")), startPos) + wx.GetApp().GetService(OutlineService.OutlineService).LoadOutline(foundView, position=startPos) + + diff --git a/wxPython/samples/ide/activegrid/tool/FindService.py b/wxPython/samples/ide/activegrid/tool/FindService.py new file mode 100644 index 0000000000..da5235b5d9 --- /dev/null +++ b/wxPython/samples/ide/activegrid/tool/FindService.py @@ -0,0 +1,521 @@ +#---------------------------------------------------------------------------- +# Name: FindService.py +# Purpose: Find Service for pydocview +# +# Author: Peter Yared, Morgan Hua +# +# Created: 8/15/03 +# CVS-ID: $Id$ +# Copyright: (c) 2003-2005 ActiveGrid, Inc. +# License: wxWindows License +#---------------------------------------------------------------------------- + +import wx +import wx.lib.docview +import wx.lib.pydocview +import re +_ = wx.GetTranslation + + +#---------------------------------------------------------------------------- +# Constants +#---------------------------------------------------------------------------- +FIND_MATCHPATTERN = "FindMatchPattern" +FIND_MATCHREPLACE = "FindMatchReplace" +FIND_MATCHCASE = "FindMatchCase" +FIND_MATCHWHOLEWORD = "FindMatchWholeWordOnly" +FIND_MATCHREGEXPR = "FindMatchRegularExpr" +FIND_MATCHWRAP = "FindMatchWrap" +FIND_MATCHUPDOWN = "FindMatchUpDown" + +FIND_SYNTAXERROR = -2 + +SPACE = 10 +HALF_SPACE = 5 + + +#---------------------------------------------------------------------------- +# Classes +#---------------------------------------------------------------------------- + +class FindService(wx.lib.pydocview.DocService): + + #---------------------------------------------------------------------------- + # Constants + #---------------------------------------------------------------------------- + FIND_ID = wx.NewId() # for bringing up Find dialog box + FINDONE_ID = wx.NewId() # for doing Find + FIND_PREVIOUS_ID = wx.NewId() # for doing Find Next + FIND_NEXT_ID = wx.NewId() # for doing Find Prev + REPLACE_ID = wx.NewId() # for bringing up Replace dialog box + REPLACEONE_ID = wx.NewId() # for doing a Replace + REPLACEALL_ID = wx.NewId() # for doing Replace All + GOTO_LINE_ID = wx.NewId() # for bringing up Goto dialog box + + # Extending bitmasks: wx.FR_WHOLEWORD, wx.FR_MATCHCASE, and wx.FR_DOWN + FR_REGEXP = max([wx.FR_WHOLEWORD, wx.FR_MATCHCASE, wx.FR_DOWN]) << 1 + FR_WRAP = FR_REGEXP << 1 + + + def __init__(self): + self._replaceDialog = None + self._findDialog = None + self._findReplaceData = wx.FindReplaceData() + self._findReplaceData.SetFlags(wx.FR_DOWN) + + + def InstallControls(self, frame, menuBar = None, toolBar = None, statusBar = None, document = None): + """ Install Find Service Menu Items """ + editMenu = menuBar.GetMenu(menuBar.FindMenu(_("&Edit"))) + editMenu.AppendSeparator() + editMenu.Append(FindService.FIND_ID, _("&Find...\tCtrl+F"), _("Finds the specified text")) + wx.EVT_MENU(frame, FindService.FIND_ID, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, FindService.FIND_ID, frame.ProcessUpdateUIEvent) + editMenu.Append(FindService.FIND_PREVIOUS_ID, _("Find &Previous\tShift+F3"), _("Finds the specified text")) + wx.EVT_MENU(frame, FindService.FIND_PREVIOUS_ID, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, FindService.FIND_PREVIOUS_ID, frame.ProcessUpdateUIEvent) + editMenu.Append(FindService.FIND_NEXT_ID, _("Find &Next\tF3"), _("Finds the specified text")) + wx.EVT_MENU(frame, FindService.FIND_NEXT_ID, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, FindService.FIND_NEXT_ID, frame.ProcessUpdateUIEvent) + editMenu.Append(FindService.REPLACE_ID, _("R&eplace...\tCtrl+H"), _("Replaces specific text with different text")) + wx.EVT_MENU(frame, FindService.REPLACE_ID, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, FindService.REPLACE_ID, frame.ProcessUpdateUIEvent) + editMenu.Append(FindService.GOTO_LINE_ID, _("&Go to Line...\tCtrl+G"), _("Goes to a certain line in the file")) + wx.EVT_MENU(frame, FindService.GOTO_LINE_ID, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, FindService.GOTO_LINE_ID, frame.ProcessUpdateUIEvent) + + # wxBug: wxToolBar::GetToolPos doesn't exist, need it to find cut tool and then insert find in front of it. + toolBar.InsertTool(6, FindService.FIND_ID, getFindBitmap(), shortHelpString = _("Find"), longHelpString = _("Finds the specified text")) + toolBar.InsertSeparator(6) + toolBar.Realize() + + frame.Bind(wx.EVT_FIND, frame.ProcessEvent) + frame.Bind(wx.EVT_FIND_NEXT, frame.ProcessEvent) + frame.Bind(wx.EVT_FIND_REPLACE, frame.ProcessEvent) + frame.Bind(wx.EVT_FIND_REPLACE_ALL, frame.ProcessEvent) + + + def ProcessUpdateUIEvent(self, event): + id = event.GetId() + if id == FindService.FIND_ID: + event.Enable(False) + return True + elif id == FindService.FIND_PREVIOUS_ID: + event.Enable(False) + return True + elif id == FindService.FIND_NEXT_ID: + event.Enable(False) + return True + elif id == FindService.REPLACE_ID: + event.Enable(False) + return True + elif id == FindService.GOTO_LINE_ID: + event.Enable(False) + return True + else: + return False + + + def ShowFindReplaceDialog(self, findString="", replace = False): + """ Display find/replace dialog box. + + Parameters: findString is the default value shown in the find/replace dialog input field. + If replace is True, the replace dialog box is shown, otherwise only the find dialog box is shown. + """ + if replace: + if self._findDialog != None: + # No reason to have both find and replace dialogs up at the same time + self._findDialog.DoClose() + self._findDialog = None + + self._replaceDialog = FindReplaceDialog(self.GetDocumentManager().FindSuitableParent(), -1, _("Replace"), size=(320,200), findString=findString) + self._replaceDialog.Show(True) + else: + if self._replaceDialog != None: + # No reason to have both find and replace dialogs up at the same time + self._replaceDialog.DoClose() + self._replaceDialog = None + + self._findDialog = FindDialog(self.GetDocumentManager().FindSuitableParent(), -1, _("Find"), size=(320,200), findString=findString) + self._findDialog.Show(True) + + + + def OnFindClose(self, event): + """ Cleanup handles when find/replace dialog is closed """ + if self._findDialog != None: + self._findDialog = None + elif self._replaceDialog != None: + self._replaceDialog = None + + + def GetCurrentDialog(self): + """ return handle to either the find or replace dialog """ + if self._findDialog != None: + return self._findDialog + return self._replaceDialog + + + def GetLineNumber(self, parent): + """ Display Goto Line Number dialog box """ + line = -1 + dialog = wx.TextEntryDialog(parent, _("Enter line number to go to:"), _("Go to Line")) + if dialog.ShowModal() == wx.ID_OK: + try: + line = int(dialog.GetValue()) + if line > 65535: + line = 65535 + except: + pass + dialog.Destroy() + # This one is ugly: wx.GetNumberFromUser("", _("Enter line number to go to:"), _("Go to Line"), 1, min = 1, max = 65535, parent = parent) + return line + + + def DoFind(self, findString, replaceString, text, startLoc, endLoc, down, matchCase, wholeWord, regExpr = False, replace = False, replaceAll = False, wrap = False): + """ Do the actual work of the find/replace. + + Returns the tuple (count, start, end, newText). + count = number of string replacements + start = start position of found string + end = end position of found string + newText = new replaced text + """ + flags = 0 + if regExpr: + pattern = findString + else: + pattern = re.escape(findString) # Treat the strings as a literal string + if not matchCase: + flags = re.IGNORECASE + if wholeWord: + pattern = r"\b%s\b" % pattern + + try: + reg = re.compile(pattern, flags) + except: + # syntax error of some sort + import sys + msgTitle = wx.GetApp().GetAppName() + if not msgTitle: + msgTitle = _("Regular Expression Search") + wx.MessageBox(_("Invalid regular expression \"%s\". %s") % (pattern, sys.exc_value), + msgTitle, + wx.OK | wx.ICON_EXCLAMATION, + self.GetView()) + return FIND_SYNTAXERROR, None, None, None + + if replaceAll: + newText, count = reg.subn(replaceString, text) + if count == 0: + return -1, None, None, None + else: + return count, None, None, newText + + start = -1 + if down: + match = reg.search(text, endLoc) + if match == None: + if wrap: # try again, but this time from top of file + match = reg.search(text, 0) + if match == None: + return -1, None, None, None + else: + return -1, None, None, None + start = match.start() + end = match.end() + else: + match = reg.search(text) + if match == None: + return -1, None, None, None + found = None + i, j = match.span() + while i < startLoc and j <= startLoc: + found = match + if i == j: + j = j + 1 + match = reg.search(text, j) + if match == None: + break + i, j = match.span() + if found == None: + if wrap: # try again, but this time from bottom of file + match = reg.search(text, startLoc) + if match == None: + return -1, None, None, None + found = None + i, j = match.span() + end = len(text) + while i < end and j <= end: + found = match + if i == j: + j = j + 1 + match = reg.search(text, j) + if match == None: + break + i, j = match.span() + if found == None: + return -1, None, None, None + else: + return -1, None, None, None + start = found.start() + end = found.end() + + if replace and start != -1: + newText, count = reg.subn(replaceString, text, 1) + return count, start, end, newText + + return 0, start, end, None + + + def SaveFindConfig(self, findString, wholeWord, matchCase, regExpr = None, wrap = None, upDown = None, replaceString = None): + """ Save find/replace patterns and search flags to registry. + + findString = search pattern + wholeWord = match whole word only + matchCase = match case + regExpr = use regular expressions in search pattern + wrap = return to top/bottom of file on search + upDown = search up or down from current cursor position + replaceString = replace string + """ + config = wx.ConfigBase_Get() + + config.Write(FIND_MATCHPATTERN, findString) + config.WriteInt(FIND_MATCHCASE, matchCase) + config.WriteInt(FIND_MATCHWHOLEWORD, wholeWord) + if replaceString != None: + config.Write(FIND_MATCHREPLACE, replaceString) + if regExpr != None: + config.WriteInt(FIND_MATCHREGEXPR, regExpr) + if wrap != None: + config.WriteInt(FIND_MATCHWRAP, wrap) + if upDown != None: + config.WriteInt(FIND_MATCHUPDOWN, upDown) + + + def GetFindString(self): + """ Load the search pattern from registry """ + return wx.ConfigBase_Get().Read(FIND_MATCHPATTERN, "") + + + def GetReplaceString(self): + """ Load the replace pattern from registry """ + return wx.ConfigBase_Get().Read(FIND_MATCHREPLACE, "") + + + def GetFlags(self): + """ Load search parameters from registry """ + config = wx.ConfigBase_Get() + + flags = 0 + if config.ReadInt(FIND_MATCHWHOLEWORD, False): + flags = flags | wx.FR_WHOLEWORD + if config.ReadInt(FIND_MATCHCASE, False): + flags = flags | wx.FR_MATCHCASE + if config.ReadInt(FIND_MATCHUPDOWN, False): + flags = flags | wx.FR_DOWN + if config.ReadInt(FIND_MATCHREGEXPR, False): + flags = flags | FindService.FR_REGEXP + if config.ReadInt(FIND_MATCHWRAP, False): + flags = flags | FindService.FR_WRAP + return flags + + +class FindDialog(wx.Dialog): + """ Find Dialog with regular expression matching and wrap to top/bottom of file. """ + + def __init__(self, parent, id, title, size, findString=None): + wx.Dialog.__init__(self, parent, id, title, size=size) + + config = wx.ConfigBase_Get() + borderSizer = wx.BoxSizer(wx.VERTICAL) + gridSizer = wx.GridBagSizer(SPACE, SPACE) + + lineSizer = wx.BoxSizer(wx.HORIZONTAL) + lineSizer.Add(wx.StaticText(self, -1, _("Find what:")), 0, wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, SPACE) + if not findString: + findString = config.Read(FIND_MATCHPATTERN, "") + self._findCtrl = wx.TextCtrl(self, -1, findString, size=(200,-1)) + lineSizer.Add(self._findCtrl, 0) + gridSizer.Add(lineSizer, pos=(0,0), span=(1,2)) + choiceSizer = wx.BoxSizer(wx.VERTICAL) + self._wholeWordCtrl = wx.CheckBox(self, -1, _("Match whole word only")) + self._wholeWordCtrl.SetValue(config.ReadInt(FIND_MATCHWHOLEWORD, False)) + self._matchCaseCtrl = wx.CheckBox(self, -1, _("Match case")) + self._matchCaseCtrl.SetValue(config.ReadInt(FIND_MATCHCASE, False)) + self._regExprCtrl = wx.CheckBox(self, -1, _("Regular expression")) + self._regExprCtrl.SetValue(config.ReadInt(FIND_MATCHREGEXPR, False)) + self._wrapCtrl = wx.CheckBox(self, -1, _("Wrap")) + self._wrapCtrl.SetValue(config.ReadInt(FIND_MATCHWRAP, False)) + choiceSizer.Add(self._wholeWordCtrl, 0, wx.BOTTOM, SPACE) + choiceSizer.Add(self._matchCaseCtrl, 0, wx.BOTTOM, SPACE) + choiceSizer.Add(self._regExprCtrl, 0, wx.BOTTOM, SPACE) + choiceSizer.Add(self._wrapCtrl, 0) + gridSizer.Add(choiceSizer, pos=(1,0), span=(2,1)) + + self._radioBox = wx.RadioBox(self, -1, _("Direction"), choices = ["Up", "Down"]) + self._radioBox.SetSelection(config.ReadInt(FIND_MATCHUPDOWN, 1)) + gridSizer.Add(self._radioBox, pos=(1,1), span=(2,1)) + + buttonSizer = wx.BoxSizer(wx.VERTICAL) + findBtn = wx.Button(self, FindService.FINDONE_ID, _("Find Next")) + findBtn.SetDefault() + wx.EVT_BUTTON(self, FindService.FINDONE_ID, self.OnActionEvent) + cancelBtn = wx.Button(self, wx.ID_CANCEL, _("Cancel")) + wx.EVT_BUTTON(self, wx.ID_CANCEL, self.OnClose) + buttonSizer.Add(findBtn, 0, wx.BOTTOM, HALF_SPACE) + buttonSizer.Add(cancelBtn, 0) + gridSizer.Add(buttonSizer, pos=(0,2), span=(3,1)) + + borderSizer.Add(gridSizer, 0, wx.ALL, SPACE) + + self.Bind(wx.EVT_CLOSE, self.OnClose) + + self.SetSizer(borderSizer) + self.Fit() + self._findCtrl.SetFocus() + + def SaveConfig(self): + """ Save find patterns and search flags to registry. """ + findService = wx.GetApp().GetService(FindService) + if findService: + findService.SaveFindConfig(self._findCtrl.GetValue(), + self._wholeWordCtrl.IsChecked(), + self._matchCaseCtrl.IsChecked(), + self._regExprCtrl.IsChecked(), + self._wrapCtrl.IsChecked(), + self._radioBox.GetSelection(), + ) + + + def DoClose(self): + self.SaveConfig() + self.Destroy() + + + def OnClose(self, event): + findService = wx.GetApp().GetService(FindService) + if findService: + findService.OnFindClose(event) + self.DoClose() + + + def OnActionEvent(self, event): + self.SaveConfig() + + if wx.GetApp().GetDocumentManager().GetFlags() & wx.lib.docview.DOC_MDI: + if wx.GetApp().GetTopWindow().ProcessEvent(event): + return True + else: + view = wx.GetApp().GetDocumentManager().GetLastActiveView() + if view and view.ProcessEvent(event): + return True + return False + + +class FindReplaceDialog(FindDialog): + """ Find/Replace Dialog with regular expression matching and wrap to top/bottom of file. """ + + def __init__(self, parent, id, title, size, findString=None): + wx.Dialog.__init__(self, parent, id, title, size=size) + + config = wx.ConfigBase_Get() + borderSizer = wx.BoxSizer(wx.VERTICAL) + gridSizer = wx.GridBagSizer(SPACE, SPACE) + + gridSizer2 = wx.GridBagSizer(SPACE, SPACE) + gridSizer2.Add(wx.StaticText(self, -1, _("Find what:")), flag=wx.ALIGN_CENTER_VERTICAL, pos=(0,0)) + if not findString: + findString = config.Read(FIND_MATCHPATTERN, "") + self._findCtrl = wx.TextCtrl(self, -1, findString, size=(200,-1)) + gridSizer2.Add(self._findCtrl, pos=(0,1)) + gridSizer2.Add(wx.StaticText(self, -1, _("Replace with:")), flag=wx.ALIGN_CENTER_VERTICAL, pos=(1,0)) + self._replaceCtrl = wx.TextCtrl(self, -1, config.Read(FIND_MATCHREPLACE, ""), size=(200,-1)) + gridSizer2.Add(self._replaceCtrl, pos=(1,1)) + gridSizer.Add(gridSizer2, pos=(0,0), span=(1,2)) + choiceSizer = wx.BoxSizer(wx.VERTICAL) + self._wholeWordCtrl = wx.CheckBox(self, -1, _("Match whole word only")) + self._wholeWordCtrl.SetValue(config.ReadInt(FIND_MATCHWHOLEWORD, False)) + self._matchCaseCtrl = wx.CheckBox(self, -1, _("Match case")) + self._matchCaseCtrl.SetValue(config.ReadInt(FIND_MATCHCASE, False)) + self._regExprCtrl = wx.CheckBox(self, -1, _("Regular expression")) + self._regExprCtrl.SetValue(config.ReadInt(FIND_MATCHREGEXPR, False)) + self._wrapCtrl = wx.CheckBox(self, -1, _("Wrap")) + self._wrapCtrl.SetValue(config.ReadInt(FIND_MATCHWRAP, False)) + choiceSizer.Add(self._wholeWordCtrl, 0, wx.BOTTOM, SPACE) + choiceSizer.Add(self._matchCaseCtrl, 0, wx.BOTTOM, SPACE) + choiceSizer.Add(self._regExprCtrl, 0, wx.BOTTOM, SPACE) + choiceSizer.Add(self._wrapCtrl, 0) + gridSizer.Add(choiceSizer, pos=(1,0), span=(2,1)) + + self._radioBox = wx.RadioBox(self, -1, _("Direction"), choices = ["Up", "Down"]) + self._radioBox.SetSelection(config.ReadInt(FIND_MATCHUPDOWN, 1)) + gridSizer.Add(self._radioBox, pos=(1,1), span=(2,1)) + + buttonSizer = wx.BoxSizer(wx.VERTICAL) + findBtn = wx.Button(self, FindService.FINDONE_ID, _("Find Next")) + findBtn.SetDefault() + wx.EVT_BUTTON(self, FindService.FINDONE_ID, self.OnActionEvent) + cancelBtn = wx.Button(self, wx.ID_CANCEL, _("Cancel")) + wx.EVT_BUTTON(self, wx.ID_CANCEL, self.OnClose) + replaceBtn = wx.Button(self, FindService.REPLACEONE_ID, _("Replace")) + wx.EVT_BUTTON(self, FindService.REPLACEONE_ID, self.OnActionEvent) + replaceAllBtn = wx.Button(self, FindService.REPLACEALL_ID, _("Replace All")) + wx.EVT_BUTTON(self, FindService.REPLACEALL_ID, self.OnActionEvent) + buttonSizer.Add(findBtn, 0, wx.BOTTOM, HALF_SPACE) + buttonSizer.Add(replaceBtn, 0, wx.BOTTOM, HALF_SPACE) + buttonSizer.Add(replaceAllBtn, 0, wx.BOTTOM, HALF_SPACE) + buttonSizer.Add(cancelBtn, 0) + gridSizer.Add(buttonSizer, pos=(0,2), span=(3,1)) + + borderSizer.Add(gridSizer, 0, wx.ALL, SPACE) + + self.Bind(wx.EVT_CLOSE, self.OnClose) + + self.SetSizer(borderSizer) + self.Fit() + self._findCtrl.SetFocus() + + + def SaveConfig(self): + """ Save find/replace patterns and search flags to registry. """ + findService = wx.GetApp().GetService(FindService) + if findService: + findService.SaveFindConfig(self._findCtrl.GetValue(), + self._wholeWordCtrl.IsChecked(), + self._matchCaseCtrl.IsChecked(), + self._regExprCtrl.IsChecked(), + self._wrapCtrl.IsChecked(), + self._radioBox.GetSelection(), + self._replaceCtrl.GetValue() + ) + + +#---------------------------------------------------------------------------- +# Menu Bitmaps - generated by encode_bitmaps.py +#---------------------------------------------------------------------------- +from wx import ImageFromStream, BitmapFromImage +import cStringIO + + +def getFindData(): + return \ +'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x10\x00\x00\x00\x10\x08\x06\ +\x00\x00\x00\x1f\xf3\xffa\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\ +\x00\x00\x81IDAT8\x8d\xa5S\xc1\x16\xc0\x10\x0ckk\xff\xff\xc7d\x87\xad^U\r\ +\x93S\xe5U$\n\xb3$:\xc1e\x17(\x19Z\xb3$\x9e\xf1DD\xe2\x15\x01x\xea\x93\xef\ +\x04\x989\xea\x1b\xf2U\xc0\xda\xb4\xeb\x11\x1f:\xd8\xb5\xff8\x93\xd4\xa9\xae\ +@/S\xaaUwJ3\x85\xc0\x81\xee\xeb.q\x17C\x81\xd5XU \x1a\x93\xc6\x18\x8d\x90\ +\xe8}\x89\x00\x9a&\x9b_k\x94\x0c\xdf\xd78\xf8\x0b\x99Y\xb4\x08c\x9e\xfe\xc6\ +\xe3\x087\xf9\xd0D\x180\xf1#\x8e\x00\x00\x00\x00IEND\xaeB`\x82' + + +def getFindBitmap(): + return BitmapFromImage(getFindImage()) + + +def getFindImage(): + stream = cStringIO.StringIO(getFindData()) + return ImageFromStream(stream) + diff --git a/wxPython/samples/ide/activegrid/tool/HtmlEditor.py b/wxPython/samples/ide/activegrid/tool/HtmlEditor.py new file mode 100644 index 0000000000..e026fb978b --- /dev/null +++ b/wxPython/samples/ide/activegrid/tool/HtmlEditor.py @@ -0,0 +1,222 @@ +#---------------------------------------------------------------------------- +# Name: HtmlEditor.py +# Purpose: Abstract Code Editor for pydocview tbat uses the Styled Text Control +# +# Author: Peter Yared +# +# Created: 8/15/04 +# CVS-ID: $Id$ +# Copyright: (c) 2004-2005 ActiveGrid, Inc. +# License: wxWindows License +#---------------------------------------------------------------------------- + + +import wx +import os.path +import string +import STCTextEditor +import CodeEditor +_ = wx.GetTranslation + + +class HtmlDocument(CodeEditor.CodeDocument): + + pass + + +class HtmlView(CodeEditor.CodeView): + + + def GetCtrlClass(self): + """ Used in split window to instantiate new instances """ + return HtmlCtrl + + + def GetAutoCompleteHint(self): + pos = self.GetCtrl().GetCurrentPos() + if pos == 0: + return None, None + + validLetters = string.letters + string.digits + '_!-' + word = '' + while (True): + pos = pos - 1 + if pos < 0: + break + char = chr(self.GetCtrl().GetCharAt(pos)) + if char not in validLetters: + break + word = char + word + + return None, word + + + def GetAutoCompleteDefaultKeywords(self): + return HTMLKEYWORDS + + +## def _CreateControl(self, parent, id): +## import wx # wxBug: When inlining the import of the appropriate html control below, have to specifically import wx for some reason +## self._notebook = wx.Notebook(parent, wx.NewId(), style = wx.NB_BOTTOM) +## self._textEditor = HtmlCtrl(self._notebook, id) +## if wx.Platform =='__WXMSW__': +## import wxPython.iewin +## self._browserCtrl = wxPython.iewin.wxIEHtmlWin(self._notebook, -1, style = wx.NO_FULL_REPAINT_ON_RESIZE) +## else: +## import wx.html +## self._browserCtrl = wx.html.HtmlWindow(self._notebook, -1, style = wx.NO_FULL_REPAINT_ON_RESIZE) +## self._notebook.AddPage(self._textEditor, _("Edit")) +## self._notebook.AddPage(self._browserCtrl, _("View")) +## self._insertMode = True +## wx.EVT_NOTEBOOK_PAGE_CHANGED(self._notebook, self._notebook.GetId(), self.OnNotebookChanging) +## return self._textEditor +## +## +## def _CreateSizer(self, frame): +## sizer = wx.BoxSizer(wx.HORIZONTAL) +## sizer.Add(self._notebook, 1, wx.EXPAND) +## frame.SetSizer(sizer) +## frame.SetAutoLayout(True) +## +## +## def OnNotebookChanging(self, event): +## if event.GetSelection() == 0: # Going to the edit page +## pass # self._textEditor.Refresh() +## elif event.GetSelection() == 1: # Going to the browser page +## text = self._textEditor.GetText() +## if wx.Platform == '__WXMSW__': +## path = os.path.join(tempfile.gettempdir(), "temp.html") +## file = open(path, 'w') +## file.write(text) +## file.close() +## self._browserCtrl.Navigate("file://" + path) +## else: +## self._browserCtrl.SetPage(text) +## event.Skip() + + +class HtmlService(CodeEditor.CodeService): + + + def __init__(self): + CodeEditor.CodeService.__init__(self) + + +class HtmlCtrl(CodeEditor.CodeCtrl): + + + def __init__(self, parent, ID = -1, style = wx.NO_FULL_REPAINT_ON_RESIZE): + CodeEditor.CodeCtrl.__init__(self, parent, ID, style) + self.SetLexer(wx.stc.STC_LEX_HTML) + self.SetProperty("fold.html", "1") + + def GetMatchingBraces(self): + return "<>[]{}()" + + def CanWordWrap(self): + return True + + + def SetViewDefaults(self): + CodeEditor.CodeCtrl.SetViewDefaults(self, configPrefix = "Html", hasWordWrap = False, hasTabs = True) + + + def GetFontAndColorFromConfig(self): + return CodeEditor.CodeCtrl.GetFontAndColorFromConfig(self, configPrefix = "Html") + + + def UpdateStyles(self): + CodeEditor.CodeCtrl.UpdateStyles(self) + + if not self.GetFont(): + return + + faces = { 'font' : self.GetFont().GetFaceName(), + 'size' : self.GetFont().GetPointSize(), + 'size2': self.GetFont().GetPointSize() - 2, + 'color' : "%02x%02x%02x" % (self.GetFontColor().Red(), self.GetFontColor().Green(), self.GetFontColor().Blue()) + } + + # White space + self.StyleSetSpec(wx.stc.STC_H_DEFAULT, "face:%(font)s,fore:#000000,face:%(font)s,size:%(size)d" % faces) + # Comment + self.StyleSetSpec(wx.stc.STC_H_COMMENT, "face:%(font)s,fore:#007F00,italic,face:%(font)s,size:%(size)d" % faces) + # Number + self.StyleSetSpec(wx.stc.STC_H_NUMBER, "face:%(font)s,fore:#007F7F,size:%(size)d" % faces) + # String + self.StyleSetSpec(wx.stc.STC_H_SINGLESTRING, "face:%(font)s,fore:#7F007F,face:%(font)s,size:%(size)d" % faces) + self.StyleSetSpec(wx.stc.STC_H_DOUBLESTRING, "face:%(font)s,fore:#7F007F,face:%(font)s,size:%(size)d" % faces) + # Tag + self.StyleSetSpec(wx.stc.STC_H_TAG, "face:%(font)s,fore:#00007F,bold,size:%(size)d" % faces) + # Attributes + self.StyleSetSpec(wx.stc.STC_H_ATTRIBUTE, "face:%(font)s,fore:#00007F,bold,size:%(size)d" % faces) + + +class HtmlOptionsPanel(STCTextEditor.TextOptionsPanel): + + def __init__(self, parent, id): + STCTextEditor.TextOptionsPanel.__init__(self, parent, id, configPrefix = "Html", label = "HTML", hasWordWrap = True, hasTabs = True) + + +HTMLKEYWORDS = [ + "A", "ABBR", "ACRONYM", "ADDRESS", "APPLET", "AREA", "B", "BASE", "BASEFONT", "BDO", "BIG", "BLOCKQUOTE", + "BODY", "BR", "BUTTON", "CAPTION", "CENTER", "CITE", "CODE", "COL", "COLGROUP", "DD", "DEL", "DFN", "DIR", + "DIV", "DL", "DT", "EM", "FIELDSET", "FONT", "FORM", "FRAME", "FRAMESET", "H1", "H2", "H3", "H4", "H5", "H6", + "HEAD", "HR", "HTML", "I", "IFRAME", "IMG", "INPUT", "INS", "ISINDEX", "KBD", "LABEL", "LEGEND", "LI", "LINK", + "MAP", "MENU", "META", "NOFRAMES", "NOSCRIPT", "OBJECT", "OL", "OPTGROUP", "OPTION", "P", "PARAM", + "PRE", "Q", "S", "SAMP", "SCRIPT", "SELECT", "SMALL", "SPAN", "STRIKE", "STRONG", "STYLE", "SUB", "SUP", + "TABLE", "TBODY", "TD", "TEXTAREA", "TFOOT", "TH", "THEAD", "TITLE", "TR", "TT", "U", "UL", "VAR", "XML", + "XMLNS", "ACCEPT-CHARSET", "ACCEPT", "ACCESSKEY", "ACTION", "ALIGN", "ALINK", "ALT", + "ARCHIVE", "AXIS", "BACKGROUND", "BGCOLOR", "BORDER", "CELLPADDING", "CELLSPACING", "CHAR", + "CHAROFF", "CHARSET", "CHECKED", "CLASS", "CLASSID", "CLEAR", "CODEBASE", "CODETYPE", + "COLOR", "COLS", "COLSPAN", "COMPACT", "CONTENT", "COORDS", "DATA", "DATAFLD", "DATAFORMATAS", + "DATAPAGESIZE", "DATASRC", "DATETIME", "DECLARE", "DEFER", "DISABLED", "ENCTYPE", + "EVENT", "FACE", "FOR", "FRAMEBORDER", "HEADERS", "HEIGHT", "HREF", "HREFLANG", "HSPACE", + "HTTP-EQUIV", "ID", "ISMAP", "LANG", "LANGUAGE", "LEFTMARGIN", "LONGDESC", + "MARGINWIDTH", "MARGINHEIGHT", "MAXLENGTH", "MEDIA", "METHOD", "MULTIPLE", "NAME", "NOHREF", + "NORESIZE", "NOSHADE", "NOWRAP", "ONBLUR", "ONCHANGE", "ONCLICK", "ONDBLCLICK", + "ONFOCUS", "ONKEYDOWN", "ONKEYPRESS", "ONKEYUP", "ONLOAD", "ONMOUSEDOWN", "ONMOUSEMOVE", + "ONMOUSEOVER", "ONMOUSEOUT", "ONMOUSEUP", "ONRESET", "ONSELECT", "ONSUBMIT", "ONUNLOAD", + "PROFILE", "PROMPT", "READONLY", "REL", "REV", "ROWS", "ROWSPAN", "RULES", "SCHEME", "SCOPE", + "SELECTED", "SHAPE", "SIZE", "SRC", "STANDBY", "START", "SUMMARY", "TABINDEX", + "TARGET", "TOPMARGIN", "TYPE", "USEMAP", "VALIGN", "VALUE", "VALUETYPE", + "VERSION", "VLINK", "VSPACE", "WIDTH", "TEXT", "PASSWORD", "CHECKBOX", "RADIO", "SUBMIT", "RESET", + "FILE", "HIDDEN", "IMAGE", "PUBLIC", "!DOCTYPE", + "ADD_DATE", "LAST_MODIFIED", "LAST_VISIT" + ] + + +#---------------------------------------------------------------------------- +# Icon Bitmaps - generated by encode_bitmaps.py +#---------------------------------------------------------------------------- +from wx import ImageFromStream, BitmapFromImage +from wx import EmptyIcon +import cStringIO + + +def getHTMLData(): + return \ +'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x10\x00\x00\x00\x10\x08\x06\ +\x00\x00\x00\x1f\xf3\xffa\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\ +\x00\x00\xcdIDAT8\x8dcd`f\xf8\xcf@\x01`\xfc\x7f\xa3\x87"\x03X\xfe}\xbeI\x89~\ +\x06&\x8at\x0f\n\x03\x18\xe4\x954\xff\xc3\x00\x8c-\xaf\xa4\xf9_\xc7\xc0\xfc\ +\xbf\x93\xab\xf7\xff\xff\xff\xff\xff70\xb6\xfe\x7f\xed\xce\x93\xff\xd7\xee<\ +\xf9\xafc`\x0eW\xf3\xf5\xd7\xff\xff,\x0f\x1f^gPP\xd6B1\xf4\xc1\xddk\x0c\xf6\ +\xb6\x16\x0c{wma````x\xf7\xfc\x06\xc3\xea\xa5\xb3\x198\xd8X\x18\xbe~|\x06W\ +\xc7\xc5\xca\xc0\xc0\xc2\xc0\xc0\xc0P\\\x9c\xcf\xf0\xf4\xc5\x1b\x86\x15K\x97\ +\xc2%Y\xd9y\xe0lF\x0e1\x86C\x87\x8e0\x88\x88\x8a3\xfccD\x88\xe3\xf4\x026\xf6\ +\xa9c{\xfe_<s\x18\xc5\x9b\xf2J\x9a\xff\x19\xff\x9eN\xa5(!\r|4\x0e\x03\x03\ +\x00R\xe4{\xe74\x9e\xbb\xd1\x00\x00\x00\x00IEND\xaeB`\x82' + + +def getHTMLBitmap(): + return BitmapFromImage(getHTMLImage()) + +def getHTMLImage(): + stream = cStringIO.StringIO(getHTMLData()) + return ImageFromStream(stream) + +def getHTMLIcon(): + icon = EmptyIcon() + icon.CopyFromBitmap(getHTMLBitmap()) + return icon diff --git a/wxPython/samples/ide/activegrid/tool/IDE.py b/wxPython/samples/ide/activegrid/tool/IDE.py new file mode 100644 index 0000000000..ea58536853 --- /dev/null +++ b/wxPython/samples/ide/activegrid/tool/IDE.py @@ -0,0 +1,2056 @@ +#---------------------------------------------------------------------------- +# Name: IDE.py +# Purpose: IDE using Python extensions to the wxWindows docview framework +# +# Author: Peter Yared +# +# Created: 5/15/03 +# Copyright: (c) 2003-2005 ActiveGrid, Inc. +# CVS-ID: $Id$ +# License: wxWindows License +#---------------------------------------------------------------------------- + +import wx +import wx.lib.docview +import wx.lib.pydocview +import sys +import wx.grid +_ = wx.GetTranslation +ACTIVEGRID_BASE_IDE = False + +#---------------------------------------------------------------------------- +# Classes +#---------------------------------------------------------------------------- + +class IDEApplication(wx.lib.pydocview.DocApp): + + def __init__(self, redirect=False): + wx.lib.pydocview.DocApp.__init__(self, redirect=redirect) + + def OnInit(self): + global ACTIVEGRID_BASE_IDE + + args = sys.argv + if "-h" in args or "-help" in args or "/help" in args: + print "Usage: ActiveGridAppBuilder.py [options] [filenames]\n" + print " option '-multiple' or '/multiple' to allow multiple instances of application." + print " option '-debug' or '/debug' for debug mode." + print " option '-h' or '-help' or '/help' to show usage information for command." + print " option '-baseide' or '/baseide' for base IDE mode." + print " [filenames] is an optional list of files you want to open when application starts." + return False + elif "-dev" in args or "/dev" in args: + self.SetAppName(_("ActiveGrid Application Builder Dev")) + self.SetDebug(False) + elif "-debug" in args or "/debug" in args: + self.SetAppName(_("ActiveGrid Application Builder Debug")) + self.SetDebug(True) + self.SetSingleInstance(False) + elif "-baseide" in args or "/baseide" in args: + self.SetAppName(_("ActiveGrid IDE")) + ACTIVEGRID_BASE_IDE = True + else: + self.SetAppName(_("ActiveGrid Application Builder")) + self.SetDebug(False) + if "-multiple" in args or "/multiple" in args: + self.SetSingleInstance(False) + + if not wx.lib.pydocview.DocApp.OnInit(self): + return False + + self.ShowSplash(getSplashBitmap()) + + import STCTextEditor + import FindInDirService + import MarkerService + import ProjectEditor + import PythonEditor + import OutlineService + import XmlEditor + import HtmlEditor + import TabbedView + import MessageService + import Service + import ImageEditor + import PerlEditor + import PHPEditor + import wx.lib.ogl as ogl + import DebuggerService + import AboutDialog + if not ACTIVEGRID_BASE_IDE: + import DataModelEditor + import ProcessModelEditor + import DeploymentService + import WebServerService + import WebBrowserService + import WelcomeService + import ViewEditor + import PropertyService + + + # This creates some pens and brushes that the OGL library uses. + # It should be called after the app object has been created, but + # before OGL is used. + ogl.OGLInitialize() + + config = wx.Config(self.GetAppName(), style = wx.CONFIG_USE_LOCAL_FILE) + if not config.Exists("MDIFrameMaximized"): # Make the initial MDI frame maximize as default + config.WriteInt("MDIFrameMaximized", True) + if not config.Exists("MDIEmbedRightVisible"): # Make the properties embedded window hidden as default + config.WriteInt("MDIEmbedRightVisible", False) + + docManager = wx.lib.docview.DocManager(flags = self.GetDefaultDocManagerFlags()) + self.SetDocumentManager(docManager) + + if not ACTIVEGRID_BASE_IDE: + dplTemplate = DeploymentService.DeploymentTemplate(docManager, + _("Deployment"), + "*.dpl", + _("Deployment"), + _(".dpl"), + _("Deployment Document"), + _("Deployment View"), + XmlEditor.XmlDocument, + XmlEditor.XmlView, + icon = getDPLIcon()) + docManager.AssociateTemplate(dplTemplate) + + htmlTemplate = wx.lib.docview.DocTemplate(docManager, + _("HTML"), + "*.html;*.htm", + _("HTML"), + _(".html"), + _("HTML Document"), + _("HTML View"), + HtmlEditor.HtmlDocument, + HtmlEditor.HtmlView, + icon = HtmlEditor.getHTMLIcon()) + docManager.AssociateTemplate(htmlTemplate) + + imageTemplate = wx.lib.docview.DocTemplate(docManager, + _("Image"), + "*.gif;*.jpg;*.jpeg", + _("Image"), + _(".gif"), + _("Image Document"), + _("Image View"), + ImageEditor.ImageDocument, + ImageEditor.ImageView, + icon = ImageEditor.getImageIcon()) + docManager.AssociateTemplate(imageTemplate) + + perlTemplate = wx.lib.docview.DocTemplate(docManager, + _("Perl"), + "*.pl", + _("Perl"), + _(".pl"), + _("Perl Document"), + _("Perl View"), + PerlEditor.PerlDocument, + PerlEditor.PerlView, + icon = PerlEditor.getPerlIcon()) + docManager.AssociateTemplate(perlTemplate) + + phpTemplate = wx.lib.docview.DocTemplate(docManager, + _("PHP"), + "*.php", + _("PHP"), + _(".php"), + _("PHP Document"), + _("PHP View"), + PHPEditor.PHPDocument, + PHPEditor.PHPView, + icon = PHPEditor.getPHPIcon()) + docManager.AssociateTemplate(phpTemplate) + + if not ACTIVEGRID_BASE_IDE: + processModelTemplate = ProcessModelEditor.ProcessModelTemplate(docManager, + _("Process"), + "*.bpel", + _("Process"), + _(".bpel"), + _("Process Document"), + _("Process View"), + ProcessModelEditor.ProcessModelDocument, + ProcessModelEditor.ProcessModelView, + icon = ProcessModelEditor.getProcessModelIcon()) + docManager.AssociateTemplate(processModelTemplate) + + projectTemplate = ProjectEditor.ProjectTemplate(docManager, + _("Project"), + "*.agp", + _("Project"), + _(".agp"), + _("Project Document"), + _("Project View"), + ProjectEditor.ProjectDocument, + ProjectEditor.ProjectView, + icon = ProjectEditor.getProjectIcon()) + docManager.AssociateTemplate(projectTemplate) + + pythonTemplate = wx.lib.docview.DocTemplate(docManager, + _("Python"), + "*.py", + _("Python"), + _(".py"), + _("Python Document"), + _("Python View"), + PythonEditor.PythonDocument, + PythonEditor.PythonView, + icon = PythonEditor.getPythonIcon()) + docManager.AssociateTemplate(pythonTemplate) + + if not ACTIVEGRID_BASE_IDE: + dataModelTemplate = DataModelEditor.DataModelTemplate(docManager, + _("Schema"), + "*.xsd", + _("Schema"), + _(".xsd"), + _("Schema Document"), + _("Schema View"), + DataModelEditor.DataModelDocument, + DataModelEditor.DataModelView, + icon = DataModelEditor.getDataModelIcon()) + docManager.AssociateTemplate(dataModelTemplate) + + textTemplate = wx.lib.docview.DocTemplate(docManager, + _("Text"), + "*.text;*.txt", + _("Text"), + _(".txt"), + _("Text Document"), + _("Text View"), + STCTextEditor.TextDocument, + STCTextEditor.TextView, + icon = STCTextEditor.getTextIcon()) + docManager.AssociateTemplate(textTemplate) + + xmlTemplate = wx.lib.docview.DocTemplate(docManager, + _("XML"), + "*.xml", + _("XML"), + _(".xml"), + _("XML Document"), + _("XML View"), + XmlEditor.XmlDocument, + XmlEditor.XmlView, + icon = XmlEditor.getXMLIcon()) + docManager.AssociateTemplate(xmlTemplate) + + + if not ACTIVEGRID_BASE_IDE: + viewTemplate = wx.lib.pydocview.ChildDocTemplate(docManager, + _("View"), + "*.none", + _("View"), + _(".bpel"), + _("ViewEditor Document"), + _("ViewEditor View"), + ViewEditor.ViewEditorDocument, + ViewEditor.ViewEditorView, + icon = ProcessModelEditor.getProcessModelIcon()) + docManager.AssociateTemplate(viewTemplate) + + dataModelChildTemplate = wx.lib.pydocview.ChildDocTemplate(docManager, + _("Schema"), + "*.none", + _("Schema"), + _(".xsd"), + _("Schema Document"), + _("Schema View"), + DataModelEditor.DataModelChildDocument, + DataModelEditor.DataModelView, + icon = DataModelEditor.getDataModelIcon()) + docManager.AssociateTemplate(dataModelChildTemplate) + + + textService = self.InstallService(STCTextEditor.TextService()) + pythonService = self.InstallService(PythonEditor.PythonService()) + perlService = self.InstallService(PerlEditor.PerlService()) + phpService = self.InstallService(PHPEditor.PHPService()) + if not ACTIVEGRID_BASE_IDE: + propertyService = self.InstallService(PropertyService.PropertyService("Property", embeddedWindowLocation = wx.lib.pydocview.EMBEDDED_WINDOW_RIGHT)) + projectService = self.InstallService(ProjectEditor.ProjectService("Project", embeddedWindowLocation = wx.lib.pydocview.EMBEDDED_WINDOW_TOPLEFT)) + findService = self.InstallService(FindInDirService.FindInDirService()) + if not ACTIVEGRID_BASE_IDE: + webServerService = self.InstallService(WebServerService.WebServerService()) + webBrowserService = self.InstallService(WebBrowserService.WebBrowserService()) + outlineService = self.InstallService(OutlineService.OutlineService("Outline", embeddedWindowLocation = wx.lib.pydocview.EMBEDDED_WINDOW_BOTTOMLEFT)) + filePropertiesService = self.InstallService(wx.lib.pydocview.FilePropertiesService()) + markerService = self.InstallService(MarkerService.MarkerService()) + messageService = self.InstallService(MessageService.MessageService("Message", embeddedWindowLocation = wx.lib.pydocview.EMBEDDED_WINDOW_BOTTOM)) + debuggerService = self.InstallService(DebuggerService.DebuggerService("Debugger", embeddedWindowLocation = wx.lib.pydocview.EMBEDDED_WINDOW_BOTTOM)) + if not ACTIVEGRID_BASE_IDE: + processModelService = self.InstallService(ProcessModelEditor.ProcessModelService()) + viewEditorService = self.InstallService(ViewEditor.ViewEditorService()) + deploymentService = self.InstallService(DeploymentService.DeploymentService()) + dataModelService = self.InstallService(DataModelEditor.DataModelService()) + welcomeService = self.InstallService(WelcomeService.WelcomeService()) + optionsService = self.InstallService(wx.lib.pydocview.DocOptionsService(allowModeChanges=False)) + aboutService = self.InstallService(wx.lib.pydocview.AboutService(AboutDialog.AboutDialog)) + + if not ACTIVEGRID_BASE_IDE: + projectService.AddRunHandler(processModelService) + + optionsService.AddOptionsPanel(ProjectEditor.ProjectOptionsPanel) + if not ACTIVEGRID_BASE_IDE: + optionsService.AddOptionsPanel(WebServerService.WebServerOptionsPanel) + optionsService.AddOptionsPanel(DataModelEditor.SchemaOptionsPanel) + optionsService.AddOptionsPanel(DataModelEditor.DataSourceOptionsPanel) + optionsService.AddOptionsPanel(DebuggerService.DebuggerOptionsPanel) + if not ACTIVEGRID_BASE_IDE: + optionsService.AddOptionsPanel(WebBrowserService.WebBrowserOptionsPanel) + optionsService.AddOptionsPanel(PythonEditor.PythonOptionsPanel) + optionsService.AddOptionsPanel(XmlEditor.XmlOptionsPanel) + optionsService.AddOptionsPanel(PerlEditor.PerlOptionsPanel) + optionsService.AddOptionsPanel(PHPEditor.PHPOptionsPanel) + optionsService.AddOptionsPanel(STCTextEditor.TextOptionsPanel) + optionsService.AddOptionsPanel(HtmlEditor.HtmlOptionsPanel) + + filePropertiesService.AddCustomEventHandler(projectService) + + outlineService.AddTemplateForBackgroundHandler(pythonTemplate) + outlineService.AddTemplateForBackgroundHandler(phpTemplate) + outlineService.AddTemplateForBackgroundHandler(projectTemplate) # special case, don't clear outline if in project + if not ACTIVEGRID_BASE_IDE: + outlineService.AddTemplateForBackgroundHandler(dataModelTemplate) + outlineService.AddTemplateForBackgroundHandler(processModelTemplate) + outlineService.StartBackgroundTimer() + + if not ACTIVEGRID_BASE_IDE: + propertyService.StartBackgroundTimer() + + self.SetDefaultIcon(getActiveGridIcon()) + self.SetUseTabbedMDI(True) + if not ACTIVEGRID_BASE_IDE: + embeddedWindows = wx.lib.pydocview.EMBEDDED_WINDOW_TOPLEFT | wx.lib.pydocview.EMBEDDED_WINDOW_BOTTOMLEFT |wx.lib.pydocview.EMBEDDED_WINDOW_BOTTOM | wx.lib.pydocview.EMBEDDED_WINDOW_RIGHT + else: + embeddedWindows = wx.lib.pydocview.EMBEDDED_WINDOW_TOPLEFT | wx.lib.pydocview.EMBEDDED_WINDOW_BOTTOMLEFT |wx.lib.pydocview.EMBEDDED_WINDOW_BOTTOM + if self.GetUseTabbedMDI(): + frame = IDEDocTabbedParentFrame(docManager, None, -1, wx.GetApp().GetAppName(), embeddedWindows=embeddedWindows) + else: + frame = IDEMDIParentFrame(docManager, None, -1, wx.GetApp().GetAppName(), embeddedWindows=embeddedWindows) + frame.Show(True) + + + wx.lib.pydocview.DocApp.CloseSplash(self) + self.OpenCommandLineArgs() + + if not projectService.OpenSavedProjects() and not docManager.GetDocuments() and self.IsSDI(): # Have to open something if it's SDI and there are no projects... + projectTemplate.CreateDocument('', wx.lib.docview.DOC_NEW).OnNewDocument() + + if not ACTIVEGRID_BASE_IDE: + if not welcomeService.RunWelcomeIfFirstTime(): + wx.CallAfter(self.ShowTip, docManager.FindSuitableParent(), wx.CreateFileTipProvider("activegrid/tool/data/tips.txt", 0)) + + return True + + +class IDEDocTabbedParentFrame(wx.lib.pydocview.DocTabbedParentFrame): + + # wxBug: Need this for linux. The status bar created in pydocview is + # replaced in IDE.py with the status bar for the code editor. On windows + # this works just fine, but on linux the pydocview status bar shows up near + # the top of the screen instead of disappearing. + def CreateDefaultStatusBar(self): + pass + +class IDEMDIParentFrame(wx.lib.pydocview.DocMDIParentFrame): + + # wxBug: Need this for linux. The status bar created in pydocview is + # replaced in IDE.py with the status bar for the code editor. On windows + # this works just fine, but on linux the pydocview status bar shows up near + # the top of the screen instead of disappearing. + def CreateDefaultStatusBar(self): + pass + +#---------------------------------------------------------------------------- +# Icon Bitmaps - generated by encode_bitmaps.py +#---------------------------------------------------------------------------- +from wx import ImageFromStream, BitmapFromImage +from wx import EmptyIcon +import cStringIO + +def getSplashData(): + return \ +'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x01\x90\x00\x00\x00\xb4\x08\x06\ +\x00\x00\x00\xba\xf5zX\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\x00\ + \x00IDATx\x9c\xec\xbdy\xb8eU}\xf7\xf9Yk\xed\xf1\x0c\xf7\xd6\xbduk.f(\xa0\ +\x98\x14\x84B\xa4\x04J\x01\x99\x14\xa3\xa2\xf8\x9a\xe45\t&\xfd\xb6\x99\xda<\ +\x1d\xcd\x9b\xf4\x9bt\x92N:\x1d\x9f7\xe9\xc4\x98\x8ei}}\xd4\xa45J\x82\x88A\ +\x01E\x19\x15\x19\xa4(\xa6\x82\x9a\xe7\xba\xe3\x99\xf7\xb4V\xff\xb1\xf6>\xe7\ +\xdc[\x05\xea5\xa6$\xec\xcf\xf3\xc0\xbdu\xee\x9e\xce>\xfb\xac\xef\xfa\x8dK\ +\x18c\x0c%%%%%%?"\xf2X_@IIII\xc9+\x93R@JJJJJ\x16E) %%%%%\x8b\xa2\x14\x90\x92\ +\x92\x92\x92\x92EQ\nHIIII\xc9\xa2(\x05\xa4\xa4\xa4\xa4\xa4dQ\x94\x02RRRRR\ +\xb2(J\x01)))))Y\x14\xa5\x80\x94\x94\x94\x94\x94,\x8aR@JJJJJ\x16E) %%%%%\x8b\ +\xa2\x14\x90\x92\x92\x92\x92\x92EQ\nHIIII\xc9\xa2(\x05\xa4\xa4\xa4\xa4\xa4dQ\ +\x94\x02RRRRR\xb2(J\x01)))))Y\x14\xa5\x80\x94\x94\x94\x94\x94,\x8aR@JJJJJ\ +\x16E) %%%%%\x8b\xa2\x14\x90\x92\x92\x92\x92\x92EQ\nHIIII\xc9\xa2(\x05\xa4\ +\xa4\xa4\xa4\xa4dQ\x94\x02RRRRR\xb2(J\x01)))))Y\x14\xa5\x80\x94\x94\x94\x94\ +\x94,\x8aR@JJJJJ\x16E) %%%%%\x8b\xa2\x14\x90\x92\x92\x92\x92\x92EQ\nHIIII\ +\xc9\xa2(\x05\xa4\xa4\xa4\xa4\xa4dQ\x94\x02RRRRR\xb2(J\x01)))))Y\x14\xa5\x80\ +\x94\x94\x94\x94\x94,\x8aR@JJJJJ\x16E) %%%%%\x8b\xa2\x14\x90\x92\x92\x92\x92\ +\x92EQ\nHIIII\xc9\xa2(\x05\xa4\xa4\xa4\xa4\xa4dQ\x94\x02RRRRR\xb2(J\x01)))))\ +Y\x14\xa5\x80\x94\x94\x94\x94\x94,\x8aR@JJJJJ\x16E) %%%%%\x8b\xa2\x14\x90\ +\x92\x92\x92\x92\x92EQ\nHIIII\xc9\xa2(\x05\xa4\xa4\xa4\xa4\xa4dQ8\xc7\xfa\ +\x02~|\xf4\x82\x7f\x1f\xa9\x89\xe6%\xf6\x14\xe8\xa1\xfde\xff\xa7\xe9\xff\xfd\ +h\xc7\x1f>\x8f\x06\xa3\xf33\x88|\'9\xff\x1a\x86O.\x8er1bh\xef\xa3]\xe8\xd0\ +\xa1\x8b\x9f\xf3\x0f\xa9\xe7o\x93\x9f{\xe1i\xfb\xef\xa3\x7f\xbd\xdan+\x14\ +\xe5<\xa2\xa4\xa4d1\xbc\xc2\x05D\x03\xd9\xd0\xbf\x8bQT\xce\x1b@\x8f.\x01\x1a\ +\x89F\x90\xe4[8\x80\xca\x87V\x99\x1fE\xe7"c\xf2\x81W\xe6\xa7\x10\x0c\x04\xa4\ +7\xf47\x95\x1f\xe7(\x83\xb8\x19z\xa1\xf8]\x82\x11\xfd\xa1\xdcn\xa3\x17\xec\ +\x18\x1bP\x02\xdc\xc1\xf6Y\xbe\x99\x02\x94\xc9\xec\xf9S\x03B\x80\xf402\xbf\ +\xea\xfc\xa58\xea!M\x8a\xef\xbb`\x12\x88\xbb`R\xf0B\x90\xd5\\\xf4JJJJ~4^\xe1\ +\x02\x02GZ\x10G\xff+\x0c&\xea\x06\x9dK\x8d\x99\xb7E!\x1e\x83\t\xbd\\p\x04\rF\ +\r\r\xb8\x12Pv\x94F\x82p@(\x8e\xa6\x03B\xd8\x01\xdf\x1e<\xb5\xa3\xbb\x04\x10\ +\x98\x85\x1fC!0\x02p\x8a\x1d\x0bkIb\x10\xc4\x1aD\x92P\x95\x1a\xa4\x01\xe9\ +\x80\x14\xfd+7\x80\x16\xd0\x8b\x0c\xbe\x1b\xe0JC\x96F()\xadp\x98\xd4n+()))Y\ +\x14\xc2\x18\xf3R\x1e\x9eW\x00\x85\x05\xd2\x9f\xc3\xe7?\xe5Q\xbdA\xc5V:\xb7>\ +\xc0\xe4\xe3\xa7\x1d\xc4\x178w\x10\x14\x83\xfe\xb0\xabk0\xd8\x1b18\xee\xb0h\ +\x1c\xe9\xa5J\x11$H2\x04q\xfe\xd3Z=\x86\x00\xf0\x10(0\xf2\xc8\x83\x88\x14D\ +\x06:A\x9b\x0c-\x15\xca\xf1\xd1\xa8\\Wr\x0b\xc4@\x92d\xc4Z`\xa4\x8b\x138 \ +\x16\xc8\x9f\x01\x93\x81o\xffD\x1c\xc5T|\xaf\xd4\x90\x92\x92\x92E\xf1\x1fD@`\ +X<\xe0\xa8\xa1\x86\xa1}\x06q\x8bb\xf6\xbf\xd0b(\xf6Q\xc3/.\x08,\x18 \x16\xf3\ +\x9dh\xc3\xc8\xfc\xc8\x82\x0cA\x9a\x8b\x96\x15\x0f\x99\xcb\x94\xc8]g\xe0X\ +\xeb\xc6\xc8!\x0b$E\xf7ZH\xa9\xc1S\xa0\x1c\nW[f$i\xa2\xf1\xbc\xf9\xd7\xaf\ +\x99\xff\x0e5\xd0\xcb\xac\x05\xe4-0\xd2\x14\xe0\xf3\x1f\xc2\x0c-))9\x06\xbc\ +\xe2\xc7\x8e\x81\r1\x9f#f\xd5\x06\x10\xc3C\xab\xe5h\xc2Q\xec/\x8f&\x1eC\xaf\ +\t1\xdf\xdb\x04\xb9\xc5\xc2@\xceD.j\x12\xf7\x88\x13\xd9pJ\x04$C/h\xeb"\xcb\ +\x0f*\xab!\x85$X+\xc9\x8a\x8d\x11\x90\xba\x92F\n\x99\x04%\xf3\x98\xc8\xd0\ +\xf15\xd0\xee\xc0\xae\xdd\xfb\x99\x9d\x9de||\x9c5\xc7\xaf\xa0\xea\xdb3\xea\ +\x08\x96\xf9Gy\xf3%%%%?\x04\xafp\x01\xf9!\x82\xbf\x85p\x14\x83s\x7f\x14\xb7a\ +\xe8a\x97\x15\x0c\r\xfc/cy\x0c\x8eko\xa0\x18\xdaO\x15\x9b,\xf4e-\x10\x0fS\ +\x1cka\x00[H\x0c\x12-\xb0?q\x88Im\x8c\x1c\x85\x12\x02\x91_s"\xa0\xe3X1\x90@\ +\n\xec\xde\x9e\xf0\xec\x93\x8f\xb1\xe5\xc9\xc78\xb0w;\xad\xd9in|\xdb\r\xdcx\ +\xc3\xf5\x8c\xd6\x14\x196\xde\x1e\np\xfc2\x04RRR\xb2x^\xe1.\xac\xa1P\xc1\x11\ +\x7f\xd1\xf37\xe8\xbf6$ B\x91\xe6\xb1\x8f"\x99\xd5\n\xc7\xf0\xber\xfe\xc1\ry\ +L$\xff\xdbB\x91\xe8_\xd0 \xe8\xdd\xdfv\x90$6@$6\xce\x81\x83\xc1\xed\xe7\x84\ +\x15?\xa3\xa3\\y+\x82\x83\x07`\xdf\x1c\xdc\xff\xfc~\xf6\xce\xcc\xb0k\xeb\xb3\ +\xecyq\x0b\xf1\xec\x01V\x8ez\x9cw\xeaq\xac;n\x82[~\xfefFC\x17Gd\xa4q\x8f \ +\x08P\xd2^H\xb7\x17Q\t\xc2#\xee\\III\xc9\x0f\xc3\xabH@\x86\xc2\xc9\xc2\xe4\ +\x16\x84\xe8\x0b\x88\x8b\rvS\xa4\xc5\xc2PM\xc7p\xadD.\x0c\xda\xd8(\xbaq\x8e\ +\xe2\x03K\xed\x89\xc5P\xea\xafP \xed1\xb2<n"\xc1\x9e\xddh\x8cpH\xb0\xc2\x11c\ +\x85#\x05\x0e\'\xb0{_\xc6\x96\xe7^\xe0\x99g\xb7\xb1g\xf7af\'\xdb\xb4[1\xcdT\ +\xd1\xaa\xd4I\x84@\xc4\r\xbc\xa4\xc9qc>\x97_\xb0\x9ew]s)\xaf9\xb9\x86\x8f\ +\x8ds@b3\xafLF\xda\xed\xe2\xb8.x\xc5_\xcb4\xde\x92\x92\x92\x1f\x9dW\xb8\x0b\ +\xeb\x87p\xc1,(\xb0\xb3\x0c,\x88^\x1c\x13x\x1eQ\xd4$\xf0\xa4\xad\x93\xd0)h\r\ +N\x00\xd2JK\x91\xf8\xdb?\x8a\xd4\x90i;(K\x99\xa7\xf2\x0eek\x89<\x96!\xa0\xdd\ +\xe9\xe2WF\x98j\xb7\xa9U\xab\xc4@\xd7@M@\x15I\x16C36\x04u\xc1\xde\x06<\xf4\ +\xfd\x03\xdc\xfd\xc0w\xd9\xfc\xe2v\xa6\xda=R\xe1\x10\xa5\x828\x01GU\x08\xbd\ +\x11\x8c\xe3\xd0\xc9b\x94\xe3\x91\xc6M\\\x93q\xe6\xbaS\xf8O7l\xe2\xc6K\'\xa8\ +\x00\xbd.\xd4C\xec{21E\xb8\xdf\t\x8b\xa2\x92\xfcz\xcb:\x90\x92\x92\x92E\xf0\ +\x8a\xb7@^\x9a\x81\xc5\xa15h\xad)\xde\xaa\xe38\x08!l\x16\x15\xa0\xd0d\xd1\ +\x1c\xbe/@\'\xd6\n\xc9\x0c\xb8!h\x8fD\x06h!\x07\xb2c\x0c\x0e\x19jH\xbdL?\xe7\ +\xca\xa6\x10\xa7\xd8\xe1:\xce\x7f\x17C\xaf\x1d\xea\xc1\xae\xdd-\xb6~\xffQ\ +\xe4\xe4n~\xedW\xde\xd7\xb7:\xa6Rx\xef\x07\xfe\x8a]\xd3\x1d2\xbf\xcaL\x14\ +\x93\xe1\xe2\x04!aPG\'\x82n+B\x18\x87Z(\xe9\xb5\xf7P\x0fR\xdet\xe9E\xfc\xd2\ +\xfb\xae\xe0\x8c\x89\xbc,\xa4\x0b\x13\x95\xc2\xc2\xc9\xad\x0fL^\x08S\xd4\x7f\ +8 r1))))\xf9\x11y\xc5[ /\xe5\xc32C\x83\xa2\x90\xd8\x02:\x86R\\\x8d\xd5\x08$\ +\xc4FS\xf1\x03\xfa\x11\x07Q\xb4%q\x88\xf0\xc8\x84\xec\xc7$\x00\x94\x10d\xf9\ +\xadK\x19\x88D\x11\x02\xc9\x18\xb8\xa2\x9e{\x11\xb6\xef\x99d\xcb\xb3\xdb\xd9\ +\xbe\xeb\x00\xcfo\xdd\x81\xf4\x02\xb4QLm\x7f\x92\x8f\xfd\xce\xcf\xd2\xe8B#\ +\x86\xa6\x86\xcf\xfc\xf3\x93\xec\x9a\x8c\xf0\x96\x1e\xcf\\l\xa8T|zi\x861\x82\ +\x04\x07%\xc1\xf5\r"\x13\xf8t9\xf7\xf8\x1a7]\xf7\x06\xder\xe5\xd9\x8c{\xf6\ +\x03\xf5\x04\xb8\x15Hz=\x1c?\xaf\x8c\x97\x1eE\xa8?\x13\x83:\x99~\xd0\xbf\xa4\ +\xa4\xa4\xe4G\xe4\x95/ \x05\xf3\xfaAY46P\x8cP(\xe5\xe0\x0c\x87+\xc4\xa0-H\ +\x92*B\x17\x84N\x07{*\x87,\xf3I\x95C\x04\xf4\xb0\xa2P\x9c\xc6\xcf\x7f6\x80\ +\x833\xb0ko\x8fm;\xf7\xb2}\xd7A^\xdc\xbd\x8f\x9d{\x0e39\xd3B\xb8\x01q*P\xd2g\ +dl\x9cf\xb6\x84\x8a\xa8\x10\xc5\t\xe7m\xd8\xc4u\xd7_\x80\x0b\xd4Bxq\x1b\xdc\ +\xfa\x95\xbb\xc0\xad\xd3M\x14\x8d\xb9\x16T\x05(\x05YD\xda\x9eeI%\xe4\xac\xd3\ +\x8e\xe7\xac\xd3O\xe7\xd4e\x01W\x9f7\xcaI\x13P\xf5\xec\xf5$\xbd\x0ccRT\xe8\ +\xa2\xfc\xfc\xe3\xcd+\xe53d\xbf\xec\xf2\x88b\xc9\x92\x92\x92\x92\x1f\x91\xff\ +8\x02\x02G\x15\x91 \xf0\xfb\xe5\x86QF?\xdfV\xe6\x9b\xa7\x00\xae\x9d\xdd\xbb\ +\xa2B\xd6i\xe0T\xaa }:\x99\x15\x8e\x060\x1d\xc3T\x03\x0e\x1c\x80m/nc\xc7\xb3\ +[\xd8\xb3\x7f\x86\xad;{t\xb3\x80\xc4\x80Q.\xd2\xaf\xa0\x9d\x90X\x1fO\x1c\xa4\ +\x04\x95\x90\xa8=\x87\x14\x9a\xac\x97\x11e1Q+"\xf0$7\xff\xdc{hjp$\xbc\xb0\ +\x1b\xfe\xf0O\xfeo\x12#\xe8\xf4b2\x13\xb3t\xf9\nzI\xc4\xd8h\xc8\x05\xe7\x9d\ +\xceY\xa7\x1dG\xe7\xd0\x1e\x0e\xecz\x81ucmnz\xf3\n\xd6\xb8\xe0kC\xdc\xe9\xe1\ +\x85>\x9eo\x88\xda=\xd2n\x82\x13\x04\xf9\xfd\x18\x88\xc7p\xc5\xfcK\xb5\x89,)\ +))\xf9ax\xe5\x0b\xc8Q\\X\x0b\x07\xc9BW\xa4\x9a_\x9d\xddK\xc0\r \xd107\xabY3\ +\xae\x88M\x05\xc7\xf8\xc4\x02\x9a\t\xfc\xf1_\xdc\xce\x8b\x87;<\xb3{\x9a\xfd3\ +-\xb4\xd6T<\x97\x9a\xaf\x902 \xf5\x97\x93j\x9fD\x0bR\xa1\xc8\xa4\x836\x8e=\ +\xabL\xe9\xa6\x80\xe3\xa3<\x85\xa7b*^\x05\x99v8s\xdd\x89lz\xad\x15\xb2\x0c\ +\xb8\xe3\xee\xbb\xd8\xfc\xcc\xb3T&N#\x0cC&\xd6\xaee\xed)\xa7p\xc9\xeb\x8fc\ +\xf2`\x87\xc7\x1e\xfc:\x9f\xfb\xfa\xa7\xd9t\xd1z~\xfb\x97\xde\xcd\tc\xe0\x01\ +\xa6\x17\x83/\xf1B\x97,\xea\xa2\x94\xc2\xafV\x01\xe8v;\x84\x95Z\xdf\x9dWt\ +\xf6R\x1cUkKJJJ~$^\xf9A\xf4b\x1a=\xd4\xf2|X8\xba)6\xe1H\x16\x8d\x14\xad`4\ +\x9a\xd0\xec\xc0\xf7\xb6\xec \xea\xf6h\x1f\xda\xce\x07\x7f\xf1\x1ad\x1e\xf1N\ +\x14\xecm\xc0\xe5\xef\xf80mo\x9c4X\x86SYB\x10\xd8F\x84I\xa7A\xd4m"=\x97TJ2\\\ +\x12Y\x04\xa5\xddAf\x93\xd0H\x99\xa1\x1b\xb3\xf8D\xb8i\x9b\x89\x11\x8f\xdf\ +\xf8\x95[\xb8\xfeR\x9f\n\xf0\x9d\xc7\x9e\xe3\x0f\xfe\xe4oX~\xdc\xb9\x9cy\xfe\ +\x9b8\xfbu\'\xf2\xdcvx\xfc\x89-<\xf1\xc4\x83,\x1f\x91l\xba\xf8\x0c\xdey\xf5\ +\x06\xceY\xeb z]B\xd3!\x0c\x03t\xe6apPC\x11\xfd,3\x08!\x8a\xac\xe1\x057\xeah\ +\xed\xef\xcb zII\xc9\x8f\x8e3\xbf\xf0\xad\x98\xd0\x0f\x05\xa0\x8b\x17E\xca\ +\xbc\xe9\xbe\xf91\x8c\x17\xa1\xed\x88>8a^+q\xb4A\xce\xe9oV\xb8\xa2\xcc\xd0\ +\xbf\xa5\x1cl\xbd\xb0\x17T\x06\xbc\xb0/\xe2\xc0\xe4\x0c/\xee\xd8\xcb\xf6\x1d\ +{\xd8\xb7\xff\x10\x87\xa7\xe6h4\xda\xb4\xba\t\xc2\xa9\x90%1\xa67\xcb;\xdf{\r\ +\xabB\xfa\x95\xde\xc6\x85`l%]1J\xa4}:\x9d\x84F\xc7v\xbfU\x08\x94\x1b \x84\ +\xc6\x08\x8d\x91\x9a\xdc!f\x7f\x8a|\x9e\x9f\xf6\x18\x19\xa9\xd2m\xb6\x98\xa8\ +(\xc2Xs\xea\xf2\x1a\xef\xbc\xd4\xefg\x815\xc5\x18\x7f\xf8\xd1\xbf\xe4\x89\'w\ +\xf1\xf0\xf7\x9e\xe1K\xb7}\x99V\xb3\xc3\xca\x95c\xfc\xec\xf5o\xe0\xcd\x97\ +\x9e\xcd\xf9\'A\x05\xf00\x84\x81\xce[\xed\xb6\x90\xc122\xc4\xbc\x8az\xa9\x06\ +\x1dy\xe7Y\x19F\xd3\xef\x19o\xf8\xb7I\xdf\x9dW\xa5\xaf\x87\xfe)\x07\xe7>Z%\ +\xffK\xbf\xfc\xf2\xa7\xe8\xfff?\xf5l\xc8\xba\x1at\x0f\xc8?\xfd\xa2\x0e\xc7\ +\xb65\x063h\x80Y<%\xa2\x14\xd0W//\xd5\xc3\xe8\xa8,\xec\xdam\x9f-\xf5*\x7fv\ +\x1c2\xec\x17M\xd9~L\x19\n\x8d\xec\x7f!\x15\xa0\x93\x04\xe9Et\x1b\x87\tG\xc6\ +\xec\xc8\x9ah\x90\x1eI\xd4\xc5u]prA)>\x94,\xb1\xf9\xb3\xae;\xff\x8c\xc6\x0c\ +\x84\xc2\x00ZB\xaa\xc1DPq\xe9E-\x82\xc0\xa7\x18\x0e\xa3^\x8c\xefW\x98\x9eM\ +\xa9\x8e\xd9\x80v\'\xffk\x0c$\x06\x0eO\xc2\xae\xbd\x9a\xdd{\xf6\xb1\xe7\xc0A\ +v\xee\xdd\xcb\xae\xbd\x87889C\xa6\\2\\2\xe1a\x8cK&\x0245\x1bX\x0f\xc11\x19A\ +\x00=g\x96]\x1dX\x16\x82\x9b\xd9\xb8u\xbb\x03M\x1d\xd3s\x05\xd2Ud\xe4\xef[\ +\x1b2%p\xbd:\x15\x15\xd0\xea\xf4H\xa3\x0c<\xc7V$\x9a\x14H\x11\xae\xa1VQ\xcc\ +\xee\xdc\xcc\xa9k\x96\xe1vg\x18\xa5\xcb\xef\xfe\xe2\x7faI\n-\x03\x7f\xff\xd5\ +\x83\xdc\xfd\xc8\xf7y\xfe\xe9\xa7qH\x08]\xcd\xea1\x9f\xab\xdf~\x05\xef\xb8v\ +\x03k\x96XW\x95\x87=\xb42\x06\x8cg\x05\xdc\xb5\xa6\x95\x14\xc3\xbd\xb7^\x8a\ +\xe1\x8az\xf5\x12\xf51?"f\xe8?i\x07d\x8d\xc1 \xf2\xeb\x91\x83uP\x86\xd5L\x80\ +A\xf7\x07\x7f\xb0M+\x8f\xb8\xf6\x05\xd6\xe4\xbc\xf7hl{\x9a\x1e\x1a\x81\x83\ +\x07\x90\x80\xa3\xb1\xff\x13]\x90\x89\x15\xf2\xcc!\x8a\x03\x1c\xcf\x01U\xc4\ +\xbe"$).a."%\xafJ\xb4\x86$\xb5\xfem\xd7\xa6\x93$\xa9F)\x89\xe8\xafv\x90?\xdb\ +&\xa3\x1b\xf5\xf0\x83\x80\x0cM;\xee2\xe6-\xf9\x89>?i\x9a\xe28\x0eY\x96\x91$\ +\tA\x10\x10E\x11RJ\\\xd7%\x8ec<\xcf#\xcb2\x94\xb2\xd7_\xbc\x06\x90e\x19q\x1c\ +\x13\x86Gv\x9c0\xc6`\x8c\xf5V\x00\x08!\xfa\xfb\xce\xcd\xcd1::\xfa\x03\xaf\ +\xcf\x19|\xb15f\xa8\xf5F\xe1+\xef\xb44\xa1\x07\x18M8R\x032\xb2^\x82TKA\x83\ +\x1b\x86\xfd\xb6\x1dY\x9c\xa2\xb5FJ\x89t\x1c\xc4B\xf1\xa08\x97\xc2\x18;\xce&\ +\x1d\xf0\xab\x928\x0bQ\x022\xe5\x93"\xf3\xcc \x07\'pi%\xe0\x8e9l\xd9\x07\xdf\ +}\xfa /\xee;\xc4\xa1\xd9\x19\x9ex\xfaY\xa6g\xbaD\xa9!N\r\x08\x17\xe9\x06h\ +\xe9\x12e\x1eQ\xb6\x1c\xd7\xab\xa2q0\xb90\xdaF\x85y\x9bC\x93\xe1\xeb\x88Tdte\ +\x85\xc9\xc8Z-\x1e1\x12\xcf\xc6L\x94$\xca\x0c&\x8e\xc1s\x90\x95\x11\xa4\x94\ +\x88\xb4C\x1a\xa7L\xcf\xb5\x11\xca\xc3\x0b}\x942\xa4Y\x97$\x9a\x83\xb4\x8d$\ +\xc2\t$\xe7\x9d\xb6\x86\xb3N>\x8ek/\xdb\xc8\xe5\xaf\r\xf1\xb0\xedH\xfe\xe8\ +\xbf\xdf\xc1?=\xf0<T\xc7\x10B\x128\x92\xcb_\x7f\x1e?\xf7\xce\xabX\xbf\x96~\ +\x15\xb9\xed\xbf\x9b\x0e\r\xd8\xca\x8a\xb8\x18\xba\xa5\xbc\x9cx\x14\xfc\x04\ +\x1et3\xffW;\xe0\xdb.^\xba\xff9\xce\xdf\xce\xb6\x99\xb7\x93\x88b\xb2\xf2R\ +\x0c\xb7p)\xc4d8k\xcc\xc9\xe7@:\xd7ok\xa2\xe6W\xa2\xf3\x15\xb8\x1c\x1f/\xcc\ +\xb7\xcb\x8d_\x07\x9d\xcb\\\xc9\xab\x9e\xf9\xbe^\x1cGZ]\x89\x13L\x96b\xb2\ +\x84\xb0VA\nI%\xa8\xd0\xca\xba\xb8\xca\xa7\xe2U\x8fx\x1e\xff\xad)DA)E\x1c\ +\xc7\x00\xf8\xbe\xed-\x11EQ_(\xba\xdd.\xb5Z-\x7f;\x92^/o[\xa4\x14a\x18\xd2h4\ +\x08\x82\xa0\xbf}!L\x85x\x14\xafy\x9e\x871\xe6\x87\x12\x0f\x00g\xd0\xd0o\xd0\ +\xaa\xa3\xdf\xbd\xc9@\xa5Z\xfc-&\x8b5\xcau\x91a\x1dm$F@\xa3\xd1\xc4q5^\xe0\ +\xe3\xf8\x1e\x06\xd9\xaf\x89H\xb077\xc3.\xac\xa7\x8d]\xf3Hb\xdbyh\x17\xc4\ +\x12\xbbm[\x83\xe9\xc2X\x18\x92\x01\x8a\x848\x81\'\x9f\xdd\xc9\x7f\xfd\xa3\ +\xbffF\x8f\xd3\xa2\xc6d;\xc6\r+\x8c\x8c\x06t\xa3\x8cXUH\x95\xc4\xb8.F\xfa\ +\x08\xe9cd\x00\xda\xc1\t\x04I\x927[\x17\x19\x92\x14a\x12{\x85"C\x88\x14#3\ +\x12R\xb4\xd3e\xba]\x18E\x11\xd6Y\x04&I\xa9U\xab\x04\xfe(q\xe6\xd0h\xa7\xa4Q\ +\xd7\xe65\xb9\x12\xaaU\xbc\xc0\xc1I;Ds\x87P\xf1\x1c\xeb\x96\xd7\xb9\xe8\xdcs\ +8\xe3\x94\xe38w\xfdi\x9ct\x12|\xff\xa9\x88\xa7\x9e\xdbJm\xf9\xd9\xacY\x03\ +\x1f\xfb\xdc3|\xe5\xe1-(\x932.`\xd3\x9b^\xcf{\xdeq1\xa7\xac\xc8\x17\x1f\xd4\ +\xb6\xfd\xfa\xc2\x0e\xbb\x14"\xf8\xd32\xf6\xbd\xacz\r\xf7\x02\x1b\xden\x90\ +\xe2\xa0\nk\xb7\xf8{\xbf\x8d\x0c}\xeb\xea\xa5:&\x83\xb5\xcc\xd2,\xc5U\xb2o\ +\x95\x90\xa4\x18\xed"\xfc\x00\x84\xcd>\xb3\xe9\xda \x94\xc6\xef\xcb\x96\xf9\ +\xa9\xb9\x8d%\xc7\x06\x93&\x08\xafX\x14\x0e\x92,\xc5Q\xb6\xdeJ\xf9\n2\x05\ +\xb8\x10%\xf4\x92\x087\xacPqjt\xb4\x9d,\xbb\xceO\xd6z\x15Bp\xf8\xf0a\x96-[\ +\xd6\xb7"\x92$\xc1u]\x84\x10}\x01\xa8T*dYF\xaf\xd7\xa3Z\xad\xe2\xe4\x1e\xa1\ +\xe9\xe9i\xc6\xc7\xc7\x19\x19\x19\xe9\x1f\xb3\xd3\xe9\xa0\x94\xc2q\x9c\xbe\ +\x90t\xbb\xddy\xc7\xf7<\x8fv\xbbM5O\xc8y)\x9c\x85\xdf\xa0\xc2d\x93F\x0e\xc5?\ +\x00m\x90^H\x8cC\x86\x87\x16\xb0\xe7 \xac]Q\x07l\t^\x9bAzl\x11\x11\xe8\xbb \ +\x04\xa4\x02Z\x1a\xf6\x1d\xd4\xec\xdc\xb1\x8b}\x87\xdalyf\x9a\xfd\x07&\t\x02\ +M=\xe8\xf2\xd1\xdf\x7f\x1fu \xc4\xc5s\xe1\xacsN`\xc9\xda\xd3y\xe0\xbb\xbb\ +\xa8\x1dw\x12\xd1\xa8C,\x04\xed\xb9Y\x94\xf2\x11^\x05-=\x8cV$FBZ\xac\xa7\x91\ +w\x9br<{%\xc6\xdaW\xca\xd8\xf59\x94\xb6"b\x84&5\x1a\x1c\xc5\xcc\\\x07M\xc5V\ +\xac\x0bH\xa2\x8c\xc0\xf3\xe8F]\xa6\xe6\xba\xe8\xd4\xc5\rjT\xaa!\xbeL\xc9t\ +\x8f\xcck\xd3j\xce\xe0\xbb\x8a+/}-7^}!\xebO\x81\xc3\xbb\xe1\xe9\'\xb7\xf1w\ +\x1f\xff\x0c\xbb\xf6\xec&\x13\x92\x8f\x7f\xea\xc3T\xaa\xf0\x7f}\xfc\xbb\xdcv\ +\xc7\xbf2Q\xafp\xd3\x9b\xaf\xe0\xdd\xd7\xbc\x9e\x93V\xdb{\x17u\xa1\x12\xda\ +\xd4\xde\xc9\x83\xd3\xac^1N\xbf\xa9#\xcc\x8f[\xfc\xb4\x8c~\xf9u\xe8#Vp\\\xb0\ +M_<t\xff\xa5y\xb1\x8b~k\x15\xac\x99 m\x8cb\xf8\xa8\xf6\xf9\xd2\x88\xfc>\x08\ +\x9d\xe1\nc]\xa0\xe8\xdc\x15\x11\xda\x02Q\x01\x9d\xc4\xbe$\xb0\x8b6\xaa<\xef\ +M H\xa3\x0c\xc7/\xddW\xafZ\x04\x08\xdf\xcd\xc7(Id4Ij M\t]\x07OH\x9a\xb3s\xd4\ +\x97\x8c\x82\xe3\x10\xb8a\xdf\xad\x9a\xc5\x82J\x10\xfc\xc4\x9d\x9fi\x9a\xf2\ +\xe7\x7f\xfe\xe7\x1c8p\x808\x8e\xc9\xb2\x8c \x08\xf8\xec\xe7\xfeAdib\x80y\ +\xee\xa6j\xb5\x8a1\x86V\xabE\xbd^g||\xbc\x7f\xac\xc2\x82q\x1c\x07\xcf\xf3\ +\x88\xa2\xa8o\xcd\x14\x96I\xbb\xdd&\x08\x82\xfe\xb1~\x10N\xf1\xc5\x1e8\x1d\ +\x86\x83\x8bX\x15p\x01\xe5\xda\xf5%\xb4DJ+\x18#+`*\xb3\x16\xa0,n,0\xd5\x86\ +\xfd\x87aj6f\xcb\xb3/\xb0\xff\xf0\x14/\xee\xde\xcb\xbe\x03\x87h\xb4\xbbdF\ +\x80Ph\x110\xb6j\x1d;wL3R5\xf8\xd9$\x0f|\x7f\x9a\xab\xce\x1b\'5v5\xd7\xb96l\ +\xba\xfa\x06\xee\xdd\xfdU\xba\xee8Y"\xf0Fj\xc43\x99m\x0c\xd8vA\x056\x06\xe3\ +\xc8|\xda.\xc0q\xed\x01:]{U\x99\xf5]\x98\xcc\xb1\x83\x93q\x11Z\x83\x12\x98,\ +\xc2\xf5\x04\xb3\xd3\x11\x92\n\xca\x04\xb6\xb1a/\xa2\xd7\x9e#\x18\x19c\xacZ!\ +\x89\r\xdd\xa4I\x96\xcc\xa0IpE\x93k/y-\x97]\xf16.:\xcb\xa3\x0b\xdc\xf3m\xf8\ +\xbd?{\x98m\xdb\xf7\x11G)q\x94\xa1\x98\xe0\xa3\x7f\xf6\x01\x96V\xe1\xd3\xff\ +\xf88\xdf\xfc\xe2\xdf\xf0\xc1\xff\xf43\xdc\xfc\xf6\xb7r\xca\x98\xbd<\'\xb3.\ +\xd8\xba\x07q3\xc1\xf5]V/\x1b\x1f\xca2+\x04d~\r\xc7\xcb\xc7=\xfe\x1d\xc8O>p_\ +\r\x0f\xf7\x85\x180\xd8H\x0c\x0b\x8c\xb1\r,\xe7\x91o\\$Z\x88\x81\x88\xf4\x8f\ +\xc9`^c\x1bZZ\xf3\xd5\x08\x83pF\x88$\xb43[\xb7S_28\xbd\x03xh\xdc,\xa6"}\x1c\ +\xd7\x1b\xa4\xe7\x95\xbc\xea\xb0\xe3\x95\xa4\xa732\x01\x8e\x90\xc8|BQ<\x95\ +\xb5\xf1Q0\xd0m\xf6\x08\xabA?\xef^e\xee\xbfK\x01\xae\xe38\xec\xdc\xb9\x93,\ +\xcb\x98\x98\x98 \xcb2\xe6\xe6\xe6\xf8\xfa\xd7\xee4\x85\xf5Q\x88\xc7\xa7?\ +\xfdi\x1c\xc7\xa1^\xaf\x13E\x11B\x08\x9a\xcd&Zkn\xba\xe9&\xda\xed6\x8f>\xfa(\ +\xadV\x8b+\xae\xb8\x82J\xa5\xd2\x17\x90$I\xb8\xfb\xee\xbb\x99\x9a\x9a\xe2\ +\xf4\xd3O\xe7\xfc\xf3\xcf\x9f\xe7\xdez\xc9\xeb+\xbe\\z\xe8[$0\x83Q*\x8f\x8d\ +\xa7\x1a\x12\xa9\x90\xd2\xa1\x0b<\xb37_\x7fbw\xcc\xd4\xe44\xbb\xf7\xeec\xd7\ +\xbe\xfd\x1c\x98\x9ca\xae\xd1\xa6\xddK\xe9\xa5\x1a\xbc\x80\xd4HR\x14F-E\x86>\ +\xda\x08\xe2,#N\xa1\xb1k\x12*Khdm\xaa\xa2\xca\xe7\xff\xe5\xeb\xbc\xf9\xbc\ +\xf7\xe0\n{\xeaeU\xd8x\xf1\nV\xfe\xcbR^\xdc;\x0b\xee\x08q7\x86\x14\xc2\xd11Z\ +]\x89\x11y\xddE\xa6!\xedYG\xb70\xd6,\xf5\xf2\x8f9\x1f\xb8\xb4R\xc4\xdaFR\x95\ +\xd1\xf8\x08D\xa2\xf1|\x87\xd9\xc9&\x821$\xae]L\xca\x18B\xdf\xa3\x95\xf6\x98\ +m\xb70\xc6\xb0z\xcdq\\\xfa\x86\r\\y\xd9j\xd6\xaf\xb5\x87\xfe\xce\xa33\xfc\ +\xee\x9f\xde\xc5\x13Omc\xb6\xa9\xd0\xaa\x02\xb2\x86\x14\x8aZ})\xb7\xfc\xdc\r\ +\x9c\xbf\x0e\xfe\xea\xaf\xbf\xc5\xcc\xbe\xa7\xb8\xe7\xf3\x7f\xcb\x8a1\x97\ +\x11\xa5\x11\xda\xa0\x93\x04%$\xae\xb4\xe5\xe4^\xc5\x9d\xaf\x12/\x91\xa2|\ +\xcc\x19\x16\x87y\x14\x99Qz(\xf3kx\xa7\xe1\x9f\xf9\xb3\xd6\x8fX\x8a\xc11\x86\ +2\xa6\x16\x9e\xaa\xef{6\xda\xc6<\x1c\x0f!\x14=\\\x9e\xdb\x9fr\xcfC\xbby\xe4\ +\xc9]\xdcu\xdf\xc3,\x19\xf79\xe7\xec\x15\\{\xf99\\s\xe9\xd9L\xa8*\x18h\xcd6\ +\xa8\x8d\x8dP\xf2\xeaD\x03\xed$F\xbav\xf6\xdd\x04\xb6\xee\xea\xb2s\xc7~z\xed\ +\x0e\x81\xebp\xf1k\xce`\xed\x04\xb8\xa3\x01\x8d\x86\xc1u\x04ah\x9b\x94\xf6:\ +\x10\x84\xfc\xc4\' Zk\x94R4\x1a\r\xdb\xc3o(\xf0]\xb8\x9eZ\xad\x16\xdf\xfe\ +\xf6\xb71\xc6P\xab\xd5\xd8\xb3g\x0fccc(\xa5\xa8\xd7\xeb\xfd\xed\xbf\xf0\x85/\ +p\xe8\xd0!\x1a\x8d\x06\xb7\xdcr\x0b\xc6\x18\x92$a\xd7\xae]|\xf6\xb3\x9f%\xcb\ +2\xf6\xee\xdd\xcb\x05\x17\\@\xb3\xd9\xa4^\xaf\xbf\xec\xb59z\xc8\x02Y\x98"\t\ +\xd8L\xd48A\x84.\x1aI\x0c\xec\x98\x83\xdf\xfe\xfd\xbfb\xcf\xc1.\xbd\xcc\x03\ +\xe1!\xa4\x83\x91\n-\x15\x86\x80\xc4HR#\xf1e\x8d\xc4@\xa2\x05:\x05R\'\xf7\ +\xe1+p5x=F\xc7\xaat;\x92\x8a\xe3\xf2\xfd\xa7_\xe0\xee\xfb\xb7\xf2\x9eKO\xc31\ +)J8\xac\xf4\xe0\xaa\rg\xf0\xd9\xaf?NV\r\xe9D)\xc4\x11\xdd\xa8\x83q\xbc|\xb6\ +\xaa\xed\x84\xd7h\xdb\x9a\xdd\x08Df\xd0\xbd\xd4\xb6O\x9727\x95d\xde\xbb]\x90\ +\xa5\x06\xa1\r2MQ\x99\xa215\x83\xe2x\x14\x02#\xc0w=\x82 \xe0\xe43\xcf\xe5\ +\x92\xd7o`\xc3\xc5\x0e\xcb+\xd0\x05\xee\xbfw\x9aO\xfc\xfd7xz\xfb>\xba\tH\xe1\ +\x13\xf8\xe38#\x82V\xa3\x8dJ\xa7\x18\t}\xae\xbex\x03?\xb3\t^x2\xe3\xfdo\xdd\ +\xc8ik.\xa3\xae\xa0\xd7:\x80_\xab\xd8\xeb\xf1\xf3\x0f!\xe9Y\x11t}\x8e\x08\ +\xcd\xe5S\xee\x9f\xba\xc9r!\xccG\x18\xf3E,\xc3\x0cb\x13\x80]0\xabXJx\xc8eU\ +\xa4\xd3\x1e\xf1\x06\xe7\xbb\xbc\x0c\xc5j\xf6\xb9\xa5\x93eV@\\\x9f\x08\x97-\ +\xfb{|\xee\xcb\x0f\xf3\xe5{\xb7\xb2\xfd`\x97%+\xd6\xb1\xaf7\xcb\xae\x87\xb6\ +\xf2\xe4\xf3\xcf\xb1u\xeb\x8b\xfc\xec\rWr\xfa\x8a\x8a\x15\x8f\x9f\xba\x1bZ\ +\xf2\xef\x89\xebzD\xc0\xe1\x0e\xdcv\xf7\xf7\xf9\xca\xdd\x0f\xf1\xe2\xaeC\xa4\ +\t\xb8B\xe3\x8b\x84\xff\xf2\xfe\x9b\xf9\xcf?s6\xc1\xa8\xc0D\xf6\x91S\x12\x82\ +\x7f\xa7\xd5<}\xdf\xc7u]\x1a\x8d\x06\xbe\xef\xe3y^?\xb8\x1e\x86!\xbd^\x8fZ\ +\xad\xc6\'>\xf1\t\xa4\x94|\xf3\x9b\xdf\xe4\x93\x9f\xfc$\xef\x7f\xff\xfb\xb9\ +\xf4\xd2K\xd9\xbbw/\xb5Z\x8d\xc3\x87\x0f399\xc9\x19g\x9c\xc1\x0b/\xbc\xd0\ +\x8fqx\x9e\xc7c\x8f=\xc6\xd2\xa5K\x99\x9a\x9a\xa2\xd5j\x01\xf4\x83\xf2/\x873\ +\x98\xd5\x15\xc1\xf2\xbc\xf2n\x08\xa5\xecJv\x1d\x9d\xd2\x96\x0e\x07\xa7#v\ +\x1d\x98DU\xd7"E\x95D\xf8\x18\r\x99\xd1d\xc6\x0e&B:\x08\xe5\xd0JR\x90\x0e\ +\xc2u\x10B\xd9\xf8\x82)FC\x83\xe3J\xe6\x0e\x1f\x02\x93\xd1\x0c$Z\xfb|\xe9\ +\x8eo\xf1\xceKO\xc3I4i\xd4fy\xbd\xca-\xefX\xc7\xedw~\x83=\x07Z\x10,\xc5\x1f\ +\x1d#\xc9R;@eI>\x13\x05O\x1a\\a\x90&\xc3\x08\x9b\xa6\x96\xe6\x99WY\xb1~\xa0\ +\xc9\x03\xd1\xa4\xb8"\x05\xd3De\x11Q{2OIM\x118\xf8\xbe\xcb\xa7\xff\xc7\x870\ +\x0e4R\xf8\xf6\x03p\xe7\xdd\xdfb\xfb\x0b\xcf\xa1\xd2\x06\xd2\xaf0\xc5\x18$6\ +\xda\x9df\n\x11\xb5\xf1L\x93\xe3\'\xea\x9cu\xe2R\xfe\xf7\xff\xf9L\\\xe0\xa4s\ +\x15\x8e\x86P\x82\x8e\x9a\xd4\xaa5\x9b:$\xb5\xbd\x96,\xb5~@\xc7\x03\x9d\x8b\ +\x87]g\xea\x88\x82\x0e\xebl,D\xfe\xd87\x138z:}\xe1\xca\xd2\xb9\xc0C\xe1p\xd3\ +E\xf68v\xf5E!\x06\xf5=\xc3)\xbbv\x15\x16\x89\x18\xb2\xb9\xc4P`>C\xa2\\\t\x19\ +\xa4\xb8\x1cl\xc3\xd7\x1ex\x96\x7f\xba\xeb{\xec\x99q\x18;\xed|\xa6\x0f\xcdP[\ +q*\xb2[c\xdb\xde-\xdcy\xdf3\x1c\xbfr-\xcb7]\xc0\xd2J\xd9L\xb2\x04\x1a=\xb8\ +\xf3\x9b[\xf8\xdc\xad\xdfd\xf3\x8e\x19F\x96\x9f\x8c\xb7t\t\x93\xb3S\xa8x\x8e\ +\xbf\xb9\xf5.\x1aq\x8f_z\xe7\xeb\x98\xf0A\xc7\xa0R\x9d\x7f\xf5~\xf21\xb48\ +\x8eq\x1c\x07\xc7q\x08\xc3\x90n\xb7K\xaf\xd7\xc3\x18C\xa7\xd3\xa1R\xa9\x006\ +\xfb\xca\x18\xc3\xaaU\xabh\xb5Z4\x1a\r\x00\xd6\xacY\xd3\xefF^\xa9T\xd8\xb8q#\ +\xff\xf8\x8f\xff\xc8C\x0f=\xc4\xa6M\x9b\x88\xa2\x88{\xee\xb9\x87k\xae\xb9\ +\x86\x87\x1ez\xa8\x9f\xf1U\xc4E^\x0e9\x98\xdfY\xfay\xcfF\xcf\xdbP\x03\x81t\ +\x90\xc0\x0b/ng\xc9\xd2\t\xe6\xba=ZB\xd3H#\x9a:\xa2\xe3\x18"\xd7\x90\xc8\x98\ +\xd8\x89Q\x15\x03n\x02\xaa\x8d\xa1\x891\xb3 \xe6@6@\xda\xdf\xd3\xd9\x03\xd4G\ +k\xe0\x86\xf4\x12\xc9L\x12\xf2\xc2\xc1\x1e\xb7}\xf3\x192\xcf\xc3\xaf\x84\xb8\ +\t\x9cT\x83\x1b7\xaegy\x90"\x93&\xda\x08t\xe6\xc2l\x8a\xaf\x03<\xedB\xa7C\ +\x14uq\xaa\nQ\x87\xa6\x99\xa5\xa3\xe6H\xe4,B\xcc\xe1\x9b\x06\xa1\xee\x12$\ +\x1d\x9cV\x13\xbfu\x98 \xd9K\xcd?D\xbd:\xc7yg/C\x99\x14\xe8 \x81z\x1d\x1e\ +\x7f\x02~\xef\xcf\x9e\xe1\xbd?\x7f+\x1f\xfb\xdb\x7f\xe6\xd9\xad\xb3\xb4:u\ +\xe2t)\x89X\r\xe68\xa8\x9c\x02\xfer\xe2L\x91\xf6f\x08\xf5$W\x9c\xb7\x94?\xf9\ +\xcd\x1bX\x06\xac@S7\t\xa1\x88 K\x90n\x08\xa6\x06\xc2\xa6\x0b\x18\\P\xbe]\ +\x7fD8\x03-w\x17|0\xc6~\x12\x02\x8d\xc2\xa00\xf9\x80z\xac(\xe2eG\xda\x1f\xc0\ +|\x8bVht\x9a`\xf2\x9e\\\xdd\xcc\xeac\xd7\xd8~c=l\x12F\xb7\xf8\xdd\xd8:\x1f\ +\x9bC7\xff\xe8\x854\x15\x9a4\xd7M\xe8\x02^\x15>\xf5\x85o\xd0RK\xf1O8\x83\x99\ +\xc33\x10\xd4ie>\xed\xb8\x06\xa3\xa7\xb1w\xd6\xe3\xcbw?J[\xd8\xe3\x1f\xcb\ +\xbbWr\xec\x91@%\x80\x7f\xf8\xfcW94\'\xd1\xde\xf1t\xc2\x139\xd4\xac\xd1[\xb2\ +\x8et\xf9\xe9\xec\xecz|\xe9\xde\x87\x99\xc5\xc6~\x1d\x0fp\x12\xc8:pD\x0c\xef\ +\xdf\x16c\x0cR\xca~yD\x1c\xc7\x18c\x08\x82\x00!\x04\x95J\x05!\x04Y\x96\xf5\ +\xf7I\xd3\x94 \x08\x90R\x92eY\xdf\xe5\x15\x04\x81m\xc5T\xa9p\xd1E\x17q\xef\ +\xbd\xf7"\xa5d\xc7\x8e\x1d4\x9bM6n\xdc\xc8\xcc\xcc\x0c###x\x9eG\x9a\xfe\xe0\ +\xf7\xf6C%\xa1\tG\x82I@H\\`\xd5\xca\xe5\xccMO\xe1W\xd6\x10\xb9\xfe\xa0XPJk\ +\x11\xa4)\xa41\xddV\xc3V\xe4\x01vE<\x18\xfe\xcaJ\xa3\x11\x95\x80v\xcf\xa6\ +\xcd\xa2<L\xd5c\xdb\xf4\x01n\x7f\xf0y6^|&K\x8c\xa4\x12@\x00\xbc\xeb\xea\xd7s\ +\xf7\xc3O2\x17G\x08?$\xd1)#\xe3\x01q\xa7\x85\x90\x86\xd1\xf1%t\x92.\xb3\x07\ +\x0f\x81#\xf0\xc7G!\x8d1I\x0f_f\xf8:\xa5;\xb7\x8f\xb4\xd3\xe5\xe4\x95\xabY\ +\xb7\xfe\x04\xd6\x9d\xb1\x96\xab\xae_\xcf\x8azQ\xac\x97@\xd2\xa5\x95:<\xf8\ +\x9d\xdd|\xe8\x0f\xff?\xbc\x15g\xd1\xed\xa4d\x9d\x04o\xe9*F\xc6G\x89f\x0f\ +\xd2\x9e\xe9\xc0\xb2\x15\x90&\x90vp\xb2&\xe7\x9e\xbe\x96\x9f\xbf\xf1\xdd\xdc\ +\xf8\x86\xe3\x18\x05lh=\xb1\xb3p\xed\x00\xae\xad\xe10\xf6~eB\x92\rUC\x081\ +\x94\x99\xd4\xcf\\\x1a~\xa2\x8a?\x16\xf7\xf1X\xf7\xd2-\xd6V\x1cdU\xd1\xff)\ +\xd1I\x0f\xe9\x00B ]\x97\xc4\xd8"z\xa5\xec\x97\xb1\xd8;\xc5\xfe\xbb\x9f\x14 \ +\x06\xbf+$\xc2\x14\xd5\xe5\xaa\x7f|\r\xcc\xb4\x1aTG\x96\xd2\xc8\xe0\xa9\x9d\ +\xd0\xd3!\x91\n\x88\xe2\x04V\xaf\x82\xc9\x86\xad\x07QKp\xab\x01\xba\x1d\xf3\ +\xe43/\xd0\x8ca"\xb4\x9fy\xc9\xab\x9b\xe9i8\xb0\x7f\x1a5~\x06K\x96\x9c\xc2l[\ +\x81\xaaC\x9c\x10%\x1ab\xc3L/\x01\xc7&g\xd4U\x86\xc8z6Y\xe7\x18G#\x8b\xd8\ +\x86\x94\xb2?\xe0k\xadI\xd3\x14\xdf\xf7\xfb\xae\xaen\xb7K\xa7\xd3\xe9\x17\ +\x0fn\xd8\xb0\x81\xcf|\xe63\xec\xdb\xb7\x8f\xfb\xee\xbb\x8f\xb3\xce:\x8be\ +\xcb\x96\xa1\x94\xe2\xe0\xc1\x83\xfdT\xe1\x1f\x84\xd3\x1f\xa8\xe6\xcd\xc5\ +\x8a\x82;\x86\xc7\x07\xc0\xe0 8y\xed8\x8eI\xc8\x8c&\xedi\x9b\xf1Td\xd8Hi\xf3\ +%\x85\xb4>}e\x03\xd6\xd2\x08D\x9e\x1e+s\xebF\x0b\x07\xe3x\xe8\x04\x9bI\x95\ +\x82\x18]A\xa7\x9d\xf2\x95G\xf6q\xc9#Mn~c\xdd\xd6\x84i8\xfb\x14\x97K/X\xcf\ +\xee{\x9f%BA\xb7I#\x9aC\x9a.\xdaxD-\x1f\xe1\xd4P#k1B\x92vR\\RTb \x9aatD\xf0\ +\xf6+\xcf\xe7\xbaM\x97p\xc6\xa9\x15;\xae\xf8\xb0\xa3\x01w|{\x8e\xc9\x17\xbe\ +\xc7\x1f\xbc\xf7\r\x8c\xf85B\xb7\xc2\x81\xc9\x19V\xad>\x8e\x03\xa9\xa122\x8a\ +\x1a\xf7\x99kw\x98\x9dm\x13:\x19K\xdc\x8clr\x0b:i\xb3\xee\xd45l\xbc\xf8\x12\ +\xce[\xbf\x96\xd7\xac\xaf!\x80Y@\x13\xe2\xe1\xe0\xa0Q\x12[\x048(\x83\xc0HH\ +\x87j!\x046\xb1\xa8H\x80\x90G\xab\xd0&\xdf\xff\x98\xaf$8\x1c\x9f\xb0"8oFo$\ +\xd2uA\xa7\x14\xb5F\xddD\xa3<\xc9T\x03\xbe\xf5\x9d\xa7\x10\xc1\x08\xd2\xaf\ +\xd1\xd3\x828\x03\xc7\xf3\x91:\xc5\xd1\x1d\xc6}\xcd\x15\xe7\xaf\xc6a\xc8Qg\ +\x06\xf7\x06\xc0+\xba-\x0b\x98\x9e\x05)<\x84\x91\x10u`\xfa ,[i\xcd\x1dc\xf0\ +\xbc\x90\xd0\x8c\xd2\x9aLAA\xcf@P\xfa\xaf^\xb5\x14\x86\xbe\'\xc1s]\x1aQ\x8a;\ +\xe2A\xd4\x83e\x13\xd0:\x0c\xe3c\xa0\xf7\xd2\x8dg\xe9\xe5\x1d\x8aL\xe1\x96=\ +\xe6\x937\xfa\x82P\xb8\xaf\\\xd7\xed\x8b\xc6\xb0\x00\x14E\x84\x85e\xb1a\xc3\ +\x06>\xf1\x89O\xf0\xe8\xa3\x8f\xf2\xe0\x83\x0fr\xd3M7\xf5\xf7)\xfe\x1b\x0e\ +\xd6\xbf\x14\xd6\x02\xc9g\x852\xaf!\x9e\xb7K.\xb0\x8e\x80\x9eNP\xd2cb\x14^\ +\xb3\xfeT\xbe\xb5\xf9 \xd4\xc6\xb1\xf5\xd2\x06\xe2\xc8n\x1f\xb8 ]+,\xd9 \xc1\ +\x13\xa3mv\x13\xda\xc6V\xa5D\xa7\xd2\x06\x8d3\x01\xa9\xc1\xe0!\xc7V37\x19\ +\xf3\xff|\xf1\x1b\\\xb6\xf1mL\x080m\xa8\xd4\xe0\xddo\xdb\xc4W\x15\xaf\xed@\ +\x00\x00 \x00IDAT\x1f\xdc\xc2\xbe\xee\x1c\x84\x1eh\x97z\xbdb\x83a\xdd\x98,\ +\xee"\xb3\x08\x93\xc4\xc4\x9d&g\x9ez\x12\x1b/~\x0b\x1b_\xbf\x8c\x13\xd7\xc2\ +\x84g?\xf6\x03S\xf0\xf4\xf3\x87\xf9\xfaC\xcf\xf3\x9d\'\x9fg\xaa9\xc79k+x\xbf\ +\xf0&\xd2$\xa2\rl\xdb\xb9\x8fF\xa7K\xd43DNd#gI\x8c[s\x19\xa9\x08\xb2\xe6\x14\ +\xab*p\xe3\xd5\x9b\xa8W\x1dj5x\xe3\xb95F\x1c[DY\xe9\xdfBE\x96\'\xa3\x8a"P\ +\x9cO\xd7\x17\x96\xda\x15\xb6\xc5\xf0L|\x90\xb2\x9ao\xa4\xf3\xc2\xa7\x05\xb1\ +\x91c\xc3@2\x84\x19\xbc\x97\xc1eIt\x96!\x04h!\xec:+\xc0\xf6]3|\xf2s\xff\xcc\ +\xfeFJ\xea\xd6\xe8f\x82\xd4H\x94\xe7"\xb2\x88\xaa\xe8\xb1\xa2j\xd8\xf8\xc9\ +\xdf\xcb\xcf\xe0\xd8\xd8\x990\xfd\xe3K\xc0w|b\xec p\xdc*\x18\t\x15\x07\xa7\ +\xa7\t&V\xd2\x8b#hMA3\x06\xe3\x12%\t\xbdx\x8au\xa7\x1cG\xbdb\xc5\xe3XKp\xc9\ +\xb1g|\t\xbca\xc3\xf9\xdc\xf1\xc06f\xf7\xed\x00S\xc5w&\x88t\x03ff\xa8\xfa)g\ +\x9f\xb4\x86@B\xa5xf\x94\x07if\xe3\x96\xc7\x90$Il\xe7\x8f\xfc?\xb0.\xac,\xcb\ +h4\x1a\xfd@y\xe1\xe6J\xd3\x94n\xb7\x8b\xef\xfb\x9c\x7f\xfe\xf9\xdc~\xfb\xedh\ +\xad9\xe7\x9cs\xc8\xb2\x8cN\xa7\xc3\xaaU\xab\x00~8\x01\x19$M.\xb0@\n\x04\xa0\ +u\xee\x9dJp\xf1\x08\x0c\xbc\xf5\xca7\xf2\xdd\xcd\xff`\xd7\xebv\xac?\xce\xf6\ +\x8aJl\x05\x97\x04\xebx\x02\xdbG\x06$\x86l\xe8,\xfd3J\xd7.\xe0\x1d\xf8\x98\ +\xe6,\x08I\xb8\xec$\x9e|f3\xff\xef\xad{\xf9\xf5w\xaca\xbcf\x07\xfe\xd7\x9c\ +\x08\x17\xac[\xce\xbe\xc7\xf6\xa2\x96\x9f\x8e\x96k\x98\x9b:\x0c\x9d\x03\xe0\ +\xc4,\xaf\xa4\xac[\xbd\x84k/\xbb\x90+/;\x8b\xe5\xe3\x83\x1a\x95\x18\xf8\xd6\ +\x16\xf8\xc2\x97\x1f\xe4\xd1\xa7^ \xd5.\x9df\x84F\x12\xd6\x8fc\xff\xd4\x0c\ +\xb3\x11\x98\x86\xa6\xb6\x0c\xa4S%\xc9\x0cc\xabW2;s\x18\xd7\x95\x1c\xbfn-\ +\x87\xf6oc\xf2\xd0\x0e6\xac?\x99\x0f\xff\xc2/\xf0\xad;nc\xe9\xc4\t\xbc\xe3\ +\xba\xf3m\x98\xde\xc0\xa8\x80\xa9\xc9\x03\xac\x98X\xdaw\xf0\xe8\\<\x8c\xb0^\ +\x95\xfe\x87p\x94\x0f\xe6\xa5?\xb6|G\xb3\xe03:\x96\x14\xf1\xb2\x85\xd6RnN\ +\x19#\xd0\x1aP\x0eRYq\xcdd\xc0\xb6\xddS\xf4\xea\xab\x89M\x9d\x8e\x96dF\xe2\ +\xa4\x0eR;h)\x19E\x932\xf4\x9c\x0c\xd5\x93\xc8!\xf1\xed\xb4\x1b\x84\xd5\x11N\ +Z\x05\x17\x9e\xb3\x8a}\xf7>\x8a\xe9y\x8c,]\xc1t\xbbK\x1a\xc0x\xe0\x90\xcdN\ +\xa1\xa2\xfdl\xba\xe4\n\xbc\xcc\xce\x9e\x8e\xf5\xad+9\xb6\xb4\xdbm\xbcj\x95w\ +\xdcp\x19\xbb\x0f\xb5xfw\x93\x8e2\x88\xd9\xe7\td\x8b\xb9]\x8fq\xde\xc5\xeb\ +\xf8\xa5\x1b/g\x85\xb4S\xe5f\xb3\xc9HX\xcf\xbf\x8a\xc76\x89\xa5\xa8H\x07\xfa\ +q\x92,\xcbX\xbe|9\xc6\x98~E:@\xab\xd5btt\x94z\xbdN\x9a\xa6\\\x7f\xfd\xf5\xdc\ +{\xef\xbd\\}\xf5\xd5\xac\\\xb9\x924M\xa9\xd5j\xf8\xbe?\xaf\xc8\xf0\xe5\xe8w\ +\xe3\x15\xc2\x0c}?\xc5 %\xdf\xc1\xa6\x96\xa2\xf3\xfeA\xe0\x1ax\xe3\x86S\xb8\ +\xe0\xccS\xb9g\xeb\x0c:QH\xd7\xc5\xf5\\\xeb\x92IR\xdb\xb7$w_\x91\x1fK/\xccC\ +\x95\x12<\xdf\xba\xbat\x8fe~\xc0\xe1\xfd\x870I\x86\x7f\xda\x19tk\x13\xdc\xfa\ +\xf5\xfby\xf7\r\xeff,\xb7\x1c*\xc0{\xde\xf2z\x1e\xdc\xfc\x05\xba\x9d\xbdt:1\ +\xabW/\xe7\x82\xf5\x1b\xb9\xe4\xfcu\\x\xa6\xe2\xa4\t\x08\x19\x18\x98\xf7<\ +\xbc\x9f\xaf\xde\xfb\x08[\xb6\x1ff\x7fCq\xb8#\xc9L\x00~\x157\xcc\x90R\x90\ +\x06\x1e\xd27H\x1fj\xcbB"\xe0\xb1\'\xb7\xb0|\xc5\x89l{q\'\xb5\x95K\xf1=\xc1\ +\x0b\x0f\xdc\xc7\xa9\x17\x9c\xc1\x07~\xf5wY=\xe2\xf0\xe7\x7f\xf2\xbb\xfc\xf2\ +\xcf\xdd\xcc\x95\x97\x9d\x83\x87m\n\xe8\t\x9b\xde\xb6z\xe9D^\x11\x0f\xa0PB\ +\x932h\xadQ\xc4\xc9\x07\x86\xb0\xbd\xcf&\xff\x14\xfa7\xce\xde\xac\x81[q\x98c\ +>\x02\x0e*\x89\xe0(\xd7\x87@yv"\x11\x03\xbd\x14\x8c\x03\xd2\r\xf1F\x96\xd1\ +\xf5\x97\x11{\xa3\xa4\xa9\x04\xa9H\x94B&-R\xd9\xc1\xa9\x0c\xd6\x91\xb7\xc7\ +\x15\xb9\x15\x92[;\xf9\xe34\x16zd\xd8g\xe3\xfd7mbzv\x92\xaf\xde\xf7$\xccL\ +\x13\x8e.\xa3\xd9h\x10\x870\xea\xf48\xfb\xcc\x95\xdct\xddF\xc6\\\x10\x89\x9d\ +\xbb\x94\xbcZ\xd1\xd4\xab\x1e1\xf0\x86\xf3\xc7\xd8w\xe0\x12\xbe\xf9\xd0V\x9e\ +{q\x1f\xcfo{\x82e\x13\x017\xbd\xfd\x8dlx\xcd\xf1\\\x7f\xfeI\xf8\x80\x8b\xa6\ +\x1b\x03E\xad\xd61f\xd8B(,\x86\xe5\xcb\x97s\xed\xb5\xd7r\xce9\xe7\xccscMLLp\ +\xc3\r7\xf0\x9a\xd7\xbc\x86N\xa7\xc3i\xa7\x9d\xc6M7\xdd\xc4%\x97\\\x82\xd6\ +\x1a\xdf\xf7\xb9\xfe\xfa\xeb\xfb\xd5\xec?\x0c\xceB7H\xdf\xa5"\x86^\xd0\xd6\ +\xb1\xe2\n\x8d&\xa5\xaa\x1c\x82:\xbc\xfb\xedW\xf3\xd4\'ncog\n\x1de8\xaa\x82r\ +=\xb2$\xb1=\xec\xdc\x904+\x8e(\x87\x9c\xd7\xc5\xd4\xcf\xc9\xcd\x83\x94\xbaL\ +\x08\xdb\x07\x183\x1d\xbaR\xd1\x9d\x9b"\xa8\xd79<\xb3\x8b\xdb\xeex\x8e5o9\ +\x8d\xd5A\x84g$W]p<\xef\xbb\xfa"\x9a1\xbc\xed\xb2\xd7p\xfa)#\x8c\xd5\x07k\ +\x93w\x80\xfb\x9f\x80{\x1f\xf8.\xdf\xfa\xcec\xe0V\x99\xebfL5\x12p]\xea\x13\ +\x13\x18\x1cZ\x8d\x06\x89c\x03\x12Qg\x8eY7#\x06k\x85x\xd0M\x12\xb6\xed\xda\ +\xc5\xf2S_\xcb\xa1\xdd\xdbP\xb5\n\xff\xf5\x8f\xfe\x80\x93\x8f\x87;o}\x80\xef\ +}\xfbv>\xf5\xb1\xdfg\xed\xb2\x00!s\x7f\xaa\xb0U\xe5\xbd\xb9\x0e\x81+!p\xf2\ +\xa2F\x032\xc3\x11Y\xff\xd6\xf6\xfb\\e\x0c\xf2\xe1DQ\xd2Y|0\xf6\xde\x99\xa1\ +\xfb6\xaf.\xefX\xba\xb1\xccQ~\x16\xff-H\x00(\xc2>\x19\x90d\x10\xa7\xa0E@\xcb\ +\xf8Dz\xd02\x12\r:U\xb4\x85\xa4\x9b\x89\xfe\x12\xbcGV\xb4\xd3\xef\xf4\xab\ +\x84A\x9a&\t\x0e\xe7\x9d\x10\xf2\xbe\xb7m\xc4w\xaa\xbc\xb0\xa7I32T\x03\xcd\ +\xb21\xc99\xebN\xe0\x86\xcb\xceg\xdd\xaa\xbcI\xe5\xb1\xcf\x80.9\xa6h\xda\xd1\ +\x0c\x8e_\xc5\xa3\xca\xbb\xaf=\x99\xb7\xbf\xe5d\x1as03\r\'\x1e\x0fd\x10\xc8\ +\x1e5\xdaDq\x17\xe5\x8d2\xb6d\x9cy\x0e\x9bcH!\x1ai\x9a\xf6\xc5b\xf9\xf2\xe5\ +\\y\xe5\x95\x84a\xd8O\xdf\x15B\xb0b\xc5\n\xae\xbb\xee\xba~\x17_\x80w\xbd\xeb\ +]x\x9eG\x92\xd8n\xec\x9b6m\x9ag\xb5\xfc \x9c\xf9\xbd0d\x1e\xce\x1dr\x8fHr?\ +\x9fD\xa2\xc8t\x82\'\x1dR\x03o\xb9t\x9c/?\xbc\x1ao\xe7a\xf6\x1eh \xc8\x10\ +\xa6\x8a!\x01\xed\x90\x89l \x1a\xc5\x00\xd0\x0f\xfc\xe6\xfdO\xe2\x0c\xa4\x8b\ +\x17V\x98=\xb4\x93\xf1\xb0\xca\xca\xfa([\x0f\xed\xc6\xafyxY\x87\xaf\xfc\xf3\ +\x97\xf8\xe05\xbf\x83\x14>Y\xd4a,\xf0\xf9\xb5\xf7\xbd\x91z\x1d\xbc\xbc\x05\ +\xc8d\x0c\xf7?\xde\xe4\xce\xfb\x1f\xe3\xd1\xe7vrh\xbaM\'\x83Dx\x18\xc7!s\x96\ +\xc0\x88\x02\x9d\xd1lE \x1a\x08_b\x92\x14g\xe98i\xaf\x8dN\x0f#\xb1\xeb\x8bw\ +\x80\xc9\xc3\x87\x98\x18]G{f?\xbf|\xcb\xfb\xb9\xe1z\xb8\xeb\xab\xf0\xdb\xbf\ +\xfe\x97\\\xb2~\r\x9f\xfd\xdb?e\xcdr{\xd8|\xf8\xa3\xd7\xd5\xd4<I0R\x19\xca:\ +\x1b\xae\x1d\xb7\xedb\xfa\xe21\xd4\xf5\xc3\x0e\xbaG\xf7\xca\x0f\xaf\xf8a$\ +\xf3\x0c\x94#bVEa\xa506h\xbf\xd02\x90`\x1dIE \xb0p\x8b-L\xa4\xe8o|\xf4Z\x0f1\ +\x94k\\\xbc\xc5\x97\xc8\x1e\xeb\xb5\xdb\xb8\xd5%T\x1c\x9b\xa6\xab\x05t\x12\ +\x8d\x08\\P\xf9\x02\\\xca\xb3\x8d\xab\x92\x18D\x86\x90}\xd9\xcc\xc9\x98\xd7z\ +$\x7f\xa6t\xa7\x8d\xacJ\xfc\xb8\x87\xf6|.\x7f\xdd\xf1\xbc\xe1\xa2\xe3yf\x07\ +\xec\xda\x1b\x11\x84\x925\xab\\N\\\x055 \x8d2\xdbF\xd1\x91\xfd\xc4\x8f\xc2U&\ +\x99_\x1b2\xb8\xcdEj\xbb}\x83\x99p\x86?\x82A\xe6\\\xfff\x14\xd7\'\x87\xd6 a\ +\xd0\xba\xc58y&\xde\xc2\xbb\xbapd:Z\x81f~\x1dF\x808b\x0e8\xef\xba\x17\x1e\ +\xb1\xa8\xc49\xf2\xd5\xa28s>\xf3R\xb1_\xf2\x9a^\xb9\xd4\xfc*\x19\x92\xe9\xc6\ +\x1cc#\xa3\xf8\x12F\x96\xc0\x84\x0f5\x07\x94C\xeeE\xc8\xf0\xa1\xff|km\xc3\ +\xbb\xeeO\xc9$Dk\xdd\x1f\xf8\x8d1\x84a\xd8\xcf\xa4*\xdcQ\xbd^\x8fJ\xa52\xcf=\ +U\xb4\x8a/\xb6+D\xa8p\x87\xfd \x9c\xc1\x97]!\xfa\xce\x94\xa1@\xaf!\xbfK\x12)\ +\x15^\xfe\xf0(ag\xfb\xff\xed\x7f\xb9\x9a\x9f\xff\xb5\xbf\xc6\x89\xa7\xf1\xc2\ +\x95$*\x1f\x90\xfc:F\xd6\xac\xf5\x92\xab\x1d\xaek\x9d\xff\x99-.\xc4\r\xb1\ +\xc1\xd1\x84)\r#\xe3kiE\r\xdaS{Y\xaeR\xaaZ\xf3\xda\xb3N\xe4\x9a\xcb.D\t\x88\ +\x90L5\x04\xcb\x03XS\x87f\x04;\xa7\x0c\x9f\xfa\xe2\xdd\xfc\xcb\x83\xdb8\x18\ +\xd7\xd1\xd5\tRV\x80;\x87tz\xb6H\xcd\x08\x8c\n\xc1\xad\xda\xfe\x03\x9d)p{T\ +\xebuZ8\xc8\xa0\x06\x87\x0e2>\xae\xf0#\x18ua\xcf\xbe\x84\n\x19\xaf]\xbf\x8a[\ +>\xf4V\xf6\xcc\xc2o|\xe8\xeb\xec{\xf1)\xceXU\xe1O\x7f\xe7\x9d\x1c?BQ\x9a\xd8\ +\xd7a?\xccoz!\x08H\xb28F\xb9n.\xa6\x8a4\x8d\x10RZ\xf33\x89\xecS*\x1c\xe2N\ +\x84\xf4B\x1cWa\x04\xc4Q\x82\xef\x17\xfd\x8c5E\x91\xa7@bDQ\x01b\xc8R\x8d4>N\ +\xbe\xa9N:H_\xe7\xb9\xae\xbe\xfd\xaf\x18Q\xf2\xf9\x81\xa1\rd\x18\xed#\xa5o\ +\x07\xc7(\xca\xd7D\x90\xb6w\x8d\xeb\x83\x90\xa4\xda\xea\xbc\x91\x83/\x8c\x04\ +2-Q\xd2G\t\x10$@\xdb\xa64k\x03~\xdd~\xc6\x00FS\x91\x1aL\x84\x10>\x87\x1b\xe0\ +\x8d@&\xc111t\x9bv0w\xaa\x90\x04`\\|7@d\xcd\xbe\x17\xd5\xaeL\x9fb\x9d\x93\ +\x82$\xcd\x90\xaeK\x96fxA\x00:\xc2q\x14\x98\x94\x9a\xf4\xe8\x00\xe7\x9e\x08\ +\xaf=\xd1G\x02]m\xad\x0e\t\x04\xbe\xc2MZ\xb9\xc8*\xa0J\x17\x9b\xa2\x19\xa8<\ +\xf9\xa1\x97\xe0\xf9\xae\xed(-@\xe9\x8c\xb4=\x83Su\xc08\xa4j\x846V>\xed:-\ +\xb6\xb1\x02\x06HzdY\x84\xaa\xd6\xc1\xd8\xe8b\xa2\xc1U\x1aE/Os\xaf\xf7S\xdc\ +\x07\x9a\xa8I\xba\x1d\xdc\xc0\xa7\xdbj\x13\xd6kt\xa3\x98\xc0\x0f\xf2\xdb\x98\ +b\xd2\x08\xe5b\x075-\xd0\xaaN\x9a\x9f\xbf\xf8^\x1a1\xdf\xe2\x03\x885\xe8\xd4\ +P\xf5\x04I\xd4\xc1d=*\x95\xb0\xff@\x14\xcb\x1d\x0c\xc7(\x0b1M\x93\x08L\x86\ +\x9b\x17\x96%I\x82\x11\x12\xd7q\xf3\x10\xa9\x9e\xd7\x19v!\xb3\xb3\xb3\xf8\ +\xbe\x7f\xc4\x9a\x14I\x92`\x8c9\xa2`\xad\x18\xf8\x8aN\xb1\xc0\xbcn\xb1\xc3\ +\xaf\x03\xfd\xb50\xb4\xb6\xdf\x88\x85\x03_\x92$(\xa5\xe6]\xa3\x9d\x95Kt\xa6p\ +\x95\xc3H \x07\x13\x07\x01\xa13\x88\x8fk\xed \xb4\x03\x8eO\x96j\x1c\x07\xa4\ +\x87m\x84\xa0\xcd\xbc\xd5@\x87\xaf\xa9H\xa5-\xdcA\xc3\xb1\x8a\xe2Z2\xa3\xc9\ +\xb2\x0c)%J\xda\xe7A\x9b\xfc\xef\xd8tL?\x0c\xe8t:\xf6>I\x81\x17\xf8t\xa3\x9e\ +My\xca\x8f9|\x0f\x0bq(\xc4\xa0\xf8wQp8\x1c\xdb\x18\xbeW/\xf5\xfa\xcb1\xd4\ +\x8d\xf7%v\x98\xf7\xf7A\x86\x96\xc2\xe6\xd0/\x95\xf0\xe1\xff\xe9\x97\xf8\xad\ +\xff\xed\xff$\x8ab\xa6[\x93\x10,\x03\xe1@7\x86J\x15\x02\x1f\x99e\xe8,\xb2\ +\x8f\xaa\xab\xd0:#\xebNC\x16\xe3T=T\xafAkv?+j\x927]\xfeZ\xae\xb9\xfc\xf5\xac\ +?\xcd%P\xb0\xb4\x06ss\xb6u\xc0\x92\xe5UR \xed%l\xde\xbc\x8d_\xfe\xc8_\x12\ +\x1c\xff:vL\x03cc\xa0CH\xba\xa0%\xb5\xd11\xa2v\x03)\x15:\x8e\xecB\xe8\x95\ +\x10g\xe5*T\xf7 \xadC\x93P_F\xfc\xc8#\xc8\xe3\xc6\x19\xf7%\xe3\x1e\xd0M\xe9\ +\xc4\x82?\xf8\x93\x8fr\xd2\x99\x8a/\x7f}\x86\xff\xfe\xf7\x9f\xc5\xf5\x14c~\ +\xc6_\xfc\xd1\xaf0\xe6\xd9\x14\x81y\x06\xdc0C\xb7S\xf9A^\x81o\x1f"\xd7)\xbeH\ +\xa9\x1d\x91\xd3\x04<\x1fU\xab\x91\x02\x93-\xfb\xe5\x1f\xa9\xd9\x06\x96\x18I\ +\xbe\xbb\x9d)\x0f\x05\x90\x9b3m\xc6\x97T\xf1l\x12\x1b\x08p\xc2\x1a\x88\x16\ +\xc4-p\x14\x08\x7f0\x81\x1c\xb6\x12\xd0$I\x84\xef\xe5\x02\xd3\xf7\x97:dIL\ +\xb7\x97\xa2*\x0e\xc6\xb5]\xad5\x83\xe2>7\x7f\x8f\x196\xde\xe3\xa5\n_\x83\ +\xf0C\x10\x92L84{\t\x9erp\x85\x8b\x14>q\'#\xf1!\x1c\x814\x82T\x16\x0f}\x9cg\ +\x95\x15\xcbf\x81\xd1\x8aT\xdb\xf9z\x0cxB"\xa4\x07&\xb4\xe2\x9a\x0f\xba\x9e\ +\x17\x00\td\x11\xa4\x1a\xed\x88\xc2\x13F\x82\x15\x9f"U3?S~\x86\x0c\xd2\x1ei\ +\x0cj\xa4\xcaLl\r\xa0\x04k!\x05\x81K\x94\xdf\xef\x14\x08\xa5\x8b\xe3\x07V\ +\xec\xbc\x90H\x15\xb7\xd3\xf6\x04p\x91\x83\xf5\x0b\\\x0f\xe5\xd9\x1b4\xd7\ +\xb1\x19\xea\xc6\x81\x04\x89\xc2\xc3w\x02T>YK\xf3{\x18wc\xea\xa1\x87\xeb{\ +\x90D\x84\xb5\x100\x04~\x80\x06:\xbd\x84 p\x91\x9e\x83ActL\xac\x05\xda\xc9\ +\x1b\x16\x88A=M\x96\xd9\xd6\xf5\x89\x06/\x84^\x04\xa1\x0f\xaeg\'\x1d\xa1_\ +\x03\xe32\xbd\x7f\x0f\xe3\xabV\x02\x0cM\x1e\xe7G\xb5\xda\xed\x16\xa3\xd5 \ +\x7fc\x06\xa3m\xaa\xa8F\xd0Kl\xd7Z\x18\xccX\x8d1\xa4i\xdaO-\xedv\xbb,Y\xb2\ +\x04\xb0\xc1\xe74M\x19\x1d\x1d\xb5k\xea\x08\xd1\x1f\xe4\x8aA4\xcb\xb2\xbe\ +\x108\x8e\xd3\x17\x93\xe1\x01r\xe1\xc2IJ\xa9\xfe~\x85\xf0t:\x1d\xc20\xec\xa7\ +\xa4\x16\x14\xb5\x10\xfdcj\x85P\x0e\x15o H\xb6\x9d\xf9`&np\x90\xf9\xe0\xee\ +\xc8\x81 Ie\xbf\xfd\xc6\x98\xfe\xfb\x17B\xf4{W)5tO\x872\x9a\x86So\xb3,\xc3u\ +\xec\xb9\xd2,%M\xd3~FUq\xdc\xa2H\xb0\xd7\xeb!\x84\xa0Z\xad\xda\xe0\xf9\xb1\ +\x0f\x80\xfex)\x04\x12\xa8\x03g\xac\x0c\xf8\x9d\x0f\xfe&\x7f\xfe\xf1\xcf\xe1\ +\x8c-\xa5\xa9+\xa8\xca\x12\xe6\x1a=h\xb4@\x1a\x1cW\xe3\x12\x93E\x1dt\xd2\xc1\ +WP\x0f2\x021\xc9\n\xdf\xe5\x92\x8d\xaf\xe3\xda\xab\xde\xc3\x19y\x83\xc2\xc2\ +\xb9b\x809`\xc7t\x93\xfb\x1ey\x8e\x1b\xdf\xfc:\xd2$a4\x10\\p\xe1\xe9\x9cp\ +\xf6F\xb66B\xc2\x15\x19\x91\x8a\xd0BC\xd6C\xc4\x11\xf1t\x86\xebz6\x07\xc0M\ +\xc0\x15@\x87\xb4\x95\x90&\n\x82eH\xcf\xc3;\xe3\x04*\xf1\x0c\xdd\xd9\x83LMu\ +\x98\x18\xaf\xb0\xfcd\xf8\xc6}\xf0\x7f\xfc\xd6\x17\xd9\xb9s\'\xab\x96\x8e\ +\xd3\x9a\xda\xcbG~\xed\xfd\x8c)\x08\xc5\xcb\x88\xc7\x10\xc5\xecH\x08qDq\x8eF\ +\x10g\t\xc6\xf3\xe8\x19\xe8d\xf9\xa4\xbdf\x07\x95\x83\xc0\xf3[\xa1\xd9\x88\ +\x99\x9a\x9af\xb6\xd5%5\x0e\x95\x91\xa5\x8c,\xad0V\x83\x0b\xd7U\xe9\x01SMHcX\ +\xb9\xd4\x0e"\xb3\xb33\xac\\R\xb7wP\xa4\x03\x97\x89(R\x85\xad\xf5\xe7\xfb.i\ +\x14a4\xb8A\x05\x90$)\xe8\x9ams\xdd\x05\xe6R\xd87\x05{\xf6w9<=E\xb7\xd7\xb6\ +\xe3\xa4r\x08\xc3\n\xe7\xad_\xc5I\x13\x92\x98*I\xde\x93D)\xc8\x02;\x83o\xa5\ +\x82j`\x85\xbf\x8b\xad\x8f\xd99\t\xb2:Nj\\\xa4\xc9\x1b\xb3\x98\x04L\nF\x93jA\ ++sh\xe5\xcfY\x87\x9a\x1d$E\xbe\x12%\xfd\xe4q\xd28%P>x.\xb1pi0\x10:\xbb@\x98\ +\x15\x05\x034\x9a\xb0\xb6\x0e\x15\xb7\x0e\xca\xc1\xf1}Z\x19\xa4j\xe0\xd4K\ +\xf2\xeb\x8c\x13\xd0\xae\rO\x8d\x005o\x04\xa5$(E\x96Ym\x96\xa48\x08T\x91\x15\ +\xa7\xb1\xe9\xde\xd2#E\xe2\xd4\xe0`\x1b|\xa7\xb0#=k\xe1\x08;\xe8w\x13\xfb\ +\x9c\x8f\x84^.\x04\x12\xa3S\x84Q\x90e\xc4Y\nn\x88\n\\\xba@\xa4\xa1\x9bHB?\ +\x00\x05\x87Z\xb0\xff \xec\xdev\x98\x03\xfb\xf7a\xe2\x98\xa5\xe3\xe3\xacY\ +\xb3\x86\x89\x89\x80SO\xc9\xd7\xde\x01H\xa13\x07\x13K\x00\xe33>q\x9c\xb5d\ +\x84\xf5\x89\x16i\xfc\xc5\\C\x02#\xd5\x1a\x90\x12\xf5z\xf8\xaemOd/_\xcc\x9bQ\ +\x03\xfdY\xf5\xf0`?\xec\x12)\x1a\xf3eY\xc6\xec\xecl_X\n\xd7\x89\x10\xa2\xbfN\ +Eq\xccb\x7f!D\x7f\x91\xa4\xc2\xdf\xef\xba.Zk:\x9d\x0e\xb5Z\r\xa5\x14\xcdf\ +\x93Z\xad6o\xfd\x8b,\xcb\x10B \xa5\xec\xcf\xc2a`\xe9\x14B4\xbc\xce\x06\xd0?_\ +\xa7\xd3!MS*\x95\n\x8e\xe3\x1cQ`\x97\xa6i\x7f\xd5\xbf\xe2<\xc5\xa0\xaf\x94"I\ +\x92y\xd5\xe1\x85\xb0\x14\xe2\x91fv\xffJX\xc1Q\xf3\x87\xe4(\xb6\x8bF\x15\xe3\ +GQY\xae\xb5F\x1b\x8d:\xc6\xb5`?\x96\x80\xb8\xf9T\xe5\xc41\xa8^8\xc2\xf6m\x1b\ +\xf9\x8bO\xdd\x8a\xb7t-\x8dV\x03/\xa81\xb2\xb4B\xb71\x85L:\xd4=\x8d\x17\xf4\ +\xa8\x8d9\x9cu\xfa\xa9\\t\xeeI\\\x7f\xf9\xc9\xac\xa8\x0e\xbe\xb8\x02;\x00\ +\xced\xb0ek\xc4\x13\x9b\x9f\xe6\x9eo~\x83\xf6\xdc\x0c\x87\xf7o\xa7\xd1\xbe\ +\x85\x9f{\xdb\xe5\xc4\xd8\xca\xe5\x0f}\xf8f~\xe6\x03\x9f\xc2\x8cT\xd1\xbd.8\ +\n%$c\xf5\x1a\xddF\x9b,\x15\xc8\xc0\x03\xcf\xb3.\x8b,\x06#P\xaeC\xa8\xa0\xd5\ +\x9a\xa2Zw\x99\xde\xb3\x8d\xcb.<\x99\xb1\xa5\x15\x9a\t|\xec\xf6\xbd|\xfc\xd6\ +{\xd1q\x840\x92\xb4y\x88w]\xb5\x81w\xbfy\r5\xec,\xb6\xdd\xeaP\xabU\x8e~sr\ +\x86\x97\x9a\x1c~\xf0\xa2(B\xf9\x1e\xda\x1d\xb1\xef%\xf7\xd5\xecl\xc2]\xf7n\ +\xe5\xae{\x1e\xe4\x85\xed{\xf0\xfc:\xda8\xa4Z\x92\x1aE\x82O*<\x8c\xf2\xf1\ +\xe9\x11vvs\xf2\x8a\x11\xde\xfe\xb6\x1b\xb9r\xd3J&5T%T\x96\x1cG\x8f6\x0e\xc6\ +V\x8b*\x06n5\x83\xb5J\x8au\x1f}\xbb>{\x94A7\x05\xe9CS\xc3\x03\x8f5\xb8\xef{\ +\x9b\xd9up\x86\x17w\x1fdrn\x8e,\x9f]i\x93\xa2\x93\x84\xd0\xf10I\x8fz\xc5\xe3\ +\x9c3O\xe3\xcd\x9b6r\xe9\xc5\x13\xd4\x8b\xcf\xd2\xc0G\xff\xe2\xf3\xb4:1\xedD\ +\x10\xbb\xa3d\xe12\xf67\r\xb1\x08mk\x7f\x89M\x89"\xb5E\x87\xc6\xba\xc7\xda\ +\x89\xe2\x7f\xfd\xe8=T\x98C&=\x94V8j)q\n\xc2\x83U\xe3\x0e\xbf\xf7\x8b\x97S\ +\xf7B \xa1\xd3\xd5|\xf1\x1b\x0f\xf2\xaf\x0f=\xc5\x9c\xa9\x91\xe0P\x95\x19\ +\x95\xd0#N\x12\xa4\x82\xc0U\xbc\xf3\xea\x8dl\\7A\xcdQ\xcc5;\xfc\xf5gn\xe5\ +\x80\x1e\xe7\xb9}3\xac]{<\xddV\x13\x1du\xf0}\x9fN\x02K\xaa.\xabj\x9a\x0f\xff\ +\xca[\xf1"\x89\xe7B\x96&x\xa1;\x88i\xe0\x0e\xd2\xe94\xe08D\x06\x9e\xdc\x0e\ +\x7f\xf9\xc9\xcf\xd3\xc2\x10\x8cT0\xa2\x87\xa7Bf\xf6E\x8c\x8f\xd4\x19\t\xbb,\ +\x1bU|\xe8\x96\x1b\x88b\x18\x95\x02\xe9\x87\x98n\x0b\x11V\xf0\x1d\x9b\x11X,\ +\xe5\x9cH\xd8\x9f\xc2]w=\xc7\xd7\xeey\x88\xdd{\xda C<il\x96d\x96b\x92\xd8\ +\x0e6i\x8c \xe3\xed\xd7]\xc5\xd5Wl\xe0\xdc\xd3`l\xdc\xae9\x93\xb5\x9bT\xc6\ +\xaay\x1c\xa5(\x06\x9d\x1f\xff\x19\xc4K\xc0\x0f\x02\x8c\xd6\xc4Q\x82r=\x84\ +\x04\xd7\xb1ox\xef\xde\xbdl\xd9\xb2\x85\x1d;v077\x07XwI\xa5R!\x0cC6l\xd8\xc0\ +\t\'\x9c\x80\x94\x12\xc7q\xfa\x1db\x0b\xb7\x94R\xb6G^\x1c\xc7\xcc\xcd\xcd\ +\xb1l\xd9\xb2\xfe <\xdc\x93\xa9X\xa7"\x8a\xa2\xbe\x1b\xcbu]j\xb5Z\xbfSm\xd1\ +\xda\xa3\xd5j\xa1\x94\xe2\x91G\x1e\xe1\xe0\xc1\x83\xec\xdb\xb7\xaf/\x02g\x9f\ +}6\x17^xa\xffx\xc5\xf1\x8b\x86\x85A\x10\xf4-\x86\xa2\xf5\xc7B\xd24\xed\x8b\ +\xc2p\xf1^\xb1\xdf\xb0\xf5Qt\xd2}\xf2\xc9\'\xd9\xbcy3{\xf7\xee\xc5u]\xc6\xc6\ +\xc6hw;\x9cx\xe2\x89\x9cy\xe6\x99\xac^\xbd\x9a0\x0c\xfb\xf7I\n9\xcf*+V\x0b\ +\x04\xfab\xa2~\xc2\x0bZ\xfd ~,\x01\x11\x80i\xf7\x18\xa9\x06\x88\n\xfc\xda\ +\xfb\xcfF\xcb\x1e_\xbc\xf3>L\xa0\xe9\xc4\rh\'xY\x8be5\x87\xf3\xcf=\x857m\xbc\ +\x90\xd7\x9d{\x1c\xcb*v\x06\xe9\xa6\x905#\xbc\xaa\x8f\x94\xf0\xdc\x01\xf8\ +\x97\xaf=\xcc}\x8f=\xcbT\xc7p`\xba\x85R!\xa1Wa\xf9I\xab\xf8\x87/=\xc0uW]N\ +\xa8!\xac\xc2\t+a\xfd\xba\xb5<\xf6\xfcv\xeaKW\xd0\xeaDdi\x82\t@\x07\x86\xc4h\ +\xdb\xae9\x95\xd0\x8e\xc0s\xa8\x8fW\xf0z\xb3L\x1f\xdc\xc5\xd8X\r\x15\xb5\xf8\ +\xf4\xc7\xfe\x98+\xce\xb0\xb3\xc4\x9e\x82\xe7\x0fvh\xb5\x04+O8\x91\xc6\xce\'\ +\xd8\xf8\xda\xd3\xf9\xc3\x0f^I4\xd5!\x18\xb7\x0fT\xad\xfa\xf2\xe2\x01\xf6\ +\xe1\\\xe8\x9b\xed\xfbl\x114S\xeb\xed{v\x17\xfc\x8f\xcf\x7f\x95\xaf\x7f\xfb{\ +D\xda\xa3\xbad9\xa2v\x1c\x87\x1bm\x84\xe3#\\\x1f\x1c\x9f\x04\x87HKH\r\xedD1\ +\x1e\x1c\xcf\xde\xb6\xe6\x8f\xff\xe6\x8b\xfc\xe5\xa7\x0c\xef|\xfb\xe5\xbc\ +\xf7=\xe7\xb0\xca\x83\x99,fBY\x17\x88[\xf8\xd9\x87\xa2\xab\x06\x87\xc4@\xb7g\ +\x07\xc3X\xc1l\x06\xdf}\xb4\xc9]\x0f~\x9f{\x1e|\x8c\x96\xf6iD\x86(\x11\xe0/\ +\xa5Z\x1fAy\xb6\xd9Z\xdci\x92y.\xc6\xd1\xb4\x84`\xff\xd3s\xdc\xfb\xdc\x979\ +\xfd_W\xf3\x8ek\xaf\xe0\x9a7\xd8\xf8\xc8}\x9b\xf7\xb0\xe7\xd0,\x84\xe3\xf8c>\ +\xa9;\xc3\xa1)\xdbbD\x06^?q\xd9\xb6kN@\xb8d\xc6Z \xf7|o+Jvl\xb2\x83\tQ\xa2G\ +\x92h\x82Pp\xf2r\xc9t\x17\xfc\x00\x94p\xd1\x1e\xec\x9e\x8ex\xe4\x99\xbd\xb4\ +\xbd\x15d\xd2\xa35s\x98\x8a\xafH\x92\x08%\rK\xeb>\xaf\xdfp!\xa2\xe2`p\xf0G\ +\x02\xf0\xea\xdc\xf1\x95o1kjl=\xd0cv\xae\x85\xeb8(\xc7\xa1\xd9\x8dY>\xea3\ +\xc6\x0c?\xfb\xde\xb7r\xea\x92\n\x02\xa8\xe7s\x01\x85 \x8d;\xa0\xf2gA[\x1f\ +\xb6F\x92\nx\xe8\xa9\xed|\xfb\xfb\xdb\x98\xc9@TC\x122<\xb7J\xe7P\x8f\xf1\x91\ +\nNk+o{\xf3\x05\xfd\xc9S\xa2\r\xbe\x92\x08\xdbp\x89(\x83XY\x0bjO\x17>\xf3\ +\xe5\xa7\xf9\xec\xad\xff\xca\xdcL\x8b\x91\xb5\xa7\xd1\xf5}\xb2T\xa0{=\xc8R\ +\xa4\x11xN\x85 T\xb8\x8e \xe9\xb6\xf8\x87{\xee\xe7\x1f\xef\xb8\x9d\x8b\xce9\ +\x91\xdf\xf8\x85w\xb3\xe1\xcc:aX\xc7\xa4i>\xdb.li\xdd\x17\x91\x828\xea\xe2\ +\x15kF\xa46\xd5\xd3 \xd9\xb9g/\x8f?\xfe8\xb7}\xe9\x8b\xf3b\x19\xc5\x8c\xbb\ +\xd7\xeb\x91\xa6)+W\xae\xe4k_\xfb\x1a\xeb\xd6\xad\xe3\xaa\xab\xaeb\xfd\xfa\ +\xf5T*\x15z\xbd\x1e\x93\x93\x93|\xe4#\x1f\xe9[-Zkj\xb5\x1a\x1f\xfc\xe0\x079\ +\xe3\x8c3\xe6\xb9\x81\x8a\x81\xb4\xddn\xf3w\x7f\xf7wl\xde\xbc\x998\x8e\x91R2\ +99\xc9\xf5\xd7_\xcf\xaf\xfe\xea\xaf\x02\xb6\xb6\xe3\xbe\xfb\xee\xe3\xce;\xef\ +dnn\x0e\xad5\xcdf\xb3\x9fi477\xc7\xd9g\x9f\xdd\xb7.*\x95\n\xedv\x9bv\xbb\xcd\ +G>\xf2\x91~\xdb\x8f\xc2\xaa\xb9\xf9\xe6\x9b\xb9\xec\xb2\xcb\xfa\x96\x91\xeb\ +\xba\xfd\xe6\x86\x85\x90\x15\xdf\xed"\xbe\x01\xd6\x82\xb9\xff\xfe\xfby\xfc\ +\xf1\xc7y\xec\xb1\xc7h4\x1a\x8c\x8e\x8eR\xadV\x89\xe3\x98V\xab\x85t\x14\x9b7\ +o\xe6\xf6\xdbo\xa7^\xaf\xb3q\xe3F\xae\xbb\xee:<\xd7\xa3\xd5nQ\xa9T\xe8t:\xc4\ +qL\xadV\x9bg\xdd\x14\xae\xafc\xc9\x8f\x99C\x90\xe2\xab\x0e \xa8H\x9f\x14\xf8\ +\xe5\xf7\xbd\x8e\x17_|\x82G\x9f\xde\xca\xca\xb1q^\xf7\xba\xf3\xb9\xe2\x8d\ +\xaf\xe7\xc2\xb3j\x84\xd8\xc9Y\x11\xcc\xecb\xd7\xb1\xee)\x9fo<\xb8\x83;\xeey\ +\x88\'\xb7\x1d\xa4\xa1+\xcc$>S3\x11\xee\xd8\x1a\\\xd7ej\xf60\xb3\xa2\xc7\xea\ +\xea\x08\xb7\xdd\xf9"?\xfb\xb6Sht\xc1\x0f\xe1\xbf\xfd\xe6\x95\xfc\xe7_\xfd8\ +\xae\x11D\xc2\'u\x03\xa6\xba\x1dH\x12\x82\xf1\x11z\xad\x0e\x98\x10FF!j\xd1\ +\xdc\xf6\x1cU\xaf\xcd)+\xea\x9cx\xf2\t\xfc\xf6o]E\x08\xfc\xd3mO\xf1\xce\x8dg\ +\x93\x84\xf0\xf0\x13[\xa0V\xe3\xc0\xf6\xad,wS~\xf1\xe6\xeb\t\x81\xd1%\x1e$\ +\xad\xbc\xf1\xe1\xc2\xaf\xdbK\xdc\xa5!\xbf\xe6\xb0E\xd2\x03\x12\x07\xfe\xeaS\ +Or\xebW\xee&\x12\x1e\xd5\x15g\xd2\x9d\xed\xb2{\xa6\x8b\x1f\x06\x8c\xae8\x818\ +I\xe8$\x11q\x12\x81\xc8\xec\x9b\xaeU\xf1\xe4\x18\x9d\x999\x1a3-\xc6\x97\x9eD\ +W7\xf9\xfb/\xde\xc9do\x8a_\xff\xc0\xe5,Wc\xc4\xd8\xf5\xd4\x87\x1b/\xf4\x85D\ +\xe4\x1d\x08B\x97\x0e\xb0s\x1an\xbbg\x0b\xb7\xdd\xf5\x10\xcf\xed\x9d!V#h\xaf\ +\x8e\tk0j?\xb5\xb6\xc6.\xf3g\x14\xb8\x1e\x91\xab\xe8\xf6\xda6\x12\xecy\xd0nq\ +\xf8\x89\x1dL\xcd~\x99\xefoY\xc7\x07?p\x1e:\x18\xfb\xff\xd9{\xf3pI\xab\xfa\ +\xde\xf7\xb3\xd6;\xd7\xb8k\x0f\xbdwOt\xd3-\xf3\xd0\xcc \x83\x08\x08\x8a\x03\ +\x12G\x8c\xc6\xf1\xe4\x98\xc4\'9\xc6L7\x9eDM\x8c1\xc6LG\x13\xa3GO\x1cbPDeP@@\ +\xe6\xa1\xa1\x81nh\xa0i\x9a\xa6\xe7\xee=V\xed\xaaz\xe7\xb5\xee\x1f\xeb}\xabw\ +\xa3\x89\xc4\xf6\x06\xee}\xeez\x9ez\xf6\xd0\xd5\xbb\xaa\xdea\xfd\xd6\xfa}\'d\ +\xab\xc1T\x1f\xf4T\n\x95\x0c\xec\x1a\xc1\xd0\x10ag\xceTP\x0cPk\xa9\xach\xa3H\ +\xa2\xdc\xc6\xf2\x86\xc8d\x8d\xdc\x02I\x80\xa5\xebd"A\xba\n\xe9[T\x83\xfd\ +\xb1\xc9q\x0e\xbd\xd4b__\x12\xe2\x91\xa5\x82\xe6\xe8* \x83$"KC:IB\'\x16\xf42\ +\xd3\xcf\x96\x0e\\t\xe9\xeb\xf8\xeb\x7f\xbd\x95eG\xafa*\x14\xf4,\x9b\xe1\xb1\ +\xc5\xcc\xcdw\x11M\x9b\x8e\x8c\xe9\xce\xceq\xe7\xba\x9d,\x7f\xe5R\xda\xed\ +\x98\xa1\x86G\x14\xf6hT\x0c>e\xecZJ<\xcb\x19x{\xddv\xff\xa3d\xc10^0D;S\x90\ +\xc6\xe8\xca0LX\xe42fi+\xe6\xf27\xbc\x11\x00\xcf5\x19\xefQ\xd8\xc3\xf7\xabt\ +\xba!v\xdd\xa1\x0b|\xef\xce=|\xee+\xdf\xe1\xe9gv\xe3\xad<\x86\xc6Qct\xf6\xcd\ +\x18g?W\x98`\n!PZ\x11\xeb\x9c(7\xac\xb1\xdaH\x934\xeaPk\xd4ylO\x9b?\xfd__\ +\xe2\xad\xaf}%\x97\xbfz\r\r\xc7.tH\x0b\xa8\x80\xcf\xcboq=\x8f,M\xc9\x15\xb8^\ +@\xa6r\xd6\xae\xbd\x8f\x1bo\xbe\x85\x8d\x1b\x1ec\xa8iDiQ\x14\x11\xc7\xf1`\ +\x87Q\xa9Tp\x1c\x87\xd9\xd9Y\xb2,c\xc3\x86\r<\xf6\xd8c\xacY\xb3\x86\xf7\xbf\ +\xff\xfd\x8c\x8d\x8d\xf1\xd8c\x8f\x1d\xa0\xa0\xeet:DQ\x84\xe7y?\x95\xd5]\x16\ +\x93F\xa3A\x9a\xa6\xf4\xfb}\xa4\x94x\x9e\xc7\xf8\xf8\xb8Y\xd0$\t\xd3\xd3\xd3\ +\xdcp\xc3\r\xdcp\xc3\rT*\x15\xba\xdd.\xd5j\x95z\xbd>\xa0\xb5\x8e\x8c\x8c\x0c\ +\xac\xcaK\xe08\x08\x02\xaa\xd5\xea\x00\xac\xb6,kP`\x86\x87\x87\x07\x00~\x89u\ +(\xa5\x06\xcc\xa7\x85\xdf\x97\xf7\xf8\xfd\xf7\xdf\xcf\x03\x0f<\xc0c\x8f=6x\ +\xff\x8dF\x83$I\x06-\xbbF\xa3\x01\xd2\xb4\xa6\x92$\xa1\xd3\xe9p\xd5UW\xb1n\ +\xdd:.\xbb\xec2\xce>\xeb\xec\xc1{{>\x1e\xf2B\xc2\x9e\xfe+\xc6\xc1\x15\x10\ +\xa1\xc1\x97\xa0\xe7\x89\xc2\x0e\x95\xca\x18\xd2\x81\x7f\xfa\xe4\xfb\xb8\xe3\ +\x91\xad\x9cq\xc2\xca\xc1S5f\xc2t0\xad\xaa\xe7\xda)\x8f=\xb6\x95\xeb\xae\x7f\ +\x80-\xcf\xcd\xb2gj\x0e\xbc&\xb2\xb2\x88\x9e\xae\x90R\x85\xf1:i7$u\x03*\x8b\ +\x86\x99\xdf\xb1\x89^ \xf9\xc2\xff\xf9\x1e\'\x1e\xf7?8\xfee\xe6\xbe=c)\x9c{\ +\xccrn|p\x1bye\t\xa2\xd92v\xae\xd5\x80H8\xa0"\x90\x8a\x96\x05\xe4!\xd2\xcb9}\ +\xcd*.8\xffL.>\x7f1;;\xf0\xb1?\xbf\x8apj\x1b\xefy\xfd\xb1l\xef`\xfa\xf1\xbd.\ ++\x97\xd4\xf8\xe4\x87\xde\xcbI\xabl\xdcTC\xde\x05\xc7\xa6?7G\xa55\xfa\x82\ +\x0eS\x9e\xe7\x83mr\xd9+\x05x\xf8\xb1\x9d\xfc\xfe?\xfc\x90m\xf3>\xb3Y\x83\ +\x08\x07=\xab\xc8e\r\xab1\x84\xb6\x04\xfbff\xc1\x16\xd8\x0exU\x87\x0cM\x9e\ +\xf7\xa0\xd7!I\x05\x96=Bmb9\xfbf\xf6\x81T\x8c\x8e\xae\xe0\xfa\x9f<J\xbb3\xc5\ +\xc7~\xfb\n\x96YF\x02z\x80\xb6\x07\x83%\xa4@;1\xed\xa0\x8d;\xe0/?\xffun}\xe0\ +I\xec\xe62\x82\xc5G\x92\xa66:\xd6 }SiRe\xd0o!\xcc\xb2\xdf\xb3\x88\x92\x10\ +\xf0\xc1\xaf\xc3\xf00\x96J\xc9g\xb6\xf1\xcc\xb6\x9d\xcc\xec\xd9\xc6\xd2E\x13\ +<\xfd\xcc6\xaa\x13\x87S\x1f\x1b\xa6\x938\xa6\xaf\x95\xc4\x84\xedySx\xa4Y\x01\ +\x0b\x95!Q\xc6\xdd^\tr%\xc9\x13a\x04\x1b\xcaA\xc9\n*w!J\xe9\xd9\x90)\xe3\x02\ +\xac\xb5\x01\x8d-\x17d\xd0\xc4\nZx\xc3\xcb\xc8"h\xc7\xfd\x82\xbcP!p]r\xd1\ +\x03\xbb\x8am\x9b\xc3\x11\xa6\xb0d\xb1\xc3\x9a5k\xd82\xdb\xa7\x9bU@\xfb\xcc$\ +\xbe\xa9J~\x95\xce\xfc$5o\x98ko\xb9\x97W\x9dv\x05K\xea\x1e.\xe0V< \xc5\x92\ +\xce\x00\xb0K\xb5\xc1\xb1\x12\xe0\x91\xcdp\xff\xa3\x1b\xe9\xf9\x13D\xca-Z\ +\x88.yl\x83\xf0\xe8&]V\xaf^\xc2\xd1\xab=tnp#\x05$\xda\xc6\x16\x12\xeaU\xda\ +\xc0\xa7\xff\xf1f\xbe\xf1\xc3{\xa0\xbe\x98%\'\x9d\xcb\xae\xc96\xf1s\xfb\x0ci\ +$+\xa0~]\xd0&\xb5D+\xcb\x1cW-\xe8j\x17\xa2\x1e=?@\xa4\t[\x9e\xd8F\xd7\xba\ +\x071<\xce\xc5\xa7M0\x06\xf8\xc8\x05\xcb\xa0\xfc@:\xb7\xd2\xd8\x8e\x87\x85d\ +\xa6\xdd\xe1\x86\x1bn\xe0\xdak\xafefj\x1f\xe3\xe3\xe3\xf4\xfb}\x94R\x04A\x80\ +m\xdb\x83VT\xaf\xd7C\x081\x083*cV\xd7\xae]\xcb\xcc\xcc\x0c\x17\\p\x01\'\x9dt\ +\xd2`WP\xadV\x07\xf7E9\xa9\x97t\xd3\x92qU.\xbe\xca\xddN\x19\xcb\xaa\x94\xa2Z\ +\xad\xb2o\xdf>\xbe\xfe\xf5\xaf\xb3q\xe3\xc6\x01\x161>>N\x9a\xa6LMM\x11\x86\ +\xe1`\x87\x04\xfb1\x8e\xf2{\xc7q\x06\xed\xa32\x13\xbc|\x8d$1m\xc1\xf2\xf9\ +\x0bYJ%i\xc0\xb6m\xe6\xe7\xe7\xb9\xef\xbe\xfb\xb8\xf1\xc6\x1bY\xbf~\xfd \xcb\ +\xa3R\xa9\x0c\xc8\x03q\x1c\x0f\x1e\xf5f\x83\xd9\xd9Y\x1c\xc7a\xd9\xb2et\xbb]\ +\x1ey\xe4\x11&\'\'y\xe2\x89\'x\xe3\x1b\xdf8\x08\x89J\xd3\x948\x8e\xf1}\xff?\ +\xa5\xd5\xf8\x7fr\x1c\xe4\xbb\x10\x05\r\xc7\xa5\x1aX\x05\x1fE\x12G)\x17\x9d\ +\xb0\x92~f\xee\xfd\x92\x84\x1a\'p\xeb\xbam\\\x7f\xdb\x9d\xac\x7fb\x0b\x9d\ +\xc4\xa5\xaf\x87\x10\xce(jl\x82$\x03\x94m\xe8\x9cv\xcd\xacrk\xc62\xa0?\xd7\ +\xa6\xb9l%s\xb3\xcf\xd1\xb4\x02\xbe\xf4\xad[\xf9\xe4G_\xc92\x17\xe6\xe6\x14\ +\xbf\xf5\xee\xd7\xb2n\xe3?\xb0\xcfI\t\xb3\x14R\x01\xc3#\xb0g;\xceh\x13?\xea1\ +\xfb\xcc\x13,\x1fk\xf0+\xbfr\to\xbc\xeceL4\xe1\xb6\x875\x7f\xf2g\x7f\x89\xe5\ +\x05,\x1b\x1e"\x01\x9e\xda<\xcb\xde\xadO0q\xc8*\xde}\xe9\x99\\t\xbc]l\x97\n\ +\xe9r\x9aSi\r\x13F1A\xf0\xf3\xe5\xfe\x0bU\x9de;+\x8a"\xee\x7fh\x1d\xcfM\xcf\ +\xb2O\x8f\x92\xd8.\xb8\x15\x84\xed\x1b\x93\xf6L\x91\xab\x1c\xaf\x1a T\x8c\ +\xce#t\xda+|x\x04H\x1b\xe1\x07d\x9d9\xe4\xf0(\xf4=\x883R\xabA\xbb\xbd\x9bG\ +\x1e\xdb\xcb\x83\xebfYzjkA\xd7*\x1d\xf0\x14\x05\xb69[.\xfcd}\x9f/\x7f\xeb\ +\x1a\x1e\xd8\xb8\x1d\x7f\xd1j\xbaT\xe9\xce&\xc6\xe7\xda\xb6L\xf1P\x1a<\xcb\ +\xb0\xeaJ\xca\xef\\\x84v\xa4\xa1\xf8\xd8>\xb4\xe7\xc9\xbbm\x90)+C\xbe=\x98\ +\x00\x00 \x00IDAT\x96-\xe1\xc8eC\xbc\xfe\xd2q~p\xfd!l\x9e\xec\x10\xc6\x12\ +\xfcQp,CE\xd6\xa5\x10\xa2\xd4\xa2\xc8\x81\xc2\\\xa3\xc9unV\xd8v\x0e\xda3\xb8\ +\x8d\x16 5\x81\xd0\xf8\x85\xfe\x06e\xdef\n\xa4\x99`\xae\x97BU@j\x817\x04I\ +\x1f-\x12r\x99\x90\xa9\x84L\r\x9afx\x8e\x99\xf4\xdf\xf9\xd6+\xf8\xf0\xc7\xff\ +\x96`\xc9\xb1Dn\xcd\xd0\xce\x9a\x13\x06d\xce$\xc1\xc8b\xee~\xe8\x11\xb6\xed\ +\xb9\x82%\xc5\xc2E\n\x03*\xc9"I\x12\xad\xd1\x96E&`\xa6\x07\xf7>\xfc82\xa8cy5\ +T\xa4\xa0\xd2\x00\xcf\x83n\x17t\x82C\xcc\xd9\'\x1eOK\x16\x0b\xab\xcc\xc8`\ +\xdc\x8a\xc7\\n,\xcf~\xf7\xcf\xbf\xc3\xc3\xcf\xec\xa1\x9d\x07\xa4\xa1\x84}s\ +\xa6\x1d\xebW\xcd\xcd\xa5\xfb\x08\xcc\x04frd,S\xc1\xa4o(\xca\xd25\x8c\x8f4\ +\xc4\x19\x1e\xc5m\x8c\xf1\xd0\x13O\x92~\xebV\x86G\xde\xce\x05\xab\xf7O\x02\ +\x96\x96\x85\xe6\xa1\xd4\xb1\x98s$\x80\x9d{\xf6\xf2\xf5\xaf\x7f\x9d{\xef\xbd\ +\xd7\x84\xac\xadZe\xc0\xe5\xa2}U^\xe3\xe5J\xb9\x04\xc3\xdb\xed6\xbe\xef\x93$\ +\t\x96e\xd1j\xb5\xd8\xb1c\x07\xd7_\x7f=\xb3\xb3\xb3T\xab\xd5A{Ik}\x00\x0b\ +\xcb\xf3\xbc\x9f\x99KQb\x89Q\x14\xd1\xef\xf7Y\xb6l\x19SSS\\\x7f\xfd\xf5\xdcw\ +\xdf}\xd4j5|\xdf\'\xcfs\xa6\xa7\xa7\xa9V\xabT\xab\xd5\x81\x06\xa2d\x7f-\x04\ +\xf8+\x95\xca\xe0=\xcc\xcf\xcf\x0f\xec\xd2K\xc1]Y\xb0\xcaQ\x16\x1f\xc3\xd82\ +\xbf\xcf\xb2\x8c\x1f\xff\xf8\xc7\\}\xf5\xd5\xcc\xcc\xcc\xb0j\xd5\xaaA+O)\xc5\ +\xdc\xdc\xdc\x00\xbb\xb0m\x1b\xdf\xf7\x99\x9c\x9c\x1c\xecx\xa6\xa7\xa7\x11B0\ +11A\x9e\xe7\xdcq\xc7\x1d,Y\xb2\x84\xbd{\xf7\xb2x\xf1\xe2\xc1\xb1)C\xa5\xe2$\ +\xc6w\xff\x8bR\xad\xfe\x9dq\x90\x05\xc4\x06\xdb0)\xcaN\xaa\x03\x8c\xfaF\xe6_\ +\xb1\x0c\xec\xf0\xf0\xfa\x9d|\xf3{7\xf3\xc0\x93;\xe8Z\xc3\xcc\xe6>=y$\xaaR\ +\x83\xb8g\x84d\x96\x052/T\xd9\x89\xa1\xa1\n\t\xbd\x1e\x0c5\xc1\xee\xd1\xee\ +\xb7\x19\x1d\xa9\xb1w\xe7$k\xb7<\xcd\x0f\xef8\x827\x9f\xb0\x98E-\t\r8\xf1\ +\xf81\xee\xd8\xd2!\xcc$\xd4F\xa1\x9b\x83]\xa1\x1a\xf7\x10\x9d\xcd\x9cvt\x93\ +\x0f\xfe\xda\x95\x9csj\x8d\xc9>\xfc\xf5\x97\x9e\xe0\xbb\xd7~\x1f\xbf\xd9\xe2\ +\xb9\xbds\x1c{\xdc\x1a\x12`j\xcfN\xaa\xc9>\xae8\xebb~\xf3\xf5\x87\xd2\x00l?3\ +K]<\xf0$ZH\xdc\x17P<\x80\x03@\xb82\xa8\xde\xf7}\xde\xf5\xae\xcb\xb8\xbbW\xe7\ +\x07\x0f>M\x1e\xc5f\x82\xd69\xd2r!Op-\x01q\x9f%\x8bZ,\x19]\x8e\xc83\xa6\'\ +\xf729=K\xa6R\xa4\'\x99\xf5\x1c:\xfdI\x101\x95\xa5\x8bi\xef\xdb\x87W_F\x98\ +\x84|\xeeo\xbe\xc9\xc5_\xfb\x105i4%v>\x8fU1\x17{\xb7\x17#\xaa#\xcc\x03_\xfc\ +\xd6u\xac\xdf\xbc\x8f\xc8n\xd1O=\x82\x91Ed3\xa1\xa1\xfc\x88\x14\xbf^%\xe9\ +\xb6Qa\x9fZ\xcd\'\x8aB\xb2~\xc8\xd0h\x0bG\xc0\xe4\xdc>\xa8hP\nQ\x13\x8c\xfa\ +\x01\x87\x8c8\xfc\xe5\x1f]\x80\x0f\x84S\xdb\x18\xa9,\xa6\xadR\xe6\xbb{\xa9\ +\x0c\x8d\x10\xf6\xa6\xb0\x83\x06\x99r!\x08 \x8b\xd0y\x8e\xb4-D\x96\xa3\xd3\ +\x1c\xdb\x81,\x0fA\xa7X\xb6\xc6Q\xc6\xfdY\xe6}Z\xb6\xa0\x92\x1aA\xa6o\x19_;\ +\x81Y\xa5V\xebMz\xb9\x02\xa7fH\x13\x96\x03Z\x91\xe71J+\x1c\xcb\x1a\x08?\x05\ +\x86\x0cr\xca1\xc3\x1cq\xc88[\xbb=\xb0\x0c\x85\xd6\x08`b\x18\x1a\xa6\x1b\xcf\ +`[\x01\xd7\xdcp+\'\xfc\xc6+\xa9H\n\x9e\xac_\\\x17\x9a,\xcf\xb0\x02\x87\xf9\ +\xdc\xcc\xefW}\xf7z\xb4\x1c\xa2\x1f% *\xa6\x08\xc7)\xb8\x12G\xa6\xd4{s\\q\ +\xc1Q\xd4\x95\xa9\x9a\x8e\r\xbd\x0c\x12\xdb\xd4\xbe\xbf\xfe\x97\x07\xb9\xfd\ +\xb1\x1d\xec\xe9d8CK\xf1\xfc\x06\xdd^DI6\x90smZ2\x81\xa4\x87\x12\x12\xc7\xad\ +\x90j\x97n\x02\xa9\xa8\x18-\x8ep0\xd93>I\x1a\x90$\x12jG\xb2u\xc6\xe7\x0f\xfe\ +\xec+\xfc\xe8\x9f\x7f\rK\x98PL\x0b\x8cf\xcb)\xb0\xba~\x17\xab\xda$\xcf5W]\ +\xf5\x1d\xd6ox\x1c\xcbv\r\xb9\xa0\xdf\xc5\xb5\x1d\xf24\xc3u]*\x95\n\x8b\x16-\ +\xe2\xf0\xc3\x0fg\xe5\xca\x95X\x96E\xbb\xddf\xef\xde\xbdl\xd8\xb0\x81\xe7\ +\x9e{\x0e0\xe0o\xb9R\xbf\xf1\xc6\x1b\x07\xbb\xf3\xb2%S\xb2\xa1J0\xba\xc4F\ +\x9e\xcf^*w\x18\x8dF\x83\x1d;v\x90e\x19\x9b6mbxx\x98~\xbf\x8f\xef\xfb\xc4q<\ +\x88{\x1d\x19\x19\x19`3\xa5\xa8n\xe1n\xa2\xc4.\xb2,\x1b\x80\xd5e!<\xf7\xbc\ +\xf3\x85\xca3#\x0b.\xdeK\t\xb4/\xdc\t\xdcz\xeb\xad\\\x7f\xfd\xf5\x84ah\x00\ +\xf2^\x0f\xdf\xf7\xe9v\xbb\x83\xac\xf2z\xbd\xcea\x87\x1d\xc6\x11G\x1ca\xdec\ +\x9a\xb0w\xef^\x1e~\xf8a\xb6m\xdb6`\x9b\xe5yN\xa3\xd1\xe0k_\xfb\x1a\xc3\xc3\ +\xc3\xf4z=\x1a\x8d\xc6\xa0-(\x840\xf4\xfb\x17y\x1cT\x01)\xb5\xd1\x1a\xd3\xd5\ +\x90\xba\xcc\xa9f\x00\xd6\xd6$\xdc~\xd3ml}f\x07I\xe2\xb1\xbb\xaf\x90#\xcbP\ +\xee\x08 \xa0\xde\x07\xdd7\xf4M\xcfP\x17IB#\xf8\x13\x02*\x1e\x96\x15\x93\xcb\ +\x0cTJ\x84O\x16\xd4\xd8<\xdd\xe5\xdfn\xb8\x85+\xcf\xfeU\xf6M\xf6\xd1\xa3\x15\ +>\xfeGo\xe1\x95\xef\xf8\x14C\xf5*s;\x9e\xc5]\xb4\x1cK\x870\xb7\x87\xf7\xbe\ +\xf9"\xde\xf1+\'\xd1\xf0Ln\xc4?\x7f\xe3~\xee{h\x03\xd2k1\xdd\xcdh-Y\x8d\x0cZ\ +\xc4\xc0\xdd?\xb9\x89\xb7]\xfa\n\xde\xf3\xfa\xd3i\x02\x96\xea\x15\x1f\xd2\ +\x06i\x0f\xd2\xf3\xca\x8f\xfa\x8bv#%\xf0\xc7\xff\xfd<\xf6\xfd]\xc6\x86\x8dO3\ +;\xb7\x13r\x1b\xc7\xab\xe0\x08\xc1\x9b.\xb9\x84\xd7^p\x08K\x86`\x91\xb7_\xb4\ +\xb8e;|\xf7\xbbw\xf2\xdd[\xee\xa0V[L\'\x03\xe2\x84\xfe\xde\xd4(\xf4l\x87\xf6\ +\\\x84g\xb9\xac{\x08.<\x15<\xd71T^\x12\xd2$\xa1Zm\xd2\x01\xbe\xfa\x9d\r\xac\ +\xdf2\xc9t\xe2 \x82&(\xdfx\xfd\xb8>\x90c\x91\xc3\xfc^\x86D\x8a\xeb\xa5\xe8\ +\xb8C\xd3\x02\xea\x10wv095\xc3\xcb\x8e>\x9e\xcd\xcf=\x8b\xa8T\xa9K\x9b\x11[\ +\xf0w\x9f|\x1bu\xca\x05\x85f_<I\xddn\xa1\xb1\xb0B\x8dc\'$\xaaO\x86c\xda-\x85\ +\xd7\x97\x12`\x0b\x81-\x14\xae\xa5\x88\xb2\x0eZ\x84xy\x17\x07\x17\x9djd\x1a\ +\xe2\xf75t\xf3B\xd3\xa1\x90Zp\x80\x8ft)\xff.]\x8b\x07\xc7\xbc\x0c\xe4\xda\ +\xcf:r\x05,o\xc1\xab\xcf9\x8d\xcf~\xfd&\xec\xb1\x06\x99**\x92\xceAJ\xc2~\xc8\ +Dk\x82G\x9ez\x96N\x0cC\x0eH\xdb\x1d\\\xfcY\x9e"\\\x97\xb0`L?\xf8\x84&\xccl"\ +\xc0rlr\xe9\x15\x02\xc2>BD\xa0\xe6x\xcbkOgi\x05\xc8{\x90{\x08\xe9\x18\x18\ +\x05\xb8\xfe\xfey\xbe\xf7\x93\x87\x98\x17MT\xdd#\xb5\xab\x84\xb3\xf3\xe0H*\ +\xae\xa0?7\xc5\xf2!\x8f\x97\xb5,.>\xfb"N>\xf5h\x84\r{\xa6\xe0\xe9\x1d=\xeeZ\ +\xf7$w\xae{\x82(\xeab\xd7F\xc9m\x0f\xdd\x0b\xcd\xa7v\x1at\xfa1\xd5J\xc0w~\ +\xb0\x89\x0f\xbe\xf1pc\x89\x0fX\xaeiy\xe19X\x81Y\xf8\xfc\xe4\x8e\xdbyt\xfdz,\ +\xd7Ed)\xbd^\x8f\xb1V\x83\xdd{v\xd2\x1a\x1a\xe1\xac\xb3^\xc1\xf9\xe7\x9f\xcf\ +\xa1\x87\x1ez\x00\xfb\xa8\x1cQ\x14\xf1\xcc3\xcfp\xc3\r7\xf0\xd0C\x0f\r\x8a\ +\xc8\x0bI\xbc\xfb\xb9\xf7\x90\x94\x8c\x8c\x8c\xd0\xeb\xf5\x98\x9f\x9f\xc7\ +\xf3<\xb2,\xa3\xd5jq\xce9\xe7p\xe2\x89\'\xa2\xb5fll\x8c4M\xd9\xb5k\x17K\x97.\ +\x1d\x84+\xbdP\xc1\xdc\xbf7\x84\x10\xf4\xfb}\xa6\xa6\xa6\xb8\xfa\xea\xab\x07\ +\xe9\x81\xe5qh\xb7\xdbx\x9e\xc7q\xc7\x1d\xc7\x85\x17^\xc8\t\'\x9cp\x80\x8e\ +\xa5\x14g\xbe\xe1\ro`\xe7\xce\x9d\xac]\xbb\x96;\xee\xb8\x83]\xbbv\rh\xc5\xa5\ +Z<I\x12\x84\x10\x07\xd0\x9c_\xecq\xd0\x8d\xb4r2-\x15\x9c\xd6\x01\xf6\x1d6*\ +\x87?\xfe\xc3+\xf9\xec\x97\xef\xe6\xaa\x1f?\xc2Xm\x9c\xdd\x990\xed\x88~\x17l\ +\x05a\xd7\xf4r]\x1bO*T\x16!\x94\xc6\xb2]\xd2$\xc3\x91\x10J\x1b\x94$\xce}\x9c\ +\xc6R\xd2\xf99\x1e\xd84\xc9\xe7\xbey\x17\xbf\xf3\x9e\xb3\x99\x8d\xa1\xe5\xc1\ +%g\x9c\xc8=\x8fn\xc6\xaa\x0b\xe2\x99\xf5\x9c}\xe2\xd1|\xe0\xca\xb7p\xd4\xa1\ +\x06\xa4\xbc}m\x8f\xcf~\xf9\xdb<\xb9\x0f\xec`\x8c\x9a\x9b\xd2\x9d\x9c\xc4\ +\xc9\x04\x96\xe3B\x06\rWq\xe5\x15\x97p\xc4\x08X\xaa\x0b*4\xefW\xd8\x03aY\xd1\ +q>\xa8D\x00\x07\xa3\xa3\xf9\xc7\xdfz\x15\x1f\xfd\xe4.\x1e\x9d\xd9\x8a\x90\ +\x0eG\x1e:\xc6_}\xe2\x9d\xd41bE\x17\xb0\x94\xd1\x05V+\xb0t9\x9c\xf0\x9e\xb3\ +\xb9\xec\x82\xa3\xf9\xc0g\xbe\xc1|\xa2\xf0\x17\x8f\x12v\x14^\xad\x89\x88\x15\ +YV\xa5\xd2lr\xfb\x1d\xeb\xb8\xe0\x94\x93\xcc\x0bJI\x9e%\xe4B\xa2qxt\xcb\x14\ +\xd7\xdct\x17\x93\xfd\x1c\xed\xb5\xb0\x83!\xe8f\xd0\xed\x83\xef!\xb2\x10\xab\ +?\x85\xea\xee\xa11\xe4\xb2d\xb8\xc6P#\xe0\xc8#\x0fgxx\x98g\xb7\xee`\xd3\x96\ +\xad<\xbde\x03\xe3^\x80\xebf\x1c~\xc82>\xf5\xb17"C\xa8\x14\x00\xf7\x15\xaf>\ +\x93PT\xd1\x95Q\xba* \xcc`jv\x8e\x9b\xee~\x98p\xd02)Z \x85e\xa7c\xe5\xd4]\ +\xcd{\xdf|9"o\xe3\x03\xbep\x91\xb1D\xa415?e\xac\x96\x15\x99\xcc\n\x89@`!\n\ +\xab\x98\xc1\xf2\xa6\x8cP\xd6\x1aKgH\x9d!\xc9\x06\x8d30\x1b\xdf\x8a\r\x97_x\ +\n_\xbd\xfaFB\x99\xd0\x919d\xa19\xdb\x99\x06a\xa1,\x9b\xa7w\xec\xe2\xa1\x8d]\ +\x16\xad\xa9Q)-b\x04DI\x8ak\xbb\xf4\x8b\xfb\xfa\x86[\xee\xa5\x9f[\xa4R\x90k\ +\x81\x14\xa0\xd2\x98\xc0RX\xe9<u\xa7\xcd[.>\x1d[\xa5\x90u\xc1\x16d\x99\x8dr\ +\x04]\xe0\xea\x1b\xefb\xf3\xf6Y\x18^\x01~\x03\xa5M\xbb\xb8\xe2\t\x82<d\xe9X\ +\x8d7\x9c\xb3\x86\xff\xf6\xba\x138d\xd8|\xdc\\@\xb6\x1c\xe2\x13\xab\\|\xda\ +\xc9\\\x7f\xc78\xff\xfc\xaf\xd7\x12\xc5!Vm\x84\xe9\xb8\r\xae\x8f\x17x\xc4\ +\xd3\x1d\x12W\xf2\xed\x1f\xfc\x88\xb7\xbe\xeepZ\x96\xb9w-\x1c@\x1a\xb32\xd7\ +\xa5\xd7\xe9p\xf3M\xb7\xb0gr\x92zc\x88J\xa5J\xbf3G?\xec\xb2\xfa\xd0Cy\xd7\ +\xbb\xdf\xcb\xca\x95\xabh6\x9b\xe6\xfc\x15BB\xdb\xb6\x07\x16\xe2\x9e\xe7q\ +\xcc1\xc7\xb0|\xf9r\xee\xba\xeb.n\xbd\xf5V\x1e\x7f\xfcq\x96/_N\xbf\xdf?\x88;\ +\xc8\xbc\xde\xcc\xcc\x0c\xbe\xef3:j0\xc9\xd3O?\x9dK.\xb9\x84\xa5K\x97\xfe\ +\x94B~bbbP4~Y@t\xa5R\xe1\xfb\xdf\xff>\xdb\xb7o\xc7\xf7}\x16/^\xcc\xec\xec\ +\xec`\x17\xf5\xd6\xb7\xbe\x95K.\xb9d\x80\x11\x95\xc3u]\x92\xcc\x14S\xcf\xf5X\ +u\xe8*\xea\xf5:CCC\xdc{\xef\xbd\xac_\xbf\x9eZ\xad6(HI\x92\x0c\xf0\x8f\x85\t\ +\x84/\xe6\xf8%#1\xca\xb0\x84Df\xccbD\x8a\xe3\x05D\x1a~\xfd}ga\x0f/\xe6\x1f\ +\xaf\xb9\x93\x86\xe3\xd1\xee\xed\x80J\xbd\xc8\x101\xd2j\xd9\xef#\xc9\xf1\xa5\ +\xc0\x92\x0eB[\xccE)\x19i\xb1=\xd7\xa4\xdd\x0c\x82\x00\xbbQG:6\xfft\xcd\xcd\ +\xbc\xea\xd5g\xb3\xb80\xbe\xfd\xfd\xf7\xbf\x9a\xcb.\xff\r\x8e;\xfc\x18\x8e<\ +\xead\xdey\xe59\x0c\xd5\xa0f\xc1\xbf|\xfbI>\xf3O\xdf@\r-CU\x97\x10)I\x16\'x\ +\xcdQ\xe2N\x8ff-\xc0V\xf0\xb67\\\xcaq\xab\x1bd\xb1\x06\xbbtI:\xb0\xe2\x1f\ +\xcc\xce\xa3\x1c.\xd0P`\xa5\xf0\xf9?\xfcU>\xf2\xfb\x9fatd\x82?\xfe\xbdw\x9a\ +\xce]\x15\xf2\x10\x9aA\x11~V/\xb4\x12Z\xd1\xaaI\xce8v\x84W\x9e}\n_\xbb\xf5Q\ +\xa4\xce!\x8f\x89{]\xc3\xfb\xc4&J%\x8f?\xb1\x998;\t[@`\x0b\xa4]A\xe2\x11c\ +\xf3o\xdf\xfb\t\xbbf#\xfc\xe6\x12"*(\xe1\x9b\xa8\xce$E\x92`g\x1d\x16U2\xc6\ +\x87\x9b\xbc\xe9\xa2\xb3\xb8\xf2\x8a5\x04va\x8b\x01\xd8\xf2pv\xcd\xc0W\xbf\ +\xf6C\xee\x7f\xf0!\x1c\x99\xf0\x89\x0f\xbf\x91\x15\x01\x90\x1b\xb6\x9d\x05\\\ +y\xf9\xabp\xeb\x96q\xe3\xc5D\xd7>\xb6y\x057\xddv\'\xd8M\x03((\rZ\x9a\xc9VklK\ +Pq5\xef~\xcd"\x1c\x16\xe1a,F,m.1\xcb2m\x17\x13\x88\xa6QB\x03\x16B\xab\x05\ +\xbea\xa5\xfd\x8bB\x92\x15\xc5%7\x9em,\x90\xc4(\x03\xb3\x1c\xb1\x18.8c\r?|d\ +\x17^}\x84\xb8\xd73\xa2\xc0$\xc2\xae\xd4\x98\x99\x9f\xa2\xe14\xf8\xfe-ws\xe1\ +\xc9\x17\x9bH\xfb\xe2b\x90E-\xb1]\x98\x05\xeeZ\xbb\x81X\x06\xa4\x96K\x9e\x15\ +\xefE)\x9a\xbe\x83\x8ec^~\xd42\x0e\x9f\x00\xe2\xae\xf9\xecHRm\x0ca\xee^\x17\ +\xb2a\xf3v\xac\x91\xc5\xe4\xf8\x06\xcf\x88B\xbc\xe1atw\x1fi\xd4\xe6\xa2\x0b/\ +\xe6\xd7\xder$\xcb\x02\xb0u\n\xb9B\xa7\x1a[\xb8\xd8\x9e\xe4\xf0q\x98\xb8b\ +\x19\xdb\x9e;\x9ck\xefx\x08%c|\xcf%\x17\x1a\xcfu\x88\xad\x9cDk\xb6M\xce\xb1\ +\xfei\xcd\xe9G\x1a\xb5\xa1\'0,\xbf~\x07\\\x9f\xb5k\x1f\xe2\xd9m\xdb\x19\x1d\ +\x1bgrz\x86\xe6P\x1d2\x9f<\xeeq\xe5\xdb\xdf\xce\x9a5k(m\x17J\xe3\xbe\x85+\ +\xe6J\xa5\xc2\xde\xbd{\x19\x1f\x1f\xa7\xd1hp\xce9\xe7099\xc93\xcf<s\x90w\xcf\ +\xfeQ\x8a\xff\xb4\xd6\x9cp\xc2\t\\x\xe1\x85,_\xbe\x9c4M\x07\xad\xaer\xe2.[O\ +\xf0\xcb) Q\x14\x11E\x11w\xdey\'\xa3\xa3\xa3(\xa5\x06\x8a\xf14My\xeb[\xdf\ +\xca\xab^\xf5*l\xdb&\x8a"\x92$\x19\x88\x1c\x95R8\xb6C\x92&\x03\x1cilt\x8c\ +\x8b/\xba\x98j\xb5\xcaSO=5\xc0\x8eJvW\xa91)\x89\x00/\xf68H\x15\x8a\xc2"\xc5"\ +\xc62:Y41\x90\x82LA\xc4 z(\xdd\xa7\xdf\xcf\xf9\xd5\xcbW\xf1\x87\xbfy\x19\xf5\ +\xfci\x9a\xf6\xb3\x04\xd9s8q\x97\xba]\xa1\xe57\xa8j\x0f;wp\xa9 \x94O\xd8\x07\ +\xcf\x19"K<\xb2\xc4\x03]\x81\xd8\x82\xae&K-\x12Q\xa5\xed\r\xf3\xa9\xff\xfd}\ +\\\xd7\x98\x1a\xae\xa8\xc1o\xbd\xe55\xfc\xf6\xbb.\xe5\x8f>t\x0e\xe3M\x98\x8d\ +\xe1}\xbfw\r\x7f\xf3\xe5\xeb\xa9\x8f\x1fOf\r\x1b\xcai\xda\'\x0bs\xdc\xa0\x81\ +_\xf5\x18\xaf:T5\x9cu\xf2\xd1\x00hG\x98\xc8BY\x98\xfd\t5\x80{\x7f\x19k\x17[\ +\xc3\x08p\x88\x03\xcb\x80\xbf\xff\x83\xdf\xe5\x13\xbf\xf9N\xaa\n\xea\xb9q}\ +\x1a*V\xf1Y\xd2%\x8b\xbb\xe4YH\x92E$yD\n\xbc\xe7]g2\\k\xd0\x9b\x9b1\xbeZq\ +\x08\xbeC}\xa4I\xa7\xd7e_{\x96n\n\x91Rd\xd8$\x04\xc4x<\xb9\x13\xee\xb8\xff\ +\x19\x12\x15\x80]E\xa5\x8a<J\xc1\xf7!p\xb1UD\xa0\xfa\xe8\xf6.~\xff\x83\xef\ +\xe0\xbf\xbfu\r-\x01N\x0c\xc3\x12Z\x12\x92\x1e\xac\x1c\x86?\xfe\xf0%\\v\xf6\ +\xd1\xfc\xed\xff\xfcu\x8e\x9f0\xaa\xed\nfu\xa23\xcdh\xdd\xa2\x8e\xf9}\xf9u\ +\xa8\x0eq\xd8\xc1%1,9mn\x08%,\xb3\xc3\x13\x9a\xc0\xd2T1\xc7h\x14h\x01M\xccy\ +\xae\x88r\xf5\xa3\xd0\x08\xf4\xc0\xb1\xb8d\xa1\x96\xbb\x0f\x05\xda\x14\x8f\ +\xfd\xde\xbeja\x97\x15\xd7\x01\x92\x08\x1f\xb8\xfc\xd5\xe7\xa1\xc3i\xac\xacg\ +\x16Byd\xbc\xc3\x04dX\xe8\xca\x08\xb7\xad{\x8aY\x05\xfd\x05\x9e\x1f\x81\xef\ +\r\xca\xd6\x86\'\x14\xcf\xed\x9bC;\r2\xe9\x83\xf4\x90\x8eo\xbc\x93\xc2\x10O\ +\xc5\xbc\xf6\x95g\x18s>\xc7\x05\xb7\x0e2@9\x86\x08\xf0\x83\x1f\xdd\xcaT\xbb\ +\x87\xe37\xc0r\x90\xae\x0fQJ\xe0\xf9dQ\xc4\xd8P\x9d+^w$CAa\xbd\xa2R2Kc\x076\ +\xae\x0fin|o\x02\x01\xef\xbc\xf2\x02\x9a\x95\x8c$\xdcK\xbd\xa6\xc9\xd4<\xfd\ +\xa4\r\x01\xa4"\xc7\xa95\xb9\xed\xee\x87\x0cA\xcc2D\x17\xd3J\xb0@)\xee\xbe\ +\xf7~\xb4\x02\xd7\x0fp<o@\x9f}\xc59gq\xd2\xa9\'\x13\x85\xe1\x01:\xa7\xd2u\ +\xc1\xb6\xed\x01\xf67>>\x0e\x98\x96M\xb5Z\xe5\xc2\x0b/\xe4\xac\xb3\xcebzz\ +\xfa\xa0\xef\xa1n\xb7\xcb\xb2e\xcb\x06\x00\xf3\x9b\xdf\xfcf\x0e=\xf4P\xc0L\ +\xee%\xb5\xb6\xa4\x01\x97\xdfGQt\xd0\xaf\r\x06\xe8\xff\xde\xf7\xbe7(J\x8dF\ +\x83\x9d;wb\xdb6\x87\x1dv\xd8\xc0\xfd\xb6\xdf\xef\xef\xa7\xeer\xa0Y\xa1\xeb\ +\xb8x\xaew@\xfb\xef\xf8\xe3\x8f\xe7\x92K.\x01\x8c\xfdJyL\x17\n\x1c_\n\xe3\ +\xa0\n\x88i\xe1\x98\x9c\x10\xabp\xf2U8\xe48\xe8\xe2\x91\x03\xaee1R\xb1\xf0\ +\x80\xd7\x9e6\xcc\x97\xfe\xe2\xb78v<\xa1\xa5v\xa3\xfa\xb3$aD\x9a\xe4h\xe1`9\ +\x15\xb4\xed\x93\xe2\x10*\x89\xf4\xeah%\xd1Ij\xe89\xb5\x8aY&\xa5!y\x1c\x93W\ +\xc7\xb8g\xe3V\xbe\xf7\xa3mf\x0b\xde\x85\x0f\xff\xda%\\p\xea\nr\xe0\xee\'4\ +\x1f\xf8\xe8\xd7\xb9c\xe3N\xa6D\x83\x9d\x9d\x18\xa7\xdeDE}\x03\xdeV\x1b\xcc\ +\xcfu\xf0EN\x85\xc8xa\xe9\x98^b<\xa7\x12\x02\x90Uswi\x85 \xc6)z\xe8\xbf\x8c\ +\x91\xb4{\xe8^Ng:f\xa4\t\x95\n\xccw\xc1\xae\x18*h\x1f\xd8\x17\xc3\x1c5\xfa~\ +\x8b\x8e3\xcc\x8c3\xc2n\xd9b\x1a\xd8\xba\x0b\xf2D\x81\xb0\xf1\xc7\xc7\xc1\ +\xb3\x11\xae\xc4\xadz$2\xa5\x9ft\xe9\xc4 ]I\x82E\x17\xdbd\x9dl\x9cd>\xa9\x93\ +\x8b\x808\xd5E(y\x8c\x90\xc6\x900i\xcf\xd0p\x15\x9f\xfd\xc4\x1fr\xf6\xc9M\ +\x02\xc0Is*2%\x99\xef\x11\xf7cF\xab\xa6\xb8\xcd\xcf\xe4|\xf4\x83\x97s\xd2\ +\xaaa\xaa(|2|K\x13\xf6\xbax\xb6\xc6"CG\xf3\x88$\xc6\xc7\xe0\xb4\xfd.\x04\xbe\ +\x8d\xabc\xec\xbcHG/Dm\xb96\xad\tG\x1a\x07T\x17\xa8\xe8\x08/\x8fq\x15\xb8z\ +\xbfy\xa0\xe9\xec\xd8\xe4\xa2h\\\x15\x1e_\x86\x8a\xaa\x06\x00\xb7,\x7f\x16\ +\xfbo<\x85Y\xfc\xdb\x02<\xcf\xc2\xca\xe1\x94\xe3j\x1c\xb2\xa8E\xda\x9d\xc1\ +\xf1\x00b\x1cG\x90G1Vc\x98N.\xd9\xd7W\xdc\xb6\xb6g\n\xc8\xc0\xb1P\x93\x92\ +\xa3\x80\xbb\x1ex\x88\x04\x8f\xcc\xa9\x98\xddCP\xc3r\x03\x84\xb0\xe9\xcc\xb5\ +\x19o59\xfb\xd4\xd5\x85\x9f\x98\x0b\xd2\'\xca\rq0\x046<\xbe\t\x85M\x14\xc5P\ +\x8a\xfdTF\x1a\x87x\xb6\xc3\xd1G\x1f\xcd\xb2\x11\xc3T\x9f\x07\xbaV\x85\x19\ +\xed3\x95\xd9\xf4\x91([\xa0\x85\xd9\xe9-\x9f\x80\xe6\xc8\x1896X\x1e:\xd1da\ +\x0e^\x83X\xf8h\xb7\xc1=\x0fm +>J\x14\xa7\x80\x047\xa0\xd7\x9ec\xcb\x96-H\ +\xdbbr\xdf4CC\xc3\xb4\xdbm\xaa\xd5*\xefx\xc7;\x00\x85\x1fx\x83\x890I\x12\xfa\ +\xfd>Q\x14\r&\xb8n\xb7;8\xdeeqY\xb2d\tg\x9f}\xf6\x0b\xca\xdc\xfey\xc3\xb6m\ +\xa6\xa7\xa7\xb1m\x9b\xf3\xce;\x8f\x89\x89\x89\xc1\xbf\x95\xd6)\xb0\x7fwT~\ +\x0f\x0c2\xc4\x0ff\x08!\xb8\xe3\x8e;\x06\x8a\xf4\xdd\xbbw\xd3j\xb5\xb0,\x8b\ +\xcb/\xbf|\xc0F\xabT*\xfb\xf5_\xc5\xae\x05 \x8a#\xd2\xcc,\x9e\x1c\xdbA\x15\ +\x14\xeaF\xbd\xc1\xeb_\xffz\x96.]:\xb0/Y\xd8\xba\xfa\xff\x86\x0eDKc\x16\xa4\ +\xa5\xc1@\x05\x03\'\xd0\xf2\xd4\xcc\xf7\xba4\xaa\x0e\x1a\x8d\x87\xf9\xc7\x97\ +/\xb7\xf9\xe6g>\xcc\xef\x7f\xfa\x9b\xdc\xb3\xab\xc2sm\x88\xfb]p=*\xbeK7Sd\ +\x16\xd0\xa8\xd1\xd5\t\x04fRC\xcc\xe3\xb8\x12[\xa6\xe8\xb8K\xa43\x92\xbc\x86\ +J\\n\xfa\xf1\xb5\xbc\xeb\x82\x0f\xe1{\xf3\x90JfzU\xbe\xfc\xc3G\xf8\xf4\xbf\ +\xde\x8e\xb7\xf8e\x84\xb5\nA\xbd\x8ek)\xda\xdb\xb7\x81\xedA\xd0\xc4\xa95I\ +\xdbs\x04A\xcaQ\xcb\x87\x11*\x05R\x12\xe1\x91bn\xaa\n\x9e\xc9\xce\xd61\xa8\ +\x1c!\x14B\x04\x07oj-\xc0mU\xc9\x01\xafn1\x9d\x99\x9c\xee\xac\x01\xf7\xac\ +\xd7\xdc~\xd7Zv\xec\x99\xa4\x1b\xa5\xccwC\xe6z1\xd3\xbd\x98\xf9$GKC\xe5[>>A{\ +\xb2\x0b5\xe3\xd3C\xdcA\'9\xd3\xd2\xc6\x93\x19\xa1\x8e\xd97\x1b\xb3\xbc\xe5\ +\x91 \x89\x8b\x06\xcf\x83\xeb\xf7\xa0\xedq\x84P\xe4Yf\xda\x16Qb\x84py\ni\xc8\ +1\xabWs\xea\xf1\x0eu\x1b\\\x85\xe9\xb5\xd9\x02\xa7\xee\x92\xe30\x9f\xe6(a\ +\xb1d\xd8\xc2%\x03\x153\xbbs\x1b\xade\xcb\xb1\x85GPq\xa0\x80g\x1dO\xe0\x08E\ +\x82\xb1\xcb\xd7\x944\xcd\x18G\xdadZ\x19\x16\x860\xedL\x85B\xaa\x1c\x17\x08\ +\xc8\xb0E\xbf\x10K\x9b\xf6\x9c%\xcb\xbflv\x1e\xaap\'\xd6\x82\x05\xf6\xe9\xa6\ +`\xc8\x81\xb5=\x83\xe7,$\x7f\x98\x91A\x0eu\xd7\xe1\xf2\xcb.\xe6O\xbf\xf4\x03\ +\xbcZ\x0biK\xe2p\x1e\xec*\xb9tA\xa5h\xaf\xc9\xf7o\xbe\x8b\xf3\xd6\\\x8c\xb6\ +\xcc\xdf\x10(4\x9a\xc9\xae\xc5}k\x1f\xc6\xf2\x1b$\x14\xf4^\xcb#\x8d\x15\xd2\ +\xf1\xb1m\x973\xcf8\x95V\x11\xd6\x19+\xc7\x90\r\x85\x11\x1e>\xbd\x1d:\xfd\ +\x10\xdf\x1f#U\xa6\xe8\xe5a\x17|\x97,I\xa9\xfb>y\xa6\xb9\xe9.\xcd\xbe\xdd\ +\xcf\xd2Nb\xc6\xc6GiU<\x92\xde<qg\x0e\xcbr\x90\x8eO"\x03bQ%d\x9c\xcc\xae0\ +\x177\r3M8 \xaa\xe8l\x96\x9eh\xb3s\xdf\x1e\x93\xd3\x02\xd4k\xce\xe0\xda\xdc\ +\xf8\xf8\x93\xcc\xcf\xf7\xb0jM2\x1d\x91)cx\xb8f\xcd\x1a\xea\xf5\x1a:M\x11\ +\x85\x1f\x95\x94\xf2\xa7\xa8\xae\xa5z\x1a\x18\xc4\xa8\x96b\xc3\x15+V\xb0z\ +\xf5\xea\x01;\xeb\x17\x1dCCC\xec\xda\xb5\x8b\x89\x89\t.\xbd\xf4R\xd2\xd4\x80\ +\xfcCCC\x07\xd8\x96\x97\xd8L\xb9\x13)\x19X\x07;fgg\x07\x1a\x91\xd2\x9eebb\ +\x82V\xab\xc5)\xa7\x9c2\x98\xf0\xb5\xd6\xf4\xfb\xfd\x03^;I\xf6;-\x97C\n\x89\ +\xc6\xbc\xd7f\xa3\xc9\x9a5k\xd8\xbd{\xf7@\x03S\xb6\x05\x7f\x19\x04\x84_\xc68\ +x\x0cd\xa1\x89\\i\x97\xb1\xa086\xaa5s\x0f)\x8d%\xc1+\x9c\xc2\xfd(\xe4\xef>\ +\xf6v\xfe\xe0\xcb\xeb\xb8m\xfd\x0e\xb6<\xdb\xc5\xb1,\xb4U!\x0b\x0b`\xad^5\ +\xcb\xf1z\xcdl\x07\xba\x1d\xd2$F\xd8)\x96\xeeS\x93\x16Y\xaa9\xf9\xb0C\xf9\ +\xab\xff\xf9j|\x07T/EV\xea\xe8\x04~x\xe3M\xd4k\x8b\x99\x9e\xcb\xc1o\x12\xce\ +\xf7\x08\xf39h\xd5@U!\x83\xb4;K\xb5e\xb3jq\xc0+N\x1b*\x84r\x9a\x8acv\x00P\ +\x12\x05,\x84(\xed\x1d\xcd\xce\xeb\xa7\xf3\x13\xccA([\x19\xb0\xc0Oh\x90%a\ +\x81\x90\x030>\x05\xa6\x8c\xb6\x8d\x1b\xeez\x8eo\\}\x13[v\xcd1;\x9f34\xb6\ +\x94(\x15\xe4\xc2!J!V\x01\xc2\x1b\xc5\x1am\x90\xe5\x90\xcc\xcf\xf1\xc4\xd6}\ +\x8c/Z\xca\xde\xb0G\xaf\x9f\x98\x9e}\xe0A\xae\xf0\xdd\x1a\xd9\x9c`z\xa6\x8d\ +\\\xb5\x08A\x8a\x8fG\x08<\xb3u/a\xea\x929\n\xb2\x1c\xbb\x12\x90\x8512OqEN\ +\xa5\x19\xf0\xf2S\x8e\xa7f\x1b\x93F\x1c\xc0\xd6\xc4\xfd.\x96g\xf4!\x9ec\r\ +\xc0\xe8$\xec\xe3\xe8\x94\xd6\xb2\xe5\x90k:Q\x97J\xadE\x9a\x85\xb8vI\x0b\x8d\ +\x91\x95\x00\x17\x10iF\xd2\x9dG\xb8Fek\x12\xb9\n\xcb{a4 \xaa0\x17\x1f0\xfb\ +\xf2\xdc\xb4\xba\n\xc39\xbd\xf0\xc2;\xe0z\x94\xa6`\x08\xb3\x94Q\x80\xa5E\xc1\ +B+\xd9h\x0c\xfeF?N\xa8\xba\xa6E\x89\x86_y\xcd\xa1\xfc\xd5\x17\xe7Q\xd1,Au\ +\x88x\xef\x1c\xc1\xea\xe5\x84Ss\x10T\xd1:\xe5\xfe\r\xcf0\x9f\x19\xba\xad+\ +\x8dI\xa2F\xb0o\x066=\xb7\x17]Yi\x8a\xa2%MAn\xcf\xe3\xd7*\x8c\rU8\xf7\xaccp\ +\x01\xa9#\xa4r\xd1R\xe2Xf\xb7\xb9\xf1\xe9=\xc4\x1a\x94\x94H\xdbAY>\xf4{\xc8j\ +\x8d<K\xe9\xe5\x92\xfb\xd6?\xc3\xfd\x0f<\x82\x17\xb8\xa4^@\x94\xc4\xa8\xb0\ +\x8f\xa3\x14\x81k\xe19\x0ei\xae\t3H\x84\x8bUk\x91\xd95c{Pm\x99\xcf\xaf-Hzd\ +\xb6M\x9a[\xcc\xc5\xe6P\x0e\x95v\x11\x1a\x1e\xdf\xb8\x814O\xf0\x1c\xc9\xa2Ec\ +\xec\xda\xbd\x87E\xad!N=\xf5tz\x9d>\xd5z\r\x90?%\xaa+\xed>\\\xd7\x1d\xd0e\ +\xcb\xf6Q\xd9BZ\xbcx1\xa3\xa3\xa3\x07]@z\xbd\x1e\xa3\xa3\xa3\xb8\xae\xcb\xa2\ +E\x8b\x00STJ\xbd\xc4Bo\xaa\x85\x96\xefp\xa0E\xfc/:6m\xda\x84\xe7y\xa4i\xca\ +\xf4\xf44\xabW\xaff\xfb\xf6\xed\x9cq\xc6\x19\x03\x16U\xf9\xb5Z\xad\x0eD\x89\ +\xa5\xf6#\x8cB\x02\xdf\xbc\x87~\xd8\xa7\x12T\x0ep\xd9=\xe2\x88#\xb8\xed\xb6\ +\xdb\x06\x82\xcd\x12\xd7y\xa9\x08\t\x0f\xde\x89\xab\x04\x05\n^\xa4\x14\xc5\\\ +S<\xca\xef\x1d)\xca(\n\\\tc\xcd\x80:\xf0\xe9\xf7\x9e\xc4\x87.\\\xce\x9a\xe1\ +\x0c\xbb\xbd\x97\xb4\xd7\xc3j4a\xa8\x06I\x07j\x1a\xc2i\x08{\xf8\x8e\xc3Dc\ +\x18\xdd\xeb\xe3\x92\xd0\xa4\xcdG.=\x81\x7f\xfa\x9dW\xb3\xd8\x87\xb9>\xcc9\ +\xc3t\x84\x83_\x87\xff\xf6\xeboFM?\xc7\n\xdf\xa6\xd6\xcb\xa1\xa7\xf1F\x1a\ +\xc6\x92\xbb\xd223xw\x07\xcb\x1b1\xe7\x9fy4\x93s\x10\xdb\xc6\xac\xb0\xc6\x81\ +\xbd\xfc\x14\x9b\x9c\xaa\xe1\xf3\xe3\x80\xce\x11:\x1b\x00\xb1\x0b{\xeb)\x94H\ +P\xd1\xddP\xa6\xcf\xafB\xc8c\xf2\xcc\xb4*:\xc0n\x05W\xdd\xfe\x0co\xf8\xe0\ +\x17\xf8\x93\xcf\x7f\x97\x07\x9fK\x90\xa3\xc7\x90\x0f\xadf:\x1b\xa2\'\x9aD\ +\xb2\n^\x15;p\x11\xae&\xd7\xf3\xa0\xe6@\xf6\x11\x81\xc7L\xa2@\x04\xe8^b\x14\ +\xe2\xb6\x84\\\x93Y\x01~e\x11\xfd\xf9\x0c\x1f\xa8\xd2\xa3Blb\xeb\xd3\x1e\xa9\ +\xd6\xc4Q\x82\xd7\x1a%\x8b"\xb0$2\ti\x05.q{\x9aSN\\Z\x8a\xa7\x99\xcfS\xb4\ +\xb0q\xabM\x84\xed\x13f\xf9\xc0\xd2\xdf\x06\x9c\xa0A\x1a\x8c\x10\x8b\x1a;\ +\xbb\x02\xb7\xd6*\x80\xf6\x00\xb4\x03\x89\x85\xe5\x0f\xa1\x95\xb9TjJS\xb34\ +\xb9V\xc4\xc2\x02Q\x05Y1\xc5A*2+#C\x17&\x9b6d\x1eh\xb7\xc8Na\xb0;\x94\xe4\ +\xd8d8\xaa\xb8\x98s\xdbl1D\x86\xc9\xac\xcdA\xaab\x82\xf7\x90\xcaGh\xe7\x00\ +\x1c\xcb\xf1\\\xa28\xc7r<\xaa\x02&l8\xf7\x98%,\xb2\xfb\xb4\xa7\xf6\x10,YA\ +\xd8O\xa11\x02\x89\xc6\xb6\xaaD4\xf9\xe7o=\x82r \xd7\x8a\x14A\x02|\xe3\xdbw\ +\xe2\xd5\'H\x85B\xb8\x89q\x81\x8e;\xf8C\x15\xecp\x8a\x97-\xf69\xfb8\xc8\xfa\ +\x11\xbe\xc8p\xf2y\xdc\\Q\x1a\xd9o\xdf;I\xe6\x04\xa4n`\xb0um\x94\xe1\xaa\x1f\ +"\x83\x1aa\xe8\xd0\x0b\x961\xe3\xafb\xd2Z\xcedZa^6I\x82\xc5\xf4+\xcb\x99\x93\ +K\xd8\x9b\x8f1)\x17\xd1\xf6\x16\xd1\xd5.m\x05\xd4+\xe6\x98d!d\x11\xcc\xcd\ +\x82\xb0q\xa4G\x14+f\xe7\xa0\x1a\x98\x90$#8\xcd\x98\x9d\xdbG\xbd\xe1\x13\'}\ +\xa2\xa8O\xadZ%Oa\xf1\xa2%T\xab\xc3\x90\x88\xfd\x0b\xc8b\x94\xf4\xdc\xb2\xc5\ +\xb2\x10\xb4\x06\xa3./\x01\xe0\xd5\xabW\xff\x02\x13\xce\x81CJI\x9a\xa6\xacZ\ +\xb5\xca\xdc\x85\xcf\xf3\x9d{~\xabga\xb1;\xd8\xe2\x01&\xeb\x04\xcc\xe7\xf6<\ +\x8f0\x0cq\x1c\x87\xe5\xcb\x97\x0fZt\x0b[u\xa5\x08\x10LK\xaf\xe2\x07\x03BG5\ +\xa8@!\xdct\x8b\xac\x95U+\x0f\xa53\xd7\xa6\x1aT\x88\xfa!\xa3\xc3#\xa4qB\xd8\ +\xeb\xbf\x04\xcc\xdc\x0f\xb6\x80\x94\x9f\\\xee\xff~!\xbd\xd5Z\xf0\x94\x9f\ +\xf5pQTU\xc2\x07_{"\x9f\xfe\xe8\xfb9\xff\x84U\x04\xe9\x1cy{\x0f\xc4s\x05\x7f\ +U\x98\x9c\x8a~\x1f\xcf\x92Ln\xdf\xcaX`\xb1\xbcn\xf1\x17\x1f\xfd W^\xb8\x86\ +\x97\x8d\x98\xe8\x91-\x93\xf0?\xfe\xeckl\x9a5\x93\xf3E/_\xc9[.9\x97\xee\xaeg\ +p\x15\x08i\x13w\xfa\xb0x1\xccw\xa0Y\xa51\xe4A4\xcd\xe2\xe1*CM3\xf1\xc798\xa4\ +\xf8\xda\xe0\x1de\x910\xf9\xdc\x12\x13\x82E\xd1_/\x86V\x07\xe8\r\x06\xadq@\ +\xe7\xfb\xd9SH\x89\xb6\xcd\xeb<\xb8\x15\xfe\xf4\x7f\xdd\xc3\x97\xbe\x7f\x0f\ +\xebw\xa7$\xd5\xe5\xc4\xde"\xb6\xef\xe9\x93:-\xe2\\\x12\')J\x80ekT\x1e\xa2\ +\xba{\xd1\xdd}XVHm\xc4\xa4?j\xadq\x83\xa0\xf0\xe7\xda\xafP\xe9\')I&\xc83\xab\ +8\xe69\x92\x18\xdb\x86~\xdcGI\xc0+\xe2,\xb3\x1ciIl\x9d\x91\xf6;T\x1c\xc1h\ +\xcb|\x86\x0c\xb0lI\x86"G\xa3\xd1\x04\xb6c\xce\xafN\xc8\xc2y\xb2L\x91(\xf3\ +\xdc\xe6\x90i[\xe4\x19\x88\xe2\xb8\xe8\x01\xe4]\\:Jb)@\x08\xb4(V e\xd4\xa2\ +\x05B\xaaA8\xa3Q\xdf{\x85\xd7\xfd~\xca\xaf\xb9\xce\xb4aV\xe9\xf2b^pI\xcb\xdc\ +\xec*D\xb1K\xd1\x96)fz\xbfmny\x8e\x1c\xbfB\x1c\xa7X\x18\xf6\xd8\xeb.8\x03;nS\ +\xf7]t\xae`f\xd6\xd0\x9b\x85D\x08\x8b\xb9P\xb3\xf6\xd1\xa7\x98\x89 \xb3$\t\ +\xb0\xaf\r\xcf\xec\x98\xa3\x9f\xda\xe4B\xa0\xa2\x1et\xe7\xc1R\xb8:D\xa4s\x9c\ +w\xfa\xb1\xb8\xc0P\xc5)\xda\xb2\x1a\xd2\x88\xe2\x93\xa3\x94"\xd3\n-E\xb1k\ +\x15\xb8K\x96\x82\xef\x93\xb4\xe7\xcdq\xd5\x12\x1dkRm\x81\xed\x80\xe5\x92[\ +\x1eJ\x06dV\x95\xd4\x0e\xc8\xac\x80\xdcv\xb0\xc7\xc7\xc1\xb5`v/\x10\x83\x93\ +\x83L\tZU\xaa\xbe p,,\x01I\xb4\x1f\x132\xbdh\xc5\xd4\xd4\x94a\xfe(\x8dUL\xc4\ +B\x08\x1a\xf5!s\xf4\xad\x17\x86a,\xb4\xf2)3:\xf2<g\xe9\xd2\xa5/\xe8\xff\xbf\ +\x94G\xb7\xdb\x1d\x08\x1f\xe38\x06L\x91\x18\x1e\x1e\xfe\x85\xfe^\xc9&+\x1f\ +\xa5x\xf0\xf9\x18\xc8\xcf\xd2\xdb\xbc\x18\xe3E\xf5\x02\x16\x80\'m:\xd3mN;\ +\xca\xe1\xef?\xfeZ>\xf8\xa6\xb3h\x89i\xe8N\xe1:6\xb4#H\x04\xde\xe2e\xb4;m\ +\x86\x1b\x16\'\xbfl\x11\xd7}\xf1\xc3\\|b\x8b\xe5K`>\x87\xeb\xee\x9b\xe1]\x1f\ +\xfd\x0b\xae\x7f\xe0i>\xf5\xf7\xdf!\x00\xa2\x08\xae\xbc\xe2\x15\xb4\xaa9nU\ +\x9bi\xa7:\x02s\x11\xd8\x19V<\x8dL\xfa\x9c\xf5\xf23\x98\x99\x9a\xa4"\nPUZ\ +\x85\xe1`^\xd8\xfa\xed/z\x07~\x00\xb1\xb0f\x80\xd0\x05U\xf4g=\xdf\xa4\x0e"<"\ +`g\x07\xber\xd5\xad\\w\xcfcl\xde\xd6&S5B\xa7\x85\xb2L&G\xa3U\x07;\xc7\xf3\ +\x04\x1e}\x9c\xbcC\xdd\x8a\x18\n$M7G\xf6f\xe8\xee\xdcL\xd3\x85\xac;\x83L\xfa\ +fe\xa9\x8a\x14\x0c\xa9\x11:G\xa1\x07\xb9\x189\x1e\n\xc7\xe4P$9ZX\xd8\xb6\x85\ +JB\xc8\x13\x1c[\x80\xca\x89{]\x02\xc7\xc6w\xcd"\xc0\x07<4\x0e\x19"\xeda\xeb\ +\x14\x9b\x14\x8b\x14T\x82mk\x1c;\'\x90\n\x87\xfd\xbb\x12\xdf\xd2\x88b\x1f&\ +\xb4\xa24o\xcc\x80LZd\xd2E"M\xaa\x9964Wc\xa5!\x8c[\x0bz?]Z\x17%Yg\xa0S\xecR\ +\xb4\xfa<z\xf5\x7ffH@\xab\xbc\xd8\x19+\xa40\xab\xbf\\\xc1%\x17\x1e\xcd\xd2%\ +\x13XR\xe1X\x05\x9bK\x00\xb6m\xce\xb0\xb0x\xfa\xb9\x9d\xac\xdb\xb8\x9b\xb8\ +\xc0/\x1eyJ\xf1\xcc\xb6m\xc4Z#\xad\xc0\xa0\xe2I\x8e\xe3\xd8\xa8,d\xb8\x19p\ +\xd1\x05\xab\x8a5W\xb1z\xb7<\xd2\\P\x10\xa7p\x84\xc2N3\xc3\xd0\xca5\xec\xddK\ +\xd2\x0f\xa1o\xce\xaf\xdb\xf0\xa9\x8b\x1e\xae\x9ea\xcc\x8b\x90\xe9<"\xeb"\ +\xb3.V\xde\xd9\xff\xc8:\xd8\xd9<\xd9\xfc>\x08g@\xf5\xb0\x9d\x18\x19M\xc1\xd4\ +\x16d\x7f\x0fYg\'y8\x85o%\xd8\xc2\x9c7w\xc0m\x96\xd8\x96\x83\xce\xf5\xbe\xa5\ +\xa4=\x00\x00 \x00IDAT~\x87\xd9<G\xa9\x8c\xa0b\x15x\xd4\xcf\x07\xa1\x17\xee>\ +\x16\x1a\x89\xce\xcd\xcd\xd1j\xb5~\xe1s\xf7R\x19e\xcb\xae\x0c\xb5*?o\xbd^\ +\xff\x85A\xfa\xb2\x88\xe4y>\xc8\x10I\xd3t\xf0\x15\x18d\xa1\xbc\xd8\xe3\xc55\ +\x93G\x92\xa5)\x13#Mj\x02F\x1d\xf8\xf0;\x8f\xe2#\xefz-K\xaa9\xc9\x96\x8d`i\ +\xec\x9aO\xdc\xd9\x87\xe7\xc4\\\xf9\xe6K\xf9\xfbO\xbe\t\xd1\x85*f2\xfa\xe7\ +\xef\xac\xe3\x8f?\xf7%\xf6\xa4\r\xe4\xe8\xcb\xb8\xe9\x9e\x8d<\xf0xL\xdd\x87U\ +\x87\xc0\xeb_w\x1e\xdd\xdeN\x9c\xa61\xe5c\xb6\xc7H3 \x9f\xdf\x8bC\xc6\xdb\ +\xdfr\x06\x0f\xad]\x0b\x98\x05\xa1kI\xfe\xc3\x89\xe9y\xd5\xe1y\xbc\x9e\xc1\ +\x0e\xac<\xb8Z\x14\x0c\x03\xe1\x90\x0bC+\xbe\xea\xbaG\xb9\xee\xf6\x07\xe8[\ +\xc3\xb0\xe8e\xd0\\B\x98\x9a\nf\x8d4\xe8v\xf6"\xe2\x19\xecd/V\x7f\x07Vw;\xb5\ +d\x92\xe5\xb5\x94\x93W\x8dp\xf1)\x87\xf3\xdasO\xe5\xc2W\x9cL\xe0ht\xd4+\xf0\ +\x1bez\xee(\x13\xb5)\x05\xc2\xf2\n\x96\x8dG\x8a\t\x8a\xb2<\x1f-\npY\x99\x82#\ +\xb5B\xeb\x1c\x8d\x01K\xb7?g\nAN\x89\xec\x14\xf1\x9d\xc2Pc\x89\x0b\xacJJ\xd0\ +\x8a<\t!K\x0b\xb8\xa4h\xea\xe8\x04\x938h\x8eR^\x14\x90D\x1a\x9d;\xd2\xc2*\ +\xf7x*7\x0f\xa1q\x04E\x01d\x7f+G\x15\xaf+\xf2"n\xf4g\xa5x\xff\xe7\x86\'-\xd2\ +4C+\x85\xeb\x1a\xd3\xc9,1\x1e\xa1\x17\x9e\x7f\x0eI\x7f\x1e\x9d\x85Pu\x91\x81\ +\x03*%\x8b"\xfcj\reU\xf8\xd1\xed\xeb\xe8h\xd3\x92\xbc\xe5\xde\xfb\x99\x8d\ +\x13R!\xb1\x9c\x8a\xb1x\xf7\xeaX\xb9"\xeb\xcfp\xd6i\xc7\xb2l\xccp\x05L\xaf\ +\xd3\xb0\xe7r\xcbB\x14\xb4\xe4\x86\xe7b\xab\x04;/|\xcb\xc6\x97 \xa4\r\xbeOu\ +\xa4\xc5P \xc8\xba\xbb\xa9\xa8)D\xe7Y\x9a\xbaC\x9d\x0eu\xe6\xa9\xd2\xa6\xae\ +\xe7\xa8\xeb\x0eu\xda\xd4u\x87\x86j\x13\xe4s4\x1b\x9aa9O#\x9ba\xb4\x922,f\ +\x19\x96\x1dto7uO\x19m\rf\'B\x92@\x9c\xa1\x94Dk\x81\xd0\x12\xa1K\x07\xda\x8c\ +86\xda\x9a\\\xed?\xaf\xff\xdeX\x98\x8dQ\x02\xd9`V\xe9333\x07y\xf6^\xfcQ\xab\ +\xd5\x0e\xc0X\xb4\xd6\x84aH\x1c\xc7\x07\x14\xcf\xff\xecXH7.w7\xa5\xb6\x04\ +\xf8)Q\xe2\x8b5^\\$F\x83my\x90jl[\x10`\x04\xb0\xef\xbfl5\x87\xad\xfa\x10\x9f\ +\xfb\xd2U\xac\x7fv\x0f\x157\xc1\xad(>\xf2\xa1\xf7s\xd1\x89f\x9an\xd5\xa0\x13\ +\xc1U7o\xe4\xcb\xdf\xff1\x93\xaaFu\xd1J\xa6\xda\x1d\x86\x86W\xf2\xf1O\x7f\ +\x89\xaf\xfc\xed\x87\x18o\xc1[\xdev\x02\xffr\xcbOp\xbc\x16\xb3{\x12Ds\x14\ +\x19v\xa8\xe8>\xaf<\xe7|,\t\x9d\xb9Y\xd3V\x93\x05\xe8\x9d\xa7\xc5\xe4dv\x15\ +\x0c~*\x87\xa9 \x83\xdb\xa7`\xf9 \xf2b\x07RF}\xe6\xd8\x96e\x96\xb5\xd2\xec>f\ +b\xb8\xf6\xf6\xb5\xc4\xce\x10:\xf3\x8c\xfdH\xda+\x0c\x9d\x12|K\xd0\xdb\xba\ +\x91\xa3\x8e\\\xc1\x99\xc7\x1f\xc9\tG\xae\xe0\xb4c\x17\xb3|\xd4\xbcj\x8e\x01\ +\xf8C`{\x1b6\xac\x7f\x9c\xbd\x93]\xb4\x06iK\xa2$\x82L\x0c\xa2]\xb5e\x17\x8c%\ +\x1b]\x98w/Z4\xca\xb3\xb3s\xa84G\xb86R\x98\xf8P\x1b\xb0]\x9fT\xc7\xdcy\xefC\ +\x9cv\xf8\xc9\xc6]F\xa5xv\xc9\x922\xad\x15\xac\x92@\xa1\xc8\x92\x08\xa4\x85c\ +I\x04Y\xd1o/\x13\x06m@\x1f\xc0\xd0\xcb\x04d\xd2\x06]d\xc3k\xb3\xfbAK\xc3\x13\ +\xd39y\x9a\x19]t\xc9\xcc(c\x00K\xbc)\xcf\x8cD\xff I\xd5\xb6\xa4\xd8\xf9\x80@\ +R\xf5\r\x05\xf6\xc2\xf3\x0f\xe3K\xdf\t\xd8\xdd\xef \\\xdb0\xf4\xe2\x18\xd2\ +\x04\x19T\xd1N\x95\x9f<\xb8\x99\xb7\xb7\r4v\xdb\xbaG\xa1V#\xd5\x1e\x96\xb6AZ\ +x\xaeE\xda\xd9M\xd3\xcex\xcdE\xa7\x19\xbcH\x14\x1fA\xba -\xb4m\xca\xb3\r\x8c\ +7+\xf8dDQ\x88\x10\x0eZK\xf4\xf4\x1chE/O\xe8\xcd\xefbi]s\xe6\xc9\x87\xd1oO\ +\x92ZF9.\xb5\xb9\x04-e(\xbcJh\xf2\xa23\xd8\r\xfb\xd4\xea\x15:\xb3sx\r\x87\ +\xa5c\xe3D\xed>J\xd9dV\x9d\xaa/\xf0H\xd0\xca\xc5w0\xa0\x97v8\xea\xf0c\xd8\ +\xb2m\x92\\X\x85\xc9\xa5\xc0\xb1$\xbbv\xed`\xf5\xaa\xa5X\xce\xcf\x9f>J\x10\ +\xbbT\xa7\xf7\xfb}l\xdb\xa6V\xab\xf1\xd4SO\x1d\xd4y{)\x8cV\xab5p\xe3-\xdd|\ +\x93$a\xdf\xbe}\x1c}\xf4\xd1/\xf8\xef\xfc\xac<w!\x04{\xf7\xee\x1dX\x97\x94\ +\x11\xbf\xe5\xf8\xff\x0b\x08\x90\x84\x19\xaeo\x13v\xe7\xc0\x914}\x93\xd0w\ +\xea\n\x8bo~\xe6m|\xe1\xab\xf7\xb1\xee\xb1\r\xfc\xc9\'\xde\xcfD\xbd\xc8\x7f\ +\xc60V\xbe|\xf5\x03|\xfe\xaa\xeb\x98\xd1\r\x96\x1d}\x12[\xefz\x10\x0eY\xc1|\ +\n\xbb\xfb>\x9f\xfd\xc2]\xfc_\x7fx6\xad*\xbc\xf3\xed\x17\xf1\x85\xaf]\x87\ +\xe7\xae\xc4\x8aS\xd2\xfe$#\x8d\n\xbf\xfa\xf6S\xf9\xf6\xbf=\xc0\x91\x87\x1d\ +\xbe\x7f\xd70\xd0\x0c\x94w\xfa\x81\x13\xd4~\x15\xfa\xcfba\x99I\xa8\xdc\xc3X\ +\xd2\x022\xf2L\x83g\xb8\xfb\x1b6\xf7y|\xeb\x1e\xfcek\x08#\xdfT\xcd(\x86\x86\ +\x03"AF\x1dN\\\xb3\x82\xdf|\xd7\xe5\x9cq\x94\xc7\x98G\x11@k\x9e&0,\xe4\x1e@\ +\x13\x84N\xcd2])\x84\xd6\xa6XY6Bhr\x95\r&n\xa3\xc1\xd6h\x04G\x1f\xb1\x92\xb5\ +O\xde[\xf4\xbcm\x84e\x91%\t\xc2r\x10\x05u\xf5\xee\x07\x1f\xe5}W\x9e\xcc\x98\ +\x05X\x15r\x15a\xc9\xc2\x1a$\xc5\xf8e)\r\x8e\x8d\xe5\x18\'*\x05\xe4Y\x8c[\ +\x86\x91\xe7\x8a\x05ev\xc0P\xcb\x05\xe4\xc2\xe8\xc2\x85\xb0\n\xfe\xad\x1e\ +\x1cS\x95C?I\x07\x18\x8ckyf\x92\x97\x800g*\xd3\x07\xab\xc7Q(\x9d\xe3X\x16\ +\x02M\x1e\'d*\xc3\n|\xd2\x14\x964\xe1\xc2W\x9c\xc1\xbf\xddx\x1fi\x16\x91k\ +\xcb\x08\xec,\x9fTkre\xb3m&\xe3\xfe\x8d\xa0\x02\xd8:9Km|%:\xb2\xc82\tJb+E\ +\x12v9n\xcdrN;\xaa\xb0\x9d\xb7\x8b\xe3"]r%\x11\xd6~\xdc\xf0\xd0%c\x8cT\x1c\ +\xb6G}\xa4[\'\xb7\\#\x0er$"\x9f\xa7\xde\xac\xf0\xcas\x8e\xe7O>p\x02J\x99\xf4\ +\xc9\xe7\xc1\x90\x03\xfc-\xc7,2\x1cJ\xc2A\x11\xd9\x8e1\x80\xf0<\xb3\x10\xe9%\ +0\xe6\x9aV\xa5\x00H\x14:\x0e\xd1\xdaBJ\x07\xa9%\xe4&>\xd5um\x1e\xdf\xb8\x81\ +\xd5\x87N\x14\xf6\x08\xff\xf1(\x0b\xc8\xcf\x9a\xec6o\xde|Pg\xef\xa50\x86\x87\ +\x87\x07\x16\xf6%y\xc0u]6o\xde\xccy\xe7\x9d\xf7\x82\xff\xce\xc2\xcc\xf4\x85\ +\xe3\xa9\xa7\x9e\x1a(\xedKQdY\x90_\n\x05\xe4Ena\x81\xed\x18V\x8d_o\xe0\xfa\ +\x1e\xdd\xde\x0c2KYQ\x81\xc5)\xfc\xc1\xe5g\xf0\xa3\xcf\xbd\x9f\xc3d\xcc(f"m\ +\xe7\xf0\xc9/\xae\xe5c_\xb8\x86iw1b\xd1\xa1l}\xfa9\x9c\xa3\x8e\xc3\xad\xd6H\ +\x94GO\x8cs\xf7\xc6}|\xef\xb6\x19\xa6\x80w]v4\xa7\x1f9\xc2\xd2 GM\xef\xc1\ +\x939g\x9f\xf9r\x16\xb7\xe0\xbe{\xeea\xc5\x8a\x15@\xa9;\xd3\xc6W\xbb\x98\xee\ +J\x1c\xe4\xc0Qr\x96\x8b\xa1\x17~U\x83\x9b\xda\xfcJ\xa2\n\xean\xa2\xe1\x8e\ +\xb5\x8f\xa0\xbc\x06a\x86\x91A[\n\x1cp\xf2.\r\xd9\xa7\x92L\xf1;\xef}#\x97\ +\x9c\xe0q\x88g\x94\xd8\xd5<\xc4K\xba4\xed\xd4(\xb13\x901\xcc\x84\xb0{r\xca\ +\xb4B2Hb\x058\x08\xd7E\x93\x93\x17\x1c}\x89Y\xa1z\xc5\xde\xe8\xf4\xe3VRu\x13\ +l\xdbxpd\x08\x90\x0e\x99\x15\x90X\x15BQa\xf3\xee9\xae\xbe\xf1Y&3cA\x12I\x9f\ +\x8c\x80\\V\x08\xb5G&\x1cB\xcbe\x1e\xc9\xa6\xc9\x98>\xa6\xa8M\xcd\xa7\x803\ +\x98\xe8\x7f\xd6(\xf5\x1aJ\x83\xc2.\x8e\xb7A#4\x92D\t\xfa\x99fO\xdf,\x16\x12\ +\x018\xc6nE\x0b\xcfP\xac-\xffg\x9e\x99\x17:\x04FDX\xc6\xb6Z\xb6Dhcs\xe2\x16\ +\x98\xc0\xe5\xaf9\x89\x91\x9aK\xc52\xdeX\xd2\x16\xb8\x81G\x92\xa4\xa4\xc2Cy\ +\x8b\xb8\xee\x8e\xf5\\s\xf3CP\x1f\xa2\xa7\x058E~\x8a\x12\xe88\xa6\xee\x08.\ +\xbb\xf0\xe5\x04@\xd56\x01_\x86\x92l\xd6oR\xec/ \x87\xaf\xa8\xb1|l\x18\x9f\ +\x1cO\x08\xc3\x0e\xc9\x81X\xa1{1\x9dN\x8f\xc7\x9f|\x82]s\xd0\x90P\xc3(\xf4\ +\x87(T\xfa\xecW\xfc\xd7\x8a\xdfU\x80\x99}\x1a\x9d\x99\xfbG\xe60\xec\x99\xdf{\ +`p\xb4\x18\x92\xa8(\xf4\xb6\x8d\xa849\xff\xbc\x0bp-\x0f[\x1a,\xc4*4\xff\x0f<\ +p\x1f\xbd\xfe<\xfb\xedb\xfe\x83c\\L\x8a%\x13\xa9\x04\xd37m\xda\xc4\xce\x9d;\ +\x7f\xe1s\xf7R\x19K\x96,\x19d\x9e\xe4yN\x9e\xe7T*\x15\x1ey\xe4\x11\xf6\xec\ +\xd9\xf3\x82\xfeFY<\x9eO\x87\x0e\xc3\x90\x07\x1f|p`\xa2\xb8\xd0\x82\xa5\xb4\ +\x84\x7f\xb1\xc7\x8b^@J\xf7\xd1~\x9a\x12\xab\x84z5\xc0\xb7\x15"\xcd\xf1$\x0c\ +\xf91V\x16\xb2\xa8\x12R#Dh\xf8\xb3\xbf\xf8*_\xf8\x97o\xb3\xe2\xf83\xc9\x82!\ +\xe20\x83f\x8bTi\x92\xed\xbb\xa9\rO\xd0\xcd\x03v\xcc)>\xf7\xc5\xff\xc3T\xc7\ +\xac\xde?\xf0\xce7\x11\xee\xdb\xce\xb0\'\x18\x0el\xde\xf8\x9a\x93yn7XN\x80\ +\xd6\x82T\x1b\xc3T\xa3:gP\x14\xc4O5\xae\x8aq\x00\x85Q\x0c\x8a\x07P\x18\xfaA\ +\x96\x1a5\x89\x90\xb6iSH\xd8\xba}\'\x13\xcbWA\x94\xc2\xdc\x1cR\x19\xb0\\\xc4\ +}\\\x9d`%\xf3\xbc\xe2X\xbfXJj\x84\xea\r\xc0c#\xca0}d\xdf\x85m;a>\xce\xa86G\ +\xb0\xdd:yV\x90\xa5\xa5 \r{\x08\xa9\xcd\xaa\x05p\x84\xc4%\xa5\x06\x1c\xbb\ +\x12Znf\xdaRJ\x99\xb7-l\xb4\x1d\x10\xe2\x11\xd9U\xda\x99\xcf\xdf|\xe5*\xee{2\ +a\x1e\x98\xca\x8c\xf2\xb9\x07\x84\x9e\xc5<\xd0\x06\xb6\xf6\xe1\xaa\x9b\xd7\ +\xf2\xe8.\xf3o^\xabF\x8cM?\xca\rsJ\x98\xfd\xd8B\xd2\x9ea\xe8e\xe4\x85\r\xc9\ +\x01\\pa\x91h\x8b~n\xb1}\n&#\xb3\x92\xee\x03\xf3Z\xd2\xc3\x144%\x0e\x1e\x05\ +\x11\x08\xc2~\x1f\x95\xa6 -\xdc\xc0#\x8f\x15~qo\x1e\xb5\x0c\x1a\x9e\xa6\xe6(\ +,\x15A\x16\x93\xe7)\xc4\t\xda\xa9"\xeb\xe3\xdc\xf5\xd0&\xee^\xfb(Ak1:\xca\ +\xc0\xf5\xc0rp\xa4\x80\xb8\xc7H\xc5\xe2\xbc3\x16\x17\x84\x04c\xa9\xa2\n,`0g\ +\xe4\xa6`\x8dVa\xc9H\x83\x8a\x05\xae\xd4\xa6\xba\xd8\x8e\xb9QF\xc6\xb1[\xa3\ +\xac{\xec\tn\xb9{3\xf3\x05n\xe1.x8\xfb\x8f\xe2\xe0Z\xdd=\x03\x7f\xf2\xf1?\ +\xe3\xaa\xabo\'\x07\xdaaa<P\xcc\xfd\x01\xd0\xf0 (l\xdc\xd3\xae\xc1\xb6\x96\ +\xacX\x89\xeb\xfa8\x96\r*G\x17\x89xO>\xb9\x91N\xa7C\x9e\xff|\x90x\xe1\xaa\ +\xbat\xe1\x8d\xa2\x88\x87\x1f~x\x90\x9f\xfe\xff\xe6\xd1h4\x18\x1e\x1e\x1e\ +\xb8\xfb\x96\x9a\x8f\xa7\x9f~\xfa\x05\xfb}\xfd\xac\xddG\xa9\xea\x7f\xe2\x89\ +\'\xe8\xf5z\xa4iz\x80\xcd\xfd\xbf\xb7c\xf9\xaf\x1e/j\x01\xd1\xc2\xb42\x94\ +\x04\xc7\xf1p\xa5[L\xbc\x19He&LRH\xfb\xa0S\xb4\x8a\xb9\xe5\xc6\x9bx\xf2\xc9\ +\x8d\xd8\xb6M\xa7\xd3)\xda\xf0\xcaP\xae\xd2\x0c\xc6\xc6\xe9v#\xf0j\xe0\xd7\ +\xc0\xab\xf2\xf9\x7f\xfaW\xa6\x15\x9c|\xd8\x10\x17\x9ew\x16\x9e\n9\xf5\xd8\ +\xd5\x1c\xb7\x12\x1eyd\x9a\xdd{&Y\xba|%\n\xb3!\xc8\xe2\xd8L\xaa\xff\x91\xd5\ +\xf3\x0b<w\x8e\xe3\x90\xe5&\xd2\xb6\\\xab\xf9\xbeon\x1e)!\xf0\tlA\xdc\xebb\ +\x0bp\x1c\x0ba\xd9\xcc\xc6\xa6C\x10iA.\xab`\x05\xe4\xca\x07\xa7\n\xd2&\xca!\ +\x14\xf0\xe5o\xfe\x10\x194\xd85\xdd#U\x8e\xd1A\xb8UtlZ1\xa0H\xe3h@\x9b\x8ezS\ +\x04(\x16\xd7\xe0M\xaf>\xc3`/:\xc3\xf2+f\x92\x8a2r\xb7Nf5\x88\xbcafU\x9dO\ +\xfd\xe3\xb7\xf8\xcb\xaf?\xc2\x93\x93\xa6`\xb4\x81\xb6\x80\x1d9<2\x05\x9f\ +\xfa\xf2}|\xe2\x8b\xdf\xe5w\xfe\xfc\x8b<\xbc\x0b\xa6\xb5)$V\xd0`\x10ld\x19\n\ +n\xa9\r\xb2\xb2\x14\x91\x9a\x14\xb8$\xc9L\xb1\xf4+f\'\x92C.lb\x19\xf0\xd5\ +\xab\xefc\xdb\x9c)\\s@\xaf\x10\xdc\xf5\x8b\xf7\x91=\xef\x12\xfe\xcf\xdfT\x92\ +\xa0RA:\xa5j^\xe3\xba\x12\x87\xfdN\xc8\xef~\xcb\xeb\xb0\xd2y|\x91\xe2;\x90\ +\xf7\xe6\xa9,^\x8c\xca\x15\xddT\x18M\x91\x08\x08\xfb\x19\xf6\xe88\xf4C\x98\ +\x9f\xc3\xd2\x11\xbeN8\xef\xf4\x13YR1\x97\xb4M\n*F\x06\xc1\x80\xf6\xde\x9b\ +\xef\xe0Y\xe6\xb8H\x05\xef~\xdb\x9bH\xe7g\xa9y\x96\x896\xa8\x04\x06\x1f\xeb\ +\xc7\xf8\xad\t\x08F\xf8\xe2\xb7\xbe\xcf\xf7o\xddD\xbf\xe8&&\x14\x14tL\xb1\ +\x8d\x8a\xe3\xb3y\x06\xfe\xe83_e\xd3T\xc6?|\xed:>\xfa\xd9\xef\xd1\xb1a\n\x93\ +KU\x1aBZ\x14\xf0R\x9e\xe2\xd4k \x89b^\xf7\xba\xd7\xb2w\xdfn@Q\x0b|\x84\xca\ +\xa9\xd7\x9b|\xe63\x9f\xc5\xb2\x1c\xe2\xf8\xa7M\xfdJf\x120\x08dZxn6m\xda\xc4\ +\xcd7\xdf\xfcKYA\x97\xb9\x1d\xa5hpaf\xf8\x7f\x95c\xed\xb9\xe7\x9e;\xb0&\x19\ +\x1b\x1bcff\x86\xe5\xcb\x97\xf3\xf9\xcf\x7f\x9e-[\xb6\x0c\x9e\xb7\x905\xb5\ +\xf0\xbd\x95\xe6\x8b\xe5\xf7`l\xe0\xbf\xf2\x95\xaf\x10\x86\xe1@y^\xd2y\xc30|\ +I\xec>\xe0%\x80\x81\xa8\xe2~/\x15\xdb\xa2\xe0\xfc\x98_\n\x10\x85\x99\xa1L\ +\xd1\xd8\x9c\x7f\xfe\xf9\x04\xa3\x87s\xd5\xb5\x0fr\xc3\xdd\xebQ\xd5:vc1\xda\ +\xf3\xc9\xa3\xc2\xab"\xa8\xe2)\x9b\xb4\x13\xd3\xed\xf6\xd8\xf0\xd8n\xee\xbc\ +\xf7\t\xdep\xd6Q\xbc\xef\xfdg\xb1\xfe\xa1Gy\xc7\xe5\xaf"O\xe1\xbe\xfb\xeeC\t\ +\x8f\xf1\xa5-\xac\x02+p\xbd*\xe8\x10\x15v\x91\x95\x1a\x14\xda\xd0\x9f*\'\x07\ +nG\x8a\x9f\x8b\x1e~\xf15\xc92<\xdb&\x0eC\xf0\xab&\xbc\xc7\x96\xa4\xfd\xb6iBg\ +)\xbd^\x17\xe9\xbb\xa4*\xa5\x13*jN\x83\xbb\xd6u8\xf7\xd4\x06\xe36L\x03\x81\ +\xb0\x91\x81m2\x9e$<\xb5\x13\xfe\xe6k\xd7\xf0\xd0\x13\xdb\xc9T\x9dj}\x04\xcb\ +\xaa\xd1\t\xe7\xa1\xdf\x03W!-\xb0m\x89.\xdcg\xd1P\xb3%\xd0g\xd8\xaf\xf1\xeaW\ +\x1c\xc3\xd7n\xb9\x1f\xc7\xab1\xf7\xe4FXq\x04\x95\xe1\t\xfa\x9b\x9e\x81f\x80\ +\xf6[\xc4\xbd\x94\xa7v\xcf\xb3\xed\x9a[\xb9\xfa\x86\xdbX\xd4j0:<B=h\xa2\xdd\ +\x80uOmc\xef\\\x9b\x91\x97\x9d\xc6\xb3s\x1d\xde\xff\x91\xcf\xf1/\x9f\xffm*Cf\ +\x1e\x9d\x08\\C\xd3\xc5L4Il\xf4\x80\xc3U\x1b\xb2\x1ei\xdc\xc7m\x8c\x90[\x1ey\ +\xbf_\xd8\x14\xb8\xd8\xfe\x10y*\xb9\xe6G\xb7\x13E}N?\xee0\x16\rU\xa8\xfa\x1e\ +\xd3\xd3\xd3l\xda\xfc4\xbd\xe9\xad|\xfec\xefc\xbf\xe2\xe6\x17\x1d\x12\x16^w\ +\x85\xb7\x9b\x8b\x99\x90_~\xc28\xa3U\x9b\xb8\x9b\xd1M{\xe0U\xe9w\xe7\x01\x8d\ +\xd6\x92\x04a\xcc\xcb\xb4&\x8b\x0b\xab`\x17|b|\x1dr\xdei\xc7\xe1\x02\xbeP\ +\x86\xc2 -\xc3\xc0\xb2$Z\xe74k>\xe8\xcc\xa4\x1cJ\x9bU\xcb\x03.\xbb\xe4<\xfe\ +\xf7u\xf7\xe3L\x1cK:\xbd\xdbh|\xb2\x8c\xee\x8eY\xa8\x8d\x90x)\xff\xf0\xf5\ +\x1f\xd0\xde}\x1a\x97\xbd\xf2LV.\xb3\x8d\xe7\x99cv\xb9\xd39\xfc\xe8\xb6\xdd\ +\\}\xc3m\xac\x7fb;\x9e\xdf\xa0\xd2\x1c\xe7\xab?\xb8\x9bM\x93!\xbf\xf3\x1boc\ +\xf5\x18\xac\xf2\xcc\xaeH\xe7f7h\xd9\x05\xe1\x03\x89[\xf7x\xf9Ygr\xd3m\xb7\ +\xb0e\xeb\xb38\x9eE\x1c\xf5\xa9\xd7\x03z\x8e\xe4w?\xf2{|\xfc\x13\x7f>02\xcc\ +\xb2\xec\x80\x08\xda2\x9c\xa9\xdb\xed\x0e\xa2]\x1f}\xf4Q\xbe\xfb\xdd\xef\xd2\ +\xef\xf7\xd1Z\xbfd\xf4\x0c\xbf\xe8\x98\x9b\x9b\xe3\xd2K/\xe5\xdb\xdf\xfe\xf6\ + \x93\xbd\xcc\xf0\xf0}\x9fk\xae\xb9\x867\xbd\xe9M\xac\\\xb9\x12\xa5\xd4 \xe4\ +j\xe1\xe7\xaeT*\x83\xdd\x8b\xef\xfbDQ\xc4\xcd7\xdf\xcc\xc3\x0f?|@\x8e:0\xf0\ +\xd6*\xdbe/\xf6\xf1{\xd1\x0bHy\xcb\xda\x85L\xaf0B*\xa87&\xbc)\xb3 \xc3\xf4\ +\xbd\x1b\x1e\\t\xcaJ\x96\x04U\x8e\x1c\xf7\xf9\xc6Mw\xa0\xdc&\xdb\xc39Pu\x18\ +\x9a\x80\xf9\x0e\xf1\xec\x14\x8b\xc6}\xba\x93\x11\xb1\x05\x9f\xff\xca\xb5\ +\x1c}\xfcQ\xac\x1e\x85\xf7|\xe0]\x9cz8\xeci\xc3#\xeb\xd6\x11*\x9b\xdd{s\x8eX\ +bQ\x1b\xec\xfde\xd1\xa8\x97/l\xb71\xe0\xcf\x9b\xff[\x82\xc5J)\x04\x02\xdfu\ +\x083\x85\xedHN9\xe6p\xae\xbd\xeb1\xdc\xe6\x04Xu\xfaS\x19~s\x94(t\xe9\xf541\ +\x0e\x9f\xfe\xd25\xdc\xfd\xf01\xbc\xe3\xf2S8d\xc2P\x96\xdb1<\xb9\x19\xee\x7f\ +\xf01\xfe\xef\xf6\xde=^\xaf\xaa>\xf0\xfe\xee\xb5o\xcf\xf5\xdcON\x12B\x12!\ +\x01\x02\x89\x04\x10\n\xcc\x0b\xa8\xa5(W\xc1)h\xed\x14\xd1\xd6\xa9\xb5\xce\ +\x0c\xf3\xfavF{\x1bk-\xe3\xdbw\xea\xa5/N;~\xc6Z[\xdb*/\xbdX\x10\xc4(UD\x054\ +\x02\x12/\x90\x90\x0b\xc4\x84\xe4$\xe7\xf2\xdc\xf7u\xbd\x7f\xac\xbd\xf6s\xc9\ +\xc9\x85\x13\xe7\x93dX\xdf\xcf\xe7|\xcem?{\xaf\xbd\x9eg\xff~k\xfd\xae\xdf\ +\xdd\xbc\x85\xad{\xa7i\xb9\x05\x1c\xbfB\xab\x15b\x8bPI\x10\xdb\x05/!\x0e"\ +\x84+\xb2]P66aA\xd4\xc4sJlX%\xb8\xfd\x96k\xf8\xf8\xdf>\xc8\xd0\xea\xa5\xb4\ +\x92\x0e\xad\xad?\x86\xa9\xd3 \xe8d\xed\x8a+H\xbfH\x9c\xb6\x99\xee4\xa8\x1d\ +\x88\xd8S\xab\x11\x87s\xcc6:\xc8\x10\xac\xa5K\x99Ml\x82F\x83\xd1\xea(w\xfe\ +\xee_q\xf7\x1f\xdd\xce\xd9\xc3j5\xec\xc7\x10\xb5\xda\x94\xab\x1e\x0e\x12\xd7\ +\xb2Y\xb9\xd4b\xbc\xec\x91\x14|\xa6;M\xd5\x13\xc3\xf6\x95]\x0eI\x1c\xc4\xc4\ +\xa9`hj5\x8f\xff\xf0\x05\x1e}\xfc\xfbxV\xccp\xb9\xacJ\\{.\xe7\x9d^>N\xd5\xa1\ +Mg\xa8wKG\xd2\x91bI\x81c\xa9v5\xab&\xe05\x1b\xce\xe4\xa5G\x9f\xa6P\xa8\xd0\ +\x89C\xb5\xb0\xb1\x1de\xf3L\x048EUm\xa0\x15\x82m\xe3;\x11vPc\xe5\x92\x02\xd7\ +\\^ nA\xa1$\xba\xd7\xb5$R\xa6$I\xa8\xfc\x80Rb\xdb\x02a\xa92\xfe\xff\xe6\xd6_\ +\xe0\x1bO>\xcb\xdex\x9ezQ\xa8\x90g\xcf\x85\xf2\x18\xd4\xa690\xd3!\x19\x9a\ +\xe0O\xefy\x84\xbf\xfc\xe27X\xb5j%k\xcf^\x03\x8e\xc7s;v\xb3c\xcf\x0c\x8d\xc8\ +\xa1\x169H\x963z\xc6\xab\xd9?\xbd\x87\xd2\x19\x93ly\xa9\xc5/\xfe\xda\x87\xf8\ +O\xbfq+o\xbdp\x92\xb3&<\x8a\x85\x02\xc2\xca\x84\x91-\x88;\x11\x8e\xef"\x85\ +\xe4\xda\xeb\xae\xe1\x0b_\xf8\x02a\x1c051\xca\xbe\xe9\x97h5m\xe28\xe1\xbf\ +\xfc\x97?\xe0\xc6\x1bo\xe4\x8a+\xae\xc0u\xdd<\xd3\xbcT*\xe5\x8a\xc4q\x1c\\\ +\xd7\xe5\x9e{\xee\xe1\xc1\x07\x1f\xc4\xb2,\xea\xf5z_\x81\xc1S\x15\xdd\x07\ +\xe5\x97\x7f\xf9\x97\xf9\xe8G?\xca\xf2\xe5\xcb\x01\x95q?77\xc7\x03\x0f<\x00\ +\xc0u\xd7]\xc7Yg\x9dE\xb1X\xa4\x9dU0.\x97\xcb4\x1a\r\xca\xe5r^\xa2\xe4\xa5\ +\x97^\xe2\xef\xff\xfe\xef\xd9\xb4iS^[L\xfb;\xe28\xce[\xed\xc2\xc9\x91Lx\xc2\ +\x15Hwe/\xbb\x02\x1b\x1b\x84 \xb1P\xdd\xf6\xe86q\xd2\x99\xc2\xff\xc7y\x93\ +\xfc\xdcy7p\xfa\xd2a>\xf6\xf9\xafA\xa3\x89\x7f\xe6%\x04\xf5:\x04-\xdc\xf1\ +\x02\xcd\xc6~\x8a\xc52al\xd3n\x16\xf9\xc0\x07\xff\x89\xbf\xf9\x93\x9by\xc35e\ +<\xe0\x91\xaf|\x13i\xd9X^\x85\xaf|\xfdQ.{\xf5U$\x0e\xb4CI\xd1\xb5\x11\xa52\ +\x0b\xec;z\x18\xa8\xc34\x10\xf8+\x01\xdf\xf3\x89\xc2\x16\xaek#\xc2\x10\xc7-p\ +\xd5%k\x99\xfck\xc1\xc1\xfaA\xd2\x92\x0f\x96E\xab\x1d(\xa7\xaa7\x8cW-\xb2{\ +\xdb\x8f\xf8r\xfc<\xff\xf8\xe5\xef\xb0t|\x08\x0b\x15\xc6\xe7\x95\xaa\xbc\xf8\ +\xd3}\xd4\xe7\x9b\x8c\xad\\\x81\x8c\x1bXB"[-b\xdbA\x94<R\x0b\x84m\x91\x92\ +\xa8\xe2u\x9e\x9b\x85\xc2\n\xb5:n7\x91I\r\xbf0\xc2\xaf\xdd\xb4\x82g~|&\xf7m\ +\xfa6KWo`o\xd9A\xbe\xf4\x02\xfe\xea3\t\x1a\xf3`\xfb\xa4VJd[\xe0\xdbD\xc2\xa6\ +\xe3\xf8\xa4\x08d)\xa2\xbcb\x8af\xbdA\xd0\xa8#\xaaS\xcc\x07\xb3\x04{\xe6\xf8\ +\xfd\x0f\x7f\x8e\x8f\xfe\xee\xbfa\xed0\x08\xc7U\xe1\xda@\xd9\xb7\tPmVN\x9b\ +\xa8\xb0g\xba\x8e\x10.\xa9\xeb(\xefn\x1a\xd2\xad{eQ\xab\x85X\x0ex\xee(\x85\ +\x92OG\x08\xf6\x1d\xdc\x83\xe3\xc0\xea\xe5\xfeq\xa4\x11v#\xc2\xd4\xeeW\xc7\ +\xcdi\x7fW\xb6\x0b\xc9~\xbd\xfe\xe7\xaf\xe4_\x1e{\x12\xe1\xc0\\\xad\x81X2N\ +\x1aHesJS\x15\x91\xd6Qy,\x96\r\x04\rdp\x90+/\xbdB%\x05\xca\xac\x82\xb0\x95=r\ +\x8eM\x9c\xa4\xb8\xae\xa3\xa2\xcb\x12\xd4{c\xa9\\\x8c\xb3O\x87w\xbf\xfdf~\ +\xffc\x9fatRPK\x03\xc2\xf9\x04\xfc\xb22\xcf\n\x9bzlQ\x1c9\x93\x83a\x8b\xb9\ +\xbd\x01?\x9e}\x8e\x18\x8bz+ I]p*X\xd5%P\xb0xiO\x1dB\x9b\x96\')\x95\xca\x14&\ +O\xe3\xbf\x7f\xea\xd3\xac\xbe\xe3\xf5\xac\xfe\x85\x8b)\x17K\xc4i\x82-\\\xe5\ +\xces\x04\x08\x18\x1d\xafr\xd5UW\xf0\xedo?\xc2\xd3O?M\xb9X\xa0\xe0\x15ISh\ +\xd4;\x1c<\xf0\x1c\x9f\xfe\xf4\xa7y\xf0\xc1\x07Y\xbbv-\x17\\p\x01g\x9ey&\xb5\ +Z\x8d}\xfb\xf6Q\xab\xd5x\xf6\xd9g\xb9\xf7\xde{)\x97\xcb,[\xb6\x8c\xe7\x9e{\ +\x8eB\xa1\xc0\xe4\xe4d^\n\xe4TEGD]t\xd1E\\\x7f\xfd\xf5<\xf8\xe0\x83\xacX\xb1\ +\x82\x83\x07\x0f\xe6m~\xbf\xf9\xcdo\xf2\xdd\xef~\x97\xd7\xbe\xf6\xb5\\{\xed\ +\xb5y\xc0\x0e\xa8|\x18}\x8e/~\xf1\x8b<\xfc\xf0\xc3\x84a\xc8\xf8\xf88sss\x04A\ +@\xab\xd5\xca\x9be\t!\x08\x82\xe0\xa4H"\x84\x13\xac@\xba\xe1\x87i\x16\x92\ +\xe3\x91\x97\x9d\xb0t\t\re\xcf\x055X\x1fpc\xb0\xec6\x1ep\xfb\xf5W\x80]\xe1O\ +\xbe\xf8\x04\xdbv\xfd\x08\x96\xaeQ\xe59\xe6_\xc0\xb1c\x82\xc8\xc1q&\xf1\n\ +\x13\xfc\xe4\xa9\xad|\xe1\x81\x03\xfc\xfau\x13\xcc\x1c\x88x\xf8\xab\x9b\x10\ +\xde\x18\x95\xa1I\x9e\xfa\xc1OhEW\x91:\xca\x95Rt\xac,s\x1c\x16V"\x83E\xfc2\ +\x83vV\xe0^\xfb\xe0Ue\x08\x0bd\x8c\xef;\x04\xcd\x98\x95\xe3\x0e\xaf\xbbh\x1d\ +\x7f\xf7\xd5\xefc\xdbe\x9c\xa1Q\xe2N\x0b\xbc\xa22L;C0\xb6\x8a\xa6,P\x1c\xaf\ +\xb0\xeb\xe0\xb4j\xcf ;\xc8N\x8d\xf2\xd4RF\'W3\xb3\xf39\xecI\x1f\xd9\xa9\xe1\ +\x15\x96Q\xf4\xcat\x92\x80 \x0eH\x9d6\x8e\x0b\xadz[\xe5\x81\xe8\x1d\x12.\x14\ +\xcbX\x96\x8b\x83\x8a\xde\xf9\x9d_}=\xb2v\x80\x87\x9fx\x9a\xf1\xc9\xb54\xc7\ +\xca\xb4\x0f\xeeQa\xcc\x9e\r\xad\x0eI\xdc\x01G\x90X\x92\xa0\x19\x82]@x\xc34\ +\x0f\xcc*\xe3ye\x14dH\x1aAix\x8c\x9d/N\xf3{\x7f\xf8)~\xef7\xde\xca\x85g\x0e)\ +\x93^\xaa\xdc!\x0e0T\x84[o\xbe\x9eG\xff\xe4\x0b\x0cM\x8d\xe1\x14}f\xeb\xed\ +\xac\'I\x05J>\x84\x02\xbbP\xc4\xb1%q\xa7\xc9LG\xd5\xdeM\xec*\xa5\x91q\x86\ +\xc6\'\x8fK\x81@7\xe4\xd5F7\xc4\xcd\x94\x88L\xb1\x10\xf8\x02\xa2\x04.{u\x81\ +\x8d\xe7\x9d\xcd#?\xdc\r\xb6\xa7\x94\x86\xeeD\x15\xc6\x08J\xa4\x96\x03\x96\ +\x85\x88\x03\xd2`\x8e\xb1\n\\\xff\xf3\xafQ\xe5\xe8\x0b\xfa\x03a\x83P\xbe\x9b\ +T\xa8m\xa1L$\x16\x02)SRK\x90$1.\x0e\xb7]\xbd\x82\xf9\xdaM\xfc\xf9_\xff#\xe1l\ +\xc8\x923\xd63\xd3\xaa\x13S\xc0\xabN\x10\xee?H\xdd/\xe1W\xc6\x89\xd2\x0e\xb5\ +\xf9\x19\xb0R\xec\xd1)\x84(\x936"dG\xa8\x0e`A\x02\xc3U\xbct\x9e\xd6\xfe\xdd\ +\x8c\x8d\xda\x9c\xbf\xe1\\\xce:\xf3\x0cFG\xc6\x91\x08\x9a\xed\x16\xb6+\xf1=\ +\x8fTX\xd9g7\xc5/:\xbc\xe5\xd6\x7f\x8d \xe5\x89\'\xbeG\xb9:\xcc\xd2e+\xd8\ +\xb3g\x0fCC#\x84a\xc8\xb6m\xdb\xd8\xbau+\xf7\xddw\x1f\x96eQ(\x14(\x95JLOOS(\ +\x14X\xb9r%\xfb\xf6\xedc\xc7\x8e\x1d\x0c\x0f\x0fS,\x16\xb9\xfa\xea\xab\xb9\ +\xf7\xde{\x8f\xf3\x1d<\xb1\xe8\xf0\xddj\xb5\xcaM7\xdd\xc4\xbe}\xfb\xd8\xbcy3\ +cccT*\x15\x9a\xcd&ccc$I\xc2\xa6M\x9b\xf8\xf2\x97\xbf\xcc\xc4\xc4\x04SSS\xf8\ +\xbe\xcf\xd0\xd0\x10?\xfa\xd1\x8f\x98\x9e\x9e\xce+\x07\xeb\x9d\xc6\xe4\xe4$\ +\xcb\x97/g\xf3\xe6\xcd\xb9\x89K7\x93:\x19Bx\xe1\x84Ga\xa9\xfe\x1a\x99\xad\ +\x04\xe5F\xf4U\xc66J\xb18(\xf3M\x19eN\xf0$\xb86`\xa5\xc4\xed:%\x91r\xc7\x9b.\ +\xe0\xce\xdboa\xf9\xb8\x80\xb9\x17\xa0u\x00hA\xd9\xa7-*Db\x8c\xd9Y\xc9\x92\ +\x15gr\xdf\x17\xff\x8e\x14\x18\x9bp9\xeb\xdcW3_o\xd1\x8e`\xba\xd6\xe4\x07?\ +\xec\x90\x00n\xd1\xca"\xb1\xb2]\xd1\x11\xc6\xdfk\x04\xe9\x86\xa1\xe6\xc68\ +\x92T\x85*\xb7\xa7\xa7AF\x0c\x95\x1c*\x02\xde\xf9\xd6\x1b\xb8\xfc\xfc\xb3\ +\x90\xad\x03xI\x0b\x926\xbe# Ih\xd5Z`\x97\x89:\x92f\xc7\xc2-\x8eQ\x19^\xc2\ +\xd8\xe4\n\xbc\xb1I\x9a\xf5&\xb3ss\x14\x96Nr\xeb\xcd\xd71V\xf5\xf1d\x8c\x1d\ +\x07\x888\xc2s]\x88\x02\xd28\xc6\xf5=\x84\xed\x92\xb5M\'\x0f\xaf\xb5\x04\x0e\ +)i\xad\xcdy\x13p\xf7\x1f\xdc\xc6\xcdWn\xc4\xefL3b\x87\xb0\xffE\x90\x01\xae#\ +\xc0s\xa0Z\xa52\xbe\x84\xca\xd8R(V\xb3$8\x01\xa9dl|\x1c\xa2\x90\xb4\xd5b\xf5\ +\xda\xb39\xb0m\x17\x89\x14l\xdf\xf1"[w\xbcH,\xc1\xf5J\xc4Q\xa4\\E\xa9\n\x93}\ +\xd3/\x9c\xc3\xeb\xaf\xf89\x9c\xa4\xc9\x81\xed?\xc1IZ\x0c\x8f\r\xab\xf2\'\ +\xcd:\x96\xeb\x92\xb4Z\x04\x11$\x89Gb\x97\xf1+\x13\xd8CKiE\x0e;\x7f\xba\x9f\ +\xe3}\x8c\xd2\x9e\xaf\xfc\x9d\xd4\xa1\xdc2VM\xb1B\xf5\xe9\xbc\xfe\x9a\xd7c\ +\xcb\x88R\xd1\'m\xd6U\xa9\x1b\x11\x02\x91:>u\xb0l\x1f+\x8a\xf0E\xc8\xc5\x17\ +\x9c\xc5\xba\xd5\xca\x0fn\xe9\x0cP\x84\xb2z\x01\xa9\xae/e\xbb\xa4\x96\xa0\ +\xd1\xee \x81j\xd9!\xc9\x1a\x8a\xbd\xfb\xcd\xe7\xf3oo\xfdy6\xae\x1e\xc1oO\ +\xe3\xb4\xe7\xa1Q\'j\x8502\x05\xc51\x82\x83-\xc2\xf9\x00ob9\xde\xf8R\x92\xd9\ +\x1a\xe9\xfe\x03\xaaD\x7f\x18\x80\'\x18\x9e\x1a\x87\xb9\xfd\x0c\xc9\x0e\xa7\ +\r\xbb\xac(\xa5|\xe8}\xef\xe4\xb2\x0b\xcf\x01\x04i\n\xaeW\xc0\xf3<R \x94\xaa\ +\xbb\xc9\\m\x16HX\xb7a\x1d\xef|\xc7\xdb\xb9\xee\xba\xeb(\x16\xcb\xfc\xf4\xc5\ +\x97\x18\x1d\x1d\xcfk6\xe9\x96\xb2K\x96,\xa1X,\xe6\x1d\tW\xacX\x91\xe7E\x8c\ +\x8d\x8dQ,\x16\x19\x1a\x1a\xe2\x9d\xef|\'\xa7\x9f~\xfaq\xbe{\'\x9e\xde$\xc9\ +\xd1\xd1Q\xde\xf3\x9e\xf7p\xfe\xf9\xe7S\xab\xd5\xd8\xbbw/a\x18\xe6%\xdfGFF\ +\x18\x19\x19!\x8ecv\xef\xde\xcd\x96-[\xf8\xfa\xd7\xbf\x8em\xdby\xe9\x93\x91\ +\x91\x11\x86\x86\x86\xe8t:\xc4q\xcc-\xb7\xdc\xc2\xf0\xf00i\x16\x01gY\x16\xae\ +\xebr\xedu\xd7[\'\x83\x129\xe1\nD9W\x95\x9d*\x11Y\x86\xb2\xa5\xfe\xe7\xd1\ +\xa2\x94NS\xa6F9I\xa9\x84 ;\xea\xf9\x0e)Ci\x02\x84 \x9c\x87\xdb^\xbb\x84O\ +\xfc\xee\xafr\xda(\xd0>\xc0\xd8\xea\xd3h7\x9a\xa4\xe5e4\xe7m\x86\xcbKhN\x1f\ +\xe0\xdc3&9p\xb0I\x02\xdcx\xdb/r\xd6\xfa\x8b\x98\xad\xb5I\xb1\xb9\xffK\x0f\ +\xd1\x8a\xd5\xc3\x9e\xa4Y\xd6\xf5\xe1\xf4\xc7B\xef\x9dT!\xa9\xda\x10\xa2\x8a\ +\xe3\xa9\x03mK\xd7q\n(\tX\xbf\n~\xfdW~\x91\x8b\xcf]\x85\x17\xce`\xb5gq\xe2\ +\x1a\xd8\x12WH\x1c\xd7\xc5\x16\x0ev\n\xb6L\x99\xdf\xbb\x97\xc6\xccA\xc6\xca>\ +$m\x8aV\x87k\xae\xbc\x84w\xbcy\x05\xe3%\x17\xd9\xae\x13\xb5\xea\x10u\x18*\ +\xf9J\xa0\xa7)\x85B\x81 \x91D)$6\x80\xea-/\xb1\x89\xa3\x0e\xa3\x15(E\x11c\ +\xc0\xc7~\xef&\xfe\xc3;n\xc5\x0b\xe7X\xbba\r\x15_`\xc5MH\x03h\xb7hLO\xd3\xd8\ +\xbf_E\xbc\t\x81m\t\xc6\xc6F\x98\xdf\xb7\x07\x9au\xbc\xa2\xcf\xcem[Y\xb1\xee\ +\\\xe28\xe6\xdd\xefy\x0f\xe7o<\x0f\xcbRn\x19\xc7\xf5I\x13I\x18&4\x1a*\t\xf0\ +\xdf\xbe\xe3\x8d\\r\xc1:\\/E$m\nv\xacjlE\x1dd\x1c)!h\xb9*\xda"\x94\x04\xb1E"\ +\x1d\xa4\xf0\x95c\xf9\xb8>}\xa2o\xa7\xa8\xde\xa9\xec\x91\xb0Rt\xb1\xcc\xa2\ +\xabL\xa7\x17_0\xc4\xba\xb3\xd6fY\xeb)\xa4\x11B\x86 #\xac8\x85X\xe2\n\x17\ +\x19\x85L\x0c\x97\xb9\xe9\xba\xd7\xa9]\xb3H\xbb\xe9\x12\x12\x924[\xdd#\x88\ +\x92\x14p\x88S\x81S(\xe6\x87\xf9B\xcd\x8f\x9d\xc0\x9do\xbb\x82\xdf\xffw\xbf\ +\xc2\xda\xc9\x02\xa3n\xcc\x92\xb1*v\x92\xc0|\r\x0b\x1bkt\x0c\x86\x86\x88\xda\ +-\xc2f\x1d\xab\xe8a\x97\x1d\x9cN\r\xea\xd3,-@\xfb\xc5\x1fSjOS\xac\xbd\xc0\ +\xdb^w>\x0f\xfd\xf9or\xce\xb8\xda\xd1\'QJ\x98(\xff\x9c\x9e\x07\xdbq\x08\t\ +\x19\x1e\x1a\x02R\xa2N\x8b\x15\xabW\xf1\xaew\xbd\x8b\xeb\xaf\xbb\x81\xa1\xea\ +\x08\xb33\xf3\xaax\x80\x94t:\x1d\xe6\xe6\xe6\xa8\xd5jy\xd64\xc0\xee\xdd\xbbI\ +\x92\x84\xd5\xabW3==\x8d\x10\x82\xdbo\xbf\x9dK/\xbd47\xcb\x9c\xca\xe8\xfc\ +\x968\x8e\xa9T*LNN\xf2[\xbf\xf5[\xac[\xb7\x8ee\xcb\x96\xe5\xcd\xa5z\xab\xf5\ +\xee\xdb\xb7\x8f \x08\xf2\xf0\xdfZ\xadF\x18\x86\xb8\xae\xcb\xdc\xdc\x1c\x8dF\ +\x83\x8b/\xbe\x98\xdf\xfe\xed\xdff\xc3\x86\ry\xd8\xae6w\x01|\xfb[\x8f\xca\ +\x93\xc1\x8cu\xc2} \x9a\\\xe0f\xbf\xab\xb7%U\xcfs\x1a\x81\x0c\xb0\x9c"\xae\ +\x0e\xfdE\xe5\x008\xc4T\xca6RZ\xfc\xab3\xe13\x7f\xf4v~\xef\xa3\xf7\xf2\xf8\ +\xa3\x0fS]\xf7\xaf\xa8\xcf\x80=\xbc\x9c0h\xe3\xda\xb0\xe6\x8c%\xac\x18/\xf3\ +\xec\x9e6S\xcb\x8b\xbc\xf5\x97\xde\xc0\xb3\xff\xf5\x7fR\xf0\x1c\xbe\xf3\xf8c\ +\x04\xf1\x9bH\x1ch\xd4C\xc6G\xfc\xbe1vMnq&\\,U\xb8\x14\xf2\\\x87\xc1c\x1d\ +\xc7"j\x07x\xe3S\xca\xb8m\xc78\xaeO\xd0\x82\xab\xcfwx\xd5\xea7\xf1\xc7\x7fz?\ +On\xd9J\xa3\xb1\x0bd\x91\x12\t\xad@u\xa2\xb3\x92\x08a\xc5,[V%\xe8\xcc\xb1o\ +\xebOXw\xd6j\xee\xb8\xe3\x97x\xe3\xe5*)K\xce\xbc\xc0\xa8]\xa0P\x10\xcc5\xea\ +\x94:E\x9c\xc6n\xc6&\x8a\x14\xa2\x18\xd9\x9cF\xc8%8\xae\x9aY)c@\xe0\xb86\xa4\ +m\xa2\xd6\x1c\xe5\xe1e\x00\xbc\xfd\xc6e\\y\xd5\xaf\xf1\x99{~\xc8\xe7\xbf\xf8\ + \x8ep\xa8\x94+\x88\x82O\x10\xa6\xc4\xa9\x85\xe3\xda\xa4I\x8bpn\x1f\x91\x88p\ +Z\xf3\xac:}\x05I\xd2\xe0\xe0\xec^6\x9c\xbe\x91?\xfc\xb3\xf71\xe2\xc0\x18\xaa\ +\xfaG\xc1S\x01\x05\xb6\xe3P\xb4\xed<\x93~\xe3r\xf8\xcd\xb7]\xc6\xd2\x92\xe4\ +\x81\xaf<\xcc\xf4s/2\xb6t\x05\xce\xd8\x08\xb3\xf5}D\x89\xc0\xf7\x8b\xc4B\x85\ +B{I@\xda\xaeQ\xf2\x0b\x8c\x17\xaa\x99~\xb7s\x0b\xa2/\x1bT\x939\xd2$%\x89\\\ +\xac4\xc6MSD\xea\xe0&\x11\x05\xd9\xa2\x98\xd6\x10\x8c\xd1\xff\x8e\xf5\xbcq\ +\xba\xd0a\xb6xp\x1d\x9531\xe5\xc3\x15\x1bV\xf3\xfd\xef}\x97\xa9\x89\x154\xc2\ +\x18/\t\x88A\x9e\xd0\xfa\x00\x00\x1fYIDAT\x13\x89\x1bC\'\x92T<\x9f \xda\xc7\ +\xf2\xaa\xcfU\x1bJ\x08\xa0\xe8\x0b\x15g\x9b\xfb\xebS\xac<\x87^\xfd1\x8ec\xbc\ +\xa2O\x94$8\xb6\xc0\xb7 \x95\x11\x05\xdb\xa5\x19J\xae\xbeh\x19\x97^\xf4\xeb\ +\xfc\xed?\xef\xe0\xf3\xf7?BZ\x9f&\xb0\x0b\x04\xad&RH,\x19b\xc9\x0e\x8e\x15\ +\xe3\xd9J\xb9\xdb\x08\nc\x05\xea\xbb\xbf\xc7R_\xf0\xda+\xce\xe7\xedo\xb9\x92\ +\x8dg\xa8\x1b/\x0b\x15)\xef8\x82Bf2nuB\x8a\x05\x0f\x1b\x1b\x1bI\x9cF\xd8H\\\ +\xcf\x03\xd4\xea\xf7\xd6\xdbn\xe1\x17\xae\xb9\x8e\xbf\xfc\xec_\xf0\xc4\x13\ +\xdf\xc1\xf3<\xa4\x94\xb9#X\x87\xd4\xb6\xdbm\xca\xe52B\x08\xa6\xa7\xa7\xb9\ +\xe1\x86\x1b\xb8\xf9\xe6\x9b\x19\x19\x19!I\x12j\xb5\xda\xb1\t\x87\x93\x1c\ +\xed\xe0\x06\xe5\xa3(\x95J|\xe4#\x1f\xe1K_\xfa\x12\xff\xf0\x0f\xff\x90\x87\ +\xdf\xea\xbc\x97\x95+WbY\x16/\xbe\xf8"SSS\xcc\xcd\xcdQ\xadVs3\xd5\x8d7\xde\ +\xc8m\xb7\xdd\x96\xcf\xe3\xcc\xcc\x0c\xc3\xc3\xc3Y\xc8\xbb\xca\x0f\xe9-\xe2x\ +"\xb1\xe4\t\xdd\x07\xf5f\xb2:}\x8bz++\xbe\x01i&\xa1U\': /\xcd!\x88\xb0h\x12\ +\x84!\x1d\xa7\x8a%T}\xffo~o\x86\xbf\xfe\xc7\x07y\xe8;;a\xfc*\xeaa\x81R!\xa4\ +\xecm\xe7}\xef9\x8f7_v\x01\x7f\xf9w\x9bX\xbd\xe626^\\\xe1?~\xe8A~\xb2\xf9q*V\ +\xc8]\xff\xf9?r\xf1\xfaI\xa6\xaa\xdd\x82\x88\xbaL\x84\xad\x15\x87\x8c\xb3zP\ +\x8eZZ[\xb9d\xa07\x02\x0bz}<z_\xd2\xdfP*\x04j\x11l\xfe\xc1O\xf9\xca7\x1e\xe7\ +;\xdf\xff\t\xfbf\x9b\x08\xb7B\xa1X\xa6\xd9\xa8\x11Gm*\x1e\x9c\xbbv\x15W\xbf\ +\xf6\xe7\xb8\xea\xf2\xf3Y1\xae\xae1\xd7\x84o=\xba\x85g\x7f\xb4\x83R\xa9B\xb5\ +Z\xa5\xd9\xae\xd1\t\xea\x14\x8a6\x95\x92\xc7\xa5\xe7\xbf\x9a\x0b\xceY\x8aC7\ +\x81\x0f N\x02\x1c\xdd\x1b\x16\x9f\x107\xcf)h\xa3\x84\xe6C_\xdd\xc9S\xcf<\ +\xcb\xe6\xa7~\xc8\xccl\x03\xdb)\x81p\t\xc2\x94\xa0S\xa3ZL(\x17%\xabV\x9c\xc6\ +\xc5\xaf\xb9\x80+.\x7f\r\xebV\xdb\xb8\x90\x7f\xe9\xeb\xf6\x95\xbb\xb7\xba}S\ +\xd2\xec\xfb\xd3?\x9ee\xd37\xbe\xc5c\x9b\x7f\xc0\xde\x03s\xc4\xd2\xa1\x13\'8\ +\x8e\xa7\xec\xc3iL\xd8\t(\x97\x8b\xbcz\xfdz.Y\xbf\x9aw\xdf|!z\x1f\xb2\x7f6\ +\xe4\xa9g_\xe0\x99m/2\xd3\x0c\xb0=\x9f4Q~\x934\x8c\xb0\x92\x88\xe5\x93\xc3l\ +\\\x7f&\xeb\xd7,\xa5Zp\xf3\xf7\x08z7\x9bi_\x92h\x9cU\xb3\x89$<\xbb}\x8e\'\ +\x9e\xdc\xc2\x9e}\x07\x10\x85\x12\x89c\x13%\x96*<\x98\x82L\x13F+.\x1b\xd6\ +\xbd\x8a\x8d\xebNg\xb2\x94\xdd\xff\xe0Sf\xa5\xf9.$\x9f\x13\x0e%\x95*n\xbb\ +\xb7<\xc9\xc1\x06<\xbde\'\xdf\xfb\xf1v\xee{d\x0b\xf3QB\x1a\x85\x08\xa1\xea\ +\x8f\xa5a\x07\x8b\x94\xa2\xe3p\xce\xda3x\xf5y\xe7p\xd5e\x97p\xf6\x1a//\x89\ +\xd3[\xd6\xc4\x1a\xb8~\xf7\xb3\xabvaz\xd5\xdbj6)\x95\xaa\xa4Y~c\x10\xaa\xee\ +\x7f\xcf?\xbf\x95g\x9ey\x86\xed\xdb\xb7333\x93\x87\xf3j\xa7\xf9\xe5\x97_\xce\ +\xd4\xd4\x14+W\xae\x04\xba~\x83\'\x9f|\x92\xbb\xef\xbe\x9bF\xa3\x91\x97\x00\ +\x91Rr\xd7]wq\xfa\xe9\xa7\xe7\xa6\xa1$I\xf2\x12\x1e\xf3\xf3\xf3<\xf3\xcc3\ +\xec\xda\xb5+WX\xae\xeb\xb2j\xd5*6l\xd8\xb0`\xcf\xf1#\xa1\xaf\x01\xb0o\xdf>\ +\x9ez\xea)v\xed\xda\x85\x94\x92j\xb5\x8a\xe7y\xac_\xbf\x9e5k\xd6\xe4\x15w\ +\xf5\xf1\xc7\x12F\xdbj\xb5\xd8\xb2e\x0b\xdf\xfa\xd6\xb7\xd8\xb2e\x0b\xb3\xb3\ +\xb3\xf9\x98\xf3\x9e\xe8\x9e\xc7\xf2\xe5\xcb\xd9\xb8q#\x97\\r\t\xabW\xaf\xce\ +\xef\xa1^\xaf\xb3y\xf3f\x9e\x7f\xfey\x1c\xc7\xc9\xbf\xd6\xad[\xc7\xc6\x8d\ +\x1b\x8fz\x7f\xff\xab9\xc1\n\xe4x\x89 \x99\x05\xdb!\xa4\xcc|;e\xacPDJx\xe4\ +\xbb{\xf9\xd4\xbd\x8f\xf1\x95\x1f\xa48c\xafbzn\x07\xc4?\xe4\x91/\xff>\xcbm\ +\xf8\xe7\x87\xb7\xf1\xd1O|\x8e\x0f}\xf4\x83x\x1e\xfc\xb7\x0f\x7f\x92`f\x9a\ +\xcb6\x9e\xcb]\xbfs\x1b\x05\xa0\xd3\x82\x91\x92V\x1e)6\xaa\xb9\x8f*=.\x94\ +\xe2\x10v\x8f\x02yyH Lb,\xdb\xc9\xbb\x13\xd6R\xd8?\x03\xd3s)\xb5\x86j\x03\ +\xbat\x89\xc3\xd2\xf1n=,\x1f\xbdC#\xefe\x1dD\xe0\xbb]a\xa0\x15\xdf\xc1\xd9\ +\x1a\x93\xa3CY\x99\xf2D\xf5\xd5\xf6}\x1c\xa1\xbd\xeaa\xfe\n\x89C\x9a)\x918U_\ +\x96\xa3\x9c\xc8\x9d\x10\xe6\xea0}\x00\x1a\xcd\x00\xb7\xe03<\xac\x9a\x10\x95\ +|(\x97t\x99\x8e^\xc5\xab\xcb_\xf4\x06\x1c\xf4\xf8\x19\xe8.\x06\xda\x11yK\x90\ +\x008P\x87\xd9ZB\x18\'\xcc\xcc\xd5\x88\xc3\x0ec#\xc3LMV)\x97\xa1\xe0\xaaD?;\ +\x82\xa2\x93f\xad@\xd5\x1c\xb6\xda\xca\xcd\xe3{\xfd\xf3\xa1\xa3\xf8@gj\xcbL\ +\xdd\xf7\xd2\xff^\xc6\x91\x12\x14\xbe\xdf\x15\x14:7N\xf8\xd0H\x95\xe9U\xbf\ +\x1fj\x9eU5_}\xbd\xee\xce\x15r\xf1\xac\xcb\xc3K\xa7Ws\x1d\x96$I\x88\xa2\x08\ +\xcf\xf3\xf2d\xb9}\xb5\x08g\xb4\xc0\xbe6\xec\xdd\x9bR\xaf\xcda%\x11\x95r\x91\ +\xa5\x13CL\x8dC\x12B\xc5\xef\x8e\xc3\x06\xc2D\xe5\xef{\xb6*ks\xa4\xcbk!\xd9+\ +\x90\xf5*xdd$\xff]g`k\x93\x8eV:\x8dF#ok\xab\x8f\xf5<\xd5\x83\xe6\xa9\xa7\x9e\ +\xe2\x13\x9f\xf8D\xde\xaf\xbcP(\xd0l6\xf9\xb3?\xfb3FFF\xf2\xeb\x86a\x98\x0b\ +\xce\xc1q\x01y\xabZ}\xcd(\x8a\xf2\x1c\x94\xa3\t\xf80\x0c\xf3J\xba\xfa\\:?E\ +\x08\xc1\xfe\xfd\xfb\x99\x9c\x9c\xec;\xb7\xbe\xc7cURz\xbcz\x87Q\xaf\xd7\x99\ +\x9d\x9d\xa5^\xaf\xb3t\xe9R|\xdf\xa7T*Q(\x14\xf2*\xbbzNt\x01\xca$I(\x16\x8bX\ +\x96\x95\xb7\x05>\x19\xda\xda\x9e4&\xac\xc5a\xa1\xdc\xeb\x02\xf0\xa9\x14\xb5\ +9\t.\xbch\x19w.\xbb\x85\xe7?|\x1f\xcf\xedy\x963\xa6J$q\x85e6\xcc\xc6)\xdb\ +\xb7\x1f I+|\xfa/\xbe\xc8\x87?\xfc&\xae\xbb\xeez>\xf3\xc9\xff\x97\xcd\xdf\ +\xdd\x8c\xcdm\xc4I\xd6\xc4\x8dnj\xa0\xd4\x85\xff,w\xd1Jcp\xf4\xaa\x8a\xad\ +\x83\xb4}\x10\x16U\x01\xe5\t8}B\x10R\xa6\x13B\xc5\xcbzkec\x91\xb1$NC\xa4%\ +\x98o\xa6T\x87}\n.\xdd\x02\x8e\xd9\xf9SI\xae<\x00\x1c!p\x0b\xc5\xeca\xd0\x82\ +L\x97\x0f\x11X\xd8\xe8\n`\xc2R_i\xaa\x82\xac\xfc\xa2\x8a\x9cZ\xb1\x04\x9a\ +\x1d\x1f\x84\xea\xf3\x95&Pt\xba\xa53\xe2\x14\xc20\xc1w$\xbe\xe3\xb0\xf0\xba:\ +\x0bZ\x93@\x92\x92\xa6\t\xae\xb4\xd4<d\xff_V\x85\x91\x8a\x8dm\xd9\xb4\x97O %\ +T\xbd~al\xa3\x95f\xf7\xbd\xb0QJ\x8d\xec\x0e;m\xd5\xa4\x11K\xa5l\xa4\xa8\x98\ +\x00dJ\'hS*\x1c\xb9+\x9d\xe3\x90\xad<U\x94-\x80~n\x13T\xf5\xe60\xbbK-\x8c\ +\x93\x18\xda\xa9\xc4s,\xdd}w\xd1\xf4\xb6d\x05r\x81e\xdb6\x13\xa36M`\xaa\x08S\ +g\x08,\xc6\xf2\xf7A\xcf\x8f\xe3\xf7+2$\x14mm@K\xfb\xe6n!\xb4\xe0\xd4\x02Y\ +\xb7\xa8\xed\x15\x9c:\xcf\xa3\xb7\n\x80*\xfd\x9e\xe6\xcaC\x0b[!D\x1eA\xb4}\ +\xfb\xf6>\xa5\xa8\x8f\x19\x1e\x1e\xce\x15\x84\x10\x82B\xa1\x90\'\xce\xe99\ +\xe9U&\xb6m\x13\xc7q~}\xcb\xb2\x8e\xb9\xdb\xa0\x16\xc2Z\t\xf5\x9eWJ\x99\xb7\ +\xc9\xd5\xf7\xd0[F\xe4X\x94\x87\xde%\xe9\xb1ig\xfb\xe4\xe4d>\x17\xbd\xf36\ +\xd8\xbb=MSJ\xa5R\xdf9\xf5\xef\xbd=\xdfO\x14\'\xd8\x89~\x9cH\x07("\x03\'w\\\ +\xa7@\xbd\x15\xe3\xd8p\xd6J\xf8\x9d\xff\xf3F6\xae)3\xbf\xe7)\xd6.\xab\xe2\ +\x02\x93\x8e`\xee`\xca\xd0\xc8*\x9e|z+\x1f\xbf\xfb\t\xde\xfc\xa6\xd5\xacz\ +\xd5Z\xb0l\x1e\xfb\xee4i\xd2\x15\xd8:\xabC\x19\xd5\x1c\x12\x04q\xe6\x8b9b\ +\x90\xd6QI)\x17\x0b\x94}\x8f\xa2cQ\x14\xbay\x93\xfa^\x01\x96x\xaa\xe8\x9dH\ +\xc1\x8a\xd5\xff\x8a\x8eE\xc9s)\xb9.\xe3#>\xae\xd5/T\xb5\x00I\xa2\x948\x96\ +\x84\x9d\x908\n\x11t?\xc8H\x8b4\xcc\x92\xe0\x12\x07\xd2l\xf9/\xc1\x91Ja\x15\ +\x84\xfa\xb2\xa5\xb2\x95\xebfQ#\x05\x18\xf1\xd4q\xa5\x9e\xe6\x8cV\xf6\x9ar\ +\xc1\xc6w\x1c\x04i^\xa8pA,\xb0d\x82\xebX\xf8\x9e\xc8\xfa\x84\xabT\x10KB\xd5R\ +\xf30\xe2\xc2\xa8\xd7\xbd~\xd2\x01+\xe9\x9a_\x04)2MT\xea\x88\xde fy\x81\xe5\ +\xa2*\x98\xa8wd\xb6\xc8~\xb6\xc8{Q\x1f\t\xbd?\xcf\xfa)\xe5\xbf\'\t\xb4ZIn\ +\x06\xca:\xcf\xaa9\xf0P%\xd2\x9d\xc1\xddG/\xd9\x88\x8e\xf2\xf9\xe9-y\xa1\x05\ +\x8a^\xc9v\xda\x91\x8e[\xcc?7\xbd?;R\xcd\x93\x9dv\xcdU\x9e\x95\xe2\x10c\x11a\ +\xe9`\x80# \x84\xa0\xddn\xe7\x82V\x97&\xd1+c}\xcc`\'=\xad\xf4\xb4\xdd_7Hr\ +\x1c\x87F\xa3\x81\xeb\xba\xec\xda\xb5+/U\xee\xba.\xadV\x8b\xa9\xa9\xa9Cv\x1b\ +\xfa\xde\xf5\xca\\\x0b^\xbd\xaa\xd7&!-\xa8_N\x99\x0f\xad\xccz\xcdRq\x1c\xe7>\ +\x06=\xd7\xbd\xf7\xfdr\xca\xe5\xe8\xact\xdb\xb6\xf3\x1dZ\xaf\xf9J\xfb\x8et\ +\xaft=\x17\x83\xcaIJI\xab\xd5\xca{\xa3\xeb99\xd1\x9c\xe2\n\x04H\x04\x96P=\ +\xae;\x89\xaa\x10Q.9xV\x80 \xe1\xd2s\xe0\x03\xef\xbd\x89u\xcb,.:kI\xde-oll%;\ +w\xcf\xe1\x95&\xd9\xf4\xc8\x13<\xf4(\xfc\xc1]\xef&L\x05\x9b\xbf\xff\x03U\x06\ +Kj\x93L\x92u\xd1P\xdd\xfd\xb4\x9f@\xb5m:>\xc2\xa0C\x14\xb6U\r"2\x9fA\n^\xac\ +\xf2]\xbc\x18\n)T\x842\x119H\xd2( \x0cZDQ\x0b\x1b\xb5\xd3\x08\xa3\xac\x10$]a\ +Y\xf0\x04\xae\xa3l\xcb\x02K\xb5f\x95\x02\x99*\xbf\x92p3c\x98\xcc\x94Go\xf8Xv\ +\x0e\xe2\x18GF\x14\xec\x04+\t\x90Q\x80H\x02\x1c\x19Q\x16J\xa0\xfb(\xe5F\xa4\ +\x84\xb6\x8d*\xe9\xdej\xd5\x8fz\xffi\xd8!i7\x88\xdbMd\xd8\xc6\x91)\x15\x0fJV\ +\xd7\x87b\xc5*\x10\x8cP\xa5]\x0c{P\xb5Um/\x19\x87 %\x8e\xb0\xf1lU\xc6\xc3\ +\x15jg`g;(\xbd\x10\xf0\x1c\xa5Ld\x9a\x12G\x11\x9dN\xb3g$\xfd\x01\xd9\x9a(\ +\x8e\x95\xa52\xcb\xc1\xd4\xb2\xc3\xb6\xa1RR\rWlIV\xfc03e%j\x17\xb2p)&\x1d\ +\xe5u\xd4\xa9\x01\xba\x82M\x97\xc1\xd09\x00\x8e\xe3P\xf6\xdd|\xfe\xed\xcc\ +\x91$\x03\x15\xb9\xe5K(Xj\\\xbe\xe8*\r\x920k8\x16s\xb4fP\x9a^A544\x94+\x8eR\ +\xa9\x94\x8fI+:-\xc4\xb5B\xd1\x99\xdaZ\xf9\x81\xea\xd6\xf7\xfc\xf3\xcf\xb3s\ +\xe7N<\xcf\xcb\xcbtt:\x1d\xce9\xe7\x1c5K\x99\x00\xd5Y\xdbz.\xb4p\xd5\n\xaaW\ +\xd0\xf6\x9a\xa2z[\xcc\x1e\x89^\x05\xad\xcf\xa9\x15\x98>\x97\xf6\xcf\xe8k\ +\xe9\xb2-\xc7B\x18\x86}\xb5\xc1\xf4\xdc\xe8s\x08!r%\xe3\xban\xdf\\\xeb\xda^\ +\xfau\xa5R\x89R\xa9\x94\xff\xedd\xa8\x87u\xea+\x90&\xb9\x14\xb7mH\xa4\xda\ +\x87\xd8\x04\xd8\xe9<%\xe05k\xe0#\x1fx\x177\xbf\xee5\xd8$\xcc7a\xff\xbe\x19\ +\xaa#KiK\x8f\xea\xf8i|\xe4c\x7f\xce\\\x1b\xde\xfc\xd6\xdb\xf9\xce\xf7\x9e\ +\xa2\xd5\xee\xae\xaa\xc9\n<\xda=\xc5\xc3\xb5C\xf3x\x1dH\x9e_\xc0\xf5<U\x142\ +\r\xb1\x92\x08WF\x14l(\xdb`G\x1d\xdc$\xc6!\xcd\xae\x9f\xe0:\x02\xcf\xf7\xf1\ +\\\xb5*\xb4%\x94\\\xf03\x1fD\x9a\xca|\xe5\xabW\xdd\xae\xe3b\t\x01R"u\x012\ +\x99\xad\x8d\xb5;\xa4Gy(\xa7\xbf\n\xa9uD\x8a\'b\nvJ\xc9\x8d(\xda1\xbe\x15\ +\xe0\x11`\xa5Jh\x96]\x15\xee\xaawC\x9ecS.\x95\x07Nz\xe8\xfb\'\x8a\x1ev\xc1\ +\xc7)x\x08\xd7Q\x82\x9al\'%\xd5\xb9\x0b6\x94DB\xc5\x95\xf8v\x8ame\x95\x89e\ +\x82\xb0R,K\xbd\xe7\x16\x90f}\xac\xa2P\x95(\'V\xc2\x1c\xd9\xddy\x08\xa1\x8aV\ +\x1e\xcb\x0e\xc4\xf3\xba\x0fi\x9aB\x10$\x84aJ\x925H\xf4\x84\xbag=f\x1b\xf57\ +\xdfV9\x98\x0b?`/\xff\xb1\xd3\x117\xbe\xef\xe7\xf6~[tw\xa5%\x01U_\x95\xfa)d\ +\xc9\x9a\xc4!Q\xbbA\x1auT\x15\xe7$V\x01 :0\xe5\x18>\xbc\xba\x9f\xf9\xce\x9d;\ +\xf9\xfc\xe7?O\x10\x04\xb9\tE\x87\x96j\xc17\xb82\xd7B\xb3\xddn\x03j5\xae#\ +\xaf\xee\xbf\xff~\x0e\x1c8\x90\x0b\xd3(\x8a\xa8T*l\xd8\xb0!\x7f]\x1c\xc7\x14\ +\x8b\xc5>\xc1\xad\xfd\x1bQ\x14\xd1\xe9th\xb7\xdb\xb9\x0f&\x0c\xc3>Ev,\xe6\ +\x9d\xde\x9d\x87nI\xab\xfb{\xb4Z-\x84\x10\x87\x08\xea^\xc1~4<\xcf\xcb\xfd\ +\x1a\xbd\t\x80\xfa\xbcz\xfcz\xee\xf4\xf5\xf5=\xe8\xb1\r\x16b\xec\xbd\xd7\x13\ +\xc9\x89Wa\xc7\x8b@\xb5\x00\'\xb3A\xdb\x16\xed\xb0E\xc5K\xa8\nAHL\xbb\xde\ +\xe1\xe7\xceYF\x94E\xe48e\xd8\xfd\xd3\x17\xf0\x8b%\x0e\xb4[\x1ch\xa6\xd8v\ +\x99\x7f\xff\x9f\xff\x8a/|\xeav\xbe\xfe\x90\xcd\xce\x9f\xb6\x99<G\x0b\x18]\ +\xa7Kb+\x11\xfe3\xd2\xbc\x99\xdd\xd7\x12\xa8~\xea\x96\x12\x86\x12\x08\xea`Y\ +\xb8\xbe\xab\xae\x1dFJ\xfa\xd9\xea\xc3(\xd3L\xf0\x0b\xa9\x04I\xcf\x92V&\x11q\ +\x02\xbe\xabl\xd6\xbe\xeb)\xe5\x01*SZ\xa5\xb5g\xa5X\x07\xb6\xc1\xda\xfe\xa5\ +\x7f\xb5\xad\xcc>\x13#e\x8a\xa5\xbb\x10\x92\x92d\x19q\x96\xedc;\xd9\x03\x1e\ +\x87$i\x84#lU\xdb\xe9(\xdb\xec\xb8\xd9\xc1q-p=\x90)I\x1c\x11K\xb0\x1d\x0f7\ +\x8b\xc0qm[Ii\x99\xa8e}\x12\xa1\xb6\x87B\xe5\x87\xd8j<RJ\\\xc7^\xf0C-%$q\x8a\ +\x94\x11\x16J\x18\xe7sr\x94wSf!\xdb\xc2\x06\x7f\x81\xfb\xb1\xe9\n\xa0t\x81\ +\x95\xf1\xc2\x1c\xdb\'H\xafR\x07\x85\x98\x16*\xae\xe3fv;I\x94\xad\xb8]\xcf\ +\xcb\xaa\xd6\x08l[\x0b\xd1\xde\xb8@ U\xbe\'\xe1\xba\x1c\t!\x04\xcdf\x93M\x9b\ +6\xf1\xf5\xaf\x7f\x1d)%\xb7\xdcrK\x9e\xbfP\xa9Tr\xff\x856\xf3\xe8zMZp\xf6\n\ +\xbar\xb9\xcc\x8e\x1d;\xd8\xbau+\xbe\xef\xe7}\xbf\x93$a\xed\xda\xb5\x9c}\xf6\ +\xd9\xf9\xdc\r\xce\xa1\xf6\xa1h\xdc\xc3\x8c\xfd\xe5\x96:\xd7\x0ek\xbd\xb3\ +\x01\xf2:T\xda\x1f\xa1\xcf\x0b/\xcft\xa4\xdf\xbf\xde\xdd\x91\xfe{\x14E\x87\ +\xf8j\xf4\xe7\xa8w\x87\xa5_\xd7\xdb\xb5\xf0d0_\xc1\xa9\xae@,\xd4\xf2KB;\x08H\ +|\x07\x0f\x9b\xb2WV2%\t\xb1\xd2&S\xd5\x12Ac\x8eb\xa5H\xa33\x8fSX\xc2p\xc5\ +\xa2\xbe\xe7\x00v\xa9D\xd2h\xe2\x0e\x8d\x92\x88\x88\x8f\xff\xf93\xbc\xeb\xdf\ +\xdf\xc9\x03\xff\xf256\x9e\xf3\xf3y\x7f\x05\x9d\xecha\xe1`\xe7}(\x8e\xcb\x05\ +\x02X\xc2!\x8c\x94`\xf4\xdc\xccs-\x13\x95|`\xa9d5\x12\xa9j\xba\xe8\x0f2\x8e\ +\x12~\x16\x99i-%Ie.P\xfc\xac\xd5h\x14\x05\x14\xfcn\xb2\x9d\xcc"L\x84\xeb*[\ +\x8c8\xec\x12\xb9\xe7\xc6\xb4\x93\xddB\xc6\x01i\x94`\xbb\xaa\x8d\xaa\xe3\xd8\ +\xaa]\xaf\x1a\x14B\xe8\x15{\xe6<\xcf\x03\xfc\x0e3K\x168\xc5"\x99\xdb\x1e\x84\ +\x85\xed\xbb\xd9|w\x9d\xc5\xe80\xe8\xac_\xba\xaa;\xae^/\xec\xcc\xd6\x88E\x92\ +\x99\x9b\xf4\xc3\x15E\t\x02\x1bi\xa5\xd8\xb6P\xcd\xb3\xe8\x96m?Vz\x1fh\xd0\ +\xf6|\xd5\x1f\xde\xca\xe6\xdf\xb6\x04v\xcfn\x05\x99d\x11G\xc5\xe3\xfa\x90\ +\xf4\n5\xe8\n;\xdb\xb6\xb1\x85\x8d\x8cC,G}\x16\xdcB\x7fTN\x12\x86\xd8\x9e\ +\x07R\x92f\xabk\xdb\xb6\xd5\xe7\xc8\x16\x88c\xe8(hY\x16\xf7\xdf\x7f?\x8f=\ +\xf6\x18\x96eq\xcf=\xf7\x10\x04\x017\xdcp\x03\x13\x13\x13\xb9\xf3\xda\xb6\ +\xedC\x84\xa1\x8e\xb8\xaaT*\xb9r\xe9t:\xdcu\xd7]4\x9b\xcd\xae)\xae\\\xa6\xdd\ +ns\xc6\x19g099\xa9\xa6/\x13\xe8\xda\xa9\xdc\xeb\xdb\xd0&!\xed\x8f\xe8\x15\ +\xd2\xbdst,&\x1e\x1dU\xa5\x15\x93\x16\xe0\xba\xfb_\xef\xcejP)\x1d\x8b\x13\ +\xbbw\x0c\xda\xa7\xa2\x17\x04\xbd\xe6I\x1d\xaa<\xa8h\xf4\x1c\xea9\xe9u\xc8\ +\x9bj\xbc?\x0b,\xc0N\xf1]\x082\xb1n\x01\xa4>\xe0\xa8\xa6>\x12\xfcb\x11\xa2\ +\x80J\xc1c\xd7\xec\x1cg\x9c^\xe5K_{\x0c\xeb\xac\x8d`9t"\x89\xb0\x05\xf7}\xf5\ +Q\xce?w-Vq\x8c\x00\x1d\xfd\xe4\x00\x01\xb2\xdd\xc2*\xaa\x04\xad\xf9\xb9\x0e\ +\xc3#\xa3\xc7\xb5\x13\xd1N\x7f\xe1\xda\xd8\xae\x16\xc4\xb1\x12\xecY\xb2\x1f\ +\x16\x99# \x8b\xf1\xb1\x1cUs2;\x87\xba_\xa9\xc2rE&l3\\\xb7\xe7w)\xb0\x1c\xa7\ +_\x96\x89n\x18m\xcf\x9f\xb2i\xed\xbd3u\x1e\xe1\xb8\xdd\x81\xe77\xa1c\xbf\xe8\ +~\xb7z<\xcf\x87\x91\x9e\x96\xcc\xfe%z\xea\x9f\xf5\xccK\xdf\xb1z\x1eD\xef\x11\ +\x03+jP\x89\x91t\'\xc7u\xecL\xb7twM\x8b\xf1Zu\x85\x80\x1a\xa7e\xc9L\x19\xa5=\ +v\xce^\x85\xa9\xf0|\xad\xac\xfa\xc7\xa9\xef@\xdd\xdb\x91Yh\x15\xde\x8b\xe58Z\ +\x7f\x1e\x82\xad\x17\x0f\x16\x08\xa1\x17\x1f\xf9\x99\x8ex\xfd8\x8e\xb3\xe6Q?\ +\xe1\x1b\xdf\xf8\x06\xf5z\x1d\xd7u)\x97\xcb|\xedk_\xe3\x89\'\x9e\xe0\xdak\ +\xaf\xe5\x9ak\xae\xe9\xb3\xeb\xf7\nL\xcb\xb2r\x01\x1d\x04\x01\x0f=\xf4\x10\ +\x0f?\xfc0333\x14\n\x85<,u\xc7\x8e\x1d\xbc\xe1\ro\xe0\xado}+\xa0\x04\xb3\x16\ +\x9a\xdaA\xde\xcb\xa0b\x18\xfc\xff\xcb\x11\xaa\x0b\xbd\xb6\xf7\xf5G\xda\xc9\ +\xbc\xdc\x08\xa8\xc1\x1d\xd4\xd1\xfe\x0e\xf4\x85\xea\x0e\xde\xd7\x89V\x1e\ +\xc0)\x9e\x07\x92\xa6Y\xc5\xc5\x88\xc0\x8e\x88\xb1q(\xe2e\xd1\'\xba4\xb7zV\ +\x12\x90\x1dpB\x1ai\x91\x7f\xfa\xd6\x01>\xff/\xdb\xf8\xf6\x8e\x19f\x9b`y\x05\ +J\x8e\x83\'\x03\xc2\x83/\xf2\xc9\xbb\xde\xcb\xaa!8\x7f\x15Te\x84#\xda\x10\ +\xb5\x01\x01N\x11\x19;XN\xe1\xb8V\x97:9L\x8b\xcfn\xa8\xa5\xce\xee\xe8\x15v\ +\xb6R\x1e\x87\xbcF\xf7P\xd1\x0c\x0e(\xfb`\xca\x81\x0fh\xb6\xe8OH\xfb:a\x80\ +\xc8\xcf\xd0U&=\xaf\xe9\xfd\x0ej$\x83\xceXk!\x896\xe0\xa4\xee9\xa47\x81\xadW\ +\x1d\x1d:\x86\x9e\x1c\x8a\xfc:}#\xe4\xb0\xf7\xdb7\xc6\xde\x9f\x8fe\ru8\xa5\ +\xa3\x93\\\xb5\xc2\xa4?,\xcf\xea\x19\xcf"\x15\xc8\x11\xc9\xdf\x87~\x05\xdc\ +\x7f\xfd\xfe\xf9\x1d\xbc\xf6\xc2\x11b\xdd\x95\xf8\x9dw\xde\xc9\x0b/\xbc\xc0\ +\xc4\xc4Dnw\xafV\xab4\x1a\rFGG\x89\xa2\x88s\xcf=\x97\xff\xeb\xb7\xfe\x93\xb5\ +c\xfb\xf3\xb2\\.\x03\xe4\x1d\xf4\x9e\x7f\xfey\xb6l\xd9\xc2\xb6m\xdb\xf2(\xa3\ +z\xbd\x9e\x17\x0c\x9c\x9f\x9fg\xc3\x86\r\xbc\xf9\xcdo\xe6\x82\x0b.\xc8\x0b\ +\x06\xea\xd0\xdc\x93\xc1Ql8<\xa7\xb6\x02\x91iVJ5 \xb0Z\xc4\xd8\xf8\x8c\xe0h\ +\x0f7tc[% \xdb\xe0\xb4\x80\x02;\x1ae\x9e\xd9\x07\xef\xff\xd8\x17\xf8\xf1\x0b\ +\xb3\x14\xab\x13\xb4k-H\x12\xd6\xacY\x01\xf5\x9f\xf2W\x7fz\x07k\xaa\xaa\xbft\ +\x81\x8eR@I\xac\x92\x08\x9d2\xba(\xe1bc\x11\x06\x95A\xf7\x81\xd6+\xe5\xdeG\ +\xdd\xc9U\x8a\xecyM\xb6\xa9\xef9\xeb\xa0\xa28\xc2\xd8,\x88\x89\xd1\xadB\xb4\ +\x90\x1fT\x1cB\x0f\x96\x1e\x99\xdd\x87\x16\xe8\xfa\x85\xdd\x8c\xfc\xc1q\xf5)\ +#\xab_Mj\x9d\xd2\xfb\xba\xc3*\xb3\xde\xeb\xf6r\xc4\xfb]H\x11\x1c\xee\xf8\x85\ +\x8e]\xe8z\x0b\x9c\xafw\x90\x87\xe9\'\xf33Q \x83\xd7?\x8a\xc2`\xe0\xef}\x8b\ +\x96\x01\xb4\x8f\xe5S\x9f\xfa\x14\x8f?\xfexn\xfe\xd2\xe6\x96\xb9\xb99\xc6\ +\xc6\xc6r\xf3\x9265\x01\xb9\xd9G;\xfe\x8b\xc5"A\x10033\x83eY\x8c\x8d\x8da\ +\xdb6\x8dF\x83e\xcb\x96q\xdbm\xb7q\xc5\x15W\x00\xfd\x89\x87\'C\x9e\x83\xe1\ +\xc8\xd8\x1f\xfc\xe0\x07?x\xa2\x07\xb1x2\x13\x89\x80\x18\x15\xe9\xe1Q\xc0J3\ +\xbb\x8c\x92\xbb$\xd93\xac,*\x01`\xe1{>\x13c\xb0\xeaU\xeb\xd9\xb6\xf59\xe2N@\ +;\xe8\xe0\x14\xcb\x1c88\x8beIv\xbd0\xcb\xe5\x97\xbd\x8a\x82\x804\x92xv\xf6\ +\xc8uB\x95\xa1\x858\xa2\x99\xe6e\xdeE\xfes\xff\xf9\xba\x99\x1d\xe9\xc0\xf1}V\ +\x9dA\xef\xb7\xecY[\xf6\x9f\xbc+h,\xb2\xec\x96\xee9\xc4\x02#\xb0\x16R\x1e\ +\xbd\x12\xd0\x92J\x91Z\xd9\xb6\xc6"+*)3SX\xf7l}35`>\xeb\x1d\xde\xe0\xac\xea\ +\xa0\xa1\xfe\x99\xd6G\xa9\xebZ:\xa2\xecpd\xef\x95\xd4\xdb\xaf\xbe\xd0\x83Azo\ +\xf6\x08>\x13\x8b\xec\xde\xad\xecK\xbf\\\xf4\xff~\xb8!\x1d\xf9\xdf\xc7N~\xdd\ +\xee\xcf\x0b)\x8e^\x05}\xa4\x1d\x88\xe6\xd2K/e\xef\xde\xbd\xbc\xf0\xc2\x0b\ +\x00\x8c\x8e\x8eR(\x14\xf2\xfe\xe6\x9e\xe7\xe5!\xa9\xb6mS*\x95\xf2\xc8)\xed\ +\xc7\xd0\x91W\xcb\x96-c||<\x0f\xfd\xadT*\xbc\xedmo\xe3\xc2\x0b/\xccM5\xfa\ +\xbb\xce\x870\x9c\xdc\x9c\xe2;\x10 UUf\x03\x0e"\x88)0\x02\xb1\x9fw(\r]\x95\ +\xafa\x93\xe5+H\xf5a\x96\xd6P\xde;\xfa[?\x8e\xf8\xd0\xc7?\xcblX`\xd7\\\x04\ +\xc2gb|\x88\x97\xb6=\xc9\x07~\xf3-\xbc\xe3\xe6\xb3Xj\xab\x84>[\xc6\xa4\xed6\ +\xc2-fN\xe8\xa3\x0b\x89\xa3\xde\x03\xfd\x96\x8f\xb4\xef\xa7\x01\x1bx\xf6}A\ +\xeb\xcd\x11.a\x1d\xe6{J\xdag\x17\x1f\\\xf1\xf7]kP\x81,\xa0\x98\x16\xdaQ\xf4\ +\x8d{\xe0<\x8bM\xc4<\xdc^b\xf0\xfc}\x0c\x8c\xaf\xef5\x87\xbdB\xef\xd1\x0b\ +\x9dXg\x99\xf4\x1fu\xb8\x9d\xd4\xcf\x9c#\\d\xa1\x9d\xc8\xe1\x14\xc8B\x91KZ1x\ +\x9e\xc7\xc1\x83\x07\xb9\xfb\xee\xbb\xd9\xb4i\x13K\x96,all\x8cF\xa3\xd1\x97\ +\x1f\xa1\x13\xe2\xa4\x94\x14\x8bE\xca\xe52\xb5Z-O\xa2\x8b\xa2\x88\xb9\xb99\ +\xca\xe52\xaf\x7f\xfd\xeby\xe3\x1b\xdf\xc8\x8a\x15+\xf2\x1aS\xbd\xd9\xdd\xad\ +V\xeb\x90\x0cl\xc3\xc9\xc7)\xad@\xa4Tn\x90\xc4\x86\x989l\xda\xf8T!.@\xea\x80\ +\x80\x96\xa3j+\xd9\xa8\x0c]O6U\x94S\xaaLP\x9d\x14B\x07\x9e\xde\rw\xfe\xe1g\ +\xd8\xdd*PK]:\xf5:K\xa6\xaa\x88\xda.\xfe\xdb\xef\xfc\x06\xd7\x9c_\xa2\x0c\ +\x14$\x08=c\xba\x8d\xc9b\xa5\xc3\xa1\xde\xe2>3\xd5\xc0\xbf\x0e/ \x17\xd2\x0e\ +\x0b\x1cv,\x1c\xe2\xef8\xdc\xef\xbd/\xe8\xb9\xde\xe1\xd6\xeaG\x1d\xff\x11\ +\x9d\xdbb\xc1!\x0c\x1a\xf9\xf4\x91\x87\xbb\xc6\xe0\xf8\x8e\xbc\x02\x1fT \x0b\ +]] q\xfb|7G\rj\xfbY\xd3\xab\xdc{\x15\xfa\xc0v\xedHo\x1f,\xac@\xf4\xdffgg)\ +\x97\xcbx\x9e\xc7\xcc\xccL\x1e\x95533C\x10\x04\xd8\xb6M\xa1P\xa0P(\xe4Ux;\ +\x9d\x0eI\x92044\x84\x10\x820\x0c\xf1}\x9f\x8d\x1b7r\xf5\xd5W\xb3n\xdd\xba\ +\xbe\x1a[\xda\xe7\xa2w \xc6|ujp\xca+\x90(UU8\x12\x1a8\xcc\xe3ST;\x10\xe9\x81\ +\xed2/T\xd6\xb8V %\x19d\tUEu\x02\xd9\x04\xb7BM\x08~8\x07w\xbc\xff^\xb6\xcfD\ +\xc4\xd2\xc6\xb1c\xe2\xd9\x1d\xdct\xe5z\xdew\xc7\x9b8\xff4pS\xf0E\x96\xf9\ +\x0b\x87\xdaZ^\x16Y\xcd\r)\xbb\xe6\x0eKW\xf3\x15\x03\x82.\xcd\x04\xa3N\x02\ +\x93\xe8\xd6\xbfp\xe4\x95\xfc\xe1D\xb3 \xdb]\x1cI\xba\x0c:\xcc\x07\xcf\xd0c\ +\x0e\xeb\xf5\xcf\x1cr\x1d\x06\x85{v\x1f\xc7\x98\r\xdd\xebk\xea]Y\xf7\xfa\x83\ +\x8e\xa6\xa4\x16Rp\x87\xf3\x01\x1c^\x81t\xc7+{\xbaO. \xb3\x81\x81\x9d\xd7\ +\xcf\x9a^\'\xba\x14\xdd\xb9<\xdc`\xf4\xb1}d\x15\xae\x0f\x93;\xd1\x1b*\xaa[\ +\xa9\x16\x8bE\xe28\xe6\xd9g\x9f\xcd+\xe3n\xdf\xbe\x9d\xf9\xf9\xf9<jJgrOMM144\ +\xc4\xfa\xf5\xeb\xb9\xf4\xd2K\x19\x1b\x1b\x03\xba\xe1\xb3\xba\xb6\x16\xf4W\ +\xc6}9\x85\n\r\'\x8eS^\x81$\xa9\xf2qH+\xc0\xa1\x81\x83\xab\xb6$\xd2\x03\xdb\ +\xa6i\x89|\x07bK(Em\xf5\xc1\x94:\xc42 \xa87\xb1\xc7\x96\xb0;\x849\x07\xde\ +\xf6\xde{\xd8=\x17\x13a3Tv\xd8\xbf\xed)\xde\xf7\x1b\xbf\xc4\xbf\xbb\xed\\\ +\xaa@1\x82b\x1e\\\x93.\xf0\xa0\x8a\xc3>\xc0\xfd\x16\x87#+\x90\xfe3.\xa0@,\ +\x0b\xddc\xfbh\nD\x0c|\xcfFyx\x05\x92/M{\x04\xce!\xc2\xbeG\xa8[\xfd\xc2y0xuA\ +\x05\xd2w\xe4Q\xc8Cq\x17V"/g\x07B\xcfk\x8e\xbe\x03\x19\xfcY\xf6\xfc\xa4{f.L\ +\x9f\x19\xf0\xb0G\x1d\x07}\xf3\xd8{\xb1\x1e\x852\xf8}\xc1Qv\x0b\x1c\x1en\x17\ +\xd2\xfb{\x1c\xc7\xb8\xae\xdb\'\xf0\xf5\xeb\xb5\x19K\x97\xff\x88\xe387E\xe9,\ +r\x9d/\xd2\xbb\xcb0\n\xe3\xd4\xe4\x94V \xc8L(d&\x10\xd5\x1f$Kn\xc8\x1e\x8c\ +\xb8\xc7Q\xabJM\xe8\xd0\xcbL\xc8\xe7!H\t\xb1c\x13\x00?\xdc\x07\xff\xfd\x9e\ +\xef\xf1\xd9\xff\xef_`\xeaUx\xd5"\xe1\xdcv\xfe\xf8w\xdf\xcd/]\xe82\x12K*\x8d\ +iU\x86\xd6\xf5I-\x81%EV*D%\xb0\xb9\xc2V\to=Q8\xbd+\xe0tp<\xf93\xba\xb0\xb9\ +\xa6\xfb\x08\xebPV\xfd\xc7\xa3?t\x0b\xf9>\xfa\xcey\xac\x9f\x80A\x014\x10at\ +\xb4\xd3\xf4\x89\xa6|\x10\xc7\x9a\x97\xb1\xf0}\x1e~\x9e\x16\xe6\xe5\x1e\xaf8\ +\xfc\x18\xe5\x11\xe6\xff\x7f\x99\xd9\xaa\x7f\x00\x0b\\\xf8\xe5*\x10\x83aq\ +\x9c\xda\n\xe4x\x91\xd0\xee@\xd1\x07\x88\x89\xdbu\x9cB\x95\x8e\xed\xf0\xcd\ +\xe7\xe1o6=\xc3g\x1fx\x02\x86\'(\x14$\xc3r\x86\xbf\xfc\xd0;\xb9b9\x94\xa3&4g\ +`x,\xdb\x05\xf4\xac\xb5\xe3\xac"\xa9\xd3\x93\x19lu\xf5\xd5\xb1\x84Q\x1a\x0c\ +\x06\xc3\xc9\xce+z\xf9!-p\x8bd\xc9\xd4\x0e\x8e;\n\xd2\xc1\x95p\xe1\x99\xf0\ +\xde;6p\xcb\xd5ka\xf6Y\x9cV\x9d\xb9}5>\xf9?\xfe\x99:\x10\xb8e\xd2\xea\x12\ +\xa4\xe5\x13#2\x93S\x16\x91c\xbbH\xcb\xee\xb3\xfeh\xba{#\x83\xc1`8\xb51r\x8c\ +\x94N\xdc!\x89\xb3\x8cu\t\x04)~\n\xab\x0bp\xe7\xaf\\\xc9\xad\xaf[\x8f\xd7\ +\x9af\xacT\xe2\xa9\xa7\x9f\xe7\xde\x87g\xd8\x0b\xcc\xd9>M\x1c\xe2\xd4&\x8c!\ +\x92B\x994,\x10\x8e\xcd\xa0O\xb2\xd7\xe6~\\\xbew\x83\xc1`8\tx\x85+\x90\x94\ +\x84\x08\xd7q\xb0\xfd\xcc\'\x12\x05\xd8nBE4)\xc6\r6\x8c\xc2\'\xdew\x1dW\xad_\ +A4\xf7\x12\x13SK\xf9\xc4g\xee\xe1\x99\x19\xd8\x8f\xca1\xb1\xb2f\x11I\xaa:\ +\xf2I8\xd4\x89\x9e\x19\n{w GK\xe22\x18\x0c\x86\x93\x99W\xb4\x0fD\x92\x92d\ +\x1e\t\x07\xa1\xaa\xbd\xca\x14\x88@\xc4`9$\x94hHA\xdb\x82\x0f|\xe4>\xbe\xfa\ +\xd8\x16di\x88\xc9\xd3W\xf2?\xff\xef\x1b9\x1d\x18\xca\xce\x97\xf9\xe2\xb1\ +\x85jf\xb4p\xd9\x0f\x8e\x12fi0\x18\x0c\xa7\x06\xaf\xf0\x1d\x88\xc0\xc2\xa1\ +\x13\x044;\xf3 \x02\xb0\x03H\x03\x88,H|\x92\xf9\x06C\x168\x01\xfc\xf1\xfbo\ +\xe4\xda\xd7\xbf\x9a\xe9};i5S\xfe\xf6\x1f\xa7\xa9\x87\xaa\x12U\x1e]eAz\xa4\ +\xdc\nz\xfe\xf7\x8aU\xdd\x06\x83\xe1\x7f\x07^\xd1;\x10\x80 \x8c\xf1<AJ\x9b\ +\x98\x00+\x95x\xa2\x00\xd2\'\xaeE8\xe5"\xcd\xfa<\xa5\xd1a\x1a\xc0\xde\x08\ +\xfe\x9fOm\xe2\x9e\x07\x9ed\xbcZ\xe1\x0b\x1f\x7f\x0f+\x86aH\xb7}HUu\x93\xbcO\ +\xf9\xb1\xcc\xae\xd9\x89\x18\x0c\x86S\x90W\xbc\x02\xd1="b"\x12R$\x0e\x16.\ +\x0e\x02;\x05\x19\x82\xe5\x02v@\x8cK\x0b\xc1\xee\x06\xfc\xc5\xe77\xf3\xb9\ +\xcf~\x8e\xcb6L\xf1\xc9?y?\xd5\xa2\xeaI^t\x94>\xb0\xe5Q\xccX\xbd\x18\x05b0\ +\x18NA^\xe1&\xac\xcc\xdf\x91\x15\xb5\x92\x14IPa\xb9\x11\x10\n\x95\xd4\xae\ +\xd2\xb7-D\x12Q\x02VU\xe0-Wo\xe4\xfd\xef}\x0b?\xdd\xb9\x95\xcf\xfd\xcd?\x91H\ +\xd5D\xb0\xd9V\xfa \x05\xa2H\x1e]9\x18\xe5a0\x18NQ^\xe1\xddZ\x12T\xa9E\x81E%\ +\x97\xe5\xba\x1b\x87\rx\xba\xa2t\xe2!\xc3\x04\xc7\x07G\xc0\x85\xabm\xce\x9c\ +\xba\x94\xb8\xbe\x9b\xfb\x1e\xb8\x8f\r\xe7\x9d\xcd\x95\x97\xad\xa3X\x84z+a\ +\xb8d\xd3\xea\xb4\xf0\xdcr\x7f\x06\xf0\x91\xfaU\x18\x0c\x06\xc3)\xc4+\xdc\ +\x84\x15\x01Y\x97AY!\x86\xbc\xf4\x89\x0e\x92rH\xb1\xb59+\x02+FURt;\xaaXc\xea\ +\xf2\x17\x7f\xf5\xcf\xfc\xe0\xe9\xef\xf3_\xff\xe8\x83\x0c\x952\xdfG\xaa\x14M\ +^\xc3J\xa3\xcb\x7f\x1cRO\xc4`0\x18N-^\xe1\xcba\x0b\xb5\xcfpA\x82#\xc1O\xc1\ +\x97\xaaw\x88CJB\xa2r;t\xddB\x1fU\xc40\x89\x80\x10_\xc0\xaf\xdeq\x13\xeb\xd7\ +\x9f\xcb\xc7>\xf6Q\x820\xb3xI]\xb4$\xa5\xafi\x93e\x82\xaf\x0c\x06\xc3\xff\ +\x1e\xbc\xc2\x15\x88\x03\x14\xb3\xca\xbc\x80T;\x07[\x82#S<\xc0\xc5\xc5\xb2T\ +\xa8n\xa2\xb3\xff\x1c\x0b\x1c\x07\xa4C\'Pj\xe8W\xdfq\x1b#\xc3C<\xf3\xcc\xd3\ +\xb4:!\xaem\xd3\xdd\xcb\xa8\xf2\x89\xbd\xd1\xbd\xd2:r\x05]\x83\xc1`8\xd9ye+\ +\x10\tR\x8a\xcc/\x11\x03M\xa0\x06i\x03\x92\x04\x12\x81\x93\x85\xe5JK\xa5\x86\ +\xc4\xba\xe4UZ\x82\xc4\xa7\xe0C,\x95\xb9\xea?\xbc\xf7\xd7\xd8\xb5k\x07\xb6c\ +\xd1h\xd5\x06/\x95\x93\xf6|\x99\xdd\x88\xc1`8Uye\xfb@d\xd6V\x03\xc8\xfd!2\ +\x06\xe9\xa8\x86S\x96\xab\xdau\xb8\x90d\xa6\'A\x82\x83\xad\xfc\xe2\x12\x02[)\ +\x10a\xa90^\xdf\x81m\xdb\x9e\xe3\xec5k\xb0Hz\\\x1c\xdd"&\xc7\xd6R\xd5`0\x18N\ +n^\xd9\n\x04\x06\xfaR$=6\xa6,~w\x01SS\xde\xb0h\xa0I\x11\x0c6\x11Z\xa8\x01\ +\xd3`S)\x83\xc1`851\n\xc4`0\x18\x0c\x8b\xe2\x95\xed\x031\x18\x0c\x06\xc3\xa2\ +1\n\xc4`0\x18\x0c\x8b\xc2(\x10\x83\xc1`0,\n\xa3@\x0c\x06\x83\xc1\xb0(\x8c\ +\x021\x18\x0c\x06\xc3\xa20\n\xc4`0\x18\x0c\x8b\xc2(\x10\x83\xc1`0,\n\xa3@\ +\x0c\x06\x83\xc1\xb0(\x8c\x021\x18\x0c\x06\xc3\xa20\n\xc4`0\x18\x0c\x8b\xc2(\ +\x10\x83\xc1`0,\n\xa3@\x0c\x06\x83\xc1\xb0(\x8c\x021\x18\x0c\x06\xc3\xa20\n\ +\xc4`0\x18\x0c\x8b\xc2(\x10\x83\xc1`0,\n\xa3@\x0c\x06\x83\xc1\xb0(\x8c\x021\ +\x18\x0c\x06\xc3\xa20\n\xc4`0\x18\x0c\x8b\xc2(\x10\x83\xc1`0,\n\xa3@\x0c\x06\ +\x83\xc1\xb0(\x8c\x021\x18\x0c\x06\xc3\xa20\n\xc4`0\x18\x0c\x8b\xc2(\x10\x83\ +\xc1`0,\n\xa3@\x0c\x06\x83\xc1\xb0(\x8c\x021\x18\x0c\x06\xc3\xa20\n\xc4`0\ +\x18\x0c\x8b\xc2(\x10\x83\xc1`0,\n\xa3@\x0c\x06\x83\xc1\xb0(\x8c\x021\x18\ +\x0c\x06\xc3\xa20\n\xc4`0\x18\x0c\x8b\xc2(\x10\x83\xc1`0,\n\xa3@\x0c\x06\x83\ +\xc1\xb0(\x8c\x021\x18\x0c\x06\xc3\xa20\n\xc4`0\x18\x0c\x8b\xe2\xff\x07\xc1\ +\x94N90\x82\xbf\xc4\x00\x00\x00\x00IEND\xaeB`\x82' + +def getSplashBitmap(): + return BitmapFromImage(getSplashImage()) + +def getSplashImage(): + stream = cStringIO.StringIO(getSplashData()) + return ImageFromStream(stream) + +#---------------------------------------------------------------------- +def getActiveGridData(): + return \ +'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00 \x00\x00\x00 \x08\x06\x00\ +\x00\x00szz\xf4\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\x00\x05cID\ +ATX\x85\xed\x97\xcbo\x1b\xd7\x15\xc6\x7fw^\x1c\x92C\x8a\xa4DQ\xa2)\xd9\xb2%?\ +\xe0\xa7\x82\xc0\x85\x83"\xa8\x1b\x17AwYtQtU\xb4\x8b\xa2\x7fH\xfe\x8c\xee\ +\x8b\xac\x124\xab\x02E\n\x17h\x01\x17\x86Q7\xaac7~I\xb2hZ\xe2sH\xce\x83s\xef\ +\xedB\xa6dI~))\x90E\xf2\x01\x17\x98\xb9s\xe7\x9c\xef\x9c\xf9\xce\xb9w\x840L\ +\xbeK\x18\xdf\xa9\xf7\x1f\x08\x00\xa2\xd5ji!\x04\xe3\xf1ma\x9a\x02!\x0c\xe0\ +\xa0-\xa55O\xb6\x06t\xfb\x11\xa0\x98\xca\x99\x880\x0c5\xf0R\xe7B\x08\xb4\xde\ +\xbd6\xc4\xd8\xee\x0bk\xb5\xde3\'\x84f\x10J\x06aB4R\xd8\x96\xc1\x97\x8f{\xfc\ +\xfbq\x97-?\xa2\xe49\x9c\x9d\xf38u\xc4\xa3\x945\xb0\xc6\x8e\r!`\x1f\t!\xd8C\ +\x004\x89\xd4\x04\xb1$\x88%#\xa9(f,\xd6\xdb!\xab\x9b\x01\x9b\xbd\x98 \x96\ +\xb4z\x11\xa6\x80\xea\x94K\x9ch\xfe\xf5\xa0\xcb\xfa\xd6\x90\xea\xa4\xcb\x99Z\ +\x8e\xead\x96\xa2\x97\xc2\x14\t\xd6s\xf3h\x04JC"\xf5\xf3\xa1\x00M.c\xd1\xe9\ +\'\xf4\x82\x11\xc3H\xd2\x0f\x13\xda~L\xcb\x8fI\x12\xc9\x953\x93\\\xff\xaa\ +\xc9\x17\xb7\xb7\xf8j\xdd\xa7\x13J\x82aB\xad\x94\xe2\x83\xe5)\xba\xc3\x84\ +\xde a\xa6\x98\xe2\xc3wf\xb8\xbcX\xa2\xe89(\xa5\x08\x82\xd1\x98\x00\x04qB/\ +\x1c\xd1\xf6Gl\xf6"\x9euc\x84\xd0\xfc\xf4\\\x99Oo\xd4\xf9\xe2\xf6\x16w\x9f\ +\x0chG\t\xbe\x1f\x13\xf9#\xa63\x16\x1f\xff\xee\x027\xefw\xb9\xf5\xb0K\xc7\ +\x8f\x11\xa6`a\xc6\xe5\xdc\xbc\xc7\xfcT\x06/msa~\x82\xa5\xd9\x1c\x8em`\x08\ +\xd0Z\xa1\x94\x02\xc0\xb2,\x8b\x8d\xe6\x90\xcfnl\xf0\xf9\xcd\x06\xf1H\x13E\ +\x92h0\xa2\x906\xe9\x0eF\xf4#I<\x88\xb9w\xa7I\x9cs\xc8\xa5-\xcae\x97\xa3\x93\ +i\xdc\x94\xa0\xe4\xd9\x143\x16\xfd~\xc4\xf4D\x8ak\x17\xa6\xb9z\xae\xcc\xd1r\ +\x06\xc76)dm\xb2)\x03\xa5\xf7jLk\xb0\xc6\x9f~\xbd\x19r}\xa5\xc9\xb0\x9fl?\ +\x1d)&2\x16n\xe9\x19?_.sf>\xcf\xbd\xc7>x6\xaeka\n0S&~\x980\x88\x12l[\xb08\ +\x9b\xe1\xda\xa5\nW\xcfW8;\x9f\'\xefZ;\x02\xd5Z\xa3\xb5~\xae\xa5\xdd\xaa\xb3\ +\x94R\x94<\x87\xc5\xaa\xc7\xe9#9V\xee\xb61\x1d\x13\xc7\xb3I\xa7L\xfe[\x1f\ +\xf0\xd1\xe5\x19\x96O\x97\x08\x84\xa6\xd1\x0c\xe9\r\x136\xfd\x98F7f\xbd\x19Q\ +\xefD\xa4]\x93\xf7O\x95\xf9\xed\xb5\x05\xa6\x0bi\xd0\xa0\xb5\x06\xa5w\x8a\ +\xe6\xc5J\x13B`Y\x16\x96\x94\n\xc76\xf9\xd9\xc5il\x03>\x1e\xc6\x94\x8b.\xc7g\ +2\xcc\x16]\xc2(a\xbd\x19\xa2\xd0,U\xb2\xfc\xf1\xcf\xab\xb4\xba#\xd2\x9eM\xed\ +H\x96 N\xa8\xe4m~\xb4X\xe47W\x8f\x92\xcf\xd8\xe8\xfd\xb9~\x05l\xdb\xde\x16\ +\xa1R\x8a\xa9\xbc\xc3\xd5\xf3\x15\x8a\x9e\xc3\xadG\x1dV\xd6|\xfe\xfa\xe5\x16\ +\x83@"\xa4f\xf9D\x9eKKE\xe6k9\xaa\x15I\xca1\xc9y\x16\xbd0ay\xa1\xc0\xf2B\x91\ +B\xd6\xd9\x8ez\x7f-\xbf\x04\xe3lX\xdb\xcdF\xe3\x98\x06\xd5\x92Kmj\x96l\xc6\ +\xa4\xd1\x89\xf8\xc7\x9d6O\x9e\x05\xa8 \xc1\x16P\x9b\xcd\xf2\xd1{U\xfe\xb3\ +\xda\xe5\xd1\xd3!A?\xa1\x92Oq\xf1X\x81\x93\xd5\xdc[E\xbd\x1f;e8f\xae\xb5\xe0\ +lm\x82\xa7\xa7c\xd67CB\x7fD\xa4!\x1a):\xc3\x84_\xfd\xf8\x08\x1b\xad!\x8f\x1a\ +CD\xa4x\xf7x\x81\xc5\x19\x8fl\xcaDJu\xe8v.\xe28\xd6cu\x8e\xb3\xa1\x81`\xa4y\ +\xd8\x18\xf0\xc9\xdf\xd6ht\x02\x0c\xd3`\xc2\xb3\t\xa5\xa2\xde\x8eX\xdb\n0\ +\x81?\xfc\xfe"\x8b3y,\xcb\xf8F\x04,8\xb8\x0f\x18B\xe0\xa5\x04K\xb3Y~\xf9\xfe\ +\x1c\xc3(\xe1\xc6\xd7m>\xffg\x9d\x87\xf7{,\x1d\xcfsr6K\xde5\x01\x81T\x1a\xeb\ +%v\xde\x9a\xc0\x9e\x94<7\xa2\xb5&e\x19\x9c\x9d\xcbo\xef\th\xee\xac\xf6xp\xb7\ +\x8b\x1f\x8c\xa8\x98i\xe6\xa6\\6\xfd\x98\xf2\xc4\xb6(w\xeb\xfc[\x10x\x81\xca\ +\x9e\xe6qy\xb1Dm2\x83e\x18\xdcZ\xed\xd2\xe8\x84,L\xbb\xdc\xaf\x0f\xa8\x163L\ +\xe6R\x87\x0b}\xec%\x8e\xe3\x9d\xba\xd9\xcf~,\xcc\xf1\xbc\xd2\xb0\xd9\r\xb8\ +\xf9\xa0\xc3\xdf\xef5Yy\xd2\xe7|-\xc7/\xae\xd4\xb8t\xac\x88\x94\xf2\xff\x99\ +\x81\x83\x84L\x01\xd5R\x1a\xcb2\t\x13\xcd\xd7\x8d!\xd7\xef\xb4x\xf7D\x89ss\ +\x13\x98\xc6\xee\xf9\xe1M\xd0Z\x93$\xc9\xe1\x8edZk\x94\x86r>\xc5\x85\xa3\x05\ +\xde;9\x89\xd2\xb0\xb2\xd6\xe3\xee\x86\x8fa\x18\xe3\x85oM\xe0\xb5\x198\x00!P\ +J\x03\x9a\xc5J\x86_\xff\xe4\x18\x00\xb7\x1ev\xf8\xd3\xcd\xa7,\xcd\xe6\xb0\ +\x0e\x11\x92R\xea\xf5\x1ax\x15\xf3\x9dk\xa0\xd9O\xf8\xcb\xed\x06\x1b\xed\x80\ +\x13\x95,\x1f\x9c\x9f\xc6s\xdf\x1c\xd7\xf6\x81$\xc08\xd0\xbb\xdf\x80=;\x1a0\ +\x9dw\xb8rj\x92w\x16\nH\xa9h\xf9\x11\xe1H\x1e \xfb*[\x96\x94r\xe7\xe6\xb0\n\ +\xd6Z\xa3\x94b\xae\x94"\x97\x12<\xde2\x08\xa2\x98 2\xb0\r\xe7\xb5}AJ\xb9]5\ +\xf5z]\x03\xbb\x02\xfa\x06\x10\x80m\x1b\x18\xa6\xc9\xda3\x1f\xd71\xc9\xb9\ +\xf6k\xdf\x91R\x12E\x11\xe2\x87\x7f\xc3\xef=\x81\xff\x01\x1d\xae\x83\xc3q\ +\xb9\xc6\x9f\x00\x00\x00\x00IEND\xaeB`\x82' + +def getActiveGridBitmap(): + return BitmapFromImage(getActiveGridImage()) + +def getActiveGridImage(): + stream = cStringIO.StringIO(getActiveGridData()) + return ImageFromStream(stream) + +def getActiveGridIcon(): + icon = EmptyIcon() + icon.CopyFromBitmap(getActiveGridBitmap()) + return icon + +#---------------------------------------------------------------------- +def getDPLData(): + return \ +'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x10\x00\x00\x00\x10\x08\x06\ +\x00\x00\x00\x1f\xf3\xffa\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\ +\x00\x00LIDAT8\x8dc<p\xe0\xc0\x7f\x06\n\x00\x13%\x9a\x07\xa1\x01\x0e\x0e\xf6\ +\x0c\x0e\x0e\xf6x\xf9\x04]p\xe0\xc0A\x14\x85\xe8|\x82\x06\x90\nho\x80\x83\ +\x83=\xc3\x81\x03\x07\xc97\x00\x9ff\xa2\x0c \x040\x0c \xe4d\xf4\x18a\x1c\xcd\ +\x0b\x0c\x00\x81@\x19X*\x8dbG\x00\x00\x00\x00IEND\xaeB`\x82' + +def getDPLBitmap(): + return BitmapFromImage(getDPLImage()) + +def getDPLImage(): + stream = cStringIO.StringIO(getDPLData()) + return ImageFromStream(stream) + +def getDPLIcon(): + icon = EmptyIcon() + icon.CopyFromBitmap(getDPLBitmap()) + return icon diff --git a/wxPython/samples/ide/activegrid/tool/IDEFindService.py b/wxPython/samples/ide/activegrid/tool/IDEFindService.py new file mode 100644 index 0000000000..cd1b74b363 --- /dev/null +++ b/wxPython/samples/ide/activegrid/tool/IDEFindService.py @@ -0,0 +1,445 @@ +#---------------------------------------------------------------------------- +# Name: IDEFindService.py +# Purpose: Find Service for pydocview +# +# Author: Morgan Hua +# +# Created: 8/15/03 +# CVS-ID: $Id$ +# Copyright: (c) 2004-2005 ActiveGrid, Inc. +# License: wxWindows License +#---------------------------------------------------------------------------- + +import wx +import wx.lib.docview +import os +from os.path import join +import re +import ProjectEditor +import MessageService +import FindService +import OutlineService +_ = wx.GetTranslation + + +#---------------------------------------------------------------------------- +# Constants +#---------------------------------------------------------------------------- +FILENAME_MARKER = _("Found in file: ") +PROJECT_MARKER = _("Searching project: ") +FIND_MATCHDIR = "FindMatchDir" +FIND_MATCHDIRSUBFOLDERS = "FindMatchDirSubfolders" + +SPACE = 10 +HALF_SPACE = 5 + + +class IDEFindService(FindService.FindService): + + #---------------------------------------------------------------------------- + # Constants + #---------------------------------------------------------------------------- + FINDALL_ID = wx.NewId() # for bringing up Find All dialog box + FINDDIR_ID = wx.NewId() # for bringing up Find Dir dialog box + + + def InstallControls(self, frame, menuBar = None, toolBar = None, statusBar = None, document = None): + FindService.FindService.InstallControls(self, frame, menuBar, toolBar, statusBar, document) + + editMenu = menuBar.GetMenu(menuBar.FindMenu(_("&Edit"))) + wx.EVT_MENU(frame, IDEFindService.FINDALL_ID, self.ProcessEvent) + wx.EVT_UPDATE_UI(frame, IDEFindService.FINDALL_ID, self.ProcessUpdateUIEvent) + editMenu.Append(IDEFindService.FINDALL_ID, _("Find in Project...\tCtrl+Shift+F"), _("Searches for the specified text in all the files in the project")) + wx.EVT_MENU(frame, IDEFindService.FINDDIR_ID, self.ProcessEvent) + wx.EVT_UPDATE_UI(frame, IDEFindService.FINDDIR_ID, self.ProcessUpdateUIEvent) + editMenu.Append(IDEFindService.FINDDIR_ID, _("Find in Directory..."), _("Searches for the specified text in all the files in the directory")) + + + def ProcessEvent(self, event): + id = event.GetId() + if id == IDEFindService.FINDALL_ID: + self.ShowFindAllDialog() + return True + elif id == IDEFindService.FINDDIR_ID: + self.ShowFindDirDialog() + return True + else: + return FindService.FindService.ProcessEvent(self, event) + + + def ProcessUpdateUIEvent(self, event): + id = event.GetId() + if id == IDEFindService.FINDALL_ID: + projectService = wx.GetApp().GetService(ProjectEditor.ProjectService) + view = projectService.GetView() + if view and view.GetDocument() and view.GetDocument().GetFiles(): + event.Enable(True) + else: + event.Enable(False) + return True + elif id == IDEFindService.FINDDIR_ID: + event.Enable(True) + else: + return FindService.FindService.ProcessUpdateUIEvent(self, event) + + + def ShowFindDirDialog(self): + config = wx.ConfigBase_Get() + + frame = wx.Dialog(None, -1, _("Find in Directory"), size= (320,200)) + borderSizer = wx.BoxSizer(wx.HORIZONTAL) + + contentSizer = wx.BoxSizer(wx.VERTICAL) + lineSizer = wx.BoxSizer(wx.HORIZONTAL) + lineSizer.Add(wx.StaticText(frame, -1, _("Directory:")), 0, wx.ALIGN_CENTER | wx.RIGHT, HALF_SPACE) + dirCtrl = wx.TextCtrl(frame, -1, config.Read(FIND_MATCHDIR, ""), size=(200,-1)) + dirCtrl.SetToolTipString(dirCtrl.GetValue()) + lineSizer.Add(dirCtrl, 0, wx.LEFT, HALF_SPACE) + findDirButton = wx.Button(frame, -1, "Browse...") + lineSizer.Add(findDirButton, 0, wx.LEFT, HALF_SPACE) + contentSizer.Add(lineSizer, 0, wx.BOTTOM, SPACE) + + def OnBrowseButton(event): + dlg = wx.DirDialog(frame, _("Choose a directory:"), style=wx.DD_DEFAULT_STYLE) + dir = dirCtrl.GetValue() + if len(dir): + dlg.SetPath(dir) + if dlg.ShowModal() == wx.ID_OK: + dirCtrl.SetValue(dlg.GetPath()) + dirCtrl.SetToolTipString(dirCtrl.GetValue()) + dirCtrl.SetInsertionPointEnd() + + dlg.Destroy() + wx.EVT_BUTTON(findDirButton, -1, OnBrowseButton) + + subfolderCtrl = wx.CheckBox(frame, -1, _("Search in subfolders")) + subfolderCtrl.SetValue(config.ReadInt(FIND_MATCHDIRSUBFOLDERS, True)) + contentSizer.Add(subfolderCtrl, 0, wx.BOTTOM, SPACE) + + lineSizer = wx.BoxSizer(wx.VERTICAL) # let the line expand horizontally without vertical expansion + lineSizer.Add(wx.StaticLine(frame, -1, size = (10,-1)), 0, flag=wx.EXPAND) + contentSizer.Add(lineSizer, flag=wx.EXPAND|wx.ALIGN_CENTER_VERTICAL|wx.BOTTOM, border=HALF_SPACE) + + lineSizer = wx.BoxSizer(wx.HORIZONTAL) + lineSizer.Add(wx.StaticText(frame, -1, _("Find what:")), 0, wx.ALIGN_CENTER | wx.RIGHT, HALF_SPACE) + findCtrl = wx.TextCtrl(frame, -1, config.Read(FindService.FIND_MATCHPATTERN, ""), size=(200,-1)) + lineSizer.Add(findCtrl, 0, wx.LEFT, HALF_SPACE) + contentSizer.Add(lineSizer, 0, wx.BOTTOM, SPACE) + wholeWordCtrl = wx.CheckBox(frame, -1, _("Match whole word only")) + wholeWordCtrl.SetValue(config.ReadInt(FindService.FIND_MATCHWHOLEWORD, False)) + matchCaseCtrl = wx.CheckBox(frame, -1, _("Match case")) + matchCaseCtrl.SetValue(config.ReadInt(FindService.FIND_MATCHCASE, False)) + regExprCtrl = wx.CheckBox(frame, -1, _("Regular expression")) + regExprCtrl.SetValue(config.ReadInt(FindService.FIND_MATCHREGEXPR, False)) + contentSizer.Add(wholeWordCtrl, 0, wx.BOTTOM, SPACE) + contentSizer.Add(matchCaseCtrl, 0, wx.BOTTOM, SPACE) + contentSizer.Add(regExprCtrl, 0, wx.BOTTOM, SPACE) + borderSizer.Add(contentSizer, 0, wx.TOP | wx.BOTTOM | wx.LEFT, SPACE) + + buttonSizer = wx.BoxSizer(wx.VERTICAL) + findBtn = wx.Button(frame, wx.ID_OK, _("Find")) + findBtn.SetDefault() + buttonSizer.Add(findBtn, 0, wx.BOTTOM, HALF_SPACE) + buttonSizer.Add(wx.Button(frame, wx.ID_CANCEL, _("Cancel")), 0) + borderSizer.Add(buttonSizer, 0, wx.ALL, SPACE) + + frame.SetSizer(borderSizer) + frame.Fit() + + status = frame.ShowModal() + + # save user choice state for this and other Find Dialog Boxes + dirString = dirCtrl.GetValue() + searchSubfolders = subfolderCtrl.IsChecked() + self.SaveFindDirConfig(dirString, searchSubfolders) + + findString = findCtrl.GetValue() + matchCase = matchCaseCtrl.IsChecked() + wholeWord = wholeWordCtrl.IsChecked() + regExpr = regExprCtrl.IsChecked() + self.SaveFindConfig(findString, wholeWord, matchCase, regExpr) + + while not os.path.exists(dirString): + dlg = wx.MessageDialog(frame, + _("'%s' does not exist.") % dirString, + _("Find in Directory"), + wx.OK | wx.ICON_EXCLAMATION + ) + dlg.ShowModal() + dlg.Destroy() + + status = frame.ShowModal() + + # save user choice state for this and other Find Dialog Boxes + dirString = dirCtrl.GetValue() + searchSubfolders = subfolderCtrl.IsChecked() + self.SaveFindDirConfig(dirString, searchSubfolders) + + findString = findCtrl.GetValue() + matchCase = matchCaseCtrl.IsChecked() + wholeWord = wholeWordCtrl.IsChecked() + regExpr = regExprCtrl.IsChecked() + self.SaveFindConfig(findString, wholeWord, matchCase, regExpr) + + if status == wx.ID_CANCEL: + break + + + if status == wx.ID_OK: + frame.Destroy() + + messageService = wx.GetApp().GetService(MessageService.MessageService) + messageService.ShowWindow() + + view = messageService.GetView() + if view: + wx.GetApp().GetTopWindow().SetCursor(wx.StockCursor(wx.CURSOR_WAIT)) + view.ClearLines() + view.SetCallback(self.OnJumpToFoundLine) + + view.AddLines(_("Searching for '%s' in '%s'\n\n") % (findString, dirString)) + + if os.path.isfile(dirString): + try: + docFile = file(dirString, 'r') + lineNum = 1 + needToDisplayFilename = True + line = docFile.readline() + while line: + count, foundStart, foundEnd, newText = self.DoFind(findString, None, line, 0, 0, True, matchCase, wholeWord, regExpr) + if count != -1: + if needToDisplayFilename: + view.AddLines(FILENAME_MARKER + dirString + "\n") + needToDisplayFilename = False + line = repr(lineNum).zfill(4) + ":" + line + view.AddLines(line) + line = docFile.readline() + lineNum += 1 + if not needToDisplayFilename: + view.AddLines("\n") + except IOError, (code, message): + print _("Warning, unable to read file: '%s'. %s") % (dirString, message) + else: + # do search in files on disk + for root, dirs, files in os.walk(dirString): + if not searchSubfolders and root != dirString: + break + + for name in files: + filename = os.path.join(root, name) + try: + docFile = file(filename, 'r') + except IOError, (code, message): + print _("Warning, unable to read file: '%s'. %s") % (filename, message) + continue + + lineNum = 1 + needToDisplayFilename = True + line = docFile.readline() + while line: + count, foundStart, foundEnd, newText = self.DoFind(findString, None, line, 0, 0, True, matchCase, wholeWord, regExpr) + if count != -1: + if needToDisplayFilename: + view.AddLines(FILENAME_MARKER + filename + "\n") + needToDisplayFilename = False + line = repr(lineNum).zfill(4) + ":" + line + view.AddLines(line) + line = docFile.readline() + lineNum += 1 + if not needToDisplayFilename: + view.AddLines("\n") + + view.AddLines(_("Search completed.")) + wx.GetApp().GetTopWindow().SetCursor(wx.StockCursor(wx.CURSOR_DEFAULT)) + + return True + else: + frame.Destroy() + return False + + + def SaveFindDirConfig(self, dirString, searchSubfolders): + """ Save search dir patterns and flags to registry. + + dirString = search directory + searchSubfolders = Search subfolders + """ + config = wx.ConfigBase_Get() + config.Write(FIND_MATCHDIR, dirString) + config.WriteInt(FIND_MATCHDIRSUBFOLDERS, searchSubfolders) + + + def ShowFindAllDialog(self): + config = wx.ConfigBase_Get() + + frame = wx.Dialog(None, -1, _("Find in Project"), size= (320,200)) + borderSizer = wx.BoxSizer(wx.HORIZONTAL) + + contentSizer = wx.BoxSizer(wx.VERTICAL) + lineSizer = wx.BoxSizer(wx.HORIZONTAL) + lineSizer.Add(wx.StaticText(frame, -1, _("Find what:")), 0, wx.ALIGN_CENTER | wx.RIGHT, HALF_SPACE) + findCtrl = wx.TextCtrl(frame, -1, config.Read(FindService.FIND_MATCHPATTERN, ""), size=(200,-1)) + lineSizer.Add(findCtrl, 0, wx.LEFT, HALF_SPACE) + contentSizer.Add(lineSizer, 0, wx.BOTTOM, SPACE) + wholeWordCtrl = wx.CheckBox(frame, -1, _("Match whole word only")) + wholeWordCtrl.SetValue(config.ReadInt(FindService.FIND_MATCHWHOLEWORD, False)) + matchCaseCtrl = wx.CheckBox(frame, -1, _("Match case")) + matchCaseCtrl.SetValue(config.ReadInt(FindService.FIND_MATCHCASE, False)) + regExprCtrl = wx.CheckBox(frame, -1, _("Regular expression")) + regExprCtrl.SetValue(config.ReadInt(FindService.FIND_MATCHREGEXPR, False)) + contentSizer.Add(wholeWordCtrl, 0, wx.BOTTOM, SPACE) + contentSizer.Add(matchCaseCtrl, 0, wx.BOTTOM, SPACE) + contentSizer.Add(regExprCtrl, 0, wx.BOTTOM, SPACE) + borderSizer.Add(contentSizer, 0, wx.TOP | wx.BOTTOM | wx.LEFT, SPACE) + + buttonSizer = wx.BoxSizer(wx.VERTICAL) + findBtn = wx.Button(frame, wx.ID_OK, _("Find")) + findBtn.SetDefault() + buttonSizer.Add(findBtn, 0, wx.BOTTOM, HALF_SPACE) + buttonSizer.Add(wx.Button(frame, wx.ID_CANCEL, _("Cancel")), 0) + borderSizer.Add(buttonSizer, 0, wx.ALL, SPACE) + + frame.SetSizer(borderSizer) + frame.Fit() + + status = frame.ShowModal() + + # save user choice state for this and other Find Dialog Boxes + findString = findCtrl.GetValue() + matchCase = matchCaseCtrl.IsChecked() + wholeWord = wholeWordCtrl.IsChecked() + regExpr = regExprCtrl.IsChecked() + self.SaveFindConfig(findString, wholeWord, matchCase, regExpr) + + if status == wx.ID_OK: + frame.Destroy() + + messageService = wx.GetApp().GetService(MessageService.MessageService) + messageService.ShowWindow() + + view = messageService.GetView() + if view: + view.ClearLines() + view.SetCallback(self.OnJumpToFoundLine) + + projectService = wx.GetApp().GetService(ProjectEditor.ProjectService) + projectFilenames = projectService.GetFilesFromCurrentProject() + + projView = projectService.GetView() + if projView: + projName = wx.lib.docview.FileNameFromPath(projView.GetDocument().GetFilename()) + view.AddLines(PROJECT_MARKER + projName + "\n\n") + + # do search in open files first, open files may have been modified and different from disk because it hasn't been saved + openDocs = wx.GetApp().GetDocumentManager().GetDocuments() + openDocsInProject = filter(lambda openDoc: openDoc.GetFilename() in projectFilenames, openDocs) + for openDoc in openDocsInProject: + if isinstance(openDoc, ProjectEditor.ProjectDocument): # don't search project model + continue + + openDocView = openDoc.GetFirstView() + # some views don't have a in memory text object to search through such as the PM and the DM + # even if they do have a non-text searchable object, how do we display it in the message window? + if not hasattr(openDocView, "GetValue"): + continue + text = openDocView.GetValue() + + lineNum = 1 + needToDisplayFilename = True + start = 0 + end = 0 + count = 0 + while count != -1: + count, foundStart, foundEnd, newText = self.DoFind(findString, None, text, start, end, True, matchCase, wholeWord, regExpr) + if count != -1: + if needToDisplayFilename: + view.AddLines(FILENAME_MARKER + openDoc.GetFilename() + "\n") + needToDisplayFilename = False + + lineNum = openDocView.LineFromPosition(foundStart) + line = repr(lineNum).zfill(4) + ":" + openDocView.GetLine(lineNum) + view.AddLines(line) + + start = text.find("\n", foundStart) + if start == -1: + break + end = start + if not needToDisplayFilename: + view.AddLines("\n") + openDocNames = map(lambda openDoc: openDoc.GetFilename(), openDocs) + + # do search in closed files, skipping the open ones we already searched + filenames = filter(lambda filename: filename not in openDocNames, projectFilenames) + for filename in filenames: + try: + docFile = file(filename, 'r') + except IOError, (code, message): + print _("Warning, unable to read file: '%s'. %s") % (filename, message) + continue + + lineNum = 1 + needToDisplayFilename = True + line = docFile.readline() + while line: + count, foundStart, foundEnd, newText = self.DoFind(findString, None, line, 0, 0, True, matchCase, wholeWord, regExpr) + if count != -1: + if needToDisplayFilename: + view.AddLines(FILENAME_MARKER + filename + "\n") + needToDisplayFilename = False + line = repr(lineNum).zfill(4) + ":" + line + view.AddLines(line) + line = docFile.readline() + lineNum += 1 + if not needToDisplayFilename: + view.AddLines("\n") + + view.AddLines(_("Search for '%s' completed.") % findString) + + return True + else: + frame.Destroy() + return False + + + def OnJumpToFoundLine(self, event): + messageService = wx.GetApp().GetService(MessageService.MessageService) + lineText, pos = messageService.GetView().GetCurrLine() + if lineText == "\n" or lineText.find(FILENAME_MARKER) != -1 or lineText.find(PROJECT_MARKER) != -1: + return + lineEnd = lineText.find(":") + if lineEnd == -1: + return + else: + lineNum = int(lineText[0:lineEnd]) + + text = messageService.GetView().GetText() + curPos = messageService.GetView().GetCurrentPos() + + startPos = text.rfind(FILENAME_MARKER, 0, curPos) + endPos = text.find("\n", startPos) + filename = text[startPos + len(FILENAME_MARKER):endPos] + + foundView = None + openDocs = wx.GetApp().GetDocumentManager().GetDocuments() + for openDoc in openDocs: + if openDoc.GetFilename() == filename: + foundView = openDoc.GetFirstView() + break + + if not foundView: + doc = wx.GetApp().GetDocumentManager().CreateDocument(filename, wx.lib.docview.DOC_SILENT) + foundView = doc.GetFirstView() + + if foundView: + foundView.GetFrame().SetFocus() + foundView.Activate() + if hasattr(foundView, "GotoLine"): + foundView.GotoLine(lineNum) + startPos = foundView.PositionFromLine(lineNum) + # wxBug: Need to select in reverse order, (end, start) to put cursor at head of line so positioning is correct + # Also, if we use the correct positioning order (start, end), somehow, when we open a edit window for the first + # time, we don't see the selection, it is scrolled off screen + foundView.SetSelection(startPos - 1 + len(lineText[lineEnd:].rstrip("\n")), startPos) + wx.GetApp().GetService(OutlineService.OutlineService).LoadOutline(foundView, position=startPos) + + diff --git a/wxPython/samples/ide/activegrid/tool/ImageEditor.py b/wxPython/samples/ide/activegrid/tool/ImageEditor.py new file mode 100644 index 0000000000..a95b139423 --- /dev/null +++ b/wxPython/samples/ide/activegrid/tool/ImageEditor.py @@ -0,0 +1,93 @@ +#---------------------------------------------------------------------------- +# Name: ImageEditor.py +# Purpose: Image Editor for pydocview +# +# Author: Morgan Hua +# +# Created: 12/24/04 +# Copyright: (c) 2004-2005 ActiveGrid, Inc. +# CVS-ID: $Id$ +# License: wxWindows License +#---------------------------------------------------------------------------- +import wx +import wx.lib.docview +_ = wx.GetTranslation + + +class ImageDocument(wx.lib.docview.Document): + pass + + +class ImageView(wx.lib.docview.View): + + + #---------------------------------------------------------------------------- + # Overridden methods + #---------------------------------------------------------------------------- + + def __init__(self): + wx.lib.docview.View.__init__(self) + self._ctrl = None + + + def OnCreate(self, doc, flags): + if len(doc.GetFilename()) == 0: + wx.MessageBox(_("Cannot create a new image file.\n%s has no paint capability.") % wx.GetApp().GetAppName(), + _("New Image File"), + wx.OK | wx.ICON_EXCLAMATION) + return False + + frame = wx.GetApp().CreateDocumentFrame(self, doc, flags) + panel = wx.Panel(frame, -1) + bitmap = wx.Image(doc.GetFilename()).ConvertToBitmap() + self._ctrl = wx.StaticBitmap(panel, -1, bitmap, (0,0), (bitmap.GetWidth(), bitmap.GetHeight())) + panel.SetClientSize(bitmap.GetSize()) + frame.SetClientSize(panel.GetSize()) + self.Activate() + return True + + + def OnClose(self, deleteWindow = True): + statusC = wx.GetApp().CloseChildDocuments(self.GetDocument()) + statusP = wx.lib.docview.View.OnClose(self, deleteWindow = deleteWindow) + if not (statusC and statusP): + return False + self.Activate(False) + if deleteWindow: + self.GetFrame().Destroy() + return True + + +#---------------------------------------------------------------------------- +# Icon Bitmaps - generated by encode_bitmaps.py +#---------------------------------------------------------------------------- +from wx import ImageFromStream, BitmapFromImage +from wx import EmptyIcon +import cStringIO + + +def getImageData(): + return \ +'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x0f\x00\x00\x00\x0e\x08\x06\ +\x00\x00\x00\xf0\x8aF\xef\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\ +\x00\x00\x97IDAT(\x91\x9d\x93Q\n\xc4 \x0cD\'\xda\xd3\xa9\xe9ac\xdb\x8bx\xa0\ +\xf4C"\xd6mAw@\x0c1/\t\x03\x123+\x16\x95s\x06\x00l\x00p\x9c\x17\xad\xc0\xe4<\ +R\x0c\xeaf\x81\x14\x83\xa6\x18\x1e[N\xc1)\x06\x15\x01Dj\xbc\x04\x7fi\x9b):\ +\xce\x8b\xf6\xbdN\xec\xfd\x99\x82G\xc8\xf4\xba\xf6\x9b9o\xfa\x81\xab9\x02\ +\x11i\xe6|6cf%\xe7A\xce\x83\x99\xd5\xc4\xccZJ\xd11\xd7\xd76\xd8\x8aJ)\xed\ +\xb6c\x8d,~\xc0\xe3\xe3L\xdc\xe0~\xcaJ\x03\xfa\xe7c\x98n\x01\x88\xc6k\xb1\ +\x83\x04\x87\x00\x00\x00\x00IEND\xaeB`\x82' + + +def getImageBitmap(): + return BitmapFromImage(getImageImage()) + +def getImageImage(): + stream = cStringIO.StringIO(getImageData()) + return ImageFromStream(stream) + +def getImageIcon(): + icon = EmptyIcon() + icon.CopyFromBitmap(getImageBitmap()) + return icon + diff --git a/wxPython/samples/ide/activegrid/tool/MarkerService.py b/wxPython/samples/ide/activegrid/tool/MarkerService.py new file mode 100644 index 0000000000..54375dd482 --- /dev/null +++ b/wxPython/samples/ide/activegrid/tool/MarkerService.py @@ -0,0 +1,91 @@ +#---------------------------------------------------------------------------- +# Name: MarkerService.py +# Purpose: Adding and removing line markers in text for easy searching +# +# Author: Morgan Hua +# +# Created: 10/6/03 +# CVS-ID: $Id$ +# Copyright: (c) 2004-2005 ActiveGrid, Inc. +# License: wxWindows License +#---------------------------------------------------------------------------- + +import wx +import wx.stc +import wx.lib.docview +import wx.lib.pydocview +import STCTextEditor +_ = wx.GetTranslation + + +class MarkerService(wx.lib.pydocview.DocService): + MARKERTOGGLE_ID = wx.NewId() + MARKERDELALL_ID = wx.NewId() + MARKERNEXT_ID = wx.NewId() + MARKERPREV_ID = wx.NewId() + + + def __init__(self): + pass + + def InstallControls(self, frame, menuBar = None, toolBar = None, statusBar = None, document = None): + if document and document.GetDocumentTemplate().GetDocumentType() != STCTextEditor.TextDocument: + return + if not document and wx.GetApp().GetDocumentManager().GetFlags() & wx.lib.docview.DOC_SDI: + return + + editMenu = menuBar.GetMenu(menuBar.FindMenu(_("&Edit"))) + editMenu.AppendSeparator() + editMenu.Append(MarkerService.MARKERTOGGLE_ID, _("Toggle &Marker\tCtrl+M"), _("Toggles a jump marker to text line")) + wx.EVT_MENU(frame, MarkerService.MARKERTOGGLE_ID, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, MarkerService.MARKERTOGGLE_ID, frame.ProcessUpdateUIEvent) + editMenu.Append(MarkerService.MARKERDELALL_ID, _("Clear Markers"), _("Removes all jump markers from selected file")) + wx.EVT_MENU(frame, MarkerService.MARKERDELALL_ID, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, MarkerService.MARKERDELALL_ID, frame.ProcessUpdateUIEvent) + editMenu.Append(MarkerService.MARKERNEXT_ID, _("Marker Next\tF4"), _("Moves to next marker in selected file")) + wx.EVT_MENU(frame, MarkerService.MARKERNEXT_ID, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, MarkerService.MARKERNEXT_ID, frame.ProcessUpdateUIEvent) + editMenu.Append(MarkerService.MARKERPREV_ID, _("Marker Previous\tShift+F4"), _("Moves to previous marker in selected file")) + wx.EVT_MENU(frame, MarkerService.MARKERPREV_ID, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, MarkerService.MARKERPREV_ID, frame.ProcessUpdateUIEvent) + + + def ProcessEvent(self, event): + id = event.GetId() + if id == MarkerService.MARKERTOGGLE_ID: + wx.GetApp().GetDocumentManager().GetCurrentView().MarkerToggle() + return True + elif id == MarkerService.MARKERDELALL_ID: + wx.GetApp().GetDocumentManager().GetCurrentView().MarkerDeleteAll() + return True + elif id == MarkerService.MARKERNEXT_ID: + wx.GetApp().GetDocumentManager().GetCurrentView().MarkerNext() + return True + elif id == MarkerService.MARKERPREV_ID: + wx.GetApp().GetDocumentManager().GetCurrentView().MarkerPrevious() + return True + else: + return False + + + def ProcessUpdateUIEvent(self, event): + id = event.GetId() + if id == MarkerService.MARKERTOGGLE_ID: + view = wx.GetApp().GetDocumentManager().GetCurrentView() + event.Enable(hasattr(view, "MarkerToggle")) + return True + elif id == MarkerService.MARKERDELALL_ID: + view = wx.GetApp().GetDocumentManager().GetCurrentView() + event.Enable(hasattr(view, "MarkerDeleteAll") and view.GetMarkerCount()) + return True + elif id == MarkerService.MARKERNEXT_ID: + view = wx.GetApp().GetDocumentManager().GetCurrentView() + event.Enable(hasattr(view, "MarkerNext") and view.GetMarkerCount()) + return True + elif id == MarkerService.MARKERPREV_ID: + view = wx.GetApp().GetDocumentManager().GetCurrentView() + event.Enable(hasattr(view, "MarkerPrevious") and view.GetMarkerCount()) + return True + else: + return False + diff --git a/wxPython/samples/ide/activegrid/tool/MessageService.py b/wxPython/samples/ide/activegrid/tool/MessageService.py new file mode 100644 index 0000000000..288d40d5c9 --- /dev/null +++ b/wxPython/samples/ide/activegrid/tool/MessageService.py @@ -0,0 +1,143 @@ +#---------------------------------------------------------------------------- +# Name: MessageService.py +# Purpose: Message View Service for pydocview +# +# Author: Morgan Hua +# +# Created: 9/2/04 +# CVS-ID: $Id$ +# Copyright: (c) 2004-2005 ActiveGrid, Inc. +# License: wxWindows License +#---------------------------------------------------------------------------- + +import wx +import Service +import STCTextEditor + +class MessageView(Service.ServiceView): + """ Reusable Message View for any document. + When an item is selected, the document view is called back (with DoSelectCallback) to highlight and display the corresponding item in the document view. + """ + + #---------------------------------------------------------------------------- + # Overridden methods + #---------------------------------------------------------------------------- + + def _CreateControl(self, parent, id): + txtCtrl = STCTextEditor.TextCtrl(parent, id) + txtCtrl.SetMarginWidth(1, 0) # hide line numbers + txtCtrl.SetReadOnly(True) + + if wx.Platform == '__WXMSW__': + font = "Courier New" + else: + font = "Courier" + txtCtrl.SetFont(wx.Font(10, wx.DEFAULT, wx.NORMAL, wx.NORMAL, faceName = font)) + txtCtrl.SetFontColor(wx.BLACK) + txtCtrl.StyleClearAll() + txtCtrl.UpdateStyles() + + return txtCtrl + + +## def ProcessEvent(self, event): +## stcControl = self.GetControl() +## if not isinstance(stcControl, wx.stc.StyledTextCtrl): +## return wx.lib.docview.View.ProcessUpdateUIEvent(self, event) +## id = event.GetId() +## if id == wx.ID_CUT: +## stcControl.Cut() +## return True +## elif id == wx.ID_COPY: +## stcControl.Copy() +## return True +## elif id == wx.ID_PASTE: +## stcControl.Paste() +## return True +## elif id == wx.ID_CLEAR: +## stcControl.Clear() +## return True +## elif id == wx.ID_SELECTALL: +## stcControl.SetSelection(0, -1) +## return True +## +## +## def ProcessUpdateUIEvent(self, event): +## stcControl = self.GetControl() +## if not isinstance(stcControl, wx.stc.StyledTextCtrl): +## return wx.lib.docview.View.ProcessUpdateUIEvent(self, event) +## id = event.GetId() +## if id == wx.ID_CUT: +## event.Enable(stcControl.CanCut()) +## return True +## elif id == wx.ID_COPY: +## event.Enable(stcControl.CanCopy()) +## return True +## elif id == wx.ID_PASTE: +## event.Enable(stcControl.CanPaste()) +## return True +## elif id == wx.ID_CLEAR: +## event.Enable(True) # wxBug: should be stcControl.CanCut()) but disabling clear item means del key doesn't work in control as expected +## return True +## elif id == wx.ID_SELECTALL: +## event.Enable(stcControl.GetTextLength() > 0) +## return True + + + #---------------------------------------------------------------------------- + # Service specific methods + #---------------------------------------------------------------------------- + + def ClearLines(self): + self.GetControl().SetReadOnly(False) + self.GetControl().ClearAll() + self.GetControl().SetReadOnly(True) + + + def AddLines(self, text): + self.GetControl().SetReadOnly(False) + self.GetControl().AddText(text) + self.GetControl().SetReadOnly(True) + + + def GetText(self): + return self.GetControl().GetText() + + + def GetCurrentPos(self): + return self.GetControl().GetCurrentPos() + + + def GetCurrLine(self): + return self.GetControl().GetCurLine() + + + #---------------------------------------------------------------------------- + # Callback Methods + #---------------------------------------------------------------------------- + + def SetCallback(self, callback): + """ Sets in the event table for a doubleclick to invoke the given callback. + Additional calls to this method overwrites the previous entry and only the last set callback will be invoked. + """ + wx.stc.EVT_STC_DOUBLECLICK(self.GetControl(), self.GetControl().GetId(), callback) + + + +class MessageService(Service.Service): + + + #---------------------------------------------------------------------------- + # Constants + #---------------------------------------------------------------------------- + SHOW_WINDOW = wx.NewId() # keep this line for each subclass, need unique ID for each Service + + + #---------------------------------------------------------------------------- + # Overridden methods + #---------------------------------------------------------------------------- + + def _CreateView(self): + return MessageView(self) + + diff --git a/wxPython/samples/ide/activegrid/tool/OutlineService.py b/wxPython/samples/ide/activegrid/tool/OutlineService.py new file mode 100644 index 0000000000..95ccd9e357 --- /dev/null +++ b/wxPython/samples/ide/activegrid/tool/OutlineService.py @@ -0,0 +1,520 @@ +#---------------------------------------------------------------------------- +# Name: OutlineService.py +# Purpose: Outline View Service for pydocview +# +# Author: Morgan Hua +# +# Created: 8/3/04 +# CVS-ID: $Id$ +# Copyright: (c) 2004-2005 ActiveGrid, Inc. +# License: wxWindows License +#---------------------------------------------------------------------------- + +import wx +import wx.lib.docview +import wx.lib.pydocview +import Service +_ = wx.GetTranslation + + +#---------------------------------------------------------------------------- +# Constants +#---------------------------------------------------------------------------- +SORT_NONE = 0 +SORT_ASC = 1 +SORT_DESC = 2 + +class OutlineView(Service.ServiceView): + """ Reusable Outline View for any document. + As a default, it uses a modified tree control (OutlineTreeCtrl) that allows sorting. + Subclass OutlineTreeCtrl to customize the tree control and call SetTreeCtrl to install a customized tree control. + When an item is selected, the document view is called back (with DoSelectCallback) to highlight and display the corresponding item in the document view. + """ + + #---------------------------------------------------------------------------- + # Overridden methods + #---------------------------------------------------------------------------- + + def __init__(self, service): + Service.ServiceView.__init__(self, service) + self._actionOnSelect = True + + + def _CreateControl(self, parent, id): + treeCtrl = OutlineTreeCtrl(parent, id) + wx.EVT_TREE_SEL_CHANGED(treeCtrl, treeCtrl.GetId(), self.DoSelection) + wx.EVT_SET_FOCUS(treeCtrl, self.DoSelection) + wx.EVT_ENTER_WINDOW(treeCtrl, treeCtrl.CallDoLoadOutlineCallback) + wx.EVT_RIGHT_DOWN(treeCtrl, self.OnRightClick) + + return treeCtrl + + + #---------------------------------------------------------------------------- + # Service specific methods + #---------------------------------------------------------------------------- + + def OnRightClick(self, event): + menu = wx.Menu() + + menu.AppendRadioItem(OutlineService.SORT_NONE, _("Unsorted"), _("Display items in original order")) + menu.AppendRadioItem(OutlineService.SORT_ASC, _("Sort A-Z"), _("Display items in ascending order")) + menu.AppendRadioItem(OutlineService.SORT_DESC, _("Sort Z-A"), _("Display items in descending order")) + + config = wx.ConfigBase_Get() + sort = config.ReadInt("OutlineSort", SORT_NONE) + if sort == SORT_NONE: + menu.Check(OutlineService.SORT_NONE, True) + elif sort == SORT_ASC: + menu.Check(OutlineService.SORT_ASC, True) + elif sort == SORT_DESC: + menu.Check(OutlineService.SORT_DESC, True) + + self.GetControl().PopupMenu(menu, event.GetPosition()) + menu.Destroy() + + + #---------------------------------------------------------------------------- + # Tree Methods + #---------------------------------------------------------------------------- + + def DoSelection(self, event): + if not self._actionOnSelect: + return + item = self.GetControl().GetSelection() + if item: + self.GetControl().CallDoSelectCallback(item) + + + def ResumeActionOnSelect(self): + self._actionOnSelect = True + + + def StopActionOnSelect(self): + self._actionOnSelect = False + + + def SetTreeCtrl(self, tree): + self.SetControl(tree) + wx.EVT_TREE_SEL_CHANGED(self.GetControl(), self.GetControl().GetId(), self.DoSelection) + wx.EVT_ENTER_WINDOW(self.GetControl(), treeCtrl.CallDoLoadOutlineCallback) + wx.EVT_RIGHT_DOWN(self.GetControl(), self.OnRightClick) + + + def GetTreeCtrl(self): + return self.GetControl() + + + def OnSort(self, sortOrder): + treeCtrl = self.GetControl() + treeCtrl.SetSortOrder(sortOrder) + treeCtrl.SortAllChildren(treeCtrl.GetRootItem()) + + + def ClearTreeCtrl(self): + if self.GetControl(): + self.GetControl().DeleteAllItems() + + + def GetExpansionState(self): + expanded = [] + + treeCtrl = self.GetControl() + if not treeCtrl: + return expanded + + parentItem = treeCtrl.GetRootItem() + + if not parentItem: + return expanded + + if not treeCtrl.IsExpanded(parentItem): + return expanded + + expanded.append(treeCtrl.GetItemText(parentItem)) + + (child, cookie) = treeCtrl.GetFirstChild(parentItem) + while child.IsOk(): + if treeCtrl.IsExpanded(child): + expanded.append(treeCtrl.GetItemText(child)) + (child, cookie) = treeCtrl.GetNextChild(parentItem, cookie) + return expanded + + + def SetExpansionState(self, expanded): + if not expanded or len(expanded) == 0: + return + + treeCtrl = self.GetControl() + parentItem = treeCtrl.GetRootItem() + if expanded[0] != treeCtrl.GetItemText(parentItem): + return + + (child, cookie) = treeCtrl.GetFirstChild(parentItem) + while child.IsOk(): + if treeCtrl.GetItemText(child) in expanded: + treeCtrl.Expand(child) + (child, cookie) = treeCtrl.GetNextChild(parentItem, cookie) + + # wxBug: This causes a crash, tried using ScrollTo which crashed as well. Then tried calling it with wx.CallAfter and that crashed as well, with both EnsureVisible and ScrollTo + # self.GetControl().EnsureVisible(self.GetControl().GetRootItem()) + # So doing the following massive hack which forces the treectrl to scroll up to the top item + treeCtrl.Collapse(parentItem) + treeCtrl.Expand(parentItem) + + +class OutlineTreeCtrl(wx.TreeCtrl): + """ Default Tree Control Class for OutlineView. + This class has the added functionality of sorting by the labels + """ + + + #---------------------------------------------------------------------------- + # Constants + #---------------------------------------------------------------------------- + ORIG_ORDER = 0 + VIEW = 1 + CALLBACKDATA = 2 + + + #---------------------------------------------------------------------------- + # Overridden Methods + #---------------------------------------------------------------------------- + + def __init__(self, parent, id, style=wx.TR_HAS_BUTTONS|wx.TR_DEFAULT_STYLE): + wx.TreeCtrl.__init__(self, parent, id, style = style) + self._origOrderIndex = 0 + self._sortOrder = SORT_NONE + + + def DeleteAllItems(self): + self._origOrderIndex = 0 + wx.TreeCtrl.DeleteAllItems(self) + + + #---------------------------------------------------------------------------- + # Sort Methods + #---------------------------------------------------------------------------- + + def SetSortOrder(self, sortOrder = SORT_NONE): + """ Sort Order constants are defined at top of file """ + self._sortOrder = sortOrder + + + def OnCompareItems(self, item1, item2): + if self._sortOrder == SORT_ASC: + return cmp(self.GetItemText(item1).lower(), self.GetItemText(item2).lower()) # sort A-Z + elif self._sortOrder == SORT_DESC: + return cmp(self.GetItemText(item2).lower(), self.GetItemText(item1).lower()) # sort Z-A + else: + return (self.GetPyData(item1)[self.ORIG_ORDER] > self.GetPyData(item2)[self.ORIG_ORDER]) # unsorted + + + def SortAllChildren(self, parentItem): + if parentItem and self.GetChildrenCount(parentItem, False): + self.SortChildren(parentItem) + (child, cookie) = self.GetFirstChild(parentItem) + while child.IsOk(): + self.SortAllChildren(child) + (child, cookie) = self.GetNextChild(parentItem, cookie) + + + #---------------------------------------------------------------------------- + # Select Callback Methods + #---------------------------------------------------------------------------- + + def CallDoSelectCallback(self, item): + """ Invoke the DoSelectCallback of the given view to highlight text in the document view + """ + data = self.GetPyData(item) + if not data: + return + + view = data[self.VIEW] + cbdata = data[self.CALLBACKDATA] + if view: + view.DoSelectCallback(cbdata) + + + def SelectClosestItem(self, position): + tree = self + distances = [] + items = [] + self.FindDistanceToTreeItems(tree.GetRootItem(), position, distances, items) + mindist = 1000000 + mindex = -1 + for index in range(0, len(distances)): + if distances[index] <= mindist: + mindist = distances[index] + mindex = index + if mindex != -1: + item = items[mindex] + self.EnsureVisible(item) + os_view = wx.GetApp().GetService(OutlineService).GetView() + if os_view: + os_view.StopActionOnSelect() + self.SelectItem(item) + if os_view: + os_view.ResumeActionOnSelect() + + + def FindDistanceToTreeItems(self, item, position, distances, items): + data = self.GetPyData(item) + this_dist = 1000000 + if data and data[2]: + positionTuple = data[2] + if position >= positionTuple[1]: + items.append(item) + distances.append(position - positionTuple[1]) + + if self.ItemHasChildren(item): + child, cookie = self.GetFirstChild(item) + while child and child.IsOk(): + self.FindDistanceToTreeItems(child, position, distances, items) + child, cookie = self.GetNextChild(item, cookie) + return False + + + def SetDoSelectCallback(self, item, view, callbackdata): + """ When an item in the outline view is selected, + a method is called to select the respective text in the document view. + The view must define the method DoSelectCallback(self, data) in order for this to work + """ + self.SetPyData(item, (self._origOrderIndex, view, callbackdata)) + self._origOrderIndex = self._origOrderIndex + 1 + + + def CallDoLoadOutlineCallback(self, event): + """ Invoke the DoLoadOutlineCallback + """ + rootItem = self.GetRootItem() + if rootItem: + data = self.GetPyData(rootItem) + if data: + view = data[self.VIEW] + if view and view.DoLoadOutlineCallback(): + self.SortAllChildren(self.GetRootItem()) + + + def GetCallbackView(self): + rootItem = self.GetRootItem() + if rootItem: + return self.GetPyData(rootItem)[self.VIEW] + else: + return None + + +class OutlineService(Service.Service): + + + #---------------------------------------------------------------------------- + # Constants + #---------------------------------------------------------------------------- + SHOW_WINDOW = wx.NewId() # keep this line for each subclass, need unique ID for each Service + SORT = wx.NewId() + SORT_ASC = wx.NewId() + SORT_DESC = wx.NewId() + SORT_NONE = wx.NewId() + + + #---------------------------------------------------------------------------- + # Overridden methods + #---------------------------------------------------------------------------- + + def __init__(self, serviceName, embeddedWindowLocation = wx.lib.pydocview.EMBEDDED_WINDOW_BOTTOM): + Service.Service.__init__(self, serviceName, embeddedWindowLocation) + self._validTemplates = [] + + + def _CreateView(self): + return OutlineView(self) + + + def InstallControls(self, frame, menuBar = None, toolBar = None, statusBar = None, document = None): + Service.Service.InstallControls(self, frame, menuBar, toolBar, statusBar, document) + + wx.EVT_MENU(frame, OutlineService.SORT_ASC, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, OutlineService.SORT_ASC, frame.ProcessUpdateUIEvent) + wx.EVT_MENU(frame, OutlineService.SORT_DESC, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, OutlineService.SORT_DESC, frame.ProcessUpdateUIEvent) + wx.EVT_MENU(frame, OutlineService.SORT_NONE, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, OutlineService.SORT_NONE, frame.ProcessUpdateUIEvent) + + + if wx.GetApp().GetDocumentManager().GetFlags() & wx.lib.docview.DOC_SDI: + return True + + viewMenu = menuBar.GetMenu(menuBar.FindMenu(_("&View"))) + self._outlineSortMenu = wx.Menu() + self._outlineSortMenu.AppendRadioItem(OutlineService.SORT_NONE, _("Unsorted"), _("Display items in original order")) + self._outlineSortMenu.AppendRadioItem(OutlineService.SORT_ASC, _("Sort A-Z"), _("Display items in ascending order")) + self._outlineSortMenu.AppendRadioItem(OutlineService.SORT_DESC, _("Sort Z-A"), _("Display items in descending order")) + viewMenu.AppendMenu(wx.NewId(), _("Outline Sort"), self._outlineSortMenu) + + return True + + + #---------------------------------------------------------------------------- + # Event Processing Methods + #---------------------------------------------------------------------------- + + def ProcessEvent(self, event): + if Service.Service.ProcessEvent(self, event): + return True + + id = event.GetId() + if id == OutlineService.SORT_ASC: + self.OnSort(event) + return True + elif id == OutlineService.SORT_DESC: + self.OnSort(event) + return True + elif id == OutlineService.SORT_NONE: + self.OnSort(event) + return True + else: + return False + + + def ProcessUpdateUIEvent(self, event): + if Service.Service.ProcessUpdateUIEvent(self, event): + return True + + id = event.GetId() + if id == OutlineService.SORT_ASC: + event.Enable(True) + + config = wx.ConfigBase_Get() + sort = config.ReadInt("OutlineSort", SORT_NONE) + if sort == SORT_ASC: + self._outlineSortMenu.Check(OutlineService.SORT_ASC, True) + else: + self._outlineSortMenu.Check(OutlineService.SORT_ASC, False) + + return True + elif id == OutlineService.SORT_DESC: + event.Enable(True) + + config = wx.ConfigBase_Get() + sort = config.ReadInt("OutlineSort", SORT_NONE) + if sort == SORT_DESC: + self._outlineSortMenu.Check(OutlineService.SORT_DESC, True) + else: + self._outlineSortMenu.Check(OutlineService.SORT_DESC, False) + + return True + elif id == OutlineService.SORT_NONE: + event.Enable(True) + + config = wx.ConfigBase_Get() + sort = config.ReadInt("OutlineSort", SORT_NONE) + if sort == SORT_NONE: + self._outlineSortMenu.Check(OutlineService.SORT_NONE, True) + else: + self._outlineSortMenu.Check(OutlineService.SORT_NONE, False) + + return True + else: + return False + + + def OnSort(self, event): + id = event.GetId() + if id == OutlineService.SORT_ASC: + wx.ConfigBase_Get().WriteInt("OutlineSort", SORT_ASC) + self.GetView().OnSort(SORT_ASC) + return True + elif id == OutlineService.SORT_DESC: + wx.ConfigBase_Get().WriteInt("OutlineSort", SORT_DESC) + self.GetView().OnSort(SORT_DESC) + return True + elif id == OutlineService.SORT_NONE: + wx.ConfigBase_Get().WriteInt("OutlineSort", SORT_NONE) + self.GetView().OnSort(SORT_NONE) + return True + + + #---------------------------------------------------------------------------- + # Service specific methods + #---------------------------------------------------------------------------- + + def LoadOutline(self, view, position=-1, force=False): + if not self.GetView(): + return + + self.SaveExpansionState() + if view.DoLoadOutlineCallback(force=force): + self.GetView().OnSort(wx.ConfigBase_Get().ReadInt("OutlineSort", SORT_NONE)) + self.LoadExpansionState() + if position >= 0: + self.SyncToPosition(position) + + + def SyncToPosition(self, position): + if not self.GetView(): + return + self.GetView().GetTreeCtrl().SelectClosestItem(position) + + + def OnCloseFrame(self, event): + Service.Service.OnCloseFrame(self, event) + self.SaveExpansionState(clear = True) + + return True + + + def SaveExpansionState(self, clear = False): + if clear: + expanded = [] + elif self.GetView(): + expanded = self.GetView().GetExpansionState() + wx.ConfigBase_Get().Write("OutlineLastExpanded", expanded.__repr__()) + + + def LoadExpansionState(self): + expanded = wx.ConfigBase_Get().Read("OutlineLastExpanded") + if expanded: + self.GetView().SetExpansionState(eval(expanded)) + + + #---------------------------------------------------------------------------- + # Timer Methods + #---------------------------------------------------------------------------- + + def StartBackgroundTimer(self): + self._timer = wx.PyTimer(self.DoBackgroundRefresh) + self._timer.Start(250) + + + def DoBackgroundRefresh(self): + """ Refresh the outline view periodically """ + self._timer.Stop() + + foundRegisteredView = False + if self.GetView(): + currView = wx.GetApp().GetDocumentManager().GetCurrentView() + if currView: + for template in self._validTemplates: + type = template.GetViewType() + if isinstance(currView, type): + self.LoadOutline(currView) + foundRegisteredView = True + break + + if not foundRegisteredView: + self.GetView().ClearTreeCtrl() + + self._timer.Start(1000) # 1 second interval + + + def AddTemplateForBackgroundHandler(self, template): + self._validTemplates.append(template) + + + def GetTemplatesForBackgroundHandler(self): + return self._validTemplates + + + def RemoveTemplateForBackgroundHandler(self, template): + self._validTemplates.remove(template) + diff --git a/wxPython/samples/ide/activegrid/tool/PHPEditor.py b/wxPython/samples/ide/activegrid/tool/PHPEditor.py new file mode 100644 index 0000000000..bf16594d3f --- /dev/null +++ b/wxPython/samples/ide/activegrid/tool/PHPEditor.py @@ -0,0 +1,297 @@ +#---------------------------------------------------------------------------- +# Name: PHPEditor.py +# Purpose: PHP Script Editor for pydocview tbat uses the Styled Text Control +# +# Author: Morgan Hua +# +# Created: 1/4/04 +# CVS-ID: $Id$ +# Copyright: (c) 2005 ActiveGrid, Inc. +# License: wxWindows License +#---------------------------------------------------------------------------- + +import wx +import string +import STCTextEditor +import CodeEditor +import OutlineService +import os +import re + + +class PHPDocument(CodeEditor.CodeDocument): + + pass + + +class PHPView(CodeEditor.CodeView): + + + def GetCtrlClass(self): + """ Used in split window to instantiate new instances """ + return PHPCtrl + + + def GetAutoCompleteHint(self): + pos = self.GetCtrl().GetCurrentPos() + if pos == 0: + return None, None + + validLetters = string.letters + string.digits + '_$' + word = '' + while (True): + pos = pos - 1 + if pos < 0: + break + char = chr(self.GetCtrl().GetCharAt(pos)) + if char not in validLetters: + break + word = char + word + + return None, word + + + def GetAutoCompleteDefaultKeywords(self): + return PHPKEYWORDS + + #---------------------------------------------------------------------------- + # Methods for OutlineService + #---------------------------------------------------------------------------- + + def DoLoadOutlineCallback(self, force=False): + outlineService = wx.GetApp().GetService(OutlineService.OutlineService) + if not outlineService: + return False + + outlineView = outlineService.GetView() + if not outlineView: + return False + + treeCtrl = outlineView.GetTreeCtrl() + if not treeCtrl: + return False + + view = treeCtrl.GetCallbackView() + newCheckSum = self.GenCheckSum() + if not force: + if view and view is self: + if self._checkSum == newCheckSum: + return False + self._checkSum = newCheckSum + + treeCtrl.DeleteAllItems() + + document = self.GetDocument() + if not document: + return True + + filename = document.GetFilename() + if filename: + rootItem = treeCtrl.AddRoot(os.path.basename(filename)) + treeCtrl.SetDoSelectCallback(rootItem, self, None) + else: + return True + + text = self.GetValue() + if not text: + return True + + INTERFACE_PATTERN = 'interface[ \t]+\w+' + CLASS_PATTERN = '((final|abstract)[ \t]+)?((public|private|protected)[ \t]+)?(static[ \t]+)?class[ \t]+\w+((implements|extends)\w+)?' + FUNCTION_PATTERN = '(abstract[ \t]+)?((public|private|protected)[ \t]+)?(static[ \t]+)?function[ \t]+?\w+\(.*?\)' + interfacePat = re.compile(INTERFACE_PATTERN, re.M|re.S) + classPat = re.compile(CLASS_PATTERN, re.M|re.S) + funcPat= re.compile(FUNCTION_PATTERN, re.M|re.S) + pattern = re.compile('^[ \t]*('+ CLASS_PATTERN + '.*?{|' + FUNCTION_PATTERN + '|' + INTERFACE_PATTERN +'\s*?{).*?$', re.M|re.S) + + iter = pattern.finditer(text) + indentStack = [(0, rootItem)] + for pattern in iter: + line = pattern.string[pattern.start(0):pattern.end(0)] + foundLine = classPat.search(line) + if foundLine: + indent = foundLine.start(0) + itemStr = foundLine.string[foundLine.start(0):foundLine.end(0)] + else: + foundLine = funcPat.search(line) + if foundLine: + indent = foundLine.start(0) + itemStr = foundLine.string[foundLine.start(0):foundLine.end(0)] + else: + foundLine = interfacePat.search(line) + if foundLine: + indent = foundLine.start(0) + itemStr = foundLine.string[foundLine.start(0):foundLine.end(0)] + + if indent == 0: + parentItem = rootItem + else: + lastItem = indentStack.pop() + while lastItem[0] >= indent: + lastItem = indentStack.pop() + indentStack.append(lastItem) + parentItem = lastItem[1] + + item = treeCtrl.AppendItem(parentItem, itemStr) + treeCtrl.SetDoSelectCallback(item, self, (pattern.end(0), pattern.start(0) + indent)) # select in reverse order because we want the cursor to be at the start of the line so it wouldn't scroll to the right + indentStack.append((indent, item)) + + treeCtrl.Expand(rootItem) + + return True + + +class PHPService(CodeEditor.CodeService): + + + def __init__(self): + CodeEditor.CodeService.__init__(self) + + +class PHPCtrl(CodeEditor.CodeCtrl): + + + def __init__(self, parent, ID = -1, style = wx.NO_FULL_REPAINT_ON_RESIZE): + CodeEditor.CodeCtrl.__init__(self, parent, ID, style) + self.SetLexer(wx.stc.STC_LEX_HTML) + self.SetStyleBits(7) + self.SetKeyWords(4, string.join(PHPKEYWORDS)) + self.SetProperty("fold.html", "1") + + + def CanWordWrap(self): + return True + + + def SetViewDefaults(self): + CodeEditor.CodeCtrl.SetViewDefaults(self, configPrefix = "PHP", hasWordWrap = True, hasTabs = True) + + + def GetFontAndColorFromConfig(self): + return CodeEditor.CodeCtrl.GetFontAndColorFromConfig(self, configPrefix = "PHP") + + + def UpdateStyles(self): + CodeEditor.CodeCtrl.UpdateStyles(self) + + if not self.GetFont(): + return + + faces = { 'font' : self.GetFont().GetFaceName(), + 'size' : self.GetFont().GetPointSize(), + 'size2': self.GetFont().GetPointSize() - 2, + 'color' : "%02x%02x%02x" % (self.GetFontColor().Red(), self.GetFontColor().Green(), self.GetFontColor().Blue()) + } + + + # HTML Styles + # White space + self.StyleSetSpec(wx.stc.STC_H_DEFAULT, "face:%(font)s,fore:#000000,face:%(font)s,size:%(size)d" % faces) + # Comment + self.StyleSetSpec(wx.stc.STC_H_COMMENT, "face:%(font)s,fore:#007F00,italic,face:%(font)s,size:%(size)d" % faces) + # Number + self.StyleSetSpec(wx.stc.STC_H_NUMBER, "face:%(font)s,fore:#007F7F,size:%(size)d" % faces) + # String + self.StyleSetSpec(wx.stc.STC_H_SINGLESTRING, "face:%(font)s,fore:#7F007F,face:%(font)s,size:%(size)d" % faces) + self.StyleSetSpec(wx.stc.STC_H_DOUBLESTRING, "face:%(font)s,fore:#7F007F,face:%(font)s,size:%(size)d" % faces) + # Tag + self.StyleSetSpec(wx.stc.STC_H_TAG, "face:%(font)s,fore:#00007F,bold,size:%(size)d" % faces) + # Attributes + self.StyleSetSpec(wx.stc.STC_H_ATTRIBUTE, "face:%(font)s,fore:#00007F,bold,size:%(size)d" % faces) + + + # PHP Styles + self.StyleSetSpec(wx.stc.STC_HPHP_DEFAULT, "face:%(font)s,fore:#000000,face:%(font)s,size:%(size)d" % faces) + self.StyleSetSpec(wx.stc.STC_HPHP_COMMENT, "face:%(font)s,fore:#007F00,italic,face:%(font)s,size:%(size)d" % faces) + self.StyleSetSpec(wx.stc.STC_HPHP_COMMENTLINE, "face:%(font)s,fore:#007F00,italic,face:%(font)s,size:%(size)d" % faces) + self.StyleSetSpec(wx.stc.STC_HPHP_NUMBER, "face:%(font)s,fore:#007F7F,size:%(size)d" % faces) + self.StyleSetSpec(wx.stc.STC_HPHP_SIMPLESTRING, "face:%(font)s,fore:#7F007F,size:%(size)d" % faces) + self.StyleSetSpec(wx.stc.STC_HPHP_HSTRING, "face:%(font)s,fore7F007F,face:%(font)s,size:%(size)d" % faces) + self.StyleSetSpec(wx.stc.STC_HPHP_HSTRING_VARIABLE, "face:%(font)s,fore:#007F7F,italic,bold,face:%(font)s,size:%(size)d" % faces) + self.StyleSetSpec(wx.stc.STC_HPHP_VARIABLE, "face:%(font)s,fore:#000000,face:%(font)s,size:%(size)d" % faces) + self.StyleSetSpec(wx.stc.STC_HPHP_OPERATOR, "face:%(font)s,size:%(size)d" % faces) + self.StyleSetSpec(wx.stc.STC_HPHP_WORD, "face:%(font)s,fore:#00007F,bold,size:%(size)d" % faces) # keyword + + +class PHPOptionsPanel(STCTextEditor.TextOptionsPanel): + + def __init__(self, parent, id): + STCTextEditor.TextOptionsPanel.__init__(self, parent, id, configPrefix = "PHP", label = "PHP", hasWordWrap = True, hasTabs = True) + + +PHPKEYWORDS = [ + "and", "or", "xor", "__FILE__", "exception", "__LINE__", "array", "as", "break", "case", + "class", "const", "continue", "declare", "default", "die", "do", "echo", "else", "elseif", + "empty", "enddeclare", "endfor", "endforeach", "endif", "endswith", "endwhile", "eval", + "exit", "extends", "for", "foreach", "function", "global", "if", "include", "include_once", + "isset", "list", "new", "print", "require", "require_once", "return", "static", "switch", + "unset", "use", "var", "while", "__FUNCTION__", "__CLASS__", "__METHOD__", "final", "php_user_filter", + "interface", "implements", "extends", "public", "private", "protected", "abstract", "clone", "try", "catch", + "throw", "cfunction", "old_function", + "$_SERVER", "$_ENV", "$_COOKIE", "$_GET", "$_POST", "$_FILES", "$_REQUEST", "$_SESSION", "$GLOBALS", "$php_errormsg", + "PHP_VERSION", "PHP_OS", "PHP_EOL", "DEFAULT_INCLUDE_PATH", "PEAR_INSTALL_DIR", "PEAR_EXTENSION_DIR", + "PHP_EXTENSION_DIR", "PHP_BINDIR", "PHP_LIBDIR", "PHP_DATADIR", "PHP_SYSCONFDIR", "PHP_LOCALSTATEDIR", + "PHP_CONFIG_FILE_PATH", "PHP_OUTPUT_HANDLER_START", "PHP_OUTPUT_HANDLER_CONT", "PHP_OUTPUT_HANDLER_END", + "E_ERROR", "E_WARNING", "E_PARSE", "E_NOTICE", "E_CORE_ERROR", "E_CORE_WARNING", "E_COMPILE_ERROR", + "E_COMPILE_WARNING", "E_USER_ERROR", "E_USER_WARNING", "E_USER_NOTICE", "E_ALL", "E_STRICT", + "TRUE", "FALSE", "NULL", "ZEND_THREAD_SAFE", + "EXTR_OVERWRITE", "EXTR_SKIP", "EXTR_PREFIX_SAME", "EXTR_PREFIX_ALL", "EXTR_PREFIX_INVALID", + "EXTR_PREFIX_IF_EXISTS", "EXTR_IF_EXISTS", "SORT_ASC", "SORT_DESC", "SORT_REGULAR", "SORT_NUMERIC", + "SORT_STRING", "CASE_LOWER", "CASE_UPPER", "COUNT_NORMAL", "COUNT_RECURSIVE", "ASSERT_ACTIVE", + "ASSERT_CALLBACK", "ASSERT_BAIL", "ASSERT_WARNING", "ASSERT_QUIET_EVAL", "CONNECTION_ABORTED", + "CONNECTION_NORMAL", "CONNECTION_TIMEOUT", "INI_USER", "INI_PERDIR", "INI_SYSTEM", "INI_ALL", + "M_E", "M_LOG2E", "M_LOG10E", "M_LN2", "M_LN10", "M_PI", "M_PI_2", "M_PI_4", "M_1_PI", "M_2_PI", + "M_2_SQRTPI", "M_SQRT2", "M_SQRT1_2", "CRYPT_SALT_LENGTH", "CRYPT_STD_DES", "CRYPT_EXT_DES", "CRYPT_MD5", + "CRYPT_BLOWFISH", "DIRECTORY_SEPARATOR", "SEEK_SET", "SEEK_CUR", "SEEK_END", "LOCK_SH", "LOCK_EX", "LOCK_UN", + "LOCK_NB", "HTML_SPECIALCHARS", "HTML_ENTITIES", "ENT_COMPAT", "ENT_QUOTES", "ENT_NOQUOTES", "INFO_GENERAL", + "INFO_CREDITS", "INFO_CONFIGURATION", "INFO_MODULES", "INFO_ENVIRONMENT", "INFO_VARIABLES", "INFO_LICENSE", + "INFO_ALL", "CREDITS_GROUP", "CREDITS_GENERAL", "CREDITS_SAPI", "CREDITS_MODULES", "CREDITS_DOCS", + "CREDITS_FULLPAGE", "CREDITS_QA", "CREDITS_ALL", "STR_PAD_LEFT", "STR_PAD_RIGHT", "STR_PAD_BOTH", + "PATHINFO_DIRNAME", "PATHINFO_BASENAME", "PATHINFO_EXTENSION", "PATH_SEPARATOR", "CHAR_MAX", "LC_CTYPE", + "LC_NUMERIC", "LC_TIME", "LC_COLLATE", "LC_MONETARY", "LC_ALL", "LC_MESSAGES", "ABDAY_1", "ABDAY_2", + "ABDAY_3", "ABDAY_4", "ABDAY_5", "ABDAY_6", "ABDAY_7", "DAY_1", "DAY_2", "DAY_3", "DAY_4", "DAY_5", + "DAY_6", "DAY_7", "ABMON_1", "ABMON_2", "ABMON_3", "ABMON_4", "ABMON_5", "ABMON_6", "ABMON_7", "ABMON_8", + "ABMON_9", "ABMON_10", "ABMON_11", "ABMON_12", "MON_1", "MON_2", "MON_3", "MON_4", "MON_5", "MON_6", "MON_7", + "MON_8", "MON_9", "MON_10", "MON_11", "MON_12", "AM_STR", "PM_STR", "D_T_FMT", "D_FMT", "T_FMT", "T_FMT_AMPM", + "ERA", "ERA_YEAR", "ERA_D_T_FMT", "ERA_D_FMT", "ERA_T_FMT", "ALT_DIGITS", "INT_CURR_SYMBOL", "CURRENCY_SYMBOL", + "CRNCYSTR", "MON_DECIMAL_POINT", "MON_THOUSANDS_SEP", "MON_GROUPING", "POSITIVE_SIGN", "NEGATIVE_SIGN", + "INT_FRAC_DIGITS", "FRAC_DIGITS", "P_CS_PRECEDES", "P_SEP_BY_SPACE", "N_CS_PRECEDES", "N_SEP_BY_SPACE", + "P_SIGN_POSN", "N_SIGN_POSN", "DECIMAL_POINT", "RADIXCHAR", "THOUSANDS_SEP", "THOUSEP", "GROUPING", + "YESEXPR", "NOEXPR", "YESSTR", "NOSTR", "CODESET", "LOG_EMERG", "LOG_ALERT", "LOG_CRIT", "LOG_ERR", + "LOG_WARNING", "LOG_NOTICE", "LOG_INFO", "LOG_DEBUG", "LOG_KERN", "LOG_USER", "LOG_MAIL", "LOG_DAEMON", + "LOG_AUTH", "LOG_SYSLOG", "LOG_LPR", "LOG_NEWS", "LOG_UUCP", "LOG_CRON", "LOG_AUTHPRIV", "LOG_LOCAL0", + "LOG_LOCAL1", "LOG_LOCAL2", "LOG_LOCAL3", "LOG_LOCAL4", "LOG_LOCAL5", "LOG_LOCAL6", "LOG_LOCAL7", + "LOG_PID", "LOG_CONS", "LOG_ODELAY", "LOG_NDELAY", "LOG_NOWAIT", "LOG_PERROR" + ] + + +#---------------------------------------------------------------------------- +# Icon Bitmaps - generated by encode_bitmaps.py +#---------------------------------------------------------------------------- +from wx import ImageFromStream, BitmapFromImage +from wx import EmptyIcon +import cStringIO + + +def getPHPData(): + return \ +'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x10\x00\x00\x00\x10\x08\x06\ +\x00\x00\x00\x1f\xf3\xffa\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\ +\x00\x00{IDAT8\x8dclh8\xf0\x9f\x81\x02\xc0D\x89f\xaa\x18\xc0\x82M0<\\\x1c\ +\xce^\xb9\xf2%y.\xd0\xd4\xd4$\xde\x05\xf8l\x0c\x0f\x17\x87\x8baS\xc7x\xfd\ +\xfa\xf5\xff\xc8\xb6]\xbf~\x1d\xc3\x05\xf8\xc4\x98\x90\x05\xae_\xbf\x8e\xa1\ +\x88\x90\xd8 \x8aF\x98\x93`~\xc3\x05\xd0\xd5\xc1\r\x80\t\xc0B\xf7\xfa\xf5\ +\xeb(l\\\xeaP\xbc\x80\x1c\x85\xb8\xd8\xe8|&b\x9c\x8dn;2`\x1c\xf0\xdc\x08\x00\ +\x8e\xf2S\xed\xb0\xbe\xaa\xbc\x00\x00\x00\x00IEND\xaeB`\x82' + +def getPHPBitmap(): + return BitmapFromImage(getPHPImage()) + +def getPHPImage(): + stream = cStringIO.StringIO(getPHPData()) + return ImageFromStream(stream) + +def getPHPIcon(): + icon = EmptyIcon() + icon.CopyFromBitmap(getPHPBitmap()) + return icon diff --git a/wxPython/samples/ide/activegrid/tool/PerlEditor.py b/wxPython/samples/ide/activegrid/tool/PerlEditor.py new file mode 100644 index 0000000000..e09f1356f4 --- /dev/null +++ b/wxPython/samples/ide/activegrid/tool/PerlEditor.py @@ -0,0 +1,425 @@ +#---------------------------------------------------------------------------- +# Name: PerlEditor.py +# Purpose: Perl Script Editor for pydocview tbat uses the Styled Text Control +# +# Author: Morgan Hua +# +# Created: 1/5/04 +# CVS-ID: $Id$ +# Copyright: (c) 2005 ActiveGrid, Inc. +# License: wxWindows License +#---------------------------------------------------------------------------- + +import wx +import string +import STCTextEditor +import CodeEditor + + +class PerlDocument(CodeEditor.CodeDocument): + + pass + + +class PerlView(CodeEditor.CodeView): + + + def GetCtrlClass(self): + """ Used in split window to instantiate new instances """ + return PerlCtrl + + + def GetAutoCompleteHint(self): + pos = self.GetCtrl().GetCurrentPos() + if pos == 0: + return None, None + + validLetters = string.letters + string.digits + '_/' + word = '' + while (True): + pos = pos - 1 + if pos < 0: + break + char = chr(self.GetCtrl().GetCharAt(pos)) + if char not in validLetters: + break + word = char + word + + return None, word + + + def GetAutoCompleteDefaultKeywords(self): + return PERLKEYWORDS + + +class PerlService(CodeEditor.CodeService): + + + def __init__(self): + CodeEditor.CodeService.__init__(self) + + +class PerlCtrl(CodeEditor.CodeCtrl): + + + def __init__(self, parent, ID = -1, style = wx.NO_FULL_REPAINT_ON_RESIZE): + CodeEditor.CodeCtrl.__init__(self, parent, ID, style) + self.SetLexer(wx.stc.STC_LEX_PERL) + self.SetKeyWords(0, string.join(PERLKEYWORDS)) + + + def CanWordWrap(self): + return True + + + def SetViewDefaults(self): + CodeEditor.CodeCtrl.SetViewDefaults(self, configPrefix = "Perl", hasWordWrap = True, hasTabs = True) + + + def GetFontAndColorFromConfig(self): + return CodeEditor.CodeCtrl.GetFontAndColorFromConfig(self, configPrefix = "Perl") + + + def UpdateStyles(self): + CodeEditor.CodeCtrl.UpdateStyles(self) + + if not self.GetFont(): + return + + faces = { 'font' : self.GetFont().GetFaceName(), + 'size' : self.GetFont().GetPointSize(), + 'size2': self.GetFont().GetPointSize() - 2, + 'color' : "%02x%02x%02x" % (self.GetFontColor().Red(), self.GetFontColor().Green(), self.GetFontColor().Blue()) + } + + # Perl Styles + self.StyleSetSpec(wx.stc.STC_PL_DEFAULT, "face:%(font)s,fore:#000000,face:%(font)s,size:%(size)d" % faces) + self.StyleSetSpec(wx.stc.STC_PL_COMMENTLINE, "face:%(font)s,fore:#007F00,italic,face:%(font)s,size:%(size)d" % faces) + self.StyleSetSpec(wx.stc.STC_PL_NUMBER, "face:%(font)s,fore:#007F7F,size:%(size)d" % faces) + self.StyleSetSpec(wx.stc.STC_PL_CHARACTER, "face:%(font)s,fore:#7F007F,face:%(font)s,size:%(size)d" % faces) + self.StyleSetSpec(wx.stc.STC_PL_STRING, "face:%(font)s,fore:#7F007F,face:%(font)s,size:%(size)d" % faces) + self.StyleSetSpec(wx.stc.STC_PL_STRING_Q, "face:%(font)s,fore:#7F007F,face:%(font)s,size:%(size)d" % faces) + self.StyleSetSpec(wx.stc.STC_PL_STRING_QQ, "face:%(font)s,fore:#7F007F,face:%(font)s,size:%(size)d" % faces) + self.StyleSetSpec(wx.stc.STC_PL_STRING_QX, "face:%(font)s,fore:#7F007F,face:%(font)s,size:%(size)d" % faces) + self.StyleSetSpec(wx.stc.STC_PL_STRING_QR, "face:%(font)s,fore:#7F007F,face:%(font)s,size:%(size)d" % faces) + self.StyleSetSpec(wx.stc.STC_PL_STRING_QW, "face:%(font)s,fore:#7F007F,face:%(font)s,size:%(size)d" % faces) + self.StyleSetSpec(wx.stc.STC_PL_BACKTICKS, "face:%(font)s,fore:#7F007F,face:%(font)s,size:%(size)d" % faces) + self.StyleSetSpec(wx.stc.STC_PL_WORD, "face:%(font)s,fore:#00007F,bold,size:%(size)d" % faces) # keyword + self.StyleSetSpec(wx.stc.STC_PL_IDENTIFIER, "face:%(font)s,fore:#%(color)s,face:%(font)s,size:%(size)d" % faces) + + # Default + self.StyleSetSpec(wx.stc.STC_PL_ARRAY, "face:%(font)s,fore:#000000,face:%(font)s,size:%(size)d" % faces) + self.StyleSetSpec(wx.stc.STC_PL_DATASECTION, "face:%(font)s,fore:#000000,face:%(font)s,size:%(size)d" % faces) + self.StyleSetSpec(wx.stc.STC_PL_ERROR, "face:%(font)s,fore:#000000,face:%(font)s,size:%(size)d" % faces) + self.StyleSetSpec(wx.stc.STC_PL_HASH, "face:%(font)s,fore:#000000,face:%(font)s,size:%(size)d" % faces) + self.StyleSetSpec(wx.stc.STC_PL_HERE_DELIM, "face:%(font)s,fore:#000000,face:%(font)s,size:%(size)d" % faces) + self.StyleSetSpec(wx.stc.STC_PL_HERE_Q, "face:%(font)s,fore:#000000,face:%(font)s,size:%(size)d" % faces) + self.StyleSetSpec(wx.stc.STC_PL_HERE_QQ, "face:%(font)s,fore:#000000,face:%(font)s,size:%(size)d" % faces) + self.StyleSetSpec(wx.stc.STC_PL_HERE_QX, "face:%(font)s,fore:#000000,face:%(font)s,size:%(size)d" % faces) + self.StyleSetSpec(wx.stc.STC_PL_LONGQUOTE, "face:%(font)s,fore:#000000,face:%(font)s,size:%(size)d" % faces) + self.StyleSetSpec(wx.stc.STC_PL_OPERATOR, "face:%(font)s,fore:#000000,face:%(font)s,size:%(size)d" % faces) + self.StyleSetSpec(wx.stc.STC_PL_POD, "face:%(font)s,fore:#000000,face:%(font)s,size:%(size)d" % faces) + self.StyleSetSpec(wx.stc.STC_PL_PREPROCESSOR, "face:%(font)s,fore:#000000,face:%(font)s,size:%(size)d" % faces) + self.StyleSetSpec(wx.stc.STC_PL_PUNCTUATION, "face:%(font)s,fore:#000000,face:%(font)s,size:%(size)d" % faces) + self.StyleSetSpec(wx.stc.STC_PL_REGEX, "face:%(font)s,fore:#000000,face:%(font)s,size:%(size)d" % faces) + self.StyleSetSpec(wx.stc.STC_PL_REGSUBST, "face:%(font)s,fore:#000000,face:%(font)s,size:%(size)d" % faces) + self.StyleSetSpec(wx.stc.STC_PL_SCALAR, "face:%(font)s,fore:#000000,face:%(font)s,size:%(size)d" % faces) + self.StyleSetSpec(wx.stc.STC_PL_SYMBOLTABLE, "face:%(font)s,fore:#000000,face:%(font)s,size:%(size)d" % faces) + + +class PerlOptionsPanel(STCTextEditor.TextOptionsPanel): + + def __init__(self, parent, id): + STCTextEditor.TextOptionsPanel.__init__(self, parent, id, configPrefix = "Perl", label = "Perl", hasWordWrap = True, hasTabs = True) + + +PERLKEYWORDS = [ + "abs", + "accept", + "alarm", + "atan2", + "bind", + "binmode", + "bless", + "caller", + "chdir", + "chmod", + "chomp", + "chop", + "chown", + "chr", + "chroot", + "close", + "closedir", + "connect", + "continue", + "cos", + "crypt", + "dbmclose", + "dbmopen", + "defined", + "delete", + "die", + "do", + "dump", + "each", + "endgrent", + "endhostent", + "endnetent", + "endprotoent", + "endpwent", + "endservent", + "eof", + "eval", + "exec", + "exists", + "exit", + "exp", + "fcntl", + "fileno", + "flock", + "fork", + "format", + "formline", + "getc", + "getgrent", + "getgrgid", + "getgrnam", + "gethostbyaddr", + "gethostbyname", + "gethostent", + "getlogin", + "getnetbyaddr", + "getnetbyname", + "getnetent", + "getpeername", + "getpgrp", + "getppid", + "getpriority", + "getprotobyname", + "getprotobynumber", + "getprotoent", + "getpwent", + "getpwnam", + "getpwuid", + "getservbyname", + "getservbyport", + "getservent", + "getsockname", + "getsockopt", + "glob", + "gmtime", + "goto", + "grep", + "hex", + "import", + "index", + "int", + "ioctl", + "join", + "keys", + "kill", + "last", + "lc", + "lcfirst", + "length", + "link", + "listen", + "local", + "localtime", + "log", + "lstat", + "m//", + "map", + "mkdir", + "msgctl", + "msgget", + "msgrcv", + "msgsnd", + "my", + "next", + "no", + "oct", + "open", + "opendir", + "ord", + "pack", + "package", + "pipe", + "pop", + "pos", + "print", + "printf", + "prototype", + "push", + "q/STRING/", + "qq/STRING/", + "quotemeta", + "qw", + "qw/STRING/", + "qx", + "qx/STRING/", + "rand", + "read", + "readdir", + "readline", + "readlink", + "readpipe", + "recv", + "redo", + "ref", + "rename", + "require", + "reset", + "return", + "reverse", + "rewinddir", + "rindex", + "rmdir", + "s///", + "scalar", + "seek", + "seekdir", + "select", + "semctl", + "semget", + "semop", + "send", + "setgrent", + "sethostent", + "setnetent", + "setpgrp", + "setpriority", + "setprotoent", + "setpwent", + "setservent", + "setsockopt", + "shift", + "shmctl", + "shmget", + "shmread", + "shmwrite", + "shutdown", + "sin", + "sleep", + "socket", + "socketpair", + "sort", + "splice", + "split", + "sprintf", + "sqrt", + "srand", + "stat", + "study", + "sub", + "substr", + "symlink", + "syscall", + "sysopen", + "sysread", + "sysseek", + "system", + "syswrite", + "tell", + "telldir", + "tie", + "tied", + "times", + "tr///", + "truncate", + "uc", + "ucfirst", + "umask", + "undef", + "unlink", + "unpack", + "unshift", + "untie", + "use", + "utime", + "values", + "vec", + "wait", + "waitpid", + "wantarray", + "warn", + "write", + "y///", + "eq", + "ne", + "lt", + "le", + "gt", + "ge", + "cmp", + "if", + "else" + "not", + "and", + "xor", + "or", + "if", + "while", + "until", + "for", + "foreach", + "last", + "next", + "redo", + "goto", + "STDIN", + "STDOUT", + "STDERR", + "WHEncE", + "BEGIN", + "END", + "require", + "integer", + "less", + "sigtrap", + "strict", + "subs" + ] + + +#---------------------------------------------------------------------------- +# Icon Bitmaps - generated by encode_bitmaps.py +#---------------------------------------------------------------------------- +from wx import ImageFromStream, BitmapFromImage +from wx import EmptyIcon +import cStringIO + + +def getPerlData(): + return \ +'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x0e\x00\x00\x00\x10\x08\x06\ +\x00\x00\x00&\x94N:\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\x00\ +\x01mIDAT(\x91\x9d\x93/\x8e\xf30\x10\xc5\x7f[\xed\x05\xca\x82,Ef\xc6e\x01Vx{\ +\x89\x04\x16\x94\x97\x045\xa8\xa0\xa44G\x08\xc9\x01\xa2\x80\x1e $ld\xc9\xa8w\ +\x08\xf0\xa2x\x93~\x1f\xda\x91F\xb2\xfd\xe6=\xcd?\x7f\xcd\xf3\x1c\xf8\x83}/\ +\x87i\x9a\x000\xc6D\xb0\xeb:\x86a MS\xce\xe7\xf3\x968M\x13}\xdf\xe3\x9cCD\ +\xb8\xddn\x18c\xe8\xba\x8e\xb2,\xc9\xb2\x0c\x11\x01\xd8\x90w\xc6\x18\x94R\ +\xf1\xa1\xef{\xba\xae\xa3i\x1a\xb4\xd6h\xad)\x8a\x02\xe7\\\xccj\x93\xea\xa2\ +\nP\xd75\xd7\xeb\x15\x00\xef\xfdFt)e\xb7\x80\x8b\xbas\x8e$I\xe2}\xc1\x8b\xa2\ +\xd8\xf4b\x07\xa0\x94\xe2\xf5za\xad\xc5Z\xcb\xfb\xfdFD\xe8\xfb\x9e\x05\x17\ +\x11\x9cs4M\xf3K<\x9dNdY\xc60\x0cx\xef\x11\x11\xea\xbaF)\x85s\x8e\xba\xae)\ +\xcb\x12\x11!M\xd3_"\xc0\xfd~\xc7Z\x8bs\x0e\x80$I\xa2:@UU1u\x00\xe6y\x0ek\ +\x1f\xc71\x1c\x0e\x87\xd0\xb6m\xd8\xef\xf7\xe1\xf1x\x84\xcb\xe5\x12\xe6y\x0e\ +\xc7\xe31\xc6\xed\xf80\x11!\xcb2\xbc\xf7TUE\x9e\xe71=\xadul\xce\xf7\'Qk\x8d\ +\xf7\x9e<\xcf\x81\xed&Yk\xb7\xe3\xf84\xa5\x14\xc6\x18D\x84\xe7\xf3\x19\x83\ +\xd75\xfe\x97\xb8\x0eXo\xcc2\x9e\x7f\x9a3\x8ech\xdb6|6l\xf15\xf6\xf5\xd7o\ +\xf5\x03\xaf\x9f\xfa@\x02\xe4\xdc\xf9\x00\x00\x00\x00IEND\xaeB`\x82' + + +def getPerlBitmap(): + return BitmapFromImage(getPerlImage()) + +def getPerlImage(): + stream = cStringIO.StringIO(getPerlData()) + return ImageFromStream(stream) + +def getPerlIcon(): + icon = EmptyIcon() + icon.CopyFromBitmap(getPerlBitmap()) + return icon diff --git a/wxPython/samples/ide/activegrid/tool/ProjectEditor.py b/wxPython/samples/ide/activegrid/tool/ProjectEditor.py new file mode 100644 index 0000000000..763e74bc0a --- /dev/null +++ b/wxPython/samples/ide/activegrid/tool/ProjectEditor.py @@ -0,0 +1,1847 @@ +#---------------------------------------------------------------------------- +# Name: ProjectEditor.py +# Purpose: IDE-style Project Editor for wx.lib.pydocview +# +# Author: Peter Yared, Morgan Hua +# +# Created: 8/15/03 +# CVS-ID: $Id$ +# Copyright: (c) 2003, 2004, 2005 ActiveGrid, Inc. +# License: wxWindows License +#---------------------------------------------------------------------------- + +import wx.lib.docview +import wx.lib.pydocview +import types +import os +import os.path +import wx +from wxPython.lib.rcsizer import RowColSizer +import time +import Service +import MessageService +import DebuggerService +import sys +import activegrid.util.xmlmarshaller +import UICommon +from IDE import ACTIVEGRID_BASE_IDE +if not ACTIVEGRID_BASE_IDE: + import ProcessModelEditor + +_ = wx.GetTranslation + +if wx.Platform == '__WXMSW__': + _WINDOWS = True +else: + _WINDOWS = False + + +#---------------------------------------------------------------------------- +# XML Marshalling Methods +#---------------------------------------------------------------------------- + +def load(fileObject): + xml = fileObject.read() + projectModel = activegrid.util.xmlmarshaller.unmarshal(xml) + return projectModel + + +def save(fileObject, projectModel): + xml = activegrid.util.xmlmarshaller.marshal(projectModel, prettyPrint=True) + fileObject.write(xml) + + +#---------------------------------------------------------------------------- +# Classes +#---------------------------------------------------------------------------- + +class ProjectModel: + __xmlname__ = "projectmodel" + __xmlrename__ = { "_files":"files", "_homepath":"homepath" } + + def __init__(self): + self._homepath = None + self._files = [] + + +class ProjectDocument(wx.lib.docview.Document): + + def __init__(self): + wx.lib.docview.Document.__init__(self) + self._projectModel = ProjectModel() + + + def GetModel(self): + return self._projectModel + + + def OnCreate(self, path, flags): + projectService = wx.GetApp().GetService(ProjectService) + if projectService.GetView(): + view = projectService.GetView() + self.AddView(view) + else: + view = self.GetDocumentTemplate().CreateView(self, flags) + projectService.SetView(view) + return view + + + def LoadObject(self, fileObject): + self._projectModel = activegrid.tool.ProjectEditor.load(fileObject) + return True + + + def SaveObject(self, fileObject): + activegrid.tool.ProjectEditor.save(fileObject, self._projectModel) + return True + + + def OnSaveDocument(self, filename): + self._projectModel._homepath = wx.lib.docview.PathOnly(filename) + return wx.lib.docview.Document.OnSaveDocument(self, filename) + + + def OnOpenDocument(self, filename): + view = self.GetFirstView() + frame = view.GetFrame() + + if not os.path.exists(filename): + wx.GetApp().CloseSplash() + msgTitle = wx.GetApp().GetAppName() + if not msgTitle: + msgTitle = _("File Error") + wx.MessageBox(_("Could not find '%s'.") % filename, + msgTitle, + wx.OK | wx.ICON_EXCLAMATION | wx.STAY_ON_TOP, + frame) + return True # if we return False, the Project View is destroyed, Service windows shouldn't be destroyed + + fileObject = file(filename, 'r') + try: + self.LoadObject(fileObject) + except: + wx.GetApp().CloseSplash() + msgTitle = wx.GetApp().GetAppName() + if not msgTitle: + msgTitle = _("File Error") + wx.MessageBox(_("Could not open '%s'. %s") % (wx.lib.docview.FileNameFromPath(filename), sys.exc_value), + msgTitle, + wx.OK | wx.ICON_EXCLAMATION | wx.STAY_ON_TOP, + frame) + return True # if we return False, the Project View is destroyed, Service windows shouldn't be destroyed + + self.Modify(False) + + # if the project file has moved, then ask the user if we should readjust the paths of all the files in the project + newHomepath = wx.lib.docview.PathOnly(filename) + if newHomepath != self._projectModel._homepath: + wx.GetApp().CloseSplash() + msgTitle = wx.GetApp().GetAppName() + if not msgTitle: + msgTitle = _("Project Moved") + projectService = wx.GetApp().GetService(activegrid.tool.ProjectEditor.ProjectService) + yesNoMsg = wx.MessageDialog(frame, + _("The project file '%s' was moved from:\n '%s'\nto:\n '%s'.\n\nWould you like to automatically adjust the project contents accordingly?") % (wx.lib.docview.FileNameFromPath(filename), self._projectModel._homepath, wx.lib.docview.PathOnly(filename)), + msgTitle, + wx.YES_NO | wx.STAY_ON_TOP + ) + if projectService.GetSuppressOpenProjectMessages() or yesNoMsg.ShowModal() == wx.ID_YES: + if not projectService.GetSuppressOpenProjectMessages(): + messageService = wx.GetApp().GetService(MessageService.MessageService) + messageService.ShowWindow() + messageView = messageService.GetView() + messageView.ClearLines() + messageView.AddLines(_("The project file '%s' was moved from:\n '%s'\nto:\n '%s'\n") % (wx.lib.docview.FileNameFromPath(filename), self._projectModel._homepath, wx.lib.docview.PathOnly(filename))) + messageView.AddLines(_("Updating file references:\n")) + + for index, filepath in enumerate(self._projectModel._files): + if filepath.startswith(self._projectModel._homepath + os.sep): + newfile = newHomepath + filepath[len(self._projectModel._homepath):len(filepath)] + if os.path.exists(newfile): + self._projectModel._files[index] = newfile + if not projectService.GetSuppressOpenProjectMessages(): + messageView.AddLines(_(" Success: '%s' location changed from '%s' to '%s'\n") % (wx.lib.docview.FileNameFromPath(filepath), wx.lib.docview.PathOnly(filepath), newHomepath)) + self.Modify(True) + else: + if not projectService.GetSuppressOpenProjectMessages(): + messageView.AddLines(_(" Failure: Couldn't find '%s', file wasn't located at '%s'\n") % (wx.lib.docview.FileNameFromPath(filepath), newHomepath)) + else: + if not projectService.GetSuppressOpenProjectMessages(): + messageView.AddLines(_( " Unmodified: '%s' location wasn't relative to '%s'\n") % (filepath, self._projectModel._homepath)) + self._projectModel._homepath = newHomepath + if not projectService.GetSuppressOpenProjectMessages(): + messageView.AddLines(_("Project file updated.")) + + self.SetFilename(filename, True) + view.AddProjectToView(self) + self.UpdateAllViews() + self._savedYet = True + view.Activate(True) + return True + + + def AddFile(self, file): + return self.AddFiles([file]) + + + def AddFiles(self, files): + notAlreadyThereFiles = filter(lambda x: x not in self._projectModel._files, files) # Filter to the files that are not already in the project + if len(notAlreadyThereFiles) == 0: + self.UpdateAllViews(hint = ("select", self, files)) + return False + else: + self._projectModel._files = self._projectModel._files + notAlreadyThereFiles + self.UpdateAllViews(hint = ("add", self, notAlreadyThereFiles)) + self.Modify(True) + return True + + + def RemoveFile(self, file): + return self.RemoveFiles([file]) + + + def RemoveFiles(self, files): + for file in files: + self._projectModel._files.remove(file) + self.UpdateAllViews(hint = ("remove", self, files)) + self.Modify(True) + return True + + + def RenameFile(self, oldFile, newFile, isProject = False): + try: + if oldFile == newFile: + return False + + # projects don't have to exist yet, so not required to rename old file, + # but files must exist, so we'll try to rename and allow exceptions to occur if can't. + if not isProject or (isProject and os.path.exists(oldFile)): + os.rename(oldFile, newFile) + + if isProject: + documents = self.GetDocumentManager().GetDocuments() + for document in documents: + if document.GetFilename() == oldFile: # If the renamed document is open, update it + document.SetFilename(newFile) + document.SetTitle(wx.lib.docview.FileNameFromPath(newFile)) + document.UpdateAllViews(hint = ("rename", document, newFile)) + else: + self.RemoveFile(oldFile) + self.AddFile(newFile) + documents = self.GetDocumentManager().GetDocuments() + for document in documents: + if document.GetFilename() == oldFile: # If the renamed document is open, update it + document.SetFilename(newFile, notifyViews = True) + document.UpdateAllViews(hint = ("rename", document, newFile)) + return True + except OSError, (code, message): + msgTitle = wx.GetApp().GetAppName() + if not msgTitle: + msgTitle = _("File Error") + wx.MessageBox("Could not rename '%s'. '%s'" % (wx.lib.docview.FileNameFromPath(oldFile), message), + msgTitle, + wx.OK | wx.ICON_EXCLAMATION, + self.GetFirstView().GetFrame()) + return False + + + def GetFiles(self): + return self._projectModel._files + + + def IsFileInProject(self, filename): + return filename in self.GetFiles() + + +import Wizard + +class NewProjectWizard(Wizard.BaseWizard): + + WIZTITLE = _("New Project Wizard") + + def __init__(self, parent): + self._parent = parent + self._fullProjectPath = None + Wizard.BaseWizard.__init__(self, parent, self.WIZTITLE) + self._projectLocationPage = self.CreateProjectLocation(self) + wx.wizard.EVT_WIZARD_PAGE_CHANGING(self, self.GetId(), self.OnWizPageChanging) + + def CreateProjectLocation(self,wizard): + page = Wizard.TitledWizardPage(wizard, _("Project File Location")) + + page.GetSizer().Add(wx.StaticText(page, -1, _("\nSelect the directory and filename for the project.\n\n"))) + self._projectName, self._dirCtrl, sizer, self._fileValidation = UICommon.CreateDirectoryControl(page, _("File Name:"), _("Directory:"), _("agp"), startingDirectory=os.getcwd()) + page.GetSizer().Add(sizer, 1, flag=wx.EXPAND) + + wizard.Layout() + wizard.FitToPage(page) + return page + + def RunWizard(self, existingTables = None, existingRelationships = None): + status = wx.wizard.Wizard.RunWizard(self, self._projectLocationPage) + if status: + docManager = wx.GetApp().GetTopWindow().GetDocumentManager() + if os.path.exists(self._fullProjectPath): + # What if the document is already open and we're overwriting it? + documents = docManager.GetDocuments() + for document in documents: + if document.GetFilename() == self._fullProjectPath: # If the renamed document is open, update it + document.DeleteAllViews() + break + os.remove(self._fullProjectPath) + + for template in docManager.GetTemplates(): + if template.GetDocumentType() == ProjectDocument: + doc = template.CreateDocument(self._fullProjectPath, flags = wx.lib.docview.DOC_NEW) + doc.OnSaveDocument(self._fullProjectPath) + view = doc.GetFirstView() + view.AddProjectToView(doc) + break + + self.Destroy() + return status + + + def OnWizPageChanging(self, event): + if event.GetDirection(): # It's going forwards + if event.GetPage() == self._projectLocationPage: + if not self._fileValidation(): + event.Veto() + return + self._fullProjectPath = os.path.join(self._dirCtrl.GetValue(),UICommon.MakeNameEndInExtension(self._projectName.GetValue(),'.agp')) + + + + def OnShowCreatePages(self): + self.Hide() + import DataModelEditor + requestedPos = self.GetPositionTuple() + projectService = wx.GetApp().GetService(ProjectService) + projectView = projectService.GetView() + + wiz = DataModelEditor.ImportExportWizard(projectView.GetFrame(), pos=requestedPos) + if wiz.RunWizard(dontDestroy=True): + self._schemaName.SetValue(wiz.GetSchemaFileName()) + wiz.Destroy() + self.Show(True) + +class ProjectTemplate(wx.lib.docview.DocTemplate): + + def CreateDocument(self, path, flags): + if path: + return wx.lib.docview.DocTemplate.CreateDocument(self, path, flags) + else: + wiz = NewProjectWizard(wx.GetApp().GetTopWindow()) + wiz.RunWizard() + wiz.Destroy() + return None # never return the doc, otherwise docview will think it is a new file and rename it + +class ProjectAddFilesCommand(wx.lib.docview.Command): + + def __init__(self, projectDoc, files): + wx.lib.docview.Command.__init__(self, canUndo = True) + self._projectDoc = projectDoc + self._files = files + + + def GetName(self): + if len(self._files) == 1: + return _("Add File") + else: + return _("Add Files") + + + def Do(self): + return self._projectDoc.AddFiles(self._files) + + + def Undo(self): + return self._projectDoc.RemoveFiles(self._files) + + +class ProjectRemoveFilesCommand(wx.lib.docview.Command): + + def __init__(self, projectDoc, files): + wx.lib.docview.Command.__init__(self, canUndo = True) + self._projectDoc = projectDoc + self._files = files + + + def GetName(self): + if len(self._files) == 1: + return _("Remove File") + else: + return _("Remove Files") + + + def Do(self): + return self._projectDoc.RemoveFiles(self._files) + + + def Undo(self): + return self._projectDoc.AddFiles(self._files) + + +class ProjectRenameFileCommand(wx.lib.docview.Command): + + def __init__(self, projectDoc, oldFile, newFile, isProject = False): + wx.lib.docview.Command.__init__(self, canUndo = True) + self._projectDoc = projectDoc + self._oldFile = oldFile + self._newFile = newFile + self._isProject = isProject + + + def GetName(self): + return _("Rename File") + + + def Do(self): + return self._projectDoc.RenameFile(self._oldFile, self._newFile, self._isProject) + + + def Undo(self): + return self._projectDoc.RenameFile(self._newFile, self._oldFile, self._isProject) + + +class ProjectTreeCtrl(wx.TreeCtrl): + + def __init__(self, parent, id, style): + wx.TreeCtrl.__init__(self, parent, id, style = style) + + templates = wx.GetApp().GetDocumentManager().GetTemplates() + iconList = wx.ImageList(16, 16, initialCount = len(templates)) + self._iconIndexLookup = [] + for template in templates: + icon = template.GetIcon() + if icon: + if icon.GetHeight() != 16: + icon.SetHeight(16) # wxBug: img2py.py uses EmptyIcon which is 32x32 + if icon.GetWidth() != 16: + icon.SetWidth(16) # wxBug: img2py.py uses EmptyIcon which is 32x32 + iconIndex = iconList.AddIcon(icon) + self._iconIndexLookup.append((template, iconIndex)) + + icon = getBlankIcon() + if icon.GetHeight() != 16: + icon.SetHeight(16) # wxBug: img2py.py uses EmptyIcon which is 32x32 + if icon.GetWidth() != 16: + icon.SetWidth(16) # wxBug: img2py.py uses EmptyIcon which is 32x32 + self._blankIconIndex = iconList.AddIcon(icon) + self.AssignImageList(iconList) + + + def OnCompareItems(self, item1, item2): + return cmp(self.GetItemText(item1).lower(), self.GetItemText(item2).lower()) + + + def AppendItem(self, parent, filepath): + item = wx.TreeCtrl.AppendItem(self, parent, filepath) + + found = False + template = wx.GetApp().GetDocumentManager().FindTemplateForPath(filepath) + if not template and parent == self.GetRootItem(): # If the parent is a root it's a new project + template = wx.GetApp().GetDocumentManager().FindTemplateForPath('.agp') + if template: + for t, iconIndex in self._iconIndexLookup: + if t is template: + self.SetItemImage(item, iconIndex, wx.TreeItemIcon_Normal) + self.SetItemImage(item, iconIndex, wx.TreeItemIcon_Expanded) + self.SetItemImage(item, iconIndex, wx.TreeItemIcon_Selected) + found = True + break + + if not found: + self.SetItemImage(item, self._blankIconIndex, wx.TreeItemIcon_Normal) + self.SetItemImage(item, self._blankIconIndex, wx.TreeItemIcon_Expanded) + self.SetItemImage(item, self._blankIconIndex, wx.TreeItemIcon_Selected) + + return item + + +class ProjectView(wx.lib.docview.View): + + + #---------------------------------------------------------------------------- + # Overridden methods + #---------------------------------------------------------------------------- + + def __init__(self, service = None): + wx.lib.docview.View.__init__(self) + self._service = service # not used, but kept to match other Services + self._lastDirectory = "" + self._treeCtrl = None + self._editingSoDontKillFocus = False + self._checkEditMenu = True + + + def Destroy(self): + projectService = wx.GetApp().GetService(ProjectService) + if projectService: + projectService.SetView(None) + wx.lib.docview.View.Destroy(self) + + + def GetDocument(self): + if not self._treeCtrl: + return None + + items = self._treeCtrl.GetSelections() + if not items: # No selection, so just return first project + item = self._treeCtrl.GetFirstVisibleItem() + if item.IsOk(): + return self._GetItemProject(item) + else: + return None + + for item in items: + project = self._GetItemProject(item) + if project: + return project + + return None + + + def GetDocumentManager(self): # Overshadow this since the superclass uses the view._viewDocument attribute directly, which the project editor doesn't use since it hosts multiple docs + return wx.GetApp().GetDocumentManager() + + + def OnChangeFilename(self): + if self.GetFrame(): + title = _("Projects") + if self.GetDocumentManager().GetFlags() & wx.lib.docview.DOC_SDI and wx.GetApp().GetAppName(): + title = title + " - " + wx.GetApp().GetAppName() + self.GetFrame().SetTitle(title) + project = self.GetDocument() + if project: + projectItem = self._GetProjectItem(project) + name = self._treeCtrl.GetItemText(self._GetProjectItem(project)) + name2 = self._MakeProjectName(project) + if name != name2: + self._treeCtrl.SetItemText(projectItem, name2) + self._treeCtrl.SortChildren(self._treeCtrl.GetRootItem()) + + + def Activate(self, activate = True): + if not wx.GetApp().IsMDI(): + if activate and not self.IsShown(): + self.Show() + + if self.IsShown(): + wx.lib.docview.View.Activate(self, activate = activate) + if activate and self._treeCtrl: + self._treeCtrl.SetFocus() + + + def OnCreate(self, doc, flags): + config = wx.ConfigBase_Get() + if wx.GetApp().IsMDI(): + self._embeddedWindow = wx.GetApp().GetTopWindow().GetEmbeddedWindow(wx.lib.pydocview.EMBEDDED_WINDOW_TOPLEFT) + self.SetFrame(self._embeddedWindow) + frame = self._embeddedWindow + else: + self._embeddedWindow = None + pos = config.ReadInt("ProjectFrameXLoc", -1), config.ReadInt("ProjectFrameYLoc", -1) + # make sure frame is visible + screenWidth = wx.SystemSettings.GetMetric(wx.SYS_SCREEN_X) + screenHeight = wx.SystemSettings.GetMetric(wx.SYS_SCREEN_Y) + if pos[0] < 0 or pos[0] >= screenWidth or pos[1] < 0 or pos[1] >= screenHeight: + pos = wx.DefaultPosition + + size = wx.Size(config.ReadInt("ProjectFrameXSize", -1), config.ReadInt("ProjectFrameYSize", -1)) + + title = _("Projects") + if self.GetDocumentManager().GetFlags() & wx.lib.docview.DOC_SDI and wx.GetApp().GetAppName(): + title = title + " - " + wx.GetApp().GetAppName() + + frame = wx.GetApp().CreateDocumentFrame(self, doc, 0, title = title, pos = pos, size = size) + if config.ReadInt("ProjectFrameMaximized", False): + frame.Maximize(True) + + sizer = wx.BoxSizer() + self._treeCtrl = ProjectTreeCtrl(frame, -1, style = wx.TR_HIDE_ROOT | wx.TR_HAS_BUTTONS | wx.TR_EDIT_LABELS | wx.TR_DEFAULT_STYLE | wx.TR_MULTIPLE) + self._treeCtrl.AddRoot(_("Projects")) + + if self._embeddedWindow: + sizer.Add(self._treeCtrl) + sizer.Fit(frame) + else: + sizer.Add(self._treeCtrl, 1, wx.EXPAND, 0) + frame.SetSizer(sizer) + frame.Layout() + self.Activate() + + if wx.GetApp().IsMDI(): + wx.EVT_SET_FOCUS(self._treeCtrl, self.OnFocus) + wx.EVT_KILL_FOCUS(self._treeCtrl, self.OnKillFocus) + + if self.GetDocumentManager().GetFlags() & wx.lib.docview.DOC_SDI: + wx.EVT_TREE_ITEM_ACTIVATED(self._treeCtrl, self._treeCtrl.GetId(), self.OnOpenSelectionSDI) + else: + wx.EVT_TREE_ITEM_ACTIVATED(self._treeCtrl, self._treeCtrl.GetId(), self.OnOpenSelection) + wx.EVT_TREE_BEGIN_LABEL_EDIT(self._treeCtrl, self._treeCtrl.GetId(), self.OnBeginLabelEdit) + wx.EVT_TREE_END_LABEL_EDIT(self._treeCtrl, self._treeCtrl.GetId(), self.OnEndLabelEdit) + wx.EVT_RIGHT_DOWN(self._treeCtrl, self.OnRightClick) + wx.EVT_KEY_DOWN(self._treeCtrl, self.OnKeyPressed) + # wx.EVT_COMMAND_RIGHT_CLICK(self._treeCtrl, self._treeCtrl.GetId(), self.OnRightClick) # wxBug: This isn't working for some reason + + # drag-and-drop support + dt = ProjectFileDropTarget(self) + self._treeCtrl.SetDropTarget(dt) + + return True + + + def WriteProjectConfig(self): + frame = self.GetFrame() + config = wx.ConfigBase_Get() + if frame and not self._embeddedWindow: + if not frame.IsMaximized(): + config.WriteInt("ProjectFrameXLoc", frame.GetPositionTuple()[0]) + config.WriteInt("ProjectFrameYLoc", frame.GetPositionTuple()[1]) + config.WriteInt("ProjectFrameXSize", frame.GetSizeTuple()[0]) + config.WriteInt("ProjectFrameYSize", frame.GetSizeTuple()[1]) + config.WriteInt("ProjectFrameMaximized", frame.IsMaximized()) + + if config.ReadInt("ProjectSaveDocs", True): + projectFileNames = [] + projectExpanded = [] + if self._treeCtrl: + for projectItem in self._GetChildItems(self._treeCtrl.GetRootItem()): + project = self._GetItemProject(projectItem) + if not project.OnSaveModified(): + return + if project.GetDocumentSaved(): # Might be a new document and "No" selected to save it + projectFileNames.append(str(project.GetFilename())) + projectExpanded.append(self._treeCtrl.IsExpanded(projectItem)) + config.Write("ProjectSavedDocs", projectFileNames.__repr__()) + config.Write("ProjectExpandedSavedDocs", projectExpanded.__repr__()) + + + def OnClose(self, deleteWindow = True): + if self.GetDocumentManager().GetFlags() & wx.lib.docview.DOC_SDI: + self.WriteProjectConfig() + project = self.GetDocument() + if not project: + return True + if not self.GetDocument().Close(): + return True + self.Activate(False) + if project: + projectItem = self._GetProjectItem(project) + if projectItem: + self._treeCtrl.Delete(projectItem) + # We don't need to delete the window since it is a floater/embedded + return True + + + def _GetParentFrame(self): + return wx.GetTopLevelParent(self.GetFrame()) + + + def OnUpdate(self, sender = None, hint = None): + wx.lib.docview.View.OnUpdate(self, sender, hint) + if hint: + if hint[0] == "add": + projectItem = self._GetProjectItem(hint[1]) + files = hint[2] + self._treeCtrl.UnselectAll() + self._treeCtrl.Expand(projectItem) + for file in files: + item = self._treeCtrl.AppendItem(projectItem, os.path.basename(file)) + self._treeCtrl.SetPyData(item, file) + self._treeCtrl.SelectItem(item) + self._treeCtrl.EnsureVisible(item) # wxBug: Doesn't work + self._treeCtrl.SortChildren(projectItem) + elif hint[0] == "remove": + projectItem = self._GetProjectItem(hint[1]) + files = hint[2] + children = self._GetChildItems(projectItem) + for child in children: + if self._GetItemFile(child) in files: + self._treeCtrl.Delete(child) + elif hint[0] == "select": + projectItem = self._GetProjectItem(hint[1]) + files = hint[2] + self._treeCtrl.UnselectAll() + children = self._GetChildItems(projectItem) + for child in children: + if self._GetItemFile(child) in files: + self._treeCtrl.SelectItem(child) + self._treeCtrl.EnsureVisible(child) # wxBug: Doesn't work + elif hint[0] == "rename": + projectItem = self._GetProjectItem(hint[1]) + self._treeCtrl.SetItemText(projectItem, os.path.basename(hint[2])) + + + def ProcessEvent(self, event): + id = event.GetId() + if id == ProjectService.ADD_FILES_TO_PROJECT_ID: + self.OnAddFileToProject(event) + return True + elif id == ProjectService.ADD_CURRENT_FILE_TO_PROJECT_ID: + return False # Implement this one in the service + elif id == ProjectService.RENAME_ID: + self.OnRename(event) + return True + elif id == wx.ID_CUT: + self.OnCut(event) + return True + elif id == wx.ID_COPY: + self.OnCopy(event) + return True + elif id == wx.ID_PASTE: + self.OnPaste(event) + return True + elif id == wx.ID_CLEAR or id == ProjectService.REMOVE_FROM_PROJECT: + self.OnClear(event) + return True + elif id == wx.ID_SELECTALL: + self.OnSelectAll(event) + return True + elif id == ProjectService.OPEN_SELECTION_ID: + self.OnOpenSelection(event) + return True + elif id == wx.lib.pydocview.FilePropertiesService.PROPERTIES_ID: + self.OnProperties(event) + return True + else: + return False + + def ProcessUpdateUIEvent(self, event): + # Hack: The edit menu is not being set for projects that are preloaded at startup, so make sure it is OK here + if self._checkEditMenu: + doc = self.GetDocument() + if doc and not doc.GetCommandProcessor().GetEditMenu(): + doc.GetCommandProcessor().SetEditMenu(wx.GetApp().GetEditMenu(self._GetParentFrame())) + self._checkEditMenu = False + id = event.GetId() + if id == ProjectService.ADD_FILES_TO_PROJECT_ID: + event.Enable(self._HasProjectsSelected() or self._HasFilesSelected()) + return True + elif id == ProjectService.ADD_CURRENT_FILE_TO_PROJECT_ID: + event.Enable(False) + return True + elif id == ProjectService.RENAME_ID: + event.Enable(self._HasFilesSelected() or self._HasProjectsSelected()) + return True + elif id == wx.ID_CUT: + event.Enable(self._AreSelectedItemsFromSameProject()) + return True + elif id == wx.ID_COPY: + event.Enable(self._HasFilesSelected()) + return True + elif id == wx.ID_PASTE: + event.Enable(self.CanPaste()) + return True + elif id == wx.ID_CLEAR or id == ProjectService.REMOVE_FROM_PROJECT: + event.Enable(self._AreSelectedItemsFromSameProject()) + return True + elif id == wx.ID_SELECTALL: + event.Enable(self._HasFiles()) + return True + elif id == ProjectService.OPEN_SELECTION_ID: + event.Enable(self._HasFilesSelected()) + return True + elif id == wx.lib.pydocview.FilePropertiesService.PROPERTIES_ID: + event.Enable(self._HasProjectsSelected() or self._HasFilesSelected()) + return True + else: + return False + + #---------------------------------------------------------------------------- + # Display Methods + #---------------------------------------------------------------------------- + + def IsShown(self): + if not self.GetFrame(): + return False + return self.GetFrame().IsShown() + + + def Hide(self): + self.Show(False) + + + def Show(self, show = True): + self.GetFrame().Show(show) + if wx.GetApp().IsMDI(): + mdiParentFrame = wx.GetApp().GetTopWindow() + mdiParentFrame.ShowEmbeddedWindow(self.GetFrame(), show) + + + #---------------------------------------------------------------------------- + # Methods for ProjectDocument and ProjectService to call + #---------------------------------------------------------------------------- + + def SetExpandedProjects(self, expandedProjects): + self._treeCtrl.UnselectAll() + firstItem = None + for i, item in enumerate(self._GetChildItems(self._treeCtrl.GetRootItem())): + if i == 0: + firstItem = item + if expandedProjects[i]: + self._treeCtrl.Expand(item) + else: + self._treeCtrl.Collapse(item) + # wxBug: This causes a crash, tried using ScrollTo which crashed as well. Then tried calling it with wx.CallAfter and that crashed as well, with both EnsureVisible and ScrollTo + # self._treeCtrl.EnsureVisible(self._treeCtrl.GetRootItem()) + # So doing the following massive hack which forces the treectrl to scroll up to the top item + if firstItem: + if expandedProjects[i]: + self._treeCtrl.Collapse(firstItem) + self._treeCtrl.Expand(firstItem) + else: + self._treeCtrl.Expand(firstItem) + self._treeCtrl.Collapse(firstItem) + + def GetSelectedFile(self): + for item in self._treeCtrl.GetSelections(): + return self._GetItemFile(item) + + def AddProjectToView(self, document): + rootItem = self._treeCtrl.GetRootItem() + projectItem = self._treeCtrl.AppendItem(rootItem, self._MakeProjectName(document)) + self._treeCtrl.SetPyData(projectItem, document) + for file in document.GetFiles(): + fileItem = self._treeCtrl.AppendItem(projectItem, os.path.basename(file)) + self._treeCtrl.SetPyData(fileItem, file) + self._treeCtrl.SortChildren(rootItem) + self._treeCtrl.SortChildren(projectItem) + self._treeCtrl.UnselectAll() + self._treeCtrl.Expand(projectItem) + self._treeCtrl.SelectItem(projectItem) + if self._embeddedWindow: + document.GetCommandProcessor().SetEditMenu(wx.GetApp().GetEditMenu(self._GetParentFrame())) + + #---------------------------------------------------------------------------- + # Methods for OutlineService + #---------------------------------------------------------------------------- + def DoLoadOutlineCallback(self, force=False): + """ Project Editor is a special case for the Outline Service. + You need to be able to be active in the Project Manager without clearing + the Outline View. So we make the Project Editor a client of the Outline + Service, but we don't load anything in the Outline View, leaving the + contents of the Outline View alone (e.g. last document's outline view). + """ + pass + + #---------------------------------------------------------------------------- + # Control events + #---------------------------------------------------------------------------- + + def OnProperties(self, event): + items = self._treeCtrl.GetSelections() + if not items: + return + item = items[0] + if self._IsItemProject(item): + projectPropertiesDialog = ProjectPropertiesDialog(wx.GetApp().GetTopWindow(), self._GetItemProject(item).GetFilename()) + if projectPropertiesDialog.ShowModal() == wx.ID_OK: + pass # Handle OK + projectPropertiesDialog.Destroy() + elif self._IsItemFile(item): + filePropertiesService = wx.GetApp().GetService(wx.lib.pydocview.FilePropertiesService) + filePropertiesService.ShowPropertiesDialog(self._GetItemFile(item)) + + + def OnAddFileToProject(self, event): + if wx.Platform == "__WXMSW__" or wx.Platform == "__WXGTK__" or wx.Platform == "__WXMAC__": + allfilter = '' + descr = '' + for temp in self.GetDocumentManager()._templates: + if temp.IsVisible(): + if len(descr) > 0: + descr = descr + _('|') + allfilter = allfilter + _(';') + descr = descr + temp.GetDescription() + _(" (") + temp.GetFileFilter() + _(") |") + temp.GetFileFilter() # spacing is important, make sure there is no space after the "|", it causes a bug on wx_gtk + allfilter = allfilter + temp.GetFileFilter() + descr = _("All") + _(" (") + allfilter + _(") |") + allfilter + _('|') + descr # spacing is important, make sure there is no space after the "|", it causes a bug on wx_gtk + descr = descr + _("|") + _("Any (*.*) | *.*") + else: + descr = _("*.*") + if True or _WINDOWS: + dialog = wx.FileDialog(self.GetFrame(), _("Add Files"), self._lastDirectory, "", descr, wx.OPEN | wx.HIDE_READONLY | wx.MULTIPLE) + if dialog.ShowModal() != wx.ID_OK: + return + paths = dialog.GetPaths() + dialog.Destroy() + else: + paths = wx.FileSelector(_("Add Files"), self._lastDirectory, "", wildcard = descr, flags = wx.OPEN | wx.HIDE_READONLY | wx.MULTIPLE, parent=self.GetFrame()) + if type(paths) == types.StringType: + paths = [paths] + if len(paths): + self._lastDirectory = wx.lib.docview.PathOnly(paths[0]) + self.GetDocument().GetCommandProcessor().Submit(ProjectAddFilesCommand(self.GetDocument(), paths)) + self.Activate(True) # after add, should put focus on project editor + + + def DoAddFilesToProject(self, filenames): + # method used by Drag-n-Drop to add files to current Project + self.GetDocument().GetCommandProcessor().Submit(ProjectAddFilesCommand(self.GetDocument(), filenames)) + + + def DoSelectFiles(self, filenames): + # method used by Drag-n-Drop to select files in current Project + for selection in self._treeCtrl.GetSelections(): + self._treeCtrl.SelectItem(selection, False) + for file in filenames: + item = self._GetFileItem(longFileName=file) + if item: + self._treeCtrl.SelectItem(item, True) + self._treeCtrl.EnsureVisible(item) + + + def DoSelectProject(self, x, y): + # method used by Drag-n-Drop to set current Project based on cursor position + item, flag = self._treeCtrl.HitTest((x,y)) + if not item: + return False + + project = self._GetItemProject(item) + if not project: + return False + + projectItem = self._GetProjectItem(project) + self._treeCtrl.UnselectAll() + self._treeCtrl.SelectItem(projectItem) + return True + + + def OnFocus(self, event): + wx.GetApp().GetDocumentManager().ActivateView(self) + event.Skip() + + + def OnKillFocus(self, event): + # Get the top MDI window and "activate" it since it is already active from the perspective of the MDIParentFrame + # wxBug: Would be preferable to call OnActivate, but have casting problem, so added Activate method to docview.DocMDIChildFrame + if not self._editingSoDontKillFocus: # wxBug: This didn't used to happen, but now when you start to edit an item in a wxTreeCtrl it puts out a KILL_FOCUS event, so we need to detect it + childFrame = wx.GetApp().GetTopWindow().GetActiveChild() + if childFrame: + childFrame.Activate() + event.Skip() + + + def OnRightClick(self, event): + self.Activate(True) + if not self._treeCtrl.GetSelections(): + return + if len(self._treeCtrl.GetSelections()) == 1 and self._IsItemRoot(self._treeCtrl.GetSelections()[0]): + return # Don't do a menu if it's just the root item selected + menu = wx.Menu() + if self._HasFilesSelected(): # Files context + menu.Append(ProjectService.OPEN_SELECTION_ID, _("&Open"), _("Opens the selection")) + menu.Enable(ProjectService.OPEN_SELECTION_ID, True) + wx.EVT_MENU(self._GetParentFrame(), ProjectService.OPEN_SELECTION_ID, self.OnOpenSelection) + itemIDs = [None] + for item in self._treeCtrl.GetSelections(): + if self._IsItemProcessModelFile(item): + itemIDs = [None, ProjectService.RUN_SELECTED_PM_ID, None] + break + else: # Project context + itemIDs = [wx.ID_CLOSE, wx.ID_SAVE, wx.ID_SAVEAS, None] + menuBar = self._GetParentFrame().GetMenuBar() + itemIDs = itemIDs + [wx.lib.pydocview.FilePropertiesService.PROPERTIES_ID, None, ProjectService.ADD_FILES_TO_PROJECT_ID, ProjectService.REMOVE_FROM_PROJECT, None, wx.ID_UNDO, wx.ID_REDO, None, wx.ID_CUT, wx.ID_COPY, wx.ID_PASTE, wx.ID_CLEAR, None, wx.ID_SELECTALL, None, ProjectService.RENAME_ID] + for itemID in itemIDs: + if not itemID: + menu.AppendSeparator() + else: + if itemID == ProjectService.RUN_SELECTED_PM_ID: + menu.Append(ProjectService.RUN_SELECTED_PM_ID, _("Run Process")) + wx.EVT_MENU(self._GetParentFrame(), ProjectService.RUN_SELECTED_PM_ID, self.OnRunSelectedPM) + elif itemID == ProjectService.REMOVE_FROM_PROJECT: + menu.Append(ProjectService.REMOVE_FROM_PROJECT, _("Remove Selected Files from Project")) + wx.EVT_MENU(self._GetParentFrame(), ProjectService.REMOVE_FROM_PROJECT, self.OnClear) + wx.EVT_UPDATE_UI(self._GetParentFrame(), ProjectService.REMOVE_FROM_PROJECT, self._GetParentFrame().ProcessUpdateUIEvent) + else: + item = menuBar.FindItemById(itemID) + if item: + menu.Append(itemID, item.GetLabel()) + self._treeCtrl.PopupMenu(menu, wx.Point(event.GetX(), event.GetY())) + menu.Destroy() + + def OnRunSelectedPM(self, event): + projectService = wx.GetApp().GetService(ProjectService) + projectService.OnRunProcessModel(event, runSelected=True) + + def OnRename(self, event): + if self._treeCtrl.GetSelections(): + self._treeCtrl.EditLabel(self._treeCtrl.GetSelections()[0]) + + + def OnBeginLabelEdit(self, event): + self._editingSoDontKillFocus = True + item = event.GetItem() + if not self._IsItemFile(item) and not self._IsItemProject(item): + event.Veto() + + + def OnEndLabelEdit(self, event): + self._editingSoDontKillFocus = False + item = event.GetItem() + newName = event.GetLabel() + if not newName or (not self._IsItemFile(item) and not self._IsItemProject(item)): + event.Veto() + return + if self._IsItemFile(item): + oldFile = self._GetItemFile(item) + newFile = os.path.join(os.path.split(oldFile)[0], newName) + if not self._GetItemProject(item).GetCommandProcessor().Submit(ProjectRenameFileCommand(self.GetDocument(), oldFile, newFile)): + event.Veto() + return + self._treeCtrl.SortChildren(self._treeCtrl.GetItemParent(self._treeCtrl.GetSelections()[0])) + elif self._IsItemProject(item): + oldFile = self._GetItemProject(item).GetFilename() + newFile = os.path.join(os.path.split(oldFile)[0], newName) + if not self._GetItemProject(item).GetCommandProcessor().Submit(ProjectRenameFileCommand(self.GetDocument(), oldFile, newFile, True)): + event.Veto() + return + self._treeCtrl.SortChildren(self._treeCtrl.GetRootItem()) + + + def CanPaste(self): + # wxBug: Should be able to use IsSupported/IsSupportedFormat here + #fileDataObject = wx.FileDataObject() + #hasFilesInClipboard = wx.TheClipboard.IsSupportedFormat(wx.FileDataObject) + if not wx.TheClipboard.IsOpened(): + if wx.TheClipboard.Open(): + fileDataObject = wx.FileDataObject() + hasFilesInClipboard = wx.TheClipboard.GetData(fileDataObject) + wx.TheClipboard.Close() + else: + hasFilesInClipboard = False + return hasFilesInClipboard + + + def OnCut(self, event): + if self._AreSelectedItemsFromSameProject(): + self.OnCopy(event) + self.OnClear(event) + + + def OnCopy(self, event): + fileDataObject = wx.FileDataObject() + items = self._treeCtrl.GetSelections() + for item in items: + if self._IsItemFile(item): + file = self._treeCtrl.GetPyData(item) + fileDataObject.AddFile(file) + if len(fileDataObject.GetFilenames()) > 0 and wx.TheClipboard.Open(): + wx.TheClipboard.SetData(fileDataObject) + wx.TheClipboard.Close() + + + def OnPaste(self, event): + if wx.TheClipboard.Open(): + fileDataObject = wx.FileDataObject() + if wx.TheClipboard.GetData(fileDataObject): + self.GetDocument().GetCommandProcessor().Submit(ProjectAddFilesCommand(self.GetDocument(), fileDataObject.GetFilenames())) + wx.TheClipboard.Close() + + + def OnClear(self, event): + if self._AreSelectedItemsFromSameProject(): + items = self._treeCtrl.GetSelections() + files = [] + for item in items: + if self._IsItemFile(item): + files.append(self._GetItemFile(item)) + self.GetDocument().GetCommandProcessor().Submit(ProjectRemoveFilesCommand(self._GetItemProject(items[0]), files)) + + + def OnKeyPressed(self, event): + key = event.KeyCode() + if key == wx.WXK_DELETE: + self.OnClear(event) + else: + event.Skip() + + + def OnSelectAll(self, event): + project = self.GetDocument() + if project: + self._treeCtrl.UnselectAll() + for child in self._GetChildItems(self._GetProjectItem(project)): + self._treeCtrl.SelectItem(child) + + + def OnOpenSelectionSDI(self, event): + # Do a call after so that the second mouseclick on a doubleclick doesn't reselect the project window + wx.CallAfter(self.OnOpenSelection, None) + + + def OnOpenSelection(self, event): + doc = None + try: + items = self._treeCtrl.GetSelections() + for item in items: + if self._IsItemFile(item): + filepath = self._GetItemFile(item) + if not os.path.exists(filepath): + msgTitle = wx.GetApp().GetAppName() + if not msgTitle: + msgTitle = _("File Not Found") + yesNoMsg = wx.MessageDialog(self.GetFrame(), + _("The file '%s' was not found in '%s'.\n\nWould you like to browse for the file?") % (wx.lib.docview.FileNameFromPath(filepath), wx.lib.docview.PathOnly(filepath)), + msgTitle, + wx.YES_NO + ) + if yesNoMsg.ShowModal() == wx.ID_NO: + continue + findFile = wx.FileDialog(self.GetFrame(), + _("Choose a file"), + wx.lib.docview.PathOnly(filepath), + wx.lib.docview.FileNameFromPath(filepath), + style = wx.OPEN + ) + if findFile.ShowModal() == wx.ID_OK and findFile.GetPath(): + newpath = findFile.GetPath() + else: + newpath = None + findFile.Destroy() + if newpath: + # update Project Model with new location + self.GetDocument().RemoveFile(filepath) + self.GetDocument().AddFile(newpath) + filepath = newpath + + doc = self.GetDocumentManager().CreateDocument(filepath, wx.lib.docview.DOC_SILENT) + + except IOError, (code, message): + msgTitle = wx.GetApp().GetAppName() + if not msgTitle: + msgTitle = _("File Error") + wx.MessageBox("Could not open '%s'." % wx.lib.docview.FileNameFromPath(filepath), + msgTitle, + wx.OK | wx.ICON_EXCLAMATION, + self.GetFrame()) + + + #---------------------------------------------------------------------------- + # Convenience methods + #---------------------------------------------------------------------------- + + def _HasFiles(self): + if not self._treeCtrl: + return False + return self._treeCtrl.GetCount() > 1 # 1 item = root item, don't count as having files + + + def _HasProjectsSelected(self): + if not self._treeCtrl: + return False + items = self._treeCtrl.GetSelections() + if not items: + return False + for item in items: + if self._IsItemProject(item): + return True + return False + + + def _HasFilesSelected(self): + if not self._treeCtrl: + return False + items = self._treeCtrl.GetSelections() + if not items: + return False + for item in items: + if not self._IsItemFile(item): + return False + return True + + + def _MakeProjectName(self, project): + return project.GetPrintableName() + + + # Return the tree item for a project + def _GetProjectItem(self, project): + children = self._GetChildItems(self._treeCtrl.GetRootItem()) + for child in children: + if self._treeCtrl.GetPyData(child) == project: + return child + return None + + + # Returns the project for an item, either for a project item or a file that is part of a project + def _GetItemProject(self, item): + if self._IsItemRoot(item): + return None + if self._IsItemProject(item): + return self._treeCtrl.GetPyData(item) + if self._IsItemFile(item): + return self._treeCtrl.GetPyData(self._treeCtrl.GetItemParent(item)) + return None + + + def _GetItemFile(self, item): + if self._IsItemFile(item): + return self._treeCtrl.GetPyData(item) + else: + return None + + + def _GetFileItem(self, shortFileName = None, longFileName = None): + """ Returns the tree item for a file given the short (display) or long (fullpath) file name. """ + + if shortFileName: + project_children = self._GetChildItems(self._treeCtrl.GetRootItem()) + for child in project_children: + file_children = self._GetChildItems(child) + for file_child in file_children: + if self._treeCtrl.GetItemText(file_child) == shortFileName: + return file_child + return None + else: + project_children = self._GetChildItems(self._treeCtrl.GetRootItem()) + for child in project_children: + file_children = self._GetChildItems(child) + for file_child in file_children: + if self._treeCtrl.GetPyData(file_child) == longFileName: + return file_child + return None + + + def GetFilePathFromTreeName(self, shortFileName): + """ + Returns the data object given a short (display) file name for a file. The data + object should be the full path. + """ + return self._GetItemFile(self._GetFileItem(shortFileName)) + + + def SelectFileInTree(self, shortFileName): + item = self._GetFileItem(shortFileName) + if item: + for selection in self._treeCtrl.GetSelections(): + self._treeCtrl.SelectItem(selection, False) + self._treeCtrl.SelectItem(item, True) + self._treeCtrl.EnsureVisible(item) + + + def _IsItemRoot(self, item): + return item == self._treeCtrl.GetRootItem() + + + def _IsItemProject(self, item): + return isinstance(self._treeCtrl.GetPyData(item), ProjectDocument) + + + def _IsItemFile(self, item): + return isinstance(self._treeCtrl.GetPyData(item), types.StringTypes) + + + def _IsItemProcessModelFile(self, item): + if ACTIVEGRID_BASE_IDE: + return False + + if isinstance(self._treeCtrl.GetPyData(item), types.StringTypes): + filename = self._treeCtrl.GetPyData(item) + ext = None + for template in self.GetDocumentManager().GetTemplates(): + if template.GetDocumentType() == ProcessModelEditor.ProcessModelDocument: + ext = template.GetDefaultExtension() + break; + if not ext: + return False + + if filename.endswith(ext): + return True + + return False + + + def _AreSelectedItemsFromSameProject(self): + if not self._treeCtrl: + return False + items = self._treeCtrl.GetSelections() + if not items: + return False + project = self._GetItemProject(items[0]) + if project == None: + return False + for item in items: + if not self._IsItemFile(item): + return False + if self._GetItemProject(item) != project: + return False + return True + + + def _GetChildItems(self, parentItem): + children = [] + (child, cookie) = self._treeCtrl.GetFirstChild(parentItem) + while child.IsOk(): + children.append(child) + (child, cookie) = self._treeCtrl.GetNextChild(parentItem, cookie) + return children + + + +class ProjectFileDropTarget(wx.FileDropTarget): + + def __init__(self, view): + wx.FileDropTarget.__init__(self) + self._view = view + + + def OnDropFiles(self, x, y, filenames): + if self._view.DoSelectProject(x, y): + self._view.DoAddFilesToProject(filenames) + self._view.DoSelectFiles(filenames) + return True + return False + + + def OnDragOver(self, x, y, default): + if self._view.DoSelectProject(x,y): + return wx.DragCopy + return wx.DragNone + + +class ProjectPropertiesDialog(wx.Dialog): + + + def __init__(self, parent, filename): + wx.Dialog.__init__(self, parent, -1, _("Project Properties"), size = (310, 330)) + + HALF_SPACE = 5 + SPACE = 10 + + filePropertiesService = wx.GetApp().GetService(wx.lib.pydocview.FilePropertiesService) + + notebook = wx.Notebook(self, -1) + tab = wx.Panel(notebook, -1) + + gridSizer = RowColSizer() + + gridSizer.Add(wx.StaticText(tab, -1, _("Filename:")), flag=wx.RIGHT, border=HALF_SPACE, row=0, col=0) + if os.path.isfile(filename): + gridSizer.Add(wx.StaticText(tab, -1, os.path.split(filename)[1]), row=0, col=1) + + gridSizer.Add(wx.StaticText(tab, -1, _("Location:")), flag=wx.RIGHT, border=HALF_SPACE, row=1, col=0) + gridSizer.Add(wx.StaticText(tab, -1, filePropertiesService.chopPath(os.path.split(filename)[0])), flag=wx.BOTTOM, border=SPACE, row=1, col=1) + + gridSizer.Add(wx.StaticText(tab, -1, _("Size:")), flag=wx.RIGHT, border=HALF_SPACE, row=2, col=0) + gridSizer.Add(wx.StaticText(tab, -1, str(os.path.getsize(filename)) + ' ' + _("bytes")), row=2, col=1) + + lineSizer = wx.BoxSizer(wx.VERTICAL) # let the line expand horizontally without vertical expansion + lineSizer.Add(wx.StaticLine(tab, -1, size = (10,-1)), 0, wx.EXPAND) + gridSizer.Add(lineSizer, flag=wx.EXPAND|wx.ALIGN_CENTER_VERTICAL|wx.TOP, border=HALF_SPACE, row=3, col=0, colspan=2) + + gridSizer.Add(wx.StaticText(tab, -1, _("Created:")), flag=wx.RIGHT, border=HALF_SPACE, row=4, col=0) + gridSizer.Add(wx.StaticText(tab, -1, time.ctime(os.path.getctime(filename))), row=4, col=1) + + gridSizer.Add(wx.StaticText(tab, -1, _("Modified:")), flag=wx.RIGHT, border=HALF_SPACE, row=5, col=0) + gridSizer.Add(wx.StaticText(tab, -1, time.ctime(os.path.getmtime(filename))), row=5, col=1) + + gridSizer.Add(wx.StaticText(tab, -1, _("Accessed:")), flag=wx.RIGHT, border=HALF_SPACE, row=6, col=0) + gridSizer.Add(wx.StaticText(tab, -1, time.ctime(os.path.getatime(filename))), row=6, col=1) + + else: + gridSizer.Add(wx.StaticText(tab, -1, os.path.split(filename)[1] + ' ' + _("[new project]")), row=0, col=1) + + # add a border around the inside of the tab + spacerGrid = wx.BoxSizer(wx.VERTICAL) + spacerGrid.Add(gridSizer, 0, wx.ALL, SPACE); + tab.SetSizer(spacerGrid) + notebook.AddPage(tab, _("General")) + if wx.Platform == "__WXMSW__": + notebook.SetPageSize((310,200)) + + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(notebook, 0, wx.ALL | wx.EXPAND, SPACE) + sizer.Add(self.CreateButtonSizer(wx.OK), 0, wx.ALIGN_RIGHT | wx.RIGHT | wx.BOTTOM, HALF_SPACE) + + sizer.Fit(self) + self.SetDimensions(-1, -1, 310, -1, wx.SIZE_USE_EXISTING) + self.SetSizer(sizer) + self.Layout() + + +class ProjectOptionsPanel(wx.Panel): + + + def __init__(self, parent, id): + wx.Panel.__init__(self, parent, id) + self._useSashMessageShown = False + SPACE = 10 + HALF_SPACE = 5 + config = wx.ConfigBase_Get() + self._projSaveDocsCheckBox = wx.CheckBox(self, -1, _("Remember open projects")) + self._projSaveDocsCheckBox.SetValue(config.ReadInt("ProjectSaveDocs", True)) + projectBorderSizer = wx.BoxSizer(wx.VERTICAL) + projectSizer = wx.BoxSizer(wx.VERTICAL) + projectSizer.Add(self._projSaveDocsCheckBox, 0, wx.ALL, HALF_SPACE) + self._projShowWelcomeCheckBox = wx.CheckBox(self, -1, _("Show Welcome Dialog")) + self._projShowWelcomeCheckBox.SetValue(config.ReadInt("RunWelcomeDialog", True)) + projectSizer.Add(self._projShowWelcomeCheckBox, 0, wx.ALL, HALF_SPACE) + projectBorderSizer.Add(projectSizer, 0, wx.ALL, SPACE) + self.SetSizer(projectBorderSizer) + self.Layout() + parent.AddPage(self, _("Project")) + + def OnUseSashSelect(self, event): + if not self._useSashMessageShown: + msgTitle = wx.GetApp().GetAppName() + if not msgTitle: + msgTitle = _("Document Options") + wx.MessageBox("Project window embedded mode changes will not appear until the application is restarted.", + msgTitle, + wx.OK | wx.ICON_INFORMATION, + self.GetParent()) + self._useSashMessageShown = True + + + def OnOK(self, optionsDialog): + config = wx.ConfigBase_Get() + config.WriteInt("ProjectSaveDocs", self._projSaveDocsCheckBox.GetValue()) + config.WriteInt("RunWelcomeDialog", self._projShowWelcomeCheckBox.GetValue()) + + +class ProjectService(Service.Service): + + #---------------------------------------------------------------------------- + # Constants + #---------------------------------------------------------------------------- + SHOW_WINDOW = wx.NewId() # keep this line for each subclass, need unique ID for each Service + RUNPM_ID = wx.NewId() + RUN_SELECTED_PM_ID = wx.NewId() + RUN_CURRENT_PM_ID = wx.NewId() + ADD_FILES_TO_PROJECT_ID = wx.NewId() + ADD_CURRENT_FILE_TO_PROJECT_ID = wx.NewId() + RENAME_ID = wx.NewId() + OPEN_SELECTION_ID = wx.NewId() + REMOVE_FROM_PROJECT = wx.NewId() + + + #---------------------------------------------------------------------------- + # Overridden methods + #---------------------------------------------------------------------------- + + def __init__(self, serviceName, embeddedWindowLocation = wx.lib.pydocview.EMBEDDED_WINDOW_LEFT): + Service.Service.__init__(self, serviceName, embeddedWindowLocation) + self._runHandlers = [] + self._suppressOpenProjectMessages = False + + + def _CreateView(self): + return ProjectView(self) + + + def ShowWindow(self, show = True): + """ Force showing of saved projects on opening, otherwise empty Project Window is disconcerting for user """ + Service.Service.ShowWindow(self, show) + + if show: + project = self.GetView().GetDocument() + if not project: + self.OpenSavedProjects() + + + #---------------------------------------------------------------------------- + # Service specific methods + #---------------------------------------------------------------------------- + + def GetSuppressOpenProjectMessages(self): + return self._suppressOpenProjectMessages + + + def SetSuppressOpenProjectMessages(self, suppressOpenProjectMessages): + self._suppressOpenProjectMessages = suppressOpenProjectMessages + + + def GetRunHandlers(self): + return self._runHandlers + + + def AddRunHandler(self, runHandler): + self._runHandlers.append(runHandler) + + + def RemoveRunHandler(self, runHandler): + self._runHandlers.remove(runHandler) + + + def InstallControls(self, frame, menuBar = None, toolBar = None, statusBar = None, document = None): + Service.Service.InstallControls(self, frame, menuBar, toolBar, statusBar, document) + + config = wx.ConfigBase_Get() + + projectMenu = wx.Menu() + +## accelTable = wx.AcceleratorTable([ +## eval(_("wx.ACCEL_CTRL, ord('R'), ProjectService.RUN_ID")) +## ]) +## frame.SetAcceleratorTable(accelTable) + isProjectDocument = document and document.GetDocumentTemplate().GetDocumentType() == ProjectDocument + if wx.GetApp().IsMDI() or isProjectDocument: + if not menuBar.FindItemById(ProjectService.ADD_FILES_TO_PROJECT_ID): + projectMenu.Append(ProjectService.ADD_FILES_TO_PROJECT_ID, _("&Add Files to Project..."), _("Adds a document to the current project")) + wx.EVT_MENU(frame, ProjectService.ADD_FILES_TO_PROJECT_ID, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, ProjectService.ADD_FILES_TO_PROJECT_ID, frame.ProcessUpdateUIEvent) + if not menuBar.FindItemById(ProjectService.ADD_CURRENT_FILE_TO_PROJECT_ID): + projectMenu.Append(ProjectService.ADD_CURRENT_FILE_TO_PROJECT_ID, _("&Add Active File to Project..."), _("Adds the active document to a project")) + wx.EVT_MENU(frame, ProjectService.ADD_CURRENT_FILE_TO_PROJECT_ID, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, ProjectService.ADD_CURRENT_FILE_TO_PROJECT_ID, frame.ProcessUpdateUIEvent) + viewMenuIndex = menuBar.FindMenu(_("&View")) + menuBar.Insert(viewMenuIndex + 1, projectMenu, _("&Project")) + editMenu = menuBar.GetMenu(menuBar.FindMenu(_("&Edit"))) + if not menuBar.FindItemById(ProjectService.RENAME_ID): + editMenu.AppendSeparator() + editMenu.Append(ProjectService.RENAME_ID, _("&Rename"), _("Renames the active item")) + wx.EVT_MENU(frame, ProjectService.RENAME_ID, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, ProjectService.RENAME_ID, frame.ProcessUpdateUIEvent) + + return True + + + def OnCloseFrame(self, event): + if not self.GetView(): + return True + + if wx.GetApp().IsMDI(): + # close all non-project documents first + for document in self.GetDocumentManager().GetDocuments()[:]: # Cloning list to make sure we go through all docs even as they are deleted + if document.GetDocumentTemplate().GetDocumentType() != ProjectDocument: + if not self.GetDocumentManager().CloseDocument(document, False): + return False + + # write project config afterwards because user may change filenames on closing of new documents + self.GetView().WriteProjectConfig() # Called onCloseWindow in all of the other services but needed to be factored out for ProjectService since it is called elsewhere + + # close all project documents after closing other documents + # because user may save a new document with a new name or cancel closing a document + for document in self.GetDocumentManager().GetDocuments()[:]: # Cloning list to make sure we go through all docs even as they are deleted + if document.GetDocumentTemplate().GetDocumentType() == ProjectDocument: + if not document.OnSaveModified(): + return False + + # This is called when any SDI frame is closed, so need to check if message window is closing or some other window + elif self.GetView() == event.GetEventObject().GetView(): + self.SetView(None) + return True + + + #---------------------------------------------------------------------------- + # Event Processing Methods + #---------------------------------------------------------------------------- + + def ProcessEventBeforeWindows(self, event): + id = event.GetId() + if id == wx.ID_CLOSE_ALL: + self.OnFileCloseAll(event) + return True + return False + + + def ProcessEvent(self, event): + if Service.Service.ProcessEvent(self, event): + return True + + id = event.GetId() + if id == ProjectService.RUN_SELECTED_PM_ID: + self.OnRunProcessModel(event, runSelected=True) + return True + elif id == ProjectService.RUN_CURRENT_PM_ID: + self.OnRunProcessModel(event, runCurrentFile=True) + return True + elif id == ProjectService.ADD_CURRENT_FILE_TO_PROJECT_ID: + self.OnAddCurrentFileToProject(event) + return True + elif id == wx.lib.pydocview.FilePropertiesService.PROPERTIES_ID: + if self.GetView(): + return self.GetView().ProcessEvent(event) + else: + return False + else: + return False + + + def ProcessUpdateUIEvent(self, event): + if Service.Service.ProcessUpdateUIEvent(self, event): + return True + + id = event.GetId() + if id == ProjectService.RUNPM_ID or id == ProjectService.RUN_SELECTED_PM_ID or id == ProjectService.RUN_CURRENT_PM_ID: + event.Enable(self._HasOpenedProjects() and self._HasProcessModel()) + return True + elif id == ProjectService.ADD_FILES_TO_PROJECT_ID: + event.Enable(False) + return True + elif id == ProjectService.ADD_CURRENT_FILE_TO_PROJECT_ID: + event.Enable(self._CanAddCurrentFileToProject()) + return True + elif id == ProjectService.RENAME_ID: + event.Enable(False) + return True + elif id == ProjectService.OPEN_SELECTION_ID: + event.Enable(False) + return True + elif id == wx.lib.pydocview.FilePropertiesService.PROPERTIES_ID: + if self.GetView(): + return self.GetView().ProcessUpdateUIEvent(event) + else: + return False + else: + return False + + + def OnRunProcessModel(self, event, runSelected=False, runCurrentFile=False): + project = self.GetView().GetDocument() + + if project: + ext = None + for template in self.GetDocumentManager().GetTemplates(): + if template.GetDocumentType() == ProcessModelEditor.ProcessModelDocument: + ext = template.GetDefaultExtension() + break; + if not ext: + return + + files = filter(lambda f: f.endswith(ext), project.GetFiles()) + if not files: + return + + docs = wx.GetApp().GetDocumentManager().GetDocuments() + for doc in docs: + if doc.GetFilename() in files and doc.GetDocumentTemplate().GetDocumentType() == ProcessModelEditor.ProcessModelDocument: + if not doc.GetProcessModel().beginProcess: + wx.MessageBox(_("Cannot run process. No begin action found."), _("Run Process")) + return + + filesModified = False + for doc in docs: + if doc.IsModified(): + filesModified = True + break + if filesModified: + frame = self.GetView().GetFrame() + yesNoMsg = wx.MessageDialog(frame, + _("Files have been modified. Process may not reflect your current changes.\n\nWould you like to save all files before running?"), + _("Run Process"), + wx.YES_NO + ) + if yesNoMsg.ShowModal() == wx.ID_YES: + wx.GetTopLevelParent(frame).OnFileSaveAll(None) + + if runCurrentFile: + fileToRun = self.GetDocumentManager().GetCurrentDocument().GetFilename() + elif runSelected: + fileToRun = self.GetView().GetSelectedFile() + elif len(files) > 1: + files.sort(lambda a, b: cmp(os.path.basename(a).lower(), os.path.basename(b).lower())) + strings = map(lambda file: os.path.basename(file), files) + res = wx.GetSingleChoiceIndex(_("Select a process to run:"), + _("Run"), + strings, + project.GetFirstView()._GetParentFrame()) + if res == -1: + return + fileToRun = files[res] + else: + fileToRun = files[0] + + self.RunProcessModel(fileToRun) + + + def RunProcessModel(self, fileToRun): + for runHandler in self.GetRunHandlers(): + if runHandler.RunProjectFile(fileToRun): + return + os.system('"' + fileToRun + '"') + + + def _HasProcessModel(self): + project = self.GetView().GetDocument() + + if project: + ext = None + for template in self.GetDocumentManager().GetTemplates(): + if template.GetDocumentType() == ProcessModelEditor.ProcessModelDocument: + ext = template.GetDefaultExtension() + break; + if not ext: + return False + + files = filter(lambda f: f.endswith(ext), project.GetFiles()) + if not files: + return False + + if len(files): + return True + + return False + + + def _HasOpenedProjects(self): + for document in self.GetDocumentManager().GetDocuments(): + if document.GetDocumentTemplate().GetDocumentType() == ProjectDocument: + return True + return False + + + def _HasCurrentFile(self): + currentDoc = self.GetDocumentManager().GetCurrentDocument() + return currentDoc + + + def _CanAddCurrentFileToProject(self): + currentDoc = self.GetDocumentManager().GetCurrentDocument() + if not currentDoc: + return False + if currentDoc.GetDocumentTemplate().GetDocumentType() == ProjectDocument: + return False + if not currentDoc._savedYet: + return False + for document in self.GetDocumentManager().GetDocuments(): + if document.GetDocumentTemplate().GetDocumentType() == ProjectDocument: + return True + return False # There are no documents open + + + def GetFilesFromCurrentProject(self): + view = self.GetView() + if view: + project = view.GetDocument() + if project: + return project.GetFiles() + return None + + + def GetCurrentProject(self): + view = self.GetView() + if view: + return view.GetDocument() + return None + + + def FindProjectByFile(self, filename): + for document in self.GetDocumentManager().GetDocuments(): + if document.GetDocumentTemplate().GetDocumentType() == ProjectDocument: + if document.GetFilename() == filename: + return document + elif document.IsFileInProject(filename): + return document + return None + + + def GetCurrentProjectNames(self): + projects = [] + for document in self.GetDocumentManager().GetDocuments(): + if document.GetDocumentTemplate().GetDocumentType() == ProjectDocument: + projects.append(document) + if not projects: + return + projects.sort(lambda a, b: cmp(a.GetPrintableName().lower(), b.GetPrintableName().lower())) + strings = map(lambda project: project.GetPrintableName(), projects) + return strings + + def OnAddCurrentFileToProject(self, event): + if not self._CanAddCurrentFileToProject(): + return + projects = [] + for document in self.GetDocumentManager().GetDocuments(): + if document.GetDocumentTemplate().GetDocumentType() == ProjectDocument: + projects.append(document) + if not projects: + return + projects.sort(lambda a, b: cmp(a.GetPrintableName().lower(), b.GetPrintableName().lower())) + strings = map(lambda project: project.GetPrintableName(), projects) + res = wx.GetSingleChoiceIndex(_("Select a project to add the file to:"), + _("Add to Project"), + strings, + self.GetDocumentManager().FindSuitableParent()) + if res == -1: + return + file = self.GetDocumentManager().GetCurrentDocument().GetFilename() + projects[res].GetCommandProcessor().Submit(ProjectAddFilesCommand(projects[res], [file])) + self.GetView().Activate(True) # after add, should put focus on project editor + + + def OnFileCloseAll(self, event): + for document in self.GetDocumentManager().GetDocuments()[:]: # Cloning list to make sure we go through all docs even as they are deleted + if document.GetDocumentTemplate().GetDocumentType() != ProjectDocument: + if not self.GetDocumentManager().CloseDocument(document, False): + return + # document.DeleteAllViews() # Implicitly delete the document when the last view is removed + + + def OpenSavedProjects(self): + config = wx.ConfigBase_Get() + openedDocs = False + if config.ReadInt("ProjectSaveDocs", True): + docString = config.Read("ProjectSavedDocs") + if docString: + doc = None + for fileName in eval(docString): + if isinstance(fileName, types.StringTypes): + if os.path.exists(fileName): + doc = self.GetDocumentManager().CreateDocument(fileName, wx.lib.docview.DOC_SILENT) + + if doc: + openedDocs = True + expandedString = config.Read("ProjectExpandedSavedDocs") + if expandedString: + view = doc.GetFirstView() + view.SetExpandedProjects(eval(expandedString)) + return openedDocs + +#---------------------------------------------------------------------------- +# Icon Bitmaps - generated by encode_bitmaps.py +#---------------------------------------------------------------------------- +from wx import ImageFromStream, BitmapFromImage +from wx import EmptyIcon +import cStringIO + + +def getProjectData(): + return \ +'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x10\x00\x00\x00\x10\x08\x06\ +\x00\x00\x00\x1f\xf3\xffa\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\ +\x00\x00[IDAT8\x8d\xc5\x93\xc1\n\xc00\x08C\x8d\xf6\xff\xffX\xb3Sa-\xf6`;:O\n\ +\x12\x1fj\x0059\t\xed\t\xc3\xc9pn\x0b\x88\x88@\rU\x81\xf6.\x18N\xa8aE\x92\rh\ +YC\x85\xa4D\x90\x91\xdc%\xf8w\x07+\xd1\xfbW\x98\xc5\x8f\t\x86W\xee\x93+\xbe\ +\xc0gn\xdc\x8d\x07\xab"<iG\x8e\xa9\r\x00\x00\x00\x00IEND\xaeB`\x82' + +def getProjectBitmap(): + return BitmapFromImage(getProjectImage()) + +def getProjectImage(): + stream = cStringIO.StringIO(getProjectData()) + return ImageFromStream(stream) + +def getProjectIcon(): + icon = EmptyIcon() + icon.CopyFromBitmap(getProjectBitmap()) + return icon + + +#---------------------------------------------------------------------------- + +def getBlankData(): + return \ +"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00 \x00\x00\x00 \x08\x06\x00\ +\x00\x00szz\xf4\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\x00\x00\ +\x85IDATX\x85\xed\x97\xc9\n\xc0 \x0cD3\xda\xff\xffcMo\x96Z\xc4\xa5\x91\x14:9\ +\x8a\xe8\xcb\xd3\xb8\x00!\x8ag\x04\xd7\xd9E\xe4\xa8\x1b4'}3 B\xc4L\x7fs\x03\ +\xb3\t<\x0c\x94\x81tN\x04p%\xae9\xe9\xa8\x89m{`\xd4\x84\xfd\x12\xa8\x16{#\ +\x10\xdb\xab\xa0\x07a\x0e\x00\xe0\xb6\x1fz\x10\xdf;\x07V\xa3U5\xb5\x8d:\xdc\ +\r\x10\x80\x00\x04 \x00\x01\x08@\x80\xe6{\xa0w\x8f[\x85\xbb\x01\xfc\xfeoH\ +\x80\x13>\xf9(3zH\x1e\xfb\x00\x00\x00\x00IEND\xaeB`\x82" + + +def getBlankBitmap(): + return BitmapFromImage(getBlankImage()) + +def getBlankImage(): + stream = cStringIO.StringIO(getBlankData()) + return ImageFromStream(stream) + +def getBlankIcon(): + icon = EmptyIcon() + icon.CopyFromBitmap(getBlankBitmap()) + return icon + diff --git a/wxPython/samples/ide/activegrid/tool/PythonEditor.py b/wxPython/samples/ide/activegrid/tool/PythonEditor.py new file mode 100644 index 0000000000..06236d126c --- /dev/null +++ b/wxPython/samples/ide/activegrid/tool/PythonEditor.py @@ -0,0 +1,614 @@ +#---------------------------------------------------------------------------- +# Name: PythonEditor.py +# Purpose: PythonEditor for wx.lib.pydocview tbat uses the Styled Text Control +# +# Author: Peter Yared +# +# Created: 8/15/03 +# CVS-ID: $Id$ +# Copyright: (c) 2004-2005 ActiveGrid, Inc. +# License: wxWindows License +#---------------------------------------------------------------------------- + +import CodeEditor +import wx +import wx.lib.docview +import wx.lib.pydocview +import string +import keyword # So it knows what to hilite +import wx.py # For the Python interpreter +import wx.stc # For the Python interpreter +import cStringIO # For indent +import OutlineService +import STCTextEditor +import keyword # for GetAutoCompleteKeywordList +import sys # for GetAutoCompleteKeywordList +import MessageService # for OnCheckCode +import OutlineService +try: + import checker # for pychecker + _CHECKER_INSTALLED = True +except ImportError: + _CHECKER_INSTALLED = False +import os.path # for pychecker +_ = wx.GetTranslation + +if wx.Platform == '__WXMSW__': + _WINDOWS = True +else: + _WINDOWS = False + + +VIEW_PYTHON_INTERPRETER_ID = wx.NewId() + + +class PythonDocument(CodeEditor.CodeDocument): + pass + + +class PythonView(CodeEditor.CodeView): + + + def ProcessUpdateUIEvent(self, event): + if not self.GetCtrl(): + return False + + id = event.GetId() + if id == CodeEditor.CHECK_CODE_ID: + hasText = self.GetCtrl().GetTextLength() > 0 + event.Enable(hasText) + return True + + return CodeEditor.CodeView.ProcessUpdateUIEvent(self, event) + + + def GetCtrlClass(self): + """ Used in split window to instantiate new instances """ + return PythonCtrl + + + def OnActivateView(self, activate, activeView, deactiveView): + STCTextEditor.TextView.OnActivateView(self, activate, activeView, deactiveView) + if activate: + wx.CallAfter(self.LoadOutline) # need CallAfter because document isn't loaded yet + + + def OnClose(self, deleteWindow = True): + status = STCTextEditor.TextView.OnClose(self, deleteWindow) + wx.CallAfter(self.ClearOutline) # need CallAfter because when closing the document, it is Activated and then Close, so need to match OnActivateView's CallAfter + return status + + + def GetAutoCompleteKeywordList(self, context, hint): + obj = None + try: + if context and len(context): + obj = eval(context, globals(), locals()) + except: + if not hint or len(hint) == 0: # context isn't valid, maybe it was the hint + hint = context + + if obj is None: + kw = keyword.kwlist[:] + else: + symTbl = dir(obj) + kw = filter(lambda item: item[0] != '_', symTbl) # remove local variables and methods + + if hint and len(hint): + lowerHint = hint.lower() + filterkw = filter(lambda item: item.lower().startswith(lowerHint), kw) # remove variables and methods that don't match hint + kw = filterkw + + kw.sort(self.CaseInsensitiveCompare) + + if hint: + replaceLen = len(hint) + else: + replaceLen = 0 + + return " ".join(kw), replaceLen + + + def OnCheckCode(self): + if not _CHECKER_INSTALLED: + wx.MessageBox(_("pychecker not found. Please install pychecker."), _("Check Code")) + return + + filename = os.path.basename(self.GetDocument().GetFilename()) + + # pychecker only works on files, doesn't take a stream or string input + if self.GetDocument().IsModified(): + dlg = wx.MessageDialog(self.GetFrame(), _("'%s' has been modfied and must be saved first. Save file and check code?") % filename, _("Check Code")) + val = dlg.ShowModal() + dlg.Destroy() + if val == wx.ID_OK: + self.GetDocument().Save() + else: + return + + messageService = wx.GetApp().GetService(MessageService.MessageService) + messageService.ShowWindow() + view = messageService.GetView() + if not view: + return + + view.ClearLines() + view.SetCallback(self.OnJumpToFoundLine) + + # Set cursor to Wait cursor + wx.GetApp().GetTopWindow().SetCursor(wx.StockCursor(wx.CURSOR_WAIT)) + + # This takes a while for involved code + checker.checkSyntax(self.GetDocument().GetFilename(), view) + + # Set cursor to Default cursor + wx.GetApp().GetTopWindow().SetCursor(wx.StockCursor(wx.CURSOR_DEFAULT)) + + + def OnJumpToFoundLine(self, event): + messageService = wx.GetApp().GetService(MessageService.MessageService) + lineText, pos = messageService.GetView().GetCurrLine() + + lineEnd = lineText.find(".py:") + if lineEnd == -1: + return + + lineStart = lineEnd + len(".py:") + lineEnd = lineText.find(":", lineStart) + lineNum = int(lineText[lineStart:lineEnd]) + + filename = lineText[0:lineStart - 1] + + foundView = None + openDocs = wx.GetApp().GetDocumentManager().GetDocuments() + for openDoc in openDocs: + if openDoc.GetFilename() == filename: + foundView = openDoc.GetFirstView() + break + + if not foundView: + doc = wx.GetApp().GetDocumentManager().CreateDocument(filename, wx.lib.docview.DOC_SILENT) + foundView = doc.GetFirstView() + + if foundView: + foundView.GetFrame().SetFocus() + foundView.Activate() + foundView.GotoLine(lineNum) + startPos = foundView.PositionFromLine(lineNum) + endPos = foundView.GetLineEndPosition(lineNum) + # wxBug: Need to select in reverse order, (end, start) to put cursor at head of line so positioning is correct + # Also, if we use the correct positioning order (start, end), somehow, when we open a edit window for the first + # time, we don't see the selection, it is scrolled off screen + foundView.SetSelection(endPos, startPos) + wx.GetApp().GetService(OutlineService.OutlineService).LoadOutline(foundView, position=startPos) + + + +class PythonInterpreterView(wx.lib.docview.View): + + + def OnCreate(self, doc, flags): + frame = wx.GetApp().CreateDocumentFrame(self, doc, flags) + sizer = wx.BoxSizer() + self._pyCrust = wx.py.crust.Crust(frame) + sizer.Add(self._pyCrust, 1, wx.EXPAND, 0) + frame.SetSizer(sizer) + frame.Layout() + self.Activate() + frame.Show() + return True + + + def ProcessEvent(self, event): + if not hasattr(self, "_pyCrust") or not self._pyCrust: + return wx.lib.docview.View.ProcessEvent(self, event) + stcControl = wx.Window_FindFocus() + if not isinstance(stcControl, wx.stc.StyledTextCtrl): + return wx.lib.docview.View.ProcessEvent(self, event) + id = event.GetId() + if id == wx.ID_UNDO: + stcControl.Undo() + return True + elif id == wx.ID_REDO: + stcControl.Redo() + return True + elif id == wx.ID_CUT: + stcControl.Cut() + return True + elif id == wx.ID_COPY: + stcControl.Copy() + return True + elif id == wx.ID_PASTE: + stcControl.Paste() + return True + elif id == wx.ID_CLEAR: + stcControl.Clear() + return True + elif id == wx.ID_SELECTALL: + stcControl.SetSelection(0, -1) + return True + else: + return wx.lib.docview.View.ProcessEvent(self, event) + + + def ProcessUpdateUIEvent(self, event): + if not hasattr(self, "_pyCrust") or not self._pyCrust: + return wx.lib.docview.View.ProcessUpdateUIEvent(self, event) + stcControl = wx.Window_FindFocus() + if not isinstance(stcControl, wx.stc.StyledTextCtrl): + return wx.lib.docview.View.ProcessUpdateUIEvent(self, event) + id = event.GetId() + if id == wx.ID_UNDO: + event.Enable(stcControl.CanUndo()) + return True + elif id == wx.ID_REDO: + event.Enable(stcControl.CanRedo()) + return True + elif id == wx.ID_CUT: + event.Enable(stcControl.CanCut()) + return True + elif id == wx.ID_COPY: + event.Enable(stcControl.CanCopy()) + return True + elif id == wx.ID_PASTE: + event.Enable(stcControl.CanPaste()) + return True + elif id == wx.ID_CLEAR: + event.Enable(True) # wxBug: should be stcControl.CanCut()) but disabling clear item means del key doesn't work in control as expected + return True + elif id == wx.ID_SELECTALL: + event.Enable(stcControl.GetTextLength() > 0) + return True + else: + return wx.lib.docview.View.ProcessUpdateUIEvent(self, event) + + + def OnClose(self, deleteWindow=True): + if deleteWindow and self.GetFrame(): + self.GetFrame().Destroy() + return True + + +class PythonService(CodeEditor.CodeService): + + + def __init__(self): + CodeEditor.CodeService.__init__(self) + + + def InstallControls(self, frame, menuBar = None, toolBar = None, statusBar = None, document = None): + CodeEditor.CodeService.InstallControls(self, frame, menuBar, toolBar, statusBar, document) + + if document and document.GetDocumentTemplate().GetDocumentType() != PythonDocument: + return + if not document and wx.GetApp().GetDocumentManager().GetFlags() & wx.lib.docview.DOC_SDI: + return + + viewMenu = menuBar.GetMenu(menuBar.FindMenu(_("&View"))) + + viewStatusBarItemPos = self.GetMenuItemPos(viewMenu, wx.lib.pydocview.VIEW_STATUSBAR_ID) + viewMenu.InsertCheckItem(viewStatusBarItemPos + 1, VIEW_PYTHON_INTERPRETER_ID, _("Python &Interpreter"), _("Shows or hides the Python interactive window")) + wx.EVT_MENU(frame, VIEW_PYTHON_INTERPRETER_ID, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, VIEW_PYTHON_INTERPRETER_ID, frame.ProcessUpdateUIEvent) + + + def ProcessEvent(self, event): + id = event.GetId() + if id == VIEW_PYTHON_INTERPRETER_ID: + self.OnViewPythonInterpreter(event) + return True + else: + return CodeEditor.CodeService.ProcessEvent(self, event) + + + def ProcessUpdateUIEvent(self, event): + id = event.GetId() + if id == VIEW_PYTHON_INTERPRETER_ID: + event.Enable(True) + docManager = wx.GetApp().GetDocumentManager() + event.Check(False) + for doc in docManager.GetDocuments(): + if isinstance(doc.GetFirstView(), PythonInterpreterView): + event.Check(True) + break + return True + else: + return CodeEditor.CodeService.ProcessUpdateUIEvent(self, event) + + + def OnViewPythonInterpreter(self, event): + for doc in wx.GetApp().GetDocumentManager().GetDocuments(): + if isinstance(doc.GetFirstView(), PythonInterpreterView): + doc.GetFirstView().GetDocument().DeleteAllViews() + return + + docManager = self.GetDocumentManager() + template = wx.lib.docview.DocTemplate(docManager, + _("Python Interpreter"), + "*.Foobar", + "Foobar", + ".Foobar", + _("Python Interpreter Document"), + _("Python Interpreter View"), + wx.lib.docview.Document, + PythonInterpreterView, + flags = wx.lib.docview.TEMPLATE_INVISIBLE) + newDoc = template.CreateDocument('', wx.lib.docview.DOC_SILENT) + if newDoc: + newDoc.SetDocumentName(template.GetDocumentName()) + newDoc.SetDocumentTemplate(template) + newDoc.OnNewDocument() + newDoc.SetWriteable(False) + newDoc.GetFirstView().GetFrame().SetTitle(_("Python Interpreter")) + + +class PythonCtrl(CodeEditor.CodeCtrl): + + + def __init__(self, parent, ID = -1, style = wx.NO_FULL_REPAINT_ON_RESIZE): + CodeEditor.CodeCtrl.__init__(self, parent, ID, style) + self.SetProperty("tab.timmy.whinge.level", "1") + self.SetProperty("fold.comment.python", "1") + self.SetProperty("fold.quotes.python", "1") + self.SetLexer(wx.stc.STC_LEX_PYTHON) + self.SetKeyWords(0, string.join(keyword.kwlist)) + + + def SetViewDefaults(self): + CodeEditor.CodeCtrl.SetViewDefaults(self, configPrefix = "Python", hasWordWrap = False, hasTabs = True) + + + def GetFontAndColorFromConfig(self): + return CodeEditor.CodeCtrl.GetFontAndColorFromConfig(self, configPrefix = "Python") + + + def UpdateStyles(self): + CodeEditor.CodeCtrl.UpdateStyles(self) + + if not self.GetFont(): + return + + faces = { 'font' : self.GetFont().GetFaceName(), + 'size' : self.GetFont().GetPointSize(), + 'size2': self.GetFont().GetPointSize() - 2, + 'color' : "%02x%02x%02x" % (self.GetFontColor().Red(), self.GetFontColor().Green(), self.GetFontColor().Blue()) + } + + # Python styles + # White space + self.StyleSetSpec(wx.stc.STC_P_DEFAULT, "face:%(font)s,fore:#000000,face:%(font)s,size:%(size)d" % faces) + # Comment + self.StyleSetSpec(wx.stc.STC_P_COMMENTLINE, "face:%(font)s,fore:#007F00,italic,face:%(font)s,size:%(size)d" % faces) + # Number + self.StyleSetSpec(wx.stc.STC_P_NUMBER, "face:%(font)s,fore:#007F7F,size:%(size)d" % faces) + # String + self.StyleSetSpec(wx.stc.STC_P_STRING, "face:%(font)s,fore:#7F007F,face:%(font)s,size:%(size)d" % faces) + # Single quoted string + self.StyleSetSpec(wx.stc.STC_P_CHARACTER, "face:%(font)s,fore:#7F007F,face:%(font)s,size:%(size)d" % faces) + # Keyword + self.StyleSetSpec(wx.stc.STC_P_WORD, "face:%(font)s,fore:#00007F,bold,size:%(size)d" % faces) + # Triple quotes + self.StyleSetSpec(wx.stc.STC_P_TRIPLE, "face:%(font)s,fore:#7F0000,size:%(size)d" % faces) + # Triple double quotes + self.StyleSetSpec(wx.stc.STC_P_TRIPLEDOUBLE, "face:%(font)s,fore:#7F0000,size:%(size)d" % faces) + # Class name definition + self.StyleSetSpec(wx.stc.STC_P_CLASSNAME, "face:%(font)s,fore:#0000FF,bold,size:%(size)d" % faces) + # Function or method name definition + self.StyleSetSpec(wx.stc.STC_P_DEFNAME, "face:%(font)s,fore:#007F7F,bold,size:%(size)d" % faces) + # Operators + self.StyleSetSpec(wx.stc.STC_P_OPERATOR, "face:%(font)s,size:%(size)d" % faces) + # Identifiers + self.StyleSetSpec(wx.stc.STC_P_IDENTIFIER, "face:%(font)s,fore:#%(color)s,face:%(font)s,size:%(size)d" % faces) + # Comment-blocks + self.StyleSetSpec(wx.stc.STC_P_COMMENTBLOCK, "face:%(font)s,fore:#7F7F7F,size:%(size)d" % faces) + # End of line where string is not closed + self.StyleSetSpec(wx.stc.STC_P_STRINGEOL, "face:%(font)s,fore:#000000,face:%(font)s,back:#E0C0E0,eol,size:%(size)d" % faces) + + + def OnUpdateUI(self, evt): + braces = self.GetMatchingBraces() + + # 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 braces and styleBefore == wx.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 braces and styleAfter == wx.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) + + evt.Skip() + + + def DoIndent(self): + (text, caretPos) = self.GetCurLine() + + self._tokenizerChars = {} # This is really too much, need to find something more like a C array + for i in range(len(text)): + self._tokenizerChars[i] = 0 + + ctext = cStringIO.StringIO(text) + try: + tokenize.tokenize(ctext.readline, self) + except: + pass + + # Left in for debugging purposes: + #for i in range(len(text)): + # print i, text[i], self._tokenizerChars[i] + + if caretPos == 0 or len(string.strip(text)) == 0: # At beginning of line or within an empty line + self.AddText('\n') + else: + doExtraIndent = False + brackets = False + commentStart = -1 + if caretPos > 1: + startParenCount = 0 + endParenCount = 0 + startSquareBracketCount = 0 + endSquareBracketCount = 0 + startCurlyBracketCount = 0 + endCurlyBracketCount = 0 + startQuoteCount = 0 + endQuoteCount = 0 + for i in range(caretPos - 1, -1, -1): # Go through each character before the caret + if i >= len(text): # Sometimes the caret is at the end of the text if there is no LF + continue + if self._tokenizerChars[i] == 1: + continue + elif self._tokenizerChars[i] == 2: + startQuoteCount = startQuoteCount + 1 + elif self._tokenizerChars[i] == 3: + endQuoteCount = endQuoteCount + 1 + elif text[i] == '(': # Would be nice to use a dict for this, but the code is much more readable this way + startParenCount = startParenCount + 1 + elif text[i] == ')': + endParenCount = endParenCount + 1 + elif text[i] == "[": + startSquareBracketCount = startSquareBracketCount + 1 + elif text[i] == "]": + endSquareBracketCount = endSquareBracketCount + 1 + elif text[i] == "{": + startCurlyBracketCount = startCurlyBracketCount + 1 + elif text[i] == "}": + endCurlyBracketCount = endCurlyBracketCount + 1 + elif text[i] == "#": + commentStart = i + break + if startQuoteCount > endQuoteCount or startParenCount > endParenCount or startSquareBracketCount > endSquareBracketCount or startCurlyBracketCount > endCurlyBracketCount: + if i + 1 >= caretPos: # Caret is right at the open paren, so just do indent as if colon was there + doExtraIndent = True + break + else: + spaces = " " * (i + 1) + brackets = True + break + if not brackets: + spaces = text[0:len(text) - len(string.lstrip(text))] + if caretPos < len(spaces): # If within the opening spaces of a line + spaces = spaces[:caretPos] + + # strip comment off + if commentStart != -1: + text = text[0:commentStart] + + textNoTrailingSpaces = text[0:caretPos].rstrip() + if doExtraIndent or len(textNoTrailingSpaces) and textNoTrailingSpaces[-1] == ':': + spaces = spaces + ' ' * self.GetIndent() + self.AddText('\n' + spaces) + + + # Callback for tokenizer in self.DoIndent + def __call__(self, toktype, toktext, (srow,scol), (erow,ecol), line): + if toktype == tokenize.COMMENT: + for i in range(scol, ecol + 1): + self._validChars[i] = False + elif toktype == token.STRING: + self._tokenizerChars[scol] = 2 # Open quote + self._tokenizerChars[ecol - 1] = 3 # Close quote + for i in range(scol + 1, ecol - 2): + self._tokenizerChars[i] = 1 # Part of string, 1 == ignore the char + + +class PythonOptionsPanel(wx.Panel): + + def __init__(self, parent, id): + wx.Panel.__init__(self, parent, id) + pathLabel = wx.StaticText(self, -1, _("python.exe Path:")) + config = wx.ConfigBase_Get() + path = config.Read("ActiveGridPythonLocation") + self._pathTextCtrl = wx.TextCtrl(self, -1, path, size = (150, -1)) + self._pathTextCtrl.SetToolTipString(self._pathTextCtrl.GetValue()) + self._pathTextCtrl.SetInsertionPointEnd() + choosePathButton = wx.Button(self, -1, _("Browse...")) + pathSizer = wx.BoxSizer(wx.HORIZONTAL) + HALF_SPACE = 5 + pathSizer.Add(pathLabel, 0, wx.ALIGN_LEFT | wx.LEFT | wx.RIGHT | wx.TOP, HALF_SPACE) + pathSizer.Add(self._pathTextCtrl, 0, wx.ALIGN_LEFT | wx.EXPAND | wx.RIGHT, HALF_SPACE) + pathSizer.Add(choosePathButton, 0, wx.ALIGN_RIGHT | wx.LEFT, HALF_SPACE) + wx.EVT_BUTTON(self, choosePathButton.GetId(), self.OnChoosePath) + mainSizer = wx.BoxSizer(wx.VERTICAL) + mainSizer.Add(pathSizer, 0, wx.LEFT | wx.RIGHT | wx.TOP, 10) + + self._otherOptions = STCTextEditor.TextOptionsPanel(self, -1, configPrefix = "Python", label = "Python", hasWordWrap = False, hasTabs = True, addPage=False) + mainSizer.Add(self._otherOptions) + self.SetSizer(mainSizer) + parent.AddPage(self, _("Python")) + + def OnChoosePath(self, event): + if _WINDOWS: + wildcard = _("*.exe") + else: + wildcard = _("*") + path = wx.FileSelector(_("Select a File"), + _(""), + _(""), + wildcard = wildcard , + flags = wx.HIDE_READONLY, + parent = wx.GetApp().GetTopWindow()) + if path: + self._pathTextCtrl.SetValue(path) + self._pathTextCtrl.SetToolTipString(self._pathTextCtrl.GetValue()) + self._pathTextCtrl.SetInsertionPointEnd() + + def OnOK(self, optionsDialog): + if len(self._pathTextCtrl.GetValue()) > 0: + config = wx.ConfigBase_Get() + config.Write("ActiveGridPythonLocation", self._pathTextCtrl.GetValue()) + + self._otherOptions.OnOK(optionsDialog) +#---------------------------------------------------------------------------- +# Icon Bitmaps - generated by encode_bitmaps.py +#---------------------------------------------------------------------------- +from wx import ImageFromStream, BitmapFromImage +from wx import EmptyIcon +import cStringIO + + +def getPythonData(): + return \ +"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x10\x00\x00\x00\x10\x08\x06\ +\x00\x00\x00\x1f\xf3\xffa\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\ +\x00\x00\xd5IDAT8\x8d\x8d\x93Y\x0e\xc3 \x0cD\x9fM\xcf\xddNr2.\x96\xb8\x1f\ +\x05\n\x84.#Y\x10\xa3\x19o\xb1\x99'*\xe2<\x82\x0e\xe6\xc9\xf8\x01\xef?\xa4\ +\xf7)]\x05\x970O\xcdr\xce!\x119\xe7\x00\x02\x88\xfe}i\xb5\x848\x8f\xa8\x19\ +\xcc\x19}+\xc5\xcc\xd3\x92<CZ\x0b\x99\xc4\xb2N\x01<\x80\xad\xdc?\x88\xf8\x1c\ +X\x8f7\xe1\x1f\xdc*\xa9a+\xe1\xa3\xdc\xe7\xb4\xf6\xd1\xe5\xb6'\xc3@\xc5\xa0#\ +\xab\x94\xd1\x0bL\xf0\xe6\x17\xa8v\xc3\x8aS\xa0.\x8be\x13\xe3\x15\x8f\xe1\ +\xa5D\xee\xc9\xdb~%\xc7y\x84\xbb'sO\xd6\xd4\x17\xe4~\xc4\xf5\xef\xac\xa7\r\ +\xbbp?b&\x0f\x89i\x14\x93\xca\x14z\xc5oh\x02E\xc4<\xd92\x03\xe0:B^\xc4K#\xe7\ +\xe5\x00\x02\xfd\xb9H\x9ex\x02\x9a\x05a\xd2\xd3c\xc0\xcc\x00\x00\x00\x00IEND\ +\xaeB`\x82" + + +def getPythonBitmap(): + return BitmapFromImage(getPythonImage()) + +def getPythonImage(): + stream = cStringIO.StringIO(getPythonData()) + return ImageFromStream(stream) + +def getPythonIcon(): + icon = EmptyIcon() + icon.CopyFromBitmap(getPythonBitmap()) + return icon diff --git a/wxPython/samples/ide/activegrid/tool/STCTextEditor.py b/wxPython/samples/ide/activegrid/tool/STCTextEditor.py new file mode 100644 index 0000000000..fca235ada2 --- /dev/null +++ b/wxPython/samples/ide/activegrid/tool/STCTextEditor.py @@ -0,0 +1,1293 @@ +#---------------------------------------------------------------------------- +# Name: STCTextEditor.py +# Purpose: Text Editor for wx.lib.pydocview tbat uses the Styled Text Control +# +# Author: Peter Yared, Morgan Hua +# +# Created: 8/10/03 +# CVS-ID: $Id$ +# Copyright: (c) 2003-2005 ActiveGrid, Inc. +# License: wxWindows License +#---------------------------------------------------------------------------- + +import wx +import wx.stc +import wx.lib.docview +import wx.lib.multisash +import wx.lib.pydocview +import string +import FindService +import os +import sys +_ = wx.GetTranslation + +#---------------------------------------------------------------------------- +# Constants +#---------------------------------------------------------------------------- + +TEXT_ID = wx.NewId() +VIEW_WHITESPACE_ID = wx.NewId() +VIEW_EOL_ID = wx.NewId() +VIEW_INDENTATION_GUIDES_ID = wx.NewId() +VIEW_RIGHT_EDGE_ID = wx.NewId() +VIEW_LINE_NUMBERS_ID = wx.NewId() +ZOOM_ID = wx.NewId() +ZOOM_NORMAL_ID = wx.NewId() +ZOOM_IN_ID = wx.NewId() +ZOOM_OUT_ID = wx.NewId() +CHOOSE_FONT_ID = wx.NewId() +WORD_WRAP_ID = wx.NewId() +TEXT_STATUS_BAR_ID = wx.NewId() + + +#---------------------------------------------------------------------------- +# Classes +#---------------------------------------------------------------------------- + +class TextDocument(wx.lib.docview.Document): + + + def OnSaveDocument(self, filename): + view = self.GetFirstView() + docFile = file(self._documentFile, "w") + docFile.write(view.GetValue()) + docFile.close() + self.Modify(False) + self.SetDocumentSaved(True) + return True + + + def OnOpenDocument(self, filename): + view = self.GetFirstView() + docFile = file(self._documentFile, 'r') + data = docFile.read() + view.SetValue(data) + self.SetFilename(filename, True) + self.Modify(False) + self.UpdateAllViews() + self._savedYet = True + return True + + + def IsModified(self): + view = self.GetFirstView() + if view: + return wx.lib.docview.Document.IsModified(self) or view.IsModified() + else: + return wx.lib.docview.Document.IsModified(self) + + + def Modify(self, mod): + view = self.GetFirstView() + wx.lib.docview.Document.Modify(self, mod) + if not mod and view: + view.SetModifyFalse() + + + def OnCreateCommandProcessor(self): + # Don't create a command processor, it has its own + pass + +# Use this to override MultiClient.Select to prevent yellow background. +def MultiClientSelectBGNotYellow(a): + a.GetParent().multiView.UnSelect() + a.selected = True + #a.SetBackgroundColour(wx.Colour(255,255,0)) # Yellow + a.Refresh() + +class TextView(wx.lib.docview.View): + MARKER_NUM = 0 + MARKER_MASK = 0x1 + + #---------------------------------------------------------------------------- + # Overridden methods + #---------------------------------------------------------------------------- + + def __init__(self): + wx.lib.docview.View.__init__(self) + self._textEditor = None + self._markerCount = 0 + self._commandProcessor = None + self._multiSash = None + + + def GetCtrlClass(self): + return TextCtrl + + + def GetCtrl(self): + # look for active one first + self._textEditor = self._GetActiveCtrl(self._multiSash) + if self._textEditor == None: # it is possible none are active + # look for any existing one + self._textEditor = self._FindCtrl(self._multiSash) + return self._textEditor + + +## def GetCtrls(self, parent = None): +## """ Walk through the MultiSash windows and find all Ctrls """ +## controls = [] +## if isinstance(parent, self.GetCtrlClass()): +## return [parent] +## if hasattr(parent, "GetChildren"): +## for child in parent.GetChildren(): +## controls = controls + self.GetCtrls(child) +## return controls + + + def OnCreate(self, doc, flags): + frame = wx.GetApp().CreateDocumentFrame(self, doc, flags, style = wx.DEFAULT_FRAME_STYLE | wx.NO_FULL_REPAINT_ON_RESIZE) + wx.lib.multisash.MultiClient.Select = MultiClientSelectBGNotYellow + self._multiSash = wx.lib.multisash.MultiSash(frame, -1) + self._multiSash.SetDefaultChildClass(self.GetCtrlClass()) # wxBug: MultiSash instantiates the first TextCtrl with this call + self._textEditor = self.GetCtrl() # wxBug: grab the TextCtrl from the MultiSash datastructure + self._CreateSizer(frame) + self.Activate() + frame.Show(True) + frame.Layout() + return True + + + def _GetActiveCtrl(self, parent): + """ Walk through the MultiSash windows and find the active Control """ + if isinstance(parent, wx.lib.multisash.MultiClient) and parent.selected: + return parent.child + if hasattr(parent, "GetChildren"): + for child in parent.GetChildren(): + found = self._GetActiveCtrl(child) + if found: + return found + return None + + + def _FindCtrl(self, parent): + """ Walk through the MultiSash windows and find the first TextCtrl """ + if isinstance(parent, self.GetCtrlClass()): + return parent + if hasattr(parent, "GetChildren"): + for child in parent.GetChildren(): + found = self._FindCtrl(child) + if found: + return found + return None + + + def _CreateSizer(self, frame): + sizer = wx.BoxSizer(wx.HORIZONTAL) + sizer.Add(self._multiSash, 1, wx.EXPAND) + frame.SetSizer(sizer) + frame.SetAutoLayout(True) + + + def OnUpdate(self, sender = None, hint = None): + if hint == "ViewStuff": + self.GetCtrl().SetViewDefaults() + elif hint == "Font": + font, color = self.GetFontAndColorFromConfig() + self.GetCtrl().SetFont(font) + self.GetCtrl().SetFontColor(color) + + + def OnActivateView(self, activate, activeView, deactiveView): + if activate and self.GetCtrl(): + # In MDI mode just calling set focus doesn't work and in SDI mode using CallAfter causes an endless loop + if self.GetDocumentManager().GetFlags() & wx.lib.docview.DOC_SDI: + self.GetCtrl().SetFocus() + else: + wx.CallAfter(self.GetCtrl().SetFocus) + + + def OnClose(self, deleteWindow = True): + if not wx.lib.docview.View.OnClose(self, deleteWindow): + return False + self.Activate(False) + if deleteWindow and self.GetFrame(): + self.GetFrame().Destroy() + return True + + + def ProcessEvent(self, event): + id = event.GetId() + if id == wx.ID_UNDO: + self.GetCtrl().Undo() + return True + elif id == wx.ID_REDO: + self.GetCtrl().Redo() + return True + elif id == wx.ID_CUT: + self.GetCtrl().Cut() + return True + elif id == wx.ID_COPY: + self.GetCtrl().Copy() + return True + elif id == wx.ID_PASTE: + self.GetCtrl().OnPaste() + return True + elif id == wx.ID_CLEAR: + self.GetCtrl().OnClear() + return True + elif id == wx.ID_SELECTALL: + self.GetCtrl().SetSelection(0, -1) + return True + elif id == VIEW_WHITESPACE_ID: + self.GetCtrl().SetViewWhiteSpace(not self.GetCtrl().GetViewWhiteSpace()) + return True + elif id == VIEW_EOL_ID: + self.GetCtrl().SetViewEOL(not self.GetCtrl().GetViewEOL()) + return True + elif id == VIEW_INDENTATION_GUIDES_ID: + self.GetCtrl().SetViewIndentationGuides(not self.GetCtrl().GetViewIndentationGuides()) + return True + elif id == VIEW_RIGHT_EDGE_ID: + self.GetCtrl().SetViewRightEdge(not self.GetCtrl().GetViewRightEdge()) + return True + elif id == VIEW_LINE_NUMBERS_ID: + self.GetCtrl().SetViewLineNumbers(not self.GetCtrl().GetViewLineNumbers()) + return True + elif id == ZOOM_NORMAL_ID: + self.GetCtrl().SetZoom(0) + return True + elif id == ZOOM_IN_ID: + self.GetCtrl().CmdKeyExecute(wx.stc.STC_CMD_ZOOMIN) + return True + elif id == ZOOM_OUT_ID: + self.GetCtrl().CmdKeyExecute(wx.stc.STC_CMD_ZOOMOUT) + return True + elif id == CHOOSE_FONT_ID: + self.OnChooseFont() + return True + elif id == WORD_WRAP_ID: + self.GetCtrl().SetWordWrap(not self.GetCtrl().GetWordWrap()) + return True + elif id == FindService.FindService.FIND_ID: + self.OnFind() + return True + elif id == FindService.FindService.FIND_PREVIOUS_ID: + self.DoFind(forceFindPrevious = True) + return True + elif id == FindService.FindService.FIND_NEXT_ID: + self.DoFind(forceFindNext = True) + return True + elif id == FindService.FindService.REPLACE_ID: + self.OnFind(replace = True) + return True + elif id == FindService.FindService.FINDONE_ID: + self.DoFind() + return True + elif id == FindService.FindService.REPLACEONE_ID: + self.DoFind(replace = True) + return True + elif id == FindService.FindService.REPLACEALL_ID: + self.DoFind(replaceAll = True) + return True + elif id == FindService.FindService.GOTO_LINE_ID: + self.OnGotoLine(event) + return True + else: + return wx.lib.docview.View.ProcessEvent(self, event) + + + def ProcessUpdateUIEvent(self, event): + if not self.GetCtrl(): + return False + + hasSelection = self.GetCtrl().GetSelectionStart() != self.GetCtrl().GetSelectionEnd() + hasText = self.GetCtrl().GetTextLength() > 0 + notOnLastChar = self.GetCtrl().GetSelectionStart() != self.GetCtrl().GetTextLength() + + id = event.GetId() + if id == wx.ID_UNDO: + event.Enable(self.GetCtrl().CanUndo()) + event.SetText(_("Undo") + '\t' + _('Ctrl+Z')) + return True + elif id == wx.ID_REDO: + event.Enable(self.GetCtrl().CanRedo()) + event.SetText(_("Redo") + '\t' + _('Ctrl+Y')) + return True + elif id == wx.ID_CUT: + event.Enable(hasSelection) + return True + elif id == wx.ID_COPY: + event.Enable(hasSelection) + return True + elif id == wx.ID_PASTE: + event.Enable(self.GetCtrl().CanPaste()) + return True + elif id == wx.ID_CLEAR: + event.Enable(hasSelection) + return True + elif id == wx.ID_SELECTALL: + event.Enable(hasText) + return True + elif id == TEXT_ID: + event.Enable(True) + return True + elif id == VIEW_WHITESPACE_ID: + event.Enable(hasText) + event.Check(self.GetCtrl().GetViewWhiteSpace()) + return True + elif id == VIEW_EOL_ID: + event.Enable(hasText) + event.Check(self.GetCtrl().GetViewEOL()) + return True + elif id == VIEW_INDENTATION_GUIDES_ID: + event.Enable(hasText) + event.Check(self.GetCtrl().GetIndentationGuides()) + return True + elif id == VIEW_RIGHT_EDGE_ID: + event.Enable(hasText) + event.Check(self.GetCtrl().GetViewRightEdge()) + return True + elif id == VIEW_LINE_NUMBERS_ID: + event.Enable(hasText) + event.Check(self.GetCtrl().GetViewLineNumbers()) + return True + elif id == ZOOM_ID: + event.Enable(True) + return True + elif id == ZOOM_NORMAL_ID: + event.Enable(self.GetCtrl().GetZoom() != 0) + return True + elif id == ZOOM_IN_ID: + event.Enable(self.GetCtrl().GetZoom() < 20) + return True + elif id == ZOOM_OUT_ID: + event.Enable(self.GetCtrl().GetZoom() > -10) + return True + elif id == CHOOSE_FONT_ID: + event.Enable(True) + return True + elif id == WORD_WRAP_ID: + event.Enable(self.GetCtrl().CanWordWrap()) + event.Check(self.GetCtrl().CanWordWrap() and self.GetCtrl().GetWordWrap()) + return True + elif id == FindService.FindService.FIND_ID: + event.Enable(hasText) + return True + elif id == FindService.FindService.FIND_PREVIOUS_ID: + event.Enable(hasText and + self._FindServiceHasString() and + self.GetCtrl().GetSelection()[0] > 0) + return True + elif id == FindService.FindService.FIND_NEXT_ID: + event.Enable(hasText and + self._FindServiceHasString() and + self.GetCtrl().GetSelection()[0] < self.GetCtrl().GetLength()) + return True + elif id == FindService.FindService.REPLACE_ID: + event.Enable(hasText) + return True + elif id == FindService.FindService.GOTO_LINE_ID: + event.Enable(True) + return True + elif id == TEXT_STATUS_BAR_ID: + self.OnUpdateStatusBar(event) + return True + else: + return wx.lib.docview.View.ProcessUpdateUIEvent(self, event) + + + def _GetParentFrame(self): + return wx.GetTopLevelParent(self.GetFrame()) + + + #---------------------------------------------------------------------------- + # Methods for TextDocument to call + #---------------------------------------------------------------------------- + + def IsModified(self): + if not self.GetCtrl(): + return False + return self.GetCtrl().GetModify() + + + def SetModifyFalse(self): + self.GetCtrl().SetSavePoint() + + + def GetValue(self): + if self.GetCtrl(): + return self.GetCtrl().GetText() + else: + return None + + + def SetValue(self, value): + self.GetCtrl().SetText(value) + self.GetCtrl().UpdateLineNumberMarginWidth() + self.GetCtrl().EmptyUndoBuffer() + + + #---------------------------------------------------------------------------- + # STC events + #---------------------------------------------------------------------------- + + def OnUpdateStatusBar(self, event): + statusBar = self._GetParentFrame().GetStatusBar() + statusBar.SetInsertMode(self.GetCtrl().GetOvertype() == 0) + statusBar.SetLineNumber(self.GetCtrl().GetCurrentLine() + 1) + statusBar.SetColumnNumber(self.GetCtrl().GetColumn(self.GetCtrl().GetCurrentPos()) + 1) + + + #---------------------------------------------------------------------------- + # Format methods + #---------------------------------------------------------------------------- + + def OnChooseFont(self): + data = wx.FontData() + data.EnableEffects(True) + data.SetInitialFont(self.GetCtrl().GetFont()) + data.SetColour(self.GetCtrl().GetFontColor()) + fontDialog = wx.FontDialog(self.GetFrame(), data) + if fontDialog.ShowModal() == wx.ID_OK: + data = fontDialog.GetFontData() + self.GetCtrl().SetFont(data.GetChosenFont()) + self.GetCtrl().SetFontColor(data.GetColour()) + self.GetCtrl().UpdateStyles() + fontDialog.Destroy() + + + #---------------------------------------------------------------------------- + # Find methods + #---------------------------------------------------------------------------- + + def OnFind(self, replace = False): + findService = wx.GetApp().GetService(FindService.FindService) + if findService: + findService.ShowFindReplaceDialog(findString = self.GetCtrl().GetSelectedText(), replace = replace) + + + def DoFind(self, forceFindNext = False, forceFindPrevious = False, replace = False, replaceAll = False): + findService = wx.GetApp().GetService(FindService.FindService) + if not findService: + return + findString = findService.GetFindString() + if len(findString) == 0: + return -1 + replaceString = findService.GetReplaceString() + flags = findService.GetFlags() + startLoc, endLoc = self.GetCtrl().GetSelection() + + wholeWord = flags & wx.FR_WHOLEWORD > 0 + matchCase = flags & wx.FR_MATCHCASE > 0 + regExp = flags & FindService.FindService.FR_REGEXP > 0 + down = flags & wx.FR_DOWN > 0 + wrap = flags & FindService.FindService.FR_WRAP > 0 + + if forceFindPrevious: # this is from function keys, not dialog box + down = False + wrap = False # user would want to know they're at the end of file + elif forceFindNext: + down = True + wrap = False # user would want to know they're at the end of file + + badSyntax = False + + # On replace dialog operations, user is allowed to replace the currently highlighted text to determine if it should be replaced or not. + # Typically, it is the text from a previous find operation, but we must check to see if it isn't, user may have moved the cursor or selected some other text accidentally. + # If the text is a match, then replace it. + if replace: + result, start, end, replText = findService.DoFind(findString, replaceString, self.GetCtrl().GetSelectedText(), 0, 0, True, matchCase, wholeWord, regExp, replace) + if result > 0: + self.GetCtrl().ReplaceSelection(replText) + self.GetDocument().Modify(True) + wx.GetApp().GetTopWindow().PushStatusText(_("1 occurrence of \"%s\" replaced") % findString) + if down: + startLoc += len(replText) # advance start location past replacement string to new text + endLoc = startLoc + elif result == FindService.FIND_SYNTAXERROR: + badSyntax = True + wx.GetApp().GetTopWindow().PushStatusText(_("Invalid regular expression \"%s\"") % findString) + + if not badSyntax: + text = self.GetCtrl().GetText() + + # Find the next matching text occurance or if it is a ReplaceAll, replace all occurances + # Even if the user is Replacing, we should replace here, but only select the text and let the user replace it with the next Replace operation + result, start, end, text = findService.DoFind(findString, replaceString, text, startLoc, endLoc, down, matchCase, wholeWord, regExp, False, replaceAll, wrap) + if result > 0: + self.GetCtrl().SetTargetStart(0) + self.GetCtrl().SetTargetEnd(self.GetCtrl().GetLength()) + self.GetCtrl().ReplaceTarget(text) # Doing a SetText causes a clear document to be shown when undoing, so using replacetarget instead + self.GetDocument().Modify(True) + if result == 1: + wx.GetApp().GetTopWindow().PushStatusText(_("1 occurrence of \"%s\" replaced") % findString) + else: + wx.GetApp().GetTopWindow().PushStatusText(_("%i occurrences of \"%s\" replaced") % (result, findString)) + elif result == 0: + self.GetCtrl().SetSelection(start, end) + self.GetCtrl().EnsureVisible(self.GetCtrl().LineFromPosition(end)) # show bottom then scroll up to top + self.GetCtrl().EnsureVisible(self.GetCtrl().LineFromPosition(start)) # do this after ensuring bottom is visible + wx.GetApp().GetTopWindow().PushStatusText(_("Found \"%s\".") % findString) + elif result == FindService.FIND_SYNTAXERROR: + # Dialog for this case gets popped up by the FindService. + wx.GetApp().GetTopWindow().PushStatusText(_("Invalid regular expression \"%s\"") % findString) + else: + wx.MessageBox(_("Can't find \"%s\".") % findString, "Find", + wx.OK | wx.ICON_INFORMATION) + + + def _FindServiceHasString(self): + findService = wx.GetApp().GetService(FindService.FindService) + if not findService or not findService.GetFindString(): + return False + return True + + + def OnGotoLine(self, event): + findService = wx.GetApp().GetService(FindService.FindService) + if findService: + line = findService.GetLineNumber(self.GetDocumentManager().FindSuitableParent()) + if line > -1: + line = line - 1 + self.GetCtrl().EnsureVisible(line) + self.GetCtrl().GotoLine(line) + + + def GotoLine(self, lineNum): + if lineNum > -1: + lineNum = lineNum - 1 # line numbering for editor is 0 based, we are 1 based. + self.GetCtrl().EnsureVisibleEnforcePolicy(lineNum) + self.GetCtrl().GotoLine(lineNum) + + + def SetSelection(self, start, end): + self.GetCtrl().SetSelection(start, end) + + + def EnsureVisible(self, line): + self.GetCtrl().EnsureVisible(line-1) # line numbering for editor is 0 based, we are 1 based. + + def EnsureVisibleEnforcePolicy(self, line): + self.GetCtrl().EnsureVisibleEnforcePolicy(line-1) # line numbering for editor is 0 based, we are 1 based. + + def LineFromPosition(self, pos): + return self.GetCtrl().LineFromPosition(pos)+1 # line numbering for editor is 0 based, we are 1 based. + + + def PositionFromLine(self, line): + return self.GetCtrl().PositionFromLine(line-1) # line numbering for editor is 0 based, we are 1 based. + + + def GetLineEndPosition(self, line): + return self.GetCtrl().GetLineEndPosition(line-1) # line numbering for editor is 0 based, we are 1 based. + + + def GetLine(self, lineNum): + return self.GetCtrl().GetLine(lineNum-1) # line numbering for editor is 0 based, we are 1 based. + + def MarkerDefine(self): + """ This must be called after the texteditor is instantiated """ + self.GetCtrl().MarkerDefine(TextView.MARKER_NUM, wx.stc.STC_MARK_CIRCLE, wx.BLACK, wx.BLUE) + + + def MarkerToggle(self, lineNum = -1, marker_index=MARKER_NUM, mask=MARKER_MASK): + if lineNum == -1: + lineNum = self.GetCtrl().GetCurrentLine() + if self.GetCtrl().MarkerGet(lineNum) & mask: + self.GetCtrl().MarkerDelete(lineNum, marker_index) + self._markerCount -= 1 + else: + self.GetCtrl().MarkerAdd(lineNum, marker_index) + self._markerCount += 1 + + def MarkerAdd(self, lineNum = -1, marker_index=MARKER_NUM, mask=MARKER_MASK): + if lineNum == -1: + lineNum = self.GetCtrl().GetCurrentLine() + self.GetCtrl().MarkerAdd(lineNum, marker_index) + self._markerCount += 1 + + + def MarkerDelete(self, lineNum = -1, marker_index=MARKER_NUM, mask=MARKER_MASK): + if lineNum == -1: + lineNum = self.GetCtrl().GetCurrentLine() + if self.GetCtrl().MarkerGet(lineNum) & mask: + self.GetCtrl().MarkerDelete(lineNum, marker_index) + self._markerCount -= 1 + + def MarkerDeleteAll(self, marker_num=MARKER_NUM): + self.GetCtrl().MarkerDeleteAll(marker_num) + if marker_num == self.MARKER_NUM: + self._markerCount = 0 + + + def MarkerNext(self, lineNum = -1): + if lineNum == -1: + lineNum = self.GetCtrl().GetCurrentLine() + 1 # start search below current line + foundLine = self.GetCtrl().MarkerNext(lineNum, self.MARKER_MASK) + if foundLine == -1: + # wrap to top of file + foundLine = self.GetCtrl().MarkerNext(0, self.MARKER_MASK) + if foundLine == -1: + wx.GetApp().GetTopWindow().PushStatusText(_("No markers")) + return + + self.GotoLine(foundLine + 1) + + + def MarkerPrevious(self, lineNum = -1): + if lineNum == -1: + lineNum = self.GetCtrl().GetCurrentLine() - 1 # start search above current line + if lineNum == -1: + lineNum = self.GetCtrl().GetLineCount() + + foundLine = self.GetCtrl().MarkerPrevious(lineNum, self.MARKER_MASK) + if foundLine == -1: + # wrap to bottom of file + foundLine = self.GetCtrl().MarkerPrevious(self.GetCtrl().GetLineCount(), self.MARKER_MASK) + if foundLine == -1: + wx.GetApp().GetTopWindow().PushStatusText(_("No markers")) + return + + self.GotoLine(foundLine + 1) + + + def MarkerExists(self, lineNum = -1, mask=MARKER_MASK): + if lineNum == -1: + lineNum = self.GetCtrl().GetCurrentLine() + if self.GetCtrl().MarkerGet(lineNum) & mask: + return True + else: + return False + + + def GetMarkerCount(self): + return self._markerCount + + +class TextService(wx.lib.pydocview.DocService): + + + def __init__(self): + wx.lib.pydocview.DocService.__init__(self) + + + def InstallControls(self, frame, menuBar = None, toolBar = None, statusBar = None, document = None): + if document and document.GetDocumentTemplate().GetDocumentType() != TextDocument: + return + if not document and wx.GetApp().GetDocumentManager().GetFlags() & wx.lib.docview.DOC_SDI: + return + + statusBar = TextStatusBar(frame, TEXT_STATUS_BAR_ID) + frame.SetStatusBar(statusBar) + wx.EVT_UPDATE_UI(frame, TEXT_STATUS_BAR_ID, frame.ProcessUpdateUIEvent) + + viewMenu = menuBar.GetMenu(menuBar.FindMenu(_("&View"))) + + viewMenu.AppendSeparator() + textMenu = wx.Menu() + textMenu.AppendCheckItem(VIEW_WHITESPACE_ID, _("&Whitespace"), _("Shows or hides whitespace")) + wx.EVT_MENU(frame, VIEW_WHITESPACE_ID, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, VIEW_WHITESPACE_ID, frame.ProcessUpdateUIEvent) + textMenu.AppendCheckItem(VIEW_EOL_ID, _("&End of Line Markers"), _("Shows or hides indicators at the end of each line")) + wx.EVT_MENU(frame, VIEW_EOL_ID, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, VIEW_EOL_ID, frame.ProcessUpdateUIEvent) + textMenu.AppendCheckItem(VIEW_INDENTATION_GUIDES_ID, _("&Indentation Guides"), _("Shows or hides indentations")) + wx.EVT_MENU(frame, VIEW_INDENTATION_GUIDES_ID, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, VIEW_INDENTATION_GUIDES_ID, frame.ProcessUpdateUIEvent) + textMenu.AppendCheckItem(VIEW_RIGHT_EDGE_ID, _("&Right Edge"), _("Shows or hides the right edge marker")) + wx.EVT_MENU(frame, VIEW_RIGHT_EDGE_ID, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, VIEW_RIGHT_EDGE_ID, frame.ProcessUpdateUIEvent) + textMenu.AppendCheckItem(VIEW_LINE_NUMBERS_ID, _("&Line Numbers"), _("Shows or hides the line numbers")) + wx.EVT_MENU(frame, VIEW_LINE_NUMBERS_ID, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, VIEW_LINE_NUMBERS_ID, frame.ProcessUpdateUIEvent) + + viewMenu.AppendMenu(TEXT_ID, _("&Text"), textMenu) + wx.EVT_UPDATE_UI(frame, TEXT_ID, frame.ProcessUpdateUIEvent) + + isWindows = (wx.Platform == '__WXMSW__') + + zoomMenu = wx.Menu() + zoomMenu.Append(ZOOM_NORMAL_ID, _("Normal Size"), _("Sets the document to its normal size")) + wx.EVT_MENU(frame, ZOOM_NORMAL_ID, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, ZOOM_NORMAL_ID, frame.ProcessUpdateUIEvent) + if isWindows: + zoomMenu.Append(ZOOM_IN_ID, _("Zoom In\tCtrl+Page Up"), _("Zooms the document to a larger size")) + else: + zoomMenu.Append(ZOOM_IN_ID, _("Zoom In"), _("Zooms the document to a larger size")) + wx.EVT_MENU(frame, ZOOM_IN_ID, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, ZOOM_IN_ID, frame.ProcessUpdateUIEvent) + if isWindows: + zoomMenu.Append(ZOOM_OUT_ID, _("Zoom Out\tCtrl+Page Down"), _("Zooms the document to a smaller size")) + else: + zoomMenu.Append(ZOOM_OUT_ID, _("Zoom Out"), _("Zooms the document to a smaller size")) + wx.EVT_MENU(frame, ZOOM_OUT_ID, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, ZOOM_OUT_ID, frame.ProcessUpdateUIEvent) + + viewMenu.AppendMenu(ZOOM_ID, _("&Zoom"), zoomMenu) + wx.EVT_UPDATE_UI(frame, ZOOM_ID, frame.ProcessUpdateUIEvent) + + formatMenuIndex = menuBar.FindMenu(_("&Format")) + if formatMenuIndex > -1: + formatMenu = menuBar.GetMenu(formatMenuIndex) + else: + formatMenu = wx.Menu() + if not menuBar.FindItemById(CHOOSE_FONT_ID): + formatMenu.Append(CHOOSE_FONT_ID, _("&Font..."), _("Sets the font to use")) + wx.EVT_MENU(frame, CHOOSE_FONT_ID, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, CHOOSE_FONT_ID, frame.ProcessUpdateUIEvent) + if not menuBar.FindItemById(WORD_WRAP_ID): + formatMenu.AppendCheckItem(WORD_WRAP_ID, _("Word Wrap"), _("Wraps text horizontally when checked")) + wx.EVT_MENU(frame, WORD_WRAP_ID, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, WORD_WRAP_ID, frame.ProcessUpdateUIEvent) + if formatMenuIndex == -1: + viewMenuIndex = menuBar.FindMenu(_("&View")) + menuBar.Insert(viewMenuIndex + 1, formatMenu, _("&Format")) + + # wxBug: wxToolBar::GetToolPos doesn't exist, need it to find cut tool and then insert find in front of it. + toolBar.AddSeparator() + toolBar.AddTool(ZOOM_IN_ID, getZoomInBitmap(), shortHelpString = _("Zoom In"), longHelpString = _("Zooms the document to a larger size")) + toolBar.AddTool(ZOOM_OUT_ID, getZoomOutBitmap(), shortHelpString = _("Zoom Out"), longHelpString = _("Zooms the document to a smaller size")) + toolBar.Realize() + + + def ProcessUpdateUIEvent(self, event): + id = event.GetId() + if id == TEXT_ID: + event.Enable(False) + return True + elif id == VIEW_WHITESPACE_ID: + event.Enable(False) + return True + elif id == VIEW_EOL_ID: + event.Enable(False) + return True + elif id == VIEW_INDENTATION_GUIDES_ID: + event.Enable(False) + return True + elif id == VIEW_RIGHT_EDGE_ID: + event.Enable(False) + return True + elif id == VIEW_LINE_NUMBERS_ID: + event.Enable(False) + return True + elif id == ZOOM_ID: + event.Enable(False) + return True + elif id == ZOOM_NORMAL_ID: + event.Enable(False) + return True + elif id == ZOOM_IN_ID: + event.Enable(False) + return True + elif id == ZOOM_OUT_ID: + event.Enable(False) + return True + elif id == CHOOSE_FONT_ID: + event.Enable(False) + return True + elif id == WORD_WRAP_ID: + event.Enable(False) + return True + else: + return False + + +class TextStatusBar(wx.StatusBar): + + # wxBug: Would be nice to show num key status in statusbar, but can't figure out how to detect if it is enabled or disabled + + def __init__(self, parent, id, style = wx.ST_SIZEGRIP, name = "statusBar"): + wx.StatusBar.__init__(self, parent, id, style, name) + self.SetFieldsCount(4) + self.SetStatusWidths([-1, 50, 50, 55]) + + def SetInsertMode(self, insert = True): + if insert: + newText = _("Ins") + else: + newText = _("") + if self.GetStatusText(1) != newText: # wxBug: Need to check if the text has changed, otherwise it flickers under win32 + self.SetStatusText(newText, 1) + + def SetLineNumber(self, lineNumber): + newText = _("Ln %i") % lineNumber + if self.GetStatusText(2) != newText: + self.SetStatusText(newText, 2) + + def SetColumnNumber(self, colNumber): + newText = _("Col %i") % colNumber + if self.GetStatusText(3) != newText: + self.SetStatusText(newText, 3) + + +class TextOptionsPanel(wx.Panel): + + + def __init__(self, parent, id, configPrefix = "Text", label = "Text", hasWordWrap = True, hasTabs = False, addPage=True): + wx.Panel.__init__(self, parent, id) + self._configPrefix = configPrefix + self._hasWordWrap = hasWordWrap + self._hasTabs = hasTabs + SPACE = 10 + HALF_SPACE = 5 + config = wx.ConfigBase_Get() + self._textFont = wx.Font(10, wx.MODERN, wx.NORMAL, wx.NORMAL) + fontData = config.Read(self._configPrefix + "EditorFont", "") + if fontData: + nativeFont = wx.NativeFontInfo() + nativeFont.FromString(fontData) + self._textFont.SetNativeFontInfo(nativeFont) + self._originalTextFont = self._textFont + self._textColor = wx.BLACK + colorData = config.Read(self._configPrefix + "EditorColor", "") + if colorData: + red = int("0x" + colorData[0:2], 16) + green = int("0x" + colorData[2:4], 16) + blue = int("0x" + colorData[4:6], 16) + self._textColor = wx.Color(red, green, blue) + self._originalTextColor = self._textColor + fontLabel = wx.StaticText(self, -1, _("Font:")) + self._sampleTextCtrl = wx.TextCtrl(self, -1, "", size = (125, 21)) + self._sampleTextCtrl.SetEditable(False) + chooseFontButton = wx.Button(self, -1, _("Choose Font...")) + wx.EVT_BUTTON(self, chooseFontButton.GetId(), self.OnChooseFont) + if self._hasWordWrap: + self._wordWrapCheckBox = wx.CheckBox(self, -1, _("Wrap words inside text area")) + self._wordWrapCheckBox.SetValue(wx.ConfigBase_Get().ReadInt(self._configPrefix + "EditorWordWrap", False)) + self._viewWhitespaceCheckBox = wx.CheckBox(self, -1, _("Show whitespace")) + self._viewWhitespaceCheckBox.SetValue(config.ReadInt(self._configPrefix + "EditorViewWhitespace", False)) + self._viewEOLCheckBox = wx.CheckBox(self, -1, _("Show end of line markers")) + self._viewEOLCheckBox.SetValue(config.ReadInt(self._configPrefix + "EditorViewEOL", False)) + self._viewIndentationGuideCheckBox = wx.CheckBox(self, -1, _("Show indentation guides")) + self._viewIndentationGuideCheckBox.SetValue(config.ReadInt(self._configPrefix + "EditorViewIndentationGuides", False)) + self._viewRightEdgeCheckBox = wx.CheckBox(self, -1, _("Show right edge")) + self._viewRightEdgeCheckBox.SetValue(config.ReadInt(self._configPrefix + "EditorViewRightEdge", False)) + self._viewLineNumbersCheckBox = wx.CheckBox(self, -1, _("Show line numbers")) + self._viewLineNumbersCheckBox.SetValue(config.ReadInt(self._configPrefix + "EditorViewLineNumbers", True)) + if self._hasTabs: + self._hasTabsCheckBox = wx.CheckBox(self, -1, _("Use spaces instead of tabs")) + self._hasTabsCheckBox.SetValue(not wx.ConfigBase_Get().ReadInt(self._configPrefix + "EditorUseTabs", False)) + indentWidthLabel = wx.StaticText(self, -1, _("Indent Width:")) + self._indentWidthChoice = wx.Choice(self, -1, choices = ["2", "4", "6", "8", "10"]) + self._indentWidthChoice.SetStringSelection(str(config.ReadInt(self._configPrefix + "EditorIndentWidth", 4))) + textPanelBorderSizer = wx.BoxSizer(wx.VERTICAL) + textPanelSizer = wx.BoxSizer(wx.VERTICAL) + textFontSizer = wx.BoxSizer(wx.HORIZONTAL) + textFontSizer.Add(fontLabel, 0, wx.ALIGN_LEFT | wx.RIGHT | wx.TOP, HALF_SPACE) + textFontSizer.Add(self._sampleTextCtrl, 0, wx.ALIGN_LEFT | wx.EXPAND | wx.RIGHT, HALF_SPACE) + textFontSizer.Add(chooseFontButton, 0, wx.ALIGN_RIGHT | wx.LEFT, HALF_SPACE) + textPanelSizer.Add(textFontSizer, 0, wx.ALL, HALF_SPACE) + if self._hasWordWrap: + textPanelSizer.Add(self._wordWrapCheckBox, 0, wx.ALL, HALF_SPACE) + textPanelSizer.Add(self._viewWhitespaceCheckBox, 0, wx.ALL, HALF_SPACE) + textPanelSizer.Add(self._viewEOLCheckBox, 0, wx.ALL, HALF_SPACE) + textPanelSizer.Add(self._viewIndentationGuideCheckBox, 0, wx.ALL, HALF_SPACE) + textPanelSizer.Add(self._viewRightEdgeCheckBox, 0, wx.ALL, HALF_SPACE) + textPanelSizer.Add(self._viewLineNumbersCheckBox, 0, wx.ALL, HALF_SPACE) + if self._hasTabs: + textPanelSizer.Add(self._hasTabsCheckBox, 0, wx.ALL, HALF_SPACE) + textIndentWidthSizer = wx.BoxSizer(wx.HORIZONTAL) + textIndentWidthSizer.Add(indentWidthLabel, 0, wx.ALIGN_LEFT | wx.RIGHT | wx.TOP, HALF_SPACE) + textIndentWidthSizer.Add(self._indentWidthChoice, 0, wx.ALIGN_LEFT | wx.EXPAND, HALF_SPACE) + textPanelSizer.Add(textIndentWidthSizer, 0, wx.ALL, HALF_SPACE) + textPanelBorderSizer.Add(textPanelSizer, 0, wx.ALL, SPACE) +## styleButton = wx.Button(self, -1, _("Choose Style...")) +## wx.EVT_BUTTON(self, styleButton.GetId(), self.OnChooseStyle) +## textPanelBorderSizer.Add(styleButton, 0, wx.ALL, SPACE) + self.SetSizer(textPanelBorderSizer) + self.UpdateSampleFont() + if addPage: + parent.AddPage(self, _(label)) + + def UpdateSampleFont(self): + nativeFont = wx.NativeFontInfo() + nativeFont.FromString(self._textFont.GetNativeFontInfoDesc()) + font = wx.NullFont + font.SetNativeFontInfo(nativeFont) + font.SetPointSize(self._sampleTextCtrl.GetFont().GetPointSize()) # Use the standard point size + self._sampleTextCtrl.SetFont(font) + self._sampleTextCtrl.SetForegroundColour(self._textColor) + self._sampleTextCtrl.SetValue(str(self._textFont.GetPointSize()) + _(" pt. ") + self._textFont.GetFaceName()) + self._sampleTextCtrl.Refresh() + self.Layout() + + +## def OnChooseStyle(self, event): +## import STCStyleEditor +## import os +## base = os.path.split(__file__)[0] +## config = os.path.abspath(os.path.join(base, 'stc-styles.rc.cfg')) +## +## dlg = STCStyleEditor.STCStyleEditDlg(None, +## 'Python', 'python', +## #'HTML', 'html', +## #'XML', 'xml', +## config) +## try: +## dlg.ShowModal() +## finally: +## dlg.Destroy() + + + def OnChooseFont(self, event): + data = wx.FontData() + data.EnableEffects(True) + data.SetInitialFont(self._textFont) + data.SetColour(self._textColor) + fontDialog = wx.FontDialog(self, data) + if fontDialog.ShowModal() == wx.ID_OK: + data = fontDialog.GetFontData() + self._textFont = data.GetChosenFont() + self._textColor = data.GetColour() + self.UpdateSampleFont() + fontDialog.Destroy() + + + def OnOK(self, optionsDialog): + config = wx.ConfigBase_Get() + doViewStuffUpdate = config.ReadInt(self._configPrefix + "EditorViewWhitespace", False) != self._viewWhitespaceCheckBox.GetValue() + config.WriteInt(self._configPrefix + "EditorViewWhitespace", self._viewWhitespaceCheckBox.GetValue()) + doViewStuffUpdate = doViewStuffUpdate or config.ReadInt(self._configPrefix + "EditorViewEOL", False) != self._viewEOLCheckBox.GetValue() + config.WriteInt(self._configPrefix + "EditorViewEOL", self._viewEOLCheckBox.GetValue()) + doViewStuffUpdate = doViewStuffUpdate or config.ReadInt(self._configPrefix + "EditorViewIndentationGuides", False) != self._viewIndentationGuideCheckBox.GetValue() + config.WriteInt(self._configPrefix + "EditorViewIndentationGuides", self._viewIndentationGuideCheckBox.GetValue()) + doViewStuffUpdate = doViewStuffUpdate or config.ReadInt(self._configPrefix + "EditorViewRightEdge", False) != self._viewRightEdgeCheckBox.GetValue() + config.WriteInt(self._configPrefix + "EditorViewRightEdge", self._viewRightEdgeCheckBox.GetValue()) + doViewStuffUpdate = doViewStuffUpdate or config.ReadInt(self._configPrefix + "EditorViewLineNumbers", True) != self._viewLineNumbersCheckBox.GetValue() + config.WriteInt(self._configPrefix + "EditorViewLineNumbers", self._viewLineNumbersCheckBox.GetValue()) + if self._hasWordWrap: + doViewStuffUpdate = doViewStuffUpdate or config.ReadInt(self._configPrefix + "EditorWordWrap", False) != self._wordWrapCheckBox.GetValue() + config.WriteInt(self._configPrefix + "EditorWordWrap", self._wordWrapCheckBox.GetValue()) + if self._hasTabs: + doViewStuffUpdate = doViewStuffUpdate or not config.ReadInt(self._configPrefix + "EditorUseTabs", True) != self._hasTabsCheckBox.GetValue() + config.WriteInt(self._configPrefix + "EditorUseTabs", not self._hasTabsCheckBox.GetValue()) + newIndentWidth = int(self._indentWidthChoice.GetStringSelection()) + oldIndentWidth = config.ReadInt(self._configPrefix + "EditorIndentWidth", 4) + if newIndentWidth != oldIndentWidth: + doViewStuffUpdate = True + config.WriteInt(self._configPrefix + "EditorIndentWidth", newIndentWidth) + doFontUpdate = self._originalTextFont != self._textFont or self._originalTextColor != self._textColor + config.Write(self._configPrefix + "EditorFont", self._textFont.GetNativeFontInfoDesc()) + config.Write(self._configPrefix + "EditorColor", "%02x%02x%02x" % (self._textColor.Red(), self._textColor.Green(), self._textColor.Blue())) + if doViewStuffUpdate or doFontUpdate: + for document in optionsDialog.GetDocManager().GetDocuments(): + if issubclass(document.GetDocumentTemplate().GetDocumentType(), TextDocument): + if doViewStuffUpdate: + document.UpdateAllViews(hint = "ViewStuff") + if doFontUpdate: + document.UpdateAllViews(hint = "Font") + + +class TextCtrl(wx.stc.StyledTextCtrl): + + def __init__(self, parent, ID = -1, style = wx.NO_FULL_REPAINT_ON_RESIZE): + if ID == -1: + ID = wx.NewId() + wx.stc.StyledTextCtrl.__init__(self, parent, ID, style = style) + + self._font = None + self._fontColor = None + + self.SetVisiblePolicy(wx.stc.STC_VISIBLE_STRICT,0) + self.SetYCaretPolicy(0, 0) + + self.CmdKeyClear(wx.stc.STC_KEY_ADD, wx.stc.STC_SCMOD_CTRL) + self.CmdKeyClear(wx.stc.STC_KEY_SUBTRACT, wx.stc.STC_SCMOD_CTRL) + self.CmdKeyAssign(wx.stc.STC_KEY_PRIOR, wx.stc.STC_SCMOD_CTRL, wx.stc.STC_CMD_ZOOMIN) + self.CmdKeyAssign(wx.stc.STC_KEY_NEXT, wx.stc.STC_SCMOD_CTRL, wx.stc.STC_CMD_ZOOMOUT) + self.Bind(wx.stc.EVT_STC_ZOOM, self.OnUpdateLineNumberMarginWidth) # auto update line num width on zoom + wx.EVT_KEY_DOWN(self, self.OnKeyPressed) + self.SetMargins(0,0) + + self.SetUseTabs(0) + self.SetTabWidth(4) + self.SetIndent(4) + + self.SetViewWhiteSpace(False) + self.SetEOLMode(wx.stc.STC_EOL_LF) + self.SetEdgeMode(wx.stc.STC_EDGE_NONE) + self.SetEdgeColumn(78) + + self.SetMarginType(1, wx.stc.STC_MARGIN_NUMBER) + self.SetMarginWidth(1, self.EstimatedLineNumberMarginWidth()) + self.UpdateStyles() + + self.SetCaretForeground("BLACK") + + self.SetViewDefaults() + font, color = self.GetFontAndColorFromConfig() + self.SetFont(font) + self.SetFontColor(color) + self.MarkerDefineDefault() + + # for multisash initialization + if isinstance(parent, wx.lib.multisash.MultiClient): + while parent.GetParent(): + parent = parent.GetParent() + if hasattr(parent, "GetView"): + break + if hasattr(parent, "GetView"): + textEditor = parent.GetView()._textEditor + if textEditor: + doc = textEditor.GetDocPointer() + if doc: + self.SetDocPointer(doc) + + + + def SetViewDefaults(self, configPrefix = "Text", hasWordWrap = True, hasTabs = False): + config = wx.ConfigBase_Get() + self.SetViewWhiteSpace(config.ReadInt(configPrefix + "EditorViewWhitespace", False)) + self.SetViewEOL(config.ReadInt(configPrefix + "EditorViewEOL", False)) + self.SetIndentationGuides(config.ReadInt(configPrefix + "EditorViewIndentationGuides", False)) + self.SetViewRightEdge(config.ReadInt(configPrefix + "EditorViewRightEdge", False)) + self.SetViewLineNumbers(config.ReadInt(configPrefix + "EditorViewLineNumbers", True)) + if hasWordWrap: + self.SetWordWrap(config.ReadInt(configPrefix + "EditorWordWrap", False)) + if hasTabs: # These methods do not exist in STCTextEditor and are meant for subclasses + self.SetUseTabs(config.ReadInt(configPrefix + "EditorUseTabs", False)) + self.SetIndent(config.ReadInt(configPrefix + "EditorIndentWidth", 4)) + self.SetTabWidth(config.ReadInt(configPrefix + "EditorIndentWidth", 4)) + else: + self.SetUseTabs(True) + self.SetIndent(4) + self.SetTabWidth(4) + + + + def GetDefaultFont(self): + """ Subclasses should override this """ + return wx.Font(10, wx.MODERN, wx.NORMAL, wx.NORMAL) + + + def GetDefaultColor(self): + """ Subclasses should override this """ + return wx.BLACK + + + def GetFontAndColorFromConfig(self, configPrefix = "Text"): + font = self.GetDefaultFont() + config = wx.ConfigBase_Get() + fontData = config.Read(configPrefix + "EditorFont", "") + if fontData: + nativeFont = wx.NativeFontInfo() + nativeFont.FromString(fontData) + font.SetNativeFontInfo(nativeFont) + color = self.GetDefaultColor() + colorData = config.Read(configPrefix + "EditorColor", "") + if colorData: + red = int("0x" + colorData[0:2], 16) + green = int("0x" + colorData[2:4], 16) + blue = int("0x" + colorData[4:6], 16) + color = wx.Color(red, green, blue) + return font, color + + + def GetFont(self): + return self._font + + def SetFont(self, font): + self._font = font + self.StyleSetFont(wx.stc.STC_STYLE_DEFAULT, self._font) + + + def GetFontColor(self): + return self._fontColor + + + def SetFontColor(self, fontColor = wx.BLACK): + self._fontColor = fontColor + self.StyleSetForeground(wx.stc.STC_STYLE_DEFAULT, "#%02x%02x%02x" % (self._fontColor.Red(), self._fontColor.Green(), self._fontColor.Blue())) + + + def UpdateStyles(self): + self.StyleClearAll() + return + + + def EstimatedLineNumberMarginWidth(self): + MARGIN = 4 + baseNumbers = "000" + lineNum = self.GetLineCount() + lineNum = lineNum/100 + while lineNum >= 10: + lineNum = lineNum/10 + baseNumbers = baseNumbers + "0" + + return self.TextWidth(wx.stc.STC_STYLE_LINENUMBER, baseNumbers) + MARGIN + + + def OnUpdateLineNumberMarginWidth(self, event): + self.UpdateLineNumberMarginWidth() + + + def UpdateLineNumberMarginWidth(self): + if self.GetViewLineNumbers(): + self.SetMarginWidth(1, self.EstimatedLineNumberMarginWidth()) + + def MarkerDefineDefault(self): + """ This must be called after the textcontrol is instantiated """ + self.MarkerDefine(TextView.MARKER_NUM, wx.stc.STC_MARK_ROUNDRECT, wx.BLACK, wx.BLUE) + + + def OnClear(self): + # Used when Delete key is hit. + sel = self.GetSelection() + + # Delete the selection or if no selection, the character after the caret. + if sel[0] == sel[1]: + self.SetSelection(sel[0], sel[0] + 1) + else: + # remove any folded lines also. + startLine = self.LineFromPosition(sel[0]) + endLine = self.LineFromPosition(sel[1]) + endLineStart = self.PositionFromLine(endLine) + if startLine != endLine and sel[1] - endLineStart == 0: + while not self.GetLineVisible(endLine): + endLine += 1 + self.SetSelectionEnd(self.PositionFromLine(endLine)) + + self.Clear() + + + def OnPaste(self): + # replace any folded lines also. + sel = self.GetSelection() + startLine = self.LineFromPosition(sel[0]) + endLine = self.LineFromPosition(sel[1]) + endLineStart = self.PositionFromLine(endLine) + if startLine != endLine and sel[1] - endLineStart == 0: + while not self.GetLineVisible(endLine): + endLine += 1 + self.SetSelectionEnd(self.PositionFromLine(endLine)) + + self.Paste() + + + def OnKeyPressed(self, event): + key = event.GetKeyCode() + if key == wx.WXK_NUMPAD_ADD: #wxBug: For whatever reason, the key accelerators for numpad add and subtract with modifiers are not working so have to trap them here + if event.ControlDown(): + self.ToggleFoldAll(expand = True, topLevelOnly = True) + elif event.ShiftDown(): + self.ToggleFoldAll(expand = True) + else: + self.ToggleFold(self.GetCurrentLine()) + elif key == wx.WXK_NUMPAD_SUBTRACT: + if event.ControlDown(): + self.ToggleFoldAll(expand = False, topLevelOnly = True) + elif event.ShiftDown(): + self.ToggleFoldAll(expand = False) + else: + self.ToggleFold(self.GetCurrentLine()) + else: + event.Skip() + + + #---------------------------------------------------------------------------- + # View Text methods + #---------------------------------------------------------------------------- + + def GetViewRightEdge(self): + return self.GetEdgeMode() != wx.stc.STC_EDGE_NONE + + + def SetViewRightEdge(self, viewRightEdge): + if viewRightEdge: + self.SetEdgeMode(wx.stc.STC_EDGE_LINE) + else: + self.SetEdgeMode(wx.stc.STC_EDGE_NONE) + + + def GetViewLineNumbers(self): + return self.GetMarginWidth(1) > 0 + + + def SetViewLineNumbers(self, viewLineNumbers = True): + if viewLineNumbers: + self.SetMarginWidth(1, self.EstimatedLineNumberMarginWidth()) + else: + self.SetMarginWidth(1, 0) + + + def CanWordWrap(self): + return True + + + def GetWordWrap(self): + return self.GetWrapMode() == wx.stc.STC_WRAP_WORD + + + def SetWordWrap(self, wordWrap): + if wordWrap: + self.SetWrapMode(wx.stc.STC_WRAP_WORD) + else: + self.SetWrapMode(wx.stc.STC_WRAP_NONE) + + +#---------------------------------------------------------------------------- +# Icon Bitmaps - generated by encode_bitmaps.py +#---------------------------------------------------------------------------- +from wx import ImageFromStream, BitmapFromImage +from wx import EmptyIcon +import cStringIO + + +def getTextData(): + return \ +'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x10\x00\x00\x00\x10\x08\x06\ +\x00\x00\x00\x1f\xf3\xffa\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\ +\x00\x00`IDAT8\x8d\xed\x931\x0e\xc00\x08\x03m\x92\xff\xff8q\xa7JU!$\x12\x1d\ +\xeb\t\t8n\x81\xb4\x86J\xfa]h\x0ee\x83\xb4\xc6\x14\x00\x00R\xcc \t\xcd\xa1\ +\x08\xd2\xa3\xe1\x08*\t$\x1d\xc4\x012\x0b\x00\xce\xe4\xc8\xe0\t}\xf7\x8f\rV\ +\xd9\x1a\xec\xe0\xbf\xc1\xd7\x06\xd9\xf5UX\xfdF+m\x03\xb8\x00\xe4\xc74B"x\ +\xf1\xf4\x00\x00\x00\x00IEND\xaeB`\x82' + + +def getTextBitmap(): + return BitmapFromImage(getTextImage()) + +def getTextImage(): + stream = cStringIO.StringIO(getTextData()) + return ImageFromStream(stream) + +def getTextIcon(): + icon = EmptyIcon() + icon.CopyFromBitmap(getTextBitmap()) + return icon + + +#---------------------------------------------------------------------------- +# Menu Bitmaps - generated by encode_bitmaps.py +#---------------------------------------------------------------------------- +#---------------------------------------------------------------------- +def getZoomInData(): + return \ +'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x10\x00\x00\x00\x10\x08\x06\ +\x00\x00\x00\x1f\xf3\xffa\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\ +\x00\x00wIDAT8\x8d\xa5\x93Q\x12\x80 \x08D\xb5\xe9X\xee\xe9\xb7{\xd5Gc\xa9\ +\xacX\xca\x1f\xa0\x8fE0\x92<\xc3\x82\xed*\x08\xa0\xf2I~\x07\x000\x17T,\xdb\ +\xd6;\x08\xa4\x00\xa4GA\xab\xca\x00\xbc*\x1eD\xb4\x90\xa4O\x1e\xe3\x16f\xcc(\ +\xc8\x95F\x95\x8d\x02\xef\xa1n\xa0\xce\xc5v\x91zc\xacU\xbey\x03\xf0.\xa8\xb8\ +\x04\x8c\xac\x04MM\xa1lA\xfe\x85?\x90\xe5=X\x06\\\xebCA\xb3Q\xf34\x14\x00\ +\x00\x00\x00IEND\xaeB`\x82' + +def getZoomInBitmap(): + return BitmapFromImage(getZoomInImage()) + +def getZoomInImage(): + stream = cStringIO.StringIO(getZoomInData()) + return ImageFromStream(stream) + +#---------------------------------------------------------------------- +def getZoomOutData(): + return \ +'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x10\x00\x00\x00\x10\x08\x06\ +\x00\x00\x00\x1f\xf3\xffa\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\ +\x00\x00qIDAT8\x8d\xa5\x92Q\x0e\xc0 \x08C-z\xff\x13O\xd9\xd7\x16"\x05\x8d\ +\xf6O\xa2\x8f"\x05\xa4\x96\x1b5V\xd4\xd1\xd5\x9e!\x15\xdb\x00\x1d]\xe7\x07\ +\xac\xf6Iv.B*fW\x0e\x90u\xc9 d\x84\x87v\x82\xb4\xf5\x08\'r\x0e\xa2N\x91~\x07\ +\xd9G\x95\xe2W\xeb\x00\x19\xc4\xd6\\FX\x12\xa3 \xb1:\x05\xacdAG[\xb0y9r`u\ +\x9d\x83k\xc0\x0b#3@0A\x0c"\x93\x00\x00\x00\x00IEND\xaeB`\x82' + + +def getZoomOutBitmap(): + return BitmapFromImage(getZoomOutImage()) + +def getZoomOutImage(): + stream = cStringIO.StringIO(getZoomOutData()) + return ImageFromStream(stream) + + + diff --git a/wxPython/samples/ide/activegrid/tool/Service.py b/wxPython/samples/ide/activegrid/tool/Service.py new file mode 100644 index 0000000000..a55702f639 --- /dev/null +++ b/wxPython/samples/ide/activegrid/tool/Service.py @@ -0,0 +1,327 @@ +#---------------------------------------------------------------------------- +# Name: Service.py +# Purpose: Basic Reusable Service View for wx.lib.pydocview +# +# Author: Morgan Hua +# +# Created: 11/4/04 +# CVS-ID: $Id$ +# Copyright: (c) 2004-2005 ActiveGrid, Inc. +# License: wxWindows License +#---------------------------------------------------------------------------- + +import wx +import wx.lib.docview +import wx.lib.pydocview +_ = wx.GetTranslation + + +FLOATING_MINIFRAME = -1 + + +class ServiceView(wx.EvtHandler): + """ Basic Service View. + """ + bottomTab = None + + #---------------------------------------------------------------------------- + # Overridden methods + #---------------------------------------------------------------------------- + + def __init__(self, service): + wx.EvtHandler.__init__(self) + self._viewFrame = None + self._service = service + self._control = None + self._embeddedWindow = None + + + def Destroy(self): + wx.EvtHandler.Destroy(self) + + + def GetFrame(self): + return self._viewFrame + + + def SetFrame(self, frame): + self._viewFrame = frame + + + def _CreateControl(self, parent, id): + return None + + + def GetControl(self): + return self._control + + + def SetControl(self, control): + self._control = control + + + def OnCreate(self, doc, flags): + config = wx.ConfigBase_Get() + windowLoc = self._service.GetEmbeddedWindowLocation() + if windowLoc == FLOATING_MINIFRAME: + pos = config.ReadInt(self._service.GetServiceName() + "FrameXLoc", -1), config.ReadInt(self._service.GetServiceName() + "FrameYLoc", -1) + # make sure frame is visible + screenWidth = wx.SystemSettings.GetMetric(wx.SYS_SCREEN_X) + screenHeight = wx.SystemSettings.GetMetric(wx.SYS_SCREEN_Y) + if pos[0] < 0 or pos[0] >= screenWidth or pos[1] < 0 or pos[1] >= screenHeight: + pos = wx.DefaultPosition + + size = wx.Size(config.ReadInt(self._service.GetServiceName() + "FrameXSize", -1), config.ReadInt(self._service.GetServiceName() + "FrameYSize", -1)) + title = _(self._service.GetServiceName()) + if wx.GetApp().GetDocumentManager().GetFlags() & wx.lib.docview.DOC_SDI and wx.GetApp().GetAppName(): + title = title + " - " + wx.GetApp().GetAppName() + frame = wx.MiniFrame(wx.GetApp().GetTopWindow(), -1, title, pos = pos, size = size, style = wx.CLOSE_BOX|wx.CAPTION|wx.SYSTEM_MENU) + wx.EVT_CLOSE(frame, self.OnCloseWindow) + elif wx.GetApp().IsMDI(): + self._embeddedWindow = wx.GetApp().GetTopWindow().GetEmbeddedWindow(windowLoc) + frame = self._embeddedWindow + else: + pos = config.ReadInt(self._service.GetServiceName() + "FrameXLoc", -1), config.ReadInt(self._service.GetServiceName() + "FrameYLoc", -1) + # make sure frame is visible + screenWidth = wx.SystemSettings.GetMetric(wx.SYS_SCREEN_X) + screenHeight = wx.SystemSettings.GetMetric(wx.SYS_SCREEN_Y) + if pos[0] < 0 or pos[0] >= screenWidth or pos[1] < 0 or pos[1] >= screenHeight: + pos = wx.DefaultPosition + + size = wx.Size(config.ReadInt(self._service.GetServiceName() + "FrameXSize", -1), config.ReadInt(self._service.GetServiceName() + "FrameYSize", -1)) + title = _(self._service.GetServiceName()) + if wx.GetApp().GetDocumentManager().GetFlags() & wx.lib.docview.DOC_SDI and wx.GetApp().GetAppName(): + title = title + " - " + wx.GetApp().GetAppName() + frame = wx.GetApp().CreateDocumentFrame(self, doc, flags, pos = pos, size = size) + frame.SetTitle(title) + if config.ReadInt(self._service.GetServiceName() + "FrameMaximized", False): + frame.Maximize(True) + wx.EVT_CLOSE(frame, self.OnCloseWindow) + + self.SetFrame(frame) + sizer = wx.BoxSizer(wx.VERTICAL) + + windowLoc = self._service.GetEmbeddedWindowLocation() + if self._embeddedWindow or windowLoc == FLOATING_MINIFRAME: + if (self._service.GetEmbeddedWindowLocation() == wx.lib.pydocview.EMBEDDED_WINDOW_BOTTOM): + if ServiceView.bottomTab == None: + ServiceView.bottomTab = wx.Notebook(frame, wx.NewId(), (0,0), (100,100), wx.LB_DEFAULT, "Bottom Tab") + sizer.Add(ServiceView.bottomTab, 1, wx.TOP|wx.EXPAND, 4) + def OnFrameResize(event): + ServiceView.bottomTab.SetSize(ServiceView.bottomTab.GetParent().GetSize()) + frame.Bind(wx.EVT_SIZE, OnFrameResize) + # Factor this out. + self._control = self._CreateControl(ServiceView.bottomTab, wx.NewId()) + if self._control != None: + ServiceView.bottomTab.AddPage(self._control, self._service.GetServiceName()) + ServiceView.bottomTab.Layout() + else: + # Factor this out. + self._control = self._CreateControl(frame, wx.NewId()) + sizer.Add(self._control) + else: + # Factor this out. + self._control = self._CreateControl(frame, wx.NewId()) + sizer.Add(self._control, 1, wx.EXPAND, 0) + frame.SetSizer(sizer) + frame.Layout() + + return True + + + def OnCloseWindow(self, event): + frame = self.GetFrame() + config = wx.ConfigBase_Get() + if frame and not self._embeddedWindow: + if not frame.IsMaximized(): + config.WriteInt(self._service.GetServiceName() + "FrameXLoc", frame.GetPositionTuple()[0]) + config.WriteInt(self._service.GetServiceName() + "FrameYLoc", frame.GetPositionTuple()[1]) + config.WriteInt(self._service.GetServiceName() + "FrameXSize", frame.GetSizeTuple()[0]) + config.WriteInt(self._service.GetServiceName() + "FrameYSize", frame.GetSizeTuple()[1]) + config.WriteInt(self._service.GetServiceName() + "FrameMaximized", frame.IsMaximized()) + + if not self._embeddedWindow: + windowLoc = self._service.GetEmbeddedWindowLocation() + if windowLoc == FLOATING_MINIFRAME: + # don't destroy it, just hide it + frame.Hide() + else: + # Call the original OnCloseWindow, could have subclassed SDIDocFrame and MDIDocFrame but this is easier since it will work for both SDI and MDI frames without subclassing both + frame.OnCloseWindow(event) + + + def Activate(self, activate = True): + """ Dummy function for SDI mode """ + pass + + + def Close(self, deleteWindow = True): + """ + Closes the view by calling OnClose. If deleteWindow is true, this + function should delete the window associated with the view. + """ + if deleteWindow: + self.Destroy() + + return True + + + #---------------------------------------------------------------------------- + # Callback Methods + #---------------------------------------------------------------------------- + + def SetCallback(self, callback): + """ Sets in the event table for a doubleclick to invoke the given callback. + Additional calls to this method overwrites the previous entry and only the last set callback will be invoked. + """ + wx.stc.EVT_STC_DOUBLECLICK(self.GetControl(), self.GetControl().GetId(), callback) + + + #---------------------------------------------------------------------------- + # Display Methods + #---------------------------------------------------------------------------- + + def IsShown(self): + if not self.GetFrame(): + return False + return self.GetFrame().IsShown() + + + def Hide(self): + self.Show(False) + + + def Show(self, show = True): + self.GetFrame().Show(show) + if self._embeddedWindow: + mdiParentFrame = wx.GetApp().GetTopWindow() + mdiParentFrame.ShowEmbeddedWindow(self.GetFrame(), show) + + +class Service(wx.lib.pydocview.DocService): + + + #---------------------------------------------------------------------------- + # Constants + #---------------------------------------------------------------------------- + SHOW_WINDOW = wx.NewId() # keep this line for each subclass, need unique ID for each Service + + + def __init__(self, serviceName, embeddedWindowLocation = wx.lib.pydocview.EMBEDDED_WINDOW_LEFT): + self._serviceName = serviceName + self._embeddedWindowLocation = embeddedWindowLocation + self._view = None + + + def GetEmbeddedWindowLocation(self): + return self._embeddedWindowLocation + + + def SetEmbeddedWindowLocation(self, embeddedWindowLocation): + self._embeddedWindowLocation = embeddedWindowLocation + + + def InstallControls(self, frame, menuBar = None, toolBar = None, statusBar = None, document = None): + viewMenu = menuBar.GetMenu(menuBar.FindMenu(_("&View"))) + menuItemPos = self.GetMenuItemPos(viewMenu, viewMenu.FindItem(_("&Status Bar"))) + 1 + + viewMenu.InsertCheckItem(menuItemPos, self.SHOW_WINDOW, self.GetMenuString(), self.GetMenuDescr()) + wx.EVT_MENU(frame, self.SHOW_WINDOW, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, self.SHOW_WINDOW, frame.ProcessUpdateUIEvent) + + return True + + + def GetServiceName(self): + """ String used to save out Service View configuration information """ + return self._serviceName + + + def GetMenuString(self): + """ Need to override this method to provide menu item for showing Service View """ + return _(self.GetServiceName()) + + + def GetMenuDescr(self): + """ Need to override this method to provide menu item for showing Service View """ + return _("Show or hides the %s window") % self.GetMenuString() + + + #---------------------------------------------------------------------------- + # Event Processing Methods + #---------------------------------------------------------------------------- + + def ProcessEvent(self, event): + id = event.GetId() + if id == self.SHOW_WINDOW: + self.ToggleWindow(event) + return True + else: + return False + + + def ProcessUpdateUIEvent(self, event): + id = event.GetId() + if id == self.SHOW_WINDOW: + event.Check(self._view != None and self._view.IsShown()) + event.Enable(True) + return True + else: + return False + + + #---------------------------------------------------------------------------- + # View Methods + #---------------------------------------------------------------------------- + + def _CreateView(self): + """ This method needs to be overridden with corresponding ServiceView """ + return ServiceView(self) + + + def GetView(self): + # Window Menu Service Method + return self._view + + + def SetView(self, view): + self._view = view + + + def ShowWindow(self, show = True): + if show: + if self._view: + if not self._view.IsShown(): + self._view.Show() + else: + view = self._CreateView() + view.OnCreate(None, flags = 0) + self.SetView(view) + else: + if self._view: + if self._view.IsShown(): + self._view.Hide() + + + def HideWindow(self): + self.ShowWindow(False) + + + def ToggleWindow(self, event): + show = event.IsChecked() + wx.ConfigBase_Get().WriteInt(self.GetServiceName()+"Shown", show) + self.ShowWindow(show) + + + def OnCloseFrame(self, event): + if not self._view: + return True + + if wx.GetApp().IsMDI(): + self._view.OnCloseWindow(event) + # This is called when any SDI frame is closed, so need to check if message window is closing or some other window + elif self._view == event.GetEventObject().GetView(): + self.SetView(None) + + return True + diff --git a/wxPython/samples/ide/activegrid/tool/TabbedView.py b/wxPython/samples/ide/activegrid/tool/TabbedView.py new file mode 100644 index 0000000000..6ac51bfbc0 --- /dev/null +++ b/wxPython/samples/ide/activegrid/tool/TabbedView.py @@ -0,0 +1,48 @@ +#---------------------------------------------------------------------------- +# Name: TabbedView.py +# Purpose: +# +# Author: Peter Yared +# +# Created: 8/17/04 +# CVS-ID: $Id$ +# Copyright: (c) 2004-2005 ActiveGrid, Inc. +# License: wxWindows License +#---------------------------------------------------------------------------- + +import wx +import wx.lib.docview + +class TabbedView(dict, wx.lib.docview.View): + + #---------------------------------------------------------------------------- + # Overridden methods + #---------------------------------------------------------------------------- + + def __init__(self): + wx.lib.docview.View.__init__(self) + self._views = {} + self._currentView = None + + + def OnCreate(self, doc, flags): + frame = wx.GetApp().CreateDocumentFrame(self, doc, flags) + sizer = wx.BoxSizer() + self._notebook = wx.Notebook(frame, -1, style = wx.NB_BOTTOM) + self.Activate() + return True + + + def AddView(self, viewName, view): + self._notebook.AddPage(wx.Panel(self._notebook, -1), viewName) + self._currentView = view + self._views[viewName] = view + + + def __getattr__(self, attrname): + return getattr(self._currentView, attrname) + + + def SetView(self, viewName): + self._currentview = self._views[viewName] + diff --git a/wxPython/samples/ide/activegrid/tool/UICommon.py b/wxPython/samples/ide/activegrid/tool/UICommon.py new file mode 100644 index 0000000000..0f83b65e91 --- /dev/null +++ b/wxPython/samples/ide/activegrid/tool/UICommon.py @@ -0,0 +1,115 @@ +#---------------------------------------------------------------------------- +# Name: UICommon.py +# Purpose: Shared UI stuff +# +# Author: Matt Fryer +# +# Created: 3/10/05 +# CVS-ID: $Id$ +# Copyright: (c) 2005 ActiveGrid, Inc. +# License: wxWindows License +#---------------------------------------------------------------------------- + +import os +import os.path +import wx +import ProjectEditor +_ = wx.GetTranslation + +def CreateDirectoryControl( parent, fileLabel, dirLabel, fileExtension, startingName="", startingDirectory=""): + + nameControl = wx.TextCtrl(parent, -1, startingName, size=(-1,-1)) + nameLabelText = wx.StaticText(parent, -1, fileLabel) + dirLabelText = wx.StaticText(parent, -1, dirLabel) + dirControl = wx.TextCtrl(parent, -1, startingDirectory, size=(-1,-1)) + dirControl.SetToolTipString(startingDirectory) + button = wx.Button(parent, -1, _("Browse..."), size=(60,-1)) + + def OnFindDirClick(event): + name = "" + nameCtrlValue = nameControl.GetValue() + if nameCtrlValue: + root, ext = os.path.splitext( nameCtrlValue ) + if ext == '.' + fileExtension: + name = nameCtrlValue + else: + name = _("%s.%s") % (nameCtrlValue, fileExtension) + path = wx.FileSelector(_("Choose a filename and directory"), + "", + "%s" % name, + wildcard=_("*.%s") % fileExtension , + flags=wx.SAVE, + parent=parent) + + if path: + dir, filename = os.path.split(path) + dirControl.SetValue(dir) + dirControl.SetToolTipString(dir) + nameControl.SetValue(filename) + + parent.Bind(wx.EVT_BUTTON, OnFindDirClick, button) + + def Validate(allowOverwriteOnPrompt=False): + if nameControl.GetValue() == "": + wx.MessageBox(_("Please provide a filename."), _("Provide a Filename")) + return False + if nameControl.GetValue().find(' ') != -1: + wx.MessageBox(_("Please provide a filename that does not contains spaces."), _("Spaces in Filename")) + return False + filePath = os.path.join(dirControl.GetValue(), MakeNameEndInExtension(nameControl.GetValue(), "." + fileExtension)) + if os.path.exists(filePath): + if allowOverwriteOnPrompt: + res = wx.MessageBox(_("That file already exists. Would you like to overwrite it."), "File Exists", style=wx.YES_NO|wx.NO_DEFAULT) + return (res == wx.YES) + else: + wx.MessageBox(_("That file already exists. Please choose a different name."), "File Exists") + return False + return True + HALF_SPACE = 5 + flexGridSizer = wx.FlexGridSizer(cols = 3, vgap = HALF_SPACE, hgap = HALF_SPACE) + flexGridSizer.AddGrowableCol(1,1) + flexGridSizer.Add(nameLabelText, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_LEFT|wx.TOP|wx.RIGHT, HALF_SPACE) + flexGridSizer.Add(nameControl, 2, flag=wx.ALIGN_CENTER_VERTICAL|wx.EXPAND) + flexGridSizer.Add(button, flag=wx.ALIGN_RIGHT|wx.LEFT, border=HALF_SPACE) + + flexGridSizer.Add(dirLabelText, flag=wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_LEFT|wx.TOP|wx.RIGHT, border=HALF_SPACE) + flexGridSizer.Add(dirControl, 2, flag=wx.ALIGN_CENTER_VERTICAL|wx.EXPAND, border=HALF_SPACE) + flexGridSizer.Add(wx.StaticText(parent, -1, ""), 0) + return nameControl, dirControl, flexGridSizer, Validate + +def AddFilesToCurrentProject(paths, save=False): + projectService = wx.GetApp().GetService(ProjectEditor.ProjectService) + if projectService: + projectDocument = projectService.GetCurrentProject() + if projectDocument: + files = projectDocument.GetFiles() + for path in paths: + if path in files: + paths.remove(path) + if paths: + projectDocument.GetCommandProcessor().Submit(ProjectEditor.ProjectAddFilesCommand(projectDocument, paths)) + if save: + projectDocument.OnSaveDocument(projectDocument.GetFilename()) + +def MakeNameEndInExtension(name, extension): + if not name: + return name + root, ext = os.path.splitext(name) + if ext == extension: + return name + else: + return name + extension + +# Lame +def PluralName(name): + if not name: + return name + if name.endswith('us'): + return name[0:-2] + 'ii' + elif name.endswith('s'): + return name + elif name.endswith('y'): + return name[0:-1] + 'ies' + else: + return name + 's' + diff --git a/wxPython/samples/ide/activegrid/tool/Wizard.py b/wxPython/samples/ide/activegrid/tool/Wizard.py new file mode 100644 index 0000000000..98d84eeb70 --- /dev/null +++ b/wxPython/samples/ide/activegrid/tool/Wizard.py @@ -0,0 +1,967 @@ +#---------------------------------------------------------------------------- +# Name: Wizard.py +# Purpose: +# +# Author: Peter Yared +# +# Created: 10/28/04 +# CVS-ID: $Id$ +# Copyright: (c) 2004-2005 ActiveGrid, Inc. +# License: wxWindows License +#---------------------------------------------------------------------------- +import wx +import wx.xrc as xrc +import wx.wizard + + +#---------------------------------------------------------------------------- +# Classes +#---------------------------------------------------------------------------- + +class BaseWizard(wx.wizard.Wizard): + + + def __init__(self, parent, title, pos=(-1,-1)): + wizardBitMap = getWizardBitmap() + wx.wizard.Wizard.__init__(self, parent, wx.NewId(), title, wizardBitMap, pos=pos) + + def GetDocument(self): + if self.GetParent() and hasattr(self.GetParent(), 'GetDocument'): + return self.GetParent().GetDocument() + else: + return None + + def SetPrevNext(self, prev, next): + prev.SetNext(next) + next.SetPrev(prev) + + +class TitledWizardPage(wx.wizard.PyWizardPage): + + + def __init__(self, parent, title): + self._prev = None + self._prevFunc = None + self._next = None + self._nextFunc = None + wx.wizard.PyWizardPage.__init__(self, parent) + self.SetSizer(wx.BoxSizer(wx.VERTICAL)) + self.MakePageTitle(title) + + + def MakePageTitle(self, title): + sizer = wx.BoxSizer(wx.VERTICAL) + title = wx.StaticText(self, -1, title) + title.SetFont(wx.Font(18, wx.SWISS, wx.NORMAL, wx.BOLD)) + sizer.Add(title, 0, wx.ALIGN_LEFT | wx.ALL, 5) + sizer.Add(wx.StaticLine(self, -1), 0, wx.EXPAND | wx.ALL, 5) + self.GetSizer().Add(sizer) + + + def GetPrev(self): + if self._prevFunc: + self._prev = self._prevFunc() + return self._prev + + + def SetPrev(self, prev): + self._prev = prev + self._prevFunc = None + + + def GetPrevFunc(self): + return self._prevFunc + + + def SetPrevFunc(self, prevFunc): + self._prevFunc = prevFunc + self._prev = None + + + def GetNext(self): + if self._nextFunc: + self._next = self._nextFunc() + return self._next + + + def SetNext(self, next): + self._next = next + self._nextFunc = None + + + def GetNextFunc(self): + return self._nextFunc + + + def SetNextFunc(self, nextFunc): + self._nextFunc = nextFunc + self._next = None + + + def SetPrevNext(self, prev, next): + self._prev = prev + self._next = next + self._nextFunc = None + self._prevFunc = None + + + +#---------------------------------------------------------------------------- +# Menu Bitmaps - generated by encode_bitmaps.py +#---------------------------------------------------------------------------- +from wx import ImageFromStream, BitmapFromImage +import cStringIO + + +def getWizardData(): + return \ +'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00}\x00\x00\x00\xfa\x08\x06\ +\x00\x00\x00\x8c5HE\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\x00 \ +\x00IDATx\x9c\xec\x9dy\x9cdUy\xf7\xbf\xe7\xdc\xbd\x96\xdegzVfe`\x86u\xd8WY\ +\x14\x90M\x10\x11D\x11W4\x89Fc\xe4M\xd4\xa8\xd1\x88\x91D\r\x1a\xb7\x98\x98\ +\xa8o\xd4\x08\xae\x08\x18EDE@\x04E\xf6}\xf6\xadgz\xaf\xae\xedn\xe7\xbc\x7f\ +\x9c[\xd5\xd53=\xc3$\x8c\xd0\xf3v\xff>\x9f\xea\xea\xbau\xebn\xbf\xf3<\xe79\ +\xcfr\x8e\xf8\xe4\'?\xa9\x99\xc1\xb4\xc15\xd7\\\x83\r\xf0\xc0\x03\x0f\xbc\ +\xd8\xd72\x83\x17\x00\xdf\xbe\xe1Fq\xcd5\xd7h\xbb\xb1\xe1[\xdf\xfa\xd6\x8by=\ +3x\x01\xf0\xed\x1bn\x04@\xbe\xc8\xd71\x83\x17\x013\xa4OC\xcc\x90>\r1C\xfa4\ +\xc4\x0c\xe9\xd3\x103\xa4OC\xcc\x90>\r1C\xfa4\xc4\x0c\xe9\xd3\x103\xa4OC\xcc\ +\x90>\r1C\xfa4\xc4\x0c\xe9\xd3\x103\xa4OC\xcc\x90>\r1C\xfa4\xc4\x0c\xe9\xd3\ +\x103\xa4OC\xcc\x90>\r1C\xfa4\xc4\x0c\xe9\xd3\x103\xa4OC\xcc\x90>\r1C\xfa4\ +\xc4\x0c\xe9\xd3\x103\xa4OC\xcc\x90>\r1C\xfa4\xc4\x0c\xe9\xd3\x103\xa4OC\xcc\ +\x90>\r1C\xfa4\xc4\x0c\xe9\xd3\x103\xa4OC\xcc\x90>\r1C\xfa4\xc4\x0c\xe9\xd3\ +\x103\xa4OC\xcc\x90>\ra?\xf7.S\x05j\xa7\xcf\xbb\xb6\xd7\xddM})P-\xbf\x97\xcd\ +w\xdd\xfc~\xb2\xe3\xb7\x9eG\x81V\xd9\x19D\xf6#9\xf1\x1aZO.&\xb9\x18\xd1\xf2\ +\xeb\xc9.\xb4\xe5\xd0\x8d\xf7\x89\x87T\x13\xf7\xc9\xce\xbd\xf3i\x9b\xf7\xa1\ +\x15\x88\xc9\xe9\xddOHW@\xda\xf2\xb9q\xe7r\xc2MON\x9bB\xa2\x10\xc4\xd9\x1e6`\ +\xa1\xb3o\xccQT\xd60tF\xae\xccN!\x18\'\xbd\xde\xf2\x9d\x95\x1dg\x92\x07\xaf[\ +64\xfe\x97\xa0\x859\xbbl\xec\xa3v\xfaa\xa4\xc1\x12\xe0\x8c\xef\x9ff\xbbY\x80\ +\xa5Ss\xfeD\x83\x10 ]\xb4\xcc\xae:\xdb\x14\x85u\xa4N\xf0<\x07t\x0c\xa20\xe9\ +\x13\xd9OH\x87]%u\xf2oa\\ 4*k\x1ez\xc2\x1e\r\xc2\xc7\x05G\xeet\x04\x05\xda\ +\xca\xa4\xb9\xf1\xbde\x9e,\xd2H\x90\xb0\x98\x8c;!\x0cI\xe6\xe0\x89aD\x02\x08\ +\xf4\xce\x8f\xbb\xd1(\x04`7~\xd8\xd0J\x12\x8d R \xe2\x98\xbcT 5H\x1b\xa4h^\ +\xb9\x06\x94\x80z\xa8\xf1\x1c\x1fGj\xd2$\xc4\x92\xbb\xef\xb9\xf7#\xd2w&f\xd7\ +o\x1bhH\x94yo\xfc\xc6\xa5\xf1\xe0\x1bD\xb5\xeegag\x92\xdc\xd0"v\x93M-$\xc8`\ +\xc2\xefZ\x05\xba\x01A\x82 F\x93"D\x84$E\x88\x86v\xf1\xb1\xd0\x08\xb2\xc6\ +\xd4P"\x8d\x83\xc8\x04D\n*F\xa5)JZ\xd8\xb6\x87\x94\x16\x96g\x83\xca$]+\xe2zJ\ +\xa4b\xb4t\xb0}\x1b\x04\xd8\x9e \x05R\x04\xca\xf2\xd1)\x14w\xc3\xfb~D:d\xca1\ +{\x9f\xfc\x8e\x04\x99\xc0\xa0\xb0\x9a\xf4\x8cKY\xabdN8\x82nl\x91\x93\xf6\xcf\ +\x91\x98\xd8\xc1\xec|U\x13\x8f\xdch\\2kt\x12\x81\x06\x92\x96\xe3\xb6\x9c]$\ +\xa8z\x19)\x15\xb8\x16\xd2\xb2\x91\xd84\x0c\x810V\xb8\xae\xd3<\x8b\xb0M/\xa0\ +Z\xce\xaa\x80zj4\x8d+\xd9#\xb3\xfb\r\xe9:\x93@\xb1\xd3\xf6\x9d?\x1b\x8e[\x1f\ +\x87\xc1\xcej\xb8\xf5\xf7\xb2\xf5\x0b\xbd\xd3;\xe6A\xb6jbh4\xac\xf1&(\xb2\ +\x06#qv9\x911\x0fB n\xd9\xa0\x0c\xf1\xd9Ae>\xa0A\xa3i0\xc6n\xd0\x02\x12GRJ \ +\x95`\xc9\xac\x8fo9\xbe\x02*U\xd8\xb8i\x1b###tuu1\xff\x80^\xda\xbcIn\x98\xfd\ +\x86\xf4\xbd\x18Y6\xc8n<\xd0\xe6\x937\xca\xbbU\x9dC\x0bY\x93\x11\xde\xd8\xa1\ +y\\\x1ar\xd7\xfc\x9d\xd5\xd8eg=\xbf\x13\xe1\xbaq,\xb1\xd3=\x08\x89F\xa2\x04\ +\xe6\x1d\x9b\x88\xc4\xd8iXXB \xb2k\x8e\x05Tm\xd3d$F_lZ\x17\xf3\xe4\xc3\x0f\ +\xf0\xd8\xc3\x0f\xd0\xb7e\x1d\xe5\x91!.\xbe\xe8B.\xbe\xf0\x02\xda\x0b\xd6n\ +\xb5\x12\xec7\xa4\xc3\xee\x89o\xe9\xe7w?f\x9b\xf0\xef8\xd9\x8d\xe1Mv\xfcV\ +\xb5\xa1\xc9\xfax@Il\xdd\xf2\xb0Z\x89nj\x95\x16\xa3O\xec\xfa\xafi<\x99\xf4\ +\xe24\xc7\x12\x8d\xf7\x10\x00;k\x04\x06\xe5\x10\xb6\xf7\xc1\xd6Q\xb8\xeb\xe9\ +ml\x19\x1ef\xe33O\xb2y\xcdcD#}\xcciw9b\xf9B\x0e?\xe4P\xae~\xc3\x15\xb4\x07\ +\x0e\xb6HI\xaa\x15r\xbe\x0f\xb2U\x1f\x8cc?"}o\xd1h\x04\xc20\xab\'6\x16\xa3\ +\x8a\x13 \x1d\'\xb59\xe6\xb6h\x92\'\x94y\xa9l\x18\xa7\xedI\x1aUf\x9d\x8bL\ +\xb34U\x819F\x9a\xd9\x01\xa6\x8b\xb5@\x0b\xb4\xb0\x891dG\x18\xb2\x13\xa0?\ +\x86M[S\x1e{\xeaY\x9exr-\x9b7\xf532P\xa1R\x8e\x18K,\xca\xb9"\xb1\x10\x88\xa8\ +\x84\xabl\x16.Z\xc6\xe9G\xaf\xe2\xd5\xe7\x9e\xc2\x91K\x0bx\x80\xd7\xb8\x7fK\ +\x82\xae\x01\xfb\xf9\x90m\x97\xbe{\xb7;\xecl\xc7\x9bm\xf5(\xc2w]\xc2p\x0c\ +\xdf\x95f\x1c\xab\x12P\nl\x1f\xa4\x83q\x81\x88\x89G\x91\nR\x05:1d\x8a\x86\ +\xd2m4\x18\xd5\xb4\xc6+\xd5\x1a^\xae\x8d\xc1J\x85B>O\x04\xd44\x14\x04\xe4\ +\x91\xa4\x11\x8cE\x1a\xbf(\xd8R\x82\xdf<\xd4\xc7\xedw\xdf\xc7#k\xd61X\xa9\ +\x93\x08\x9b0\x11D1\xd8V\x8e\xc0mC\xdb6\xd54\xc2\xb2]\x92h\x0cG\xa7\xac\\\ +\xb1\x8c\xd7]x&\x17\x9f\xd2C\x0e\xa8\xd7\xa0\x18`\xeeIG\xec\xde\xe44\xd8oH\ +\xdf\x1b(\x05J)\xb46"i\xdb6B\x18\xbb\xd9q]@\x19)\x17"s^\xa4\x18\tU\x90jb)QBd\ +t\n\xa4\x16\xc6\x95c\x8f\xabI\x9di\x84\xc68?\xd3\x19D@\x92\xf3\xa8\x01:\x9fg\ +\x04\xd8Q\x87\x8d\x9b\xca<\xf3\xd0\xef\x91\x03\x9bx\xd7\x9f\\I\xe0\x19\xe9\ +\x969\xf8\xc2W\xbf\xc3\xc6\xa1*\xa9\x97g8tHq\xb0\xfd\x80\xa0\xadH\x12\x0bv\ +\x94C\x84V\x14\x02\x97\xda\xe0f\x8a~\xc2KO9\x8e\xb7^y\x06\x07\xf7\x18\x034\ +\xaaAOn\x12\xaf\xe3\x1e\xa4d\xff!}\xa2\xcf\xb4e\xf3\xb8d\x0bI\xd3)\xd1\xb4\ +\xdf5\xa4\x99W,\xd2\x8a\x9c\xe7\xd3\xe8A\x8d\xd4\x02\xc2&\xc4%\x15\xb2\xd9\ +\xc7\x02XB\x90f\x8f(!#\x96\xf1.=e\\M?\xb5\x06\xd6m\x1e\xe0\xb1\'\xd7\xb1nc\ +\x1fO?\xb3\x1e\xe9\xfa(m1\xb8\xeea\xbe\xf0\x81\xd7S\xaaA)\x821\x05\xff\xf9\ +\x83\x87\xd98\x10\xe2v\x1f\xc0h\xa4\xc9\xe5<\xeaI\x8a\xd6\x82\x18\x1bK\x82\ +\xe3iD*\xf0\xa8q\xf8\x01\x05.;\xffd^~\xd6\xa1t\xb9\x868W\x80\x93\x83\xb8^\ +\xc7\xf62\x0f\xa1t\x19\xb7\\&\xc7\xfeCz\x03\x13\xfc\xcf\x06\n\xa8\xd5C\x10\ +\x16\x96ec\xb7v\xbfb\xdc\xa5\x19\'\x16\x81\x03B%\xe3\xbf\xb4l\xd2\xd4#\xb1lB\ +\xa0\x8e!\xb2q\x1a/{/\x01\xdb\x87a\xe3\x96:k7la\xdd\xc6\xed\xac\xd9\xb4\x95\ +\r\x9b\xfb\x19\x18.#\x1c\x9f(\x11X\xd2\xa3\xad\xb3\x8b\xb1\xb4\x83\x9c\xc8\ +\x11F1G\x1c\x7f&\xe7_p4\x0eP\x08`\xcdZ\xf8\xfe-?\x03\xa7H-\xb6(\x8d\x96!/\ +\xc0\xb2 \rI*#t\xe4\x02\x0e9\xf0\x00\x0e9\xe8 \x96\xcf\xf29\xe7\x88v\x96\xf4\ +@\xde5\xd7\x13\xd7S\xb4N\xb0\x02\xc78o\xa0\xe91L\x91\xa4\x18w\xd4d\xd8\xffH\ +\x87I\x89\xf7}\xaf\xe9\xa1\x0fS\x9a\x8d\xbd\xe1\xf4J\x00\x1c#E\x8e\xc8\x91VK\ +\xd8\xb9<H\x8fjj\xc8.\x01C\x11\x0c\x96\xa0\xaf\x0f\xd6\xaeY\xcb\xfa\'\x1fc\ +\xf3\xb6a\x9e\xd9P\xa7\x96\xfa\xc4\x1a\xb4\xe5 \xbd\x1c\xca\x0e\x88\xd4\x01D\ +~\x82\x9f\x0b\x08+\xa3H\xa1H\xeb)a\x1a\x11\x96C|Wr\xc5U\xafaL\x81-\xe1\xd9M\ +\xf0\xb1O\xfc3\xb1\x16T\xeb\x11\xa9\x8e\xe8\x9e\xddK=\x0e\xe9l\x0f8\xfa\x88\ +\x838\xe4\xc0\x85Twl\xa6o\xe3\xb3\xac\xe8\xacp\xd9\xcbz\x99\xef\x80\xa74Q\ +\xb5\x8e\x1bx\xb8\x9e&\xac\xd4Ij1\xb6\xefg\xcfc\x9c\xf0=\xad\xbb\xb6\xff\x90\ +>\x89zou\x89\xb6\xc6\xc0\xa45\xd1KU\x8f\xc1\xf1!V0:\xa2\x98\xdfe\x11\xe9\x1c\ +\xb6\xf6\x88\x04\x8c\xc5\xf0\xf1\xcf\xdc\xcc\x9a\xfe*Ol\x1ab\xdbp\x19\xa5\ +\x149\xd7\xa1\xe0YH\xe9\x93x\xb3I\x94G\xac\x04\x89\xb0H\xa5\x8d\xd2\xb69\xab\ +L\xa8%\x80\xeda\xb9\x16\xae\x15\x91ss\xc8\xa4\xca\xca\x15\x8b9s\xb5i|)p\xeb\ +\xed?\xe3\x91\'\x9e$\xd7s A\x10\xd0\xb3`\x01\x0b\x96-\xe3\xa4\x13\x172\xb0\ +\xbd\xca\x03\xf7\xdc\xc67o\xfb:g\x1e\xb7\x8a\xbf~\xeb\xe5,\xea4\x12\xab\xeb\ +\x11x\x127pH\xc3\x1a\x96e\xe1\xe5\xf3\x00\xd4jU\x82\\\xa1\xd9\xd5\xed\xd9a\ +\xbd?\x91\xfe\x1c\x08\x13\x8c\x07U\x8eG6c\x05\xa52\x8cU\xe1w\x8f\xad\'\xac\ +\xd5\xa9\xecX\xc7;\xdfr.\xb6\xef\x11i\xa3\xca\xcb\x11\xfc\xe0\'wSq\xbbH\xfcY\ +\x04\xdds\xf0\xfd\x00tB\xbdZ"\xac\x8d!\xdda\x12K\x92Z\x0e\xb1\xb4A8\x803\xee\ +t\x11\n\xe9(\xe2\xd2\x10\x92\x10\x9dT\xe8is\xb9\xf4\xa5\'\xe3c\xba\x89\xbb\ +\x1ex\x8a[~|\x0b\xc7\x1cw4+\x8fz)\x87\x1e\xb3\x98\xa7\xd6\xc1\x1f\x1e|\x8c\ +\xcf}\xea\xdf\x98\xdd&9\xf3\x84\x83\xf9\xf8\x9f}\x88\xc3\x16\xd8\x88z\r\xbfV\ +%\x08|\x94\xe3\x92*\x0b\xcb\x12X\xbe\xa1-M5B\x08\x82\\\x9b\xb9\x04\xc8\\\xcf\ +0\x1eQ\xdc\x15\x93lm\x98*\xd9a\xf4\xf3h\x17B\x8d\x8f\x93[%U\xb4^\xd8\xc4KiHh\ +CE5=ir|\xef\xc66\xd5\xb2\xef\xb3[C\xfa\x06\x86Y\xb3~\x0b\xeb\xd6of\xeb\xb6\ +\x1d\xf4\x0f\x8eR*U(\xd7b\x84\x9d#\x8d#t}\x84K_{.s\x03\x9a\x1e/\xed\x80\xdf9\ +\x87\x9ah\'T\x1e\xd5jL\xa9j\xa2Z\x16\x02\xcb\xf1\x11B\xa1\x85BK\xc5\xb8\x0f=\ +\xc9\x1c.\x1a\x92:mmyjcezr\x16A\xa4X>\xbb\xc0\xa5\xa7xh\x8c\xb17&:\xf9\xd8\ +\xa7?\xcb\x83\x0fo\xe4\xde\xdf=\xc1\xf7n\xfa\x11\xe5\xb1*s\xe6t\xf2\xfa\x0bN\ +\xe6e\xa7\x1c\xcaQK \x07\xb8h\x02_e!\xb42\xd2\x9fE\x9ay\xf0\x1b\xae i\x8dG\ +\xda&\xf4vZ\xed\xe4j\x9c\x88I\x18\xadS+\xf5\x13\xb4u\x9a\xa7\x11+\x90.qX\xc3\ +q\x1c\xb0\xed\x89$\xa6\xb1\x19+9\xce\xc4\xc34\x9d\x16\r\x96$$\nt\x089\x87zX\ +\xc6\xf7\xbd\xe6-\x84\xf5\x08\xcf\xcb14\x92\x90\xef4FU5\xfb6\x02b\r\xfd\x03\ +\xb0q\x8bb\xd3\xe6\xadl\xee\xdb\xce\x86-[\xd8\xb8e\x07\xdb\x07\x86I-3\xe4I\ +\x85\x8b\xd6\x0e\xa9\xf0Q\x14\x8cq\x17\x80\xadS|\x1f\xea\xf6\x08\x1b\xab0+\ +\x00\'5\xb6S\xa5\nc*\xa2\xee\x08\xa4c\x91\x92\xdd\xb7\xd2\xa4\x96\xc0q\x8b\ +\xe4,\x9fr\xb5N\x12\xa6\xe0f\x11\x0f\x9d\x00\t\xc2\xd1\x14r\x16#\x1b\x1ea\ +\xf9\xfcY8\xb5a\xda\xa9\xf1\xc1\xb7\xfc\x19\x1d\t\x945|\xe5\xc7\xdb\xb9\xfd\ +\xfe\x87x\xfa\xf1\xc7\xb1\x89\t\x1c\xc5\xbcN\x8fs^y\x06\xaf:\xefx\xe6w\x185\ +\xeeb\x0emi\r\xda5B\xe7\x18\x15&E\xab\xaf\x7fw\xd8\xf3pmr\xd2\xb5"h+\x00)i=F\ +Z\xdd\xa0\xc0\t\x82\xa6\xcb1\x8d\x12\x94RH)\x91\xb6\x8d\xd8\x99p\x1aWe\xa1\ +\xb5y6q\x15\xbc\xbc$J\x03,\x01\xa9\xe5\x91 \xb3\xc0\x81\x8d\xed;\x94cp:m\x1e\ +\xdb\n\xf7=\xbe\x9d5[w\xb0cd\x98\x07\x1f\x7f\x92\xa1\xe1\x1aa\xa2\x89\x12\r\ +\xc2A:>J:\x84\xa9K\x98\xce\xc6q\xf3(lt6\x866\xc1\x8c,\x14\xa2S<\x15\x92\x88\ +\x94\x9a\xcc1\x10\x92Y\xb7\x11\x12\xd7\xd8\x00\x96$L5:\x8a\xc0\xb5\x91\xb96\ +\xa4\x94\x88\xa4J\x12%\x0c\x8dV\x10\x96\x8b\x1bxX\x96&Ik\xc4\xe1($\x15$!\xb6\ +/9\xe2\xc0\xf9\x1c\xb2t!\xe7\x9dv*\xa7\xaf\x0ep1\xae\xd4k\xaf\xbf\x95\xef\ +\xdc\xfd4\xe4;\x11B\xe2\xdb\x92\xd3O<\x82\xab.=\x9bU\x0bhz\xd3LzG2\xae\xe2\ +\xb4e\x04O\xb4<\xd2=\x12\xdeB\xfc\xff\x88t,\xd2Ha9\x0e2(\xa2\xb4D\x0b(\x95\ +\xc6\xb0\x1d\x85\xeb{\xd8\x9e\x8bF6;\x82\x18\xe3\xc0L1\t J\x9b8\xbf\xc4\xb8"\ +\x95\x03\xa2\xc3\xec[Q\xc6C\xd8\x19\x04\xa4\x80EL\x14\xc3\xc3On\xe0o\xae\xfd\ +<\xc3\xaa\x8b2\x05\x06*\x11N\x90\xa3\xad\xdd\xa7\x16\xa6DV\x8e\xc4\x92h\xc7A\ +K\x0f!=\xb4\xf4A\xd9\xd8\xbe \x8e3/\x94H\x91$\x08\x1d\x9b+\x14)B$h\x99\x12\ +\x93\xa0\xec\x1aC\x95\x86\xf2\t1\x8a\x14t\x9cP\xc8\xe7\xf1\xbdv\xa2\xd4\xa6T\ +IH\xc2\x9a\xb1\x87\x1d\t\xf9<\xaeoc\'U\xc2\xd1\x1dX\xd1(+f\x179\xee\xf0\xc38\ +x\xd9B\x0e_u K\x96\xc0C\x8f\x86<\xfa\xd43\x14f\x1f\xca\xfc\xf9\xf0\x85o>\xc1\ +-\xf7>\x86\xa5\x13\xba\x04\x9c\xf9\xd2\x13y\xcd\xabN`Yo\x96$\xa3L(t\xe7\xc8\ +\x19\x8d\x86\xfb\xdc\x0c\xff\x8f\xb1+\xe9J#\xdd\x80\x08\x9b\x14\x17%`\xf3vX\ +\xd0[\x04\x8c[\xa3\xc2\xf8P\xa8\xd1\xc35-h\x01\x89\x80\xb2\x82\xad\xdb\x15\ +\x1b\xd6od\xeb\x8e\n\x8f=1\xc4\xb6\xbe\x01|_Q\xf4k|\xfa#WR\x04\x02\x1c\\\x07\ +\x0e9l\x11\x1d\x0b\x0e\xe2\xee\xfb6RX\xb8\x84\xb0\xdd&\x12\x82\xca\xe8\x08\ +\x96\xe5!\xdc\x1cJ\xbahe\x11k\tI\x16\x96\xd4\x99w\xdbvi\xf8\xc2\x15`i\x85 \ +\xc5R\x86x-\x14\x89V`[\x0c\x8fVQ\xe4\x8c\xe7N@\x1c\xa6\xf8\xaeK-\xac18ZC%\ +\x0e\x8e_ \x97\x0f\xf0dB\xaa\xea\xa4n\x85\xf2\xd80\x9ecq\xd6)\xab\xb9\xf8\ +\x9ccY\xb5\x0c\xfa7\xc1\xe3\x0f\xaf\xe5_\xbf\xf4\x9fl\xdc\xbc\x89TH\xbe\xf4\ +\xd5\xf7\x91\xcb\xc3\'\xbft\x1f7\xdd\xfa\xdf\xf4\x14s\\\xf6\xb23\xb8\xfc\xdc\ +\x13Y2\xcf<\xbb\xb0\x06\xb9\xc0\x0c\xe3\x06\xb6\x0f1\xaf\xb7\xab%\xa6\xcfN\ +\xf1\xf6?6\xe9\x96c\xe2\xb3J"\xa5!\xb9\xad\x17\x06ScLI1\xee\x8d\x1a\xac\xc0\ +\xb6~\x18\x1c\x89x\xec\xc9g\xd9\xd6?\xc8\x9aM[\xd8\xda\xb7\x83R\xa5F\xaa\x05\ +\x08\x0b%|:\xe7\xae`\xc3\xfa!\xda\xf2\x1a/\x1d\xe0\xee\x87\x868\xfb\x88.\x12\ +m2\x85F+p\xe69\x17\xf2\xcbM?\xa6\xe6t\x91\xc6\x02\xb7\xad@4\x9c\x82NI*\x0eX\ +\xbe\xb1)l\x99\x89\x87\x00\xdb1\x07\xa8\xd6\xccU\xa5\xc6x\xd4\xa9m\xd4\xbbv\ +\x10J\x81%\xd0i\x88\xe3\nF\x86B$9,\xed\x9b\xe0G=\xa4^\x19\xc5o\xeb\xa43\x9f#\ +\x8e4\xb5x\x8c4\x1eF\x11\xe3\x881\xce;i5\xa7\x9dq\x11\xc7\x1d\xe2R\x03~~\'|\ +\xe8\x1f\xefe\xed\xba\xadDaB\x14\xa6X\xf4\xf0\xe9\x7f|\x1b\xddy\xf8\xfa\x7f\ +\xfd\x81_|\xf7\x8b\xbc\xf3u\x97p\xc5+_\xc1\xb2Nsyv\n\x8e\x05E\x17\xa2\xb1\ +\x18\xc7s\x987\xab\xab%N\xd4 }\\\x90\x1aMa_q\xbf\x0b\xe9\x89\x82XZHiS\x03\ +\x9e\xd8\x92\xc5o7E\x0c\x0e\x0c\xb1i\xcbV6n\xddF\xdf\xc00\xa3\xa5\n\x95zB=Q\ +\xe0\xfa$Z\x92`\xa1\xadnd\xe0\xa1\xb4 JS\xa2\x04J\x1b\x07 \xd7A)\xad\x90\x17\ +yn\xf8\xe1m\xbc\xec\x88\xd7\xe0d\xb1\xeaYy8\xf5\x84^\xe6\xfc\xb0\x9b5[F\xc0i\ +#\xaaE\x90@\xd0\xdeI\xb9&\xd1"\x1b\x17\xa7\n\x92\xba1 E\x96\x15\xe8f\xca13\ +\x1e\x95e\x11)3`\xb7\xb4\xc2C b\x85\xeb\xd9\x8c\x0c\x8c!\xe8D\xe2\x98\x04\n\ +\xad\t<\x97rRg\xa4RFk\xcd\xbc\xf9\x0b9\xe5\xe4\xe39\xeb\xb4y\xacZ`\x0e\xfd\ +\xdb\xdf\x0f\xf3\xc1\xeb~\xc6\x83\x8f\xaeed\xccBY9\x90\x05\xa4\xb0(\x14\xbb\ +\xb9\xfa\xaa\x0b9j\x05|\xee\xf3\xbfbx\xeb\xa3\xfc\xfc\x86\x7f\xa1\xb7\xd3\ +\xa1\xcdR\x08\xa5Qq\x8c%$\x8e4n57\xe7Ld\xb6\xa5\xd3\x9e,\xffn_a\x17\xd2\x85t\ +PH"`\xfd(\xfc\xf5G>\xc7\xe6\xed5\xea\xa9\x0b\xc2EH\x1b--\x94\xb4\xd0\xf8\xc4\ +Z\x92h\x89\'\x0b\xc4\x1ab%P\t\x90\xd8Y\x9fd\x81\xa3\xc0\xad\xd3\xde\x99\xa7V\ +\x95\xe4l\x87\x87\x1e\x7f\x96\xdb\xefz\x86\xd7\x9cr \xb6N\xb0\x84\xcd\x1c\ +\x17\xce>\xfe`\xbeq\xdb\x1fH\xf3\x01\xd50\x81(\xa4\x16V\xd1\xb6\x9b\r\x01\ +\x95I:\xd1\n\x07\x10Z R\x8d\xaa\'&\x94)e\xa6\x92\xa4\xe94\x85 M4Bid\x92`\xa5\ +\x16\xa5\xc1a,\x0e\xc0B\xa0\x05x\x8e\x8b\xef\xfb,]y8\'\x9dx<\xc7\x9f`3;\x075\ +\xe0\xae_\x0e\xf1o_\xb9\x83\xc7\xd7m\xa5\x16\x83\x14\x1e\xbe\xd7\x85\xdd&(\ +\x97*X\xc9 m\x81\xc79\'\x1c\xcf%g\xc2\xb3\x0f\xa7\xbc\xe9\x15\xa7r\xe0\xfc\ +\xd3(ZP/\xf7\xe1\x15r\xe6z\xbc,\xf6\x1a\xd7M\xc3u<v\xee\xc9\x1b\xe3\xaf?BW\ +\xbe{\xd25PU\t\x15i\xb3}(dc\xdf\x00V~\x01R\xe4\x89\x85\x87V\x90jE\xaaMF\xa9\ +\x906\xc2\xb2)\xc7\tH\x1b\xe1\xd8\x08a\x99\xfeR7\xee@c;\x92\xd1\xfe\x1d\xa0S\ +\xc6|\x89R\x1e\xdf\xbb\xf5W\\z\xca\x81\xd8\xb1"\t+\xcc.\xe6\xb9\xfaU+\xb8\ +\xf9\'w\xb0\xb9\xaf\x0c~7^{\'q\x9a\x18\x89N\xe3,\xb6\r\xae\xd48B#u\x8a\x16\ +\x1a\xad5If\xb1\xa7\xcd\xfc\xb2\xcc\x18"\xc1\x11\t\xe81\xac4$\xac\x0c\x98\ +\x1b\x95\t\x02\x1b\xcfs\xf8\xfa\xd7\xde\x8b\xb6\xa1\x94\xc0\x9dw\xc3On\xff\ +\x15\xeb\x9e}\n+)!\xbd\x1c\x83tBl,\xae$\xb5\x10a\x05W\x8fq@O\x91C\x16w\xf3w\ +\xefX\x89\x03,9\xdc\xc2V\x10HP\xe1\x18\x85|\xc1$5\xca,\x04\x9b&\xa6\x8f\xb4]\ +P\x19\xe1I\xc6\xc4N\x03n\x89B5\xd5\xc0\xbe\xf3\xa3\xedr$\x05\xf8\x99j\x7fv\ +\xcd::\xba{\xd8<\\\'\xc9\x07\x84I\x16\x9dr\xb2\x9f\xa5\x11\xd8\x8a \xb0\xa0\ +\x1a\x03\xa1q\x056.\xbey\x03)\xc9H\x89\xe2\xac^\xc6*\x11\xf58&\x8a\x03\x9e\ +\xdd^\xe7\xa6_<\xc1\xa5g\xac\xc4\xb3lT\x0cK\np\xf1\xa9\xab\xb8\xf1\x97O0\x10\ +\x8f\xa1\x9c.T\xea\xc0X\x84W(\xa0uLT\x1b%t \xe8\xcc\xa3I)\x8d\x8e -\x10B \ +\xb0\xf0\xb4\x83T.:\x15$\xb1\xc6J\xca\xf8n\x15\xcf\x1b!\xc8\x179\xe2\xd0\xa3\ +\xb0t\x02\xa2\x8a\xa4\x8db\x11~\xf5 |\xef\x8e\'\xf8\xc3CO\x90*A\xac%q\xadH \ +\x1d,\xbf\x13T\x07\xe4|`\x8c(\x1d\xc1\xaa\x0f\xd3\xa6\x868\xe3\x88\xc5\xbc\ +\xe7\xcd\x172\x0b\xf0P\xc6\xb0\x14\nR\x89t\x023\xce\x16\xc6\x85\xa4Q\x08+K\ +\xd4\xd0r|d\xe50\xd1y\x95\xa5~\tT\x96\xe8\xd5`f/\xd2\xc6\xfe7\xa4\x9b\x8b\ +\x968\xc0\xdc9\xb3\x19\x1d\x1a\xc4\xcb\xcd\'t\xbcq\x07\x8c\x94F\xf2\x92\x04\ +\x92\x88Z\xb9d\xbc\x1c\x00\xc4\xd9\r\x8c{\xdb\xa4V\x88\x9cO\xa5n\x86HX.:\xef\ +\xb2v\xa8\x8f\x9b\xefy\x9aSOXI\x87\x96\xe4|\xf0\x81W\x9fs"\xb7\xdf\xfb0\xa3Q\ +\x88\xf0\x02b\x95\xd0\xd6\xe5\x13U\xcb\x08\xa9i\xef\xea\xa0\x1a\xd7\x18\xd9\ +\xbe\x03l\x81\xd7\xd5\x0eI\x84\x8e\xebx2\xc5S\t\xb5\xd1\xad$\xd5\x1aK\xe7\ +\xccc\xc5\xaaE\xac8x\x01g_\xb0\x8a\xdeb\xc3\x01\x12C\\\xa3\x9c\xd8\xdc\xf3\ +\xdbM\xbc\xf7c\xdf\xc6\xed=\x84Z5!\xad\xc6\xb8\xddsi\xebj\'\x1c\xd9Ne\xb8\n\ +\xb3z!\x89!\xa9b\xa7c\x1c~\xd0\x02\xdep\xf1\xe5\\|\xf2B\xda\x01c\xde\xc5Y\ +\xb6\x8dm\x98\xd4Y\x9f-%\xa9\x90\xa4Y~,\x183\xa4\x99\xb65A@\x1a<4\xbel\x06z\ +\x9f\'\xd5\xe3\xd8\x8d\xce\xd0\xd8\x08\x96.\xe8\xc2\xd61\xa9V$ue,\xe5F\x02\ +\xa2\x94&\xf1^H\xd3GY\xc6h\x92Z \xb2\xa1\x90\xcc\xd2\x91\x94\xb0\xd1\xb6\x8b\ +\x8a1\x16x\x02\xa2\xbd\x97j%\xe1\x96\xfb\xb7r\xd2\xfdc\\\xf1\x92"hc\x9b\x1d\ +\xba\xcc\xe1\x94\xa3W\xb1\xe9\x97O\x12bAm\x8cR8\x8a\xd45\x94v\t\xcb\x1e\xc2.\ +`\xb5-@\x0bIRMpH\xb0b\r\xe10\xedm\x82W\x9eu\x14\xe7\x9fy\x12\x07/\xcf\xa1$X\ +\x1e\xac/\xc1\xadw\x8e2\xf0\xec\xef\xf8\xe8kO\xa6\xcd+\x1089\xfa\x06\x86\x99\ +;o!}\x89&\xd7\xd6\x8e\xd5\xe51Z\xa922R!\xb0S:\x9c\x94t\xe01T\\a\xc5\xf2\xf9\ +\x9cz\xc2I\x1c\xb1j\x01G\xae* \x80\x11@\x11\xe0b\x9b<V\x89q\xac\xb4D\x83\xb4\ +\x84$\xa3\xbc\xc1\xb1\x16\xe3!}\xa9w\xd3\x8fk\xd8%\xa9\xf2yb\x17\xd2m\x01u\ +\x15cI\x97\x9ev8r\xd5r~\xf5\xc8v(ta\xfcF\x1a\xa2\xd0\\\x8c\xef\x984#\xdb\xc9\ +2\x15\x1aw\xa8\xb2\xb4b\x95\xa5\xa9IT"\x8d\xe1\x92\nH4\x1a\x17\xd99\x8f\xd1\ +\x81\x88/\x7f\xf7\x0eN;\xf5"z\x04\xe8\n\xe4\np\xf9Eg\xf2\xe3{\x1eckm\x14\x02\ +\x17\x94C\xb1\x98\xc3q\\\xc2ZD\x1a\xd5\x90i\x88\x8e#\xa2\xea\x18+\x97/\xe1\ +\xd4\x13^\xce\xa9\'\xceb\xf1\x02\xe8q\x8dl\xf4\r\xc2\xe3O\xf7s\xdbo\x9e\xe6\ +\xb7\x0f?\xcd\xe0\xd8(\x87-\xc8\xe1\xbe\xf9\xa5$qH\x05X\xbba+\xa5j\x8d\xb0\ +\xae\t\xed\xd0\xe4\x98\xc5\x11N\xc1\xa1-\'H\xc7\x06\x99\x9b\x83\x8b\xcf9\x93\ +b\xde\xa6P\x80\x97\x1c^\xa0-\xcbP\xcd5\xf9\xb1H\x11H\xa4I\x89nI\x9f\xddY9\ +\xb7\xfa\xd0M\x8atK\x97\xde\xd4\xe8YU\xcd$\xa1\xe4}J\xbaQ91\x0e.\xbe\x86W\ +\x9c\xf5\x12\xee{\xe4[&\x7f\xcb\xf6\x11Bd\xbe\xe9\xd8T\x00H0J\xd9\x90\xac\ +\xb4)\xc8i\xcd\xd2j*z\xe9\x98\x84.\xdfC\x8f\x8d\x80\x90\x04\xb3\x96\xf0\xf0\ +\x13\x8f\xf0\xef\xdf\xdf\xc2\xbb_5\x9f\xae\x82!\xeb\xc8\xc5p\xf4\x8a\xd9l}`\ +\x0b\xd6\xec\x83Pr>\xa3\x83\xfdP\xed\x03;bv.a\xc5\xbc\x0e\xce;\xedX\xce:\xed\ +\x10fw\x8d\xfb\x10"\xe0W\x8f\xc1\x8d?\xba\x87\xdf?\xfa,\x89r\xa8\x8e\x85($Aq\ +!\xdb\x06\x87\x19\tA\x97\x14\x85Y \xed<q\xaa\xe9\x9c7\x87\x91\xe1~\x1cGr\xc0\ +\x8a\x05\xec\xd8\xb6\x96\x81\x1d\xeb9~\xd5R\xde\xf7\xe67\xf3\xab[o\xa2\xbbg\ +\x11\xaf:\xff(c*jh\x1708\xd0GoOwCqgn`#\xc9\xaa\x85\xe9\xc9\xd4\xea\x1e}\xe8\ +\xc0\x84\xc4\xce}D\xfc$\xd7\xa1\xb0\xb3\xf4@G\xc3K\x8e_\xc6\xd1+\x97\xf3\xf3\ +g\x86Q\xb1\x85t\x1c\x1c\xd71\xea*N\x8c\xcf5S\xed\x8d\x0bS;\x8f9\xa4\x04\xd73\ +\xdd\x80\xaa3\xcb\xf3\xe9\xdf\xb6\x03\x1d\xa7x\x07\x1eL\xad\xd0\xc3\xf7o\xbb\ +\x8b\xcb/\xbc\x9c\xceLBs\xc0k^~"\xf7<r#\xb5\xea\x16\xaa\xd5\x88y\xf3fs\xf4\ +\xaaS9\xe9\xa8\x15\x1c\xbb\xd2bI\x0f\x04\x8c\xf7v?\xbfw\x1b?\xfe\xe5\xfd<\ +\xb6\xae\x9fm%\x8b\xfe\xaa$\xd5>xy\x9c EJA\xe2\xbbHO#=(\xcc\n\x08\x81\x07\ +\x1e~\x8c\xd9\xbd\x8bY\xbbf\x03\x859\xddx\xae\xe0\xd9\xbb\x7f\xcd\xf2\xa3\ +\x0f\xe6m\x7f\xfeA\xe6\xb5\xd9|\xea\x13\x1f\xe4\xedW]\xc1Y\xa7\x1d\x86\x0bX\ +\xda\xa4+\xd9\x1a\xe6u\xf7d\x9eA\x00\x0bK(\x92,\x99\xa1\x91\r\xeb\xd0\xda+7\ +\x8a\x1a`\xd7D\x81F\xdc`\'Z\xfe\x98\x92\x0e\nG(\x14\ty\xcb\xc6/\xc2\xe5\xaf<\ +\x87G\xff\xed&\xb6T\x07Qa\x8am\xe5\xb0\x1c\x974\x8e\xd1\x02l\' I\x9b\x01\xbf\ +\xf1\x8e\xaa\x19\xf1\xc9\x8a\xeetBQ\xc6\x04\x95>:u\x95\x9a\xb4\xa8\x8d\x0e\ +\xe2\x17\x8b\xf4\x0fo\xe4\xa6[\x9fb\xfe\xcb\x0fd\x9e\x1f\xe2j\xc9\xd9G\x1f\ +\xc0\x95\xe7\x1c\xc7X\x04\x17\x9dv$\x07-k\xa3\xb38\x9e\xabV\x05\xeez\x10~y\ +\xf7}\xfc\xea\xb7\x0f\x80\x93g\xb4\x962X\x8a\xc1q(\xf6\xf4\xa0\xb1)\x97J\xc4\ +\xb6\xe9`\xc3\xea(#NJ\x04F\xda]\xa8\xc51k7nd\xf6\xf2\xd5\xec\xd8\xb4\x16\xab\ +\x90\xe3o\xae\xfd(K\x0f\x80\x9f|\xffn~w\xe7\xcd|\xf5\x0b\x1fa\xc1,\x1f\x91\ +\x19\xdd\xae0\xde\xb5\xfah\x15\xdf\x91\xe0\xdb\x99\xa3H\x83L\xb1E:\xa1\x1a\ +\xc6\x82\xac\x05d\x01a\xd1Z\\\x99\xa59\xa5\xae1\xf02\x9d\xaf[\x98\x17z\xdf\r\ +\xde\'!]"\xb1HU\x8c+m\x12\r/?\xa5\x8b\x1f\xdd;\x0fwC?[\xfaJ\x08R\x84\xce\xa3\ +\x89A\xd9\xa4"\x1d\'\xba9\xe4hI\xfe\x97\x02\xa2\x14\xa4\x83\x1b\xe4\x18\xd9\ +\xb1\x81\xae \xcf\x9cb;\xcf\xec\xd8\x84Wpq\xd3*\xb7\xfc\xe0{\xbc\xf3\xdc\x0f\ + \x85G\x1aV\xe9\xf4=\xdeu\xe5K(\x16\xc1\xcd\xdc\x97\x03\x11\xdc\xf5\x871~r\ +\xd7\x03\xfc\xfe\xa9\r\xec\x18\xaaPM!\x16.\xda\xb6I\xed\x0eh\xb3@\xa5\x8c\ +\x95C\x10%\x84\'\xd1q\x82\xdd\xddER\xaf\xa0\x92~$&\xdf\xac\n\x0c\xf4\xef\xa0\ +\xa7}\x05\x95\xe1m\xbc\xfd\xea7q\xe1\x05\xf0\xb3\x1f\xc3_\xbf\xfb\xb3\x9c\ +\xb4j>\xdf\xf8\x97\xeb\x98?\xdb\x1c\xd6\xce\x08\xac\xd7\x14\x05W\xe2\xb7\xe5\ +ZF+\x13+kdV\x9c\xd4,F\xcd\xde\x13=IT\x92\xf1G\xa8\xb5I\x8e\x10b\xcf,k\xad\ +\x9b\x99\xbf\x8d\xfd\x1b\x9f\x1b\xdb\xf6\x92t\x07)-\xdc\xacO\xb1\x84\x91\xaa\ +\xbf\xfd\xcbsx\xc3\xbb>\x8f\x1d\r\xe1\x06s\x88\x1b\xe3M\xaf\x88\x96\x05\xa3\ +\xe6\xa3(;\x84c:\xb3\xd48lp\x02s*\x1d3\xa8\xa0\xadk\x01\xe5\xb0Dep\x0b\xb3\ +\xad\x84\xbcR\xac>d1\xe7\x9ev,\x96\x80\x10\xc9`I0\xdb\x87\xf9E\x18\x0ba\xc3\ +\xa0\xe6\xab\xdf\xbd\x9d\x1f\xde\xb3\x96\xedQ\x11\x95\xef!\xa1\x17\x9cQ\xa4]\ +G\x08H\xb5@[\x018y\xa8W\xa1:\x08N\x9d|\xb1H\x19\x1b\xe9\x17`\xc7v\xba\xba,\ +\xbc\x10\xda\x1d\xd8\xbc5&G\xca\xeaUs\xb9\xfa\xbd\xaf`\xf3\x08\xfc\xc5{oc\ +\xeb\x9aG9xn\x8e\xeb>p)\x07\xb4\x8d\x9755\x0c//\x90M\x12\x1b\xf9i;[\xd9\xa2\ +\xf5\x01?G\xdf\xfe\xbf\xc1d\r\xa3\xf1Y)\xf5?!\xdd\xa8\xe8V\xd5\xe4\x02\xdd\ +\x12\xde\xf7\xa7o\xe5\x9a\x0f\xff\x03a\x181T\x1e\x00\x7f\x16\x08\x1bj\x11\ +\xe4\xf2\xe0{\xc84E\xa5!\x12\x8d\xe5X(\x95\x92\xd6\x86 \x8d\xb0\xf3.V\xbdDyd\ +\x1b\xbd\x05\xc9KO_\xcd\xb9\xa7\x9f\xc8\xaa\x03\x1d|\x0b\xba\x0b0:\n\xbe\x07\ +\x1d\xb3\xf3$@R\x8fy\xe4\x91\xb5\xbc\xfd\xfd\x9f\xc5?\xe0\x18\xd6\x0f\x01\ +\x9d\x9d\xa0\x02\x88k\xa0$\x85\xf6N\xc2J\t)-T\x14\x9a\xc4\xb8\\\x80=g.Vm;\ +\xe5\x1d\x03P\x9cEt\xff\xfd\xc8\x85]ty\x92.\x17\xa8%T#\xc1G?\xf1i\x96\xac\ +\xb4\xf8\xd1m\xc3\\\xff\x95o\xe0\xb8\x16\x9d^\xcag\xae\xfd\x13:]c\xa6\xee6\ +\xe0\xb1oGS/\x08\xf6\xaa\xd1I\xa0\x08\x1c<\xc7\xe7\x03\xef|\x0f\x9f\xfa\xd27\ +\xb1;\xbb\x19S9\xac\\\x07\xa3\xa5\xbaIF\x93\x1a\xdbQ8D\xa4a\x15\x15W\xf1,(\ +\xfa)\xbe\x18\xa0\xd7s8\xe9\xd4c8\xef\xec\xd7pp\x16\xc4P\x98\x86\xa5\x81Q`\ +\xfd\xd0\x18\xbf\xbe\xff).~\xd91$qL\xbb/8\xfa\xd8\x83Xt\xe8\xa9<S\n\x08zSB+D\ +\t\x05i\x1d\x11\x85DC)\x8e\xe3\x92*\xc0\x89\xc1\x11@\x95\xa4\x1c\x93\xc4\x16\ +\xf8\xb3\x90\xae\x8b{\xf0"r\xd10\xb5\x91\xed\x0c\x0eV\xe9\xe9\xca1{)\xdc\xf1\ +k\xf8\xfbk\xbe\xcb\x86\r\x1b\x98\xdb\xddEyp\x0b\xef\x7f\xd7\x9b\xe8\xb4 \x10\ +{ \xbc\x05i\x9abY\xfb\xce\x81\xf2\xc7\xc4^\x91\xeed\xdd\xc4\xe2N\xc8\x1f\xdb\ +\xc6\xba\xb5\xa7\xf2\x99\xaf~\x1f\xb7{\x01\xa5r\t\xd7/\xd0\xd6\x9d\xa3V\x1aD\ +\xc6U\x8a\xae\xc2\xf5\xeb\x14:m\x0e9h9\xc7\x1d\xbe\x84\x0bN_Jo~\xbchO`\xba\ +\x8d\xe1\x14\x1e{&\xe4\xc1G\x1e\xe7\xe7\xbf\xb8\x83\xca\xe80\xfd\xdb\xd6Q\ +\xaa\\\xcdU\x17\x9d\xde\xac\xf7z\xef\xfb\xae\xe0\x92\xb7}\x15\xdd\x96G\xd5k`\ +[XB\xd2Y,P+UH\x13\x81\xf4]p]\xe3\x9fO#\xd0\x02\xcb\xb1\t,(\x97\x07\xc9\x17\ +\x1d\x866\xaf\xe5\xb4c\x97\xd2\xd9\x9dc,\x86/\xdc\xbc\x85/}\xff\x97\xa8(DhI2\ +\xb6\x83W\x9f}<\x97\xbfl>\x05\x8c\xd5])W)\x14r\x93?\x9c\x0cS\x8d\xf4=\xd9\ +\x03{E\xba\x00t\xa5N[\xdeG\xe4\xe0]o:\x14%\xeb|\xf7\'\xbfF\xfb\x8ajT\x82J\ +\x8c\x9b\x96\x99U\xb09\xea\xf0e\xbc\xf4\xd4c9\xe6\xf0\x85\xcc\xca\x19\x97\ +\x8e\x93@:\x16\xe2\xe6=\xa4\x84\xa7\xfa\xe0\x87?\xbd\x97_?\xf0$\x83UM\xdfP\ +\x19\xcb\n\x08\xdc\x1c\xb3\x97\xcc\xe5[\xdf\xbb\x9b\xf3\xcf>\x9d@A\x90\x87Es\ +`\xd5\x8a\x05<\xf0\xf4:\x8a\xdd\xbd\x94\xab!i\x12\xa3}P\xbe&\xd6\n\xe9\xb8&\ +\xb9\xa2\x12\x82kS\xec\xca\xe1\xd6G\x18\xda\xbe\x91\xce\xce\x02VX\xe6\xeb_\ +\xf88g\x1cl\x0c\xe9\xba\x05Oo\xafR.\x0b\xe6,ZLi\xc3\x83\x9c\xba\xfa >\xf6\ +\xce\xb3\x08\x07\xab\xf8]\x86\xe8B~\xcf\x84\x03\xb8\xee\xeeJ\x0b\xa6\x1e\xf6\ +\xd2\xa6H\xf0\xac* \xc8I\x8f\x04x\xfb\x95\xc7\xb0f\xcd\x83\xfc\xfe\xf1g\x98\ +\xd3\xd9\xc51\xc7\x1c\xc5\x19/9\x91c\x0f)\xd0(\xaf\xf70\xaa\xb1\x86\xc9%\xac\ +[\x1ew\xdc\xb3\x9e[\x7f\xfe\x1b\x1e^\xbb\x9d\x92\xca1\x1c{\x0c\x0e\x878\x9d\ +\xf3q\x1c\x87\xc1\x91~FD\x9dy\xf96n\xfa\xc9\x1a^\x7f\xd12J5\xf0\x02\xf8\xdb\ +\xf7\x9c\xc5\x1b\xff\xfcK8Z\x10\n\x8f\xc4\xf1\x19\xacU!\x8e\xf1\xbb\xda\xa8\ +\x97\xab\xa0\x03hk\x87\xb0\xcc\xd8\xda\xa7\xc8\xbb\x15\x96\xf5\x16Y\xbct\x11\ +\x7f}\xcd\xd9\x04\xc0wnz\x94KO=\x948\x80{\x1f|\x0c\n\x05\xfa\xd6=\xc3l\'\xe1\ +-W\\@\x00\xb4w\xb8\x10\x97Mq\xa30\xc9\xc5\xff\xbf`\xefH\x17\x1a|\tz\x8cz\xad\ +D.7\x0b\xe9\xc0\x97\xaf}+w>\xb8\x9e\x13\x8e\\\xdc\xdcUc\xaaE\x1c\x8c\x1a\xdf\ +0\x1a\xf3\xe8\xa3\xeb\xb9\xe5\xd6\xfbX\xbba\x98\xbe\x81\x11\xf0\xda\x91\xb9\ +\xd9Tt\x8e\x98<\xf4\x16\x89\xcb5b7 7\xbb\x8b\xb1\xcdOS\t$_\xfa\xfa\x0fY}\xd8\ +{9|\xb9)0=a>\x9cv\xc8B~\xfa\xbb\x8d\xa4\xb9y\x88\xf6N\xa8h\xc8\x07\xd4\x85\ +\x03\xaa\x0eR\xd1i\x01i\r\xe9\xa5\x1c\x7f\xc4R^z\xc6I\x9cs\xc6\\\xb6\x94\xe0\ +\xc3\x7f\x7f#\xb5\x81\x8d\xbc\xf9\xa2C\xd9T\x02t\x0c\x952\x8b\xe7\x15\xb8\ +\xf6\x1do\xe1\xa8\xa56n\xac!-\x83cS\x1d\x19!\xd7\xd9\xb3\xcf\x1f\xfc\x8b\x89\ +\xbd\x94ta\xf4\xa1\xed\x92\x0f\xac,\xc6+\t\xeb1g\x1f\xb9\x98jb\xb2\x98\x1aS`\ +\x85\x11\xdc\xf1\xc0Fn\xfd\xc5\xafy\xf8\x89\xb5\x94"\x97\xaa\xee@8=\xa8Ys\ +\x88\x12L$\xca\xce\x83]0\xc1\x89B\x11$TGFi_\xb0\x98\x91\xe1\r\xb4[\x01_\xf9\ +\xf6\x1d\\\xfbWg\xb2\xc0\x85\x91\x11\xc5\xbb\xdex\x01\x0f<\xfeyv81\xb5$6\xd3\ +4tuC\xdf&\x9c\x9ev\xfcz\x85\xe15O\xb0pV\x1b\xaf~\xf5\xb9\xbc\xf2\xc2\xe5\xcc\ +i\x87_\xfcA\xf3\x91\x8f\xff#\x96\x17\xb0\xa0\xab\xc3\x14\x1d>;\xcc\xf6\xf5O0\ +\xe7\x80\xa5\xbc\xf1\xbc\x938\xfbp;SK\xb1q\x19\xc7)\xb9\xce.j\xf5\x90 \xd8\ +\xcd\\\x1eS\x14\xa6\x06\x7fr\x7f\xc0^\x92n\x83\xdd\x91\x1dl\xdc\xad\xd8\xe3\ +\x9b\xa8[\xce2\xdd\xe8\x1f\x1e\xde\xc2\xb7~\xf83\xee{r3e\xab\x8b\xe1\xd4\xa7\ +"\x0fF\xe5\n\x10V\xc0r\x8c\xcbV\xa6\x99w*\x02Q6\xe3\xdbJ\x05:\xda\xc1\xae0Z\ +\x1d\xa5\xa7\xbb\xc0\xf6-\xfd\xdc\xbf\xf6\x19\xfe\xfb\xce\x83\xb8\xec\xc8\ +\xb9\xcc\xee\x94\xd0\x06\xab\x0f\x9f\xc5\x9dkK\xd4\x12\t\x85\x1e(\xa7`\xe7\ +\xc8\x87\x15D\xe9Y\x8e[\xd5\xce\xdb\xdf>^\xd7\xda\x00\x00 \x00IDATt%/9\xb6@\ +\x7f\x15\xfe\xe9+O\xf0\xfd\x9bo\xc2o\xefd\xc3\xf6\x11\x0e=\xec\x08"`\xa0o\ +\x0b\xf9h\x07\x97\x9e|\x0e\x7f~\xd1\x12\xda\x00\xdb\xcf&\x1a\xc0\x03O\xa2\ +\x85\xc4\xdd\xcf\x08\x070s\xdcLN\xfa^\x8d2\x1b\x89\x90\t&Hf\xf2\xce\xc0R\xe6\ +e+(H\xf8\xd5m\xbf`\xfd\x9a\xcdD\x91\xc7\xb6AEd-@yK\x81^(\xce1N\x1a!\xc1s\xc1\ +\xb3L.t}\x04\xc2a\xc8\t,+\x04\x99\x80\x8a\xa9#H\x82\x02\xcf\x0e\x96\xb9\xe1\ +\xc7?\xc7-Hv\xf4W\xd1\n\xfe\xeeo.G\xd6w\xd0\x11\xc4\xb0c\x1d\xae\x88\tt\rF\ +\xb6\xf0\x96W\x9f\xcd\x97\xaf\xffSN?\xb6\xc0\xe3\x1b\xe0\xef?\xf3[\xbe\xf3\ +\xdfw\xa3\xbdN\x06\xcb\t\x9d\xf3\x96!\x83NB\xe0\xee_\xde\xc6\x15\xe7\x9d\xca\ +\x9b/:\x9ev\xc0V\x15PY\xa2\x88\xb4\xb3\x18\xf8x\x1a\xfa\xfe\x85h\xb7\xdf\xec\ +\xb5k!my\xa9\xe6h`\xbc\xc4G\xa5\xf0\xa1\x0f\\\xc9Y/;\x83|>\xc7\xac\xd9s\x88\ +\xb4\x00\xe1A52\x81\xf2j\x19\xc6F\xa1V\xc6K*8I\x05WU\x08D\x84\x1dUp\xc3\xd0x\ +\xf0\xf0\x08S\x1f\xa7m>8\xb3\xb8\xef\xe9~\xae\xff\xd6]\xf8sr\xd4S\xe8\xb4\ +\xe1\xdc\x13V\xd3\x9d\x0c\xd0]\xac\xe0\x0e=\xcci+;\xf8\x8fO\xbe\x9b\xb7\\v\ +\x14\xed\x1e\xdcy\x7f\x85\xbf\xf9\xc4\xd7\xf8\xc9\xef\x9e\xa0\xec\xcfB\xe5\ +\xbb)G\x92r"\xb0\x1c\x17\x12hs\x15o\xb9\xf4\\\x0e\xea\x06K\x95AU\xcd\x1df^\ +\xc8\x98\xf1.k\x7fC\x9a\xec\x03\xd2\'B\x81\x88A\x86F\x8d\xc8\x1a\x8eg\xb4\ +\xe2\x9f\xbe\xf5d^{\xd9\xb9xv\x85\xb6\xa0\x0e\xd5\xcd\xe0\xa7Y\x0c\xde\x04\ +\x96e\xb5\x8a,W\xc8%\x8a\x82\xb0\xc9i\x0b]OH\xea1\x8e\x95\x03+O\\V\xc4*\xc0n\ +[\x88,\xce\xe5\xcb?\xf8\x19\x0fo59\xf5I\n\xef\xbb\xfa\xe5\xe8\xadOrX\xaf\xe4\ +\xcas\x8f\xe6C\xd7\x9c\xc3A\xcb\xa1\xe0\xc2\x7f}\xe7I\xde\xf3\xfe\xeb\xd88\ +\x10\xa2\xf2\xdd\xd4\xb1\x19\t5^{\x0fq\xa9B{!\xc0Vp\xc5\xc5\xe7q\xd8\xb26\ +\x920+\xc3\x990\x9b\x83\xc1\xfe\'\xe1\x06\xd2\xde\xfd0s/\xfbt\x85\xd5\xc8\ +\xf1\xca\x1e\x8a6s\x13fQ#\xb3\x8fR\x82j\xd5\xe3\xaaK\x96\xd2\xb5\xa0\x83\x8f\ +~\xf6k`\x07DI;I\x1a\xe0{yl\xcb!\xa9\x8c\x81\x8aq\xa5\x8bR\x16\xb5z\x8a\xe7uP\ +\x8b\x04\xe0\x99. \xac@\xaaIr\x16Z\xe4\x19\xf5\xba\xf8\xc4\x7f\xdc\xc4\xbf|\ +\xf0"\xf2\xc0\xac\x02\xbc\xeb\xf2\xf3Yr\xf8*\x8e:v\x11!\xd0W\x85\xbf\xfc\xe8\ +\x0fx\xe0\xa1\xb5\x14{\x0fg0\xc18k\xc6J$qJ\xd0\xdd\x89\xd0\x8a\xde\xbcC^\xc3\ +\xc9G\xaf\xa2\x02hG\x80\xf4\x8c6B\x820\x81\x92?V\n\xf2\x0b\x81\x88\x80\xddY"\ +{%\xe9f\x94\xaa\xb2)\xed$f\x1eDS0\xa8\xb3W\n\xb8\x96Ew\xce\xc2\x03.8\xae\x8b\ +\xaf\\\xf7.\x0e\xed\x8d\xe8T\xdbP\xd5a\xa2Z\x9d8J\xd1\xc2\xc1rrh\xdb\'\xc6\ +\xa1\xa6$\xd2+\xa2\x95DG\xb1\x89\xca\x15r&e8\xae\x91\x86!i~\x16\xf7<\xbe\x9e\ +\x1f\xfed\xa3\xb1\'\xca\xf0\xee7\x9d\xcbK\x8f]D\n\xdc\xfd\x84\xe6m\x7f\xf5\r\ +\xee||\x0b\x03\xa2\x8d-\xa5\x10\xa7\xd8\x8e\xaaW!\x08 \xdf\xc6\xd8H\t_\xa4\ +\xe4\xa8\x1b\xdf\xbb\x0e\xa9D&6\x14\x11\x80\xcc\x9b\x94m\xad\x10\x848Y\x94l\ +\x7fDy\x0f\xf2\xbcw\xea]KH\x1dH\x02P\x1eB{h<R\x02"\xf2D\xe4\x19\xaeh\x14\x0e\ +\x1am\n\xf2\x14\x9c\xb8\xd0\xe6[\x9f|7\xa7\x1d2\x97\x05\x0b\xba\x08\x1d(\x87\ +e\xca\x16D\x81\xcb\x88\x80\x92\x05\xba\xad@YG\x10X\xe0& \xc6p\xbc\x1aA\xae\ +\x86\xef\x0c\x82]\'J}\xca\x91\xcbm\xb7\xdf\x8cH\xc0\xf7\xc6 \xae04\x04\xff\ +\xfe\xcd\x07\xb9\xfa\xff\xfc3\x9b\xc2Nj\x85y\xf8\x0b\x17S\\0\x8b\xd1\xbe\x8d\ +P)C\xa4p\x82"\x841\x81\x8cY\xb9\xb0\x0b\xa1b\xd01\x910\xe1\xd52P\xc73\x01$RP\ +!B\x87\xcf1{\xcb\xd4E\xf8\xdc99{\x81F\xdaN\xa6\xf3v.\x7fn\xcb\x17\x10H,e<qm\ +\xd2\x10\xef\xd7k\xfc\xf3\x87_\xcbY\xc7\x1e\xc0\xb2\xd9 t\x19\xd7J\xd0\x96m2\ +o\xd2\x04r\x0e$\x15\xc8K\xc8I\x88*\xc4\xe5\x01\xd2\xea\x00"\x19\xa2 +\xf8\ +\xf1\x18\xc7\x1f\xb8\x84O]\xfb\x0e|\x07T\x14\x83\xeb\xa2\x1d\xf8\xef\x9f\xde\ +F\xb1\xd0\xc5\xd8H\nv;\xb5\xb1\x1a\xa3\x83\xdb\xa1\xb3\x00\xf9\x0eP\x82\xb8<\ +L\xbe\xd3f\xe9\xbc6N=\xae\x83\xc6\xfc\xed9g\xdc\xd7f,u\xcbH{\x16&\xb5&\x95\ +\xf6F\x177n\xdcN\xb5n\xc0\xdf\xc3w{Ozc^\xbdl~L)\xcc(\xd0\xce^\x8d\xff\x1d\ +\xd9\x98%\xd5\x94\x9b\xcdj\x0f(\x02\xff\xf0\x96\xa3x\xc7Y\x0b9\xa2+\xc1\x1e\ +\xddN\\\xa9`\xb5\xb5CG\x01\xa2\x12\x144\xd4\x06\xa1V\xc1w\x1c\xe6\xb4u\xa1+U\ +\\"\xda\x19\xe5\x9a\xf3\x8e\xe4\xcb\x7f\xf9r\xe6\xfa0R\x85\x11\xa7\x8b\x92p\ +\xf0\x8b\xf0\'\x7fz\x19jp\x03\x8b|\x9bB%\x85\x8a\xc6\xebn\x83\xda\x18\xe4:\ +\x8d)^\xde\xcc\xc2\xb6\x903NZE\xff\x08\x84\xb6G"\\\n@\x1b&=\xcb\x06blR\xf2 r\ +\xe6\xaet\x8a\xd0I37\xa45\xd5\xc9d\xfa\x8f[\xf9S\t\xb9\xc6\x0cZ\x93`\xefHo\ +\xc9\xeal\xfc\xdf\xd8\xd4\xc8\x0c\x11{x\xb9(\xf2*\xe2\xed\x17\xac\xe6\x1f\ +\xfe\xeaj\xce8r)A<B:\xda\x07\xe1\x88\t\xd8[\xc2$_T\xabx\x96\xa4\x7f\xd3zf\ +\x05\x16\x0b\x8b\x16\xd7\xfd\xd5\xdb\xb9\xf2\xac#X\xdemB\xf7k\xfb\xe1\xbd\ +\x1f\xffO\x9e\x1e6\x93\x03\x9d}\xe2b.?\xf74\xca[\xd7\xe0*\x10\xd2&,Ua\xee\\\ +\x18+A{\x9e\xb6\x0e\x0f\xea\x83\xcc\xed\xca\xd3\xd1n\xc8\nS\x93\xff\xeek\xd3\ +\x7f7\x885s\xc3I\x9a\xd3\x7f\xb7d\xa34*l\x9a\x1f\x99z\x84\x03\xc8\xe7M\xfa\ +\xf3\x84\x00<iS\x1a\x1c\xe5\xb8\x95\x0e\x9f\xfb\xbb\x0bx\xfb\xabN\xa6S\x0cBy\ +\x00\xd7\xb1a\xb4\x0e\x91\xc0\x9b\xbb\x80\xd1\xd2(]m\x16G/\x9f\xcd-\xff\xfan\ +\xceY\xdd\xc9\xc2y0\x96\xc2-\xf7\x0e\xf1\x86\xbf\xba\x8e[\xef{\x86O|\xee\xbb\ +\x04@\xbd\x0eW^z*\x9d\xf9\x147\x9fe\x96\xe5\xbba\xa4\x0ev\x82\x15\x0e"\xa3*\ +\'\x9fx\x02C\x03\xfd\xe4\x84I\xf4\xb1\xa5\x95\xa5\x1c\xa7H\x93\xc3:i\xdd\x01\ +\xd9\x04\x84\xe3\x9fu#\xe7u\xf2\xfd\xa7\x00\xd4n\xbcq\xf0\x82\xd9(\x92$\x8e\ +\x99\xd3\xddNA@\x8f\x03\xef~\xfdJ\xaey\xc3\x05\xcc\xcb\xa7Dk\x1f\x07Kc\x17|\ +\xc2\xd2\x0e<\'\xe4\xca\xcb\xce\xe3s\xd7\xbe\nQ\x86<F\xfa\xfe\xed\xbb\x0f\ +\xf0\xa1\xeb\xbfB_\xdc\x86\xecY\xcem\xf7<\xce}\x8f\x85\x14}Xz\x00\\\xf4\x8a\ +\xd3)W\xb6\xe0\xb4\xdbf&\x84\xe1\n\xdd\xed\x01\xe9\xd8v\x1c\x12^{\xf9\t\xfc\ +\xfe\xfe\xfb\x01c\x93\xb8\x96d\x8f\xae\x97\x9d\x18U\x13\xfeSMM7\x15\r\xbd\ +\xf8E\']\x83my\x10kl\r\x81\x86 \x86\xab/\\\xc6g\xde\xff\x0eN\\\xb5\x88|\xb5\ +\x8f\xceh\x07\xf3sU\xae\xfb\xc0\xd5\xbc\xf5\xd5\x07\xa0\x80\xb6\x02\x94\xea\ +\xf0\xb5\x9b\x1f\xe7\xdfo\xba\x9d~U@t/f t\xf1\xba\x16\xf3w\xff\xf0\x15\xfa\ +\x86\xcdi.\xbf\xe2H\x82\xa0D\xc1\xab\xc2\x8e~D{\x0f\xb2V"\xa7\xab\x9c\xf9\ +\x92S\xb0$\x94F\x86M\x97#M\x86:il\xca\xae\xd0M\x1fDv\xc9\x19\xb2\x9c\xb3\xe6\ +G\x9d}\x99\xcdx\x91\xed\xa1\xd4\xd4R\xf2\xbb,\x1d\xd2\x82\x17\xac\x91F5S\xad\ +Y+\x8f\x10\x87%\xda\x1d#\xc1\xc7.\xb2\xf8\xd6\'\xaf\xe0\x1d\x17\x9c\xc8\x11\ +\x1d\x117\\\xff&.\\\r\x8d\xa5q\xaa\xc0\xbf}\xef>>\xf1\x95o\xb3\xa9*Y\xb0\xf2\ +(F\x9fXKj\xe7\x19\x8b]\xb6\x95|>\xfd\xa5\xbb(\x01\x9dyx\xfdk\xcf\xa6>\xf2\ +\x0c\x9e+\x08\xc2\x98x\xb8\x9f\xee\xb6\x1cW\xbd\xf6X\xbes\xc3}\x1c|\xe0\x8aq\ +\xe9\xd4\x8dt\xe4\xd6\x9a\xb1q\x8c\x13\xbf\xf3cjH\xban\xaaxg\x1f\x97\x1e=_\ +\xa4{\x18O\xbc`Wj;f\xad\x11\xbf\xd8\x86\xeb{\x94+C\xc8$fQ\x0e\xe6\xc6\xf0\ +\xfeKN\xe0\'\xd7_\xcd\x812\xa4\x07S\xc40\x9a\xc2\xb5\xffz?\x1f\xfe\xd2\x0f\ +\x18t\xe7"f/a\xfd3\x1bpV\x1e\x86\x9b/\x10)\x8f\x8a\xe8\xe5\xee\xc7w\xf0\xc3_\ +\x0c1\x00\xbc\xe1\xc2U\x1c\x7fp7\xf3\x83\x145\xd8\x87\'SN9\xe9D\xe6v\xc2\xbd\ +\xf7\xdc\xc3\xa2E\x8b\x80\xcc6\xd3\x1a3\'\xba!\xbd\xd1\xaf\xef\xf2\x88Z;m\ +\xdd\xfa\xae\xa6\xec8\xde\xdb\'\xe3\xf4\xe7\tag\x05\nqL\xa8"\x8a\xf9\x00\xdf\ +V\x888\xc5\x93\xd0\xe1\x87XI\x8d\xd9\xb9\x1a\x05j\x08\r\x1f\xbf\xeek|\xe9\ +\xff~\x87E\x87\x9fD\x12t\x10\xd6\x12h\xef$V\x9ah\xd36\n]s(\xa7\x01\x9bG\x14\ +\xd7\xff\xeb\xd7\x19(\x19\xed\xf0\xb6\xd7\xbf\x8a\xda\x8eMty\x82\xae\xc0\xe6\ +\x95\xe7\x1f\xcd\x86m`9\x81\x99p7+\xb85\xde7\x9aD\x8a]\x94z\x86\ts\xc6\x8b\t\ +U\xb9\x12\x85T\x8a8\x8e\xff\x88O\xef\x7f\x0e\x97\xdd_\xcf\x0bB\xba\x16\xd9,S\ +\x12\x1c\xc7\xc3\x95.\xb2\x11\xa1\x93\xcad\xaf\x10\x9by\xc7t\x8cV!?\xff\xe9m\ +<\xf9\xe4\xe3\xd8\xb6M\xa9T2\x95\xa0(c\xaa\xc7\t\xcc\xea\xa5\\\xae\x83W\x00\ +\xbf\x00^\x9e/~\xf9\xbf\x18Tp\xf4\x81\x1d\x9cu\xfa\xc9x\xaa\xc6\xb1\x87.\xe3\ +\xb0\xc5\xf0\xe0\x83\x83l\xeb\xebg\xfe\xc2\xc5(\xc0u \tC\xe3o\xdf\xc3rV{k\ +\x9a;\x93M\xab\xf6"\xa2^\x19\xd8\xedw/\x98\xa47\xc2\xb1\xa6\x0f43?5EL\nSK\ +\xec\x14@\x06h\xe9s\xc6\x19g\xf0\xbew\xff\x19\xaf8\xed(j}O\xa1\x867c\xcb\x14\ ++\xe7\x9b\xf1V\xaa!\xc8\xe3\xe5\x1dR\x15R.Wx\xe4\xd1\xa7\xf9\xf5o\x9e \x01\ +\xdez\xf5\xc9\xb4\xb7\xb9\xbc\xee\x92\x97\x91\xc6p\xef\xbd\xf7\xa2\x84G\xef\ +\xfcN,a\xe4\xd4\xf6\xf2 mT\xad\x96]\xa5\x98\\]O\x14\xfb\xf1\xe2\x06hv\x08SM\ +\xd2\x0b\xf6\xee\xa9}\xc1\xe6\x86m\\\x82\xdd\x9c\x16_e\xb5\xba\xa6\x0e$\x05\ +\x12\x0b\x12<"\xa0\xcd\x83\xb3\x8fY\xcc\xbc \xcf\xc1\xbd>\xdf\xbc\xedN\x94\ +\xdb\xce\xa6\xda\x08\xa8"t\xcc\x81\xb1\x12\xe1\xf0\x00\xb3{}\xca\xfduB\x0b\ +\xbe\xf8\xd5\x9bYu\xf8J\x96\xf5\xc0\x9b\xdf\xf6\x06\x8e]\x01}\xa3\xf0\xe0\ +\x03\x0fPS6\xdb\xb6\xa7\x1c4\xcf\xa2\xd0\x1c\x8eI\xa3\x8a\xb4\xdc;\xa9n\x96m\ +\x99\xdfj\x00\xadQj\x8aE\xdd\xe5T\xe8\xd3i\xf8\xb8uK\r\xafefi\xb0\xa0\xa4\ +\x8d\xa5\x9e\xa51\x00\xc6\x87\x7f\xca!\xb3\xf8\xdb?\xbb\x80\xf7]u!\xc1\xd8V\ +\x18\xde\x82\xd7\x93\xb9X\xc3*N\xb7O\xa5\xbc\x83 \xc8\x13%\x01\xa3\x95\x80\ +\xf7\x7f\xe4\x87\xc4\xc09g\xe7q\x81;o\xfb5ZX\x08\xb7\xc0m\xbf\xbc\x8b0\xcb\ +\xd6\xaaE\x1a\xb0\x90\xb9\xfcs<\x8a\x9d\t5\x8e\xe6\xd6A\x9e\xe7M\xad\x94\xaa\ +ZR\xdc\xedw/\x98G\xae\xa1\xd6\r\xe1.\x86R\xb7\x99\xa5b\xd9\xe3\x93\xeb[\x8c\ +\xe7\xca\x0b]\xc3\xd55\xae:\xffT\xde\xf3\xba\x8bX>\'G\xb8\xe1qS\xd1\xd8\xe5\ +\x12G;PVB\xa8l\x84=\x0b\xd7_\xcc\x93\x0f\xae\xe5\x86\x1f\x0f`\x03C\x031w\xdc\ +\xfe3\xa4\x9b\xa3\xd09\x8b\x07\x1f~\x92j\x9c\x15]$\x98\xeb\xc1\x9e\xa0\xb2\'\ +b\'\xc2\x9b\xd5\xa3V6-\xe9\xc4B\xc2\xa9\x02\xb9\x07;\xe5\x05\x92t\x13\x9f\ +\xceRj1\xe1\x19\x0f\x84\x89\xc3\x0b\x146f\xdc\x9e\xc7\xe4\xdc\xb9\xdaT\xa9"\ +\x14Im\x8c\x9cT\xbc\xf1\xa2\xd5\xfc\xc5U\xafd^\xb7\x84\x91\x8dP\x1d\x00\xaa\ +\x90\xf7\xa8\xc9\x02\xb1\xecbxX3{\xc12n\xbe\xe9\xbfP@W\x8f\xc3\x8aU\x873:V\ +\xa5\x16C\x7f\xa9\xc2\xc3\x8f\xd5I\x01\'\x10\x99\x05\x9fi\x9f=\\\x7f#\x88d\ +\xd0\x90\xf4,\xc26\x05I\xdfS\xb5\xcd\x0bF\xba\x91c\x93O\x97\xca,\xedI\x98\ +\xef\\\xaa\xe4T?yJ\xe4SE!\xc2,b\x0cD\xe4!\xd7\x03R\x12\x8d\xc2e\xa7\xcf\xe6\ +\xb3\x1f|\x0b\xf3;\x81\xda\x00]\x8b\xe7S+WP\xf9\xb9TF-\xda\xf3\xb3\xa9\xf4\ +\x0f\xb0j\xe9,\x06\x06+\xa4\xc0\x85\x97]\xca\x8aC\x8ff\xb8TCaq\xcb\xad?\xa5\ +\x9a\xad\xdf\x93*\xcd\x1e\xe7`\x9d\x8cKm\xdc2\x8d\xcc\x9a\xc8q\xa6\\\x9f>\ +\x05$}\x1c-3\xd3\xb4\xcc\xc3\xa2\xcc?*\x06\x1d\x9a\xb5D]c\xf1\x9b\xc9\xb8$\ +\x90P\xc8k\\\r\'/\x83\xaf~\xfc\r\x1c\xbf\xa2\x97\xa1{\xee\xa0\xd8\xde\x85.\ +\x83\xd5>\x8f(L\xb1-X\xbet6\x0b\xba\xf3<\xb5\xb5F\xef\\x\xcd\x15\xe7\xe0\xd9\ +)\xbeg\xf3\x9b\xdf\xdeK\x98\x18)\x1d\x1e\x8b\xd0\xc2Fc\xd3X\x19a\xbc;\xca\ +\xd2\xa1[\'\x01\xda\xa9\x814#\x8dS\xa8\x8e\r\xa6\x04\xe9\x12\x13\xd6w\xccR\ +\xd3\xc2\x14\x96\x9a\x95\xd0$\xc6\xff\xd6\x06\xa2\x90\xe5\xc6\x9b\x9f4\xf2\ +\xeb]b\xb0\xca\xc4\xaa\x1ft\x8d<p|\x0f|\xe8\x923\xb9\xfc\xd4\xe3\xb1\x9ey\ +\x96\x0e\x02\xa8*,K\x10tx\xf4,\xe8$\x02n\xfe\xd5]\xfc\xfc\xfe2g\x1f\x03\xc7\ +\x1d1\x9f\xa8\xda\x8f\xe3\n~\xf7\x87~\xb6\x8fA\xd0\xe1\xed\x12\x13\xb7\xb2Y\ +\xa4Q1\xa8\x08\xd24K\xa1j\xdc\x8b)\xe5\xce:)s\x1f{\x1a\xebO1\xbc\x80\xa47\ +\xd2-&\xc6\xda\xc7\xa9\xf5L\xba\xb4\xb0\'\xc4\xec\x9bk\x9a\xa6\x11\x9ek\x13H\ +IX\xab\x91\xd7p\xceQ]\xbc\xed\x15gr\xce\xd1\xab\x10\xa5\xedt\xe5$c\xa5m\xf4\ +\xf5\xad\xe5\xf8\xe3V\xa3\x81\xce\xde%\xfc\xed\xb5\x9f\xe2\xa15\xf0\xb67\x9d\ +\xcb\xbc9\xb3\xf0\x1c\x97\xdb\xee\xf8\x05\xf9\xa2\xe9pF\xab\xe3\xdag\xc2<``\ +\xaey\x17\xbf\xfa\xb8Fh\xcd\'\x98J\xa8V\xab\xbb\xfdn?i\x9e\x02c\xe2\x05\x80G\ +!\x08\x8c\xdd%\xe1\xa8\xa3\xe7\xf2\x17\x7f\xfeJ\x96-r\t\xc7\x9ebi\xaf\xcd\ +\xa2\xde\x02s-\x18N\x14k\xd7\x0e\x90\xaa\x02\xff\xfe\x1f71\x7f\x01\x9cw\xde\ +\xf9\x8c\x8c\x8e\xf1\xfb\xfb\x7f\x8f\x85I\xa7.\xe6\xc6U\xba\xc9\x8a\xc9\x0c<\ +\xe9\x19\xa7\x91t\xd8o\x1eU\x86=\x95a\xed\x1fw\xa2m @\x87\x13\x17\xbc\x1f\ +\xab&\xd8\x16\xac8\x00\xfe\xe6//\xe4\xc8\xe5yF\xb7>\xc8\x81s\x8b8\xc0,[22\ +\xa8h\xebX\xc4\x1f\x1ez\x86\xcf|\xfe>.\xb9h1\x8b\x96\x1c\x08\xc2\xe2\xde\xfb\ +\xfbQ\xe9xL\\dG7v\x84m\x82\xa7\x99\x0by\x8f\xc6\xfd\x14\xc4\x9e\xea\xd3\xf7\ +\x13\xd2\x81T"\xa4\x83\xc4\xcc.\x12+\xc8\xe7l\\\x11"I9\xe1`x\xff;_\xc1\xca\ +\xb9\x82\xa3W\xcc\xc6\xc5t&]]\x07\xb0~\xf3\x08nn\x16?\xbb\xf3>~z\x17|\xf4\ +\xef\xff\x84HI~\xff\xc0\xc3\xc6\xed\xae\x1bj:\xcdV;\xd2\x13VWl]\x95q\xbf\x81\ +~\xd1\r\xb9\xe7\t\x8dYN"{\xf2\x96\x05\xa96\xf2n\x11b\xa9Qr\xc01\xcb\xe1\xba\ +\xf7_\xcd\xc5g\x1c\x83E\xcah\x05vl\x1f\xa2\xd81\x87\x9av)v\xcf\xe7\xba\xeb\ +\xff\x85\x91\x1a\\\xf2\x9a\xab\xf8\xcd\xef\x1e\xa4Z3\x06\xfax\xaaD\xd2X\x05\ +\x06\xd87\xd9\xae:s\xd36^i\x9a6_\r\x84aH\x9a\xa6\xcd1\x7f\xeb\xfe\x90\x90\ +\x00\xa126H\xe3\x15)\xbdk\xad]\xe3\xc3\x1e\xfc\x06\xfb\x07\xe9`\xae4m1\t-A-2\ +\xab9\x14\xa5\xc4\'!\x1d+s\xfc\xc1s9b\xc9\x1c\xda\xd1\xcc\xc9\xc3\xe6-\x1b\ +\xf1\x82\x1ca"\x19\xa8(B+\xcf\xbb\xfe\xfa\xffr\xd6+VP\x8e-\xd6o\xa9\xb5<\x85\ +F\\ \xc6"\xdeg\xb1\xf2\xc6T_\r\x95\xdb\x98\x15J\x08\x01\xf51\x08\xcbx\x0eX"A\ +\xc4uD\x1a"E\xb6\x18\xb0NM\xe1\r\x1aK\xea\tF\xb0Nc\x928B\x00a\xbd\x8eNU\xebI\ +\xf7\xf8(\xa7>\xb2j\'\x04\xd4\xc2\x908\x1b\\\xe5\xdd<\x16.\xa4\x1a\x11W\xe8-\ +z\xa4\xe5\x11\x02+!\xae\x0fa\x03\xed\x05\xc1Xe\x00\xcb\xcf\x91\x96cd\xd0I\ +\xea\x16\xf9\xcc\xbf<\xc2\xd5\xef\xfa\x0b~\xfc\x8b{vJa6K\xe9\n\xa2\xac\xa2g\ +\xdfX\xe7\x96e\x91\xa6)q\x1c7\xbdwJ)3\x8d\xba#\xcc\xf00\x8e\xb24\xf3F\x94\ +\xc2FH\x07!m\xacl\xe6Y\xadb\xd2$D\xa2\xf0\x1c\x1b\xcf\xb1\x89\xe3\x10\xdf\ +\xf3\x9b\xc3F\x9d$\xa8=D\xfd\xf6\x0f\xd2\xc1<u[\xe1y`\xb5\xa4)\xa1< \x87#\ +\x03\xd0\xe0\x05\x01\xc4!\x05\xdfexx\x84\xa5\x0b\x8b\x946=\x8a\xd2\x11\x08\ +\x9bz\xac\xa9&\x92\x9bo\xbf\x8bu\xdb\xeb\x88\xa0\xab\xb9p}sA\xc0Z\x15#\xed!\ +\xd5\x91a\xf6\x85\xed\xae\xb5FJ\x89\xe7yMG\x8e\x94\rG\x8f4\xa3\x05\xc76A\x08\ +\x04\x08\xdbL\xa4,\xc6\xed\t\x81\xc6\x96\x02\xd7\xb6\x18_:[\xe18\x16f\x1dw\ +\xb3\xa7\xb0m\xe4\x1e\xe2\xfb\xfb\t\xe9\xd9\r\xc9\x18M\x8cl\xa8^\x8d\x99\xd1\ +By\xd9{f\x87\x0b\x01\xa4t\xb7\xbb\x1c\xb7z1\xe7_|\x06\x1d\x81\xce\x86\xdc\ +\x02ay\xb4\xcd\x9e\xc7\xa7\xbe\xf0\x15\x0e:j5\x8f\xae\x87\xaa\x86D\x99\xceC\ +\xd8\xd9\x84\xc7:\xa53\x1f`\xe9\xe7/\xe9\xad}5\x8c{\xccT\xa3`S\xca\xcc\x1f\ +\x90\x11\xcexN\xfd\xc4\xde\xf9\xf9W\xcb\xef\x1f\xa47:W\xa9\xc8\xca\x14\xc8b1\ +-\xa5\xa5\x8d\x87\xd6\xa8\xb5\x81\x82\xd4\x9c\xbc\xfa\x00\xde\xf6\xba3\x99\ +\x93O\xa1>\x80\xaf\xebT\x06w0\xdc?\xc4\xdc\xa5\x07q\xed\xf5_\xc3\xeb\xceb\ +\x01\xd2\xf8\xff\xb0=\xd3p\xe2\x08a\t\x8c\x1e\xd8w\xf6{\xeb\xd4\x9e\xe6\x9f\ +\xec\xda\x85\xd5$|\xe7\xb3\x89\tY\xf6\xcd\x072\xfe\xd2r\x8f\x16{+\xf6\x0f\ +\xd2\x1b\x84\n;+\x97\xce\x86V\xad\x8d\xde2\x81\x1c\xe3\xb4i\xe4\x86\xa4\xf4\ +\x16\xe0\xb8e\xf0\xe1w\\\xce\x11\x8b\xdah\x17U,\xea89\x9fg\xd7oa\xb8\xa6\xf9\ +\xc4\xe7\xef\xa0?1\xd3\xcdTb\xcbx\x06\xa5o\xe6\xb3\xddG\xd3PL0\xdev\xda\xa6\ +\x11\xe8\xac\xd3j%\xbc\xb5\xa8\xa8\xf9\x1cv\x8a\xf7M [0\xf1}7\xd8OH7H1+I\x99\ +|\xf3\x16CE@$\xcd\xacV\xa1\x04-\xc7\x97\xd2\xc8\xbc\xfa\x9c\xbd\x12>\xf6g\ +\xaffA>bqO\x0e\x9d\xd6\xb1\x1d\x17\xa7m\x167\xff\xecn\xbeq\xd3\xd3\x0c\xa4\ +\xa0\x1c\x87\x94\x00\x84\x8f\x92\xbe\xe92\xb4\xb5\xd7R\xb4;\x08!\xcc2\x9e-\ +\xc47\xc3\xb1\xdaj\xe6\xe26\x08oPkgS\xbdL\xdc\xda\xf2j\x11~\xcd\xc4\xf7\xdda\ +\xbf \xbd\xb5:\x14\x1aFL\x16\xaa\xcdn\xd8\xd8\xdc\xe3\xce\x140\x99\xae"I\xc9\ +\xa5\x90\x8b\xe1\xb4\x83\x1d\xfe\xe9\x83o\xa5\xdd\x89\xe9\xca\xfb\xd8\x9eK\ +\xdf\xb6~f/]\xc9Wo\xb8\x99\xfb\x1e\xad\x9a\x86\x83\x99\xf5^\xfaEcX\xa9\xe7\ +\xdb\xa3\xef\xe1\xde\xb2\xbe\xbeU\xa14\x15y\xab\x86\xd1\xbbyg\xa22\x9a\xe4\ +\xeb]\xb0_\x90\x0e\xe3s\xbf\x98\xca\r\x05\x8d\tDE\x0cr<Z\xdfxAf\xd8i\x01i\ +\x8a\xafJ\xb4i\xc5\x11\x0b\xe1\xf3\xff\xf8&:\n\x0eI\x12\x83\x9fg\xa8\x1c\xd1\ +7Z\xe7\xc6[~\xc6\x13[\xcc\xb1\xea\xdat\x17\xc0\xf3\x9e7pgg\x8bRj\xd7~\x9d\ +\x86:\xcff\xfb\xd0\x89y\xa9l\xd5jhf\x9aiZ\xde[\xce\xa3vz\xed\x0e\xfb\r\xe9\ +\x12\xd3\xf2-\x1c,|\x9a)N\x02\x10\xaa9\x0f\xbb\x85I\x96U\xb1\xa2\x99\xf7)LQC\ +82@\x0e\x98\x9b\x83\x1b\xbex)\x07\xf6X\x14\x9d\x04[Z\xcc>\xe0`~t\xfb\xef\xf8\ +\xd1\xdd\x8f3\x02\xd4%D\x8d\xfc\xadf\x9e\xfb$\x8fu_\xf5\xf7{\xdca\xef\x8dH\ +\xb9\xd3\xfb\x9e\xf6\x99\xd2\x10\x98\xb5tl\x01\x0e\x0e\x16\x05 \xc8\xa2`f\ +\x98\xe6Ac\xab\x99\xb9\xd9\xf5Ll\xde\x96\xd9\xd87\x8f\xd76\x1b;IY\xe0\xc2\ +\x81\x12\xbe\xf6\xb7\x97q\xc9\x89+\xa8o\xde\xc8\x8e\x11\x8d\xbb\xe4\x04>\xfd\ +\xed\x9fs\xe3\x03\xb1I\xd0\x14\x1a\xc6v@2\x04\xba\x86\xd2!Ze\xae\x1c\x9d\x12\ +Gu\xb3@`\xc3\xa8\xcc\xd0\xe8\x8e\x1a.W\x98h\xc8M\xf6\x1a\x0f\xfa4\xfa\xeal\ +\x18\'\xadl8G3\xdc\xbc\xbb\x97\xdc\xe9}w\xd8/HG\x98\x11\x94\x91d\x89h\xa4.\ +\x88\xc6\xd2\x9eF\xa6\x1b\x8b\xceg\xae\x8f\xf1,\x17\x01\xb5\x84\xcc\xf6\xd1P\ +\x19&\x9f&\x1c\xde\x0b\xaf;\xef\x18\xde\xf0\xda\x97C8J\x14\'\xf8\xdd\x0b\xf9\ +\xf4\x17\xff\x93G\xb7\x82\xb0\x05\xe4\xf3P\xab\x80\x00)\xe4x\xb2\x84\x108\ +\xb2\xb1\xd2d\xb6m\xe7~6S\xe1\xad+5\xeci\xc5\x86\x89[[\xc8\xdfK\x9a\xf6\xd2x\ +\xdfOH\x7f\x9e\xd0\xc2\xcc[\xa8%\xa0ll\xa7\x13\xb4\x8d\xa3\xe1\xa8e\xf0\xce7\ +\x1e\xc6+_v \x0c?\x85]\x1dcd{\x89/|\xf9G\x8c\x01\xa1\x93G\x15g\xa3\x85G\xd2\ +\xacv\xcb\xe4\xd2r\xd0\xc2BO\xa2}\x05\xec\x91\xe0\x17\x13\xd3\x82t\x03E=\xa9\ +\x93&Y>\x9e\x06B\x85\xa7`\xb1\x0f\x7f\xf1\xfa\x97\xf0\xea3\x0e\xc5\xad\xf6\ +\xd3\x95\xcb\xf1\xe0Ck\xf8\xee\x1dCl\x03F,\x8f\n6\x89\xb2\x88\x12\x88\xb54\ +\x8e`\x01\xd2\xb6v\x89m4U\xecsH\xf6\x8b\x85iB\xba"%\xc6\xb1m,O\x1a\xc2\xe3\ +\x10\xcbI)\xc8\nAR\xe6\xb0N\xf8\xec{\xcf\xe3\xb4C\x17\x10\x8f\xf4\xd1\xd3;\ +\x87\xcf~\xf5F\x1e\x19\x82\x1dd\x0b\x13H\t\x96eV\xf2nt\xe3\xcdj\x97\x0c\xcdb\ +\xc8\x19\xd2_tX\x8dq\x97P`\xc5&jG\x08:&oC\x1b\x8a\x9c\x86/~\xec2.8c5\x03\xdb\ +\xd7SKS>\xfc\x0f7Sa\xbc\x10\xa3\x91I\xd5(\xa7\x9b4\xa3f\x1fy\xf1\xfeX\x98&\ +\xa4K\x046\xf50\xa4R\x1f\x05\x19\x82\x15\x9a\xc9\x7fc\x01\xa9G:Z\xa6M\x80\ +\x1d\xc2?\xbe\xefB\xce=\xf3p\xfa\xb7\xaf\xa7ZQ|\xeb\x07\xfd\x8cE\xe3\x1exM\ +\x96\x9e\xfd\\\xe4NQ\xe2\xa7\x05\xe9\x02H\xa2\x84\xbc\x17\xe0\xfb.!\x11\x91\ +\x8a\xc1\xf1\xc1\xc9\x93\x94\x13\xdc|\x1b\xd5\xe1Q\xba=\x93\xac}\xcd\x9f\x9e\ +\xcfU\x97\x9cM\xdf\xba\xa7\xf8\xc1\r\xdfah\x08\xc6j\xe3\xf9yM\xe2\x9f+\x7fn\ +\n\x12?-H\x07\xf0\\\x89@e\x91x\x0f%\x0b\x84\x04\xa4\xc2\xc6.\x06\xe8\x04\xf2\ +m\xed\x08B\x02\x14s\x1cx\xcf\x1b\xce\xe2\xeaW\xbe\x94\xea\x96g\xf8\xfb\xbf\ +\xbb\xaeIx\x98dCg\xb1\xc7\xac\xa4qL1\xe2\xa7\t\xe9\r_\xbd\xa9\xa5\xd3\x04\ +\xa4\x98!X\x8c\t\xd6\xa4\r\xef\x88\x12\xc84&\x07,*\xc0\xe5/;\x92\xf7\xbd\xf3\ +r\xb6\xac\x7f\x86o|\xf3\x87\xa4\xda\xe4:TjF\x83( \x8e\xf7A\xc0\xfd\x05\xc4\ +\x0bV\x9f\xfe\xe2"\xc5\x84Q$\x82B\x93\x9f\x863\xd5\xc2\xccLavu\xd1Q\x8a\xed\ +\x19g\xdeQ\x8b-\x96\xf5\x9e@2\xb6\x99\x9b\x7f|3\x87\x1dr\x10/9q%A\x00c\xd5\ +\x94\xf6\x9cE\xb5^\xc5u\xf2\x13\xdd\xa5\xcf3*\xf7\xc7\xc44!}\x1c\x96\x1e\x0f\ +X\x08\xc65\xafB\x91"\xb1$H\xcb2qZKa9u\xba\x02\x87\xab\xdf|)\x96\xedr\xe3\x8d\ +7\xb0\xfa\x88\x8f\xd0\x96\x83|\xce\x0c\xdf\xf2\xc5|\x16\xe7\xdf\t\xad\'\x98B\ +\x98\xba\xcdq\x9f\xa2Q\x80db\xec\xb66\x93\x15{\xda\x8c\xdcl\x14)\xa9\x19{\ +\x9bl\xa5,\x11Sd\xbe\xf5\x08O\xc2[\xde\xf8\n\x0e=t\x15\xd7_\xffO\x84Y\x0e\ +\xa3\xd2\x8d\xf8\x9f\xca\xca$\xc6O9\x05\xf9\x06\xa6\r\xe9\xa6B\x86\xc6j\xc6:\ +\x1b\xaek\xb0\xb5\xc2\xc5\x04rD\x16\x97O\x9b\x19\x0c\xc2,C\xa5m\xea\xa1i:oy\ +\xd3et\xb4\xb7\xf1\xc8#\x0fQ\xadG8V\xebr\x00\xc6\xd4\x9b\x10\xdf\x9e\x82\xd5\ +1\xd3\x83t\r\xba\x99V\x94`*\'J\xa0\xca\xa6"5\x95\xd8\n\xb3\xc4\xbb0C\xf7\xa4\ +\xe1bW9H=|\x0f\x12m\xfa\xf9w\xbf\xf3\xadl\xd8\xb0\x0e\xcb\x16\x94\xab\xa5\ +\x9dO\xd5Dk\x10v*I\xfd\xf4 \x1d&\x96\x80\x88\x14D\xe6j\xc9\xc4R%\xe3\x99*\ +\xa6+\xce\xe6\\\x94\xa2\x19\xd2lD\xf0\xd2\x04.\xbf\xe4b6\xad_G1W@L)J\x9f\x1b\ +\xd3\xc3\x90\x13-#*aaT}\xf6Yg\x81\xd8,\xc9f\\\n\xb2\x19\xa2\xb3\xe8\x89\x8b\ +\xd1\xf6`\x96\x10\x95\xc0\xca\xe5+\x1a\x87\xdf\t\xb2\xe5/\xbb\xd9\xe7\xc5\ +\xc3\xf4 \x1dZ\x9ezKB\xe1\x84\xed\xbb!F\x8c\xbf\xfdOgq\x99JD\xb7b\xfa\xa8\ +\xf7\x1941C\xfa4\xc4\x0c\xe9\xd3\x103\xa4OC\xcc\x90>\r1C\xfa4\xc4\x0c\xe9\ +\xd3\x103\xa4OC\xcc\x90>\r1C\xfa4\xc4\x0c\xe9\xd3\x103\xa4OC\xcc\x90>\r1C\ +\xfa4\xc4\x0c\xe9\xd3\x103\xa4OC\xcc\x90>\r1EH\xdf\xbfr\xcc\xf6wL\t\xd2\xe34\ +d\x86\xf8\x17\x0eS\x82\xf4hJ\xae\x82\xf2\xff/\xa6\x04\xe9\xbe\x9c\x91\xf2\ +\x17\x12S\x82t-\xfe\xb7\xb3\xf3M\xd5)\x1f\xa6\xe25\x8dcJ\xa4@\xdb\x93\xb5=\ +\xadI\x84\xc2V\x16J*\xd0\x12\x89&\x15\x02\x89"\xd5\x1a+M\x88\xa5\x8d\x14)\ +\xb60\xab\xbc\xbdX\xd0\xa9"\x15\x02\x91\xc6\xc4\x96\xc6\x93\xde\xa4W\xb3~\ +\xfdz\x1c\xc7a\xce\x9c9\x13\xb67\xe6\x8d}!0%$}2\xb2T\xa3\xd2@\x98\xd9^\x04)\ +\x1a\xcd\xffk\xef\xdc\x83\xa3\xaa\xee\x00\xfc\x9d{\xf6\xbd!\xbby@\x1e\xc4@B\ +\x91G\x84\x12\x9fT\xd1RPl3\xad\xb4\x96\xf1Q\x18t\xda\xd2Z\xb1\xd3\x8aN\xad:\ +\xe3L\x9d\xda\x87Z-\xed\xd4>\xa6\xd5"\x91\x82\x03\x96\xfa\xc0X\xa7(T#\x85\ +\x82$D\xb0\x89\x08I\x0c\xe4\x9d\x05\x93\xecn\xf6\xde{\xfa\xc7n\x96DQ\xab\xd3\ +Ml\xef\xf9f2\x9b=\xd9\xdc\xf3\xf8rN\xee9;\xfb\xfb\xc9\xd4g\xc5\x84er\xef\xd6\ +F\xae\xb9\xf7y\xdel\xef?\xed5\xc6\n\x05\xbc\xde;\xc0\xf7\xff\xb0\x93\x1d\x07\ +;pY\xa7\x17\x0eP]]MMM\r\'N\x9c\xe0\xd1G\x1f\xa5\xb1\xb1\x91G\x1ey\x84\xba\ +\xba\xba1k\xef\xc7D\xfa;IJN\xcem\x93\x96\x9e(\x1bw\x1de\xd3\xee\xa3\xe9\xb4\ +\x1b]\t\x17?\xdc\xf2\x1a[_\xf3\xf0\xa7\x1dM\xe3\xbe\xa0>\xb3\xfd ?\xdf.\xb8\ +\xfb\x99\x06\x94|\xff\xdd\x88\x10\x82p8Lqq1S\xa7N%\x1a\x8d\xd2\xd1\xd11fm\ +\xcd\xe8\xf2>\x94\xb0pK\x85e\x08\xa4J~TT\x90\n\xa5-\xe0T<\xae\x91\x03\x94\ +\x9c#B\xd9\x08\x1b,\xe1\xe2\x9fotp\xdd/^%\xe8J\xb0\xf4\xfc2\xfcJ\x91\xebMp\ +\xe3\xe5\xa5\xec>\x1c\xe1\xf3\x17\xccyG\xcd\n\xc5\xc8x\xab#\xeb\x19\xf98\xfc\ +\xddp\xfd\x1fm\xb5\x10(\x96]:\x8b!\xeb0\x8b\xcf\x9d\x82aI\xde/i\xfd0\x15\x15\ +\x15H)Y\xb6l\xd9\xa8\xccM\x99&\xa3\xd2=n\x89\xb2m\xa4\x82\x98ms\xe8\xf8 GZ"\ +\xc4\xf0p\xd6d?3J\xfc\xf8\x94\x1b%-\x94\xad\x88\x9a\x06\xbb\xdf\xec\xa3\xad;\ +BQ\xb6\x97\xca\x99\xc5\xf4\xf4D8\xdc\x97\xc06\x14\t\x95M}k\x07><TL\xf6s\xd5\ +\xa2O\xb0t!L\xce\xf5s\xe8X7\xfdC\x10\xf6IJ\'\xfa\x91\xc2\x83\xcbV\xd4\xb7\ +\xbf\x8d\x95\x18\xa2(7\x8b\xc2\xa0 nx9~r\x80\xba\xd7\xfbPq\x93Y3\x0b(\x0f\ +\xbbp{\rL\xdcH;\x81\xc2`\xa0\x7f\x80H$Bnn.>\x9f\x0f\xc30\xd2\xe1>G\x86\xff\ +\xec\xee\xee&\x18\x0cR\x9a\x93\xc5\xf7\xae\x9e\x9b\\:\x85\xc0\xb6m\x84\x10$\ +\x12\t:::\x08\x04\x83\x84\xc2\xa1d\xfax\x92\xf9\\JJJ\x00(**\xca\xa4\x86w\x91\ +\xf1\x1b9\x05\xf4\x0f\xc6\xf9\xea\xda\xbfSS\xd7\x8de\xc0\x90p\x13\x182\xf8\ +\xe6US\xb8\xe7\xaasp+\x83W\x8e\xf6p\xd3\xaf^\xa6\xfe\x98\x00\x97\xc4P6\xb3\n\ +\x0f\x92\xe3\x1f\xa2\xf6\x88\x0b\x0bA,\xa1\xb8\xe8\xce\xed\x84d\x90\xa3\x0f/\ +e\xf9\x8f\x9f\xa2\xa5g\x90_\x7f\xe7\x12ZZ\x8e\xf1\xe3\'\xdb(\tK\xf6\xfe\xe2J\ +\xbcF\x82\x9e\x98\xe4\xf2;\x9f\xa6\'\xe1\xe1\xe9\xdb?E\xce\xec"\xee\xdf\xba\ +\x97\x9f<\xf9&\x03\xa6@*APD\xf9\xc1\xf2O\xf2\xed\xcf\xcd\xc1\xad\x12\xf4\x0f\ +Fyx\xddz\xfeQ[\x0b@ \x10\xe0\x8a+\xae`\xe3\xc6\x8d\xdcr\xcb-TVV\xb2v\xed\xda\ +t\x1e\xb5\x83\x07\x0f\xb2r\xe5J\x16/^\xccu\xd7]\xc7\xea\xd5\xab\x99?\x7f>\ +\x00555l\xde\xbc\x99h4\x8a\x90\x06\xf3\xe7\xcf\'\x1e\x8f\x8f{\xae\xf5\x8cK7\ +\x0c\x83`\xd0O\xc1\x14\x1f_\xcc\x9d\xcc\xf2\x85g\xd1u\xbc\x9b\x1b\xffX\xc7\ +\xef6\xb7q\xd3\xc2\x99d\x07\x13\\\xff\xc0v\x0e\xf7e1=4\xc8\xf9\xb3\xf2\xd9\ +\xd3\xd0\xca\xea+?M\x7fg;\x86g\x80\x17\x1bM|\x86\xc9\x17..\xa7\xc0\xb6\xf0\t\ +E\xdc\x08\x11w\x05\x10\x18\\\xb3\xe4l~\xba\xad\x87\xa3\'\xa0\xb6\xfe-.\x9dW\ +\xc2\xb6\xbdo\xd0\x13\xf70\xbb\xc4\xcf\xe2Y\x93x\xf8\xa5f\xee\xde|\x84)9n~p\ +\xd5\\ZNJ~\xb4\xe9\x00\xb7\xafo\xe2\xe2\x8aR\xce)\xf4\xf2\xf0\xba\xc7\xa8\ +\xab\xdb\xcf\r7\xdc\xc0\xec\xd9\xb3iiia\xdd\xbau\xa3\xfa$\x84\xe0\xc0\x81\ +\x03,X\xb0\x805k\xd6P\\\\\x9c\xce\xc04\x1c!r\xd7\xae]<\xf6\xd8c,Y\xb2\x84\ +\xcb.\xbb\x8ch<\xc6\x96-[8r\xe4\x08eee\x99\x1e\xf6\xf7e\x0c\xb6l\n\x03\xc1=_\ +\xba\x88\xed\rm\x1c<t\x8c\xde\x84"/\xec\xe7\xf8\x90\xc1\x81\xd6n\xbaO(\x8ev{\ +\xc8\t\x98<{O\x15\x85a7\x83q\x93l\xaf@\x1a\x85\x94\x9c\xd1\xceK\x87^\xc2#\ +\x03\xfc~\xd5\xd9\xf8p\'\xc3v*\x13C\xc5\x10X\x94\xe5x\xf9\xec\xd9a\xb6\xee\ +\xe9e\xfd\xae6\x16W\x96\xf0\x97\xdd\xc7\xb1\xe4\x04\xae\xbfx\n\t\xe9\xa6\xfa\ +o\x87\xb0e\x16\xe7\x9d\x99C\xdc\x90\x14g+\xca\x8b\x0c\xea\x8f\xb9\xa9\xd9\ +\xd7\xcc\x8c\x85S\xa9\xdd\xb3\x8f\xab\xaf\xacb\xc1\x82\x8bP\n\xc2\xe10+V\xac\ +\xe0\xc1\x07\x1f\x1c\xd5\xab\xe2\xe2bV\xadZ\x85\x94\xf2\xb43\xf7\x85\x17^\ +\xa0\xbc\xbc\x9c\xe5\xcb\x97\'\x0b\x84\xe0\xa6\xd5\xab\xf9\xee\xcd7\xff\x7f\ +\xcf\xf4!\xc0m\xc7\xd9\x7f,\xc65\xf7\xee\xe0H\'\xb8U?\x81,\xc9@\xdc\x83\xe9\ +\xf6\x10\x8b\xda\xb4\xf7F\xb1\x85\x8b\x8aBIA\xbe\x17\xaf2\xf1\xf9$\t\xe1B*\ +\x0b\x9f\xa5\x007\xb64\x91J \xb0\x91\x02\xd4p\\8\xe5A!\xf8\xda\xc2)<\xb5\xbb\ +\x87\xa7_\xe9\xe4_\xcbb\xbcX\xdfF\x96p\xb3\xec\x92R\xb0-\x8e\xf5EQd\xb1\xb5\ +\xb6\x93-\xbbZ\x91\xb6A\xc2m\xe25\xfd\x9c8\x99\xa0\xab\xfb8\xb6m2m\xea\x0c\ +\x840Rq\xe2\x14\xd3\xa6M{W\xdfJJJ\x90R\x9eJ\xbe\xf3\x0e\x91]]]\xcc\x9b7/\xbd\ +\x02\x00\xf8\xbc>\x8a\n\n1\xc6\xf9\xc89\xa3\xd2]\x98\xd8\xb8\xf9\xe5\xb6\xfd\ +4\xf5x\xa9\xaa\x94\xfc\xe6[K\x08\x05}|\xee\xee\xed\xbc\xf2F\x84!\xe1\xa6p\ +\x92\x1b0y\xbd\xb5\x9f\xe6\x9e(\xd3s<Db6\xd9~\x13\xd4\x10\x83R"\x94\xc44\x87\ +\x18\xb4%R\xa6\x02\xfa\x8c\xfa\xec\xb8\xc9EsK\x99^P\xc7\xbf\x8e\x0b\xbe\xf1\ +\xd0+D\xd4\x04\xbe|n\x1e\x85\xd9~lC2=\xdb\xc5\xe1>\x0f\xd7.\x9e\xc07/=\x13\ +\x054\xf7\xbd\xcd\xe4\xf0\x04\xf2C\x01\xf2I\xe0\xb6\\tt\xb6\x02\xb3\xd2\x15\ +\xb4\xb5\xb5}\xe8\xbeggg\xd3\xde\xde\x9e~>\x9c\xd2\xa3\xa7\xa7\'}\x037^d|\ +\x9fn+\x85e&\x90v\x82\x81\xb8\xcd\xfe\xd6\x08\x0f=\xf9\x1a{\x9b\xdeF\xe1A\n\ +\x9b/\x9d\x13bj\xbe\x8bn\x95\xc7\xa5w>\xcb\xb2\x07j\xa9\xbc\xf9\x19~\xbb\xa3\ +\x19Sx\xc8\x91&\xca\x10\x0c\xaa +\xee\xdb\xcew\x7f\xb7sD\n\xdcTG\x0cI\x96\ +\x01+?3\x15\xa4E\xed\x1bCH\xcbdY\x1f\x03%\x00\x00\x04\xd0IDAT\xe5%e\xb8\x84\ +\x8d[%\xb8\xfe\xb3sq\xa9A\x9e\xd8\xd5\xc6\xb6=\xcd<U\xd7\xce\xad\xbf\xd9\xcb\ +\xdam\r\x14\x04\r\xb2\xc2!\xce={.[\x9f\xf83\xfb\xf6\xbdJ4\x1a\xa5\xa9\xa9\ +\x89\xea\xea\xeaw\xf5\xeb\x83N\xcf.\xbc\xf0B\x1a\x1a\x1ax\xee\xb9\xe70M\x93X\ +,\xc6\xfa\xf5\xeb\xe9\xed\xed\x1d\xf7\xa8\xd0\x19\x9d\xe9&.\xa4=\xc0\x8d\x97\ +\xcfb\xfb\xfe\x1d\xbc|\xd0`\xf7k;)\x9c\xe4ez\xa1\x97\xa6\xb7\xfa\xb0\x84 \ +\x14\xf0\xf0\xc4\xad\xe7\xb3\xea\xa1W\xd9\xff\x96\xcd\xb6\xbd\'q)\xc9\xa6\ +\x9azV\\0\x95\x0b+\xce\xe0\x82i\r\xec9<\xc0_\xeb\x13\x84}p\xff7@*\x13\xc9\ +\x10B\xd9\x98H\\\xca\xe4\xdaE\xb3\xb8\xf7\x89\x06N\x0c)\xce\xcc7X4\xa7\x08\ +\x13\x85\xa1L\xbe\xb0\xa0\x9c\xfb#\x11\xee~\xbc\x99\x1f\xfe\xe5\x18R\xc4\xf0\ +\xd9\x01\xcas\xb3\xf0\xba<\xc4%|\xed\xeb_\xe1\x0f\xbf\xdf\xc0\xcf~\xf6\x00B\ +\x80\xcb\xe5b\xe9\xd2\xa5\xb4\xb6\xb6~\xa8\xbe/Z\xb4\x88\xf6\xf6v\xaa\xab\ +\xab\xd9\xb0a\x03J)\xca\xcb\xcb)--\xcd\xd0h\xff\xe7\x88\xfb\xee\xbbO\xed\xdb\ +\xb7\x8f\r\x1b6d\xa8\x8a\xe4AIg\x7f\x94\x7f4v"%\xcc\x9f9\x99\x13o\xc7\xb0\ +\x12\t&\x86\xfc\x84\xfc\x12\x81"\x8a\x87\xfa\xc3]\xb4uE\x99\x98\xebb\xde\'\ +\x8a\xc8\x92\n0\x184m\xfe\xd9\xf4\x16]\'\xe3\x14\xe5eq^y!\xcd]\'1M\x8b\xe2\ +\x9c \xa1\x80\x07\x10\xd86\x1c\xef:I\x9fm3\xc1\xe7\xe1\x8c\x9c@*\xdbYre\xb0\ +\x95\xa2\xaf?N\xed\x9b\x9d\x08s\x889\xe5\xc5\x14\x85\xfdxD\xf2\x88F\xd9\x16\ +\xb6\xad\xe8\xeb\x8b\x10\x89\xf4QPP@,\x16c\xcd\x9a5\xdcv\xdbmTTT\x10\x89D\ +\x10B\x10\n\x85\xd2\xe93-\xcb\xa2\xb3\xb3\x93\xec\xecl\x02\x81@z6wvv\xd2\xda\ +\xdaJ0\x18d\xfa\xf4\xe9\xf4\xf5\xf5!\xa5$\'\'\'C\xe3\xfd\xde\x18\xd2%l\xcbTc\ +p\xf7\x9e<\x19\x9b\x18p\xf1\xf9O\x16\xa7\x8a\x14\xd9yA\x0c\x14\x86\x12\x88T\ +\x1e\x14\xbf\xa18\xaf<\x8bs\xa7\x05\xb0U \x95t/9\xa8\x01\x15\xe7\xe2\x99%\ +\xd8*\xf9\xff\x1baQ61\x94\xec\xcc\x88\xc4|B(\x8a&N\xa00u\xf0f\xa4\xda\x90F)r\ +\x82^\xaa\xe6\x94\x80R\x18\xc2D\xa88\x08/\x02\xc1\xbeW\xf7STTDaa!\xf9\xf9y(\ +\xa5x\xfe\xf9\xe7\xf1x<\x94\x95\x95\xa5\x8fP\x93u\x9d\xba\xaea\x18\x14\x14\ +\x14\xbck\xe9\xce\xcf\xcf\'???\xfd\xfa\xbc\xbc\xbc\xff\xe6\xe0~$\xc6\xec]6e\ +\xb8\x91*\x991\xcd\x12.\xdc\xe9S\xf4d\xf8\xa6\x84\x91\xcc\xadh\x08\x1f\x86\r\ +R(\x10F\xfa8U\xb9\xfd(\x92\xe5"\x15\xf2\'\x95\x03\x99tbZ\x14B$\xbfP*\x19\xf1\ +\x11c\xd4\x91\xec\xf0\xccD\x812\x04\n\x0f\x16"\x99\xed\xcd\xb6\xd9\xb4i\x13\ +\xbd\xbd\xbdTVV\x92\x97\x97Gss3\r\r\r\xac\\\xb9\x12\xbf\xdf\xff\x9e\x99\x1a\ +\xde\xab<]\xdf\xc7\x881\x93.\x95\x8d\x9dz\xdf\\*\x1b\x84\x1c\x95\x1e\xd2\x95\ +\n\xa0;\x1c\xcbMa`\xa5~,\xd5p\x16\xd2\xe1?\x04\x99\x0e\xe4\x0b\xc9\xcc\xab\ +\xa76F\xc9P}\xb6p3|\xf9\xf7\xbam\x1a.\x97\xa9\xe5\xdf0\x0c\xee\xba\xeb.v\xee\ +\xdc\xc9\xa1C\x87hlld\xd2\xa4I\xdcq\xc7\x1d\xcc\x981c\xdco\xc0\xfe[dT\xba\ +\xb2-\x84!1\x01\x97\x10\xa7\xf6\xa7\xa3\x1fF?I\r\xac\x18\xd9\xb8\xf4`\'\x15\ +\xa6\xc3{\xa5\x8aO\t\x1f\xfe-\xf5\xc1\xdb\x92\x11\xf5\x8c$++\x8b\xaa\xaa*\ +\xaa\xaa\xaa>\xe8\n\xff\xb3dt\xed\xb1\x85L\x07\xe0\x1d=\xbc\x1fe\xc6\xbc\xdf\ +\x9c=\xddk?\xec\xb5\x9dCF\xa5\'l\xc5\xe8,\xb1\x9a\x8f\x03\x19]\xde}\xf2t\xeb\ +\xb8f\xbc\xf9\xf8\xddZj2\x8e\x96\xee@\xb4t\x07\xa2\xa5;\x10-\xdd\x81h\xe9\ +\x0eDKw Z\xba\x03\xd1\xd2\x1d\x88\x96\xee@\xb4t\x07\xa2\xa5;\x10-\xdd\x81h\ +\xe9\x0eDKw Z\xba\x03\xd1\xd2\x1d\x88\x96\xee@\xb4t\x07\xa2\xa5;\x10-\xdd\ +\x81h\xe9\x0eDKw Z\xba\x03\xd1\xd2\x1d\x88\x96\xee@\xb4t\x07\xa2\xa5;\x10-\ +\xdd\x81h\xe9\x0eDKw Z\xba\x03\xd1\xd2\x1d\x88\x96\xee@\xb4t\x07\xa2\xa5;\ +\x10-\xdd\x81h\xe9\x0eDKw Z\xba\x03\xd1\xd2\x1d\x88\x96\xee@\xb4t\x07\xa2\ +\xa5;\x10-\xdd\x81h\xe9\x0eDKw Z\xba\x03\xd1\xd2\x1d\x88\x96\xee@\xb4t\x07\ +\xa2\xa5;\x10-\xdd\x81h\xe9\x0eDKw Z\xba\x03\xd1\xd2\x1d\x88\x96\xee@\xb4t\ +\x07\xa2\xa5;\x10-\xdd\x81h\xe9\x0eDKw Z\xba\x03\xd1\xd2\x1d\x88\x0b`\xe3\ +\xa6\xc7\xc5\xc6M\x8f\x8fw[4c\x84PJ\xa9\xf1n\x84fl\xf97\x87\xba&\x9e\x12\xca\ +GT\x00\x00\x00\x00IEND\xaeB`\x82' + +def getWizardDataOld(): + return \ +'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00t\x00\x00\x01\x04\x08\x06\ +\x00\x00\x00\xf9\xcf\x10R\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\ +\x00\x0f\xdfIDATx\x9c\xed]K\xb6\xe3(\x0cU\xf5\xa9MU\x86\xd9\xd6\xcb\xd0\xd9\ +\x96\x87\xaeeU\x0f\xfc\t\x06\x01\x12\x16\x1f+\xba\xe7\xf4\xe9\xd4\xb3-d\xf0E\ + \t\xf8\xb5,\xcb\xbf\xc7c\x06\xc3\x05L\x00\xf0\xfa\xd9~\xbf\xcf\xbf\x81p\r\ +\xbb\xaf\xf0\xdao\xb1\x97\xfa6L\xdb\xff_?\x00/\xb8\xd6@\xdc\x0f q\xed?\x89w\ +\xfb\x0e\xfc\xac\x8d8m\xbf\x07\x8514\x8b\x9dU\xefj\xac\x92\xecr\x8d\xa1\x01~\ +\xe0`\xe3\xc0L\x8c\xc1\x18z\xc2O5\xe6$\xaf\t\xb2\xfd\xd7:\xca\xfd\xc3\x7fw\ +\xc3\x90\xd8\x18\xfa\xee\xabE\x17\x10\xd8X\xc2\x9c\xa3.\x192\xa5\xd8\x0eoc\ +\xa86|\x19C\xa5F\x9f\x992\xc8\xacz\xe7\x9fs\xef#80\x8c\xa1\xca\xa0\x9f\xa1\ +\xaeG\xe7\xca\xe83\xc9J\x80\x13\xfb\xb3\xb6\x17\xa9o\x9f}\xae^~\xd9\x13Du6\ +\x86*\x83b\x86\xd6\x9a\x1f\x12\xd8\x95\x1d\x01#\xe00t\x7f?c\xa8~\xe8bh\xec+\ +\xdf\xff\xdd\xcb\xc7J\xaa_\xc2(\x97\xf0\x9c1T\x19\x140\xb4\xd4\xff\x1a{ga\ +\x1fkvt\xbc\x81mC\x11\xbd\xcd\x86\xea\xc3\x8d\x19\xca\xf4\xbf\x96\xbec\x8a9~\ +\xd9\xa5\xd7\x04\xed\xfeM\xc3g?\xf1\x97E_\xfcBQ~z\xc9!s\xc0\xc1\xd7=\x03\xdc\ +?\xe8_\'\x00\x80\xe9\r\xcb\xf3y8\x87\xd6,\x03\x81"_ #\xa7\x01~\xdd+\xeb/1\ +\x00\xda\x1a\xf31;\xef"\xda\x08\xa5\x83/\xc25A\xb6\xdf\x88\xa183\xa3\xb8\t\ +\xa3\xa4q\x13\x1b\x9a\xff\xb2wv.\xcf\'\x00\x00<@\xb0\xd7\x99\x00\xf2Ib\x8e\ +\x9e\x1dm\xe8\r\x1a\x94\xc9\xcc*\xe5gF\xc8Go\xb0\xdb\xec\xb7\xf7\xf7\x8a\xf0\ +\xca\x1b\xdf\x86\xba\xe1/\x00\xba\r\x95\xaaL\xd2\xb4\xa5dJT\xc7I\xa1\xc6\xb1\ +\xb0,\x7f\xcf]\xae\x82w*\xc1\xc0\x8e\x05\xaa=\xdau\x7fz\xcfK\xbc\x13\x819Ruw\ +\xd9\xf5\xb7B\rC\xc3.\xb7\xb7\xed\xed\x83a\x18:M\x00/w\xa4\x08\x90\x19\xe5\ +\xe5$^x\xa7\t\xf0\x8f\xe3\x02s\xf2(\r\x9fyX\x96\xe5\x1f\xc0\xbf\xae\xff-\xcb\ +\xf2oY\x96\x7f0M\x97d\xc04}d]\xd4g\x97\xd5\xbbn\xb8\xff\xf5g\xe8\x04p\xb2\ +\x7f\xc1\xc8\x8e\xcaL\x01\x1b\xba13\xfc\xbb\x90\xa3?S\xb6\x1e\x1b\xba\xbd\ +\xc01B\x9dg\xbe\r\x14\xb0\xa1\xfbHy\xd7\xa5H\x8f\xce\xe8\xcfP\x00\x80\x17\ +\xc0\xb2<\xcf\x95\t\xc0\x9a\x87\x85\x9e"\xe6;M\x00\x8f9\xf2Q]\x9es\xd2\xca\ +\xd7\xc3\xd0\x1d\x17\x98Z<\x0f\xf5\x98\xbd<\x9f\xb7\x9e\xc3\x8e\xe1)r\xbcAh\ +\xb7\x07\x90\xffJ\x83\x91)\xad\xe8\xbdg8}D\x9b.\xe2\xb1\xd5$dF\xb9\xbf\x0fa=\ +\xe1T\xd2\xe3\xf1\x07`\xc2>\xb0\x9c\x8e\xfeKg\xee\xdf\x98\xe9be\xa6\xc3N\xbf\ +w\xa8mO\xa3\xe5\xd1\xcb\xed\xcc\xd0\xb87(\xe8B\xe79mC}\xa6e\xde)\xb0\xd9\xd8\ +\x9c\xb3\t37\xa8\xb4\xa1>\x1c\x16\xe5F\xafd\x1b\xca\x90yG\xf4c(i\x11\xd1\xe7\ +\xbe,S\x8964`\xe6c\xae\x18Q\xe1@\xc6\x86v\xecr\t\xe1\xa3\xedE\xd0F\xf0@\xear\ +\xa3\x1f\x07!5\xe4&]n\x9f\x14\x94)\x7f\x8b\xfbU\xfa\x8d\xb3,O\xa2\x8cOy\xcb\ +\xe2\x0f\x80\xe6S\x0f\x90\xc4\x8d\xd2Y\xfad, \xc1\xe9\xe37\x00\xfeUb\xce\x87\ +\xe9\xd3\xfd\xce\x1b;\x1f\xf3\x1c\xb4Q\xe0t8%\x92%\xc2t\xe4@\x80\x00\x92\td\ +\xeciKK\x10\x06 \x91\n<\xec\x1d\xac\r\xe8\xde\xb7Fjf\xf4\xf1\xd9i@\xb6y\xb9\ +\x11;\x01z\xd8P,\xa5\x84i\xab>\xa16F\xb1\xd3&\x9a\xb1\x1a\xba\xcd`\xc8\xd3c/\ +{\xd7\x85iC\x1b3T\xa6\x82\xb8\x8d\x19}\x86c\x87o\x82\xb6\x0c\x1dbz\xe0"7\xd2\ +n\xac\x8b\xdc\xb4eP\xc7\x82\x81\x8d\xba\xe13g\xde\xe7\x8eN\x01\x00u\xbb\x91\ +\xaf!\xdd\xe7n#\x97\xe7\xf3\x18\x04\xf9\xdd\xec>u\xc1d^\xd1K\xa4\x87\xbb\x83\ +\xeboY\xfe\x02@\xbc\xc2J\xaf\xa1\xfaR\xc2`Xx\x0e\xe2\x8dG\xd6k \xd7aU\x86\ +\xce3\xc0\x13K\xe9\xb8\x0c\x9a\xbeG(n\xfb\xc2\'\xa85\x0b\x11J\x195\x1b\xfa\ +\x01\x1a\x9d\x81\xfb\x07\xac\xb9h\x93\x82\x92\xdd\x9dD\xa2\x90xO\xe03uE\xca[\ +\xd5!%\xe7\x0e6\xb4)\\\x1b\xba\xc5S\x01\x10;9\x90\xbd\xab\x81\x06\x0c\xa5,i\ +\xe0I\xc4<E\xae\xbf\xf6\x01ox<\xe0\x94\x96\xe9^\xcb\xef2f\x0cm\x03/8=\x01\ +\xc0k\xab\x04\xd7\x86\xba\xef\x13\x1di+ej}\x86\xa6|\xa5\x00\xac\xaf/\x98\x96\ +\xcc3\xec\xba\xef)\x98\xeb\xdf\\g<\x04\xc1\xef5R\x93H\xe8\xee\xe2\x90\xff\ +\xa6Q.%m\xc4\xb3\xa1>\xfcl\xc2C\x962\xa66\xf0\xe5\xa6l(u>\xc9\xcbX\x88\xbd\ +\x0f\xe6):\x1a\xb5e\xec\x13\xc3W\xd8\xd0\xc2$\xb1\xd4\xfbhgj]\x86\xe6\x12\ +\xc1R_\x1ef\xf7Rz\xba\xf7g\xf6\x84\x8fOi\xb2oT\x11w\xc8XH\xae"K+\x9aL\x1b\ +\xc9\xdc\x9f\xdc\x01ez\xc3\xe3\xf1\x13\xb0?\xfb\\m\xa4\xd2`\x86H\x12\xcb\x06\ +\x8f#\x8d\x99J\xe8\x92\x04:pb&\x9f\r\x88z\x0c\xcd%\x82E\x1a\x14\x9f\x9a\xe4q\ +\xb0,w\xbf\xa3\x8b\xcb\xd4\xf3\xca\xb7\x0eL\x15J\x83\x19g\'\xb1V\xcc\xf4\xa1\ +\x8c\xa9\xf5\x18\xca\\\x89}5h\xcc\xb1\xa1\xbe.\x8f\xc7\x0f,\xcb\xfa\xa7X\x9a\ +hu\x0coCSp\x15\x8c1\xb31\xa2]\xf5\xcd\x98Zq\xdaB\x0bO\x1d\xe9\x95p1\x9d\x834\ +mI\x05\x08V\xbd\\\xe7\x03u\xe5\x9b\x0c\x84RIG\xd8\xe9c\xdaw\x1c\x11\xda\x05\ +\xe5\xb2N\xae>7\xdb\r\xa5\x9es>u8\x8d\xf7\xb5\xbbY\xef\xe5\xba\xb8\xddv|J\ +\xb4\x16\x98\xb1U/\x00X\xce\x8f\xe2ArA|\x85\xeb\x8f\x83\x8cs\xbe\x04\xa8Gi\ +\xf0\xba\xaa\x18>\xeb\x99\xe2\x11\x93\xcf\x0b\xb2?\x1e\xe1\x80\xad\x1eS\xbf)\ +|F\x00\xd59_*\x1b\xe0\x1e\xe9,}l(\xd4\xb8\x96\xd8\x8dl\x7f.\x1adO\xb1\xe3<\ +\xe2=\xbf\xa3SoW\xde\x87\xabW\xe2\x9a\x1a\x86\xd6\xb0\xa1>\xee\x90\xce\xa2\ +\x86\xa1\'O\x910C\xb3\xbe\xdf\xacW\xcc\x18\xcaFM\x1b\x1a+\x0b`\xbc y\xa7Q.\ +\x88_;G[\xfe\xc8\xf4\x04\x91\xb2\xe3\xbe\xdf\xb7HOp\xe5\x9a\x1a\x86\xb6\xb0\ +\xa1>Fdj=_nO\x1b\xeaV\xaa\xa0\r\xc5\xae%\xa74fC\xcb\xd1\xd2\x86\xc6\xca\x06\ +\x88l3\xd7\x10j<E\xb1D\xeb@\'\x00\x99\xa8\x86[\xb6\xe3Q:MiX~XQOQ\xe3.\xb7\ +\x82\x83\x9b\x92\x97Kv\xce\x97\x82\x9b\xa9\xe8\xeb&\xa0\xd78)(\x1a\x80T|\xeb\ +t\x96\xee\x01n10\xf2r\xa5\xbb\\\x1fh\x86~\xb6\x8ee\xf4\xea\xc4\xd0\xfe\x13\ +\xf0\x9a\x88-\xd5h\xc1\xd4\x8e\xd3\x16\xd9\xe2H6\x14\xd3\x0b@\xce\x86":\x01\ +\x103\xf4\xa5\xf4\xbaSz\x059m\xa4\xb7.\x88^\xad\x0e\xf7i\xb2OQ|\x92]\xa7\xd8\ +\xf4\xfb\xf0l\x95\x9f\xc4\x06\xc0\xdf\x8a\'\x94\xf9F\xb6\xaa\x13\x9d\xb6\x98\ +c!\x8a)\\\x01\'\xb1\x91V-\xa7C\xe5\x15\xdc97\x9ad\xb9\x84$1\x80\xa66\xd4\xc7\ +\xb1\xcd\xab{*\xaf\xb4^_mC\xb74M\xea}w\xa8\xab\xbe{,t\xb4\xa1k\xf7\xb7\xfe\ +\xe5\x93\xf8\x95\xef5p\xfb\'\x01\xb3\xa1\'\xb0l\xe8Vq\xc1\xfaP\xe2\xca\xef\ +\x11\x02\xd91\xdcj\x17\x94\x14h\xcey8<J\xeb3\xfe`\xe5\x0f\xc4\xea\xe2t\xd0]\ +\x95\xdeE\x86\xa1\xf5\x8f\xca:\xb2\xd1\xbd2\xc4\xbfr\xf7\xa5#\xb2\xa7L\xc5\ +\xb8\x83\xb7\xa4\xfcD\x19W\x10\xad#zY\xb7\xdcI\x0c\x83\xbf\x93X\x80\x083\xf7\ +\x7f\x1f\xbf\x01\x00\xab\x0f<1\xec\xba\xde\xae~\x12\xa3\xdc6\x87\xd9\xb9_^5\ +\xa6\xfeu~\x9fe\x9e\xec\x9fS\xee\x03>\x19\x07\x07",]7\xb0\xf2\xbbr\xc1zs\x1b\ +-U_\x194\xdas\x9e\x90J!\x19\x8b\xf4\xd6\x9f\x02D\x96\xfa;i$\xd4\xc8\x08\xd9g\ +\xcc\xc6]l\xe8\x8e\x143EX\x1a\xda\xd0\x143\xddc%\xc3#.S\xfa|\x18\xbd,\xc2^)\ +\x01\x1b\xda\xf0T\x88:\xe9\x1f;\x02\xe6 \xe7\x9c\x9dYuf\x84\x9f\xec\x15\xed1\ +\xbc\xac\x84u\x03\xc9K\xaa\x1fr%z\xafv\r\xda!\xfd\x03 \x11d\xc6\xc2{\xae\xdf\ +\x96\xb0\xc9\xd5!_dpt\xb7\x007\xe1\xc8\xe5\x9ae\xb3?\xda\x94>\x03\x1f\x9f\ +\xd5\xf8\xa8\xacL\xae\xea\x85n7v\x96v|\xf0\x12\xa6\xc8\xe0\x03\xa7H\x81N\x97\ +>\xcf\xf3\xf5nWe\x92\x984K\x93\xcc\x0c?\x1e\xea&W\xbb\xec\x1d\xcf\xe78\xfb\ +\x1au8\x106\x97\x01^\xc8\xd2\xa2\x14J<\xbcwrR\x10\xa60\xf4\xf22\xba\xdc\xca\ +\x86\x92!0\x85!\xdb\xcc|E\xf9K\xf2]\xf4;\x8c>\x8e>G6WH \xdbSE\xd8\xd3\x08,x\ +\xe0\xd9\xd3\x94L?\x11\xacx\xc0d\xbb\xa0T\x86\x9fz\x92r~\x9c\xa6;}\xeb\xb2\ +\xcdA<\x01RI\xd8\xd0$\x1d$\xd0\x05c\xc7\xc9\xcfK\xac\xa3b\xdd-\xc0]\x1fN |\ +\x9d\x9a\xe0,EW\x9fu\n\x82wb\xe8\x8e6\xa15\xb2\x1eA\xd9\x9f\xbf\xafS\x13\\\ +\x9f\xfd(\x913\x98uj6\xb4\r\xc2HL>M\xa5\xe7\x8ec\x9d\x19\n\xd0$\xb4F\xd5\x03\ +);\x9c\x00\xa4\xea\xea3\xc5Y\x96\xbf\xcci\x8d\xd9\xd0f\x08#1\xf1\x14\x97p\ +\xba\xd3\xd6\x96\xf6\x99\x87\xfa\xc8Eb\x00\xfa\xd8Rdn\nP+\x12c\x0cm\x0b\xc6\ +\xbc\xf4\xc4\xe8\xc6\x0c\x1d\xc0\x86\xee\xc8DbZ\xd8\xd2\x84\x07\x8b\x93$v$p?\ +\x9f0S\x13\xb3m\x94\xdb\x01\xce\xbc\x94\xe5=j\xc8\xd4\x81\x18\xbac\x00/R,9\ +\x1c\x08\xe9\xa2\xde}\xc7\xefl\x1d\x9b\r\xed\x02\xf4\xf0\xf6\xad\x01\xa6\xe9\ +\xbd\xeen\x8e\x06\xd9\xdb\xd4\xf1\x80\x0c\x05\xc8/C\x84\xcaL\x8d{\xb0\xfc\ +\x13\x9c\x0e\xf6m\xd1\x9ex^Sf&q\xbb$1\x0er+\xbf\x8f\x7f7\xc8\xf8\xc7\xca\x06\ +b\x16>+\xa4\xa66\xc0\r\xf4/\xb2j\xdaG\xa2\x12\t\x03\x9c\xe3\x98\xaf\xc6\tec2\ +tG\xac\x1b\xda\xff-\x90\\\x96F\xa6l\x7f$\xbb\xab\\\xd2\x88*\x93\xc4|tgj\xe6C\ +y\xfd\xc0<\xcf\'6\xd6Y\x0cL\xc7\xd8\x0c\xdd\x91\xfaz\x01\xea2\x95T\xb6D\xb9\ +\x9am\xa8\x0f\x16S\x85\'\xf0\xd4\x01M\xea\xda\x04\x1f\xdd\xdc\xdf\xfb\xbf\ +\xb3:\xd3\x9f\xbb\x07CwP6U\xae\xe5\xcc\'m\xe8\xec2\x89\xe0\xca\x0ct\xberm\ +\xfdm\x8e\x05e8;\x16Ze\x7fK-\x1b 1\xe0bY~\xb9{y"\xac\x8a\xe9\\~\xcd\x18\xaa\ +\x0c+C\'\xfa(\xaa*\xd8l*`\x87T"\xb4 \xab$\xd9n\x0cU\x86\xb1\x18\x8a\x813m\ +\xe0\xb0#%7\xbb\xb7\xd2\x85k\xa9\xa30I\xf3P\xb3\xa1_\x85\x0fC\xfd/\x96\xb2\ +\x82\xd9\xbf\xe7\x85\xfc\x8d\xf2\x1c\x05W\xd28\xf6\x7f\xc7\xae\x1d\x0c \x04\ +\xd7\xd9\xd7\xa8\xbd\xdf\x05O\x913\xfa6\x86*C\x18\xe0\xe6\xec-\x801\xb4DN\ +\xee~\xca\xf3\x01\xc6\x9a\x1f\x92X\xc6\x1d\x99#\xe9\xaf\xc6Pe\xa0\x8drs_H\ +\xea\xbe\xda\xde\'N6\x80\xc4\xfc\x90\xc2\x96\xa2\xb90\xc7\x86\xc6\xdf\xc7\ +\x18\xaa\x0cc2\xb4D\x0e\'\xc4&bC\x99\xf21\x99\xfe\x98#\xcb\xe8\xfc(|e(g;\x97\ +\xda\xc8M\xf8K\x9f\xc5d\x89\xb9\xe4<B\x90\xd6\xeap\xc2mo\\&"\x7f\x9c\x007%\ +\xa1*w\xcf\x11\x04&\x96\'\x96\xe1\xf0\xe3\x94\xfd\x93\xb9\x17y\x8e\x12\xe0&\ +\xe27\xa3\xf4q\x90s`\xb0\xf0n4mI\\\x13t1\x86\x0c\xad\x99\xe4\x94b\x18\xb7\ +\x81r\xb2X\xf2\xde\xb4\x1e\xe2\x0683\xf4\xe5\xfd\x1f\xe0:\x13nWIu\x98\x93\ +\xbc&\xc8\xf6|\x97\xcb\x19\xa4P\x1b/\xd6eN\x0c\x19~\x991y\x1c\xbdNpG\x97o\ +\xb8\xcb\x11\x99\xf7\xb4\xa1]\xb07p\xa1\x93"u\xcd\x02\xdc\x86\x18\xae\xad>+\ +\xedrc\xcf\x97\xca\xa1\xc8ma\xcbK\x03\xe3\x82l7\x86*C_\x86\xc6\xe4\\\x91\x95\ +\x92)!wp\x18C\x95a\x0c\x86b\xb2\xae\xca\x8b\xc9\x94\x90;0\x8c\xa1\xca06C\xaf\ +\xca\xac)wP\x18C\x95a\x1c\x86b\xf2F\x97; \x8c\xa1\xca0\x16C1\x995\xe5J\xc9\ +\x1e\x08\xc6Pe\xf8\x1e\x86\xd6\x96=\x08\xc6\xc9)JA*\xdd\xa4G\xdepc\x8c\xd7\ +\xa0\xca\x18\xd3\x1a\xe35h\x0c5\x99\xa4\x88\xa5c6h\xedD5\xc5\x18\xb3A{0F\tK\ +\xc7lP\xe5,\xaa\x891\x1b\xb46\x14\x7f0\xe6XP\x86\xf1\x1c\x0b1\xd9w\x93\xdf\t\ +\xc6Pe\x18\x97\xa1\x98|\xc92\x8c\xa1\x86;\xc0\x18ZCvG\x18C\x95\xc1\x18ZCvG\ +\x18C\x95a\xfc\xdd8[B\x01C\xd7\x06}\xddc1\xab!\x0fc\xa8\x0b\x05\x0c\xfdN\xe7\ +|\x0c\nBh[\x97[\xf8\xb4\x8dr\x87\x831\xd4\x85\x02\x86\xde\xabAk3\xc8\x18Z\ +\x11\n\xd8\xd2\x03\xe36\xa8\xa1\x08\xd6\xa0\xca`\xae?e\x18\xd79\xdfz\xca")\ +\xbf#\x8c\xa1\xca`\x0c\x95\x96\xdd\x19\xc6Pe\x18\x93\xa15m\x9cbv\x02\x18C\ +\xd5\xe1>\x0c\xad%[\x11;\x01\x8c\xa1\xea0\x1eCmt{\t\xc6Pe\xf8\x1e\x86*\xf6\ +\x0e\xb90\x86*\xc3X\x0cm\xc9Ne\xcc\xdc1N\xf8\xacV@\xfb\xcb\x02\xe5\xe34(\x86\ +Z\xbbp*\xc6\xd8\rZ\x0bJ\xbb[\x80Q\x1a\xd4\xf6\x9b\x17\xc3\x18\rj\x10C\xff\ +\x06\xada\xe7Fd\xe7~\xf0\xeb\xe9\xe0\xd8\x9f\xf0wp\x8d\x87\xbe\x87\xd9\xd5\ +\xa8\xf8a\x1a\xd3;\x16\xeb\x05\x02\x87\xd9\xe5\xa7\x97\xe6XP\x86\xcf\xea\xb3\ +\x92/\xf8\x8acA\x9aI\xa9\xee\xa9*;\x079\x10v{Gc\xa82\x9c\xd7\x87^=.\x92\xfa\ +\xbc$;\x9b3\x93z \xec\xfb\xfc\xcc\xe9\x1a\xac\xd7\x83\xe3)\x1d\xf9\xe0\\\xc3\ +\x8e\xd0\xb6\x03a\xbf\x03\xe3\xac\xe0\x96\xb0\xbf%\xb2(eQl!:\x02\xc5X\xec1\ +\xd7gm\xecZt\x9cs\xd6\xc5\x18\xaa\x0cc0tHv\x96\x8cV==\x83\xe7"e\xc4X\xee\xcb\ +\xc0\x9e\xf7\xf42\x86*\x03\x9d\xa1\x943O\xb0\xd1\x18\x17\xa5g\xabH2\x13 2\ +\x92\x8d<\x120)g;\xbd\xe7r\x0c>d@\xa2\x97X\x7f\x1bC\x95\xa1\x1eC\xb1\xeb\x92\ +\x10\x9fcF\x18\xc0\xb5\xef\xcd=E\xe7\xb63\x86*\x03\x9e$F\xf5\xe4\xc4\x18\xca\ +\xf1\xdepX\\\xc3\xf3\x83\xb2\x8a9\xeaGmhLF\xc1\x88\xd9/\xcb}\xce\xbboe\xe8<\ +\xd3\x074\xa9\xfb^\x8cgs\xe5\xe4\x9e\x15A\xda\xd1\xcd\x92S\xd4Ug\xa6-\xb1g\ +\x0e=\xc3\xe7>\xf1Pj\xe5\xa6\xee\xcb\xc9(\xb1\xa9\xb5"%SD\xb6DyU\xa3;o8\x1au\ +\x87\xf3.\x1f\x86\x8e\x86\xea\x01\xe9RVyp\xbb@\x00\x9e\x9c\xec\xb4\x85\xfa\ +\xdc\xa7\x8b\xef\x9f\x82\xe2\xe3\x05m\x1a\x13+\xb7\x9b\x1c&b>]\x18\x89\xa1\ +\xcdRD\xa8\x8eu\x02\xb2\xae9\xa2.\xc5z\x84\x8e\x8f\xf3n\x9c\xad\x12\x93\x87J\ +\xa5\x1c \xd2T\x0c\xdf\x9e\x1e\xbe\\\x8f\xa1\xb5\x1a\xb6wC^\xb1w(\x84\xa6;\ +\xa5\t\x06\xae\x0c\x00\x0bpk\xc4\xb5\xd5gw\xc2e{\xe7B\xca\x0e_\xb5\xa1\xa1.\ +\xc6Pe\xf8\x0e\x86J\xb8\xf7\x0ePSCJ\xf4*\x94\x03fC\xd5\xe2\x0b\x18\x9a\xf6}\ +\xb2Q\x93\xed\x02z\x19C\x95\xe1\x0b\x19z\xf1]\xc5\xec\xde\xa6\x9b1\xd4\x90\ +\x82r\x86\nET0\x99\x83\xb2\xdd\x18\xaa\x0c\xba\x19*\xea\x1d\x82l\xfa\x07\x1f\ +\x826t\x93g\x0cU\x06\xdd\x0c\xbd\x1a\r\xc1\xe4\x01\x0cjCWyx\xf8L\x0b\xbe\xae\ +\xcb\x1d1\x05E\n\xe2;\x85"\xe9&\x03B1C\x89\xebR\xa8`\xe5\xde2eJ\xe9\x08\x9a\ +\x19Z\x1dc\x8e;\xf42\x14M7a\xae!\xe1\xaeQ)\xbd&\xb1\x0b\xcav\xcd\x18\xaa\x0c\ +\xd6\xa0\xca`\x8e\x05eP\xecX\xa8=\xca\xbd(\xcf\x95)8\xca5\x86*\x83^\x86\xfa\ +\xa3\\\t\xb7_\xd5P\x1c\x80D;\x18C\x95\xe17\x00\xc0\xd4{\x9f"\x83\x18\xfe\x07\ +\xd2\x8b\x12\xe7\xc3\x8c\xd4\xb6\x00\x00\x00\x00IEND\xaeB`\x82' + +def getWizardBitmap(): + return BitmapFromImage(getWizardImage()) + + +def getWizardImage(): + stream = cStringIO.StringIO(getWizardDataOld()) # NOTE: This reverts us to the bitmap Peter likes. + return ImageFromStream(stream) + diff --git a/wxPython/samples/ide/activegrid/tool/XmlEditor.py b/wxPython/samples/ide/activegrid/tool/XmlEditor.py new file mode 100644 index 0000000000..f82ed7d5c9 --- /dev/null +++ b/wxPython/samples/ide/activegrid/tool/XmlEditor.py @@ -0,0 +1,165 @@ +#---------------------------------------------------------------------------- +# Name: XmlEditor.py +# Purpose: Abstract Code Editor for pydocview tbat uses the Styled Text Control +# +# Author: Peter Yared +# +# Created: 8/15/04 +# CVS-ID: $Id$ +# Copyright: (c) 2004-2005 ActiveGrid, Inc. +# License: wxWindows License +#---------------------------------------------------------------------------- + + +import wx +import string +import STCTextEditor +import CodeEditor + + +class XmlDocument(CodeEditor.CodeDocument): + + pass + + +class XmlView(CodeEditor.CodeView): + + + def GetCtrlClass(self): + """ Used in split window to instantiate new instances """ + return XmlCtrl + + + def GetAutoCompleteHint(self): + pos = self.GetCtrl().GetCurrentPos() + if pos == 0: + return None, None + + validLetters = string.letters + string.digits + '_:' + word = '' + while (True): + pos = pos - 1 + if pos < 0: + break + char = chr(self.GetCtrl().GetCharAt(pos)) + if char not in validLetters: + break + word = char + word + + return None, word + + + def GetAutoCompleteDefaultKeywords(self): + return XMLKEYWORDS + + +class XmlService(CodeEditor.CodeService): + + + def __init__(self): + CodeEditor.CodeService.__init__(self) + + +class XmlCtrl(CodeEditor.CodeCtrl): + + + def __init__(self, parent, ID = -1, style = wx.NO_FULL_REPAINT_ON_RESIZE): + CodeEditor.CodeCtrl.__init__(self, parent, ID, style) + self.SetLexer(wx.stc.STC_LEX_XML) + self.SetProperty("fold.html", "1") + + + def GetMatchingBraces(self): + return "<>[]{}()" + + + def CanWordWrap(self): + return True + + + def SetViewDefaults(self): + CodeEditor.CodeCtrl.SetViewDefaults(self, configPrefix = "Xml", hasWordWrap = True, hasTabs = True) + + + def GetFontAndColorFromConfig(self): + return CodeEditor.CodeCtrl.GetFontAndColorFromConfig(self, configPrefix = "Xml") + + + def UpdateStyles(self): + CodeEditor.CodeCtrl.UpdateStyles(self) + + if not self.GetFont(): + return + + faces = { 'font' : self.GetFont().GetFaceName(), + 'size' : self.GetFont().GetPointSize(), + 'size2': self.GetFont().GetPointSize() - 2, + 'color' : "%02x%02x%02x" % (self.GetFontColor().Red(), self.GetFontColor().Green(), self.GetFontColor().Blue()) + } + + # White space + self.StyleSetSpec(wx.stc.STC_H_DEFAULT, "face:%(font)s,fore:#000000,face:%(font)s,size:%(size)d" % faces) + # Comment + self.StyleSetSpec(wx.stc.STC_H_COMMENT, "face:%(font)s,fore:#007F00,italic,face:%(font)s,size:%(size)d" % faces) + # Number + self.StyleSetSpec(wx.stc.STC_H_NUMBER, "face:%(font)s,fore:#007F7F,size:%(size)d" % faces) + # String + self.StyleSetSpec(wx.stc.STC_H_SINGLESTRING, "face:%(font)s,fore:#7F007F,face:%(font)s,size:%(size)d" % faces) + self.StyleSetSpec(wx.stc.STC_H_DOUBLESTRING, "face:%(font)s,fore:#7F007F,face:%(font)s,size:%(size)d" % faces) + # Tag + self.StyleSetSpec(wx.stc.STC_H_TAG, "face:%(font)s,fore:#00007F,bold,size:%(size)d" % faces) + # Attributes + self.StyleSetSpec(wx.stc.STC_H_ATTRIBUTE, "face:%(font)s,fore:#00007F,bold,size:%(size)d" % faces) + + +class XmlOptionsPanel(STCTextEditor.TextOptionsPanel): + + def __init__(self, parent, id): + STCTextEditor.TextOptionsPanel.__init__(self, parent, id, configPrefix = "Xml", label = "XML", hasWordWrap = True, hasTabs = True) + + +XMLKEYWORDS = [ + "ag:connectionstring", "ag:datasource", "ag:editorBounds", "ag:label", "ag:name", "ag:shortLabel", "ag:type", + "element", "fractionDigits", "length", "minOccurs", "name", "objtype", "refer", "schema", "type", "xpath", "xmlns", + "xs:complexType", "xs:element", "xs:enumeration", "xs:field", "xs:key", "xs:keyref", "xs:schema", "xs:selector" + ] + + +#---------------------------------------------------------------------------- +# Icon Bitmaps - generated by encode_bitmaps.py +#---------------------------------------------------------------------------- +from wx import ImageFromStream, BitmapFromImage +from wx import EmptyIcon +import cStringIO + + +def getXMLData(): + return \ +'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x10\x00\x00\x00\x10\x08\x06\ +\x00\x00\x00\x1f\xf3\xffa\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\ +\x00\x01\x18IDAT8\x8d\xed\x92=N\xc3P\x0c\xc7\x7f~\xc9K\xd2\xa0\x16\xa9\xdc\ +\x84+\xf4\x06\x1c\xa1\x12\x133\xa7`\xea\x05\xba\xc0\xc0\xd0\x93\x80*uc``e\t\ +\x82\xb6J\xf3Q?3D\x04\x81`\xea\xc2\x80\x17K\xb6\xfc\xff\xb0-ff\x1c\x10\xee\ +\x90\xe1\xbf\x01\x10s}\x0em\tu\t\xfb\x06\xcbFP\xad\x11\x17\x81\x196\x18!\xdb\ +\x02\xd2#hk\xc8\x8f\t\xc1p\x89g\xb9\\\x11\xdb\xfd-\xbcn\x91\xa8C\x94,\x81\ +\xaa\xe9\x19\xe4\x1b\xa3}R\xf3\xf0\x08\x0e\x9f\x81\xef\x9c\x94s\x83\xaa\xe92\ +P\xcf\nv\xa7g\xd4\xb3\xa2\xef\xaf\xc5#i\x04\x89#\x8a\x05\'m\r)\x84\r\xe4S\ +\xa1\x9c\x1b\xf9\xb4\xe3\xd5\xe1\x18?\xb9@\x87\xe3^\x81\xbe\xb5H\xab`\x013\ +\xc3\xa9\xf3h\x15pC\xfa\xe1\x0f\x05\x00\xf1\xd5\xe4\x8b\x85la\x10@[0q\x88]\ +\x9e\x18/\x05\xe8/k\xde\x01\x83\x1f\xea\x19,\x9e\x1c\xf1\xcdj\xc3\xae\x01jP\ +\x05\x9fv\x07q1\x88\x83(\x8f\xd0\x8d"1h\x05\xba\x077\x80$\x87\xbb\xe7\x80\ +\xfc\xbf\xf2\xe1\x00\xef\x8c\xb8x\x06\x07\xd1$\xff\x00\x00\x00\x00IEND\xaeB`\ +\x82' + + +def getXMLBitmap(): + return BitmapFromImage(getXMLImage()) + +def getXMLImage(): + stream = cStringIO.StringIO(getXMLData()) + return ImageFromStream(stream) + +def getXMLIcon(): + icon = EmptyIcon() + icon.CopyFromBitmap(getXMLBitmap()) + return icon diff --git a/wxPython/samples/ide/activegrid/tool/__init__.py b/wxPython/samples/ide/activegrid/tool/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/wxPython/samples/ide/activegrid/tool/checker.py b/wxPython/samples/ide/activegrid/tool/checker.py new file mode 100644 index 0000000000..985222a1dc --- /dev/null +++ b/wxPython/samples/ide/activegrid/tool/checker.py @@ -0,0 +1,896 @@ +#!/usr/bin/env python + +# Copyright (c) 2001-2004, MetaSlash Inc. All rights reserved. + +""" +Copyright notice from pychecker: + +Copyright (c) 2000-2001, MetaSlash Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the + distribution. + + - Neither name of MetaSlash Inc. nor the names of contributors + may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +""" + + +""" +Check python source code files for possible errors and print warnings + +Contact Info: + http://pychecker.sourceforge.net/ + pychecker-list@lists.sourceforge.net +""" + +import string +import types +import sys +import imp +import os +import glob +import traceback +import re +import wx +_ = wx.GetTranslation + +# see __init__.py for meaning, this must match the version there +LOCAL_MAIN_VERSION = 1 + + +def setupNamespace(path) : + # remove pychecker if it's the first component, it needs to be last + if sys.path[0][-9:] == 'pychecker' : + del sys.path[0] + + # make sure pychecker is last in path, so we can import + checker_path = os.path.dirname(os.path.dirname(path)) + if checker_path not in sys.path : + sys.path.append(checker_path) + +if __name__ == '__main__' : + setupNamespace(sys.argv[0]) + +from pychecker import utils +from pychecker import printer +from pychecker import warn +from pychecker import OP +from pychecker import Config +from pychecker import function +from pychecker.Warning import Warning + +# Globals for storing a dictionary of info about modules and classes +_allModules = {} +_cfg = None + +# Constants +_DEFAULT_MODULE_TOKENS = ('__builtins__', '__doc__', '__file__', '__name__', + '__path__') +_DEFAULT_CLASS_TOKENS = ('__doc__', '__name__', '__module__') + +_VERSION_MISMATCH_ERROR = ''' +There seem to be two versions of PyChecker being used. +One is probably in python/site-packages, the other in a local directory. +If you want to run the local version, you must remove the version +from site-packages. Or you can install the current version +by doing python setup.py install. +''' + +def cfg() : + return utils.cfg() + +def _flattenList(list) : + "Returns a list which contains no lists" + + new_list = [] + for element in list : + if type(element) == types.ListType : + new_list.extend(_flattenList(element)) + else : + new_list.append(element) + + return new_list + +def getModules(arg_list) : + "Returns a list of module names that can be imported" + + global _output + + new_arguments = [] + for arg in arg_list : + # is this a wildcard filespec? (necessary for windows) + if '*' in arg or '?' in arg or '[' in arg : + arg = glob.glob(arg) + new_arguments.append(arg) + + PY_SUFFIXES = ['.py'] + PY_SUFFIX_LENS = [3] + if _cfg.quixote: + PY_SUFFIXES.append('.ptl') + PY_SUFFIX_LENS.append(4) + + modules = [] + for arg in _flattenList(new_arguments) : + fullpath = arg + # is it a .py file? + for suf, suflen in zip(PY_SUFFIXES, PY_SUFFIX_LENS): + if len(arg) > suflen and arg[-suflen:] == suf: + arg_dir = os.path.dirname(arg) + if arg_dir and not os.path.exists(arg) : + txt = _('File or pathname element does not exist: "%s"') % arg + _output.AddLines(txt) + continue + + module_name = os.path.basename(arg)[:-suflen] + if arg_dir not in sys.path : + sys.path.insert(0, arg_dir) + arg = module_name + modules.append((arg, fullpath)) + + return modules + +def _q_file(f): + # crude hack!!! + # imp.load_module requires a real file object, so we can't just + # fiddle def lines and yield them + import tempfile + fd, newfname = tempfile.mkstemp(suffix=".py", text=True) + newf = os.fdopen(fd, 'r+') + os.unlink(newfname) + for line in f: + mat = re.match(r'(\s*def\s+\w+\s*)\[(html|plain)\](.*)', line) + if mat is None: + newf.write(line) + else: + newf.write(mat.group(1)+mat.group(3)+'\n') + newf.seek(0) + return newf + +def _q_find_module(p, path): + if not _cfg.quixote: + return imp.find_module(p, path) + else: + for direc in path: + try: + return imp.find_module(p, [direc]) + except ImportError: + f = os.path.join(direc, p+".ptl") + if os.path.exists(f): + return _q_file(file(f)), f, ('.ptl', 'U', 1) + +def _findModule(name) : + """Returns the result of an imp.find_module(), ie, (file, filename, smt) + name can be a module or a package name. It is *not* a filename.""" + + path = sys.path[:] + packages = string.split(name, '.') + for p in packages : + # smt = (suffix, mode, type) + file, filename, smt = _q_find_module(p, path) + if smt[-1] == imp.PKG_DIRECTORY : + try : + # package found - read path info from init file + m = imp.load_module(p, file, filename, smt) + finally : + if file is not None : + file.close() + + # importing xml plays a trick, which replaces itself with _xmlplus + # both have subdirs w/same name, but different modules in them + # we need to choose the real (replaced) version + if m.__name__ != p : + try : + file, filename, smt = _q_find_module(m.__name__, path) + m = imp.load_module(p, file, filename, smt) + finally : + if file is not None : + file.close() + + new_path = m.__path__ + if type(new_path) == types.ListType : + new_path = filename + if new_path not in path : + path.insert(1, new_path) + elif smt[-1] != imp.PY_COMPILED: + if p is not packages[-1] : + if file is not None : + file.close() + raise ImportError, "No module named %s" % packages[-1] + return file, filename, smt + + # in case we have been given a package to check + return file, filename, smt + + +class Variable : + "Class to hold all information about a variable" + + def __init__(self, name, type): + self.name = name + self.type = type + self.value = None + + def __str__(self) : + return self.name + + __repr__ = utils.std_repr + + +def _filterDir(object, ignoreList) : + "Return a list of tokens (attributes) in a class, except for ignoreList" + + tokens = dir(object) + for token in ignoreList : + if token in tokens : + tokens.remove(token) + return tokens + +def _getClassTokens(c) : + return _filterDir(c, _DEFAULT_CLASS_TOKENS) + + +class Class : + "Class to hold all information about a class" + + def __init__(self, name, module) : + self.name = name + self.classObject = getattr(module, name) + + modname = getattr(self.classObject, '__module__', None) + if modname is None: + # hm, some ExtensionClasses don't have a __module__ attribute + # so try parsing the type output + typerepr = repr(type(self.classObject)) + mo = re.match("^<type ['\"](.+)['\"]>$", typerepr) + if mo: + modname = ".".join(mo.group(1).split(".")[:-1]) + + self.module = sys.modules.get(modname) + if not self.module: + self.module = module + + global _output + txt = _("warning: couldn't find real module for class %s (module name: %s)\n") % (self.classObject, modname) + _output.AddLines(txt) + + self.ignoreAttrs = 0 + self.methods = {} + self.members = { '__class__': types.ClassType, + '__doc__': types.StringType, + '__dict__': types.DictType, } + self.memberRefs = {} + self.statics = {} + self.lineNums = {} + + def __str__(self) : + return self.name + + __repr__ = utils.std_repr + + def getFirstLine(self) : + "Return first line we can find in THIS class, not any base classes" + + lineNums = [] + classDir = dir(self.classObject) + for m in self.methods.values() : + if m != None and m.function.func_code.co_name in classDir: + lineNums.append(m.function.func_code.co_firstlineno) + if lineNums : + return min(lineNums) + return 0 + + + def allBaseClasses(self, c = None) : + "Return a list of all base classes for this class and it's subclasses" + + baseClasses = [] + if c == None : + c = self.classObject + for base in c.__bases__ : + baseClasses = baseClasses + [ base ] + self.allBaseClasses(base) + return baseClasses + + def __getMethodName(self, func_name, className = None) : + if func_name[0:2] == '__' and func_name[-2:] != '__' : + if className == None : + className = self.name + if className[0] != '_' : + className = '_' + className + func_name = className + func_name + return func_name + + def addMethod(self, method, methodName = None) : + if type(method) == types.StringType : + self.methods[method] = None + else : + assert methodName is not None, "must supply methodName" + self.methods[methodName] = function.Function(method, 1) + + def addMethods(self, classObject) : + for classToken in _getClassTokens(classObject) : + token = getattr(classObject, classToken, None) + if token is None: + continue + + # Looks like a method. Need to code it this way to + # accommodate ExtensionClass and Python 2.2. Yecchh. + if (hasattr(token, "func_code") and + hasattr(token.func_code, "co_argcount")): + self.addMethod(token, token.__name__) + + elif hasattr(token, '__get__') and \ + not hasattr(token, '__set__') and \ + type(token) is not types.ClassType : + self.addMethod(getattr(token, '__name__', classToken)) + else : + self.members[classToken] = type(token) + self.memberRefs[classToken] = None + + self.cleanupMemberRefs() + # add standard methods + for methodName in ('__class__',) : + self.addMethod(methodName, classObject.__name__) + + def addMembers(self, classObject) : + if not cfg().onlyCheckInitForMembers : + for classToken in _getClassTokens(classObject) : + method = getattr(classObject, classToken, None) + if type(method) == types.MethodType : + self.addMembersFromMethod(method.im_func) + else: + try: + self.addMembersFromMethod(classObject.__init__.im_func) + except AttributeError: + pass + + def addMembersFromMethod(self, method) : + if not hasattr(method, 'func_code') : + return + + func_code, code, i, maxCode, extended_arg = OP.initFuncCode(method) + stack = [] + while i < maxCode : + op, oparg, i, extended_arg = OP.getInfo(code, i, extended_arg) + if op >= OP.HAVE_ARGUMENT : + operand = OP.getOperand(op, func_code, oparg) + if OP.LOAD_CONST(op) or OP.LOAD_FAST(op) : + stack.append(operand) + elif OP.STORE_ATTR(op) : + if len(stack) > 0 : + if stack[-1] == cfg().methodArgName: + value = None + if len(stack) > 1 : + value = type(stack[-2]) + self.members[operand] = value + self.memberRefs[operand] = None + stack = [] + + self.cleanupMemberRefs() + + def cleanupMemberRefs(self) : + try : + del self.memberRefs[Config.CHECKER_VAR] + except KeyError : + pass + + def abstractMethod(self, m): + """Return 1 if method is abstract, None if not + An abstract method always raises an exception. + """ + if not self.methods.get(m, None): + return None + func_code, bytes, i, maxCode, extended_arg = \ + OP.initFuncCode(self.methods[m].function) + # abstract if the first conditional is RAISE_VARARGS + while i < maxCode: + op, oparg, i, extended_arg = OP.getInfo(bytes, i, extended_arg) + if OP.RAISE_VARARGS(op): + return 1 + if OP.conditional(op): + break + return None + + def isAbstract(self): + """Return the method names that make a class abstract. + An abstract class has at least one abstract method.""" + result = [] + for m in self.methods.keys(): + if self.abstractMethod(m): + result.append(m) + return result + +def _getLineInFile(moduleName, linenum): + line = '' + file, filename, smt = _findModule(moduleName) + try: + lines = file.readlines() + line = string.rstrip(lines[linenum - 1]) + except (IOError, IndexError): + pass + file.close() + return line + +def importError(moduleName): + exc_type, exc_value, tb = sys.exc_info() + + # First, try to get a nice-looking name for this exception type. + exc_name = getattr(exc_type, '__name__', None) + if not exc_name: + # either it's a string exception or a user-defined exception class + # show string or fully-qualified class name + exc_name = str(exc_type) + + # Print a traceback, unless this is an ImportError. ImportError is + # presumably the most common import-time exception, so this saves + # the clutter of a traceback most of the time. Also, the locus of + # the error is usually irrelevant for ImportError, so the lack of + # traceback shouldn't be a problem. + if exc_type is SyntaxError: + # SyntaxErrors are special, we want to control how we format + # the output and make it consistent for all versions of Python + e = exc_value + msg = '%s (%s, line %d)' % (e.msg, e.filename, e.lineno) + line = _getLineInFile(moduleName, e.lineno) + offset = e.offset + if type(offset) is not types.IntType: + offset = 0 + exc_value = '%s\n %s\n %s^' % (msg, line, ' ' * offset) + elif exc_type is not ImportError: + global _output + txt = _(" Caught exception importing module %s:\n") % moduleName + _output.AddLines(txt) + + try: + tbinfo = traceback.extract_tb(tb) + except: + tbinfo = [] + txt = _(" Unable to format traceback\n") + _output.AddLines(txt) + for filename, line, func, text in tbinfo[1:]: + txt = _(" File \"%s\", line %d") % (filename, line) + _output.AddLines(txt) + if func != "?": + txt = _(", in %s()") % func + _output.AddLines(txt) + _output.AddLines("\n") + if text: + txt = _(" %s\n") % text + _output.AddLines(txt) + + # And finally print the exception type and value. + # Careful formatting exc_value -- can fail for some user exceptions + txt = " %s: " % exc_name + _output.AddLines(txt) + try: + txt = str(exc_value) + '\n' + _output.AddLines(txt) + except: + txt = _('**error formatting exception value**\n') + _output.AddLines(txt) + + +def _getPyFile(filename): + """Return the file and '.py' filename from a filename which could + end with .py, .pyc, or .pyo""" + + if filename[-1] in 'oc' and filename[-4:-1] == '.py': + return filename[:-1] + return filename + +class Module : + "Class to hold all information for a module" + + def __init__(self, moduleName, check = 1, fullpath = None) : + self.moduleName = moduleName + self.variables = {} + self.functions = {} + self.classes = {} + self.modules = {} + self.moduleLineNums = {} + self.attributes = [ '__dict__' ] + self.main_code = None + self.module = None + self.check = check + self.fullpath = fullpath + _allModules[moduleName] = self + + def __str__(self) : + return self.moduleName + + __repr__ = utils.std_repr + + def addVariable(self, var, varType) : + self.variables[var] = Variable(var, varType) + + def addFunction(self, func) : + self.functions[func.__name__] = function.Function(func) + + def __addAttributes(self, c, classObject) : + for base in classObject.__bases__ : + self.__addAttributes(c, base) + c.addMethods(classObject) + c.addMembers(classObject) + + def addClass(self, name) : + self.classes[name] = c = Class(name, self.module) + try: + objName = str(c.classObject) + except TypeError: + # this can happen if there is a goofy __getattr__ + c.ignoreAttrs = 1 + else: + packages = string.split(objName, '.') + c.ignoreAttrs = packages[0] in cfg().blacklist + if not c.ignoreAttrs : + self.__addAttributes(c, c.classObject) + + def addModule(self, name) : + module = _allModules.get(name, None) + if module is None : + self.modules[name] = module = Module(name, 0) + if imp.is_builtin(name) == 0 : + module.load() + else : + globalModule = globals().get(name) + if globalModule : + module.attributes.extend(dir(globalModule)) + else : + self.modules[name] = module + + def filename(self) : + try : + filename = self.module.__file__ + except AttributeError : + filename = self.moduleName + return _getPyFile(filename) + + def load(self, warnings = None): + try : + # there's no need to reload modules we already have + global _output, _statusDlg, _count + txt = _("Loading Module %s\n") % self.moduleName + _output.AddLines(txt) + _count += 1 + if _count == 100: + _count = 95 + _statusDlg.Update(_count, txt) + + module = sys.modules.get(self.moduleName) + if module : + if not _allModules[self.moduleName].module : + return self._initModule(module) + return 1 + + return self._initModule(self.setupMainCode()) + except (SystemExit, KeyboardInterrupt) : + exc_type, exc_value, exc_tb = sys.exc_info() + raise exc_type, exc_value + except SyntaxError, (message, (fileName, line, col, text)): + # ActiveGrid: added this for better feedback when module couldn't be loaded. + w = Warning(self.fullpath, line, _("Syntax Error: %s\n%s\n%s^error near here") % (message, text, ' '*(col-1))) + warnings.append(w) + return 0 + except: + w = Warning(self.moduleName, 1, sys.exc_info()[0] + " NOT PROCESSED UNABLE TO IMPORT") + warnings.append(w) + importError(self.moduleName) + return 0 + + def initModule(self, module) : + if not self.module: + filename = _getPyFile(module.__file__) + if string.lower(filename[-3:]) == '.py': + try: + file = open(filename) + except IOError: + pass + else: + self._setupMainCode(file, filename, module) + return self._initModule(module) + return 1 + + def _initModule(self, module): + self.module = module + self.attributes = dir(self.module) + + pychecker_attr = getattr(module, Config.CHECKER_VAR, None) + if pychecker_attr is not None : + utils.pushConfig() + utils.updateCheckerArgs(pychecker_attr, 'suppressions', 0, []) + + for tokenName in _filterDir(self.module, _DEFAULT_MODULE_TOKENS) : + token = getattr(self.module, tokenName) + if isinstance(token, types.ModuleType) : + # get the real module name, tokenName could be an alias + self.addModule(token.__name__) + elif isinstance(token, types.FunctionType) : + self.addFunction(token) + elif isinstance(token, types.ClassType) or \ + hasattr(token, '__bases__') : + self.addClass(tokenName) + else : + self.addVariable(tokenName, type(token)) + + if pychecker_attr is not None : + utils.popConfig() + return 1 + + def setupMainCode(self) : + file, filename, smt = _findModule(self.moduleName) + # FIXME: if the smt[-1] == imp.PKG_DIRECTORY : load __all__ + module = imp.load_module(self.moduleName, file, filename, smt) + self._setupMainCode(file, filename, module) + return module + + def _setupMainCode(self, file, filename, module): + try : + self.main_code = function.create_from_file(file, filename, module) + finally : + if file != None : + file.close() + + +def getAllModules() : + "Returns a list of all modules that should be checked." + modules = [] + for module in _allModules.values() : + if module.check : + modules.append(module) + return modules + +_BUILTIN_MODULE_ATTRS = { 'sys': [ 'ps1', 'ps2', 'tracebacklimit', + 'exc_type', 'exc_value', 'exc_traceback', + 'last_type', 'last_value', 'last_traceback', + ], + } + +def fixupBuiltinModules(needs_init=0): + for moduleName in sys.builtin_module_names : + if needs_init: + _ = Module(moduleName, 0) + module = _allModules.get(moduleName, None) + if module is not None : + try : + m = imp.init_builtin(moduleName) + except ImportError : + pass + else : + extra_attrs = _BUILTIN_MODULE_ATTRS.get(moduleName, []) + module.attributes = [ '__dict__' ] + dir(m) + extra_attrs + + +def _printWarnings(warnings, stream=None): + if stream is None: + stream = sys.stdout + + warnings.sort() + lastWarning = None + for warning in warnings : + if lastWarning != None : + # ignore duplicate warnings + if cmp(lastWarning, warning) == 0 : + continue + # print blank line between files + if lastWarning.file != warning.file : + global _output + _output.AddLines("\n") + + lastWarning = warning + _output.AddLines(warning.format() + "\n") + + +def processFiles(files, cfg = None, pre_process_cb = None) : + # insert this here, so we find files in the local dir before std library + if sys.path[0] != '' : + sys.path.insert(0, '') + + # ensure we have a config object, it's necessary + global _cfg + if cfg is not None : + _cfg = cfg + elif _cfg is None : + _cfg = Config.Config() + + warnings = [] + utils.initConfig(_cfg) + for moduleName, filename in getModules(files) : + if callable(pre_process_cb) : + pre_process_cb(moduleName) + module = Module(moduleName, fullpath = filename) + + module.load(warnings) + utils.popConfig() + return warnings + + +def getWarnings(files, cfg = None, suppressions = None): + warnings = processFiles(files, cfg) + fixupBuiltinModules() + return warnings + warn.find(getAllModules(), _cfg, suppressions) + + +def _print_processing(name) : + if not _cfg.quiet : + global _output, _statusDlg, _count + txt = _("Processing %s...\n") % name + _output.AddLines(txt) + _count += 1 + _statusDlg.Update(_count, txt) + + + +def checkSyntax(filename, messageView): + """ Massively hacked version of main for ActiveGrid IDE integration """ + global _cfg + _cfg, files, suppressions = Config.setupFromArgs([filename]) + if not files : + return 0 + + global _output, _statusDlg, _count + _output = messageView + # wxBug: Need to show progress dialog box, or message window never gets updated until the method returns + _statusDlg = wx.ProgressDialog(_("Check Code"), _("Checking %s") % filename, maximum = 100, style = wx.PD_AUTO_HIDE | wx.PD_APP_MODAL | wx.PD_ELAPSED_TIME) + _count = 0 + + # insert this here, so we find files in the local dir before std library + sys.path.insert(0, '') + + importWarnings = processFiles(files, _cfg, _print_processing) + fixupBuiltinModules() + if _cfg.printParse : + for module in getAllModules() : + printer.module(module) + + warnings = warn.find(getAllModules(), _cfg, suppressions) + + _statusDlg.Update(100, _("Done")) + _statusDlg.Destroy() + + if not _cfg.quiet : + _output.AddLines(_("\nWarnings and Errors...\n")) + if warnings or importWarnings : + _printWarnings(importWarnings + warnings) + return 1 + + if not _cfg.quiet : + _output.AddLines(_("No Syntax Errors")) + return 0 + +## +## +##def main(argv) : +## __pychecker__ = 'no-miximport' +## import pychecker +## if LOCAL_MAIN_VERSION != pychecker.MAIN_MODULE_VERSION : +## sys.stderr.write(_VERSION_MISMATCH_ERROR) +## sys.exit(100) +## +## # remove empty arguments +## argv = filter(None, argv) +## +## # if the first arg starts with an @, read options from the file +## # after the @ (this is mostly for windows) +## if len(argv) >= 2 and argv[1][0] == '@': +## # read data from the file +## command_file = argv[1][1:] +## try: +## f = open(command_file, 'r') +## command_line = f.read() +## f.close() +## except IOError, err: +## sys.stderr.write("Unable to read commands from file: %s\n %s\n" % \ +## (command_file, err)) +## sys.exit(101) +## +## # convert to an argv list, keeping argv[0] and the files to process +## argv = argv[:1] + string.split(command_line) + argv[2:] +## +## global _cfg +## _cfg, files, suppressions = Config.setupFromArgs(argv[1:]) +## if not files : +## return 0 +## +## # insert this here, so we find files in the local dir before std library +## sys.path.insert(0, '') +## +## importWarnings = processFiles(files, _cfg, _print_processing) +## fixupBuiltinModules() +## if _cfg.printParse : +## for module in getAllModules() : +## printer.module(module) +## +## warnings = warn.find(getAllModules(), _cfg, suppressions) +## if not _cfg.quiet : +## print "\nWarnings...\n" +## if warnings or importWarnings : +## _printWarnings(importWarnings + warnings) +## return 1 +## +## if not _cfg.quiet : +## print "None" +## return 0 +## +## +##if __name__ == '__main__' : +## try : +## sys.exit(main(sys.argv)) +## except Config.UsageError : +## sys.exit(127) +## +##else : +## _orig__import__ = None +## _suppressions = None +## _warnings_cache = {} +## +## def _get_unique_warnings(warnings): +## for i in range(len(warnings)-1, -1, -1): +## w = warnings[i].format() +## if _warnings_cache.has_key(w): +## del warnings[i] +## else: +## _warnings_cache[w] = 1 +## return warnings +## +## def __import__(name, globals=None, locals=None, fromlist=None): +## if globals is None: +## globals = {} +## if locals is None: +## locals = {} +## if fromlist is None: +## fromlist = [] +## +## check = not sys.modules.has_key(name) and name[:10] != 'pychecker.' +## pymodule = _orig__import__(name, globals, locals, fromlist) +## if check : +## try : +## module = Module(pymodule.__name__) +## if module.initModule(pymodule): +## warnings = warn.find([module], _cfg, _suppressions) +## _printWarnings(_get_unique_warnings(warnings)) +## else : +## print 'Unable to load module', pymodule.__name__ +## except Exception: +## name = getattr(pymodule, '__name__', str(pymodule)) +## importError(name) +## +## return pymodule +## +## def _init() : +## global _cfg, _suppressions, _orig__import__ +## +## args = string.split(os.environ.get('PYCHECKER', '')) +## _cfg, files, _suppressions = Config.setupFromArgs(args) +## utils.initConfig(_cfg) +## fixupBuiltinModules(1) +## +## # keep the orig __import__ around so we can call it +## import __builtin__ +## _orig__import__ = __builtin__.__import__ +## __builtin__.__import__ = __import__ +## +## if not os.environ.get('PYCHECKER_DISABLED') : +## _init() +## diff --git a/wxPython/samples/ide/activegrid/tool/data/tips.txt b/wxPython/samples/ide/activegrid/tool/data/tips.txt new file mode 100644 index 0000000000..1d2008f10a --- /dev/null +++ b/wxPython/samples/ide/activegrid/tool/data/tips.txt @@ -0,0 +1,7 @@ +Ctrl-Space in any editor does code completion. +Right-clicking on something in the 'Thing' column of the debugger's frame tab may allow you to introspect it for more information. +Right-Mouse-Click in Outline window allows you to change display sorting. +In an editor, you can add line markers via Ctrl-M and jump to the next marker with F4 or previous marker with Shift-F4. +In an editor. you can use the numpad + and - keys to toggle folding. +In 'Find in Directory', if you specify a file, it will display all matches in the Message Window. +Breakpoints for the debugger can be set while the process is running \ No newline at end of file diff --git a/wxPython/samples/ide/activegrid/tool/process.py b/wxPython/samples/ide/activegrid/tool/process.py new file mode 100644 index 0000000000..a521f0cf50 --- /dev/null +++ b/wxPython/samples/ide/activegrid/tool/process.py @@ -0,0 +1,2364 @@ +#!/usr/bin/env python +# Copyright (c) 2002-2003 ActiveState +# See LICENSE.txt for license details. +""" Contents of LICENSE.txt: +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +""" + +r""" + Python interface for process control. + + This module defines three Process classes for spawning, + communicating and control processes. They are: Process, ProcessOpen, + ProcessProxy. All of the classes allow one to specify the command (cmd), + starting working directory (cwd), and environment to create for the + new process (env) and to "wait" for termination of the child and + "kill" the child. + + Process: + Use this class to simply launch a process (either a GUI app or a + console app in a new console) with which you do not intend to + communicate via it std handles. + + ProcessOpen: + Think of this as a super version of Python's os.popen3() method. + This spawns the given command and sets up pipes for + stdin/stdout/stderr which can then be used to communicate with + the child. + + ProcessProxy: + This is a heavy-weight class that, similar to ProcessOpen, + spawns the given commands and sets up pipes to the child's + stdin/stdout/stderr. However, it also starts three threads to + proxy communication between each of the child's and parent's std + handles. At the parent end of this communication are, by + default, IOBuffer objects. You may specify your own objects here + (usually sub-classing from IOBuffer, which handles some + synchronization issues for you). The result is that it is + possible to have your own IOBuffer instance that gets, say, a + .write() "event" for every write that the child does on its + stdout. + + Understanding ProcessProxy is pretty complex. Some examples + below attempt to help show some uses. Here is a diagram of the + comminucation: + + <parent process> + ,---->->->------' ^ `------>->->----, + | | v + IOBuffer IOBuffer IOBuffer + (p.stdout) (p.stderr) (p.stdin) + | | | + _OutFileProxy _OutFileProxy _InFileProxy + thread thread thread + | ^ | + `----<-<-<------, | ,------<-<-<----' + <child process> + + Usage: + import process + p = process.<Process class>(cmd='echo hi', ...) + #... use the various methods and attributes + + Examples: + A simple 'hello world': + >>> import process + >>> p = process.ProcessOpen(['echo', 'hello']) + >>> p.stdout.read() + 'hello\r\n' + >>> p.wait() # .wait() returns the child's exit status + 0 + + Redirecting the stdout handler: + >>> import sys + >>> p = process.ProcessProxy(['echo', 'hello'], stdout=sys.stdout) + hello + + Using stdin (need to use ProcessProxy here because it defaults to + text-mode translation on Windows, ProcessOpen does not support + this): + >>> p = process.ProcessProxy(['sort']) + >>> p.stdin.write('5\n') + >>> p.stdin.write('2\n') + >>> p.stdin.write('7\n') + >>> p.stdin.close() + >>> p.stdout.read() + '2\n5\n7\n' + + Specifying environment variables: + >>> p = process.ProcessOpen(['perl', '-e', 'print $ENV{FOO}']) + >>> p.stdout.read() + '' + >>> p = process.ProcessOpen(['perl', '-e', 'print $ENV{FOO}'], + ... env={'FOO':'bar'}) + >>> p.stdout.read() + 'bar' + + Killing a long running process (On Linux, to poll you must use + p.wait(os.WNOHANG)): + >>> p = ProcessOpen(['perl', '-e', 'while (1) {}']) + >>> try: + ... p.wait(os.WNOHANG) # poll to see if is process still running + ... except ProcessError, ex: + ... if ex.errno == ProcessProxy.WAIT_TIMEOUT: + ... print "process is still running" + ... + process is still running + >>> p.kill(42) + >>> p.wait() + 42 + + Providing objects for stdin/stdout/stderr: + XXX write this, mention IOBuffer subclassing. +""" +#TODO: +# - Discuss the decision to NOT have the stdout/stderr _OutFileProxy's +# wait for process termination before closing stdin. It will just +# close stdin when stdout is seen to have been closed. That is +# considered Good Enough (tm). Theoretically it would be nice to +# only abort the stdin proxying when the process terminates, but +# watching for process termination in any of the parent's thread +# adds the undesired condition that the parent cannot exit with the +# child still running. That sucks. +# XXX Note that I don't even know if the current stdout proxy even +# closes the stdin proxy at all. +# - DavidA: if I specify "unbuffered" for my stdin handler (in the +# ProcessProxy constructor) then the stdin IOBuffer should do a +# fparent.read() rather than a fparent.readline(). TrentM: can I do +# that? What happens? +# + +import os +import sys +import threading +import types +import pprint +if sys.platform.startswith("win"): + import msvcrt + import win32api + import win32file + import win32pipe + import pywintypes + import win32process + import win32event + # constants pulled from win32con to save memory + VER_PLATFORM_WIN32_WINDOWS = 1 + CTRL_BREAK_EVENT = 1 + SW_SHOWDEFAULT = 10 + WM_CLOSE = 0x10 + DUPLICATE_SAME_ACCESS = 2 + +else: + import signal + + +#---- exceptions + +class ProcessError(Exception): + def __init__(self, msg, errno=-1): + Exception.__init__(self, msg) + self.errno = errno + + +#---- internal logging facility + +class Logger: + DEBUG, INFO, WARN, ERROR, FATAL = range(5) + def __init__(self, name, level=None, streamOrFileName=sys.stderr): + self.name = name + if level is None: + self.level = self.WARN + else: + self.level = level + if type(streamOrFileName) == types.StringType: + self.stream = open(streamOrFileName, 'w') + self._opennedStream = 1 + else: + self.stream = streamOrFileName + self._opennedStream = 0 + def __del__(self): + if self._opennedStream: + self.stream.close() + def _getLevelName(self, level): + levelNameMap = { + self.DEBUG: "DEBUG", + self.INFO: "INFO", + self.WARN: "WARN", + self.ERROR: "ERROR", + self.FATAL: "FATAL", + } + return levelNameMap[level] + def log(self, level, msg, *args): + if level < self.level: + return + message = "%s: %s:" % (self.name, self._getLevelName(level).lower()) + message = message + (msg % args) + "\n" + self.stream.write(message) + self.stream.flush() + def debug(self, msg, *args): + self.log(self.DEBUG, msg, *args) + def info(self, msg, *args): + self.log(self.INFO, msg, *args) + def warn(self, msg, *args): + self.log(self.WARN, msg, *args) + def error(self, msg, *args): + self.log(self.ERROR, msg, *args) + def fatal(self, msg, *args): + self.log(self.FATAL, msg, *args) + +# Loggers: +# - 'log' to log normal process handling +# - 'logres' to track system resource life +# - 'logfix' to track wait/kill proxying in _ThreadFixer +if 1: # normal/production usage + log = Logger("process", Logger.WARN) +else: # development/debugging usage + log = Logger("process", Logger.DEBUG, sys.stdout) +if 1: # normal/production usage + logres = Logger("process.res", Logger.WARN) +else: # development/debugging usage + logres = Logger("process.res", Logger.DEBUG, sys.stdout) +if 1: # normal/production usage + logfix = Logger("process.waitfix", Logger.WARN) +else: # development/debugging usage + logfix = Logger("process.waitfix", Logger.DEBUG, sys.stdout) + + + +#---- globals + +_version_ = (0, 5, 0) + +# List of registered processes (see _(un)registerProcess). +_processes = [] + + + +#---- internal support routines + +def _escapeArg(arg): + """Escape the given command line argument for the shell.""" + #XXX There is a probably more that we should escape here. + return arg.replace('"', r'\"') + + +def _joinArgv(argv): + r"""Join an arglist to a string appropriate for running. + + >>> import os + >>> _joinArgv(['foo', 'bar "baz']) + 'foo "bar \\"baz"' + """ + cmdstr = "" + for arg in argv: + if ' ' in arg or ';' in arg: + cmdstr += '"%s"' % _escapeArg(arg) + else: + cmdstr += _escapeArg(arg) + cmdstr += ' ' + if cmdstr.endswith(' '): cmdstr = cmdstr[:-1] # strip trailing space + return cmdstr + + +def _getPathFromEnv(env): + """Return the PATH environment variable or None. + + Do the right thing for case sensitivity per platform. + XXX Icky. This guarantee of proper case sensitivity of environment + variables should be done more fundamentally in this module. + """ + if sys.platform.startswith("win"): + for key in env.keys(): + if key.upper() == "PATH": + return env[key] + else: + return None + else: + if env.has_key("PATH"): + return env["PATH"] + else: + return None + + +def _whichFirstArg(cmd, env=None): + """Return the given command ensuring that the first arg (the command to + launch) is a full path to an existing file. + + Raise a ProcessError if no such executable could be found. + """ + # Parse out the first arg. + if cmd.startswith('"'): + # The .replace() is to ensure it does not mistakenly find the + # second '"' in, say (escaped quote): + # "C:\foo\"bar" arg1 arg2 + idx = cmd.replace('\\"', 'XX').find('"', 1) + if idx == -1: + raise ProcessError("Malformed command: %r" % cmd) + first, rest = cmd[1:idx], cmd[idx+1:] + rest = rest.lstrip() + else: + if ' ' in cmd: + first, rest = cmd.split(' ', 1) + else: + first, rest = cmd, "" + + # Ensure the first arg is a valid path to the appropriate file. + import which + if os.sep in first: + altpath = [os.path.dirname(first)] + firstbase = os.path.basename(first) + candidates = list(which.which(firstbase, path=altpath)) + elif env: + altpath = _getPathFromEnv(env) + if altpath: + candidates = list(which.which(first, altpath.split(os.pathsep))) + else: + candidates = list(which.which(first)) + else: + candidates = list(which.which(first)) + if candidates: + return _joinArgv( [candidates[0]] ) + ' ' + rest + else: + raise ProcessError("Could not find an appropriate leading command "\ + "for: %r" % cmd) + + +if sys.platform.startswith("win"): + def _SaferCreateProcess(appName, # app name + cmd, # command line + processSA, # process security attributes + threadSA, # thread security attributes + inheritHandles, # are handles are inherited + creationFlags, # creation flags + env, # environment + cwd, # current working directory + si): # STARTUPINFO pointer + """If CreateProcess fails from environment type inconsistency then + fix that and try again. + + win32process.CreateProcess requires that all environment keys and + values either be all ASCII or all unicode. Try to remove this burden + from the user of process.py. + """ + isWin9x = win32api.GetVersionEx()[3] == VER_PLATFORM_WIN32_WINDOWS + # On Win9x all keys and values of 'env' must be ASCII (XXX + # Actually this is probably only true if the Unicode support + # libraries, which are not installed by default, are not + # installed). On other Windows flavours all keys and values of + # 'env' must all be ASCII *or* all Unicode. We will try to + # automatically convert to the appropriate type, issuing a + # warning if such an automatic conversion is necessary. + + #XXX Komodo 2.0 Beta 1 hack. This requirement should be + # pushed out to Komodo code using process.py. Or should it? + if isWin9x and env: + aenv = {} + for key, value in env.items(): + aenv[str(key)] = str(value) + env = aenv + + log.debug("""\ +_SaferCreateProcess(appName=%r, + cmd=%r, + env=%r, + cwd=%r) + os.getcwd(): %r +""", appName, cmd, env, cwd, os.getcwd()) + try: + hProcess, hThread, processId, threadId\ + = win32process.CreateProcess(appName, cmd, processSA, + threadSA, inheritHandles, + creationFlags, env, cwd, si) + except TypeError, ex: + if ex.args == ('All dictionary items must be strings, or all must be unicode',): + # Try again with an all unicode environment. + #XXX Would be nice if didn't have to depend on the error + # string to catch this. + #XXX Removing this warning for 2.3 release. See bug + # 23215. The right fix is to correct the PHPAppInfo + # stuff to heed the warning. + #import warnings + #warnings.warn('env: ' + str(ex), stacklevel=4) + if isWin9x and env: + aenv = {} + try: + for key, value in env.items(): + aenv[str(key)] = str(value) + except UnicodeError, ex: + raise ProcessError(str(ex)) + env = aenv + elif env: + uenv = {} + for key, val in env.items(): + uenv[unicode(key)] = unicode(val) + env = uenv + hProcess, hThread, processId, threadId\ + = win32process.CreateProcess(appName, cmd, processSA, + threadSA, inheritHandles, + creationFlags, env, cwd, + si) + else: + raise + return hProcess, hThread, processId, threadId + + +# Maintain references to all spawned ProcessProxy objects to avoid hangs. +# Otherwise, if the user lets the a ProcessProxy object go out of +# scope before the process has terminated, it is possible to get a +# hang (at least it *used* to be so when we had the +# win32api.CloseHandle(<stdin handle>) call in the __del__() method). +# XXX Is this hang possible on Linux as well? +# A reference is removed from this list when the process's .wait or +# .kill method is called. +# XXX Should an atexit() handler be registered to kill all curently +# running processes? Else *could* get hangs, n'est ce pas? +def _registerProcess(process): + global _processes + log.info("_registerprocess(process=%r)", process) + + # Clean up zombie processes. + # If the user does not call .wait() or .kill() on processes then + # the ProcessProxy object will not get cleaned up until Python + # exits and _processes goes out of scope. Under heavy usage that + # is a big memory waste. Cleaning up here alleviates that. + for p in _processes[:]: # use copy of _process, because we may modifiy it + try: + # poll to see if is process still running + if sys.platform.startswith("win"): + timeout = 0 + else: + timeout = os.WNOHANG + p.wait(timeout) + _unregisterProcess(p) + except ProcessError, ex: + if ex.errno == ProcessProxy.WAIT_TIMEOUT: + pass + else: + raise + + _processes.append(process) + +def _unregisterProcess(process): + global _processes + log.info("_unregisterProcess(process=%r)", process) + try: + _processes.remove(process) + del process + except ValueError: + pass + + +def _fixupCommand(cmd, env=None): + """Fixup the command string so it is launchable via CreateProcess. + + One cannot just launch, say "python", via CreateProcess. A full path + to an executable is required. In general there are two choices: + 1. Launch the command string via the shell. The shell will find + the fullpath to the appropriate executable. This shell will + also be able to execute special shell commands, like "dir", + which don't map to an actual executable. + 2. Find the fullpath to the appropriate executable manually and + launch that exe. + + Option (1) is preferred because you don't have to worry about not + exactly duplicating shell behaviour and you get the added bonus of + being able to launch "dir" and friends. + + However, (1) is not always an option. Doing so when the shell is + command.com (as on all Win9x boxes) or when using WinNT's cmd.exe, + problems are created with .kill() because these shells seem to eat + up Ctrl-C's and Ctrl-Break's sent via + win32api.GenerateConsoleCtrlEvent(). Strangely this only happens + when spawn via this Python interface. For example, Ctrl-C get + through to hang.exe here: + C:\> ...\w9xpopen.exe "C:\WINDOWS\COMMAND.COM /c hang.exe" + ^C + but not here: + >>> p = ProcessOpen('hang.exe') + # This results in the same command to CreateProcess as + # above. + >>> p.kill() + + Hence, for these platforms we fallback to option (2). Cons: + - cannot spawn shell commands like 'dir' directly + - cannot spawn batch files + """ + if sys.platform.startswith("win"): + # Fixup the command string to spawn. (Lifted from + # posixmodule.c::_PyPopenCreateProcess() with some modifications) + comspec = os.environ.get("COMSPEC", None) + win32Version = win32api.GetVersion() + if comspec is None: + raise ProcessError("Cannot locate a COMSPEC environment "\ + "variable to use as the shell") + # Explicitly check if we are using COMMAND.COM. If we + # are then use the w9xpopen hack. + elif (win32Version & 0x80000000L == 0) and\ + (win32Version & 0x5L >= 5) and\ + os.path.basename(comspec).lower() != "command.com": + # 2000/XP and not using command.com. + if '"' in cmd or "'" in cmd: + cmd = comspec + ' /c "%s"' % cmd + else: + cmd = comspec + ' /c ' + cmd + elif (win32Version & 0x80000000L == 0) and\ + (win32Version & 0x5L < 5) and\ + os.path.basename(comspec).lower() != "command.com": + # NT and not using command.com. + try: + cmd = _whichFirstArg(cmd, env) + except ProcessError: + raise ProcessError("Could not find a suitable executable "\ + "to launch for '%s'. On WinNT you must manually prefix "\ + "shell commands and batch files with 'cmd.exe /c' to "\ + "have the shell run them." % cmd) + else: + # Oh gag, we're on Win9x and/or using COMMAND.COM. Use the + # workaround listed in KB: Q150956 + w9xpopen = os.path.join( + os.path.dirname(win32api.GetModuleFileName(0)), + 'w9xpopen.exe') + if not os.path.exists(w9xpopen): + # Eeek - file-not-found - possibly an embedding + # situation - see if we can locate it in sys.exec_prefix + w9xpopen = os.path.join(os.path.dirname(sys.exec_prefix), + 'w9xpopen.exe') + if not os.path.exists(w9xpopen): + raise ProcessError(\ + "Can not locate 'w9xpopen.exe' which is needed "\ + "for ProcessOpen to work with your shell or "\ + "platform.") + ## This would be option (1): + #cmd = '%s "%s /c %s"'\ + # % (w9xpopen, comspec, cmd.replace('"', '\\"')) + try: + cmd = _whichFirstArg(cmd, env) + except ProcessError: + raise ProcessError("Could not find a suitable executable "\ + "to launch for '%s'. On Win9x you must manually prefix "\ + "shell commands and batch files with 'command.com /c' "\ + "to have the shell run them." % cmd) + cmd = '%s "%s"' % (w9xpopen, cmd.replace('"', '\\"')) + return cmd + +class _FileWrapper: + """Wrap a system file object, hiding some nitpicky details. + + This class provides a Python file-like interface to either a Python + file object (pretty easy job), a file descriptor, or an OS-specific + file handle (e.g. Win32 handles to file objects on Windows). Any or + all of these object types may be passed to this wrapper. If more + than one is specified this wrapper prefers to work with certain one + in this order: + - file descriptor (because usually this allows for + return-immediately-on-read-if-anything-available semantics and + also provides text mode translation on Windows) + - OS-specific handle (allows for the above read semantics) + - file object (buffering can cause difficulty for interacting + with spawned programs) + + It also provides a place where related such objects can be kept + alive together to prevent premature ref-counted collection. (E.g. on + Windows a Python file object may be associated with a Win32 file + handle. If the file handle is not kept alive the Python file object + will cease to function.) + """ + def __init__(self, file=None, descriptor=None, handle=None): + self._file = file + self._descriptor = descriptor + self._handle = handle + self._closed = 0 + if self._descriptor is not None or self._handle is not None: + self._lineBuf = "" # to support .readline() + + def __del__(self): + self.close() + + def __getattr__(self, name): + """Forward to the underlying file object.""" + if self._file is not None: + return getattr(self._file, name) + else: + raise ProcessError("no file object to pass '%s' attribute to" + % name) + + def _win32Read(self, nBytes): + try: + log.info("[%s] _FileWrapper.read: waiting for read on pipe", + id(self)) + errCode, text = win32file.ReadFile(self._handle, nBytes) + except pywintypes.error, ex: + # Ignore errors for now, like "The pipe is being closed.", + # etc. XXX There *may* be errors we don't want to avoid. + log.info("[%s] _FileWrapper.read: error reading from pipe: %s", + id(self), ex) + return "" + assert errCode == 0,\ + "Why is 'errCode' from ReadFile non-zero? %r" % errCode + if not text: + # Empty text signifies that the pipe has been closed on + # the parent's end. + log.info("[%s] _FileWrapper.read: observed close of parent", + id(self)) + # Signal the child so it knows to stop listening. + self.close() + return "" + else: + log.info("[%s] _FileWrapper.read: read %d bytes from pipe: %r", + id(self), len(text), text) + return text + + def read(self, nBytes=-1): + # nBytes <= 0 means "read everything" + # Note that we are changing the "read everything" cue to + # include 0, because actually doing + # win32file.ReadFile(<handle>, 0) results in every subsequent + # read returning 0, i.e. it shuts down the pipe. + if self._descriptor is not None: + if nBytes <= 0: + text, self._lineBuf = self._lineBuf, "" + while 1: + t = os.read(self._descriptor, 4092) + if not t: + break + else: + text += t + else: + if len(self._lineBuf) >= nBytes: + text, self._lineBuf =\ + self._lineBuf[:nBytes], self._lineBuf[nBytes:] + else: + nBytesToGo = nBytes - len(self._lineBuf) + text = self._lineBuf + os.read(self._descriptor, + nBytesToGo) + self._lineBuf = "" + return text + elif self._handle is not None: + if nBytes <= 0: + text, self._lineBuf = self._lineBuf, "" + while 1: + t = self._win32Read(4092) + if not t: + break + else: + text += t + else: + if len(self._lineBuf) >= nBytes: + text, self._lineBuf =\ + self._lineBuf[:nBytes], self._lineBuf[nBytes:] + else: + nBytesToGo = nBytes - len(self._lineBuf) + text, self._lineBuf =\ + self._lineBuf + self._win32Read(nBytesToGo), "" + return text + elif self._file is not None: + return self._file.read(nBytes) + else: + raise "FileHandle.read: no handle to read with" + + def readline(self): + if self._descriptor is not None or self._handle is not None: + while 1: + #XXX This is not portable to the Mac. + idx = self._lineBuf.find('\n') + if idx != -1: + line, self._lineBuf =\ + self._lineBuf[:idx+1], self._lineBuf[idx+1:] + break + else: + lengthBefore = len(self._lineBuf) + t = self.read(4092) + if len(t) <= lengthBefore: # no new data was read + line, self._lineBuf = self._lineBuf, "" + break + else: + self._lineBuf += t + return line + elif self._file is not None: + return self._file.readline() + else: + raise "FileHandle.readline: no handle to read with" + + def readlines(self): + if self._descriptor is not None or self._handle is not None: + lines = [] + while 1: + line = self.readline() + if line: + lines.append(line) + else: + break + return lines + elif self._file is not None: + return self._file.readlines() + else: + raise "FileHandle.readline: no handle to read with" + + def write(self, text): + if self._descriptor is not None: + os.write(self._descriptor, text) + elif self._handle is not None: + try: + errCode, nBytesWritten = win32file.WriteFile(self._handle, text) + except pywintypes.error, ex: + # Ingore errors like "The pipe is being closed.", for + # now. + log.info("[%s] _FileWrapper.write: error writing to pipe, "\ + "ignored", id(self)) + return + assert errCode == 0,\ + "Why is 'errCode' from WriteFile non-zero? %r" % errCode + if not nBytesWritten: + # No bytes written signifies that the pipe has been + # closed on the child's end. + log.info("[%s] _FileWrapper.write: observed close of pipe", + id(self)) + return + else: + log.info("[%s] _FileWrapper.write: wrote %d bytes to pipe: %r", + id(self), len(text), text) + elif self._file is not None: + self._file.write(text) + else: + raise "FileHandle.write: nothing to write with" + + def close(self): + """Close all associated file objects and handles.""" + log.debug("[%s] _FileWrapper.close()", id(self)) + if not self._closed: + self._closed = 1 + if self._file is not None: + log.debug("[%s] _FileWrapper.close: close file", id(self)) + self._file.close() + log.debug("[%s] _FileWrapper.close: done file close", id(self)) + if self._descriptor is not None: + try: + os.close(self._descriptor) + except OSError, ex: + if ex.errno == 9: + # Ignore: OSError: [Errno 9] Bad file descriptor + # XXX *Should* we be ignoring this? It appears very + # *in*frequently in test_wait.py. + log.debug("[%s] _FileWrapper.close: closing "\ + "descriptor raised OSError", id(self)) + else: + raise + if self._handle is not None: + log.debug("[%s] _FileWrapper.close: close handle", id(self)) + try: + win32api.CloseHandle(self._handle) + except win32api.error: + log.debug("[%s] _FileWrapper.close: closing handle raised", + id(self)) + pass + log.debug("[%s] _FileWrapper.close: done closing handle", + id(self)) + + def __repr__(self): + return "<_FileWrapper: file:%r fd:%r os_handle:%r>"\ + % (self._file, self._descriptor, self._handle) + + +class _CountingCloser: + """Call .close() on the given object after own .close() is called + the precribed number of times. + """ + def __init__(self, objectsToClose, count): + """ + "objectsToClose" is a list of object on which to call .close(). + "count" is the number of times this object's .close() method + must be called before .close() is called on the given objects. + """ + self.objectsToClose = objectsToClose + self.count = count + if self.count <= 0: + raise ProcessError("illegal 'count' value: %s" % self.count) + + def close(self): + self.count -= 1 + log.debug("[%d] _CountingCloser.close(): count=%d", id(self), + self.count) + if self.count == 0: + for objectToClose in self.objectsToClose: + objectToClose.close() + + + +#---- public interface + +class Process: + """Create a process. + + One can optionally specify the starting working directory, the + process environment, and std handles to have the child process + inherit (all defaults are the parent's current settings). 'wait' and + 'kill' method allow for control of the child's termination. + """ + # TODO: + # - Rename this or merge it with ProcessOpen somehow. + # + if sys.platform.startswith("win"): + # .wait() argument constants + INFINITE = win32event.INFINITE + # .wait() return error codes + WAIT_FAILED = win32event.WAIT_FAILED + WAIT_TIMEOUT = win32event.WAIT_TIMEOUT + # creation "flags" constants + # XXX Should drop these and just document usage of + # win32process.CREATE_* constants on windows. + CREATE_NEW_CONSOLE = win32process.CREATE_NEW_CONSOLE + else: + # .wait() argument constants + INFINITE = 0 + # .wait() return error codes + WAIT_TIMEOUT = 258 + WAIT_FAILED = -1 + # creation "flags" constants + CREATE_NEW_CONSOLE = 0x10 # same as win32process.CREATE_NEW_CONSOLE + + def __init__(self, cmd, cwd=None, env=None, flags=0): + """Create a child process. + + "cmd" is a command string or argument vector to spawn. + "cwd" is a working directory in which to start the child process. + "env" is an environment dictionary for the child. + "flags" are system-specific process creation flags. On Windows + this can be a bitwise-OR of any of the win32process.CREATE_* + constants (Note: win32process.CREATE_NEW_PROCESS_GROUP is always + OR'd in). On Unix, this is currently ignored. + """ + log.info("Process.__init__(cmd=%r, cwd=%r, env=%r, flags=%r)", + cmd, cwd, env, flags) + self._cmd = cmd + if not self._cmd: + raise ProcessError("You must specify a command.") + self._cwd = cwd + self._env = env + self._flags = flags + if sys.platform.startswith("win"): + self._flags |= win32process.CREATE_NEW_PROCESS_GROUP + + if sys.platform.startswith("win"): + self._startOnWindows() + else: + self.__retvalCache = None + self._startOnUnix() + + def _runChildOnUnix(self): + #XXX Errors running the child do *not* get communicated back. + + #XXX Perhaps we should *always* prefix with '/bin/sh -c'? There is a + # disparity btwn how this works on Linux and Windows. + if isinstance(self._cmd, types.StringTypes): + # This is easier than trying to reproduce shell interpretation to + # separate the arguments. + cmd = ['/bin/sh', '-c', self._cmd] + else: + cmd = self._cmd + + # Close all file descriptors (except std*) inherited from the parent. + MAXFD = 256 # Max number of file descriptors (os.getdtablesize()???) + for i in range(3, MAXFD): + try: + os.close(i) + except OSError: + pass + + try: + if self._env: + os.execvpe(cmd[0], cmd, self._env) + else: + os.execvp(cmd[0], cmd) + finally: + os._exit(1) # Should never get here. + + def _forkAndExecChildOnUnix(self): + """Fork and start the child process. + + Sets self._pid as a side effect. + """ + pid = os.fork() + if pid == 0: # child + self._runChildOnUnix() + # parent + self._pid = pid + + def _startOnUnix(self): + if self._cwd: + oldDir = os.getcwd() + try: + os.chdir(self._cwd) + except OSError, ex: + raise ProcessError(msg=str(ex), errno=ex.errno) + self._forkAndExecChildOnUnix() + + # parent + if self._cwd: + os.chdir(oldDir) + + def _startOnWindows(self): + if type(self._cmd) in (types.ListType, types.TupleType): + # And arg vector was passed in. + cmd = _joinArgv(self._cmd) + else: + cmd = self._cmd + + si = win32process.STARTUPINFO() + si.dwFlags = win32process.STARTF_USESHOWWINDOW + si.wShowWindow = SW_SHOWDEFAULT + + if not (self._flags & self.CREATE_NEW_CONSOLE): + #XXX This is hacky. + # We cannot then use _fixupCommand because this will cause a + # shell to be openned as the command is launched. Therefore need + # to ensure be have the full path to the executable to launch. + try: + cmd = _whichFirstArg(cmd, self._env) + except ProcessError: + # Could not find the command, perhaps it is an internal + # shell command -- fallback to _fixupCommand + cmd = _fixupCommand(cmd, self._env) + else: + cmd = _fixupCommand(cmd, self._env) + log.debug("cmd = %r", cmd) + + # Start the child process. + try: + self._hProcess, self._hThread, self._processId, self._threadId\ + = _SaferCreateProcess( + None, # app name + cmd, # command line + None, # process security attributes + None, # primary thread security attributes + 0, # handles are inherited + self._flags, # creation flags + self._env, # environment + self._cwd, # current working directory + si) # STARTUPINFO pointer + win32api.CloseHandle(self._hThread) + except win32api.error, ex: + raise ProcessError(msg="Error creating process for '%s': %s"\ + % (cmd, ex.args[2]), + errno=ex.args[0]) + + def wait(self, timeout=None): + """Wait for the started process to complete. + + "timeout" (on Windows) is a floating point number of seconds after + which to timeout. Default is win32event.INFINITE. + "timeout" (on Unix) is akin to the os.waitpid() "options" argument + (os.WNOHANG may be used to return immediately if the process has + not exited). Default is 0, i.e. wait forever. + + If the wait time's out it will raise a ProcessError. Otherwise it + will return the child's exit value (on Windows) or the child's exit + status excoded as per os.waitpid() (on Linux): + "a 16-bit number, whose low byte is the signal number that killed + the process, and whose high byte is the exit status (if the + signal number is zero); the high bit of the low byte is set if a + core file was produced." + In the latter case, use the os.W*() methods to interpret the return + value. + """ + # XXX Or should returning the exit value be move out to another + # function as on Win32 process control? If so, then should + # perhaps not make WaitForSingleObject semantic transformation. + if sys.platform.startswith("win"): + if timeout is None: + timeout = win32event.INFINITE + else: + timeout = timeout * 1000.0 # Win32 API's timeout is in millisecs + + rc = win32event.WaitForSingleObject(self._hProcess, timeout) + if rc == win32event.WAIT_FAILED: + raise ProcessError("'WAIT_FAILED' when waiting for process to "\ + "terminate: %r" % self._cmd, rc) + elif rc == win32event.WAIT_TIMEOUT: + raise ProcessError("'WAIT_TIMEOUT' when waiting for process to "\ + "terminate: %r" % self._cmd, rc) + + retval = win32process.GetExitCodeProcess(self._hProcess) + else: + # os.waitpid() will raise: + # OSError: [Errno 10] No child processes + # on subsequent .wait() calls. Change these semantics to have + # subsequent .wait() calls return the exit status and return + # immediately without raising an exception. + # (XXX It would require synchronization code to handle the case + # of multiple simultaneous .wait() requests, however we can punt + # on that because it is moot while Linux still has the problem + # for which _ThreadFixer() exists.) + if self.__retvalCache is not None: + retval = self.__retvalCache + else: + if timeout is None: + timeout = 0 + pid, sts = os.waitpid(self._pid, timeout) + if pid == self._pid: + self.__retvalCache = retval = sts + else: + raise ProcessError("Wait for process timed out.", + self.WAIT_TIMEOUT) + return retval + + def kill(self, exitCode=0, gracePeriod=1.0, sig=None): + """Kill process. + + "exitCode" [deprecated, not supported] (Windows only) is the + code the terminated process should exit with. + "gracePeriod" (Windows only) is a number of seconds the process is + allowed to shutdown with a WM_CLOSE signal before a hard + terminate is called. + "sig" (Unix only) is the signal to use to kill the process. Defaults + to signal.SIGKILL. See os.kill() for more information. + + Windows: + Try for an orderly shutdown via WM_CLOSE. If still running + after gracePeriod (1 sec. default), terminate. + """ + if sys.platform.startswith("win"): + import win32gui + # Send WM_CLOSE to windows in this process group. + win32gui.EnumWindows(self._close_, 0) + + # Send Ctrl-Break signal to all processes attached to this + # console. This is supposed to trigger shutdown handlers in + # each of the processes. + try: + win32api.GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, + self._processId) + except AttributeError: + log.warn("The win32api module does not have "\ + "GenerateConsoleCtrlEvent(). This may mean that "\ + "parts of this process group have NOT been killed.") + except win32api.error, ex: + if ex.args[0] not in (6, 87): + # Ignore the following: + # api_error: (87, 'GenerateConsoleCtrlEvent', 'The parameter is incorrect.') + # api_error: (6, 'GenerateConsoleCtrlEvent', 'The handle is invalid.') + # Get error 6 if there is no console. + raise + + # Last resort: call TerminateProcess if it has not yet. + retval = 0 + try: + self.wait(gracePeriod) + except ProcessError, ex: + log.info("[%s] Process.kill: calling TerminateProcess", id(self)) + win32process.TerminateProcess(self._hProcess, -1) + win32api.Sleep(100) # wait for resources to be released + + else: + if sig is None: + sig = signal.SIGKILL + try: + os.kill(self._pid, sig) + except OSError, ex: + if ex.errno != 3: + # Ignore: OSError: [Errno 3] No such process + raise + + def _close_(self, hwnd, dummy): + """Callback used by .kill() on Windows. + + EnumWindows callback - sends WM_CLOSE to any window owned by this + process. + """ + threadId, processId = win32process.GetWindowThreadProcessId(hwnd) + if processId == self._processId: + import win32gui + win32gui.PostMessage(hwnd, WM_CLOSE, 0, 0) + + +class ProcessOpen(Process): + """Create a process and setup pipes to it standard handles. + + This is a super popen3. + """ + # TODO: + # - Share some implementation with Process and ProcessProxy. + # + + def __init__(self, cmd, mode='t', cwd=None, env=None): + """Create a Process with proxy threads for each std handle. + + "cmd" is the command string or argument vector to run. + "mode" (Windows only) specifies whether the pipes used to communicate + with the child are openned in text, 't', or binary, 'b', mode. + This is ignored on platforms other than Windows. Default is 't'. + "cwd" optionally specifies the directory in which the child process + should be started. Default is None, a.k.a. inherits the cwd from + the parent. + "env" is optionally a mapping specifying the environment in which to + start the child. Default is None, a.k.a. inherits the environment + of the parent. + """ + # Keep a reference to ensure it is around for this object's destruction. + self.__log = log + log.info("ProcessOpen.__init__(cmd=%r, mode=%r, cwd=%r, env=%r)", + cmd, mode, cwd, env) + self._cmd = cmd + if not self._cmd: + raise ProcessError("You must specify a command.") + self._cwd = cwd + self._env = env + self._mode = mode + if self._mode not in ('t', 'b'): + raise ProcessError("'mode' must be 't' or 'b'.") + self._closed = 0 + + if sys.platform.startswith("win"): + self._startOnWindows() + else: + self.__retvalCache = None + self._startOnUnix() + + _registerProcess(self) + + def __del__(self): + #XXX Should probably not rely upon this. + logres.info("[%s] ProcessOpen.__del__()", id(self)) + self.close() + del self.__log # drop reference + + def close(self): + if not self._closed: + self.__log.info("[%s] ProcessOpen.close()" % id(self)) + + # Ensure that all IOBuffer's are closed. If they are not, these + # can cause hangs. + try: + self.__log.info("[%s] ProcessOpen: closing stdin (%r)."\ + % (id(self), self.stdin)) + self.stdin.close() + except AttributeError: + # May not have gotten far enough in the __init__ to set + # self.stdin, etc. + pass + try: + self.__log.info("[%s] ProcessOpen: closing stdout (%r)."\ + % (id(self), self.stdout)) + self.stdout.close() + except AttributeError: + # May not have gotten far enough in the __init__ to set + # self.stdout, etc. + pass + try: + self.__log.info("[%s] ProcessOpen: closing stderr (%r)."\ + % (id(self), self.stderr)) + self.stderr.close() + except AttributeError: + # May not have gotten far enough in the __init__ to set + # self.stderr, etc. + pass + + self._closed = 1 + + def _forkAndExecChildOnUnix(self, fdChildStdinRd, fdChildStdoutWr, + fdChildStderrWr): + """Fork and start the child process. + + Sets self._pid as a side effect. + """ + pid = os.fork() + if pid == 0: # child + os.dup2(fdChildStdinRd, 0) + os.dup2(fdChildStdoutWr, 1) + os.dup2(fdChildStderrWr, 2) + self._runChildOnUnix() + # parent + self._pid = pid + + def _startOnUnix(self): + # Create pipes for std handles. + fdChildStdinRd, fdChildStdinWr = os.pipe() + fdChildStdoutRd, fdChildStdoutWr = os.pipe() + fdChildStderrRd, fdChildStderrWr = os.pipe() + + if self._cwd: + oldDir = os.getcwd() + try: + os.chdir(self._cwd) + except OSError, ex: + raise ProcessError(msg=str(ex), errno=ex.errno) + self._forkAndExecChildOnUnix(fdChildStdinRd, fdChildStdoutWr, + fdChildStderrWr) + if self._cwd: + os.chdir(oldDir) + + os.close(fdChildStdinRd) + os.close(fdChildStdoutWr) + os.close(fdChildStderrWr) + + self.stdin = _FileWrapper(descriptor=fdChildStdinWr) + logres.info("[%s] ProcessOpen._start(): create child stdin: %r", + id(self), self.stdin) + self.stdout = _FileWrapper(descriptor=fdChildStdoutRd) + logres.info("[%s] ProcessOpen._start(): create child stdout: %r", + id(self), self.stdout) + self.stderr = _FileWrapper(descriptor=fdChildStderrRd) + logres.info("[%s] ProcessOpen._start(): create child stderr: %r", + id(self), self.stderr) + + def _startOnWindows(self): + if type(self._cmd) in (types.ListType, types.TupleType): + # An arg vector was passed in. + cmd = _joinArgv(self._cmd) + else: + cmd = self._cmd + + # Create pipes for std handles. + # (Set the bInheritHandle flag so pipe handles are inherited.) + saAttr = pywintypes.SECURITY_ATTRIBUTES() + saAttr.bInheritHandle = 1 + #XXX Should maybe try with os.pipe. Dunno what that does for + # inheritability though. + hChildStdinRd, hChildStdinWr = win32pipe.CreatePipe(saAttr, 0) + hChildStdoutRd, hChildStdoutWr = win32pipe.CreatePipe(saAttr, 0) + hChildStderrRd, hChildStderrWr = win32pipe.CreatePipe(saAttr, 0) + + try: + # Duplicate the parent ends of the pipes so they are not + # inherited. + hChildStdinWrDup = win32api.DuplicateHandle( + win32api.GetCurrentProcess(), + hChildStdinWr, + win32api.GetCurrentProcess(), + 0, + 0, # not inherited + DUPLICATE_SAME_ACCESS) + win32api.CloseHandle(hChildStdinWr) + self._hChildStdinWr = hChildStdinWrDup + hChildStdoutRdDup = win32api.DuplicateHandle( + win32api.GetCurrentProcess(), + hChildStdoutRd, + win32api.GetCurrentProcess(), + 0, + 0, # not inherited + DUPLICATE_SAME_ACCESS) + win32api.CloseHandle(hChildStdoutRd) + self._hChildStdoutRd = hChildStdoutRdDup + hChildStderrRdDup = win32api.DuplicateHandle( + win32api.GetCurrentProcess(), + hChildStderrRd, + win32api.GetCurrentProcess(), + 0, + 0, # not inherited + DUPLICATE_SAME_ACCESS) + win32api.CloseHandle(hChildStderrRd) + self._hChildStderrRd = hChildStderrRdDup + + # Set the translation mode and buffering. + if self._mode == 't': + flags = os.O_TEXT + else: + flags = 0 + fdChildStdinWr = msvcrt.open_osfhandle(self._hChildStdinWr, flags) + fdChildStdoutRd = msvcrt.open_osfhandle(self._hChildStdoutRd, flags) + fdChildStderrRd = msvcrt.open_osfhandle(self._hChildStderrRd, flags) + + self.stdin = _FileWrapper(descriptor=fdChildStdinWr, + handle=self._hChildStdinWr) + logres.info("[%s] ProcessOpen._start(): create child stdin: %r", + id(self), self.stdin) + self.stdout = _FileWrapper(descriptor=fdChildStdoutRd, + handle=self._hChildStdoutRd) + logres.info("[%s] ProcessOpen._start(): create child stdout: %r", + id(self), self.stdout) + self.stderr = _FileWrapper(descriptor=fdChildStderrRd, + handle=self._hChildStderrRd) + logres.info("[%s] ProcessOpen._start(): create child stderr: %r", + id(self), self.stderr) + + # Start the child process. + si = win32process.STARTUPINFO() + si.dwFlags = win32process.STARTF_USESHOWWINDOW + si.wShowWindow = 0 # SW_HIDE + si.hStdInput = hChildStdinRd + si.hStdOutput = hChildStdoutWr + si.hStdError = hChildStderrWr + si.dwFlags |= win32process.STARTF_USESTDHANDLES + + cmd = _fixupCommand(cmd, self._env) + + creationFlags = win32process.CREATE_NEW_PROCESS_GROUP + try: + self._hProcess, hThread, self._processId, threadId\ + = _SaferCreateProcess( + None, # app name + cmd, # command line + None, # process security attributes + None, # primary thread security attributes + 1, # handles are inherited + creationFlags, # creation flags + self._env, # environment + self._cwd, # current working directory + si) # STARTUPINFO pointer + except win32api.error, ex: + raise ProcessError(msg=ex.args[2], errno=ex.args[0]) + win32api.CloseHandle(hThread) + + finally: + # Close child ends of pipes on the parent's side (the + # parent's ends of the pipe are closed in the _FileWrappers.) + win32file.CloseHandle(hChildStdinRd) + win32file.CloseHandle(hChildStdoutWr) + win32file.CloseHandle(hChildStderrWr) + + def wait(self, timeout=None): + """Wait for the started process to complete. + + "timeout" (on Windows) is a floating point number of seconds after + which to timeout. Default is win32event.INFINITE. + "timeout" (on Unix) is akin to the os.waitpid() "options" argument + (os.WNOHANG may be used to return immediately if the process has + not exited). Default is 0, i.e. wait forever. + + If the wait time's out it will raise a ProcessError. Otherwise it + will return the child's exit value (on Windows) or the child's exit + status excoded as per os.waitpid() (on Linux): + "a 16-bit number, whose low byte is the signal number that killed + the process, and whose high byte is the exit status (if the + signal number is zero); the high bit of the low byte is set if a + core file was produced." + In the latter case, use the os.W*() methods to interpret the return + value. + """ + # XXX Or should returning the exit value be move out to another + # function as on Win32 process control? If so, then should + # perhaps not make WaitForSingleObject semantic + # transformation. + # TODO: + # - Need to rationalize the .wait() API for Windows vs. Unix. + # It is a real pain in the current situation. + if sys.platform.startswith("win"): + if timeout is None: + timeout = win32event.INFINITE + else: + timeout = timeout * 1000.0 # Win32 API's timeout is in millisecs + + #rc = win32event.WaitForSingleObject(self._hProcess, timeout) + rc = win32event.WaitForSingleObject(self._hProcess, int(timeout)) # MATT -- Making timeout an integer + if rc == win32event.WAIT_FAILED: + raise ProcessError("'WAIT_FAILED' when waiting for process to "\ + "terminate: %r" % self._cmd, rc) + elif rc == win32event.WAIT_TIMEOUT: + raise ProcessError("'WAIT_TIMEOUT' when waiting for process to "\ + "terminate: %r" % self._cmd, rc) + + retval = win32process.GetExitCodeProcess(self._hProcess) + else: + # os.waitpid() will raise: + # OSError: [Errno 10] No child processes + # on subsequent .wait() calls. Change these semantics to have + # subsequent .wait() calls return the exit status and return + # immediately without raising an exception. + # (XXX It would require synchronization code to handle the case + # of multiple simultaneous .wait() requests, however we can punt + # on that because it is moot while Linux still has the problem + # for which _ThreadFixer() exists.) + if self.__retvalCache is not None: + retval = self.__retvalCache + else: + if timeout is None: + timeout = 0 + pid, sts = os.waitpid(self._pid, timeout) + if pid == self._pid: + self.__retvalCache = retval = sts + else: + raise ProcessError("Wait for process timed out.", + self.WAIT_TIMEOUT) + _unregisterProcess(self) + return retval + + def kill(self, exitCode=0, gracePeriod=1.0, sig=None): + """Kill process. + + "exitCode" [deprecated, not supported] (Windows only) is the + code the terminated process should exit with. + "gracePeriod" (Windows only) is a number of seconds the process is + allowed to shutdown with a WM_CLOSE signal before a hard + terminate is called. + "sig" (Unix only) is the signal to use to kill the process. Defaults + to signal.SIGKILL. See os.kill() for more information. + + Windows: + Try for an orderly shutdown via WM_CLOSE. If still running + after gracePeriod (1 sec. default), terminate. + """ + if sys.platform.startswith("win"): + import win32gui + # Send WM_CLOSE to windows in this process group. + win32gui.EnumWindows(self._close_, 0) + + # Send Ctrl-Break signal to all processes attached to this + # console. This is supposed to trigger shutdown handlers in + # each of the processes. + try: + win32api.GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, + self._processId) + except AttributeError: + log.warn("The win32api module does not have "\ + "GenerateConsoleCtrlEvent(). This may mean that "\ + "parts of this process group have NOT been killed.") + except win32api.error, ex: + if ex.args[0] not in (6, 87): + # Ignore the following: + # api_error: (87, 'GenerateConsoleCtrlEvent', 'The parameter is incorrect.') + # api_error: (6, 'GenerateConsoleCtrlEvent', 'The handle is invalid.') + # Get error 6 if there is no console. + raise + + # Last resort: call TerminateProcess if it has not yet. + retval = 0 + try: + self.wait(gracePeriod) + except ProcessError, ex: + log.info("[%s] Process.kill: calling TerminateProcess", id(self)) + win32process.TerminateProcess(self._hProcess, -1) + win32api.Sleep(100) # wait for resources to be released + + else: + if sig is None: + sig = signal.SIGKILL + try: + os.kill(self._pid, sig) + except OSError, ex: + if ex.errno != 3: + # Ignore: OSError: [Errno 3] No such process + raise + + _unregisterProcess(self) + + def _close_(self, hwnd, dummy): + """Callback used by .kill() on Windows. + + EnumWindows callback - sends WM_CLOSE to any window owned by this + process. + """ + threadId, processId = win32process.GetWindowThreadProcessId(hwnd) + if processId == self._processId: + import win32gui + win32gui.PostMessage(hwnd, WM_CLOSE, 0, 0) + + +class ProcessProxy(Process): + """Create a process and proxy communication via the standard handles. + """ + #XXX To add to docstring: + # - stdout/stderr proxy handling + # - stdin proxy handling + # - termination + # - how to .start(), i.e. basic usage rules + # - mention that pased in stdin/stdout/stderr objects have to + # implement at least .write (is .write correct for stdin)? + # - if you pass in stdin, stdout, and/or stderr streams it is the + # user's responsibility to close them afterwards. + # - 'cmd' arg can be a command string or an arg vector + # - etc. + #TODO: + # - .suspend() and .resume()? See Win32::Process Perl module. + # + def __init__(self, cmd, mode='t', cwd=None, env=None, + stdin=None, stdout=None, stderr=None): + """Create a Process with proxy threads for each std handle. + + "cmd" is the command string or argument vector to run. + "mode" (Windows only) specifies whether the pipes used to communicate + with the child are openned in text, 't', or binary, 'b', mode. + This is ignored on platforms other than Windows. Default is 't'. + "cwd" optionally specifies the directory in which the child process + should be started. Default is None, a.k.a. inherits the cwd from + the parent. + "env" is optionally a mapping specifying the environment in which to + start the child. Default is None, a.k.a. inherits the environment + of the parent. + "stdin", "stdout", "stderr" can be used to specify objects with + file-like interfaces to handle read (stdout/stderr) and write + (stdin) events from the child. By default a process.IOBuffer + instance is assigned to each handler. IOBuffer may be + sub-classed. See the IOBuffer doc string for more information. + """ + # Keep a reference to ensure it is around for this object's destruction. + self.__log = log + log.info("ProcessProxy.__init__(cmd=%r, mode=%r, cwd=%r, env=%r, "\ + "stdin=%r, stdout=%r, stderr=%r)", + cmd, mode, cwd, env, stdin, stdout, stderr) + self._cmd = cmd + if not self._cmd: + raise ProcessError("You must specify a command.") + self._mode = mode + if self._mode not in ('t', 'b'): + raise ProcessError("'mode' must be 't' or 'b'.") + self._cwd = cwd + self._env = env + if stdin is None: + self.stdin = IOBuffer(name='<stdin>') + else: + self.stdin = stdin + if stdout is None: + self.stdout = IOBuffer(name='<stdout>') + else: + self.stdout = stdout + if stderr is None: + self.stderr = IOBuffer(name='<stderr>') + else: + self.stderr = stderr + self._closed = 0 + + if sys.platform.startswith("win"): + self._startOnWindows() + else: + self.__retvalCache = None + self._startOnUnix() + + _registerProcess(self) + + def __del__(self): + #XXX Should probably not rely upon this. + logres.info("[%s] ProcessProxy.__del__()", id(self)) + self.close() + del self.__log # drop reference + + def close(self): + if not self._closed: + self.__log.info("[%s] ProcessProxy.close()" % id(self)) + + # Ensure that all IOBuffer's are closed. If they are not, these + # can cause hangs. + self.__log.info("[%s] ProcessProxy: closing stdin (%r)."\ + % (id(self), self.stdin)) + try: + self.stdin.close() + self._stdinProxy.join() + except AttributeError: + # May not have gotten far enough in the __init__ to set + # self.stdin, etc. + pass + self.__log.info("[%s] ProcessProxy: closing stdout (%r)."\ + % (id(self), self.stdout)) + try: + self.stdout.close() + if self._stdoutProxy is not threading.currentThread(): + self._stdoutProxy.join() + except AttributeError: + # May not have gotten far enough in the __init__ to set + # self.stdout, etc. + pass + self.__log.info("[%s] ProcessProxy: closing stderr (%r)."\ + % (id(self), self.stderr)) + try: + self.stderr.close() + if self._stderrProxy is not threading.currentThread(): + self._stderrProxy.join() + except AttributeError: + # May not have gotten far enough in the __init__ to set + # self.stderr, etc. + pass + + self._closed = 1 + + def _forkAndExecChildOnUnix(self, fdChildStdinRd, fdChildStdoutWr, + fdChildStderrWr): + """Fork and start the child process. + + Sets self._pid as a side effect. + """ + pid = os.fork() + if pid == 0: # child + os.dup2(fdChildStdinRd, 0) + os.dup2(fdChildStdoutWr, 1) + os.dup2(fdChildStderrWr, 2) + self._runChildOnUnix() + # parent + self._pid = pid + + def _startOnUnix(self): + # Create pipes for std handles. + fdChildStdinRd, fdChildStdinWr = os.pipe() + fdChildStdoutRd, fdChildStdoutWr = os.pipe() + fdChildStderrRd, fdChildStderrWr = os.pipe() + + if self._cwd: + oldDir = os.getcwd() + try: + os.chdir(self._cwd) + except OSError, ex: + raise ProcessError(msg=str(ex), errno=ex.errno) + self._forkAndExecChildOnUnix(fdChildStdinRd, fdChildStdoutWr, + fdChildStderrWr) + if self._cwd: + os.chdir(oldDir) + + os.close(fdChildStdinRd) + os.close(fdChildStdoutWr) + os.close(fdChildStderrWr) + + childStdin = _FileWrapper(descriptor=fdChildStdinWr) + logres.info("[%s] ProcessProxy._start(): create child stdin: %r", + id(self), childStdin) + childStdout = _FileWrapper(descriptor=fdChildStdoutRd) + logres.info("[%s] ProcessProxy._start(): create child stdout: %r", + id(self), childStdout) + childStderr = _FileWrapper(descriptor=fdChildStderrRd) + logres.info("[%s] ProcessProxy._start(): create child stderr: %r", + id(self), childStderr) + + # Create proxy threads for the out pipes. + self._stdinProxy = _InFileProxy(self.stdin, childStdin, name='<stdin>') + self._stdinProxy.start() + # Clean up the parent's side of <stdin> when it is observed that + # the child has closed its side of <stdout> and <stderr>. (This + # is one way of determining when it is appropriate to clean up + # this pipe, with compromises. See the discussion at the top of + # this module.) + closer = _CountingCloser([self.stdin, childStdin, self], 2) + self._stdoutProxy = _OutFileProxy(childStdout, self.stdout, + [closer], + name='<stdout>') + self._stdoutProxy.start() + self._stderrProxy = _OutFileProxy(childStderr, self.stderr, + [closer], + name='<stderr>') + self._stderrProxy.start() + + def _startOnWindows(self): + if type(self._cmd) in (types.ListType, types.TupleType): + # An arg vector was passed in. + cmd = _joinArgv(self._cmd) + else: + cmd = self._cmd + + # Create pipes for std handles. + # (Set the bInheritHandle flag so pipe handles are inherited.) + saAttr = pywintypes.SECURITY_ATTRIBUTES() + saAttr.bInheritHandle = 1 + #XXX Should maybe try with os.pipe. Dunno what that does for + # inheritability though. + hChildStdinRd, hChildStdinWr = win32pipe.CreatePipe(saAttr, 0) + hChildStdoutRd, hChildStdoutWr = win32pipe.CreatePipe(saAttr, 0) + hChildStderrRd, hChildStderrWr = win32pipe.CreatePipe(saAttr, 0) + + try: + # Duplicate the parent ends of the pipes so they are not + # inherited. + hChildStdinWrDup = win32api.DuplicateHandle( + win32api.GetCurrentProcess(), + hChildStdinWr, + win32api.GetCurrentProcess(), + 0, + 0, # not inherited + DUPLICATE_SAME_ACCESS) + win32api.CloseHandle(hChildStdinWr) + self._hChildStdinWr = hChildStdinWrDup + hChildStdoutRdDup = win32api.DuplicateHandle( + win32api.GetCurrentProcess(), + hChildStdoutRd, + win32api.GetCurrentProcess(), + 0, + 0, # not inherited + DUPLICATE_SAME_ACCESS) + win32api.CloseHandle(hChildStdoutRd) + self._hChildStdoutRd = hChildStdoutRdDup + hChildStderrRdDup = win32api.DuplicateHandle( + win32api.GetCurrentProcess(), + hChildStderrRd, + win32api.GetCurrentProcess(), + 0, + 0, # not inherited + DUPLICATE_SAME_ACCESS) + win32api.CloseHandle(hChildStderrRd) + self._hChildStderrRd = hChildStderrRdDup + + # Set the translation mode. + if self._mode == 't': + flags = os.O_TEXT + mode = '' + else: + flags = 0 + mode = 'b' + fdChildStdinWr = msvcrt.open_osfhandle(self._hChildStdinWr, flags) + fdChildStdoutRd = msvcrt.open_osfhandle(self._hChildStdoutRd, flags) + fdChildStderrRd = msvcrt.open_osfhandle(self._hChildStderrRd, flags) + + childStdin = _FileWrapper(descriptor=fdChildStdinWr, + handle=self._hChildStdinWr) + logres.info("[%s] ProcessProxy._start(): create child stdin: %r", + id(self), childStdin) + childStdout = _FileWrapper(descriptor=fdChildStdoutRd, + handle=self._hChildStdoutRd) + logres.info("[%s] ProcessProxy._start(): create child stdout: %r", + id(self), childStdout) + childStderr = _FileWrapper(descriptor=fdChildStderrRd, + handle=self._hChildStderrRd) + logres.info("[%s] ProcessProxy._start(): create child stderr: %r", + id(self), childStderr) + + # Start the child process. + si = win32process.STARTUPINFO() + si.dwFlags = win32process.STARTF_USESHOWWINDOW + si.wShowWindow = 0 # SW_HIDE + si.hStdInput = hChildStdinRd + si.hStdOutput = hChildStdoutWr + si.hStdError = hChildStderrWr + si.dwFlags |= win32process.STARTF_USESTDHANDLES + + cmd = _fixupCommand(cmd, self._env) + log.debug("cmd = %r", cmd) + + creationFlags = win32process.CREATE_NEW_PROCESS_GROUP + try: + self._hProcess, hThread, self._processId, threadId\ + = _SaferCreateProcess( + None, # app name + cmd, # command line + None, # process security attributes + None, # primary thread security attributes + 1, # handles are inherited + creationFlags, # creation flags + self._env, # environment + self._cwd, # current working directory + si) # STARTUPINFO pointer + except win32api.error, ex: + raise ProcessError(msg=ex.args[2], errno=ex.args[0]) + win32api.CloseHandle(hThread) + + finally: + # Close child ends of pipes on the parent's side (the + # parent's ends of the pipe are closed in the _FileWrappers.) + win32file.CloseHandle(hChildStdinRd) + win32file.CloseHandle(hChildStdoutWr) + win32file.CloseHandle(hChildStderrWr) + + # Create proxy threads for the pipes. + self._stdinProxy = _InFileProxy(self.stdin, childStdin, name='<stdin>') + self._stdinProxy.start() + # Clean up the parent's side of <stdin> when it is observed that + # the child has closed its side of <stdout>. (This is one way of + # determining when it is appropriate to clean up this pipe, with + # compromises. See the discussion at the top of this module.) + self._stdoutProxy = _OutFileProxy(childStdout, self.stdout, + [self.stdin, childStdin, self], + name='<stdout>') + self._stdoutProxy.start() + self._stderrProxy = _OutFileProxy(childStderr, self.stderr, + name='<stderr>') + self._stderrProxy.start() + + def wait(self, timeout=None): + """Wait for the started process to complete. + + "timeout" (on Windows) is a floating point number of seconds after + which to timeout. Default is win32event.INFINITE. + "timeout" (on Unix) is akin to the os.waitpid() "options" argument + (os.WNOHANG may be used to return immediately if the process has + not exited). Default is 0, i.e. wait forever. + + If the wait time's out it will raise a ProcessError. Otherwise it + will return the child's exit value (on Windows) or the child's exit + status excoded as per os.waitpid() (on Linux): + "a 16-bit number, whose low byte is the signal number that killed + the process, and whose high byte is the exit status (if the + signal number is zero); the high bit of the low byte is set if a + core file was produced." + In the latter case, use the os.W*() methods to interpret the return + value. + """ + # XXX Or should returning the exit value be move out to another + # function as on Win32 process control? If so, then should + # perhaps not make WaitForSingleObject semantic transformation. + if sys.platform.startswith("win"): + if timeout is None: + timeout = win32event.INFINITE + else: + timeout = timeout * 1000.0 # Win32 API's timeout is in millisecs + + rc = win32event.WaitForSingleObject(self._hProcess, timeout) + if rc == win32event.WAIT_FAILED: + raise ProcessError("'WAIT_FAILED' when waiting for process to "\ + "terminate: %r" % self._cmd, rc) + elif rc == win32event.WAIT_TIMEOUT: + raise ProcessError("'WAIT_TIMEOUT' when waiting for process to "\ + "terminate: %r" % self._cmd, rc) + + retval = win32process.GetExitCodeProcess(self._hProcess) + else: + # os.waitpid() will raise: + # OSError: [Errno 10] No child processes + # on subsequent .wait() calls. Change these semantics to have + # subsequent .wait() calls return the exit status and return + # immediately without raising an exception. + # (XXX It would require synchronization code to handle the case + # of multiple simultaneous .wait() requests, however we can punt + # on that because it is moot while Linux still has the problem + # for which _ThreadFixer() exists.) + if self.__retvalCache is not None: + retval = self.__retvalCache + else: + if timeout is None: + timeout = 0 + pid, sts = os.waitpid(self._pid, timeout) + if pid == self._pid: + self.__retvalCache = retval = sts + else: + raise ProcessError("Wait for process timed out.", + self.WAIT_TIMEOUT) + _unregisterProcess(self) + return retval + + def kill(self, exitCode=0, gracePeriod=1.0, sig=None): + """Kill process. + + "exitCode" [deprecated, not supported] (Windows only) is the + code the terminated process should exit with. + "gracePeriod" (Windows only) is a number of seconds the process is + allowed to shutdown with a WM_CLOSE signal before a hard + terminate is called. + "sig" (Unix only) is the signal to use to kill the process. Defaults + to signal.SIGKILL. See os.kill() for more information. + + Windows: + Try for an orderly shutdown via WM_CLOSE. If still running + after gracePeriod (1 sec. default), terminate. + """ + if sys.platform.startswith("win"): + import win32gui + # Send WM_CLOSE to windows in this process group. + win32gui.EnumWindows(self._close_, 0) + + # Send Ctrl-Break signal to all processes attached to this + # console. This is supposed to trigger shutdown handlers in + # each of the processes. + try: + win32api.GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, + self._processId) + except AttributeError: + log.warn("The win32api module does not have "\ + "GenerateConsoleCtrlEvent(). This may mean that "\ + "parts of this process group have NOT been killed.") + except win32api.error, ex: + if ex.args[0] not in (6, 87): + # Ignore the following: + # api_error: (87, 'GenerateConsoleCtrlEvent', 'The parameter is incorrect.') + # api_error: (6, 'GenerateConsoleCtrlEvent', 'The handle is invalid.') + # Get error 6 if there is no console. + raise + + # Last resort: call TerminateProcess if it has not yet. + retval = 0 + try: + self.wait(gracePeriod) + except ProcessError, ex: + log.info("[%s] Process.kill: calling TerminateProcess", id(self)) + win32process.TerminateProcess(self._hProcess, -1) + win32api.Sleep(100) # wait for resources to be released + + else: + if sig is None: + sig = signal.SIGKILL + try: + os.kill(self._pid, sig) + except OSError, ex: + if ex.errno != 3: + # Ignore: OSError: [Errno 3] No such process + raise + + _unregisterProcess(self) + + def _close_(self, hwnd, dummy): + """Callback used by .kill() on Windows. + + EnumWindows callback - sends WM_CLOSE to any window owned by this + process. + """ + threadId, processId = win32process.GetWindowThreadProcessId(hwnd) + if processId == self._processId: + import win32gui + win32gui.PostMessage(hwnd, WM_CLOSE, 0, 0) + + +class IOBuffer: + """Want to be able to both read and write to this buffer from + difference threads and have the same read/write semantics as for a + std handler. + + This class is subclass-able. _doRead(), _doWrite(), _doReadline(), + _doClose(), _haveLine(), and _haveNumBytes() can be overridden for + specific functionality. The synchronization issues (block on read + until write provides the needed data, termination) are handled for + free. + + Cannot support: + .seek() # Because we are managing *two* positions (one each + .tell() # for reading and writing), these do not make + # sense. + """ + #TODO: + # - Is performance a problem? This will likely be slower that + # StringIO.StringIO(). + # + def __init__(self, mutex=None, stateChange=None, name=None): + """'name' can be set for debugging, it will be used in log messages.""" + if name is not None: + self._name = name + else: + self._name = id(self) + log.info("[%s] IOBuffer.__init__()" % self._name) + + self.__buf = '' + # A state change is defined as the buffer being closed or a + # write occuring. + if mutex is not None: + self._mutex = mutex + else: + self._mutex = threading.Lock() + if stateChange is not None: + self._stateChange = stateChange + else: + self._stateChange = threading.Condition() + self._closed = 0 + + def _doWrite(self, s): + self.__buf += s # Append to buffer. + + def write(self, s): + log.info("[%s] IOBuffer.write(s=%r)", self._name, s) + # Silently drop writes after the buffer has been close()'d. + if self._closed: + return + # If empty write, close buffer (mimicking behaviour from + # koprocess.cpp.) + if not s: + self.close() + return + + self._mutex.acquire() + self._doWrite(s) + self._stateChange.acquire() + self._stateChange.notifyAll() # Notify of the write(). + self._stateChange.release() + self._mutex.release() + + def writelines(self, list): + self.write(''.join(list)) + + def _doRead(self, n): + """Pop 'n' bytes from the internal buffer and return them.""" + if n < 0: + idx = len(self.__buf) + else: + idx = min(n, len(self.__buf)) + retval, self.__buf = self.__buf[:idx], self.__buf[idx:] + return retval + + def read(self, n=-1): + log.info("[%s] IOBuffer.read(n=%r)" % (self._name, n)) + log.info("[%s] IOBuffer.read(): wait for data" % self._name) + if n < 0: + # Wait until the buffer is closed, i.e. no more writes will + # come. + while 1: + if self._closed: break + #log.debug("[%s] <<< IOBuffer.read: state change .wait()"\ + # % self._name) + self._stateChange.acquire() + self._stateChange.wait() + self._stateChange.release() + #log.debug("[%s] >>> IOBuffer.read: done change .wait()"\ + # % self._name) + else: + # Wait until there are the requested number of bytes to read + # (or until the buffer is closed, i.e. no more writes will + # come). + # XXX WARNING: I *think* there is a race condition around + # here whereby self.fparent.read() in _InFileProxy can + # hang. *Sometime* test_stdin::test_stdin_buffer() will + # hang. This was *before* I moved the + # _stateChange.acquire() and .release() calls out side + # of the 'while 1:' here. ...and now they are back + # inside. + while 1: + if self._closed: break + if self._haveNumBytes(n): break + #log.debug("[%s] <<< IOBuffer.read: state change .wait()"\ + # % self._name) + self._stateChange.acquire() + self._stateChange.wait() + self._stateChange.release() + #log.debug("[%s] >>> IOBuffer.read: done change .wait()"\ + # % self._name) + log.info("[%s] IOBuffer.read(): done waiting for data" % self._name) + + self._mutex.acquire() + retval = self._doRead(n) + self._mutex.release() + return retval + + def _doReadline(self, n): + """Pop the front line (or n bytes of it, whichever is less) from + the internal buffer and return it. + """ + idx = self.__buf.find('\n') + if idx == -1: + idx = len(self.__buf) + else: + idx += 1 # include the '\n' + if n is not None: + idx = min(idx, n) + retval, self.__buf = self.__buf[:idx], self.__buf[idx:] + return retval + + def _haveLine(self): + return self.__buf.find('\n') != -1 + + def _haveNumBytes(self, n=None): + return len(self.__buf) >= n + + def readline(self, n=None): + # Wait until there is a full line (or at least 'n' bytes) + # in the buffer or until the buffer is closed, i.e. no more + # writes will come. + log.info("[%s] IOBuffer.readline(n=%r)" % (self._name, n)) + + log.info("[%s] IOBuffer.readline(): wait for data" % self._name) + while 1: + if self._closed: break + if self._haveLine(): break + if n is not None and self._haveNumBytes(n): break + self._stateChange.acquire() + self._stateChange.wait() + self._stateChange.release() + log.info("[%s] IOBuffer.readline(): done waiting for data"\ + % self._name) + + self._mutex.acquire() + retval = self._doReadline(n) + self._mutex.release() + return retval + + def readlines(self): + lines = [] + while 1: + line = self.readline() + if line: + lines.append(line) + else: + break + return lines + + def _doClose(self): + pass + + def close(self): + if not self._closed: + log.info("[%s] IOBuffer.close()" % self._name) + self._doClose() + self._closed = 1 + self._stateChange.acquire() + self._stateChange.notifyAll() # Notify of the close(). + self._stateChange.release() + + def flush(self): + log.info("[%s] IOBuffer.flush()" % self._name) + #XXX Perhaps flush() should unwedged possible waiting .read() + # and .readline() calls that are waiting for more data??? + + +class _InFileProxy(threading.Thread): + """A thread to proxy stdin.write()'s from the parent to the child.""" + def __init__(self, fParent, fChild, name=None): + """ + "fParent" is a Python file-like object setup for writing. + "fChild" is a Win32 handle to the a child process' output pipe. + "name" can be set for debugging, it will be used in log messages. + """ + log.info("[%s, %s] _InFileProxy.__init__(fChild=%r, fParent=%r)", + name, id(self), fChild, fParent) + threading.Thread.__init__(self, name=name) + self.fChild = fChild + self.fParent = fParent + + def run(self): + log.info("[%s] _InFileProxy: start" % self.getName()) + try: + self._proxyFromParentToChild() + finally: + log.info("[%s] _InFileProxy: closing parent (%r)"\ + % (self.getName(), self.fParent)) + try: + self.fParent.close() + except IOError: + pass # Ignore: IOError: [Errno 4] Interrupted system call + log.info("[%s] _InFileProxy: done" % self.getName()) + + def _proxyFromParentToChild(self): + CHUNKSIZE = 4096 + # Read output from the child process, and (for now) just write + # it out. + while 1: + log.info("[%s] _InFileProxy: waiting for read on parent (%r)"\ + % (self.getName(), self.fParent)) + # XXX Get hangs here (!) even with + # self.stdin.close() in ProcessProxy' __del__() under this + # cond: + # p = ProcessProxy([...], stdin=sys.stdin) + # The user must manually send '\n' via <Enter> or EOF + # via <Ctrl-Z> to unlock this. How to get around that? + # See cleanOnTermination note in _OutFileProxy.run() + # below. + #log.debug("XXX -> start read on %r" % self.fParent) + try: + text = self.fParent.read(CHUNKSIZE) + except ValueError, ex: + # ValueError is raised with trying to write to a closed + # file/pipe. + text = None + #log.debug("XXX <- done read on %r" % self.fParent) + if not text: + # Empty text signifies that the pipe has been closed on + # the parent's end. + log.info("[%s] _InFileProxy: observed close of parent (%r)"\ + % (self.getName(), self.fParent)) + # Signal the child so it knows to stop listening. + try: + logres.info("[%s] _InFileProxy: closing child after "\ + "observing parent's close: %r", self.getName(), + self.fChild) + try: + self.fChild.close() + except IOError: + pass # Ignore: IOError: [Errno 4] Interrupted system call + except IOError, ex: + # Ignore: IOError: [Errno 9] Bad file descriptor + # XXX Do we *know* we want to do that? + pass + break + else: + log.info("[%s] _InFileProxy: read %d bytes from parent: %r"\ + % (self.getName(), len(text), text)) + + log.info("[%s, %s] _InFileProxy: writing %r to child (%r)", + self.getName(), id(self), text, self.fChild) + try: + self.fChild.write(text) + except (OSError, IOError), ex: + # Ignore errors for now. For example: + # - Get this on Win9x when writing multiple lines to "dir": + # OSError: [Errno 32] Broken pipe + #XXX There *may* be errors we don't want to avoid. + #XXX Should maybe just ignore EnvironmentError (base class). + log.info("[%s] _InFileProxy: error writing to child (%r), "\ + "closing: %s" % (self.getName(), self.fParent, ex)) + break + log.info("[%s] _InFileProxy: wrote %d bytes to child: %r"\ + % (self.getName(), len(text), text)) + + +class _OutFileProxy(threading.Thread): + """A thread to watch an "out" file from the spawned child process + and pass on write's to the parent. + """ + def __init__(self, fChild, fParent, toClose=[], name=None): + """ + "fChild" is a Win32 handle to the a child process' output pipe. + "fParent" is a Python file-like object setup for writing. + "toClose" is a list of objects on which to call .close when this + proxy is terminating. + "name" can be set for debugging, it will be used in log messages. + """ + log.info("[%s] _OutFileProxy.__init__(fChild=%r, fParent=%r, "\ + "toClose=%r)", name, fChild, fParent, toClose) + threading.Thread.__init__(self, name=name) + self.fChild = fChild + self.fParent = fParent + self.toClose = toClose + + def run(self): + log.info("[%s] _OutFileProxy: start" % self.getName()) + try: + self._proxyFromChildToParent() + finally: + logres.info("[%s] _OutFileProxy: terminating, close child (%r)", + self.getName(), self.fChild) + try: + self.fChild.close() + except IOError: + pass # Ignore: IOError: [Errno 4] Interrupted system call + log.info("[%s] _OutFileProxy: closing parent (%r)", + self.getName(), self.fParent) + try: + self.fParent.close() + except IOError: + pass # Ignore: IOError: [Errno 4] Interrupted system call + while self.toClose: + logres.info("[%s] _OutFileProxy: closing %r after "\ + "closing parent", self.getName(), self.toClose[0]) + try: + self.toClose[0].close() + except IOError: + pass # Ignore: IOError: [Errno 4] Interrupted system call + del self.toClose[0] + log.info("[%s] _OutFileProxy: done" % self.getName()) + + def _proxyFromChildToParent(self): + CHUNKSIZE = 4096 + # Read output from the child process, and (for now) just write + # it out. + while 1: + text = None + try: + log.info("[%s] _OutFileProxy: waiting for read on child (%r)"\ + % (self.getName(), self.fChild)) + text = self.fChild.read(CHUNKSIZE) + except IOError, ex: + # Ignore: IOError: [Errno 9] Bad file descriptor + # XXX Do we *know* we want to do that? + log.info("[%s] _OutFileProxy: error reading from child (%r), "\ + "shutting down: %s", self.getName(), self.fChild, ex) + break + if not text: + # Empty text signifies that the pipe has been closed on + # the child's end. + log.info("[%s] _OutFileProxy: observed close of child (%r)"\ + % (self.getName(), self.fChild)) + break + + log.info("[%s] _OutFileProxy: text(len=%d): %r", + self.getName(), len(text), text) + self.fParent.write(text) + + + +if sys.platform.startswith("linux"): + class _ThreadFixer: + """Mixin class for various classes in the Process hierarchy to + work around the known LinuxThreads bug where one cannot .wait() + on a created process from a subthread of the thread that created + the process. + + Usage: + class ProcessXXX(_ThreadFixer, BrokenProcessXXX): + _pclass = BrokenProcessXXX + + Details: + Because we must do all real os.wait() calls on the child + process from the thread that spawned it, we use a proxy + thread whose only responsibility is just that. The proxy + thread just starts the child and then immediately wait's for + the child to terminate. On termination is stores the exit + status (for use by the main thread) and notifies any thread + waiting for this termination (possibly the main thread). The + overriden .wait() uses this stored exit status and the + termination notification to simulate the .wait(). + """ + def __init__(self, *args, **kwargs): + # Keep a reference to 'log' ensure it is around for this object's + # destruction. + self.__log = log + self.__waiter = None + self.__hasTerminated = threading.Condition() + self.__terminationResult = None + self.__childStarted = threading.Condition() + self._pclass.__init__(self, *args, **kwargs) + + def _forkAndExecChildOnUnix(self, *args, **kwargs): + """Fork and start the child process do it in a special subthread + that will negotiate subsequent .wait()'s. + + Sets self._pid as a side effect. + """ + self.__waiter = threading.Thread( + target=self.__launchAndWait, args=args, kwargs=kwargs) + + # Start subthread that will launch child and wait until it + # *has* started. + self.__childStarted.acquire() + self.__waiter.start() + self.__childStarted.wait() + self.__childStarted.release() + + def __launchAndWait(self, *args, **kwargs): + """Launch the given command and wait for it to terminate. + + When the process has terminated then store its exit value + and finish. + """ + logfix.info("start child in thread %s", + threading.currentThread().getName()) + + # Spawn the child process and notify the main thread of + # this. + self.__childStarted.acquire() + self._pclass._forkAndExecChildOnUnix(self, *args, **kwargs) + self.__childStarted.notifyAll() + self.__childStarted.release() + + # Wait on the thread and store appropriate results when + # finished. + try: + waitResult = self._pclass.wait(self) + except ProcessError, ex: + waitResult = ex + self.__hasTerminated.acquire() + self.__terminationResult = waitResult + self.__hasTerminated.notifyAll() + self.__hasTerminated.release() + + self.__waiter = None # drop ref that would keep instance alive + + def wait(self, timeout=None): + # If the process __hasTerminated then return the exit + # status. Otherwise simulate the wait as appropriate. + # Note: + # - This class is only used on linux so 'timeout' has the + # Unix 'timeout' semantics. + self.__hasTerminated.acquire() + if self.__terminationResult is None: + if timeout == os.WNOHANG: # Poll. + self.__hasTerminated.wait(0) + else: # Block until process finishes. + self.__hasTerminated.wait() + terminationResult = self.__terminationResult + self.__hasTerminated.release() + + if terminationResult is None: + # process has not finished yet + raise ProcessError("Wait for process timed out.", + self.WAIT_TIMEOUT) + elif isinstance(terminationResult, Exception): + # some error waiting for process termination + raise terminationResult + else: + # the process terminated + return terminationResult + + _ThreadBrokenProcess = Process + class Process(_ThreadFixer, _ThreadBrokenProcess): + _pclass = _ThreadBrokenProcess + + _ThreadBrokenProcessOpen = ProcessOpen + class ProcessOpen(_ThreadFixer, _ThreadBrokenProcessOpen): + _pclass = _ThreadBrokenProcessOpen + + _ThreadBrokenProcessProxy = ProcessProxy + class ProcessProxy(_ThreadFixer, _ThreadBrokenProcessProxy): + _pclass = _ThreadBrokenProcessProxy + + diff --git a/wxPython/samples/ide/activegrid/util/__init__.py b/wxPython/samples/ide/activegrid/util/__init__.py new file mode 100644 index 0000000000..905f93a0ab --- /dev/null +++ b/wxPython/samples/ide/activegrid/util/__init__.py @@ -0,0 +1,72 @@ +import logging +import cStringIO +import traceback +import sys +import string +import os + +def classForName(className): + pathList = className.split('.') + moduleName = string.join(pathList[:-1], '.') + code = __import__(moduleName) + for name in pathList[1:]: + code = code.__dict__[name] + return code + +def hasattrignorecase(object, name): + for attr in dir(object): + if attr.lower() == name.lower(): + return True + for attr in dir(object): + if attr.lower() == '_' + name.lower(): + return True + return False + + +def setattrignorecase(object, name, value): + for attr in object.__dict__: + if attr.lower() == name.lower(): + object.__dict__[attr] = value + return +## for attr in dir(object): +## if attr.lower() == '_' + name.lower(): +## object.__dict__[attr] = value +## return + object.__dict__[name] = value + +def getattrignorecase(object, name): + for attr in object.__dict__: + if attr.lower() == name.lower(): + return object.__dict__[attr] +## for attr in dir(object): +## if attr.lower() == '_' + name.lower(): +## return object.__dict__[attr] + return object.__dict__[name] + + +def defaultLoad(fileObject): + xml = fileObject.read() + loadedObject = xmlmarshaller.unmarshal(xml) + if hasattr(fileObject, 'name'): + loadedObject.fileName = os.path.abspath(fileObject.name) + loadedObject.initialize() + return loadedObject + +def defaultSave(fileObject, objectToSave): + xml = xmlmarshaller.marshal(objectToSave, prettyPrint=True) + fileObject.write(xml) + + +def clone(objectToClone): + xml = xmlmarshaller.marshal(objectToClone, prettyPrint=True) + clonedObject = xmlmarshaller.unmarshal(xml) + if hasattr(objectToClone, 'fileName'): + clonedObject.fileName = objectToClone.fileName + clonedObject.initialize() + return clonedObject + +def exceptionToString(e): + sio = cStringIO.StringIO() + traceback.print_exception(e.__class__, e, sys.exc_traceback, file=sio) + return sio.getvalue() + diff --git a/wxPython/samples/ide/activegrid/util/aglogging.py b/wxPython/samples/ide/activegrid/util/aglogging.py new file mode 100644 index 0000000000..880ff822f2 --- /dev/null +++ b/wxPython/samples/ide/activegrid/util/aglogging.py @@ -0,0 +1,84 @@ +#---------------------------------------------------------------------------- +# Name: aglogging.py +# Purpose: Utilities to help with logging +# +# Author: Jeff Norton +# +# Created: 01/04/05 +# CVS-ID: $Id$ +# Copyright: (c) 2005 ActiveGrid, Inc. +# License: wxWindows License +#---------------------------------------------------------------------------- + +import sys +import os +import re +import traceback + +global agTestMode +agTestMode = False + +def setTestMode(mode): + global agTestMode + if (mode): + agTestMode = True + else: + agTestMode = False + +def getTestMode(): + global agTestMode + return agTestMode + +def testMode(normalObj, testObj=None): + if getTestMode(): + return testObj + return normalObj + +def toDiffableString(value): + s = repr(value) + ds = "" + i = s.find(" at 0x") + start = 0 + while (i >= 0): + j = s.find(">", i) + if (j < i): + break + ds += s[start:i] + start = j + i = s.find(" at 0x", start) + return ds + s[start:] + +def removeFileRefs(str): + str = re.sub(r'(?<=File ")[^"]*(\\[^\\]*")(, line )[0-9]*', _fileNameReplacement, str) + return str + +def _fileNameReplacement(match): + return "...%s" % match.group(1) + +def getTraceback(): + extype, val, tb = sys.exc_info() + tbs = "\n" + for s in traceback.format_tb(tb): + tbs += s + return tbs + +def reportException(out=None, stacktrace=False, diffable=False): + extype, val, t = sys.exc_info() + if (diffable): + exstr = removeFileRefs(str(val)) + else: + exstr = str(val) + if (out == None): + print "Got Exception = %s: %s" % (extype, exstr) + else: + print >> out, "Got Exception = %s: %s" % (extype, exstr) + if (stacktrace): + fmt = traceback.format_exception(extype, val, t) + for s in fmt: + if (diffable): + s = removeFileRefs(s) + if (out == None): + print s + else: + print >> out, s + diff --git a/wxPython/samples/ide/activegrid/util/cachedloader.py b/wxPython/samples/ide/activegrid/util/cachedloader.py new file mode 100644 index 0000000000..f0fc69de15 --- /dev/null +++ b/wxPython/samples/ide/activegrid/util/cachedloader.py @@ -0,0 +1,87 @@ +#---------------------------------------------------------------------------- +# Name: cachedloader.py +# Purpose: +# +# Author: Joel Hare +# +# Created: 8/31/04 +# CVS-ID: $Id$ +# Copyright: (c) 2004-2005 ActiveGrid, Inc. +# License: wxWindows License +#---------------------------------------------------------------------------- + +import copy +import os.path +import string +import cStringIO + +import time + +# TODO: Instantiate the database and create a pool + + +class CachedLoader(object): + def __init__(self): + self.cache = {} + self.baseLoadDir = None + + def fullPath(self, fileName): + if os.path.isabs(fileName): + absPath = fileName + elif self.baseLoadDir: + absPath = os.path.join(self.baseLoadDir, fileName) + else: + absPath = os.path.abspath(fileName) + return absPath + + def setPrototype(self, fileName, loadedFile): + absPath = self.fullPath(fileName) + mtime = time.time() + 31536000.0 # Make sure prototypes aren't replaced by files on disk + self.cache[absPath] = (mtime, loadedFile) + + def update(self, loader): + self.cache.update(loader.cache) + + def clear(self): + self.cache.clear() + + def delete(self, fileName): + absPath = self.fullPath(fileName) + del self.cache[absPath] + + def needsLoad(self, fileName): + absPath = self.fullPath(fileName) + try: + cached = self.cache[absPath] + cachedTime = cached[0] + if cachedTime >= os.path.getmtime(absPath): + return False + except KeyError: + pass + return True + + def load(self, fileName, loader): + absPath = self.fullPath(fileName) + loadedFile = None + try: + cached = self.cache[absPath] + except KeyError: + cached = None + + if cached: + cachedTime = cached[0] + # ToDO We might need smarter logic for checking if a file needs to be reloaded + # ToDo We need a way to disable checking if this is a production server + if cachedTime >= os.path.getmtime(absPath): + loadedFile = cached[1] + + if not loadedFile: + targetFile = file(absPath) + try: + mtime = os.path.getmtime(absPath) + loadedFile = loader(targetFile) + self.cache[absPath] = (mtime, loadedFile) + finally: + targetFile.close() + return loadedFile + diff --git a/wxPython/samples/ide/activegrid/util/dependencymgr.py b/wxPython/samples/ide/activegrid/util/dependencymgr.py new file mode 100644 index 0000000000..272e8ebb0e --- /dev/null +++ b/wxPython/samples/ide/activegrid/util/dependencymgr.py @@ -0,0 +1,145 @@ +#---------------------------------------------------------------------------- +# Name: dependencymgr.py +# Purpose: Dependency Manager +# +# Author: Jeff Norton +# +# Created: 01/28/05 +# CVS-ID: $Id$ +# Copyright: (c) 2004-2005 ActiveGrid, Inc. +#---------------------------------------------------------------------------- + +DM_NO_ID = 0 +DM_ID_ATTR = "_DependencyMgr__ID" + +##class ManageableObject(object): +## +## def __init__(self): +## self.__id = DM_NO_ID +## +## def __repr__(self): +## return "<ManageableObject id = %s>" % self.__id +## +## def __getID(self): +## return self.__id +## +## def __setID(self, value): +## if (self.__id != DM_NO_ID): +## raise DependencyMgrException("Cannot set the dependency ID on object %s to \"%s\" because it already has one (\"%s\")." % (repr(self), value, self.__id)) +## self.__id = value +## +## _DependencyMgr__ID = property(__getID, __setID) + +class DependencyMgr(object): + + def __init__(self): + self.clear() + + def clear(self): + self.__dependencies = {} + self.__lastID = DM_NO_ID + + def addDependency(self, parent, child): + pid = self._initObjectID(parent) + try: + parentCollection = self.__dependencies[pid] + except KeyError: + parentCollection = self._newDependencyCollection() + self.__dependencies[pid] = parentCollection + if (child not in parentCollection): + parentCollection.append(child) + + def removeDependency(self, parent, child): + pid = self._getObjectID(parent) + if (pid != DM_NO_ID): + try: + parentCollection = self.__dependencies[pid] + parentCollection.remove(child) + if (len(parentCollection) == 0): + del self.__dependencies[pid] + except KeyError, ValueError: + pass + + def clearDependencies(self, parent): + "Returns a list of objects or an empty list if no dependencies exist as for getDependencies, and then removes the dependency list." + pid = self._getObjectID(parent) + try: + deps = self.__dependencies[pid] + del self.__dependencies[pid] + return deps + except KeyError: + return [] + + def hasDependency(self, parent): + "Returns a boolean" + return (self._getObjectID(parent) in self.__dependencies) + + def getDependencies(self, parent): + "Returns a list of objects or an empty list if no dependencies exist." + try: + return self.__dependencies[self._getObjectID(parent)] + except KeyError: + return [] + + def dumpState(self, out): + "Writes the state of the dependency manager (as reported by getState) to out" + for line in self.getState(): + print >> out, line + + def getState(self): + "Returns the state of the dependency manager including all managed objects as a list of strings" + out = [] + out.append("DependencyMgr %s has %i parent objects, last id assigned is %i" % (repr(self), len(self.__dependencies), self.__lastID)) + for key, val in self.__dependencies.iteritems(): + out.append("Object %s has dependents: %s " % (repr(key), ", ".join([repr(d) for d in val]))) + return out + + def _initObjectID(self, obj): + try: + id = getattr(obj, DM_ID_ATTR) + except AttributeError: + id = DM_NO_ID + if (id == DM_NO_ID): + id = self._newID() + setattr(obj, DM_ID_ATTR, id) + return id + + def _getObjectID(self, obj): + try: + id = getattr(obj, DM_ID_ATTR) + except AttributeError: + id = DM_NO_ID + return id + + def _newID(self): + self.__lastID += 1 + return self.__lastID + + def _newDependencyCollection(self): + return [] + +globalDM = DependencyMgr() + +def addDependency(parent, child): + getGlobalDM().addDependency(parent, child) + +def removeDependency(parent, child): + getGlobalDM().removeDependency(parent, child) + +def clearDependencies(parent): + return getGlobalDM().clearDependencies(parent) + +def hasDependency(parent): + return getGlobalDM().hasDependency(parent) + +def getDependencies(parent): + return getGlobalDM().getDependencies(parent) + +def getState(): + return getGlobalDM().getState() + +def dumpState(out): + getGlobalDM().dumpState(out) + +def getGlobalDM(): + return globalDM diff --git a/wxPython/samples/ide/activegrid/util/fileutils.py b/wxPython/samples/ide/activegrid/util/fileutils.py new file mode 100644 index 0000000000..7d334c7dde --- /dev/null +++ b/wxPython/samples/ide/activegrid/util/fileutils.py @@ -0,0 +1,39 @@ +#---------------------------------------------------------------------------- +# Name: fileutils.py +# Purpose: Active grid miscellaneous utilities +# +# Author: Jeff Norton +# +# Created: 12/10/04 +# CVS-ID: $Id$ +# Copyright: (c) 2004-2005 ActiveGrid, Inc. +# License: wxWindows License +#---------------------------------------------------------------------------- + +import os + +def createFile(filename, mode='w'): + f = None + try: + f = file(filename, mode) + except: + os.makedirs(filename[:filename.rindex(os.sep)]) + f = file(filename, mode) + return f + +def compareFiles(file1, file2): + file1.seek(0) + file2.seek(0) + while True: + line1 = file1.readline() + line2 = file2.readline() + if (len(line1) == 0): + if (len(line2) == 0): + return 0 + else: + return -1 + elif (len(line2) == 0): + return -1 + elif (line1 != line2): + return -1 + diff --git a/wxPython/samples/ide/activegrid/util/gettersetter.py b/wxPython/samples/ide/activegrid/util/gettersetter.py new file mode 100644 index 0000000000..34d62481ad --- /dev/null +++ b/wxPython/samples/ide/activegrid/util/gettersetter.py @@ -0,0 +1,36 @@ +#---------------------------------------------------------------------------- +# Name: gettersetter.py +# Purpose: +# +# Author: Peter Yared +# +# Created: 7/28/04 +# CVS-ID: $Id$ +# Copyright: (c) 2004-2005 ActiveGrid, Inc. +# License: wxWindows License +#---------------------------------------------------------------------------- +def gettersetter(list): + for attr in list: + lowercase = attr[0].lower() + attr[1:] + uppercase = attr[0].upper() + attr[1:] + print " def get%s(self):" % uppercase + print " return self._%s" % lowercase + print + print " def set%s(self, %s):" % (uppercase, lowercase) + print " self._%s = %s" % (lowercase, lowercase) + print + +def listgettersetter(list): + for attr in list: + lowercase = attr[0].lower() + attr[1:] + uppercase = attr[0].upper() + attr[1:] + print " def get%s(self):" % uppercase + print " return self._%s" % lowercase + print + print " def add%s(self, %s):" % (uppercase[:-1], lowercase[:-1]) + print " self._%s.append(%s)" % (lowercase, lowercase[:-1]) + print + print " def remove%s(self, %s):" % (uppercase[:-1], lowercase[:-1]) + print " self._%s.remove(%s)" % (lowercase, lowercase[:-1]) + print + diff --git a/wxPython/samples/ide/activegrid/util/xmlmarshaller.py b/wxPython/samples/ide/activegrid/util/xmlmarshaller.py new file mode 100644 index 0000000000..f305f0ada8 --- /dev/null +++ b/wxPython/samples/ide/activegrid/util/xmlmarshaller.py @@ -0,0 +1,649 @@ +#---------------------------------------------------------------------------- +# Name: xmlmarshaller.py +# Purpose: +# +# Author: John Spurling +# +# Created: 7/28/04 +# CVS-ID: $Id$ +# Copyright: (c) 2004-2005 ActiveGrid, Inc. +# License: wxWindows License +#---------------------------------------------------------------------------- +from activegrid import util +import inspect +from types import * +import xml.sax +import xml.sax.handler +import __builtin__ +from xml.sax import saxutils + +### ToDO remove maxOccurs "unbounded" resolves to -1 hacks after bug 177 is fixed + +""" + +More documentation later, but here are some special Python attributes +that McLane recognizes: + +name: __xmlname__ +type: string +description: the name of the xml element for the marshalled object + +name: __xmlattributes__ +type: tuple or list +description: the name(s) of the Python string attribute(s) to be +marshalled as xml attributes instead of nested xml elements. currently +these can only be strings since there's not a way to get the type +information back when unmarshalling. + +name: __xmlexclude__ +type: tuple or list +description: the name(s) of the python attribute(s) to skip when +marshalling. + +name: __xmlrename__ +type: dict +description: describes an alternate Python <-> XML name mapping. +Normally the name mapping is the identity function. __xmlrename__ +overrides that. The keys are the Python names, the values are their +associated XML names. + +name: __xmlflattensequence__ +type: dict, tuple, or list +description: the name(s) of the Python sequence attribute(s) whose +items are to be marshalled as a series of xml elements (with an +optional keyword argument that specifies the element name to use) as +opposed to containing them in a separate sequence element, e.g.: + +myseq = (1, 2) +<!-- normal way of marshalling --> +<myseq> + <item objtype='int'>1</item> + <item objtype='int'>2</item> +</myseq> +<!-- with __xmlflattensequence__ set to {'myseq': 'squish'} --> +<squish objtype='int'>1</squish> +<squish objtype='int'>2</squish> + +name: __xmlnamespaces__ +type: dict +description: a dict of the namespaces that the object uses. Each item +in the dict should consist of a prefix,url combination where the key is +the prefix and url is the value, e.g.: + +__xmlnamespaces__ = { "xsd":"http://www.w3c.org/foo.xsd" } + +name: __xmldefaultnamespace__ +type: String +description: the prefix of a namespace defined in __xmlnamespaces__ that +should be used as the default namespace for the object. + +name: __xmlattrnamespaces__ +type: dict +description: a dict assigning the Python object's attributes to the namespaces +defined in __xmlnamespaces__. Each item in the dict should consist of a +prefix,attributeList combination where the key is the prefix and the value is +a list of the Python attribute names. e.g.: + +__xmlattrnamespaces__ = { "ag":["firstName", "lastName", "addressLine1", "city"] } + + +""" + +################################################################################ +# +# module exceptions +# +################################################################################ + +class Error(Exception): + """Base class for errors in this module.""" + pass + +class UnhandledTypeException(Error): + """Exception raised when attempting to marshal an unsupported + type. + """ + def __init__(self, typename): + self.typename = typename + def __str__(self): + return "%s is not supported for marshalling." % str(self.typename) + +class XMLAttributeIsNotStringType(Error): + """Exception raised when an object's attribute is specified to be + marshalled as an XML attribute of the enclosing object instead of + a nested element. + """ + def __init__(self, attrname, typename): + self.attrname = attrname + self.typename = typename + def __str__(self): + return """%s was set to be marshalled as an XML attribute + instead of a nested element, but the object's type is %s, not + string.""" % (self.attrname, self.typename) + +################################################################################ +# +# constants and such +# +################################################################################ + +XMLNS = 'xmlns' +XMLNS_PREFIX = XMLNS + ':' +XMLNS_PREFIX_LENGTH = len(XMLNS_PREFIX) + +BASETYPE_ELEMENT_NAME = 'item' +MEMBERS_TO_SKIP = ('__module__', '__doc__', '__xmlname__', '__xmlattributes__', + '__xmlexclude__', '__xmlflattensequence__', '__xmlnamespaces__', + '__xmldefaultnamespace__', '__xmlattrnamespaces__') + +WELL_KNOWN_OBJECTS = { "xs:element" : "activegrid.model.schema.XsdElement", + "xs:complexType" : "activegrid.model.schema.XsdComplexType", + "xs:complexType" : "activegrid.model.schema.XsdComplexType", + "xs:element" : "activegrid.model.schema.XsdElement", + "xs:key" : "activegrid.model.schema.XsdKey", + "xs:field" : "activegrid.model.schema.XsdKeyField", + "xs:keyref" : "activegrid.model.schema.XsdKeyRef", + "xs:selector" : "activegrid.model.schema.XsdKeySelector", + "xs:schema" : "activegrid.model.schema.Schema", + "ag:schemaOptions":"activegrid.model.schema.SchemaOptions", + "ag:debug" : "activegrid.model.processmodel.DebugOperation", + } + + +################################################################################ +# +# classes and functions +# +################################################################################ + +def _objectfactory(objname, objargs=None, xsname=None): + try: + '''dynamically create an object based on the objname and return + it. look it up in the BASETYPE_ELEMENT_MAP first. + ''' +## print "_objectfactory creating an object of type %s and value %s, xsname=%s" % (objname, objargs, xsname) + # split the objname into the typename and module path, + # importing the module if need be. + if not isinstance(objargs, list): + objargs = [objargs] + + if (xsname): + try: + objname = WELL_KNOWN_OBJECTS[xsname] + except KeyError: + pass + + objtype = objname.split('.')[-1] + pathlist = objname.split('.') + modulename = '.'.join(pathlist[0:-1]) + +## print "[objectfactory] objtype is %s" % objtype +## print "[objectfactory] objargs is %s" % `objargs` + + ## since the bool constructor will turn a string of non-zero + ## length into True, we call it with no argument (yielding a + ## False) if the string contains 'false' + if objtype == 'bool' and objargs[0].lower() == 'false': + objargs = None + +## if objtype == 'str': +## print type(objargs) +## print "string we're unescaping: '%s'" % objargs[0] +## objargs = saxutils.unescape(objargs[0]) + if objtype in ('float', 'int', 'str', 'long'): + objargs = [x.strip() for x in objargs] + + if objtype == 'str': + objargs = [saxutils.unescape(x) for x in objargs] + + if __builtin__.__dict__.has_key(objname): + module = __builtin__ + else: + if modulename: + module = __import__(modulename) + for name in pathlist[1:-1]: + module = module.__dict__[name] + if objargs: + return module.__dict__[objtype](*objargs) + else: + if objtype == 'None': + return None + return module.__dict__[objtype]() + except KeyError: + raise KeyError("Could not find class %s" % objname) + +class Element: + def __init__(self, name, attrs=None): + self.name = name + self.attrs = attrs + self.content = '' + self.children = [] + def getobjtype(self): + if self.attrs.has_key('objtype'): + return self.attrs.getValue('objtype') + else: + return 'str' + + +class XMLObjectFactory(xml.sax.ContentHandler): + def __init__(self): + self.rootelement = None + self.elementstack = [] + xml.sax.handler.ContentHandler.__init__(self) + + ## ContentHandler methods + def startElement(self, name, attrs): + if name.find(':') > -1: # Strip namespace prefixes for now until actually looking them up in xsd + name = name[name.index(':') + 1:] +## for attrname in attrs.getNames(): +## print "%s: %s" % (attrname, attrs.getValue(attrname)) + element = Element(name, attrs.copy()) + self.elementstack.append(element) +## print self.elementstack + + def characters(self, content): +## print "got content: %s" % content + if content: + self.elementstack[-1].content += content + + def endElement(self, name): +## print "[endElement] name of element we're at the end of: %s" % name + xsname = name + if name.find(':') > -1: # Strip namespace prefixes for now until actually looking them up in xsd + name = name[name.index(':') + 1:] + element = self.elementstack.pop() + objtype = element.getobjtype() + constructorarglist = [] + if element.content: + strippedElementContent = element.content.strip() + if strippedElementContent: + constructorarglist.append(element.content) + obj = _objectfactory(objtype, constructorarglist, xsname) + complexType = None + if hasattr(obj, '__xsdcomplextype__'): + complexType = getattr(obj, '__xsdcomplextype__') + if len(self.elementstack) > 0: + self.elementstack[-1].children.append((name, obj)) + else: + self.rootelement = obj + if element.attrs and not isinstance(obj, list): + for attrname, attr in element.attrs.items(): + if attrname == XMLNS or attrname.startswith(XMLNS_PREFIX): + if attrname.startswith(XMLNS_PREFIX): + ns = attrname[XMLNS_PREFIX_LENGTH:] + else: + ns = "" + if not hasattr(obj, '__xmlnamespaces__'): + obj.__xmlnamespaces__ = {ns:attr} + elif ns not in obj.__xmlnamespaces__: + if (hasattr(obj.__class__, '__xmlnamespaces__') + and obj.__xmlnamespaces__ is obj.__class__.__xmlnamespaces__): + obj.__xmlnamespaces__ = dict(obj.__xmlnamespaces__) + obj.__xmlnamespaces__[ns] = attr + elif not attrname == 'objtype': + if attrname.find(':') > -1: # Strip namespace prefixes for now until actually looking them up in xsd + attrname = attrname[attrname.index(':') + 1:] + if complexType: + xsdElement = complexType.findElement(attrname) + if xsdElement: + type = xsdElement.type + if type: + type = xsdToPythonType(type) + ### ToDO remove maxOccurs hack after bug 177 is fixed + if attrname == "maxOccurs" and attr == "unbounded": + attr = "-1" + attr = _objectfactory(type, attr) + util.setattrignorecase(obj, _toAttrName(obj, attrname), attr) +## obj.__dict__[_toAttrName(obj, attrname)] = attr + # stuff any child attributes meant to be in a sequence via the __xmlflattensequence__ + flattenDict = {} + if hasattr(obj, '__xmlflattensequence__'): + for sequencename, xmlnametuple in obj.__xmlflattensequence__.items(): + for xmlname in xmlnametuple: + flattenDict[xmlname] = sequencename + + # reattach an object's attributes to it + for childname, child in element.children: + if flattenDict.has_key(childname): + sequencename = _toAttrName(obj, flattenDict[childname]) + try: + sequencevalue = obj.__dict__[sequencename] + except AttributeError: + sequencevalue = None + if sequencevalue == None: + sequencevalue = [] + obj.__dict__[sequencename] = sequencevalue + sequencevalue.append(child) + elif isinstance(obj, list): + obj.append(child) + else: +## print "childname = %s, obj = %s, child = %s" % (childname, repr(obj), repr(child)) + util.setattrignorecase(obj, _toAttrName(obj, childname), child) +## obj.__dict__[_toAttrName(obj, childname)] = child + + if complexType: + for element in complexType.elements: + if element.default: + elementName = _toAttrName(obj, element.name) + if ((elementName not in obj.__dict__) or (obj.__dict__[elementName] == None)): + pythonType = xsdToPythonType(element.type) + defaultValue = _objectfactory(pythonType, element.default) + obj.__dict__[elementName] = defaultValue + + def getRootObject(self): + return self.rootelement + +def _toAttrName(obj, name): + if (hasattr(obj, "__xmlrename__")): + for key, val in obj.__xmlrename__.iteritems(): + if (name == val): + name = key + break +## if (name.startswith("__") and not name.endswith("__")): +## name = "_%s%s" % (obj.__class__.__name__, name) + return name + +__typeMappingXsdToPython = { + "string": "str", + "char": "str", + "varchar": "str", + "date": "str", # ToDO Need to work out how to create python date types + "boolean": "bool", + "decimal": "float", # ToDO Does python have a better fixed point type? + "int": "int", + "long": "long", + "float": "float", + "bool": "bool", + "str": "str", + "unicode": "unicode", + } + +def xsdToPythonType(xsdType): + try: + return __typeMappingXsdToPython[xsdType] + except KeyError: + raise Exception("Unknown xsd type %s" % xsdType) + +def _getXmlValue(pythonValue): + if (isinstance(pythonValue, bool)): + return str(pythonValue).lower() + else: + return str(pythonValue) + +def unmarshal(xmlstr): + objectfactory = XMLObjectFactory() + xml.sax.parseString(xmlstr, objectfactory) + return objectfactory.getRootObject() + + +def marshal(obj, elementName=None, nameSpacePrefix='', nameSpaces=None, prettyPrint=False, indent=0): + if prettyPrint or indent: + prefix = ' '*indent + newline = '\n' + increment = 4 + else: + prefix = '' + newline = '' + increment = 0 + + ## Determine the XML element name. If it isn't specified in the + ## parameter list, look for it in the __xmlname__ Python + ## attribute, else use the default generic BASETYPE_ELEMENT_NAME. + if not nameSpaces: nameSpaces = {} # Need to do this since if the {} is a default parameter it gets shared by all calls into the function + nameSpaceAttrs = '' + if hasattr(obj, '__xmlnamespaces__'): + for nameSpaceKey, nameSpaceUrl in getattr(obj, '__xmlnamespaces__').items(): + if nameSpaceUrl in nameSpaces: + nameSpaceKey = nameSpaces[nameSpaceUrl] + else: +## # TODO: Wait to do this until there is shared state for use when going through the object graph +## origNameSpaceKey = nameSpaceKey # Make sure there is no key collision, ie: same key referencing two different URL's +## i = 1 +## while nameSpaceKey in nameSpaces.values(): +## nameSpaceKey = origNameSpaceKey + str(i) +## i += 1 + nameSpaces[nameSpaceUrl] = nameSpaceKey + if nameSpaceKey == '': + nameSpaceAttrs += ' xmlns="%s" ' % (nameSpaceUrl) + else: + nameSpaceAttrs += ' xmlns:%s="%s" ' % (nameSpaceKey, nameSpaceUrl) + nameSpaceAttrs = nameSpaceAttrs.rstrip() + if hasattr(obj, '__xmldefaultnamespace__'): + nameSpacePrefix = getattr(obj, '__xmldefaultnamespace__') + ':' + if not elementName: + if hasattr(obj, '__xmlname__'): + elementName = nameSpacePrefix + obj.__xmlname__ + else: + elementName = nameSpacePrefix + BASETYPE_ELEMENT_NAME + else: + elementName = nameSpacePrefix + elementName + + members_to_skip = [] + ## Add more members_to_skip based on ones the user has selected + ## via the __xmlexclude__ attribute. + if hasattr(obj, '__xmlexclude__'): + members_to_skip += list(obj.__xmlexclude__) + # Marshal the attributes that are selected to be XML attributes. + objattrs = '' + className = obj.__class__.__name__ + classNamePrefix = "_" + className + if hasattr(obj, '__xmlattributes__'): + xmlattributes = obj.__xmlattributes__ + members_to_skip += xmlattributes + for attr in xmlattributes: + internalAttrName = attr + if (attr.startswith("__") and not attr.endswith("__")): + internalAttrName = classNamePrefix + attr + # Fail silently if a python attribute is specified to be + # an XML attribute but is missing. + try: + value = obj.__dict__[internalAttrName] + except KeyError: + continue +## # But, check and see if it is a property first: +## if (hasPropertyValue(obj, attr)): +## value = getattr(obj, attr) +## else: +## continue + xsdElement = None + if hasattr(obj, '__xsdcomplextype__'): + complexType = getattr(obj, '__xsdcomplextype__') + xsdElement = complexType.findElement(attr) + if xsdElement: + default = xsdElement.default + if default == value or default == _getXmlValue(value): + continue + elif value == None: + continue + + # ToDO remove maxOccurs hack after bug 177 is fixed + if attr == "maxOccurs" and value == -1: + value = "unbounded" + + if isinstance(value, bool): + if value == True: + value = "true" + else: + value = "false" + + attrNameSpacePrefix = '' + if hasattr(obj, '__xmlattrnamespaces__'): + for nameSpaceKey, nameSpaceAttributes in getattr(obj, '__xmlattrnamespaces__').items(): + if nameSpaceKey == nameSpacePrefix[:-1]: # Don't need to specify attribute namespace if it is the same as it selement + continue + if attr in nameSpaceAttributes: + attrNameSpacePrefix = nameSpaceKey + ':' + break +## if attr.startswith('_'): +## attr = attr[1:] + if (hasattr(obj, "__xmlrename__") and attr in obj.__xmlrename__): + attr = obj.__xmlrename__[attr] + + objattrs += ' %s%s="%s"' % (attrNameSpacePrefix, attr, value) + + objtype = type(obj) + if isinstance(obj, NoneType): + return '' +# return '%s<%s objtype="None"/>%s' % (prefix, elementName, newline) + elif isinstance(obj, bool): + return '%s<%s objtype="bool">%s</%s>%s' % (prefix, elementName, obj, elementName, newline) + elif isinstance(obj, int): + return '''%s<%s objtype="int">%s</%s>%s''' % (prefix, elementName, str(obj), elementName, newline) + elif isinstance(obj, long): + return '%s<%s objtype="long">%s</%s>%s' % (prefix, elementName, str(obj), elementName, newline) + elif isinstance(obj, float): + return '%s<%s objtype="float">%s</%s>%s' % (prefix, elementName, str(obj), elementName, newline) + elif isinstance(obj, basestring): + return '''%s<%s>%s</%s>%s''' % (prefix, elementName, saxutils.escape(obj), elementName, newline) +## elif isinstance(obj, unicode): +## return '''%s<%s>%s</%s>%s''' % (prefix, elementName, obj, elementName, newline) + elif isinstance(obj, list): + if len(obj) < 1: + return '' + xmlString = '%s<%s objtype="list">%s' % (prefix, elementName, newline) + for item in obj: + xmlString += marshal(item, nameSpaces=nameSpaces, indent=indent+increment) + xmlString += '%s</%s>%s' % (prefix, elementName, newline) + return xmlString + elif isinstance(obj, tuple): + if len(obj) < 1: + return '' + xmlString = '%s<%s objtype="list" mutable="false">%s' % (prefix, elementName, newline) + for item in obj: + xmlString += marshal(item, nameSpaces=nameSpaces, indent=indent+increment) + xmlString += '%s</%s>%s' % (prefix, elementName, newline) + return xmlString + elif isinstance(obj, dict): + xmlString = '%s<%s objtype="dict">%s' % (prefix, elementName, newline) + subprefix = prefix + ' '*increment + subindent = indent + 2*increment + for key, val in obj.iteritems(): + xmlString += "%s<key>%s%s%s</key>%s%s<value>%s%s%s</value>%s" \ + % (subprefix, newline, marshal(key, indent=subindent), subprefix, newline, subprefix, newline, marshal(val, nameSpaces=nameSpaces, indent=subindent), subprefix, newline) + xmlString += '%s</%s>%s' % (prefix, elementName, newline) + return xmlString + else: + moduleName = obj.__class__.__module__ + if (moduleName == "activegrid.model.schema"): + xmlString = '%s<%s%s%s' % (prefix, elementName, nameSpaceAttrs, objattrs) + else: + xmlString = '%s<%s%s%s objtype="%s.%s"' % (prefix, elementName, nameSpaceAttrs, objattrs, moduleName, className) + # get the member, value pairs for the object, filtering out + # the types we don't support. + xmlMemberString = '' + if hasattr(obj, '__xmlbody__'): + xmlMemberString = getattr(obj, obj.__xmlbody__) + else: + entryList = obj.__dict__.items() +## # Add in properties +## for key in obj.__class__.__dict__.iterkeys(): +## if (key not in members_to_skip and key not in obj.__dict__ +## and hasPropertyValue(obj, key)): +## value = getattr(obj, key) +## entryList.append((key, value)) + entryList.sort() + for name, value in entryList: +## # special name handling for private "__*" attributes: +## # remove the _<class-name> added by Python +## if name.startswith(classNamePrefix): name = name[len(classNamePrefix):] + if name in members_to_skip: continue + if name.startswith('__') and name.endswith('__'): continue +## idx = name.find('__') +## if idx > 0: +## newName = name[idx+2:] +## if newName: +## name = newName + subElementNameSpacePrefix = nameSpacePrefix + if hasattr(obj, '__xmlattrnamespaces__'): + for nameSpaceKey, nameSpaceValues in getattr(obj, '__xmlattrnamespaces__').items(): + if name in nameSpaceValues: + subElementNameSpacePrefix = nameSpaceKey + ':' + break + # handle sequences listed in __xmlflattensequence__ + # specially: instead of listing the contained items inside + # of a separate list, as god intended, list them inside + # the object containing the sequence. + if hasattr(obj, '__xmlflattensequence__') and name in obj.__xmlflattensequence__ and value: + try: + xmlnametuple = obj.__xmlflattensequence__[name] + xmlname = None + if len(xmlnametuple) == 1: + xmlname = xmlnametuple[0] + except: + xmlname = name +## xmlname = name.lower() + for seqitem in value: + xmlMemberString += marshal(seqitem, xmlname, subElementNameSpacePrefix, nameSpaces=nameSpaces, indent=indent+increment) + else: + if (hasattr(obj, "__xmlrename__") and name in obj.__xmlrename__): + xmlname = obj.__xmlrename__[name] + else: + xmlname = name +## xmlname = name.lower() +## # skip +## if xmlname.startswith('_') and not xmlname.startswith('__'): +## xmlname = xmlname[1:] +## if (indent > 30): +## print "getting pretty deep, xmlname = ", xmlname + xmlMemberString += marshal(value, xmlname, subElementNameSpacePrefix, nameSpaces=nameSpaces, indent=indent+increment) + # if we have nested elements, add them here, otherwise close the element tag immediately. + if xmlMemberString: + xmlString += '>' + if hasattr(obj, '__xmlbody__'): + xmlString += xmlMemberString + xmlString += '</%s>%s' % (elementName, newline) + else: + xmlString += newline + xmlString += xmlMemberString + xmlString += '%s</%s>%s' % (prefix, elementName, newline) + else: + xmlString = xmlString + '/>%s' % newline + return xmlString + +# We don't use this anymore but in case we want to get properties this is how +# you do it +def hasPropertyValue(obj, attr): + hasProp = False + try: + prop = obj.__class__.__dict__[attr] + if (isinstance(prop, property)): + hasProp = hasattr(obj, attr) + if (hasProp): + # It's a property and it has a value but sometimes we don't want it. + # If there is a _hasattr method execute it and the + # result will tell us whether to include this value + try: + hasProp = obj._hasattr(attr) + except: + pass + except KeyError: + pass + return hasProp + +if __name__ == '__main__': + from xmlmarshallertests import Person, marshalledint, marshalledlist + + l = [1, 2, 3] + d = {'1': 1, '2': 2} + outerlist = [l] + xmlstr = marshal(d, "d", prettyPrint=True) + print xmlstr + + person = Person() + person.firstName = "Albert" + person.lastName = "Camus" + person.addressLine1 = "23 Absurd St." + person.city = "Ennui" + person.state = "MO" + person.zip = "54321" + person._phoneNumber = "808-303-2323" + person.favoriteWords = ['angst', 'ennui', 'existence'] + person.weight = 150 + + xmlstring = marshal(person, 'person', prettyPrint=True) + print xmlstring + + obj = unmarshal(marshalledlist) + print "obj has type %s and value %s" % (type(obj), str(obj)) + for item in obj: + print "item: %s" % str(item) diff --git a/wxPython/samples/ide/activegrid/util/xmlmarshallertests.py b/wxPython/samples/ide/activegrid/util/xmlmarshallertests.py new file mode 100644 index 0000000000..b02b0fb097 --- /dev/null +++ b/wxPython/samples/ide/activegrid/util/xmlmarshallertests.py @@ -0,0 +1,183 @@ +#---------------------------------------------------------------------------- +# Name: xmlmarshallertests.py +# Purpose: +# +# Author: John Spurling +# +# Created: 8/16/04 +# CVS-ID: $Id$ +# Copyright: (c) 2004-2005 ActiveGrid, Inc. +# License: wxWindows License +#---------------------------------------------------------------------------- + +import unittest +import xmlmarshaller +from xmlprettyprinter import xmlprettyprint + +marshalledPersonObject = """ +<person objtype="Person"> + <firstName>Albert</firstName> + <lastName>Camus</lastName> + <address>23 Absurd St.</address> + <city>Ennui</city> + <state>MO</state> + <zip>54321</zip> + <_phoneNumber>808-303-2323</_phoneNumber> + <favoriteWords objtype="list"> + <item>angst</item> + <item>ennui</item> + <item>existence</item> + </favoriteWords> + <weight objtype="float">150</weight> +</person> +""" + +marshalledint = ''' +<item objtype="int">23</item> +''' + +marshalledlist = ''' +<mylist objtype="list"> + <item>foo</item> + <item>bar</item> +</mylist> +''' + +## a dummy class taken from the old XmlMarshaller module. +## class Person: +## def __init__(self): +## # These are not necessary but are nice if you want to tailor +## # the Python object <-> XML binding + +## # The xml element name to use for this object, otherwise it +## # will use a fully qualified Python name like __main__.Person +## # which can be ugly. +## self.__xmlname__ = "person" +## self.firstName = None +## self.lastName = None +## self.addressLine1 = None +## self.addressLine2 = None +## self.city = None +## self.state = None +## self.zip = None +## self._phoneNumber = None +## self.favoriteWords = None +## self.weight = None +class Person: + __xmlflattensequence__ = {'asequence': ('the_earth_is_flat',)} + +class XmlMarshallerTestFunctions(unittest.TestCase): + + def setUp(self): + '''common setup code goes here.''' + pass + + def testInt(self): + xml = xmlmarshaller.marshal(1) + print "\n#########################################" + print "# testString test case #" + print "#########################################" + print "marshalled int object:\n" + print xmlprettyprint(xml) + + def testDict(self): + xml = xmlmarshaller.marshal({'one': 1, + 'two': 2, + 'three': 3}) + print "\n#########################################" + print "# testString test case #" + print "#########################################" + print "marshalled dict object:\n" + print xmlprettyprint(xml) + + def testBool(self): + xmltrue = xmlmarshaller.marshal(True) + xmlfalse = xmlmarshaller.marshal(False) + print "\n#########################################" + print "# testBool test case #" + print "#########################################" + print "marshalled boolean true object:\n" + print xmlprettyprint(xmltrue) + print "\nmarshalled boolean false object:\n" + print xmlprettyprint(xmlfalse) + pytrue = xmlmarshaller.unmarshal(xmltrue) + assert pytrue is True + pyfalse = xmlmarshaller.unmarshal(xmlfalse) + assert pyfalse is False + + def testString(self): + xml = xmlmarshaller.marshal( + "all your marshalled objects are belong to us") + print "\n#########################################" + print "# testString test case #" + print "#########################################" + print xmlprettyprint(xml) + + def testEmptyElement(self): + person = Person() + person.firstName = "Albert" + person.__xmlattributes__ = ('firstName',) + xml = xmlmarshaller.marshal(person, 'person') + print "\n#########################################" + print "# testEmptyElement test case #" + print "#########################################" + print xml + assert (xml == """<person objtype="__main__.Person" firstName="Albert"/>""") + + def testXMLFlattenSequence(self): + person = Person() + person.asequence = ('one', 'two') + xml = xmlmarshaller.marshal(person, 'person') + print "\n#########################################" + print "# testXMLFlattenSequence test case #" + print "#########################################" + print xml + assert (xml == """<person objtype="__main__.Person"><the_earth_is_flat>one</the_earth_is_flat><the_earth_is_flat>two</the_earth_is_flat></person>""") + unmarshalledperson = xmlmarshaller.unmarshal(xml) + assert(hasattr(unmarshalledperson, 'asequence')) + assert(len(unmarshalledperson.asequence) == 2) + + def testInstance(self): + print "\n#########################################" + print "# testInstance test case #" + print "#########################################" + class Foo: + def __init__(self): + self.alist = [1,2] + self.astring = 'f00' + f = Foo() + xml = xmlmarshaller.marshal(f, 'foo') + print xml + + def testPerson(self): + person = Person() + person.firstName = "Albert" + person.lastName = "Camus" + person.addressLine1 = "23 Absurd St." + person.city = "Ennui" + person.state = "MO" + person.zip = "54321" + person._phoneNumber = "808-303-2323" + person.favoriteWords = ['angst', 'ennui', 'existence'] + person.weight = 150 +# __xmlattributes__ = ('fabulousness',) + person.fabulousness = "tres tres" + xml = xmlmarshaller.marshal(person) + print "\n#########################################" + print "# testPerson test case #" + print "#########################################" + print "Person object marshalled into XML:\n" + print xml + # When encountering a "person" element, use the Person class +## elementMappings = { "person" : Person } +## obj = unmarshal(xml, elementMappings = elementMappings) +## print "Person object recreated from XML with attribute types indicated:" +## print obj.person.__class__ +## for (attr, value) in obj.person.__dict__.items(): +## if not attr.startswith("__"): +## print attr, "=", value, type(value) +## print + + +if __name__ == "__main__": + unittest.main() diff --git a/wxPython/samples/ide/activegrid/util/xmlprettyprinter.py b/wxPython/samples/ide/activegrid/util/xmlprettyprinter.py new file mode 100644 index 0000000000..29dbf16be3 --- /dev/null +++ b/wxPython/samples/ide/activegrid/util/xmlprettyprinter.py @@ -0,0 +1,63 @@ +#---------------------------------------------------------------------------- +# Name: xmlprettyprinter.py +# Purpose: +# +# Author: John Spurling +# +# Created: 9/21/04 +# CVS-ID: $Id$ +# Copyright: (c) 2004-2005 ActiveGrid, Inc. +# License: wxWindows License +#---------------------------------------------------------------------------- +import xml.sax +import xml.sax.handler + + +class XMLPrettyPrinter(xml.sax.ContentHandler): + def __init__(self, indentationChar=' ', newlineChar='\n'): + self.xmlOutput = '' + self.indentationLevel = 0 + self.indentationChar = indentationChar + self.elementStack = [] + self.newlineChar = newlineChar + self.hitCharData = False + + ## ContentHandler methods + def startElement(self, name, attrs): + indentation = self.newlineChar + (self.indentationLevel * self.indentationChar) + # build attribute string + attrstring = '' + for attr in attrs.getNames(): + value = attrs[attr] + attrstring += ' %s="%s"' % (attr, value) + self.xmlOutput += '%s<%s%s>' % (indentation, name, attrstring) + self.indentationLevel += 1 + self.elementStack.append(name) + self.hitCharData = False + + def characters(self, content): + self.xmlOutput += content + self.hitCharData = True + + def endElement(self, name): + self.indentationLevel -= 1 + indentation = '' + if not self.hitCharData: +## indentation += self.newlineChar + (self.indentationLevel * self.indentationChar) + indentation += self.indentationLevel * self.indentationChar + else: + self.hitCharData = False + self.xmlOutput += '%s</%s>%s' % (indentation, self.elementStack.pop(), self.newlineChar) + + def getXMLString(self): + return self.xmlOutput[1:] + +def xmlprettyprint(xmlstr, spaces=4): + xpp = XMLPrettyPrinter(indentationChar=' ' * spaces) + xml.sax.parseString(xmlstr, xpp) + return xpp.getXMLString() + +if __name__ == '__main__': + simpleTestString = """<one>some text<two anattr="booga">two's data</two></one>""" + print prettyprint(simpleTestString) + -- 2.47.2