From c8c2e1d97be99f2ec420a60b1d53815e16574549 Mon Sep 17 00:00:00 2001 From: Robin Dunn Date: Fri, 22 Jun 2012 21:02:42 +0000 Subject: [PATCH] set svn:eol-style to native git-svn-id: https://svn.wxwidgets.org/svn/wx/wxPython/Phoenix/trunk@71832 c3d73ce0-8a6f-49c7-b76d-6d57e0e08775 --- .../snippets/python/converted/BusyInfo.1.py | 14 +- .../snippets/python/converted/BusyInfo.2.py | 22 +- wx/lib/CDate.py | 129 + wx/lib/ClickableHtmlWindow.py | 57 + wx/lib/__init__.py | 4 + wx/lib/activex.py | 173 + wx/lib/activexwrapper.py | 155 + wx/lib/analogclock/__init__.py | 144 + wx/lib/analogclock/analogclock.py | 633 ++ wx/lib/analogclock/helpers.py | 985 +++ wx/lib/analogclock/lib_setup/__init__.py | 0 .../lib_setup/buttontreectrlpanel.py | 290 + wx/lib/analogclock/lib_setup/colourselect.py | 80 + wx/lib/analogclock/lib_setup/fontselect.py | 61 + wx/lib/analogclock/setup.py | 473 ++ wx/lib/analogclock/styles.py | 47 + wx/lib/anchors.py | 104 + wx/lib/art/__init__.py | 1 + wx/lib/art/flagart.py | 1583 ++++ wx/lib/art/img2pyartprov.py | 57 + wx/lib/buttonpanel.py | 13 + wx/lib/buttons.py | 657 ++ wx/lib/calendar.py | 1236 +++ wx/lib/colourchooser/__init__.py | 36 + wx/lib/colourchooser/canvas.py | 143 + wx/lib/colourchooser/intl.py | 24 + wx/lib/colourchooser/pycolourbox.py | 87 + wx/lib/colourchooser/pycolourchooser.py | 413 + wx/lib/colourchooser/pycolourslider.py | 92 + wx/lib/colourchooser/pypalette.py | 176 + wx/lib/colourdb.py | 676 ++ wx/lib/colourselect.py | 176 + wx/lib/colourutils.py | 92 + wx/lib/combotreebox.py | 927 +++ wx/lib/customtreectrl.py | 13 + wx/lib/delayedresult.py | 420 + wx/lib/dialogs.py | 505 ++ wx/lib/docview.py | 3230 ++++++++ wx/lib/dragscroller.py | 79 + wx/lib/editor/README.txt | 77 + wx/lib/editor/__init__.py | 25 + wx/lib/editor/editor.py | 976 +++ wx/lib/editor/images.py | 15 + wx/lib/editor/selection.py | 44 + wx/lib/embeddedimage.py | 75 + wx/lib/eventStack.py | 136 + wx/lib/eventwatcher.py | 458 ++ wx/lib/evtmgr.py | 534 ++ wx/lib/expando.py | 225 + wx/lib/fancytext.py | 462 ++ wx/lib/filebrowsebutton.py | 460 ++ wx/lib/flashwin.py | 262 + wx/lib/flashwin_old.py | 652 ++ wx/lib/flatnotebook.py | 13 + wx/lib/floatbar.py | 310 + wx/lib/foldmenu.py | 89 + wx/lib/foldpanelbar.py | 13 + wx/lib/gestures.py | 310 + wx/lib/graphics.py | 1706 ++++ wx/lib/gridmovers.py | 487 ++ wx/lib/grids.py | 302 + wx/lib/hyperlink.py | 13 + wx/lib/iewin.py | 250 + wx/lib/iewin_old.py | 895 ++ wx/lib/imagebrowser.py | 753 ++ wx/lib/imageutils.py | 98 + wx/lib/infoframe.py | 492 ++ wx/lib/inspection.py | 1237 +++ wx/lib/intctrl.py | 921 +++ wx/lib/itemspicker.py | 243 + wx/lib/langlistctrl.py | 424 + wx/lib/layoutf.py | 270 + wx/lib/masked/__init__.py | 26 + wx/lib/masked/combobox.py | 775 ++ wx/lib/masked/ctrl.py | 108 + wx/lib/masked/ipaddrctrl.py | 220 + wx/lib/masked/maskededit.py | 7241 +++++++++++++++++ wx/lib/masked/numctrl.py | 1926 +++++ wx/lib/masked/textctrl.py | 419 + wx/lib/masked/timectrl.py | 1405 ++++ wx/lib/mixins/__init__.py | 18 + wx/lib/mixins/grid.py | 48 + wx/lib/mixins/gridlabelrenderer.py | 248 + wx/lib/mixins/imagelist.py | 78 + wx/lib/mixins/inspection.py | 89 + wx/lib/mixins/listctrl.py | 877 ++ wx/lib/mixins/rubberband.py | 406 + wx/lib/mixins/treemixin.py | 661 ++ wx/lib/msgpanel.py | 96 + wx/lib/multisash.py | 746 ++ wx/lib/mvctree.py | 1150 +++ wx/lib/myole4ax.idl | 178 + wx/lib/myole4ax.tlb | Bin 0 -> 8736 bytes wx/lib/newevent.py | 115 + wx/lib/nvdlg.py | 155 + wx/lib/ogl/__init__.py | 22 + wx/lib/ogl/_basic.py | 3179 ++++++++ wx/lib/ogl/_bmpshape.py | 64 + wx/lib/ogl/_canvas.py | 363 + wx/lib/ogl/_composit.py | 1442 ++++ wx/lib/ogl/_diagram.py | 167 + wx/lib/ogl/_divided.py | 402 + wx/lib/ogl/_drawn.py | 888 ++ wx/lib/ogl/_lines.py | 1532 ++++ wx/lib/ogl/_oglmisc.py | 415 + wx/lib/pdfwin.py | 296 + wx/lib/pdfwin_old.py | 790 ++ wx/lib/platebtn.py | 759 ++ wx/lib/plot.py | 2430 ++++++ wx/lib/popupctl.py | 249 + wx/lib/printout.py | 1156 +++ wx/lib/progressindicator.py | 152 + wx/lib/pydocview.py | 3298 ++++++++ wx/lib/pyshell.py | 349 + wx/lib/rcsizer.py | 228 + wx/lib/resizewidget.py | 247 + wx/lib/rightalign.py | 109 + wx/lib/rpcMixin.py | 422 + wx/lib/scrolledpanel.py | 136 + wx/lib/sheet.py | 349 + wx/lib/shell.py | 376 + wx/lib/sized_controls.py | 746 ++ wx/lib/softwareupdate.py | 339 + wx/lib/splashscreen.py | 132 + wx/lib/splitter.py | 788 ++ wx/lib/statbmp.py | 89 + wx/lib/stattext.py | 189 + wx/lib/throbber.py | 322 + wx/lib/ticker.py | 215 + wx/lib/ticker_xrc.py | 49 + wx/lib/utils.py | 79 + wx/lib/wordwrap.py | 97 + wx/lib/wxPlotCanvas.py | 489 ++ wx/lib/wxcairo.py | 466 ++ wx/lib/wxpTag.py | 273 + 135 files changed, 68294 insertions(+), 18 deletions(-) create mode 100644 wx/lib/CDate.py create mode 100644 wx/lib/ClickableHtmlWindow.py create mode 100644 wx/lib/__init__.py create mode 100644 wx/lib/activex.py create mode 100644 wx/lib/activexwrapper.py create mode 100644 wx/lib/analogclock/__init__.py create mode 100644 wx/lib/analogclock/analogclock.py create mode 100644 wx/lib/analogclock/helpers.py create mode 100644 wx/lib/analogclock/lib_setup/__init__.py create mode 100644 wx/lib/analogclock/lib_setup/buttontreectrlpanel.py create mode 100644 wx/lib/analogclock/lib_setup/colourselect.py create mode 100644 wx/lib/analogclock/lib_setup/fontselect.py create mode 100644 wx/lib/analogclock/setup.py create mode 100644 wx/lib/analogclock/styles.py create mode 100644 wx/lib/anchors.py create mode 100644 wx/lib/art/__init__.py create mode 100644 wx/lib/art/flagart.py create mode 100644 wx/lib/art/img2pyartprov.py create mode 100644 wx/lib/buttonpanel.py create mode 100644 wx/lib/buttons.py create mode 100644 wx/lib/calendar.py create mode 100644 wx/lib/colourchooser/__init__.py create mode 100644 wx/lib/colourchooser/canvas.py create mode 100644 wx/lib/colourchooser/intl.py create mode 100644 wx/lib/colourchooser/pycolourbox.py create mode 100644 wx/lib/colourchooser/pycolourchooser.py create mode 100644 wx/lib/colourchooser/pycolourslider.py create mode 100644 wx/lib/colourchooser/pypalette.py create mode 100644 wx/lib/colourdb.py create mode 100644 wx/lib/colourselect.py create mode 100644 wx/lib/colourutils.py create mode 100644 wx/lib/combotreebox.py create mode 100644 wx/lib/customtreectrl.py create mode 100644 wx/lib/delayedresult.py create mode 100644 wx/lib/dialogs.py create mode 100644 wx/lib/docview.py create mode 100644 wx/lib/dragscroller.py create mode 100644 wx/lib/editor/README.txt create mode 100644 wx/lib/editor/__init__.py create mode 100644 wx/lib/editor/editor.py create mode 100644 wx/lib/editor/images.py create mode 100644 wx/lib/editor/selection.py create mode 100644 wx/lib/embeddedimage.py create mode 100644 wx/lib/eventStack.py create mode 100644 wx/lib/eventwatcher.py create mode 100644 wx/lib/evtmgr.py create mode 100644 wx/lib/expando.py create mode 100644 wx/lib/fancytext.py create mode 100644 wx/lib/filebrowsebutton.py create mode 100644 wx/lib/flashwin.py create mode 100644 wx/lib/flashwin_old.py create mode 100644 wx/lib/flatnotebook.py create mode 100644 wx/lib/floatbar.py create mode 100644 wx/lib/foldmenu.py create mode 100644 wx/lib/foldpanelbar.py create mode 100644 wx/lib/gestures.py create mode 100644 wx/lib/graphics.py create mode 100644 wx/lib/gridmovers.py create mode 100644 wx/lib/grids.py create mode 100644 wx/lib/hyperlink.py create mode 100644 wx/lib/iewin.py create mode 100644 wx/lib/iewin_old.py create mode 100644 wx/lib/imagebrowser.py create mode 100644 wx/lib/imageutils.py create mode 100644 wx/lib/infoframe.py create mode 100644 wx/lib/inspection.py create mode 100644 wx/lib/intctrl.py create mode 100644 wx/lib/itemspicker.py create mode 100644 wx/lib/langlistctrl.py create mode 100644 wx/lib/layoutf.py create mode 100644 wx/lib/masked/__init__.py create mode 100644 wx/lib/masked/combobox.py create mode 100644 wx/lib/masked/ctrl.py create mode 100644 wx/lib/masked/ipaddrctrl.py create mode 100644 wx/lib/masked/maskededit.py create mode 100644 wx/lib/masked/numctrl.py create mode 100644 wx/lib/masked/textctrl.py create mode 100644 wx/lib/masked/timectrl.py create mode 100644 wx/lib/mixins/__init__.py create mode 100644 wx/lib/mixins/grid.py create mode 100644 wx/lib/mixins/gridlabelrenderer.py create mode 100644 wx/lib/mixins/imagelist.py create mode 100644 wx/lib/mixins/inspection.py create mode 100644 wx/lib/mixins/listctrl.py create mode 100644 wx/lib/mixins/rubberband.py create mode 100644 wx/lib/mixins/treemixin.py create mode 100644 wx/lib/msgpanel.py create mode 100644 wx/lib/multisash.py create mode 100644 wx/lib/mvctree.py create mode 100644 wx/lib/myole4ax.idl create mode 100644 wx/lib/myole4ax.tlb create mode 100644 wx/lib/newevent.py create mode 100755 wx/lib/nvdlg.py create mode 100644 wx/lib/ogl/__init__.py create mode 100644 wx/lib/ogl/_basic.py create mode 100644 wx/lib/ogl/_bmpshape.py create mode 100644 wx/lib/ogl/_canvas.py create mode 100644 wx/lib/ogl/_composit.py create mode 100644 wx/lib/ogl/_diagram.py create mode 100644 wx/lib/ogl/_divided.py create mode 100644 wx/lib/ogl/_drawn.py create mode 100644 wx/lib/ogl/_lines.py create mode 100644 wx/lib/ogl/_oglmisc.py create mode 100644 wx/lib/pdfwin.py create mode 100644 wx/lib/pdfwin_old.py create mode 100644 wx/lib/platebtn.py create mode 100644 wx/lib/plot.py create mode 100644 wx/lib/popupctl.py create mode 100644 wx/lib/printout.py create mode 100644 wx/lib/progressindicator.py create mode 100644 wx/lib/pydocview.py create mode 100644 wx/lib/pyshell.py create mode 100644 wx/lib/rcsizer.py create mode 100644 wx/lib/resizewidget.py create mode 100644 wx/lib/rightalign.py create mode 100644 wx/lib/rpcMixin.py create mode 100644 wx/lib/scrolledpanel.py create mode 100644 wx/lib/sheet.py create mode 100644 wx/lib/shell.py create mode 100644 wx/lib/sized_controls.py create mode 100644 wx/lib/softwareupdate.py create mode 100644 wx/lib/splashscreen.py create mode 100644 wx/lib/splitter.py create mode 100644 wx/lib/statbmp.py create mode 100644 wx/lib/stattext.py create mode 100644 wx/lib/throbber.py create mode 100644 wx/lib/ticker.py create mode 100644 wx/lib/ticker_xrc.py create mode 100644 wx/lib/utils.py create mode 100644 wx/lib/wordwrap.py create mode 100644 wx/lib/wxPlotCanvas.py create mode 100644 wx/lib/wxcairo.py create mode 100644 wx/lib/wxpTag.py diff --git a/docs/sphinx/rest_substitutions/snippets/python/converted/BusyInfo.1.py b/docs/sphinx/rest_substitutions/snippets/python/converted/BusyInfo.1.py index 9351a7fc..7f7ac166 100644 --- a/docs/sphinx/rest_substitutions/snippets/python/converted/BusyInfo.1.py +++ b/docs/sphinx/rest_substitutions/snippets/python/converted/BusyInfo.1.py @@ -1,7 +1,7 @@ - - wait = wx.BusyInfo("Please wait, working...") - - for i in xrange(10000): - DoACalculation() - - del wait + + wait = wx.BusyInfo("Please wait, working...") + + for i in xrange(10000): + DoACalculation() + + del wait diff --git a/docs/sphinx/rest_substitutions/snippets/python/converted/BusyInfo.2.py b/docs/sphinx/rest_substitutions/snippets/python/converted/BusyInfo.2.py index 1eb002f8..1a1487bc 100644 --- a/docs/sphinx/rest_substitutions/snippets/python/converted/BusyInfo.2.py +++ b/docs/sphinx/rest_substitutions/snippets/python/converted/BusyInfo.2.py @@ -1,11 +1,11 @@ - - disableAll = wx.WindowDisabler() - wait = wx.BusyInfo("Please wait, working...") - - for i in xrange(10000): - DoACalculation() - - if i % 1000 == 0: - wx.GetApp().Yield() - - del wait + + disableAll = wx.WindowDisabler() + wait = wx.BusyInfo("Please wait, working...") + + for i in xrange(10000): + DoACalculation() + + if i % 1000 == 0: + wx.GetApp().Yield() + + del wait diff --git a/wx/lib/CDate.py b/wx/lib/CDate.py new file mode 100644 index 00000000..02ef8609 --- /dev/null +++ b/wx/lib/CDate.py @@ -0,0 +1,129 @@ +# Name: CDate.py +# Purpose: Date and Calendar classes +# +# Author: Lorne White (email: lwhite1@planet.eon.net) +# +# Created: +# Version 0.2 08-Nov-1999 +# Licence: wxWindows license +#---------------------------------------------------------------------------- +# Updated: 01-Dec-2004 +# Action: Cast the year variable to an integer under the Date Class +# Reason: When the year was compared in the isleap() function, if it was +# in a string format, then an error was raised. + +import time + +Month = {2: 'February', 3: 'March', None: 0, 'July': 7, 11: + 'November', 'December': 12, 'June': 6, 'January': 1, 'September': 9, + 'August': 8, 'March': 3, 'November': 11, 'April': 4, 12: 'December', + 'May': 5, 10: 'October', 9: 'September', 8: 'August', 7: 'July', 6: + 'June', 5: 'May', 4: 'April', 'October': 10, 'February': 2, 1: + 'January', 0: None} + +# Number of days per month (except for February in leap years) +mdays = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + +# Full and abbreviated names of weekdays +day_name = [ 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] +day_abbr = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', ] + +# Return number of leap years in range [y1, y2) +# Assume y1 <= y2 and no funny (non-leap century) years + +def leapdays(y1, y2): + return (y2+3)/4 - (y1+3)/4 + +# Return 1 for leap years, 0 for non-leap years +def isleap(year): + return year % 4 == 0 and (year % 100 <> 0 or year % 400 == 0) + +def FillDate(val): + s = str(val) + if len(s) < 2: + s = '0' + s + return s + + +def julianDay(year, month, day): + b = 0L + year, month, day = long(year), long(month), long(day) + if month > 12L: + year = year + month/12L + month = month%12 + elif month < 1L: + month = -month + year = year - month/12L - 1L + month = 12L - month%12L + if year > 0L: + yearCorr = 0L + else: + yearCorr = 3L + if month < 3L: + year = year - 1L + month = month + 12L + if year*10000L + month*100L + day > 15821014L: + b = 2L - year/100L + year/400L + return (1461L*year - yearCorr)/4L + 306001L*(month + 1L)/10000L + day + 1720994L + b + + +def TodayDay(): + date = time.localtime(time.time()) + year = date[0] + month = date[1] + day = date[2] + julian = julianDay(year, month, day) + daywk = dayOfWeek(julian) + daywk = day_name[daywk] + return(daywk) + +def FormatDay(value): + date = FromFormat(value) + daywk = DateCalc.dayOfWeek(date) + daywk = day_name[daywk] + return(daywk) + +def FromJulian(julian): + julian = long(julian) + if (julian < 2299160L): + b = julian + 1525L + else: + alpha = (4L*julian - 7468861L)/146097L + b = julian + 1526L + alpha - alpha/4L + c = (20L*b - 2442L)/7305L + d = 1461L*c/4L + e = 10000L*(b - d)/306001L + day = int(b - d - 306001L*e/10000L) + if e < 14L: + month = int(e - 1L) + else: + month = int(e - 13L) + if month > 2: + year = c - 4716L + else: + year = c - 4715L + year = int(year) + return year, month, day + +def dayOfWeek(julian): + return int((julian + 1L)%7L) + +def daysPerMonth(month, year): + ndays = mdays[month] + (month == 2 and isleap(year)) + return ndays + +class now(object): + def __init__(self): + self.date = time.localtime(time.time()) + self.year = self.date[0] + self.month = self.date[1] + self.day = self.date[2] + +class Date(object): + def __init__(self, year, month, day): + self.julian = julianDay(year, month, day) + self.month = month + self.year = int(year) + self.day_of_week = dayOfWeek(self.julian) + self.days_in_month = daysPerMonth(self.month, self.year) + diff --git a/wx/lib/ClickableHtmlWindow.py b/wx/lib/ClickableHtmlWindow.py new file mode 100644 index 00000000..51c81263 --- /dev/null +++ b/wx/lib/ClickableHtmlWindow.py @@ -0,0 +1,57 @@ +# 12/01/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o Updated for wx namespace. Not tested though. +# +# 12/17/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o Removed wx prefix from class name, +# updated reverse renamer +# + +""" +sorry no documentation... +Christopher J. Fama +""" + + +import wx +import wx.html as html + +class PyClickableHtmlWindow(html.HtmlWindow): + """ + Class for a wxHtmlWindow which responds to clicks on links by opening a + browser pointed at that link, and to shift-clicks by copying the link + to the clipboard. + """ + def __init__(self,parent,ID,**kw): + apply(html.HtmlWindow.__init__,(self,parent,ID),kw) + + def OnLinkClicked(self,link): + self.link = wx.TextDataObject(link.GetHref()) + if link.GetEvent().ShiftDown(): + if wx.TheClipboard.Open(): + wx.TheClipboard.SetData(self.link) + wx.TheClipboard.Close() + else: + dlg = wx.MessageDialog(self,"Couldn't open clipboard!\n",wx.OK) + wx.Bell() + dlg.ShowModal() + dlg.Destroy() + else: + if 0: # Chris's original code... + if sys.platform not in ["windows",'nt'] : + #TODO: A MORE APPROPRIATE COMMAND LINE FOR Linux + #[or rather, non-Windows platforms... as of writing, + #this MEANS Linux, until wxPython for wxMac comes along...] + command = "/usr/bin/netscape" + else: + command = "start" + command = "%s \"%s\"" % (command, + self.link.GetText ()) + os.system (command) + + else: # My alternative + import webbrowser + webbrowser.open(link.GetHref()) + + diff --git a/wx/lib/__init__.py b/wx/lib/__init__.py new file mode 100644 index 00000000..54e9b268 --- /dev/null +++ b/wx/lib/__init__.py @@ -0,0 +1,4 @@ +# + + + diff --git a/wx/lib/activex.py b/wx/lib/activex.py new file mode 100644 index 00000000..54370a2e --- /dev/null +++ b/wx/lib/activex.py @@ -0,0 +1,173 @@ +#---------------------------------------------------------------------- +# Name: wx.lib.activex +# Purpose: The 3rd (and hopefully final) implementation of an +# ActiveX container for wxPython. +# +# Author: Robin Dunn +# +# Created: 5-June-2008 +# RCS-ID: $Id: $ +# Copyright: (c) 2008 by Total Control Software +# Licence: wxWindows license +#---------------------------------------------------------------------- + + +""" +This module provides a wx.Window that hosts ActiveX Controls using +just the ctypes and comtypes packages. This provides a light-weight +COM implementation with full dynamic dispatch support. + +The only requirements are ctypes (included with Python 2.5 and +available separately for earlier versions of Python) and the comtypes +package, which is available from +http://starship.python.net/crew/theller/comtypes/. Be sure to get at +least version 0.5, which at the time of this writing is only available +from SVN. You can fetch it with easy_install with a command like +this: + + easy_install http://svn.python.org/projects/ctypes/trunk/comtypes + +""" + +import wx + +import ctypes as ct +import ctypes.wintypes as wt +import comtypes +import comtypes.client as cc +import comtypes.hresult as hr + +import sys, os +if not hasattr(sys, 'frozen'): + f = os.path.join(os.path.dirname(__file__), 'myole4ax.tlb') + cc.GetModule(f) +from comtypes.gen import myole4ax + + +kernel32 = ct.windll.kernel32 +user32 = ct.windll.user32 +atl = ct.windll.atl + +WS_CHILD = 0x40000000 +WS_VISIBLE = 0x10000000 +WS_CLIPCHILDREN = 0x2000000 +WS_CLIPSIBLINGS = 0x4000000 +CW_USEDEFAULT = 0x80000000 +WM_KEYDOWN = 256 +WM_DESTROY = 2 + +#------------------------------------------------------------------------------ + +class ActiveXCtrl(wx.PyAxBaseWindow): + """ + A wx.Window for hosting ActiveX controls. The COM interface of + the ActiveX control is accessible through the ctrl property of + this class, and this class is also set as the event sink for COM + events originating from the ActiveX control. In other words, to + catch the COM events you mearly have to derive from this class and + provide a method with the correct name. See the comtypes package + documentation for more details. + """ + + def __init__(self, parent, axID, wxid=-1, pos=wx.DefaultPosition, + size=wx.DefaultSize, style=0, name="activeXCtrl"): + """ + All parameters are like those used in normal wx.Windows with + the addition of axID which is a string that is either a ProgID + or a CLSID used to identify the ActiveX control. + """ + pos = wx.Point(*pos) # in case the arg is a tuple + size = wx.Size(*size) # ditto + + x = pos.x + y = pos.y + if x == -1: x = CW_USEDEFAULT + if y == -1: y = 20 + w = size.width + h = size.height + if w == -1: w = 20 + if h == -1: h = 20 + + # create the control + atl.AtlAxWinInit() + hInstance = kernel32.GetModuleHandleA(None) + hwnd = user32.CreateWindowExA(0, "AtlAxWin", axID, + WS_CHILD | WS_VISIBLE + | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, + x,y, w,h, parent.GetHandle(), None, + hInstance, 0) + assert hwnd != 0 + + # get the Interface for the Ax control + unknown = ct.POINTER(comtypes.IUnknown)() + res = atl.AtlAxGetControl(hwnd, ct.byref(unknown)) + assert res == hr.S_OK + self._ax = cc.GetBestInterface(unknown) + + # Fetch the interface for IOleInPlaceActiveObject. We'll use this + # later to call its TranslateAccelerator method so the AX Control can + # deal with things like tab traversal and such within itself. + self.ipao = self._ax.QueryInterface(myole4ax.IOleInPlaceActiveObject) + + # Use this object as the event sink for the ActiveX events + self._evt_connections = [] + self.AddEventSink(self) + + # Turn the window handle into a wx.Window and set this object to be that window + win = wx.PyAxBaseWindow_FromHWND(parent, hwnd) + self.PostCreate(win) + + # Set some wx.Window properties + if wxid == wx.ID_ANY: + wxid = wx.Window.NewControlId() + self.SetId(wxid) + self.SetName(name) + self.SetMinSize(size) + + self.Bind(wx.EVT_SET_FOCUS, self.OnSetFocus) + self.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus) + self.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroyWindow) + + def AddEventSink(self, sink, interface=None): + """ + Add a new target to search for method names that match the COM + Event names. + """ + self._evt_connections.append(cc.GetEvents(self._ax, sink, interface)) + + def GetCtrl(self): + """Easy access to the COM interface for the ActiveX Control""" + return self._ax + # And an even easier property + ctrl = property(GetCtrl) + + + def MSWTranslateMessage(self, msg): + # Pass native messages to the IOleInPlaceActiveObject + # interface before wx processes them, so navigation keys and + # accelerators can be dealt with the way that the AXControl + # wants them to be done. MSWTranslateMessage is called before + # wxWidgets handles and eats the navigation keys itself. + res = self.ipao.TranslateAccelerator(msg) + if res == hr.S_OK: + return True + else: + return wx.PyAxBaseWindow.MSWTranslateMessage(self, msg) + + + # TBD: Are the focus handlers needed? + def OnSetFocus(self, evt): + self.ipao.OnFrameWindowActivate(True) + + def OnKillFocus(self, evt): + self.ipao.OnFrameWindowActivate(False) + + def OnDestroyWindow(self, evt): + # release our event sinks while the window still exists + self._evt_connections = None + +#------------------------------------------------------------------------------ + + + + diff --git a/wx/lib/activexwrapper.py b/wx/lib/activexwrapper.py new file mode 100644 index 00000000..67d09d6f --- /dev/null +++ b/wx/lib/activexwrapper.py @@ -0,0 +1,155 @@ +#---------------------------------------------------------------------- +# Name: wxPython.lib.activexwrapper +# Purpose: a wxWindow derived class that can hold an ActiveX control +# +# Author: Robin Dunn +# +# RCS-ID: $Id$ +# Copyright: (c) 2000 by Total Control Software +# Licence: wxWindows license +#---------------------------------------------------------------------- +# 11/30/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o Updated for wx namespace +# o Tested with updated demo +# + +import wx + +try: + import win32ui + import pywin.mfc.activex + import win32com.client +except ImportError: + import sys + if hasattr(sys, "frozen"): + import os, win32api + dllpath = os.path.join(win32api.GetSystemDirectory(), 'MFC71.DLL') + if sys.version[:3] >= '2.4' and not os.path.exists(dllpath): + message = "%s not found" % dllpath + else: + raise # original error message + else: + message = "ActiveXWrapper requires PythonWin. Please install the PyWin32 package." + raise ImportError(message) + +##from win32con import WS_TABSTOP, WS_VISIBLE +WS_TABSTOP = 0x00010000 +WS_VISIBLE = 0x10000000 + +#---------------------------------------------------------------------- + + +def MakeActiveXClass(CoClass, eventClass=None, eventObj=None): + """ + Dynamically construct a new class that derives from wxWindow, the + ActiveX control and the appropriate COM classes. This new class + can be used just like the wxWindow class, but will also respond + appropriately to the methods and properties of the COM object. If + this class, a derived class or a mix-in class has method names + that match the COM object's event names, they will be called + automatically. + + CoClass -- A COM control class from a module generated by + makepy.py from a COM TypeLibrary. Can also accept a + CLSID. + + eventClass -- If given, this class will be added to the set of + base classes that the new class is drived from. It is + good for mix-in classes for catching events. + + eventObj -- If given, this object will be searched for attributes + by the new class's __getattr__ method, (like a mix-in + object.) This is useful if you want to catch COM + callbacks in an existing object, (such as the parent + window.) + + """ + + + if type(CoClass) == type(""): + # use the CLSID to get the real class + CoClass = win32com.client.CLSIDToClass(CoClass) + + # determine the base classes + axEventClass = CoClass.default_source + baseClasses = [pywin.mfc.activex.Control, wx.Window, CoClass, axEventClass] + if eventClass: + baseClasses.append(eventClass) + baseClasses = tuple(baseClasses) + + # define the class attributes + className = 'AXControl_'+CoClass.__name__ + classDict = { '__init__' : axw__init__, + '__getattr__' : axw__getattr__, + 'axw_OnSize' : axw_OnSize, + 'axw_OEB' : axw_OEB, + '_name' : className, + '_eventBase' : axEventClass, + '_eventObj' : eventObj, + 'Cleanup' : axw_Cleanup, + } + + # make a new class object + import new + classObj = new.classobj(className, baseClasses, classDict) + return classObj + + + + +# These functions will be used as methods in the new class +def axw__init__(self, parent, ID=-1, pos=wx.DefaultPosition, size=wx.DefaultSize, style=0): + + # init base classes + pywin.mfc.activex.Control.__init__(self) + wx.Window.__init__( self, parent, -1, pos, size, style|wx.NO_FULL_REPAINT_ON_RESIZE) + self.this.own(False) # this should be set in wx.Window.__init__ when it calls _setOORInfo, but... + + win32ui.EnableControlContainer() + self._eventObj = self._eventObj # move from class to instance + + # create a pythonwin wrapper around this wxWindow + handle = self.GetHandle() + self._wnd = win32ui.CreateWindowFromHandle(handle) + + # create the control + sz = self.GetSize() + self.CreateControl(self._name, WS_TABSTOP | WS_VISIBLE, + (0, 0, sz.width, sz.height), self._wnd, ID) + + # init the ax events part of the object + self._eventBase.__init__(self, self._dispobj_) + + # hook some wx events + self.Bind(wx.EVT_SIZE, self.axw_OnSize) + +def axw__getattr__(self, attr): + try: + return pywin.mfc.activex.Control.__getattr__(self, attr) + except AttributeError: + try: + eo = self.__dict__['_eventObj'] + return getattr(eo, attr) + except AttributeError: + raise AttributeError('Attribute not found: %s' % attr) + + +def axw_OnSize(self, event): + sz = self.GetClientSize() # get wxWindow size + self.MoveWindow((0, 0, sz.width, sz.height), 1) # move the AXControl + + +def axw_OEB(self, event): + pass + + +def axw_Cleanup(self): + #del self._wnd + self.close() + pass + + + + + diff --git a/wx/lib/analogclock/__init__.py b/wx/lib/analogclock/__init__.py new file mode 100644 index 00000000..03c3bc8a --- /dev/null +++ b/wx/lib/analogclock/__init__.py @@ -0,0 +1,144 @@ +__author__ = "E. A. Tacao " +__date__ = "15 Fev 2006, 22:00 GMT-03:00" +__version__ = "0.02" +__doc__ = """ +AnalogClock - an analog clock. + +This control creates an analog clock window. Its features include shadowing, +the ability to render numbers as well as any arbitrary polygon as tick marks, +resize marks and hands proportionally as the widget itself is resized, rotate +marks in a way the get aligned to the watch. It also has a dialog, accessed +via a context menu item, allowing one to change on the fly all of its settings. + + +Usage: + + AnalogClock(parent, id=-1, pos=wx.DefaultPosition, + size=wx.DefaultSize, style=wx.NO_BORDER, name="AnalogClock", + clockStyle=DEFAULT_CLOCK_STYLE, + minutesStyle=TICKS_CIRCLE, hoursStyle=TICKS_POLY) + +- parent, id, pos, size, style and name are used as in a wx.Window. Please + refer to the wx.Window docs for more details. + +- clockStyle defines the clock style, according to the options below: + + ==================== ================================ + SHOW_QUARTERS_TICKS Show marks for hours 3, 6, 9, 12 + SHOW_HOURS_TICKS Show marks for all hours + SHOW_MINUTES_TICKS Show marks for minutes + + SHOW_HOURS_HAND Show hours hand + SHOW_MINUTES_HAND Show minutes hand + SHOW_SECONDS_HAND Show seconds hand + + SHOW_SHADOWS Show hands and marks shadows + + ROTATE_TICKS Align tick marks to watch + OVERLAP_TICKS Draw tick marks for minutes even + when they match the hours marks. + + DEFAULT_CLOCK_STYLE The same as SHOW_HOURS_TICKS| + SHOW_MINUTES_TICKS| + SHOW_HOURS_HAND| + SHOW_MINUTES_HAND| + SHOW_SECONDS_HAND| + SHOW_SHADOWS|ROTATE_TICKS + ==================== ================================ + +- minutesStyle and hoursStyle define the the tick styles, according to the + options below: + + ================= ====================================== + TICKS_NONE Don't show tick marks. + TICKS_SQUARE Use squares as tick marks. + TICKS_CIRCLE Use circles as tick marks. + TICKS_POLY Use a polygon as tick marks. A + polygon can be passed using + SetTickPolygon, otherwise the default + polygon will be used. + TICKS_DECIMAL Use decimal numbers as tick marks. + TICKS_ROMAN Use Roman numbers as tick marks. + TICKS_BINARY Use binary numbers as tick marks. + TICKS_HEX Use hexadecimal numbers as tick marks. + ================= ====================================== + + +Notes: + +The 'target' keyword that's present in various of the AnalogClock methods may +accept one (or more, combined using '|') of the following values: + + ========= =========================================== + HOUR The values passed/retrieved are related to + the hours hand/ticks + + MINUTE The values passed/retrieved are related to + the minutes hand/ticks + + SECOND The values passed/retrieved are related to + the seconds hand/ticks + + ALL The same as HOUR|MINUTE|SECOND, i. e., the + values passed/retrieved are related to all + of the hours hands/ticks. This is the + default value in all methods. + ========= =========================================== + +It is legal to pass target=ALL to methods that don't handle seconds (tick +mark related methods). In such cases, ALL will be equivalent to HOUR|MINUTE. + +All of the 'Get' AnalogClock methods that allow the 'target' keyword +will always return a tuple, e. g.: + + ================================= ======================================== + GetHandSize(target=HOUR) Returns a 1 element tuple, containing + the size of the hours hand. + + GetHandSize(target=HOUR|MINUTE) Returns a 2 element tuple, containing + the sizes of the hours and the minutes + hands, respectively. + + GetHandSize(target=ALL) Returns a 3 element tuple, containing + or the sizes of the hours, minutes and + GetHandSize() seconds hands, respectively. + ================================= ======================================== + + +About: + +Most of the ideas and part of the code of AnalogClock were based on the +original wxPython's AnalogClock module, which was created by several folks on +the wxPython-users list. + +AnalogClock is distributed under the wxWidgets license. + +This code should meet the wxPython Coding Guidelines + and the wxPython Style Guide +. + +For all kind of problems, requests, enhancements, bug reports, etc, +please drop me an e-mail. + +For updates please visit . +""" + +# History: +# +# Version 0.02: +# - Module/namespace rearranges; +# - All '-1' occurrences meaning "use any id" were eliminated or replaced +# to 'wx.ID_ANY'. +# - Better names to the methods triggered by the context menu events. +# - Included small demo class code in analogclock.py. +# Version 0.01: +# - Initial release. + +#---------------------------------------------------------------------- + +from analogclock import AnalogClock, AnalogClockWindow +from styles import * + +# +## +### eof diff --git a/wx/lib/analogclock/analogclock.py b/wx/lib/analogclock/analogclock.py new file mode 100644 index 00000000..16a7c4d3 --- /dev/null +++ b/wx/lib/analogclock/analogclock.py @@ -0,0 +1,633 @@ +# AnalogClock's main class +# E. A. Tacao +# http://j.domaindlx.com/elements28/wxpython/ +# 15 Fev 2006, 22:00 GMT-03:00 +# Distributed under the wxWidgets license. +# +# For more info please see the __init__.py file. + +import wx + +from styles import * +from helpers import Dyer, Face, Hand, HandSet, TickSet, Box +from setup import Setup + +#---------------------------------------------------------------------- + +class AnalogClock(wx.PyWindow): + """An analog clock.""" + + def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, + size=wx.DefaultSize, style=wx.NO_BORDER, name="AnalogClock", + clockStyle=DEFAULT_CLOCK_STYLE, + minutesStyle=TICKS_CIRCLE, hoursStyle=TICKS_POLY): + + wx.PyWindow.__init__(self, parent, id, pos, size, style, name) + + # Base size for scale calc purposes. + self.basesize = wx.Size(348, 348) + + # Store some references. + self.clockStyle = clockStyle + self.minutesStyle = minutesStyle + self.hoursStyle = hoursStyle + + self.DrawHands = self._drawHands + self.DrawBox = self._drawBox + self.RecalcCoords = self._recalcCoords + + self.shadowOffset = 3 + + self.allHandStyles = [SHOW_HOURS_HAND, + SHOW_MINUTES_HAND, + SHOW_SECONDS_HAND] + + # Initialize clock face. + # + # By default we don't use colours or borders on the clock face. + bg = self.GetBackgroundColour() + face = Face(dyer=Dyer(bg, 0, bg)) + + # Initialize tick marks. + # + # TickSet is a set of tick marks; there's always two TickSets defined + # regardless whether they're being shown or not. + ticksM = TickSet(self, style=minutesStyle, size=5, kind="minutes") + ticksH = TickSet(self, style=hoursStyle, size=25, kind="hours", + rotate=clockStyle&ROTATE_TICKS) + + # Box holds the clock face and tick marks. + self.Box = Box(self, face, ticksM, ticksH) + + # Initialize hands. + # + # HandSet is the set of hands; there's always one HandSet defined + # regardless whether hands are being shown or not. + # + # A 'lenfac = 0.95', e.g., means that the lenght of that hand will + # be 95% of the maximum allowed hand lenght ('nice' maximum lenght). + handH = Hand(size=7, lenfac=0.7) + handM = Hand(size=5, lenfac=0.95) + handS = Hand(size=1, lenfac=0.95) + self.Hands = HandSet(self, handH, handM, handS) + + # Create the customization dialog. + self.Setup = None + + # Make a context menu. + popup1 = wx.NewId() + popup2 = wx.NewId() + cm = self.cm = wx.Menu() + cm.Append(popup1, "Customize...") + cm.Append(popup2, "About...") + + # Set event handlers. + self.Bind(wx.EVT_SIZE, self._OnSize) + self.Bind(wx.EVT_PAINT, self._OnPaint) + self.Bind(wx.EVT_ERASE_BACKGROUND, lambda evt: None) + self.Bind(wx.EVT_TIMER, self._OnTimer) + self.Bind(wx.EVT_WINDOW_DESTROY, self._OnDestroyWindow) + self.Bind(wx.EVT_CONTEXT_MENU, self._OnContextMenu) + self.Bind(wx.EVT_MENU, self._OnShowSetup, id=popup1) + self.Bind(wx.EVT_MENU, self._OnShowAbout, id=popup2) + + # Set initial size based on given size, or best size + self.SetInitialSize(size) + + # Do initial drawing (in case there is not an initial size event) + self.RecalcCoords(self.GetSize()) + self.DrawBox() + + # Initialize the timer that drives the update of the clock face. + # Update every half second to ensure that there is at least one true + # update during each realtime second. + self.timer = wx.Timer(self) + self.timer.Start(500) + + + def DoGetBestSize(self): + # Just pull a number out of the air. If there is a way to + # calculate this then it should be done... + size = wx.Size(50,50) + self.CacheBestSize(size) + return size + + + def _OnSize(self, evt): + size = self.GetClientSize() + if size.x < 1 or size.y < 1: + return + + self.RecalcCoords(size) + self.DrawBox() + + + def _OnPaint(self, evt): + dc = wx.BufferedPaintDC(self) + self.DrawHands(dc) + + + def _OnTimer(self, evt): + self.Refresh(False) + self.Update() + + + def _OnDestroyWindow(self, evt): + self.timer.Stop() + del self.timer + + + def _OnContextMenu(self, evt): + self.PopupMenu(self.cm) + + + def _OnShowSetup(self, evt): + if self.Setup is None: + self.Setup = Setup(self) + self.Setup.Show() + self.Setup.Raise() + + + def _OnShowAbout(self, evt): + msg = "AnalogClock\n\n" \ + "by Several folks on wxPython-users\n" \ + "with enhancements from E. A. Tacao." + title = "About..." + style = wx.OK|wx.ICON_INFORMATION + + dlg = wx.MessageDialog(self, msg, title, style) + dlg.ShowModal() + dlg.Destroy() + + + def _recalcCoords(self, size): + """ + Recalculates all coordinates/geometry and inits the faceBitmap + to make sure the buffer is always the same size as the window. + """ + + self.faceBitmap = wx.EmptyBitmap(*size.Get()) + + # Recalc all coords. + scale = min([float(size.width) / self.basesize.width, + float(size.height) / self.basesize.height]) + + centre = wx.Point(size.width / 2., size.height / 2.) + + self.Box.RecalcCoords(size, centre, scale) + self.Hands.RecalcCoords(size, centre, scale) + + # Try to find a 'nice' maximum length for the hands so that they won't + # overlap the tick marks. OTOH, if you do want to allow overlapping the + # lenfac value (defined on __init__ above) has to be set to + # something > 1. + niceradius = self.Box.GetNiceRadiusForHands(centre) + self.Hands.SetMaxRadius(niceradius) + + + def _drawBox(self): + """Draws clock face and tick marks onto the faceBitmap.""" + dc = wx.BufferedDC(None, self.faceBitmap) + dc.SetBackground(wx.Brush(self.GetBackgroundColour(), wx.SOLID)) + dc.Clear() + self.Box.Draw(dc) + + + def _drawHands(self, dc): + """ + Draws the face bitmap, created on the last DrawBox call, and + clock hands. + """ + dc.DrawBitmap(self.faceBitmap, 0, 0) + self.Hands.Draw(dc) + + + # Public methods -------------------------------------------------- + + def GetHandSize(self, target=ALL): + """Gets thickness of hands.""" + + return self.Hands.GetSize(target) + + + def GetHandFillColour(self, target=ALL): + """Gets fill colours of hands.""" + + return self.Hands.GetFillColour(target) + + + def GetHandBorderColour(self, target=ALL): + """Gets border colours of hands.""" + + return self.Hands.GetBorderColour(target) + + + def GetHandBorderWidth(self, target=ALL): + """Gets border widths of hands.""" + + return self.Hands.GetBorderWidth(target) + + + def GetTickSize(self, target=ALL): + """Gets sizes of ticks.""" + + return self.Box.GetTickSize(target) + + + + def GetTickFillColour(self, target=ALL): + """Gets fill colours of ticks.""" + + return self.Box.GetTickFillColour(target) + + + + def GetTickBorderColour(self, target=ALL): + """Gets border colours of ticks.""" + + return self.Box.GetTickBorderColour(target) + + + + def GetTickBorderWidth(self, target=ALL): + """Gets border widths of ticks.""" + + return self.Box.GetTickBorderWidth(target) + + + + def GetTickPolygon(self, target=ALL): + """ + Gets lists of points to be used as polygon shapes + when using the TICKS_POLY style. + """ + + return self.Box.GetTickPolygon(target) + + + + def GetTickFont(self, target=ALL): + """ + Gets fonts for tick marks when using TICKS_DECIMAL or + TICKS_ROMAN style. + """ + + return self.Box.GetTickFont(target) + + + + def GetTickOffset(self, target=ALL): + """Gets the distance of tick marks for hours from border.""" + + return self.Box.GetTickOffset(target) + + + + def GetFaceFillColour(self): + """Gets fill colours of watch.""" + + return self.Box.Face.GetFillColour() + + + + def GetFaceBorderColour(self): + """Gets border colours of watch.""" + + return self.Box.Face.GetBorderColour() + + + + def GetFaceBorderWidth(self): + """Gets border width of watch.""" + + return self.Box.Face.GetBorderWidth() + + + + def GetShadowColour(self): + """Gets the colour to be used to draw shadows.""" + + a_clock_part = self.Box + return a_clock_part.GetShadowColour() + + + + def GetClockStyle(self): + """Returns the current clock style.""" + + return self.clockStyle + + + def GetTickStyle(self, target=ALL): + """Gets the tick style(s).""" + + return self.Box.GetTickStyle(target) + + + def Reset(self): + """ + Forces an immediate recalculation and redraw of all clock + elements. + """ + size = self.GetClientSize() + if size.x < 1 or size.y < 1: + return + self.RecalcCoords(size) + self.DrawBox() + self.Refresh(False) + + + def SetHandSize(self, size, target=ALL): + """Sets thickness of hands.""" + + self.Hands.SetSize(size, target) + + + def SetHandFillColour(self, colour, target=ALL): + """Sets fill colours of hands.""" + + self.Hands.SetFillColour(colour, target) + + + def SetHandBorderColour(self, colour, target=ALL): + """Sets border colours of hands.""" + + self.Hands.SetBorderColour(colour, target) + + + def SetHandBorderWidth(self, width, target=ALL): + """Sets border widths of hands.""" + + self.Hands.SetBorderWidth(width, target) + + + def SetTickSize(self, size, target=ALL): + """Sets sizes of ticks.""" + + self.Box.SetTickSize(size, target) + self.Reset() + + + def SetTickFillColour(self, colour, target=ALL): + """Sets fill colours of ticks.""" + + self.Box.SetTickFillColour(colour, target) + self.Reset() + + + def SetTickBorderColour(self, colour, target=ALL): + """Sets border colours of ticks.""" + + self.Box.SetTickBorderColour(colour, target) + self.Reset() + + + def SetTickBorderWidth(self, width, target=ALL): + """Sets border widths of ticks.""" + + self.Box.SetTickBorderWidth(width, target) + self.Reset() + + + def SetTickPolygon(self, polygon, target=ALL): + """ + Sets lists of points to be used as polygon shapes + when using the TICKS_POLY style. + """ + + self.Box.SetTickPolygon(polygon, target) + self.Reset() + + + def SetTickFont(self, font, target=ALL): + """ + Sets fonts for tick marks when using text-based tick styles + such as TICKS_DECIMAL or TICKS_ROMAN. + """ + + self.Box.SetTickFont(font, target) + self.Reset() + + + def SetTickOffset(self, offset, target=ALL): + """Sets the distance of tick marks for hours from border.""" + + self.Box.SetTickOffset(offset, target) + self.Reset() + + + def SetFaceFillColour(self, colour): + """Sets fill colours of watch.""" + + self.Box.Face.SetFillColour(colour) + self.Reset() + + + def SetFaceBorderColour(self, colour): + """Sets border colours of watch.""" + + self.Box.Face.SetBorderColour(colour) + self.Reset() + + + def SetFaceBorderWidth(self, width): + """Sets border width of watch.""" + + self.Box.Face.SetBorderWidth(width) + self.Reset() + + + def SetShadowColour(self, colour): + """Sets the colour to be used to draw shadows.""" + + self.Hands.SetShadowColour(colour) + self.Box.SetShadowColour(colour) + self.Reset() + + + def SetClockStyle(self, style): + """ + Set the clock style, according to the options below. + + ==================== ================================ + SHOW_QUARTERS_TICKS Show marks for hours 3, 6, 9, 12 + SHOW_HOURS_TICKS Show marks for all hours + SHOW_MINUTES_TICKS Show marks for minutes + + SHOW_HOURS_HAND Show hours hand + SHOW_MINUTES_HAND Show minutes hand + SHOW_SECONDS_HAND Show seconds hand + + SHOW_SHADOWS Show hands and marks shadows + + ROTATE_TICKS Align tick marks to watch + OVERLAP_TICKS Draw tick marks for minutes even + when they match the hours marks. + ==================== ================================ + """ + + self.clockStyle = style + self.Box.SetIsRotated(style & ROTATE_TICKS) + self.Reset() + + + def SetTickStyle(self, style, target=ALL): + """ + Set the tick style, according to the options below. + + ================= ====================================== + TICKS_NONE Don't show tick marks. + TICKS_SQUARE Use squares as tick marks. + TICKS_CIRCLE Use circles as tick marks. + TICKS_POLY Use a polygon as tick marks. A + polygon can be passed using + SetTickPolygon, otherwise the default + polygon will be used. + TICKS_DECIMAL Use decimal numbers as tick marks. + TICKS_ROMAN Use Roman numbers as tick marks. + TICKS_BINARY Use binary numbers as tick marks. + TICKS_HEX Use hexadecimal numbers as tick marks. + ================= ====================================== + """ + + self.Box.SetTickStyle(style, target) + self.Reset() + + + def SetBackgroundColour(self, colour): + """Overriden base wx.Window method.""" + + wx.Window.SetBackgroundColour(self, colour) + self.Reset() + + + def SetForegroundColour(self, colour): + """ + Overriden base wx.Window method. This method sets a colour for + all hands and ticks at once. + """ + + wx.Window.SetForegroundColour(self, colour) + self.SetHandFillColour(colour) + self.SetHandBorderColour(colour) + self.SetTickFillColour(colour) + self.SetTickBorderColour(colour) + self.Reset() + + + def SetWindowStyle(self, *args, **kwargs): + """Overriden base wx.Window method.""" + + size = self.GetSize() + self.Freeze() + wx.Window.SetWindowStyle(self, *args, **kwargs) + self.SetSize((10, 10)) + self.SetSize(size) + self.Thaw() + + + def SetWindowStyleFlag(self, *args, **kwargs): + """Overriden base wx.Window method.""" + + self.SetWindowStyle(*args, **kwargs) + + +# For backwards compatibility ----------------------------------------- + +class AnalogClockWindow(AnalogClock): + """ + A simple derived class that provides some backwards compatibility + with the old analogclock module. + """ + def SetTickShapes(self, tsh, tsm=None): + self.SetTickPolygon(tsh) + + def SetHandWeights(self, h=None, m=None, s=None): + if h: + self.SetHandSize(h, HOUR) + if m: + self.SetHandSize(m, MINUTE) + if s: + self.SetHandSize(s, SECOND) + + def SetHandColours(self, h=None, m=None, s=None): + if h and not m and not s: + m=h + s=h + if h: + self.SetHandBorderColour(h, HOUR) + self.SetHandFillColour(h, HOUR) + if m: + self.SetHandBorderColour(m, MINUTE) + self.SetHandFillColour(m, MINUTE) + if s: + self.SetHandBorderColour(s, SECOND) + self.SetHandFillColour(s, SECOND) + + def SetTickColours(self, h=None, m=None): + if not m: + m=h + if h: + self.SetTickBorderColour(h, HOUR) + self.SetTickFillColour(h, HOUR) + if m: + self.SetTickBorderColour(m, MINUTE) + self.SetTickFillColour(m, MINUTE) + + def SetTickSizes(self, h=None, m=None): + if h: + self.SetTickSize(h, HOUR) + if m: + self.SetTickSize(m, MINUTE) + + def SetTickFontss(self, h=None, m=None): + if h: + self.SetTickFont(h, HOUR) + if m: + self.SetTickFont(m, MINUTE) + + + def SetMinutesOffset(self, o): + pass + + def SetShadowColour(self, s): + pass + + def SetWatchPenBrush(self, p=None, b=None): + if p: + self.SetFaceBorderColour(p.GetColour()) + self.SetFaceBorderWidth(p.GetWidth()) + if b: + self.SetFaceFillColour(b.GetColour()) + + def SetClockStyle(self, style): + style |= SHOW_HOURS_HAND|SHOW_MINUTES_HAND|SHOW_SECONDS_HAND + AnalogClock.SetClockStyle(self, style) + + def SetTickStyles(self, h=None, m=None): + if h: + self.SetTickStyle(h, HOUR) + if m: + self.SetTickStyle(h, MINUTE) + + +# Test stuff ---------------------------------------------------------- + +if __name__ == "__main__": + print wx.VERSION_STRING + + class AcDemoApp(wx.App): + def OnInit(self): + frame = wx.Frame(None, -1, "AnalogClock", size=(375, 375)) + clock = AnalogClock(frame) + frame.CentreOnScreen() + frame.Show() + return True + + acApp = AcDemoApp(0) + acApp.MainLoop() + + +# +## +### eof diff --git a/wx/lib/analogclock/helpers.py b/wx/lib/analogclock/helpers.py new file mode 100644 index 00000000..c459067d --- /dev/null +++ b/wx/lib/analogclock/helpers.py @@ -0,0 +1,985 @@ +# AnalogClock's base classes +# E. A. Tacao +# http://j.domaindlx.com/elements28/wxpython/ +# 15 Fev 2006, 22:00 GMT-03:00 +# Distributed under the wxWidgets license. + +from time import strftime, localtime +import math +import wx + +from styles import * + +#---------------------------------------------------------------------- + +_targets = [HOUR, MINUTE, SECOND] + +#---------------------------------------------------------------------- + +class Element: + """Base class for face, hands and tick marks.""" + + def __init__(self, idx=0, pos=None, size=None, offset=0, clocksize=None, + scale=1, rotate=False, kind=""): + + self.idx = idx + self.pos = pos + self.size = size + self.offset = offset + self.clocksize = clocksize + self.scale = scale + self.rotate = rotate + self.kind = kind + + self.text = None + self.angfac = [6, 30][self.kind == "hours"] + + + def _pol2rect(self, m, t): + return m * math.cos(math.radians(t)), m * math.sin(math.radians(t)) + + + def _rect2pol(self, x, y): + return math.hypot(x, y), math.degrees(math.atan2(y, x)) + + + def DrawRotated(self, dc, offset=0): + pass + + + def DrawStraight(self, dc, offset=0): + pass + + + def Draw(self, dc, offset=0): + if self.rotate: + self.DrawRotated(dc, offset) + else: + self.DrawStraight(dc, offset) + + + def RecalcCoords(self, clocksize, centre, scale): + pass + + + def GetSize(self): + return self.size + + + def GetOffset(self): + return self.offset + + + def GetIsRotated(self, rotate): + return self.rotate + + + def GetMaxSize(self, scale=1): + return self.size * scale + + + def GetScale(self): + return self.scale + + + def SetIsRotated(self, rotate): + self.rotate = rotate + + + def GetMaxSize(self, scale=1): + return self.size * scale + + + def GetPolygon(self): + return self.polygon + + + def SetPosition(self, pos): + self.pos = pos + + + def SetSize(self, size): + self.size = size + + + def SetOffset(self, offset): + self.offset = offset + + + def SetClockSize(self, clocksize): + self.clocksize = clocksize + + + def SetScale(self, scale): + self.scale = scale + + + def SetIsRotated(self, rotate): + self.rotate = rotate + + + def SetPolygon(self, polygon): + self.polygon = polygon + +#---------------------------------------------------------------------- + +class ElementWithDyer(Element): + """Base class for clock face and hands.""" + + def __init__(self, **kwargs): + self.dyer = kwargs.pop("dyer", Dyer()) + Element.__init__(self, **kwargs) + + + def GetFillColour(self): + return self.dyer.GetFillColour() + + + def GetBorderColour(self): + return self.dyer.GetBorderColour() + + + def GetBorderWidth(self): + return self.dyer.GetBorderWidth() + + + def GetShadowColour(self): + return self.dyer.GetShadowColour() + + + def SetFillColour(self, colour): + self.dyer.SetFillColour(colour) + + + def SetBorderColour(self, colour): + self.dyer.SetBorderColour(colour) + + + def SetBorderWidth(self, width): + self.dyer.SetBorderWidth(width) + + + def SetShadowColour(self, colour): + self.dyer.SetShadowColour(colour) + +#---------------------------------------------------------------------- + +class Face(ElementWithDyer): + """Holds info about the clock face.""" + + def __init__(self, **kwargs): + ElementWithDyer.__init__(self, **kwargs) + + + def Draw(self, dc): + self.dyer.Select(dc) + dc.DrawCircle(self.pos.x, self.pos.y, self.radius) + + + def RecalcCoords(self, clocksize, centre, scale): + self.radius = min(clocksize.Get()) / 2. - self.dyer.width / 2. + self.pos = centre + +#---------------------------------------------------------------------- + +class Hand(ElementWithDyer): + """Holds info about a clock hand.""" + + def __init__(self, **kwargs): + self.lenfac = kwargs.pop("lenfac") + ElementWithDyer.__init__(self, **kwargs) + + self.SetPolygon([[-1, 0], [0, -1], [1, 0], [0, 4]]) + + + def Draw(self, dc, end, offset=0): + radius, centre, r = end + angle = math.degrees(r) + polygon = self.polygon[:] + vscale = radius / max([y for x, y in polygon]) + + for i, (x, y) in enumerate(polygon): + x *= self.scale * self.size + y *= vscale * self.lenfac + m, t = self._rect2pol(x, y) + polygon[i] = self._pol2rect(m, t - angle) + + dc.DrawPolygon(polygon, centre.x + offset, centre.y + offset) + + + def RecalcCoords(self, clocksize, centre, scale): + self.pos = centre + self.scale = scale + +#---------------------------------------------------------------------- + +class TickSquare(Element): + """Holds info about a tick mark.""" + + def __init__(self, **kwargs): + Element.__init__(self, **kwargs) + + + def Draw(self, dc, offset=0): + width = height = self.size * self.scale + x = self.pos.x - width / 2. + y = self.pos.y - height / 2. + + dc.DrawRectangle(x + offset, y + offset, width, height) + +#---------------------------------------------------------------------- + +class TickCircle(Element): + """Holds info about a tick mark.""" + + def __init__(self, **kwargs): + Element.__init__(self, **kwargs) + + + def Draw(self, dc, offset=0): + radius = self.size * self.scale / 2. + x = self.pos.x + y = self.pos.y + + dc.DrawCircle(x + offset, y + offset, radius) + +#---------------------------------------------------------------------- + +class TickPoly(Element): + """Holds info about a tick mark.""" + + def __init__(self, **kwargs): + Element.__init__(self, **kwargs) + + self.SetPolygon([[0, 1], [1, 0], [2, 1], [1, 5]]) + + + def _calcPolygon(self): + width = max([x for x, y in self.polygon]) + height = max([y for x, y in self.polygon]) + tscale = self.size / max(width, height) * self.scale + polygon = [(x * tscale, y * tscale) for x, y in self.polygon] + + width = max([x for x, y in polygon]) + height = max([y for x, y in polygon]) + + return polygon, width, height + + + def DrawStraight(self, dc, offset=0): + polygon, width, height = self._calcPolygon() + + x = self.pos.x - width / 2. + y = self.pos.y - height / 2. + + dc.DrawPolygon(polygon, x + offset, y + offset) + + + def DrawRotated(self, dc, offset=0): + polygon, width, height = self._calcPolygon() + + angle = 360 - self.angfac * (self.idx + 1) + r = math.radians(angle) + + for i in range(len(polygon)): + m, t = self._rect2pol(*polygon[i]) + t -= angle + polygon[i] = self._pol2rect(m, t) + + x = self.pos.x - math.cos(r) * width / 2. - math.sin(r) * height / 2. + y = self.pos.y - math.cos(r) * height / 2. + math.sin(r) * width / 2. + + dc.DrawPolygon(polygon, x + offset, y + offset) + +#---------------------------------------------------------------------- + +class TickDecimal(Element): + """Holds info about a tick mark.""" + + def __init__(self, **kwargs): + Element.__init__(self, **kwargs) + + self.text = "%s" % (self.idx + 1) + + + def DrawStraight(self, dc, offset=0): + width, height = dc.GetTextExtent(self.text) + + x = self.pos.x - width / 2. + y = self.pos.y - height / 2. + + dc.DrawText(self.text, x + offset, y + offset) + + + def DrawRotated(self, dc, offset=0): + width, height = dc.GetTextExtent(self.text) + + angle = 360 - self.angfac * (self.idx + 1) + r = math.radians(angle) + + x = self.pos.x - math.cos(r) * width / 2. - math.sin(r) * height / 2. + y = self.pos.y - math.cos(r) * height / 2. + math.sin(r) * width / 2. + + dc.DrawRotatedText(self.text, x + offset, y + offset, angle) + + +#---------------------------------------------------------------------- + +class TickRoman(TickDecimal): + """Holds info about a tick mark.""" + + def __init__(self, **kwargs): + TickDecimal.__init__(self, **kwargs) + + self.text = ["I","II","III","IV","V", \ + "VI","VII","VIII","IX","X", \ + "XI","XII","XIII","XIV","XV", \ + "XVI","XVII","XVIII","XIX","XX", \ + "XXI","XXII","XXIII","XXIV","XXV", \ + "XXVI","XXVII","XXVIII","XXIX","XXX", \ + "XXXI","XXXII","XXXIII","XXXIV","XXXV", \ + "XXXVI","XXXVII","XXXVIII","XXXIX","XL", \ + "XLI","XLII","XLIII","XLIV","XLV", \ + "XLVI","XLVII","XLVIII","XLIX","L", \ + "LI","LII","LIII","LIV","LV", \ + "LVI","LVII","LVIII","LIX","LX"][self.idx] + +#---------------------------------------------------------------------- + +class TickBinary(TickDecimal): + """Holds info about a tick mark.""" + + def __init__(self, **kwargs): + TickDecimal.__init__(self, **kwargs) + + def d2b(n, b=""): + while n > 0: + b = str(n % 2) + b; n = n >> 1 + return b.zfill(4) + + self.text = d2b(self.idx + 1) + +#---------------------------------------------------------------------- + +class TickHex(TickDecimal): + """Holds info about a tick mark.""" + + def __init__(self, **kwargs): + TickDecimal.__init__(self, **kwargs) + + self.text = hex(self.idx + 1)[2:].upper() + +#---------------------------------------------------------------------- + +class TickNone(Element): + """Holds info about a tick mark.""" + + def __init__(self, **kwargs): + Element.__init__(self, **kwargs) + + + def Draw(self, dc, offset=0): + pass + +#---------------------------------------------------------------------- + +class Dyer: + """Stores info about colours and borders of clock Elements.""" + + def __init__(self, border=None, width=0, fill=None, shadow=None): + """ + self.border (wx.Colour) border colour + self.width (int) border width + self.fill (wx.Colour) fill colour + self.shadow (wx.Colour) shadow colour + """ + + self.border = border or \ + wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT) + self.fill = fill or \ + wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT) + self.shadow = shadow or \ + wx.SystemSettings.GetColour(wx.SYS_COLOUR_3DSHADOW) + self.width = width + + + def Select(self, dc, shadow=False): + """Selects the current settings into the dc.""" + + if not shadow: + dc.SetPen(wx.Pen(self.border, self.width, wx.SOLID)) + dc.SetBrush(wx.Brush(self.fill, wx.SOLID)) + dc.SetTextForeground(self.fill) + else: + dc.SetPen(wx.Pen(self.shadow, self.width, wx.SOLID)) + dc.SetBrush(wx.Brush(self.shadow, wx.SOLID)) + dc.SetTextForeground(self.shadow) + + + def GetFillColour(self): + return self.fill + + + def GetBorderColour(self): + return self.border + + + def GetBorderWidth(self): + return self.width + + + def GetShadowColour(self): + return self.shadow + + + def SetFillColour(self, colour): + self.fill = colour + + + def SetBorderColour(self, colour): + self.border = colour + + + def SetBorderWidth(self, width): + self.width = width + + + def SetShadowColour(self, colour): + self.shadow = colour + +#---------------------------------------------------------------------- + +class HandSet: + """Manages the set of hands.""" + + def __init__(self, parent, h, m, s): + self.parent = parent + + self.hands = [h, m, s] + self.radius = 1 + self.centre = wx.Point(1, 1) + + + def _draw(self, dc, shadow=False): + ends = [int(x) for x in strftime("%I %M %S", localtime()).split()] + + flags = [self.parent.clockStyle & flag \ + for flag in self.parent.allHandStyles] + + a_hand = self.hands[0] + + if shadow: + offset = self.parent.shadowOffset * a_hand.GetScale() + else: + offset = 0 + + for i, hand in enumerate(self.hands): + # Is this hand supposed to be drawn? + if flags[i]: + idx = ends[i] + # Is this the hours hand? + if i == 0: + idx = idx * 5 + ends[1] / 12 - 1 + # else prevent exceptions on leap seconds + elif idx <= 0 or idx > 60: + idx = 59 + # and adjust idx offset for minutes and non-leap seconds + else: + idx = idx - 1 + angle = math.radians(180 - 6 * (idx + 1)) + + hand.dyer.Select(dc, shadow) + hand.Draw(dc, (self.radius, self.centre, angle), offset) + + + def Draw(self, dc): + if self.parent.clockStyle & SHOW_SHADOWS: + self._draw(dc, True) + self._draw(dc) + + + def RecalcCoords(self, clocksize, centre, scale): + self.centre = centre + [hand.RecalcCoords(clocksize, centre, scale) for hand in self.hands] + + + def SetMaxRadius(self, radius): + self.radius = radius + + + def GetSize(self, target): + r = [] + for i, hand in enumerate(self.hands): + if _targets[i] & target: + r.append(hand.GetSize()) + return tuple(r) + + + def GetFillColour(self, target): + r = [] + for i, hand in enumerate(self.hands): + if _targets[i] & target: + r.append(hand.GetFillColour()) + return tuple(r) + + + def GetBorderColour(self, target): + r = [] + for i, hand in enumerate(self.hands): + if _targets[i] & target: + r.append(hand.GetBorderColour()) + return tuple(r) + + + def GetBorderWidth(self, target): + r = [] + for i, hand in enumerate(self.hands): + if _targets[i] & target: + r.append(hand.GetBorderWidth()) + return tuple(r) + + + def GetShadowColour(self): + r = [] + for i, hand in enumerate(self.hands): + if _targets[i] & target: + r.append(hand.GetShadowColour()) + return tuple(r) + + + def SetSize(self, size, target): + for i, hand in enumerate(self.hands): + if _targets[i] & target: + hand.SetSize(size) + + + def SetFillColour(self, colour, target): + for i, hand in enumerate(self.hands): + if _targets[i] & target: + hand.SetFillColour(colour) + + + def SetBorderColour(self, colour, target): + for i, hand in enumerate(self.hands): + if _targets[i] & target: + hand.SetBorderColour(colour) + + + def SetBorderWidth(self, width, target): + for i, hand in enumerate(self.hands): + if _targets[i] & target: + hand.SetBorderWidth(width) + + + def SetShadowColour(self, colour): + for i, hand in enumerate(self.hands): + hand.SetShadowColour(colour) + +#---------------------------------------------------------------------- + +class TickSet: + """Manages a set of tick marks.""" + + def __init__(self, parent, **kwargs): + self.parent = parent + self.dyer = Dyer() + self.noe = {"minutes": 60, "hours": 12}[kwargs["kind"]] + self.font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) + + style = kwargs.pop("style") + self.kwargs = kwargs + self.SetStyle(style) + + + def _draw(self, dc, shadow=False): + dc.SetFont(self.font) + + a_tick = self.ticks[0] + + if shadow: + offset = self.parent.shadowOffset * a_tick.GetScale() + else: + offset = 0 + + clockStyle = self.parent.clockStyle + + for idx, tick in self.ticks.items(): + draw = False + + # Are we a set of hours? + if self.noe == 12: + # Should we show all hours ticks? + if clockStyle & SHOW_HOURS_TICKS: + draw = True + # Or is this tick a quarter and should we show only quarters? + elif clockStyle & SHOW_QUARTERS_TICKS and not (idx + 1) % 3.: + draw = True + # Are we a set of minutes and minutes should be shown? + elif self.noe == 60 and clockStyle & SHOW_MINUTES_TICKS: + # If this tick occupies the same position of an hour/quarter + # tick, should we still draw it anyway? + if clockStyle & OVERLAP_TICKS: + draw = True + # Right, sir. I promise I won't overlap any tick. + else: + # Ensure that this tick won't overlap an hour tick. + if clockStyle & SHOW_HOURS_TICKS: + if (idx + 1) % 5.: + draw = True + # Ensure that this tick won't overlap a quarter tick. + elif clockStyle & SHOW_QUARTERS_TICKS: + if (idx + 1) % 15.: + draw = True + # We're not drawing quarters nor hours, so we can draw all + # minutes ticks. + else: + draw = True + + if draw: + tick.Draw(dc, offset) + + + def Draw(self, dc): + if self.parent.clockStyle & SHOW_SHADOWS: + self.dyer.Select(dc, True) + self._draw(dc, True) + self.dyer.Select(dc) + self._draw(dc) + + + def RecalcCoords(self, clocksize, centre, scale): + a_tick = self.ticks[0] + + size = a_tick.GetMaxSize(scale) + maxsize = size + + # Try to find a 'good' max size for text-based ticks. + if a_tick.text is not None: + self.font.SetPointSize(size) + dc = wx.MemoryDC() + dc.SelectObject(wx.EmptyBitmap(*clocksize.Get())) + dc.SetFont(self.font) + maxsize = size + for tick in self.ticks.values(): + maxsize = max(*(dc.GetTextExtent(tick.text) + (maxsize,))) + + radius = self.radius = min(clocksize.Get()) / 2. - \ + self.dyer.width / 2. - \ + maxsize / 2. - \ + a_tick.GetOffset() * scale - \ + self.parent.shadowOffset * scale + + # If we are a set of hours, the number of elements of this tickset is + # 12 and ticks are separated by a distance of 30 degrees; + # if we are a set of minutes, the number of elements of this tickset is + # 60 and ticks are separated by a distance of 6 degrees. + angfac = [6, 30][self.noe == 12] + + for i, tick in self.ticks.items(): + tick.SetClockSize(clocksize) + tick.SetScale(scale) + + deg = 180 - angfac * (i + 1) + angle = math.radians(deg) + + x = centre.x + radius * math.sin(angle) + y = centre.y + radius * math.cos(angle) + + tick.SetPosition(wx.Point(x, y)) + + + def GetSize(self): + return self.kwargs["size"] + + + def GetFillColour(self): + return self.dyer.GetFillColour() + + + def GetBorderColour(self): + return self.dyer.GetBorderColour() + + + def GetBorderWidth(self): + return self.dyer.GetBorderWidth() + + + def GetPolygon(self): + a_tick = self.ticks.values()[0] + return a_tick.GetPolygon() + + + def GetFont(self): + return self.font + + + def GetOffset(self): + a_tick = self.ticks[0] + return a_tick.GetOffset() + + + def GetShadowColour(self): + return self.dyer.GetShadowColour() + + + def GetIsRotated(self): + a_tick = self.ticks[0] + return a_tick.GetIsRotated() + + + def GetStyle(self): + return self.style + + + def SetSize(self, size): + self.kwargs["size"] = size + [tick.SetSize(size) for tick in self.ticks.values()] + + + def SetFillColour(self, colour): + self.dyer.SetFillColour(colour) + + + def SetBorderColour(self, colour): + self.dyer.SetBorderColour(colour) + + + def SetBorderWidth(self, width): + self.dyer.SetBorderWidth(width) + + + def SetPolygon(self, polygon): + [tick.SetPolygon(polygon) for tick in self.ticks.values()] + + + def SetFont(self, font): + self.font = font + + + def SetOffset(self, offset): + self.kwargs["offset"] = offset + [tick.SetOffset(offset) for tick in self.ticks.values()] + + + def SetShadowColour(self, colour): + self.dyer.SetShadowColour(colour) + + + def SetIsRotated(self, rotate): + self.kwargs["rotate"] = rotate + [tick.SetIsRotated(rotate) for tick in self.ticks.values()] + + + def SetStyle(self, style): + self.style = style + tickclass = allTickStyles[style] + self.kwargs["rotate"] = self.parent.clockStyle & ROTATE_TICKS + + self.ticks = {} + for i in range(self.noe): + self.kwargs["idx"] = i + self.ticks[i] = tickclass(**self.kwargs) + +#---------------------------------------------------------------------- + +class Box: + """Gathers info about the clock face and tick sets.""" + + def __init__(self, parent, Face, TicksM, TicksH): + self.parent = parent + self.Face = Face + self.TicksH = TicksH + self.TicksM = TicksM + + + def GetNiceRadiusForHands(self, centre): + a_tick = self.TicksM.ticks[0] + scale = a_tick.GetScale() + bw = max(self.TicksH.dyer.width / 2. * scale, + self.TicksM.dyer.width / 2. * scale) + + mgt = self.TicksM.ticks[59] + my = mgt.pos.y + mgt.GetMaxSize(scale) + bw + + hgt = self.TicksH.ticks[11] + hy = hgt.pos.y + hgt.GetMaxSize(scale) + bw + + niceradius = centre.y - max(my, hy) + return niceradius + + + def Draw(self, dc): + [getattr(self, attr).Draw(dc) \ + for attr in ["Face", "TicksM", "TicksH"]] + + + def RecalcCoords(self, size, centre, scale): + [getattr(self, attr).RecalcCoords(size, centre, scale) \ + for attr in ["Face", "TicksH", "TicksM"]] + + + def GetTickSize(self, target): + r = [] + for i, attr in enumerate(["TicksH", "TicksM"]): + if _targets[i] & target: + tick = getattr(self, attr) + r.append(tick.GetSize()) + return tuple(r) + + + def GetTickFillColour(self, target): + r = [] + for i, attr in enumerate(["TicksH", "TicksM"]): + if _targets[i] & target: + tick = getattr(self, attr) + r.append(tick.GetFillColour()) + return tuple(r) + + + def GetTickBorderColour(self, target): + r = [] + for i, attr in enumerate(["TicksH", "TicksM"]): + if _targets[i] & target: + tick = getattr(self, attr) + r.append(tick.GetBorderColour()) + return tuple(r) + + + def GetTickBorderWidth(self, target): + r = [] + for i, attr in enumerate(["TicksH", "TicksM"]): + if _targets[i] & target: + tick = getattr(self, attr) + r.append(tick.GetBorderWidth()) + return tuple(r) + + + def GetTickPolygon(self, target): + r = [] + for i, attr in enumerate(["TicksH", "TicksM"]): + if _targets[i] & target: + tick = getattr(self, attr) + r.append(tick.GetPolygon()) + return tuple(r) + + + def GetTickFont(self, target): + r = [] + for i, attr in enumerate(["TicksH", "TicksM"]): + if _targets[i] & target: + tick = getattr(self, attr) + r.append(tick.GetFont()) + return tuple(r) + + + def GetIsRotated(self): + a_tickset = self.TicksH + return a_tickset.GetIsRotated() + + + def GetTickOffset(self, target): + r = [] + for i, attr in enumerate(["TicksH", "TicksM"]): + if _targets[i] & target: + tick = getattr(self, attr) + r.append(tick.GetOffset()) + return tuple(r) + + + def GetShadowColour(self): + a_tickset = self.TicksH + return a_tickset.GetShadowColour() + + + def GetTickStyle(self, target): + r = [] + for i, attr in enumerate(["TicksH", "TicksM"]): + if _targets[i] & target: + tick = getattr(self, attr) + r.append(tick.GetStyle()) + return tuple(r) + + + def SetTickSize(self, size, target): + for i, attr in enumerate(["TicksH", "TicksM"]): + if _targets[i] & target: + tick = getattr(self, attr) + tick.SetSize(size) + + + def SetTickFillColour(self, colour, target): + for i, attr in enumerate(["TicksH", "TicksM"]): + if _targets[i] & target: + tick = getattr(self, attr) + tick.SetFillColour(colour) + + + def SetTickBorderColour(self, colour, target): + for i, attr in enumerate(["TicksH", "TicksM"]): + if _targets[i] & target: + tick = getattr(self, attr) + tick.SetBorderColour(colour) + + + def SetTickBorderWidth(self, width, target): + for i, attr in enumerate(["TicksH", "TicksM"]): + if _targets[i] & target: + tick = getattr(self, attr) + tick.SetBorderWidth(width) + + + def SetTickPolygon(self, polygon, target): + for i, attr in enumerate(["TicksH", "TicksM"]): + if _targets[i] & target: + tick = getattr(self, attr) + tick.SetPolygon(polygon) + + + def SetTickFont(self, font, target): + fs = font.GetNativeFontInfoDesc() + for i, attr in enumerate(["TicksH", "TicksM"]): + if _targets[i] & target: + tick = getattr(self, attr) + tick.SetFont(wx.FontFromNativeInfoString(fs)) + + + def SetIsRotated(self, rotate): + [getattr(self, attr).SetIsRotated(rotate) \ + for attr in ["TicksH", "TicksM"]] + + + def SetTickOffset(self, offset, target): + for i, attr in enumerate(["TicksH", "TicksM"]): + if _targets[i] & target: + tick = getattr(self, attr) + tick.SetOffset(offset) + + + def SetShadowColour(self, colour): + for attr in ["TicksH", "TicksM"]: + tick = getattr(self, attr) + tick.SetShadowColour(colour) + + + def SetTickStyle(self, style, target): + for i, attr in enumerate(["TicksH", "TicksM"]): + if _targets[i] & target: + tick = getattr(self, attr) + tick.SetStyle(style) + +#---------------------------------------------------------------------- + +# Relationship between styles and ticks class names. +allTickStyles = {TICKS_BINARY: TickBinary, + TICKS_CIRCLE: TickCircle, + TICKS_DECIMAL: TickDecimal, + TICKS_HEX: TickHex, + TICKS_NONE: TickNone, + TICKS_POLY: TickPoly, + TICKS_ROMAN: TickRoman, + TICKS_SQUARE: TickSquare} + + +# +## +### eof diff --git a/wx/lib/analogclock/lib_setup/__init__.py b/wx/lib/analogclock/lib_setup/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/wx/lib/analogclock/lib_setup/buttontreectrlpanel.py b/wx/lib/analogclock/lib_setup/buttontreectrlpanel.py new file mode 100644 index 00000000..b76aea6b --- /dev/null +++ b/wx/lib/analogclock/lib_setup/buttontreectrlpanel.py @@ -0,0 +1,290 @@ +__author__ = "E. A. Tacao " +__date__ = "12 Fev 2006, 22:00 GMT-03:00" +__version__ = "0.03" +__doc__ = """ +ButtonTreeCtrlPanel is a widget where one can place check buttons, tri-state +check buttons, radio buttons, both, and the ability to display them +hierarchically. + + +About: + +ButtonTreeCtrlPanel is distributed under the wxWidgets license. + +For all kind of problems, requests, enhancements, bug reports, etc, +please drop me an e-mail. + +For updates please visit . +""" + +import cStringIO + +import wx +from wx.lib.newevent import NewEvent + +#---------------------------------------------------------------------------- + +(ButtonTreeCtrlPanelEvent, EVT_BUTTONTREECTRLPANEL) = NewEvent() +EVT_CHANGED = EVT_BUTTONTREECTRLPANEL + +#---------------------------------------------------------------------------- + +class ButtonTreeCtrlPanel(wx.Panel): + def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, + size=wx.DefaultSize, style=wx.WANTS_CHARS): + wx.Panel.__init__(self, parent, id, pos, size, style) + + self.tree = wx.TreeCtrl(self, style=wx.TR_NO_LINES|wx.TR_HIDE_ROOT) + + il = self.il = wx.ImageList(16, 16) + self.tree.SetImageList(il) + + for bl in ["checkbox_checked", "checkbox_unchecked", "checkbox_tri", + "radiobox_checked", "radiobox_unchecked"]: + bitmap = getattr(self.__class__, bl).GetBitmap() + setattr(self, bl, il.Add(bitmap)) + + bmp = wx.ArtProvider.GetBitmap(wx.ART_FOLDER, wx.ART_TOOLBAR, (16, 16)) + self.empty_bitmap = il.Add(bmp) + + self.root = self.tree.AddRoot("Root Item for ButtonTreeCtrlPanel") + + self.Bind(wx.EVT_SIZE, self.OnSize) + self.tree.Bind(wx.EVT_LEFT_DCLICK, self.OnLeftClicks) + self.tree.Bind(wx.EVT_LEFT_DOWN, self.OnLeftClicks) + self.tree.Bind(wx.EVT_RIGHT_DOWN, self.OnRightClick) + + self.allitems = [] + + wx.CallAfter(self.OnSize) + + + def _doLogicTest(self, style, value, item): + if style in [wx.CHK_2STATE, wx.CHK_3STATE]: + n = [self.checkbox_unchecked, self.checkbox_checked, \ + self.checkbox_tri][value] + + self.tree.SetPyData(item, (value, style)) + self.tree.SetItemImage(item, n, wx.TreeItemIcon_Normal) + + elif style == wx.RB_SINGLE: + if value: + parent = self.tree.GetItemParent(item) + (child, cookie) = self.tree.GetFirstChild(parent) + + if self.tree.GetPyData(child): + self.tree.SetPyData(child, (False, wx.RB_SINGLE)) + self.tree.SetItemImage(child, self.radiobox_unchecked, \ + wx.TreeItemIcon_Normal) + + for x in range(1, self.tree.GetChildrenCount(parent, False)): + (child, cookie) = self.tree.GetNextChild(parent, cookie) + + if self.tree.GetPyData(child): + self.tree.SetPyData(child, (False, wx.RB_SINGLE)) + self.tree.SetItemImage(child, self.radiobox_unchecked, \ + wx.TreeItemIcon_Normal) + + self.tree.SetPyData(item, (True, wx.RB_SINGLE)) + self.tree.SetItemImage(item, self.radiobox_checked, \ + wx.TreeItemIcon_Normal) + + else: + self.tree.SetPyData(item, (False, wx.RB_SINGLE)) + self.tree.SetItemImage(item, self.radiobox_unchecked, \ + wx.TreeItemIcon_Normal) + + + def _getItems(self, parent=None, value=None): + if not parent: + parent = self.root + cil = [] + (child, cookie) = self.tree.GetFirstChild(parent) + if child.IsOk(): + d = self.tree.GetPyData(child) + if value is None or (d and d[0] == value): + cil.append(child) + for x in range(1, self.tree.GetChildrenCount(parent, False)): + (child, cookie) = self.tree.GetNextChild(parent, cookie) + if child.IsOk(): + d = self.tree.GetPyData(child) + if value is None or (d and d[0] == value): + cil.append(child) + return cil + + + def AddItem(self, label, bmp=None, parent=None, style=None, value=False): + v = None + + if bmp: + n = self.il.Add(bmp) + if not parent: + parent = self.root + if style is not None: + v = (value, style) + + this_item = self.tree.AppendItem(parent, label) + self.tree.SetPyData(this_item, v) + + if v: + self._doLogicTest(style, value, this_item) + else: + if bmp is None: + bmp = self.empty_bitmap + else: + bmp = self.il.Add(bmp) + + self.tree.SetItemImage(this_item, bmp, wx.TreeItemIcon_Normal) + + self.allitems.append(this_item) + [self.tree.Expand(x) for x in self.allitems] + + return this_item + + + def ExpandItem(self, item): + self.tree.Expand(item) + + + def CollapseItem(self, item): + self.tree.Collapse(item) + + + def EnsureFirstVisible(self): + (child, cookie) = self.tree.GetFirstChild(self.root) + if child.IsOk(): + self.tree.SelectItem(child) + self.tree.EnsureVisible(child) + + + def SetItemValue(self, item, value): + data = self.tree.GetPyData(item) + if data: + self._doLogicTest(data[1], value, item) + + + def GetItemValue(self, item): + data = self.tree.GetPyData(item) + if data: + return data[0] + else: + return None + + + def GetItemByLabel(self, label, parent=None): + r = None + for item in self._getItems(parent): + if self.tree.GetItemText(item) == label: + r = item; break + return r + + + def GetAllItems(self): + return self.allitems + + + def GetRootItems(self): + cil = [] + for x in range(0, len(self.allitems)): + d = self.tree.GetPyData(self.allitems[x]) + if not d: + cil.append(self.allitems[x]) + return cil + + + def GetStringRootItems(self): + return [self.tree.GetItemText(x) for x in self.GetRootItems] + + + def GetItemsUnchecked(self, parent=None): + return self._getItems(parent, 0) + + + def GetItemsChecked(self, parent=None): + return self._getItems(parent, 1) + + + def GetItemsTri(self, parent=None): + return self._getItems(parent, 2) + + + def GetStringItemsUnchecked(self, parent=None): + return [self.tree.GetItemText(x) \ + for x in self.GetItemsUnchecked(parent)] + + + def GetStringItemsChecked(self, parent=None): + return [self.tree.GetItemText(x) for x in self.GetItemsChecked(parent)] + + + def GetStringItemsTri(self, parent=None): + return [self.tree.GetItemText(x) for x in self.GetItemsTri(parent)] + + + def OnRightClick(self, evt): + item, flags = self.tree.HitTest(evt.GetPosition()) + self.tree.SelectItem(item) + + + def OnLeftClicks(self, evt): + item, flags = self.tree.HitTest(evt.GetPosition()) + if item: + text, data = self.tree.GetItemText(item), self.tree.GetPyData(item) + if data: + style = data[1] + if style == wx.CHK_2STATE: + value = not data[0] + elif style == wx.CHK_3STATE: + value = data[0] + 1 + if value == 3: value = 0 + else: + value = True + + self._doLogicTest(style, value, item) + + if value <> data[0]: + nevt = ButtonTreeCtrlPanelEvent(obj=self, id=self.GetId(), + item=item, val=value) + wx.PostEvent(self, nevt) + + evt.Skip() + + + def OnSize(self, evt=None): + self.tree.SetSize(self.GetClientSize()) + +# # Images generated by encode_bitmaps.py ----------------------------- +from wx.lib.embeddedimage import PyEmbeddedImage + +ButtonTreeCtrlPanel.checkbox_unchecked = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAEFJ" + "REFUOI3tkzsOACAUwsrT+9/Yz6yDieJkZKfpAFIknITVBjJAq6XtFhVJ9wxm6iqzrW3wAU8A" + "hiGdTNo2kHvnDr+YDCrzE+JlAAAAAElFTkSuQmCC") + +ButtonTreeCtrlPanel.radiobox_checked = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAHFJ" + "REFUOI2tUtESgCAIA+3//1jpqW7R5tkRb8o2GODeulWildhmdqhEzBH49tad4TxbyMQXIQk9" + "BJCcgSpHZ8DaVRZugasCAmOOYJXxT24BQau5lNcoBdCK8m8mtqAILE87YJ7VHP49pJXQ9il/" + "jfIaT195QDiwOHL5AAAAAElFTkSuQmCC") + +ButtonTreeCtrlPanel.radiobox_unchecked = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAGdJ" + "REFUOI3NkksSgDAIQ4F6/xtru9LBmHTq4EJ2Hchr+LhHs0pESW1mm0r0Y+/57dGc1Tm2gMKH" + "AEA3QBZjocrRGTC7qoULcP6gCnMuuylv4UcA1h8GmxN1wCAK/O0hzUDLp/w2ylsY3w4wQW9/" + "cegAAAAASUVORK5CYII=") + +ButtonTreeCtrlPanel.checkbox_checked = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAGdJ" + "REFUOI2tk1EOgDAIQ1vm/W+s82uJqbAxkW9eU6CQ1lApK9EADgDo19l3QVrjfw5UdVbqNu0g" + "GjMlMNvRS0CbVwt2HQzoCUf7CUfIwK6ANq8u4zoYUOas4QgZGJAgfYl0OcqsvvMNP8koKiUm" + "7JsAAAAASUVORK5CYII=") + +ButtonTreeCtrlPanel.checkbox_tri = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAHBJ" + "REFUOI2tk0EOgDAIBJfqq9Sj+mj1aP1We2piCCCKnJnN0GyJUofIpBANoAeAaRzKW/DMF/1n" + "wFOt4bZug2PfxDNdARosBvBlC1YNGnSH52UV30c9wQOLAXzZglWDBj3BaoAXBliRvlQ6XGWK" + "fucKTYUl4c5UOHYAAAAASUVORK5CYII=") + +# +## +### eof diff --git a/wx/lib/analogclock/lib_setup/colourselect.py b/wx/lib/analogclock/lib_setup/colourselect.py new file mode 100644 index 00000000..f3a1ddf7 --- /dev/null +++ b/wx/lib/analogclock/lib_setup/colourselect.py @@ -0,0 +1,80 @@ +# AnalogClock's colour selector for setup dialog +# E. A. Tacao +# http://j.domaindlx.com/elements28/wxpython/ +# 15 Fev 2006, 22:00 GMT-03:00 +# Distributed under the wxWidgets license. + +import wx +from wx.lib.newevent import NewEvent +from wx.lib.buttons import GenBitmapButton + +#---------------------------------------------------------------------------- + +(ColourSelectEvent, EVT_COLOURSELECT) = NewEvent() + +#---------------------------------------------------------------------------- + +class ColourSelect(GenBitmapButton): + def __init__(self, parent, size=(21, 21), value=wx.BLACK): + w, h = size[0] - 5, size[1] - 5 + GenBitmapButton.__init__(self, parent, wx.ID_ANY, wx.EmptyBitmap(w, h), + size=size) + self.SetBezelWidth(1) + + self.parent = parent + self.SetValue(value) + + self.parent.Bind(wx.EVT_BUTTON, self.OnClick, self) + + + def _makeBitmap(self): + bdr = 8; w, h = self.GetSize() + bmp = wx.EmptyBitmap(w - bdr, h - bdr) + + dc = wx.MemoryDC() + dc.SelectObject(bmp) + dc.SetBackground(wx.Brush(self.value, wx.SOLID)) + dc.Clear() + dc.SelectObject(wx.NullBitmap) + + self.SetBitmapLabel(bmp) + self.Refresh() + + + def GetValue(self): + return self.value + + + def SetValue(self, value): + self.value = value + self._makeBitmap() + + + def OnClick(self, event): + win = wx.GetTopLevelParent(self) + + data = wx.ColourData() + data.SetChooseFull(True) + data.SetColour(self.value) + [data.SetCustomColour(colour_index, win.customcolours[colour_index]) + for colour_index in range(0, 16)] + + dlg = wx.ColourDialog(win, data) + dlg.SetTitle("Select Colour") + changed = dlg.ShowModal() == wx.ID_OK + + if changed: + data = dlg.GetColourData() + self.SetValue(data.GetColour()) + win.customcolours = [data.GetCustomColour(colour_index) \ + for colour_index in range(0, 16)] + dlg.Destroy() + + if changed: + nevt = ColourSelectEvent(id=self.GetId(), obj=self, val=self.value) + wx.PostEvent(self.parent, nevt) + + +# +## +### eof diff --git a/wx/lib/analogclock/lib_setup/fontselect.py b/wx/lib/analogclock/lib_setup/fontselect.py new file mode 100644 index 00000000..9f93e024 --- /dev/null +++ b/wx/lib/analogclock/lib_setup/fontselect.py @@ -0,0 +1,61 @@ +# AnalogClock's font selector for setup dialog +# E. A. Tacao +# http://j.domaindlx.com/elements28/wxpython/ +# 15 Fev 2006, 22:00 GMT-03:00 +# Distributed under the wxWidgets license. + +import wx +from wx.lib.newevent import NewEvent +from wx.lib.buttons import GenButton + +#---------------------------------------------------------------------------- + +(FontSelectEvent, EVT_FONTSELECT) = NewEvent() + +#---------------------------------------------------------------------------- + +class FontSelect(GenButton): + def __init__(self, parent, size=(75, 21), value=None): + GenButton.__init__(self, parent, wx.ID_ANY, label="Select...", + size=size) + self.SetBezelWidth(1) + + self.parent = parent + self.SetValue(value) + + self.parent.Bind(wx.EVT_BUTTON, self.OnClick, self) + + + def GetValue(self): + return self.value + + + def SetValue(self, value): + if value is None: + value = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) + self.value = value + + + def OnClick(self, event): + data = wx.FontData() + data.EnableEffects(False) + font = self.value; font.SetPointSize(10) + data.SetInitialFont(font) + + dlg = wx.FontDialog(self, data) + changed = dlg.ShowModal() == wx.ID_OK + + if changed: + data = dlg.GetFontData() + self.value = data.GetChosenFont() + self.Refresh() + dlg.Destroy() + + if changed: + nevt = FontSelectEvent(id=self.GetId(), obj=self, val=self.value) + wx.PostEvent(self.parent, nevt) + + +# +## +### eof diff --git a/wx/lib/analogclock/setup.py b/wx/lib/analogclock/setup.py new file mode 100644 index 00000000..c09d2fc0 --- /dev/null +++ b/wx/lib/analogclock/setup.py @@ -0,0 +1,473 @@ +# AnalogClock setup dialog +# E. A. Tacao +# http://j.domaindlx.com/elements28/wxpython/ +# 15 Fev 2006, 22:00 GMT-03:00 +# Distributed under the wxWidgets license. + +import wx + +import styles +import lib_setup.colourselect as csel +import lib_setup.fontselect as fsel +import lib_setup.buttontreectrlpanel as bt + +#---------------------------------------------------------------------- + +_window_styles = ['wx.SIMPLE_BORDER', 'wx.DOUBLE_BORDER', 'wx.SUNKEN_BORDER', + 'wx.RAISED_BORDER', 'wx.STATIC_BORDER', 'wx.NO_BORDER'] + +#---------------------------------------------------------------------- + +class _GroupBase(wx.Panel): + def __init__(self, parent, title, group): + wx.Panel.__init__(self, parent) + + self.parent = parent + self.clock = self.parent.clock + self.group = group + self.target = {"Hours": styles.HOUR, + "Minutes": styles.MINUTE, + "Seconds": styles.SECOND}.get(title) + + self.Bind(fsel.EVT_FONTSELECT, self.OnSelectFont) + self.Bind(csel.EVT_COLOURSELECT, self.OnSelectColour) + self.Bind(wx.EVT_SPINCTRL, self.OnSpin) + self.Bind(wx.EVT_TEXT_ENTER, self.OnSpin) + self.Bind(wx.EVT_CHOICE, self.OnChoice) + + + def OnSelectFont(self, evt): + self.clock.SetTickFont(evt.val, self.target) + + + def OnSelectColour(self, evt): + obj = evt.obj; val = evt.val + + if hasattr(self, "fc") and obj == self.fc: + if self.group == "Hands": + self.clock.SetHandFillColour(val, self.target) + elif self.group == "Ticks": + self.clock.SetTickFillColour(val, self.target) + elif self.group == "Face": + self.clock.SetFaceFillColour(val) + + elif hasattr(self, "bc") and obj == self.bc: + if self.group == "Hands": + self.clock.SetHandBorderColour(val, self.target) + elif self.group == "Ticks": + self.clock.SetTickBorderColour(val, self.target) + elif self.group == "Face": + self.clock.SetFaceBorderColour(val) + + elif hasattr(self, "sw") and obj == self.sw: + self.clock.SetShadowColour(val) + + elif hasattr(self, "bg") and obj == self.bg: + self.clock.SetBackgroundColour(val) + + elif hasattr(self, "fg") and obj == self.fg: + self.clock.SetForegroundColour(val) + self.parent.GetGrandParent().UpdateControls() + + + def OnSpin(self, evt): + obj = evt.GetEventObject(); val = evt.GetInt() + + if hasattr(self, "bw") and obj == self.bw: + if self.group == "Hands": + self.clock.SetHandBorderWidth(val, self.target) + elif self.group == "Ticks": + self.clock.SetTickBorderWidth(val, self.target) + elif self.group == "Face": + self.clock.SetFaceBorderWidth(val) + + elif hasattr(self, "sz") and obj == self.sz: + if self.group == "Hands": + self.clock.SetHandSize(val, self.target) + elif self.group == "Ticks": + self.clock.SetTickSize(val, self.target) + + elif hasattr(self, "of") and obj == self.of: + self.clock.SetTickOffset(val, self.target) + + + def OnChoice(self, evt): + self.clock.SetWindowStyle(eval(evt.GetString())) + + + def UpdateControls(self): + if hasattr(self, "ft"): + self.ft.SetValue(self.clock.GetTickFont(self.target)[0]) + + if hasattr(self, "fc"): + if self.group == "Hands": + self.fc.SetValue(self.clock.GetHandFillColour(self.target)[0]) + elif self.group == "Ticks": + self.fc.SetValue(self.clock.GetTickFillColour(self.target)[0]) + elif self.group == "Face": + self.fc.SetValue(self.clock.GetFaceFillColour()) + + if hasattr(self, "bc"): + if self.group == "Hands": + self.bc.SetValue(self.clock.GetHandBorderColour(self.target)[0]) + elif self.group == "Ticks": + self.bc.SetValue(self.clock.GetTickBorderColour(self.target)[0]) + elif self.group == "Face": + self.bc.SetValue(self.clock.GetFaceBorderColour()) + + if hasattr(self, "sw"): + self.sw.SetValue(self.clock.GetShadowColour()) + + if hasattr(self, "bg"): + self.bg.SetValue(self.clock.GetBackgroundColour()) + + if hasattr(self, "fg"): + self.fg.SetValue(self.clock.GetForegroundColour()) + + if hasattr(self, "bw"): + if self.group == "Hands": + self.bw.SetValue(self.clock.GetHandBorderWidth(self.target)[0]) + elif self.group == "Ticks": + self.bw.SetValue(self.clock.GetTickBorderWidth(self.target)[0]) + elif self.group == "Face": + self.bw.SetValue(self.clock.GetFaceBorderWidth()) + + if hasattr(self, "sz"): + if self.group == "Hands": + self.sz.SetValue(self.clock.GetHandSize(self.target)[0]) + elif self.group == "Ticks": + self.sz.SetValue(self.clock.GetTickSize(self.target)[0]) + + if hasattr(self, "of"): + self.of.SetValue(self.clock.GetTickOffset(self.target)[0]) + + if hasattr(self, "ws"): + for style in _window_styles: + if self.clock.GetWindowStyleFlag() & eval(style): + self.ws.SetStringSelection(style) + break + +#---------------------------------------------------------------------- + +class _Group_1(_GroupBase): + def __init__(self, parent, title, group="Hands"): + _GroupBase.__init__(self, parent, title, group) + + box = wx.StaticBoxSizer(wx.StaticBox(self, label=title), wx.VERTICAL) + + sizer = self.sizer = wx.GridBagSizer(2, 6) + + p = wx.StaticText(self, label="Border:") + sizer.Add(p, pos=(0, 0), flag=wx.ALIGN_CENTRE_VERTICAL) + + p = self.bc = csel.ColourSelect(self) + sizer.Add(p, pos=(0, 1), flag=wx.ALIGN_CENTRE_VERTICAL) + + p = self.bw = wx.SpinCtrl(self, size=(75, 21), + min=0, max=100, value="75") + sizer.Add(p, pos=(0, 2), span=(1, 2), flag=wx.ALIGN_CENTRE_VERTICAL) + + p = wx.StaticText(self, label="Fill:") + sizer.Add(p, pos=(1, 0), flag=wx.ALIGN_CENTRE_VERTICAL) + + p = self.fc = csel.ColourSelect(self) + sizer.Add(p, pos=(1, 1), flag=wx.ALIGN_CENTRE_VERTICAL) + + p = self.ls = wx.StaticText(self, label="Size:") + sizer.Add(p, pos=(2, 0), flag=wx.ALIGN_CENTRE_VERTICAL) + + p = self.sz = wx.SpinCtrl(self, size=(75, 21), + min=0, max=100, value="75") + sizer.Add(p, pos=(2, 1), span=(1, 3), flag=wx.ALIGN_CENTRE_VERTICAL) + + box.Add(sizer) + + self.SetSizer(box) + +#---------------------------------------------------------------------- + +class _Group_2(_Group_1): + def __init__(self, parent, title, group="Ticks"): + _Group_1.__init__(self, parent, title, group) + + sizer = self.sizer + + p = wx.StaticText(self, label="Offset:") + sizer.Add(p, pos=(3, 0), flag=wx.ALIGN_CENTRE_VERTICAL) + + p = self.of = wx.SpinCtrl(self, size=(75, 21), + min=0, max=100, value="75") + sizer.Add(p, pos=(3, 1), span=(1, 3), flag=wx.ALIGN_CENTRE_VERTICAL) + + p = wx.StaticText(self, label="Font:") + sizer.Add(p, pos=(4, 0), flag=wx.ALIGN_CENTRE_VERTICAL) + + p = self.ft = fsel.FontSelect(self) + sizer.Add(p, pos=(4, 1), span=(1, 3), flag=wx.ALIGN_CENTRE_VERTICAL) + + self.GetSizer().Layout() + +#---------------------------------------------------------------------- + +class _Group_3(_Group_1): + def __init__(self, parent, title, group="Face"): + _Group_1.__init__(self, parent, title, group) + + sizer = self.sizer + + for widget in [self.ls, self.sz]: + sizer.Detach(widget) + widget.Destroy() + sizer.Layout() + + p = wx.StaticText(self, label="Shadow:") + sizer.Add(p, pos=(2, 0), flag=wx.ALIGN_CENTRE_VERTICAL) + + p = self.sw = csel.ColourSelect(self) + sizer.Add(p, pos=(2, 1), span=(1, 3), flag=wx.ALIGN_CENTRE_VERTICAL) + + self.GetSizer().Layout() + +#---------------------------------------------------------------------- + +class _Group_4(_GroupBase): + def __init__(self, parent, title, group="Window"): + _GroupBase.__init__(self, parent, title, group) + + box = wx.StaticBoxSizer(wx.StaticBox(self, label=title), wx.VERTICAL) + + sizer = self.sizer = wx.GridBagSizer(2, 6) + + p = wx.StaticText(self, label="Foreground:") + sizer.Add(p, pos=(0, 0), flag=wx.ALIGN_CENTRE_VERTICAL) + + p = self.fg = csel.ColourSelect(self) + sizer.Add(p, pos=(0, 1), span=(1, 3), flag=wx.ALIGN_CENTRE_VERTICAL) + + p = wx.StaticText(self, label="Background:") + sizer.Add(p, pos=(1, 0), flag=wx.ALIGN_CENTRE_VERTICAL) + + p = self.bg = csel.ColourSelect(self) + sizer.Add(p, pos=(1, 1), span=(1, 3), flag=wx.ALIGN_CENTRE_VERTICAL) + + p = wx.StaticText(self, label="Style:") + sizer.Add(p, pos=(2, 0), flag=wx.ALIGN_CENTRE_VERTICAL) + + p = self.ws = wx.Choice(self, choices=_window_styles) + sizer.Add(p, pos=(2, 1), span=(1, 3), flag=wx.ALIGN_CENTRE_VERTICAL) + + box.Add(sizer) + + self.SetSizer(box) + +#---------------------------------------------------------------------- + +class _PageBase(wx.Panel): + def __init__(self, parent): + wx.Panel.__init__(self, parent) + + self.clock = self.GetGrandParent().GetParent() + self.sizer = wx.BoxSizer(wx.VERTICAL) + self.SetSizer(self.sizer) + + + def UpdateControls(self): + [group.UpdateControls() for group in self.GetChildren()] + +#---------------------------------------------------------------------- + +class StylesPanel(bt.ButtonTreeCtrlPanel): + def __init__(self, parent): + bt.ButtonTreeCtrlPanel.__init__(self, parent) + + self.clock = self.GetGrandParent().GetParent() + + root = self.AddItem("Styles") + g1 = self.AddItem("General", parent=root) + g2 = self.AddItem("Hours", parent=root) + g3 = self.AddItem("Minutes", parent=root) + self.groups = [g1, g2, g3] + + clockStyle = self.clock.GetClockStyle() + hourStyle, minuteStyle = self.clock.GetTickStyle() + + for label in dir(styles): + if label.startswith("TICKS_"): + value = bool(getattr(styles, label) & hourStyle) + self.AddItem(label, parent=g2, style=wx.RB_SINGLE, value=value) + + value = bool(getattr(styles, label) & minuteStyle) + self.AddItem(label, parent=g3, style=wx.RB_SINGLE, value=value) + + elif not (label.startswith("DEFAULT_") or \ + label.startswith("_") or \ + label in ["HOUR", "MINUTE", "SECOND", "ALL"]): + value = bool(getattr(styles, label) & clockStyle) + self.AddItem(label, parent=g1, style=wx.CHK_2STATE, value=value) + + self.EnsureFirstVisible() + + self.Bind(bt.EVT_CHANGED, self.OnChanged) + + + def OnChanged(self, evt): + clockStyle, hourStyle, minuteStyle = \ + [reduce(lambda x, y: x | y, + [getattr(styles, item) \ + for item in self.GetStringItemsChecked(group)], 0) \ + for group in self.groups] + + self.clock.SetClockStyle(clockStyle) + self.clock.SetTickStyle(hourStyle, styles.HOUR) + self.clock.SetTickStyle(minuteStyle, styles.MINUTE) + + + def UpdateControls(self): + clockStyle = self.clock.GetClockStyle() + hourStyle, minuteStyle = self.clock.GetTickStyle() + + [g1, g2, g3] = self.groups + + for label in dir(styles): + if label.startswith("TICKS_"): + item = self.GetItemByLabel(label, g2) + value = bool(getattr(styles, label) & hourStyle) + self.SetItemValue(item, value) + + item = self.GetItemByLabel(label, g3) + value = bool(getattr(styles, label) & minuteStyle) + self.SetItemValue(item, value) + + elif not (label.startswith("DEFAULT_") or \ + label.startswith("_") or \ + label in ["HOUR", "MINUTE", "SECOND", "ALL"]): + item = self.GetItemByLabel(label, g1) + value = bool(getattr(styles, label) & clockStyle) + self.SetItemValue(item, value) + +#---------------------------------------------------------------------- + +class HandsPanel(_PageBase): + def __init__(self, parent): + _PageBase.__init__(self, parent) + + [self.sizer.Add(_Group_1(self, title), 1, + flag=wx.EXPAND|wx.ALL, border=6) \ + for title in ["Hours", "Minutes", "Seconds"]] + +#---------------------------------------------------------------------- + +class TicksPanel(_PageBase): + def __init__(self, parent): + _PageBase.__init__(self, parent) + + [self.sizer.Add(_Group_2(self, title), 1, + flag=wx.EXPAND|wx.ALL, border=6) \ + for title in ["Hours", "Minutes"]] + +#---------------------------------------------------------------------- + +class MiscPanel(_PageBase): + def __init__(self, parent): + _PageBase.__init__(self, parent) + + self.sizer.Add(_Group_3(self, "Face"), 1, + flag=wx.EXPAND|wx.ALL, border=6) + self.sizer.Add(_Group_4(self, "Window"), 1, + flag=wx.EXPAND|wx.ALL, border=6) + +#---------------------------------------------------------------------- + +class Setup(wx.Dialog): + """AnalogClock customization dialog.""" + + def __init__(self, parent): + wx.Dialog.__init__(self, parent, title="AnalogClock Setup") + + sizer = wx.BoxSizer(wx.VERTICAL) + + nb = self.nb = wx.Notebook(self) + for s in ["Styles", "Hands", "Ticks", "Misc"]: + page = eval(s + "Panel(nb)"); page.Fit() + nb.AddPage(page, s) + nb.Fit() + sizer.Add(nb, 1, flag = wx.EXPAND|wx.ALL, border=6) + + bsizer = wx.BoxSizer(wx.HORIZONTAL) + bsizer.Add(wx.Button(self, label="Reset"), + flag = wx.LEFT|wx.RIGHT, border=6) + bsizer.Add(wx.Button(self, wx.ID_OK), + flag = wx.LEFT|wx.RIGHT, border=6) + sizer.Add(bsizer, 0, flag=wx.ALIGN_RIGHT|wx.ALL, border=6) + + self.Bind(wx.EVT_CLOSE, self.OnClose) + self.Bind(wx.EVT_BUTTON, self.OnButton) + + self.customcolours = [None] * 16 + + self.SetSizerAndFit(sizer) + wx.CallAfter(self.UpdateControls) + + + def OnClose(self, evt): + self.Hide() + + + def OnButton(self, evt): + if evt.GetEventObject().GetLabel() == "Reset": + self.ResetClock() + evt.Skip() + + + def UpdateControls(self): + wx.BeginBusyCursor() + for i in range(self.nb.GetPageCount()): + self.nb.GetPage(i).UpdateControls() + wx.EndBusyCursor() + + + def ResetClock(self): + clock = self.GetParent() + + wx.BeginBusyCursor() + + clock.SetClockStyle(styles.DEFAULT_CLOCK_STYLE) + clock.SetTickStyle(styles.TICKS_POLY, styles.HOUR) + clock.SetTickStyle(styles.TICKS_CIRCLE, styles.MINUTE) + + clock.SetTickFont(wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)) + + clock.SetHandBorderWidth(0) + clock.SetTickBorderWidth(0) + clock.SetFaceBorderWidth(0) + + clock.SetHandSize(7, styles.HOUR) + clock.SetHandSize(5, styles.MINUTE) + clock.SetHandSize(1, styles.SECOND) + + clock.SetTickSize(25, styles.HOUR) + clock.SetTickSize(5, styles.MINUTE) + + clock.SetTickOffset(0) + + clock.SetWindowStyle(wx.NO_BORDER) + + sw = wx.SystemSettings.GetColour(wx.SYS_COLOUR_3DSHADOW) + clock.SetShadowColour(sw) + + no_color = wx.SystemSettings.GetColour(wx.SYS_COLOUR_3DFACE) + clock.SetFaceFillColour(no_color) + clock.SetFaceBorderColour(no_color) + clock.SetBackgroundColour(no_color) + + fg = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT) + clock.SetForegroundColour(fg) + + self.UpdateControls() + + wx.EndBusyCursor() + + +# +# +### eof diff --git a/wx/lib/analogclock/styles.py b/wx/lib/analogclock/styles.py new file mode 100644 index 00000000..a4799201 --- /dev/null +++ b/wx/lib/analogclock/styles.py @@ -0,0 +1,47 @@ +# AnalogClock constants +# E. A. Tacao +# http://j.domaindlx.com/elements28/wxpython/ +# 15 Fev 2006, 22:00 GMT-03:00 +# Distributed under the wxWidgets license. + +# Style options that control the general clock appearance, +# chosen via SetClockStyle. +SHOW_QUARTERS_TICKS = 1 +SHOW_HOURS_TICKS = 2 +SHOW_MINUTES_TICKS = 4 +ROTATE_TICKS = 8 +SHOW_HOURS_HAND = 16 +SHOW_MINUTES_HAND = 32 +SHOW_SECONDS_HAND = 64 +SHOW_SHADOWS = 128 +OVERLAP_TICKS = 256 + +DEFAULT_CLOCK_STYLE = SHOW_HOURS_TICKS|SHOW_MINUTES_TICKS| \ + SHOW_HOURS_HAND|SHOW_MINUTES_HAND|SHOW_SECONDS_HAND| \ + SHOW_SHADOWS|ROTATE_TICKS + + +# Style options that control the appearance of tick marks, +# chosen via SetTickStyle. +TICKS_NONE = 1 +TICKS_SQUARE = 2 +TICKS_CIRCLE = 4 +TICKS_POLY = 8 +TICKS_DECIMAL = 16 +TICKS_ROMAN = 32 +TICKS_BINARY = 64 +TICKS_HEX = 128 + + +# Constants that may be used as 'target' keyword value in +# the various Get/Set methods. +HOUR = 1 +MINUTE = 2 +SECOND = 4 + +ALL = HOUR|MINUTE|SECOND + + +# +## +### eof diff --git a/wx/lib/anchors.py b/wx/lib/anchors.py new file mode 100644 index 00000000..d90eebf6 --- /dev/null +++ b/wx/lib/anchors.py @@ -0,0 +1,104 @@ +#---------------------------------------------------------------------- +# Name: wxPython.lib.anchors +# Purpose: A class that provides an easy to use interface over layout +# constraints for anchored layout. +# +# Author: Riaan Booysen +# +# Created: 15-Dec-2000 +# RCS-ID: $Id$ +# Copyright: (c) 2000 by Total Control Software +# Licence: wxWindows license +#---------------------------------------------------------------------- +# 11/30/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o Updated for wx namespace +# o Tested with updated demo +# +""" +`LayoutAnchors` is a class that implements Delphi's Anchors using +`wx.LayoutConstraints`. +""" + +import wx + +class LayoutAnchors(wx.LayoutConstraints): + """ + A class that implements Delphi's Anchors with wx.LayoutConstraints. + + Anchored sides maintain the distance from the edge of the control + to the same edge of the parent. When neither side is selected, + the control keeps the same relative position to both sides. + + The current position and size of the control and it's parent is + used when setting up the constraints. To change the size or + position of an already anchored control, set the constraints to + None, reposition or resize and reapply the anchors. + + Examples:: + + Let's anchor the right and bottom edge of a control and + resize it's parent. + + ctrl.SetConstraints(LayoutAnchors(ctrl, left=0, top=0, right=1, bottom=1)) + + +=========+ +===================+ + | +-----+ | | | + | | * | -> | | + | +--*--+ | | +-----+ | + +---------+ | | * | + | +--*--+ | + +-------------------+ + * = anchored edge + + When anchored on both sides the control will stretch horizontally. + + ctrl.SetConstraints(LayoutAnchors(ctrl, 1, 0, 1, 1)) + + +=========+ +===================+ + | +-----+ | | | + | * * | -> | | + | +--*--+ | | +---------------+ | + +---------+ | * ctrl * | + | +-------*-------+ | + +-------------------+ + * = anchored edge + + """ + def __init__(self, control, left=1, top=1, right=0, bottom=0): + wx.LayoutConstraints.__init__(self) + parent = control.GetParent() + if not parent: return + + pPos, pSize = parent.GetPosition(), parent.GetClientSize() + cPos, cSize = control.GetPosition(), control.GetSize() + + self.setConstraintSides(self.left, wx.Left, left, + self.right, wx.Right, right, + self.width, wx.Width, self.centreX, + cPos.x, cSize.width, pSize.width, parent) + + self.setConstraintSides(self.top, wx.Top, top, + self.bottom, wx.Bottom, bottom, + self.height, wx.Height, self.centreY, + cPos.y, cSize.height, pSize.height, parent) + + def setConstraintSides(self, side1, side1Edge, side1Anchor, + side2, side2Edge, side2Anchor, + size, sizeEdge, centre, + cPos, cSize, pSize, parent): + if side2Anchor: + side2.SameAs(parent, side2Edge, pSize - (cPos + cSize)) + + if side1Anchor: + side1.SameAs(parent, side1Edge, cPos) + + if not side2Anchor: + size.AsIs() + else: + size.AsIs() + + if not side2Anchor: + centre.PercentOf(parent, sizeEdge, + int(((cPos + cSize / 2.0) / pSize)*100)) + diff --git a/wx/lib/art/__init__.py b/wx/lib/art/__init__.py new file mode 100644 index 00000000..d310fdde --- /dev/null +++ b/wx/lib/art/__init__.py @@ -0,0 +1 @@ +# package \ No newline at end of file diff --git a/wx/lib/art/flagart.py b/wx/lib/art/flagart.py new file mode 100644 index 00000000..e8204b9d --- /dev/null +++ b/wx/lib/art/flagart.py @@ -0,0 +1,1583 @@ +#Boa:PyImgResource: + +#---------------------------------------------------------------------- +# This file was generated by famfamfam_flags.py +# + +""" +See: http://www.famfamfam.com/lab/icons/flags + +FamFamFam Flag icons are available free for any purpose +Copyright Mark James (mjames@gmail.com) + +This module is (c) 2006 Riaan Booysen and licensed under the Python license. + +""" + +from wx.lib.embeddedimage import PyEmbeddedImage + +catalog = {} +index = [] + +index.append('AE') +catalog['AE'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAAA3UlE" + "QVQokYWOsU0DUBBD3ye/pKOKBF2YIiUd89BFTMEUabNJNiASTIBozr47igQBRcjTybrClj2a" + "HwoWz2AwFAQY4s8zATYboKtG1dNjujLLbrvsktIuqazU7mU3Aeh+eyeTzNcPZaZaSqmkVGRE" + "hVJ313enhq4+ulvaPmz7DGOMw/1hFoxM7LaRz7m7G6iqCeBs6XJgDNsT6HTbrfg/MLqBWTCk" + "jsDuiH/2dHdVzYKF3RJSS+v1OiIknVRShC07V6vVfr+fBVcyy2VLOG+tI7aPaju/AcYn1K+7" + "4QJfesZpBoG4DtcAAAAASUVORK5CYII=") + +index.append('AF') +catalog['AF'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABbUlE" + "QVQokVWQvU5VYRRE5+ABAX8KCQIlFeEFKKkt7WngNSzvG1hb8gR0xqixMrHSwoiJMcZOIMHk" + "JtyzZ2Z/37YgJroy7SpmDfv7+/P5HAAASb+urvrycrcBJADg2SaeHwMBCADG6+vr2WwGoPXW" + "W4c9ZLtjlb1id+npbnnvRk1sPH1xOo7jCODi8jLtzCy7pKJKLLKTv+/Wt+05HdvrOxDG3ntr" + "LW1nWiq7gH5yXC3rzbv68N6uyZNMJRFYkpSZskVKKrKOjvT2tc7O+uFhD5qkOWWwCQuMsVhk" + "piXJZJTU11bz4ABTDKsrFZHRp5zCZBKB8SbCtiSSJLvUfnxf2trp9+7n+ddhsbARGUoxCWFE" + "le2IuHV6RL18FY83y7ny+RxkqmhGhrshjLf5JUUEyYqoL+drHz91skc00kS0SS3d/gokH21s" + "3J4GBXGQBnKwl6SHW9h9sOF09sQCA/7nJ9CB/GdPgIs9IIAE1vEH+0VpUzSLjT4AAAAASUVO" + "RK5CYII=") + +index.append('AL') +catalog['AL'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABgElE" + "QVQokU3RP49NYRSF8XXe83IdF5mMiZhoFBKJRKX0MZCoVNOIZgqKaZSiEaLRjIbaF9AqNDq+" + "gj8Tk+u6M/fOefdae2/FkGif5il+3ecBNYACAFUQUYEROG4GCBAQQAAjUCuw+eI5gHSPiOq+" + "+2t5eyjF/fX+4f1p71InCzLIL6/eVAAAtPe9D387W90ZyvXl+Hi2lNndobP9ePZzsT0tMjt5" + "cdOAGoF0T/eUbvS582Mxtpa009LLg0ZrD4Y+jGEWooBSDOmeZIqfDo8uhYxcNvt6NLaxbbg+" + "LJbRLM3CWICCRLpCTOoKHOTDaddaa63tnC1rYVeTYc1bSxJADSAlUO681vnlSb6brSpN1t7P" + "Y2vSTZRhTBpIA4oBoJKm1gbZ073ftyY5H9t8bPdOdY++zQdZ0mAWrtXxIZxBCzOYPTnX9+LN" + "Pg9KnBF310+4WZLpCglABZBUv3GhkEGCFtT22vkkwxlSkUCPYLoC6D4CAOI/UQH2z/WvEmAA" + "gBH4A2X6QkAkcTlLAAAAAElFTkSuQmCC") + +index.append('AM') +catalog['AM'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABE0lE" + "QVQokVVPvU6bQRCc+3yV3VKgiJY3oMzTIapILtMktDwaEgqS9WEJEm7nJ8XZkKxWoxntzGq3" + "1fU11hWzbI9hEmNAMjAbZ2Kg43BY7u4AxEaykTY2yEiZKIEMCem433csC5D8eoYUGT6NQ6bq" + "FKtCVbu8JNCxrnEwN0mYjukmwwqZIlipMtAvtj9uefPEN8tkSJOq8r+kylX6UrufeO9j0LZo" + "0qLrP58os1ylKYE/fYzPKz5uqQrpqkhz/fwfANrv7+hfv+X1MWHMqD7RZVUsuGIuu6un+4fu" + "BTATxhV9ZCouT2nCjOkYHd1HZAibC4AAWyPaydEWtQhSLIN4p1e0lz3wBg+YZyR4JpiSgGEC" + "W/wFGOaHzQZ1JyAAAAAASUVORK5CYII=") + +index.append('AR') +catalog['AR'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABOUlE" + "QVQokU2RvU5VURCF1z5sDyihwBsMjYWVEUsTShqUxEfwAWxIKK2peR5ibLTz5w1oTLAmuZFo" + "LnKy78y3LM6B3NXMl8maZGZNeffDi9AoREOEQmpBwBA0aCMEfaf6N/T+2ei2XbCQwOmSrKW6" + "xOC00z75vqhDaImul8bCSjsRduDAYScTz9aLGvX06fzFk9kSWZKNZRls27LxiNgPinZezuvz" + "LWa9JNmWZdm3X3JxjqPbfOuNI9+pFO3P1I3nTr2xDp/hl/PS/z55VRJQV90aJ/oDDXOpeeNw" + "1V9soF78YW/LLW3rbt03fvQaG8zN5Ab11d/mWY9/Pv6wpqtbB7oPJO9TYkop0e5DnX3drh0U" + "dTvrU6BpJUpKWoGwQOmS2JJEefXx5nebHjkEQyhBgRoCjRxIKKRe/wH2AXDnJMhVrAAAAABJ" + "RU5ErkJggg==") + +index.append('AT') +catalog['AT'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAAA6UlE" + "QVQokX2RrW5CQRSEv21vatA3NwgcpjUoHgyN54EwvAGeF0C1glBca/b8TMXeFGhIJydfRkw2" + "Z+cUcVXe+4dTBKzXgDLJJEIehMsdd5nphl/bbQeA9P5BBBEyaxynmqyqVpmV2SyhY7nU6xv9" + "gFLuRChC7nK/Gvda68sw5G5XTqdT3/eAJITQH9VaGyeTyeVyaSuNqV/zWCCpu4Yevn+blzKz" + "2GJRViudz9xsLA/F3TeIKMPwudl0eTg87/c6HhlrGTsZ2YwbHsznQJfwZM50KjM8cMOsmOFO" + "o3trnIiE8n1/yH9u3PQDfNZaTYpscjsAAAAASUVORK5CYII=") + +index.append('AU') +catalog['AU'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAACC0lE" + "QVQokU3HT0hTcRwA8O/b3mhLndO5tmnPtuUi56YdYguzwrKB2SIiKOiUFR2joGSHLkXBQBE6" + "ZlHQJbBGBDGFKK1cRA0TTALdFhk129JtbHu/9/vbVfhcPtLHkdszxo7o1PpYMS5zfoWHr7f+" + "PLy3ffB+AapFAApABy8MJB6+BFgHwNJQZOxRtI/YFTSfEkiVdngsNsvrNPqy+JtRrmJyLhKQ" + "ZSmzupGYXXkyOi6lTCbnnbv6wl9SLnNNM9TXaVyqqThmPFCkEkZYcTaG97tjE3MupWny3rju" + "nf+4rqvH6OtkVhsorq3BIFHc6e3diIoT/R0A8D2bj03MRS/3Oax1ADVd8fQwWK0vuOtPvT1n" + "sMSbg3p/oGRuOXLQq9gbrg7vUxE5e8xnkKWjvW4ApPdLrfkGx0wWkRrKVnVTS6Xlit7C1efJ" + "X15Py2Ti21q+UthQmxqNT18trnx9r/+c6QwMdFeLlQwxZfAWmeJcvjKfI0yIZGp17V9FCHFm" + "qOtxfMFkNGQW3ugAKFBOKD91yHPzYggTRhkHzgimnHPBhKbR0QfJUrnGGQOgOgCMKRNChPa0" + "tTvNJ8O7NUwwoZhQTJiKSZvDPHKpVyOMMQFAZQDMOW9uNE1/SPfsss1++qE4zJrGmOCUChXh" + "a+dDvp3bNMyeTS8BIAkgAkABEADehG6ut//G8ttbABwA/gMK+Buk8wRxpgAAAABJRU5ErkJg" + "gg==") + +index.append('AZ') +catalog['AZ'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABbklE" + "QVQokU2QvWpUYRiE5zs5uyFrwEYkP0SzaqugtW1uIhewd2AVvIM0ptvgJWjjDSguqNgJWbtV" + "RBFt1MRE93zvzLwp1oDDw/DAdFPw6DXmxCIGaJggQIL+19UgUYm2aXHKvZ0hACecqUwasplJ" + "m0bItMMZymfjVy0GAyy1X85ChjLDljKkSIQznFWudshbl/ror5ZJKTcODuq372lBMpmkSZMZ" + "YdIRjkiyv7b2cjxu0TQgoUgJQc3/Xt3d7W1sHj9/0bu+dTyZ/JlOM2hGRm2AxpITlpMSuTy8" + "2dseft7fH9y7u3xte+X2HVEiRdlJoAFQFg9UllB39L6bfVwfjX4+eTqfzc7evC0dS2XpiEoD" + "bQUkSpGiGW7y6+PDrGHx1/TIEV5M5pJ5CrQ7DzG6rw+/FWJYVayqUaNCYVYzTIq0bl32u09o" + "cYIIrvevhEiTYpjRI82wKDEpSSl1wg8UPABOAAL1Av4nvBADBFZxDq7cZQ8Pjp14AAAAAElF" + "TkSuQmCC") + +index.append('BE') +catalog['BE'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAAA7klE" + "QVQokW2RQUqDYQxE36dVRFTEjXThxgt4IW/SW/U61V0FV4riKjNJXPT/WxWHIau8zEAGv/Xx" + "zkUAVFHX1Cd1S3HwAlitVkBVVdXZeXKV4NE+xkeX6ke31HZLX+v1AoDebl8yMzOrBNmtyane" + "REe0NO7upoSqzkxnWurO7uh2t7qjOzq0A5AmIDO9k3y4/R/ADrBT0g8gDkCpY6qEvU+w7VD8" + "BuY5J4x9JUkRYVsRfxNa+/Ntb+dK1qyp9377RB3RFk4yPSd4uVxKsnMMgcYQGITMvcncuWD8" + "+fTmmVjgoszNGfXG6wOA4RSe4BuvWmXvCEAfcgAAAABJRU5ErkJggg==") + +index.append('BG') +catalog['BG'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABB0lE" + "QVQokW1OMU5DUQzz+7wuldhYKhV1YeEenKkjCzfgMsyVmDgCqhg6dWEBFYnYcRh+Cx+EEzm2" + "nEhph8MBE9geeSq+IbsDmM/nAKpqvKka3WlMsN/v+2mppqL+Q2vNdt++bZfDMit/EpTLo/Sp" + "XJ4N/fnjueEO65v17n0nSxZNmkxGkhmRQTMUNFfnq839pgNQJYsqqUQzHDRZjIoo0hEVNFWC" + "0TEgB2lQVKiJjUREYyAIBoIDo4JFDkRHxyNW15y9hlJSKMkMiUpSIZEZMiUtL/S0Qf98wNml" + "/EKQRVYcHznyKEQo21XeAt3AQGGxKBJKiCAbCQkjS8gc20A7AJ40fts/EYAvPNBlhsfxqY8A" + "AAAASUVORK5CYII=") + +index.append('BH') +catalog['BH'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABIklE" + "QVQokV2RPW7CQBCFn5FbFzSxlIgbQJPchpvQkZQhSKRBSorkDjTcgaSDC9BQ2AqJTBDOzuy8" + "FOsfwdNoNSt92jezLyqKArWS5dKGQ3gfrgYYoHUTKiqKIkmSQNA88hxv78xyeKUqVO10oghV" + "KfK7WMQVSgLAeoPXF37t4RxFQtnxSOcoEvV6BnRaGsBgwPsHpGkg6ITOtSViQGxmzQ5crzF5" + "4s939bwTSktDBMEhmJBkv8/nGW+ua8idOagaEDc0AH5+4HHC/Z7iqnlEGodIxICOmbEWbu84" + "mzG94p+7oKFKVW12qPZerTidcrdr6XCqQH3IJ8qyrNvtViN5j+2W4zEPB9R/b2VJ7+E9vM83" + "m5hkWZbBx+ZzG41wHm2TdNA/d+tUbaz3axgAAAAASUVORK5CYII=") + +index.append('BI') +catalog['BI'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAAB/0lE" + "QVQokTXJTUiTcRwH8O/zbMl8HvfC1tzKaI4sGy1q7JCCdIhePEanLqVEVqNL0SnommKHFkR0" + "6VSSQW8g2EkQKTIhiqSpGLY2UsKtPXtrm//n9///OkS3D3w0zmQgjJW+HrUlnaGQIlJEEEK1" + "WsSsAAW0AADx6XnlrGoHJqNfz34H0FhYqM/MOIJBSEm1GojgcrGUdj5vnho0Txxn5vbRdoc+" + "UNHchr8jHO6Om8n+8qvnVCwGzg274gerb6ZFLhceu7sttjfTWr318ebi9KLT7/F52nwPvqQT" + "ncnh6FB4fJyZi6W8sCmUTjMzM99bSU9lp/pC/fBBJ6UAhDp2LBUzqfkrZW4ysypYWvE3M9dZ" + "nJk9/Xb9XbcvyhqjBb2hWpKlYmm4TEHiW2WZayLQEw/2JrjaXKutVpo1r+m1JZEiKOi/XKRY" + "ElP5j9VUzT3uQz9ePCpll6yN7Nrj+1EzJp2iUC6QsqWS6IAulbQl5axcl7vrycCEh3n3+cu+" + "yH5vOBJN3TCYZ47N9vr3rRSXbRJQcKKEjfr6SDx1xJtkxT8vjUDT/NevMWuF0dvCKkWevR49" + "PDYXeZ/+fAeb0PAQ9kWbmRvzH6ynE47AdkhpVyrKtnXDYKKtfM4/dMEcPAnAc9WjzWXndpU7" + "PyViVaANIEABBAhA/fe/Ojr5cmnn5l99aCZ0THLLKQAAAABJRU5ErkJggg==") + +index.append('BLANK') +catalog['BLANK'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAYAAAB24g05AAAABHNCSVQICAgIfAhkiAAAABlJ" + "REFUKJFjZGRiZqAEMFGke9SAUQOoZQAAMjgAHL4jfPwAAAAASUVORK5CYII=") + +index.append('BN') +catalog['BN'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABw0lE" + "QVQokS3MMWhTYRTF8fN976U+kxgNdojBoZAIDopUyZIOoougm4ug4CIdRASJFNRdaCdx6OAo" + "KMFZcLKbi2jBKKJRaRMpCVbb5L00eS/fvbnXIT3Tbzl/M2wCDrCYTgSQfQgDApF9s0AERlUB" + "TAYvwH9FepCJqsAwDAszwBBWJVFW5d2vr/xp2GZvALCATLYlfDre23ZRdDDvhAiGREnFmdRx" + "YXjpIKguLExvqmpMxgQXNnbO0juXLbVgVGQkTCLO2kPD7g/v7dpaHMf1en22WHTM+VxuEsfu" + "+RMNd+n0s5nCNT9702YuarIOg367aQDUavfb7RYzM/FK9aTn+dHl6+Wj+ejlqrhxofZYVVXV" + "WBv2+2bwDebIg18/Nx8uR0xM5K+eL9158z7taOXSmd7V26lM5tTcnKpaa3u9ngm/ICguCbVM" + "KhkPqPtbw3+Y/VRCsrf0+vv66I9zjojOzc/fWlysVCombODAsXtCLUgCjxQuCYViVuXOJq7c" + "7ZBzzMQ8KZfLjUbDF4YqG68gmiizgoPDZGcmEDpRpebH3DiSrQ5vdfnR8hCA2fkAYYiA3T6E" + "YX04howAgU2DA0AgjI3P+A9mXiz0vUkDDQAAAABJRU5ErkJggg==") + +index.append('BO') +catalog['BO'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABHElE" + "QVQokW1RPaoUYRCs2f008pmpCwaC4DG8h7G5eAkRQzHzDiYewnsYi4Esbx/jV38GMysGNkVT" + "1d10QffSR0dcIzImEGBFiuA/GADw4ROApkuMpA7sJT7aB7Fyrco1b99/HABQ9OcP2EgqYW+7" + "YqWKJSsuj08Bxt0r338x82RF1Lh2w0awatasFbPm4eHd+RnG7WvcPF3NSyvUjVo2TP8SNmzm" + "4d7Nr3cYCBq3aoW4ZcLuQwpmw4aJDlGCEaFgK4StcnXYd28mYaPCEsYX4aV50e+EKRN6I6Wz" + "y0auHhzXzyvGmzPe8vJ9nhkqYjgzWe5kK5aKn+Py7YwBQdFpnFQxcjQxFW3YTuvYtWEIC74C" + "AfQPNjmvv92K2PMfrSN8t7ZlHI0AAAAASUVORK5CYII=") + +index.append('BR') +catalog['BR'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABrUlE" + "QVQokU2RO2iTYRSG3/75Qq0oIYMS24JDFcFbBkcVlAoi4uAloMRJUSGTQ10KCkWx4KK03kAc" + "xMvQCirWwUXNpi62OAgOUhW8ECxNm1/id95zjkOU5OFweOHlcIanC8fRxgD+3xGIHaE1TQQA" + "I+URB9zNzNRVTbet/x3AqXcJjWpKihhFZeLsRADg8G8L37XVGe8cTN173GWw8Gvv3UykUCVq" + "7M/1//vg7mqkcc+mtNSrw8Nj01YQ+kDzx9OLhx58levVDFWEgogEBho1SW/tT0sDnP5QOlIp" + "3Bz6UynVZ7pXnrkwdHhNrB6rR4qYoIkERG+ueXufJC4Oufx88PQNv/oky7jk/AnPbyy6R/f4" + "5tR8sa+BFAkiZueSk8/oiO5xS/6t0Gc++ehkmK95cfnr1sGue/HVe0EOCQiScwvJgfth6nOs" + "HB1du5jWfrosevVRY/f2c49n44ax7JdaNrs0izoCIugUFVEZf5m51JO+uLLDpc9FvPvj5vEg" + "FqhCJY0AAiKELCxbRRUxIVeUJ6W8tZEJdu3hutV5qilVaWJqALqws22x02jbtKKTv/JDL0E5" + "0S0eAAAAAElFTkSuQmCC") + +index.append('BT') +catalog['BT'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABnklE" + "QVQokW3Kv2/McRzH8ef3e5+79tL2gtzFj5RGJcLAH+APYLIYDQYJ6R8gFpMwMHWiDFi7IGmE" + "ySAGgxCLgaQGOYk0vetdr+e+/Xzen/fLIGLxyHN8FpO3BMP5y3EHIzTwEq/wiIM38IA/IAQj" + "HL8BCEeOZ5HB1FhUtBA/q7BMKqLpVdp5uhY8AFL8gTKepQRZzRO+b6mqFKtx2bs26sbO6pfa" + "scNepwSE/t2eFaK2Pw4Go5hy3WPRXt5a+1nsiZpKnihx8IyblJCpjL9G5cbkkA9fD8fx/fOV" + "75tbncu3tBOVExBwUJaSZBSmKnVnVomTWl2hplPnr0I59+iSmolkDsENyeSmIlKz3t7rPqn2" + "Nwa5Sv1Jcy5t2MubGkTNpCInh9IdeVItTmYvrIe7/c12M2+nXrd17+LCh2fjufn+2TsaJcyU" + "zaD0CDXT9Mn+8PT0t+3Fh1eK6ebwzWOxq08r7dtnjtw/p3aUJXkGQjmLStO73sH1Zaayjs63" + "vr6oN2JxYIEy0TLcUCZnPDsUoyewhO/iHTyC4WOc//THb4UhI+oZPvWXAAAAAElFTkSuQmCC") + +index.append('BW') +catalog['BW'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABDklE" + "QVQokX1RsUqjYRCcjT+KYAicoJ2BA/EN8wRi6YvcA9jYpFMfIU0gWB0XuIBC2J1vxuIzIXeI" + "U8wu7O6wzMT1s/8SHZRSoJCCJUigQGHfA8OfxN1PGBBsRxME0JaCPmoeNZtyyc2+f3obRoCM" + "3+VmSKDdDMq0O5dcMuXLkwAVm81mPB73l2x3/hIRsVwuA8BsNlutViRZLFZHZlVl7kpVTafT" + "+Xw+ACBbVe0P+vif9cy+AGAA0BpJZuVOPzPrUHsvASDW6/VkMvn++z6NiMViMZy/nN3e4HXr" + "Eg496Uy5ul3C1Sl+Pf4YQMmji+NuJZpBoRS9aUIzmqPJAkAFHt6xPQiV+Iw2/8tYIHCMD2d1" + "fWGuzZK5AAAAAElFTkSuQmCC") + +index.append('BY') +catalog['BY'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABTElE" + "QVQokU2OsWpUYRCFv717sd1qCVuk8R3yEhbiC4RgqjRhGyux09ikixY+RTBBEFJY2vgQESxE" + "FpG9d6/unTPzj8W9yWY4HA7MfMMhuy6PjvL2NiEeSNDDP+ighTX8gd8wSaDvOTtLiVKISA/C" + "0x33lPKBb66va9Zrbm5yuWS5JCKlwUeZUpZmKU329wvUzGa52XB6urs2S/fxVJamAUAqUJXV" + "iqsrzs9TQj7s0mxUb7ssATXzOV2Xx8cj4OO/EbuHJdwLVKxWeXmZF+/uCtx53+/cLM2GSnV5" + "8iy/fO5Onh+euIpZSGEKWZGKKWRhHlLxxzP/+paqfPpY3rzefnjfTq2dqp1aM6pvKltX1kyt" + "qa2t1VXBI+qyN2fTVK9eLJ4uvISHVKQiL66QF/f0iIiMyGDLpPv56+9i7/uPbwcvDzAwcHYB" + "MNhCYZj/bzd1lgzDLI4AAAAASUVORK5CYII=") + +index.append('BZ') +catalog['BZ'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABpElE" + "QVQokVWRv2uTYRSFz/v1a5Jq2kRNJEJCsaU6KIIUXPxBJ0FxdHBycvZfcHARBPE/cBPH7oJQ" + "7SgFKzrZgrbW2LRJkybpl/eee69DKNSHMx14pieM8B8ECGQAgNzxaccDEDC78eLZWQAGN8Uo" + "X8gPhqcLUYyd3mR/Yiq0u1QInbQ3L7+mqNSyU7mdtqs6DQmxdKd1vp5lqlvN+H75SrdbErrQ" + "69UEuJQAcAPVxZAEffR49e7l6cWkccNqDxqVJ09XcpOjSI1UoQIxAUFzKpQ+d6FZz5eL3bD+" + "/dPal4/+a1QLxavXNqOEKCZ0wBLQ1FzVqahfbJb9TK5YybbpW83a7FwZ1dr8H1GLdNGxEE0V" + "oh7VmtvlQz84yo56+epPLOz19kcYtH9PS3QRJw2wFBGiLnRR//ytvnhvlVl6+/p8VNvo/Dic" + "aa18uBmpkUZLAEuRgQpRRGLA5PWrW/cfrs3kOyQOEN49X9rZh9BoiWoAkGJ3NwzPNaachNDj" + "RLr+dqFU6qvi716u7F6a6au5qbPvwGZonQg5zjwE+gCBAlA4UXrMPyh0GTDv/QqiAAAAAElF" + "TkSuQmCC") + +index.append('CA') +catalog['CA'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABoUlE" + "QVQokU2RvW4TQRhF78zOetc/YMsOFqJBQAEPQEidmpI3SJkOCckSVV6AhoKfElHnBRBvYJTK" + "EkUKIhNSRCTKkoWdmW/muxSWEKe6xSmudEzz9JleXVln/fnpcLnEf5AEEGM8m816QAQS4HR9" + "2l88B1BcXw4GA5IkY9MYRTG5SdXSlbf291010py/vX7lMKoB6s+LfHm+sZOyffNWfR6+fFEY" + "QzIeH2t/Yu/cjoBDUmalJKREMqy+Bo3FyRrR/15+cTlXT7bRddnUDDEBTmNEzhRhCCTbd+/d" + "aGCGY5Z1/vDx+vvJ/PBQuwDTmRgS4OC9SlIRjUISD+7y6IjlyLTeNmfFw0eqmkJnXV+jRMAm" + "75mEIho8ydHe3p/ZVmwakbadzseLBUnxniHmuLnUthTREDQEVUVdjw8Ous+fTFVPd3ZMXeec" + "GUIuI0QS4NLFD4owRPVeVUmaXontx7ZXo6pyzqqqvtNesClFwCVAg5jpxObOGLOJVU23DKmA" + "tdYYU967X0zmKkkBcwwokIBfwI3Vynv/L/BmiMh6d7cHKADgLxaxMt388uCoAAAAAElFTkSu" + "QmCC") + +index.append('CB') +catalog['CB'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABTklE" + "QVQokVWRL2/TcRyEr82P/UlaycKydCAIOIIhJAjI9AKC9AXMTPEOhsHxKta9BBI0eh5PBXSi" + "yC0hdJ+77x2ivwYwl0dc7sQzwPExzs6wtYXVCgBsSAAg9Wz3IGE4HAB4P5vh6Mi7uyZbIqDZ" + "SmQLoC2bCZPPl5cdAMy/Xc3nbTptd+9xxZbQYJ8upxzak+078KgDYKctfuj8/N3L5YunLa5E" + "MeNKagMc+PB69aUD0FqTpOXy4pMG4PMnTNj3XMkaiBDAEIDUSJL8ec2PM36/qgyrb5tpG26y" + "sH6QpGKNhjqdcrJfqX7134eBaWEIgGRVaTQ+fVuvnlVUaZv2JmEl0godAEnc2+PJyYfJg/pV" + "ZdP5m7FsOQ+z/bW77ADw8P7+m9ccj3V7q63Qpq2EGxstaXbLb9Syw8HBxeNHWCxwc9PrXHut" + "+s/x2v3Ozh9eL3jobZO0pwAAAABJRU5ErkJggg==") + +index.append('CD') +catalog['CD'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABoUlE" + "QVQokU2RPWjTARDFLzEoNhZt0cYU/gqCH4TiR4nB4sfkZGdnHVx1FAQXlw6CheLgIKKDk5tV" + "R4eiaA1RSiF2KBmSGm3VBkHU5u6993dIKo7H3eO9d79McnGydf3FvtuTK4sVk8xlgDnMZe62" + "AUvdzM1g5maescO3Hs2MN9YLSFMxpVJIpKAUEKggAQUV5OzNu7nkaPXS8e6dudJCe5RSSAQD" + "aUgBBuigQwEmu/Nmnl1ZLD/8MLHQHoUUFEFHGmQx/9X71/SABwIy82yhVL184lVppAUSpENp" + "6hPJ0tT5J8mO1W7Qoy8Lysyza7Xx+7VT79vFoAIK6Nz++o2zT4e3d6YvPC4MfOsLQgDNPJsf" + "e31lfG5spPkvwLOPRz79GEjVfb50qLG+09E3CcgMuV/z5XvvKtVWHlT0d3pQK883kwND7V7p" + "AAFRMvMtdtJnr27Uv2xtdgY3f8L66p7O79zy9yEPBggK1K78trXqy5y9qcy8Pf35z97iMEEB" + "DCjYIyCQYEqK6jkoY8cO/pxaHrxWsMaZHstNrvhvlBnMZGZ/AR0PdcdtApHFAAAAAElFTkSu" + "QmCC") + +index.append('CF') +catalog['CF'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABj0lE" + "QVQokU3KP0hVARiG8ffcc+x2K7qeweIqDWnSFEFrNdTqUK3V5ODQP5e7OkSTLS3h5OzgkhBc" + "XbMlCLpNIRQoaESbcU+D3/e939tgQb/tgafoXN0pu/Wbpd6d5z/Qgmeywa/PPQLjl/YxlgjC" + "EiQsYVGV3fGdtY2pqaX9a2vrHx4zoSOcevdUES9vTHgpUkx5iMrlZ9+qJLaG8/c6L16/fZSF" + "0pGUdnfl/v1yBuWhYHrofF2gQXFw8HNycoIEgKIQAECam5O5tjb1T6aKohgOP1WAAyhLSIIg" + "SJLMZK7/AAIwGo2qhfcLi9cXD48OKTLJVjLjoZncV7+uRjKNFCOjbtf9Qb8abA5mL87uNXuW" + "FgxPd9oDc5mtfFwxmtMtzenT3elmo6kQcLnRIsNplm40mcv9703zygPBMnACVX8e92fit3vK" + "M52wlMlcZq9uOdOTlvIUz4xx+QmquydxpQ6pJzlAySXHBUfE7Z5LIYVEgADP3USxvY1WC50O" + "2m2cPgsQmeAMEsgv4HESmTj2B5KxXKDSA72eAAAAAElFTkSuQmCC") + +index.append('CH') +catalog['CH'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAYAAAB24g05AAAABHNCSVQICAgIfAhkiAAAALBJ" + "REFUKJGFUsEVgzAI/VEPvk5gOoZrZIyM4SwePXp0Jydo/kXtIU1NClYuEODDh2BMVQMA9n07" + "8EeY2Y+qNsluiqxhUJAESLQfm+NYhBsBWFcgBGCaTp9zUVtbMAGAShQIQZsAJNWYZJB3TsB5" + "jg/vRbpk8Au+kbJABtDAJMUOviMQQEsCzkXwspxZfR+1tfcMLmlf+MUS266LhvdnsdRZ+QWT" + "LvG1b4fWQ/M9s0t8Ay6VSOU8nBloAAAAAElFTkSuQmCC") + +index.append('CL') +catalog['CL'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABC0lE" + "QVQokXWMPUpDURCFv3vfExOLIGqT0qxBRFyKhTaCZRZg5U5SuQcbWyE2toJIEOxifiSa5M7c" + "GYsEyTPmcDgwnO9M4OqBb0UhwszYiblzCti/gpKJXp8dXhzvdx4HWXKjXkAADyEURRFj9BVN" + "JpOIcXlygHN+tPf2MX8fzWFZr33HzCJw+zTI7nfPI1HXjG+WmZXUdse6dXM/VEe3a5rTJhoA" + "Sl5f4me9GM8sezRLjbpPpw6rb5eDGK3fDxlot73Xc0mIuohL8iSekkslY6s17HZLgyDiKaFa" + "gao0qq6qUBoUqi7CL7Q+U0EzObMYRFGaTRdBMyqIBBFUWaQqOS9sEL7AVkz1/FMBP4aNaAa6" + "+++rAAAAAElFTkSuQmCC") + +index.append('CN') +catalog['CN'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABJElE" + "QVQokV2RMUpDYRCEJ8mLYhGwElOkEAQvoEfwGOYQllp4HQuRmMLGUkG8gJ7AQkUQExOzM7tr" + "8b8XHsIWw7Iz+7HbeQECCKALANg+xvsd0DQFAFBLVwHsXpwPTjC7jP5ebB759qvPJoIppSRD" + "SrLo5+m0CmAwzt7O62Dsn2ceC24cev+A8xsmmcakhVmS1WikAjK/ypj5z637LzsDT5m/MY1h" + "llyFWV2kgApAfPnHqZDaOlK1z+UDl/eMVnaaBZnFEECsvLAun4QtziaWZJIlFU18SLUhXSmB" + "5gt9XzNphSfW2cVARm0oLSlbAP+nJaw3QCqIQYZZPdFwp1mIKU93AJWAoHrDYZJdeYp1fHP+" + "kOAe7ukuoPPYemRbqPXsaKoC/gA6rnMwXmwqoAAAAABJRU5ErkJggg==") + +index.append('CO') +catalog['CO'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABEUlE" + "QVQokWWOsa6OYRCEn48vR6MhIhQSci5A7Z60Kjegdiki0WuERknjiEKhUvrtzDuj+M+fHGwm" + "kye7O5vdfr4h4bJCQnIC/yfYE24/fg6U0NBVFnFr4qLGjVq3unj9ageg1Xe6yGpFV6NWJ59m" + "Gm03HsTsQOnf29O6UTPtnEBEHnYMLHA3g4u6qVVRt2mnqEwrEGHj1rtnLx5++zFelSNXvoRR" + "5MyRvR7du/H25fudAzZ2xz15RsdYR50rhyA7B36LX4OFzJgxUuWOLzsy9jYDsH/qk7ufn+ri" + "azUZVcoMUjSRjm9VyvLZ+flHPuxncN2Ha/fv1O5MfRyvSkfGzkqXuw43YfsCBmAgEPAV/0fA" + "H7CAa8EDIY7HAAAAAElFTkSuQmCC") + +index.append('CR') +catalog['CR'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABJElE" + "QVQokWVRMUpDURCcl/wiKKg38BCeQCy8gxfwAEJuEFKr2FhYBCzEU3gLWy2sFDSKkt2dGYuf" + "RMVhGBjYXYadhu0JAktIiIIF1C/GSgNQh43RxeWhARuyRZckqWjSpJJiqcQqn44v2sIeArAB" + "GIBtwH/R+9ba63zeYTbD3p6/Pi3DslakLNkCadJS29iM25v2AOyMx3x8dJWyXOlMZCrSGYpw" + "Zq/D3d37u7tOAIrOVJX7hQhnrucU4QhlDqoEdALEUpUynOVMZzjy9+2lZgpob9PJaP9AH+99" + "0F61ym0SpGSLg62tp/Oz9rVYdMOhpeWLbNv6/yWgtfb88tKAk8nVEUukiiqKVJYoslwSKclF" + "2b6ejhtwvGpxXefa/hQMFCBg8A0y93t3R/GzpwAAAABJRU5ErkJggg==") + +index.append('CZ') +catalog['CZ'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABO0lE" + "QVQokXWRvWpUcRDFz978CWgUVkgQ/CjcUhtBbFcrC0GwSG9vkRfxRQIR8Va+QNrsRhDFF5C1" + "EBVSeGfOOWNxs0uQeIqZU/zODMNM8Orkzcvd1892AQCwPdaLZiPa7YbPtq/f/brSw3vXxkwV" + "gKpNu6DVatV1XbdTPDo9O/7yeyQ22D/jx4XNhlxXNfSfLHt+f1r/0XmAtKtctaPh3UdRfvrg" + "8gwAki1o21RJdUXD4Ylufjia7W2XZLIkSCWVNJlOcXDQSMqQiqqUX5y+vbN4P0RWhDIrojId" + "UZltNvs1DE20XClTNV/280V/DmUiApkVgUyQIAE0BDnSi/7Jsq+Icd4mVhFmgiqJQEOYquff" + "jh//WOLWbTO7TGSarMwJWeTW+gwADX/4ef/RPn5+BwwYAMC15/j+temAvzMZeRNn/GS2AAAA" + "AElFTkSuQmCC") + +index.append('DE') +catalog['DE'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABNUlE" + "QVQokXWQMaoUARBEa2ZnwUgUEwN/aqjgNzJUzD5o6D1+4C28iLCXMNHUowgy3V3dVQazaOQL" + "iqbgVdDL3d07zA4Jp61axbYICP9hI/f7+88AJEma6Zkj+wrZ090kebl83dZ1BfD9x88mj7qq" + "Kisr/xIRGfHh41sA2w3wSLohu7urmmQmM6uKEZWRmdwjK582AWyfmLczt6SrfGSmMxWpDGcq" + "wnuq4szegM0sz6DKRWe4SlchnKlM7fvhm2VgM4Dpf8NVjlDEVdt3V06kqzwcYOMb4Ll9kkei" + "xDkOt0W5MbQbov16iW/Y/AJ+Jj8eaaSxRtOWpD7erLElCX6y6D02FNzE+dWKWjRWLWqY1li0" + "2h677Ub1+hvL5cvLBwMVfELuEOCHWAMa1AlT0OC8QgGc8KvwBzggh2H1+79aAAAAAElFTkSu" + "QmCC") + +index.append('DK') +catalog['DK'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABKElE" + "QVQokWVSO04DUQyc93YLBIpEhZRiG27AQeAgtNDlVilziFwhAkFBBFKWBKR4bA/FW5AAy4Vl" + "j39jlxFITJLAeURut7v5PIE/6gCAPoGTxQIAMpFZSqmz2dn9XX58yl0k3EXKPcmX5bIHACif" + "nhGhCEmKiM1DHvYiZRRNZknWYcjWAakJTQKQlLRsUJqMaSaykDYlRMhd7qBLUq3a72EETWYi" + "YQZSZAIlIkopkiS18spUM35LqXXc7Xr8xCAILe8/WlKRMrPfdt3s9jY2G7nL7HS10jgerm9y" + "/97maQvIvb+8fFqvewCNPpEgp2Z2xPE4Oc3khAciJpZEr/O5SHkAQNfVYcDhUBr97jUCERkB" + "oDwD/n1IBy7e3vzx8fXq6sfZ/qCduQJfBq5Z9b21BQQAAAAASUVORK5CYII=") + +index.append('DO') +catalog['DO'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABN0lE" + "QVQokXVRMWqUYRSc798fixWEEIwsmCKN4gG8jYVN6lR2OYOFJxCCF0iX3gukiN1KakFZu+TN" + "vBmLfyOxcIrh8Zh5A/PG6s0FHtAyyrB/fnv/TLvvB4cGDOgRzwA+nb8NkMAdO7LXTzDm9fPT" + "05CRQloKub28nAEk+PHrrhd1uzudzG5utymG5aqQq+NjA3MbCbrTTrfVkewkcarCcjFVJidS" + "wAxZflBrb5hHMq1S5SqTqQppUsDo7jFGkiQA8n+Mafq9240tcHh2ptvbsEKZDOvg6qqn+d2H" + "r5Kplkz55YunXz5+ngxkHypXpSr3lfu76ixqypSl7jag2cC+OHJxuiqJk+L+trrVaf81UNNm" + "M8ioLQ5yTKtpjM3RuttLdbbVATxu/n3kwq9urnfro9cn54/Wy4A/xLJ3307zCyYAAAAASUVO" + "RK5CYII=") + +index.append('DZ') +catalog['DZ'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABmUlE" + "QVQokU2PPWtUURCG33PO3bu7lxWjZA2bsIUEFMk/0DqI6EJs9AfY2FhYGkFsBP0B1tqITSxD" + "GrHRRrAQO0VBtsr6lTWud8+Zr2NxF3WKYYrnnZnHnbw1msYZAAAkEok0zkJZfbqzU4YWgJyz" + "mZmZiJhZ8a2e3t+6kQHLpqaiKiJWhP5g0IZHzpb/1Xg8LgpfZOTJ4Q9RFRUyYab1nySr720w" + "sFbLbBGpqirnXMDMchZVVmGV4a/68sevXUo17/XOj9DvI4S/F8zMRyFRIRUWpiybn79XlO6e" + "OREujGa7z5z3jV4TEBEfiUSVhZJyIlr+PX8ntN8p5vdupzevJ9eueO9DCM65Rt1niqyShBNT" + "Up4Yn9qflgfT9s3tcv306qOdhg4hLF5CrEmFmBNTSvHpSmfewoNXH/jJ46NbVzORmQFwzgFQ" + "VY9Ys3ASSkyR0tsurm8sPVzrVZuX8nBonc7/xgAKxEMSXu4tkTIxJ2Fqp/FKaefO5uwc0Kxv" + "aDNzuHgMXw4QgRqLPgNKvHj+8nj3SAOpajMA+AMw503J6/1+rgAAAABJRU5ErkJggg==") + +index.append('EC') +catalog['EC'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABKklE" + "QVQokW2OPUqdYRCFz6evIFcJiCERK7lNmuwgq8kCbMTaStKkdUMuINiqlSI2MYkXo6LvnB+L" + "zysKDofhcOZhZobHky/wDcay7Q7T6HY3YwOGDRM2ADT43+KnfQCAkCxEiBONQpgQYeyEs+Of" + "DWhAwKs54YQJYyaEGVRcMYelz+5oMAC/S8eVEKmYScGk0drkZlh8RB5eHpjrmRvXJzW0++UJ" + "2sfdvR/fv/2+vqUiizJlSkVRLkkU6ZI31lcPti/a9enm5ezDxZ9Wcsmki+50yUUVMw/1XxNg" + "2vBgahy7+GI0ms5wPEVLBjwcAZs7O/3sLFWueu69u8rkmIQVamk6/XV42LaA5bsZ1lbCSmkk" + "wEoxrIghQ0HK7d+vwHAOEPBb8ZXBK7MAPAH8bIEjRK/4bgAAAABJRU5ErkJggg==") + +index.append('EE') +catalog['EE'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAAA8klE" + "QVQokYWRMUpDURBFz4MHLsAilVi5BBcgrsfWxl25gSC4gCwhBFPbhpk7dyy++cZCPM0beOde" + "GGbwuGXFEEZGJkSYMCcRIsTJpCbw8nQH2Li73DJlq1plFVlWOd1Z/fr0Ppfqj88sd7mzXNVZ" + "zuqUszrkKGf65voKNDE25Zat6qqOsqpz8eRQh5x2lkGT7UPdP+twkKRUKhciMjPi/GRm3t7C" + "2wSkysw18P19qUcsAjCBKkmKjHN/RORl91oBjOPxuNlsuhvovwHGGLvdbtpe1H8DgO1pe7XX" + "A44x+MWA/gns93vbGGNJtu3vYVnUZ4Av1C5+7JEmXZcAAAAASUVORK5CYII=") + +index.append('EG') +catalog['EG'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABI0lE" + "QVQokZ2PIU5DYRCE5388UCSoUipwNWCqkByCO5D6OhIsqUByABwXqEFyhl6gCkigwVQQ+s/s" + "DuIVAki+bCaTyewmW96AxIb8rd+jH2GbwO7lJQBnItMRVjhkCZJJS0laAvk8m7UADMfjEyIc" + "AdIRJk0m6UqzZq0gm8NDAe3OyUlzdIze/uZ8BCK45tbpe/OwnayWGslS6fV27u9LRJRSANiG" + "YTgzPpa34OP2wQXc+otSymq1arp3Nxlsu75ca73KWNfnK/8EyMzmT9u2Lcof3EvzV9/OzDIa" + "jSaTyXK5jAhJkkidn73aurnbk5QZkiKi3+9Pp9MCYDweLxYLkiRrrbXWzpDsnEQphsPhfD5v" + "AZAaDAYkpZC6RUrqtDvfgX/wCf7XTmN4WqTRAAAAAElFTkSuQmCC") + +index.append('ES') +catalog['ES'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABH0lE" + "QVQokX2RrU5cURSFv3PntghCJQkCBUgQ9RieAVmN6CtMJsFjeBZsVVUtg2dUK5owyYTOlLln" + "/yzEvRM6CenOypcl9l5i7SLeJrf9uyoCxmNAmWQSIQ/C5Y67zPQPl3d3LQDSz19EECGznoOq" + "yapqlVk5PExoCVQEAfF3kS8L7e3X0Y5LJlWpDiaNYvlMkwABvlx53MJNu3q6WHd1s70RVRjQ" + "ABCSje6/+G8vXycfHo+6edmKH248kwaQXPL27OTPSXI9GZ1+bvVR6qS6YZUqskyaTPqYH48P" + "u+d1fdl9n35bf1ptsgeCS+5O6WB0dZWzGUMtQycDe+OGRzk+nk+nbUJjzsGBzPDADbNihjs9" + "3fvGiUgoq+1H/ufH/bwCdrRKr4/GDeYAAAAASUVORK5CYII=") + +index.append('ESPERANTO') +catalog['ESPERANTO'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAYAAAB24g05AAAABHNCSVQICAgIfAhkiAAAALtJ" + "REFUKJHNzLEKglAYhuH3yCGEaDu41RhODjk4uUejV+A1eAVdhIObU12DEDjW7BK1BdFmNVRk" + "GdjqEFhOfePP/7winIUPy7RkVVUIIQCYLqe3dJ8+adqei7RMS7ojV4u3Mf7QRwiBcTZ6dBo5" + "ABpAtI4IVgHewiMv8u9kPTDuj3EMB1vZKF39FJAASlckk4Rref0JA8goi+6D16BbP2bH7PvA" + "fDMvKOk2v36e1hb+T0Cy40TJo5U+cHsD6JgyzFPQGaoAAAAASUVORK5CYII=") + +index.append('ET') +catalog['ET'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABhElE" + "QVQokV2LsWuTYRjE7/3yGSsWDbRKGgixnYRujg4d/A8KXerk4iK4OIiguDQ4Cy5uTk7ZRHB2" + "FXEqKFhqCUrBBFNjkn5573mec6iKevyG+3FcwvYqJmOcJAzZEBmZMMCACshA/l0WUGL87f7N" + "RwAiFHKPsHC3bOEWMne6mxvD6Xxx+3GJqg6mz6OBh3sEI3uyVLboMZ3usWI2ZCPJ9nITQIm1" + "wXJjzmJm4RE2J85dvFOr1T1iUlWH/QdSRae5Nc+fxQUkd6R0F+hLBtmTt93hbGmj3Ts4urQ7" + "uNKoH967ekPKElPqjMevCwCAS5QocJrrG+3ewdGqRVzrPB8eN6T8C1gECgCSSSZkKaeierW/" + "FcqtxQ8vP26fOTX6c4AYgUQgdW/pyyeYyXKeY2fz6ffTiwwt/Bh1n22VmoiEWWqvDXvvygBq" + "fdM+QYosc364c/19c93NL++9KThTzjLCHMcOoAygoGFlRSTMYSzIdX4VTJ2WzGAG9xMCSFMg" + "/gL/6n8TgJ8hCj2miB4d6gAAAABJRU5ErkJggg==") + +index.append('FI') +catalog['FI'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABN0lE" + "QVQokW2RvWqUYRCFz3x+ooQVFAIKCRaSIlYKtpZeR65C0moj8UpSCNYWBsRKOysF0ZA0ESQB" + "Q7JF8r5nnkmxy+KKD1PNz4EzJ46OzzVnWHv2WQ1Nhq+7j1duykjMMQCnZ9NxkO6tTiRV1avt" + "TYpMbT64XVWz5t98+9lGpNlA0q+TS6NM6n9ERDMjKJmL9VSCXQlVS/IqFNFM6MnHnecbRyct" + "k9bLWT0paC67etI6Ni1ZX73x5uX7URdu5rKTWU66y4lNz+qmGXda0ns1I12MwlVVFNBdNs0A" + "mZUJSQIJFCARPw7+3F+/BVRp6/V+N928fbHBsnNJEfHpy+E4jFwLRUih2XY3VSX9U5IEbRQs" + "Xn73znUntiJCS8TiJr7v/14E+ejhntSk4d2Hp5MVGWS8SBudTadX03Ro9+qwo6AAAAAASUVO" + "RK5CYII=") + +index.append('FJ') +catalog['FJ'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABzElE" + "QVQokU2RP2iTYRjE702+tDEl1mgS27TFarFoh9JWHVxcKkKd2kGqg4OTFFGRDoIViujmIg4F" + "FxUHRUGnWBCqoLSCQ0HBuFnt0qQojSY1Cd/7PM85BP9sd8PB7+7c0vmZhTA3e+fT7I73iZbI" + "5WL32c7K+PjBscpV5AxiaBjEYIaaQeCGey4+fzApO3ONwkfWaq4j157dNl+or6aHgzhCUIxi" + "FKUYb89tBnfliSv0b3n7MlouW73ekkrVBYeFiweGkKQqxeiVXtnR5hBakE8Mnenbnyiv1YVu" + "eybe17vxpViotnqlKqfzl2L1avqklO55dPfOhdORtZFjLh5//DlY8W1fa7GHP7siu3ZvsNUr" + "KTQvmVNCCbOnxbxAEKQH9uVLyaVIf6Ozy5m+Womu7h0YPCS+TFPi+q3YVpYmJzKPnmIkghM/" + "HK7pzVGsVZvN0CQWpTfWyHMvrvSUClrdZBD9Nnjk6OJUgNCUTpRiEKVXemNTEGyfvZFpZ3Fi" + "LPtsvlFxePM9gJhoVJT+b+DPMiE485rH93DU+/sfuLwOhAjQMI9IKgltIhGiUHOiEIMQyyW8" + "u7Bg61QDzBymfkEMoSFs/op/1v4TYjAA+A0AOEaMXbWFtAAAAABJRU5ErkJggg==") + +index.append('FO') +catalog['FO'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABQklE" + "QVQokW2RvUoDQRDHZ7kVm9wV4S4GycUPCD6FhYW+gYXvICjEj9pOW30CCx/ABxARsbFNIYci" + "Z9CQi0KKNGZmdsZiY4gmv2JYZnb+O/sfMxgM4Jew3n3uNxrzWb9bEREAkCksAJRKJQBQ1ePT" + "JC4O9stJFEWq6pOTdDod67V9uV0wf+RvQ9JZGGNGL/jbBEBOhB3yf2GPn9CGte7RWdzuOWZh" + "p0BITrcOP5GUWJCVSJCEWJeq9u7ixWQAyUmT33N1rMTKpIRKJERKKERC6A9z9ZXW9YNdC572" + "ypV8yOiUSS/bO9sLV0iCrMQjbR9XK/YRWrb/VQ3D0I+42ewp4hDl/jye/oAx5nV32Xq/fQpJ" + "hRBnmeTNZGYrImPLF+Mg+K6lSWCMgT8YgFGPKYpivMU0zW9hfQNusqw2uV0QYRHf8AN4MF/m" + "C35t4wAAAABJRU5ErkJggg==") + +index.append('FR') +catalog['FR'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABPElE" + "QVQokW2RPYpUURSE67x+BiM4iYK0Zi5AcANGxoIbMTExFjE0EWPd02xBzIycbu49t34MegQD" + "v6iCgiqq6t3XG/zF9NvX15LmnJJIVtWzVy98C04QMLADePPyCYA4do7HB0kAJLGd5N7Hz+Fy" + "d9b69f7DDgDB7/OSYvt0OiW5uKtK0vXPH+6Znnj4iMAGI44U2VS2bUtSVRdh2z08p9dMt4Gd" + "tGzJUmgl2bYtvtQJAM+Znu5V3Q3s3bQjedliDoeDbQP7BruqynNmtbuLNLA3TWXJlEnnHyQl" + "SU91p9d2SRhN2aSXzCX7ro10p9Wdnl4LXAb2MUh60aQW/R/mDJfXKorAfu4mff/qIILcLvtc" + "fqyqqjo8PposMVoG9vOZn77fjNHnwdvRT6+O3b3GaKm7xxjPv3wz7m4G8AfurXtmcb4hTgAA" + "AABJRU5ErkJggg==") + +index.append('GB') +catalog['GB'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAACAklE" + "QVQokQXBW0hTYQDA8f93XKtWlIdFmkaidoGkhyiokRDRzRFFPkSZRQkVQhJIIPQQlpYRXZ5S" + "KUPowSLohrMLuS5iuaAyy7UopqVZmYquXc7Ozrbv6/cTNbXt7oJxvB7l3t0RmCWVWrHMkX+n" + "TujO4IbKT4GIQm4vSqRbL80oP9zQpVNzqn3kr5RGTPo/yETyx28ZNaR891p+6Y3E5OBPKQ1T" + "PrgpwyGfX67f1qahNAVvgw6Rmyc62vMc8WhciP4+8S0YNkS+IyKOVwvXxqans+MGRKWGAEDw" + "Zlhny2a8j7MyogwMYI7lZIQ5WUPDmSvdztA/LAvA5tZ6crXVC+1TKBhRFC3nQj39QfoCvHpP" + "1TEGv1bNizHXUs4Fdi7alO8lJW4+BpAwOMTYLybGCUUwDUb/8LyMpIlpkUpxZM8k2ETBEvIW" + "49BB4XKRmUnFASIRTJOZdq63YBiYSRIWRYX6iy6bR9+ZL7OGE9kCigtD1NfR8YQ1K5k7nbvP" + "KCvlWvPRW9nRuFmSYb/KVk0qhQIoXjTB6VpO1E4m7Bgm8VTPpJPGVvbua9zkV1Y0nU6BpSFB" + "Ubw0rHyd6uz57pE5tmlKHSxV5TuydXV7KEve91g+741Kk3QSkjZU+vvDpkBbi1ZRfW7/PabS" + "63bNX9vpBS73rlKjn5uJHXL4Jx55cgBc/wFytP7Lx9suAgAAAABJRU5ErkJggg==") + +index.append('GE') +catalog['GE'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABtklE" + "QVQokUWRT2sTURTF73t5mUCptg0BjUhXuo60OouCf3cu/IPahfgBXPkNBMFVoRuhIPgN3HRR" + "BQMt1kXQEpVqcRvIIo1aEhOcSTLz5t13j4uJehYXLvdy4Jyf6vV69Fciks/gdJU8TTodERER" + "Zv53MkRUqVQwHPivX3TtnFooA5StrYHdXLUKAMMhv3urLoRUPdVqtXTu7b4dDD5/cAf7AIjg" + "v3f94dTe7n862q3bvQYRMYshIgDFi1fmj82a2nK+gh2cAwDAXL5WLgUmXAEgwmr86GHx9l38" + "jkgrZBm8R5Lw3ns4p5eWEZTgPRW0TKyeP/7zyWOTbrzQ5Yo/7IAZjsk5iMfMDDzz6y2kCRyL" + "c3CusLg46hwZ82BVnw/pzFlSSqyF90gm/LEJZn3pKgUBhAtK+9TqhXJQ36Zut8vMzrl4p55l" + "mbU2TdPB/Xu/7twYj8ej0SiO4x8b61EUxXHcaDR0XnD6anO4/Way+TIPCvc/dPz8WX93J1p/" + "CkBEjIgAKC2Fc/1+KVzJW1YnTlJmlVJENHtr1SRJ8frNHJxqt9tTkEJCU6hBrSZEo2aTmfO/" + "XET0B9K2TnP0V/LHAAAAAElFTkSuQmCC") + +index.append('GH') +catalog['GH'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABF0lE" + "QVQokW2OPYpUARCE6+kLhA2Ml2VMvIE38AaCiayB4GYewGQjT6ORsOM5PIOJHkBZ0KmfLoO3" + "g7NgUxRNV33QS/Fv5v7+Xy0FcH0NoDOYQVIHcW3YlXrit/v9CgBov/9AgqTS5neiKpastOx2" + "A6wAOr3XJmvfVcVSGwBpgHWCZQng1qgL3f7Wp4+idHnJs0ds1bIVFuEXtpfSqjXgVjefub/R" + "4Y+W8urt1t4Az+ABgNatiy3gq5d8csHdxeHNa7aHI0BUM1gfvse7F/r2kx4rZMShnkvRsy9U" + "xFCRx08f++sHrDAcK9JoixlqdHQp9MgTT0CsIDQ+PztX5Ik3cuSxRo5dJ0mTCQYLrgACBngi" + "n1x81ADAX4YyZWXkOpMSAAAAAElFTkSuQmCC") + +index.append('GL') +catalog['GL'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABJklE" + "QVQokWVPu2oCURA9V1cLC8GFBKwC/oKNH2JnbWtvn7QJWiSFjVr4G6IkNta2IhaL3e7i2zt3" + "TopdxMfhwDAz58zDxHGMG6gCUL2GJ3gAisUiAJIAEIY8neD7zOWS4i2CIPCSwSSx22Ew4HyO" + "45G+z0aDtdqt2hgjIplUTaLf52SC7ZaHA1crvn9wsXjYoKqZ9PYg4HQKK7xcaC0vF+73HI2e" + "DcY1m6ZeZxRRBKp0Sid07pEiplSK220PvR4LBary6xO8/3I8ZquVLrTWVCoahhkFaC2XS/z+" + "3amt5XB4VUOEIgJ4CmRFeD6j06Eqq1Vms9xs2O1yNksNYiEOziExZKygXKa1+P7B6wvyebNe" + "I4rw9gYRiMC5hAqYPaA3xH360ALwDxiKW0pwzWHQAAAAAElFTkSuQmCC") + +index.append('GR') +catalog['GR'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABTklE" + "QVQokWWRP0sDQRDF311ONGLgCBEb01jpgV1EwUIQyWcR8TtYCxYW4qcQRAWLdEYFEcUUAUnE" + "qIdICvEPBDXH7nsWe4kBYWZ4swxvfzvr5ZbOAVzshgBKq2+g951YJERCJCatPwY/RELIBACW" + "S2EURZJW5m4h7W1OSxAACXIpSRQe4xcvjuNisSgJAABJ5zfvFCiRkkSmOjvin1aeAzfUnwZQ" + "u/u2pLGylsbKGBlLWuXDYGOn7g0vnpbn8wdbM5KW1+qkjrejlAIpirvJ9/1mMw5M0mN0DFR5" + "vU5HQqUhURrLBrXqvdduvxYK4YDhoGA/Afm+32o9BfvVzsJs5qtL9ZzkvHswFJwYHcmcVTrB" + "yVV7ajL30UmslaWsZV/QtaSlLJXPDW0eNrzGw+d4flgS3PogSUgf+kcIkcLlddPDxFH6kV0C" + "5l8QcOdw9RfrG2tQxEeGNAAAAABJRU5ErkJggg==") + +index.append('GT') +catalog['GT'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABG0lE" + "QVQokWWPMWpWURSEv5t3i7+QYCDEwsIyy7DKHizsk8reFbgBV+Ai3IRrsLIIhCBJiI/HvXNm" + "LF5A8D/NwOE7M3Pacp3aADCs/vX14dBtG7D9tPryw2tkZDZzoNczXz4ClDPV3pyfLSdAkgCn" + "MzefYkcVOd8+P3c6wO1jVGzCybLjSRI5P+9iZ1benjXkjrEjU0aV5B+9z1DsyFHB5o5cbirK" + "Uf1PxxmVVOTMgkFHVKjKdKaODpKplx+mGuWOqBeDzKMEO0PJXskBd2SlzcoUU8cB2ZcqqgLq" + "DEvLVKYYdYSToZDIqAA6q6dPLk6ZlW3QGkDbBZbGu3NIs1MB3Lj6w28zzGpW//h+9+qAJNu2" + "75909f4CDN4t/gLVAHPNcoxygQAAAABJRU5ErkJggg==") + +index.append('HK') +catalog['HK'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABUElE" + "QVQokVWRsS6EcRDE5/vfR3wcIqJQ0OoUSjqPQucJJDyFwlOI4pQaEi+h4nJESET4jrCzu6O4" + "I2wmky0mmd38qmcgMZ788b9yAIADCRSgdqB7cAAAmcpUBDwUjgiRIuX+64Nery4AoLy7V4Qi" + "QCqiu7mlmem3k5M0iiYzkWVlBUABgNQ4HVFKKZ2Odne0tja1uFgVpNlIIh2oE1CE3OFeVVVz" + "eKiXF93caqrpHB3NPDy87+3ZcDg6L4EagDxEwj0Zur7W9rZ6Z2oaZej8PD8+0gyk3POnweEu" + "muivx8ezgDY2lKnppj09ZduOfyAdKAmAlJnoaTY5P6/1dZnp8kJNM9HtjtJyz98GuItMUuTX" + "42O1v//Z71vbzl1dDQcDmcGZHiUiRxySXi0vF1Iecn6SubTUWVgYPj3Vq6twz4gSoQgHqj5g" + "/7n+Xfw/+0ngG/KLPeZjxNoEAAAAAElFTkSuQmCC") + +index.append('HN') +catalog['HN'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABYklE" + "QVQokW2RMWsUcRTEZ+MSc14fIxhFCNjmCyjIgWI6BbvUfgkLEYuDQFC0SKHYiKBCIJ0g9oel" + "pY0WRi4KWpjdsPt/82Ysluvy+PGYx/CKYSpcfoN/xDAUCkEhBfA0UONvebRzDYAEWZlIKWmm" + "STPFFKmgmH63+7pGUQkd/uozlYmF7aAGSlFhRtH6hRHQ1dOnm7dvrJ10aTtlpwVbzoFwQpIl" + "jUc1m+uYz+eSJGXmcRN7b79HRN+Xru+7rpu++Pr7z3Hbtk3TtG07m82WhrS2bZ9dxp3Jedv7" + "n47efziyvb21NlquBteApCVJi9t90cv9H7bvTlbv3Vq1/ergZ9fnwjfJ6vHzz1uTq+0JJaVM" + "CnDKkpRmyoYkyeNz9fTZxwrYvf/w5rfDNkLBLHQpGZGFjlChI8gU6Y1L4y8HT2qAZ2pcuTgK" + "ihTpQQTFFNOZHh4sA6iAB8DKoshymhhaH/bKf9m9dMO0S+5pAAAAAElFTkSuQmCC") + +index.append('HR') +catalog['HR'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABbElE" + "QVQokW2RMWhTcRjE78VUO3QRRHAIiiCU7u3cDl3EVQQXETrpLAgFHQulgtDZxaHYybmb4JIh" + "Q0cHt/jImOTl/d9L/t939zmkjQg9juOG301XBP5J//cbXQSAw0MAIQFApxMK0YMqLMMs3OM6" + "6/PzLgAg4k8JEmSYvXt+4g/Df4c/0un712E5cg6zotcT0AEQihVt5PHXt5/Wp5+/HJyuT8wW" + "kfOVzQSApCRJJOls29YuL/Pe3mIwmO/sVFXVNE1Kqa7rlNJoNLoakHR3d2+aJu/uLum2359M" + "p0t6NpvVKZVlCZJL2tzMLKW0GAzm29ttv99sbY3H4yVdVVVd18PhsMCLn0evHpeThTGcMmo2" + "/HX2/UOYPXv68faDTaNcckbv7p1vb350kd0lo1aDW/efvDw4M2qNMspcHuEMKgB1keWKextr" + "TpEygZQzSJmCFCOcoQhXACqwfwEX5o7syEJ2zK9LdrgjC1wdjb8S03cdKh6exgAAAABJRU5E" + "rkJggg==") + +index.append('HU') +catalog['HU'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAAA7ElE" + "QVQokX2OKVJDURBF76v6OBQuIgaWwSLYBCoKn11lH8kWYsAiolJ9h0b8fGa4out01elh7PEl" + "AgBkAS08VwETgPV2C6CTTmBDjgUpUpMtNRkp5GG3mwAAzeeXtmE3GRtkyCZTDKurQl6t15kv" + "dPqz3VUtdTGssLqYqnm+gGF7jAGgu9Fo9F8ZY5xOp/mli/UOvwZjJJk+pP/3d4/uJAMP2Dxu" + "jq9HWXSVWC6aNMvFpZV1d3O7fzpMOEMSTeoilepim2XSJVO22gAmFBitrlcMJSukyVASm7IU" + "WTZsG8DAPVBAAecfUEDjW94AIuRtBo6TpQMAAAAASUVORK5CYII=") + +index.append('ID') +catalog['ID'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAAA9klE" + "QVQokX2OO0oEURBF7+vuwEBhxsBAtyK4GkPXYeYCDN2PuAADQ82aEZXh1f0Y9AyoMB4qKKh7" + "qqq9rEe8CwAACwAMGADAfe8f/YRZp3e3AGLDjhQ5IsyULIYMK8WQz/cPE0YA4NsrpEghd6Vy" + "MVVmpSq9j+cXBiYIkZe0yZBghWUyRVcPy73CaiSBiYC/PvXxHgq7fUx1V4XlqlS5d7Pa8UkH" + "ptXN9dHllTabSLHi5THFCmUxy3FpXK3Pnh6bJABJgiDIYYZhmOd5st1aS7LTDrBMSQ62/4/+" + "0SbbwzAsDva01vCLBgSA7cn2drv1AgzDB1jUb/9idu6XLw4+AAAAAElFTkSuQmCC") + +index.append('IE') +catalog['IE'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABFElE" + "QVQokW2PPYpUYRREz/d4gjhM5M8wgyaCGxGMXICJoamZgjCrcCsmsxiTCexAMNIWH9+tW2XQ" + "jYE9hxvWKeoOPsMGgEHs3u0WFsAHtD16+cLGE098n5U916+uj4n44vHFGAskCRArn94mFStd" + "+/dfVlYg337v2t3de+3P753nH39+5fvXZKZqPHxmWDF22i23unKKZnomRZdhBZpWJEvWSdyp" + "Gc+46AJWhNzVpb5TSDSTGRWSD0Jbsqan+kSw08dJ4zhJVGr2VKt6Ov7fqRmKVlo6TmpV1+Hu" + "+EEzFG7cwMpGWZdnl9Ul9TIWYIzBgWXlyXNounEbBh/hJwg22Lj5cHP14Mq2JNv6cfv09RuD" + "jwX8BUrDcqkVhANcAAAAAElFTkSuQmCC") + +index.append('IL') +catalog['IL'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABDUlE" + "QVQokW2RsUoDURBF54UEwcZusRDs/AlLBbuQykKsRBBBAiJ2aQQbEQWtLBQbBe3SaOtfhEBK" + "q/cNO3OPxYbdjTrVfcy53Jl5KedsrZIkqRaS3N3M3L16pqWdfD8szCwALGSShYggZB64CCeE" + "O9dH05RzLooCqBIqwX+VUppMJt0aatO3LwhO9xYM1ZzdauI5jT1/MP1mpcdqh+EdG2scDxqD" + "u3dqGsAYbOIlJ7v0+5Ql+1sLEZI6kmoaeP+idMZjbl4pg6fPFg/u3q0NVc5hH+DqkeUeD2dE" + "0O42O/w6zvlBfZnGMF96fVuXF7gsHJe5m4dFJA9zWYSFLCKFEPY2UprNZu2P/CvaZWY/KQh5" + "EgUg+FQAAAAASUVORK5CYII=") + +index.append('IN') +catalog['IN'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABQElE" + "QVQokU1NvWpUYRA93/WiGIv4s6YwMYgxIBaKgq8hPoLgI1hapcuTmN4mYJNGsBE7qyx2W1nI" + "Jm6xd86Pxd1dM5wZZs6ZOdNyhk14TAOGCRseG65HogeAw48AArcYUSCYLbxmdqiYcSVM6u/X" + "zz0AIKkZIlhJIYorqXUd4iGuduOhid47r9vNZ+h3EidCNP199/t0X9arR+eHk1nCmA5bf98P" + "TltJXWsJkozfPp3+Wl4sXjy992N6+e7NYydxnHSt/ZlfdB2AAMkGi8XyYHfry9n55WKZJE5G" + "CYDdja5A0AIkyfODO99+znl96+WT20lW6gpu+IDjt8ez+YwmVSWWi2KpaJaKIkWae9t7J0cn" + "PYiR2uz9XzU3l7RkYUCPATQntyajzaqaNGVJYihJkSwYDe+BASAwXAGvMFzDAPAP+jxNFrI5" + "ksMAAAAASUVORK5CYII=") + +index.append('IQ') +catalog['IQ'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABXElE" + "QVQokZ2OsWqTYRSG309/bCnqICYh1IBCFl0CpR30CvQCpO1lZO6eqygddS9CxE3U6Jq6KC0B" + "xRrBVmyaNMn3nnNeh9+iQicfXh6e5cBJwh/i375wSQC2tgAoAhFwlzncZAYzkfrL493dAgAg" + "fTmEO9xFlv69TDErZ5Gp0QigwNqa7t5DpQZBluEhN7lklFHuoilCeY5KJbpduHtEzMOefeqa" + "+czsjPnl4etXX99MZrPj6ejH2cn2+53ReDyZTIbDYfkSXnx+/uHnx/F8VF1ari7eePftbXHp" + "8kJaPJ2f7h3vHYz2FdpsbkREAUDSo1sPs+cH9fvufu3K1dXqioTb1xuSVm62nuw/Xb/zGFJE" + "pFar1W63j74fJSWS5kYyPGik0c1pTJGmnNZqtU6nU/T7/V6vNxgMSJLMOeecyyBZlhnNvNls" + "AigAkFav10mauVl5SDMrbWZ+Dv6DX3iOQn5NOXoqAAAAAElFTkSuQmCC") + +index.append('IR') +catalog['IR'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABVUlE" + "QVQokV1Pu05CURCcczkBlZioBYUIDVQW+gHW/oKFf0BHb/wH7CyNhbG2wVhqQ0FMSOxM8BGs" + "1BARgvfs2R0LHkEnk9nX7CbrcAL8YAoD4kwXGWbq4THE0f4RADMzmJpGU2WMNqGIxkgRjaJy" + "dXrl4eHgeqM3o85NkRpUokqwICpBg2gorZaRwDcPmxvZjbXsWm/U21zZfPl+2cqXnr6ftlaK" + "z6PX0nKxO+iW8+XHr8ft9e3hwdCpqnMOAEkQBDlOeXdLI/f2uJTlDM65wWCQTL6d9ibx8oIf" + "78x6np9xEYCZOVWdLkzvk6psNCjCep2ZzNyfJEm/388c53KoVtluIw3sdJim7HS4s0vvacZW" + "i2nKVgvjMR4ehjfXLgUytZp1uwiBIhThPFnIoeoqlc/7e29A8pOiUKAIoiIKRJwIYsREY4Qq" + "VBGCAW4E2ALxt/w3AvALz3tDHmqDQaIAAAAASUVORK5CYII=") + +index.append('IS') +catalog['IS'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABYUlE" + "QVQokW1RsWrUcRjL7/5/h9ap7VXhKLQ4uvgABXFXN8EX8AlcvB4FEScHQRwENxEHcRMnB/sI" + "DuImOPQG0bYcHBSO70u+OJzn1AxZQhJCGm68xgr6+uBn112Tur2XCCKIBRGECARAIHoAzw72" + "AVS5tTacHADt8MlN0SyTRVmlZKXqzfhdDwDwr7OFqgDw+Bjw9Pd50qkiFayUyBptrQHsUVWC" + "qiTbNtN2sEinlKykgiWJMhBNUmsNgG0Yp/fvbb7/4IvQWpvP5+0HMBw/4nRq0pmViUxnVoYj" + "K7MinCzGpd29b1+OmqRlvGEYp3dvb378dGHDYDCYzWZ9t/Ni8vTW9M95Um/H+xVp+87kKLkc" + "UMliiazdq5c/P3/VY0GqlrLtirCdVPwzmNTSQAFgjwiqrmyskQLQj0YARsN1qpIlWRKrJMgF" + "VEP3+P+RZ7PD7xtb109OtrcfAgHU6uBaMf4CWkFmlGgAUDYAAAAASUVORK5CYII=") + +index.append('IT') +catalog['IT'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAAA4klE" + "QVQokYWRQUoDURBE38jfeYIRAhLIObLyFll5BzfmFF4l98gJsjUXkAjD7+pqFyMjCaKPontT" + "Dxp64A0mAAzi/Hz2NQ+bjWFJ48L+aQ/Ydnkcxyqgalm7XUWUVBGXw6HRgHr/PKczM396C6dT" + "9V4Rw2plaBi70imnMm7bVdX7LBBhaECSKsmSddu+FoCGkDMylP8JkmchLVndXfmXMEQY7hBR" + "0bMrFdl/FyKQStL3SanImPO7oEBJJtCYCGu8HyNDSgAYhoGF9ZrMOYaBF/gAwQQTx9ejJNvz" + "BB632/nHM18uVneE/JK6zgAAAABJRU5ErkJggg==") + +index.append('JM') +catalog['JM'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAAByElE" + "QVQokVWRT0gTcBzF3/5EtZpmG4NyNYsyvYycEZF5SCIp81DQIUQooVMFHYPoEkg3b0XUIZiC" + "B7FDl+gfHcpIzCLaIGxtFGg5so05a/u+7/e3DjOoxzu8y+fw3vPU3mGhjFMfMZ0HADjAAfqP" + "BagAEbyPIxqA53QXkmNX123AdME9LrqSmsLMqdZUnSq53qv9jTwc0l9F7u544J+YQvf92pHE" + "/P49tt1r977zzbKxRjqSjAd4ISzRoMy+5tjTbUuAD8DezkOPXhTlt+1rYXfIfE4+rKhzHAjJ" + "5agEybvjcv0OqxbMZrN+AGZWqerEE51N62AfT+5g+xqasS0gqbSMjHImJSQ3kwC8AFSNJMl0" + "htduMvdVWoPS1iC5L3LpBmdSUpeqrgJmqqpCaY7IlbMS2yKZoswVqrGtMnyxuqt5FSAJwA+A" + "pKqc6NLB47I2yMl5ufWN4ng+LGcSHN0pI0nenlRTRb300Z6Oob6l/l7+JIdzksxL2Vg2eV5g" + "qsTORjl2kPGYflpomvu86GlvxNTLoY2b+OoHx/O2KKxvqk7pqNQmn54LW2/ECnmL9aQ8K8+w" + "UELiLZYz/78rf3MFUKAFDw+gtQF/ALzGPhZXojC7AAAAAElFTkSuQmCC") + +index.append('JO') +catalog['JO'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABLUlE" + "QVQokZWOMUubcRjE741vnYSCuGQIxGq/gFvWTqEFIZMgCG6FLo4dXF261OxuWUqhIIRSCIqQ" + "IWu3fIBAuxUkU+B/zz3noJEO7+KP47jhDg5XeBmVgX/n5xNgnpmZkiIkxRMk105yPB7XAHbg" + "4z9/76Rv0pKUxGcKC0sphWSn0wFQA3Aa0jtpl+z2P/j0xE1UVbVYLFoJQMLBgTO7DE9+eTRq" + "HNjOzBYA7O/77My9nkkvlx6N/PWyoQ5ERA3A87mHQ0+nYLFrHw786WPDJRtAKwGTvr3FarXY" + "Kn7fb2wDeLxUJ7ARYfLmLYf9Teq6/PxOkWJRYUYkKUZq7/Wb2cWsTuB+M34ctX/vvWorIoMi" + "k5ERCjmYIUmWUghUX4DPA2AbKGvFfyHWIYEAgAe4hlu5xLXqFgAAAABJRU5ErkJggg==") + +index.append('JP') +catalog['JP'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABA0lE" + "QVQokXWRPW4CMRCFx6utttgG0VBwDEoOQENBS4GUXAwlHeICFDTkGlsgEmmbRUKCImNrvhT+" + "CSnyZNkz1hu992z3eDzkCWYW9+eiIJjVItI0jYgAcQZil48n9H1fZxK/xfHI6QOvzGYsFoXt" + "nLOo8Ie937PdEgLqOZ34+uTlNQ5Eh1VxD8gwsNvhPappvb1zuRSREEIVwxWP3G6J6hVV7nfO" + "Z7KEmdVmlntkPKaq8Ip6VPEeESaTlAGSpcJnNGK55FsTW5XVium0vJiI1MVSmtxsaBoOB1SZ" + "z1mvS2IRCSG4YRjati23qYg+nctpRQQR6bquNrPr9Zo/Mliw/xBFfgAmk2yfBC2dxAAAAABJ" + "RU5ErkJggg==") + +index.append('KE') +catalog['KE'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABeklE" + "QVQokZWOP09TYRyFz3t70UJqdLHqUkg3Q5j0U/gRymBgRcNsGmUiYXdnYnGtiwvRgaXuRG8b" + "Y3JvA9II0qCXe9/z++NCE1efnOFJnuUA/0kA0O/3AdgcVSUpIqLqqlVdC0kRkoPBIAEA+GQy" + "KYoiz/P89LS3vt6ezR5c/NrY3PxWFOPxOBuNsiwjCSAcD4fdtTU38znldGoHBxCxXm+p3b4J" + "ZknaOBq8D2fAndc7OjlxERdCZKf6sx3SBdU3s4vd2qyqPUZXTZc7o8MPaQk0y0qvfruIRyYm" + "57zWhaZTp+W1Rvc6WqRLRFVHIL0FJElACBbgIQQPl0J4I1FtqQWKMYICtaAGIBx/+dhdeaqq" + "N1fdLn/keLufiNZbz+896ribu5t52kg/Hb0LeIUXz15+vcqopMVoftsWn4wZIg+7PNef0UpR" + "iunq3cef94YpKohwpdmhUkxopPH7qtD4UOW+tdxBE3UxNVQI2AYqQIA4n/wjMhcDBGjhL1ti" + "PSucHkU3AAAAAElFTkSuQmCC") + +index.append('KG') +catalog['KG'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABKklE" + "QVQokW1RMWqVYRCc78sTKwkGLAK+RhuDheI5cqPcxMoz5AKWYhHBwlqESIJdIIG8mdkdi/9/" + "iYXLsmyxuzOzM27wGL2v/zYPaQDABsDTszMA6UY3quJCOXbsSLEhtR3pz/n5BgCQvvyNqlRF" + "wpMaW2Gov6tvFbHJSHO7NbBpIJ3H6VGbU84PRmmccPeJfaeQkSI1MAGg6gF9vuF4LX+lv3C8" + "4sEJQ4YrSAOzAbiWA6HGC+WAuWZdsQfzck+JjO0FIYtEMWL9Iu6YQ+Jol1v25W6dJlvCqkFa" + "DoD0N423nO+VUv2gPis7Rmp72r0uLAJWVrz/yPlOuacv1DcK2RZcqdp/SR7Hx0OKq60h5adi" + "z+caz9z2XH5YBWBcAd4b2f9zd3F9sXkCfwH251SCACM16gAAAABJRU5ErkJggg==") + +index.append('KH') +catalog['KH'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABbUlE" + "QVQokV2RsWpUYRCFz/29d/eGda9rWMiquGhjKRYGKzsDbmETfAftFUFFsbA14CMIqQy+gcRC" + "3yI2ErUQqyAmO+fMjMVNIDh8DKcYZjhzKow/wQJ9CfAAenTSTwvUMLzaugYgAx7pAffwSCno" + "cIXU6yRj5+2XGvjTpv38tfTI8JBnP2EeYkhJBhkmv7TWAr/rz8t7V388tf39cE8yPdyWg9ks" + "PA6/7kHuy6M0S3Iwn9/GxwIA7ilVJFyjG9cHk3Oz588uvHzRjEdnb63DlWZpBhJAERDyIEVm" + "VVYfPBzdXaBdQdt2m/enjx5n3chMZpKOgBoAXJCSdmalK13Xbdwpk/Np1i0WZTIpg+Z4fX8h" + "gCTDDO4X32wBqJomM7Ouq+Ewgfn2dpUeUkgCKuDdk9c3v33/S4ZQSCeDdGNQQYU8eUi5X7k8" + "2t15XwPdAYfjaUP1XoLHjw8p3VO9iDzwBKYV8AGwkyD/43TSPeUfjDQzrVjF/Z4AAAAASUVO" + "RK5CYII=") + +index.append('KR') +catalog['KR'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAAByklE" + "QVQokU1RS08TYRS9X1uYdsa2004Z0o4kIMUJRlRSFq410oYfYHDnD/DB1hBt4p4FC/snhLia" + "hYnB16qJj6hx0UUJAVL6mIlTtSl1Pu9hMWI4yck9q3PPyRH94YDOgJmJiDlU/3gKycwRIkrF" + "1VRcbe3tf//8ZQwiFdd0VU1rWkZLJqLRbx8+HjSbE+m0qWeZORYa93o9x3H8H37ONGdnZtBs" + "QkoUi612Z3t7S89kjGz2/NSUDD8A8H0/CILFpdJsIY+NDayt4cE9PFmfNieWV1Zc1z1qtwEw" + "s+gPB0klQUTdbldNJsWrN/FnmwiC3zIyGv3R767+XL3T77kFqyCE2GsdxsKWAHK5HBG9dFMl" + "GQ0iiaeLD1sjZf3g01VF0awCABJCMseYCQAAz/POaerAXrhvP4qOj+0rpn88apaW7eEv1+vn" + "83kBEHOEmAE0Go1areY4TuVaYu568TCapeHxzcvjt25YOzuvq9VqvV4PO8SYiIhUVQXw9t37" + "+Uvzj29f/LqkyL9YmFZ2dxvPt14YhqHrOoAwEgOwLKtcLnc6HdPME9GVC2qYc3KyUKlUDMOw" + "bZuIiEkced7ZIf8fkixPhyYpJRExEfEJhTEZN2ZjOv0AAAAASUVORK5CYII=") + +index.append('KW') +catalog['KW'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABKElE" + "QVQokX1RP0odcRic3awi8WHxTMQnSBpPEN4BvISeIZ1t4J3APuoNcoBX5AYewNrWtCEQkeU3" + "f74U65b6McUwDHwzTAcA18AeMAIjoBmZSZshQBgAbLLBV+Rjwris2JFKihTSUsiI5vZmOwDA" + "cz39/O1z+9A0HTNkSJNmc2tpNE8XpxgxAEjK/6xfvry6OPh84DiJy6kkdmVS9nf2d7/tDgBs" + "S9Ko9XJ9sjqpN67ruvViPQCQTFJSkLfcVYWuS9IDmB40Nr9jr0JVkgEAydZaQbq/z3JpT/ld" + "9tQCSSX9YvF8dzdF0gB+f+GX29uX1qq1Iqu1kK9EhPzh7Ozvw8MAIJ1+fFodk6PcizX5pJ6c" + "xuht2GX3QAfgETgC/gCZ98VMJgWzCOA/nWhlKT+0ek8AAAAASUVORK5CYII=") + +index.append('KZ') +catalog['KZ'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABk0lE" + "QVQokU3R32vNcRzH8df3e75a7XA2u2CJNY3UslLIzYpyoZW7ccElhRu5FFfu3C21qxUllBsn" + "d9Kio/wqV3JBmnEzK3ScTZNzvt/35/lyg/wBj6tHVmv5g7Lxwv1SiNWQQkIKBAIFylGgQKWy" + "WsudnTPLGnmk6WRhJQko3O1R66pIOOGwK/vai7VC0rK23aqmcwsUdthbeb/HrTYbH/voGn0J" + "B97cl6kklyT5mOcS6tnJmGqf5ye5f5DmAR78whWu7MCCPIlhf7zL2bAzVzndutvjfmb1Glra" + "64d1VipcJYetUrmQnY57trQH+NJPexevXqf9jrITg29j90R6XuEKR5KCQqHPHrntk+Af1Ie9" + "OOEn9zi1wPZvDK3QOOw78xwJHFhBrsBOJ5gBtb1hlfqnNDblG5NujvrNeV9oMxQ4ULJFFBJb" + "WLjIFeFkLbLjHWPr+Trlm4MszaXLT30o+wMkVCjA1bl115s+HSisCgWbXvoM0PHAKE7OEk6W" + "gkzNn1ejcSn//u9S5d/X+G9aqCvl+g2/aVpzpMUQkwAAAABJRU5ErkJggg==") + +index.append('LA') +catalog['LA'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABZklE" + "QVQokWWRvWqUYRCFz/vtZ4RFTUxcFdQUbpne1k5IoRaprERIY7edhAi5BgnY2go2snfgfZgi" + "IvnBaDSJZL93zjljsRhWnGJ4DoeBgacI/4wB/4WLOMtFQNncBJA27JRAQQSZZEZcbEScjcft" + "qPd0bfDg5PA4ZcuWaieGRKUIWaRIBudvL25h3G7fH91bHO6ro1NCV7U03z57NMjMd+P9nb1J" + "ASoV9J0bl3fwpgEdQnUJlQldmvLq+d2VYX9l2N94sTw315zV7Fg6ls4FqA2qlTAlKjotXGmu" + "9nuZmZnXr/VuLfTqeTDIICWADWjJ1ajKMA6+x8lvTg+OfsXuQUegY1YmIwG2qKQywlTaOD7n" + "xtvd9Sc3AWx/2Dv8yaag0qKVBlwe4+H61ssfX78pJIrBOpn+IIWblCiJopeWB6OPr9v3+HTp" + "yxCMjEgqGXDYkcnMSBIkJEv4rFWgnP4nlTM8rTjj/g9Gz0RszhHEXAAAAABJRU5ErkJggg==") + +index.append('LB') +catalog['LB'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABUElE" + "QVQokVVRvUoeARCcPQ/tRDu5iILPoOYt8gwJMVY+QXwAbUVrIXU6LaIhpLIIEpD4RUyhRJTE" + "Dwsb8TPf7exOiuNOXZZhFmaW/bHfeIxssSFsebacQAlgan0VgDKRqQhFiFQQpOgiky4S9J8f" + "dkoAKMu4/oukGAqq1Ymu2tNdrFV7UU0nUI5eXY1VFQBIkgC7ub/ZPtz+N3x48/JtNV4pU5Ik" + "M4wuHRUoCpjBTGawQob98y/H/d73/tHe2b4rBbRpzCwys+0uQcf93ufTvduH29qHu72dX/0T" + "dQFkY+jUKW0dbIZCCQ+vw9e+rrPTSyTLztCssfFq48/d9fLHpVC8m3+9MLMY6YaRZgoAdgq8" + "eL8Slxdine5yz3ooOuuhkSLlFF3Bcnbu26fDkoAGA5uYBL1giG7uohfOpKO5coQYuh8AsB/P" + "H5lPHtyVaBHAfxBTROajm4qVAAAAAElFTkSuQmCC") + +index.append('LI') +catalog['LI'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABVklE" + "QVQokWWMMWqUYRiE5//20xAli0ZYCLJCIgYsAkLK3MAjCN7CCwh6iTQpxUrSBbXwAIKFGJBY" + "xEZRgmBgl+Sdmfe12KRQ4WGYgYcZMH2BBBqABgESYkHCCQgQEJdFHcLT59t3Vn/evfXtwfTL" + "+6/rh9/Xj09WpVKWlHJKSaWcL5/tdzQA9XBzf6mfNcXO9GhjvPLk42My6aRMJZWkb0+WATUA" + "WTj4tHX96nxpdCbr1YdtcqE6lEEHHUopgejIxhzeft56c7gp57mvUAibhogQ6CEECucG0Pr9" + "04N78/GN2W+pLFu0JKVo0pJNSZY8uXbzNXb7u9neyq8xfhyXo6gii/F/pjhqG48w63Ng2dTC" + "ViRZihRLTEWRpSixpJF1AnQAJRWZ5MUZo8i8+GYxSky72Qn0BEoaJmtNhJzkYCbZpBQhlZTp" + "ZsNOYDgCBORl4u/5DwD+AH37YQGY0d3gAAAAAElFTkSuQmCC") + +index.append('LT') +catalog['LT'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABIElE" + "QVQokVWNMU6cUQyE518eQjShBgpOgOi4QSROxAWi3CNtijSp03ACRKJUVFAgBaVCK4E845kU" + "b1mCZY0+22N74W9A2IYFAzDgDVuvLHiFAWF19AlA0oh30DsxomzTQpgI5tOvr2OsgAXRw5Ke" + "PqTnGJlZMWMuu8cAxs81jkosJj1P2kw0TU55gmt3r64fMc5+4PLj8/16rZYsmjTZLJNd1cXm" + "1JMPB1dfMPACudmUJYvNcrFJs7qq39bUwhoDQEeKyqWeH6q8WSsXJ4cMAYxvn3EaPt+Vqai6" + "aE0bzdpoM9T+ib7fYJwDB3/VfxjSZKpStYFtR4x67PcFMApoKoeHIaGOGBIkJJCLBCndS7e6" + "X4DlFhDgV/X7Ev/1DayAfyUid0PrCp4LAAAAAElFTkSuQmCC") + +index.append('LU') +catalog['LU'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABGklE" + "QVQokX2PMUqDURCE5yWvUETjD2IEQUERbKy9hZV4EgttAxaewnPoCSw8hpW/gagRw3s7MxZ/" + "TBp1WIZvh2Vhkk9P8D4BAEASSiBmKKFSVKoKVCEsJ2PyiusRAEvJQtAWyET1yV5UkyYd1eR0" + "dJsxK6jF7QtEUI4Aw6SDjuoajuqorjUNdwRknJ374Ahb26BsgjLpuYdJi44wicFA+8M0Ho+b" + "pgFgG4bhv5RSats2d3W7aAG/CilJysuj///byZaULx83Lo49+TIF2pQph7zgro6EZtVX9/18" + "96TDzf7zhysRcqUrHXSVYwFyCHvreHtABtRDb3cNYQdBgUJV6iDUJYm2BGSldPPpqRBCEYoQ" + "QPlZpTmrc2AF3+HEeWs6fQpmAAAAAElFTkSuQmCC") + +index.append('LV') +catalog['LV'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABGklE" + "QVQokXWRMWpCURBF75OHQhRSC2rhXtJkKfYhhQtInTLbcBNi87dgCEkRSKGSgMydO5Pi/y+m" + "yDBc7oOZN8Oc8gyc0UUA3ut1Wq8DoH4Dd+s1gIzIiJTCFfJ0D/cg5R5kuIt82WxqBYD8ef9I" + "KaQgWxUZpIyihZnI8XweQD0DHsnrajO5h1E00WSUmcgheQLK5243WSxklpmISCDa3TI7E92j" + "Doev222RVErJzMwEkP9HGQyOh0P9aprxbCYzdH9mXib0vh1VR6O33a48Aver1XG/D3eZBbuN" + "dTFm7ZVul8unpqkBhHuS0aZZmAWZZklma5zpSglANSDpN9NpkOGS9509hHSX1B7dgfIAnHqQ" + "dgXV/mJu8U+AX3wogUmU2HzTAAAAAElFTkSuQmCC") + +index.append('LY') +catalog['LY'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAAA3ElE" + "QVQokVVR223DQAxjAH+kE7RARul0QaboAP3IYpmhEB/qh2zHPgiCdEeKPOiCH5yOgADaYup6" + "5wXA4/vRQOB03E6iSC21FCliKIvN5+9zAdDo19/LsduDGNBEhXQxvH3cVoV0HM88xwwVlYtN" + "hjRpVkotFBYEhsfDoFdQyFRtdbloQkOI1WLkaIOyUjQrZGouHaOwQHBrbOzo8XAky2K4EmTT" + "3D866rsIm/MqCxlC62T9ML5ChTIZuw1hgcDw8/rliJHylpJlSLYit91GcMH9tMg1T5FDOysH" + "/gF/KJDzB2IFMQAAAABJRU5ErkJggg==") + +index.append('MA') +catalog['MA'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAAA7ElE" + "QVQokX1RMUoEQRCsORe5L5ywqWBiYuDj7lf3jENDQQMzzQ3uELuquwxmj90FsSmKYaaLrqlu" + "xly1Pv+JZgD7PQBXoQqZViJlCZJJL/h0OAwAAPvjE5nINNl5QtAMR5hs41jAAMDlVXdESSYR" + "YYaDXQCygKGAlgnJEiiL3+bzPUt8OMb1T5izAMAGALTwQL7fxHkbp228jpfuDuliKWXJDFAm" + "b9/iy0zF3Qu9mNBmS6QjIPWHDePxiSaLCz+SJXXBVf/iFMuUiSPmGxFKZKILNhR2O5NQQgTZ" + "SEjoLPUMkVlAO68X+c+Oe/0C+ctawBP+hnQAAAAASUVORK5CYII=") + +index.append('MC') +catalog['MC'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAAAyUlE" + "QVQokYWPS2oDQRBDXw/tC3hpmJP6Vr6BT+ELJGDwNqtutZRFD06GOIkoChUl1aeEL3jPX0YJ" + "cD4DsbEZIxoMRUJK7/mWPy6XCkDy9s4YjJHeZ96i9fSW1tJ7WVdDBeLs1K1F2qS9pfVpoHdD" + "NZQxkCLRFb2Y/TQAlcOBdU2tGZ6nbxf/eIDTyddreTwex+MxSQghv2NZltvtVm3PGv5Sz67t" + "ansW/xoASduGp2eilMIOBbZutX2/3yVJsj0XPrkkY4ykZVmAT1MWYm5ocuzlAAAAAElFTkSu" + "QmCC") + +index.append('MD') +catalog['MD'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABW0lE" + "QVQokT2Qu2pVYRSEZ++zTzTeQIIQIl664Bv4AsHCdGmigpa2AUtbGysfQHuxsbKLaLCxtROs" + "BI1gJKJED8k/s2YszjbDx1SzZmB1WNzGsYT6tIYB+AUbPgUbH1ZhQBh9APDoySqAMmx0Sw+T" + "4Cy71CQHfaZXthQxJVMfnz0fACDY/cGqVCX6gunBq5enXby5Pkt49JkhQw4rFxvQwyhjnlYF" + "qb3dunpp1g759dvfpIXNIzLQQ3CgisqUE+68Xjz/tF1/M9vZXoibG91atRZRwICyFM1dTljV" + "nbnRYup7HzdTJkP+P4DKJq2K6LhJ3cbbx6TvXNtKOKZJSwYGwJKpUC46aZu32I4elNrde39i" + "pimiyajGBRYozytiZvh5e4N2S8f4pDl/a7nGBUleXppKYRmTFfjC5Bx7K/mNyYmFy5UqV6HK" + "QAe8AHTM/vv77uFD2CPv1mCMAPgHcLpkVfGzF3MAAAAASUVORK5CYII=") + +index.append('MG') +catalog['MG'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABAElE" + "QVQokXWOPU5CARCE58lLiAlGEgoqGhsu5Dk8hVyIQ1h4DUQSMaGQhp3ZWYv3g8a42Uy2+GZm" + "m9PphGFszxeL/v5nWwDz+RxAVQHAZlPHT6QaaSLdkCXVoOfttu3yehqo/R6HQ5H9BotREUU2" + "q1XfcKWrIFVEST3KqGBnAGmgtX2lCxX6mz0aMBqqqus5TnmehcUizTBpRjFSnN7rZXyp+nw8" + "PfL9K6IYk2AyHExGBs2HO75e0Noe46sqFRcGzV90UlZOhI8fL3U65g1KZsiUU05oaBg9y9ul" + "0yzKoqmUSpmZlelEoNntdrZtS7K9fl7jDTBwAwgQEIAAAwJm+AYksGMfxpLrpwAAAABJRU5E" + "rkJggg==") + +index.append('MK') +catalog['MK'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABiElE" + "QVQokT3JPUiWYRiG4fP1e9vCpqAiJQpC+gENnGyoraGfKYiWmqwxajGCoFGniIYmCQKpKcqt" + "oqXBisihJSz74cOhwETy07qv+3muFgnO6Tix8SUMhvK/LuryFzagB79hFVZGWF6hZQJu3+Aq" + "flyblUopzkJk02RnPPtCzvRW+UJ6SGunn7ZMwg57dInzhZnid6IUL8qt/FkOeTh8ObygZnqg" + "vqIF/NPcK+wrPia2FT8Lz6eRCZ8IH5Anw2/EoCq0dZRmsPAnvZgspA/KR+RRucrL4V/hKXk9" + "vEsMiRe0vAaK1+W15Hv6vUx4TLb8IdyTb4b3hvtFf9brtBzFF9Mf0z+CjfSQ3AnPyQ5/k9fD" + "j2SHt6s5rHqHts7RjMhfg4H02fAX+UlYsuWX4ZPylfADMZ9ezYS2Qqc/PS4OydPy83CEh+Ut" + "4Yfyffl4eEq8LcwWoK2n6JtIujt9VywVdgup2S/6kj1CyafkVuFa4UypAzS9HvUcdZYKFYAK" + "dY5aqWObuLk6sMA/749Ehz/eU98AAAAASUVORK5CYII=") + +index.append('MM') +catalog['MM'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABF0lE" + "QVQokW2RsS5EURCGv3Ndi0i0KLZQUPEgHkWv3YfZnsYT6MQ7CMm2IuJirzv/nDOKw41FMplm" + "/u+ffzIJbsABmIBBgRY6cVrgb7Xgs9nB7t7k5Hjz4yOur7u+Tykr+dmaeyOFe3z3t6urFlhf" + "b44ON6bTSSn0/fZ8/owsdB9SmEIWZiGl6bRuoJSAiEgRpUSYleTlSyoLUwWQKtD0Aze3vYn3" + "Zb64fJEaVMJs9B4B6n0xDHcPw+Pitet8+ZyTUaQVdQXcC7RPnG7l89wvSickpJAa68OG0fvr" + "hhppB5KWYa+4/xz/UuMe7g5tgTX3+PYeA6xgLjyTMxVo5Ozvh4RnXEhJwp3a3cm5VoH0vvpI" + "/vvuOAI+ATPsVhKSc7rOAAAAAElFTkSuQmCC") + +index.append('MN') +catalog['MN'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABHklE" + "QVQokV2RsWqVQRSEv/3vHxSxiCSFUdKIknfIO1iFQArfJPggvkZewN7WwsJKREEQQbwJyc7s" + "Tor9b+7FwxR7DjNn5rAlbKvDil9wuOk63FzzrG+aDjPA5SWn5FMvt/19f/pPpbXYxV6hx3t+" + "Fyl2pPXV1QxAcvSD340/7bvbX0dakNq7vqbWSOX4uMME5G3Yb1y0SDK1RorqoPXUukDqMHco" + "HxtvnC9msm+X3UOWmgcpEsOB8xaUM+VQ2q570GxG9nJ0ivPamSuy1Ad1sNlxKNtIXXEFp9Za" + "c3e3TcVwkLBjewhWzfkpHinWf3kWgYUbrTEE0wezPsosaC9eTk+EVGwkiqfiV7Q20KFc7/xi" + "hwM+w/OdwfobJ+M16h6eGU4S17GA1gAAAABJRU5ErkJggg==") + +index.append('MO') +catalog['MO'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABj0lE" + "QVQokU2RMWtTcRTFz3v+CZQXaeoSGoliJlts1aVLqZP9CN0EQRRBQQSjgt38Burk7GqHdujk" + "IkjBSRdXoWrqEEig5JH37j33XocY6Bl+nOXAgV+GN0CF/3GAc8q8zPqMCQkT7G7vAvBwD7cw" + "0rKcACc1GapOuqpTTQ/eHyQkAPGnHFiYmXlokcxC77azd8flhHpai4SoabfZhSOHwz3MjW4e" + "eq+TlypFuvS5vClORP20C6EIRU0hyAEYjEGaRvDD33Jc8/bF7f76C7GCLm9/T4QiJuqzAUE3" + "NVXXO22strYeXL2/2lpBxLP1Jw9XXo0rvLyciwmNIM7hFjZ6G6N6JC5fx/L42qPN9mYjb5yU" + "J9cv3DjfaC41Wq9/fNLQxcbi4MsggdBQMaFTTfpH/YW0ULOecpqyNKpGw+lQXek0J4gEgsbZ" + "JTWdarVzZadTdNaW1g5/HX4bft/7+ZGhdKMbiIQK6lwultWUbqTuH++7O52VVXT2Wj0zszBz" + "gyDDc+B0LlLmUqszpuWM/ib+Ab9BPtFV+EpyAAAAAElFTkSuQmCC") + +index.append('MT') +catalog['MT'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAAA70lE" + "QVQokW2RsW7CUAxFb17crfQLYGKhZUAMZeGv+DT+JlLVMjEEBqLQ5dnPt8NLQtL2yrI83HNt" + "yUXTNHBHCBjJr9fn5dKBvyUi8iRSVdV6/YYiACABER4OZdsGVZqx7/fjUdzs4+uzPp9NdbPd" + "kgTAlHg68XZjVGpkjFQtFgsHJIisVq/t/fvhJunOmH2RUTMAVQfE3RHC+25HEgTBTqo98wDQ" + "Aehcw0By4s6AWbehc0zye2C0ocgn/R/vHHxDPM1sfNIEoE/iTWEJKWHYMPSsoiwxn2M2gxnM" + "kFIuB4rL5WJm3qub6/plv//146wfNaZa7RkAoEoAAAAASUVORK5CYII=") + +index.append('MX') +catalog['MX'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABcElE" + "QVQokUWQQYvNcRiFz/83/7lNSHfuNIRmalaUBRsbuhuz4SNI8gUsfAULCxtla+EL2CsWYqGk" + "xJQN6VIyZaKkG9f7nvcci/+UZ/HsTk+dDtcPovUYIL/dnTU0SQAkcbHY29oCQECAgB5ot67e" + "ATqponKyuta6Btg2AGcu37gJWUVnfnhwvwcAY/fXXqn+ZNhG5//IMftsy5lLx48NBckqiSqK" + "hm3P3j2TtHHqwpKtDEgiG5NAg1gWXazKou0v719+//T24b3bH988NuAIZSpSSQE9IkpiMYus" + "sj0aH5m3wwc2T49PnLRkUiWRJgk00FRlMYuhtP309YuvK5PVs2eevHouWxHKNNNMAA0ExaEQ" + "GYYvntvW/Aerts9fsl2RzlSkWQR6BIpDIaNoeTJev3L52v5JEc6wYZVL+4MUjx5ay8rf+bfr" + "OgCDAXStLW9uALAspoAOU4DAAhDwEzuPdkZtRFKSpJjPd6fTBhAA0IB/+qNU8P1bz0wAAAAA" + "SUVORK5CYII=") + +index.append('MY') +catalog['MY'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABoklE" + "QVQokU2RP2gUYRDF35dvveyZU3NiGj0METGNBCzsBLX0KquAVqK9GywMWFqlsBElIiJYpIg2" + "hojYRFJd4Cr/gMihuTtDvIuaxj8k7jez8yx2o07x+M0UjzczDmhWy68X524BqF+89jOdAAyw" + "Jk4P5wTof+qq5fufOh7AcmP8+PhaqZQ+fX7iQ3v/zM0Dgz4jQDPu1K9eL7p+dRE4v9Lcd3Ji" + "pbJ7C5bVz/anXk5uXr4SI6UoJZgIQ/C1Qx/nH0cAANv4Ujl2ZKAS69a2JzMRDD+Yjb2SRhbi" + "vD+aTLkhf+9r35j1Z+7UL00ugzr76NSrtwcflqbLSE3VRBiEKr5We7+w4IDG3dtD5868ILPk" + "xgVRhmAifPZkpOwDyWIHwHn/vd12TeBwMp121yBiKlRhEAsSyw8XD1LERHLdNTr6bmnJSf+z" + "27OXWQaiCJuHNiu8zUiCdFG0ubrqWsBIkmi3ayJUpYjlmUQYAkUsH2ZZaWzsTaPhtjudqFot" + "bHauDrN/HwBoBhLAt1YrChu93+vrZmaqpvoXVNXMkPeagw0AfwC6FlrZi8M1hwAAAABJRU5E" + "rkJggg==") + +index.append('MZ') +catalog['MZ'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABhUlE" + "QVQokU3GPWuTYRjF8XM3d2JtbenSQoRHLXQUOvsFBMFJ0E27dxMXUayOgopzcXByEHEJiC+f" + "QDrVLtVJF4tpkqIxiel9rutcDvKA8Od3TnqyjlvXgFPAFBBgtf9XajNSAN+e3u2s48eSVOTh" + "JneZhZnMRLqZSBmdne1OBnCuHxsvvz+/4DurTtLldFKkSGdRKV7orOYrZGQAoVhMvvner96/" + "2WyfjYiIUKgeRYSknPP+6f08AT7P+epl+/PC2luP9pifkQc0kiylkDSykGR1pup86MwUoH0x" + "bb1eWdiYizQ5Px5cGfV7o15v1Dv81/CwO+x2h93BeAAgT4FXb/zh9cnRu9nZI//ozW1DIQpB" + "Q2GioViiNWgtAOkYaNzZjN2vmDc9uPdracEkRIQUkkKQFB5So3nyy97bDAB9iwZxiWjdXvxd" + "QiXEWoZKBBGeTqytHXzKAmaWDTfaURFTRxDBFAQMIJIhGeAIx7GrjzRehh5DK9BPyACDBBlU" + "INXfIAAGtPAXn2ZJkhleGGQAAAAASUVORK5CYII=") + +index.append('NG') +catalog['NG'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABHklE" + "QVQokXWRsU5VYRCEv3M54ZpAIrkNoaCh4UGIvQ9AbXwBKxore0tfg5fgIShotKEgkWBy8u/M" + "rMWJSiBOsc3O5pvJTnyFBYCAuPtwd7R/lARI8rA8nH85RxAYMDPzxNW7q3WdzvHueLt5A93d" + "wLa2l+8vRVVUrutv1zMz0N9//XBsOwmb/ivHtz9vR0alTg9PCTMhaceK5epXGh4jo1zlYjAD" + "xmopUvTCnWR4DI9KVdYDobhcshQ1rwgaayRZiBnhSNHIkF8S/kXqZ4TqGh6yyqPT/ax0OsOj" + "uhQ5fwiy1k71n9LqUlsxYmahopODk3JJnqYJWCewt9k7e3vmttuOGUx8gkcQLLBw8/lmt79b" + "Py3p/un+4uMFAUBwyG/TfHYgwNw2twAAAABJRU5ErkJggg==") + +index.append('NI') +catalog['NI'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABO0lE" + "QVQokV1RsWpUARCcvXuB3CkcwYQIKmmMrU0ES/2CNPmItPcFgRT+gpWFpSCIVcBfSJO0NldZ" + "KUEfd5683ZlJ8e7gzmFgYZllmZnA1OiwgoTCmkIJ3TaBBh0uzgBAthwUaJRMRmmYHJRcdMol" + "f76cNyjA+NGaAoWSi0g5udJluaOTfjoJ/FXz6gTH+94bQQbVn5laUpUaF91/KPlgjK9vFCQj" + "AoBtGIY9/5DzVspmPPKDc68REW3bDnq3q10/2V3NXnz/8sdMbwKQ1GyqsbqIt0c3fLZrbOnD" + "lhQv3+f0dfxceO3YJUX+prXUhELRZZM4fBjvPv5qbr/p+slwduckOvaZIDXJcpa7PlO6iOeP" + "gE9oUEoMHk9QdBIlJJGMIlIQkUYxZJcAKHC6wHxdZAn/8H+7feUpANjBPS8xYruwWMQjAAAA" + "AElFTkSuQmCC") + +index.append('NL') +catalog['NL'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAAA/UlE" + "QVQokX2OzSqFYRSF16tPUueMJAnhElyByzKQCzAyNXQN5u7lJDE4RUeU3r1+DD4OCqvd6mm3" + "9mq34Ev+yb9OC4DTcwCxYUEKDTEUyBRDpWr0l+urAQDA3D1AgpwiVCmmmKp0pnp6pXrb2zEw" + "YPMou/tYm0AOCSlUyJAfMB6T2No00BaLxXQ6BZAEQZC/1FqbzWaD7TGdZAm/Cq2RHLwM/d+f" + "tARAw+HFydnx7fy1aMqdKrmo4tJNuqSDrcnN+eWA++en1z5fvBVdcqd7qahOL5007cn6KvA4" + "oLvk7Y31sYlyyaWx1aToSJYtjS/hFOhAB/gN+G1DwJ++8g7ZJGpzVKC8RwAAAABJRU5ErkJg" + "gg==") + +index.append('NO') +catalog['NO'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABWklE" + "QVQokW2RsWpUYRSE57/32lhIltVik6yE2FiIpY3gS9hZiGAXTBGxkzyBgTxAGou0wpKnEAIq" + "QuyMK2iIRbgQ0vjPnDMWdxcsMsVwisOcM3zlJxa6a9989vXq8MG8bRNIAMAw6D/vAIx23wKw" + "vfditZQyfr3jECRLFk1ZTMrk99lRBwBw/voN4E8v25rPLYJM0qRZs1ZXttPp4gLCVgCITABW" + "NeVKs5o1SdeaZEMK6NYjSinDSwrbXvkw83Uqpdy/vCzt08/7L9fOe0ZasjIVZqTCVCqSYcmM" + "XB3dOHh1XCJiiDf85v3Zu+eTa+NtN03T93132ra3t7d0+mNldsSw7Ysnj7MySbBm5aIG1d3b" + "PDn+1CVgyqRtRtrOv8NGNenKZDWZEVAI6BKw1Ewmtu/c6gC069MmmBTEpFopIxCREQDKyRLh" + "Rt+PHn68+PLo23isJeMBMJYO4B/0nlqS8nC3FwAAAABJRU5ErkJggg==") + +index.append('NP') +catalog['NP'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAYAAAB24g05AAAABHNCSVQICAgIfAhkiAAAATdJ" + "REFUKJGVkMErg3Ecxj/vEGvZZi1pxEWtxYk4OCvJRUpOjv6M5eCyUu5ylByclDhZOCAHuSo1" + "jNW7wstqel/vfo+bNjNtn9tzeD49369FFWOpS/PS2Q1IDzejbbTKeOpcDqhgWWZg5KTUTCdQ" + "HSqmhA/E83nrdjcW6k/tl1sSGPNB1HGQ69IeDFq544lgYnjLb1rgV14pZ7MEwmECkQj5RILD" + "u5W2vsFVNRJY1SE5tKajhzQGmJ8+4N1+pOzYSC4yHm+FDeu3oL06fFVsPCCeyZD1rklulnh7" + "Xq8rNWSwb1HFdFru/b3cXE65uTmFe5cbzoe6H9hUHAd8H3yfjliMneI2oejMv5IfemOTugA9" + "LS0pv7CgM9ApaA/U1T31p6TmvlDPrKQvkIdkkAzIIAESkvA+r2o63/P7kcPE9IErAAAAAElF" + "TkSuQmCC") + +index.append('NR') +catalog['NR'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABUUlE" + "QVQokVWRMUtXARTFz9M3SBCiQfgHg3RyCFpahKQv0BC0Bq0ObU0trroJTuFnkD5E5Ozg5BAh" + "JZQRgmLvvXvPOdfh9Zc8nOHC/V24nNNg8hbEP9kIgkQQCoBA/OceYAtia+cNAMsuSJJNWTZp" + "SkmTSimpT9sfWgCoOvt9KZckalw7qVtHMqnlh/NAtDAkUEVZMumQSSc9pZVUUJkCosU9zs5w" + "FlmNClUly5JkSeT4oGTRReBv++3j58W1FzmcU4TVQGWWWU6bZdpZps12bulkUe3Kq2fvdyff" + "f86tP3l0eHx6eT1EKpMhZSookplK6vFk4cs+ZpDM9MbT1Xevn29vvpSbrmeX1Q/uw/2gbnAX" + "7sJBAGiBIHV0cvbrz9XXH+d9P+Q0RJKUSFGSbWs86JOuBnsHhxdX3dKD+5STnqKmxBGXADTA" + "+t06x0Zv58Jd3QAELXK+OQKNnAAAAABJRU5ErkJggg==") + +index.append('NZ') +catalog['NZ'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAAB2ElE" + "QVQokU2RPWtTYRiG75ycJD2HNM1Jk8YQa8FGMQGx/QEWwUmloOhoR3FwEFcdpIO7Y6GI2ILo" + "IipiBRUsOuhQ6JdFB/tB09CYxMamyUne53ne1yEg3W4urmu6A5/vTL7bTz54tHw/tRq2rHvl" + "IzeG9MVzJ688KQEtgAEG2oACFGAHTqdvzT2f4GS6vfYdfguZbCyVeLNQXVzfJ9LM+rxb/tDo" + "b/hMbF4/nrRnrBfB1ULkby20Vzd+O+TF26TP+upVbdQnM+4Uc83fdT8+tZMczMYBtl66I5TL" + "h4dzFEvoo0M9I2cOEpml0LEOG0XyrJb6VO+bKg1MeEUjAiirOnY5EI093XC2At4WuTP1QTuf" + "r4djbSV3vbWkg9nKwDV384SpXYgUARU81fB2ndSX5T++31mv0sdvOyu7kuqRxTKPO9sFU5nb" + "8xYabr9uvjXD2z/e27O/7NvBOMfwlXuZhPr4Z4VWSqLI3NwsKGIiViTTfva4I4CyACbSSjST" + "EIlSukNGke56RFqRXI9uMImwBpQNMLOQEmLpSoqElKhuz/wwveTpZqe3My+j3aBFbDLpKJEw" + "C7MmFmJhEmLNLNOhsUuR4rxOixhAAsBV4ODQlwrgQ/s/YUAD7j/4kDqsVjcKdgAAAABJRU5E" + "rkJggg==") + +index.append('OM') +catalog['OM'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABGklE" + "QVQokXWNPUpDARCE58WngvjTCYIQiB7AVtBjeAA9g6RO52ls08YcwNzAJkUsTRrBndlZixf/" + "UL9iZpadZZsJfnD2sgJsG539ogVwuVy+zmY75+fPo9HBwV4VgKpP+8ZisWgB5Gq1e3Hx9vQU" + "8/mfvY6maST1AGwcHi7H463BwOR/7Q7bzQQ4HQ7f5vOSikqxSJMOFsMRJivC5Ha//zCdtgZK" + "WWRJpiAiAiTIiqhOuyABaAVUqiQzTPXIYiDYRDRca3WZ7K0PSEdYqoibW0YGk/Q6hIOmUif7" + "erxDC8CSSZNFRsZXLxlJOmTKqUx4/UGbR0dFWnm8Q5pMyqIpS6XMTGciITT3gAABBgRcXwEB" + "CHgFDPhj7NbAO7zseXjgtt+GAAAAAElFTkSuQmCC") + +index.append('PA') +catalog['PA'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABTUlE" + "QVQokVWQvUrcURTE517/KQJxm4WFCGJhZcDWVhAsrO3zDLaxTC1i6UMEBXshPoGVbBOIhVoo" + "BnfV4J6PGYvrLjocDlP8zmcZjUZ4JxIASaLfJ9AipoZAB6DX6wH4918k+vOQAMh2dvj8LHdF" + "zPLdyUnXGt+M9es8nyb6vlYXekVS/r3k41jmdJOZ3OviYrQJkr7OY3UBT5PSaEmNo5vMWwHd" + "31ZqWl+uEDTTaEw3msldZnSfcw+g+7bf/dyK20dlKqhIBIWI3Y2N6lYilKmI6l4Gg+7srLv+" + "w5sHXT3IU0F5ylMe5dPuD31UrTW2tyuMQUw5eehlIk/OODs8tIODdirJCpvSKUt5KkWPNzqH" + "Qz899eOjvLiQFBEdgkkNvpRIOBGpYIkspRQAcysrn/f2QGJpqf2mYPMeRrwQFjBOPYe/LSJI" + "kgQZZCt4BT95XQotDMJeAAAAAElFTkSuQmCC") + +index.append('PE') +catalog['PE'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAAAz0lE" + "QVQokYWRzW2CMRBEn+HrAXHgAH3QCZ3QFY1QANdENJBcvLM7OfgLf0LJaA+WPE8e7zRzVwHX" + "K1VA3bTdFtxmAjgeAVe1KlYrG3Czl7CwfTg4wpIjvk6nCQD745NMMj3sj7pc3Lsj2mYzv+Dy" + "7I54ddvufQBEFEwFLRPJEqFX9zPA/AflSPkPIP1GSlly9L+Bdo8U4d6R3Pt7IGJk1gCW0ojo" + "iPeAAiWZDGARYr12BMrRYGvtXuduN3ZIZkH7fmixgPN5LliaD/v9fAXADwI9YYS3Muu2AAAA" + "AElFTkSuQmCC") + +index.append('PH') +catalog['PH'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABcUlE" + "QVQokWWRsWuTARTEL18/tfhZyVAzNUsVhLagiODYf6CLdnXt6tS9Q/8BoUJEBQcRK6QgEdrZ" + "qRShOjlUsIouLomgJua9e+8cvipBj+O44XfTNXZ2OqurQ+AHACCBBAgQSMAAAjZZGtKNfv9R" + "r3fm6GiQqQiRGZGkyKSnM+jpTPfo9bYao9HX6elWhHZ3v3S774dDi4C73NM93MI8zNI92u1q" + "b+9uAZyXhkXxYWXl1Pr6Qrs9Yyb3cKOd0GFGM7onYEVmAp+BjtRZXJja2Li+tDRjFmNLsxiP" + "60GYRT0oge/AJ+mjdBY4rqqrm5vb3a3Fw+cDWtDpFjTS2eKFV3hdApU0J10SWtBFjcZ6Mn9r" + "+/FNM7lrIgufvwOUmZDmpNtAU++Ode+h3hzK/R8apEgCZeZPYFa6jBcv9eC+vg0muZNCBwMR" + "AMrMa+o/w9O3OthHs4lzFdwb7iBRJ4mI2gmUp7tXfq0t55+T/779v2v9BjchVftPWcpLAAAA" + "AElFTkSuQmCC") + +index.append('PK') +catalog['PK'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABjUlE" + "QVQokWVRzUpbYRScm3u9kCYRaRclTQQLmiLd2K0V6gP4COILuK3bZunaTelbKFkpuHNV6M/W" + "RcXUxEqlVGKSJt+cn6+Lq1ToWRzOYmY4M5MMwgAAgBFHjfcNjAAHFFDAAQIK8P7IkAGYzWcB" + "5GneftMey1hh5qqmClUTcVUTMRWXzodOVsjHGN29N+7dhltzi4iNSvP7zdnEJhTSKS7z1XkQ" + "pQJdbHU1Nyq3X26vPX09CZMsyXZWdurlOpXiAkUJ9xMR1ZXK1fpqa6512DsKHpqV5vLj5c0X" + "m1SKCYjM3Qv5GKOYiEhtplZOyz9GlzR+vv6y+2n3YnjBSI0KouR+hy4IjLz+8zNP863WFgMZ" + "wkn/pDvsiouYQJHB/QGBdB52j1aevNp4vjEIgyGHC7WFg28Hx/1jc4Uic/x7iS40MrL98V1/" + "2F9/tl7Nq3tf905/n05tqmZ3HoqUkiSpV+qVtCJRxKRz1tk/3ze3ICGdSRfnFi0aFMnVzZW7" + "u/tUp0tvl/Drv4IfFv8IfwGHykCMTQsl5QAAAABJRU5ErkJggg==") + +index.append('PL') +catalog['PL'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAAAuklE" + "QVQokY2NMW5CQQxEn2FresSduQM3g+b3oKTZ8XpSbPLFD0TKaGzZ8hs5fL/zo1qrqqDeqQEc" + "DoDtAMAGHPYedt7oY1ka35CfB79TRFRm+yc9T1XVCmKljf/kDVAVA+J89rI4k0yvlqx0ypIl" + "Z8bpdL9cGuDbzder1dHkurvcu7Xp8XgUtIKQ3DuZG2hLz+c5A/tMS6zQayxFDsZgBnZKjkdL" + "5CCFFBKZzJ7JGNMF8Qn1ZLbrrxPwBcJ/XXnBf5aSAAAAAElFTkSuQmCC") + +index.append('PR') +catalog['PR'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABi0lE" + "QVQokW2Rz0uUYRSFz/u9r+OPmcGNiZBaSASlYIjmvv6B/oAgglaCUNuGFq0V3LVt0cY2sxI1" + "ClxMiouQoYU0LgqGmoWIheA38333vve4GDUXHp7F3Vx4DscBjQ94MIV2B8gBu4ICCuDiUCAB" + "AnC3WTkaQm0q2e1xmWSRalSxGKnaxURMhKq71aoD+KaC5m/eLh2+qgyUh3ppJEASvEiSkITZ" + "34ODBIAay/1oHA8vvi42/4SYBHgP73kJQADeWwgOiCsr7vEjbH3hXh1Hx3w2Vp04qZXlkJKd" + "y4iYSGF0dGd1NQB4OIt791kcYO0rTPjux5OnsTGTffN5uyNieW55biJ9ZAsIAD5tMEaurzE9" + "RTvni1sfZ0v/vE5TpVfEur1j7BkZGd/edoC8XHCNX1RFXz+Xljg2zuR/3/MAcM619vcDYB31" + "IrxTbr1dLg3eLJKk0VRJ0oxA9905hzR1QHvxeTavG3P4XMBplqp1NS5lVKnRLCLGzXo9AD+H" + "30/eAL4D6ZWB8+v2LgBnnkcsD9MsvJUAAAAASUVORK5CYII=") + +index.append('PT') +catalog['PT'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABXklE" + "QVQokW2RMWuTYRSFz/fmbVIrpUI62CGCgyLipP4Dndw6unTrIJ2cM3YpiL9A3ARnCwr+AUH0" + "D+igS52iaYmJ6XfPufc6JKFLDw+XM9yHO9wGT4Eplgmgwt8t66VUTDDcHwKIiMjwxnHT4Wqk" + "jlTIlHI1p8fHFQVAnpz+2uj4/b7f61O7rhG7b5mnTGPS0izJZjAIoAKISA9//tC3WnXfOOfM" + "x5zuceOFJS2NCwFkAAUBT5d0NuP6e3UPDifPjuzLdtOjYNla2goSQIFB7nRWcD6ztlw5u367" + "LWtRWq5ZkheCFECFwUMKfR3ZrUd+59Vw++qN+e43S+uPLduVQzZkABUCneb28Yf6dzl5cnLt" + "z8/ud3Y+WZldbENKSYsLCtH5W3z5mb1N+/Da1GMZW5JLQYQc7gAq/oHSztYORbkriQErmZsC" + "CQkS3BcE0OABMAbOAQPOgYLR38t/vMh/jtdGdgUCTU8AAAAASUVORK5CYII=") + +index.append('PY') +catalog['PY'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABH0lE" + "QVQokW1SO0oEQRSsnhkHNRHBQITdC3gMUwPxOG5gYGLoLUQQg8UbGIqBV/CDLKz/XdTpelUG" + "4/7A4lFUP+oV3fRLxgxa1P9WMoBeD4AlSIgwA0GTIJ2z53jU71cAAPvhERGIcM4t/1WTnRs3" + "jXNOnY6ACuOx6xoSANvwFJgdJNsoCg2H1ZvqtbJEWf5ZYN3d/VxeKOd6dx/d7iwgpWFTF+1L" + "p6G2Pfo43X45HO3489XzAEgVkhbcNlaW96588H2G5ZUFvw0pPb9/rdZLIbUTmlwfVnjSkGVU" + "ZfHwNEhYvz063rofZFKZIJVpUnkmTCno7mZ9fnJdoRHDG+tFsGCYUZCKSIySFMMRoqAwLQAJ" + "5Q2CgIB5bgXnvpjtBvwCb+9W8SwiBKMAAAAASUVORK5CYII=") + +index.append('QA') +catalog['QA'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABJElE" + "QVQokX2QzyqFURTF1719GdxkYKSQl/AulJSMTJSBKKLIQClMjQwMmHgPI15BZpJ/6Za719rr" + "GJx7P0ZWuzM4+7fXWft0Bv2+MZTh1anJ+y8KCOB7dBK/6gz6/bFeD0AppV5d7W5n0pmpTDIl" + "kSlZ3Lq+bap9S98c7r88PaYkSowkRYohcnJ6BkD3L217Ye9AqkSIJEMMRjDCIoCmDVfHrna2" + "klSEqNZbESKTAtC1Xek6sHR0XNviQBGMIc0IkQAa22WIl+JyublBhuI3ukgFTaYIoOvWvqCU" + "snJypggxFLU4epCZAtBgFKlucrG+pmijU6REk0qlDKDz+f7aG59odyilnC4vpijVv8+UnMpM" + "Z57fPTS2P97ebNuWNT8794z/9ANrfVvlnss+EwAAAABJRU5ErkJggg==") + +index.append('RO') +catalog['RO'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABFElE" + "QVQokV2RMUpdYRCFv3vzP0l6myAW2roAF5LCJgsJ2GVJD1IG7FyEnQgWgRiCqG/mzJwU9waf" + "Dqf95nxwJs6u+SsAZhgPV+cfD6DpppvxQp/RvGbwW5ffT4DuqT1/Ovq22exAtkB+TH+VMy05" + "83G7HQwwd7+y2tVz9y0827lG6ZtwhDOn4+OGAbSptsoq27LDlp12uMORC0Bmw0BdbVVLqGzH" + "63uH/QYABtEqp/wfyD0m7HSsSki9AFUtOWQVe0DYua80rUrRWYRacopV411DJpIlwWAnqTM7" + "5axpD1ga0hFWoqIKGKCUPx9uUlZ9mOcjeJqmBEEyxKmoWtIwwQ8I0LLj/c8vY9CioUX/oS/W" + "jZf7Byetb52vN2aSAAAAAElFTkSuQmCC") + +index.append('RS') +catalog['RS'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABEAAAALCAYAAACZIGYHAAAABHNCSVQICAgIfAhkiAAAASdJ" + "REFUKJGdkb1Kw2AUht8kX0OamDRFq8FBDUVRyeLg4DWIo5OLgjfhZYiDm65ehZPwIUKnBhws" + "VSzaFAtJJTZpk3wuGjSlTfUZ38N5OD/cyeo6M2UF/6X54YOYsgJLK/0qhCTBkESYCcSpRCQb" + "POhAuNIDX+QgNCSsdZJ8yVWk1meH+vt3sLtUNefL3TI4wDX03nmn1QyGUTxO0I0SlVwbOxbm" + "qml4YLrwXj0w8NDMhcpdf6Ny73Pjx3hrgM9mvq6DC0UocREDYzF3FQCjEt5powBAlCTELy0E" + "yYQpviD7m2LdWJbSm/DPtW1fkEmfCVAeabJnHd9OErSfRBWUUvaTgeux2sUluzk9Y4HjsDwo" + "pWzkxYWShq2jw6luka5j2/afGrLYto1PPjGOVBw0sOsAAAAASUVORK5CYII=") + +index.append('RU') +catalog['RU'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAAA8klE" + "QVQokW2PPU7CURDEfw/+FXY0hljQ2Fh4DRNuQss9PIVXoOAuFhYUho6YSEzezNu1eEQ+ZLLZ" + "nd3Zj2w5HJITIqKHzuIKDgZgMgHITChAZs8KjDNHeYbd7nvomzPznOQtlFIioiwWuVzmfk8E" + "rWVrRGRraV94O6fTslp9DZtNPD+X7RY7LeSUUspar/18DjAANlL+DXS591Wl6rFiF4gB6Eer" + "0uLU+u+ClBDDmvGTXn/qZ9hZHXJUhRzSkVQ3Kdzu/LDmbXiBsd9DH7eXd2LhVtrjPQwBI5nZ" + "LCXcsJCK1D/Dxqa1bgHlAHFmXKZXEvALP4llHm/RUYYAAAAASUVORK5CYII=") + +index.append('RW') +catalog['RW'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABRUlE" + "QVQokW2MMYqUURCE673/zYKyKshssJHgZQQzgz2BCAZiZCKCgeCaGegBzPQQIngNMRFBHFc3" + "GXUcu6urDH4GdsGmKaqgvmqXnjqE+SgVAQEEKEgIgULsjDBCeH4DAGyXmwwJJZdbaUp1i0fL" + "F83x+uu945ca8/bJb5chgXIVaLNMeZu+c/hkPz50bY6WcYz7XYDlEkqgzELaWWY5y5TfrO7+" + "Yf8WB29PbyE0MkS3KJQ8MyVXebtjvsTBo4+vKC/3J6zX7edfLKaH1meDdtq0w0447ZRjjjZb" + "v7b6/m5cHAA27r+MgGcm/qu9X728hyGhtbQD4PnSuTZAmyTG4jEe3OSndaYyK7MiFLPJSjqz" + "gpWpun6l3j/DwAlOt7wwDody0Yo99yqzJztzShY5sVRV9WNTWKHhNrAF4szzjAagXRQw8A/4" + "uWjB1/pT4AAAAABJRU5ErkJggg==") + +index.append('SA') +catalog['SA'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABc0lE" + "QVQokX2RvWpUcRTE535ks5vAInGLgDFGQmqtVRRsfYtUfjR5BH2G1IKd0bxAKpsQLFQEhVsI" + "G7tV2BXW5CZ77zkz529horFximGaHwMzGR7grwIgQMDO3YDmQmhQAnj28GkCEoKiQhHBIEVK" + "DHe6Qgq5/PWj3RJAAkY/RxGRIwfSaXtqtLmy47SZNQnppK3r5nhl6SqAEoGUUsvm1vU765c3" + "jpujd1/f31y90ck7RZYfDA/ubtz7PPr04u1zp58BDFJy+tFs2p3rrVy60it6h+PDlu10Nv1R" + "TwoUJrocQA6DQpQPFgdrg7VxPamtrr5X3c589a3qFgvD8XCv2pNI8axBwZSw+/FVljIGTWZu" + "JBu2eZZPTiakp0jngIGiy83bzdubC+ViSunPVv35vsuf7Dxu1TIuAnQGt99sMxQKkzndZFv3" + "t/a/7Lv899AASjTw4HJ/meFOUu5yih5OcefDS1GrS9cYjAgAGdb/ORIt/q9fMHo3F1zEQB0A" + "AAAASUVORK5CYII=") + +index.append('SB') +catalog['SB'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABz0lE" + "QVQokYWNQUiTYRzGn7Xv+0gl3MKCCS0LVgdDSQyExU4WEVGHygiCWF285KkI8VKwaJcOUQQd" + "WjQIpnjx0mjhoGIOYY7ARtGhSQh90Sq3nM73+b/v12F17nd4Ts/ze3x+/1Ii0Ts5+XFq6kAy" + "6WrdAAwAQADzLzuB1fsX4kOjsFKp8NjYTtvGxMTugYHtpVKThNZGxBMxQh/FOhldiA298zqu" + "Jh4/8QFvU6mDw8Nd6fRP11WtltHaIw3pkbYfmzcvPz/UX1xZt2+X7Oz1rAVY9boJBv2iTXf3" + "tmZTlPJEfFTO6dibc8fm0VnLVnF38Ut4VxiAFY0Gxsd74leqT1N9s7NrmUyNdGBaNy5NHx4s" + "fGjItRfulih6pCEAq1Co5fMhKszM/Jqba5AdJ0YWzh9/1XSquVV/ctGlUcooCkUEgAWoXG5N" + "xMtM/x6MfD51tBjZt1Td2LpX2KjU1pWnqKlE0ZCa7YEhoZQTP/Oyv2/ZBMqvv+24U3Rp2Ba3" + "2yKi9d8Ha3SkEowtR/YWv0OevXfyK1/pUem2mxQlhqK1GA3AunjkVihwtnd/5VM99Kj8Qzx7" + "T08XNUULhWJEjOg2RgPwFR/AdfCwjPk0sIn/8gcg3yvgjsU2IAAAAABJRU5ErkJggg==") + +index.append('SD') +catalog['SD'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABOUlE" + "QVQokZWPsUpCcRSHf957CVP0Sks4uPkALY1Ba20+QS9QW0NEUwVBDbX2BC1NQmtQb+ATOOiQ" + "3SI1Rfyf3zmnQTODlj4+vukHh4PqHhxzdUkBpsAEGAOfwAD4AN6BHK5xs3G5/ziIaDCDqlOh" + "dBKki/hSR81mAiKr9E82J+f3WSTqIlB1kblBXIKH4CK5Ws2ABIQF71ayw0Z6tX0Wec7d/Adb" + "BHFs7XYEQF0Z2C31Dp6OOytjL5eXTL2cepp6mnqp9JLPRyBoKioi0kk6Jw9H+uvCEoCZRSDU" + "SGOwsK7rpzunOcffe3czS0DM/iq+FS92L6Jp1J/0zczdZ10Qx/FwOExgoLPQK2R3WeO2EUII" + "IYjIvCISAimk1uv1VquVYIT862rhuVBdq7KspMwgOStJ/QYAsIV/8QVvRk9166paHAAAAABJ" + "RU5ErkJggg==") + +index.append('SE') +catalog['SE'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABTElE" + "QVQokU2RP2rUcRTE57v+NEVEQVEIEgRzAEsrKwuxEe9gERC2Mm4jKBaWNtp6hTSCpUUO4BkC" + "IZ2KqLBs3ps/FmZNhscwxXxeMwMP3mAtvXoNA8aFxQuUUYUiVnUWuiYAL+ePANgZO0tYgeZ7" + "j6lQptwS5bZb+jT/MAEAcvxzKQcnR0kjOvz2p+WmWiqq5G5tX98EaoJtQw5PLv5rxzWN1ZKz" + "kpsqqqi2WgZq6ADjzgJ1lBBh3HEnHXdccZ2G9Ni4/fvLwQQCUdLxGkidYmftihumiQlGwphJ" + "weeBc56Ke7hNDNx9/mzx5PD7L8qfH75LOq57+3tNltyrWdEtk9q5eeXr2/cTVqTVUtPrZ1Vk" + "US0XqiEOc2bONoDjCVVNb13dbAqXbiEN9/a1y5SbpkVHsmTZgAe2nv4f8sfHfRAmbuzeBwjU" + "+gwQMIC/3Ll5iJVVWFoAAAAASUVORK5CYII=") + +index.append('SG') +catalog['SG'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABJ0lE" + "QVQokY2SPUpDURCFz73vQjQknY34k9IiriArcD3au5asIVWKYNIpaRRMHwKCiCikkTgz7xyL" + "p4KK4sd0881p5qQHgEDZ3oZZ1DUBAgD4YwIAUAjszOfV8bHNZry9zf0+1+u4ulKEIuDOCLkr" + "Qu53o1EBUPX7fnkZk0k5OdkMh/nwMO3t+/RC7jKXG83kXh0cBFAAqK6rXm9zf8/xeOvsTBEv" + "5+cyp5vcZC4zudOdQHUKtAaDfHQEKa5vfDZ7HY+bbLo1qszonrvdx9WqFKBeLGw6FRI6HUQr" + "t9uMgHt2b4IRkTzS7m4Bkq/XudORJAiCfifn/LRcFgJJkgT8ZTfbAArJCvjPAQBEFJKfNj5I" + "KeELCVDzzQJy8/zMd4LBb4AMEmTTgDfsYE4MubWOHgAAAABJRU5ErkJggg==") + +index.append('SI') +catalog['SI'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABRklE" + "QVQokW2MsWpUYRCFz3/vD8oWgaBgFjux1EotrXwFCxEfQ7BTLAJ2Kgoi2KhFejtbfQMbG7GJ" + "RViFhM29uvznzBmLu4ZV/Bi+OTMDU8ZxxAa2J2+GE2RXALPZLBOrVaDk7HQPAMjMXLcNhmGo" + "0+PHz/cPFoe9Y/fhZXRd/o9Siu3Sb3988ujC8bFu3zzz6u1iGMIJyaKplEyZtOSdc6dePv1Q" + "43DY/7a6f+1LbF2/d+nzrdc7RE+50WxuNOnWTMbRksCPCliJN3uLs+/2xGCeb5lUUtmUbQqR" + "FBQAXIEa7t73V9uRRTOTdFPXAgy0KGsbdAfUuos7N8YHy+WBpWgy/5iMppDcZCkYW9vzZ3hR" + "7+J7/+uTf34FmWS2lq2tw8lGhKKsLl4BqoGOwnyeJBQQQRYSEiZLiJjKQBkBbxT+Hv85AfgN" + "XohcMYYB/8YAAAAASUVORK5CYII=") + +index.append('SK') +catalog['SK'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABgElE" + "QVQokWWOv0uVcRTGn+97X296jSxEXByi/yIhIlFq6A+owaBwyhwa6kLR1NbUINRUQ6ijRDY0" + "iAVN/djMKMJFb/Tbe015r/ec8zwNr0jWh8PheXg4DyfFdgEQe5AEQJBkabiHk8gBZLUaAEnl" + "SSYASlIF0H62vv3Iy/ri4SNsbkmUB8Duq1P6j5SSu+cjE+tTFwZOr3zs3Wjo5i3Mzca799MP" + "Gm1WwhlBD7nTg/2H8/qVlfzlq+bYSP9Yu63Lk+g7pPHxuH7j53rrQ7Mb7ua0kBs7zqHBKuJL" + "Fg5RDOr+tFqbmpvJMv3uZE5ZwANuMocHggCQA1WJa+cmFxY+N+4V2Dlz7fyJr897LBiUMZky" + "F51pRxWgmpaAY/U7XX214uTo7cdd9bNeW3zaevGa5jSj2e5P5geODi0tzuTDQGXjE9+u4tfq" + "3eOn0rM31SfzA2bqdGSmUrjBIx1sHgFSAeQXL8lNZvDYFWZyV7ndFYEIRHxfXk7bAP8a7Lf/" + "RAD+AEvfR7lINIfTAAAAAElFTkSuQmCC") + +index.append('SN') +catalog['SN'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABK0lE" + "QVQokVWRMWqWYRCEnzf5QUEEGzERYpEulZ12dhZ2HsBCsPQIqXIDbyKk8SJ2goVoJdgYFXdm" + "dyy+X4PDtM/ssLO44FqDXnJwAMNsNnPGcO0dcP7sHJjMZNaDhgavXz4cH9xUXjhS7EhXl5c7" + "APL5x5dO9zQRqxPlhhJllA+Vqkjr5GRgxzCTnnbao9CkEne0UkmltAFIe6Bpx257nOhKevtR" + "v1vPT+vO4X8AG+BptTz2mOj9t3r3Sd+l+7fr6T2l9pWwB3aYHntcU24nenS3Hh/pZ9WTI2Wu" + "L6x9JaOoujxWVxCp12dK1KmM/sXH9ga4rZZGaiVFKlFSK0qUqli46QZ2FBof3zpWy9tb0VoC" + "g1jm1HRvHli8ggJDQfH1DfB35mGKebjfeNMf/ixbkUHqougAAAAASUVORK5CYII=") + +index.append('SV') +catalog['SV'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABPElE" + "QVQokVWRv2qUURDFz3U/99P0EVaICImkSCEBU/peopAHSJM2PolFioAQQhJSbyE2FiGoCC6G" + "yL0zv2Nxs0schuEwzL9zpuj1XFX3BqooUJUqqqGKGhJS9Dio6vDDs14MAiKVOCFTEY50BB0c" + "7c8HQaKbnwHOdKQjnekWjqCFW7gFrTFbfyzVcnJ5u7kxrRVbtukORrbAYAx4nD76fP69ZGYp" + "RZJtWZZt//qyf8v4fOudH1gpZbFYDJ1tT3VwNb87//ojaG/u/uztrK0aVAowrKpX83e3p4v2" + "Nsm9naf/bbCBcnz2+9WL8W8FbLsfHU6QLHBngP1knByfXheNl+8PNr7d1KUm1HCmW6O17CpF" + "EumXs+mnjxeDwNZsfWjhDCId4ZZEEjHJvNcanEiiaHKq1OqRyxhLzAOApH+kWGxNzYWtjQAA" + "AABJRU5ErkJggg==") + +index.append('SY') +catalog['SY'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABDElE" + "QVQokZ1OPUsDQRScDdeG1ClSqIe/R0UQ0Tp/Ij8kpWClhxBIGUF/wxVWOU4iEW0i3BEM2Xn7" + "nsUmp0KwcHgM8z5md5zhG/pb7yxnAAYDAKYKVYRgEhDERCBipP3g5XicAADM5q8IASEYGXlT" + "nkZv3hvpej0Fkrqq2u02Nj6LvBNw7mM2azWhzWyl66y8WxnjhbdwNb2uZdl4VLWlqs2ro+fR" + "4/whm97ENitu718mw6fh9guISKJNBtjJ3lG9rk4PjuPgfP/s/fPt8vAits4MgFssFp1O5+/0" + "ceucK4rCAej3+2VZkiTpvffeR0EyKhGKhDRN8zxPAJDS7XZJigSRaKSIRBaRsAX+gS89B00d" + "BrzDKwAAAABJRU5ErkJggg==") + +index.append('SZ') +catalog['SZ'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABxklE" + "QVQokSXRTUuUYRQG4Pt955nJmqIsQkqaTQ3SoqDAZRCmNgW2axO0rgja6KLCQZAKatN3qwrU" + "RVFEURFE0spdJPRlgUGBo4uQ8GvGd85zzn1adP2FKzl+/UMjUwAASCohpCpVKKqizFRFVJSZ" + "aEtIA8PS6UoZABx0p7u7m9HUCNBhpNGNNLJ/dCKMFnoL+Zu++sdJuDnNqU5zVzC6qTO6RVLT" + "tW094W64dcp6Lswuz8y4WfD4u2lvHFFlbOThyrcvn+/cWPw1a1GyZmwtle6PIyUQzZBouWvd" + "5oPF16QmabMpU9M/Wzp27xwc6jixvXPkwQ+RGOMCkCqQwEqVbaubtg6/mu/rO3rsSHeMsV6v" + "D5yvvhh/X+w8O/3y4uEz+0ORAgQFgHQiOyS51m6ZfPzoiZplWRZCqA4N1iY/fRyofj9wsrGh" + "vHfhsgIhBUxty/N76lgjzUqM70QWYswbk6mvc9eu/K3NtY1d0jSXtLcDSBavotA1zOWaW3SP" + "MCOjWQTNTUAh1U3dGDbuqN1+FnobT8/JHuZdcySpOXc6SZHIHOhudKPT6eLVbFeyr//t/FL2" + "PzITFaGrQhSioEIIIVRBQon14R8k7mFOvA7V6wAAAABJRU5ErkJggg==") + +index.append('TH') +catalog['TH'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABDklE" + "QVQokX1RsUpDURTLtQ+KVB2qi1MXdXJx0N3BjxEcqz/gz3QVuklxL35F9zrUCvaeJMfhQWuh" + "GkIIZwjhpCQ28LbfyZIAnp8BpAUnyHSCkRLIJDMiI5JMcjkalcwE0Ora5C6UUhaLRbm9e3m4" + "v/z4WMlppWzRVIqiLFoyw7SP+/tPj+Pm7fX9+upkNvskzXDQEYpQrY5QrVrrYHAEjBvAbU8y" + "GRnMWtvartU1MqrbCwnADfAtkYwaZmgd/zu7VkU4ogLLpnd4cXbePzjskkkqaNIMkQ6aFGnS" + "kk9Pe9PpTVmtVp1Ox/Zfz2kBoJQyn88bd7t7wyHajhKCEEsEyA1tSJA8mZSv7SH/2bjFD1//" + "ZYwkMQleAAAAAElFTkSuQmCC") + +index.append('TJ') +catalog['TJ'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABO0lE" + "QVQokY2PvU5bURCE51ybIEuWDBXix0kKXPEE9DwLBQUdEpKDkoYiVXgWXiBFmrT0kS1HuKHC" + "hRE6M7tDceGGMp9Wq1lpNNopP/GPBBIAIEBvZ6db0QcwmU4BONOZiLAiQ5YsmUzJpKUkf93e" + "9gEArvdLRyAiyYwwmaTJrExW15rkh/FYQFmv14PBoH3JNoD651szPDJ6sfrd+zx1Gdi2XUpZ" + "LpdNV8A2DC5umq1jx5P12Ds4i783foekJjNbt20ufvTG52X7xJZT3tgrHy9j9vXVDmRmWa1W" + "w+HQbb5hOxbf/TRzypv75dNVF980zXw+73fxnSjjC9jtdHQNCyY4/XI6e5hRZLCqVlWKNSrF" + "KjKqgoo43Dm8u77r4xkM7Y52GVSEggxSVKrdCkVGREQGAGAHGAGb+E9eACEOOiSM812rAAAA" + "AElFTkSuQmCC") + +index.append('TM') +catalog['TM'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABj0lE" + "QVQokYWOP2tTcRiFTy733tyk9s8krThoFwdxKEjFyVFwlI4uIvoFghSku6M6FvoNnARB0MXB" + "qbQOTQsabdJgNFiqJiY1ue857+86JLj6DGc7PE8J1wHgzc+l3sfuELi7Aggzq1dO39ZhwBjT" + "zTEhBrBRe7LczYtQTheqD9PeTLnya9i7XdvabR3sdxpyMZByOl/UNiMAKPD76GuUt4933i9U" + "zuy1P8xXZx+/3FxbvZnLPn0/anTbjW9NugBECAhFSNKi3x2lSdhpHdy7sbZ9uLd+60FUiipJ" + "2USjmZMigBgBCj4e8HSUZ6VCjtf1d0/vbND9+far1knHSBMpUgIQwxCCJ4n/GYbZajCp3mnc" + "33p0aXG5efLluP+DookmTZJiCAo+Glgz41yfFMzNpN32PklzUjKRkpxTg+Rp4pkxqxYmmwSY" + "i5oeKNLlwQFEGINBcyvXrqbILly2aQBNZjKjJj1yyf2fQYPDRnz2ovW655YWJdFJp1zyIJcH" + "d3cFAShhHhjjWY7zwGdgHf/hLwvQQSbnuK52AAAAAElFTkSuQmCC") + +index.append('TN') +catalog['TN'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABN0lE" + "QVQokX2RsUpDYQyFz61dBUEoFFsHp452cfIB1KFbp4Kzo25C36GrTr6C4uAktljhLrp07aq7" + "UF1ukv9z+ItaEEMI4ZCPhJwC/URa7f/MAknDoSRSUkqKwEPhuMsdM37Vj9vbuiQJXt8UoQhO" + "TrS9zcYGwHRKWTIeU1WYFe12kuqSSCynj4+1WHB5yXxOu02vR2uL7i7TJ8xklqRakhSRtysl" + "Hh/pdLi4YHOTsuTgEHOqKi+RVJMkD8wwI4L5nKMjRiMmE7pd1tcxWwLuKQOE445VpESrxc01" + "p6cMBpQl7+/fwM9JWZI57uzv8/xCv894zN4es1n+j9xxd6mepLUsmTEacX7O2RmNBsDdHff3" + "PDzgJg9FKAM1czWbmMlDV1cyK8zkrlx3dvIPFZGk4nPVyH88zvEFZ5M7foT/uzgAAAAASUVO" + "RK5CYII=") + +index.append('TO') +catalog['TO'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAAA9klE" + "QVQokXVSQW4CMRBzlj1Wizhy4MiN5/ANpLZ8gfYX9AEceQ8S7RNWooeOnbiHLGhXaiPLUhTb" + "cTRJfd9jtEoBULrFogB/ogXQdR0uF59O2G6xXtvAbpekmdSQlnzn2/nc1mAfDsgZb+/+OCIl" + "X68mTTpohiNMptWqAA0A29jv0bZ+ea7bQRph/jhiADlUAoDNxscjDMO2HTHOrgwSQPs0n+N1" + "769PS6Cs4fihe8RDGm5wliUzQJk1mOPs4Q21UgES6QhIE9FUDcmSqmEm1YrjAhObCGXkjGpo" + "KCyXJqEMEWQiIaGyhJwrCpC+p4PEPwMu96/wC/4dTt9th7WGAAAAAElFTkSuQmCC") + +index.append('TR') +catalog['TR'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABOUlE" + "QVQokX2RPUpDURCFz4uxUwOxsrAQwcbaLCBoOhcgdi7BBVgHN2CvRSwsJFWqlIFg5xJiVqB5" + "oHdm7mdxH2hAPAzDFPMxP6dCP8rr9Z9RIenmRhHkrJwVgYfCcZc7ZvzKq/G4LUlVxXKpCEUw" + "GChnJhPe3zEjGZZICbNqfz9LbUnk3HRfXurkBDMODxkOSQlLJCuAzLLUypIi5M7Bgfp97u64" + "uuL5mbpmNGJrGzNSM0RSS5I+vzDj9IwIZjNS4uWFx0e6XZ6e6PUawD0XgHDcWb4hcXyMJY6O" + "eH1lOGSxYDotQFmpnaXKjJT08MDODtfX1DXAxQWrFff3zTLuuHsBNtzLTdzecn7O5iajER8f" + "mDUHuMlDESpAC7S3h5k8NJ/LrNrdVacjM7nLvfxQEVmq6nUj//G46BtdQD5PmN5peQAAAABJ" + "RU5ErkJggg==") + +index.append('TT') +catalog['TT'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAAB0UlE" + "QVQokU2Lv0sbYRyHP5fDJZ5kDBmyBFzbimBLoA2lBYO0HToINzgoFRGU0C4Oaav/gf9CV1c3" + "iTg4abgm9jh7JO8ld8lLIiS9q43mh/cm77dTSz48PPAMH0y63UQiAQBAPB6ftFoTXZ8AAngA" + "hkAfuAP+AL8BH1Bofp6urr4dHTHGpJSpVOqDruPsjE5OMB6TEDTl++Nj9SAIlEzmSTb73TSr" + "1SpjzHacF6urdHtL5+fU/UXdLnU61OkomjZyHPUAoGQSvbu0rtu1Wrvd5pxbjL3c2CApqVyi" + "UFAYkhCYmxvV6+pXQEmncXkJoudra9eMcc4552al8npri6Qkw/h/eGg01H1AefqMgoAsC0SZ" + "9XWzUmk2m41Go/TTzu7ukpRULJIQiMVGnKv7AJaWKAhIhPhhgujV5mbZtj3P8zzPsKw3nz6S" + "JLq4UGKxYaulfgGwuEi+j/GYwpDKZSJa3t4u2bbruq7rGtb1u709AKhW729u1M9AZGGBfB9C" + "kBAUhmQYBKzkcoZl1er1muMUTfN9Po9IZFgoqHkg8ugxZqMUjWJWg6ZB0xTHgaK8zeXavj8Y" + "DHq9nsnY8s5O//BQ6QNyCkznzIw8PU1mMvg3Vij8BZ4ENaDZLG35AAAAAElFTkSuQmCC") + +index.append('TW') +catalog['TW'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABEUlE" + "QVQokW2RsU0DURBE585nOUUCCRw4JiGiBWqwKAMKgIwuoApTAhRgUYB1BIYEgUC2bP7Mzif4" + "YHyC1WiSnbcaaSsc3mIzMmQkY63Ht9MDwH/UALi6PAJwvDvo1fX9fBFGROxMz3ta1mSW8o8v" + "JpOmnN5b+mS83+9X7fXi7mUlWbPW/MiJmSmnlMlqNDLQwLDNFEAmvVqJYcnfUaacWACQBhpI" + "EZiueXEzk/30/qkwbW/d3gAAGiTZDsXzqyQzckRIHnDxmy6AZKCJ+RgPZ7ltMxOoTG7X2Paq" + "VDJQkTklSJ1QNw0pSypATyoVtwt0MBEKRKAANYXhMJNQQARZkZBQXEJEkYFq2X0k/vvuZgXg" + "C0lTZuDB8+IlAAAAAElFTkSuQmCC") + +index.append('UA') +catalog['UA'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAAA90lE" + "QVQokW2MQUpDUQxFz/t9CIrDTpQK4mZcjgtw7CLchmsQhO7AmYJzHVjQl+ReB/2tFgzhcBNO" + "0rgz+5JIECSk5h47DgEdcXsNIFtuZVKUnG6pRWqKcsohR/nh/rMzAbxtXKZEyFWEHTVLozzk" + "KF+cNr7UEZJLpEm5imFnOXbeKI9ylKMAdYbKLUXKuf/9x54ph+CDznK9PL4c9V1ySiFSCink" + "KKUUNefVyRHnj61eYXVjv5iB0w57/Mtpunp/WneJ5rAH5KF0YEPamdAlFqQdsJf+OYOCIukS" + "kxPOdtuAaC0gYcucbUqibZ6RUM5kn7eB3xEB/ABIh27o+OhYYQAAAABJRU5ErkJggg==") + +index.append('US') +catalog['US'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABuUlE" + "QVQokVXRvWtTYRgF8PPWBBdRKAgWqcHF1U2koMVN7aCDINVFvGoGJzGLCuLQP0Ckg4jQVoQ6" + "dHBoQUFjLRksfixSDIUiJC3xJiG5uXnz3vfzeRykoZ7pBwfOckRUrGAYIpAHLMCAfVH4wGrQ" + "bv2xAAEEeABRsUIUXq16x3ah7J1zLz96Y/Tz995aY4zROssylWVKKbm0NCeiYmVy+vTiGgVC" + "IFjPgREI2vN3PEInQdqD7EMpce1C9VsFUfHTQtkbYyYf28FgMPHAdLvdk/d1rqTTNE2SpNPp" + "tNvtEK5Lma6vfxbR7bWz0xNzZQrEgeEdDPMGjFdffWMWSkFK1hrWjty8/GXjB6LonXPuzEPT" + "6/VOlUyr1dp3b6der+PSyr/tZrMZx3Gj0fhFYXn5TQ7g+VXhPc7N7CfPR2ZUMNXxOwE1Ojg1" + "xVrDGBjDWh++e7W6VRW12tbY2DHnHBExMxHtxdBZlh3PY/bt69zR+afi/JWRlTLHMZIEUkJK" + "aM1KwVoewjkq3Ti0/Vtsbv4sFE708/kDVjODmfj/eO+0HgW2Q6DRZ0/Ezq2LgfccuQsN9AEF" + "KMABerf9C2iAYqrHmBTIAAAAAElFTkSuQmCC") + +index.append('UY') +catalog['UY'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABYklE" + "QVQokWWRsYqTURSEz725ucZCsovCFrL4CLaWgi+wb2AnqN2CjQ+wvoK2iqA2woqInYWFWGhh" + "iAqyQVlJsfzKjxA3+c98x2KTKDjNmTnVx0xq29bMzAwsZwMzg/X5T8XManVpPyVqf8fTxsO3" + "zfFcWAiT6BQiXCHYu/M6NU1T637Or8wEl6J3LSKFxVqsTM559GlSwOF3zoqQ2SiFvxzPiBAh" + "QkKEA8SZQe/W/YNS66D0duaL74EP6vVF9F98aH8de0d0onM6Red0YnuzTu99SUfNz83hcLHo" + "IqJfCzCbg/EXiSVYyfn96HPZfXJ49XJuZy5CACGFCAUClwlEABuny427B+XBo2/bW8PDH4sV" + "w5qEVVSnkOLC2VPzx+P0cTI9v3VO4t9ClsFYfS0iSklv3o2TXXl2++ZFBwlXdCeVC1c4OEjh" + "MgIRz3efpsnXqePLGU+cOY5j4O6YAYY7ZtnsD8yTlKlFNE/4AAAAAElFTkSuQmCC") + +index.append('UZ') +catalog['UZ'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABZElE" + "QVQokX2RPUtcYRSE54WrYLGIuaYwYAgiQUhtvZWkkhTp0hlIE61SpLAQa39AQAgEf8BC2G6L" + "dCnlpgnpViMrQbJ+sAtx1zNnzpviZoNF4lMMw+EwxUwCNlqttysr83t7nw8OPgLe6ex8+/pz" + "+fH8+vobwAEDDBjXJgEbw+G7TqdbVWcRsbm5OhppejpdXIz29w+dQVetpNrt7QJAv3/dbD46" + "P79eW1s6PR2W92Z6vV9lOdPtXtJklJmTWlycBcbFK3w4ef9k0Du7L3355JIfm8t5ZHpBigyj" + "G0XO8sEh+mkwuGk0pgDknJGRkf9HSqmqvhcRUX/nnP+af4KUIrwIxORwZ37OKWd3pOfAy9e7" + "V8e9cJdRpJuLFCmj02V/mppberhVtYvWM5RPfxxdnlCkaG4mo2gyOi1ImYsuLZcZFQqMQflC" + "Y4GiSx51+KT9cJdLUpZCABKakylvD1prvXLGbX4DOjxvXkiHuOcAAAAASUVORK5CYII=") + +index.append('VA') +catalog['VA'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABa0lE" + "QVQokVWRsU5UYRSE5/z3v7CuuqsbthA0JmoMpQVGSKwsNBY+i8/gE/gOPoGx08YCY2I0MRYS" + "moWGYlciQQKu7J1/xuKyUb56Zs45c2L/6xA6ADIAgWCRoBTdOztIV3QeShk6unr7JQC4wIYL" + "QGMh+kOg4/OMx+OMFk7gYheY9szRSYUO2baLbTsiQlIGBJd/ahSbBh1pOuXx0dbu7uvTWb16" + "91GvtywpSYQ9zy4WrcZq7Pxj/3jz06u6RB87W9+fT39/I5kgw3SrdmM3Nu3GZXp95fKzpy9O" + "9vZGX/r3N952+48lZQFGmXva+JmjciyeHL75NXl3q/dwJa59/vhhsHxjoaqzCLhYjUGLZ0OQ" + "rabubizdXD8YbS9WP9cePGlUJuNJlmDxbA3PD4hszapq4OSLa6uRIuXciZpkJmET1RBmmEhE" + "OUV0IhIiAFwYDGwAbvuP0XvoDwSIEEFChIBL9zaFpf/f3Br+AlOWRi5rOFPDAAAAAElFTkSu" + "QmCC") + +index.append('VE') +catalog['VE'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABZklE" + "QVQokV2RPWtUYRCFz3tzQbZII2lSLCHGTv+AWppAkjLY2ysiCklhk38QtEkjbPIDYpPAFvEi" + "bGFjYSmIXxBTqCEibrbYOWdmLO4mqPDwMF+cZoqf4PAbUGH5MiIAAAEE4gIghAiEEEK9uIf9" + "4Wbnsb94N712/SfgmQ6opKagKpmpPPfZm/26Cjy1u8cPbO3RM+BXJgHP5F9YpmWylG4IdbOM" + "nH8OfG3vAM+089TJaWuAIRRgsLGxcHQ0liAGlb1et9OpMnM00urqJzLMgsy5uUuDwasKCCnI" + "JIPMXq/bNMOVlY9LSx/6/d8HB/Nm0SIFoBqQO6Q05u5Ot2mG29s/zNIstra+j8d++HLh5o33" + "bSKgchu37t97ePrl1OVucrpaU24hyk1Bl3zmysz62yd1H6+ndC34GWSSaZZmk+JiIkJe/Ood" + "oA6gojA7myTkEEEWEhJaS3BvCaCMJp+dgH/b/1YA/gB/9Fp0l9PEswAAAABJRU5ErkJggg==") + +index.append('VN') +catalog['VN'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABC0lE" + "QVQokX2RIU7dYRDE50+fAEETJOJpZB2GAyA4RV2v0CBAwFXaag5R1VM0gCKIhjZhZ/b7IT5S" + "eAlhM9ms2Jmd7Czopcbm/CYWJJ2eSmIMjaFu0uqQKMHmVX+4ulpJkuD6Rt3qxlY3B2aYX6aM" + "iyrsZb0e0koSg43tKo7DrvlZuChPguwhrYa0dCshUYcL8898MpjLYq/4/ELQvKD0dCmHM/Ot" + "wGAOixPz59mSkiFtSaJDggsXd8XXgoJHzovfj1RNvLJkU6VkKnFZfDH35nvxw//lSTIJH5Jp" + "EZtVcVLcmiqOzLZ5KGKl1a1J2HK0v4+ttGLZy9pKZGsn+pj5Q3UPafm7GeQ7Gc96AotzVU2K" + "OmSCAAAAAElFTkSuQmCC") + +index.append('VOLAPUK') +catalog['VOLAPUK'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABhUlE" + "QVQokWVQwYqCUAB8PtRU0kgUs0KSToWUlw59Qqc+OuhalwpSKUvUKJPSyJfK20OHdre5DTPD" + "DENgjMEX4jhOkkTTtG+J/Mcdx1EUxbKsMAwFQUAIKYry20C8G2azmW3blmXVarXhcKhp2na7" + "LcvScZw8zzudjiiKk8nkEwAAhGG4WCwwxrvdjmXZLMuiKBqPx/V6Xdd1URQ/DVEULZfL6/X6" + "eDziODZNc7PZlGWpKArP8zRNR1F0v9+n06ksyzBJkv1+L8tyq9XCGLMsS5KkJEkQQt/3Pc87" + "HA4QQo7jXNdN0xQGQZCmaRiG8/mcIAiGYV6v1+Vy6ff7PM/rug4AOB6PgiDcbjff98lqtQoA" + "aDabNE1TFJXnuW3bhmFkWfbePRgMyrJ0XRchZJomgTF2HGe9XjMMw3FcmqaSJAVBEATBaDTK" + "sowgiKIoEEK9Xk9VVRIA0O12ZVn2PA8A0G63VVVdrVYQQsMwTqfT8/mkKKrRaFQqlT+3/sb5" + "fC6KQlXVb+kHSbvUw5wmwUQAAAAASUVORK5CYII=") + +index.append('VU') +catalog['VU'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABkUlE" + "QVQokU2LTYjMARjGn//M38e6cBNa2nIy2MO4SGoPnOQwLOXjMAdOitO6rDPOo72uIkWKGuXG" + "QbFKe9mJk5ZCLnbXxyz/eZ/nfV8Hs6V+/XoOvwf7duDtQ4wAM4D/B4EB8AdYBX4BP4AVYAko" + "e1+w0J/+/L58MGs9j/3uKYerkOpSjUwp19zvdksA7xZz6dvHU2dqt25UP8lD8iSHGJOWZkkW" + "o6MB1ACE55sFv9etLl0tnrg/pQ1Ts+QgzYaQAZQAzh73rZu1/F13HuvKNC5eY0c0p9xMZk66" + "mXOX89m/Q1X5zfucuqCX86oq3r4uZxGBjCKiFlFk1MN93aZ67xFKAJ++6tiElles2VC7VZy+" + "XJlRNCNpQ5McG+u/eo0SwPM5fli0A3vVbuF8pz+YtMER0mlhTJobk0oNNggvUAIQ1Wyw3SrO" + "dVZtwqqDxooWxqAFmaagwlU4+igB7NmtE0d3Tt39ve3kFjZIJ9dTKQaVUsjTPdzTYSgLoDky" + "u30cOAyMA3NAAAIMiLUtAICAjfgLJMVRmyd9f8QAAAAASUVORK5CYII=") + +index.append('WS') +catalog['WS'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABKUlE" + "QVQokWWRwUpCURRF99OXg0ZCTRxY1LChOAj6AT+gWb/RzB+p33BYgRBhEEg0kEhToiBCGiQU" + "PM/e954GT1PzsNmTuxbnwkmAIywmAjbvbIphBP4lBdBsnu7tb9fru1eXj7Vatdt9G7+Og2JR" + "NwXSJZ/3d6uVAgB8d2fr/OzaLJTLm53Oc4lTMkQO3eg0N3MyqVYjUABijGi3nyaTrNE46PXe" + "zdwos+BmzqmbzULOhBAKo9Hn8cnhV+b9/pgETTS6mU9tWQCQApYqKzG7u3gYDj42FGAyisw3" + "cEFLEUgD7hFuXS8+MFDO/NNc0PNOyAikEUhIN4O0Aq3SkFxSLhQlJ/EHrWsiFBACcqFAoVJx" + "EgoQQSYkJOQtIYQ8EUh+Vg+JtdMuPwH4BW/TXRAimTLYAAAAAElFTkSuQmCC") + +index.append('YE') +catalog['YE'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAAA+ElE" + "QVQokZWPPUpDURCFz5XbPEhArAJik42kCVlONmBtmUcW4QpcRXYRCIh2Fkks7pk5x+KRZwoV" + "/BhmzvzClA98o4u/FqMFIKAC6J4eAdhCCkpHIsMZjnDQEQiK4eD780sFAFhvr8h0poNgOuig" + "SQfdmthM3tw/CKi3m40XC5xOsC1BsuRMp2w500pHWiqTyd1nLefzues6ALZhGP6NUsrhcKiS" + "hmnbo/gRlCKpSroU/rxvF1tS3W775XJ1PB4zU9LoR2wPxel02vd9AbBer/f7PUmSrbWrwEYG" + "GRERMZ/Pd7tdBUDGbDbj0GAwGBGXlCkNC8O3/+YLVjNhourzb1MAAAAASUVORK5CYII=") + +index.append('YU') +catalog['YU'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABEAAAALCAYAAACZIGYHAAAABHNCSVQICAgIfAhkiAAAASdJ" + "REFUKJGdkb1Kw2AUht8kX0OamDRFq8FBDUVRyeLg4DWIo5OLgjfhZYiDm65ehZPwIUKnBhws" + "VSzaFAtJJTZpk3wuGjSlTfUZ38N5OD/cyeo6M2UF/6X54YOYsgJLK/0qhCTBkESYCcSpRCQb" + "POhAuNIDX+QgNCSsdZJ8yVWk1meH+vt3sLtUNefL3TI4wDX03nmn1QyGUTxO0I0SlVwbOxbm" + "qml4YLrwXj0w8NDMhcpdf6Ny73Pjx3hrgM9mvq6DC0UocREDYzF3FQCjEt5powBAlCTELy0E" + "yYQpviD7m2LdWJbSm/DPtW1fkEmfCVAeabJnHd9OErSfRBWUUvaTgeux2sUluzk9Y4HjsDwo" + "pWzkxYWShq2jw6luka5j2/afGrLYto1PPjGOVBw0sOsAAAAASUVORK5CYII=") + +index.append('ZA') +catalog['ZA'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAAB00lE" + "QVQokU3GT0iTcRzH8c+z/ba25xkamg1KkWA1D3UoQoSSpKCgIKGgrnWJIjoJ81JGiFC3rG5B" + "HTtIUEYQRURUginB6lAesnnInpw5ZHv25/f98+sgQfDizRuY6MRE+4k3owvFuWo+L8AGAlpA" + "A4iAKrAOVIA/gHfjHWziHpSzHdtPbR3sev3efJiBkGMGsyNy/7U2PW32fUP/2eq1+drS0u+P" + "4ZfRofM76zUzM+dKi47IWXJknbWOyOvpUcADMPt2bEd+Zfyr/6tpRBojey/mku0di8suipyq" + "U3GiTtVL++tTUx6AQqGwuy8cGsRkKf2TE2u2JQ6peEqUSTYQOe7OZJ/cmTQAmOXp86hciRdO" + "Nm79SJXVb3K9Wg2tkBVLQlaJhDdJCyEMABE+ejh25JDeLyVLrUSTI3bqB51JURIiYVYi5Yy/" + "BRkYAMPHvL493s2F4HPVWBvdPXg9iAWr34N6TUScqlN1opoJTPeuC+bhOPL95sps22rESNDI" + "/nOvHiWePa6E5WUiteTIKrMyay6XLhZfGj2Oq8XAj8UHervObBt+8SA9/6kWtKHXTxMps2NW" + "ESeiIgDUwyWAkR04MLb59uXTK0AIKKCA/TcMMACAgeRf4EIuX9eJDOYAAAAASUVORK5CYII=") + +index.append('ZW') +catalog['ZW'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAIAAAD5gJpuAAAAA3NCSVQICAjb4U/gAAABmklE" + "QVQokU2RP2iTcRRF3y/9mvgn0Ukq0lIDdaiCLk4ODkqREmjAoUIEEUFc1bGCg5OrOlU3h4Ii" + "iF0yaEWFoE5Fl0wdOrlYIzFfk7z73rsONRK4cM98Tmpvt+eX52VJxERCJERsxCpiIjoGRUn5" + "n7y0v7TafDBzGs4IetCDFjQP84DTPOBhRtz5uJ7yPD8wWfICN97eWljoM0HESYxNSSWR0ky3" + "28xEhBMP8xfli8ura/duXIhBISnNAqApTcNAaBiyKbQfSdZ9vFKoH69c+fL76a/F6enXX7dO" + "9HMHAiDUoa4IqAMHe1gTSTutjWxrs9z4TNru+8bk+aXFWg3DIQBV1X+nAKrVaqvVyrKTZ/37" + "B9J2Px0unKu9vF5/cmTgQEADCCgNAQ3Dvim8E8lC4tDN+xHBM53ntxuXJnpZUgZCwKSREEmZ" + "wIIVizYnkjqdTqVSGQ5662+uXq53mCmpjD05I0sCoac0t/PzW+r3+wMMnzXvHj01Uk6LgBMW" + "Fv6/iZO+8mozbf/Ynj02K9dEeqOoOoo9znvty/IXxrxcMJLCp60AAAAASUVORK5CYII=") + diff --git a/wx/lib/art/img2pyartprov.py b/wx/lib/art/img2pyartprov.py new file mode 100644 index 00000000..a03bf691 --- /dev/null +++ b/wx/lib/art/img2pyartprov.py @@ -0,0 +1,57 @@ +#----------------------------------------------------------------------------- +# Name: img2pyartprov.py +# Purpose: +# +# Author: Riaan Booysen +# +# RCS-ID: $Id$ +# Copyright: (c) 2006 +# Licence: wxPython +#----------------------------------------------------------------------------- +""" ArtProvider class that publishes images from modules generated by img2py. + +Image modules must be generated with the -u and -n parameters. + +Typical usage: +>>> import wx, wx.lib.art.img2pyartprov, myimagemodule +>>> wx.ArtProvider.PushProvider(wx.lib.art.img2pyartprov.Img2PyArtProvider(myimagemodule)) + +If myimagemodule.catalog['MYIMAGE'] is defined, it can be accessed as: +>>> wx.ArtProvider.GetBitmap('wxART_MYIMAGE') + +""" + +import wx + +_NULL_BMP = wx.NullBitmap +class Img2PyArtProvider(wx.ArtProvider): + def __init__(self, imageModule, artIdPrefix='wxART_'): + self.catalog = {} + self.index = [] + self.UpdateFromImageModule(imageModule) + self.artIdPrefix = artIdPrefix + + wx.ArtProvider.__init__(self) + + def UpdateFromImageModule(self, imageModule): + try: + self.catalog.update(imageModule.catalog) + except AttributeError: + raise Exception, 'No catalog dictionary defined for the image module' + + try: + self.index.extend(imageModule.index) + except AttributeError: + raise Exception, 'No index list defined for the image module' + + def GenerateArtIdList(self): + return [self.artIdPrefix+name for name in self.index] + + def CreateBitmap(self, artId, artClient, size): + if artId.startswith(self.artIdPrefix): + name = artId[len(self.artIdPrefix):] + if name in self.catalog: + return self.catalog[name].GetBitmap() + + return _NULL_BMP + diff --git a/wx/lib/buttonpanel.py b/wx/lib/buttonpanel.py new file mode 100644 index 00000000..cb3ed0e1 --- /dev/null +++ b/wx/lib/buttonpanel.py @@ -0,0 +1,13 @@ +# ============================================================== # +# This is now just a stub, importing the real module which lives # +# under wx.lib.agw. +# ============================================================== # + +""" +Attention! ButtonPanel now lives in wx.lib.agw, together with +its friends in the Advanced Generic Widgets family. + +Please update your code! +""" + +from wx.lib.agw.buttonpanel import * \ No newline at end of file diff --git a/wx/lib/buttons.py b/wx/lib/buttons.py new file mode 100644 index 00000000..1d874374 --- /dev/null +++ b/wx/lib/buttons.py @@ -0,0 +1,657 @@ +#---------------------------------------------------------------------- +# Name: wx.lib.buttons +# Purpose: Various kinds of generic buttons, (not native controls but +# self-drawn.) +# +# Author: Robin Dunn +# +# Created: 9-Dec-1999 +# RCS-ID: $Id$ +# Copyright: (c) 1999 by Total Control Software +# Licence: wxWindows license +#---------------------------------------------------------------------- +# 11/30/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o Updated for wx namespace +# o Tested with updated demo +# + +""" +This module implements various forms of generic buttons, meaning that +they are not built on native controls but are self-drawn. They act +like normal buttons but you are able to better control how they look, +bevel width, colours, etc. +""" + +import wx +import imageutils + + +#---------------------------------------------------------------------- + +class GenButtonEvent(wx.PyCommandEvent): + """Event sent from the generic buttons when the button is activated. """ + def __init__(self, eventType, id): + wx.PyCommandEvent.__init__(self, eventType, id) + self.isDown = False + self.theButton = None + + def SetIsDown(self, isDown): + self.isDown = isDown + + def GetIsDown(self): + return self.isDown + + def SetButtonObj(self, btn): + self.theButton = btn + + def GetButtonObj(self): + return self.theButton + + +#---------------------------------------------------------------------- + +class GenButton(wx.PyControl): + """A generic button, and base class for the other generic buttons.""" + + labelDelta = 1 + + def __init__(self, parent, id=-1, label='', + pos = wx.DefaultPosition, size = wx.DefaultSize, + style = 0, validator = wx.DefaultValidator, + name = "genbutton"): + cstyle = style + if cstyle & wx.BORDER_MASK == 0: + cstyle |= wx.BORDER_NONE + wx.PyControl.__init__(self, parent, id, pos, size, cstyle, validator, name) + + self.up = True + self.hasFocus = False + self.style = style + if style & wx.BORDER_NONE: + self.bezelWidth = 0 + self.useFocusInd = False + else: + self.bezelWidth = 2 + self.useFocusInd = True + + self.SetLabel(label) + self.InheritAttributes() + self.SetInitialSize(size) + self.InitColours() + + self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) + self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp) + self.Bind(wx.EVT_LEFT_DCLICK, self.OnLeftDown) + self.Bind(wx.EVT_MOTION, self.OnMotion) + self.Bind(wx.EVT_SET_FOCUS, self.OnGainFocus) + self.Bind(wx.EVT_KILL_FOCUS, self.OnLoseFocus) + self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown) + self.Bind(wx.EVT_KEY_UP, self.OnKeyUp) + self.Bind(wx.EVT_PAINT, self.OnPaint) + self.Bind(wx.EVT_ERASE_BACKGROUND, lambda evt: None) + self.Bind(wx.EVT_SIZE, self.OnSize) + self.InitOtherEvents() + + def InitOtherEvents(self): + """ + Override in a subclass to initialize any other events that + need to be bound. Added so __init__ doesn't need to be + overriden, which is complicated with multiple inheritance + """ + pass + + + def SetInitialSize(self, size=None): + """ + Given the current font and bezel width settings, calculate + and set a good size. + """ + if size is None: + size = wx.DefaultSize + wx.PyControl.SetInitialSize(self, size) + SetBestSize = SetInitialSize + + + def DoGetBestSize(self): + """ + Overridden base class virtual. Determines the best size of the + button based on the label and bezel size. + """ + w, h, useMin = self._GetLabelSize() + if self.style & wx.BU_EXACTFIT: + width = w + 2 + 2 * self.bezelWidth + 4 * int(self.useFocusInd) + height = h + 2 + 2 * self.bezelWidth + 4 * int(self.useFocusInd) + else: + defSize = wx.Button.GetDefaultSize() + width = 12 + w + if useMin and width < defSize.width: + width = defSize.width + height = 11 + h + if useMin and height < defSize.height: + height = defSize.height + width = width + self.bezelWidth - 1 + height = height + self.bezelWidth - 1 + return (width, height) + + + def AcceptsFocus(self): + """Overridden base class virtual.""" + return self.IsShown() and self.IsEnabled() + + + def GetDefaultAttributes(self): + """ + Overridden base class virtual. By default we should use + the same font/colour attributes as the native Button. + """ + return wx.Button.GetClassDefaultAttributes() + + + def ShouldInheritColours(self): + """ + Overridden base class virtual. Buttons usually don't inherit + the parent's colours. + """ + return False + + + def Enable(self, enable=True): + if enable != self.IsEnabled(): + wx.PyControl.Enable(self, enable) + self.Refresh() + + + def SetBezelWidth(self, width): + """Set the width of the 3D effect""" + self.bezelWidth = width + + def GetBezelWidth(self): + """Return the width of the 3D effect""" + return self.bezelWidth + + def SetUseFocusIndicator(self, flag): + """Specifiy if a focus indicator (dotted line) should be used""" + self.useFocusInd = flag + + def GetUseFocusIndicator(self): + """Return focus indicator flag""" + return self.useFocusInd + + + def InitColours(self): + """ + Calculate a new set of highlight and shadow colours based on + the background colour. Works okay if the colour is dark... + """ + faceClr = self.GetBackgroundColour() + r, g, b = faceClr.Get() + fr, fg, fb = min(255,r+32), min(255,g+32), min(255,b+32) + self.faceDnClr = wx.Colour(fr, fg, fb) + sr, sg, sb = max(0,r-32), max(0,g-32), max(0,b-32) + self.shadowPenClr = wx.Colour(sr,sg,sb) + hr, hg, hb = min(255,r+64), min(255,g+64), min(255,b+64) + self.highlightPenClr = wx.Colour(hr,hg,hb) + self.focusClr = wx.Colour(hr, hg, hb) + + + def SetBackgroundColour(self, colour): + wx.PyControl.SetBackgroundColour(self, colour) + self.InitColours() + + + def SetForegroundColour(self, colour): + wx.PyControl.SetForegroundColour(self, colour) + self.InitColours() + + def SetDefault(self): + tlw = wx.GetTopLevelParent(self) + if hasattr(tlw, 'SetDefaultItem'): + tlw.SetDefaultItem(self) + + def _GetLabelSize(self): + """ used internally """ + w, h = self.GetTextExtent(self.GetLabel()) + return w, h, True + + + def Notify(self): + evt = GenButtonEvent(wx.wxEVT_COMMAND_BUTTON_CLICKED, self.GetId()) + evt.SetIsDown(not self.up) + evt.SetButtonObj(self) + evt.SetEventObject(self) + self.GetEventHandler().ProcessEvent(evt) + + + def DrawBezel(self, dc, x1, y1, x2, y2): + # draw the upper left sides + if self.up: + dc.SetPen(wx.Pen(self.highlightPenClr, 1, wx.SOLID)) + else: + dc.SetPen(wx.Pen(self.shadowPenClr, 1, wx.SOLID)) + for i in range(self.bezelWidth): + dc.DrawLine(x1+i, y1, x1+i, y2-i) + dc.DrawLine(x1, y1+i, x2-i, y1+i) + + # draw the lower right sides + if self.up: + dc.SetPen(wx.Pen(self.shadowPenClr, 1, wx.SOLID)) + else: + dc.SetPen(wx.Pen(self.highlightPenClr, 1, wx.SOLID)) + for i in range(self.bezelWidth): + dc.DrawLine(x1+i, y2-i, x2+1, y2-i) + dc.DrawLine(x2-i, y1+i, x2-i, y2) + + + def DrawLabel(self, dc, width, height, dx=0, dy=0): + dc.SetFont(self.GetFont()) + if self.IsEnabled(): + dc.SetTextForeground(self.GetForegroundColour()) + else: + dc.SetTextForeground(wx.SystemSettings.GetColour(wx.SYS_COLOUR_GRAYTEXT)) + label = self.GetLabel() + tw, th = dc.GetTextExtent(label) + if not self.up: + dx = dy = self.labelDelta + dc.DrawText(label, (width-tw)/2+dx, (height-th)/2+dy) + + + def DrawFocusIndicator(self, dc, w, h): + bw = self.bezelWidth + textClr = self.GetForegroundColour() + focusIndPen = wx.Pen(textClr, 1, wx.USER_DASH) + focusIndPen.SetDashes([1,1]) + focusIndPen.SetCap(wx.CAP_BUTT) + + if wx.Platform == "__WXMAC__": + dc.SetLogicalFunction(wx.XOR) + else: + focusIndPen.SetColour(self.focusClr) + dc.SetLogicalFunction(wx.INVERT) + dc.SetPen(focusIndPen) + dc.SetBrush(wx.TRANSPARENT_BRUSH) + dc.DrawRectangle(bw+2,bw+2, w-bw*2-4, h-bw*2-4) + dc.SetLogicalFunction(wx.COPY) + + + def OnPaint(self, event): + (width, height) = self.GetClientSizeTuple() + x1 = y1 = 0 + x2 = width-1 + y2 = height-1 + + dc = wx.PaintDC(self) + brush = self.GetBackgroundBrush(dc) + if brush is not None: + dc.SetBackground(brush) + dc.Clear() + + self.DrawBezel(dc, x1, y1, x2, y2) + self.DrawLabel(dc, width, height) + if self.hasFocus and self.useFocusInd: + self.DrawFocusIndicator(dc, width, height) + + + def OnSize(self, event): + self.Refresh() + event.Skip() + + + def GetBackgroundBrush(self, dc): + if self.up: + colBg = self.GetBackgroundColour() + brush = wx.Brush(colBg, wx.SOLID) + if self.style & wx.BORDER_NONE: + myAttr = self.GetDefaultAttributes() + parAttr = self.GetParent().GetDefaultAttributes() + myDef = colBg == myAttr.colBg + parDef = self.GetParent().GetBackgroundColour() == parAttr.colBg + if myDef and parDef: + if wx.Platform == "__WXMAC__": + c = wx.MacThemeColour(1) # 1 == kThemeBrushDialogBackgroundActive + brush = wx.Brush(c) + elif wx.Platform == "__WXMSW__": + if self.DoEraseBackground(dc): + brush = None + elif myDef and not parDef: + colBg = self.GetParent().GetBackgroundColour() + brush = wx.Brush(colBg, wx.SOLID) + else: + # this line assumes that a pressed button should be hilighted with + # a solid colour even if the background is supposed to be transparent + brush = wx.Brush(self.faceDnClr, wx.SOLID) + return brush + + + def OnLeftDown(self, event): + if not self.IsEnabled(): + return + self.up = False + self.CaptureMouse() + self.SetFocus() + self.Refresh() + event.Skip() + + + def OnLeftUp(self, event): + if not self.IsEnabled() or not self.HasCapture(): + return + if self.HasCapture(): + self.ReleaseMouse() + if not self.up: # if the button was down when the mouse was released... + self.Notify() + self.up = True + if self: # in case the button was destroyed in the eventhandler + self.Refresh() + event.Skip() + + + def OnMotion(self, event): + if not self.IsEnabled() or not self.HasCapture(): + return + if event.LeftIsDown() and self.HasCapture(): + x,y = event.GetPositionTuple() + w,h = self.GetClientSizeTuple() + if self.up and x=0 and y=0: + self.up = False + self.Refresh() + return + if not self.up and (x<0 or y<0 or x>=w or y>=h): + self.up = True + self.Refresh() + return + event.Skip() + + + def OnGainFocus(self, event): + self.hasFocus = True + self.Refresh() + self.Update() + + + def OnLoseFocus(self, event): + self.hasFocus = False + self.Refresh() + self.Update() + + + def OnKeyDown(self, event): + if self.hasFocus and event.GetKeyCode() == ord(" "): + self.up = False + self.Refresh() + event.Skip() + + + def OnKeyUp(self, event): + if self.hasFocus and event.GetKeyCode() == ord(" "): + self.up = True + self.Notify() + self.Refresh() + event.Skip() + + +#---------------------------------------------------------------------- + +class GenBitmapButton(GenButton): + """A generic bitmap button.""" + + def __init__(self, parent, id=-1, bitmap=wx.NullBitmap, + pos = wx.DefaultPosition, size = wx.DefaultSize, + style = 0, validator = wx.DefaultValidator, + name = "genbutton"): + self.bmpDisabled = None + self.bmpFocus = None + self.bmpSelected = None + self.SetBitmapLabel(bitmap) + GenButton.__init__(self, parent, id, "", pos, size, style, validator, name) + + + def GetBitmapLabel(self): + return self.bmpLabel + def GetBitmapDisabled(self): + return self.bmpDisabled + def GetBitmapFocus(self): + return self.bmpFocus + def GetBitmapSelected(self): + return self.bmpSelected + + + def SetBitmapDisabled(self, bitmap): + """Set bitmap to display when the button is disabled""" + self.bmpDisabled = bitmap + + def SetBitmapFocus(self, bitmap): + """Set bitmap to display when the button has the focus""" + self.bmpFocus = bitmap + self.SetUseFocusIndicator(False) + + def SetBitmapSelected(self, bitmap): + """Set bitmap to display when the button is selected (pressed down)""" + self.bmpSelected = bitmap + + def SetBitmapLabel(self, bitmap, createOthers=True): + """ + Set the bitmap to display normally. + This is the only one that is required. If + createOthers is True, then the other bitmaps + will be generated on the fly. Currently, + only the disabled bitmap is generated. + """ + self.bmpLabel = bitmap + if bitmap is not None and createOthers: + image = wx.ImageFromBitmap(bitmap) + imageutils.grayOut(image) + self.SetBitmapDisabled(wx.BitmapFromImage(image)) + + + def _GetLabelSize(self): + """ used internally """ + if not self.bmpLabel: + return -1, -1, False + return self.bmpLabel.GetWidth()+2, self.bmpLabel.GetHeight()+2, False + + def DrawLabel(self, dc, width, height, dx=0, dy=0): + bmp = self.bmpLabel + if self.bmpDisabled and not self.IsEnabled(): + bmp = self.bmpDisabled + if self.bmpFocus and self.hasFocus: + bmp = self.bmpFocus + if self.bmpSelected and not self.up: + bmp = self.bmpSelected + bw,bh = bmp.GetWidth(), bmp.GetHeight() + if not self.up: + dx = dy = self.labelDelta + hasMask = bmp.GetMask() != None + dc.DrawBitmap(bmp, (width-bw)/2+dx, (height-bh)/2+dy, hasMask) + + +#---------------------------------------------------------------------- + + +class GenBitmapTextButton(GenBitmapButton): + """A generic bitmapped button with text label""" + def __init__(self, parent, id=-1, bitmap=wx.NullBitmap, label='', + pos = wx.DefaultPosition, size = wx.DefaultSize, + style = 0, validator = wx.DefaultValidator, + name = "genbutton"): + GenBitmapButton.__init__(self, parent, id, bitmap, pos, size, style, validator, name) + self.SetLabel(label) + + + def _GetLabelSize(self): + """ used internally """ + w, h = self.GetTextExtent(self.GetLabel()) + if not self.bmpLabel: + return w, h, True # if there isn't a bitmap use the size of the text + + w_bmp = self.bmpLabel.GetWidth()+2 + h_bmp = self.bmpLabel.GetHeight()+2 + width = w + w_bmp + if h_bmp > h: + height = h_bmp + else: + height = h + return width, height, True + + + def DrawLabel(self, dc, width, height, dx=0, dy=0): + bmp = self.bmpLabel + if bmp is not None: # if the bitmap is used + if self.bmpDisabled and not self.IsEnabled(): + bmp = self.bmpDisabled + if self.bmpFocus and self.hasFocus: + bmp = self.bmpFocus + if self.bmpSelected and not self.up: + bmp = self.bmpSelected + bw,bh = bmp.GetWidth(), bmp.GetHeight() + if not self.up: + dx = dy = self.labelDelta + hasMask = bmp.GetMask() is not None + else: + bw = bh = 0 # no bitmap -> size is zero + + dc.SetFont(self.GetFont()) + if self.IsEnabled(): + dc.SetTextForeground(self.GetForegroundColour()) + else: + dc.SetTextForeground(wx.SystemSettings.GetColour(wx.SYS_COLOUR_GRAYTEXT)) + + label = self.GetLabel() + tw, th = dc.GetTextExtent(label) # size of text + if not self.up: + dx = dy = self.labelDelta + + pos_x = (width-bw-tw)/2+dx # adjust for bitmap and text to centre + if bmp is not None: + dc.DrawBitmap(bmp, pos_x, (height-bh)/2+dy, hasMask) # draw bitmap if available + pos_x = pos_x + 2 # extra spacing from bitmap + + dc.DrawText(label, pos_x + dx+bw, (height-th)/2+dy) # draw the text + + +#---------------------------------------------------------------------- + + +class __ToggleMixin: + def SetToggle(self, flag): + self.up = not flag + self.Refresh() + SetValue = SetToggle + + def GetToggle(self): + return not self.up + GetValue = GetToggle + + def OnLeftDown(self, event): + if not self.IsEnabled(): + return + self.saveUp = self.up + self.up = not self.up + self.CaptureMouse() + self.SetFocus() + self.Refresh() + + def OnLeftUp(self, event): + if not self.IsEnabled() or not self.HasCapture(): + return + if self.HasCapture(): + self.ReleaseMouse() + self.Refresh() + if self.up != self.saveUp: + self.Notify() + + def OnKeyDown(self, event): + event.Skip() + + def OnMotion(self, event): + if not self.IsEnabled(): + return + if event.LeftIsDown() and self.HasCapture(): + x,y = event.GetPositionTuple() + w,h = self.GetClientSizeTuple() + if x=0 and y=0: + self.up = not self.saveUp + self.Refresh() + return + if (x<0 or y<0 or x>=w or y>=h): + self.up = self.saveUp + self.Refresh() + return + event.Skip() + + def OnKeyUp(self, event): + if self.hasFocus and event.GetKeyCode() == ord(" "): + self.up = not self.up + self.Notify() + self.Refresh() + event.Skip() + + + + +class GenToggleButton(__ToggleMixin, GenButton): + """A generic toggle button""" + pass + +class GenBitmapToggleButton(__ToggleMixin, GenBitmapButton): + """A generic toggle bitmap button""" + pass + +class GenBitmapTextToggleButton(__ToggleMixin, GenBitmapTextButton): + """A generic toggle bitmap button with text label""" + pass + +#---------------------------------------------------------------------- + + +class __ThemedMixin: + """ Use the native renderer to draw the bezel, also handle mouse-overs""" + def InitOtherEvents(self): + self.Bind(wx.EVT_ENTER_WINDOW, self.OnMouse) + self.Bind(wx.EVT_LEAVE_WINDOW, self.OnMouse) + + def OnMouse(self, evt): + self.Refresh() + evt.Skip() + + def DrawBezel(self, dc, x1, y1, x2, y2): + rect = wx.Rect(x1, y1, x2, y2) + if self.up: + state = 0 + else: + state = wx.CONTROL_PRESSED | wx.CONTROL_SELECTED + if not self.IsEnabled(): + state = wx.CONTROL_DISABLED + pt = self.ScreenToClient(wx.GetMousePosition()) + if self.GetClientRect().Contains(pt): + state |= wx.CONTROL_CURRENT + wx.RendererNative.Get().DrawPushButton(self, dc, rect, state) + + + +class ThemedGenButton(__ThemedMixin, GenButton): + """A themed generic button""" + pass + +class ThemedGenBitmapButton(__ThemedMixin, GenBitmapButton): + """A themed generic bitmap button.""" + pass + +class ThemedGenBitmapTextButton(__ThemedMixin, GenBitmapTextButton): + """A themed generic bitmapped button with text label""" + pass + +class ThemedGenToggleButton(__ThemedMixin, GenToggleButton): + """A themed generic toggle button""" + pass + +class ThemedGenBitmapToggleButton(__ThemedMixin, GenBitmapToggleButton): + """A themed generic toggle bitmap button""" + pass + +class ThemedGenBitmapTextToggleButton(__ThemedMixin, GenBitmapTextToggleButton): + """A themed generic toggle bitmap button with text label""" + pass + + +#---------------------------------------------------------------------- diff --git a/wx/lib/calendar.py b/wx/lib/calendar.py new file mode 100644 index 00000000..80fdbd0b --- /dev/null +++ b/wx/lib/calendar.py @@ -0,0 +1,1236 @@ +#---------------------------------------------------------------------------- +# Name: calendar.py +# Purpose: Calendar display control +# +# Author: Lorne White (email: lorne.white@telusplanet.net) +# +# Created: +# Version 0.92 +# Date: Nov 26, 2001 +# Licence: wxWindows license +#---------------------------------------------------------------------------- +# 12/01/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o Updated for wx namespace +# o Tested with updated demo +# o Added new event type EVT_CALENDAR. The reason for this is that the original +# library used a hardcoded ID of 2100 for generating events. This makes it +# very difficult to fathom when trying to decode the code since there's no +# published API. Creating the new event binder might seem like overkill - +# after all, you might ask, why not just use a new event ID and be done with +# it? However, a consistent interface is very useful at times; also it makes +# it clear that we're not just hunting for mouse clicks -- we're hunting +# wabbit^H^H^H^H (sorry bout that) for calender-driven mouse clicks. So +# that's my sad story. Shoot me if you must :-) +# o There's still one deprecation warning buried in here somewhere, but I +# haven't been able to find it yet. It only occurs when displaying a +# print preview, and only the first time. It *could* be an error in the +# demo, I suppose. +# +# Here's the traceback: +# +# C:\Python\lib\site-packages\wx\core.py:949: DeprecationWarning: +# integer argument expected, got float +# newobj = _core.new_Rect(*args, **kwargs) +# +# 12/17/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o A few style-guide nips and tucks +# o Renamed wxCalendar to Calendar +# o Couple of bugfixes +# +# 06/02/2004 - Joerg "Adi" Sieker adi@sieker.info +# +# o Changed color handling, use dictionary instead of members. +# This causes all color changes to be ignored if they manipluate the members directly. +# SetWeekColor and other method color methods were adapted to use the new dictionary. +# o Added COLOR_* constants +# o Added SetColor method for Calendar class +# o Added 3D look of week header +# o Added colors for 3D look of header +# o Fixed width calculation. +# Because of rounding difference the total width and height of the +# calendar could be up to 6 pixels to small. The last column and row +# are now wider/taller by the missing amount. +# o Added SetTextAlign method to wxCalendar. This exposes logic +# which was already there. +# o Fixed CalDraw.SetMarg which set set_x_st and set_y_st which don't get used anywhere. +# Instead set set_x_mrg and set_y_mrg +# o Changed default X and Y Margin to 0. +# o Added wxCalendar.SetMargin. +# +# 17/03/2004 - Joerg "Adi" Sieker adi@sieker.info +# o Added keyboard navigation to the control. +# Use the cursor keys to navigate through the ages. :) +# The Home key function as go to today +# o select day is now a filled rect instead of just an outline +# +# 15/04/2005 - Joe "shmengie" Brown joebrown@podiatryfl.com +# o Adjusted spin control size/placement (On Windows ctrls were overlapping). +# o Set Ok/Cancel buttons to wx.ID_OK & wx.ID_CANCEL to provide default dialog +# behaviour. +# o If no date has been clicked clicked, OnOk set the result to calend's date, +# important if keyboard only navigation is used. +# +# 12/10/2006 - Walter Barnes walter_barnes05@yahoo.com +# o Fixed CalDraw to properly render months that start on a Sunday. +# +# 21/10/2006 - Walter Barnes walter_barnes05@yahoo.com +# o Fixed a bug in Calendar: Shift and Control key status was only recorded for +# left-down events. +# o Added handlers for wxEVT_MIDDLE_DOWN and wxEVT_MIDDLE_DCLICK to generate +# EVT_CALENDAR for these mouse events. + + +import wx + +from CDate import * + +CalDays = [6, 0, 1, 2, 3, 4, 5] +AbrWeekday = {6:"Sun", 0:"Mon", 1:"Tue", 2:"Wed", 3:"Thu", 4:"Fri", 5:"Sat"} +_MIDSIZE = 180 + +COLOR_GRID_LINES = "grid_lines" +COLOR_BACKGROUND = "background" +COLOR_SELECTION_FONT = "selection_font" +COLOR_SELECTION_BACKGROUND = "selection_background" +COLOR_BORDER = "border" +COLOR_HEADER_BACKGROUND = "header_background" +COLOR_HEADER_FONT = "header_font" +COLOR_WEEKEND_BACKGROUND = "weekend_background" +COLOR_WEEKEND_FONT = "weekend_font" +COLOR_FONT = "font" +COLOR_3D_LIGHT = "3d_light" +COLOR_3D_DARK = "3d_dark" +COLOR_HIGHLIGHT_FONT = "highlight_font" +COLOR_HIGHLIGHT_BACKGROUND = "highlight_background" + +BusCalDays = [0, 1, 2, 3, 4, 5, 6] + +# Calendar click event - added 12/1/03 by jmg (see above) +wxEVT_COMMAND_PYCALENDAR_DAY_CLICKED = wx.NewEventType() +EVT_CALENDAR = wx.PyEventBinder(wxEVT_COMMAND_PYCALENDAR_DAY_CLICKED, 1) + +def GetMonthList(): + monthlist = [] + for i in range(13): + name = Month[i] + if name != None: + monthlist.append(name) + return monthlist + +def MakeColor(in_color): + try: + color = wxNamedColour(in_color) + except: + color = in_color + return color + +def DefaultColors(): + colors = {} + colors[COLOR_GRID_LINES] = 'BLACK' + colors[COLOR_BACKGROUND] = 'WHITE' + colors[COLOR_SELECTION_FONT] = wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOWTEXT) + colors[COLOR_SELECTION_BACKGROUND] =wx.Colour(255,255,225) + colors[COLOR_BORDER] = 'BLACK' + colors[COLOR_HEADER_BACKGROUND] = wx.SystemSettings_GetColour(wx.SYS_COLOUR_3DFACE) + colors[COLOR_HEADER_FONT] = wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOWTEXT) + colors[COLOR_WEEKEND_BACKGROUND] = 'LIGHT GREY' + colors[COLOR_WEEKEND_FONT] = wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOWTEXT) + colors[COLOR_FONT] = wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOWTEXT) + colors[COLOR_3D_LIGHT] = wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNHIGHLIGHT) + colors[COLOR_3D_DARK] = wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNSHADOW) + colors[COLOR_HIGHLIGHT_FONT] = wx.SystemSettings_GetColour(wx.SYS_COLOUR_HIGHLIGHTTEXT) + colors[COLOR_HIGHLIGHT_BACKGROUND] = wx.SystemSettings_GetColour(wx.SYS_COLOUR_HIGHLIGHT) + return colors +# calendar drawing routing + +class CalDraw: + def __init__(self, parent): + self.pwidth = 1 + self.pheight = 1 + try: + self.scale = parent.scale + except: + self.scale = 1 + + self.gridx = [] + self.gridy = [] + + self.DefParms() + + def DefParms(self): + self.num_auto = True # auto scale of the cal number day size + self.num_size = 12 # default size of calendar if no auto size + self.max_num_size = 12 # maximum size for calendar number + + self.num_align_horz = wx.ALIGN_CENTRE # alignment of numbers + self.num_align_vert = wx.ALIGN_CENTRE + self.num_indent_horz = 0 # points indent from position, used to offset if not centered + self.num_indent_vert = 0 + + self.week_auto = True # auto scale of week font text + self.week_size = 10 + self.max_week_size = 12 + + self.colors = DefaultColors() + + self.font = wx.SWISS + self.bold = wx.NORMAL + + self.hide_title = False + self.hide_grid = False + self.outer_border = True + + self.title_offset = 0 + self.cal_week_scale = 0.7 + self.show_weekend = False + self.cal_type = "NORMAL" + + def SetWeekColor(self, font_color, week_color): + # set font and background color for week title + self.colors[COLOR_HEADER_FONT] = MakeColor(font_color) + self.colors[COLOR_HEADER_BACKGROUND] = MakeColor(week_color) + self.colors[COLOR_3D_LIGHT] = MakeColor(week_color) + self.colors[COLOR_3D_DARK] = MakeColor(week_color) + + def SetSize(self, size): + self.set_sizew = size[0] + self.set_sizeh = size[1] + + def InitValues(self): # default dimensions of various elements of the calendar + self.rg = {} + self.cal_sel = {} + self.set_cy_st = 0 # start position + self.set_cx_st = 0 + + self.set_y_mrg = 1 # start of vertical draw default + self.set_x_mrg = 1 + self.set_y_end = 1 + def SetPos(self, xpos, ypos): + self.set_cx_st = xpos + self.set_cy_st = ypos + + def SetMarg(self, xmarg, ymarg): + self.set_x_mrg = xmarg + self.set_y_mrg = ymarg + self.set_y_end = ymarg + + def InitScale(self): # scale position values + self.sizew = int(self.set_sizew * self.pwidth) + self.sizeh = int(self.set_sizeh * self.pheight) + + self.cx_st = int(self.set_cx_st * self.pwidth) # draw start position + self.cy_st = int(self.set_cy_st * self.pheight) + + self.x_mrg = int(self.set_x_mrg * self.pwidth) # calendar draw margins + self.y_mrg = int(self.set_y_mrg * self.pheight) + self.y_end = int(self.set_y_end * self.pheight) + + def DrawCal(self, DC, sel_lst=[]): + self.InitScale() + + self.DrawBorder(DC) + + if self.hide_title is False: + self.DrawMonth(DC) + + self.Center() + + self.DrawGrid(DC) + self.GetRect() + if self.show_weekend is True: # highlight weekend dates + self.SetWeekEnd() + + self.AddSelect(sel_lst) # overrides the weekend highlight + + self.DrawSel(DC) # highlighted days + self.DrawWeek(DC) + self.DrawNum(DC) + + def AddSelect(self, list, cfont=None, cbackgrd = None): + if cfont is None: + cfont = self.colors[COLOR_SELECTION_FONT] # font digit color + if cbackgrd is None: + cbackgrd = self.colors[COLOR_SELECTION_BACKGROUND] # select background color + + for val in list: + self.cal_sel[val] = (cfont, cbackgrd) + + # draw border around the outside of the main display rectangle + def DrawBorder(self, DC, transparent = False): + if self.outer_border is True: + if transparent == False: + brush = wx.Brush(MakeColor(self.colors[COLOR_BACKGROUND]), wx.SOLID) + else: + brush = wx.TRANSPARENT_BRUSH + DC.SetBrush(brush) + DC.SetPen(wx.Pen(MakeColor(self.colors[COLOR_BORDER]))) + # full display window area + rect = wx.Rect(self.cx_st, self.cy_st, self.sizew, self.sizeh) + DC.DrawRectangleRect(rect) + + def DrawFocusIndicator(self, DC): + if self.outer_border is True: + DC.SetBrush(wx.TRANSPARENT_BRUSH) + DC.SetPen(wx.Pen(MakeColor(self.colors[COLOR_HIGHLIGHT_BACKGROUND]), style=wx.DOT)) + # full display window area + rect = wx.Rect(self.cx_st, self.cy_st, self.sizew, self.sizeh) + DC.DrawRectangleRect(rect) + + def DrawNumVal(self): + self.DrawNum() + + # calculate the calendar days and offset position + def SetCal(self, year, month): + self.InitValues() # reset initial values + + self.year = year + self.month = month + + day = 1 + t = Date(year, month, day) + dow = self.dow = t.day_of_week # start day in month + dim = self.dim = t.days_in_month # number of days in month + + if self.cal_type == "NORMAL": + start_pos = dow+1 + else: + start_pos = dow + + if start_pos > 6: + start_pos = 0 + + self.st_pos = start_pos + + self.cal_days = [] + for i in range(start_pos): + self.cal_days.append('') + + i = 1 + while i <= dim: + self.cal_days.append(str(i)) + i = i + 1 + + self.end_pos = start_pos + dim - 1 + + return start_pos + + def SetWeekEnd(self, font_color=None, backgrd = None): + if font_color != None: + self.SetColor(COLOR_WEEKEND_FONT, MakeColor(font_color)) + if backgrd != None: + self.SetColor(COLOR_WEEKEND_BACKGROUND, MakeColor(backgrd)) + + date = 6 - int(self.dow) # start day of first saturday + if date == 0: #...unless we start on Sunday + self.cal_sel[1] = (self.GetColor(COLOR_WEEKEND_FONT), self.GetColor(COLOR_WEEKEND_BACKGROUND)) + date = 7 + + while date <= self.dim: + self.cal_sel[date] = (self.GetColor(COLOR_WEEKEND_FONT), self.GetColor(COLOR_WEEKEND_BACKGROUND)) # Saturday + date = date + 1 + + if date <= self.dim: + self.cal_sel[date] = (self.GetColor(COLOR_WEEKEND_FONT), self.GetColor(COLOR_WEEKEND_BACKGROUND)) # Sunday + date = date + 6 + else: + date = date + 7 + + # get the display rectange list of the day grid + def GetRect(self): + cnt = 0 + h = 0 + w = 0 + for y in self.gridy[1:-1]: + if y == self.gridy[-2]: + h = h + self.restH + + for x in self.gridx[:-1]: + assert type(y) == int + assert type(x) == int + + w = self.cellW + h = self.cellH + + if x == self.gridx[-2]: + w = w + self.restW + + rect = wx.Rect(x, y, w+1, h+1) # create rect region + + self.rg[cnt] = rect + cnt = cnt + 1 + + return self.rg + + def GetCal(self): + return self.cal_days + + def GetOffset(self): + return self.st_pos + + # month and year title + def DrawMonth(self, DC): + month = Month[self.month] + + sizef = 11 + if self.sizeh < _MIDSIZE: + sizef = 10 + + f = wx.Font(sizef, self.font, wx.NORMAL, self.bold) + DC.SetFont(f) + + tw,th = DC.GetTextExtent(month) + adjust = self.cx_st + (self.sizew-tw)/2 + DC.DrawText(month, adjust, self.cy_st + th) + + year = str(self.year) + tw,th = DC.GetTextExtent(year) + adjust = self.sizew - tw - self.x_mrg + + self.title_offset = th * 2 + + f = wx.Font(sizef, self.font, wx.NORMAL, self.bold) + DC.SetFont(f) + DC.DrawText(year, self.cx_st + adjust, self.cy_st + th) + + def DrawWeek(self, DC): # draw the week days + # increase by 1 to include all gridlines + width = self.gridx[1] - self.gridx[0] + 1 + height = self.gridy[1] - self.gridy[0] + 1 + rect_w = self.gridx[-1] - self.gridx[0] + + f = wx.Font(10, self.font, wx.NORMAL, self.bold) # initial font setting + + if self.week_auto == True: + test_size = self.max_week_size # max size + test_day = ' Sun ' + while test_size > 2: + f.SetPointSize(test_size) + DC.SetFont(f) + tw,th = DC.GetTextExtent(test_day) + + if tw < width and th < height: + break + + test_size = test_size - 1 + else: + f.SetPointSize(self.week_size) # set fixed size + DC.SetFont(f) + + DC.SetTextForeground(MakeColor(self.colors[COLOR_HEADER_FONT])) + + cnt_x = 0 + cnt_y = 0 + + brush = wx.Brush(MakeColor(self.colors[COLOR_HEADER_BACKGROUND]), wx.SOLID) + DC.SetBrush(brush) + + if self.cal_type == "NORMAL": + cal_days = CalDays + else: + cal_days = BusCalDays + + for val in cal_days: + if val == cal_days[-1]: + width = width + self.restW + + day = AbrWeekday[val] + + if self.sizew < 200: + day = day[0] + + dw,dh = DC.GetTextExtent(day) + + diffx = (width-dw)/2 + diffy = (height-dh)/2 + + x = self.gridx[cnt_x] + y = self.gridy[cnt_y] + pointXY = (x, y) + pointWH = (width, height) + if self.hide_grid == False: + pen = wx.Pen(MakeColor(self.GetColor(COLOR_GRID_LINES)), 1, wx.SOLID) + else: + pen = wx.Pen(MakeColor(self.GetColor(COLOR_BACKGROUND)), 1, wx.SOLID) + DC.SetPen(pen) + DC.DrawRectanglePointSize( pointXY, pointWH) + + old_pen = DC.GetPen() + + pen = wx.Pen(MakeColor(self.colors[COLOR_3D_LIGHT]), 1, wx.SOLID) + DC.SetPen(pen) + # draw the horizontal hilight + startPoint = wx.Point(x + 1 , y + 1) + endPoint = wx.Point(x + width - 1, y + 1) + DC.DrawLinePoint(startPoint, endPoint ) + + # draw the vertical hilight + startPoint = wx.Point(x + 1 , y + 1) + endPoint = wx.Point(x + 1, y + height - 2) + DC.DrawLinePoint(startPoint, endPoint ) + + pen = wx.Pen(MakeColor(self.colors[COLOR_3D_DARK]), 1, wx.SOLID) + DC.SetPen(pen) + + # draw the horizontal lowlight + startPoint = wx.Point(x + 1, y + height - 2) + endPoint = wx.Point(x + width - 1, y + height - 2) + DC.DrawLinePoint(startPoint, endPoint ) + + # draw the vertical lowlight + startPoint = wx.Point(x + width - 2 , y + 2) + endPoint = wx.Point(x + width - 2, y + height - 2) + DC.DrawLinePoint(startPoint, endPoint ) + + pen = wx.Pen(MakeColor(self.colors[COLOR_FONT]), 1, wx.SOLID) + + DC.SetPen(pen) + + point = (x+diffx, y+diffy) + DC.DrawTextPoint(day, point) + cnt_x = cnt_x + 1 + + def _CalcFontSize(self, DC, f): + if self.num_auto == True: + test_size = self.max_num_size # max size + test_day = ' 99 ' + + while test_size > 2: + f.SetPointSize(test_size) + DC.SetFont(f) + tw,th = DC.GetTextExtent(test_day) + + if tw < self.cellW and th < self.cellH: + sizef = test_size + break + test_size = test_size - 1 + else: + f.SetPointSize(self.num_size) # set fixed size + DC.SetFont(f) + + # draw the day numbers + def DrawNum(self, DC): + f = wx.Font(10, self.font, wx.NORMAL, self.bold) # initial font setting + self._CalcFontSize(DC, f) + + cnt_x = 0 + cnt_y = 1 + for val in self.cal_days: + x = self.gridx[cnt_x] + y = self.gridy[cnt_y] + + self._DrawDayText(x, y, val, f, DC) + + if cnt_x < 6: + cnt_x = cnt_x + 1 + else: + cnt_x = 0 + cnt_y = cnt_y + 1 + + def _DrawDayText(self, x, y, text, font, DC): + + try: + num_val = int(text) + num_color = self.cal_sel[num_val][0] + except: + num_color = self.colors[COLOR_FONT] + + DC.SetTextForeground(MakeColor(num_color)) + DC.SetFont(font) + + tw,th = DC.GetTextExtent(text) + + if self.num_align_horz == wx.ALIGN_CENTRE: + adj_h = (self.cellW - tw)/2 + elif self.num_align_horz == wx.ALIGN_RIGHT: + adj_h = self.cellW - tw + else: + adj_h = 0 # left alignment + + adj_h = adj_h + self.num_indent_horz + + if self.num_align_vert == wx.ALIGN_CENTRE: + adj_v = (self.cellH - th)/2 + elif self.num_align_vert == wx.ALIGN_BOTTOM: + adj_v = self.cellH - th + else: + adj_v = 0 # left alignment + + adj_v = adj_v + self.num_indent_vert + + DC.DrawTextPoint(text, (x+adj_h, y+adj_v)) + + def DrawDayText(self, DC, key): + f = wx.Font(10, self.font, wx.NORMAL, self.bold) # initial font setting + self._CalcFontSize(DC, f) + + if key > self.end_pos: + key = self.end_pos + + val = self.cal_days[key] + cnt_x = key % 7 + cnt_y = int(key / 7)+1 + x = self.gridx[cnt_x] + y = self.gridy[cnt_y] + self._DrawDayText(x, y, val, f, DC) + + + # calculate the dimensions in the center of the drawing area + def Center(self): + borderW = self.x_mrg * 2 + borderH = self.y_mrg + self.y_end + self.title_offset + + self.cellW = int((self.sizew - borderW)/7) + self.cellH = int((self.sizeh - borderH)/7) + + self.restW = ((self.sizew - borderW)%7 ) - 1 + + # week title adjustment + self.weekHdrCellH = int(self.cellH * self.cal_week_scale) + # recalculate the cell height exkl. the week header and + # subtracting the size + self.cellH = int((self.sizeh - borderH - self.weekHdrCellH)/6) + + self.restH = ((self.sizeh - borderH - self.weekHdrCellH)%6 ) - 1 + self.calW = self.cellW * 7 + self.calH = self.cellH * 6 + self.weekHdrCellH + + # highlighted selected days + def DrawSel(self, DC): + + for key in self.cal_sel.keys(): + sel_color = self.cal_sel[key][1] + brush = wx.Brush(MakeColor(sel_color), wx.SOLID) + DC.SetBrush(brush) + + if self.hide_grid is False: + DC.SetPen(wx.Pen(MakeColor(self.colors[COLOR_GRID_LINES]), 0)) + else: + DC.SetPen(wx.Pen(MakeColor(self.colors[COLOR_BACKGROUND]), 0)) + + nkey = key + self.st_pos -1 + rect = self.rg[nkey] + + DC.DrawRectangleRect(rect) + + # calculate and draw the grid lines + def DrawGrid(self, DC): + DC.SetPen(wx.Pen(MakeColor(self.colors[COLOR_GRID_LINES]), 0)) + + self.gridx = [] + self.gridy = [] + + self.x_st = self.cx_st + self.x_mrg + # start postion of draw + self.y_st = self.cy_st + self.y_mrg + self.title_offset + + x1 = self.x_st + y1 = self.y_st + y2 = y1 + self.calH + self.restH + + for i in range(8): + if i == 7: + x1 = x1 + self.restW + + if self.hide_grid is False: + DC.DrawLinePoint((x1, y1), (x1, y2)) + + self.gridx.append(x1) + + x1 = x1 + self.cellW + + x1 = self.x_st + y1 = self.y_st + x2 = x1 + self.calW + self.restW + + for i in range(8): + if i == 7: + y1 = y1 + self.restH + + if self.hide_grid is False: + DC.DrawLinePoint((x1, y1), (x2, y1)) + + self.gridy.append(y1) + + if i == 0: + y1 = y1 + self.weekHdrCellH + else: + y1 = y1 + self.cellH + + def GetColor(self, name): + return MakeColor(self.colors[name]) + + def SetColor(self, name, value): + self.colors[name] = MakeColor(value) + + +class PrtCalDraw(CalDraw): + def InitValues(self): + self.rg = {} + self.cal_sel = {} + # start draw border location + self.set_cx_st = 1.0 + self.set_cy_st = 1.0 + + # draw offset position + self.set_y_mrg = 0.2 + self.set_x_mrg = 0.2 + self.set_y_end = 0.2 + + # calculate the dimensions in the center of the drawing area + def SetPSize(self, pwidth, pheight): + self.pwidth = int(pwidth)/self.scale + self.pheight = int(pheight)/self.scale + + def SetPreview(self, preview): + self.preview = preview + + + +class Calendar( wx.PyControl ): + def __init__(self, parent, id=-1, pos=wx.DefaultPosition, size=wx.Size(200,200), + style= 0, validator=wx.DefaultValidator, + name= "calendar"): + wx.PyControl.__init__(self, parent, id, pos, size, style | wx.WANTS_CHARS, validator, name) + + self.hasFocus = False + # set the calendar control attributes + + self.hide_grid = False + self.hide_title = False + self.show_weekend = False + self.cal_type = "NORMAL" + self.outer_border = True + self.num_align_horz = wx.ALIGN_CENTRE + self.num_align_vert = wx.ALIGN_CENTRE + self.colors = DefaultColors() + self.set_x_mrg = 1 + self.set_y_mrg = 1 + self.set_y_end = 1 + + self.select_list = [] + + self.SetBackgroundColour(MakeColor(self.colors[COLOR_BACKGROUND])) + self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftEvent) + self.Bind(wx.EVT_LEFT_DCLICK, self.OnLeftDEvent) + self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightEvent) + self.Bind(wx.EVT_RIGHT_DCLICK, self.OnRightDEvent) + self.Bind(wx.EVT_MIDDLE_DOWN, self.OnMiddleEvent) + self.Bind(wx.EVT_MIDDLE_DCLICK, self.OnMiddleDEvent) + self.Bind(wx.EVT_SET_FOCUS, self.OnSetFocus) + self.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus) + self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown) + + self.sel_key = None # last used by + self.sel_lst = [] # highlighted selected days + + # default calendar for current month + self.SetNow() + + self.size = None + self.set_day = None + + self.Bind(wx.EVT_PAINT, self.OnPaint) + self.Bind(wx.EVT_SIZE, self.OnSize) + + def AcceptsFocus(self): + return self.IsShown() and self.IsEnabled() + + def GetColor(self, name): + return MakeColor(self.colors[name]) + + def SetColor(self, name, value): + self.colors[name] = MakeColor(value) + + # control some of the main calendar attributes + + def HideTitle(self): + self.hide_title = True + + def HideGrid(self): + self.hide_grid = True + + # determine the calendar rectangle click area and draw a selection + + def ProcessClick(self, event): + self.SetFocus() + self.x, self.y = event.GetX(), event.GetY() + self.shiftkey = event.ShiftDown() + self.ctrlkey = event.ControlDown() + key = self.GetDayHit(self.x, self.y) + self.SelectDay(key) + + # tab mouse click events and process + + def OnLeftEvent(self, event): + self.click = 'LEFT' + self.ProcessClick(event) + + def OnLeftDEvent(self, event): + self.click = 'DLEFT' + self.ProcessClick(event) + + def OnRightEvent(self, event): + self.click = 'RIGHT' + self.ProcessClick(event) + + def OnRightDEvent(self, event): + self.click = 'DRIGHT' + self.ProcessClick(event) + + def OnMiddleEvent(self, event): + self.click = 'MIDDLE' + self.ProcessClick(event) + + def OnMiddleDEvent(self, event): + self.click = 'DMIDDLE' + self.ProcessClick(event) + + def OnSetFocus(self, event): + self.hasFocus = True + self.DrawFocusIndicator(True) + + def OnKillFocus(self, event): + self.hasFocus = False + self.DrawFocusIndicator(False) + + def OnKeyDown(self, event): + if not self.hasFocus: + event.Skip() + return + + key_code = event.GetKeyCode() + + if key_code == wx.WXK_TAB: + forward = not event.ShiftDown() + ne = wx.NavigationKeyEvent() + ne.SetDirection(forward) + ne.SetCurrentFocus(self) + ne.SetEventObject(self) + self.GetParent().GetEventHandler().ProcessEvent(ne) + event.Skip() + return + + delta = None + + if key_code == wx.WXK_UP: + delta = -7 + elif key_code == wx.WXK_DOWN: + delta = 7 + elif key_code == wx.WXK_LEFT: + delta = -1 + elif key_code == wx.WXK_RIGHT: + delta = 1 + elif key_code == wx.WXK_HOME: + curDate = wx.DateTimeFromDMY(int(self.cal_days[self.sel_key]),self.month - 1,self.year) + newDate = wx.DateTime_Now() + ts = newDate - curDate + delta = ts.GetDays() + + if delta <> None: + curDate = wx.DateTimeFromDMY(int(self.cal_days[self.sel_key]),self.month - 1,self.year) + timeSpan = wx.TimeSpan_Days(delta) + newDate = curDate + timeSpan + + if curDate.GetMonth() == newDate.GetMonth(): + self.set_day = newDate.GetDay() + key = self.sel_key + delta + self.SelectDay(key) + else: + self.month = newDate.GetMonth() + 1 + self.year = newDate.GetYear() + self.set_day = newDate.GetDay() + self.sel_key = None + self.DoDrawing(wx.ClientDC(self)) + + event.Skip() + + def SetSize(self, set_size): + self.size = set_size + + def SetSelDay(self, sel): + # list of highlighted days + self.sel_lst = sel + + # get the current date + def SetNow(self): + dt = now() + self.month = dt.month + self.year = dt.year + self.day = dt.day + + # set the current day + def SetCurrentDay(self): + self.SetNow() + self.set_day = self.day + + # get the date, day, month, year set in calendar + + def GetDate(self): + return self.day, self.month, self.year + + def GetDay(self): + return self.day + + def GetMonth(self): + return self.month + + def GetYear(self): + return self.year + + # set the day, month, and year + + def SetDayValue(self, day): + self.set_day = day + self.day = day + + def SetMonth(self, month): + if month >= 1 and month <= 12: + self.month = month + else: + self.month = 1 + self.set_day = None + + def SetYear(self, year): + self.year = year + + # increment year and month + + def IncYear(self): + self.year = self.year + 1 + self.set_day = None + + def DecYear(self): + self.year = self.year - 1 + self.set_day = None + + def IncMonth(self): + self.month = self.month + 1 + if self.month > 12: + self.month = 1 + self.year = self.year + 1 + self.set_day = None + + def DecMonth(self): + self.month = self.month - 1 + if self.month < 1: + self.month = 12 + self.year = self.year - 1 + self.set_day = None + + # test to see if the selection has a date and create event + + def TestDay(self, key): + try: + self.day = int(self.cal_days[key]) + except: + return None + + if self.day == "": + return None + else: + # Changed 12/1/03 by jmg (see above) to support 2.5 event binding + evt = wx.PyCommandEvent(wxEVT_COMMAND_PYCALENDAR_DAY_CLICKED, self.GetId()) + evt.click, evt.day, evt.month, evt.year = self.click, self.day, self.month, self.year + evt.shiftkey = self.shiftkey + evt.ctrlkey = self.ctrlkey + self.GetEventHandler().ProcessEvent(evt) + + self.set_day = self.day + return key + + # find the clicked area rectangle + + def GetDayHit(self, mx, my): + for key in self.rg.keys(): + val = self.rg[key] + ms_rect = wx.Rect(mx, my, 1, 1) + if wx.IntersectRect(ms_rect, val) is not None: + result = self.TestDay(key) + return result + + return None + + # calendar drawing + + def SetWeekColor(self, font_color, week_color): + # set font and background color for week title + self.colors[COLOR_HEADER_FONT] = MakeColor(font_color) + self.colors[COLOR_HEADER_BACKGROUND] = MakeColor(week_color) + self.colors[COLOR_3D_LIGHT] = MakeColor(week_color) + self.colors[COLOR_3D_DARK] = MakeColor(week_color) + + def SetTextAlign(self, vert, horz): + self.num_align_horz = horz + self.num_align_vert = vert + + def AddSelect(self, list, font_color, back_color): + list_val = [list, font_color, back_color] + self.select_list.append(list_val) + + def ShowWeekEnd(self): + # highlight weekend + self.show_weekend = True + + def SetBusType(self): + self.cal_type = "BUS" + + def OnSize(self, evt): + self.Refresh(False) + evt.Skip() + + def OnPaint(self, event): + DC = wx.PaintDC(self) + self.DoDrawing(DC) + + def DoDrawing(self, DC): + #DC = wx.PaintDC(self) + DC.BeginDrawing() + + try: + cal = self.caldraw + except: + self.caldraw = CalDraw(self) + cal = self.caldraw + + cal.hide_grid = self.hide_grid + cal.hide_title = self.hide_title + cal.show_weekend = self.show_weekend + cal.cal_type = self.cal_type + cal.outer_border = self.outer_border + cal.num_align_horz = self.num_align_horz + cal.num_align_vert = self.num_align_vert + cal.colors = self.colors + + if self.size is None: + size = self.GetClientSize() + else: + size = self.size + + # drawing attributes + + cal.SetSize(size) + cal.SetCal(self.year, self.month) + + # these have to set after SetCal as SetCal would overwrite them again. + cal.set_x_mrg = self.set_x_mrg + cal.set_y_mrg = self.set_y_mrg + cal.set_y_end = self.set_y_end + + for val in self.select_list: + cal.AddSelect(val[0], val[1], val[2]) + + cal.DrawCal(DC, self.sel_lst) + + self.rg = cal.GetRect() + self.cal_days = cal.GetCal() + self.st_pos = cal.GetOffset() + self.ymax = DC.MaxY() + + if self.set_day != None: + self.SetDay(self.set_day) + + DC.EndDrawing() + + # draw the selection rectangle + def DrawFocusIndicator(self, draw): + DC = wx.ClientDC(self) + try: + if draw == True: + self.caldraw.DrawFocusIndicator(DC) + else: + self.caldraw.DrawBorder(DC,True) + except: + pass + + def DrawRect(self, key, bgcolor = 'WHITE', fgcolor= 'PINK',width = 0): + if key == None: + return + + DC = wx.ClientDC(self) + DC.BeginDrawing() + + brush = wx.Brush(MakeColor(bgcolor)) + DC.SetBrush(brush) + + DC.SetPen(wx.TRANSPARENT_PEN) + + rect = self.rg[key] + DC.DrawRectangle(rect.x+1, rect.y+1, rect.width-2, rect.height-2) + + self.caldraw.DrawDayText(DC,key) + + DC.EndDrawing() + + def DrawRectOrg(self, key, fgcolor = 'BLACK', width = 0): + if key == None: + return + + DC = wx.ClientDC(self) + DC.BeginDrawing() + + brush = wx.Brush(wx.Colour(0, 0xFF, 0x80), wx.TRANSPARENT) + DC.SetBrush(brush) + + try: + DC.SetPen(wx.Pen(MakeColor(fgcolor), width)) + except: + DC.SetPen(wx.Pen(MakeColor(self.GetColor(COLOR_GRID_LINES)), width)) + + rect = self.rg[key] + DC.DrawRectangleRect(rect) + + DC.EndDrawing() + + # set the day selection + + def SetDay(self, day): + d = day + self.st_pos - 1 + self.SelectDay(d) + + def IsDayInWeekend(self, key): + try: + t = Date(self.year, self.month, 1) + + day = self.cal_days[key] + day = int(day) + t.day_of_week + + if day % 7 == 6 or day % 7 == 0: + return True + except: + return False + + def SelectDay(self, key): + sel_size = 1 + # clear large selection + + if self.sel_key != None: + (cfont, bgcolor) = self.__GetColorsForDay(self.sel_key) + self.DrawRect(self.sel_key, bgcolor,cfont, sel_size) + + self.DrawRect(key, self.GetColor(COLOR_HIGHLIGHT_BACKGROUND), self.GetColor(COLOR_HIGHLIGHT_FONT), sel_size) + # store last used by + self.sel_key = key + + def SetMargin(self, xmarg, ymarg): + self.set_x_mrg = xmarg + self.set_y_mrg = ymarg + self.set_y_end = ymarg + def __GetColorsForDay(self, key): + cfont = self.GetColor(COLOR_FONT) + bgcolor = self.GetColor(COLOR_BACKGROUND) + + if self.IsDayInWeekend(key) is True and self.show_weekend is True: + cfont = self.GetColor(COLOR_WEEKEND_FONT) + bgcolor = self.GetColor(COLOR_WEEKEND_BACKGROUND) + + try: + dayIdx = int(self.cal_days[key]) + (cfont, bgcolor) = self.caldraw.cal_sel[dayIdx] + except: + pass + + return (cfont, bgcolor) + +class CalenDlg(wx.Dialog): + def __init__(self, parent, month=None, day = None, year=None): + wx.Dialog.__init__(self, parent, -1, "Event Calendar", wx.DefaultPosition, (280, 360)) + self.result = None + + # set the calendar and attributes + self.calend = Calendar(self, -1, (20, 60), (240, 200)) + + if month == None: + self.calend.SetCurrentDay() + start_month = self.calend.GetMonth() + start_year = self.calend.GetYear() + else: + self.calend.month = start_month = month + self.calend.year = start_year = year + self.calend.SetDayValue(day) + + self.calend.HideTitle() + self.ResetDisplay() + + # get month list from DateTime + monthlist = GetMonthList() + + # select the month + self.date = wx.ComboBox(self, -1, Month[start_month], (20, 20), (90, -1), + monthlist, wx.CB_DROPDOWN) + self.Bind(wx.EVT_COMBOBOX, self.EvtComboBox, self.date) + + # alternate spin button to control the month + h = self.date.GetSize().height + self.m_spin = wx.SpinButton(self, -1, (115, 20), (h*1.5, h), wx.SP_VERTICAL) + self.m_spin.SetRange(1, 12) + self.m_spin.SetValue(start_month) + self.Bind(wx.EVT_SPIN, self.OnMonthSpin, self.m_spin) + + # spin button to control the year + self.dtext = wx.TextCtrl(self, -1, str(start_year), (160, 20), (60, -1)) + h = self.dtext.GetSize().height + + self.y_spin = wx.SpinButton(self, -1, (225, 20), (h*1.5, h), wx.SP_VERTICAL) + self.y_spin.SetRange(1980, 2010) + self.y_spin.SetValue(start_year) + + self.Bind(wx.EVT_SPIN, self.OnYrSpin, self.y_spin) + self.Bind(EVT_CALENDAR, self.MouseClick, self.calend) + + x_pos = 50 + y_pos = 280 + but_size = (60, 25) + + btn = wx.Button(self, wx.ID_OK, ' Ok ', (x_pos, y_pos), but_size) + self.Bind(wx.EVT_BUTTON, self.OnOk, btn) + + btn = wx.Button(self, wx.ID_CANCEL, ' Close ', (x_pos + 120, y_pos), but_size) + self.Bind(wx.EVT_BUTTON, self.OnCancel, btn) + + def OnOk(self, evt): + self.result = ['None', str(self.calend.day), Month[self.calend.month], str(self.calend.year)] + self.EndModal(wx.ID_OK) + + def OnCancel(self, event): + self.EndModal(wx.ID_CANCEL) + + # log the mouse clicks + def MouseClick(self, evt): + self.month = evt.month + # result click type and date + self.result = [evt.click, str(evt.day), Month[evt.month], str(evt.year)] + + if evt.click == 'DLEFT': + self.EndModal(wx.ID_OK) + + # month and year spin selection routines + def OnMonthSpin(self, event): + month = event.GetPosition() + self.date.SetValue(Month[month]) + self.calend.SetMonth(month) + self.calend.Refresh() + + def OnYrSpin(self, event): + year = event.GetPosition() + self.dtext.SetValue(str(year)) + self.calend.SetYear(year) + self.calend.Refresh() + + def EvtComboBox(self, event): + name = event.GetString() + monthval = self.date.FindString(name) + self.m_spin.SetValue(monthval+1) + + self.calend.SetMonth(monthval+1) + self.ResetDisplay() + + # set the calendar for highlighted days + + def ResetDisplay(self): + month = self.calend.GetMonth() + self.calend.Refresh() + + + diff --git a/wx/lib/colourchooser/__init__.py b/wx/lib/colourchooser/__init__.py new file mode 100644 index 00000000..ccce2e3c --- /dev/null +++ b/wx/lib/colourchooser/__init__.py @@ -0,0 +1,36 @@ +""" +PyColourChooser +Copyright (C) 2002 Michael Gilfix + +This file is part of PyColourChooser. + +This version of PyColourChooser is open source; you can redistribute it +and/or modify it under the licensed terms. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +""" + +# 12/14/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o 2.5 compatability update. +# +# 12/21/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o wxPyColorChooser -> PyColorChooser +# o wxPyColourChooser -> PyColourChooser +# + +from pycolourchooser import * + +# For the American in you +PyColorChooser = PyColourChooser + +__all__ = [ + 'canvas', + 'pycolourbox', + 'pycolourchooser', + 'pycolourslider', + 'pypalette', +] diff --git a/wx/lib/colourchooser/canvas.py b/wx/lib/colourchooser/canvas.py new file mode 100644 index 00000000..cf9af063 --- /dev/null +++ b/wx/lib/colourchooser/canvas.py @@ -0,0 +1,143 @@ +""" +PyColourChooser +Copyright (C) 2002 Michael Gilfix + +This file is part of PyColourChooser. + +This version of PyColourChooser is open source; you can redistribute it +and/or modify it under the licensed terms. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +""" + +# 12/14/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o 2.5 compatability update. +# +# 12/21/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o wxPyColorChooser -> PyColorChooser +# o wxPyColourChooser -> PyColourChooser +# + +import wx + +class BitmapBuffer(wx.MemoryDC): + """A screen buffer class. + + This class implements a screen output buffer. Data is meant to + be drawn in the buffer class and then blitted directly to the + output device, or on-screen window. + """ + def __init__(self, width, height, colour): + """Initialize the empty buffer object.""" + wx.MemoryDC.__init__(self) + + self.width = width + self.height = height + self.colour = colour + + self.bitmap = wx.EmptyBitmap(self.width, self.height) + self.SelectObject(self.bitmap) + + # Initialize the buffer to the background colour + self.SetBackground(wx.Brush(self.colour, wx.SOLID)) + self.Clear() + + # Make each logical unit of the buffer equal to 1 pixel + self.SetMapMode(wx.MM_TEXT) + + def GetBitmap(self): + """Returns the internal bitmap for direct drawing.""" + return self.bitmap + + # GetPixel seems to always return (-1, -1, -1, 255) + # on OS X so this is a workaround for that issue. + def GetPixelColour(self, x, y): + """Gets the color value of the pixel at the given + cords. + + """ + img = self.GetAsBitmap().ConvertToImage() + red = img.GetRed(x, y) + green = img.GetGreen(x, y) + blue = img.GetBlue(x, y) + return wx.Colour(red, green, blue) + +class Canvas(wx.Window): + """A canvas class for arbitrary drawing. + + The Canvas class implements a window that allows for drawing + arbitrary graphics. It implements a double buffer scheme and + blits the off-screen buffer to the window during paint calls + by the windowing system for speed. + + Some other methods for determining the canvas colour and size + are also provided. + """ + def __init__(self, parent, id, + pos=wx.DefaultPosition, + size=wx.DefaultSize, + style=wx.SIMPLE_BORDER): + """Creates a canvas instance and initializes the off-screen + buffer. Also sets the handler for rendering the canvas + automatically via size and paint calls from the windowing + system.""" + wx.Window.__init__(self, parent, id, pos, size, style) + + # Perform an intial sizing + self.ReDraw() + + # Register event handlers + self.Bind(wx.EVT_SIZE, self.onSize) + self.Bind(wx.EVT_PAINT, self.onPaint) + + def MakeNewBuffer(self): + size = self.GetSize() + self.buffer = BitmapBuffer(size[0], size[1], + self.GetBackgroundColour()) + + def onSize(self, event): + """Perform actual redraw to off-screen buffer only when the + size of the canvas has changed. This saves a lot of computation + since the same image can be re-used, provided the canvas size + hasn't changed.""" + self.MakeNewBuffer() + self.DrawBuffer() + self.Refresh() + + def ReDraw(self): + """Explicitly tells the canvas to redraw it's contents.""" + self.onSize(None) + + def Refresh(self): + """Re-draws the buffer contents on-screen.""" + dc = wx.ClientDC(self) + self.Blit(dc) + + def onPaint(self, event): + """Renders the off-screen buffer on-screen.""" + dc = wx.PaintDC(self) + self.Blit(dc) + + def Blit(self, dc): + """Performs the blit of the buffer contents on-screen.""" + width, height = self.buffer.GetSize() + dc.BeginDrawing() + dc.Blit(0, 0, width, height, self.buffer, 0, 0) + dc.EndDrawing() + + def GetBoundingRect(self): + """Returns a tuple that contains the co-ordinates of the + top-left and bottom-right corners of the canvas.""" + x, y = self.GetPosition() + w, h = self.GetSize() + return(x, y + h, x + w, y) + + def DrawBuffer(self): + """Actual drawing function for drawing into the off-screen + buffer. To be overrideen in the implementing class. Do nothing + by default.""" + pass diff --git a/wx/lib/colourchooser/intl.py b/wx/lib/colourchooser/intl.py new file mode 100644 index 00000000..b10d291a --- /dev/null +++ b/wx/lib/colourchooser/intl.py @@ -0,0 +1,24 @@ +""" +PyColourChooser +Copyright (C) 2002 Michael Gilfix + +This file is part of PyColourChooser. + +This version of PyColourChooser is open source; you can redistribute it +and/or modify it under the licensed terms. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +""" + +try: + import gettext + + gettext.bindtextdomain('pycolourchooser') + gettext.textdomain('pycolourchooser') + _ = gettext.gettext +except Exception, strerror: + print "Warning: Couldn't import translation function: %(str)s" %{ 'str' : strerror } + print "Defaulting to En" + _ = lambda x: x diff --git a/wx/lib/colourchooser/pycolourbox.py b/wx/lib/colourchooser/pycolourbox.py new file mode 100644 index 00000000..a1108c7b --- /dev/null +++ b/wx/lib/colourchooser/pycolourbox.py @@ -0,0 +1,87 @@ +""" +PyColourChooser +Copyright (C) 2002 Michael Gilfix + +This file is part of PyColourChooser. + +This version of PyColourChooser is open source; you can redistribute it +and/or modify it under the licensed terms. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +""" +# 12/14/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o 2.5 compatability update. +# +# 12/21/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o wxPyColorChooser -> PyColorChooser +# o wxPyColourChooser -> PyColourChooser +# + +import wx + +class PyColourBox(wx.Panel): + """A Colour Selection Box + + The Colour selection box implements button like behavior but contains + a solid-filled, coloured sub-box. Placing the colour in a sub-box allows + for filling in the main panel's background for a high-lighting effect. + """ + def __init__(self, parent, id, colour=(0, 0, 0), size=(25, 20)): + """Creates a new colour box instance and initializes the colour + content.""" + wx.Panel.__init__(self, parent, id, size=size, style=wx.NO_BORDER) + + self.colour_box = wx.Window(self, -1, style=wx.SIMPLE_BORDER) + + sizer = wx.GridSizer(1, 1) + sizer.Add(self.colour_box, 0, wx.FIXED_MINSIZE | wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_CENTER_HORIZONTAL) + sizer.SetItemMinSize(self.colour_box, size[0] - 5, size[1] - 5) + self.SetAutoLayout(True) + self.SetSizer(sizer) + self.Layout() + + self.real_bg = self.GetBackgroundColour() + self.SetColourTuple(colour) + + def GetColourBox(self): + """Returns a reference to the internal box object containing the + color. This function is useful for setting up event handlers for + the box.""" + return self.colour_box + + def GetColour(self): + """Returns a wxColour object indicating the box's current colour.""" + return self.colour_box.GetBackgroundColour() + + def SetColour(self, colour): + """Accepts a wxColour object and sets the box's current color.""" + self.colour_box.SetBackgroundColour(colour) + self.colour_box.Refresh() + + def SetColourTuple(self, colour): + """Sets the box's current couple to the given tuple.""" + self.colour = colour + self.colour_box.SetBackgroundColour(wx.Colour(*self.colour)) + + def Update(self): + wx.Panel.Update(self) + self.colour_box.Update() + + def SetHighlight(self, val): + """Accepts a boolean 'val' toggling the box's highlighting.""" + # XXX This code has been disabled for now until I can figure out + # how to get this to work reliably across all platforms. + if val: + #A wxColourPtr is returned in windows, making this difficult + red =(self.real_bg.Red() - 45) % 255 + green =(self.real_bg.Green() - 45) % 255 + blue =(self.real_bg.Blue() - 45) % 255 + new_colour = wx.Colour(red, green, blue) + self.SetBackgroundColour(new_colour) + else: + self.SetBackgroundColour(self.real_bg) + self.Refresh() diff --git a/wx/lib/colourchooser/pycolourchooser.py b/wx/lib/colourchooser/pycolourchooser.py new file mode 100644 index 00000000..442eb743 --- /dev/null +++ b/wx/lib/colourchooser/pycolourchooser.py @@ -0,0 +1,413 @@ +""" +PyColourChooser +Copyright (C) 2002 Michael Gilfix + +This file is part of PyColourChooser. + +This version of PyColourChooser is open source; you can redistribute it +and/or modify it under the licensed terms. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +""" + +# 12/14/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o 2.5 compatability update. +# +# 12/21/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o wxPyColorChooser -> PyColorChooser +# o wxPyColourChooser -> PyColourChooser +# o Added wx.InitAllImageHandlers() to test code since +# that's where it belongs. +# + +import wx + +import pycolourbox +import pypalette +import pycolourslider +import colorsys +import intl + +from intl import _ # _ + +class PyColourChooser(wx.Panel): + """A Pure-Python implementation of the colour chooser dialog. + + The PyColourChooser is a pure python implementation of the colour + chooser dialog. It's useful for embedding the colour choosing functionality + inside other widgets, when the pop-up dialog is undesirable. It can also + be used as a drop-in replacement on the GTK platform, as the native + dialog is kind of ugly. + """ + + colour_names = [ + 'ORANGE', + 'GOLDENROD', + 'WHEAT', + 'SPRING GREEN', + 'SKY BLUE', + 'SLATE BLUE', + 'MEDIUM VIOLET RED', + 'PURPLE', + + 'RED', + 'YELLOW', + 'MEDIUM SPRING GREEN', + 'PALE GREEN', + 'CYAN', + 'LIGHT STEEL BLUE', + 'ORCHID', + 'LIGHT MAGENTA', + + 'BROWN', + 'YELLOW', + 'GREEN', + 'CADET BLUE', + 'MEDIUM BLUE', + 'MAGENTA', + 'MAROON', + 'ORANGE RED', + + 'FIREBRICK', + 'CORAL', + 'FOREST GREEN', + 'AQUAMARINE', + 'BLUE', + 'NAVY', + 'THISTLE', + 'MEDIUM VIOLET RED', + + 'INDIAN RED', + 'GOLD', + 'MEDIUM SEA GREEN', + 'MEDIUM BLUE', + 'MIDNIGHT BLUE', + 'GREY', + 'PURPLE', + 'KHAKI', + + 'BLACK', + 'MEDIUM FOREST GREEN', + 'KHAKI', + 'DARK GREY', + 'SEA GREEN', + 'LIGHT GREY', + 'MEDIUM SLATE BLUE', + 'WHITE', + ] + + # Generate the custom colours. These colours are shared across + # all instances of the colour chooser + NO_CUSTOM_COLOURS = 16 + custom_colours = [ (wx.Colour(255, 255, 255), + pycolourslider.PyColourSlider.HEIGHT / 2) + ] * NO_CUSTOM_COLOURS + last_custom = 0 + + idADD_CUSTOM = wx.NewId() + idSCROLL = wx.NewId() + + def __init__(self, parent, id): + """Creates an instance of the colour chooser. Note that it is best to + accept the given size of the colour chooser as it is currently not + resizeable.""" + wx.Panel.__init__(self, parent, id) + + self.basic_label = wx.StaticText(self, -1, _("Basic Colours:")) + self.custom_label = wx.StaticText(self, -1, _("Custom Colours:")) + self.add_button = wx.Button(self, self.idADD_CUSTOM, _("Add to Custom Colours")) + + self.Bind(wx.EVT_BUTTON, self.onAddCustom, self.add_button) + + # Since we're going to be constructing widgets that require some serious + # computation, let's process any events (like redraws) right now + wx.Yield() + + # Create the basic colours palette + self.colour_boxs = [ ] + colour_grid = wx.GridSizer(6, 8) + for name in self.colour_names: + new_id = wx.NewId() + box = pycolourbox.PyColourBox(self, new_id) + + box.GetColourBox().Bind(wx.EVT_LEFT_DOWN, lambda x, b=box: self.onBasicClick(x, b)) + + self.colour_boxs.append(box) + colour_grid.Add(box, 0, wx.EXPAND) + + # Create the custom colours palette + self.custom_boxs = [ ] + custom_grid = wx.GridSizer(2, 8) + for wxcolour, slidepos in self.custom_colours: + new_id = wx.NewId() + custom = pycolourbox.PyColourBox(self, new_id) + + custom.GetColourBox().Bind(wx.EVT_LEFT_DOWN, lambda x, b=custom: self.onCustomClick(x, b)) + + custom.SetColour(wxcolour) + custom_grid.Add(custom, 0, wx.EXPAND) + self.custom_boxs.append(custom) + + csizer = wx.BoxSizer(wx.VERTICAL) + csizer.Add((1, 25)) + csizer.Add(self.basic_label, 0, wx.EXPAND) + csizer.Add((1, 5)) + csizer.Add(colour_grid, 0, wx.EXPAND) + csizer.Add((1, 25)) + csizer.Add(self.custom_label, 0, wx.EXPAND) + csizer.Add((1, 5)) + csizer.Add(custom_grid, 0, wx.EXPAND) + csizer.Add((1, 5)) + csizer.Add(self.add_button, 0, wx.EXPAND) + + self.palette = pypalette.PyPalette(self, -1) + self.colour_slider = pycolourslider.PyColourSlider(self, -1) + self.slider = wx.Slider( + self, self.idSCROLL, 86, 0, self.colour_slider.HEIGHT - 1, + style=wx.SL_VERTICAL, size=(15, self.colour_slider.HEIGHT) + ) + + self.Bind(wx.EVT_COMMAND_SCROLL, self.onScroll, self.slider) + psizer = wx.BoxSizer(wx.HORIZONTAL) + psizer.Add(self.palette, 0, 0) + psizer.Add((10, 1)) + psizer.Add(self.colour_slider, 0, wx.ALIGN_CENTER_VERTICAL) + psizer.Add(self.slider, 0, wx.ALIGN_CENTER_VERTICAL) + + # Register mouse events for dragging across the palette + self.palette.Bind(wx.EVT_LEFT_DOWN, self.onPaletteDown) + self.palette.Bind(wx.EVT_LEFT_UP, self.onPaletteUp) + self.palette.Bind(wx.EVT_MOTION, self.onPaletteMotion) + self.mouse_down = False + + self.solid = pycolourbox.PyColourBox(self, -1, size=(75, 50)) + slabel = wx.StaticText(self, -1, _("Solid Colour")) + ssizer = wx.BoxSizer(wx.VERTICAL) + ssizer.Add(self.solid, 0, 0) + ssizer.Add((1, 2)) + ssizer.Add(slabel, 0, wx.ALIGN_CENTER_HORIZONTAL) + + hlabel = wx.StaticText(self, -1, _("H:")) + self.hentry = wx.TextCtrl(self, -1) + self.hentry.SetSize((40, -1)) + slabel = wx.StaticText(self, -1, _("S:")) + self.sentry = wx.TextCtrl(self, -1) + self.sentry.SetSize((40, -1)) + vlabel = wx.StaticText(self, -1, _("V:")) + self.ventry = wx.TextCtrl(self, -1) + self.ventry.SetSize((40, -1)) + hsvgrid = wx.FlexGridSizer(1, 6, 2, 2) + hsvgrid.AddMany ([ + (hlabel, 0, wx.ALIGN_CENTER_VERTICAL), (self.hentry, 0, wx.FIXED_MINSIZE), + (slabel, 0, wx.ALIGN_CENTER_VERTICAL), (self.sentry, 0, wx.FIXED_MINSIZE), + (vlabel, 0, wx.ALIGN_CENTER_VERTICAL), (self.ventry, 0, wx.FIXED_MINSIZE), + ]) + + rlabel = wx.StaticText(self, -1, _("R:")) + self.rentry = wx.TextCtrl(self, -1) + self.rentry.SetSize((40, -1)) + glabel = wx.StaticText(self, -1, _("G:")) + self.gentry = wx.TextCtrl(self, -1) + self.gentry.SetSize((40, -1)) + blabel = wx.StaticText(self, -1, _("B:")) + self.bentry = wx.TextCtrl(self, -1) + self.bentry.SetSize((40, -1)) + lgrid = wx.FlexGridSizer(1, 6, 2, 2) + lgrid.AddMany([ + (rlabel, 0, wx.ALIGN_CENTER_VERTICAL), (self.rentry, 0, wx.FIXED_MINSIZE), + (glabel, 0, wx.ALIGN_CENTER_VERTICAL), (self.gentry, 0, wx.FIXED_MINSIZE), + (blabel, 0, wx.ALIGN_CENTER_VERTICAL), (self.bentry, 0, wx.FIXED_MINSIZE), + ]) + + gsizer = wx.GridSizer(2, 1) + gsizer.SetVGap (10) + gsizer.SetHGap (2) + gsizer.Add(hsvgrid, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_CENTER_HORIZONTAL) + gsizer.Add(lgrid, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_CENTER_HORIZONTAL) + + hsizer = wx.BoxSizer(wx.HORIZONTAL) + hsizer.Add(ssizer, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_CENTER_HORIZONTAL) + hsizer.Add(gsizer, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_CENTER_HORIZONTAL) + + vsizer = wx.BoxSizer(wx.VERTICAL) + vsizer.Add((1, 5)) + vsizer.Add(psizer, 0, 0) + vsizer.Add((1, 15)) + vsizer.Add(hsizer, 0, wx.EXPAND) + + sizer = wx.BoxSizer(wx.HORIZONTAL) + sizer.Add((5, 1)) + sizer.Add(csizer, 0, wx.EXPAND) + sizer.Add((10, 1)) + sizer.Add(vsizer, 0, wx.EXPAND) + self.SetAutoLayout(True) + self.SetSizer(sizer) + sizer.Fit(self) + + self.InitColours() + self.UpdateColour(self.solid.GetColour()) + + def InitColours(self): + """Initializes the pre-set palette colours.""" + for i in range(len(self.colour_names)): + colour = wx.TheColourDatabase.FindColour(self.colour_names[i]) + self.colour_boxs[i].SetColourTuple((colour.Red(), + colour.Green(), + colour.Blue())) + + def onBasicClick(self, event, box): + """Highlights the selected colour box and updates the solid colour + display and colour slider to reflect the choice.""" + if hasattr(self, '_old_custom_highlight'): + self._old_custom_highlight.SetHighlight(False) + if hasattr(self, '_old_colour_highlight'): + self._old_colour_highlight.SetHighlight(False) + box.SetHighlight(True) + self._old_colour_highlight = box + self.UpdateColour(box.GetColour()) + + def onCustomClick(self, event, box): + """Highlights the selected custom colour box and updates the solid + colour display and colour slider to reflect the choice.""" + if hasattr(self, '_old_colour_highlight'): + self._old_colour_highlight.SetHighlight(False) + if hasattr(self, '_old_custom_highlight'): + self._old_custom_highlight.SetHighlight(False) + box.SetHighlight(True) + self._old_custom_highlight = box + + # Update the colour panel and then the slider accordingly + box_index = self.custom_boxs.index(box) + base_colour, slidepos = self.custom_colours[box_index] + self.UpdateColour(box.GetColour()) + self.slider.SetValue(slidepos) + + def onAddCustom(self, event): + """Adds a custom colour to the custom colour box set. Boxes are + chosen in a round-robin fashion, eventually overwriting previously + added colours.""" + # Store the colour and slider position so we can restore the + # custom colours just as they were + self.setCustomColour(self.last_custom, + self.solid.GetColour(), + self.colour_slider.GetBaseColour(), + self.slider.GetValue()) + self.last_custom = (self.last_custom + 1) % self.NO_CUSTOM_COLOURS + + def setCustomColour (self, index, true_colour, base_colour, slidepos): + """Sets the custom colour at the given index. true_colour is wxColour + object containing the actual rgb value of the custom colour. + base_colour (wxColour) and slidepos (int) are used to configure the + colour slider and set everything to its original position.""" + self.custom_boxs[index].SetColour(true_colour) + self.custom_colours[index] = (base_colour, slidepos) + + def UpdateColour(self, colour): + """Performs necessary updates for when the colour selection has + changed.""" + # Reset the palette to erase any highlighting + self.palette.ReDraw() + + # Set the color info + self.solid.SetColour(colour) + self.colour_slider.SetBaseColour(colour) + self.colour_slider.ReDraw() + self.slider.SetValue(0) + self.UpdateEntries(colour) + + def UpdateEntries(self, colour): + """Updates the color levels to display the new values.""" + # Temporary bindings + r = colour.Red() + g = colour.Green() + b = colour.Blue() + + # Update the RGB entries + self.rentry.SetValue(str(r)) + self.gentry.SetValue(str(g)) + self.bentry.SetValue(str(b)) + + # Convert to HSV + h,s,v = colorsys.rgb_to_hsv(r / 255.0, g / 255.0, b / 255.0) + self.hentry.SetValue("%.2f" % (h)) + self.sentry.SetValue("%.2f" % (s)) + self.ventry.SetValue("%.2f" % (v)) + + def onPaletteDown(self, event): + """Stores state that the mouse has been pressed and updates + the selected colour values.""" + self.mouse_down = True + self.palette.ReDraw() + self.doPaletteClick(event.X, event.Y) + + def onPaletteUp(self, event): + """Stores state that the mouse is no longer depressed.""" + self.mouse_down = False + + def onPaletteMotion(self, event): + """Updates the colour values during mouse motion while the + mouse button is depressed.""" + if self.mouse_down: + self.doPaletteClick(event.X, event.Y) + + def doPaletteClick(self, m_x, m_y): + """Updates the colour values based on the mouse location + over the palette.""" + # Get the colour value and update + colour = self.palette.GetValue(m_x, m_y) + self.UpdateColour(colour) + + # Highlight a fresh selected area + self.palette.ReDraw() + self.palette.HighlightPoint(m_x, m_y) + + # Force an onscreen update + self.solid.Update() + self.colour_slider.Refresh() + + def onScroll(self, event): + """Updates the solid colour display to reflect the changing slider.""" + value = self.slider.GetValue() + colour = self.colour_slider.GetValue(value) + self.solid.SetColour(colour) + self.UpdateEntries(colour) + + def SetValue(self, colour): + """Updates the colour chooser to reflect the given wxColour.""" + self.UpdateColour(colour) + + def GetValue(self): + """Returns a wxColour object indicating the current colour choice.""" + return self.solid.GetColour() + +def main(): + """Simple test display.""" + class App(wx.App): + def OnInit(self): + frame = wx.Frame(None, -1, 'PyColourChooser Test') + + # Added here because that's where it's supposed to be, + # not embedded in the library. If it's embedded in the + # library, debug messages will be generated for duplicate + # handlers. + wx.InitAllImageHandlers() + + chooser = PyColourChooser(frame, -1) + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(chooser, 0, 0) + frame.SetAutoLayout(True) + frame.SetSizer(sizer) + sizer.Fit(frame) + + frame.Show(True) + self.SetTopWindow(frame) + return True + app = App(False) + app.MainLoop() + +if __name__ == '__main__': + main() diff --git a/wx/lib/colourchooser/pycolourslider.py b/wx/lib/colourchooser/pycolourslider.py new file mode 100644 index 00000000..25b37271 --- /dev/null +++ b/wx/lib/colourchooser/pycolourslider.py @@ -0,0 +1,92 @@ +""" +PyColourChooser +Copyright (C) 2002 Michael Gilfix + +This file is part of PyColourChooser. + +You should have received a file COPYING containing license terms +along with this program; if not, write to Michael Gilfix +(mgilfix@eecs.tufts.edu) for a copy. + +This version of PyColourChooser is open source; you can redistribute it and/or +modify it under the terms listed in the file COPYING. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +""" + +# 12/14/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o 2.5 compatability update. +# +# 12/21/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o wxPyColorChooser -> PyColorChooser +# o wxPyColourChooser -> PyColourChooser +# + +import wx + +import canvas +import colorsys + +class PyColourSlider(canvas.Canvas): + """A Pure-Python Colour Slider + + The colour slider displays transitions from value 0 to value 1 in + HSV, allowing the user to select a colour within the transition + spectrum. + + This class is best accompanying by a wxSlider that allows the user + to select a particular colour shade. + """ + + HEIGHT = 172 + WIDTH = 12 + + def __init__(self, parent, id, colour=None): + """Creates a blank slider instance. A colour must be set before the + slider will be filled in.""" + # Set the base colour first since our base class calls the buffer + # drawing function + self.SetBaseColour(colour) + + canvas.Canvas.__init__(self, parent, id, size=(self.WIDTH, self.HEIGHT)) + + def SetBaseColour(self, colour): + """Sets the base, or target colour, to use as the central colour + when calculating colour transitions.""" + self.base_colour = colour + + def GetBaseColour(self): + """Return the current colour used as a colour base for filling out + the slider.""" + return self.base_colour + + def GetValue(self, pos): + """Returns the colour value for a position on the slider. The position + must be within the valid height of the slider, or results can be + unpredictable.""" + return self.buffer.GetPixelColour(0, pos) + + def DrawBuffer(self): + """Actual implementation of the widget's drawing. We simply draw + from value 0.0 to value 1.0 in HSV.""" + if self.base_colour is None: + return + + target_red = self.base_colour.Red() + target_green = self.base_colour.Green() + target_blue = self.base_colour.Blue() + + h,s,v = colorsys.rgb_to_hsv(target_red / 255.0, target_green / 255.0, + target_blue / 255.0) + v = 1.0 + vstep = 1.0 / self.HEIGHT + for y_pos in range(0, self.HEIGHT): + r,g,b = [c * 255.0 for c in colorsys.hsv_to_rgb(h,s,v)] + colour = wx.Colour(int(r), int(g), int(b)) + self.buffer.SetPen(wx.Pen(colour, 1, wx.SOLID)) + self.buffer.DrawRectangle(0, y_pos, 15, 1) + v = v - vstep diff --git a/wx/lib/colourchooser/pypalette.py b/wx/lib/colourchooser/pypalette.py new file mode 100644 index 00000000..49a33e4d --- /dev/null +++ b/wx/lib/colourchooser/pypalette.py @@ -0,0 +1,176 @@ +""" +PyColourChooser +Copyright (C) 2002 Michael Gilfix + +This file is part of PyColourChooser. + +You should have received a file COPYING containing license terms +along with this program; if not, write to Michael Gilfix +(mgilfix@eecs.tufts.edu) for a copy. + +This version of PyColourChooser is open source; you can redistribute it and/or +modify it under the terms listed in the file COPYING. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +""" +# 12/14/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o 2.5 compatability update. +# +# 12/21/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o wxPyColorChooser -> PyColorChooser +# o wxPyColourChooser -> PyColourChooser +# o Commented out wx.InitAllImageHandlers() (see comments at that +# point for explanation +# + +import wx + +import canvas +import colorsys + +from wx.lib.embeddedimage import PyEmbeddedImage + +Image = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAAMgAAADACAYAAABBCyzzAAAABHNCSVQICAgIfAhkiAAACwNJ" + "REFUeJztnc16o0YQRZHt8SJZJO//lMkiWWQsKwsLK7S49dNdjTyTczYYhBBd7vqoom7B6bIs" + "l2VZluW3Zdld/l603fn8n18/ln8u2+Ufy/527/Pe731ffsmdeO+Ave3ZgRUb6tvf+2dXPS08" + "670uf9UMqPN7TwsASHAQAAMcBMDgZfn27eOv6+Ju+SKWz2L55CxP+0ux2T2cOg112utSDbfO" + "EJ5B1Iidj7OG8AwihnvUtFDDvFnjsYbgCgJggIMAGOAgAAb3Ocjs4DJJVQQajTyPj7aDHGyI" + "Lz4tjCPVGoQrCIABDgJggIMAGOg6iFdImBx8Hp173Ocg7Z7twNv19kzagbfri1h3PlaHUz+v" + "TlcNz6mDPC4XmT0j9mcGVxAAAxwEwAAHATB4WV5fP/6Kim5+ktxDbT99oah7w8FJ2curvfvP" + "l4ugxQJIg4MAGOAgAAZxLdakPhCF1ycw2g3QRpyvn8enH2RZluXbm/lx+ajrc4+aeghXEAAD" + "HATAAAcBMIj3pGclSNHcQ4Te7frxWqxo7qEM8qDCUFSD5fWDiBxkVv1jEev+jFAG6RWlUQcB" + "CIODABjgIAAGWos1Gmyq0LAz9O69/Z/NRU53BqgSpx2kxeqVHjXbX877H2erQL25yD3ewEcN" + "Qj8IQBocBMAABwEwyGuxshKkztC79/b/uszWP+r6QYrqIbPFaF4/yHl3szstekedfz5WVTJG" + "DgLQDQ4CYICDABjktVjzRDfmbtV3u3Ud5LXZEtVgTeqEUKF2NhfxNFlNY4yqg1Q9qqAdxiLW" + "7z/xkjKlwYp2CNEPAhAGBwEwwEEADOq0WHWim93ds7lHVoq0Zh5P3Q8IU2c0aIiVrCG8XMQp" + "DEXrIL1arP46yLEFIa4gAAY4CIABDgJgMP5+kFHxzSm1W7oVO18HyRqi2iDB3SYbItoPMnla" + "JPasMgg5CEAYHATAAAcBMND9IKNSJKUd6qyD9GqxYk8/ivSkjxaEOg0yyxC2BKmsDlJkhf8Q" + "NUivGA0tFkAYHATAAAcBMIhrsUY1WMGg03sSa7Xipl2/12Jlm/OzFYAgvbf9Oxtjnt/Nj8ty" + "kf5+kNmNMeQgAC44CIABDgJgMK7FmhR69z4Pa1SLpftBom/na5dRLVFSi9Uus6flDHPNQWZp" + "serrINm+ELRYAMPgIAAGOAiAQb8WS/U+j9/w3v1adT1E9aS/fbVkbOXgOsjL++7m8vpH3Bq9" + "FbJscz5aLIAwOAiAAQ4CYFBfBxnMRY7KPfye9KrGmMFcZLQfpK8N4i4HyU6Hur6Qql50etIB" + "ysFBAAxwEACDun6Q+hvfu4erijiVFGleP0ixFmt2P8hld3O6PBbNxOJ4BsmK0shBALrBQQAM" + "cBAAg7wWKxp0eje6nZ700XqId/tf94O07yj8n70f5Dr8NgfxpkF2WuRTU28mZGeGMsT3zTpX" + "EAADHATAAAcBMMhrsXo1WMkb39ncI5uLrMN8uy7r+kGKn0o7GnJn6yBXg3j9INHcI5uL+GQN" + "0ZuLvG2+DQA74CAABjgIgIHWYqmgMxpyJ2+AV0uPVMTZ5h7t0n9P+mRDLMHdJhtEabFG+0Dy" + "1nisIbiCABjgIAAGOAiAgdZiVd/+n/w4qOxtfxV5PklDZKPwyYYYrYO0A2+2Ky3WqERvUJG2" + "883qghDPxQIIg4MAGOAgAAa+FitaBsg2HxdJkGoUNz096UqN1HvnXzB6+z/bGHN9P/qag4zm" + "Hu16pyJtZ4/ROog3E86bowDADjgIgAEOAmAQ12KpoHNQctTSq7zJPv1IhN7Lc7chsslZpxZr" + "tgHO28MePfp4LtJroJwhuIIAGOAgAAY4CIBBXIvlNXsXS5GqQvBgyL28f66P5h5F9Y+WWQZR" + "dZBm8+OtMWqA9UzfxHa0WABpcBAAAxwEwGD8uVjRYDOoyfIUN1EtVhtxruvnZn0d7pqD+D3p" + "B0ff1bmHZwCRg3jSPFUei2qxFrFeNyP6DMIVBMAABwEwwEEADMa1WD9Y6L0u29B7XT6XNeMX" + "1T9aDjLM02mz+ujpYHxj7ozgCgJggIMAGOAgAAb9WqyDW7CrI84297gt2/vh1e8JObgfRBnA" + "McSjcpD6XGRshnAFATDAQQAMcBAAg7wWKxp0eg9A6nwulooslRarVdro3GNdjj4g7IsWhFrD" + "KANcn4fV5iC9Twc77rlYSpPl5RzvYv2yORoA7ICDABjgIAAGeS3WaMidJBqCD4bcn8t8HeQH" + "6UlPGkLVQR4/LaoLRLYhuIIAGOAgAAY4CIBB/h2Fkx7G2qvBGr39r3OQWYaYpMV6cB2k2go3" + "a1Sr83KG4AoCYICDABjgIAAGcS2WEt2MPhdLkK1/nJv1NsK8iPV8HSRqCKVCmtQgowyhDLKu" + "twZYD3va7pZNUZUVxqtCngYrWxBSM2V7FADYAQcBMMBBAAzGtVheg0Yy6PRCbi8Xae9mq1Bb" + "hN7LRT6VtjopG9RieSG3CrE9g6w/cz1edUe+GoZvld7CkGcYNRO23waAHXAQAAMcBMAgr8XK" + "9qT3PwBpc7hshOlprvYjzr0cZPSF8Z1PhBoVp6kmfc8A6880OYhXFRrVYvlEJ1TvTNg3DFcQ" + "AAMcBMAABwEw0DnIrGBTPd5I7Jatf7Tr0dxjPf57ug6SrQAk6yFe4cAziCdCEzxdj5d9N+Go" + "FuveGmrCZBtkPIPs/y5XEAADHATAAAcBMJjfkx7MPVp6Q2+tsYr93vtddO11RFQlY8ETzIbe" + "wZyjpc1BestjRZnYzh7R7LQzCSMHAfDBQQAMcBAAg1s/iLrB7S17JUgOvU9B8hQ23v33y+cR" + "R5OxQXFatjEmagBFc9zT9XjZLpj5TyruFaWtZ5z7Ha4gAAY4CIABDgJgcKuDeEHmQRKk0bvd" + "7Xr2+Pc5SNYgdXf+zRNdD/ferCvDJI9/uh531Ap15bEqA3gzY3t8riAABjgIgAEOAmCgcxDv" + "tQqTQ28VcV7E+noavcf1c5BRg3QWhh5kCJWDPHhaGCfe5iI1BuEKAmCAgwAY4CAABjctlrqB" + "nV1mm49Pqd1kKO4R1XZd7rYcbAjvhGcZojnemoOMTousFuveGrMM4R2fHATABQcBMMBBAAxu" + "dRBPRBMV2WTFN4JZIXd7WudmXecg3vqoCslhVsjdnt4191hzkNFRZzMyn6whctqr1hBcQQAM" + "cBAAAxwEwEDnIF7zd28vemc/SOvJvf0eqnvg9tSkUQMc9GzeVovl0RpAGSSYg1RPj/Y074nW" + "QVbaGdLuf27W9w3BFQTAAAcBMMBBAAxuWiwVKlctB2/7exGn2k+9qU6E3ju/4A2o1xCDWqxs" + "7uHUPdSr/Kr+7Z0pqUHUEN4v2gbgCgJggIMAGOAgAAa3OogXNEaDSxVsFt3+V/u1t/fV99Wb" + "6nQOUmWQSVqs6H5e8iVeozE6yjpreAOLZqfKIPsG4AoCYICDABjgIAAGOgeJ5hCjuYe4/a/W" + "leLmJNZb6ZL3xrrL9IEnKwBRg6hkrNMgp3UpzrZ31Pl6iBrIijcjlEFUErZdcgUBMMBBAAxw" + "EACDmxZLxbqzlw5qtzaUXpp1FWrHX+F3lAGEQaIDVy9hbLdnDZI8a+/fOzgNAnuqASqDeIbZ" + "fgoAO+AgAAY4CIDBrQ7i3W+PBpfR7QL1sYokF2c9u/3+F48yjMOoYToNVv3vHreK+mavAWzD" + "cAUBMMBBAAxwEAADnYO0eDHw6Pbk171H0maf1KpzkKO3d359fMC726tH1W+N6JGrDPEBVxAA" + "AxwEwAAHATD4F0lpw33hNrduAAAAAElFTkSuQmCC") + + +class PyPalette(canvas.Canvas): + """The Pure-Python Palette + + The PyPalette is a pure python implementation of a colour palette. The + palette implementation here imitates the palette layout used by MS + Windows and Adobe Photoshop. + + The actual palette image has been embedded as an XPM for speed. The + actual reverse-engineered drawing algorithm is provided in the + GeneratePaletteBMP() method. The algorithm is tweakable by supplying + the granularity factor to improve speed at the cost of display + beauty. Since the generator isn't used in real time, no one will + likely care :) But if you need it for some sort of unforeseen realtime + application, it's there. + """ + + HORIZONTAL_STEP = 2 + VERTICAL_STEP = 4 + + def __init__(self, parent, id): + """Creates a palette object.""" + # Load the pre-generated palette XPM + + # Leaving this in causes warning messages in some cases. + # It is the responsibility of the app to init the image + # handlers, IAW RD + #wx.InitAllImageHandlers() + + self.palette = Image.GetBitmap() + canvas.Canvas.__init__ (self, parent, id, size=(200, 192)) + + def GetValue(self, x, y): + """Returns a colour value at a specific x, y coordinate pair. This + is useful for determining the colour found a specific mouse click + in an external event handler.""" + return self.buffer.GetPixelColour(x, y) + + def DrawBuffer(self): + """Draws the palette XPM into the memory buffer.""" + #self.GeneratePaletteBMP ("foo.bmp") + self.buffer.DrawBitmap(self.palette, 0, 0, 0) + + def HighlightPoint(self, x, y): + """Highlights an area of the palette with a little circle around + the coordinate point""" + colour = wx.Colour(0, 0, 0) + self.buffer.SetPen(wx.Pen(colour, 1, wx.SOLID)) + self.buffer.SetBrush(wx.Brush(colour, wx.TRANSPARENT)) + self.buffer.DrawCircle(x, y, 3) + self.Refresh() + + def GeneratePaletteBMP(self, file_name, granularity=1): + """The actual palette drawing algorithm. + + This used to be 100% reverse engineered by looking at the + values on the MS map, but has since been redone Correctly(tm) + according to the HSV (hue, saturation, value) colour model by + Charl P. Botha . + + Speed is tweakable by changing the granularity factor, but + that affects how nice the output looks (makes the vertical + blocks bigger. This method was used to generate the embedded + XPM data.""" + self.vertical_step = self.VERTICAL_STEP * granularity + width, height = self.GetSize () + + # simply iterate over hue (horizontal) and saturation (vertical) + value = 1.0 + for y in range(0, height, self.vertical_step): + saturation = 1.0 - float(y) / float(height) + for x in range(0, width, self.HORIZONTAL_STEP): + hue = float(x) / float(width) + r,g,b = colorsys.hsv_to_rgb(hue, saturation, value) + colour = wx.Colour(int(r * 255.0), int(g * 255.0), int(b * 255.0)) + self.buffer.SetPen(wx.Pen(colour, 1, wx.SOLID)) + self.buffer.SetBrush(wx.Brush(colour, wx.SOLID)) + self.buffer.DrawRectangle(x, y, + self.HORIZONTAL_STEP, self.vertical_step) + + # this code is now simpler (and works) + bitmap = self.buffer.GetBitmap() + image = wx.ImageFromBitmap(bitmap) + image.SaveFile (file_name, wx.BITMAP_TYPE_XPM) diff --git a/wx/lib/colourdb.py b/wx/lib/colourdb.py new file mode 100644 index 00000000..c053c5ce --- /dev/null +++ b/wx/lib/colourdb.py @@ -0,0 +1,676 @@ +#---------------------------------------------------------------------- +# Name: wxPython.lib.colourdb.py +# Purpose: Adds a bunch of colour names and RGB values to the +# colour database so they can be found by name +# +# Author: Robin Dunn +# +# Created: 13-March-2001 +# RCS-ID: $Id$ +# Copyright: (c) 2001 by Total Control Software +# Licence: wxWindows license +#---------------------------------------------------------------------- + +""" +Load addition color names/values into the wx colour database. These +names and values originally came from the rgb.txt file on my system... +""" + + +def getColourList(): + """Returns a list of just the colour names used by this module.""" + return [ x[0] for x in getColourInfoList() ] + + + +def getColourInfoList(): + """Returns the list of colour name/value tuples used by this module.""" + return [ + ("SNOW", 255, 250, 250), + ("GHOST WHITE", 248, 248, 255), + ("GHOSTWHITE", 248, 248, 255), + ("WHITE SMOKE", 245, 245, 245), + ("WHITESMOKE", 245, 245, 245), + ("GAINSBORO", 220, 220, 220), + ("FLORAL WHITE", 255, 250, 240), + ("FLORALWHITE", 255, 250, 240), + ("OLD LACE", 253, 245, 230), + ("OLDLACE", 253, 245, 230), + ("LINEN", 250, 240, 230), + ("ANTIQUE WHITE", 250, 235, 215), + ("ANTIQUEWHITE", 250, 235, 215), + ("PAPAYA WHIP", 255, 239, 213), + ("PAPAYAWHIP", 255, 239, 213), + ("BLANCHED ALMOND", 255, 235, 205), + ("BLANCHEDALMOND", 255, 235, 205), + ("BISQUE", 255, 228, 196), + ("PEACH PUFF", 255, 218, 185), + ("PEACHPUFF", 255, 218, 185), + ("NAVAJO WHITE", 255, 222, 173), + ("NAVAJOWHITE", 255, 222, 173), + ("MOCCASIN", 255, 228, 181), + ("CORNSILK", 255, 248, 220), + ("IVORY", 255, 255, 240), + ("LEMON CHIFFON", 255, 250, 205), + ("LEMONCHIFFON", 255, 250, 205), + ("SEASHELL", 255, 245, 238), + ("HONEYDEW", 240, 255, 240), + ("MINT CREAM", 245, 255, 250), + ("MINTCREAM", 245, 255, 250), + ("AZURE", 240, 255, 255), + ("ALICE BLUE", 240, 248, 255), + ("ALICEBLUE", 240, 248, 255), + ("LAVENDER", 230, 230, 250), + ("LAVENDER BLUSH", 255, 240, 245), + ("LAVENDERBLUSH", 255, 240, 245), + ("MISTY ROSE", 255, 228, 225), + ("MISTYROSE", 255, 228, 225), + ("WHITE", 255, 255, 255), + ("BLACK", 0, 0, 0), + ("DARK SLATE GREY", 47, 79, 79), + ("DARKSLATEGREY", 47, 79, 79), + ("DIM GREY", 105, 105, 105), + ("DIMGREY", 105, 105, 105), + ("SLATE GREY", 112, 128, 144), + ("SLATEGREY", 112, 128, 144), + ("LIGHT SLATE GREY", 119, 136, 153), + ("LIGHTSLATEGREY", 119, 136, 153), + ("GREY", 190, 190, 190), + ("LIGHT GREY", 211, 211, 211), + ("LIGHTGREY", 211, 211, 211), + ("MIDNIGHT BLUE", 25, 25, 112), + ("MIDNIGHTBLUE", 25, 25, 112), + ("NAVY", 0, 0, 128), + ("NAVY BLUE", 0, 0, 128), + ("NAVYBLUE", 0, 0, 128), + ("CORNFLOWER BLUE", 100, 149, 237), + ("CORNFLOWERBLUE", 100, 149, 237), + ("DARK SLATE BLUE", 72, 61, 139), + ("DARKSLATEBLUE", 72, 61, 139), + ("SLATE BLUE", 106, 90, 205), + ("SLATEBLUE", 106, 90, 205), + ("MEDIUM SLATE BLUE", 123, 104, 238), + ("MEDIUMSLATEBLUE", 123, 104, 238), + ("LIGHT SLATE BLUE", 132, 112, 255), + ("LIGHTSLATEBLUE", 132, 112, 255), + ("MEDIUM BLUE", 0, 0, 205), + ("MEDIUMBLUE", 0, 0, 205), + ("ROYAL BLUE", 65, 105, 225), + ("ROYALBLUE", 65, 105, 225), + ("BLUE", 0, 0, 255), + ("DODGER BLUE", 30, 144, 255), + ("DODGERBLUE", 30, 144, 255), + ("DEEP SKY BLUE", 0, 191, 255), + ("DEEPSKYBLUE", 0, 191, 255), + ("SKY BLUE", 135, 206, 235), + ("SKYBLUE", 135, 206, 235), + ("LIGHT SKY BLUE", 135, 206, 250), + ("LIGHTSKYBLUE", 135, 206, 250), + ("STEEL BLUE", 70, 130, 180), + ("STEELBLUE", 70, 130, 180), + ("LIGHT STEEL BLUE", 176, 196, 222), + ("LIGHTSTEELBLUE", 176, 196, 222), + ("LIGHT BLUE", 173, 216, 230), + ("LIGHTBLUE", 173, 216, 230), + ("POWDER BLUE", 176, 224, 230), + ("POWDERBLUE", 176, 224, 230), + ("PALE TURQUOISE", 175, 238, 238), + ("PALETURQUOISE", 175, 238, 238), + ("DARK TURQUOISE", 0, 206, 209), + ("DARKTURQUOISE", 0, 206, 209), + ("MEDIUM TURQUOISE", 72, 209, 204), + ("MEDIUMTURQUOISE", 72, 209, 204), + ("TURQUOISE", 64, 224, 208), + ("CYAN", 0, 255, 255), + ("LIGHT CYAN", 224, 255, 255), + ("LIGHTCYAN", 224, 255, 255), + ("CADET BLUE", 95, 158, 160), + ("CADETBLUE", 95, 158, 160), + ("MEDIUM AQUAMARINE", 102, 205, 170), + ("MEDIUMAQUAMARINE", 102, 205, 170), + ("AQUAMARINE", 127, 255, 212), + ("DARK GREEN", 0, 100, 0), + ("DARKGREEN", 0, 100, 0), + ("DARK OLIVE GREEN", 85, 107, 47), + ("DARKOLIVEGREEN", 85, 107, 47), + ("DARK SEA GREEN", 143, 188, 143), + ("DARKSEAGREEN", 143, 188, 143), + ("SEA GREEN", 46, 139, 87), + ("SEAGREEN", 46, 139, 87), + ("MEDIUM SEA GREEN", 60, 179, 113), + ("MEDIUMSEAGREEN", 60, 179, 113), + ("LIGHT SEA GREEN", 32, 178, 170), + ("LIGHTSEAGREEN", 32, 178, 170), + ("PALE GREEN", 152, 251, 152), + ("PALEGREEN", 152, 251, 152), + ("SPRING GREEN", 0, 255, 127), + ("SPRINGGREEN", 0, 255, 127), + ("LAWN GREEN", 124, 252, 0), + ("LAWNGREEN", 124, 252, 0), + ("GREEN", 0, 255, 0), + ("CHARTREUSE", 127, 255, 0), + ("MEDIUM SPRING GREEN", 0, 250, 154), + ("MEDIUMSPRINGGREEN", 0, 250, 154), + ("GREEN YELLOW", 173, 255, 47), + ("GREENYELLOW", 173, 255, 47), + ("LIME GREEN", 50, 205, 50), + ("LIMEGREEN", 50, 205, 50), + ("YELLOW GREEN", 154, 205, 50), + ("YELLOWGREEN", 154, 205, 50), + ("FOREST GREEN", 34, 139, 34), + ("FORESTGREEN", 34, 139, 34), + ("OLIVE DRAB", 107, 142, 35), + ("OLIVEDRAB", 107, 142, 35), + ("DARK KHAKI", 189, 183, 107), + ("DARKKHAKI", 189, 183, 107), + ("KHAKI", 240, 230, 140), + ("PALE GOLDENROD", 238, 232, 170), + ("PALEGOLDENROD", 238, 232, 170), + ("LIGHT GOLDENROD YELLOW", 250, 250, 210), + ("LIGHTGOLDENRODYELLOW", 250, 250, 210), + ("LIGHT YELLOW", 255, 255, 224), + ("LIGHTYELLOW", 255, 255, 224), + ("YELLOW", 255, 255, 0), + ("GOLD", 255, 215, 0), + ("LIGHT GOLDENROD", 238, 221, 130), + ("LIGHTGOLDENROD", 238, 221, 130), + ("GOLDENROD", 218, 165, 32), + ("DARK GOLDENROD", 184, 134, 11), + ("DARKGOLDENROD", 184, 134, 11), + ("ROSY BROWN", 188, 143, 143), + ("ROSYBROWN", 188, 143, 143), + ("INDIAN RED", 205, 92, 92), + ("INDIANRED", 205, 92, 92), + ("SADDLE BROWN", 139, 69, 19), + ("SADDLEBROWN", 139, 69, 19), + ("SIENNA", 160, 82, 45), + ("PERU", 205, 133, 63), + ("BURLYWOOD", 222, 184, 135), + ("BEIGE", 245, 245, 220), + ("WHEAT", 245, 222, 179), + ("SANDY BROWN", 244, 164, 96), + ("SANDYBROWN", 244, 164, 96), + ("TAN", 210, 180, 140), + ("CHOCOLATE", 210, 105, 30), + ("FIREBRICK", 178, 34, 34), + ("BROWN", 165, 42, 42), + ("DARK SALMON", 233, 150, 122), + ("DARKSALMON", 233, 150, 122), + ("SALMON", 250, 128, 114), + ("LIGHT SALMON", 255, 160, 122), + ("LIGHTSALMON", 255, 160, 122), + ("ORANGE", 255, 165, 0), + ("DARK ORANGE", 255, 140, 0), + ("DARKORANGE", 255, 140, 0), + ("CORAL", 255, 127, 80), + ("LIGHT CORAL", 240, 128, 128), + ("LIGHTCORAL", 240, 128, 128), + ("TOMATO", 255, 99, 71), + ("ORANGE RED", 255, 69, 0), + ("ORANGERED", 255, 69, 0), + ("RED", 255, 0, 0), + ("HOT PINK", 255, 105, 180), + ("HOTPINK", 255, 105, 180), + ("DEEP PINK", 255, 20, 147), + ("DEEPPINK", 255, 20, 147), + ("PINK", 255, 192, 203), + ("LIGHT PINK", 255, 182, 193), + ("LIGHTPINK", 255, 182, 193), + ("PALE VIOLET RED", 219, 112, 147), + ("PALEVIOLETRED", 219, 112, 147), + ("MAROON", 176, 48, 96), + ("MEDIUM VIOLET RED", 199, 21, 133), + ("MEDIUMVIOLETRED", 199, 21, 133), + ("VIOLET RED", 208, 32, 144), + ("VIOLETRED", 208, 32, 144), + ("MAGENTA", 255, 0, 255), + ("VIOLET", 238, 130, 238), + ("PLUM", 221, 160, 221), + ("ORCHID", 218, 112, 214), + ("MEDIUM ORCHID", 186, 85, 211), + ("MEDIUMORCHID", 186, 85, 211), + ("DARK ORCHID", 153, 50, 204), + ("DARKORCHID", 153, 50, 204), + ("DARK VIOLET", 148, 0, 211), + ("DARKVIOLET", 148, 0, 211), + ("BLUE VIOLET", 138, 43, 226), + ("BLUEVIOLET", 138, 43, 226), + ("PURPLE", 160, 32, 240), + ("MEDIUM PURPLE", 147, 112, 219), + ("MEDIUMPURPLE", 147, 112, 219), + ("THISTLE", 216, 191, 216), + ("SNOW1", 255, 250, 250), + ("SNOW2", 238, 233, 233), + ("SNOW3", 205, 201, 201), + ("SNOW4", 139, 137, 137), + ("SEASHELL1", 255, 245, 238), + ("SEASHELL2", 238, 229, 222), + ("SEASHELL3", 205, 197, 191), + ("SEASHELL4", 139, 134, 130), + ("ANTIQUEWHITE1", 255, 239, 219), + ("ANTIQUEWHITE2", 238, 223, 204), + ("ANTIQUEWHITE3", 205, 192, 176), + ("ANTIQUEWHITE4", 139, 131, 120), + ("BISQUE1", 255, 228, 196), + ("BISQUE2", 238, 213, 183), + ("BISQUE3", 205, 183, 158), + ("BISQUE4", 139, 125, 107), + ("PEACHPUFF1", 255, 218, 185), + ("PEACHPUFF2", 238, 203, 173), + ("PEACHPUFF3", 205, 175, 149), + ("PEACHPUFF4", 139, 119, 101), + ("NAVAJOWHITE1", 255, 222, 173), + ("NAVAJOWHITE2", 238, 207, 161), + ("NAVAJOWHITE3", 205, 179, 139), + ("NAVAJOWHITE4", 139, 121, 94), + ("LEMONCHIFFON1", 255, 250, 205), + ("LEMONCHIFFON2", 238, 233, 191), + ("LEMONCHIFFON3", 205, 201, 165), + ("LEMONCHIFFON4", 139, 137, 112), + ("CORNSILK1", 255, 248, 220), + ("CORNSILK2", 238, 232, 205), + ("CORNSILK3", 205, 200, 177), + ("CORNSILK4", 139, 136, 120), + ("IVORY1", 255, 255, 240), + ("IVORY2", 238, 238, 224), + ("IVORY3", 205, 205, 193), + ("IVORY4", 139, 139, 131), + ("HONEYDEW1", 240, 255, 240), + ("HONEYDEW2", 224, 238, 224), + ("HONEYDEW3", 193, 205, 193), + ("HONEYDEW4", 131, 139, 131), + ("LAVENDERBLUSH1", 255, 240, 245), + ("LAVENDERBLUSH2", 238, 224, 229), + ("LAVENDERBLUSH3", 205, 193, 197), + ("LAVENDERBLUSH4", 139, 131, 134), + ("MISTYROSE1", 255, 228, 225), + ("MISTYROSE2", 238, 213, 210), + ("MISTYROSE3", 205, 183, 181), + ("MISTYROSE4", 139, 125, 123), + ("AZURE1", 240, 255, 255), + ("AZURE2", 224, 238, 238), + ("AZURE3", 193, 205, 205), + ("AZURE4", 131, 139, 139), + ("SLATEBLUE1", 131, 111, 255), + ("SLATEBLUE2", 122, 103, 238), + ("SLATEBLUE3", 105, 89, 205), + ("SLATEBLUE4", 71, 60, 139), + ("ROYALBLUE1", 72, 118, 255), + ("ROYALBLUE2", 67, 110, 238), + ("ROYALBLUE3", 58, 95, 205), + ("ROYALBLUE4", 39, 64, 139), + ("BLUE1", 0, 0, 255), + ("BLUE2", 0, 0, 238), + ("BLUE3", 0, 0, 205), + ("BLUE4", 0, 0, 139), + ("DODGERBLUE1", 30, 144, 255), + ("DODGERBLUE2", 28, 134, 238), + ("DODGERBLUE3", 24, 116, 205), + ("DODGERBLUE4", 16, 78, 139), + ("STEELBLUE1", 99, 184, 255), + ("STEELBLUE2", 92, 172, 238), + ("STEELBLUE3", 79, 148, 205), + ("STEELBLUE4", 54, 100, 139), + ("DEEPSKYBLUE1", 0, 191, 255), + ("DEEPSKYBLUE2", 0, 178, 238), + ("DEEPSKYBLUE3", 0, 154, 205), + ("DEEPSKYBLUE4", 0, 104, 139), + ("SKYBLUE1", 135, 206, 255), + ("SKYBLUE2", 126, 192, 238), + ("SKYBLUE3", 108, 166, 205), + ("SKYBLUE4", 74, 112, 139), + ("LIGHTSKYBLUE1", 176, 226, 255), + ("LIGHTSKYBLUE2", 164, 211, 238), + ("LIGHTSKYBLUE3", 141, 182, 205), + ("LIGHTSKYBLUE4", 96, 123, 139), + ("LIGHTSTEELBLUE1", 202, 225, 255), + ("LIGHTSTEELBLUE2", 188, 210, 238), + ("LIGHTSTEELBLUE3", 162, 181, 205), + ("LIGHTSTEELBLUE4", 110, 123, 139), + ("LIGHTBLUE1", 191, 239, 255), + ("LIGHTBLUE2", 178, 223, 238), + ("LIGHTBLUE3", 154, 192, 205), + ("LIGHTBLUE4", 104, 131, 139), + ("LIGHTCYAN1", 224, 255, 255), + ("LIGHTCYAN2", 209, 238, 238), + ("LIGHTCYAN3", 180, 205, 205), + ("LIGHTCYAN4", 122, 139, 139), + ("PALETURQUOISE1", 187, 255, 255), + ("PALETURQUOISE2", 174, 238, 238), + ("PALETURQUOISE3", 150, 205, 205), + ("PALETURQUOISE4", 102, 139, 139), + ("CADETBLUE1", 152, 245, 255), + ("CADETBLUE2", 142, 229, 238), + ("CADETBLUE3", 122, 197, 205), + ("CADETBLUE4", 83, 134, 139), + ("TURQUOISE1", 0, 245, 255), + ("TURQUOISE2", 0, 229, 238), + ("TURQUOISE3", 0, 197, 205), + ("TURQUOISE4", 0, 134, 139), + ("CYAN1", 0, 255, 255), + ("CYAN2", 0, 238, 238), + ("CYAN3", 0, 205, 205), + ("CYAN4", 0, 139, 139), + ("AQUAMARINE1", 127, 255, 212), + ("AQUAMARINE2", 118, 238, 198), + ("AQUAMARINE3", 102, 205, 170), + ("AQUAMARINE4", 69, 139, 116), + ("DARKSEAGREEN1", 193, 255, 193), + ("DARKSEAGREEN2", 180, 238, 180), + ("DARKSEAGREEN3", 155, 205, 155), + ("DARKSEAGREEN4", 105, 139, 105), + ("SEAGREEN1", 84, 255, 159), + ("SEAGREEN2", 78, 238, 148), + ("SEAGREEN3", 67, 205, 128), + ("SEAGREEN4", 46, 139, 87), + ("PALEGREEN1", 154, 255, 154), + ("PALEGREEN2", 144, 238, 144), + ("PALEGREEN3", 124, 205, 124), + ("PALEGREEN4", 84, 139, 84), + ("SPRINGGREEN1", 0, 255, 127), + ("SPRINGGREEN2", 0, 238, 118), + ("SPRINGGREEN3", 0, 205, 102), + ("SPRINGGREEN4", 0, 139, 69), + ("GREEN1", 0, 255, 0), + ("GREEN2", 0, 238, 0), + ("GREEN3", 0, 205, 0), + ("GREEN4", 0, 139, 0), + ("CHARTREUSE1", 127, 255, 0), + ("CHARTREUSE2", 118, 238, 0), + ("CHARTREUSE3", 102, 205, 0), + ("CHARTREUSE4", 69, 139, 0), + ("OLIVEDRAB1", 192, 255, 62), + ("OLIVEDRAB2", 179, 238, 58), + ("OLIVEDRAB3", 154, 205, 50), + ("OLIVEDRAB4", 105, 139, 34), + ("DARKOLIVEGREEN1", 202, 255, 112), + ("DARKOLIVEGREEN2", 188, 238, 104), + ("DARKOLIVEGREEN3", 162, 205, 90), + ("DARKOLIVEGREEN4", 110, 139, 61), + ("KHAKI1", 255, 246, 143), + ("KHAKI2", 238, 230, 133), + ("KHAKI3", 205, 198, 115), + ("KHAKI4", 139, 134, 78), + ("LIGHTGOLDENROD1", 255, 236, 139), + ("LIGHTGOLDENROD2", 238, 220, 130), + ("LIGHTGOLDENROD3", 205, 190, 112), + ("LIGHTGOLDENROD4", 139, 129, 76), + ("LIGHTYELLOW1", 255, 255, 224), + ("LIGHTYELLOW2", 238, 238, 209), + ("LIGHTYELLOW3", 205, 205, 180), + ("LIGHTYELLOW4", 139, 139, 122), + ("YELLOW1", 255, 255, 0), + ("YELLOW2", 238, 238, 0), + ("YELLOW3", 205, 205, 0), + ("YELLOW4", 139, 139, 0), + ("GOLD1", 255, 215, 0), + ("GOLD2", 238, 201, 0), + ("GOLD3", 205, 173, 0), + ("GOLD4", 139, 117, 0), + ("GOLDENROD1", 255, 193, 37), + ("GOLDENROD2", 238, 180, 34), + ("GOLDENROD3", 205, 155, 29), + ("GOLDENROD4", 139, 105, 20), + ("DARKGOLDENROD1", 255, 185, 15), + ("DARKGOLDENROD2", 238, 173, 14), + ("DARKGOLDENROD3", 205, 149, 12), + ("DARKGOLDENROD4", 139, 101, 8), + ("ROSYBROWN1", 255, 193, 193), + ("ROSYBROWN2", 238, 180, 180), + ("ROSYBROWN3", 205, 155, 155), + ("ROSYBROWN4", 139, 105, 105), + ("INDIANRED1", 255, 106, 106), + ("INDIANRED2", 238, 99, 99), + ("INDIANRED3", 205, 85, 85), + ("INDIANRED4", 139, 58, 58), + ("SIENNA1", 255, 130, 71), + ("SIENNA2", 238, 121, 66), + ("SIENNA3", 205, 104, 57), + ("SIENNA4", 139, 71, 38), + ("BURLYWOOD1", 255, 211, 155), + ("BURLYWOOD2", 238, 197, 145), + ("BURLYWOOD3", 205, 170, 125), + ("BURLYWOOD4", 139, 115, 85), + ("WHEAT1", 255, 231, 186), + ("WHEAT2", 238, 216, 174), + ("WHEAT3", 205, 186, 150), + ("WHEAT4", 139, 126, 102), + ("TAN1", 255, 165, 79), + ("TAN2", 238, 154, 73), + ("TAN3", 205, 133, 63), + ("TAN4", 139, 90, 43), + ("CHOCOLATE1", 255, 127, 36), + ("CHOCOLATE2", 238, 118, 33), + ("CHOCOLATE3", 205, 102, 29), + ("CHOCOLATE4", 139, 69, 19), + ("FIREBRICK1", 255, 48, 48), + ("FIREBRICK2", 238, 44, 44), + ("FIREBRICK3", 205, 38, 38), + ("FIREBRICK4", 139, 26, 26), + ("BROWN1", 255, 64, 64), + ("BROWN2", 238, 59, 59), + ("BROWN3", 205, 51, 51), + ("BROWN4", 139, 35, 35), + ("SALMON1", 255, 140, 105), + ("SALMON2", 238, 130, 98), + ("SALMON3", 205, 112, 84), + ("SALMON4", 139, 76, 57), + ("LIGHTSALMON1", 255, 160, 122), + ("LIGHTSALMON2", 238, 149, 114), + ("LIGHTSALMON3", 205, 129, 98), + ("LIGHTSALMON4", 139, 87, 66), + ("ORANGE1", 255, 165, 0), + ("ORANGE2", 238, 154, 0), + ("ORANGE3", 205, 133, 0), + ("ORANGE4", 139, 90, 0), + ("DARKORANGE1", 255, 127, 0), + ("DARKORANGE2", 238, 118, 0), + ("DARKORANGE3", 205, 102, 0), + ("DARKORANGE4", 139, 69, 0), + ("CORAL1", 255, 114, 86), + ("CORAL2", 238, 106, 80), + ("CORAL3", 205, 91, 69), + ("CORAL4", 139, 62, 47), + ("TOMATO1", 255, 99, 71), + ("TOMATO2", 238, 92, 66), + ("TOMATO3", 205, 79, 57), + ("TOMATO4", 139, 54, 38), + ("ORANGERED1", 255, 69, 0), + ("ORANGERED2", 238, 64, 0), + ("ORANGERED3", 205, 55, 0), + ("ORANGERED4", 139, 37, 0), + ("RED1", 255, 0, 0), + ("RED2", 238, 0, 0), + ("RED3", 205, 0, 0), + ("RED4", 139, 0, 0), + ("DEEPPINK1", 255, 20, 147), + ("DEEPPINK2", 238, 18, 137), + ("DEEPPINK3", 205, 16, 118), + ("DEEPPINK4", 139, 10, 80), + ("HOTPINK1", 255, 110, 180), + ("HOTPINK2", 238, 106, 167), + ("HOTPINK3", 205, 96, 144), + ("HOTPINK4", 139, 58, 98), + ("PINK1", 255, 181, 197), + ("PINK2", 238, 169, 184), + ("PINK3", 205, 145, 158), + ("PINK4", 139, 99, 108), + ("LIGHTPINK1", 255, 174, 185), + ("LIGHTPINK2", 238, 162, 173), + ("LIGHTPINK3", 205, 140, 149), + ("LIGHTPINK4", 139, 95, 101), + ("PALEVIOLETRED1", 255, 130, 171), + ("PALEVIOLETRED2", 238, 121, 159), + ("PALEVIOLETRED3", 205, 104, 137), + ("PALEVIOLETRED4", 139, 71, 93), + ("MAROON1", 255, 52, 179), + ("MAROON2", 238, 48, 167), + ("MAROON3", 205, 41, 144), + ("MAROON4", 139, 28, 98), + ("VIOLETRED1", 255, 62, 150), + ("VIOLETRED2", 238, 58, 140), + ("VIOLETRED3", 205, 50, 120), + ("VIOLETRED4", 139, 34, 82), + ("MAGENTA1", 255, 0, 255), + ("MAGENTA2", 238, 0, 238), + ("MAGENTA3", 205, 0, 205), + ("MAGENTA4", 139, 0, 139), + ("ORCHID1", 255, 131, 250), + ("ORCHID2", 238, 122, 233), + ("ORCHID3", 205, 105, 201), + ("ORCHID4", 139, 71, 137), + ("PLUM1", 255, 187, 255), + ("PLUM2", 238, 174, 238), + ("PLUM3", 205, 150, 205), + ("PLUM4", 139, 102, 139), + ("MEDIUMORCHID1", 224, 102, 255), + ("MEDIUMORCHID2", 209, 95, 238), + ("MEDIUMORCHID3", 180, 82, 205), + ("MEDIUMORCHID4", 122, 55, 139), + ("DARKORCHID1", 191, 62, 255), + ("DARKORCHID2", 178, 58, 238), + ("DARKORCHID3", 154, 50, 205), + ("DARKORCHID4", 104, 34, 139), + ("PURPLE1", 155, 48, 255), + ("PURPLE2", 145, 44, 238), + ("PURPLE3", 125, 38, 205), + ("PURPLE4", 85, 26, 139), + ("MEDIUMPURPLE1", 171, 130, 255), + ("MEDIUMPURPLE2", 159, 121, 238), + ("MEDIUMPURPLE3", 137, 104, 205), + ("MEDIUMPURPLE4", 93, 71, 139), + ("THISTLE1", 255, 225, 255), + ("THISTLE2", 238, 210, 238), + ("THISTLE3", 205, 181, 205), + ("THISTLE4", 139, 123, 139), + ("GREY0", 0, 0, 0), + ("GREY1", 3, 3, 3), + ("GREY2", 5, 5, 5), + ("GREY3", 8, 8, 8), + ("GREY4", 10, 10, 10), + ("GREY5", 13, 13, 13), + ("GREY6", 15, 15, 15), + ("GREY7", 18, 18, 18), + ("GREY8", 20, 20, 20), + ("GREY9", 23, 23, 23), + ("GREY10", 26, 26, 26), + ("GREY11", 28, 28, 28), + ("GREY12", 31, 31, 31), + ("GREY13", 33, 33, 33), + ("GREY14", 36, 36, 36), + ("GREY15", 38, 38, 38), + ("GREY16", 41, 41, 41), + ("GREY17", 43, 43, 43), + ("GREY18", 46, 46, 46), + ("GREY19", 48, 48, 48), + ("GREY20", 51, 51, 51), + ("GREY21", 54, 54, 54), + ("GREY22", 56, 56, 56), + ("GREY23", 59, 59, 59), + ("GREY24", 61, 61, 61), + ("GREY25", 64, 64, 64), + ("GREY26", 66, 66, 66), + ("GREY27", 69, 69, 69), + ("GREY28", 71, 71, 71), + ("GREY29", 74, 74, 74), + ("GREY30", 77, 77, 77), + ("GREY31", 79, 79, 79), + ("GREY32", 82, 82, 82), + ("GREY33", 84, 84, 84), + ("GREY34", 87, 87, 87), + ("GREY35", 89, 89, 89), + ("GREY36", 92, 92, 92), + ("GREY37", 94, 94, 94), + ("GREY38", 97, 97, 97), + ("GREY39", 99, 99, 99), + ("GREY40", 102, 102, 102), + ("GREY41", 105, 105, 105), + ("GREY42", 107, 107, 107), + ("GREY43", 110, 110, 110), + ("GREY44", 112, 112, 112), + ("GREY45", 115, 115, 115), + ("GREY46", 117, 117, 117), + ("GREY47", 120, 120, 120), + ("GREY48", 122, 122, 122), + ("GREY49", 125, 125, 125), + ("GREY50", 127, 127, 127), + ("GREY51", 130, 130, 130), + ("GREY52", 133, 133, 133), + ("GREY53", 135, 135, 135), + ("GREY54", 138, 138, 138), + ("GREY55", 140, 140, 140), + ("GREY56", 143, 143, 143), + ("GREY57", 145, 145, 145), + ("GREY58", 148, 148, 148), + ("GREY59", 150, 150, 150), + ("GREY60", 153, 153, 153), + ("GREY61", 156, 156, 156), + ("GREY62", 158, 158, 158), + ("GREY63", 161, 161, 161), + ("GREY64", 163, 163, 163), + ("GREY65", 166, 166, 166), + ("GREY66", 168, 168, 168), + ("GREY67", 171, 171, 171), + ("GREY68", 173, 173, 173), + ("GREY69", 176, 176, 176), + ("GREY70", 179, 179, 179), + ("GREY71", 181, 181, 181), + ("GREY72", 184, 184, 184), + ("GREY73", 186, 186, 186), + ("GREY74", 189, 189, 189), + ("GREY75", 191, 191, 191), + ("GREY76", 194, 194, 194), + ("GREY77", 196, 196, 196), + ("GREY78", 199, 199, 199), + ("GREY79", 201, 201, 201), + ("GREY80", 204, 204, 204), + ("GREY81", 207, 207, 207), + ("GREY82", 209, 209, 209), + ("GREY83", 212, 212, 212), + ("GREY84", 214, 214, 214), + ("GREY85", 217, 217, 217), + ("GREY86", 219, 219, 219), + ("GREY87", 222, 222, 222), + ("GREY88", 224, 224, 224), + ("GREY89", 227, 227, 227), + ("GREY90", 229, 229, 229), + ("GREY91", 232, 232, 232), + ("GREY92", 235, 235, 235), + ("GREY93", 237, 237, 237), + ("GREY94", 240, 240, 240), + ("GREY95", 242, 242, 242), + ("GREY96", 245, 245, 245), + ("GREY97", 247, 247, 247), + ("GREY98", 250, 250, 250), + ("GREY99", 252, 252, 252), + ("GREY100", 255, 255, 255), + ("DARK GREY", 169, 169, 169), + ("DARKGREY", 169, 169, 169), + ("DARK BLUE", 0, 0, 139), + ("DARKBLUE", 0, 0, 139), + ("DARK CYAN", 0, 139, 139), + ("DARKCYAN", 0, 139, 139), + ("DARK MAGENTA", 139, 0, 139), + ("DARKMAGENTA", 139, 0, 139), + ("DARK RED", 139, 0, 0), + ("DARKRED", 139, 0, 0), + ("LIGHT GREEN", 144, 238, 144), + ("LIGHTGREEN", 144, 238, 144), + ] + + +_haveUpdated = False + +def updateColourDB(): + """Updates the wx colour database by adding new colour names and RGB values.""" + global _haveUpdated + if not _haveUpdated: + import wx + assert wx.GetApp() is not None, "You must have a wx.App object before you can use the colour database." + cl = getColourInfoList() + + for info in cl: + wx.TheColourDatabase.Append(*info) + + _haveUpdated = True + diff --git a/wx/lib/colourselect.py b/wx/lib/colourselect.py new file mode 100644 index 00000000..a41c5fd3 --- /dev/null +++ b/wx/lib/colourselect.py @@ -0,0 +1,176 @@ +#---------------------------------------------------------------------------- +# Name: ColourSelect.py +# Purpose: Colour Box Selection Control +# +# Author: Lorne White, Lorne.White@telusplanet.net +# +# Created: Feb 25, 2001 +# Licence: wxWindows license +#---------------------------------------------------------------------------- + +# creates a colour wxButton with selectable color +# button click provides a colour selection box +# button colour will change to new colour +# GetColour method to get the selected colour + +# Updates: +# call back to function if changes made + +# Cliff Wells, logiplexsoftware@earthlink.net: +# - Made ColourSelect into "is a button" rather than "has a button" +# - Added label parameter and logic to adjust the label colour according to the background +# colour +# - Added id argument +# - Rearranged arguments to more closely follow wx conventions +# - Simplified some of the code + +# Cliff Wells, 2002/02/07 +# - Added ColourSelect Event + +# 12/01/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o Updated for 2.5 compatability. +# + +""" +Provides a `ColourSelect` button that, when clicked, will display a +colour selection dialog. The selected colour is displayed on the +button itself. +""" + +#---------------------------------------------------------------------------- + +import wx + +#---------------------------------------------------------------------------- + +wxEVT_COMMAND_COLOURSELECT = wx.NewEventType() + +class ColourSelectEvent(wx.PyCommandEvent): + def __init__(self, id, value): + wx.PyCommandEvent.__init__(self, id = id) + self.SetEventType(wxEVT_COMMAND_COLOURSELECT) + self.value = value + + def GetValue(self): + return self.value + +EVT_COLOURSELECT = wx.PyEventBinder(wxEVT_COMMAND_COLOURSELECT, 1) + +#---------------------------------------------------------------------------- + +class ColourSelect(wx.BitmapButton): + def __init__(self, parent, id=wx.ID_ANY, label="", colour=wx.BLACK, + pos=wx.DefaultPosition, size=wx.DefaultSize, + callback=None, style=0): + size = wx.Size(*size) + if label: + mdc = wx.MemoryDC(wx.EmptyBitmap(1,1)) + w, h = mdc.GetTextExtent(label) + w += 6 + h += 6 + else: + w, h = 20, 20 + size.width = size.width if size.width != -1 else w + size.height = size.height if size.height != -1 else h + wx.BitmapButton.__init__(self, parent, id, wx.EmptyBitmap(w,h), + pos=pos, size=size, style=style|wx.BU_AUTODRAW) + + if type(colour) == type( () ): + colour = wx.Colour(*colour) + self.colour = colour + self.SetLabel(label) + self.callback = callback + bmp = self.MakeBitmap() + self.SetBitmap(bmp) + parent.Bind(wx.EVT_BUTTON, self.OnClick, self) + + + def GetColour(self): + return self.colour + + def GetValue(self): + return self.colour + + def SetValue(self, colour): + self.SetColour(colour) + + def SetColour(self, colour): + if type(colour) == tuple: + colour = wx.Colour(*colour) + if type(colour) == str: + colour = wx.NamedColour(colour) + + self.colour = colour + bmp = self.MakeBitmap() + self.SetBitmap(bmp) + + + def SetLabel(self, label): + self.label = label + + def GetLabel(self): + return self.label + + + def MakeBitmap(self): + bdr = 8 + width, height = self.GetSize() + + # yes, this is weird, but it appears to work around a bug in wxMac + if "wxMac" in wx.PlatformInfo and width == height: + height -= 1 + + bmp = wx.EmptyBitmap(width-bdr, height-bdr) + dc = wx.MemoryDC() + dc.SelectObject(bmp) + dc.SetFont(self.GetFont()) + label = self.GetLabel() + # Just make a little colored bitmap + dc.SetBackground(wx.Brush(self.colour)) + dc.Clear() + + if label: + # Add a label to it + avg = reduce(lambda a, b: a + b, self.colour.Get()) / 3 + fcolour = avg > 128 and wx.BLACK or wx.WHITE + dc.SetTextForeground(fcolour) + dc.DrawLabel(label, (0,0, width-bdr, height-bdr), + wx.ALIGN_CENTER) + + dc.SelectObject(wx.NullBitmap) + return bmp + + + def SetBitmap(self, bmp): + self.SetBitmapLabel(bmp) + #self.SetBitmapSelected(bmp) + #self.SetBitmapDisabled(bmp) + #self.SetBitmapFocus(bmp) + #self.SetBitmapSelected(bmp) + self.Refresh() + + + def OnChange(self): + evt = ColourSelectEvent(self.GetId(), self.GetValue()) + evt.SetEventObject(self) + wx.PostEvent(self, evt) + if self.callback is not None: + self.callback() + + def OnClick(self, event): + data = wx.ColourData() + data.SetChooseFull(True) + data.SetColour(self.colour) + dlg = wx.ColourDialog(wx.GetTopLevelParent(self), data) + changed = dlg.ShowModal() == wx.ID_OK + + if changed: + data = dlg.GetColourData() + self.SetColour(data.GetColour()) + dlg.Destroy() + + # moved after dlg.Destroy, since who knows what the callback will do... + if changed: + self.OnChange() + diff --git a/wx/lib/colourutils.py b/wx/lib/colourutils.py new file mode 100644 index 00000000..619f78fd --- /dev/null +++ b/wx/lib/colourutils.py @@ -0,0 +1,92 @@ +""" +Some useful colour-related utility functions. + +""" + +__author__ = "Cody Precord " +__svnid__ = "$Id: $" +__revision__ = "$Revision: $" + +import wx + +# Used on OSX to get access to carbon api constants +if wx.Platform == '__WXMAC__': + try: + import Carbon.Appearance + except ImportError: + CARBON = False + else: + CARBON = True + +#-----------------------------------------------------------------------------# + +def AdjustAlpha(colour, alpha): + """Adjust the alpha of a given colour""" + return wx.Colour(colour.Red(), colour.Green(), colour.Blue(), alpha) + + +def AdjustColour(color, percent, alpha=wx.ALPHA_OPAQUE): + """ + Brighten/Darken input colour by percent and adjust alpha + channel if needed. Returns the modified color. + + :param Colour `color`: color object to adjust + :param integer `percent`: percent to adjust +(brighten) or -(darken) + :keyword `alpha`: amount to adjust alpha channel + + """ + radj, gadj, badj = [ int(val * (abs(percent) / 100.)) + for val in color.Get() ] + + if percent < 0: + radj, gadj, badj = [ val * -1 for val in [radj, gadj, badj] ] + else: + radj, gadj, badj = [ val or 255 for val in [radj, gadj, badj] ] + + red = min(color.Red() + radj, 255) + green = min(color.Green() + gadj, 255) + blue = min(color.Blue() + badj, 255) + return wx.Colour(red, green, blue, alpha) + + +def BestLabelColour(color, bw=False): + """ + Get the best color to use for the label that will be drawn on + top of the given color. + + :param Colour `color`: background color that text will be drawn on + :keyword `bw`: If True, only return black or white + + """ + avg = sum(color.Get()) / 3 + if avg > 192: + txt_color = wx.BLACK + elif avg > 128: + if bw: txt_color = wx.BLACK + else: txt_color = AdjustColour(color, -95) + elif avg < 64: + txt_color = wx.WHITE + else: + if bw: txt_color = wx.WHITE + else: txt_color = AdjustColour(color, 95) + return txt_color + +def GetHighlightColour(): + """Get the default highlight color + + :return: :class:`Colour` + + """ + if wx.Platform == '__WXMAC__': + if CARBON: + if wx.VERSION < (2, 9, 0, 0, ''): + # kThemeBrushButtonPressedLightHighlight + brush = wx.Brush(wx.BLACK) + brush.MacSetTheme(Carbon.Appearance.kThemeBrushFocusHighlight) + return brush.GetColour() + else: + color = wx.MacThemeColour(Carbon.Appearance.kThemeBrushFocusHighlight) + return color + + # Fallback to text highlight color + return wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHT) diff --git a/wx/lib/combotreebox.py b/wx/lib/combotreebox.py new file mode 100644 index 00000000..945401d3 --- /dev/null +++ b/wx/lib/combotreebox.py @@ -0,0 +1,927 @@ +""" +ComboTreeBox provides a ComboBox that pops up a tree instead of a list. + +ComboTreeBox tries to provide the same interface as :class:`ComboBox` as much as +possible. However, whereas the ComboBox widget uses indices to access +items in the list of choices, ComboTreeBox uses TreeItemId's instead. If +you add an item to the ComboTreeBox (using Append or Insert), the +:class:`TreeItemId` associated with the added item is returned. You can then use +that `TreeItemId` to add items as children of that first item. For +example:: + + from wx.lib.combotreebox import ComboTreeBox + combo = ComboTreeBox(parent) + item1 = combo.Append('Item 1') # Add a root item + item1a = combo.Append('Item 1a', parent=item1) # Add a child to item1 + + +You can also add client data to each of the items like this:: + + item1 = combo.Append('Item 1', clientData=somePythonObject) + item1a = combo.Append('Item 1a', parent=item1, + clientData=someOtherPythonObject) + + +And later fetch the client data like this:: + + somePythonObject = combo.GetClientData(item1) + + +To get the client data of the currently selected item (if any):: + + currentItem = combo.GetSelection() + if currentItem: + somePythonObject = combo.GetClientData(currentItem) + + +Supported styles are the same as for :class:`ComboBox`, i.e. ``wx.CB_READONLY`` and +``wx.CB_SORT``. Provide them as usual:: + + combo = ComboTreeBox(parent, style=wx.CB_READONLY|wx.CB_SORT) + + +Supported platforms: wxMSW and wxMAC natively, wxGTK by means of a +workaround. + +.. moduleauthor:: Frank Niessink + +Copyright 2006, 2008, 2010, Frank Niessink +License: wxWidgets license +Version: 1.1 +Date: August 1, 2010 + +""" + +import wx + +__all__ = ['ComboTreeBox'] # Export only the ComboTreeBox widget + + +# --------------------------------------------------------------------------- + + +class IterableTreeCtrl(wx.TreeCtrl): + """ + TreeCtrl is the same as :class:`TreeCtrl`, with a few convenience methods + added for easier navigation of items. """ + + def GetPreviousItem(self, item): + """ + Returns the item that is on the line immediately above item + (as is displayed when the tree is fully expanded). The returned + item is invalid if item is the first item in the tree. + + :param TreeItemId `item`: a :class:`TreeItemId` + :return: the :class:`TreeItemId` previous to the one passed in or an invalid item + :rtype: :class:`TreeItemId` + + """ + previousSibling = self.GetPrevSibling(item) + if previousSibling: + return self.GetLastChildRecursively(previousSibling) + else: + parent = self.GetItemParent(item) + if parent == self.GetRootItem() and \ + (self.GetWindowStyle() & wx.TR_HIDE_ROOT): + # Return an invalid item, because the root item is hidden + return previousSibling + else: + return parent + + def GetNextItem(self, item): + """ + Returns the item that is on the line immediately below item + (as is displayed when the tree is fully expanded). The returned + item is invalid if item is the last item in the tree. + + :param TreeItemId `item`: a :class:`TreeItemId` + :return: :class:`TreeItemId` of the next item or an invalid item + :rtype: :class:`TreeItemId` + + """ + if self.ItemHasChildren(item): + firstChild, cookie = self.GetFirstChild(item) + return firstChild + else: + return self.GetNextSiblingRecursively(item) + + def GetFirstItem(self): + """ + Returns the very first item in the tree. This is the root item + unless the root item is hidden. In that case the first child of + the root item is returned, if any. If the tree is empty, an + invalid tree item is returned. + + :return: :class:`TreeItemId` + :rtype: :class:`TreeItemId` + + """ + rootItem = self.GetRootItem() + if rootItem and (self.GetWindowStyle() & wx.TR_HIDE_ROOT): + firstChild, cookie = self.GetFirstChild(rootItem) + return firstChild + else: + return rootItem + + def GetLastChildRecursively(self, item): + """ + Returns the last child of the last child ... of item. If item + has no children, item itself is returned. So the returned item + is always valid, assuming a valid item has been passed. + + :param TreeItemId `item`: a :class:`TreeItemId` + :return: :class:`TreeItemId` of the last item or an invalid item + :rtype: :class:`TreeItemId` + + """ + lastChild = item + while self.ItemHasChildren(lastChild): + lastChild = self.GetLastChild(lastChild) + return lastChild + + def GetNextSiblingRecursively(self, item): + """ + Returns the next sibling of item if it has one. If item has no + next sibling the next sibling of the parent of item is returned. + If the parent has no next sibling the next sibling of the parent + of the parent is returned, etc. If none of the ancestors of item + has a next sibling, an invalid item is returned. + + :param TreeItemId `item`: a :class:`TreeItemId` + :return: :class:`TreeItemId` of the next item or an invalid item + :rtype: :class:`TreeItemId` + + """ + if item == self.GetRootItem(): + return wx.TreeItemId() # Return an invalid TreeItemId + nextSibling = self.GetNextSibling(item) + if nextSibling: + return nextSibling + else: + parent = self.GetItemParent(item) + return self.GetNextSiblingRecursively(parent) + + def GetSelection(self): + """ + Extend GetSelection to never return the root item if the + root item is hidden. + """ + selection = super(IterableTreeCtrl, self).GetSelection() + if selection == self.GetRootItem() and \ + (self.GetWindowStyle() & wx.TR_HIDE_ROOT): + return wx.TreeItemId() # Return an invalid TreeItemId + else: + return selection + + +# --------------------------------------------------------------------------- + + +class BasePopupFrame(wx.Frame): + """ + BasePopupFrame is the base class for platform specific versions of the + PopupFrame. The PopupFrame is the frame that is popped up by ComboTreeBox. + It contains the tree of items that the user can select one item from. Upon + selection, or when focus is lost, the frame is hidden. + """ + + def __init__(self, parent): + super(BasePopupFrame, self).__init__(parent, + style=wx.DEFAULT_FRAME_STYLE & wx.FRAME_FLOAT_ON_PARENT & + ~(wx.RESIZE_BORDER | wx.CAPTION)) + self._createInterior() + self._layoutInterior() + self._bindEventHandlers() + + def _createInterior(self): + self._tree = IterableTreeCtrl(self, + style=wx.TR_HIDE_ROOT|wx.TR_LINES_AT_ROOT|wx.TR_HAS_BUTTONS) + self._tree.AddRoot('Hidden root node') + + def _layoutInterior(self): + frameSizer = wx.BoxSizer(wx.HORIZONTAL) + frameSizer.Add(self._tree, flag=wx.EXPAND, proportion=1) + self.SetSizerAndFit(frameSizer) + + def _bindEventHandlers(self): + self._tree.Bind(wx.EVT_CHAR, self.OnChar) + self._tree.Bind(wx.EVT_TREE_ITEM_ACTIVATED, self.OnItemActivated) + self._tree.Bind(wx.EVT_LEFT_DOWN, self.OnMouseClick) + + def _bindKillFocus(self): + self._tree.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus) + + def _unbindKillFocus(self): + self._tree.Unbind(wx.EVT_KILL_FOCUS) + + def OnKillFocus(self, event): + # We hide the frame rather than destroy it, so it can be + # popped up again later. Use CallAfter so that clicking the combobox + # button doesn't immediately popup the frame again. + wx.CallAfter(self.Hide) + self.GetParent().NotifyNoItemSelected() + event.Skip() + + def OnChar(self, keyEvent): + if self._keyShouldHidePopup(keyEvent): + self.Hide() + self.GetParent().NotifyNoItemSelected() + keyEvent.Skip() + + def _keyShouldHidePopup(self, keyEvent): + return keyEvent.GetKeyCode() == wx.WXK_ESCAPE + + def OnMouseClick(self, event): + item, flags = self._tree.HitTest(event.GetPosition()) + if item and (flags & wx.TREE_HITTEST_ONITEMLABEL): + self._tree.SelectItem(item) + self.Hide() + self.GetParent().NotifyItemSelected(self._tree.GetItemText(item)) + else: + event.Skip() + + def OnItemActivated(self, event): + item = event.GetItem() + self.Hide() + self.GetParent().NotifyItemSelected(self._tree.GetItemText(item)) + + def Show(self): + self._bindKillFocus() + wx.CallAfter(self._tree.SetFocus) + super(BasePopupFrame, self).Show() + + def Hide(self): + self._unbindKillFocus() + super(BasePopupFrame, self).Hide() + + def GetTree(self): + return self._tree + + +class MSWPopupFrame(BasePopupFrame): + """MSWPopupFrame is the base class Windows PopupFrame.""" + def Show(self): + # Comply with the MS Windows Combobox behaviour: if the text in + # the text field is not in the tree, the first item in the tree + # is selected. + if not self._tree.GetSelection(): + self._tree.SelectItem(self._tree.GetFirstItem()) + super(MSWPopupFrame, self).Show() + + +class MACPopupFrame(BasePopupFrame): + """MacPopupFrame is the base class Mac PopupFrame.""" + def _bindKillFocus(self): + # On wxMac, the kill focus event doesn't work, but the + # deactivate event does: + self.Bind(wx.EVT_ACTIVATE, self.OnKillFocus) + + def _unbindKillFocus(self): + self.Unbind(wx.EVT_ACTIVATE) + + def OnKillFocus(self, event): + if not event.GetActive(): # We received a deactivate event + self.Hide() + wx.CallAfter(self.GetParent().NotifyNoItemSelected) + event.Skip() + + +class GTKPopupFrame(BasePopupFrame): + """GTKPopupFrame is the base class GTK PopupFrame.""" + def _keyShouldHidePopup(self, keyEvent): + # On wxGTK, Alt-Up also closes the popup: + return super(GTKPopupFrame, self)._keyShouldHidePopup(keyEvent) or \ + (keyEvent.AltDown() and keyEvent.GetKeyCode() == wx.WXK_UP) + + +# --------------------------------------------------------------------------- + + +class BaseComboTreeBox(object): + """ + BaseComboTreeBox is the base class for platform specific versions of the + ComboTreeBox. + """ + + def __init__(self, *args, **kwargs): + style = kwargs.pop('style', 0) + if style & wx.CB_READONLY: + style &= ~wx.CB_READONLY # We manage readonlyness ourselves + self._readOnly = True + else: + self._readOnly = False + if style & wx.CB_SORT: + style &= ~wx.CB_SORT # We manage sorting ourselves + self._sort = True + else: + self._sort = False + super(BaseComboTreeBox, self).__init__(style=style, *args, **kwargs) + self._createInterior() + self._layoutInterior() + self._bindEventHandlers() + + # Methods to construct the widget. + + def _createInterior(self): + self._popupFrame = self._createPopupFrame() + self._text = self._createTextCtrl() + self._button = self._createButton() + self._tree = self._popupFrame.GetTree() + + def _createTextCtrl(self): + return self # By default, the text control is the control itself. + + def _createButton(self): + return self # By default, the dropdown button is the control itself. + + def _createPopupFrame(self): + # It is a subclass responsibility to provide the right PopupFrame, + # depending on platform: + raise NotImplementedError + + def _layoutInterior(self): + pass # By default, there is no layout to be done. + + def _bindEventHandlers(self): + for eventSource, eventType, eventHandler in self._eventsToBind(): + eventSource.Bind(eventType, eventHandler) + + def _eventsToBind(self): + """ + _eventsToBind returns a list of eventSource, eventType, + eventHandlers tuples that will be bound. This method can be + extended to bind additional events. In that case, don't + forget to call _eventsToBind on the super class. + + :return: [(eventSource, eventType, eventHandlers), ] + :rtype: list + + """ + return [(self._text, wx.EVT_KEY_DOWN, self.OnKeyDown), + (self._text, wx.EVT_TEXT, self.OnText), + (self._button, wx.EVT_BUTTON, self.OnMouseClick)] + + # Event handlers + + def OnMouseClick(self, event): + if self._popupFrame.IsShown(): + self.Hide() + else: + self.Popup() + # Note that we don't call event.Skip() to prevent popping up the + # ComboBox's own box. + + def OnKeyDown(self, keyEvent): + if self._keyShouldNavigate(keyEvent): + self._navigateUpOrDown(keyEvent) + elif self._keyShouldPopUpTree(keyEvent): + self.Popup() + else: + keyEvent.Skip() + + def _keyShouldPopUpTree(self, keyEvent): + return (keyEvent.AltDown() or keyEvent.MetaDown()) and \ + keyEvent.GetKeyCode() == wx.WXK_DOWN + + def _keyShouldNavigate(self, keyEvent): + return keyEvent.GetKeyCode() in (wx.WXK_DOWN, wx.WXK_UP) and not \ + self._keyShouldPopUpTree(keyEvent) + + def _navigateUpOrDown(self, keyEvent): + item = self.GetSelection() + if item: + navigationMethods = {wx.WXK_DOWN: self._tree.GetNextItem, + wx.WXK_UP: self._tree.GetPreviousItem} + getNextItem = navigationMethods[keyEvent.GetKeyCode()] + nextItem = getNextItem(item) + else: + nextItem = self._tree.GetFirstItem() + if nextItem: + self.SetSelection(nextItem) + + def OnText(self, event): + event.Skip() + textValue = self._text.GetValue() + selection = self._tree.GetSelection() + if not selection or self._tree.GetItemText(selection) != textValue: + # We need to change the selection because it doesn't match the + # text just entered + item = self.FindString(textValue) + if item: + self._tree.SelectItem(item) + else: + self._tree.Unselect() + + # Methods called by the PopupFrame, to let the ComboTreeBox know + # about what the user did. + + def NotifyItemSelected(self, text): + """ + Simulate selection of an item by the user. This is meant to + be called by the PopupFrame when the user selects an item. + """ + self._text.SetValue(text) + self._postComboBoxSelectedEvent(text) + self.SetFocus() + + def _postComboBoxSelectedEvent(self, text): + """Simulate a selection event. """ + event = wx.CommandEvent(wx.wxEVT_COMMAND_COMBOBOX_SELECTED, + self.GetId()) + event.SetString(text) + self.GetEventHandler().ProcessEvent(event) + + def NotifyNoItemSelected(self): + """ + This is called by the PopupFrame when the user closes the + PopupFrame, without selecting an item. + """ + self.SetFocus() + + # Misc methods, not part of the ComboBox API. + + def Popup(self): + """Pops up the frame with the tree.""" + comboBoxSize = self.GetSize() + x, y = self.GetParent().ClientToScreen(self.GetPosition()) + y += comboBoxSize[1] + width = comboBoxSize[0] + height = 300 + self._popupFrame.SetDimensions(x, y, width, height) + # On wxGTK, when the Combobox width has been increased a call + # to SetMinSize is needed to force a resize of the popupFrame: + self._popupFrame.SetMinSize((width, height)) + self._popupFrame.Show() + + def Hide(self): + """Hide the popped up frame with the tree.""" + self._popupFrame.Hide() + + def GetTree(self): + """Returns the tree control that is popped up.""" + return self._popupFrame.GetTree() + + def FindClientData(self, clientData, parent=None): + """ + Finds the *first* item in the tree with client data equal to the + given clientData. If no such item exists, an invalid item is + returned. + + :param PyObject `clientData`: the client data to find + :keyword TreeItemId `parent`: :class:`TreeItemId` parent or None + :return: :class:`TreeItemId` + :rtype: :class:`TreeItemId` + + """ + parent = parent or self._tree.GetRootItem() + child, cookie = self._tree.GetFirstChild(parent) + while child: + if self.GetClientData(child) == clientData: + return child + else: + result = self.FindClientData(clientData, child) + if result: + return result + child, cookie = self._tree.GetNextChild(parent, cookie) + return child + + def SetClientDataSelection(self, clientData): + """ + Selects the item with the provided clientData in the control. + Returns True if the item belonging to the clientData has been + selected, False if it wasn't found in the control. + + :param PyObject `clientData`: the client data to find + :return: True if an item has been selected, otherwise False + :rtype: bool + + """ + item = self.FindClientData(clientData) + if item: + self._tree.SelectItem(item) + string = self._tree.GetItemText(item) + if self._text.GetValue() != string: + self._text.SetValue(string) + return True + else: + return False + + # The following methods are all part of the ComboBox API (actually + # the ControlWithItems API) and have been adapted to take TreeItemIds + # as parameter and return :class:`TreeItemId`s, rather than indices. + + def Append(self, itemText, parent=None, clientData=None): + """ + Adds the itemText to the control, associating the given clientData + with the item if not None. If parent is None, itemText is added + as a root item, else itemText is added as a child item of + parent. The return value is the :class:`TreeItemId` of the newly added + item. + + :param string `itemText`: text to add to the control + :keyword TreeItemId `parent`: if None item is added as a root, else it + is added as a child of the parent. + :keyword PyObject `clientData`: the client data to find + :return: :class:`TreeItemId` of newly added item + :rtype: :class:`TreeItemId` + + """ + if parent is None: + parent = self._tree.GetRootItem() + item = self._tree.AppendItem(parent, itemText, + data=wx.TreeItemData(clientData)) + if self._sort: + self._tree.SortChildren(parent) + return item + + def Clear(self): + """Removes all items from the control.""" + return self._tree.DeleteAllItems() + + def Delete(self, item): + """Deletes the item from the control.""" + return self._tree.Delete(item) + + def FindString(self, string, parent=None): + """ + Finds the *first* item in the tree with a label equal to the + given string. If no such item exists, an invalid item is + returned. + + :param string `string`: string to be found in label + :keyword TreeItemId `parent`: :class:`TreeItemId` parent or None + :return: :class:`TreeItemId` + :rtype: :class:`TreeItemId` + + """ + parent = parent or self._tree.GetRootItem() + child, cookie = self._tree.GetFirstChild(parent) + while child: + if self._tree.GetItemText(child) == string: + return child + else: + result = self.FindString(string, child) + if result: + return result + child, cookie = self._tree.GetNextChild(parent, cookie) + return child + + def GetSelection(self): + """ + Returns the :class:`TreeItemId` of the selected item or an invalid item + if no item is selected. + + :return: a TreeItemId + :rtype: :class:`TreeItemId` + + """ + selectedItem = self._tree.GetSelection() + if selectedItem and selectedItem != self._tree.GetRootItem(): + return selectedItem + else: + return self.FindString(self.GetValue()) + + def GetString(self, item): + """ + Returns the label of the given item. + + :param TreeItemId `item`: :class:`TreeItemId` for which to get the label + :return: label + :rtype: string + + """ + if item: + return self._tree.GetItemText(item) + else: + return '' + + def GetStringSelection(self): + """ + Returns the label of the selected item or an empty string if no item + is selected. + + :return: the label of the selected item or an empty string + :rtype: string + + """ + return self.GetValue() + + def Insert(self, itemText, previous=None, parent=None, clientData=None): + """ + Insert an item into the control before the ``previous`` item + and/or as child of the ``parent`` item. The itemText is associated + with clientData when not None. + + :param string `itemText`: the items label + :keyword TreeItemId `previous`: the previous item + :keyword TreeItemId `parent`: the parent item + :keyword PyObject `clientData`: the data to associate + :return: the create :class:`TreeItemId` + :rtype: :class:`TreeItemId` + + """ + data = wx.TreeItemData(clientData) + if parent is None: + parent = self._tree.GetRootItem() + if previous is None: + item = self._tree.InsertItemBefore(parent, 0, itemText, data=data) + else: + item = self._tree.InsertItem(parent, previous, itemText, data=data) + if self._sort: + self._tree.SortChildren(parent) + return item + + def IsEmpty(self): + """ + Returns True if the control is empty or False if it has some items. + + :return: True if control is empty + :rtype: boolean + + """ + return self.GetCount() == 0 + + def GetCount(self): + """ + Returns the number of items in the control. + + :return: items in control + :rtype: integer + + """ + # Note: We don't need to substract 1 for the hidden root item, + # because the TreeCtrl does that for us + return self._tree.GetCount() + + def SetSelection(self, item): + """ + Sets the provided item to be the selected item. + + :param TreeItemId `item`: Select this item + + """ + self._tree.SelectItem(item) + self._text.SetValue(self._tree.GetItemText(item)) + + Select = SetSelection + + def SetString(self, item, string): + """ + Sets the label for the provided item. + + :param TreeItemId `item`: item on which to set the label + :param string `string`: the label to set + + """ + self._tree.SetItemText(item, string) + if self._sort: + self._tree.SortChildren(self._tree.GetItemParent(item)) + + def SetStringSelection(self, string): + """ + Selects the item with the provided string in the control. + Returns True if the provided string has been selected, False if + it wasn't found in the control. + + :param string `string`: try to select the item with this string + :return: True if an item has been selected + :rtype: boolean + + """ + item = self.FindString(string) + if item: + if self._text.GetValue() != string: + self._text.SetValue(string) + self._tree.SelectItem(item) + return True + else: + return False + + def GetClientData(self, item): + """ + Returns the client data associated with the given item, if any. + + :param TreeItemId `item`: item for which to get clientData + :return: the client data + :rtype: PyObject + + """ + return self._tree.GetItemPyData(item) + + def SetClientData(self, item, clientData): + """ + Associate the given client data with the provided item. + + :param TreeItemId `item`: item for which to set the clientData + :param PyObject `clientData`: the data to set + + """ + self._tree.SetItemPyData(item, clientData) + + def GetValue(self): + """ + Returns the current value in the combobox text field. + + :return: the current value in the combobox text field + :rtype: string + + """ + if self._text == self: + return super(BaseComboTreeBox, self).GetValue() + else: + return self._text.GetValue() + + def SetValue(self, value): + """ + Sets the text for the combobox text field. + + NB: For a combobox with wxCB_READONLY style the string must be + in the combobox choices list, otherwise the call to SetValue() + is ignored. + + :param string `value`: set the combobox text field + + """ + item = self._tree.GetSelection() + if not item or self._tree.GetItemText(item) != value: + item = self.FindString(value) + if self._readOnly and not item: + return + if self._text == self: + super(BaseComboTreeBox, self).SetValue(value) + else: + self._text.SetValue(value) + if item: + if self._tree.GetSelection() != item: + self._tree.SelectItem(item) + else: + self._tree.Unselect() + + +class NativeComboTreeBox(BaseComboTreeBox, wx.ComboBox): + """ + NativeComboTreeBox, and any subclass, uses the native ComboBox as basis, + but prevent it from popping up its drop down list and instead pops up a + PopupFrame containing a tree of items. + """ + + def _eventsToBind(self): + events = super(NativeComboTreeBox, self)._eventsToBind() + # Bind all mouse click events to self.OnMouseClick so we can + # intercept those events and prevent the native Combobox from + # popping up its list of choices. + for eventType in (wx.EVT_LEFT_DOWN, wx.EVT_LEFT_DCLICK, + wx.EVT_MIDDLE_DOWN, wx.EVT_MIDDLE_DCLICK, + wx.EVT_RIGHT_DOWN, wx.EVT_RIGHT_DCLICK): + events.append((self._button, eventType, self.OnMouseClick)) + if self._readOnly: + events.append((self, wx.EVT_CHAR, self.OnChar)) + return events + + def OnChar(self, event): + # OnChar is only called when in read only mode. We don't call + # event.Skip() on purpose, to prevent the characters from being + # displayed in the text field. + pass + + +class MSWComboTreeBox(NativeComboTreeBox): + """ + MSWComboTreeBox adds one piece of functionality as compared to + NativeComboTreeBox: when the user browses through the tree, the + ComboTreeBox's text field is continuously updated to show the + currently selected item in the tree. If the user cancels + selecting a new item from the tree, e.g. by hitting escape, the + previous value (the one that was selected before the PopupFrame + was popped up) is restored. + """ + + def _createPopupFrame(self): + return MSWPopupFrame(self) + + def _eventsToBind(self): + events = super(MSWComboTreeBox, self)._eventsToBind() + events.append((self._tree, wx.EVT_TREE_SEL_CHANGED, + self.OnSelectionChangedInTree)) + return events + + def OnSelectionChangedInTree(self, event): + if self.IsBeingDeleted(): + return + item = event.GetItem() + if item: + selectedValue = self._tree.GetItemText(item) + if self.GetValue() != selectedValue: + self.SetValue(selectedValue) + event.Skip() + + def _keyShouldPopUpTree(self, keyEvent): + return super(MSWComboTreeBox, self)._keyShouldPopUpTree(keyEvent) or \ + (keyEvent.GetKeyCode() == wx.WXK_F4 and not keyEvent.HasModifiers()) or \ + ((keyEvent.AltDown() or keyEvent.MetaDown()) and \ + keyEvent.GetKeyCode() == wx.WXK_UP) + + def SetValue(self, value): + """ + Extend SetValue to also select the text in the + ComboTreeBox's text field. + + :param string `value`: set the value and select it + + """ + super(MSWComboTreeBox, self).SetValue(value) + # We select the text in the ComboTreeBox's text field. + # There is a slight complication, however. When the control is + # deleted, SetValue is called. But if we call SetMark at that + # time, wxPython will crash. We can prevent this by comparing the + # result of GetLastPosition and the length of the value. If they + # match, all is fine. If they don't match, we don't call SetMark. + if self._text.GetLastPosition() == len(value): + self._text.SetMark(0, self._text.GetLastPosition()) + + def Popup(self, *args, **kwargs): + """ + Extend Popup to store a copy of the current value, so we can + restore it later (in NotifyNoItemSelected). This is necessary + because MSWComboTreeBox will change the value as the user + browses through the items in the popped up tree. + """ + self._previousValue = self.GetValue() + super(MSWComboTreeBox, self).Popup(*args, **kwargs) + + def NotifyNoItemSelected(self, *args, **kwargs): + """ + Restore the value copied previously, because the user has + not selected a new value. + """ + self.SetValue(self._previousValue) + super(MSWComboTreeBox, self).NotifyNoItemSelected(*args, **kwargs) + + +class MACComboTreeBox(NativeComboTreeBox): + def _createPopupFrame(self): + return MACPopupFrame(self) + + def _createButton(self): + return self.GetChildren()[0] # The choice button + + def _keyShouldNavigate(self, keyEvent): + return False # No navigation with up and down on wxMac + + def _keyShouldPopUpTree(self, keyEvent): + return super(MACComboTreeBox, self)._keyShouldPopUpTree(keyEvent) or \ + keyEvent.GetKeyCode() == wx.WXK_DOWN + + +class GTKComboTreeBox(BaseComboTreeBox, wx.Panel): + """ + The ComboTreeBox widget for wxGTK. This is actually a work + around because on wxGTK, there doesn't seem to be a way to intercept + mouse events sent to the Combobox. Intercepting those events is + necessary to prevent the Combobox from popping up the list and pop up + the tree instead. So, until wxPython makes intercepting those events + possible we build a poor man's Combobox ourselves using a TextCtrl and + a BitmapButton. + """ + + def _createPopupFrame(self): + return GTKPopupFrame(self) + + def _createTextCtrl(self): + if self._readOnly: + style = wx.TE_READONLY + else: + style = 0 + return wx.TextCtrl(self, style=style) + + def _createButton(self): + bitmap = wx.ArtProvider.GetBitmap(wx.ART_GO_DOWN, client=wx.ART_BUTTON) + return wx.BitmapButton(self, bitmap=bitmap) + + def _layoutInterior(self): + panelSizer = wx.BoxSizer(wx.HORIZONTAL) + panelSizer.Add(self._text, flag=wx.EXPAND, proportion=1) + panelSizer.Add(self._button) + self.SetSizerAndFit(panelSizer) + + +# --------------------------------------------------------------------------- + + +def ComboTreeBox(*args, **kwargs): + """ + Factory function to create the right ComboTreeBox depending on + platform. You may force a specific class, e.g. for testing + purposes, by setting the keyword argument 'platform', e.g. + 'platform=GTK' or 'platform=MSW' or 'platform=MAC'. + + :keyword string `platform`: 'GTK'|'MSW'|'MAC' can be used to override the + actual platform for testing + + """ + + platform = kwargs.pop('platform', None) or wx.PlatformInfo[0][4:7] + ComboTreeBoxClassName = '%sComboTreeBox' % platform + ComboTreeBoxClass = globals()[ComboTreeBoxClassName] + return ComboTreeBoxClass(*args, **kwargs) + diff --git a/wx/lib/customtreectrl.py b/wx/lib/customtreectrl.py new file mode 100644 index 00000000..f84c0766 --- /dev/null +++ b/wx/lib/customtreectrl.py @@ -0,0 +1,13 @@ +# ============================================================== # +# This is now just a stub, importing the real module which lives # +# under wx.lib.agw. +# ============================================================== # + +""" +Attention! CustomTreeCtrl now lives in wx.lib.agw, together with +its friends in the Advanced Generic Widgets family. + +Please update your code! +""" + +from wx.lib.agw.customtreectrl import * \ No newline at end of file diff --git a/wx/lib/delayedresult.py b/wx/lib/delayedresult.py new file mode 100644 index 00000000..fdb0a4b5 --- /dev/null +++ b/wx/lib/delayedresult.py @@ -0,0 +1,420 @@ +""" +This module supports the thread-safe, asynchronous transmission of data +('delayed results') from a worker (non-GUI) thread to the main thread. Ie you don't +need to mutex lock any data, the worker thread doesn't wait (or even check) +for the result to be received, and the main thread doesn't wait for the +worker thread to send the result. Instead, the consumer will be called +automatically by the wx app when the worker thread result is available. + +In most cases you just need to use startWorker() with the correct parameters +(your worker function and your 'consumer' in the simplest of cases). The +only requirement on consumer is that it must accept a DelayedResult instance +as first arg. + +In the following example, this will call consumer(delayedResult) with the +return value from workerFn:: + + from delayedresult import startWorker + startWorker(consumer, workerFn) + +More advanced uses: + +- The other parameters to startWorker() +- Derive from Producer to override _extraInfo (e.g. to provide traceback info) +- Create your own worker-function-thread wrapper instead of using Producer +- Create your own Handler-like wrapper to pre- or post-process the result + (see PreProcessChain) +- Derive from Sender to use your own way of making result hop over the + "thread boundary" (from non-main thread to main thread), e.g. using Queue + +Thanks to Josiah Carlson for critical feedback/ideas that helped me +improve this module. + +:Copyright: (c) 2006 by Oliver Schoenborn +:License: wxWidgets license +:Version: 1.0 + +""" + +__author__ = 'Oliver Schoenborn at utoronto dot ca' +__version__ = '1.0' + +__all__ = ('Sender', 'SenderNoWx', 'SenderWxEvent', 'SenderCallAfter', + 'Handler', 'DelayedResult', 'Producer', 'startWorker', 'PreProcessChain') + + +import wx +import threading +import traceback + + +class Struct: + """ + An object that has attributes built from the dictionary given in + constructor. So ss=Struct(a=1, b='b') will satisfy assert ss.a == 1 + and assert ss.b == 'b'. + """ + + def __init__(self, **kwargs): + self.__dict__.update( kwargs ) + + +class Handler: + """ + Bind some of the arguments and keyword arguments of a callable ('listener'). + Then when the Handler instance is called (e.g. `handler(result, **kwargs)`) + the result is passed as first argument to callable, the kwargs is + combined with those given at construction, and the args are those + given at construction. Its return value is returned. + """ + def __init__(self, listener, *args, **kwargs ): + """Bind args and kwargs to listener. """ + self.__listener = listener + self.__args = args + self.__kwargs = kwargs + + def __call__(self, result, **moreKwargs): + """Listener is assumed to take result as first `arg`, then `*args`, + then the combination of moreKwargs and the kwargs given at construction.""" + if moreKwargs: + moreKwargs.update(self.__kwargs) + else: + moreKwargs = self.__kwargs + return self.__listener(result, *self.__args, **moreKwargs) + + +class Sender: + """ + Base class for various kinds of senders. A sender sends a result + produced by a worker funtion to a result handler (listener). Note + that each sender can be given a "job id". This can be anything + (number, string, id, and object, etc) and is not used, it is + simply added as attribute whenever a DelayedResult is created. + This allows you to know, if desired, what result corresponds to + which sender. Note that uniqueness is not necessary. + + Derive from this class if none of the existing derived classes + are adequate, and override _sendImpl(). + """ + + def __init__(self, jobID=None): + """The optional jobID can be anything that you want to use to + track which sender particular results come from. """ + self.__jobID = jobID + + def getJobID(self): + """Return the jobID given at construction""" + return self.__jobID + + def sendResult(self, result): + """This will send the result to handler, using whatever + technique the derived class uses. """ + delayedResult = DelayedResult(result, jobID=self.__jobID) + self._sendImpl(delayedResult) + + def sendException(self, exception, extraInfo = None, originalTb = None): + """Use this when the worker function raised an exception. + The *exception* is the instance of Exception caught. The extraInfo + could be anything you want (e.g. locals or traceback etc), + it will be added to the exception as attribute 'extraInfo'. The + exception will be raised when DelayedResult.get() is called.""" + assert exception is not None + delayedResult = DelayedResult(extraInfo, + exception=exception, jobID=self.__jobID, originalTb=originalTb) + self._sendImpl(delayedResult) + + def _sendImpl(self, delayedResult): + msg = '_sendImpl() must be implemented in %s' % self.__class__ + raise NotImplementedError(msg) + + +class SenderNoWx( Sender ): + """ + Sender that works without wx. The results are sent directly, ie + the consumer will get them "in the worker thread". So it should + only be used for testing. + """ + def __init__(self, consumer, jobID=None, args=(), kwargs={}): + """The consumer can be any callable of the form + `callable(result, *args, **kwargs)`""" + Sender.__init__(self, jobID) + if args or kwargs: + self.__consumer = Handler(consumer, *args, **kwargs) + else: + self.__consumer = consumer + + def _sendImpl(self, delayedResult): + self.__consumer(delayedResult) + + +class SenderWxEvent( Sender ): + """ + This sender sends the delayed result produced in the worker thread + to an event handler in the main thread, via a wx event of class + *eventClass*. The result is an attribute of the event (default: + "delayedResult". + """ + def __init__(self, handler, eventClass, resultAttr="delayedResult", + jobID=None, **kwargs): + """The handler must derive from wx.EvtHandler. The event class + is typically the first item in the pair returned by + wx.lib.newevent.NewEvent(). You can use the *resultAttr* + to change the attribute name of the generated event's + delayed result. """ + Sender.__init__(self, jobID) + if not isinstance(handler, wx.EvtHandler): + msg = 'SenderWxEvent(handler=%s, ...) not allowed,' % type(handler) + msg = '%s handler must derive from wx.EvtHandler' % msg + raise ValueError(msg) + self.__consumer = Struct(handler=handler, eventClass=eventClass, + resultAttr=resultAttr, kwargs=kwargs) + + def _sendImpl(self, delayedResult): + """Must not modify the consumer (that was created at construction) + since might be shared by several senders, each sending from + separate threads.""" + consumer = self.__consumer + kwargs = consumer.kwargs.copy() + kwargs[ consumer.resultAttr ] = delayedResult + event = consumer.eventClass(** kwargs) + wx.PostEvent(consumer.handler, event) + + +class SenderCallAfter( Sender ): + """ + This sender sends the delayed result produced in the worker thread + to a callable in the main thread, via wx.CallAfter. + """ + def __init__(self, listener, jobID=None, args=(), kwargs={}): + Sender.__init__(self, jobID) + if args or kwargs: + self.__consumer = Handler(listener, *args, **kwargs) + else: + self.__consumer = listener + + def _sendImpl(self, delayedResult): + wx.CallAfter(self.__consumer, delayedResult) + + +class DelayedResult: + """ + Represent the actual delayed result coming from the non-main thread. + An instance of this is given to the result handler. This result is + either a (reference to a) the value sent, or an exception. + If the latter, the exception is raised when the get() method gets + called. + """ + + def __init__(self, result, jobID=None, exception = None, originalTb = None): + """You should never have to call this yourself. A DelayedResult + is created by a concrete Sender for you.""" + self.__result = result + self.__exception = exception + self.__original_traceback = originalTb + self.__jobID = jobID + + def getJobID(self): + """Return the jobID given when Sender initialized, + or None if none given. """ + return self.__jobID + + def get(self): + """Get the result. If an exception was sent instead of a result, + (via Sender's sendExcept()), that **exception is raised**, and + the original traceback is available as the 'originalTraceback' + variable in the exception object. + + Otherwise, the result is simply returned. + """ + if self.__exception: # exception was raised! + self.__exception.extraInfo = self.__result + self.__exception.originalTraceback = self.__original_traceback + raise self.__exception + + return self.__result + + +class AbortedException(Exception): + """Raise this in your worker function so that the sender knows + not to send a result to handler.""" + pass + + +class Producer(threading.Thread): + """ + Represent the worker thread that produces delayed results. + It causes the given function to run in a separate thread, + and a sender to be used to send the return value of the function. + As with any threading.Thread, instantiate and call start(). + Note that if the workerFn raises AbortedException, the result is not + sent and the thread terminates gracefully. + """ + + def __init__(self, sender, workerFn, args=(), kwargs={}, + name=None, group=None, daemon=False, + sendReturn=True, senderArg=None): + """The sender will send the return value of + `workerFn(*args, **kwargs)` to the main thread. The name and group + are same as threading.Thread constructor parameters. Daemon causes + setDaemon() to be called. If sendReturn is False, then the return + value of workerFn() will not be sent. If senderArg is given, it + must be the name of the keyword arg to use to pass the sender into + the workerFn, so the function can send (typically many) results.""" + if senderArg: + kwargs[senderArg] = sender + def wrapper(): + try: + result = workerFn(*args, **kwargs) + except AbortedException: + pass + except Exception, exc: + originalTb = traceback.format_exc() + extraInfo = self._extraInfo(exc) + sender.sendException(exc, extraInfo, originalTb) + else: + if sendReturn: + sender.sendResult(result) + + threading.Thread.__init__(self, name=name, group=group, target=wrapper) + if daemon: + self.setDaemon(daemon) + + def _extraInfo(self, exception): + """This method could be overridden in a derived class to provide + extra information when an exception is being sent instead of a + result. """ + return None + + +class AbortEvent: + """ + Convenience class that represents a kind of threading.Event that + raises AbortedException when called (see the __call__ method, everything + else is just to make it look like threading.Event). + """ + + def __init__(self): + self.__ev = threading.Event() + + def __call__(self, timeout=None): + """See if event has been set (wait at most timeout if given). If so, + raise AbortedException. Otherwise return None. Allows you to do + 'while not event():' which will always succeed unless the event + has been set (then AbortedException will cause while to exit).""" + if timeout: + self.__ev.wait(timeout) + if self.__ev.isSet(): + raise AbortedException() + return None + + def __getattr__(self, name): + """This allows us to be a kind of threading.Event.""" + if name in ('set','clear','wait','isSet'): + return getattr(self.__ev, name) + + +def startWorker( + consumer, workerFn, + cargs=(), ckwargs={}, + wargs=(), wkwargs={}, + jobID=None, group=None, daemon=False, + sendReturn=True, senderArg=None): + """ + Convenience function to send data produced by `workerFn(*wargs, **wkwargs)` + running in separate thread, to a `consumer(*cargs, **ckwargs)` running in + the main thread. This function merely creates a SenderCallAfter (or a + SenderWxEvent, if consumer derives from wx.EvtHandler), and a Producer, + and returns immediately after starting the Producer thread. The jobID + is used for the Sender and as name for the Producer thread. Returns the + thread created, in case caller needs join/etc. + """ + + if isinstance(consumer, wx.EvtHandler): + eventClass = cargs[0] + sender = SenderWxEvent(consumer, eventClass, jobID=jobID, **ckwargs) + else: + sender = SenderCallAfter(consumer, jobID, args=cargs, kwargs=ckwargs) + + thread = Producer( + sender, workerFn, args=wargs, kwargs=wkwargs, + name=jobID, group=group, daemon=daemon, + senderArg=senderArg, sendReturn=sendReturn) + + thread.start() + return thread + + +class PreProcessChain: + """ + Represent a 'delayed result pre-processing chain', a kind of Handler. + Useful when lower-level objects need to apply a sequence of transformations + to the delayed result before handing it over to a final handler. + This allows the starter of the worker function to not know + anything about the lower-level objects. + """ + def __init__(self, handler, *args, **kwargs): + """Wrap `handler(result, *args, **kwargs)` so that the result + it receives has been transformed by us. """ + if handler is None:# assume rhs is a chain + self.__chain = args[0] + else: + if args or kwargs: + handler = Handler(handler, *args, **kwargs) + self.__chain = [handler] + + def addSub(self, callable, *args, **kwargs): + """Add a sub-callable, ie a `callable(result, *args, **kwargs)` + that returns a transformed result to the previously added + sub-callable (or the handler given at construction, if this is + the first call to addSub). """ + self.__chain.append( Handler(callable, *args, **kwargs) ) + + def clone(self): + """Clone the chain. Shallow only. Useful when several threads + must be started but have different sub-callables. """ + return PreProcessChain(None, self.__chain[:] ) + + def cloneAddSub(self, callable, *args, **kwargs): + """Convenience method that first clones self, then calls addSub() + on that clone with given arguments. """ + cc = self.clone() + cc.addSub(callable, *args, **kwargs) + + def count(self): + """How many pre-processors in the chain""" + return len(self.__chain) + + class Traverser: + """ + Traverses the chain of pre-processors it is given, transforming + the original delayedResult along the way. The return value of each + callable added via addSub() is given to the previous addSub() callable, + until the handler is reached. + """ + def __init__(self, delayedResult, chain): + self.__dr = delayedResult + self.__chain = chain + + def get(self): + """This makes handler think we are a delayedResult.""" + if not self.__chain: + return self.__dr.get() + + handler = self.__chain[0] + del self.__chain[0] + return handler(self) + + def getJobID(self): + """Return the job id for the delayedResult we transform.""" + return self.__dr.getJobID() + + + def __call__(self, delayedResult): + """This makes us a Handler. We just call handler(Traverser). The + handler will think it is getting a delayed result, but in fact + will be getting an instance of Traverser, which will take care + of properly applying the chain of transformations to delayedResult.""" + chainTrav = self.Traverser(delayedResult, self.__chain[1:]) + handler = self.__chain[0] + handler( chainTrav ) + diff --git a/wx/lib/dialogs.py b/wx/lib/dialogs.py new file mode 100644 index 00000000..c55bfa97 --- /dev/null +++ b/wx/lib/dialogs.py @@ -0,0 +1,505 @@ +#---------------------------------------------------------------------- +# Name: wx.lib.dialogs +# Purpose: ScrolledMessageDialog, MultipleChoiceDialog and +# function wrappers for the common dialogs by Kevin Altis. +# +# Author: Various +# +# Created: 3-January-2002 +# RCS-ID: $Id$ +# Copyright: (c) 2002 by Total Control Software +# Licence: wxWindows license +#---------------------------------------------------------------------- +# 12/01/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o Updated for 2.5 compatability. +# +# 12/18/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o wxScrolledMessageDialog -> ScrolledMessageDialog +# o wxMultipleChoiceDialog -> MultipleChoiceDialog +# + +import wx +import wx.lib.layoutf as layoutf + +#---------------------------------------------------------------------- + +class ScrolledMessageDialog(wx.Dialog): + def __init__(self, parent, msg, caption, + pos=wx.DefaultPosition, size=(500,300), + style=wx.DEFAULT_DIALOG_STYLE): + wx.Dialog.__init__(self, parent, -1, caption, pos, size, style) + x, y = pos + if x == -1 and y == -1: + self.CenterOnScreen(wx.BOTH) + + self.text = text = wx.TextCtrl(self, -1, msg, + style=wx.TE_MULTILINE | wx.TE_READONLY) + + ok = wx.Button(self, wx.ID_OK, "OK") + ok.SetDefault() + lc = layoutf.Layoutf('t=t5#1;b=t5#2;l=l5#1;r=r5#1', (self,ok)) + text.SetConstraints(lc) + + lc = layoutf.Layoutf('b=b5#1;x%w50#1;w!80;h*', (self,)) + ok.SetConstraints(lc) + self.SetAutoLayout(1) + self.Layout() + + +class MultipleChoiceDialog(wx.Dialog): + def __init__(self, parent, msg, title, lst, pos = wx.DefaultPosition, + size = (200,200), style = wx.DEFAULT_DIALOG_STYLE): + wx.Dialog.__init__(self, parent, -1, title, pos, size, style) + + x, y = pos + if x == -1 and y == -1: + self.CenterOnScreen(wx.BOTH) + + stat = wx.StaticText(self, -1, msg) + self.lbox = wx.ListBox(self, 100, wx.DefaultPosition, wx.DefaultSize, + lst, wx.LB_MULTIPLE) + + ok = wx.Button(self, wx.ID_OK, "OK") + ok.SetDefault() + cancel = wx.Button(self, wx.ID_CANCEL, "Cancel") + + dlgsizer = wx.BoxSizer(wx.VERTICAL) + dlgsizer.Add(stat, 0, wx.ALL, 4) + dlgsizer.Add(self.lbox, 1, wx.EXPAND | wx.ALL, 4) + + btnsizer = wx.StdDialogButtonSizer() + btnsizer.AddButton(ok) + btnsizer.AddButton(cancel) + btnsizer.Realize() + + dlgsizer.Add(btnsizer, 0, wx.ALL | wx.ALIGN_RIGHT, 4) + + self.SetSizer(dlgsizer) + + self.lst = lst + self.Layout() + + def GetValue(self): + return self.lbox.GetSelections() + + def GetValueString(self): + sel = self.lbox.GetSelections() + val = [ self.lst[i] for i in sel ] + return tuple(val) + + +#---------------------------------------------------------------------- +""" +function wrappers for wxPython system dialogs +Author: Kevin Altis +Date: 2003-1-2 +Rev: 3 + +This is the third refactor of the PythonCard dialog.py module +for inclusion in the main wxPython distribution. There are a number of +design decisions and subsequent code refactoring to be done, so I'm +releasing this just to get some feedback. + +rev 3: +- result dictionary replaced by DialogResults class instance +- should message arg be replaced with msg? most wxWindows dialogs + seem to use the abbreviation? + +rev 2: +- All dialog classes have been replaced by function wrappers +- Changed arg lists to more closely match wxWindows docs and wxPython.lib.dialogs +- changed 'returned' value to the actual button id the user clicked on +- added a returnedString value for the string version of the return value +- reworked colorDialog and fontDialog so you can pass in just a color or font + for the most common usage case +- probably need to use colour instead of color to match the English English + spelling in wxWindows (sigh) +- I still think we could lose the parent arg and just always use None +""" + +class DialogResults: + def __init__(self, returned): + self.returned = returned + self.accepted = returned in (wx.ID_OK, wx.ID_YES) + self.returnedString = returnedString(returned) + + def __repr__(self): + return str(self.__dict__) + +def returnedString(ret): + if ret == wx.ID_OK: + return "Ok" + elif ret == wx.ID_CANCEL: + return "Cancel" + elif ret == wx.ID_YES: + return "Yes" + elif ret == wx.ID_NO: + return "No" + + +## findDialog was created before wxPython got a Find/Replace dialog +## but it may be instructive as to how a function wrapper can +## be added for your own custom dialogs +## this dialog is always modal, while wxFindReplaceDialog is +## modeless and so doesn't lend itself to a function wrapper +def findDialog(parent=None, searchText='', wholeWordsOnly=0, caseSensitive=0): + dlg = wx.Dialog(parent, -1, "Find", wx.DefaultPosition, (380, 120)) + + wx.StaticText(dlg, -1, 'Find what:', (7, 10)) + wSearchText = wx.TextCtrl(dlg, -1, searchText, (80, 7), (195, -1)) + wSearchText.SetValue(searchText) + wx.Button(dlg, wx.ID_OK, "Find Next", (285, 5), wx.DefaultSize).SetDefault() + wx.Button(dlg, wx.ID_CANCEL, "Cancel", (285, 35), wx.DefaultSize) + + wWholeWord = wx.CheckBox(dlg, -1, 'Match whole word only', + (7, 35), wx.DefaultSize, wx.NO_BORDER) + + if wholeWordsOnly: + wWholeWord.SetValue(1) + + wCase = wx.CheckBox(dlg, -1, 'Match case', (7, 55), wx.DefaultSize, wx.NO_BORDER) + + if caseSensitive: + wCase.SetValue(1) + + wSearchText.SetSelection(0, len(wSearchText.GetValue())) + wSearchText.SetFocus() + + result = DialogResults(dlg.ShowModal()) + result.searchText = wSearchText.GetValue() + result.wholeWordsOnly = wWholeWord.GetValue() + result.caseSensitive = wCase.GetValue() + dlg.Destroy() + return result + + +def colorDialog(parent=None, colorData=None, color=None): + if colorData: + dialog = wx.ColourDialog(parent, colorData) + else: + dialog = wx.ColourDialog(parent) + dialog.GetColourData().SetChooseFull(1) + + if color is not None: + dialog.GetColourData().SetColour(color) + + result = DialogResults(dialog.ShowModal()) + result.colorData = dialog.GetColourData() + result.color = result.colorData.GetColour().Get() + dialog.Destroy() + return result + + +## it is easier to just duplicate the code than +## try and replace color with colour in the result +def colourDialog(parent=None, colourData=None, colour=None): + if colourData: + dialog = wx.ColourDialog(parent, colourData) + else: + dialog = wx.ColourDialog(parent) + dialog.GetColourData().SetChooseFull(1) + + if colour is not None: + dialog.GetColourData().SetColour(color) + + result = DialogResults(dialog.ShowModal()) + result.colourData = dialog.GetColourData() + result.colour = result.colourData.GetColour().Get() + dialog.Destroy() + return result + + +def fontDialog(parent=None, fontData=None, font=None): + if fontData is None: + fontData = wx.FontData() + fontData.SetColour(wx.BLACK) + fontData.SetInitialFont(wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)) + + if font is not None: + fontData.SetInitialFont(font) + + dialog = wx.FontDialog(parent, fontData) + result = DialogResults(dialog.ShowModal()) + + if result.accepted: + fontData = dialog.GetFontData() + result.fontData = fontData + result.color = fontData.GetColour().Get() + result.colour = result.color + result.font = fontData.GetChosenFont() + else: + result.color = None + result.colour = None + result.font = None + + dialog.Destroy() + return result + + +def textEntryDialog(parent=None, message='', title='', defaultText='', + style=wx.OK | wx.CANCEL): + dialog = wx.TextEntryDialog(parent, message, title, defaultText, style) + result = DialogResults(dialog.ShowModal()) + result.text = dialog.GetValue() + dialog.Destroy() + return result + + +def messageDialog(parent=None, message='', title='Message box', + aStyle = wx.OK | wx.CANCEL | wx.CENTRE, + pos=wx.DefaultPosition): + dialog = wx.MessageDialog(parent, message, title, aStyle, pos) + result = DialogResults(dialog.ShowModal()) + dialog.Destroy() + return result + + +## KEA: alerts are common, so I'm providing a class rather than +## requiring the user code to set up the right icons and buttons +## the with messageDialog function +def alertDialog(parent=None, message='', title='Alert', pos=wx.DefaultPosition): + return messageDialog(parent, message, title, wx.ICON_EXCLAMATION | wx.OK, pos) + + +def scrolledMessageDialog(parent=None, message='', title='', pos=wx.DefaultPosition, + size=(500,300)): + + dialog = ScrolledMessageDialog(parent, message, title, pos, size) + result = DialogResults(dialog.ShowModal()) + dialog.Destroy() + return result + + +def fileDialog(parent=None, title='Open', directory='', filename='', wildcard='*.*', + style=wx.OPEN | wx.MULTIPLE): + + dialog = wx.FileDialog(parent, title, directory, filename, wildcard, style) + result = DialogResults(dialog.ShowModal()) + if result.accepted: + result.paths = dialog.GetPaths() + else: + result.paths = None + dialog.Destroy() + return result + + +## openFileDialog and saveFileDialog are convenience functions +## they represent the most common usages of the fileDialog +## with the most common style options +def openFileDialog(parent=None, title='Open', directory='', filename='', + wildcard='All Files (*.*)|*.*', + style=wx.OPEN | wx.MULTIPLE): + return fileDialog(parent, title, directory, filename, wildcard, style) + + +def saveFileDialog(parent=None, title='Save', directory='', filename='', + wildcard='All Files (*.*)|*.*', + style=wx.SAVE | wx.OVERWRITE_PROMPT): + return fileDialog(parent, title, directory, filename, wildcard, style) + + +def dirDialog(parent=None, message='Choose a directory', path='', style=0, + pos=wx.DefaultPosition, size=wx.DefaultSize): + + dialog = wx.DirDialog(parent, message, path, style, pos, size) + result = DialogResults(dialog.ShowModal()) + if result.accepted: + result.path = dialog.GetPath() + else: + result.path = None + dialog.Destroy() + return result + +directoryDialog = dirDialog + + +def singleChoiceDialog(parent=None, message='', title='', lst=[], + style=wx.OK | wx.CANCEL | wx.CENTRE): + dialog = wx.SingleChoiceDialog(parent, message, title, list(lst), style | wx.DEFAULT_DIALOG_STYLE) + result = DialogResults(dialog.ShowModal()) + result.selection = dialog.GetStringSelection() + dialog.Destroy() + return result + + +def multipleChoiceDialog(parent=None, message='', title='', lst=[], + pos=wx.DefaultPosition, size=wx.DefaultSize): + + dialog = wx.MultiChoiceDialog(parent, message, title, lst, + wx.CHOICEDLG_STYLE, pos) + result = DialogResults(dialog.ShowModal()) + result.selection = tuple([lst[i] for i in dialog.GetSelections()]) + dialog.Destroy() + return result + + + +#--------------------------------------------------------------------------- + +try: + wx.CANCEL_DEFAULT + wx.OK_DEFAULT +except AttributeError: + wx.CANCEL_DEFAULT = 0 + wx.OK_DEFAULT = 0 + + + +class MultiMessageDialog(wx.Dialog): + """ + A dialog like wx.MessageDialog, but with an optional 2nd message string + that is shown in a scrolled window, and also allows passing in the icon to + be shown instead of the stock error, question, etc. icons. The btnLabels + can be used if you'd like to change the stock labels on the buttons, it's + a dictionary mapping stock IDs to label strings. + """ + CONTENT_MAX_W = 550 + CONTENT_MAX_H = 350 + + def __init__(self, parent, message, caption = "Message Box", msg2="", + style = wx.OK | wx.CANCEL, pos = wx.DefaultPosition, icon=None, + btnLabels=None): + if 'wxMac' not in wx.PlatformInfo: + title = caption # the caption will be displayed inside the dialog on Macs + else: + title = "" + + wx.Dialog.__init__(self, parent, -1, title, pos, + style = wx.DEFAULT_DIALOG_STYLE | style & (wx.STAY_ON_TOP | wx.DIALOG_NO_PARENT)) + + bitmap = None + isize = (32,32) + + # was an icon passed to us? + if icon is not None: + if isinstance(icon, wx.Icon): + bitmap = wx.BitmapFromIcon(icon) + elif isinstance(icon, wx.Image): + bitmap = wx.BitmapFromImage(icon) + else: + assert isinstance(icon, wx.Bitmap) + bitmap = icon + + else: + # check for icons in the style flags + artid = None + if style & wx.ICON_ERROR or style & wx.ICON_HAND: + artid = wx.ART_ERROR + elif style & wx.ICON_EXCLAMATION: + artid = wx.ART_WARNING + elif style & wx.ICON_QUESTION: + artid = wx.ART_QUESTION + elif style & wx.ICON_INFORMATION: + artid = wx.ART_INFORMATION + + if artid is not None: + bitmap = wx.ArtProvider.GetBitmap(artid, wx.ART_MESSAGE_BOX, isize) + + if bitmap: + bitmap = wx.StaticBitmap(self, -1, bitmap) + else: + bitmap = isize # will be a spacer when added to the sizer + + # Sizer to contain the icon, text area and buttons + sizer = wx.BoxSizer(wx.HORIZONTAL) + sizer.Add(bitmap, 0, wx.TOP|wx.LEFT, 12) + sizer.Add((10,10)) + + # Make the text area + messageSizer = wx.BoxSizer(wx.VERTICAL) + if 'wxMac' in wx.PlatformInfo and caption: + caption = wx.StaticText(self, -1, caption) + caption.SetFont(wx.Font(18, wx.SWISS, wx.NORMAL, wx.BOLD)) + messageSizer.Add(caption) + messageSizer.Add((10,10)) + + stext = wx.StaticText(self, -1, message) + #stext.SetLabelMarkup(message) Wrap() causes all markup to be lost, so don't try to use it yet... + stext.Wrap(self.CONTENT_MAX_W) + messageSizer.Add(stext) + + if msg2: + messageSizer.Add((15,15)) + t = wx.TextCtrl(self, style=wx.TE_MULTILINE|wx.TE_READONLY|wx.TE_RICH|wx.TE_DONTWRAP) + t.SetValue(msg2) + + # Set size to be used by the sizer based on the message content, + # with good maximums + dc = wx.ClientDC(t) + dc.SetFont(t.GetFont()) + w,h,lh = dc.GetMultiLineTextExtent(msg2) + w = min(self.CONTENT_MAX_W, 10 + w + wx.SystemSettings.GetMetric(wx.SYS_VSCROLL_X)) + h = min(self.CONTENT_MAX_H, 10 + h) + t.SetMinSize((w,h)) + messageSizer.Add(t, 0, wx.EXPAND) + + # Make the buttons + buttonSizer = self.CreateStdDialogButtonSizer( + style & (wx.OK | wx.CANCEL | wx.YES_NO | wx.NO_DEFAULT + | wx.CANCEL_DEFAULT | wx.YES_DEFAULT | wx.OK_DEFAULT + )) + self.Bind(wx.EVT_BUTTON, self.OnButton) + if btnLabels: + for sid, label in btnLabels.iteritems(): + btn = self.FindWindowById(sid) + if btn: + btn.SetLabel(label) + messageSizer.Add(wx.Size(1, 15)) + messageSizer.Add(buttonSizer, 0, wx.LEFT | wx.RIGHT | wx.BOTTOM | wx.EXPAND, 12) + + sizer.Add(messageSizer, 0, wx.LEFT | wx.RIGHT | wx.TOP, 12) + self.SetSizer(sizer) + self.Fit() + if parent: + self.CenterOnParent() + else: + self.CenterOnScreen() + + for c in self.Children: + if isinstance(c, wx.Button): + wx.CallAfter(c.SetFocus) + break + + + def OnButton(self, evt): + if self.IsModal(): + self.EndModal(evt.EventObject.Id) + else: + self.Close() + + + + +def MultiMessageBox(message, caption, msg2="", style=wx.OK, parent=None, + icon=None, btnLabels=None): + """ + A function like wx.MessageBox which uses MultiMessageDialog. + """ + #if not style & wx.ICON_NONE and not style & wx.ICON_MASK: + if not style & wx.ICON_MASK: + if style & wx.YES: + style |= wx.ICON_QUESTION + else: + style |= wx.ICON_INFORMATION + + dlg = MultiMessageDialog(parent, message, caption, msg2, style, + icon=icon, btnLabels=btnLabels) + ans = dlg.ShowModal() + dlg.Destroy() + + if ans == wx.ID_OK: + return wx.OK + elif ans == wx.ID_YES: + return wx.YES + elif ans == wx.ID_NO: + return wx.NO + elif ans == wx.ID_CANCEL: + return wx.CANCEL + + print "unexpected return code from MultiMessageDialog??" + return wx.CANCEL + + +#--------------------------------------------------------------------------- diff --git a/wx/lib/docview.py b/wx/lib/docview.py new file mode 100644 index 00000000..52b59a44 --- /dev/null +++ b/wx/lib/docview.py @@ -0,0 +1,3230 @@ +#---------------------------------------------------------------------------- +# Name: docview.py +# Purpose: Port of the wxWindows docview classes +# +# Author: Peter Yared +# +# Created: 5/15/03 +# CVS-ID: $Id$ +# Copyright: (c) 2003-2006 ActiveGrid, Inc. (Port of wxWindows classes by Julian Smart et al) +# License: wxWindows license +#---------------------------------------------------------------------------- + + +import os +import os.path +import shutil +import wx +import sys +_ = wx.GetTranslation + + +#---------------------------------------------------------------------- +# docview globals +#---------------------------------------------------------------------- + +DOC_SDI = 1 +DOC_MDI = 2 +DOC_NEW = 4 +DOC_SILENT = 8 +DOC_OPEN_ONCE = 16 +DOC_NO_VIEW = 32 +DEFAULT_DOCMAN_FLAGS = DOC_SDI & DOC_OPEN_ONCE + +TEMPLATE_VISIBLE = 1 +TEMPLATE_INVISIBLE = 2 +TEMPLATE_NO_CREATE = (4 | TEMPLATE_VISIBLE) +DEFAULT_TEMPLATE_FLAGS = TEMPLATE_VISIBLE + +MAX_FILE_HISTORY = 9 + + +#---------------------------------------------------------------------- +# Convenience functions from wxWindows used in docview +#---------------------------------------------------------------------- + + +def FileNameFromPath(path): + """ + Returns the filename for a full path. + """ + return os.path.split(path)[1] + +def FindExtension(path): + """ + Returns the extension of a filename for a full path. + """ + return os.path.splitext(path)[1].lower() + +def FileExists(path): + """ + Returns True if the path exists. + """ + return os.path.isfile(path) + +def PathOnly(path): + """ + Returns the path of a full path without the filename. + """ + return os.path.split(path)[0] + + +#---------------------------------------------------------------------- +# Document/View Classes +#---------------------------------------------------------------------- + + +class Document(wx.EvtHandler): + """ + The document class can be used to model an application's file-based data. It + is part of the document/view framework supported by wxWindows, and cooperates + with the wxView, wxDocTemplate and wxDocManager classes. + + Note this wxPython version also keeps track of the modification date of the + document and if it changes on disk outside of the application, we will warn the + user before saving to avoid clobbering the file. + """ + + + def __init__(self, parent=None): + """ + Constructor. Define your own default constructor to initialize + application-specific data. + """ + wx.EvtHandler.__init__(self) + + self._documentParent = parent + self._documentTemplate = None + self._commandProcessor = None + self._savedYet = False + self._writeable = True + + self._documentTitle = None + self._documentFile = None + self._documentTypeName = None + self._documentModified = False + self._documentModificationDate = None + self._documentViews = [] + + + def ProcessEvent(self, event): + """ + Processes an event, searching event tables and calling zero or more + suitable event handler function(s). Note that the ProcessEvent + method is called from the wxPython docview framework directly since + wxPython does not have a virtual ProcessEvent function. + """ + return False + + + def GetFilename(self): + """ + Gets the filename associated with this document, or "" if none is + associated. + """ + return self._documentFile + + + def GetTitle(self): + """ + Gets the title for this document. The document title is used for an + associated frame (if any), and is usually constructed by the framework + from the filename. + """ + return self._documentTitle + + + def SetTitle(self, title): + """ + Sets the title for this document. The document title is used for an + associated frame (if any), and is usually constructed by the framework + from the filename. + """ + self._documentTitle = title + + + def GetDocumentName(self): + """ + The document type name given to the wxDocTemplate constructor, + copied to this document when the document is created. If several + document templates are created that use the same document type, this + variable is used in wxDocManager::CreateView to collate a list of + alternative view types that can be used on this kind of document. + """ + return self._documentTypeName + + + def SetDocumentName(self, name): + """ + Sets he document type name given to the wxDocTemplate constructor, + copied to this document when the document is created. If several + document templates are created that use the same document type, this + variable is used in wxDocManager::CreateView to collate a list of + alternative view types that can be used on this kind of document. Do + not change the value of this variable. + """ + self._documentTypeName = name + + + def GetDocumentSaved(self): + """ + Returns True if the document has been saved. This method has been + added to wxPython and is not in wxWindows. + """ + return self._savedYet + + + def SetDocumentSaved(self, saved=True): + """ + Sets whether the document has been saved. This method has been + added to wxPython and is not in wxWindows. + """ + self._savedYet = saved + + + def GetCommandProcessor(self): + """ + Returns the command processor associated with this document. + """ + return self._commandProcessor + + + def SetCommandProcessor(self, processor): + """ + Sets the command processor to be used for this document. The document + will then be responsible for its deletion. Normally you should not + call this; override OnCreateCommandProcessor instead. + """ + self._commandProcessor = processor + + + def IsModified(self): + """ + Returns true if the document has been modified since the last save, + false otherwise. You may need to override this if your document view + maintains its own record of being modified (for example if using + wxTextWindow to view and edit the document). + """ + return self._documentModified + + + def Modify(self, modify): + """ + Call with true to mark the document as modified since the last save, + false otherwise. You may need to override this if your document view + maintains its own record of being modified (for example if using + xTextWindow to view and edit the document). + This method has been extended to notify its views that the dirty flag has changed. + """ + self._documentModified = modify + self.UpdateAllViews(hint=("modify", self, self._documentModified)) + + + def SetDocumentModificationDate(self): + """ + Saves the file's last modification date. + This is used to check if the file has been modified outside of the application. + This method has been added to wxPython and is not in wxWindows. + """ + self._documentModificationDate = os.path.getmtime(self.GetFilename()) + + + def GetDocumentModificationDate(self): + """ + Returns the file's modification date when it was loaded from disk. + This is used to check if the file has been modified outside of the application. + This method has been added to wxPython and is not in wxWindows. + """ + return self._documentModificationDate + + + def IsDocumentModificationDateCorrect(self): + """ + Returns False if the file has been modified outside of the application. + This method has been added to wxPython and is not in wxWindows. + """ + if not os.path.exists(self.GetFilename()): # document must be in memory only and can't be out of date + return True + return self._documentModificationDate == os.path.getmtime(self.GetFilename()) + + + def GetViews(self): + """ + Returns the list whose elements are the views on the document. + """ + return self._documentViews + + + def GetDocumentTemplate(self): + """ + Returns the template that created the document. + """ + return self._documentTemplate + + + def SetDocumentTemplate(self, template): + """ + Sets the template that created the document. Should only be called by + the framework. + """ + self._documentTemplate = template + + + def DeleteContents(self): + """ + Deletes the contents of the document. Override this method as + necessary. + """ + return True + + + def Destroy(self): + """ + Destructor. Removes itself from the document manager. + """ + self.DeleteContents() + self._documentModificationDate = None + if self.GetDocumentManager(): + self.GetDocumentManager().RemoveDocument(self) + wx.EvtHandler.Destroy(self) + + + def Close(self): + """ + Closes the document, by calling OnSaveModified and then (if this true) + OnCloseDocument. This does not normally delete the document object: + use DeleteAllViews to do this implicitly. + """ + if self.OnSaveModified(): + if self.OnCloseDocument(): + return True + else: + return False + else: + return False + + + def OnCloseDocument(self): + """ + The default implementation calls DeleteContents (an empty + implementation) sets the modified flag to false. Override this to + supply additional behaviour when the document is closed with Close. + """ + self.NotifyClosing() + self.DeleteContents() + self.Modify(False) + return True + + + def DeleteAllViews(self): + """ + Calls wxView.Close and deletes each view. Deleting the final view will + implicitly delete the document itself, because the wxView destructor + calls RemoveView. This in turns calls wxDocument::OnChangedViewList, + whose default implemention is to save and delete the document if no + views exist. + """ + manager = self.GetDocumentManager() + for view in self._documentViews: + if not view.Close(): + return False + if self in manager.GetDocuments(): + self.Destroy() + return True + + + def GetFirstView(self): + """ + A convenience function to get the first view for a document, because + in many cases a document will only have a single view. + """ + if len(self._documentViews) == 0: + return None + return self._documentViews[0] + + + def GetDocumentManager(self): + """ + Returns the associated document manager. + """ + if self._documentTemplate: + return self._documentTemplate.GetDocumentManager() + return None + + + def OnNewDocument(self): + """ + The default implementation calls OnSaveModified and DeleteContents, + makes a default title for the document, and notifies the views that + the filename (in fact, the title) has changed. + """ + if not self.OnSaveModified() or not self.OnCloseDocument(): + return False + self.DeleteContents() + self.Modify(False) + self.SetDocumentSaved(False) + name = self.GetDocumentManager().MakeDefaultName() + self.SetTitle(name) + self.SetFilename(name, notifyViews = True) + + + def Save(self): + """ + Saves the document by calling OnSaveDocument if there is an associated + filename, or SaveAs if there is no filename. + """ + if not self.IsModified(): # and self._savedYet: This was here, but if it is not modified who cares if it hasn't been saved yet? + return True + + """ check for file modification outside of application """ + if not self.IsDocumentModificationDateCorrect(): + msgTitle = wx.GetApp().GetAppName() + if not msgTitle: + msgTitle = _("Application") + res = wx.MessageBox(_("'%s' has been modified outside of %s. Overwrite '%s' with current changes?") % (self.GetPrintableName(), msgTitle, self.GetPrintableName()), + msgTitle, + wx.YES_NO | wx.CANCEL | wx.ICON_QUESTION, + self.GetDocumentWindow()) + + if res == wx.NO: + return True + elif res == wx.YES: + pass + else: # elif res == wx.CANCEL: + return False + + if not self._documentFile or not self._savedYet: + return self.SaveAs() + return self.OnSaveDocument(self._documentFile) + + + def SaveAs(self): + """ + Prompts the user for a file to save to, and then calls OnSaveDocument. + """ + docTemplate = self.GetDocumentTemplate() + if not docTemplate: + return False + + descr = docTemplate.GetDescription() + _(" (") + docTemplate.GetFileFilter() + _(") |") + docTemplate.GetFileFilter() # spacing is important, make sure there is no space after the "|", it causes a bug on wx_gtk + filename = wx.FileSelector(_("Save As"), + docTemplate.GetDirectory(), + FileNameFromPath(self.GetFilename()), + docTemplate.GetDefaultExtension(), + wildcard = descr, + flags = wx.SAVE | wx.OVERWRITE_PROMPT, + parent = self.GetDocumentWindow()) + if filename == "": + return False + + name, ext = os.path.splitext(filename) + if ext == "": + filename += '.' + docTemplate.GetDefaultExtension() + + self.SetFilename(filename) + self.SetTitle(FileNameFromPath(filename)) + + for view in self._documentViews: + view.OnChangeFilename() + + if not self.OnSaveDocument(filename): + return False + + if docTemplate.FileMatchesTemplate(filename): + self.GetDocumentManager().AddFileToHistory(filename) + + return True + + + def OnSaveDocument(self, filename): + """ + Constructs an output file for the given filename (which must + not be empty), and calls SaveObject. If SaveObject returns true, the + document is set to unmodified; otherwise, an error message box is + displayed. + """ + if not filename: + return False + + msgTitle = wx.GetApp().GetAppName() + if not msgTitle: + msgTitle = _("File Error") + + backupFilename = None + fileObject = None + copied = False + try: + # if current file exists, move it to a safe place temporarily + if os.path.exists(filename): + + # Check if read-only. + if not os.access(filename, os.W_OK): + wx.MessageBox("Could not save '%s'. No write permission to overwrite existing file." % FileNameFromPath(filename), + msgTitle, + wx.OK | wx.ICON_EXCLAMATION, + self.GetDocumentWindow()) + return False + + i = 1 + backupFilename = "%s.bak%s" % (filename, i) + while os.path.exists(backupFilename): + i += 1 + backupFilename = "%s.bak%s" % (filename, i) + shutil.copy(filename, backupFilename) + copied = True + + fileObject = file(filename, 'w') + self.SaveObject(fileObject) + fileObject.close() + fileObject = None + + if backupFilename: + os.remove(backupFilename) + except: + # for debugging purposes + import traceback + traceback.print_exc() + + if fileObject: + fileObject.close() # file is still open, close it, need to do this before removal + + # save failed, remove copied file + if backupFilename and copied: + os.remove(backupFilename) + + wx.MessageBox("Could not save '%s'. %s" % (FileNameFromPath(filename), sys.exc_value), + msgTitle, + wx.OK | wx.ICON_EXCLAMATION, + self.GetDocumentWindow()) + return False + + self.SetDocumentModificationDate() + self.SetFilename(filename, True) + self.Modify(False) + self.SetDocumentSaved(True) + #if wx.Platform == '__WXMAC__': # Not yet implemented in wxPython + # wx.FileName(file).MacSetDefaultTypeAndCreator() + return True + + + def OnOpenDocument(self, filename): + """ + Constructs an input file for the given filename (which must not + be empty), and calls LoadObject. If LoadObject returns true, the + document is set to unmodified; otherwise, an error message box is + displayed. The document's views are notified that the filename has + changed, to give windows an opportunity to update their titles. All of + the document's views are then updated. + """ + if not self.OnSaveModified(): + return False + + msgTitle = wx.GetApp().GetAppName() + if not msgTitle: + msgTitle = _("File Error") + + fileObject = file(filename, 'r') + try: + self.LoadObject(fileObject) + fileObject.close() + fileObject = None + except: + # for debugging purposes + import traceback + traceback.print_exc() + + if fileObject: + fileObject.close() # file is still open, close it + + wx.MessageBox("Could not open '%s'. %s" % (FileNameFromPath(filename), sys.exc_value), + msgTitle, + wx.OK | wx.ICON_EXCLAMATION, + self.GetDocumentWindow()) + return False + + self.SetDocumentModificationDate() + self.SetFilename(filename, True) + self.Modify(False) + self.SetDocumentSaved(True) + self.UpdateAllViews() + return True + + + def LoadObject(self, file): + """ + Override this function and call it from your own LoadObject before + loading your own data. LoadObject is called by the framework + automatically when the document contents need to be loaded. + + Note that the wxPython version simply sends you a Python file object, + so you can use pickle. + """ + return True + + + def SaveObject(self, file): + """ + Override this function and call it from your own SaveObject before + saving your own data. SaveObject is called by the framework + automatically when the document contents need to be saved. + + Note that the wxPython version simply sends you a Python file object, + so you can use pickle. + """ + return True + + + def Revert(self): + """ + Override this function to revert the document to its last saved state. + """ + return False + + + def GetPrintableName(self): + """ + Copies a suitable document name into the supplied name buffer. + The default function uses the title, or if there is no title, uses the + filename; or if no filename, the string 'Untitled'. + """ + if self._documentTitle: + return self._documentTitle + elif self._documentFile: + return FileNameFromPath(self._documentFile) + else: + return _("Untitled") + + + def GetDocumentWindow(self): + """ + Intended to return a suitable window for using as a parent for + document-related dialog boxes. By default, uses the frame associated + with the first view. + """ + if len(self._documentViews) > 0: + return self._documentViews[0].GetFrame() + else: + return wx.GetApp().GetTopWindow() + + + def OnCreateCommandProcessor(self): + """ + Override this function if you want a different (or no) command + processor to be created when the document is created. By default, it + returns an instance of wxCommandProcessor. + """ + return CommandProcessor() + + + def OnSaveModified(self): + """ + If the document has been modified, prompts the user to ask if the + changes should be changed. If the user replies Yes, the Save function + is called. If No, the document is marked as unmodified and the + function succeeds. If Cancel, the function fails. + """ + if not self.IsModified(): + return True + + """ check for file modification outside of application """ + if not self.IsDocumentModificationDateCorrect(): + msgTitle = wx.GetApp().GetAppName() + if not msgTitle: + msgTitle = _("Warning") + res = wx.MessageBox(_("'%s' has been modified outside of %s. Overwrite '%s' with current changes?") % (self.GetPrintableName(), msgTitle, self.GetPrintableName()), + msgTitle, + wx.YES_NO | wx.CANCEL | wx.ICON_QUESTION, + self.GetDocumentWindow()) + + if res == wx.NO: + self.Modify(False) + return True + elif res == wx.YES: + return wx.lib.docview.Document.Save(self) + else: # elif res == wx.CANCEL: + return False + + msgTitle = wx.GetApp().GetAppName() + if not msgTitle: + msgTitle = _("Warning") + + res = wx.MessageBox(_("Save changes to '%s'?") % self.GetPrintableName(), + msgTitle, + wx.YES_NO | wx.CANCEL | wx.ICON_QUESTION, + self.GetDocumentWindow()) + + if res == wx.NO: + self.Modify(False) + return True + elif res == wx.YES: + return self.Save() + else: # elif res == wx.CANCEL: + return False + + + def Draw(context): + """ + Called by printing framework to draw the view. + """ + return True + + + def AddView(self, view): + """ + If the view is not already in the list of views, adds the view and + calls OnChangedViewList. + """ + if not view in self._documentViews: + self._documentViews.append(view) + self.OnChangedViewList() + return True + + + def RemoveView(self, view): + """ + Removes the view from the document's list of views, and calls + OnChangedViewList. + """ + if view in self._documentViews: + self._documentViews.remove(view) + self.OnChangedViewList() + return True + + + def OnCreate(self, path, flags): + """ + The default implementation calls DeleteContents (an empty + implementation) sets the modified flag to false. Override this to + supply additional behaviour when the document is opened with Open. + """ + if flags & DOC_NO_VIEW: + return True + return self.GetDocumentTemplate().CreateView(self, flags) + + + def OnChangedViewList(self): + """ + Called when a view is added to or deleted from this document. The + default implementation saves and deletes the document if no views + exist (the last one has just been removed). + """ + if len(self._documentViews) == 0: + if self.OnSaveModified(): + pass # C version does a delete but Python will garbage collect + + + def UpdateAllViews(self, sender = None, hint = None): + """ + Updates all views. If sender is non-NULL, does not update this view. + hint represents optional information to allow a view to optimize its + update. + """ + for view in self._documentViews: + if view != sender: + view.OnUpdate(sender, hint) + + + def NotifyClosing(self): + """ + Notifies the views that the document is going to close. + """ + for view in self._documentViews: + view.OnClosingDocument() + + + def SetFilename(self, filename, notifyViews = False): + """ + Sets the filename for this document. Usually called by the framework. + If notifyViews is true, wxView.OnChangeFilename is called for all + views. + """ + self._documentFile = filename + if notifyViews: + for view in self._documentViews: + view.OnChangeFilename() + + + def GetWriteable(self): + """ + Returns true if the document can be written to its accociated file path. + This method has been added to wxPython and is not in wxWindows. + """ + if not self._writeable: + return False + if not self._documentFile: # Doesn't exist, do a save as + return True + else: + return os.access(self._documentFile, os.W_OK) + + + def SetWriteable(self, writeable): + """ + Set to False if the document can not be saved. This will disable the ID_SAVE_AS + event and is useful for custom documents that should not be saveable. The ID_SAVE + event can be disabled by never Modifying the document. This method has been added + to wxPython and is not in wxWindows. + """ + self._writeable = writeable + + +class View(wx.EvtHandler): + """ + The view class can be used to model the viewing and editing component of + an application's file-based data. It is part of the document/view + framework supported by wxWindows, and cooperates with the wxDocument, + wxDocTemplate and wxDocManager classes. + """ + + def __init__(self): + """ + Constructor. Define your own default constructor to initialize + application-specific data. + """ + wx.EvtHandler.__init__(self) + self._viewDocument = None + self._viewFrame = None + + + def Destroy(self): + """ + Destructor. Removes itself from the document's list of views. + """ + if self._viewDocument: + self._viewDocument.RemoveView(self) + wx.EvtHandler.Destroy(self) + + + def ProcessEvent(self, event): + """ + Processes an event, searching event tables and calling zero or more + suitable event handler function(s). Note that the ProcessEvent + method is called from the wxPython docview framework directly since + wxPython does not have a virtual ProcessEvent function. + """ + if not self.GetDocument() or not self.GetDocument().ProcessEvent(event): + return False + else: + return True + + + def ProcessUpdateUIEvent(self, event): + """ + Processes a UI event, searching event tables and calling zero or more + suitable event handler function(s). Note that the ProcessEvent + method is called from the wxPython docview framework directly since + wxPython does not have a virtual ProcessEvent function. + """ + return False + + + def OnActivateView(self, activate, activeView, deactiveView): + """ + Called when a view is activated by means of wxView::Activate. The + default implementation does nothing. + """ + pass + + + def OnClosingDocument(self): + """ + Override this to clean up the view when the document is being closed. + The default implementation does nothing. + """ + pass + + + def OnDraw(self, dc): + """ + Override this to draw the view for the printing framework. The + default implementation does nothing. + """ + pass + + + def OnPrint(self, dc, info): + """ + Override this to print the view for the printing framework. The + default implementation calls View.OnDraw. + """ + self.OnDraw(dc) + + + def OnUpdate(self, sender, hint): + """ + Called when the view should be updated. sender is a pointer to the + view that sent the update request, or NULL if no single view requested + the update (for instance, when the document is opened). hint is as yet + unused but may in future contain application-specific information for + making updating more efficient. + """ + if hint: + if hint[0] == "modify": # if dirty flag changed, update the view's displayed title + frame = self.GetFrame() + if frame and hasattr(frame, "OnTitleIsModified"): + frame.OnTitleIsModified() + return True + return False + + + def OnChangeFilename(self): + """ + Called when the filename has changed. The default implementation + constructs a suitable title and sets the title of the view frame (if + any). + """ + if self.GetFrame(): + appName = wx.GetApp().GetAppName() + if not self.GetDocument(): + if appName: + title = appName + else: + return + else: + if appName and isinstance(self.GetFrame(), DocChildFrame): # Only need app name in title for SDI + title = appName + _(" - ") + else: + title = '' + self.GetFrame().SetTitle(title + self.GetDocument().GetPrintableName()) + + + def GetDocument(self): + """ + Returns the document associated with the view. + """ + return self._viewDocument + + + def SetDocument(self, doc): + """ + Associates the given document with the view. Normally called by the + framework. + """ + self._viewDocument = doc + if doc: + doc.AddView(self) + + + def GetViewName(self): + """ + Gets the name associated with the view (passed to the wxDocTemplate + constructor). Not currently used by the framework. + """ + return self._viewTypeName + + + def SetViewName(self, name): + """ + Sets the view type name. Should only be called by the framework. + """ + self._viewTypeName = name + + + 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 self.OnClose(deleteWindow = deleteWindow): + return True + else: + return False + + + def Activate(self, activate=True): + """ + Call this from your view frame's OnActivate member to tell the + framework which view is currently active. If your windowing system + doesn't call OnActivate, you may need to call this function from + OnMenuCommand or any place where you know the view must be active, and + the framework will need to get the current view. + + The prepackaged view frame wxDocChildFrame calls wxView.Activate from + its OnActivate member and from its OnMenuCommand member. + """ + if self.GetDocument() and self.GetDocumentManager(): + self.OnActivateView(activate, self, self.GetDocumentManager().GetCurrentView()) + self.GetDocumentManager().ActivateView(self, activate) + + + def OnClose(self, deleteWindow=True): + """ + Implements closing behaviour. The default implementation calls + wxDocument.Close to close the associated document. Does not delete the + view. The application may wish to do some cleaning up operations in + this function, if a call to wxDocument::Close succeeded. For example, + if your application's all share the same window, you need to + disassociate the window from the view and perhaps clear the window. If + deleteWindow is true, delete the frame associated with the view. + """ + if self.GetDocument(): + return self.GetDocument().Close() + else: + return True + + + def OnCreate(self, doc, flags): + """ + wxDocManager or wxDocument creates a wxView via a wxDocTemplate. Just + after the wxDocTemplate creates the wxView, it calls wxView::OnCreate. + In its OnCreate member function, the wxView can create a + wxDocChildFrame or a derived class. This wxDocChildFrame provides user + interface elements to view and/or edit the contents of the wxDocument. + + By default, simply returns true. If the function returns false, the + view will be deleted. + """ + return True + + + def OnCreatePrintout(self): + """ + Returns a wxPrintout object for the purposes of printing. It should + create a new object every time it is called; the framework will delete + objects it creates. + + By default, this function returns an instance of wxDocPrintout, which + prints and previews one page by calling wxView.OnDraw. + + Override to return an instance of a class other than wxDocPrintout. + """ + return DocPrintout(self, self.GetDocument().GetPrintableName()) + + + def GetFrame(self): + """ + Gets the frame associated with the view (if any). Note that this + "frame" is not a wxFrame at all in the generic MDI implementation + which uses the notebook pages instead of the frames and this is why + this method returns a wxWindow and not a wxFrame. + """ + return self._viewFrame + + + def SetFrame(self, frame): + """ + Sets the frame associated with this view. The application should call + this if possible, to tell the view about the frame. See GetFrame for + the explanation about the mismatch between the "Frame" in the method + name and the type of its parameter. + """ + self._viewFrame = frame + + + def GetDocumentManager(self): + """ + Returns the document manager instance associated with this view. + """ + if self._viewDocument: + return self.GetDocument().GetDocumentManager() + else: + return None + + +class DocTemplate(wx.Object): + """ + The wxDocTemplate class is used to model the relationship between a + document class and a view class. + """ + + + def __init__(self, manager, description, filter, dir, ext, docTypeName, viewTypeName, docType, viewType, flags=DEFAULT_TEMPLATE_FLAGS, icon=None): + """ + Constructor. Create instances dynamically near the start of your + application after creating a wxDocManager instance, and before doing + any document or view operations. + + manager is the document manager object which manages this template. + + description is a short description of what the template is for. This + string will be displayed in the file filter list of Windows file + selectors. + + filter is an appropriate file filter such as \*.txt. + + dir is the default directory to use for file selectors. + + ext is the default file extension (such as txt). + + docTypeName is a name that should be unique for a given type of + document, used for gathering a list of views relevant to a + particular document. + + viewTypeName is a name that should be unique for a given view. + + docClass is a Python class. If this is not supplied, you will need to + derive a new wxDocTemplate class and override the CreateDocument + member to return a new document instance on demand. + + viewClass is a Python class. If this is not supplied, you will need to + derive a new wxDocTemplate class and override the CreateView member to + return a new view instance on demand. + + flags is a bit list of the following: + wx.TEMPLATE_VISIBLE The template may be displayed to the user in + dialogs. + + wx.TEMPLATE_INVISIBLE The template may not be displayed to the user in + dialogs. + + wx.DEFAULT_TEMPLATE_FLAGS Defined as wxTEMPLATE_VISIBLE. + """ + self._docManager = manager + self._description = description + self._fileFilter = filter + self._directory = dir + self._defaultExt = ext + self._docTypeName = docTypeName + self._viewTypeName = viewTypeName + self._docType = docType + self._viewType = viewType + self._flags = flags + self._icon = icon + + self._docManager.AssociateTemplate(self) + + + def GetDefaultExtension(self): + """ + Returns the default file extension for the document data, as passed to + the document template constructor. + """ + return self._defaultExt + + + def SetDefaultExtension(self, defaultExt): + """ + Sets the default file extension. + """ + self._defaultExt = defaultExt + + + def GetDescription(self): + """ + Returns the text description of this template, as passed to the + document template constructor. + """ + return self._description + + + def SetDescription(self, description): + """ + Sets the template description. + """ + self._description = description + + + def GetDirectory(self): + """ + Returns the default directory, as passed to the document template + constructor. + """ + return self._directory + + + def SetDirectory(self, dir): + """ + Sets the default directory. + """ + self._directory = dir + + + def GetDocumentManager(self): + """ + Returns the document manager instance for which this template was + created. + """ + return self._docManager + + + def SetDocumentManager(self, manager): + """ + Sets the document manager instance for which this template was + created. Should not be called by the application. + """ + self._docManager = manager + + + def GetFileFilter(self): + """ + Returns the file filter, as passed to the document template + constructor. + """ + return self._fileFilter + + + def SetFileFilter(self, filter): + """ + Sets the file filter. + """ + self._fileFilter = filter + + + def GetFlags(self): + """ + Returns the flags, as passed to the document template constructor. + (see the constructor description for more details). + """ + return self._flags + + + def SetFlags(self, flags): + """ + Sets the internal document template flags (see the constructor + description for more details). + """ + self._flags = flags + + + def GetIcon(self): + """ + Returns the icon, as passed to the document template + constructor. This method has been added to wxPython and is + not in wxWindows. + """ + return self._icon + + + def SetIcon(self, flags): + """ + Sets the icon. This method has been added to wxPython and is not + in wxWindows. + """ + self._icon = icon + + + def GetDocumentType(self): + """ + Returns the Python document class, as passed to the document template + constructor. + """ + return self._docType + + + def GetViewType(self): + """ + Returns the Python view class, as passed to the document template + constructor. + """ + return self._viewType + + + def IsVisible(self): + """ + Returns true if the document template can be shown in user dialogs, + false otherwise. + """ + return (self._flags & TEMPLATE_VISIBLE) == TEMPLATE_VISIBLE + + + def IsNewable(self): + """ + Returns true if the document template can be shown in "New" dialogs, + false otherwise. + + This method has been added to wxPython and is not in wxWindows. + """ + return (self._flags & TEMPLATE_NO_CREATE) != TEMPLATE_NO_CREATE + + + def GetDocumentName(self): + """ + Returns the document type name, as passed to the document template + constructor. + """ + return self._docTypeName + + + def GetViewName(self): + """ + Returns the view type name, as passed to the document template + constructor. + """ + return self._viewTypeName + + + def CreateDocument(self, path, flags): + """ + Creates a new instance of the associated document class. If you have + not supplied a class to the template constructor, you will need to + override this function to return an appropriate document instance. + """ + doc = self._docType() + doc.SetFilename(path) + doc.SetDocumentTemplate(self) + self.GetDocumentManager().AddDocument(doc) + doc.SetCommandProcessor(doc.OnCreateCommandProcessor()) + if doc.OnCreate(path, flags): + return doc + else: + if doc in self.GetDocumentManager().GetDocuments(): + doc.DeleteAllViews() + return None + + + def CreateView(self, doc, flags): + """ + Creates a new instance of the associated document view. If you have + not supplied a class to the template constructor, you will need to + override this function to return an appropriate view instance. + """ + view = self._viewType() + view.SetDocument(doc) + if view.OnCreate(doc, flags): + return view + else: + view.Destroy() + return None + + + def FileMatchesTemplate(self, path): + """ + Returns True if the path's extension matches one of this template's + file filter extensions. + """ + ext = FindExtension(path) + if not ext: return False + + extList = self.GetFileFilter().replace('*','').split(';') + return ext in extList + + +class DocManager(wx.EvtHandler): + """ + The wxDocManager class is part of the document/view framework supported by + wxWindows, and cooperates with the wxView, wxDocument and wxDocTemplate + classes. + """ + + def __init__(self, flags=DEFAULT_DOCMAN_FLAGS, initialize=True): + """ + Constructor. Create a document manager instance dynamically near the + start of your application before doing any document or view operations. + + flags is used in the Python version to indicate whether the document + manager is in DOC_SDI or DOC_MDI mode. + + If initialize is true, the Initialize function will be called to + create a default history list object. If you derive from wxDocManager, + you may wish to call the base constructor with false, and then call + Initialize in your own constructor, to allow your own Initialize or + OnCreateFileHistory functions to be called. + """ + + wx.EvtHandler.__init__(self) + + self._defaultDocumentNameCounter = 1 + self._flags = flags + self._currentView = None + self._lastActiveView = None + self._maxDocsOpen = 10000 + self._fileHistory = None + self._templates = [] + self._docs = [] + self._lastDirectory = "" + + if initialize: + self.Initialize() + + wx.EVT_MENU(self, wx.ID_OPEN, self.OnFileOpen) + wx.EVT_MENU(self, wx.ID_CLOSE, self.OnFileClose) + wx.EVT_MENU(self, wx.ID_CLOSE_ALL, self.OnFileCloseAll) + wx.EVT_MENU(self, wx.ID_REVERT, self.OnFileRevert) + wx.EVT_MENU(self, wx.ID_NEW, self.OnFileNew) + wx.EVT_MENU(self, wx.ID_SAVE, self.OnFileSave) + wx.EVT_MENU(self, wx.ID_SAVEAS, self.OnFileSaveAs) + wx.EVT_MENU(self, wx.ID_UNDO, self.OnUndo) + wx.EVT_MENU(self, wx.ID_REDO, self.OnRedo) + wx.EVT_MENU(self, wx.ID_PRINT, self.OnPrint) + wx.EVT_MENU(self, wx.ID_PRINT_SETUP, self.OnPrintSetup) + wx.EVT_MENU(self, wx.ID_PREVIEW, self.OnPreview) + + wx.EVT_UPDATE_UI(self, wx.ID_OPEN, self.OnUpdateFileOpen) + wx.EVT_UPDATE_UI(self, wx.ID_CLOSE, self.OnUpdateFileClose) + wx.EVT_UPDATE_UI(self, wx.ID_CLOSE_ALL, self.OnUpdateFileCloseAll) + wx.EVT_UPDATE_UI(self, wx.ID_REVERT, self.OnUpdateFileRevert) + wx.EVT_UPDATE_UI(self, wx.ID_NEW, self.OnUpdateFileNew) + wx.EVT_UPDATE_UI(self, wx.ID_SAVE, self.OnUpdateFileSave) + wx.EVT_UPDATE_UI(self, wx.ID_SAVEAS, self.OnUpdateFileSaveAs) + wx.EVT_UPDATE_UI(self, wx.ID_UNDO, self.OnUpdateUndo) + wx.EVT_UPDATE_UI(self, wx.ID_REDO, self.OnUpdateRedo) + wx.EVT_UPDATE_UI(self, wx.ID_PRINT, self.OnUpdatePrint) + wx.EVT_UPDATE_UI(self, wx.ID_PRINT_SETUP, self.OnUpdatePrintSetup) + wx.EVT_UPDATE_UI(self, wx.ID_PREVIEW, self.OnUpdatePreview) + + + def Destroy(self): + """ + Destructor. + """ + self.Clear() + wx.EvtHandler.Destroy(self) + + + def GetFlags(self): + """ + Returns the document manager's flags. This method has been + added to wxPython and is not in wxWindows. + """ + return self._flags + + + def CloseDocument(self, doc, force=True): + """ + Closes the specified document. + """ + if doc.Close() or force: + doc.DeleteAllViews() + if doc in self._docs: + doc.Destroy() + return True + return False + + + def CloseDocuments(self, force=True): + """ + Closes all currently opened documents. + """ + for document in self._docs[::-1]: # Close in lifo (reverse) order. We clone the list to make sure we go through all docs even as they are deleted + if not self.CloseDocument(document, force): + return False + if document: + document.DeleteAllViews() # Implicitly delete the document when the last view is removed + return True + + + def Clear(self, force=True): + """ + Closes all currently opened document by callling CloseDocuments and + clears the document manager's templates. + """ + if not self.CloseDocuments(force): + return False + self._templates = [] + return True + + + def Initialize(self): + """ + Initializes data; currently just calls OnCreateFileHistory. Some data + cannot always be initialized in the constructor because the programmer + must be given the opportunity to override functionality. In fact + Initialize is called from the wxDocManager constructor, but this can + be vetoed by passing false to the second argument, allowing the + derived class's constructor to call Initialize, possibly calling a + different OnCreateFileHistory from the default. + + The bottom line: if you're not deriving from Initialize, forget it and + construct wxDocManager with no arguments. + """ + self.OnCreateFileHistory() + return True + + + def OnCreateFileHistory(self): + """ + A hook to allow a derived class to create a different type of file + history. Called from Initialize. + """ + self._fileHistory = wx.FileHistory() + + + def OnFileClose(self, event): + """ + Closes and deletes the currently active document. + """ + doc = self.GetCurrentDocument() + if doc: + doc.DeleteAllViews() + if doc in self._docs: + self._docs.remove(doc) + + + def OnFileCloseAll(self, event): + """ + Closes and deletes all the currently opened documents. + """ + return self.CloseDocuments(force = False) + + + def OnFileNew(self, event): + """ + Creates a new document and reads in the selected file. + """ + self.CreateDocument('', DOC_NEW) + + + def OnFileOpen(self, event): + """ + Creates a new document and reads in the selected file. + """ + if not self.CreateDocument('', DEFAULT_DOCMAN_FLAGS): + self.OnOpenFileFailure() + + + def OnFileRevert(self, event): + """ + Reverts the current document by calling wxDocument.Save for the current + document. + """ + doc = self.GetCurrentDocument() + if not doc: + return + doc.Revert() + + + def OnFileSave(self, event): + """ + Saves the current document by calling wxDocument.Save for the current + document. + """ + doc = self.GetCurrentDocument() + if not doc: + return + doc.Save() + + + def OnFileSaveAs(self, event): + """ + Calls wxDocument.SaveAs for the current document. + """ + doc = self.GetCurrentDocument() + if not doc: + return + doc.SaveAs() + + + def OnPrint(self, event): + """ + Prints the current document by calling its View's OnCreatePrintout + method. + """ + view = self.GetCurrentView() + if not view: + return + + printout = view.OnCreatePrintout() + if printout: + if not hasattr(self, "printData"): + self.printData = wx.PrintData() + self.printData.SetPaperId(wx.PAPER_LETTER) + self.printData.SetPrintMode(wx.PRINT_MODE_PRINTER) + + pdd = wx.PrintDialogData(self.printData) + printer = wx.Printer(pdd) + printer.Print(view.GetFrame(), printout) + + + def OnPrintSetup(self, event): + """ + Presents the print setup dialog. + """ + view = self.GetCurrentView() + if view: + parentWin = view.GetFrame() + else: + parentWin = wx.GetApp().GetTopWindow() + + if not hasattr(self, "printData"): + self.printData = wx.PrintData() + self.printData.SetPaperId(wx.PAPER_LETTER) + + data = wx.PrintDialogData(self.printData) + printDialog = wx.PrintDialog(parentWin, data) + printDialog.GetPrintDialogData().SetSetupDialog(True) + printDialog.ShowModal() + + # this makes a copy of the wx.PrintData instead of just saving + # a reference to the one inside the PrintDialogData that will + # be destroyed when the dialog is destroyed + self.printData = wx.PrintData(printDialog.GetPrintDialogData().GetPrintData()) + + printDialog.Destroy() + + + def OnPreview(self, event): + """ + Previews the current document by calling its View's OnCreatePrintout + method. + """ + view = self.GetCurrentView() + if not view: + return + + printout = view.OnCreatePrintout() + if printout: + if not hasattr(self, "printData"): + self.printData = wx.PrintData() + self.printData.SetPaperId(wx.PAPER_LETTER) + self.printData.SetPrintMode(wx.PRINT_MODE_PREVIEW) + + data = wx.PrintDialogData(self.printData) + # Pass two printout objects: for preview, and possible printing. + preview = wx.PrintPreview(printout, view.OnCreatePrintout(), data) + if not preview.Ok(): + wx.MessageBox(_("Unable to display print preview.")) + return + # wxWindows source doesn't use base frame's pos, size, and icon, but did it this way so it would work like MS Office etc. + mimicFrame = wx.GetApp().GetTopWindow() + frame = wx.PreviewFrame(preview, mimicFrame, _("Print Preview"), mimicFrame.GetPosition(), mimicFrame.GetSize()) + frame.SetIcon(mimicFrame.GetIcon()) + frame.SetTitle(_("%s - %s - Preview") % (mimicFrame.GetTitle(), view.GetDocument().GetPrintableName())) + frame.Initialize() + frame.Show(True) + + + def OnUndo(self, event): + """ + Issues an Undo command to the current document's command processor. + """ + doc = self.GetCurrentDocument() + if not doc: + return + if doc.GetCommandProcessor(): + doc.GetCommandProcessor().Undo() + + + def OnRedo(self, event): + """ + Issues a Redo command to the current document's command processor. + """ + doc = self.GetCurrentDocument() + if not doc: + return + if doc.GetCommandProcessor(): + doc.GetCommandProcessor().Redo() + + + def OnUpdateFileOpen(self, event): + """ + Updates the user interface for the File Open command. + """ + event.Enable(True) + + + def OnUpdateFileClose(self, event): + """ + Updates the user interface for the File Close command. + """ + event.Enable(self.GetCurrentDocument() != None) + + + def OnUpdateFileCloseAll(self, event): + """ + Updates the user interface for the File Close All command. + """ + event.Enable(self.GetCurrentDocument() != None) + + + def OnUpdateFileRevert(self, event): + """ + Updates the user interface for the File Revert command. + """ + event.Enable(self.GetCurrentDocument() != None) + + + def OnUpdateFileNew(self, event): + """ + Updates the user interface for the File New command. + """ + return True + + + def OnUpdateFileSave(self, event): + """ + Updates the user interface for the File Save command. + """ + doc = self.GetCurrentDocument() + event.Enable(doc != None and doc.IsModified()) + + + def OnUpdateFileSaveAs(self, event): + """ + Updates the user interface for the File Save As command. + """ + event.Enable(self.GetCurrentDocument() != None and self.GetCurrentDocument().GetWriteable()) + + + def OnUpdateUndo(self, event): + """ + Updates the user interface for the Undo command. + """ + doc = self.GetCurrentDocument() + event.Enable(doc != None and doc.GetCommandProcessor() != None and doc.GetCommandProcessor().CanUndo()) + if doc and doc.GetCommandProcessor(): + doc.GetCommandProcessor().SetMenuStrings() + else: + event.SetText(_("&Undo\tCtrl+Z")) + + + def OnUpdateRedo(self, event): + """ + Updates the user interface for the Redo command. + """ + doc = self.GetCurrentDocument() + event.Enable(doc != None and doc.GetCommandProcessor() != None and doc.GetCommandProcessor().CanRedo()) + if doc and doc.GetCommandProcessor(): + doc.GetCommandProcessor().SetMenuStrings() + else: + event.SetText(_("&Redo\tCtrl+Y")) + + + def OnUpdatePrint(self, event): + """ + Updates the user interface for the Print command. + """ + event.Enable(self.GetCurrentDocument() != None) + + + def OnUpdatePrintSetup(self, event): + """ + Updates the user interface for the Print Setup command. + """ + return True + + + def OnUpdatePreview(self, event): + """ + Updates the user interface for the Print Preview command. + """ + event.Enable(self.GetCurrentDocument() != None) + + + def GetCurrentView(self): + """ + Returns the currently active view. + """ + if self._currentView: + return self._currentView + if len(self._docs) == 1: + return self._docs[0].GetFirstView() + return None + + + def GetLastActiveView(self): + """ + Returns the last active view. This is used in the SDI framework where dialogs can be mistaken for a view + and causes the framework to deactivete the current view. This happens when something like a custom dialog box used + to operate on the current view is shown. + """ + if len(self._docs) >= 1: + return self._lastActiveView + else: + return None + + + def ProcessEvent(self, event): + """ + Processes an event, searching event tables and calling zero or more + suitable event handler function(s). Note that the ProcessEvent + method is called from the wxPython docview framework directly since + wxPython does not have a virtual ProcessEvent function. + """ + view = self.GetCurrentView() + if view: + if view.ProcessEvent(event): + return True + id = event.GetId() + if id == wx.ID_OPEN: + self.OnFileOpen(event) + return True + elif id == wx.ID_CLOSE: + self.OnFileClose(event) + return True + elif id == wx.ID_CLOSE_ALL: + self.OnFileCloseAll(event) + return True + elif id == wx.ID_REVERT: + self.OnFileRevert(event) + return True + elif id == wx.ID_NEW: + self.OnFileNew(event) + return True + elif id == wx.ID_SAVE: + self.OnFileSave(event) + return True + elif id == wx.ID_SAVEAS: + self.OnFileSaveAs(event) + return True + elif id == wx.ID_UNDO: + self.OnUndo(event) + return True + elif id == wx.ID_REDO: + self.OnRedo(event) + return True + elif id == wx.ID_PRINT: + self.OnPrint(event) + return True + elif id == wx.ID_PRINT_SETUP: + self.OnPrintSetup(event) + return True + elif id == wx.ID_PREVIEW: + self.OnPreview(event) + return True + else: + return False + + + def ProcessUpdateUIEvent(self, event): + """ + Processes a UI event, searching event tables and calling zero or more + suitable event handler function(s). Note that the ProcessEvent + method is called from the wxPython docview framework directly since + wxPython does not have a virtual ProcessEvent function. + """ + id = event.GetId() + view = self.GetCurrentView() + if view: + if view.ProcessUpdateUIEvent(event): + return True + if id == wx.ID_OPEN: + self.OnUpdateFileOpen(event) + return True + elif id == wx.ID_CLOSE: + self.OnUpdateFileClose(event) + return True + elif id == wx.ID_CLOSE_ALL: + self.OnUpdateFileCloseAll(event) + return True + elif id == wx.ID_REVERT: + self.OnUpdateFileRevert(event) + return True + elif id == wx.ID_NEW: + self.OnUpdateFileNew(event) + return True + elif id == wx.ID_SAVE: + self.OnUpdateFileSave(event) + return True + elif id == wx.ID_SAVEAS: + self.OnUpdateFileSaveAs(event) + return True + elif id == wx.ID_UNDO: + self.OnUpdateUndo(event) + return True + elif id == wx.ID_REDO: + self.OnUpdateRedo(event) + return True + elif id == wx.ID_PRINT: + self.OnUpdatePrint(event) + return True + elif id == wx.ID_PRINT_SETUP: + self.OnUpdatePrintSetup(event) + return True + elif id == wx.ID_PREVIEW: + self.OnUpdatePreview(event) + return True + else: + return False + + + def CreateDocument(self, path, flags=0): + """ + Creates a new document in a manner determined by the flags parameter, + which can be: + + wx.lib.docview.DOC_NEW Creates a fresh document. + wx.lib.docview.DOC_SILENT Silently loads the given document file. + + If wx.lib.docview.DOC_NEW is present, a new document will be created and returned, + possibly after asking the user for a template to use if there is more + than one document template. If wx.lib.docview.DOC_SILENT is present, a new document + will be created and the given file loaded into it. If neither of these + flags is present, the user will be presented with a file selector for + the file to load, and the template to use will be determined by the + extension (Windows) or by popping up a template choice list (other + platforms). + + If the maximum number of documents has been reached, this function + will delete the oldest currently loaded document before creating a new + one. + + wxPython version supports the document manager's wx.lib.docview.DOC_OPEN_ONCE + and wx.lib.docview.DOC_NO_VIEW flag. + + if wx.lib.docview.DOC_OPEN_ONCE is present, trying to open the same file multiple + times will just return the same document. + if wx.lib.docview.DOC_NO_VIEW is present, opening a file will generate the document, + but not generate a corresponding view. + """ + templates = [] + for temp in self._templates: + if temp.IsVisible(): + templates.append(temp) + if len(templates) == 0: + return None + + if len(self.GetDocuments()) >= self._maxDocsOpen: + doc = self.GetDocuments()[0] + if not self.CloseDocument(doc, False): + return None + + if flags & DOC_NEW: + for temp in templates[:]: + if not temp.IsNewable(): + templates.remove(temp) + if len(templates) == 1: + temp = templates[0] + else: + temp = self.SelectDocumentType(templates) + if temp: + newDoc = temp.CreateDocument(path, flags) + if newDoc: + newDoc.SetDocumentName(temp.GetDocumentName()) + newDoc.SetDocumentTemplate(temp) + newDoc.OnNewDocument() + return newDoc + else: + return None + + if path and flags & DOC_SILENT: + temp = self.FindTemplateForPath(path) + else: + temp, path = self.SelectDocumentPath(templates, path, flags) + + # Existing document + if path and self.GetFlags() & DOC_OPEN_ONCE: + for document in self._docs: + if document.GetFilename() and os.path.normcase(document.GetFilename()) == os.path.normcase(path): + """ check for file modification outside of application """ + if not document.IsDocumentModificationDateCorrect(): + msgTitle = wx.GetApp().GetAppName() + if not msgTitle: + msgTitle = _("Warning") + shortName = document.GetPrintableName() + res = wx.MessageBox(_("'%s' has been modified outside of %s. Reload '%s' from file system?") % (shortName, msgTitle, shortName), + msgTitle, + wx.YES_NO | wx.ICON_QUESTION, + self.FindSuitableParent()) + if res == wx.YES: + if not self.CloseDocument(document, False): + wx.MessageBox(_("Couldn't reload '%s'. Unable to close current '%s'.") % (shortName, shortName)) + return None + return self.CreateDocument(path, flags) + elif res == wx.NO: # don't ask again + document.SetDocumentModificationDate() + + firstView = document.GetFirstView() + if not firstView and not (flags & DOC_NO_VIEW): + document.GetDocumentTemplate().CreateView(document, flags) + document.UpdateAllViews() + firstView = document.GetFirstView() + + if firstView and firstView.GetFrame() and not (flags & DOC_NO_VIEW): + firstView.GetFrame().SetFocus() # Not in wxWindows code but useful nonetheless + if hasattr(firstView.GetFrame(), "IsIconized") and firstView.GetFrame().IsIconized(): # Not in wxWindows code but useful nonetheless + firstView.GetFrame().Iconize(False) + return None + + if temp: + newDoc = temp.CreateDocument(path, flags) + if newDoc: + newDoc.SetDocumentName(temp.GetDocumentName()) + newDoc.SetDocumentTemplate(temp) + if not newDoc.OnOpenDocument(path): + frame = newDoc.GetFirstView().GetFrame() + newDoc.DeleteAllViews() # Implicitly deleted by DeleteAllViews + if frame: + frame.Destroy() # DeleteAllViews doesn't get rid of the frame, so we'll explicitly destroy it. + return None + self.AddFileToHistory(path) + return newDoc + + return None + + + def CreateView(self, doc, flags=0): + """ + Creates a new view for the given document. If more than one view is + allowed for the document (by virtue of multiple templates mentioning + the same document type), a choice of view is presented to the user. + """ + templates = [] + for temp in self._templates: + if temp.IsVisible(): + if temp.GetDocumentName() == doc.GetDocumentName(): + templates.append(temp) + if len(templates) == 0: + return None + + if len(templates) == 1: + temp = templates[0] + view = temp.CreateView(doc, flags) + if view: + view.SetViewName(temp.GetViewName()) + return view + + temp = SelectViewType(templates) + if temp: + view = temp.CreateView(doc, flags) + if view: + view.SetViewName(temp.GetViewName()) + return view + else: + return None + + + def DeleteTemplate(self, template, flags): + """ + Placeholder, not yet implemented in wxWindows. + """ + pass + + + def FlushDoc(self, doc): + """ + Placeholder, not yet implemented in wxWindows. + """ + return False + + + def MatchTemplate(self, path): + """ + Placeholder, not yet implemented in wxWindows. + """ + return None + + + def GetCurrentDocument(self): + """ + Returns the document associated with the currently active view (if any). + """ + view = self.GetCurrentView() + if view: + return view.GetDocument() + else: + return None + + + def MakeDefaultName(self): + """ + Returns a suitable default name. This is implemented by appending an + integer counter to the string "Untitled" and incrementing the counter. + """ + name = _("Untitled %d") % self._defaultDocumentNameCounter + self._defaultDocumentNameCounter = self._defaultDocumentNameCounter + 1 + return name + + + def MakeFrameTitle(self): + """ + Returns a suitable title for a document frame. This is implemented by + appending the document name to the application name. + """ + appName = wx.GetApp().GetAppName() + if not doc: + title = appName + else: + docName = doc.GetPrintableName() + title = docName + _(" - ") + appName + return title + + + def AddFileToHistory(self, fileName): + """ + Adds a file to the file history list, if we have a pointer to an + appropriate file menu. + """ + if self._fileHistory: + self._fileHistory.AddFileToHistory(fileName) + + + def RemoveFileFromHistory(self, i): + """ + Removes a file from the file history list, if we have a pointer to an + appropriate file menu. + """ + if self._fileHistory: + self._fileHistory.RemoveFileFromHistory(i) + + + def GetFileHistory(self): + """ + Returns the file history. + """ + return self._fileHistory + + + def GetHistoryFile(self, i): + """ + Returns the file at index i from the file history. + """ + if self._fileHistory: + return self._fileHistory.GetHistoryFile(i) + else: + return None + + + def FileHistoryUseMenu(self, menu): + """ + Use this menu for appending recently-visited document filenames, for + convenient access. Calling this function with a valid menu enables the + history list functionality. + + Note that you can add multiple menus using this function, to be + managed by the file history object. + """ + if self._fileHistory: + self._fileHistory.UseMenu(menu) + + + def FileHistoryRemoveMenu(self, menu): + """ + Removes the given menu from the list of menus managed by the file + history object. + """ + if self._fileHistory: + self._fileHistory.RemoveMenu(menu) + + + def FileHistoryLoad(self, config): + """ + Loads the file history from a config object. + """ + if self._fileHistory: + self._fileHistory.Load(config) + + + def FileHistorySave(self, config): + """ + Saves the file history into a config object. This must be called + explicitly by the application. + """ + if self._fileHistory: + self._fileHistory.Save(config) + + + def FileHistoryAddFilesToMenu(self, menu=None): + """ + Appends the files in the history list, to all menus managed by the + file history object. + + If menu is specified, appends the files in the history list to the + given menu only. + """ + if self._fileHistory: + if menu: + self._fileHistory.AddFilesToThisMenu(menu) + else: + self._fileHistory.AddFilesToMenu() + + + def GetHistoryFilesCount(self): + """ + Returns the number of files currently stored in the file history. + """ + if self._fileHistory: + return self._fileHistory.GetNoHistoryFiles() + else: + return 0 + + + def FindTemplateForPath(self, path): + """ + Given a path, try to find template that matches the extension. This is + only an approximate method of finding a template for creating a + document. + + Note this wxPython verson looks for and returns a default template if no specific template is found. + """ + default = None + for temp in self._templates: + if temp.FileMatchesTemplate(path): + return temp + + if "*.*" in temp.GetFileFilter(): + default = temp + return default + + + def FindSuitableParent(self): + """ + Returns a parent frame or dialog, either the frame with the current + focus or if there is no current focus the application's top frame. + """ + parent = wx.GetApp().GetTopWindow() + focusWindow = wx.Window_FindFocus() + if focusWindow: + while focusWindow and not isinstance(focusWindow, wx.Dialog) and not isinstance(focusWindow, wx.Frame): + focusWindow = focusWindow.GetParent() + if focusWindow: + parent = focusWindow + return parent + + + def SelectDocumentPath(self, templates, flags, save): + """ + Under Windows, pops up a file selector with a list of filters + corresponding to document templates. The wxDocTemplate corresponding + to the selected file's extension is returned. + + On other platforms, if there is more than one document template a + choice list is popped up, followed by a file selector. + + This function is used in wxDocManager.CreateDocument. + """ + if wx.Platform == "__WXMSW__" or wx.Platform == "__WXGTK__" or wx.Platform == "__WXMAC__": + descr = '' + for temp in templates: + if temp.IsVisible(): + if len(descr) > 0: + descr = descr + _('|') + 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 + descr = _("All|*.*|%s") % descr # spacing is important, make sure there is no space after the "|", it causes a bug on wx_gtk + else: + descr = _("*.*") + + dlg = wx.FileDialog(self.FindSuitableParent(), + _("Select a File"), + wildcard=descr, + style=wx.OPEN|wx.FILE_MUST_EXIST|wx.CHANGE_DIR) + # dlg.CenterOnParent() # wxBug: caused crash with wx.FileDialog + if dlg.ShowModal() == wx.ID_OK: + path = dlg.GetPath() + else: + path = None + dlg.Destroy() + + if path: + theTemplate = self.FindTemplateForPath(path) + return (theTemplate, path) + + return (None, None) + + + def OnOpenFileFailure(self): + """ + Called when there is an error opening a file. + """ + pass + + + def SelectDocumentType(self, temps, sort=False): + """ + Returns a document template by asking the user (if there is more than + one template). This function is used in wxDocManager.CreateDocument. + + Parameters + + templates - list of templates from which to choose a desired template. + + sort - If more than one template is passed in in templates, then this + parameter indicates whether the list of templates that the user will + have to choose from is sorted or not when shown the choice box dialog. + Default is false. + """ + templates = [] + for temp in temps: + if temp.IsVisible(): + want = True + for temp2 in templates: + if temp.GetDocumentName() == temp2.GetDocumentName() and temp.GetViewName() == temp2.GetViewName(): + want = False + break + if want: + templates.append(temp) + + if len(templates) == 0: + return None + elif len(templates) == 1: + return templates[0] + + if sort: + def tempcmp(a, b): + return cmp(a.GetDescription(), b.GetDescription()) + templates.sort(tempcmp) + + strings = [] + for temp in templates: + strings.append(temp.GetDescription()) + + res = wx.GetSingleChoiceIndex(_("Select a document type:"), + _("Documents"), + strings, + self.FindSuitableParent()) + if res == -1: + return None + return templates[res] + + + def SelectViewType(self, temps, sort=False): + """ + Returns a document template by asking the user (if there is more than one template), displaying a list of valid views. This function is used in wxDocManager::CreateView. The dialog normally will not appear because the array of templates only contains those relevant to the document in question, and often there will only be one such. + """ + templates = [] + strings = [] + for temp in temps: + if temp.IsVisible() and temp.GetViewTypeName(): + if temp.GetViewName() not in strings: + templates.append(temp) + strings.append(temp.GetViewTypeName()) + + if len(templates) == 0: + return None + elif len(templates) == 1: + return templates[0] + + if sort: + def tempcmp(a, b): + return cmp(a.GetViewTypeName(), b.GetViewTypeName()) + templates.sort(tempcmp) + + res = wx.GetSingleChoiceIndex(_("Select a document view:"), + _("Views"), + strings, + self.FindSuitableParent()) + if res == -1: + return None + return templates[res] + + + def GetTemplates(self): + """ + Returns the document manager's template list. This method has been added to + wxPython and is not in wxWindows. + """ + return self._templates + + + def AssociateTemplate(self, docTemplate): + """ + Adds the template to the document manager's template list. + """ + if docTemplate not in self._templates: + self._templates.append(docTemplate) + + + def DisassociateTemplate(self, docTemplate): + """ + Removes the template from the list of templates. + """ + self._templates.remove(docTemplate) + + + def AddDocument(self, document): + """ + Adds the document to the list of documents. + """ + if document not in self._docs: + self._docs.append(document) + + + def RemoveDocument(self, doc): + """ + Removes the document from the list of documents. + """ + if doc in self._docs: + self._docs.remove(doc) + + + def ActivateView(self, view, activate=True, deleting=False): + """ + Sets the current view. + """ + if activate: + self._currentView = view + self._lastActiveView = view + else: + self._currentView = None + + + def GetMaxDocsOpen(self): + """ + Returns the number of documents that can be open simultaneously. + """ + return self._maxDocsOpen + + + def SetMaxDocsOpen(self, maxDocsOpen): + """ + Sets the maximum number of documents that can be open at a time. By + default, this is 10,000. If you set it to 1, existing documents will + be saved and deleted when the user tries to open or create a new one + (similar to the behaviour of Windows Write, for example). Allowing + multiple documents gives behaviour more akin to MS Word and other + Multiple Document Interface applications. + """ + self._maxDocsOpen = maxDocsOpen + + + def GetDocuments(self): + """ + Returns the list of documents. + """ + return self._docs + + +class DocParentFrame(wx.Frame): + """ + The wxDocParentFrame class provides a default top-level frame for + applications using the document/view framework. This class can only be + used for SDI (not MDI) parent frames. + + It cooperates with the wxView, wxDocument, wxDocManager and wxDocTemplates + classes. + """ + + def __init__(self, manager, frame, id, title, pos=wx.DefaultPosition, size=wx.DefaultSize, style=wx.DEFAULT_FRAME_STYLE, name="frame"): + """ + Constructor. Note that the event table must be rebuilt for the + frame since the EvtHandler is not virtual. + """ + wx.Frame.__init__(self, frame, id, title, pos, size, style) + self._docManager = manager + + wx.EVT_CLOSE(self, self.OnCloseWindow) + + wx.EVT_MENU(self, wx.ID_EXIT, self.OnExit) + wx.EVT_MENU_RANGE(self, wx.ID_FILE1, wx.ID_FILE9, self.OnMRUFile) + + wx.EVT_MENU(self, wx.ID_NEW, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_OPEN, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_CLOSE_ALL, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_CLOSE, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_REVERT, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_SAVE, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_SAVEAS, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_UNDO, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_REDO, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_PRINT, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_PRINT_SETUP, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_PREVIEW, self.ProcessEvent) + + wx.EVT_UPDATE_UI(self, wx.ID_NEW, self.ProcessUpdateUIEvent) + wx.EVT_UPDATE_UI(self, wx.ID_OPEN, self.ProcessUpdateUIEvent) + wx.EVT_UPDATE_UI(self, wx.ID_CLOSE_ALL, self.ProcessUpdateUIEvent) + wx.EVT_UPDATE_UI(self, wx.ID_CLOSE, self.ProcessUpdateUIEvent) + wx.EVT_UPDATE_UI(self, wx.ID_REVERT, self.ProcessUpdateUIEvent) + wx.EVT_UPDATE_UI(self, wx.ID_SAVE, self.ProcessUpdateUIEvent) + wx.EVT_UPDATE_UI(self, wx.ID_SAVEAS, self.ProcessUpdateUIEvent) + wx.EVT_UPDATE_UI(self, wx.ID_UNDO, self.ProcessUpdateUIEvent) + wx.EVT_UPDATE_UI(self, wx.ID_REDO, self.ProcessUpdateUIEvent) + wx.EVT_UPDATE_UI(self, wx.ID_PRINT, self.ProcessUpdateUIEvent) + wx.EVT_UPDATE_UI(self, wx.ID_PRINT_SETUP, self.ProcessUpdateUIEvent) + wx.EVT_UPDATE_UI(self, wx.ID_PREVIEW, self.ProcessUpdateUIEvent) + + + def ProcessEvent(self, event): + """ + Processes an event, searching event tables and calling zero or more + suitable event handler function(s). Note that the ProcessEvent + method is called from the wxPython docview framework directly since + wxPython does not have a virtual ProcessEvent function. + """ + return self._docManager and self._docManager.ProcessEvent(event) + + + def ProcessUpdateUIEvent(self, event): + """ + Processes a UI event, searching event tables and calling zero or more + suitable event handler function(s). Note that the ProcessEvent + method is called from the wxPython docview framework directly since + wxPython does not have a virtual ProcessEvent function. + """ + return self._docManager and self._docManager.ProcessUpdateUIEvent(event) + + + def OnExit(self, event): + """ + Called when File/Exit is chosen and closes the window. + """ + self.Close() + + + def OnMRUFile(self, event): + """ + Opens the appropriate file when it is selected from the file history + menu. + """ + n = event.GetId() - wx.ID_FILE1 + filename = self._docManager.GetHistoryFile(n) + if filename: + self._docManager.CreateDocument(filename, DOC_SILENT) + else: + self._docManager.RemoveFileFromHistory(n) + msgTitle = wx.GetApp().GetAppName() + if not msgTitle: + msgTitle = _("File Error") + wx.MessageBox("The file '%s' doesn't exist and couldn't be opened.\nIt has been removed from the most recently used files list" % FileNameFromPath(file), + msgTitle, + wx.OK | wx.ICON_EXCLAMATION, + self) + + + def OnCloseWindow(self, event): + """ + Deletes all views and documents. If no user input cancelled the + operation, the frame will be destroyed and the application will exit. + """ + if self._docManager.Clear(not event.CanVeto()): + self.Destroy() + else: + event.Veto() + + +class DocChildFrame(wx.Frame): + """ + The wxDocChildFrame class provides a default frame for displaying + documents on separate windows. This class can only be used for SDI (not + MDI) child frames. + + The class is part of the document/view framework supported by wxWindows, + and cooperates with the wxView, wxDocument, wxDocManager and wxDocTemplate + classes. + """ + + + def __init__(self, doc, view, frame, id, title, pos=wx.DefaultPosition, size=wx.DefaultSize, style=wx.DEFAULT_FRAME_STYLE, name="frame"): + """ + Constructor. Note that the event table must be rebuilt for the + frame since the EvtHandler is not virtual. + """ + wx.Frame.__init__(self, frame, id, title, pos, size, style, name) + wx.EVT_ACTIVATE(self, self.OnActivate) + wx.EVT_CLOSE(self, self.OnCloseWindow) + self._childDocument = doc + self._childView = view + if view: + view.SetFrame(self) + + wx.EVT_MENU(self, wx.ID_NEW, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_OPEN, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_CLOSE_ALL, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_CLOSE, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_REVERT, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_SAVE, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_SAVEAS, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_UNDO, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_REDO, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_PRINT, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_PRINT_SETUP, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_PREVIEW, self.ProcessEvent) + + wx.EVT_UPDATE_UI(self, wx.ID_NEW, self.ProcessUpdateUIEvent) + wx.EVT_UPDATE_UI(self, wx.ID_OPEN, self.ProcessUpdateUIEvent) + wx.EVT_UPDATE_UI(self, wx.ID_CLOSE_ALL, self.ProcessUpdateUIEvent) + wx.EVT_UPDATE_UI(self, wx.ID_CLOSE, self.ProcessUpdateUIEvent) + wx.EVT_UPDATE_UI(self, wx.ID_REVERT, self.ProcessUpdateUIEvent) + wx.EVT_UPDATE_UI(self, wx.ID_SAVE, self.ProcessUpdateUIEvent) + wx.EVT_UPDATE_UI(self, wx.ID_SAVEAS, self.ProcessUpdateUIEvent) + wx.EVT_UPDATE_UI(self, wx.ID_UNDO, self.ProcessUpdateUIEvent) + wx.EVT_UPDATE_UI(self, wx.ID_REDO, self.ProcessUpdateUIEvent) + wx.EVT_UPDATE_UI(self, wx.ID_PRINT, self.ProcessUpdateUIEvent) + wx.EVT_UPDATE_UI(self, wx.ID_PRINT_SETUP, self.ProcessUpdateUIEvent) + wx.EVT_UPDATE_UI(self, wx.ID_PREVIEW, self.ProcessUpdateUIEvent) + + + def ProcessEvent(self, event): + """ + Processes an event, searching event tables and calling zero or more + suitable event handler function(s). Note that the ProcessEvent + method is called from the wxPython docview framework directly since + wxPython does not have a virtual ProcessEvent function. + """ + if self._childView: + self._childView.Activate(True) + if not self._childView or not self._childView.ProcessEvent(event): + # IsInstance not working, but who cares just send all the commands up since this isn't a real ProcessEvent like wxWindows + # if not isinstance(event, wx.CommandEvent) or not self.GetParent() or not self.GetParent().ProcessEvent(event): + if not self.GetParent() or not self.GetParent().ProcessEvent(event): + return False + else: + return True + else: + return True + + + def ProcessUpdateUIEvent(self, event): + """ + Processes a UI event, searching event tables and calling zero or more + suitable event handler function(s). Note that the ProcessEvent + method is called from the wxPython docview framework directly since + wxPython does not have a virtual ProcessEvent function. + """ + if self.GetParent(): + self.GetParent().ProcessUpdateUIEvent(event) + else: + return False + + + def OnActivate(self, event): + """ + Activates the current view. + """ + event.Skip() + if self._childView: + self._childView.Activate(event.GetActive()) + + + def OnCloseWindow(self, event): + """ + Closes and deletes the current view and document. + """ + if self._childView: + ans = False + if not event.CanVeto(): + ans = True + else: + ans = self._childView.Close(deleteWindow = False) + + if ans: + self._childView.Activate(False) + self._childView.Destroy() + self._childView = None + if self._childDocument: + self._childDocument.Destroy() # This isn't in the wxWindows codebase but the document needs to be disposed of somehow + self._childDocument = None + self.Destroy() + else: + event.Veto() + else: + event.Veto() + + + def GetDocument(self): + """ + Returns the document associated with this frame. + """ + return self._childDocument + + + def SetDocument(self, document): + """ + Sets the document for this frame. + """ + self._childDocument = document + + + def GetView(self): + """ + Returns the view associated with this frame. + """ + return self._childView + + + def SetView(self, view): + """ + Sets the view for this frame. + """ + self._childView = view + + +class DocMDIParentFrame(wx.MDIParentFrame): + """ + The wxDocMDIParentFrame class provides a default top-level frame for + applications using the document/view framework. This class can only be + used for MDI parent frames. + + It cooperates with the wxView, wxDocument, wxDocManager and wxDocTemplate + classes. + """ + + + def __init__(self, manager, frame, id, title, pos=wx.DefaultPosition, size=wx.DefaultSize, style=wx.DEFAULT_FRAME_STYLE, name="frame"): + """ + Constructor. Note that the event table must be rebuilt for the + frame since the EvtHandler is not virtual. + """ + wx.MDIParentFrame.__init__(self, frame, id, title, pos, size, style, name) + self._docManager = manager + + wx.EVT_CLOSE(self, self.OnCloseWindow) + + wx.EVT_MENU(self, wx.ID_EXIT, self.OnExit) + wx.EVT_MENU_RANGE(self, wx.ID_FILE1, wx.ID_FILE9, self.OnMRUFile) + + wx.EVT_MENU(self, wx.ID_NEW, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_OPEN, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_CLOSE_ALL, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_CLOSE, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_REVERT, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_SAVE, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_SAVEAS, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_UNDO, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_REDO, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_PRINT, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_PRINT_SETUP, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_PREVIEW, self.ProcessEvent) + + wx.EVT_UPDATE_UI(self, wx.ID_NEW, self.ProcessUpdateUIEvent) + wx.EVT_UPDATE_UI(self, wx.ID_OPEN, self.ProcessUpdateUIEvent) + wx.EVT_UPDATE_UI(self, wx.ID_CLOSE_ALL, self.ProcessUpdateUIEvent) + wx.EVT_UPDATE_UI(self, wx.ID_CLOSE, self.ProcessUpdateUIEvent) + wx.EVT_UPDATE_UI(self, wx.ID_REVERT, self.ProcessUpdateUIEvent) + wx.EVT_UPDATE_UI(self, wx.ID_SAVE, self.ProcessUpdateUIEvent) + wx.EVT_UPDATE_UI(self, wx.ID_SAVEAS, self.ProcessUpdateUIEvent) + wx.EVT_UPDATE_UI(self, wx.ID_UNDO, self.ProcessUpdateUIEvent) + wx.EVT_UPDATE_UI(self, wx.ID_REDO, self.ProcessUpdateUIEvent) + wx.EVT_UPDATE_UI(self, wx.ID_PRINT, self.ProcessUpdateUIEvent) + wx.EVT_UPDATE_UI(self, wx.ID_PRINT_SETUP, self.ProcessUpdateUIEvent) + wx.EVT_UPDATE_UI(self, wx.ID_PREVIEW, self.ProcessUpdateUIEvent) + + + def ProcessEvent(self, event): + """ + Processes an event, searching event tables and calling zero or more + suitable event handler function(s). Note that the ProcessEvent + method is called from the wxPython docview framework directly since + wxPython does not have a virtual ProcessEvent function. + """ + return self._docManager and self._docManager.ProcessEvent(event) + + + def ProcessUpdateUIEvent(self, event): + """ + Processes a UI event, searching event tables and calling zero or more + suitable event handler function(s). Note that the ProcessEvent + method is called from the wxPython docview framework directly since + wxPython does not have a virtual ProcessEvent function. + """ + return self._docManager and self._docManager.ProcessUpdateUIEvent(event) + + + def OnExit(self, event): + """ + Called when File/Exit is chosen and closes the window. + """ + self.Close() + + + def OnMRUFile(self, event): + """ + Opens the appropriate file when it is selected from the file history + menu. + """ + n = event.GetId() - wx.ID_FILE1 + filename = self._docManager.GetHistoryFile(n) + if filename: + self._docManager.CreateDocument(filename, DOC_SILENT) + else: + self._docManager.RemoveFileFromHistory(n) + msgTitle = wx.GetApp().GetAppName() + if not msgTitle: + msgTitle = _("File Error") + wx.MessageBox("The file '%s' doesn't exist and couldn't be opened.\nIt has been removed from the most recently used files list" % FileNameFromPath(file), + msgTitle, + wx.OK | wx.ICON_EXCLAMATION, + self) + + + def OnCloseWindow(self, event): + """ + Deletes all views and documents. If no user input cancelled the + operation, the frame will be destroyed and the application will exit. + """ + if self._docManager.Clear(not event.CanVeto()): + self.Destroy() + else: + event.Veto() + + +class DocMDIChildFrame(wx.MDIChildFrame): + """ + The wxDocMDIChildFrame class provides a default frame for displaying + documents on separate windows. This class can only be used for MDI child + frames. + + The class is part of the document/view framework supported by wxWindows, + and cooperates with the wxView, wxDocument, wxDocManager and wxDocTemplate + classes. + """ + + + def __init__(self, doc, view, frame, id, title, pos=wx.DefaultPosition, size=wx.DefaultSize, style=wx.DEFAULT_FRAME_STYLE, name="frame"): + """ + Constructor. Note that the event table must be rebuilt for the + frame since the EvtHandler is not virtual. + """ + wx.MDIChildFrame.__init__(self, frame, id, title, pos, size, style, name) + self._childDocument = doc + self._childView = view + if view: + view.SetFrame(self) + # self.Create(doc, view, frame, id, title, pos, size, style, name) + self._activeEvent = None + self._activated = 0 + wx.EVT_ACTIVATE(self, self.OnActivate) + wx.EVT_CLOSE(self, self.OnCloseWindow) + + if frame: # wxBug: For some reason the EVT_ACTIVATE event is not getting triggered for the first mdi client window that is opened so we have to do it manually + mdiChildren = filter(lambda x: isinstance(x, wx.MDIChildFrame), frame.GetChildren()) + if len(mdiChildren) == 1: + self.Activate() + + +## # Couldn't get this to work, but seems to work fine with single stage construction +## def Create(self, doc, view, frame, id, title, pos, size, style, name): +## self._childDocument = doc +## self._childView = view +## if wx.MDIChildFrame.Create(self, frame, id, title, pos, size, style, name): +## if view: +## view.SetFrame(self) +## return True +## return False + + + + def Activate(self): # Need this in case there are embedded sash windows and such, OnActivate is not getting called + """ + Activates the current view. + """ + if self._childView: + self._childView.Activate(True) + + + def ProcessEvent(event): + """ + Processes an event, searching event tables and calling zero or more + suitable event handler function(s). Note that the ProcessEvent + method is called from the wxPython docview framework directly since + wxPython does not have a virtual ProcessEvent function. + """ + if self._activeEvent == event: + return False + + self._activeEvent = event # Break recursion loops + + if self._childView: + self._childView.Activate(True) + + if not self._childView or not self._childView.ProcessEvent(event): + if not isinstance(event, wx.CommandEvent) or not self.GetParent() or not self.GetParent().ProcessEvent(event): + ret = False + else: + ret = True + else: + ret = True + + self._activeEvent = None + return ret + + + def OnActivate(self, event): + """ + Sets the currently active view to be the frame's view. You may need to + override (but still call) this function in order to set the keyboard + focus for your subwindow. + """ + event.Skip() + if self._activated != 0: + return True + self._activated += 1 + wx.MDIChildFrame.Activate(self) + if event.GetActive() and self._childView: + self._childView.Activate(event.GetActive()) + self._activated = 0 + + + def OnCloseWindow(self, event): + """ + Closes and deletes the current view and document. + """ + if self._childView: + ans = False + if not event.CanVeto(): + ans = True + else: + ans = self._childView.Close(deleteWindow = False) + + if ans: + self._childView.Activate(False) + self._childView.Destroy() + self._childView = None + if self._childDocument: # This isn't in the wxWindows codebase but the document needs to be disposed of somehow + self._childDocument.DeleteContents() + if self._childDocument.GetDocumentManager(): + self._childDocument.GetDocumentManager().RemoveDocument(self._childDocument) + self._childDocument = None + self.Destroy() + else: + event.Veto() + else: + event.Veto() + + + def GetDocument(self): + """ + Returns the document associated with this frame. + """ + return self._childDocument + + + def SetDocument(self, document): + """ + Sets the document for this frame. + """ + self._childDocument = document + + + def GetView(self): + """ + Returns the view associated with this frame. + """ + return self._childView + + + def SetView(self, view): + """ + Sets the view for this frame. + """ + self._childView = view + + + def OnTitleIsModified(self): + """ + Add/remove to the frame's title an indication that the document is dirty. + If the document is dirty, an '*' is appended to the title + This method has been added to wxPython and is not in wxWindows. + """ + title = self.GetTitle() + if title: + if self.GetDocument().IsModified(): + if title.endswith("*"): + return + else: + title = title + "*" + self.SetTitle(title) + else: + if title.endswith("*"): + title = title[:-1] + self.SetTitle(title) + else: + return + + +class DocPrintout(wx.Printout): + """ + DocPrintout is a default Printout that prints the first page of a document + view. + """ + + + def __init__(self, view, title="Printout"): + """ + Constructor. + """ + wx.Printout.__init__(self, title) + self._printoutView = view + + + def GetView(self): + """ + Returns the DocPrintout's view. + """ + return self._printoutView + + + def OnPrintPage(self, page): + """ + Prints the first page of the view. + """ + dc = self.GetDC() + ppiScreenX, ppiScreenY = self.GetPPIScreen() + ppiPrinterX, ppiPrinterY = self.GetPPIPrinter() + scale = ppiPrinterX/ppiScreenX + w, h = dc.GetSize() + pageWidth, pageHeight = self.GetPageSizePixels() + overallScale = scale * w / pageWidth + dc.SetUserScale(overallScale, overallScale) + if self._printoutView: + self._printoutView.OnDraw(dc) + return True + + + def HasPage(self, pageNum): + """ + Indicates that the DocPrintout only has a single page. + """ + return pageNum == 1 + + + def GetPageInfo(self): + """ + Indicates that the DocPrintout only has a single page. + """ + minPage = 1 + maxPage = 1 + selPageFrom = 1 + selPageTo = 1 + return (minPage, maxPage, selPageFrom, selPageTo) + + +#---------------------------------------------------------------------- +# Command Classes +#---------------------------------------------------------------------- + +class Command(wx.Object): + """ + wxCommand is a base class for modelling an application command, which is + an action usually performed by selecting a menu item, pressing a toolbar + button or any other means provided by the application to change the data + or view. + """ + + + def __init__(self, canUndo = False, name = None): + """ + Constructor. wxCommand is an abstract class, so you will need to + derive a new class and call this constructor from your own constructor. + + canUndo tells the command processor whether this command is undo-able. + You can achieve the same functionality by overriding the CanUndo member + function (if for example the criteria for undoability is context- + dependent). + + name must be supplied for the command processor to display the command + name in the application's edit menu. + """ + self._canUndo = canUndo + self._name = name + + + def CanUndo(self): + """ + Returns true if the command can be undone, false otherwise. + """ + return self._canUndo + + + def GetName(self): + """ + Returns the command name. + """ + return self._name + + + def Do(self): + """ + Override this member function to execute the appropriate action when + called. Return true to indicate that the action has taken place, false + otherwise. Returning false will indicate to the command processor that + the action is not undoable and should not be added to the command + history. + """ + return True + + + def Undo(self): + """ + Override this member function to un-execute a previous Do. Return true + to indicate that the action has taken place, false otherwise. Returning + false will indicate to the command processor that the action is not + redoable and no change should be made to the command history. + + How you implement this command is totally application dependent, but + typical strategies include: + + Perform an inverse operation on the last modified piece of data in the + document. When redone, a copy of data stored in command is pasted back + or some operation reapplied. This relies on the fact that you know the + ordering of Undos; the user can never Undo at an arbitrary position in + he command history. + + Restore the entire document state (perhaps using document + transactioning). Potentially very inefficient, but possibly easier to + code if the user interface and data are complex, and an 'inverse + execute' operation is hard to write. + """ + return True + + +class CommandProcessor(wx.Object): + """ + wxCommandProcessor is a class that maintains a history of wxCommands, with + undo/redo functionality built-in. Derive a new class from this if you want + different behaviour. + """ + + + def __init__(self, maxCommands=-1): + """ + Constructor. maxCommands may be set to a positive integer to limit + the number of commands stored to it, otherwise (and by default) the + list of commands can grow arbitrarily. + """ + self._maxCommands = maxCommands + self._editMenu = None + self._undoAccelerator = _("Ctrl+Z") + self._redoAccelerator = _("Ctrl+Y") + self.ClearCommands() + + + def _GetCurrentCommand(self): + if len(self._commands) == 0: + return None + else: + return self._commands[-1] + + + def _GetCurrentRedoCommand(self): + if len(self._redoCommands) == 0: + return None + else: + return self._redoCommands[-1] + + + def GetMaxCommands(self): + """ + Returns the maximum number of commands that the command processor + stores. + + """ + return self._maxCommands + + + def GetCommands(self): + """ + Returns the list of commands. + """ + return self._commands + + + def ClearCommands(self): + """ + Deletes all the commands in the list and sets the current command + pointer to None. + """ + self._commands = [] + self._redoCommands = [] + + + def GetEditMenu(self): + """ + Returns the edit menu associated with the command processor. + """ + return self._editMenu + + + def SetEditMenu(self, menu): + """ + Tells the command processor to update the Undo and Redo items on this + menu as appropriate. Set this to NULL if the menu is about to be + destroyed and command operations may still be performed, or the + command processor may try to access an invalid pointer. + """ + self._editMenu = menu + + + def GetUndoAccelerator(self): + """ + Returns the string that will be appended to the Undo menu item. + """ + return self._undoAccelerator + + + def SetUndoAccelerator(self, accel): + """ + Sets the string that will be appended to the Redo menu item. + """ + self._undoAccelerator = accel + + + def GetRedoAccelerator(self): + """ + Returns the string that will be appended to the Redo menu item. + """ + return self._redoAccelerator + + + def SetRedoAccelerator(self, accel): + """ + Sets the string that will be appended to the Redo menu item. + """ + self._redoAccelerator = accel + + + def SetMenuStrings(self): + """ + Sets the menu labels according to the currently set menu and the + current command state. + """ + if self.GetEditMenu() != None: + undoCommand = self._GetCurrentCommand() + redoCommand = self._GetCurrentRedoCommand() + undoItem = self.GetEditMenu().FindItemById(wx.ID_UNDO) + redoItem = self.GetEditMenu().FindItemById(wx.ID_REDO) + if self.GetUndoAccelerator(): + undoAccel = '\t' + self.GetUndoAccelerator() + else: + undoAccel = '' + if self.GetRedoAccelerator(): + redoAccel = '\t' + self.GetRedoAccelerator() + else: + redoAccel = '' + if undoCommand and undoItem and undoCommand.CanUndo(): + undoItem.SetText(_("&Undo ") + undoCommand.GetName() + undoAccel) + #elif undoCommand and not undoCommand.CanUndo(): + # undoItem.SetText(_("Can't Undo") + undoAccel) + else: + undoItem.SetText(_("&Undo" + undoAccel)) + if redoCommand and redoItem: + redoItem.SetText(_("&Redo ") + redoCommand.GetName() + redoAccel) + else: + redoItem.SetText(_("&Redo") + redoAccel) + + + def CanUndo(self): + """ + Returns true if the currently-active command can be undone, false + otherwise. + """ + if self._GetCurrentCommand() == None: + return False + return self._GetCurrentCommand().CanUndo() + + + def CanRedo(self): + """ + Returns true if the currently-active command can be redone, false + otherwise. + """ + return self._GetCurrentRedoCommand() != None + + + def Submit(self, command, storeIt=True): + """ + Submits a new command to the command processor. The command processor + calls wxCommand::Do to execute the command; if it succeeds, the + command is stored in the history list, and the associated edit menu + (if any) updated appropriately. If it fails, the command is deleted + immediately. Once Submit has been called, the passed command should + not be deleted directly by the application. + + storeIt indicates whether the successful command should be stored in + the history list. + """ + done = command.Do() + if done: + del self._redoCommands[:] + if storeIt: + self._commands.append(command) + if self._maxCommands > -1: + if len(self._commands) > self._maxCommands: + del self._commands[0] + return done + + + def Redo(self): + """ + Redoes the command just undone. + """ + cmd = self._GetCurrentRedoCommand() + if not cmd: + return False + done = cmd.Do() + if done: + self._commands.append(self._redoCommands.pop()) + return done + + + def Undo(self): + """ + Undoes the command just executed. + """ + cmd = self._GetCurrentCommand() + if not cmd: + return False + done = cmd.Undo() + if done: + self._redoCommands.append(self._commands.pop()) + return done + + diff --git a/wx/lib/dragscroller.py b/wx/lib/dragscroller.py new file mode 100644 index 00000000..7410da81 --- /dev/null +++ b/wx/lib/dragscroller.py @@ -0,0 +1,79 @@ +#----------------------------------------------------------------------------- +# Name: dragscroller.py +# Purpose: Scrolls a wx.ScrollWindow by dragging +# +# Author: Riaan Booysen +# +# Created: 2006/09/05 +# Copyright: (c) 2006 +# Licence: wxPython +#----------------------------------------------------------------------------- + +import wx + +class DragScroller: + """ Scrolls a wx.ScrollWindow in the direction and speed of a mouse drag. + + Call Start with the position of the drag start. + Call Stop on the drag release. """ + + def __init__(self, scrollwin, rate=30, sensitivity=0.75): + self.scrollwin = scrollwin + self.rate = rate + self.sensitivity = sensitivity + + self.pos = None + self.timer = None + + def GetScrollWindow(self): + return self.scrollwin + def SetScrollWindow(self, scrollwin): + self.scrollwin = scrollwin + + def GetUpdateRate(self): + return self.rate + def SetUpdateRate(self, rate): + self.rate = rate + + def GetSensitivity(self): + return self.sensitivity + def SetSensitivity(self, sensitivity): + self.sensitivity = sensitivity + + def Start(self, pos): + """ Start a drag scroll operation """ + if not self.scrollwin: + raise Exception, 'No ScrollWindow defined' + + self.pos = pos + self.scrollwin.SetCursor(wx.StockCursor(wx.CURSOR_SIZING)) + if not self.scrollwin.HasCapture(): + self.scrollwin.CaptureMouse() + + self.timer = wx.Timer(self.scrollwin) + self.scrollwin.Bind(wx.EVT_TIMER, self.OnTimerDoScroll, id=self.timer.GetId()) + self.timer.Start(self.rate) + + def Stop(self): + """ Stops a drag scroll operation """ + if self.timer and self.scrollwin: + self.timer.Stop() + self.scrollwin.Disconnect(self.timer.GetId()) + self.timer.Destroy() + self.timer = None + + self.scrollwin.SetCursor(wx.STANDARD_CURSOR) + if self.scrollwin.HasCapture(): + self.scrollwin.ReleaseMouse() + + def OnTimerDoScroll(self, event): + if self.pos is None or not self.timer or not self.scrollwin: + return + + new = self.scrollwin.ScreenToClient(wx.GetMousePosition()) + dx = int((new.x-self.pos.x)*self.sensitivity) + dy = int((new.y-self.pos.y)*self.sensitivity) + spx = self.scrollwin.GetScrollPos(wx.HORIZONTAL) + spy = self.scrollwin.GetScrollPos(wx.VERTICAL) + + self.scrollwin.Scroll(spx+dx, spy+dy) diff --git a/wx/lib/editor/README.txt b/wx/lib/editor/README.txt new file mode 100644 index 00000000..6bf61637 --- /dev/null +++ b/wx/lib/editor/README.txt @@ -0,0 +1,77 @@ +wxEditor component +------------------ + +The wxEditor class implements a simple text editor using wxPython. You +can create a custom editor by subclassing wxEditor. Even though much of +the editor is implemented in Python, it runs surprisingly smoothly on +normal hardware with small files. + + +Keys +---- +Keys are similar to Windows-based editors: + +Tab: 1 to 4 spaces (to next tab stop) +Cursor movement: Arrow keys +Beginning of line: Home +End of line: End +Beginning of buffer: Control-Home +End of the buffer: Control-End +Select text: Hold down Shift while moving the cursor +Copy: Shift-Insert, Control-C +Cut: Shift-Delete, Control-X +Paste: Control-Insert, Control-V + +How to use it +------------- +The demo code (demo/wxEditor.py) shows how to use it as a simple text +box. Use the SetText() and GetText() methods to set or get text from +the component; these both return a list of strings. + +The samples/FrogEdit directory has an example of a simple text editor +application that uses the wxEditor component. + +Subclassing +----------- +To add or change functionality, you can subclass this +component. One example of this might be to change the key +Alt key commands. In that case you would (for example) override the +SetAltFuncs() method. + +History +------- +The original author of this component was Dirk Holtwic. It originally +had limited support for syntax highlighting, but was not a usable text +editor, as it didn't implement select (with keys or mouse), or any of +the usual key sequences you'd expect in an editor. Robin Dunn did some +refactoring work to make it more usable. Steve Howell and Adam Feuer +did a lot of refactoring, and added some functionality, including +keyboard and mouse select, properly working scrollbars, and +overridable keys. Adam and Steve also removed support for +syntax-highlighting while refactoring the code. + +To do +----- +Alt/Ctrl Arrow keys move by word +Descriptive help text for keys +Speed improvements +Different fonts/colors + + +Authors +------- +Steve Howell, Adam Feuer, Dirk Holtwic, Robin Dunn + + +Contact +------- +You can find the latest code for wxEditor here: +http://www.pobox.com/~adamf/software/ + +We're not actively maintaining this code, but we can answer +questions about it. You can email us at: + +Adam Feuer +Steve Howell + +29 November 2001 diff --git a/wx/lib/editor/__init__.py b/wx/lib/editor/__init__.py new file mode 100644 index 00000000..05c8d86a --- /dev/null +++ b/wx/lib/editor/__init__.py @@ -0,0 +1,25 @@ +#---------------------------------------------------------------------- +# Name: wxPython.lib.editor +# Purpose: A package containing a colourizable text editror +# +# Author: Robin Dunn +# +# Created: 30-Dec-1999 +# RCS-ID: $Id$ +# Copyright: (c) 1999 by Total Control Software +# Licence: wxWindows license +#---------------------------------------------------------------------- +# 12/14/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o 2.5 compatability update. +# +# 12/21/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o wxEditor -> Editor +# + +# This file makes this directory into a Python package + + +# import the main classes into the package namespace. +from editor import Editor diff --git a/wx/lib/editor/editor.py b/wx/lib/editor/editor.py new file mode 100644 index 00000000..1bb0a989 --- /dev/null +++ b/wx/lib/editor/editor.py @@ -0,0 +1,976 @@ +#---------------------------------------------------------------------- +# Name: wxPython.lib.editor.Editor +# Purpose: An intelligent text editor with colorization capabilities. +# +# Original +# Authors: Dirk Holtwic, Robin Dunn +# +# New +# Authors: Adam Feuer, Steve Howell +# +# History: +# This code used to support a fairly complex subclass that did +# syntax coloring and outliner collapse mode. Adam and Steve +# inherited the code, and added a lot of basic editor +# functionality that had not been there before, such as cut-and-paste. +# +# +# Created: 15-Dec-1999 +# RCS-ID: $Id$ +# Copyright: (c) 1999 by Dirk Holtwick, 1999 +# Licence: wxWindows license +#---------------------------------------------------------------------- +# 12/14/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o 2.5 compatability update. +# +# 12/21/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o wxEditor -> Editor +# + +import os +import time + +import wx + +import selection +import images + +#---------------------------- + +def ForceBetween(min, val, max): + if val > max: + return max + if val < min: + return min + return val + + +def LineTrimmer(lineOfText): + if len(lineOfText) == 0: + return "" + elif lineOfText[-1] == '\r': + return lineOfText[:-1] + else: + return lineOfText + +def LineSplitter(text): + return map (LineTrimmer, text.split('\n')) + + +#---------------------------- + +class Scroller: + def __init__(self, parent): + self.parent = parent + self.ow = 0 + self.oh = 0 + self.ox = 0 + self.oy = 0 + + def SetScrollbars(self, fw, fh, w, h, x, y): + if (self.ow != w or self.oh != h or self.ox != x or self.oy != y): + self.parent.SetScrollbars(fw, fh, w, h, x, y) + self.ow = w + self.oh = h + self.ox = x + self.oy = y + +#---------------------------------------------------------------------- + +class Editor(wx.ScrolledWindow): + + def __init__(self, parent, id, + pos=wx.DefaultPosition, size=wx.DefaultSize, style=0): + + wx.ScrolledWindow.__init__(self, parent, id, + pos, size, + style|wx.WANTS_CHARS) + + self.isDrawing = False + + self.InitCoords() + self.InitFonts() + self.SetColors() + self.MapEvents() + self.LoadImages() + self.InitDoubleBuffering() + self.InitScrolling() + self.SelectOff() + self.SetFocus() + self.SetText([""]) + self.SpacesPerTab = 4 + +##------------------ Init stuff + + def InitCoords(self): + self.cx = 0 + self.cy = 0 + self.oldCx = 0 + self.oldCy = 0 + self.sx = 0 + self.sy = 0 + self.sw = 0 + self.sh = 0 + self.sco_x = 0 + self.sco_y = 0 + + def MapEvents(self): + self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) + self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp) + self.Bind(wx.EVT_MOTION, self.OnMotion) + self.Bind(wx.EVT_SCROLLWIN, self.OnScroll) + self.Bind(wx.EVT_CHAR, self.OnChar) + self.Bind(wx.EVT_PAINT, self.OnPaint) + self.Bind(wx.EVT_SIZE, self.OnSize) + self.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroy) + self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground) + +##------------------- Platform-specific stuff + + def NiceFontForPlatform(self): + if wx.Platform == "__WXMSW__": + font = wx.Font(10, wx.MODERN, wx.NORMAL, wx.NORMAL) + else: + font = wx.Font(12, wx.MODERN, wx.NORMAL, wx.NORMAL, False) + if wx.Platform == "__WXMAC__": + font.SetNoAntiAliasing() + return font + + def UnixKeyHack(self, key): + # + # this will be obsolete when we get the new wxWindows patch + # + # 12/14/03 - jmg + # + # Which patch? I don't know if this is needed, but I don't know + # why it's here either. Play it safe; leave it in. + # + if key <= 26: + key += ord('a') - 1 + return key + +##-------------------- UpdateView/Cursor code + + def OnSize(self, event): + self.AdjustScrollbars() + self.SetFocus() + + def SetCharDimensions(self): + # TODO: We need a code review on this. It appears that Linux + # improperly reports window dimensions when the scrollbar's there. + self.bw, self.bh = self.GetClientSize() + + if wx.Platform == "__WXMSW__": + self.sh = self.bh / self.fh + self.sw = (self.bw / self.fw) - 1 + else: + self.sh = self.bh / self.fh + if self.LinesInFile() >= self.sh: + self.bw = self.bw - wx.SystemSettings_GetMetric(wx.SYS_VSCROLL_X) + self.sw = (self.bw / self.fw) - 1 + + self.sw = (self.bw / self.fw) - 1 + if self.CalcMaxLineLen() >= self.sw: + self.bh = self.bh - wx.SystemSettings_GetMetric(wx.SYS_HSCROLL_Y) + self.sh = self.bh / self.fh + + + def UpdateView(self, dc = None): + if dc is None: + dc = wx.ClientDC(self) + if dc.Ok(): + self.SetCharDimensions() + self.KeepCursorOnScreen() + self.DrawSimpleCursor(0,0, dc, True) + self.Draw(dc) + + def OnPaint(self, event): + dc = wx.PaintDC(self) + if self.isDrawing: + return + self.isDrawing = True + self.UpdateView(dc) + wx.CallAfter(self.AdjustScrollbars) + self.isDrawing = False + + def OnEraseBackground(self, evt): + pass + +##-------------------- Drawing code + + def InitFonts(self): + dc = wx.ClientDC(self) + self.font = self.NiceFontForPlatform() + dc.SetFont(self.font) + self.fw = dc.GetCharWidth() + self.fh = dc.GetCharHeight() + + def SetColors(self): + self.fgColor = wx.NamedColour('black') + self.bgColor = wx.NamedColour('white') + self.selectColor = wx.Colour(238, 220, 120) # r, g, b = emacsOrange + + def InitDoubleBuffering(self): + pass + + def DrawEditText(self, t, x, y, dc): + dc.DrawText(t, x * self.fw, y * self.fh) + + def DrawLine(self, line, dc): + if self.IsLine(line): + l = line + t = self.lines[l] + dc.SetTextForeground(self.fgColor) + fragments = selection.Selection( + self.SelectBegin, self.SelectEnd, + self.sx, self.sw, line, t) + x = 0 + for (data, selected) in fragments: + if selected: + dc.SetTextBackground(self.selectColor) + if x == 0 and len(data) == 0 and len(fragments) == 1: + data = ' ' + else: + dc.SetTextBackground(self.bgColor) + self.DrawEditText(data, x, line - self.sy, dc) + x += len(data) + + def Draw(self, odc=None): + if not odc: + odc = wx.ClientDC(self) + + dc = wx.BufferedDC(odc) + if dc.IsOk(): + dc.SetFont(self.font) + dc.SetBackgroundMode(wx.SOLID) + dc.SetTextBackground(self.bgColor) + dc.SetTextForeground(self.fgColor) + dc.Clear() + for line in range(self.sy, self.sy + self.sh): + self.DrawLine(line, dc) + if len(self.lines) < self.sh + self.sy: + self.DrawEofMarker(dc) + self.DrawCursor(dc) + +##------------------ eofMarker stuff + + def LoadImages(self): + self.eofMarker = images.EofImage.GetBitmap() + + def DrawEofMarker(self,dc): + x = 0 + y = (len(self.lines) - self.sy) * self.fh + hasTransparency = 1 + dc.DrawBitmap(self.eofMarker, x, y, hasTransparency) + +##------------------ cursor-related functions + + def DrawCursor(self, dc = None): + if not dc: + dc = wx.ClientDC(self) + + if (self.LinesInFile())maxlen: + maxlen = len(line) + return maxlen + + def KeepCursorOnScreen(self): + self.sy = ForceBetween(max(0, self.cy-self.sh), self.sy, self.cy) + self.sx = ForceBetween(max(0, self.cx-self.sw), self.sx, self.cx) + self.AdjustScrollbars() + + def HorizBoundaries(self): + self.SetCharDimensions() + maxLineLen = self.CalcMaxLineLen() + self.sx = ForceBetween(0, self.sx, max(self.sw, maxLineLen - self.sw + 1)) + self.cx = ForceBetween(self.sx, self.cx, self.sx + self.sw - 1) + + def VertBoundaries(self): + self.SetCharDimensions() + self.sy = ForceBetween(0, self.sy, max(self.sh, self.LinesInFile() - self.sh + 1)) + self.cy = ForceBetween(self.sy, self.cy, self.sy + self.sh - 1) + + def cVert(self, num): + self.cy = self.cy + num + self.cy = ForceBetween(0, self.cy, self.LinesInFile() - 1) + self.sy = ForceBetween(self.cy - self.sh + 1, self.sy, self.cy) + self.cx = min(self.cx, self.CurrentLineLength()) + + def cHoriz(self, num): + self.cx = self.cx + num + self.cx = ForceBetween(0, self.cx, self.CurrentLineLength()) + self.sx = ForceBetween(self.cx - self.sw + 1, self.sx, self.cx) + + def AboveScreen(self, row): + return row < self.sy + + def BelowScreen(self, row): + return row >= self.sy + self.sh + + def LeftOfScreen(self, col): + return col < self.sx + + def RightOfScreen(self, col): + return col >= self.sx + self.sw + +##----------------- data structure helper functions + + def GetText(self): + return self.lines + + def SetText(self, lines): + self.InitCoords() + self.lines = lines + self.UnTouchBuffer() + self.SelectOff() + self.AdjustScrollbars() + self.UpdateView(None) + + def IsLine(self, lineNum): + return (0<=lineNum) and (lineNum self.nextScrollTime: + self.nextScrollTime = time.time() + self.SCROLLDELAY + return True + else: + return False + + def SetScrollTimer(self): + oneShot = True + self.scrollTimer.Start(1000*self.SCROLLDELAY/2, oneShot) + self.Bind(wx.EVT_TIMER, self.OnTimer) + + def OnTimer(self, event): + screenX, screenY = wx.GetMousePosition() + x, y = self.ScreenToClientXY(screenX, screenY) + self.MouseToRow(y) + self.MouseToCol(x) + self.SelectUpdate() + +##-------------------------- Mouse off screen functions + + def HandleAboveScreen(self, row): + self.SetScrollTimer() + if self.CanScroll(): + row = self.sy - 1 + row = max(0, row) + self.cy = row + + def HandleBelowScreen(self, row): + self.SetScrollTimer() + if self.CanScroll(): + row = self.sy + self.sh + row = min(row, self.LinesInFile() - 1) + self.cy = row + + def HandleLeftOfScreen(self, col): + self.SetScrollTimer() + if self.CanScroll(): + col = self.sx - 1 + col = max(0,col) + self.cx = col + + def HandleRightOfScreen(self, col): + self.SetScrollTimer() + if self.CanScroll(): + col = self.sx + self.sw + col = min(col, self.CurrentLineLength()) + self.cx = col + +##------------------------ mousing functions + + def MouseToRow(self, mouseY): + row = self.sy + (mouseY/ self.fh) + if self.AboveScreen(row): + self.HandleAboveScreen(row) + elif self.BelowScreen(row): + self.HandleBelowScreen(row) + else: + self.cy = min(row, self.LinesInFile() - 1) + + def MouseToCol(self, mouseX): + col = self.sx + (mouseX / self.fw) + if self.LeftOfScreen(col): + self.HandleLeftOfScreen(col) + elif self.RightOfScreen(col): + self.HandleRightOfScreen(col) + else: + self.cx = min(col, self.CurrentLineLength()) + + def MouseToCursor(self, event): + self.MouseToRow(event.GetY()) + self.MouseToCol(event.GetX()) + + def OnMotion(self, event): + if event.LeftIsDown() and self.HasCapture(): + self.Selecting = True + self.MouseToCursor(event) + self.SelectUpdate() + + def OnLeftDown(self, event): + self.MouseToCursor(event) + self.SelectBegin = (self.cy, self.cx) + self.SelectEnd = None + self.UpdateView() + self.CaptureMouse() + self.SetFocus() + + def OnLeftUp(self, event): + if not self.HasCapture(): + return + + if self.SelectEnd is None: + self.OnClick() + else: + self.Selecting = False + self.SelectNotify(False, self.SelectBegin, self.SelectEnd) + + self.ReleaseMouse() + self.scrollTimer.Stop() + + +#------------------------- Scrolling + + def HorizScroll(self, event, eventType): + maxLineLen = self.CalcMaxLineLen() + + if eventType == wx.wxEVT_SCROLLWIN_LINEUP: + self.sx -= 1 + elif eventType == wx.wxEVT_SCROLLWIN_LINEDOWN: + self.sx += 1 + elif eventType == wx.wxEVT_SCROLLWIN_PAGEUP: + self.sx -= self.sw + elif eventType == wx.wxEVT_SCROLLWIN_PAGEDOWN: + self.sx += self.sw + elif eventType == wx.wxEVT_SCROLLWIN_TOP: + self.sx = self.cx = 0 + elif eventType == wx.wxEVT_SCROLLWIN_BOTTOM: + self.sx = maxLineLen - self.sw + self.cx = maxLineLen + else: + self.sx = event.GetPosition() + + self.HorizBoundaries() + + def VertScroll(self, event, eventType): + if eventType == wx.wxEVT_SCROLLWIN_LINEUP: + self.sy -= 1 + elif eventType == wx.wxEVT_SCROLLWIN_LINEDOWN: + self.sy += 1 + elif eventType == wx.wxEVT_SCROLLWIN_PAGEUP: + self.sy -= self.sh + elif eventType == wx.wxEVT_SCROLLWIN_PAGEDOWN: + self.sy += self.sh + elif eventType == wx.wxEVT_SCROLLWIN_TOP: + self.sy = self.cy = 0 + elif eventType == wx.wxEVT_SCROLLWIN_BOTTOM: + self.sy = self.LinesInFile() - self.sh + self.cy = self.LinesInFile() + else: + self.sy = event.GetPosition() + + self.VertBoundaries() + + def OnScroll(self, event): + dir = event.GetOrientation() + eventType = event.GetEventType() + if dir == wx.HORIZONTAL: + self.HorizScroll(event, eventType) + else: + self.VertScroll(event, eventType) + self.UpdateView() + + + def AdjustScrollbars(self): + if self: + for i in range(2): + self.SetCharDimensions() + self.scroller.SetScrollbars( + self.fw, self.fh, + self.CalcMaxLineLen()+3, max(self.LinesInFile()+1, self.sh), + self.sx, self.sy) + +#------------ backspace, delete, return + + def BreakLine(self, event): + if self.IsLine(self.cy): + t = self.lines[self.cy] + self.lines = self.lines[:self.cy] + [t[:self.cx],t[self.cx:]] + self.lines[self.cy+1:] + self.cVert(1) + self.cx = 0 + self.TouchBuffer() + + def InsertChar(self,char): + if self.IsLine(self.cy): + t = self.lines[self.cy] + t = t[:self.cx] + char + t[self.cx:] + self.SetTextLine(self.cy, t) + self.cHoriz(1) + self.TouchBuffer() + + def JoinLines(self): + t1 = self.lines[self.cy] + t2 = self.lines[self.cy+1] + self.cx = len(t1) + self.lines = self.lines[:self.cy] + [t1 + t2] + self.lines[self.cy+2:] + self.TouchBuffer() + + + def DeleteChar(self,x,y,oldtext): + newtext = oldtext[:x] + oldtext[x+1:] + self.SetTextLine(y, newtext) + self.TouchBuffer() + + + def BackSpace(self, event): + t = self.GetTextLine(self.cy) + if self.cx>0: + self.DeleteChar(self.cx-1,self.cy,t) + self.cHoriz(-1) + self.TouchBuffer() + elif self.cx == 0: + if self.cy > 0: + self.cy -= 1 + self.JoinLines() + self.TouchBuffer() + else: + wx.Bell() + + def Delete(self, event): + t = self.GetTextLine(self.cy) + if self.cx31) and (key<256): + self.InsertChar(chr(key)) + else: + wx.Bell() + return + self.UpdateView() + self.AdjustScrollbars() + + def OnChar(self, event): + key = event.GetKeyCode() + filters = [self.AltKey, + self.MoveSpecialControlKey, + self.ControlKey, + self.SpecialControlKey, + self.MoveSpecialKey, + self.ShiftKey, + self.NormalChar] + for filter in filters: + if filter(event,key): + break + return 0 + +#----------------------- Eliminate memory leaks + + def OnDestroy(self, event): + self.mdc = None + self.odc = None + self.bgColor = None + self.fgColor = None + self.font = None + self.selectColor = None + self.scrollTimer = None + self.eofMarker = None + +#-------------------- Abstract methods for subclasses + + def OnClick(self): + pass + + def SelectNotify(self, Selecting, SelectionBegin, SelectionEnd): + pass + diff --git a/wx/lib/editor/images.py b/wx/lib/editor/images.py new file mode 100644 index 00000000..86259533 --- /dev/null +++ b/wx/lib/editor/images.py @@ -0,0 +1,15 @@ + +# images converted with wxPython's img2py.py tool + +# 12/14/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o 2.5 compatability update. +# + +from wx.lib.embeddedimage import PyEmbeddedImage + +EofImage = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAYAAADEUlfTAAAABHNCSVQICAgIfAhkiAAAADpJ" + "REFUeJxdjsENADAIAsUJbv8p3aB90WD5KJwJCihrZg4g+06Q88EM0quqFkh1dqQAtZcfrIcc" + "5OEFYDIVnsU0yrQAAAAASUVORK5CYII=") + diff --git a/wx/lib/editor/selection.py b/wx/lib/editor/selection.py new file mode 100644 index 00000000..df61cdc5 --- /dev/null +++ b/wx/lib/editor/selection.py @@ -0,0 +1,44 @@ +# 12/14/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o 2.5 compatability update. +# + +def RestOfLine(sx, width, data, bool): + if len(data) == 0 and sx == 0: + return [('', bool)] + if sx >= len(data): + return [] + return [(data[sx:sx+width], bool)] + +def Selection(SelectBegin,SelectEnd, sx, width, line, data): + if SelectEnd is None or SelectBegin is None: + return RestOfLine(sx, width, data, False) + (bRow, bCol) = SelectBegin + (eRow, eCol) = SelectEnd + if (eRow < bRow): + (bRow, bCol) = SelectEnd + (eRow, eCol) = SelectBegin + if (line < bRow or eRow < line): + return RestOfLine(sx, width, data, False) + if (bRow < line and line < eRow): + return RestOfLine(sx, width, data, True) + if (bRow == eRow) and (eCol < bCol): + (bCol, eCol) = (eCol, bCol) + # selection either starts or ends on this line + end = min(sx+width, len(data)) + if (bRow < line): + bCol = 0 + if (line < eRow): + eCol = end + pieces = [] + if (sx < bCol): + if bCol <= end: + pieces += [(data[sx:bCol], False)] + else: + return [(data[sx:end], False)] + pieces += [(data[max(bCol,sx):min(eCol,end)], True)] + if (eCol < end): + pieces += [(data[eCol:end], False)] + return pieces + + diff --git a/wx/lib/embeddedimage.py b/wx/lib/embeddedimage.py new file mode 100644 index 00000000..06b968a8 --- /dev/null +++ b/wx/lib/embeddedimage.py @@ -0,0 +1,75 @@ +#---------------------------------------------------------------------- +# Name: wx.lib.embeddedimage +# Purpose: Defines a class used for embedding PNG images in Python +# code. The primary method of using this module is via +# the code generator in wx.tools.img2py. +# +# Author: Anthony Tuininga +# +# Created: 26-Nov-2007 +# RCS-ID: $Id$ +# Copyright: (c) 2007 by Anthony Tuininga +# Licence: wxWindows license +#---------------------------------------------------------------------- + +import base64 +import cStringIO +import wx + +try: + b64decode = base64.b64decode +except AttributeError: + b64decode = base64.decodestring + + +class PyEmbeddedImage(object): + """ + PyEmbeddedImage is primarily intended to be used by code generated + by img2py as a means of embedding image data in a python module so + the image can be used at runtime without needing to access the + image from an image file. This makes distributing icons and such + that an application uses simpler since tools like py2exe will + automatically bundle modules that are imported, and the + application doesn't have to worry about how to locate the image + files on the user's filesystem. + + The class can also be used for image data that may be acquired + from some other source at runtime, such as over the network or + from a database. In this case pass False for isBase64 (unless the + data actually is base64 encoded.) Any image type that + wx.ImageFromStream can handle should be okay. + """ + + def __init__(self, data, isBase64=True): + self.data = data + self.isBase64 = isBase64 + + def GetBitmap(self): + return wx.BitmapFromImage(self.GetImage()) + + def GetData(self): + data = self.data + if self.isBase64: + data = b64decode(self.data) + return data + + def GetIcon(self): + icon = wx.EmptyIcon() + icon.CopyFromBitmap(self.GetBitmap()) + return icon + + def GetImage(self): + stream = cStringIO.StringIO(self.GetData()) + return wx.ImageFromStream(stream) + + # added for backwards compatibility + getBitmap = GetBitmap + getData = GetData + getIcon = GetIcon + getImage = GetImage + + # define properties, for convenience + Bitmap = property(GetBitmap) + Icon = property(GetIcon) + Image = property(GetImage) + diff --git a/wx/lib/eventStack.py b/wx/lib/eventStack.py new file mode 100644 index 00000000..d057414c --- /dev/null +++ b/wx/lib/eventStack.py @@ -0,0 +1,136 @@ +#---------------------------------------------------------------------- +# Name: wx.lib.eventStack +# Purpose: These mixins implement a push and pop menu/UI update event +# handler system at the wx.App level. This is useful for resolving +# cases where multiple views may want to respond to an event +# (say, wx.ID_COPY) and where you also want a "default" handler +# for the event (and UI update status) when there is no active +# view which wishes to handle the event. +# +# Author: Kevin Ollivier +# +# Created: -Mar- +# Copyright: (c) Kevin Ollivier +# Licence: wxWindows license +#---------------------------------------------------------------------- + +import sys, os +import wx + +class AppEventManager: + ui_events = [ + wx.ID_NEW, wx.ID_OPEN, wx.ID_CLOSE_ALL, wx.ID_CLOSE, + wx.ID_REVERT, wx.ID_SAVE, wx.ID_SAVEAS, wx.ID_UNDO, + wx.ID_REDO, wx.ID_PRINT, wx.ID_PRINT_SETUP, wx.ID_PREVIEW, + wx.ID_EXIT + ] + + def __init__(self): + pass + + def RegisterEvents(self): + app = wx.GetApp() + #app.AddHandlerForID(wx.ID_EXIT, self.OnExit) + #app.AddHandlerForID(wx.ID_ABOUT, self.OnAbout) + + for eventID in self.ui_events: + app.AddHandlerForID(eventID, self.ProcessEvent) + app.AddUIHandlerForID(eventID, self.ProcessUpdateUIEvent) + +class AppEventHandlerMixin: + """ + The purpose of the AppEventHandlerMixin is to provide a centralized + location to manage menu and toolbar events. In an IDE which may have + any number of file editors and services open that may want to respond + to certain menu and toolbar events (e.g. copy, paste, select all), + we need this to efficiently make sure that the right handler is handling + the event. + + To work with this system, views must call:: + + AddHandlerForID(ID, handlerFunc) + + + or:: + + AddUIHandlerForID(ID, handlerFunc) + + + in their EVT_SET_FOCUS handler, and call Remove(UI)HandlerForID(ID) in their + EVT_KILL_FOCUS handler. + """ + + def __init__(self): + self.handlers = {} + self.uihandlers = {} + + # When a view changes the handler, move the old one here. + # Then "pop" the handler when the view loses the focus + self.pushed_handlers = {} + self.pushed_uihandlers = {} + + def AddHandlerForIDs(self, eventID_list, handlerFunc): + for eventID in eventID_list: + self.AddHandlerForID(eventID, handlerFunc) + + def AddHandlerForID(self, eventID, handlerFunc): + self.Bind(wx.EVT_MENU, self.HandleEvent, id=eventID) + + if eventID in self.handlers: + self.pushed_handlers[eventID] = self.handlers[eventID] + + self.handlers[eventID] = handlerFunc + + def AddUIHandlerForID(self, eventID, handlerFunc): + self.Bind(wx.EVT_UPDATE_UI, self.HandleUpdateUIEvent, id=eventID) + + if eventID in self.uihandlers: + self.pushed_uihandlers[eventID] = self.uihandlers[eventID] + + self.uihandlers[eventID] = handlerFunc + + def RemoveHandlerForIDs(self, eventID_list): + for eventID in eventID_list: + self.RemoveHandlerForID(eventID) + + def RemoveHandlerForID(self, eventID): + self.Unbind(wx.EVT_MENU, id=eventID) + self.handlers[eventID] = None + + if eventID in self.pushed_handlers: + self.handlers[eventID] = self.pushed_handlers[eventID] + + def RemoveUIHandlerForID(self, eventID): + self.Unbind(wx.EVT_UPDATE_UI, id=eventID) + self.uihandlers[eventID] = None + + if eventID in self.pushed_uihandlers: + self.uihandlers[eventID] = self.pushed_uihandlers[eventID] + + def HandleEvent(self, event): + e_id = event.GetId() + if e_id in self.handlers: + handler = self.handlers[e_id] + try: + if handler: + return handler(event) + except wx.PyDeadObjectError: + self.RemoveHandlerForID(e_id) + else: + event.Skip() + + return False + + def HandleUpdateUIEvent(self, event): + e_id = event.GetId() + if e_id in self.uihandlers: + handler = self.uihandlers[e_id] + try: + if handler: + return handler(event) + except wx.PyDeadObjectError: + self.RemoveUIHandlerForID(e_id) + else: + event.Skip() + + return False diff --git a/wx/lib/eventwatcher.py b/wx/lib/eventwatcher.py new file mode 100644 index 00000000..9b61054c --- /dev/null +++ b/wx/lib/eventwatcher.py @@ -0,0 +1,458 @@ +#---------------------------------------------------------------------------- +# Name: wx.lib.eventwatcher +# Purpose: A widget that allows some or all events for a particular widget +# to be captured and displayed. +# +# Author: Robin Dunn +# +# Created: 21-Jan-2009 +# RCS-ID: $Id: $ +# Copyright: (c) 2009 by Total Control Software +# Licence: wxWindows license +#---------------------------------------------------------------------------- + +""" +A widget and supporting classes for watching the events sent to some other widget. +""" + +import wx +from wx.lib.mixins.listctrl import CheckListCtrlMixin + +#---------------------------------------------------------------------------- +# Helpers for building the data structures used for tracking the +# various event binders that are available + +_eventBinders = None +_eventIdMap = None + +def _buildModuleEventMap(module): + count = 0 + for name in dir(module): + if name.startswith('EVT_'): + item = getattr(module, name) + if isinstance(item, wx.PyEventBinder) and \ + len(item.evtType) == 1 and \ + item not in _eventBinders: + _eventBinders.append(item) + _eventIdMap[item.typeId] = name + count += 1 + return count + + +def buildWxEventMap(): + """ + Add the event binders from the main wx namespace. This is called + automatically from the EventWatcher. + """ + global _eventBinders + global _eventIdMap + if _eventBinders is None: + _eventBinders = list() + _eventIdMap = dict() + _buildModuleEventMap(wx) + + +def addModuleEvents(module): + """ + Adds all the items in module that start with ``EVT_`` to the event + data structures used by the EventWatcher. + """ + if _eventBinders is None: + buildWxEventMap() + return _buildModuleEventMap(module) + + +# Events that should not be watched by default +_noWatchList = [ + wx.EVT_PAINT, + wx.EVT_NC_PAINT, + wx.EVT_ERASE_BACKGROUND, + wx.EVT_IDLE, + wx.EVT_UPDATE_UI, + wx.EVT_UPDATE_UI_RANGE, + ] +OTHER_WIDTH = 250 + + +def _makeSourceString(wdgt): + if wdgt is None: + return "None" + else: + name = '' + id = 0 + if hasattr(wdgt, 'GetName'): + name = wdgt.GetName() + if hasattr(wdgt, 'GetId'): + id = wdgt.GetId() + return '%s "%s" (%d)' % (wdgt.__class__.__name__, name, id) + +def _makeAttribString(evt): + "Find all the getters" + attribs = "" + for name in dir(evt): + if (name.startswith('Get') or name.startswith('Is')) and \ + name not in [ 'GetEventObject', + 'GetEventType', + 'GetId', + 'GetSkipped', + 'GetTimestamp', + 'GetClientData', + 'GetClientObject', + ]: + try: + value = getattr(evt, name)() + attribs += "%s : %s\n" % (name, value) + except: + pass + + return attribs.rstrip() + +#---------------------------------------------------------------------------- + +class EventLog(wx.ListCtrl): + """ + A virtual listctrl that displays information about the watched events. + """ + def __init__(self, *args, **kw): + kw['style'] = wx.LC_REPORT|wx.LC_SINGLE_SEL|wx.LC_VIRTUAL|wx.LC_HRULES|wx.LC_VRULES + wx.ListCtrl.__init__(self, *args, **kw) + self.clear() + + if 'wxMac' in wx.PlatformInfo: + self.SetWindowVariant(wx.WINDOW_VARIANT_SMALL) + + self.InsertColumn(0, "#", format=wx.LIST_FORMAT_RIGHT, width=50) + self.InsertColumn(1, "Event", width=200) + self.InsertColumn(2, "Source", width=200) + + self.SetMinSize((450+wx.SystemSettings.GetMetric(wx.SYS_VSCROLL_X), 450)) + self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.onItemSelected) + self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.onItemActivated) + self.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self.onItemActivated) + + def append(self, evt): + evtName = _eventIdMap.get(evt.GetEventType(), None) + if evtName is None: + evtName = 'Unknown: %d' % evt.GetEventType() + source = _makeSourceString(evt.GetEventObject()) + attribs = _makeAttribString(evt) + + lastIsSelected = self.currItem == len(self.data)-1 + self.data.append( (evtName, source, attribs) ) + + count = len(self.data) + self.SetItemCount(count) + self.RefreshItem(count-1) + if lastIsSelected: + self.Select(count-1) + self.EnsureVisible(count-1) + + def clear(self): + self.data = [] + self.SetItemCount(0) + self.currItem = -1 + self.Refresh() + + def OnGetItemText(self, item, col): + if col == 0: + val = str(item+1) + else: + val = self.data[item][col-1] + return val + + def OnGetItemAttr(self, item): return None + def OnGetItemImage(self, item): return -1 + + def onItemSelected(self, evt): + self.currItem = evt.GetIndex() + + def onItemActivated(self, evt): + idx = evt.GetIndex() + text = self.data[idx][2] + wx.CallAfter(wx.TipWindow, self, text, OTHER_WIDTH) + +#---------------------------------------------------------------------------- + + +class EventChooser(wx.Panel): + """ + Panel with CheckListCtrl for selecting which events will be watched. + """ + + class EventChooserLC(wx.ListCtrl, CheckListCtrlMixin): + def __init__(self, parent): + wx.ListCtrl.__init__(self, parent, + style=wx.LC_REPORT|wx.LC_SINGLE_SEL|wx.LC_HRULES|wx.LC_VRULES) + CheckListCtrlMixin.__init__(self) + if 'wxMac' in wx.PlatformInfo: + self.SetWindowVariant(wx.WINDOW_VARIANT_SMALL) + + # this is called by the base class when an item is checked/unchecked + def OnCheckItem(self, index, flag): + self.Parent.OnCheckItem(index, flag) + + + def __init__(self, *args, **kw): + wx.Panel.__init__(self, *args, **kw) + self.updateCallback = lambda: None + self.doUpdate = True + self._event_name_filter = wx.SearchCtrl(self) + self._event_name_filter.ShowCancelButton(True) + self._event_name_filter.Bind(wx.EVT_TEXT, lambda evt: self.setWatchList(self.watchList)) + self._event_name_filter.Bind(wx.EVT_SEARCHCTRL_CANCEL_BTN, self._ClearEventFilter) + self.lc = EventChooser.EventChooserLC(self) + btn1 = wx.Button(self, -1, "All") + btn2 = wx.Button(self, -1, "None") + btn1.SetToolTipString("Check all events") + btn2.SetToolTipString("Uncheck all events") + + self.Bind(wx.EVT_BUTTON, self.onCheckAll, btn1) + self.Bind(wx.EVT_BUTTON, self.onUncheckAll, btn2) + + self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.onItemActivated, self.lc) + self.lc.InsertColumn(0, "Binder", width=OTHER_WIDTH) + + btnSizer = wx.BoxSizer(wx.HORIZONTAL) + btnSizer.Add(btn1, 0, wx.ALL, 5) + btnSizer.Add(btn2, 0, wx.ALL, 5) + + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(self._event_name_filter, 0, wx.EXPAND|wx.ALL, 5) + sizer.Add(self.lc, 1, wx.EXPAND) + sizer.Add(btnSizer) + self.SetSizer(sizer) + + + def setUpdateCallback(self, func): + self.updateCallback = func + + def setWatchList(self, watchList): + self.doUpdate = False + searched = self._event_name_filter.GetValue().lower() + self.watchList = watchList + self.lc.DeleteAllItems() + count = 0 + for index, (item, flag) in enumerate(watchList): + typeId = item.typeId + text = _eventIdMap.get(typeId, "[Unknown]") + if text.lower().find(searched) == -1: + continue + self.lc.InsertStringItem(count, text) + self.lc.SetItemData(count, index) + if flag: + self.lc.CheckItem(count) + count += 1 + self.lc.SortItems(self.sortCompare) + self.doUpdate = True + self.updateCallback() + + + def OnCheckItem(self, index, flag): + index = self.lc.GetItemData(index) + item, f = self.watchList[index] + self.watchList[index] = (item, flag) + if self.doUpdate: + self.updateCallback() + + + def onItemActivated(self, evt): + self.lc.ToggleItem(evt.m_itemIndex) + + def onCheckAll(self, evt): + self.doUpdate = False + for idx in range(self.lc.GetItemCount()): + self.lc.CheckItem(idx, True) + self.doUpdate = True + self.updateCallback() + + def onUncheckAll(self, evt): + self.doUpdate = False + for idx in range(self.lc.GetItemCount()): + self.lc.CheckItem(idx, False) + self.doUpdate = True + self.updateCallback() + + + def sortCompare(self, data1, data2): + item1 = self.watchList[data1][0] + item2 = self.watchList[data2][0] + text1 = _eventIdMap.get(item1.typeId) + text2 = _eventIdMap.get(item2.typeId) + return cmp(text1, text2) + + def _ClearEventFilter(self, evt): + self._event_name_filter.SetValue("") + +#---------------------------------------------------------------------------- + +class EventWatcher(wx.Frame): + """ + A frame that will catch and display al events sent to some widget. + """ + def __init__(self, *args, **kw): + wx.Frame.__init__(self, *args, **kw) + self.SetTitle("EventWatcher") + self.SetExtraStyle(wx.WS_EX_BLOCK_EVENTS) + self._watchedWidget = None + + buildWxEventMap() + self.buildWatchList(_noWatchList) + + # Make the widgets + self.splitter = wx.SplitterWindow(self) + panel = wx.Panel(self.splitter) + self.splitter.Initialize(panel) + self.log = EventLog(panel) + clearBtn = wx.Button(panel, -1, "Clear") + addBtn = wx.Button(panel, -1, "Add Module") + watchBtn = wx.ToggleButton(panel, -1, "Watch") + watchBtn.SetValue(True) + selectBtn = wx.ToggleButton(panel, -1, ">>>") + self.selectBtn = selectBtn + + clearBtn.SetToolTipString("Clear the event log") + addBtn.SetToolTipString("Add the event binders in an additional package or module to the watcher") + watchBtn.SetToolTipString("Toggle the watching of events") + selectBtn.SetToolTipString("Show/hide the list of events to be logged") + + # Do the layout + sizer = wx.BoxSizer(wx.VERTICAL) + btnSizer = wx.BoxSizer(wx.HORIZONTAL) + btnSizer.Add(clearBtn, 0, wx.RIGHT, 5) + btnSizer.Add(addBtn, 0, wx.RIGHT, 5) + btnSizer.Add((1,1), 1) + btnSizer.Add(watchBtn, 0, wx.RIGHT, 5) + btnSizer.Add((1,1), 1) + btnSizer.Add(selectBtn, 0, wx.RIGHT, 5) + sizer.Add(self.log, 1, wx.EXPAND) + sizer.Add(btnSizer, 0, wx.EXPAND|wx.ALL, 5) + panel.SetSizer(sizer) + self.Sizer = wx.BoxSizer() + self.Sizer.Add(self.splitter, 1, wx.EXPAND) + self.Fit() + + # Bind events + self.Bind(wx.EVT_CLOSE, self.onCloseWindow) + self.Bind(wx.EVT_BUTTON, self.onClear, clearBtn) + self.Bind(wx.EVT_BUTTON, self.onAddModule, addBtn) + self.Bind(wx.EVT_TOGGLEBUTTON, self.onToggleWatch, watchBtn) + self.Bind(wx.EVT_TOGGLEBUTTON, self.onToggleSelectEvents, selectBtn) + + + + def watch(self, widget): + assert self._watchedWidget is None, "Can only watch one widget at a time" + self.SetTitle("EventWatcher for " + _makeSourceString(widget)) + for evtBinder, flag in self._watchedEvents: + if flag: + widget.Bind(evtBinder, self.onWatchedEvent) + self._watchedWidget = widget + + + def unwatch(self): + self.SetTitle("EventWatcher") + if self._watchedWidget: + for evtBinder, flag in self._watchedEvents: + self._watchedWidget.Unbind(evtBinder, handler=self.onWatchedEvent) + self._watchedWidget = None + + + def updateBindings(self): + widget = self._watchedWidget + self.unwatch() + self.watch(widget) + + + def onWatchedEvent(self, evt): + if self: + self.log.append(evt) + evt.Skip() + + def buildWatchList(self, exclusions): + # This is a list of (PyEventBinder, flag) tuples where the flag indicates + # whether to bind that event or not. By default all execpt those in + # the _noWatchList wil be set to be watched. + self._watchedEvents = list() + for item in _eventBinders: + self._watchedEvents.append( (item, item not in exclusions) ) + + def onCloseWindow(self, evt): + self.unwatch() + evt.Skip() + + def onClear(self, evt): + self.log.clear() + + def onAddModule(self, evt): + try: + dlg = wx.TextEntryDialog( + self, + "Enter the package or module name to be scanned for \"EVT_\" event binders.", + "Add Module") + if dlg.ShowModal() == wx.ID_OK: + modname = dlg.GetValue() + try: + # Passing a non-empty fromlist will cause __import__ to + # return the imported submodule if a dotted name is passed. + module = __import__(modname, fromlist=[0]) + except ImportError: + wx.MessageBox("Unable to import \"%s\"" % modname, + "Error") + return + count = addModuleEvents(module) + wx.MessageBox("%d new event binders found" % count, + "Success") + + # Now unwatch and re-watch so we can get the new events bound + self.updateBindings() + finally: + dlg.Destroy() + + + def onToggleWatch(self, evt): + if evt.Checked(): + self.watch(self._unwatchedWidget) + self._unwatchedWidget = None + else: + self._unwatchedWidget = self._watchedWidget + self.unwatch() + + + def onToggleSelectEvents(self, evt): + if evt.Checked(): + self.selectBtn.SetLabel("<<<") + self._selectList = EventChooser(self.splitter) + self._selectList.setUpdateCallback(self.updateBindings) + self._selectList.setWatchList(self._watchedEvents) + + self.SetSize(self.GetSize() + (OTHER_WIDTH,0)) + self.splitter.SplitVertically(self.splitter.GetWindow1(), + self._selectList, + -OTHER_WIDTH) + else: + self.selectBtn.SetLabel(">>>") + sashPos = self.splitter.GetSashPosition() + self.splitter.Unsplit() + self._selectList.Destroy() + cs = self.GetClientSize() + self.SetClientSize((sashPos, cs.height)) + +#---------------------------------------------------------------------------- + +if __name__ == '__main__': + app = wx.App(redirect=False) + frm = wx.Frame(None, title="Test Frame") + pnl = wx.Panel(frm) + txt = wx.TextCtrl(pnl, -1, "text", pos=(20,20)) + btn = wx.Button(pnl, -1, "button", pos=(20,50)) + frm.Show() + + ewf=EventWatcher(frm) + ewf.watch(frm) + ewf.Show() + + #import wx.lib.inspection + #wx.lib.inspection.InspectionTool().Show() + + app.MainLoop() + diff --git a/wx/lib/evtmgr.py b/wx/lib/evtmgr.py new file mode 100644 index 00000000..439ccc76 --- /dev/null +++ b/wx/lib/evtmgr.py @@ -0,0 +1,534 @@ +#--------------------------------------------------------------------------- +# Name: wxPython.lib.evtmgr +# Purpose: An easier, more "Pythonic" and more OO method of registering +# handlers for wxWindows events using the Publish/Subscribe +# pattern. +# +# Author: Robb Shecter and Robin Dunn +# +# Created: 12-December-2002 +# RCS-ID: $Id$ +# Copyright: (c) 2003 by db-X Corporation +# Licence: wxWindows license +#--------------------------------------------------------------------------- +# 12/02/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o Updated for 2.5 compatability. +# + +""" +A module that allows multiple handlers to respond to single wxWidgets +events. This allows true NxN Observer/Observable connections: One +event can be received by multiple handlers, and one handler can +receive multiple events. + +There are two ways to register event handlers. The first way is +similar to standard wxPython handler registration:: + + from wx.lib.evtmgr import eventManager + eventManager.Register(handleEvents, EVT_BUTTON, win=frame, id=101) + +There's also a new object-oriented way to register for events. This +invocation is equivalent to the one above, but does not require the +programmer to declare or track control ids or parent containers:: + + eventManager.Register(handleEvents, EVT_BUTTON, myButton) + +This module is Python 2.1+ compatible. + +""" +import wx +import pubsub # publish / subscribe library + +#--------------------------------------------------------------------------- + + +class EventManager: + """ + This is the main class in the module, and is the only class that + the application programmer needs to use. There is a pre-created + instance of this class called 'eventManager'. It should not be + necessary to create other instances. + """ + def __init__(self): + self.eventAdapterDict = {} + self.messageAdapterDict = {} + self.windowTopicLookup = {} + self.listenerTopicLookup = {} + self.__publisher = pubsub.Publisher() + self.EMPTY_LIST = [] + + + def Register(self, listener, event, source=None, win=None, id=None): + """ + Registers a listener function (or any callable object) to + receive events of type event coming from the source window. + For example:: + + eventManager.Register(self.OnButton, EVT_BUTTON, theButton) + + Alternatively, the specific window where the event is + delivered, and/or the ID of the event source can be specified. + For example:: + + eventManager.Register(self.OnButton, EVT_BUTTON, win=self, id=ID_BUTTON) + + or:: + + eventManager.Register(self.OnButton, EVT_BUTTON, theButton, self) + + """ + + # 1. Check if the 'event' is actually one of the multi- + # event macros. + if _macroInfo.isMultiEvent(event): + raise 'Cannot register the macro, '+`event`+'. Register instead the individual events.' + + # Support a more OO API. This allows the GUI widget itself to + # be specified, and the id to be retrieved from the system, + # instead of kept track of explicitly by the programmer. + # (Being used to doing GUI work with Java, this seems to me to be + # the natural way of doing things.) + if source is not None: + id = source.GetId() + + if win is None: + # Some widgets do not function as their own windows. + win = self._determineWindow(source) + + topic = (event, win, id) + + # Create an adapter from the PS system back to wxEvents, and + # possibly one from wxEvents: + if not self.__haveMessageAdapter(listener, topic): + messageAdapter = MessageAdapter(eventHandler=listener, topicPattern=topic) + try: + self.messageAdapterDict[topic][listener] = messageAdapter + except KeyError: + self.messageAdapterDict[topic] = {} + self.messageAdapterDict[topic][listener] = messageAdapter + + if not self.eventAdapterDict.has_key(topic): + self.eventAdapterDict[topic] = EventAdapter(event, win, id) + else: + # Throwing away a duplicate request + pass + + # For time efficiency when deregistering by window: + try: + self.windowTopicLookup[win].append(topic) + except KeyError: + self.windowTopicLookup[win] = [] + self.windowTopicLookup[win].append(topic) + + # For time efficiency when deregistering by listener: + try: + self.listenerTopicLookup[listener].append(topic) + except KeyError: + self.listenerTopicLookup[listener] = [] + self.listenerTopicLookup[listener].append(topic) + + # See if the source understands the listeningFor protocol. + # This is a bit of a test I'm working on - it allows classes + # to know when their events are being listened to. I use + # it to enable chaining events from contained windows only + # when needed. + if source is not None: + try: + # Let the source know that we're listening for this + # event. + source.listeningFor(event) + except AttributeError: + pass + + # Some aliases for Register, just for kicks + Bind = Register + Subscribe = Register + + + def DeregisterWindow(self, win): + """ + Deregister all events coming from the given window. + """ + win = self._determineWindow(win) + topics = self.__getTopics(win) + + if topics: + for aTopic in topics: + self.__deregisterTopic(aTopic) + + del self.windowTopicLookup[win] + + + def DeregisterListener(self, listener): + """ + Deregister all event notifications for the given listener. + """ + try: + topicList = self.listenerTopicLookup[listener] + except KeyError: + return + + for topic in topicList: + topicDict = self.messageAdapterDict[topic] + + if topicDict.has_key(listener): + topicDict[listener].Destroy() + del topicDict[listener] + + if len(topicDict) == 0: + self.eventAdapterDict[topic].Destroy() + del self.eventAdapterDict[topic] + del self.messageAdapterDict[topic] + + del self.listenerTopicLookup[listener] + + + def GetStats(self): + """ + Return a dictionary with data about my state. + """ + stats = {} + stats['Adapters: Message'] = reduce(lambda x,y: x+y, [0] + map(len, self.messageAdapterDict.values())) + stats['Adapters: Event'] = len(self.eventAdapterDict) + stats['Topics: Total'] = len(self.__getTopics()) + stats['Topics: Dead'] = len(self.GetDeadTopics()) + return stats + + + def DeregisterDeadTopics(self): + """ + Deregister any entries relating to dead + wxPython objects. Not sure if this is an + important issue; 1) My app code always de-registers + listeners it doesn't need. 2) I don't think + that lingering references to these dead objects + is a problem. + """ + for topic in self.GetDeadTopics(): + self.__deregisterTopic(topic) + + + def GetDeadTopics(self): + """ + Return a list of topics relating to dead wxPython + objects. + """ + return filter(self.__isDeadTopic, self.__getTopics()) + + + def __winString(self, aWin): + """ + A string rep of a window for debugging + """ + try: + name = aWin.GetClassName() + i = id(aWin) + return '%s #%d' % (name, i) + except wx.PyDeadObjectError: + return '(dead wx.Object)' + + + def __topicString(self, aTopic): + """ + A string rep of a topic for debugging + """ + return '[%-26s %s]' % (aTopic[0].__name__, self.winString(aTopic[1])) + + + def __listenerString(self, aListener): + """ + A string rep of a listener for debugging + """ + try: + return aListener.im_class.__name__ + '.' + aListener.__name__ + except: + return 'Function ' + aListener.__name__ + + + def __deregisterTopic(self, aTopic): + try: + messageAdapterList = self.messageAdapterDict[aTopic].values() + except KeyError: + # This topic isn't valid. Probably because it was deleted + # by listener. + return + + for messageAdapter in messageAdapterList: + messageAdapter.Destroy() + + self.eventAdapterDict[aTopic].Destroy() + del self.messageAdapterDict[aTopic] + del self.eventAdapterDict[aTopic] + + + def __getTopics(self, win=None): + if win is None: + return self.messageAdapterDict.keys() + + if win is not None: + try: + return self.windowTopicLookup[win] + except KeyError: + return self.EMPTY_LIST + + + def __isDeadWxObject(self, anObject): + return isinstance(anObject, wx._core._wxPyDeadObject) + + + def __isDeadTopic(self, aTopic): + return self.__isDeadWxObject(aTopic[1]) + + + def __haveMessageAdapter(self, eventHandler, topicPattern): + """ + Return True if there's already a message adapter + with these specs. + """ + try: + return self.messageAdapterDict[topicPattern].has_key(eventHandler) + except KeyError: + return 0 + + + def _determineWindow(self, aComponent): + """ + Return the window that corresponds to this component. + A window is something that supports the Connect protocol. + Most things registered with the event manager are a window, + but there are apparently some exceptions. If more are + discovered, the implementation can be changed to a dictionary + lookup along the lines of class : function-to-get-window. + """ + if isinstance(aComponent, wx.MenuItem): + return aComponent.GetMenu() + else: + return aComponent + + + +#--------------------------------------------------------------------------- +# From here down is implementaion and support classes, although you may +# find some of them useful in other contexts. +#--------------------------------------------------------------------------- + + +class EventMacroInfo: + """ + A class that provides information about event macros. + """ + def __init__(self): + self.lookupTable = {} + + + def getEventTypes(self, eventMacro): + """ + Return the list of event types that the given + macro corresponds to. + """ + try: + return self.lookupTable[eventMacro] + except KeyError: + win = FakeWindow() + try: + eventMacro(win, None, None) + except (TypeError, AssertionError): + eventMacro(win, None) + self.lookupTable[eventMacro] = win.eventTypes + return win.eventTypes + + + def eventIsA(self, event, macroList): + """ + Return True if the event is one of the given + macros. + """ + eventType = event.GetEventType() + for macro in macroList: + if eventType in self.getEventTypes(macro): + return 1 + return 0 + + + def macroIsA(self, macro, macroList): + """ + Return True if the macro is in the macroList. + The added value of this method is that it takes + multi-events into account. The macroList parameter + will be coerced into a sequence if needed. + """ + if callable(macroList): + macroList = (macroList,) + testList = self.getEventTypes(macro) + eventList = [] + for m in macroList: + eventList.extend(self.getEventTypes(m)) + # Return True if every element in testList is in eventList + for element in testList: + if element not in eventList: + return 0 + return 1 + + + def isMultiEvent(self, macro): + """ + Return True if the given macro actually causes + multiple events to be registered. + """ + return len(self.getEventTypes(macro)) > 1 + + +#--------------------------------------------------------------------------- + +class FakeWindow: + """ + Used internally by the EventMacroInfo class. The FakeWindow is + the most important component of the macro-info utility: it + implements the Connect() protocol of wxWindow, but instead of + registering for events, it keeps track of what parameters were + passed to it. + """ + def __init__(self): + self.eventTypes = [] + + def Connect(self, id1, id2, eventType, handlerFunction): + self.eventTypes.append(eventType) + + +#--------------------------------------------------------------------------- + +class EventAdapter: + """ + A class that adapts incoming wxWindows events to + Publish/Subscribe messages. + + In other words, this is the object that's seen by the + wxWindows system. Only one of these registers for any + particular wxWindows event. It then relays it into the + PS system, which lets many listeners respond. + """ + def __init__(self, func, win, id): + """ + Instantiate a new adapter. Pre-compute my Publish/Subscribe + topic, which is constant, and register with wxWindows. + """ + self.publisher = pubsub.Publisher() + self.topic = ((func, win, id),) + self.id = id + self.win = win + self.eventType = _macroInfo.getEventTypes(func)[0] + + # Register myself with the wxWindows event system + try: + func(win, id, self.handleEvent) + self.callStyle = 3 + except (TypeError, AssertionError): + func(win, self.handleEvent) + self.callStyle = 2 + + + def disconnect(self): + if self.callStyle == 3: + return self.win.Disconnect(self.id, -1, self.eventType) + else: + return self.win.Disconnect(-1, -1, self.eventType) + + + def handleEvent(self, event): + """ + In response to a wxWindows event, send a PS message + """ + self.publisher.sendMessage(topic=self.topic, data=event) + + + def Destroy(self): + try: + if not self.disconnect(): + print 'disconnect failed' + except wx.PyDeadObjectError: + print 'disconnect failed: dead object' ##???? + + +#--------------------------------------------------------------------------- + +class MessageAdapter: + """ + A class that adapts incoming Publish/Subscribe messages + to wxWindows event calls. + + This class works opposite the EventAdapter, and + retrieves the information an EventAdapter has sent in a message. + Strictly speaking, this class is not required: Event listeners + could pull the original wxEvent object out of the PS Message + themselves. + + However, by pairing an instance of this class with each wxEvent + handler, the handlers can use the standard API: they receive an + event as a parameter. + """ + def __init__(self, eventHandler, topicPattern): + """ + Instantiate a new MessageAdapter that send wxEvents to the + given eventHandler. + """ + self.eventHandler = eventHandler + pubsub.Publisher().subscribe(listener=self.deliverEvent, topic=(topicPattern,)) + + def deliverEvent(self, message): + event = message.data # Extract the wxEvent + self.eventHandler(event) # Perform the call as wxWindows would + + def Destroy(self): + pubsub.Publisher().unsubscribe(listener=self.deliverEvent) + + +#--------------------------------------------------------------------------- +# Create globals + +_macroInfo = EventMacroInfo() + +# For now a singleton is not enforced. Should it be or can we trust +# the programmers? +eventManager = EventManager() + + +#--------------------------------------------------------------------------- +# simple test code + + +if __name__ == '__main__': + app = wx.PySimpleApp() + frame = wx.Frame(None, -1, 'Event Test', size=(300,300)) + button = wx.ToggleButton(frame, -1, 'Listen for Mouse Events') + sizer = wx.BoxSizer(wx.HORIZONTAL) + sizer.Add(button, 0, 0 | wx.ALL, 10) + frame.SetAutoLayout(1) + frame.SetSizer(sizer) + + # + # Demonstrate 1) register/deregister, 2) Multiple listeners receiving + # one event, and 3) Multiple events going to one listener. + # + + def printEvent(event): + print 'Name:',event.GetClassName(),'Timestamp',event.GetTimestamp() + + def enableFrameEvents(event): + # Turn the output of mouse events on and off + if event.IsChecked(): + print '\nEnabling mouse events...' + eventManager.Register(printEvent, wx.EVT_MOTION, frame) + eventManager.Register(printEvent, wx.EVT_LEFT_DOWN, frame) + else: + print '\nDisabling mouse events...' + eventManager.DeregisterWindow(frame) + + # Send togglebutton events to both the on/off code as well + # as the function that prints to stdout. + eventManager.Register(printEvent, wx.EVT_TOGGLEBUTTON, button) + eventManager.Register(enableFrameEvents, wx.EVT_TOGGLEBUTTON, button) + + frame.CenterOnScreen() + frame.Show(1) + app.MainLoop() diff --git a/wx/lib/expando.py b/wx/lib/expando.py new file mode 100644 index 00000000..72fcab04 --- /dev/null +++ b/wx/lib/expando.py @@ -0,0 +1,225 @@ +#--------------------------------------------------------------------------- +# Name: expando.py +# Purpose: A multi-line text control that expands and collapses as more +# or less lines are needed to display its content. +# +# Author: Robin Dunn +# +# Created: 18-Sept-2006 +# RCS-ID: $Id$ +# Copyright: (c) 2006 by Total Control Software +# Licence: wxWindows license +# +#--------------------------------------------------------------------------- +""" +This module contains the `ExpandoTextCtrl` which is a multi-line +text control that will expand its height on the fly to be able to show +all the lines of the content of the control. +""" + +import wx +import wx.lib.newevent + + +# This event class and binder object can be used to catch +# notifications that the ExpandoTextCtrl has resized itself and +# that layout adjustments may need to be made. +wxEVT_ETC_LAYOUT_NEEDED = wx.NewEventType() +EVT_ETC_LAYOUT_NEEDED = wx.PyEventBinder( wxEVT_ETC_LAYOUT_NEEDED, 1 ) + + +#--------------------------------------------------------------------------- + +class ExpandoTextCtrl(wx.TextCtrl): + """ + The ExpandoTextCtrl is a multi-line wx.TextCtrl that will + adjust its height on the fly as needed to accomodate the number of + lines needed to display the current content of the control. It is + assumed that the width of the control will be a fixed value and + that only the height will be adjusted automatically. If the + control is used in a sizer then the width should be set as part of + the initial or min size of the control. + + When the control resizes itself it will attempt to also make + necessary adjustments in the sizer hierarchy it is a member of (if + any) but if that is not suffiecient then the programmer can catch + the EVT_ETC_LAYOUT_NEEDED event in the container and make any + other layout adjustments that may be needed. + """ + _defaultHeight = -1 + _leading = 1 # TODO: find a way to calculate this, it may vary by platform + + def __init__(self, parent, id=-1, value="", + pos=wx.DefaultPosition, size=wx.DefaultSize, + style=0, validator=wx.DefaultValidator, name="expando"): + # find the default height of a single line control + self.defaultHeight = self._getDefaultHeight(parent) + # make sure we default to that height if none was given + w, h = size + if h == -1: + h = self.defaultHeight + # always use the multi-line style + style = style | wx.TE_MULTILINE | wx.TE_NO_VSCROLL | wx.TE_RICH2 + # init the base class + wx.TextCtrl.__init__(self, parent, id, value, pos, (w, h), + style, validator, name) + # save some basic metrics + self.extraHeight = self.defaultHeight - self.GetCharHeight() + self.numLines = 1 + self.maxHeight = -1 + if value: + wx.CallAfter(self._adjustCtrl) + + self.Bind(wx.EVT_TEXT, self.OnTextChanged) + self.Bind(wx.EVT_SIZE, self.OnSize) + + + def SetMaxHeight(self, h): + """ + Sets the max height that the control will expand to on its + own, and adjusts it down if needed. + """ + self.maxHeight = h + if h != -1 and self.GetSize().height > h: + self.SetSize((-1, h)) + + def GetMaxHeight(self): + """Sets the max height that the control will expand to on its own""" + return self.maxHeight + + + def SetFont(self, font): + wx.TextCtrl.SetFont(self, font) + self.numLines = -1 + self._adjustCtrl() + + def WriteText(self, text): + # work around a bug of a lack of a EVT_TEXT when calling + # WriteText on wxMac + wx.TextCtrl.WriteText(self, text) + self._adjustCtrl() + + def AppendText(self, text): + # Instead of using wx.TextCtrl.AppendText append and set the + # insertion point ourselves. This works around a bug on wxMSW + # where it scrolls the old text out of view, and since there + # is no scrollbar there is no way to get back to it. + self.SetValue(self.GetValue() + text) + self.SetInsertionPointEnd() + + + def OnTextChanged(self, evt): + # check if any adjustments are needed on every text update + self._adjustCtrl() + evt.Skip() + + + def OnSize(self, evt): + # The number of lines needed can change when the ctrl is resized too. + self._adjustCtrl() + evt.Skip() + + + def _adjustCtrl(self): + # if the current number of lines is different than before + # then recalculate the size needed and readjust + numLines = self.GetNumberOfLines() + if numLines != self.numLines: + self.numLines = numLines + charHeight = self.GetCharHeight() + height = numLines * (charHeight+self._leading) + self.extraHeight + if not (self.maxHeight != -1 and height > self.maxHeight): + # The size is changing... if the control is not in a + # sizer then we just want to change the size and + # that's it, the programmer will need to deal with + # potential layout issues. If it is being managed by + # a sizer then we'll change the min size setting and + # then try to do a layout. In either case we'll also + # send an event so the parent can handle any special + # layout issues that it wants to deal with. + if self.GetContainingSizer() is not None: + mw, mh = self.GetMinSize() + self.SetMinSize((mw, height)) + if self.GetParent().GetSizer() is not None: + self.GetParent().Layout() + else: + self.GetContainingSizer().Layout() + else: + self.SetSize((-1, height)) + # send notification that layout may be needed + evt = wx.PyCommandEvent(wxEVT_ETC_LAYOUT_NEEDED, self.GetId()) + evt.SetEventObject(self) + evt.height = height + evt.numLines = numLines + self.GetEventHandler().ProcessEvent(evt) + + + def _getDefaultHeight(self, parent): + # checked for cached value + if self.__class__._defaultHeight != -1: + return self.__class__._defaultHeight + # otherwise make a single line textctrl and find out its default height + tc = wx.TextCtrl(parent) + sz = tc.GetSize() + tc.Destroy() + self.__class__._defaultHeight = sz.height + return sz.height + + + if 'wxGTK' in wx.PlatformInfo or 'wxOSX-cocoa' in wx.PlatformInfo: + # GetNumberOfLines in some ports doesn't count wrapped lines, so we + # need to implement our own. + def GetNumberOfLines(self): + text = self.GetValue() + width = self.GetClientSize().width + dc = wx.ClientDC(self) + dc.SetFont(self.GetFont()) + count = 0 + for line in text.split('\n'): + count += 1 + w, h = dc.GetTextExtent(line) + if w > width - self._getExtra(): + # the width of the text is wider than the control, + # calc how many lines it will be wrapped to + count += self._wrapLine(line, dc, width) + + if not count: + count = 1 + return count + + def _getExtra(self): + if 'wxOSX-cocoa' in wx.PlatformInfo: + return wx.SystemSettings.GetMetric(wx.SYS_VSCROLL_X) + else: + return 0 + + def _wrapLine(self, line, dc, width): + # Estimate where the control will wrap the lines and + # return the count of extra lines needed. + pte = dc.GetPartialTextExtents(line) + width -= wx.SystemSettings.GetMetric(wx.SYS_VSCROLL_X) + if not pte or width < pte[0]: + return 1 + idx = 0 + start = 0 + count = 0 + spc = -1 + while idx < len(pte): + if line[idx] == ' ': + spc = idx + if pte[idx] - start > width: + # we've reached the max width, add a new line + count += 1 + # did we see a space? if so restart the count at that pos + if spc != -1: + idx = spc + 1 + spc = -1 + try: + start = pte[idx] + except IndexError: + start = pte[-1] + else: + idx += 1 + return count + +#--------------------------------------------------------------------------- diff --git a/wx/lib/fancytext.py b/wx/lib/fancytext.py new file mode 100644 index 00000000..df7772e9 --- /dev/null +++ b/wx/lib/fancytext.py @@ -0,0 +1,462 @@ +# 12/02/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o Updated for 2.5 compatability. +# + +""" +FancyText -- methods for rendering XML specified text + +This module exports four main methods:: + + def GetExtent(str, dc=None, enclose=True) + def GetFullExtent(str, dc=None, enclose=True) + def RenderToBitmap(str, background=None, enclose=True) + def RenderToDC(str, dc, x, y, enclose=True) + +In all cases, 'str' is an XML string. Note that start and end tags are +only required if *enclose* is set to False. In this case the text +should be wrapped in FancyText tags. + +In addition, the module exports one class:: + + class StaticFancyText(self, window, id, text, background, ...) + +This class works similar to StaticText except it interprets its text +as FancyText. + +The text can support superscripts and subscripts, text in different +sizes, colors, styles, weights and families. It also supports a +limited set of symbols, currently *times*, *infinity*, *angle* as well +as greek letters in both upper case (*Alpha* *Beta*... *Omega*) and +lower case (*alpha* *beta*... *omega*). + +>>> frame = wx.Frame(wx.NULL, -1, "FancyText demo", wx.DefaultPosition) +>>> sft = StaticFancyText(frame, -1, testText, wx.Brush("light grey", wx.SOLID)) +>>> frame.SetClientSize(sft.GetSize()) +>>> didit = frame.Show() +>>> from guitest import PauseTests; PauseTests() + +""" + +# Copyright 2001-2003 Timothy Hochberg +# Use as you see fit. No warantees, I cannot be held responsible, etc. + +import copy +import math +import sys + +import wx +import xml.parsers.expat + +__all__ = "GetExtent", "GetFullExtent", "RenderToBitmap", "RenderToDC", "StaticFancyText" + +if sys.platform == "win32": + _greekEncoding = str(wx.FONTENCODING_CP1253) +else: + _greekEncoding = str(wx.FONTENCODING_ISO8859_7) + +_families = {"fixed" : wx.FIXED, "default" : wx.DEFAULT, "decorative" : wx.DECORATIVE, "roman" : wx.ROMAN, + "script" : wx.SCRIPT, "swiss" : wx.SWISS, "modern" : wx.MODERN} +_styles = {"normal" : wx.NORMAL, "slant" : wx.SLANT, "italic" : wx.ITALIC} +_weights = {"normal" : wx.NORMAL, "light" : wx.LIGHT, "bold" : wx.BOLD} + +# The next three classes: Renderer, SizeRenderer and DCRenderer are +# what you will need to override to extend the XML language. All of +# the font stuff as well as the subscript and superscript stuff are in +# Renderer. + +_greek_letters = ("alpha", "beta", "gamma", "delta", "epsilon", "zeta", + "eta", "theta", "iota", "kappa", "lambda", "mu", "nu", + "xi", "omnikron", "pi", "rho", "altsigma", "sigma", "tau", "upsilon", + "phi", "chi", "psi", "omega") + +def iround(number): + return int(round(number)) + +def iceil(number): + return int(math.ceil(number)) + +class Renderer: + """Class for rendering XML marked up text. + + See the module docstring for a description of the markup. + + This class must be subclassed and the methods the methods that do + the drawing overridden for a particular output device. + + """ + defaultSize = None + defaultFamily = wx.DEFAULT + defaultStyle = wx.NORMAL + defaultWeight = wx.NORMAL + defaultEncoding = None + defaultColor = "black" + + def __init__(self, dc=None, x=0, y=None): + if dc == None: + dc = wx.MemoryDC() + self.dc = dc + self.offsets = [0] + self.fonts = [{}] + self.width = self.height = 0 + self.x = x + self.minY = self.maxY = self._y = y + if Renderer.defaultSize is None: + Renderer.defaultSize = wx.NORMAL_FONT.GetPointSize() + if Renderer.defaultEncoding is None: + Renderer.defaultEncoding = wx.Font_GetDefaultEncoding() + + def getY(self): + if self._y is None: + self.minY = self.maxY = self._y = self.dc.GetTextExtent("M")[1] + return self._y + def setY(self, value): + self._y = y + y = property(getY, setY) + + def startElement(self, name, attrs): + method = "start_" + name + if not hasattr(self, method): + raise ValueError("XML tag '%s' not supported" % name) + getattr(self, method)(attrs) + + def endElement(self, name): + methname = "end_" + name + if hasattr(self, methname): + getattr(self, methname)() + elif hasattr(self, "start_" + name): + pass + else: + raise ValueError("XML tag '%s' not supported" % methname) + + def characterData(self, data): + self.dc.SetFont(self.getCurrentFont()) + for i, chunk in enumerate(data.split('\n')): + if i: + self.x = 0 + self.y = self.mayY = self.maxY + self.dc.GetTextExtent("M")[1] + if chunk: + width, height, descent, extl = self.dc.GetFullTextExtent(chunk) + self.renderCharacterData(data, iround(self.x), iround(self.y + self.offsets[-1] - height + descent)) + else: + width = height = descent = extl = 0 + self.updateDims(width, height, descent, extl) + + def updateDims(self, width, height, descent, externalLeading): + self.x += width + self.width = max(self.x, self.width) + self.minY = min(self.minY, self.y+self.offsets[-1]-height+descent) + self.maxY = max(self.maxY, self.y+self.offsets[-1]+descent) + self.height = self.maxY - self.minY + + def start_FancyText(self, attrs): + pass + start_wxFancyText = start_FancyText # For backward compatibility + + def start_font(self, attrs): + for key, value in attrs.items(): + if key == "size": + value = int(value) + elif key == "family": + value = _families[value] + elif key == "style": + value = _styles[value] + elif key == "weight": + value = _weights[value] + elif key == "encoding": + value = int(value) + elif key == "color": + pass + else: + raise ValueError("unknown font attribute '%s'" % key) + attrs[key] = value + font = copy.copy(self.fonts[-1]) + font.update(attrs) + self.fonts.append(font) + + def end_font(self): + self.fonts.pop() + + def start_sub(self, attrs): + if attrs.keys(): + raise ValueError(" does not take attributes") + font = self.getCurrentFont() + self.offsets.append(self.offsets[-1] + self.dc.GetFullTextExtent("M", font)[1]*0.5) + self.start_font({"size" : font.GetPointSize() * 0.8}) + + def end_sub(self): + self.fonts.pop() + self.offsets.pop() + + def start_sup(self, attrs): + if attrs.keys(): + raise ValueError(" does not take attributes") + font = self.getCurrentFont() + self.offsets.append(self.offsets[-1] - self.dc.GetFullTextExtent("M", font)[1]*0.3) + self.start_font({"size" : font.GetPointSize() * 0.8}) + + def end_sup(self): + self.fonts.pop() + self.offsets.pop() + + def getCurrentFont(self): + font = self.fonts[-1] + return wx.Font(font.get("size", self.defaultSize), + font.get("family", self.defaultFamily), + font.get("style", self.defaultStyle), + font.get("weight",self.defaultWeight), + False, "", + font.get("encoding", self.defaultEncoding)) + + def getCurrentColor(self): + font = self.fonts[-1] + return wx.TheColourDatabase.FindColour(font.get("color", self.defaultColor)) + + def getCurrentPen(self): + return wx.Pen(self.getCurrentColor(), 1, wx.SOLID) + + def renderCharacterData(self, data, x, y): + raise NotImplementedError() + + +def _addGreek(): + alpha = 0xE1 + Alpha = 0xC1 + def end(self): + pass + for i, name in enumerate(_greek_letters): + def start(self, attrs, code=chr(alpha+i)): + self.start_font({"encoding" : _greekEncoding}) + self.characterData(code) + self.end_font() + setattr(Renderer, "start_%s" % name, start) + setattr(Renderer, "end_%s" % name, end) + if name == "altsigma": + continue # There is no capital for altsigma + def start(self, attrs, code=chr(Alpha+i)): + self.start_font({"encoding" : _greekEncoding}) + self.characterData(code) + self.end_font() + setattr(Renderer, "start_%s" % name.capitalize(), start) + setattr(Renderer, "end_%s" % name.capitalize(), end) +_addGreek() + + + +class SizeRenderer(Renderer): + """Processes text as if rendering it, but just computes the size.""" + + def __init__(self, dc=None): + Renderer.__init__(self, dc, 0, 0) + + def renderCharacterData(self, data, x, y): + pass + + def start_angle(self, attrs): + self.characterData("M") + + def start_infinity(self, attrs): + width, height = self.dc.GetTextExtent("M") + width = max(width, 10) + height = max(height, width / 2) + self.updateDims(width, height, 0, 0) + + def start_times(self, attrs): + self.characterData("M") + + def start_in(self, attrs): + self.characterData("M") + + def start_times(self, attrs): + self.characterData("M") + + +class DCRenderer(Renderer): + """Renders text to a wxPython device context DC.""" + + def renderCharacterData(self, data, x, y): + self.dc.SetTextForeground(self.getCurrentColor()) + self.dc.DrawText(data, x, y) + + def start_angle(self, attrs): + self.dc.SetFont(self.getCurrentFont()) + self.dc.SetPen(self.getCurrentPen()) + width, height, descent, leading = self.dc.GetFullTextExtent("M") + y = self.y + self.offsets[-1] + self.dc.DrawLine(iround(self.x), iround(y), iround( self.x+width), iround(y)) + self.dc.DrawLine(iround(self.x), iround(y), iround(self.x+width), iround(y-width)) + self.updateDims(width, height, descent, leading) + + + def start_infinity(self, attrs): + self.dc.SetFont(self.getCurrentFont()) + self.dc.SetPen(self.getCurrentPen()) + width, height, descent, leading = self.dc.GetFullTextExtent("M") + width = max(width, 10) + height = max(height, width / 2) + self.dc.SetPen(wx.Pen(self.getCurrentColor(), max(1, width/10))) + self.dc.SetBrush(wx.TRANSPARENT_BRUSH) + y = self.y + self.offsets[-1] + r = iround( 0.95 * width / 4) + xc = (2*self.x + width) / 2 + yc = iround(y-1.5*r) + self.dc.DrawCircle(xc - r, yc, r) + self.dc.DrawCircle(xc + r, yc, r) + self.updateDims(width, height, 0, 0) + + def start_times(self, attrs): + self.dc.SetFont(self.getCurrentFont()) + self.dc.SetPen(self.getCurrentPen()) + width, height, descent, leading = self.dc.GetFullTextExtent("M") + y = self.y + self.offsets[-1] + width *= 0.8 + width = iround(width+.5) + self.dc.SetPen(wx.Pen(self.getCurrentColor(), 1)) + self.dc.DrawLine(iround(self.x), iround(y-width), iround(self.x+width-1), iround(y-1)) + self.dc.DrawLine(iround(self.x), iround(y-2), iround(self.x+width-1), iround(y-width-1)) + self.updateDims(width, height, 0, 0) + + +def RenderToRenderer(str, renderer, enclose=True): + try: + if enclose: + str = '%s' % str + p = xml.parsers.expat.ParserCreate() + p.returns_unicode = 0 + p.StartElementHandler = renderer.startElement + p.EndElementHandler = renderer.endElement + p.CharacterDataHandler = renderer.characterData + p.Parse(str, 1) + except xml.parsers.expat.error, err: + raise ValueError('error parsing text text "%s": %s' % (str, err)) + + +# Public interface + + +def GetExtent(str, dc=None, enclose=True): + "Return the extent of str" + renderer = SizeRenderer(dc) + RenderToRenderer(str, renderer, enclose) + return iceil(renderer.width), iceil(renderer.height) # XXX round up + + +def GetFullExtent(str, dc=None, enclose=True): + renderer = SizeRenderer(dc) + RenderToRenderer(str, renderer, enclose) + return iceil(renderer.width), iceil(renderer.height), -iceil(renderer.minY) # XXX round up + + +def RenderToBitmap(str, background=None, enclose=1): + "Return str rendered on a minumum size bitmap" + dc = wx.MemoryDC() + # Chicken and egg problem, we need a bitmap in the DC in order to + # measure how big the bitmap should be... + dc.SelectObject(wx.EmptyBitmap(1,1)) + width, height, dy = GetFullExtent(str, dc, enclose) + bmp = wx.EmptyBitmap(width, height) + dc.SelectObject(bmp) + if background is None: + dc.SetBackground(wx.WHITE_BRUSH) + else: + dc.SetBackground(background) + dc.Clear() + renderer = DCRenderer(dc, y=dy) + dc.BeginDrawing() + RenderToRenderer(str, renderer, enclose) + dc.EndDrawing() + dc.SelectObject(wx.NullBitmap) + if background is None: + img = wx.ImageFromBitmap(bmp) + bg = dc.GetBackground().GetColour() + img.SetMaskColour(bg.Red(), bg.Green(), bg.Blue()) + bmp = img.ConvertToBitmap() + return bmp + + +def RenderToDC(str, dc, x, y, enclose=1): + "Render str onto a wxDC at (x,y)" + width, height, dy = GetFullExtent(str, dc) + renderer = DCRenderer(dc, x, y+dy) + RenderToRenderer(str, renderer, enclose) + + +class StaticFancyText(wx.StaticBitmap): + def __init__(self, window, id, text, *args, **kargs): + args = list(args) + kargs.setdefault('name', 'staticFancyText') + if 'background' in kargs: + background = kargs.pop('background') + elif args: + background = args.pop(0) + else: + background = wx.Brush(window.GetBackgroundColour(), wx.SOLID) + + bmp = RenderToBitmap(text, background) + wx.StaticBitmap.__init__(self, window, id, bmp, *args, **kargs) + + +# Old names for backward compatibiliry +getExtent = GetExtent +renderToBitmap = RenderToBitmap +renderToDC = RenderToDC + + +# Test Driver + +def test(): + testText = \ +"""FancyText -- methods for rendering XML specified text + +This module exports four main methods:: + + def GetExtent(str, dc=None, enclose=True) + def GetFullExtent(str, dc=None, enclose=True) + def RenderToBitmap(str, background=None, enclose=True) + def RenderToDC(str, dc, x, y, enclose=True) + +In all cases, 'str' is an XML string. Note that start and end tags +are only required if *enclose* is set to False. In this case the +text should be wrapped in FancyText tags. + +In addition, the module exports one class:: + + class StaticFancyText(self, window, id, text, background, ...) + +This class works similar to StaticText except it interprets its text +as FancyText. + +The text can supportsuperscripts and subscripts, text +in different sizes, colors, styles, weights and +families. It also supports a limited set of symbols, +currently , , as well as greek letters in both +upper case (...) and lower case (...). + +We can use doctest/guitest to display this string in all its marked up glory. + +>>> frame = wx.Frame(wx.NULL, -1, "FancyText demo", wx.DefaultPosition) +>>> sft = StaticFancyText(frame, -1, __doc__, wx.Brush("light grey", wx.SOLID)) +>>> frame.SetClientSize(sft.GetSize()) +>>> didit = frame.Show() +>>> from guitest import PauseTests; PauseTests() + + +The End""" + + app = wx.PySimpleApp() + box = wx.BoxSizer(wx.VERTICAL) + frame = wx.Frame(None, -1, "FancyText demo", wx.DefaultPosition) + frame.SetBackgroundColour("light grey") + sft = StaticFancyText(frame, -1, testText) + box.Add(sft, 1, wx.EXPAND) + frame.SetSizer(box) + frame.SetAutoLayout(True) + box.Fit(frame) + box.SetSizeHints(frame) + frame.Show() + app.MainLoop() + +if __name__ == "__main__": + test() + + diff --git a/wx/lib/filebrowsebutton.py b/wx/lib/filebrowsebutton.py new file mode 100644 index 00000000..0fb1bae4 --- /dev/null +++ b/wx/lib/filebrowsebutton.py @@ -0,0 +1,460 @@ +#---------------------------------------------------------------------- +# Name: wxPython.lib.filebrowsebutton +# Purpose: Composite controls that provide a Browse button next to +# either a wxTextCtrl or a wxComboBox. The Browse button +# launches a wxFileDialog and loads the result into the +# other control. +# +# Author: Mike Fletcher +# +# RCS-ID: $Id$ +# Copyright: (c) 2000 by Total Control Software +# Licence: wxWindows license +#---------------------------------------------------------------------- +# 12/02/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o 2.5 Compatability changes +# + +import os +import types + +import wx + +#---------------------------------------------------------------------- + +class FileBrowseButton(wx.Panel): + """ + A control to allow the user to type in a filename or browse with + the standard file dialog to select file + """ + def __init__ (self, parent, id= -1, + pos = wx.DefaultPosition, + size = wx.DefaultSize, + style = wx.TAB_TRAVERSAL, + labelText= "File Entry:", + buttonText= "Browse", + toolTip= "Type filename or click browse to choose file", + # following are the values for a file dialog box + dialogTitle = "Choose a file", + startDirectory = ".", + initialValue = "", + fileMask = "*.*", + fileMode = wx.OPEN, + # callback for when value changes (optional) + changeCallback= lambda x:x, + labelWidth = 0, + name = 'fileBrowseButton', + ): + """ + :param labelText: Text for label to left of text field + :param buttonText: Text for button which launches the file dialog + :param toolTip: Help text + :param dialogTitle: Title used in file dialog + :param startDirectory: Default directory for file dialog startup + :param fileMask: File mask (glob pattern, such as *.*) to use in file dialog + :param fileMode: wx.OPEN or wx.SAVE, indicates type of file dialog to use + :param changeCallback: Optional callback called for all changes in value of the control + :param labelWidth: Width of the label + """ + + # store variables + self.labelText = labelText + self.buttonText = buttonText + self.toolTip = toolTip + self.dialogTitle = dialogTitle + self.startDirectory = startDirectory + self.initialValue = initialValue + self.fileMask = fileMask + self.fileMode = fileMode + self.changeCallback = changeCallback + self.callCallback = True + self.labelWidth = labelWidth + + # create the dialog + self.createDialog(parent, id, pos, size, style, name ) + # Setting a value causes the changeCallback to be called. + # In this case that would be before the return of the + # constructor. Not good. So a default value on + # SetValue is used to disable the callback + self.SetValue( initialValue, 0) + + + def createDialog( self, parent, id, pos, size, style, name ): + """Setup the graphic representation of the dialog""" + wx.Panel.__init__ (self, parent, id, pos, size, style, name) + self.SetMinSize(size) # play nice with sizers + + box = wx.BoxSizer(wx.HORIZONTAL) + + self.label = self.createLabel( ) + box.Add( self.label, 0, wx.CENTER ) + + self.textControl = self.createTextControl() + box.Add( self.textControl, 1, wx.LEFT|wx.CENTER, 5) + + self.browseButton = self.createBrowseButton() + box.Add( self.browseButton, 0, wx.LEFT|wx.CENTER, 5) + + # add a border around the whole thing and resize the panel to fit + outsidebox = wx.BoxSizer(wx.VERTICAL) + outsidebox.Add(box, 1, wx.EXPAND|wx.ALL, 3) + outsidebox.Fit(self) + + self.SetAutoLayout(True) + self.SetSizer( outsidebox ) + self.Layout() + if type( size ) == types.TupleType: + size = apply( wx.Size, size) + self.SetDimensions(-1, -1, size.width, size.height, wx.SIZE_USE_EXISTING) + +# if size.width != -1 or size.height != -1: +# self.SetSize(size) + + def SetBackgroundColour(self,color): + wx.Panel.SetBackgroundColour(self,color) + self.label.SetBackgroundColour(color) + + def createLabel( self ): + """Create the label/caption""" + label = wx.StaticText(self, -1, self.labelText, style =wx.ALIGN_RIGHT ) + font = label.GetFont() + w, h, d, e = self.GetFullTextExtent(self.labelText, font) + if self.labelWidth > 0: + label.SetSize((self.labelWidth+5, h)) + else: + label.SetSize((w+5, h)) + return label + + def createTextControl( self): + """Create the text control""" + textControl = wx.TextCtrl(self, -1) + textControl.SetToolTipString( self.toolTip ) + if self.changeCallback: + textControl.Bind(wx.EVT_TEXT, self.OnChanged) + textControl.Bind(wx.EVT_COMBOBOX, self.OnChanged) + return textControl + + def OnChanged(self, evt): + if self.callCallback and self.changeCallback: + self.changeCallback(evt) + + def createBrowseButton( self): + """Create the browse-button control""" + button =wx.Button(self, -1, self.buttonText) + button.SetToolTipString( self.toolTip ) + button.Bind(wx.EVT_BUTTON, self.OnBrowse) + return button + + + def OnBrowse (self, event = None): + """ Going to browse for file... """ + current = self.GetValue() + directory = os.path.split(current) + if os.path.isdir( current): + directory = current + current = '' + elif directory and os.path.isdir( directory[0] ): + current = directory[1] + directory = directory [0] + else: + directory = self.startDirectory + current = '' + dlg = wx.FileDialog(self, self.dialogTitle, directory, current, + self.fileMask, self.fileMode) + + if dlg.ShowModal() == wx.ID_OK: + self.SetValue(dlg.GetPath()) + dlg.Destroy() + + + def GetValue (self): + """ + retrieve current value of text control + """ + return self.textControl.GetValue() + + def SetValue (self, value, callBack=1): + """set current value of text control""" + save = self.callCallback + self.callCallback = callBack + self.textControl.SetValue(value) + self.callCallback = save + + + def GetLabel( self ): + """ Retrieve the label's current text """ + return self.label.GetLabel() + + def SetLabel( self, value ): + """ Set the label's current text """ + rvalue = self.label.SetLabel( value ) + self.Refresh( True ) + return rvalue + + + + +class FileBrowseButtonWithHistory( FileBrowseButton ): + """ + with following additions: + __init__(..., history=None) + + history -- optional list of paths for initial history drop-down + (must be passed by name, not a positional argument) + If history is callable it will must return a list used + for the history drop-down + + changeCallback -- as for FileBrowseButton, but with a work-around + for win32 systems which don't appear to create wx.EVT_COMBOBOX + events properly. There is a (slight) chance that this work-around + will cause some systems to create two events for each Combobox + selection. If you discover this condition, please report it! + + As for a FileBrowseButton.__init__ otherwise. + + GetHistoryControl() + Return reference to the control which implements interfaces + required for manipulating the history list. See GetHistoryControl + documentation for description of what that interface is. + + GetHistory() + Return current history list + + SetHistory( value=(), selectionIndex = None ) + Set current history list, if selectionIndex is not None, select that index + + """ + def __init__( self, *arguments, **namedarguments): + self.history = namedarguments.get( "history" ) + if self.history: + del namedarguments["history"] + + self.historyCallBack=None + if callable(self.history): + self.historyCallBack=self.history + self.history=None + name = namedarguments.get('name', 'fileBrowseButtonWithHistory') + namedarguments['name'] = name + FileBrowseButton.__init__(self, *arguments, **namedarguments) + + + def createTextControl( self): + """Create the text control""" + textControl = wx.ComboBox(self, -1, style = wx.CB_DROPDOWN ) + textControl.SetToolTipString( self.toolTip ) + textControl.Bind(wx.EVT_SET_FOCUS, self.OnSetFocus) + if self.changeCallback: + textControl.Bind(wx.EVT_TEXT, self.OnChanged) + textControl.Bind(wx.EVT_COMBOBOX, self.OnChanged) + if self.history: + history=self.history + self.history=None + self.SetHistory( history, control=textControl) + return textControl + + + def GetHistoryControl( self ): + """ + Return a pointer to the control which provides (at least) + the following methods for manipulating the history list: + + Append( item ) -- add item + Clear() -- clear all items + Delete( index ) -- 0-based index to delete from list + SetSelection( index ) -- 0-based index to select in list + + Semantics of the methods follow those for the wxComboBox control + """ + return self.textControl + + + def SetHistory( self, value=(), selectionIndex = None, control=None ): + """Set the current history list""" + if control is None: + control = self.GetHistoryControl() + if self.history == value: + return + self.history = value + # Clear history values not the selected one. + tempValue=control.GetValue() + # clear previous values + control.Clear() + control.SetValue(tempValue) + # walk through, appending new values + for path in value: + control.Append( path ) + if selectionIndex is not None: + control.SetSelection( selectionIndex ) + + + def GetHistory( self ): + """Return the current history list""" + if self.historyCallBack != None: + return self.historyCallBack() + elif self.history: + return list( self.history ) + else: + return [] + + + def OnSetFocus(self, event): + """When the history scroll is selected, update the history""" + if self.historyCallBack != None: + self.SetHistory( self.historyCallBack(), control=self.textControl) + event.Skip() + + + if wx.Platform == "__WXMSW__": + def SetValue (self, value, callBack=1): + """ Convenient setting of text control value, works + around limitation of wx.ComboBox """ + save = self.callCallback + self.callCallback = callBack + self.textControl.SetValue(value) + self.callCallback = save + + # Hack to call an event handler + class LocalEvent: + def __init__(self, string): + self._string=string + def GetString(self): + return self._string + if callBack==1: + # The callback wasn't being called when SetValue was used ?? + # So added this explicit call to it + self.changeCallback(LocalEvent(value)) + + +class DirBrowseButton(FileBrowseButton): + def __init__(self, parent, id = -1, + pos = wx.DefaultPosition, size = wx.DefaultSize, + style = wx.TAB_TRAVERSAL, + labelText = 'Select a directory:', + buttonText = 'Browse', + toolTip = 'Type directory name or browse to select', + dialogTitle = '', + startDirectory = '.', + changeCallback = None, + dialogClass = wx.DirDialog, + newDirectory = False, + name = 'dirBrowseButton'): + FileBrowseButton.__init__(self, parent, id, pos, size, style, + labelText, buttonText, toolTip, + dialogTitle, startDirectory, + changeCallback = changeCallback, + name = name) + self.dialogClass = dialogClass + self.newDirectory = newDirectory + # + + def OnBrowse(self, ev = None): + style=0 + + if not self.newDirectory: + style |= wx.DD_DIR_MUST_EXIST + + dialog = self.dialogClass(self, + message = self.dialogTitle, + defaultPath = self.startDirectory, + style = style) + + if dialog.ShowModal() == wx.ID_OK: + self.SetValue(dialog.GetPath()) + dialog.Destroy() + # + + +#---------------------------------------------------------------------- + + +if __name__ == "__main__": + #from skeletonbuilder import rulesfile + class SimpleCallback: + def __init__( self, tag ): + self.tag = tag + def __call__( self, event ): + print self.tag, event.GetString() + class DemoFrame( wx.Frame ): + def __init__(self, parent): + wx.Frame.__init__(self, parent, -1, "File entry with browse", size=(500,260)) + self.Bind(wx.EVT_CLOSE, self.OnCloseWindow) + panel = wx.Panel (self,-1) + innerbox = wx.BoxSizer(wx.VERTICAL) + control = FileBrowseButton( + panel, + initialValue = "z:\\temp", + ) + innerbox.Add( control, 0, wx.EXPAND ) + middlecontrol = FileBrowseButtonWithHistory( + panel, + labelText = "With History", + initialValue = "d:\\temp", + history = ["c:\\temp", "c:\\tmp", "r:\\temp","z:\\temp"], + changeCallback= SimpleCallback( "With History" ), + ) + innerbox.Add( middlecontrol, 0, wx.EXPAND ) + middlecontrol = FileBrowseButtonWithHistory( + panel, + labelText = "History callback", + initialValue = "d:\\temp", + history = self.historyCallBack, + changeCallback= SimpleCallback( "History callback" ), + ) + innerbox.Add( middlecontrol, 0, wx.EXPAND ) + self.bottomcontrol = control = FileBrowseButton( + panel, + labelText = "With Callback", + style = wx.SUNKEN_BORDER|wx.CLIP_CHILDREN , + changeCallback= SimpleCallback( "With Callback" ), + ) + innerbox.Add( control, 0, wx.EXPAND) + self.bottommostcontrol = control = DirBrowseButton( + panel, + labelText = "Simple dir browse button", + style = wx.SUNKEN_BORDER|wx.CLIP_CHILDREN) + innerbox.Add( control, 0, wx.EXPAND) + ID = wx.NewId() + innerbox.Add( wx.Button( panel, ID,"Change Label", ), 1, wx.EXPAND) + self.Bind(wx.EVT_BUTTON, self.OnChangeLabel , id=ID) + ID = wx.NewId() + innerbox.Add( wx.Button( panel, ID,"Change Value", ), 1, wx.EXPAND) + self.Bind(wx.EVT_BUTTON, self.OnChangeValue, id=ID ) + panel.SetAutoLayout(True) + panel.SetSizer( innerbox ) + self.history={"c:\\temp":1, "c:\\tmp":1, "r:\\temp":1,"z:\\temp":1} + + def historyCallBack(self): + keys=self.history.keys() + keys.sort() + return keys + + def OnFileNameChangedHistory (self, event): + self.history[event.GetString ()]=1 + + def OnCloseMe(self, event): + self.Close(True) + def OnChangeLabel( self, event ): + self.bottomcontrol.SetLabel( "Label Updated" ) + def OnChangeValue( self, event ): + self.bottomcontrol.SetValue( "r:\\somewhere\\over\\the\\rainbow.htm" ) + + def OnCloseWindow(self, event): + self.Destroy() + + class DemoApp(wx.App): + def OnInit(self): + wx.InitAllImageHandlers() + frame = DemoFrame(None) + frame.Show(True) + self.SetTopWindow(frame) + return True + + def test( ): + app = DemoApp(0) + app.MainLoop() + print 'Creating dialog' + test( ) + + + diff --git a/wx/lib/flashwin.py b/wx/lib/flashwin.py new file mode 100644 index 00000000..8d3009f0 --- /dev/null +++ b/wx/lib/flashwin.py @@ -0,0 +1,262 @@ +#---------------------------------------------------------------------- +# Name: wx.lib.flashwin +# Purpose: A class that allows the use of the Shockwave Flash +# ActiveX control +# +# Author: Robin Dunn +# +# Created: 22-March-2004 +# RCS-ID: $Id$ +# Copyright: (c) 2008 by Total Control Software +# Licence: wxWindows license +#---------------------------------------------------------------------- + +import wx +import wx.lib.activex +import comtypes.client as cc + +import sys +if not hasattr(sys, 'frozen'): + cc.GetModule( ('{D27CDB6B-AE6D-11CF-96B8-444553540000}', 1, 0) ) +from comtypes.gen import ShockwaveFlashObjects + + +clsID = '{D27CDB6E-AE6D-11CF-96B8-444553540000}' +progID = 'ShockwaveFlash.ShockwaveFlash.1' + + + +class FlashWindow(wx.lib.activex.ActiveXCtrl): + def __init__(self, parent, id=-1, pos=wx.DefaultPosition, + size=wx.DefaultSize, style=0, name='FlashWindow'): + wx.lib.activex.ActiveXCtrl.__init__(self, parent, progID, + id, pos, size, style, name) + + def SetZoomRect(self, left, top, right, bottom): + return self.ctrl.SetZoomRect(left, top, right, bottom) + + def Zoom(self, factor): + return self.ctrl.Zoom(factor) + + def Pan(self, x, y, mode): + return self.ctrl.Pan(x, y, mode) + + def Play(self): + return self.ctrl.Play() + + def Stop(self): + return self.ctrl.Stop() + + def Back(self): + return self.ctrl.Back() + + def Forward(self): + return self.ctrl.Forward() + + def Rewind(self): + return self.ctrl.Rewind() + + def StopPlay(self): + return self.ctrl.StopPlay() + + def GotoFrame(self, FrameNum): + return self.ctrl.GotoFrame(FrameNum) + + def CurrentFrame(self): + return self.ctrl.CurrentFrame() + + def IsPlaying(self): + return self.ctrl.IsPlaying() + + def PercentLoaded(self): + return self.ctrl.PercentLoaded() + + def FrameLoaded(self, FrameNum): + return self.ctrl.FrameLoaded(FrameNum) + + def FlashVersion(self): + return self.ctrl.FlashVersion() + + def LoadMovie(self, layer, url): + return self.ctrl.LoadMovie(layer, url) + + def TGotoFrame(self, target, FrameNum): + return self.ctrl.TGotoFrame(target, FrameNum) + + def TGotoLabel(self, target, label): + return self.ctrl.TGotoLabel(target, label) + + def TCurrentFrame(self, target): + return self.ctrl.TCurrentFrame(target) + + def TCurrentLabel(self, target): + return self.ctrl.TCurrentLabel(target) + + def TPlay(self, target): + return self.ctrl.TPlay(target) + + def TStopPlay(self, target): + return self.ctrl.TStopPlay(target) + + def SetVariable(self, name, value): + return self.ctrl.SetVariable(name, value) + + def GetVariable(self, name): + return self.ctrl.GetVariable(name) + + def TSetProperty(self, target, property, value): + return self.ctrl.TSetProperty(target, property, value) + + def TGetProperty(self, target, property): + return self.ctrl.TGetProperty(target, property) + + def TCallFrame(self, target, FrameNum): + return self.ctrl.TCallFrame(target, FrameNum) + + def TCallLabel(self, target, label): + return self.ctrl.TCallLabel(target, label) + + def TSetPropertyNum(self, target, property, value): + return self.ctrl.TSetPropertyNum(target, property, value) + + def TGetPropertyNum(self, target, property): + return self.ctrl.TGetPropertyNum(target, property) + + def TGetPropertyAsNumber(self, target, property): + return self.ctrl.TGetPropertyAsNumber(target, property) + + # Getters, Setters and properties + def _get_ReadyState(self): + return self.ctrl.ReadyState + readystate = property(_get_ReadyState, None) + + def _get_TotalFrames(self): + return self.ctrl.TotalFrames + totalframes = property(_get_TotalFrames, None) + + def _get_Playing(self): + return self.ctrl.Playing + def _set_Playing(self, Playing): + self.ctrl.Playing = Playing + playing = property(_get_Playing, _set_Playing) + + def _get_Quality(self): + return self.ctrl.Quality + def _set_Quality(self, Quality): + self.ctrl.Quality = Quality + quality = property(_get_Quality, _set_Quality) + + def _get_ScaleMode(self): + return self.ctrl.ScaleMode + def _set_ScaleMode(self, ScaleMode): + self.ctrl.ScaleMode = ScaleMode + scalemode = property(_get_ScaleMode, _set_ScaleMode) + + def _get_AlignMode(self): + return self.ctrl.AlignMode + def _set_AlignMode(self, AlignMode): + self.ctrl.AlignMode = AlignMode + alignmode = property(_get_AlignMode, _set_AlignMode) + + def _get_BackgroundColor(self): + return self.ctrl.BackgroundColor + def _set_BackgroundColor(self, BackgroundColor): + self.ctrl.BackgroundColor = BackgroundColor + backgroundcolor = property(_get_BackgroundColor, _set_BackgroundColor) + + def _get_Loop(self): + return self.ctrl.Loop + def _set_Loop(self, Loop): + self.ctrl.Loop = Loop + loop = property(_get_Loop, _set_Loop) + + def _get_Movie(self): + return self.ctrl.Movie + def _set_Movie(self, Movie): + self.ctrl.Movie = Movie + movie = property(_get_Movie, _set_Movie) + + def _get_FrameNum(self): + return self.ctrl.FrameNum + def _set_FrameNum(self, FrameNum): + self.ctrl.FrameNum = FrameNum + framenum = property(_get_FrameNum, _set_FrameNum) + + def _get_WMode(self): + return self.ctrl.WMode + def _set_WMode(self, WMode): + self.ctrl.WMode = WMode + wmode = property(_get_WMode, _set_WMode) + + def _get_SAlign(self): + return self.ctrl.SAlign + def _set_SAlign(self, SAlign): + self.ctrl.SAlign = SAlign + salign = property(_get_SAlign, _set_SAlign) + + def _get_Menu(self): + return self.ctrl.Menu + def _set_Menu(self, Menu): + self.ctrl.Menu = Menu + menu = property(_get_Menu, _set_Menu) + + def _get_Base(self): + return self.ctrl.Base + def _set_Base(self, Base): + self.ctrl.Base = Base + base = property(_get_Base, _set_Base) + + def _get_Scale(self): + return self.ctrl.Scale + def _set_Scale(self, Scale): + self.ctrl.Scale = Scale + scale = property(_get_Scale, _set_Scale) + + def _get_DeviceFont(self): + return self.ctrl.DeviceFont + def _set_DeviceFont(self, DeviceFont): + self.ctrl.DeviceFont = DeviceFont + devicefont = property(_get_DeviceFont, _set_DeviceFont) + + def _get_EmbedMovie(self): + return self.ctrl.EmbedMovie + def _set_EmbedMovie(self, EmbedMovie): + self.ctrl.EmbedMovie = EmbedMovie + embedmovie = property(_get_EmbedMovie, _set_EmbedMovie) + + def _get_BGColor(self): + return self.ctrl.BGColor + def _set_BGColor(self, BGColor): + self.ctrl.BGColor = BGColor + bgcolor = property(_get_BGColor, _set_BGColor) + + def _get_Quality2(self): + return self.ctrl.Quality2 + def _set_Quality2(self, Quality2): + self.ctrl.Quality2 = Quality2 + quality2 = property(_get_Quality2, _set_Quality2) + + def _get_SWRemote(self): + return self.ctrl.SWRemote + def _set_SWRemote(self, SWRemote): + self.ctrl.SWRemote = SWRemote + swremote = property(_get_SWRemote, _set_SWRemote) + + def _get_FlashVars(self): + return self.ctrl.FlashVars + def _set_FlashVars(self, FlashVars): + self.ctrl.FlashVars = FlashVars + flashvars = property(_get_FlashVars, _set_FlashVars) + + def _get_AllowScriptAccess(self): + return self.ctrl.AllowScriptAccess + def _set_AllowScriptAccess(self, AllowScriptAccess): + self.ctrl.AllowScriptAccess = AllowScriptAccess + allowscriptaccess = property(_get_AllowScriptAccess, _set_AllowScriptAccess) + + def _get_MovieData(self): + return self.ctrl.MovieData + def _set_MovieData(self, MovieData): + self.ctrl.MovieData = MovieData + moviedata = property(_get_MovieData, _set_MovieData) + diff --git a/wx/lib/flashwin_old.py b/wx/lib/flashwin_old.py new file mode 100644 index 00000000..77fb2939 --- /dev/null +++ b/wx/lib/flashwin_old.py @@ -0,0 +1,652 @@ +#---------------------------------------------------------------------- +# Name: wx.lib.flashwin +# Purpose: A class that allows the use of the Shockwave Flash +# ActiveX control +# +# Author: Robin Dunn +# +# Created: 22-March-2004 +# RCS-ID: $Id: flashwin.py 26301 2004-03-23 05:29:50Z RD $ +# Copyright: (c) 2004 by Total Control Software +# Licence: wxWindows license +#---------------------------------------------------------------------- +# This module was generated by the wx.activex.GernerateAXModule class +# (See also the genaxmodule script.) + +import wx +import wx.activex + +clsID = '{D27CDB6E-AE6D-11CF-96B8-444553540000}' +progID = 'ShockwaveFlash.ShockwaveFlash.1' + + + +# Create eventTypes and event binders +wxEVT_ReadyStateChange = wx.activex.RegisterActiveXEvent('OnReadyStateChange') +wxEVT_Progress = wx.activex.RegisterActiveXEvent('OnProgress') +wxEVT_FSCommand = wx.activex.RegisterActiveXEvent('FSCommand') + +EVT_ReadyStateChange = wx.PyEventBinder(wxEVT_ReadyStateChange, 1) +EVT_Progress = wx.PyEventBinder(wxEVT_Progress, 1) +EVT_FSCommand = wx.PyEventBinder(wxEVT_FSCommand, 1) + + +# Derive a new class from ActiveXWindow +class FlashWindow(wx.activex.ActiveXWindow): + def __init__(self, parent, ID=-1, pos=wx.DefaultPosition, + size=wx.DefaultSize, style=0, name='FlashWindow'): + wx.activex.ActiveXWindow.__init__(self, parent, + wx.activex.CLSID('{D27CDB6E-AE6D-11CF-96B8-444553540000}'), + ID, pos, size, style, name) + + # Methods exported by the ActiveX object + def QueryInterface(self, riid): + return self.CallAXMethod('QueryInterface', riid) + + def AddRef(self): + return self.CallAXMethod('AddRef') + + def Release(self): + return self.CallAXMethod('Release') + + def GetTypeInfoCount(self): + return self.CallAXMethod('GetTypeInfoCount') + + def GetTypeInfo(self, itinfo, lcid): + return self.CallAXMethod('GetTypeInfo', itinfo, lcid) + + def GetIDsOfNames(self, riid, rgszNames, cNames, lcid): + return self.CallAXMethod('GetIDsOfNames', riid, rgszNames, cNames, lcid) + + def Invoke(self, dispidMember, riid, lcid, wFlags, pdispparams): + return self.CallAXMethod('Invoke', dispidMember, riid, lcid, wFlags, pdispparams) + + def SetZoomRect(self, left, top, right, bottom): + return self.CallAXMethod('SetZoomRect', left, top, right, bottom) + + def Zoom(self, factor): + return self.CallAXMethod('Zoom', factor) + + def Pan(self, x, y, mode): + return self.CallAXMethod('Pan', x, y, mode) + + def Play(self): + return self.CallAXMethod('Play') + + def Stop(self): + return self.CallAXMethod('Stop') + + def Back(self): + return self.CallAXMethod('Back') + + def Forward(self): + return self.CallAXMethod('Forward') + + def Rewind(self): + return self.CallAXMethod('Rewind') + + def StopPlay(self): + return self.CallAXMethod('StopPlay') + + def GotoFrame(self, FrameNum): + return self.CallAXMethod('GotoFrame', FrameNum) + + def CurrentFrame(self): + return self.CallAXMethod('CurrentFrame') + + def IsPlaying(self): + return self.CallAXMethod('IsPlaying') + + def PercentLoaded(self): + return self.CallAXMethod('PercentLoaded') + + def FrameLoaded(self, FrameNum): + return self.CallAXMethod('FrameLoaded', FrameNum) + + def FlashVersion(self): + return self.CallAXMethod('FlashVersion') + + def LoadMovie(self, layer, url): + return self.CallAXMethod('LoadMovie', layer, url) + + def TGotoFrame(self, target, FrameNum): + return self.CallAXMethod('TGotoFrame', target, FrameNum) + + def TGotoLabel(self, target, label): + return self.CallAXMethod('TGotoLabel', target, label) + + def TCurrentFrame(self, target): + return self.CallAXMethod('TCurrentFrame', target) + + def TCurrentLabel(self, target): + return self.CallAXMethod('TCurrentLabel', target) + + def TPlay(self, target): + return self.CallAXMethod('TPlay', target) + + def TStopPlay(self, target): + return self.CallAXMethod('TStopPlay', target) + + def SetVariable(self, name, value): + return self.CallAXMethod('SetVariable', name, value) + + def GetVariable(self, name): + return self.CallAXMethod('GetVariable', name) + + def TSetProperty(self, target, property, value): + return self.CallAXMethod('TSetProperty', target, property, value) + + def TGetProperty(self, target, property): + return self.CallAXMethod('TGetProperty', target, property) + + def TCallFrame(self, target, FrameNum): + return self.CallAXMethod('TCallFrame', target, FrameNum) + + def TCallLabel(self, target, label): + return self.CallAXMethod('TCallLabel', target, label) + + def TSetPropertyNum(self, target, property, value): + return self.CallAXMethod('TSetPropertyNum', target, property, value) + + def TGetPropertyNum(self, target, property): + return self.CallAXMethod('TGetPropertyNum', target, property) + + def TGetPropertyAsNumber(self, target, property): + return self.CallAXMethod('TGetPropertyAsNumber', target, property) + + # Getters, Setters and properties + def _get_ReadyState(self): + return self.GetAXProp('ReadyState') + readystate = property(_get_ReadyState, None) + + def _get_TotalFrames(self): + return self.GetAXProp('TotalFrames') + totalframes = property(_get_TotalFrames, None) + + def _get_Playing(self): + return self.GetAXProp('Playing') + def _set_Playing(self, Playing): + self.SetAXProp('Playing', Playing) + playing = property(_get_Playing, _set_Playing) + + def _get_Quality(self): + return self.GetAXProp('Quality') + def _set_Quality(self, Quality): + self.SetAXProp('Quality', Quality) + quality = property(_get_Quality, _set_Quality) + + def _get_ScaleMode(self): + return self.GetAXProp('ScaleMode') + def _set_ScaleMode(self, ScaleMode): + self.SetAXProp('ScaleMode', ScaleMode) + scalemode = property(_get_ScaleMode, _set_ScaleMode) + + def _get_AlignMode(self): + return self.GetAXProp('AlignMode') + def _set_AlignMode(self, AlignMode): + self.SetAXProp('AlignMode', AlignMode) + alignmode = property(_get_AlignMode, _set_AlignMode) + + def _get_BackgroundColor(self): + return self.GetAXProp('BackgroundColor') + def _set_BackgroundColor(self, BackgroundColor): + self.SetAXProp('BackgroundColor', BackgroundColor) + backgroundcolor = property(_get_BackgroundColor, _set_BackgroundColor) + + def _get_Loop(self): + return self.GetAXProp('Loop') + def _set_Loop(self, Loop): + self.SetAXProp('Loop', Loop) + loop = property(_get_Loop, _set_Loop) + + def _get_Movie(self): + return self.GetAXProp('Movie') + def _set_Movie(self, Movie): + self.SetAXProp('Movie', Movie) + movie = property(_get_Movie, _set_Movie) + + def _get_FrameNum(self): + return self.GetAXProp('FrameNum') + def _set_FrameNum(self, FrameNum): + self.SetAXProp('FrameNum', FrameNum) + framenum = property(_get_FrameNum, _set_FrameNum) + + def _get_WMode(self): + return self.GetAXProp('WMode') + def _set_WMode(self, WMode): + self.SetAXProp('WMode', WMode) + wmode = property(_get_WMode, _set_WMode) + + def _get_SAlign(self): + return self.GetAXProp('SAlign') + def _set_SAlign(self, SAlign): + self.SetAXProp('SAlign', SAlign) + salign = property(_get_SAlign, _set_SAlign) + + def _get_Menu(self): + return self.GetAXProp('Menu') + def _set_Menu(self, Menu): + self.SetAXProp('Menu', Menu) + menu = property(_get_Menu, _set_Menu) + + def _get_Base(self): + return self.GetAXProp('Base') + def _set_Base(self, Base): + self.SetAXProp('Base', Base) + base = property(_get_Base, _set_Base) + + def _get_Scale(self): + return self.GetAXProp('Scale') + def _set_Scale(self, Scale): + self.SetAXProp('Scale', Scale) + scale = property(_get_Scale, _set_Scale) + + def _get_DeviceFont(self): + return self.GetAXProp('DeviceFont') + def _set_DeviceFont(self, DeviceFont): + self.SetAXProp('DeviceFont', DeviceFont) + devicefont = property(_get_DeviceFont, _set_DeviceFont) + + def _get_EmbedMovie(self): + return self.GetAXProp('EmbedMovie') + def _set_EmbedMovie(self, EmbedMovie): + self.SetAXProp('EmbedMovie', EmbedMovie) + embedmovie = property(_get_EmbedMovie, _set_EmbedMovie) + + def _get_BGColor(self): + return self.GetAXProp('BGColor') + def _set_BGColor(self, BGColor): + self.SetAXProp('BGColor', BGColor) + bgcolor = property(_get_BGColor, _set_BGColor) + + def _get_Quality2(self): + return self.GetAXProp('Quality2') + def _set_Quality2(self, Quality2): + self.SetAXProp('Quality2', Quality2) + quality2 = property(_get_Quality2, _set_Quality2) + + def _get_SWRemote(self): + return self.GetAXProp('SWRemote') + def _set_SWRemote(self, SWRemote): + self.SetAXProp('SWRemote', SWRemote) + swremote = property(_get_SWRemote, _set_SWRemote) + + def _get_FlashVars(self): + return self.GetAXProp('FlashVars') + def _set_FlashVars(self, FlashVars): + self.SetAXProp('FlashVars', FlashVars) + flashvars = property(_get_FlashVars, _set_FlashVars) + + def _get_AllowScriptAccess(self): + return self.GetAXProp('AllowScriptAccess') + def _set_AllowScriptAccess(self, AllowScriptAccess): + self.SetAXProp('AllowScriptAccess', AllowScriptAccess) + allowscriptaccess = property(_get_AllowScriptAccess, _set_AllowScriptAccess) + + def _get_MovieData(self): + return self.GetAXProp('MovieData') + def _set_MovieData(self, MovieData): + self.SetAXProp('MovieData', MovieData) + moviedata = property(_get_MovieData, _set_MovieData) + + +# PROPERTIES +# -------------------- +# readystate +# type:int arg:VT_EMPTY canGet:True canSet:False +# +# totalframes +# type:int arg:VT_EMPTY canGet:True canSet:False +# +# playing +# type:bool arg:bool canGet:True canSet:True +# +# quality +# type:int arg:int canGet:True canSet:True +# +# scalemode +# type:int arg:int canGet:True canSet:True +# +# alignmode +# type:int arg:int canGet:True canSet:True +# +# backgroundcolor +# type:int arg:int canGet:True canSet:True +# +# loop +# type:bool arg:bool canGet:True canSet:True +# +# movie +# type:string arg:string canGet:True canSet:True +# +# framenum +# type:int arg:int canGet:True canSet:True +# +# wmode +# type:string arg:string canGet:True canSet:True +# +# salign +# type:string arg:string canGet:True canSet:True +# +# menu +# type:bool arg:bool canGet:True canSet:True +# +# base +# type:string arg:string canGet:True canSet:True +# +# scale +# type:string arg:string canGet:True canSet:True +# +# devicefont +# type:bool arg:bool canGet:True canSet:True +# +# embedmovie +# type:bool arg:bool canGet:True canSet:True +# +# bgcolor +# type:string arg:string canGet:True canSet:True +# +# quality2 +# type:string arg:string canGet:True canSet:True +# +# swremote +# type:string arg:string canGet:True canSet:True +# +# flashvars +# type:string arg:string canGet:True canSet:True +# +# allowscriptaccess +# type:string arg:string canGet:True canSet:True +# +# moviedata +# type:string arg:string canGet:True canSet:True +# +# +# +# +# METHODS +# -------------------- +# QueryInterface +# retType: VT_VOID +# params: +# riid +# in:True out:False optional:False type:unsupported type 29 +# ppvObj +# in:False out:True optional:False type:unsupported type 26 +# +# AddRef +# retType: int +# +# Release +# retType: int +# +# GetTypeInfoCount +# retType: VT_VOID +# params: +# pctinfo +# in:False out:True optional:False type:int +# +# GetTypeInfo +# retType: VT_VOID +# params: +# itinfo +# in:True out:False optional:False type:int +# lcid +# in:True out:False optional:False type:int +# pptinfo +# in:False out:True optional:False type:unsupported type 26 +# +# GetIDsOfNames +# retType: VT_VOID +# params: +# riid +# in:True out:False optional:False type:unsupported type 29 +# rgszNames +# in:True out:False optional:False type:unsupported type 26 +# cNames +# in:True out:False optional:False type:int +# lcid +# in:True out:False optional:False type:int +# rgdispid +# in:False out:True optional:False type:int +# +# Invoke +# retType: VT_VOID +# params: +# dispidMember +# in:True out:False optional:False type:int +# riid +# in:True out:False optional:False type:unsupported type 29 +# lcid +# in:True out:False optional:False type:int +# wFlags +# in:True out:False optional:False type:int +# pdispparams +# in:True out:False optional:False type:unsupported type 29 +# pvarResult +# in:False out:True optional:False type:VT_VARIANT +# pexcepinfo +# in:False out:True optional:False type:unsupported type 29 +# puArgErr +# in:False out:True optional:False type:int +# +# SetZoomRect +# retType: VT_VOID +# params: +# left +# in:True out:False optional:False type:int +# top +# in:True out:False optional:False type:int +# right +# in:True out:False optional:False type:int +# bottom +# in:True out:False optional:False type:int +# +# Zoom +# retType: VT_VOID +# params: +# factor +# in:True out:False optional:False type:int +# +# Pan +# retType: VT_VOID +# params: +# x +# in:True out:False optional:False type:int +# y +# in:True out:False optional:False type:int +# mode +# in:True out:False optional:False type:int +# +# Play +# retType: VT_VOID +# +# Stop +# retType: VT_VOID +# +# Back +# retType: VT_VOID +# +# Forward +# retType: VT_VOID +# +# Rewind +# retType: VT_VOID +# +# StopPlay +# retType: VT_VOID +# +# GotoFrame +# retType: VT_VOID +# params: +# FrameNum +# in:True out:False optional:False type:int +# +# CurrentFrame +# retType: int +# +# IsPlaying +# retType: bool +# +# PercentLoaded +# retType: int +# +# FrameLoaded +# retType: bool +# params: +# FrameNum +# in:True out:False optional:False type:int +# +# FlashVersion +# retType: int +# +# LoadMovie +# retType: VT_VOID +# params: +# layer +# in:True out:False optional:False type:int +# url +# in:True out:False optional:False type:string +# +# TGotoFrame +# retType: VT_VOID +# params: +# target +# in:True out:False optional:False type:string +# FrameNum +# in:True out:False optional:False type:int +# +# TGotoLabel +# retType: VT_VOID +# params: +# target +# in:True out:False optional:False type:string +# label +# in:True out:False optional:False type:string +# +# TCurrentFrame +# retType: int +# params: +# target +# in:True out:False optional:False type:string +# +# TCurrentLabel +# retType: string +# params: +# target +# in:True out:False optional:False type:string +# +# TPlay +# retType: VT_VOID +# params: +# target +# in:True out:False optional:False type:string +# +# TStopPlay +# retType: VT_VOID +# params: +# target +# in:True out:False optional:False type:string +# +# SetVariable +# retType: VT_VOID +# params: +# name +# in:True out:False optional:False type:string +# value +# in:True out:False optional:False type:string +# +# GetVariable +# retType: string +# params: +# name +# in:True out:False optional:False type:string +# +# TSetProperty +# retType: VT_VOID +# params: +# target +# in:True out:False optional:False type:string +# property +# in:True out:False optional:False type:int +# value +# in:True out:False optional:False type:string +# +# TGetProperty +# retType: string +# params: +# target +# in:True out:False optional:False type:string +# property +# in:True out:False optional:False type:int +# +# TCallFrame +# retType: VT_VOID +# params: +# target +# in:True out:False optional:False type:string +# FrameNum +# in:True out:False optional:False type:int +# +# TCallLabel +# retType: VT_VOID +# params: +# target +# in:True out:False optional:False type:string +# label +# in:True out:False optional:False type:string +# +# TSetPropertyNum +# retType: VT_VOID +# params: +# target +# in:True out:False optional:False type:string +# property +# in:True out:False optional:False type:int +# value +# in:True out:False optional:False type:double +# +# TGetPropertyNum +# retType: double +# params: +# target +# in:True out:False optional:False type:string +# property +# in:True out:False optional:False type:int +# +# TGetPropertyAsNumber +# retType: double +# params: +# target +# in:True out:False optional:False type:string +# property +# in:True out:False optional:False type:int +# +# +# +# +# EVENTS +# -------------------- +# ReadyStateChange +# retType: VT_VOID +# params: +# newState +# in:False out:False optional:False type:int +# +# Progress +# retType: VT_VOID +# params: +# percentDone +# in:False out:False optional:False type:int +# +# FSCommand +# retType: VT_VOID +# params: +# command +# in:True out:False optional:False type:string +# args +# in:True out:False optional:False type:string +# +# +# +# diff --git a/wx/lib/flatnotebook.py b/wx/lib/flatnotebook.py new file mode 100644 index 00000000..1afdd582 --- /dev/null +++ b/wx/lib/flatnotebook.py @@ -0,0 +1,13 @@ +# ============================================================== # +# This is now just a stub, importing the real module which lives # +# under wx.lib.agw. +# ============================================================== # + +""" +Attention! FlatNotebook now lives in wx.lib.agw, together with +its friends in the Advanced Generic Widgets family. + +Please update your code! +""" + +from wx.lib.agw.flatnotebook import * \ No newline at end of file diff --git a/wx/lib/floatbar.py b/wx/lib/floatbar.py new file mode 100644 index 00000000..2fef791b --- /dev/null +++ b/wx/lib/floatbar.py @@ -0,0 +1,310 @@ +#---------------------------------------------------------------------------- +# Name: floatbar.py +# Purpose: Contains floating toolbar class +# +# Author: Bryn Keller +# +# Created: 10/4/99 +#---------------------------------------------------------------------------- +# 12/02/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o 2.5 Compatability changes +# +# 12/07/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o Added deprecation warning. +# +# 12/18/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o wxFloatBar -> FloatBar +# + +""" +NOTE: This module is *not* supported in any way. Use it however you + wish, but be warned that dealing with any consequences is + entirly up to you. + --Robin +""" + +import warnings +import wx + +warningmsg = r"""\ + +################################################\ +# This module is not supported in any way! | +# | +# See cource code for wx.lib.floatbar for more | +# information. | +################################################/ + +""" + +warnings.warn(warningmsg, DeprecationWarning, stacklevel=2) + +if wx.Platform == '__WXGTK__': + # + # For wxGTK all we have to do is set the wxTB_DOCKABLE flag + # + class FloatBar(wx.ToolBar): + def __init__(self, parent, ID, + pos = wx.DefaultPosition, + size = wx.DefaultSize, + style = 0, + name = 'toolbar'): + wx.ToolBar.__init__(self, parent, ID, pos, size, + style|wx.TB_DOCKABLE, name) + + # these other methods just become no-ops + def SetFloatable(self, float): + pass + + def IsFloating(self): + return 1 + + def GetTitle(self): + return "" + + + def SetTitle(self, title): + pass + +else: + _DOCKTHRESHOLD = 25 + + class FloatBar(wx.ToolBar): + """ + wxToolBar subclass which can be dragged off its frame and later + replaced there. Drag on the toolbar to release it, close it like + a normal window to make it return to its original + position. Programmatically, call SetFloatable(True) and then + Float(True) to float, Float(False) to dock. + """ + + def __init__(self,*_args,**_kwargs): + """ + In addition to the usual arguments, wxFloatBar accepts keyword + args of: title(string): the title that should appear on the + toolbar's frame when it is floating. floatable(bool): whether + user actions (i.e., dragging) can float the toolbar or not. + """ + args = (self,) + _args + apply(wx.ToolBar.__init__, args, _kwargs) + if _kwargs.has_key('floatable'): + self.floatable = _kwargs['floatable'] + assert type(self.floatable) == type(0) + else: + self.floatable = 0 + self.floating = 0 + if _kwargs.has_key('title'): + self.title = _kwargs['title'] + assert type(self.title) == type("") + else: + self.title = "" + self.Bind(wx.EVT_MOUSE_EVENTS, self.OnMouse) + self.parentframe = args[1] + + + def IsFloatable(self): + return self.floatable + + + def SetFloatable(self, float): + self.floatable = float + #Find the size of a title bar. + if not hasattr(self, 'titleheight'): + test = wx.MiniFrame(None, -1, "TEST") + test.SetClientSize((0,0)) + self.titleheight = test.GetSize()[1] + test.Destroy() + + + def IsFloating(self): + return self.floating + + + def Realize(self): + wx.ToolBar.Realize(self) + + + def GetTitle(self): + return self.title + + + def SetTitle(self, title): + print 'SetTitle', title + self.title = title + if self.IsFloating(): + self.floatframe.SetTitle(self.title) + + + ## def GetHome(self): + ## """ + ## Returns the frame which this toolbar will return to when + ## docked, or the parent if currently docked. + ## """ + ## if hasattr(self, 'parentframe'): + ## return self.parentframe + ## else: + ## return (self.GetParent()) + + + ## def SetHome(self, frame): + ## """ + ## Called when docked, this will remove the toolbar from its + ## current frame and attach it to another. If called when + ## floating, it will dock to the frame specified when the toolbar + ## window is closed. + ## """ + ## if self.IsFloating(): + ## self.parentframe = frame + ## self.floatframe.Reparent(frame) + ## else: + ## parent = self.GetParent() + ## self.Reparent(frame) + ## parent.SetToolBar(None) + ## size = parent.GetSize() + ## parent.SetSize(wxSize(0,0)) + ## parent.SetSize(size) + ## frame.SetToolBar(self) + ## size = frame.GetSize() + ## frame.SetSize(wxSize(0,0)) + ## frame.SetSize(size) + + + def Float(self, bool): + "Floats or docks the toolbar programmatically." + if bool: + self.parentframe = self.GetParent() + print self.title + if self.title: + useStyle = wx.DEFAULT_FRAME_STYLE + else: + useStyle = wx.THICK_FRAME + self.floatframe = wx.MiniFrame(self.parentframe, -1, self.title, + style = useStyle) + + self.Reparent(self.floatframe) + self.parentframe.SetToolBar(None) + self.floating = 1 + psize = self.parentframe.GetSize() + self.parentframe.SetSize((0,0)) + self.parentframe.SetSize(psize) + self.floatframe.SetToolBar(self) + self.oldcolor = self.GetBackgroundColour() + + w = psize[0] + h = self.GetSize()[1] + if self.title: + h = h + self.titleheight + self.floatframe.SetSize((w,h)) + self.floatframe.SetClientSize(self.GetSize()) + newpos = self.parentframe.GetPosition() + newpos.y = newpos.y + _DOCKTHRESHOLD * 2 + self.floatframe.SetPosition(newpos) + self.floatframe.Show(True) + + self.floatframe.Bind(wx.EVT_CLOSE, self.OnDock) + #self.floatframe.Bind(wx.EVT_MOVE, self.OnMove) + + else: + self.Reparent(self.parentframe) + self.parentframe.SetToolBar(self) + self.floating = 0 + self.floatframe.SetToolBar(None) + self.floatframe.Destroy() + size = self.parentframe.GetSize() + self.parentframe.SetSize((0,0)) + self.parentframe.SetSize(size) + self.SetBackgroundColour(self.oldcolor) + + + def OnDock(self, e): + self.Float(0) + if hasattr(self, 'oldpos'): + del self.oldpos + + + def OnMove(self, e): + homepos = self.parentframe.ClientToScreen((0,0)) + floatpos = self.floatframe.GetPosition() + if (abs(homepos.x - floatpos.x) < _DOCKTHRESHOLD and + abs(homepos.y - floatpos.y) < _DOCKTHRESHOLD): + self.Float(0) + #homepos = self.parentframe.GetPositionTuple() + #homepos = homepos[0], homepos[1] + self.titleheight + #floatpos = self.floatframe.GetPositionTuple() + #if abs(homepos[0] - floatpos[0]) < 35 and abs(homepos[1] - floatpos[1]) < 35: + # self._SetFauxBarVisible(True) + #else: + # self._SetFauxBarVisible(False) + + + def OnMouse(self, e): + if not self.IsFloatable(): + e.Skip() + return + + if e.ButtonDClick(1) or e.ButtonDClick(2) or e.ButtonDClick(3) or e.ButtonDown() or e.ButtonUp(): + e.Skip() + + if e.ButtonDown(): + self.CaptureMouse() + self.oldpos = (e.GetX(), e.GetY()) + + if e.Entering(): + self.oldpos = (e.GetX(), e.GetY()) + + if e.ButtonUp(): + self.ReleaseMouse() + if self.IsFloating(): + homepos = self.parentframe.ClientToScreen((0,0)) + floatpos = self.floatframe.GetPosition() + if (abs(homepos.x - floatpos.x) < _DOCKTHRESHOLD and + abs(homepos.y - floatpos.y) < _DOCKTHRESHOLD): + self.Float(0) + return + + if e.Dragging(): + if not self.IsFloating(): + self.Float(True) + self.oldpos = (e.GetX(), e.GetY()) + else: + if hasattr(self, 'oldpos'): + loc = self.floatframe.GetPosition() + pt = (loc.x - (self.oldpos[0]-e.GetX()), loc.y - (self.oldpos[1]-e.GetY())) + self.floatframe.Move(pt) + + + + def _SetFauxBarVisible(self, vis): + return + if vis: + if self.parentframe.GetToolBar() == None: + if not hasattr(self, 'nullbar'): + self.nullbar = wx.ToolBar(self.parentframe, -1) + print "Adding fauxbar." + self.nullbar.Reparent(self.parentframe) + print "Reparented." + self.parentframe.SetToolBar(self.nullbar) + print "Set toolbar" + col = wx.NamedColour("GREY") + self.nullbar.SetBackgroundColour(col) + print "Set color" + size = self.parentframe.GetSize() + self.parentframe.SetSize((0,0)) + self.parentframe.SetSize(size) + print "Set size" + else: + print self.parentframe.GetToolBar() + else: + if self.parentframe.GetToolBar() != None: + print "Removing fauxbar" + self.nullbar.Reparent(self.floatframe) + self.parentframe.SetToolBar(None) + size = self.parentframe.GetSize() + self.parentframe.SetSize((0,0)) + self.parentframe.SetSize(size) + + + diff --git a/wx/lib/foldmenu.py b/wx/lib/foldmenu.py new file mode 100644 index 00000000..1c1bc1f8 --- /dev/null +++ b/wx/lib/foldmenu.py @@ -0,0 +1,89 @@ +# 12/07/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o 2.5 Compatability changes +# + +import wx +from wx.lib.evtmgr import eventManager + +class FoldOutWindow(wx.PopupWindow): + def __init__(self,parent,style=0): + wx.PopupWindow.__init__(self,parent,style) + self.SetAutoLayout(True) + self.sizer=wx.BoxSizer(wx.HORIZONTAL) + self.SetSizer(self.sizer, deleteOld=False) + self.handlers={} + self.InitColors() + self.inWindow=False + self.Bind(wx.EVT_ENTER_WINDOW, self.evEnter) + self.Bind(wx.EVT_LEAVE_WINDOW, self.evLeave) + + def InitColors(self): + faceClr = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW) + self.SetBackgroundColour(faceClr) + + def AddButton(self,bitmap,handler=None): + id=wx.NewId() + btn=wx.BitmapButton(self,id,bitmap) + self.sizer.Add(btn, 1, wx.ALIGN_CENTER|wx.ALL|wx.EXPAND, 2) + self.Bind(wx.EVT_BUTTON, self.OnBtnClick, btn) + self.sizer.Fit(self) + self.Layout() + + if handler: + self.handlers[id]=handler + + return id + + def Popup(self): + if not self.IsShown(): + self.Show() + + def OnBtnClick(self,event): + id=event.GetEventObject().GetId() + + if self.handlers.has_key(id): + self.handlers[id](event) + + self.Hide() + self.inWindow=False + event.Skip() + + def evEnter(self,event): + self.inWindow=True + self.rect=self.GetRect() + event.Skip() + + def evLeave(self,event): + if self.inWindow: + if not self.rect.Inside(self.ClientToScreen(event.GetPosition())): + self.Hide() + + event.Skip() + + + + + +class FoldOutMenu(wx.BitmapButton): + def __init__(self,parent,id,bitmap,pos = wx.DefaultPosition, + size = wx.DefaultSize, style = wx.BU_AUTODRAW, + validator = wx.DefaultValidator, name = "button"): + + wx.BitmapButton.__init__(self, parent, id, bitmap, pos, size, style, + validator, name) + + self.parent=parent + self.parent.Bind(wx.EVT_BUTTON, self.click, self) + self.popwin=FoldOutWindow(self.parent) + + def AddButton(self,bitmap,handler=None): + return self.popwin.AddButton(bitmap,handler=handler) + + def click(self,event): + pos=self.GetPosition() + sz=self.GetSize() + pos.x=pos.x+sz.width + pos.y=pos.y+sz.height/2 + self.popwin.Position(pos,sz) + self.popwin.Popup() diff --git a/wx/lib/foldpanelbar.py b/wx/lib/foldpanelbar.py new file mode 100644 index 00000000..20d44165 --- /dev/null +++ b/wx/lib/foldpanelbar.py @@ -0,0 +1,13 @@ +# ============================================================== # +# This is now just a stub, importing the real module which lives # +# under wx.lib.agw. +# ============================================================== # + +""" +Attention! FoldPanelBar now lives in wx.lib.agw, together with +its friends in the Advanced Generic Widgets family. + +Please update your code! +""" + +from wx.lib.agw.foldpanelbar import * \ No newline at end of file diff --git a/wx/lib/gestures.py b/wx/lib/gestures.py new file mode 100644 index 00000000..6d7278b5 --- /dev/null +++ b/wx/lib/gestures.py @@ -0,0 +1,310 @@ +#Mouse Gestures + +#Version 0.0.1 + +#By Daniel Pozmanter +#drpython@bluebottle.com + +#Released under the terms of the wxWindows License. + +""" +This is a class to add Mouse Gestures to a program. +It can be used in two ways: + +1. Automatic: + Automatically runs mouse gestures. + You need to set the gestures, and their associated actions, + as well as the Mouse Button/Modifiers to use. + +2. Manual: + Same as above, but you do not need to set the mouse button/modifiers. + You can launch this from events as you wish. + +An example is provided in the demo. +The parent window is where the mouse events will be recorded. +(So if you want to record them in a pop up window, use manual mode, +and set the pop up as the parent). + +Start() starts recording mouse movement. +End() stops the recording, compiles all the gestures into a list, +and looks through the registered gestures to find a match. +The first matchs associated action is then run. + +The marginoferror is how much to forgive when calculating movement: +If the margin is 25, then movement less than 25 pixels will not be detected. + +Recognized: L, R, U, D, 1, 3, 7, 9 + +Styles: Manual (Automatic By Default), DisplayNumbersForDiagonals (Off By Default). +Not Yet Implemented + +The criteria for a direction is as follows: +x in a row. (Where x is the WobbleTolerance). +So if the WobbleTolerance is 9 +'URUUUUUUUUUUUUUUURUURUUUU1' is Up. + +The higher this number, the less sensitive this class is. +So the more likely something like 1L will translate to 1. + +This is good, since the mouse does tend to wobble somewhat, +and a higher number allows for this. + +To change this, use SetWobbleTolerance + +Also, to help with recognition of a diagonal versus +a vey messy straight line, if the greater absolute value +is not greater than twice the lesser, only the grater value +is counted. + +In automatic mode, EVT_MOUSE_EVENTS is used. +This allows the user to change the mouse button/modifiers at runtime. +""" + +########################################### + +''' +Changelog: +0.0.1: Treats a mouse leaving event as mouse up. + (Bug Report, Thanks Peter Damoc). + + +0.0.0: Initial Release. +''' + +########################################### +#ToDo: + +#Fully Implement Manual Mode + +#Add "Ends With": AddGestureEndsWith(self, gesture, action, args) +#Add "Starts With": AddGestuteStartsWith(self, gesture, action, args) + +#For better control of when the gesture starts and stops, +#use manual mode. +#At the moment, you need to Bind the OnMouseMotion event if you want to use +#manual mode. + +import wx + +class MouseGestures: + def __init__(self, parent, Auto=True, MouseButton=wx.MOUSE_BTN_MIDDLE): + self.parent = parent + + self.gestures = [] + self.actions = [] + self.actionarguments = [] + + self.mousebutton = MouseButton + self.modifiers = [] + + self.recording = False + + self.lastposition = (-1, -1) + + self.pen = wx.Pen(wx.Colour(0, 144, 255), 5) + + self.dc = wx.ScreenDC() + self.dc.SetPen(self.pen) + + self.showgesture = False + + self.wobbletolerance = 7 + + self.rawgesture = '' + + self.SetAuto(Auto) + + def _check_modifiers(self, event): + '''Internal: Returns True if all needed modifiers are down + for the given event.''' + if len(self.modifiers) > 0: + good = True + if wx.WXK_CONTROL in self.modifiers: + good = good and event.ControlDown() + if wx.WXK_SHIFT in self.modifiers: + good = good and event.ShiftDown() + if wx.WXK_ALT in self.modifiers: + good = good and event.AltDown() + return good + return True + + def AddGesture(self, gesture, action, *args): + '''Registers a gesture, and an associated function, with any arguments needed.''' + #Make Sure not a duplicate: + self.RemoveGesture(gesture) + + self.gestures.append(gesture) + self.actions.append(action) + self.actionarguments.append(args) + + def DoAction(self, gesture): + '''If the gesture is in the array of registered gestures, run the associated function.''' + if gesture in self.gestures: + i = self.gestures.index(gesture) + apply(self.actions[i], self.actionarguments[i]) + + def End(self): + '''Stops recording the points to create the mouse gesture from, + and creates the mouse gesture, returns the result as a string.''' + self.recording = False + + #Figure out the gestures (Look for occurances of 5 in a row or more): + + tempstring = '0' + possiblechange = '0' + + directions = '' + + for g in self.rawgesture: + l = len(tempstring) + if g != tempstring[l - 1]: + if g == possiblechange: + tempstring = g + g + else: + possiblechange = g + else: + tempstring += g + if len(tempstring) >= self.wobbletolerance: + ld = len(directions) + if ld > 0: + if directions[ld - 1] != g: + directions += g + else: + directions += g + tempstring = '0' + + if self.showgesture: + self.parent.Refresh() + + return directions + + def GetDirection(self, point1, point2): + '''Gets the direction between two points.''' + #point1 is the old point + #point2 is current + + x1, y1 = point1 + x2, y2 = point2 + + #(Negative = Left, Up) + #(Positive = Right, Down) + + horizontal = x2 - x1 + vertical = y2 - y1 + + horizontalchange = abs(horizontal) > 0 + verticalchange = abs(vertical) > 0 + + if horizontalchange and verticalchange: + ah = abs(horizontal) + av = abs(vertical) + if ah > av: + if (ah / av) > 2: + vertical = 0 + verticalchange = False + elif av > ah: + if (av / ah) > 2: + horizontal = 0 + horizontalchange = False + + if horizontalchange and verticalchange: + #Diagonal + if (horizontal > 0) and (vertical > 0): + return '3' + elif (horizontal > 0) and (vertical < 0): + return '9' + elif (horizontal < 0) and (vertical > 0): + return '1' + else: + return '7' + else: + #Straight Line + if horizontalchange: + if horizontal > 0: + return 'R' + else: + return 'L' + else: + if vertical > 0: + return 'D' + else: + return 'U' + + def GetRecording(self): + '''Returns whether or not Gesture Recording has started.''' + return self.recording + + def OnMotion(self, event): + '''Internal. Used if Start() has been run''' + if self.recording: + currentposition = event.GetPosition() + if self.lastposition != (-1, -1): + self.rawgesture += self.GetDirection(self.lastposition, currentposition) + if self.showgesture: + #Draw it! + px1, py1 = self.parent.ClientToScreen(self.lastposition) + px2, py2 = self.parent.ClientToScreen(currentposition) + self.dc.DrawLine(px1, py1, px2, py2) + + self.lastposition = currentposition + + event.Skip() + + def OnMouseEvent(self, event): + '''Internal. Used in Auto Mode.''' + if event.ButtonDown(self.mousebutton) and self._check_modifiers(event): + self.Start() + elif (event.ButtonUp(self.mousebutton) or event.Leaving()) and self.GetRecording(): + result = self.End() + self.DoAction(result) + event.Skip() + + def RemoveGesture(self, gesture): + '''Removes a gesture, and its associated action''' + if gesture in self.gestures: + i = self.gestures.index(gesture) + + del self.gestures[i] + del self.actions[i] + del self.actionarguments[i] + + def SetAuto(self, auto): + '''Warning: Once auto is set, it stays set, unless you manually use UnBind''' + if auto: + self.parent.Bind(wx.EVT_MOUSE_EVENTS, self.OnMouseEvent) + self.parent.Bind(wx.EVT_MOTION, self.OnMotion) + + def SetGesturePen(self, pen): + '''Sets the wx pen used to visually represent each gesture''' + self.pen = pen + self.dc.SetPen(self.pen) + + def SetGesturePen(self, colour, width): + '''Sets the colour and width of the line drawn to visually represent each gesture''' + self.pen = wx.Pen(colour, width) + self.dc.SetPen(self.pen) + + def SetGesturesVisible(self, vis): + '''Sets whether a line is drawn to visually represent each gesture''' + self.showgesture = vis + + def SetModifiers(self, modifiers=[]): + '''Takes an array of wx Key constants (Control, Shift, and/or Alt). + Leave empty to unset all modifiers.''' + self.modifiers = modifiers + + def SetMouseButton(self, mousebutton): + '''Takes the wx constant for the target mousebutton''' + self.mousebutton = mousebutton + + def SetWobbleTolerance(self, wobbletolerance): + '''Sets just how much wobble this class can take!''' + self.WobbleTolerance = wobbletolerance + + def Start(self): + '''Starts recording the points to create the mouse gesture from''' + self.recording = True + self.rawgesture = '' + self.lastposition = (-1, -1) + if self.showgesture: + self.parent.Refresh() diff --git a/wx/lib/graphics.py b/wx/lib/graphics.py new file mode 100644 index 00000000..aef35b5f --- /dev/null +++ b/wx/lib/graphics.py @@ -0,0 +1,1706 @@ +#---------------------------------------------------------------------- +# Name: wx.lib.graphics +# Purpose: A wx.GraphicsContext-like API implemented using wx.lib.wxcairo +# +# Author: Robin Dunn +# +# Created: 15-Sept-2008 +# RCS-ID: $Id$ +# Copyright: (c) 2008 by Total Control Software +# Licence: wxWindows license +#---------------------------------------------------------------------- + +""" +This module implements an API similar to wx.GraphicsContext and the +related classes. In this case the implementation for all platforms is +done using Cairo, via the wx.lib.wxcairo glue module. + +Why do this? Why not just use wx.GraphicsContext everywhere? Using +Cairo on every platform enables us to more easily be totally +consistent on all platforms. Implementing it in Python means that it +is easy to fill in the gaps in functionality with features of Cairo +that GraphicsContext may not provide, like converting text to a path, +using compositing operators, or being able to provide an +implementation for things like context.Clear(). + +Why not just use Cairo directly? There may be times when you do want +to use wx.GrpahicsContext, so being able to share code between that +and this implementation is nice. Also, I like the class hierarchy and +API exposed by the wx.GraphicsContext classes a little better than +Cairo's. +""" + +import cairo +import math + +import wx +import wx.lib.wxcairo + + + +# Other ideas: +# 1. TextToPath (or maybe make this part of the Path class +# 3. Relative moves, lines, curves, etc. +# 5. maybe expose cairo_paint, cairo_paint_with_alpha, cairo_mask? + +#--------------------------------------------------------------------------- +# Image surface formats + +FORMAT_ARGB32 = cairo.FORMAT_ARGB32 +FORMAT_RGB24 = cairo.FORMAT_RGB24 +FORMAT_A8 = cairo.FORMAT_A8 +FORMAT_A1 = cairo.FORMAT_A1 + + +#--------------------------------------------------------------------------- +# Compositing operators. See http://cairographics.org/operators + +# clear destination layer (bounded) +OPERATOR_CLEAR = cairo.OPERATOR_CLEAR + +# replace destination layer (bounded) +OPERATOR_SOURCE = cairo.OPERATOR_SOURCE + +# draw source layer on top of destination layer (bounded) +OPERATOR_OVER = cairo.OPERATOR_OVER + +# draw source where there was destination content (unbounded) +OPERATOR_IN = cairo.OPERATOR_IN + +# draw source where there was no destination content (unbounded) +OPERATOR_OUT = cairo.OPERATOR_OUT + +# draw source on top of destination content and only there +OPERATOR_ATOP = cairo.OPERATOR_ATOP + +# ignore the source +OPERATOR_DEST = cairo.OPERATOR_DEST + +# draw destination on top of source +OPERATOR_DEST_OVER = cairo.OPERATOR_DEST_OVER + +# leave destination only where there was source content (unbounded) +OPERATOR_DEST_IN = cairo.OPERATOR_DEST_IN + +# leave destination only where there was no source content +OPERATOR_DEST_OUT = cairo.OPERATOR_DEST_OUT + +# leave destination on top of source content and only there (unbounded) +OPERATOR_DEST_ATOP = cairo.OPERATOR_DEST_ATOP + +# source and destination are shown where there is only one of them +OPERATOR_XOR = cairo.OPERATOR_XOR + +# source and destination layers are accumulated +OPERATOR_ADD = cairo.OPERATOR_ADD + +# like over, but assuming source and dest are disjoint geometries +OPERATOR_SATURATE = cairo.OPERATOR_SATURATE + + + +#--------------------------------------------------------------------------- +# Anti-alias modes. Note that according to the Cairo docs none of the +# current backends support the the SUBPIXEL mode. + +# Use the default antialiasing for the subsystem and target device +ANTIALIAS_DEFAULT = cairo.ANTIALIAS_DEFAULT + +# Use a bilevel alpha mask +ANTIALIAS_NONE = cairo.ANTIALIAS_NONE + +# Perform single-color antialiasing (using shades of gray for black +# text on a white background, for example). +ANTIALIAS_GRAY = cairo.ANTIALIAS_GRAY + +# Perform antialiasing by taking advantage of the order of subpixel +# elements on devices such as LCD panels +ANTIALIAS_SUBPIXEL = cairo.ANTIALIAS_SUBPIXEL + + + +#--------------------------------------------------------------------------- +# A decorator that makes creating properties a little cleaner and simpler + +def Property( function ): + return property( **function() ) + +#--------------------------------------------------------------------------- + +NullGraphicsPen = None +NullGraphicsBrush = None +NullGraphicsFont = None +NullGraphicsMatrix = None +NullGraphicsPath = None + + +class GraphicsObject(object): + # This probably isn't needed at all anymore since we'll just use + # None insead of the Null objects, but we'll keep it anyway in + # case it's needed to help write compatible code. + def IsNull(self): + return False + + +#--------------------------------------------------------------------------- + +class GraphicsPen(GraphicsObject): + """ + A Pen is used to define the properties of how a stroke is drawn. + """ + _capMap = { wx.CAP_BUTT : cairo.LINE_CAP_BUTT, + wx.CAP_ROUND : cairo.LINE_CAP_ROUND, + wx.CAP_PROJECTING : cairo.LINE_CAP_SQUARE } + + _joinMap = { wx.JOIN_BEVEL : cairo.LINE_JOIN_BEVEL, + wx.JOIN_MITER : cairo.LINE_JOIN_MITER, + wx.JOIN_ROUND : cairo.LINE_JOIN_ROUND } + + + def __init__(self, colour=wx.BLACK, width=1, style=wx.SOLID): + GraphicsObject.__init__(self) + self._colour = _makeColour(colour) + self._width = width + self._style = style + self._cap = wx.CAP_ROUND + self._dashes = [] + self._join = wx.JOIN_ROUND + self._stipple = None + self._pattern = None + + + @staticmethod + def CreateFromPen(pen): + """Convert a wx.Pen to a GraphicsPen""" + assert isinstance(pen, wx.Pen) + p = GraphicsPen(pen.Colour, pen.Width, pen.Style) + p._cap = pen.Cap + p._dashes = pen.Dashes + p._join = pen.Join + return p + + + @staticmethod + def CreateFromPattern(pattern, width=1): + """ + Create a Pen directly from a Cairo Pattern object. This is + similar to using a stipple bitmap, but saves a step, and + patterns can include gradients, etc. + """ + p = GraphicsPen(wx.BLACK, width, wx.STIPPLE) + p._pattern = pattern + return p + + + @Property + def Colour(): + def fget(self): + return self._colour + def fset(self, value): + self._colour = value + return locals() + + @Property + def Width(): + def fget(self): + return self._width + def fset(self, value): + self._width = value + return locals() + + @Property + def Style(): + def fget(self): + return self._style + def fset(self, value): + self._style = value + return locals() + + @Property + def Cap(): + def fget(self): + return self._cap + def fset(self, value): + self._cap = value + return locals() + + @Property + def Dashes(): + def fget(self): + return self._dashes + def fset(self, value): + self._dashes = value + return locals() + + @Property + def Join(): + def fget(self): + return self._join + def fset(self, value): + self._join = value + return locals() + + @Property + def Stipple(): + def fget(self): + return self._stipple + def fset(self, value): + self._stipple = value + self._pattern = None + return locals() + + @Property + def Pattern(): + def fget(self): + return self._pattern + def fset(self, value): + self._pattern = value + return locals() + + + + def Apply(self, ctx): + # set up the context with this pen's parameters + ctx = ctx.GetNativeContext() + ctx.set_line_width(self._width) + ctx.set_line_cap(self._capMap[self._cap]) + ctx.set_line_join(self._joinMap[self._join]) + ctx.set_dash([]) + + if self._style == wx.SOLID: + ctx.set_source_rgba( *_colourToValues(self._colour) ) + + elif self._style == wx.STIPPLE: + if not self._pattern and self._stipple: + # make a pattern from the stipple bitmap + img = wx.lib.wxcairo.ImageSurfaceFromBitmap(self._stipple) + self._pattern = cairo.SurfacePattern(img) + self._pattern.set_extend(cairo.EXTEND_REPEAT) + ctx.set_source(self._pattern) + + elif self._style == wx.USER_DASH: + ctx.set_source_rgba( *_colourToValues(self._colour) ) + ctx.set_dash(self._dashes) + + elif self._style in [wx.DOT, wx.DOT_DASH, wx.LONG_DASH, wx.SHORT_DASH]: + ctx.set_source_rgba( *_colourToValues(self._colour) ) + ctx.set_dash( _stdDashes(self._style, self._width) ) + + elif self._style in [wx.BDIAGONAL_HATCH, wx.CROSSDIAG_HATCH, wx.FDIAGONAL_HATCH, + wx.CROSS_HATCH, wx.HORIZONTAL_HATCH, wx.VERTICAL_HATCH]: + pass # TODO make a stock pattern... + + +#--------------------------------------------------------------------------- + +class GraphicsBrush(GraphicsObject): + """ + A Brush is used to define how fills are painted. They can have + either a solid fill (colors with or without alpha), a stipple + created from a wx.Bitmap, or a cairo Pattern object. + """ + + def __init__(self, colour=wx.BLACK, style=wx.SOLID): + self._colour = _makeColour(colour) + self._style = style + self._stipple = None + self._pattern = None + + + @staticmethod + def CreateFromBrush(brush): + """Converts a wx.Brush to a GraphicsBrush""" + assert isinstance(brush, wx.Brush) + b = GraphicsBrush(brush.Colour, brush.Style) + if brush.Style == wx.STIPPLE: + b._stipple = brush.Stipple + else: + b._stipple = None + return b + + + @staticmethod + def CreateFromPattern(pattern): + """ + Create a Brush directly from a Cairo Pattern object. This is + similar to using a stipple bitmap, but saves a step, and + patterns can include gradients, etc. + """ + b = GraphicsBrush(style=wx.STIPPLE) + b._pattern = pattern + return b + + + @Property + def Colour(): + def fget(self): + return self._colour + def fset(self, value): + self._colour = value + return locals() + + @Property + def Style(): + def fget(self): + return self._style + def fset(self, value): + self._style = value + return locals() + + @Property + def Stipple(): + def fget(self): + return self._stipple + def fset(self, value): + self._stipple = value + self._pattern = None + return locals() + + + @Property + def Pattern(): + def fget(self): + return self._pattern + def fset(self, value): + self._pattern = value + return locals() + + + def Apply(self, ctx): + ctx = ctx.GetNativeContext() + + if self._style == wx.SOLID: + ctx.set_source_rgba( *_colourToValues(self._colour) ) + + elif self._style == wx.STIPPLE: + if not self._pattern and self._stipple: + # make a pattern from the stipple bitmap + img = wx.lib.wxcairo.ImageSurfaceFromBitmap(self._stipple) + self._pattern = cairo.SurfacePattern(img) + self._pattern.set_extend(cairo.EXTEND_REPEAT) + ctx.set_source(self._pattern) + +#--------------------------------------------------------------------------- + +class GraphicsFont(GraphicsObject): + """ + """ + def __init__(self): + # TODO: Should we be able to create a GrpahicsFont from other + # properties, or will it always be via a wx.Font? What about + # creating from a cairo.FontFace or cairo.ScaledFont? + self._font = None + self._colour = None + self._pointSize = None + self._fontface = None + # To remain consistent with the GC API a color is associated + # with the font, and nothing else. Since this is Cairo and + # it's easy to do, we'll also allow a brush to be used... + self._brush = None + + + def IsNull(self): + return self._font is None + + + @staticmethod + def CreateFromFont(font, colour=None): + f = GraphicsFont() + f._font = font + f._colour = _makeColour(colour) + f._pointSize = font.GetPointSize() + f._fontface = wx.lib.wxcairo.FontFaceFromFont(font) + return f + + + @Property + def Colour(): + def fget(self): + return self._colour + def fset(self, value): + self._colour = value + return locals() + + @Property + def PointSize(): + def fget(self): + return self._pointSize + def fset(self, value): + self._pointSize = value + return locals() + + @Property + def Brush(): + def fget(self): + return self._brush + def fset(self, value): + self._brush = value + return locals() + + + def Apply(self, ctx, colour): + nctx = ctx.GetNativeContext() + if self._brush is not None: + self._brush.Apply(ctx) + else: + if colour is None: colour = wx.BLACK + nctx.set_source_rgba( *_colourToValues(colour) ) + nctx.set_font_face(self._fontface) + nctx.set_font_size(self._pointSize) + + +#--------------------------------------------------------------------------- + +class GraphicsBitmap(GraphicsObject): + """ + A GraphicsBitmap is a wrapper around a cairo ImageSurface. It can + be used as a source for drawing images, or as a target of drawing + operations. + """ + def __init__(self, width=-1, height=-1, format=FORMAT_ARGB32): + """Create either a NULL GraphicsBitmap or an empty one if a size is given""" + self._surface = None + if width > 0 and height > 0: + self._surface = cairo.ImageSurface(format, width, height) + + + def IsNull(self): + return self._surface is None + + + @staticmethod + def CreateFromBitmap(bitmap): + """Create a GraphicsBitmap from a wx.Bitmap""" + b = GraphicsBitmap() + b._surface = wx.lib.wxcairo.ImageSurfaceFromBitmap(bitmap) + return b + + + @staticmethod + def CreateFromPNG(filename): + """Create a GraphicsBitmap from a PNG file""" + b = GraphicsBitmap() + b._surface = cairo.ImageSurface.create_from_png(filename) + return b + + + @staticmethod + def CreateFromSurface(surface): + """Use an existing cairo ImageSurface as a GraphicsBitmap""" + b = GraphicsBitmap() + b._surface = surface + return b + + + @staticmethod + def CreateFromBuffer(buffer, width, height, + format=FORMAT_ARGB32, stride=-1): + """ + Creates a GraphicsBitmap that uses the given buffer object as + the pixel storage. This means that the current contents of + the buffer will be the initial state of the bitmap, and + anything drawn to this surface will be stored in the given + buffer. + """ + b = GraphicsBitmap() + if stride == -1: + try: + stride = cairo.ImageSurface.format_stride_for_width(format, width) + except AttributeError: + stride = width * 4 + b._surface = cairo.ImageSurface.create_for_data( + buffer, format, width, height, stride) + + # save a reference to the buffer to ensure that it lives as + # long as this object does + b._buffer = buffer + return b + + + @Property + def Width(): + def fget(self): + return self._surface.get_width() + return locals() + + @Property + def Height(): + def fget(self): + return self._surface.get_height() + return locals() + + @Property + def Size(): + def fget(self): + return (self.Width, self.Height) + return locals() + + + @Property + def Format(): + def fget(self): + return self._surface.get_format() + return locals() + + @Property + def Stride(): + def fget(self): + return self._surface.get_stride() + return locals() + + @Property + def Surface(): + def fget(self): + return self._surface + return locals() + + +#--------------------------------------------------------------------------- + +class GraphicsMatrix(GraphicsObject): + """ + A matrix holds an affine transformations, such as a scale, + rotation, shear, or a combination of these, and is used to convert + between different coordinante spaces. + """ + def __init__(self): + self._matrix = cairo.Matrix() + + + def Set(self, a=1.0, b=0.0, c=0.0, d=1.0, tx=0.0, ty=0.0): + """Set the componenets of the matrix by value, default values + are the identity matrix.""" + self._matrix = cairo.Matrix(a, b, c, d, tx, ty) + + + def Get(self): + """Return the component values of the matrix as a tuple.""" + return tuple(self._matrix) + + + def GetNativeMatrix(self): + return self._matrix + + + def Concat(self, matrix): + """Concatenates the matrix passed with the current matrix.""" + self._matrix = self._matrix * matrix._matrix + return self + + + def Invert(self): + """Inverts the matrix.""" + self._matrix.invert() + return self + + + def IsEqual(self, matrix): + """Returns True if the elements of the transformation matricies are equal.""" + return self._matrix == matrix._matrix + + + def IsIdentity(): + """Returns True if this is the identity matrix.""" + return self._matrix == cairo.Matrix() + + + def Rotate(self, angle): + """Rotates the matrix in radians""" + self._matrix.rotate(angle) + return self + + + def Scale(self, xScale, yScale): + """Scale the matrix""" + self._matrix.scale(xScale, yScale) + return self + + + def Translate(self, dx, dy): + """Translate the metrix. This shifts the origin.""" + self._matrix.translate(dx, dy) + return self + + + def TransformPoint(self, x, y): + """Applies this matrix to a point and returns the result""" + return self._matrix.transform_point(x, y) + + + def TransformDistance(self, dx, dy): + """ + Applies this matrix to a distance (ie. performs all transforms + except translations.) + """ + return self._matrix.transform_distance(dx, dy) + + + def Clone(self): + m = GraphicsMatrix() + m.Set(*self.Get()) + return m + +#--------------------------------------------------------------------------- + +class GraphicsPath(GraphicsObject): + """ + A GraphicsPath is a representaion of a geometric path, essentially + a collection of lines and curves. Paths can be used to define + areas to be stroked and filled on a GraphicsContext. + """ + def __init__(self): + # A path is essentially just a context that we use just for + # collecting path moves, lines, and curves in order to apply + # them to the real context. So we'll use a 1x1 image surface + # for the backend, since we won't ever actually use it for + # rendering in this context. + surface = cairo.ImageSurface(FORMAT_ARGB32, 1, 1) + self._pathContext = cairo.Context(surface) + + + def AddArc(self, x, y, radius, startAngle, endAngle, clockwise=True): + """ + Adds an arc of a circle centering at (x,y) with radius, from + startAngle to endAngle. + """ + # clockwise means positive in our system (y pointing downwards) + if clockwise or endAngle-startAngle >= 2*math.pi: + self._pathContext.arc(x, y, radius, startAngle, endAngle) + else: + self._pathContext.arc_negative(x, y, radius, startAngle, endAngle) + return self + + + def AddArcToPoint(self, x1, y1 , x2, y2, radius ): + """ + Adds a an arc to two tangents connecting (current) to (x1,y1) + and (x1,y1) to (x2,y2), also a straight line from (current) to + (x1,y1) + """ + current = wx.Point2D(*self.GetCurrentPoint()) + p1 = wx.Point2D(x1, y1) + p2 = wx.Point2D(x2, y2) + + v1 = current - p1 + v1.Normalize() + v2 = p2 - p1 + v2.Normalize() + + alpha = v1.GetVectorAngle() - v2.GetVectorAngle() + if alpha < 0: + alpha = 360 + alpha + alpha = math.radians(alpha) + + dist = radius / math.sin(alpha/2) * math.cos(alpha/2) + + # calculate tangential points + t1 = (v1 * dist) + p1 + t2 = (v2 * dist) + p1 + + nv1 = wx.Point2D(*v1.Get()) + nv1.SetVectorAngle(v1.GetVectorAngle() - 90) + c = t1 + nv1 * radius + + a1 = v1.GetVectorAngle() + 90 + a2 = v2.GetVectorAngle() - 90 + + self.AddLineToPoint(t1.x, t1.y) + self.AddArc(c.x, c.y, radius, math.radians(a1), math.radians(a2), True) + self.AddLineToPoint(p2.x, p2.y) + return self + + + def AddCircle(self, x, y, radius): + """ + Appends a new closed sub-path as a circle around (x,y). + """ + self.MoveToPoint(x + radius, y) + self.AddArc( x, y, radius, 0, 2*math.pi, False) + self.CloseSubpath() + return self + + + def AddCurveToPoint(self, cx1, cy1, cx2, cy2, x, y): + """ + Adds a cubic Bezier curve from the current point, using two + control points and an end point. + """ + self._pathContext.curve_to(cx1, cy1, cx2, cy2, x, y) + return self + + + def AddEllipse(self, x, y, w, h): + """ + Appends an elipse fitting into the given rectangle as a closed sub-path. + """ + rw = w / 2.0 + rh = h / 2.0 + xc = x + rw + yc = y + rh + m = GraphicsMatrix() + m.Translate(xc, yc) + m.Scale(rw / rh, 1.0) + p = GraphicsPath() + p.AddCircle(0,0, rh) + p.Transform(m) + self.AddPath(p) + return self + + + def AddLineToPoint(self, x, y): + """ + Adds a straight line from the current point to (x,y) + """ + self._pathContext.line_to(x, y) + return self + + + def AddPath(self, path): + """ + Appends the given path to this path. + """ + self._pathContext.append_path(path.GetNativePath()) + return self + + + def AddQuadCurveToPoint(self, cx, cy, x, y): + """ + Adds a quadratic Bexier curve from the current point, using a + control point and an end point. + """ + # calculate using degree elevation to a cubic bezier + start = wx.Point2D() + start.x, start.y = self.GetCurrentPoint() + end = wx.Point2D(x, y) + c = wx.Point2D(cx, cy) + c1 = start * (1/3.0) + c * (2/3.0) + c2 = c * (2/3.0) + end * (1/3.0) + self.AddCurveToPoint(c1.x, c1.y, c2.x, c2.y, x, y); + return self + + + def AddRectangle(self, x, y, w, h): + """ + Adds a new rectanlge as a closed sub-path. + """ + self._pathContext.rectangle(x, y, w, h) + return self + + + def AddRoundedRectangle(self, x, y, w, h, radius): + """ + Adds a new rounded rectanlge as a closed sub-path. + """ + if radius == 0: + self.AddRectangle(x,y,w,h) + else: + self.MoveToPoint( x + w, y + h / 2.0) + self.AddArcToPoint(x + w, y + h, x + w / 2.0, y + h, radius) + self.AddArcToPoint(x, y + h, x, y + h / 2.0, radius) + self.AddArcToPoint(x, y , x + w / 2.0, y, radius) + self.AddArcToPoint(x + w, y, x + w, y + h / 2.0, radius) + self.CloseSubpath() + return self + + + def CloseSubpath(self): + """ + Adds a line segment to the path from the current point to the + beginning of the current sub-path, and closes this sub-path. + """ + self._pathContext.close_path() + return self + + + def Contains(self, x, y, fillStyle=wx.ODDEVEN_RULE): + """ + Returns True if the point lies within the path. + """ + d = { wx.WINDING_RULE : cairo.FILL_RULE_WINDING, + wx.ODDEVEN_RULE : cairo.FILL_RULE_EVEN_ODD } + rule = d[fillStyle] + self._pathContext.set_fill_rule(rule) + return self._pathContext.in_stroke(x,y) or self._pathContext.in_fill(x,y) + + + def GetCurrentPoint(self): + """ + Gets the current point of the path, which is conceptually the + final point reached by the last path operation. + """ + return self._pathContext.get_current_point() + + + def GetNativePath(self): + """ + Returns the path as a cairo.Path object. + """ + return self._pathContext.copy_path() + + + def MoveToPoint(self, x, y): + """ + Begins a new sub-path at (x,y) by moving the "current point" there. + """ + self._pathContext.move_to(x, y) + return self + + + def Transform(self, matrix): + """ + Transforms each point in this path by the matirx + """ + # as we don't have a true path object, we have to apply the + # inverse matrix to the context + # TODO: should we clone the matrix before inverting it? + m = matrix.GetNativeMatrix() + m.invert() + self._pathContext.transform(m) + return self + + + def Clone(self): + """ + Return a new path initialized with the current contents of this path. + """ + p = GraphicsPath() + p.AddPath(self) + return p + + + def GetBox(self): + """ + Return the bounding box enclosing all points on this path. + """ + x1,y1,x2,y2 = self._pathContext.stroke_extents() + if x2 < x1: + x = x2 + w = x1 - x2 + else: + x = x1 + w = x2 - x1 + + if y2 < y1: + y = y2 + h = y1 - y2 + else: + y = y1 + h = y2 - y1 + return (x, y, w, h) + + +#--------------------------------------------------------------------------- + +class GraphicsGradientStop(object): + """ + This class represents a single color-stop in a gradient brush. The + position is a floating poitn value between zero and 1.0 which represents + the distance between the gradient's starting point and ending point. + """ + def __init__(self, colour=wx.TransparentColour, pos=0.0): + self.SetColour(colour) + self.SetPosition(pos) + + def GetColour(self): + return self._colour + def SetColour(self, value): + value = _makeColour(value) + assert isinstance(value, wx.Colour) + self._colour = value + Colour = property(GetColour, SetColour) + + + def GetPosition(self): + return self._pos + def SetPosition(self, value): + assert value >= 0.0 and value <= 1.0 + self._pos = value + Position = property(GetPosition, SetPosition) + + + +class GraphicsGradientStops(object): + """ + An ordered collection of gradient color stops for a gradient brush. There + is always at least the starting stop and the ending stop in the collection. + """ + def __init__(self, startColour=wx.TransparentColour, + endColour=wx.TransparentColour): + self._stops = list() + self.Add(startColour, 0.0) + self.Add(endColour, 1.0) + + + def Add(self, *args): + """ + Add a new color to the collection. args may be either a gradient stop, + or a colour and position. + """ + if len(args) == 2: + col, pos = args + stop = GraphicsGradientStop(col, pos) + elif len(args) == 1: + stop = args[0] + else: + raise ValueError, "Invalid parameters passed to Add" + assert isinstance(stop, GraphicsGradientStop) + + self._stops.append(stop) + self._stops.sort(key=lambda x: x.Position) + + + def GetCount(self): + return len(self._stops) + Count = property(GetCount) + def __len__(self): + return self.GetCount() + + + def Item(self, n): + return self._stops[n] + def __getitem__(self, n): + return self._stops[n] + + + def GetStartColour(self): + return self._stops[0].Colour + def SetStartColour(self, col): + self._stops[0].Colour = col + StartColour = property(GetStartColour, SetStartColour) + + + def GetEndColour(self): + return self._stops[-1].Colour + def SetEndColour(self, col): + self._stops[-1].Colour = col + EndColour = property(GetEndColour, SetEndColour) + + +#--------------------------------------------------------------------------- + +class GraphicsContext(GraphicsObject): + """ + The GraphicsContext is the object which facilitates drawing to a surface. + """ + def __init__(self, context=None, size=None): + self._context = context + self._pen = None + self._brush = None + self._font = None + self._fontColour = None + self._layerOpacities = [] + self._width = 10000.0 + self._height = 10000.0 + if size is not None: + self._width, self._height = size + + + def IsNull(self): + return self._context is None + + + @staticmethod + def Create(dc): + # TODO: Support creating directly from a wx.Window too. + assert isinstance(dc, wx.DC) + ctx = wx.lib.wxcairo.ContextFromDC(dc) + return GraphicsContext(ctx, dc.GetSize()) + + @staticmethod + def CreateFromNative(cairoContext): + return GraphicsContext(cairoContext) + + @staticmethod + def CreateMeasuringContext(): + """ + If you need a temporary context just to quickly measure some + text extents, or etc. then using this function will be a + little less expensive than creating a real DC for it. + """ + surface = cairo.ImageSurface(FORMAT_ARGB32, 1, 1) + ctx = cairo.Context(surface) + return GraphicsContext(ctx, + (surface.get_width(), surface.get_height())) + + @staticmethod + def CreateFromSurface(surface): + """ + Wrap a context around the given cairo Surface. Note that a + GraphicsBitmap contains a cairo ImageSurface which is + accessible via the Surface property. + """ + return GraphicsContext(cairo.Context(surface), + (surface.get_width(), surface.get_height())) + + + @Property + def Context(): + def fget(self): + return self._context + return locals() + + + # Our implementation is able to create these things direclty, but + # we'll keep them here too for compatibility with wx.GraphicsContext. + + def CreateBrush(self, brush): + """ + Create a brush from a wx.Brush. + """ + return GraphicsBrush.CreateFromBrush(brush) + + def CreateFont(self, font, colour=None): + """ + Create a font from a wx.Font + """ + return GraphicsFont.CreateFromFont(font, colour) + + + def CreateLinearGradientBrush(self, x1, y1, x2, y2, *args): + """ + Creates a native brush having a linear gradient, starting at (x1,y1) + to (x2,y2) with the given boundary colors or the specified stops. + + The `*args` can be either a GraphicsGradientStops or just two colours to + be used as the starting and ending gradient colours. + """ + if len(args) ==1: + stops = args[0] + elif len(args) == 2: + c1 = _makeColour(c1) + c2 = _makeColour(c2) + stops = GraphicsGradientStops(c1, c2) + else: + raise ValueError, "Invalid args passed to CreateLinearGradientBrush" + + pattern = cairo.LinearGradient(x1, y1, x2, y2) + for stop in stops: + pattern.add_color_stop_rgba(stop.Position, *_colourToValues(stop.Colour)) + return GraphicsBrush.CreateFromPattern(pattern) + + + def CreateRadialGradientBrush(self, xo, yo, xc, yc, radius, *args): + """ + Creates a native brush, having a radial gradient originating at point + (xo,yo) and ending on a circle around (xc,yc) with the given radius; + the colours may be specified by just the two extremes or the full + array of gradient stops. + + The `*args` can be either a GraphicsGradientStops or just two colours to + be used as the starting and ending gradient colours. + """ + if len(args) ==1: + stops = args[0] + elif len(args) == 2: + oColour = _makeColour(oColour) + cColour = _makeColour(cColour) + stops = GraphicsGradientStops(oColour, cColour) + else: + raise ValueError, "Invalid args passed to CreateLinearGradientBrush" + + pattern = cairo.RadialGradient(xo, yo, 0.0, xc, yc, radius) + for stop in stops: + pattern.add_color_stop_rgba(stop.Position, *_colourToValues(stop.Colour)) + return GraphicsBrush.CreateFromPattern(pattern) + + + def CreateMatrix(self, a=1.0, b=0, c=0, d=1.0, tx=0, ty=0): + """ + Create a new matrix object. + """ + m = GraphicsMatrix() + m.Set(a, b, c, d, tx, ty) + return m + + def CreatePath(self): + """ + Create a new path obejct. + """ + return GraphicsPath() + + def CreatePen(self, pen): + """ + Create a new pen from a wx.Pen. + """ + return GraphicsPen.CreateFromPen(pen) + + + + + def PushState(self): + """ + Makes a copy of the current state of the context (ie the + transformation matrix) and saves it on an internal stack of + saved states. The saved state will be restored when PopState + is called. + """ + self._context.save() + + + def PopState(self): + """ + Restore the most recently saved state which was saved with + PushState. + """ + self._context.restore() + + + def Clip(self, x, y, w, h): + """ + Adds the rectangle to the current clipping region. The + clipping region causes drawing operations to be limited to the + clipped areas of the context. + """ + p = GraphicsPath() + p.AddRectangle(x, y, w, h) + self._context.append_path(p.GetNativePath()) + self._context.clip() + + + def ClipRegion(self, region): + """ + Adds the wx.Region to the current clipping region. + """ + p = GraphicsPath() + ri = wx.RegionIterator(region) + while ri: + rect = ri.GetRect() + p.AddRectangle( *rect ) + ri.Next() + self._context.append_path(p.GetNativePath()) + self._context.clip() + + + def ResetClip(self): + """ + Resets the clipping region to the original shape of the context. + """ + self._context.reset_clip() + + + def GetNativeContext(self): + return self._context + + + # Since DC logical functions are conceptually different than + # compositing operators don't pretend they are the same thing, or + # try ot implement them using the compositing operators. + def GetLogicalFunction(self): + raise NotImplementedError("See GetCompositingOperator") + def SetLogicalFunction(self, function): + raise NotImplementedError("See SetCompositingOperator") + + + def Translate(self, dx, dy): + """ + Modifies the current transformation matrix by translating the + user-space origin by (dx, dy). + """ + self._context.translate(dx, dy) + + + def Scale(self, xScale, yScale): + """ + Modifies the current transformation matrix by translating the + user-space axes by xScale and yScale. + """ + self._context.scale(xScale, yScale) + + + def Rotate(self, angle): + """ + Modifies the current transformation matrix by rotating the + user-space axes by angle radians. + """ + self._context.rotate(angle) + + + def ConcatTransform(self, matrix): + """ + Modifies the current transformation matrix by applying matrix + as an additional transformation. + """ + self._context.transform(matrix.GetNativeMatrix()) + + + def SetTransform(self, matrix): + """ + Set the context's current transformation matrix to matrix. + """ + self._context.set_matrix(matrix.GetNativeMatrix()) + + + def GetTransform(self): + """ + Returns the context's current transformation matrix. + """ + gm = GraphicsMatrix() + gm.Set( *tuple(self._context.get_matrix()) ) + return gm + + + def SetPen(self, pen): + """ + Set the pen to be used for stroking lines in future drawing + operations. Either a wx.Pen or a GraphicsPen object may be + used. + """ + if isinstance(pen, wx.Pen): + if not pen.Ok() or pen.Style == wx.TRANSPARENT: + pen = None + else: + pen = GraphicsPen.CreateFromPen(pen) + self._pen = pen + + def GetPen(self): return self._pen + Pen = property(GetPen, SetPen) + + + def SetBrush(self, brush): + """ + Set the brush to be used for filling shapes in future drawing + operations. Either a wx.Brush or a GraphicsBrush object may + be used. + """ + if isinstance(brush, wx.Brush): + if not brush.Ok() or brush.Style == wx.TRANSPARENT: + brush = None + else: + brush = GraphicsBrush.CreateFromBrush(brush) + self._brush = brush + + def GetBrush(self): return self._brush + Brush = property(GetBrush, SetBrush) + + + def SetFont(self, font, colour=None): + """ + Sets the font to be used for drawing text. Either a wx.Font + or a GrpahicsFont may be used. + """ + if isinstance(font, wx.Font): + font = GraphicsFont.CreateFromFont(font, colour) + self._font = font + if colour is not None: + self._fontColour = _makeColour(colour) + else: + self._fontColour = font._colour + + def GetFont(self): return (self._font, self._fontColour) + def _SetFont(self, *both): self.SetFont(*both) + Font = property(GetFont, _SetFont) + + + def StrokePath(self, path): + """ + Strokes the path (draws the lines) using the current pen. + """ + if self._pen: + offset = _OffsetHelper(self) + self._context.append_path(path.GetNativePath()) + self._pen.Apply(self) + self._context.stroke() + + + def FillPath(self, path, fillStyle=wx.ODDEVEN_RULE): + """ + Fills the path using the current brush. + """ + if self._brush: + offset = _OffsetHelper(self) + self._context.append_path(path.GetNativePath()) + self._brush.Apply(self) + d = { wx.WINDING_RULE : cairo.FILL_RULE_WINDING, + wx.ODDEVEN_RULE : cairo.FILL_RULE_EVEN_ODD } + rule = d[fillStyle] + self._context.set_fill_rule(rule) + self._context.fill() + + + def DrawPath(self, path, fillStyle=wx.ODDEVEN_RULE): + """ + Draws the path by first filling it and then stroking it. + """ + # TODO: this could be optimized by moving the stroke and fill + # code here and only loading the path once. + self.FillPath(path, fillStyle) + self.StrokePath(path) + + + def DrawText(self, text, x, y, backgroundBrush=None): + """ + Draw the text at (x,y) using the current font. If + backgroundBrush is set then it is used to fill the rectangle + behind the text. + """ + if backgroundBrush: + formerBrush = self._brush + formerPen = self._pen + self.SetBrush(backgroundBrush) + self.SetPen(None) + width, height = self.GetTextExtent(text) + path = GraphicsPath() + path.AddRectangle(x, y, width, height) + self.FillPath(path) + self._DrawText(text, x, y) + self.SetBrush(formerBrush) + self.SetPen(formerPen) + + else: + self._DrawText(text, x, y) + + + def _DrawText(self, text, x, y, angle=None): + # helper used by DrawText and DrawRotatedText + if angle is not None: + self.PushState() + self.Translate(x, y) + self.Rotate(-angle) + x = y = 0 + + self._font.Apply(self, self._fontColour) + # Cairo's x,y for drawing text is at the baseline, so we need to adjust + # the position we move to by the ascent. + fe = self._context.font_extents() + ascent = fe[0] + self._context.move_to( x, y + ascent ) + self._context.show_text(text) + + if angle is not None: + self.PopState() + + + def DrawRotatedText(self, text, x, y, angle, backgroundBrush=None): + """ + Draw the text at (x,y) using the current font and rotated + angle radians. If backgroundBrush is set then it is used to + fill the rectangle behind the text. + """ + if backgroundBrush: + formerBrush = self._brush + formerPen = self._pen + self.SetBrush(backgroundBrush) + self.SetPen(None) + width, height = self.GetTextExtent(text) + path = GraphicsPath() + path.AddRectangle(0, 0, width, height) + self.PushState() + self.Translate(x, y) + self.Rotate(-angle) + self.FillPath(path) + self.PopState() + self._DrawText(text, x, y, angle) + self.SetBrush(formerBrush) + self.SetPen(formerPen) + + else: + self._DrawText(text, x, y, angle) + + + def GetFullTextExtent(self, text): + """ + Returns the (width, height, descent, externalLeading) of the + text using the current font. + """ + if not text: + return (0,0,0,0) + + self._font.Apply(self, self._fontColour) + + te = self._context.text_extents(text) + width = te[2] + + fe = self._context.font_extents() + height = fe[2] + descent = fe[1] + ascent = fe[0] + externalLeading = max(0, height - (ascent + descent)) + + return (width, height, descent, externalLeading) + + + def GetTextExtent(self, text): + """ + Returns the (width, height) of the text using the current + font. + """ + (width, height, descent, externalLeading) = self.GetFullTextExtent(text) + return (width, height) + + + def GetPartialTextExtents(self, text): + raise NotImplementedError("TODO") + + + def DrawBitmap(self, bmp, x, y, w=-1, h=-1): + """ + Draw the bitmap at (x,y). If the width and height parameters + are passed then the bitmap is scaled to fit that size. Either + a wx.Bitmap or a GraphicsBitmap may be used. + """ + if isinstance(bmp, wx.Bitmap): + bmp = GraphicsBitmap.CreateFromBitmap(bmp) + + # In case we're scaling the image by using a width and height + # different than the bitmap's size, create a pattern + # transformation on the surface and draw the transformed + # pattern. + self.PushState() + pattern = cairo.SurfacePattern(bmp.Surface) + + bw, bh = bmp.Size + if w == -1: w = bw + if h == -1: h = bh + scaleX = w / float(bw) + scaleY = h / float(bh) + + self._context.translate(x, y) + self._context.scale(scaleX, scaleY) + self._context.set_source(pattern) + + # use the original size here since the context is scaled already... + self._context.rectangle(0, 0, bw, bh) + # fill the rectangle with the pattern + self._context.fill() + + self.PopState() + + + def DrawIcon(self, icon, x, y, w=-1, h=-1): + raise NotImplementedError("TODO") + + + def StrokeLine(self, x1, y1, x2, y2): + """ + Strokes a single line using the current pen. + """ + path = GraphicsPath() + path.MoveToPoint(x1, y1) + path.AddLineToPoint(x2, y2) + self.StrokePath(path) + + + def StrokeLines(self, points): + """ + Stroke a series of conencted lines using the current pen. + Points is a sequence of points or 2-tuples, and lines are + drawn from point to point through the end of the sequence. + """ + path = GraphicsPath() + x, y = points[0] + path.MoveToPoint(x, y) + for point in points[1:]: + x, y = point + path.AddLineToPoint(x, y) + self.StrokePath(path) + + + def StrokeLineSegments(self, beginPoints, endPoints): + """ + Stroke a series of lines using the current pen. For each line + the begin point is taken from the beginPoints sequence and the + ending point is taken from the endPoints sequence. + """ + path = GraphicsPath() + for begin, end in zip(beginPoints, endPoints): + path.MoveToPoint(begin[0], begin[1]) + path.AddLineToPoint(end[0], end[1]) + self.StrokePath(path) + + + def DrawLines(self, points, fillStyle=wx.ODDEVEN_RULE): + """ + Stroke and fill a series of connected lines using the current + pen and current brush. + """ + path = GraphicsPath() + x, y = points[0] + path.MoveToPoint(x, y) + for point in points[1:]: + x, y = point + path.AddLineToPoint(x, y) + self.DrawPath(path, fillStyle) + + + def DrawRectangle(self, x, y, w, h): + """ + Stroke and fill a rectangle using the current pen and current + brush. + """ + path = GraphicsPath() + path.AddRectangle(x, y, w, h) + self.DrawPath(path) + + + def DrawEllipse(self, x, y, w, h): + """ + Stroke and fill an elipse that fits in the given rectangle, + using the current pen and current brush. + """ + path = GraphicsPath() + path.AddEllipse(x, y, w, h) + self.DrawPath(path) + + + def DrawRoundedRectangle(self, x, y, w, h, radius): + """ + Stroke and fill a rounded rectangle using the current pen and + current brush. + """ + path = GraphicsPath() + path.AddRoundedRectangle(x, y, w, h, radius) + self.DrawPath(path) + + + + def GetCompositingOperator(self): + """ + Returns the current compositing operator for the context. + """ + return self._context.get_operator() + + def SetCompositingOperator(self, op): + """ + Sets the compositin operator to be used for all drawing + operations. The default operator is OPERATOR_OVER. + """ + return self._context.set_operator(op) + + + def GetAntialiasMode(self): + """ + Returns the current antialias mode. + """ + return self._context.get_antialias() + + def SetAntialiasMode(self, mode=ANTIALIAS_DEFAULT): + """ + Set the antialiasing mode of the rasterizer used for drawing + shapes. This value is a hint, and a particular backend may or + may not support a particular value. + """ + self._context.set_antialias(mode) + + + def BeginLayer(self, opacity): + """ + Redirects future rendering to a temorary context. See `EndLayer`. + """ + self._layerOpacities.append(opacity) + self._context.push_group() + + + def EndLayer(self): + """ + Composites the drawing done on the temporary context created + in `BeginLayer` back into the main context, using the opacity + specified for the layer. + """ + opacity = self._layerOpacities.pop() + self._context.pop_group_to_source() + self._context.paint_with_alpha(opacity) + + + def GetSize(self): + return (self._width, self._height) + Size = property(GetSize) + + + # Some things not in wx.GraphicsContext (yet) + + def DrawCircle(self, x, y, radius): + """ + Stroke and fill a circle centered at (x,y) with the given + radius, using the current pen and brush. + """ + path = GraphicsPath() + path.AddCircle(x, y, radius) + self.DrawPath(path) + + + def ClipPath(self, path): + """ + Set the clip region to the path. + """ + self._context.append_path(path.GetNativePath()) + self._context.clip() + + + def Clear(self, colour=None): + """ + Clear the context using the given color or the currently set brush. + """ + if colour is not None: + brush = GraphicsBrush(colour) + elif self._brush is None: + brush = GraphicsBrush(wx.WHITE) + else: + brush = self._brush + + self.PushState() + op = self._context.get_operator() + self._context.set_operator(cairo.OPERATOR_SOURCE) + self._context.reset_clip() + + brush.Apply(self) + self._context.paint() + + self._context.set_operator(op) + self.PopState() + + +#--------------------------------------------------------------------------- +# Utility functions + +def _makeColour(colour): + # make a wx.Colour from any of the allowed typemaps (string, tuple, + # etc.) + if isinstance(colour, (basestring, tuple)): + return wx.NamedColour(colour) + else: + return colour + + +def _colourToValues(c): + # Convert wx.Colour components to a set of values between 0 and 1 + return tuple( [x/255.0 for x in c.Get(True)] ) + + + +class _OffsetHelper(object): + def __init__(self, ctx): + self.ctx = ctx + self.offset = 0 + if ctx._pen: + penwidth = ctx._pen.Width + if penwidth == 0: + penwidth = 1 + self.offset = (penwidth % 2) == 1; + if self.offset: + ctx.Translate(0.5, 0.5) + + def __del__(self): + if self.offset: + self.ctx.Translate(-0.5, -0.5) + + + +def _stdDashes(style, width): + if width < 1.0: + width = 1.0 + + if style == wx.DOT: + dashes = [ width, width + 2.0] + elif style == wx.DOT_DASH: + dashes = [ 9.0, 6.0, 3.0, 3.0 ] + elif style == wx.LONG_DASH: + dashes = [ 19.0, 9.0 ] + elif style == wx.SHORT_DASH: + dashes = [ 9.0, 6.0 ] + + return dashes + + +#--------------------------------------------------------------------------- + + diff --git a/wx/lib/gridmovers.py b/wx/lib/gridmovers.py new file mode 100644 index 00000000..d3abd35d --- /dev/null +++ b/wx/lib/gridmovers.py @@ -0,0 +1,487 @@ +#---------------------------------------------------------------------------- +# Name: GridColMover.py +# Purpose: Grid Column Mover Extension +# +# Author: Gerrit van Dyk (email: gerritvd@decillion.net) +# +# Version 0.1 +# Date: Nov 19, 2002 +# RCS-ID: $Id$ +# Licence: wxWindows license +#---------------------------------------------------------------------------- +# 12/07/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o 2.5 Compatability changes +# +# 12/18/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o wxGridColMoveEvent -> GridColMoveEvent +# o wxGridRowMoveEvent -> GridRowMoveEvent +# o wxGridColMover -> GridColMover +# o wxGridRowMover -> GridRowMover +# + + +import wx +import wx.grid + +#---------------------------------------------------------------------------- +# event class and macros +# +# New style 12/7/03 +# + +wxEVT_COMMAND_GRID_COL_MOVE = wx.NewEventType() +wxEVT_COMMAND_GRID_ROW_MOVE = wx.NewEventType() + +EVT_GRID_COL_MOVE = wx.PyEventBinder(wxEVT_COMMAND_GRID_COL_MOVE, 1) +EVT_GRID_ROW_MOVE = wx.PyEventBinder(wxEVT_COMMAND_GRID_ROW_MOVE, 1) + +#---------------------------------------------------------------------------- + +class GridColMoveEvent(wx.PyCommandEvent): + def __init__(self, id, dCol, bCol): + wx.PyCommandEvent.__init__(self, id = id) + self.SetEventType(wxEVT_COMMAND_GRID_COL_MOVE) + self.moveColumn = dCol + self.beforeColumn = bCol + + def GetMoveColumn(self): + return self.moveColumn + + def GetBeforeColumn(self): + return self.beforeColumn + + +class GridRowMoveEvent(wx.PyCommandEvent): + def __init__(self, id, dRow, bRow): + wx.PyCommandEvent.__init__(self,id = id) + self.SetEventType(wxEVT_COMMAND_GRID_ROW_MOVE) + self.moveRow = dRow + self.beforeRow = bRow + + def GetMoveRow(self): + return self.moveRow + + def GetBeforeRow(self): + return self.beforeRow + + +#---------------------------------------------------------------------------- +# graft new methods into the wxGrid class + +def _ColToRect(self,col): + if self.GetNumberRows() > 0: + rect = self.CellToRect(0,col) + else: + rect = wx.Rect() + rect.height = self.GetColLabelSize() + rect.width = self.GetColSize(col) + + for cCol in range(0,col): + rect.x += self.GetColSize(cCol) + + rect.y = self.GetGridColLabelWindow().GetPosition()[1] + return rect + +wx.grid.Grid.ColToRect = _ColToRect + + +def _RowToRect(self,row): + if self.GetNumberCols() > 0: + rect = self.CellToRect(row,0) + else: + rect = wx.Rect() + rect.width = self.GetRowLabelSize() + rect.height = self.GetRowSize(row) + + for cRow in range(0,row): + rect.y += self.GetRowSize(cRow) + + rect.x = self.GetGridRowLabelWindow().GetPosition()[0] + return rect + +wx.grid.Grid.RowToRect = _RowToRect + + +#---------------------------------------------------------------------------- + +class ColDragWindow(wx.Window): + def __init__(self,parent,image,dragCol): + wx.Window.__init__(self,parent,-1, style=wx.SIMPLE_BORDER) + self.image = image + self.SetSize((self.image.GetWidth(),self.image.GetHeight())) + self.ux = parent.GetScrollPixelsPerUnit()[0] + self.moveColumn = dragCol + + self.Bind(wx.EVT_PAINT, self.OnPaint) + + def DisplayAt(self,pos,y): + x = self.GetPositionTuple()[0] + if x == pos: + self.Refresh() # Need to display insertion point + else: + self.MoveXY(pos,y) + + def GetMoveColumn(self): + return self.moveColumn + + def _GetInsertionInfo(self): + parent = self.GetParent() + sx = parent.GetViewStart()[0] * self.ux + sx -= parent.GetRowLabelSize() + x = self.GetPosition()[0] + w = self.GetSize()[0] + sCol = parent.XToCol(x + sx) + eCol = parent.XToCol(x + w + sx) + iPos = xPos = xCol = 99999 + centerPos = x + sx + (w / 2) + + for col in range(sCol,eCol + 1): + cx = parent.ColToRect(col)[0] + + if abs(cx - centerPos) < iPos: + iPos = abs(cx - centerPos) + xCol = col + xPos = cx + + if xCol < 0 or xCol > parent.GetNumberCols(): + xCol = parent.GetNumberCols() + + return (xPos - sx - x,xCol) + + def GetInsertionColumn(self): + return self._GetInsertionInfo()[1] + + def GetInsertionPos(self): + return self._GetInsertionInfo()[0] + + def OnPaint(self,evt): + dc = wx.PaintDC(self) + w,h = self.GetSize() + dc.DrawBitmap(self.image, 0,0) + dc.SetPen(wx.Pen(wx.BLACK,1,wx.SOLID)) + dc.SetBrush(wx.TRANSPARENT_BRUSH) + dc.DrawRectangle(0,0, w,h) + iPos = self.GetInsertionPos() + dc.DrawLine(iPos,h - 10, iPos,h) + + + + +class RowDragWindow(wx.Window): + def __init__(self,parent,image,dragRow): + wx.Window.__init__(self,parent,-1, style=wx.SIMPLE_BORDER) + self.image = image + self.SetSize((self.image.GetWidth(),self.image.GetHeight())) + self.uy = parent.GetScrollPixelsPerUnit()[1] + self.moveRow = dragRow + + self.Bind(wx.EVT_PAINT, self.OnPaint) + + def DisplayAt(self,x,pos): + y = self.GetPosition()[1] + if y == pos: + self.Refresh() # Need to display insertion point + else: + self.MoveXY(x,pos) + + def GetMoveRow(self): + return self.moveRow + + def _GetInsertionInfo(self): + parent = self.GetParent() + sy = parent.GetViewStart()[1] * self.uy + sy -= parent.GetColLabelSize() + y = self.GetPosition()[1] + h = self.GetSize()[1] + sRow = parent.YToRow(y + sy) + eRow = parent.YToRow(y + h + sy) + iPos = yPos = yRow = 99999 + centerPos = y + sy + (h / 2) + + for row in range(sRow,eRow + 1): + cy = parent.RowToRect(row)[1] + + if abs(cy - centerPos) < iPos: + iPos = abs(cy - centerPos) + yRow = row + yPos = cy + + if yRow < 0 or yRow > parent.GetNumberRows(): + yRow = parent.GetNumberRows() + + return (yPos - sy - y,yRow) + + def GetInsertionRow(self): + return self._GetInsertionInfo()[1] + + def GetInsertionPos(self): + return self._GetInsertionInfo()[0] + + def OnPaint(self,evt): + dc = wx.PaintDC(self) + w,h = self.GetSize() + dc.DrawBitmap(self.image, 0,0) + dc.SetPen(wx.Pen(wx.BLACK,1,wx.SOLID)) + dc.SetBrush(wx.TRANSPARENT_BRUSH) + dc.DrawRectangle(0,0, w,h) + iPos = self.GetInsertionPos() + dc.DrawLine(w - 10,iPos, w,iPos) + +#---------------------------------------------------------------------------- + +class GridColMover(wx.EvtHandler): + def __init__(self,grid): + wx.EvtHandler.__init__(self) + + self.grid = grid + self.lwin = grid.GetGridColLabelWindow() + self.lwin.PushEventHandler(self) + self.colWin = None + self.ux = self.grid.GetScrollPixelsPerUnit()[0] + self.startX = -10 + self.cellX = 0 + self.didMove = False + self.isDragging = False + + self.Bind(wx.EVT_MOTION, self.OnMouseMove) + self.Bind(wx.EVT_LEFT_DOWN, self.OnPress) + self.Bind(wx.EVT_LEFT_UP, self.OnRelease) + + def OnMouseMove(self,evt): + if not self.isDragging: + evt.Skip() + else: + _rlSize = self.grid.GetRowLabelSize() + if abs(self.startX - evt.X) >= 3 \ + and abs(evt.X - self.lastX) >= 3: + self.lastX = evt.X + self.didMove = True + sx,y = self.grid.GetViewStart() + w,h = self.lwin.GetClientSize() + x = sx * self.ux + + if (evt.X + x) < x: + x = evt.X + x + elif evt.X > w: + x += evt.X - w + + if x < 1: x = 0 + else: x /= self.ux + + if x != sx: + if wx.Platform == '__WXMSW__': + self.colWin.Show(False) + + self.grid.Scroll(x,y) + + x,y = self.lwin.ClientToScreenXY(evt.X,0) + x,y = self.grid.ScreenToClientXY(x,y) + + if not self.colWin.IsShown(): + self.colWin.Show(True) + + px = x - self.cellX + + if px < 0 + _rlSize: px = 0 + _rlSize + + if px > w - self.colWin.GetSize()[0] + _rlSize: + px = w - self.colWin.GetSize()[0] + _rlSize + + self.colWin.DisplayAt(px,y) + return + + + def OnPress(self,evt): + self.startX = self.lastX = evt.X + _rlSize = self.grid.GetRowLabelSize() + sx = self.grid.GetViewStart()[0] * self.ux + sx -= _rlSize + px,py = self.lwin.ClientToScreenXY(evt.X,evt.Y) + px,py = self.grid.ScreenToClientXY(px,py) + + if self.grid.XToEdgeOfCol(px + sx) != wx.NOT_FOUND: + evt.Skip() + return + + self.isDragging = True + self.didMove = False + col = self.grid.XToCol(px + sx) + rect = self.grid.ColToRect(col) + self.cellX = px + sx - rect.x + size = self.lwin.GetSize() + rect.y = 0 + rect.x -= sx + _rlSize + rect.height = size[1] + colImg = self._CaptureImage(rect) + self.colWin = ColDragWindow(self.grid,colImg,col) + self.colWin.Show(False) + self.lwin.CaptureMouse() + evt.Skip() + + def OnRelease(self,evt): + if self.isDragging: + self.lwin.ReleaseMouse() + self.colWin.Show(False) + self.isDragging = False + + if not self.didMove: + px = self.lwin.ClientToScreenXY(self.startX,0)[0] + px = self.grid.ScreenToClientXY(px,0)[0] + sx = self.grid.GetViewStart()[0] * self.ux + sx -= self.grid.GetRowLabelSize() + col = self.grid.XToCol(px+sx) + + if col != wx.NOT_FOUND: + self.grid.SelectCol(col,evt.m_controlDown) + + return + else: + bCol = self.colWin.GetInsertionColumn() + dCol = self.colWin.GetMoveColumn() + wx.PostEvent(self, + GridColMoveEvent(self.grid.GetId(), dCol, bCol)) + + self.colWin.Destroy() + evt.Skip() + + def _CaptureImage(self,rect): + bmp = wx.EmptyBitmap(rect.width,rect.height) + memdc = wx.MemoryDC() + memdc.SelectObject(bmp) + dc = wx.WindowDC(self.lwin) + memdc.Blit(0,0, rect.width, rect.height, dc, rect.x, rect.y) + memdc.SelectObject(wx.NullBitmap) + return bmp + + +class GridRowMover(wx.EvtHandler): + def __init__(self,grid): + wx.EvtHandler.__init__(self) + + self.grid = grid + self.lwin = grid.GetGridRowLabelWindow() + self.lwin.PushEventHandler(self) + self.rowWin = None + self.uy = self.grid.GetScrollPixelsPerUnit()[1] + self.startY = -10 + self.cellY = 0 + self.didMove = False + self.isDragging = False + + self.Bind(wx.EVT_MOTION, self.OnMouseMove) + self.Bind(wx.EVT_LEFT_DOWN, self.OnPress) + self.Bind(wx.EVT_LEFT_UP, self.OnRelease) + + def OnMouseMove(self,evt): + if not self.isDragging: + evt.Skip() + else: + _clSize = self.grid.GetColLabelSize() + if abs(self.startY - evt.Y) >= 3 \ + and abs(evt.Y - self.lastY) >= 3: + self.lastY = evt.Y + self.didMove = True + x,sy = self.grid.GetViewStart() + w,h = self.lwin.GetClientSizeTuple() + y = sy * self.uy + + if (evt.Y + y) < y: + y = evt.Y + y + elif evt.Y > h: + y += evt.Y - h + + if y < 1: + y = 0 + else: + y /= self.uy + + if y != sy: + if wx.Platform == '__WXMSW__': + self.rowWin.Show(False) + + self.grid.Scroll(x,y) + + x,y = self.lwin.ClientToScreenXY(0,evt.Y) + x,y = self.grid.ScreenToClientXY(x,y) + + if not self.rowWin.IsShown(): + self.rowWin.Show(True) + + py = y - self.cellY + + if py < 0 + _clSize: + py = 0 + _clSize + + if py > h - self.rowWin.GetSize()[1] + _clSize: + py = h - self.rowWin.GetSize()[1] + _clSize + + self.rowWin.DisplayAt(x,py) + return + + + def OnPress(self,evt): + self.startY = self.lastY = evt.Y + _clSize = self.grid.GetColLabelSize() + sy = self.grid.GetViewStart()[1] * self.uy + sy -= _clSize + px,py = self.lwin.ClientToScreenXY(evt.X,evt.Y) + px,py = self.grid.ScreenToClientXY(px,py) + + if self.grid.YToEdgeOfRow(py + sy) != wx.NOT_FOUND: + evt.Skip() + return + + self.isDragging = True + self.didMove = False + row = self.grid.YToRow(py + sy) + rect = self.grid.RowToRect(row) + self.cellY = py + sy - rect.y + size = self.lwin.GetSize() + rect.x = 0 + rect.y -= sy + _clSize + rect.width = size[0] + rowImg = self._CaptureImage(rect) + self.rowWin = RowDragWindow(self.grid,rowImg,row) + self.rowWin.Show(False) + self.lwin.CaptureMouse() + evt.Skip() + + def OnRelease(self,evt): + if self.isDragging: + self.lwin.ReleaseMouse() + self.rowWin.Show(False) + self.isDragging = False + + if not self.didMove: + py = self.lwin.ClientToScreenXY(0,self.startY)[1] + py = self.grid.ScreenToClientXY(0,py)[1] + sy = self.grid.GetViewStart()[1] * self.uy + sy -= self.grid.GetColLabelSize() + row = self.grid.YToRow(py + sy) + + if row != wx.NOT_FOUND: + self.grid.SelectRow(row,evt.m_controlDown) + return + else: + bRow = self.rowWin.GetInsertionRow() + dRow = self.rowWin.GetMoveRow() + + wx.PostEvent(self, + GridRowMoveEvent(self.grid.GetId(), dRow, bRow)) + + self.rowWin.Destroy() + evt.Skip() + + def _CaptureImage(self,rect): + bmp = wx.EmptyBitmap(rect.width,rect.height) + memdc = wx.MemoryDC() + memdc.SelectObject(bmp) + dc = wx.WindowDC(self.lwin) + memdc.Blit(0,0, rect.width, rect.height, dc, rect.x, rect.y) + memdc.SelectObject(wx.NullBitmap) + return bmp + + +#---------------------------------------------------------------------------- diff --git a/wx/lib/grids.py b/wx/lib/grids.py new file mode 100644 index 00000000..5adfc20f --- /dev/null +++ b/wx/lib/grids.py @@ -0,0 +1,302 @@ +#---------------------------------------------------------------------- +# Name: wxPython.lib.grids +# Purpose: An example sizer derived from the C++ wxPySizer that +# sizes items in a fixed or flexible grid. +# +# Author: Robin Dunn +# +# Created: 21-Sept-1999 +# RCS-ID: $Id$ +# Copyright: (c) 1999 by Total Control Software +# Licence: wxWindows license +#---------------------------------------------------------------------- +# 12/07/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o 2.5 Compatability changes +# +# 12/20/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o In keeping with the common idiom, the sizers in this module +# have been given the 'Py' prefix to avoid confusion with the +# native sizers of the same name. However, the reverse renamer +# still has the old wx*Sizer since the whole point of the +# reverse renamer is backward compatability. +# o wxGridSizer -> PyGridSizer +# o wxFlexGridSizer -> PyFlexGridSizer +# o Deprecation warning added. +# + +""" +In this module you will find PyGridSizer and PyFlexGridSizer. Please +note that these sizers have since been ported to C++ (as wx.GridSizer +and wx.FlexGridSizer) and those versions are now exposed in the regular +wxPython wrappers. However I am also leaving them here in the library +so they can serve as an example of how to implement sizers in Python. + +PyGridSizer: Sizes and positions items such that all rows are the same +height and all columns are the same width. You can specify a gap in +pixels to be used between the rows and/or the columns. When you +create the sizer you specify the number of rows or the number of +columns and then as you add items it figures out the other dimension +automatically. Like other sizers, items can be set to fill their +available space, or to be aligned on a side, in a corner, or in the +center of the space. When the sizer is resized, all the items are +resized the same amount so all rows and all columns remain the same +size. + +PyFlexGridSizer: Derives from PyGridSizer and adds the ability for +particular rows and/or columns to be marked as growable. This means +that when the sizer changes size, the growable rows and colums are the +ones that stretch. The others remain at their initial size. +""" + + +import operator +import warnings +import wx + +warningmsg = r"""\ + +################################################\ +# THIS MODULE IS DEPRECATED | +# | +# You should use the native wx.GridSizer and | +# wx.FlexGridSizer unless there is a compelling | +# need to use this module. | +################################################/ + + +""" + +warnings.warn(warningmsg, DeprecationWarning, stacklevel=2) + + +#---------------------------------------------------------------------- + +class PyGridSizer(wx.PySizer): + def __init__(self, rows=0, cols=0, hgap=0, vgap=0): + wx.PySizer.__init__(self) + if rows == 0 and cols == 0: + raise ValueError, "rows and cols cannot both be zero" + + self.rows = rows + self.cols = cols + self.hgap = hgap + self.vgap = vgap + + + def SetRows(self, rows): + if rows == 0 and self.cols == 0: + raise ValueError, "rows and cols cannot both be zero" + self.rows = rows + + def SetColumns(self, cols): + if self.rows == 0 and cols == 0: + raise ValueError, "rows and cols cannot both be zero" + self.cols = cols + + def GetRows(self): + return self.rows + + def GetColumns(self): + return self.cols + + def SetHgap(self, hgap): + self.hgap = hgap + + def SetVgap(self, vgap): + self.vgap = vgap + + def GetHgap(self, hgap): + return self.hgap + + def GetVgap(self, vgap): + return self.vgap + + #-------------------------------------------------- + def CalcMin(self): + items = self.GetChildren() + nitems = len(items) + nrows = self.rows + ncols = self.cols + + if ncols > 0: + nrows = (nitems + ncols-1) / ncols + else: + ncols = (nitems + nrows-1) / nrows + + # Find the max width and height for any component. + w = 0 + h = 0 + for item in items: + size = item.CalcMin() + w = max(w, size.width) + h = max(h, size.height) + + return wx.Size(ncols * w + (ncols-1) * self.hgap, + nrows * h + (nrows-1) * self.vgap) + + + #-------------------------------------------------- + def RecalcSizes(self): + items = self.GetChildren() + if not items: + return + + nitems = len(items) + nrows = self.rows + ncols = self.cols + + if ncols > 0: + nrows = (nitems + ncols-1) / ncols + else: + ncols = (nitems + nrows-1) / nrows + + + sz = self.GetSize() + pt = self.GetPosition() + w = (sz.width - (ncols - 1) * self.hgap) / ncols; + h = (sz.height - (nrows - 1) * self.vgap) / nrows; + + x = pt.x + for c in range(ncols): + y = pt.y + for r in range(nrows): + i = r * ncols + c + if i < nitems: + self.SetItemBounds(items[i], x, y, w, h) + + y = y + h + self.vgap + + x = x + w + self.hgap + + + #-------------------------------------------------- + def SetItemBounds(self, item, x, y, w, h): + # calculate the item's size and position within + # its grid cell + ipt = wx.Point(x, y) + isz = item.CalcMin() + flag = item.GetFlag() + + if flag & wx.EXPAND or flag & wx.SHAPED: + isz = (w, h) + else: + if flag & wx.ALIGN_CENTER_HORIZONTAL: + ipt.x = x + (w - isz.width) / 2 + elif flag & wx.ALIGN_RIGHT: + ipt.x = x + (w - isz.width) + + if flag & wx.ALIGN_CENTER_VERTICAL: + ipt.y = y + (h - isz.height) / 2 + elif flag & wx.ALIGN_BOTTOM: + ipt.y = y + (h - isz.height) + + item.SetDimension(ipt, isz) + + +#---------------------------------------------------------------------- + + + +class PyFlexGridSizer(PyGridSizer): + def __init__(self, rows=0, cols=0, hgap=0, vgap=0): + PyGridSizer.__init__(self, rows, cols, hgap, vgap) + self.rowHeights = [] + self.colWidths = [] + self.growableRows = [] + self.growableCols = [] + + def AddGrowableRow(self, idx): + self.growableRows.append(idx) + + def AddGrowableCol(self, idx): + self.growableCols.append(idx) + + #-------------------------------------------------- + def CalcMin(self): + items = self.GetChildren() + nitems = len(items) + nrows = self.rows + ncols = self.cols + + if ncols > 0: + nrows = (nitems + ncols-1) / ncols + else: + ncols = (nitems + nrows-1) / nrows + + # Find the max width and height for any component. + self.rowHeights = [0] * nrows + self.colWidths = [0] * ncols + + for i in range(len(items)): + size = items[i].CalcMin() + row = i / ncols + col = i % ncols + self.rowHeights[row] = max(size.height, self.rowHeights[row]) + self.colWidths[col] = max(size.width, self.colWidths[col]) + + # Add up all the widths and heights + cellsWidth = reduce(operator.__add__, self.colWidths) + cellHeight = reduce(operator.__add__, self.rowHeights) + + return wx.Size(cellsWidth + (ncols-1) * self.hgap, + cellHeight + (nrows-1) * self.vgap) + + + #-------------------------------------------------- + def RecalcSizes(self): + items = self.GetChildren() + if not items: + return + + nitems = len(items) + nrows = self.rows + ncols = self.cols + + if ncols > 0: + nrows = (nitems + ncols-1) / ncols + else: + ncols = (nitems + nrows-1) / nrows + + minsz = self.CalcMin() + sz = self.GetSize() + pt = self.GetPosition() + + # Check for growables + if self.growableRows and sz.height > minsz.height: + delta = (sz.height - minsz.height) / len(self.growableRows) + for idx in self.growableRows: + self.rowHeights[idx] = self.rowHeights[idx] + delta + + if self.growableCols and sz.width > minsz.width: + delta = (sz.width - minsz.width) / len(self.growableCols) + for idx in self.growableCols: + self.colWidths[idx] = self.colWidths[idx] + delta + + # bottom right corner + sz = wx.Size(pt.x + sz.width, pt.y + sz.height) + + # Layout each cell + x = pt.x + for c in range(ncols): + y = pt.y + for r in range(nrows): + i = r * ncols + c + + if i < nitems: + w = max(0, min(self.colWidths[c], sz.width - x)) + h = max(0, min(self.rowHeights[r], sz.height - y)) + self.SetItemBounds(items[i], x, y, w, h) + + y = y + self.rowHeights[r] + self.vgap + + x = x + self.colWidths[c] + self.hgap + +#---------------------------------------------------------------------- + + + + + + diff --git a/wx/lib/hyperlink.py b/wx/lib/hyperlink.py new file mode 100644 index 00000000..dfc045c4 --- /dev/null +++ b/wx/lib/hyperlink.py @@ -0,0 +1,13 @@ +# ============================================================== # +# This is now just a stub, importing the real module which lives # +# under wx.lib.agw. +# ============================================================== # + +""" +Attention! HyperLinkCtrl now lives in wx.lib.agw, together with +its friends in the Advanced Generic Widgets family. + +Please update your code! +""" + +from wx.lib.agw.hyperlink import * \ No newline at end of file diff --git a/wx/lib/iewin.py b/wx/lib/iewin.py new file mode 100644 index 00000000..36dfe59c --- /dev/null +++ b/wx/lib/iewin.py @@ -0,0 +1,250 @@ +#---------------------------------------------------------------------- +# Name: wx.lib.iewin +# Purpose: A class that allows the use of the IE web browser +# ActiveX control +# +# Author: Robin Dunn +# +# Created: 22-March-2004 +# RCS-ID: $Id$ +# Copyright: (c) 2008 by Total Control Software +# Licence: wxWindows license +#---------------------------------------------------------------------- + +import wx +import wx.lib.activex +import comtypes.client as cc + +import sys +if not hasattr(sys, 'frozen'): + cc.GetModule('shdocvw.dll') # IWebBrowser2 and etc. +from comtypes.gen import SHDocVw + + +clsID = '{8856F961-340A-11D0-A96B-00C04FD705A2}' +progID = 'Shell.Explorer.2' + + +# Flags to be used with the RefreshPage method +REFRESH_NORMAL = 0 +REFRESH_IFEXPIRED = 1 +REFRESH_CONTINUE = 2 +REFRESH_COMPLETELY = 3 + +# Flags to be used with LoadUrl, Navigate, Navigate2 methods +NAV_OpenInNewWindow = 0x1 +NAV_NoHistory = 0x2 +NAV_NoReadFromCache = 0x4 +NAV_NoWriteToCache = 0x8 +NAV_AllowAutosearch = 0x10 +NAV_BrowserBar = 0x20 +NAV_Hyperlink = 0x40 +NAV_EnforceRestricted = 0x80, +NAV_NewWindowsManaged = 0x0100, +NAV_UntrustedForDownload = 0x0200, +NAV_TrustedForActiveX = 0x0400, +NAV_OpenInNewTab = 0x0800, +NAV_OpenInBackgroundTab = 0x1000, +NAV_KeepWordWheelText = 0x2000 + + +#---------------------------------------------------------------------- + +class IEHtmlWindow(wx.lib.activex.ActiveXCtrl): + def __init__(self, parent, id=-1, pos=wx.DefaultPosition, + size=wx.DefaultSize, style=0, name='IEHtmlWindow'): + wx.lib.activex.ActiveXCtrl.__init__(self, parent, progID, + id, pos, size, style, name) + + self._canGoBack = False + self._canGoForward = False + + + def LoadString(self, html): + """Load the html document from a string""" + if self.ctrl.Document is None: + self.LoadUrl('about:blank') + doc = self.ctrl.Document + doc.write(html) + doc.close() + + + def LoadStream(self, stream): + """ + Load the html document from a Python file-like object. + """ + if self.ctrl.Document is None: + self.LoadUrl('about:blank') + doc = self.ctrl.Document + for line in stream: + doc.write(line) + doc.close() + + + def LoadUrl(self, URL, Flags=0): + """Load the document from url.""" + return self.ctrl.Navigate2(URL, Flags) + + + def GetStringSelection(self, asHTML=True): + """ + Returns the contents of the selected portion of the document as + either html or plain text. + """ + if self.ctrl.Document is None: + return "" + if not hasattr(sys, 'frozen'): cc.GetModule('mshtml.tlb') + from comtypes.gen import MSHTML + doc = self.ctrl.Document.QueryInterface(MSHTML.IHTMLDocument2) + sel = doc.selection + range = sel.createRange() + if asHTML: + return range.htmlText + else: + return range.text + + + def GetText(self, asHTML=True): + """ + Returns the contents of the the html document as either html or plain text. + """ + if self.ctrl.Document is None: + return "" + if not hasattr(sys, 'frozen'): cc.GetModule('mshtml.tlb') + from comtypes.gen import MSHTML + doc = self.ctrl.Document.QueryInterface(MSHTML.IHTMLDocument2) + if not asHTML: + # if just fetching the text then get it from the body property + return doc.body.innerText + + # otherwise look in the all property + for idx in range(doc.all.length): + # the first item with content should be the tag and all its + # children. + item = doc.all.item(idx) + if item is None: + continue + return item.outerHTML + return "" + + + def Print(self, showDialog=False): + if showDialog: + prompt = SHDocVw.OLECMDEXECOPT_PROMPTUSER + else: + prompt = SHDocVw.OLECMDEXECOPT_DONTPROMPTUSER + self.ctrl.ExecWB(SHDocVw.OLECMDID_PRINT, prompt) + + + def PrintPreview(self): + self.ctrl.ExecWB( SHDocVw.OLECMDID_PRINTPREVIEW, + SHDocVw.OLECMDEXECOPT_DODEFAULT) + + + + def GoBack(self): + if self.CanGoBack(): + return self.ctrl.GoBack() + + def GoForward(self): + if self.CanGoForward(): + return self.ctrl.GoForward() + + def CanGoBack(self): + return self._canGoBack + + def CanGoForward(self): + return self._canGoForward + + def GoHome(self): + return self.ctrl.GoHome() + + def GoSearch(self): + return self.ctrl.GoSearch() + + def Navigate(self, URL, Flags=0, TargetFrameName=None, PostData=None, Headers=None): + return self.ctrl.Navigate2( URL, Flags, TargetFrameName, PostData, Headers) + + def RefreshPage(self, Level=REFRESH_NORMAL): + return self.ctrl.Refresh2(Level) + + def Stop(self): + return self.ctrl.Stop() + + def Quit(self): + return self.ctrl.Quit() + + + # COM Event handlers + def CommandStateChange(self, this, command, enable): + # watch the command states to know when it is possible to use + # GoBack or GoForward + if command == SHDocVw.CSC_NAVIGATEFORWARD: + self._canGoForward = enable + if command == SHDocVw.CSC_NAVIGATEBACK: + self._canGoBack = enable + + + # Getters, Setters and properties + def _get_Busy(self): + return self.ctrl.Busy + busy = property(_get_Busy, None) + + def _get_Document(self): + return self.ctrl.Document + document = property(_get_Document, None) + + def _get_LocationName(self): + return self.ctrl.LocationName + locationname = property(_get_LocationName, None) + + def _get_LocationURL(self): + return self.ctrl.LocationURL + locationurl = property(_get_LocationURL, None) + + def _get_ReadyState(self): + return self.ctrl.ReadyState + readystate = property(_get_ReadyState, None) + + def _get_Offline(self): + return self.ctrl.Offline + def _set_Offline(self, Offline): + self.ctrl.Offline = Offline + offline = property(_get_Offline, _set_Offline) + + def _get_Silent(self): + return self.ctrl.Silent + def _set_Silent(self, Silent): + self.ctrl.Silent = Silent + silent = property(_get_Silent, _set_Silent) + + def _get_RegisterAsBrowser(self): + return self.ctrl.RegisterAsBrowser + def _set_RegisterAsBrowser(self, RegisterAsBrowser): + self.ctrl.RegisterAsBrowser = RegisterAsBrowser + registerasbrowser = property(_get_RegisterAsBrowser, _set_RegisterAsBrowser) + + def _get_RegisterAsDropTarget(self): + return self.ctrl.RegisterAsDropTarget + def _set_RegisterAsDropTarget(self, RegisterAsDropTarget): + self.ctrl.RegisterAsDropTarget = RegisterAsDropTarget + registerasdroptarget = property(_get_RegisterAsDropTarget, _set_RegisterAsDropTarget) + + def _get_Type(self): + return self.ctrl.Type + type = property(_get_Type, None) + + + +if __name__ == '__main__': + app = wx.App(False) + frm = wx.Frame(None, title="AX Test Window") + + ie = IEHtmlWindow(frm) + + frm.Show() + import wx.lib.inspection + wx.lib.inspection.InspectionTool().Show() + app.MainLoop() + + diff --git a/wx/lib/iewin_old.py b/wx/lib/iewin_old.py new file mode 100644 index 00000000..53802855 --- /dev/null +++ b/wx/lib/iewin_old.py @@ -0,0 +1,895 @@ +#---------------------------------------------------------------------- +# Name: wx.lib.iewin +# Purpose: A class that allows the use of the IE web browser +# ActiveX control +# +# Author: Robin Dunn +# +# Created: 22-March-2004 +# RCS-ID: $Id: iewin.py 41669 2006-10-06 23:21:07Z RD $ +# Copyright: (c) 2004 by Total Control Software +# Licence: wxWindows license +#---------------------------------------------------------------------- + +# This module was originally generated by the +# wx.activex.GernerateAXModule class but has been tweaked somewhat as +# indicated below. + +import wx +import wx.activex + +clsID = '{8856F961-340A-11D0-A96B-00C04FD705A2}' +progID = 'Shell.Explorer.2' + + +# Flags to be used with the RefreshPage method +REFRESH_NORMAL = 0 +REFRESH_IFEXPIRED = 1 +REFRESH_CONTINUE = 2 +REFRESH_COMPLETELY = 3 + +# Flags to be used with LoadUrl, Navigate, Navigate2 methods +NAV_OpenInNewWindow = 0x1 +NAV_NoHistory = 0x2 +NAV_NoReadFromCache = 0x4 +NAV_NoWriteToCache = 0x8 +NAV_AllowAutosearch = 0x10 +NAV_BrowserBar = 0x20 +NAV_Hyperlink = 0x40 + + + +# Create eventTypes and event binders +wxEVT_StatusTextChange = wx.activex.RegisterActiveXEvent('StatusTextChange') +wxEVT_ProgressChange = wx.activex.RegisterActiveXEvent('ProgressChange') +wxEVT_CommandStateChange = wx.activex.RegisterActiveXEvent('CommandStateChange') +wxEVT_DownloadBegin = wx.activex.RegisterActiveXEvent('DownloadBegin') +wxEVT_DownloadComplete = wx.activex.RegisterActiveXEvent('DownloadComplete') +wxEVT_TitleChange = wx.activex.RegisterActiveXEvent('TitleChange') +wxEVT_PropertyChange = wx.activex.RegisterActiveXEvent('PropertyChange') +wxEVT_BeforeNavigate2 = wx.activex.RegisterActiveXEvent('BeforeNavigate2') +wxEVT_NewWindow2 = wx.activex.RegisterActiveXEvent('NewWindow2') +wxEVT_NavigateComplete2 = wx.activex.RegisterActiveXEvent('NavigateComplete2') +wxEVT_DocumentComplete = wx.activex.RegisterActiveXEvent('DocumentComplete') +wxEVT_Quit = wx.activex.RegisterActiveXEvent('OnQuit') +wxEVT_Visible = wx.activex.RegisterActiveXEvent('OnVisible') +wxEVT_ToolBar = wx.activex.RegisterActiveXEvent('OnToolBar') +wxEVT_MenuBar = wx.activex.RegisterActiveXEvent('OnMenuBar') +wxEVT_StatusBar = wx.activex.RegisterActiveXEvent('OnStatusBar') +wxEVT_FullScreen = wx.activex.RegisterActiveXEvent('OnFullScreen') +wxEVT_TheaterMode = wx.activex.RegisterActiveXEvent('OnTheaterMode') +wxEVT_WindowSetResizable = wx.activex.RegisterActiveXEvent('WindowSetResizable') +wxEVT_WindowSetLeft = wx.activex.RegisterActiveXEvent('WindowSetLeft') +wxEVT_WindowSetTop = wx.activex.RegisterActiveXEvent('WindowSetTop') +wxEVT_WindowSetWidth = wx.activex.RegisterActiveXEvent('WindowSetWidth') +wxEVT_WindowSetHeight = wx.activex.RegisterActiveXEvent('WindowSetHeight') +wxEVT_WindowClosing = wx.activex.RegisterActiveXEvent('WindowClosing') +wxEVT_ClientToHostWindow = wx.activex.RegisterActiveXEvent('ClientToHostWindow') +wxEVT_SetSecureLockIcon = wx.activex.RegisterActiveXEvent('SetSecureLockIcon') +wxEVT_FileDownload = wx.activex.RegisterActiveXEvent('FileDownload') +wxEVT_NavigateError = wx.activex.RegisterActiveXEvent('NavigateError') +wxEVT_PrintTemplateInstantiation = wx.activex.RegisterActiveXEvent('PrintTemplateInstantiation') +wxEVT_PrintTemplateTeardown = wx.activex.RegisterActiveXEvent('PrintTemplateTeardown') +wxEVT_UpdatePageStatus = wx.activex.RegisterActiveXEvent('UpdatePageStatus') +wxEVT_PrivacyImpactedStateChange = wx.activex.RegisterActiveXEvent('PrivacyImpactedStateChange') + +EVT_StatusTextChange = wx.PyEventBinder(wxEVT_StatusTextChange, 1) +EVT_ProgressChange = wx.PyEventBinder(wxEVT_ProgressChange, 1) +EVT_CommandStateChange = wx.PyEventBinder(wxEVT_CommandStateChange, 1) +EVT_DownloadBegin = wx.PyEventBinder(wxEVT_DownloadBegin, 1) +EVT_DownloadComplete = wx.PyEventBinder(wxEVT_DownloadComplete, 1) +EVT_TitleChange = wx.PyEventBinder(wxEVT_TitleChange, 1) +EVT_PropertyChange = wx.PyEventBinder(wxEVT_PropertyChange, 1) +EVT_BeforeNavigate2 = wx.PyEventBinder(wxEVT_BeforeNavigate2, 1) +EVT_NewWindow2 = wx.PyEventBinder(wxEVT_NewWindow2, 1) +EVT_NavigateComplete2 = wx.PyEventBinder(wxEVT_NavigateComplete2, 1) +EVT_DocumentComplete = wx.PyEventBinder(wxEVT_DocumentComplete, 1) +EVT_Quit = wx.PyEventBinder(wxEVT_Quit, 1) +EVT_Visible = wx.PyEventBinder(wxEVT_Visible, 1) +EVT_ToolBar = wx.PyEventBinder(wxEVT_ToolBar, 1) +EVT_MenuBar = wx.PyEventBinder(wxEVT_MenuBar, 1) +EVT_StatusBar = wx.PyEventBinder(wxEVT_StatusBar, 1) +EVT_FullScreen = wx.PyEventBinder(wxEVT_FullScreen, 1) +EVT_TheaterMode = wx.PyEventBinder(wxEVT_TheaterMode, 1) +EVT_WindowSetResizable = wx.PyEventBinder(wxEVT_WindowSetResizable, 1) +EVT_WindowSetLeft = wx.PyEventBinder(wxEVT_WindowSetLeft, 1) +EVT_WindowSetTop = wx.PyEventBinder(wxEVT_WindowSetTop, 1) +EVT_WindowSetWidth = wx.PyEventBinder(wxEVT_WindowSetWidth, 1) +EVT_WindowSetHeight = wx.PyEventBinder(wxEVT_WindowSetHeight, 1) +EVT_WindowClosing = wx.PyEventBinder(wxEVT_WindowClosing, 1) +EVT_ClientToHostWindow = wx.PyEventBinder(wxEVT_ClientToHostWindow, 1) +EVT_SetSecureLockIcon = wx.PyEventBinder(wxEVT_SetSecureLockIcon, 1) +EVT_FileDownload = wx.PyEventBinder(wxEVT_FileDownload, 1) +EVT_NavigateError = wx.PyEventBinder(wxEVT_NavigateError, 1) +EVT_PrintTemplateInstantiation = wx.PyEventBinder(wxEVT_PrintTemplateInstantiation, 1) +EVT_PrintTemplateTeardown = wx.PyEventBinder(wxEVT_PrintTemplateTeardown, 1) +EVT_UpdatePageStatus = wx.PyEventBinder(wxEVT_UpdatePageStatus, 1) +EVT_PrivacyImpactedStateChange = wx.PyEventBinder(wxEVT_PrivacyImpactedStateChange, 1) + + +# For this there are a few special methods implemented in C++ in the +# IEHtmlWindowBase class, so derive from it instead of ActiveXWindow. +class IEHtmlWindow(wx.activex.IEHtmlWindowBase): + def __init__(self, parent, id=-1, pos=wx.DefaultPosition, + size=wx.DefaultSize, style=0, name='IEHtmlWindow', ID=-1): + # in case the old 'ID' param is used as a keyword + if ID != -1: + id = ID + + wx.activex.IEHtmlWindowBase.__init__(self, parent, + wx.activex.CLSID('{8856F961-340A-11D0-A96B-00C04FD705A2}'), + id, pos, size, style, name) + + + # Methods from IEHtmlWindowBase. Redirected from here just for + # the sake of completeness... + def LoadString(self, html): + """Load the html document from a string""" + return wx.activex.IEHtmlWindowBase.LoadString(self, html) + + + def LoadStream(self, stream): + """ + Load the html document from a wx.InputStream or a Python + file-like object. + """ + return wx.activex.IEHtmlWindowBase.LoadStream(self, stream) + + + def LoadUrl(self, URL, Flags=0): + """Load the document from url.""" + return self.Navigate2(URL, Flags) + + + def GetStringSelection(self, asHTML=True): + """ + Returns the contents of the selected portion of the document as + either html or plain text. + """ + return wx.activex.IEHtmlWindowBase.GetStringSelection(self, asHTML) + + + def GetText(self, asHTML=True): + """ + Returns the contents of the the html document as either html or plain text. + """ + return wx.activex.IEHtmlWindowBase.GetText(self, asHTML) + + + def SetCharset(self, charset): + """""" + return wx.activex.IEHtmlWindowBase.SetCharset(self, charset) + + + # Methods exported by the ActiveX object + def QueryInterface(self, riid): + return self.CallAXMethod('QueryInterface', riid) + + def AddRef(self): + return self.CallAXMethod('AddRef') + + def Release(self): + return self.CallAXMethod('Release') + + def GetTypeInfoCount(self): + return self.CallAXMethod('GetTypeInfoCount') + + def GetTypeInfo(self, itinfo, lcid): + return self.CallAXMethod('GetTypeInfo', itinfo, lcid) + + def GetIDsOfNames(self, riid, rgszNames, cNames, lcid): + return self.CallAXMethod('GetIDsOfNames', riid, rgszNames, cNames, lcid) + + def Invoke(self, dispidMember, riid, lcid, wFlags, pdispparams): + return self.CallAXMethod('Invoke', dispidMember, riid, lcid, wFlags, pdispparams) + + def GoBack(self): + return self.CallAXMethod('GoBack') + + def GoForward(self): + return self.CallAXMethod('GoForward') + + def GoHome(self): + return self.CallAXMethod('GoHome') + + def GoSearch(self): + return self.CallAXMethod('GoSearch') + + # added default for Flags + def Navigate(self, URL, Flags=0, TargetFrameName=None, PostData=None, Headers=None): + return self.CallAXMethod('Navigate', URL, Flags, TargetFrameName, PostData, Headers) + + # Removed to prevent conflict with wx.Window.Refresh + #def Refresh(self): + # return self.CallAXMethod('Refresh') + + # renamed + def RefreshPage(self, Level=REFRESH_NORMAL): + return self.CallAXMethod('Refresh2', Level) + + def Stop(self): + return self.CallAXMethod('Stop') + + def Quit(self): + return self.CallAXMethod('Quit') + + def ClientToWindow(self, pcx, pcy): + return self.CallAXMethod('ClientToWindow', pcx, pcy) + + def PutProperty(self, Property, vtValue): + return self.CallAXMethod('PutProperty', Property, vtValue) + + def GetProperty(self, Property): + return self.CallAXMethod('GetProperty', Property) + + # added default for flags + def Navigate2(self, URL, Flags=0, TargetFrameName=None, PostData=None, Headers=None): + return self.CallAXMethod('Navigate2', URL, Flags, TargetFrameName, PostData, Headers) + + def QueryStatusWB(self, cmdID): + return self.CallAXMethod('QueryStatusWB', cmdID) + + def ExecWB(self, cmdID, cmdexecopt, pvaIn, pvaOut=None): + return self.CallAXMethod('ExecWB', cmdID, cmdexecopt, pvaIn, pvaOut) + + def ShowBrowserBar(self, pvaClsid, pvarShow, pvarSize=None): + return self.CallAXMethod('ShowBrowserBar', pvaClsid, pvarShow, pvarSize) + + # Getters, Setters and properties + def _get_Application(self): + return self.GetAXProp('Application') + application = property(_get_Application, None) + + def _get_Parent(self): + return self.GetAXProp('Parent') + parent = property(_get_Parent, None) + + def _get_Container(self): + return self.GetAXProp('Container') + container = property(_get_Container, None) + + def _get_Document(self): + return self.GetAXProp('Document') + document = property(_get_Document, None) + + def _get_TopLevelContainer(self): + return self.GetAXProp('TopLevelContainer') + toplevelcontainer = property(_get_TopLevelContainer, None) + + def _get_Type(self): + return self.GetAXProp('Type') + type = property(_get_Type, None) + + def _get_Left(self): + return self.GetAXProp('Left') + def _set_Left(self, Left): + self.SetAXProp('Left', Left) + left = property(_get_Left, _set_Left) + + def _get_Top(self): + return self.GetAXProp('Top') + def _set_Top(self, Top): + self.SetAXProp('Top', Top) + top = property(_get_Top, _set_Top) + + def _get_Width(self): + return self.GetAXProp('Width') + def _set_Width(self, Width): + self.SetAXProp('Width', Width) + width = property(_get_Width, _set_Width) + + def _get_Height(self): + return self.GetAXProp('Height') + def _set_Height(self, Height): + self.SetAXProp('Height', Height) + height = property(_get_Height, _set_Height) + + def _get_LocationName(self): + return self.GetAXProp('LocationName') + locationname = property(_get_LocationName, None) + + def _get_LocationURL(self): + return self.GetAXProp('LocationURL') + locationurl = property(_get_LocationURL, None) + + def _get_Busy(self): + return self.GetAXProp('Busy') + busy = property(_get_Busy, None) + + def _get_Name(self): + return self.GetAXProp('Name') + name = property(_get_Name, None) + + def _get_HWND(self): + return self.GetAXProp('HWND') + hwnd = property(_get_HWND, None) + + def _get_FullName(self): + return self.GetAXProp('FullName') + fullname = property(_get_FullName, None) + + def _get_Path(self): + return self.GetAXProp('Path') + path = property(_get_Path, None) + + def _get_Visible(self): + return self.GetAXProp('Visible') + def _set_Visible(self, Visible): + self.SetAXProp('Visible', Visible) + visible = property(_get_Visible, _set_Visible) + + def _get_StatusBar(self): + return self.GetAXProp('StatusBar') + def _set_StatusBar(self, StatusBar): + self.SetAXProp('StatusBar', StatusBar) + statusbar = property(_get_StatusBar, _set_StatusBar) + + def _get_StatusText(self): + return self.GetAXProp('StatusText') + def _set_StatusText(self, StatusText): + self.SetAXProp('StatusText', StatusText) + statustext = property(_get_StatusText, _set_StatusText) + + def _get_ToolBar(self): + return self.GetAXProp('ToolBar') + def _set_ToolBar(self, ToolBar): + self.SetAXProp('ToolBar', ToolBar) + toolbar = property(_get_ToolBar, _set_ToolBar) + + def _get_MenuBar(self): + return self.GetAXProp('MenuBar') + def _set_MenuBar(self, MenuBar): + self.SetAXProp('MenuBar', MenuBar) + menubar = property(_get_MenuBar, _set_MenuBar) + + def _get_FullScreen(self): + return self.GetAXProp('FullScreen') + def _set_FullScreen(self, FullScreen): + self.SetAXProp('FullScreen', FullScreen) + fullscreen = property(_get_FullScreen, _set_FullScreen) + + def _get_ReadyState(self): + return self.GetAXProp('ReadyState') + readystate = property(_get_ReadyState, None) + + def _get_Offline(self): + return self.GetAXProp('Offline') + def _set_Offline(self, Offline): + self.SetAXProp('Offline', Offline) + offline = property(_get_Offline, _set_Offline) + + def _get_Silent(self): + return self.GetAXProp('Silent') + def _set_Silent(self, Silent): + self.SetAXProp('Silent', Silent) + silent = property(_get_Silent, _set_Silent) + + def _get_RegisterAsBrowser(self): + return self.GetAXProp('RegisterAsBrowser') + def _set_RegisterAsBrowser(self, RegisterAsBrowser): + self.SetAXProp('RegisterAsBrowser', RegisterAsBrowser) + registerasbrowser = property(_get_RegisterAsBrowser, _set_RegisterAsBrowser) + + def _get_RegisterAsDropTarget(self): + return self.GetAXProp('RegisterAsDropTarget') + def _set_RegisterAsDropTarget(self, RegisterAsDropTarget): + self.SetAXProp('RegisterAsDropTarget', RegisterAsDropTarget) + registerasdroptarget = property(_get_RegisterAsDropTarget, _set_RegisterAsDropTarget) + + def _get_TheaterMode(self): + return self.GetAXProp('TheaterMode') + def _set_TheaterMode(self, TheaterMode): + self.SetAXProp('TheaterMode', TheaterMode) + theatermode = property(_get_TheaterMode, _set_TheaterMode) + + def _get_AddressBar(self): + return self.GetAXProp('AddressBar') + def _set_AddressBar(self, AddressBar): + self.SetAXProp('AddressBar', AddressBar) + addressbar = property(_get_AddressBar, _set_AddressBar) + + def _get_Resizable(self): + return self.GetAXProp('Resizable') + def _set_Resizable(self, Resizable): + self.SetAXProp('Resizable', Resizable) + resizable = property(_get_Resizable, _set_Resizable) + + +# PROPERTIES +# -------------------- +# application +# type:VT_DISPATCH arg:VT_EMPTY canGet:True canSet:False +# +# parent +# type:VT_DISPATCH arg:VT_EMPTY canGet:True canSet:False +# +# container +# type:VT_DISPATCH arg:VT_EMPTY canGet:True canSet:False +# +# document +# type:VT_DISPATCH arg:VT_EMPTY canGet:True canSet:False +# +# toplevelcontainer +# type:bool arg:VT_EMPTY canGet:True canSet:False +# +# type +# type:string arg:VT_EMPTY canGet:True canSet:False +# +# left +# type:int arg:int canGet:True canSet:True +# +# top +# type:int arg:int canGet:True canSet:True +# +# width +# type:int arg:int canGet:True canSet:True +# +# height +# type:int arg:int canGet:True canSet:True +# +# locationname +# type:string arg:VT_EMPTY canGet:True canSet:False +# +# locationurl +# type:string arg:VT_EMPTY canGet:True canSet:False +# +# busy +# type:bool arg:VT_EMPTY canGet:True canSet:False +# +# name +# type:string arg:VT_EMPTY canGet:True canSet:False +# +# hwnd +# type:int arg:VT_EMPTY canGet:True canSet:False +# +# fullname +# type:string arg:VT_EMPTY canGet:True canSet:False +# +# path +# type:string arg:VT_EMPTY canGet:True canSet:False +# +# visible +# type:bool arg:bool canGet:True canSet:True +# +# statusbar +# type:bool arg:bool canGet:True canSet:True +# +# statustext +# type:string arg:string canGet:True canSet:True +# +# toolbar +# type:int arg:int canGet:True canSet:True +# +# menubar +# type:bool arg:bool canGet:True canSet:True +# +# fullscreen +# type:bool arg:bool canGet:True canSet:True +# +# readystate +# type:unsupported type 29 arg:VT_EMPTY canGet:True canSet:False +# +# offline +# type:bool arg:bool canGet:True canSet:True +# +# silent +# type:bool arg:bool canGet:True canSet:True +# +# registerasbrowser +# type:bool arg:bool canGet:True canSet:True +# +# registerasdroptarget +# type:bool arg:bool canGet:True canSet:True +# +# theatermode +# type:bool arg:bool canGet:True canSet:True +# +# addressbar +# type:bool arg:bool canGet:True canSet:True +# +# resizable +# type:bool arg:bool canGet:True canSet:True +# +# +# +# +# METHODS +# -------------------- +# QueryInterface +# retType: VT_VOID +# params: +# riid +# in:True out:False optional:False type:unsupported type 29 +# ppvObj +# in:False out:True optional:False type:unsupported type 26 +# +# AddRef +# retType: int +# +# Release +# retType: int +# +# GetTypeInfoCount +# retType: VT_VOID +# params: +# pctinfo +# in:False out:True optional:False type:int +# +# GetTypeInfo +# retType: VT_VOID +# params: +# itinfo +# in:True out:False optional:False type:int +# lcid +# in:True out:False optional:False type:int +# pptinfo +# in:False out:True optional:False type:unsupported type 26 +# +# GetIDsOfNames +# retType: VT_VOID +# params: +# riid +# in:True out:False optional:False type:unsupported type 29 +# rgszNames +# in:True out:False optional:False type:unsupported type 26 +# cNames +# in:True out:False optional:False type:int +# lcid +# in:True out:False optional:False type:int +# rgdispid +# in:False out:True optional:False type:int +# +# Invoke +# retType: VT_VOID +# params: +# dispidMember +# in:True out:False optional:False type:int +# riid +# in:True out:False optional:False type:unsupported type 29 +# lcid +# in:True out:False optional:False type:int +# wFlags +# in:True out:False optional:False type:int +# pdispparams +# in:True out:False optional:False type:unsupported type 29 +# pvarResult +# in:False out:True optional:False type:VT_VARIANT +# pexcepinfo +# in:False out:True optional:False type:unsupported type 29 +# puArgErr +# in:False out:True optional:False type:int +# +# GoBack +# retType: VT_VOID +# +# GoForward +# retType: VT_VOID +# +# GoHome +# retType: VT_VOID +# +# GoSearch +# retType: VT_VOID +# +# Navigate +# retType: VT_VOID +# params: +# URL +# in:True out:False optional:False type:string +# Flags +# in:True out:False optional:False type:VT_VARIANT +# TargetFrameName +# in:True out:False optional:True type:VT_VARIANT +# PostData +# in:True out:False optional:True type:VT_VARIANT +# Headers +# in:True out:False optional:True type:VT_VARIANT +# +# Refresh +# retType: VT_VOID +# +# Refresh2 +# retType: VT_VOID +# params: +# Level +# in:True out:False optional:False type:VT_VARIANT +# +# Stop +# retType: VT_VOID +# +# Quit +# retType: VT_VOID +# +# ClientToWindow +# retType: VT_VOID +# params: +# pcx +# in:True out:True optional:False type:int +# pcy +# in:True out:True optional:False type:int +# +# PutProperty +# retType: VT_VOID +# params: +# Property +# in:True out:False optional:False type:string +# vtValue +# in:True out:False optional:False type:VT_VARIANT +# +# GetProperty +# retType: VT_VARIANT +# params: +# Property +# in:True out:False optional:False type:string +# +# Navigate2 +# retType: VT_VOID +# params: +# URL +# in:True out:False optional:False type:VT_VARIANT +# Flags +# in:True out:False optional:False type:VT_VARIANT +# TargetFrameName +# in:True out:False optional:True type:VT_VARIANT +# PostData +# in:True out:False optional:True type:VT_VARIANT +# Headers +# in:True out:False optional:True type:VT_VARIANT +# +# QueryStatusWB +# retType: unsupported type 29 +# params: +# cmdID +# in:True out:False optional:False type:unsupported type 29 +# +# ExecWB +# retType: VT_VOID +# params: +# cmdID +# in:True out:False optional:False type:unsupported type 29 +# cmdexecopt +# in:True out:False optional:False type:unsupported type 29 +# pvaIn +# in:True out:False optional:False type:VT_VARIANT +# pvaOut +# in:True out:True optional:True type:VT_VARIANT +# +# ShowBrowserBar +# retType: VT_VOID +# params: +# pvaClsid +# in:True out:False optional:False type:VT_VARIANT +# pvarShow +# in:True out:False optional:False type:VT_VARIANT +# pvarSize +# in:True out:False optional:True type:VT_VARIANT +# +# +# +# +# EVENTS +# -------------------- +# StatusTextChange +# retType: VT_VOID +# params: +# Text +# in:True out:False optional:False type:string +# +# ProgressChange +# retType: VT_VOID +# params: +# Progress +# in:True out:False optional:False type:int +# ProgressMax +# in:True out:False optional:False type:int +# +# CommandStateChange +# retType: VT_VOID +# params: +# Command +# in:True out:False optional:False type:int +# Enable +# in:True out:False optional:False type:bool +# +# DownloadBegin +# retType: VT_VOID +# +# DownloadComplete +# retType: VT_VOID +# +# TitleChange +# retType: VT_VOID +# params: +# Text +# in:True out:False optional:False type:string +# +# PropertyChange +# retType: VT_VOID +# params: +# szProperty +# in:True out:False optional:False type:string +# +# BeforeNavigate2 +# retType: VT_VOID +# params: +# pDisp +# in:True out:False optional:False type:VT_DISPATCH +# URL +# in:True out:False optional:False type:VT_VARIANT +# Flags +# in:True out:False optional:False type:VT_VARIANT +# TargetFrameName +# in:True out:False optional:False type:VT_VARIANT +# PostData +# in:True out:False optional:False type:VT_VARIANT +# Headers +# in:True out:False optional:False type:VT_VARIANT +# Cancel +# in:True out:True optional:False type:bool +# +# NewWindow2 +# retType: VT_VOID +# params: +# ppDisp +# in:True out:True optional:False type:VT_DISPATCH +# Cancel +# in:True out:True optional:False type:bool +# +# NavigateComplete2 +# retType: VT_VOID +# params: +# pDisp +# in:True out:False optional:False type:VT_DISPATCH +# URL +# in:True out:False optional:False type:VT_VARIANT +# +# DocumentComplete +# retType: VT_VOID +# params: +# pDisp +# in:True out:False optional:False type:VT_DISPATCH +# URL +# in:True out:False optional:False type:VT_VARIANT +# +# Quit +# retType: VT_VOID +# +# Visible +# retType: VT_VOID +# params: +# Visible +# in:True out:False optional:False type:bool +# +# ToolBar +# retType: VT_VOID +# params: +# ToolBar +# in:True out:False optional:False type:bool +# +# MenuBar +# retType: VT_VOID +# params: +# MenuBar +# in:True out:False optional:False type:bool +# +# StatusBar +# retType: VT_VOID +# params: +# StatusBar +# in:True out:False optional:False type:bool +# +# FullScreen +# retType: VT_VOID +# params: +# FullScreen +# in:True out:False optional:False type:bool +# +# TheaterMode +# retType: VT_VOID +# params: +# TheaterMode +# in:True out:False optional:False type:bool +# +# WindowSetResizable +# retType: VT_VOID +# params: +# Resizable +# in:True out:False optional:False type:bool +# +# WindowSetLeft +# retType: VT_VOID +# params: +# Left +# in:True out:False optional:False type:int +# +# WindowSetTop +# retType: VT_VOID +# params: +# Top +# in:True out:False optional:False type:int +# +# WindowSetWidth +# retType: VT_VOID +# params: +# Width +# in:True out:False optional:False type:int +# +# WindowSetHeight +# retType: VT_VOID +# params: +# Height +# in:True out:False optional:False type:int +# +# WindowClosing +# retType: VT_VOID +# params: +# IsChildWindow +# in:True out:False optional:False type:bool +# Cancel +# in:True out:True optional:False type:bool +# +# ClientToHostWindow +# retType: VT_VOID +# params: +# CX +# in:True out:True optional:False type:int +# CY +# in:True out:True optional:False type:int +# +# SetSecureLockIcon +# retType: VT_VOID +# params: +# SecureLockIcon +# in:True out:False optional:False type:int +# +# FileDownload +# retType: VT_VOID +# params: +# Cancel +# in:True out:True optional:False type:bool +# +# NavigateError +# retType: VT_VOID +# params: +# pDisp +# in:True out:False optional:False type:VT_DISPATCH +# URL +# in:True out:False optional:False type:VT_VARIANT +# Frame +# in:True out:False optional:False type:VT_VARIANT +# StatusCode +# in:True out:False optional:False type:VT_VARIANT +# Cancel +# in:True out:True optional:False type:bool +# +# PrintTemplateInstantiation +# retType: VT_VOID +# params: +# pDisp +# in:True out:False optional:False type:VT_DISPATCH +# +# PrintTemplateTeardown +# retType: VT_VOID +# params: +# pDisp +# in:True out:False optional:False type:VT_DISPATCH +# +# UpdatePageStatus +# retType: VT_VOID +# params: +# pDisp +# in:True out:False optional:False type:VT_DISPATCH +# nPage +# in:True out:False optional:False type:VT_VARIANT +# fDone +# in:True out:False optional:False type:VT_VARIANT +# +# PrivacyImpactedStateChange +# retType: VT_VOID +# params: +# bImpacted +# in:True out:False optional:False type:bool +# +# +# +# diff --git a/wx/lib/imagebrowser.py b/wx/lib/imagebrowser.py new file mode 100644 index 00000000..5b80b4ca --- /dev/null +++ b/wx/lib/imagebrowser.py @@ -0,0 +1,753 @@ +#---------------------------------------------------------------------------- +# Name: BrowseImage.py +# Purpose: Display and Select Image Files +# +# Original Author: Lorne White +# +# Version: 2.0 +# Date: June 16, 2007 +# Licence: wxWindows license +#---------------------------------------------------------------------------- +# 2.0 Release - Bill Baxter (wbaxter@gmail.com) +# Date: June 16, 2007 +# o Changed to use sizers instead of fixed placement. +# o Made dialog resizeable +# o Added a splitter between file list and view pane +# o Made directory path editable +# o Added an "up" button" to go to the parent dir +# o Changed to show directories in file list +# o Don't select images on double click any more +# o Added a 'broken image' display for files that wx fails to identify +# o Redesigned appearance -- using bitmap buttons now, and rearranged things +# o Fixed display of masked gifs +# o Fixed zooming logic to show shrunken images at correct aspect ratio +# o Added different background modes for preview (white/grey/dark/checkered) +# o Added framing modes for preview (no frame/box frame/tinted border) +# +#---------------------------------------------------------------------------- +# +# 12/08/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o Updated for wx namespace +# o Corrected a nasty bug or two - see comments below. +# o There was a duplicate ImageView.DrawImage() method. Que? +# +#---------------------------------------------------------------------------- +# 1.0 Release - Lorne White +# Date: January 29, 2002 +# Create list of all available image file types +# View "All Image" File Types as default filter +# Sort the file list +# Use newer "re" function for patterns +# + +#--------------------------------------------------------------------------- + +import os +import sys +import wx + +#--------------------------------------------------------------------------- + +BAD_IMAGE = -1 +ID_WHITE_BG = wx.NewId() +ID_BLACK_BG = wx.NewId() +ID_GREY_BG = wx.NewId() +ID_CHECK_BG = wx.NewId() +ID_NO_FRAME = wx.NewId() +ID_BOX_FRAME = wx.NewId() +ID_CROP_FRAME = wx.NewId() + +def ConvertBMP(file_nm): + if file_nm is None: + return None + + fl_fld = os.path.splitext(file_nm) + ext = fl_fld[1] + ext = ext[1:].lower() + + # Don't try to create it directly because wx throws up + # an annoying messasge dialog if the type isn't supported. + if wx.Image.CanRead(file_nm): + image = wx.Image(file_nm, wx.BITMAP_TYPE_ANY) + return image + + # BAD_IMAGE means a bad image, None just means no image (i.e. directory) + return BAD_IMAGE + + +def GetCheckeredBitmap(blocksize=8,ntiles=4,rgb0='\xFF', rgb1='\xCC'): + """Creates a square RGB checkered bitmap using the two specified colors. + + Inputs: + + - blocksize: the number of pixels in each solid color square + - ntiles: the number of tiles along width and height. Each tile is 2x2 blocks. + - rbg0, rgb1: the first and second colors, as 3-byte strings. + If only 1 byte is provided, it is treated as a grey value. + + The bitmap returned will have width = height = blocksize*ntiles*2 + """ + size = blocksize*ntiles*2 + + if len(rgb0)==1: + rgb0 = rgb0 * 3 + if len(rgb1)==1: + rgb1 = rgb1 * 3 + + strip0 = (rgb0*blocksize + rgb1*blocksize)*(ntiles*blocksize) + strip1 = (rgb1*blocksize + rgb0*blocksize)*(ntiles*blocksize) + band = strip0 + strip1 + data = band * ntiles + return wx.BitmapFromBuffer(size, size, data) + +def GetNamedBitmap(name): + return IMG_CATALOG[name].getBitmap() + + +class ImageView(wx.Window): + def __init__(self, parent, id=-1, pos=wx.DefaultPosition, size=wx.DefaultSize, + style=wx.BORDER_SUNKEN + ): + wx.Window.__init__(self, parent, id, pos, size, style=style) + + self.image = None + + self.check_bmp = None + self.check_dim_bmp = None + + # dark_bg is the brush/bitmap to use for painting in the whole background + # lite_bg is the brush/bitmap/pen to use for painting the image rectangle + self.dark_bg = None + self.lite_bg = None + + self.border_mode = ID_CROP_FRAME + self.SetBackgroundMode( ID_WHITE_BG ) + self.SetBorderMode( ID_NO_FRAME ) + + # Changed API of wx uses tuples for size and pos now. + self.Bind(wx.EVT_PAINT, self.OnPaint) + self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground) + self.Bind(wx.EVT_SIZE, self.OnSize) + + + def SetValue(self, file_nm): # display the selected file in the panel + image = ConvertBMP(file_nm) + self.image = image + self.Refresh() + + def SetBackgroundMode(self, mode): + self.bg_mode = mode + self._updateBGInfo() + + def _updateBGInfo(self): + bg = self.bg_mode + border = self.border_mode + + self.dark_bg = None + self.lite_bg = None + + if border == ID_BOX_FRAME: + self.lite_bg = wx.BLACK_PEN + + if bg == ID_WHITE_BG: + if border == ID_CROP_FRAME: + self.SetBackgroundColour('LIGHT GREY') + self.lite_bg = wx.WHITE_BRUSH + else: + self.SetBackgroundColour('WHITE') + + elif bg == ID_GREY_BG: + if border == ID_CROP_FRAME: + self.SetBackgroundColour('GREY') + self.lite_bg = wx.LIGHT_GREY_BRUSH + else: + self.SetBackgroundColour('LIGHT GREY') + + elif bg == ID_BLACK_BG: + if border == ID_BOX_FRAME: + self.lite_bg = wx.WHITE_PEN + if border == ID_CROP_FRAME: + self.SetBackgroundColour('GREY') + self.lite_bg = wx.BLACK_BRUSH + else: + self.SetBackgroundColour('BLACK') + + else: + if self.check_bmp is None: + self.check_bmp = GetCheckeredBitmap() + self.check_dim_bmp = GetCheckeredBitmap(rgb0='\x7F', rgb1='\x66') + if border == ID_CROP_FRAME: + self.dark_bg = self.check_dim_bmp + self.lite_bg = self.check_bmp + else: + self.dark_bg = self.check_bmp + + self.Refresh() + + def SetBorderMode(self, mode): + self.border_mode = mode + self._updateBGInfo() + + def OnSize(self, event): + event.Skip() + self.Refresh() + + def OnPaint(self, event): + dc = wx.PaintDC(self) + self.DrawImage(dc) + + def OnEraseBackground(self, evt): + if self.bg_mode != ID_CHECK_BG: + evt.Skip() + return + dc = evt.GetDC() + if dc: + self.PaintBackground(dc, self.dark_bg) + + def PaintBackground(self, dc, painter, rect=None): + if painter is None: + return + if rect is None: + pos = self.GetPosition() + sz = self.GetSize() + else: + pos = rect.Position + sz = rect.Size + + if type(painter)==wx.Brush: + dc.SetPen(wx.TRANSPARENT_PEN) + dc.SetBrush(painter) + dc.DrawRectangle(pos.x,pos.y,sz.width,sz.height) + elif type(painter)==wx.Pen: + dc.SetPen(painter) + dc.SetBrush(wx.TRANSPARENT_BRUSH) + dc.DrawRectangle(pos.x-1,pos.y-1,sz.width+2,sz.height+2) + else: + self.TileBackground(dc, painter, pos.x,pos.y,sz.width,sz.height) + + + def TileBackground(self, dc, bmp, x,y,w,h): + """Tile bmp into the specified rectangle""" + bw = bmp.GetWidth() + bh = bmp.GetHeight() + + dc.SetClippingRegion(x,y,w,h) + + # adjust so 0,0 so we always match with a tiling starting at 0,0 + dx = x % bw + x = x - dx + w = w + dx + + dy = y % bh + y = y - dy + h = h + dy + + tx = x + x2 = x+w + y2 = y+h + + while tx < x2: + ty = y + while ty < y2: + dc.DrawBitmap(bmp, tx, ty) + ty += bh + tx += bw + + def DrawImage(self, dc): + + if not hasattr(self,'image') or self.image is None: + return + + wwidth,wheight = self.GetSize() + image = self.image + bmp = None + if image != BAD_IMAGE and image.IsOk(): + iwidth = image.GetWidth() # dimensions of image file + iheight = image.GetHeight() + else: + bmp = wx.ArtProvider.GetBitmap(wx.ART_MISSING_IMAGE, wx.ART_MESSAGE_BOX, (64,64)) + iwidth = bmp.GetWidth() + iheight = bmp.GetHeight() + + # squeeze iwidth x iheight image into window, preserving aspect ratio + + xfactor = float(wwidth) / iwidth + yfactor = float(wheight) / iheight + + if xfactor < 1.0 and xfactor < yfactor: + scale = xfactor + elif yfactor < 1.0 and yfactor < xfactor: + scale = yfactor + else: + scale = 1.0 + + owidth = int(scale*iwidth) + oheight = int(scale*iheight) + + diffx = (wwidth - owidth)/2 # center calc + diffy = (wheight - oheight)/2 # center calc + + if not bmp: + if owidth!=iwidth or oheight!=iheight: + sc_image = sc_image = image.Scale(owidth,oheight) + else: + sc_image = image + bmp = sc_image.ConvertToBitmap() + + if image != BAD_IMAGE and image.IsOk(): + self.PaintBackground(dc, self.lite_bg, wx.Rect(diffx,diffy,owidth,oheight)) + + dc.DrawBitmap(bmp, diffx, diffy, useMask=True) # draw the image to window + + +class ImagePanel(wx.Panel): + def __init__(self, parent, id=-1, pos=wx.DefaultPosition, size=wx.DefaultSize, + style=wx.NO_BORDER + ): + wx.Panel.__init__(self, parent, id, pos, size, style=style) + + vbox = wx.BoxSizer(wx.VERTICAL) + self.SetSizer(vbox) + + self.view = ImageView(self) + vbox.Add(self.view, 1, wx.GROW|wx.ALL, 0) + + hbox_ctrls = wx.BoxSizer(wx.HORIZONTAL) + vbox.Add(hbox_ctrls, 0, wx.ALIGN_RIGHT|wx.TOP, 4) + + bmp = GetNamedBitmap('White') + btn = wx.BitmapButton(self, ID_WHITE_BG, bmp, style=wx.BU_EXACTFIT) + self.Bind(wx.EVT_BUTTON, self.OnSetImgBackground, btn) + btn.SetToolTipString("Set background to white") + hbox_ctrls.Add(btn, 0, wx.ALIGN_LEFT|wx.LEFT, 4) + + bmp = GetNamedBitmap('Grey') + btn = wx.BitmapButton(self, ID_GREY_BG, bmp, style=wx.BU_EXACTFIT) + self.Bind(wx.EVT_BUTTON, self.OnSetImgBackground, btn) + btn.SetToolTipString("Set background to grey") + hbox_ctrls.Add(btn, 0, wx.ALIGN_LEFT|wx.LEFT, 4) + + bmp = GetNamedBitmap('Black') + btn = wx.BitmapButton(self, ID_BLACK_BG, bmp, style=wx.BU_EXACTFIT) + self.Bind(wx.EVT_BUTTON, self.OnSetImgBackground, btn) + btn.SetToolTipString("Set background to black") + hbox_ctrls.Add(btn, 0, wx.ALIGN_LEFT|wx.LEFT, 4) + + bmp = GetNamedBitmap('Checked') + btn = wx.BitmapButton(self, ID_CHECK_BG, bmp, style=wx.BU_EXACTFIT) + self.Bind(wx.EVT_BUTTON, self.OnSetImgBackground, btn) + btn.SetToolTipString("Set background to chekered pattern") + hbox_ctrls.Add(btn, 0, wx.ALIGN_LEFT|wx.LEFT, 4) + + + hbox_ctrls.AddSpacer(7) + + bmp = GetNamedBitmap('NoFrame') + btn = wx.BitmapButton(self, ID_NO_FRAME, bmp, style=wx.BU_EXACTFIT) + self.Bind(wx.EVT_BUTTON, self.OnSetBorderMode, btn) + btn.SetToolTipString("No framing around image") + hbox_ctrls.Add(btn, 0, wx.ALIGN_LEFT|wx.LEFT, 4) + + bmp = GetNamedBitmap('BoxFrame') + btn = wx.BitmapButton(self, ID_BOX_FRAME, bmp, style=wx.BU_EXACTFIT) + self.Bind(wx.EVT_BUTTON, self.OnSetBorderMode, btn) + btn.SetToolTipString("Frame image with a box") + hbox_ctrls.Add(btn, 0, wx.ALIGN_LEFT|wx.LEFT, 4) + + bmp = GetNamedBitmap('CropFrame') + btn = wx.BitmapButton(self, ID_CROP_FRAME, bmp, style=wx.BU_EXACTFIT|wx.BORDER_SIMPLE) + self.Bind(wx.EVT_BUTTON, self.OnSetBorderMode, btn) + btn.SetToolTipString("Frame image with a dimmed background") + hbox_ctrls.Add(btn, 0, wx.ALIGN_LEFT|wx.LEFT, 4) + + + def SetValue(self, file_nm): # display the selected file in the panel + self.view.SetValue(file_nm) + + def SetBackgroundMode(self, mode): + self.view.SetBackgroundMode(mode) + + def SetBorderMode(self, mode): + self.view.SetBorderMode(mode) + + def OnSetImgBackground(self, event): + mode = event.GetId() + self.SetBackgroundMode(mode) + + def OnSetBorderMode(self, event): + mode = event.GetId() + self.SetBorderMode(mode) + + + +class ImageDialog(wx.Dialog): + def __init__(self, parent, set_dir = None): + wx.Dialog.__init__(self, parent, -1, "Image Browser", wx.DefaultPosition, (400, 400),style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER) + + self.set_dir = os.getcwd() + self.set_file = None + + if set_dir != None: + if os.path.exists(set_dir): # set to working directory if nothing set + self.set_dir = set_dir + + vbox_top = wx.BoxSizer(wx.VERTICAL) + self.SetSizer(vbox_top) + + hbox_loc = wx.BoxSizer(wx.HORIZONTAL) + vbox_top.Add(hbox_loc, 0, wx.GROW|wx.ALIGN_LEFT|wx.ALL, 0) + + loc_label = wx.StaticText( self, -1, "Folder:") + hbox_loc.Add(loc_label, 0, wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.ALL|wx.ADJUST_MINSIZE, 5) + + self.dir = wx.TextCtrl( self, -1, self.set_dir, style=wx.TE_RICH|wx.TE_PROCESS_ENTER) + self.Bind(wx.EVT_TEXT_ENTER, self.OnDirectoryTextSet, self.dir) + hbox_loc.Add(self.dir, 1, wx.GROW|wx.ALIGN_LEFT|wx.ALL, 5) + + up_bmp = wx.ArtProvider.GetBitmap(wx.ART_GO_DIR_UP, wx.ART_BUTTON, (16,16)) + btn = wx.BitmapButton(self, -1, up_bmp) + btn.SetHelpText("Up one level") + btn.SetToolTipString("Up one level") + self.Bind(wx.EVT_BUTTON, self.OnUpDirectory, btn) + hbox_loc.Add(btn, 0, wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, 2) + + folder_bmp = wx.ArtProvider.GetBitmap(wx.ART_FOLDER_OPEN, wx.ART_BUTTON, (16,16)) + btn = wx.BitmapButton(self, -1, folder_bmp) + btn.SetHelpText("Browse for a &folder...") + btn.SetToolTipString("Browse for a folder...") + self.Bind(wx.EVT_BUTTON, self.OnChooseDirectory, btn) + hbox_loc.Add(btn, 0, wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, 5) + + hbox_nav = wx.BoxSizer(wx.HORIZONTAL) + vbox_top.Add(hbox_nav, 0, wx.ALIGN_LEFT|wx.ALL, 0) + + + label = wx.StaticText( self, -1, "Files of type:") + hbox_nav.Add(label, 0, wx.ALIGN_CENTER_VERTICAL|wx.LEFT, 5) + + self.fl_ext = '*.bmp' # initial setting for file filtering + self.GetFiles() # get the file list + + self.fl_ext_types = ( + # display, filter + ("All supported formats", "All"), + ("BMP (*.bmp)", "*.bmp"), + ("GIF (*.gif)", "*.gif"), + ("PNG (*.png)", "*.png"), + ("JPEG (*.jpg)", "*.jpg"), + ("ICO (*.ico)", "*.ico"), + ("PNM (*.pnm)", "*.pnm"), + ("PCX (*.pcx)", "*.pcx"), + ("TIFF (*.tif)", "*.tif"), + ("All Files", "*.*"), + ) + self.set_type,self.fl_ext = self.fl_ext_types[0] # initial file filter setting + self.fl_types = [ x[0] for x in self.fl_ext_types ] + self.sel_type = wx.ComboBox( self, -1, self.set_type, + wx.DefaultPosition, wx.DefaultSize, self.fl_types, + wx.CB_DROPDOWN ) + # after this we don't care about the order any more + self.fl_ext_types = dict(self.fl_ext_types) + + self.Bind(wx.EVT_COMBOBOX, self.OnSetType, self.sel_type) + hbox_nav.Add(self.sel_type, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5) + + splitter = wx.SplitterWindow( self, -1, wx.DefaultPosition, wx.Size(100, 100), 0 ) + splitter.SetMinimumPaneSize(100) + + split_left = wx.Panel( splitter, -1, wx.DefaultPosition, wx.DefaultSize, + wx.NO_BORDER|wx.TAB_TRAVERSAL ) + vbox_left = wx.BoxSizer(wx.VERTICAL) + split_left.SetSizer(vbox_left) + + + self.tb = tb = wx.ListBox( split_left, -1, wx.DefaultPosition, wx.DefaultSize, + self.fl_list, wx.LB_SINGLE ) + self.Bind(wx.EVT_LISTBOX, self.OnListClick, tb) + self.Bind(wx.EVT_LISTBOX_DCLICK, self.OnListDClick, tb) + vbox_left.Add(self.tb, 1, wx.GROW|wx.ALL, 0) + + width, height = self.tb.GetSize() + + split_right = wx.Panel( splitter, -1, wx.DefaultPosition, wx.DefaultSize, + wx.NO_BORDER|wx.TAB_TRAVERSAL ) + vbox_right = wx.BoxSizer(wx.VERTICAL) + split_right.SetSizer(vbox_right) + + self.image_view = ImagePanel( split_right ) + vbox_right.Add(self.image_view, 1, wx.GROW|wx.ALL, 0) + + splitter.SplitVertically(split_left, split_right, 150) + vbox_top.Add(splitter, 1, wx.GROW|wx.ALL, 5) + + hbox_btns = wx.BoxSizer(wx.HORIZONTAL) + vbox_top.Add(hbox_btns, 0, wx.ALIGN_RIGHT|wx.ALL, 5) + + ok_btn = wx.Button( self, wx.ID_OPEN, "", wx.DefaultPosition, wx.DefaultSize, 0 ) + self.Bind(wx.EVT_BUTTON, self.OnOk, ok_btn) + #ok_btn.SetDefault() + hbox_btns.Add(ok_btn, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5) + + cancel_btn = wx.Button( self, wx.ID_CANCEL, "", + wx.DefaultPosition, wx.DefaultSize, 0 ) + hbox_btns.Add(cancel_btn, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5) + + self.ResetFiles() + + def ChangeFileTypes(self, ft_tuple): + # Change list of file types to be supported + self.fl_ext_types = ft_tuple + self.set_type, self.fl_ext = self.fl_ext_types[0] # initial file filter setting + self.fl_types = [ x[0] for x in self.fl_ext_types ] + self.sel_type.Clear() + self.sel_type.AppendItems(self.fl_types) + self.sel_type.SetSelection(0) + self.fl_ext_types = dict(self.fl_ext_types) + + def GetFiles(self): # get the file list using directory and extension values + if self.fl_ext == "All": + all_files = [] + + if self.fl_types[-1] == 'All Files': + allTypes = self.fl_types[-1:] + else: + allTypes = self.fl_types[1:] + for ftypes in allTypes: # get list of all + filter = self.fl_ext_types[ftypes] + #print "filter = ", filter + self.fl_val = FindFiles(self, self.set_dir, filter) + all_files = all_files + self.fl_val.files # add to list of files + + self.fl_list = all_files + else: + self.fl_val = FindFiles(self, self.set_dir, self.fl_ext) + self.fl_list = self.fl_val.files + + + self.fl_list.sort() # sort the file list + # prepend the directories + self.fl_ndirs = len(self.fl_val.dirs) + self.fl_list = sorted(self.fl_val.dirs) + self.fl_list + + def DisplayDir(self): # display the working directory + if self.dir: + ipt = self.dir.GetInsertionPoint() + self.dir.SetValue(self.set_dir) + self.dir.SetInsertionPoint(ipt) + + def OnSetType(self, event): + val = event.GetString() # get file type value + self.fl_ext = self.fl_ext_types[val] + self.ResetFiles() + + def OnListDClick(self, event): + self.OnOk('dclick') + + def OnListClick(self, event): + val = event.GetSelection() + self.SetListValue(val) + + def SetListValue(self, val): + file_nm = self.fl_list[val] + self.set_file = file_val = os.path.join(self.set_dir, file_nm) + if val>=self.fl_ndirs: + self.image_view.SetValue(file_val) + else: + self.image_view.SetValue(None) + + def OnDirectoryTextSet(self,event): + event.Skip() + path = event.GetString() + if os.path.isdir(path): + self.set_dir = path + self.ResetFiles() + return + + if os.path.isfile(path): + dname,fname = os.path.split(path) + if os.path.isdir(dname): + self.ResetFiles() + # try to select fname in list + try: + idx = self.fl_list.index(fname) + self.tb.SetSelection(idx) + self.SetListValue(idx) + return + except ValueError: + pass + + wx.Bell() + + def OnUpDirectory(self, event): + sdir = os.path.split(self.set_dir)[0] + self.set_dir = sdir + self.ResetFiles() + + def OnChooseDirectory(self, event): # set the new directory + dlg = wx.DirDialog(self) + dlg.SetPath(self.set_dir) + + if dlg.ShowModal() == wx.ID_OK: + self.set_dir = dlg.GetPath() + self.ResetFiles() + + dlg.Destroy() + + def ResetFiles(self): # refresh the display with files and initial image + self.DisplayDir() + self.GetFiles() + + # Changed 12/8/03 jmg + # + # o Clear listbox first + # o THEN check to see if there are any valid files of the selected + # type, + # o THEN if we have any files to display, set the listbox up, + # + # OTHERWISE + # + # o Leave it cleared + # o Clear the image viewer. + # + # This avoids a nasty assert error. + # + self.tb.Clear() + + if len(self.fl_list): + self.tb.Set(self.fl_list) + + for idir in xrange(self.fl_ndirs): + d = self.fl_list[idir] + # mark directories as 'True' with client data + self.tb.SetClientData(idir, True) + self.tb.SetString(idir,'['+d+']') + + try: + self.tb.SetSelection(0) + self.SetListValue(0) + except: + self.image_view.SetValue(None) + else: + self.image_view.SetValue(None) + + def GetFile(self): + return self.set_file + + def GetDirectory(self): + return self.set_dir + + def OnCancel(self, event): + self.result = None + self.EndModal(wx.ID_CANCEL) + + def OnOk(self, event): + if os.path.isdir(self.set_file): + sdir = os.path.split(self.set_file) + + #os.path.normapth? + if sdir and sdir[-1]=='..': + sdir = os.path.split(sdir[0])[0] + sdir = os.path.split(sdir) + self.set_dir = os.path.join(*sdir) + self.set_file = None + self.ResetFiles() + elif event != 'dclick': + self.result = self.set_file + self.EndModal(wx.ID_OK) + + + +class FindFiles: + def __init__(self, parent, dir, mask, with_dirs=True): + filelist = [] + dirlist = [".."] + self.dir = dir + self.file = "" + mask = mask.upper() + pattern = self.MakeRegex(mask) + + for i in os.listdir(dir): + if i == "." or i == "..": + continue + + path = os.path.join(dir, i) + + if os.path.isdir(path): + dirlist.append(i) + continue + + path = path.upper() + value = i.upper() + + if pattern.match(value) != None: + filelist.append(i) + + + self.files = filelist + if with_dirs: + self.dirs = dirlist + + def MakeRegex(self, pattern): + import re + f = "" # Set up a regex for file names + + for ch in pattern: + if ch == "*": + f = f + ".*" + elif ch == ".": + f = f + "\." + elif ch == "?": + f = f + "." + else: + f = f + ch + + return re.compile(f+'$') + + def StripExt(self, file_nm): + fl_fld = os.path.splitext(file_nm) + fl_name = fl_fld[0] + ext = fl_fld[1] + return ext[1:] + + +#---------------------------------------------------------------------- +# This part of the file was generated by C:\Python25\Scripts\img2py +# then edited slightly. + +from wx.lib.embeddedimage import PyEmbeddedImage + +IMG_CATALOG = {} + +IMG_CATALOG['White'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAA3NCSVQICAjb4U/gAAAAIUlE" + "QVQYlWNgIAIwMjAw/P//H58KRkYmYkwaVUScIqIAAMjRAxRV8+5MAAAAAElFTkSuQmCC") + +IMG_CATALOG['Grey'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAA3NCSVQICAjb4U/gAAAAIklE" + "QVQYlWNgIAIwMjAwnDlzBo8KExMTJmJMGlVEnCKiAAC24wMULFLZGAAAAABJRU5ErkJggg==") + +IMG_CATALOG['Black'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAA3NCSVQICAjb4U/gAAAADklE" + "QVQYlWNgGAVDFQAAAbwAATN8mzYAAAAASUVORK5CYII=") + +IMG_CATALOG['Checked'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAA3NCSVQICAjb4U/gAAAAMUlE" + "QVQYlWNgIAIwMjAwnDlzBlnI2NgYRQUjIxMxJtFZEQsDhkvPnj07sG4iShFRAAAougYW+urT" + "ZwAAAABJRU5ErkJggg==") + +IMG_CATALOG['NoFrame'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAA3NCSVQICAjb4U/gAAAANklE" + "QVQYla2PQQoAIBACnej/X7ZbUEQtkudhVKkQJNm+EdAqpggCgB+m44kFml1bY39q0k15Bsuc" + "CR/z8ajiAAAAAElFTkSuQmCC") + +IMG_CATALOG['BoxFrame'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAA3NCSVQICAjb4U/gAAAAQ0lE" + "QVQYlZ2O0QoAIAgDd9L//7I9CFEhJu1psmNOaghJ7l4RYJ0m1U0R2X4vevcHVOiG0tcHBABh" + "8nWpIhpPLtn0rwm4WyD966x3sgAAAABJRU5ErkJggg==") + +IMG_CATALOG['CropFrame'] = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAA3NCSVQICAjb4U/gAAAASUlE" + "QVQYlb2QMQrAQAgEZ0P+q0/RF5tCuIMUh2myhcgyjCAMIiAiDoS7XxPTCLrXZmaAJKCqgMz8" + "YHpD7ThBkvpcz93z6wtGeQD/sQ8bfXs8NAAAAABJRU5ErkJggg==") + diff --git a/wx/lib/imageutils.py b/wx/lib/imageutils.py new file mode 100644 index 00000000..46868870 --- /dev/null +++ b/wx/lib/imageutils.py @@ -0,0 +1,98 @@ +#---------------------------------------------------------------------- +# Name: wx.lib.imageutils +# Purpose: A collection of functions for simple image manipulations +# +# Author: Robb Shecter +# +# Created: 7-Nov-2002 +# RCS-ID: $Id$ +# Copyright: (c) 2002 by +# Licence: wxWindows license +#---------------------------------------------------------------------- + +import wx + +def grayOut(anImage): + """ + Convert the given image (in place) to a grayed-out + version, appropriate for a 'disabled' appearance. + """ + factor = 0.7 # 0 < f < 1. Higher is grayer. + if anImage.HasMask(): + maskColor = (anImage.GetMaskRed(), anImage.GetMaskGreen(), anImage.GetMaskBlue()) + else: + maskColor = None + if anImage.HasAlpha(): + alpha = anImage.GetAlphaData() + else: + alpha = None + + data = map(ord, list(anImage.GetData())) + + for i in range(0, len(data), 3): + pixel = (data[i], data[i+1], data[i+2]) + pixel = makeGray(pixel, factor, maskColor) + for x in range(3): + data[i+x] = pixel[x] + anImage.SetData(''.join(map(chr, data))) + if alpha: + anImage.SetAlphaData(alpha) + + +def makeGray((r,g,b), factor, maskColor): + """ + Make a pixel grayed-out. If the pixel + matches the maskColor, it won't be + changed. + """ + if (r,g,b) != maskColor: + return map(lambda x: int((230 - x) * factor) + x, (r,g,b)) + else: + return (r,g,b) + + + +def stepColour(c, step): + """ + stepColour is a utility function that simply darkens or lightens a + color, based on the specified step value. A step of 0 is + completely black and a step of 200 is totally white, and 100 + results in the same color as was passed in. + """ + def _blendColour(fg, bg, dstep): + result = bg + (dstep * (fg - bg)) + if result < 0: + result = 0 + if result > 255: + result = 255 + return result + + if step == 100: + return c + + r = c.Red() + g = c.Green() + b = c.Blue() + + # step is 0..200 where 0 is completely black + # and 200 is completely white and 100 is the same + # convert that to a range of -1.0 .. 1.0 + step = min(step, 200) + step = max(step, 0) + dstep = (step - 100.0)/100.0 + + if step > 100: + # blend with white + bg = 255.0 + dstep = 1.0 - dstep # 0 = transparent fg; 1 = opaque fg + else: + # blend with black + bg = 0.0 + dstep = 1.0 + dstep; # 0 = transparent fg; 1 = opaque fg + + r = _blendColour(r, bg, dstep) + g = _blendColour(g, bg, dstep) + b = _blendColour(b, bg, dstep) + + return wx.Colour(int(r), int(g), int(b)) + diff --git a/wx/lib/infoframe.py b/wx/lib/infoframe.py new file mode 100644 index 00000000..14ab217c --- /dev/null +++ b/wx/lib/infoframe.py @@ -0,0 +1,492 @@ +# 12/20/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o wxPyInformationalMessagesFrame -> PyInformationalMessagesFrame +# o dummy_wxPyInformationalMessagesFrame -> dummy_PyInformationalMessagesFrame +# + +""" +infoframe.py +Released under wxWindows license etc. + +This is a fairly rudimentary, but slightly fancier tha +wxPyOnDemandOutputWindow (on which it's based; thanks Robin), version +of the same sort of thing: a file-like class called +wxInformationalMessagesFrame. This window also has a status bar with a +couple of buttons for controlling the echoing of all output to a file +with a randomly-chosen filename... + +The class behaves similarly to wxPyOnDemandOutputWindow in that (at +least by default) the frame does not appear until written to, but is +somewhat different in that, either under programmatic (the +DisableOutput method) or user (the frame's close button, it's status +bar's "Dismiss" button, or a "Disable output" item of some menu, +perhaps of some other frame), the frame will be destroyed, an +associated file closed, and writing to it will then do nothing. This +can be reversed: either under programmatic (the EnableOutput method) +or user (an "Enable output" item of some menu), a new frame will be +opened,And an associated file (with a "randomly"selected filename) +opened for writing [to which all subsequent displayed messages will be +echoed]. + +Please note that, like wxPyOnDemandOutputWindow, the instance is not +itself a subclass of wxWindow: when the window is open (and ONLY +then), it's "frame" attribute is the actual instance of wFrame... + +Typical usage:: + + from wx.lib.infoframe import * + ... # ... modify your wxApp as follows: + class myApp(wxApp): + outputWindowClass = PyInformationalMessagesFrame + # ... + + +If you're running on Linux, you'll also have to supply an argument 1 to your +constructor of myApp to redirect stdout/stderr to this window (it's done +automatically for you on Windows). + +If you don't want to redirect stdout/stderr, but use the class directly: do +it this way:: + + InformationalMessagesFrame = PyInformationalMessagesFrame( \ + options_from_progname, # (default = "") + txt), # (default = "informational messages") + + #^^^^ early in the program + # ... + + InformationalMessagesFrame(list_of_items) + + # where list_of_items: + # + # comma-separated list of items to display. + # Note that these will never be separated by spaces as they may + # be when used in the Python 'print' command + + +The latter statement, of course, may be repeated arbitrarily often. +The window will not appear until it is written to, and it may be +manually closed by the user, after which it will reappear again until +written to... Also note that all output is echoed to a file with a +randomly-generated name [see the mktemp module in the standard +library], in the directory given as the 'dir' keyword argument to the +InformationalMessagesFrame constructor [which has a default value of +'.'), or set via the method SetOutputDirectory... This file will be +closed with the window--a new one will be created [by default] upon +each subsequent reopening. + +Please also note the methods EnableOutput and DisableOutput, and the +possible arguments for the constructor in the code below... (* TO DO: +explain this here...*) Neither of these methods need be used at all, +and in this case the frame will only be displayed once it has been +written to, like wxPyOnDemandOutputWindow. + +The former, EnableOutput, displays the frame with an introductory +message, opens a random file to which future displayed output also +goes (unless the nofile attribute is present), and sets the __debug__ +variable of each module to 1 (unless the no __debug__ attribute is +present]. This is so that you can say, in any module whatsoever:: + + if __debug__: + InformationalMessagesFrame("... with lots of % constructs" + % TUPLE) + + +without worrying about the overhead of evaluating the arguments, and +calling the wxInformationalMessagesFrame instance, in the case where +debugging is not turned on. (This won't happen if the instance has an +attribute no__debug__; you can arrange this by sub-classing...) + +"Debug mode" can also be turned on by selecting the item-"Enable +output" from the "Debug" menu [the default--see the optional arguments +to the SetOtherMenuBar method] of a frame which has been either passed +appropriately to the constructor of the wxInformationalMessagesFrame +(see the code), or set via the SetOtherMenuBar method thereof. This +also writes an empty string to the instance, meaning that the frame +will open (unless DisablOutput has been called) with an appropriate +introductory message (which will vary according to whether the +instance/class has the "no __debug__" attribute)^ I have found this to +be an extremely useful tool, in lieu of a full wxPython debugger... + +Following this, the menu item is also disabled, and an item "Disable +output" (again, by default) is enabled. Note that these need not be +done: e.g., you don't NEED to have a menu with appropriate items; in +this case simply do not call the SetOtherMenuBar method or use the +othermenubar keyword argument of the class instance constructor. + +The DisableOutput method does the reverse of this; it closes the +window (and associated file), and sets the __debug__ variable of each +module whose name begins with a capital letter {this happens to be the +author's personal practice; all my python module start with capital +letters} to 0. It also enables/disabled the appropriate menu items, +if this was done previously (or SetOtherMenuBar has been called...). +Note too that after a call to DisableOutput, nothing further will be +done upon subsequent write()'s, until the EnableOutput method is +called, either explicitly or by the menu selection above... + +Finally, note that the file-like method close() destroys the window +(and closes any associated file) and there is a file-like method +write() which displays it's argument. + +All (well, most) of this is made clear by the example code at the end +of this file, which is run if the file is run by itself; otherwise, +see the appropriate "stub" file in the wxPython demo. + +""" + +import os +import sys +import tempfile + +import wx + +class _MyStatusBar(wx.StatusBar): + def __init__(self, parent, callbacks=None, useopenbutton=0): + wx.StatusBar.__init__(self, parent, -1, style=wx.TAB_TRAVERSAL) + self.SetFieldsCount(3) + + self.SetStatusText("",0) + + self.button1 = wx.Button(self, -1, "Dismiss", style=wx.TAB_TRAVERSAL) + self.Bind(wx.EVT_BUTTON, self.OnButton1, self.button1) + + if not useopenbutton: + self.button2 = wx.Button(self, -1, "Close File", style=wx.TAB_TRAVERSAL) + else: + self.button2 = wx.Button(self, -1, "Open New File", style=wx.TAB_TRAVERSAL) + + self.Bind(wx.EVT_BUTTON, self.OnButton2, self.button2) + self.useopenbutton = useopenbutton + self.callbacks = callbacks + + # figure out how tall to make the status bar + dc = wx.ClientDC(self) + dc.SetFont(self.GetFont()) + (w,h) = dc.GetTextExtent('X') + h = int(h * 1.8) + self.SetSize((100, h)) + self.OnSize("dummy") + self.Bind(wx.EVT_SIZE, self.OnSize) + + # reposition things... + def OnSize(self, event): + self.CalculateSizes() + rect = self.GetFieldRect(1) + self.button1.SetPosition((rect.x+5, rect.y+2)) + self.button1.SetSize((rect.width-10, rect.height-4)) + rect = self.GetFieldRect(2) + self.button2.SetPosition((rect.x+5, rect.y+2)) + self.button2.SetSize((rect.width-10, rect.height-4)) + + # widths........ + def CalculateSizes(self): + dc = wx.ClientDC(self.button1) + dc.SetFont(self.button1.GetFont()) + (w1,h) = dc.GetTextExtent(self.button1.GetLabel()) + + dc = wx.ClientDC(self.button2) + dc.SetFont(self.button2.GetFont()) + (w2,h) = dc.GetTextExtent(self.button2.GetLabel()) + + self.SetStatusWidths([-1,w1+15,w2+15]) + + def OnButton1(self,event): + self.callbacks[0] () + + def OnButton2(self,event): + if self.useopenbutton and self.callbacks[2] (): + self.button2.SetLabel ("Close File") + elif self.callbacks[1] (): + self.button2.SetLabel ("Open New File") + + self.useopenbutton = 1 - self.useopenbutton + self.OnSize("") + self.button2.Refresh(True) + self.Refresh() + + + +class PyInformationalMessagesFrame(object): + def __init__(self, + progname="", + text="informational messages", + dir='.', + menuname="Debug", + enableitem="Enable output", + disableitem="Disable output", + othermenubar=None): + + self.SetOtherMenuBar(othermenubar, + menuname=menuname, + enableitem=enableitem, + disableitem=disableitem) + + if hasattr(self,"othermenu") and self.othermenu is not None: + i = self.othermenu.FindMenuItem(self.menuname,self.disableitem) + self.othermenu.Enable(i,0) + i = self.othermenu.FindMenuItem(self.menuname,self.enableitem) + self.othermenu.Enable(i,1) + + self.frame = None + self.title = "%s %s" % (progname,text) + self.parent = None # use the SetParent method if desired... + self.softspace = 1 # of rather limited use + + if dir: + self.SetOutputDirectory(dir) + + + def SetParent(self, parent): + self.parent = parent + + + def SetOtherMenuBar(self, + othermenu, + menuname="Debug", + enableitem="Enable output", + disableitem="Disable output"): + self.othermenu = othermenu + self.menuname = menuname + self.enableitem = enableitem + self.disableitem = disableitem + + + f = None + + + def write(self, string): + if not wx.Thread_IsMain(): + # Aquire the GUI mutex before making GUI calls. Mutex is released + # when locker is deleted at the end of this function. + # + # TODO: This should be updated to use wx.CallAfter similarly to how + # PyOnDemandOutputWindow.write was so it is not necessary + # to get the gui mutex + locker = wx.MutexGuiLocker() + + if self.Enabled: + if self.f: + self.f.write(string) + self.f.flush() + + move = 1 + if (hasattr(self,"text") + and self.text is not None + and self.text.GetInsertionPoint() != self.text.GetLastPosition()): + move = 0 + + if not self.frame: + self.frame = wx.Frame(self.parent, -1, self.title, size=(450, 300), + style=wx.DEFAULT_FRAME_STYLE|wx.NO_FULL_REPAINT_ON_RESIZE) + + self.text = wx.TextCtrl(self.frame, -1, "", + style = wx.TE_MULTILINE|wx.TE_READONLY|wx.TE_RICH) + + self.frame.sb = _MyStatusBar(self.frame, + callbacks=[self.DisableOutput, + self.CloseFile, + self.OpenNewFile], + useopenbutton=hasattr(self, + "nofile")) + self.frame.SetStatusBar(self.frame.sb) + self.frame.Show(True) + self.frame.Bind(wx.EVT_CLOSE, self.OnCloseWindow) + + if hasattr(self,"nofile"): + self.text.AppendText( + "Please close this window (or select the " + "'Dismiss' button below) when desired. By " + "default all messages written to this window " + "will NOT be written to a file--you " + "may change this by selecting 'Open New File' " + "below, allowing you to select a " + "new file...\n\n") + else: + tempfile.tempdir = self.dir + filename = os.path.abspath(tempfile.mktemp ()) + + self.text.AppendText( + "Please close this window (or select the " + "'Dismiss' button below) when desired. By " + "default all messages written to this window " + "will also be written to the file '%s'--you " + "may close this file by selecting 'Close " + "File' below, whereupon this button will be " + "replaced with one allowing you to select a " + "new file...\n\n" % filename) + try: + self.f = open(filename, 'w') + self.frame.sb.SetStatusText("File '%s' opened..." + % filename, + 0) + except EnvironmentError: + self.frame.sb.SetStatusText("File creation failed " + "(filename '%s')..." + % filename, + 0) + self.text.AppendText(string) + + if move: + self.text.ShowPosition(self.text.GetLastPosition()) + + if not hasattr(self,"no__debug__"): + for m in sys.modules.values(): + if m is not None:# and m.__dict__.has_key("__debug__"): + m.__dict__["__debug__"] = 1 + + if hasattr(self,"othermenu") and self.othermenu is not None: + i = self.othermenu.FindMenuItem(self.menuname,self.disableitem) + self.othermenu.Enable(i,1) + i = self.othermenu.FindMenuItem(self.menuname,self.enableitem) + self.othermenu.Enable(i,0) + + + Enabled = 1 + + def OnCloseWindow(self, event, exiting=0): + if self.f: + self.f.close() + self.f = None + + if (hasattr(self,"othermenu") and self.othermenu is not None + and self.frame is not None + and not exiting): + + i = self.othermenu.FindMenuItem(self.menuname,self.disableitem) + self.othermenu.Enable(i,0) + i = self.othermenu.FindMenuItem(self.menuname,self.enableitem) + self.othermenu.Enable(i,1) + + if not hasattr(self,"no__debug__"): + for m in sys.modules.values(): + if m is not None:# and m.__dict__.has_key("__debug__"): + m.__dict__["__debug__"] = 0 + + if self.frame is not None: # typically True, but, e.g., allows + # DisableOutput method (which calls this + # one) to be called when the frame is not + # actually open, so that it is always safe + # to call this method... + frame = self.frame + self.frame = self.text = None + frame.Destroy() + self.Enabled = 1 + + + def EnableOutput(self, + event=None,# event must be the first optional argument... + othermenubar=None, + menuname="Debug", + enableitem="Enable output", + disableitem="Disable output"): + + if othermenubar is not None: + self.SetOtherMenuBar(othermenubar, + menuname=menuname, + enableitem=enableitem, + disableitem=disableitem) + self.Enabled = 1 + if self.f: + self.f.close() + self.f = None + self.write("") + + + def CloseFile(self): + if self.f: + if self.frame: + self.frame.sb.SetStatusText("File '%s' closed..." + % os.path.abspath(self.f.name), + 0) + self.f.close () + self.f = None + else: + if self.frame: + self.frame.sb.SetStatusText("") + if self.frame: + self.frame.sb.Refresh() + return 1 + + + def OpenNewFile(self): + self.CloseFile() + dlg = wx.FileDialog(self.frame, + "Choose a new log file", self.dir,"","*", + wx.SAVE | wx.OVERWRITE_PROMPT) + if dlg.ShowModal() == wx.ID_CANCEL: + dlg.Destroy() + return 0 + else: + try: + self.f = open(os.path.abspath(dlg.GetPath()),'w') + except EnvironmentError: + dlg.Destroy() + return 0 + dlg.Destroy() + if self.frame: + self.frame.sb.SetStatusText("File '%s' opened..." + % os.path.abspath(self.f.name), + 0) + if hasattr(self,"nofile"): + self.frame.sb = _MyStatusBar(self.frame, + callbacks=[self.DisableOutput, + self.CloseFile, + self.OpenNewFile]) + self.frame.SetStatusBar(self.frame.sb) + if hasattr(self,"nofile"): + delattr(self,"nofile") + return 1 + + + def DisableOutput(self, + event=None,# event must be the first optional argument... + exiting=0): + self.write(".DisableOutput()\n") + if hasattr(self,"frame") \ + and self.frame is not None: + self.OnCloseWindow("Dummy",exiting=exiting) + self.Enabled = 0 + + + def close(self): + self.DisableOutput() + + + def flush(self): + if self.text: + self.text.SetInsertionPointEnd() + wx.Yield() + + + def __call__(self,* args): + for s in args: + self.write (str (s)) + + + def SetOutputDirectory(self,dir): + self.dir = os.path.abspath(dir) +## sys.__stderr__.write("Directory: os.path.abspath(%s) = %s\n" +## % (dir,self.dir)) + + + +class Dummy_PyInformationalMessagesFrame(object): + def __init__(self,progname=""): + self.softspace = 1 + def __call__(self,*args): + pass + def write(self,s): + pass + def flush(self): + pass + def close(self): + pass + def EnableOutput(self): + pass + def __call__(self,* args): + pass + def DisableOutput(self,exiting=0): + pass + def SetParent(self,wX): + pass + diff --git a/wx/lib/inspection.py b/wx/lib/inspection.py new file mode 100644 index 00000000..5a8f8d28 --- /dev/null +++ b/wx/lib/inspection.py @@ -0,0 +1,1237 @@ +#---------------------------------------------------------------------------- +# Name: wx.lib.inspection +# Purpose: A widget inspection tool that allows easy introspection of +# all the live widgets and sizers in an application. +# +# Author: Robin Dunn +# +# Created: 26-Jan-2007 +# RCS-ID: $Id$ +# Copyright: (c) 2007 by Total Control Software +# Licence: wxWindows license +#---------------------------------------------------------------------------- + +# NOTE: This class was originally based on ideas sent to the +# wxPython-users mail list by Dan Eloff. See also +# wx.lib.mixins.inspect for a class that can be mixed-in with wx.App +# to provide Hot-Key access to the inspection tool. + +import wx +import wx.py +import wx.stc +#import wx.aui as aui +import wx.lib.agw.aui as aui +import wx.lib.utils as utils +import sys +import inspect + +#---------------------------------------------------------------------------- + +class InspectionTool: + """ + The InspectionTool is a singleton that manages creating and + showing an InspectionFrame. + """ + + # Note: This is the Borg design pattern which ensures that all + # instances of this class are actually using the same set of + # instance data. See + # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66531 + __shared_state = {} + def __init__(self): + self.__dict__ = self.__shared_state + if not hasattr(self, 'initialized'): + self.initialized = False + + def Init(self, pos=wx.DefaultPosition, size=wx.Size(850,700), + config=None, locals=None, app=None): + """ + Init is used to set some parameters that will be used later + when the inspection tool is shown. Suitable defaults will be + used for all of these parameters if they are not provided. + + :param pos: The default position to show the frame at + :param size: The default size of the frame + :param config: A wx.Config object to be used to store layout + and other info to when the inspection frame is closed. + This info will be restored the next time the inspection + frame is used. + :param locals: A dictionary of names to be added to the PyCrust + namespace. + :param app: A reference to the wx.App object. + """ + self._frame = None + self._pos = pos + self._size = size + self._config = config + self._locals = locals + self._app = app + if not self._app: + self._app = wx.GetApp() + self.initialized = True + + + def Show(self, selectObj=None, refreshTree=False): + """ + Creates the inspection frame if it hasn't been already, and + raises it if neccessary. Pass a widget or sizer in selectObj + to have that object be preselected in widget tree. If + refreshTree is True then the widget tree will be rebuilt, + otherwise if the tree has already been built it will be left + alone. + """ + if not self.initialized: + self.Init() + + parent = self._app.GetTopWindow() + if not selectObj: + selectObj = parent + if not self._frame: + self._frame = InspectionFrame( parent=parent, + pos=self._pos, + size=self._size, + config=self._config, + locals=self._locals, + app=self._app) + if selectObj: + self._frame.SetObj(selectObj) + if refreshTree: + self._frame.RefreshTree() + self._frame.Show() + if self._frame.IsIconized(): + self._frame.Iconize(False) + self._frame.Raise() + + +#---------------------------------------------------------------------------- + + +class InspectionFrame(wx.Frame): + """ + This class is the frame that holds the wxPython inspection tools. + The toolbar and AUI splitters/floating panes are also managed + here. The contents of the tool windows are handled by other + classes. + """ + def __init__(self, wnd=None, locals=None, config=None, + app=None, title="wxPython Widget Inspection Tool", + *args, **kw): + kw['title'] = title + wx.Frame.__init__(self, *args, **kw) + + self.SetExtraStyle(wx.WS_EX_BLOCK_EVENTS) + self.includeSizers = False + self.started = False + + self.SetIcon(Icon.GetIcon()) + self.MakeToolBar() + panel = wx.Panel(self, size=self.GetClientSize()) + + # tell FrameManager to manage this frame + self.mgr = aui.AuiManager(panel, + aui.AUI_MGR_DEFAULT + | aui.AUI_MGR_TRANSPARENT_DRAG + | aui.AUI_MGR_ALLOW_ACTIVE_PANE) + + # make the child tools + self.tree = InspectionTree(panel, size=(100,300)) + self.info = InspectionInfoPanel(panel, + style=wx.NO_BORDER, + ) + + if not locals: + locals = {} + myIntroText = ( + "Python %s on %s, wxPython %s\n" + "NOTE: The 'obj' variable refers to the object selected in the tree." + % (sys.version.split()[0], sys.platform, wx.version())) + self.crust = wx.py.crust.Crust(panel, locals=locals, + intro=myIntroText, + showInterpIntro=False, + style=wx.NO_BORDER, + ) + self.locals = self.crust.shell.interp.locals + self.crust.shell.interp.introText = '' + self.locals['obj'] = self.obj = wnd + self.locals['app'] = app + self.locals['wx'] = wx + wx.CallAfter(self._postStartup) + + # put the chlid tools in AUI panes + self.mgr.AddPane(self.info, + aui.AuiPaneInfo().Name("info").Caption("Object Info"). + CenterPane().CaptionVisible(True). + CloseButton(False).MaximizeButton(True) + ) + self.mgr.AddPane(self.tree, + aui.AuiPaneInfo().Name("tree").Caption("Widget Tree"). + CaptionVisible(True).Left().Dockable(True).Floatable(True). + BestSize((280,200)).CloseButton(False).MaximizeButton(True) + ) + self.mgr.AddPane(self.crust, + aui.AuiPaneInfo().Name("crust").Caption("PyCrust"). + CaptionVisible(True).Bottom().Dockable(True).Floatable(True). + BestSize((400,200)).CloseButton(False).MaximizeButton(True) + ) + + self.mgr.Update() + + if config is None: + config = wx.Config('wxpyinspector') + self.config = config + self.Bind(wx.EVT_CLOSE, self.OnClose) + if self.Parent: + tlw = self.Parent.GetTopLevelParent() + tlw.Bind(wx.EVT_CLOSE, self.OnClose) + self.LoadSettings(self.config) + self.crust.shell.lineNumbers = False + self.crust.shell.setDisplayLineNumbers(False) + self.crust.shell.SetMarginWidth(1, 0) + + + def MakeToolBar(self): + tbar = self.CreateToolBar(wx.TB_HORIZONTAL | wx.TB_FLAT | wx.TB_TEXT | wx.NO_BORDER ) + tbar.SetToolBitmapSize((24,24)) + + refreshBmp = Refresh.GetBitmap() + findWidgetBmp = Find.GetBitmap() + showSizersBmp = ShowSizers.GetBitmap() + expandTreeBmp = ExpandTree.GetBitmap() + collapseTreeBmp = CollapseTree.GetBitmap() + highlightItemBmp = HighlightItem.GetBitmap() + evtWatcherBmp = EvtWatcher.GetBitmap() + + toggleFillingBmp = ShowFilling.GetBitmap() + + refreshTool = tbar.AddLabelTool(-1, 'Refresh', refreshBmp, + shortHelp = 'Refresh widget tree (F1)') + findWidgetTool = tbar.AddLabelTool(-1, 'Find', findWidgetBmp, + shortHelp='Find new target widget. (F2) Click here and\nthen on another widget in the app.') + showSizersTool = tbar.AddLabelTool(-1, 'Sizers', showSizersBmp, + shortHelp='Include sizers in widget tree (F3)', + kind=wx.ITEM_CHECK) + expandTreeTool = tbar.AddLabelTool(-1, 'Expand', expandTreeBmp, + shortHelp='Expand all tree items (F4)') + collapseTreeTool = tbar.AddLabelTool(-1, 'Collapse', collapseTreeBmp, + shortHelp='Collapse all tree items (F5)') + highlightItemTool = tbar.AddLabelTool(-1, 'Highlight', highlightItemBmp, + shortHelp='Attempt to highlight live item (F6)') + evtWatcherTool = tbar.AddLabelTool(-1, 'Events', evtWatcherBmp, + shortHelp='Watch the events of the selected item (F7)') + + toggleFillingTool = tbar.AddLabelTool(-1, 'Filling', toggleFillingBmp, + shortHelp='Show PyCrust \'filling\' (F8)', + kind=wx.ITEM_CHECK) + tbar.Realize() + + self.Bind(wx.EVT_TOOL, self.OnRefreshTree, refreshTool) + self.Bind(wx.EVT_TOOL, self.OnFindWidget, findWidgetTool) + self.Bind(wx.EVT_TOOL, self.OnShowSizers, showSizersTool) + self.Bind(wx.EVT_TOOL, self.OnExpandTree, expandTreeTool) + self.Bind(wx.EVT_TOOL, self.OnCollapseTree, collapseTreeTool) + self.Bind(wx.EVT_TOOL, self.OnHighlightItem, highlightItemTool) + self.Bind(wx.EVT_TOOL, self.OnWatchEvents, evtWatcherTool) + self.Bind(wx.EVT_TOOL, self.OnToggleFilling, toggleFillingTool) + self.Bind(wx.EVT_UPDATE_UI, self.OnShowSizersUI, showSizersTool) + self.Bind(wx.EVT_UPDATE_UI, self.OnWatchEventsUI, evtWatcherTool) + self.Bind(wx.EVT_UPDATE_UI, self.OnToggleFillingUI, toggleFillingTool) + + tbl = wx.AcceleratorTable( + [(wx.ACCEL_NORMAL, wx.WXK_F1, refreshTool.GetId()), + (wx.ACCEL_NORMAL, wx.WXK_F2, findWidgetTool.GetId()), + (wx.ACCEL_NORMAL, wx.WXK_F3, showSizersTool.GetId()), + (wx.ACCEL_NORMAL, wx.WXK_F4, expandTreeTool.GetId()), + (wx.ACCEL_NORMAL, wx.WXK_F5, collapseTreeTool.GetId()), + (wx.ACCEL_NORMAL, wx.WXK_F6, highlightItemTool.GetId()), + (wx.ACCEL_NORMAL, wx.WXK_F7, evtWatcherTool.GetId()), + (wx.ACCEL_NORMAL, wx.WXK_F8, toggleFillingTool.GetId()), + ]) + self.SetAcceleratorTable(tbl) + + + def _postStartup(self): + if self.crust.ToolsShown(): + self.crust.ToggleTools() + self.UpdateInfo() + self.started = True + + + def OnClose(self, evt): + self.SaveSettings(self.config) + evt.Skip() + if hasattr(self, 'mgr'): + self.mgr.UnInit() + del self.mgr + if self.Parent: + tlw = self.Parent.GetTopLevelParent() + tlw.Unbind(wx.EVT_CLOSE, handler=self.OnClose) + + + def UpdateInfo(self): + self.info.UpdateInfo(self.obj) + + + def SetObj(self, obj): + if self.obj is obj: + return + self.locals['obj'] = self.obj = obj + self.UpdateInfo() + if not self.tree.built: + self.tree.BuildTree(obj, includeSizers=self.includeSizers) + else: + self.tree.SelectObj(obj) + + + def HighlightCurrentItem(self): + """ + Draw a highlight rectangle around the item represented by the + current tree selection. + """ + if not hasattr(self, 'highlighter'): + self.highlighter = _InspectionHighlighter() + self.highlighter.HighlightCurrentItem(self.tree) + + + def RefreshTree(self): + self.tree.BuildTree(self.obj, includeSizers=self.includeSizers) + + + def OnRefreshTree(self, evt): + self.RefreshTree() + self.UpdateInfo() + + + def OnFindWidget(self, evt): + self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) + self.Bind(wx.EVT_MOUSE_CAPTURE_LOST, self.OnCaptureLost) + self.CaptureMouse() + self.finding = wx.BusyInfo("Click on any widget in the app...") + + + def OnCaptureLost(self, evt): + self.Unbind(wx.EVT_LEFT_DOWN) + self.Unbind(wx.EVT_MOUSE_CAPTURE_LOST) + del self.finding + + def OnLeftDown(self, evt): + self.ReleaseMouse() + wnd = wx.FindWindowAtPointer() + if wnd is not None: + self.SetObj(wnd) + else: + wx.Bell() + self.OnCaptureLost(evt) + + + def OnShowSizers(self, evt): + self.includeSizers = not self.includeSizers + self.RefreshTree() + + + def OnExpandTree(self, evt): + current = self.tree.GetSelection() + self.tree.ExpandAll() + self.tree.EnsureVisible(current) + + + def OnCollapseTree(self, evt): + current = self.tree.GetSelection() + self.tree.CollapseAll() + self.tree.EnsureVisible(current) + self.tree.SelectItem(current) + + + def OnHighlightItem(self, evt): + self.HighlightCurrentItem() + + + def OnWatchEvents(self, evt): + item = self.tree.GetSelection() + obj = self.tree.GetItemPyData(item) + if isinstance(obj, wx.Window): + import wx.lib.eventwatcher as ew + watcher = ew.EventWatcher(self) + watcher.watch(obj) + watcher.Show() + + def OnWatchEventsUI(self, evt): + item = self.tree.GetSelection() + if item: + obj = self.tree.GetItemPyData(item) + evt.Enable(isinstance(obj, wx.Window)) + + + def OnToggleFilling(self, evt): + self.crust.ToggleTools() + + + def OnShowSizersUI(self, evt): + evt.Check(self.includeSizers) + + + def OnToggleFillingUI(self, evt): + if self.started: + evt.Check(self.crust.ToolsShown()) + + + def LoadSettings(self, config): + self.crust.LoadSettings(config) + self.info.LoadSettings(config) + + pos = wx.Point(config.ReadInt('Window/PosX', -1), + config.ReadInt('Window/PosY', -1)) + + size = wx.Size(config.ReadInt('Window/Width', -1), + config.ReadInt('Window/Height', -1)) + self.SetSize(size) + self.Move(pos) + rect = utils.AdjustRectToScreen(self.GetRect()) + self.SetRect(rect) + + perspective = config.Read('perspective', '') + if perspective: + try: + self.mgr.LoadPerspective(perspective) + except wx.PyAssertionError: + # ignore bad perspective string errors + pass + self.includeSizers = config.ReadBool('includeSizers', False) + + + def SaveSettings(self, config): + self.crust.SaveSettings(config) + self.info.SaveSettings(config) + + if not self.IsIconized() and not self.IsMaximized(): + w, h = self.GetSize() + config.WriteInt('Window/Width', w) + config.WriteInt('Window/Height', h) + + px, py = self.GetPosition() + config.WriteInt('Window/PosX', px) + config.WriteInt('Window/PosY', py) + + perspective = self.mgr.SavePerspective() + config.Write('perspective', perspective) + config.WriteBool('includeSizers', self.includeSizers) + +#--------------------------------------------------------------------------- + +# should inspection frame (and children) be includeed in the tree? +INCLUDE_INSPECTOR = True + +USE_CUSTOMTREECTRL = False +if USE_CUSTOMTREECTRL: + import wx.lib.agw.customtreectrl as CT + TreeBaseClass = CT.CustomTreeCtrl +else: + TreeBaseClass = wx.TreeCtrl + +class InspectionTree(TreeBaseClass): + """ + All of the widgets in the app, and optionally their sizers, are + loaded into this tree. + """ + def __init__(self, *args, **kw): + #s = kw.get('style', 0) + #kw['style'] = s | wx.TR_DEFAULT_STYLE | wx.TR_HIDE_ROOT + TreeBaseClass.__init__(self, *args, **kw) + self.roots = [] + self.built = False + self.Bind(wx.EVT_TREE_SEL_CHANGED, self.OnSelectionChanged) + self.toolFrame = wx.GetTopLevelParent(self) + if 'wxMac' in wx.PlatformInfo: + self.SetWindowVariant(wx.WINDOW_VARIANT_SMALL) + + + def BuildTree(self, startWidget, includeSizers=False, expandFrame=False): + if self.GetCount(): + self.DeleteAllItems() + self.roots = [] + self.built = False + + realRoot = self.AddRoot('Top-level Windows') + + for w in wx.GetTopLevelWindows(): + if w is wx.GetTopLevelParent(self) and not INCLUDE_INSPECTOR: + continue + root = self._AddWidget(realRoot, w, includeSizers) + self.roots.append(root) + + # Expand the subtree containing the startWidget, and select it. + if not startWidget or not isinstance(startWidget, wx.Window): + startWidget = wx.GetApp().GetTopWindow() + if expandFrame: + top = wx.GetTopLevelParent(startWidget) + topItem = self.FindWidgetItem(top) + if topItem: + self.ExpandAllChildren(topItem) + self.built = True + self.SelectObj(startWidget) + + + def _AddWidget(self, parentItem, widget, includeSizers): + text = self.GetTextForWidget(widget) + item = self.AppendItem(parentItem, text) + self.SetItemPyData(item, widget) + + # Add the sizer and widgets in the sizer, if we're showing them + widgetsInSizer = [] + if includeSizers and widget.GetSizer() is not None: + widgetsInSizer = self._AddSizer(item, widget.GetSizer()) + + # Add any children not in the sizer, or all children if we're + # not showing the sizers + for child in widget.GetChildren(): + if (not child in widgetsInSizer and + (not child.IsTopLevel() or + isinstance(child, wx.PopupWindow))): + self._AddWidget(item, child, includeSizers) + + return item + + + def _AddSizer(self, parentItem, sizer): + widgets = [] + text = self.GetTextForSizer(sizer) + item = self.AppendItem(parentItem, text) + self.SetItemPyData(item, sizer) + self.SetItemTextColour(item, "blue") + + for si in sizer.GetChildren(): + if si.IsWindow(): + w = si.GetWindow() + self._AddWidget(item, w, True) + widgets.append(w) + elif si.IsSizer(): + ss = si.GetSizer() + widgets += self._AddSizer(item, ss) + ss._parentSizer = sizer + else: + i = self.AppendItem(item, "Spacer") + self.SetItemPyData(i, si) + self.SetItemTextColour(i, "blue") + return widgets + + + def FindWidgetItem(self, widget): + """ + Find the tree item for a widget. + """ + for item in self.roots: + found = self._FindWidgetItem(widget, item) + if found: + return found + return None + + def _FindWidgetItem(self, widget, item): + if self.GetItemPyData(item) is widget: + return item + child, cookie = self.GetFirstChild(item) + while child: + found = self._FindWidgetItem(widget, child) + if found: + return found + child, cookie = self.GetNextChild(item, cookie) + return None + + + def GetTextForWidget(self, widget): + """ + Returns the string to be used in the tree for a widget + """ + if hasattr(widget, 'GetName'): + return "%s (\"%s\")" % (widget.__class__.__name__, widget.GetName()) + return widget.__class__.__name__ + + + def GetTextForSizer(self, sizer): + """ + Returns the string to be used in the tree for a sizer + """ + return "%s" % sizer.__class__.__name__ + + + def SelectObj(self, obj): + item = self.FindWidgetItem(obj) + if item: + self.EnsureVisible(item) + self.SelectItem(item) + + + def OnSelectionChanged(self, evt): + item = evt.GetItem() + if item: + obj = self.GetItemPyData(item) + self.toolFrame.SetObj(obj) + + +#--------------------------------------------------------------------------- + +class InspectionInfoPanel(wx.stc.StyledTextCtrl): + """ + Used to display information about the currently selected items. + Currently just a read-only wx.stc.StyledTextCtrl with some plain + text. Should probably add some styles to make things easier to + read. + """ + def __init__(self, *args, **kw): + wx.stc.StyledTextCtrl.__init__(self, *args, **kw) + + from wx.py.editwindow import FACES + self.StyleSetSpec(wx.stc.STC_STYLE_DEFAULT, + "face:%(mono)s,size:%(size)d,back:%(backcol)s" % FACES) + self.StyleClearAll() + self.SetReadOnly(True) + self.SetMarginType(1, 0) + self.SetMarginWidth(1, 0) + self.SetSelForeground(True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHTTEXT)) + self.SetSelBackground(True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHT)) + + + def LoadSettings(self, config): + zoom = config.ReadInt('View/Zoom/Info', 0) + self.SetZoom(zoom) + + def SaveSettings(self, config): + config.WriteInt('View/Zoom/Info', self.GetZoom()) + + + def UpdateInfo(self, obj): + st = [] + if not obj: + st.append("Item is None or has been destroyed.") + + elif isinstance(obj, wx.Window): + st += self.FmtWidget(obj) + + elif isinstance(obj, wx.Sizer): + st += self.FmtSizer(obj) + + elif isinstance(obj, wx.SizerItem): + st += self.FmtSizerItem(obj) + + self.SetReadOnly(False) + self.SetText('\n'.join(st)) + self.SetReadOnly(True) + + + def Fmt(self, name, value): + if isinstance(value, (str, unicode)): + return " %s = '%s'" % (name, value) + else: + return " %s = %s" % (name, value) + + + def FmtWidget(self, obj): + def _countChildren(children): + count = 0 + for child in children: + if not child.IsTopLevel(): + count += 1 + count += _countChildren(child.GetChildren()) + return count + def _countAllChildren(children): + count = 0 + for child in children: + count += 1 + count += _countAllChildren(child.GetChildren()) + return count + + count = len([c for c in obj.GetChildren() if not c.IsTopLevel()]) + rcount = _countChildren(obj.GetChildren()) + tlwcount = _countAllChildren(obj.GetChildren()) + + st = ["Widget:"] + if hasattr(obj, 'GetName'): + st.append(self.Fmt('name', obj.GetName())) + st.append(self.Fmt('class', obj.__class__)) + st.append(self.Fmt('bases', obj.__class__.__bases__)) + st.append(self.Fmt('module', inspect.getmodule(obj))) + if hasattr(obj, 'this'): + st.append(self.Fmt('this', repr(obj.this))) + st.append(self.Fmt('id', obj.GetId())) + st.append(self.Fmt('style', obj.GetWindowStyle())) + st.append(self.Fmt('pos', obj.GetPosition())) + st.append(self.Fmt('size', obj.GetSize())) + st.append(self.Fmt('minsize', obj.GetMinSize())) + st.append(self.Fmt('bestsize', obj.GetBestSize())) + st.append(self.Fmt('client size', obj.GetClientSize())) + st.append(self.Fmt('virtual size',obj.GetVirtualSize())) + st.append(self.Fmt('IsEnabled', obj.IsEnabled())) + st.append(self.Fmt('IsShown', obj.IsShown())) + st.append(self.Fmt('IsFrozen', obj.IsFrozen())) + st.append(self.Fmt('fg color', obj.GetForegroundColour())) + st.append(self.Fmt('bg color', obj.GetBackgroundColour())) + st.append(self.Fmt('label', obj.GetLabel())) + if hasattr(obj, 'GetTitle'): + st.append(self.Fmt('title', obj.GetTitle())) + if hasattr(obj, 'GetValue'): + try: + st.append(self.Fmt('value', obj.GetValue())) + except: + pass + st.append(' child count = %d (direct) %d (recursive) %d (include TLWs)' % + (count, rcount, tlwcount)) + if obj.GetContainingSizer() is not None: + st.append('') + sizer = obj.GetContainingSizer() + st += self.FmtSizerItem(sizer.GetItem(obj)) + return st + + + def FmtSizerItem(self, obj): + if obj is None: + return ['SizerItem: None'] + + st = ['SizerItem:'] + st.append(self.Fmt('proportion', obj.GetProportion())) + st.append(self.Fmt('flag', + FlagsFormatter(itemFlags, obj.GetFlag()))) + st.append(self.Fmt('border', obj.GetBorder())) + st.append(self.Fmt('pos', obj.GetPosition())) + st.append(self.Fmt('size', obj.GetSize())) + st.append(self.Fmt('minsize', obj.GetMinSize())) + st.append(self.Fmt('ratio', obj.GetRatio())) + st.append(self.Fmt('IsWindow', obj.IsWindow())) + st.append(self.Fmt('IsSizer', obj.IsSizer())) + st.append(self.Fmt('IsSpacer', obj.IsSpacer())) + st.append(self.Fmt('IsShown', obj.IsShown())) + if isinstance(obj, wx.GBSizerItem): + st.append(self.Fmt('cellpos', obj.GetPos())) + st.append(self.Fmt('cellspan', obj.GetSpan())) + st.append(self.Fmt('endpos', obj.GetEndPos())) + return st + + + def FmtSizer(self, obj): + st = ['Sizer:'] + st.append(self.Fmt('class', obj.__class__)) + if hasattr(obj, 'this'): + st.append(self.Fmt('this', repr(obj.this))) + st.append(self.Fmt('pos', obj.GetPosition())) + st.append(self.Fmt('size', obj.GetSize())) + st.append(self.Fmt('minsize', obj.GetMinSize())) + if isinstance(obj, wx.BoxSizer): + st.append(self.Fmt('orientation', + FlagsFormatter(orientFlags, obj.GetOrientation()))) + if isinstance(obj, wx.GridSizer): + st.append(self.Fmt('cols', obj.GetCols())) + st.append(self.Fmt('rows', obj.GetRows())) + st.append(self.Fmt('vgap', obj.GetVGap())) + st.append(self.Fmt('hgap', obj.GetHGap())) + if isinstance(obj, wx.FlexGridSizer): + st.append(self.Fmt('rowheights', obj.GetRowHeights())) + st.append(self.Fmt('colwidths', obj.GetColWidths())) + st.append(self.Fmt('flexdir', + FlagsFormatter(orientFlags, obj.GetFlexibleDirection()))) + st.append(self.Fmt('nonflexmode', + FlagsFormatter(flexmodeFlags, obj.GetNonFlexibleGrowMode()))) + if isinstance(obj, wx.GridBagSizer): + st.append(self.Fmt('emptycell', obj.GetEmptyCellSize())) + + if hasattr(obj, '_parentSizer'): + st.append('') + st += self.FmtSizerItem(obj._parentSizer.GetItem(obj)) + + return st + + +class FlagsFormatter(object): + def __init__(self, d, val): + self.d = d + self.val = val + + def __str__(self): + st = [] + for k in self.d.keys(): + if self.val & k: + st.append(self.d[k]) + if st: + return '|'.join(st) + else: + return '0' + +orientFlags = { + wx.HORIZONTAL : 'wx.HORIZONTAL', + wx.VERTICAL : 'wx.VERTICAL', + } + +itemFlags = { + wx.TOP : 'wx.TOP', + wx.BOTTOM : 'wx.BOTTOM', + wx.LEFT : 'wx.LEFT', + wx.RIGHT : 'wx.RIGHT', +# wx.ALL : 'wx.ALL', + wx.EXPAND : 'wx.EXPAND', +# wx.GROW : 'wx.GROW', + wx.SHAPED : 'wx.SHAPED', + wx.STRETCH_NOT : 'wx.STRETCH_NOT', +# wx.ALIGN_CENTER : 'wx.ALIGN_CENTER', + wx.ALIGN_LEFT : 'wx.ALIGN_LEFT', + wx.ALIGN_RIGHT : 'wx.ALIGN_RIGHT', + wx.ALIGN_TOP : 'wx.ALIGN_TOP', + wx.ALIGN_BOTTOM : 'wx.ALIGN_BOTTOM', + wx.ALIGN_CENTER_VERTICAL : 'wx.ALIGN_CENTER_VERTICAL', + wx.ALIGN_CENTER_HORIZONTAL : 'wx.ALIGN_CENTER_HORIZONTAL', + wx.ADJUST_MINSIZE : 'wx.ADJUST_MINSIZE', + wx.FIXED_MINSIZE : 'wx.FIXED_MINSIZE', + } + +flexmodeFlags = { + wx.FLEX_GROWMODE_NONE : 'wx.FLEX_GROWMODE_NONE', + wx.FLEX_GROWMODE_SPECIFIED : 'wx.FLEX_GROWMODE_SPECIFIED', + wx.FLEX_GROWMODE_ALL : 'wx.FLEX_GROWMODE_ALL', + } + + + + +#--------------------------------------------------------------------------- + +class _InspectionHighlighter(object): + """ + All the highlighting code. A separate class to help reduce the + clutter in InspectionFrame. + """ + + # should non TLWs be flashed too? Otherwise use a highlight rectangle + flashAll = False + + color1 = 'red' # for widgets and sizers + color2 = 'red' # for item boundaries in sizers + color3 = '#00008B' # for items in sizers + + highlightTime = 3000 # how long to display the highlights + + # how to draw it + useOverlay = 'wxMac' in wx.PlatformInfo + + + def __init__(self): + if self.useOverlay: + self.overlay = wx.Overlay() + + + def HighlightCurrentItem(self, tree): + """ + Draw a highlight rectangle around the item represented by the + current tree selection. + """ + item = tree.GetSelection() + obj = tree.GetItemPyData(item) + + if isinstance(obj, wx.Window): + self.HighlightWindow(obj) + + elif isinstance(obj, wx.Sizer): + self.HighlightSizer(obj) + + elif isinstance(obj, wx.SizerItem): # Spacer + pItem = tree.GetItemParent(item) + sizer = tree.GetItemPyData(pItem) + self.HighlightSizerItem(obj, sizer) + + else: + raise RuntimeError("unknown object type: %s" % obj.__class__.__name__) + + + def HighlightWindow(self, win): + rect = win.GetRect() + tlw = win.GetTopLevelParent() + if self.flashAll or tlw is win: + self.FlickerTLW(win) + return + else: + pos = self.FindHighlightPos(tlw, win.ClientToScreen((0,0))) + rect.SetPosition(pos) + self.DoHighlight(tlw, rect, self.color1) + + + def HighlightSizerItem(self, item, sizer, penWidth=2): + win = sizer.GetContainingWindow() + tlw = win.GetTopLevelParent() + rect = item.GetRect() + pos = rect.GetPosition() + pos = self.FindHighlightPos(tlw, win.ClientToScreen(pos)) + rect.SetPosition(pos) + if rect.width < 1: rect.width = 1 + if rect.width < 1: rect.width = 1 + self.DoHighlight(tlw, rect, self.color1, penWidth) + + + def HighlightSizer(self, sizer): + # first do the outline of the whole sizer like normal + win = sizer.GetContainingWindow() + tlw = win.GetTopLevelParent() + pos = sizer.GetPosition() + pos = self.FindHighlightPos(tlw, win.ClientToScreen(pos)) + rect = wx.RectPS(pos, sizer.GetSize()) + dc, dco = self.DoHighlight(tlw, rect, self.color1) + + # Now highlight the actual items within the sizer. This may + # get overdrawn by the code below for item boundaries, but if + # there is border padding then this will help make it more + # obvious. + dc.SetPen(wx.Pen(self.color3, 1)) + for item in sizer.GetChildren(): + if item.IsShown(): + if item.IsWindow(): + r = item.GetWindow().GetRect() + elif item.IsSizer(): + p = item.GetSizer().GetPosition() + s = item.GetSizer().GetSize() + r = wx.RectPS(p,s) + else: + continue + r = self.AdjustRect(tlw, win, r) + dc.DrawRectangleRect(r) + + # Next highlight the area allocated to each item in the sizer. + # Each kind of sizer will need to be done a little + # differently. + dc.SetPen(wx.Pen(self.color2, 1)) + + if isinstance(sizer, wx.WrapSizer): + for item in sizer.GetChildren(): + ir = self.AdjustRect(tlw, win, item.Rect) + dc.DrawRectangleRect(ir) + + # wx.BoxSizer, wx.StaticBoxSizer + elif isinstance(sizer, wx.BoxSizer): + # NOTE: we have to do some reverse-engineering here for + # borders because the sizer and sizer item don't give us + # enough information to know for sure where item + # (allocated) boundaries are, just the boundaries of the + # actual widgets. TODO: It would be nice to add something + # to wx.SizerItem that would give us the full bounds, but + # that will have to wait until 2.9... + x, y = rect.GetPosition() + if sizer.Orientation == wx.HORIZONTAL: + y1 = y + rect.height + for item in sizer.GetChildren(): + ir = self.AdjustRect(tlw, win, item.Rect) + x = ir.x + if item.Flag & wx.LEFT: + x -= item.Border + dc.DrawLine(x, y, x, y1) + if item.IsSizer(): + dc.DrawRectangleRect(ir) + + if sizer.Orientation == wx.VERTICAL: + x1 = x + rect.width + for item in sizer.GetChildren(): + ir = self.AdjustRect(tlw, win, item.Rect) + y = ir.y + if item.Flag & wx.TOP: + y -= item.Border + dc.DrawLine(x, y, x1, y) + if item.IsSizer(): + dc.DrawRectangleRect(ir) + + # wx.FlexGridSizer, wx.GridBagSizer + elif isinstance(sizer, wx.FlexGridSizer): + sizer.Layout() + y = rect.y + for rh in sizer.RowHeights[:-1]: + y += rh + dc.DrawLine(rect.x, y, rect.x+rect.width, y) + y+= sizer.VGap + dc.DrawLine(rect.x, y, rect.x+rect.width, y) + x = rect.x + for cw in sizer.ColWidths[:-1]: + x += cw + dc.DrawLine(x, rect.y, x, rect.y+rect.height) + x+= sizer.HGap + dc.DrawLine(x, rect.y, x, rect.y+rect.height) + + # wx.GridSizer + elif isinstance(sizer, wx.GridSizer): + # NOTE: More reverse engineering (see above.) This time we + # need to determine what the sizer is using for row + # heights and column widths. + #rh = cw = 0 + #for item in sizer.GetChildren(): + # rh = max(rh, item.Size.height) + # cw = max(cw, item.Size.width) + cw = (rect.width - sizer.HGap*(sizer.Cols-1)) / sizer.Cols + rh = (rect.height - sizer.VGap*(sizer.Rows-1)) / sizer.Rows + y = rect.y + for i in range(sizer.Rows-1): + y += rh + dc.DrawLine(rect.x, y, rect.x+rect.width, y) + y+= sizer.VGap + dc.DrawLine(rect.x, y, rect.x+rect.width, y) + x = rect.x + for i in range(sizer.Cols-1): + x += cw + dc.DrawLine(x, rect.y, x, rect.y+rect.height) + x+= sizer.HGap + dc.DrawLine(x, rect.y, x, rect.y+rect.height) + + # Anything else is probably a custom sizer, just highlight the items + else: + del dc, odc + for item in sizer.GetChildren(): + self.HighlightSizerItem(item, sizer, 1) + + + def FindHighlightPos(self, tlw, pos): + if self.useOverlay: + # We'll be using a ClientDC in this case so adjust the + # position accordingly + pos = tlw.ScreenToClient(pos) + return pos + + + def AdjustRect(self, tlw, win, rect): + pos = self.FindHighlightPos(tlw, win.ClientToScreen(rect.Position)) + rect.Position = pos + return wx.RectPS(pos, rect.Size) + + + def DoHighlight(self, tlw, rect, colour, penWidth=2): + if not tlw.IsFrozen(): + tlw.Freeze() + + if self.useOverlay: + dc = wx.ClientDC(tlw) + dco = wx.DCOverlay(self.overlay, dc) + dco.Clear() + else: + dc = wx.ScreenDC() + dco = None + + dc.SetPen(wx.Pen(colour, penWidth)) + dc.SetBrush(wx.TRANSPARENT_BRUSH) + + drawRect = wx.Rect(*rect) + dc.DrawRectangleRect(drawRect) + + drawRect.Inflate(2,2) + if not self.useOverlay: + pos = tlw.ScreenToClient(drawRect.GetPosition()) + drawRect.SetPosition(pos) + wx.CallLater(self.highlightTime, self.DoUnhighlight, tlw, drawRect) + + return dc, dco + + + def DoUnhighlight(self, tlw, rect): + if not tlw: + return + if tlw.IsFrozen(): + tlw.Thaw() + if self.useOverlay: + dc = wx.ClientDC(tlw) + dco = wx.DCOverlay(self.overlay, dc) + dco.Clear() + del dc, dco + self.overlay.Reset() + else: + tlw.RefreshRect(rect) + + + def FlickerTLW(self, tlw): + """ + Use a timer to alternate a TLW between shown and hidded state a + few times. Use to highlight a TLW since drawing and clearing an + outline is trickier. + """ + self.flickerCount = 0 + tlw.Hide() + self.cl = wx.CallLater(300, self._Toggle, tlw) + + + def _Toggle(self, tlw): + if tlw.IsShown(): + tlw.Hide() + self.cl.Restart() + else: + tlw.Show() + self.flickerCount += 1 + if self.flickerCount < 4: + self.cl.Restart() + + +#--------------------------------------------------------------------------- +from wx.lib.embeddedimage import PyEmbeddedImage + +Refresh = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAABehJ" + "REFUSImdll1olNkZx3/vRzIxk5lJbMwmGHccP+JHS6VrYo3TKCvL0i0LLTRB8cbLitp6p9ib" + "elHohVLT0BqXBnetqBQveiWF0oXiF+1FS4PUxFgbm0yYTN/JZL4nmcl7/r3IJMRlodAHDhwO" + "z8d5/uf/PM+x+N9yADgDfAtwAAvwgafAJ8DfvsxIq3q4G86cuiHAB8C/gZLjOO/4vv8u8LWu" + "rq4lgGQy2dTQ0JDZuXPn9snJyXmgGYgBnwMGcCzwBZb7BedbgJ+5rntk69atJdd1f/D69evX" + "tm1bAwMDDA4ONlmWxYMHD5iYmGj0fT8BhGOx2Cezs7MdKysrfwZ+DCTXgmzMaovjOPdXs1tf" + "nwJXgX8ODQ0plUqpXC7r9OnTAmZDodDNtra2zzba2Lb9AOj8MtjGAIVCIfX29ppDhw6Z1tZW" + "AWpvb9fNmzf9dDqtUqmksbExPxQKCdC+ffvU29ur3t5eEw6H1wL9po7KunzgOM4/AB08eNBM" + "TU3J8zxdunRJtm3r4sWLkqRCoaBkMilJunz5smzb1oULFzQ/P6/p6Wn19/cbQK7rvgQ+2hig" + "Z/v27c8A9fX1yfM8JRIJJZNJzczMKJVKqVQqKZ/PK5fLqVgsKpVKaWZmRslkUolEQouLixoY" + "GDCAotHo34H9bEijMZvNft7W1hYJBAJf9zyPeDxOR0cHoVCIxsZGarUalmVhWRbGGILBIJFI" + "hGAwSK1WY3h4mIcPH1qVSuVue3v75cXFxQyQBzjQ09Pz3V27dn0jEon8qv5QmpmZ0crKirLZ" + "rMrlsr4olUpF2WxW1WpVnucpGAyu4f8LYKfjOB8CBxzgSqFQ+NhxnI8zmUxfMBiMnD9/nmPH" + "jtHY2Iht2xSLRcbHx3ny5AnPnz8nn88TCoXYtGkTxhh8f5WNExMTlMvlDtu2+4wx/cBugOeA" + "4vG4Tp48qdHRUV+SisWicrmcJOnp06d6//331dDQINu2dfToUT169EiSlMvlVCgUJEm3bt3y" + "BwcHdfz4cdm2rbpvXnR1dVVGRkaUy+WUz+eVTCbX95J07949NTQ0bOS6bt++LUnK5/PK5/Mq" + "FApKp9NKpVIaHR1Vd3f3MvDCZa1nuC6+72NZFsFg8K0CkbQOA4AxBmPMWzrFYpFwOIxlWdi2" + "jWVZAJYD/KhUKr2ztLTE48ePWVpaMocPH7Z838cYQyAQIJ/P8+rVK2ZnZ5HEkSNHGBoaIhqN" + "sry8jG3bbN68mfv375uRkRHr2bNnjI+PO0DKAq4AvbZtNxljdnR0dMTOnDnDuXPnCIfDABQK" + "BSYnJ5mensYYw44dO9i7dy/hcBhJVCoVRkZGGB4eJpfLzXV2ds5mMpmVarX6AqDDcZzj9cL4" + "+f9L0+bmZgEKh8O3enp6+vbs2fN94D0HKEmqxWKxYDabPRqJRN47e/YsAwMDBINBXNfFGEOl" + "UqFarVKtVtdhCQQCACwvL1Or1VhcXKRUKk3Ozc39cWFh4V/Ay7U32rWxVczPzyuRSMjzPHme" + "p4WFBRUKhbcYk8lk5Hme0um0EomE0um04vG4AMVisWfAPoFl1wNsT8zNbV4jTaVSIRgMcv36" + "daLRKFevXqWlpYVyuQxAS0sLN27cIBqNcu3aNZqamlhaWkKSABKJxBYgZoEQWEOrPenTOobq" + "7+838Xjc7N+/X4BaWlo0Njbm5/N5ZbNZ3blzx+/s7BSg1tZWxeNxxePx9fYO3AUaV69brwOg" + "qz4s1guqtbX1t+Fw+NfA7IkTJ5TL5ZTJZHTq1CkBb4BfAp9ttHFd93dA95pvF+AgNPwVksaY" + "HwIV13W/2d3dnX/z5s1Pd+/e7TQ3N+9LJpPdd+/exXVdPM/Dtu2XxpiRWCzWJOmrc3NzbbVa" + "7S8rKyuXgASrqBh+AnY9i43z+aM6bbf29PR8LxAI/AlQd3f38rZt25YdxxHwB8dxvg28C+wF" + "vrMOS30MrGdwBSytDmgLMBb8fo1eU1NT7cAE8JVEIrHx2zLt+/5/gJm66mT9oharPwsL4L/1" + "GXlKb/xX4wAAAABJRU5ErkJggg==") + +Find = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAABgRJ" + "REFUSIm1lU1oG9sVx/9z5440+kBWJUvGDZESXuskZPMIwVaoybNp4niXEChdJPDAIWk+IGnA" + "i1Ioz9208apk10WcZFMI3Zgugrww6cNxKcakdoK/ghU7k5EsW2PLljXfc2duFw/5uaRv1x4Y" + "uHc4c3/38P+fM8D/OYTDm7Gxsd/4vv/H169fQ5IkAIDjODh16hSy2ey3t27d6geAJ0+eFDVN" + "G1xYWEA4HAYAeJ6H3t5eUEp/f+PGjZHPSOPj48P37t1j+XyeAzh4QqEQLxQK/Pr1639v5V67" + "dq3Y29t7kEMI4aIo8lwux2/fvs3Gx8d/28qlrYXv+18RQsTNzU129epVWigUUC6X8fz5c8zN" + "zUEQBKuVu7a2Zs7MzOD06dO4c+cOJicnUavVMDs7ywRBoIyxfgB/+A8ApXS7Xq8jkUjQCxcu" + "4MqVK1hbW8OrV6/w6dMndHV1fXHmzJmvCSGs2WyeePPmDU6ePImbN2+CUgpVVVEqleju7i4o" + "pdufVSDLMhhj0DQNMzMz2Nragu/72N7ehizLLJ1Od3me91wQBKRSKSSTSW9+fl56/PgxFhcX" + "IQgCNE2DbdsIhUL4DOC6LjjnIIRAFEXU63VYloUgCBAEAVUUJTBN0wGAWCwW5pxLtm1jdXUV" + "mqYhnU4fGIMxdgAgrcWHDx+aiqJAFEVks1l4nodisQjHcdDT04NsNvuPYrEYLRaL0Ww2++rc" + "uXMwDAMTExM4duwYGGNwXRfVahUrKysHABEA7t69+7u3b9/ekmU50t3dDV3XMTExgUqlAlmW" + "cfbsWdi2Td+9e3cEwIWurq6vkslk29LSEmq1GjRNQz6fR0dHByRJgqqq06VS6eUBoLu7+2+r" + "q6s/2traYslkkszOzkJRFMiyjFwux8LhMNF1PWGa5rl4PP6zeDze5rouDMNg9Xqd7O3tQRRF" + "ZDIZqKqKcrncdv/+/a2pqalFCgDhcPhjpVL50jRNWigU0N/fj0uXLkFVVayvr9OFhYVSNBot" + "p1KpPgAol8tTjUajI5/PnxgYGIAoitB1HdVqFe/fv/dyudxPG43GXwD8FQDw8OHDuVQqxQcG" + "BnitVuOGYfD19XU+PDzM29raOIBhAJFDDZgEcLuvr48risKbzSbXNI2PjIxwWZZ5LpfjDx48" + "WD5wESEElFLoug5VVRGJRFAqlaDressZDIB7qPE9AL7jOFBVFYZhYGNjA3t7e5AkCYIggBDy" + "vU0dx3FM04Smadjc3IQsy1heXoZpmq1Z8ysAg4cA4wB+7DgOKpUKPM/DysoKdnZ2YJomJEmC" + "4zguAIhjY2MjL168+DmAeKFQQGdnJ2zbRrVaRb1ex/Lyssc57+jp6fnJ8ePHkc/ncfTo0S/K" + "5XI2kUh43d3douu6KJfLkCQJ7e3taDQaqFQq4qNHj2KUMfbN4uIiLl686J8/f16sVqtIJpNw" + "HAecc9i2LeVyOQwODm7ruv4tgCAej/dVq9WsYRiSaZpwHAe6riMajeLy5ctgjPkvX75sZ4x9" + "Q6anp8E5RyaTET3Pw87ODmzbxs7ODhhjoJSCEIL9/f1/jY6O/mJ0dPSXzWbzn5RSuK6Ler0O" + "27bRaDSgKAosy0ImkxEBYHp6GqQlimVZYIyBEALHcUAIgSB897vgnINzHjqkQbg1VgRBgOM4" + "EAQBoVAInufBsr4bvJIkodUHKJVKkGUZrutid3cXhmHA9338UFBKYRgGVldXEQqFYJomLMvC" + "3NwcSqXSwVyirRuKoohUKoVYLIZMJoOPHz8iCALIsvxfQUEQgFKKzs5OdHR0QFVVbG9vgzEG" + "y7K+t2kkEkEQBFBVNVhaWiLRaBTxeByKoiAIAtRqNT+dTosA2g8d3k4pRb1e9+fn58V0Og1V" + "VdFsNsE5h6Ioge/7JBKJgFqWNX/kyJETGxsbkampKXDOEQTBQYmSJInxeBwAploAQsjrWCx2" + "VpIkcXJyEr7vQ5Kkw7qRzs5Ox7KsZQEAhoaG5iKRyJctcQ5HIpFAo9H487Nnz+4cfj80NPSn" + "RCLx6/39/c++kWUZtm2vPH369NQPivi/in8Df18XwyUA5+QAAAAASUVORK5CYII=") + +ShowSizers = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAABChJ" + "REFUSIm1lU1oXFUUx3/33nn3zbx5mcm0M5OZxGlNAn6XUqlRKIIWKrXqRlCLOwvitrooqKDg" + "RhcirkSpigSECl0WBa104UKoSIoLS8TWlja1NTOTyUzz5s37uNdFWpumJSktHriry//8zj38" + "z7nwP4dY5/7BUqn0quu62621i9cJhSjGcTzTarUOAr/dFn1sbGy/N+SdksgoQ2bh2mFBQpTz" + "vdP1ev3AWjkya10qpe4yPbPZ3GeUecEMswQoYK4I3w+wzWiz3qgbtw0AQoHoswWfNxyYLYE2" + "8GcVfr8IzU5gjOnfCWA5YuCvHPw0CRkLhGDjW5LeGiAFWjGcaYKyUHLAwvoeWQcgpczZjM3z" + "A3CiD5fPAlikFXRicNy8lNJbK4das/A0VdKVJd/4oWqJZhSGVcJUeH3n5JA/NGe1OhEG/W+i" + "KJq9LUAURe1KuVJQrrqnn4YVrXXkOE5gFCpf8NteLvdts9k8AgRXJDf0bC1ArlgsTiVJsqfX" + "6+1K03RQLpenfd//tdvtbh0MBvdaaxe01pcGg8FFlq1wU8jNoqiUeq5Wqx3SWlutdWvTpk3v" + "ANuBbY1G423Xdecdx7EjIyOHlVLPA6X1kl4lK6XU7rGxsU+UUkue54XlcvlL4LEVL36kUql8" + "ns/nl6SUwejo6EGl1DNcM81/r7ihRfl8fmehUHil2Wzu1Vr3Xdc9lCTJZ4PB4DhXzAlcAOa0" + "1koIMdntdqdKpZInhGjHcXx6ZT4FjDQajR21Wm2L7/sPJ0nyYr/ff1ZKOV8ul2eTJDnS6XSO" + "sjwN4mp1cRw3s9lssVQq1cIwVEmSbNVa+9VqNTsyMjLh+/744uJiT3ied8BxnJcx6QMS0wkH" + "UaEfJe6w4mc8v5AINWOS+KMgCGZWVuZ53taMlPtRaqovTRAv9LaTdQIn5y0oYzZkkaejOJ6m" + "Xq8fk0IkCGVxci2UnsfNz1MatSDtUN7rT0xMvLa6lePj4/v8wlAfsCVUuJFMZxh1eQMqyIFF" + "irRerx/LGGMCYa3ioV1f8el30/w4k8V178bNfMjHL3mcPREA0U1MES3ZNK2R6Z9katpgkpBU" + "jFLovcnJRz8QF54wxgTXVoUQl9iBzz/bniQlT39JIdecQywICfEwQwFkd4KqdAmDS8gKEMLK" + "XZTaOVImsbyOpM1QXiPkmgBAZBApeOEsS5OSjD8gJYsG6AFkhBA5C3D6+G72vudTGYXg8gYQ" + "0J6DDB4sK67LLISjhXQ6xLm3+OXxU6RuBGxEM0sPLFkhRC5jjDmntD5D2N4jDr8LsMCV+bCQ" + "t8Xhs0mStFYD4jhu55B/LEpx//vi/A5gieWdJLBoV+m2MeacAJ6uVqt7pZQNa+11v5MQohAE" + "wdFut/sFcH4VY9T3/X2+7z9lre2t0mXTNP17fn7+6/V6fMfxL1klnkaQRVDaAAAAAElFTkSu" + "QmCC") + +ShowFilling = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAZ5J" + "REFUSIntlc9KAkEcxz+tu7OxLRSuGUXtIfUpukW9QJcO3nqELoLXLr2Bt47dRBAqgxJ6BqMw" + "PJe0FEliaLNrFxV1zXatY9/TMDPf72eG3/yBf/2guaH2DrALrADtGfME8AKUgCsAdWhwX1XV" + "bSnlMtCZEaApivrqeXJxEmBDSrl+dHQoNjf3OD9/xjSDJ5vmMre3Z1xeHi8AG/3+YcAH8GEY" + "SbG6uoVtP2IYwQFLS2s8PT0MciYBALi5uUfTrrEsB88LDtB1C027A+h+N6cAvBUK+e6surg4" + "6wLvvSwAlHFKu/0ZfNkBvD6AEGJmwCSvrwaNRgOAZrMZKtw0zYF3KiCXy1Eul2m1WqEAhmFQ" + "q9VgrMg+QKVSoVqt4oU5QoCiKHQ6/vvpA2QyGdLpNPV6PRQgHo+Tz+fJZrPTAYlEgmQyiW3b" + "oQBCCFKpFIy+b36ArusDQ1j1vCM18B3TaDQaOrgvy7Jgyg7mgflisQiA4zihwmOxGKVSCUDv" + "ZTFOO/mL5zoSiby6rnsFHMDoDk6llA6//HBc1+1/OP8Kpi8497f1tG0HzQAAAABJRU5ErkJg" + "gg==") + +Icon = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABHNCSVQICAgIfAhkiAAAALhJ" + "REFUWIXtl80SgyAMhHeR99Y+eJseLAdHbJM0TjiwN2dI+MgfQpYFmSqpu0+AEQDqrwXyeorH" + "McvCEIBdm3F7/fr0FKgBRFaIrHkAdykdQFmEGm2HL233BAIAYmxYEqjePo9SBYBvBKppclDz" + "prMcqAhbAtknJx+3AKRHgGhnv4iApQY+jtSWpOY27BnifNt5uyk9BekAoZNwl21yDBSBi/63" + "yOMiLAXaf8AuwP9n94vzaTYBsgHeht4lXXmb7yQAAAAASUVORK5CYII=") + +CollapseTree = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAf9J" + "REFUSIm9lD9r20AYhx9Fxe5SKDIynVJodFMaChkC6eBCJ+WWdulSb1o6RR8gH6AfQF5aSksX" + "aQ0eajwVkinO1MFk0YXg0uLSISZDoQhcddAf5MR241buD4670/tyD+/vXh0sWdqUb1vAQ2Bj" + "Suwb0E7Xx38DaAKPgTuAnJLfSSEAn4DWIoAm8ByQruti2zYAlmUBoJSi2+3ieV4R1v0TJANs" + "AS8Ap9PpYFkWUSSuJFcqIUopAKSUAO+A18yxS0/nZ8AD13WFbdtEkWB9Hep1ODqC0QgMA8bj" + "GqYJhmGgaRq9Xm8I3AY+zgKspPMGIIuHZ6qn4/Q02UeRIIpEZqEkua+ZWpkXLEM3ipvEe9C0" + "ad2bqN+P89zr6P9WoJRidVXQ78e55/U09h1YW5vMTTWcB8i66B7wq1arie1ti/G4hmEknfNl" + "BD8Kh1cqIbp+ThAEVKtVBoNBCziZBfjX/wDgEHg0C7D0O8gs+grcAm76vi80TcM04eJCYZqg" + "6+ecnR0TBAGu6+L7PlJKhAgJQ+6SvF/vrwNsAm+BD0B8eTQajXztOMT7HnFrL48fpGNCizzX" + "Q5IXdDfdN/Y92Hna5s2rJ+y+zPPm3skiOoCkip+f23Fr70o1pWgCkoGKkKV3URnKqyjaxTKs" + "omCX4+SQ0pS1aew4xJub5VZwGZQdfv83yOfTR/iA1xwAAAAASUVORK5CYII=") + +ExpandTree = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAepJ" + "REFUSIm1lDFr20AYhp+ri+iSxUUhW4dIU9wlQ4YOKnRSj4K7Z9NSCFg/ID+gP0BZWkpLF3kN" + "Giq0pkOpMxWSTFIIFIqHQCFbUQnXQTpFslXXptYLx90nvdzD9913Bx1LtHzbA54Aj1v+nQFf" + "yvXpMoD7M/E+8AzYAmSLP66BbSBcBTACXED6vo/rugBYlgVAlmUkSSKDIND+rXJeCNEl2gNe" + "AV4cx1iWRZ7bc2bDSMmyDAApJcAH4C0LytUr5wPA8n3fdl2XPLfZ2YHNTbi+vjPf3j7ENKHf" + "7yOEYDKZTIHfwNe/Ae7V0pX1zbUGA8FgILi8LOI8t8lzW5dQ0t4Mc4DO1ADoA724ACEEQtx1" + "8XBYZDLrXQnQhRoA3SEAUaSIItWIz89Vm3e6DOAMiJMkwTBSALa3i6Gl14aRYhgpSZLgOA7A" + "t0WA/70HAJ+Bp//KoDPpi/YD2AAehGFoCyEwTbi5yTBN6PV+cnV1yng8xvd9wjBESoltp6Qp" + "jyjer4/LAPeB98AnQM0Ox3GqteehjgPU0WH1/6QcDa3yXE8pDnRUxs5xAM9fRrx7M2T0uvIt" + "PJNVdAJFFr++R+rocC6btagB0aA6pPMuWoeqLOrlootSUSuX51WQtUm3qfI81O7uejOYBenN" + "X/wBVz/ONKbGYPkAAAAASUVORK5CYII=") + +HighlightItem = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAQZJ" + "REFUSInFlTGSgjAUhv8XuIRl9ga0XgmuoI5abwfH8Ai22GlJZ0otuQD5t3DZ1V1CwgzgP8OQ" + "QCZfkv+9FxEVYUrFbYO2oW+wqEiGAtRzhyRIQh9eH+RXAMBmvfIuohcwheLnTnZ6vM3NjAaQ" + "1mTahvrAHwCzj+BJVI83sesHAMjRM3OVgNkFm/WK292+EzKvB86zr5Lu76b2AubdAbqMda0+" + "UOIqFdY2lKMHYGrw06DL3Tbrxzmi/Iq0JNLyO/Pxm/Uze/BXVRIUKajvKM6AXuh/kfjeHTC7" + "TAdw1RfahmlJFOewgtjvQY/0QgeNe3MUOVQsw2/OwQBRkQy5Op2lYixN7sEXVhRd4PXVHvwA" + "AAAASUVORK5CYII=") + +EvtWatcher = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAABwxJ" + "REFUSIltlltsXFcVhr+zzzlzztw949vMxGPHl3Hiqk6I07RJhcBGoUK0VSuaSFwahAChSkiV" + "QCotlQAJP/FAeKFPSChRJUAtqZACjUpJBCqUlEY0l7HjpraT+DKejDPjmfHcznXz0Bg1iPWy" + "ttbD/629tbT/pXB/KIAEVCADxIBdwMi93AX4QBlYA5aBIlADNvg/oXzirAIeoAcCgVFN0460" + "Wq0DuVzuc7VaLVspl52xsTHN9TwWFxfdbDarKYqysLKy8tdYLHalXq9fBG7fa0Dcy/8F7BSi" + "hmEcdF33Cc/znt63b1//1NRUMBQKqeFolOHRMTzbYmlxEcdx2Nrast+7eNFaWl6+bZrm667r" + "nnNd9ypg7Whq9yA+EInH4w/VarUTmqY9MTMz03vs2HFS/X1eOjvojmUHFM22sIMRXN/nxlxe" + "lutVdXLqYPTM73774KVLl+KKosSCwaBst9vXdiDqzg0MwzjSbDafBR6bmppKv/TSS+7MzLTs" + "7elTUwMp0ed7wlwvCnOwX8QjQaHHYiKdSCqHcjkvkckwl893VSqVbtd1DV3XC77vFwFFvff2" + "4wjxFen7xx46dCjzwosvuuO5nNbT0yuSXWFk0KSx3sa53aLal6QtBVFF0GdZinBsoaZSjO7e" + "LfP5fG+1Wu0TQmwbhnHLdd1tFRgIhULTtmV9Z3p6Ovvyyy+7ubExbWhoCEWoBBWPSzWdM4UE" + "C+U011YM3gvojCwskZAOzclxkgFDCYZCyv79+735+fnuUqkUjUQii51Op6YCWcdxnsiNjR0+" + "fuKE+cUjR+Sg7wurt5eA6bB2I8b3/1Hn3EKFY+UYzS2H2Y1bbFlpDuaydJsbBAo1woMDaLqO" + "lFIpFApsbGysxGKxD1XgwJ7x8ecOPPLI4NMPP+w9ODqq2uEwkSvLrJWzfP5PS/SoJb7xpSy/" + "GNUJd6noWoK3bn1E+a7D4XiaaJ+FpwWpVatKu9Vyt5vNrmaj4ZZKpUsCGCkWi/1d8TiZ3cMS" + "VcHvWLyvJHj2gy0mBz1+/7VJHvRXmDv5dc6e/jF1c4HUeJjXS02ev9HhcjCDY2ok0ymGRkak" + "qigU79xJCyFGNGBXrV4XXaEQxsiI4mnglRv8OZbjxvYax2cGaEfavPnTU/CrV1kH1qv/5Oi3" + "XyG1Z4wr60Xenw+xJ75JVzyAOzSkqIEAtmUZpmkOCqBrYmJCGx4bI9SxICCoZrJEFySvtNL8" + "xoqQ+uGP+OXJn0EkigiY8Ie/cPvfeY4GVX7QTMFNG+PGBqwX0KWv7H3gAbIDA4FOp5PQAN+2" + "LGlLiV6vozp1lOgAvq6wrnkc3FQpVvtYBYRl47s2AJoQtARsuS6RqE7j0Ql0Ddy7VdxOB8u2" + "JeALoLy0vOzevL5AvbsLPJPEu9exd8NPomtcr3jseupJeOYxcCyQEp47wa7Dh3hrq8PJ3iLa" + "uABVQVU1LMeR1+fnKZVKlmmamxqwtiuT8betDvXFRUlmEMZ7eDzZ4vrGCK9duUx/LsL0t37N" + "+sQ7aIZB6tHPkF8pYJeafDfdw5Pr8/j9uymUNvlwfl5quk4ymbQrlcqKBiwJIZaqtVpvcXVd" + "jA4OSW9olzJuW/xcKxJWDV5d1Ulne/jqp79Mx4U3i228lQDP70nyvf0msZaPVymzcrcsC8Wi" + "aGxvI4T4CFhSgWC9Xk90ms29vcPD4eHJSS9gu0JtO8Q6qxwaHmYpFmF5ZZPHb0VQq5Lzosg3" + "H0jzwsEg+qhLM9GLslGg4jj+uQvn1fNvv32rVCqdicfjH6iAHg6HzVKpNNWoVvt6+/tlJBAQ" + "aduiMZ4jNqiwt0cwkOjBNyVG3OXop7p5Zp9NT9ii09bwOy3my2X+/u678tXTp8Xq2trlZDL5" + "RrVaXVWBhuM4DdM01ZXV1ZFrV68md4+O+mp/v5Lo6kJKlRgukynJ/u46hwvzjOdMjFAAPAXL" + "arO4tMTfLlxgdnZWFIvFq4ZhnG40GheAzZ3vuuq6bktVVSqVSnd+bq53ct8+T1NVGYnElGg4" + "pPjSQ8VD85r43XGMaIzKZlmura95Fy9eFLOzs9RqtX8JId5wXfeP8PFk7wAUoBQMBu86jhOs" + "1Wp983Nz3YBotZredqPhSV/BcjzZDGjybrPl5/Nz3rWrV+XZs2e1U6dO+Xfu3MkLId7QNO01" + "z/MWd/z9fy3TCIfDeyzL+oLruk8NDw8PT05O9qiqqgcCAbl3YkJxfJ/r165JVQil0Wy2L1++" + "XC4WiwuGYZyRUp63bfsm4O5oftL0dyAiGAxmDMM4UK1WJ4eGhj67vb09tFWtkk6lDN9xZHFz" + "08pkMhJYKBQK7ySTyXylUrkClO51vmPD920V90GA/lgsFqrX6/26ro8oijJo23YC8E3TvGvb" + "9m0hxHI4HC7XarXmJ8Th49UHgP8A40NGDcCfTKIAAAAASUVORK5CYII=") diff --git a/wx/lib/intctrl.py b/wx/lib/intctrl.py new file mode 100644 index 00000000..0e34dc36 --- /dev/null +++ b/wx/lib/intctrl.py @@ -0,0 +1,921 @@ +#---------------------------------------------------------------------------- +# Name: wxPython.lib.intctrl.py +# Author: Will Sadkin +# Created: 01/16/2003 +# Copyright: (c) 2003 by Will Sadkin +# RCS-ID: $Id$ +# License: wxWindows license +#---------------------------------------------------------------------------- +# NOTE: +# This was written to provide a standard integer edit control for wxPython. +# +# IntCtrl permits integer (long) values to be retrieved or set via +# .GetValue() and .SetValue(), and provides an EVT_INT() event function +# for trapping changes to the control. +# +# It supports negative integers as well as the naturals, and does not +# permit leading zeros or an empty control; attempting to delete the +# contents of the control will result in a (selected) value of zero, +# thus preserving a legitimate integer value, or an empty control +# (if a value of None is allowed for the control.) Similarly, replacing the +# contents of the control with '-' will result in a selected (absolute) +# value of -1. +# +# IntCtrl also supports range limits, with the option of either +# enforcing them or simply coloring the text of the control if the limits +# are exceeded. +#---------------------------------------------------------------------------- +# 12/08/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o 2.5 Compatability changes +# +# 12/20/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o wxIntUpdateEvent -> IntUpdateEvent +# o wxIntValidator -> IntValidator +# o wxIntCtrl -> IntCtrl +# + +import string +import types + +import wx + +#---------------------------------------------------------------------------- + +from sys import maxint +MAXINT = maxint # (constants should be in upper case) +MININT = -maxint-1 + +#---------------------------------------------------------------------------- + +# Used to trap events indicating that the current +# integer value of the control has been changed. +wxEVT_COMMAND_INT_UPDATED = wx.NewEventType() +EVT_INT = wx.PyEventBinder(wxEVT_COMMAND_INT_UPDATED, 1) + +#---------------------------------------------------------------------------- + +# wxWindows' wxTextCtrl translates Composite "control key" +# events into single events before returning them to its OnChar +# routine. The doc says that this results in 1 for Ctrl-A, 2 for +# Ctrl-B, etc. However, there are no wxPython or wxWindows +# symbols for them, so I'm defining codes for Ctrl-X (cut) and +# Ctrl-V (paste) here for readability: +WXK_CTRL_X = (ord('X')+1) - ord('A') +WXK_CTRL_V = (ord('V')+1) - ord('A') + +class IntUpdatedEvent(wx.PyCommandEvent): + def __init__(self, id, value = 0, object=None): + wx.PyCommandEvent.__init__(self, wxEVT_COMMAND_INT_UPDATED, id) + + self.__value = value + self.SetEventObject(object) + + def GetValue(self): + """Retrieve the value of the control at the time + this event was generated.""" + return self.__value + + +#---------------------------------------------------------------------------- + +class IntValidator( wx.PyValidator ): + """ + Validator class used with IntCtrl; handles all validation of input + prior to changing the value of the underlying wx.TextCtrl. + """ + def __init__(self): + wx.PyValidator.__init__(self) + self.Bind(wx.EVT_CHAR, self.OnChar) + + def Clone (self): + return self.__class__() + + def Validate(self, window): # window here is the *parent* of the ctrl + """ + Because each operation on the control is vetted as it's made, + the value of the control is always valid. + """ + return 1 + + + def OnChar(self, event): + """ + Validates keystrokes to make sure the resulting value will a legal + value. Erasing the value causes it to be set to 0, with the value + selected, so it can be replaced. Similarly, replacing the value + with a '-' sign causes the value to become -1, with the value + selected. Leading zeros are removed if introduced by selection, + and are prevented from being inserted. + """ + key = event.GetKeyCode() + ctrl = event.GetEventObject() + + if 'wxMac' in wx.PlatformInfo: + if event.CmdDown() and key == ord('c'): + key = WXK_CTRL_C + elif event.CmdDown() and key == ord('v'): + key = WXK_CTRL_V + + value = ctrl.GetValue() + textval = wx.TextCtrl.GetValue(ctrl) + allow_none = ctrl.IsNoneAllowed() + + pos = ctrl.GetInsertionPoint() + sel_start, sel_to = ctrl.GetSelection() + select_len = sel_to - sel_start + +# (Uncomment for debugging:) +## print 'keycode:', key +## print 'pos:', pos +## print 'sel_start, sel_to:', sel_start, sel_to +## print 'select_len:', select_len +## print 'textval:', textval + + # set defaults for processing: + allow_event = 1 + set_to_none = 0 + set_to_zero = 0 + set_to_minus_one = 0 + paste = 0 + internally_set = 0 + + new_value = value + new_text = textval + new_pos = pos + + # Validate action, and predict resulting value, so we can + # range check the result and validate that too. + + if key in (wx.WXK_DELETE, wx.WXK_BACK, WXK_CTRL_X): + if select_len: + new_text = textval[:sel_start] + textval[sel_to:] + elif key == wx.WXK_DELETE and pos < len(textval): + new_text = textval[:pos] + textval[pos+1:] + elif key == wx.WXK_BACK and pos > 0: + new_text = textval[:pos-1] + textval[pos:] + # (else value shouldn't change) + + if new_text in ('', '-'): + # Deletion of last significant digit: + if allow_none and new_text == '': + new_value = None + set_to_none = 1 + else: + new_value = 0 + set_to_zero = 1 + else: + try: + new_value = ctrl._fromGUI(new_text) + except ValueError: + allow_event = 0 + + + elif key == WXK_CTRL_V: # (see comments at top of file) + # Only allow paste if number: + paste_text = ctrl._getClipboardContents() + new_text = textval[:sel_start] + paste_text + textval[sel_to:] + if new_text == '' and allow_none: + new_value = None + set_to_none = 1 + else: + try: + # Convert the resulting strings, verifying they + # are legal integers and will fit in proper + # size if ctrl limited to int. (if not, + # disallow event.) + new_value = ctrl._fromGUI(new_text) + + if paste_text: + paste_value = ctrl._fromGUI(paste_text) + else: + paste_value = 0 + + new_pos = sel_start + len(str(paste_value)) + + # if resulting value is 0, truncate and highlight value: + if new_value == 0 and len(new_text) > 1: + set_to_zero = 1 + + elif paste_value == 0: + # Disallow pasting a leading zero with nothing selected: + if( select_len == 0 + and value is not None + and ( (value >= 0 and pos == 0) + or (value < 0 and pos in [0,1]) ) ): + allow_event = 0 + + paste = 1 + + except ValueError: + allow_event = 0 + + + elif key < wx.WXK_SPACE or key > 255: + pass # event ok + + + elif chr(key) == '-': + # Allow '-' to result in -1 if replacing entire contents: + if( value is None + or (value == 0 and pos == 0) + or (select_len >= len(str(abs(value)))) ): + new_value = -1 + set_to_minus_one = 1 + + # else allow negative sign only at start, and only if + # number isn't already zero or negative: + elif pos != 0 or (value is not None and value < 0): + allow_event = 0 + else: + new_text = '-' + textval + new_pos = 1 + try: + new_value = ctrl._fromGUI(new_text) + except ValueError: + allow_event = 0 + + + elif chr(key) in string.digits: + # disallow inserting a leading zero with nothing selected + if( chr(key) == '0' + and select_len == 0 + and value is not None + and ( (value >= 0 and pos == 0) + or (value < 0 and pos in [0,1]) ) ): + allow_event = 0 + # disallow inserting digits before the minus sign: + elif value is not None and value < 0 and pos == 0: + allow_event = 0 + else: + new_text = textval[:sel_start] + chr(key) + textval[sel_to:] + try: + new_value = ctrl._fromGUI(new_text) + except ValueError: + allow_event = 0 + + else: + # not a legal char + allow_event = 0 + + + if allow_event: + # Do range checking for new candidate value: + if ctrl.IsLimited() and not ctrl.IsInBounds(new_value): + allow_event = 0 + elif new_value is not None: + # ensure resulting text doesn't result in a leading 0: + if not set_to_zero and not set_to_minus_one: + if( (new_value > 0 and new_text[0] == '0') + or (new_value < 0 and new_text[1] == '0') + or (new_value == 0 and select_len > 1 ) ): + + # Allow replacement of leading chars with + # zero, but remove the leading zero, effectively + # making this like "remove leading digits" + + # Account for leading zero when positioning cursor: + if( key == wx.WXK_BACK + or (paste and paste_value == 0 and new_pos > 0) ): + new_pos = new_pos - 1 + + wx.CallAfter(ctrl.SetValue, new_value) + wx.CallAfter(ctrl.SetInsertionPoint, new_pos) + internally_set = 1 + + elif paste: + # Always do paste numerically, to remove + # leading/trailing spaces + wx.CallAfter(ctrl.SetValue, new_value) + wx.CallAfter(ctrl.SetInsertionPoint, new_pos) + internally_set = 1 + + elif (new_value == 0 and len(new_text) > 1 ): + allow_event = 0 + + if allow_event: + ctrl._colorValue(new_value) # (one way or t'other) + +# (Uncomment for debugging:) +## if allow_event: +## print 'new value:', new_value +## if paste: print 'paste' +## if set_to_none: print 'set_to_none' +## if set_to_zero: print 'set_to_zero' +## if set_to_minus_one: print 'set_to_minus_one' +## if internally_set: print 'internally_set' +## else: +## print 'new text:', new_text +## print 'disallowed' +## print + + if allow_event: + if set_to_none: + wx.CallAfter(ctrl.SetValue, new_value) + + elif set_to_zero: + # select to "empty" numeric value + wx.CallAfter(ctrl.SetValue, new_value) + wx.CallAfter(ctrl.SetInsertionPoint, 0) + wx.CallAfter(ctrl.SetSelection, 0, 1) + + elif set_to_minus_one: + wx.CallAfter(ctrl.SetValue, new_value) + wx.CallAfter(ctrl.SetInsertionPoint, 1) + wx.CallAfter(ctrl.SetSelection, 1, 2) + + elif not internally_set: + event.Skip() # allow base wxTextCtrl to finish processing + + elif not wx.Validator_IsSilent(): + wx.Bell() + + + def TransferToWindow(self): + """ Transfer data from validator to window. + + The default implementation returns False, indicating that an error + occurred. We simply return True, as we don't do any data transfer. + """ + return True # Prevent wx.Dialog from complaining. + + + def TransferFromWindow(self): + """ Transfer data from window to validator. + + The default implementation returns False, indicating that an error + occurred. We simply return True, as we don't do any data transfer. + """ + return True # Prevent wx.Dialog from complaining. + + +#---------------------------------------------------------------------------- + +class IntCtrl(wx.TextCtrl): + """ + This class provides a control that takes and returns integers as + value, and provides bounds support and optional value limiting. + + IntCtrl( + parent, id = -1, + value = 0, + pos = wxDefaultPosition, + size = wxDefaultSize, + style = 0, + validator = wxDefaultValidator, + name = "integer", + min = None, + max = None, + limited = False, + allow_none = False, + allow_long = False, + default_color = wxBLACK, + oob_color = wxRED ) + + value + If no initial value is set, the default will be zero, or + the minimum value, if specified. If an illegal string is specified, + a ValueError will result. (You can always later set the initial + value with SetValue() after instantiation of the control.) + min + The minimum value that the control should allow. This can be + adjusted with SetMin(). If the control is not limited, any value + below this bound will be colored with the current out-of-bounds color. + If min < -sys.maxint-1 and the control is configured to not allow long + values, the minimum bound will still be set to the long value, but + the implicit bound will be -sys.maxint-1. + max + The maximum value that the control should allow. This can be + adjusted with SetMax(). If the control is not limited, any value + above this bound will be colored with the current out-of-bounds color. + if max > sys.maxint and the control is configured to not allow long + values, the maximum bound will still be set to the long value, but + the implicit bound will be sys.maxint. + + limited + Boolean indicating whether the control prevents values from + exceeding the currently set minimum and maximum values (bounds). + If False and bounds are set, out-of-bounds values will + be colored with the current out-of-bounds color. + + allow_none + Boolean indicating whether or not the control is allowed to be + empty, representing a value of None for the control. + + allow_long + Boolean indicating whether or not the control is allowed to hold + and return a long as well as an int. + + default_color + Color value used for in-bounds values of the control. + + oob_color + Color value used for out-of-bounds values of the control + when the bounds are set but the control is not limited. + + validator + Normally None, IntCtrl uses its own validator to do value + validation and input control. However, a validator derived + from IntValidator can be supplied to override the data + transfer methods for the IntValidator class. + """ + + def __init__ ( + self, parent, id=-1, value = 0, + pos = wx.DefaultPosition, size = wx.DefaultSize, + style = 0, validator = wx.DefaultValidator, + name = "integer", + min=None, max=None, + limited = 0, allow_none = 0, allow_long = 0, + default_color = wx.BLACK, oob_color = wx.RED, + ): + + # Establish attrs required for any operation on value: + self.__min = None + self.__max = None + self.__limited = 0 + self.__default_color = wx.BLACK + self.__oob_color = wx.RED + self.__allow_none = 0 + self.__allow_long = 0 + self.__oldvalue = None + + if validator == wx.DefaultValidator: + validator = IntValidator() + + wx.TextCtrl.__init__( + self, parent, id, self._toGUI(0), + pos, size, style, validator, name ) + + # The following lets us set out our "integer update" events: + self.Bind(wx.EVT_TEXT, self.OnText ) + + # Establish parameters, with appropriate error checking + + self.SetBounds(min, max) + self.SetLimited(limited) + self.SetColors(default_color, oob_color) + self.SetNoneAllowed(allow_none) + self.SetLongAllowed(allow_long) + self.SetValue(value) + self.__oldvalue = 0 + + + def OnText( self, event ): + """ + Handles an event indicating that the text control's value + has changed, and issue EVT_INT event. + NOTE: using wx.TextCtrl.SetValue() to change the control's + contents from within a wx.EVT_CHAR handler can cause double + text events. So we check for actual changes to the text + before passing the events on. + """ + value = self.GetValue() + if value != self.__oldvalue: + try: + self.GetEventHandler().ProcessEvent( + IntUpdatedEvent( self.GetId(), self.GetValue(), self ) ) + except ValueError: + return + # let normal processing of the text continue + event.Skip() + self.__oldvalue = value # record for next event + + + def GetValue(self): + """ + Returns the current integer (long) value of the control. + """ + return self._fromGUI( wx.TextCtrl.GetValue(self) ) + + def SetValue(self, value): + """ + Sets the value of the control to the integer value specified. + The resulting actual value of the control may be altered to + conform with the bounds set on the control if limited, + or colored if not limited but the value is out-of-bounds. + A ValueError exception will be raised if an invalid value + is specified. + """ + wx.TextCtrl.SetValue( self, self._toGUI(value) ) + self._colorValue() + + + def ChangeValue(self, value): + "Change the value without sending an EVT_TEXT event.""" + wx.TextCtrl.ChangeValue(self, self._toGUI(value)) + self.__oldvalue = self.GetValue() # record for next event + self._colorValue() + + + def SetMin(self, min=None): + """ + Sets the minimum value of the control. If a value of None + is provided, then the control will have no explicit minimum value. + If the value specified is greater than the current maximum value, + then the function returns 0 and the minimum will not change from + its current setting. On success, the function returns 1. + + If successful and the current value is lower than the new lower + bound, if the control is limited, the value will be automatically + adjusted to the new minimum value; if not limited, the value in the + control will be colored with the current out-of-bounds color. + + If min > -sys.maxint-1 and the control is configured to not allow longs, + the function will return 0, and the min will not be set. + """ + if( self.__max is None + or min is None + or (self.__max is not None and self.__max >= min) ): + self.__min = min + + if self.IsLimited() and min is not None and self.GetValue() < min: + self.SetValue(min) + else: + self._colorValue() + return 1 + else: + return 0 + + + def GetMin(self): + """ + Gets the minimum value of the control. It will return the current + minimum integer, or None if not specified. + """ + return self.__min + + + def SetMax(self, max=None): + """ + Sets the maximum value of the control. If a value of None + is provided, then the control will have no explicit maximum value. + If the value specified is less than the current minimum value, then + the function returns 0 and the maximum will not change from its + current setting. On success, the function returns 1. + + If successful and the current value is greater than the new upper + bound, if the control is limited the value will be automatically + adjusted to this maximum value; if not limited, the value in the + control will be colored with the current out-of-bounds color. + + If max > sys.maxint and the control is configured to not allow longs, + the function will return 0, and the max will not be set. + """ + if( self.__min is None + or max is None + or (self.__min is not None and self.__min <= max) ): + self.__max = max + + if self.IsLimited() and max is not None and self.GetValue() > max: + self.SetValue(max) + else: + self._colorValue() + return 1 + else: + return 0 + + + def GetMax(self): + """ + Gets the maximum value of the control. It will return the current + maximum integer, or None if not specified. + """ + return self.__max + + + def SetBounds(self, min=None, max=None): + """ + This function is a convenience function for setting the min and max + values at the same time. The function only applies the maximum bound + if setting the minimum bound is successful, and returns True + only if both operations succeed. + NOTE: leaving out an argument will remove the corresponding bound. + """ + ret = self.SetMin(min) + return ret and self.SetMax(max) + + + def GetBounds(self): + """ + This function returns a two-tuple (min,max), indicating the + current bounds of the control. Each value can be None if + that bound is not set. + """ + return (self.__min, self.__max) + + + def SetLimited(self, limited): + """ + If called with a value of True, this function will cause the control + to limit the value to fall within the bounds currently specified. + If the control's value currently exceeds the bounds, it will then + be limited accordingly. + + If called with a value of 0, this function will disable value + limiting, but coloring of out-of-bounds values will still take + place if bounds have been set for the control. + """ + self.__limited = limited + if limited: + min = self.GetMin() + max = self.GetMax() + if not min is None and self.GetValue() < min: + self.SetValue(min) + elif not max is None and self.GetValue() > max: + self.SetValue(max) + else: + self._colorValue() + + + def IsLimited(self): + """ + Returns True if the control is currently limiting the + value to fall within the current bounds. + """ + return self.__limited + + + def IsInBounds(self, value=None): + """ + Returns True if no value is specified and the current value + of the control falls within the current bounds. This function can + also be called with a value to see if that value would fall within + the current bounds of the given control. + """ + if value is None: + value = self.GetValue() + + if( not (value is None and self.IsNoneAllowed()) + and type(value) not in (types.IntType, types.LongType) ): + raise ValueError ( + 'IntCtrl requires integer values, passed %s'% repr(value) ) + + min = self.GetMin() + max = self.GetMax() + if min is None: min = value + if max is None: max = value + + # if bounds set, and value is None, return False + if value == None and (min is not None or max is not None): + return 0 + else: + return min <= value <= max + + + def SetNoneAllowed(self, allow_none): + """ + Change the behavior of the validation code, allowing control + to have a value of None or not, as appropriate. If the value + of the control is currently None, and allow_none is 0, the + value of the control will be set to the minimum value of the + control, or 0 if no lower bound is set. + """ + self.__allow_none = allow_none + if not allow_none and self.GetValue() is None: + min = self.GetMin() + if min is not None: self.SetValue(min) + else: self.SetValue(0) + + + def IsNoneAllowed(self): + return self.__allow_none + + + def SetLongAllowed(self, allow_long): + """ + Change the behavior of the validation code, allowing control + to have a long value or not, as appropriate. If the value + of the control is currently long, and allow_long is 0, the + value of the control will be adjusted to fall within the + size of an integer type, at either the sys.maxint or -sys.maxint-1, + for positive and negative values, respectively. + """ + current_value = self.GetValue() + if not allow_long and type(current_value) is types.LongType: + if current_value > 0: + self.SetValue(MAXINT) + else: + self.SetValue(MININT) + self.__allow_long = allow_long + + + def IsLongAllowed(self): + return self.__allow_long + + + + def SetColors(self, default_color=wx.BLACK, oob_color=wx.RED): + """ + Tells the control what colors to use for normal and out-of-bounds + values. If the value currently exceeds the bounds, it will be + recolored accordingly. + """ + self.__default_color = default_color + self.__oob_color = oob_color + self._colorValue() + + + def GetColors(self): + """ + Returns a tuple of (default_color, oob_color), indicating + the current color settings for the control. + """ + return self.__default_color, self.__oob_color + + + def _colorValue(self, value=None): + """ + Colors text with oob_color if current value exceeds bounds + set for control. + """ + if not self.IsInBounds(value): + self.SetForegroundColour(self.__oob_color) + else: + self.SetForegroundColour(self.__default_color) + self.Refresh() + + + def _toGUI( self, value ): + """ + Conversion function used to set the value of the control; does + type and bounds checking and raises ValueError if argument is + not a valid value. + """ + if value is None and self.IsNoneAllowed(): + return '' + elif type(value) == types.LongType and not self.IsLongAllowed(): + raise ValueError ( + 'IntCtrl requires integer value, passed long' ) + elif type(value) not in (types.IntType, types.LongType): + raise ValueError ( + 'IntCtrl requires integer value, passed %s'% repr(value) ) + + elif self.IsLimited(): + min = self.GetMin() + max = self.GetMax() + if not min is None and value < min: + raise ValueError ( + 'value is below minimum value of control %d'% value ) + if not max is None and value > max: + raise ValueError ( + 'value exceeds value of control %d'% value ) + + return str(value) + + + def _fromGUI( self, value ): + """ + Conversion function used in getting the value of the control. + """ + + # One or more of the underlying text control implementations + # issue an intermediate EVT_TEXT when replacing the control's + # value, where the intermediate value is an empty string. + # So, to ensure consistency and to prevent spurious ValueErrors, + # we make the following test, and react accordingly: + # + if value == '': + if not self.IsNoneAllowed(): + return 0 + else: + return None + else: + try: + return int( value ) + except ValueError: + if self.IsLongAllowed(): + return long( value ) + else: + raise + + + def Cut( self ): + """ + Override the wxTextCtrl's .Cut function, with our own + that does validation. Will result in a value of 0 + if entire contents of control are removed. + """ + sel_start, sel_to = self.GetSelection() + select_len = sel_to - sel_start + textval = wx.TextCtrl.GetValue(self) + + do = wx.TextDataObject() + do.SetText(textval[sel_start:sel_to]) + wx.TheClipboard.Open() + wx.TheClipboard.SetData(do) + wx.TheClipboard.Close() + if select_len == len(wxTextCtrl.GetValue(self)): + if not self.IsNoneAllowed(): + self.SetValue(0) + self.SetInsertionPoint(0) + self.SetSelection(0,1) + else: + self.SetValue(None) + else: + new_value = self._fromGUI(textval[:sel_start] + textval[sel_to:]) + self.SetValue(new_value) + + + def _getClipboardContents( self ): + """ + Subroutine for getting the current contents of the clipboard. + """ + do = wx.TextDataObject() + wx.TheClipboard.Open() + success = wx.TheClipboard.GetData(do) + wx.TheClipboard.Close() + + if not success: + return None + else: + # Remove leading and trailing spaces before evaluating contents + return do.GetText().strip() + + + def Paste( self ): + """ + Override the wxTextCtrl's .Paste function, with our own + that does validation. Will raise ValueError if not a + valid integerizable value. + """ + paste_text = self._getClipboardContents() + if paste_text: + # (conversion will raise ValueError if paste isn't legal) + sel_start, sel_to = self.GetSelection() + text = wx.TextCtrl.GetValue( self ) + new_text = text[:sel_start] + paste_text + text[sel_to:] + if new_text == '' and self.IsNoneAllowed(): + self.SetValue(None) + else: + value = self._fromGUI(new_text) + self.SetValue(value) + new_pos = sel_start + len(paste_text) + wx.CallAfter(self.SetInsertionPoint, new_pos) + + + +#=========================================================================== + +if __name__ == '__main__': + + import traceback + + class myDialog(wx.Dialog): + def __init__(self, parent, id, title, + pos = wx.DefaultPosition, size = wx.DefaultSize, + style = wx.DEFAULT_DIALOG_STYLE ): + wx.Dialog.__init__(self, parent, id, title, pos, size, style) + + self.int_ctrl = IntCtrl(self, wx.NewId(), size=(55,20)) + self.OK = wx.Button( self, wx.ID_OK, "OK") + self.Cancel = wx.Button( self, wx.ID_CANCEL, "Cancel") + + vs = wx.BoxSizer( wx.VERTICAL ) + vs.Add( self.int_ctrl, 0, wx.ALIGN_CENTRE|wx.ALL, 5 ) + hs = wx.BoxSizer( wx.HORIZONTAL ) + hs.Add( self.OK, 0, wx.ALIGN_CENTRE|wx.ALL, 5 ) + hs.Add( self.Cancel, 0, wx.ALIGN_CENTRE|wx.ALL, 5 ) + vs.Add(hs, 0, wx.ALIGN_CENTRE|wx.ALL, 5 ) + + self.SetAutoLayout( True ) + self.SetSizer( vs ) + vs.Fit( self ) + vs.SetSizeHints( self ) + self.Bind(EVT_INT, self.OnInt, self.int_ctrl) + + def OnInt(self, event): + print 'int now', event.GetValue() + + class TestApp(wx.App): + def OnInit(self): + try: + self.frame = wx.Frame(None, -1, "Test", (20,20), (120,100) ) + self.panel = wx.Panel(self.frame, -1) + button = wx.Button(self.panel, 10, "Push Me", (20, 20)) + self.Bind(wx.EVT_BUTTON, self.OnClick, button) + except: + traceback.print_exc() + return False + return True + + def OnClick(self, event): + dlg = myDialog(self.panel, -1, "test IntCtrl") + dlg.int_ctrl.SetValue(501) + dlg.int_ctrl.SetInsertionPoint(1) + dlg.int_ctrl.SetSelection(1,2) + rc = dlg.ShowModal() + print 'final value', dlg.int_ctrl.GetValue() + del dlg + self.frame.Destroy() + + def Show(self): + self.frame.Show(True) + + try: + app = TestApp(0) + app.Show() + app.MainLoop() + except: + traceback.print_exc() diff --git a/wx/lib/itemspicker.py b/wx/lib/itemspicker.py new file mode 100644 index 00000000..038489c7 --- /dev/null +++ b/wx/lib/itemspicker.py @@ -0,0 +1,243 @@ +''' +Created on Oct 3, 2010 + +@authors: Daphna Rosenbom,Gitty Zinger,Moshe Cohavi and Yoav Glazner +The widget is contributed by NDS ltd under the same license as wxPython + +items_picker.ItemsPicker: + - Displays available items to choose from, + - Selection is done by the Add button or Double Click, + - De-Selection is done by the Remove button or Double Click, + + Derived from wxPanel +''' +import wx +__version__ = 0.1 + +IP_DEFAULT_STYLE = 0 +IP_SORT_CHOICES = 1 +IP_SORT_SELECTED = 2 +IP_REMOVE_FROM_CHOICES = 4 + + +wxEVT_IP_SELECTION_CHANGED = wx.NewEventType() +EVT_IP_SELECTION_CHANGED = wx.PyEventBinder(wxEVT_IP_SELECTION_CHANGED, 1) +LB_STYLE = wx.LB_EXTENDED|wx.LB_HSCROLL + + +class IpSelectionChanged(wx.PyCommandEvent): + def __init__(self, id, items, object = None): + wx.PyCommandEvent.__init__(self, wxEVT_IP_SELECTION_CHANGED, id) + self.__items = items + self.SetEventObject(object) + + def GetItems(self): + return self.__items + + +class ItemsPicker(wx.Panel): + ''' + ItemsPicker is a widget that allows the user to form a set of picked + items out of a given list + ''' + def __init__(self, parent, id=wx.ID_ANY, choices = [], + label = '', selectedLabel = '', + ipStyle = IP_DEFAULT_STYLE, + *args, **kw): + ''' + ItemsPicker(parent, choices = [], label = '', selectedLabel = '', + ipStyle = IP_DEFAULT_STYLE) + ''' + wx.Panel.__init__(self, parent, id, *args, **kw) + self._ipStyle = ipStyle + sizer = wx.BoxSizer(wx.HORIZONTAL) + sizer.Add(self._CreateSourceList(choices, label), 1, + wx.EXPAND|wx.ALL, 5) + sizer.Add(self._CreateButtons(), 0, wx.ALIGN_CENTER|wx.ALL, 5) + sizer.Add(self._CreateDestList(selectedLabel), 1, + wx.EXPAND|wx.ALL, 5) + self.SetSizer(sizer) + + + def SetItems(self, items): + '''SetItems(self, items)=> None + items - Sequence of strings that the user can pick from''' + return self._source.SetItems(items) + + + def GetItems(self): + '''GetItems(self)=> items + returns list of strings that the user can pick from''' + return self._source.GetItems() + + + Items = property(fget = GetItems, + fset = SetItems, + doc = 'See GetItems/SetItems') + + + def GetSelections(self): + '''GetSelections(self)=>items + returns list of strings that were selected + ''' + return self._dest.GetItems() + + + def SetSelections(self, items): + '''SetSelections(self, items)=>None + items - Sequence of strings to be selected + The items are displayed in the selection part of the widget''' + assert len(items)==len(set(items)),"duplicate items are not allowed" + if items != self._dest.GetItems(): + self._dest.SetItems(items) + self._FireIpSelectionChanged() + + + Selections = property(fget = GetSelections, + fset = SetSelections, + doc = 'See GetSelections/SetSelections') + + + def _CreateButtons(self): + sizer = wx.BoxSizer(wx.VERTICAL) + self.bAdd = wx.Button(self, -1, label = 'Add ->') + self.bAdd.Bind(wx.EVT_BUTTON, self._OnAdd) + self.bRemove = wx.Button(self, -1, label = '<- Remove') + self.bRemove.Bind(wx.EVT_BUTTON, self._OnRemove) + sizer.Add(self.bAdd, 0, wx.EXPAND|wx.ALL|wx.ALIGN_CENTER_VERTICAL, 5) + sizer.Add(self.bRemove, 0, wx.EXPAND|wx.ALL, 5) + return sizer + + + def _set_add_button_label(self, label=None): + if label is None: + return + self.bAdd.SetLabel(label) + + add_button_label = property(fset = _set_add_button_label, fget = lambda x:x) + + + def _set_remove_button_label(self, label=None): + if label is None: + return + self.bRemove.SetLabel(label) + + remove_button_label = property(fset = _set_remove_button_label, fget = lambda x:x) + + + def _OnAdd(self, e): + if self._ipStyle & IP_REMOVE_FROM_CHOICES: + self._MoveItems(self._source,self._dest) + else: + self._AddSelectedItems() + + def _MoveItems(self,source,dest): + selections = source.GetSelections() + selectedItems = map(source.GetString, selections) + dest.SetItems(dest.GetItems() + selectedItems) + selections = set(selections) + source.SetItems([item for i, item in enumerate(source.GetItems())\ + if i not in selections]) + self._FireIpSelectionChanged() + + def _AddSelectedItems(self): + newItems = map(self._source.GetString, self._source.GetSelections()) + items = self._dest.GetItems() + oldItems = set(items) + for newItem in newItems: + if newItem not in oldItems: + items.append(newItem) + self.SetSelections(items) + + + def _FireIpSelectionChanged(self): + self.GetEventHandler().ProcessEvent( + IpSelectionChanged(self.GetId(), + self._dest.GetItems(), + self )) + + + def _OnRemove(self, e): + if self._ipStyle & IP_REMOVE_FROM_CHOICES: + self._MoveItems(self._dest, self._source) + else: + self._RemoveSelected() + + def _RemoveSelected(self): + selections = self._dest.GetSelections() + if selections: + allItems = self._dest.GetItems() + items = [item for i, item in enumerate(allItems)\ + if i not in selections] + self.SetSelections(items) + self._FireIpSelectionChanged() + + + def _CreateSourceList(self, items, label): + style = LB_STYLE + if self._ipStyle & IP_SORT_CHOICES: + style |= wx.LB_SORT + sizer = wx.BoxSizer(wx.VERTICAL) + if label: + sizer.Add(wx.StaticText(self, label = label), 0, + wx.ALIGN_LEFT|wx.ALL, 5) + self._source = wx.ListBox(self, -1, style = style) + self._source.Bind(wx.EVT_LISTBOX_DCLICK, self._OnDClick) + self._source.SetItems(items) + sizer.Add(self._source, 1, wx.EXPAND|wx.ALL, 5) + return sizer + + + def _CreateDestList(self, label): + style = LB_STYLE + if self._ipStyle & IP_SORT_SELECTED: + style |= wx.LB_SORT + sizer = wx.BoxSizer(wx.VERTICAL) + if label: + sizer.Add(wx.StaticText(self, label = label), 0, + wx.ALIGN_LEFT|wx.ALL, 5) + self._dest = wx.ListBox(self, -1, style = style) + self._dest.Bind(wx.EVT_LISTBOX_DCLICK, self._OnDClick) + sizer.Add(self._dest, 1, wx.EXPAND|wx.ALL, 5) + return sizer + + + def _OnDClick(self, e): + lb = e.GetEventObject() + selections = lb.GetSelections() + if len(selections) != 1: + return #DCLICK only works on one item + if e.GetSelection() != selections[0]: + #this can happen using ^DCLICK when two items are selected + return + if lb == self._source: + self._OnAdd(e) + else: + self._OnRemove(e) + + + + +if __name__ == '__main__': + test = wx.PySimpleApp(0) + frame = wx.Frame(None, -1) + d = wx.Dialog(frame, style = wx.RESIZE_BORDER|wx.DEFAULT_DIALOG_STYLE) + + d.sizer = wx.BoxSizer(wx.VERTICAL) + d.sizer.Add(wx.StaticText(d, -1, label = 'Example of the ItemsPicker'), + 0, wx.ALL, 10) + ip = ItemsPicker(d, -1, ['pop', 'cool', 'lame'], + 'Stuff:', 'Selected stuff:',IP_SORT_SELECTED|IP_SORT_CHOICES|IP_REMOVE_FROM_CHOICES) + ip.add_button_label = u'left -> right' + ip.remove_button_label = u'right -> left' + d.sizer.Add(ip, 1, wx.EXPAND, 1) + d.SetSizer(d.sizer) + test.SetTopWindow(frame) + def callback(e): + print 'selected items', e.GetItems() + d.Bind(EVT_IP_SELECTION_CHANGED, callback) + d.ShowModal() + d.Destroy() + frame.Close() + + diff --git a/wx/lib/langlistctrl.py b/wx/lib/langlistctrl.py new file mode 100644 index 00000000..f2348b08 --- /dev/null +++ b/wx/lib/langlistctrl.py @@ -0,0 +1,424 @@ +#----------------------------------------------------------------------------- +# Name: languagectrls.py +# Purpose: +# +# Author: Riaan Booysen +# +# Created: 2006 +# RCS-ID: $Id$ +# Copyright: (c) 2006 Riaan Booysen +# License: wxPython +#----------------------------------------------------------------------------- +""" ListCtrl and functions to display languages and the flags of their countries +""" +import wx + +from wx.lib.art import flagart + +langIdCountryMap = { + # generated from wx.Locale info and locale.windows_locale + wx.LANGUAGE_AFRIKAANS: 'ZA', + wx.LANGUAGE_ALBANIAN: 'AL', + wx.LANGUAGE_ARABIC_ALGERIA: 'DZ', + wx.LANGUAGE_ARABIC_BAHRAIN: 'BH', + wx.LANGUAGE_ARABIC_EGYPT: 'EG', + wx.LANGUAGE_ARABIC_IRAQ: 'IQ', + wx.LANGUAGE_ARABIC_JORDAN: 'JO', + wx.LANGUAGE_ARABIC_KUWAIT: 'KW', + wx.LANGUAGE_ARABIC_LEBANON: 'LB', + wx.LANGUAGE_ARABIC_LIBYA: 'LY', + wx.LANGUAGE_ARABIC_MOROCCO: 'MA', + wx.LANGUAGE_ARABIC_OMAN: 'OM', + wx.LANGUAGE_ARABIC_QATAR: 'QA', + wx.LANGUAGE_ARABIC_SAUDI_ARABIA: 'SA', + wx.LANGUAGE_ARABIC_SUDAN: 'SD', + wx.LANGUAGE_ARABIC_SYRIA: 'SY', + wx.LANGUAGE_ARABIC_TUNISIA: 'TN', + wx.LANGUAGE_ARABIC_UAE: 'AE', + wx.LANGUAGE_ARABIC_YEMEN: 'YE', + wx.LANGUAGE_ARMENIAN: 'AM', + wx.LANGUAGE_AZERI: 'AZ', + wx.LANGUAGE_AZERI_CYRILLIC: 'AZ', + wx.LANGUAGE_AZERI_LATIN: 'AZ', + wx.LANGUAGE_BASQUE: 'ES', + wx.LANGUAGE_BELARUSIAN: 'BY', + wx.LANGUAGE_BENGALI: 'IN', + wx.LANGUAGE_BRETON: 'FR', + wx.LANGUAGE_BULGARIAN: 'BG', + wx.LANGUAGE_CATALAN: 'ES', + wx.LANGUAGE_CHINESE: 'CN', + wx.LANGUAGE_CHINESE_HONGKONG: 'HK', + wx.LANGUAGE_CHINESE_MACAU: 'MO', + wx.LANGUAGE_CHINESE_SIMPLIFIED: 'CN', + wx.LANGUAGE_CHINESE_SINGAPORE: 'SG', + wx.LANGUAGE_CHINESE_TAIWAN: 'TW', + wx.LANGUAGE_CHINESE_TRADITIONAL: 'CN', + wx.LANGUAGE_CROATIAN: 'HR', + wx.LANGUAGE_CZECH: 'CZ', + wx.LANGUAGE_DANISH: 'DK', +# wx.LANGUAGE_DEFAULT: 'ZA', + wx.LANGUAGE_DUTCH: 'NL', + wx.LANGUAGE_DUTCH_BELGIAN: 'BE', + wx.LANGUAGE_ENGLISH: 'GB', + wx.LANGUAGE_ENGLISH_AUSTRALIA: 'AU', + wx.LANGUAGE_ENGLISH_BELIZE: 'BZ', + wx.LANGUAGE_ENGLISH_BOTSWANA: 'BW', + wx.LANGUAGE_ENGLISH_CANADA: 'CA', + wx.LANGUAGE_ENGLISH_CARIBBEAN: 'CB', + wx.LANGUAGE_ENGLISH_DENMARK: 'DK', + wx.LANGUAGE_ENGLISH_EIRE: 'IE', + wx.LANGUAGE_ENGLISH_JAMAICA: 'JM', + wx.LANGUAGE_ENGLISH_NEW_ZEALAND: 'NZ', + wx.LANGUAGE_ENGLISH_PHILIPPINES: 'PH', + wx.LANGUAGE_ENGLISH_SOUTH_AFRICA: 'ZA', + wx.LANGUAGE_ENGLISH_TRINIDAD: 'TT', + wx.LANGUAGE_ENGLISH_UK: 'GB', + wx.LANGUAGE_ENGLISH_US: 'US', + wx.LANGUAGE_ENGLISH_ZIMBABWE: 'ZW', + wx.LANGUAGE_ESTONIAN: 'EE', + wx.LANGUAGE_FAEROESE: 'FO', + wx.LANGUAGE_FARSI: 'IR', + wx.LANGUAGE_FINNISH: 'FI', + wx.LANGUAGE_FRENCH: 'FR', + wx.LANGUAGE_FRENCH_BELGIAN: 'BE', + wx.LANGUAGE_FRENCH_CANADIAN: 'CA', + wx.LANGUAGE_FRENCH_LUXEMBOURG: 'LU', + wx.LANGUAGE_FRENCH_MONACO: 'MC', + wx.LANGUAGE_FRENCH_SWISS: 'CH', + wx.LANGUAGE_FRISIAN: 'NL', + wx.LANGUAGE_GALICIAN: 'ES', + wx.LANGUAGE_GEORGIAN: 'GE', + wx.LANGUAGE_GERMAN: 'DE', + wx.LANGUAGE_GERMAN_AUSTRIAN: 'AT', + wx.LANGUAGE_GERMAN_BELGIUM: 'BE', + wx.LANGUAGE_GERMAN_LIECHTENSTEIN: 'LI', + wx.LANGUAGE_GERMAN_LUXEMBOURG: 'LU', + wx.LANGUAGE_GERMAN_SWISS: 'CH', + wx.LANGUAGE_GREEK: 'GR', + wx.LANGUAGE_GREENLANDIC: 'GL', + wx.LANGUAGE_GUJARATI: 'IN', + wx.LANGUAGE_HEBREW: 'IL', + wx.LANGUAGE_HINDI: 'IN', + wx.LANGUAGE_HUNGARIAN: 'HU', + wx.LANGUAGE_ICELANDIC: 'IS', + wx.LANGUAGE_INDONESIAN: 'ID', + wx.LANGUAGE_INUKTITUT: 'CA', + wx.LANGUAGE_IRISH: 'IE', + wx.LANGUAGE_ITALIAN: 'IT', + wx.LANGUAGE_ITALIAN_SWISS: 'CH', + wx.LANGUAGE_JAPANESE: 'JP', + wx.LANGUAGE_KANNADA: 'IN', + wx.LANGUAGE_KASHMIRI_INDIA: 'IN', + wx.LANGUAGE_KAZAKH: 'KZ', + wx.LANGUAGE_KERNEWEK: 'GB', + wx.LANGUAGE_KIRGHIZ: 'KG', + wx.LANGUAGE_KOREAN: 'KR', + wx.LANGUAGE_LATVIAN: 'LV', + wx.LANGUAGE_LITHUANIAN: 'LT', + wx.LANGUAGE_MACEDONIAN: 'MK', + wx.LANGUAGE_MALAY: 'MY', + wx.LANGUAGE_MALAYALAM: 'IN', + wx.LANGUAGE_MALAY_BRUNEI_DARUSSALAM: 'BN', + wx.LANGUAGE_MALAY_MALAYSIA: 'MY', + wx.LANGUAGE_MALTESE: 'MT', + wx.LANGUAGE_MAORI: 'NZ', + wx.LANGUAGE_MARATHI: 'IN', + wx.LANGUAGE_MONGOLIAN: 'MN', + wx.LANGUAGE_NEPALI: 'NP', + wx.LANGUAGE_NEPALI_INDIA: 'IN', + wx.LANGUAGE_NORWEGIAN_BOKMAL: 'NO', + wx.LANGUAGE_NORWEGIAN_NYNORSK: 'NO', + wx.LANGUAGE_OCCITAN: 'FR', + wx.LANGUAGE_ORIYA: 'IN', + wx.LANGUAGE_PASHTO: 'AF', + wx.LANGUAGE_POLISH: 'PL', + wx.LANGUAGE_PORTUGUESE: 'PT', + wx.LANGUAGE_PORTUGUESE_BRAZILIAN: 'BR', + wx.LANGUAGE_PUNJABI: 'IN', + wx.LANGUAGE_RHAETO_ROMANCE: 'CH', + wx.LANGUAGE_ROMANIAN: 'RO', + wx.LANGUAGE_RUSSIAN: 'RU', + wx.LANGUAGE_RUSSIAN_UKRAINE: 'UA', + wx.LANGUAGE_SANSKRIT: 'IN', + wx.LANGUAGE_SERBIAN_CYRILLIC: 'YU', + wx.LANGUAGE_SERBIAN_LATIN: 'YU', + wx.LANGUAGE_SETSWANA: 'ZA', + wx.LANGUAGE_SLOVAK: 'SK', + wx.LANGUAGE_SLOVENIAN: 'SI', + wx.LANGUAGE_SPANISH: 'ES', + wx.LANGUAGE_SPANISH_ARGENTINA: 'AR', + wx.LANGUAGE_SPANISH_BOLIVIA: 'BO', + wx.LANGUAGE_SPANISH_CHILE: 'CL', + wx.LANGUAGE_SPANISH_COLOMBIA: 'CO', + wx.LANGUAGE_SPANISH_COSTA_RICA: 'CR', + wx.LANGUAGE_SPANISH_DOMINICAN_REPUBLIC: 'DO', + wx.LANGUAGE_SPANISH_ECUADOR: 'EC', + wx.LANGUAGE_SPANISH_EL_SALVADOR: 'SV', + wx.LANGUAGE_SPANISH_GUATEMALA: 'GT', + wx.LANGUAGE_SPANISH_HONDURAS: 'HN', + wx.LANGUAGE_SPANISH_MEXICAN: 'MX', + wx.LANGUAGE_SPANISH_MODERN: 'ES', + wx.LANGUAGE_SPANISH_NICARAGUA: 'NI', + wx.LANGUAGE_SPANISH_PANAMA: 'PA', + wx.LANGUAGE_SPANISH_PARAGUAY: 'PY', + wx.LANGUAGE_SPANISH_PERU: 'PE', + wx.LANGUAGE_SPANISH_PUERTO_RICO: 'PR', + wx.LANGUAGE_SPANISH_URUGUAY: 'UY', + wx.LANGUAGE_SPANISH_US: 'US', + wx.LANGUAGE_SPANISH_VENEZUELA: 'VE', + wx.LANGUAGE_SWAHILI: 'KE', + wx.LANGUAGE_SWEDISH: 'SE', + wx.LANGUAGE_SWEDISH_FINLAND: 'FI', + wx.LANGUAGE_TAGALOG: 'PH', + wx.LANGUAGE_TAMIL: 'IN', + wx.LANGUAGE_TATAR: 'RU', + wx.LANGUAGE_TELUGU: 'IN', + wx.LANGUAGE_THAI: 'TH', + wx.LANGUAGE_TURKISH: 'TR', + wx.LANGUAGE_UKRAINIAN: 'UA', + wx.LANGUAGE_URDU: 'PK', + wx.LANGUAGE_URDU_INDIA: 'IN', + wx.LANGUAGE_URDU_PAKISTAN: 'PK', + wx.LANGUAGE_UZBEK: 'UZ', + wx.LANGUAGE_UZBEK_CYRILLIC: 'UZ', + wx.LANGUAGE_UZBEK_LATIN: 'UZ', + wx.LANGUAGE_VIETNAMESE: 'VN', + wx.LANGUAGE_WELSH: 'GB', + wx.LANGUAGE_XHOSA: 'ZA', + wx.LANGUAGE_ZULU: 'ZA', + # manually defined language/country mapping + wx.LANGUAGE_ABKHAZIAN: 'GE', + wx.LANGUAGE_AFAR: 'ET', + wx.LANGUAGE_AMHARIC: 'ET', + wx.LANGUAGE_ASSAMESE: 'IN', + wx.LANGUAGE_AYMARA: 'BO', + wx.LANGUAGE_ARABIC: 'SA', + wx.LANGUAGE_BASHKIR: 'RU', + wx.LANGUAGE_BHUTANI: 'BT', + wx.LANGUAGE_BIHARI: 'IN', + wx.LANGUAGE_BISLAMA: 'VU', + wx.LANGUAGE_BURMESE: 'MM', + wx.LANGUAGE_CAMBODIAN: 'KH', + wx.LANGUAGE_CORSICAN: 'FR', + wx.LANGUAGE_ESPERANTO: 'ESPERANTO', + wx.LANGUAGE_FIJI: 'FJ', + wx.LANGUAGE_GUARANI: 'PY', + wx.LANGUAGE_HAUSA: 'NG', + wx.LANGUAGE_INTERLINGUA: 'US', + wx.LANGUAGE_INTERLINGUE: 'US', + wx.LANGUAGE_INUPIAK: 'US', + wx.LANGUAGE_JAVANESE: 'IN', + wx.LANGUAGE_KASHMIRI: 'IN', + wx.LANGUAGE_KINYARWANDA: 'RW', + wx.LANGUAGE_KIRUNDI: 'BI', + wx.LANGUAGE_KONKANI: 'IN', + wx.LANGUAGE_KURDISH: 'IQ', + wx.LANGUAGE_LAOTHIAN: 'LA', + wx.LANGUAGE_LATIN: 'VA', + wx.LANGUAGE_LINGALA: 'CD', + wx.LANGUAGE_MALAGASY: 'MG', + wx.LANGUAGE_MANIPURI: 'IN', + wx.LANGUAGE_MOLDAVIAN: 'MD', + wx.LANGUAGE_NAURU: 'NR', + wx.LANGUAGE_OROMO: 'ET', + wx.LANGUAGE_QUECHUA: 'BO', + wx.LANGUAGE_SAMOAN: 'WS', + wx.LANGUAGE_SANGHO: 'CF', + wx.LANGUAGE_SCOTS_GAELIC: 'GB', + wx.LANGUAGE_SERBO_CROATIAN: 'HR', + wx.LANGUAGE_SESOTHO: 'ZA', + wx.LANGUAGE_SHONA: 'ZW', + wx.LANGUAGE_SINDHI: 'PK', + wx.LANGUAGE_SINHALESE: 'IN', + wx.LANGUAGE_SISWATI: 'SZ', + wx.LANGUAGE_SOMALI: 'SB', + wx.LANGUAGE_SUNDANESE: 'SD', + wx.LANGUAGE_TAJIK: 'TJ', + wx.LANGUAGE_TIBETAN: 'CN', + wx.LANGUAGE_TIGRINYA: 'ET', + wx.LANGUAGE_TONGA: 'TO', + wx.LANGUAGE_TSONGA: 'MZ', + wx.LANGUAGE_TURKMEN: 'TM', + wx.LANGUAGE_TWI: 'GH', + wx.LANGUAGE_UIGHUR: 'CN', + wx.LANGUAGE_VOLAPUK: 'VOLAPUK', + wx.LANGUAGE_WOLOF: 'SN', + wx.LANGUAGE_YIDDISH: 'IL', + wx.LANGUAGE_YORUBA: 'NG', + wx.LANGUAGE_ZHUANG: 'CN', +} + +LC_AVAILABLE, LC_ALL, LC_ONLY = 1, 2, 4 + + +# wx.LANGUAGE_SERBIAN gives an error for me +_wxLangIds = [n for n in dir(wx) if n.startswith('LANGUAGE_')] +for _l in ('LANGUAGE_UNKNOWN', 'LANGUAGE_USER_DEFINED', 'LANGUAGE_SERBIAN'): + if _l in _wxLangIds: + _wxLangIds.remove(_l) + + +def CreateLanguagesResourceLists(filter=LC_AVAILABLE, only=()): + """ Returns a tuple of (bitmaps, language descriptions, language ids) """ + icons = wx.ImageList(16, 11) + names = [] + langs = [] + + langIdNameMap = BuildLanguageCountryMapping() + + wxLangIds = [] + for li in _wxLangIds: + wxLI = getattr(wx, li) + try: + if (filter == LC_ONLY and wxLI in only) or \ + (filter == LC_AVAILABLE and wx.Locale.IsAvailable(wxLI)) or \ + (filter == LC_ALL): + wxLangIds.append(wxLI) + except wx.PyAssertionError: + # invalid language assertions + pass + except AttributeError: + # wx 2.6 + wxLangIds.append(wxLI) + + langCodes = [(langIdNameMap[wxLangId], wxLangId) + for wxLangId in wxLangIds + if wxLangId in langIdNameMap] + + for lc, wxli in langCodes: + l, cnt = lc.split('_') + + if cnt in flagart.catalog: + bmp = flagart.catalog[cnt].getBitmap() + else: + bmp = flagart.catalog['BLANK'].getBitmap() + + icons.Add(bmp) + name = wx.Locale.GetLanguageName(wxli) + if wxli == wx.LANGUAGE_DEFAULT: + #print cnt, name, lc, wxli + name = 'Default: '+name + + names.append(name) + langs.append(wxli) + + return icons, names, langs + + +def GetLanguageFlag(lang): + """ Returns a bitmap of the flag for the country of the language id """ + langIdNameMap = BuildLanguageCountryMapping() + if lang in langIdNameMap: + cnt = langIdNameMap[lang].split('_')[1] + if cnt in flagart.catalog: + return flagart.catalog[cnt].getBitmap() + return flagart.catalog['BLANK'].getBitmap() + + +def BuildLanguageCountryMapping(): + """ Builds a mapping of language ids to LANG_COUNTRY codes """ + res = {} + for name in _wxLangIds: + n = 'wx.'+name + wn = getattr(wx, name) + + li = wx.Locale.GetLanguageInfo(wn) + if li: + code = li.CanonicalName + + if wn in langIdCountryMap: + # override, drop country + if '_' in code: + code = code.split('_')[0] + code += '_'+langIdCountryMap[wn] + + # map unhandled to blank images + elif '_' not in code: + code += '_BLANK' + + res[wn] = code + return res + +def GetWxIdentifierForLanguage(lang): + """ Returns the language id as a string """ + for n in dir(wx): + if n.startswith('LANGUAGE_') and getattr(wx, n) == lang: + return n + raise Exception, 'Language %s not found'%lang + + +#------------------------------------------------------------------------------- + +class LanguageListCtrl(wx.ListCtrl): + """ wx.ListCtrl derived control that displays languages and flags """ + def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, + size=wx.DefaultSize, style=wx.LC_REPORT | wx.LC_NO_HEADER | wx.LC_SINGLE_SEL, + filter=LC_AVAILABLE, only=(), select=None, name='languagelistctrl'): + + wx.ListCtrl.__init__(self, parent, id, pos, size, style, name=name) + + self.SetUpFilter(filter, only) + self.Language = select + + def SetUpFilter(self, filter=LC_AVAILABLE, only=()): + """ Filters the languages displayed in the control """ + lang = self.GetLanguage() + + self.filter, self.only = filter, only + self.icons, self.choices, self.langs = CreateLanguagesResourceLists(filter, only) + + self.AssignImageList(self.icons, wx.IMAGE_LIST_SMALL) + + self.ClearAll() + self.InsertColumn(0, '', width=175) + for i in range(len(self.choices)): + self.InsertImageStringItem(i, self.choices[i], i) + + self.SetLanguage(lang) + + def GetLanguage(self): + """ Returns the language id for the currently selected language in the control """ + idx = self.GetFirstSelected() + if idx != -1: + return self.langs[idx] + else: + None + + def SetLanguage(self, lang): + """ Selects the given language ids item in the control """ + if lang is not None: + if lang in self.langs: + idx = self.langs.index(lang) + self.Select(idx) + self.Focus(idx) + + Language = property(GetLanguage, SetLanguage, doc="See `GetLanguage` and `SetLanguage`") + +#------------------------------------------------------------------------------- + + +if __name__ == '__main__': + a = wx.PySimpleApp() + + print GetLanguageFlag(wx.LANGUAGE_AFRIKAANS) + + f=wx.Frame(None, -1) + f.p=wx.Panel(f, -1) + s=wx.BoxSizer(wx.VERTICAL) + f.p.SetSizer(s) + try: + f.lc=LanguageChoice(f.p, pos = (220, 10), size = (200, 25)) + s.Add(f.lc, 0, wx.GROW) + except: + pass + f.llc=LanguageListCtrl(f.p, pos = (10, 10), size = (200, 200), + filter=LC_ONLY, + only=(wx.LANGUAGE_AFRIKAANS, wx.LANGUAGE_ENGLISH, + wx.LANGUAGE_FRENCH, wx.LANGUAGE_GERMAN, wx.LANGUAGE_ITALIAN, + wx.LANGUAGE_PORTUGUESE_BRAZILIAN, wx.LANGUAGE_SPANISH), + select=wx.LANGUAGE_ENGLISH) +## filter=LC_ALL) + s.Add(f.llc, 1, wx.GROW) + f.Show() + + a.MainLoop() diff --git a/wx/lib/layoutf.py b/wx/lib/layoutf.py new file mode 100644 index 00000000..82111fb4 --- /dev/null +++ b/wx/lib/layoutf.py @@ -0,0 +1,270 @@ +# 12/09/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o Updated for wx namespace +# +# 12/18/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o wxScrolledMessageDialog -> ScrolledMessageDialog +# + +import re +import wx + +class Layoutf(wx.LayoutConstraints): + """ + The class Layoutf(wxLayoutConstraints) presents a simplification + of the wxLayoutConstraints syntax. The name Layoutf is choosen + because of the similarity with C's printf function. + + Quick Example:: + + lc = Layoutf('t=t#1;l=r10#2;r!100;h%h50#1', (self, self.panel)) + + + is equivalent to:: + + lc = wx.LayoutContraints() + lc.top.SameAs(self, wx.Top) + lc.left.SameAs(self.panel, wx.Right, 10) + lc.right.Absolute(100) + lc.height.PercentOf(self, wx.Height, 50) + + + Usage: + + You can give a constraint string to the Layoutf constructor, + or use the 'pack' method. The following are equivalent:: + + lc = Layoutf('t=t#1;l=r#2;r!100;h%h50#1', (self, self.panel)) + + + and:: + + lc = Layoutf() + lc.pack('t=t#1;l=r#2;r!100;h%h50#1', (self, self.panel)) + + + Besides 'pack' there's also 'debug_pack' which does not set + constraints, but prints traditional wxLayoutConstraint calls to + stdout. + + The calls to the Layoutf constructor and pack methods have + the following argument list: + + `(constraint_string, objects_tuple)` + + + Constraint String syntax: + + Constraint directives are separated by semi-colons. You + generally (always?) need four directives to completely describe a + subwindow's location. + + A single directive has either of the following forms: + + 1. [numerical argument] + for example ``r!100`` -> lc.right.Absolute(100) ) + and ``w*`` -> lc.width.AsIs() + + 2. [numerical argument] + # + for example ``t_10#2`` -> lc.top.Below(, 10) + + 3. + [numerical argument]# + for example ``w%h50#2`` -> lc.width.PercentOf(, wx.Height, 50) and ``t=b#1`` -> lc.top.SameAs(, wx.Bottom) + + Which one you need is defined by the + type. The following take type 1 (no object to compare with): + + * '!': 'Absolute', '?': 'Unconstrained', '*': 'AsIs' + + These take type 2 (need to be compared with another object) + + * '<': 'LeftOf', '>': 'RightOf', '^': 'Above', '_': 'Below' + + These take type 3 (need to be compared to another object + attribute) + + * '=': 'SameAs', '%': 'PercentOf' + + For all types, the letter can be any of + + * 't': 'top', 'l': 'left', 'b': 'bottom', + * 'r': 'right', 'h': 'height', 'w': 'width', + * 'x': 'centreX', 'y': 'centreY' + + If the operation takes an (optional) numerical argument, place it + in [numerical argument]. For type 3 directives, the letter can be any of + + * 't': 'wxTop', 'l': 'wxLeft', 'b': 'wx.Bottom' + * 'r': 'wxRight', 'h': 'wxHeight', 'w': 'wx.Width', + * 'x': 'wxCentreX', 'y': 'wx.CentreY' + + Note that these are the same letters as used for , + so you'll only need to remember one set. Finally, the object + whose attribute is refered to, is specified by #, where is the 1-based (stupid, I know, + but I've gotten used to it) index of the object in the + objects_tuple argument. + + Bugs: + + Not entirely happy about the logic in the order of arguments + after the character. + + Not all wxLayoutConstraint methods are included in the + syntax. However, the type 3 directives are generally the most + used. Further excuse: wxWindows layout constraints are at the + time of this writing not documented. + +""" + + attr_d = { 't': 'top', 'l': 'left', 'b': 'bottom', + 'r': 'right', 'h': 'height', 'w': 'width', + 'x': 'centreX', 'y': 'centreY' } + op_d = { '=': 'SameAs', '%': 'PercentOf', '<': 'LeftOf', + '>': 'RightOf', '^': 'Above', '_': 'Below', + '!': 'Absolute', '?': 'Unconstrained', '*': 'AsIs' } + cmp_d = { 't': 'wx.Top', 'l': 'wx.Left', 'b': 'wx.Bottom', + 'r': 'wx.Right', 'h': 'wx.Height', 'w': 'wx.Width', + 'x': 'wx.CentreX', 'y': 'wx.CentreY' } + + rexp1 = re.compile('^\s*([tlrbhwxy])\s*([!\?\*])\s*(\d*)\s*$') + rexp2 = re.compile('^\s*([tlrbhwxy])\s*([=%<>^_])\s*([tlrbhwxy]?)\s*(\d*)\s*#(\d+)\s*$') + + def __init__(self,pstr=None,winlist=None): + wx.LayoutConstraints.__init__(self) + if pstr: + self.pack(pstr,winlist) + + def pack(self, pstr, winlist): + pstr = pstr.lower() + for item in pstr.split(';'): + m = self.rexp1.match(item) + if m: + g = list(m.groups()) + attr = getattr(self, self.attr_d[g[0]]) + func = getattr(attr, self.op_d[g[1]]) + if g[1] == '!': + func(int(g[2])) + else: + func() + continue + m = self.rexp2.match(item) + if not m: raise ValueError + g = list(m.groups()) + attr = getattr(self, self.attr_d[g[0]]) + func = getattr(attr, self.op_d[g[1]]) + if g[3]: g[3] = int(g[3]) + else: g[3] = None; + g[4] = int(g[4]) - 1 + if g[1] in '<>^_': + if g[3]: func(winlist[g[4]], g[3]) + else: func(winlist[g[4]]) + else: + cmp = eval(self.cmp_d[g[2]]) + if g[3]: func(winlist[g[4]], cmp, g[3]) + else: func(winlist[g[4]], cmp) + + def debug_pack(self, pstr, winlist): + pstr = pstr.lower() + for item in pstr.split(';'): + m = self.rexp1.match(item) + if m: + g = list(m.groups()) + attr = getattr(self, self.attr_d[g[0]]) + func = getattr(attr, self.op_d[g[1]]) + if g[1] == '!': + print "%s.%s.%s(%s)" % \ + ('self',self.attr_d[g[0]],self.op_d[g[1]],g[2]) + else: + print "%s.%s.%s()" % \ + ('self',self.attr_d[g[0]],self.op_d[g[1]]) + continue + m = self.rexp2.match(item) + if not m: raise ValueError + g = list(m.groups()) + if g[3]: g[3] = int(g[3]) + else: g[3] = 0; + g[4] = int(g[4]) - 1 + if g[1] in '<>^_': + if g[3]: print "%s.%s.%s(%s,%d)" % \ + ('self',self.attr_d[g[0]],self.op_d[g[1]],winlist[g[4]], + g[3]) + else: print "%s.%s.%s(%s)" % \ + ('self',self.attr_d[g[0]],self.op_d[g[1]],winlist[g[4]]) + else: + if g[3]: print "%s.%s.%s(%s,%s,%d)" % \ + ('self',self.attr_d[g[0]],self.op_d[g[1]],winlist[g[4]], + self.cmp_d[g[2]],g[3]) + else: print "%s.%s.%s(%s,%s)" % \ + ('self',self.attr_d[g[0]],self.op_d[g[1]],winlist[g[4]], + self.cmp_d[g[2]]) + +if __name__=='__main__': + + class TestLayoutf(wx.Frame): + def __init__(self, parent): + wx.Frame.__init__(self, parent, -1, 'Test Layout Constraints', + wx.DefaultPosition, (500, 300)) + self.Bind(wx.EVT_CLOSE, self.OnCloseWindow) + + self.SetAutoLayout(True) + + self.panelA = wx.Window(self, -1, style=wx.SIMPLE_BORDER) + self.panelA.SetBackgroundColour(wx.BLUE) + self.panelA.SetConstraints(Layoutf('t=t10#1;l=l10#1;b=b10#1;r%r50#1',(self,))) + + self.panelB = wx.Window(self, -1, style=wx.SIMPLE_BORDER) + self.panelB.SetBackgroundColour(wx.RED) + self.panelB.SetConstraints(Layoutf('t=t10#1;r=r10#1;b%b30#1;l>10#2', (self,self.panelA))) + + self.panelC = wx.Window(self, -1, style=wx.SIMPLE_BORDER) + self.panelC.SetBackgroundColour(wx.WHITE) + self.panelC.SetConstraints(Layoutf('t_10#3;r=r10#1;b=b10#1;l>10#2', (self,self.panelA,self.panelB))) + + b = wx.Button(self.panelA, -1, ' About: ') + b.SetConstraints(Layoutf('X=X#1;Y=Y#1;h*;w%w50#1', (self.panelA,))) + self.Bind(wx.EVT_BUTTON, self.OnAbout, b) + + b = wx.Button(self.panelB, 100, ' Panel B ') + b.SetConstraints(Layoutf('t=t2#1;r=r4#1;h*;w*', (self.panelB,))) + + self.panelD = wx.Window(self.panelC, -1, style=wx.SIMPLE_BORDER) + self.panelD.SetBackgroundColour(wx.GREEN) + self.panelD.SetConstraints(Layoutf('b%h50#1;r%w50#1;h=h#2;w=w#2', (self.panelC, b))) + + b = wx.Button(self.panelC, -1, ' Panel C ') + b.SetConstraints(Layoutf('t_#1;l>#1;h*;w*', (self.panelD,))) + self.Bind(wx.EVT_BUTTON, self.OnButton, b) + + wx.StaticText(self.panelD, -1, "Panel D", (4, 4)).SetBackgroundColour(wx.GREEN) + + def OnButton(self, event): + self.Close(True) + + def OnAbout(self, event): + import wx.lib.dialogs + msg = wx.lib.dialogs.ScrolledMessageDialog(self, Layoutf.__doc__, "about") + msg.ShowModal() + msg.Destroy() + + def OnCloseWindow(self, event): + self.Destroy() + + class TestApp(wx.App): + def OnInit(self): + frame = TestLayoutf(None) + frame.Show(1) + self.SetTopWindow(frame) + return 1 + + app = TestApp(0) + app.MainLoop() + + + + + diff --git a/wx/lib/masked/__init__.py b/wx/lib/masked/__init__.py new file mode 100644 index 00000000..0f8824bd --- /dev/null +++ b/wx/lib/masked/__init__.py @@ -0,0 +1,26 @@ +#---------------------------------------------------------------------- +# Name: wxPython.lib.masked +# Purpose: A package containing the masked edit controls +# +# Author: Will Sadkin, Jeff Childers +# +# Created: 6-Mar-2004 +# RCS-ID: $Id$ +# Copyright: (c) 2004 +# License: wxWidgets license +#---------------------------------------------------------------------- +__author__ = "Will Sadkin " +__date__ = "02 Dec 2006, 19:00 GMT-05:00" +__version__ = "1.11" +__doc__ = """\ +package providing "masked edit" controls, allowing characters within a data entry control to remain fixed, and providing fine-grain control over allowed user input. +""" + +# import relevant external symbols into package namespace: +from maskededit import * +from textctrl import BaseMaskedTextCtrl, PreMaskedTextCtrl, TextCtrl +from combobox import BaseMaskedComboBox, PreMaskedComboBox, ComboBox, MaskedComboBoxSelectEvent +from numctrl import NumCtrl, wxEVT_COMMAND_MASKED_NUMBER_UPDATED, EVT_NUM, NumberUpdatedEvent +from timectrl import TimeCtrl, wxEVT_TIMEVAL_UPDATED, EVT_TIMEUPDATE, TimeUpdatedEvent +from ipaddrctrl import IpAddrCtrl +from ctrl import Ctrl, controlTypes diff --git a/wx/lib/masked/combobox.py b/wx/lib/masked/combobox.py new file mode 100644 index 00000000..145a701a --- /dev/null +++ b/wx/lib/masked/combobox.py @@ -0,0 +1,775 @@ +#---------------------------------------------------------------------------- +# Name: masked.combobox.py +# Authors: Will Sadkin +# Email: wsadkin@nameconnector.com +# Created: 02/11/2003 +# Copyright: (c) 2003 by Will Sadkin, 2003 +# RCS-ID: $Id$ +# License: wxWidgets license +#---------------------------------------------------------------------------- +# +# This masked edit class allows for the semantics of masked controls +# to be applied to combo boxes. +# +#---------------------------------------------------------------------------- + +""" +Provides masked edit capabilities within a ComboBox format, as well as +a base class from which you can derive masked comboboxes tailored to a specific +function. See maskededit module overview for how to configure the control. +""" + +import wx, types, string +from wx.lib.masked import * + +# jmg 12/9/03 - when we cut ties with Py 2.2 and earlier, this would +# be a good place to implement the 2.3 logger class +from wx.tools.dbg import Logger +##dbg = Logger() +##dbg(enable=1) + +## ---------- ---------- ---------- ---------- ---------- ---------- ---------- +## Because calling SetSelection programmatically does not fire EVT_COMBOBOX +## events, we have to do it ourselves when we auto-complete. +class MaskedComboBoxSelectEvent(wx.PyCommandEvent): + """ + Because calling SetSelection programmatically does not fire EVT_COMBOBOX + events, the derived control has to do it itself when it auto-completes. + """ + def __init__(self, id, selection = 0, object=None): + wx.PyCommandEvent.__init__(self, wx.wxEVT_COMMAND_COMBOBOX_SELECTED, id) + + self.__selection = selection + self.SetEventObject(object) + + def GetSelection(self): + """Retrieve the value of the control at the time + this event was generated.""" + return self.__selection + +class MaskedComboBoxEventHandler(wx.EvtHandler): + """ + This handler ensures that the derived control can react to events + from the base control before any external handlers run, to ensure + proper behavior. + """ + def __init__(self, combobox): + wx.EvtHandler.__init__(self) + self.combobox = combobox + combobox.PushEventHandler(self) + self.Bind(wx.EVT_SET_FOCUS, self.combobox._OnFocus ) ## defeat automatic full selection + self.Bind(wx.EVT_KILL_FOCUS, self.combobox._OnKillFocus ) ## run internal validator + self.Bind(wx.EVT_LEFT_DCLICK, self.combobox._OnDoubleClick) ## select field under cursor on dclick + self.Bind(wx.EVT_RIGHT_UP, self.combobox._OnContextMenu ) ## bring up an appropriate context menu + self.Bind(wx.EVT_CHAR, self.combobox._OnChar ) ## handle each keypress + self.Bind(wx.EVT_KEY_DOWN, self.combobox._OnKeyDownInComboBox ) ## for special processing of up/down keys + self.Bind(wx.EVT_KEY_DOWN, self.combobox._OnKeyDown ) ## for processing the rest of the control keys + ## (next in evt chain) + self.Bind(wx.EVT_COMBOBOX, self.combobox._OnDropdownSelect ) ## to bring otherwise completely independent base + ## ctrl selection into maskededit framework + self.Bind(wx.EVT_TEXT, self.combobox._OnTextChange ) ## color control appropriately & keep + ## track of previous value for undo + + + +class BaseMaskedComboBox( wx.ComboBox, MaskedEditMixin ): + """ + Base class for generic masked edit comboboxes; allows auto-complete of values. + It is not meant to be instantiated directly, but rather serves as a base class + for any subsequent refinements. + """ + def __init__( self, parent, id=-1, value = '', + pos = wx.DefaultPosition, + size = wx.DefaultSize, + choices = [], + style = wx.CB_DROPDOWN, + validator = wx.DefaultValidator, + name = "maskedComboBox", + setupEventHandling = True, ## setup event handling by default): + **kwargs): + + + kwargs['choices'] = choices ## set up maskededit to work with choice list too + + ## Since combobox completion is case-insensitive, always validate same way + if not kwargs.has_key('compareNoCase'): + kwargs['compareNoCase'] = True + + MaskedEditMixin.__init__( self, name, **kwargs ) + + self._choices = self._ctrl_constraints._choices +## dbg('self._choices:', self._choices) + + if self._ctrl_constraints._alignRight: + choices = [choice.rjust(self._masklength) for choice in choices] + else: + choices = [choice.ljust(self._masklength) for choice in choices] + + wx.ComboBox.__init__(self, parent, id, value='', + pos=pos, size = size, + choices=choices, style=style|wx.WANTS_CHARS, + validator=validator, + name=name) + self.controlInitialized = True + + self._PostInit(style=style, setupEventHandling=setupEventHandling, + name=name, value=value, **kwargs) + + + def _PostInit(self, style=wx.CB_DROPDOWN, + setupEventHandling = True, ## setup event handling by default): + name = "maskedComboBox", value='', **kwargs): + + # This is necessary, because wxComboBox currently provides no + # method for determining later if this was specified in the + # constructor for the control... + self.__readonly = style & wx.CB_READONLY == wx.CB_READONLY + + if not hasattr(self, 'controlInitialized'): + + self.controlInitialized = True ## must have been called via XRC, therefore base class is constructed + if not kwargs.has_key('choices'): + choices=[] + kwargs['choices'] = choices ## set up maskededit to work with choice list too + self._choices = [] + + ## Since combobox completion is case-insensitive, always validate same way + if not kwargs.has_key('compareNoCase'): + kwargs['compareNoCase'] = True + + MaskedEditMixin.__init__( self, name, **kwargs ) + + self._choices = self._ctrl_constraints._choices +## dbg('self._choices:', self._choices) + + if self._ctrl_constraints._alignRight: + choices = [choice.rjust(self._masklength) for choice in choices] + else: + choices = [choice.ljust(self._masklength) for choice in choices] + wx.ComboBox.Clear(self) + wx.ComboBox.AppendItems(self, choices) + + + # Set control font - fixed width by default + self._setFont() + + if self._autofit: + self.SetClientSize(self._CalcSize()) + width = self.GetSize().width + height = self.GetBestSize().height + self.SetInitialSize((width, height)) + + + if value: + # ensure value is width of the mask of the control: + if self._ctrl_constraints._alignRight: + value = value.rjust(self._masklength) + else: + value = value.ljust(self._masklength) + + if self.__readonly: + self.SetStringSelection(value) + else: + self._SetInitialValue(value) + + + self._SetKeycodeHandler(wx.WXK_UP, self._OnSelectChoice) + self._SetKeycodeHandler(wx.WXK_DOWN, self._OnSelectChoice) + + self.replace_next_combobox_event = False + self.correct_selection = -1 + + if setupEventHandling: + ## Setup event handling functions through event handler object, + ## to guarantee processing prior to giving event callbacks from + ## outside the class: + self.evt_handler = MaskedComboBoxEventHandler(self) + self.Bind(wx.EVT_WINDOW_DESTROY, self.OnWindowDestroy ) + + + + def __repr__(self): + return "" % self.GetValue() + + + def OnWindowDestroy(self, event): + # clean up associated event handler object: + if self.RemoveEventHandler(self.evt_handler): + wx.CallAfter(self.evt_handler.Destroy) + event.Skip() + + + def _CalcSize(self, size=None): + """ + Calculate automatic size if allowed; augment base mixin function + to account for the selector button. + """ + size = self._calcSize(size) + return (size[0]+20, size[1]) + + + def SetFont(self, *args, **kwargs): + """ Set the font, then recalculate control size, if appropriate. """ + wx.ComboBox.SetFont(self, *args, **kwargs) + if self._autofit: +## dbg('calculated size:', self._CalcSize()) + self.SetClientSize(self._CalcSize()) + width = self.GetSize().width + height = self.GetBestSize().height +## dbg('setting client size to:', (width, height)) + self.SetInitialSize((width, height)) + + + def _GetSelection(self): + """ + Allow mixin to get the text selection of this control. + REQUIRED by any class derived from MaskedEditMixin. + """ +## dbg('MaskedComboBox::_GetSelection()') + return self.GetMark() + + def _SetSelection(self, sel_start, sel_to): + """ + Allow mixin to set the text selection of this control. + REQUIRED by any class derived from MaskedEditMixin. + """ +## dbg('MaskedComboBox::_SetSelection: setting mark to (%d, %d)' % (sel_start, sel_to)) + return self.SetMark( sel_start, sel_to ) + + + def _GetInsertionPoint(self): +## dbg('MaskedComboBox::_GetInsertionPoint()', indent=1) +## ret = self.GetInsertionPoint() + # work around new bug in 2.5, in which the insertion point + # returned is always at the right side of the selection, + # rather than the start, as is the case with TextCtrl. + ret = self.GetMark()[0] +## dbg('returned', ret, indent=0) + return ret + + def _SetInsertionPoint(self, pos): +## dbg('MaskedComboBox::_SetInsertionPoint(%d)' % pos) + self.SetInsertionPoint(pos) + + + def IsEmpty(*args, **kw): + return MaskedEditMixin.IsEmpty(*args, **kw) + + + def _GetValue(self): + """ + Allow mixin to get the raw value of the control with this function. + REQUIRED by any class derived from MaskedEditMixin. + """ + return self.GetValue() + + def _SetValue(self, value): + """ + Allow mixin to set the raw value of the control with this function. + REQUIRED by any class derived from MaskedEditMixin. + """ + # For wxComboBox, ensure that values are properly padded so that + # if varying length choices are supplied, they always show up + # in the window properly, and will be the appropriate length + # to match the mask: + if self._ctrl_constraints._alignRight: + value = value.rjust(self._masklength) + else: + value = value.ljust(self._masklength) + + # Record current selection and insertion point, for undo + self._prevSelection = self._GetSelection() + self._prevInsertionPoint = self._GetInsertionPoint() +## dbg('MaskedComboBox::_SetValue(%s), selection beforehand: %d' % (value, self.GetSelection())) + wx.ComboBox.SetValue(self, value) +## dbg('MaskedComboBox::_SetValue(%s), selection now: %d' % (value, self.GetSelection())) + # text change events don't always fire, so we check validity here + # to make certain formatting is applied: + self._CheckValid() + + def SetValue(self, value): + """ + This function redefines the externally accessible .SetValue to be + a smart "paste" of the text in question, so as not to corrupt the + masked control. NOTE: this must be done in the class derived + from the base wx control. + """ +## dbg('MaskedComboBox::SetValue(%s)' % value, indent=1) + if not self._mask: + wx.ComboBox.SetValue(value) # revert to base control behavior +## dbg('no mask; deferring to base class', indent=0) + return + # else... + # empty previous contents, replacing entire value: +## dbg('MaskedComboBox::SetValue: selection beforehand: %d' % (self.GetSelection())) + self._SetInsertionPoint(0) + self._SetSelection(0, self._masklength) + + if( len(value) < self._masklength # value shorter than control + and (self._isFloat or self._isInt) # and it's a numeric control + and self._ctrl_constraints._alignRight ): # and it's a right-aligned control + # try to intelligently "pad out" the value to the right size: + value = self._template[0:self._masklength - len(value)] + value +## dbg('padded value = "%s"' % value) + + # For wxComboBox, ensure that values are properly padded so that + # if varying length choices are supplied, they always show up + # in the window properly, and will be the appropriate length + # to match the mask: + elif self._ctrl_constraints._alignRight: + value = value.rjust(self._masklength) + else: + value = value.ljust(self._masklength) + + + # make SetValue behave the same as if you had typed the value in: + try: + value, replace_to = self._Paste(value, raise_on_invalid=True, just_return_value=True) + if self._isFloat: + self._isNeg = False # (clear current assumptions) + value = self._adjustFloat(value) + elif self._isInt: + self._isNeg = False # (clear current assumptions) + value = self._adjustInt(value) + elif self._isDate and not self.IsValid(value) and self._4digityear: + value = self._adjustDate(value, fixcentury=True) + except ValueError: + # If date, year might be 2 digits vs. 4; try adjusting it: + if self._isDate and self._4digityear: + dateparts = value.split(' ') + dateparts[0] = self._adjustDate(dateparts[0], fixcentury=True) + value = string.join(dateparts, ' ') + value = self._Paste(value, raise_on_invalid=True, just_return_value=True) + else: + raise +## dbg('adjusted value: "%s"' % value) + + # Attempt to compensate for fact that calling .SetInsertionPoint() makes the + # selection index -1, even if the resulting set value is in the list. + # So, if we are setting a value that's in the list, use index selection instead. + if value in self._choices: + index = self._choices.index(value) + self._prevValue = self._curValue + self._curValue = self._choices[index] + self._ctrl_constraints._autoCompleteIndex = index + self.SetSelection(index) + else: + self._SetValue(value) +#### dbg('queuing insertion after .SetValue', replace_to) + wx.CallAfter(self._SetInsertionPoint, replace_to) + wx.CallAfter(self._SetSelection, replace_to, replace_to) +## dbg(indent=0) + + + def _Refresh(self): + """ + Allow mixin to refresh the base control with this function. + REQUIRED by any class derived from MaskedEditMixin. + """ + wx.ComboBox.Refresh(self) + + def Refresh(self): + """ + This function redefines the externally accessible .Refresh() to + validate the contents of the masked control as it refreshes. + NOTE: this must be done in the class derived from the base wx control. + """ + self._CheckValid() + self._Refresh() + + + def _IsEditable(self): + """ + Allow mixin to determine if the base control is editable with this function. + REQUIRED by any class derived from MaskedEditMixin. + """ + return not self.__readonly + + + def Cut(self): + """ + This function redefines the externally accessible .Cut to be + a smart "erase" of the text in question, so as not to corrupt the + masked control. NOTE: this must be done in the class derived + from the base wx control. + """ + if self._mask: + self._Cut() # call the mixin's Cut method + else: + wx.ComboBox.Cut(self) # else revert to base control behavior + + + def Paste(self): + """ + This function redefines the externally accessible .Paste to be + a smart "paste" of the text in question, so as not to corrupt the + masked control. NOTE: this must be done in the class derived + from the base wx control. + """ + if self._mask: + self._Paste() # call the mixin's Paste method + else: + wx.ComboBox.Paste(self) # else revert to base control behavior + + + def Undo(self): + """ + This function defines the undo operation for the control. (The default + undo is 1-deep.) + """ + if self._mask: + self._Undo() + else: + wx.ComboBox.Undo() # else revert to base control behavior + + def Append( self, choice, clientData=None ): + """ + This base control function override is necessary so the control can keep track + of any additions to the list of choices, because wx.ComboBox doesn't have an + accessor for the choice list. The code here is the same as in the + SetParameters() mixin function, but is done for the individual value + as appended, so the list can be built incrementally without speed penalty. + """ + if self._mask: + if type(choice) not in (types.StringType, types.UnicodeType): + raise TypeError('%s: choices must be a sequence of strings' % str(self._index)) + elif not self.IsValid(choice): + raise ValueError('%s: "%s" is not a valid value for the control as specified.' % (str(self._index), choice)) + + if not self._ctrl_constraints._choices: + self._ctrl_constraints._compareChoices = [] + self._ctrl_constraints._choices = [] + self._hasList = True + + compareChoice = choice.strip() + + if self._ctrl_constraints._compareNoCase: + compareChoice = compareChoice.lower() + + if self._ctrl_constraints._alignRight: + choice = choice.rjust(self._masklength) + else: + choice = choice.ljust(self._masklength) + if self._ctrl_constraints._fillChar != ' ': + choice = choice.replace(' ', self._fillChar) +## dbg('updated choice:', choice) + + + self._ctrl_constraints._compareChoices.append(compareChoice) + self._ctrl_constraints._choices.append(choice) + self._choices = self._ctrl_constraints._choices # (for shorthand) + + if( not self.IsValid(choice) and + (not self._ctrl_constraints.IsEmpty(choice) or + (self._ctrl_constraints.IsEmpty(choice) and self._ctrl_constraints._validRequired) ) ): + raise ValueError('"%s" is not a valid value for the control "%s" as specified.' % (choice, self.name)) + + wx.ComboBox.Append(self, choice, clientData) + + + def AppendItems( self, choices ): + """ + AppendItems() is handled in terms of Append, to avoid code replication. + """ + for choice in choices: + self.Append(choice) + + + def Clear( self ): + """ + This base control function override is necessary so the derived control can + keep track of any additions to the list of choices, because wx.ComboBox + doesn't have an accessor for the choice list. + """ + if self._mask: + self._choices = [] + self._ctrl_constraints._autoCompleteIndex = -1 + if self._ctrl_constraints._choices: + self.SetCtrlParameters(choices=[]) + wx.ComboBox.Clear(self) + + + def _OnCtrlParametersChanged(self): + """ + This overrides the mixin's default OnCtrlParametersChanged to detect + changes in choice list, so masked.Combobox can update the base control: + """ + if self.controlInitialized and self._choices != self._ctrl_constraints._choices: + wx.ComboBox.Clear(self) + self._choices = self._ctrl_constraints._choices + for choice in self._choices: + wx.ComboBox.Append( self, choice ) + + + # Not all wx platform implementations have .GetMark, so we make the following test, + # and fall back to our old hack if they don't... + # + if not hasattr(wx.ComboBox, 'GetMark'): + def GetMark(self): + """ + This function is a hack to make up for the fact that wx.ComboBox has no + method for returning the selected portion of its edit control. It + works, but has the nasty side effect of generating lots of intermediate + events. + """ +## dbg(suspend=1) # turn off debugging around this function +## dbg('MaskedComboBox::GetMark', indent=1) + if self.__readonly: +## dbg(indent=0) + return 0, 0 # no selection possible for editing +## sel_start, sel_to = wxComboBox.GetMark(self) # what I'd *like* to have! + sel_start = sel_to = self.GetInsertionPoint() +## dbg("current sel_start:", sel_start) + value = self.GetValue() +## dbg('value: "%s"' % value) + + self._ignoreChange = True # tell _OnTextChange() to ignore next event (if any) + + wx.ComboBox.Cut(self) + newvalue = self.GetValue() +## dbg("value after Cut operation:", newvalue) + + if newvalue != value: # something was selected; calculate extent +## dbg("something selected") + sel_to = sel_start + len(value) - len(newvalue) + wx.ComboBox.SetValue(self, value) # restore original value and selection (still ignoring change) + wx.ComboBox.SetInsertionPoint(self, sel_start) + wx.ComboBox.SetMark(self, sel_start, sel_to) + + self._ignoreChange = False # tell _OnTextChange() to pay attn again + +## dbg('computed selection:', sel_start, sel_to, indent=0, suspend=0) + return sel_start, sel_to + else: + def GetMark(self): +## dbg('MaskedComboBox::GetMark()', indent = 1) + ret = wx.ComboBox.GetMark(self) +## dbg('returned', ret, indent=0) + return ret + + + def SetSelection(self, index): + """ + Necessary override for bookkeeping on choice selection, to keep current value + current. + """ +## dbg('MaskedComboBox::SetSelection(%d)' % index, indent=1) + if self._mask: + self._prevValue = self._curValue + self._ctrl_constraints._autoCompleteIndex = index + if index != -1: + self._curValue = self._choices[index] + else: + self._curValue = None + wx.ComboBox.SetSelection(self, index) +## dbg('selection now: %d' % self.GetCurrentSelection(), indent=0) + + + def _OnKeyDownInComboBox(self, event): + """ + This function is necessary because navigation and control key events + do not seem to normally be seen by the wxComboBox's EVT_CHAR routine. + (Tabs don't seem to be visible no matter what, except for CB_READONLY + controls, for some bizarre reason... {:-( ) + """ + key = event.GetKeyCode() +## dbg('MaskedComboBox::OnKeyDownInComboBox(%d)' % key) + if event.GetKeyCode() in self._nav + self._control: + if not self._IsEditable(): + # WANTS_CHARS with CB_READONLY apparently prevents navigation on WXK_TAB; + # ensure we can still navigate properly, as maskededit mixin::OnChar assumes + # that event.Skip() will just work, but it doesn't: + if self._keyhandlers.has_key(key): + self._keyhandlers[key](event) + # else pass + else: +## dbg('calling OnChar()') + self._OnChar(event) + else: + event.Skip() # let mixin default KeyDown behavior occur +## dbg(indent=0) + + + def _OnDropdownSelect(self, event): + """ + This function appears to be necessary because dropdown selection seems to + manipulate the contents of the control in an inconsistent way, properly + changing the selection index, but *not* the value. (!) Calling SetSelection() + on a selection event for the same selection would seem like a nop, but it seems to + fix the problem. + """ +## dbg('MaskedComboBox::OnDropdownSelect(%d)' % event.GetSelection(), indent=1) + if self.replace_next_combobox_event: +## dbg('replacing EVT_COMBOBOX') + self.replace_next_combobox_event = False + self._OnAutoSelect(self._ctrl_constraints, self.correct_selection) + else: +## dbg('skipping EVT_COMBOBOX') + event.Skip() +## dbg(indent=0) + + + def _OnSelectChoice(self, event): + """ + This function appears to be necessary, because the processing done + on the text of the control somehow interferes with the combobox's + selection mechanism for the arrow keys. + """ +## dbg('MaskedComboBox::OnSelectChoice', indent=1) + + if not self._mask: + event.Skip() + return + + value = self.GetValue().strip() + + if self._ctrl_constraints._compareNoCase: + value = value.lower() + + if event.GetKeyCode() == wx.WXK_UP: + direction = -1 + else: + direction = 1 + match_index, partial_match = self._autoComplete( + direction, + self._ctrl_constraints._compareChoices, + value, + self._ctrl_constraints._compareNoCase, + current_index = self._ctrl_constraints._autoCompleteIndex) + if match_index is not None: +## dbg('setting selection to', match_index) + # issue appropriate event to outside: + self._OnAutoSelect(self._ctrl_constraints, match_index=match_index) + self._CheckValid() + keep_processing = False + else: + pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode()) + field = self._FindField(pos) + if self.IsEmpty() or not field._hasList: +## dbg('selecting 1st value in list') + self._OnAutoSelect(self._ctrl_constraints, match_index=0) + self._CheckValid() + keep_processing = False + else: + # attempt field-level auto-complete +## dbg(indent=0) + keep_processing = self._OnAutoCompleteField(event) +## dbg('keep processing?', keep_processing, indent=0) + return keep_processing + + + def _OnAutoSelect(self, field, match_index): + """ + Override mixin (empty) autocomplete handler, so that autocompletion causes + combobox to update appropriately. + """ +## dbg('MaskedComboBox::OnAutoSelect(%d, %d)' % (field._index, match_index), indent=1) +## field._autoCompleteIndex = match_index + if field == self._ctrl_constraints: + self.SetSelection(match_index) +## dbg('issuing combo selection event') + self.GetEventHandler().ProcessEvent( + MaskedComboBoxSelectEvent( self.GetId(), match_index, self ) ) + self._CheckValid() +## dbg('field._autoCompleteIndex:', match_index) +## dbg('self.GetCurrentSelection():', self.GetCurrentSelection()) + end = self._goEnd(getPosOnly=True) +## dbg('scheduling set of end position to:', end) + # work around bug in wx 2.5 + wx.CallAfter(self.SetInsertionPoint, 0) + wx.CallAfter(self.SetInsertionPoint, end) +## dbg(indent=0) + + + def _OnReturn(self, event): + """ + For wx.ComboBox, it seems that if you hit return when the dropdown is + dropped, the event that dismisses the dropdown will also blank the + control, because of the implementation of wxComboBox. So this function + examines the selection and if it is -1, and the value according to + (the base control!) is a value in the list, then it schedules a + programmatic wxComboBox.SetSelection() call to pick the appropriate + item in the list. (and then does the usual OnReturn bit.) + """ +## dbg('MaskedComboBox::OnReturn', indent=1) +## dbg('current value: "%s"' % self.GetValue(), 'current selection:', self.GetCurrentSelection()) + if self.GetCurrentSelection() == -1 and self.GetValue().lower().strip() in self._ctrl_constraints._compareChoices: +## dbg('attempting to correct the selection to make it %d' % self._ctrl_constraints._autoCompleteIndex) +## wx.CallAfter(self.SetSelection, self._ctrl_constraints._autoCompleteIndex) + self.replace_next_combobox_event = True + self.correct_selection = self._ctrl_constraints._autoCompleteIndex + event.m_keyCode = wx.WXK_TAB + event.Skip() +## dbg(indent=0) + + + def _LostFocus(self): +## dbg('MaskedComboBox::LostFocus; Selection=%d, value="%s"' % (self.GetSelection(), self.GetValue())) + if self.GetCurrentSelection() == -1 and self.GetValue().lower().strip() in self._ctrl_constraints._compareChoices: +## dbg('attempting to correct the selection to make it %d' % self._ctrl_constraints._autoCompleteIndex) + wx.CallAfter(self.SetSelection, self._ctrl_constraints._autoCompleteIndex) + + +class ComboBox( BaseMaskedComboBox, MaskedEditAccessorsMixin ): + """ + The "user-visible" masked combobox control, this class is + identical to the BaseMaskedComboBox class it's derived from. + (This extra level of inheritance allows us to add the generic + set of masked edit parameters only to this class while allowing + other classes to derive from the "base" masked combobox control, + and provide a smaller set of valid accessor functions.) + See BaseMaskedComboBox for available methods. + """ + pass + + +class PreMaskedComboBox( BaseMaskedComboBox, MaskedEditAccessorsMixin ): + """ + This class exists to support the use of XRC subclassing. + """ + # This should really be wx.EVT_WINDOW_CREATE but it is not + # currently delivered for native controls on all platforms, so + # we'll use EVT_SIZE instead. It should happen shortly after the + # control is created as the control is set to its "best" size. + _firstEventType = wx.EVT_SIZE + + def __init__(self): + pre = wx.PreComboBox() + self.PostCreate(pre) + self.Bind(self._firstEventType, self.OnCreate) + + + def OnCreate(self, evt): + self.Unbind(self._firstEventType) + self._PostInit() + +__i = 0 +## CHANGELOG: +## ==================== +## Version 1.4 +## 1. Added handler for EVT_COMBOBOX to address apparently inconsistent behavior +## of control when the dropdown control is used to do a selection. +## NOTE: due to misbehavior of wx.ComboBox re: losing all concept of the +## current selection index if SetInsertionPoint() is called, which is required +## to support masked .SetValue(), this control is flaky about retaining selection +## information. I can't truly fix this without major changes to the base control, +## but I've tried to compensate as best I can. +## TODO: investigate replacing base control with ComboCtrl instead... +## 2. Fixed navigation in readonly masked combobox, which was not working because +## the base control doesn't do navigation if style=CB_READONLY|WANTS_CHARS. +## +## +## Version 1.3 +## 1. Made definition of "hack" GetMark conditional on base class not +## implementing it properly, to allow for migration in wx code base +## while taking advantage of improvements therein for some platforms. +## +## Version 1.2 +## 1. Converted docstrings to reST format, added doc for ePyDoc. +## 2. Renamed helper functions, vars etc. not intended to be visible in public +## interface to code. +## +## Version 1.1 +## 1. Added .SetFont() method that properly resizes control +## 2. Modified control to support construction via XRC mechanism. +## 3. Added AppendItems() to conform with latest combobox. diff --git a/wx/lib/masked/ctrl.py b/wx/lib/masked/ctrl.py new file mode 100644 index 00000000..9a55549f --- /dev/null +++ b/wx/lib/masked/ctrl.py @@ -0,0 +1,108 @@ +#---------------------------------------------------------------------------- +# Name: wxPython.lib.masked.ctrl.py +# Author: Will Sadkin +# Created: 09/24/2003 +# Copyright: (c) 2003 by Will Sadkin +# RCS-ID: $Id$ +# License: wxWindows license +#---------------------------------------------------------------------------- +# 12/09/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o Updated for wx namespace (minor) +# +# 12/20/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o Removed wx prefix +# + +""" + +*masked.Ctrl* is actually a factory function for several types of +masked edit controls: + + ================= ========================================================= + masked.TextCtrl standard masked edit text box + masked.ComboBox adds combobox capabilities + masked.IpAddrCtrl adds logical input semantics for IP address entry + masked.TimeCtrl special subclass handling lots of time formats as values + masked.NumCtrl special subclass handling numeric values + ================= ========================================================= + +masked.Ctrl works by looking for a special *controlType* +parameter in the variable arguments of the control, to determine +what kind of instance to return. +controlType can be one of:: + + controlTypes.TEXT + controlTypes.COMBO + controlTypes.IPADDR + controlTypes.TIME + controlTypes.NUMBER + +These constants are also available individually, ie, you can +use either of the following:: + + from wxPython.wx.lib.masked import Ctrl, COMBO, TEXT, NUMBER, TIME + from wxPython.wx.lib.masked import Ctrl, controlTypes + +If not specified as a keyword argument, the default controlType is +controlTypes.TEXT. + +Each of the above classes has its own unique arguments, but Masked.Ctrl +provides a single "unified" interface for masked controls. + + +""" + +from wx.lib.masked import TextCtrl, ComboBox, IpAddrCtrl +from wx.lib.masked import NumCtrl +from wx.lib.masked import TimeCtrl + + +# "type" enumeration for class instance factory function +TEXT = 0 +COMBO = 1 +IPADDR = 2 +TIME = 3 +NUMBER = 4 + +# for ease of import +class controlTypes: + TEXT = TEXT + COMBO = COMBO + IPADDR = IPADDR + TIME = TIME + NUMBER = NUMBER + + +def Ctrl( *args, **kwargs): + """ + Actually a factory function providing a unifying + interface for generating masked controls. + """ + if not kwargs.has_key('controlType'): + controlType = TEXT + else: + controlType = kwargs['controlType'] + del kwargs['controlType'] + + if controlType == TEXT: + return TextCtrl(*args, **kwargs) + + elif controlType == COMBO: + return ComboBox(*args, **kwargs) + + elif controlType == IPADDR: + return IpAddrCtrl(*args, **kwargs) + + elif controlType == TIME: + return TimeCtrl(*args, **kwargs) + + elif controlType == NUMBER: + return NumCtrl(*args, **kwargs) + + else: + raise AttributeError( + "invalid controlType specified: %s" % repr(controlType)) + + diff --git a/wx/lib/masked/ipaddrctrl.py b/wx/lib/masked/ipaddrctrl.py new file mode 100644 index 00000000..086b60e7 --- /dev/null +++ b/wx/lib/masked/ipaddrctrl.py @@ -0,0 +1,220 @@ +#---------------------------------------------------------------------------- +# Name: masked.ipaddrctrl.py +# Authors: Will Sadkin +# Email: wsadkin@nameconnector.com +# Created: 02/11/2003 +# Copyright: (c) 2003 by Will Sadkin, 2003 +# RCS-ID: $Id$ +# License: wxWidgets license +#---------------------------------------------------------------------------- +# NOTE: +# Masked.IpAddrCtrl is a minor modification to masked.TextCtrl, that is +# specifically tailored for entering IP addresses. It allows for +# right-insert fields and provides an accessor to obtain the entered +# address with extra whitespace removed. +# +#---------------------------------------------------------------------------- +""" +Provides a smart text input control that understands the structure and +limits of IP Addresses, and allows automatic field navigation as the +user hits '.' when typing. +""" + +import wx, types, string +from wx.lib.masked import BaseMaskedTextCtrl + +# jmg 12/9/03 - when we cut ties with Py 2.2 and earlier, this would +# be a good place to implement the 2.3 logger class +from wx.tools.dbg import Logger +##dbg = Logger() +##dbg(enable=0) + +class IpAddrCtrlAccessorsMixin: + """ + Defines IpAddrCtrl's list of attributes having their own + Get/Set functions, exposing only those that make sense for + an IP address control. + """ + + exposed_basectrl_params = ( + 'fields', + 'retainFieldValidation', + 'formatcodes', + 'fillChar', + 'defaultValue', + 'description', + + 'useFixedWidthFont', + 'signedForegroundColour', + 'emptyBackgroundColour', + 'validBackgroundColour', + 'invalidBackgroundColour', + + 'emptyInvalid', + 'validFunc', + 'validRequired', + ) + + for param in exposed_basectrl_params: + propname = param[0].upper() + param[1:] + exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) + exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) + + if param.find('Colour') != -1: + # add non-british spellings, for backward-compatibility + propname.replace('Colour', 'Color') + + exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) + exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) + + +class IpAddrCtrl( BaseMaskedTextCtrl, IpAddrCtrlAccessorsMixin ): + """ + This class is a particular type of MaskedTextCtrl that accepts + and understands the semantics of IP addresses, reformats input + as you move from field to field, and accepts '.' as a navigation + character, so that typing an IP address can be done naturally. + """ + + + + def __init__( self, parent, id=-1, value = '', + pos = wx.DefaultPosition, + size = wx.DefaultSize, + style = wx.TE_PROCESS_TAB, + validator = wx.DefaultValidator, + name = 'IpAddrCtrl', + setupEventHandling = True, ## setup event handling by default + **kwargs): + + if not kwargs.has_key('mask'): + kwargs['mask'] = mask = "###.###.###.###" + if not kwargs.has_key('formatcodes'): + kwargs['formatcodes'] = 'F_Sr<>' + if not kwargs.has_key('validRegex'): + kwargs['validRegex'] = "( \d| \d\d|(1\d\d|2[0-4]\d|25[0-5]))(\.( \d| \d\d|(1\d\d|2[0-4]\d|25[0-5]))){3}" + + + BaseMaskedTextCtrl.__init__( + self, parent, id=id, value = value, + pos=pos, size=size, + style = style, + validator = validator, + name = name, + setupEventHandling = setupEventHandling, + **kwargs) + + + # set up individual field parameters as well: + field_params = {} + field_params['validRegex'] = "( | \d| \d |\d | \d\d|\d\d |\d \d|(1\d\d|2[0-4]\d|25[0-5]))" + + # require "valid" string; this prevents entry of any value > 255, but allows + # intermediate constructions; overall control validation requires well-formatted value. + field_params['formatcodes'] = 'V' + + if field_params: + for i in self._field_indices: + self.SetFieldParameters(i, **field_params) + + # This makes '.' act like tab: + self._AddNavKey('.', handler=self.OnDot) + self._AddNavKey('>', handler=self.OnDot) # for "shift-." + + + def OnDot(self, event): + """ + Defines what action to take when the '.' character is typed in the + control. By default, the current field is right-justified, and the + cursor is placed in the next field. + """ +## dbg('IpAddrCtrl::OnDot', indent=1) + pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode()) + oldvalue = self.GetValue() + edit_start, edit_end, slice = self._FindFieldExtent(pos, getslice=True) + if not event.ShiftDown(): + if pos > edit_start and pos < edit_end: + # clip data in field to the right of pos, if adjusting fields + # when not at delimeter; (assumption == they hit '.') + newvalue = oldvalue[:pos] + ' ' * (edit_end - pos) + oldvalue[edit_end:] + self._SetValue(newvalue) + self._SetInsertionPoint(pos) +## dbg(indent=0) + return self._OnChangeField(event) + + + + def GetAddress(self): + """ + Returns the control value, with any spaces removed. + """ + value = BaseMaskedTextCtrl.GetValue(self) + return value.replace(' ','') # remove spaces from the value + + + def _OnCtrl_S(self, event): +## dbg("IpAddrCtrl::_OnCtrl_S") + if self._demo: + print "value:", self.GetAddress() + return False + + def SetValue(self, value): + """ + Takes a string value, validates it for a valid IP address, + splits it into an array of 4 fields, justifies it + appropriately, and inserts it into the control. + Invalid values will raise a ValueError exception. + """ +## dbg('IpAddrCtrl::SetValue(%s)' % str(value), indent=1) + if type(value) not in (types.StringType, types.UnicodeType): +## dbg(indent=0) + raise ValueError('%s must be a string', str(value)) + + bValid = True # assume True + parts = value.split('.') + + if len(parts) != 4: + bValid = False + else: + for i in range(4): + part = parts[i] + if not 0 <= len(part) <= 3: + bValid = False + break + elif part.strip(): # non-empty part + try: + j = string.atoi(part) + if not 0 <= j <= 255: + bValid = False + break + else: + parts[i] = '%3d' % j + except: + bValid = False + break + else: + # allow empty sections for SetValue (will result in "invalid" value, + # but this may be useful for initializing the control: + parts[i] = ' ' # convert empty field to 3-char length + + if not bValid: +## dbg(indent=0) + raise ValueError('value (%s) must be a string of form n.n.n.n where n is empty or in range 0-255' % str(value)) + else: +## dbg('parts:', parts) + value = string.join(parts, '.') + BaseMaskedTextCtrl.SetValue(self, value) +## dbg(indent=0) + +__i=0 +## CHANGELOG: +## ==================== +## Version 1.2 +## 1. Fixed bugs involving missing imports now that these classes are in +## their own module. +## 2. Added doc strings for ePyDoc. +## 3. Renamed helper functions, vars etc. not intended to be visible in public +## interface to code. +## +## Version 1.1 +## Made ipaddrctrls allow right-insert in subfields, now that insert/cut/paste works better diff --git a/wx/lib/masked/maskededit.py b/wx/lib/masked/maskededit.py new file mode 100644 index 00000000..8cb02ea7 --- /dev/null +++ b/wx/lib/masked/maskededit.py @@ -0,0 +1,7241 @@ +#---------------------------------------------------------------------------- +# Name: maskededit.py +# Authors: Will Sadkin, Jeff Childers +# Email: wsadkin@parlancecorp.com, jchilders_98@yahoo.com +# Created: 02/11/2003 +# Copyright: (c) 2003 by Jeff Childers, Will Sadkin, 2003 +# Portions: (c) 2002 by Will Sadkin, 2002-2007 +# RCS-ID: $Id$ +# License: wxWidgets license +#---------------------------------------------------------------------------- +# NOTE: +# MaskedEdit controls are based on a suggestion made on [wxPython-Users] by +# Jason Hihn, and borrows liberally from Will Sadkin's original masked edit +# control for time entry, TimeCtrl (which is now rewritten using this +# control!). +# +# MaskedEdit controls do not normally use validators, because they do +# careful manipulation of the cursor in the text window on each keystroke, +# and validation is cursor-position specific, so the control intercepts the +# key codes before the validator would fire. However, validators can be +# provided to do data transfer to the controls. +# +#---------------------------------------------------------------------------- +# +# This file now contains the bulk of the logic behind all masked controls, +# the MaskedEditMixin class, the Field class, and the autoformat codes. +# +#---------------------------------------------------------------------------- +# +# 03/30/2004 - Will Sadkin (wsadkin@parlancecorp.com) +# +# o Split out TextCtrl, ComboBox and IpAddrCtrl into their own files, +# o Reorganized code into masked package +# +# 12/09/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o Updated for wx namespace. No guarantees. This is one huge file. +# +# 12/13/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o Missed wx.DateTime stuff earlier. +# +# 12/20/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o MaskedEditMixin -> MaskedEditMixin +# o wxMaskedTextCtrl -> maskedTextCtrl +# o wxMaskedComboBoxSelectEvent -> MaskedComboBoxSelectEvent +# o wxMaskedComboBox -> MaskedComboBox +# o wxIpAddrCtrl -> IpAddrCtrl +# o wxTimeCtrl -> TimeCtrl +# + +__doc__ = """\ +contains MaskedEditMixin class that drives all the other masked controls. + +==================== +Masked Edit Overview +==================== + +masked.TextCtrl: + is a sublassed text control that can carefully control the user's input + based on a mask string you provide. + + General usage example:: + + control = masked.TextCtrl( win, -1, '', mask = '(###) ###-####') + + The example above will create a text control that allows only numbers to be + entered and then only in the positions indicated in the mask by the # sign. + +masked.ComboBox: + is a similar subclass of wxComboBox that allows the same sort of masking, + but also can do auto-complete of values, and can require the value typed + to be in the list of choices to be colored appropriately. + +masked.Ctrl: + is actually a factory function for several types of masked edit controls: + + ================= ================================================== + masked.TextCtrl standard masked edit text box + masked.ComboBox adds combobox capabilities + masked.IpAddrCtrl adds special semantics for IP address entry + masked.TimeCtrl special subclass handling lots of types as values + masked.NumCtrl special subclass handling numeric values + ================= ================================================== + + It works by looking for a *controlType* parameter in the keyword + arguments of the control, to determine what kind of instance to return. + If not specified as a keyword argument, the default control type returned + will be masked.TextCtrl. + + Each of the above classes has its own set of arguments, but masked.Ctrl + provides a single "unified" interface for masked controls. + +What follows is a description of how to configure the generic masked.TextCtrl +and masked.ComboBox; masked.NumCtrl and masked.TimeCtrl have their own demo +pages and interface descriptions. + +========================= + +Initialization Parameters +------------------------- +mask + Allowed mask characters and function: + + ========= ========================================================== + Character Function + ========= ========================================================== + # Allow numeric only (0-9) + N Allow letters and numbers (0-9) + A Allow uppercase letters only + a Allow lowercase letters only + C Allow any letter, upper or lower + X Allow string.letters, string.punctuation, string.digits + & Allow string.punctuation only (doesn't include all unicode symbols) + \* Allow any visible character + | explicit field boundary (takes no space in the control; allows mix + of adjacent mask characters to be treated as separate fields, + eg: '&|###' means "field 0 = '&', field 1 = '###'", but there's + no fixed characters in between. + ========= ========================================================== + + + These controls define these sets of characters using string.letters, + string.uppercase, etc. These sets are affected by the system locale + setting, so in order to have the masked controls accept characters + that are specific to your users' language, your application should + set the locale. + For example, to allow international characters to be used in the + above masks, you can place the following in your code as part of + your application's initialization code:: + + import locale + locale.setlocale(locale.LC_ALL, '') + + The controls now also support (by popular demand) all "visible" characters, + by use of the * mask character, including unicode characters above + the standard ANSI keycode range. + Note: As string.punctuation doesn't typically include all unicode + symbols, you will have to use includechars to get some of these into + otherwise restricted positions in your control, such as those specified + with &. + + Using these mask characters, a variety of template masks can be built. See + the demo for some other common examples include date+time, social security + number, etc. If any of these characters are needed as template rather + than mask characters, they can be escaped with \, ie. \N means "literal N". + (use \\ for literal backslash, as in: r'CCC\\NNN'.) + + + .. note:: + + Masks containing only # characters and one optional decimal point + character are handled specially, as "numeric" controls. Such + controls have special handling for typing the '-' key, handling + the "decimal point" character as truncating the integer portion, + optionally allowing grouping characters and so forth. + There are several parameters and format codes that only make sense + when combined with such masks, eg. groupChar, decimalChar, and so + forth (see below). These allow you to construct reasonable + numeric entry controls. + + + .. note:: + + Changing the mask for a control deletes any previous field classes + (and any associated validation or formatting constraints) for them. + + +useFixedWidthFont + By default, masked edit controls use a fixed width font, so that + the mask characters are fixed within the control, regardless of + subsequent modifications to the value. Set to False if having + the control font be the same as other controls is required. (This is + a control-level parameter.) + +defaultEncoding + (Applies to unicode systems only) By default, the default unicode encoding + used is latin1, or iso-8859-1. If necessary, you can set this control-level + parameter to govern the codec used to decode your keyboard inputs. + (This is a control-level parameter.) + +formatcodes + These other properties can be passed to the class when instantiating it: + Formatcodes are specified as a string of single character formatting + codes that modify behavior of the control:: + + _ Allow spaces + ! Force upper + ^ Force lower + R Right-align field(s) + r Right-insert in field(s) (implies R) + < Stay in field until explicit navigation out of it + + > Allow insert/delete within partially filled fields (as + opposed to the default "overwrite" mode for fixed-width + masked edit controls.) This allows single-field controls + or each field within a multi-field control to optionally + behave more like standard text controls. + (See EMAIL or phone number autoformat examples.) + + *Note: This also governs whether backspace/delete operations + shift contents of field to right of cursor, or just blank the + erased section. + + Also, when combined with 'r', this indicates that the field + or control allows right insert anywhere within the current + non-empty value in the field. (Otherwise right-insert behavior + is only performed to when the entire right-insertable field is + selected or the cursor is at the right edge of the field.* + + + , Allow grouping character in integer fields of numeric controls + and auto-group/regroup digits (if the result fits) when leaving + such a field. (If specified, .SetValue() will attempt to + auto-group as well.) + ',' is also the default grouping character. To change the + grouping character and/or decimal character, use the groupChar + and decimalChar parameters, respectively. + Note: typing the "decimal point" character in such fields will + clip the value to that left of the cursor for integer + fields of controls with "integer" or "floating point" masks. + If the ',' format code is specified, this will also cause the + resulting digits to be regrouped properly, using the current + grouping character. + - Prepend and reserve leading space for sign to mask and allow + signed values (negative #s shown in red by default.) Can be + used with argument useParensForNegatives (see below.) + 0 integer fields get leading zeros + D Date[/time] field + T Time field + F Auto-Fit: the control calulates its size from + the length of the template mask + V validate entered chars against validRegex before allowing them + to be entered vs. being allowed by basic mask and then having + the resulting value just colored as invalid. + (See USSTATE autoformat demo for how this can be used.) + S select entire field when navigating to new field + +fillChar + +defaultValue + These controls have two options for the initial state of the control. + If a blank control with just the non-editable characters showing + is desired, simply leave the constructor variable fillChar as its + default (' '). If you want some other character there, simply + change the fillChar to that value. Note: changing the control's fillChar + will implicitly reset all of the fields' fillChars to this value. + + If you need different default characters in each mask position, + you can specify a defaultValue parameter in the constructor, or + set them for each field individually. + This value must satisfy the non-editable characters of the mask, + but need not conform to the replaceable characters. + +groupChar + +decimalChar + These parameters govern what character is used to group numbers + and is used to indicate the decimal point for numeric format controls. + The default groupChar is ',', the default decimalChar is '.' + By changing these, you can customize the presentation of numbers + for your location. + + Eg:: + + formatcodes = ',', groupChar='\'' allows 12'345.34 + formatcodes = ',', groupChar='.', decimalChar=',' allows 12.345,34 + + (These are control-level parameters.) + +shiftDecimalChar + The default "shiftDecimalChar" (used for "backwards-tabbing" until + shift-tab is fixed in wxPython) is '>' (for QUERTY keyboards.) for + other keyboards, you may want to customize this, eg '?' for shift ',' on + AZERTY keyboards, ':' or ';' for other European keyboards, etc. + (This is a control-level parameter.) + +useParensForNegatives=False + This option can be used with signed numeric format controls to + indicate signs via () rather than '-'. + (This is a control-level parameter.) + +autoSelect=False + This option can be used to have a field or the control try to + auto-complete on each keystroke if choices have been specified. + +autoCompleteKeycodes=[] + By default, DownArrow, PageUp and PageDown will auto-complete a + partially entered field. Shift-DownArrow, Shift-UpArrow, PageUp + and PageDown will also auto-complete, but if the field already + contains a matched value, these keys will cycle through the list + of choices forward or backward as appropriate. Shift-Up and + Shift-Down also take you to the next/previous field after any + auto-complete action. + + Additional auto-complete keys can be specified via this parameter. + Any keys so specified will act like PageDown. + (This is a control-level parameter.) + + + +Validating User Input +===================== +There are a variety of initialization parameters that are used to validate +user input. These parameters can apply to the control as a whole, and/or +to individual fields: + +======================== ================================================================== +excludeChars A string of characters to exclude even if otherwise allowed +includeChars A string of characters to allow even if otherwise disallowed +validRegex Use a regular expression to validate the contents of the text box +validRange Pass a rangeas list (low,high) to limit numeric fields/values +choices A list of strings that are allowed choices for the control. +choiceRequired value must be member of choices list +compareNoCase Perform case-insensitive matching when validating against list. *Note: for masked.ComboBox, this defaults to True.* +emptyInvalid Boolean indicating whether an empty value should be considered invalid +validFunc A function to call of the form: bool = func(candidate_value) which will return True if the candidate_value satisfies some external criteria for the control in addition to the the other validation, or False if not. (This validation is applied last in the chain of validations.) +validRequired Boolean indicating whether or not keys that are allowed by the mask, but result in an invalid value are allowed to be entered into the control. Setting this to True implies that a valid default value is set for the control. +retainFieldValidation False by default; if True, this allows individual fields to retain their own validation constraints independently of any subsequent changes to the control's overall parameters. (This is a control-level parameter.) +validator Validators are not normally needed for masked controls, because of the nature of the validation and control of input. However, you can supply one to provide data transfer routines for the controls. +raiseOnInvalidPaste False by default; normally a bad paste simply is ignored with a bell; if True, this will cause a ValueError exception to be thrown, with the .value attribute of the exception containing the bad value. +stopFieldChangeIfInvalid False by default; tries to prevent navigation out of a field if its current value is invalid. Can be used to create a hybrid of validation settings, allowing intermediate invalid values in a field without sacrificing ability to limit values as with validRequired. NOTE: It is possible to end up with an invalid value when using this option if focus is switched to some other control via mousing. To avoid this, consider deriving a class that defines _LostFocus() function that returns the control to a valid value when the focus shifts. (AFAICT, The change in focus is unpreventable.) +======================== ================================================================== + + +Coloring Behavior +================= + +The following parameters have been provided to allow you to change the default +coloring behavior of the control. These can be set at construction, or via +the .SetCtrlParameters() function. Pass a color as string e.g. 'Yellow': + +======================== ======================================================================= +emptyBackgroundColour Control Background color when identified as empty. Default=White +invalidBackgroundColour Control Background color when identified as Not valid. Default=Yellow +validBackgroundColour Control Background color when identified as Valid. Default=white +======================== ======================================================================= + + +The following parameters control the default foreground color coloring behavior of the +control. Pass a color as string e.g. 'Yellow': + +======================== ====================================================================== +foregroundColour Control foreground color when value is not negative. Default=Black +signedForegroundColour Control foreground color when value is negative. Default=Red +======================== ====================================================================== + + +Fields +====== + +Each part of the mask that allows user input is considered a field. The fields +are represented by their own class instances. You can specify field-specific +constraints by constructing or accessing the field instances for the control +and then specifying those constraints via parameters. + +fields + This parameter allows you to specify Field instances containing + constraints for the individual fields of a control, eg: local + choice lists, validation rules, functions, regexps, etc. + It can be either an ordered list or a dictionary. If a list, + the fields will be applied as fields 0, 1, 2, etc. + If a dictionary, it should be keyed by field index. + the values should be a instances of maskededit.Field. + + Any field not represented by the list or dictionary will be + implicitly created by the control. + + Eg:: + + fields = [ Field(formatcodes='_r'), Field('choices=['a', 'b', 'c']) ] + + Or:: + + fields = { + 1: ( Field(formatcodes='_R', choices=['a', 'b', 'c']), + 3: ( Field(choices=['01', '02', '03'], choiceRequired=True) + } + + The following parameters are available for individual fields, with the + same semantics as for the whole control but applied to the field in question: + + ============== ============================================================================= + fillChar if set for a field, it will override the control's fillChar for that field + groupChar if set for a field, it will override the control's default + defaultValue sets field-specific default value; overrides any default from control + compareNoCase overrides control's settings + emptyInvalid determines whether field is required to be filled at all times + validRequired if set, requires field to contain valid value + ============== ============================================================================= + + If any of the above parameters are subsequently specified for the control as a + whole, that new value will be propagated to each field, unless the + retainFieldValidation control-level parameter is set. + + ============== ============================== + formatcodes Augments control's settings + excludeChars ' ' ' + includeChars ' ' ' + validRegex ' ' ' + validRange ' ' ' + choices ' ' ' + choiceRequired ' ' ' + validFunc ' ' ' + ============== ============================== + + + +Control Class Functions +======================= + +.GetPlainValue(value=None) + Returns the value specified (or the control's text value + not specified) without the formatting text. + In the example above, might return phone no='3522640075', + whereas control.GetValue() would return '(352) 264-0075' +.ClearValue() + Returns the control's value to its default, and places the + cursor at the beginning of the control. +.SetValue() + Does "smart replacement" of passed value into the control, as does + the .Paste() method. As with other text entry controls, the + .SetValue() text replacement begins at left-edge of the control, + with missing mask characters inserted as appropriate. + .SetValue will also adjust integer, float or date mask entry values, + adding commas, auto-completing years, etc. as appropriate. + For "right-aligned" numeric controls, it will also now automatically + right-adjust any value whose length is less than the width of the + control before attempting to set the value. + If a value does not follow the format of the control's mask, or will + not fit into the control, a ValueError exception will be raised. + + Eg:: + + mask = '(###) ###-####' + .SetValue('1234567890') => '(123) 456-7890' + .SetValue('(123)4567890') => '(123) 456-7890' + .SetValue('(123)456-7890') => '(123) 456-7890' + .SetValue('123/4567-890') => illegal paste; ValueError + + mask = '#{6}.#{2}', formatcodes = '_,-', + .SetValue('111') => ' 111 . ' + .SetValue(' %9.2f' % -111.12345 ) => ' -111.12' + .SetValue(' %9.2f' % 1234.00 ) => ' 1,234.00' + .SetValue(' %9.2f' % -1234567.12345 ) => insufficient room; ValueError + + mask = '#{6}.#{2}', formatcodes = '_,-R' # will right-adjust value for right-aligned control + .SetValue('111') => padded value misalignment ValueError: " 111" will not fit + .SetValue('%.2f' % 111 ) => ' 111.00' + .SetValue('%.2f' % -111.12345 ) => ' -111.12' + + +.IsValid(value=None) + Returns True if the value specified (or the value of the control + if not specified) passes validation tests +.IsEmpty(value=None) + Returns True if the value specified (or the value of the control + if not specified) is equal to an "empty value," ie. all + editable characters == the fillChar for their respective fields. +.IsDefault(value=None) + Returns True if the value specified (or the value of the control + if not specified) is equal to the initial value of the control. + +.Refresh() + Recolors the control as appropriate to its current settings. + +.SetCtrlParameters(\*\*kwargs) + This function allows you to set up and/or change the control parameters + after construction; it takes a list of key/value pairs as arguments, + where the keys can be any of the mask-specific parameters in the constructor. + + Eg:: + + ctl = masked.TextCtrl( self, -1 ) + ctl.SetCtrlParameters( mask='###-####', + defaultValue='555-1212', + formatcodes='F') + +.GetCtrlParameter(parametername) + This function allows you to retrieve the current value of a parameter + from the control. + + *Note:* Each of the control parameters can also be set using its + own Set and Get function. These functions follow a regular form: + All of the parameter names start with lower case; for their + corresponding Set/Get function, the parameter name is capitalized. + + Eg:: + + ctl.SetMask('###-####') + ctl.SetDefaultValue('555-1212') + ctl.GetChoiceRequired() + ctl.GetFormatcodes() + + *Note:* After any change in parameters, the choices for the + control are reevaluated to ensure that they are still legal. If you + have large choice lists, it is therefore more efficient to set parameters + before setting the choices available. + +.SetFieldParameters(field_index, \*\*kwargs) + This function allows you to specify change individual field + parameters after construction. (Indices are 0-based.) + +.GetFieldParameter(field_index, parametername) + Allows the retrieval of field parameters after construction + + +The control detects certain common constructions. In order to use the signed feature +(negative numbers and coloring), the mask has to be all numbers with optionally one +decimal point. Without a decimal (e.g. '######', the control will treat it as an integer +value. With a decimal (e.g. '###.##'), the control will act as a floating point control +(i.e. press decimal to 'tab' to the decimal position). Pressing decimal in the +integer control truncates the value. However, for a true numeric control, +masked.NumCtrl provides all this, and true numeric input/output support as well. + + +Check your controls by calling each control's .IsValid() function and the +.IsEmpty() function to determine which controls have been a) filled in and +b) filled in properly. + + +Regular expression validations can be used flexibly and creatively. +Take a look at the demo; the zip-code validation succeeds as long as the +first five numerals are entered. the last four are optional, but if +any are entered, there must be 4 to be valid. + +masked.Ctrl Configuration +========================= +masked.Ctrl works by looking for a special *controlType* +parameter in the variable arguments of the control, to determine +what kind of instance to return. +controlType can be one of:: + + controlTypes.TEXT + controlTypes.COMBO + controlTypes.IPADDR + controlTypes.TIME + controlTypes.NUMBER + +These constants are also available individually, ie, you can +use either of the following:: + + from wx.lib.masked import MaskedCtrl, controlTypes + from wx.lib.masked import MaskedCtrl, COMBO, TEXT, NUMBER, IPADDR + +If not specified as a keyword argument, the default controlType is +controlTypes.TEXT. + +""" + +""" ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +DEVELOPER COMMENTS: + +Naming Conventions +------------------ + All methods of the Mixin that are not meant to be exposed to the external + interface are prefaced with '_'. Those functions that are primarily + intended to be internal subroutines subsequently start with a lower-case + letter; those that are primarily intended to be used and/or overridden + by derived subclasses start with a capital letter. + + The following methods must be used and/or defined when deriving a control + from MaskedEditMixin. NOTE: if deriving from a *masked edit* control + (eg. class IpAddrCtrl(masked.TextCtrl) ), then this is NOT necessary, + as it's already been done for you in the base class. + + ._SetInitialValue() + This function must be called after the associated base + control has been initialized in the subclass __init__ + function. It sets the initial value of the control, + either to the value specified if non-empty, the + default value if specified, or the "template" for + the empty control as necessary. It will also set/reset + the font if necessary and apply formatting to the + control at this time. + + ._GetSelection() + REQUIRED + Each class derived from MaskedEditMixin must define + the function for getting the start and end of the + current text selection. The reason for this is + that not all controls have the same function name for + doing this; eg. wx.TextCtrl uses .GetSelection(), + whereas we had to write a .GetMark() function for + wxComboBox, because .GetSelection() for the control + gets the currently selected list item from the combo + box, and the control doesn't (yet) natively provide + a means of determining the text selection. + ._SetSelection() + REQUIRED + Similarly to _GetSelection, each class derived from + MaskedEditMixin must define the function for setting + the start and end of the current text selection. + (eg. .SetSelection() for masked.TextCtrl, and .SetMark() for + masked.ComboBox. + + ._GetInsertionPoint() + ._SetInsertionPoint() + REQUIRED + For consistency, and because the mixin shouldn't rely + on fixed names for any manipulations it does of any of + the base controls, we require each class derived from + MaskedEditMixin to define these functions as well. + + ._GetValue() + ._SetValue() REQUIRED + Each class derived from MaskedEditMixin must define + the functions used to get and set the raw value of the + control. + This is necessary so that recursion doesn't take place + when setting the value, and so that the mixin can + call the appropriate function after doing all its + validation and manipulation without knowing what kind + of base control it was mixed in with. To handle undo + functionality, the ._SetValue() must record the current + selection prior to setting the value. + + .Cut() + .Paste() + .Undo() + .SetValue() REQUIRED + Each class derived from MaskedEditMixin must redefine + these functions to call the _Cut(), _Paste(), _Undo() + and _SetValue() methods, respectively for the control, + so as to prevent programmatic corruption of the control's + value. This must be done in each derivation, as the + mixin cannot itself override a member of a sibling class. + + ._Refresh() REQUIRED + Each class derived from MaskedEditMixin must define + the function used to refresh the base control. + + .Refresh() REQUIRED + Each class derived from MaskedEditMixin must redefine + this function so that it checks the validity of the + control (via self._CheckValid) and then refreshes + control using the base class method. + + ._IsEditable() REQUIRED + Each class derived from MaskedEditMixin must define + the function used to determine if the base control is + editable or not. (For masked.ComboBox, this has to + be done with code, rather than specifying the proper + function in the base control, as there isn't one...) + ._CalcSize() REQUIRED + Each class derived from MaskedEditMixin must define + the function used to determine how wide the control + should be given the mask. (The mixin function + ._calcSize() provides a baseline estimate.) + + +Event Handling +-------------- + Event handlers are "chained", and MaskedEditMixin usually + swallows most of the events it sees, thereby preventing any other + handlers from firing in the chain. It is therefore required that + each class derivation using the mixin to have an option to hook up + the event handlers itself or forego this operation and let a + subclass of the masked control do so. For this reason, each + subclass should probably include the following code:: + + if setupEventHandling: + ## Setup event handlers + EVT_SET_FOCUS( self, self._OnFocus ) ## defeat automatic full selection + EVT_KILL_FOCUS( self, self._OnKillFocus ) ## run internal validator + EVT_LEFT_DCLICK(self, self._OnDoubleClick) ## select field under cursor on dclick + EVT_RIGHT_UP(self, self._OnContextMenu ) ## bring up an appropriate context menu + EVT_KEY_DOWN( self, self._OnKeyDown ) ## capture control events not normally seen, eg ctrl-tab. + EVT_CHAR( self, self._OnChar ) ## handle each keypress + EVT_TEXT( self, self.GetId(), self._OnTextChange ) ## color control appropriately & keep + ## track of previous value for undo + + where setupEventHandling is an argument to its constructor. + + These 5 handlers must be "wired up" for the masked edit + controls to provide default behavior. (The setupEventHandling + is an argument to masked.TextCtrl and masked.ComboBox, so + that controls derived from *them* may replace one of these + handlers if they so choose.) + + If your derived control wants to preprocess events before + taking action, it should then set up the event handling itself, + so it can be first in the event handler chain. + + + The following routines are available to facilitate changing + the default behavior of masked edit controls: + + ._SetKeycodeHandler(keycode, func) + ._SetKeyHandler(char, func) + Use to replace default handling for any given keycode. + func should take the key event as argument and return + False if no further action is required to handle the + key. Eg: + self._SetKeycodeHandler(WXK_UP, self.IncrementValue) + self._SetKeyHandler('-', self._OnChangeSign) + + (Setting a func of None removes any keyhandler for the given key.) + + "Navigation" keys are assumed to change the cursor position, and + therefore don't cause automatic motion of the cursor as insertable + characters do. + + ._AddNavKeycode(keycode, handler=None) + ._AddNavKey(char, handler=None) + Allows controls to specify other keys (and optional handlers) + to be treated as navigational characters. (eg. '.' in IpAddrCtrl) + + ._GetNavKeycodes() Returns the current list of navigational keycodes. + + ._SetNavKeycodes(key_func_tuples) + Allows replacement of the current list of keycode + processed as navigation keys, and bind associated + optional keyhandlers. argument is a list of key/handler + tuples. Passing a value of None for the handler in a + given tuple indicates that default processing for the key + is desired. + + ._FindField(pos) Returns the Field object associated with this position + in the control. + + ._FindFieldExtent(pos, getslice=False, value=None) + Returns edit_start, edit_end of the field corresponding + to the specified position within the control, and + optionally also returns the current contents of that field. + If value is specified, it will retrieve the slice the corresponding + slice from that value, rather than the current value of the + control. + + ._AdjustField(pos) + This is, the function that gets called for a given position + whenever the cursor is adjusted to leave a given field. + By default, it adjusts the year in date fields if mask is a date, + It can be overridden by a derived class to + adjust the value of the control at that time. + (eg. IpAddrCtrl reformats the address in this way.) + + ._Change() Called by internal EVT_TEXT handler. Return False to force + skip of the normal class change event. + ._Keypress(key) Called by internal EVT_CHAR handler. Return False to force + skip of the normal class keypress event. + ._LostFocus() Called by internal EVT_KILL_FOCUS handler + + ._OnKeyDown(event) + This is the default EVT_KEY_DOWN routine; it just checks for + "navigation keys", and if event.ControlDown(), it fires the + mixin's _OnChar() routine, as such events are not always seen + by the "cooked" EVT_CHAR routine. + + ._OnChar(event) This is the main EVT_CHAR handler for the + MaskedEditMixin. + + The following routines are used to handle standard actions + for control keys: + _OnArrow(event) used for arrow navigation events + _OnCtrl_A(event) 'select all' + _OnCtrl_C(event) 'copy' (uses base control function, as copy is non-destructive) + _OnCtrl_S(event) 'save' (does nothing) + _OnCtrl_V(event) 'paste' - calls _Paste() method, to do smart paste + _OnCtrl_X(event) 'cut' - calls _Cut() method, to "erase" selection + _OnCtrl_Z(event) 'undo' - resets value to previous value (if any) + + _OnChangeField(event) primarily used for tab events, but can be + used for other keys (eg. '.' in IpAddrCtrl) + + _OnErase(event) used for backspace and delete + _OnHome(event) + _OnEnd(event) + + The following routine provides a hook back to any class derivations, so that + they can react to parameter changes before any value is set/reset as a result of + those changes. (eg. masked.ComboBox needs to detect when the choices list is + modified, either implicitly or explicitly, so it can reset the base control + to have the appropriate choice list *before* the initial value is reset to match.) + + _OnCtrlParametersChanged() + +Accessor Functions +------------------ + For convenience, each class derived from MaskedEditMixin should + define an accessors mixin, so that it exposes only those parameters + that make sense for the derivation. This is done with an intermediate + level of inheritance, ie: + + class BaseMaskedTextCtrl( TextCtrl, MaskedEditMixin ): + + class TextCtrl( BaseMaskedTextCtrl, MaskedEditAccessorsMixin ): + class ComboBox( BaseMaskedComboBox, MaskedEditAccessorsMixin ): + class NumCtrl( BaseMaskedTextCtrl, MaskedNumCtrlAccessorsMixin ): + class IpAddrCtrl( BaseMaskedTextCtrl, IpAddrCtrlAccessorsMixin ): + class TimeCtrl( BaseMaskedTextCtrl, TimeCtrlAccessorsMixin ): + + etc. + + Each accessors mixin defines Get/Set functions for the base class parameters + that are appropriate for that derivation. + This allows the base classes to be "more generic," exposing the widest + set of options, while not requiring derived classes to be so general. +""" + +import copy +import difflib +import re +import string +import types + +import wx + +# jmg 12/9/03 - when we cut ties with Py 2.2 and earlier, this would +# be a good place to implement the 2.3 logger class +from wx.tools.dbg import Logger + +##dbg = Logger() +##dbg(enable=1) + +## ---------- ---------- ---------- ---------- ---------- ---------- ---------- + +## Constants for identifying control keys and classes of keys: + +WXK_CTRL_A = (ord('A')+1) - ord('A') ## These keys are not already defined in wx +WXK_CTRL_C = (ord('C')+1) - ord('A') +WXK_CTRL_S = (ord('S')+1) - ord('A') +WXK_CTRL_V = (ord('V')+1) - ord('A') +WXK_CTRL_X = (ord('X')+1) - ord('A') +WXK_CTRL_Z = (ord('Z')+1) - ord('A') + +nav = ( + wx.WXK_BACK, wx.WXK_LEFT, wx.WXK_RIGHT, wx.WXK_UP, wx.WXK_DOWN, wx.WXK_TAB, + wx.WXK_HOME, wx.WXK_END, wx.WXK_RETURN, wx.WXK_PRIOR, wx.WXK_NEXT, + wx.WXK_NUMPAD_LEFT, wx.WXK_NUMPAD_RIGHT, wx.WXK_NUMPAD_UP, wx.WXK_NUMPAD_DOWN, + wx.WXK_NUMPAD_HOME, wx.WXK_NUMPAD_END, wx.WXK_NUMPAD_ENTER, wx.WXK_NUMPAD_PRIOR, wx.WXK_NUMPAD_NEXT + ) + +control = ( + wx.WXK_BACK, wx.WXK_DELETE, wx.WXK_INSERT, + wx.WXK_NUMPAD_DELETE, wx.WXK_NUMPAD_INSERT, + WXK_CTRL_A, WXK_CTRL_C, WXK_CTRL_S, WXK_CTRL_V, + WXK_CTRL_X, WXK_CTRL_Z + ) + +# Because unicode can go over the ansi character range, we need to explicitly test +# for all non-visible keystrokes, rather than just assuming a particular range for +# visible characters: +wx_control_keycodes = range(32) + list(nav) + list(control) + [ + wx.WXK_START, wx.WXK_LBUTTON, wx.WXK_RBUTTON, wx.WXK_CANCEL, wx.WXK_MBUTTON, + wx.WXK_CLEAR, wx.WXK_SHIFT, wx.WXK_CONTROL, wx.WXK_MENU, wx.WXK_PAUSE, + wx.WXK_CAPITAL, wx.WXK_SELECT, wx.WXK_PRINT, wx.WXK_EXECUTE, wx.WXK_SNAPSHOT, + wx.WXK_HELP, wx.WXK_NUMPAD0, wx.WXK_NUMPAD1, wx.WXK_NUMPAD2, wx.WXK_NUMPAD3, + wx.WXK_NUMPAD4, wx.WXK_NUMPAD5, wx.WXK_NUMPAD6, wx.WXK_NUMPAD7, wx.WXK_NUMPAD8, + wx.WXK_NUMPAD9, wx.WXK_MULTIPLY, wx.WXK_ADD, wx.WXK_SEPARATOR, wx.WXK_SUBTRACT, + wx.WXK_DECIMAL, wx.WXK_DIVIDE, wx.WXK_F1, wx.WXK_F2, wx.WXK_F3, wx.WXK_F4, + wx.WXK_F5, wx.WXK_F6, wx.WXK_F7, wx.WXK_F8, wx.WXK_F9, wx.WXK_F10, wx.WXK_F11, + wx.WXK_F12, wx.WXK_F13, wx.WXK_F14, wx.WXK_F15, wx.WXK_F16, wx.WXK_F17, + wx.WXK_F18, wx.WXK_F19, wx.WXK_F20, wx.WXK_F21, wx.WXK_F22, wx.WXK_F23, + wx.WXK_F24, wx.WXK_NUMLOCK, wx.WXK_SCROLL, wx.WXK_PAGEUP, wx.WXK_PAGEDOWN, + wx.WXK_NUMPAD_SPACE, wx.WXK_NUMPAD_TAB, wx.WXK_NUMPAD_ENTER, wx.WXK_NUMPAD_F1, + wx.WXK_NUMPAD_F2, wx.WXK_NUMPAD_F3, wx.WXK_NUMPAD_F4, wx.WXK_NUMPAD_HOME, + wx.WXK_NUMPAD_LEFT, wx.WXK_NUMPAD_UP, wx.WXK_NUMPAD_RIGHT, wx.WXK_NUMPAD_DOWN, + wx.WXK_NUMPAD_PRIOR, wx.WXK_NUMPAD_PAGEUP, wx.WXK_NUMPAD_NEXT, wx.WXK_NUMPAD_PAGEDOWN, + wx.WXK_NUMPAD_END, wx.WXK_NUMPAD_BEGIN, wx.WXK_NUMPAD_INSERT, wx.WXK_NUMPAD_DELETE, + wx.WXK_NUMPAD_EQUAL, wx.WXK_NUMPAD_MULTIPLY, wx.WXK_NUMPAD_ADD, wx.WXK_NUMPAD_SEPARATOR, + wx.WXK_NUMPAD_SUBTRACT, wx.WXK_NUMPAD_DECIMAL, wx.WXK_NUMPAD_DIVIDE, wx.WXK_WINDOWS_LEFT, + wx.WXK_WINDOWS_RIGHT, wx.WXK_WINDOWS_MENU, wx.WXK_COMMAND, + # Hardware-specific buttons + wx.WXK_SPECIAL1, wx.WXK_SPECIAL2, wx.WXK_SPECIAL3, wx.WXK_SPECIAL4, wx.WXK_SPECIAL5, + wx.WXK_SPECIAL6, wx.WXK_SPECIAL7, wx.WXK_SPECIAL8, wx.WXK_SPECIAL9, wx.WXK_SPECIAL10, + wx.WXK_SPECIAL11, wx.WXK_SPECIAL12, wx.WXK_SPECIAL13, wx.WXK_SPECIAL14, wx.WXK_SPECIAL15, + wx.WXK_SPECIAL16, wx.WXK_SPECIAL17, wx.WXK_SPECIAL18, wx.WXK_SPECIAL19, wx.WXK_SPECIAL20 + ] + + +## ---------- ---------- ---------- ---------- ---------- ---------- ---------- + +## Constants for masking. This is where mask characters +## are defined. +## maskchars used to identify valid mask characters from all others +## # - allow numeric 0-9 only +## A - allow uppercase only. Combine with forceupper to force lowercase to upper +## a - allow lowercase only. Combine with forcelower to force upper to lowercase +## C - allow any letter, upper or lower +## X - allow string.letters, string.punctuation, string.digits +## & - allow string.punctuation only (doesn't include all unicode symbols) +## * - allow any visible character + +## Note: locale settings affect what "uppercase", lowercase, etc comprise. +## Note: '|' is not a maskchar, in that it is a mask processing directive, and so +## does not appear here. +## +maskchars = ("#","A","a","X","C","N",'*','&') +ansichars = "" +for i in xrange(32, 256): + ansichars += chr(i) + +months = '(01|02|03|04|05|06|07|08|09|10|11|12)' +charmonths = '(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec|JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)' +charmonths_dict = {'jan': 1, 'feb': 2, 'mar': 3, 'apr': 4, 'may': 5, 'jun': 6, + 'jul': 7, 'aug': 8, 'sep': 9, 'oct': 10, 'nov': 11, 'dec': 12} + +days = '(01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31)' +hours = '(0\d| \d|1[012])' +milhours = '(00|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|16|17|18|19|20|21|22|23)' +minutes = """(00|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|\ +16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32|33|34|35|\ +36|37|38|39|40|41|42|43|44|45|46|47|48|49|50|51|52|53|54|55|\ +56|57|58|59)""" +seconds = minutes +am_pm_exclude = 'BCDEFGHIJKLMNOQRSTUVWXYZ\x8a\x8c\x8e\x9f\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd8\xd9\xda\xdb\xdc\xdd\xde' + +states = "AL,AK,AZ,AR,CA,CO,CT,DE,DC,FL,GA,GU,HI,ID,IL,IN,IA,KS,KY,LA,MA,ME,MD,MI,MN,MS,MO,MT,NE,NV,NH,NJ,NM,NY,NC,ND,OH,OK,OR,PA,PR,RI,SC,SD,TN,TX,UT,VA,VT,VI,WA,WV,WI,WY".split(',') + +state_names = ['Alabama','Alaska','Arizona','Arkansas', + 'California','Colorado','Connecticut', + 'Delaware','District of Columbia', + 'Florida','Georgia','Hawaii', + 'Idaho','Illinois','Indiana','Iowa', + 'Kansas','Kentucky','Louisiana', + 'Maine','Maryland','Massachusetts','Michigan', + 'Minnesota','Mississippi','Missouri','Montana', + 'Nebraska','Nevada','New Hampshire','New Jersey', + 'New Mexico','New York','North Carolina','North Dakokta', + 'Ohio','Oklahoma','Oregon', + 'Pennsylvania','Puerto Rico','Rhode Island', + 'South Carolina','South Dakota', + 'Tennessee','Texas','Utah', + 'Vermont','Virginia', + 'Washington','West Virginia', + 'Wisconsin','Wyoming'] + +## ---------- ---------- ---------- ---------- ---------- ---------- ---------- + +## The following dictionary defines the current set of autoformats: + +masktags = { + "USPHONEFULLEXT": { + 'mask': "(###) ###-#### x:###", + 'formatcodes': 'F^->', + 'validRegex': "^\(\d{3}\) \d{3}-\d{4}", + 'description': "Phone Number w/opt. ext" + }, + "USPHONETIGHTEXT": { + 'mask': "###-###-#### x:###", + 'formatcodes': 'F^->', + 'validRegex': "^\d{3}-\d{3}-\d{4}", + 'description': "Phone Number\n (w/hyphens and opt. ext)" + }, + "USPHONEFULL": { + 'mask': "(###) ###-####", + 'formatcodes': 'F^->', + 'validRegex': "^\(\d{3}\) \d{3}-\d{4}", + 'description': "Phone Number only" + }, + "USPHONETIGHT": { + 'mask': "###-###-####", + 'formatcodes': 'F^->', + 'validRegex': "^\d{3}-\d{3}-\d{4}", + 'description': "Phone Number\n(w/hyphens)" + }, + "USSTATE": { + 'mask': "AA", + 'formatcodes': 'F!V', + 'validRegex': "([ACDFGHIKLMNOPRSTUVW] |%s)" % string.join(states,'|'), + 'choices': states, + 'choiceRequired': True, + 'description': "US State Code" + }, + "USSTATENAME": { + 'mask': "ACCCCCCCCCCCCCCCCCCC", + 'formatcodes': 'F_', + 'validRegex': "([ACDFGHIKLMNOPRSTUVW] |%s)" % string.join(state_names,'|'), + 'choices': state_names, + 'choiceRequired': True, + 'description': "US State Name" + }, + + "USDATETIMEMMDDYYYY/HHMMSS": { + 'mask': "##/##/#### ##:##:## AM", + 'excludeChars': am_pm_exclude, + 'formatcodes': 'DF!', + 'validRegex': '^' + months + '/' + days + '/' + '\d{4} ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M', + 'description': "US Date + Time" + }, + "USDATETIMEMMDDYYYY-HHMMSS": { + 'mask': "##-##-#### ##:##:## AM", + 'excludeChars': am_pm_exclude, + 'formatcodes': 'DF!', + 'validRegex': '^' + months + '-' + days + '-' + '\d{4} ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M', + 'description': "US Date + Time\n(w/hypens)" + }, + "USDATE24HRTIMEMMDDYYYY/HHMMSS": { + 'mask': "##/##/#### ##:##:##", + 'formatcodes': 'DF', + 'validRegex': '^' + months + '/' + days + '/' + '\d{4} ' + milhours + ':' + minutes + ':' + seconds, + 'description': "US Date + 24Hr (Military) Time" + }, + "USDATE24HRTIMEMMDDYYYY-HHMMSS": { + 'mask': "##-##-#### ##:##:##", + 'formatcodes': 'DF', + 'validRegex': '^' + months + '-' + days + '-' + '\d{4} ' + milhours + ':' + minutes + ':' + seconds, + 'description': "US Date + 24Hr Time\n(w/hypens)" + }, + "USDATETIMEMMDDYYYY/HHMM": { + 'mask': "##/##/#### ##:## AM", + 'excludeChars': am_pm_exclude, + 'formatcodes': 'DF!', + 'validRegex': '^' + months + '/' + days + '/' + '\d{4} ' + hours + ':' + minutes + ' (A|P)M', + 'description': "US Date + Time\n(without seconds)" + }, + "USDATE24HRTIMEMMDDYYYY/HHMM": { + 'mask': "##/##/#### ##:##", + 'formatcodes': 'DF', + 'validRegex': '^' + months + '/' + days + '/' + '\d{4} ' + milhours + ':' + minutes, + 'description': "US Date + 24Hr Time\n(without seconds)" + }, + "USDATETIMEMMDDYYYY-HHMM": { + 'mask': "##-##-#### ##:## AM", + 'excludeChars': am_pm_exclude, + 'formatcodes': 'DF!', + 'validRegex': '^' + months + '-' + days + '-' + '\d{4} ' + hours + ':' + minutes + ' (A|P)M', + 'description': "US Date + Time\n(w/hypens and w/o secs)" + }, + "USDATE24HRTIMEMMDDYYYY-HHMM": { + 'mask': "##-##-#### ##:##", + 'formatcodes': 'DF', + 'validRegex': '^' + months + '-' + days + '-' + '\d{4} ' + milhours + ':' + minutes, + 'description': "US Date + 24Hr Time\n(w/hyphens and w/o seconds)" + }, + "USDATEMMDDYYYY/": { + 'mask': "##/##/####", + 'formatcodes': 'DF', + 'validRegex': '^' + months + '/' + days + '/' + '\d{4}', + 'description': "US Date\n(MMDDYYYY)" + }, + "USDATEMMDDYY/": { + 'mask': "##/##/##", + 'formatcodes': 'DF', + 'validRegex': '^' + months + '/' + days + '/\d\d', + 'description': "US Date\n(MMDDYY)" + }, + "USDATEMMDDYYYY-": { + 'mask': "##-##-####", + 'formatcodes': 'DF', + 'validRegex': '^' + months + '-' + days + '-' +'\d{4}', + 'description': "MM-DD-YYYY" + }, + + "EUDATEYYYYMMDD/": { + 'mask': "####/##/##", + 'formatcodes': 'DF', + 'validRegex': '^' + '\d{4}'+ '/' + months + '/' + days, + 'description': "YYYY/MM/DD" + }, + "EUDATEYYYYMMDD.": { + 'mask': "####.##.##", + 'formatcodes': 'DF', + 'validRegex': '^' + '\d{4}'+ '.' + months + '.' + days, + 'description': "YYYY.MM.DD" + }, + "EUDATEDDMMYYYY/": { + 'mask': "##/##/####", + 'formatcodes': 'DF', + 'validRegex': '^' + days + '/' + months + '/' + '\d{4}', + 'description': "DD/MM/YYYY" + }, + "EUDATEDDMMYYYY.": { + 'mask': "##.##.####", + 'formatcodes': 'DF', + 'validRegex': '^' + days + '.' + months + '.' + '\d{4}', + 'description': "DD.MM.YYYY" + }, + "EUDATEDDMMMYYYY.": { + 'mask': "##.CCC.####", + 'formatcodes': 'DF', + 'validRegex': '^' + days + '.' + charmonths + '.' + '\d{4}', + 'description': "DD.Month.YYYY" + }, + "EUDATEDDMMMYYYY/": { + 'mask': "##/CCC/####", + 'formatcodes': 'DF', + 'validRegex': '^' + days + '/' + charmonths + '/' + '\d{4}', + 'description': "DD/Month/YYYY" + }, + + "EUDATETIMEYYYYMMDD/HHMMSS": { + 'mask': "####/##/## ##:##:## AM", + 'excludeChars': am_pm_exclude, + 'formatcodes': 'DF!', + 'validRegex': '^' + '\d{4}'+ '/' + months + '/' + days + ' ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M', + 'description': "YYYY/MM/DD HH:MM:SS" + }, + "EUDATETIMEYYYYMMDD.HHMMSS": { + 'mask': "####.##.## ##:##:## AM", + 'excludeChars': am_pm_exclude, + 'formatcodes': 'DF!', + 'validRegex': '^' + '\d{4}'+ '.' + months + '.' + days + ' ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M', + 'description': "YYYY.MM.DD HH:MM:SS" + }, + "EUDATETIMEDDMMYYYY/HHMMSS": { + 'mask': "##/##/#### ##:##:## AM", + 'excludeChars': am_pm_exclude, + 'formatcodes': 'DF!', + 'validRegex': '^' + days + '/' + months + '/' + '\d{4} ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M', + 'description': "DD/MM/YYYY HH:MM:SS" + }, + "EUDATETIMEDDMMYYYY.HHMMSS": { + 'mask': "##.##.#### ##:##:## AM", + 'excludeChars': am_pm_exclude, + 'formatcodes': 'DF!', + 'validRegex': '^' + days + '.' + months + '.' + '\d{4} ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M', + 'description': "DD.MM.YYYY HH:MM:SS" + }, + + "EUDATETIMEYYYYMMDD/HHMM": { + 'mask': "####/##/## ##:## AM", + 'excludeChars': am_pm_exclude, + 'formatcodes': 'DF!', + 'validRegex': '^' + '\d{4}'+ '/' + months + '/' + days + ' ' + hours + ':' + minutes + ' (A|P)M', + 'description': "YYYY/MM/DD HH:MM" + }, + "EUDATETIMEYYYYMMDD.HHMM": { + 'mask': "####.##.## ##:## AM", + 'excludeChars': am_pm_exclude, + 'formatcodes': 'DF!', + 'validRegex': '^' + '\d{4}'+ '.' + months + '.' + days + ' ' + hours + ':' + minutes + ' (A|P)M', + 'description': "YYYY.MM.DD HH:MM" + }, + "EUDATETIMEDDMMYYYY/HHMM": { + 'mask': "##/##/#### ##:## AM", + 'excludeChars': am_pm_exclude, + 'formatcodes': 'DF!', + 'validRegex': '^' + days + '/' + months + '/' + '\d{4} ' + hours + ':' + minutes + ' (A|P)M', + 'description': "DD/MM/YYYY HH:MM" + }, + "EUDATETIMEDDMMYYYY.HHMM": { + 'mask': "##.##.#### ##:## AM", + 'excludeChars': am_pm_exclude, + 'formatcodes': 'DF!', + 'validRegex': '^' + days + '.' + months + '.' + '\d{4} ' + hours + ':' + minutes + ' (A|P)M', + 'description': "DD.MM.YYYY HH:MM" + }, + + "EUDATE24HRTIMEYYYYMMDD/HHMMSS": { + 'mask': "####/##/## ##:##:##", + 'formatcodes': 'DF', + 'validRegex': '^' + '\d{4}'+ '/' + months + '/' + days + ' ' + milhours + ':' + minutes + ':' + seconds, + 'description': "YYYY/MM/DD 24Hr Time" + }, + "EUDATE24HRTIMEYYYYMMDD.HHMMSS": { + 'mask': "####.##.## ##:##:##", + 'formatcodes': 'DF', + 'validRegex': '^' + '\d{4}'+ '.' + months + '.' + days + ' ' + milhours + ':' + minutes + ':' + seconds, + 'description': "YYYY.MM.DD 24Hr Time" + }, + "EUDATE24HRTIMEDDMMYYYY/HHMMSS": { + 'mask': "##/##/#### ##:##:##", + 'formatcodes': 'DF', + 'validRegex': '^' + days + '/' + months + '/' + '\d{4} ' + milhours + ':' + minutes + ':' + seconds, + 'description': "DD/MM/YYYY 24Hr Time" + }, + "EUDATE24HRTIMEDDMMYYYY.HHMMSS": { + 'mask': "##.##.#### ##:##:##", + 'formatcodes': 'DF', + 'validRegex': '^' + days + '.' + months + '.' + '\d{4} ' + milhours + ':' + minutes + ':' + seconds, + 'description': "DD.MM.YYYY 24Hr Time" + }, + "EUDATE24HRTIMEYYYYMMDD/HHMM": { + 'mask': "####/##/## ##:##", + 'formatcodes': 'DF','validRegex': '^' + '\d{4}'+ '/' + months + '/' + days + ' ' + milhours + ':' + minutes, + 'description': "YYYY/MM/DD 24Hr Time\n(w/o seconds)" + }, + "EUDATE24HRTIMEYYYYMMDD.HHMM": { + 'mask': "####.##.## ##:##", + 'formatcodes': 'DF', + 'validRegex': '^' + '\d{4}'+ '.' + months + '.' + days + ' ' + milhours + ':' + minutes, + 'description': "YYYY.MM.DD 24Hr Time\n(w/o seconds)" + }, + "EUDATE24HRTIMEDDMMYYYY/HHMM": { + 'mask': "##/##/#### ##:##", + 'formatcodes': 'DF', + 'validRegex': '^' + days + '/' + months + '/' + '\d{4} ' + milhours + ':' + minutes, + 'description': "DD/MM/YYYY 24Hr Time\n(w/o seconds)" + }, + "EUDATE24HRTIMEDDMMYYYY.HHMM": { + 'mask': "##.##.#### ##:##", + 'formatcodes': 'DF', + 'validRegex': '^' + days + '.' + months + '.' + '\d{4} ' + milhours + ':' + minutes, + 'description': "DD.MM.YYYY 24Hr Time\n(w/o seconds)" + }, + + "TIMEHHMMSS": { + 'mask': "##:##:## AM", + 'excludeChars': am_pm_exclude, + 'formatcodes': 'TF!', + 'validRegex': '^' + hours + ':' + minutes + ':' + seconds + ' (A|P)M', + 'description': "HH:MM:SS (A|P)M\n(see TimeCtrl)" + }, + "TIMEHHMM": { + 'mask': "##:## AM", + 'excludeChars': am_pm_exclude, + 'formatcodes': 'TF!', + 'validRegex': '^' + hours + ':' + minutes + ' (A|P)M', + 'description': "HH:MM (A|P)M\n(see TimeCtrl)" + }, + "24HRTIMEHHMMSS": { + 'mask': "##:##:##", + 'formatcodes': 'TF', + 'validRegex': '^' + milhours + ':' + minutes + ':' + seconds, + 'description': "24Hr HH:MM:SS\n(see TimeCtrl)" + }, + "24HRTIMEHHMM": { + 'mask': "##:##", + 'formatcodes': 'TF', + 'validRegex': '^' + milhours + ':' + minutes, + 'description': "24Hr HH:MM\n(see TimeCtrl)" + }, + "USSOCIALSEC": { + 'mask': "###-##-####", + 'formatcodes': 'F', + 'validRegex': "\d{3}-\d{2}-\d{4}", + 'description': "Social Sec#" + }, + "CREDITCARD": { + 'mask': "####-####-####-####", + 'formatcodes': 'F', + 'validRegex': "\d{4}-\d{4}-\d{4}-\d{4}", + 'description': "Credit Card" + }, + "EXPDATEMMYY": { + 'mask': "##/##", + 'formatcodes': "F", + 'validRegex': "^" + months + "/\d\d", + 'description': "Expiration MM/YY" + }, + "USZIP": { + 'mask': "#####", + 'formatcodes': 'F', + 'validRegex': "^\d{5}", + 'description': "US 5-digit zip code" + }, + "USZIPPLUS4": { + 'mask': "#####-####", + 'formatcodes': 'F', + 'validRegex': "\d{5}-(\s{4}|\d{4})", + 'description': "US zip+4 code" + }, + "PERCENT": { + 'mask': "0.##", + 'formatcodes': 'F', + 'validRegex': "^0.\d\d", + 'description': "Percentage" + }, + "AGE": { + 'mask': "###", + 'formatcodes': "F", + 'validRegex': "^[1-9]{1} |[1-9][0-9] |1[0|1|2][0-9]", + 'description': "Age" + }, + "EMAIL": { + 'mask': "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + 'excludeChars': " \\/*&%$#!+='\"", + 'formatcodes': "F>", + 'validRegex': "^\w+([\-\.]\w+)*@((([a-zA-Z0-9]+(\-[a-zA-Z0-9]+)*\.)+)[a-zA-Z]{2,4}|\[(\d|\d\d|(1\d\d|2[0-4]\d|25[0-5]))(\.(\d|\d\d|(1\d\d|2[0-4]\d|25[0-5]))){3}\]) *$", + 'description': "Email address" + }, + "IPADDR": { + 'mask': "###.###.###.###", + 'formatcodes': 'F_Sr', + 'validRegex': "( \d| \d\d|(1\d\d|2[0-4]\d|25[0-5]))(\.( \d| \d\d|(1\d\d|2[0-4]\d|25[0-5]))){3}", + 'description': "IP Address\n(see IpAddrCtrl)" + } + } + +# build demo-friendly dictionary of descriptions of autoformats +autoformats = [] +for key, value in masktags.items(): + autoformats.append((key, value['description'])) +autoformats.sort() + +## ---------- ---------- ---------- ---------- ---------- ---------- ---------- + +class Field: + """ + This class manages the individual fields in a masked edit control. + Each field has a zero-based index, indicating its position in the + control, an extent, an associated mask, and a plethora of optional + parameters. Fields can be instantiated and then associated with + parent masked controls, in order to provide field-specific configuration. + Alternatively, fields will be implicitly created by the parent control + if not provided at construction, at which point, the fields can then + manipulated by the controls .SetFieldParameters() method. + """ + valid_params = { + 'index': None, ## which field of mask; set by parent control. + 'mask': "", ## mask chars for this field + 'extent': (), ## (edit start, edit_end) of field; set by parent control. + 'formatcodes': "", ## codes indicating formatting options for the control + 'fillChar': ' ', ## used as initial value for each mask position if initial value is not given + 'groupChar': ',', ## used with numeric fields; indicates what char groups 3-tuple digits + 'decimalChar': '.', ## used with numeric fields; indicates what char separates integer from fraction + 'shiftDecimalChar': '>', ## used with numeric fields, indicates what is above the decimal point char on keyboard + 'useParensForNegatives': False, ## used with numeric fields, indicates that () should be used vs. - to show negative numbers. + 'defaultValue': "", ## use if you want different positional defaults vs. all the same fillChar + 'excludeChars': "", ## optional string of chars to exclude even if main mask type does + 'includeChars': "", ## optional string of chars to allow even if main mask type doesn't + 'validRegex': "", ## optional regular expression to use to validate the control + 'validRange': (), ## Optional hi-low range for numerics + 'choices': [], ## Optional list for character expressions + 'choiceRequired': False, ## If choices supplied this specifies if valid value must be in the list + 'compareNoCase': False, ## Optional flag to indicate whether or not to use case-insensitive list search + 'autoSelect': False, ## Set to True to try auto-completion on each keystroke: + 'validFunc': None, ## Optional function for defining additional, possibly dynamic validation constraints on contrl + 'validRequired': False, ## Set to True to disallow input that results in an invalid value + 'emptyInvalid': False, ## Set to True to make EMPTY = INVALID + 'description': "", ## primarily for autoformats, but could be useful elsewhere + 'raiseOnInvalidPaste': False, ## if True, paste into field will cause ValueError + 'stopFieldChangeIfInvalid': False,## if True, disallow field navigation out of invalid field + } + + # This list contains all parameters that when set at the control level should + # propagate down to each field: + propagating_params = ('fillChar', 'groupChar', 'decimalChar','useParensForNegatives', + 'compareNoCase', 'emptyInvalid', 'validRequired', 'raiseOnInvalidPaste', + 'stopFieldChangeIfInvalid') + + def __init__(self, **kwargs): + """ + This is the "constructor" for setting up parameters for fields. + a field_index of -1 is used to indicate "the entire control." + """ +#### dbg('Field::Field', indent=1) + # Validate legitimate set of parameters: + for key in kwargs.keys(): + if key not in Field.valid_params.keys(): +#### dbg(indent=0) + ae = AttributeError('invalid parameter "%s"' % (key)) + ae.attribute = key + raise ae + + # Set defaults for each parameter for this instance, and fully + # populate initial parameter list for configuration: + for key, value in Field.valid_params.items(): + setattr(self, '_' + key, copy.copy(value)) + if not kwargs.has_key(key): + kwargs[key] = copy.copy(value) + + self._autoCompleteIndex = -1 + self._SetParameters(**kwargs) + self._ValidateParameters(**kwargs) + +#### dbg(indent=0) + + + def _SetParameters(self, **kwargs): + """ + This function can be used to set individual or multiple parameters for + a masked edit field parameter after construction. + """ +## dbg(suspend=1) +## dbg('maskededit.Field::_SetParameters', indent=1) + # Validate keyword arguments: + for key in kwargs.keys(): + if key not in Field.valid_params.keys(): +## dbg(indent=0, suspend=0) + ae = AttributeError('invalid keyword argument "%s"' % key) + ae.attribute = key + raise ae + +## if self._index is not None: dbg('field index:', self._index) +## dbg('parameters:', indent=1) + for key, value in kwargs.items(): +## dbg('%s:' % key, value) + pass +## dbg(indent=0) + + + old_fillChar = self._fillChar # store so we can change choice lists accordingly if it changes + + # First, Assign all parameters specified: + for key in Field.valid_params.keys(): + if kwargs.has_key(key): + setattr(self, '_' + key, kwargs[key] ) + + if kwargs.has_key('formatcodes'): # (set/changed) + self._forceupper = '!' in self._formatcodes + self._forcelower = '^' in self._formatcodes + self._groupdigits = ',' in self._formatcodes + self._okSpaces = '_' in self._formatcodes + self._padZero = '0' in self._formatcodes + self._autofit = 'F' in self._formatcodes + self._insertRight = 'r' in self._formatcodes + self._allowInsert = '>' in self._formatcodes + self._alignRight = 'R' in self._formatcodes or 'r' in self._formatcodes + self._moveOnFieldFull = not '<' in self._formatcodes + self._selectOnFieldEntry = 'S' in self._formatcodes + + if kwargs.has_key('groupChar'): + self._groupChar = kwargs['groupChar'] + if kwargs.has_key('decimalChar'): + self._decimalChar = kwargs['decimalChar'] + if kwargs.has_key('shiftDecimalChar'): + self._shiftDecimalChar = kwargs['shiftDecimalChar'] + + if kwargs.has_key('formatcodes') or kwargs.has_key('validRegex'): + self._regexMask = 'V' in self._formatcodes and self._validRegex + + if kwargs.has_key('fillChar'): + self._old_fillChar = old_fillChar +#### dbg("self._old_fillChar: '%s'" % self._old_fillChar) + + if kwargs.has_key('mask') or kwargs.has_key('validRegex'): # (set/changed) + self._isInt = _isInteger(self._mask) +## dbg('isInt?', self._isInt, 'self._mask:"%s"' % self._mask) + +## dbg(indent=0, suspend=0) + + + def _ValidateParameters(self, **kwargs): + """ + This function can be used to validate individual or multiple parameters for + a masked edit field parameter after construction. + """ +## dbg(suspend=1) +## dbg('maskededit.Field::_ValidateParameters', indent=1) +## if self._index is not None: dbg('field index:', self._index) +#### dbg('parameters:', indent=1) +## for key, value in kwargs.items(): +#### dbg('%s:' % key, value) +#### dbg(indent=0) +#### dbg("self._old_fillChar: '%s'" % self._old_fillChar) + + # Verify proper numeric format params: + if self._groupdigits and self._groupChar == self._decimalChar: +## dbg(indent=0, suspend=0) + ae = AttributeError("groupChar '%s' cannot be the same as decimalChar '%s'" % (self._groupChar, self._decimalChar)) + ae.attribute = self._groupChar + raise ae + + + # Now go do validation, semantic and inter-dependency parameter processing: + if kwargs.has_key('choices') or kwargs.has_key('compareNoCase') or kwargs.has_key('choiceRequired'): # (set/changed) + + self._compareChoices = [choice.strip() for choice in self._choices] + + if self._compareNoCase and self._choices: + self._compareChoices = [item.lower() for item in self._compareChoices] + + if kwargs.has_key('choices'): + self._autoCompleteIndex = -1 + + + if kwargs.has_key('validRegex'): # (set/changed) + if self._validRegex: + try: + if self._compareNoCase: + self._filter = re.compile(self._validRegex, re.IGNORECASE) + else: + self._filter = re.compile(self._validRegex) + except: +## dbg(indent=0, suspend=0) + raise TypeError('%s: validRegex "%s" not a legal regular expression' % (str(self._index), self._validRegex)) + else: + self._filter = None + + if kwargs.has_key('validRange'): # (set/changed) + self._hasRange = False + self._rangeHigh = 0 + self._rangeLow = 0 + if self._validRange: + if type(self._validRange) != types.TupleType or len( self._validRange )!= 2 or self._validRange[0] > self._validRange[1]: +## dbg(indent=0, suspend=0) + raise TypeError('%s: validRange %s parameter must be tuple of form (a,b) where a <= b' + % (str(self._index), repr(self._validRange)) ) + + self._hasRange = True + self._rangeLow = self._validRange[0] + self._rangeHigh = self._validRange[1] + + if kwargs.has_key('choices') or (len(self._choices) and len(self._choices[0]) != len(self._mask)): # (set/changed) + self._hasList = False + if self._choices and type(self._choices) not in (types.TupleType, types.ListType): +## dbg(indent=0, suspend=0) + raise TypeError('%s: choices must be a sequence of strings' % str(self._index)) + elif len( self._choices) > 0: + for choice in self._choices: + if type(choice) not in (types.StringType, types.UnicodeType): +## dbg(indent=0, suspend=0) + raise TypeError('%s: choices must be a sequence of strings' % str(self._index)) + + length = len(self._mask) +## dbg('len(%s)' % self._mask, length, 'len(self._choices):', len(self._choices), 'length:', length, 'self._alignRight?', self._alignRight) + if len(self._choices) and length: + if len(self._choices[0]) > length: + # changed mask without respecifying choices; readjust the width as appropriate: + self._choices = [choice.strip() for choice in self._choices] + if self._alignRight: + self._choices = [choice.rjust( length ) for choice in self._choices] + else: + self._choices = [choice.ljust( length ) for choice in self._choices] +## dbg('aligned choices:', self._choices) + + if hasattr(self, '_template'): + # Verify each choice specified is valid: + for choice in self._choices: + if self.IsEmpty(choice) and not self._validRequired: + # allow empty values even if invalid, (just colored differently) + continue + if not self.IsValid(choice): +## dbg(indent=0, suspend=0) + ve = ValueError('%s: "%s" is not a valid value for the control as specified.' % (str(self._index), choice)) + ve.value = choice + raise ve + self._hasList = True + +#### dbg("kwargs.has_key('fillChar')?", kwargs.has_key('fillChar'), "len(self._choices) > 0?", len(self._choices) > 0) +#### dbg("self._old_fillChar:'%s'" % self._old_fillChar, "self._fillChar: '%s'" % self._fillChar) + if kwargs.has_key('fillChar') and len(self._choices) > 0: + if kwargs['fillChar'] != ' ': + self._choices = [choice.replace(' ', self._fillChar) for choice in self._choices] + else: + self._choices = [choice.replace(self._old_fillChar, self._fillChar) for choice in self._choices] +## dbg('updated choices:', self._choices) + + + if kwargs.has_key('autoSelect') and kwargs['autoSelect']: + if not self._hasList: +## dbg('no list to auto complete; ignoring "autoSelect=True"') + self._autoSelect = False + + # reset field validity assumption: + self._valid = True +## dbg(indent=0, suspend=0) + + + def _GetParameter(self, paramname): + """ + Routine for retrieving the value of any given parameter + """ + if Field.valid_params.has_key(paramname): + return getattr(self, '_' + paramname) + else: + TypeError('Field._GetParameter: invalid parameter "%s"' % key) + + + def IsEmpty(self, slice): + """ + Indicates whether the specified slice is considered empty for the + field. + """ +## dbg('Field::IsEmpty("%s")' % slice, indent=1) + if not hasattr(self, '_template'): +## dbg(indent=0) + raise AttributeError('_template') + +## dbg('self._template: "%s"' % self._template) +## dbg('self._defaultValue: "%s"' % str(self._defaultValue)) + if slice == self._template and not self._defaultValue: +## dbg(indent=0) + return True + + elif slice == self._template: + empty = True + for pos in range(len(self._template)): +#### dbg('slice[%(pos)d] != self._fillChar?' %locals(), slice[pos] != self._fillChar[pos]) + if slice[pos] not in (' ', self._fillChar): + empty = False + break +## dbg("IsEmpty? %(empty)d (do all mask chars == fillChar?)" % locals(), indent=0) + return empty + else: +## dbg("IsEmpty? 0 (slice doesn't match template)", indent=0) + return False + + + def IsValid(self, slice): + """ + Indicates whether the specified slice is considered a valid value for the + field. + """ +## dbg(suspend=1) +## dbg('Field[%s]::IsValid("%s")' % (str(self._index), slice), indent=1) + valid = True # assume true to start + + if self.IsEmpty(slice): +## dbg(indent=0, suspend=0) + if self._emptyInvalid: + return False + else: + return True + + elif self._hasList and self._choiceRequired: +## dbg("(member of list required)") + # do case-insensitive match on list; strip surrounding whitespace from slice (already done for choices): + if self._fillChar != ' ': + slice = slice.replace(self._fillChar, ' ') +## dbg('updated slice:"%s"' % slice) + compareStr = slice.strip() + + if self._compareNoCase: + compareStr = compareStr.lower() + valid = compareStr in self._compareChoices + + elif self._hasRange and not self.IsEmpty(slice): +## dbg('validating against range') + try: + # allow float as well as int ranges (int comparisons for free.) + valid = self._rangeLow <= float(slice) <= self._rangeHigh + except: + valid = False + + elif self._validRegex and self._filter: +## dbg('validating against regex') + valid = (re.match( self._filter, slice) is not None) + + if valid and self._validFunc: +## dbg('validating against supplied function') + valid = self._validFunc(slice) +## dbg('valid?', valid, indent=0, suspend=0) + return valid + + + def _AdjustField(self, slice): + """ 'Fixes' an integer field. Right or left-justifies, as required.""" +## dbg('Field::_AdjustField("%s")' % slice, indent=1) + length = len(self._mask) +#### dbg('length(self._mask):', length) +#### dbg('self._useParensForNegatives?', self._useParensForNegatives) + if self._isInt: + if self._useParensForNegatives: + signpos = slice.find('(') + right_signpos = slice.find(')') + intStr = slice.replace('(', '').replace(')', '') # drop sign, if any + else: + signpos = slice.find('-') + intStr = slice.replace( '-', '' ) # drop sign, if any + right_signpos = -1 + + intStr = intStr.replace(' ', '') # drop extra spaces + intStr = string.replace(intStr,self._fillChar,"") # drop extra fillchars + intStr = string.replace(intStr,"-","") # drop sign, if any + intStr = string.replace(intStr, self._groupChar, "") # lose commas/dots +#### dbg('intStr:"%s"' % intStr) + start, end = self._extent + field_len = end - start + if not self._padZero and len(intStr) != field_len and intStr.strip(): + intStr = str(long(intStr)) +#### dbg('raw int str: "%s"' % intStr) +#### dbg('self._groupdigits:', self._groupdigits, 'self._formatcodes:', self._formatcodes) + if self._groupdigits: + new = '' + cnt = 1 + for i in range(len(intStr)-1, -1, -1): + new = intStr[i] + new + if (cnt) % 3 == 0: + new = self._groupChar + new + cnt += 1 + if new and new[0] == self._groupChar: + new = new[1:] + if len(new) <= length: + # expanded string will still fit and leave room for sign: + intStr = new + # else... leave it without the commas... + +## dbg('padzero?', self._padZero) +## dbg('len(intStr):', len(intStr), 'field length:', length) + if self._padZero and len(intStr) < length: + intStr = '0' * (length - len(intStr)) + intStr + if signpos != -1: # we had a sign before; restore it + if self._useParensForNegatives: + intStr = '(' + intStr[1:] + if right_signpos != -1: + intStr += ')' + else: + intStr = '-' + intStr[1:] + elif signpos != -1 and slice[0:signpos].strip() == '': # - was before digits + if self._useParensForNegatives: + intStr = '(' + intStr + if right_signpos != -1: + intStr += ')' + else: + intStr = '-' + intStr + elif right_signpos != -1: + # must have had ')' but '(' was before field; re-add ')' + intStr += ')' + slice = intStr + + slice = slice.strip() # drop extra spaces + + if self._alignRight: ## Only if right-alignment is enabled + slice = slice.rjust( length ) + else: + slice = slice.ljust( length ) + if self._fillChar != ' ': + slice = slice.replace(' ', self._fillChar) +## dbg('adjusted slice: "%s"' % slice, indent=0) + return slice + + +## ---------- ---------- ---------- ---------- ---------- ---------- ---------- + +class MaskedEditMixin: + """ + This class allows us to abstract the masked edit functionality that could + be associated with any text entry control. (eg. wx.TextCtrl, wx.ComboBox, etc.) + It forms the basis for all of the lib.masked controls. + """ + valid_ctrl_params = { + 'mask': 'XXXXXXXXXXXXX', ## mask string for formatting this control + 'autoformat': "", ## optional auto-format code to set format from masktags dictionary + 'fields': {}, ## optional list/dictionary of maskededit.Field class instances, indexed by position in mask + 'datestyle': 'MDY', ## optional date style for date-type values. Can trigger autocomplete year + 'autoCompleteKeycodes': [], ## Optional list of additional keycodes which will invoke field-auto-complete + 'useFixedWidthFont': True, ## Use fixed-width font instead of default for base control + 'defaultEncoding': 'latin1', ## optional argument to indicate unicode codec to use (unicode ctrls only) + 'retainFieldValidation': False, ## Set this to true if setting control-level parameters independently, + ## from field validation constraints + 'emptyBackgroundColour': "White", + 'validBackgroundColour': "White", + 'invalidBackgroundColour': "Yellow", + 'foregroundColour': "Black", + 'signedForegroundColour': "Red", + 'demo': False} + + + def __init__(self, name = 'MaskedEdit', **kwargs): + """ + This is the "constructor" for setting up the mixin variable parameters for the composite class. + """ + + self.name = name + + # set up flag for doing optional things to base control if possible + if not hasattr(self, 'controlInitialized'): + self.controlInitialized = False + + # Set internal state var for keeping track of whether or not a character + # action results in a modification of the control, since .SetValue() + # doesn't modify the base control's internal state: + self.modified = False + self._previous_mask = None + + # Validate legitimate set of parameters: + for key in kwargs.keys(): + if key.replace('Color', 'Colour') not in MaskedEditMixin.valid_ctrl_params.keys() + Field.valid_params.keys(): + raise TypeError('%s: invalid parameter "%s"' % (name, key)) + + ## Set up dictionary that can be used by subclasses to override or add to default + ## behavior for individual characters. Derived subclasses needing to change + ## default behavior for keys can either redefine the default functions for the + ## common keys or add functions for specific keys to this list. Each function + ## added should take the key event as argument, and return False if the key + ## requires no further processing. + ## + ## Initially populated with navigation and function control keys: + self._keyhandlers = { + # default navigation keys and handlers: + wx.WXK_BACK: self._OnErase, + wx.WXK_LEFT: self._OnArrow, + wx.WXK_NUMPAD_LEFT: self._OnArrow, + wx.WXK_RIGHT: self._OnArrow, + wx.WXK_NUMPAD_RIGHT: self._OnArrow, + wx.WXK_UP: self._OnAutoCompleteField, + wx.WXK_NUMPAD_UP: self._OnAutoCompleteField, + wx.WXK_DOWN: self._OnAutoCompleteField, + wx.WXK_NUMPAD_DOWN: self._OnAutoCompleteField, + wx.WXK_TAB: self._OnChangeField, + wx.WXK_HOME: self._OnHome, + wx.WXK_NUMPAD_HOME: self._OnHome, + wx.WXK_END: self._OnEnd, + wx.WXK_NUMPAD_END: self._OnEnd, + wx.WXK_RETURN: self._OnReturn, + wx.WXK_NUMPAD_ENTER: self._OnReturn, + wx.WXK_PRIOR: self._OnAutoCompleteField, + wx.WXK_NUMPAD_PRIOR: self._OnAutoCompleteField, + wx.WXK_NEXT: self._OnAutoCompleteField, + wx.WXK_NUMPAD_NEXT: self._OnAutoCompleteField, + + # default function control keys and handlers: + wx.WXK_DELETE: self._OnDelete, + wx.WXK_NUMPAD_DELETE: self._OnDelete, + wx.WXK_INSERT: self._OnInsert, + wx.WXK_NUMPAD_INSERT: self._OnInsert, + + WXK_CTRL_A: self._OnCtrl_A, + WXK_CTRL_C: self._OnCtrl_C, + WXK_CTRL_S: self._OnCtrl_S, + WXK_CTRL_V: self._OnCtrl_V, + WXK_CTRL_X: self._OnCtrl_X, + WXK_CTRL_Z: self._OnCtrl_Z, + } + + ## bind standard navigational and control keycodes to this instance, + ## so that they can be augmented and/or changed in derived classes: + self._nav = list(nav) + self._control = list(control) + + ## Dynamically evaluate and store string constants for mask chars + ## so that locale settings can be made after this module is imported + ## and the controls created after that is done can allow the + ## appropriate characters: + self.maskchardict = { + '#': string.digits, + 'A': string.uppercase, + 'a': string.lowercase, + 'X': string.letters + string.punctuation + string.digits, + 'C': string.letters, + 'N': string.letters + string.digits, + '&': string.punctuation, + '*': ansichars # to give it a value, but now allows any non-wxcontrol character + } + + ## self._ignoreChange is used by MaskedComboBox, because + ## of the hack necessary to determine the selection; it causes + ## EVT_TEXT messages from the combobox to be ignored if set. + self._ignoreChange = False + + # These are used to keep track of previous value, for undo functionality: + self._curValue = None + self._prevValue = None + + self._valid = True + + # Set defaults for each parameter for this instance, and fully + # populate initial parameter list for configuration: + for key, value in MaskedEditMixin.valid_ctrl_params.items(): + setattr(self, '_' + key, copy.copy(value)) + if not kwargs.has_key(key): +#### dbg('%s: "%s"' % (key, repr(value))) + kwargs[key] = copy.copy(value) + + # Create a "field" that holds global parameters for control constraints + self._ctrl_constraints = self._fields[-1] = Field(index=-1) + self.SetCtrlParameters(**kwargs) + + + + def SetCtrlParameters(self, **kwargs): + """ + This public function can be used to set individual or multiple masked edit + parameters after construction. (See maskededit module overview for the list + of valid parameters.) + """ +## dbg(suspend=1) +## dbg('MaskedEditMixin::SetCtrlParameters', indent=1) +#### dbg('kwargs:', indent=1) +## for key, value in kwargs.items(): +#### dbg(key, '=', value) +#### dbg(indent=0) + + # Validate keyword arguments: + constraint_kwargs = {} + ctrl_kwargs = {} + for key, value in kwargs.items(): + key = key.replace('Color', 'Colour') # for b-c, and standard wxPython spelling + if key not in MaskedEditMixin.valid_ctrl_params.keys() + Field.valid_params.keys(): +## dbg(indent=0, suspend=0) + ae = AttributeError('Invalid keyword argument "%s" for control "%s"' % (key, self.name)) + ae.attribute = key + raise ae + elif key in Field.valid_params.keys(): + constraint_kwargs[key] = value + else: + ctrl_kwargs[key] = value + + mask = None + reset_args = {} + + if ctrl_kwargs.has_key('autoformat'): + autoformat = ctrl_kwargs['autoformat'] + else: + autoformat = None + + # handle "parochial name" backward compatibility: + if autoformat and autoformat.find('MILTIME') != -1 and autoformat not in masktags.keys(): + autoformat = autoformat.replace('MILTIME', '24HRTIME') + + if autoformat != self._autoformat and autoformat in masktags.keys(): +## dbg('autoformat:', autoformat) + self._autoformat = autoformat + mask = masktags[self._autoformat]['mask'] + # gather rest of any autoformat parameters: + for param, value in masktags[self._autoformat].items(): + if param == 'mask': continue # (must be present; already accounted for) + constraint_kwargs[param] = value + + elif autoformat and not autoformat in masktags.keys(): + ae = AttributeError('invalid value for autoformat parameter: %s' % repr(autoformat)) + ae.attribute = autoformat + raise ae + else: +## dbg('autoformat not selected') + if kwargs.has_key('mask'): + mask = kwargs['mask'] +## dbg('mask:', mask) + + ## Assign style flags + if mask is None: +## dbg('preserving previous mask') + mask = self._previous_mask # preserve previous mask + else: +## dbg('mask (re)set') + reset_args['reset_mask'] = mask + constraint_kwargs['mask'] = mask + + # wipe out previous fields; preserve new control-level constraints + self._fields = {-1: self._ctrl_constraints} + + + if ctrl_kwargs.has_key('fields'): + # do field parameter type validation, and conversion to internal dictionary + # as appropriate: + fields = ctrl_kwargs['fields'] + if type(fields) in (types.ListType, types.TupleType): + for i in range(len(fields)): + field = fields[i] + if not isinstance(field, Field): +## dbg(indent=0, suspend=0) + raise TypeError('invalid type for field parameter: %s' % repr(field)) + self._fields[i] = field + + elif type(fields) == types.DictionaryType: + for index, field in fields.items(): + if not isinstance(field, Field): +## dbg(indent=0, suspend=0) + raise TypeError('invalid type for field parameter: %s' % repr(field)) + self._fields[index] = field + else: +## dbg(indent=0, suspend=0) + raise TypeError('fields parameter must be a list or dictionary; not %s' % repr(fields)) + + # Assign constraint parameters for entire control: +#### dbg('control constraints:', indent=1) +## for key, value in constraint_kwargs.items(): +#### dbg('%s:' % key, value) +#### dbg(indent=0) + + # determine if changing parameters that should affect the entire control: + for key in MaskedEditMixin.valid_ctrl_params.keys(): + if key in ( 'mask', 'fields' ): continue # (processed separately) + if ctrl_kwargs.has_key(key): + setattr(self, '_' + key, ctrl_kwargs[key]) + + # Validate color parameters, converting strings to named colors and validating + # result if appropriate: + for key in ('emptyBackgroundColour', 'invalidBackgroundColour', 'validBackgroundColour', + 'foregroundColour', 'signedForegroundColour'): + if ctrl_kwargs.has_key(key): + if type(ctrl_kwargs[key]) in (types.StringType, types.UnicodeType): + c = wx.NamedColour(ctrl_kwargs[key]) + if c.Get() == (-1, -1, -1): + raise TypeError('%s not a legal color specification for %s' % (repr(ctrl_kwargs[key]), key)) + else: + # replace attribute with wxColour object: + setattr(self, '_' + key, c) + # attach a python dynamic attribute to wxColour for debug printouts + c._name = ctrl_kwargs[key] + + elif type(ctrl_kwargs[key]) != type(wx.BLACK): + raise TypeError('%s not a legal color specification for %s' % (repr(ctrl_kwargs[key]), key)) + + +## dbg('self._retainFieldValidation:', self._retainFieldValidation) + if not self._retainFieldValidation: + # Build dictionary of any changing parameters which should be propagated to the + # component fields: + for arg in Field.propagating_params: +#### dbg('kwargs.has_key(%s)?' % arg, kwargs.has_key(arg)) +#### dbg('getattr(self._ctrl_constraints, _%s)?' % arg, getattr(self._ctrl_constraints, '_'+arg)) + reset_args[arg] = kwargs.has_key(arg) and kwargs[arg] != getattr(self._ctrl_constraints, '_'+arg) +#### dbg('reset_args[%s]?' % arg, reset_args[arg]) + + # Set the control-level constraints: + self._ctrl_constraints._SetParameters(**constraint_kwargs) + + # This routine does the bulk of the interdependent parameter processing, determining + # the field extents of the mask if changed, resetting parameters as appropriate, + # determining the overall template value for the control, etc. + self._configure(mask, **reset_args) + + # now that we've propagated the field constraints and mask portions to the + # various fields, validate the constraints + self._ctrl_constraints._ValidateParameters(**constraint_kwargs) + + # Validate that all choices for given fields are at least of the + # necessary length, and that they all would be valid pastes if pasted + # into their respective fields: +#### dbg('validating choices') + self._validateChoices() + + + self._autofit = self._ctrl_constraints._autofit + self._isNeg = False + + self._isDate = 'D' in self._ctrl_constraints._formatcodes and _isDateType(mask) + self._isTime = 'T' in self._ctrl_constraints._formatcodes and _isTimeType(mask) + if self._isDate: + # Set _dateExtent, used in date validation to locate date in string; + # always set as though year will be 4 digits, even if mask only has + # 2 digits, so we can always properly process the intended year for + # date validation (leap years, etc.) + if self._mask.find('CCC') != -1: self._dateExtent = 11 + else: self._dateExtent = 10 + + self._4digityear = len(self._mask) > 8 and self._mask[9] == '#' + + if self._isDate and self._autoformat: + # Auto-decide datestyle: + if self._autoformat.find('MDDY') != -1: self._datestyle = 'MDY' + elif self._autoformat.find('YMMD') != -1: self._datestyle = 'YMD' + elif self._autoformat.find('YMMMD') != -1: self._datestyle = 'YMD' + elif self._autoformat.find('DMMY') != -1: self._datestyle = 'DMY' + elif self._autoformat.find('DMMMY') != -1: self._datestyle = 'DMY' + + # Give derived controls a chance to react to parameter changes before + # potentially changing current value of the control. + self._OnCtrlParametersChanged() + + if self.controlInitialized: + # Then the base control is available for configuration; + # take action on base control based on new settings, as appropriate. + if kwargs.has_key('useFixedWidthFont'): + # Set control font - fixed width by default + self._setFont() + + if reset_args.has_key('reset_mask'): +## dbg('reset mask') + curvalue = self._GetValue() + if curvalue.strip(): + try: +## dbg('attempting to _SetInitialValue(%s)' % self._GetValue()) + self._SetInitialValue(self._GetValue()) + except Exception, e: +## dbg('exception caught:', e) +## dbg("current value doesn't work; attempting to reset to template") + self._SetInitialValue() + else: +## dbg('attempting to _SetInitialValue() with template') + self._SetInitialValue() + + elif kwargs.has_key('useParensForNegatives'): + newvalue = self._getSignedValue()[0] + + if newvalue is not None: + # Adjust for new mask: + if len(newvalue) < len(self._mask): + newvalue += ' ' + elif len(newvalue) > len(self._mask): + if newvalue[-1] in (' ', ')'): + newvalue = newvalue[:-1] + +## dbg('reconfiguring value for parens:"%s"' % newvalue) + self._SetValue(newvalue) + + if self._prevValue != newvalue: + self._prevValue = newvalue # disallow undo of sign type + + if self._autofit: +## dbg('calculated size:', self._CalcSize()) + self.SetClientSize(self._CalcSize()) + width = self.GetSize().width + height = self.GetBestSize().height +## dbg('setting client size to:', (width, height)) + self.SetInitialSize((width, height)) + + # Set value/type-specific formatting + self._applyFormatting() +## dbg(indent=0, suspend=0) + + def SetMaskParameters(self, **kwargs): + """ old name for the SetCtrlParameters function (DEPRECATED)""" + return self.SetCtrlParameters(**kwargs) + + + def GetCtrlParameter(self, paramname): + """ + Routine for retrieving the value of any given parameter + """ + if MaskedEditMixin.valid_ctrl_params.has_key(paramname.replace('Color','Colour')): + return getattr(self, '_' + paramname.replace('Color', 'Colour')) + elif Field.valid_params.has_key(paramname): + return self._ctrl_constraints._GetParameter(paramname) + else: + TypeError('"%s".GetCtrlParameter: invalid parameter "%s"' % (self.name, paramname)) + + def GetMaskParameter(self, paramname): + """ old name for the GetCtrlParameters function (DEPRECATED)""" + return self.GetCtrlParameter(paramname) + + +## This idea worked, but Boa was unable to use this solution... +## def _attachMethod(self, func): +## import new +## setattr(self, func.__name__, new.instancemethod(func, self, self.__class__)) +## +## +## def _DefinePropertyFunctions(exposed_params): +## for param in exposed_params: +## propname = param[0].upper() + param[1:] +## +## exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) +## exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) +## self._attachMethod(locals()['Set%s' % propname]) +## self._attachMethod(locals()['Get%s' % propname]) +## +## if param.find('Colour') != -1: +## # add non-british spellings, for backward-compatibility +## propname.replace('Colour', 'Color') +## +## exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) +## exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) +## self._attachMethod(locals()['Set%s' % propname]) +## self._attachMethod(locals()['Get%s' % propname]) +## + + + def SetFieldParameters(self, field_index, **kwargs): + """ + Routine provided to modify the parameters of a given field. + Because changes to fields can affect the overall control, + direct access to the fields is prevented, and the control + is always "reconfigured" after setting a field parameter. + (See maskededit module overview for the list of valid field-level + parameters.) + """ + if field_index not in self._field_indices: + ie = IndexError('%s is not a valid field for control "%s".' % (str(field_index), self.name)) + ie.index = field_index + raise ie + # set parameters as requested: + self._fields[field_index]._SetParameters(**kwargs) + + # Possibly reprogram control template due to resulting changes, and ensure + # control-level params are still propagated to fields: + self._configure(self._previous_mask) + self._fields[field_index]._ValidateParameters(**kwargs) + + if self.controlInitialized: + if kwargs.has_key('fillChar') or kwargs.has_key('defaultValue'): + self._SetInitialValue() + + if self._autofit: + # this is tricky, because, as Robin explains: + # "Basically there are two sizes to deal with, that are potentially + # different. The client size is the inside size and may, depending + # on platform, exclude the borders and such. The normal size is + # the outside size that does include the borders. What you are + # calculating (in _CalcSize) is the client size, but the sizers + # deal with the full size and so that is the minimum size that + # we need to set with SetInitialSize. The root of the problem is + # that in _calcSize the current client size height is returned, + # instead of a height based on the current font. So I suggest using + # _calcSize to just get the width, and then use GetBestSize to + # get the height." + self.SetClientSize(self._CalcSize()) + width = self.GetSize().width + height = self.GetBestSize().height + self.SetInitialSize((width, height)) + + + # Set value/type-specific formatting + self._applyFormatting() + + + def GetFieldParameter(self, field_index, paramname): + """ + Routine provided for getting a parameter of an individual field. + """ + if field_index not in self._field_indices: + ie = IndexError('%s is not a valid field for control "%s".' % (str(field_index), self.name)) + ie.index = field_index + raise ie + elif Field.valid_params.has_key(paramname): + return self._fields[field_index]._GetParameter(paramname) + else: + ae = AttributeError('"%s".GetFieldParameter: invalid parameter "%s"' % (self.name, paramname)) + ae.attribute = paramname + raise ae + + + def _SetKeycodeHandler(self, keycode, func): + """ + This function adds and/or replaces key event handling functions + used by the control. should take the event as argument + and return False if no further action on the key is necessary. + """ + if func: + self._keyhandlers[keycode] = func + elif self._keyhandlers.has_key(keycode): + del self._keyhandlers[keycode] + + + def _SetKeyHandler(self, char, func): + """ + This function adds and/or replaces key event handling functions + for ascii characters. should take the event as argument + and return False if no further action on the key is necessary. + """ + self._SetKeycodeHandler(ord(char), func) + + + def _AddNavKeycode(self, keycode, handler=None): + """ + This function allows a derived subclass to augment the list of + keycodes that are considered "navigational" keys. + """ + self._nav.append(keycode) + if handler: + self._keyhandlers[keycode] = handler + elif self.keyhandlers.has_key(keycode): + del self._keyhandlers[keycode] + + + + def _AddNavKey(self, char, handler=None): + """ + This function is a convenience function so you don't have to + remember to call ord() for ascii chars to be used for navigation. + """ + self._AddNavKeycode(ord(char), handler) + + + def _GetNavKeycodes(self): + """ + This function retrieves the current list of navigational keycodes for + the control. + """ + return self._nav + + + def _SetNavKeycodes(self, keycode_func_tuples): + """ + This function allows you to replace the current list of keycode processed + as navigation keys, and bind associated optional keyhandlers. + """ + self._nav = [] + for keycode, func in keycode_func_tuples: + self._nav.append(keycode) + if func: + self._keyhandlers[keycode] = func + elif self.keyhandlers.has_key(keycode): + del self._keyhandlers[keycode] + + + def _processMask(self, mask): + """ + This subroutine expands {n} syntax in mask strings, and looks for escaped + special characters and returns the expanded mask, and an dictionary + of booleans indicating whether or not a given position in the mask is + a mask character or not. + """ +## dbg('_processMask: mask', mask, indent=1) + # regular expression for parsing c{n} syntax: + rex = re.compile('([' +string.join(maskchars,"") + '])\{(\d+)\}') + s = mask + match = rex.search(s) + while match: # found an(other) occurrence + maskchr = s[match.start(1):match.end(1)] # char to be repeated + repcount = int(s[match.start(2):match.end(2)]) # the number of times + replacement = string.join( maskchr * repcount, "") # the resulting substr + s = s[:match.start(1)] + replacement + s[match.end(2)+1:] #account for trailing '}' + match = rex.search(s) # look for another such entry in mask + + self._decimalChar = self._ctrl_constraints._decimalChar + self._shiftDecimalChar = self._ctrl_constraints._shiftDecimalChar + + self._isFloat = _isFloatingPoint(s) and not self._ctrl_constraints._validRegex + self._isInt = _isInteger(s) and not self._ctrl_constraints._validRegex + self._signOk = '-' in self._ctrl_constraints._formatcodes and (self._isFloat or self._isInt) + self._useParens = self._ctrl_constraints._useParensForNegatives + self._isNeg = False +#### dbg('self._signOk?', self._signOk, 'self._useParens?', self._useParens) +#### dbg('isFloatingPoint(%s)?' % (s), _isFloatingPoint(s), +## 'ctrl regex:', self._ctrl_constraints._validRegex) + + if self._signOk and s[0] != ' ': + s = ' ' + s + if self._ctrl_constraints._defaultValue and self._ctrl_constraints._defaultValue[0] != ' ': + self._ctrl_constraints._defaultValue = ' ' + self._ctrl_constraints._defaultValue + self._signpos = 0 + + if self._useParens: + s += ' ' + self._ctrl_constraints._defaultValue += ' ' + + + # Now, go build up a dictionary of booleans, indexed by position, + # indicating whether or not a given position is masked or not. + # Also, strip out any '|' chars, adjusting the mask as necessary, + # marking the appropriate positions for field boundaries: + ismasked = {} + explicit_field_boundaries = [] + s = list(s) + i = 0 + while i < len(s): + if s[i] == '\\': # if escaped character: + ismasked[i] = False # mark position as not a mask char + if i+1 < len(s): # if another char follows... + del s[i] # elide the '\' + if s[i] == '\\': # if next char also a '\', char is a literal '\' + del s[i] # elide the 2nd '\' as well + i += 1 # increment to next char + elif s[i] == '|': + del s[i] # elide the '|' + explicit_field_boundaries.append(i) + # keep index where it is: + else: # else if special char, mark position accordingly + ismasked[i] = s[i] in maskchars +#### dbg('ismasked[%d]:' % i, ismasked[i], ''.join(s)) + i += 1 # increment to next char +#### dbg('ismasked:', ismasked) + s = ''.join(s) +## dbg('new mask: "%s"' % s, indent=0) + + return s, ismasked, explicit_field_boundaries + + + def _calcFieldExtents(self): + """ + Subroutine responsible for establishing/configuring field instances with + indices and editable extents appropriate to the specified mask, and building + the lookup table mapping each position to the corresponding field. + """ + self._lookupField = {} + if self._mask: + + ## Create dictionary of positions,characters in mask + self.maskdict = {} + for charnum in range( len( self._mask)): + self.maskdict[charnum] = self._mask[charnum:charnum+1] + + # For the current mask, create an ordered list of field extents + # and a dictionary of positions that map to field indices: + + if self._signOk: start = 1 + else: start = 0 + + if self._isFloat: + # Skip field "discovery", and just construct a 2-field control with appropriate + # constraints for a floating-point entry. + + # .setdefault always constructs 2nd argument even if not needed, so we do this + # the old-fashioned way... + if not self._fields.has_key(0): + self._fields[0] = Field() + if not self._fields.has_key(1): + self._fields[1] = Field() + + self._decimalpos = string.find( self._mask, '.') +## dbg('decimal pos =', self._decimalpos) + + formatcodes = self._fields[0]._GetParameter('formatcodes') + if 'R' not in formatcodes: formatcodes += 'R' + self._fields[0]._SetParameters(index=0, extent=(start, self._decimalpos), + mask=self._mask[start:self._decimalpos], formatcodes=formatcodes) + end = len(self._mask) + if self._signOk and self._useParens: + end -= 1 + self._fields[1]._SetParameters(index=1, extent=(self._decimalpos+1, end), + mask=self._mask[self._decimalpos+1:end]) + + for i in range(self._decimalpos+1): + self._lookupField[i] = 0 + + for i in range(self._decimalpos+1, len(self._mask)+1): + self._lookupField[i] = 1 + + elif self._isInt: + # Skip field "discovery", and just construct a 1-field control with appropriate + # constraints for a integer entry. + if not self._fields.has_key(0): + self._fields[0] = Field(index=0) + end = len(self._mask) + if self._signOk and self._useParens: + end -= 1 + self._fields[0]._SetParameters(index=0, extent=(start, end), + mask=self._mask[start:end]) + for i in range(len(self._mask)+1): + self._lookupField[i] = 0 + else: + # generic control; parse mask to figure out where the fields are: + field_index = 0 + pos = 0 + i = self._findNextEntry(pos,adjustInsert=False) # go to 1st entry point: + if i < len(self._mask): # no editable chars! + for j in range(pos, i+1): + self._lookupField[j] = field_index + pos = i # figure out field for 1st editable space: + + while i <= len(self._mask): +#### dbg('searching: outer field loop: i = ', i) + if self._isMaskChar(i): +#### dbg('1st char is mask char; recording edit_start=', i) + edit_start = i + # Skip to end of editable part of current field: + while i < len(self._mask) and self._isMaskChar(i): + self._lookupField[i] = field_index + i += 1 + if i in self._explicit_field_boundaries: + break +#### dbg('edit_end =', i) + edit_end = i + self._lookupField[i] = field_index +#### dbg('self._fields.has_key(%d)?' % field_index, self._fields.has_key(field_index)) + if not self._fields.has_key(field_index): + kwargs = Field.valid_params.copy() + kwargs['index'] = field_index + kwargs['extent'] = (edit_start, edit_end) + kwargs['mask'] = self._mask[edit_start:edit_end] + self._fields[field_index] = Field(**kwargs) + else: + self._fields[field_index]._SetParameters( + index=field_index, + extent=(edit_start, edit_end), + mask=self._mask[edit_start:edit_end]) + pos = i + i = self._findNextEntry(pos, adjustInsert=False) # go to next field: +#### dbg('next entry:', i) + if i > pos: + for j in range(pos, i+1): + self._lookupField[j] = field_index + if i >= len(self._mask): + break # if past end, we're done + else: + field_index += 1 +#### dbg('next field:', field_index) + + indices = self._fields.keys() + indices.sort() + self._field_indices = indices[1:] +#### dbg('lookupField map:', indent=1) +## for i in range(len(self._mask)): +#### dbg('pos %d:' % i, self._lookupField[i]) +#### dbg(indent=0) + + # Verify that all field indices specified are valid for mask: + for index in self._fields.keys(): + if index not in [-1] + self._lookupField.values(): + ie = IndexError('field %d is not a valid field for mask "%s"' % (index, self._mask)) + ie.index = index + raise ie + + + + def _calcTemplate(self, reset_fillchar, reset_default): + """ + Subroutine for processing current fillchars and default values for + whole control and individual fields, constructing the resulting + overall template, and adjusting the current value as necessary. + """ + default_set = False + if self._ctrl_constraints._defaultValue: + default_set = True + else: + for field in self._fields.values(): + if field._defaultValue and not reset_default: + default_set = True +## dbg('default set?', default_set) + + # Determine overall new template for control, and keep track of previous + # values, so that current control value can be modified as appropriate: + if self.controlInitialized: curvalue = list(self._GetValue()) + else: curvalue = None + + if hasattr(self, '_fillChar'): old_fillchars = self._fillChar + else: old_fillchars = None + + if hasattr(self, '_template'): old_template = self._template + else: old_template = None + + self._template = "" + + self._fillChar = {} + reset_value = False + + for field in self._fields.values(): + field._template = "" + + for pos in range(len(self._mask)): +#### dbg('pos:', pos) + field = self._FindField(pos) +#### dbg('field:', field._index) + start, end = field._extent + + if pos == 0 and self._signOk: + self._template = ' ' # always make 1st 1st position blank, regardless of fillchar + elif self._isFloat and pos == self._decimalpos: + self._template += self._decimalChar + elif self._isMaskChar(pos): + if field._fillChar != self._ctrl_constraints._fillChar and not reset_fillchar: + fillChar = field._fillChar + else: + fillChar = self._ctrl_constraints._fillChar + self._fillChar[pos] = fillChar + + # Replace any current old fillchar with new one in current value; + # if action required, set reset_value flag so we can take that action + # after we're all done + if self.controlInitialized and old_fillchars and old_fillchars.has_key(pos) and curvalue: + if curvalue[pos] == old_fillchars[pos] and old_fillchars[pos] != fillChar: + reset_value = True + curvalue[pos] = fillChar + + if not field._defaultValue and not self._ctrl_constraints._defaultValue: +#### dbg('no default value') + self._template += fillChar + field._template += fillChar + + elif field._defaultValue and not reset_default: +#### dbg('len(field._defaultValue):', len(field._defaultValue)) +#### dbg('pos-start:', pos-start) + if len(field._defaultValue) > pos-start: +#### dbg('field._defaultValue[pos-start]: "%s"' % field._defaultValue[pos-start]) + self._template += field._defaultValue[pos-start] + field._template += field._defaultValue[pos-start] + else: +#### dbg('field default not long enough; using fillChar') + self._template += fillChar + field._template += fillChar + else: + if len(self._ctrl_constraints._defaultValue) > pos: +#### dbg('using control default') + self._template += self._ctrl_constraints._defaultValue[pos] + field._template += self._ctrl_constraints._defaultValue[pos] + else: +#### dbg('ctrl default not long enough; using fillChar') + self._template += fillChar + field._template += fillChar +#### dbg('field[%d]._template now "%s"' % (field._index, field._template)) +#### dbg('self._template now "%s"' % self._template) + else: + self._template += self._mask[pos] + + self._fields[-1]._template = self._template # (for consistency) + + if curvalue: # had an old value, put new one back together + newvalue = string.join(curvalue, "") + else: + newvalue = None + + if default_set: + self._defaultValue = self._template +## dbg('self._defaultValue:', self._defaultValue) + if not self.IsEmpty(self._defaultValue) and not self.IsValid(self._defaultValue): +#### dbg(indent=0) + ve = ValueError('Default value of "%s" is not a valid value for control "%s"' % (self._defaultValue, self.name)) + ve.value = self._defaultValue + raise ve + + # if no fillchar change, but old value == old template, replace it: + if newvalue == old_template: + newvalue = self._template + reset_value = True + else: + self._defaultValue = None + + if reset_value: +## dbg('resetting value to: "%s"' % newvalue) + pos = self._GetInsertionPoint() + sel_start, sel_to = self._GetSelection() + self._SetValue(newvalue) + self._SetInsertionPoint(pos) + self._SetSelection(sel_start, sel_to) + + + def _propagateConstraints(self, **reset_args): + """ + Subroutine for propagating changes to control-level constraints and + formatting to the individual fields as appropriate. + """ + parent_codes = self._ctrl_constraints._formatcodes + parent_includes = self._ctrl_constraints._includeChars + parent_excludes = self._ctrl_constraints._excludeChars + for i in self._field_indices: + field = self._fields[i] + inherit_args = {} + if len(self._field_indices) == 1: + inherit_args['formatcodes'] = parent_codes + inherit_args['includeChars'] = parent_includes + inherit_args['excludeChars'] = parent_excludes + else: + field_codes = current_codes = field._GetParameter('formatcodes') + for c in parent_codes: + if c not in field_codes: field_codes += c + if field_codes != current_codes: + inherit_args['formatcodes'] = field_codes + + include_chars = current_includes = field._GetParameter('includeChars') + for c in parent_includes: + if not c in include_chars: include_chars += c + if include_chars != current_includes: + inherit_args['includeChars'] = include_chars + + exclude_chars = current_excludes = field._GetParameter('excludeChars') + for c in parent_excludes: + if not c in exclude_chars: exclude_chars += c + if exclude_chars != current_excludes: + inherit_args['excludeChars'] = exclude_chars + + if reset_args.has_key('defaultValue') and reset_args['defaultValue']: + inherit_args['defaultValue'] = "" # (reset for field) + + for param in Field.propagating_params: +#### dbg('reset_args.has_key(%s)?' % param, reset_args.has_key(param)) +#### dbg('reset_args.has_key(%(param)s) and reset_args[%(param)s]?' % locals(), reset_args.has_key(param) and reset_args[param]) + if reset_args.has_key(param): + inherit_args[param] = self.GetCtrlParameter(param) +#### dbg('inherit_args[%s]' % param, inherit_args[param]) + + if inherit_args: + field._SetParameters(**inherit_args) + field._ValidateParameters(**inherit_args) + + + def _validateChoices(self): + """ + Subroutine that validates that all choices for given fields are at + least of the necessary length, and that they all would be valid pastes + if pasted into their respective fields. + """ + for field in self._fields.values(): + if field._choices: + index = field._index + if len(self._field_indices) == 1 and index == 0 and field._choices == self._ctrl_constraints._choices: +## dbg('skipping (duplicate) choice validation of field 0') + continue +#### dbg('checking for choices for field', field._index) + start, end = field._extent + field_length = end - start +#### dbg('start, end, length:', start, end, field_length) + for choice in field._choices: +#### dbg('testing "%s"' % choice) + valid_paste, ignore, replace_to = self._validatePaste(choice, start, end) + if not valid_paste: +#### dbg(indent=0) + ve = ValueError('"%s" could not be entered into field %d of control "%s"' % (choice, index, self.name)) + ve.value = choice + ve.index = index + raise ve + elif replace_to > end: +#### dbg(indent=0) + ve = ValueError('"%s" will not fit into field %d of control "%s"' (choice, index, self.name)) + ve.value = choice + ve.index = index + raise ve + +#### dbg(choice, 'valid in field', index) + + + def _configure(self, mask, **reset_args): + """ + This function sets flags for automatic styling options. It is + called whenever a control or field-level parameter is set/changed. + + This routine does the bulk of the interdependent parameter processing, determining + the field extents of the mask if changed, resetting parameters as appropriate, + determining the overall template value for the control, etc. + + reset_args is supplied if called from control's .SetCtrlParameters() + routine, and indicates which if any parameters which can be + overridden by individual fields have been reset by request for the + whole control. + + """ +## dbg(suspend=1) +## dbg('MaskedEditMixin::_configure("%s")' % mask, indent=1) + + # Preprocess specified mask to expand {n} syntax, handle escaped + # mask characters, etc and build the resulting positionally keyed + # dictionary for which positions are mask vs. template characters: + self._mask, self._ismasked, self._explicit_field_boundaries = self._processMask(mask) + self._masklength = len(self._mask) +#### dbg('processed mask:', self._mask) + + # Preserve original mask specified, for subsequent reprocessing + # if parameters change. +## dbg('mask: "%s"' % self._mask, 'previous mask: "%s"' % self._previous_mask) + self._previous_mask = mask # save unexpanded mask for next time + # Set expanded mask and extent of field -1 to width of entire control: + self._ctrl_constraints._SetParameters(mask = self._mask, extent=(0,self._masklength)) + + # Go parse mask to determine where each field is, construct field + # instances as necessary, configure them with those extents, and + # build lookup table mapping each position for control to its corresponding + # field. +#### dbg('calculating field extents') + + self._calcFieldExtents() + + + # Go process defaultValues and fillchars to construct the overall + # template, and adjust the current value as necessary: + reset_fillchar = reset_args.has_key('fillChar') and reset_args['fillChar'] + reset_default = reset_args.has_key('defaultValue') and reset_args['defaultValue'] + +#### dbg('calculating template') + self._calcTemplate(reset_fillchar, reset_default) + + # Propagate control-level formatting and character constraints to each + # field if they don't already have them; if only one field, propagate + # control-level validation constraints to field as well: +#### dbg('propagating constraints') + self._propagateConstraints(**reset_args) + + + if self._isFloat and self._fields[0]._groupChar == self._decimalChar: + raise AttributeError('groupChar (%s) and decimalChar (%s) must be distinct.' % + (self._fields[0]._groupChar, self._decimalChar) ) + +#### dbg('fields:', indent=1) +## for i in [-1] + self._field_indices: +#### dbg('field %d:' % i, self._fields[i].__dict__) +#### dbg(indent=0) + + # Set up special parameters for numeric control, if appropriate: + if self._signOk: + self._signpos = 0 # assume it starts here, but it will move around on floats + signkeys = ['-', '+', ' '] + if self._useParens: + signkeys += ['(', ')'] + for key in signkeys: + keycode = ord(key) + if not self._keyhandlers.has_key(keycode): + self._SetKeyHandler(key, self._OnChangeSign) + elif self._isInt or self._isFloat: + signkeys = ['-', '+', ' ', '(', ')'] + for key in signkeys: + keycode = ord(key) + if self._keyhandlers.has_key(keycode) and self._keyhandlers[keycode] == self._OnChangeSign: + self._SetKeyHandler(key, None) + + + + if self._isFloat or self._isInt: + if self.controlInitialized: + value = self._GetValue() +#### dbg('value: "%s"' % value, 'len(value):', len(value), +## 'len(self._ctrl_constraints._mask):',len(self._ctrl_constraints._mask)) + if len(value) < len(self._ctrl_constraints._mask): + newvalue = value + if self._useParens and len(newvalue) < len(self._ctrl_constraints._mask) and newvalue.find('(') == -1: + newvalue += ' ' + if self._signOk and len(newvalue) < len(self._ctrl_constraints._mask) and newvalue.find(')') == -1: + newvalue = ' ' + newvalue + if len(newvalue) < len(self._ctrl_constraints._mask): + if self._ctrl_constraints._alignRight: + newvalue = newvalue.rjust(len(self._ctrl_constraints._mask)) + else: + newvalue = newvalue.ljust(len(self._ctrl_constraints._mask)) +## dbg('old value: "%s"' % value) +## dbg('new value: "%s"' % newvalue) + try: + self._ChangeValue(newvalue) + except Exception, e: +## dbg('exception raised:', e, 'resetting to initial value') + self._SetInitialValue() + + elif len(value) > len(self._ctrl_constraints._mask): + newvalue = value + if not self._useParens and newvalue[-1] == ' ': + newvalue = newvalue[:-1] + if not self._signOk and len(newvalue) > len(self._ctrl_constraints._mask): + newvalue = newvalue[1:] + if not self._signOk: + newvalue, signpos, right_signpos = self._getSignedValue(newvalue) + +## dbg('old value: "%s"' % value) +## dbg('new value: "%s"' % newvalue) + try: + self._ChangeValue(newvalue) + except Exception, e: +## dbg('exception raised:', e, 'resetting to initial value') + self._SetInitialValue() + elif not self._signOk and ('(' in value or '-' in value): + newvalue, signpos, right_signpos = self._getSignedValue(value) +## dbg('old value: "%s"' % value) +## dbg('new value: "%s"' % newvalue) + try: + self._ChangeValue(newvalue) + except e: +## dbg('exception raised:', e, 'resetting to initial value') + self._SetInitialValue() + + # Replace up/down arrow default handling: + # make down act like tab, up act like shift-tab: + +#### dbg('Registering numeric navigation and control handlers (if not already set)') + if not self._keyhandlers.has_key(wx.WXK_DOWN): + self._SetKeycodeHandler(wx.WXK_DOWN, self._OnChangeField) + if not self._keyhandlers.has_key(wx.WXK_NUMPAD_DOWN): + self._SetKeycodeHandler(wx.WXK_DOWN, self._OnChangeField) + if not self._keyhandlers.has_key(wx.WXK_UP): + self._SetKeycodeHandler(wx.WXK_UP, self._OnUpNumeric) # (adds "shift" to up arrow, and calls _OnChangeField) + if not self._keyhandlers.has_key(wx.WXK_NUMPAD_UP): + self._SetKeycodeHandler(wx.WXK_UP, self._OnUpNumeric) # (adds "shift" to up arrow, and calls _OnChangeField) + + # On ., truncate contents right of cursor to decimal point (if any) + # leaves cursor after decimal point if floating point, otherwise at 0. + if not self._keyhandlers.has_key(ord(self._decimalChar)) or self._keyhandlers[ord(self._decimalChar)] != self._OnDecimalPoint: + self._SetKeyHandler(self._decimalChar, self._OnDecimalPoint) + + if not self._keyhandlers.has_key(ord(self._shiftDecimalChar)) or self._keyhandlers[ord(self._shiftDecimalChar)] != self._OnChangeField: + self._SetKeyHandler(self._shiftDecimalChar, self._OnChangeField) # (Shift-'.' == '>' on US keyboards) + + # Allow selective insert of groupchar in numbers: + if not self._keyhandlers.has_key(ord(self._fields[0]._groupChar)) or self._keyhandlers[ord(self._fields[0]._groupChar)] != self._OnGroupChar: + self._SetKeyHandler(self._fields[0]._groupChar, self._OnGroupChar) + +## dbg(indent=0, suspend=0) + + + def _SetInitialValue(self, value=""): + """ + fills the control with the generated or supplied default value. + It will also set/reset the font if necessary and apply + formatting to the control at this time. + """ +## dbg('MaskedEditMixin::_SetInitialValue("%s")' % value, indent=1) + if not value: + self._prevValue = self._curValue = self._template + # don't apply external validation rules in this case, as template may + # not coincide with "legal" value... + try: + if isinstance(self, wx.TextCtrl): + self._ChangeValue(self._curValue) # note the use of "raw" ._ChangeValue()... + else: + self._SetValue(self._curValue) # note the use of "raw" ._SetValue()... + except Exception, e: +## dbg('exception thrown:', e, indent=0) + raise + else: + # Otherwise apply validation as appropriate to passed value: +#### dbg('value = "%s", length:' % value, len(value)) + self._prevValue = self._curValue = value + try: + if isinstance(self, wx.TextCtrl): + self.ChangeValue(value) # use public (validating) .SetValue() + else: + self.SetValue(value) + except Exception, e: +## dbg('exception thrown:', e, indent=0) + raise + + + # Set value/type-specific formatting + self._applyFormatting() +## dbg(indent=0) + + + def _calcSize(self, size=None): + """ Calculate automatic size if allowed; must be called after the base control is instantiated""" +#### dbg('MaskedEditMixin::_calcSize', indent=1) + cont = (size is None or size == wx.DefaultSize) + + if cont and self._autofit: +#### dbg('isinstance(self, wx.lib.masked.numctrl.NumCtrl): "%s"' % self.__class__) + # sizing of numctrl when using proportional font and + # wxPython 2.9 is not working when using "M" + # GetTextExtent returns a width which is way to large + # instead we use '9' for numctrl and a selection of + # characters instead of just 'M' for textctrl and combobox + # where the mask is larger then 10 characters long + if isinstance(self, wx.lib.masked.numctrl.NumCtrl): + sizing_text = '9' * self._masklength + wAdjust = 8 + elif isinstance(self, wx.lib.masked.combobox.ComboBox): + if self._masklength > 10: + sizing_text = 'FDSJKLREUI' * (self._masklength/10) + wAdjust = 26 + else: + sizing_text = 'M' * self._masklength + wAdjust = 4 + else: + if self._masklength > 10: + sizing_text = 'FDSJKLREUI' * (self._masklength/10) + else: + sizing_text = 'M' * self._masklength + wAdjust = 4 + if wx.Platform != "__WXMSW__": # give it a little extra space + sizing_text += 'M' + if wx.Platform == "__WXMAC__": # give it even a little more... + sizing_text += 'M' +#### dbg('len(sizing_text):', len(sizing_text), 'sizing_text: "%s"' % sizing_text) + w, h = self.GetTextExtent(sizing_text) + size = (w+wAdjust, self.GetSize().height) +#### dbg('size:', size, indent=0) + return size + + + def _setFont(self): + """ Set the control's font typeface -- pass the font name as str.""" +#### dbg('MaskedEditMixin::_setFont', indent=1) + if not self._useFixedWidthFont: + self._font = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT) + else: + font = self.GetFont() # get size, weight, etc from current font + points = font.GetPointSize() + if 'wxMac' in wx.PlatformInfo \ + and self.GetWindowVariant() == wx.WINDOW_VARIANT_MINI: + points -= 1 + + # Set to teletype font (guaranteed to be mappable to all wxWindows + # platforms: + self._font = wx.Font( points, wx.TELETYPE, font.GetStyle(), + font.GetWeight(), font.GetUnderlined()) +#### dbg('font string: "%s"' % font.GetNativeFontInfo().ToString()) + + self.SetFont(self._font) +#### dbg(indent=0) + + + def _OnTextChange(self, event): + """ + Handler for EVT_TEXT event. + self._Change() is provided for subclasses, and may return False to + skip this method logic. This function returns True if the event + detected was a legitimate event, or False if it was a "bogus" + EVT_TEXT event. (NOTE: There is currently an issue with calling + .SetValue from within the EVT_CHAR handler that causes duplicate + EVT_TEXT events for the same change.) + """ + newvalue = self._GetValue() +## dbg('MaskedEditMixin::_OnTextChange: value: "%s"' % newvalue, indent=1) + bValid = False + if self._ignoreChange: # ie. if an "intermediate text change event" +## dbg(indent=0) + return bValid + + ##! WS: For some inexplicable reason, every wx.TextCtrl.SetValue + ## call is generating two (2) EVT_TEXT events. On certain platforms, + ## (eg. linux/GTK) the 1st is an empty string value. + ## This is the only mechanism I can find to mask this problem: + if newvalue == self._curValue or len(newvalue) == 0: +## dbg('ignoring bogus text change event', indent=0) + pass + else: +## dbg('curvalue: "%s", newvalue: "%s", len(newvalue): %d' % (self._curValue, newvalue, len(newvalue))) + if self._Change(): + if self._signOk and self._isNeg and newvalue.find('-') == -1 and newvalue.find('(') == -1: +## dbg('clearing self._isNeg') + self._isNeg = False + text, self._signpos, self._right_signpos = self._getSignedValue() + self._CheckValid() # Recolor control as appropriate +## dbg('calling event.Skip()') + event.Skip() + bValid = True + self._prevValue = self._curValue # save for undo + self._curValue = newvalue # Save last seen value for next iteration +## dbg(indent=0) + return bValid + + + def _OnKeyDown(self, event): + """ + This function allows the control to capture Ctrl-events like Ctrl-tab, + that are not normally seen by the "cooked" EVT_CHAR routine. + """ + # Get keypress value, adjusted by control options (e.g. convert to upper etc) + key = event.GetKeyCode() + if key in self._nav and event.ControlDown(): + # then this is the only place we will likely see these events; + # process them now: +## dbg('MaskedEditMixin::OnKeyDown: calling _OnChar') + self._OnChar(event) + return + # else allow regular EVT_CHAR key processing + event.Skip() + + + def _OnChar(self, event): + """ + This is the engine of MaskedEdit controls. It examines each keystroke, + decides if it's allowed, where it should go or what action to take. + """ +## dbg('MaskedEditMixin::_OnChar', indent=1) + + # Get keypress value, adjusted by control options (e.g. convert to upper etc) + key = event.GetKeyCode() + orig_pos = self._GetInsertionPoint() + orig_value = self._GetValue() +## dbg('keycode = ', key) +## dbg('current pos = ', orig_pos) +## dbg('current selection = ', self._GetSelection()) + + if not self._Keypress(key): +## dbg(indent=0) + return + + # If no format string for this control, or the control is marked as "read-only", + # skip the rest of the special processing, and just "do the standard thing:" + if not self._mask or not self._IsEditable(): + event.Skip() +## dbg(indent=0) + return + + # Process navigation and control keys first, with + # position/selection unadulterated: + if key in self._nav + self._control: + if self._keyhandlers.has_key(key): + keep_processing = self._keyhandlers[key](event) + if self._GetValue() != orig_value: + self.modified = True + if not keep_processing: +## dbg(indent=0) + return + self._applyFormatting() +## dbg(indent=0) + return + + # Else... adjust the position as necessary for next input key, + # and determine resulting selection: + pos = self._adjustPos( orig_pos, key ) ## get insertion position, adjusted as needed + sel_start, sel_to = self._GetSelection() ## check for a range of selected text +## dbg("pos, sel_start, sel_to:", pos, sel_start, sel_to) + + keep_processing = True + # Capture user past end of format field + if pos > len(self.maskdict): +## dbg("field length exceeded:",pos) + keep_processing = False + + key = self._adjustKey(pos, key) # apply formatting constraints to key: + + if self._keyhandlers.has_key(key): + # there's an override for default behavior; use override function instead +## dbg('using supplied key handler:', self._keyhandlers[key]) + keep_processing = self._keyhandlers[key](event) + if self._GetValue() != orig_value: + self.modified = True + if not keep_processing: +## dbg(indent=0) + return + # else skip default processing, but do final formatting + if key in wx_control_keycodes: +## dbg('key in wx_control_keycodes') + event.Skip() # non-printable; let base control handle it + keep_processing = False + else: + field = self._FindField(pos) + + if 'unicode' in wx.PlatformInfo: + if key < 256: + char = chr(key) # (must work if we got this far) + char = char.decode(self._defaultEncoding) + else: + char = unichr(event.GetUnicodeKey()) +## dbg('unicode char:', char) + excludes = u'' + if type(field._excludeChars) != types.UnicodeType: + excludes += field._excludeChars.decode(self._defaultEncoding) + if type(self._ctrl_constraints) != types.UnicodeType: + excludes += self._ctrl_constraints._excludeChars.decode(self._defaultEncoding) + else: + char = chr(key) # (must work if we got this far) + excludes = field._excludeChars + self._ctrl_constraints._excludeChars + +## dbg("key ='%s'" % chr(key)) + if chr(key) == ' ': +## dbg('okSpaces?', field._okSpaces) + pass + + + if char in excludes: + keep_processing = False + + if keep_processing and self._isCharAllowed( char, pos, checkRegex = True ): +## dbg("key allowed by mask") + # insert key into candidate new value, but don't change control yet: + oldstr = self._GetValue() + newstr, newpos, new_select_to, match_field, match_index = self._insertKey( + char, pos, sel_start, sel_to, self._GetValue(), allowAutoSelect = True) +## dbg("str with '%s' inserted:" % char, '"%s"' % newstr) + if self._ctrl_constraints._validRequired and not self.IsValid(newstr): +## dbg('not valid; checking to see if adjusted string is:') + keep_processing = False + if self._isFloat and newstr != self._template: + newstr = self._adjustFloat(newstr) +## dbg('adjusted str:', newstr) + if self.IsValid(newstr): +## dbg("it is!") + keep_processing = True + wx.CallAfter(self._SetInsertionPoint, self._decimalpos) + if not keep_processing: +## dbg("key disallowed by validation") + if not wx.Validator_IsSilent() and orig_pos == pos: + wx.Bell() + + if keep_processing: + unadjusted = newstr + + # special case: adjust date value as necessary: + if self._isDate and newstr != self._template: + newstr = self._adjustDate(newstr) +## dbg('adjusted newstr:', newstr) + + if newstr != orig_value: + self.modified = True + + wx.CallAfter(self._SetValue, newstr) + + # Adjust insertion point on date if just entered 2 digit year, and there are now 4 digits: + if not self.IsDefault() and self._isDate and self._4digityear: + year2dig = self._dateExtent - 2 + if pos == year2dig and unadjusted[year2dig] != newstr[year2dig]: + newpos = pos+2 + +## dbg('queuing insertion point: (%d)' % newpos) + wx.CallAfter(self._SetInsertionPoint, newpos) + + if match_field is not None: +## dbg('matched field') + self._OnAutoSelect(match_field, match_index) + + if new_select_to != newpos: +## dbg('queuing selection: (%d, %d)' % (newpos, new_select_to)) + wx.CallAfter(self._SetSelection, newpos, new_select_to) + else: + newfield = self._FindField(newpos) + if newfield != field and newfield._selectOnFieldEntry: +## dbg('queuing insertion point: (%d)' % newfield._extent[0]) + wx.CallAfter(self._SetInsertionPoint, newfield._extent[0]) +## dbg('queuing selection: (%d, %d)' % (newfield._extent[0], newfield._extent[1])) + wx.CallAfter(self._SetSelection, newfield._extent[0], newfield._extent[1]) + else: + wx.CallAfter(self._SetSelection, newpos, new_select_to) + keep_processing = False + + elif keep_processing: +## dbg('char not allowed') + keep_processing = False + if (not wx.Validator_IsSilent()) and orig_pos == pos: + wx.Bell() + + self._applyFormatting() + + # Move to next insertion point + if keep_processing and key not in self._nav: + pos = self._GetInsertionPoint() + next_entry = self._findNextEntry( pos ) + if pos != next_entry: +## dbg("moving from %(pos)d to next valid entry: %(next_entry)d" % locals()) + wx.CallAfter(self._SetInsertionPoint, next_entry ) + + if self._isTemplateChar(pos): + self._AdjustField(pos) +## dbg(indent=0) + + + def _FindFieldExtent(self, pos=None, getslice=False, value=None): + """ returns editable extent of field corresponding to + position pos, and, optionally, the contents of that field + in the control or the value specified. + Template chars are bound to the preceding field. + For masks beginning with template chars, these chars are ignored + when calculating the current field. + + Eg: with template (###) ###-####, + >>> self._FindFieldExtent(pos=0) + 1, 4 + >>> self._FindFieldExtent(pos=1) + 1, 4 + >>> self._FindFieldExtent(pos=5) + 1, 4 + >>> self._FindFieldExtent(pos=6) + 6, 9 + >>> self._FindFieldExtent(pos=10) + 10, 14 + etc. + """ +## dbg('MaskedEditMixin::_FindFieldExtent(pos=%s, getslice=%s)' % (str(pos), str(getslice)) ,indent=1) + + field = self._FindField(pos) + if not field: + if getslice: + return None, None, "" + else: + return None, None + edit_start, edit_end = field._extent + if getslice: + if value is None: value = self._GetValue() + slice = value[edit_start:edit_end] +## dbg('edit_start:', edit_start, 'edit_end:', edit_end, 'slice: "%s"' % slice) +## dbg(indent=0) + return edit_start, edit_end, slice + else: +## dbg('edit_start:', edit_start, 'edit_end:', edit_end) +## dbg(indent=0) + return edit_start, edit_end + + + def _FindField(self, pos=None): + """ + Returns the field instance in which pos resides. + Template chars are bound to the preceding field. + For masks beginning with template chars, these chars are ignored + when calculating the current field. + + """ +#### dbg('MaskedEditMixin::_FindField(pos=%s)' % str(pos) ,indent=1) + if pos is None: pos = self._GetInsertionPoint() + elif pos < 0 or pos > self._masklength: + raise IndexError('position %s out of range of control' % str(pos)) + + if len(self._fields) == 0: +## dbg(indent=0) + return None + + # else... +#### dbg(indent=0) + return self._fields[self._lookupField[pos]] + + + def ClearValue(self): + """ Blanks the current control value by replacing it with the default value.""" +## dbg("MaskedEditMixin::ClearValue - value reset to default value (template)") + self._SetValue( self._template ) + self._SetInsertionPoint(0) + self.Refresh() + + def ClearValueAlt(self): + """ Blanks the current control value by replacing it with the default value. + Using ChangeValue, so not to fire a change event""" +## dbg("MaskedEditMixin::ClearValueAlt - value reset to default value (template)") + self._ChangeValue( self._template ) + self._SetInsertionPoint(0) + self.Refresh() + + def _baseCtrlEventHandler(self, event): + """ + This function is used whenever a key should be handled by the base control. + """ + event.Skip() + return False + + + def _OnUpNumeric(self, event): + """ + Makes up-arrow act like shift-tab should; ie. take you to start of + previous field. + """ +## dbg('MaskedEditMixin::_OnUpNumeric', indent=1) + event.shiftDown = 1 +## dbg('event.ShiftDown()?', event.ShiftDown()) + self._OnChangeField(event) +## dbg(indent=0) + + + def _OnArrow(self, event): + """ + Used in response to left/right navigation keys; makes these actions skip + over mask template chars. + """ +## dbg("MaskedEditMixin::_OnArrow", indent=1) + pos = self._GetInsertionPoint() + keycode = event.GetKeyCode() + sel_start, sel_to = self._GetSelection() + entry_end = self._goEnd(getPosOnly=True) + if keycode in (wx.WXK_RIGHT, wx.WXK_DOWN, wx.WXK_NUMPAD_RIGHT, wx.WXK_NUMPAD_DOWN): + if( ( not self._isTemplateChar(pos) and pos+1 > entry_end) + or ( self._isTemplateChar(pos) and pos >= entry_end) ): +## dbg("can't advance", indent=0) + return False + elif self._isTemplateChar(pos): + self._AdjustField(pos) + elif keycode in (wx.WXK_LEFT, wx.WXK_UP, wx.WXK_NUMPAD_LEFT, wx.WXK_NUMPAD_UP) and sel_start == sel_to and pos > 0 and self._isTemplateChar(pos-1): +## dbg('adjusting field') + self._AdjustField(pos) + + # treat as shifted up/down arrows as tab/reverse tab: + if event.ShiftDown() and keycode in (wx.WXK_UP, wx.WXK_DOWN, wx.WXK_NUMPAD_UP, wx.WXK_NUMPAD_DOWN): + # remove "shifting" and treat as (forward) tab: + event.shiftDown = False + keep_processing = self._OnChangeField(event) + + elif self._FindField(pos)._selectOnFieldEntry: + if( keycode in (wx.WXK_UP, wx.WXK_LEFT, wx.WXK_NUMPAD_UP, wx.WXK_NUMPAD_LEFT) + and sel_start != 0 + and self._isTemplateChar(sel_start-1) + and sel_start != self._masklength + and not self._signOk and not self._useParens): + + # call _OnChangeField to handle "ctrl-shifted event" + # (which moves to previous field and selects it.) + event.shiftDown = True + event.sontrolDown = True + keep_processing = self._OnChangeField(event) + elif( keycode in (wx.WXK_DOWN, wx.WXK_RIGHT, wx.WXK_NUMPAD_DOWN, wx.WXK_NUMPAD_RIGHT) + and sel_to != self._masklength + and self._isTemplateChar(sel_to)): + + # when changing field to the right, ensure don't accidentally go left instead + event.shiftDown = False + keep_processing = self._OnChangeField(event) + else: + # treat arrows as normal, allowing selection + # as appropriate: +## dbg('using base ctrl event processing') + event.Skip() + else: + if( (sel_to == self._fields[0]._extent[0] and keycode in (wx.WXK_LEFT, wx.WXK_NUMPAD_LEFT) ) + or (sel_to == self._masklength and keycode in (wx.WXK_RIGHT, wx.WXK_NUMPAD_RIGHT) ) ): + if not wx.Validator_IsSilent(): + wx.Bell() + else: + # treat arrows as normal, allowing selection + # as appropriate: +## dbg('using base event processing') + event.Skip() + + keep_processing = False +## dbg(indent=0) + return keep_processing + + + def _OnCtrl_S(self, event): + """ Default Ctrl-S handler; prints value information if demo enabled. """ +## dbg("MaskedEditMixin::_OnCtrl_S") + if self._demo: + print 'MaskedEditMixin.GetValue() = "%s"\nMaskedEditMixin.GetPlainValue() = "%s"' % (self.GetValue(), self.GetPlainValue()) + print "Valid? => " + str(self.IsValid()) + print "Current field, start, end, value =", str( self._FindFieldExtent(getslice=True)) + return False + + + def _OnCtrl_X(self, event=None): + """ Handles ctrl-x keypress in control and Cut operation on context menu. + Should return False to skip other processing. """ +## dbg("MaskedEditMixin::_OnCtrl_X", indent=1) + self.Cut() +## dbg(indent=0) + return False + + def _OnCtrl_C(self, event=None): + """ Handles ctrl-C keypress in control and Copy operation on context menu. + Uses base control handling. Should return False to skip other processing.""" + self.Copy() + return False + + def _OnCtrl_V(self, event=None): + """ Handles ctrl-V keypress in control and Paste operation on context menu. + Should return False to skip other processing. """ +## dbg("MaskedEditMixin::_OnCtrl_V", indent=1) + self.Paste() +## dbg(indent=0) + return False + + def _OnInsert(self, event=None): + """ Handles shift-insert and control-insert operations (paste and copy, respectively)""" +## dbg("MaskedEditMixin::_OnInsert", indent=1) + if event and isinstance(event, wx.KeyEvent): + if event.ShiftDown(): + self.Paste() + elif event.ControlDown(): + self.Copy() + # (else do nothing) + # (else do nothing) +## dbg(indent=0) + return False + + def _OnDelete(self, event=None): + """ Handles shift-delete and delete operations (cut and erase, respectively)""" +## dbg("MaskedEditMixin::_OnDelete", indent=1) + if event and isinstance(event, wx.KeyEvent): + if event.ShiftDown(): + self.Cut() + else: + self._OnErase(event) + else: + self._OnErase(event) +## dbg(indent=0) + return False + + def _OnCtrl_Z(self, event=None): + """ Handles ctrl-Z keypress in control and Undo operation on context menu. + Should return False to skip other processing. """ +## dbg("MaskedEditMixin::_OnCtrl_Z", indent=1) + self.Undo() +## dbg(indent=0) + return False + + def _OnCtrl_A(self,event=None): + """ Handles ctrl-a keypress in control. Should return False to skip other processing. """ + end = self._goEnd(getPosOnly=True) + if not event or (isinstance(event, wx.KeyEvent) and event.ShiftDown()): + wx.CallAfter(self._SetInsertionPoint, 0) + wx.CallAfter(self._SetSelection, 0, self._masklength) + else: + wx.CallAfter(self._SetInsertionPoint, 0) + wx.CallAfter(self._SetSelection, 0, end) + return False + + + def _OnErase(self, event=None, just_return_value=False): + """ Handles backspace and delete keypress in control. Should return False to skip other processing.""" +## dbg("MaskedEditMixin::_OnErase", indent=1) + sel_start, sel_to = self._GetSelection() ## check for a range of selected text + + if event is None: # called as action routine from Cut() operation. + key = wx.WXK_DELETE + else: + key = event.GetKeyCode() + + field = self._FindField(sel_to) + start, end = field._extent + value = self._GetValue() + oldstart = sel_start + + # If trying to erase beyond "legal" bounds, disallow operation: + if( (sel_to == 0 and key == wx.WXK_BACK) + or (self._signOk and sel_to == 1 and value[0] == ' ' and key == wx.WXK_BACK) + or (sel_to == self._masklength and sel_start == sel_to and key in (wx.WXK_DELETE, wx.WXK_NUMPAD_DELETE) and not field._insertRight) + or (self._signOk and self._useParens + and sel_start == sel_to + and sel_to == self._masklength - 1 + and value[sel_to] == ' ' and key in (wx.WXK_DELETE, wx.WXK_NUMPAD_DELETE) and not field._insertRight) ): + if not wx.Validator_IsSilent(): + wx.Bell() +## dbg(indent=0) + return False + + + if( field._insertRight # an insert-right field + and value[start:end] != self._template[start:end] # and field not empty + and sel_start >= start # and selection starts in field + and ((sel_to == sel_start # and no selection + and sel_to == end # and cursor at right edge + and key in (wx.WXK_BACK, wx.WXK_DELETE, wx.WXK_NUMPAD_DELETE)) # and either delete or backspace key + or # or + (key == wx.WXK_BACK # backspacing + and (sel_to == end # and selection ends at right edge + or sel_to < end and field._allowInsert)) ) ): # or allow right insert at any point in field + +## dbg('delete left') + # if backspace but left of cursor is empty, adjust cursor right before deleting + while( key == wx.WXK_BACK + and sel_start == sel_to + and sel_start < end + and value[start:sel_start] == self._template[start:sel_start]): + sel_start += 1 + sel_to = sel_start + +## dbg('sel_start, start:', sel_start, start) + + if sel_start == sel_to: + keep = sel_start -1 + else: + keep = sel_start + newfield = value[start:keep] + value[sel_to:end] + + # handle sign char moving from outside field into the field: + move_sign_into_field = False + if not field._padZero and self._signOk and self._isNeg and value[0] in ('-', '('): + signchar = value[0] + newfield = signchar + newfield + move_sign_into_field = True +## dbg('cut newfield: "%s"' % newfield) + + # handle what should fill in from the left: + left = "" + for i in range(start, end - len(newfield)): + if field._padZero: + left += '0' + elif( self._signOk and self._isNeg and i == 1 + and ((self._useParens and newfield.find('(') == -1) + or (not self._useParens and newfield.find('-') == -1)) ): + left += ' ' + else: + left += self._template[i] # this can produce strange results in combination with default values... + newfield = left + newfield +## dbg('filled newfield: "%s"' % newfield) + + newstr = value[:start] + newfield + value[end:] + + # (handle sign located in "mask position" in front of field prior to delete) + if move_sign_into_field: + newstr = ' ' + newstr[1:] + pos = sel_to + else: + # handle erasure of (left) sign, moving selection accordingly... + if self._signOk and sel_start == 0: + newstr = value = ' ' + value[1:] + sel_start += 1 + + if field._allowInsert and sel_start >= start: + # selection (if any) falls within current insert-capable field: + select_len = sel_to - sel_start + # determine where cursor should end up: + if key == wx.WXK_BACK: + if select_len == 0: + newpos = sel_start -1 + else: + newpos = sel_start + erase_to = sel_to + else: + newpos = sel_start + if sel_to == sel_start: + erase_to = sel_to + 1 + else: + erase_to = sel_to + + if self._isTemplateChar(newpos) and select_len == 0: + if self._signOk: + if value[newpos] in ('(', '-'): + newpos += 1 # don't move cusor + newstr = ' ' + value[newpos:] + elif value[newpos] == ')': + # erase right sign, but don't move cursor; (matching left sign handled later) + newstr = value[:newpos] + ' ' + else: + # no deletion; just move cursor + newstr = value + else: + # no deletion; just move cursor + newstr = value + else: + if erase_to > end: erase_to = end + erase_len = erase_to - newpos + + left = value[start:newpos] +## dbg("retained ='%s'" % value[erase_to:end], 'sel_to:', sel_to, "fill: '%s'" % self._template[end - erase_len:end]) + right = value[erase_to:end] + self._template[end-erase_len:end] + pos_adjust = 0 + if field._alignRight: + rstripped = right.rstrip() + if rstripped != right: + pos_adjust = len(right) - len(rstripped) + right = rstripped + + if not field._insertRight and value[-1] == ')' and end == self._masklength - 1: + # need to shift ) into the field: + right = right[:-1] + ')' + value = value[:-1] + ' ' + + newfield = left+right + if pos_adjust: + newfield = newfield.rjust(end-start) + newpos += pos_adjust +## dbg("left='%s', right ='%s', newfield='%s'" %(left, right, newfield)) + newstr = value[:start] + newfield + value[end:] + + pos = newpos + + else: + if sel_start == sel_to: +## dbg("current sel_start, sel_to:", sel_start, sel_to) + if key == wx.WXK_BACK: + sel_start, sel_to = sel_to-1, sel_to-1 +## dbg("new sel_start, sel_to:", sel_start, sel_to) + + if field._padZero and not value[start:sel_to].replace('0', '').replace(' ','').replace(field._fillChar, ''): + # preceding chars (if any) are zeros, blanks or fillchar; new char should be 0: + newchar = '0' + else: + newchar = self._template[sel_to] ## get an original template character to "clear" the current char +## dbg('value = "%s"' % value, 'value[%d] = "%s"' %(sel_start, value[sel_start])) + + if self._isTemplateChar(sel_to): + if sel_to == 0 and self._signOk and value[sel_to] == '-': # erasing "template" sign char + newstr = ' ' + value[1:] + sel_to += 1 + elif self._signOk and self._useParens and (value[sel_to] == ')' or value[sel_to] == '('): + # allow "change sign" by removing both parens: + newstr = value[:self._signpos] + ' ' + value[self._signpos+1:-1] + ' ' + else: + newstr = value + newpos = sel_to + else: + if field._insertRight and sel_start == sel_to: + # force non-insert-right behavior, by selecting char to be replaced: + sel_to += 1 + newstr, ignore = self._insertKey(newchar, sel_start, sel_start, sel_to, value) + + else: + # selection made + newstr = self._eraseSelection(value, sel_start, sel_to) + + pos = sel_start # put cursor back at beginning of selection + + if self._signOk and self._useParens: + # account for resultant unbalanced parentheses: + left_signpos = newstr.find('(') + right_signpos = newstr.find(')') + + if left_signpos == -1 and right_signpos != -1: + # erased left-sign marker; get rid of right sign marker: + newstr = newstr[:right_signpos] + ' ' + newstr[right_signpos+1:] + + elif left_signpos != -1 and right_signpos == -1: + # erased right-sign marker; get rid of left-sign marker: + newstr = newstr[:left_signpos] + ' ' + newstr[left_signpos+1:] + +## dbg("oldstr:'%s'" % value, 'oldpos:', oldstart) +## dbg("newstr:'%s'" % newstr, 'pos:', pos) + + # if erasure results in an invalid field, disallow it: +## dbg('field._validRequired?', field._validRequired) +## dbg('field.IsValid("%s")?' % newstr[start:end], field.IsValid(newstr[start:end])) + if field._validRequired and not field.IsValid(newstr[start:end]): + if not wx.Validator_IsSilent(): + wx.Bell() +## dbg(indent=0) + return False + + # if erasure results in an invalid value, disallow it: + if self._ctrl_constraints._validRequired and not self.IsValid(newstr): + if not wx.Validator_IsSilent(): + wx.Bell() +## dbg(indent=0) + return False + + if just_return_value: +## dbg(indent=0) + return newstr + + # else... +## dbg('setting value (later) to', newstr) + wx.CallAfter(self._SetValue, newstr) +## dbg('setting insertion point (later) to', pos) + wx.CallAfter(self._SetInsertionPoint, pos) +## dbg(indent=0) + if newstr != value: + self.modified = True + return False + + + def _OnEnd(self,event): + """ Handles End keypress in control. Should return False to skip other processing. """ +## dbg("MaskedEditMixin::_OnEnd", indent=1) + pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode()) + if not event.ControlDown(): + end = self._masklength # go to end of control + if self._signOk and self._useParens: + end = end - 1 # account for reserved char at end + else: + end_of_input = self._goEnd(getPosOnly=True) + sel_start, sel_to = self._GetSelection() + if sel_to < pos: sel_to = pos + field = self._FindField(sel_to) + field_end = self._FindField(end_of_input) + + # pick different end point if either: + # - cursor not in same field + # - or at or past last input already + # - or current selection = end of current field: +#### dbg('field != field_end?', field != field_end) +#### dbg('sel_to >= end_of_input?', sel_to >= end_of_input) + if field != field_end or sel_to >= end_of_input: + edit_start, edit_end = field._extent +#### dbg('edit_end:', edit_end) +#### dbg('sel_to:', sel_to) +#### dbg('sel_to == edit_end?', sel_to == edit_end) +#### dbg('field._index < self._field_indices[-1]?', field._index < self._field_indices[-1]) + + if sel_to == edit_end and field._index < self._field_indices[-1]: + edit_start, edit_end = self._FindFieldExtent(self._findNextEntry(edit_end)) # go to end of next field: + end = edit_end +## dbg('end moved to', end) + + elif sel_to == edit_end and field._index == self._field_indices[-1]: + # already at edit end of last field; select to end of control: + end = self._masklength +## dbg('end moved to', end) + else: + end = edit_end # select to end of current field +## dbg('end moved to ', end) + else: + # select to current end of input + end = end_of_input + + +#### dbg('pos:', pos, 'end:', end) + + if event.ShiftDown(): + if not event.ControlDown(): +## dbg("shift-end; select to end of control") + pass + else: +## dbg("shift-ctrl-end; select to end of non-whitespace") + pass + wx.CallAfter(self._SetInsertionPoint, pos) + wx.CallAfter(self._SetSelection, pos, end) + else: + if not event.ControlDown(): +## dbg('go to end of control:') + pass + wx.CallAfter(self._SetInsertionPoint, end) + wx.CallAfter(self._SetSelection, end, end) + +## dbg(indent=0) + return False + + + def _OnReturn(self, event): + """ + Swallows the return, issues a Navigate event instead, since + masked controls are "single line" by defn. + """ +## dbg('MaskedEditMixin::OnReturn') + self.Navigate(True) + return False + + + def _OnHome(self,event): + """ Handles Home keypress in control. Should return False to skip other processing.""" +## dbg("MaskedEditMixin::_OnHome", indent=1) + pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode()) + sel_start, sel_to = self._GetSelection() + + # There are 5 cases here: + + # 1) shift: select from start of control to end of current + # selection. + if event.ShiftDown() and not event.ControlDown(): +## dbg("shift-home; select to start of control") + start = 0 + end = sel_start + + # 2) no shift, no control: move cursor to beginning of control. + elif not event.ControlDown(): +## dbg("home; move to start of control") + start = 0 + end = 0 + + # 3) No shift, control: move cursor back to beginning of field; if + # there already, go to beginning of previous field. + # 4) shift, control, start of selection not at beginning of control: + # move sel_start back to start of field; if already there, go to + # start of previous field. + elif( event.ControlDown() + and (not event.ShiftDown() + or (event.ShiftDown() and sel_start > 0) ) ): + if len(self._field_indices) > 1: + field = self._FindField(sel_start) + start, ignore = field._extent + if sel_start == start and field._index != self._field_indices[0]: # go to start of previous field: + start, ignore = self._FindFieldExtent(sel_start-1) + elif sel_start == start: + start = 0 # go to literal beginning if edit start + # not at that point + end_of_field = True + + else: + start = 0 + + if not event.ShiftDown(): +## dbg("ctrl-home; move to beginning of field") + end = start + else: +## dbg("shift-ctrl-home; select to beginning of field") + end = sel_to + + else: + # 5) shift, control, start of selection at beginning of control: + # unselect by moving sel_to backward to beginning of current field; + # if already there, move to start of previous field. + start = sel_start + if len(self._field_indices) > 1: + # find end of previous field: + field = self._FindField(sel_to) + if sel_to > start and field._index != self._field_indices[0]: + ignore, end = self._FindFieldExtent(field._extent[0]-1) + else: + end = start + end_of_field = True + else: + end = start + end_of_field = False +## dbg("shift-ctrl-home; unselect to beginning of field") + +## dbg('queuing new sel_start, sel_to:', (start, end)) + wx.CallAfter(self._SetInsertionPoint, start) + wx.CallAfter(self._SetSelection, start, end) +## dbg(indent=0) + return False + + + def _OnChangeField(self, event): + """ + Primarily handles TAB events, but can be used for any key that + designer wants to change fields within a masked edit control. + """ +## dbg('MaskedEditMixin::_OnChangeField', indent = 1) + # determine end of current field: + pos = self._GetInsertionPoint() +## dbg('current pos:', pos) + sel_start, sel_to = self._GetSelection() + + if self._masklength < 0: # no fields; process tab normally + self._AdjustField(pos) + if event.GetKeyCode() == wx.WXK_TAB: +## dbg('tab to next ctrl') + # As of 2.5.2, you don't call event.Skip() to do + # this, but instead force explicit navigation, if + # wx.TE_PROCESS_TAB is used (like in the masked edits) + self.Navigate(True) + #else: do nothing +## dbg(indent=0) + return False + + field = self._FindField(sel_to) + index = field._index + field_start, field_end = field._extent + slice = self._GetValue()[field_start:field_end] + +## dbg('field._stopFieldChangeIfInvalid?', field._stopFieldChangeIfInvalid) +## dbg('field.IsValid(slice)?', field.IsValid(slice)) + + if field._stopFieldChangeIfInvalid and not field.IsValid(slice): +## dbg('field invalid; field change disallowed') + if not wx.Validator_IsSilent(): + wx.Bell() +## dbg(indent=0) + return False + + + if event.ShiftDown(): + + # "Go backward" + + # NOTE: doesn't yet work with SHIFT-tab under wx; the control + # never sees this event! (But I've coded for it should it ever work, + # and it *does* work for '.' in IpAddrCtrl.) + + if pos < field_start: +## dbg('cursor before 1st field; cannot change to a previous field') + if not wx.Validator_IsSilent(): + wx.Bell() +## dbg(indent=0) + return False + + if event.ControlDown(): +## dbg('queuing select to beginning of field:', field_start, pos) + wx.CallAfter(self._SetInsertionPoint, field_start) + wx.CallAfter(self._SetSelection, field_start, pos) +## dbg(indent=0) + return False + + elif index == 0: + # We're already in the 1st field; process shift-tab normally: + self._AdjustField(pos) + if event.GetKeyCode() == wx.WXK_TAB: +## dbg('tab to previous ctrl') + # As of 2.5.2, you don't call event.Skip() to do + # this, but instead force explicit navigation, if + # wx.TE_PROCESS_TAB is used (like in the masked edits) + self.Navigate(False) + else: +## dbg('position at beginning') + wx.CallAfter(self._SetInsertionPoint, field_start) +## dbg(indent=0) + return False + else: + # find beginning of previous field: + begin_prev = self._FindField(field_start-1)._extent[0] + self._AdjustField(pos) +## dbg('repositioning to', begin_prev) + wx.CallAfter(self._SetInsertionPoint, begin_prev) + if self._FindField(begin_prev)._selectOnFieldEntry: + edit_start, edit_end = self._FindFieldExtent(begin_prev) +## dbg('queuing selection to (%d, %d)' % (edit_start, edit_end)) + wx.CallAfter(self._SetInsertionPoint, edit_start) + wx.CallAfter(self._SetSelection, edit_start, edit_end) +## dbg(indent=0) + return False + + else: + # "Go forward" + if event.ControlDown(): +## dbg('queuing select to end of field:', pos, field_end) + wx.CallAfter(self._SetInsertionPoint, pos) + wx.CallAfter(self._SetSelection, pos, field_end) +## dbg(indent=0) + return False + else: + if pos < field_start: +## dbg('cursor before 1st field; go to start of field') + wx.CallAfter(self._SetInsertionPoint, field_start) + if field._selectOnFieldEntry: + wx.CallAfter(self._SetSelection, field_end, field_start) + else: + wx.CallAfter(self._SetSelection, field_start, field_start) + return False + # else... +## dbg('end of current field:', field_end) +## dbg('go to next field') + if field_end == self._fields[self._field_indices[-1]]._extent[1]: + self._AdjustField(pos) + if event.GetKeyCode() == wx.WXK_TAB: +## dbg('tab to next ctrl') + # As of 2.5.2, you don't call event.Skip() to do + # this, but instead force explicit navigation, if + # wx.TE_PROCESS_TAB is used (like in the masked edits) + self.Navigate(True) + else: +## dbg('position at end') + wx.CallAfter(self._SetInsertionPoint, field_end) +## dbg(indent=0) + return False + else: + # we have to find the start of the next field + next_pos = self._findNextEntry(field_end) + if next_pos == field_end: +## dbg('already in last field') + self._AdjustField(pos) + if event.GetKeyCode() == wx.WXK_TAB: +## dbg('tab to next ctrl') + # As of 2.5.2, you don't call event.Skip() to do + # this, but instead force explicit navigation, if + # wx.TE_PROCESS_TAB is used (like in the masked edits) + self.Navigate(True) + #else: do nothing +## dbg(indent=0) + return False + else: + self._AdjustField( pos ) + + # move cursor to appropriate point in the next field and select as necessary: + field = self._FindField(next_pos) + edit_start, edit_end = field._extent + if field._selectOnFieldEntry: +## dbg('move to ', next_pos) + wx.CallAfter(self._SetInsertionPoint, next_pos) + edit_start, edit_end = self._FindFieldExtent(next_pos) +## dbg('queuing select', edit_start, edit_end) + wx.CallAfter(self._SetSelection, edit_end, edit_start) + else: + if field._insertRight: + next_pos = field._extent[1] +## dbg('move to ', next_pos) + wx.CallAfter(self._SetInsertionPoint, next_pos) +## dbg(indent=0) + return False +## dbg(indent=0) + + + def _OnDecimalPoint(self, event): +## dbg('MaskedEditMixin::_OnDecimalPoint', indent=1) + field = self._FindField(self._GetInsertionPoint()) + start, end = field._extent + slice = self._GetValue()[start:end] + + if field._stopFieldChangeIfInvalid and not field.IsValid(slice): + if not wx.Validator_IsSilent(): + wx.Bell() + return False + + pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode()) + + if self._isFloat: ## handle float value, move to decimal place +## dbg('key == Decimal tab; decimal pos:', self._decimalpos) + value = self._GetValue() + if pos < self._decimalpos: + clipped_text = value[0:pos] + self._decimalChar + value[self._decimalpos+1:] +## dbg('value: "%s"' % self._GetValue(), "clipped_text:'%s'" % clipped_text) + newstr = self._adjustFloat(clipped_text) + else: + newstr = self._adjustFloat(value) + wx.CallAfter(self._SetValue, newstr) + fraction = self._fields[1] + start, end = fraction._extent + wx.CallAfter(self._SetInsertionPoint, start) + if fraction._selectOnFieldEntry: +## dbg('queuing selection after decimal point to:', (start, end)) + wx.CallAfter(self._SetSelection, end, start) + else: + wx.CallAfter(self._SetSelection, start, start) + keep_processing = False + + if self._isInt: ## handle integer value, truncate from current position +## dbg('key == Integer decimal event') + value = self._GetValue() + clipped_text = value[0:pos] +## dbg('value: "%s"' % self._GetValue(), "clipped_text:'%s'" % clipped_text) + newstr = self._adjustInt(clipped_text) +## dbg('newstr: "%s"' % newstr) + wx.CallAfter(self._SetValue, newstr) + newpos = len(newstr.rstrip()) + if newstr.find(')') != -1: + newpos -= 1 # (don't move past right paren) + wx.CallAfter(self._SetInsertionPoint, newpos) + wx.CallAfter(self._SetSelection, newpos, newpos) + keep_processing = False +## dbg(indent=0) + + + def _OnChangeSign(self, event): +## dbg('MaskedEditMixin::_OnChangeSign', indent=1) + key = event.GetKeyCode() + pos = self._adjustPos(self._GetInsertionPoint(), key) + value = self._eraseSelection() + integer = self._fields[0] + start, end = integer._extent + sel_start, sel_to = self._GetSelection() + +#### dbg('adjusted pos:', pos) + if chr(key) in ('-','+','(', ')') or (chr(key) == " " and pos == self._signpos): + cursign = self._isNeg +## dbg('cursign:', cursign) + if chr(key) in ('-','(', ')'): + if sel_start <= self._signpos: + self._isNeg = True + else: + self._isNeg = (not self._isNeg) ## flip value + else: + self._isNeg = False +## dbg('isNeg?', self._isNeg) + + text, self._signpos, self._right_signpos = self._getSignedValue(candidate=value) +## dbg('text:"%s"' % text, 'signpos:', self._signpos, 'right_signpos:', self._right_signpos) + if text is None: + text = value + + if self._isNeg and self._signpos is not None and self._signpos != -1: + if self._useParens and self._right_signpos is not None: + text = text[:self._signpos] + '(' + text[self._signpos+1:self._right_signpos] + ')' + text[self._right_signpos+1:] + else: + text = text[:self._signpos] + '-' + text[self._signpos+1:] + else: +#### dbg('self._isNeg?', self._isNeg, 'self.IsValid(%s)' % text, self.IsValid(text)) + if self._useParens: + text = text[:self._signpos] + ' ' + text[self._signpos+1:self._right_signpos] + ' ' + text[self._right_signpos+1:] + else: + text = text[:self._signpos] + ' ' + text[self._signpos+1:] +## dbg('clearing self._isNeg') + self._isNeg = False + + wx.CallAfter(self._SetValue, text) + wx.CallAfter(self._applyFormatting) +## dbg('pos:', pos, 'signpos:', self._signpos) + if pos == self._signpos or integer.IsEmpty(text[start:end]): + wx.CallAfter(self._SetInsertionPoint, self._signpos+1) + else: + wx.CallAfter(self._SetInsertionPoint, pos) + + keep_processing = False + else: + keep_processing = True +## dbg(indent=0) + return keep_processing + + + def _OnGroupChar(self, event): + """ + This handler is only registered if the mask is a numeric mask. + It allows the insertion of ',' or '.' if appropriate. + """ +## dbg('MaskedEditMixin::_OnGroupChar', indent=1) + keep_processing = True + pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode()) + sel_start, sel_to = self._GetSelection() + groupchar = self._fields[0]._groupChar + if not self._isCharAllowed(groupchar, pos, checkRegex=True): + keep_processing = False + if not wx.Validator_IsSilent(): + wx.Bell() + + if keep_processing: + newstr, newpos = self._insertKey(groupchar, pos, sel_start, sel_to, self._GetValue() ) +## dbg("str with '%s' inserted:" % groupchar, '"%s"' % newstr) + if self._ctrl_constraints._validRequired and not self.IsValid(newstr): + keep_processing = False + if not wx.Validator_IsSilent(): + wx.Bell() + + if keep_processing: + wx.CallAfter(self._SetValue, newstr) + wx.CallAfter(self._SetInsertionPoint, newpos) + keep_processing = False +## dbg(indent=0) + return keep_processing + + + def _findNextEntry(self,pos, adjustInsert=True): + """ Find the insertion point for the next valid entry character position.""" +## dbg('MaskedEditMixin::_findNextEntry', indent=1) + if self._isTemplateChar(pos) or pos in self._explicit_field_boundaries: # if changing fields, pay attn to flag + adjustInsert = adjustInsert + else: # else within a field; flag not relevant + adjustInsert = False + + while self._isTemplateChar(pos) and pos < self._masklength: + pos += 1 + + # if changing fields, and we've been told to adjust insert point, + # look at new field; if empty and right-insert field, + # adjust to right edge: + if adjustInsert and pos < self._masklength: + field = self._FindField(pos) + start, end = field._extent + slice = self._GetValue()[start:end] + if field._insertRight and field.IsEmpty(slice): + pos = end +## dbg('final pos:', pos, indent=0) + return pos + + + def _findNextTemplateChar(self, pos): + """ Find the position of the next non-editable character in the mask.""" + while not self._isTemplateChar(pos) and pos < self._masklength: + pos += 1 + return pos + + + def _OnAutoCompleteField(self, event): +## dbg('MaskedEditMixin::_OnAutoCompleteField', indent =1) + pos = self._GetInsertionPoint() + field = self._FindField(pos) + edit_start, edit_end, slice = self._FindFieldExtent(pos, getslice=True) + + match_index = None + keycode = event.GetKeyCode() + + if field._fillChar != ' ': + text = slice.replace(field._fillChar, '') + else: + text = slice + text = text.strip() + keep_processing = True # (assume True to start) +## dbg('field._hasList?', field._hasList) + if field._hasList: +## dbg('choices:', field._choices) +## dbg('compareChoices:', field._compareChoices) + choices, choice_required = field._compareChoices, field._choiceRequired + if keycode in (wx.WXK_PRIOR, wx.WXK_UP, wx.WXK_NUMPAD_PRIOR, wx.WXK_NUMPAD_UP): + direction = -1 + else: + direction = 1 + match_index, partial_match = self._autoComplete(direction, choices, text, compareNoCase=field._compareNoCase, current_index = field._autoCompleteIndex) + if( match_index is None + and (keycode in self._autoCompleteKeycodes + [wx.WXK_PRIOR, wx.WXK_NEXT, wx.WXK_NUMPAD_PRIOR, wx.WXK_NUMPAD_NEXT] + or (keycode in [wx.WXK_UP, wx.WXK_DOWN, wx.WXK_NUMPAD_UP, wx.WXK_NUMPAD_DOWN] and event.ShiftDown() ) ) ): + # Select the 1st thing from the list: + match_index = 0 + + if( match_index is not None + and ( keycode in self._autoCompleteKeycodes + [wx.WXK_PRIOR, wx.WXK_NEXT, wx.WXK_NUMPAD_PRIOR, wx.WXK_NUMPAD_NEXT] + or (keycode in [wx.WXK_UP, wx.WXK_DOWN, wx.WXK_NUMPAD_UP, wx.WXK_NUMPAD_DOWN] and event.ShiftDown()) + or (keycode in [wx.WXK_DOWN, wx.WXK_NUMPAD_DOWN] and partial_match) ) ): + + # We're allowed to auto-complete: +## dbg('match found') + value = self._GetValue() + newvalue = value[:edit_start] + field._choices[match_index] + value[edit_end:] +## dbg('setting value to "%s"' % newvalue) + self._SetValue(newvalue) + self._SetInsertionPoint(min(edit_end, len(newvalue.rstrip()))) + self._OnAutoSelect(field, match_index) + self._CheckValid() # recolor as appopriate + + + if keycode in (wx.WXK_UP, wx.WXK_DOWN, wx.WXK_LEFT, wx.WXK_RIGHT, + wx.WXK_NUMPAD_UP, wx.WXK_NUMPAD_DOWN, wx.WXK_NUMPAD_LEFT, wx.WXK_NUMPAD_RIGHT): + # treat as left right arrow if unshifted, tab/shift tab if shifted. + if event.ShiftDown(): + if keycode in (wx.WXK_DOWN, wx.WXK_RIGHT, wx.WXK_NUMPAD_DOWN, wx.WXK_NUMPAD_RIGHT): + # remove "shifting" and treat as (forward) tab: + event.shiftDown = False + keep_processing = self._OnChangeField(event) + else: + keep_processing = self._OnArrow(event) + # else some other key; keep processing the key + +## dbg('keep processing?', keep_processing, indent=0) + return keep_processing + + + def _OnAutoSelect(self, field, match_index = None): + """ + Function called if autoselect feature is enabled and entire control + is selected: + """ +## dbg('MaskedEditMixin::OnAutoSelect', field._index) + if match_index is not None: + field._autoCompleteIndex = match_index + + + def _autoComplete(self, direction, choices, value, compareNoCase, current_index): + """ + This function gets called in response to Auto-complete events. + It attempts to find a match to the specified value against the + list of choices; if exact match, the index of then next + appropriate value in the list, based on the given direction. + If not an exact match, it will return the index of the 1st value from + the choice list for which the partial value can be extended to match. + If no match found, it will return None. + The function returns a 2-tuple, with the 2nd element being a boolean + that indicates if partial match was necessary. + """ +## dbg('autoComplete(direction=', direction, 'choices=',choices, 'value=',value,'compareNoCase?', compareNoCase, 'current_index:', current_index, indent=1) + if value is None: +## dbg('nothing to match against', indent=0) + return (None, False) + + partial_match = False + + if compareNoCase: + value = value.lower() + + last_index = len(choices) - 1 + if value in choices: +## dbg('"%s" in', choices) + if current_index is not None and choices[current_index] == value: + index = current_index + else: + index = choices.index(value) + +## dbg('matched "%s" (%d)' % (choices[index], index)) + if direction == -1: +## dbg('going to previous') + if index == 0: index = len(choices) - 1 + else: index -= 1 + else: + if index == len(choices) - 1: index = 0 + else: index += 1 +## dbg('change value to "%s" (%d)' % (choices[index], index)) + match = index + else: + partial_match = True + value = value.strip() +## dbg('no match; try to auto-complete:') + match = None +## dbg('searching for "%s"' % value) + if current_index is None: + indices = range(len(choices)) + if direction == -1: + indices.reverse() + else: + if direction == 1: + indices = range(current_index +1, len(choices)) + range(current_index+1) +## dbg('range(current_index+1 (%d), len(choices) (%d)) + range(%d):' % (current_index+1, len(choices), current_index+1), indices) + else: + indices = range(current_index-1, -1, -1) + range(len(choices)-1, current_index-1, -1) +## dbg('range(current_index-1 (%d), -1) + range(len(choices)-1 (%d)), current_index-1 (%d):' % (current_index-1, len(choices)-1, current_index-1), indices) +#### dbg('indices:', indices) + for index in indices: + choice = choices[index] + if choice.find(value, 0) == 0: +## dbg('match found:', choice) + match = index + break + else: +## dbg('choice: "%s" - no match' % choice) + pass + if match is not None: +## dbg('matched', match) + pass + else: +## dbg('no match found') + pass +## dbg(indent=0) + return (match, partial_match) + + + def _AdjustField(self, pos): + """ + This function gets called by default whenever the cursor leaves a field. + The pos argument given is the char position before leaving that field. + By default, floating point, integer and date values are adjusted to be + legal in this function. Derived classes may override this function + to modify the value of the control in a different way when changing fields. + + NOTE: these change the value immediately, and restore the cursor to + the passed location, so that any subsequent code can then move it + based on the operation being performed. + """ + newvalue = value = self._GetValue() + field = self._FindField(pos) + start, end, slice = self._FindFieldExtent(getslice=True) + newfield = field._AdjustField(slice) + newvalue = value[:start] + newfield + value[end:] + + if self._isFloat and newvalue != self._template: + newvalue = self._adjustFloat(newvalue) + + if self._ctrl_constraints._isInt and value != self._template: + newvalue = self._adjustInt(value) + + if self._isDate and value != self._template: + newvalue = self._adjustDate(value, fixcentury=True) + if self._4digityear: + year2dig = self._dateExtent - 2 + if pos == year2dig and value[year2dig] != newvalue[year2dig]: + pos = pos+2 + + if newvalue != value: +## dbg('old value: "%s"\nnew value: "%s"' % (value, newvalue)) + self._SetValue(newvalue) + self._SetInsertionPoint(pos) + + + def _adjustKey(self, pos, key): + """ Apply control formatting to the key (e.g. convert to upper etc). """ + field = self._FindField(pos) + if field._forceupper and key in range(97,123): + key = ord( chr(key).upper()) + + if field._forcelower and key in range(65,90): + key = ord( chr(key).lower()) + + return key + + + def _adjustPos(self, pos, key): + """ + Checks the current insertion point position and adjusts it if + necessary to skip over non-editable characters. + """ +## dbg('_adjustPos', pos, key, indent=1) + sel_start, sel_to = self._GetSelection() + # If a numeric or decimal mask, and negatives allowed, reserve the + # first space for sign, and last one if using parens. + if( self._signOk + and ((pos == self._signpos and key in (ord('-'), ord('+'), ord(' ')) ) + or (self._useParens and pos == self._masklength -1))): +## dbg('adjusted pos:', pos, indent=0) + return pos + + if key not in self._nav: + field = self._FindField(pos) + +## dbg('field._insertRight?', field._insertRight) +## if self._signOk: dbg('self._signpos:', self._signpos) + if field._insertRight: # if allow right-insert + start, end = field._extent + slice = self._GetValue()[start:end].strip() + field_len = end - start + if pos == end: # if cursor at right edge of field + # if not filled or supposed to stay in field, keep current position +#### dbg('pos==end') +#### dbg('len (slice):', len(slice)) +#### dbg('field_len?', field_len) +#### dbg('pos==end; len (slice) < field_len?', len(slice) < field_len) +#### dbg('not field._moveOnFieldFull?', not field._moveOnFieldFull) + if( len(slice) == field_len and field._moveOnFieldFull + and (not field._stopFieldChangeIfInvalid or + field._stopFieldChangeIfInvalid and field.IsValid(slice))): + # move cursor to next field: + pos = self._findNextEntry(pos) + self._SetInsertionPoint(pos) + if pos < sel_to: + self._SetSelection(pos, sel_to) # restore selection + else: + self._SetSelection(pos, pos) # remove selection + else: # leave cursor alone + pass + else: + # if at start of control, move to right edge + if (sel_to == sel_start + and (self._isTemplateChar(pos) or (pos == start and len(slice)+ 1 < field_len)) + and pos != end): + pos = end # move to right edge +## elif sel_start <= start and sel_to == end: +## # select to right edge of field - 1 (to replace char) +## pos = end - 1 +## self._SetInsertionPoint(pos) +## # restore selection +## self._SetSelection(sel_start, pos) + + # if selected to beginning and signed, and not changing sign explicitly: + elif self._signOk and sel_start == 0 and key not in (ord('-'), ord('+'), ord(' ')): + # adjust to past reserved sign position: + pos = self._fields[0]._extent[0] +## dbg('adjusting field to ', pos) + self._SetInsertionPoint(pos) + # but keep original selection, to allow replacement of any sign: + self._SetSelection(0, sel_to) + else: + pass # leave position/selection alone + + # else make sure the user is not trying to type over a template character + # If they are, move them to the next valid entry position + elif self._isTemplateChar(pos): + if( (not field._moveOnFieldFull + and (not self._signOk + or (self._signOk and field._index == 0 and pos > 0) ) ) + + or (field._stopFieldChangeIfInvalid + and not field.IsValid(self._GetValue()[start:end]) ) ): + + # don't move to next field without explicit cursor movement + pass + else: + # find next valid position + pos = self._findNextEntry(pos) + self._SetInsertionPoint(pos) + if pos < sel_to: # restore selection + self._SetSelection(pos, sel_to) + else: + self._SetSelection(pos, pos) +## dbg('adjusted pos:', pos, indent=0) + return pos + + + def _adjustFloat(self, candidate=None): + """ + 'Fixes' an floating point control. Collapses spaces, right-justifies, etc. + """ +## dbg('MaskedEditMixin::_adjustFloat, candidate = "%s"' % candidate, indent=1) + lenInt,lenFraction = [len(s) for s in self._mask.split('.')] ## Get integer, fraction lengths + + if candidate is None: value = self._GetValue() + else: value = candidate +## dbg('value = "%(value)s"' % locals(), 'len(value):', len(value)) + intStr, fracStr = value.split(self._decimalChar) + + intStr = self._fields[0]._AdjustField(intStr) +## dbg('adjusted intStr: "%s"' % intStr) + lenInt = len(intStr) + fracStr = fracStr + ('0'*(lenFraction-len(fracStr))) # add trailing spaces to decimal + +## dbg('intStr "%(intStr)s"' % locals()) +## dbg('lenInt:', lenInt) + + intStr = string.rjust( intStr[-lenInt:], lenInt) +## dbg('right-justifed intStr = "%(intStr)s"' % locals()) + newvalue = intStr + self._decimalChar + fracStr + + if self._signOk: + if len(newvalue) < self._masklength: + newvalue = ' ' + newvalue + signedvalue = self._getSignedValue(newvalue)[0] + if signedvalue is not None: newvalue = signedvalue + + # Finally, align string with decimal position, left-padding with + # fillChar: + newdecpos = newvalue.find(self._decimalChar) + if newdecpos < self._decimalpos: + padlen = self._decimalpos - newdecpos + newvalue = string.join([' ' * padlen] + [newvalue] ,'') + + if self._signOk and self._useParens: + if newvalue.find('(') != -1: + newvalue = newvalue[:-1] + ')' + else: + newvalue = newvalue[:-1] + ' ' + +## dbg('newvalue = "%s"' % newvalue) + if candidate is None: + wx.CallAfter(self._SetValue, newvalue) +## dbg(indent=0) + return newvalue + + + def _adjustInt(self, candidate=None): + """ 'Fixes' an integer control. Collapses spaces, right or left-justifies.""" +## dbg("MaskedEditMixin::_adjustInt", candidate) + lenInt = self._masklength + if candidate is None: value = self._GetValue() + else: value = candidate + + intStr = self._fields[0]._AdjustField(value) + intStr = intStr.strip() # drop extra spaces +## dbg('adjusted field: "%s"' % intStr) + + if self._isNeg and intStr.find('-') == -1 and intStr.find('(') == -1: + if self._useParens: + intStr = '(' + intStr + ')' + else: + intStr = '-' + intStr + elif self._isNeg and intStr.find('-') != -1 and self._useParens: + intStr = intStr.replace('-', '(') + + if( self._signOk and ((self._useParens and intStr.find('(') == -1) + or (not self._useParens and intStr.find('-') == -1))): + intStr = ' ' + intStr + if self._useParens: + intStr += ' ' # space for right paren position + + elif self._signOk and self._useParens and intStr.find('(') != -1 and intStr.find(')') == -1: + # ensure closing right paren: + intStr += ')' + + if self._fields[0]._alignRight: ## Only if right-alignment is enabled + intStr = intStr.rjust( lenInt ) + else: + intStr = intStr.ljust( lenInt ) + + if candidate is None: + wx.CallAfter(self._SetValue, intStr ) + return intStr + + + def _adjustDate(self, candidate=None, fixcentury=False, force4digit_year=False): + """ + 'Fixes' a date control, expanding the year if it can. + Applies various self-formatting options. + """ +## dbg("MaskedEditMixin::_adjustDate", indent=1) + if candidate is None: text = self._GetValue() + else: text = candidate +## dbg('text=', text) + if self._datestyle == "YMD": + year_field = 0 + else: + year_field = 2 + +## dbg('getYear: "%s"' % _getYear(text, self._datestyle)) + year = string.replace( _getYear( text, self._datestyle),self._fields[year_field]._fillChar,"") # drop extra fillChars + month = _getMonth( text, self._datestyle) + day = _getDay( text, self._datestyle) +## dbg('self._datestyle:', self._datestyle, 'year:', year, 'Month', month, 'day:', day) + + yearVal = None + yearstart = self._dateExtent - 4 + if( len(year) < 4 + and (fixcentury + or force4digit_year + or (self._GetInsertionPoint() > yearstart+1 and text[yearstart+2] == ' ') + or (self._GetInsertionPoint() > yearstart+2 and text[yearstart+3] == ' ') ) ): + ## user entered less than four digits and changing fields or past point where we could + ## enter another digit: + try: + yearVal = int(year) + except: +## dbg('bad year=', year) + year = text[yearstart:self._dateExtent] + + if len(year) < 4 and yearVal: + if len(year) == 2: + # Fix year adjustment to be less "20th century" :-) and to adjust heuristic as the + # years pass... + now = wx.DateTime_Now() + century = (now.GetYear() /100) * 100 # "this century" + twodig_year = now.GetYear() - century # "this year" (2 digits) + # if separation between today's 2-digit year and typed value > 50, + # assume last century, + # else assume this century. + # + # Eg: if 2003 and yearVal == 30, => 2030 + # if 2055 and yearVal == 80, => 2080 + # if 2010 and yearVal == 96, => 1996 + # + if abs(yearVal - twodig_year) > 50: + yearVal = (century - 100) + yearVal + else: + yearVal = century + yearVal + year = str( yearVal ) + else: # pad with 0's to make a 4-digit year + year = "%04d" % yearVal + if self._4digityear or force4digit_year: + text = _makeDate(year, month, day, self._datestyle, text) + text[self._dateExtent:] +## dbg('newdate: "%s"' % text, indent=0) + return text + + + def _goEnd(self, getPosOnly=False): + """ Moves the insertion point to the end of user-entry """ +## dbg("MaskedEditMixin::_goEnd; getPosOnly:", getPosOnly, indent=1) + text = self._GetValue() +#### dbg('text: "%s"' % text) + i = 0 + if len(text.rstrip()): + for i in range( min( self._masklength-1, len(text.rstrip())), -1, -1): +#### dbg('i:', i, 'self._isMaskChar(%d)' % i, self._isMaskChar(i)) + if self._isMaskChar(i): + char = text[i] +#### dbg("text[%d]: '%s'" % (i, char)) + if char != ' ': + i += 1 + break + + if i == 0: + pos = self._goHome(getPosOnly=True) + else: + pos = min(i,self._masklength) + + field = self._FindField(pos) + start, end = field._extent + if field._insertRight and pos < end: + pos = end +## dbg('next pos:', pos) +## dbg(indent=0) + if getPosOnly: + return pos + else: + self._SetInsertionPoint(pos) + + + def _goHome(self, getPosOnly=False): + """ Moves the insertion point to the beginning of user-entry """ +## dbg("MaskedEditMixin::_goHome; getPosOnly:", getPosOnly, indent=1) + text = self._GetValue() + for i in range(self._masklength): + if self._isMaskChar(i): + break + pos = max(i, 0) +## dbg(indent=0) + if getPosOnly: + return pos + else: + self._SetInsertionPoint(max(i,0)) + + + + def _getAllowedChars(self, pos): + """ Returns a string of all allowed user input characters for the provided + mask character plus control options + """ + maskChar = self.maskdict[pos] + okchars = self.maskchardict[maskChar] ## entry, get mask approved characters + + # convert okchars to unicode if required; will force subsequent appendings to + # result in unicode strings + if 'unicode' in wx.PlatformInfo and type(okchars) != types.UnicodeType: + okchars = okchars.decode(self._defaultEncoding) + + field = self._FindField(pos) + if okchars and field._okSpaces: ## Allow spaces? + okchars += " " + if okchars and field._includeChars: ## any additional included characters? + okchars += field._includeChars +#### dbg('okchars[%d]:' % pos, okchars) + return okchars + + + def _isMaskChar(self, pos): + """ Returns True if the char at position pos is a special mask character (e.g. NCXaA#) + """ + if pos < self._masklength: + return self._ismasked[pos] + else: + return False + + + def _isTemplateChar(self,Pos): + """ Returns True if the char at position pos is a template character (e.g. -not- NCXaA#) + """ + if Pos < self._masklength: + return not self._isMaskChar(Pos) + else: + return False + + + def _isCharAllowed(self, char, pos, checkRegex=False, allowAutoSelect=True, ignoreInsertRight=False): + """ Returns True if character is allowed at the specific position, otherwise False.""" +## dbg('_isCharAllowed', char, pos, checkRegex, indent=1) + field = self._FindField(pos) + right_insert = False + + if self.controlInitialized: + sel_start, sel_to = self._GetSelection() + else: + sel_start, sel_to = pos, pos + + if (field._insertRight or self._ctrl_constraints._insertRight) and not ignoreInsertRight: + start, end = field._extent + field_len = end - start + if self.controlInitialized: + value = self._GetValue() + fstr = value[start:end].strip() + if field._padZero: + while fstr and fstr[0] == '0': + fstr = fstr[1:] + input_len = len(fstr) + if self._signOk and '-' in fstr or '(' in fstr: + input_len -= 1 # sign can move out of field, so don't consider it in length + else: + value = self._template + input_len = 0 # can't get the current "value", so use 0 + + + # if entire field is selected or position is at end and field is not full, + # or if allowed to right-insert at any point in field and field is not full and cursor is not at a fillChar + # or the field is a singleton integer field and is currently 0 and we're at the end: + if( (sel_start, sel_to) == field._extent + or (pos == end and ((input_len < field_len) + or (field_len == 1 + and input_len == field_len + and field._isInt + and value[end-1] == '0' + ) + ) ) ): + pos = end - 1 +## dbg('pos = end - 1 = ', pos, 'right_insert? 1') + right_insert = True + elif( field._allowInsert and sel_start == sel_to + and (sel_to == end or (sel_to < self._masklength and value[sel_start] != field._fillChar)) + and input_len < field_len ): + pos = sel_to - 1 # where character will go +## dbg('pos = sel_to - 1 = ', pos, 'right_insert? 1') + right_insert = True + # else leave pos alone... + else: +## dbg('pos stays ', pos, 'right_insert? 0') + pass + + if self._isTemplateChar( pos ): ## if a template character, return empty +## dbg('%d is a template character; returning False' % pos, indent=0) + return False + + if self._isMaskChar( pos ): + okChars = self._getAllowedChars(pos) + + if self._fields[0]._groupdigits and (self._isInt or (self._isFloat and pos < self._decimalpos)): + okChars += self._fields[0]._groupChar + + if self._signOk: + if self._isInt or (self._isFloat and pos < self._decimalpos): + okChars += '-' + if self._useParens: + okChars += '(' + elif self._useParens and (self._isInt or (self._isFloat and pos > self._decimalpos)): + okChars += ')' + +#### dbg('%s in %s?' % (char, okChars), char in okChars) + approved = (self.maskdict[pos] == '*' or char in okChars) + + if approved and checkRegex: +## dbg("checking appropriate regex's") + value = self._eraseSelection(self._GetValue()) + if right_insert: + # move the position to the right side of the insertion: + at = pos+1 + else: + at = pos + if allowAutoSelect: + newvalue, ignore, ignore, ignore, ignore = self._insertKey(char, at, sel_start, sel_to, value, allowAutoSelect=True) + else: + newvalue, ignore = self._insertKey(char, at, sel_start, sel_to, value) +## dbg('newvalue: "%s"' % newvalue) + + fields = [self._FindField(pos)] + [self._ctrl_constraints] + for field in fields: # includes fields[-1] == "ctrl_constraints" + if field._regexMask and field._filter: +## dbg('checking vs. regex') + start, end = field._extent + slice = newvalue[start:end] + approved = (re.match( field._filter, slice) is not None) +## dbg('approved?', approved) + if not approved: break +## dbg(indent=0) + return approved + else: +## dbg('%d is a !???! character; returning False', indent=0) + return False + + + def _applyFormatting(self): + """ Apply formatting depending on the control's state. + Need to find a way to call this whenever the value changes, in case the control's + value has been changed or set programatically. + """ +## dbg(suspend=1) +## dbg('MaskedEditMixin::_applyFormatting', indent=1) + + # Handle negative numbers + if self._signOk: + text, signpos, right_signpos = self._getSignedValue() +## dbg('text: "%s", signpos:' % text, signpos) + if text and signpos != self._signpos: + self._signpos = signpos + if not text or text[signpos] not in ('-','('): + self._isNeg = False +## dbg('no valid sign found; new sign:', self._isNeg) + elif text and self._valid and not self._isNeg and text[signpos] in ('-', '('): +## dbg('setting _isNeg to True') + self._isNeg = True +## dbg('self._isNeg:', self._isNeg) + + if self._signOk and self._isNeg: + fc = self._signedForegroundColour + else: + fc = self._foregroundColour + + if hasattr(fc, '_name'): + c =fc._name + else: + c = fc +## dbg('setting foreground to', c) + self.SetForegroundColour(fc) + + if self._valid: +## dbg('valid') + if self.IsEmpty(): + bc = self._emptyBackgroundColour + else: + bc = self._validBackgroundColour + else: +## dbg('invalid') + bc = self._invalidBackgroundColour + if hasattr(bc, '_name'): + c =bc._name + else: + c = bc +## dbg('setting background to', c) + self.SetBackgroundColour(bc) + self._Refresh() +## dbg(indent=0, suspend=0) + + + def _getAbsValue(self, candidate=None): + """ Return an unsigned value (i.e. strip the '-' prefix if any), and sign position(s). + """ +## dbg('MaskedEditMixin::_getAbsValue; candidate="%s"' % candidate, indent=1) + if candidate is None: text = self._GetValue() + else: text = candidate + right_signpos = text.find(')') + + if self._isInt: + if self._ctrl_constraints._alignRight and self._fields[0]._fillChar == ' ': + signpos = text.find('-') + if signpos == -1: +## dbg('no - found; searching for (') + signpos = text.find('(') + elif signpos != -1: +## dbg('- found at', signpos) + pass + + if signpos == -1: +## dbg('signpos still -1') +## dbg('len(%s) (%d) < len(%s) (%d)?' % (text, len(text), self._mask, self._masklength), len(text) < self._masklength) + if len(text) < self._masklength: + text = ' ' + text + if len(text) < self._masklength: + text += ' ' + if len(text) > self._masklength and text[-1] in (')', ' '): + text = text[:-1] + else: +## dbg('len(%s) (%d), len(%s) (%d)' % (text, len(text), self._mask, self._masklength)) +## dbg('len(%s) - (len(%s) + 1):' % (text, text.lstrip()) , len(text) - (len(text.lstrip()) + 1)) + signpos = len(text) - (len(text.lstrip()) + 1) + + if self._useParens and not text.strip(): + signpos -= 1 # empty value; use penultimate space +## dbg('signpos:', signpos) + if signpos >= 0: + text = text[:signpos] + ' ' + text[signpos+1:] + + else: + if self._signOk: + signpos = 0 + text = self._template[0] + text[1:] + else: + signpos = -1 + + if right_signpos != -1: + if self._signOk: + text = text[:right_signpos] + ' ' + text[right_signpos+1:] + elif len(text) > self._masklength: + text = text[:right_signpos] + text[right_signpos+1:] + right_signpos = -1 + + + elif self._useParens and self._signOk: + # figure out where it ought to go: + right_signpos = self._masklength - 1 # initial guess + if not self._ctrl_constraints._alignRight: +## dbg('not right-aligned') + if len(text.strip()) == 0: + right_signpos = signpos + 1 + elif len(text.strip()) < self._masklength: + right_signpos = len(text.rstrip()) +## dbg('right_signpos:', right_signpos) + + groupchar = self._fields[0]._groupChar + try: + value = long(text.replace(groupchar,'').replace('(','-').replace(')','').replace(' ', '')) + except: +## dbg('invalid number', indent=0) + return None, signpos, right_signpos + + else: # float value + try: + groupchar = self._fields[0]._groupChar + value = float(text.replace(groupchar,'').replace(self._decimalChar, '.').replace('(', '-').replace(')','').replace(' ', '')) +## dbg('value:', value) + except: + value = None + + if value < 0 and value is not None: + signpos = text.find('-') + if signpos == -1: + signpos = text.find('(') + + text = text[:signpos] + self._template[signpos] + text[signpos+1:] + else: + # look forwards up to the decimal point for the 1st non-digit +## dbg('decimal pos:', self._decimalpos) +## dbg('text: "%s"' % text) + if self._signOk: + signpos = self._decimalpos - (len(text[:self._decimalpos].lstrip()) + 1) + # prevent checking for empty string - Tomo - Wed 14 Jan 2004 03:19:09 PM CET + if len(text) >= signpos+1 and text[signpos+1] in ('-','('): + signpos += 1 + else: + signpos = -1 +## dbg('signpos:', signpos) + + if self._useParens: + if self._signOk: + right_signpos = self._masklength - 1 + text = text[:right_signpos] + ' ' + if text[signpos] == '(': + text = text[:signpos] + ' ' + text[signpos+1:] + else: + right_signpos = text.find(')') + if right_signpos != -1: + text = text[:-1] + right_signpos = -1 + + if value is None: +## dbg('invalid number') + text = None + +## dbg('abstext = "%s"' % text, 'signpos:', signpos, 'right_signpos:', right_signpos) +## dbg(indent=0) + return text, signpos, right_signpos + + + def _getSignedValue(self, candidate=None): + """ Return a signed value by adding a "-" prefix if the value + is set to negative, or a space if positive. + """ +## dbg('MaskedEditMixin::_getSignedValue; candidate="%s"' % candidate, indent=1) + if candidate is None: text = self._GetValue() + else: text = candidate + + + abstext, signpos, right_signpos = self._getAbsValue(text) + if self._signOk: + if abstext is None: +## dbg(indent=0) + return abstext, signpos, right_signpos + + if self._isNeg or text[signpos] in ('-', '('): + if self._useParens: + sign = '(' + else: + sign = '-' + else: + sign = ' ' + if abstext[signpos] not in string.digits: + text = abstext[:signpos] + sign + abstext[signpos+1:] + else: + # this can happen if value passed is too big; sign assumed to be + # in position 0, but if already filled with a digit, prepend sign... + text = sign + abstext + if self._useParens and text.find('(') != -1: + text = text[:right_signpos] + ')' + text[right_signpos+1:] + else: + text = abstext +## dbg('signedtext = "%s"' % text, 'signpos:', signpos, 'right_signpos', right_signpos) +## dbg(indent=0) + return text, signpos, right_signpos + + + def GetPlainValue(self, candidate=None): + """ Returns control's value stripped of the template text. + plainvalue = MaskedEditMixin.GetPlainValue() + """ +## dbg('MaskedEditMixin::GetPlainValue; candidate="%s"' % candidate, indent=1) + + if candidate is None: text = self._GetValue() + else: text = candidate + + if self.IsEmpty(): +## dbg('returned ""', indent=0) + return "" + else: + plain = "" + for idx in range( min(len(self._template), len(text)) ): + if self._mask[idx] in maskchars: + plain += text[idx] + + if self._isFloat or self._isInt: +## dbg('plain so far: "%s"' % plain) + plain = plain.replace('(', '-').replace(')', ' ') +## dbg('plain after sign regularization: "%s"' % plain) + + if self._signOk and self._isNeg and plain.count('-') == 0: + # must be in reserved position; add to "plain value" + plain = '-' + plain.strip() + + if self._fields[0]._alignRight: + lpad = plain.count(',') + plain = ' ' * lpad + plain.replace(',','') + else: + plain = plain.replace(',','') +## dbg('plain after pad and group:"%s"' % plain) + +## dbg('returned "%s"' % plain.rstrip(), indent=0) + return plain.rstrip() + + + def IsEmpty(self, value=None): + """ + Returns True if control is equal to an empty value. + (Empty means all editable positions in the template == fillChar.) + """ + if value is None: value = self._GetValue() + if value == self._template and not self._defaultValue: +#### dbg("IsEmpty? 1 (value == self._template and not self._defaultValue)") + return True # (all mask chars == fillChar by defn) + elif value == self._template: + empty = True + for pos in range(len(self._template)): +#### dbg('isMaskChar(%(pos)d)?' % locals(), self._isMaskChar(pos)) +#### dbg('value[%(pos)d] != self._fillChar?' %locals(), value[pos] != self._fillChar[pos]) + if self._isMaskChar(pos) and value[pos] not in (' ', self._fillChar[pos]): + empty = False +#### dbg("IsEmpty? %(empty)d (do all mask chars == fillChar?)" % locals()) + return empty + else: +#### dbg("IsEmpty? 0 (value doesn't match template)") + return False + + + def IsDefault(self, value=None): + """ + Returns True if the value specified (or the value of the control if not specified) + is equal to the default value. + """ + if value is None: value = self._GetValue() + return value == self._template + + + def IsValid(self, value=None): + """ Indicates whether the value specified (or the current value of the control + if not specified) is considered valid.""" +#### dbg('MaskedEditMixin::IsValid("%s")' % value, indent=1) + if value is None: value = self._GetValue() + ret = self._CheckValid(value) +#### dbg(indent=0) + return ret + + + def _eraseSelection(self, value=None, sel_start=None, sel_to=None): + """ Used to blank the selection when inserting a new character. """ +## dbg("MaskedEditMixin::_eraseSelection", indent=1) + if value is None: value = self._GetValue() + if sel_start is None or sel_to is None: + sel_start, sel_to = self._GetSelection() ## check for a range of selected text +## dbg('value: "%s"' % value) +## dbg("current sel_start, sel_to:", sel_start, sel_to) + + newvalue = list(value) + for i in range(sel_start, sel_to): + if self._signOk and newvalue[i] in ('-', '(', ')'): +## dbg('found sign (%s) at' % newvalue[i], i) + + # balance parentheses: + if newvalue[i] == '(': + right_signpos = value.find(')') + if right_signpos != -1: + newvalue[right_signpos] = ' ' + + elif newvalue[i] == ')': + left_signpos = value.find('(') + if left_signpos != -1: + newvalue[left_signpos] = ' ' + + newvalue[i] = ' ' + + elif self._isMaskChar(i): + field = self._FindField(i) + if field._padZero: + newvalue[i] = '0' + else: + newvalue[i] = self._template[i] + + value = string.join(newvalue,"") +## dbg('new value: "%s"' % value) +## dbg(indent=0) + return value + + + def _insertKey(self, char, pos, sel_start, sel_to, value, allowAutoSelect=False): + """ Handles replacement of the character at the current insertion point.""" +## dbg('MaskedEditMixin::_insertKey', "\'" + char + "\'", pos, sel_start, sel_to, '"%s"' % value, indent=1) + + text = self._eraseSelection(value) + field = self._FindField(pos) + start, end = field._extent + newtext = "" + newpos = pos + + # if >= 2 chars selected in a right-insert field, do appropriate erase on field, + # then set selection to end, and do usual right insert. + if sel_start != sel_to and sel_to >= sel_start+2: + field = self._FindField(sel_start) + if( field._insertRight # if right-insert + and field._allowInsert # and allow insert at any point in field + and field == self._FindField(sel_to) ): # and selection all in same field + text = self._OnErase(just_return_value=True) # remove selection before insert +## dbg('text after (left)erase: "%s"' % text) + pos = sel_start = sel_to + + if pos != sel_start and sel_start == sel_to: + # adjustpos must have moved the position; make selection match: + sel_start = sel_to = pos + +## dbg('field._insertRight?', field._insertRight) +## dbg('field._allowInsert?', field._allowInsert) +## dbg('sel_start, end', sel_start, end) + if sel_start < end: +## dbg('text[sel_start] != field._fillChar?', text[sel_start] != field._fillChar) + pass + + if( field._insertRight # field allows right insert + and ((sel_start, sel_to) == field._extent # and whole field selected + or (sel_start == sel_to # or nothing selected + and (sel_start == end # and cursor at right edge + or (field._allowInsert # or field allows right-insert + and sel_start < end # next to other char in field: + and text[sel_start] != field._fillChar) ) ) ) ): +## dbg('insertRight') + fstr = text[start:end] + erasable_chars = [field._fillChar, ' '] + + # if zero padding field, or a single digit, and currently a value of 0, allow erasure of 0: + if field._padZero or (field._isInt and (end - start == 1) and fstr[0] == '0'): + erasable_chars.append('0') + + erased = '' +#### dbg("fstr[0]:'%s'" % fstr[0]) +#### dbg('field_index:', field._index) +#### dbg("fstr[0] in erasable_chars?", fstr[0] in erasable_chars) +#### dbg("self._signOk and field._index == 0 and fstr[0] in ('-','(')?", self._signOk and field._index == 0 and fstr[0] in ('-','(')) + if fstr[0] in erasable_chars or (self._signOk and field._index == 0 and fstr[0] in ('-','(')): + erased = fstr[0] +#### dbg('value: "%s"' % text) +#### dbg('fstr: "%s"' % fstr) +#### dbg("erased: '%s'" % erased) + field_sel_start = sel_start - start + field_sel_to = sel_to - start +## dbg('left fstr: "%s"' % fstr[1:field_sel_start]) +## dbg('right fstr: "%s"' % fstr[field_sel_to:end]) + fstr = fstr[1:field_sel_start] + char + fstr[field_sel_to:end] + if field._alignRight and sel_start != sel_to: + field_len = end - start +## pos += (field_len - len(fstr)) # move cursor right by deleted amount + pos = sel_to +## dbg('setting pos to:', pos) + if field._padZero: + fstr = '0' * (field_len - len(fstr)) + fstr + else: + fstr = fstr.rjust(field_len) # adjust the field accordingly +## dbg('field str: "%s"' % fstr) + + newtext = text[:start] + fstr + text[end:] + if erased in ('-', '(') and self._signOk: + newtext = erased + newtext[1:] +## dbg('newtext: "%s"' % newtext) + + if self._signOk and field._index == 0: + start -= 1 # account for sign position + +#### dbg('field._moveOnFieldFull?', field._moveOnFieldFull) +#### dbg('len(fstr.lstrip()) == end-start?', len(fstr.lstrip()) == end-start) + if( field._moveOnFieldFull and pos == end + and len(fstr.lstrip()) == end-start # if field now full + and (not field._stopFieldChangeIfInvalid # and we either don't care about valid + or (field._stopFieldChangeIfInvalid # or we do and the current field value is valid + and field.IsValid(fstr)))): + + newpos = self._findNextEntry(end) # go to next field + else: + newpos = pos # else keep cursor at current position + + if not newtext: +## dbg('not newtext') + if newpos != pos: +## dbg('newpos:', newpos) + pass + if self._signOk and self._useParens: + old_right_signpos = text.find(')') + + if field._allowInsert and not field._insertRight and sel_to <= end and sel_start >= start: +## dbg('inserting within a left-insert-capable field') + field_len = end - start + before = text[start:sel_start] + after = text[sel_to:end].strip() +#### dbg("current field:'%s'" % text[start:end]) +#### dbg("before:'%s'" % before, "after:'%s'" % after) + new_len = len(before) + len(after) + 1 # (for inserted char) +#### dbg('new_len:', new_len) + + if new_len < field_len: + retained = after + self._template[end-(field_len-new_len):end] + elif new_len > end-start: + retained = after[1:] + else: + retained = after + + left = text[0:start] + before +#### dbg("left:'%s'" % left, "retained:'%s'" % retained) + right = retained + text[end:] + else: + left = text[0:pos] + right = text[pos+1:] + + if 'unicode' in wx.PlatformInfo and type(char) != types.UnicodeType: + # convert the keyboard constant to a unicode value, to + # ensure it can be concatenated into the control value: + char = char.decode(self._defaultEncoding) + + newtext = left + char + right +#### dbg('left: "%s"' % left) +#### dbg('right: "%s"' % right) +#### dbg('newtext: "%s"' % newtext) + + if self._signOk and self._useParens: + # Balance parentheses: + left_signpos = newtext.find('(') + + if left_signpos == -1: # erased '('; remove ')' + right_signpos = newtext.find(')') + if right_signpos != -1: + newtext = newtext[:right_signpos] + ' ' + newtext[right_signpos+1:] + + elif old_right_signpos != -1: + right_signpos = newtext.find(')') + + if right_signpos == -1: # just replaced right-paren + if newtext[pos] == ' ': # we just erased '); erase '(' + newtext = newtext[:left_signpos] + ' ' + newtext[left_signpos+1:] + else: # replaced with digit; move ') over + if self._ctrl_constraints._alignRight or self._isFloat: + newtext = newtext[:-1] + ')' + else: + rstripped_text = newtext.rstrip() + right_signpos = len(rstripped_text) +## dbg('old_right_signpos:', old_right_signpos, 'right signpos now:', right_signpos) + newtext = newtext[:right_signpos] + ')' + newtext[right_signpos+1:] + + if( field._insertRight # if insert-right field (but we didn't start at right edge) + and field._moveOnFieldFull # and should move cursor when full + and len(newtext[start:end].strip()) == end-start # and field now full + and (not field._stopFieldChangeIfInvalid # and we either don't care about valid + or (field._stopFieldChangeIfInvalid # or we do and the current field value is valid + and field.IsValid(newtext[start:end].strip())))): + + newpos = self._findNextEntry(end) # go to next field +## dbg('newpos = nextentry =', newpos) + else: +## dbg('pos:', pos, 'newpos:', pos+1) + newpos = pos+1 + + + if allowAutoSelect: + new_select_to = newpos # (default return values) + match_field = None + match_index = None + + if field._autoSelect: + match_index, partial_match = self._autoComplete(1, # (always forward) + field._compareChoices, + newtext[start:end], + compareNoCase=field._compareNoCase, + current_index = field._autoCompleteIndex-1) + if match_index is not None and partial_match: + matched_str = newtext[start:end] + newtext = newtext[:start] + field._choices[match_index] + newtext[end:] + new_select_to = end + match_field = field + if field._insertRight: + # adjust position to just after partial match in field + newpos = end - (len(field._choices[match_index].strip()) - len(matched_str.strip())) + + elif self._ctrl_constraints._autoSelect: + match_index, partial_match = self._autoComplete( + 1, # (always forward) + self._ctrl_constraints._compareChoices, + newtext, + self._ctrl_constraints._compareNoCase, + current_index = self._ctrl_constraints._autoCompleteIndex - 1) + if match_index is not None and partial_match: + matched_str = newtext + newtext = self._ctrl_constraints._choices[match_index] + edit_end = self._ctrl_constraints._extent[1] + new_select_to = min(edit_end, len(newtext.rstrip())) + match_field = self._ctrl_constraints + if self._ctrl_constraints._insertRight: + # adjust position to just after partial match in control: + newpos = self._masklength - (len(self._ctrl_constraints._choices[match_index].strip()) - len(matched_str.strip())) + +## dbg('newtext: "%s"' % newtext, 'newpos:', newpos, 'new_select_to:', new_select_to) +## dbg(indent=0) + return newtext, newpos, new_select_to, match_field, match_index + else: +## dbg('newtext: "%s"' % newtext, 'newpos:', newpos) +## dbg(indent=0) + return newtext, newpos + + + def _OnFocus(self,event): + """ + This event handler is currently necessary to work around new default + behavior as of wxPython2.3.3; + The TAB key auto selects the entire contents of the wx.TextCtrl *after* + the EVT_SET_FOCUS event occurs; therefore we can't query/adjust the selection + *here*, because it hasn't happened yet. So to prevent this behavior, and + preserve the correct selection when the focus event is not due to tab, + we need to pull the following trick: + """ +## dbg('MaskedEditMixin::_OnFocus') + if self.IsBeingDeleted() or self.GetParent().IsBeingDeleted(): + return + wx.CallAfter(self._fixSelection) + event.Skip() + self.Refresh() + + + def _CheckValid(self, candidate=None): + """ + This is the default validation checking routine; It verifies that the + current value of the control is a "valid value," and has the side + effect of coloring the control appropriately. + """ +## dbg(suspend=1) +## dbg('MaskedEditMixin::_CheckValid: candidate="%s"' % candidate, indent=1) + oldValid = self._valid + if candidate is None: value = self._GetValue() + else: value = candidate +## dbg('value: "%s"' % value) + oldvalue = value + valid = True # assume True + + if not self.IsDefault(value) and self._isDate: ## Date type validation + valid = self._validateDate(value) +## dbg("valid date?", valid) + + elif not self.IsDefault(value) and self._isTime: + valid = self._validateTime(value) +## dbg("valid time?", valid) + + elif not self.IsDefault(value) and (self._isInt or self._isFloat): ## Numeric type + valid = self._validateNumeric(value) +## dbg("valid Number?", valid) + + if valid: # and not self.IsDefault(value): ## generic validation accounts for IsDefault() + ## valid so far; ensure also allowed by any list or regex provided: + valid = self._validateGeneric(value) +## dbg("valid value?", valid) + +## dbg('valid?', valid) + + if not candidate: + self._valid = valid + self._applyFormatting() + if self._valid != oldValid: +## dbg('validity changed: oldValid =',oldValid,'newvalid =', self._valid) +## dbg('oldvalue: "%s"' % oldvalue, 'newvalue: "%s"' % self._GetValue()) + pass +## dbg(indent=0, suspend=0) + return valid + + + def _validateGeneric(self, candidate=None): + """ Validate the current value using the provided list or Regex filter (if any). + """ + if candidate is None: + text = self._GetValue() + else: + text = candidate + + valid = True # assume True + for i in [-1] + self._field_indices: # process global constraints first: + field = self._fields[i] + start, end = field._extent + slice = text[start:end] + valid = field.IsValid(slice) + if not valid: + break + + return valid + + + def _validateNumeric(self, candidate=None): + """ Validate that the value is within the specified range (if specified.)""" + if candidate is None: value = self._GetValue() + else: value = candidate + try: + groupchar = self._fields[0]._groupChar + if self._isFloat: + number = float(value.replace(groupchar, '').replace(self._decimalChar, '.').replace('(', '-').replace(')', '')) + else: + number = long( value.replace(groupchar, '').replace('(', '-').replace(')', '')) + if value.strip(): + if self._fields[0]._alignRight: + require_digit_at = self._fields[0]._extent[1]-1 + else: + require_digit_at = self._fields[0]._extent[0] +## dbg('require_digit_at:', require_digit_at) +## dbg("value[rda]: '%s'" % value[require_digit_at]) + if value[require_digit_at] not in list(string.digits): + valid = False + return valid + # else... +## dbg('number:', number) + if self._ctrl_constraints._hasRange: + valid = self._ctrl_constraints._rangeLow <= number <= self._ctrl_constraints._rangeHigh + else: + valid = True + groupcharpos = value.rfind(groupchar) + if groupcharpos != -1: # group char present +## dbg('groupchar found at', groupcharpos) + if self._isFloat and groupcharpos > self._decimalpos: + # 1st one found on right-hand side is past decimal point +## dbg('groupchar in fraction; illegal') + return False + elif self._isFloat: + integer = value[:self._decimalpos].strip() + else: + integer = value.strip() +## dbg("integer:'%s'" % integer) + if integer[0] in ('-', '('): + integer = integer[1:] + if integer[-1] == ')': + integer = integer[:-1] + + parts = integer.split(groupchar) +## dbg('parts:', parts) + for i in range(len(parts)): + if i == 0 and abs(int(parts[0])) > 999: +## dbg('group 0 too long; illegal') + valid = False + break + elif i > 0 and (len(parts[i]) != 3 or ' ' in parts[i]): +## dbg('group %i (%s) not right size; illegal' % (i, parts[i])) + valid = False + break + except ValueError: +## dbg('value not a valid number') + valid = False + return valid + + + def _validateDate(self, candidate=None): + """ Validate the current date value using the provided Regex filter. + Generally used for character types.BufferType + """ +## dbg('MaskedEditMixin::_validateDate', indent=1) + if candidate is None: value = self._GetValue() + else: value = candidate +## dbg('value = "%s"' % value) + text = self._adjustDate(value, force4digit_year=True) ## Fix the date up before validating it +## dbg('text =', text) + valid = True # assume True until proven otherwise + + try: + # replace fillChar in each field with space: + datestr = text[0:self._dateExtent] + for i in range(3): + field = self._fields[i] + start, end = field._extent + fstr = datestr[start:end] + fstr.replace(field._fillChar, ' ') + datestr = datestr[:start] + fstr + datestr[end:] + + year, month, day = _getDateParts( datestr, self._datestyle) + year = int(year) +## dbg('self._dateExtent:', self._dateExtent) + if self._dateExtent == 11: + month = charmonths_dict[month.lower()] + else: + month = int(month) + day = int(day) +## dbg('year, month, day:', year, month, day) + + except ValueError: +## dbg('cannot convert string to integer parts') + valid = False + except KeyError: +## dbg('cannot convert string to integer month') + valid = False + + if valid: + # use wxDateTime to unambiguously try to parse the date: + # ### Note: because wxDateTime is *brain-dead* and expects months 0-11, + # rather than 1-12, so handle accordingly: + if month > 12: + valid = False + else: + month -= 1 + try: +## dbg("trying to create date from values day=%d, month=%d, year=%d" % (day,month,year)) + dateHandler = wx.DateTimeFromDMY(day,month,year) +## dbg("succeeded") + dateOk = True + except: +## dbg('cannot convert string to valid date') + dateOk = False + if not dateOk: + valid = False + + if valid: + # wxDateTime doesn't take kindly to leading/trailing spaces when parsing, + # so we eliminate them here: + timeStr = text[self._dateExtent+1:].strip() ## time portion of the string + if timeStr: +## dbg('timeStr: "%s"' % timeStr) + try: + checkTime = dateHandler.ParseTime(timeStr) + valid = checkTime == len(timeStr) + except: + valid = False + if not valid: +## dbg('cannot convert string to valid time') + pass +## if valid: dbg('valid date') +## dbg(indent=0) + return valid + + + def _validateTime(self, candidate=None): + """ Validate the current time value using the provided Regex filter. + Generally used for character types.BufferType + """ +## dbg('MaskedEditMixin::_validateTime', indent=1) + # wxDateTime doesn't take kindly to leading/trailing spaces when parsing, + # so we eliminate them here: + if candidate is None: value = self._GetValue().strip() + else: value = candidate.strip() +## dbg('value = "%s"' % value) + valid = True # assume True until proven otherwise + + dateHandler = wx.DateTime_Today() + try: + checkTime = dateHandler.ParseTime(value) +## dbg('checkTime:', checkTime, 'len(value)', len(value)) + valid = checkTime == len(value) + except: + valid = False + + if not valid: +## dbg('cannot convert string to valid time') + pass +## if valid: dbg('valid time') +## dbg(indent=0) + return valid + + + def _OnKillFocus(self,event): + """ Handler for EVT_KILL_FOCUS event. + """ +## dbg('MaskedEditMixin::_OnKillFocus', 'isDate=',self._isDate, indent=1) + if self.IsBeingDeleted() or self.GetParent().IsBeingDeleted(): + return + if self._mask and self._IsEditable(): + self._AdjustField(self._GetInsertionPoint()) + self._CheckValid() ## Call valid handler + + self._LostFocus() ## Provided for subclass use + event.Skip() +## dbg(indent=0) + + + def _fixSelection(self): + """ + This gets called after the TAB traversal selection is made, if the + focus event was due to this, but before the EVT_LEFT_* events if + the focus shift was due to a mouse event. + + The trouble is that, a priori, there's no explicit notification of + why the focus event we received. However, the whole reason we need to + do this is because the default behavior on TAB traveral in a wx.TextCtrl is + now to select the entire contents of the window, something we don't want. + So we can *now* test the selection range, and if it's "the whole text" + we can assume the cause, change the insertion point to the start of + the control, and deselect. + """ +## dbg('MaskedEditMixin::_fixSelection', indent=1) + # can get here if called with wx.CallAfter after underlying + # control has been destroyed on close, but after focus + # events + if not self or not self._mask or not self._IsEditable(): +## dbg(indent=0) + return + + sel_start, sel_to = self._GetSelection() +## dbg('sel_start, sel_to:', sel_start, sel_to, 'self.IsEmpty()?', self.IsEmpty()) + + if( sel_start == 0 and sel_to >= len( self._mask ) #(can be greater in numeric controls because of reserved space) + and (not self._ctrl_constraints._autoSelect or self.IsEmpty() or self.IsDefault() ) ): + # This isn't normally allowed, and so assume we got here by the new + # "tab traversal" behavior, so we need to reset the selection + # and insertion point: +## dbg('entire text selected; resetting selection to start of control') + self._goHome() + field = self._FindField(self._GetInsertionPoint()) + edit_start, edit_end = field._extent + if field._selectOnFieldEntry: + if self._isFloat or self._isInt and field == self._fields[0]: + edit_start = 0 + self._SetInsertionPoint(edit_start) + self._SetSelection(edit_start, edit_end) + + elif field._insertRight: + self._SetInsertionPoint(edit_end) + self._SetSelection(edit_end, edit_end) + + elif (self._isFloat or self._isInt): + + text, signpos, right_signpos = self._getAbsValue() + if text is None or text == self._template: + integer = self._fields[0] + edit_start, edit_end = integer._extent + + if integer._selectOnFieldEntry: +## dbg('select on field entry:') + self._SetInsertionPoint(0) + self._SetSelection(0, edit_end) + + elif integer._insertRight: +## dbg('moving insertion point to end') + self._SetInsertionPoint(edit_end) + self._SetSelection(edit_end, edit_end) + else: +## dbg('numeric ctrl is empty; start at beginning after sign') + self._SetInsertionPoint(signpos+1) ## Move past minus sign space if signed + self._SetSelection(signpos+1, signpos+1) + + elif sel_start > self._goEnd(getPosOnly=True): +## dbg('cursor beyond the end of the user input; go to end of it') + self._goEnd() + else: +## dbg('sel_start, sel_to:', sel_start, sel_to, 'self._masklength:', self._masklength) + pass +## dbg(indent=0) + + + def _Keypress(self,key): + """ Method provided to override OnChar routine. Return False to force + a skip of the 'normal' OnChar process. Called before class OnChar. + """ + return True + + + def _LostFocus(self): + """ Method provided for subclasses. _LostFocus() is called after + the class processes its EVT_KILL_FOCUS event code. + """ + pass + + + def _OnDoubleClick(self, event): + """ selects field under cursor on dclick.""" + pos = self._GetInsertionPoint() + field = self._FindField(pos) + start, end = field._extent + self._SetInsertionPoint(start) + self._SetSelection(start, end) + + + def _Change(self): + """ Method provided for subclasses. Called by internal EVT_TEXT + handler. Return False to override the class handler, True otherwise. + """ + return True + + + def _Cut(self): + """ + Used to override the default Cut() method in base controls, instead + copying the selection to the clipboard and then blanking the selection, + leaving only the mask in the selected area behind. + Note: _Cut (read "undercut" ;-) must be called from a Cut() override in the + derived control because the mixin functions can't override a method of + a sibling class. + """ +## dbg("MaskedEditMixin::_Cut", indent=1) + value = self._GetValue() +## dbg('current value: "%s"' % value) + sel_start, sel_to = self._GetSelection() ## check for a range of selected text +## dbg('selected text: "%s"' % value[sel_start:sel_to].strip()) + do = wx.TextDataObject() + do.SetText(value[sel_start:sel_to].strip()) + wx.TheClipboard.Open() + wx.TheClipboard.SetData(do) + wx.TheClipboard.Close() + + if sel_to - sel_start != 0: + self._OnErase() +## dbg(indent=0) + + +# WS Note: overriding Copy is no longer necessary given that you +# can no longer select beyond the last non-empty char in the control. +# +## def _Copy( self ): +## """ +## Override the wx.TextCtrl's .Copy function, with our own +## that does validation. Need to strip trailing spaces. +## """ +## sel_start, sel_to = self._GetSelection() +## select_len = sel_to - sel_start +## textval = wx.TextCtrl._GetValue(self) +## +## do = wx.TextDataObject() +## do.SetText(textval[sel_start:sel_to].strip()) +## wx.TheClipboard.Open() +## wx.TheClipboard.SetData(do) +## wx.TheClipboard.Close() + + + def _getClipboardContents( self ): + """ Subroutine for getting the current contents of the clipboard. + """ + do = wx.TextDataObject() + wx.TheClipboard.Open() + success = wx.TheClipboard.GetData(do) + wx.TheClipboard.Close() + + if not success: + return None + else: + # Remove leading and trailing spaces before evaluating contents + return do.GetText().strip() + + + def _validatePaste(self, paste_text, sel_start, sel_to, raise_on_invalid=False): + """ + Used by paste routine and field choice validation to see + if a given slice of paste text is legal for the area in question: + returns validity, replacement text, and extent of paste in + template. + """ +## dbg(suspend=1) +## dbg('MaskedEditMixin::_validatePaste("%(paste_text)s", %(sel_start)d, %(sel_to)d), raise_on_invalid? %(raise_on_invalid)d' % locals(), indent=1) + select_length = sel_to - sel_start + maxlength = select_length +## dbg('sel_to - sel_start:', maxlength) + if maxlength == 0: + maxlength = self._masklength - sel_start + item = 'control' + else: + item = 'selection' +## dbg('maxlength:', maxlength) + if 'unicode' in wx.PlatformInfo and type(paste_text) != types.UnicodeType: + paste_text = paste_text.decode(self._defaultEncoding) + + length_considered = len(paste_text) + if length_considered > maxlength: +## dbg('paste text will not fit into the %s:' % item, indent=0) + if raise_on_invalid: +## dbg(indent=0, suspend=0) + if item == 'control': + ve = ValueError('"%s" will not fit into the control "%s"' % (paste_text, self.name)) + ve.value = paste_text + raise ve + else: + ve = ValueError('"%s" will not fit into the selection' % paste_text) + ve.value = paste_text + raise ve + else: +## dbg(indent=0, suspend=0) + return False, None, None + + text = self._template +## dbg('length_considered:', length_considered) + + valid_paste = True + replacement_text = "" + replace_to = sel_start + i = 0 + while valid_paste and i < length_considered and replace_to < self._masklength: + if paste_text[i:] == self._template[replace_to:length_considered]: + # remainder of paste matches template; skip char-by-char analysis +## dbg('remainder paste_text[%d:] (%s) matches template[%d:%d]' % (i, paste_text[i:], replace_to, length_considered)) + replacement_text += paste_text[i:] + replace_to = i = length_considered + continue + # else: + char = paste_text[i] + field = self._FindField(replace_to) + if not field._compareNoCase: + if field._forceupper: char = char.upper() + elif field._forcelower: char = char.lower() + +## dbg('char:', "'"+char+"'", 'i =', i, 'replace_to =', replace_to) +## dbg('self._isTemplateChar(%d)?' % replace_to, self._isTemplateChar(replace_to)) + if not self._isTemplateChar(replace_to) and self._isCharAllowed( char, replace_to, allowAutoSelect=False, ignoreInsertRight=True): + replacement_text += char +## dbg("not template(%(replace_to)d) and charAllowed('%(char)s',%(replace_to)d)" % locals()) +## dbg("replacement_text:", '"'+replacement_text+'"') + i += 1 + replace_to += 1 + elif( char == self._template[replace_to] + or (self._signOk and + ( (i == 0 and (char == '-' or (self._useParens and char == '('))) + or (i == self._masklength - 1 and self._useParens and char == ')') ) ) ): + replacement_text += char +## dbg("'%(char)s' == template(%(replace_to)d)" % locals()) +## dbg("replacement_text:", '"'+replacement_text+'"') + i += 1 + replace_to += 1 + else: + next_entry = self._findNextEntry(replace_to, adjustInsert=False) + if next_entry == replace_to: + valid_paste = False + else: + replacement_text += self._template[replace_to:next_entry] +## dbg("skipping template; next_entry =", next_entry) +## dbg("replacement_text:", '"'+replacement_text+'"') + replace_to = next_entry # so next_entry will be considered on next loop + + if not valid_paste and raise_on_invalid: +## dbg('raising exception', indent=0, suspend=0) + ve = ValueError('"%s" cannot be inserted into the control "%s"' % (paste_text, self.name)) + ve.value = paste_text + raise ve + + + elif i < len(paste_text): + valid_paste = False + if raise_on_invalid: +## dbg('raising exception', indent=0, suspend=0) + ve = ValueError('"%s" will not fit into the control "%s"' % (paste_text, self.name)) + ve.value = paste_text + raise ve + +## dbg('valid_paste?', valid_paste) + if valid_paste: +## dbg('replacement_text: "%s"' % replacement_text, 'replace to:', replace_to) + pass +## dbg(indent=0, suspend=0) + return valid_paste, replacement_text, replace_to + + + def _Paste( self, value=None, raise_on_invalid=False, just_return_value=False ): + """ + Used to override the base control's .Paste() function, + with our own that does validation. + Note: _Paste must be called from a Paste() override in the + derived control because the mixin functions can't override a + method of a sibling class. + """ +## dbg('MaskedEditMixin::_Paste (value = "%s")' % value, indent=1) + if value is None: + paste_text = self._getClipboardContents() + else: + paste_text = value + + if paste_text is not None: + + if 'unicode' in wx.PlatformInfo and type(paste_text) != types.UnicodeType: + paste_text = paste_text.decode(self._defaultEncoding) + +## dbg('paste text: "%s"' % paste_text) + # (conversion will raise ValueError if paste isn't legal) + sel_start, sel_to = self._GetSelection() +## dbg('selection:', (sel_start, sel_to)) + + # special case: handle allowInsert fields properly + field = self._FindField(sel_start) + edit_start, edit_end = field._extent + new_pos = None + if field._allowInsert and sel_to <= edit_end and (sel_start + len(paste_text) < edit_end or field._insertRight): + if field._insertRight: + # want to paste to the left; see if it will fit: + left_text = self._GetValue()[edit_start:sel_start].lstrip() +## dbg('len(left_text):', len(left_text)) +## dbg('len(paste_text):', len(paste_text)) +## dbg('sel_start - (len(left_text) + len(paste_text)) >= edit_start?', sel_start - (len(left_text) + len(paste_text)) >= edit_start) + if sel_start - (len(left_text) - (sel_to - sel_start) + len(paste_text)) >= edit_start: + # will fit! create effective paste text, and move cursor back to do so: + paste_text = left_text + paste_text + sel_start -= len(left_text) + paste_text = paste_text.rjust(sel_to - sel_start) +## dbg('modified paste_text to be: "%s"' % paste_text) +## dbg('modified selection to:', (sel_start, sel_to)) + else: +## dbg("won't fit left;", 'paste text remains: "%s"' % paste_text) + pass + else: + paste_text = paste_text + self._GetValue()[sel_to:edit_end].rstrip() +## dbg("allow insert, but not insert right;", 'paste text set to: "%s"' % paste_text) + + + new_pos = sel_start + len(paste_text) # store for subsequent positioning +## dbg('paste within insertable field; adjusted paste_text: "%s"' % paste_text, 'end:', edit_end) +## dbg('expanded selection to:', (sel_start, sel_to)) + + # Another special case: paste won't fit, but it's a right-insert field where entire + # non-empty value is selected, and there's room if the selection is expanded leftward: + if( len(paste_text) > sel_to - sel_start + and field._insertRight + and sel_start > edit_start + and sel_to >= edit_end + and not self._GetValue()[edit_start:sel_start].strip() ): + # text won't fit within selection, but left of selection is empty; + # check to see if we can expand selection to accommodate the value: + empty_space = sel_start - edit_start + amount_needed = len(paste_text) - (sel_to - sel_start) + if amount_needed <= empty_space: + sel_start -= amount_needed +## dbg('expanded selection to:', (sel_start, sel_to)) + + + # another special case: deal with signed values properly: + if self._signOk: + signedvalue, signpos, right_signpos = self._getSignedValue() + paste_signpos = paste_text.find('-') + if paste_signpos == -1: + paste_signpos = paste_text.find('(') + + # if paste text will result in signed value: +#### dbg('paste_signpos != -1?', paste_signpos != -1) +#### dbg('sel_start:', sel_start, 'signpos:', signpos) +#### dbg('field._insertRight?', field._insertRight) +#### dbg('sel_start - len(paste_text) >= signpos?', sel_start - len(paste_text) <= signpos) + if paste_signpos != -1 and (sel_start <= signpos + or (field._insertRight and sel_start - len(paste_text) <= signpos)): + signed = True + else: + signed = False + # remove "sign" from paste text, so we can auto-adjust for sign type after paste: + paste_text = paste_text.replace('-', ' ').replace('(',' ').replace(')','') +## dbg('unsigned paste text: "%s"' % paste_text) + else: + signed = False + + # another special case: deal with insert-right fields when selection is empty and + # cursor is at end of field: +#### dbg('field._insertRight?', field._insertRight) +#### dbg('sel_start == edit_end?', sel_start == edit_end) +#### dbg('sel_start', sel_start, 'sel_to', sel_to) + if field._insertRight and sel_start == edit_end and sel_start == sel_to: + sel_start -= len(paste_text) + if sel_start < 0: + sel_start = 0 +## dbg('adjusted selection:', (sel_start, sel_to)) + + raise_on_invalid = raise_on_invalid or field._raiseOnInvalidPaste + try: + valid_paste, replacement_text, replace_to = self._validatePaste(paste_text, sel_start, sel_to, raise_on_invalid) + except: +## dbg('exception thrown', indent=0) + raise + + if not valid_paste: +## dbg('paste text not legal for the selection or portion of the control following the cursor;') + if not wx.Validator_IsSilent(): + wx.Bell() +## dbg(indent=0) + return None, -1 + # else... + text = self._eraseSelection() + + new_text = text[:sel_start] + replacement_text + text[replace_to:] + if new_text: + new_text = string.ljust(new_text,self._masklength) + if signed: + new_text, signpos, right_signpos = self._getSignedValue(candidate=new_text) + if new_text: + if self._useParens: + new_text = new_text[:signpos] + '(' + new_text[signpos+1:right_signpos] + ')' + new_text[right_signpos+1:] + else: + new_text = new_text[:signpos] + '-' + new_text[signpos+1:] + if not self._isNeg: + self._isNeg = 1 + +## dbg("new_text:", '"'+new_text+'"') + + if not just_return_value: + if new_text != self._GetValue(): + self.modified = True + if new_text == '': + self.ClearValue() + else: + wx.CallAfter(self._SetValue, new_text) + if new_pos is None: + new_pos = sel_start + len(replacement_text) + wx.CallAfter(self._SetInsertionPoint, new_pos) + else: +## dbg(indent=0) + return new_text, replace_to + elif just_return_value: +## dbg(indent=0) + return self._GetValue(), sel_to +## dbg(indent=0) + + def _Undo(self, value=None, prev=None, just_return_results=False): + """ Provides an Undo() method in base controls. """ +## dbg("MaskedEditMixin::_Undo", indent=1) + if value is None: + value = self._GetValue() + if prev is None: + prev = self._prevValue +## dbg('current value: "%s"' % value) +## dbg('previous value: "%s"' % prev) + if prev is None: +## dbg('no previous value', indent=0) + return + + elif value != prev: + # Determine what to select: (relies on fixed-length strings) + # (This is a lot harder than it would first appear, because + # of mask chars that stay fixed, and so break up the "diff"...) + + # Determine where they start to differ: + i = 0 + length = len(value) # (both are same length in masked control) + + while( value[:i] == prev[:i] ): + i += 1 + sel_start = i - 1 + + + # handle signed values carefully, so undo from signed to unsigned or vice-versa + # works properly: + if self._signOk: + text, signpos, right_signpos = self._getSignedValue(candidate=prev) + if self._useParens: + if prev[signpos] == '(' and prev[right_signpos] == ')': + self._isNeg = True + else: + self._isNeg = False + # eliminate source of "far-end" undo difference if using balanced parens: + value = value.replace(')', ' ') + prev = prev.replace(')', ' ') + elif prev[signpos] == '-': + self._isNeg = True + else: + self._isNeg = False + + # Determine where they stop differing in "undo" result: + sm = difflib.SequenceMatcher(None, a=value, b=prev) + i, j, k = sm.find_longest_match(sel_start, length, sel_start, length) +## dbg('i,j,k = ', (i,j,k), 'value[i:i+k] = "%s"' % value[i:i+k], 'prev[j:j+k] = "%s"' % prev[j:j+k] ) + + if k == 0: # no match found; select to end + sel_to = length + else: + code_5tuples = sm.get_opcodes() + for op, i1, i2, j1, j2 in code_5tuples: +## dbg("%7s value[%d:%d] (%s) prev[%d:%d] (%s)" % (op, i1, i2, value[i1:i2], j1, j2, prev[j1:j2])) + pass + + diff_found = False + # look backward through operations needed to produce "previous" value; + # first change wins: + for next_op in range(len(code_5tuples)-1, -1, -1): + op, i1, i2, j1, j2 = code_5tuples[next_op] +## dbg('value[i1:i2]: "%s"' % value[i1:i2], 'template[i1:i2] "%s"' % self._template[i1:i2]) + field = self._FindField(i2) + if op == 'insert' and prev[j1:j2] != self._template[j1:j2]: +## dbg('insert found: selection =>', (j1, j2)) + sel_start = j1 + sel_to = j2 + diff_found = True + break + elif op == 'delete' and value[i1:i2] != self._template[i1:i2]: + edit_start, edit_end = field._extent + if field._insertRight and (field._allowInsert or i2 == edit_end): + sel_start = i2 + sel_to = i2 + else: + sel_start = i1 + sel_to = j1 +## dbg('delete found: selection =>', (sel_start, sel_to)) + diff_found = True + break + elif op == 'replace': + if not prev[i1:i2].strip() and field._insertRight: + sel_start = sel_to = j2 + else: + sel_start = j1 + sel_to = j2 +## dbg('replace found: selection =>', (sel_start, sel_to)) + diff_found = True + break + + + if diff_found: + # now go forwards, looking for earlier changes: +## dbg('searching forward...') + for next_op in range(len(code_5tuples)): + op, i1, i2, j1, j2 = code_5tuples[next_op] + field = self._FindField(i1) + if op == 'equal': + continue + elif op == 'replace': + if field._insertRight: + # if replace with spaces in an insert-right control, ignore "forward" replace + if not prev[i1:i2].strip(): + continue + elif j1 < i1: +## dbg('setting sel_start to', j1) + sel_start = j1 + else: +## dbg('setting sel_start to', i1) + sel_start = i1 + else: +## dbg('setting sel_start to', i1) + sel_start = i1 +## dbg('saw replace; breaking') + break + elif op == 'insert' and not value[i1:i2]: +## dbg('forward %s found' % op) + if prev[j1:j2].strip(): +## dbg('item to insert non-empty; setting sel_start to', j1) + sel_start = j1 + break + elif not field._insertRight: +## dbg('setting sel_start to inserted space:', j1) + sel_start = j1 + break + elif op == 'delete': +## dbg('delete; field._insertRight?', field._insertRight, 'value[%d:%d].lstrip: "%s"' % (i1,i2,value[i1:i2].lstrip())) + if field._insertRight: + if value[i1:i2].lstrip(): +## dbg('setting sel_start to ', j1) + sel_start = j1 +## dbg('breaking loop') + break + else: + continue + else: +## dbg('saw delete; breaking') + break + else: +## dbg('unknown code!') + # we've got what we need + break + + + if not diff_found: +## dbg('no insert,delete or replace found (!)') + # do "left-insert"-centric processing of difference based on l.c.s.: + if i == j and j != sel_start: # match starts after start of selection + sel_to = sel_start + (j-sel_start) # select to start of match + else: + sel_to = j # (change ends at j) + + + # There are several situations where the calculated difference is + # not what we want to select. If changing sign, or just adding + # group characters, we really don't want to highlight the characters + # changed, but instead leave the cursor where it is. + # Also, there a situations in which the difference can be ambiguous; + # Consider: + # + # current value: 11234 + # previous value: 1111234 + # + # Where did the cursor actually lie and which 1s were selected on the delete + # operation? + # + # Also, difflib can "get it wrong;" Consider: + # + # current value: " 128.66" + # previous value: " 121.86" + # + # difflib produces the following opcodes, which are sub-optimal: + # equal value[0:9] ( 12) prev[0:9] ( 12) + # insert value[9:9] () prev[9:11] (1.) + # equal value[9:10] (8) prev[11:12] (8) + # delete value[10:11] (.) prev[12:12] () + # equal value[11:12] (6) prev[12:13] (6) + # delete value[12:13] (6) prev[13:13] () + # + # This should have been: + # equal value[0:9] ( 12) prev[0:9] ( 12) + # replace value[9:11] (8.6) prev[9:11] (1.8) + # equal value[12:13] (6) prev[12:13] (6) + # + # But it didn't figure this out! + # + # To get all this right, we use the previous selection recorded to help us... + + if (sel_start, sel_to) != self._prevSelection: +## dbg('calculated selection', (sel_start, sel_to), "doesn't match previous", self._prevSelection) + + prev_sel_start, prev_sel_to = self._prevSelection + field = self._FindField(sel_start) + if( self._signOk + and sel_start < self._masklength + and (prev[sel_start] in ('-', '(', ')') + or value[sel_start] in ('-', '(', ')')) ): + # change of sign; leave cursor alone... +## dbg("prev[sel_start] in ('-', '(', ')')?", prev[sel_start] in ('-', '(', ')')) +## dbg("value[sel_start] in ('-', '(', ')')?", value[sel_start] in ('-', '(', ')')) +## dbg('setting selection to previous one') + sel_start, sel_to = self._prevSelection + + elif field._groupdigits and (value[sel_start:sel_to] == field._groupChar + or prev[sel_start:sel_to] == field._groupChar): + # do not highlight grouping changes +## dbg('value[sel_start:sel_to] == field._groupChar?', value[sel_start:sel_to] == field._groupChar) +## dbg('prev[sel_start:sel_to] == field._groupChar?', prev[sel_start:sel_to] == field._groupChar) +## dbg('setting selection to previous one') + sel_start, sel_to = self._prevSelection + + else: + calc_select_len = sel_to - sel_start + prev_select_len = prev_sel_to - prev_sel_start + +## dbg('sel_start == prev_sel_start', sel_start == prev_sel_start) +## dbg('sel_to > prev_sel_to', sel_to > prev_sel_to) + + if prev_select_len >= calc_select_len: + # old selection was bigger; trust it: +## dbg('prev_select_len >= calc_select_len?', prev_select_len >= calc_select_len) + if not field._insertRight: +## dbg('setting selection to previous one') + sel_start, sel_to = self._prevSelection + else: + sel_to = self._prevSelection[1] +## dbg('setting selection to', (sel_start, sel_to)) + + elif( sel_to > prev_sel_to # calculated select past last selection + and prev_sel_to < len(self._template) # and prev_sel_to not at end of control + and sel_to == len(self._template) ): # and calculated selection goes to end of control + + i, j, k = sm.find_longest_match(prev_sel_to, length, prev_sel_to, length) +## dbg('i,j,k = ', (i,j,k), 'value[i:i+k] = "%s"' % value[i:i+k], 'prev[j:j+k] = "%s"' % prev[j:j+k] ) + if k > 0: + # difflib must not have optimized opcodes properly; + sel_to = j + + else: + # look for possible ambiguous diff: + + # if last change resulted in no selection, test from resulting cursor position: + if prev_sel_start == prev_sel_to: + calc_select_len = sel_to - sel_start + field = self._FindField(prev_sel_start) + + # determine which way to search from last cursor position for ambiguous change: + if field._insertRight: + test_sel_start = prev_sel_start + test_sel_to = prev_sel_start + calc_select_len + else: + test_sel_start = prev_sel_start - calc_select_len + test_sel_to = prev_sel_start + else: + test_sel_start, test_sel_to = prev_sel_start, prev_sel_to + +## dbg('test selection:', (test_sel_start, test_sel_to)) +## dbg('calc change: "%s"' % prev[sel_start:sel_to]) +## dbg('test change: "%s"' % prev[test_sel_start:test_sel_to]) + + # if calculated selection spans characters, and same characters + # "before" the previous insertion point are present there as well, + # select the ones related to the last known selection instead. + if( sel_start != sel_to + and test_sel_to < len(self._template) + and prev[test_sel_start:test_sel_to] == prev[sel_start:sel_to] ): + + sel_start, sel_to = test_sel_start, test_sel_to + + # finally, make sure that the old and new values are + # different where we say they're different: + while( sel_to - 1 > 0 + and sel_to > sel_start + and value[sel_to-1:] == prev[sel_to-1:]): + sel_to -= 1 + while( sel_start + 1 < self._masklength + and sel_start < sel_to + and value[:sel_start+1] == prev[:sel_start+1]): + sel_start += 1 + +## dbg('sel_start, sel_to:', sel_start, sel_to) +## dbg('previous value: "%s"' % prev) +## dbg(indent=0) + if just_return_results: + return prev, (sel_start, sel_to) + # else... + self._SetValue(prev) + self._SetInsertionPoint(sel_start) + self._SetSelection(sel_start, sel_to) + + else: +## dbg('no difference between previous value') +## dbg(indent=0) + if just_return_results: + return prev, self._GetSelection() + + + def _OnClear(self, event): + """ Provides an action for context menu delete operation """ + self.ClearValue() + + + def _OnContextMenu(self, event): +## dbg('MaskedEditMixin::OnContextMenu()', indent=1) + menu = wx.Menu() + menu.Append(wx.ID_UNDO, "Undo", "") + menu.AppendSeparator() + menu.Append(wx.ID_CUT, "Cut", "") + menu.Append(wx.ID_COPY, "Copy", "") + menu.Append(wx.ID_PASTE, "Paste", "") + menu.Append(wx.ID_CLEAR, "Delete", "") + menu.AppendSeparator() + menu.Append(wx.ID_SELECTALL, "Select All", "") + + wx.EVT_MENU(menu, wx.ID_UNDO, self._OnCtrl_Z) + wx.EVT_MENU(menu, wx.ID_CUT, self._OnCtrl_X) + wx.EVT_MENU(menu, wx.ID_COPY, self._OnCtrl_C) + wx.EVT_MENU(menu, wx.ID_PASTE, self._OnCtrl_V) + wx.EVT_MENU(menu, wx.ID_CLEAR, self._OnClear) + wx.EVT_MENU(menu, wx.ID_SELECTALL, self._OnCtrl_A) + + # ## WSS: The base control apparently handles + # enable/disable of wx.ID_CUT, wx.ID_COPY, wx.ID_PASTE + # and wx.ID_CLEAR menu items even if the menu is one + # we created. However, it doesn't do undo properly, + # so we're keeping track of previous values ourselves. + # Therefore, we have to override the default update for + # that item on the menu: + wx.EVT_UPDATE_UI(self, wx.ID_UNDO, self._UndoUpdateUI) + self._contextMenu = menu + + self.PopupMenu(menu, event.GetPosition()) + menu.Destroy() + self._contextMenu = None +## dbg(indent=0) + + def _UndoUpdateUI(self, event): + if self._prevValue is None or self._prevValue == self._curValue: + self._contextMenu.Enable(wx.ID_UNDO, False) + else: + self._contextMenu.Enable(wx.ID_UNDO, True) + + + def _OnCtrlParametersChanged(self): + """ + Overridable function to allow derived classes to take action as a + result of parameter changes prior to possibly changing the value + of the control. + """ + pass + + ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- +class MaskedEditAccessorsMixin: + """ + To avoid a ton of boiler-plate, and to automate the getter/setter generation + for each valid control parameter so we never forget to add the functions when + adding parameters, this class programmatically adds the masked edit mixin + parameters to itself. + (This makes it easier for Designers like Boa to deal with masked controls.) + + To further complicate matters, this is done with an extra level of inheritance, + so that "general" classes like masked.TextCtrl can have all possible attributes, + while derived classes, like masked.TimeCtrl and masked.NumCtrl can prevent + exposure of those optional attributes of their base class that do not make + sense for their derivation. + + Therefore, we define: + BaseMaskedTextCtrl(TextCtrl, MaskedEditMixin) + and + masked.TextCtrl(BaseMaskedTextCtrl, MaskedEditAccessorsMixin). + + This allows us to then derive: + masked.NumCtrl( BaseMaskedTextCtrl ) + + and not have to expose all the same accessor functions for the + derived control when they don't all make sense for it. + + """ + + # Define the default set of attributes exposed by the most generic masked controls: + exposed_basectrl_params = MaskedEditMixin.valid_ctrl_params.keys() + Field.valid_params.keys() + exposed_basectrl_params.remove('index') + exposed_basectrl_params.remove('extent') + exposed_basectrl_params.remove('foregroundColour') # (base class already has this) + + for param in exposed_basectrl_params: + propname = param[0].upper() + param[1:] + exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) + exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) + + if param.find('Colour') != -1: + # add non-british spellings, for backward-compatibility + propname.replace('Colour', 'Color') + + exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) + exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) + + + + +## ---------- ---------- ---------- ---------- ---------- ---------- ---------- +## these are helper subroutines: + +def _movetofloat( origvalue, fmtstring, neg, addseparators=False, sepchar = ',',fillchar=' '): + """ addseparators = add separator character every three numerals if True + """ + fmt0 = fmtstring.split('.') + fmt1 = fmt0[0] + fmt2 = fmt0[1] + val = origvalue.split('.')[0].strip() + ret = fillchar * (len(fmt1)-len(val)) + val + "." + "0" * len(fmt2) + if neg: + ret = '-' + ret[1:] + return (ret,len(fmt1)) + + +def _isDateType( fmtstring ): + """ Checks the mask and returns True if it fits an allowed + date or datetime format. + """ + dateMasks = ("^##/##/####", + "^##-##-####", + "^##.##.####", + "^####/##/##", + "^####-##-##", + "^####.##.##", + "^##/CCC/####", + "^##.CCC.####", + "^##/##/##$", + "^##/##/## ", + "^##/CCC/##$", + "^##.CCC.## ",) + reString = "|".join(dateMasks) + filter = re.compile( reString) + if re.match(filter,fmtstring): return True + return False + +def _isTimeType( fmtstring ): + """ Checks the mask and returns True if it fits an allowed + time format. + """ + reTimeMask = "^##:##(:##)?( (AM|PM))?" + filter = re.compile( reTimeMask ) + if re.match(filter,fmtstring): return True + return False + + +def _isFloatingPoint( fmtstring): + filter = re.compile("[ ]?[#]+\.[#]+\n") + if re.match(filter,fmtstring+"\n"): return True + return False + + +def _isInteger( fmtstring ): + filter = re.compile("[#]+\n") + if re.match(filter,fmtstring+"\n"): return True + return False + + +def _getDateParts( dateStr, dateFmt ): + if len(dateStr) > 11: clip = dateStr[0:11] + else: clip = dateStr + if clip[-2] not in string.digits: + clip = clip[:-1] # (got part of time; drop it) + + dateSep = (('/' in clip) * '/') + (('-' in clip) * '-') + (('.' in clip) * '.') + slices = clip.split(dateSep) + if dateFmt == "MDY": + y,m,d = (slices[2],slices[0],slices[1]) ## year, month, date parts + elif dateFmt == "DMY": + y,m,d = (slices[2],slices[1],slices[0]) ## year, month, date parts + elif dateFmt == "YMD": + y,m,d = (slices[0],slices[1],slices[2]) ## year, month, date parts + else: + y,m,d = None, None, None + if not y: + return None + else: + return y,m,d + + +def _getDateSepChar(dateStr): + clip = dateStr[0:10] + dateSep = (('/' in clip) * '/') + (('-' in clip) * '-') + (('.' in clip) * '.') + return dateSep + + +def _makeDate( year, month, day, dateFmt, dateStr): + sep = _getDateSepChar( dateStr) + if dateFmt == "MDY": + return "%s%s%s%s%s" % (month,sep,day,sep,year) ## year, month, date parts + elif dateFmt == "DMY": + return "%s%s%s%s%s" % (day,sep,month,sep,year) ## year, month, date parts + elif dateFmt == "YMD": + return "%s%s%s%s%s" % (year,sep,month,sep,day) ## year, month, date parts + else: + return None + + +def _getYear(dateStr,dateFmt): + parts = _getDateParts( dateStr, dateFmt) + return parts[0] + +def _getMonth(dateStr,dateFmt): + parts = _getDateParts( dateStr, dateFmt) + return parts[1] + +def _getDay(dateStr,dateFmt): + parts = _getDateParts( dateStr, dateFmt) + return parts[2] + +## ---------- ---------- ---------- ---------- ---------- ---------- ---------- +class __test(wx.App): + def OnInit(self): + from wx.lib.rcsizer import RowColSizer + self.frame = wx.Frame( None, -1, "MaskedEditMixin 0.0.7 Demo Page #1", size = (700,600)) + self.panel = wx.Panel( self.frame, -1) + self.sizer = RowColSizer() + self.labels = [] + self.editList = [] + rowcount = 4 + + id, id1 = wx.NewId(), wx.NewId() + self.command1 = wx.Button( self.panel, id, "&Close" ) + self.command2 = wx.Button( self.panel, id1, "&AutoFormats" ) + self.sizer.Add(self.command1, row=0, col=0, flag=wx.ALL, border = 5) + self.sizer.Add(self.command2, row=0, col=1, colspan=2, flag=wx.ALL, border = 5) + self.panel.Bind(wx.EVT_BUTTON, self.onClick, self.command1 ) +## self.panel.SetDefaultItem(self.command1 ) + self.panel.Bind(wx.EVT_BUTTON, self.onClickPage, self.command2) + + self.check1 = wx.CheckBox( self.panel, -1, "Disallow Empty" ) + self.check2 = wx.CheckBox( self.panel, -1, "Highlight Empty" ) + self.sizer.Add( self.check1, row=0,col=3, flag=wx.ALL,border=5 ) + self.sizer.Add( self.check2, row=0,col=4, flag=wx.ALL,border=5 ) + self.panel.Bind(wx.EVT_CHECKBOX, self._onCheck1, self.check1 ) + self.panel.Bind(wx.EVT_CHECKBOX, self._onCheck2, self.check2 ) + + + label = """Press ctrl-s in any field to output the value and plain value. Press ctrl-x to clear and re-set any field. +Note that all controls have been auto-sized by including F in the format code. +Try entering nonsensical or partial values in validated fields to see what happens (use ctrl-s to test the valid status).""" + label2 = "\nNote that the State and Last Name fields are list-limited (Name:Smith,Jones,Williams)." + + self.label1 = wx.StaticText( self.panel, -1, label) + self.label2 = wx.StaticText( self.panel, -1, "Description") + self.label3 = wx.StaticText( self.panel, -1, "Mask Value") + self.label4 = wx.StaticText( self.panel, -1, "Format") + self.label5 = wx.StaticText( self.panel, -1, "Reg Expr Val. (opt)") + self.label6 = wx.StaticText( self.panel, -1, "MaskedEdit Ctrl") + self.label7 = wx.StaticText( self.panel, -1, label2) + self.label7.SetForegroundColour("Blue") + self.label1.SetForegroundColour("Blue") + self.label2.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) + self.label3.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) + self.label4.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) + self.label5.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) + self.label6.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) + + self.sizer.Add( self.label1, row=1,col=0,colspan=7, flag=wx.ALL,border=5) + self.sizer.Add( self.label7, row=2,col=0,colspan=7, flag=wx.ALL,border=5) + self.sizer.Add( self.label2, row=3,col=0, flag=wx.ALL,border=5) + self.sizer.Add( self.label3, row=3,col=1, flag=wx.ALL,border=5) + self.sizer.Add( self.label4, row=3,col=2, flag=wx.ALL,border=5) + self.sizer.Add( self.label5, row=3,col=3, flag=wx.ALL,border=5) + self.sizer.Add( self.label6, row=3,col=4, flag=wx.ALL,border=5) + + # The following list is of the controls for the demo. Feel free to play around with + # the options! + controls = [ + #description mask excl format regexp range,list,initial + ("Phone No", "(###) ###-#### x:###", "", 'F!^-R', "^\(\d\d\d\) \d\d\d-\d\d\d\d", (),[],''), + ("Last Name Only", "C{14}", "", 'F {list}', '^[A-Z][a-zA-Z]+', (),('Smith','Jones','Williams'),''), + ("Full Name", "C{14}", "", 'F_', '^[A-Z][a-zA-Z]+ [A-Z][a-zA-Z]+', (),[],''), + ("Social Sec#", "###-##-####", "", 'F', "\d{3}-\d{2}-\d{4}", (),[],''), + ("U.S. Zip+4", "#{5}-#{4}", "", 'F', "\d{5}-(\s{4}|\d{4})",(),[],''), + ("U.S. State (2 char)\n(with default)","AA", "", 'F!', "[A-Z]{2}", (),states, 'AZ'), + ("Customer No", "\CAA-###", "", 'F!', "C[A-Z]{2}-\d{3}", (),[],''), + ("Date (MDY) + Time\n(with default)", "##/##/#### ##:## AM", 'BCDEFGHIJKLMNOQRSTUVWXYZ','DFR!',"", (),[], r'03/05/2003 12:00 AM'), + ("Invoice Total", "#{9}.##", "", 'F-R,', "", (),[], ''), + ("Integer (signed)\n(with default)", "#{6}", "", 'F-R', "", (),[], '0 '), + ("Integer (unsigned)\n(with default), 1-399", "######", "", 'F', "", (1,399),[], '1 '), + ("Month selector", "XXX", "", 'F', "", (), + ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],""), + ("fraction selector","#/##", "", 'F', "^\d\/\d\d?", (), + ['2/3', '3/4', '1/2', '1/4', '1/8', '1/16', '1/32', '1/64'], "") + ] + + for control in controls: + self.sizer.Add( wx.StaticText( self.panel, -1, control[0]),row=rowcount, col=0,border=5,flag=wx.ALL) + self.sizer.Add( wx.StaticText( self.panel, -1, control[1]),row=rowcount, col=1,border=5, flag=wx.ALL) + self.sizer.Add( wx.StaticText( self.panel, -1, control[3]),row=rowcount, col=2,border=5, flag=wx.ALL) + self.sizer.Add( wx.StaticText( self.panel, -1, control[4][:20]),row=rowcount, col=3,border=5, flag=wx.ALL) + + if control in controls[:]:#-2]: + newControl = MaskedTextCtrl( self.panel, -1, "", + mask = control[1], + excludeChars = control[2], + formatcodes = control[3], + includeChars = "", + validRegex = control[4], + validRange = control[5], + choices = control[6], + defaultValue = control[7], + demo = True) + if control[6]: newControl.SetCtrlParameters(choiceRequired = True) + else: + newControl = MaskedComboBox( self.panel, -1, "", + choices = control[7], + choiceRequired = True, + mask = control[1], + formatcodes = control[3], + excludeChars = control[2], + includeChars = "", + validRegex = control[4], + validRange = control[5], + demo = True) + self.editList.append( newControl ) + + self.sizer.Add( newControl, row=rowcount,col=4,flag=wx.ALL,border=5) + rowcount += 1 + + self.sizer.AddGrowableCol(4) + + self.panel.SetSizer(self.sizer) + self.panel.SetAutoLayout(1) + + self.frame.Show(1) + self.MainLoop() + + return True + + def onClick(self, event): + self.frame.Close() + + def onClickPage(self, event): + self.page2 = __test2(self.frame,-1,"") + self.page2.Show(True) + + def _onCheck1(self,event): + """ Set required value on/off """ + value = event.IsChecked() + if value: + for control in self.editList: + control.SetCtrlParameters(emptyInvalid=True) + control.Refresh() + else: + for control in self.editList: + control.SetCtrlParameters(emptyInvalid=False) + control.Refresh() + self.panel.Refresh() + + def _onCheck2(self,event): + """ Highlight empty values""" + value = event.IsChecked() + if value: + for control in self.editList: + control.SetCtrlParameters( emptyBackgroundColour = 'Aquamarine') + control.Refresh() + else: + for control in self.editList: + control.SetCtrlParameters( emptyBackgroundColour = 'White') + control.Refresh() + self.panel.Refresh() + + +## ---------- ---------- ---------- ---------- ---------- ---------- ---------- + +class __test2(wx.Frame): + def __init__(self, parent, id, caption): + wx.Frame.__init__( self, parent, id, "MaskedEdit control 0.0.7 Demo Page #2 -- AutoFormats", size = (550,600)) + from wx.lib.rcsizer import RowColSizer + self.panel = wx.Panel( self, -1) + self.sizer = RowColSizer() + self.labels = [] + self.texts = [] + rowcount = 4 + + label = """\ +All these controls have been created by passing a single parameter, the AutoFormat code. +The class contains an internal dictionary of types and formats (autoformats). +To see a great example of validations in action, try entering a bad email address, then tab out.""" + + self.label1 = wx.StaticText( self.panel, -1, label) + self.label2 = wx.StaticText( self.panel, -1, "Description") + self.label3 = wx.StaticText( self.panel, -1, "AutoFormat Code") + self.label4 = wx.StaticText( self.panel, -1, "MaskedEdit Control") + self.label1.SetForegroundColour("Blue") + self.label2.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) + self.label3.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) + self.label4.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) + + self.sizer.Add( self.label1, row=1,col=0,colspan=3, flag=wx.ALL,border=5) + self.sizer.Add( self.label2, row=3,col=0, flag=wx.ALL,border=5) + self.sizer.Add( self.label3, row=3,col=1, flag=wx.ALL,border=5) + self.sizer.Add( self.label4, row=3,col=2, flag=wx.ALL,border=5) + + id, id1 = wx.NewId(), wx.NewId() + self.command1 = wx.Button( self.panel, id, "&Close") + self.command2 = wx.Button( self.panel, id1, "&Print Formats") + self.panel.Bind(wx.EVT_BUTTON, self.onClick, self.command1) + self.panel.SetDefaultItem(self.command1) + self.panel.Bind(wx.EVT_BUTTON, self.onClickPrint, self.command2) + + # The following list is of the controls for the demo. Feel free to play around with + # the options! + controls = [ + ("Phone No","USPHONEFULLEXT"), + ("US Date + Time","USDATETIMEMMDDYYYY/HHMM"), + ("US Date MMDDYYYY","USDATEMMDDYYYY/"), + ("Time (with seconds)","TIMEHHMMSS"), + ("Military Time\n(without seconds)","24HRTIMEHHMM"), + ("Social Sec#","USSOCIALSEC"), + ("Credit Card","CREDITCARD"), + ("Expiration MM/YY","EXPDATEMMYY"), + ("Percentage","PERCENT"), + ("Person's Age","AGE"), + ("US Zip Code","USZIP"), + ("US Zip+4","USZIPPLUS4"), + ("Email Address","EMAIL"), + ("IP Address", "(derived control IpAddrCtrl)") + ] + + for control in controls: + self.sizer.Add( wx.StaticText( self.panel, -1, control[0]),row=rowcount, col=0,border=5,flag=wx.ALL) + self.sizer.Add( wx.StaticText( self.panel, -1, control[1]),row=rowcount, col=1,border=5, flag=wx.ALL) + if control in controls[:-1]: + self.sizer.Add( MaskedTextCtrl( self.panel, -1, "", + autoformat = control[1], + demo = True), + row=rowcount,col=2,flag=wx.ALL,border=5) + else: + self.sizer.Add( IpAddrCtrl( self.panel, -1, "", demo=True ), + row=rowcount,col=2,flag=wx.ALL,border=5) + rowcount += 1 + + self.sizer.Add(self.command1, row=0, col=0, flag=wx.ALL, border = 5) + self.sizer.Add(self.command2, row=0, col=1, flag=wx.ALL, border = 5) + self.sizer.AddGrowableCol(3) + + self.panel.SetSizer(self.sizer) + self.panel.SetAutoLayout(1) + + def onClick(self, event): + self.Close() + + def onClickPrint(self, event): + for format in masktags.keys(): + sep = "+------------------------+" + print "%s\n%s \n Mask: %s \n RE Validation string: %s\n" % (sep,format, masktags[format]['mask'], masktags[format]['validRegex']) + +## ---------- ---------- ---------- ---------- ---------- ---------- ---------- + +if __name__ == "__main__": + app = __test(False) + +__i=0 +## +## Current Issues: +## =================================== +## +## 1. WS: For some reason I don't understand, the control is generating two (2) +## EVT_TEXT events for every one (1) .SetValue() of the underlying control. +## I've been unsuccessful in determining why or in my efforts to make just one +## occur. So, I've added a hack to save the last seen value from the +## control in the EVT_TEXT handler, and if *different*, call event.Skip() +## to propagate it down the event chain, and let the application see it. +## +## 2. WS: MaskedComboBox is deficient in several areas, all having to do with the +## behavior of the underlying control that I can't fix. The problems are: +## a) The background coloring doesn't work in the text field of the control; +## instead, there's a only border around it that assumes the correct color. +## b) The control will not pass WXK_TAB to the event handler, no matter what +## I do, and there's no style wxCB_PROCESS_TAB like wxTE_PROCESS_TAB to +## indicate that we want these events. As a result, MaskedComboBox +## doesn't do the nice field-tabbing that MaskedTextCtrl does. +## c) Auto-complete had to be reimplemented for the control because programmatic +## setting of the value of the text field does not set up the auto complete +## the way that the control processing keystrokes does. (But I think I've +## implemented a fairly decent approximation.) Because of this the control +## also won't auto-complete on dropdown, and there's no event I can catch +## to work around this problem. +## d) There is no method provided for getting the selection; the hack I've +## implemented has its flaws, not the least of which is that due to the +## strategy that I'm using, the paste buffer is always replaced by the +## contents of the control's selection when in focus, on each keystroke; +## this makes it impossible to paste anything into a MaskedComboBox +## at the moment... :-( +## e) The other deficient behavior, likely induced by the workaround for (d), +## is that you can can't shift-left to select more than one character +## at a time. +## +## +## 3. WS: Controls on wxPanels don't seem to pass Shift-WXK_TAB to their +## EVT_KEY_DOWN or EVT_CHAR event handlers. Until this is fixed in +## wxWindows, shift-tab won't take you backwards through the fields of +## a MaskedTextCtrl like it should. Until then Shifted arrow keys will +## work like shift-tab and tab ought to. +## + +## To-Do's: +## =============================## +## 1. Add Popup list for auto-completable fields that simulates combobox on individual +## fields. Example: City validates against list of cities, or zip vs zip code list. +## 2. Allow optional monetary symbols (eg. $, pounds, etc.) at front of a "decimal" +## control. +## 3. Fix shift-left selection for MaskedComboBox. +## 5. Transform notion of "decimal control" to be less "entire control"-centric, +## so that monetary symbols can be included and still have the appropriate +## semantics. (Big job, as currently written, but would make control even +## more useful for business applications.) + + +## CHANGELOG: +## ==================== +## Version 1.13 +## 1. Added parameter option stopFieldChangeIfInvalid, which can be used to relax the +## validation rules for a control, but make best efforts to stop navigation out of +## that field should its current value be invalid. Note: this does not prevent the +## value from remaining invalid if focus for the control is lost, via mousing etc. +## +## Version 1.12 +## 1. Added proper support for NUMPAD keypad keycodes for navigation and control. +## +## Version 1.11 +## 1. Added value member to ValueError exceptions, so that people can catch them +## and then display their own errors, and added attribute raiseOnInvalidPaste, +## so one doesn't have to subclass the controls simply to force generation of +## a ValueError on a bad paste operation. +## 2. Fixed handling of unicode charsets by converting to explicit control char +## set testing for passing those keystrokes to the base control, and then +## changing the semantics of the * maskchar to indicate any visible char. +## 3. Added '|' mask specification character, which allows splitting of contiguous +## mask characters into separate fields, allowing finer control of behavior +## of a control. +## +## +## Version 1.10 +## 1. Added handling for WXK_DELETE and WXK_INSERT, such that shift-delete +## cuts, shift-insert pastes, and ctrl-insert copies. +## +## Version 1.9 +## 1. Now ignores kill focus events when being destroyed. +## 2. Added missing call to set insertion point on changing fields. +## 3. Modified SetKeyHandler() to accept None as means of removing one. +## 4. Fixed keyhandler processing for group and decimal character changes. +## 5. Fixed a problem that prevented input into the integer digit of a +## integerwidth=1 numctrl, if the current value was 0. +## 6. Fixed logic involving processing of "_signOk" flag, to remove default +## sign key handlers if false, so that SetAllowNegative(False) in the +## NumCtrl works properly. +## 7. Fixed selection logic for numeric controls so that if selectOnFieldEntry +## is true, and the integer portion of an integer format control is selected +## and the sign position is selected, the sign keys will always result in a +## negative value, rather than toggling the previous sign. +## +## +## Version 1.8 +## 1. Fixed bug involving incorrect variable name, causing combobox autocomplete to fail. +## 2. Added proper support for unicode version of wxPython +## 3. Added * as mask char meaning "all ansi chars" (ordinals 32-255). +## 4. Converted doc strings to use reST format, for ePyDoc documentation. +## 5. Renamed helper functions, classes, etc. not intended to be visible in public +## interface to code. +## +## Version 1.7 +## 1. Fixed intra-right-insert-field erase, such that it doesn't leave a hole, but instead +## shifts the text to the left accordingly. +## 2. Fixed _SetValue() to place cursor after last character inserted, rather than end of +## mask. +## 3. Fixed some incorrect undo behavior for right-insert fields, and allowed derived classes +## (eg. numctrl) to pass modified values for undo processing (to handle/ignore grouping +## chars properly.) +## 4. Fixed autoselect behavior to work similarly to (2) above, so that combobox +## selection will only select the non-empty text, as per request. +## 5. Fixed tabbing to work with 2.5.2 semantics. +## 6. Fixed size calculation to handle changing fonts +## +## Version 1.6 +## 1. Reorganized masked controls into separate package, renamed things accordingly +## 2. Split actual controls out of this file into their own files. +## Version 1.5 +## (Reported) bugs fixed: +## 1. Crash ensues if you attempt to change the mask of a read-only +## MaskedComboBox after initial construction. +## 2. Changed strategy of defining Get/Set property functions so that +## these are now generated dynamically at runtime, rather than as +## part of the class definition. (This makes it possible to have +## more general base classes that have many more options for configuration +## without requiring that derivations support the same options.) +## 3. Fixed IsModified for _Paste() and _OnErase(). +## +## Enhancements: +## 1. Fixed "attribute function inheritance," since base control is more +## generic than subsequent derivations, not all property functions of a +## generic control should be exposed in those derivations. New strategy +## uses base control classes (eg. BaseMaskedTextCtrl) that should be +## used to derive new class types, and mixed with their own mixins to +## only expose those attributes from the generic masked controls that +## make sense for the derivation. (This makes Boa happier.) +## 2. Renamed (with b-c) MILTIME autoformats to 24HRTIME, so as to be less +## "parochial." +## +## Version 1.4 +## (Reported) bugs fixed: +## 1. Right-click menu allowed "cut" operation that destroyed mask +## (was implemented by base control) +## 2. MaskedComboBox didn't allow .Append() of mixed-case values; all +## got converted to lower case. +## 3. MaskedComboBox selection didn't deal with spaces in values +## properly when autocompleting, and didn't have a concept of "next" +## match for handling choice list duplicates. +## 4. Size of MaskedComboBox was always default. +## 5. Email address regexp allowed some "non-standard" things, and wasn't +## general enough. +## 6. Couldn't easily reset MaskedComboBox contents programmatically. +## 7. Couldn't set emptyInvalid during construction. +## 8. Under some versions of wxPython, readonly comboboxes can apparently +## return a GetInsertionPoint() result (655535), causing masked control +## to fail. +## 9. Specifying an empty mask caused the controls to traceback. +## 10. Can't specify float ranges for validRange. +## 11. '.' from within a the static portion of a restricted IP address +## destroyed the mask from that point rightward; tab when cursor is +## before 1st field takes cursor past that field. +## +## Enhancements: +## 12. Added Ctrl-Z/Undo handling, (and implemented context-menu properly.) +## 13. Added auto-select option on char input for masked controls with +## choice lists. +## 14. Added '>' formatcode, allowing insert within a given or each field +## as appropriate, rather than requiring "overwrite". This makes single +## field controls that just have validation rules (eg. EMAIL) much more +## friendly. The same flag controls left shift when deleting vs just +## blanking the value, and for right-insert fields, allows right-insert +## at any non-blank (non-sign) position in the field. +## 15. Added option to use to indicate negative values for numeric controls. +## 16. Improved OnFocus handling of numeric controls. +## 17. Enhanced Home/End processing to allow operation on a field level, +## using ctrl key. +## 18. Added individual Get/Set functions for control parameters, for +## simplified integration with Boa Constructor. +## 19. Standardized "Colour" parameter names to match wxPython, with +## non-british spellings still supported for backward-compatibility. +## 20. Added '&' mask specification character for punctuation only (no letters +## or digits). +## 21. Added (in a separate file) wx.MaskedCtrl() factory function to provide +## unified interface to the masked edit subclasses. +## +## +## Version 1.3 +## 1. Made it possible to configure grouping, decimal and shift-decimal characters, +## to make controls more usable internationally. +## 2. Added code to smart "adjust" value strings presented to .SetValue() +## for right-aligned numeric format controls if they are shorter than +## than the control width, prepending the missing portion, prepending control +## template left substring for the missing characters, so that setting +## numeric values is easier. +## 3. Renamed SetMaskParameters SetCtrlParameters() (with old name preserved +## for b-c), as this makes more sense. +## +## Version 1.2 +## 1. Fixed .SetValue() to replace the current value, rather than the current +## selection. Also changed it to generate ValueError if presented with +## either a value which doesn't follow the format or won't fit. Also made +## set value adjust numeric and date controls as if user entered the value. +## Expanded doc explaining how SetValue() works. +## 2. Fixed EUDATE* autoformats, fixed IsDateType mask list, and added ability to +## use 3-char months for dates, and EUDATETIME, and EUDATEMILTIME autoformats. +## 3. Made all date autoformats automatically pick implied "datestyle". +## 4. Added IsModified override, since base wx.TextCtrl never reports modified if +## .SetValue used to change the value, which is what the masked edit controls +## use internally. +## 5. Fixed bug in date position adjustment on 2 to 4 digit date conversion when +## using tab to "leave field" and auto-adjust. +## 6. Fixed bug in _isCharAllowed() for negative number insertion on pastes, +## and bug in ._Paste() that didn't account for signs in signed masks either. +## 7. Fixed issues with _adjustPos for right-insert fields causing improper +## selection/replacement of values +## 8. Fixed _OnHome handler to properly handle extending current selection to +## beginning of control. +## 9. Exposed all (valid) autoformats to demo, binding descriptions to +## autoformats. +## 10. Fixed a couple of bugs in email regexp. +## 11. Made maskchardict an instance var, to make mask chars to be more +## amenable to international use. +## 12. Clarified meaning of '-' formatcode in doc. +## 13. Fixed a couple of coding bugs being flagged by Python2.1. +## 14. Fixed several issues with sign positioning, erasure and validity +## checking for "numeric" masked controls. +## 15. Added validation to IpAddrCtrl.SetValue(). +## +## Version 1.1 +## 1. Changed calling interface to use boolean "useFixedWidthFont" (True by default) +## vs. literal font facename, and use wxTELETYPE as the font family +## if so specified. +## 2. Switched to use of dbg module vs. locally defined version. +## 3. Revamped entire control structure to use Field classes to hold constraint +## and formatting data, to make code more hierarchical, allow for more +## sophisticated masked edit construction. +## 4. Better strategy for managing options, and better validation on keywords. +## 5. Added 'V' format code, which requires that in order for a character +## to be accepted, it must result in a string that passes the validRegex. +## 6. Added 'S' format code which means "select entire field when navigating +## to new field." +## 7. Added 'r' format code to allow "right-insert" fields. (implies 'R'--right-alignment) +## 8. Added '<' format code to allow fields to require explicit cursor movement +## to leave field. +## 9. Added validFunc option to other validation mechanisms, that allows derived +## classes to add dynamic validation constraints to the control. +## 10. Fixed bug in validatePaste code causing possible IndexErrors, and also +## fixed failure to obey case conversion codes when pasting. +## 11. Implemented '0' (zero-pad) formatting code, as it wasn't being done anywhere... +## 12. Removed condition from OnDecimalPoint, so that it always truncates right on '.' +## 13. Enhanced IpAddrCtrl to use right-insert fields, selection on field traversal, +## individual field validation to prevent field values > 255, and require explicit +## tab/. to change fields. +## 14. Added handler for left double-click to select field under cursor. +## 15. Fixed handling for "Read-only" styles. +## 16. Separated signedForegroundColor from 'R' style, and added foregroundColor +## attribute, for more consistent and controllable coloring. +## 17. Added retainFieldValidation parameter, allowing top-level constraints +## such as "validRequired" to be set independently of field-level equivalent. +## (needed in TimeCtrl for bounds constraints.) +## 18. Refactored code a bit, cleaned up and commented code more heavily, fixed +## some of the logic for setting/resetting parameters, eg. fillChar, defaultValue, +## etc. +## 19. Fixed maskchar setting for upper/lowercase, to work in all locales. +## +## +## Version 1.0 +## 1. Decimal point behavior restored for decimal and integer type controls: +## decimal point now trucates the portion > 0. +## 2. Return key now works like the tab character and moves to the next field, +## provided no default button is set for the form panel on which the control +## resides. +## 3. Support added in _FindField() for subclasses controls (like timecontrol) +## to determine where the current insertion point is within the mask (i.e. +## which sub-'field'). See method documentation for more info and examples. +## 4. Added Field class and support for all constraints to be field-specific +## in addition to being globally settable for the control. +## Choices for each field are validated for length and pastability into +## the field in question, raising ValueError if not appropriate for the control. +## Also added selective additional validation based on individual field constraints. +## By default, SHIFT-WXK_DOWN, SHIFT-WXK_UP, WXK_PRIOR and WXK_NEXT all +## auto-complete fields with choice lists, supplying the 1st entry in +## the choice list if the field is empty, and cycling through the list in +## the appropriate direction if already a match. WXK_DOWN will also auto- +## complete if the field is partially completed and a match can be made. +## SHIFT-WXK_UP/DOWN will also take you to the next field after any +## auto-completion performed. +## 5. Added autoCompleteKeycodes=[] parameters for allowing further +## customization of the control. Any keycode supplied as a member +## of the _autoCompleteKeycodes list will be treated like WXK_NEXT. If +## requireFieldChoice is set, then a valid value from each non-empty +## choice list will be required for the value of the control to validate. +## 6. Fixed "auto-sizing" to be relative to the font actually used, rather +## than making assumptions about character width. +## 7. Fixed GetMaskParameter(), which was non-functional in previous version. +## 8. Fixed exceptions raised to provide info on which control had the error. +## 9. Fixed bug in choice management of MaskedComboBox. +## 10. Fixed bug in IpAddrCtrl causing traceback if field value was of +## the form '# #'. Modified control code for IpAddrCtrl so that '.' +## in the middle of a field clips the rest of that field, similar to +## decimal and integer controls. +## +## +## Version 0.0.7 +## 1. "-" is a toggle for sign; "+" now changes - signed numerics to positive. +## 2. ',' in formatcodes now causes numeric values to be comma-delimited (e.g.333,333). +## 3. New support for selecting text within the control.(thanks Will Sadkin!) +## Shift-End and Shift-Home now select text as you would expect +## Control-Shift-End selects to the end of the mask string, even if value not entered. +## Control-A selects all *entered* text, Shift-Control-A selects everything in the control. +## 4. event.Skip() added to onKillFocus to correct remnants when running in Linux (contributed- +## for some reason I couldn't find the original email but thanks!!!) +## 5. All major key-handling code moved to their own methods for easier subclassing: OnHome, +## OnErase, OnEnd, OnCtrl_X, OnCtrl_A, etc. +## 6. Email and autoformat validations corrected using regex provided by Will Sadkin (thanks!). +## (The rest of the changes in this version were done by Will Sadkin with permission from Jeff...) +## 7. New mechanism for replacing default behavior for any given key, using +## ._SetKeycodeHandler(keycode, func) and ._SetKeyHandler(char, func) now available +## for easier subclassing of the control. +## 8. Reworked the delete logic, cut, paste and select/replace logic, as well as some bugs +## with insertion point/selection modification. Changed Ctrl-X to use standard "cut" +## semantics, erasing the selection, rather than erasing the entire control. +## 9. Added option for an "default value" (ie. the template) for use when a single fillChar +## is not desired in every position. Added IsDefault() function to mean "does the value +## equal the template?" and modified .IsEmpty() to mean "do all of the editable +## positions in the template == the fillChar?" +## 10. Extracted mask logic into mixin, so we can have both MaskedTextCtrl and MaskedComboBox, +## now included. +## 11. MaskedComboBox now adds the capability to validate from list of valid values. +## Example: City validates against list of cities, or zip vs zip code list. +## 12. Fixed oversight in EVT_TEXT handler that prevented the events from being +## passed to the next handler in the event chain, causing updates to the +## control to be invisible to the parent code. +## 13. Added IPADDR autoformat code, and subclass IpAddrCtrl for controlling tabbing within +## the control, that auto-reformats as you move between cells. +## 14. Mask characters [A,a,X,#] can now appear in the format string as literals, by using '\'. +## 15. It is now possible to specify repeating masks, e.g. #{3}-#{3}-#{14} +## 16. Fixed major bugs in date validation, due to the fact that +## wxDateTime.ParseDate is too liberal, and will accept any form that +## makes any kind of sense, regardless of the datestyle you specified +## for the control. Unfortunately, the strategy used to fix it only +## works for versions of wxPython post 2.3.3.1, as a C++ assert box +## seems to show up on an invalid date otherwise, instead of a catchable +## exception. +## 17. Enhanced date adjustment to automatically adjust heuristic based on +## current year, making last century/this century determination on +## 2-digit year based on distance between today's year and value; +## if > 50 year separation, assume last century (and don't assume last +## century is 20th.) +## 18. Added autoformats and support for including HHMMSS as well as HHMM for +## date times, and added similar time, and militaray time autoformats. +## 19. Enhanced tabbing logic so that tab takes you to the next field if the +## control is a multi-field control. +## 20. Added stub method called whenever the control "changes fields", that +## can be overridden by subclasses (eg. IpAddrCtrl.) +## 21. Changed a lot of code to be more functionally-oriented so side-effects +## aren't as problematic when maintaining code and/or adding features. +## Eg: IsValid() now does not have side-effects; it merely reflects the +## validity of the value of the control; to determine validity AND recolor +## the control, _CheckValid() should be used with a value argument of None. +## Similarly, made most reformatting function take an optional candidate value +## rather than just using the current value of the control, and only +## have them change the value of the control if a candidate is not specified. +## In this way, you can do validation *before* changing the control. +## 22. Changed validRequired to mean "disallow chars that result in invalid +## value." (Old meaning now represented by emptyInvalid.) (This was +## possible once I'd made the changes in (19) above.) +## 23. Added .SetMaskParameters and .GetMaskParameter methods, so they +## can be set/modified/retrieved after construction. Removed individual +## parameter setting functions, in favor of this mechanism, so that +## all adjustment of the control based on changing parameter values can +## be handled in one place with unified mechanism. +## 24. Did a *lot* of testing and fixing re: numeric values. Added ability +## to type "grouping char" (ie. ',') and validate as appropriate. +## 25. Fixed ZIPPLUS4 to allow either 5 or 4, but if > 5 must be 9. +## 26. Fixed assumption about "decimal or integer" masks so that they're only +## made iff there's no validRegex associated with the field. (This +## is so things like zipcodes which look like integers can have more +## restrictive validation (ie. must be 5 digits.) +## 27. Added a ton more doc strings to explain use and derivation requirements +## and did regularization of the naming conventions. +## 28. Fixed a range bug in _adjustKey preventing z from being handled properly. +## 29. Changed behavior of '.' (and shift-.) in numeric controls to move to +## reformat the value and move the next field as appropriate. (shift-'.', +## ie. '>' moves to the previous field. + +## Version 0.0.6 +## 1. Fixed regex bug that caused autoformat AGE to invalidate any age ending +## in '0'. +## 2. New format character 'D' to trigger date type. If the user enters 2 digits in the +## year position, the control will expand the value to four digits, using numerals below +## 50 as 21st century (20+nn) and less than 50 as 20th century (19+nn). +## Also, new optional parameter datestyle = set to one of {MDY|DMY|YDM} +## 3. revalid parameter renamed validRegex to conform to standard for all validation +## parameters (see 2 new ones below). +## 4. New optional init parameter = validRange. Used only for int/dec (numeric) types. +## Allows the developer to specify a valid low/high range of values. +## 5. New optional init parameter = validList. Used for character types. Allows developer +## to send a list of values to the control to be used for specific validation. +## See the Last Name Only example - it is list restricted to Smith/Jones/Williams. +## 6. Date type fields now use wxDateTime's parser to validate the date and time. +## This works MUCH better than my kludgy regex!! Thanks to Robin Dunn for pointing +## me toward this solution! +## 7. Date fields now automatically expand 2-digit years when it can. For example, +## if the user types "03/10/67", then "67" will auto-expand to "1967". If a two-year +## date is entered it will be expanded in any case when the user tabs out of the +## field. +## 8. New class functions: SetValidBackgroundColor, SetInvalidBackgroundColor, SetEmptyBackgroundColor, +## SetSignedForeColor allow accessto override default class coloring behavior. +## 9. Documentation updated and improved. +## 10. Demo - page 2 is now a wxFrame class instead of a wxPyApp class. Works better. +## Two new options (checkboxes) - test highlight empty and disallow empty. +## 11. Home and End now work more intuitively, moving to the first and last user-entry +## value, respectively. +## 12. New class function: SetRequired(bool). Sets the control's entry required flag +## (i.e. disallow empty values if True). +## +## Version 0.0.5 +## 1. get_plainValue method renamed to GetPlainValue following the wxWindows +## StudlyCaps(tm) standard (thanks Paul Moore). ;) +## 2. New format code 'F' causes the control to auto-fit (auto-size) itself +## based on the length of the mask template. +## 3. Class now supports "autoformat" codes. These can be passed to the class +## on instantiation using the parameter autoformat="code". If the code is in +## the dictionary, it will self set the mask, formatting, and validation string. +## I have included a number of samples, but I am hoping that someone out there +## can help me to define a whole bunch more. +## 4. I have added a second page to the demo (as well as a second demo class, test2) +## to showcase how autoformats work. The way they self-format and self-size is, +## I must say, pretty cool. +## 5. Comments added and some internal cosmetic revisions re: matching the code +## standards for class submission. +## 6. Regex validation is now done in real time - field turns yellow immediately +## and stays yellow until the entered value is valid +## 7. Cursor now skips over template characters in a more intuitive way (before the +## next keypress). +## 8. Change, Keypress and LostFocus methods added for convenience of subclasses. +## Developer may use these methods which will be called after EVT_TEXT, EVT_CHAR, +## and EVT_KILL_FOCUS, respectively. +## 9. Decimal and numeric handlers have been rewritten and now work more intuitively. +## +## Version 0.0.4 +## 1. New .IsEmpty() method returns True if the control's value is equal to the +## blank template string +## 2. Control now supports a new init parameter: revalid. Pass a regular expression +## that the value will have to match when the control loses focus. If invalid, +## the control's BackgroundColor will turn yellow, and an internal flag is set (see next). +## 3. Demo now shows revalid functionality. Try entering a partial value, such as a +## partial social security number. +## 4. New .IsValid() value returns True if the control is empty, or if the value matches +## the revalid expression. If not, .IsValid() returns False. +## 5. Decimal values now collapse to decimal with '.00' on losefocus if the user never +## presses the decimal point. +## 6. Cursor now goes to the beginning of the field if the user clicks in an +## "empty" field intead of leaving the insertion point in the middle of the +## field. +## 7. New "N" mask type includes upper and lower chars plus digits. a-zA-Z0-9. +## 8. New formatcodes init parameter replaces other init params and adds functions. +## String passed to control on init controls: +## _ Allow spaces +## ! Force upper +## ^ Force lower +## R Show negative #s in red +## , Group digits +## - Signed numerals +## 0 Numeric fields get leading zeros +## 9. Ctrl-X in any field clears the current value. +## 10. Code refactored and made more modular (esp in OnChar method). Should be more +## easy to read and understand. +## 11. Demo enhanced. +## 12. Now has _doc_. +## +## Version 0.0.3 +## 1. GetPlainValue() now returns the value without the template characters; +## so, for example, a social security number (123-33-1212) would return as +## 123331212; also removes white spaces from numeric/decimal values, so +## "- 955.32" is returned "-955.32". Press ctrl-S to see the plain value. +## 2. Press '.' in an integer style masked control and truncate any trailing digits. +## 3. Code moderately refactored. Internal names improved for clarity. Additional +## internal documentation. +## 4. Home and End keys now supported to move cursor to beginning or end of field. +## 5. Un-signed integers and decimals now supported. +## 6. Cosmetic improvements to the demo. +## 7. Class renamed to MaskedTextCtrl. +## 8. Can now specify include characters that will override the basic +## controls: for example, includeChars = "@." for email addresses +## 9. Added mask character 'C' -> allow any upper or lowercase character +## 10. .SetSignColor(str:color) sets the foreground color for negative values +## in signed controls (defaults to red) +## 11. Overview documentation written. +## +## Version 0.0.2 +## 1. Tab now works properly when pressed in last position +## 2. Decimal types now work (e.g. #####.##) +## 3. Signed decimal or numeric values supported (i.e. negative numbers) +## 4. Negative decimal or numeric values now can show in red. +## 5. Can now specify an "exclude list" with the excludeChars parameter. +## See date/time formatted example - you can only enter A or P in the +## character mask space (i.e. AM/PM). +## 6. Backspace now works properly, including clearing data from a selected +## region but leaving template characters intact. Also delete key. +## 7. Left/right arrows now work properly. +## 8. Removed EventManager call from test so demo should work with wxPython 2.3.3 +## diff --git a/wx/lib/masked/numctrl.py b/wx/lib/masked/numctrl.py new file mode 100644 index 00000000..16e75b5d --- /dev/null +++ b/wx/lib/masked/numctrl.py @@ -0,0 +1,1926 @@ +#---------------------------------------------------------------------------- +# Name: wxPython.lib.masked.numctrl.py +# Author: Will Sadkin +# Created: 09/06/2003 +# Copyright: (c) 2003-2007 by Will Sadkin +# RCS-ID: $Id$ +# License: wxWidgets license +#---------------------------------------------------------------------------- +# NOTE: +# This was written to provide a numeric edit control for wxPython that +# does things like right-insert (like a calculator), and does grouping, etc. +# (ie. the features of masked.TextCtrl), but allows Get/Set of numeric +# values, rather than text. +# +# Masked.NumCtrl permits integer, and floating point values to be set +# retrieved or set via .GetValue() and .SetValue() (type chosen based on +# fraction width, and provides an masked.EVT_NUM() event function for trapping +# changes to the control. +# +# It supports negative numbers as well as the naturals, and has the option +# of not permitting leading zeros or an empty control; if an empty value is +# not allowed, attempting to delete the contents of the control will result +# in a (selected) value of zero, thus preserving a legitimate numeric value. +# Similarly, replacing the contents of the control with '-' will result in +# a selected (absolute) value of -1. +# +# masked.NumCtrl also supports range limits, with the option of either +# enforcing them or simply coloring the text of the control if the limits +# are exceeded. +# +# masked.NumCtrl is intended to support fixed-point numeric entry, and +# is derived from BaseMaskedTextCtrl. As such, it supports a limited range +# of values to comply with a fixed-width entry mask. +#---------------------------------------------------------------------------- +# 12/09/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o Updated for wx namespace +# +# 12/20/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o wxMaskedEditMixin -> MaskedEditMixin +# o wxMaskedTextCtrl -> masked.TextCtrl +# o wxMaskedNumNumberUpdatedEvent -> masked.NumberUpdatedEvent +# o wxMaskedNumCtrl -> masked.NumCtrl +# + +""" +masked.NumCtrl: + - allows you to get and set integer or floating point numbers as value, + - provides bounds support and optional value limiting, + - has the right-insert input style that MaskedTextCtrl supports, + - provides optional automatic grouping, sign control and format, grouping and decimal + character selection, etc. etc. + + + Being derived from masked.TextCtrl, the control only allows + fixed-point notation. That is, it has a fixed (though reconfigurable) + maximum width for the integer portion and optional fixed width + fractional portion. + + Here's the API:: + + from wx.lib.masked import NumCtrl + + NumCtrl( + parent, id = -1, + value = 0, + pos = wx.DefaultPosition, + size = wx.DefaultSize, + style = 0, + validator = wx.DefaultValidator, + name = "masked.number", + integerWidth = 10, + fractionWidth = 0, + allowNone = False, + allowNegative = True, + useParensForNegatives = False, + groupDigits = False, + groupChar = ',', + decimalChar = '.', + min = None, + max = None, + limited = False, + limitOnFieldChange = False, + selectOnEntry = True, + foregroundColour = "Black", + signedForegroundColour = "Red", + emptyBackgroundColour = "White", + validBackgroundColour = "White", + invalidBackgroundColour = "Yellow", + autoSize = True + ) + + + value + If no initial value is set, the default will be zero, or + the minimum value, if specified. If an illegal string is specified, + a ValueError will result. (You can always later set the initial + value with SetValue() after instantiation of the control.) + + integerWidth + Indicates how many places to the right of any decimal point + should be allowed in the control. This will, perforce, limit + the size of the values that can be entered. This number need + not include space for grouping characters or the sign, if either + of these options are enabled, as the resulting underlying + mask is automatically by the control. The default of 10 + will allow any 32 bit integer value. The minimum value + for integerWidth is 1. + + fractionWidth + Indicates how many decimal places to show for numeric value. + If default (0), then the control will display and return only + integer or long values. + + allowNone + Boolean indicating whether or not the control is allowed to be + empty, representing a value of None for the control. + + allowNegative + Boolean indicating whether or not control is allowed to hold + negative numbers. + + useParensForNegatives + If true, this will cause negative numbers to be displayed with ()s + rather than -, (although '-' will still trigger a negative number.) + + groupDigits + Indicates whether or not grouping characters should be allowed and/or + inserted when leaving the control or the decimal character is entered. + + groupChar + What grouping character will be used if allowed. (By default ',') + + decimalChar + If fractionWidth is > 0, what character will be used to represent + the decimal point. (By default '.') + + min + The minimum value that the control should allow. This can be also be + adjusted with SetMin(). If the control is not limited, any value + below this bound will result in a background colored with the current + invalidBackgroundColour. If the min specified will not fit into the + control, the min setting will be ignored. + + max + The maximum value that the control should allow. This can be + adjusted with SetMax(). If the control is not limited, any value + above this bound will result in a background colored with the current + invalidBackgroundColour. If the max specified will not fit into the + control, the max setting will be ignored. + + limited + Boolean indicating whether the control prevents values from + exceeding the currently set minimum and maximum values (bounds). + If False and bounds are set, out-of-bounds values will + result in a background colored with the current invalidBackgroundColour. + + limitOnFieldChange + An alternative to limited, this boolean indicates whether or not a + field change should be allowed if the value in the control + is out of bounds. If True, and control focus is lost, this will also + cause the control to take on the nearest bound value. + + selectOnEntry + Boolean indicating whether or not the value in each field of the + control should be automatically selected (for replacement) when + that field is entered, either by cursor movement or tabbing. + This can be desirable when using these controls for rapid data entry. + + foregroundColour + Color value used for positive values of the control. + + signedForegroundColour + Color value used for negative values of the control. + + emptyBackgroundColour + What background color to use when the control is considered + "empty." (allow_none must be set to trigger this behavior.) + + validBackgroundColour + What background color to use when the control value is + considered valid. + + invalidBackgroundColour + Color value used for illegal values or values out-of-bounds of the + control when the bounds are set but the control is not limited. + + autoSize + Boolean indicating whether or not the control should set its own + width based on the integer and fraction widths. True by default. + Note: Setting this to False will produce seemingly odd + behavior unless the control is large enough to hold the maximum + specified value given the widths and the sign positions; if not, + the control will appear to "jump around" as the contents scroll. + (ie. autoSize is highly recommended.) + +-------------------------- + +masked.EVT_NUM(win, id, func) + Respond to a EVT_COMMAND_MASKED_NUMBER_UPDATED event, generated when + the value changes. Notice that this event will always be sent when the + control's contents changes - whether this is due to user input or + comes from the program itself (for example, if SetValue() is called.) + + +SetValue(int|long|float|string) + Sets the value of the control to the value specified, if + possible. The resulting actual value of the control may be + altered to conform to the format of the control, changed + to conform with the bounds set on the control if limited, + or colored if not limited but the value is out-of-bounds. + A ValueError exception will be raised if an invalid value + is specified. + +GetValue() + Retrieves the numeric value from the control. The value + retrieved will be either be returned as a long if the + fractionWidth is 0, or a float otherwise. + + +SetParameters(\*\*kwargs) + Allows simultaneous setting of various attributes + of the control after construction. Keyword arguments + allowed are the same parameters as supported in the constructor. + + +SetIntegerWidth(value) + Resets the width of the integer portion of the control. The + value must be >= 1, or an AttributeError exception will result. + This value should account for any grouping characters that might + be inserted (if grouping is enabled), but does not need to account + for the sign, as that is handled separately by the control. +GetIntegerWidth() + Returns the current width of the integer portion of the control, + not including any reserved sign position. + + +SetFractionWidth(value) + Resets the width of the fractional portion of the control. The + value must be >= 0, or an AttributeError exception will result. If + 0, the current value of the control will be truncated to an integer + value. +GetFractionWidth() + Returns the current width of the fractional portion of the control. + + +SetMin(min=None) + Resets the minimum value of the control. If a value of None + is provided, then the control will have no explicit minimum value. + If the value specified is greater than the current maximum value, + then the function returns False and the minimum will not change from + its current setting. On success, the function returns True. + + If successful and the current value is lower than the new lower + bound, if the control is limited, the value will be automatically + adjusted to the new minimum value; if not limited, the value in the + control will be colored as invalid. + + If min > the max value allowed by the width of the control, + the function will return False, and the min will not be set. + +GetMin() + Gets the current lower bound value for the control. + It will return None if no lower bound is currently specified. + + +SetMax(max=None) + Resets the maximum value of the control. If a value of None + is provided, then the control will have no explicit maximum value. + If the value specified is less than the current minimum value, then + the function returns False and the maximum will not change from its + current setting. On success, the function returns True. + + If successful and the current value is greater than the new upper + bound, if the control is limited the value will be automatically + adjusted to this maximum value; if not limited, the value in the + control will be colored as invalid. + + If max > the max value allowed by the width of the control, + the function will return False, and the max will not be set. + +GetMax() + Gets the current upper bound value for the control. + It will return None if no upper bound is currently specified. + + +SetBounds(min=None,max=None) + This function is a convenience function for setting the min and max + values at the same time. The function only applies the maximum bound + if setting the minimum bound is successful, and returns True + only if both operations succeed. Note: leaving out an argument + will remove the corresponding bound. +GetBounds() + This function returns a two-tuple (min,max), indicating the + current bounds of the control. Each value can be None if + that bound is not set. + + +IsInBounds(value=None) + Returns True if no value is specified and the current value + of the control falls within the current bounds. This function can also + be called with a value to see if that value would fall within the current + bounds of the given control. + + +SetLimited(bool) + If called with a value of True, this function will cause the control + to limit the value to fall within the bounds currently specified. + If the control's value currently exceeds the bounds, it will then + be limited accordingly. + If called with a value of False, this function will disable value + limiting, but coloring of out-of-bounds values will still take + place if bounds have been set for the control. + +GetLimited() + +IsLimited() + Returns True if the control is currently limiting the + value to fall within the current bounds. + +SetLimitOnFieldChange() + If called with a value of True, will cause the control to allow + out-of-bounds values, but will prevent field change if attempted + via navigation, and if the control loses focus, it will change + the value to the nearest bound. + +GetLimitOnFieldChange() + +IsLimitedOnFieldChange() + Returns True if the control is currently limiting the + value on field change. + + +SetAllowNone(bool) + If called with a value of True, this function will cause the control + to allow the value to be empty, representing a value of None. + If called with a value of False, this function will prevent the value + from being None. If the value of the control is currently None, + ie. the control is empty, then the value will be changed to that + of the lower bound of the control, or 0 if no lower bound is set. + +GetAllowNone() + +IsNoneAllowed() + Returns True if the control currently allows its + value to be None. + + +SetAllowNegative(bool) + If called with a value of True, this function will cause the + control to allow the value to be negative (and reserve space for + displaying the sign. If called with a value of False, and the + value of the control is currently negative, the value of the + control will be converted to the absolute value, and then + limited appropriately based on the existing bounds of the control + (if any). + +GetAllowNegative() + +IsNegativeAllowed() + Returns True if the control currently permits values + to be negative. + + +SetGroupDigits(bool) + If called with a value of True, this will make the control + automatically add and manage grouping characters to the presented + value in integer portion of the control. + +GetGroupDigits() + +IsGroupingAllowed() + Returns True if the control is currently set to group digits. + + +SetGroupChar() + Sets the grouping character for the integer portion of the + control. (The default grouping character this is ','. +GetGroupChar() + Returns the current grouping character for the control. + + +SetSelectOnEntry() + If called with a value of True, this will make the control + automatically select the contents of each field as it is entered + within the control. (The default is True.) + GetSelectOnEntry() + Returns True if the control currently auto selects + the field values on entry. + + +SetAutoSize(bool) + Resets the autoSize attribute of the control. +GetAutoSize() + Returns the current state of the autoSize attribute for the control. + +""" + +import copy +import string +import types + +import wx + +from sys import maxint +MAXINT = maxint # (constants should be in upper case) +MININT = -maxint-1 + +from wx.tools.dbg import Logger +from wx.lib.masked import MaskedEditMixin, Field, BaseMaskedTextCtrl +##dbg = Logger() +##dbg(enable=1) + +#---------------------------------------------------------------------------- + +wxEVT_COMMAND_MASKED_NUMBER_UPDATED = wx.NewEventType() +EVT_NUM = wx.PyEventBinder(wxEVT_COMMAND_MASKED_NUMBER_UPDATED, 1) + +#---------------------------------------------------------------------------- + +class NumberUpdatedEvent(wx.PyCommandEvent): + """ + Used to fire an EVT_NUM event whenever the value in a NumCtrl changes. + """ + + def __init__(self, id, value = 0, object=None): + wx.PyCommandEvent.__init__(self, wxEVT_COMMAND_MASKED_NUMBER_UPDATED, id) + + self.__value = value + self.SetEventObject(object) + + def GetValue(self): + """Retrieve the value of the control at the time + this event was generated.""" + return self.__value + + +#---------------------------------------------------------------------------- +class NumCtrlAccessorsMixin: + """ + Defines masked.NumCtrl's list of attributes having their own + Get/Set functions, ignoring those that make no sense for + a numeric control. + """ + exposed_basectrl_params = ( + 'decimalChar', + 'shiftDecimalChar', + 'groupChar', + 'useParensForNegatives', + 'defaultValue', + 'description', + + 'useFixedWidthFont', + 'autoSize', + 'signedForegroundColour', + 'emptyBackgroundColour', + 'validBackgroundColour', + 'invalidBackgroundColour', + + 'emptyInvalid', + 'validFunc', + 'validRequired', + 'stopFieldChangeIfInvalid', + ) + for param in exposed_basectrl_params: + propname = param[0].upper() + param[1:] + exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) + exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) + + if param.find('Colour') != -1: + # add non-british spellings, for backward-compatibility + propname.replace('Colour', 'Color') + + exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) + exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) + + + +#---------------------------------------------------------------------------- + +class NumCtrl(BaseMaskedTextCtrl, NumCtrlAccessorsMixin): + """ + Masked edit control supporting "native" numeric values, ie. .SetValue(3), for + example, and supporting a variety of formatting options, including automatic + rounding specifiable precision, grouping and decimal place characters, etc. + """ + + + valid_ctrl_params = { + 'integerWidth': 10, # by default allow all 32-bit integers + 'fractionWidth': 0, # by default, use integers + 'decimalChar': '.', # by default, use '.' for decimal point + 'allowNegative': True, # by default, allow negative numbers + 'useParensForNegatives': False, # by default, use '-' to indicate negatives + 'groupDigits': True, # by default, don't insert grouping + 'groupChar': ',', # by default, use ',' for grouping + 'min': None, # by default, no bounds set + 'max': None, + 'limited': False, # by default, no limiting even if bounds set + 'limitOnFieldChange': False, # by default, don't limit if changing fields, even if bounds set + 'allowNone': False, # by default, don't allow empty value + 'selectOnEntry': True, # by default, select the value of each field on entry + 'foregroundColour': "Black", + 'signedForegroundColour': "Red", + 'emptyBackgroundColour': "White", + 'validBackgroundColour': "White", + 'invalidBackgroundColour': "Yellow", + 'useFixedWidthFont': True, # by default, use a fixed-width font + 'autoSize': True, # by default, set the width of the control based on the mask + } + + + def __init__ ( + self, parent, id=-1, value = 0, + pos = wx.DefaultPosition, size = wx.DefaultSize, + style = wx.TE_PROCESS_TAB, validator = wx.DefaultValidator, + name = "masked.num", + **kwargs ): + +## dbg('masked.NumCtrl::__init__', indent=1) + + # Set defaults for control: +## dbg('setting defaults:') + for key, param_value in NumCtrl.valid_ctrl_params.items(): + # This is done this way to make setattr behave consistently with + # "private attribute" name mangling + setattr(self, '_' + key, copy.copy(param_value)) + + # Assign defaults for all attributes: + init_args = copy.deepcopy(NumCtrl.valid_ctrl_params) +## dbg('kwargs:', kwargs) + for key, param_value in kwargs.items(): + key = key.replace('Color', 'Colour') + if key not in NumCtrl.valid_ctrl_params.keys(): + raise AttributeError('invalid keyword argument "%s"' % key) + else: + init_args[key] = param_value +## dbg('init_args:', indent=1) + for key, param_value in init_args.items(): +## dbg('%s:' % key, param_value) + pass +## dbg(indent=0) + + # Process initial fields for the control, as part of construction: + if type(init_args['integerWidth']) != types.IntType: + raise AttributeError('invalid integerWidth (%s) specified; expected integer' % repr(init_args['integerWidth'])) + elif init_args['integerWidth'] < 1: + raise AttributeError('invalid integerWidth (%s) specified; must be > 0' % repr(init_args['integerWidth'])) + + fields = {} + + if init_args.has_key('fractionWidth'): + if type(init_args['fractionWidth']) != types.IntType: + raise AttributeError('invalid fractionWidth (%s) specified; expected integer' % repr(self._fractionWidth)) + elif init_args['fractionWidth'] < 0: + raise AttributeError('invalid fractionWidth (%s) specified; must be >= 0' % repr(init_args['fractionWidth'])) + self._fractionWidth = init_args['fractionWidth'] + + if self._fractionWidth: + fracmask = '.' + '#{%d}' % self._fractionWidth +## dbg('fracmask:', fracmask) + fields[1] = Field(defaultValue='0'*self._fractionWidth) + else: + fracmask = '' + + self._integerWidth = init_args['integerWidth'] + if init_args['groupDigits']: + self._groupSpace = (self._integerWidth - 1) / 3 + else: + self._groupSpace = 0 + intmask = '#{%d}' % (self._integerWidth + self._groupSpace) + if self._fractionWidth: + emptyInvalid = False + else: + emptyInvalid = True + fields[0] = Field(formatcodes='r<>', emptyInvalid=emptyInvalid) +## dbg('intmask:', intmask) + + # don't bother to reprocess these arguments: + del init_args['integerWidth'] + del init_args['fractionWidth'] + + self._autoSize = init_args['autoSize'] + if self._autoSize: + formatcodes = 'FR<' + else: + formatcodes = 'R<' + + + mask = intmask+fracmask + + # initial value of state vars + self._oldvalue = 0 + self._integerEnd = 0 + self._typedSign = False + + # Construct the base control: + BaseMaskedTextCtrl.__init__( + self, parent, id, '', + pos, size, style, validator, name, + mask = mask, + formatcodes = formatcodes, + fields = fields, + validFunc=self.IsInBounds, + setupEventHandling = False) + + self.Bind(wx.EVT_SET_FOCUS, self._OnFocus ) ## defeat automatic full selection + self.Bind(wx.EVT_KILL_FOCUS, self._OnKillFocus ) ## run internal validator + self.Bind(wx.EVT_LEFT_DCLICK, self._OnDoubleClick) ## select field under cursor on dclick + self.Bind(wx.EVT_RIGHT_UP, self._OnContextMenu ) ## bring up an appropriate context menu + self.Bind(wx.EVT_KEY_DOWN, self._OnKeyDown ) ## capture control events not normally seen, eg ctrl-tab. + self.Bind(wx.EVT_CHAR, self._OnChar ) ## handle each keypress + self.Bind(wx.EVT_TEXT, self.OnTextChange ) ## color control appropriately & keep + ## track of previous value for undo + + # Establish any additional parameters, with appropriate error checking + self.SetParameters(**init_args) + + # right alignment with prefixed blanks doesn't work with wxPython 2.9+ + if wx.Platform == "__WXMSW__": + self.SetWindowStyleFlag(self.GetWindowStyleFlag() | wx.TE_RIGHT) + + # Set the value requested (if possible) +## wxCallAfter(self.SetValue, value) + self.SetValue(value) + + # Ensure proper coloring: + self.Refresh() +## dbg('finished NumCtrl::__init__', indent=0) + + + def SetParameters(self, **kwargs): + """ + This function is used to initialize and reconfigure the control. + See TimeCtrl module overview for available parameters. + """ +## dbg('NumCtrl::SetParameters', indent=1) + maskededit_kwargs = {} + reset_fraction_width = False + + + if( (kwargs.has_key('integerWidth') and kwargs['integerWidth'] != self._integerWidth) + or (kwargs.has_key('fractionWidth') and kwargs['fractionWidth'] != self._fractionWidth) + or (kwargs.has_key('groupDigits') and kwargs['groupDigits'] != self._groupDigits) + or (kwargs.has_key('autoSize') and kwargs['autoSize'] != self._autoSize) ): + + fields = {} + + if kwargs.has_key('fractionWidth'): + if type(kwargs['fractionWidth']) != types.IntType: + raise AttributeError('invalid fractionWidth (%s) specified; expected integer' % repr(kwargs['fractionWidth'])) + elif kwargs['fractionWidth'] < 0: + raise AttributeError('invalid fractionWidth (%s) specified; must be >= 0' % repr(kwargs['fractionWidth'])) + else: + if self._fractionWidth != kwargs['fractionWidth']: + self._fractionWidth = kwargs['fractionWidth'] + + if self._fractionWidth: + fracmask = '.' + '#{%d}' % self._fractionWidth + fields[1] = Field(defaultValue='0'*self._fractionWidth) + emptyInvalid = False + else: + emptyInvalid = True + fracmask = '' +## dbg('fracmask:', fracmask) + + if kwargs.has_key('integerWidth'): + if type(kwargs['integerWidth']) != types.IntType: +## dbg(indent=0) + raise AttributeError('invalid integerWidth (%s) specified; expected integer' % repr(kwargs['integerWidth'])) + elif kwargs['integerWidth'] < 0: +## dbg(indent=0) + raise AttributeError('invalid integerWidth (%s) specified; must be > 0' % repr(kwargs['integerWidth'])) + else: + self._integerWidth = kwargs['integerWidth'] + + if kwargs.has_key('groupDigits'): + self._groupDigits = kwargs['groupDigits'] + + if self._groupDigits: + self._groupSpace = (self._integerWidth - 1) / 3 + else: + self._groupSpace = 0 + + intmask = '#{%d}' % (self._integerWidth + self._groupSpace) +## dbg('intmask:', intmask) + fields[0] = Field(formatcodes='r<>', emptyInvalid=emptyInvalid) + maskededit_kwargs['fields'] = fields + + # don't bother to reprocess these arguments: + if kwargs.has_key('integerWidth'): + del kwargs['integerWidth'] + if kwargs.has_key('fractionWidth'): + del kwargs['fractionWidth'] + + maskededit_kwargs['mask'] = intmask+fracmask + + if kwargs.has_key('groupChar') or kwargs.has_key('decimalChar'): + old_groupchar = self._groupChar # save so we can reformat properly + old_decimalchar = self._decimalChar +## dbg("old_groupchar: '%s'" % old_groupchar) +## dbg("old_decimalchar: '%s'" % old_decimalchar) + groupchar = old_groupchar + decimalchar = old_decimalchar + old_numvalue = self._GetNumValue(self._GetValue()) + + if kwargs.has_key('groupChar'): + maskededit_kwargs['groupChar'] = kwargs['groupChar'] + groupchar = kwargs['groupChar'] + if kwargs.has_key('decimalChar'): + maskededit_kwargs['decimalChar'] = kwargs['decimalChar'] + decimalchar = kwargs['decimalChar'] + + # Add sanity check to make sure these are distinct, and if not, + # raise attribute error + if groupchar == decimalchar: + raise AttributeError('groupChar and decimalChar must be distinct') + + + # for all other parameters, assign keyword args as appropriate: + for key, param_value in kwargs.items(): + key = key.replace('Color', 'Colour') + if key not in NumCtrl.valid_ctrl_params.keys(): + raise AttributeError('invalid keyword argument "%s"' % key) + elif key not in MaskedEditMixin.valid_ctrl_params.keys(): + setattr(self, '_' + key, param_value) + elif key in ('mask', 'autoformat'): # disallow explicit setting of mask + raise AttributeError('invalid keyword argument "%s"' % key) + else: + maskededit_kwargs[key] = param_value +## dbg('kwargs:', kwargs) + + # reprocess existing format codes to ensure proper resulting format: + formatcodes = self.GetCtrlParameter('formatcodes') + if kwargs.has_key('allowNegative'): + if kwargs['allowNegative'] and '-' not in formatcodes: + formatcodes += '-' + maskededit_kwargs['formatcodes'] = formatcodes + elif not kwargs['allowNegative'] and '-' in formatcodes: + formatcodes = formatcodes.replace('-','') + maskededit_kwargs['formatcodes'] = formatcodes + + if kwargs.has_key('groupDigits'): + if kwargs['groupDigits'] and ',' not in formatcodes: + formatcodes += ',' + maskededit_kwargs['formatcodes'] = formatcodes + elif not kwargs['groupDigits'] and ',' in formatcodes: + formatcodes = formatcodes.replace(',','') + maskededit_kwargs['formatcodes'] = formatcodes + + if kwargs.has_key('selectOnEntry'): + self._selectOnEntry = kwargs['selectOnEntry'] +## dbg("kwargs['selectOnEntry']?", kwargs['selectOnEntry'], "'S' in formatcodes?", 'S' in formatcodes) + if kwargs['selectOnEntry'] and 'S' not in formatcodes: + formatcodes += 'S' + maskededit_kwargs['formatcodes'] = formatcodes + elif not kwargs['selectOnEntry'] and 'S' in formatcodes: + formatcodes = formatcodes.replace('S','') + maskededit_kwargs['formatcodes'] = formatcodes + + if kwargs.has_key('autoSize'): + self._autoSize = kwargs['autoSize'] + if kwargs['autoSize'] and 'F' not in formatcodes: + formatcodes += 'F' + maskededit_kwargs['formatcodes'] = formatcodes + elif not kwargs['autoSize'] and 'F' in formatcodes: + formatcodes = formatcodes.replace('F', '') + maskededit_kwargs['formatcodes'] = formatcodes + + + if 'r' in formatcodes and self._fractionWidth: + # top-level mask should only be right insert if no fractional + # part will be shown; ie. if reconfiguring control, remove + # previous "global" setting. + formatcodes = formatcodes.replace('r', '') + maskededit_kwargs['formatcodes'] = formatcodes + + + if kwargs.has_key('limited'): + if kwargs['limited'] and not self._limited: + maskededit_kwargs['validRequired'] = True + elif not kwargs['limited'] and self._limited: + maskededit_kwargs['validRequired'] = False + self._limited = kwargs['limited'] + + if kwargs.has_key('limitOnFieldChange'): + if kwargs['limitOnFieldChange'] and not self._limitOnFieldChange: + maskededit_kwargs['stopFieldChangeIfInvalid'] = True + elif kwargs['limitOnFieldChange'] and self._limitOnFieldChange: + maskededit_kwargs['stopFieldChangeIfInvalid'] = False + +## dbg('maskededit_kwargs:', maskededit_kwargs) + if maskededit_kwargs.keys(): + self.SetCtrlParameters(**maskededit_kwargs) + + # Go ensure all the format codes necessary are present: + orig_intformat = intformat = self.GetFieldParameter(0, 'formatcodes') + if 'r' not in intformat: + intformat += 'r' + if '>' not in intformat: + intformat += '>' + if intformat != orig_intformat: + if self._fractionWidth: + self.SetFieldParameters(0, formatcodes=intformat) + else: + self.SetCtrlParameters(formatcodes=intformat) + + # Record end of integer and place cursor there unless selecting, or select entire field: + integerStart, integerEnd = self._fields[0]._extent + if not self._fields[0]._selectOnFieldEntry: + self.SetInsertionPoint(0) + self.SetInsertionPoint(integerEnd) + self.SetSelection(integerEnd, integerEnd) + else: + self.SetInsertionPoint(0) # include any sign + self.SetSelection(0, integerEnd) + + + # Set min and max as appropriate: + if kwargs.has_key('min'): + min = kwargs['min'] + if( self._max is None + or min is None + or (self._max is not None and self._max >= min) ): +## dbg('examining min') + if min is not None: + try: + textmin = self._toGUI(min, apply_limits = False) + except ValueError: +## dbg('min will not fit into control; ignoring', indent=0) + raise +## dbg('accepted min') + self._min = min + else: +## dbg('ignoring min') + pass + + + if kwargs.has_key('max'): + max = kwargs['max'] + if( self._min is None + or max is None + or (self._min is not None and self._min <= max) ): +## dbg('examining max') + if max is not None: + try: + textmax = self._toGUI(max, apply_limits = False) + except ValueError: +## dbg('max will not fit into control; ignoring', indent=0) + raise +## dbg('accepted max') + self._max = max + else: +## dbg('ignoring max') + pass + + if kwargs.has_key('allowNegative'): + self._allowNegative = kwargs['allowNegative'] + + # Ensure current value of control obeys any new restrictions imposed: + text = self._GetValue() +## dbg('text value: "%s"' % text) + if kwargs.has_key('groupChar') and self._groupChar != old_groupchar and text.find(old_groupchar) != -1: + text = old_numvalue +## dbg('old_groupchar: "%s" newgroupchar: "%s"' % (old_groupchar, self._groupChar)) + if kwargs.has_key('decimalChar') and self._decimalChar != old_decimalchar and text.find(old_decimalchar) != -1: + text = old_numvalue + + if text != self._GetValue(): + if self._decimalChar != '.': + # ensure latest decimal char is in "numeric value" so it won't be removed + # when going to the GUI: + text = text.replace('.', self._decimalChar) + newtext = self._toGUI(text) +## dbg('calling wx.TextCtrl.ChangeValue(self, %s)' % newtext) + wx.TextCtrl.ChangeValue(self, newtext) + + value = self.GetValue() + +## dbg('self._allowNegative?', self._allowNegative) + if not self._allowNegative and self._isNeg: + value = abs(value) +## dbg('abs(value):', value) + self._isNeg = False + + elif not self._allowNone and BaseMaskedTextCtrl.GetValue(self) == '': + if self._min > 0: + value = self._min + else: + value = 0 + + sel_start, sel_to = self.GetSelection() + if self.IsLimited() and self._min is not None and value < self._min: +## dbg('Set to min value:', self._min) + self._ChangeValue(self._toGUI(self._min)) + + elif self.IsLimited() and self._max is not None and value > self._max: +## dbg('Setting to max value:', self._max) + self._ChangeValue(self._toGUI(self._max)) + else: + # reformat current value as appropriate to possibly new conditions +## dbg('Reformatting value:', value) + sel_start, sel_to = self.GetSelection() + self._ChangeValue(self._toGUI(value)) + self.Refresh() # recolor as appropriate +## dbg('finished NumCtrl::SetParameters', indent=0) + + + + def _GetNumValue(self, value): + """ + This function attempts to "clean up" a text value, providing a regularized + convertable string, via atol() or atof(), for any well-formed numeric text value. + """ + return value.replace(self._groupChar, '').replace(self._decimalChar, '.').replace('(', '-').replace(')','').strip() + + + def GetFraction(self, candidate=None): + """ + Returns the fractional portion of the value as a float. If there is no + fractional portion, the value returned will be 0.0. + """ + if not self._fractionWidth: + return 0.0 + else: + fracstart, fracend = self._fields[1]._extent + if candidate is None: + value = self._toGUI(BaseMaskedTextCtrl.GetValue(self)) + else: + value = self._toGUI(candidate) + fracstring = value[fracstart:fracend].strip() + if not value: + return 0.0 + else: + return string.atof(fracstring) + + def _OnChangeSign(self, event): +## dbg('NumCtrl::_OnChangeSign', indent=1) + self._typedSign = True + MaskedEditMixin._OnChangeSign(self, event) +## dbg(indent=0) + + + def _disallowValue(self): +## dbg('NumCtrl::_disallowValue') + # limited and -1 is out of bounds + if self._typedSign: + self._isNeg = False + if not wx.Validator_IsSilent(): + wx.Bell() + sel_start, sel_to = self._GetSelection() +## dbg('queuing reselection of (%d, %d)' % (sel_start, sel_to)) + wx.CallAfter(self.SetInsertionPoint, sel_start) # preserve current selection/position + wx.CallAfter(self.SetSelection, sel_start, sel_to) + + + def _OnChangeField(self, event): + """ + This routine enhances the base masked control _OnFieldChange(). It's job + is to ensure limits are imposed if limitOnFieldChange is enabled. + """ +## dbg('NumCtrl::_OnFieldChange', indent=1) + if self._limitOnFieldChange and not (self._min <= self.GetValue() <= self._max): + self._disallowValue() +## dbg('oob - field change disallowed',indent=0) + return False + else: +## dbg(indent=0) + return MaskedEditMixin._OnChangeField(self, event) # call the baseclass function + + + def _LostFocus(self): + """ + On loss of focus, if limitOnFieldChange is set, ensure value conforms to limits. + """ +## dbg('NumCtrl::_LostFocus', indent=1) + if self._limitOnFieldChange: +## dbg("limiting on loss of focus") + value = self.GetValue() + if self._min is not None and value < self._min: +## dbg('Set to min value:', self._min) + self._SetValue(self._toGUI(self._min)) + + elif self._max is not None and value > self._max: +## dbg('Setting to max value:', self._max) + self._SetValue(self._toGUI(self._max)) + # (else do nothing.) + # (else do nothing.) +## dbg(indent=0) + return True + + + def _SetValue(self, value): + """ + This routine supersedes the base masked control _SetValue(). It is + needed to ensure that the value of the control is always representable/convertable + to a numeric return value (via GetValue().) This routine also handles + automatic adjustment and grouping of the value without explicit intervention + by the user. + """ + +## dbg('NumCtrl::_SetValue("%s")' % value, indent=1) + + if( (self._fractionWidth and value.find(self._decimalChar) == -1) or + (self._fractionWidth == 0 and value.find(self._decimalChar) != -1) ) : + value = self._toGUI(value) + + numvalue = self._GetNumValue(value) +## dbg('cleansed value: "%s"' % numvalue) + replacement = None + + if numvalue == "": + if self._allowNone: +## dbg('calling base BaseMaskedTextCtrl._SetValue(self, "%s")' % value) + BaseMaskedTextCtrl._SetValue(self, value) + self.Refresh() +## dbg(indent=0) + return + elif self._min > 0 and self.IsLimited(): + replacement = self._min + else: + replacement = 0 +## dbg('empty value; setting replacement:', replacement) + + if replacement is None: + # Go get the integer portion about to be set and verify its validity + intstart, intend = self._fields[0]._extent +## dbg('intstart, intend:', intstart, intend) +## dbg('raw integer:"%s"' % value[intstart:intend]) + int = self._GetNumValue(value[intstart:intend]) + numval = self._fromGUI(value) + +## dbg('integer: "%s"' % int) + try: + # if a float value, this will implicitly verify against limits, + # and generate an exception if out-of-bounds and limited + # if not a float, it will just return 0.0, and we therefore + # have to test against the limits explicitly after testing + # special cases for handling -0 and empty controls... + fracval = self.GetFraction(value) + except ValueError, e: +## dbg('Exception:', e, 'must be out of bounds; disallow value') + self._disallowValue() +## dbg(indent=0) + return + + if fracval == 0.0: # (can be 0 for floats as well as integers) + # we have to do special testing to account for emptying controls, or -0 + # and/or just leaving the sign character or changing the sign, + # so we can do appropriate things to the value of the control, + # we can't just immediately test to see if the value is valid + # If all of these special cases are not in play, THEN we can do + # a limits check and see if the value is otherwise ok... + +## dbg('self._isNeg?', self._isNeg) + if int == '-' and self._oldvalue < 0 and not self._typedSign: +## dbg('just a negative sign; old value < 0; setting replacement of 0') + replacement = 0 + self._isNeg = False + elif int[:2] == '-0': + if self._oldvalue < 0: +## dbg('-0; setting replacement of 0') + replacement = 0 + self._isNeg = False + elif not self._limited or (self._min < -1 and self._max >= -1): +## dbg('-0; setting replacement of -1') + replacement = -1 + self._isNeg = True + else: + # limited and -1 is out of bounds + self._disallowValue() +## dbg(indent=0) + return + + elif int == '-' and (self._oldvalue >= 0 or self._typedSign): + if not self._limited or (self._min < -1 and self._max >= -1): +## dbg('just a negative sign; setting replacement of -1') + replacement = -1 + else: + # limited and -1 is out of bounds + self._disallowValue() +## dbg(indent=0) + return + + elif( self._typedSign + and int.find('-') != -1 + and self._limited + and not self._min <= numval <= self._max): + # changed sign resulting in value that's now out-of-bounds; + # disallow + self._disallowValue() +## dbg(indent=0) + return + + if replacement is None: + if int and int != '-': + try: + string.atol(int) + except ValueError: + # integer requested is not legal. This can happen if the user + # is attempting to insert a digit in the middle of the control + # resulting in something like " 3 45". Disallow such actions: +## dbg('>>>>>>>>>>>>>>>> "%s" does not convert to a long!' % int) + if not wx.Validator_IsSilent(): + wx.Bell() + sel_start, sel_to = self._GetSelection() +## dbg('queuing reselection of (%d, %d)' % (sel_start, sel_to)) + wx.CallAfter(self.SetInsertionPoint, sel_start) # preserve current selection/position + wx.CallAfter(self.SetSelection, sel_start, sel_to) +## dbg(indent=0) + return + +## dbg('numvalue: "%s"' % numvalue.replace(' ', '')) + # finally, (potentially re) verify that numvalue will pass any limits imposed: + try: + if self._fractionWidth: + value = self._toGUI(string.atof(numvalue)) + else: + value = self._toGUI(string.atol(numvalue)) + except ValueError, e: +## dbg('Exception:', e, 'must be out of bounds; disallow value') + self._disallowValue() +## dbg(indent=0) + return + +## dbg('modified value: "%s"' % value) + + + self._typedSign = False # reset state var + + if replacement is not None: + # Value presented wasn't a legal number, but control should do something + # reasonable instead: +## dbg('setting replacement value:', replacement) + self._SetValue(self._toGUI(replacement)) + sel_start = BaseMaskedTextCtrl.GetValue(self).find(str(abs(replacement))) # find where it put the 1, so we can select it + sel_to = sel_start + len(str(abs(replacement))) +## dbg('queuing selection of (%d, %d)' %(sel_start, sel_to)) + wx.CallAfter(self.SetInsertionPoint, sel_start) + wx.CallAfter(self.SetSelection, sel_start, sel_to) +## dbg(indent=0) + return + + # Otherwise, apply appropriate formatting to value: + + # Because we're intercepting the value and adjusting it + # before a sign change is detected, we need to do this here: + if '-' in value or '(' in value: + self._isNeg = True + else: + self._isNeg = False + +## dbg('value:"%s"' % value, 'self._useParens:', self._useParens) + if self._fractionWidth: + adjvalue = self._adjustFloat(self._GetNumValue(value).replace('.',self._decimalChar)) + else: + adjvalue = self._adjustInt(self._GetNumValue(value)) +## dbg('adjusted value: "%s"' % adjvalue) + + + sel_start, sel_to = self._GetSelection() # record current insertion point +## dbg('calling BaseMaskedTextCtrl._SetValue(self, "%s")' % adjvalue) + BaseMaskedTextCtrl._SetValue(self, adjvalue) + # After all actions so far scheduled, check that resulting cursor + # position is appropriate, and move if not: + wx.CallAfter(self._CheckInsertionPoint) + +## dbg('finished NumCtrl::_SetValue', indent=0) + + def _CheckInsertionPoint(self): + # If current insertion point is before the end of the integer and + # its before the 1st digit, place it just after the sign position: +## dbg('NumCtrl::CheckInsertionPoint', indent=1) + sel_start, sel_to = self._GetSelection() + text = self._GetValue() + if sel_to < self._fields[0]._extent[1] and text[sel_to] in (' ', '-', '('): + text, signpos, right_signpos = self._getSignedValue() +## dbg('setting selection(%d, %d)' % (signpos+1, signpos+1)) + self.SetInsertionPoint(signpos+1) + self.SetSelection(signpos+1, signpos+1) +## dbg(indent=0) + + + def _OnErase( self, event=None, just_return_value=False ): + """ + This overrides the base control _OnErase, so that erasing around + grouping characters auto selects the digit before or after the + grouping character, so that the erasure does the right thing. + """ +## dbg('NumCtrl::_OnErase', indent=1) + if event is None: # called as action routine from Cut() operation. + key = wx.WXK_DELETE + else: + key = event.GetKeyCode() + #if grouping digits, make sure deletes next to group char always + # delete next digit to appropriate side: + if self._groupDigits: + value = BaseMaskedTextCtrl.GetValue(self) + sel_start, sel_to = self._GetSelection() + + if key == wx.WXK_BACK: + # if 1st selected char is group char, select to previous digit + if sel_start > 0 and sel_start < len(self._mask) and value[sel_start:sel_to] == self._groupChar: + self.SetInsertionPoint(sel_start-1) + self.SetSelection(sel_start-1, sel_to) + + # elif previous char is group char, select to previous digit + elif sel_start > 1 and sel_start == sel_to and value[sel_start-1:sel_start] == self._groupChar: + self.SetInsertionPoint(sel_start-2) + self.SetSelection(sel_start-2, sel_to) + + elif key == wx.WXK_DELETE: + if( sel_to < len(self._mask) - 2 + (1 *self._useParens) + and sel_start == sel_to + and value[sel_to] == self._groupChar ): + self.SetInsertionPoint(sel_start) + self.SetSelection(sel_start, sel_to+2) + + elif( sel_to < len(self._mask) - 2 + (1 *self._useParens) + and value[sel_start:sel_to] == self._groupChar ): + self.SetInsertionPoint(sel_start) + self.SetSelection(sel_start, sel_to+1) +## dbg(indent=0) + return BaseMaskedTextCtrl._OnErase(self, event, just_return_value) + + + def OnTextChange( self, event ): + """ + Handles an event indicating that the text control's value + has changed, and issue EVT_NUM event. + NOTE: using wxTextCtrl.SetValue() to change the control's + contents from within a EVT_CHAR handler can cause double + text events. So we check for actual changes to the text + before passing the events on. + """ +## dbg('NumCtrl::OnTextChange', indent=1) + if not BaseMaskedTextCtrl._OnTextChange(self, event): +## dbg(indent=0) + return + + # else... legal value + + value = self.GetValue() + if value != self._oldvalue: + try: + self.GetEventHandler().ProcessEvent( + NumberUpdatedEvent( self.GetId(), self.GetValue(), self ) ) + except ValueError: +## dbg(indent=0) + return + # let normal processing of the text continue + event.Skip() + self._oldvalue = value # record for next event +## dbg(indent=0) + + def _GetValue(self): + """ + Override of BaseMaskedTextCtrl to allow mixin to get the raw text value of the + control with this function. + """ + return wx.TextCtrl.GetValue(self) + + + def GetValue(self): + """ + Returns the current numeric value of the control. + """ + return self._fromGUI( BaseMaskedTextCtrl.GetValue(self) ) + + def SetValue(self, value): + """ + Sets the value of the control to the value specified. + The resulting actual value of the control may be altered to + conform with the bounds set on the control if limited, + or colored if not limited but the value is out-of-bounds. + A ValueError exception will be raised if an invalid value + is specified. + """ +## dbg('NumCtrl::SetValue(%s)' % value, indent=1) + BaseMaskedTextCtrl.SetValue( self, self._toGUI(value) ) +## dbg(indent=0) + + def ChangeValue(self, value): + """ + Sets the value of the control to the value specified. + The resulting actual value of the control may be altered to + conform with the bounds set on the control if limited, + or colored if not limited but the value is out-of-bounds. + A ValueError exception will be raised if an invalid value + is specified. + """ +## dbg('NumCtrl::ChangeValue(%s)' % value, indent=1) + BaseMaskedTextCtrl.ChangeValue( self, self._toGUI(value) ) +## dbg(indent=0) + + + + + def SetIntegerWidth(self, value): + self.SetParameters(integerWidth=value) + def GetIntegerWidth(self): + return self._integerWidth + + def SetFractionWidth(self, value): + self.SetParameters(fractionWidth=value) + def GetFractionWidth(self): + return self._fractionWidth + + + + def SetMin(self, min=None): + """ + Sets the minimum value of the control. If a value of None + is provided, then the control will have no explicit minimum value. + If the value specified is greater than the current maximum value, + then the function returns False and the minimum will not change from + its current setting. On success, the function returns True. + + If successful and the current value is lower than the new lower + bound, if the control is limited, the value will be automatically + adjusted to the new minimum value; if not limited, the value in the + control will be colored as invalid. + + If min > the max value allowed by the width of the control, + the function will return False, and the min will not be set. + """ +## dbg('NumCtrl::SetMin(%s)' % repr(min), indent=1) + if( self._max is None + or min is None + or (self._max is not None and self._max >= min) ): + try: + self.SetParameters(min=min) + bRet = True + except ValueError: + bRet = False + else: + bRet = False +## dbg(indent=0) + return bRet + + def GetMin(self): + """ + Gets the lower bound value of the control. It will return + None if not specified. + """ + return self._min + + + def SetMax(self, max=None): + """ + Sets the maximum value of the control. If a value of None + is provided, then the control will have no explicit maximum value. + If the value specified is less than the current minimum value, then + the function returns False and the maximum will not change from its + current setting. On success, the function returns True. + + If successful and the current value is greater than the new upper + bound, if the control is limited the value will be automatically + adjusted to this maximum value; if not limited, the value in the + control will be colored as invalid. + + If max > the max value allowed by the width of the control, + the function will return False, and the max will not be set. + """ + if( self._min is None + or max is None + or (self._min is not None and self._min <= max) ): + try: + self.SetParameters(max=max) + bRet = True + except ValueError: + bRet = False + else: + bRet = False + + return bRet + + + def GetMax(self): + """ + Gets the maximum value of the control. It will return the current + maximum integer, or None if not specified. + """ + return self._max + + + def SetBounds(self, min=None, max=None): + """ + This function is a convenience function for setting the min and max + values at the same time. The function only applies the maximum bound + if setting the minimum bound is successful, and returns True + only if both operations succeed. + NOTE: leaving out an argument will remove the corresponding bound. + """ + ret = self.SetMin(min) + return ret and self.SetMax(max) + + + def GetBounds(self): + """ + This function returns a two-tuple (min,max), indicating the + current bounds of the control. Each value can be None if + that bound is not set. + """ + return (self._min, self._max) + + + def SetLimited(self, limited): + """ + If called with a value of True, this function will cause the control + to limit the value to fall within the bounds currently specified. + If the control's value currently exceeds the bounds, it will then + be limited accordingly. + + If called with a value of False, this function will disable value + limiting, but coloring of out-of-bounds values will still take + place if bounds have been set for the control. + """ + self.SetParameters(limited = limited) + + + def IsLimited(self): + """ + Returns True if the control is currently limiting the + value to fall within the current bounds. + """ + return self._limited + + def GetLimited(self): + """ (For regularization of property accessors) """ + return self.IsLimited() + + def SetLimitOnFieldChange(self, limit): + """ + If called with a value of True, this function will cause the control + to prevent navigation out of the current field if its value is out-of-bounds, + and limit the value to fall within the bounds currently specified if the + control loses focus. + + If called with a value of False, this function will disable value + limiting, but coloring of out-of-bounds values will still take + place if bounds have been set for the control. + """ + self.SetParameters(limitOnFieldChange = limit) + + + def IsLimitedOnFieldChange(self): + """ + Returns True if the control is currently limiting the + value to fall within the current bounds. + """ + return self._limitOnFieldChange + + def GetLimitOnFieldChange(self): + """ (For regularization of property accessors) """ + return self.IsLimitedOnFieldChange() + + + def IsInBounds(self, value=None): + """ + Returns True if no value is specified and the current value + of the control falls within the current bounds. This function can + also be called with a value to see if that value would fall within + the current bounds of the given control. + """ +## dbg('IsInBounds(%s)' % repr(value), indent=1) + if value is None: + value = self.GetValue() + else: + try: + value = self._GetNumValue(self._toGUI(value)) + except ValueError, e: +## dbg('error getting NumValue(self._toGUI(value)):', e, indent=0) + return False + if value.strip() == '': + value = None + elif self._fractionWidth: + value = float(value) + else: + value = long(value) + + min = self.GetMin() + max = self.GetMax() + if min is None: min = value + if max is None: max = value + + # if bounds set, and value is None, return False + if value == None and (min is not None or max is not None): +## dbg('finished IsInBounds', indent=0) + return 0 + else: +## dbg('finished IsInBounds', indent=0) + return min <= value <= max + + + def SetAllowNone(self, allow_none): + """ + Change the behavior of the validation code, allowing control + to have a value of None or not, as appropriate. If the value + of the control is currently None, and allow_none is False, the + value of the control will be set to the minimum value of the + control, or 0 if no lower bound is set. + """ + self._allowNone = allow_none + if not allow_none and self.GetValue() is None: + min = self.GetMin() + if min is not None: self.SetValue(min) + else: self.SetValue(0) + + + def IsNoneAllowed(self): + return self._allowNone + def GetAllowNone(self): + """ (For regularization of property accessors) """ + return self.IsNoneAllowed() + + def SetAllowNegative(self, value): + self.SetParameters(allowNegative=value) + def IsNegativeAllowed(self): + return self._allowNegative + def GetAllowNegative(self): + """ (For regularization of property accessors) """ + return self.IsNegativeAllowed() + + def SetGroupDigits(self, value): + self.SetParameters(groupDigits=value) + def IsGroupingAllowed(self): + return self._groupDigits + def GetGroupDigits(self): + """ (For regularization of property accessors) """ + return self.IsGroupingAllowed() + + def SetGroupChar(self, value): + self.SetParameters(groupChar=value) + def GetGroupChar(self): + return self._groupChar + + def SetDecimalChar(self, value): + self.SetParameters(decimalChar=value) + def GetDecimalChar(self): + return self._decimalChar + + def SetSelectOnEntry(self, value): + self.SetParameters(selectOnEntry=value) + def GetSelectOnEntry(self): + return self._selectOnEntry + + def SetAutoSize(self, value): + self.SetParameters(autoSize=value) + def GetAutoSize(self): + return self._autoSize + + + # (Other parameter accessors are inherited from base class) + + + def _toGUI( self, value, apply_limits = True ): + """ + Conversion function used to set the value of the control; does + type and bounds checking and raises ValueError if argument is + not a valid value. + """ +## dbg('NumCtrl::_toGUI(%s)' % repr(value), indent=1) + if value is None and self.IsNoneAllowed(): +## dbg(indent=0) + return self._template + + elif type(value) in (types.StringType, types.UnicodeType): + value = self._GetNumValue(value) +## dbg('cleansed num value: "%s"' % value) + if value == "": + if self.IsNoneAllowed(): +## dbg(indent=0) + return self._template + else: +## dbg('exception raised:', e, indent=0) + raise ValueError ('NumCtrl requires numeric value, passed %s'% repr(value) ) + # else... + try: + if self._fractionWidth or value.find('.') != -1: + value = float(value) + else: + value = long(value) + except Exception, e: +## dbg('exception raised:', e, indent=0) + raise ValueError ('NumCtrl requires numeric value, passed %s'% repr(value) ) + + elif type(value) not in (types.IntType, types.LongType, types.FloatType): +## dbg(indent=0) + raise ValueError ( + 'NumCtrl requires numeric value, passed %s'% repr(value) ) + + if not self._allowNegative and value < 0: + raise ValueError ( + 'control configured to disallow negative values, passed %s'% repr(value) ) + + if self.IsLimited() and apply_limits: + min = self.GetMin() + max = self.GetMax() + if not min is None and value < min: +## dbg(indent=0) + raise ValueError ( + 'value %d is below minimum value of control'% value ) + if not max is None and value > max: +## dbg(indent=0) + raise ValueError ( + 'value %d exceeds value of control'% value ) + + adjustwidth = len(self._mask) - (1 * self._useParens * self._signOk) +## dbg('len(%s):' % self._mask, len(self._mask)) +## dbg('adjustwidth - groupSpace:', adjustwidth - self._groupSpace) +## dbg('adjustwidth:', adjustwidth) + if self._fractionWidth == 0: + s = str(long(value)).rjust(self._integerWidth) + else: + format = '%' + '%d.%df' % (self._integerWidth+self._fractionWidth+1, self._fractionWidth) + s = format % float(value) +## dbg('s:"%s"' % s, 'len(s):', len(s)) + if len(s) > (adjustwidth - self._groupSpace): +## dbg(indent=0) + raise ValueError ('value %s exceeds the integer width of the control (%d)' % (s, self._integerWidth)) + elif s[0] not in ('-', ' ') and self._allowNegative and len(s) == (adjustwidth - self._groupSpace): +## dbg(indent=0) + raise ValueError ('value %s exceeds the integer width of the control (%d)' % (s, self._integerWidth)) + + s = s.rjust(adjustwidth).replace('.', self._decimalChar) + if self._signOk and self._useParens: + if s.find('-') != -1: + s = s.replace('-', '(') + ')' + else: + s += ' ' +## dbg('returned: "%s"' % s, indent=0) + return s + + + def _fromGUI( self, value ): + """ + Conversion function used in getting the value of the control. + """ +## dbg(suspend=0) +## dbg('NumCtrl::_fromGUI(%s)' % value, indent=1) + # One or more of the underlying text control implementations + # issue an intermediate EVT_TEXT when replacing the control's + # value, where the intermediate value is an empty string. + # So, to ensure consistency and to prevent spurious ValueErrors, + # we make the following test, and react accordingly: + # + if value.strip() == '': + if not self.IsNoneAllowed(): +## dbg('empty value; not allowed,returning 0', indent = 0) + if self._fractionWidth: + return 0.0 + else: + return 0 + else: +## dbg('empty value; returning None', indent = 0) + return None + else: + value = self._GetNumValue(value) +## dbg('Num value: "%s"' % value) + if self._fractionWidth: + try: +## dbg(indent=0) + return float( value ) + except ValueError: +## dbg("couldn't convert to float; returning None") + return None + else: + raise + else: + try: +## dbg(indent=0) + return int( value ) + except ValueError: + try: +## dbg(indent=0) + return long( value ) + except ValueError: +## dbg("couldn't convert to long; returning None") + return None + + else: + raise + else: +## dbg('exception occurred; returning None') + return None + + + def _Paste( self, value=None, raise_on_invalid=False, just_return_value=False ): + """ + Preprocessor for base control paste; if value needs to be right-justified + to fit in control, do so prior to paste: + """ +## dbg('NumCtrl::_Paste (value = "%s")' % value, indent=1) + if value is None: + paste_text = self._getClipboardContents() + else: + paste_text = value + sel_start, sel_to = self._GetSelection() + orig_sel_start = sel_start + orig_sel_to = sel_to +## dbg('selection:', (sel_start, sel_to)) + old_value = self._GetValue() + + # + field = self._FindField(sel_start) + edit_start, edit_end = field._extent + + # handle possibility of groupChar being a space: + newtext = paste_text.lstrip() + lspace_count = len(paste_text) - len(newtext) + paste_text = ' ' * lspace_count + newtext.replace(self._groupChar, '').replace('(', '-').replace(')','') + + if field._insertRight and self._groupDigits: + # want to paste to the left; see if it will fit: + left_text = old_value[edit_start:sel_start].lstrip() +## dbg('len(left_text):', len(left_text)) +## dbg('len(paste_text):', len(paste_text)) +## dbg('sel_start - (len(left_text) + len(paste_text)) >= edit_start?', sel_start - (len(left_text) + len(paste_text)) >= edit_start) + if sel_start - (len(left_text) + len(paste_text)) >= edit_start: + # will fit! create effective paste text, and move cursor back to do so: + paste_text = left_text + paste_text + sel_start -= len(paste_text) + sel_start += sel_to - orig_sel_start # decrease by amount selected + else: +## dbg("won't fit left;", 'paste text remains: "%s"' % paste_text) +## dbg('adjusted start before accounting for grouping:', sel_start) +## dbg('adjusted paste_text before accounting for grouping: "%s"' % paste_text) + pass + if self._groupDigits and sel_start != orig_sel_start: + left_len = len(old_value[:sel_to].lstrip()) + # remove group chars from adjusted paste string, and left pad to wipe out + # old characters, so that selection will remove the right chars, and + # readjust will do the right thing: + paste_text = paste_text.replace(self._groupChar,'') + adjcount = left_len - len(paste_text) + paste_text = ' ' * adjcount + paste_text + sel_start = sel_to - len(paste_text) +## dbg('adjusted start after accounting for grouping:', sel_start) +## dbg('adjusted paste_text after accounting for grouping: "%s"' % paste_text) + self.SetInsertionPoint(sel_to) + self.SetSelection(sel_start, sel_to) + + new_text, replace_to = MaskedEditMixin._Paste(self, + paste_text, + raise_on_invalid=raise_on_invalid, + just_return_value=True) + self._SetInsertionPoint(orig_sel_to) + self._SetSelection(orig_sel_start, orig_sel_to) + if not just_return_value and new_text is not None: + if new_text != self._GetValue(): + self.modified = True + if new_text == '': + self.ClearValue() + else: + wx.CallAfter(self._SetValue, new_text) + wx.CallAfter(self._SetInsertionPoint, replace_to) +## dbg(indent=0) + else: +## dbg(indent=0) + return new_text, replace_to + + def _Undo(self, value=None, prev=None): + '''numctrl's undo is more complicated than the base control's, due to + grouping characters; we don't want to consider them when calculating + the undone portion.''' +## dbg('NumCtrl::_Undo', indent=1) + if value is None: value = self._GetValue() + if prev is None: prev = self._prevValue + if not self._groupDigits: + ignore, (new_sel_start, new_sel_to) = BaseMaskedTextCtrl._Undo(self, value, prev, just_return_results = True) + self._SetValue(prev) + self._SetInsertionPoint(new_sel_start) + self._SetSelection(new_sel_start, new_sel_to) + self._prevSelection = (new_sel_start, new_sel_to) +## dbg('resetting "prev selection" to', self._prevSelection) +## dbg(indent=0) + return + # else... + sel_start, sel_to = self._prevSelection + edit_start, edit_end = self._FindFieldExtent(0) + + adjvalue = self._GetNumValue(value).rjust(self._masklength) + adjprev = self._GetNumValue(prev ).rjust(self._masklength) + + # move selection to account for "ungrouped" value: + left_text = value[sel_start:].lstrip() + numleftgroups = len(left_text) - len(left_text.replace(self._groupChar, '')) + adjsel_start = sel_start + numleftgroups + right_text = value[sel_to:].lstrip() + numrightgroups = len(right_text) - len(right_text.replace(self._groupChar, '')) + adjsel_to = sel_to + numrightgroups +## dbg('adjusting "previous" selection from', (sel_start, sel_to), 'to:', (adjsel_start, adjsel_to)) + self._prevSelection = (adjsel_start, adjsel_to) + + # determine appropriate selection for ungrouped undo + ignore, (new_sel_start, new_sel_to) = BaseMaskedTextCtrl._Undo(self, adjvalue, adjprev, just_return_results = True) + + # adjust new selection based on grouping: + left_len = edit_end - new_sel_start + numleftgroups = left_len / 3 + new_sel_start -= numleftgroups + if numleftgroups and left_len % 3 == 0: + new_sel_start += 1 + + if new_sel_start < self._masklength and prev[new_sel_start] == self._groupChar: + new_sel_start += 1 + + right_len = edit_end - new_sel_to + numrightgroups = right_len / 3 + new_sel_to -= numrightgroups + + if new_sel_to and prev[new_sel_to-1] == self._groupChar: + new_sel_to -= 1 + + if new_sel_start > new_sel_to: + new_sel_to = new_sel_start + + # for numbers, we don't care about leading whitespace; adjust selection if + # it includes leading space. + prev_stripped = prev.lstrip() + prev_start = self._masklength - len(prev_stripped) + if new_sel_start < prev_start: + new_sel_start = prev_start + +## dbg('adjusted selection accounting for grouping:', (new_sel_start, new_sel_to)) + self._SetValue(prev) + self._SetInsertionPoint(new_sel_start) + self._SetSelection(new_sel_start, new_sel_to) + self._prevSelection = (new_sel_start, new_sel_to) +## dbg('resetting "prev selection" to', self._prevSelection) +## dbg(indent=0) + +#=========================================================================== + +if __name__ == '__main__': + + import traceback + + class myDialog(wx.Dialog): + def __init__(self, parent, id, title, + pos = wx.DefaultPosition, size = wx.DefaultSize, + style = wx.DEFAULT_DIALOG_STYLE ): + wx.Dialog.__init__(self, parent, id, title, pos, size, style) + + self.int_ctrl = NumCtrl(self, wx.NewId(), size=(55,20)) + self.OK = wx.Button( self, wx.ID_OK, "OK") + self.Cancel = wx.Button( self, wx.ID_CANCEL, "Cancel") + + vs = wx.BoxSizer( wx.VERTICAL ) + vs.Add( self.int_ctrl, 0, wx.ALIGN_CENTRE|wx.ALL, 5 ) + hs = wx.BoxSizer( wx.HORIZONTAL ) + hs.Add( self.OK, 0, wx.ALIGN_CENTRE|wx.ALL, 5 ) + hs.Add( self.Cancel, 0, wx.ALIGN_CENTRE|wx.ALL, 5 ) + vs.Add(hs, 0, wx.ALIGN_CENTRE|wx.ALL, 5 ) + + self.SetAutoLayout( True ) + self.SetSizer( vs ) + vs.Fit( self ) + vs.SetSizeHints( self ) + self.Bind(EVT_NUM, self.OnChange, self.int_ctrl) + + def OnChange(self, event): + print 'value now', event.GetValue() + + class TestApp(wx.App): + def OnInit(self): + try: + self.frame = wx.Frame(None, -1, "Test", (20,20), (120,100) ) + self.panel = wx.Panel(self.frame, -1) + button = wx.Button(self.panel, -1, "Push Me", (20, 20)) + self.Bind(wx.EVT_BUTTON, self.OnClick, button) + except: + traceback.print_exc() + return False + return True + + def OnClick(self, event): + dlg = myDialog(self.panel, -1, "test NumCtrl") + dlg.int_ctrl.SetValue(501) + dlg.int_ctrl.SetInsertionPoint(1) + dlg.int_ctrl.SetSelection(1,2) + rc = dlg.ShowModal() + print 'final value', dlg.int_ctrl.GetValue() + del dlg + self.frame.Destroy() + + def Show(self): + self.frame.Show(True) + + try: + app = TestApp(0) + app.Show() + app.MainLoop() + except: + traceback.print_exc() + +__i=0 +## To-Do's: +## =============================## +## 1. Add support for printf-style format specification. +## 2. Add option for repositioning on 'illegal' insertion point. +## +## Version 1.4 +## 1. In response to user request, added limitOnFieldChange feature, so that +## out-of-bounds values can be temporarily added to the control, but should +## navigation be attempted out of an invalid field, it will not navigate, +## and if focus is lost on a control so limited with an invalid value, it +## will change the value to the nearest bound. +## +## Version 1.3 +## 1. fixed to allow space for a group char. +## +## Version 1.2 +## 1. Allowed select/replace digits. +## 2. Fixed undo to ignore grouping chars. +## +## Version 1.1 +## 1. Fixed .SetIntegerWidth() and .SetFractionWidth() functions. +## 2. Added autoSize parameter, to allow manual sizing of the control. +## 3. Changed inheritance to use wxBaseMaskedTextCtrl, to remove exposure of +## nonsensical parameter methods from the control, so it will work +## properly with Boa. +## 4. Fixed allowNone bug found by user sameerc1@grandecom.net +## diff --git a/wx/lib/masked/textctrl.py b/wx/lib/masked/textctrl.py new file mode 100644 index 00000000..727f4484 --- /dev/null +++ b/wx/lib/masked/textctrl.py @@ -0,0 +1,419 @@ +#---------------------------------------------------------------------------- +# Name: masked.textctrl.py +# Authors: Jeff Childers, Will Sadkin +# Email: jchilders_98@yahoo.com, wsadkin@nameconnector.com +# Created: 02/11/2003 +# Copyright: (c) 2003 by Jeff Childers, Will Sadkin, 2003 +# Portions: (c) 2002 by Will Sadkin, 2002-2003 +# RCS-ID: $Id$ +# License: wxWidgets license +#---------------------------------------------------------------------------- +# +# This file contains the most typically used generic masked control, +# masked.TextCtrl. It also defines the BaseMaskedTextCtrl, which can +# be used to derive other "semantics-specific" classes, like masked.NumCtrl, +# masked.TimeCtrl, and masked.IpAddrCtrl. +# +#---------------------------------------------------------------------------- +""" +Provides a generic, fully configurable masked edit text control, as well as +a base class from which you can derive masked controls tailored to a specific +function. See maskededit module overview for how to configure the control. +""" + +import wx +from wx.lib.masked import * + +# jmg 12/9/03 - when we cut ties with Py 2.2 and earlier, this would +# be a good place to implement the 2.3 logger class +from wx.tools.dbg import Logger +##dbg = Logger() +##dbg(enable=1) + + +class BaseMaskedTextCtrl( wx.TextCtrl, MaskedEditMixin ): + """ + This is the primary derivation from MaskedEditMixin. It provides + a general masked text control that can be configured with different + masks. + + However, this is done with an extra level of inheritance, so that + "general" classes like masked.TextCtrl can have all possible attributes, + while derived classes, like masked.TimeCtrl and masked.NumCtrl + can prevent exposure of those optional attributes of their base + class that do not make sense for their derivation. Therefore, + we define:: + + BaseMaskedTextCtrl(TextCtrl, MaskedEditMixin) + + and:: + + masked.TextCtrl(BaseMaskedTextCtrl, MaskedEditAccessorsMixin). + + This allows us to then derive:: + + masked.NumCtrl( BaseMaskedTextCtrl ) + + and not have to expose all the same accessor functions for the + derived control when they don't all make sense for it. + + In practice, BaseMaskedTextCtrl should never be instantiated directly, + but should only be used in derived classes. + """ + + def __init__( self, parent, id=-1, value = '', + pos = wx.DefaultPosition, + size = wx.DefaultSize, + style = wx.TE_PROCESS_TAB, + validator=wx.DefaultValidator, ## placeholder provided for data-transfer logic + name = 'maskedTextCtrl', + setupEventHandling = True, ## setup event handling by default + **kwargs): + + if not hasattr(self, 'this'): + wx.TextCtrl.__init__(self, parent, id, value='', + pos=pos, size = size, + style=style, validator=validator, + name=name) + + self._PostInit(setupEventHandling = setupEventHandling, + name=name, value=value,**kwargs ) + + + def _PostInit(self,setupEventHandling=True, + name='maskedTextCtrl' , value='', **kwargs): + + self.controlInitialized = True + MaskedEditMixin.__init__( self, name, **kwargs ) + + self._SetInitialValue(value) + + if setupEventHandling: + ## Setup event handlers + self.Bind(wx.EVT_SET_FOCUS, self._OnFocus ) ## defeat automatic full selection + self.Bind(wx.EVT_KILL_FOCUS, self._OnKillFocus ) ## run internal validator + self.Bind(wx.EVT_LEFT_DCLICK, self._OnDoubleClick) ## select field under cursor on dclick + self.Bind(wx.EVT_RIGHT_UP, self._OnContextMenu ) ## bring up an appropriate context menu + self.Bind(wx.EVT_KEY_DOWN, self._OnKeyDown ) ## capture control events not normally seen, eg ctrl-tab. + self.Bind(wx.EVT_CHAR, self._OnChar ) ## handle each keypress + self.Bind(wx.EVT_TEXT, self._OnTextChange ) ## color control appropriately & keep + ## track of previous value for undo + + + def __repr__(self): + return "" % self.GetValue() + + + def _GetSelection(self): + """ + Allow mixin to get the text selection of this control. + REQUIRED by any class derived from MaskedEditMixin. + """ + return self.GetSelection() + + def _SetSelection(self, sel_start, sel_to): + """ + Allow mixin to set the text selection of this control. + REQUIRED by any class derived from MaskedEditMixin. + """ +#### dbg("MaskedTextCtrl::_SetSelection(%(sel_start)d, %(sel_to)d)" % locals()) + if self: + return self.SetSelection( sel_start, sel_to ) + +## def SetSelection(self, sel_start, sel_to): +## """ +## This is just for debugging... +## """ +## dbg("MaskedTextCtrl::SetSelection(%(sel_start)d, %(sel_to)d)" % locals()) +## wx.TextCtrl.SetSelection(self, sel_start, sel_to) + + + def _GetInsertionPoint(self): + return self.GetInsertionPoint() + + def _SetInsertionPoint(self, pos): +#### dbg("MaskedTextCtrl::_SetInsertionPoint(%(pos)d)" % locals()) + if self: + self.SetInsertionPoint(pos) + +## def SetInsertionPoint(self, pos): +## """ +## This is just for debugging... +## """ +## dbg("MaskedTextCtrl::SetInsertionPoint(%(pos)d)" % locals()) +## wx.TextCtrl.SetInsertionPoint(self, pos) + + + def IsEmpty(*args, **kw): + return MaskedEditMixin.IsEmpty(*args, **kw) + + def _GetValue(self): + """ + Allow mixin to get the raw value of the control with this function. + REQUIRED by any class derived from MaskedEditMixin. + """ + return self.GetValue() + + + def _SetValue(self, value): + """ + Allow mixin to set the raw value of the control with this function. + REQUIRED by any class derived from MaskedEditMixin. + """ +## dbg('MaskedTextCtrl::_SetValue("%(value)s", use_change_value=%(use_change_value)d)' % locals(), indent=1) + # Record current selection and insertion point, for undo + self._prevSelection = self._GetSelection() + self._prevInsertionPoint = self._GetInsertionPoint() + wx.TextCtrl.SetValue(self, value) +## dbg(indent=0) + + def _ChangeValue(self, value): + """ + Allow mixin to set the raw value of the control with this function without + generating an event as a result. (New for masked.TextCtrl as of 2.8.4) + """ +## dbg('MaskedTextCtrl::_ChangeValue("%(value)s", use_change_value=%(use_change_value)d)' % locals(), indent=1) + # Record current selection and insertion point, for undo + self._prevSelection = self._GetSelection() + self._prevInsertionPoint = self._GetInsertionPoint() + wx.TextCtrl.ChangeValue(self, value) +## dbg(indent=0) + + def SetValue(self, value): + """ + This function redefines the externally accessible .SetValue() to be + a smart "paste" of the text in question, so as not to corrupt the + masked control. NOTE: this must be done in the class derived + from the base wx control. + """ + self.ModifyValue(value, use_change_value=False) + + def ChangeValue(self, value): + """ + Provided to accomodate similar functionality added to base control in wxPython 2.7.1.1. + """ + self.ModifyValue(value, use_change_value=True) + + + def ModifyValue(self, value, use_change_value=False): + """ + This factored function of common code does the bulk of the work for SetValue + and ChangeValue. + """ +## dbg('MaskedTextCtrl::ModifyValue("%(value)s", use_change_value=%(use_change_value)d)' % locals(), indent=1) + + if not self._mask: + if use_change_value: + wx.TextCtrl.ChangeValue(self, value) # revert to base control behavior + else: + wx.TextCtrl.SetValue(self, value) # revert to base control behavior + return + + # empty previous contents, replacing entire value: + self._SetInsertionPoint(0) + self._SetSelection(0, self._masklength) + if self._signOk and self._useParens: + signpos = value.find('-') + if signpos != -1: + value = value[:signpos] + '(' + value[signpos+1:].strip() + ')' + elif value.find(')') == -1 and len(value) < self._masklength: + value += ' ' # add place holder for reserved space for right paren + + if( len(value) < self._masklength # value shorter than control + and (self._isFloat or self._isInt) # and it's a numeric control + and self._ctrl_constraints._alignRight ): # and it's a right-aligned control + +## dbg('len(value)', len(value), ' < self._masklength', self._masklength) + # try to intelligently "pad out" the value to the right size: + value = self._template[0:self._masklength - len(value)] + value + if self._isFloat and value.find('.') == -1: + value = value[1:] +## dbg('padded value = "%s"' % value) + + # make Set/ChangeValue behave the same as if you had typed the value in: + try: + value, replace_to = self._Paste(value, raise_on_invalid=True, just_return_value=True) + if self._isFloat: + self._isNeg = False # (clear current assumptions) + value = self._adjustFloat(value) + elif self._isInt: + self._isNeg = False # (clear current assumptions) + value = self._adjustInt(value) + elif self._isDate and not self.IsValid(value) and self._4digityear: + value = self._adjustDate(value, fixcentury=True) + except ValueError: + # If date, year might be 2 digits vs. 4; try adjusting it: + if self._isDate and self._4digityear: + dateparts = value.split(' ') + dateparts[0] = self._adjustDate(dateparts[0], fixcentury=True) + value = string.join(dateparts, ' ') +## dbg('adjusted value: "%s"' % value) + value, replace_to = self._Paste(value, raise_on_invalid=True, just_return_value=True) + else: +## dbg('exception thrown', indent=0) + raise + if use_change_value: + self._ChangeValue(value) + else: + self._SetValue(value) # note: to preserve similar capability, .SetValue() + # does not change IsModified() +#### dbg('queuing insertion after ._Set/ChangeValue', replace_to) + # set selection to last char replaced by paste + wx.CallAfter(self._SetInsertionPoint, replace_to) + wx.CallAfter(self._SetSelection, replace_to, replace_to) +## dbg(indent=0) + + + def SetFont(self, *args, **kwargs): + """ Set the font, then recalculate control size, if appropriate. """ + wx.TextCtrl.SetFont(self, *args, **kwargs) + if self._autofit: +## dbg('calculated size:', self._CalcSize()) + self.SetClientSize(self._CalcSize()) + width = self.GetSize().width + height = self.GetBestSize().height +## dbg('setting client size to:', (width, height)) + self.SetInitialSize((width, height)) + + + def Clear(self): + """ Blanks the current control value by replacing it with the default value.""" +## dbg("MaskedTextCtrl::Clear - value reset to default value (template)") + if self._mask: + self.ClearValue() + else: + wx.TextCtrl.Clear(self) # else revert to base control behavior + + + def _Refresh(self): + """ + Allow mixin to refresh the base control with this function. + REQUIRED by any class derived from MaskedEditMixin. + """ +## dbg('MaskedTextCtrl::_Refresh', indent=1) + wx.TextCtrl.Refresh(self) +## dbg(indent=0) + + + def Refresh(self): + """ + This function redefines the externally accessible .Refresh() to + validate the contents of the masked control as it refreshes. + NOTE: this must be done in the class derived from the base wx control. + """ +## dbg('MaskedTextCtrl::Refresh', indent=1) + self._CheckValid() + self._Refresh() +## dbg(indent=0) + + + def _IsEditable(self): + """ + Allow mixin to determine if the base control is editable with this function. + REQUIRED by any class derived from MaskedEditMixin. + """ + return wx.TextCtrl.IsEditable(self) + + + def Cut(self): + """ + This function redefines the externally accessible .Cut to be + a smart "erase" of the text in question, so as not to corrupt the + masked control. NOTE: this must be done in the class derived + from the base wx control. + """ + if self._mask: + self._Cut() # call the mixin's Cut method + else: + wx.TextCtrl.Cut(self) # else revert to base control behavior + + + def Paste(self): + """ + This function redefines the externally accessible .Paste to be + a smart "paste" of the text in question, so as not to corrupt the + masked control. NOTE: this must be done in the class derived + from the base wx control. + """ + if self._mask: + self._Paste() # call the mixin's Paste method + else: + wx.TextCtrl.Paste(self, value) # else revert to base control behavior + + + def Undo(self): + """ + This function defines the undo operation for the control. (The default + undo is 1-deep.) + """ + if self._mask: + self._Undo() + else: + wx.TextCtrl.Undo(self) # else revert to base control behavior + + + def IsModified(self): + """ + This function overrides the raw wx.TextCtrl method, because the + masked edit mixin uses SetValue to change the value, which doesn't + modify the state of this attribute. So, the derived control keeps track + on each keystroke to see if the value changes, and if so, it's been + modified. + """ + return wx.TextCtrl.IsModified(self) or self.modified + + + def _CalcSize(self, size=None): + """ + Calculate automatic size if allowed; use base mixin function. + """ + return self._calcSize(size) + + +class TextCtrl( BaseMaskedTextCtrl, MaskedEditAccessorsMixin ): + """ + The "user-visible" masked text control; it is identical to the + BaseMaskedTextCtrl class it's derived from. + (This extra level of inheritance allows us to add the generic + set of masked edit parameters only to this class while allowing + other classes to derive from the "base" masked text control, + and provide a smaller set of valid accessor functions.) + See BaseMaskedTextCtrl for available methods. + """ + pass + + +class PreMaskedTextCtrl( BaseMaskedTextCtrl, MaskedEditAccessorsMixin ): + """ + This class exists to support the use of XRC subclassing. + """ + # This should really be wx.EVT_WINDOW_CREATE but it is not + # currently delivered for native controls on all platforms, so + # we'll use EVT_SIZE instead. It should happen shortly after the + # control is created as the control is set to its "best" size. + _firstEventType = wx.EVT_SIZE + + def __init__(self): + pre = wx.PreTextCtrl() + self.PostCreate(pre) + self.Bind(self._firstEventType, self.OnCreate) + + + def OnCreate(self, evt): + self.Unbind(self._firstEventType) + self._PostInit() + +__i=0 +## CHANGELOG: +## ==================== +## Version 1.3 +## - Added support for ChangeValue() function, similar to that of the base +## control, added in wxPython 2.7.1.1. +## +## Version 1.2 +## - Converted docstrings to reST format, added doc for ePyDoc. +## removed debugging override functions. +## +## Version 1.1 +## 1. Added .SetFont() method that properly resizes control +## 2. Modified control to support construction via XRC mechanism. diff --git a/wx/lib/masked/timectrl.py b/wx/lib/masked/timectrl.py new file mode 100644 index 00000000..0a350186 --- /dev/null +++ b/wx/lib/masked/timectrl.py @@ -0,0 +1,1405 @@ +#---------------------------------------------------------------------------- +# Name: timectrl.py +# Author: Will Sadkin +# Created: 09/19/2002 +# Copyright: (c) 2002 by Will Sadkin, 2002 +# RCS-ID: $Id$ +# License: wxWindows license +#---------------------------------------------------------------------------- +# NOTE: +# This was written way it is because of the lack of masked edit controls +# in wxWindows/wxPython. I would also have preferred to derive this +# control from a wxSpinCtrl rather than wxTextCtrl, but the wxTextCtrl +# component of that control is inaccessible through the interface exposed in +# wxPython. +# +# TimeCtrl does not use validators, because it does careful manipulation +# of the cursor in the text window on each keystroke, and validation is +# cursor-position specific, so the control intercepts the key codes before the +# validator would fire. +# +# TimeCtrl now also supports .SetValue() with either strings or wxDateTime +# values, as well as range limits, with the option of either enforcing them +# or simply coloring the text of the control if the limits are exceeded. +# +# Note: this class now makes heavy use of wxDateTime for parsing and +# regularization, but it always does so with ephemeral instances of +# wxDateTime, as the C++/Python validity of these instances seems to not +# persist. Because "today" can be a day for which an hour can "not exist" +# or be counted twice (1 day each per year, for DST adjustments), the date +# portion of all wxDateTimes used/returned have their date portion set to +# Jan 1, 1970 (the "epoch.") +#---------------------------------------------------------------------------- +# 12/13/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o Updated for V2.5 compatability +# o wx.SpinCtl has some issues that cause the control to +# lock up. Noted in other places using it too, it's not this module +# that's at fault. +# +# 12/20/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o wxMaskedTextCtrl -> masked.TextCtrl +# o wxTimeCtrl -> masked.TimeCtrl +# + +""" +*TimeCtrl* provides a multi-cell control that allows manipulation of a time +value. It supports 12 or 24 hour format, and you can use wxDateTime or mxDateTime +to get/set values from the control. + +Left/right/tab keys to switch cells within a TimeCtrl, and the up/down arrows act +like a spin control. TimeCtrl also allows for an actual spin button to be attached +to the control, so that it acts like the up/down arrow keys. + +The ! or c key sets the value of the control to the current time. + + Here's the API for TimeCtrl:: + + from wx.lib.masked import TimeCtrl + + TimeCtrl( + parent, id = -1, + value = '00:00:00', + pos = wx.DefaultPosition, + size = wx.DefaultSize, + style = wxTE_PROCESS_TAB, + validator = wx.DefaultValidator, + name = "time", + format = 'HHMMSS', + fmt24hr = False, + displaySeconds = True, + spinButton = None, + min = None, + max = None, + limited = None, + oob_color = "Yellow" + ) + + + value + If no initial value is set, the default will be midnight; if an illegal string + is specified, a ValueError will result. (You can always later set the initial time + with SetValue() after instantiation of the control.) + + size + The size of the control will be automatically adjusted for 12/24 hour format + if wx.DefaultSize is specified. NOTE: due to a problem with wx.DateTime, if the + locale does not use 'AM/PM' for its values, the default format will automatically + change to 24 hour format, and an AttributeError will be thrown if a non-24 format + is specified. + + style + By default, TimeCtrl will process TAB events, by allowing tab to the + different cells within the control. + + validator + By default, TimeCtrl just uses the default (empty) validator, as all + of its validation for entry control is handled internally. However, a validator + can be supplied to provide data transfer capability to the control. + + format + This parameter can be used instead of the fmt24hr and displaySeconds + parameters, respectively; it provides a shorthand way to specify the time + format you want. Accepted values are 'HHMMSS', 'HHMM', '24HHMMSS', and + '24HHMM'. If the format is specified, the other two arguments will be ignored. + + fmt24hr + If True, control will display time in 24 hour time format; if False, it will + use 12 hour AM/PM format. SetValue() will adjust values accordingly for the + control, based on the format specified. (This value is ignored if the *format* + parameter is specified.) + + displaySeconds + If True, control will include a seconds field; if False, it will + just show hours and minutes. (This value is ignored if the *format* + parameter is specified.) + + spinButton + If specified, this button's events will be bound to the behavior of the + TimeCtrl, working like up/down cursor key events. (See BindSpinButton.) + + min + Defines the lower bound for "valid" selections in the control. + By default, TimeCtrl doesn't have bounds. You must set both upper and lower + bounds to make the control pay attention to them, (as only one bound makes no sense + with times.) "Valid" times will fall between the min and max "pie wedge" of the + clock. + max + Defines the upper bound for "valid" selections in the control. + "Valid" times will fall between the min and max "pie wedge" of the + clock. (This can be a "big piece", ie. min = 11pm, max= 10pm + means *all but the hour from 10:00pm to 11pm are valid times.*) + limited + If True, the control will not permit entry of values that fall outside the + set bounds. + + oob_color + Sets the background color used to indicate out-of-bounds values for the control + when the control is not limited. This is set to "Yellow" by default. + +-------------------- + +EVT_TIMEUPDATE(win, id, func) + func is fired whenever the value of the control changes. + + +SetValue(time_string | wxDateTime | wxTimeSpan | mx.DateTime | mx.DateTimeDelta) + Sets the value of the control to a particular time, given a valid + value; raises ValueError on invalid value. + +*NOTE:* This will only allow mx.DateTime or mx.DateTimeDelta if mx.DateTime + was successfully imported by the class module. + +GetValue(as_wxDateTime = False, as_mxDateTime = False, as_wxTimeSpan=False, as mxDateTimeDelta=False) + Retrieves the value of the time from the control. By default this is + returned as a string, unless one of the other arguments is set; args are + searched in the order listed; only one value will be returned. + +GetWxDateTime(value=None) + When called without arguments, retrieves the value of the control, and applies + it to the wxDateTimeFromHMS() constructor, and returns the resulting value. + The date portion will always be set to Jan 1, 1970. This form is the same + as GetValue(as_wxDateTime=True). GetWxDateTime can also be called with any of the + other valid time formats settable with SetValue, to regularize it to a single + wxDateTime form. The function will raise ValueError on an unconvertable argument. + +GetMxDateTime() + Retrieves the value of the control and applies it to the DateTime.Time() + constructor,and returns the resulting value. (The date portion will always be + set to Jan 1, 1970.) (Same as GetValue(as_wxDateTime=True); provided for backward + compatibility with previous release.) + + +BindSpinButton(SpinBtton) + Binds an externally created spin button to the control, so that up/down spin + events change the active cell or selection in the control (in addition to the + up/down cursor keys.) (This is primarily to allow you to create a "standard" + interface to time controls, as seen in Windows.) + + +SetMin(min=None) + Sets the expected minimum value, or lower bound, of the control. + (The lower bound will only be enforced if the control is + configured to limit its values to the set bounds.) + If a value of *None* is provided, then the control will have + explicit lower bound. If the value specified is greater than + the current lower bound, then the function returns False and the + lower bound will not change from its current setting. On success, + the function returns True. Even if set, if there is no corresponding + upper bound, the control will behave as if it is unbounded. + + If successful and the current value is outside the + new bounds, if the control is limited the value will be + automatically adjusted to the nearest bound; if not limited, + the background of the control will be colored with the current + out-of-bounds color. + +GetMin(as_string=False) + Gets the current lower bound value for the control, returning + None, if not set, or a wxDateTime, unless the as_string parameter + is set to True, at which point it will return the string + representation of the lower bound. + + +SetMax(max=None) + Sets the expected maximum value, or upper bound, of the control. + (The upper bound will only be enforced if the control is + configured to limit its values to the set bounds.) + If a value of *None* is provided, then the control will + have no explicit upper bound. If the value specified is less + than the current lower bound, then the function returns False and + the maximum will not change from its current setting. On success, + the function returns True. Even if set, if there is no corresponding + lower bound, the control will behave as if it is unbounded. + + If successful and the current value is outside the + new bounds, if the control is limited the value will be + automatically adjusted to the nearest bound; if not limited, + the background of the control will be colored with the current + out-of-bounds color. + +GetMax(as_string = False) + Gets the current upper bound value for the control, returning + None, if not set, or a wxDateTime, unless the as_string parameter + is set to True, at which point it will return the string + representation of the lower bound. + + + +SetBounds(min=None,max=None) + This function is a convenience function for setting the min and max + values at the same time. The function only applies the maximum bound + if setting the minimum bound is successful, and returns True + only if both operations succeed. *Note: leaving out an argument + will remove the corresponding bound, and result in the behavior of + an unbounded control.* + +GetBounds(as_string = False) + This function returns a two-tuple (min,max), indicating the + current bounds of the control. Each value can be None if + that bound is not set. The values will otherwise be wxDateTimes + unless the as_string argument is set to True, at which point they + will be returned as string representations of the bounds. + + +IsInBounds(value=None) + Returns *True* if no value is specified and the current value + of the control falls within the current bounds. This function can also + be called with a value to see if that value would fall within the current + bounds of the given control. It will raise ValueError if the value + specified is not a wxDateTime, mxDateTime (if available) or parsable string. + + +IsValid(value) + Returns *True* if specified value is a legal time value and + falls within the current bounds of the given control. + + +SetLimited(bool) + If called with a value of True, this function will cause the control + to limit the value to fall within the bounds currently specified. + (Provided both bounds have been set.) + If the control's value currently exceeds the bounds, it will then + be set to the nearest bound. + If called with a value of False, this function will disable value + limiting, but coloring of out-of-bounds values will still take + place if bounds have been set for the control. +IsLimited() + Returns *True* if the control is currently limiting the + value to fall within the current bounds. + + +""" + +import copy +import string +import types + +import wx + +from wx.tools.dbg import Logger +from wx.lib.masked import Field, BaseMaskedTextCtrl + +dbg = Logger() +##dbg(enable=0) + +try: + from mx import DateTime + accept_mx = True +except ImportError: + accept_mx = False + +# This class of event fires whenever the value of the time changes in the control: +wxEVT_TIMEVAL_UPDATED = wx.NewEventType() +EVT_TIMEUPDATE = wx.PyEventBinder(wxEVT_TIMEVAL_UPDATED, 1) + +class TimeUpdatedEvent(wx.PyCommandEvent): + """ + Used to fire an EVT_TIMEUPDATE event whenever the value in a TimeCtrl changes. + """ + def __init__(self, id, value ='12:00:00 AM'): + wx.PyCommandEvent.__init__(self, wxEVT_TIMEVAL_UPDATED, id) + self.value = value + def GetValue(self): + """Retrieve the value of the time control at the time this event was generated""" + return self.value + +class TimeCtrlAccessorsMixin: + """ + Defines TimeCtrl's list of attributes having their own Get/Set functions, + ignoring those in the base class that make no sense for a time control. + """ + exposed_basectrl_params = ( + 'defaultValue', + 'description', + + 'useFixedWidthFont', + 'emptyBackgroundColour', + 'validBackgroundColour', + 'invalidBackgroundColour', + + 'validFunc', + 'validRequired', + ) + for param in exposed_basectrl_params: + propname = param[0].upper() + param[1:] + exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) + exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) + + if param.find('Colour') != -1: + # add non-british spellings, for backward-compatibility + propname.replace('Colour', 'Color') + + exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) + exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) + + +class TimeCtrl(BaseMaskedTextCtrl): + """ + Masked control providing several time formats and manipulation of time values. + """ + + valid_ctrl_params = { + 'format' : 'HHMMSS', # default format code + 'displaySeconds' : True, # by default, shows seconds + 'min': None, # by default, no bounds set + 'max': None, + 'limited': False, # by default, no limiting even if bounds set + 'useFixedWidthFont': True, # by default, use a fixed-width font + 'oob_color': "Yellow" # by default, the default masked.TextCtrl "invalid" color + } + + def __init__ ( + self, parent, id=-1, value = '00:00:00', + pos = wx.DefaultPosition, size = wx.DefaultSize, + fmt24hr=False, + spinButton = None, + style = wx.TE_PROCESS_TAB, + validator = wx.DefaultValidator, + name = "time", + **kwargs ): + + # set defaults for control: +## dbg('setting defaults:') + + self.__fmt24hr = False + wxdt = wx.DateTimeFromDMY(1, 0, 1970) + try: + if wxdt.Format('%p') != 'AM': + TimeCtrl.valid_ctrl_params['format'] = '24HHMMSS' + self.__fmt24hr = True + fmt24hr = True # force/change default positional argument + # (will countermand explicit set to False too.) + except: + TimeCtrl.valid_ctrl_params['format'] = '24HHMMSS' + self.__fmt24hr = True + fmt24hr = True # force/change default positional argument + # (will countermand explicit set to False too.) + + for key, param_value in TimeCtrl.valid_ctrl_params.items(): + # This is done this way to make setattr behave consistently with + # "private attribute" name mangling + setattr(self, "_TimeCtrl__" + key, copy.copy(param_value)) + + # create locals from current defaults, so we can override if + # specified in kwargs, and handle uniformly: + min = self.__min + max = self.__max + limited = self.__limited + self.__posCurrent = 0 + # handle deprecated keword argument name: + if kwargs.has_key('display_seconds'): + kwargs['displaySeconds'] = kwargs['display_seconds'] + del kwargs['display_seconds'] + if not kwargs.has_key('displaySeconds'): + kwargs['displaySeconds'] = True + + # (handle positional arg (from original release) differently from rest of kwargs:) + if not kwargs.has_key('format'): + if fmt24hr: + if kwargs.has_key('displaySeconds') and kwargs['displaySeconds']: + kwargs['format'] = '24HHMMSS' + del kwargs['displaySeconds'] + else: + kwargs['format'] = '24HHMM' + else: + if kwargs.has_key('displaySeconds') and kwargs['displaySeconds']: + kwargs['format'] = 'HHMMSS' + del kwargs['displaySeconds'] + else: + kwargs['format'] = 'HHMM' + + if not kwargs.has_key('useFixedWidthFont'): + # allow control over font selection: + kwargs['useFixedWidthFont'] = self.__useFixedWidthFont + + maskededit_kwargs = self.SetParameters(**kwargs) + + # allow for explicit size specification: + if size != wx.DefaultSize: + # override (and remove) "autofit" autoformat code in standard time formats: + maskededit_kwargs['formatcodes'] = 'T!' + + # This allows range validation if set + maskededit_kwargs['validFunc'] = self.IsInBounds + + # This allows range limits to affect insertion into control or not + # dynamically without affecting individual field constraint validation + maskededit_kwargs['retainFieldValidation'] = True + + # Now we can initialize the base control: + BaseMaskedTextCtrl.__init__( + self, parent, id=id, + pos=pos, size=size, + style = style, + validator = validator, + name = name, + setupEventHandling = False, + **maskededit_kwargs) + + + # This makes ':' act like tab (after we fix each ':' key event to remove "shift") + self._SetKeyHandler(':', self._OnChangeField) + + + # This makes the up/down keys act like spin button controls: + self._SetKeycodeHandler(wx.WXK_UP, self.__OnSpinUp) + self._SetKeycodeHandler(wx.WXK_DOWN, self.__OnSpinDown) + + + # This allows ! and c/C to set the control to the current time: + self._SetKeyHandler('!', self.__OnSetToNow) + self._SetKeyHandler('c', self.__OnSetToNow) + self._SetKeyHandler('C', self.__OnSetToNow) + + + # Set up event handling ourselves, so we can insert special + # processing on the ":' key to remove the "shift" attribute + # *before* the default handlers have been installed, so + # that : takes you forward, not back, and so we can issue + # EVT_TIMEUPDATE events on changes: + + self.Bind(wx.EVT_SET_FOCUS, self._OnFocus ) ## defeat automatic full selection + self.Bind(wx.EVT_KILL_FOCUS, self._OnKillFocus ) ## run internal validator + self.Bind(wx.EVT_LEFT_UP, self.__LimitSelection) ## limit selections to single field + self.Bind(wx.EVT_LEFT_DCLICK, self._OnDoubleClick ) ## select field under cursor on dclick + self.Bind(wx.EVT_KEY_DOWN, self._OnKeyDown ) ## capture control events not normally seen, eg ctrl-tab. + self.Bind(wx.EVT_CHAR, self.__OnChar ) ## remove "shift" attribute from colon key event, + ## then call BaseMaskedTextCtrl._OnChar with + ## the possibly modified event. + self.Bind(wx.EVT_TEXT, self.__OnTextChange, self ) ## color control appropriately and EVT_TIMEUPDATE events + + + # Validate initial value and set if appropriate + try: + self.SetBounds(min, max) + self.SetLimited(limited) + self.SetValue(value) + except: + self.SetValue('00:00:00') + + if spinButton: + self.BindSpinButton(spinButton) # bind spin button up/down events to this control + + + def SetParameters(self, **kwargs): + """ + Function providing access to the parameters governing TimeCtrl display and bounds. + """ +## dbg('TimeCtrl::SetParameters(%s)' % repr(kwargs), indent=1) + maskededit_kwargs = {} + reset_format = False + + if kwargs.has_key('display_seconds'): + kwargs['displaySeconds'] = kwargs['display_seconds'] + del kwargs['display_seconds'] + if kwargs.has_key('format') and kwargs.has_key('displaySeconds'): + del kwargs['displaySeconds'] # always apply format if specified + + # assign keyword args as appropriate: + for key, param_value in kwargs.items(): + if key not in TimeCtrl.valid_ctrl_params.keys(): + raise AttributeError('invalid keyword argument "%s"' % key) + + if key == 'format': + wxdt = wx.DateTimeFromDMY(1, 0, 1970) + try: + if wxdt.Format('%p') != 'AM': + require24hr = True + else: + require24hr = False + except: + require24hr = True + + # handle both local or generic 'maskededit' autoformat codes: + if param_value == 'HHMMSS' or param_value == 'TIMEHHMMSS': + self.__displaySeconds = True + self.__fmt24hr = False + elif param_value == 'HHMM' or param_value == 'TIMEHHMM': + self.__displaySeconds = False + self.__fmt24hr = False + elif param_value == '24HHMMSS' or param_value == '24HRTIMEHHMMSS': + self.__displaySeconds = True + self.__fmt24hr = True + elif param_value == '24HHMM' or param_value == '24HRTIMEHHMM': + self.__displaySeconds = False + self.__fmt24hr = True + else: + raise AttributeError('"%s" is not a valid format' % param_value) + + if require24hr and not self.__fmt24hr: + raise AttributeError('"%s" is an unsupported time format for the current locale' % param_value) + + reset_format = True + + elif key in ("displaySeconds", "display_seconds") and not kwargs.has_key('format'): + self.__displaySeconds = param_value + reset_format = True + + elif key == "min": min = param_value + elif key == "max": max = param_value + elif key == "limited": limited = param_value + + elif key == "useFixedWidthFont": + maskededit_kwargs[key] = param_value + + elif key == "oob_color": + maskededit_kwargs['invalidBackgroundColor'] = param_value + + if reset_format: + if self.__fmt24hr: + if self.__displaySeconds: maskededit_kwargs['autoformat'] = '24HRTIMEHHMMSS' + else: maskededit_kwargs['autoformat'] = '24HRTIMEHHMM' + + # Set hour field to zero-pad, right-insert, require explicit field change, + # select entire field on entry, and require a resultant valid entry + # to allow character entry: + hourfield = Field(formatcodes='0r" % self.GetValue() + + + def SetValue(self, value): + """ + Validating SetValue function for time values: + This function will do dynamic type checking on the value argument, + and convert wxDateTime, mxDateTime, or 12/24 format time string + into the appropriate format string for the control. + """ +## dbg('TimeCtrl::SetValue(%s)' % repr(value), indent=1) + try: + strtime = self._toGUI(self.__validateValue(value)) + except: +## dbg('validation failed', indent=0) + raise + +## dbg('strtime:', strtime) + self._SetValue(strtime) +## dbg(indent=0) + + def ChangeValue(self, value): + """ + Validating ChangeValue function for time values: + This function will do dynamic type checking on the value argument, + and convert wxDateTime, mxDateTime, or 12/24 format time string + into the appropriate format string for the control. + """ +## dbg('TimeCtrl::ChangeValue(%s)' % repr(value), indent=1) + try: + strtime = self._toGUI(self.__validateValue(value)) + except: +## dbg('validation failed', indent=0) + raise + +## dbg('strtime:', strtime) + self._ChangeValue(strtime) +## dbg(indent=0) + + def GetValue(self, + as_wxDateTime = False, + as_mxDateTime = False, + as_wxTimeSpan = False, + as_mxDateTimeDelta = False): + """ + This function returns the value of the display as a string by default, but + supports return as a wx.DateTime, mx.DateTime, wx.TimeSpan, or mx.DateTimeDelta, + if requested. (Evaluated in the order above-- first one wins!) + """ + + + if as_wxDateTime or as_mxDateTime or as_wxTimeSpan or as_mxDateTimeDelta: + value = self.GetWxDateTime() + if as_wxDateTime: + pass + elif as_mxDateTime: + value = DateTime.DateTime(1970, 1, 1, value.GetHour(), value.GetMinute(), value.GetSecond()) + elif as_wxTimeSpan: + value = wx.TimeSpan(value.GetHour(), value.GetMinute(), value.GetSecond()) + elif as_mxDateTimeDelta: + value = DateTime.DateTimeDelta(0, value.GetHour(), value.GetMinute(), value.GetSecond()) + else: + value = BaseMaskedTextCtrl.GetValue(self) + return value + + + def SetWxDateTime(self, wxdt): + """ + Because SetValue can take a wx.DateTime, this is now just an alias. + """ + self.SetValue(wxdt) + + + def GetWxDateTime(self, value=None): + """ + This function is the conversion engine for TimeCtrl; it takes + one of the following types: + + * time string + * wx.DateTime + * wx.TimeSpan + * mxDateTime + * mxDateTimeDelta + + and converts it to a wx.DateTime that always has Jan 1, 1970 as its date + portion, so that range comparisons around values can work using + wx.DateTime's built-in comparison function. If a value is not + provided to convert, the string value of the control will be used. + If the value is not one of the accepted types, a ValueError will be + raised. + """ + global accept_mx +## dbg(suspend=1) +## dbg('TimeCtrl::GetWxDateTime(%s)' % repr(value), indent=1) + if value is None: +## dbg('getting control value') + value = self.GetValue() +## dbg('value = "%s"' % value) + + if type(value) == types.UnicodeType: + value = str(value) # convert to regular string + + valid = True # assume true + if type(value) == types.StringType: + + # Construct constant wxDateTime, then try to parse the string: + wxdt = wx.DateTimeFromDMY(1, 0, 1970) +## dbg('attempting conversion') + value = value.strip() # (parser doesn't like leading spaces) + valid = wxdt.ParseTime(value) + + if not valid: + # deal with bug/deficiency in wx.DateTime: + try: + if wxdt.Format('%p') not in ('AM', 'PM') and checkTime in (5,8): + # couldn't parse the AM/PM field + raise ValueError('cannot convert string "%s" to valid time for the current locale; please use 24hr time instead' % value) + else: + ## dbg(indent=0, suspend=0) + raise ValueError('cannot convert string "%s" to valid time' % value) + except: + raise ValueError('cannot convert string "%s" to valid time for the current locale; please use 24hr time instead' % value) + + else: + if isinstance(value, wx.DateTime): + hour, minute, second = value.GetHour(), value.GetMinute(), value.GetSecond() + elif isinstance(value, wx.TimeSpan): + totalseconds = value.GetSeconds() + hour = totalseconds / 3600 + minute = totalseconds / 60 - (hour * 60) + second = totalseconds - ((hour * 3600) + (minute * 60)) + + elif accept_mx and isinstance(value, DateTime.DateTimeType): + hour, minute, second = value.hour, value.minute, value.second + elif accept_mx and isinstance(value, DateTime.DateTimeDeltaType): + hour, minute, second = value.hour, value.minute, value.second + else: + # Not a valid function argument + if accept_mx: + error = 'GetWxDateTime requires wxDateTime, mxDateTime or parsable time string, passed %s'% repr(value) + else: + error = 'GetWxDateTime requires wxDateTime or parsable time string, passed %s'% repr(value) +## dbg(indent=0, suspend=0) + raise ValueError(error) + + wxdt = wx.DateTimeFromDMY(1, 0, 1970) + wxdt.SetHour(hour) + wxdt.SetMinute(minute) + wxdt.SetSecond(second) + +## dbg('wxdt:', wxdt, indent=0, suspend=0) + return wxdt + + + def SetMxDateTime(self, mxdt): + """ + Because SetValue can take an mx.DateTime, (if DateTime is importable), + this is now just an alias. + """ + self.SetValue(value) + + + def GetMxDateTime(self, value=None): + """ + Returns the value of the control as an mx.DateTime, with the date + portion set to January 1, 1970. + """ + if value is None: + t = self.GetValue(as_mxDateTime=True) + else: + # Convert string 1st to wxDateTime, then use components, since + # mx' DateTime.Parser.TimeFromString() doesn't handle AM/PM: + wxdt = self.GetWxDateTime(value) + hour, minute, second = wxdt.GetHour(), wxdt.GetMinute(), wxdt.GetSecond() + t = DateTime.DateTime(1970,1,1) + DateTimeDelta(0, hour, minute, second) + return t + + + def SetMin(self, min=None): + """ + Sets the minimum value of the control. If a value of None + is provided, then the control will have no explicit minimum value. + If the value specified is greater than the current maximum value, + then the function returns 0 and the minimum will not change from + its current setting. On success, the function returns 1. + + If successful and the current value is lower than the new lower + bound, if the control is limited, the value will be automatically + adjusted to the new minimum value; if not limited, the value in the + control will be colored as invalid. + """ +## dbg('TimeCtrl::SetMin(%s)'% repr(min), indent=1) + if min is not None: + try: + min = self.GetWxDateTime(min) + self.__min = self._toGUI(min) + except: +## dbg('exception occurred', indent=0) + return False + else: + self.__min = min + + if self.IsLimited() and not self.IsInBounds(): + self.SetLimited(self.__limited) # force limited value: + else: + self._CheckValid() + ret = True +## dbg('ret:', ret, indent=0) + return ret + + + def GetMin(self, as_string = False): + """ + Gets the minimum value of the control. + If None, it will return None. Otherwise it will return + the current minimum bound on the control, as a wxDateTime + by default, or as a string if as_string argument is True. + """ +## dbg(suspend=1) +## dbg('TimeCtrl::GetMin, as_string?', as_string, indent=1) + if self.__min is None: +## dbg('(min == None)') + ret = self.__min + elif as_string: + ret = self.__min +## dbg('ret:', ret) + else: + try: + ret = self.GetWxDateTime(self.__min) + except: +## dbg(suspend=0) +## dbg('exception occurred', indent=0) + raise +## dbg('ret:', repr(ret)) +## dbg(indent=0, suspend=0) + return ret + + + def SetMax(self, max=None): + """ + Sets the maximum value of the control. If a value of None + is provided, then the control will have no explicit maximum value. + If the value specified is less than the current minimum value, then + the function returns False and the maximum will not change from its + current setting. On success, the function returns True. + + If successful and the current value is greater than the new upper + bound, if the control is limited the value will be automatically + adjusted to this maximum value; if not limited, the value in the + control will be colored as invalid. + """ +## dbg('TimeCtrl::SetMax(%s)' % repr(max), indent=1) + if max is not None: + try: + max = self.GetWxDateTime(max) + self.__max = self._toGUI(max) + except: +## dbg('exception occurred', indent=0) + return False + else: + self.__max = max +## dbg('max:', repr(self.__max)) + if self.IsLimited() and not self.IsInBounds(): + self.SetLimited(self.__limited) # force limited value: + else: + self._CheckValid() + ret = True +## dbg('ret:', ret, indent=0) + return ret + + + def GetMax(self, as_string = False): + """ + Gets the minimum value of the control. + If None, it will return None. Otherwise it will return + the current minimum bound on the control, as a wxDateTime + by default, or as a string if as_string argument is True. + """ +## dbg(suspend=1) +## dbg('TimeCtrl::GetMin, as_string?', as_string, indent=1) + if self.__max is None: +## dbg('(max == None)') + ret = self.__max + elif as_string: + ret = self.__max +## dbg('ret:', ret) + else: + try: + ret = self.GetWxDateTime(self.__max) + except: +## dbg(suspend=0) +## dbg('exception occurred', indent=0) + raise +## dbg('ret:', repr(ret)) +## dbg(indent=0, suspend=0) + return ret + + + def SetBounds(self, min=None, max=None): + """ + This function is a convenience function for setting the min and max + values at the same time. The function only applies the maximum bound + if setting the minimum bound is successful, and returns True + only if both operations succeed. + **NOTE:** leaving out an argument will remove the corresponding bound. + """ + ret = self.SetMin(min) + return ret and self.SetMax(max) + + + def GetBounds(self, as_string = False): + """ + This function returns a two-tuple (min,max), indicating the + current bounds of the control. Each value can be None if + that bound is not set. + """ + return (self.GetMin(as_string), self.GetMax(as_string)) + + + def SetLimited(self, limited): + """ + If called with a value of True, this function will cause the control + to limit the value to fall within the bounds currently specified. + If the control's value currently exceeds the bounds, it will then + be limited accordingly. + + If called with a value of 0, this function will disable value + limiting, but coloring of out-of-bounds values will still take + place if bounds have been set for the control. + """ +## dbg('TimeCtrl::SetLimited(%d)' % limited, indent=1) + self.__limited = limited + + if not limited: + self.SetMaskParameters(validRequired = False) + self._CheckValid() +## dbg(indent=0) + return + +## dbg('requiring valid value') + self.SetMaskParameters(validRequired = True) + + min = self.GetMin() + max = self.GetMax() + if min is None or max is None: +## dbg('both bounds not set; no further action taken') + return # can't limit without 2 bounds + + elif not self.IsInBounds(): + # set value to the nearest bound: + try: + value = self.GetWxDateTime() + except: +## dbg('exception occurred', indent=0) + raise + + if min <= max: # valid range doesn't span midnight +## dbg('min <= max') + # which makes the "nearest bound" computation trickier... + + # determine how long the "invalid" pie wedge is, and cut + # this interval in half for comparison purposes: + + # Note: relies on min and max and value date portions + # always being the same. + interval = (min + wx.TimeSpan(24, 0, 0, 0)) - max + + half_interval = wx.TimeSpan( + 0, # hours + 0, # minutes + interval.GetSeconds() / 2, # seconds + 0) # msec + + if value < min: # min is on next day, so use value on + # "next day" for "nearest" interval calculation: + cmp_value = value + wx.TimeSpan(24, 0, 0, 0) + else: # "before midnight; ok + cmp_value = value + + if (cmp_value - max) > half_interval: +## dbg('forcing value to min (%s)' % min.FormatTime()) + self.SetValue(min) + else: +## dbg('forcing value to max (%s)' % max.FormatTime()) + self.SetValue(max) + else: +## dbg('max < min') + # therefore max < value < min guaranteed to be true, + # so "nearest bound" calculation is much easier: + if (value - max) >= (min - value): + # current value closer to min; pick that edge of pie wedge +## dbg('forcing value to min (%s)' % min.FormatTime()) + self.SetValue(min) + else: +## dbg('forcing value to max (%s)' % max.FormatTime()) + self.SetValue(max) + +## dbg(indent=0) + + + + def IsLimited(self): + """ + Returns True if the control is currently limiting the + value to fall within any current bounds. *Note:* can + be set even if there are no current bounds. + """ + return self.__limited + + + def IsInBounds(self, value=None): + """ + Returns True if no value is specified and the current value + of the control falls within the current bounds. As the clock + is a "circle", both minimum and maximum bounds must be set for + a value to ever be considered "out of bounds". This function can + also be called with a value to see if that value would fall within + the current bounds of the given control. + """ + if value is not None: + try: + value = self.GetWxDateTime(value) # try to regularize passed value + except ValueError: +## dbg('ValueError getting wxDateTime for %s' % repr(value), indent=0) + raise + +## dbg('TimeCtrl::IsInBounds(%s)' % repr(value), indent=1) + if self.__min is None or self.__max is None: +## dbg(indent=0) + return True + + elif value is None: + try: + value = self.GetWxDateTime() + except: +## dbg('exception occurred', indent=0) + raise + +## dbg('value:', value.FormatTime()) + + # Get wxDateTime representations of bounds: + min = self.GetMin() + max = self.GetMax() + + midnight = wx.DateTimeFromDMY(1, 0, 1970) + if min <= max: # they don't span midnight + ret = min <= value <= max + + else: + # have to break into 2 tests; to be in bounds + # either "min" <= value (<= midnight of *next day*) + # or midnight <= value <= "max" + ret = min <= value or (midnight <= value <= max) +## dbg('in bounds?', ret, indent=0) + return ret + + + def IsValid( self, value ): + """ + Can be used to determine if a given value would be a legal and + in-bounds value for the control. + """ + try: + self.__validateValue(value) + return True + except ValueError: + return False + + def SetFormat(self, format): + self.SetParameters(format=format) + + def GetFormat(self): + if self.__displaySeconds: + if self.__fmt24hr: return '24HHMMSS' + else: return 'HHMMSS' + else: + if self.__fmt24hr: return '24HHMM' + else: return 'HHMM' + +#------------------------------------------------------------------------------------------------------------- +# these are private functions and overrides: + + + def __OnTextChange(self, event=None): +## dbg('TimeCtrl::OnTextChange', indent=1) + + # Allow Maskedtext base control to color as appropriate, + # and Skip the EVT_TEXT event (if appropriate.) + ##! WS: For some inexplicable reason, every wxTextCtrl.SetValue() + ## call is generating two (2) EVT_TEXT events. (!) + ## The the only mechanism I can find to mask this problem is to + ## keep track of last value seen, and declare a valid EVT_TEXT + ## event iff the value has actually changed. The masked edit + ## OnTextChange routine does this, and returns True on a valid event, + ## False otherwise. + if not BaseMaskedTextCtrl._OnTextChange(self, event): + return + +## dbg('firing TimeUpdatedEvent...') + evt = TimeUpdatedEvent(self.GetId(), self.GetValue()) + evt.SetEventObject(self) + self.GetEventHandler().ProcessEvent(evt) +## dbg(indent=0) + + + def SetInsertionPoint(self, pos): + """ + This override records the specified position and associated cell before + calling base class' function. This is necessary to handle the optional + spin button, because the insertion point is lost when the focus shifts + to the spin button. + """ +## dbg('TimeCtrl::SetInsertionPoint', pos, indent=1) + BaseMaskedTextCtrl.SetInsertionPoint(self, pos) # (causes EVT_TEXT event to fire) + self.__posCurrent = self.GetInsertionPoint() +## dbg(indent=0) + + + def SetSelection(self, sel_start, sel_to): +## dbg('TimeCtrl::SetSelection', sel_start, sel_to, indent=1) + + # Adjust selection range to legal extent if not already + if sel_start < 0: + sel_start = 0 + + if self.__posCurrent != sel_start: # force selection and insertion point to match + self.SetInsertionPoint(sel_start) + cell_start, cell_end = self._FindField(sel_start)._extent + if not cell_start <= sel_to <= cell_end: + sel_to = cell_end + + self.__bSelection = sel_start != sel_to + BaseMaskedTextCtrl.SetSelection(self, sel_start, sel_to) +## dbg(indent=0) + + + def __OnSpin(self, key): + """ + This is the function that gets called in response to up/down arrow or + bound spin button events. + """ + self.__IncrementValue(key, self.__posCurrent) # changes the value + + # Ensure adjusted control regains focus and has adjusted portion + # selected: + self.SetFocus() + start, end = self._FindField(self.__posCurrent)._extent + self.SetInsertionPoint(start) + self.SetSelection(start, end) +## dbg('current position:', self.__posCurrent) + + + def __OnSpinUp(self, event): + """ + Event handler for any bound spin button on EVT_SPIN_UP; + causes control to behave as if up arrow was pressed. + """ +## dbg('TimeCtrl::OnSpinUp', indent=1) + self.__OnSpin(wx.WXK_UP) + keep_processing = False +## dbg(indent=0) + return keep_processing + + + def __OnSpinDown(self, event): + """ + Event handler for any bound spin button on EVT_SPIN_DOWN; + causes control to behave as if down arrow was pressed. + """ +## dbg('TimeCtrl::OnSpinDown', indent=1) + self.__OnSpin(wx.WXK_DOWN) + keep_processing = False +## dbg(indent=0) + return keep_processing + + + def __OnChar(self, event): + """ + Handler to explicitly look for ':' keyevents, and if found, + clear the m_shiftDown field, so it will behave as forward tab. + It then calls the base control's _OnChar routine with the modified + event instance. + """ +## dbg('TimeCtrl::OnChar', indent=1) + keycode = event.GetKeyCode() +## dbg('keycode:', keycode) + if keycode == ord(':'): +## dbg('colon seen! removing shift attribute') + event.m_shiftDown = False + BaseMaskedTextCtrl._OnChar(self, event ) ## handle each keypress +## dbg(indent=0) + + + def __OnSetToNow(self, event): + """ + This is the key handler for '!' and 'c'; this allows the user to + quickly set the value of the control to the current time. + """ + self.SetValue(wx.DateTime_Now().FormatTime()) + keep_processing = False + return keep_processing + + + def __LimitSelection(self, event): + """ + Event handler for motion events; this handler + changes limits the selection to the new cell boundaries. + """ +## dbg('TimeCtrl::LimitSelection', indent=1) + pos = self.GetInsertionPoint() + self.__posCurrent = pos + sel_start, sel_to = self.GetSelection() + selection = sel_start != sel_to + if selection: + # only allow selection to end of current cell: + start, end = self._FindField(sel_start)._extent + if sel_to < pos: sel_to = start + elif sel_to > pos: sel_to = end + +## dbg('new pos =', self.__posCurrent, 'select to ', sel_to) + self.SetInsertionPoint(self.__posCurrent) + self.SetSelection(self.__posCurrent, sel_to) + if event: event.Skip() +## dbg(indent=0) + + + def __IncrementValue(self, key, pos): +## dbg('TimeCtrl::IncrementValue', key, pos, indent=1) + text = self.GetValue() + field = self._FindField(pos) +## dbg('field: ', field._index) + start, end = field._extent + slice = text[start:end] + if key == wx.WXK_UP: increment = 1 + else: increment = -1 + + if slice in ('A', 'P'): + if slice == 'A': newslice = 'P' + elif slice == 'P': newslice = 'A' + newvalue = text[:start] + newslice + text[end:] + + elif field._index == 0: + # adjusting this field is trickier, as its value can affect the + # am/pm setting. So, we use wxDateTime to generate a new value for us: + # (Use a fixed date not subject to DST variations:) + converter = wx.DateTimeFromDMY(1, 0, 1970) +## dbg('text: "%s"' % text) + converter.ParseTime(text.strip()) + currenthour = converter.GetHour() +## dbg('current hour:', currenthour) + newhour = (currenthour + increment) % 24 +## dbg('newhour:', newhour) + converter.SetHour(newhour) +## dbg('converter.GetHour():', converter.GetHour()) + newvalue = converter # take advantage of auto-conversion for am/pm in .SetValue() + + else: # minute or second field; handled the same way: + newslice = "%02d" % ((int(slice) + increment) % 60) + newvalue = text[:start] + newslice + text[end:] + + try: + self.SetValue(newvalue) + + except ValueError: # must not be in bounds: + if not wx.Validator_IsSilent(): + wx.Bell() +## dbg(indent=0) + + + def _toGUI( self, wxdt ): + """ + This function takes a wxdt as an unambiguous representation of a time, and + converts it to a string appropriate for the format of the control. + """ + if self.__fmt24hr: + if self.__displaySeconds: strval = wxdt.Format('%H:%M:%S') + else: strval = wxdt.Format('%H:%M') + else: + if self.__displaySeconds: strval = wxdt.Format('%I:%M:%S %p') + else: strval = wxdt.Format('%I:%M %p') + + return strval + + + def __validateValue( self, value ): + """ + This function converts the value to a wxDateTime if not already one, + does bounds checking and raises ValueError if argument is + not a valid value for the control as currently specified. + It is used by both the SetValue() and the IsValid() methods. + """ +## dbg('TimeCtrl::__validateValue(%s)' % repr(value), indent=1) + if not value: +## dbg(indent=0) + raise ValueError('%s not a valid time value' % repr(value)) + + valid = True # assume true + try: + value = self.GetWxDateTime(value) # regularize form; can generate ValueError if problem doing so + except: +## dbg('exception occurred', indent=0) + raise + + if self.IsLimited() and not self.IsInBounds(value): +## dbg(indent=0) + raise ValueError ( + 'value %s is not within the bounds of the control' % str(value) ) +## dbg(indent=0) + return value + +#---------------------------------------------------------------------------- +# Test jig for TimeCtrl: + +if __name__ == '__main__': + import traceback + + class TestPanel(wx.Panel): + def __init__(self, parent, id, + pos = wx.DefaultPosition, size = wx.DefaultSize, + fmt24hr = 0, test_mx = 0, + style = wx.TAB_TRAVERSAL ): + + wx.Panel.__init__(self, parent, id, pos, size, style) + + self.test_mx = test_mx + + self.tc = TimeCtrl(self, 10, fmt24hr = fmt24hr) + sb = wx.SpinButton( self, 20, wx.DefaultPosition, (-1,20), 0 ) + self.tc.BindSpinButton(sb) + + sizer = wx.BoxSizer( wx.HORIZONTAL ) + sizer.Add( self.tc, 0, wx.ALIGN_CENTRE|wx.LEFT|wx.TOP|wx.BOTTOM, 5 ) + sizer.Add( sb, 0, wx.ALIGN_CENTRE|wx.RIGHT|wx.TOP|wx.BOTTOM, 5 ) + + self.SetAutoLayout( True ) + self.SetSizer( sizer ) + sizer.Fit( self ) + sizer.SetSizeHints( self ) + + self.Bind(EVT_TIMEUPDATE, self.OnTimeChange, self.tc) + + def OnTimeChange(self, event): +## dbg('OnTimeChange: value = ', event.GetValue()) + wxdt = self.tc.GetWxDateTime() +## dbg('wxdt =', wxdt.GetHour(), wxdt.GetMinute(), wxdt.GetSecond()) + if self.test_mx: + mxdt = self.tc.GetMxDateTime() +## dbg('mxdt =', mxdt.hour, mxdt.minute, mxdt.second) + + + class MyApp(wx.App): + def OnInit(self): + import sys + fmt24hr = '24' in sys.argv + test_mx = 'mx' in sys.argv + try: + frame = wx.Frame(None, -1, "TimeCtrl Test", (20,20), (100,100) ) + panel = TestPanel(frame, -1, (-1,-1), fmt24hr=fmt24hr, test_mx = test_mx) + frame.Show(True) + except: + traceback.print_exc() + return False + return True + + try: + app = MyApp(0) + app.MainLoop() + except: + traceback.print_exc() +__i=0 + +## CHANGELOG: +## ==================== +## Version 1.3 +## 1. Converted docstrings to reST format, added doc for ePyDoc. +## 2. Renamed helper functions, vars etc. not intended to be visible in public +## interface to code. +## +## Version 1.2 +## 1. Changed parameter name display_seconds to displaySeconds, to follow +## other masked edit conventions. +## 2. Added format parameter, to remove need to use both fmt24hr and displaySeconds. +## 3. Changed inheritance to use BaseMaskedTextCtrl, to remove exposure of +## nonsensical parameter methods from the control, so it will work +## properly with Boa. diff --git a/wx/lib/mixins/__init__.py b/wx/lib/mixins/__init__.py new file mode 100644 index 00000000..5586cba2 --- /dev/null +++ b/wx/lib/mixins/__init__.py @@ -0,0 +1,18 @@ +#---------------------------------------------------------------------- +# Name: wxPython.lib.mixins +# Purpose: A package for helpful wxPython mix-in classes +# +# Author: Robin Dunn +# +# Created: 15-May-2001 +# RCS-ID: $Id$ +# Copyright: (c) 2001 by Total Control Software +# Licence: wxWindows license +#---------------------------------------------------------------------- +# 12/14/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o 2.5 compatability update. +# + + + diff --git a/wx/lib/mixins/grid.py b/wx/lib/mixins/grid.py new file mode 100644 index 00000000..586d24ee --- /dev/null +++ b/wx/lib/mixins/grid.py @@ -0,0 +1,48 @@ +#---------------------------------------------------------------------------- +# Name: wx.lib.mixins.grid +# Purpose: Helpful mix-in classes for wx.Grid +# +# Author: Robin Dunn +# +# Created: 5-June-2001 +# RCS-ID: $Id$ +# Copyright: (c) 2001 by Total Control Software +# Licence: wxWindows license +#---------------------------------------------------------------------------- +# 12/14/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o 2.5 compatability update. +# o Untested +# +# 12/21/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o wxGridAutoEditMixin -> GridAutoEditMixin +# + +import wx +import wx.grid + +#---------------------------------------------------------------------------- + + +class GridAutoEditMixin: + """A mix-in class that automatically enables the grid edit control when + a cell is selected. + + If your class hooks EVT_GRID_SELECT_CELL be sure to call event.Skip so + this handler will be called too. + """ + + def __init__(self): + self.Bind(wx.grid.EVT_GRID_SELECT_CELL, self.__OnSelectCell) + + + def __DoEnableEdit(self): + if self.CanEnableCellControl(): + self.EnableCellEditControl() + + + def __OnSelectCell(self, evt): + wx.CallAfter(self.__DoEnableEdit) + evt.Skip() + diff --git a/wx/lib/mixins/gridlabelrenderer.py b/wx/lib/mixins/gridlabelrenderer.py new file mode 100644 index 00000000..e7df71b3 --- /dev/null +++ b/wx/lib/mixins/gridlabelrenderer.py @@ -0,0 +1,248 @@ +#---------------------------------------------------------------------- +# Name: wx.lib.mixins.gridlabelrenderer +# Purpose: A Grid mixin that enables renderers to be plugged in +# for drawing the row and col labels, similar to how the +# cell renderers work. +# +# Author: Robin Dunn +# +# Created: 20-Mar-2009 +# RCS-ID: $Id$ +# Copyright: (c) 2009 by Total Control Software +# Licence: wxWindows license +#---------------------------------------------------------------------- + +""" +A Grid mixin that enables renderers to be plugged in for drawing the +row and col labels, similar to how the cell renderers work. +""" + +import wx + + +class GridWithLabelRenderersMixin(object): + """ + This class can be mixed with wx.grid.Grid to add the ability to plugin + label renderer objects for the row, column and corner labels, similar to + how the cell renderers work in the main Grid class. + """ + def __init__(self): + self.GetGridRowLabelWindow().Bind(wx.EVT_PAINT, self._onPaintRowLabels) + self.GetGridColLabelWindow().Bind(wx.EVT_PAINT, self._onPaintColLabels) + self.GetGridCornerLabelWindow().Bind(wx.EVT_PAINT, self._onPaintCornerLabel) + + self._rowRenderers = dict() + self._colRenderers = dict() + self._cornderRenderer = None + self._defRowRenderer = None + self._defColRenderer = None + + + def SetRowLabelRenderer(self, row, renderer): + """ + Register a renderer to be used for drawing the label for the + given row. + """ + if renderer is None: + if row in self._rowRenderers: + del self._rowRenderers[row] + else: + self._rowRenderers[row] = renderer + + + def SetDefaultRowLabelRenderer(self, renderer): + """ + Set the row label renderer that should be used for any row + that does not have an explicitly set renderer. Defaults to + an instance of `GridDefaultRowLabelRenderer`. + """ + self._defRowRenderer = renderer + + + def SetColLabelRenderer(self, col, renderer): + """ + Register a renderer to be used for drawing the label for the + given column. + """ + if renderer is None: + if col in self._colRenderers: + del self._colRenderers[col] + else: + self._colRenderers[col] = renderer + + + def SetDefaultColLabelRenderer(self, renderer): + """ + Set the column label renderer that should be used for any + column that does not have an explicitly set renderer. + Defaults to an instance of `GridDefaultColLabelRenderer`. + """ + self._defColRenderer = renderer + + + + def SetCornerLabelRenderer(self, renderer): + """ + Sets the renderer that should be used for drawing the area in + the upper left corner of the Grid, between the row labels and + the column labels. Defaults to an instance of + `GridDefaultCornerLabelRenderer` + """ + self._cornderRenderer = renderer + + + #---------------------------------------------------------------- + + def _onPaintRowLabels(self, evt): + window = evt.GetEventObject() + dc = wx.PaintDC(window) + + rows = self.CalcRowLabelsExposed(window.GetUpdateRegion()) + if rows == [-1]: + return + + x, y = self.CalcUnscrolledPosition((0,0)) + pt = dc.GetDeviceOrigin() + dc.SetDeviceOrigin(pt.x, pt.y-y) + for row in rows: + top, bottom = self._getRowTopBottom(row) + rect = wx.Rect() + rect.top = top + rect.bottom = bottom + rect.x = 0 + rect.width = self.GetRowLabelSize() + + renderer = self._rowRenderers.get(row, None) or \ + self._defRowRenderer or GridDefaultRowLabelRenderer() + renderer.Draw(self, dc, rect, row) + + + def _onPaintColLabels(self, evt): + window = evt.GetEventObject() + dc = wx.PaintDC(window) + + cols = self.CalcColLabelsExposed(window.GetUpdateRegion()) + if cols == [-1]: + return + + x, y = self.CalcUnscrolledPosition((0,0)) + pt = dc.GetDeviceOrigin() + dc.SetDeviceOrigin(pt.x-x, pt.y) + for col in cols: + left, right = self._getColLeftRight(col) + rect = wx.Rect() + rect.left = left + rect.right = right + rect.y = 0 + rect.height = self.GetColLabelSize() + + renderer = self._colRenderers.get(col, None) or \ + self._defColRenderer or GridDefaultColLabelRenderer() + renderer.Draw(self, dc, rect, col) + + + def _onPaintCornerLabel(self, evt): + window = evt.GetEventObject() + dc = wx.PaintDC(window) + w, h = window.GetSize() + rect = wx.Rect(0, 0, w, h) + + renderer = self._cornderRenderer or GridDefaultCornerLabelRenderer() + renderer.Draw(self, dc, rect, -1) + + + + # NOTE: These helpers or something like them should probably be publicly + # available in the C++ wxGrid class, but they are currently protected so + # for now we will have to calculate them ourselves. + def _getColLeftRight(self, col): + c = 0 + left = 0 + while c < col: + left += self.GetColSize(c) + c += 1 + right = left + self.GetColSize(col) + return left, right + + def _getRowTopBottom(self, row): + r = 0 + top = 0 + while r < row: + top += self.GetRowSize(r) + r += 1 + bottom = top + self.GetRowSize(row) - 1 + return top, bottom + + + + +class GridLabelRenderer(object): + """ + Base class for row, col or corner label renderers. + """ + + def Draw(self, grid, dc, rect, row_or_col): + """ + Override this method in derived classes to do the actual + drawing of the label. + """ + raise NotImplementedError + + + # These two can be used to duplicate the default wxGrid label drawing + def DrawBorder(self, grid, dc, rect): + """ + Draw a standard border around the label, to give a simple 3D + effect like the stock wx.grid.Grid labels do. + """ + top = rect.top + bottom = rect.bottom + left = rect.left + right = rect.right + dc.SetPen(wx.Pen(wx.SystemSettings.GetColour(wx.SYS_COLOUR_3DSHADOW))) + dc.DrawLine(right, top, right, bottom) + dc.DrawLine(left, top, left, bottom) + dc.DrawLine(left, bottom, right, bottom) + dc.SetPen(wx.WHITE_PEN) + dc.DrawLine(left+1, top, left+1, bottom) + dc.DrawLine(left+1, top, right, top) + + + def DrawText(self, grid, dc, rect, text, hAlign, vAlign): + """ + Draw the label's text in the rectangle, using the alignment + flags, and the grid's specified label font and color. + """ + dc.SetBackgroundMode(wx.TRANSPARENT) + dc.SetTextForeground(grid.GetLabelTextColour()) + dc.SetFont(grid.GetLabelFont()) + rect = wx.Rect(*rect) + rect.Deflate(2,2) + grid.DrawTextRectangle(dc, text, rect, hAlign, vAlign) + + + +# These classes draw approximately the same things that the built-in +# label windows do in C++, but are adapted to fit into this label +# renderer scheme. + +class GridDefaultRowLabelRenderer(GridLabelRenderer): + def Draw(self, grid, dc, rect, row): + hAlign, vAlign = grid.GetRowLabelAlignment() + text = grid.GetRowLabelValue(row) + self.DrawBorder(grid, dc, rect) + self.DrawText(grid, dc, rect, text, hAlign, vAlign) + +class GridDefaultColLabelRenderer(GridLabelRenderer): + def Draw(self, grid, dc, rect, col): + hAlign, vAlign = grid.GetColLabelAlignment() + text = grid.GetColLabelValue(col) + self.DrawBorder(grid, dc, rect) + self.DrawText(grid, dc, rect, text, hAlign, vAlign) + +class GridDefaultCornerLabelRenderer(GridLabelRenderer): + def Draw(self, grid, dc, rect, row_or_col): + self.DrawBorder(grid, dc, rect) + + +#--------------------------------------------------------------------------- diff --git a/wx/lib/mixins/imagelist.py b/wx/lib/mixins/imagelist.py new file mode 100644 index 00000000..d599d7ae --- /dev/null +++ b/wx/lib/mixins/imagelist.py @@ -0,0 +1,78 @@ +#---------------------------------------------------------------------------- +# Name: wx.lib.mixins.imagelist +# Purpose: Helpful mix-in classes for using a wxImageList +# +# Author: Robin Dunn +# +# Created: 15-May-2001 +# RCS-ID: $Id$ +# Copyright: (c) 2001 by Total Control Software +# Licence: wxWindows license +#---------------------------------------------------------------------------- +# 12/14/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o 2.5 compatability update. +# o Untested. +# + +import wx + +#---------------------------------------------------------------------------- + +class MagicImageList: + ''' + Mix-in to provide "magic" growing image lists + By Mike Fletcher + ''' + + ### LAZYTREE and LISTCONTROL Methods + DEFAULTICONSIZE = 16 + + def SetupIcons(self, images=(), size=None): + self.__size = size or self.DEFAULTICONSIZE + self.__magicImageList = wx.ImageList (self.__size,self.__size) + self.__magicImageListMapping = {} + self.SetImageList ( + self.__magicImageList, { + 16:wx.IMAGE_LIST_SMALL, + 32:wx.IMAGE_LIST_NORMAL, + }[self.__size] + ) + for image in images: + self.AddIcon (image) + + def GetIcons (self, node): + '''Get icon indexes for a given node, or None if no associated icon''' + icon = self.GetIcon( node ) + if icon: + index = self.AddIcon (icon) + return index, index + return None + + + ### Local methods... + def AddIcon(self, icon, mask = wx.NullBitmap): + '''Add an icon to the image list, or get the index if already there''' + index = self.__magicImageListMapping.get (id (icon)) + if index is None: + if isinstance( icon, wxIconPtr ): + index = self.__magicImageList.AddIcon( icon ) + elif isinstance( icon, wx.BitmapPtr ): + if isinstance( mask, wx.Colour ): + index = self.__magicImageList.AddWithColourMask( icon, mask ) + else: + index = self.__magicImageList.Add( icon, mask ) + else: + raise ValueError("Unexpected icon object %s, " + "expected wx.Icon or wx.Bitmap" % (icon)) + self.__magicImageListMapping [id (icon)] = index + return index + + ### Customisation point... + def GetIcon( self, node ): + '''Get the actual icon object for a node''' + if hasattr (node,"DIAGRAMICON"): + return node.DIAGRAMICON + + + diff --git a/wx/lib/mixins/inspection.py b/wx/lib/mixins/inspection.py new file mode 100644 index 00000000..0a5c377b --- /dev/null +++ b/wx/lib/mixins/inspection.py @@ -0,0 +1,89 @@ +#---------------------------------------------------------------------------- +# Name: wx.lib.mixins.inspection +# Purpose: A mix-in class that can add PyCrust-based inspection of the +# app's widgets and sizers. +# +# Author: Robin Dunn +# +# Created: 21-Nov-2006 +# RCS-ID: $Id$ +# Copyright: (c) 2006 by Total Control Software +# Licence: wxWindows license +#---------------------------------------------------------------------------- + +# NOTE: This class was originally based on ideas sent to the +# wxPython-users mail list by Dan Eloff. + +import wx +from wx.lib.inspection import InspectionTool + + +#---------------------------------------------------------------------------- + +class InspectionMixin(object): + """ + This class is intended to be used as a mix-in with the wx.App + class. When used it will add the ability to popup a + InspectionFrame window where the widget under the mouse cursor + will be selected in the tree and loaded into the shell's namespace + as 'obj'. The default key sequence to activate the inspector is + Ctrl-Alt-I (or Cmd-Alt-I on Mac) but this can be changed via + parameters to the `Init` method, or the application can call + `ShowInspectionTool` from other event handlers if desired. + + To use this class simply derive a class from wx.App and + InspectionMixin and then call the `Init` method from the app's + OnInit. + """ + def InitInspection(self, pos=wx.DefaultPosition, size=wx.Size(850,700), + config=None, locals=None, + alt=True, cmd=True, shift=False, keyCode=ord('I')): + """ + Make the event binding that will activate the InspectionFrame window. + """ + self.Bind(wx.EVT_KEY_DOWN, self._OnKeyPress) + self._alt = alt + self._cmd = cmd + self._shift = shift + self._keyCode = keyCode + InspectionTool().Init(pos, size, config, locals, self) + + def _OnKeyPress(self, evt): + """ + Event handler, check for our hot-key. Normally it is + Ctrl-Alt-I but that can be changed by what is passed to the + Init method. + """ + if evt.AltDown() == self._alt and \ + evt.CmdDown() == self._cmd and \ + evt.ShiftDown() == self._shift and \ + evt.GetKeyCode() == self._keyCode: + self.ShowInspectionTool() + else: + evt.Skip() + + Init = InitInspection # compatibility alias + + def ShowInspectionTool(self): + """ + Show the Inspection tool, creating it if neccesary, setting it + to display the widget under the cursor. + """ + # get the current widget under the mouse + wnd = wx.FindWindowAtPointer() + InspectionTool().Show(wnd) + + +#--------------------------------------------------------------------------- + +class InspectableApp(wx.App, InspectionMixin): + """ + A simple mix of wx.App and InspectionMixin that can be used stand-alone. + """ + + def OnInit(self): + self.InitInspection() + return True + +#--------------------------------------------------------------------------- + diff --git a/wx/lib/mixins/listctrl.py b/wx/lib/mixins/listctrl.py new file mode 100644 index 00000000..924a52be --- /dev/null +++ b/wx/lib/mixins/listctrl.py @@ -0,0 +1,877 @@ +#---------------------------------------------------------------------------- +# Name: wx.lib.mixins.listctrl +# Purpose: Helpful mix-in classes for wxListCtrl +# +# Author: Robin Dunn +# +# Created: 15-May-2001 +# RCS-ID: $Id$ +# Copyright: (c) 2001 by Total Control Software +# Licence: wxWindows license +#---------------------------------------------------------------------------- +# 12/14/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o 2.5 compatability update. +# o ListCtrlSelectionManagerMix untested. +# +# 12/21/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o wxColumnSorterMixin -> ColumnSorterMixin +# o wxListCtrlAutoWidthMixin -> ListCtrlAutoWidthMixin +# ... +# 13/10/2004 - Pim Van Heuven (pim@think-wize.com) +# o wxTextEditMixin: Support Horizontal scrolling when TAB is pressed on long +# ListCtrls, support for WXK_DOWN, WXK_UP, performance improvements on +# very long ListCtrls, Support for virtual ListCtrls +# +# 15-Oct-2004 - Robin Dunn +# o wxTextEditMixin: Added Shift-TAB support +# +# 2008-11-19 - raf +# o ColumnSorterMixin: Added GetSortState() +# + +import locale +import wx + +#---------------------------------------------------------------------------- + +class ColumnSorterMixin: + """ + A mixin class that handles sorting of a wx.ListCtrl in REPORT mode when + the column header is clicked on. + + There are a few requirments needed in order for this to work genericly: + + 1. The combined class must have a GetListCtrl method that + returns the wx.ListCtrl to be sorted, and the list control + must exist at the time the wx.ColumnSorterMixin.__init__ + method is called because it uses GetListCtrl. + + 2. Items in the list control must have a unique data value set + with list.SetItemData. + + 3. The combined class must have an attribute named itemDataMap + that is a dictionary mapping the data values to a sequence of + objects representing the values in each column. These values + are compared in the column sorter to determine sort order. + + Interesting methods to override are GetColumnSorter, + GetSecondarySortValues, and GetSortImages. See below for details. + """ + + def __init__(self, numColumns): + self.SetColumnCount(numColumns) + list = self.GetListCtrl() + if not list: + raise ValueError, "No wx.ListCtrl available" + list.Bind(wx.EVT_LIST_COL_CLICK, self.__OnColClick, list) + + + def SetColumnCount(self, newNumColumns): + self._colSortFlag = [0] * newNumColumns + self._col = -1 + + + def SortListItems(self, col=-1, ascending=1): + """Sort the list on demand. Can also be used to set the sort column and order.""" + oldCol = self._col + if col != -1: + self._col = col + self._colSortFlag[col] = ascending + self.GetListCtrl().SortItems(self.GetColumnSorter()) + self.__updateImages(oldCol) + + + def GetColumnWidths(self): + """ + Returns a list of column widths. Can be used to help restore the current + view later. + """ + list = self.GetListCtrl() + rv = [] + for x in range(len(self._colSortFlag)): + rv.append(list.GetColumnWidth(x)) + return rv + + + def GetSortImages(self): + """ + Returns a tuple of image list indexesthe indexes in the image list for an image to be put on the column + header when sorting in descending order. + """ + return (-1, -1) # (decending, ascending) image IDs + + + def GetColumnSorter(self): + """Returns a callable object to be used for comparing column values when sorting.""" + return self.__ColumnSorter + + + def GetSecondarySortValues(self, col, key1, key2): + """Returns a tuple of 2 values to use for secondary sort values when the + items in the selected column match equal. The default just returns the + item data values.""" + return (key1, key2) + + + def __OnColClick(self, evt): + oldCol = self._col + self._col = col = evt.GetColumn() + self._colSortFlag[col] = int(not self._colSortFlag[col]) + self.GetListCtrl().SortItems(self.GetColumnSorter()) + if wx.Platform != "__WXMAC__" or wx.SystemOptions.GetOptionInt("mac.listctrl.always_use_generic") == 1: + self.__updateImages(oldCol) + evt.Skip() + self.OnSortOrderChanged() + + + def OnSortOrderChanged(self): + """ + Callback called after sort order has changed (whenever user + clicked column header). + """ + pass + + + def GetSortState(self): + """ + Return a tuple containing the index of the column that was last sorted + and the sort direction of that column. + Usage: + col, ascending = self.GetSortState() + # Make changes to list items... then resort + self.SortListItems(col, ascending) + """ + return (self._col, self._colSortFlag[self._col]) + + + def __ColumnSorter(self, key1, key2): + col = self._col + ascending = self._colSortFlag[col] + item1 = self.itemDataMap[key1][col] + item2 = self.itemDataMap[key2][col] + + #--- Internationalization of string sorting with locale module + if type(item1) == unicode and type(item2) == unicode: + cmpVal = locale.strcoll(item1, item2) + elif type(item1) == str or type(item2) == str: + cmpVal = locale.strcoll(str(item1), str(item2)) + else: + cmpVal = cmp(item1, item2) + #--- + + # If the items are equal then pick something else to make the sort value unique + if cmpVal == 0: + cmpVal = apply(cmp, self.GetSecondarySortValues(col, key1, key2)) + + if ascending: + return cmpVal + else: + return -cmpVal + + + def __updateImages(self, oldCol): + sortImages = self.GetSortImages() + if self._col != -1 and sortImages[0] != -1: + img = sortImages[self._colSortFlag[self._col]] + list = self.GetListCtrl() + if oldCol != -1: + list.ClearColumnImage(oldCol) + list.SetColumnImage(self._col, img) + + +#---------------------------------------------------------------------------- +#---------------------------------------------------------------------------- + +class ListCtrlAutoWidthMixin: + """ A mix-in class that automatically resizes the last column to take up + the remaining width of the wx.ListCtrl. + + This causes the wx.ListCtrl to automatically take up the full width of + the list, without either a horizontal scroll bar (unless absolutely + necessary) or empty space to the right of the last column. + + NOTE: This only works for report-style lists. + + WARNING: If you override the EVT_SIZE event in your wx.ListCtrl, make + sure you call event.Skip() to ensure that the mixin's + _OnResize method is called. + + This mix-in class was written by Erik Westra + """ + def __init__(self): + """ Standard initialiser. + """ + self._resizeColMinWidth = None + self._resizeColStyle = "LAST" + self._resizeCol = 0 + self.Bind(wx.EVT_SIZE, self._onResize) + self.Bind(wx.EVT_LIST_COL_END_DRAG, self._onResize, self) + + + def setResizeColumn(self, col): + """ + Specify which column that should be autosized. Pass either + 'LAST' or the column number. Default is 'LAST'. + """ + if col == "LAST": + self._resizeColStyle = "LAST" + else: + self._resizeColStyle = "COL" + self._resizeCol = col + + + def resizeLastColumn(self, minWidth): + """ Resize the last column appropriately. + + If the list's columns are too wide to fit within the window, we use + a horizontal scrollbar. Otherwise, we expand the right-most column + to take up the remaining free space in the list. + + This method is called automatically when the wx.ListCtrl is resized; + you can also call it yourself whenever you want the last column to + be resized appropriately (eg, when adding, removing or resizing + columns). + + 'minWidth' is the preferred minimum width for the last column. + """ + self.resizeColumn(minWidth) + + + def resizeColumn(self, minWidth): + self._resizeColMinWidth = minWidth + self._doResize() + + + # ===================== + # == Private Methods == + # ===================== + + def _onResize(self, event): + """ Respond to the wx.ListCtrl being resized. + + We automatically resize the last column in the list. + """ + if 'gtk2' in wx.PlatformInfo: + self._doResize() + else: + wx.CallAfter(self._doResize) + event.Skip() + + + def _doResize(self): + """ Resize the last column as appropriate. + + If the list's columns are too wide to fit within the window, we use + a horizontal scrollbar. Otherwise, we expand the right-most column + to take up the remaining free space in the list. + + We remember the current size of the last column, before resizing, + as the preferred minimum width if we haven't previously been given + or calculated a minimum width. This ensure that repeated calls to + _doResize() don't cause the last column to size itself too large. + """ + + if not self: # avoid a PyDeadObject error + return + + if self.GetSize().height < 32: + return # avoid an endless update bug when the height is small. + + numCols = self.GetColumnCount() + if numCols == 0: return # Nothing to resize. + + if(self._resizeColStyle == "LAST"): + resizeCol = self.GetColumnCount() + else: + resizeCol = self._resizeCol + + resizeCol = max(1, resizeCol) + + if self._resizeColMinWidth == None: + self._resizeColMinWidth = self.GetColumnWidth(resizeCol - 1) + + # We're showing the vertical scrollbar -> allow for scrollbar width + # NOTE: on GTK, the scrollbar is included in the client size, but on + # Windows it is not included + listWidth = self.GetClientSize().width + if wx.Platform != '__WXMSW__': + if self.GetItemCount() > self.GetCountPerPage(): + scrollWidth = wx.SystemSettings_GetMetric(wx.SYS_VSCROLL_X) + listWidth = listWidth - scrollWidth + + totColWidth = 0 # Width of all columns except last one. + for col in range(numCols): + if col != (resizeCol-1): + totColWidth = totColWidth + self.GetColumnWidth(col) + + resizeColWidth = self.GetColumnWidth(resizeCol - 1) + + if totColWidth + self._resizeColMinWidth > listWidth: + # We haven't got the width to show the last column at its minimum + # width -> set it to its minimum width and allow the horizontal + # scrollbar to show. + self.SetColumnWidth(resizeCol-1, self._resizeColMinWidth) + return + + # Resize the last column to take up the remaining available space. + + self.SetColumnWidth(resizeCol-1, listWidth - totColWidth) + + + + +#---------------------------------------------------------------------------- +#---------------------------------------------------------------------------- + +SEL_FOC = wx.LIST_STATE_SELECTED | wx.LIST_STATE_FOCUSED +def selectBeforePopup(event): + """Ensures the item the mouse is pointing at is selected before a popup. + + Works with both single-select and multi-select lists.""" + ctrl = event.GetEventObject() + if isinstance(ctrl, wx.ListCtrl): + n, flags = ctrl.HitTest(event.GetPosition()) + if n >= 0: + if not ctrl.GetItemState(n, wx.LIST_STATE_SELECTED): + for i in range(ctrl.GetItemCount()): + ctrl.SetItemState(i, 0, SEL_FOC) + #for i in getListCtrlSelection(ctrl, SEL_FOC): + # ctrl.SetItemState(i, 0, SEL_FOC) + ctrl.SetItemState(n, SEL_FOC, SEL_FOC) + + +def getListCtrlSelection(listctrl, state=wx.LIST_STATE_SELECTED): + """ Returns list of item indexes of given state (selected by defaults) """ + res = [] + idx = -1 + while 1: + idx = listctrl.GetNextItem(idx, wx.LIST_NEXT_ALL, state) + if idx == -1: + break + res.append(idx) + return res + +wxEVT_DOPOPUPMENU = wx.NewEventType() +EVT_DOPOPUPMENU = wx.PyEventBinder(wxEVT_DOPOPUPMENU, 0) + + +class ListCtrlSelectionManagerMix: + """Mixin that defines a platform independent selection policy + + As selection single and multi-select list return the item index or a + list of item indexes respectively. + """ + _menu = None + + def __init__(self): + self.Bind(wx.EVT_RIGHT_DOWN, self.OnLCSMRightDown) + self.Bind(EVT_DOPOPUPMENU, self.OnLCSMDoPopup) +# self.Connect(-1, -1, self.wxEVT_DOPOPUPMENU, self.OnLCSMDoPopup) + + + def getPopupMenu(self): + """ Override to implement dynamic menus (create) """ + return self._menu + + + def setPopupMenu(self, menu): + """ Must be set for default behaviour """ + self._menu = menu + + + def afterPopupMenu(self, menu): + """ Override to implement dynamic menus (destroy) """ + pass + + + def getSelection(self): + res = getListCtrlSelection(self) + if self.GetWindowStyleFlag() & wx.LC_SINGLE_SEL: + if res: + return res[0] + else: + return -1 + else: + return res + + + def OnLCSMRightDown(self, event): + selectBeforePopup(event) + event.Skip() + menu = self.getPopupMenu() + if menu: + evt = wx.PyEvent() + evt.SetEventType(wxEVT_DOPOPUPMENU) + evt.menu = menu + evt.pos = event.GetPosition() + wx.PostEvent(self, evt) + + + def OnLCSMDoPopup(self, event): + self.PopupMenu(event.menu, event.pos) + self.afterPopupMenu(event.menu) + + +#---------------------------------------------------------------------------- +#---------------------------------------------------------------------------- +from bisect import bisect + + +class TextEditMixin: + """ + A mixin class that enables any text in any column of a + multi-column listctrl to be edited by clicking on the given row + and column. You close the text editor by hitting the ENTER key or + clicking somewhere else on the listctrl. You switch to the next + column by hiting TAB. + + To use the mixin you have to include it in the class definition + and call the __init__ function:: + + class TestListCtrl(wx.ListCtrl, TextEditMixin): + def __init__(self, parent, ID, pos=wx.DefaultPosition, + size=wx.DefaultSize, style=0): + wx.ListCtrl.__init__(self, parent, ID, pos, size, style) + TextEditMixin.__init__(self) + + + Authors: Steve Zatz, Pim Van Heuven (pim@think-wize.com) + """ + + editorBgColour = wx.Colour(255,255,175) # Yellow + editorFgColour = wx.Colour(0,0,0) # black + + def __init__(self): + #editor = wx.TextCtrl(self, -1, pos=(-1,-1), size=(-1,-1), + # style=wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB \ + # |wx.TE_RICH2) + + self.make_editor() + self.Bind(wx.EVT_TEXT_ENTER, self.CloseEditor) + self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) + self.Bind(wx.EVT_LEFT_DCLICK, self.OnLeftDown) + self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnItemSelected, self) + + + def make_editor(self, col_style=wx.LIST_FORMAT_LEFT): + + style =wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB|wx.TE_RICH2 + style |= {wx.LIST_FORMAT_LEFT: wx.TE_LEFT, + wx.LIST_FORMAT_RIGHT: wx.TE_RIGHT, + wx.LIST_FORMAT_CENTRE : wx.TE_CENTRE + }[col_style] + + editor = wx.TextCtrl(self, -1, style=style) + editor.SetBackgroundColour(self.editorBgColour) + editor.SetForegroundColour(self.editorFgColour) + font = self.GetFont() + editor.SetFont(font) + + self.curRow = 0 + self.curCol = 0 + + editor.Hide() + if hasattr(self, 'editor'): + self.editor.Destroy() + self.editor = editor + + self.col_style = col_style + self.editor.Bind(wx.EVT_CHAR, self.OnChar) + self.editor.Bind(wx.EVT_KILL_FOCUS, self.CloseEditor) + + + def OnItemSelected(self, evt): + self.curRow = evt.GetIndex() + evt.Skip() + + + def OnChar(self, event): + ''' Catch the TAB, Shift-TAB, cursor DOWN/UP key code + so we can open the editor at the next column (if any).''' + + keycode = event.GetKeyCode() + if keycode == wx.WXK_TAB and event.ShiftDown(): + self.CloseEditor() + if self.curCol-1 >= 0: + self.OpenEditor(self.curCol-1, self.curRow) + + elif keycode == wx.WXK_TAB: + self.CloseEditor() + if self.curCol+1 < self.GetColumnCount(): + self.OpenEditor(self.curCol+1, self.curRow) + + elif keycode == wx.WXK_ESCAPE: + self.CloseEditor() + + elif keycode == wx.WXK_DOWN: + self.CloseEditor() + if self.curRow+1 < self.GetItemCount(): + self._SelectIndex(self.curRow+1) + self.OpenEditor(self.curCol, self.curRow) + + elif keycode == wx.WXK_UP: + self.CloseEditor() + if self.curRow > 0: + self._SelectIndex(self.curRow-1) + self.OpenEditor(self.curCol, self.curRow) + + else: + event.Skip() + + + def OnLeftDown(self, evt=None): + ''' Examine the click and double + click events to see if a row has been click on twice. If so, + determine the current row and columnn and open the editor.''' + + if self.editor.IsShown(): + self.CloseEditor() + + x,y = evt.GetPosition() + row,flags = self.HitTest((x,y)) + + if row != self.curRow: # self.curRow keeps track of the current row + evt.Skip() + return + + # the following should really be done in the mixin's init but + # the wx.ListCtrl demo creates the columns after creating the + # ListCtrl (generally not a good idea) on the other hand, + # doing this here handles adjustable column widths + + self.col_locs = [0] + loc = 0 + for n in range(self.GetColumnCount()): + loc = loc + self.GetColumnWidth(n) + self.col_locs.append(loc) + + + col = bisect(self.col_locs, x+self.GetScrollPos(wx.HORIZONTAL)) - 1 + self.OpenEditor(col, row) + + + def OpenEditor(self, col, row): + ''' Opens an editor at the current position. ''' + + # give the derived class a chance to Allow/Veto this edit. + evt = wx.ListEvent(wx.wxEVT_COMMAND_LIST_BEGIN_LABEL_EDIT, self.GetId()) + evt.m_itemIndex = row + evt.m_col = col + item = self.GetItem(row, col) + evt.m_item.SetId(item.GetId()) + evt.m_item.SetColumn(item.GetColumn()) + evt.m_item.SetData(item.GetData()) + evt.m_item.SetText(item.GetText()) + ret = self.GetEventHandler().ProcessEvent(evt) + if ret and not evt.IsAllowed(): + return # user code doesn't allow the edit. + + if self.GetColumn(col).m_format != self.col_style: + self.make_editor(self.GetColumn(col).m_format) + + x0 = self.col_locs[col] + x1 = self.col_locs[col+1] - x0 + + scrolloffset = self.GetScrollPos(wx.HORIZONTAL) + + # scroll forward + if x0+x1-scrolloffset > self.GetSize()[0]: + if wx.Platform == "__WXMSW__": + # don't start scrolling unless we really need to + offset = x0+x1-self.GetSize()[0]-scrolloffset + # scroll a bit more than what is minimum required + # so we don't have to scroll everytime the user presses TAB + # which is very tireing to the eye + addoffset = self.GetSize()[0]/4 + # but be careful at the end of the list + if addoffset + scrolloffset < self.GetSize()[0]: + offset += addoffset + + self.ScrollList(offset, 0) + scrolloffset = self.GetScrollPos(wx.HORIZONTAL) + else: + # Since we can not programmatically scroll the ListCtrl + # close the editor so the user can scroll and open the editor + # again + self.editor.SetValue(self.GetItem(row, col).GetText()) + self.curRow = row + self.curCol = col + self.CloseEditor() + return + + y0 = self.GetItemRect(row)[1] + + editor = self.editor + editor.SetDimensions(x0-scrolloffset,y0, x1,-1) + + editor.SetValue(self.GetItem(row, col).GetText()) + editor.Show() + editor.Raise() + editor.SetSelection(-1,-1) + editor.SetFocus() + + self.curRow = row + self.curCol = col + + + # FIXME: this function is usually called twice - second time because + # it is binded to wx.EVT_KILL_FOCUS. Can it be avoided? (MW) + def CloseEditor(self, evt=None): + ''' Close the editor and save the new value to the ListCtrl. ''' + if not self.editor.IsShown(): + return + text = self.editor.GetValue() + self.editor.Hide() + self.SetFocus() + + # post wxEVT_COMMAND_LIST_END_LABEL_EDIT + # Event can be vetoed. It doesn't has SetEditCanceled(), what would + # require passing extra argument to CloseEditor() + evt = wx.ListEvent(wx.wxEVT_COMMAND_LIST_END_LABEL_EDIT, self.GetId()) + evt.m_itemIndex = self.curRow + evt.m_col = self.curCol + item = self.GetItem(self.curRow, self.curCol) + evt.m_item.SetId(item.GetId()) + evt.m_item.SetColumn(item.GetColumn()) + evt.m_item.SetData(item.GetData()) + evt.m_item.SetText(text) #should be empty string if editor was canceled + ret = self.GetEventHandler().ProcessEvent(evt) + if not ret or evt.IsAllowed(): + if self.IsVirtual(): + # replace by whather you use to populate the virtual ListCtrl + # data source + self.SetVirtualData(self.curRow, self.curCol, text) + else: + self.SetStringItem(self.curRow, self.curCol, text) + self.RefreshItem(self.curRow) + + def _SelectIndex(self, row): + listlen = self.GetItemCount() + if row < 0 and not listlen: + return + if row > (listlen-1): + row = listlen -1 + + self.SetItemState(self.curRow, ~wx.LIST_STATE_SELECTED, + wx.LIST_STATE_SELECTED) + self.EnsureVisible(row) + self.SetItemState(row, wx.LIST_STATE_SELECTED, + wx.LIST_STATE_SELECTED) + + + +#---------------------------------------------------------------------------- +#---------------------------------------------------------------------------- + +""" +FILENAME: CheckListCtrlMixin.py +AUTHOR: Bruce Who (bruce.who.hk at gmail.com) +DATE: 2006-02-09 +$Revision$ +DESCRIPTION: + This script provide a mixin for ListCtrl which add a checkbox in the first + column of each row. It is inspired by limodou's CheckList.py(which can be + got from his NewEdit) and improved: + - You can just use InsertStringItem() to insert new items; + - Once a checkbox is checked/unchecked, the corresponding item is not + selected; + - You can use SetItemData() and GetItemData(); + - Interfaces are changed to OnCheckItem(), IsChecked(), CheckItem(). + + You should not set a imagelist for the ListCtrl once this mixin is used. + +HISTORY: +1.3 - You can check/uncheck a group of sequential items by : + First click(or ) item1 to check/uncheck it, then + Shift-click item2 to check/uncheck it, and you'll find that all + items between item1 and item2 are check/unchecked! +1.2 - Add ToggleItem() +1.1 - Initial version +""" + +class CheckListCtrlMixin: + """ + This is a mixin for ListCtrl which add a checkbox in the first + column of each row. It is inspired by limodou's CheckList.py(which + can be got from his NewEdit) and improved: + + - You can just use InsertStringItem() to insert new items; + + - Once a checkbox is checked/unchecked, the corresponding item + is not selected; + + - You can use SetItemData() and GetItemData(); + + - Interfaces are changed to OnCheckItem(), IsChecked(), + CheckItem(). + + You should not set a imagelist for the ListCtrl once this mixin is used. + """ + def __init__(self, check_image=None, uncheck_image=None, imgsz=(16,16)): + if check_image is not None: + imgsz = check_image.GetSize() + elif uncheck_image is not None: + imgsz = check_image.GetSize() + + self.__imagelist_ = wx.ImageList(*imgsz) + + # Create default checkbox images if none were specified + if check_image is None: + check_image = self.__CreateBitmap(wx.CONTROL_CHECKED, imgsz) + + if uncheck_image is None: + uncheck_image = self.__CreateBitmap(0, imgsz) + + self.uncheck_image = self.__imagelist_.Add(uncheck_image) + self.check_image = self.__imagelist_.Add(check_image) + self.SetImageList(self.__imagelist_, wx.IMAGE_LIST_SMALL) + self.__last_check_ = None + + self.Bind(wx.EVT_LEFT_DOWN, self.__OnLeftDown_) + + # override the default methods of ListCtrl/ListView + self.InsertStringItem = self.__InsertStringItem_ + + def __CreateBitmap(self, flag=0, size=(16, 16)): + """Create a bitmap of the platforms native checkbox. The flag + is used to determine the checkboxes state (see wx.CONTROL_*) + + """ + bmp = wx.EmptyBitmap(*size) + dc = wx.MemoryDC(bmp) + dc.Clear() + wx.RendererNative.Get().DrawCheckBox(self, dc, + (0, 0, size[0], size[1]), flag) + dc.SelectObject(wx.NullBitmap) + return bmp + + # NOTE: if you use InsertItem, InsertImageItem or InsertImageStringItem, + # you must set the image yourself. + def __InsertStringItem_(self, index, label): + index = self.InsertImageStringItem(index, label, 0) + return index + + def __OnLeftDown_(self, evt): + (index, flags) = self.HitTest(evt.GetPosition()) + if flags == wx.LIST_HITTEST_ONITEMICON: + img_idx = self.GetItem(index).GetImage() + flag_check = img_idx == 0 + begin_index = index + end_index = index + if self.__last_check_ is not None \ + and wx.GetKeyState(wx.WXK_SHIFT): + last_index, last_flag_check = self.__last_check_ + if last_flag_check == flag_check: + # XXX what if the previous item is deleted or new items + # are inserted? + item_count = self.GetItemCount() + if last_index < item_count: + if last_index < index: + begin_index = last_index + end_index = index + elif last_index > index: + begin_index = index + end_index = last_index + else: + assert False + while begin_index <= end_index: + self.CheckItem(begin_index, flag_check) + begin_index += 1 + self.__last_check_ = (index, flag_check) + else: + evt.Skip() + + def OnCheckItem(self, index, flag): + pass + + def IsChecked(self, index): + return self.GetItem(index).GetImage() == 1 + + def CheckItem(self, index, check = True): + img_idx = self.GetItem(index).GetImage() + if img_idx == 0 and check is True: + self.SetItemImage(index, 1) + self.OnCheckItem(index, True) + elif img_idx == 1 and check is False: + self.SetItemImage(index, 0) + self.OnCheckItem(index, False) + + def ToggleItem(self, index): + self.CheckItem(index, not self.IsChecked(index)) + + +#---------------------------------------------------------------------------- +#---------------------------------------------------------------------------- + +# Mode Flags +HIGHLIGHT_ODD = 1 # Highlight the Odd rows +HIGHLIGHT_EVEN = 2 # Highlight the Even rows + +class ListRowHighlighter: + """Editra Control Library: ListRowHighlighter + Mixin class that handles automatic background highlighting of alternate + rows in the a ListCtrl. The background of the rows are highlighted + automatically as items are added or inserted in the control based on the + mixins Mode and set Color. By default the Even rows will be highlighted with + the systems highlight color. + + """ + def __init__(self, color=None, mode=HIGHLIGHT_EVEN): + """Initialize the highlighter mixin + @keyword color: Set a custom highlight color (default uses system color) + @keyword mode: HIGHLIGHT_EVEN (default) or HIGHLIGHT_ODD + + """ + # Attributes + self._color = color + self._defaultb = wx.SystemSettings.GetColour(wx.SYS_COLOUR_LISTBOX) + self._mode = mode + + # Event Handlers + self.Bind(wx.EVT_LIST_INSERT_ITEM, lambda evt: self.RefreshRows()) + self.Bind(wx.EVT_LIST_DELETE_ITEM, lambda evt: self.RefreshRows()) + + def RefreshRows(self): + """Re-color all the rows""" + for row in xrange(self.GetItemCount()): + if self._defaultb is None: + self._defaultb = self.GetItemBackgroundColour(row) + + if self._mode & HIGHLIGHT_EVEN: + dohlight = not row % 2 + else: + dohlight = row % 2 + + if dohlight: + if self._color is None: + if wx.Platform in ['__WXGTK__', '__WXMSW__']: + color = wx.SystemSettings_GetColour(wx.SYS_COLOUR_3DLIGHT) + else: + color = wx.Colour(237, 243, 254) + else: + color = self._color + else: + color = self._defaultb + + self.SetItemBackgroundColour(row, color) + + def SetHighlightColor(self, color): + """Set the color used to highlight the rows. Call :meth:`RefreshRows` after + this if you wish to update all the rows highlight colors. + @param color: wx.Color or None to set default + + """ + self._color = color + + def SetHighlightMode(self, mode): + """Set the highlighting mode to either HIGHLIGHT_EVEN or to + HIGHLIGHT_ODD. Call :meth:`RefreshRows` afterwards to update the list + state. + @param mode: HIGHLIGHT_* mode value + + """ + self._mode = mode + +#---------------------------------------------------------------------------- diff --git a/wx/lib/mixins/rubberband.py b/wx/lib/mixins/rubberband.py new file mode 100644 index 00000000..454f8c93 --- /dev/null +++ b/wx/lib/mixins/rubberband.py @@ -0,0 +1,406 @@ +#--------------------------------------------------------------------------- +# Name: wxPython.lib.mixins.rubberband +# Purpose: A mixin class for doing "RubberBand"-ing on a window. +# +# Author: Robb Shecter and members of wxPython-users +# +# Created: 11-September-2002 +# RCS-ID: $Id$ +# Copyright: (c) 2002 by db-X Corporation +# Licence: wxWindows license +#--------------------------------------------------------------------------- +# 12/14/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o 2.5 compatability update. +# o Tested, but there is an anomaly between first use and subsequent uses. +# First use is odd, subsequent uses seem to be OK. Init error? +# -- No, the first time it uses an aspect ratio, but after the reset it doesn't. +# + +""" +A mixin class for doing "RubberBand"-ing on a window. +""" + +import wx + +# +# Some miscellaneous mathematical and geometrical functions +# + +def isNegative(aNumber): + """ + x < 0: 1 + else: 0 + """ + return aNumber < 0 + + +def normalizeBox(box): + """ + Convert any negative measurements in the current + box to positive, and adjust the origin. + """ + x, y, w, h = box + if w < 0: + x += (w+1) + w *= -1 + if h < 0: + y += (h+1) + h *= -1 + return (x, y, w, h) + + +def boxToExtent(box): + """ + Convert a box specification to an extent specification. + I put this into a seperate function after I realized that + I had been implementing it wrong in several places. + """ + b = normalizeBox(box) + return (b[0], b[1], b[0]+b[2]-1, b[1]+b[3]-1) + + +def pointInBox(x, y, box): + """ + Return True if the given point is contained in the box. + """ + e = boxToExtent(box) + return x >= e[0] and x <= e[2] and y >= e[1] and y <= e[3] + + +def pointOnBox(x, y, box, thickness=1): + """ + Return True if the point is on the outside edge + of the box. The thickness defines how thick the + edge should be. This is necessary for HCI reasons: + For example, it's normally very difficult for a user + to manuever the mouse onto a one pixel border. + """ + outerBox = box + innerBox = (box[0]+thickness, box[1]+thickness, box[2]-(thickness*2), box[3]-(thickness*2)) + return pointInBox(x, y, outerBox) and not pointInBox(x, y, innerBox) + + +def getCursorPosition(x, y, box, thickness=1): + """ + Return a position number in the range 0 .. 7 to indicate + where on the box border the point is. The layout is: + + 0 1 2 + 7 3 + 6 5 4 + """ + x0, y0, x1, y1 = boxToExtent(box) + w, h = box[2], box[3] + delta = thickness - 1 + p = None + + if pointInBox(x, y, (x0, y0, thickness, thickness)): + p = 0 + elif pointInBox(x, y, (x1-delta, y0, thickness, thickness)): + p = 2 + elif pointInBox(x, y, (x1-delta, y1-delta, thickness, thickness)): + p = 4 + elif pointInBox(x, y, (x0, y1-delta, thickness, thickness)): + p = 6 + elif pointInBox(x, y, (x0+thickness, y0, w-(thickness*2), thickness)): + p = 1 + elif pointInBox(x, y, (x1-delta, y0+thickness, thickness, h-(thickness*2))): + p = 3 + elif pointInBox(x, y, (x0+thickness, y1-delta, w-(thickness*2), thickness)): + p = 5 + elif pointInBox(x, y, (x0, y0+thickness, thickness, h-(thickness*2))): + p = 7 + + return p + + + + +class RubberBand: + """ + A stretchable border which is drawn on top of an + image to define an area. + """ + def __init__(self, drawingSurface, aspectRatio=None): + self.__THICKNESS = 5 + self.drawingSurface = drawingSurface + self.aspectRatio = aspectRatio + self.hasLetUp = 0 + self.currentlyMoving = None + self.currentBox = None + self.__enabled = 1 + self.__currentCursor = None + + drawingSurface.Bind(wx.EVT_MOUSE_EVENTS, self.__handleMouseEvents) + drawingSurface.Bind(wx.EVT_PAINT, self.__handleOnPaint) + + def __setEnabled(self, enabled): + self.__enabled = enabled + + def __isEnabled(self): + return self.__enabled + + def __handleOnPaint(self, event): + #print 'paint' + event.Skip() + + def __isMovingCursor(self): + """ + Return True if the current cursor is one used to + mean moving the rubberband. + """ + return self.__currentCursor == wx.CURSOR_HAND + + def __isSizingCursor(self): + """ + Return True if the current cursor is one of the ones + I may use to signify sizing. + """ + sizingCursors = [wx.CURSOR_SIZENESW, + wx.CURSOR_SIZENS, + wx.CURSOR_SIZENWSE, + wx.CURSOR_SIZEWE, + wx.CURSOR_SIZING, + wx.CURSOR_CROSS] + try: + sizingCursors.index(self.__currentCursor) + return 1 + except ValueError: + return 0 + + + def __handleMouseEvents(self, event): + """ + React according to the new event. This is the main + entry point into the class. This method contains the + logic for the class's behavior. + """ + if not self.enabled: + return + + x, y = event.GetPosition() + + # First make sure we have started a box. + if self.currentBox == None and not event.LeftDown(): + # No box started yet. Set cursor to the initial kind. + self.__setCursor(wx.CURSOR_CROSS) + return + + if event.LeftDown(): + if self.currentBox == None: + # No RB Box, so start a new one. + self.currentBox = (x, y, 0, 0) + self.hasLetUp = 0 + elif self.__isSizingCursor(): + # Starting a sizing operation. Change the origin. + position = getCursorPosition(x, y, self.currentBox, thickness=self.__THICKNESS) + self.currentBox = self.__denormalizeBox(position, self.currentBox) + + elif event.Dragging() and event.LeftIsDown(): + # Use the cursor type to determine operation + if self.__isMovingCursor(): + if self.currentlyMoving or pointInBox(x, y, self.currentBox): + if not self.currentlyMoving: + self.currentlyMoving = (x - self.currentBox[0], y - self.currentBox[1]) + self.__moveTo(x - self.currentlyMoving[0], y - self.currentlyMoving[1]) + elif self.__isSizingCursor(): + self.__resizeBox(x, y) + + elif event.LeftUp(): + self.hasLetUp = 1 + self.currentlyMoving = None + self.__normalizeBox() + + elif event.Moving() and not event.Dragging(): + # Simple mouse movement event + self.__mouseMoved(x,y) + + def __denormalizeBox(self, position, box): + x, y, w, h = box + b = box + if position == 2 or position == 3: + b = (x, y + (h-1), w, h * -1) + elif position == 0 or position == 1 or position == 7: + b = (x + (w-1), y + (h-1), w * -1, h * -1) + elif position == 6: + b = (x + (w-1), y, w * -1, h) + return b + + def __resizeBox(self, x, y): + """ + Resize and repaint the box based on the given mouse + coordinates. + """ + # Implement the correct behavior for dragging a side + # of the box: Only change one dimension. + if not self.aspectRatio: + if self.__currentCursor == wx.CURSOR_SIZENS: + x = None + elif self.__currentCursor == wx.CURSOR_SIZEWE: + y = None + + x0,y0,w0,h0 = self.currentBox + currentExtent = boxToExtent(self.currentBox) + if x == None: + if w0 < 1: + w0 += 1 + else: + w0 -= 1 + x = x0 + w0 + if y == None: + if h0 < 1: + h0 += 1 + else: + h0 -= 1 + y = y0 + h0 + x1,y1 = x, y + w, h = abs(x1-x0)+1, abs(y1-y0)+1 + if self.aspectRatio: + w = max(w, int(h * self.aspectRatio)) + h = int(w / self.aspectRatio) + w *= [1,-1][isNegative(x1-x0)] + h *= [1,-1][isNegative(y1-y0)] + newbox = (x0, y0, w, h) + self.__drawAndErase(boxToDraw=normalizeBox(newbox), boxToErase=normalizeBox(self.currentBox)) + self.currentBox = (x0, y0, w, h) + + def __normalizeBox(self): + """ + Convert any negative measurements in the current + box to positive, and adjust the origin. + """ + self.currentBox = normalizeBox(self.currentBox) + + def __mouseMoved(self, x, y): + """ + Called when the mouse moved without any buttons pressed + or dragging being done. + """ + # Are we on the bounding box? + if pointOnBox(x, y, self.currentBox, thickness=self.__THICKNESS): + position = getCursorPosition(x, y, self.currentBox, thickness=self.__THICKNESS) + cursor = [ + wx.CURSOR_SIZENWSE, + wx.CURSOR_SIZENS, + wx.CURSOR_SIZENESW, + wx.CURSOR_SIZEWE, + wx.CURSOR_SIZENWSE, + wx.CURSOR_SIZENS, + wx.CURSOR_SIZENESW, + wx.CURSOR_SIZEWE + ] [position] + self.__setCursor(cursor) + elif pointInBox(x, y, self.currentBox): + self.__setCursor(wx.CURSOR_HAND) + else: + self.__setCursor() + + def __setCursor(self, id=None): + """ + Set the mouse cursor to the given id. + """ + if self.__currentCursor != id: # Avoid redundant calls + if id: + self.drawingSurface.SetCursor(wx.StockCursor(id)) + else: + self.drawingSurface.SetCursor(wx.NullCursor) + self.__currentCursor = id + + def __moveCenterTo(self, x, y): + """ + Move the rubber band so that its center is at (x,y). + """ + x0, y0, w, h = self.currentBox + x2, y2 = x - (w/2), y - (h/2) + self.__moveTo(x2, y2) + + def __moveTo(self, x, y): + """ + Move the rubber band so that its origin is at (x,y). + """ + newbox = (x, y, self.currentBox[2], self.currentBox[3]) + self.__drawAndErase(boxToDraw=newbox, boxToErase=self.currentBox) + self.currentBox = newbox + + def __drawAndErase(self, boxToDraw, boxToErase=None): + """ + Draw one box shape and possibly erase another. + """ + dc = wx.ClientDC(self.drawingSurface) + dc.BeginDrawing() + dc.SetPen(wx.Pen(wx.WHITE, 1, wx.DOT)) + dc.SetBrush(wx.TRANSPARENT_BRUSH) + dc.SetLogicalFunction(wx.XOR) + if boxToErase: + r = wx.Rect(*boxToErase) + dc.DrawRectangleRect(r) + + r = wx.Rect(*boxToDraw) + dc.DrawRectangleRect(r) + dc.EndDrawing() + + def __dumpMouseEvent(self, event): + print 'Moving: ',event.Moving() + print 'Dragging: ',event.Dragging() + print 'LeftDown: ',event.LeftDown() + print 'LeftisDown: ',event.LeftIsDown() + print 'LeftUp: ',event.LeftUp() + print 'Position: ',event.GetPosition() + print 'x,y: ',event.GetX(),event.GetY() + print + + + # + # The public API: + # + + def reset(self, aspectRatio=None): + """ + Clear the existing rubberband + """ + self.currentBox = None + self.aspectRatio = aspectRatio + self.drawingSurface.Refresh() + + def getCurrentExtent(self): + """ + Return (x0, y0, x1, y1) or None if + no drawing has yet been done. + """ + if not self.currentBox: + extent = None + else: + extent = boxToExtent(self.currentBox) + return extent + + enabled = property(__isEnabled, __setEnabled, None, 'True if I am responding to mouse events') + + + +if __name__ == '__main__': + app = wx.PySimpleApp() + frame = wx.Frame(None, -1, title='RubberBand Test', size=(300,300)) + + # Add a panel that the rubberband will work on. + panel = wx.Panel(frame, -1) + panel.SetBackgroundColour(wx.BLUE) + + # Create the rubberband + frame.rubberBand = RubberBand(drawingSurface=panel) + frame.rubberBand.reset(aspectRatio=0.5) + + # Add a button that creates a new rubberband + def __newRubberBand(event): + frame.rubberBand.reset() + button = wx.Button(frame, 100, 'Reset Rubberband') + frame.Bind(wx.EVT_BUTTON, __newRubberBand, button) + + # Layout the frame + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(panel, 1, wx.EXPAND | wx.ALL, 5) + sizer.Add(button, 0, wx.ALIGN_CENTER | wx.ALL, 5) + frame.SetAutoLayout(1) + frame.SetSizer(sizer) + frame.Show(1) + app.MainLoop() diff --git a/wx/lib/mixins/treemixin.py b/wx/lib/mixins/treemixin.py new file mode 100644 index 00000000..e057103d --- /dev/null +++ b/wx/lib/mixins/treemixin.py @@ -0,0 +1,661 @@ +""" +treemixin.py + +This module provides three mixin classes that can be used with tree +controls: + +- VirtualTree is a class that, when mixed in with a tree control, + makes the tree control virtual, similar to a ListCtrl in virtual mode. + A virtual tree control builds the tree itself by means of callbacks, + so the programmer is freed from the burden of building the tree herself. + +- DragAndDrop is a mixin class that helps with dragging and dropping of + items. The graphical part of dragging and dropping tree items is done by + this mixin class. You only need to implement the OnDrop method that is + called when the drop happens. + +- ExpansionState is a mixin that can be queried for the expansion state of + all items in the tree to restore it later. + +All mixin classes work with wx.TreeCtrl, wx.gizmos.TreeListCtrl, +and wx.lib.customtreectrl.CustomTreeCtrl. They can be used together or +separately. + +The VirtualTree and DragAndDrop mixins force the wx.TR_HIDE_ROOT style. + +.. moduleauthor:: Frank Niessink + +License: wxWidgets license +Version: 1.1 +Date: 24 September 2007 + +ExpansionState is based on code and ideas from Karsten Hilbert. +Andrea Gavana provided help with the CustomTreeCtrl integration. +""" + + +import wx + + +class TreeAPIHarmonizer(object): + """ This class attempts to hide the differences in API between the + different tree controls that are part of wxPython. """ + + def __callSuper(self, methodName, default, *args, **kwargs): + # If our super class has a method called methodName, call it, + # otherwise return the default value. + superClass = super(TreeAPIHarmonizer, self) + if hasattr(superClass, methodName): + return getattr(superClass, methodName)(*args, **kwargs) + else: + return default + + def GetColumnCount(self, *args, **kwargs): + # Only TreeListCtrl has columns, return 0 if we are mixed in + # with another tree control. + return self.__callSuper('GetColumnCount', 0, *args, **kwargs) + + def GetItemType(self, *args, **kwargs): + # Only CustomTreeCtrl has different item types, return the + # default item type if we are mixed in with another tree control. + return self.__callSuper('GetItemType', 0, *args, **kwargs) + + def SetItemType(self, item, newType): + # CustomTreeCtrl doesn't support changing the item type on the fly, + # so we create a new item and delete the old one. We currently only + # keep the item text, would be nicer to also retain other attributes. + text = self.GetItemText(item) + newItem = self.InsertItem(self.GetItemParent(item), item, text, + ct_type=newType) + self.Delete(item) + return newItem + + def IsItemChecked(self, *args, **kwargs): + # Only CustomTreeCtrl supports checkable items, return False if + # we are mixed in with another tree control. + return self.__callSuper('IsItemChecked', False, *args, **kwargs) + + def GetItemChecked(self, *args, **kwargs): + # For consistency's sake, provide a 'Get' and 'Set' method for + # checkable items. + return self.IsItemChecked(*args, **kwargs) + + def SetItemChecked(self, *args, **kwargs): + # For consistency's sake, provide a 'Get' and 'Set' method for + # checkable items. + return self.CheckItem(*args, **kwargs) + + def GetMainWindow(self, *args, **kwargs): + # Only TreeListCtrl has a separate main window, return self if we are + # mixed in with another tree control. + return self.__callSuper('GetMainWindow', self, *args, **kwargs) + + def GetItemImage(self, item, which=wx.TreeItemIcon_Normal, column=-1): + # CustomTreeCtrl always wants the which argument, so provide it + # TreeListCtr.GetItemImage has a different order of arguments than + # the other tree controls. Hide the differenes. + if self.GetColumnCount(): + args = (item, column, which) + else: + args = (item, which) + return super(TreeAPIHarmonizer, self).GetItemImage(*args) + + def SetItemImage(self, item, imageIndex, which=wx.TreeItemIcon_Normal, + column=-1): + # The SetItemImage signature is different for TreeListCtrl and + # other tree controls. This adapter method hides the differences. + if self.GetColumnCount(): + args = (item, imageIndex, column, which) + else: + args = (item, imageIndex, which) + super(TreeAPIHarmonizer, self).SetItemImage(*args) + + def UnselectAll(self): + # Unselect all items, regardless of whether we are in multiple + # selection mode or not. + if self.HasFlag(wx.TR_MULTIPLE): + super(TreeAPIHarmonizer, self).UnselectAll() + else: + # CustomTreeCtrl Unselect() doesn't seem to work in all cases, + # also invoke UnselectAll just to be sure. + self.Unselect() + super(TreeAPIHarmonizer, self).UnselectAll() + + def GetCount(self): + # TreeListCtrl correctly ignores the root item when it is hidden, + # but doesn't count the root item when it is visible + itemCount = super(TreeAPIHarmonizer, self).GetCount() + if self.GetColumnCount() and not self.HasFlag(wx.TR_HIDE_ROOT): + itemCount += 1 + return itemCount + + def GetSelections(self): + # Always return a list of selected items, regardless of whether + # we are in multiple selection mode or not. + if self.HasFlag(wx.TR_MULTIPLE): + selections = super(TreeAPIHarmonizer, self).GetSelections() + else: + selection = self.GetSelection() + if selection: + selections = [selection] + else: + selections = [] + # If the root item is hidden, it should never be selected, + # unfortunately, CustomTreeCtrl allows it to be selected. + if self.HasFlag(wx.TR_HIDE_ROOT): + rootItem = self.GetRootItem() + if rootItem and rootItem in selections: + selections.remove(rootItem) + return selections + + def GetFirstVisibleItem(self): + # TreeListCtrl raises an exception or even crashes when invoking + # GetFirstVisibleItem on an empty tree. + if self.GetRootItem(): + return super(TreeAPIHarmonizer, self).GetFirstVisibleItem() + else: + return wx.TreeItemId() + + def SelectItem(self, item, *args, **kwargs): + # Prevent the hidden root from being selected, otherwise TreeCtrl + # crashes + if self.HasFlag(wx.TR_HIDE_ROOT) and item == self.GetRootItem(): + return + else: + return super(TreeAPIHarmonizer, self).SelectItem(item, *args, + **kwargs) + + def HitTest(self, *args, **kwargs): + """ HitTest returns a two-tuple (item, flags) for tree controls + without columns and a three-tuple (item, flags, column) for tree + controls with columns. Our caller can indicate this method to + always return a three-tuple no matter what tree control we're mixed + in with by specifying the optional argument 'alwaysReturnColumn' + to be True. """ + alwaysReturnColumn = kwargs.pop('alwaysReturnColumn', False) + hitTestResult = super(TreeAPIHarmonizer, self).HitTest(*args, **kwargs) + if len(hitTestResult) == 2 and alwaysReturnColumn: + hitTestResult += (0,) + return hitTestResult + + def ExpandAll(self, item=None): + # TreeListCtrl wants an item as argument. That's an inconsistency with + # the TreeCtrl API. Also, TreeCtrl doesn't allow invoking ExpandAll + # on a tree with hidden root node, so prevent that. + if self.HasFlag(wx.TR_HIDE_ROOT): + rootItem = self.GetRootItem() + if rootItem: + child, cookie = self.GetFirstChild(rootItem) + while child: + self.ExpandAllChildren(child) + child, cookie = self.GetNextChild(rootItem, cookie) + else: + try: + super(TreeAPIHarmonizer, self).ExpandAll() + except TypeError: + if item is None: + item = self.GetRootItem() + super(TreeAPIHarmonizer, self).ExpandAll(item) + + def ExpandAllChildren(self, item): + # TreeListCtrl and CustomTreeCtrl don't have ExpandallChildren + try: + super(TreeAPIHarmonizer, self).ExpandAllChildren(item) + except AttributeError: + self.Expand(item) + child, cookie = self.GetFirstChild(item) + while child: + self.ExpandAllChildren(child) + child, cookie = self.GetNextChild(item, cookie) + + +class TreeHelper(object): + """ This class provides methods that are not part of the API of any + tree control, but are convenient to have available. """ + + def GetItemChildren(self, item=None, recursively=False): + """ Return the children of item as a list. """ + if not item: + item = self.GetRootItem() + if not item: + return [] + children = [] + child, cookie = self.GetFirstChild(item) + while child: + children.append(child) + if recursively: + children.extend(self.GetItemChildren(child, True)) + child, cookie = self.GetNextChild(item, cookie) + return children + + def GetIndexOfItem(self, item): + """ Return the index of item. """ + parent = self.GetItemParent(item) + if parent: + parentIndices = self.GetIndexOfItem(parent) + ownIndex = self.GetItemChildren(parent).index(item) + return parentIndices + (ownIndex,) + else: + return () + + def GetItemByIndex(self, index): + """ Return the item specified by index. """ + item = self.GetRootItem() + for i in index: + children = self.GetItemChildren(item) + item = children[i] + return item + + +class VirtualTree(TreeAPIHarmonizer, TreeHelper): + """ This is a mixin class that can be used to allow for virtual tree + controls. It can be mixed in with wx.TreeCtrl, wx.gizmos.TreeListCtrl, + wx.lib.customtree.CustomTreeCtrl. + + To use it derive a new class from this class and one of the tree + controls, e.g.:: + + class MyTree(VirtualTree, wx.TreeCtrl): + # Other code here + + + VirtualTree uses several callbacks (such as OnGetItemText) to + retrieve information needed to construct the tree and render the + items. To specify what item the callback needs information about, + the callback passes an item index. Whereas for list controls a simple + integer index can be used, for tree controls indicating a specific + item is a little bit more complicated. See below for a more detailed + explanation of the how index works. + + Note that VirtualTree forces the wx.TR_HIDE_ROOT style. + + In your subclass you *must* override OnGetItemText and + OnGetChildrenCount. These two methods are the minimum needed to + construct the tree and render the item labels. If you want to add + images, change fonts our colours, etc., you need to override the + appropriate OnGetXXX method as well. + + About indices: your callbacks are passed a tuple of integers that + identifies the item the VirtualTree wants information about. An + empty tuple, i.e. (), represents the hidden root item. A tuple with + one integer, e.g. (3,), represents a visible root item, in this case + the fourth one. A tuple with two integers, e.g. (3,0), represents a + child of a visible root item, in this case the first child of the + fourth root item. + """ + + def __init__(self, *args, **kwargs): + kwargs['style'] = kwargs.get('style', wx.TR_DEFAULT_STYLE) | \ + wx.TR_HIDE_ROOT + super(VirtualTree, self).__init__(*args, **kwargs) + self.Bind(wx.EVT_TREE_ITEM_EXPANDING, self.OnItemExpanding) + self.Bind(wx.EVT_TREE_ITEM_COLLAPSED, self.OnItemCollapsed) + + def OnGetChildrenCount(self, index): + """ This function *must* be overloaded in the derived class. + It should return the number of child items of the item with the + provided index. If index == () it should return the number of + root items. """ + raise NotImplementedError + + def OnGetItemText(self, index, column=0): + """ This function *must* be overloaded in the derived class. It + should return the string containing the text of the specified + item. """ + raise NotImplementedError + + def OnGetItemFont(self, index): + """ This function may be overloaded in the derived class. It + should return the wx.Font to be used for the specified item. """ + return wx.NullFont + + def OnGetItemTextColour(self, index): + """ This function may be overloaded in the derived class. It + should return the wx.Colour to be used as text colour for the + specified item. """ + return wx.NullColour + + def OnGetItemBackgroundColour(self, index): + """ This function may be overloaded in the derived class. It + should return the wx.Colour to be used as background colour for + the specified item. """ + return wx.NullColour + + def OnGetItemImage(self, index, which=wx.TreeItemIcon_Normal, column=0): + """ This function may be overloaded in the derived class. It + should return the index of the image to be used. Don't forget + to associate an ImageList with the tree control. """ + return -1 + + def OnGetItemType(self, index): + """ This function may be overloaded in the derived class, but + that only makes sense when this class is mixed in with a tree + control that supports checkable items, i.e. CustomTreeCtrl. + This method should return whether the item is to be normal (0, + the default), a checkbox (1) or a radiobutton (2). + Note that OnGetItemChecked needs to be implemented as well; it + should return whether the item is actually checked. """ + return 0 + + def OnGetItemChecked(self, index): + """ This function may be overloaded in the derived class, but + that only makes sense when this class is mixed in with a tree + control that supports checkable items, i.e. CustomTreeCtrl. + This method should return whether the item is to be checked. + Note that OnGetItemType should return 1 (checkbox) or 2 + (radiobutton) for this item. """ + return False + + def RefreshItems(self): + """ Redraws all visible items. """ + rootItem = self.GetRootItem() + if not rootItem: + rootItem = self.AddRoot('Hidden root') + self.RefreshChildrenRecursively(rootItem) + + def RefreshItem(self, index): + """ Redraws the item with the specified index. """ + try: + item = self.GetItemByIndex(index) + except IndexError: + # There's no corresponding item for index, because its parent + # has not been expanded yet. + return + hasChildren = bool(self.OnGetChildrenCount(index)) + self.DoRefreshItem(item, index, hasChildren) + + def RefreshChildrenRecursively(self, item, itemIndex=None): + """ Refresh the children of item, reusing as much of the + existing items in the tree as possible. """ + if itemIndex is None: + itemIndex = self.GetIndexOfItem(item) + reusableChildren = self.GetItemChildren(item) + for childIndex in self.ChildIndices(itemIndex): + if reusableChildren: + child = reusableChildren.pop(0) + else: + child = self.AppendItem(item, '') + self.RefreshItemRecursively(child, childIndex) + for child in reusableChildren: + self.Delete(child) + + def RefreshItemRecursively(self, item, itemIndex): + """ Refresh the item and its children recursively. """ + hasChildren = bool(self.OnGetChildrenCount(itemIndex)) + item = self.DoRefreshItem(item, itemIndex, hasChildren) + # We need to refresh the children when the item is expanded and + # when the item has no children, because in the latter case we + # might have to delete old children from the tree: + if self.IsExpanded(item) or not hasChildren: + self.RefreshChildrenRecursively(item, itemIndex) + self.SetItemHasChildren(item, hasChildren) + + def DoRefreshItem(self, item, index, hasChildren): + """ Refresh one item. """ + item = self.RefreshItemType(item, index) + self.RefreshItemText(item, index) + self.RefreshColumns(item, index) + self.RefreshItemFont(item, index) + self.RefreshTextColour(item, index) + self.RefreshBackgroundColour(item, index) + self.RefreshItemImage(item, index, hasChildren) + self.RefreshCheckedState(item, index) + return item + + def RefreshItemText(self, item, index): + self.__refreshAttribute(item, index, 'ItemText') + + def RefreshColumns(self, item, index): + for columnIndex in range(1, self.GetColumnCount()): + self.__refreshAttribute(item, index, 'ItemText', columnIndex) + + def RefreshItemFont(self, item, index): + self.__refreshAttribute(item, index, 'ItemFont') + + def RefreshTextColour(self, item, index): + self.__refreshAttribute(item, index, 'ItemTextColour') + + def RefreshBackgroundColour(self, item, index): + self.__refreshAttribute(item, index, 'ItemBackgroundColour') + + def RefreshItemImage(self, item, index, hasChildren): + regularIcons = [wx.TreeItemIcon_Normal, wx.TreeItemIcon_Selected] + expandedIcons = [wx.TreeItemIcon_Expanded, + wx.TreeItemIcon_SelectedExpanded] + # Refresh images in first column: + for icon in regularIcons: + self.__refreshAttribute(item, index, 'ItemImage', icon) + for icon in expandedIcons: + if hasChildren: + imageIndex = self.OnGetItemImage(index, icon) + else: + imageIndex = -1 + if self.GetItemImage(item, icon) != imageIndex or imageIndex == -1: + self.SetItemImage(item, imageIndex, icon) + # Refresh images in remaining columns, if any: + for columnIndex in range(1, self.GetColumnCount()): + for icon in regularIcons: + self.__refreshAttribute(item, index, 'ItemImage', icon, + columnIndex) + + def RefreshItemType(self, item, index): + return self.__refreshAttribute(item, index, 'ItemType') + + def RefreshCheckedState(self, item, index): + self.__refreshAttribute(item, index, 'ItemChecked') + + def ChildIndices(self, itemIndex): + childrenCount = self.OnGetChildrenCount(itemIndex) + return [itemIndex + (childNumber,) for childNumber \ + in range(childrenCount)] + + def OnItemExpanding(self, event): + self.RefreshChildrenRecursively(event.GetItem()) + event.Skip() + + def OnItemCollapsed(self, event): + parent = self.GetItemParent(event.GetItem()) + if not parent: + parent = self.GetRootItem() + self.RefreshChildrenRecursively(parent) + event.Skip() + + def __refreshAttribute(self, item, index, attribute, *args): + """ Refresh the specified attribute if necessary. """ + value = getattr(self, 'OnGet%s'%attribute)(index, *args) + if getattr(self, 'Get%s'%attribute)(item, *args) != value: + return getattr(self, 'Set%s'%attribute)(item, value, *args) + else: + return item + + +class DragAndDrop(TreeAPIHarmonizer, TreeHelper): + """ This is a mixin class that can be used to easily implement + dragging and dropping of tree items. It can be mixed in with + wx.TreeCtrl, wx.gizmos.TreeListCtrl, or wx.lib.customtree.CustomTreeCtrl. + + To use it derive a new class from this class and one of the tree + controls, e.g.:: + + class MyTree(DragAndDrop, wx.TreeCtrl): + # Other code here + + + You *must* implement OnDrop. OnDrop is called when the user has + dropped an item on top of another item. It's up to you to decide how + to handle the drop. If you are using this mixin together with the + VirtualTree mixin, it makes sense to rearrange your underlying data + and then call RefreshItems to let the virtual tree refresh itself. """ + + def __init__(self, *args, **kwargs): + kwargs['style'] = kwargs.get('style', wx.TR_DEFAULT_STYLE) | \ + wx.TR_HIDE_ROOT + super(DragAndDrop, self).__init__(*args, **kwargs) + self.Bind(wx.EVT_TREE_BEGIN_DRAG, self.OnBeginDrag) + + def OnDrop(self, dropItem, dragItem): + """ This function must be overloaded in the derived class. + dragItem is the item being dragged by the user. dropItem is the + item dragItem is dropped upon. If the user doesn't drop dragItem + on another item, dropItem equals the (hidden) root item of the + tree control. """ + raise NotImplementedError + + def OnBeginDrag(self, event): + # We allow only one item to be dragged at a time, to keep it simple + self._dragItem = event.GetItem() + if self.IsValidDragItem(self._dragItem): + self.StartDragging() + event.Allow() + else: + event.Veto() + + def OnEndDrag(self, event): + self.StopDragging() + dropTarget = event.GetItem() + if not dropTarget: + dropTarget = self.GetRootItem() + if self.IsValidDropTarget(dropTarget): + self.UnselectAll() + if dropTarget != self.GetRootItem(): + self.SelectItem(dropTarget) + self.OnDrop(dropTarget, self._dragItem) + + def OnDragging(self, event): + if not event.Dragging(): + self.StopDragging() + return + item, flags, column = self.HitTest(wx.Point(event.GetX(), event.GetY()), + alwaysReturnColumn=True) + if not item: + item = self.GetRootItem() + if self.IsValidDropTarget(item): + self.SetCursorToDragging() + else: + self.SetCursorToDroppingImpossible() + if flags & wx.TREE_HITTEST_ONITEMBUTTON: + self.Expand(item) + if self.GetSelections() != [item]: + self.UnselectAll() + if item != self.GetRootItem(): + self.SelectItem(item) + event.Skip() + + def StartDragging(self): + self.GetMainWindow().Bind(wx.EVT_MOTION, self.OnDragging) + self.Bind(wx.EVT_TREE_END_DRAG, self.OnEndDrag) + self.SetCursorToDragging() + + def StopDragging(self): + self.GetMainWindow().Unbind(wx.EVT_MOTION) + self.Unbind(wx.EVT_TREE_END_DRAG) + self.ResetCursor() + self.UnselectAll() + self.SelectItem(self._dragItem) + + def SetCursorToDragging(self): + self.GetMainWindow().SetCursor(wx.StockCursor(wx.CURSOR_HAND)) + + def SetCursorToDroppingImpossible(self): + self.GetMainWindow().SetCursor(wx.StockCursor(wx.CURSOR_NO_ENTRY)) + + def ResetCursor(self): + self.GetMainWindow().SetCursor(wx.NullCursor) + + def IsValidDropTarget(self, dropTarget): + if dropTarget: + allChildren = self.GetItemChildren(self._dragItem, recursively=True) + parent = self.GetItemParent(self._dragItem) + return dropTarget not in [self._dragItem, parent] + allChildren + else: + return True + + def IsValidDragItem(self, dragItem): + return dragItem and dragItem != self.GetRootItem() + + +class ExpansionState(TreeAPIHarmonizer, TreeHelper): + """ This is a mixin class that can be used to save and restore + the expansion state (i.e. which items are expanded and which items + are collapsed) of a tree. It can be mixed in with wx.TreeCtrl, + wx.gizmos.TreeListCtrl, or wx.lib.customtree.CustomTreeCtrl. + + To use it derive a new class from this class and one of the tree + controls, e.g.:: + + class MyTree(ExpansionState, wx.TreeCtrl): + # Other code here + + + By default, ExpansionState uses the position of tree items in the tree + to keep track of which items are expanded. This should be sufficient + for the simple scenario where you save the expansion state of the tree + when the user closes the application or file so that you can restore + the expansion state when the user start the application or loads that + file for the next session. + + If you need to add or remove items between the moments of saving and + restoring the expansion state (e.g. in case of a multi-user application) + you must override GetItemIdentity so that saving and loading of the + expansion doesn't depend on the position of items in the tree, but + rather on some more stable characteristic of the underlying domain + object, e.g. a social security number in case of persons or an isbn + number in case of books. """ + + def GetItemIdentity(self, item): + """ Return a hashable object that represents the identity of the + item. By default this returns the position of the item in the + tree. You may want to override this to return the item label + (if you know that labels are unique and don't change), or return + something that represents the underlying domain object, e.g. + a database key. """ + return self.GetIndexOfItem(item) + + def GetExpansionState(self): + """ GetExpansionState() -> list of expanded items. Expanded items + are coded as determined by the result of GetItemIdentity(item). """ + root = self.GetRootItem() + if not root: + return [] + if self.HasFlag(wx.TR_HIDE_ROOT): + return self.GetExpansionStateOfChildren(root) + else: + return self.GetExpansionStateOfItem(root) + + def SetExpansionState(self, listOfExpandedItems): + """ SetExpansionState(listOfExpandedItems). Expands all tree items + whose identity, as determined by GetItemIdentity(item), is present + in the list and collapses all other tree items. """ + root = self.GetRootItem() + if not root: + return + if self.HasFlag(wx.TR_HIDE_ROOT): + self.SetExpansionStateOfChildren(listOfExpandedItems, root) + else: + self.SetExpansionStateOfItem(listOfExpandedItems, root) + + ExpansionState = property(GetExpansionState, SetExpansionState) + + def GetExpansionStateOfItem(self, item): + listOfExpandedItems = [] + if self.IsExpanded(item): + listOfExpandedItems.append(self.GetItemIdentity(item)) + listOfExpandedItems.extend(self.GetExpansionStateOfChildren(item)) + return listOfExpandedItems + + def GetExpansionStateOfChildren(self, item): + listOfExpandedItems = [] + for child in self.GetItemChildren(item): + listOfExpandedItems.extend(self.GetExpansionStateOfItem(child)) + return listOfExpandedItems + + def SetExpansionStateOfItem(self, listOfExpandedItems, item): + if self.GetItemIdentity(item) in listOfExpandedItems: + self.Expand(item) + self.SetExpansionStateOfChildren(listOfExpandedItems, item) + else: + self.Collapse(item) + + def SetExpansionStateOfChildren(self, listOfExpandedItems, item): + for child in self.GetItemChildren(item): + self.SetExpansionStateOfItem(listOfExpandedItems, child) diff --git a/wx/lib/msgpanel.py b/wx/lib/msgpanel.py new file mode 100644 index 00000000..a69b1ebd --- /dev/null +++ b/wx/lib/msgpanel.py @@ -0,0 +1,96 @@ +#---------------------------------------------------------------------- +# Name: wx.lib.msgpanel +# Purpose: The MessagePanel class (Note: this class used to live +# in the demo's Main module.) +# +# Author: Robin Dunn +# +# Created: 19-Oct-2009 +# RCS-ID: $Id: $ +# Copyright: (c) 2009 by Total Control Software +# Licence: wxWindows license +#---------------------------------------------------------------------- + +""" +MessagePanel is a simple panel class for displaying a message, very +much like how wx.MessageDialog works, including the icon flags. +""" + +import wx + +#---------------------------------------------------------------------- + +class MessagePanel(wx.Panel): + def __init__(self, parent, message, caption='', flags=0): + wx.Panel.__init__(self, parent) + + # Make widgets + icon = None + if flags: + artid = None + if flags & wx.ICON_EXCLAMATION: + artid = wx.ART_WARNING + elif flags & wx.ICON_ERROR: + artid = wx.ART_ERROR + elif flags & wx.ICON_QUESTION: + artid = wx.ART_QUESTION + elif flags & wx.ICON_INFORMATION: + artid = wx.ART_INFORMATION + + if artid is not None: + bmp = wx.ArtProvider.GetBitmap(artid, wx.ART_MESSAGE_BOX, (32,32)) + icon = wx.StaticBitmap(self, -1, bmp) + + if not icon: + icon = (32,32) # make a spacer instead + + if caption: + caption = wx.StaticText(self, -1, caption) + caption.SetFont(wx.Font(24, wx.SWISS, wx.NORMAL, wx.BOLD)) + + message = wx.StaticText(self, -1, message) + + # add to sizers for layout + tbox = wx.BoxSizer(wx.VERTICAL) + if caption: + tbox.Add(caption) + tbox.Add((10,10)) + tbox.Add(message) + + hbox = wx.BoxSizer(wx.HORIZONTAL) + hbox.Add((10,10), 1) + hbox.Add(icon) + hbox.Add((10,10)) + hbox.Add(tbox) + hbox.Add((10,10), 1) + + box = wx.BoxSizer(wx.VERTICAL) + box.Add((10,10), 1) + box.Add(hbox, 0, wx.EXPAND) + box.Add((10,10), 2) + + self.SetSizer(box) + self.Fit() + + +#---------------------------------------------------------------------- + + +if __name__ == '__main__': + app = wx.App(redirect=False) + frm = wx.Frame(None, title='MessagePanel Test') + pnl = MessagePanel(frm, flags=wx.ICON_EXCLAMATION, + caption="Please stand by...", + message="""\ +This is a test. This is a test of the emergency broadcast +system. Had this been a real emergency, you would have +already been reduced to a pile of radioactive cinders and +wondering why 'duck and cover' didn't help. + +This is only a test...""") + frm.Sizer = wx.BoxSizer() + frm.Sizer.Add(pnl, 1, wx.EXPAND) + frm.Fit() + frm.Show() + app.MainLoop() + diff --git a/wx/lib/multisash.py b/wx/lib/multisash.py new file mode 100644 index 00000000..3f6d7fc2 --- /dev/null +++ b/wx/lib/multisash.py @@ -0,0 +1,746 @@ +#---------------------------------------------------------------------- +# Name: multisash +# Purpose: Multi Sash control +# +# Author: Gerrit van Dyk +# +# Created: 2002/11/20 +# Version: 0.1 +# RCS-ID: $Id$ +# License: wxWindows license +#---------------------------------------------------------------------- +# 12/09/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o 2.5 compatability update. +# +# 12/20/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o wxMultiSash -> MultiSash +# o wxMultiSplit -> MultiSplit +# o wxMultiViewLeaf -> MultiViewLeaf +# + +import wx + +MV_HOR = 0 +MV_VER = not MV_HOR + +SH_SIZE = 5 +CR_SIZE = SH_SIZE * 3 + +#---------------------------------------------------------------------- + +class MultiSash(wx.Window): + def __init__(self, *_args,**_kwargs): + apply(wx.Window.__init__,(self,) + _args,_kwargs) + self._defChild = EmptyChild + self.child = MultiSplit(self,self,(0,0),self.GetSize()) + self.Bind(wx.EVT_SIZE,self.OnMultiSize) + + def SetDefaultChildClass(self,childCls): + self._defChild = childCls + self.child.DefaultChildChanged() + + def OnMultiSize(self,evt): + self.child.SetSize(self.GetSize()) + + def UnSelect(self): + self.child.UnSelect() + + def Clear(self): + old = self.child + self.child = MultiSplit(self,self,(0,0),self.GetSize()) + old.Destroy() + self.child.OnSize(None) + + def GetSaveData(self): + saveData = {} + saveData['_defChild_class'] = self._defChild.__name__ + saveData['_defChild_mod'] = self._defChild.__module__ + saveData['child'] = self.child.GetSaveData() + return saveData + + def SetSaveData(self,data): + mod = data['_defChild_mod'] + dChild = mod + '.' + data['_defChild_class'] + exec 'import %s' % mod + self._defChild = eval(dChild) + old = self.child + self.child = MultiSplit(self,self,wx.Point(0,0),self.GetSize()) + self.child.SetSaveData(data['child']) + old.Destroy() + self.OnMultiSize(None) + self.child.OnSize(None) + + +#---------------------------------------------------------------------- + + +class MultiSplit(wx.Window): + def __init__(self,multiView,parent,pos,size,view1 = None): + wx.Window.__init__(self,id = -1,parent = parent,pos = pos,size = size, + style = wx.CLIP_CHILDREN) + self.multiView = multiView + self.view2 = None + if view1: + self.view1 = view1 + self.view1.Reparent(self) + self.view1.MoveXY(0,0) + else: + self.view1 = MultiViewLeaf(self.multiView,self, + (0,0),self.GetSize()) + self.direction = None + + self.Bind(wx.EVT_SIZE,self.OnSize) + + def GetSaveData(self): + saveData = {} + if self.view1: + saveData['view1'] = self.view1.GetSaveData() + if isinstance(self.view1,MultiSplit): + saveData['view1IsSplit'] = 1 + if self.view2: + saveData['view2'] = self.view2.GetSaveData() + if isinstance(self.view2,MultiSplit): + saveData['view2IsSplit'] = 1 + saveData['direction'] = self.direction + v1,v2 = self.GetPosition() + saveData['x'] = v1 + saveData['y'] = v2 + v1,v2 = self.GetSize() + saveData['w'] = v1 + saveData['h'] = v2 + return saveData + + def SetSaveData(self,data): + self.direction = data['direction'] + self.SetDimensions(int(data['x']), int(data['y']), int(data['w']), int(data['h'])) + v1Data = data.get('view1',None) + if v1Data: + isSplit = data.get('view1IsSplit',None) + old = self.view1 + if isSplit: + self.view1 = MultiSplit(self.multiView,self, + (0,0),self.GetSize()) + else: + self.view1 = MultiViewLeaf(self.multiView,self, + (0,0),self.GetSize()) + self.view1.SetSaveData(v1Data) + if old: + old.Destroy() + v2Data = data.get('view2',None) + if v2Data: + isSplit = data.get('view2IsSplit',None) + old = self.view2 + if isSplit: + self.view2 = MultiSplit(self.multiView,self, + (0,0),self.GetSize()) + else: + self.view2 = MultiViewLeaf(self.multiView,self, + (0,0),self.GetSize()) + self.view2.SetSaveData(v2Data) + if old: + old.Destroy() + if self.view1: + self.view1.OnSize(None) + if self.view2: + self.view2.OnSize(None) + + def UnSelect(self): + if self.view1: + self.view1.UnSelect() + if self.view2: + self.view2.UnSelect() + + def DefaultChildChanged(self): + if not self.view2: + self.view1.DefaultChildChanged() + + def AddLeaf(self,direction,caller,pos): + if self.view2: + if caller == self.view1: + self.view1 = MultiSplit(self.multiView,self, + caller.GetPosition(), + caller.GetSize(), + caller) + self.view1.AddLeaf(direction,caller,pos) + else: + self.view2 = MultiSplit(self.multiView,self, + caller.GetPosition(), + caller.GetSize(), + caller) + self.view2.AddLeaf(direction,caller,pos) + else: + self.direction = direction + w,h = self.GetSize() + if direction == MV_HOR: + x,y = (pos,0) + w1,h1 = (w-pos,h) + w2,h2 = (pos,h) + else: + x,y = (0,pos) + w1,h1 = (w,h-pos) + w2,h2 = (w,pos) + self.view2 = MultiViewLeaf(self.multiView, self, (x,y), (w1,h1)) + self.view1.SetSize((w2,h2)) + self.view2.OnSize(None) + + def DestroyLeaf(self,caller): + if not self.view2: # We will only have 2 windows if + return # we need to destroy any + parent = self.GetParent() # Another splitview + if parent == self.multiView: # We'r at the root + if caller == self.view1: + old = self.view1 + self.view1 = self.view2 + self.view2 = None + old.Destroy() + else: + self.view2.Destroy() + self.view2 = None + self.view1.SetSize(self.GetSize()) + self.view1.Move(self.GetPosition()) + else: + w,h = self.GetSize() + x,y = self.GetPosition() + if caller == self.view1: + if self == parent.view1: + parent.view1 = self.view2 + else: + parent.view2 = self.view2 + self.view2.Reparent(parent) + self.view2.SetDimensions(x,y,w,h) + else: + if self == parent.view1: + parent.view1 = self.view1 + else: + parent.view2 = self.view1 + self.view1.Reparent(parent) + self.view1.SetDimensions(x,y,w,h) + self.view1 = None + self.view2 = None + self.Destroy() + + def CanSize(self,side,view): + if self.SizeTarget(side,view): + return True + return False + + def SizeTarget(self,side,view): + if self.direction == side and self.view2 and view == self.view1: + return self + parent = self.GetParent() + if parent != self.multiView: + return parent.SizeTarget(side,self) + return None + + def SizeLeaf(self,leaf,pos,side): + if self.direction != side: + return + if not (self.view1 and self.view2): + return + if pos < 10: return + w,h = self.GetSize() + if side == MV_HOR: + if pos > w - 10: return + else: + if pos > h - 10: return + if side == MV_HOR: + self.view1.SetDimensions(0,0,pos,h) + self.view2.SetDimensions(pos,0,w-pos,h) + else: + self.view1.SetDimensions(0,0,w,pos) + self.view2.SetDimensions(0,pos,w,h-pos) + + def OnSize(self,evt): + if not self.view2: + self.view1.SetSize(self.GetSize()) + self.view1.OnSize(None) + return + v1w,v1h = self.view1.GetSize() + v2w,v2h = self.view2.GetSize() + v1x,v1y = self.view1.GetPosition() + v2x,v2y = self.view2.GetPosition() + w,h = self.GetSize() + + if v1x != v2x: + ratio = float(w) / float((v1w + v2w)) + v1w *= ratio + v2w = w - v1w + v2x = v1w + else: + v1w = v2w = w + + if v1y != v2y: + ratio = float(h) / float((v1h + v2h)) + v1h *= ratio + v2h = h - v1h + v2y = v1h + else: + v1h = v2h = h + + self.view1.SetDimensions(int(v1x), int(v1y), int(v1w), int(v1h)) + self.view2.SetDimensions(int(v2x), int(v2y), int(v2w), int(v2h)) + self.view1.OnSize(None) + self.view2.OnSize(None) + + +#---------------------------------------------------------------------- + + +class MultiViewLeaf(wx.Window): + def __init__(self,multiView,parent,pos,size): + wx.Window.__init__(self,id = -1,parent = parent,pos = pos,size = size, + style = wx.CLIP_CHILDREN) + self.multiView = multiView + + self.sizerHor = MultiSizer(self,MV_HOR) + self.sizerVer = MultiSizer(self,MV_VER) + self.creatorHor = MultiCreator(self,MV_HOR) + self.creatorVer = MultiCreator(self,MV_VER) + self.detail = MultiClient(self,multiView._defChild) + self.closer = MultiCloser(self) + + self.Bind(wx.EVT_SIZE,self.OnSize) + + self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_3DFACE)) + + + def GetSaveData(self): + saveData = {} + saveData['detailClass_class'] = self.detail.child.__class__.__name__ + saveData['detailClass_mod'] = self.detail.child.__module__ + if hasattr(self.detail.child,'GetSaveData'): + attr = getattr(self.detail.child,'GetSaveData') + if callable(attr): + dData = attr() + if dData: + saveData['detail'] = dData + v1,v2 = self.GetPosition() + saveData['x'] = v1 + saveData['y'] = v2 + v1,v2 = self.GetSize() + saveData['w'] = v1 + saveData['h'] = v2 + return saveData + + def SetSaveData(self,data): + mod = data['detailClass_mod'] + dChild = mod + '.' + data['detailClass_class'] + exec 'import %s' % mod + detClass = eval(dChild) + self.SetDimensions(data['x'],data['y'],data['w'],data['h']) + old = self.detail + self.detail = MultiClient(self,detClass) + dData = data.get('detail',None) + if dData: + if hasattr(self.detail.child,'SetSaveData'): + attr = getattr(self.detail.child,'SetSaveData') + if callable(attr): + attr(dData) + old.Destroy() + self.detail.OnSize(None) + + def UnSelect(self): + self.detail.UnSelect() + + def DefaultChildChanged(self): + self.detail.SetNewChildCls(self.multiView._defChild) + + def AddLeaf(self,direction,pos): + if pos < 10: return + w,h = self.GetSize() + if direction == MV_VER: + if pos > h - 10: return + else: + if pos > w - 10: return + self.GetParent().AddLeaf(direction,self,pos) + + def DestroyLeaf(self): + self.GetParent().DestroyLeaf(self) + + def SizeTarget(self,side): + return self.GetParent().SizeTarget(side,self) + + def CanSize(self,side): + return self.GetParent().CanSize(side,self) + + def OnSize(self,evt): + def doresize(): + try: + self.sizerHor.OnSize(evt) + self.sizerVer.OnSize(evt) + self.creatorHor.OnSize(evt) + self.creatorVer.OnSize(evt) + self.detail.OnSize(evt) + self.closer.OnSize(evt) + except: + pass + wx.CallAfter(doresize) + +#---------------------------------------------------------------------- + + +class MultiClient(wx.Window): + def __init__(self,parent,childCls): + w,h = self.CalcSize(parent) + wx.Window.__init__(self,id = -1,parent = parent, + pos = (0,0), + size = (w,h), + style = wx.CLIP_CHILDREN | wx.SUNKEN_BORDER) + self.child = childCls(self) + self.child.MoveXY(2,2) + self.normalColour = self.GetBackgroundColour() + self.selected = False + + self.Bind(wx.EVT_SET_FOCUS,self.OnSetFocus) + self.Bind(wx.EVT_CHILD_FOCUS,self.OnChildFocus) + + def UnSelect(self): + if self.selected: + self.selected = False + self.SetBackgroundColour(self.normalColour) + self.Refresh() + + def Select(self): + self.GetParent().multiView.UnSelect() + self.selected = True + self.SetBackgroundColour(wx.Colour(255,255,0)) # Yellow + self.Refresh() + + def CalcSize(self,parent): + w,h = parent.GetSize() + w -= SH_SIZE + h -= SH_SIZE + return (w,h) + + def OnSize(self,evt): + w,h = self.CalcSize(self.GetParent()) + self.SetDimensions(0,0,w,h) + w,h = self.GetClientSize() + self.child.SetSize((w-4,h-4)) + + def SetNewChildCls(self,childCls): + if self.child: + self.child.Destroy() + self.child = None + self.child = childCls(self) + self.child.MoveXY(2,2) + + def OnSetFocus(self,evt): + self.Select() + + def OnChildFocus(self,evt): + self.OnSetFocus(evt) +## from Funcs import FindFocusedChild +## child = FindFocusedChild(self) +## child.Bind(wx.EVT_KILL_FOCUS,self.OnChildKillFocus) + + +#---------------------------------------------------------------------- + + +class MultiSizer(wx.Window): + def __init__(self,parent,side): + self.side = side + x,y,w,h = self.CalcSizePos(parent) + wx.Window.__init__(self,id = -1,parent = parent, + pos = (x,y), + size = (w,h), + style = wx.CLIP_CHILDREN) + + self.px = None # Previous X + self.py = None # Previous Y + self.isDrag = False # In Dragging + self.dragTarget = None # View being sized + + self.Bind(wx.EVT_LEAVE_WINDOW,self.OnLeave) + self.Bind(wx.EVT_ENTER_WINDOW,self.OnEnter) + self.Bind(wx.EVT_MOTION,self.OnMouseMove) + self.Bind(wx.EVT_LEFT_DOWN,self.OnPress) + self.Bind(wx.EVT_LEFT_UP,self.OnRelease) + + self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_3DFACE)) + + + def CalcSizePos(self,parent): + pw,ph = parent.GetSize() + if self.side == MV_HOR: + x = CR_SIZE + 2 + y = ph - SH_SIZE + w = pw - CR_SIZE - SH_SIZE - 2 + h = SH_SIZE + else: + x = pw - SH_SIZE + y = CR_SIZE + 2 + SH_SIZE + w = SH_SIZE + h = ph - CR_SIZE - SH_SIZE - 4 - SH_SIZE # For Closer + return (x,y,w,h) + + def OnSize(self,evt): + x,y,w,h = self.CalcSizePos(self.GetParent()) + self.SetDimensions(x,y,w,h) + + def OnLeave(self,evt): + self.SetCursor(wx.StockCursor(wx.CURSOR_ARROW)) + + def OnEnter(self,evt): + if not self.GetParent().CanSize(not self.side): + return + if self.side == MV_HOR: + self.SetCursor(wx.StockCursor(wx.CURSOR_SIZENS)) + else: + self.SetCursor(wx.StockCursor(wx.CURSOR_SIZEWE)) + + def OnMouseMove(self,evt): + if self.isDrag: + DrawSash(self.dragTarget,self.px,self.py,self.side) + self.px,self.py = self.ClientToScreenXY(evt.x, evt.y) + self.px,self.py = self.dragTarget.ScreenToClientXY(self.px,self.py) + DrawSash(self.dragTarget,self.px,self.py,self.side) + else: + evt.Skip() + + def OnPress(self,evt): + self.dragTarget = self.GetParent().SizeTarget(not self.side) + if self.dragTarget: + self.isDrag = True + self.px,self.py = self.ClientToScreenXY(evt.x, evt.y) + self.px,self.py = self.dragTarget.ScreenToClientXY(self.px,self.py) + DrawSash(self.dragTarget,self.px,self.py,self.side) + self.CaptureMouse() + else: + evt.Skip() + + def OnRelease(self,evt): + if self.isDrag: + DrawSash(self.dragTarget,self.px,self.py,self.side) + self.ReleaseMouse() + self.isDrag = False + if self.side == MV_HOR: + self.dragTarget.SizeLeaf(self.GetParent(), + self.py,not self.side) + else: + self.dragTarget.SizeLeaf(self.GetParent(), + self.px,not self.side) + self.dragTarget = None + else: + evt.Skip() + +#---------------------------------------------------------------------- + + +class MultiCreator(wx.Window): + def __init__(self,parent,side): + self.side = side + x,y,w,h = self.CalcSizePos(parent) + wx.Window.__init__(self,id = -1,parent = parent, + pos = (x,y), + size = (w,h), + style = wx.CLIP_CHILDREN) + + self.px = None # Previous X + self.py = None # Previous Y + self.isDrag = False # In Dragging + + self.Bind(wx.EVT_LEAVE_WINDOW,self.OnLeave) + self.Bind(wx.EVT_ENTER_WINDOW,self.OnEnter) + self.Bind(wx.EVT_MOTION,self.OnMouseMove) + self.Bind(wx.EVT_LEFT_DOWN,self.OnPress) + self.Bind(wx.EVT_LEFT_UP,self.OnRelease) + self.Bind(wx.EVT_PAINT,self.OnPaint) + + def CalcSizePos(self,parent): + pw,ph = parent.GetSize() + if self.side == MV_HOR: + x = 2 + y = ph - SH_SIZE + w = CR_SIZE + h = SH_SIZE + else: + x = pw - SH_SIZE + y = 4 + SH_SIZE # Make provision for closer + w = SH_SIZE + h = CR_SIZE + return (x,y,w,h) + + def OnSize(self,evt): + x,y,w,h = self.CalcSizePos(self.GetParent()) + self.SetDimensions(x,y,w,h) + + def OnLeave(self,evt): + self.SetCursor(wx.StockCursor(wx.CURSOR_ARROW)) + + def OnEnter(self,evt): + if self.side == MV_HOR: + self.SetCursor(wx.StockCursor(wx.CURSOR_HAND)) + else: + self.SetCursor(wx.StockCursor(wx.CURSOR_POINT_LEFT)) + + def OnMouseMove(self,evt): + if self.isDrag: + parent = self.GetParent() + DrawSash(parent,self.px,self.py,self.side) + self.px,self.py = self.ClientToScreenXY(evt.x, evt.y) + self.px,self.py = parent.ScreenToClientXY(self.px,self.py) + DrawSash(parent,self.px,self.py,self.side) + else: + evt.Skip() + + def OnPress(self,evt): + self.isDrag = True + parent = self.GetParent() + self.px,self.py = self.ClientToScreenXY(evt.x, evt.y) + self.px,self.py = parent.ScreenToClientXY(self.px,self.py) + DrawSash(parent,self.px,self.py,self.side) + self.CaptureMouse() + + def OnRelease(self,evt): + if self.isDrag: + parent = self.GetParent() + DrawSash(parent,self.px,self.py,self.side) + self.ReleaseMouse() + self.isDrag = False + + if self.side == MV_HOR: + parent.AddLeaf(MV_VER,self.py) + else: + parent.AddLeaf(MV_HOR,self.px) + else: + evt.Skip() + + def OnPaint(self,evt): + dc = wx.PaintDC(self) + dc.SetBackground(wx.Brush(self.GetBackgroundColour(),wx.SOLID)) + dc.Clear() + + highlight = wx.Pen(wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNHIGHLIGHT), 1, wx.SOLID) + shadow = wx.Pen(wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNSHADOW), 1, wx.SOLID) + black = wx.Pen(wx.BLACK,1,wx.SOLID) + w,h = self.GetSize() + w -= 1 + h -= 1 + + # Draw outline + dc.SetPen(highlight) + dc.DrawLine(0,0, 0,h) + dc.DrawLine(0,0, w,0) + dc.SetPen(black) + dc.DrawLine(0,h, w+1,h) + dc.DrawLine(w,0, w,h) + dc.SetPen(shadow) + dc.DrawLine(w-1,2, w-1,h) + +#---------------------------------------------------------------------- + + +class MultiCloser(wx.Window): + def __init__(self,parent): + x,y,w,h = self.CalcSizePos(parent) + wx.Window.__init__(self,id = -1,parent = parent, + pos = (x,y), + size = (w,h), + style = wx.CLIP_CHILDREN) + + self.down = False + self.entered = False + + self.Bind(wx.EVT_LEFT_DOWN,self.OnPress) + self.Bind(wx.EVT_LEFT_UP,self.OnRelease) + self.Bind(wx.EVT_PAINT,self.OnPaint) + self.Bind(wx.EVT_LEAVE_WINDOW,self.OnLeave) + self.Bind(wx.EVT_ENTER_WINDOW,self.OnEnter) + + def OnLeave(self,evt): + self.SetCursor(wx.StockCursor(wx.CURSOR_ARROW)) + self.entered = False + + def OnEnter(self,evt): + self.SetCursor(wx.StockCursor(wx.CURSOR_BULLSEYE)) + self.entered = True + + def OnPress(self,evt): + self.down = True + evt.Skip() + + def OnRelease(self,evt): + if self.down and self.entered: + self.GetParent().DestroyLeaf() + else: + evt.Skip() + self.down = False + + def OnPaint(self,evt): + dc = wx.PaintDC(self) + dc.SetBackground(wx.Brush(wx.RED,wx.SOLID)) + dc.Clear() + + def CalcSizePos(self,parent): + pw,ph = parent.GetSize() + x = pw - SH_SIZE + w = SH_SIZE + h = SH_SIZE + 2 + y = 1 + return (x,y,w,h) + + def OnSize(self,evt): + x,y,w,h = self.CalcSizePos(self.GetParent()) + self.SetDimensions(x,y,w,h) + + +#---------------------------------------------------------------------- + + +class EmptyChild(wx.Window): + def __init__(self,parent): + wx.Window.__init__(self,parent,-1, style = wx.CLIP_CHILDREN) + + +#---------------------------------------------------------------------- + + +def DrawSash(win,x,y,direction): + dc = wx.ScreenDC() + dc.StartDrawingOnTopWin(win) + bmp = wx.EmptyBitmap(8,8) + bdc = wx.MemoryDC() + bdc.SelectObject(bmp) + bdc.DrawRectangle(-1,-1, 10,10) + for i in range(8): + for j in range(8): + if ((i + j) & 1): + bdc.DrawPoint(i,j) + + brush = wx.Brush(wx.Colour(0,0,0)) + brush.SetStipple(bmp) + + dc.SetBrush(brush) + dc.SetLogicalFunction(wx.XOR) + + body_w,body_h = win.GetClientSize() + + if y < 0: + y = 0 + if y > body_h: + y = body_h + if x < 0: + x = 0 + if x > body_w: + x = body_w + + if direction == MV_HOR: + x = 0 + else: + y = 0 + + x,y = win.ClientToScreenXY(x,y) + + w = body_w + h = body_h + + if direction == MV_HOR: + dc.DrawRectangle(x,y-2, w,4) + else: + dc.DrawRectangle(x-2,y, 4,h) + + dc.EndDrawingOnTop() diff --git a/wx/lib/mvctree.py b/wx/lib/mvctree.py new file mode 100644 index 00000000..cb24e593 --- /dev/null +++ b/wx/lib/mvctree.py @@ -0,0 +1,1150 @@ +# 12/09/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o 2.5 compatability update. +# o I'm a little nervous about some of it though. +# +# 12/20/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o wxTreeModel -> TreeModel +# o wxMVCTree -> MVCTree +# o wxMVCTreeEvent -> MVCTreeEvent +# o wxMVCTreeNotifyEvent -> MVCTreeNotifyEvent +# + +""" +MVCTree is a control which handles hierarchical data. It is constructed +in model-view-controller architecture, so the display of that data, and +the content of the data can be changed greatly without affecting the other parts. + +MVCTree actually is even more configurable than MVC normally implies, because +almost every aspect of it is pluggable: + +* MVCTree - Overall controller, and the window that actually gets placed in the GUI. + + * Painter - Paints the control. The 'view' part of MVC. + + * NodePainter - Paints just the nodes + * LinePainter - Paints just the lines between the nodes + * TextConverter - Figures out what text to print for each node + + * Editor - Edits the contents of a node, if the model is editable. + * LayoutEngine - Determines initial placement of nodes + * Transform - Adjusts positions of nodes for movement or special effects. + * TreeModel - Contains the data which the rest of the control acts on. The 'model' part of MVC. + + +Author/Maintainer - Bryn Keller + + +.. note:: + + This module is *not* supported in any way. Use it however you + wish, but be warned that dealing with any consequences is + entirly up to you. + --Robin +""" + +#------------------------------------------------------------------------ +import os +import sys +import traceback +import warnings + +import wx +#------------------------------------------------------------------------ + +warningmsg = r"""\ + +################################################\ +# This module is not supported in any way! | +# | +# See cource code for wx.lib.mvctree for more | +# information. | +################################################/ + +""" + +warnings.warn(warningmsg, DeprecationWarning, stacklevel=2) +#------------------------------------------------------------------------ + +class MVCTreeNode: + """ + Used internally by MVCTree to manage its data. Contains information about + screen placement, the actual data associated with it, and more. These are + the nodes passed to all the other helper parts to do their work with. + """ + def __init__(self, data=None, parent = None, kids = None, x = 0, y = 0): + self.x = 0 + self.y = 0 + self.projx = 0 + self.projy = 0 + self.parent = parent + self.kids = kids + if self.kids is None: + self.kids = [] + self.data = data + self.expanded = False + self.selected = False + self.built = False + self.scale = 0 + + def GetChildren(self): + return self.kids + + def GetParent(self): + return self.parent + + def Remove(self, node): + try: + self.kids.remove(node) + except: + pass + def Add(self, node): + self.kids.append(node) + node.SetParent(self) + + def SetParent(self, parent): + if self.parent and not (self.parent is parent): + self.parent.Remove(self) + self.parent = parent + def __str__(self): + return "Node: " + str(self.data) + " (" + str(self.x) + ", " + str(self.y) + ")" + def __repr__(self): + return str(self.data) + def GetTreeString(self, tabs=0): + s = tabs * '\t' + str(self) + '\n' + for kid in self.kids: + s = s + kid.GetTreeString(tabs + 1) + return s + + +class Editor: + def __init__(self, tree): + self.tree = tree + def Edit(self, node): + raise NotImplementedError + def EndEdit(self, node, commit): + raise NotImplementedError + def CanEdit(self, node): + raise NotImplementedError + +class LayoutEngine: + """ + Interface for layout engines. + """ + def __init__(self, tree): + self.tree = tree + def Layout(self, node): + raise NotImplementedError + def GetNodeList(self): + raise NotImplementedError + +class Transform: + """ + Transform interface. + """ + def __init__(self, tree): + self.tree = tree + def Transform(self, node, offset, rotation): + """ + This method should only change the projx and projy attributes of + the node. These represent the position of the node as it should + be drawn on screen. Adjusting the x and y attributes can and + should cause havoc. + """ + raise NotImplementedError + + def GetSize(self): + """ + Returns the size of the entire tree as laid out and transformed + as a tuple + """ + raise NotImplementedError + +class Painter: + """ + This is the interface that MVCTree expects from painters. All painters should + be Painter subclasses. + """ + def __init__(self, tree): + self.tree = tree + self.textcolor = wx.NamedColour("BLACK") + self.bgcolor = wx.NamedColour("WHITE") + self.fgcolor = wx.NamedColour("BLUE") + self.linecolor = wx.NamedColour("GREY") + self.font = wx.Font(9, wx.DEFAULT, wx.NORMAL, wx.NORMAL, False) + self.bmp = None + + def GetFont(self): + return self.font + + def SetFont(self, font): + self.font = font + self.tree.Refresh() + def GetBuffer(self): + return self.bmp + def ClearBuffer(self): + self.bmp = None + def Paint(self, dc, node, doubleBuffered=1, paintBackground=1): + raise NotImplementedError + def GetTextColour(self): + return self.textcolor + def SetTextColour(self, color): + self.textcolor = color + self.textbrush = wx.Brush(color) + self.textpen = wx.Pen(color, 1, wx.SOLID) + def GetBackgroundColour(self): + return self.bgcolor + def SetBackgroundColour(self, color): + self.bgcolor = color + self.bgbrush = wx.Brush(color) + self.bgpen = wx.Pen(color, 1, wx.SOLID) + def GetForegroundColour(self): + return self.fgcolor + def SetForegroundColour(self, color): + self.fgcolor = color + self.fgbrush = wx.Brush(color) + self.fgpen = wx.Pen(color, 1, wx.SOLID) + def GetLineColour(self): + return self.linecolor + def SetLineColour(self, color): + self.linecolor = color + self.linebrush = wx.Brush(color) + self.linepen = wx.Pen( color, 1, wx.SOLID) + def GetForegroundPen(self): + return self.fgpen + def GetBackgroundPen(self): + return self.bgpen + def GetTextPen(self): + return self.textpen + def GetForegroundBrush(self): + return self.fgbrush + def GetBackgroundBrush(self): + return self.bgbrush + def GetTextBrush(self): + return self.textbrush + def GetLinePen(self): + return self.linepen + def GetLineBrush(self): + return self.linebrush + def OnMouse(self, evt): + if evt.LeftDClick(): + x, y = self.tree.CalcUnscrolledPosition(evt.GetX(), evt.GetY()) + for item in self.rectangles: + if item[1].Contains((x,y)): + self.tree.Edit(item[0].data) + self.tree.OnNodeClick(item[0], evt) + return + elif evt.ButtonDown(): + x, y = self.tree.CalcUnscrolledPosition(evt.GetX(), evt.GetY()) + for item in self.rectangles: + if item[1].Contains((x, y)): + self.tree.OnNodeClick(item[0], evt) + return + for item in self.knobs: + if item[1].Contains((x, y)): + self.tree.OnKnobClick(item[0]) + return + evt.Skip() + + +class TreeModel: + """ + Interface for tree models + """ + def GetRoot(self): + raise NotImplementedError + def SetRoot(self, root): + raise NotImplementedError + def GetChildCount(self, node): + raise NotImplementedError + def GetChildAt(self, node, index): + raise NotImplementedError + def GetParent(self, node): + raise NotImplementedError + def AddChild(self, parent, child): + if hasattr(self, 'tree') and self.tree: + self.tree.NodeAdded(parent, child) + def RemoveNode(self, child): + if hasattr(self, 'tree') and self.tree: + self.tree.NodeRemoved(child) + def InsertChild(self, parent, child, index): + if hasattr(self, 'tree') and self.tree: + self.tree.NodeInserted(parent, child, index) + def IsLeaf(self, node): + raise NotImplementedError + + def IsEditable(self, node): + return False + + def SetEditable(self, node): + return False + +class NodePainter: + """ + This is the interface expected of a nodepainter. + """ + def __init__(self, painter): + self.painter = painter + def Paint(self, node, dc, location = None): + """ + location should be provided only to draw in an unusual position + (not the node's normal position), otherwise the node's projected x and y + coordinates will be used. + """ + raise NotImplementedError + +class LinePainter: + """ + The linepainter interface. + """ + def __init__(self, painter): + self.painter = painter + def Paint(self, parent, child, dc): + raise NotImplementedError + +class TextConverter: + """ + TextConverter interface. + """ + def __init__(self, painter): + self.painter = painter + def Convert(node): + """ + Should return a string. The node argument will be an + MVCTreeNode. + """ + raise NotImplementedError + + +class BasicTreeModel(TreeModel): + """ + A very simple treemodel implementation, but flexible enough for many needs. + """ + def __init__(self): + self.children = {} + self.parents = {} + self.root = None + def GetRoot(self): + return self.root + def SetRoot(self, root): + self.root = root + def GetChildCount(self, node): + if self.children.has_key(node): + return len(self.children[node]) + else: + return 0 + def GetChildAt(self, node, index): + return self.children[node][index] + + def GetParent(self, node): + return self.parents[node] + + def AddChild(self, parent, child): + self.parents[child]=parent + if not self.children.has_key(parent): + self.children[parent]=[] + self.children[parent].append(child) + TreeModel.AddChild(self, parent, child) + return child + + def RemoveNode(self, node): + parent = self.parents[node] + del self.parents[node] + self.children[parent].remove(node) + TreeModel.RemoveNode(self, node) + + def InsertChild(self, parent, child, index): + self.parents[child]=parent + if not self.children.has_key(parent): + self.children[parent]=[] + self.children[parent].insert(child, index) + TreeModel.InsertChild(self, parent, child, index) + return child + + def IsLeaf(self, node): + return not self.children.has_key(node) + + def IsEditable(self, node): + return False + + def SetEditable(self, node, bool): + return False + + +class FileEditor(Editor): + def Edit(self, node): + treenode = self.tree.nodemap[node] + self.editcomp = wxTextCtrl(self.tree, -1) + for rect in self.tree.painter.rectangles: + if rect[0] == treenode: + self.editcomp.SetPosition((rect[1][0], rect[1][1])) + break + self.editcomp.SetValue(node.fileName) + self.editcomp.SetSelection(0, len(node.fileName)) + self.editcomp.SetFocus() + self.treenode = treenode +# self.editcomp.Bind(wx.EVT_KEY_DOWN, self._key) + self.editcomp.Bind(wx.EVT_KEY_UP, self._key) + self.editcomp.Bind(wx.EVT_LEFT_DOWN, self._mdown) + self.editcomp.CaptureMouse() + + def CanEdit(self, node): + return isinstance(node, FileWrapper) + + def EndEdit(self, commit): + if not self.tree._EditEnding(self.treenode.data): + return + if commit: + node = self.treenode.data + try: + os.rename(node.path + os.sep + node.fileName, node.path + os.sep + self.editcomp.GetValue()) + node.fileName = self.editcomp.GetValue() + except: + traceback.print_exc() + self.editcomp.ReleaseMouse() + self.editcomp.Destroy() + del self.editcomp + self.tree.Refresh() + + + def _key(self, evt): + if evt.GetKeyCode() == wx.WXK_RETURN: + self.EndEdit(True) + elif evt.GetKeyCode() == wx.WXK_ESCAPE: + self.EndEdit(False) + else: + evt.Skip() + + def _mdown(self, evt): + if evt.IsButton(): + x, y = evt.GetPosition() + w, h = self.editcomp.GetSize() + if x < 0 or y < 0 or x > w or y > h: + self.EndEdit(False) + + +class FileWrapper: + """ + Node class for FSTreeModel. + """ + def __init__(self, path, fileName): + self.path = path + self.fileName = fileName + + def __str__(self): + return self.fileName + +class FSTreeModel(BasicTreeModel): + """ + This treemodel models the filesystem starting from a given path. + """ + def __init__(self, path): + BasicTreeModel.__init__(self) + fw = FileWrapper(path, path.split(os.sep)[-1]) + self._Build(path, fw) + self.SetRoot(fw) + self._editable = True + def _Build(self, path, fileWrapper): + for name in os.listdir(path): + fw = FileWrapper(path, name) + self.AddChild(fileWrapper, fw) + childName = path + os.sep + name + if os.path.isdir(childName): + self._Build(childName, fw) + + def IsEditable(self, node): + return self._editable + + def SetEditable(self, node, bool): + self._editable = bool + +class LateFSTreeModel(FSTreeModel): + """ + This treemodel models the filesystem starting from a given path. + It retrieves the directory list as requested. + """ + def __init__(self, path): + BasicTreeModel.__init__(self) + name = path.split(os.sep)[-1] + pathpart = path[:-len(name)] + fw = FileWrapper(pathpart, name) + self._Build(path, fw) + self.SetRoot(fw) + self._editable = True + self.children = {} + self.parents = {} + def _Build(self, path, parent): + ppath = parent.path + os.sep + parent.fileName + if not os.path.isdir(ppath): + return + for name in os.listdir(ppath): + fw = FileWrapper(ppath, name) + self.AddChild(parent, fw) + def GetChildCount(self, node): + if self.children.has_key(node): + return FSTreeModel.GetChildCount(self, node) + else: + self._Build(node.path, node) + return FSTreeModel.GetChildCount(self, node) + + def IsLeaf(self, node): + return not os.path.isdir(node.path + os.sep + node.fileName) + +class StrTextConverter(TextConverter): + def Convert(self, node): + return str(node.data) + +class NullTransform(Transform): + def GetSize(self): + return tuple(self.size) + + def Transform(self, node, offset, rotation): + self.size = [0,0] + list = self.tree.GetLayoutEngine().GetNodeList() + for node in list: + node.projx = node.x + offset[0] + node.projy = node.y + offset[1] + if node.projx > self.size[0]: + self.size[0] = node.projx + if node.projy > self.size[1]: + self.size[1] = node.projy + +class Rect(object): + def __init__(self, x, y, width, height): + self.x = x + self.y = y + self.width = width + self.height = height + def __getitem__(self, index): + return (self.x, self.y, self.width, self.height)[index] + + def __setitem__(self, index, value): + name = ['x', 'y', 'width', 'height'][index] + setattr(self, name, value) + + def Contains(self, other): + if type(other) == type(()): + other = Rect(other[0], other[1], 0, 0) + if other.x >= self.x: + if other.y >= self.y: + if other.width + other.x <= self.width + self.x: + if other.height + other.y <= self.height + self.y: + return True + return False + + def __str__(self): + return "Rect: " + str([self.x, self.y, self.width, self.height]) + +class TreeLayout(LayoutEngine): + def SetHeight(self, num): + self.NODE_HEIGHT = num + + def __init__(self, tree): + LayoutEngine.__init__(self, tree) + self.NODE_STEP = 20 + self.NODE_HEIGHT = 20 + self.nodelist = [] + + def Layout(self, node): + self.nodelist = [] + self.NODE_HEIGHT = self.tree.GetFont().GetPointSize() * 2 + self.layoutwalk(node) + + def GetNodeList(self): + return self.nodelist + + def layoutwalk(self, node): + if node == self.tree.currentRoot: + node.level = 1 + self.lastY = (-self.NODE_HEIGHT) + node.x = self.NODE_STEP * node.level + node.y = self.lastY + self.NODE_HEIGHT + self.lastY = node.y + self.nodelist.append(node) + if node.expanded: + for kid in node.kids: + kid.level = node.level + 1 + self.layoutwalk(kid) + +class TreePainter(Painter): + """ + The default painter class. Uses double-buffering, delegates the painting of nodes and + lines to helper classes deriving from NodePainter and LinePainter. + """ + def __init__(self, tree, nodePainter = None, linePainter = None, textConverter = None): + Painter.__init__(self, tree) + if not nodePainter: + nodePainter = TreeNodePainter(self) + self.nodePainter = nodePainter + if not linePainter: + linePainter = TreeLinePainter(self) + self.linePainter = linePainter + if not textConverter: + textConverter = StrTextConverter(self) + self.textConverter = textConverter + self.charWidths = [] + + def Paint(self, dc, node, doubleBuffered=1, paintBackground=1): + if not self.charWidths: + self.charWidths = [] + for i in range(25): + self.charWidths.append(dc.GetTextExtent("D")[0] * i) + self.charHeight = dc.GetTextExtent("D")[1] + self.textpen = wx.Pen(self.GetTextColour(), 1, wx.SOLID) + self.fgpen = wx.Pen(self.GetForegroundColour(), 1, wx.SOLID) + self.bgpen = wx.Pen(self.GetBackgroundColour(), 1, wx.SOLID) + self.linepen = wx.Pen(self.GetLineColour(), 1, wx.SOLID) + self.dashpen = wx.Pen(self.GetLineColour(), 1, wx.DOT) + self.textbrush = wx.Brush(self.GetTextColour(), wx.SOLID) + self.fgbrush = wx.Brush(self.GetForegroundColour(), wx.SOLID) + self.bgbrush = wx.Brush(self.GetBackgroundColour(), wx.SOLID) + self.linebrush = wx.Pen(self.GetLineColour(), 1, wx.SOLID) + treesize = self.tree.GetSize() + size = self.tree.transform.GetSize() + size = (max(treesize.width, size[0]+50), max(treesize.height, size[1]+50)) + dc.BeginDrawing() + if doubleBuffered: + mem_dc = wx.MemoryDC() + if not self.GetBuffer(): + self.knobs = [] + self.rectangles = [] + self.bmp = wx.EmptyBitmap(size[0], size[1]) + mem_dc.SelectObject(self.GetBuffer()) + mem_dc.SetPen(self.GetBackgroundPen()) + mem_dc.SetBrush(self.GetBackgroundBrush()) + mem_dc.DrawRectangle(0, 0, size[0], size[1]) + mem_dc.SetFont(self.tree.GetFont()) + self.paintWalk(node, mem_dc) + else: + mem_dc.SelectObject(self.GetBuffer()) + xstart, ystart = self.tree.CalcUnscrolledPosition(0,0) + size = self.tree.GetClientSizeTuple() + dc.Blit(xstart, ystart, size[0], size[1], mem_dc, xstart, ystart) + else: + if node == self.tree.currentRoot: + self.knobs = [] + self.rectangles = [] + dc.SetPen(self.GetBackgroundPen()) + dc.SetBrush(self.GetBackgroundBrush()) + dc.SetFont(self.tree.GetFont()) + if paintBackground: + dc.DrawRectangle(0, 0, size[0], size[1]) + if node: + #Call with not paintBackground because if we are told not to paint the + #whole background, we have to paint in parts to undo selection coloring. + pb = paintBackground + self.paintWalk(node, dc, not pb) + dc.EndDrawing() + + def GetDashPen(self): + return self.dashpen + + def SetLinePen(self, pen): + Painter.SetLinePen(self, pen) + self.dashpen = wx.Pen(pen.GetColour(), 1, wx.DOT) + + def paintWalk(self, node, dc, paintRects=0): + self.linePainter.Paint(node.parent, node, dc) + self.nodePainter.Paint(node, dc, drawRects = paintRects) + if node.expanded: + for kid in node.kids: + if not self.paintWalk(kid, dc, paintRects): + return False + for kid in node.kids: + px = (kid.projx - self.tree.layout.NODE_STEP) + 5 + py = kid.projy + kid.height/2 + if (not self.tree.model.IsLeaf(kid.data)) or ((kid.expanded or self.tree._assumeChildren) and len(kid.kids)): + dc.SetPen(self.linepen) + dc.SetBrush(self.bgbrush) + dc.DrawRectangle(px -4, py-4, 9, 9) + self.knobs.append( (kid, Rect(px -4, py -4, 9, 9)) ) + dc.SetPen(self.textpen) + if not kid.expanded: + dc.DrawLine(px, py -2, px, py + 3) + dc.DrawLine(px -2, py, px + 3, py) + if node == self.tree.currentRoot: + px = (node.projx - self.tree.layout.NODE_STEP) + 5 + py = node.projy + node.height/2 + dc.SetPen(self.linepen) + dc.SetBrush(self.bgbrush) + dc.DrawRectangle(px -4, py-4, 9, 9) + self.knobs.append( (node, Rect(px -4, py -4, 9, 9)) ) + dc.SetPen(self.textpen) + if not node.expanded: + dc.DrawLine(px, py -2, px, py + 3) + dc.DrawLine(px -2, py, px + 3, py) + return True + + def OnMouse(self, evt): + Painter.OnMouse(self, evt) + +class TreeNodePainter(NodePainter): + def Paint(self, node, dc, location = None, drawRects = 0): + text = self.painter.textConverter.Convert(node) + extent = dc.GetTextExtent(text) + node.width = extent[0] + node.height = extent[1] + if node.selected: + dc.SetPen(self.painter.GetLinePen()) + dc.SetBrush(self.painter.GetForegroundBrush()) + dc.SetTextForeground(wx.NamedColour("WHITE")) + dc.DrawRectangle(node.projx -1, node.projy -1, node.width + 3, node.height + 3) + else: + if drawRects: + dc.SetBrush(self.painter.GetBackgroundBrush()) + dc.SetPen(self.painter.GetBackgroundPen()) + dc.DrawRectangle(node.projx -1, node.projy -1, node.width + 3, node.height + 3) + dc.SetTextForeground(self.painter.GetTextColour()) + dc.DrawText(text, node.projx, node.projy) + self.painter.rectangles.append((node, Rect(node.projx, node.projy, node.width, node.height))) + +class TreeLinePainter(LinePainter): + def Paint(self, parent, child, dc): + dc.SetPen(self.painter.GetDashPen()) + px = py = cx = cy = 0 + if parent is None or child == self.painter.tree.currentRoot: + px = (child.projx - self.painter.tree.layout.NODE_STEP) + 5 + py = child.projy + self.painter.tree.layout.NODE_HEIGHT/2 -2 + cx = child.projx + cy = py + dc.DrawLine(px, py, cx, cy) + else: + px = parent.projx + 5 + py = parent.projy + parent.height + cx = child.projx -5 + cy = child.projy + self.painter.tree.layout.NODE_HEIGHT/2 -3 + dc.DrawLine(px, py, px, cy) + dc.DrawLine(px, cy, cx, cy) + +#>> Event defs +wxEVT_MVCTREE_BEGIN_EDIT = wx.NewEventType() #Start editing. Vetoable. +wxEVT_MVCTREE_END_EDIT = wx.NewEventType() #Stop editing. Vetoable. +wxEVT_MVCTREE_DELETE_ITEM = wx.NewEventType() #Item removed from model. +wxEVT_MVCTREE_ITEM_EXPANDED = wx.NewEventType() +wxEVT_MVCTREE_ITEM_EXPANDING = wx.NewEventType() +wxEVT_MVCTREE_ITEM_COLLAPSED = wx.NewEventType() +wxEVT_MVCTREE_ITEM_COLLAPSING = wx.NewEventType() +wxEVT_MVCTREE_SEL_CHANGED = wx.NewEventType() +wxEVT_MVCTREE_SEL_CHANGING = wx.NewEventType() #Vetoable. +wxEVT_MVCTREE_KEY_DOWN = wx.NewEventType() +wxEVT_MVCTREE_ADD_ITEM = wx.NewEventType() #Item added to model. + +EVT_MVCTREE_SEL_CHANGED = wx.PyEventBinder(wxEVT_MVCTREE_SEL_CHANGED, 1) +EVT_MVCTREE_SEL_CHANGING = wx.PyEventBinder(wxEVT_MVCTREE_SEL_CHANGING, 1) +EVT_MVCTREE_ITEM_EXPANDED = wx.PyEventBinder(wxEVT_MVCTREE_ITEM_EXPANDED, 1) +EVT_MVCTREE_ITEM_EXPANDING = wx.PyEventBinder(wxEVT_MVCTREE_ITEM_EXPANDING, 1) +EVT_MVCTREE_ITEM_COLLAPSED = wx.PyEventBinder(wxEVT_MVCTREE_ITEM_COLLAPSED, 1) +EVT_MVCTREE_ITEM_COLLAPSING = wx.PyEventBinder(wxEVT_MVCTREE_ITEM_COLLAPSING, 1) +EVT_MVCTREE_ADD_ITEM = wx.PyEventBinder(wxEVT_MVCTREE_ADD_ITEM, 1) +EVT_MVCTREE_DELETE_ITEM = wx.PyEventBinder(wxEVT_MVCTREE_DELETE_ITEM, 1) +EVT_MVCTREE_KEY_DOWN = wx.PyEventBinder(wxEVT_MVCTREE_KEY_DOWN, 1) + +class MVCTreeEvent(wx.PyCommandEvent): + def __init__(self, type, id, node = None, nodes = None, keyEvent = None, **kwargs): + apply(wx.PyCommandEvent.__init__, (self, type, id), kwargs) + self.node = node + self.nodes = nodes + self.keyEvent = keyEvent + def GetNode(self): + return self.node + def GetNodes(self): + return self.nodes + def getKeyEvent(self): + return self.keyEvent + +class MVCTreeNotifyEvent(MVCTreeEvent): + def __init__(self, type, id, node = None, nodes = None, **kwargs): + apply(MVCTreeEvent.__init__, (self, type, id, node, nodes), kwargs) + self.notify = wx.NotifyEvent(type, id) + def getNotifyEvent(self): + return self.notify + +class MVCTree(wx.ScrolledWindow): + """ + The main mvc tree class. + """ + def __init__(self, parent, id, model = None, layout = None, transform = None, + painter = None, *args, **kwargs): + apply(wx.ScrolledWindow.__init__, (self, parent, id), kwargs) + self.nodemap = {} + self._multiselect = False + self._selections = [] + self._assumeChildren = False + self._scrollx = False + self._scrolly = False + self.doubleBuffered = False + self._lastPhysicalSize = self.GetSize() + self._editors = [] + if not model: + model = BasicTreeModel() + model.SetRoot("Root") + self.SetModel(model) + if not layout: + layout = TreeLayout(self) + self.layout = layout + if not transform: + transform = NullTransform(self) + self.transform = transform + if not painter: + painter = TreePainter(self) + self.painter = painter + self.SetFont(wx.Font(9, wx.DEFAULT, wx.NORMAL, wx.NORMAL, False)) + self.Bind(wx.EVT_MOUSE_EVENTS, self.OnMouse) + self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown) + self.doubleBuffered = True + self.Bind(wx.EVT_SIZE, self.OnSize) + self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground) + self.Bind(wx.EVT_PAINT, self.OnPaint) + + + def Refresh(self): + if self.doubleBuffered: + self.painter.ClearBuffer() + wx.ScrolledWindow.Refresh(self, False) + + def GetPainter(self): + return self.painter + + def GetLayoutEngine(self): + return self.layout + + def GetTransform(self): + return self.transform + + def __repr__(self): + return "" % str(hex(id(self))) + + def __str__(self): + return self.__repr__() + + def NodeAdded(self, parent, child): + e = MVCTreeEvent(wxEVT_MVCTREE_ADD_ITEM, self.GetId(), node = child, nodes = [parent, child]) + self.GetEventHandler().ProcessEvent(e) + self.painter.ClearBuffer() + + def NodeInserted(self, parent, child, index): + e = MVCTreeEvent(wxEVT_MVCTREE_ADD_ITEM, self.GetId(), node = child, nodes = [parent, child]) + self.GetEventHandler().ProcessEvent(e) + self.painter.ClearBuffer() + + def NodeRemoved(self, node): + e = MVCTreeEvent(wxEVT_MVCTREE_DELETE_ITEM, self.GetId(), node = child, nodes = [parent, child]) + self.GetEventHandler().ProcessEvent(e) + self.painter.ClearBuffer() + + def OnKeyDown(self, evt): + e = MVCTreeEvent(wxEVT_MVCTREE_KEY_DOWN, self.GetId(), keyEvent = evt) + self.GetEventHandler().ProcessEvent(e) + + def SetFont(self, font): + self.painter.SetFont(font) + dc = wx.ClientDC(self) + dc.SetFont(font) + self.layout.SetHeight(dc.GetTextExtent("")[1] + 18) + self.painter.ClearBuffer() + + def GetFont(self): + return self.painter.GetFont() + + def AddEditor(self, editor): + self._editors.append(editor) + + def RemoveEditor(self, editor): + self._editors.remove(editor) + + def OnMouse(self, evt): + self.painter.OnMouse(evt) + + def OnNodeClick(self, node, mouseEvent): + if node.selected and (self.IsMultiSelect() and mouseEvent.ControlDown()): + self.RemoveFromSelection(node.data) + else: + self.AddToSelection(node.data, mouseEvent.ControlDown(), mouseEvent.ShiftDown()) + + def OnKnobClick(self, node): + self.SetExpanded(node.data, not node.expanded) + + def GetDisplayText(self, node): + treenode = self.nodemap[node] + return self.painter.textConverter.Convert(treenode) + + def IsDoubleBuffered(self): + return self.doubleBuffered + + def SetDoubleBuffered(self, bool): + """ + By default MVCTree is double-buffered. + """ + self.doubleBuffered = bool + + def GetModel(self): + return self.model + + def SetModel(self, model): + """ + Completely change the data to be displayed. + """ + self.model = model + model.tree = self + self.laidOut = 0 + self.transformed = 0 + self._selections = [] + self.layoutRoot = MVCTreeNode() + self.layoutRoot.data = self.model.GetRoot() + self.layoutRoot.expanded = True + self.LoadChildren(self.layoutRoot) + self.currentRoot = self.layoutRoot + self.offset = [0,0] + self.rotation = 0 + self._scrollset = None + self.Refresh() + + def GetCurrentRoot(self): + return self.currentRoot + + def LoadChildren(self, layoutNode): + if layoutNode.built: + return + else: + self.nodemap[layoutNode.data]=layoutNode + for i in range(self.GetModel().GetChildCount(layoutNode.data)): + p = MVCTreeNode("RAW", layoutNode, []) + layoutNode.Add(p) + p.data = self.GetModel().GetChildAt(layoutNode.data, i) + self.nodemap[p.data]=p + layoutNode.built = True + if not self._assumeChildren: + for kid in layoutNode.kids: + self.LoadChildren(kid) + + def OnEraseBackground(self, evt): + pass + + def OnSize(self, evt): + size = self.GetSize() + self.center = (size.width/2, size.height/2) + if self._lastPhysicalSize.width < size.width or self._lastPhysicalSize.height < size.height: + self.painter.ClearBuffer() + self._lastPhysicalSize = size + + def GetSelection(self): + "Returns a tuple of selected nodes." + return tuple(self._selections) + + def SetSelection(self, nodeTuple): + if type(nodeTuple) != type(()): + nodeTuple = (nodeTuple,) + e = MVCTreeNotifyEvent(wxEVT_MVCTREE_SEL_CHANGING, self.GetId(), nodeTuple[0], nodes = nodeTuple) + self.GetEventHandler().ProcessEvent(e) + if not e.notify.IsAllowed(): + return + for node in nodeTuple: + treenode = self.nodemap[node] + treenode.selected = True + for node in self._selections: + treenode = self.nodemap[node] + node.selected = False + self._selections = list(nodeTuple) + e = MVCTreeEvent(wxEVT_MVCTREE_SEL_CHANGED, self.GetId(), nodeTuple[0], nodes = nodeTuple) + self.GetEventHandler().ProcessEvent(e) + + def IsMultiSelect(self): + return self._multiselect + + def SetMultiSelect(self, bool): + self._multiselect = bool + + def IsSelected(self, node): + return self.nodemap[node].selected + + def Edit(self, node): + if not self.model.IsEditable(node): + return + for ed in self._editors: + if ed.CanEdit(node): + e = MVCTreeNotifyEvent(wxEVT_MVCTREE_BEGIN_EDIT, self.GetId(), node) + self.GetEventHandler().ProcessEvent(e) + if not e.notify.IsAllowed(): + return + ed.Edit(node) + self._currentEditor = ed + break + + def EndEdit(self): + if self._currentEditor: + self._currentEditor.EndEdit + self._currentEditor = None + + def _EditEnding(self, node): + e = MVCTreeNotifyEvent(wxEVT_MVCTREE_END_EDIT, self.GetId(), node) + self.GetEventHandler().ProcessEvent(e) + if not e.notify.IsAllowed(): + return False + self._currentEditor = None + return True + + + def SetExpanded(self, node, bool): + treenode = self.nodemap[node] + if bool: + e = MVCTreeNotifyEvent(wxEVT_MVCTREE_ITEM_EXPANDING, self.GetId(), node) + self.GetEventHandler().ProcessEvent(e) + if not e.notify.IsAllowed(): + return + if not treenode.built: + self.LoadChildren(treenode) + else: + e = MVCTreeNotifyEvent(wxEVT_MVCTREE_ITEM_COLLAPSING, self.GetId(), node) + self.GetEventHandler().ProcessEvent(e) + if not e.notify.IsAllowed(): + return + treenode.expanded = bool + e = None + if treenode.expanded: + e = MVCTreeEvent(wxEVT_MVCTREE_ITEM_EXPANDED, self.GetId(), node) + else: + e = MVCTreeEvent(wxEVT_MVCTREE_ITEM_COLLAPSED, self.GetId(), node) + self.GetEventHandler().ProcessEvent(e) + self.layout.Layout(self.currentRoot) + self.transform.Transform(self.currentRoot, self.offset, self.rotation) + self.Refresh() + + def IsExpanded(self, node): + return self.nodemap[node].expanded + + def AddToSelection(self, nodeOrTuple, enableMulti = True, shiftMulti = False): + nodeTuple = nodeOrTuple + if type(nodeOrTuple)!= type(()): + nodeTuple = (nodeOrTuple,) + e = MVCTreeNotifyEvent(wxEVT_MVCTREE_SEL_CHANGING, self.GetId(), nodeTuple[0], nodes = nodeTuple) + self.GetEventHandler().ProcessEvent(e) + if not e.notify.IsAllowed(): + return + changeparents = [] + if not (self.IsMultiSelect() and (enableMulti or shiftMulti)): + for node in self._selections: + treenode = self.nodemap[node] + treenode.selected = False + changeparents.append(treenode) + node = nodeTuple[0] + self._selections = [node] + treenode = self.nodemap[node] + changeparents.append(treenode) + treenode.selected = True + else: + if shiftMulti: + for node in nodeTuple: + treenode = self.nodemap[node] + oldtreenode = self.nodemap[self._selections[0]] + if treenode.parent == oldtreenode.parent: + found = 0 + for kid in oldtreenode.parent.kids: + if kid == treenode or kid == oldtreenode: + found = not found + kid.selected = True + self._selections.append(kid.data) + changeparents.append(kid) + elif found: + kid.selected = True + self._selections.append(kid.data) + changeparents.append(kid) + else: + for node in nodeTuple: + try: + self._selections.index(node) + except ValueError: + self._selections.append(node) + treenode = self.nodemap[node] + treenode.selected = True + changeparents.append(treenode) + e = MVCTreeEvent(wxEVT_MVCTREE_SEL_CHANGED, self.GetId(), nodeTuple[0], nodes = nodeTuple) + self.GetEventHandler().ProcessEvent(e) + dc = wx.ClientDC(self) + self.PrepareDC(dc) + for node in changeparents: + if node: + self.painter.Paint(dc, node, doubleBuffered = 0, paintBackground = 0) + self.painter.ClearBuffer() + + def RemoveFromSelection(self, nodeTuple): + if type(nodeTuple) != type(()): + nodeTuple = (nodeTuple,) + changeparents = [] + for node in nodeTuple: + self._selections.remove(node) + treenode = self.nodemap[node] + changeparents.append(treenode) + treenode.selected = False + e = MVCTreeEvent(wxEVT_MVCTREE_SEL_CHANGED, self.GetId(), node, nodes = nodeTuple) + self.GetEventHandler().ProcessEvent(e) + dc = wx.ClientDC(self) + self.PrepareDC(dc) + for node in changeparents: + if node: + self.painter.Paint(dc, node, doubleBuffered = 0, paintBackground = 0) + self.painter.ClearBuffer() + + + def GetBackgroundColour(self): + if hasattr(self, 'painter') and self.painter: + return self.painter.GetBackgroundColour() + else: + return wx.Window.GetBackgroundColour(self) + def SetBackgroundColour(self, color): + if hasattr(self, 'painter') and self.painter: + self.painter.SetBackgroundColour(color) + else: + wx.Window.SetBackgroundColour(self, color) + def GetForegroundColour(self): + if hasattr(self, 'painter') and self.painter: + return self.painter.GetForegroundColour() + else: + return wx.Window.GetBackgroundColour(self) + def SetForegroundColour(self, color): + if hasattr(self, 'painter') and self.painter: + self.painter.SetForegroundColour(color) + else: + wx.Window.SetBackgroundColour(self, color) + + def SetAssumeChildren(self, bool): + self._assumeChildren = bool + + def GetAssumeChildren(self): + return self._assumeChildren + + def OnPaint(self, evt): + """ + Ensures that the tree has been laid out and transformed, then calls the painter + to paint the control. + """ + try: + self.EnableScrolling(False, False) + if not self.laidOut: + self.layout.Layout(self.currentRoot) + self.laidOut = True + self.transformed = False + if not self.transformed: + self.transform.Transform(self.currentRoot, self.offset, self.rotation) + self.transformed = True + tsize = None + tsize = list(self.transform.GetSize()) + tsize[0] = tsize[0] + 50 + tsize[1] = tsize[1] + 50 + w, h = self.GetSize() + if tsize[0] > w or tsize[1] > h: + if not hasattr(self, '_oldsize') or (tsize[0] > self._oldsize[0] or tsize[1] > self._oldsize[1]): + self._oldsize = tsize + oldstart = self.GetViewStart() + self._lastPhysicalSize = self.GetSize() + self.SetScrollbars(10, 10, tsize[0]/10, tsize[1]/10) + self.Scroll(oldstart[0], oldstart[1]) + dc = wx.PaintDC(self) + self.PrepareDC(dc) + dc.SetFont(self.GetFont()) + self.painter.Paint(dc, self.currentRoot, self.doubleBuffered) + except: + traceback.print_exc() + + + diff --git a/wx/lib/myole4ax.idl b/wx/lib/myole4ax.idl new file mode 100644 index 00000000..92adc063 --- /dev/null +++ b/wx/lib/myole4ax.idl @@ -0,0 +1,178 @@ +#define CALLCONV __stdcall +[ + //Regenerate this, stolen from GetObj.odl + uuid(99AB80C4-5E19-4fd5-B3CA-5EF62FC3F765), + helpstring("My Ole Guid and interface definitions"), + lcid(0x0), + version(1.0) + ] + +library myole4ax +{ + + importlib("stdole2.tlb"); + interface IOleInPlaceUIWindow; + +// typedef struct +// { +// LONG Left; +// LONG Top; +// LONG Right; +// LONG Bottom; +// }RECT; + +// typedef struct +// { +// LONG x; +// LONG y; +// }POINT; + +// typedef struct +// { +// float x; +// float y; +// }POINTF; + +// typedef struct { +// long hWnd; +// long message; +// long wParam; +// long lParam; +// long time; +// POINT pt; +// }MSG; + +// typedef [public] RECT BORDERWIDTHS; +// typedef [public] long StructPtr; + +// typedef struct +// { +// LONG cx; +// LONG cy; +// }SIZE; + + typedef struct + { + long cb; + long fMDIApp; + OLE_HANDLE hwndFrame; + OLE_HANDLE haccel; + LONG cAccelEntries; + } OLEINPLACEFRAMEINFO; + + +// [ +// uuid(00000000-0000-0000-C000-000000000046), +// odl, +// hidden +// ] +// interface IUnknownUnrestricted +// { +// long QueryInterface([in] long priid, [out,in] long* pvObj); +// long AddRef(); +// long Release(); +// }; + + + [ + uuid(00000114-0000-0000-C000-000000000046), + odl + ] + interface IOleWindow : IUnknown + { + HRESULT GetWindow([out,retval] long *phwnd); + HRESULT ContextSensitiveHelp([in] long fEnterMode); + }; + + [ + uuid(00000118-0000-0000-C000-000000000046), + odl + ] + interface IOleClientSite : IUnknown + { + }; + + [ + uuid(00000112-0000-0000-C000-000000000046), + odl + ] + interface IOleObject : IUnknown + { + HRESULT SetClientSite([in] IOleClientSite *pClientSite); + HRESULT GetClientSite([out,retval] IOleClientSite **ppClientSite); + //Lots more. + }; + + [ + uuid(B196B289-BAB4-101A-B69C-00AA00341D07), + odl + ] + interface IOleControlSite : IUnknown + { + HRESULT OnControlInfoChanged(); + HRESULT LockInPlaceActive([in] long fLock); + HRESULT GetExtendedControl([out,retval] IDispatch** ppDisp); + HRESULT TransformCoords([in] StructPtr pPtlHimetric, [in] StructPtr pPtfContainer, [in] long dwFlags); + long TranslateAccelerator([in] StructPtr lpmsg, [in] long grfModifiers); + HRESULT OnFocus([in] long fGotFocus); + HRESULT ShowPropertyFrame(); + }; + + [ + uuid(00000117-0000-0000-C000-000000000046), + odl + ] + interface IOleInPlaceActiveObject : IOleWindow + { + long TranslateAccelerator([in] long lpmsg); + long OnFrameWindowActivate([in] long fActivate); + long OnDocWindowActivate([in] long fActivate); + long ResizeBorder([in] StructPtr prcBorder, + [in] IOleInPlaceUIWindow* pUIWindow, + [in] long fFrameWindow); + long EnableModeless([in] long fEnable); + }; + + [ + uuid(00000115-0000-0000-C000-000000000046), + odl + ] + interface IOleInPlaceUIWindow : IOleWindow + { + HRESULT GetBorder([in] StructPtr lprectBorder); + HRESULT RequestBorderSpace([in] StructPtr pborderwidths); + HRESULT SetBorderSpace([in] StructPtr pborderwidths); + HRESULT SetActiveObject([in] IOleInPlaceActiveObject *pActiveObject, [in] LPWSTR pszObjName); + }; + + [ + uuid(00000116-0000-0000-C000-000000000046), + odl + ] + interface IOleInPlaceFrame : IOleInPlaceUIWindow + { + //Not done, placeholder only + }; + + [ + uuid(00000119-0000-0000-C000-000000000046), + odl + ] + interface IOleInPlaceSite : IOleWindow + { + long CanInPlaceActivate(); + HRESULT OnInPlaceActivate(); + HRESULT OnUIActivate(); + HRESULT GetWindowContext([out] IOleInPlaceFrame** ppFrame, + [out] IOleInPlaceUIWindow** ppDoc, + [in] StructPtr lprcPosRect, + [in] StructPtr lprcClipRect, + [in] StructPtr lpFrameInfo); + HRESULT Scroll([in] CURRENCY scrollExtant); + HRESULT OnUIDeactivate([in] long fUndoable); + HRESULT OnInPlaceDeactivate(); + HRESULT DiscardUndoState(); + HRESULT DeactivateAndUndo(); + HRESULT OnPosRectChange([in] long lprcPosRect); + } +} diff --git a/wx/lib/myole4ax.tlb b/wx/lib/myole4ax.tlb new file mode 100644 index 0000000000000000000000000000000000000000..7aa1b93f13bab1d06bb266405bf1a7b8e2888f10 GIT binary patch literal 8736 zcmbtZeQcY>8Gn*EA8wPzZBj@}yVnpg#bKRT(}iiEamVp3U|lrh)_D-fWXDpYM9A5w=ZKvOq?gb;#m>sUqh``vqI zzj0ccY9~FuyXT(oyL;~5^WbRjShZ9Mi`Szn*@nu&h+OBOO22Wmey8bXQQFY=1Bw9l zt5LHg0H*-!A?Og`0$>*gPXPQiA`b&T1PnmdG@!LsWD0N&&{-$)6riDABn>zN=x7jm z5;BgUrk-ZVUezc<{iff6{wi0Wcy!C-i^_JA|2g!>A=B^=gI{1@slPS`zVV*Hpc}=7p>1Fm&W(yFE}R zySY(xzpTT-xql6BS`(SR;bo02DPcBrAx$7quXKz9w+m3#F@NKf zj$Wf5XRe~7lfreOjzGYxbXMRHTu!2mqVp2%B84bqX0E&0)fR! z$5E{L2G{-&L87fFMrQCTIt(4z*=`Rs%CXgsF!sxHUZFtK6Fc)2I(!)C+A+n^(R9r^ z&VGdsuHC3nfSNi2fh9`Ewaod4SJ6SaMh)BTfoA#m8ecn5V7?#DS(^ZDjo^}kLYlQ}mODDI9;aP?;HopPnDeiVn*e3Lj;)tH z2RR3%{eQ|?$Y36#j40a20Q7kVHP;DkW1K5>gfMopc>IYK_q;Ron>X(H*DbIA+xk)y zSuak$_u!p>S^D#LMJ`Tm{mAgfuYgZn0pAL}EHmm?dRb9c$m6UygJ+L;jrXGVv{4!^!5CSNb0NgZxrLjmMibeOm=R@5X%?TULSReR&G_N1y%i zub+EyrSG>-LQco(dfuTa|GEl#-m6(wRN#5nZc`&wYKd~btE38AML2KN!9xFUl7gMc zrmc-P+v&ZLNQ8TWy4>Y)ti^NMhO^lFr@-eP?}nD$6?{CWNz;a<9o2X6^}6n^;5V`o z&|})}dJO(Nr+JCrrTLF)dai%sABK(0O^4wa`zN){9oiP+5*Xw63fnBadmRBC&wQSj zJO`LVyP=Oc=yAyY5by|ajO#AYeXM=Gco{!VV=kTnts{MQ_6X#RYdeTr1%2x<&b-ip zb~j|52alm=Jq6s9%f=vd+M4EV$UC9^CGCv%jc3cWZgZKR0nHR>kGW_`f0BMc_Xn4; z{fNu%1{waDMo2OouTKRZmIyX_+2c3pzx!m18 zl7(%c3|CjVOP?HC+{&pi`@O}P^LQ@cVbzhFtQuv-uFkWLh63Y37=!d z?5tOIy`0O(=fsp_Uiq$_E6q$fW{q+pn{=6I8)99#|za7Rp z;9zMkn`Og$i0_=(8Tux9>Cd@2BV}2OqB}j+*eD&kv4nkMPZh{b2Z&h46P6 zdXMjxSH~lngz4A&e)i7^%zA8}rDXEkzs|RJ<>YV->p4==Q|J5Pt?3G!$9Mb{J2hb$ zS=GK5?#Ut0Cbu_+Z*0C}Xr#MmB+}nK);G%cDcbA_fAZha{=0gnbUzrLS%?pt4Slum z-0nC6UGD#NcN~G{yBEJxTEd?k92)59-!VL}t*fVZWZNLxy+e^sYafkGtbIm`%UlH~UI*u)BX-E~jw(j`aHO`~lOfv&G7^b-s&_jmMZr92^sQlkbu0 zxXLX^viX$7{Kfmodgib2bc#8uE1j}3_GpSpgm)6!u_)YiiDNotmGDh+7jvGQMeX4> z#AC7Sektb~F|wLu$3<K}v-ydxEDni6Nk^j`KA0P}(|uU8hV~rgTP)VP5OymSSSEHyzvO$SGnUHp(}Dv@f4T!b~MoR=!~Dt(Sv; z!a0zQ?{{dyjJ&^TLvnl8cCe0})4(s4gRhT{XD5gA*_@TP514hs`{X*T1z-5C)vmKj z4fF0po14Qoea$td=E00DlkJGilj7FwU-hLCSE*{G#$USO9 zD#uR4W~}o>nPpo>Q6L6}w^_!!hB7(=&K`-}D1!(5*w_B;`%?*jER*o3c=<}=(&JB9 z$y5e6v}~pjSt&K0e6w9Gvk#rZT4$X=y%M!g_j#_f@7H~vd8FeR#hT|1Yo2?onY&r@ zJZ8|Ux&Ub3T%`)dKZ9&vY*to;cr#^4iA=ud;lQ7TSuEq2Wgmt9GdEq9jg?L z^RJSdCIqXDF2C%Ctu^W!avkLG&O$j~moI0TbJ@lhX0!@W4FF~I6fhS79T?;{8VmjC z1Ec|`b>I9q;P!_&=U(IZV`$v|5O*4~sXGYE42|0#H_cX3fCuE_hXwQpOH{pw^E-`l zIM@76W1FGne;B|s#n|Ui z5XToO$7bR@imvP{aSaMbU&?e`lPH}~+XH|oXPTryTR=`4D%2}uk#$lX{s+8A+ z8^<+qxi|nl&u6}W90BlcgKr3D0W6iiGJZjT$rCE?Aud>VQUvh5gKc+C^5I`rZ?}{8 z9>EyjKb-f@swndw?<9O*VxerlJ91x^b##>e`<^RzZa(F^jibAq>snP_u2%R%km;{? zbTc;?`E7{2J2&tiKsgNnzU{EhwZ!))wnMJ9wTixZu=6#&$zi1DyOU#2ZAq73<&xTx zOuk`}rVTd_V^2`%uMP;}!x+JXXbk{aC<^Xq9f0>3-U|dW_~yghGmV;Wc`S?}_jwe+ zvxNEgQvknHIA-=otI`JqN*k;(cC;xxts}l1(^U%ZB(^a~G4y_UO2v^&=NJJ2B-J#4 zZ(1fse7mAlRO;=`w0S>bn_17ubm!hWsVnl0(V;2FbX`SEqngGY)Ag>Hwjp3fen{b+ zfVIIQgYQsyT1b1mC45-v@+un`KjX{g&VpL0#ynL=>j4b_FMxNoF@O!=+aupk`4-8y gJiaCJ9h7f?ES#edpbx" % (self.__class__.__module__, self.__class__.__name__) + + def GetClassName(self): + return str(self.__class__).split(".")[-1][:-2] + + def Delete(self): + """ + Fully disconnect this shape from parents, children, the + canvas, etc. + """ + if self._parent: + self._parent.GetChildren().remove(self) + + for child in self.GetChildren(): + child.Delete() + + self.ClearText() + self.ClearRegions() + self.ClearAttachments() + + self._handlerShape = None + + if self._canvas: + self.RemoveFromCanvas(self._canvas) + + if self.GetEventHandler(): + self.GetEventHandler().OnDelete() + self._eventHandler = None + + def Draggable(self): + """TRUE if the shape may be dragged by the user.""" + return True + + def SetShape(self, sh): + self._handlerShape = sh + + def GetCanvas(self): + """Get the internal canvas.""" + return self._canvas + + def GetBranchStyle(self): + return self._branchStyle + + def GetRotation(self): + """Return the angle of rotation in radians.""" + return self._rotation + + def SetRotation(self, rotation): + self._rotation = rotation + + def SetHighlight(self, hi, recurse = False): + """Set the highlight for a shape. Shape highlighting is unimplemented.""" + self._highlighted = hi + if recurse: + for shape in self._children: + shape.SetHighlight(hi, recurse) + + def SetSensitivityFilter(self, sens = OP_ALL, recursive = False): + """Set the shape to be sensitive or insensitive to specific mouse + operations. + + sens is a bitlist of the following: + + * OP_CLICK_LEFT + * OP_CLICK_RIGHT + * OP_DRAG_LEFT + * OP_DRAG_RIGHT + * OP_ALL (equivalent to a combination of all the above). + """ + self._draggable = sens & OP_DRAG_LEFT + + self._sensitivity = sens + if recursive: + for shape in self._children: + shape.SetSensitivityFilter(sens, True) + + def SetDraggable(self, drag, recursive = False): + """Set the shape to be draggable or not draggable.""" + self._draggable = drag + if drag: + self._sensitivity |= OP_DRAG_LEFT + elif self._sensitivity & OP_DRAG_LEFT: + self._sensitivity -= OP_DRAG_LEFT + + if recursive: + for shape in self._children: + shape.SetDraggable(drag, True) + + def SetDrawHandles(self, drawH): + """Set the drawHandles flag for this shape and all descendants. + If drawH is TRUE (the default), any handles (control points) will + be drawn. Otherwise, the handles will not be drawn. + """ + self._drawHandles = drawH + for shape in self._children: + shape.SetDrawHandles(drawH) + + def SetShadowMode(self, mode, redraw = False): + """Set the shadow mode (whether a shadow is drawn or not). + mode can be one of the following: + + SHADOW_NONE + No shadow (the default). + SHADOW_LEFT + Shadow on the left side. + SHADOW_RIGHT + Shadow on the right side. + """ + if redraw and self.GetCanvas(): + dc = wx.ClientDC(self.GetCanvas()) + self.GetCanvas().PrepareDC(dc) + self.Erase(dc) + self._shadowMode = mode + self.Draw(dc) + else: + self._shadowMode = mode + + def GetShadowMode(self): + """Return the current shadow mode setting""" + return self._shadowMode + + def SetCanvas(self, theCanvas): + """Identical to Shape.Attach.""" + self._canvas = theCanvas + for shape in self._children: + shape.SetCanvas(theCanvas) + + def AddToCanvas(self, theCanvas, addAfter = None): + """Add the shape to the canvas's shape list. + If addAfter is non-NULL, will add the shape after this one. + """ + theCanvas.AddShape(self, addAfter) + + lastImage = self + for object in self._children: + object.AddToCanvas(theCanvas, lastImage) + lastImage = object + + def InsertInCanvas(self, theCanvas): + """Insert the shape at the front of the shape list of canvas.""" + theCanvas.InsertShape(self) + + lastImage = self + for object in self._children: + object.AddToCanvas(theCanvas, lastImage) + lastImage = object + + def RemoveFromCanvas(self, theCanvas): + """Remove the shape from the canvas.""" + if self.Selected(): + self.Select(False) + + self._canvas = None + theCanvas.RemoveShape(self) + for object in self._children: + object.RemoveFromCanvas(theCanvas) + + def ClearAttachments(self): + """Clear internal custom attachment point shapes (of class + wxAttachmentPoint). + """ + self._attachmentPoints = [] + + def ClearText(self, regionId = 0): + """Clear the text from the specified text region.""" + if regionId == 0: + self._text = "" + if regionId < len(self._regions): + self._regions[regionId].ClearText() + + def ClearRegions(self): + """Clear the ShapeRegions from the shape.""" + self._regions = [] + + def AddRegion(self, region): + """Add a region to the shape.""" + self._regions.append(region) + + def SetDefaultRegionSize(self): + """Set the default region to be consistent with the shape size.""" + if not self._regions: + return + w, h = self.GetBoundingBoxMax() + self._regions[0].SetSize(w, h) + + def HitTest(self, x, y): + """Given a point on a canvas, returns TRUE if the point was on the + shape, and returns the nearest attachment point and distance from + the given point and target. + """ + width, height = self.GetBoundingBoxMax() + if abs(width) < 4: + width = 4.0 + if abs(height) < 4: + height = 4.0 + + width += 4 # Allowance for inaccurate mousing + height += 4 + + left = self._xpos - width / 2.0 + top = self._ypos - height / 2.0 + right = self._xpos + width / 2.0 + bottom = self._ypos + height / 2.0 + + nearest_attachment = 0 + + # If within the bounding box, check the attachment points + # within the object. + if x >= left and x <= right and y >= top and y <= bottom: + n = self.GetNumberOfAttachments() + nearest = 999999 + + # GetAttachmentPosition[Edge] takes a logical attachment position, + # i.e. if it's rotated through 90%, position 0 is East-facing. + + for i in range(n): + e = self.GetAttachmentPositionEdge(i) + if e: + xp, yp = e + l = math.sqrt(((xp - x) * (xp - x)) + (yp - y) * (yp - y)) + if l < nearest: + nearest = l + nearest_attachment = i + + return nearest_attachment, nearest + return False + + # Format a text string according to the region size, adding + # strings with positions to region text list + + def FormatText(self, dc, s, i = 0): + """Reformat the given text region; defaults to formatting the + default region. + """ + self.ClearText(i) + + if not self._regions: + return + + if i >= len(self._regions): + return + + region = self._regions[i] + region._regionText = s + dc.SetFont(region.GetFont()) + + w, h = region.GetSize() + + stringList = FormatText(dc, s, (w - 2 * self._textMarginX), (h - 2 * self._textMarginY), region.GetFormatMode()) + for s in stringList: + line = ShapeTextLine(0.0, 0.0, s) + region.GetFormattedText().append(line) + + actualW = w + actualH = h + # Don't try to resize an object with more than one image (this + # case should be dealt with by overriden handlers) + if (region.GetFormatMode() & FORMAT_SIZE_TO_CONTENTS) and \ + len(region.GetFormattedText()) and \ + len(self._regions) == 1 and \ + not Shape.GraphicsInSizeToContents: + + actualW, actualH = GetCentredTextExtent(dc, region.GetFormattedText()) + if actualW + 2 * self._textMarginX != w or actualH + 2 * self._textMarginY != h: + # If we are a descendant of a composite, must make sure + # the composite gets resized properly + + topAncestor = self.GetTopAncestor() + if topAncestor != self: + Shape.GraphicsInSizeToContents = True + + composite = topAncestor + composite.Erase(dc) + self.SetSize(actualW + 2 * self._textMarginX, actualH + 2 * self._textMarginY) + self.Move(dc, self._xpos, self._ypos) + composite.CalculateSize() + if composite.Selected(): + composite.DeleteControlPoints(dc) + composite.MakeControlPoints() + composite.MakeMandatoryControlPoints() + # Where infinite recursion might happen if we didn't stop it + composite.Draw(dc) + Shape.GraphicsInSizeToContents = False + else: + self.Erase(dc) + + self.SetSize(actualW + 2 * self._textMarginX, actualH + 2 * self._textMarginY) + self.Move(dc, self._xpos, self._ypos) + self.EraseContents(dc) + CentreText(dc, region.GetFormattedText(), self._xpos, self._ypos, actualW - 2 * self._textMarginX, actualH - 2 * self._textMarginY, region.GetFormatMode()) + self._formatted = True + + def Recentre(self, dc): + """Do recentring (or other formatting) for all the text regions + for this shape. + """ + w, h = self.GetBoundingBoxMin() + for region in self._regions: + CentreText(dc, region.GetFormattedText(), self._xpos, self._ypos, w - 2 * self._textMarginX, h - 2 * self._textMarginY, region.GetFormatMode()) + + def GetPerimeterPoint(self, x1, y1, x2, y2): + """Get the point at which the line from (x1, y1) to (x2, y2) hits + the shape. Returns False if the line doesn't hit the perimeter. + """ + return False + + def SetPen(self, the_pen): + """Set the pen for drawing the shape's outline.""" + self._pen = the_pen + + def SetBrush(self, the_brush): + """Set the brush for filling the shape's shape.""" + self._brush = the_brush + + # Get the top - most (non-division) ancestor, or self + def GetTopAncestor(self): + """Return the top-most ancestor of this shape (the root of + the composite). + """ + if not self.GetParent(): + return self + + if isinstance(self.GetParent(), DivisionShape): + return self + return self.GetParent().GetTopAncestor() + + # Region functions + def SetFont(self, the_font, regionId = 0): + """Set the font for the specified text region.""" + self._font = the_font + if regionId < len(self._regions): + self._regions[regionId].SetFont(the_font) + + def GetFont(self, regionId = 0): + """Get the font for the specified text region.""" + if regionId >= len(self._regions): + return None + return self._regions[regionId].GetFont() + + def SetFormatMode(self, mode, regionId = 0): + """Set the format mode of the default text region. The argument + can be a bit list of the following: + + FORMAT_NONE + No formatting. + FORMAT_CENTRE_HORIZ + Horizontal centring. + FORMAT_CENTRE_VERT + Vertical centring. + """ + if regionId < len(self._regions): + self._regions[regionId].SetFormatMode(mode) + + def GetFormatMode(self, regionId = 0): + if regionId >= len(self._regions): + return 0 + return self._regions[regionId].GetFormatMode() + + def SetTextColour(self, the_colour, regionId = 0): + """Set the colour for the specified text region.""" + self._textColour = wx.TheColourDatabase.Find(the_colour) + self._textColourName = the_colour + + if regionId < len(self._regions): + self._regions[regionId].SetColour(the_colour) + + def GetTextColour(self, regionId = 0): + """Get the colour for the specified text region.""" + if regionId >= len(self._regions): + return "" + return self._regions[regionId].GetColour() + + def SetRegionName(self, name, regionId = 0): + """Set the name for this region. + The name for a region is unique within the scope of the whole + composite, whereas a region id is unique only for a single image. + """ + if regionId < len(self._regions): + self._regions[regionId].SetName(name) + + def GetRegionName(self, regionId = 0): + """Get the region's name. + A region's name can be used to uniquely determine a region within + an entire composite image hierarchy. See also Shape.SetRegionName. + """ + if regionId >= len(self._regions): + return "" + return self._regions[regionId].GetName() + + def GetRegionId(self, name): + """Get the region's identifier by name. + This is not unique for within an entire composite, but is unique + for the image. + """ + for i, r in enumerate(self._regions): + if r.GetName() == name: + return i + return -1 + + # Name all _regions in all subimages recursively + def NameRegions(self, parentName=""): + """Make unique names for all the regions in a shape or composite shape.""" + n = self.GetNumberOfTextRegions() + for i in range(n): + if parentName: + buff = parentName+"."+str(i) + else: + buff = str(i) + self.SetRegionName(buff, i) + + for j, child in enumerate(self._children): + if parentName: + buff = parentName+"."+str(j) + else: + buff = str(j) + child.NameRegions(buff) + + # Get a region by name, possibly looking recursively into composites + def FindRegion(self, name): + """Find the actual image ('this' if non-composite) and region id + for the given region name. + """ + id = self.GetRegionId(name) + if id > -1: + return self, id + + for child in self._children: + actualImage, regionId = child.FindRegion(name) + if actualImage: + return actualImage, regionId + + return None, -1 + + # Finds all region names for this image (composite or simple). + def FindRegionNames(self): + """Get a list of all region names for this image (composite or simple).""" + list = [] + n = self.GetNumberOfTextRegions() + for i in range(n): + list.append(self.GetRegionName(i)) + + for child in self._children: + list += child.FindRegionNames() + + return list + + def AssignNewIds(self): + """Assign new ids to this image and its children.""" + self._id = wx.NewId() + for child in self._children: + child.AssignNewIds() + + def OnDraw(self, dc): + pass + + def OnMoveLinks(self, dc): + # Want to set the ends of all attached links + # to point to / from this object + + for line in self._lines: + line.GetEventHandler().OnMoveLink(dc) + + def OnDrawContents(self, dc): + if not self._regions: + return + + bound_x, bound_y = self.GetBoundingBoxMin() + + if self._pen: + dc.SetPen(self._pen) + + for region in self._regions: + if region.GetFont(): + dc.SetFont(region.GetFont()) + + dc.SetTextForeground(region.GetActualColourObject()) + dc.SetBackgroundMode(wx.TRANSPARENT) + if not self._formatted: + CentreText(dc, region.GetFormattedText(), self._xpos, self._ypos, bound_x - 2 * self._textMarginX, bound_y - 2 * self._textMarginY, region.GetFormatMode()) + self._formatted = True + + if not self.GetDisableLabel(): + DrawFormattedText(dc, region.GetFormattedText(), self._xpos, self._ypos, bound_x - 2 * self._textMarginX, bound_y - 2 * self._textMarginY, region.GetFormatMode()) + + + def DrawContents(self, dc): + """Draw the internal graphic of the shape (such as text). + + Do not override this function: override OnDrawContents, which + is called by this function. + """ + self.GetEventHandler().OnDrawContents(dc) + + def OnSize(self, x, y): + pass + + def OnMovePre(self, dc, x, y, old_x, old_y, display = True): + return True + + def OnErase(self, dc): + if not self._visible: + return + + # Erase links + for line in self._lines: + line.GetEventHandler().OnErase(dc) + + self.GetEventHandler().OnEraseContents(dc) + + def OnEraseContents(self, dc): + if not self._visible: + return + + xp, yp = self.GetX(), self.GetY() + minX, minY = self.GetBoundingBoxMin() + maxX, maxY = self.GetBoundingBoxMax() + + topLeftX = xp - maxX / 2.0 - 2 + topLeftY = yp - maxY / 2.0 - 2 + + penWidth = 0 + if self._pen: + penWidth = self._pen.GetWidth() + + dc.SetPen(self.GetBackgroundPen()) + dc.SetBrush(self.GetBackgroundBrush()) + + dc.DrawRectangle(topLeftX - penWidth, topLeftY - penWidth, maxX + penWidth * 2 + 4, maxY + penWidth * 2 + 4) + + def EraseLinks(self, dc, attachment = -1, recurse = False): + """Erase links attached to this shape, but do not repair damage + caused to other shapes. + """ + if not self._visible: + return + + for line in self._lines: + if attachment == -1 or (line.GetTo() == self and line.GetAttachmentTo() == attachment or line.GetFrom() == self and line.GetAttachmentFrom() == attachment): + line.GetEventHandler().OnErase(dc) + + if recurse: + for child in self._children: + child.EraseLinks(dc, attachment, recurse) + + def DrawLinks(self, dc, attachment = -1, recurse = False): + """Draws any lines linked to this shape.""" + if not self._visible: + return + + for line in self._lines: + if attachment == -1 or (line.GetTo() == self and line.GetAttachmentTo() == attachment or line.GetFrom() == self and line.GetAttachmentFrom() == attachment): + line.Draw(dc) + + if recurse: + for child in self._children: + child.DrawLinks(dc, attachment, recurse) + + # Returns TRUE if pt1 <= pt2 in the sense that one point comes before + # another on an edge of the shape. + # attachmentPoint is the attachment point (= side) in question. + + # This is the default, rectangular implementation. + def AttachmentSortTest(self, attachmentPoint, pt1, pt2): + """Return TRUE if pt1 is less than or equal to pt2, in the sense + that one point comes before another on an edge of the shape. + + attachment is the attachment point (side) in question. + + This function is used in Shape.MoveLineToNewAttachment to determine + the new line ordering. + """ + physicalAttachment = self.LogicalToPhysicalAttachment(attachmentPoint) + if physicalAttachment in [0, 2]: + return pt1[0] <= pt2[0] + elif physicalAttachment in [1, 3]: + return pt1[1] <= pt2[1] + + return False + + def MoveLineToNewAttachment(self, dc, to_move, x, y): + """Move the given line (which must already be attached to the shape) + to a different attachment point on the shape, or a different order + on the same attachment. + + Calls Shape.AttachmentSortTest and then + ShapeEvtHandler.OnChangeAttachment. + """ + if self.GetAttachmentMode() == ATTACHMENT_MODE_NONE: + return False + + # Is (x, y) on this object? If so, find the new attachment point + # the user has moved the point to + hit = self.HitTest(x, y) + if not hit: + return False + + newAttachment, distance = hit + + self.EraseLinks(dc) + + if to_move.GetTo() == self: + oldAttachment = to_move.GetAttachmentTo() + else: + oldAttachment = to_move.GetAttachmentFrom() + + # The links in a new ordering + # First, add all links to the new list + newOrdering = self._lines[:] + + # Delete the line object from the list of links; we're going to move + # it to another position in the list + del newOrdering[newOrdering.index(to_move)] + + old_x = -99999.9 + old_y = -99999.9 + + found = False + + for line in newOrdering: + if line.GetTo() == self and oldAttachment == line.GetAttachmentTo() or \ + line.GetFrom() == self and oldAttachment == line.GetAttachmentFrom(): + startX, startY, endX, endY = line.GetEnds() + if line.GetTo() == self: + xp = endX + yp = endY + else: + xp = startX + yp = startY + + thisPoint = wx.RealPoint(xp, yp) + lastPoint = wx.RealPoint(old_x, old_y) + newPoint = wx.RealPoint(x, y) + + if self.AttachmentSortTest(newAttachment, newPoint, thisPoint) and self.AttachmentSortTest(newAttachment, lastPoint, newPoint): + found = True + newOrdering.insert(newOrdering.index(line), to_move) + + old_x = xp + old_y = yp + if found: + break + + if not found: + newOrdering.append(to_move) + + self.GetEventHandler().OnChangeAttachment(newAttachment, to_move, newOrdering) + return True + + def OnChangeAttachment(self, attachment, line, ordering): + if line.GetTo() == self: + line.SetAttachmentTo(attachment) + else: + line.SetAttachmentFrom(attachment) + + self.ApplyAttachmentOrdering(ordering) + + dc = wx.ClientDC(self.GetCanvas()) + self.GetCanvas().PrepareDC(dc) + self.MoveLinks(dc) + + if not self.GetCanvas().GetQuickEditMode(): + self.GetCanvas().Redraw(dc) + + # Reorders the lines according to the given list + def ApplyAttachmentOrdering(self, linesToSort): + """Apply the line ordering in linesToSort to the shape, to reorder + the way lines are attached. + """ + linesStore = self._lines[:] + + self._lines = [] + + for line in linesToSort: + if line in linesStore: + del linesStore[linesStore.index(line)] + self._lines.append(line) + + # Now add any lines that haven't been listed in linesToSort + self._lines += linesStore + + def SortLines(self, attachment, linesToSort): + """ Reorder the lines coming into the node image at this attachment + position, in the order in which they appear in linesToSort. + + Any remaining lines not in the list will be added to the end. + """ + # This is a temporary store of all the lines at this attachment + # point. We'll tick them off as we've processed them. + linesAtThisAttachment = [] + + for line in self._lines[:]: + if line.GetTo() == self and line.GetAttachmentTo() == attachment or \ + line.GetFrom() == self and line.GetAttachmentFrom() == attachment: + linesAtThisAttachment.append(line) + del self._lines[self._lines.index(line)] + + for line in linesToSort: + if line in linesAtThisAttachment: + # Done this one + del linesAtThisAttachment[linesAtThisAttachment.index(line)] + self._lines.append(line) + + # Now add any lines that haven't been listed in linesToSort + self._lines += linesAtThisAttachment + + def OnHighlight(self, dc): + pass + + def OnLeftClick(self, x, y, keys = 0, attachment = 0): + if self._sensitivity & OP_CLICK_LEFT != OP_CLICK_LEFT: + if self._parent: + attachment, dist = self._parent.HitTest(x, y) + self._parent.GetEventHandler().OnLeftClick(x, y, keys, attachment) + + def OnRightClick(self, x, y, keys = 0, attachment = 0): + if self._sensitivity & OP_CLICK_RIGHT != OP_CLICK_RIGHT: + attachment, dist = self._parent.HitTest(x, y) + self._parent.GetEventHandler().OnRightClick(x, y, keys, attachment) + + def OnDragLeft(self, draw, x, y, keys = 0, attachment = 0): + if self._sensitivity & OP_DRAG_LEFT != OP_DRAG_LEFT: + if self._parent: + hit = self._parent.HitTest(x, y) + if hit: + attachment, dist = hit + self._parent.GetEventHandler().OnDragLeft(draw, x, y, keys, attachment) + return + + dc = wx.ClientDC(self.GetCanvas()) + self.GetCanvas().PrepareDC(dc) + dc.SetLogicalFunction(OGLRBLF) + + dottedPen = wx.Pen(wx.Colour(0, 0, 0), 1, wx.DOT) + dc.SetPen(dottedPen) + dc.SetBrush(wx.TRANSPARENT_BRUSH) + + xx = x + DragOffsetX + yy = y + DragOffsetY + + xx, yy = self._canvas.Snap(xx, yy) + w, h = self.GetBoundingBoxMax() + self.GetEventHandler().OnDrawOutline(dc, xx, yy, w, h) + + def OnBeginDragLeft(self, x, y, keys = 0, attachment = 0): + global DragOffsetX, DragOffsetY + + if self._sensitivity & OP_DRAG_LEFT != OP_DRAG_LEFT: + if self._parent: + hit = self._parent.HitTest(x, y) + if hit: + attachment, dist = hit + self._parent.GetEventHandler().OnBeginDragLeft(x, y, keys, attachment) + return + + DragOffsetX = self._xpos - x + DragOffsetY = self._ypos - y + + dc = wx.ClientDC(self.GetCanvas()) + self.GetCanvas().PrepareDC(dc) + + # New policy: don't erase shape until end of drag. + # self.Erase(dc) + xx = x + DragOffsetX + yy = y + DragOffsetY + xx, yy = self._canvas.Snap(xx, yy) + dc.SetLogicalFunction(OGLRBLF) + + dottedPen = wx.Pen(wx.Colour(0, 0, 0), 1, wx.DOT) + dc.SetPen(dottedPen) + dc.SetBrush(wx.TRANSPARENT_BRUSH) + + w, h = self.GetBoundingBoxMax() + self.GetEventHandler().OnDrawOutline(dc, xx, yy, w, h) + self._canvas.CaptureMouse() + + def OnEndDragLeft(self, x, y, keys = 0, attachment = 0): + if self._canvas.HasCapture(): + self._canvas.ReleaseMouse() + if self._sensitivity & OP_DRAG_LEFT != OP_DRAG_LEFT: + if self._parent: + hit = self._parent.HitTest(x, y) + if hit: + attachment, dist = hit + self._parent.GetEventHandler().OnEndDragLeft(x, y, keys, attachment) + return + + dc = wx.ClientDC(self.GetCanvas()) + self.GetCanvas().PrepareDC(dc) + + dc.SetLogicalFunction(wx.COPY) + xx = x + DragOffsetX + yy = y + DragOffsetY + xx, yy = self._canvas.Snap(xx, yy) + + # New policy: erase shape at end of drag. + self.Erase(dc) + + self.Move(dc, xx, yy) + if self._canvas and not self._canvas.GetQuickEditMode(): + self._canvas.Redraw(dc) + + def OnDragRight(self, draw, x, y, keys = 0, attachment = 0): + if self._sensitivity & OP_DRAG_RIGHT != OP_DRAG_RIGHT: + if self._parent: + attachment, dist = self._parent.HitTest(x, y) + self._parent.GetEventHandler().OnDragRight(draw, x, y, keys, attachment) + return + + def OnBeginDragRight(self, x, y, keys = 0, attachment = 0): + if self._sensitivity & OP_DRAG_RIGHT != OP_DRAG_RIGHT: + if self._parent: + attachment, dist = self._parent.HitTest(x, y) + self._parent.GetEventHandler().OnBeginDragRight(x, y, keys, attachment) + return + + def OnEndDragRight(self, x, y, keys = 0, attachment = 0): + if self._sensitivity & OP_DRAG_RIGHT != OP_DRAG_RIGHT: + if self._parent: + attachment, dist = self._parent.HitTest(x, y) + self._parent.GetEventHandler().OnEndDragRight(x, y, keys, attachment) + return + + def OnDrawOutline(self, dc, x, y, w, h): + points = [[x - w / 2.0, y - h / 2.0], + [x + w / 2.0, y - h / 2.0], + [x + w / 2.0, y + h / 2.0], + [x - w / 2.0, y + h / 2.0], + [x - w / 2.0, y - h / 2.0], + ] + + dc.DrawLines(points) + + def Attach(self, can): + """Set the shape's internal canvas pointer to point to the given canvas.""" + self._canvas = can + + def Detach(self): + """Disassociates the shape from its canvas.""" + self._canvas = None + + def Move(self, dc, x, y, display = True): + """Move the shape to the given position. + Redraw if display is TRUE. + """ + old_x = self._xpos + old_y = self._ypos + + if not self.GetEventHandler().OnMovePre(dc, x, y, old_x, old_y, display): + return + + self._xpos, self._ypos = x, y + + self.ResetControlPoints() + + if display: + self.Draw(dc) + + self.MoveLinks(dc) + + self.GetEventHandler().OnMovePost(dc, x, y, old_x, old_y, display) + + def MoveLinks(self, dc): + """Redraw all the lines attached to the shape.""" + self.GetEventHandler().OnMoveLinks(dc) + + def Draw(self, dc): + """Draw the whole shape and any lines attached to it. + + Do not override this function: override OnDraw, which is called + by this function. + """ + if self._visible: + self.GetEventHandler().OnDraw(dc) + self.GetEventHandler().OnDrawContents(dc) + self.GetEventHandler().OnDrawControlPoints(dc) + self.GetEventHandler().OnDrawBranches(dc) + + def Flash(self): + """Flash the shape.""" + if self.GetCanvas(): + dc = wx.ClientDC(self.GetCanvas()) + self.GetCanvas().PrepareDC(dc) + + dc.SetLogicalFunction(OGLRBLF) + self.Draw(dc) + dc.SetLogicalFunction(wx.COPY) + self.Draw(dc) + + def Show(self, show): + """Set a flag indicating whether the shape should be drawn.""" + self._visible = show + for child in self._children: + child.Show(show) + + def Erase(self, dc): + """Erase the shape. + Does not repair damage caused to other shapes. + """ + self.GetEventHandler().OnErase(dc) + self.GetEventHandler().OnEraseControlPoints(dc) + self.GetEventHandler().OnDrawBranches(dc, erase = True) + + def EraseContents(self, dc): + """Erase the shape contents, that is, the area within the shape's + minimum bounding box. + """ + self.GetEventHandler().OnEraseContents(dc) + + def AddText(self, string): + """Add a line of text to the shape's default text region.""" + if not self._regions: + return + + region = self._regions[0] + #region.ClearText() + new_line = ShapeTextLine(0, 0, string) + text = region.GetFormattedText() + text.append(new_line) + + self._formatted = False + + def SetSize(self, x, y, recursive = True): + """Set the shape's size.""" + self.SetAttachmentSize(x, y) + self.SetDefaultRegionSize() + + def SetAttachmentSize(self, w, h): + width, height = self.GetBoundingBoxMin() + if width == 0: + scaleX = 1.0 + else: + scaleX = float(w) / width + if height == 0: + scaleY = 1.0 + else: + scaleY = float(h) / height + + for point in self._attachmentPoints: + point._x = point._x * scaleX + point._y = point._y * scaleY + + # Add line FROM this object + def AddLine(self, line, other, attachFrom = 0, attachTo = 0, positionFrom = -1, positionTo = -1): + """Add a line between this shape and the given other shape, at the + specified attachment points. + + The position in the list of lines at each end can also be specified, + so that the line will be drawn at a particular point on its attachment + point. + """ + if positionFrom == -1: + if not line in self._lines: + self._lines.append(line) + else: + # Don't preserve old ordering if we have new ordering instructions + try: + self._lines.remove(line) + except ValueError: + pass + if positionFrom < len(self._lines): + self._lines.insert(positionFrom, line) + else: + self._lines.append(line) + + if positionTo == -1: + if not other in other._lines: + other._lines.append(line) + else: + # Don't preserve old ordering if we have new ordering instructions + try: + other._lines.remove(line) + except ValueError: + pass + if positionTo < len(other._lines): + other._lines.insert(positionTo, line) + else: + other._lines.append(line) + + line.SetFrom(self) + line.SetTo(other) + line.SetAttachments(attachFrom, attachTo) + + dc = wx.ClientDC(self._canvas) + self._canvas.PrepareDC(dc) + self.MoveLinks(dc) + + def RemoveLine(self, line): + """Remove the given line from the shape's list of attached lines.""" + if line.GetFrom() == self: + line.GetTo()._lines.remove(line) + else: + line.GetFrom()._lines.remove(line) + + self._lines.remove(line) + + # Default - make 6 control points + def MakeControlPoints(self): + """Make a list of control points (draggable handles) appropriate to + the shape. + """ + maxX, maxY = self.GetBoundingBoxMax() + minX, minY = self.GetBoundingBoxMin() + + widthMin = minX + CONTROL_POINT_SIZE + 2 + heightMin = minY + CONTROL_POINT_SIZE + 2 + + # Offsets from main object + top = -heightMin / 2.0 + bottom = heightMin / 2.0 + (maxY - minY) + left = -widthMin / 2.0 + right = widthMin / 2.0 + (maxX - minX) + + control = ControlPoint(self._canvas, self, CONTROL_POINT_SIZE, left, top, CONTROL_POINT_DIAGONAL) + self._canvas.AddShape(control) + self._controlPoints.append(control) + + control = ControlPoint(self._canvas, self, CONTROL_POINT_SIZE, 0, top, CONTROL_POINT_VERTICAL) + self._canvas.AddShape(control) + self._controlPoints.append(control) + + control = ControlPoint(self._canvas, self, CONTROL_POINT_SIZE, right, top, CONTROL_POINT_DIAGONAL) + self._canvas.AddShape(control) + self._controlPoints.append(control) + + control = ControlPoint(self._canvas, self, CONTROL_POINT_SIZE, right, 0, CONTROL_POINT_HORIZONTAL) + self._canvas.AddShape(control) + self._controlPoints.append(control) + + control = ControlPoint(self._canvas, self, CONTROL_POINT_SIZE, right, bottom, CONTROL_POINT_DIAGONAL) + self._canvas.AddShape(control) + self._controlPoints.append(control) + + control = ControlPoint(self._canvas, self, CONTROL_POINT_SIZE, 0, bottom, CONTROL_POINT_VERTICAL) + self._canvas.AddShape(control) + self._controlPoints.append(control) + + control = ControlPoint(self._canvas, self, CONTROL_POINT_SIZE, left, bottom, CONTROL_POINT_DIAGONAL) + self._canvas.AddShape(control) + self._controlPoints.append(control) + + control = ControlPoint(self._canvas, self, CONTROL_POINT_SIZE, left, 0, CONTROL_POINT_HORIZONTAL) + self._canvas.AddShape(control) + self._controlPoints.append(control) + + def MakeMandatoryControlPoints(self): + """Make the mandatory control points. + + For example, the control point on a dividing line should appear even + if the divided rectangle shape's handles should not appear (because + it is the child of a composite, and children are not resizable). + """ + for child in self._children: + child.MakeMandatoryControlPoints() + + def ResetMandatoryControlPoints(self): + """Reset the mandatory control points.""" + for child in self._children: + child.ResetMandatoryControlPoints() + + def ResetControlPoints(self): + """Reset the positions of the control points (for instance when the + shape's shape has changed). + """ + self.ResetMandatoryControlPoints() + + if len(self._controlPoints) == 0: + return + + maxX, maxY = self.GetBoundingBoxMax() + minX, minY = self.GetBoundingBoxMin() + + widthMin = minX + CONTROL_POINT_SIZE + 2 + heightMin = minY + CONTROL_POINT_SIZE + 2 + + # Offsets from main object + top = -heightMin / 2.0 + bottom = heightMin / 2.0 + (maxY - minY) + left = -widthMin / 2.0 + right = widthMin / 2.0 + (maxX - minX) + + self._controlPoints[0]._xoffset = left + self._controlPoints[0]._yoffset = top + + self._controlPoints[1]._xoffset = 0 + self._controlPoints[1]._yoffset = top + + self._controlPoints[2]._xoffset = right + self._controlPoints[2]._yoffset = top + + self._controlPoints[3]._xoffset = right + self._controlPoints[3]._yoffset = 0 + + self._controlPoints[4]._xoffset = right + self._controlPoints[4]._yoffset = bottom + + self._controlPoints[5]._xoffset = 0 + self._controlPoints[5]._yoffset = bottom + + self._controlPoints[6]._xoffset = left + self._controlPoints[6]._yoffset = bottom + + self._controlPoints[7]._xoffset = left + self._controlPoints[7]._yoffset = 0 + + def DeleteControlPoints(self, dc = None): + """Delete the control points (or handles) for the shape. + + Does not redraw the shape. + """ + for control in self._controlPoints[:]: + if dc: + control.GetEventHandler().OnErase(dc) + control.Delete() + self._controlPoints.remove(control) + self._controlPoints = [] + + # Children of divisions are contained objects, + # so stop here + if not isinstance(self, DivisionShape): + for child in self._children: + child.DeleteControlPoints(dc) + + def OnDrawControlPoints(self, dc): + if not self._drawHandles: + return + + dc.SetBrush(wx.BLACK_BRUSH) + dc.SetPen(wx.BLACK_PEN) + + for control in self._controlPoints: + control.Draw(dc) + + # Children of divisions are contained objects, + # so stop here. + # This test bypasses the type facility for speed + # (critical when drawing) + + if not isinstance(self, DivisionShape): + for child in self._children: + child.GetEventHandler().OnDrawControlPoints(dc) + + def OnEraseControlPoints(self, dc): + for control in self._controlPoints: + control.Erase(dc) + + if not isinstance(self, DivisionShape): + for child in self._children: + child.GetEventHandler().OnEraseControlPoints(dc) + + def Select(self, select, dc = None): + """Select or deselect the given shape, drawing or erasing control points + (handles) as necessary. + """ + self._selected = select + if select: + self.MakeControlPoints() + # Children of divisions are contained objects, + # so stop here + if not isinstance(self, DivisionShape): + for child in self._children: + child.MakeMandatoryControlPoints() + if dc: + self.GetEventHandler().OnDrawControlPoints(dc) + else: + self.DeleteControlPoints(dc) + if not isinstance(self, DivisionShape): + for child in self._children: + child.DeleteControlPoints(dc) + + def Selected(self): + """TRUE if the shape is currently selected.""" + return self._selected + + def AncestorSelected(self): + """TRUE if the shape's ancestor is currently selected.""" + if self._selected: + return True + if not self.GetParent(): + return False + return self.GetParent().AncestorSelected() + + def GetNumberOfAttachments(self): + """Get the number of attachment points for this shape.""" + # Should return the MAXIMUM attachment point id here, + # so higher-level functions can iterate through all attachments, + # even if they're not contiguous. + + if len(self._attachmentPoints) == 0: + return 4 + else: + maxN = 3 + for point in self._attachmentPoints: + if point._id > maxN: + maxN = point._id + return maxN + 1 + + def AttachmentIsValid(self, attachment): + """TRUE if attachment is a valid attachment point.""" + if len(self._attachmentPoints) == 0: + return attachment in range(4) + + for point in self._attachmentPoints: + if point._id == attachment: + return True + return False + + def GetAttachmentPosition(self, attachment, nth = 0, no_arcs = 1, line = None): + """Get the position at which the given attachment point should be drawn. + + If attachment isn't found among the attachment points of the shape, + returns None. + """ + if self._attachmentMode == ATTACHMENT_MODE_NONE: + return self._xpos, self._ypos + elif self._attachmentMode == ATTACHMENT_MODE_BRANCHING: + pt, stemPt = self.GetBranchingAttachmentPoint(attachment, nth) + return pt[0], pt[1] + elif self._attachmentMode == ATTACHMENT_MODE_EDGE: + if len(self._attachmentPoints): + for point in self._attachmentPoints: + if point._id == attachment: + return self._xpos + point._x, self._ypos + point._y + return None + else: + # Assume is rectangular + w, h = self.GetBoundingBoxMax() + top = self._ypos + h / 2.0 + bottom = self._ypos - h / 2.0 + left = self._xpos - w / 2.0 + right = self._xpos + w / 2.0 + + # wtf? + line and line.IsEnd(self) + + physicalAttachment = self.LogicalToPhysicalAttachment(attachment) + + # Simplified code + if physicalAttachment == 0: + pt = self.CalcSimpleAttachment((left, bottom), (right, bottom), nth, no_arcs, line) + elif physicalAttachment == 1: + pt = self.CalcSimpleAttachment((right, bottom), (right, top), nth, no_arcs, line) + elif physicalAttachment == 2: + pt = self.CalcSimpleAttachment((left, top), (right, top), nth, no_arcs, line) + elif physicalAttachment == 3: + pt = self.CalcSimpleAttachment((left, bottom), (left, top), nth, no_arcs, line) + else: + return None + return pt[0], pt[1] + return None + + def GetBoundingBoxMax(self): + """Get the maximum bounding box for the shape, taking into account + external features such as shadows. + """ + ww, hh = self.GetBoundingBoxMin() + if self._shadowMode != SHADOW_NONE: + ww += self._shadowOffsetX + hh += self._shadowOffsetY + return ww, hh + + def GetBoundingBoxMin(self): + """Get the minimum bounding box for the shape, that defines the area + available for drawing the contents (such as text). + + Must be overridden. + """ + return 0, 0 + + def HasDescendant(self, image): + """TRUE if image is a descendant of this composite.""" + if image == self: + return True + for child in self._children: + if child.HasDescendant(image): + return True + return False + + # Assuming the attachment lies along a vertical or horizontal line, + # calculate the position on that point. + def CalcSimpleAttachment(self, pt1, pt2, nth, noArcs, line): + """Assuming the attachment lies along a vertical or horizontal line, + calculate the position on that point. + + Parameters: + + pt1 + The first point of the line repesenting the edge of the shape. + + pt2 + The second point of the line representing the edge of the shape. + + nth + The position on the edge (for example there may be 6 lines at + this attachment point, and this may be the 2nd line. + + noArcs + The number of lines at this edge. + + line + The line shape. + + Remarks + + This function expects the line to be either vertical or horizontal, + and determines which. + """ + isEnd = line and line.IsEnd(self) + + # Are we horizontal or vertical? + isHorizontal = RoughlyEqual(pt1[1], pt2[1]) + + if isHorizontal: + if pt1[0] > pt2[0]: + firstPoint = pt2 + secondPoint = pt1 + else: + firstPoint = pt1 + secondPoint = pt2 + + if self._spaceAttachments: + if line and line.GetAlignmentType(isEnd) == LINE_ALIGNMENT_TO_NEXT_HANDLE: + # Align line according to the next handle along + point = line.GetNextControlPoint(self) + if point[0] < firstPoint[0]: + x = firstPoint[0] + elif point[0] > secondPoint[0]: + x = secondPoint[0] + else: + x = point[0] + else: + x = firstPoint[0] + (nth + 1) * (secondPoint[0] - firstPoint[0]) / (noArcs + 1.0) + else: + x = (secondPoint[0] - firstPoint[0]) / 2.0 # Midpoint + y = pt1[1] + else: + assert RoughlyEqual(pt1[0], pt2[0]) + + if pt1[1] > pt2[1]: + firstPoint = pt2 + secondPoint = pt1 + else: + firstPoint = pt1 + secondPoint = pt2 + + if self._spaceAttachments: + if line and line.GetAlignmentType(isEnd) == LINE_ALIGNMENT_TO_NEXT_HANDLE: + # Align line according to the next handle along + point = line.GetNextControlPoint(self) + if point[1] < firstPoint[1]: + y = firstPoint[1] + elif point[1] > secondPoint[1]: + y = secondPoint[1] + else: + y = point[1] + else: + y = firstPoint[1] + (nth + 1) * (secondPoint[1] - firstPoint[1]) / (noArcs + 1.0) + else: + y = (secondPoint[1] - firstPoint[1]) / 2.0 # Midpoint + x = pt1[0] + + return x, y + + # Return the zero-based position in m_lines of line + def GetLinePosition(self, line): + """Get the zero-based position of line in the list of lines + for this shape. + """ + try: + return self._lines.index(line) + except: + return 0 + + + # |________| + # | <- root + # | <- neck + # shoulder1 ->---------<- shoulder2 + # | | | | | + # <- branching attachment point N-1 + + def GetBranchingAttachmentInfo(self, attachment): + """Get information about where branching connections go. + + Returns FALSE if there are no lines at this attachment. + """ + physicalAttachment = self.LogicalToPhysicalAttachment(attachment) + + # Number of lines at this attachment + lineCount = self.GetAttachmentLineCount(attachment) + + if not lineCount: + return False + + totalBranchLength = self._branchSpacing * (lineCount - 1) + root = self.GetBranchingAttachmentRoot(attachment) + + neck = wx.RealPoint() + shoulder1 = wx.RealPoint() + shoulder2 = wx.RealPoint() + + # Assume that we have attachment points 0 to 3: top, right, bottom, left + if physicalAttachment == 0: + neck[0] = self.GetX() + neck[1] = root[1] - self._branchNeckLength + + shoulder1[0] = root[0] - totalBranchLength / 2.0 + shoulder2[0] = root[0] + totalBranchLength / 2.0 + + shoulder1[1] = neck[1] + shoulder2[1] = neck[1] + elif physicalAttachment == 1: + neck[0] = root[0] + self._branchNeckLength + neck[1] = root[1] + + shoulder1[0] = neck[0] + shoulder2[0] = neck[0] + + shoulder1[1] = neck[1] - totalBranchLength / 2.0 + shoulder1[1] = neck[1] + totalBranchLength / 2.0 + elif physicalAttachment == 2: + neck[0] = self.GetX() + neck[1] = root[1] + self._branchNeckLength + + shoulder1[0] = root[0] - totalBranchLength / 2.0 + shoulder2[0] = root[0] + totalBranchLength / 2.0 + + shoulder1[1] = neck[1] + shoulder2[1] = neck[1] + elif physicalAttachment == 3: + neck[0] = root[0] - self._branchNeckLength + neck[1] = root[1] + + shoulder1[0] = neck[0] + shoulder2[0] = neck[0] + + shoulder1[1] = neck[1] - totalBranchLength / 2.0 + shoulder2[1] = neck[1] + totalBranchLength / 2.0 + else: + raise "Unrecognised attachment point in GetBranchingAttachmentInfo" + return root, neck, shoulder1, shoulder2 + + def GetBranchingAttachmentPoint(self, attachment, n): + physicalAttachment = self.LogicalToPhysicalAttachment(attachment) + + root, neck, shoulder1, shoulder2 = self.GetBranchingAttachmentInfo(attachment) + pt = wx.RealPoint() + stemPt = wx.RealPoint() + + if physicalAttachment == 0: + pt[1] = neck[1] - self._branchStemLength + pt[0] = shoulder1[0] + n * self._branchSpacing + + stemPt[0] = pt[0] + stemPt[1] = neck[1] + elif physicalAttachment == 2: + pt[1] = neck[1] + self._branchStemLength + pt[0] = shoulder1[0] + n * self._branchStemLength + + stemPt[0] = pt[0] + stemPt[1] = neck[1] + elif physicalAttachment == 1: + pt[0] = neck[0] + self._branchStemLength + pt[1] = shoulder1[1] + n * self._branchSpacing + + stemPt[0] = neck[0] + stemPt[1] = pt[1] + elif physicalAttachment == 3: + pt[0] = neck[0] - self._branchStemLength + pt[1] = shoulder1[1] + n * self._branchSpacing + + stemPt[0] = neck[0] + stemPt[1] = pt[1] + else: + raise "Unrecognised attachment point in GetBranchingAttachmentPoint" + + return pt, stemPt + + def GetAttachmentLineCount(self, attachment): + """Get the number of lines at this attachment position.""" + count = 0 + for lineShape in self._lines: + if lineShape.GetFrom() == self and lineShape.GetAttachmentFrom() == attachment: + count += 1 + elif lineShape.GetTo() == self and lineShape.GetAttachmentTo() == attachment: + count += 1 + return count + + def GetBranchingAttachmentRoot(self, attachment): + """Get the root point at the given attachment.""" + physicalAttachment = self.LogicalToPhysicalAttachment(attachment) + + root = wx.RealPoint() + + width, height = self.GetBoundingBoxMax() + + # Assume that we have attachment points 0 to 3: top, right, bottom, left + if physicalAttachment == 0: + root[0] = self.GetX() + root[1] = self.GetY() - height / 2.0 + elif physicalAttachment == 1: + root[0] = self.GetX() + width / 2.0 + root[1] = self.GetY() + elif physicalAttachment == 2: + root[0] = self.GetX() + root[1] = self.GetY() + height / 2.0 + elif physicalAttachment == 3: + root[0] = self.GetX() - width / 2.0 + root[1] = self.GetY() + else: + raise "Unrecognised attachment point in GetBranchingAttachmentRoot" + + return root + + # Draw or erase the branches (not the actual arcs though) + def OnDrawBranchesAttachment(self, dc, attachment, erase = False): + count = self.GetAttachmentLineCount(attachment) + if count == 0: + return + + root, neck, shoulder1, shoulder2 = self.GetBranchingAttachmentInfo(attachment) + + if erase: + dc.SetPen(wx.WHITE_PEN) + dc.SetBrush(wx.WHITE_BRUSH) + else: + dc.SetPen(wx.BLACK_PEN) + dc.SetBrush(wx.BLACK_BRUSH) + + # Draw neck + dc.DrawLine(root[0], root[1], neck[0], neck[1]) + + if count > 1: + # Draw shoulder-to-shoulder line + dc.DrawLine(shoulder1[0], shoulder1[1], shoulder2[0], shoulder2[1]) + # Draw all the little branches + for i in range(count): + pt, stemPt = self.GetBranchingAttachmentPoint(attachment, i) + dc.DrawLine(stemPt[0], stemPt[1], pt[0], pt[1]) + + if self.GetBranchStyle() & BRANCHING_ATTACHMENT_BLOB and count > 1: + blobSize = 6.0 + dc.DrawEllipse(stemPt[0] - blobSize / 2.0, stemPt[1] - blobSize / 2.0, blobSize, blobSize) + + def OnDrawBranches(self, dc, erase = False): + if self._attachmentMode != ATTACHMENT_MODE_BRANCHING: + return + for i in range(self.GetNumberOfAttachments()): + self.OnDrawBranchesAttachment(dc, i, erase) + + def GetAttachmentPositionEdge(self, attachment, nth = 0, no_arcs = 1, line = None): + """ Only get the attachment position at the _edge_ of the shape, + ignoring branching mode. This is used e.g. to indicate the edge of + interest, not the point on the attachment branch. + """ + oldMode = self._attachmentMode + + # Calculate as if to edge, not branch + if self._attachmentMode == ATTACHMENT_MODE_BRANCHING: + self._attachmentMode = ATTACHMENT_MODE_EDGE + res = self.GetAttachmentPosition(attachment, nth, no_arcs, line) + self._attachmentMode = oldMode + + return res + + def PhysicalToLogicalAttachment(self, physicalAttachment): + """ Rotate the standard attachment point from physical + (0 is always North) to logical (0 -> 1 if rotated by 90 degrees) + """ + if RoughlyEqual(self.GetRotation(), 0): + i = physicalAttachment + elif RoughlyEqual(self.GetRotation(), math.pi / 2.0): + i = physicalAttachment - 1 + elif RoughlyEqual(self.GetRotation(), math.pi): + i = physicalAttachment - 2 + elif RoughlyEqual(self.GetRotation(), 3 * math.pi / 2.0): + i = physicalAttachment - 3 + else: + # Can't handle -- assume the same + return physicalAttachment + + if i < 0: + i += 4 + + return i + + def LogicalToPhysicalAttachment(self, logicalAttachment): + """Rotate the standard attachment point from logical + to physical (0 is always North). + """ + if RoughlyEqual(self.GetRotation(), 0): + i = logicalAttachment + elif RoughlyEqual(self.GetRotation(), math.pi / 2.0): + i = logicalAttachment + 1 + elif RoughlyEqual(self.GetRotation(), math.pi): + i = logicalAttachment + 2 + elif RoughlyEqual(self.GetRotation(), 3 * math.pi / 2.0): + i = logicalAttachment + 3 + else: + return logicalAttachment + + if i > 3: + i -= 4 + + return i + + def Rotate(self, x, y, theta): + """Rotate about the given axis by the given amount in radians.""" + self._rotation = theta + if self._rotation < 0: + self._rotation += 2 * math.pi + elif self._rotation > 2 * math.pi: + self._rotation -= 2 * math.pi + + def GetBackgroundPen(self): + """Return pen of the right colour for the background.""" + if self.GetCanvas(): + return wx.Pen(self.GetCanvas().GetBackgroundColour(), 1, wx.SOLID) + return WhiteBackgroundPen + + def GetBackgroundBrush(self): + """Return brush of the right colour for the background.""" + if self.GetCanvas(): + return wx.Brush(self.GetCanvas().GetBackgroundColour(), wx.SOLID) + return WhiteBackgroundBrush + + def GetX(self): + """Get the x position of the centre of the shape.""" + return self._xpos + + def GetY(self): + """Get the y position of the centre of the shape.""" + return self._ypos + + def SetX(self, x): + """Set the x position of the shape.""" + self._xpos = x + + def SetY(self, y): + """Set the y position of the shape.""" + self._ypos = y + + def GetParent(self): + """Return the parent of this shape, if it is part of a composite.""" + return self._parent + + def SetParent(self, p): + self._parent = p + + def GetChildren(self): + """Return the list of children for this shape.""" + return self._children + + def GetDrawHandles(self): + """Return the list of drawhandles.""" + return self._drawHandles + + def GetEventHandler(self): + """Return the event handler for this shape.""" + return self._eventHandler + + def SetEventHandler(self, handler): + """Set the event handler for this shape.""" + self._eventHandler = handler + + def Recompute(self): + """Recomputes any constraints associated with the shape. + + Normally applicable to CompositeShapes only, but harmless for + other classes of Shape. + """ + return True + + def IsHighlighted(self): + """TRUE if the shape is highlighted. Shape highlighting is unimplemented.""" + return self._highlighted + + def GetSensitivityFilter(self): + """Return the sensitivity filter, a bitlist of values. + + See Shape.SetSensitivityFilter. + """ + return self._sensitivity + + def SetFixedSize(self, x, y): + """Set the shape to be fixed size.""" + self._fixedWidth = x + self._fixedHeight = y + + def GetFixedSize(self): + """Return flags indicating whether the shape is of fixed size in + either direction. + """ + return self._fixedWidth, self._fixedHeight + + def GetFixedWidth(self): + """TRUE if the shape cannot be resized in the horizontal plane.""" + return self._fixedWidth + + def GetFixedHeight(self): + """TRUE if the shape cannot be resized in the vertical plane.""" + return self._fixedHeight + + def SetSpaceAttachments(self, sp): + """Indicate whether lines should be spaced out evenly at the point + they touch the node (sp = True), or whether they should join at a single + point (sp = False). + """ + self._spaceAttachments = sp + + def GetSpaceAttachments(self): + """Return whether lines should be spaced out evenly at the point they + touch the node (True), or whether they should join at a single point + (False). + """ + return self._spaceAttachments + + def SetCentreResize(self, cr): + """Specify whether the shape is to be resized from the centre (the + centre stands still) or from the corner or side being dragged (the + other corner or side stands still). + """ + self._centreResize = cr + + def GetCentreResize(self): + """TRUE if the shape is to be resized from the centre (the centre stands + still), or FALSE if from the corner or side being dragged (the other + corner or side stands still) + """ + return self._centreResize + + def SetMaintainAspectRatio(self, ar): + """Set whether a shape that resizes should not change the aspect ratio + (width and height should be in the original proportion). + """ + self._maintainAspectRatio = ar + + def GetMaintainAspectRatio(self): + """TRUE if shape keeps aspect ratio during resize.""" + return self._maintainAspectRatio + + def GetLines(self): + """Return the list of lines connected to this shape.""" + return self._lines + + def SetDisableLabel(self, flag): + """Set flag to TRUE to stop the default region being shown.""" + self._disableLabel = flag + + def GetDisableLabel(self): + """TRUE if the default region will not be shown, FALSE otherwise.""" + return self._disableLabel + + def SetAttachmentMode(self, mode): + """Set the attachment mode. + + If TRUE, attachment points will be significant when drawing lines to + and from this shape. + If FALSE, lines will be drawn as if to the centre of the shape. + """ + self._attachmentMode = mode + + def GetAttachmentMode(self): + """Return the attachment mode. + + See Shape.SetAttachmentMode. + """ + return self._attachmentMode + + def SetId(self, i): + """Set the integer identifier for this shape.""" + self._id = i + + def GetId(self): + """Return the integer identifier for this shape.""" + return self._id + + def IsShown(self): + """TRUE if the shape is in a visible state, FALSE otherwise. + + Note that this has nothing to do with whether the window is hidden + or the shape has scrolled off the canvas; it refers to the internal + visibility flag. + """ + return self._visible + + def GetPen(self): + """Return the pen used for drawing the shape's outline.""" + return self._pen + + def GetBrush(self): + """Return the brush used for filling the shape.""" + return self._brush + + def GetNumberOfTextRegions(self): + """Return the number of text regions for this shape.""" + return len(self._regions) + + def GetRegions(self): + """Return the list of ShapeRegions.""" + return self._regions + + # Control points ('handles') redirect control to the actual shape, to + # make it easier to override sizing behaviour. + def OnSizingDragLeft(self, pt, draw, x, y, keys = 0, attachment = 0): + bound_x, bound_y = self.GetBoundingBoxMin() + + dc = wx.ClientDC(self.GetCanvas()) + self.GetCanvas().PrepareDC(dc) + + dc.SetLogicalFunction(OGLRBLF) + + dottedPen = wx.Pen(wx.Colour(0, 0, 0), 1, wx.DOT) + dc.SetPen(dottedPen) + dc.SetBrush(wx.TRANSPARENT_BRUSH) + + if self.GetCentreResize(): + # Maintain the same centre point + new_width = 2.0 * abs(x - self.GetX()) + new_height = 2.0 * abs(y - self.GetY()) + + # Constrain sizing according to what control point you're dragging + if pt._type == CONTROL_POINT_HORIZONTAL: + if self.GetMaintainAspectRatio(): + new_height = bound_y * (new_width / bound_x) + else: + new_height = bound_y + elif pt._type == CONTROL_POINT_VERTICAL: + if self.GetMaintainAspectRatio(): + new_width = bound_x * (new_height / bound_y) + else: + new_width = bound_x + elif pt._type == CONTROL_POINT_DIAGONAL and (keys & KEY_SHIFT): + new_height = bound_y * (new_width / bound_x) + + if self.GetFixedWidth(): + new_width = bound_x + + if self.GetFixedHeight(): + new_height = bound_y + + pt._controlPointDragEndWidth = new_width + pt._controlPointDragEndHeight = new_height + + self.GetEventHandler().OnDrawOutline(dc, self.GetX(), self.GetY(), new_width, new_height) + else: + # Don't maintain the same centre point + newX1 = min(pt._controlPointDragStartX, x) + newY1 = min(pt._controlPointDragStartY, y) + newX2 = max(pt._controlPointDragStartX, x) + newY2 = max(pt._controlPointDragStartY, y) + if pt._type == CONTROL_POINT_HORIZONTAL: + newY1 = pt._controlPointDragStartY + newY2 = newY1 + pt._controlPointDragStartHeight + elif pt._type == CONTROL_POINT_VERTICAL: + newX1 = pt._controlPointDragStartX + newX2 = newX1 + pt._controlPointDragStartWidth + elif pt._type == CONTROL_POINT_DIAGONAL and (keys & KEY_SHIFT or self.GetMaintainAspectRatio()): + newH = (newX2 - newX1) * (float(pt._controlPointDragStartHeight) / pt._controlPointDragStartWidth) + if self.GetY() > pt._controlPointDragStartY: + newY2 = newY1 + newH + else: + newY1 = newY2 - newH + + newWidth = float(newX2 - newX1) + newHeight = float(newY2 - newY1) + + if pt._type == CONTROL_POINT_VERTICAL and self.GetMaintainAspectRatio(): + newWidth = bound_x * (newHeight / bound_y) + + if pt._type == CONTROL_POINT_HORIZONTAL and self.GetMaintainAspectRatio(): + newHeight = bound_y * (newWidth / bound_x) + + pt._controlPointDragPosX = newX1 + newWidth / 2.0 + pt._controlPointDragPosY = newY1 + newHeight / 2.0 + if self.GetFixedWidth(): + newWidth = bound_x + + if self.GetFixedHeight(): + newHeight = bound_y + + pt._controlPointDragEndWidth = newWidth + pt._controlPointDragEndHeight = newHeight + self.GetEventHandler().OnDrawOutline(dc, pt._controlPointDragPosX, pt._controlPointDragPosY, newWidth, newHeight) + + def OnSizingBeginDragLeft(self, pt, x, y, keys = 0, attachment = 0): + self._canvas.CaptureMouse() + + dc = wx.ClientDC(self.GetCanvas()) + self.GetCanvas().PrepareDC(dc) + + dc.SetLogicalFunction(OGLRBLF) + + bound_x, bound_y = self.GetBoundingBoxMin() + self.GetEventHandler().OnBeginSize(bound_x, bound_y) + + # Choose the 'opposite corner' of the object as the stationary + # point in case this is non-centring resizing. + if pt.GetX() < self.GetX(): + pt._controlPointDragStartX = self.GetX() + bound_x / 2.0 + else: + pt._controlPointDragStartX = self.GetX() - bound_x / 2.0 + + if pt.GetY() < self.GetY(): + pt._controlPointDragStartY = self.GetY() + bound_y / 2.0 + else: + pt._controlPointDragStartY = self.GetY() - bound_y / 2.0 + + if pt._type == CONTROL_POINT_HORIZONTAL: + pt._controlPointDragStartY = self.GetY() - bound_y / 2.0 + elif pt._type == CONTROL_POINT_VERTICAL: + pt._controlPointDragStartX = self.GetX() - bound_x / 2.0 + + # We may require the old width and height + pt._controlPointDragStartWidth = bound_x + pt._controlPointDragStartHeight = bound_y + + dottedPen = wx.Pen(wx.Colour(0, 0, 0), 1, wx.DOT) + dc.SetPen(dottedPen) + dc.SetBrush(wx.TRANSPARENT_BRUSH) + + if self.GetCentreResize(): + new_width = 2.0 * abs(x - self.GetX()) + new_height = 2.0 * abs(y - self.GetY()) + + # Constrain sizing according to what control point you're dragging + if pt._type == CONTROL_POINT_HORIZONTAL: + if self.GetMaintainAspectRatio(): + new_height = bound_y * (new_width / bound_x) + else: + new_height = bound_y + elif pt._type == CONTROL_POINT_VERTICAL: + if self.GetMaintainAspectRatio(): + new_width = bound_x * (new_height / bound_y) + else: + new_width = bound_x + elif pt._type == CONTROL_POINT_DIAGONAL and (keys & KEY_SHIFT): + new_height = bound_y * (new_width / bound_x) + + if self.GetFixedWidth(): + new_width = bound_x + + if self.GetFixedHeight(): + new_height = bound_y + + pt._controlPointDragEndWidth = new_width + pt._controlPointDragEndHeight = new_height + self.GetEventHandler().OnDrawOutline(dc, self.GetX(), self.GetY(), new_width, new_height) + else: + # Don't maintain the same centre point + newX1 = min(pt._controlPointDragStartX, x) + newY1 = min(pt._controlPointDragStartY, y) + newX2 = max(pt._controlPointDragStartX, x) + newY2 = max(pt._controlPointDragStartY, y) + if pt._type == CONTROL_POINT_HORIZONTAL: + newY1 = pt._controlPointDragStartY + newY2 = newY1 + pt._controlPointDragStartHeight + elif pt._type == CONTROL_POINT_VERTICAL: + newX1 = pt._controlPointDragStartX + newX2 = newX1 + pt._controlPointDragStartWidth + elif pt._type == CONTROL_POINT_DIAGONAL and (keys & KEY_SHIFT or self.GetMaintainAspectRatio()): + newH = (newX2 - newX1) * (float(pt._controlPointDragStartHeight) / pt._controlPointDragStartWidth) + if pt.GetY() > pt._controlPointDragStartY: + newY2 = newY1 + newH + else: + newY1 = newY2 - newH + + newWidth = float(newX2 - newX1) + newHeight = float(newY2 - newY1) + + if pt._type == CONTROL_POINT_VERTICAL and self.GetMaintainAspectRatio(): + newWidth = bound_x * (newHeight / bound_y) + + if pt._type == CONTROL_POINT_HORIZONTAL and self.GetMaintainAspectRatio(): + newHeight = bound_y * (newWidth / bound_x) + + pt._controlPointDragPosX = newX1 + newWidth / 2.0 + pt._controlPointDragPosY = newY1 + newHeight / 2.0 + if self.GetFixedWidth(): + newWidth = bound_x + + if self.GetFixedHeight(): + newHeight = bound_y + + pt._controlPointDragEndWidth = newWidth + pt._controlPointDragEndHeight = newHeight + self.GetEventHandler().OnDrawOutline(dc, pt._controlPointDragPosX, pt._controlPointDragPosY, newWidth, newHeight) + + def OnSizingEndDragLeft(self, pt, x, y, keys = 0, attachment = 0): + dc = wx.ClientDC(self.GetCanvas()) + self.GetCanvas().PrepareDC(dc) + + if self._canvas.HasCapture(): + self._canvas.ReleaseMouse() + dc.SetLogicalFunction(wx.COPY) + self.Recompute() + self.ResetControlPoints() + + self.Erase(dc) + + self.SetSize(pt._controlPointDragEndWidth, pt._controlPointDragEndHeight) + + # The next operation could destroy this control point (it does for + # label objects, via formatting the text), so save all values we're + # going to use, or we'll be accessing garbage. + + #return + + if self.GetCentreResize(): + self.Move(dc, self.GetX(), self.GetY()) + else: + self.Move(dc, pt._controlPointDragPosX, pt._controlPointDragPosY) + + # Recursively redraw links if we have a composite + if len(self.GetChildren()): + self.DrawLinks(dc, -1, True) + + width, height = self.GetBoundingBoxMax() + self.GetEventHandler().OnEndSize(width, height) + + if not self._canvas.GetQuickEditMode() and pt._eraseObject: + self._canvas.Redraw(dc) + + + +class RectangleShape(Shape): + """ + The wxRectangleShape has rounded or square corners. + + Derived from: + Shape + """ + def __init__(self, w = 0.0, h = 0.0): + Shape.__init__(self) + self._width = w + self._height = h + self._cornerRadius = 0.0 + self.SetDefaultRegionSize() + + def OnDraw(self, dc): + x1 = self._xpos - self._width / 2.0 + y1 = self._ypos - self._height / 2.0 + + if self._shadowMode != SHADOW_NONE: + if self._shadowBrush: + dc.SetBrush(self._shadowBrush) + dc.SetPen(TransparentPen) + + if self._cornerRadius: + dc.DrawRoundedRectangle(x1 + self._shadowOffsetX, y1 + self._shadowOffsetY, self._width, self._height, self._cornerRadius) + else: + dc.DrawRectangle(x1 + self._shadowOffsetX, y1 + self._shadowOffsetY, self._width, self._height) + + if self._pen: + if self._pen.GetWidth() == 0: + dc.SetPen(TransparentPen) + else: + dc.SetPen(self._pen) + if self._brush: + dc.SetBrush(self._brush) + + if self._cornerRadius: + dc.DrawRoundedRectangle(x1, y1, self._width, self._height, self._cornerRadius) + else: + dc.DrawRectangle(x1, y1, self._width, self._height) + + def GetBoundingBoxMin(self): + return self._width, self._height + + def SetSize(self, x, y, recursive = False): + self.SetAttachmentSize(x, y) + self._width = max(x, 1) + self._height = max(y, 1) + self.SetDefaultRegionSize() + + def GetCornerRadius(self): + """Get the radius of the rectangle's rounded corners.""" + return self._cornerRadius + + def SetCornerRadius(self, rad): + """Set the radius of the rectangle's rounded corners. + + If the radius is zero, a non-rounded rectangle will be drawn. + If the radius is negative, the value is the proportion of the smaller + dimension of the rectangle. + """ + self._cornerRadius = rad + + # Assume (x1, y1) is centre of box (most generally, line end at box) + def GetPerimeterPoint(self, x1, y1, x2, y2): + bound_x, bound_y = self.GetBoundingBoxMax() + return FindEndForBox(bound_x, bound_y, self._xpos, self._ypos, x2, y2) + + def GetWidth(self): + return self._width + + def GetHeight(self): + return self._height + + def SetWidth(self, w): + self._width = w + + def SetHeight(self, h): + self._height = h + + + +class PolygonShape(Shape): + """A PolygonShape's shape is defined by a number of points passed to + the object's constructor. It can be used to create new shapes such as + diamonds and triangles. + """ + def __init__(self): + Shape.__init__(self) + + self._points = None + self._originalPoints = None + + def Create(self, the_points = None): + """Takes a list of wx.RealPoints or tuples; each point is an offset + from the centre. + """ + self.ClearPoints() + + if not the_points: + self._originalPoints = [] + self._points = [] + else: + self._originalPoints = the_points + + # Duplicate the list of points + self._points = [] + for point in the_points: + new_point = wx.Point(point[0], point[1]) + self._points.append(new_point) + self.CalculateBoundingBox() + self._originalWidth = self._boundWidth + self._originalHeight = self._boundHeight + self.SetDefaultRegionSize() + + def ClearPoints(self): + self._points = [] + self._originalPoints = [] + + # Width and height. Centre of object is centre of box + def GetBoundingBoxMin(self): + return self._boundWidth, self._boundHeight + + def GetPoints(self): + """Return the internal list of polygon vertices.""" + return self._points + + def GetOriginalPoints(self): + return self._originalPoints + + def GetOriginalWidth(self): + return self._originalWidth + + def GetOriginalHeight(self): + return self._originalHeight + + def SetOriginalWidth(self, w): + self._originalWidth = w + + def SetOriginalHeight(self, h): + self._originalHeight = h + + def CalculateBoundingBox(self): + # Calculate bounding box at construction (and presumably resize) time + left = 10000 + right = -10000 + top = 10000 + bottom = -10000 + + for point in self._points: + if point[0] < left: + left = point[0] + if point[0] > right: + right = point[0] + + if point[1] < top: + top = point[1] + if point[1] > bottom: + bottom = point[1] + + self._boundWidth = right - left + self._boundHeight = bottom - top + + def CalculatePolygonCentre(self): + """Recalculates the centre of the polygon, and + readjusts the point offsets accordingly. + Necessary since the centre of the polygon + is expected to be the real centre of the bounding + box. + """ + left = 10000 + right = -10000 + top = 10000 + bottom = -10000 + + for point in self._points: + if point[0] < left: + left = point[0] + if point[0] > right: + right = point[0] + + if point[1] < top: + top = point[1] + if point[1] > bottom: + bottom = point[1] + + bwidth = right - left + bheight = bottom - top + + newCentreX = left + bwidth / 2.0 + newCentreY = top + bheight / 2.0 + + for i in range(len(self._points)): + self._points[i] = self._points[i][0] - newCentreX, self._points[i][1] - newCentreY + self._xpos += newCentreX + self._ypos += newCentreY + + def HitTest(self, x, y): + # Imagine four lines radiating from this point. If all of these lines + # hit the polygon, we're inside it, otherwise we're not. Obviously + # we'd need more radiating lines to be sure of correct results for + # very strange (concave) shapes. + endPointsX = [x, x + 1000, x, x - 1000] + endPointsY = [y - 1000, y, y + 1000, y] + + xpoints = [] + ypoints = [] + + for point in self._points: + xpoints.append(point[0] + self._xpos) + ypoints.append(point[1] + self._ypos) + + # We assume it's inside the polygon UNLESS one or more + # lines don't hit the outline. + isContained = True + + for i in range(4): + if not PolylineHitTest(xpoints, ypoints, x, y, endPointsX[i], endPointsY[i]): + isContained = False + + if not isContained: + return False + + nearest_attachment = 0 + + # If a hit, check the attachment points within the object + nearest = 999999 + + for i in range(self.GetNumberOfAttachments()): + e = self.GetAttachmentPositionEdge(i) + if e: + xp, yp = e + l = math.sqrt((xp - x) * (xp - x) + (yp - y) * (yp - y)) + if l < nearest: + nearest = l + nearest_attachment = i + + return nearest_attachment, nearest + + # Really need to be able to reset the shape! Otherwise, if the + # points ever go to zero, we've lost it, and can't resize. + def SetSize(self, new_width, new_height, recursive = True): + self.SetAttachmentSize(new_width, new_height) + + # Multiply all points by proportion of new size to old size + x_proportion = abs(float(new_width) / self._originalWidth) + y_proportion = abs(float(new_height) / self._originalHeight) + + for i in range(max(len(self._points), len(self._originalPoints))): + self._points[i] = wx.Point(self._originalPoints[i][0] * x_proportion, self._originalPoints[i][1] * y_proportion) + + self._boundWidth = abs(new_width) + self._boundHeight = abs(new_height) + self.SetDefaultRegionSize() + + # Make the original points the same as the working points + def UpdateOriginalPoints(self): + """If we've changed the shape, must make the original points match the + working points with this function. + """ + self._originalPoints = [] + + for point in self._points: + original_point = wx.RealPoint(point[0], point[1]) + self._originalPoints.append(original_point) + + self.CalculateBoundingBox() + self._originalWidth = self._boundWidth + self._originalHeight = self._boundHeight + + def AddPolygonPoint(self, pos): + """Add a control point after the given point.""" + try: + firstPoint = self._points[pos] + except ValueError: + firstPoint = self._points[0] + + try: + secondPoint = self._points[pos + 1] + except ValueError: + secondPoint = self._points[0] + + x = (secondPoint[0] - firstPoint[0]) / 2.0 + firstPoint[0] + y = (secondPoint[1] - firstPoint[1]) / 2.0 + firstPoint[1] + point = wx.RealPoint(x, y) + + if pos >= len(self._points) - 1: + self._points.append(point) + else: + self._points.insert(pos + 1, point) + + self.UpdateOriginalPoints() + + if self._selected: + self.DeleteControlPoints() + self.MakeControlPoints() + + def DeletePolygonPoint(self, pos): + """Delete the given control point.""" + if pos < len(self._points): + del self._points[pos] + self.UpdateOriginalPoints() + if self._selected: + self.DeleteControlPoints() + self.MakeControlPoints() + + # Assume (x1, y1) is centre of box (most generally, line end at box) + def GetPerimeterPoint(self, x1, y1, x2, y2): + # First check for situation where the line is vertical, + # and we would want to connect to a point on that vertical -- + # oglFindEndForPolyline can't cope with this (the arrow + # gets drawn to the wrong place). + if self._attachmentMode == ATTACHMENT_MODE_NONE and x1 == x2: + # Look for the point we'd be connecting to. This is + # a heuristic... + for point in self._points: + if point[0] == 0: + if y2 > y1 and point[1] > 0: + return point[0] + self._xpos, point[1] + self._ypos + elif y2 < y1 and point[1] < 0: + return point[0] + self._xpos, point[1] + self._ypos + + xpoints = [] + ypoints = [] + for point in self._points: + xpoints.append(point[0] + self._xpos) + ypoints.append(point[1] + self._ypos) + + return FindEndForPolyline(xpoints, ypoints, x1, y1, x2, y2) + + def OnDraw(self, dc): + if self._shadowMode != SHADOW_NONE: + if self._shadowBrush: + dc.SetBrush(self._shadowBrush) + dc.SetPen(TransparentPen) + + dc.DrawPolygon(self._points, self._xpos + self._shadowOffsetX, self._ypos, self._shadowOffsetY) + + if self._pen: + if self._pen.GetWidth() == 0: + dc.SetPen(TransparentPen) + else: + dc.SetPen(self._pen) + if self._brush: + dc.SetBrush(self._brush) + dc.DrawPolygon(self._points, self._xpos, self._ypos) + + def OnDrawOutline(self, dc, x, y, w, h): + dc.SetBrush(wx.TRANSPARENT_BRUSH) + # Multiply all points by proportion of new size to old size + x_proportion = abs(float(w) / self._originalWidth) + y_proportion = abs(float(h) / self._originalHeight) + + intPoints = [] + for point in self._originalPoints: + intPoints.append(wx.Point(x_proportion * point[0], y_proportion * point[1])) + dc.DrawPolygon(intPoints, x, y) + + # Make as many control points as there are vertices + def MakeControlPoints(self): + for point in self._points: + control = PolygonControlPoint(self._canvas, self, CONTROL_POINT_SIZE, point, point[0], point[1]) + self._canvas.AddShape(control) + self._controlPoints.append(control) + + def ResetControlPoints(self): + for i in range(min(len(self._points), len(self._controlPoints))): + point = self._points[i] + self._controlPoints[i]._xoffset = point[0] + self._controlPoints[i]._yoffset = point[1] + self._controlPoints[i].polygonVertex = point + + def GetNumberOfAttachments(self): + maxN = max(len(self._points) - 1, 0) + for point in self._attachmentPoints: + if point._id > maxN: + maxN = point._id + return maxN + 1 + + def GetAttachmentPosition(self, attachment, nth = 0, no_arcs = 1, line = None): + if self._attachmentMode == ATTACHMENT_MODE_EDGE and self._points and attachment < len(self._points): + point = self._points[0] + return point[0] + self._xpos, point[1] + self._ypos + return Shape.GetAttachmentPosition(self, attachment, nth, no_arcs, line) + + def AttachmentIsValid(self, attachment): + if not self._points: + return False + + if attachment >= 0 and attachment < len(self._points): + return True + + for point in self._attachmentPoints: + if point._id == attachment: + return True + + return False + + # Rotate about the given axis by the given amount in radians + def Rotate(self, x, y, theta): + actualTheta = theta - self._rotation + + # Rotate attachment points + sinTheta = math.sin(actualTheta) + cosTheta = math.cos(actualTheta) + + for point in self._attachmentPoints: + x1 = point._x + y1 = point._y + + point._x = x1 * cosTheta - y1 * sinTheta + x * (1 - cosTheta) + y * sinTheta + point._y = x1 * sinTheta + y1 * cosTheta + y * (1 - cosTheta) + x * sinTheta + + for i in range(len(self._points)): + x1, y1 = self._points[i] + + self._points[i] = x1 * cosTheta - y1 * sinTheta + x * (1 - cosTheta) + y * sinTheta, x1 * sinTheta + y1 * cosTheta + y * (1 - cosTheta) + x * sinTheta + + for i in range(len(self._originalPoints)): + x1, y1 = self._originalPoints[i] + + self._originalPoints[i] = x1 * cosTheta - y1 * sinTheta + x * (1 - cosTheta) + y * sinTheta, x1 * sinTheta + y1 * cosTheta + y * (1 - cosTheta) + x * sinTheta + + # Added by Pierre Hjälm. If we don't do this the outline will be + # the wrong size. Hopefully it won't have any ill effects. + self.UpdateOriginalPoints() + + self._rotation = theta + + self.CalculatePolygonCentre() + self.CalculateBoundingBox() + self.ResetControlPoints() + + # Control points ('handles') redirect control to the actual shape, to + # make it easier to override sizing behaviour. + def OnSizingDragLeft(self, pt, draw, x, y, keys = 0, attachment = 0): + dc = wx.ClientDC(self.GetCanvas()) + self.GetCanvas().PrepareDC(dc) + + dc.SetLogicalFunction(OGLRBLF) + + dottedPen = wx.Pen(wx.Colour(0, 0, 0), 1, wx.DOT) + dc.SetPen(dottedPen) + dc.SetBrush(wx.TRANSPARENT_BRUSH) + + # Code for CTRL-drag in C++ version commented out + + pt.CalculateNewSize(x, y) + + self.GetEventHandler().OnDrawOutline(dc, self.GetX(), self.GetY(), pt.GetNewSize()[0], pt.GetNewSize()[1]) + + def OnSizingBeginDragLeft(self, pt, x, y, keys = 0, attachment = 0): + dc = wx.ClientDC(self.GetCanvas()) + self.GetCanvas().PrepareDC(dc) + + self.Erase(dc) + + dc.SetLogicalFunction(OGLRBLF) + + bound_x, bound_y = self.GetBoundingBoxMin() + + dist = math.sqrt((x - self.GetX()) * (x - self.GetX()) + (y - self.GetY()) * (y - self.GetY())) + + pt._originalDistance = dist + pt._originalSize[0] = bound_x + pt._originalSize[1] = bound_y + + if pt._originalDistance == 0: + pt._originalDistance = 0.0001 + + dottedPen = wx.Pen(wx.Colour(0, 0, 0), 1, wx.DOT) + dc.SetPen(dottedPen) + dc.SetBrush(wx.TRANSPARENT_BRUSH) + + # Code for CTRL-drag in C++ version commented out + + pt.CalculateNewSize(x, y) + + self.GetEventHandler().OnDrawOutline(dc, self.GetX(), self.GetY(), pt.GetNewSize()[0], pt.GetNewSize()[1]) + + self._canvas.CaptureMouse() + + def OnSizingEndDragLeft(self, pt, x, y, keys = 0, attachment = 0): + dc = wx.ClientDC(self.GetCanvas()) + self.GetCanvas().PrepareDC(dc) + + if self._canvas.HasCapture(): + self._canvas.ReleaseMouse() + dc.SetLogicalFunction(wx.COPY) + + # If we're changing shape, must reset the original points + if keys & KEY_CTRL: + self.CalculateBoundingBox() + self.CalculatePolygonCentre() + else: + self.SetSize(pt.GetNewSize()[0], pt.GetNewSize()[1]) + + self.Recompute() + self.ResetControlPoints() + self.Move(dc, self.GetX(), self.GetY()) + if not self._canvas.GetQuickEditMode(): + self._canvas.Redraw(dc) + + + +class EllipseShape(Shape): + """The EllipseShape behaves similarly to the RectangleShape but is + elliptical. + + Derived from: + wxShape + """ + def __init__(self, w, h): + Shape.__init__(self) + self._width = w + self._height = h + self.SetDefaultRegionSize() + + def GetBoundingBoxMin(self): + return self._width, self._height + + def GetPerimeterPoint(self, x1, y1, x2, y2): + bound_x, bound_y = self.GetBoundingBoxMax() + + return DrawArcToEllipse(self._xpos, self._ypos, bound_x, bound_y, x2, y2, x1, y1) + + def GetWidth(self): + return self._width + + def GetHeight(self): + return self._height + + def SetWidth(self, w): + self._width = w + + def SetHeight(self, h): + self._height = h + + def OnDraw(self, dc): + if self._shadowMode != SHADOW_NONE: + if self._shadowBrush: + dc.SetBrush(self._shadowBrush) + dc.SetPen(TransparentPen) + dc.DrawEllipse(self._xpos - self.GetWidth() / 2.0 + self._shadowOffsetX, + self._ypos - self.GetHeight() / 2.0 + self._shadowOffsetY, + self.GetWidth(), self.GetHeight()) + + if self._pen: + if self._pen.GetWidth() == 0: + dc.SetPen(TransparentPen) + else: + dc.SetPen(self._pen) + if self._brush: + dc.SetBrush(self._brush) + dc.DrawEllipse(self._xpos - self.GetWidth() / 2.0, self._ypos - self.GetHeight() / 2.0, self.GetWidth(), self.GetHeight()) + + def SetSize(self, x, y, recursive = True): + self.SetAttachmentSize(x, y) + self._width = x + self._height = y + self.SetDefaultRegionSize() + + def GetNumberOfAttachments(self): + return Shape.GetNumberOfAttachments(self) + + # There are 4 attachment points on an ellipse - 0 = top, 1 = right, + # 2 = bottom, 3 = left. + def GetAttachmentPosition(self, attachment, nth = 0, no_arcs = 1, line = None): + if self._attachmentMode == ATTACHMENT_MODE_BRANCHING: + return Shape.GetAttachmentPosition(self, attachment, nth, no_arcs, line) + + if self._attachmentMode != ATTACHMENT_MODE_NONE: + top = self._ypos + self._height / 2.0 + bottom = self._ypos - self._height / 2.0 + left = self._xpos - self._width / 2.0 + right = self._xpos + self._width / 2.0 + + physicalAttachment = self.LogicalToPhysicalAttachment(attachment) + + if physicalAttachment == 0: + if self._spaceAttachments: + x = left + (nth + 1) * self._width / (no_arcs + 1.0) + else: + x = self._xpos + y = top + # We now have the point on the bounding box: but get the point + # on the ellipse by imagining a vertical line from + # (x, self._ypos - self._height - 500) to (x, self._ypos) intersecting + # the ellipse. + + return DrawArcToEllipse(self._xpos, self._ypos, self._width, self._height, x, self._ypos - self._height - 500, x, self._ypos) + elif physicalAttachment == 1: + x = right + if self._spaceAttachments: + y = bottom + (nth + 1) * self._height / (no_arcs + 1.0) + else: + y = self._ypos + return DrawArcToEllipse(self._xpos, self._ypos, self._width, self._height, self._xpos + self._width + 500, y, self._xpos, y) + elif physicalAttachment == 2: + if self._spaceAttachments: + x = left + (nth + 1) * self._width / (no_arcs + 1.0) + else: + x = self._xpos + y = bottom + return DrawArcToEllipse(self._xpos, self._ypos, self._width, self._height, x, self._ypos + self._height + 500, x, self._ypos) + elif physicalAttachment == 3: + x = left + if self._spaceAttachments: + y = bottom + (nth + 1) * self._height / (no_arcs + 1.0) + else: + y = self._ypos + return DrawArcToEllipse(self._xpos, self._ypos, self._width, self._height, self._xpos - self._width - 500, y, self._xpos, y) + else: + return Shape.GetAttachmentPosition(self, attachment, x, y, nth, no_arcs, line) + else: + return self._xpos, self._ypos + + + +class CircleShape(EllipseShape): + """An EllipseShape whose width and height are the same.""" + def __init__(self, diameter): + EllipseShape.__init__(self, diameter, diameter) + self.SetMaintainAspectRatio(True) + + def GetPerimeterPoint(self, x1, y1, x2, y2): + return FindEndForCircle(self._width / 2.0, self._xpos, self._ypos, x2, y2) + + + +class TextShape(RectangleShape): + """As wxRectangleShape, but only the text is displayed.""" + def __init__(self, width, height): + RectangleShape.__init__(self, width, height) + + def OnDraw(self, dc): + pass + + + +class ShapeRegion(object): + """Object region.""" + def __init__(self, region = None): + if region: + self._regionText = region._regionText + self._regionName = region._regionName + self._textColour = region._textColour + + self._font = region._font + self._minHeight = region._minHeight + self._minWidth = region._minWidth + self._width = region._width + self._height = region._height + self._x = region._x + self._y = region._y + + self._regionProportionX = region._regionProportionX + self._regionProportionY = region._regionProportionY + self._formatMode = region._formatMode + self._actualColourObject = region._actualColourObject + self._actualPenObject = None + self._penStyle = region._penStyle + self._penColour = region._penColour + + self.ClearText() + for line in region._formattedText: + new_line = ShapeTextLine(line.GetX(), line.GetY(), line.GetText()) + self._formattedText.append(new_line) + else: + self._regionText = "" + self._font = NormalFont + self._minHeight = 5.0 + self._minWidth = 5.0 + self._width = 0.0 + self._height = 0.0 + self._x = 0.0 + self._y = 0.0 + + self._regionProportionX = -1.0 + self._regionProportionY = -1.0 + self._formatMode = FORMAT_CENTRE_HORIZ | FORMAT_CENTRE_VERT + self._regionName = "" + self._textColour = "BLACK" + self._penColour = "BLACK" + self._penStyle = wx.SOLID + self._actualColourObject = wx.TheColourDatabase.Find("BLACK") + self._actualPenObject = None + + self._formattedText = [] + + def ClearText(self): + self._formattedText = [] + + def SetFont(self, f): + self._font = f + + def SetMinSize(self, w, h): + self._minWidth = w + self._minHeight = h + + def SetSize(self, w, h): + self._width = w + self._height = h + + def SetPosition(self, xp, yp): + self._x = xp + self._y = yp + + def SetProportions(self, xp, yp): + self._regionProportionX = xp + self._regionProportionY = yp + + def SetFormatMode(self, mode): + self._formatMode = mode + + def SetColour(self, col): + self._textColour = col + self._actualColourObject = col + + def GetActualColourObject(self): + self._actualColourObject = wx.TheColourDatabase.Find(self.GetColour()) + return self._actualColourObject + + def SetPenColour(self, col): + self._penColour = col + self._actualPenObject = None + + # Returns NULL if the pen is invisible + # (different to pen being transparent; indicates that + # region boundary should not be drawn.) + def GetActualPen(self): + if self._actualPenObject: + return self._actualPenObject + + if not self._penColour: + return None + if self._penColour=="Invisible": + return None + self._actualPenObject = wx.Pen(self._penColour, 1, self._penStyle) + return self._actualPenObject + + def SetText(self, s): + self._regionText = s + + def SetName(self, s): + self._regionName = s + + def GetText(self): + return self._regionText + + def GetFont(self): + return self._font + + def GetMinSize(self): + return self._minWidth, self._minHeight + + def GetProportion(self): + return self._regionProportionX, self._regionProportionY + + def GetSize(self): + return self._width, self._height + + def GetPosition(self): + return self._x, self._y + + def GetFormatMode(self): + return self._formatMode + + def GetName(self): + return self._regionName + + def GetColour(self): + return self._textColour + + def GetFormattedText(self): + return self._formattedText + + def GetPenColour(self): + return self._penColour + + def GetPenStyle(self): + return self._penStyle + + def SetPenStyle(self, style): + self._penStyle = style + self._actualPenObject = None + + def GetWidth(self): + return self._width + + def GetHeight(self): + return self._height + + + +class ControlPoint(RectangleShape): + def __init__(self, theCanvas, object, size, the_xoffset, the_yoffset, the_type): + RectangleShape.__init__(self, size, size) + + self._canvas = theCanvas + self._shape = object + self._xoffset = the_xoffset + self._yoffset = the_yoffset + self._type = the_type + self.SetPen(BlackForegroundPen) + self.SetBrush(wx.BLACK_BRUSH) + self._oldCursor = None + self._visible = True + self._eraseObject = True + + # Don't even attempt to draw any text - waste of time + def OnDrawContents(self, dc): + pass + + def OnDraw(self, dc): + self._xpos = self._shape.GetX() + self._xoffset + self._ypos = self._shape.GetY() + self._yoffset + RectangleShape.OnDraw(self, dc) + + def OnErase(self, dc): + RectangleShape.OnErase(self, dc) + + # Implement resizing of canvas object + def OnDragLeft(self, draw, x, y, keys = 0, attachment = 0): + self._shape.GetEventHandler().OnSizingDragLeft(self, draw, x, y, keys, attachment) + + def OnBeginDragLeft(self, x, y, keys = 0, attachment = 0): + self._shape.GetEventHandler().OnSizingBeginDragLeft(self, x, y, keys, attachment) + + def OnEndDragLeft(self, x, y, keys = 0, attachment = 0): + self._shape.GetEventHandler().OnSizingEndDragLeft(self, x, y, keys, attachment) + + def GetNumberOfAttachments(self): + return 1 + + def GetAttachmentPosition(self, attachment, nth = 0, no_arcs = 1, line = None): + return self._xpos, self._ypos + + def SetEraseObject(self, er): + self._eraseObject = er + + +class PolygonControlPoint(ControlPoint): + def __init__(self, theCanvas, object, size, vertex, the_xoffset, the_yoffset): + ControlPoint.__init__(self, theCanvas, object, size, the_xoffset, the_yoffset, 0) + self._polygonVertex = vertex + self._originalDistance = 0.0 + self._newSize = wx.RealPoint() + self._originalSize = wx.RealPoint() + + def GetNewSize(self): + return self._newSize + + # Calculate what new size would be, at end of resize + def CalculateNewSize(self, x, y): + bound_x, bound_y = self.GetShape().GetBoundingBoxMax() + dist = math.sqrt((x - self._shape.GetX()) * (x - self._shape.GetX()) + (y - self._shape.GetY()) * (y - self._shape.GetY())) + + self._newSize[0] = dist / self._originalDistance * self._originalSize[0] + self._newSize[1] = dist / self._originalDistance * self._originalSize[1] + + # Implement resizing polygon or moving the vertex + def OnDragLeft(self, draw, x, y, keys = 0, attachment = 0): + self._shape.GetEventHandler().OnSizingDragLeft(self, draw, x, y, keys, attachment) + + def OnBeginDragLeft(self, x, y, keys = 0, attachment = 0): + self._shape.GetEventHandler().OnSizingBeginDragLeft(self, x, y, keys, attachment) + + def OnEndDragLeft(self, x, y, keys = 0, attachment = 0): + self._shape.GetEventHandler().OnSizingEndDragLeft(self, x, y, keys, attachment) + +from _canvas import * +from _lines import * +from _composit import * diff --git a/wx/lib/ogl/_bmpshape.py b/wx/lib/ogl/_bmpshape.py new file mode 100644 index 00000000..68e2e2e8 --- /dev/null +++ b/wx/lib/ogl/_bmpshape.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +#---------------------------------------------------------------------------- +# Name: bmpshape.py +# Purpose: Bitmap shape +# +# Author: Pierre Hjälm (from C++ original by Julian Smart) +# +# Created: 2004-05-08 +# RCS-ID: $Id$ +# Copyright: (c) 2004 Pierre Hjälm - 1998 Julian Smart +# Licence: wxWindows license +#---------------------------------------------------------------------------- + +from _basic import RectangleShape + + +class BitmapShape(RectangleShape): + """Draws a bitmap (non-resizable).""" + def __init__(self): + RectangleShape.__init__(self, 100, 50) + self._filename = "" + + def OnDraw(self, dc): + if not self._bitmap.Ok(): + return + + x = self._xpos - self._bitmap.GetWidth() / 2.0 + y = self._ypos - self._bitmap.GetHeight() / 2.0 + dc.DrawBitmap(self._bitmap, x, y, True) + + def SetSize(self, w, h, recursive = True): + if self._bitmap.Ok(): + w = self._bitmap.GetWidth() + h = self._bitmap.GetHeight() + + self.SetAttachmentSize(w, h) + + self._width = w + self._height = h + + self.SetDefaultRegionSize() + + def GetBitmap(self): + """Return a the bitmap associated with this shape.""" + return self._bitmap + + def SetBitmap(self, bitmap): + """Set the bitmap associated with this shape. + + You can delete the bitmap from the calling application, since + reference counting will take care of holding on to the internal bitmap + data. + """ + self._bitmap = bitmap + if self._bitmap.Ok(): + self.SetSize(self._bitmap.GetWidth(), self._bitmap.GetHeight()) + + def SetFilename(self, f): + """Set the bitmap filename.""" + self._filename = f + + def GetFilename(self): + """Return the bitmap filename.""" + return self._filename diff --git a/wx/lib/ogl/_canvas.py b/wx/lib/ogl/_canvas.py new file mode 100644 index 00000000..b04b69fe --- /dev/null +++ b/wx/lib/ogl/_canvas.py @@ -0,0 +1,363 @@ +# -*- coding: utf-8 -*- +#---------------------------------------------------------------------------- +# Name: canvas.py +# Purpose: The canvas class +# +# Author: Pierre Hjälm (from C++ original by Julian Smart) +# +# Created: 2004-05-08 +# RCS-ID: $Id$ +# Copyright: (c) 2004 Pierre Hjälm - 1998 Julian Smart +# Licence: wxWindows license +#---------------------------------------------------------------------------- + +import wx +from _lines import LineShape +from _composit import * + +NoDragging, StartDraggingLeft, ContinueDraggingLeft, StartDraggingRight, ContinueDraggingRight = 0, 1, 2, 3, 4 + +KEY_SHIFT, KEY_CTRL = 1, 2 + + + +# Helper function: True if 'contains' wholly contains 'contained'. +def WhollyContains(contains, contained): + xp1, yp1 = contains.GetX(), contains.GetY() + xp2, yp2 = contained.GetX(), contained.GetY() + + w1, h1 = contains.GetBoundingBoxMax() + w2, h2 = contained.GetBoundingBoxMax() + + left1 = xp1 - w1 / 2.0 + top1 = yp1 - h1 / 2.0 + right1 = xp1 + w1 / 2.0 + bottom1 = yp1 + h1 / 2.0 + + left2 = xp2 - w2 / 2.0 + top2 = yp2 - h2 / 2.0 + right2 = xp2 + w2 / 2.0 + bottom2 = yp2 + h2 / 2.0 + + return ((left1 <= left2) and (top1 <= top2) and (right1 >= right2) and (bottom1 >= bottom2)) + + + +class ShapeCanvas(wx.ScrolledWindow): + def __init__(self, parent = None, id = -1, pos = wx.DefaultPosition, size = wx.DefaultSize, style = wx.BORDER, name = "ShapeCanvas"): + wx.ScrolledWindow.__init__(self, parent, id, pos, size, style, name) + + self._shapeDiagram = None + self._dragState = NoDragging + self._draggedShape = None + self._oldDragX = 0 + self._oldDragY = 0 + self._firstDragX = 0 + self._firstDragY = 0 + self._checkTolerance = True + + wx.EVT_PAINT(self, self.OnPaint) + wx.EVT_MOUSE_EVENTS(self, self.OnMouseEvent) + + def SetDiagram(self, diag): + self._shapeDiagram = diag + + def GetDiagram(self): + return self._shapeDiagram + + def OnPaint(self, evt): + dc = wx.PaintDC(self) + self.PrepareDC(dc) + + dc.SetBackground(wx.Brush(self.GetBackgroundColour(), wx.SOLID)) + dc.Clear() + + if self.GetDiagram(): + self.GetDiagram().Redraw(dc) + + def OnMouseEvent(self, evt): + dc = wx.ClientDC(self) + self.PrepareDC(dc) + + x, y = evt.GetLogicalPosition(dc) + + keys = 0 + if evt.ShiftDown(): + keys |= KEY_SHIFT + if evt.ControlDown(): + keys |= KEY_CTRL + + dragging = evt.Dragging() + + # Check if we're within the tolerance for mouse movements. + # If we're very close to the position we started dragging + # from, this may not be an intentional drag at all. + if dragging: + if self._checkTolerance: + # the difference between two logical coordinates is a logical coordinate + dx = abs(x - self._firstDragX) + dy = abs(y - self._firstDragY) + toler = self.GetDiagram().GetMouseTolerance() + if (dx <= toler) and (dy <= toler): + return + # If we've ignored the tolerance once, then ALWAYS ignore + # tolerance in this drag, even if we come back within + # the tolerance range. + self._checkTolerance = False + + # Dragging - note that the effect of dragging is left entirely up + # to the object, so no movement is done unless explicitly done by + # object. + if dragging and self._draggedShape and self._dragState == StartDraggingLeft: + self._dragState = ContinueDraggingLeft + + # If the object isn't m_draggable, transfer message to canvas + if self._draggedShape.Draggable(): + self._draggedShape.GetEventHandler().OnBeginDragLeft(x, y, keys, self._draggedAttachment) + else: + self._draggedShape = None + self.OnBeginDragLeft(x, y, keys) + + self._oldDragX, self._oldDragY = x, y + + elif dragging and self._draggedShape and self._dragState == ContinueDraggingLeft: + # Continue dragging + self._draggedShape.GetEventHandler().OnDragLeft(False, self._oldDragX, self._oldDragY, keys, self._draggedAttachment) + self._draggedShape.GetEventHandler().OnDragLeft(True, x, y, keys, self._draggedAttachment) + self._oldDragX, self._oldDragY = x, y + + elif evt.LeftUp() and self._draggedShape and self._dragState == ContinueDraggingLeft: + self._dragState = NoDragging + self._checkTolerance = True + + self._draggedShape.GetEventHandler().OnDragLeft(False, self._oldDragX, self._oldDragY, keys, self._draggedAttachment) + self._draggedShape.GetEventHandler().OnEndDragLeft(x, y, keys, self._draggedAttachment) + self._draggedShape = None + + elif dragging and self._draggedShape and self._dragState == StartDraggingRight: + self._dragState = ContinueDraggingRight + if self._draggedShape.Draggable: + self._draggedShape.GetEventHandler().OnBeginDragRight(x, y, keys, self._draggedAttachment) + else: + self._draggedShape = None + self.OnBeginDragRight(x, y, keys) + self._oldDragX, self._oldDragY = x, y + + elif dragging and self._draggedShape and self._dragState == ContinueDraggingRight: + # Continue dragging + self._draggedShape.GetEventHandler().OnDragRight(False, self._oldDragX, self._oldDragY, keys, self._draggedAttachment) + self._draggedShape.GetEventHandler().OnDragRight(True, x, y, keys, self._draggedAttachment) + self._oldDragX, self._oldDragY = x, y + + elif evt.RightUp() and self._draggedShape and self._dragState == ContinueDraggingRight: + self._dragState = NoDragging + self._checkTolerance = True + + self._draggedShape.GetEventHandler().OnDragRight(False, self._oldDragX, self._oldDragY, keys, self._draggedAttachment) + self._draggedShape.GetEventHandler().OnEndDragRight(x, y, keys, self._draggedAttachment) + self._draggedShape = None + + # All following events sent to canvas, not object + elif dragging and not self._draggedShape and self._dragState == StartDraggingLeft: + self._dragState = ContinueDraggingLeft + self.OnBeginDragLeft(x, y, keys) + self._oldDragX, self._oldDragY = x, y + + elif dragging and not self._draggedShape and self._dragState == ContinueDraggingLeft: + # Continue dragging + self.OnDragLeft(False, self._oldDragX, self._oldDragY, keys) + self.OnDragLeft(True, x, y, keys) + self._oldDragX, self._oldDragY = x, y + + elif evt.LeftUp() and not self._draggedShape and self._dragState == ContinueDraggingLeft: + self._dragState = NoDragging + self._checkTolerance = True + + self.OnDragLeft(False, self._oldDragX, self._oldDragY, keys) + self.OnEndDragLeft(x, y, keys) + self._draggedShape = None + + elif dragging and not self._draggedShape and self._dragState == StartDraggingRight: + self._dragState = ContinueDraggingRight + self.OnBeginDragRight(x, y, keys) + self._oldDragX, self._oldDragY = x, y + + elif dragging and not self._draggedShape and self._dragState == ContinueDraggingRight: + # Continue dragging + self.OnDragRight(False, self._oldDragX, self._oldDragY, keys) + self.OnDragRight(True, x, y, keys) + self._oldDragX, self._oldDragY = x, y + + elif evt.RightUp() and not self._draggedShape and self._dragState == ContinueDraggingRight: + self._dragState = NoDragging + self._checkTolerance = True + + self.OnDragRight(False, self._oldDragX, self._oldDragY, keys) + self.OnEndDragRight(x, y, keys) + self._draggedShape = None + + # Non-dragging events + elif evt.IsButton(): + self._checkTolerance = True + + # Find the nearest object + attachment = 0 + + nearest_object, attachment = self.FindShape(x, y) + if nearest_object: # Object event + if evt.LeftDown(): + self._draggedShape = nearest_object + self._draggedAttachment = attachment + self._dragState = StartDraggingLeft + self._firstDragX = x + self._firstDragY = y + + elif evt.LeftUp(): + # N.B. Only register a click if the same object was + # identified for down *and* up. + if nearest_object == self._draggedShape: + nearest_object.GetEventHandler().OnLeftClick(x, y, keys, attachment) + self._draggedShape = None + self._dragState = NoDragging + + elif evt.LeftDClick(): + nearest_object.GetEventHandler().OnLeftDoubleClick(x, y, keys, attachment) + self._draggedShape = None + self._dragState = NoDragging + + elif evt.RightDown(): + self._draggedShape = nearest_object + self._draggedAttachment = attachment + self._dragState = StartDraggingRight + self._firstDragX = x + self._firstDragY = y + + elif evt.RightUp(): + if nearest_object == self._draggedShape: + nearest_object.GetEventHandler().OnRightClick(x, y, keys, attachment) + self._draggedShape = None + self._dragState = NoDragging + + else: # Canvas event + if evt.LeftDown(): + self._draggedShape = None + self._dragState = StartDraggingLeft + self._firstDragX = x + self._firstDragY = y + + elif evt.LeftUp(): + self.OnLeftClick(x, y, keys) + self._draggedShape = None + self._dragState = NoDragging + + elif evt.RightDown(): + self._draggedShape = None + self._dragState = StartDraggingRight + self._firstDragX = x + self._firstDragY = y + + elif evt.RightUp(): + self.OnRightClick(x, y, keys) + self._draggedShape = None + self._dragState = NoDragging + + def FindShape(self, x, y, info = None, notObject = None): + nearest = 100000.0 + nearest_attachment = 0 + nearest_object = None + + # Go backward through the object list, since we want: + # (a) to have the control points drawn LAST to overlay + # the other objects + # (b) to find the control points FIRST if they exist + + rl = self.GetDiagram().GetShapeList()[:] + rl.reverse() + for object in rl: + # First pass for lines, which might be inside a container, so we + # want lines to take priority over containers. This first loop + # could fail if we clickout side a line, so then we'll + # try other shapes. + if object.IsShown() and \ + isinstance(object, LineShape) and \ + object.HitTest(x, y) and \ + ((info == None) or isinstance(object, info)) and \ + (not notObject or not notObject.HasDescendant(object)): + temp_attachment, dist = object.HitTest(x, y) + # A line is trickier to spot than a normal object. + # For a line, since it's the diagonal of the box + # we use for the hit test, we may have several + # lines in the box and therefore we need to be able + # to specify the nearest point to the centre of the line + # as our hit criterion, to give the user some room for + # manouevre. + if dist < nearest: + nearest = dist + nearest_object = object + nearest_attachment = temp_attachment + + for object in rl: + # On second pass, only ever consider non-composites or + # divisions. If children want to pass up control to + # the composite, that's up to them. + if (object.IsShown() and + (isinstance(object, DivisionShape) or + not isinstance(object, CompositeShape)) and + object.HitTest(x, y) and + (info == None or isinstance(object, info)) and + (not notObject or not notObject.HasDescendant(object))): + temp_attachment, dist = object.HitTest(x, y) + if not isinstance(object, LineShape): + # If we've hit a container, and we have already + # found a line in the first pass, then ignore + # the container in case the line is in the container. + # Check for division in case line straddles divisions + # (i.e. is not wholly contained). + if not nearest_object or not (isinstance(object, DivisionShape) or WhollyContains(object, nearest_object)): + nearest_object = object + nearest_attachment = temp_attachment + break + + return nearest_object, nearest_attachment + + def AddShape(self, object, addAfter = None): + self.GetDiagram().AddShape(object, addAfter) + + def InsertShape(self, object): + self.GetDiagram().InsertShape(object) + + def RemoveShape(self, object): + self.GetDiagram().RemoveShape(object) + + def GetQuickEditMode(self): + return self.GetDiagram().GetQuickEditMode() + + def Redraw(self, dc): + self.GetDiagram().Redraw(dc) + + def Snap(self, x, y): + return self.GetDiagram().Snap(x, y) + + def OnLeftClick(self, x, y, keys = 0): + pass + + def OnRightClick(self, x, y, keys = 0): + pass + + def OnDragLeft(self, draw, x, y, keys = 0): + pass + + def OnBeginDragLeft(self, x, y, keys = 0): + pass + + def OnEndDragLeft(self, x, y, keys = 0): + pass + + def OnDragRight(self, draw, x, y, keys = 0): + pass + + def OnBeginDragRight(self, x, y, keys = 0): + pass + + def OnEndDragRight(self, x, y, keys = 0): + pass diff --git a/wx/lib/ogl/_composit.py b/wx/lib/ogl/_composit.py new file mode 100644 index 00000000..013e06c7 --- /dev/null +++ b/wx/lib/ogl/_composit.py @@ -0,0 +1,1442 @@ +# -*- coding: utf-8 -*- +#---------------------------------------------------------------------------- +# Name: composit.py +# Purpose: Composite class +# +# Author: Pierre Hjälm (from C++ original by Julian Smart) +# +# Created: 2004-05-08 +# RCS-ID: $Id$ +# Copyright: (c) 2004 Pierre Hjälm - 1998 Julian Smart +# Licence: wxWindows license +#---------------------------------------------------------------------------- + +import sys +import wx + +from _basic import RectangleShape, Shape, ControlPoint +from _oglmisc import * + +KEY_SHIFT, KEY_CTRL = 1, 2 + +_objectStartX = 0.0 +_objectStartY = 0.0 + +CONSTRAINT_CENTRED_VERTICALLY = 1 +CONSTRAINT_CENTRED_HORIZONTALLY = 2 +CONSTRAINT_CENTRED_BOTH = 3 +CONSTRAINT_LEFT_OF = 4 +CONSTRAINT_RIGHT_OF = 5 +CONSTRAINT_ABOVE = 6 +CONSTRAINT_BELOW = 7 +CONSTRAINT_ALIGNED_TOP = 8 +CONSTRAINT_ALIGNED_BOTTOM = 9 +CONSTRAINT_ALIGNED_LEFT = 10 +CONSTRAINT_ALIGNED_RIGHT = 11 + +# Like aligned, but with the objects centred on the respective edge +# of the reference object. +CONSTRAINT_MIDALIGNED_TOP = 12 +CONSTRAINT_MIDALIGNED_BOTTOM = 13 +CONSTRAINT_MIDALIGNED_LEFT = 14 +CONSTRAINT_MIDALIGNED_RIGHT = 15 + + +# Backwards compatibility names. These should be removed eventually. +gyCONSTRAINT_CENTRED_VERTICALLY = CONSTRAINT_CENTRED_VERTICALLY +gyCONSTRAINT_CENTRED_HORIZONTALLY = CONSTRAINT_CENTRED_HORIZONTALLY +gyCONSTRAINT_CENTRED_BOTH = CONSTRAINT_CENTRED_BOTH +gyCONSTRAINT_LEFT_OF = CONSTRAINT_LEFT_OF +gyCONSTRAINT_RIGHT_OF = CONSTRAINT_RIGHT_OF +gyCONSTRAINT_ABOVE = CONSTRAINT_ABOVE +gyCONSTRAINT_BELOW = CONSTRAINT_BELOW +gyCONSTRAINT_ALIGNED_TOP = CONSTRAINT_ALIGNED_TOP +gyCONSTRAINT_ALIGNED_BOTTOM = CONSTRAINT_ALIGNED_BOTTOM +gyCONSTRAINT_ALIGNED_LEFT = CONSTRAINT_ALIGNED_LEFT +gyCONSTRAINT_ALIGNED_RIGHT = CONSTRAINT_ALIGNED_RIGHT +gyCONSTRAINT_MIDALIGNED_TOP = CONSTRAINT_MIDALIGNED_TOP +gyCONSTRAINT_MIDALIGNED_BOTTOM = CONSTRAINT_MIDALIGNED_BOTTOM +gyCONSTRAINT_MIDALIGNED_LEFT = CONSTRAINT_MIDALIGNED_LEFT +gyCONSTRAINT_MIDALIGNED_RIGHT = CONSTRAINT_MIDALIGNED_RIGHT + + + +class ConstraintType(object): + def __init__(self, theType, theName, thePhrase): + self._type = theType + self._name = theName + self._phrase = thePhrase + + + +ConstraintTypes = [ + [CONSTRAINT_CENTRED_VERTICALLY, + ConstraintType(CONSTRAINT_CENTRED_VERTICALLY, "Centre vertically", "centred vertically w.r.t.")], + + [CONSTRAINT_CENTRED_HORIZONTALLY, + ConstraintType(CONSTRAINT_CENTRED_HORIZONTALLY, "Centre horizontally", "centred horizontally w.r.t.")], + + [CONSTRAINT_CENTRED_BOTH, + ConstraintType(CONSTRAINT_CENTRED_BOTH, "Centre", "centred w.r.t.")], + + [CONSTRAINT_LEFT_OF, + ConstraintType(CONSTRAINT_LEFT_OF, "Left of", "left of")], + + [CONSTRAINT_RIGHT_OF, + ConstraintType(CONSTRAINT_RIGHT_OF, "Right of", "right of")], + + [CONSTRAINT_ABOVE, + ConstraintType(CONSTRAINT_ABOVE, "Above", "above")], + + [CONSTRAINT_BELOW, + ConstraintType(CONSTRAINT_BELOW, "Below", "below")], + + # Alignment + [CONSTRAINT_ALIGNED_TOP, + ConstraintType(CONSTRAINT_ALIGNED_TOP, "Top-aligned", "aligned to the top of")], + + [CONSTRAINT_ALIGNED_BOTTOM, + ConstraintType(CONSTRAINT_ALIGNED_BOTTOM, "Bottom-aligned", "aligned to the bottom of")], + + [CONSTRAINT_ALIGNED_LEFT, + ConstraintType(CONSTRAINT_ALIGNED_LEFT, "Left-aligned", "aligned to the left of")], + + [CONSTRAINT_ALIGNED_RIGHT, + ConstraintType(CONSTRAINT_ALIGNED_RIGHT, "Right-aligned", "aligned to the right of")], + + # Mid-alignment + [CONSTRAINT_MIDALIGNED_TOP, + ConstraintType(CONSTRAINT_MIDALIGNED_TOP, "Top-midaligned", "centred on the top of")], + + [CONSTRAINT_MIDALIGNED_BOTTOM, + ConstraintType(CONSTRAINT_MIDALIGNED_BOTTOM, "Bottom-midaligned", "centred on the bottom of")], + + [CONSTRAINT_MIDALIGNED_LEFT, + ConstraintType(CONSTRAINT_MIDALIGNED_LEFT, "Left-midaligned", "centred on the left of")], + + [CONSTRAINT_MIDALIGNED_RIGHT, + ConstraintType(CONSTRAINT_MIDALIGNED_RIGHT, "Right-midaligned", "centred on the right of")] + ] + + + + +class Constraint(object): + """A Constraint object helps specify how child shapes are laid out with + respect to siblings and parents. + + Derived from: + wxObject + """ + def __init__(self, type, constraining, constrained): + self._xSpacing = 0.0 + self._ySpacing = 0.0 + + self._constraintType = type + self._constrainingObject = constraining + + self._constraintId = 0 + self._constraintName = "noname" + + self._constrainedObjects = constrained[:] + + def __repr__(self): + return "<%s.%s>" % (self.__class__.__module__, self.__class__.__name__) + + def SetSpacing(self, x, y): + """Sets the horizontal and vertical spacing for the constraint.""" + self._xSpacing = x + self._ySpacing = y + + def Equals(self, a, b): + """Return TRUE if x and y are approximately equal (for the purposes + of evaluating the constraint). + """ + marg = 0.5 + + return b <= a + marg and b >= a - marg + + def Evaluate(self): + """Evaluate this constraint and return TRUE if anything changed.""" + maxWidth, maxHeight = self._constrainingObject.GetBoundingBoxMax() + minWidth, minHeight = self._constrainingObject.GetBoundingBoxMin() + x = self._constrainingObject.GetX() + y = self._constrainingObject.GetY() + + dc = wx.ClientDC(self._constrainingObject.GetCanvas()) + self._constrainingObject.GetCanvas().PrepareDC(dc) + + if self._constraintType == CONSTRAINT_CENTRED_VERTICALLY: + n = len(self._constrainedObjects) + totalObjectHeight = 0.0 + for constrainedObject in self._constrainedObjects: + width2, height2 = constrainedObject.GetBoundingBoxMax() + totalObjectHeight += height2 + + # Check if within the constraining object... + if totalObjectHeight + (n + 1) * self._ySpacing <= minHeight: + spacingY = (minHeight - totalObjectHeight) / (n + 1.0) + startY = y - minHeight / 2.0 + else: # Otherwise, use default spacing + spacingY = self._ySpacing + startY = y - (totalObjectHeight + (n + 1) * spacingY) / 2.0 + + # Now position the objects + changed = False + for constrainedObject in self._constrainedObjects: + width2, height2 = constrainedObject.GetBoundingBoxMax() + startY += spacingY + height2 / 2.0 + if not self.Equals(startY, constrainedObject.GetY()): + constrainedObject.Move(dc, constrainedObject.GetX(), startY, False) + changed = True + startY += height2 / 2.0 + return changed + elif self._constraintType == CONSTRAINT_CENTRED_HORIZONTALLY: + n = len(self._constrainedObjects) + totalObjectWidth = 0.0 + for constrainedObject in self._constrainedObjects: + width2, height2 = constrainedObject.GetBoundingBoxMax() + totalObjectWidth += width2 + + # Check if within the constraining object... + if totalObjectWidth + (n + 1) * self._xSpacing <= minWidth: + spacingX = (minWidth - totalObjectWidth) / (n + 1.0) + startX = x - minWidth / 2.0 + else: # Otherwise, use default spacing + spacingX = self._xSpacing + startX = x - (totalObjectWidth + (n + 1) * spacingX) / 2.0 + + # Now position the objects + changed = False + for constrainedObject in self._constrainedObjects: + width2, height2 = constrainedObject.GetBoundingBoxMax() + startX += spacingX + width2 / 2.0 + if not self.Equals(startX, constrainedObject.GetX()): + constrainedObject.Move(dc, startX, constrainedObject.GetY(), False) + changed = True + startX += width2 / 2.0 + return changed + elif self._constraintType == CONSTRAINT_CENTRED_BOTH: + n = len(self._constrainedObjects) + totalObjectWidth = 0.0 + totalObjectHeight = 0.0 + + for constrainedObject in self._constrainedObjects: + width2, height2 = constrainedObject.GetBoundingBoxMax() + totalObjectWidth += width2 + totalObjectHeight += height2 + + # Check if within the constraining object... + if totalObjectHeight + (n + 1) * self._xSpacing <= minWidth: + spacingX = (minWidth - totalObjectWidth) / (n + 1.0) + startX = x - minWidth / 2.0 + else: # Otherwise, use default spacing + spacingX = self._xSpacing + startX = x - (totalObjectWidth + (n + 1) * spacingX) / 2.0 + + # Check if within the constraining object... + if totalObjectHeight + (n + 1) * self._ySpacing <= minHeight: + spacingY = (minHeight - totalObjectHeight) / (n + 1.0) + startY = y - minHeight / 2.0 + else: # Otherwise, use default spacing + spacingY = self._ySpacing + startY = y - (totalObjectHeight + (n + 1) * spacingY) / 2.0 + + # Now position the objects + changed = False + for constrainedObject in self._constrainedObjects: + width2, height2 = constrainedObject.GetBoundingBoxMax() + startX += spacingX + width2 / 2.0 + startY += spacingY + height2 / 2.0 + + if not self.Equals(startX, constrainedObject.GetX()) or not self.Equals(startY, constrainedObject.GetY()): + constrainedObject.Move(dc, startX, startY, False) + changed = True + + startX += width2 / 2.0 + startY += height2 / 2.0 + return changed + elif self._constraintType == CONSTRAINT_LEFT_OF: + changed = False + for constrainedObject in self._constrainedObjects: + width2, height2 = constrainedObject.GetBoundingBoxMax() + + x3 = x - minWidth / 2.0 - width2 / 2.0 - self._xSpacing + if not self.Equals(x3, constrainedObject.GetX()): + changed = True + constrainedObject.Move(dc, x3, constrainedObject.GetY(), False) + return changed + elif self._constraintType == CONSTRAINT_RIGHT_OF: + changed = False + + for constrainedObject in self._constrainedObjects: + width2, height2 = constrainedObject.GetBoundingBoxMax() + x3 = x + minWidth / 2.0 + width2 / 2.0 + self._xSpacing + if not self.Equals(x3, constrainedObject.GetX()): + constrainedObject.Move(dc, x3, constrainedObject.GetY(), False) + changed = True + return changed + elif self._constraintType == CONSTRAINT_ABOVE: + changed = False + + for constrainedObject in self._constrainedObjects: + width2, height2 = constrainedObject.GetBoundingBoxMax() + + y3 = y - minHeight / 2.0 - height2 / 2.0 - self._ySpacing + if not self.Equals(y3, constrainedObject.GetY()): + changed = True + constrainedObject.Move(dc, constrainedObject.GetX(), y3, False) + return changed + elif self._constraintType == CONSTRAINT_BELOW: + changed = False + + for constrainedObject in self._constrainedObjects: + width2, height2 = constrainedObject.GetBoundingBoxMax() + + y3 = y + minHeight / 2.0 + height2 / 2.0 + self._ySpacing + if not self.Equals(y3, constrainedObject.GetY()): + changed = True + constrainedObject.Move(dc, constrainedObject.GetX(), y3, False) + return changed + elif self._constraintType == CONSTRAINT_ALIGNED_LEFT: + changed = False + for constrainedObject in self._constrainedObjects: + width2, height2 = constrainedObject.GetBoundingBoxMax() + x3 = x - minWidth / 2.0 + width2 / 2.0 + self._xSpacing + if not self.Equals(x3, constrainedObject.GetX()): + changed = True + constrainedObject.Move(dc, x3, constrainedObject.GetY(), False) + return changed + elif self._constraintType == CONSTRAINT_ALIGNED_RIGHT: + changed = False + for constrainedObject in self._constrainedObjects: + width2, height2 = constrainedObject.GetBoundingBoxMax() + x3 = x + minWidth / 2.0 - width2 / 2.0 - self._xSpacing + if not self.Equals(x3, constrainedObject.GetX()): + changed = True + constrainedObject.Move(dc, x3, constrainedObject.GetY(), False) + return changed + elif self._constraintType == CONSTRAINT_ALIGNED_TOP: + changed = False + for constrainedObject in self._constrainedObjects: + width2, height2 = constrainedObject.GetBoundingBoxMax() + y3 = y - minHeight / 2.0 + height2 / 2.0 + self._ySpacing + if not self.Equals(y3, constrainedObject.GetY()): + changed = True + constrainedObject.Move(dc, constrainedObject.GetX(), y3, False) + return changed + elif self._constraintType == CONSTRAINT_ALIGNED_BOTTOM: + changed = False + for constrainedObject in self._constrainedObjects: + width2, height2 = constrainedObject.GetBoundingBoxMax() + y3 = y + minHeight / 2.0 - height2 / 2.0 - self._ySpacing + if not self.Equals(y3, constrainedObject.GetY()): + changed = True + constrainedObject.Move(dc, constrainedObject.GetX(), y3, False) + return changed + elif self._constraintType == CONSTRAINT_MIDALIGNED_LEFT: + changed = False + for constrainedObject in self._constrainedObjects: + x3 = x - minWidth / 2.0 + if not self.Equals(x3, constrainedObject.GetX()): + changed = True + constrainedObject.Move(dc, x3, constrainedObject.GetY(), False) + return changed + elif self._constraintType == CONSTRAINT_MIDALIGNED_RIGHT: + changed = False + for constrainedObject in self._constrainedObjects: + x3 = x + minWidth / 2.0 + if not self.Equals(x3, constrainedObject.GetX()): + changed = True + constrainedObject.Move(dc, x3, constrainedObject.GetY(), False) + return changed + elif self._constraintType == CONSTRAINT_MIDALIGNED_TOP: + changed = False + for constrainedObject in self._constrainedObjects: + y3 = y - minHeight / 2.0 + if not self.Equals(y3, constrainedObject.GetY()): + changed = True + constrainedObject.Move(dc, constrainedObject.GetX(), y3, False) + return changed + elif self._constraintType == CONSTRAINT_MIDALIGNED_BOTTOM: + changed = False + for constrainedObject in self._constrainedObjects: + y3 = y + minHeight / 2.0 + if not self.Equals(y3, constrainedObject.GetY()): + changed = True + constrainedObject.Move(dc, constrainedObject.GetX(), y3, False) + return changed + + return False + +OGLConstraint = wx.deprecated(Constraint, + "The OGLConstraint name is deprecated, use `ogl.Constraint` instead.") + + +class CompositeShape(RectangleShape): + """This is an object with a list of child objects, and a list of size + and positioning constraints between the children. + + Derived from: + wxRectangleShape + """ + def __init__(self): + RectangleShape.__init__(self, 100.0, 100.0) + + self._oldX = self._xpos + self._oldY = self._ypos + + self._constraints = [] + self._divisions = [] # In case it's a container + + def OnDraw(self, dc): + x1 = self._xpos - self._width / 2.0 + y1 = self._ypos - self._height / 2.0 + + if self._shadowMode != SHADOW_NONE: + if self._shadowBrush: + dc.SetBrush(self._shadowBrush) + dc.SetPen(wx.Pen(wx.WHITE, 1, wx.TRANSPARENT)) + + if self._cornerRadius: + dc.DrawRoundedRectangle(x1 + self._shadowOffsetX, y1 + self._shadowOffsetY, self._width, self._height, self._cornerRadius) + else: + dc.DrawRectangle(x1 + self._shadowOffsetX, y1 + self._shadowOffsetY, self._width, self._height) + + # For debug purposes /pi + #dc.DrawRectangle(x1, y1, self._width, self._height) + + def OnDrawContents(self, dc): + for object in self._children: + object.Draw(dc) + object.DrawLinks(dc) + + Shape.OnDrawContents(self, dc) + + def OnMovePre(self, dc, x, y, old_x, old_y, display = True): + diffX = x - old_x + diffY = y - old_y + + for object in self._children: + object.Erase(dc) + object.Move(dc, object.GetX() + diffX, object.GetY() + diffY, display) + + return True + + def OnErase(self, dc): + RectangleShape.OnErase(self, dc) + for object in self._children: + object.Erase(dc) + + def OnDragLeft(self, draw, x, y, keys = 0, attachment = 0): + xx, yy = self._canvas.Snap(x, y) + offsetX = xx - _objectStartX + offsetY = yy - _objectStartY + + dc = wx.ClientDC(self.GetCanvas()) + self.GetCanvas().PrepareDC(dc) + + dc.SetLogicalFunction(OGLRBLF) + dottedPen = wx.Pen(wx.Colour(0, 0, 0), 1, wx.DOT) + dc.SetPen(dottedPen) + dc.SetBrush(wx.TRANSPARENT_BRUSH) + + self.GetEventHandler().OnDrawOutline(dc, self.GetX() + offsetX, self.GetY() + offsetY, self.GetWidth(), self.GetHeight()) + + def OnBeginDragLeft(self, x, y, keys = 0, attachment = 0): + global _objectStartX, _objectStartY + + _objectStartX = x + _objectStartY = y + + dc = wx.ClientDC(self.GetCanvas()) + self.GetCanvas().PrepareDC(dc) + + #self.Erase(dc) + + dc.SetLogicalFunction(OGLRBLF) + dottedPen = wx.Pen(wx.Colour(0, 0, 0), 1, wx.DOT) + dc.SetPen(dottedPen) + dc.SetBrush(wx.TRANSPARENT_BRUSH) + self._canvas.CaptureMouse() + + xx, yy = self._canvas.Snap(x, y) + offsetX = xx - _objectStartX + offsetY = yy - _objectStartY + + self.GetEventHandler().OnDrawOutline(dc, self.GetX() + offsetX, self.GetY() + offsetY, self.GetWidth(), self.GetHeight()) + + def OnEndDragLeft(self, x, y, keys = 0, attachment = 0): + dc = wx.ClientDC(self.GetCanvas()) + self.GetCanvas().PrepareDC(dc) + + if self._canvas.HasCapture(): + self._canvas.ReleaseMouse() + + if not self._draggable: + if self._parent: + self._parent.GetEventHandler().OnEndDragLeft(x, y, keys, 0) + return + + self.Erase(dc) + + dc.SetLogicalFunction(wx.COPY) + + xx, yy = self._canvas.Snap(x, y) + offsetX = xx - _objectStartX + offsetY = yy - _objectStartY + + self.Move(dc, self.GetX() + offsetX, self.GetY() + offsetY) + + if self._canvas and not self._canvas.GetQuickEditMode(): + self._canvas.Redraw(dc) + + def OnRightClick(self, x, y, keys = 0, attachment = 0): + # If we get a ctrl-right click, this means send the message to + # the division, so we can invoke a user interface for dealing + # with regions. + if keys & KEY_CTRL: + for division in self._divisions: + hit = division.HitTest(x, y) + if hit: + division.GetEventHandler().OnRightClick(x, y, keys, hit[0]) + break + + def SetSize(self, w, h, recursive = True): + self.SetAttachmentSize(w, h) + + xScale = float(w) / max(1, self.GetWidth()) + yScale = float(h) / max(1, self.GetHeight()) + + self._width = w + self._height = h + + if not recursive: + return + + dc = wx.ClientDC(self.GetCanvas()) + self.GetCanvas().PrepareDC(dc) + + for object in self._children: + # Scale the position first + newX = (object.GetX() - self.GetX()) * xScale + self.GetX() + newY = (object.GetY() - self.GetY()) * yScale + self.GetY() + object.Show(False) + object.Move(dc, newX, newY) + object.Show(True) + + # Now set the scaled size + xbound, ybound = object.GetBoundingBoxMax() + if not object.GetFixedWidth(): + xbound *= xScale + if not object.GetFixedHeight(): + ybound *= yScale + object.SetSize(xbound, ybound) + + self.SetDefaultRegionSize() + + def AddChild(self, child, addAfter = None): + """Adds a child shape to the composite. + + If addAfter is not None, the shape will be added after this shape. + """ + self._children.append(child) + child.SetParent(self) + if self._canvas: + # Ensure we add at the right position + if addAfter: + child.RemoveFromCanvas(self._canvas) + child.AddToCanvas(self._canvas, addAfter) + + def RemoveChild(self, child): + """Removes the child from the composite and any constraint + relationships, but does not delete the child. + """ + if child in self._children: + self._children.remove(child) + if child in self._divisions: + self._divisions.remove(child) + self.RemoveChildFromConstraints(child) + child.SetParent(None) + + def Delete(self): + """ + Fully disconnect this shape from parents, children, the + canvas, etc. + """ + for child in self.GetChildren(): + self.RemoveChild(child) + child.Delete() + RectangleShape.Delete(self) + self._constraints = [] + self._divisions = [] + + def DeleteConstraintsInvolvingChild(self, child): + """This function deletes constraints which mention the given child. + + Used when deleting a child from the composite. + """ + for constraint in self._constraints: + if constraint._constrainingObject == child or child in constraint._constrainedObjects: + self._constraints.remove(constraint) + + def RemoveChildFromConstraints(self, child): + for constraint in self._constraints: + if child in constraint._constrainedObjects: + constraint._constrainedObjects.remove(child) + if constraint._constrainingObject == child: + constraint._constrainingObject = None + + # Delete the constraint if no participants left + if not constraint._constrainingObject: + self._constraints.remove(constraint) + + def AddConstraint(self, constraint): + """Adds a constraint to the composite.""" + self._constraints.append(constraint) + if constraint._constraintId == 0: + constraint._constraintId = wx.NewId() + return constraint + + def AddSimpleConstraint(self, type, constraining, constrained): + """Add a constraint of the given type to the composite. + + constraining is the shape doing the constraining + constrained is a list of shapes being constrained + """ + constraint = Constraint(type, constraining, constrained) + if constraint._constraintId == 0: + constraint._constraintId = wx.NewId() + self._constraints.append(constraint) + return constraint + + def FindConstraint(self, cId): + """Finds the constraint with the given id. + + Returns a tuple of the constraint and the actual composite the + constraint was in, in case that composite was a descendant of + this composit. + + Returns None if not found. + """ + for constraint in self._constraints: + if constraint._constraintId == cId: + return constraint, self + + # If not found, try children + for child in self._children: + if isinstance(child, CompositeShape): + constraint = child.FindConstraint(cId) + if constraint: + return constraint[0], child + + return None + + def DeleteConstraint(self, constraint): + """Deletes constraint from composite.""" + self._constraints.remove(constraint) + + def CalculateSize(self): + """Calculates the size and position of the composite based on + child sizes and positions. + """ + maxX = -999999.9 + maxY = -999999.9 + minX = 999999.9 + minY = 999999.9 + + for child in self._children: + # Recalculate size of composite objects because may not conform + # to size it was set to - depends on the children. + if isinstance(child, CompositeShape): + child.CalculateSize() + + w, h = child.GetBoundingBoxMax() + if child.GetX() + w / 2.0 > maxX: + maxX = child.GetX() + w / 2.0 + if child.GetX() - w / 2.0 < minX: + minX = child.GetX() - w / 2.0 + if child.GetY() + h / 2.0 > maxY: + maxY = child.GetY() + h / 2.0 + if child.GetY() - h / 2.0 < minY: + minY = child.GetY() - h / 2.0 + + self._width = maxX - minX + self._height = maxY - minY + self._xpos = self._width / 2.0 + minX + self._ypos = self._height / 2.0 + minY + + def Recompute(self): + """Recomputes any constraints associated with the object. If FALSE is + returned, the constraints could not be satisfied (there was an + inconsistency). + """ + noIterations = 0 + changed = True + while changed and noIterations < 500: + changed = self.Constrain() + noIterations += 1 + + return not changed + + def Constrain(self): + self.CalculateSize() + + changed = False + for child in self._children: + if isinstance(child, CompositeShape) and child.Constrain(): + changed = True + + for constraint in self._constraints: + if constraint.Evaluate(): + changed = True + + return changed + + def MakeContainer(self): + """Makes this composite into a container by creating one child + DivisionShape. + """ + division = self.OnCreateDivision() + self._divisions.append(division) + self.AddChild(division) + + division.SetSize(self._width, self._height) + + dc = wx.ClientDC(self.GetCanvas()) + self.GetCanvas().PrepareDC(dc) + + division.Move(dc, self.GetX(), self.GetY()) + self.Recompute() + division.Show(True) + + def OnCreateDivision(self): + return DivisionShape() + + def FindContainerImage(self): + """Finds the image used to visualize a container. This is any child of + the composite that is not in the divisions list. + """ + for child in self._children: + if child in self._divisions: + return child + + return None + + def ContainsDivision(self, division): + """Returns TRUE if division is a descendant of this container.""" + if division in self._divisions: + return True + + for child in self._children: + if isinstance(child, CompositeShape): + return child.ContainsDivision(division) + + return False + + def GetDivisions(self): + """Return the list of divisions.""" + return self._divisions + + def GetConstraints(self): + """Return the list of constraints.""" + return self._constraints + + +# A division object is a composite with special properties, +# to be used for containment. It's a subdivision of a container. +# A containing node image consists of a composite with a main child shape +# such as rounded rectangle, plus a list of division objects. +# It needs to be a composite because a division contains pieces +# of diagram. +# NOTE a container has at least one wxDivisionShape for consistency. +# This can be subdivided, so it turns into two objects, then each of +# these can be subdivided, etc. + +DIVISION_SIDE_NONE =0 +DIVISION_SIDE_LEFT =1 +DIVISION_SIDE_TOP =2 +DIVISION_SIDE_RIGHT =3 +DIVISION_SIDE_BOTTOM =4 + +originalX = 0.0 +originalY = 0.0 +originalW = 0.0 +originalH = 0.0 + + + +class DivisionControlPoint(ControlPoint): + def __init__(self, the_canvas, object, size, the_xoffset, the_yoffset, the_type): + ControlPoint.__init__(self, the_canvas, object, size, the_xoffset, the_yoffset, the_type) + self.SetEraseObject(False) + + # Implement resizing of canvas object + def OnDragLeft(self, draw, x, y, keys = 0, attachment = 0): + ControlPoint.OnDragLeft(self, draw, x, y, keys, attachment) + + def OnBeginDragLeft(self, x, y, keys = 0, attachment = 0): + global originalX, originalY, originalW, originalH + + originalX = self._shape.GetX() + originalY = self._shape.GetY() + originalW = self._shape.GetWidth() + originalH = self._shape.GetHeight() + + ControlPoint.OnBeginDragLeft(self, x, y, keys, attachment) + + def OnEndDragLeft(self, x, y, keys = 0, attachment = 0): + ControlPoint.OnEndDragLeft(self, x, y, keys, attachment) + + dc = wx.ClientDC(self.GetCanvas()) + self.GetCanvas().PrepareDC(dc) + + division = self._shape + divisionParent = division.GetParent() + + # Need to check it's within the bounds of the parent composite + x1 = divisionParent.GetX() - divisionParent.GetWidth() / 2.0 + y1 = divisionParent.GetY() - divisionParent.GetHeight() / 2.0 + x2 = divisionParent.GetX() + divisionParent.GetWidth() / 2.0 + y2 = divisionParent.GetY() + divisionParent.GetHeight() / 2.0 + + # Need to check it has not made the division zero or negative + # width / height + dx1 = division.GetX() - division.GetWidth() / 2.0 + dy1 = division.GetY() - division.GetHeight() / 2.0 + dx2 = division.GetX() + division.GetWidth() / 2.0 + dy2 = division.GetY() + division.GetHeight() / 2.0 + + success = True + if division.GetHandleSide() == DIVISION_SIDE_LEFT: + if x <= x1 or x >= x2 or x >= dx2: + success = False + # Try it out first... + elif not division.ResizeAdjoining(DIVISION_SIDE_LEFT, x, True): + success = False + else: + division.ResizeAdjoining(DIVISION_SIDE_LEFT, x, False) + elif division.GetHandleSide() == DIVISION_SIDE_TOP: + if y <= y1 or y >= y2 or y >= dy2: + success = False + elif not division.ResizeAdjoining(DIVISION_SIDE_TOP, y, True): + success = False + else: + division.ResizingAdjoining(DIVISION_SIDE_TOP, y, False) + elif division.GetHandleSide() == DIVISION_SIDE_RIGHT: + if x <= x1 or x >= x2 or x <= dx1: + success = False + elif not division.ResizeAdjoining(DIVISION_SIDE_RIGHT, x, True): + success = False + else: + division.ResizeAdjoining(DIVISION_SIDE_RIGHT, x, False) + elif division.GetHandleSide() == DIVISION_SIDE_BOTTOM: + if y <= y1 or y >= y2 or y <= dy1: + success = False + elif not division.ResizeAdjoining(DIVISION_SIDE_BOTTOM, y, True): + success = False + else: + division.ResizeAdjoining(DIVISION_SIDE_BOTTOM, y, False) + + if not success: + division.SetSize(originalW, originalH) + division.Move(dc, originalX, originalY) + + divisionParent.Draw(dc) + division.GetEventHandler().OnDrawControlPoints(dc) + + + +DIVISION_MENU_SPLIT_HORIZONTALLY =1 +DIVISION_MENU_SPLIT_VERTICALLY =2 +DIVISION_MENU_EDIT_LEFT_EDGE =3 +DIVISION_MENU_EDIT_TOP_EDGE =4 +DIVISION_MENU_EDIT_RIGHT_EDGE =5 +DIVISION_MENU_EDIT_BOTTOM_EDGE =6 +DIVISION_MENU_DELETE_ALL =7 + + + +class PopupDivisionMenu(wx.Menu): + def __init__(self): + wx.Menu.__init__(self) + self.Append(DIVISION_MENU_SPLIT_HORIZONTALLY,"Split horizontally") + self.Append(DIVISION_MENU_SPLIT_VERTICALLY,"Split vertically") + self.AppendSeparator() + self.Append(DIVISION_MENU_EDIT_LEFT_EDGE,"Edit left edge") + self.Append(DIVISION_MENU_EDIT_TOP_EDGE,"Edit top edge") + + wx.EVT_MENU_RANGE(self, DIVISION_MENU_SPLIT_HORIZONTALLY, DIVISION_MENU_EDIT_BOTTOM_EDGE, self.OnMenu) + + def SetClientData(self, data): + self._clientData = data + + def GetClientData(self): + return self._clientData + + def OnMenu(self, event): + division = self.GetClientData() + if event.GetId() == DIVISION_MENU_SPLIT_HORIZONTALLY: + division.Divide(wx.HORIZONTAL) + elif event.GetId() == DIVISION_MENU_SPLIT_VERTICALLY: + division.Divide(wx.VERTICAL) + elif event.GetId() == DIVISION_MENU_EDIT_LEFT_EDGE: + division.EditEdge(DIVISION_SIDE_LEFT) + elif event.GetId() == DIVISION_MENU_EDIT_TOP_EDGE: + division.EditEdge(DIVISION_SIDE_TOP) + + + +class DivisionShape(CompositeShape): + """A division shape is like a composite in that it can contain further + objects, but is used exclusively to divide another shape into regions, + or divisions. A wxDivisionShape is never free-standing. + + Derived from: + wxCompositeShape + """ + def __init__(self): + CompositeShape.__init__(self) + self.SetSensitivityFilter(OP_CLICK_LEFT | OP_CLICK_RIGHT | OP_DRAG_RIGHT) + self.SetCentreResize(False) + self.SetAttachmentMode(True) + self._leftSide = None + self._rightSide = None + self._topSide = None + self._bottomSide = None + self._handleSide = DIVISION_SIDE_NONE + self._leftSidePen = wx.BLACK_PEN + self._topSidePen = wx.BLACK_PEN + self._leftSideColour = "BLACK" + self._topSideColour = "BLACK" + self._leftSideStyle = "Solid" + self._topSideStyle = "Solid" + self.ClearRegions() + + def SetLeftSide(self, shape): + """Set the the division on the left side of this division.""" + self._leftSide = shape + + def SetTopSide(self, shape): + """Set the the division on the top side of this division.""" + self._topSide = shape + + def SetRightSide(self, shape): + """Set the the division on the right side of this division.""" + self._rightSide = shape + + def SetBottomSide(self, shape): + """Set the the division on the bottom side of this division.""" + self._bottomSide = shape + + def GetLeftSide(self): + """Return the division on the left side of this division.""" + return self._leftSide + + def GetTopSide(self): + """Return the division on the top side of this division.""" + return self._topSide + + def GetRightSide(self): + """Return the division on the right side of this division.""" + return self._rightSide + + def GetBottomSide(self): + """Return the division on the bottom side of this division.""" + return self._bottomSide + + def SetHandleSide(self, side): + """Sets the side which the handle appears on. + + Either DIVISION_SIDE_LEFT or DIVISION_SIDE_TOP. + """ + self._handleSide = side + + def GetHandleSide(self): + """Return the side which the handle appears on.""" + return self._handleSide + + def SetLeftSidePen(self, pen): + """Set the colour for drawing the left side of the division.""" + self._leftSidePen = pen + + def SetTopSidePen(self, pen): + """Set the colour for drawing the top side of the division.""" + self._topSidePen = pen + + def GetLeftSidePen(self): + """Return the pen used for drawing the left side of the division.""" + return self._leftSidePen + + def GetTopSidePen(self): + """Return the pen used for drawing the top side of the division.""" + return self._topSidePen + + def GetLeftSideColour(self): + """Return the colour used for drawing the left side of the division.""" + return self._leftSideColour + + def GetTopSideColour(self): + """Return the colour used for drawing the top side of the division.""" + return self._topSideColour + + def SetLeftSideColour(self, colour): + """Set the colour for drawing the left side of the division.""" + self._leftSideColour = colour + + def SetTopSideColour(self, colour): + """Set the colour for drawing the top side of the division.""" + self._topSideColour = colour + + def GetLeftSideStyle(self): + """Return the style used for the left side of the division.""" + return self._leftSideStyle + + def GetTopSideStyle(self): + """Return the style used for the top side of the division.""" + return self._topSideStyle + + def SetLeftSideStyle(self, style): + self._leftSideStyle = style + + def SetTopSideStyle(self, style): + self._lefttopStyle = style + + def OnDraw(self, dc): + dc.SetBrush(wx.TRANSPARENT_BRUSH) + dc.SetBackgroundMode(wx.TRANSPARENT) + + x1 = self.GetX() - self.GetWidth() / 2.0 + y1 = self.GetY() - self.GetHeight() / 2.0 + x2 = self.GetX() + self.GetWidth() / 2.0 + y2 = self.GetY() + self.GetHeight() / 2.0 + + # Should subtract 1 pixel if drawing under Windows + if sys.platform[:3] == "win": + y2 -= 1 + + if self._leftSide: + dc.SetPen(self._leftSidePen) + dc.DrawLine(x1, y2, x1, y1) + + if self._topSide: + dc.SetPen(self._topSidePen) + dc.DrawLine(x1, y1, x2, y1) + + # For testing purposes, draw a rectangle so we know + # how big the division is. + #dc.SetBrush(wx.RED_BRUSH) + #dc.DrawRectangle(x1, y1, self.GetWidth(), self.GetHeight()) + + def OnDrawContents(self, dc): + CompositeShape.OnDrawContents(self, dc) + + def OnMovePre(self, dc, x, y, oldx, oldy, display = True): + diffX = x - oldx + diffY = y - oldy + for object in self._children: + object.Erase(dc) + object.Move(dc, object.GetX() + diffX, object.GetY() + diffY, display) + return True + + def OnDragLeft(self, draw, x, y, keys = 0, attachment = 0): + if self._sensitivity & OP_DRAG_LEFT != OP_DRAG_LEFT: + if self._parent: + hit = self._parent.HitTest(x, y) + if hit: + attachment, dist = hit + self._parent.GetEventHandler().OnDragLeft(draw, x, y, keys, attachment) + return + Shape.OnDragLeft(self, draw, x, y, keys, attachment) + + def OnBeginDragLeft(self, x, y, keys = 0, attachment = 0): + if self._sensitivity & OP_DRAG_LEFT != OP_DRAG_LEFT: + if self._parent: + hit = self._parent.HitTest(x, y) + if hit: + attachment, dist = hit + self._parent.GetEventHandler().OnBeginDragLeft(x, y, keys, attachment) + return + Shape.OnBeginDragLeft(x, y, keys, attachment) + + def OnEndDragLeft(self, x, y, keys = 0, attachment = 0): + if self._canvas.HasCapture(): + self._canvas.ReleaseMouse() + if self._sensitivity & OP_DRAG_LEFT != OP_DRAG_LEFT: + if self._parent: + hit = self._parent.HitTest(x, y) + if hit: + attachment, dist = hit + self._parent.GetEventHandler().OnEndDragLeft(x, y, keys, attachment) + return + + dc = wx.ClientDC(self.GetCanvas()) + self.GetCanvas().PrepareDC(dc) + + dc.SetLogicalFunction(wx.COPY) + + self._xpos, self._ypos = self._canvas.Snap(self._xpos, self._ypos) + self.GetEventHandler().OnMovePre(dc, x, y, self._oldX, self._oldY) + + self.ResetControlPoints() + self.Draw(dc) + self.MoveLinks(dc) + self.GetEventHandler().OnDrawControlPoints(dc) + + if self._canvas and not self._canvas.GetQuickEditMode(): + self._canvas.Redraw(dc) + + def SetSize(self, w, h, recursive = True): + self._width = w + self._height = h + RectangleShape.SetSize(self, w, h, recursive) + + def CalculateSize(self): + pass + + # Experimental + def OnRightClick(self, x, y, keys = 0, attachment = 0): + if keys & KEY_CTRL: + self.PopupMenu(x, y) + else: + if self._parent: + hit = self._parent.HitTest(x, y) + if hit: + attachment, dist = hit + self._parent.GetEventHandler().OnRightClick(x, y, keys, attachment) + + # Divide wx.HORIZONTALly or wx.VERTICALly + def Divide(self, direction): + """Divide this division into two further divisions, + horizontally (direction is wxHORIZONTAL) or + vertically (direction is wxVERTICAL). + """ + # Calculate existing top-left, bottom-right + x1 = self.GetX() - self.GetWidth() / 2.0 + y1 = self.GetY() - self.GetHeight() / 2.0 + + compositeParent = self.GetParent() + oldWidth = self.GetWidth() + oldHeight = self.GetHeight() + if self.Selected(): + self.Select(False) + + dc = wx.ClientDC(self.GetCanvas()) + self.GetCanvas().PrepareDC(dc) + + if direction == wx.VERTICAL: + # Dividing vertically means notionally putting a horizontal + # line through it. + # Break existing piece into two. + newXPos1 = self.GetX() + newYPos1 = y1 + self.GetHeight() / 4.0 + newXPos2 = self.GetX() + newYPos2 = y1 + 3 * self.GetHeight() / 4.0 + newDivision = compositeParent.OnCreateDivision() + newDivision.Show(True) + + self.Erase(dc) + + # Anything adjoining the bottom of this division now adjoins the + # bottom of the new division. + for obj in compositeParent.GetDivisions(): + if obj.GetTopSide() == self: + obj.SetTopSide(newDivision) + + newDivision.SetTopSide(self) + newDivision.SetBottomSide(self._bottomSide) + newDivision.SetLeftSide(self._leftSide) + newDivision.SetRightSide(self._rightSide) + self._bottomSide = newDivision + + compositeParent.GetDivisions().append(newDivision) + + # CHANGE: Need to insert this division at start of divisions in the + # object list, because e.g.: + # 1) Add division + # 2) Add contained object + # 3) Add division + # Division is now receiving mouse events _before_ the contained + # object, because it was added last (on top of all others) + + # Add after the image that visualizes the container + compositeParent.AddChild(newDivision, compositeParent.FindContainerImage()) + + self._handleSide = DIVISION_SIDE_BOTTOM + newDivision.SetHandleSide(DIVISION_SIDE_TOP) + + self.SetSize(oldWidth, oldHeight / 2.0) + self.Move(dc, newXPos1, newYPos1) + + newDivision.SetSize(oldWidth, oldHeight / 2.0) + newDivision.Move(dc, newXPos2, newYPos2) + else: + # Dividing horizontally means notionally putting a vertical line + # through it. + # Break existing piece into two. + newXPos1 = x1 + self.GetWidth() / 4.0 + newYPos1 = self.GetY() + newXPos2 = x1 + 3 * self.GetWidth() / 4.0 + newYPos2 = self.GetY() + newDivision = compositeParent.OnCreateDivision() + newDivision.Show(True) + + self.Erase(dc) + + # Anything adjoining the left of this division now adjoins the + # left of the new division. + for obj in compositeParent.GetDivisions(): + if obj.GetLeftSide() == self: + obj.SetLeftSide(newDivision) + + newDivision.SetTopSide(self._topSide) + newDivision.SetBottomSide(self._bottomSide) + newDivision.SetLeftSide(self) + newDivision.SetRightSide(self._rightSide) + self._rightSide = newDivision + + compositeParent.GetDivisions().append(newDivision) + compositeParent.AddChild(newDivision, compositeParent.FindContainerImage()) + + self._handleSide = DIVISION_SIDE_RIGHT + newDivision.SetHandleSide(DIVISION_SIDE_LEFT) + + self.SetSize(oldWidth / 2.0, oldHeight) + self.Move(dc, newXPos1, newYPos1) + + newDivision.SetSize(oldWidth / 2.0, oldHeight) + newDivision.Move(dc, newXPos2, newYPos2) + + if compositeParent.Selected(): + compositeParent.DeleteControlPoints(dc) + compositeParent.MakeControlPoints() + compositeParent.MakeMandatoryControlPoints() + + compositeParent.Draw(dc) + return True + + def MakeControlPoints(self): + self.MakeMandatoryControlPoints() + + def MakeMandatoryControlPoints(self): + maxX, maxY = self.GetBoundingBoxMax() + x = y = 0.0 + direction = 0 + + if self._handleSide == DIVISION_SIDE_LEFT: + x = -maxX / 2.0 + direction = CONTROL_POINT_HORIZONTAL + elif self._handleSide == DIVISION_SIDE_TOP: + y = -maxY / 2.0 + direction = CONTROL_POINT_VERTICAL + elif self._handleSide == DIVISION_SIDE_RIGHT: + x = maxX / 2.0 + direction = CONTROL_POINT_HORIZONTAL + elif self._handleSide == DIVISION_SIDE_BOTTOM: + y = maxY / 2.0 + direction = CONTROL_POINT_VERTICAL + + if self._handleSide != DIVISION_SIDE_NONE: + control = DivisionControlPoint(self._canvas, self, CONTROL_POINT_SIZE, x, y, direction) + self._canvas.AddShape(control) + self._controlPoints.append(control) + + def ResetControlPoints(self): + self.ResetMandatoryControlPoints() + + def ResetMandatoryControlPoints(self): + if not self._controlPoints: + return + + maxX, maxY = self.GetBoundingBoxMax() + + node = self._controlPoints[0] + + if self._handleSide == DIVISION_SIDE_LEFT and node: + node._xoffset = -maxX / 2.0 + node._yoffset = 0.0 + + if self._handleSide == DIVISION_SIDE_TOP and node: + node._xoffset = 0.0 + node._yoffset = -maxY / 2.0 + + if self._handleSide == DIVISION_SIDE_RIGHT and node: + node._xoffset = maxX / 2.0 + node._yoffset = 0.0 + + if self._handleSide == DIVISION_SIDE_BOTTOM and node: + node._xoffset = 0.0 + node._yoffset = maxY / 2.0 + + def AdjustLeft(self, left, test): + """Adjust a side. + + Returns FALSE if it's not physically possible to adjust it to + this point. + """ + x2 = self.GetX() + self.GetWidth() / 2.0 + + if left >= x2: + return False + + if test: + return True + + newW = x2 - left + newX = left + newW / 2.0 + self.SetSize(newW, self.GetHeight()) + + dc = wx.ClientDC(self.GetCanvas()) + self.GetCanvas().PrepareDC(dc) + + self.Move(dc, newX, self.GetY()) + return True + + def AdjustTop(self, top, test): + """Adjust a side. + + Returns FALSE if it's not physically possible to adjust it to + this point. + """ + y2 = self.GetY() + self.GetHeight() / 2.0 + + if top >= y2: + return False + + if test: + return True + + newH = y2 - top + newY = top + newH / 2.0 + self.SetSize(self.GetWidth(), newH) + + dc = wx.ClientDC(self.GetCanvas()) + self.GetCanvas().PrepareDC(dc) + + self.Move(dc, self.GetX(), newY) + return True + + def AdjustRight(self, right, test): + """Adjust a side. + + Returns FALSE if it's not physically possible to adjust it to + this point. + """ + x1 = self.GetX() - self.GetWidth() / 2.0 + + if right <= x1: + return False + + if test: + return True + + newW = right - x1 + newX = x1 + newW / 2.0 + self.SetSize(newW, self.GetHeight()) + + dc = wx.ClientDC(self.GetCanvas()) + self.GetCanvas().PrepareDC(dc) + + self.Move(dc, newX, self.GetY()) + return True + + def AdjustTop(self, top, test): + """Adjust a side. + + Returns FALSE if it's not physically possible to adjust it to + this point. + """ + y1 = self.GetY() - self.GetHeight() / 2.0 + + if bottom <= y1: + return False + + if test: + return True + + newH = bottom - y1 + newY = y1 + newH / 2.0 + self.SetSize(self.GetWidth(), newH) + + dc = wx.ClientDC(self.GetCanvas()) + self.GetCanvas().PrepareDC(dc) + + self.Move(dc, self.GetX(), newY) + return True + + # Resize adjoining divisions. + + # Behaviour should be as follows: + # If right edge moves, find all objects whose left edge + # adjoins this object, and move left edge accordingly. + # If left..., move ... right. + # If top..., move ... bottom. + # If bottom..., move top. + # If size goes to zero or end position is other side of start position, + # resize to original size and return. + # + def ResizeAdjoining(self, side, newPos, test): + """Resize adjoining divisions at the given side. + + If test is TRUE, just see whether it's possible for each adjoining + region, returning FALSE if it's not. + + side can be one of: + + * DIVISION_SIDE_NONE + * DIVISION_SIDE_LEFT + * DIVISION_SIDE_TOP + * DIVISION_SIDE_RIGHT + * DIVISION_SIDE_BOTTOM + """ + divisionParent = self.GetParent() + for division in divisionParent.GetDivisions(): + if side == DIVISION_SIDE_LEFT: + if division._rightSide == self: + success = division.AdjustRight(newPos, test) + if not success and test: + return false + elif side == DIVISION_SIDE_TOP: + if division._bottomSide == self: + success = division.AdjustBottom(newPos, test) + if not success and test: + return False + elif side == DIVISION_SIDE_RIGHT: + if division._leftSide == self: + success = division.AdjustLeft(newPos, test) + if not success and test: + return False + elif side == DIVISION_SIDE_BOTTOM: + if division._topSide == self: + success = division.AdjustTop(newPos, test) + if not success and test: + return False + return True + + def EditEdge(self, side): + print "EditEdge() not implemented." + + def PopupMenu(self, x, y): + menu = PopupDivisionMenu() + menu.SetClientData(self) + if self._leftSide: + menu.Enable(DIVISION_MENU_EDIT_LEFT_EDGE, True) + else: + menu.Enable(DIVISION_MENU_EDIT_LEFT_EDGE, False) + if self._topSide: + menu.Enable(DIVISION_MENU_EDIT_TOP_EDGE, True) + else: + menu.Enable(DIVISION_MENU_EDIT_TOP_EDGE, False) + + x1, y1 = self._canvas.GetViewStart() + unit_x, unit_y = self._canvas.GetScrollPixelsPerUnit() + + dc = wx.ClientDC(self.GetCanvas()) + self.GetCanvas().PrepareDC(dc) + + mouse_x = dc.LogicalToDeviceX(x - x1 * unit_x) + mouse_y = dc.LogicalToDeviceY(y - y1 * unit_y) + + self._canvas.PopupMenu(menu, (mouse_x, mouse_y)) + + diff --git a/wx/lib/ogl/_diagram.py b/wx/lib/ogl/_diagram.py new file mode 100644 index 00000000..345a4648 --- /dev/null +++ b/wx/lib/ogl/_diagram.py @@ -0,0 +1,167 @@ +# -*- coding: utf-8 -*- +#---------------------------------------------------------------------------- +# Name: diagram.py +# Purpose: Diagram class +# +# Author: Pierre Hjälm (from C++ original by Julian Smart) +# +# Created: 2004-05-08 +# RCS-ID: $Id$ +# Copyright: (c) 2004 Pierre Hjälm - 1998 Julian Smart +# Licence: wxWindows license +#---------------------------------------------------------------------------- + +import wx + +DEFAULT_MOUSE_TOLERANCE = 3 + + + +class Diagram(object): + """Encapsulates an entire diagram, with methods for drawing. A diagram has + an associated ShapeCanvas. + + Derived from: + Object + """ + def __init__(self): + self._diagramCanvas = None + self._quickEditMode = False + self._snapToGrid = True + self._gridSpacing = 5.0 + self._shapeList = [] + self._mouseTolerance = DEFAULT_MOUSE_TOLERANCE + + def Redraw(self, dc): + """Draw the shapes in the diagram on the specified device context.""" + if self._shapeList: + if self.GetCanvas(): + self.GetCanvas().SetCursor(wx.HOURGLASS_CURSOR) + for object in self._shapeList: + object.Draw(dc) + if self.GetCanvas(): + self.GetCanvas().SetCursor(wx.STANDARD_CURSOR) + + def Clear(self, dc): + """Clear the specified device context.""" + dc.Clear() + + def AddShape(self, object, addAfter = None): + """Adds a shape to the diagram. If addAfter is not None, the shape + will be added after addAfter. + """ + if not object in self._shapeList: + if addAfter: + self._shapeList.insert(self._shapeList.index(addAfter) + 1, object) + else: + self._shapeList.append(object) + + object.SetCanvas(self.GetCanvas()) + + def InsertShape(self, object): + """Insert a shape at the front of the shape list.""" + self._shapeList.insert(0, object) + + def RemoveShape(self, object): + """Remove the shape from the diagram (non-recursively) but do not + delete it. + """ + if object in self._shapeList: + self._shapeList.remove(object) + + def RemoveAllShapes(self): + """Remove all shapes from the diagram but do not delete the shapes.""" + self._shapeList = [] + + def DeleteAllShapes(self): + """Remove and delete all shapes in the diagram.""" + for shape in self._shapeList[:]: + if not shape.GetParent(): + self.RemoveShape(shape) + shape.Delete() + + def ShowAll(self, show): + """Call Show for each shape in the diagram.""" + for shape in self._shapeList: + shape.Show(show) + + def DrawOutline(self, dc, x1, y1, x2, y2): + """Draw an outline rectangle on the current device context.""" + dc.SetPen(wx.Pen(wx.Colour(0, 0, 0), 1, wx.DOT)) + dc.SetBrush(wx.TRANSPARENT_BRUSH) + + dc.DrawLines([[x1, y1], [x2, y1], [x2, y2], [x1, y2], [x1, y1]]) + + def RecentreAll(self, dc): + """Make sure all text that should be centred, is centred.""" + for shape in self._shapeList: + shape.Recentre(dc) + + def SetCanvas(self, canvas): + """Set the canvas associated with this diagram.""" + self._diagramCanvas = canvas + + def GetCanvas(self): + """Return the shape canvas associated with this diagram.""" + return self._diagramCanvas + + def FindShape(self, id): + """Return the shape for the given identifier.""" + for shape in self._shapeList: + if shape.GetId() == id: + return shape + return None + + def Snap(self, x, y): + """'Snaps' the coordinate to the nearest grid position, if + snap-to-grid is on.""" + if self._snapToGrid: + return self._gridSpacing * int(x / self._gridSpacing + 0.5), self._gridSpacing * int(y / self._gridSpacing + 0.5) + return x, y + + def SetGridSpacing(self, spacing): + """Sets grid spacing.""" + self._gridSpacing = spacing + + def SetSnapToGrid(self, snap): + """Sets snap-to-grid mode.""" + self._snapToGrid = snap + + def GetGridSpacing(self): + """Return the grid spacing.""" + return self._gridSpacing + + def GetSnapToGrid(self): + """Return snap-to-grid mode.""" + return self._snapToGrid + + def SetQuickEditMode(self, mode): + """Set quick-edit-mode on of off. + + In this mode, refreshes are minimized, but the diagram may need + manual refreshing occasionally. + """ + self._quickEditMode = mode + + def GetQuickEditMode(self): + """Return quick edit mode.""" + return self._quickEditMode + + def SetMouseTolerance(self, tolerance): + """Set the tolerance within which a mouse move is ignored. + + The default is 3 pixels. + """ + self._mouseTolerance = tolerance + + def GetMouseTolerance(self): + """Return the tolerance within which a mouse move is ignored.""" + return self._mouseTolerance + + def GetShapeList(self): + """Return the internal shape list.""" + return self._shapeList + + def GetCount(self): + """Return the number of shapes in the diagram.""" + return len(self._shapeList) diff --git a/wx/lib/ogl/_divided.py b/wx/lib/ogl/_divided.py new file mode 100644 index 00000000..27276389 --- /dev/null +++ b/wx/lib/ogl/_divided.py @@ -0,0 +1,402 @@ +# -*- coding: utf-8 -*- +#---------------------------------------------------------------------------- +# Name: divided.py +# Purpose: DividedShape class +# +# Author: Pierre Hjälm (from C++ original by Julian Smart) +# +# Created: 2004-05-08 +# RCS-ID: $Id$ +# Copyright: (c) 2004 Pierre Hjälm - 1998 Julian Smart +# Licence: wxWindows license +#---------------------------------------------------------------------------- + +import sys +import wx + +from _basic import ControlPoint, RectangleShape, Shape +from _oglmisc import * + + + +class DividedShapeControlPoint(ControlPoint): + def __init__(self, the_canvas, object, region, size, the_m_xoffset, the_m_yoffset, the_type): + ControlPoint.__init__(self, the_canvas, object, size, the_m_xoffset, the_m_yoffset, the_type) + self.regionId = region + + # Implement resizing of divided object division + def OnDragLeft(self, draw, x, y, keys = 0, attachment = 0): + dc = wx.ClientDC(self.GetCanvas()) + self.GetCanvas().PrepareDC(dc) + + dc.SetLogicalFunction(OGLRBLF) + dottedPen = wx.Pen(wx.Colour(0, 0, 0), 1, wx.DOT) + dc.SetPen(dottedPen) + dc.SetBrush(wx.TRANSPARENT_BRUSH) + + dividedObject = self._shape + x1 = dividedObject.GetX() - dividedObject.GetWidth() / 2.0 + y1 = y + x2 = dividedObject.GetX() + dividedObject.GetWidth() / 2.0 + y2 = y + + dc.DrawLine(x1, y1, x2, y2) + + def OnBeginDragLeft(self, x, y, keys = 0, attachment = 0): + dc = wx.ClientDC(self.GetCanvas()) + self.GetCanvas().PrepareDC(dc) + + dc.SetLogicalFunction(OGLRBLF) + dottedPen = wx.Pen(wx.Colour(0, 0, 0), 1, wx.DOT) + dc.SetPen(dottedPen) + dc.SetBrush(wx.TRANSPARENT_BRUSH) + + dividedObject = self._shape + + x1 = dividedObject.GetX() - dividedObject.GetWidth() / 2.0 + y1 = y + x2 = dividedObject.GetX() + dividedObject.GetWidth() / 2.0 + y2 = y + + dc.DrawLine(x1, y1, x2, y2) + self._canvas.CaptureMouse() + + def OnEndDragLeft(self, x, y, keys = 0, attachment = 0): + dc = wx.ClientDC(self.GetCanvas()) + self.GetCanvas().PrepareDC(dc) + + dividedObject = self._shape + if not dividedObject.GetRegions()[self.regionId]: + return + + thisRegion = dividedObject.GetRegions()[self.regionId] + nextRegion = None + + dc.SetLogicalFunction(wx.COPY) + + if self._canvas.HasCapture(): + self._canvas.ReleaseMouse() + + # Find the old top and bottom of this region, + # and calculate the new proportion for this region + # if legal. + currentY = dividedObject.GetY() - dividedObject.GetHeight() / 2.0 + maxY = dividedObject.GetY() + dividedObject.GetHeight() / 2.0 + + # Save values + theRegionTop = 0 + nextRegionBottom = 0 + + for i in range(len(dividedObject.GetRegions())): + region = dividedObject.GetRegions()[i] + proportion = region._regionProportionY + yy = currentY + dividedObject.GetHeight() * proportion + actualY = min(maxY, yy) + + if region == thisRegion: + thisRegionTop = currentY + + if i + 1 < len(dividedObject.GetRegions()): + nextRegion = dividedObject.GetRegions()[i + 1] + if region == nextRegion: + nextRegionBottom = actualY + + currentY = actualY + + if not nextRegion: + return + + # Check that we haven't gone above this region or below + # next region. + if y <= thisRegionTop or y >= nextRegionBottom: + return + + dividedObject.EraseLinks(dc) + + # Now calculate the new proportions of this region and the next region + thisProportion = float(y - thisRegionTop) / dividedObject.GetHeight() + nextProportion = float(nextRegionBottom - y) / dividedObject.GetHeight() + + thisRegion.SetProportions(0, thisProportion) + nextRegion.SetProportions(0, nextProportion) + self._yoffset = y - dividedObject.GetY() + + # Now reformat text + for i, region in enumerate(dividedObject.GetRegions()): + if region.GetText(): + s = region.GetText() + dividedObject.FormatText(dc, s, i) + + dividedObject.SetRegionSizes() + dividedObject.Draw(dc) + dividedObject.GetEventHandler().OnMoveLinks(dc) + + + +class DividedShape(RectangleShape): + """A DividedShape is a rectangle with a number of vertical divisions. + Each division may have its text formatted with independent characteristics, + and the size of each division relative to the whole image may be specified. + + Derived from: + RectangleShape + """ + def __init__(self, w, h): + RectangleShape.__init__(self, w, h) + self.ClearRegions() + + def OnDraw(self, dc): + RectangleShape.OnDraw(self, dc) + + def OnDrawContents(self, dc): + if self.GetRegions(): + defaultProportion = 1.0 / len(self.GetRegions()) + else: + defaultProportion = 0.0 + currentY = self._ypos - self._height / 2.0 + maxY = self._ypos + self._height / 2.0 + + leftX = self._xpos - self._width / 2.0 + rightX = self._xpos + self._width / 2.0 + + if self._pen: + dc.SetPen(self._pen) + + dc.SetTextForeground(self._textColour) + + # For efficiency, don't do this under X - doesn't make + # any visible difference for our purposes. + if sys.platform[:3] == "win": + dc.SetTextBackground(self._brush.GetColour()) + + if self.GetDisableLabel(): + return + + xMargin = 2 + yMargin = 2 + + dc.SetBackgroundMode(wx.TRANSPARENT) + + for region in self.GetRegions(): + dc.SetFont(region.GetFont()) + dc.SetTextForeground(region.GetActualColourObject()) + + if region._regionProportionY < 0: + proportion = defaultProportion + else: + proportion = region._regionProportionY + + y = currentY + self._height * proportion + actualY = min(maxY, y) + + centreX = self._xpos + centreY = currentY + (actualY - currentY) / 2.0 + + DrawFormattedText(dc, region._formattedText, centreX, centreY, self._width - 2 * xMargin, actualY - currentY - 2 * yMargin, region._formatMode) + + if y <= maxY and region != self.GetRegions()[-1]: + regionPen = region.GetActualPen() + if regionPen: + dc.SetPen(regionPen) + dc.DrawLine(leftX, y, rightX, y) + + currentY = actualY + + def SetSize(self, w, h, recursive = True): + self.SetAttachmentSize(w, h) + self._width = w + self._height = h + self.SetRegionSizes() + + def SetRegionSizes(self): + """Set all region sizes according to proportions and this object + total size. + """ + if not self.GetRegions(): + return + + if self.GetRegions(): + defaultProportion = 1.0 / len(self.GetRegions()) + else: + defaultProportion = 0.0 + currentY = self._ypos - self._height / 2.0 + maxY = self._ypos + self._height / 2.0 + + for region in self.GetRegions(): + if region._regionProportionY <= 0: + proportion = defaultProportion + else: + proportion = region._regionProportionY + + sizeY = proportion * self._height + y = currentY + sizeY + actualY = min(maxY, y) + + centreY = currentY + (actualY - currentY) / 2.0 + + region.SetSize(self._width, sizeY) + region.SetPosition(0, centreY - self._ypos) + + currentY = actualY + + # Attachment points correspond to regions in the divided box + def GetAttachmentPosition(self, attachment, nth = 0, no_arcs = 1, line = None): + totalNumberAttachments = len(self.GetRegions()) * 2 + 2 + if self.GetAttachmentMode() == ATTACHMENT_MODE_NONE or attachment >= totalNumberAttachments: + return Shape.GetAttachmentPosition(self, attachment, nth, no_arcs) + + n = len(self.GetRegions()) + isEnd = line and line.IsEnd(self) + + left = self._xpos - self._width / 2.0 + right = self._xpos + self._width / 2.0 + top = self._ypos - self._height / 2.0 + bottom = self._ypos + self._height / 2.0 + + # Zero is top, n + 1 is bottom + if attachment == 0: + y = top + if self._spaceAttachments: + if line and line.GetAlignmentType(isEnd) == LINE_ALIGNMENT_TO_NEXT_HANDLE: + # Align line according to the next handle along + point = line.GetNextControlPoint(self) + if point[0] < left: + x = left + elif point[0] > right: + x = right + else: + x = point[0] + else: + x = left + (nth + 1) * self._width / (no_arcs + 1.0) + else: + x = self._xpos + elif attachment == n + 1: + y = bottom + if self._spaceAttachments: + if line and line.GetAlignmentType(isEnd) == LINE_ALIGNMENT_TO_NEXT_HANDLE: + # Align line according to the next handle along + point = line.GetNextControlPoint(self) + if point[0] < left: + x = left + elif point[0] > right: + x = right + else: + x = point[0] + else: + x = left + (nth + 1) * self._width / (no_arcs + 1.0) + else: + x = self._xpos + else: # Left or right + isLeft = not attachment < (n + 1) + if isLeft: + i = totalNumberAttachments - attachment - 1 + else: + i = attachment - 1 + + region = self.GetRegions()[i] + if region: + if isLeft: + x = left + else: + x = right + + # Calculate top and bottom of region + top = self._ypos + region._y - region._height / 2.0 + bottom = self._ypos + region._y + region._height / 2.0 + + # Assuming we can trust the absolute size and + # position of these regions + if self._spaceAttachments: + if line and line.GetAlignmentType(isEnd) == LINE_ALIGNMENT_TO_NEXT_HANDLE: + # Align line according to the next handle along + point = line.GetNextControlPoint(self) + if point[1] < bottom: + y = bottom + elif point[1] > top: + y = top + else: + y = point[1] + else: + y = top + (nth + 1) * region._height / (no_arcs + 1.0) + else: + y = self._ypos + region._y + else: + return False + return x, y + + def GetNumberOfAttachments(self): + # There are two attachments for each region (left and right), + # plus one on the top and one on the bottom. + n = len(self.GetRegions()) * 2 + 2 + + maxN = n - 1 + for point in self._attachmentPoints: + if point._id > maxN: + maxN = point._id + + return maxN + 1 + + def AttachmentIsValid(self, attachment): + totalNumberAttachments = len(self.GetRegions()) * 2 + 2 + if attachment >= totalNumberAttachments: + return Shape.AttachmentIsValid(self, attachment) + else: + return attachment >= 0 + + def MakeControlPoints(self): + RectangleShape.MakeControlPoints(self) + self.MakeMandatoryControlPoints() + + def MakeMandatoryControlPoints(self): + currentY = self.GetY() - self._height / 2.0 + maxY = self.GetY() + self._height / 2.0 + + for i, region in enumerate(self.GetRegions()): + proportion = region._regionProportionY + + y = currentY + self._height * proportion + actualY = min(maxY, y) + + if region != self.GetRegions()[-1]: + controlPoint = DividedShapeControlPoint(self._canvas, self, i, CONTROL_POINT_SIZE, 0, actualY - self.GetY(), 0) + self._canvas.AddShape(controlPoint) + self._controlPoints.append(controlPoint) + + currentY = actualY + + def ResetControlPoints(self): + # May only have the region handles, (n - 1) of them + if len(self._controlPoints) > len(self.GetRegions()) - 1: + RectangleShape.ResetControlPoints(self) + + self.ResetMandatoryControlPoints() + + def ResetMandatoryControlPoints(self): + currentY = self.GetY() - self._height / 2.0 + maxY = self.GetY() + self._height / 2.0 + + i = 0 + for controlPoint in self._controlPoints: + if isinstance(controlPoint, DividedShapeControlPoint): + region = self.GetRegions()[i] + proportion = region._regionProportionY + + y = currentY + self._height * proportion + actualY = min(maxY, y) + + controlPoint._xoffset = 0 + controlPoint._yoffset = actualY - self.GetY() + + currentY = actualY + + i += 1 + + def EditRegions(self): + """Edit the region colours and styles. Not implemented.""" + print "EditRegions() is unimplemented" + + def OnRightClick(self, x, y, keys = 0, attachment = 0): + if keys & KEY_CTRL: + self.EditRegions() + else: + RectangleShape.OnRightClick(self, x, y, keys, attachment) diff --git a/wx/lib/ogl/_drawn.py b/wx/lib/ogl/_drawn.py new file mode 100644 index 00000000..5862b034 --- /dev/null +++ b/wx/lib/ogl/_drawn.py @@ -0,0 +1,888 @@ +# -*- coding: utf-8 -*- +#---------------------------------------------------------------------------- +# Name: drawn.py +# Purpose: DrawnShape class +# +# Author: Pierre Hjälm (from C++ original by Julian Smart) +# +# Created: 2004-08-25 +# RCS-ID: $Id$ +# Copyright: (c) 2004 Pierre Hjälm - 1998 Julian Smart +# License: wxWindows license +#---------------------------------------------------------------------------- + +import os.path + +from _basic import RectangleShape +from _oglmisc import * + +METAFLAGS_OUTLINE = 1 +METAFLAGS_ATTACHMENTS = 2 + +DRAWN_ANGLE_0 = 0 +DRAWN_ANGLE_90 = 1 +DRAWN_ANGLE_180 = 2 +DRAWN_ANGLE_270 = 3 + +# Drawing operations +DRAWOP_SET_PEN = 1 +DRAWOP_SET_BRUSH = 2 +DRAWOP_SET_FONT = 3 +DRAWOP_SET_TEXT_COLOUR = 4 +DRAWOP_SET_BK_COLOUR = 5 +DRAWOP_SET_BK_MODE = 6 +DRAWOP_SET_CLIPPING_RECT = 7 +DRAWOP_DESTROY_CLIPPING_RECT = 8 + +DRAWOP_DRAW_LINE = 20 +DRAWOP_DRAW_POLYLINE = 21 +DRAWOP_DRAW_POLYGON = 22 +DRAWOP_DRAW_RECT = 23 +DRAWOP_DRAW_ROUNDED_RECT = 24 +DRAWOP_DRAW_ELLIPSE = 25 +DRAWOP_DRAW_POINT = 26 +DRAWOP_DRAW_ARC = 27 +DRAWOP_DRAW_TEXT = 28 +DRAWOP_DRAW_SPLINE = 29 +DRAWOP_DRAW_ELLIPTIC_ARC = 30 + +class DrawOp(object): + def __init__(self, theOp): + self._op = theOp + + def GetOp(self): + return self._op + + def GetPerimeterPoint(self, x1, y1, x2, y2, xOffset, yOffset, attachmentMode): + return False + + def Scale(self,scaleX, scaleY): + pass + + def Translate(self, x, y): + pass + + def Rotate(self, x, y, theta, sinTheta, cosTheta): + pass + +class OpSetGDI(DrawOp): + """Set font, brush, text colour.""" + def __init__(self, theOp, theImage, theGdiIndex, theMode = 0): + DrawOp.__init__(self, theOp) + + self._gdiIndex = theGdiIndex + self._image = theImage + self._mode = theMode + + def Do(self, dc, xoffset = 0, yoffset = 0): + if self._op == DRAWOP_SET_PEN: + # Check for overriding this operation for outline colour + if self._gdiIndex in self._image._outlineColours: + if self._image._outlinePen: + dc.SetPen(self._image._outlinePen) + else: + try: + dc.SetPen(self._image._gdiObjects[self._gdiIndex]) + except IndexError: + pass + elif self._op == DRAWOP_SET_BRUSH: + # Check for overriding this operation for outline or fill colour + if self._gdiIndex in self._image._outlineColours: + # Need to construct a brush to match the outline pen's colour + if self._image._outlinePen: + br = wx.Brush(self._image._outlinePen, wx.SOLID) + if br: + dc.SetBrush(br) + elif self._gdiIndex in self._image._fillColours: + if self._image._fillBrush: + dc.SetBrush(self._image._fillBrush) + else: + brush = self._image._gdiObjects[self._gdiIndex] + if brush: + dc.SetBrush(brush) + elif self._op == DRAWOP_SET_FONT: + try: + dc.SetFont(self._image._gdiObjects[self._gdiIndex]) + except IndexError: + pass + elif self._op == DRAWOP_SET_TEXT_COLOUR: + dc.SetTextForeground(wx.Colour(self._r, self._g, self._b)) + elif self._op == DRAWOP_SET_BK_COLOUR: + dc.SetTextBackground(wx.Colour(self._r, self._g, self._b)) + elif self._op == DRAWOP_SET_BK_MODE: + dc.SetBackgroundMode(self._mode) + + +class OpSetClipping(DrawOp): + """Set/destroy clipping.""" + def __init__(self, theOp, theX1, theY1, theX2, theY2): + DrawOp.__init__(self, theOp) + + self._x1 = theX1 + self._y1 = theY1 + self._x2 = theX2 + self._y2 = theY2 + + def Do(self, dc, xoffset, yoffset): + if self._op == DRAWOP_SET_CLIPPING_RECT: + dc.SetClippingRegion(self._x1 + xoffset, self._y1 + yoffset, self._x2 + xoffset, self._y2 + yoffset) + elif self._op == DRAWOP_DESTROY_CLIPPING_RECT: + dc.DestroyClippingRegion() + + def Scale(self, scaleX, scaleY): + self._x1 *= scaleX + self._y1 *= scaleY + self._x2 *= scaleX + self._y2 *= scaleY + + def Translate(self, x, y): + self._x1 += x + self._y1 += y + + +class OpDraw(DrawOp): + """Draw line, rectangle, rounded rectangle, ellipse, point, arc, text.""" + def __init__(self, theOp, theX1, theY1, theX2, theY2, theRadius = 0.0, s = ""): + DrawOp.__init__(self, theOp) + + self._x1 = theX1 + self._y1 = theY1 + self._x2 = theX2 + self._y2 = theY2 + self._x3 = 0.0 + self._y3 = 0.0 + self._radius = theRadius + self._textString = s + + def Do(self, dc, xoffset, yoffset): + if self._op == DRAWOP_DRAW_LINE: + dc.DrawLine(self._x1 + xoffset, self._y1 + yoffset, self._x2 + xoffset, self._y2 + yoffset) + elif self._op == DRAWOP_DRAW_RECT: + dc.DrawRectangle(self._x1 + xoffset, self._y1 + yoffset, self._x2, self._y2) + elif self._op == DRAWOP_DRAW_ROUNDED_RECT: + dc.DrawRoundedRectangle(self._x1 + xoffset, self._y1 + yoffset, self._x2, self._y2, self._radius) + elif self._op == DRAWOP_DRAW_ELLIPSE: + dc.DrawEllipse(self._x1 + xoffset, self._y1 + yoffset, self._x2, self._y2) + elif self._op == DRAWOP_DRAW_ARC: + dc.DrawArc(self._x2 + xoffset, self._y2 + yoffset, self._x3 + xoffset, self._y3 + yoffset, self._x1 + xoffset, self._y1 + yoffset) + elif self._op == DRAWOP_DRAW_ELLIPTIC_ARC: + dc.DrawEllipticArc(self._x1 + xoffset, self._y1 + yoffset, self._x2, self._y2, self._x3 * 360 / (2 * math.pi), self._y3 * 360 / (2 * math.pi)) + elif self._op == DRAWOP_DRAW_POINT: + dc.DrawPoint(self._x1 + xoffset, self._y1 + yoffset) + elif self._op == DRAWOP_DRAW_TEXT: + dc.DrawText(self._textString, self._x1 + xoffset, self._y1 + yoffset) + def Scale(self, scaleX, scaleY): + self._x1 *= scaleX + self._y1 *= scaleY + self._x2 *= scaleX + self._y2 *= scaleY + + if self._op != DRAWOP_DRAW_ELLIPTIC_ARC: + self._x3 *= scaleX + self._y3 *= scaleY + + self._radius *= scaleX + + def Translate(self, x, y): + self._x1 += x + self._y1 += y + + if self._op == DRAWOP_DRAW_LINE: + self._x2 += x + self._y2 += y + elif self._op == DRAWOP_DRAW_ARC: + self._x2 += x + self._y2 += y + self._x3 += x + self._y3 += y + + def Rotate(self, x, y, theta, sinTheta, cosTheta): + newX1 = self._x1 * cosTheta + self._y1 * sinTheta + x * (1 - cosTheta) + y * sinTheta + newY1 = self._x1 * sinTheta + self._y1 * cosTheta + y * (1 - cosTheta) + x * sinTheta + + if self._op == DRAWOP_DRAW_LINE: + newX2 = self._x2 * cosTheta - self._y2 * sinTheta + x * (1 - cosTheta) + y * sinTheta + newY2 = self._x2 * sinTheta + self._y2 * cosTheta + y * (1 - cosTheta) + x * sinTheta; + + self._x1 = newX1 + self._y1 = newY1 + self._x2 = newX2 + self._y2 = newY2 + + elif self._op in [DRAWOP_DRAW_RECT, DRAWOP_DRAW_ROUNDED_RECT, DRAWOP_DRAW_ELLIPTIC_ARC]: + # Assume only 0, 90, 180, 270 degree rotations. + # oldX1, oldY1 represents the top left corner. Find the + # bottom right, and rotate that. Then the width/height is + # the difference between x/y values. + oldBottomRightX = self._x1 + self._x2 + oldBottomRightY = self._y1 + self._y2 + newBottomRightX = oldBottomRightX * cosTheta - oldBottomRightY * sinTheta + x * (1 - cosTheta) + y * sinTheta + newBottomRightY = oldBottomRightX * sinTheta + oldBottomRightY * cosTheta + y * (1 - cosTheta) + x * sinTheta + + # Now find the new top-left, bottom-right coordinates. + minX = min(newX1, newBottomRightX) + minY = min(newY1, newBottomRightY) + maxX = max(newX1, newBottomRightX) + maxY = max(newY1, newBottomRightY) + + self._x1 = minX + self._y1 = minY + self._x2 = maxX - minX # width + self._y2 = maxY - minY # height + + if self._op == DRAWOP_DRAW_ELLIPTIC_ARC: + # Add rotation to angles + self._x3 += theta + self._y3 += theta + elif self._op == DRAWOP_DRAW_ARC: + newX2 = self._x2 * cosTheta - self._y2 * sinTheta + x * (1 - cosTheta) + y * sinTheta + newY2 = self._x2 * sinTheta + self._y2 * cosTheta + y * (1 - cosTheta) + x * sinTheta + newX3 = self._x3 * cosTheta - self._y3 * sinTheta + x * (1 - cosTheta) + y * sinTheta + newY3 = self._x3 * sinTheta + self._y3 * cosTheta + y * (1 - cosTheta) + x * sinTheta + + self._x1 = newX1 + self._y1 = newY1 + self._x2 = newX2 + self._y2 = newY2 + self._x3 = newX3 + self._y3 = newY3 + + +class OpPolyDraw(DrawOp): + """Draw polygon, polyline, spline.""" + def __init__(self, theOp, thePoints): + DrawOp.__init__(self, theOp) + + self._noPoints = len(thePoints) + self._points = thePoints + + def Do(self, dc, xoffset, yoffset): + if self._op == DRAWOP_DRAW_POLYLINE: + dc.DrawLines(self._points, xoffset, yoffset) + elif self._op == DRAWOP_DRAW_POLYGON: + dc.DrawPolygon(self._points, xoffset, yoffset) + elif self._op == DRAWOP_DRAW_SPLINE: + dc.DrawSpline(self._points) # no offsets in DrawSpline + + def Scale(self, scaleX, scaleY): + for i in range(self._noPoints): + self._points[i] = wx.Point(self._points[i][0] * scaleX, self._points[i][1] * scaleY) + + def Translate(self, x, y): + for i in range(self._noPoints): + self._points[i][0] += x + self._points[i][1] += y + + def Rotate(self, x, y, theta, sinTheta, cosTheta): + for i in range(self._noPoints): + x1 = self._points[i][0] + y1 = self._points[i][1] + + self._points[i] = x1 * cosTheta - y1 * sinTheta + x * (1 - cosTheta) + y * sinTheta, x1 * sinTheta + y1 * cosTheta + y * (1 - cosTheta) + x * sinTheta + + def OnDrawOutline(self, dc, x, y, w, h, oldW, oldH): + dc.SetBrush(wx.TRANSPARENT_BRUSH) + + # Multiply all points by proportion of new size to old size + x_proportion = abs(w / oldW) + y_proportion = abs(h / oldH) + + dc.DrawPolygon([(x_proportion * x, y_proportion * y) for x, y in self._points], x, y) + + def GetPerimeterPoint(self, x1, y1, x2, y2, xOffset, yOffset, attachmentMode): + # First check for situation where the line is vertical, + # and we would want to connect to a point on that vertical -- + # oglFindEndForPolyline can't cope with this (the arrow + # gets drawn to the wrong place). + if attachmentMode == ATTACHMENT_MODE_NONE and x1 == x2: + # Look for the point we'd be connecting to. This is + # a heuristic... + for point in self._points: + if point[0] == 0: + if y2 > y1 and point[1] > 0: + return point[0]+xOffset, point[1]+yOffset + elif y2 < y1 and point[1] < 0: + return point[0]+xOffset, point[1]+yOffset + + return FindEndForPolyline([ p[0] + xOffset for p in self._points ], + [ p[1] + yOffset for p in self._points ], + x1, y1, x2, y2) + + +class PseudoMetaFile(object): + """ + A simple metafile-like class which can load data from a Windows + metafile on all platforms. + """ + def __init__(self): + self._currentRotation = 0 + self._rotateable = True + self._width = 0.0 + self._height = 0.0 + self._outlinePen = None + self._fillBrush = None + self._outlineOp = -1 + + self._ops = [] + self._gdiObjects = [] + + self._outlineColours = [] + self._fillColours = [] + + def Clear(self): + self._ops = [] + self._gdiObjects = [] + self._outlineColours = [] + self._fillColours = [] + self._outlineColours = -1 + + def IsValid(self): + return self._ops != [] + + def GetOps(self): + return self._ops + + def SetOutlineOp(self, op): + self._outlineOp = op + + def GetOutlineOp(self): + return self._outlineOp + + def SetOutlinePen(self, pen): + self._outlinePen = pen + + def GetOutlinePen(self, pen): + return self._outlinePen + + def SetFillBrush(self, brush): + self._fillBrush = brush + + def GetFillBrush(self): + return self._fillBrush + + def SetSize(self, w, h): + self._width = w + self._height = h + + def SetRotateable(self, rot): + self._rotateable = rot + + def GetRotateable(self): + return self._rotateable + + def GetFillColours(self): + return self._fillColours + + def GetOutlineColours(self): + return self._outlineColours + + def Draw(self, dc, xoffset, yoffset): + for op in self._ops: + op.Do(dc, xoffset, yoffset) + + def Scale(self, sx, sy): + for op in self._ops: + op.Scale(sx, sy) + + self._width *= sx + self._height *= sy + + def Translate(self, x, y): + for op in self._ops: + op.Translate(x, y) + + def Rotate(self, x, y, theta): + theta1 = theta - self._currentRotation + if theta1 == 0: + return + + cosTheta = math.cos(theta1) + sinTheta = math.sin(theta1) + + for op in self._ops: + op.Rotate(x, y, theta, sinTheta, cosTheta) + + self._currentRotation = theta + + def LoadFromMetaFile(self, filename, rwidth, rheight): + if not os.path.exist(filename): + return False + + print "LoadFromMetaFile not implemented yet." + return False # TODO + + # Scale to fit size + def ScaleTo(self, w, h): + scaleX = w / self._width + scaleY = h / self._height + + self.Scale(scaleX, scaleY) + + def GetBounds(self): + maxX, maxY, minX, minY = -99999.9, -99999.9, 99999.9, 99999.9 + + for op in self._ops: + if op.GetOp() in [DRAWOP_DRAW_LINE, DRAWOP_DRAW_RECT, DRAWOP_DRAW_ROUNDED_RECT, DRAWOP_DRAW_ELLIPSE, DRAWOP_DRAW_POINT, DRAWOP_DRAW_TEXT]: + if op._x1 < minX: + minX = op._x1 + if op._x1 > maxX: + maxX = op._x1 + if op._y1 < minY: + minY = op._y1 + if op._y1 > maxY: + maxY = op._y1 + if op.GetOp() == DRAWOP_DRAW_LINE: + if op._x2 < minX: + minX = op._x2 + if op._x2 > maxX: + maxX = op._x2 + if op._y2 < minY: + minY = op._y2 + if op._y2 > maxY: + maxY = op._y2 + elif op.GetOp() in [ DRAWOP_DRAW_RECT, DRAWOP_DRAW_ROUNDED_RECT, DRAWOP_DRAW_ELLIPSE]: + if op._x1 + op._x2 < minX: + minX = op._x1 + op._x2 + if op._x1 + op._x2 > maxX: + maxX = op._x1 + op._x2 + if op._y1 + op._y2 < minY: + minY = op._y1 + op._y2 + if op._y1 + op._y2 > maxX: + maxY = op._y1 + op._y2 + elif op.GetOp() == DRAWOP_DRAW_ARC: + # TODO: don't yet know how to calculate the bounding box + # for an arc. So pretend it's a line; to get a correct + # bounding box, draw a blank rectangle first, of the + # correct size. + if op._x1 < minX: + minX = op._x1 + if op._x1 > maxX: + maxX = op._x1 + if op._y1 < minY: + minY = op._y1 + if op._y1 > maxY: + maxY = op._y1 + if op._x2 < minX: + minX = op._x2 + if op._x2 > maxX: + maxX = op._x2 + if op._y2 < minY: + minY = op._y2 + if op._y2 > maxY: + maxY = op._y2 + elif op.GetOp() in [DRAWOP_DRAW_POLYLINE, DRAWOP_DRAW_POLYGON, DRAWOP_DRAW_SPLINE]: + for point in op._points: + if point[0] < minX: + minX = point[0] + if point[0] > maxX: + maxX = point[0] + if point[1] < minY: + minY = point[1] + if point[1] > maxY: + maxY = point[1] + + return [minX, minY, maxX, maxY] + + # Calculate size from current operations + def CalculateSize(self, shape): + boundMinX, boundMinY, boundMaxX, boundMaxY = self.GetBounds() + + # By Pierre Hjälm: This is NOT in the old version, which + # gets this totally wrong. Since the drawing is centered, we + # cannot get the width by measuring from left to right, we + # must instead make enough room to handle the largest + # coordinates + #self.SetSize(boundMaxX - boundMinX, boundMaxY - boundMinY) + + w = max(abs(boundMinX), abs(boundMaxX)) * 2 + h = max(abs(boundMinY), abs(boundMaxY)) * 2 + + self.SetSize(w, h) + + if shape: + shape.SetWidth(self._width) + shape.SetHeight(self._height) + + # Set of functions for drawing into a pseudo metafile + def DrawLine(self, pt1, pt2): + op = OpDraw(DRAWOP_DRAW_LINE, pt1[0], pt1[1], pt2[0], pt2[1]) + self._ops.append(op) + + def DrawRectangle(self, rect): + op = OpDraw(DRAWOP_DRAW_RECT, rect[0], rect[1], rect[2], rect[3]) + self._ops.append(op) + + def DrawRoundedRectangle(self, rect, radius): + op = OpDraw(DRAWOP_DRAW_ROUNDED_RECT, rect[0], rect[1], rect[2], rect[3]) + op._radius = radius + self._ops.append(op) + + def DrawEllipse(self, rect): + op = OpDraw(DRAWOP_DRAW_ELLIPSE, rect[0], rect[1], rect[2], rect[3]) + self._ops.append(op) + + def DrawArc(self, centrePt, startPt, endPt): + op = OpDraw(DRAWOP_DRAW_ARC, centrePt[0], centrePt[1], startPt[0], startPt[1]) + op._x3, op._y3 = endPt + + self._ops.append(op) + + def DrawEllipticArc(self, rect, startAngle, endAngle): + startAngleRadians = startAngle * math.pi * 2 / 360 + endAngleRadians = endAngle * math.pi * 2 / 360 + + op = OpDraw(DRAWOP_DRAW_ELLIPTIC_ARC, rect[0], rect[1], rect[2], rect[3]) + op._x3 = startAngleRadians + op._y3 = endAngleRadians + + self._ops.append(op) + + def DrawPoint(self, pt): + op = OpDraw(DRAWOP_DRAW_POINT, pt[0], pt[1], 0, 0) + self._ops.append(op) + + def DrawText(self, text, pt): + op = OpDraw(DRAWOP_DRAW_TEXT, pt[0], pt[1], 0, 0) + op._textString = text + self._ops.append(op) + + def DrawLines(self, pts): + op = OpPolyDraw(DRAWOP_DRAW_POLYLINE, pts) + self._ops.append(op) + + # flags: + # oglMETAFLAGS_OUTLINE: will be used for drawing the outline and + # also drawing lines/arrows at the circumference. + # oglMETAFLAGS_ATTACHMENTS: will be used for initialising attachment + # points at the vertices (perhaps a rare case...) + def DrawPolygon(self, pts, flags = 0): + op = OpPolyDraw(DRAWOP_DRAW_POLYGON, pts) + self._ops.append(op) + + if flags & METAFLAGS_OUTLINE: + self._outlineOp = len(self._ops) - 1 + + def DrawSpline(self, pts): + op = OpPolyDraw(DRAWOP_DRAW_SPLINE, pts) + self._ops.append(op) + + def SetClippingRect(self, rect): + OpSetClipping(DRAWOP_SET_CLIPPING_RECT, rect[0], rect[1], rect[2], rect[3]) + + def DestroyClippingRect(self): + op = OpSetClipping(DRAWOP_DESTROY_CLIPPING_RECT, 0, 0, 0, 0) + self._ops.append(op) + + def SetPen(self, pen, isOutline = False): + self._gdiObjects.append(pen) + op = OpSetGDI(DRAWOP_SET_PEN, self, len(self._gdiObjects) - 1) + self._ops.append(op) + + if isOutline: + self._outlineColours.append(len(self._gdiObjects) - 1) + + def SetBrush(self, brush, isFill = False): + self._gdiObjects.append(brush) + op = OpSetGDI(DRAWOP_SET_BRUSH, self, len(self._gdiObjects) - 1) + self._ops.append(op) + + if isFill: + self._fillColours.append(len(self._gdiObjects) - 1) + + def SetFont(self, font): + self._gdiObjects.append(font) + op = OpSetGDI(DRAWOP_SET_FONT, self, len(self._gdiObjects) - 1) + self._ops.append(op) + + def SetTextColour(self, colour): + op = OpSetGDI(DRAWOP_SET_TEXT_COLOUR, self, 0) + op._r, op._g, op._b = colour.Red(), colour.Green(), colour.Blue() + + self._ops.append(op) + + def SetBackgroundColour(self, colour): + op = OpSetGDI(DRAWOP_SET_BK_COLOUR, self, 0) + op._r, op._g, op._b = colour.Red(), colour.Green(), colour.Blue() + + self._ops.append(op) + + def SetBackgroundMode(self, mode): + op = OpSetGDI(DRAWOP_SET_BK_MODE, self, 0) + self._ops.append(op) + +class DrawnShape(RectangleShape): + """ + Draws a pseudo-metafile shape, which can be loaded from a simple + Windows metafile. + + wxDrawnShape allows you to specify a different shape for each of four + orientations (North, West, South and East). It also provides a set of + drawing functions for programmatic drawing of a shape, so that during + construction of the shape you can draw into it as if it were a device + context. + + Derived from: + RectangleShape + """ + def __init__(self): + RectangleShape.__init__(self, 100, 50) + self._saveToFile = True + self._currentAngle = DRAWN_ANGLE_0 + + self._metafiles=PseudoMetaFile(), PseudoMetaFile(), PseudoMetaFile(), PseudoMetaFile() + + def OnDraw(self, dc): + # Pass pen and brush in case we have force outline + # and fill colours + if self._shadowMode != SHADOW_NONE: + if self._shadowBrush: + self._metafiles[self._currentAngle]._fillBrush = self._shadowBrush + self._metafiles[self._currentAngle]._outlinePen = wx.Pen(wx.WHITE, 1, wx.TRANSPARENT) + self._metafiles[self._currentAngle].Draw(dc, self._xpos + self._shadowOffsetX, self._ypos + self._shadowOffsetY) + + self._metafiles[self._currentAngle]._outlinePen = self._pen + self._metafiles[self._currentAngle]._fillBrush = self._brush + self._metafiles[self._currentAngle].Draw(dc, self._xpos, self._ypos) + + def SetSize(self, w, h, recursive = True): + self.SetAttachmentSize(w, h) + + if self.GetWidth() == 0.0: + scaleX = 1 + else: + scaleX = w / self.GetWidth() + + if self.GetHeight() == 0.0: + scaleY = 1 + else: + scaleY = h / self.GetHeight() + + for i in range(4): + if self._metafiles[i].IsValid(): + self._metafiles[i].Scale(scaleX, scaleY) + + self._width = w + self._height = h + self.SetDefaultRegionSize() + + def Scale(self, sx, sy): + """Scale the shape by the given amount.""" + for i in range(4): + if self._metafiles[i].IsValid(): + self._metafiles[i].Scale(sx, sy) + self._metafiles[i].CalculateSize(self) + + def Translate(self, x, y): + """Translate the shape by the given amount.""" + for i in range(4): + if self._metafiles[i].IsValid(): + self._metafiles[i].Translate(x, y) + self._metafiles[i].CalculateSize(self) + + # theta is absolute rotation from the zero position + def Rotate(self, x, y, theta): + """Rotate about the given axis by the given amount in radians.""" + self._currentAngle = self.DetermineMetaFile(theta) + + if self._currentAngle == 0: + # Rotate metafile + if not self._metafiles[0].GetRotateable(): + return + + self._metafiles[0].Rotate(x, y, theta) + + actualTheta = theta - self._rotation + + # Rotate attachment points + sinTheta = math.sin(actualTheta) + cosTheta = math.cos(actualTheta) + + for point in self._attachmentPoints: + x1 = point._x + y1 = point._y + + point._x = x1 * cosTheta - y1 * sinTheta + x * (1.0 - cosTheta) + y * sinTheta + point._y = x1 * sinTheta + y1 * cosTheta + y * (1.0 - cosTheta) + x * sinTheta + + self._rotation = theta + + self._metafiles[self._currentAngle].CalculateSize(self) + + # Which metafile do we use now? Based on current rotation and validity + # of metafiles. + def DetermineMetaFile(self, rotation): + tolerance = 0.0001 + angles = [0.0, math.pi / 2, math.pi, 3 * math.pi / 2] + + whichMetaFile = 0 + + for i in range(4): + if RoughlyEqual(rotation, angles[i], tolerance): + whichMetaFile = i + break + + if whichMetaFile > 0 and not self._metafiles[whichMetaFile].IsValid(): + whichMetaFile = 0 + + return whichMetaFile + + def OnDrawOutline(self, dc, x, y, w, h): + if self._metafiles[self._currentAngle].GetOutlineOp() != -1: + op = self._metafiles[self._currentAngle].GetOps()[self._metafiles[self._currentAngle].GetOutlineOp()] + if op.OnDrawOutline(dc, x, y, w, h, self._width, self._height): + return + + # Default... just use a rectangle + RectangleShape.OnDrawOutline(self, dc, x, y, w, h) + + # Get the perimeter point using the special outline op, if there is one, + # otherwise use default wxRectangleShape scheme + def GetPerimeterPoint(self, x1, y1, x2, y2): + if self._metafiles[self._currentAngle].GetOutlineOp() != -1: + op = self._metafiles[self._currentAngle].GetOps()[self._metafiles[self._currentAngle].GetOutlineOp()] + p = op.GetPerimeterPoint(x1, y1, x2, y2, self.GetX(), self.GetY(), self.GetAttachmentMode()) + if p: + return p + + return RectangleShape.GetPerimeterPoint(self, x1, y1, x2, y2) + + def LoadFromMetaFile(self, filename): + """Load a (very simple) Windows metafile, created for example by + Top Draw, the Windows shareware graphics package.""" + return self._metafiles[0].LoadFromMetaFile(filename) + + # Set of functions for drawing into a pseudo metafile. + # They use integers, but doubles are used internally for accuracy + # when scaling. + def DrawLine(self, pt1, pt2): + self._metafiles[self._currentAngle].DrawLine(pt1, pt2) + + def DrawRectangle(self, rect): + self._metafiles[self._currentAngle].DrawRectangle(rect) + + def DrawRoundedRectangle(self, rect, radius): + """Draw a rounded rectangle. + + radius is the corner radius. If radius is negative, it expresses + the radius as a proportion of the smallest dimension of the rectangle. + """ + self._metafiles[self._currentAngle].DrawRoundedRectangle(rect, radius) + + def DrawEllipse(self, rect): + self._metafiles[self._currentAngle].DrawEllipse(rect) + + def DrawArc(self, centrePt, startPt, endPt): + """Draw an arc.""" + self._metafiles[self._currentAngle].DrawArc(centrePt, startPt, endPt) + + def DrawEllipticArc(self, rect, startAngle, endAngle): + """Draw an elliptic arc.""" + self._metafiles[self._currentAngle].DrawEllipticArc(rect, startAngle, endAngle) + + def DrawPoint(self, pt): + self._metafiles[self._currentAngle].DrawPoint(pt) + + def DrawText(self, text, pt): + self._metafiles[self._currentAngle].DrawText(text, pt) + + def DrawLines(self, pts): + self._metafiles[self._currentAngle].DrawLines(pts) + + def DrawPolygon(self, pts, flags = 0): + """Draw a polygon. + + flags can be one or more of: + METAFLAGS_OUTLINE (use this polygon for the drag outline) and + METAFLAGS_ATTACHMENTS (use the vertices of this polygon for attachments). + """ + if flags and METAFLAGS_ATTACHMENTS: + self.ClearAttachments() + for i in range(len(pts)): + self._attachmentPoints.append(AttachmentPoint(i,pts[i][0],pts[i][1])) + self._metafiles[self._currentAngle].DrawPolygon(pts, flags) + + def DrawSpline(self, pts): + self._metafiles[self._currentAngle].DrawSpline(pts) + + def SetClippingRect(self, rect): + """Set the clipping rectangle.""" + self._metafiles[self._currentAngle].SetClippingRect(rect) + + def DestroyClippingRect(self): + """Destroy the clipping rectangle.""" + self._metafiles[self._currentAngle].DestroyClippingRect() + + def SetDrawnPen(self, pen, isOutline = False): + """Set the pen for this metafile. + + If isOutline is True, this pen is taken to indicate the outline + (and if the outline pen is changed for the whole shape, the pen + will be replaced with the outline pen). + """ + self._metafiles[self._currentAngle].SetPen(pen, isOutline) + + def SetDrawnBrush(self, brush, isFill = False): + """Set the brush for this metafile. + + If isFill is True, the brush is used as the fill brush. + """ + self._metafiles[self._currentAngle].SetBrush(brush, isFill) + + def SetDrawnFont(self, font): + self._metafiles[self._currentAngle].SetFont(font) + + def SetDrawnTextColour(self, colour): + """Set the current text colour for the current metafile.""" + self._metafiles[self._currentAngle].SetTextColour(colour) + + def SetDrawnBackgroundColour(self, colour): + """Set the current background colour for the current metafile.""" + self._metafiles[self._currentAngle].SetBackgroundColour(colour) + + def SetDrawnBackgroundMode(self, mode): + """Set the current background mode for the current metafile.""" + self._metafiles[self._currentAngle].SetBackgroundMode(mode) + + def CalculateSize(self): + """Calculate the wxDrawnShape size from the current metafile. + + Call this after you have drawn into the shape. + """ + self._metafiles[self._currentAngle].CalculateSize(self) + + def DrawAtAngle(self, angle): + """Set the metafile for the given orientation, which can be one of: + + * DRAWN_ANGLE_0 + * DRAWN_ANGLE_90 + * DRAWN_ANGLE_180 + * DRAWN_ANGLE_270 + """ + self._currentAngle = angle + + def GetAngle(self): + """Return the current orientation, which can be one of: + + * DRAWN_ANGLE_0 + * DRAWN_ANGLE_90 + * DRAWN_ANGLE_180 + * DRAWN_ANGLE_270 + """ + return self._currentAngle + + def GetRotation(self): + """Return the current rotation of the shape in radians.""" + return self._rotation + + def SetSaveToFile(self, save): + """If save is True, the image will be saved along with the shape's + other attributes. The reason why this might not be desirable is that + if there are many shapes with the same image, it would be more + efficient for the application to save one copy, and not duplicate + the information for every shape. The default is True. + """ + self._saveToFile = save + + def GetMetaFile(self, which = 0): + """Return a reference to the internal 'pseudo-metafile'.""" + return self._metafiles[which] diff --git a/wx/lib/ogl/_lines.py b/wx/lib/ogl/_lines.py new file mode 100644 index 00000000..eeebf865 --- /dev/null +++ b/wx/lib/ogl/_lines.py @@ -0,0 +1,1532 @@ +# -*- coding: utf-8 -*- +#---------------------------------------------------------------------------- +# Name: lines.py +# Purpose: LineShape class +# +# Author: Pierre Hjälm (from C++ original by Julian Smart) +# +# Created: 2004-05-08 +# RCS-ID: $Id$ +# Copyright: (c) 2004 Pierre Hjälm - 1998 Julian Smart +# Licence: wxWindows license +#---------------------------------------------------------------------------- + +import sys +import math + +from _basic import Shape, ShapeRegion, ShapeTextLine, ControlPoint, RectangleShape +from _oglmisc import * + +# Line alignment flags +# Vertical by default +LINE_ALIGNMENT_HORIZ = 1 +LINE_ALIGNMENT_VERT = 0 +LINE_ALIGNMENT_TO_NEXT_HANDLE = 2 +LINE_ALIGNMENT_NONE = 0 + + + +class LineControlPoint(ControlPoint): + def __init__(self, theCanvas = None, object = None, size = 0.0, x = 0.0, y = 0.0, the_type = 0): + ControlPoint.__init__(self, theCanvas, object, size, x, y, the_type) + self._xpos = x + self._ypos = y + self._type = the_type + self._point = None + self._originalPos = None + + def OnDraw(self, dc): + RectangleShape.OnDraw(self, dc) + + # Implement movement of Line point + def OnDragLeft(self, draw, x, y, keys = 0, attachment = 0): + self._shape.GetEventHandler().OnSizingDragLeft(self, draw, x, y, keys, attachment) + + def OnBeginDragLeft(self, x, y, keys = 0, attachment = 0): + self._shape.GetEventHandler().OnSizingBeginDragLeft(self, x, y, keys, attachment) + + def OnEndDragLeft(self, x, y, keys = 0, attachment = 0): + self._shape.GetEventHandler().OnSizingEndDragLeft(self, x, y, keys, attachment) + + + +class ArrowHead(object): + def __init__(self, type = 0, end = 0, size = 0.0, dist = 0.0, name = "", mf = None, arrowId = -1): + if isinstance(type, ArrowHead): + pass + else: + self._arrowType = type + self._arrowEnd = end + self._arrowSize = size + self._xOffset = dist + self._yOffset = 0.0 + self._spacing = 5.0 + + self._arrowName = name + self._metaFile = mf + self._id = arrowId + if self._id == -1: + self._id = wx.NewId() + + def _GetType(self): + return self._arrowType + + def GetPosition(self): + return self._arrowEnd + + def SetPosition(self, pos): + self._arrowEnd = pos + + def GetXOffset(self): + return self._xOffset + + def GetYOffset(self): + return self._yOffset + + def GetSpacing(self): + return self._spacing + + def GetSize(self): + return self._arrowSize + + def SetSize(self, size): + self._arrowSize = size + if self._arrowType == ARROW_METAFILE and self._metaFile: + oldWidth = self._metaFile._width + if oldWidth == 0: + return + + scale = float(size) / oldWidth + if scale != 1: + self._metaFile.Scale(scale, scale) + + def GetName(self): + return self._arrowName + + def SetXOffset(self, x): + self._xOffset = x + + def SetYOffset(self, y): + self._yOffset = y + + def GetMetaFile(self): + return self._metaFile + + def GetId(self): + return self._id + + def GetArrowEnd(self): + return self._arrowEnd + + def GetArrowSize(self): + return self._arrowSize + + def SetSpacing(self, sp): + self._spacing = sp + + + +class LabelShape(RectangleShape): + def __init__(self, parent, region, w, h): + RectangleShape.__init__(self, w, h) + self._lineShape = parent + self._shapeRegion = region + self.SetPen(wx.Pen(wx.Colour(0, 0, 0), 1, wx.DOT)) + + def OnDraw(self, dc): + if self._lineShape and not self._lineShape.GetDrawHandles(): + return + + x1 = self._xpos - self._width / 2.0 + y1 = self._ypos - self._height / 2.0 + + if self._pen: + if self._pen.GetWidth() == 0: + dc.SetPen(wx.Pen(wx.WHITE, 1, wx.TRANSPARENT)) + else: + dc.SetPen(self._pen) + dc.SetBrush(wx.TRANSPARENT_BRUSH) + + if self._cornerRadius > 0: + dc.DrawRoundedRectangle(x1, y1, self._width, self._height, self._cornerRadius) + else: + dc.DrawRectangle(x1, y1, self._width, self._height) + + def OnDrawContents(self, dc): + pass + + def OnDragLeft(self, draw, x, y, keys = 0, attachment = 0): + RectangleShape.OnDragLeft(self, draw, x, y, keys, attachment) + + def OnBeginDragLeft(self, x, y, keys = 0, attachment = 0): + RectangleShape.OnBeginDragLeft(self, x, y, keys, attachment) + + def OnEndDragLeft(self, x, y, keys = 0, attachment = 0): + RectangleShape.OnEndDragLeft(self, x, y, keys, attachment) + + def OnMovePre(self, dc, x, y, old_x, old_y, display): + return self._lineShape.OnLabelMovePre(dc, self, x, y, old_x, old_y, display) + + # Divert left and right clicks to line object + def OnLeftClick(self, x, y, keys = 0, attachment = 0): + self._lineShape.GetEventHandler().OnLeftClick(x, y, keys, attachment) + + def OnRightClick(self, x, y, keys = 0, attachment = 0): + self._lineShape.GetEventHandler().OnRightClick(x, y, keys, attachment) + + + +class LineShape(Shape): + """LineShape may be attached to two nodes; + it may be segmented, in which case a control point is drawn for each joint. + + A wxLineShape may have arrows at the beginning, end and centre. + + Derived from: + Shape + """ + def __init__(self): + Shape.__init__(self) + + self._sensitivity = OP_CLICK_LEFT | OP_CLICK_RIGHT + self._draggable = False + self._attachmentTo = 0 + self._attachmentFrom = 0 + self._from = None + self._to = None + self._erasing = False + self._arrowSpacing = 5.0 + self._ignoreArrowOffsets = False + self._isSpline = False + self._maintainStraightLines = False + self._alignmentStart = 0 + self._alignmentEnd = 0 + + self._lineControlPoints = None + + # Clear any existing regions (created in an earlier constructor) + # and make the three line regions. + self.ClearRegions() + for name in ["Middle","Start","End"]: + newRegion = ShapeRegion() + newRegion.SetName(name) + newRegion.SetSize(150, 50) + self._regions.append(newRegion) + + self._labelObjects = [None, None, None] + self._lineOrientations = [] + self._lineControlPoints = [] + self._arcArrows = [] + + def GetFrom(self): + """Return the 'from' object.""" + return self._from + + def GetTo(self): + """Return the 'to' object.""" + return self._to + + def GetAttachmentFrom(self): + """Return the attachment point on the 'from' node.""" + return self._attachmentFrom + + def GetAttachmentTo(self): + """Return the attachment point on the 'to' node.""" + return self._attachmentTo + + def GetLineControlPoints(self): + return self._lineControlPoints + + def SetSpline(self, spline): + """Specifies whether a spline is to be drawn through the control points.""" + self._isSpline = spline + + def IsSpline(self): + """TRUE if a spline is drawn through the control points.""" + return self._isSpline + + def SetAttachmentFrom(self, attach): + """Set the 'from' shape attachment.""" + self._attachmentFrom = attach + + def SetAttachmentTo(self, attach): + """Set the 'to' shape attachment.""" + self._attachmentTo = attach + + # This is really to distinguish between lines and other images. + # For lines, want to pass drag to canvas, since lines tend to prevent + # dragging on a canvas (they get in the way.) + def Draggable(self): + return False + + def SetIgnoreOffsets(self, ignore): + """Set whether to ignore offsets from the end of the line when drawing.""" + self._ignoreArrowOffsets = ignore + + def GetArrows(self): + return self._arcArrows + + def GetAlignmentStart(self): + return self._alignmentStart + + def GetAlignmentEnd(self): + return self._alignmentEnd + + def IsEnd(self, nodeObject): + """TRUE if shape is at the end of the line.""" + return self._to == nodeObject + + def MakeLineControlPoints(self, n): + """Make a given number of control points (minimum of two).""" + self._lineControlPoints = [] + + for _ in range(n): + point = wx.RealPoint(-999, -999) + self._lineControlPoints.append(point) + + # pi: added _initialised to keep track of when we have set + # the middle points to something other than (-999, -999) + self._initialised = False + + def InsertLineControlPoint(self, dc = None, point = None): + """Insert a control point at an optional given position.""" + if dc: + self.Erase(dc) + + if point: + line_x, line_y = point + else: + last_point = self._lineControlPoints[-1] + second_last_point = self._lineControlPoints[-2] + + line_x = (last_point[0] + second_last_point[0]) / 2.0 + line_y = (last_point[1] + second_last_point[1]) / 2.0 + + point = wx.RealPoint(line_x, line_y) + self._lineControlPoints.insert(len(self._lineControlPoints)-1, point) + + def DeleteLineControlPoint(self): + """Delete an arbitary point on the line.""" + if len(self._lineControlPoints) < 3: + return False + + del self._lineControlPoints[-2] + return True + + def Initialise(self): + """Initialise the line object.""" + if self._lineControlPoints: + # Just move the first and last control points + first_point = self._lineControlPoints[0] + last_point = self._lineControlPoints[-1] + + # If any of the line points are at -999, we must + # initialize them by placing them half way between the first + # and the last. + + for i in range(1,len(self._lineControlPoints)): + point = self._lineControlPoints[i] + if point[0] == -999: + if first_point[0] < last_point[0]: + x1 = first_point[0] + x2 = last_point[0] + else: + x2 = first_point[0] + x1 = last_point[0] + if first_point[1] < last_point[1]: + y1 = first_point[1] + y2 = last_point[1] + else: + y2 = first_point[1] + y1 = last_point[1] + self._lineControlPoints[i] = wx.RealPoint((x2 - x1) / 2.0 + x1, (y2 - y1) / 2.0 + y1) + self._initialised = True + + def FormatText(self, dc, s, i): + """Format a text string according to the region size, adding + strings with positions to region text list. + """ + self.ClearText(i) + + if len(self._regions) == 0 or i >= len(self._regions): + return + + region = self._regions[i] + region.SetText(s) + dc.SetFont(region.GetFont()) + + w, h = region.GetSize() + # Initialize the size if zero + if (w == 0 or h == 0) and s: + w, h = 100, 50 + region.SetSize(w, h) + + string_list = FormatText(dc, s, w - 5, h - 5, region.GetFormatMode()) + for s in string_list: + line = ShapeTextLine(0.0, 0.0, s) + region.GetFormattedText().append(line) + + actualW = w + actualH = h + if region.GetFormatMode() & FORMAT_SIZE_TO_CONTENTS: + actualW, actualH = GetCentredTextExtent(dc, region.GetFormattedText(), self._xpos, self._ypos, w, h) + if actualW != w or actualH != h: + xx, yy = self.GetLabelPosition(i) + self.EraseRegion(dc, region, xx, yy) + if len(self._labelObjects) < i: + self._labelObjects[i].Select(False, dc) + self._labelObjects[i].Erase(dc) + self._labelObjects[i].SetSize(actualW, actualH) + + region.SetSize(actualW, actualH) + + if len(self._labelObjects) < i: + self._labelObjects[i].Select(True, dc) + self._labelObjects[i].Draw(dc) + + CentreText(dc, region.GetFormattedText(), self._xpos, self._ypos, actualW, actualH, region.GetFormatMode()) + self._formatted = True + + def DrawRegion(self, dc, region, x, y): + """Format one region at this position.""" + if self.GetDisableLabel(): + return + + w, h = region.GetSize() + + # Get offset from x, y + xx, yy = region.GetPosition() + + xp = xx + x + yp = yy + y + + # First, clear a rectangle for the text IF there is any + if len(region.GetFormattedText()): + dc.SetPen(self.GetBackgroundPen()) + dc.SetBrush(self.GetBackgroundBrush()) + + # Now draw the text + if region.GetFont(): + dc.SetFont(region.GetFont()) + dc.DrawRectangle(xp - w / 2.0, yp - h / 2.0, w, h) + + if self._pen: + dc.SetPen(self._pen) + dc.SetTextForeground(region.GetActualColourObject()) + + DrawFormattedText(dc, region.GetFormattedText(), xp, yp, w, h, region.GetFormatMode()) + + def EraseRegion(self, dc, region, x, y): + """Erase one region at this position.""" + if self.GetDisableLabel(): + return + + w, h = region.GetSize() + + # Get offset from x, y + xx, yy = region.GetPosition() + + xp = xx + x + yp = yy + y + + if region.GetFormattedText(): + dc.SetPen(self.GetBackgroundPen()) + dc.SetBrush(self.GetBackgroundBrush()) + + dc.DrawRectangle(xp - w / 2.0, yp - h / 2.0, w, h) + + def GetLabelPosition(self, position): + """Get the reference point for a label. + + Region x and y are offsets from this. + position is 0 (middle), 1 (start), 2 (end). + """ + if position == 0: + # Want to take the middle section for the label + half_way = int(len(self._lineControlPoints) / 2.0) + + # Find middle of this line + point = self._lineControlPoints[half_way - 1] + next_point = self._lineControlPoints[half_way] + + dx = next_point[0] - point[0] + dy = next_point[1] - point[1] + + return point[0] + dx / 2.0, point[1] + dy / 2.0 + elif position == 1: + return self._lineControlPoints[0][0], self._lineControlPoints[0][1] + elif position == 2: + return self._lineControlPoints[-1][0], self._lineControlPoints[-1][1] + + def Straighten(self, dc = None): + """Straighten verticals and horizontals.""" + if len(self._lineControlPoints) < 3: + return + + if dc: + self.Erase(dc) + + GraphicsStraightenLine(self._lineControlPoints[-1], self._lineControlPoints[-2]) + + for i in range(len(self._lineControlPoints) - 2): + GraphicsStraightenLine(self._lineControlPoints[i], self._lineControlPoints[i + 1]) + + if dc: + self.Draw(dc) + + def Unlink(self): + """Unlink the line from the nodes at either end.""" + if self._to: + self._to.GetLines().remove(self) + if self._from: + self._from.GetLines().remove(self) + self._to = None + self._from = None + for i in range(3): + if self._labelObjects[i]: + self._labelObjects[i].Select(False) + self._labelObjects[i].RemoveFromCanvas(self._canvas) + self.ClearArrowsAtPosition(-1) + + # Override Delete to unlink before deleting + def Delete(self): + self.Unlink() + Shape.Delete(self) + + def SetEnds(self, x1, y1, x2, y2): + """Set the end positions of the line.""" + self._lineControlPoints[0] = wx.RealPoint(x1, y1) + self._lineControlPoints[-1] = wx.RealPoint(x2, y2) + + # Find centre point + self._xpos = (x1 + x2) / 2.0 + self._ypos = (y1 + y2) / 2.0 + + # Get absolute positions of ends + def GetEnds(self): + """Get the visible endpoints of the lines for drawing between two objects.""" + first_point = self._lineControlPoints[0] + last_point = self._lineControlPoints[-1] + + return first_point[0], first_point[1], last_point[0], last_point[1] + + def SetAttachments(self, from_attach, to_attach): + """Specify which object attachment points should be used at each end + of the line. + """ + self._attachmentFrom = from_attach + self._attachmentTo = to_attach + + def HitTest(self, x, y): + if not self._lineControlPoints: + return False + + # Look at label regions in case mouse is over a label + inLabelRegion = False + for i in range(3): + if self._regions[i]: + region = self._regions[i] + if len(region._formattedText): + xp, yp = self.GetLabelPosition(i) + # Offset region from default label position + cx, cy = region.GetPosition() + cw, ch = region.GetSize() + cx += xp + cy += yp + + rLeft = cx - cw / 2.0 + rTop = cy - ch / 2.0 + rRight = cx + cw / 2.0 + rBottom = cy + ch / 2.0 + if x > rLeft and x < rRight and y > rTop and y < rBottom: + inLabelRegion = True + break + + for i in range(len(self._lineControlPoints) - 1): + point1 = self._lineControlPoints[i] + point2 = self._lineControlPoints[i + 1] + + # For inaccurate mousing allow 8 pixel corridor + extra = 4 + + dx = point2[0] - point1[0] + dy = point2[1] - point1[1] + + seg_len = math.sqrt(dx * dx + dy * dy) + if dy == 0 and dx == 0: + continue + distance_from_seg = seg_len * float((x - point1[0]) * dy - (y - point1[1]) * dx) / (dy * dy + dx * dx) + distance_from_prev = seg_len * float((y - point1[1]) * dy + (x - point1[0]) * dx) / (dy * dy + dx * dx) + + if abs(distance_from_seg) < extra and distance_from_prev >= 0 and distance_from_prev <= seg_len or inLabelRegion: + return 0, distance_from_seg + + return False + + def DrawArrows(self, dc): + """Draw all arrows.""" + # Distance along line of each arrow: space them out evenly + startArrowPos = 0.0 + endArrowPos = 0.0 + middleArrowPos = 0.0 + + for arrow in self._arcArrows: + ah = arrow.GetArrowEnd() + if ah == ARROW_POSITION_START: + if arrow.GetXOffset() and not self._ignoreArrowOffsets: + # If specified, x offset is proportional to line length + self.DrawArrow(dc, arrow, arrow.GetXOffset(), True) + else: + self.DrawArrow(dc, arrow, startArrowPos, False) + startArrowPos += arrow.GetSize() + arrow.GetSpacing() + elif ah == ARROW_POSITION_END: + if arrow.GetXOffset() and not self._ignoreArrowOffsets: + self.DrawArrow(dc, arrow, arrow.GetXOffset(), True) + else: + self.DrawArrow(dc, arrow, endArrowPos, False) + endArrowPos += arrow.GetSize() + arrow.GetSpacing() + elif ah == ARROW_POSITION_MIDDLE: + arrow.SetXOffset(middleArrowPos) + if arrow.GetXOffset() and not self._ignoreArrowOffsets: + self.DrawArrow(dc, arrow, arrow.GetXOffset(), True) + else: + self.DrawArrow(dc, arrow, middleArrowPos, False) + middleArrowPos += arrow.GetSize() + arrow.GetSpacing() + + def DrawArrow(self, dc, arrow, XOffset, proportionalOffset): + """Draw the given arrowhead (or annotation).""" + first_line_point = self._lineControlPoints[0] + second_line_point = self._lineControlPoints[1] + + last_line_point = self._lineControlPoints[-1] + second_last_line_point = self._lineControlPoints[-2] + + # Position of start point of line, at the end of which we draw the arrow + startPositionX, startPositionY = 0.0, 0.0 + + ap = arrow.GetPosition() + if ap == ARROW_POSITION_START: + # If we're using a proportional offset, calculate just where this + # will be on the line. + realOffset = XOffset + if proportionalOffset: + totalLength = math.sqrt((second_line_point[0] - first_line_point[0]) * (second_line_point[0] - first_line_point[0]) + (second_line_point[1] - first_line_point[1]) * (second_line_point[1] - first_line_point[1])) + realOffset = XOffset * totalLength + + positionOnLineX, positionOnLineY = GetPointOnLine(second_line_point[0], second_line_point[1], first_line_point[0], first_line_point[1], realOffset) + + startPositionX = second_line_point[0] + startPositionY = second_line_point[1] + elif ap == ARROW_POSITION_END: + # If we're using a proportional offset, calculate just where this + # will be on the line. + realOffset = XOffset + if proportionalOffset: + totalLength = math.sqrt((second_last_line_point[0] - last_line_point[0]) * (second_last_line_point[0] - last_line_point[0]) + (second_last_line_point[1] - last_line_point[1]) * (second_last_line_point[1] - last_line_point[1])); + realOffset = XOffset * totalLength + + positionOnLineX, positionOnLineY = GetPointOnLine(second_last_line_point[0], second_last_line_point[1], last_line_point[0], last_line_point[1], realOffset) + + startPositionX = second_last_line_point[0] + startPositionY = second_last_line_point[1] + elif ap == ARROW_POSITION_MIDDLE: + # Choose a point half way between the last and penultimate points + x = (last_line_point[0] + second_last_line_point[0]) / 2.0 + y = (last_line_point[1] + second_last_line_point[1]) / 2.0 + + # If we're using a proportional offset, calculate just where this + # will be on the line. + realOffset = XOffset + if proportionalOffset: + totalLength = math.sqrt((second_last_line_point[0] - x) * (second_last_line_point[0] - x) + (second_last_line_point[1] - y) * (second_last_line_point[1] - y)); + realOffset = XOffset * totalLength + + positionOnLineX, positionOnLineY = GetPointOnLine(second_last_line_point[0], second_last_line_point[1], x, y, realOffset) + startPositionX = second_last_line_point[0] + startPositionY = second_last_line_point[1] + + # Add yOffset to arrow, if any + + # The translation that the y offset may give + deltaX = 0.0 + deltaY = 0.0 + if arrow.GetYOffset and not self._ignoreArrowOffsets: + # |(x4, y4) + # |d + # | + # (x1, y1)--------------(x3, y3)------------------(x2, y2) + # x4 = x3 - d * math.sin(theta) + # y4 = y3 + d * math.cos(theta) + # + # Where theta = math.tan(-1) of (y3-y1) / (x3-x1) + x1 = startPositionX + y1 = startPositionY + x3 = float(positionOnLineX) + y3 = float(positionOnLineY) + d = -arrow.GetYOffset() # Negate so +offset is above line + + if x3 == x1: + theta = math.pi / 2.0 + else: + theta = math.atan((y3 - y1) / (x3 - x1)) + + x4 = x3 - d * math.sin(theta) + y4 = y3 + d * math.cos(theta) + + deltaX = x4 - positionOnLineX + deltaY = y4 - positionOnLineY + + at = arrow._GetType() + if at == ARROW_ARROW: + arrowLength = arrow.GetSize() + arrowWidth = arrowLength / 3.0 + + tip_x, tip_y, side1_x, side1_y, side2_x, side2_y = GetArrowPoints(startPositionX + deltaX, startPositionY + deltaY, positionOnLineX + deltaX, positionOnLineY + deltaY, arrowLength, arrowWidth) + + points = [[tip_x, tip_y], + [side1_x, side1_y], + [side2_x, side2_y], + [tip_x, tip_y]] + + dc.SetPen(self._pen) + dc.SetBrush(self._brush) + dc.DrawPolygon(points) + elif at in [ARROW_HOLLOW_CIRCLE, ARROW_FILLED_CIRCLE]: + # Find point on line of centre of circle, which is a radius away + # from the end position + diameter = arrow.GetSize() + x, y = GetPointOnLine(startPositionX + deltaX, startPositionY + deltaY, + positionOnLineX + deltaX, positionOnLineY + deltaY, + diameter / 2.0) + x1 = x - diameter / 2.0 + y1 = y - diameter / 2.0 + dc.SetPen(self._pen) + if arrow._GetType() == ARROW_HOLLOW_CIRCLE: + dc.SetBrush(self.GetBackgroundBrush()) + else: + dc.SetBrush(self._brush) + + dc.DrawEllipse(x1, y1, diameter, diameter) + elif at == ARROW_SINGLE_OBLIQUE: + pass + elif at == ARROW_METAFILE: + if arrow.GetMetaFile(): + # Find point on line of centre of object, which is a half-width away + # from the end position + # + # width + # <-- start pos <-----><-- positionOnLineX + # _____ + # --------------| x | <-- e.g. rectangular arrowhead + # ----- + # + x, y = GetPointOnLine(startPositionX, startPositionY, + positionOnLineX, positionOnLineY, + arrow.GetMetaFile()._width / 2.0) + # Calculate theta for rotating the metafile. + # + # | + # | o(x2, y2) 'o' represents the arrowhead. + # | / + # | / + # | /theta + # | /(x1, y1) + # |______________________ + # + theta = 0.0 + x1 = startPositionX + y1 = startPositionY + x2 = float(positionOnLineX) + y2 = float(positionOnLineY) + + if x1 == x2 and y1 == y2: + theta = 0.0 + elif x1 == x2 and y1 > y2: + theta = 3.0 * math.pi / 2.0 + elif x1 == x2 and y2 > y1: + theta = math.pi / 2.0 + elif x2 > x1 and y2 >= y1: + theta = math.atan((y2 - y1) / (x2 - x1)) + elif x2 < x1: + theta = math.pi + math.atan((y2 - y1) / (x2 - x1)) + elif x2 > x1 and y2 < y1: + theta = 2 * math.pi + math.atan((y2 - y1) / (x2 - x1)) + else: + raise "Unknown arrowhead rotation case" + + # Rotate about the centre of the object, then place + # the object on the line. + if arrow.GetMetaFile().GetRotateable(): + arrow.GetMetaFile().Rotate(0.0, 0.0, theta) + + if self._erasing: + # If erasing, just draw a rectangle + minX, minY, maxX, maxY = arrow.GetMetaFile().GetBounds() + # Make erasing rectangle slightly bigger or you get droppings + extraPixels = 4 + dc.DrawRectangle(deltaX + x + minX - extraPixels / 2.0, deltaY + y + minY - extraPixels / 2.0, maxX - minX + extraPixels, maxY - minY + extraPixels) + else: + arrow.GetMetaFile().Draw(dc, x + deltaX, y + deltaY) + + def OnErase(self, dc): + old_pen = self._pen + old_brush = self._brush + + bg_pen = self.GetBackgroundPen() + bg_brush = self.GetBackgroundBrush() + self.SetPen(bg_pen) + self.SetBrush(bg_brush) + + bound_x, bound_y = self.GetBoundingBoxMax() + if self._font: + dc.SetFont(self._font) + + # Undraw text regions + for i in range(3): + if self._regions[i]: + x, y = self.GetLabelPosition(i) + self.EraseRegion(dc, self._regions[i], x, y) + + # Undraw line + dc.SetPen(self.GetBackgroundPen()) + dc.SetBrush(self.GetBackgroundBrush()) + + # Drawing over the line only seems to work if the line has a thickness + # of 1. + if old_pen and old_pen.GetWidth() > 1: + dc.DrawRectangle(self._xpos - bound_x / 2.0 - 2, self._ypos - bound_y / 2.0 - 2, + bound_x + 4, bound_y + 4) + else: + self._erasing = True + self.GetEventHandler().OnDraw(dc) + self.GetEventHandler().OnEraseControlPoints(dc) + self._erasing = False + + if old_pen: + self.SetPen(old_pen) + if old_brush: + self.SetBrush(old_brush) + + def GetBoundingBoxMin(self): + x1, y1 = 10000, 10000 + x2, y2 = -10000, -10000 + + for point in self._lineControlPoints: + if point[0] < x1: + x1 = point[0] + if point[1] < y1: + y1 = point[1] + if point[0] > x2: + x2 = point[0] + if point[1] > y2: + y2 = point[1] + + return x2 - x1, y2 - y1 + + # For a node image of interest, finds the position of this arc + # amongst all the arcs which are attached to THIS SIDE of the node image, + # and the number of same. + def FindNth(self, image, incoming): + """Find the position of the line on the given object. + + Specify whether incoming or outgoing lines are being considered + with incoming. + """ + n = -1 + num = 0 + + if image == self._to: + this_attachment = self._attachmentTo + else: + this_attachment = self._attachmentFrom + + # Find number of lines going into / out of this particular attachment point + for line in image.GetLines(): + if line._from == image: + # This is the nth line attached to 'image' + if line == self and not incoming: + n = num + + # Increment num count if this is the same side (attachment number) + if line._attachmentFrom == this_attachment: + num += 1 + + if line._to == image: + # This is the nth line attached to 'image' + if line == self and incoming: + n = num + + # Increment num count if this is the same side (attachment number) + if line._attachmentTo == this_attachment: + num += 1 + + return n, num + + def OnDrawOutline(self, dc, x, y, w, h): + old_pen = self._pen + old_brush = self._brush + + dottedPen = wx.Pen(wx.Colour(0, 0, 0), 1, wx.DOT) + self.SetPen(dottedPen) + self.SetBrush(wx.TRANSPARENT_BRUSH) + + self.GetEventHandler().OnDraw(dc) + + if old_pen: + self.SetPen(old_pen) + else: + self.SetPen(None) + if old_brush: + self.SetBrush(old_brush) + else: + self.SetBrush(None) + + def OnMovePre(self, dc, x, y, old_x, old_y, display = True): + x_offset = x - old_x + y_offset = y - old_y + + if self._lineControlPoints and not (x_offset == 0 and y_offset == 0): + for point in self._lineControlPoints: + point[0] += x_offset + point[1] += y_offset + + # Move temporary label rectangles if necessary + for i in range(3): + if self._labelObjects[i]: + self._labelObjects[i].Erase(dc) + xp, yp = self.GetLabelPosition(i) + if i < len(self._regions): + xr, yr = self._regions[i].GetPosition() + else: + xr, yr = 0, 0 + self._labelObjects[i].Move(dc, xp + xr, yp + yr) + return True + + def OnMoveLink(self, dc, moveControlPoints = True): + """Called when a connected object has moved, to move the link to + correct position + """ + if not self._from or not self._to: + return + + # Do each end - nothing in the middle. User has to move other points + # manually if necessary + end_x, end_y, other_end_x, other_end_y = self.FindLineEndPoints() + + oldX, oldY = self._xpos, self._ypos + + # pi: The first time we go through FindLineEndPoints we can't + # use the middle points (since they don't have sane values), + # so we just do what we do for a normal line. Then we call + # Initialise to set the middle points, and then FindLineEndPoints + # again, but this time (and from now on) we use the middle + # points to calculate the end points. + # This was buggy in the C++ version too. + + self.SetEnds(end_x, end_y, other_end_x, other_end_y) + + if len(self._lineControlPoints) > 2: + self.Initialise() + + # Do a second time, because one may depend on the other + end_x, end_y, other_end_x, other_end_y = self.FindLineEndPoints() + self.SetEnds(end_x, end_y, other_end_x, other_end_y) + + # Try to move control points with the arc + x_offset = self._xpos - oldX + y_offset = self._ypos - oldY + + # Only move control points if it's a self link. And only works + # if attachment mode is ON + if self._from == self._to and self._from.GetAttachmentMode() != ATTACHMENT_MODE_NONE and moveControlPoints and self._lineControlPoints and not (x_offset == 0 and y_offset == 0): + for point in self._lineControlPoints[1:-1]: + point[0] += x_offset + point[1] += y_offset + + self.Move(dc, self._xpos, self._ypos) + + def FindLineEndPoints(self): + """Finds the x, y points at the two ends of the line. + + This function can be used by e.g. line-routing routines to + get the actual points on the two node images where the lines will be + drawn to / from. + """ + if not self._from or not self._to: + return + + # Do each end - nothing in the middle. User has to move other points + # manually if necessary. + second_point = self._lineControlPoints[1] + second_last_point = self._lineControlPoints[-2] + + # pi: If we have a segmented line and this is the first time, + # do this as a straight line. + if len(self._lineControlPoints) > 2 and self._initialised: + if self._from.GetAttachmentMode() != ATTACHMENT_MODE_NONE: + nth, no_arcs = self.FindNth(self._from, False) # Not incoming + end_x, end_y = self._from.GetAttachmentPosition(self._attachmentFrom, nth, no_arcs, self) + else: + end_x, end_y = self._from.GetPerimeterPoint(self._from.GetX(), self._from.GetY(), second_point[0], second_point[1]) + + if self._to.GetAttachmentMode() != ATTACHMENT_MODE_NONE: + nth, no_arch = self.FindNth(self._to, True) # Incoming + other_end_x, other_end_y = self._to.GetAttachmentPosition(self._attachmentTo, nth, no_arch, self) + else: + other_end_x, other_end_y = self._to.GetPerimeterPoint(self._to.GetX(), self._to.GetY(), second_last_point[0], second_last_point[1]) + else: + fromX = self._from.GetX() + fromY = self._from.GetY() + toX = self._to.GetX() + toY = self._to.GetY() + + if self._from.GetAttachmentMode() != ATTACHMENT_MODE_NONE: + nth, no_arcs = self.FindNth(self._from, False) + end_x, end_y = self._from.GetAttachmentPosition(self._attachmentFrom, nth, no_arcs, self) + fromX = end_x + fromY = end_y + + if self._to.GetAttachmentMode() != ATTACHMENT_MODE_NONE: + nth, no_arcs = self.FindNth(self._to, True) + other_end_x, other_end_y = self._to.GetAttachmentPosition(self._attachmentTo, nth, no_arcs, self) + toX = other_end_x + toY = other_end_y + + if self._from.GetAttachmentMode() == ATTACHMENT_MODE_NONE: + end_x, end_y = self._from.GetPerimeterPoint(self._from.GetX(), self._from.GetY(), toX, toY) + + if self._to.GetAttachmentMode() == ATTACHMENT_MODE_NONE: + other_end_x, other_end_y = self._to.GetPerimeterPoint(self._to.GetX(), self._to.GetY(), fromX, fromY) + + return end_x, end_y, other_end_x, other_end_y + + + def OnDraw(self, dc): + if not self._lineControlPoints: + return + + if self._pen: + dc.SetPen(self._pen) + if self._brush: + dc.SetBrush(self._brush) + + points = [] + for point in self._lineControlPoints: + points.append(wx.Point(point[0], point[1])) + + if self._isSpline: + dc.DrawSpline(points) + else: + dc.DrawLines(points) + + if sys.platform[:3] == "win": + # For some reason, last point isn't drawn under Windows + pt = points[-1] + dc.DrawPoint(pt[0], pt[1]) + + # Problem with pen - if not a solid pen, does strange things + # to the arrowhead. So make (get) a new pen that's solid. + if self._pen and self._pen.GetStyle() != wx.SOLID: + solid_pen = wx.Pen(self._pen.GetColour(), 1, wx.SOLID) + if solid_pen: + dc.SetPen(solid_pen) + + self.DrawArrows(dc) + + def OnDrawControlPoints(self, dc): + if not self._drawHandles: + return + + # Draw temporary label rectangles if necessary + for i in range(3): + if self._labelObjects[i]: + self._labelObjects[i].Draw(dc) + + Shape.OnDrawControlPoints(self, dc) + + def OnEraseControlPoints(self, dc): + # Erase temporary label rectangles if necessary + + for i in range(3): + if self._labelObjects[i]: + self._labelObjects[i].Erase(dc) + + Shape.OnEraseControlPoints(self, dc) + + def OnDragLeft(self, draw, x, y, keys = 0, attachment = 0): + pass + + def OnBeginDragLeft(self, x, y, keys = 0, attachment = 0): + pass + + def OnEndDragLeft(self, x, y, keys = 0, attachment = 0): + pass + + def OnDrawContents(self, dc): + if self.GetDisableLabel(): + return + + for i in range(3): + if self._regions[i]: + x, y = self.GetLabelPosition(i) + self.DrawRegion(dc, self._regions[i], x, y) + + def SetTo(self, object): + """Set the 'to' object for the line.""" + self._to = object + + def SetFrom(self, object): + """Set the 'from' object for the line.""" + self._from = object + + def MakeControlPoints(self): + """Make handle control points.""" + if self._canvas and self._lineControlPoints: + first = self._lineControlPoints[0] + last = self._lineControlPoints[-1] + + control = LineControlPoint(self._canvas, self, CONTROL_POINT_SIZE, first[0], first[1], CONTROL_POINT_ENDPOINT_FROM) + control._point = first + self._canvas.AddShape(control) + self._controlPoints.append(control) + + for point in self._lineControlPoints[1:-1]: + control = LineControlPoint(self._canvas, self, CONTROL_POINT_SIZE, point[0], point[1], CONTROL_POINT_LINE) + control._point = point + self._canvas.AddShape(control) + self._controlPoints.append(control) + + control = LineControlPoint(self._canvas, self, CONTROL_POINT_SIZE, last[0], last[1], CONTROL_POINT_ENDPOINT_TO) + control._point = last + self._canvas.AddShape(control) + self._controlPoints.append(control) + + def ResetControlPoints(self): + if self._canvas and self._lineControlPoints and self._controlPoints: + for i in range(min(len(self._controlPoints), len(self._lineControlPoints))): + point = self._lineControlPoints[i] + control = self._controlPoints[i] + control.SetX(point[0]) + control.SetY(point[1]) + + # Override select, to create / delete temporary label-moving objects + def Select(self, select, dc = None): + Shape.Select(self, select, dc) + if select: + for i in range(3): + if self._regions[i]: + region = self._regions[i] + if region._formattedText: + w, h = region.GetSize() + x, y = region.GetPosition() + xx, yy = self.GetLabelPosition(i) + + if self._labelObjects[i]: + self._labelObjects[i].Select(False) + self._labelObjects[i].RemoveFromCanvas(self._canvas) + + self._labelObjects[i] = self.OnCreateLabelShape(self, region, w, h) + self._labelObjects[i].AddToCanvas(self._canvas) + self._labelObjects[i].Show(True) + if dc: + self._labelObjects[i].Move(dc, x + xx, y + yy) + self._labelObjects[i].Select(True, dc) + else: + for i in range(3): + if self._labelObjects[i]: + self._labelObjects[i].Select(False, dc) + self._labelObjects[i].Erase(dc) + self._labelObjects[i].RemoveFromCanvas(self._canvas) + self._labelObjects[i] = None + + # Control points ('handles') redirect control to the actual shape, to + # make it easier to override sizing behaviour. + def OnSizingDragLeft(self, pt, draw, x, y, keys = 0, attachment = 0): + dc = wx.ClientDC(self.GetCanvas()) + self.GetCanvas().PrepareDC(dc) + + dc.SetLogicalFunction(OGLRBLF) + + dottedPen = wx.Pen(wx.Colour(0, 0, 0), 1, wx.DOT) + dc.SetPen(dottedPen) + dc.SetBrush(wx.TRANSPARENT_BRUSH) + + if pt._type == CONTROL_POINT_LINE: + x, y = self._canvas.Snap(x, y) + + pt.SetX(x) + pt.SetY(y) + pt._point[0] = x + pt._point[1] = y + + old_pen = self.GetPen() + old_brush = self.GetBrush() + + self.SetPen(dottedPen) + self.SetBrush(wx.TRANSPARENT_BRUSH) + + self.GetEventHandler().OnMoveLink(dc, False) + + self.SetPen(old_pen) + self.SetBrush(old_brush) + + def OnSizingBeginDragLeft(self, pt, x, y, keys = 0, attachment = 0): + dc = wx.ClientDC(self.GetCanvas()) + self.GetCanvas().PrepareDC(dc) + + if pt._type == CONTROL_POINT_LINE: + pt._originalPos = pt._point + x, y = self._canvas.Snap(x, y) + + self.Erase(dc) + + # Redraw start and end objects because we've left holes + # when erasing the line + self.GetFrom().OnDraw(dc) + self.GetFrom().OnDrawContents(dc) + self.GetTo().OnDraw(dc) + self.GetTo().OnDrawContents(dc) + + self.SetDisableLabel(True) + dc.SetLogicalFunction(OGLRBLF) + + pt._xpos = x + pt._ypos = y + pt._point[0] = x + pt._point[1] = y + + old_pen = self.GetPen() + old_brush = self.GetBrush() + + dottedPen = wx.Pen(wx.Colour(0, 0, 0), 1, wx.DOT) + self.SetPen(dottedPen) + self.SetBrush(wx.TRANSPARENT_BRUSH) + + self.GetEventHandler().OnMoveLink(dc, False) + + self.SetPen(old_pen) + self.SetBrush(old_brush) + + if pt._type == CONTROL_POINT_ENDPOINT_FROM or pt._type == CONTROL_POINT_ENDPOINT_TO: + self._canvas.SetCursor(wx.StockCursor(wx.CURSOR_BULLSEYE)) + pt._oldCursor = wx.STANDARD_CURSOR + + def OnSizingEndDragLeft(self, pt, x, y, keys = 0, attachment = 0): + dc = wx.ClientDC(self.GetCanvas()) + self.GetCanvas().PrepareDC(dc) + + self.SetDisableLabel(False) + + if pt._type == CONTROL_POINT_LINE: + x, y = self._canvas.Snap(x, y) + + rpt = wx.RealPoint(x, y) + + # Move the control point back to where it was; + # MoveControlPoint will move it to the new position + # if it decides it wants. We only moved the position + # during user feedback so we could redraw the line + # as it changed shape. + pt._xpos = pt._originalPos[0] + pt._ypos = pt._originalPos[1] + pt._point[0] = pt._originalPos[0] + pt._point[1] = pt._originalPos[1] + + self.OnMoveMiddleControlPoint(dc, pt, rpt) + + if pt._type == CONTROL_POINT_ENDPOINT_FROM: + if pt._oldCursor: + self._canvas.SetCursor(pt._oldCursor) + + if self.GetFrom(): + self.GetFrom().MoveLineToNewAttachment(dc, self, x, y) + + if pt._type == CONTROL_POINT_ENDPOINT_TO: + if pt._oldCursor: + self._canvas.SetCursor(pt._oldCursor) + + if self.GetTo(): + self.GetTo().MoveLineToNewAttachment(dc, self, x, y) + + # This is called only when a non-end control point is moved + def OnMoveMiddleControlPoint(self, dc, lpt, pt): + lpt._xpos = pt[0] + lpt._ypos = pt[1] + + lpt._point[0] = pt[0] + lpt._point[1] = pt[1] + + self.GetEventHandler().OnMoveLink(dc) + + return True + + def AddArrow(self, type, end = ARROW_POSITION_END, size = 10.0, xOffset = 0.0, name = "", mf = None, arrowId = -1): + """Add an arrow (or annotation) to the line. + + type may currently be one of: + + ARROW_HOLLOW_CIRCLE + Hollow circle. + ARROW_FILLED_CIRCLE + Filled circle. + ARROW_ARROW + Conventional arrowhead. + ARROW_SINGLE_OBLIQUE + Single oblique stroke. + ARROW_DOUBLE_OBLIQUE + Double oblique stroke. + ARROW_DOUBLE_METAFILE + Custom arrowhead. + + end may currently be one of: + + ARROW_POSITION_END + Arrow appears at the end. + ARROW_POSITION_START + Arrow appears at the start. + + arrowSize specifies the length of the arrow. + + xOffset specifies the offset from the end of the line. + + name specifies a name for the arrow. + + mf can be a wxPseduoMetaFile, perhaps loaded from a simple Windows + metafile. + + arrowId is the id for the arrow. + """ + arrow = ArrowHead(type, end, size, xOffset, name, mf, arrowId) + self._arcArrows.append(arrow) + return arrow + + # Add arrowhead at a particular position in the arrowhead list + def AddArrowOrdered(self, arrow, referenceList, end): + """Add an arrowhead in the position indicated by the reference list + of arrowheads, which contains all legal arrowheads for this line, in + the correct order. E.g. + + Reference list: a b c d e + Current line list: a d + + Add c, then line list is: a c d. + + If no legal arrowhead position, return FALSE. Assume reference list + is for one end only, since it potentially defines the ordering for + any one of the 3 positions. So we don't check the reference list for + arrowhead position. + """ + if not referenceList: + return False + + targetName = arrow.GetName() + + # First check whether we need to insert in front of list, + # because this arrowhead is the first in the reference + # list and should therefore be first in the current list. + refArrow = referenceList[0] + if refArrow.GetName() == targetName: + self._arcArrows.insert(0, arrow) + return True + + i1 = i2 = 0 + while i1 < len(referenceList) and i2 < len(self._arcArrows): + refArrow = referenceList[i1] + currArrow = self._arcArrows[i2] + + # Matching: advance current arrow pointer + if currArrow.GetArrowEnd() == end and currArrow.GetName() == refArrow.GetName(): + i2 += 1 + + # Check if we're at the correct position in the + # reference list + if targetName == refArrow.GetName(): + if i2 < len(self._arcArrows): + self._arcArrows.insert(i2, arrow) + else: + self._arcArrows.append(arrow) + return True + i1 += 1 + + self._arcArrows.append(arrow) + return True + + def ClearArrowsAtPosition(self, end): + """Delete the arrows at the specified position, or at any position + if position is -1. + """ + if end == -1: + self._arcArrows = [] + return + + for arrow in self._arcArrows: + if arrow.GetArrowEnd() == end: + self._arcArrows.remove(arrow) + + def ClearArrow(self, name): + """Delete the arrow with the given name.""" + for arrow in self._arcArrows: + if arrow.GetName() == name: + self._arcArrows.remove(arrow) + return True + return False + + def FindArrowHead(self, position, name): + """Find arrowhead by position and name. + + if position is -1, matches any position. + """ + for arrow in self._arcArrows: + if (position == -1 or position == arrow.GetArrowEnd()) and arrow.GetName() == name: + return arrow + + return None + + def FindArrowHeadId(self, arrowId): + """Find arrowhead by id.""" + for arrow in self._arcArrows: + if arrowId == arrow.GetId(): + return arrow + + return None + + def DeleteArrowHead(self, position, name): + """Delete arrowhead by position and name. + + if position is -1, matches any position. + """ + for arrow in self._arcArrows: + if (position == -1 or position == arrow.GetArrowEnd()) and arrow.GetName() == name: + self._arcArrows.remove(arrow) + return True + return False + + def DeleteArrowHeadId(self, id): + """Delete arrowhead by id.""" + for arrow in self._arcArrows: + if arrowId == arrow.GetId(): + self._arcArrows.remove(arrow) + return True + return False + + # Calculate the minimum width a line + # occupies, for the purposes of drawing lines in tools. + def FindMinimumWidth(self): + """Find the horizontal width for drawing a line with arrows in + minimum space. Assume arrows at end only. + """ + minWidth = 0.0 + for arrowHead in self._arcArrows: + minWidth += arrowHead.GetSize() + if arrowHead != self._arcArrows[-1]: + minWidth += arrowHead + GetSpacing + + # We have ABSOLUTE minimum now. So + # scale it to give it reasonable aesthetics + # when drawing with line. + if minWidth > 0: + minWidth = minWidth * 1.4 + else: + minWidth = 20.0 + + self.SetEnds(0.0, 0.0, minWidth, 0.0) + self.Initialise() + + return minWidth + + def FindLinePosition(self, x, y): + """Find which position we're talking about at this x, y. + + Returns ARROW_POSITION_START, ARROW_POSITION_MIDDLE, ARROW_POSITION_END. + """ + startX, startY, endX, endY = self.GetEnds() + + # Find distances from centre, start and end. The smallest wins + centreDistance = math.sqrt((x - self._xpos) * (x - self._xpos) + (y - self._ypos) * (y - self._ypos)) + startDistance = math.sqrt((x - startX) * (x - startX) + (y - startY) * (y - startY)) + endDistance = math.sqrt((x - endX) * (x - endX) + (y - endY) * (y - endY)) + + if centreDistance < startDistance and centreDistance < endDistance: + return ARROW_POSITION_MIDDLE + elif startDistance < endDistance: + return ARROW_POSITION_START + else: + return ARROW_POSITION_END + + def SetAlignmentOrientation(self, isEnd, isHoriz): + if isEnd: + if isHoriz and self._alignmentEnd & LINE_ALIGNMENT_HORIZ != LINE_ALIGNMENT_HORIZ: + self._alignmentEnd != LINE_ALIGNMENT_HORIZ + elif not isHoriz and self._alignmentEnd & LINE_ALIGNMENT_HORIZ == LINE_ALIGNMENT_HORIZ: + self._alignmentEnd -= LINE_ALIGNMENT_HORIZ + else: + if isHoriz and self._alignmentStart & LINE_ALIGNMENT_HORIZ != LINE_ALIGNMENT_HORIZ: + self._alignmentStart != LINE_ALIGNMENT_HORIZ + elif not isHoriz and self._alignmentStart & LINE_ALIGNMENT_HORIZ == LINE_ALIGNMENT_HORIZ: + self._alignmentStart -= LINE_ALIGNMENT_HORIZ + + def SetAlignmentType(self, isEnd, alignType): + if isEnd: + if alignType == LINE_ALIGNMENT_TO_NEXT_HANDLE: + if self._alignmentEnd & LINE_ALIGNMENT_TO_NEXT_HANDLE != LINE_ALIGNMENT_TO_NEXT_HANDLE: + self._alignmentEnd |= LINE_ALIGNMENT_TO_NEXT_HANDLE + elif self._alignmentEnd & LINE_ALIGNMENT_TO_NEXT_HANDLE == LINE_ALIGNMENT_TO_NEXT_HANDLE: + self._alignmentEnd -= LINE_ALIGNMENT_TO_NEXT_HANDLE + else: + if alignType == LINE_ALIGNMENT_TO_NEXT_HANDLE: + if self._alignmentStart & LINE_ALIGNMENT_TO_NEXT_HANDLE != LINE_ALIGNMENT_TO_NEXT_HANDLE: + self._alignmentStart |= LINE_ALIGNMENT_TO_NEXT_HANDLE + elif self._alignmentStart & LINE_ALIGNMENT_TO_NEXT_HANDLE == LINE_ALIGNMENT_TO_NEXT_HANDLE: + self._alignmentStart -= LINE_ALIGNMENT_TO_NEXT_HANDLE + + def GetAlignmentOrientation(self, isEnd): + if isEnd: + return self._alignmentEnd & LINE_ALIGNMENT_HORIZ == LINE_ALIGNMENT_HORIZ + else: + return self._alignmentStart & LINE_ALIGNMENT_HORIZ == LINE_ALIGNMENT_HORIZ + + def GetAlignmentType(self, isEnd): + if isEnd: + return self._alignmentEnd & LINE_ALIGNMENT_TO_NEXT_HANDLE + else: + return self._alignmentStart & LINE_ALIGNMENT_TO_NEXT_HANDLE + + def GetNextControlPoint(self, shape): + """Find the next control point in the line after the start / end point, + depending on whether the shape is at the start or end. + """ + n = len(self._lineControlPoints) + if self._to == shape: + # Must be END of line, so we want (n - 1)th control point. + # But indexing ends at n-1, so subtract 2. + nn = n - 2 + else: + nn = 1 + if nn < len(self._lineControlPoints): + return self._lineControlPoints[nn] + return None + + def OnCreateLabelShape(self, parent, region, w, h): + return LabelShape(parent, region, w, h) + + + def OnLabelMovePre(self, dc, labelShape, x, y, old_x, old_y, display): + labelShape._shapeRegion.SetSize(labelShape.GetWidth(), labelShape.GetHeight()) + + # Find position in line's region list + i = self._regions.index(labelShape._shapeRegion) + + xx, yy = self.GetLabelPosition(i) + # Set the region's offset, relative to the default position for + # each region. + labelShape._shapeRegion.SetPosition(x - xx, y - yy) + labelShape.SetX(x) + labelShape.SetY(y) + + # Need to reformat to fit region + if labelShape._shapeRegion.GetText(): + s = labelShape._shapeRegion.GetText() + labelShape.FormatText(dc, s, i) + self.DrawRegion(dc, labelShape._shapeRegion, xx, yy) + return True + diff --git a/wx/lib/ogl/_oglmisc.py b/wx/lib/ogl/_oglmisc.py new file mode 100644 index 00000000..e29df42c --- /dev/null +++ b/wx/lib/ogl/_oglmisc.py @@ -0,0 +1,415 @@ +# -*- coding: utf-8 -*- +#---------------------------------------------------------------------------- +# Name: oglmisc.py +# Purpose: Miscellaneous OGL support functions +# +# Author: Pierre Hjälm (from C++ original by Julian Smart) +# +# Created: 2004-05-08 +# RCS-ID: $Id$ +# Copyright: (c) 2004 Pierre Hjälm - 1998 Julian Smart +# Licence: wxWindows license +#---------------------------------------------------------------------------- + +import math + +import wx + +# Control point types +# Rectangle and most other shapes +CONTROL_POINT_VERTICAL = 1 +CONTROL_POINT_HORIZONTAL = 2 +CONTROL_POINT_DIAGONAL = 3 + +# Line +CONTROL_POINT_ENDPOINT_TO = 4 +CONTROL_POINT_ENDPOINT_FROM = 5 +CONTROL_POINT_LINE = 6 + +# Types of formatting: can be combined in a bit list +FORMAT_NONE = 0 # Left justification +FORMAT_CENTRE_HORIZ = 1 # Centre horizontally +FORMAT_CENTRE_VERT = 2 # Centre vertically +FORMAT_SIZE_TO_CONTENTS = 4 # Resize shape to contents + +# Attachment modes +ATTACHMENT_MODE_NONE, ATTACHMENT_MODE_EDGE, ATTACHMENT_MODE_BRANCHING = 0, 1, 2 + +# Shadow mode +SHADOW_NONE, SHADOW_LEFT, SHADOW_RIGHT = 0, 1, 2 + +OP_CLICK_LEFT, OP_CLICK_RIGHT, OP_DRAG_LEFT, OP_DRAG_RIGHT = 1, 2, 4, 8 +OP_ALL = OP_CLICK_LEFT | OP_CLICK_RIGHT | OP_DRAG_LEFT | OP_DRAG_RIGHT + +# Sub-modes for branching attachment mode +BRANCHING_ATTACHMENT_NORMAL = 1 +BRANCHING_ATTACHMENT_BLOB = 2 + +# logical function to use when drawing rubberband boxes, etc. +OGLRBLF = wx.INVERT + +CONTROL_POINT_SIZE = 6 + +# Types of arrowhead +# (i) Built-in +ARROW_HOLLOW_CIRCLE = 1 +ARROW_FILLED_CIRCLE = 2 +ARROW_ARROW = 3 +ARROW_SINGLE_OBLIQUE = 4 +ARROW_DOUBLE_OBLIQUE = 5 +# (ii) Custom +ARROW_METAFILE = 20 + +# Position of arrow on line +ARROW_POSITION_START = 0 +ARROW_POSITION_END = 1 +ARROW_POSITION_MIDDLE = 2 + +# Line alignment flags +# Vertical by default +LINE_ALIGNMENT_HORIZ = 1 +LINE_ALIGNMENT_VERT = 0 +LINE_ALIGNMENT_TO_NEXT_HANDLE = 2 +LINE_ALIGNMENT_NONE = 0 + + + +# Format a string to a list of strings that fit in the given box. +# Interpret %n and 10 or 13 as a new line. +def FormatText(dc, text, width, height, formatMode): + i = 0 + word = "" + word_list = [] + end_word = False + new_line = False + while i < len(text): + if text[i] == "%": + i += 1 + if i == len(text): + word += "%" + else: + if text[i] == "n": + new_line = True + end_word = True + i += 1 + else: + word += "%" + text[i] + i += 1 + elif text[i] in ["\012","\015"]: + new_line = True + end_word = True + i += 1 + elif text[i] == " ": + end_word = True + i += 1 + else: + word += text[i] + i += 1 + + if i == len(text): + end_word = True + + if end_word: + word_list.append(word) + word = "" + end_word = False + if new_line: + word_list.append(None) + new_line = False + + # Now, make a list of strings which can fit in the box + string_list = [] + buffer = "" + for s in word_list: + oldBuffer = buffer + if s is None: + # FORCE NEW LINE + if len(buffer) > 0: + string_list.append(buffer) + buffer = "" + else: + if len(buffer): + buffer += " " + buffer += s + x, y = dc.GetTextExtent(buffer) + + # Don't fit within the bounding box if we're fitting + # shape to contents + if (x > width) and not (formatMode & FORMAT_SIZE_TO_CONTENTS): + # Deal with first word being wider than box + if len(oldBuffer): + string_list.append(oldBuffer) + buffer = s + if len(buffer): + string_list.append(buffer) + + return string_list + + + +def GetCentredTextExtent(dc, text_list, xpos = 0, ypos = 0, width = 0, height = 0): + if not text_list: + return 0, 0 + + max_width = 0 + for line in text_list: + current_width, char_height = dc.GetTextExtent(line.GetText()) + if current_width > max_width: + max_width = current_width + + return max_width, len(text_list) * char_height + + + +def CentreText(dc, text_list, xpos, ypos, width, height, formatMode): + if not text_list: + return + + # First, get maximum dimensions of box enclosing text + char_height = 0 + max_width = 0 + current_width = 0 + + # Store text extents for speed + widths = [] + for line in text_list: + current_width, char_height = dc.GetTextExtent(line.GetText()) + widths.append(current_width) + if current_width > max_width: + max_width = current_width + + max_height = len(text_list) * char_height + + if formatMode & FORMAT_CENTRE_VERT: + if max_height < height: + yoffset = ypos - height / 2.0 + (height - max_height) / 2.0 + else: + yoffset = ypos - height / 2.0 + yOffset = ypos + else: + yoffset = 0.0 + yOffset = 0.0 + + if formatMode & FORMAT_CENTRE_HORIZ: + xoffset = xpos - width / 2.0 + xOffset = xpos + else: + xoffset = 0.0 + xOffset = 0.0 + + for i, line in enumerate(text_list): + if formatMode & FORMAT_CENTRE_HORIZ and widths[i] < width: + x = (width - widths[i]) / 2.0 + xoffset + else: + x = xoffset + y = i * char_height + yoffset + + line.SetX(x - xOffset) + line.SetY(y - yOffset) + + + +def DrawFormattedText(dc, text_list, xpos, ypos, width, height, formatMode): + if formatMode & FORMAT_CENTRE_HORIZ: + xoffset = xpos + else: + xoffset = xpos - width / 2.0 + + if formatMode & FORMAT_CENTRE_VERT: + yoffset = ypos + else: + yoffset = ypos - height / 2.0 + + # +1 to allow for rounding errors + dc.SetClippingRegion(xpos - width / 2.0, ypos - height / 2.0, width + 1, height + 1) + + for line in text_list: + dc.DrawText(line.GetText(), xoffset + line.GetX(), yoffset + line.GetY()) + + dc.DestroyClippingRegion() + + + +def RoughlyEqual(val1, val2, tol = 0.00001): + return val1 < (val2 + tol) and val1 > (val2 - tol) and \ + val2 < (val1 + tol) and val2 > (val1 - tol) + + + +def FindEndForBox(width, height, x1, y1, x2, y2): + xvec = [x1 - width / 2.0, x1 - width / 2.0, x1 + width / 2.0, x1 + width / 2.0, x1 - width / 2.0] + yvec = [y1 - height / 2.0, y1 + height / 2.0, y1 + height / 2.0, y1 - height / 2.0, y1 - height / 2.0] + + return FindEndForPolyline(xvec, yvec, x2, y2, x1, y1) + + + +def CheckLineIntersection(x1, y1, x2, y2, x3, y3, x4, y4): + denominator_term = (y4 - y3) * (x2 - x1) - (y2 - y1) * (x4 - x3) + numerator_term = (x3 - x1) * (y4 - y3) + (x4 - x3) * (y1 - y3) + + length_ratio = 1.0 + k_line = 1.0 + + # Check for parallel lines + if denominator_term < 0.005 and denominator_term > -0.005: + line_constant = -1.0 + else: + line_constant = float(numerator_term) / denominator_term + + # Check for intersection + if line_constant < 1.0 and line_constant > 0.0: + # Now must check that other line hits + if (y4 - y3) < 0.005 and (y4 - y3) > -0.005: + k_line = (x1 - x3 + line_constant * (x2 - x1)) / (x4 - x3) + else: + k_line = (y1 - y3 + line_constant * (y2 - y1)) / (y4 - y3) + if k_line >= 0 and k_line < 1: + length_ratio = line_constant + else: + k_line = 1 + + return length_ratio, k_line + + + +def FindEndForPolyline(xvec, yvec, x1, y1, x2, y2): + lastx = xvec[0] + lasty = yvec[0] + + min_ratio = 1.0 + + for i in range(1, len(xvec)): + line_ratio, other_ratio = CheckLineIntersection(x1, y1, x2, y2, lastx, lasty, xvec[i], yvec[i]) + lastx = xvec[i] + lasty = yvec[i] + + if line_ratio < min_ratio: + min_ratio = line_ratio + + # Do last (implicit) line if last and first doubles are not identical + if not (xvec[0] == lastx and yvec[0] == lasty): + line_ratio, other_ratio = CheckLineIntersection(x1, y1, x2, y2, lastx, lasty, xvec[0], yvec[0]) + if line_ratio < min_ratio: + min_ratio = line_ratio + + return x1 + (x2 - x1) * min_ratio, y1 + (y2 - y1) * min_ratio + + + +def PolylineHitTest(xvec, yvec, x1, y1, x2, y2): + isAHit = False + lastx = xvec[0] + lasty = yvec[0] + + min_ratio = 1.0 + + for i in range(1, len(xvec)): + line_ratio, other_ratio = CheckLineIntersection(x1, y1, x2, y2, lastx, lasty, xvec[i], yvec[i]) + if line_ratio != 1.0: + isAHit = True + lastx = xvec[i] + lasty = yvec[i] + + if line_ratio < min_ratio: + min_ratio = line_ratio + + # Do last (implicit) line if last and first doubles are not identical + if not (xvec[0] == lastx and yvec[0] == lasty): + line_ratio, other_ratio = CheckLineIntersection(x1, y1, x2, y2, lastx, lasty, xvec[0], yvec[0]) + if line_ratio != 1.0: + isAHit = True + + return isAHit + + + +def GraphicsStraightenLine(point1, point2): + dx = point2[0] - point1[0] + dy = point2[1] - point1[1] + + if dx == 0: + return + elif abs(float(dy) / dx) > 1: + point2[0] = point1[0] + else: + point2[1] = point1[1] + + + +def GetPointOnLine(x1, y1, x2, y2, length): + l = math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)) + if l < 0.01: + l = 0.01 + + i_bar = (x2 - x1) / l + j_bar = (y2 - y1) / l + + return -length * i_bar + x2, -length * j_bar + y2 + + + +def GetArrowPoints(x1, y1, x2, y2, length, width): + l = math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)) + + if l < 0.01: + l = 0.01 + + i_bar = (x2 - x1) / l + j_bar = (y2 - y1) / l + + x3 = -length * i_bar + x2 + y3 = -length * j_bar + y2 + + return x2, y2, width * -j_bar + x3, width * i_bar + y3, -width * -j_bar + x3, -width * i_bar + y3 + + + +def DrawArcToEllipse(x1, y1, width1, height1, x2, y2, x3, y3): + a1 = width1 / 2.0 + b1 = height1 / 2.0 + + # Check that x2 != x3 + if abs(x2 - x3) < 0.05: + x4 = x2 + if y3 > y2: + y4 = y1 - math.sqrt((b1 * b1 - (((x2 - x1) * (x2 - x1)) * (b1 * b1) / (a1 * a1)))) + else: + y4 = y1 + math.sqrt((b1 * b1 - (((x2 - x1) * (x2 - x1)) * (b1 * b1) / (a1 * a1)))) + return x4, y4 + + # Calculate the x and y coordinates of the point where arc intersects ellipse + A = (1 / (a1 * a1)) + B = ((y3 - y2) * (y3 - y2)) / ((x3 - x2) * (x3 - x2) * b1 * b1) + C = (2 * (y3 - y2) * (y2 - y1)) / ((x3 - x2) * b1 * b1) + D = ((y2 - y1) * (y2 - y1)) / (b1 * b1) + E = (A + B) + F = (C - (2 * A * x1) - (2 * B * x2)) + G = ((A * x1 * x1) + (B * x2 * x2) - (C * x2) + D - 1) + H = (float(y3 - y2) / (x3 - x2)) + K = ((F * F) - (4 * E * G)) + + if K >= 0: + # In this case the line intersects the ellipse, so calculate intersection + if x2 >= x1: + ellipse1_x = ((F * -1) + math.sqrt(K)) / (2 * E) + ellipse1_y = ((H * (ellipse1_x - x2)) + y2) + else: + ellipse1_x = (((F * -1) - math.sqrt(K)) / (2 * E)) + ellipse1_y = ((H * (ellipse1_x - x2)) + y2) + else: + # in this case, arc does not intersect ellipse, so just draw arc + ellipse1_x = x3 + ellipse1_y = y3 + + return ellipse1_x, ellipse1_y + + + +def FindEndForCircle(radius, x1, y1, x2, y2): + H = math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)) + + if H == 0: + return x1, y1 + else: + return radius * (x2 - x1) / H + x1, radius * (y2 - y1) / H + y1 diff --git a/wx/lib/pdfwin.py b/wx/lib/pdfwin.py new file mode 100644 index 00000000..73fadbac --- /dev/null +++ b/wx/lib/pdfwin.py @@ -0,0 +1,296 @@ +#---------------------------------------------------------------------- +# Name: wx.lib.pdfwin +# Purpose: A class that allows the use of the Acrobat PDF reader +# ActiveX control +# +# Author: Robin Dunn +# +# Created: 22-March-2004 +# RCS-ID: $Id$ +# Copyright: (c) 2008 by Total Control Software +# Licence: wxWindows license +#---------------------------------------------------------------------- + +import wx + +_min_adobe_version = None + +def get_min_adobe_version(): + return _min_adobe_version + +def get_acroversion(): + " Included for backward compatibility" + return _min_adobe_version + +#---------------------------------------------------------------------- + +if wx.PlatformInfo[1] == 'wxMSW': + import wx.lib.activex + import comtypes.client as cc + import comtypes + import ctypes + + try: # Adobe Reader >= 7.0 + cc.GetModule( ('{05BFD3F1-6319-4F30-B752-C7A22889BCC4}', 1, 0) ) + progID = 'AcroPDF.PDF.1' + _min_adobe_version = 7.0 + except: + try: # Adobe Reader 5 or 6 + cc.GetModule( ('{CA8A9783-280D-11CF-A24D-444553540000}', 1, 0) ) + progID = 'PDF.PdfCtrl.5' + _min_adobe_version = 5.0 + except: + pass # Adobe Reader not installed (progID is not defined) + # Use get_min_adobe_version() before instantiating PDFWindow + + #------------------------------------------------------------------------------ + + class PDFWindow(wx.lib.activex.ActiveXCtrl): + def __init__(self, parent, id=-1, pos=wx.DefaultPosition, + size=wx.DefaultSize, style=0, name='PDFWindow'): + wx.lib.activex.ActiveXCtrl.__init__(self, parent, progID, + id, pos, size, style, name) + self.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroyWindow) + + def OnDestroyWindow(self, event): + wx.CallAfter(self.FreeDlls) + + def FreeDlls(self): + """ + Unloads any DLLs that are no longer in use when all COM object instances are + released. This prevents the error 'The instruction at "0x0700609c" referenced + memory at "0x00000014". The memory could not be read' when application closes + """ + ctypes.windll.ole32.CoFreeUnusedLibraries() + + def LoadFile(self, fileName): + """ + Opens and displays the specified document within the browser. + """ + return self.ctrl.LoadFile(fileName) + + def GetVersions(self): + """ + Deprecated: No longer available - do not use. + """ + return self.ctrl.GetVersions() + + def Print(self): + """ + Prints the document according to the specified options in a user dialog box. + """ + return self.ctrl.Print() + + def goBackwardStack(self): + """ + Goes to the previous view on the view stack, if it exists. + """ + return self.ctrl.goBackwardStack() + + def goForwardStack(self): + """ + Goes to the next view on the view stack, if it exists. + """ + return self.ctrl.goForwardStack() + + def gotoFirstPage(self): + """ + Goes to the first page in the document. + """ + return self.ctrl.gotoFirstPage() + + def gotoLastPage(self): + """ + Goes to the last page in the document. + """ + return self.ctrl.gotoLastPage() + + def gotoNextPage(self): + """ + Goes to the next page in the document, if it exists + """ + return self.ctrl.gotoNextPage() + + def gotoPreviousPage(self): + """ + Goes to the previous page in the document, if it exists. + """ + return self.ctrl.gotoPreviousPage() + + def printAll(self): + """ + Prints the entire document without displaying a user + dialog box. The current printer, page settings, and job + settings are used. This method returns immediately, even + if the printing has not completed. + """ + return self.ctrl.printAll() + + def printAllFit(self, shrinkToFit): + """ + Prints the entire document without a user dialog box, and + (if shrinkToFit) shrinks pages as needed to fit the + imageable area of a page in the printer. + """ + return self.ctrl.printAllFit(shrinkToFit) + + def printPages(self, from_, to): + """ + Prints the specified pages without displaying a user dialog box. + """ + return self.ctrl.printPages(from_, to) + + def printPagesFit(self, from_, to, shrinkToFit): + """ + Prints the specified pages without displaying a user + dialog box, and (if shrinkToFit) shrinks pages as needed + to fit the imageable area of a page in the printer. + """ + return self.ctrl.printPagesFit( from_, to, shrinkToFit) + + def printWithDialog(self): + """ + Prints the document according to the specified options in + a user dialog box. These options may include embedded + printing and specifying which printer is to be used. + + NB. The page range in the dialog defaults to + 'From Page 1 to 1' - Use Print() above instead. (dfh) + """ + return self.ctrl.printWithDialog() + + def setCurrentHighlight(self, a, b, c, d): + return self.ctrl.setCurrentHighlight(a, b, c, d) + + def setCurrentPage(self, npage): + """ + Goes to the specified page in the document. Maintains the + current location within the page and zoom level. npage is + the page number of the destination page. The first page + in a document is page 0. + + ## Oh no it isn't! The first page is 1 (dfh) + """ + return self.ctrl.setCurrentPage(npage) + + def setLayoutMode(self, layoutMode): + """ + LayoutMode possible values: + + ================= ==================================== + 'DontCare' use the current user preference + 'SinglePage' use single page mode (as in pre-Acrobat 3.0 viewers) + 'OneColumn' use one-column continuous mode + 'TwoColumnLeft' use two-column continuous mode, first page on the left + 'TwoColumnRight' use two-column continuous mode, first page on the right + ================= ==================================== + """ + return self.ctrl.setLayoutMode(layoutMode) + + def setNamedDest(self, namedDest): + """ + Changes the page view to the named destination in the specified string. + """ + return self.ctrl.setNamedDest(namedDest) + + def setPageMode(self, pageMode): + """ + Sets the page mode to display the document only, or to + additionally display bookmarks or thumbnails. pageMode = + 'none' or 'bookmarks' or 'thumbs'. + + ## NB.'thumbs' is case-sensitive, the other are not (dfh) + """ + return self.ctrl.setPageMode(pageMode) + + def setShowScrollbars(self, On): + """ + Determines whether scrollbars will appear in the document + view. + + ## NB. If scrollbars are off, the navigation tools disappear as well (dfh) + """ + return self.ctrl.setShowScrollbars(On) + + def setShowToolbar(self, On): + """ + Determines whether a toolbar will appear in the application. + """ + return self.ctrl.setShowToolbar(On) + + def setView(self, viewMode): + """ + Determines how the page will fit in the current view. + viewMode possible values: + + ======== ============================================== + 'Fit' fits whole page within the window both vertically and horizontally. + 'FitH' fits the width of the page within the window. + 'FitV' fits the height of the page within the window. + 'FitB' fits bounding box within the window both vertically and horizontally. + 'FitBH' fits the width of the bounding box within the window. + 'FitBV' fits the height of the bounding box within the window. + ======== ============================================== + """ + return self.ctrl.setView(viewMode) + + def setViewRect(self, left, top, width, height): + """ + Sets the view rectangle according to the specified coordinates. + + :param left: The upper left horizontal coordinate. + :param top: The vertical coordinate in the upper left corner. + :param width: The horizontal width of the rectangle. + :param height: The vertical height of the rectangle. + """ + return self.ctrl.setViewRect(left, top, width, height) + + def setViewScroll(self, viewMode, offset): + """ + Sets the view of a page according to the specified string. + Depending on the view mode, the page is either scrolled to + the right or scrolled down by the amount specified in + offset. Possible values of viewMode are as in setView + above. offset is the horizontal or vertical coordinate + positioned either at the left or top edge. + """ + return self.ctrl.setViewScroll(viewMode, offset) + + def setZoom(self, percent): + """ + Sets the magnification according to the specified value + expressed as a percentage (float) + """ + return self.ctrl.setZoom(percent) + + def setZoomScroll(self, percent, left, top): + """ + Sets the magnification according to the specified value, + and scrolls the page view both horizontally and vertically + according to the specified amounts. + + :param left: the horizontal coordinate positioned at the left edge. + :param top: the vertical coordinate positioned at the top edge. + """ + return self.ctrl.setZoomScroll(percent, left, top) + + +#------------------------------------------------------------------------------ + + + + +if __name__ == '__main__': + app = wx.App(False) + frm = wx.Frame(None, title="AX Test Window") + + pdf = PDFWindow(frm) + + frm.Show() + import wx.lib.inspection + wx.lib.inspection.InspectionTool().Show() + app.MainLoop() + + + + diff --git a/wx/lib/pdfwin_old.py b/wx/lib/pdfwin_old.py new file mode 100644 index 00000000..19b73ae4 --- /dev/null +++ b/wx/lib/pdfwin_old.py @@ -0,0 +1,790 @@ +#---------------------------------------------------------------------- +# Name: wx.lib.pdfwin +# Purpose: A class that allows the use of the Acrobat PDF reader +# ActiveX control +# +# Author: Robin Dunn +# +# Created: 22-March-2004 +# RCS-ID: $Id: pdfwin.py 49208 2007-10-18 00:40:21Z RD $ +# Copyright: (c) 2004 by Total Control Software +# Licence: wxWindows license +#---------------------------------------------------------------------- + +import wx + +#---------------------------------------------------------------------- + +_acroversion = None + +def get_acroversion(): + " Return version of Adobe Acrobat executable or None" + global _acroversion + if _acroversion == None and wx.PlatformInfo[1] == 'wxMSW': + import _winreg + regKey = _winreg.HKEY_LOCAL_MACHINE + acrokeys, acroversions = [], [] + try: + adobesoft = _winreg.OpenKey(regKey, r'Software\Adobe') + except WindowsError: + regKey = _winreg.HKEY_CURRENT_USER + adobesoft = _winreg.OpenKey(regKey, r'Software\Adobe') + + for index in range(_winreg.QueryInfoKey(adobesoft)[0]): + key = _winreg.EnumKey(adobesoft, index) + if "acrobat" in key.lower(): + acrokeys.append(_winreg.OpenKey(regKey, 'Software\\Adobe\\%s' % key)) + for acrokey in acrokeys: + for index in range(_winreg.QueryInfoKey(acrokey)[0]): + key = _winreg.EnumKey(acrokey, index) + try: + acroversions.append(float(key)) + except: + pass + acroversions.sort(reverse=True) + if acroversions: + _acroversion = acroversions[0] + return _acroversion + + +#---------------------------------------------------------------------- + +# The ActiveX module from Acrobat 7.0 has changed and it seems to now +# require much more from the container than what our wx.activex module +# provides. If we try to use it via wx.activex then Acrobat crashes. +# So instead we will use Internet Explorer (via the win32com modules +# so we can use better dynamic dispatch) and embed the Acrobat control +# within that. Acrobat provides the IAcroAXDocShim COM class that is +# accessible via the IE window's Document property after a PDF file +# has been loaded. +# +# Details of the Acrobat reader methods can be found in +# http://partners.adobe.com/public/developer/en/acrobat/sdk/pdf/iac/IACOverview.pdf +# http://partners.adobe.com/public/developer/en/acrobat/sdk/pdf/iac/IACReference.pdf +# +# Co-ordinates passed as parameters are in points (1/72 inch). + + +if get_acroversion() >= 7.0: + + from wx.lib.activexwrapper import MakeActiveXClass + import win32com.client.gencache + _browserModule = win32com.client.gencache.EnsureModule( + "{EAB22AC0-30C1-11CF-A7EB-0000C05BAE0B}", 0, 1, 1) + + class PDFWindowError(RuntimeError): + def __init__(self): + RuntimeError.__init__(self, "A PDF must be loaded before calling this method.") + + + class PDFWindow(wx.Panel): + def __init__(self, *args, **kw): + wx.Panel.__init__(self, *args, **kw) + + # Make a new class that derives from the WebBrowser class + # in the COM module imported above. This class also + # derives from wxWindow and implements the machinery + # needed to integrate the two worlds. + _WebBrowserClass = MakeActiveXClass(_browserModule.WebBrowser) + self.ie = _WebBrowserClass(self, -1) + sizer = wx.BoxSizer() + sizer.Add(self.ie, 1, wx.EXPAND) + self.SetSizer(sizer) + + + def LoadFile(self, fileName): + """ + Opens and displays the specified document within the browser. + """ + if self.ie.Document: + return self.ie.Document.LoadFile(fileName) + else: + self.ie.Navigate2(fileName) + return True # can we sense failure at this point? + + def GetVersions(self): + """ + Deprecated: No longer available - do not use. + """ + if self.ie.Document: + return self.ie.Document.GetVersions() + else: + raise PDFWindowError() + + def Print(self): + """ + Prints the document according to the specified options in a user dialog box. + """ + if self.ie.Document: + return self.ie.Document.Print() + else: + raise PDFWindowError() + + def goBackwardStack(self): + """ + Goes to the previous view on the view stack, if it exists. + """ + if self.ie.Document: + return self.ie.Document.goBackwardStack() + else: + raise PDFWindowError() + + def goForwardStack(self): + """ + Goes to the next view on the view stack, if it exists. + """ + if self.ie.Document: + return self.ie.Document.goForwardStack() + else: + raise PDFWindowError() + + def gotoFirstPage(self): + """ + Goes to the first page in the document. + """ + if self.ie.Document: + return self.ie.Document.gotoFirstPage() + else: + raise PDFWindowError() + + def gotoLastPage(self): + """ + Goes to the last page in the document. + """ + if self.ie.Document: + return self.ie.Document.gotoLastPage() + else: + raise PDFWindowError() + + def gotoNextPage(self): + """ + Goes to the next page in the document, if it exists + """ + if self.ie.Document: + return self.ie.Document.gotoNextPage() + else: + raise PDFWindowError() + + def gotoPreviousPage(self): + """ + Goes to the previous page in the document, if it exists. + """ + if self.ie.Document: + return self.ie.Document.gotoPreviousPage() + else: + raise PDFWindowError() + + def printAll(self): + """ + Prints the entire document without displaying a user + dialog box. The current printer, page settings, and job + settings are used. This method returns immediately, even + if the printing has not completed. + """ + if self.ie.Document: + return self.ie.Document.printAll() + else: + raise PDFWindowError() + + def printAllFit(self, shrinkToFit): + """ + Prints the entire document without a user dialog box, and + (if shrinkToFit) shrinks pages as needed to fit the + imageable area of a page in the printer. + """ + if self.ie.Document: + return self.ie.Document.printAllFit(shrinkToFit) + else: + raise PDFWindowError() + + def printPages(self, from_, to): + """ + Prints the specified pages without displaying a user dialog box. + """ + if self.ie.Document: + return self.ie.Document.printPages(from_, to) + else: + raise PDFWindowError() + + def printPagesFit(self, from_, to, shrinkToFit): + """ + Prints the specified pages without displaying a user + dialog box, and (if shrinkToFit) shrinks pages as needed + to fit the imageable area of a page in the printer. + """ + if self.ie.Document: + return self.ie.Document.printPagesFit( from_, to, shrinkToFit) + else: + raise PDFWindowError() + + def printWithDialog(self): + """ + Prints the document according to the specified options in + a user dialog box. These options may include embedded + printing and specifying which printer is to be used. + + NB. The page range in the dialog defaults to + 'From Page 1 to 1' - Use Print() above instead. (dfh) + """ + if self.ie.Document: + return self.ie.Document.printWithDialog() + else: + raise PDFWindowError() + + def setCurrentHighlight(self, a, b, c, d): + if self.ie.Document: + return self.ie.Document.setCurrentHighlight(a, b, c, d) + else: + raise PDFWindowError() + + def setCurrentPage(self, npage): + """ + Goes to the specified page in the document. Maintains the + current location within the page and zoom level. npage is + the page number of the destination page. The first page + in a document is page 0. + + ## Oh no it isn't! The first page is 1 (dfh) + """ + if self.ie.Document: + return self.ie.Document.setCurrentPage(npage) + else: + raise PDFWindowError() + + def setLayoutMode(self, layoutMode): + """ + LayoutMode possible values: + + ================= ==================================== + 'DontCare' use the current user preference + 'SinglePage' use single page mode (as in pre-Acrobat + 3.0 viewers) + 'OneColumn' use one-column continuous mode + 'TwoColumnLeft' use two-column continuous mode, first + page on the left + 'TwoColumnRight' use two-column continuous mode, first + page on the right + ================= ==================================== + """ + if self.ie.Document: + return self.ie.Document.setLayoutMode(layoutMode) + else: + raise PDFWindowError() + + def setNamedDest(self, namedDest): + """ + Changes the page view to the named destination in the specified string. + """ + if self.ie.Document: + return self.ie.Document.setNamedDest(namedDest) + else: + raise PDFWindowError() + + def setPageMode(self, pageMode): + """ + Sets the page mode to display the document only, or to + additionally display bookmarks or thumbnails. pageMode = + 'none' or 'bookmarks' or 'thumbs'. + + ## NB.'thumbs' is case-sensitive, the other are not (dfh) + """ + if self.ie.Document: + return self.ie.Document.setPageMode(pageMode) + else: + raise PDFWindowError() + + def setShowScrollbars(self, On): + """ + Determines whether scrollbars will appear in the document + view. + + ## NB. If scrollbars are off, the navigation tools disappear as well (dfh) + """ + if self.ie.Document: + return self.ie.Document.setShowScrollbars(On) + else: + raise PDFWindowError() + + def setShowToolbar(self, On): + """ + Determines whether a toolbar will appear in the application. + """ + if self.ie.Document: + return self.ie.Document.setShowToolbar(On) + else: + raise PDFWindowError() + + def setView(self, viewMode): + """ + Determines how the page will fit in the current view. + viewMode possible values: + + ======== ============================================== + 'Fit' fits whole page within the window both vertically + and horizontally. + 'FitH' fits the width of the page within the window. + 'FitV' fits the height of the page within the window. + 'FitB' fits bounding box within the window both vertically + and horizontally. + 'FitBH' fits the width of the bounding box within the window. + 'FitBV' fits the height of the bounding box within the window. + ======== ============================================== + """ + if self.ie.Document: + return self.ie.Document.setView(viewMode) + else: + raise PDFWindowError() + + def setViewRect(self, left, top, width, height): + """ + Sets the view rectangle according to the specified coordinates. + + :param left: The upper left horizontal coordinate. + :param top: The vertical coordinate in the upper left corner. + :param width: The horizontal width of the rectangle. + :param height: The vertical height of the rectangle. + """ + if self.ie.Document: + return self.ie.Document.setViewRect(left, top, width, height) + else: + raise PDFWindowError() + + def setViewScroll(self, viewMode, offset): + """ + Sets the view of a page according to the specified string. + Depending on the view mode, the page is either scrolled to + the right or scrolled down by the amount specified in + offset. Possible values of viewMode are as in setView + above. offset is the horizontal or vertical coordinate + positioned either at the left or top edge. + """ + if self.ie.Document: + return self.ie.Document.setViewScroll(viewMode, offset) + else: + raise PDFWindowError() + + def setZoom(self, percent): + """ + Sets the magnification according to the specified value + expressed as a percentage (float) + """ + if self.ie.Document: + return self.ie.Document.setZoom(percent) + else: + raise PDFWindowError() + + def setZoomScroll(self, percent, left, top): + """ + Sets the magnification according to the specified value, + and scrolls the page view both horizontally and vertically + according to the specified amounts. + + :param left: the horizontal coordinate positioned at the left edge. + :param top: the vertical coordinate positioned at the top edge. + """ + if self.ie.Document: + return self.ie.Document.setZoomScroll(percent, left, top) + else: + raise PDFWindowError() + + + +elif get_acroversion() is not None: + import wx.activex + + clsID = '{CA8A9780-280D-11CF-A24D-444553540000}' + progID = 'AcroPDF.PDF.1' + + + # Create eventTypes and event binders + wxEVT_Error = wx.activex.RegisterActiveXEvent('OnError') + wxEVT_Message = wx.activex.RegisterActiveXEvent('OnMessage') + + EVT_Error = wx.PyEventBinder(wxEVT_Error, 1) + EVT_Message = wx.PyEventBinder(wxEVT_Message, 1) + + + # Derive a new class from ActiveXWindow + class PDFWindow(wx.activex.ActiveXWindow): + def __init__(self, parent, ID=-1, pos=wx.DefaultPosition, + size=wx.DefaultSize, style=0, name='PDFWindow'): + wx.activex.ActiveXWindow.__init__(self, parent, + wx.activex.CLSID('{CA8A9780-280D-11CF-A24D-444553540000}'), + ID, pos, size, style, name) + + # Methods exported by the ActiveX object + def QueryInterface(self, riid): + return self.CallAXMethod('QueryInterface', riid) + + def AddRef(self): + return self.CallAXMethod('AddRef') + + def Release(self): + return self.CallAXMethod('Release') + + def GetTypeInfoCount(self): + return self.CallAXMethod('GetTypeInfoCount') + + def GetTypeInfo(self, itinfo, lcid): + return self.CallAXMethod('GetTypeInfo', itinfo, lcid) + + def GetIDsOfNames(self, riid, rgszNames, cNames, lcid): + return self.CallAXMethod('GetIDsOfNames', riid, rgszNames, cNames, lcid) + + def Invoke(self, dispidMember, riid, lcid, wFlags, pdispparams): + return self.CallAXMethod('Invoke', dispidMember, riid, lcid, wFlags, pdispparams) + + def LoadFile(self, fileName): + return self.CallAXMethod('LoadFile', fileName) + + def setShowToolbar(self, On): + return self.CallAXMethod('setShowToolbar', On) + + def gotoFirstPage(self): + return self.CallAXMethod('gotoFirstPage') + + def gotoLastPage(self): + return self.CallAXMethod('gotoLastPage') + + def gotoNextPage(self): + return self.CallAXMethod('gotoNextPage') + + def gotoPreviousPage(self): + return self.CallAXMethod('gotoPreviousPage') + + def setCurrentPage(self, n): + return self.CallAXMethod('setCurrentPage', n) + + def goForwardStack(self): + return self.CallAXMethod('goForwardStack') + + def goBackwardStack(self): + return self.CallAXMethod('goBackwardStack') + + def setPageMode(self, pageMode): + return self.CallAXMethod('setPageMode', pageMode) + + def setLayoutMode(self, layoutMode): + return self.CallAXMethod('setLayoutMode', layoutMode) + + def setNamedDest(self, namedDest): + return self.CallAXMethod('setNamedDest', namedDest) + + def Print(self): + return self.CallAXMethod('Print') + + def printWithDialog(self): + return self.CallAXMethod('printWithDialog') + + def setZoom(self, percent): + return self.CallAXMethod('setZoom', percent) + + def setZoomScroll(self, percent, left, top): + return self.CallAXMethod('setZoomScroll', percent, left, top) + + def setView(self, viewMode): + return self.CallAXMethod('setView', viewMode) + + def setViewScroll(self, viewMode, offset): + return self.CallAXMethod('setViewScroll', viewMode, offset) + + def setViewRect(self, left, top, width, height): + return self.CallAXMethod('setViewRect', left, top, width, height) + + def printPages(self, from_, to): + return self.CallAXMethod('printPages', from_, to) + + def printPagesFit(self, from_, to, shrinkToFit): + return self.CallAXMethod('printPagesFit', from_, to, shrinkToFit) + + def printAll(self): + return self.CallAXMethod('printAll') + + def printAllFit(self, shrinkToFit): + return self.CallAXMethod('printAllFit', shrinkToFit) + + def setShowScrollbars(self, On): + return self.CallAXMethod('setShowScrollbars', On) + + def GetVersions(self): + return self.CallAXMethod('GetVersions') + + def setCurrentHighlight(self, a, b, c, d): + return self.CallAXMethod('setCurrentHighlight', a, b, c, d) + + def postMessage(self, strArray): + return self.CallAXMethod('postMessage', strArray) + + # Getters, Setters and properties + def _get_src(self): + return self.GetAXProp('src') + def _set_src(self, src): + self.SetAXProp('src', src) + src = property(_get_src, _set_src) + + def _get_messageHandler(self): + return self.GetAXProp('messageHandler') + def _set_messageHandler(self, messageHandler): + self.SetAXProp('messageHandler', messageHandler) + messagehandler = property(_get_messageHandler, _set_messageHandler) + + +# PROPERTIES +# -------------------- +# src +# type:string arg:string canGet:True canSet:True +# +# messagehandler +# type:VT_VARIANT arg:VT_VARIANT canGet:True canSet:True +# +# +# +# +# METHODS +# -------------------- +# QueryInterface +# retType: VT_VOID +# params: +# riid +# in:True out:False optional:False type:unsupported type 29 +# ppvObj +# in:False out:True optional:False type:unsupported type 26 +# +# AddRef +# retType: int +# +# Release +# retType: int +# +# GetTypeInfoCount +# retType: VT_VOID +# params: +# pctinfo +# in:False out:True optional:False type:int +# +# GetTypeInfo +# retType: VT_VOID +# params: +# itinfo +# in:True out:False optional:False type:int +# lcid +# in:True out:False optional:False type:int +# pptinfo +# in:False out:True optional:False type:unsupported type 26 +# +# GetIDsOfNames +# retType: VT_VOID +# params: +# riid +# in:True out:False optional:False type:unsupported type 29 +# rgszNames +# in:True out:False optional:False type:unsupported type 26 +# cNames +# in:True out:False optional:False type:int +# lcid +# in:True out:False optional:False type:int +# rgdispid +# in:False out:True optional:False type:int +# +# Invoke +# retType: VT_VOID +# params: +# dispidMember +# in:True out:False optional:False type:int +# riid +# in:True out:False optional:False type:unsupported type 29 +# lcid +# in:True out:False optional:False type:int +# wFlags +# in:True out:False optional:False type:int +# pdispparams +# in:True out:False optional:False type:unsupported type 29 +# pvarResult +# in:False out:True optional:False type:VT_VARIANT +# pexcepinfo +# in:False out:True optional:False type:unsupported type 29 +# puArgErr +# in:False out:True optional:False type:int +# +# LoadFile +# retType: bool +# params: +# fileName +# in:True out:False optional:False type:string +# +# setShowToolbar +# retType: VT_VOID +# params: +# On +# in:True out:False optional:False type:bool +# +# gotoFirstPage +# retType: VT_VOID +# +# gotoLastPage +# retType: VT_VOID +# +# gotoNextPage +# retType: VT_VOID +# +# gotoPreviousPage +# retType: VT_VOID +# +# setCurrentPage +# retType: VT_VOID +# params: +# n +# in:True out:False optional:False type:int +# +# goForwardStack +# retType: VT_VOID +# +# goBackwardStack +# retType: VT_VOID +# +# setPageMode +# retType: VT_VOID +# params: +# pageMode +# in:True out:False optional:False type:string +# +# setLayoutMode +# retType: VT_VOID +# params: +# layoutMode +# in:True out:False optional:False type:string +# +# setNamedDest +# retType: VT_VOID +# params: +# namedDest +# in:True out:False optional:False type:string +# +# Print +# retType: VT_VOID +# +# printWithDialog +# retType: VT_VOID +# +# setZoom +# retType: VT_VOID +# params: +# percent +# in:True out:False optional:False type:double +# +# setZoomScroll +# retType: VT_VOID +# params: +# percent +# in:True out:False optional:False type:double +# left +# in:True out:False optional:False type:double +# top +# in:True out:False optional:False type:double +# +# setView +# retType: VT_VOID +# params: +# viewMode +# in:True out:False optional:False type:string +# +# setViewScroll +# retType: VT_VOID +# params: +# viewMode +# in:True out:False optional:False type:string +# offset +# in:True out:False optional:False type:double +# +# setViewRect +# retType: VT_VOID +# params: +# left +# in:True out:False optional:False type:double +# top +# in:True out:False optional:False type:double +# width +# in:True out:False optional:False type:double +# height +# in:True out:False optional:False type:double +# +# printPages +# retType: VT_VOID +# params: +# from +# in:True out:False optional:False type:int +# to +# in:True out:False optional:False type:int +# +# printPagesFit +# retType: VT_VOID +# params: +# from +# in:True out:False optional:False type:int +# to +# in:True out:False optional:False type:int +# shrinkToFit +# in:True out:False optional:False type:bool +# +# printAll +# retType: VT_VOID +# +# printAllFit +# retType: VT_VOID +# params: +# shrinkToFit +# in:True out:False optional:False type:bool +# +# setShowScrollbars +# retType: VT_VOID +# params: +# On +# in:True out:False optional:False type:bool +# +# GetVersions +# retType: VT_VARIANT +# +# setCurrentHightlight +# retType: VT_VOID +# params: +# a +# in:True out:False optional:False type:int +# b +# in:True out:False optional:False type:int +# c +# in:True out:False optional:False type:int +# d +# in:True out:False optional:False type:int +# +# setCurrentHighlight +# retType: VT_VOID +# params: +# a +# in:True out:False optional:False type:int +# b +# in:True out:False optional:False type:int +# c +# in:True out:False optional:False type:int +# d +# in:True out:False optional:False type:int +# +# postMessage +# retType: VT_VOID +# params: +# strArray +# in:True out:False optional:False type:VT_VARIANT +# +# +# +# +# EVENTS +# -------------------- +# Error +# retType: VT_VOID +# +# Message +# retType: VT_VOID +# +# +# +# diff --git a/wx/lib/platebtn.py b/wx/lib/platebtn.py new file mode 100644 index 00000000..423bb4be --- /dev/null +++ b/wx/lib/platebtn.py @@ -0,0 +1,759 @@ +############################################################################### +# Name: platebtn.py # +# Purpose: PlateButton is a flat label button with support for bitmaps and # +# drop menu. # +# Author: Cody Precord # +# Copyright: (c) 2007 Cody Precord # +# Licence: wxWindows Licence # +############################################################################### + +""" +Editra Control Library: PlateButton + +The PlateButton is a custom owner drawn flat button, that in many ways emulates +the buttons found the bookmark bar of the Safari browser. It can be used as a +drop in replacement for wx.Button/wx.BitmapButton under most circumstances. It +also offers a wide range of options for customizing its appearance, a +description of each of the main style settings is listed below. + +Main Button Styles: +Any combination of the following values may be passed to the constructor's style +keyword parameter. + +PB_STYLE_DEFAULT: +Creates a flat label button with rounded corners, the highlight for mouse over +and press states is based off of the hightlight color from the systems current +theme. + +PB_STYLE_GRADIENT: +The highlight and press states are drawn with gradient using the current +highlight color. + +PB_STYLE_SQUARE: +Instead of the default rounded shape use a rectangular shaped button with +square edges. + +PB_STYLE_NOBG: +This style only has an effect on Windows but does not cause harm to use on the +platforms. It should only be used when the control is shown on a panel or other +window that has a non solid color for a background. i.e a gradient or image is +painted on the background of the parent window. If used on a background with +a solid color it may cause the control to loose its transparent appearance. + +PB_STYLE_DROPARROW: +Add a drop button arrow to the button that will send a separate event when +clicked on. + +Other attributes can be configured after the control has been created. The +settings that are currently available are as follows: + + - SetBitmap: Change/Add the bitmap at any time and the control will resize and + refresh to display it. + - SetLabelColor: Explicitly set text colors + - SetMenu: Set the button to have a popupmenu. When a menu is set a small drop + arrow will be drawn on the button that can then be clicked to show + a menu. + - SetPressColor: Use a custom highlight color + + +Overridden Methods Inherited from PyControl: + + - SetFont: Changing the font is one way to set the size of the button, by + default the control will inherit its font from its parent. + + - SetWindowVariant: Setting the window variant will cause the control to + resize to the corresponding variant size. However if the + button is using a bitmap the bitmap will remain unchanged + and only the font will be adjusted. + +Requirements: + - python2.4 or higher + - wxPython2.8 or higher + +""" + +__author__ = "Cody Precord " +__svnid__ = "$Id: platebtn.py 69230 2011-09-29 15:23:52Z CJP $" +__revision__ = "$Revision: 69230 $" + +__all__ = ["PlateButton", + "PLATE_NORMAL", "PLATE_PRESSED", "PLATE_HIGHLIGHT", + + "PB_STYLE_DEFAULT", "PB_STYLE_GRADIENT", "PB_STYLE_SQUARE", + "PB_STYLE_NOBG", "PB_STYLE_DROPARROW", "PB_STYLE_TOGGLE", + + "EVT_PLATEBTN_DROPARROW_PRESSED"] + +#-----------------------------------------------------------------------------# +# Imports +import wx +import wx.lib.newevent + +# Local Imports +from wx.lib.colourutils import * + +#-----------------------------------------------------------------------------# +# Button States +PLATE_NORMAL = 0 +PLATE_PRESSED = 1 +PLATE_HIGHLIGHT = 2 + +# Button Styles +PB_STYLE_DEFAULT = 1 # Normal Flat Background +PB_STYLE_GRADIENT = 2 # Gradient Filled Background +PB_STYLE_SQUARE = 4 # Use square corners instead of rounded +PB_STYLE_NOBG = 8 # Useful on Windows to get a transparent appearance + # when the control is shown on a non solid background +PB_STYLE_DROPARROW = 16 # Draw drop arrow and fire EVT_PLATEBTN_DROPRROW_PRESSED event +PB_STYLE_TOGGLE = 32 # Stay pressed until clicked again + +#-----------------------------------------------------------------------------# + +# EVT_BUTTON used for normal event notification +# EVT_TOGGLE_BUTTON used for toggle button mode notification +PlateBtnDropArrowPressed, EVT_PLATEBTN_DROPARROW_PRESSED = wx.lib.newevent.NewEvent() + +#-----------------------------------------------------------------------------# + +class PlateButton(wx.PyControl): + """PlateButton is a custom type of flat button with support for + displaying bitmaps and having an attached dropdown menu. + + """ + def __init__(self, parent, id=wx.ID_ANY, label='', bmp=None, + pos=wx.DefaultPosition, size=wx.DefaultSize, + style=PB_STYLE_DEFAULT, name=wx.ButtonNameStr): + """Create a PlateButton + + :keyword string `label`: Buttons label text + :keyword Bitmap `bmp`: Buttons bitmap + :keyword `style`: Button style + + """ + super(PlateButton, self).__init__(parent, id, pos, size, + wx.BORDER_NONE|wx.TRANSPARENT_WINDOW, + name=name) + + # Attributes + self.InheritAttributes() + self._bmp = dict(enable=None, disable=None) + if bmp is not None: + assert isinstance(bmp, wx.Bitmap) and bmp.IsOk() + self._bmp['enable'] = bmp + img = bmp.ConvertToImage() + img = img.ConvertToGreyscale(.795, .073, .026) #(.634, .224, .143) + self._bmp['disable'] = wx.BitmapFromImage(img) + + self._menu = None + self.SetLabel(label) + self._style = style + self._state = dict(pre=PLATE_NORMAL, cur=PLATE_NORMAL) + self._color = self.__InitColors() + self._pressed = False + + # Setup Initial Size + self.SetInitialSize(size) + + # Event Handlers + self.Bind(wx.EVT_PAINT, lambda evt: self.__DrawButton()) + self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnErase) + self.Bind(wx.EVT_SET_FOCUS, self.OnFocus) + self.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus) + + # Mouse Events + self.Bind(wx.EVT_LEFT_DCLICK, lambda evt: self._ToggleState()) + self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) + self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp) + self.Bind(wx.EVT_ENTER_WINDOW, + lambda evt: self._SetState(PLATE_HIGHLIGHT)) + self.Bind(wx.EVT_LEAVE_WINDOW, + lambda evt: wx.CallLater(80, self.__LeaveWindow)) + + # Other events + self.Bind(wx.EVT_KEY_UP, self.OnKeyUp) + self.Bind(wx.EVT_CONTEXT_MENU, lambda evt: self.ShowMenu()) + + def __DrawBitmap(self, gc): + """Draw the bitmap if one has been set + + :param GCDC `gc`: :class:`GCDC` to draw with + :return: x cordinate to draw text at + + """ + if self.IsEnabled(): + bmp = self._bmp['enable'] + else: + bmp = self._bmp['disable'] + + if bmp is not None and bmp.IsOk(): + bw, bh = bmp.GetSize() + ypos = (self.GetSize()[1] - bh) // 2 + gc.DrawBitmap(bmp, 6, ypos, bmp.GetMask() != None) + return bw + 6 + else: + return 6 + + def __DrawDropArrow(self, gc, xpos, ypos): + """Draw a drop arrow if needed and restore pen/brush after finished + + :param GCDC `gc`: :class:`GCDC` to draw with + :param int `xpos`: x cord to start at + :param int `ypos`: y cord to start at + + """ + if self._menu is not None or self._style & PB_STYLE_DROPARROW: + # Positioning needs a little help on Windows + if wx.Platform == '__WXMSW__': + xpos -= 2 + tripoints = [(xpos, ypos), (xpos + 6, ypos), (xpos + 3, ypos + 5)] + brush_b = gc.GetBrush() + pen_b = gc.GetPen() + gc.SetPen(wx.TRANSPARENT_PEN) + gc.SetBrush(wx.Brush(gc.GetTextForeground())) + gc.DrawPolygon(tripoints) + gc.SetBrush(brush_b) + gc.SetPen(pen_b) + else: + pass + + def __DrawHighlight(self, gc, width, height): + """Draw the main highlight/pressed state + + :param GCDC `gc`: :class:`GCDC` to draw with + :param int `width`: width of highlight + :param int `height`: height of highlight + + """ + if self._state['cur'] == PLATE_PRESSED: + color = self._color['press'] + else: + color = self._color['hlight'] + + if self._style & PB_STYLE_SQUARE: + rad = 0 + else: + rad = (height - 3) / 2 + + if self._style & PB_STYLE_GRADIENT: + gc.SetBrush(wx.TRANSPARENT_BRUSH) + rgc = gc.GetGraphicsContext() + brush = rgc.CreateLinearGradientBrush(0, 1, 0, height, + color, AdjustAlpha(color, 55)) + rgc.SetBrush(brush) + else: + gc.SetBrush(wx.Brush(color)) + + gc.DrawRoundedRectangle(1, 1, width - 2, height - 2, rad) + + def __PostEvent(self): + """Post a button event to parent of this control""" + if self._style & PB_STYLE_TOGGLE: + etype = wx.wxEVT_COMMAND_TOGGLEBUTTON_CLICKED + else: + etype = wx.wxEVT_COMMAND_BUTTON_CLICKED + bevt = wx.CommandEvent(etype, self.GetId()) + bevt.SetEventObject(self) + bevt.SetString(self.GetLabel()) + self.GetEventHandler().ProcessEvent(bevt) + + def __DrawButton(self): + """Draw the button""" + # TODO using a buffered paintdc on windows with the nobg style + # causes lots of weird drawing. So currently the use of a + # buffered dc is dissabled for this style. + if PB_STYLE_NOBG & self._style: + dc = wx.PaintDC(self) + else: + dc = wx.AutoBufferedPaintDCFactory(self) + + gc = wx.GCDC(dc) + + # Setup + dc.SetBrush(wx.TRANSPARENT_BRUSH) + gc.SetBrush(wx.TRANSPARENT_BRUSH) + gc.SetFont(self.Font) + dc.SetFont(self.Font) + gc.SetBackgroundMode(wx.TRANSPARENT) + + # The background needs some help to look transparent on + # on Gtk and Windows + if wx.Platform in ['__WXGTK__', '__WXMSW__']: + gc.SetBackground(self.GetBackgroundBrush(gc)) + gc.Clear() + + # Calc Object Positions + width, height = self.GetSize() + if wx.Platform == '__WXGTK__': + tw, th = dc.GetTextExtent(self.Label) + else: + tw, th = gc.GetTextExtent(self.Label) + txt_y = max((height - th) // 2, 1) + + if self._state['cur'] == PLATE_HIGHLIGHT: + gc.SetTextForeground(self._color['htxt']) + gc.SetPen(wx.TRANSPARENT_PEN) + self.__DrawHighlight(gc, width, height) + + elif self._state['cur'] == PLATE_PRESSED: + gc.SetTextForeground(self._color['htxt']) + if wx.Platform == '__WXMAC__': + pen = wx.Pen(GetHighlightColour(), 1, wx.SOLID) + else: + pen = wx.Pen(AdjustColour(self._color['press'], -80, 220), 1) + gc.SetPen(pen) + + self.__DrawHighlight(gc, width, height) + txt_x = self.__DrawBitmap(gc) + if wx.Platform == '__WXGTK__': + dc.DrawText(self.Label, txt_x + 2, txt_y) + else: + gc.DrawText(self.Label, txt_x + 2, txt_y) + self.__DrawDropArrow(gc, width - 10, (height // 2) - 2) + + else: + if self.IsEnabled(): + gc.SetTextForeground(self.GetForegroundColour()) + else: + txt_c = wx.SystemSettings.GetColour(wx.SYS_COLOUR_GRAYTEXT) + gc.SetTextForeground(txt_c) + + # Draw bitmap and text + if self._state['cur'] != PLATE_PRESSED: + txt_x = self.__DrawBitmap(gc) + if wx.Platform == '__WXGTK__': + dc.DrawText(self.Label, txt_x + 2, txt_y) + else: + gc.DrawText(self.Label, txt_x + 2, txt_y) + self.__DrawDropArrow(gc, width - 10, (height // 2) - 2) + + def __InitColors(self): + """Initialize the default colors""" + color = GetHighlightColour() + pcolor = AdjustColour(color, -12) + colors = dict(default=True, + hlight=color, + press=pcolor, + htxt=BestLabelColour(self.GetForegroundColour())) + return colors + + def __LeaveWindow(self): + """Handle updating the buttons state when the mouse cursor leaves""" + if (self._style & PB_STYLE_TOGGLE) and self._pressed: + self._SetState(PLATE_PRESSED) + else: + self._SetState(PLATE_NORMAL) + self._pressed = False + + def _SetState(self, state): + """Manually set the state of the button + + :param `state`: one of the PLATE_* values + + ..note:: + the state may be altered by mouse actions + + ..note:: + Internal use only! + + """ + self._state['pre'] = self._state['cur'] + self._state['cur'] = state + if wx.Platform == '__WXMSW__': + self.Parent.RefreshRect(self.Rect, False) + else: + self.Refresh() + + def _ToggleState(self): + """Toggle button state + + ..note:: + Internal Use Only! + + """ + if self._state['cur'] != PLATE_PRESSED: + self._SetState(PLATE_PRESSED) + else: + self._SetState(PLATE_HIGHLIGHT) + + #---- End Private Member Function ----# + + #---- Public Member Functions ----# + + BitmapDisabled = property(lambda self: self.GetBitmapDisabled(), + lambda self, bmp: self.SetBitmapDisabled(bmp)) + BitmapLabel = property(lambda self: self.GetBitmapLabel(), + lambda self, bmp: self.SetBitmap(bmp)) + + # Aliases + BitmapFocus = BitmapLabel + BitmapHover = BitmapLabel + BitmapSelected = BitmapLabel + + LabelText = property(lambda self: self.GetLabel(), + lambda self, lbl: self.SetLabel(lbl)) + + def AcceptsFocus(self): + """Can this window have the focus?""" + return self.IsEnabled() + + def Disable(self): + """Disable the control""" + super(PlateButton, self).Disable() + self.Refresh() + + def DoGetBestSize(self): + """Calculate the best size of the button + + :return: :class:`Size` + + """ + width = 4 + height = 6 + if self.Label: + # NOTE: Should measure with a GraphicsContext to get right + # size, but due to random segfaults on linux special + # handling is done in the drawing instead... + lsize = self.GetFullTextExtent(self.Label) + width += lsize[0] + height += lsize[1] + + if self._bmp['enable'] is not None: + bsize = self._bmp['enable'].Size + width += (bsize[0] + 10) + if height <= bsize[1]: + height = bsize[1] + 6 + else: + height += 3 + else: + width += 10 + + if self._menu is not None or self._style & PB_STYLE_DROPARROW: + width += 12 + + best = wx.Size(width, height) + self.CacheBestSize(best) + return best + + def Enable(self, enable=True): + """Enable/Disable the control""" + super(PlateButton, self).Enable(enable) + self.Refresh() + + def GetBackgroundBrush(self, dc): + """Get the brush for drawing the background of the button + + :return: :class:`Brush` + + ..note:: + used internally when on gtk + + """ + if wx.Platform == '__WXMAC__' or self._style & PB_STYLE_NOBG: + return wx.TRANSPARENT_BRUSH + + bkgrd = self.GetBackgroundColour() + brush = wx.Brush(bkgrd, wx.SOLID) + my_attr = self.GetDefaultAttributes() + p_attr = self.Parent.GetDefaultAttributes() + my_def = bkgrd == my_attr.colBg + p_def = self.Parent.GetBackgroundColour() == p_attr.colBg + if my_def and not p_def: + bkgrd = self.Parent.GetBackgroundColour() + brush = wx.Brush(bkgrd, wx.SOLID) + return brush + + def GetBitmapDisabled(self): + """Get the bitmap of the disable state + + :return: :class:`Bitmap` or None + + """ + return self.BitmapDisabled + + def GetBitmapLabel(self): + """Get the label bitmap + + :return: :class:`Bitmap` or None + + """ + return self.BitmapLabel + + # GetBitmap Aliases for BitmapButton api + GetBitmapFocus = GetBitmapLabel + GetBitmapHover = GetBitmapLabel + + # Alias for GetLabel + GetLabelText = wx.PyControl.GetLabel + + def GetMenu(self): + """Return the menu associated with this button or None if no + menu is associated with it. + + """ + return self._menu + + def GetState(self): + """Get the current state of the button + + :return: int + + ..seeAlso:: + PLATE_NORMAL, PLATE_HIGHLIGHT, PLATE_PRESSED + + """ + return self._state['cur'] + + def HasTransparentBackground(self): + """Override setting of background fill""" + return True + + def IsPressed(self): + """Return if button is pressed (PB_STYLE_TOGGLE) + + :return: bool + + """ + return self._pressed + + #---- Event Handlers ----# + + def OnErase(self, evt): + """Trap the erase event to keep the background transparent + on windows. + + :param `evt`: wx.EVT_ERASE_BACKGROUND + + """ + pass + + def OnFocus(self, evt): + """Set the visual focus state if need be""" + if self._state['cur'] == PLATE_NORMAL: + self._SetState(PLATE_HIGHLIGHT) + + def OnKeyUp(self, evt): + """Execute a single button press action when the Return key is pressed + and this control has the focus. + + :param `evt`: wx.EVT_KEY_UP + + """ + if evt.GetKeyCode() == wx.WXK_SPACE: + self._SetState(PLATE_PRESSED) + self.__PostEvent() + wx.CallLater(100, self._SetState, PLATE_HIGHLIGHT) + else: + evt.Skip() + + def OnKillFocus(self, evt): + """Set the visual state back to normal when focus is lost + unless the control is currently in a pressed state. + + """ + # Note: this delay needs to be at least as much as the on in the KeyUp + # handler to prevent ghost highlighting from happening when + # quickly changing focus and activating buttons + if self._state['cur'] != PLATE_PRESSED: + self._SetState(PLATE_NORMAL) + + def OnLeftDown(self, evt): + """Sets the pressed state and depending on the click position will + show the popup menu if one has been set. + + """ + if (self._style & PB_STYLE_TOGGLE): + self._pressed = not self._pressed + + pos = evt.GetPositionTuple() + self._SetState(PLATE_PRESSED) + size = self.GetSizeTuple() + if pos[0] >= size[0] - 16: + if self._menu is not None: + self.ShowMenu() + elif self._style & PB_STYLE_DROPARROW: + event = PlateBtnDropArrowPressed() + event.SetEventObject(self) + self.EventHandler.ProcessEvent(event) + + self.SetFocus() + + def OnLeftUp(self, evt): + """Post a button event if the control was previously in a + pressed state. + + :param `evt`: :class:`MouseEvent` + + """ + if self._state['cur'] == PLATE_PRESSED: + pos = evt.GetPositionTuple() + size = self.GetSizeTuple() + if not (self._style & PB_STYLE_DROPARROW and pos[0] >= size[0] - 16): + self.__PostEvent() + + if self._pressed: + self._SetState(PLATE_PRESSED) + else: + self._SetState(PLATE_HIGHLIGHT) + + def OnMenuClose(self, evt): + """Refresh the control to a proper state after the menu has been + dismissed. + + :param `evt`: wx.EVT_MENU_CLOSE + + """ + mpos = wx.GetMousePosition() + if self.HitTest(self.ScreenToClient(mpos)) != wx.HT_WINDOW_OUTSIDE: + self._SetState(PLATE_HIGHLIGHT) + else: + self._SetState(PLATE_NORMAL) + evt.Skip() + + #---- End Event Handlers ----# + + def SetBitmap(self, bmp): + """Set the bitmap displayed in the button + + :param `bmp`: :class:`Bitmap` + + """ + self._bmp['enable'] = bmp + img = bmp.ConvertToImage() + img = img.ConvertToGreyscale(.795, .073, .026) #(.634, .224, .143) + self._bmp['disable'] = img.ConvertToBitmap() + self.InvalidateBestSize() + + def SetBitmapDisabled(self, bmp): + """Set the bitmap for the disabled state + + :param `bmp`: :class:`Bitmap` + + """ + self._bmp['disable'] = bmp + + # Aliases for SetBitmap* functions from BitmapButton + SetBitmapFocus = SetBitmap + SetBitmapHover = SetBitmap + SetBitmapLabel = SetBitmap + SetBitmapSelected = SetBitmap + + def SetFocus(self): + """Set this control to have the focus""" + if self._state['cur'] != PLATE_PRESSED: + self._SetState(PLATE_HIGHLIGHT) + super(PlateButton, self).SetFocus() + + def SetFont(self, font): + """Adjust size of control when font changes""" + super(PlateButton, self).SetFont(font) + self.InvalidateBestSize() + + def SetLabel(self, label): + """Set the label of the button + + :param string `label`: lable string + + """ + super(PlateButton, self).SetLabel(label) + self.InvalidateBestSize() + + def SetLabelColor(self, normal, hlight=wx.NullColour): + """Set the color of the label. The optimal label color is usually + automatically selected depending on the button color. In some + cases the colors that are chosen may not be optimal. + + The normal state must be specified, if the other two params are left + Null they will be automatically guessed based on the normal color. To + prevent this automatic color choices from happening either specify + a color or None for the other params. + + :param Colour `normal`: Label color for normal state (:class:`Colour`) + :keyword Colour `hlight`: Color for when mouse is hovering over + + """ + assert isinstance(normal, wx.Colour), "Must supply a colour object" + self._color['default'] = False + self.SetForegroundColour(normal) + + if hlight is not None: + if hlight.IsOk(): + self._color['htxt'] = hlight + else: + self._color['htxt'] = BestLabelColour(normal) + + if wx.Platform == '__WXMSW__': + self.Parent.RefreshRect(self.GetRect(), False) + else: + self.Refresh() + + def SetMenu(self, menu): + """Set the menu that can be shown when clicking on the + drop arrow of the button. + + :param Menu `menu`: :class:`Menu` to use as a PopupMenu + + ..note:: + Arrow is not drawn unless a menu is set + + """ + if self._menu is not None: + self.Unbind(wx.EVT_MENU_CLOSE) + + self._menu = menu + self.Bind(wx.EVT_MENU_CLOSE, self.OnMenuClose) + self.InvalidateBestSize() + + def SetPressColor(self, color): + """Set the color used for highlighting the pressed state + + :param Colour `color`: :class:`Colour` + + ..note:: + also resets all text colours as necessary + + """ + self._color['default'] = False + if color.Alpha() == 255: + self._color['hlight'] = AdjustAlpha(color, 200) + else: + self._color['hlight'] = color + self._color['press'] = AdjustColour(color, -10, 160) + self._color['htxt'] = BestLabelColour(self._color['hlight']) + self.Refresh() + + def SetWindowStyle(self, style): + """Sets the window style bytes, the updates take place + immediately no need to call refresh afterwards. + + :param `style`: bitmask of PB_STYLE_* values + + """ + self._style = style + self.Refresh() + + def SetWindowVariant(self, variant): + """Set the variant/font size of this control""" + super(PlateButton, self).SetWindowVariant(variant) + self.InvalidateBestSize() + + def ShouldInheritColours(self): + """Overridden base class virtual. If the parent has non-default + colours then we want this control to inherit them. + + """ + return True + + def ShowMenu(self): + """Show the dropdown menu if one is associated with this control""" + if self._menu is not None: + size = self.GetSizeTuple() + adj = wx.Platform == '__WXMAC__' and 3 or 0 + + if self._style & PB_STYLE_SQUARE: + xpos = 1 + else: + xpos = size[1] / 2 + + self.PopupMenu(self._menu, (xpos, size[1] + adj)) + + #---- End Public Member Functions ----# diff --git a/wx/lib/plot.py b/wx/lib/plot.py new file mode 100644 index 00000000..3687211c --- /dev/null +++ b/wx/lib/plot.py @@ -0,0 +1,2430 @@ +#----------------------------------------------------------------------------- +# Name: wx.lib.plot.py +# Purpose: Line, Bar and Scatter Graphs +# +# Author: Gordon Williams +# +# Created: 2003/11/03 +# RCS-ID: $Id$ +# Copyright: (c) 2002 +# Licence: Use as you wish. +#----------------------------------------------------------------------------- +# 12/15/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o 2.5 compatability update. +# o Renamed to plot.py in the wx.lib directory. +# o Reworked test frame to work with wx demo framework. This saves a bit +# of tedious cut and paste, and the test app is excellent. +# +# 12/18/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o wxScrolledMessageDialog -> ScrolledMessageDialog +# +# Oct 6, 2004 Gordon Williams (g_will@cyberus.ca) +# - Added bar graph demo +# - Modified line end shape from round to square. +# - Removed FloatDCWrapper for conversion to ints and ints in arguments +# +# Oct 15, 2004 Gordon Williams (g_will@cyberus.ca) +# - Imported modules given leading underscore to name. +# - Added Cursor Line Tracking and User Point Labels. +# - Demo for Cursor Line Tracking and Point Labels. +# - Size of plot preview frame adjusted to show page better. +# - Added helper functions PositionUserToScreen and PositionScreenToUser in PlotCanvas. +# - Added functions GetClosestPoints (all curves) and GetClosestPoint (only closest curve) +# can be in either user coords or screen coords. +# +# Jun 22, 2009 Florian Hoech (florian.hoech@gmx.de) +# - Fixed exception when drawing empty plots on Mac OS X +# - Fixed exception when trying to draw point labels on Mac OS X (Mac OS X +# point label drawing code is still slow and only supports wx.COPY) +# - Moved label positions away from axis lines a bit +# - Added PolySpline class and modified demo 1 and 2 to use it +# - Added center and diagonal lines option (Set/GetEnableCenterLines, +# Set/GetEnableDiagonals) +# - Added anti-aliasing option with optional high-resolution mode +# (Set/GetEnableAntiAliasing, Set/GetEnableHiRes) and demo +# - Added option to specify exact number of tick marks to use for each axis +# (SetXSpec(, SetYSpec() -- work like 'min', but with +# tick marks) +# - Added support for background and foreground colours (enabled via +# SetBackgroundColour/SetForegroundColour on a PlotCanvas instance) +# - Changed PlotCanvas printing initialization from occuring in __init__ to +# occur on access. This will postpone any IPP and / or CUPS warnings +# which appear on stderr on some Linux systems until printing functionality +# is actually used. +# +# + +""" +This is a simple light weight plotting module that can be used with +Boa or easily integrated into your own wxPython application. The +emphasis is on small size and fast plotting for large data sets. It +has a reasonable number of features to do line and scatter graphs +easily as well as simple bar graphs. It is not as sophisticated or +as powerful as SciPy Plt or Chaco. Both of these are great packages +but consume huge amounts of computer resources for simple plots. +They can be found at http://scipy.com + +This file contains two parts; first the re-usable library stuff, then, +after a "if __name__=='__main__'" test, a simple frame and a few default +plots for examples and testing. + +Based on wxPlotCanvas +Written by K.Hinsen, R. Srinivasan; +Ported to wxPython Harm van der Heijden, feb 1999 + +Major Additions Gordon Williams Feb. 2003 (g_will@cyberus.ca) + -More style options + -Zooming using mouse "rubber band" + -Scroll left, right + -Grid(graticule) + -Printing, preview, and page set up (margins) + -Axis and title labels + -Cursor xy axis values + -Doc strings and lots of comments + -Optimizations for large number of points + -Legends + +Did a lot of work here to speed markers up. Only a factor of 4 +improvement though. Lines are much faster than markers, especially +filled markers. Stay away from circles and triangles unless you +only have a few thousand points. + +Times for 25,000 points +Line - 0.078 sec +Markers +Square - 0.22 sec +dot - 0.10 +circle - 0.87 +cross,plus - 0.28 +triangle, triangle_down - 0.90 + +Thanks to Chris Barker for getting this version working on Linux. + +Zooming controls with mouse (when enabled): + Left mouse drag - Zoom box. + Left mouse double click - reset zoom. + Right mouse click - zoom out centred on click location. +""" + +import string as _string +import time as _time +import sys +import wx + +# Needs Numeric or numarray or NumPy +try: + import numpy.oldnumeric as _Numeric +except: + try: + import numarray as _Numeric #if numarray is used it is renamed Numeric + except: + try: + import Numeric as _Numeric + except: + msg= """ + This module requires the Numeric/numarray or NumPy module, + which could not be imported. It probably is not installed + (it's not part of the standard Python distribution). See the + Numeric Python site (http://numpy.scipy.org) for information on + downloading source or binaries.""" + raise ImportError, "Numeric,numarray or NumPy not found. \n" + msg + + + +# +# Plotting classes... +# +class PolyPoints: + """Base Class for lines and markers + - All methods are private. + """ + + def __init__(self, points, attr): + self._points = _Numeric.array(points).astype(_Numeric.Float64) + self._logscale = (False, False) + self._pointSize = (1.0, 1.0) + self.currentScale= (1,1) + self.currentShift= (0,0) + self.scaled = self.points + self.attributes = {} + self.attributes.update(self._attributes) + for name, value in attr.items(): + if name not in self._attributes.keys(): + raise KeyError, "Style attribute incorrect. Should be one of %s" % self._attributes.keys() + self.attributes[name] = value + + def setLogScale(self, logscale): + self._logscale = logscale + + def __getattr__(self, name): + if name == 'points': + if len(self._points)>0: + data = _Numeric.array(self._points,copy=True) + if self._logscale[0]: + data = self.log10(data, 0) + if self._logscale[1]: + data = self.log10(data, 1) + return data + else: + return self._points + else: + raise AttributeError, name + + def log10(self, data, ind): + data = _Numeric.compress(data[:,ind]>0,data,0) + data[:,ind] = _Numeric.log10(data[:,ind]) + return data + + def boundingBox(self): + if len(self.points) == 0: + # no curves to draw + # defaults to (-1,-1) and (1,1) but axis can be set in Draw + minXY= _Numeric.array([-1.0,-1.0]) + maxXY= _Numeric.array([ 1.0, 1.0]) + else: + minXY= _Numeric.minimum.reduce(self.points) + maxXY= _Numeric.maximum.reduce(self.points) + return minXY, maxXY + + def scaleAndShift(self, scale=(1,1), shift=(0,0)): + if len(self.points) == 0: + # no curves to draw + return + if (scale is not self.currentScale) or (shift is not self.currentShift): + # update point scaling + self.scaled = scale*self.points+shift + self.currentScale= scale + self.currentShift= shift + # else unchanged use the current scaling + + def getLegend(self): + return self.attributes['legend'] + + def getClosestPoint(self, pntXY, pointScaled= True): + """Returns the index of closest point on the curve, pointXY, scaledXY, distance + x, y in user coords + if pointScaled == True based on screen coords + if pointScaled == False based on user coords + """ + if pointScaled == True: + #Using screen coords + p = self.scaled + pxy = self.currentScale * _Numeric.array(pntXY)+ self.currentShift + else: + #Using user coords + p = self.points + pxy = _Numeric.array(pntXY) + #determine distance for each point + d= _Numeric.sqrt(_Numeric.add.reduce((p-pxy)**2,1)) #sqrt(dx^2+dy^2) + pntIndex = _Numeric.argmin(d) + dist = d[pntIndex] + return [pntIndex, self.points[pntIndex], self.scaled[pntIndex] / self._pointSize, dist] + + +class PolyLine(PolyPoints): + """Class to define line type and style + - All methods except __init__ are private. + """ + + _attributes = {'colour': 'black', + 'width': 1, + 'style': wx.SOLID, + 'legend': ''} + + def __init__(self, points, **attr): + """ + Creates PolyLine object + + :param `points`: sequence (array, tuple or list) of (x,y) points making up line + :keyword `attr`: keyword attributes, default to: + + ========================== ================================ + 'colour'= 'black' wx.Pen Colour any wx.NamedColour + 'width'= 1 Pen width + 'style'= wx.SOLID wx.Pen style + 'legend'= '' Line Legend to display + ========================== ================================ + + """ + PolyPoints.__init__(self, points, attr) + + def draw(self, dc, printerScale, coord= None): + colour = self.attributes['colour'] + width = self.attributes['width'] * printerScale * self._pointSize[0] + style= self.attributes['style'] + if not isinstance(colour, wx.Colour): + colour = wx.NamedColour(colour) + pen = wx.Pen(colour, width, style) + pen.SetCap(wx.CAP_BUTT) + dc.SetPen(pen) + if coord == None: + if len(self.scaled): # bugfix for Mac OS X + dc.DrawLines(self.scaled) + else: + dc.DrawLines(coord) # draw legend line + + def getSymExtent(self, printerScale): + """Width and Height of Marker""" + h= self.attributes['width'] * printerScale * self._pointSize[0] + w= 5 * h + return (w,h) + +class PolySpline(PolyLine): + """Class to define line type and style + - All methods except __init__ are private. + """ + + _attributes = {'colour': 'black', + 'width': 1, + 'style': wx.SOLID, + 'legend': ''} + + def __init__(self, points, **attr): + """ + Creates PolyLine object + + :param `points`: sequence (array, tuple or list) of (x,y) points making up spline + :keyword `attr`: keyword attributes, default to: + + ========================== ================================ + 'colour'= 'black' wx.Pen Colour any wx.NamedColour + 'width'= 1 Pen width + 'style'= wx.SOLID wx.Pen style + 'legend'= '' Line Legend to display + ========================== ================================ + + """ + PolyLine.__init__(self, points, **attr) + + def draw(self, dc, printerScale, coord= None): + colour = self.attributes['colour'] + width = self.attributes['width'] * printerScale * self._pointSize[0] + style= self.attributes['style'] + if not isinstance(colour, wx.Colour): + colour = wx.NamedColour(colour) + pen = wx.Pen(colour, width, style) + pen.SetCap(wx.CAP_ROUND) + dc.SetPen(pen) + if coord == None: + if len(self.scaled): # bugfix for Mac OS X + dc.DrawSpline(self.scaled) + else: + dc.DrawLines(coord) # draw legend line + +class PolyMarker(PolyPoints): + """Class to define marker type and style + - All methods except __init__ are private. + """ + + _attributes = {'colour': 'black', + 'width': 1, + 'size': 2, + 'fillcolour': None, + 'fillstyle': wx.SOLID, + 'marker': 'circle', + 'legend': ''} + + def __init__(self, points, **attr): + """ + Creates PolyMarker object + + :param `points`: sequence (array, tuple or list) of (x,y) points + :keyword `attr`: keyword attributes, default to: + + ============================ ================================ + 'colour'= 'black' wx.Pen Colour any wx.NamedColour + 'width'= 1 Pen width + 'size'= 2 Marker size + 'fillcolour'= same as colour wx.Brush Colour any wx.NamedColour + 'fillstyle'= wx.SOLID wx.Brush fill style (use wx.TRANSPARENT for no fill) + 'style'= wx.SOLID wx.Pen style + 'marker'= 'circle' Marker shape + 'legend'= '' Line Legend to display + ============================ ================================ + + Marker Shapes: + - 'circle' + - 'dot' + - 'square' + - 'triangle' + - 'triangle_down' + - 'cross' + - 'plus' + """ + + PolyPoints.__init__(self, points, attr) + + def draw(self, dc, printerScale, coord= None): + colour = self.attributes['colour'] + width = self.attributes['width'] * printerScale * self._pointSize[0] + size = self.attributes['size'] * printerScale * self._pointSize[0] + fillcolour = self.attributes['fillcolour'] + fillstyle = self.attributes['fillstyle'] + marker = self.attributes['marker'] + + if colour and not isinstance(colour, wx.Colour): + colour = wx.NamedColour(colour) + if fillcolour and not isinstance(fillcolour, wx.Colour): + fillcolour = wx.NamedColour(fillcolour) + + dc.SetPen(wx.Pen(colour, width)) + if fillcolour: + dc.SetBrush(wx.Brush(fillcolour,fillstyle)) + else: + dc.SetBrush(wx.Brush(colour, fillstyle)) + if coord == None: + if len(self.scaled): # bugfix for Mac OS X + self._drawmarkers(dc, self.scaled, marker, size) + else: + self._drawmarkers(dc, coord, marker, size) # draw legend marker + + def getSymExtent(self, printerScale): + """Width and Height of Marker""" + s= 5*self.attributes['size'] * printerScale * self._pointSize[0] + return (s,s) + + def _drawmarkers(self, dc, coords, marker,size=1): + f = eval('self._' +marker) + f(dc, coords, size) + + def _circle(self, dc, coords, size=1): + fact= 2.5*size + wh= 5.0*size + rect= _Numeric.zeros((len(coords),4),_Numeric.Float)+[0.0,0.0,wh,wh] + rect[:,0:2]= coords-[fact,fact] + dc.DrawEllipseList(rect.astype(_Numeric.Int32)) + + def _dot(self, dc, coords, size=1): + dc.DrawPointList(coords) + + def _square(self, dc, coords, size=1): + fact= 2.5*size + wh= 5.0*size + rect= _Numeric.zeros((len(coords),4),_Numeric.Float)+[0.0,0.0,wh,wh] + rect[:,0:2]= coords-[fact,fact] + dc.DrawRectangleList(rect.astype(_Numeric.Int32)) + + def _triangle(self, dc, coords, size=1): + shape= [(-2.5*size,1.44*size), (2.5*size,1.44*size), (0.0,-2.88*size)] + poly= _Numeric.repeat(coords,3) + poly.shape= (len(coords),3,2) + poly += shape + dc.DrawPolygonList(poly.astype(_Numeric.Int32)) + + def _triangle_down(self, dc, coords, size=1): + shape= [(-2.5*size,-1.44*size), (2.5*size,-1.44*size), (0.0,2.88*size)] + poly= _Numeric.repeat(coords,3) + poly.shape= (len(coords),3,2) + poly += shape + dc.DrawPolygonList(poly.astype(_Numeric.Int32)) + + def _cross(self, dc, coords, size=1): + fact= 2.5*size + for f in [[-fact,-fact,fact,fact],[-fact,fact,fact,-fact]]: + lines= _Numeric.concatenate((coords,coords),axis=1)+f + dc.DrawLineList(lines.astype(_Numeric.Int32)) + + def _plus(self, dc, coords, size=1): + fact= 2.5*size + for f in [[-fact,0,fact,0],[0,-fact,0,fact]]: + lines= _Numeric.concatenate((coords,coords),axis=1)+f + dc.DrawLineList(lines.astype(_Numeric.Int32)) + +class PlotGraphics: + """Container to hold PolyXXX objects and graph labels + - All methods except __init__ are private. + """ + + def __init__(self, objects, title='', xLabel='', yLabel= ''): + """Creates PlotGraphics object + objects - list of PolyXXX objects to make graph + title - title shown at top of graph + xLabel - label shown on x-axis + yLabel - label shown on y-axis + """ + if type(objects) not in [list,tuple]: + raise TypeError, "objects argument should be list or tuple" + self.objects = objects + self.title= title + self.xLabel= xLabel + self.yLabel= yLabel + self._pointSize = (1.0, 1.0) + + def setLogScale(self, logscale): + if type(logscale) != tuple: + raise TypeError, 'logscale must be a tuple of bools, e.g. (False, False)' + if len(self.objects) == 0: + return + for o in self.objects: + o.setLogScale(logscale) + + def boundingBox(self): + p1, p2 = self.objects[0].boundingBox() + for o in self.objects[1:]: + p1o, p2o = o.boundingBox() + p1 = _Numeric.minimum(p1, p1o) + p2 = _Numeric.maximum(p2, p2o) + return p1, p2 + + def scaleAndShift(self, scale=(1,1), shift=(0,0)): + for o in self.objects: + o.scaleAndShift(scale, shift) + + def setPrinterScale(self, scale): + """Thickens up lines and markers only for printing""" + self.printerScale= scale + + def setXLabel(self, xLabel= ''): + """Set the X axis label on the graph""" + self.xLabel= xLabel + + def setYLabel(self, yLabel= ''): + """Set the Y axis label on the graph""" + self.yLabel= yLabel + + def setTitle(self, title= ''): + """Set the title at the top of graph""" + self.title= title + + def getXLabel(self): + """Get x axis label string""" + return self.xLabel + + def getYLabel(self): + """Get y axis label string""" + return self.yLabel + + def getTitle(self, title= ''): + """Get the title at the top of graph""" + return self.title + + def draw(self, dc): + for o in self.objects: + #t=_time.clock() # profile info + o._pointSize = self._pointSize + o.draw(dc, self.printerScale) + #dt= _time.clock()-t + #print o, "time=", dt + + def getSymExtent(self, printerScale): + """Get max width and height of lines and markers symbols for legend""" + self.objects[0]._pointSize = self._pointSize + symExt = self.objects[0].getSymExtent(printerScale) + for o in self.objects[1:]: + o._pointSize = self._pointSize + oSymExt = o.getSymExtent(printerScale) + symExt = _Numeric.maximum(symExt, oSymExt) + return symExt + + def getLegendNames(self): + """Returns list of legend names""" + lst = [None]*len(self) + for i in range(len(self)): + lst[i]= self.objects[i].getLegend() + return lst + + def __len__(self): + return len(self.objects) + + def __getitem__(self, item): + return self.objects[item] + + +#------------------------------------------------------------------------------- +# Main window that you will want to import into your application. + +class PlotCanvas(wx.Panel): + """ + Subclass of a wx.Panel which holds two scrollbars and the actual + plotting canvas (self.canvas). It allows for simple general plotting + of data with zoom, labels, and automatic axis scaling.""" + + def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, + size=wx.DefaultSize, style=0, name="plotCanvas"): + """Constructs a panel, which can be a child of a frame or + any other non-control window""" + + wx.Panel.__init__(self, parent, id, pos, size, style, name) + + sizer = wx.FlexGridSizer(2,2,0,0) + self.canvas = wx.Window(self, -1) + self.sb_vert = wx.ScrollBar(self, -1, style=wx.SB_VERTICAL) + self.sb_vert.SetScrollbar(0,1000,1000,1000) + self.sb_hor = wx.ScrollBar(self, -1, style=wx.SB_HORIZONTAL) + self.sb_hor.SetScrollbar(0,1000,1000,1000) + + sizer.Add(self.canvas, 1, wx.EXPAND) + sizer.Add(self.sb_vert, 0, wx.EXPAND) + sizer.Add(self.sb_hor, 0, wx.EXPAND) + sizer.Add((0,0)) + + sizer.AddGrowableRow(0, 1) + sizer.AddGrowableCol(0, 1) + + self.sb_vert.Show(False) + self.sb_hor.Show(False) + + self.SetSizer(sizer) + self.Fit() + + self.border = (1,1) + + self.SetBackgroundColour("white") + + # Create some mouse events for zooming + self.canvas.Bind(wx.EVT_LEFT_DOWN, self.OnMouseLeftDown) + self.canvas.Bind(wx.EVT_LEFT_UP, self.OnMouseLeftUp) + self.canvas.Bind(wx.EVT_MOTION, self.OnMotion) + self.canvas.Bind(wx.EVT_LEFT_DCLICK, self.OnMouseDoubleClick) + self.canvas.Bind(wx.EVT_RIGHT_DOWN, self.OnMouseRightDown) + + # scrollbar events + self.Bind(wx.EVT_SCROLL_THUMBTRACK, self.OnScroll) + self.Bind(wx.EVT_SCROLL_PAGEUP, self.OnScroll) + self.Bind(wx.EVT_SCROLL_PAGEDOWN, self.OnScroll) + self.Bind(wx.EVT_SCROLL_LINEUP, self.OnScroll) + self.Bind(wx.EVT_SCROLL_LINEDOWN, self.OnScroll) + + # set curser as cross-hairs + self.canvas.SetCursor(wx.CROSS_CURSOR) + self.HandCursor = wx.CursorFromImage(Hand.GetImage()) + self.GrabHandCursor = wx.CursorFromImage(GrabHand.GetImage()) + self.MagCursor = wx.CursorFromImage(MagPlus.GetImage()) + + # Things for printing + self._print_data = None + self._pageSetupData= None + self.printerScale = 1 + self.parent= parent + + # scrollbar variables + self._sb_ignore = False + self._adjustingSB = False + self._sb_xfullrange = 0 + self._sb_yfullrange = 0 + self._sb_xunit = 0 + self._sb_yunit = 0 + + self._dragEnabled = False + self._screenCoordinates = _Numeric.array([0.0, 0.0]) + + self._logscale = (False, False) + + # Zooming variables + self._zoomInFactor = 0.5 + self._zoomOutFactor = 2 + self._zoomCorner1= _Numeric.array([0.0, 0.0]) # left mouse down corner + self._zoomCorner2= _Numeric.array([0.0, 0.0]) # left mouse up corner + self._zoomEnabled= False + self._hasDragged= False + + # Drawing Variables + self.last_draw = None + self._pointScale= 1 + self._pointShift= 0 + self._xSpec= 'auto' + self._ySpec= 'auto' + self._gridEnabled= False + self._legendEnabled= False + self._titleEnabled= True + self._centerLinesEnabled = False + self._diagonalsEnabled = False + + # Fonts + self._fontCache = {} + self._fontSizeAxis= 10 + self._fontSizeTitle= 15 + self._fontSizeLegend= 7 + + # pointLabels + self._pointLabelEnabled= False + self.last_PointLabel= None + self._pointLabelFunc= None + self.canvas.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeave) + if sys.platform != "darwin": + self._logicalFunction = wx.EQUIV # (NOT src) XOR dst + else: + self._logicalFunction = wx.COPY # wx.EQUIV not supported on Mac OS X + + self._useScientificNotation = False + + self._antiAliasingEnabled = False + self._hiResEnabled = False + self._pointSize = (1.0, 1.0) + self._fontScale = 1.0 + + self.canvas.Bind(wx.EVT_PAINT, self.OnPaint) + self.canvas.Bind(wx.EVT_SIZE, self.OnSize) + # OnSize called to make sure the buffer is initialized. + # This might result in OnSize getting called twice on some + # platforms at initialization, but little harm done. + self.OnSize(None) # sets the initial size based on client size + + self._gridColour = wx.NamedColour('black') + + def SetCursor(self, cursor): + self.canvas.SetCursor(cursor) + + def GetGridColour(self): + return self._gridColour + + def SetGridColour(self, colour): + if isinstance(colour, wx.Colour): + self._gridColour = colour + else: + self._gridColour = wx.NamedColour(colour) + + + # SaveFile + def SaveFile(self, fileName= ''): + """Saves the file to the type specified in the extension. If no file + name is specified a dialog box is provided. Returns True if sucessful, + otherwise False. + + .bmp Save a Windows bitmap file. + .xbm Save an X bitmap file. + .xpm Save an XPM bitmap file. + .png Save a Portable Network Graphics file. + .jpg Save a Joint Photographic Experts Group file. + """ + extensions = { + "bmp": wx.BITMAP_TYPE_BMP, # Save a Windows bitmap file. + "xbm": wx.BITMAP_TYPE_XBM, # Save an X bitmap file. + "xpm": wx.BITMAP_TYPE_XPM, # Save an XPM bitmap file. + "jpg": wx.BITMAP_TYPE_JPEG, # Save a JPG file. + "png": wx.BITMAP_TYPE_PNG, # Save a PNG file. + } + + fType = _string.lower(fileName[-3:]) + dlg1 = None + while fType not in extensions: + + if dlg1: # FileDialog exists: Check for extension + dlg2 = wx.MessageDialog(self, 'File name extension\n' + 'must be one of\nbmp, xbm, xpm, png, or jpg', + 'File Name Error', wx.OK | wx.ICON_ERROR) + try: + dlg2.ShowModal() + finally: + dlg2.Destroy() + else: # FileDialog doesn't exist: just check one + dlg1 = wx.FileDialog( + self, + "Choose a file with extension bmp, gif, xbm, xpm, png, or jpg", ".", "", + "BMP files (*.bmp)|*.bmp|XBM files (*.xbm)|*.xbm|XPM file (*.xpm)|*.xpm|PNG files (*.png)|*.png|JPG files (*.jpg)|*.jpg", + wx.SAVE|wx.OVERWRITE_PROMPT + ) + + if dlg1.ShowModal() == wx.ID_OK: + fileName = dlg1.GetPath() + fType = _string.lower(fileName[-3:]) + else: # exit without saving + dlg1.Destroy() + return False + + if dlg1: + dlg1.Destroy() + + # Save Bitmap + res= self._Buffer.SaveFile(fileName, extensions[fType]) + return res + + @property + def print_data(self): + if not self._print_data: + self._print_data = wx.PrintData() + self._print_data.SetPaperId(wx.PAPER_LETTER) + self._print_data.SetOrientation(wx.LANDSCAPE) + return self._print_data + + @property + def pageSetupData(self): + if not self._pageSetupData: + self._pageSetupData= wx.PageSetupDialogData() + self._pageSetupData.SetMarginBottomRight((25,25)) + self._pageSetupData.SetMarginTopLeft((25,25)) + self._pageSetupData.SetPrintData(self.print_data) + return self._pageSetupData + + def PageSetup(self): + """Brings up the page setup dialog""" + data = self.pageSetupData + data.SetPrintData(self.print_data) + dlg = wx.PageSetupDialog(self.parent, data) + try: + if dlg.ShowModal() == wx.ID_OK: + data = dlg.GetPageSetupData() # returns wx.PageSetupDialogData + # updates page parameters from dialog + self.pageSetupData.SetMarginBottomRight(data.GetMarginBottomRight()) + self.pageSetupData.SetMarginTopLeft(data.GetMarginTopLeft()) + self.pageSetupData.SetPrintData(data.GetPrintData()) + self.print_data=wx.PrintData(data.GetPrintData()) # updates print_data + finally: + dlg.Destroy() + + def Printout(self, paper=None): + """Print current plot.""" + if paper != None: + self.print_data.SetPaperId(paper) + pdd = wx.PrintDialogData(self.print_data) + printer = wx.Printer(pdd) + out = PlotPrintout(self) + print_ok = printer.Print(self.parent, out) + if print_ok: + self._print_data = wx.PrintData(printer.GetPrintDialogData().GetPrintData()) + out.Destroy() + + def PrintPreview(self): + """Print-preview current plot.""" + printout = PlotPrintout(self) + printout2 = PlotPrintout(self) + self.preview = wx.PrintPreview(printout, printout2, self.print_data) + if not self.preview.Ok(): + wx.MessageDialog(self, "Print Preview failed.\n" \ + "Check that default printer is configured\n", \ + "Print error", wx.OK|wx.CENTRE).ShowModal() + self.preview.SetZoom(40) + # search up tree to find frame instance + frameInst= self + while not isinstance(frameInst, wx.Frame): + frameInst= frameInst.GetParent() + frame = wx.PreviewFrame(self.preview, frameInst, "Preview") + frame.Initialize() + frame.SetPosition(self.GetPosition()) + frame.SetSize((600,550)) + frame.Centre(wx.BOTH) + frame.Show(True) + + def setLogScale(self, logscale): + if type(logscale) != tuple: + raise TypeError, 'logscale must be a tuple of bools, e.g. (False, False)' + if self.last_draw is not None: + graphics, xAxis, yAxis= self.last_draw + graphics.setLogScale(logscale) + self.last_draw = (graphics, None, None) + self.SetXSpec('min') + self.SetYSpec('min') + self._logscale = logscale + + def getLogScale(self): + return self._logscale + + def SetFontSizeAxis(self, point= 10): + """Set the tick and axis label font size (default is 10 point)""" + self._fontSizeAxis= point + + def GetFontSizeAxis(self): + """Get current tick and axis label font size in points""" + return self._fontSizeAxis + + def SetFontSizeTitle(self, point= 15): + """Set Title font size (default is 15 point)""" + self._fontSizeTitle= point + + def GetFontSizeTitle(self): + """Get current Title font size in points""" + return self._fontSizeTitle + + def SetFontSizeLegend(self, point= 7): + """Set Legend font size (default is 7 point)""" + self._fontSizeLegend= point + + def GetFontSizeLegend(self): + """Get current Legend font size in points""" + return self._fontSizeLegend + + def SetShowScrollbars(self, value): + """Set True to show scrollbars""" + if value not in [True,False]: + raise TypeError, "Value should be True or False" + if value == self.GetShowScrollbars(): + return + self.sb_vert.Show(value) + self.sb_hor.Show(value) + wx.CallAfter(self.Layout) + + def GetShowScrollbars(self): + """Set True to show scrollbars""" + return self.sb_vert.IsShown() + + def SetUseScientificNotation(self, useScientificNotation): + self._useScientificNotation = useScientificNotation + + def GetUseScientificNotation(self): + return self._useScientificNotation + + def SetEnableAntiAliasing(self, enableAntiAliasing): + """Set True to enable anti-aliasing.""" + self._antiAliasingEnabled = enableAntiAliasing + self.Redraw() + + def GetEnableAntiAliasing(self): + return self._antiAliasingEnabled + + def SetEnableHiRes(self, enableHiRes): + """Set True to enable high-resolution mode when using anti-aliasing.""" + self._hiResEnabled = enableHiRes + self.Redraw() + + def GetEnableHiRes(self): + return self._hiResEnabled + + def SetEnableDrag(self, value): + """Set True to enable drag.""" + if value not in [True,False]: + raise TypeError, "Value should be True or False" + if value: + if self.GetEnableZoom(): + self.SetEnableZoom(False) + self.SetCursor(self.HandCursor) + else: + self.SetCursor(wx.CROSS_CURSOR) + self._dragEnabled = value + + def GetEnableDrag(self): + return self._dragEnabled + + def SetEnableZoom(self, value): + """Set True to enable zooming.""" + if value not in [True,False]: + raise TypeError, "Value should be True or False" + if value: + if self.GetEnableDrag(): + self.SetEnableDrag(False) + self.SetCursor(self.MagCursor) + else: + self.SetCursor(wx.CROSS_CURSOR) + self._zoomEnabled= value + + def GetEnableZoom(self): + """True if zooming enabled.""" + return self._zoomEnabled + + def SetEnableGrid(self, value): + """Set True, 'Horizontal' or 'Vertical' to enable grid.""" + if value not in [True,False,'Horizontal','Vertical']: + raise TypeError, "Value should be True, False, Horizontal or Vertical" + self._gridEnabled= value + self.Redraw() + + def GetEnableGrid(self): + """True if grid enabled.""" + return self._gridEnabled + + def SetEnableCenterLines(self, value): + """Set True, 'Horizontal' or 'Vertical' to enable center line(s).""" + if value not in [True,False,'Horizontal','Vertical']: + raise TypeError, "Value should be True, False, Horizontal or Vertical" + self._centerLinesEnabled= value + self.Redraw() + + def GetEnableCenterLines(self): + """True if grid enabled.""" + return self._centerLinesEnabled + + def SetEnableDiagonals(self, value): + """Set True, 'Bottomleft-Topright' or 'Bottomright-Topleft' to enable + center line(s).""" + if value not in [True,False,'Bottomleft-Topright','Bottomright-Topleft']: + raise TypeError, "Value should be True, False, Bottomleft-Topright or Bottomright-Topleft" + self._diagonalsEnabled= value + self.Redraw() + + def GetEnableDiagonals(self): + """True if grid enabled.""" + return self._diagonalsEnabled + + def SetEnableLegend(self, value): + """Set True to enable legend.""" + if value not in [True,False]: + raise TypeError, "Value should be True or False" + self._legendEnabled= value + self.Redraw() + + def GetEnableLegend(self): + """True if Legend enabled.""" + return self._legendEnabled + + def SetEnableTitle(self, value): + """Set True to enable title.""" + if value not in [True,False]: + raise TypeError, "Value should be True or False" + self._titleEnabled= value + self.Redraw() + + def GetEnableTitle(self): + """True if title enabled.""" + return self._titleEnabled + + def SetEnablePointLabel(self, value): + """Set True to enable pointLabel.""" + if value not in [True,False]: + raise TypeError, "Value should be True or False" + self._pointLabelEnabled= value + self.Redraw() #will erase existing pointLabel if present + self.last_PointLabel = None + + def GetEnablePointLabel(self): + """True if pointLabel enabled.""" + return self._pointLabelEnabled + + def SetPointLabelFunc(self, func): + """Sets the function with custom code for pointLabel drawing + ******** more info needed *************** + """ + self._pointLabelFunc= func + + def GetPointLabelFunc(self): + """Returns pointLabel Drawing Function""" + return self._pointLabelFunc + + def Reset(self): + """Unzoom the plot.""" + self.last_PointLabel = None #reset pointLabel + if self.last_draw is not None: + self._Draw(self.last_draw[0]) + + def ScrollRight(self, units): + """Move view right number of axis units.""" + self.last_PointLabel = None #reset pointLabel + if self.last_draw is not None: + graphics, xAxis, yAxis= self.last_draw + xAxis= (xAxis[0]+units, xAxis[1]+units) + self._Draw(graphics,xAxis,yAxis) + + def ScrollUp(self, units): + """Move view up number of axis units.""" + self.last_PointLabel = None #reset pointLabel + if self.last_draw is not None: + graphics, xAxis, yAxis= self.last_draw + yAxis= (yAxis[0]+units, yAxis[1]+units) + self._Draw(graphics,xAxis,yAxis) + + def GetXY(self, event): + """Wrapper around _getXY, which handles log scales""" + x,y = self._getXY(event) + if self.getLogScale()[0]: + x = _Numeric.power(10,x) + if self.getLogScale()[1]: + y = _Numeric.power(10,y) + return x,y + + def _getXY(self,event): + """Takes a mouse event and returns the XY user axis values.""" + x,y= self.PositionScreenToUser(event.GetPosition()) + return x,y + + def PositionUserToScreen(self, pntXY): + """Converts User position to Screen Coordinates""" + userPos= _Numeric.array(pntXY) + x,y= userPos * self._pointScale + self._pointShift + return x,y + + def PositionScreenToUser(self, pntXY): + """Converts Screen position to User Coordinates""" + screenPos= _Numeric.array(pntXY) + x,y= (screenPos-self._pointShift)/self._pointScale + return x,y + + def SetXSpec(self, type= 'auto'): + """xSpec- defines x axis type. Can be 'none', 'min' or 'auto' + where: + + * 'none' - shows no axis or tick mark values + * 'min' - shows min bounding box values + * 'auto' - rounds axis range to sensible values + * - like 'min', but with tick marks + """ + self._xSpec= type + + def SetYSpec(self, type= 'auto'): + """ySpec- defines x axis type. Can be 'none', 'min' or 'auto' + where: + + * 'none' - shows no axis or tick mark values + * 'min' - shows min bounding box values + * 'auto' - rounds axis range to sensible values + * - like 'min', but with tick marks + """ + self._ySpec= type + + def GetXSpec(self): + """Returns current XSpec for axis""" + return self._xSpec + + def GetYSpec(self): + """Returns current YSpec for axis""" + return self._ySpec + + def GetXMaxRange(self): + xAxis = self._getXMaxRange() + if self.getLogScale()[0]: + xAxis = _Numeric.power(10,xAxis) + return xAxis + + def _getXMaxRange(self): + """Returns (minX, maxX) x-axis range for displayed graph""" + graphics= self.last_draw[0] + p1, p2 = graphics.boundingBox() # min, max points of graphics + xAxis = self._axisInterval(self._xSpec, p1[0], p2[0]) # in user units + return xAxis + + def GetYMaxRange(self): + yAxis = self._getYMaxRange() + if self.getLogScale()[1]: + yAxis = _Numeric.power(10,yAxis) + return yAxis + + def _getYMaxRange(self): + """Returns (minY, maxY) y-axis range for displayed graph""" + graphics= self.last_draw[0] + p1, p2 = graphics.boundingBox() # min, max points of graphics + yAxis = self._axisInterval(self._ySpec, p1[1], p2[1]) + return yAxis + + def GetXCurrentRange(self): + xAxis = self._getXCurrentRange() + if self.getLogScale()[0]: + xAxis = _Numeric.power(10,xAxis) + return xAxis + + def _getXCurrentRange(self): + """Returns (minX, maxX) x-axis for currently displayed portion of graph""" + return self.last_draw[1] + + def GetYCurrentRange(self): + yAxis = self._getYCurrentRange() + if self.getLogScale()[1]: + yAxis = _Numeric.power(10,yAxis) + return yAxis + + def _getYCurrentRange(self): + """Returns (minY, maxY) y-axis for currently displayed portion of graph""" + return self.last_draw[2] + + def Draw(self, graphics, xAxis = None, yAxis = None, dc = None): + """Wrapper around _Draw, which handles log axes""" + + graphics.setLogScale(self.getLogScale()) + + # check Axis is either tuple or none + if type(xAxis) not in [type(None),tuple]: + raise TypeError, "xAxis should be None or (minX,maxX)"+str(type(xAxis)) + if type(yAxis) not in [type(None),tuple]: + raise TypeError, "yAxis should be None or (minY,maxY)"+str(type(xAxis)) + + # check case for axis = (a,b) where a==b caused by improper zooms + if xAxis != None: + if xAxis[0] == xAxis[1]: + return + if self.getLogScale()[0]: + xAxis = _Numeric.log10(xAxis) + if yAxis != None: + if yAxis[0] == yAxis[1]: + return + if self.getLogScale()[1]: + yAxis = _Numeric.log10(yAxis) + self._Draw(graphics, xAxis, yAxis, dc) + + def _Draw(self, graphics, xAxis = None, yAxis = None, dc = None): + """\ + Draw objects in graphics with specified x and y axis. + graphics- instance of PlotGraphics with list of PolyXXX objects + xAxis - tuple with (min, max) axis range to view + yAxis - same as xAxis + dc - drawing context - doesn't have to be specified. + If it's not, the offscreen buffer is used + """ + + if dc == None: + # sets new dc and clears it + dc = wx.BufferedDC(wx.ClientDC(self.canvas), self._Buffer) + bbr = wx.Brush(self.GetBackgroundColour(), wx.SOLID) + dc.SetBackground(bbr) + dc.SetBackgroundMode(wx.SOLID) + dc.Clear() + if self._antiAliasingEnabled: + if not isinstance(dc, wx.GCDC): + try: + dc = wx.GCDC(dc) + except Exception, exception: + pass + else: + if self._hiResEnabled: + dc.SetMapMode(wx.MM_TWIPS) # high precision - each logical unit is 1/20 of a point + self._pointSize = tuple(1.0 / lscale for lscale in dc.GetLogicalScale()) + self._setSize() + elif self._pointSize != (1.0, 1.0): + self._pointSize = (1.0, 1.0) + self._setSize() + if sys.platform in ("darwin", "win32") or not isinstance(dc, wx.GCDC): + self._fontScale = sum(self._pointSize) / 2.0 + else: + # on Linux, we need to correct the font size by a certain factor if wx.GCDC is used, + # to make text the same size as if wx.GCDC weren't used + ppi = dc.GetPPI() + self._fontScale = (96.0 / ppi[0] * self._pointSize[0] + 96.0 / ppi[1] * self._pointSize[1]) / 2.0 + graphics._pointSize = self._pointSize + + dc.SetTextForeground(self.GetForegroundColour()) + dc.SetTextBackground(self.GetBackgroundColour()) + + dc.BeginDrawing() + # dc.Clear() + + # set font size for every thing but title and legend + dc.SetFont(self._getFont(self._fontSizeAxis)) + + # sizes axis to axis type, create lower left and upper right corners of plot + if xAxis == None or yAxis == None: + # One or both axis not specified in Draw + p1, p2 = graphics.boundingBox() # min, max points of graphics + if xAxis == None: + xAxis = self._axisInterval(self._xSpec, p1[0], p2[0]) # in user units + if yAxis == None: + yAxis = self._axisInterval(self._ySpec, p1[1], p2[1]) + # Adjust bounding box for axis spec + p1[0],p1[1] = xAxis[0], yAxis[0] # lower left corner user scale (xmin,ymin) + p2[0],p2[1] = xAxis[1], yAxis[1] # upper right corner user scale (xmax,ymax) + else: + # Both axis specified in Draw + p1= _Numeric.array([xAxis[0], yAxis[0]]) # lower left corner user scale (xmin,ymin) + p2= _Numeric.array([xAxis[1], yAxis[1]]) # upper right corner user scale (xmax,ymax) + + self.last_draw = (graphics, _Numeric.array(xAxis), _Numeric.array(yAxis)) # saves most recient values + + # Get ticks and textExtents for axis if required + if self._xSpec is not 'none': + xticks = self._xticks(xAxis[0], xAxis[1]) + xTextExtent = dc.GetTextExtent(xticks[-1][1])# w h of x axis text last number on axis + else: + xticks = None + xTextExtent= (0,0) # No text for ticks + if self._ySpec is not 'none': + yticks = self._yticks(yAxis[0], yAxis[1]) + if self.getLogScale()[1]: + yTextExtent = dc.GetTextExtent('-2e-2') + else: + yTextExtentBottom = dc.GetTextExtent(yticks[0][1]) + yTextExtentTop = dc.GetTextExtent(yticks[-1][1]) + yTextExtent= (max(yTextExtentBottom[0],yTextExtentTop[0]), + max(yTextExtentBottom[1],yTextExtentTop[1])) + else: + yticks = None + yTextExtent= (0,0) # No text for ticks + + # TextExtents for Title and Axis Labels + titleWH, xLabelWH, yLabelWH= self._titleLablesWH(dc, graphics) + + # TextExtents for Legend + legendBoxWH, legendSymExt, legendTextExt = self._legendWH(dc, graphics) + + # room around graph area + rhsW= max(xTextExtent[0], legendBoxWH[0])+5*self._pointSize[0] # use larger of number width or legend width + lhsW= yTextExtent[0]+ yLabelWH[1] + 3*self._pointSize[0] + bottomH= max(xTextExtent[1], yTextExtent[1]/2.)+ xLabelWH[1] + 2*self._pointSize[1] + topH= yTextExtent[1]/2. + titleWH[1] + textSize_scale= _Numeric.array([rhsW+lhsW,bottomH+topH]) # make plot area smaller by text size + textSize_shift= _Numeric.array([lhsW, bottomH]) # shift plot area by this amount + + # draw title if requested + if self._titleEnabled: + dc.SetFont(self._getFont(self._fontSizeTitle)) + titlePos= (self.plotbox_origin[0]+ lhsW + (self.plotbox_size[0]-lhsW-rhsW)/2.- titleWH[0]/2., + self.plotbox_origin[1]- self.plotbox_size[1]) + dc.DrawText(graphics.getTitle(),titlePos[0],titlePos[1]) + + # draw label text + dc.SetFont(self._getFont(self._fontSizeAxis)) + xLabelPos= (self.plotbox_origin[0]+ lhsW + (self.plotbox_size[0]-lhsW-rhsW)/2.- xLabelWH[0]/2., + self.plotbox_origin[1]- xLabelWH[1]) + dc.DrawText(graphics.getXLabel(),xLabelPos[0],xLabelPos[1]) + yLabelPos= (self.plotbox_origin[0] - 3*self._pointSize[0], + self.plotbox_origin[1]- bottomH- (self.plotbox_size[1]-bottomH-topH)/2.+ yLabelWH[0]/2.) + if graphics.getYLabel(): # bug fix for Linux + dc.DrawRotatedText(graphics.getYLabel(),yLabelPos[0],yLabelPos[1],90) + + # drawing legend makers and text + if self._legendEnabled: + self._drawLegend(dc,graphics,rhsW,topH,legendBoxWH, legendSymExt, legendTextExt) + + # allow for scaling and shifting plotted points + scale = (self.plotbox_size-textSize_scale) / (p2-p1)* _Numeric.array((1,-1)) + shift = -p1*scale + self.plotbox_origin + textSize_shift * _Numeric.array((1,-1)) + self._pointScale= scale / self._pointSize # make available for mouse events + self._pointShift= shift / self._pointSize + self._drawAxes(dc, p1, p2, scale, shift, xticks, yticks) + + graphics.scaleAndShift(scale, shift) + graphics.setPrinterScale(self.printerScale) # thicken up lines and markers if printing + + # set clipping area so drawing does not occur outside axis box + ptx,pty,rectWidth,rectHeight= self._point2ClientCoord(p1, p2) + # allow graph to overlap axis lines by adding units to width and height + dc.SetClippingRegion(ptx*self._pointSize[0],pty*self._pointSize[1],rectWidth*self._pointSize[0]+2,rectHeight*self._pointSize[1]+1) + # Draw the lines and markers + #start = _time.clock() + graphics.draw(dc) + # print "entire graphics drawing took: %f second"%(_time.clock() - start) + # remove the clipping region + dc.DestroyClippingRegion() + dc.EndDrawing() + + self._adjustScrollbars() + + def Redraw(self, dc=None): + """Redraw the existing plot.""" + if self.last_draw is not None: + graphics, xAxis, yAxis= self.last_draw + self._Draw(graphics,xAxis,yAxis,dc) + + def Clear(self): + """Erase the window.""" + self.last_PointLabel = None #reset pointLabel + dc = wx.BufferedDC(wx.ClientDC(self.canvas), self._Buffer) + bbr = wx.Brush(self.GetBackgroundColour(), wx.SOLID) + dc.SetBackground(bbr) + dc.SetBackgroundMode(wx.SOLID) + dc.Clear() + if self._antiAliasingEnabled: + try: + dc = wx.GCDC(dc) + except Exception, exception: + pass + dc.SetTextForeground(self.GetForegroundColour()) + dc.SetTextBackground(self.GetBackgroundColour()) + self.last_draw = None + + def Zoom(self, Center, Ratio): + """ Zoom on the plot + Centers on the X,Y coords given in Center + Zooms by the Ratio = (Xratio, Yratio) given + """ + self.last_PointLabel = None #reset maker + x,y = Center + if self.last_draw != None: + (graphics, xAxis, yAxis) = self.last_draw + w = (xAxis[1] - xAxis[0]) * Ratio[0] + h = (yAxis[1] - yAxis[0]) * Ratio[1] + xAxis = ( x - w/2, x + w/2 ) + yAxis = ( y - h/2, y + h/2 ) + self._Draw(graphics, xAxis, yAxis) + + def GetClosestPoints(self, pntXY, pointScaled= True): + """Returns list with + [curveNumber, legend, index of closest point, pointXY, scaledXY, distance] + list for each curve. + Returns [] if no curves are being plotted. + + x, y in user coords + if pointScaled == True based on screen coords + if pointScaled == False based on user coords + """ + if self.last_draw == None: + #no graph available + return [] + graphics, xAxis, yAxis= self.last_draw + l = [] + for curveNum,obj in enumerate(graphics): + #check there are points in the curve + if len(obj.points) == 0: + continue #go to next obj + #[curveNumber, legend, index of closest point, pointXY, scaledXY, distance] + cn = [curveNum]+ [obj.getLegend()]+ obj.getClosestPoint( pntXY, pointScaled) + l.append(cn) + return l + + def GetClosestPoint(self, pntXY, pointScaled= True): + """Returns list with + [curveNumber, legend, index of closest point, pointXY, scaledXY, distance] + list for only the closest curve. + Returns [] if no curves are being plotted. + + x, y in user coords + if pointScaled == True based on screen coords + if pointScaled == False based on user coords + """ + #closest points on screen based on screen scaling (pointScaled= True) + #list [curveNumber, index, pointXY, scaledXY, distance] for each curve + closestPts= self.GetClosestPoints(pntXY, pointScaled) + if closestPts == []: + return [] #no graph present + #find one with least distance + dists = [c[-1] for c in closestPts] + mdist = min(dists) #Min dist + i = dists.index(mdist) #index for min dist + return closestPts[i] #this is the closest point on closest curve + + GetClosetPoint = GetClosestPoint + + def UpdatePointLabel(self, mDataDict): + """Updates the pointLabel point on screen with data contained in + mDataDict. + + mDataDict will be passed to your function set by + SetPointLabelFunc. It can contain anything you + want to display on the screen at the scaledXY point + you specify. + + This function can be called from parent window with onClick, + onMotion events etc. + """ + if self.last_PointLabel != None: + #compare pointXY + if _Numeric.sometrue(mDataDict["pointXY"] != self.last_PointLabel["pointXY"]): + #closest changed + self._drawPointLabel(self.last_PointLabel) #erase old + self._drawPointLabel(mDataDict) #plot new + else: + #just plot new with no erase + self._drawPointLabel(mDataDict) #plot new + #save for next erase + self.last_PointLabel = mDataDict + + # event handlers ********************************** + def OnMotion(self, event): + if self._zoomEnabled and event.LeftIsDown(): + if self._hasDragged: + self._drawRubberBand(self._zoomCorner1, self._zoomCorner2) # remove old + else: + self._hasDragged= True + self._zoomCorner2[0], self._zoomCorner2[1] = self._getXY(event) + self._drawRubberBand(self._zoomCorner1, self._zoomCorner2) # add new + elif self._dragEnabled and event.LeftIsDown(): + coordinates = event.GetPosition() + newpos, oldpos = map(_Numeric.array, map(self.PositionScreenToUser, [coordinates, self._screenCoordinates])) + dist = newpos-oldpos + self._screenCoordinates = coordinates + + if self.last_draw is not None: + graphics, xAxis, yAxis= self.last_draw + yAxis -= dist[1] + xAxis -= dist[0] + self._Draw(graphics,xAxis,yAxis) + + def OnMouseLeftDown(self,event): + self._zoomCorner1[0], self._zoomCorner1[1]= self._getXY(event) + self._screenCoordinates = _Numeric.array(event.GetPosition()) + if self._dragEnabled: + self.SetCursor(self.GrabHandCursor) + self.canvas.CaptureMouse() + + def OnMouseLeftUp(self, event): + if self._zoomEnabled: + if self._hasDragged == True: + self._drawRubberBand(self._zoomCorner1, self._zoomCorner2) # remove old + self._zoomCorner2[0], self._zoomCorner2[1]= self._getXY(event) + self._hasDragged = False # reset flag + minX, minY= _Numeric.minimum( self._zoomCorner1, self._zoomCorner2) + maxX, maxY= _Numeric.maximum( self._zoomCorner1, self._zoomCorner2) + self.last_PointLabel = None #reset pointLabel + if self.last_draw != None: + self._Draw(self.last_draw[0], xAxis = (minX,maxX), yAxis = (minY,maxY), dc = None) + #else: # A box has not been drawn, zoom in on a point + ## this interfered with the double click, so I've disables it. + # X,Y = self._getXY(event) + # self.Zoom( (X,Y), (self._zoomInFactor,self._zoomInFactor) ) + if self._dragEnabled: + self.SetCursor(self.HandCursor) + if self.canvas.HasCapture(): + self.canvas.ReleaseMouse() + + def OnMouseDoubleClick(self,event): + if self._zoomEnabled: + # Give a little time for the click to be totally finished + # before (possibly) removing the scrollbars and trigering + # size events, etc. + wx.FutureCall(200,self.Reset) + + def OnMouseRightDown(self,event): + if self._zoomEnabled: + X,Y = self._getXY(event) + self.Zoom( (X,Y), (self._zoomOutFactor, self._zoomOutFactor) ) + + def OnPaint(self, event): + # All that is needed here is to draw the buffer to screen + if self.last_PointLabel != None: + self._drawPointLabel(self.last_PointLabel) #erase old + self.last_PointLabel = None + dc = wx.BufferedPaintDC(self.canvas, self._Buffer) + if self._antiAliasingEnabled: + try: + dc = wx.GCDC(dc) + except Exception, exception: + pass + + def OnSize(self,event): + # The Buffer init is done here, to make sure the buffer is always + # the same size as the Window + Size = self.canvas.GetClientSize() + Size.width = max(1, Size.width) + Size.height = max(1, Size.height) + + # Make new offscreen bitmap: this bitmap will always have the + # current drawing in it, so it can be used to save the image to + # a file, or whatever. + self._Buffer = wx.EmptyBitmap(Size.width, Size.height) + self._setSize() + + self.last_PointLabel = None #reset pointLabel + + if self.last_draw is None: + self.Clear() + else: + graphics, xSpec, ySpec = self.last_draw + self._Draw(graphics,xSpec,ySpec) + + def OnLeave(self, event): + """Used to erase pointLabel when mouse outside window""" + if self.last_PointLabel != None: + self._drawPointLabel(self.last_PointLabel) #erase old + self.last_PointLabel = None + + def OnScroll(self, evt): + if not self._adjustingSB: + self._sb_ignore = True + sbpos = evt.GetPosition() + + if evt.GetOrientation() == wx.VERTICAL: + fullrange,pagesize = self.sb_vert.GetRange(),self.sb_vert.GetPageSize() + sbpos = fullrange-pagesize-sbpos + dist = sbpos*self._sb_yunit-(self._getYCurrentRange()[0]-self._sb_yfullrange[0]) + self.ScrollUp(dist) + + if evt.GetOrientation() == wx.HORIZONTAL: + dist = sbpos*self._sb_xunit-(self._getXCurrentRange()[0]-self._sb_xfullrange[0]) + self.ScrollRight(dist) + + # Private Methods ************************************************** + def _setSize(self, width=None, height=None): + """DC width and height.""" + if width == None: + (self.width,self.height) = self.canvas.GetClientSize() + else: + self.width, self.height= width,height + self.width *= self._pointSize[0] # high precision + self.height *= self._pointSize[1] # high precision + self.plotbox_size = 0.97*_Numeric.array([self.width, self.height]) + xo = 0.5*(self.width-self.plotbox_size[0]) + yo = self.height-0.5*(self.height-self.plotbox_size[1]) + self.plotbox_origin = _Numeric.array([xo, yo]) + + def _setPrinterScale(self, scale): + """Used to thicken lines and increase marker size for print out.""" + # line thickness on printer is very thin at 600 dot/in. Markers small + self.printerScale= scale + + def _printDraw(self, printDC): + """Used for printing.""" + if self.last_draw != None: + graphics, xSpec, ySpec= self.last_draw + self._Draw(graphics,xSpec,ySpec,printDC) + + def _drawPointLabel(self, mDataDict): + """Draws and erases pointLabels""" + width = self._Buffer.GetWidth() + height = self._Buffer.GetHeight() + if sys.platform != "darwin": + tmp_Buffer = wx.EmptyBitmap(width,height) + dcs = wx.MemoryDC() + dcs.SelectObject(tmp_Buffer) + dcs.Clear() + else: + tmp_Buffer = self._Buffer.GetSubBitmap((0, 0, width, height)) + dcs = wx.MemoryDC(self._Buffer) + dcs.BeginDrawing() + self._pointLabelFunc(dcs,mDataDict) #custom user pointLabel function + dcs.EndDrawing() + + dc = wx.ClientDC( self.canvas ) + #this will erase if called twice + dc.Blit(0, 0, width, height, dcs, 0, 0, self._logicalFunction) + if sys.platform == "darwin": + self._Buffer = tmp_Buffer + + + def _drawLegend(self,dc,graphics,rhsW,topH,legendBoxWH, legendSymExt, legendTextExt): + """Draws legend symbols and text""" + # top right hand corner of graph box is ref corner + trhc= self.plotbox_origin+ (self.plotbox_size-[rhsW,topH])*[1,-1] + legendLHS= .091* legendBoxWH[0] # border space between legend sym and graph box + lineHeight= max(legendSymExt[1], legendTextExt[1]) * 1.1 #1.1 used as space between lines + dc.SetFont(self._getFont(self._fontSizeLegend)) + for i in range(len(graphics)): + o = graphics[i] + s= i*lineHeight + if isinstance(o,PolyMarker): + # draw marker with legend + pnt= (trhc[0]+legendLHS+legendSymExt[0]/2., trhc[1]+s+lineHeight/2.) + o.draw(dc, self.printerScale, coord= _Numeric.array([pnt])) + elif isinstance(o,PolyLine): + # draw line with legend + pnt1= (trhc[0]+legendLHS, trhc[1]+s+lineHeight/2.) + pnt2= (trhc[0]+legendLHS+legendSymExt[0], trhc[1]+s+lineHeight/2.) + o.draw(dc, self.printerScale, coord= _Numeric.array([pnt1,pnt2])) + else: + raise TypeError, "object is neither PolyMarker or PolyLine instance" + # draw legend txt + pnt= (trhc[0]+legendLHS+legendSymExt[0]+5*self._pointSize[0], trhc[1]+s+lineHeight/2.-legendTextExt[1]/2) + dc.DrawText(o.getLegend(),pnt[0],pnt[1]) + dc.SetFont(self._getFont(self._fontSizeAxis)) # reset + + def _titleLablesWH(self, dc, graphics): + """Draws Title and labels and returns width and height for each""" + # TextExtents for Title and Axis Labels + dc.SetFont(self._getFont(self._fontSizeTitle)) + if self._titleEnabled: + title= graphics.getTitle() + titleWH= dc.GetTextExtent(title) + else: + titleWH= (0,0) + dc.SetFont(self._getFont(self._fontSizeAxis)) + xLabel, yLabel= graphics.getXLabel(),graphics.getYLabel() + xLabelWH= dc.GetTextExtent(xLabel) + yLabelWH= dc.GetTextExtent(yLabel) + return titleWH, xLabelWH, yLabelWH + + def _legendWH(self, dc, graphics): + """Returns the size in screen units for legend box""" + if self._legendEnabled != True: + legendBoxWH= symExt= txtExt= (0,0) + else: + # find max symbol size + symExt= graphics.getSymExtent(self.printerScale) + # find max legend text extent + dc.SetFont(self._getFont(self._fontSizeLegend)) + txtList= graphics.getLegendNames() + txtExt= dc.GetTextExtent(txtList[0]) + for txt in graphics.getLegendNames()[1:]: + txtExt= _Numeric.maximum(txtExt,dc.GetTextExtent(txt)) + maxW= symExt[0]+txtExt[0] + maxH= max(symExt[1],txtExt[1]) + # padding .1 for lhs of legend box and space between lines + maxW= maxW* 1.1 + maxH= maxH* 1.1 * len(txtList) + dc.SetFont(self._getFont(self._fontSizeAxis)) + legendBoxWH= (maxW,maxH) + return (legendBoxWH, symExt, txtExt) + + def _drawRubberBand(self, corner1, corner2): + """Draws/erases rect box from corner1 to corner2""" + ptx,pty,rectWidth,rectHeight= self._point2ClientCoord(corner1, corner2) + # draw rectangle + dc = wx.ClientDC( self.canvas ) + dc.BeginDrawing() + dc.SetPen(wx.Pen(wx.BLACK)) + dc.SetBrush(wx.Brush( wx.WHITE, wx.TRANSPARENT ) ) + dc.SetLogicalFunction(wx.INVERT) + dc.DrawRectangle( ptx,pty, rectWidth,rectHeight) + dc.SetLogicalFunction(wx.COPY) + dc.EndDrawing() + + def _getFont(self,size): + """Take font size, adjusts if printing and returns wx.Font""" + s = size*self.printerScale*self._fontScale + of = self.GetFont() + # Linux speed up to get font from cache rather than X font server + key = (int(s), of.GetFamily (), of.GetStyle (), of.GetWeight ()) + font = self._fontCache.get (key, None) + if font: + return font # yeah! cache hit + else: + font = wx.Font(int(s), of.GetFamily(), of.GetStyle(), of.GetWeight()) + self._fontCache[key] = font + return font + + + def _point2ClientCoord(self, corner1, corner2): + """Converts user point coords to client screen int coords x,y,width,height""" + c1= _Numeric.array(corner1) + c2= _Numeric.array(corner2) + # convert to screen coords + pt1= c1*self._pointScale+self._pointShift + pt2= c2*self._pointScale+self._pointShift + # make height and width positive + pul= _Numeric.minimum(pt1,pt2) # Upper left corner + plr= _Numeric.maximum(pt1,pt2) # Lower right corner + rectWidth, rectHeight= plr-pul + ptx,pty= pul + return ptx, pty, rectWidth, rectHeight + + def _axisInterval(self, spec, lower, upper): + """Returns sensible axis range for given spec""" + if spec == 'none' or spec == 'min' or isinstance(spec, (float, int)): + if lower == upper: + return lower-0.5, upper+0.5 + else: + return lower, upper + elif spec == 'auto': + range = upper-lower + if range == 0.: + return lower-0.5, upper+0.5 + log = _Numeric.log10(range) + power = _Numeric.floor(log) + fraction = log-power + if fraction <= 0.05: + power = power-1 + grid = 10.**power + lower = lower - lower % grid + mod = upper % grid + if mod != 0: + upper = upper - mod + grid + return lower, upper + elif type(spec) == type(()): + lower, upper = spec + if lower <= upper: + return lower, upper + else: + return upper, lower + else: + raise ValueError, str(spec) + ': illegal axis specification' + + def _drawAxes(self, dc, p1, p2, scale, shift, xticks, yticks): + + penWidth= self.printerScale * self._pointSize[0] # increases thickness for printing only + dc.SetPen(wx.Pen(self._gridColour, penWidth)) + + # set length of tick marks--long ones make grid + if self._gridEnabled: + x,y,width,height= self._point2ClientCoord(p1,p2) + if self._gridEnabled == 'Horizontal': + yTickLength= (width/2.0 +1) * self._pointSize[1] + xTickLength= 3 * self.printerScale * self._pointSize[0] + elif self._gridEnabled == 'Vertical': + yTickLength= 3 * self.printerScale * self._pointSize[1] + xTickLength= (height/2.0 +1) * self._pointSize[0] + else: + yTickLength= (width/2.0 +1) * self._pointSize[1] + xTickLength= (height/2.0 +1) * self._pointSize[0] + else: + yTickLength= 3 * self.printerScale * self._pointSize[1] # lengthens lines for printing + xTickLength= 3 * self.printerScale * self._pointSize[0] + + if self._xSpec is not 'none': + lower, upper = p1[0],p2[0] + text = 1 + for y, d in [(p1[1], -xTickLength), (p2[1], xTickLength)]: # miny, maxy and tick lengths + for x, label in xticks: + pt = scale*_Numeric.array([x, y])+shift + dc.DrawLine(pt[0],pt[1],pt[0],pt[1] + d) # draws tick mark d units + if text: + dc.DrawText(label,pt[0],pt[1]+2*self._pointSize[1]) + a1 = scale*_Numeric.array([lower, y])+shift + a2 = scale*_Numeric.array([upper, y])+shift + dc.DrawLine(a1[0],a1[1],a2[0],a2[1]) # draws upper and lower axis line + text = 0 # axis values not drawn on top side + + if self._ySpec is not 'none': + lower, upper = p1[1],p2[1] + text = 1 + h = dc.GetCharHeight() + for x, d in [(p1[0], -yTickLength), (p2[0], yTickLength)]: + for y, label in yticks: + pt = scale*_Numeric.array([x, y])+shift + dc.DrawLine(pt[0],pt[1],pt[0]-d,pt[1]) + if text: + dc.DrawText(label,pt[0]-dc.GetTextExtent(label)[0]-3*self._pointSize[0], + pt[1]-0.75*h) + a1 = scale*_Numeric.array([x, lower])+shift + a2 = scale*_Numeric.array([x, upper])+shift + dc.DrawLine(a1[0],a1[1],a2[0],a2[1]) + text = 0 # axis values not drawn on right side + + if self._centerLinesEnabled: + if self._centerLinesEnabled in ('Horizontal', True): + y1 = scale[1]*p1[1]+shift[1] + y2 = scale[1]*p2[1]+shift[1] + y = (y1 - y2) / 2.0 + y2 + dc.DrawLine(scale[0] * p1[0] + shift[0], y, scale[0] * p2[0] + shift[0], y) + if self._centerLinesEnabled in ('Vertical', True): + x1 = scale[0]*p1[0]+shift[0] + x2 = scale[0]*p2[0]+shift[0] + x = (x1 - x2) / 2.0 + x2 + dc.DrawLine(x, scale[1] * p1[1] + shift[1], x, scale[1] * p2[1] + shift[1]) + + if self._diagonalsEnabled: + if self._diagonalsEnabled in ('Bottomleft-Topright', True): + dc.DrawLine(scale[0] * p1[0] + shift[0], scale[1] * p1[1] + shift[1], scale[0] * p2[0] + shift[0], scale[1] * p2[1] + shift[1]) + if self._diagonalsEnabled in ('Bottomright-Topleft', True): + dc.DrawLine(scale[0] * p1[0] + shift[0], scale[1] * p2[1] + shift[1], scale[0] * p2[0] + shift[0], scale[1] * p1[1] + shift[1]) + + def _xticks(self, *args): + if self._logscale[0]: + return self._logticks(*args) + else: + attr = {'numticks': self._xSpec} + return self._ticks(*args, **attr) + + def _yticks(self, *args): + if self._logscale[1]: + return self._logticks(*args) + else: + attr = {'numticks': self._ySpec} + return self._ticks(*args, **attr) + + def _logticks(self, lower, upper): + #lower,upper = map(_Numeric.log10,[lower,upper]) + #print 'logticks',lower,upper + ticks = [] + mag = _Numeric.power(10,_Numeric.floor(lower)) + if upper-lower > 6: + t = _Numeric.power(10,_Numeric.ceil(lower)) + base = _Numeric.power(10,_Numeric.floor((upper-lower)/6)) + def inc(t): + return t*base-t + else: + t = _Numeric.ceil(_Numeric.power(10,lower)/mag)*mag + def inc(t): + return 10**int(_Numeric.floor(_Numeric.log10(t)+1e-16)) + majortick = int(_Numeric.log10(mag)) + while t <= pow(10,upper): + if majortick != int(_Numeric.floor(_Numeric.log10(t)+1e-16)): + majortick = int(_Numeric.floor(_Numeric.log10(t)+1e-16)) + ticklabel = '1e%d'%majortick + else: + if upper-lower < 2: + minortick = int(t/pow(10,majortick)+.5) + ticklabel = '%de%d'%(minortick,majortick) + else: + ticklabel = '' + ticks.append((_Numeric.log10(t), ticklabel)) + t += inc(t) + if len(ticks) == 0: + ticks = [(0,'')] + return ticks + + def _ticks(self, lower, upper, numticks=None): + if isinstance(numticks, (float, int)): + ideal = (upper-lower)/float(numticks) + else: + ideal = (upper-lower)/7. + log = _Numeric.log10(ideal) + power = _Numeric.floor(log) + if isinstance(numticks, (float, int)): + grid = ideal + else: + fraction = log-power + factor = 1. + error = fraction + for f, lf in self._multiples: + e = _Numeric.fabs(fraction-lf) + if e < error: + error = e + factor = f + grid = factor * 10.**power + if self._useScientificNotation and (power > 4 or power < -4): + format = '%+7.1e' + elif power >= 0: + digits = max(1, int(power)) + format = '%' + `digits`+'.0f' + else: + digits = -int(power) + format = '%'+`digits+2`+'.'+`digits`+'f' + ticks = [] + t = -grid*_Numeric.floor(-lower/grid) + while t <= upper: + if t == -0: + t = 0 + ticks.append( (t, format % (t,)) ) + t = t + grid + return ticks + + _multiples = [(2., _Numeric.log10(2.)), (5., _Numeric.log10(5.))] + + + def _adjustScrollbars(self): + if self._sb_ignore: + self._sb_ignore = False + return + + if not self.GetShowScrollbars(): + return + + self._adjustingSB = True + needScrollbars = False + + # horizontal scrollbar + r_current = self._getXCurrentRange() + r_max = list(self._getXMaxRange()) + sbfullrange = float(self.sb_hor.GetRange()) + + r_max[0] = min(r_max[0],r_current[0]) + r_max[1] = max(r_max[1],r_current[1]) + + self._sb_xfullrange = r_max + + unit = (r_max[1]-r_max[0])/float(self.sb_hor.GetRange()) + pos = int((r_current[0]-r_max[0])/unit) + + if pos >= 0: + pagesize = int((r_current[1]-r_current[0])/unit) + + self.sb_hor.SetScrollbar(pos, pagesize, sbfullrange, pagesize) + self._sb_xunit = unit + needScrollbars = needScrollbars or (pagesize != sbfullrange) + else: + self.sb_hor.SetScrollbar(0, 1000, 1000, 1000) + + # vertical scrollbar + r_current = self._getYCurrentRange() + r_max = list(self._getYMaxRange()) + sbfullrange = float(self.sb_vert.GetRange()) + + r_max[0] = min(r_max[0],r_current[0]) + r_max[1] = max(r_max[1],r_current[1]) + + self._sb_yfullrange = r_max + + unit = (r_max[1]-r_max[0])/sbfullrange + pos = int((r_current[0]-r_max[0])/unit) + + if pos >= 0: + pagesize = int((r_current[1]-r_current[0])/unit) + pos = (sbfullrange-1-pos-pagesize) + self.sb_vert.SetScrollbar(pos, pagesize, sbfullrange, pagesize) + self._sb_yunit = unit + needScrollbars = needScrollbars or (pagesize != sbfullrange) + else: + self.sb_vert.SetScrollbar(0, 1000, 1000, 1000) + + self.SetShowScrollbars(needScrollbars) + self._adjustingSB = False + +#------------------------------------------------------------------------------- +# Used to layout the printer page + +class PlotPrintout(wx.Printout): + """Controls how the plot is made in printing and previewing""" + # Do not change method names in this class, + # we have to override wx.Printout methods here! + def __init__(self, graph): + """graph is instance of plotCanvas to be printed or previewed""" + wx.Printout.__init__(self) + self.graph = graph + + def HasPage(self, page): + if page == 1: + return True + else: + return False + + def GetPageInfo(self): + return (1, 1, 1, 1) # disable page numbers + + def OnPrintPage(self, page): + dc = self.GetDC() # allows using floats for certain functions +## print "PPI Printer",self.GetPPIPrinter() +## print "PPI Screen", self.GetPPIScreen() +## print "DC GetSize", dc.GetSize() +## print "GetPageSizePixels", self.GetPageSizePixels() + # Note PPIScreen does not give the correct number + # Calulate everything for printer and then scale for preview + PPIPrinter= self.GetPPIPrinter() # printer dots/inch (w,h) + #PPIScreen= self.GetPPIScreen() # screen dots/inch (w,h) + dcSize= dc.GetSize() # DC size + if self.graph._antiAliasingEnabled and not isinstance(dc, wx.GCDC): + try: + dc = wx.GCDC(dc) + except Exception, exception: + pass + else: + if self.graph._hiResEnabled: + dc.SetMapMode(wx.MM_TWIPS) # high precision - each logical unit is 1/20 of a point + pageSize= self.GetPageSizePixels() # page size in terms of pixcels + clientDcSize= self.graph.GetClientSize() + + # find what the margins are (mm) + margLeftSize,margTopSize= self.graph.pageSetupData.GetMarginTopLeft() + margRightSize, margBottomSize= self.graph.pageSetupData.GetMarginBottomRight() + + # calculate offset and scale for dc + pixLeft= margLeftSize*PPIPrinter[0]/25.4 # mm*(dots/in)/(mm/in) + pixRight= margRightSize*PPIPrinter[0]/25.4 + pixTop= margTopSize*PPIPrinter[1]/25.4 + pixBottom= margBottomSize*PPIPrinter[1]/25.4 + + plotAreaW= pageSize[0]-(pixLeft+pixRight) + plotAreaH= pageSize[1]-(pixTop+pixBottom) + + # ratio offset and scale to screen size if preview + if self.IsPreview(): + ratioW= float(dcSize[0])/pageSize[0] + ratioH= float(dcSize[1])/pageSize[1] + pixLeft *= ratioW + pixTop *= ratioH + plotAreaW *= ratioW + plotAreaH *= ratioH + + # rescale plot to page or preview plot area + self.graph._setSize(plotAreaW,plotAreaH) + + # Set offset and scale + dc.SetDeviceOrigin(pixLeft,pixTop) + + # Thicken up pens and increase marker size for printing + ratioW= float(plotAreaW)/clientDcSize[0] + ratioH= float(plotAreaH)/clientDcSize[1] + aveScale= (ratioW+ratioH)/2 + if self.graph._antiAliasingEnabled and not self.IsPreview(): + scale = dc.GetUserScale() + dc.SetUserScale(scale[0] / self.graph._pointSize[0], scale[1] / self.graph._pointSize[1]) + self.graph._setPrinterScale(aveScale) # tickens up pens for printing + + self.graph._printDraw(dc) + # rescale back to original + self.graph._setSize() + self.graph._setPrinterScale(1) + self.graph.Redraw() #to get point label scale and shift correct + + return True + + +#---------------------------------------------------------------------- +from wx.lib.embeddedimage import PyEmbeddedImage + +MagPlus = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAOFJ" + "REFUeJy1VdEOxCAIo27//8XbuKfuPASGZ0Zisoi2FJABbZM3bY8c13lo5GvbjioBPAUEB0Yc" + "VZ0iGRRc56Ee8DcikEgrJD8EFpzRegQASiRtBtzuA0hrdRPYQxaEKyJPG6IHyiK3xnNZvUSS" + "NvUuzgYh0il4y14nCFPk5XgmNbRbQbVotGo9msj47G3UXJ7fuz8Q8FAGEu0/PbZh2D3NoshU" + "1VUydBGVZKMimlGeErdNGUmf/x7YpjMjcf8HVYvS2adr6aFVlCy/5Ijk9q8SeCR9isJR8SeJ" + "8pv7S0Wu2Acr0qdj3w7DRAAAAABJRU5ErkJggg==") + +GrabHand = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAARFJ" + "REFUeJy1VdESgzAIS2j//4s3s5fRQ6Rad5M7H0oxCZhWSpK1TjwUBCBJAIBItL1fijlfe1yJ" + "8noCGC9KgrXO7f0SyZEDAF/H2opsAHv9V/548nplT5Jo7YAFQKQ1RMWzmHUS96suqdBrHkuV" + "uxpdJjCS8CfGXWdJ2glzcquKSR5c46QOtCpgNyIHj6oieAXg3282QvMX45hy8a8H0VonJZUO" + "clesjOPg/dhBTq64o1Kacz4Ri2x5RKsf8+wcWQaJJL+A+xRcZHeQeBKjK+5EFiVJ4xy4x2Mn" + "1Vk4U5/DWmfPieiqbye7a3tV/cCsWKu76K76KUFFchVnhigJ/hmktelm/m3e3b8k+Ec8PqLH" + "CT4JRfyK9o1xYwAAAABJRU5ErkJggg==") + +Hand = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAARBJ" + "REFUeJytluECwiAIhDn1/Z942/UnjCGoq+6XNeWDC1xAqbKr6zyo61Ibds60J8GBT0yS3IEM" + "ABuIpJTa4IOLiAAQksuKyixLH1ShHgTgZl8KiALxOsODPoEMkgJ25Su6zoO3ZrjRnI96OLIq" + "k7dsqOCboDa4XV/nwQEQVeFtmMnvbSJja+oagKBUaLn9hzd7VipRa9ostIv0O1uhzzaqNJxk" + "hViwDVxqg51kksMg9r2rDDIFwHCap130FBhdMzeAfWg//6Ki5WWQrHSv6EIUeVs0g3wT3J7r" + "FmWQp/JJDXeRh2TXcJa91zAH2uN2mvXFsrIrsjS8rnftWmWfAiLIStuD9m9h9belvzgS/1fP" + "X7075IwDENteAAAAAElFTkSuQmCC") + +#--------------------------------------------------------------------------- +# if running standalone... +# +# ...a sample implementation using the above +# + +def _draw1Objects(): + # 100 points sin function, plotted as green circles + data1 = 2.*_Numeric.pi*_Numeric.arange(200)/200. + data1.shape = (100, 2) + data1[:,1] = _Numeric.sin(data1[:,0]) + markers1 = PolyMarker(data1, legend='Green Markers', colour='green', marker='circle',size=1) + + # 50 points cos function, plotted as red line + data1 = 2.*_Numeric.pi*_Numeric.arange(100)/100. + data1.shape = (50,2) + data1[:,1] = _Numeric.cos(data1[:,0]) + lines = PolySpline(data1, legend= 'Red Line', colour='red') + + # A few more points... + pi = _Numeric.pi + markers2 = PolyMarker([(0., 0.), (pi/4., 1.), (pi/2, 0.), + (3.*pi/4., -1)], legend='Cross Legend', colour='blue', + marker='cross') + + return PlotGraphics([markers1, lines, markers2],"Graph Title", "X Axis", "Y Axis") + +def _draw2Objects(): + # 100 points sin function, plotted as green dots + data1 = 2.*_Numeric.pi*_Numeric.arange(200)/200. + data1.shape = (100, 2) + data1[:,1] = _Numeric.sin(data1[:,0]) + line1 = PolySpline(data1, legend='Green Line', colour='green', width=6, style=wx.DOT) + + # 50 points cos function, plotted as red dot-dash + data1 = 2.*_Numeric.pi*_Numeric.arange(100)/100. + data1.shape = (50,2) + data1[:,1] = _Numeric.cos(data1[:,0]) + line2 = PolySpline(data1, legend='Red Line', colour='red', width=3, style= wx.DOT_DASH) + + # A few more points... + pi = _Numeric.pi + markers1 = PolyMarker([(0., 0.), (pi/4., 1.), (pi/2, 0.), + (3.*pi/4., -1)], legend='Cross Hatch Square', colour='blue', width= 3, size= 6, + fillcolour= 'red', fillstyle= wx.CROSSDIAG_HATCH, + marker='square') + + return PlotGraphics([markers1, line1, line2], "Big Markers with Different Line Styles") + +def _draw3Objects(): + markerList= ['circle', 'dot', 'square', 'triangle', 'triangle_down', + 'cross', 'plus', 'circle'] + m=[] + for i in range(len(markerList)): + m.append(PolyMarker([(2*i+.5,i+.5)], legend=markerList[i], colour='blue', + marker=markerList[i])) + return PlotGraphics(m, "Selection of Markers", "Minimal Axis", "No Axis") + +def _draw4Objects(): + # 25,000 point line + data1 = _Numeric.arange(5e5,1e6,10) + data1.shape = (25000, 2) + line1 = PolyLine(data1, legend='Wide Line', colour='green', width=5) + + # A few more points... + markers2 = PolyMarker(data1, legend='Square', colour='blue', + marker='square') + return PlotGraphics([line1, markers2], "25,000 Points", "Value X", "") + +def _draw5Objects(): + # Empty graph with axis defined but no points/lines + points=[] + line1 = PolyLine(points, legend='Wide Line', colour='green', width=5) + return PlotGraphics([line1], "Empty Plot With Just Axes", "Value X", "Value Y") + +def _draw6Objects(): + # Bar graph + points1=[(1,0), (1,10)] + line1 = PolyLine(points1, colour='green', legend='Feb.', width=10) + points1g=[(2,0), (2,4)] + line1g = PolyLine(points1g, colour='red', legend='Mar.', width=10) + points1b=[(3,0), (3,6)] + line1b = PolyLine(points1b, colour='blue', legend='Apr.', width=10) + + points2=[(4,0), (4,12)] + line2 = PolyLine(points2, colour='Yellow', legend='May', width=10) + points2g=[(5,0), (5,8)] + line2g = PolyLine(points2g, colour='orange', legend='June', width=10) + points2b=[(6,0), (6,4)] + line2b = PolyLine(points2b, colour='brown', legend='July', width=10) + + return PlotGraphics([line1, line1g, line1b, line2, line2g, line2b], + "Bar Graph - (Turn on Grid, Legend)", "Months", "Number of Students") +def _draw7Objects(): + # Empty graph with axis defined but no points/lines + x = _Numeric.arange(1,1000,1) + y1 = 4.5*x**2 + y2 = 2.2*x**3 + points1 = _Numeric.transpose([x,y1]) + points2 = _Numeric.transpose([x,y2]) + line1 = PolyLine(points1, legend='quadratic', colour='blue', width=1) + line2 = PolyLine(points2, legend='cubic', colour='red', width=1) + return PlotGraphics([line1,line2], "double log plot", "Value X", "Value Y") + + +class TestFrame(wx.Frame): + def __init__(self, parent, id, title): + wx.Frame.__init__(self, parent, id, title, + wx.DefaultPosition, (600, 400)) + + # Now Create the menu bar and items + self.mainmenu = wx.MenuBar() + + menu = wx.Menu() + menu.Append(200, 'Page Setup...', 'Setup the printer page') + self.Bind(wx.EVT_MENU, self.OnFilePageSetup, id=200) + + menu.Append(201, 'Print Preview...', 'Show the current plot on page') + self.Bind(wx.EVT_MENU, self.OnFilePrintPreview, id=201) + + menu.Append(202, 'Print...', 'Print the current plot') + self.Bind(wx.EVT_MENU, self.OnFilePrint, id=202) + + menu.Append(203, 'Save Plot...', 'Save current plot') + self.Bind(wx.EVT_MENU, self.OnSaveFile, id=203) + + menu.Append(205, 'E&xit', 'Enough of this already!') + self.Bind(wx.EVT_MENU, self.OnFileExit, id=205) + self.mainmenu.Append(menu, '&File') + + menu = wx.Menu() + menu.Append(206, 'Draw1', 'Draw plots1') + self.Bind(wx.EVT_MENU,self.OnPlotDraw1, id=206) + menu.Append(207, 'Draw2', 'Draw plots2') + self.Bind(wx.EVT_MENU,self.OnPlotDraw2, id=207) + menu.Append(208, 'Draw3', 'Draw plots3') + self.Bind(wx.EVT_MENU,self.OnPlotDraw3, id=208) + menu.Append(209, 'Draw4', 'Draw plots4') + self.Bind(wx.EVT_MENU,self.OnPlotDraw4, id=209) + menu.Append(210, 'Draw5', 'Draw plots5') + self.Bind(wx.EVT_MENU,self.OnPlotDraw5, id=210) + menu.Append(260, 'Draw6', 'Draw plots6') + self.Bind(wx.EVT_MENU,self.OnPlotDraw6, id=260) + menu.Append(261, 'Draw7', 'Draw plots7') + self.Bind(wx.EVT_MENU,self.OnPlotDraw7, id=261) + + menu.Append(211, '&Redraw', 'Redraw plots') + self.Bind(wx.EVT_MENU,self.OnPlotRedraw, id=211) + menu.Append(212, '&Clear', 'Clear canvas') + self.Bind(wx.EVT_MENU,self.OnPlotClear, id=212) + menu.Append(213, '&Scale', 'Scale canvas') + self.Bind(wx.EVT_MENU,self.OnPlotScale, id=213) + menu.Append(214, 'Enable &Zoom', 'Enable Mouse Zoom', kind=wx.ITEM_CHECK) + self.Bind(wx.EVT_MENU,self.OnEnableZoom, id=214) + menu.Append(215, 'Enable &Grid', 'Turn on Grid', kind=wx.ITEM_CHECK) + self.Bind(wx.EVT_MENU,self.OnEnableGrid, id=215) + menu.Append(217, 'Enable &Drag', 'Activates dragging mode', kind=wx.ITEM_CHECK) + self.Bind(wx.EVT_MENU,self.OnEnableDrag, id=217) + menu.Append(220, 'Enable &Legend', 'Turn on Legend', kind=wx.ITEM_CHECK) + self.Bind(wx.EVT_MENU,self.OnEnableLegend, id=220) + menu.Append(222, 'Enable &Point Label', 'Show Closest Point', kind=wx.ITEM_CHECK) + self.Bind(wx.EVT_MENU,self.OnEnablePointLabel, id=222) + + menu.Append(223, 'Enable &Anti-Aliasing', 'Smooth output', kind=wx.ITEM_CHECK) + self.Bind(wx.EVT_MENU,self.OnEnableAntiAliasing, id=223) + menu.Append(224, 'Enable &High-Resolution AA', 'Draw in higher resolution', kind=wx.ITEM_CHECK) + self.Bind(wx.EVT_MENU,self.OnEnableHiRes, id=224) + + menu.Append(226, 'Enable Center Lines', 'Draw center lines', kind=wx.ITEM_CHECK) + self.Bind(wx.EVT_MENU,self.OnEnableCenterLines, id=226) + menu.Append(227, 'Enable Diagonal Lines', 'Draw diagonal lines', kind=wx.ITEM_CHECK) + self.Bind(wx.EVT_MENU,self.OnEnableDiagonals, id=227) + + menu.Append(231, 'Set Gray Background', 'Change background colour to gray') + self.Bind(wx.EVT_MENU,self.OnBackgroundGray, id=231) + menu.Append(232, 'Set &White Background', 'Change background colour to white') + self.Bind(wx.EVT_MENU,self.OnBackgroundWhite, id=232) + menu.Append(233, 'Set Red Label Text', 'Change label text colour to red') + self.Bind(wx.EVT_MENU,self.OnForegroundRed, id=233) + menu.Append(234, 'Set &Black Label Text', 'Change label text colour to black') + self.Bind(wx.EVT_MENU,self.OnForegroundBlack, id=234) + + menu.Append(225, 'Scroll Up 1', 'Move View Up 1 Unit') + self.Bind(wx.EVT_MENU,self.OnScrUp, id=225) + menu.Append(230, 'Scroll Rt 2', 'Move View Right 2 Units') + self.Bind(wx.EVT_MENU,self.OnScrRt, id=230) + menu.Append(235, '&Plot Reset', 'Reset to original plot') + self.Bind(wx.EVT_MENU,self.OnReset, id=235) + + self.mainmenu.Append(menu, '&Plot') + + menu = wx.Menu() + menu.Append(300, '&About', 'About this thing...') + self.Bind(wx.EVT_MENU, self.OnHelpAbout, id=300) + self.mainmenu.Append(menu, '&Help') + + self.SetMenuBar(self.mainmenu) + + # A status bar to tell people what's happening + self.CreateStatusBar(1) + + self.client = PlotCanvas(self) + #define the function for drawing pointLabels + self.client.SetPointLabelFunc(self.DrawPointLabel) + # Create mouse event for showing cursor coords in status bar + self.client.canvas.Bind(wx.EVT_LEFT_DOWN, self.OnMouseLeftDown) + # Show closest point when enabled + self.client.canvas.Bind(wx.EVT_MOTION, self.OnMotion) + + self.Show(True) + + def DrawPointLabel(self, dc, mDataDict): + """This is the fuction that defines how the pointLabels are plotted + dc - DC that will be passed + mDataDict - Dictionary of data that you want to use for the pointLabel + + As an example I have decided I want a box at the curve point + with some text information about the curve plotted below. + Any wxDC method can be used. + """ + # ---------- + dc.SetPen(wx.Pen(wx.BLACK)) + dc.SetBrush(wx.Brush( wx.BLACK, wx.SOLID ) ) + + sx, sy = mDataDict["scaledXY"] #scaled x,y of closest point + dc.DrawRectangle( sx-5,sy-5, 10, 10) #10by10 square centered on point + px,py = mDataDict["pointXY"] + cNum = mDataDict["curveNum"] + pntIn = mDataDict["pIndex"] + legend = mDataDict["legend"] + #make a string to display + s = "Crv# %i, '%s', Pt. (%.2f,%.2f), PtInd %i" %(cNum, legend, px, py, pntIn) + dc.DrawText(s, sx , sy+1) + # ----------- + + def OnMouseLeftDown(self,event): + s= "Left Mouse Down at Point: (%.4f, %.4f)" % self.client._getXY(event) + self.SetStatusText(s) + event.Skip() #allows plotCanvas OnMouseLeftDown to be called + + def OnMotion(self, event): + #show closest point (when enbled) + if self.client.GetEnablePointLabel() == True: + #make up dict with info for the pointLabel + #I've decided to mark the closest point on the closest curve + dlst= self.client.GetClosestPoint( self.client._getXY(event), pointScaled= True) + if dlst != []: #returns [] if none + curveNum, legend, pIndex, pointXY, scaledXY, distance = dlst + #make up dictionary to pass to my user function (see DrawPointLabel) + mDataDict= {"curveNum":curveNum, "legend":legend, "pIndex":pIndex,\ + "pointXY":pointXY, "scaledXY":scaledXY} + #pass dict to update the pointLabel + self.client.UpdatePointLabel(mDataDict) + event.Skip() #go to next handler + + def OnFilePageSetup(self, event): + self.client.PageSetup() + + def OnFilePrintPreview(self, event): + self.client.PrintPreview() + + def OnFilePrint(self, event): + self.client.Printout() + + def OnSaveFile(self, event): + self.client.SaveFile() + + def OnFileExit(self, event): + self.Close() + + def OnPlotDraw1(self, event): + self.resetDefaults() + self.client.Draw(_draw1Objects()) + + def OnPlotDraw2(self, event): + self.resetDefaults() + self.client.Draw(_draw2Objects()) + + def OnPlotDraw3(self, event): + self.resetDefaults() + self.client.SetFont(wx.Font(10,wx.SCRIPT,wx.NORMAL,wx.NORMAL)) + self.client.SetFontSizeAxis(20) + self.client.SetFontSizeLegend(12) + self.client.SetXSpec('min') + self.client.SetYSpec('none') + self.client.Draw(_draw3Objects()) + + def OnPlotDraw4(self, event): + self.resetDefaults() + drawObj= _draw4Objects() + self.client.Draw(drawObj) +## # profile +## start = _time.clock() +## for x in range(10): +## self.client.Draw(drawObj) +## print "10 plots of Draw4 took: %f sec."%(_time.clock() - start) +## # profile end + + def OnPlotDraw5(self, event): + # Empty plot with just axes + self.resetDefaults() + drawObj= _draw5Objects() + # make the axis X= (0,5), Y=(0,10) + # (default with None is X= (-1,1), Y= (-1,1)) + self.client.Draw(drawObj, xAxis= (0,5), yAxis= (0,10)) + + def OnPlotDraw6(self, event): + #Bar Graph Example + self.resetDefaults() + #self.client.SetEnableLegend(True) #turn on Legend + #self.client.SetEnableGrid(True) #turn on Grid + self.client.SetXSpec('none') #turns off x-axis scale + self.client.SetYSpec('auto') + self.client.Draw(_draw6Objects(), xAxis= (0,7)) + + def OnPlotDraw7(self, event): + #log scale example + self.resetDefaults() + self.client.setLogScale((True,True)) + self.client.Draw(_draw7Objects()) + + def OnPlotRedraw(self,event): + self.client.Redraw() + + def OnPlotClear(self,event): + self.client.Clear() + + def OnPlotScale(self, event): + if self.client.last_draw != None: + graphics, xAxis, yAxis= self.client.last_draw + self.client.Draw(graphics,(1,3.05),(0,1)) + + def OnEnableZoom(self, event): + self.client.SetEnableZoom(event.IsChecked()) + self.mainmenu.Check(217, not event.IsChecked()) + + def OnEnableGrid(self, event): + self.client.SetEnableGrid(event.IsChecked()) + + def OnEnableDrag(self, event): + self.client.SetEnableDrag(event.IsChecked()) + self.mainmenu.Check(214, not event.IsChecked()) + + def OnEnableLegend(self, event): + self.client.SetEnableLegend(event.IsChecked()) + + def OnEnablePointLabel(self, event): + self.client.SetEnablePointLabel(event.IsChecked()) + + def OnEnableAntiAliasing(self, event): + self.client.SetEnableAntiAliasing(event.IsChecked()) + + def OnEnableHiRes(self, event): + self.client.SetEnableHiRes(event.IsChecked()) + + def OnEnableCenterLines(self, event): + self.client.SetEnableCenterLines(event.IsChecked()) + + def OnEnableDiagonals(self, event): + self.client.SetEnableDiagonals(event.IsChecked()) + + def OnBackgroundGray(self, event): + self.client.SetBackgroundColour("#CCCCCC") + self.client.Redraw() + + def OnBackgroundWhite(self, event): + self.client.SetBackgroundColour("white") + self.client.Redraw() + + def OnForegroundRed(self, event): + self.client.SetForegroundColour("red") + self.client.Redraw() + + def OnForegroundBlack(self, event): + self.client.SetForegroundColour("black") + self.client.Redraw() + + def OnScrUp(self, event): + self.client.ScrollUp(1) + + def OnScrRt(self,event): + self.client.ScrollRight(2) + + def OnReset(self,event): + self.client.Reset() + + def OnHelpAbout(self, event): + from wx.lib.dialogs import ScrolledMessageDialog + about = ScrolledMessageDialog(self, __doc__, "About...") + about.ShowModal() + + def resetDefaults(self): + """Just to reset the fonts back to the PlotCanvas defaults""" + self.client.SetFont(wx.Font(10,wx.SWISS,wx.NORMAL,wx.NORMAL)) + self.client.SetFontSizeAxis(10) + self.client.SetFontSizeLegend(7) + self.client.setLogScale((False,False)) + self.client.SetXSpec('auto') + self.client.SetYSpec('auto') + + + +def __test(): + + class MyApp(wx.App): + def OnInit(self): + wx.InitAllImageHandlers() + frame = TestFrame(None, -1, "PlotCanvas") + #frame.Show(True) + self.SetTopWindow(frame) + return True + + + app = MyApp(0) + app.MainLoop() + +if __name__ == '__main__': + __test() diff --git a/wx/lib/popupctl.py b/wx/lib/popupctl.py new file mode 100644 index 00000000..9f70fd52 --- /dev/null +++ b/wx/lib/popupctl.py @@ -0,0 +1,249 @@ +#---------------------------------------------------------------------- +# Name: popup +# Purpose: Generic popup control +# +# Author: Gerrit van Dyk +# +# Created: 2002/11/20 +# Version: 0.1 +# RCS-ID: $Id$ +# License: wxWindows license +#---------------------------------------------------------------------- +# 11/24/2007 - Cody Precord +# +# o Use RendererNative to draw button +# +# 12/09/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o 2.5 compatability update. +# +# 12/20/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o wxPopupDialog -> PopupDialog +# o wxPopupControl -> PopupControl +# + +import wx +from wx.lib.buttons import GenButtonEvent + + +class PopButton(wx.PyControl): + def __init__(self,*_args,**_kwargs): + wx.PyControl.__init__(self, *_args, **_kwargs) + + self.up = True + self.didDown = False + + self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) + self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp) + self.Bind(wx.EVT_MOTION, self.OnMotion) + self.Bind(wx.EVT_PAINT, self.OnPaint) + + def Notify(self): + evt = GenButtonEvent(wx.wxEVT_COMMAND_BUTTON_CLICKED, self.GetId()) + evt.SetIsDown(not self.up) + evt.SetButtonObj(self) + evt.SetEventObject(self) + self.GetEventHandler().ProcessEvent(evt) + + def OnEraseBackground(self, event): + pass + + def OnLeftDown(self, event): + if not self.IsEnabled(): + return + self.didDown = True + self.up = False + self.CaptureMouse() + self.GetParent().textCtrl.SetFocus() + self.Refresh() + event.Skip() + + def OnLeftUp(self, event): + if not self.IsEnabled(): + return + if self.didDown: + self.ReleaseMouse() + if not self.up: + self.Notify() + self.up = True + self.Refresh() + self.didDown = False + event.Skip() + + def OnMotion(self, event): + if not self.IsEnabled(): + return + if event.LeftIsDown(): + if self.didDown: + x,y = event.GetPosition() + w,h = self.GetClientSize() + if self.up and x=0 and y=0: + self.up = False + self.Refresh() + return + if not self.up and (x<0 or y<0 or x>=w or y>=h): + self.up = True + self.Refresh() + return + event.Skip() + + def OnPaint(self, event): + dc = wx.BufferedPaintDC(self) + if self.up: + flag = wx.CONTROL_CURRENT + else: + flag = wx.CONTROL_PRESSED + wx.RendererNative.Get().DrawComboBoxDropButton(self, dc, self.GetClientRect(), flag) + + +#--------------------------------------------------------------------------- + + +# Tried to use wxPopupWindow but the control misbehaves on MSW +class PopupDialog(wx.Dialog): + def __init__(self,parent,content = None): + wx.Dialog.__init__(self,parent,-1,'', style = wx.BORDER_SIMPLE|wx.STAY_ON_TOP) + + self.ctrl = parent + self.win = wx.Window(self,-1,pos = (0,0),style = 0) + + if content: + self.SetContent(content) + + def SetContent(self,content): + self.content = content + self.content.Reparent(self.win) + self.content.Show(True) + self.win.SetClientSize(self.content.GetSize()) + self.SetSize(self.win.GetSize()) + + def Display(self): + pos = self.ctrl.ClientToScreen( (0,0) ) + dSize = wx.GetDisplaySize() + selfSize = self.GetSize() + tcSize = self.ctrl.GetSize() + + pos.x -= (selfSize.width - tcSize.width) / 2 + if pos.x + selfSize.width > dSize.width: + pos.x = dSize.width - selfSize.width + if pos.x < 0: + pos.x = 0 + + pos.y += tcSize.height + if pos.y + selfSize.height > dSize.height: + pos.y = dSize.height - selfSize.height + if pos.y < 0: + pos.y = 0 + + self.Move(pos) + + self.ctrl.FormatContent() + + self.ShowModal() + + +#--------------------------------------------------------------------------- + + +class PopupControl(wx.PyControl): + def __init__(self,*_args,**_kwargs): + if _kwargs.has_key('value'): + del _kwargs['value'] + style = _kwargs.get('style', 0) + if (style & wx.BORDER_MASK) == 0: + style |= wx.BORDER_NONE + _kwargs['style'] = style + wx.PyControl.__init__(self, *_args, **_kwargs) + + self.textCtrl = wx.TextCtrl(self, wx.ID_ANY, '', pos = (0,0)) + self.bCtrl = PopButton(self, wx.ID_ANY, style=wx.BORDER_NONE) + self.pop = None + self.content = None + + self.Bind(wx.EVT_SIZE, self.OnSize) + self.bCtrl.Bind(wx.EVT_BUTTON, self.OnButton, self.bCtrl) + self.Bind(wx.EVT_SET_FOCUS, self.OnFocus) + + self.SetInitialSize(_kwargs.get('size', wx.DefaultSize)) + self.SendSizeEvent() + + + def OnFocus(self,evt): + # embedded control should get focus on TAB keypress + self.textCtrl.SetFocus() + evt.Skip() + + + def OnSize(self, evt): + # layout the child widgets + w,h = self.GetClientSize() + self.textCtrl.SetDimensions(0, 0, w - self.marginWidth - self.buttonWidth, h) + self.bCtrl.SetDimensions(w - self.buttonWidth, 0, self.buttonWidth, h) + + def DoGetBestSize(self): + # calculate the best size of the combined control based on the + # needs of the child widgets. + tbs = self.textCtrl.GetBestSize() + return wx.Size(tbs.width + self.marginWidth + self.buttonWidth, + tbs.height) + + + def OnButton(self, evt): + if not self.pop: + if self.content: + self.pop = PopupDialog(self,self.content) + del self.content + else: + print 'No Content to pop' + if self.pop: + self.pop.Display() + + + def Enable(self, flag): + wx.PyControl.Enable(self,flag) + self.textCtrl.Enable(flag) + self.bCtrl.Enable(flag) + + + def SetPopupContent(self, content): + if not self.pop: + self.content = content + self.content.Show(False) + else: + self.pop.SetContent(content) + + def FormatContent(self): + pass + + def PopDown(self): + if self.pop: + self.pop.EndModal(1) + + def SetValue(self, value): + self.textCtrl.SetValue(value) + + def GetValue(self): + return self.textCtrl.GetValue() + + def SetFont(self, font): + self.textCtrl.SetFont(font) + + def GetFont(self): + return self.textCtrl.GetFont() + + + def _get_marginWidth(self): + if 'wxMac' in wx.PlatformInfo: + return 6 + else: + return 3 + marginWidth = property(_get_marginWidth) + + def _get_buttonWidth(self): + return 20 + buttonWidth = property(_get_buttonWidth) + + +# an alias +PopupCtrl = PopupControl diff --git a/wx/lib/printout.py b/wx/lib/printout.py new file mode 100644 index 00000000..93a3b987 --- /dev/null +++ b/wx/lib/printout.py @@ -0,0 +1,1156 @@ +#---------------------------------------------------------------------------- +# Name: printout.py +# Purpose: preview and printing class -> table/grid printing +# +# Author: Lorne White (email: lorne.white@telusplanet.net) +# +# Created: +# Version 0.75 +# Date: May 15, 2002 +# Licence: wxWindows license +#---------------------------------------------------------------------------- +# Release Notes + +# fixed bug for string wider than print region +# add index to data list after parsing total pages for paging +#---------------------------------------------------------------------------- +# 12/10/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# o 2.5 compatability update. +#---------------------------------------------------------------------------- +# 11/23/2004 - Vernon Cole (wnvcole@peppermillcas.com) +# o Generalize for non-2-dimensional sequences and non-text data +# (can use as a simple text printer by supplying a list of strings.) +# o Add a small _main_ for self test + +import copy +import types +import wx + +class PrintBase(object): + def SetPrintFont(self, font): # set the DC font parameters + fattr = font["Attr"] + if fattr[0] == 1: + weight = wx.BOLD + else: + weight = wx.NORMAL + + if fattr[1] == 1: + set_style = wx.ITALIC + else: + set_style = wx.NORMAL + + underline = fattr[2] + fcolour = self.GetFontColour(font) + self.DC.SetTextForeground(fcolour) + + setfont = wx.Font(font["Size"], wx.SWISS, set_style, weight, underline) + setfont.SetFaceName(font["Name"]) + self.DC.SetFont(setfont) + + def GetFontColour(self, font): + fcolour = font["Colour"] + return wx.Colour(fcolour[0], fcolour[1], fcolour[2]) + + def OutTextRegion(self, textout, txtdraw = True): + textlines = textout.split('\n') + y = copy.copy(self.y) + self.pt_space_before + for text in textlines: + remain = 'X' + while remain != "": + vout, remain = self.SetFlow(text, self.region) + if self.draw == True and txtdraw == True: + test_out = self.TestFull(vout) + if self.align == wx.ALIGN_LEFT: + self.DC.DrawText(test_out, self.indent+self.pcell_left_margin, y) + + elif self.align == wx.ALIGN_CENTRE: + diff = self.GetCellDiff(test_out, self.region) + self.DC.DrawText(test_out, self.indent+diff/2, y) + + elif self.align == wx.ALIGN_RIGHT: + diff = self.GetCellDiff(test_out, self.region) + self.DC.DrawText(test_out, self.indent+diff, y) + + else: + self.DC.DrawText(test_out, self.indent+self.pcell_left_margin, y) + text = remain + y = y + self.space + return y - self.space + self.pt_space_after + + def GetCellDiff(self, text, width): # get the remaining cell size for adjustment + w, h = self.DC.GetTextExtent(text) + diff = width - w + if diff < 0: + diff = 0 + return diff + + def TestFull(self, text_test): + w, h = self.DC.GetTextExtent(text_test) + if w > self.region: # trouble fitting into cell + return self.SetChar(text_test, self.region) # fit the text to the cell size + else: + return text_test + + def SetFlow(self, ln_text, width): + width = width - self.pcell_right_margin + text = "" + split = ln_text.split() + if len(split) == 1: + return ln_text, "" + + try: + w, h = self.DC.GetTextExtent(" " + split[0]) + if w >= width: + return ln_text, "" + except: + pass + + cnt = 0 + for word in split: + bword = " " + word # blank + word + length = len(bword) + + w, h = self.DC.GetTextExtent(text + bword) + if w < width: + text = text + bword + cnt = cnt + 1 + else: + remain = ' '.join(split[cnt:]) + text = text.strip() + return text, remain + + remain = ' '.join(split[cnt:]) + vout = text.strip() + return vout, remain + + def SetChar(self, ln_text, width): # truncate string to fit into width + width = width - self.pcell_right_margin - self.pcell_left_margin + text = "" + for val in ln_text: + w, h = self.DC.GetTextExtent(text + val) + if w > width: + text = text + ".." + return text # fitted text value + text = text + val + return text + + def OutTextPageWidth(self, textout, y_out, align, indent, txtdraw = True): + textlines = textout.split('\n') + y = copy.copy(y_out) + + pagew = self.parent.page_width * self.pwidth # full page width + w, h = self.DC.GetTextExtent(textout) + y_line = h + + for text in textlines: + remain = 'X' + while remain != "": + vout, remain = self.SetFlow(text, pagew) + if self.draw == True and txtdraw == True: + test_out = vout + if align == wx.ALIGN_LEFT: + self.DC.DrawText(test_out, indent, y) + + elif align == wx.ALIGN_CENTRE: + diff = self.GetCellDiff(test_out, pagew) + self.DC.DrawText(test_out, indent+diff/2, y) + + elif align == wx.ALIGN_RIGHT: + diff = self.GetCellDiff(test_out, pagew) + self.DC.DrawText(test_out, indent+diff, y) + + else: + self.DC.DrawText(test_out, indent, y_out) + text = remain + y = y + y_line + return y - y_line + + def GetDate(self): + date, time = self.GetNow() + return date + + def GetDateTime(self): + date, time = self.GetNow() + return date + ' ' + time + + def GetNow(self): + now = wx.DateTime.Now() + date = now.FormatDate() + time = now.FormatTime() + return date, time + + def SetPreview(self, preview): + self.preview = preview + + def SetPSize(self, width, height): + self.pwidth = width/self.scale + self.pheight = height/self.scale + + def SetScale(self, scale): + self.scale = scale + + def SetPTSize(self, width, height): + self.ptwidth = width + self.ptheight = height + + def getWidth(self): + return self.sizew + + def getHeight(self): + return self.sizeh + + +class PrintTableDraw(wx.ScrolledWindow, PrintBase): + def __init__(self, parent, DC, size): + self.parent = parent + self.DC = DC + self.scale = parent.scale + self.width = size[0] + self.height = size[1] + self.SetDefaults() + + def SetDefaults(self): + self.page = 1 + self.total_pages = None + + self.page_width = self.parent.page_width + self.page_height = self.parent.page_height + + self.left_margin = self.parent.left_margin + self.right_margin = self.parent.right_margin + + self.top_margin = self.parent.top_margin + self.bottom_margin = self.parent.bottom_margin + self.cell_left_margin = self.parent.cell_left_margin + self.cell_right_margin = self.parent.cell_right_margin + + self.label_colour = self.parent.label_colour + + self.row_line_colour = self.parent.row_line_colour + self.row_line_size = self.parent.row_line_size + + self.row_def_line_colour = self.parent.row_def_line_colour + self.row_def_line_size = self.parent.row_def_line_size + + self.column_line_colour = self.parent.column_line_colour + self.column_line_size = self.parent.column_line_size + + self.column_def_line_size = self.parent.column_def_line_size + self.column_def_line_colour = self.parent.column_def_line_colour + + self.text_font = self.parent.text_font + + self.label_font = self.parent.label_font + + def AdjustValues(self): + self.vertical_offset = self.pheight * self.parent.vertical_offset + self.horizontal_offset = self.pheight * self.parent.horizontal_offset + + self.pcell_left_margin = self.pwidth * self.cell_left_margin + self.pcell_right_margin = self.pwidth * self.cell_right_margin + self.ptop_margin = self.pheight * self.top_margin + self.pbottom_margin = self.pheight * self.bottom_margin + + self.pheader_margin = self.pheight * self.parent.header_margin + self.pfooter_margin = self.pheight * self.parent.footer_margin + + self.cell_colour = self.parent.set_cell_colour + self.cell_text = self.parent.set_cell_text + + self.column = [] + self.column_align = [] + self.column_bgcolour = [] + self.column_txtcolour = [] + + set_column_align = self.parent.set_column_align + set_column_bgcolour = self.parent.set_column_bgcolour + set_column_txtcolour = self.parent.set_column_txtcolour + + pos_x = self.left_margin * self.pwidth + self.horizontal_offset # left margin + self.column.append(pos_x) + + #module logic expects two dimensional data -- fix input if needed + if isinstance(self.data,types.StringTypes): + self.data = [[copy.copy(self.data)]] # a string becomes a single cell + try: + rows = len(self.data) + except TypeError: + self.data = [[str(self.data)]] # a non-iterable becomes a single cell + rows = 1 + first_value = self.data[0] + + if isinstance(first_value, types.StringTypes): # a sequence of strings + if self.label == [] and self.set_column == []: + data = [] + for x in self.data: #becomes one column + data.append([x]) + else: + data = [self.data] #becames one row + self.data = data + first_value = data[0] + try: + column_total = len(first_value) + except TypeError: # a sequence of non-iterables + if self.label == [] and self.set_column == []: + data = [] #becomes one column + for x in self.data: + data.append([str(x)]) + column_total = 1 + else: + data = [self.data] #becomes one row + column_total = len(self.data) + self.data = data + first_value = data[0] + + if self.set_column == []: + table_width = self.page_width - self.left_margin - self.right_margin + if self.label == []: + temp = first_value + else: + temp = self.label + width = table_width/(len(temp)) + for val in temp: + column_width = width * self.pwidth + pos_x = pos_x + column_width + self.column.append(pos_x) # position of each column + else: + for val in self.set_column: + column_width = val * self.pwidth + pos_x = pos_x + column_width + self.column.append(pos_x) # position of each column + + if pos_x > self.page_width * self.pwidth: # check if it fits in page + print "Warning, Too Wide for Page" + return + + if self.label != []: + if len(self.column) -1 != len(self.label): + print "Column Settings Incorrect", "\nColumn Value: " + str(self.column), "\nLabel Value: " + str(self.label) + return + + if column_total != len(self.column) -1: + print "Cannot fit", first_value, 'in', len(self.column)-1, 'columns.' + return + + for col in range(column_total): + try: + align = set_column_align[col] # check if custom column alignment + except: + align = wx.ALIGN_LEFT + self.column_align.append(align) + + try: + colour = set_column_bgcolour[col] # check if custom column background colour + except: + colour = self.parent.column_colour + self.column_bgcolour.append(colour) + + try: + colour = set_column_txtcolour[col] # check if custom column text colour + except: + colour = self.GetFontColour(self.parent.text_font) + self.column_txtcolour.append(colour) + + + def SetPointAdjust(self): + f = wx.Font(10, wx.SWISS, wx.NORMAL, wx.NORMAL) # setup using 10 point + self.DC.SetFont(f) + f.SetFaceName(self.text_font["Name"]) + x, y = self.DC.GetTextExtent("W") + + self.label_pt_space_before = self.parent.label_pt_adj_before * y/10 # extra spacing for label per point value + self.label_pt_space_after = self.parent.label_pt_adj_after * y/10 + + self.text_pt_space_before = self.parent.text_pt_adj_before * y/10 # extra spacing for row text per point value + self.text_pt_space_after = self.parent.text_pt_adj_after * y/10 + + def SetPage(self, page): + self.page = page + + def SetColumns(self, col): + self.column = col + + def OutCanvas(self): + self.AdjustValues() + self.SetPointAdjust() + + self.y_start = self.ptop_margin + self.vertical_offset + self.y_end = self.parent.page_height * self.pheight - self.pbottom_margin + self.vertical_offset + + self.SetPrintFont(self.label_font) + + x, y = self.DC.GetTextExtent("W") + self.label_space = y + + self.SetPrintFont(self.text_font) + + x, y = self.DC.GetTextExtent("W") + self.space = y + + if self.total_pages is None: + self.GetTotalPages() # total pages for display/printing + + self.data_cnt = self.page_index[self.page-1] + + self.draw = True + self.PrintHeader() + self.PrintFooter() + self.OutPage() + + def GetTotalPages(self): + self.data_cnt = 0 + self.draw = False + self.page_index = [0] + + cnt = 0 + while 1: + test = self.OutPage() + self.page_index.append(self.data_cnt) + if test == False: + break + cnt = cnt + 1 + + self.total_pages = cnt + 1 + + def OutPage(self): + self.y = self.y_start + self.end_x = self.column[-1] + + if self.data_cnt < len(self.data): # if there data for display on the page + if self.label != []: # check if header defined + self.PrintLabel() + else: + return False + + for val in self.data: + try: + row_val = self.data[self.data_cnt] + except: + self.FinishDraw() + return False + + max_y = self.PrintRow(row_val, False) # test to see if row will fit in remaining space + test = max_y + self.space + + if test > self.y_end: + break + + self.ColourRowCells(max_y-self.y+self.space) # colour the row/column + max_y = self.PrintRow(row_val, True) # row fits - print text + self.DrawGridLine() # top line of cell + self.y = max_y + self.space + + if self.y > self.y_end: + break + + self.data_cnt = self.data_cnt + 1 + + self.FinishDraw() + + if self.data_cnt == len(self.data): # last value in list + return False + + return True + + + def PrintLabel(self): + self.pt_space_before = self.label_pt_space_before # set the point spacing + self.pt_space_after = self.label_pt_space_after + + self.LabelColorRow(self.label_colour) + self.SetPrintFont(self.label_font) + + self.col = 0 + max_y = 0 + for vtxt in self.label: + self.region = self.column[self.col+1] - self.column[self.col] + self.indent = self.column[self.col] + + self.align = wx.ALIGN_LEFT + + max_out = self.OutTextRegion(vtxt, True) + if max_out > max_y: + max_y = max_out + self.col = self.col + 1 + + self.DrawGridLine() # top line of label + self.y = max_y + self.label_space + + def PrintHeader(self): # print the header array + if self.draw == False: + return + + for val in self.parent.header: + self.SetPrintFont(val["Font"]) + + header_indent = val["Indent"] * self.pwidth + text = val["Text"] + + htype = val["Type"] + if htype == "Date": + addtext = self.GetDate() + elif htype == "Date & Time": + addtext = self.GetDateTime() + else: + addtext = "" + + self.OutTextPageWidth(text+addtext, self.pheader_margin, val["Align"], header_indent, True) + + def PrintFooter(self): # print the header array + if self.draw == False: + return + + footer_pos = self.parent.page_height * self.pheight - self.pfooter_margin + self.vertical_offset + for val in self.parent.footer: + self.SetPrintFont(val["Font"]) + + footer_indent = val["Indent"] * self.pwidth + text = val["Text"] + + ftype = val["Type"] + if ftype == "Pageof": + addtext = "Page " + str(self.page) + " of " + str(self.total_pages) + elif ftype == "Page": + addtext = "Page " + str(self.page) + elif ftype == "Num": + addtext = str(self.page) + elif ftype == "Date": + addtext = self.GetDate() + elif ftype == "Date & Time": + addtext = self.GetDateTime() + else: + addtext = "" + + self.OutTextPageWidth(text+addtext, footer_pos, val["Align"], footer_indent, True) + + + def LabelColorRow(self, colour): + brush = wx.Brush(colour, wx.SOLID) + self.DC.SetBrush(brush) + height = self.label_space + self.label_pt_space_before + self.label_pt_space_after + self.DC.DrawRectangle(self.column[0], self.y, + self.end_x-self.column[0]+1, height) + + def ColourRowCells(self, height): + if self.draw == False: + return + + col = 0 + for colour in self.column_bgcolour: + cellcolour = self.GetCellColour(self.data_cnt, col) + if cellcolour is not None: + colour = cellcolour + + brush = wx.Brush(colour, wx.SOLID) + self.DC.SetBrush(brush) + self.DC.SetPen(wx.Pen(wx.NamedColour('WHITE'), 0)) + + start_x = self.column[col] + width = self.column[col+1] - start_x + 2 + self.DC.DrawRectangle(start_x, self.y, width, height) + col = col + 1 + + def PrintRow(self, row_val, draw = True, align = wx.ALIGN_LEFT): + self.SetPrintFont(self.text_font) + + self.pt_space_before = self.text_pt_space_before # set the point spacing + self.pt_space_after = self.text_pt_space_after + + self.col = 0 + max_y = 0 + for vtxt in row_val: + if not isinstance(vtxt,types.StringTypes): + vtxt = str(vtxt) + self.region = self.column[self.col+1] - self.column[self.col] + self.indent = self.column[self.col] + self.align = self.column_align[self.col] + + fcolour = self.column_txtcolour[self.col] # set font colour + celltext = self.GetCellTextColour(self.data_cnt, self.col) + if celltext is not None: + fcolour = celltext # override the column colour + + self.DC.SetTextForeground(fcolour) + + max_out = self.OutTextRegion(vtxt, draw) + if max_out > max_y: + max_y = max_out + self.col = self.col + 1 + return max_y + + def GetCellColour(self, row, col): # check if custom colour defined for the cell background + try: + set = self.cell_colour[row] + except: + return None + try: + colour = set[col] + return colour + except: + return None + + def GetCellTextColour(self, row, col): # check if custom colour defined for the cell text + try: + set = self.cell_text[row] + except: + return None + try: + colour = set[col] + return colour + except: + return None + + def FinishDraw(self): + self.DrawGridLine() # draw last row line + self.DrawColumns() # draw all vertical lines + + def DrawGridLine(self): + if self.draw == True \ + and len(self.column) > 2: #supress grid lines if only one column + try: + size = self.row_line_size[self.data_cnt] + except: + size = self.row_def_line_size + + if size < 1: return + + try: + colour = self.row_line_colour[self.data_cnt] + except: + colour = self.row_def_line_colour + + self.DC.SetPen(wx.Pen(colour, size)) + + y_out = self.y +# y_out = self.y + self.pt_space_before + self.pt_space_after # adjust for extra spacing + self.DC.DrawLine(self.column[0], y_out, self.end_x, y_out) + + def DrawColumns(self): + if self.draw == True \ + and len(self.column) > 2: #surpress grid line if only one column + col = 0 + for val in self.column: + try: + size = self.column_line_size[col] + except: + size = self.column_def_line_size + + if size < 1: continue + + try: + colour = self.column_line_colour[col] + except: + colour = self.column_def_line_colour + + indent = val + + self.DC.SetPen(wx.Pen(colour, size)) + self.DC.DrawLine(indent, self.y_start, indent, self.y) + col = col + 1 + + def DrawText(self): + self.DoRefresh() + + def DoDrawing(self, DC): + size = DC.GetSize() + self.DC = DC + + DC.BeginDrawing() + self.DrawText() + DC.EndDrawing() + + self.sizew = DC.MaxY() + self.sizeh = DC.MaxX() + + +class PrintTable(object): + def __init__(self, parentFrame=None): + self.data = [] + self.set_column = [] + self.label = [] + self.header = [] + self.footer = [] + + self.set_column_align = {} + self.set_column_bgcolour = {} + self.set_column_txtcolour = {} + self.set_cell_colour = {} + self.set_cell_text = {} + self.column_line_size = {} + self.column_line_colour = {} + self.row_line_size = {} + self.row_line_colour = {} + + self.parentFrame = parentFrame + self.SetPreviewSize() + + self.printData = wx.PrintData() + self.scale = 1.0 + + self.SetParms() + self.SetColors() + self.SetFonts() + self.TextSpacing() + + self.SetPrinterOffset() + self.SetHeaderValue() + self.SetFooterValue() + self.SetMargins() + self.SetPortrait() + + def SetPreviewSize(self, position = wx.Point(0, 0), size="Full"): + if size == "Full": + r = wx.GetClientDisplayRect() + self.preview_frame_size = r.GetSize() + self.preview_frame_pos = r.GetPosition() + else: + self.preview_frame_size = size + self.preview_frame_pos = position + + def SetPaperId(self, paper): + self.printData.SetPaperId(paper) + + def SetOrientation(self, orient): + self.printData.SetOrientation(orient) + + def SetColors(self): + self.row_def_line_colour = wx.NamedColour('BLACK') + self.row_def_line_size = 1 + + self.column_def_line_colour = wx.NamedColour('BLACK') + self.column_def_line_size = 1 + self.column_colour = wx.NamedColour('WHITE') + + self.label_colour = wx.NamedColour('LIGHT GREY') + + def SetFonts(self): + self.label_font = { "Name": self.default_font_name, "Size": 12, "Colour": [0, 0, 0], "Attr": [0, 0, 0] } + self.text_font = { "Name": self.default_font_name, "Size": 10, "Colour": [0, 0, 0], "Attr": [0, 0, 0] } + + def TextSpacing(self): + self.label_pt_adj_before = 0 # point adjustment before and after the label text + self.label_pt_adj_after = 0 + + self.text_pt_adj_before = 0 # point adjustment before and after the row text + self.text_pt_adj_after = 0 + + def SetLabelSpacing(self, before, after): # method to set the label space adjustment + self.label_pt_adj_before = before + self.label_pt_adj_after = after + + def SetRowSpacing(self, before, after): # method to set the row space adjustment + self.text_pt_adj_before = before + self.text_pt_adj_after = after + + def SetPrinterOffset(self): # offset to adjust for printer + self.vertical_offset = -0.1 + self.horizontal_offset = -0.1 + + def SetHeaderValue(self): + self.header_margin = 0.25 + self.header_font = { "Name": self.default_font_name, "Size": 11, "Colour": [0, 0, 0], "Attr": [0, 0, 0] } + self.header_align = wx.ALIGN_CENTRE + self.header_indent = 0 + self.header_type = "Text" + + def SetFooterValue(self): + self.footer_margin = 0.7 + self.footer_font = { "Name": self.default_font_name, "Size": 11, "Colour": [0, 0, 0], "Attr": [0, 0, 0] } + self.footer_align = wx.ALIGN_CENTRE + self.footer_indent = 0 + self.footer_type = "Pageof" + + def SetMargins(self): + self.left_margin = 0.5 + self.right_margin = 0.5 # only used if no column sizes + + self.top_margin = 0.8 + self.bottom_margin = 1.0 + self.cell_left_margin = 0.1 + self.cell_right_margin = 0.1 + + def SetPortrait(self): + self.printData.SetPaperId(wx.PAPER_LETTER) + self.printData.SetOrientation(wx.PORTRAIT) + self.page_width = 8.5 + self.page_height = 11.0 + + def SetLandscape(self): + self.printData.SetOrientation(wx.LANDSCAPE) + self.page_width = 11.0 + self.page_height = 8.5 + + def SetParms(self): + self.ymax = 1 + self.xmax = 1 + self.page = 1 + self.total_pg = 100 + + self.preview = None + self.page = 0 + + self.default_font_name = "Arial" + self.default_font = { "Name": self.default_font_name, "Size": 10, "Colour": [0, 0, 0], "Attr": [0, 0, 0] } + + def SetColAlignment(self, col, align=wx.ALIGN_LEFT): + self.set_column_align[col] = align + + def SetColBackgroundColour(self, col, colour): + self.set_column_bgcolour[col] = colour + + def SetColTextColour(self, col, colour): + self.set_column_txtcolour[col] = colour + + def SetCellColour(self, row, col, colour): # cell background colour + try: + set = self.set_cell_colour[row] # test if row already exists + try: + set[col] = colour # test if column already exists + except: + set = { col: colour } # create the column value + except: + set = { col: colour } # create the column value + + self.set_cell_colour[row] = set # create dictionary item for colour settings + + def SetCellText(self, row, col, colour): # font colour for custom cells + try: + set = self.set_cell_text[row] # test if row already exists + try: + set[col] = colour # test if column already exists + except: + set = { col: colour } # create the column value + except: + set = { col: colour } # create the column value + + self.set_cell_text[row] = set # create dictionary item for colour settings + + def SetColumnLineSize(self, col, size): # column line size + self.column_line_size[col] = size # create dictionary item for column line settings + + def SetColumnLineColour(self, col, colour): + self.column_line_colour[col] = colour + + def SetRowLineSize(self, row, size): + self.row_line_size[row] = size + + def SetRowLineColour(self, row, colour): + self.row_line_colour[row] = colour + + def GetColour(self, colour): # returns colours based from wxColour value + red = colour.Red() + blue = colour.Blue() + green = colour.Green() + return [red, green, blue ] + + def SetHeader(self, text = "", type = "Text", font=None, align = None, indent = None, colour = None, size = None): + set = { "Text": text } + + if font is None: + set["Font"] = copy.copy(self.default_font) + else: + set["Font"] = font + + if colour is not None: + setfont = set["Font"] + setfont["Colour"] = self.GetColour(colour) + + if size is not None: + setfont = set["Font"] + setfont["Size"] = size + + if align is None: + set["Align"] = self.header_align + else: + set["Align"] = align + + if indent is None: + set["Indent"] = self.header_indent + else: + set["Indent"] = indent + + if type is None: + set["Type"] = self.header_type + else: + set["Type"] = type + + self.header.append(set) + + def SetFooter(self, text = "", type = None, font=None, align = None, indent = None, colour = None, size = None): + set = { "Text": text } + + if font is None: + set["Font"] = copy.copy(self.default_font) + else: + set["Font"] = font + + if colour is not None: + setfont = set["Font"] + setfont["Colour"] = self.GetColour(colour) + + if size is not None: + setfont = set["Font"] + setfont["Size"] = size + + if align is None: + set["Align"] = self.footer_align + else: + set["Align"] = align + + if indent is None: + set["Indent"] = self.footer_indent + else: + set["Indent"] = indent + + if type is None: + set["Type"] = self.footer_type + else: + set["Type"] = type + + self.footer.append(set) + + def Preview(self): + data = wx.PrintDialogData(self.printData) + printout = SetPrintout(self) + printout2 = SetPrintout(self) + self.preview = wx.PrintPreview(printout, printout2, data) + if not self.preview.Ok(): + wx.MessageBox("There was a problem printing!", "Printing", wx.OK) + return + + self.preview.SetZoom(60) # initial zoom value + frame = wx.PreviewFrame(self.preview, self.parentFrame, "Print preview") + + frame.Initialize() + if self.parentFrame: + frame.SetPosition(self.preview_frame_pos) + frame.SetSize(self.preview_frame_size) + frame.Show(True) + + def Print(self): + pdd = wx.PrintDialogData(self.printData) + printer = wx.Printer(pdd) + printout = SetPrintout(self) + if not printer.Print(self.parentFrame, printout): + if wx.Printer.GetLastError() == wx.PRINTER_ERROR: + wx.MessageBox("There was a problem printing.\n" + "Perhaps your current printer is not set correctly?", + "Printing", wx.OK) + else: + self.printData = wx.PrintData( printer.GetPrintDialogData().GetPrintData() ) + printout.Destroy() + + def DoDrawing(self, DC): + size = DC.GetSize() + DC.BeginDrawing() + + table = PrintTableDraw(self, DC, size) + table.data = self.data + table.set_column = self.set_column + table.label = self.label + table.SetPage(self.page) + + if self.preview is None: + table.SetPSize(size[0]/self.page_width, size[1]/self.page_height) + table.SetPTSize(size[0], size[1]) + table.SetPreview(False) + else: + if self.preview == 1: + table.scale = self.scale + table.SetPSize(size[0]/self.page_width, size[1]/self.page_height) + else: + table.SetPSize(self.pwidth, self.pheight) + + table.SetPTSize(self.ptwidth, self.ptheight) + table.SetPreview(self.preview) + + table.OutCanvas() + self.page_total = table.total_pages # total display pages + + DC.EndDrawing() + + self.ymax = DC.MaxY() + self.xmax = DC.MaxX() + + self.sizeh = size[0] + self.sizew = size[1] + + def GetTotalPages(self): + self.page_total = 100 + return self.page_total + + def HasPage(self, page): + if page <= self.page_total: + return True + else: + return False + + def SetPage(self, page): + self.page = page + + def SetPageSize(self, width, height): + self.pwidth, self.pheight = width, height + + def SetTotalSize(self, width, height): + self.ptwidth, self.ptheight = width, height + + def SetPreview(self, preview, scale): + self.preview = preview + self.scale = scale + + def SetTotalSize(self, width, height): + self.ptwidth = width + self.ptheight = height + +class PrintGrid(object): + def __init__(self, parent, grid, format = [], total_col = None, total_row = None): + if total_row is None: + total_row = grid.GetNumberRows() + if total_col is None: + total_col = grid.GetNumberCols() + + self.total_row = total_row + self.total_col = total_col + self.grid = grid + + data = [] + for row in range(total_row): + row_val = [] + value = grid.GetRowLabelValue(row) + row_val.append(value) + + for col in range(total_col): + value = grid.GetCellValue(row, col) + row_val.append(value) + data.append(row_val) + + label = [""] + for col in range(total_col): + value = grid.GetColLabelValue(col) + label.append(value) + + self.table = PrintTable(parent) + self.table.cell_left_margin = 0.0 + self.table.cell_right_margin = 0.0 + + self.table.label = label + self.table.set_column = format + self.table.data = data + + def GetTable(self): + return self.table + + def SetAttributes(self): + for row in range(self.total_row): + for col in range(self.total_col): + colour = self.grid.GetCellTextColour(row, col-1) + self.table.SetCellText(row, col, colour) + + colour = self.grid.GetCellBackgroundColour(row, col-1) + self.table.SetCellColour(row, col, colour) + + def Preview(self): + self.table.Preview() + + def Print(self): + self.table.Print() + + +class SetPrintout(wx.Printout): + def __init__(self, canvas): + wx.Printout.__init__(self) + self.canvas = canvas + self.end_pg = 1000 + + def OnBeginDocument(self, start, end): + return super(SetPrintout, self).OnBeginDocument(start, end) + + def OnEndDocument(self): + super(SetPrintout, self).OnEndDocument() + + def HasPage(self, page): + try: + end = self.canvas.HasPage(page) + return end + except: + return True + + def GetPageInfo(self): + try: + self.end_pg = self.canvas.GetTotalPages() + except: + pass + + end_pg = self.end_pg + str_pg = 1 + return (str_pg, end_pg, str_pg, end_pg) + + def OnPreparePrinting(self): + super(SetPrintout, self).OnPreparePrinting() + + def OnBeginPrinting(self): + dc = self.GetDC() + + self.preview = self.IsPreview() + if (self.preview): + self.pixelsPerInch = self.GetPPIScreen() + else: + self.pixelsPerInch = self.GetPPIPrinter() + + (w, h) = dc.GetSize() + scaleX = float(w) / 1000 + scaleY = float(h) / 1000 + self.printUserScale = min(scaleX, scaleY) + + super(SetPrintout, self).OnBeginPrinting() + + def GetSize(self): + self.psizew, self.psizeh = self.GetPPIPrinter() + return self.psizew, self.psizeh + + def GetTotalSize(self): + self.ptsizew, self.ptsizeh = self.GetPageSizePixels() + return self.ptsizew, self.ptsizeh + + def OnPrintPage(self, page): + dc = self.GetDC() + (w, h) = dc.GetSize() + scaleX = float(w) / 1000 + scaleY = float(h) / 1000 + self.printUserScale = min(scaleX, scaleY) + dc.SetUserScale(self.printUserScale, self.printUserScale) + + self.preview = self.IsPreview() + + self.canvas.SetPreview(self.preview, self.printUserScale) + self.canvas.SetPage(page) + + self.ptsizew, self.ptsizeh = self.GetPageSizePixels() + self.canvas.SetTotalSize(self.ptsizew, self.ptsizeh) + + self.psizew, self.psizeh = self.GetPPIPrinter() + self.canvas.SetPageSize(self.psizew, self.psizeh) + + self.canvas.DoDrawing(dc) + return True + +if __name__ == '__main__': + app = wx.PySimpleApp() + frame = wx.Frame(None, -1, "Dummy wx frame for testing printout.py") + frame.Show(True) + ptbl = PrintTable(frame) + ptbl.SetHeader('This is the test HEADER') + # a single sequence will print out as a single column with no borders ... + ptbl.data = ( + 'This is the first line of text.', + 'This is the second line\nand the third. The fourth will be the number "4.0".', + 04.00, + 'This is the fifth line, but by design it is too long to fit in the width of a standard'\ + ' page, so it will be forced to wrap around in order to fit without having '\ + 'some of its verbose verbage truncated.', + 'Here we have the final line.' + ) + #... but, if labels or columns are defined, a single sequence will print out as a single row + ##ptbl.label = ('One','Two','Three','Four','5') + ptbl.Preview() + app.MainLoop() diff --git a/wx/lib/progressindicator.py b/wx/lib/progressindicator.py new file mode 100644 index 00000000..c3373688 --- /dev/null +++ b/wx/lib/progressindicator.py @@ -0,0 +1,152 @@ +#---------------------------------------------------------------------- +# Name: wx.lib.progressindicator +# Purpose: +# +# Author: Robin Dunn +# +# Created: 22-Oct-2009 +# RCS-ID: $Id: $ +# Copyright: (c) 2009 by Total Control Software +# Licence: wxWindows license +#---------------------------------------------------------------------- + +""" +A simple gauge with a label that is suitable for use in places like a +status bar. It can be used in either an automatic indeterminant +(pulse) mode or in determinante mode where the programmer will need to +update the position of the progress bar. The indicator can be set to +hide itself when it is not active. +""" + +import wx + +#---------------------------------------------------------------------- +# Supported Styles + +PI_PULSEMODE = 1 +PI_HIDEINACTIVE = 2 + +#---------------------------------------------------------------------- + +class ProgressIndicator(wx.Panel): + def __init__(self, *args, **kw): + wx.Panel.__init__(self, *args, **kw) + + self.label = wx.StaticText(self) + self.gauge = wx.Gauge(self, range=100, + style=wx.GA_HORIZONTAL|wx.GA_SMOOTH) + + self._startCount = 0 + self.Sizer = wx.BoxSizer(wx.HORIZONTAL) + self.Sizer.Add(self.label, 0, wx.ALIGN_CENTER_VERTICAL) + self.Sizer.Add(self.gauge, 1, wx.EXPAND) + + size = wx.DefaultSize + if kw.has_key('size'): + size = kw['size'] + elif len(args) >= 4: + size=args[3] # parent, id, pos, size, style, name + + self.SetInitialSize(size) + + if self.HasFlag(PI_PULSEMODE): + self.timer = wx.Timer(self) + self.Bind(wx.EVT_TIMER, self.onTimer) + if self.HasFlag(PI_HIDEINACTIVE): + self.Hide() + + + def __del__(self): + if hasattr(self, 'timer'): + self.timer.Stop() + del self.timer + + + def Start(self, label=None): + """ + Show (if necessary) and begin displaying progress + """ + self._startCount += 1 + if label is not None: + self.SetLabel(label) + if self.HasFlag(PI_HIDEINACTIVE): + self.Show() + self.Layout() + if self.HasFlag(PI_PULSEMODE): + self.gauge.Pulse() + self.timer.Start(250) + else: + self.gauge.SetValue(0) + + + def Stop(self, clearLabel=False): + """ + Stop showing progress + """ + # Make sure Stop is called as many times as Start was. Only really + # stop when the count reaches zero. + if self._startCount == 0: + return # should be already stopped... + self._startCount -= 1 + if self._startCount: + return # there's still more starts than stops... + + if self.HasFlag(PI_HIDEINACTIVE): + self.Hide() + + if self.HasFlag(PI_PULSEMODE): + self.timer.Stop() + + if clearLabel: + self.label.SetLabel("") + + + def SetLabel(self, text): + """ + Set the text displayed in the label. + """ + self.label.SetLabel(text) + self.Layout() + + + def SetValue(self, value, label=None): + """ + For determinante mode (non-pulse) update the progress indicator to the + given value. For example, if the job is 45% done then pass 45 to this + method (as long as the range is still set to 100.) + """ + if label is not None: + self.SetLabel(label) + self.gauge.SetValue(value) + + + def SetRange(self, maxval): + """ + For determinante mode (non-pulse) set the max value that the gauge can + be set to. Defaults to 100. + """ + self.gauge.SetRange(maxval) + + + def onTimer(self, evt): + self.gauge.Pulse() + + +#---------------------------------------------------------------------- + +if __name__ == '__main__': + app = wx.App(redirect=False) + frm = wx.Frame(None, title="ProgressIndicator") + pnl = wx.Panel(frm) + pi1 = ProgressIndicator(pnl, pos=(20,20), size=(150,-1), + style=PI_HIDEINACTIVE|PI_PULSEMODE) + + pi2 = ProgressIndicator(pnl, pos=(20,60), size=(150,-1), + style=PI_HIDEINACTIVE) + + import wx.lib.inspection + wx.lib.inspection.InspectionTool().Show() + + frm.Show() + app.MainLoop() + diff --git a/wx/lib/pydocview.py b/wx/lib/pydocview.py new file mode 100644 index 00000000..4f0244e7 --- /dev/null +++ b/wx/lib/pydocview.py @@ -0,0 +1,3298 @@ +#---------------------------------------------------------------------------- +# Name: pydocview.py +# Purpose: Python extensions to the wxWindows docview framework +# +# Author: Peter Yared, Morgan Hua, Matt Fryer +# +# Created: 5/15/03 +# CVS-ID: $Id$ +# Copyright: (c) 2003-2006 ActiveGrid, Inc. +# License: wxWindows license +#---------------------------------------------------------------------------- + + +import wx +import wx.lib.docview +import sys +import getopt +from wx.lib.rcsizer import RowColSizer +import os +import os.path +import time +import string +import pickle +import tempfile +import mmap +_ = wx.GetTranslation +if wx.Platform == '__WXMSW__': + _WINDOWS = True +else: + _WINDOWS = False + +#---------------------------------------------------------------------------- +# Constants +#---------------------------------------------------------------------------- + +VIEW_TOOLBAR_ID = wx.NewId() +VIEW_STATUSBAR_ID = wx.NewId() + +EMBEDDED_WINDOW_TOP = 1 +EMBEDDED_WINDOW_BOTTOM = 2 +EMBEDDED_WINDOW_LEFT = 4 +EMBEDDED_WINDOW_RIGHT = 8 +EMBEDDED_WINDOW_TOPLEFT = 16 +EMBEDDED_WINDOW_BOTTOMLEFT = 32 +EMBEDDED_WINDOW_TOPRIGHT = 64 +EMBEDDED_WINDOW_BOTTOMRIGHT = 128 +EMBEDDED_WINDOW_ALL = EMBEDDED_WINDOW_TOP | EMBEDDED_WINDOW_BOTTOM | EMBEDDED_WINDOW_LEFT | EMBEDDED_WINDOW_RIGHT | \ + EMBEDDED_WINDOW_TOPLEFT | EMBEDDED_WINDOW_BOTTOMLEFT | EMBEDDED_WINDOW_TOPRIGHT | EMBEDDED_WINDOW_BOTTOMRIGHT + +SAVEALL_ID = wx.NewId() + +WINDOW_MENU_NUM_ITEMS = 9 + + +class DocFrameMixIn: + """ + Class with common code used by DocMDIParentFrame, DocTabbedParentFrame, and + DocSDIFrame. + """ + + + def GetDocumentManager(self): + """ + Returns the document manager associated with the DocMDIParentFrame. + """ + return self._docManager + + + def InitializePrintData(self): + """ + Initializes the PrintData that is used when printing. + """ + self._printData = wx.PrintData() + self._printData.SetPaperId(wx.PAPER_LETTER) + + + def CreateDefaultMenuBar(self, sdi=False): + """ + Creates the default MenuBar. Contains File, Edit, View, Tools, and Help menus. + """ + menuBar = wx.MenuBar() + + fileMenu = wx.Menu() + fileMenu.Append(wx.ID_NEW, _("&New...\tCtrl+N"), _("Creates a new document")) + fileMenu.Append(wx.ID_OPEN, _("&Open...\tCtrl+O"), _("Opens an existing document")) + fileMenu.Append(wx.ID_CLOSE, _("&Close"), _("Closes the active document")) + if not sdi: + fileMenu.Append(wx.ID_CLOSE_ALL, _("Close A&ll"), _("Closes all open documents")) + fileMenu.AppendSeparator() + fileMenu.Append(wx.ID_SAVE, _("&Save\tCtrl+S"), _("Saves the active document")) + fileMenu.Append(wx.ID_SAVEAS, _("Save &As..."), _("Saves the active document with a new name")) + fileMenu.Append(SAVEALL_ID, _("Save All\tCtrl+Shift+A"), _("Saves the all active documents")) + wx.EVT_MENU(self, SAVEALL_ID, self.ProcessEvent) + wx.EVT_UPDATE_UI(self, SAVEALL_ID, self.ProcessUpdateUIEvent) + fileMenu.AppendSeparator() + fileMenu.Append(wx.ID_PRINT, _("&Print\tCtrl+P"), _("Prints the active document")) + fileMenu.Append(wx.ID_PREVIEW, _("Print Pre&view"), _("Displays full pages")) + fileMenu.Append(wx.ID_PRINT_SETUP, _("Page Set&up"), _("Changes page layout settings")) + fileMenu.AppendSeparator() + if wx.Platform == '__WXMAC__': + fileMenu.Append(wx.ID_EXIT, _("&Quit"), _("Closes this program")) + else: + fileMenu.Append(wx.ID_EXIT, _("E&xit"), _("Closes this program")) + self._docManager.FileHistoryUseMenu(fileMenu) + self._docManager.FileHistoryAddFilesToMenu() + menuBar.Append(fileMenu, _("&File")); + + editMenu = wx.Menu() + editMenu.Append(wx.ID_UNDO, _("&Undo\tCtrl+Z"), _("Reverses the last action")) + editMenu.Append(wx.ID_REDO, _("&Redo\tCtrl+Y"), _("Reverses the last undo")) + editMenu.AppendSeparator() + #item = wxMenuItem(self.editMenu, wxID_CUT, _("Cu&t\tCtrl+X"), _("Cuts the selection and puts it on the Clipboard")) + #item.SetBitmap(getCutBitmap()) + #editMenu.AppendItem(item) + editMenu.Append(wx.ID_CUT, _("Cu&t\tCtrl+X"), _("Cuts the selection and puts it on the Clipboard")) + wx.EVT_MENU(self, wx.ID_CUT, self.ProcessEvent) + wx.EVT_UPDATE_UI(self, wx.ID_CUT, self.ProcessUpdateUIEvent) + editMenu.Append(wx.ID_COPY, _("&Copy\tCtrl+C"), _("Copies the selection and puts it on the Clipboard")) + wx.EVT_MENU(self, wx.ID_COPY, self.ProcessEvent) + wx.EVT_UPDATE_UI(self, wx.ID_COPY, self.ProcessUpdateUIEvent) + editMenu.Append(wx.ID_PASTE, _("&Paste\tCtrl+V"), _("Inserts Clipboard contents")) + wx.EVT_MENU(self, wx.ID_PASTE, self.ProcessEvent) + wx.EVT_UPDATE_UI(self, wx.ID_PASTE, self.ProcessUpdateUIEvent) + editMenu.Append(wx.ID_CLEAR, _("&Delete"), _("Erases the selection")) + wx.EVT_MENU(self, wx.ID_CLEAR, self.ProcessEvent) + wx.EVT_UPDATE_UI(self, wx.ID_CLEAR, self.ProcessUpdateUIEvent) + editMenu.AppendSeparator() + editMenu.Append(wx.ID_SELECTALL, _("Select A&ll\tCtrl+A"), _("Selects all available data")) + wx.EVT_MENU(self, wx.ID_SELECTALL, self.ProcessEvent) + wx.EVT_UPDATE_UI(self, wx.ID_SELECTALL, self.ProcessUpdateUIEvent) + menuBar.Append(editMenu, _("&Edit")) + if sdi: + if self.GetDocument() and self.GetDocument().GetCommandProcessor(): + self.GetDocument().GetCommandProcessor().SetEditMenu(editMenu) + + viewMenu = wx.Menu() + viewMenu.AppendCheckItem(VIEW_TOOLBAR_ID, _("&Toolbar"), _("Shows or hides the toolbar")) + wx.EVT_MENU(self, VIEW_TOOLBAR_ID, self.OnViewToolBar) + wx.EVT_UPDATE_UI(self, VIEW_TOOLBAR_ID, self.OnUpdateViewToolBar) + viewMenu.AppendCheckItem(VIEW_STATUSBAR_ID, _("&Status Bar"), _("Shows or hides the status bar")) + wx.EVT_MENU(self, VIEW_STATUSBAR_ID, self.OnViewStatusBar) + wx.EVT_UPDATE_UI(self, VIEW_STATUSBAR_ID, self.OnUpdateViewStatusBar) + menuBar.Append(viewMenu, _("&View")) + + helpMenu = wx.Menu() + helpMenu.Append(wx.ID_ABOUT, _("&About" + " " + wx.GetApp().GetAppName()), _("Displays program information, version number, and copyright")) + menuBar.Append(helpMenu, _("&Help")) + + wx.EVT_MENU(self, wx.ID_ABOUT, self.OnAbout) + wx.EVT_UPDATE_UI(self, wx.ID_ABOUT, self.ProcessUpdateUIEvent) # Using ID_ABOUT to update the window menu, the window menu items are not triggering + + if sdi: # TODO: Is this really needed? + wx.EVT_COMMAND_FIND_CLOSE(self, -1, self.ProcessEvent) + + return menuBar + + + def CreateDefaultStatusBar(self): + """ + Creates the default StatusBar. + """ + wx.Frame.CreateStatusBar(self) + self.GetStatusBar().Show(wx.ConfigBase_Get().ReadInt("ViewStatusBar", True)) + self.UpdateStatus() + return self.GetStatusBar() + + + def CreateDefaultToolBar(self): + """ + Creates the default ToolBar. + """ + self._toolBar = self.CreateToolBar(wx.TB_HORIZONTAL | wx.NO_BORDER | wx.TB_FLAT) + self._toolBar.AddSimpleTool(wx.ID_NEW, New.GetBitmap(), _("New"), _("Creates a new document")) + self._toolBar.AddSimpleTool(wx.ID_OPEN, Open.GetBitmap(), _("Open"), _("Opens an existing document")) + self._toolBar.AddSimpleTool(wx.ID_SAVE, Save.GetBitmap(), _("Save"), _("Saves the active document")) + self._toolBar.AddSimpleTool(SAVEALL_ID, SaveAll.GetBitmap(), _("Save All"), _("Saves all the active documents")) + self._toolBar.AddSeparator() + self._toolBar.AddSimpleTool(wx.ID_PRINT, Print.GetBitmap(), _("Print"), _("Displays full pages")) + self._toolBar.AddSimpleTool(wx.ID_PREVIEW, PrintPreview.GetBitmap(), _("Print Preview"), _("Prints the active document")) + self._toolBar.AddSeparator() + self._toolBar.AddSimpleTool(wx.ID_CUT, Cut.GetBitmap(), _("Cut"), _("Cuts the selection and puts it on the Clipboard")) + self._toolBar.AddSimpleTool(wx.ID_COPY, Copy.GetBitmap(), _("Copy"), _("Copies the selection and puts it on the Clipboard")) + self._toolBar.AddSimpleTool(wx.ID_PASTE, Paste.GetBitmap(), _("Paste"), _("Inserts Clipboard contents")) + self._toolBar.AddSimpleTool(wx.ID_UNDO, Undo.GetBitmap(), _("Undo"), _("Reverses the last action")) + self._toolBar.AddSimpleTool(wx.ID_REDO, Redo.GetBitmap(), _("Redo"), _("Reverses the last undo")) + self._toolBar.Realize() + self._toolBar.Show(wx.ConfigBase_Get().ReadInt("ViewToolBar", True)) + + return self._toolBar + + + def OnFileSaveAll(self, event): + """ + Saves all of the currently open documents. + """ + docs = wx.GetApp().GetDocumentManager().GetDocuments() + + # save child documents first + for doc in docs: + if isinstance(doc, wx.lib.pydocview.ChildDocument): + doc.Save() + + # save parent and other documents later + for doc in docs: + if not isinstance(doc, wx.lib.pydocview.ChildDocument): + doc.Save() + + + def OnAbout(self, event): + """ + Invokes the about dialog. + """ + aboutService = wx.GetApp().GetService(AboutService) + if aboutService: + aboutService.ShowAbout() + + + def OnViewToolBar(self, event): + """ + Toggles whether the ToolBar is visible. + """ + self._toolBar.Show(not self._toolBar.IsShown()) + self._LayoutFrame() + + + def OnUpdateViewToolBar(self, event): + """ + Updates the View ToolBar menu item. + """ + event.Check(self.GetToolBar().IsShown()) + + + def OnViewStatusBar(self, event): + """ + Toggles whether the StatusBar is visible. + """ + self.GetStatusBar().Show(not self.GetStatusBar().IsShown()) + self._LayoutFrame() + + + def OnUpdateViewStatusBar(self, event): + """ + Updates the View StatusBar menu item. + """ + event.Check(self.GetStatusBar().IsShown()) + + + def UpdateStatus(self, message = _("Ready")): + """ + Updates the StatusBar. + """ + # wxBug: Menubar and toolbar help strings don't pop the status text back + if self.GetStatusBar().GetStatusText() != message: + self.GetStatusBar().PushStatusText(message) + + +class DocMDIParentFrameMixIn: + """ + Class with common code used by DocMDIParentFrame and DocTabbedParentFrame. + """ + + + def _GetPosSizeFromConfig(self, pos, size): + """ + Adjusts the position and size of the frame using the saved config position and size. + """ + config = wx.ConfigBase_Get() + if pos == wx.DefaultPosition and size == wx.DefaultSize and config.ReadInt("MDIFrameMaximized", False): + pos = [0, 0] + size = wx.DisplaySize() + # wxBug: Need to set to fill screen to get around bug where maximize is leaving shadow of statusbar, check out maximize call at end of this function + else: + if pos == wx.DefaultPosition: + pos = config.ReadInt("MDIFrameXLoc", -1), config.ReadInt("MDIFrameYLoc", -1) + + if wx.Display_GetFromPoint(pos) == -1: # Check if the frame position is offscreen + pos = wx.DefaultPosition + + if size == wx.DefaultSize: + size = wx.Size(config.ReadInt("MDIFrameXSize", 450), config.ReadInt("MDIFrameYSize", 300)) + return pos, size + + + def _InitFrame(self, embeddedWindows, minSize): + """ + Initializes the frame and creates the default menubar, toolbar, and status bar. + """ + self._embeddedWindows = [] + self.SetDropTarget(_DocFrameFileDropTarget(self._docManager, self)) + + if wx.GetApp().GetDefaultIcon(): + self.SetIcon(wx.GetApp().GetDefaultIcon()) + + wx.EVT_MENU(self, wx.ID_ABOUT, self.OnAbout) + wx.EVT_SIZE(self, self.OnSize) + + self.InitializePrintData() + + toolBar = self.CreateDefaultToolBar() + self.SetToolBar(toolBar) + menuBar = self.CreateDefaultMenuBar() + statusBar = self.CreateDefaultStatusBar() + + config = wx.ConfigBase_Get() + if config.ReadInt("MDIFrameMaximized", False): + # wxBug: On maximize, statusbar leaves a residual that needs to be refereshed, happens even when user does it + self.Maximize() + + self.CreateEmbeddedWindows(embeddedWindows, minSize) + self._LayoutFrame() + + if wx.Platform == '__WXMAC__': + self.SetMenuBar(menuBar) # wxBug: Have to set the menubar at the very end or the automatic MDI "window" menu doesn't get put in the right place when the services add new menus to the menubar + + wx.GetApp().SetTopWindow(self) # Need to do this here in case the services are looking for wx.GetApp().GetTopWindow() + for service in wx.GetApp().GetServices(): + service.InstallControls(self, menuBar = menuBar, toolBar = toolBar, statusBar = statusBar) + if hasattr(service, "ShowWindow"): + service.ShowWindow() # instantiate service windows for correct positioning, we'll hide/show them later based on user preference + + if wx.Platform != '__WXMAC__': + self.SetMenuBar(menuBar) # wxBug: Have to set the menubar at the very end or the automatic MDI "window" menu doesn't get put in the right place when the services add new menus to the menubar + + + def ProcessEvent(self, event): + """ + Processes an event, searching event tables and calling zero or more + suitable event handler function(s). Note that the ProcessEvent + method is called from the wxPython docview framework directly since + wxPython does not have a virtual ProcessEvent function. + """ + id = event.GetId() + if id == SAVEALL_ID: + self.OnFileSaveAll(event) + return True + + return wx.GetApp().ProcessEvent(event) + + + def ProcessUpdateUIEvent(self, event): + """ + Processes a UI event, searching event tables and calling zero or more + suitable event handler function(s). Note that the ProcessEvent + method is called from the wxPython docview framework directly since + wxPython does not have a virtual ProcessEvent function. + """ + id = event.GetId() + if id == wx.ID_CUT: + event.Enable(False) + return True + elif id == wx.ID_COPY: + event.Enable(False) + return True + elif id == wx.ID_PASTE: + event.Enable(False) + return True + elif id == wx.ID_CLEAR: + event.Enable(False) + return True + elif id == wx.ID_SELECTALL: + event.Enable(False) + return True + elif id == SAVEALL_ID: + filesModified = False + docs = wx.GetApp().GetDocumentManager().GetDocuments() + for doc in docs: + if doc.IsModified(): + filesModified = True + break + + event.Enable(filesModified) + return True + else: + return wx.GetApp().ProcessUpdateUIEvent(event) + + + def CreateEmbeddedWindows(self, windows=0, minSize=20): + """ + Create the specified embedded windows around the edges of the frame. + """ + frameSize = self.GetSize() # TODO: GetClientWindow.GetSize is still returning 0,0 since the frame isn't fully constructed yet, so using full frame size + defaultHSize = max(minSize, int(frameSize[0] / 6)) + defaultVSize = max(minSize, int(frameSize[1] / 7)) + defaultSubVSize = int(frameSize[1] / 2) + config = wx.ConfigBase_Get() + if windows & (EMBEDDED_WINDOW_LEFT | EMBEDDED_WINDOW_TOPLEFT | EMBEDDED_WINDOW_BOTTOMLEFT): + self._leftEmbWindow = self._CreateEmbeddedWindow(self, (max(minSize,config.ReadInt("MDIEmbedLeftSize", defaultHSize)), -1), wx.LAYOUT_VERTICAL, wx.LAYOUT_LEFT, visible = config.ReadInt("MDIEmbedLeftVisible", 1), sash = wx.SASH_RIGHT) + else: + self._leftEmbWindow = None + if windows & EMBEDDED_WINDOW_TOPLEFT: + self._topLeftEmbWindow = self._CreateEmbeddedWindow(self._leftEmbWindow, (-1, config.ReadInt("MDIEmbedTopLeftSize", defaultSubVSize)), wx.LAYOUT_HORIZONTAL, wx.LAYOUT_TOP, visible = config.ReadInt("MDIEmbedTopLeftVisible", 1), sash = wx.SASH_BOTTOM) + else: + self._topLeftEmbWindow = None + if windows & EMBEDDED_WINDOW_BOTTOMLEFT: + self._bottomLeftEmbWindow = self._CreateEmbeddedWindow(self._leftEmbWindow, (-1, config.ReadInt("MDIEmbedBottomLeftSize", defaultSubVSize)), wx.LAYOUT_HORIZONTAL, wx.LAYOUT_BOTTOM, visible = config.ReadInt("MDIEmbedBottomLeftVisible", 1)) + else: + self._bottomLeftEmbWindow = None + if windows & (EMBEDDED_WINDOW_RIGHT | EMBEDDED_WINDOW_TOPRIGHT | EMBEDDED_WINDOW_BOTTOMRIGHT): + self._rightEmbWindow = self._CreateEmbeddedWindow(self, (max(minSize,config.ReadInt("MDIEmbedRightSize", defaultHSize)), -1), wx.LAYOUT_VERTICAL, wx.LAYOUT_RIGHT, visible = config.ReadInt("MDIEmbedRightVisible", 1), sash = wx.SASH_LEFT) + else: + self._rightEmbWindow = None + if windows & EMBEDDED_WINDOW_TOPRIGHT: + self._topRightEmbWindow = self._CreateEmbeddedWindow(self._rightEmbWindow, (-1, config.ReadInt("MDIEmbedTopRightSize", defaultSubVSize)), wx.LAYOUT_HORIZONTAL, wx.LAYOUT_TOP, visible = config.ReadInt("MDIEmbedTopRightVisible", 1), sash = wx.SASH_BOTTOM) + else: + self._topRightEmbWindow = None + if windows & EMBEDDED_WINDOW_BOTTOMRIGHT: + self._bottomRightEmbWindow = self._CreateEmbeddedWindow(self._rightEmbWindow, (-1, config.ReadInt("MDIEmbedBottomRightSize", defaultSubVSize)), wx.LAYOUT_HORIZONTAL, wx.LAYOUT_BOTTOM, visible = config.ReadInt("MDIEmbedBottomRightVisible", 1)) + else: + self._bottomRightEmbWindow = None + if windows & EMBEDDED_WINDOW_TOP: + self._topEmbWindow = self._CreateEmbeddedWindow(self, (-1, max(minSize,config.ReadInt("MDIEmbedTopSize", defaultVSize))), wx.LAYOUT_HORIZONTAL, wx.LAYOUT_TOP, visible = config.ReadInt("MDIEmbedTopVisible", 1), sash = wx.SASH_BOTTOM) + else: + self._topEmbWindow = None + if windows & EMBEDDED_WINDOW_BOTTOM: + self._bottomEmbWindow = self._CreateEmbeddedWindow(self, (-1, max(minSize,config.ReadInt("MDIEmbedBottomSize", defaultVSize))), wx.LAYOUT_HORIZONTAL, wx.LAYOUT_BOTTOM, visible = config.ReadInt("MDIEmbedBottomVisible", 1), sash = wx.SASH_TOP) + else: + self._bottomEmbWindow = None + + + def SaveEmbeddedWindowSizes(self): + """ + Saves the sizes of the embedded windows. + """ + config = wx.ConfigBase_Get() + if not self.IsMaximized(): + config.WriteInt("MDIFrameXLoc", self.GetPositionTuple()[0]) + config.WriteInt("MDIFrameYLoc", self.GetPositionTuple()[1]) + config.WriteInt("MDIFrameXSize", self.GetSizeTuple()[0]) + config.WriteInt("MDIFrameYSize", self.GetSizeTuple()[1]) + config.WriteInt("MDIFrameMaximized", self.IsMaximized()) + config.WriteInt("ViewToolBar", self._toolBar.IsShown()) + config.WriteInt("ViewStatusBar", self.GetStatusBar().IsShown()) + + if self._leftEmbWindow: + config.WriteInt("MDIEmbedLeftSize", self._leftEmbWindow.GetSize()[0]) + config.WriteInt("MDIEmbedLeftVisible", self._leftEmbWindow.IsShown()) + if self._topLeftEmbWindow: + if self._topLeftEmbWindow._sizeBeforeHidden: + size = self._topLeftEmbWindow._sizeBeforeHidden[1] + else: + size = self._topLeftEmbWindow.GetSize()[1] + config.WriteInt("MDIEmbedTopLeftSize", size) + config.WriteInt("MDIEmbedTopLeftVisible", self._topLeftEmbWindow.IsShown()) + if self._bottomLeftEmbWindow: + if self._bottomLeftEmbWindow._sizeBeforeHidden: + size = self._bottomLeftEmbWindow._sizeBeforeHidden[1] + else: + size = self._bottomLeftEmbWindow.GetSize()[1] + config.WriteInt("MDIEmbedBottomLeftSize", size) + config.WriteInt("MDIEmbedBottomLeftVisible", self._bottomLeftEmbWindow.IsShown()) + if self._rightEmbWindow: + config.WriteInt("MDIEmbedRightSize", self._rightEmbWindow.GetSize()[0]) + config.WriteInt("MDIEmbedRightVisible", self._rightEmbWindow.IsShown()) + if self._topRightEmbWindow: + if self._topRightEmbWindow._sizeBeforeHidden: + size = self._topRightEmbWindow._sizeBeforeHidden[1] + else: + size = self._topRightEmbWindow.GetSize()[1] + config.WriteInt("MDIEmbedTopRightSize", size) + config.WriteInt("MDIEmbedTopRightVisible", self._topRightEmbWindow.IsShown()) + if self._bottomRightEmbWindow: + if self._bottomRightEmbWindow._sizeBeforeHidden: + size = self._bottomRightEmbWindow._sizeBeforeHidden[1] + else: + size = self._bottomRightEmbWindow.GetSize()[1] + config.WriteInt("MDIEmbedBottomRightSize", size) + config.WriteInt("MDIEmbedBottomRightVisible", self._bottomRightEmbWindow.IsShown()) + if self._topEmbWindow: + config.WriteInt("MDIEmbedTopSize", self._topEmbWindow.GetSize()[1]) + config.WriteInt("MDIEmbedTopVisible", self._topEmbWindow.IsShown()) + if self._bottomEmbWindow: + config.WriteInt("MDIEmbedBottomSize", self._bottomEmbWindow.GetSize()[1]) + config.WriteInt("MDIEmbedBottomVisible", self._bottomEmbWindow.IsShown()) + + + def GetEmbeddedWindow(self, loc): + """ + Returns the instance of the embedded window specified by the embedded window location constant. + """ + if loc == EMBEDDED_WINDOW_TOP: + return self._topEmbWindow + elif loc == EMBEDDED_WINDOW_BOTTOM: + return self._bottomEmbWindow + elif loc == EMBEDDED_WINDOW_LEFT: + return self._leftEmbWindow + elif loc == EMBEDDED_WINDOW_RIGHT: + return self._rightEmbWindow + elif loc == EMBEDDED_WINDOW_TOPLEFT: + return self._topLeftEmbWindow + elif loc == EMBEDDED_WINDOW_BOTTOMLEFT: + return self._bottomLeftEmbWindow + elif loc == EMBEDDED_WINDOW_TOPRIGHT: + return self._topRightEmbWindow + elif loc == EMBEDDED_WINDOW_BOTTOMRIGHT: + return self._bottomRightEmbWindow + return None + + + def _CreateEmbeddedWindow(self, parent, size, orientation, alignment, visible=True, sash=None): + """ + Creates the embedded window with the specified size, orientation, and alignment. If the + window is not visible it will retain the size with which it was last viewed. + """ + window = wx.SashLayoutWindow(parent, wx.NewId(), style = wx.NO_BORDER | wx.SW_3D) + window.SetDefaultSize(size) + window.SetOrientation(orientation) + window.SetAlignment(alignment) + if sash != None: # wx.SASH_TOP is 0 so check for None instead of just doing "if sash:" + window.SetSashVisible(sash, True) + #### + def OnEmbeddedWindowSashDrag(event): + if event.GetDragStatus() == wx.SASH_STATUS_OUT_OF_RANGE: + return + sashWindow = event.GetEventObject() + if sashWindow.GetAlignment() == wx.LAYOUT_TOP or sashWindow.GetAlignment() == wx.LAYOUT_BOTTOM: + size = wx.Size(-1, event.GetDragRect().height) + else: + size = wx.Size(event.GetDragRect().width, -1) + event.GetEventObject().SetDefaultSize(size) + self._LayoutFrame() + sashWindow.Refresh() + if isinstance(sashWindow.GetParent(), wx.SashLayoutWindow): + sashWindow.Show() + parentSashWindow = sashWindow.GetParent() # Force a refresh + parentSashWindow.Layout() + parentSashWindow.Refresh() + parentSashWindow.SetSize((parentSashWindow.GetSize().width + 1, parentSashWindow.GetSize().height + 1)) + #### + wx.EVT_SASH_DRAGGED(window, window.GetId(), OnEmbeddedWindowSashDrag) + window._sizeBeforeHidden = None + if not visible: + window.Show(False) + if isinstance(parent, wx.SashLayoutWindow): # It's a window embedded in another sash window so remember its actual size to show it again + window._sizeBeforeHidden = size + return window + + + def ShowEmbeddedWindow(self, window, show=True): + """ + Shows or hides the embedded window specified by the embedded window location constant. + """ + window.Show(show) + if isinstance(window.GetParent(), wx.SashLayoutWindow): # It is a parent sashwindow with multiple embedded sashwindows + parentSashWindow = window.GetParent() + if show: # Make sure it is visible in case all of the subwindows were hidden + parentSashWindow.Show() + if show and window._sizeBeforeHidden: + if window._sizeBeforeHidden[1] == parentSashWindow.GetClientSize()[1]: + if window == self.GetEmbeddedWindow(EMBEDDED_WINDOW_BOTTOMLEFT) and self.GetEmbeddedWindow(EMBEDDED_WINDOW_TOPLEFT).IsShown(): + window.SetDefaultSize((window._sizeBeforeHidden[0], window._sizeBeforeHidden[0] - self.GetEmbeddedWindow(EMBEDDED_WINDOW_TOPLEFT).GetSize()[1])) + elif window == self.GetEmbeddedWindow(EMBEDDED_WINDOW_TOPLEFT) and self.GetEmbeddedWindow(EMBEDDED_WINDOW_BOTTOMLEFT).IsShown(): + window.SetDefaultSize((window._sizeBeforeHidden[0], window._sizeBeforeHidden[0] - self.GetEmbeddedWindow(EMBEDDED_WINDOW_BOTTOMLEFT).GetSize()[1])) + elif window == self.GetEmbeddedWindow(EMBEDDED_WINDOW_BOTTOMRIGHT) and self.GetEmbeddedWindow(EMBEDDED_WINDOW_TOPRIGHT).IsShown(): + window.SetDefaultSize((window._sizeBeforeHidden[0], window._sizeBeforeHidden[0] - self.GetEmbeddedWindow(EMBEDDED_WINDOW_TOPRIGHT).GetSize()[1])) + elif window == self.GetEmbeddedWindow(EMBEDDED_WINDOW_TOPRIGHT) and self.GetEmbeddedWindow(EMBEDDED_WINDOW_BOTTOMRIGHT).IsShown(): + window.SetDefaultSize((window._sizeBeforeHidden[0], window._sizeBeforeHidden[0] - self.GetEmbeddedWindow(EMBEDDED_WINDOW_BOTTOMRIGHT).GetSize()[1])) + else: + window.SetDefaultSize(window._sizeBeforeHidden) + # If it is not the size of the full parent sashwindow set the other window's size so that if it gets shown it will have a cooresponding size + if window._sizeBeforeHidden[1] < parentSashWindow.GetClientSize()[1]: + otherWindowSize = (-1, parentSashWindow.GetClientSize()[1] - window._sizeBeforeHidden[1]) + if window == self.GetEmbeddedWindow(EMBEDDED_WINDOW_BOTTOMLEFT): + self.GetEmbeddedWindow(EMBEDDED_WINDOW_TOPLEFT).SetDefaultSize(otherWindowSize) + elif window == self.GetEmbeddedWindow(EMBEDDED_WINDOW_TOPLEFT): + self.GetEmbeddedWindow(EMBEDDED_WINDOW_BOTTOMLEFT).SetDefaultSize(otherWindowSize) + elif window == self.GetEmbeddedWindow(EMBEDDED_WINDOW_BOTTOMRIGHT): + self.GetEmbeddedWindow(EMBEDDED_WINDOW_TOPRIGHT).SetDefaultSize(otherWindowSize) + elif window == self.GetEmbeddedWindow(EMBEDDED_WINDOW_TOPRIGHT): + self.GetEmbeddedWindow(EMBEDDED_WINDOW_BOTTOMRIGHT).SetDefaultSize(otherWindowSize) + + if not show: + if window == self.GetEmbeddedWindow(EMBEDDED_WINDOW_BOTTOMRIGHT) and not self.GetEmbeddedWindow(EMBEDDED_WINDOW_TOPRIGHT).IsShown() \ + or window == self.GetEmbeddedWindow(EMBEDDED_WINDOW_TOPRIGHT) and not self.GetEmbeddedWindow(EMBEDDED_WINDOW_BOTTOMRIGHT).IsShown() \ + or window == self.GetEmbeddedWindow(EMBEDDED_WINDOW_BOTTOMLEFT) and not self.GetEmbeddedWindow(EMBEDDED_WINDOW_TOPLEFT).IsShown() \ + or window == self.GetEmbeddedWindow(EMBEDDED_WINDOW_TOPLEFT) and not self.GetEmbeddedWindow(EMBEDDED_WINDOW_BOTTOMLEFT).IsShown(): + parentSashWindow.Hide() # Hide the parent sashwindow if all of the children are hidden + parentSashWindow.Layout() # Force a refresh + parentSashWindow.Refresh() + parentSashWindow.SetSize((parentSashWindow.GetSize().width + 1, parentSashWindow.GetSize().height + 1)) + self._LayoutFrame() + + + def HideEmbeddedWindow(self, window): + """ + Hides the embedded window specified by the embedded window location constant. + """ + self.ShowEmbeddedWindow(window, show=False) + + +class DocTabbedChildFrame(wx.Panel): + """ + The wxDocMDIChildFrame class provides a default frame for displaying + documents on separate windows. This class can only be used for MDI child + frames. + + The class is part of the document/view framework supported by wxWindows, + and cooperates with the wxView, wxDocument, wxDocManager and wxDocTemplate + classes. + """ + + + def __init__(self, doc, view, frame, id, title, pos=wx.DefaultPosition, size=wx.DefaultSize, style=wx.DEFAULT_FRAME_STYLE, name="frame"): + """ + Constructor. Note that the event table must be rebuilt for the + frame since the EvtHandler is not virtual. + """ + wx.Panel.__init__(self, frame.GetNotebook(), id) + self._childDocument = doc + self._childView = view + frame.AddNotebookPage(self, doc.GetPrintableName()) + if view: + view.SetFrame(self) + + + def GetIcon(self): + """ + Dummy method since the icon of tabbed frames are managed by the notebook. + """ + return None + + + def SetIcon(self, icon): + """ + Dummy method since the icon of tabbed frames are managed by the notebook. + """ + pass + + + def Destroy(self): + """ + Removes the current notebook page. + """ + wx.GetApp().GetTopWindow().RemoveNotebookPage(self) + + + def SetFocus(self): + """ + Activates the current notebook page. + """ + wx.GetApp().GetTopWindow().ActivateNotebookPage(self) + + + def Activate(self): # Need this in case there are embedded sash windows and such, OnActivate is not getting called + """ + Activates the current view. + """ + # Called by Project Editor + if self._childView: + self._childView.Activate(True) + + + def GetTitle(self): + """ + Returns the frame's title. + """ + return wx.GetApp().GetTopWindow().GetNotebookPageTitle(self) + + + def SetTitle(self, title): + """ + Sets the frame's title. + """ + wx.GetApp().GetTopWindow().SetNotebookPageTitle(self, title) + + + def OnTitleIsModified(self): + """ + Add/remove to the frame's title an indication that the document is dirty. + If the document is dirty, an '*' is appended to the title + """ + title = self.GetTitle() + if title: + if self.GetDocument().IsModified(): + if not title.endswith("*"): + title = title + "*" + self.SetTitle(title) + else: + if title.endswith("*"): + title = title[:-1] + self.SetTitle(title) + + + def ProcessEvent(event): + """ + Processes an event, searching event tables and calling zero or more + suitable event handler function(s). Note that the ProcessEvent + method is called from the wxPython docview framework directly since + wxPython does not have a virtual ProcessEvent function. + """ + if not self._childView or not self._childView.ProcessEvent(event): + if not isinstance(event, wx.CommandEvent) or not self.GetParent() or not self.GetParent().ProcessEvent(event): + return False + else: + return True + else: + return True + + + def GetDocument(self): + """ + Returns the document associated with this frame. + """ + return self._childDocument + + + def SetDocument(self, document): + """ + Sets the document for this frame. + """ + self._childDocument = document + + + def GetView(self): + """ + Returns the view associated with this frame. + """ + return self._childView + + + def SetView(self, view): + """ + Sets the view for this frame. + """ + self._childView = view + + +class DocTabbedParentFrame(wx.Frame, DocFrameMixIn, DocMDIParentFrameMixIn): + """ + The DocTabbedParentFrame class provides a default top-level frame for + applications using the document/view framework. This class can only be + used for MDI parent frames that use a tabbed interface. + + It cooperates with the wxView, wxDocument, wxDocManager and wxDocTemplate + classes. + """ + + + def __init__(self, docManager, frame, id, title, pos = wx.DefaultPosition, size = wx.DefaultSize, style = wx.DEFAULT_FRAME_STYLE, name = "DocTabbedParentFrame", embeddedWindows = 0, minSize=20): + """ + Constructor. Note that the event table must be rebuilt for the + frame since the EvtHandler is not virtual. + """ + pos, size = self._GetPosSizeFromConfig(pos, size) + wx.Frame.__init__(self, frame, id, title, pos, size, style, name) + + # From docview.MDIParentFrame + self._docManager = docManager + + wx.EVT_CLOSE(self, self.OnCloseWindow) + + wx.EVT_MENU(self, wx.ID_EXIT, self.OnExit) + wx.EVT_MENU_RANGE(self, wx.ID_FILE1, wx.ID_FILE9, self.OnMRUFile) + + wx.EVT_MENU(self, wx.ID_NEW, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_OPEN, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_CLOSE_ALL, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_CLOSE, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_REVERT, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_SAVE, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_SAVEAS, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_UNDO, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_REDO, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_PRINT, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_PRINT_SETUP, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_PREVIEW, self.ProcessEvent) + wx.EVT_MENU(self, wx.ID_ABOUT, self.OnAbout) + + wx.EVT_UPDATE_UI(self, wx.ID_NEW, self.ProcessUpdateUIEvent) + wx.EVT_UPDATE_UI(self, wx.ID_OPEN, self.ProcessUpdateUIEvent) + wx.EVT_UPDATE_UI(self, wx.ID_CLOSE_ALL, self.ProcessUpdateUIEvent) + wx.EVT_UPDATE_UI(self, wx.ID_CLOSE, self.ProcessUpdateUIEvent) + wx.EVT_UPDATE_UI(self, wx.ID_REVERT, self.ProcessUpdateUIEvent) + wx.EVT_UPDATE_UI(self, wx.ID_SAVE, self.ProcessUpdateUIEvent) + wx.EVT_UPDATE_UI(self, wx.ID_SAVEAS, self.ProcessUpdateUIEvent) + wx.EVT_UPDATE_UI(self, wx.ID_UNDO, self.ProcessUpdateUIEvent) + wx.EVT_UPDATE_UI(self, wx.ID_REDO, self.ProcessUpdateUIEvent) + wx.EVT_UPDATE_UI(self, wx.ID_PRINT, self.ProcessUpdateUIEvent) + wx.EVT_UPDATE_UI(self, wx.ID_PRINT_SETUP, self.ProcessUpdateUIEvent) + wx.EVT_UPDATE_UI(self, wx.ID_PREVIEW, self.ProcessUpdateUIEvent) + # End From docview.MDIParentFrame + + self.CreateNotebook() + self._InitFrame(embeddedWindows, minSize) + + + def _LayoutFrame(self): + """ + Lays out the frame. + """ + wx.LayoutAlgorithm().LayoutFrame(self, self._notebook) + + + def CreateNotebook(self): + """ + Creates the notebook to use for the tabbed document interface. + """ + if wx.Platform != "__WXMAC__": + self._notebook = wx.Notebook(self, wx.NewId()) + else: + self._notebook = wx.Listbook(self, wx.NewId(), style=wx.LB_LEFT) + # self._notebook.SetSizer(wx.NotebookSizer(self._notebook)) + if wx.Platform != "__WXMAC__": + wx.EVT_NOTEBOOK_PAGE_CHANGED(self, self._notebook.GetId(), self.OnNotebookPageChanged) + else: + wx.EVT_LISTBOOK_PAGE_CHANGED(self, self._notebook.GetId(), self.OnNotebookPageChanged) + wx.EVT_RIGHT_DOWN(self._notebook, self.OnNotebookRightClick) + wx.EVT_MIDDLE_DOWN(self._notebook, self.OnNotebookMiddleClick) + + # wxBug: wx.Listbook does not implement HitTest the same way wx.Notebook + # does, so for now don't fire MouseOver events. + if wx.Platform != "__WXMAC__": + wx.EVT_MOTION(self._notebook, self.OnNotebookMouseOver) + + 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 or icon.GetWidth() != 16: + icon.SetHeight(16) + icon.SetWidth(16) + if wx.GetApp().GetDebug(): + print "Warning: icon for '%s' isn't 16x16, not crossplatform" % template._docTypeName + iconIndex = iconList.AddIcon(icon) + self._iconIndexLookup.append((template, iconIndex)) + + icon = Blank.GetIcon() + if icon.GetHeight() != 16 or icon.GetWidth() != 16: + icon.SetHeight(16) + icon.SetWidth(16) + if wx.GetApp().GetDebug(): + print "Warning: getBlankIcon isn't 16x16, not crossplatform" + self._blankIconIndex = iconList.AddIcon(icon) + self._notebook.AssignImageList(iconList) + + + def GetNotebook(self): + """ + Returns the notebook used by the tabbed document interface. + """ + return self._notebook + + + def GetActiveChild(self): + """ + Returns the active notebook page, which to the framework is treated as + a document frame. + """ + index = self._notebook.GetSelection() + if index == -1: + return None + return self._notebook.GetPage(index) + + + def OnNotebookPageChanged(self, event): + """ + Activates a notebook page's view when it is selected. + """ + index = self._notebook.GetSelection() + if index > -1: + self._notebook.GetPage(index).GetView().Activate() + + + def OnNotebookMouseOver(self, event): + # wxBug: On Windows XP the tooltips don't automatically disappear when you move the mouse and it is on a notebook tab, has nothing to do with this code!!! + index, type = self._notebook.HitTest(event.GetPosition()) + + if index > -1: + doc = self._notebook.GetPage(index).GetView().GetDocument() + # wxBug: Tooltips no longer appearing on tabs except on + # about a 2 pixel area between tab top and contents that will show tip. + self._notebook.GetParent().SetToolTip(wx.ToolTip(doc.GetFilename())) + else: + self._notebook.SetToolTip(wx.ToolTip("")) + event.Skip() + + + def OnNotebookMiddleClick(self, event): + """ + Handles middle clicks for the notebook, closing the document whose tab was + clicked on. + """ + index, type = self._notebook.HitTest(event.GetPosition()) + if index > -1: + doc = self._notebook.GetPage(index).GetView().GetDocument() + if doc: + doc.DeleteAllViews() + + def OnNotebookRightClick(self, event): + """ + Handles right clicks for the notebook, enabling users to either close + a tab or select from the available documents if the user clicks on the + notebook's white space. + """ + index, type = self._notebook.HitTest(event.GetPosition()) + menu = wx.Menu() + x, y = event.GetX(), event.GetY() + if index > -1: + doc = self._notebook.GetPage(index).GetView().GetDocument() + id = wx.NewId() + menu.Append(id, _("Close")) + def OnRightMenuSelect(event): + doc.DeleteAllViews() + wx.EVT_MENU(self, id, OnRightMenuSelect) + if self._notebook.GetPageCount() > 1: + id = wx.NewId() + menu.Append(id, _("Close All but \"%s\"" % doc.GetPrintableName())) + def OnRightMenuSelect(event): + for i in range(self._notebook.GetPageCount()-1, -1, -1): # Go from len-1 to 0 + if i != index: + doc = self._notebook.GetPage(i).GetView().GetDocument() + if not self.GetDocumentManager().CloseDocument(doc, False): + return + wx.EVT_MENU(self, id, OnRightMenuSelect) + menu.AppendSeparator() + tabsMenu = wx.Menu() + menu.AppendMenu(wx.NewId(), _("Select Tab"), tabsMenu) + else: + y = y - 25 # wxBug: It is offsetting click events in the blank notebook area + tabsMenu = menu + + if self._notebook.GetPageCount() > 1: + selectIDs = {} + for i in range(0, self._notebook.GetPageCount()): + id = wx.NewId() + selectIDs[id] = i + tabsMenu.Append(id, self._notebook.GetPageText(i)) + def OnRightMenuSelect(event): + self._notebook.SetSelection(selectIDs[event.GetId()]) + wx.EVT_MENU(self, id, OnRightMenuSelect) + + self._notebook.PopupMenu(menu, wx.Point(x, y)) + menu.Destroy() + + + def AddNotebookPage(self, panel, title): + """ + Adds a document page to the notebook. + """ + self._notebook.AddPage(panel, title) + index = self._notebook.GetPageCount() - 1 + self._notebook.SetSelection(index) + + found = False # Now set the icon + template = panel.GetDocument().GetDocumentTemplate() + if template: + for t, iconIndex in self._iconIndexLookup: + if t is template: + self._notebook.SetPageImage(index, iconIndex) + found = True + break + if not found: + self._notebook.SetPageImage(index, self._blankIconIndex) + + # wxBug: the wxListbook used on Mac needs its tabs list resized + # whenever a new tab is added, but the only way to do this is + # to resize the entire control + if wx.Platform == "__WXMAC__": + content_size = self._notebook.GetSize() + self._notebook.SetSize((content_size.x+2, -1)) + self._notebook.SetSize((content_size.x, -1)) + + self._notebook.Layout() + + windowMenuService = wx.GetApp().GetService(WindowMenuService) + if windowMenuService: + windowMenuService.BuildWindowMenu(wx.GetApp().GetTopWindow()) # build file menu list when we open a file + + + def RemoveNotebookPage(self, panel): + """ + Removes a document page from the notebook. + """ + index = self.GetNotebookPageIndex(panel) + if index > -1: + if self._notebook.GetPageCount() == 1 or index < 2: + pass + elif index >= 1: + self._notebook.SetSelection(index - 1) + elif index < self._notebook.GetPageCount(): + self._notebook.SetSelection(index + 1) + self._notebook.DeletePage(index) + self._notebook.GetParent().SetToolTip(wx.ToolTip("")) + + windowMenuService = wx.GetApp().GetService(WindowMenuService) + if windowMenuService: + windowMenuService.BuildWindowMenu(wx.GetApp().GetTopWindow()) # build file menu list when we open a file + + + def ActivateNotebookPage(self, panel): + """ + Sets the notebook to the specified panel. + """ + index = self.GetNotebookPageIndex(panel) + if index > -1: + self._notebook.SetFocus() + self._notebook.SetSelection(index) + + + def GetNotebookPageTitle(self, panel): + index = self.GetNotebookPageIndex(panel) + if index != -1: + return self._notebook.GetPageText(self.GetNotebookPageIndex(panel)) + else: + return None + + + def SetNotebookPageTitle(self, panel, title): + self._notebook.SetPageText(self.GetNotebookPageIndex(panel), title) + + + def GetNotebookPageIndex(self, panel): + """ + Returns the index of particular notebook panel. + """ + index = -1 + for i in range(self._notebook.GetPageCount()): + if self._notebook.GetPage(i) == panel: + index = i + break + return index + + + def ProcessEvent(self, event): + """ + Processes an event, searching event tables and calling zero or more + suitable event handler function(s). Note that the ProcessEvent + method is called from the wxPython docview framework directly since + wxPython does not have a virtual ProcessEvent function. + """ + if wx.GetApp().ProcessEventBeforeWindows(event): + return True + if self._docManager and self._docManager.ProcessEvent(event): + return True + return DocMDIParentFrameMixIn.ProcessEvent(self, event) + + + def ProcessUpdateUIEvent(self, event): + """ + Processes a UI event, searching event tables and calling zero or more + suitable event handler function(s). Note that the ProcessEvent + method is called from the wxPython docview framework directly since + wxPython does not have a virtual ProcessEvent function. + """ + if wx.GetApp().ProcessUpdateUIEventBeforeWindows(event): + return True + if self._docManager and self._docManager.ProcessUpdateUIEvent(event): + return True + return DocMDIParentFrameMixIn.ProcessUpdateUIEvent(self, event) + + + def OnExit(self, event): + """ + Called when File/Exit is chosen and closes the window. + """ + self.Close() + + + def OnMRUFile(self, event): + """ + Opens the appropriate file when it is selected from the file history + menu. + """ + n = event.GetId() - wx.ID_FILE1 + filename = self._docManager.GetHistoryFile(n) + if filename: + self._docManager.CreateDocument(filename, wx.lib.docview.DOC_SILENT) + else: + self._docManager.RemoveFileFromHistory(n) + msgTitle = wx.GetApp().GetAppName() + if not msgTitle: + msgTitle = _("File Error") + wx.MessageBox("The file '%s' doesn't exist and couldn't be opened.\nIt has been removed from the most recently used files list" % FileNameFromPath(file), + msgTitle, + wx.OK | wx.ICON_EXCLAMATION, + self) + + + def OnSize(self, event): + """ + Called when the frame is resized and lays out the client window. + """ + # Needed in case there are splitpanels around the mdi frame + self._LayoutFrame() + + + def OnCloseWindow(self, event): + """ + Called when the frame is closed. Remembers the frame size. + """ + self.SaveEmbeddedWindowSizes() + + # save and close services last + for service in wx.GetApp().GetServices(): + if not service.OnCloseFrame(event): + return + + # From docview.MDIParentFrame + if self._docManager.Clear(not event.CanVeto()): + self.Destroy() + else: + event.Veto() + + +class DocMDIChildFrame(wx.MDIChildFrame): + """ + The wxDocMDIChildFrame class provides a default frame for displaying + documents on separate windows. This class can only be used for MDI child + frames. + + The class is part of the document/view framework supported by wxWindows, + and cooperates with the wxView, wxDocument, wxDocManager and wxDocTemplate + classes. + """ + + + def __init__(self, doc, view, frame, id, title, pos=wx.DefaultPosition, size=wx.DefaultSize, style=wx.DEFAULT_FRAME_STYLE, name="frame"): + """ + Constructor. Note that the event table must be rebuilt for the + frame since the EvtHandler is not virtual. + """ + wx.MDIChildFrame.__init__(self, frame, id, title, pos, size, style, name) + self._childDocument = doc + self._childView = view + if view: + view.SetFrame(self) + # self.Create(doc, view, frame, id, title, pos, size, style, name) + self._activeEvent = None + self._activated = 0 + wx.EVT_ACTIVATE(self, self.OnActivate) + wx.EVT_CLOSE(self, self.OnCloseWindow) + + if frame: # wxBug: For some reason the EVT_ACTIVATE event is not getting triggered for the first mdi client window that is opened so we have to do it manually + mdiChildren = filter(lambda x: isinstance(x, wx.MDIChildFrame), frame.GetChildren()) + if len(mdiChildren) == 1: + self.Activate() + + +## # Couldn't get this to work, but seems to work fine with single stage construction +## def Create(self, doc, view, frame, id, title, pos, size, style, name): +## self._childDocument = doc +## self._childView = view +## if wx.MDIChildFrame.Create(self, frame, id, title, pos, size, style, name): +## if view: +## view.SetFrame(self) +## return True +## return False + + + + def Activate(self): # Need this in case there are embedded sash windows and such, OnActivate is not getting called + """ + Activates the current view. + """ + if self._childView: + self._childView.Activate(True) + + + def OnTitleIsModified(self): + """ + Add/remove to the frame's title an indication that the document is dirty. + If the document is dirty, an '*' is appended to the title + """ + title = self.GetTitle() + if title: + if self.GetDocument().IsModified(): + if title.endswith("*"): + return + else: + title = title + "*" + self.SetTitle(title) + else: + if title.endswith("*"): + title = title[:-1] + self.SetTitle(title) + else: + return + + + def ProcessEvent(event): + """ + Processes an event, searching event tables and calling zero or more + suitable event handler function(s). Note that the ProcessEvent + method is called from the wxPython docview framework directly since + wxPython does not have a virtual ProcessEvent function. + """ + if self._activeEvent == event: + return False + + self._activeEvent = event # Break recursion loops + + if self._childView: + self._childView.Activate(True) + + if not self._childView or not self._childView.ProcessEvent(event): + if not isinstance(event, wx.CommandEvent) or not self.GetParent() or not self.GetParent().ProcessEvent(event): + ret = False + else: + ret = True + else: + ret = True + + self._activeEvent = None + return ret + + + def OnActivate(self, event): + """ + Sets the currently active view to be the frame's view. You may need to + override (but still call) this function in order to set the keyboard + focus for your subwindow. + """ + event.Skip() + if self._activated != 0: + return True + self._activated += 1 + wx.MDIChildFrame.Activate(self) + if event.GetActive() and self._childView: + self._childView.Activate(event.GetActive()) + self._activated = 0 + + + def OnCloseWindow(self, event): + """ + Closes and deletes the current view and document. + """ + if self._childView: + ans = False + if not event.CanVeto(): + ans = True + else: + ans = self._childView.Close(deleteWindow = False) + + if ans: + self._childView.Activate(False) + self._childView.Destroy() + self._childView = None + if self._childDocument: + self._childDocument.Destroy() # This isn't in the wxWindows codebase but the document needs to be disposed of somehow + self._childDocument = None + self.Destroy() + else: + event.Veto() + else: + event.Veto() + + + def GetDocument(self): + """ + Returns the document associated with this frame. + """ + return self._childDocument + + + def SetDocument(self, document): + """ + Sets the document for this frame. + """ + self._childDocument = document + + + def GetView(self): + """ + Returns the view associated with this frame. + """ + return self._childView + + + def SetView(self, view): + """ + Sets the view for this frame. + """ + self._childView = view + + +class DocService(wx.EvtHandler): + """ + An abstract class used to add reusable services to a docview application. + """ + + + def __init__(self): + """Initializes the DocService.""" + pass + + + def GetDocumentManager(self): + """Returns the DocManager for the docview application.""" + return self._docManager + + + def SetDocumentManager(self, docManager): + """Sets the DocManager for the docview application.""" + self._docManager = docManager + + + def InstallControls(self, frame, menuBar=None, toolBar=None, statusBar=None, document=None): + """Called to install controls into the menubar and toolbar of a SDI or MDI window. Override this method for a particular service.""" + pass + + + def ProcessEventBeforeWindows(self, event): + """ + Processes an event before the main window has a chance to process the window. + Override this method for a particular service. + """ + return False + + + def ProcessUpdateUIEventBeforeWindows(self, event): + """ + Processes a UI event before the main window has a chance to process the window. + Override this method for a particular service. + """ + return False + + + def ProcessEvent(self, event): + """ + Processes an event, searching event tables and calling zero or more + suitable event handler function(s). Note that the ProcessEvent + method is called from the wxPython docview framework directly since + wxPython does not have a virtual ProcessEvent function. + """ + return False + + + def ProcessUpdateUIEvent(self, event): + """ + Processes a UI event, searching event tables and calling zero or more + suitable event handler function(s). Note that the ProcessEvent + method is called from the wxPython docview framework directly since + wxPython does not have a virtual ProcessEvent function. + """ + return False + + + def OnCloseFrame(self, event): + """ + Called when the a docview frame is being closed. Override this method + so a service can either do cleanup or veto the frame being closed by + returning false. + """ + return True + + + def OnExit(self): + """ + Called when the the docview application is being closed. Override this method + so a service can either do cleanup or veto the frame being closed by + returning false. + """ + pass + + + def GetMenuItemPos(self, menu, id): + """ + Utility method used to find the position of a menu item so that services can + easily find where to insert a menu item in InstallControls. + """ + menuItems = menu.GetMenuItems() + for i, menuItem in enumerate(menuItems): + if menuItem.GetId() == id: + return i + return i + + + def GetView(self): + """ + Called by WindowMenuService to get views for services that don't + have dedicated documents such as the Outline Service. + """ + return None + + +class DocOptionsService(DocService): + """ + A service that implements an options menu item and an options dialog with + notebook tabs. New tabs can be added by other services by calling the + "AddOptionsPanel" method. + """ + + + def __init__(self, showGeneralOptions=True, supportedModes=wx.lib.docview.DOC_SDI & wx.lib.docview.DOC_MDI): + """ + Initializes the options service with the option of suppressing the default + general options pane that is included with the options service by setting + showGeneralOptions to False. It allowModeChanges is set to False, the + default general options pane will allow users to change the document + interface mode between SDI and MDI modes. + """ + DocService.__init__(self) + self.ClearOptionsPanels() + self._supportedModes = supportedModes + self._toolOptionsID = wx.ID_PREFERENCES + if showGeneralOptions: + self.AddOptionsPanel(GeneralOptionsPanel) + + + def InstallControls(self, frame, menuBar=None, toolBar=None, statusBar=None, document=None): + """ + Installs a "Tools" menu with an "Options" menu item. + """ + toolsMenuIndex = menuBar.FindMenu(_("&Tools")) + if toolsMenuIndex > -1: + toolsMenu = menuBar.GetMenu(toolsMenuIndex) + else: + toolsMenu = wx.Menu() + if toolsMenuIndex == -1: + formatMenuIndex = menuBar.FindMenu(_("&Format")) + menuBar.Insert(formatMenuIndex + 1, toolsMenu, _("&Tools")) + if toolsMenu: + if toolsMenu.GetMenuItemCount(): + toolsMenu.AppendSeparator() + toolsMenu.Append(self._toolOptionsID, _("&Options..."), _("Sets options")) + wx.EVT_MENU(frame, self._toolOptionsID, frame.ProcessEvent) + + + def ProcessEvent(self, event): + """ + Checks to see if the "Options" menu item has been selected. + """ + id = event.GetId() + if id == self._toolOptionsID: + self.OnOptions(event) + return True + else: + return False + + + def GetSupportedModes(self): + """ + Return the modes supported by the application. Use docview.DOC_SDI and + docview.DOC_MDI flags to check if SDI and/or MDI modes are supported. + """ + return self._supportedModes + + + def SetSupportedModes(self, _supportedModessupportedModes): + """ + Sets the modes supported by the application. Use docview.DOC_SDI and + docview.DOC_MDI flags to set if SDI and/or MDI modes are supported. + """ + self._supportedModes = supportedModes + + + def ClearOptionsPanels(self): + """ + Clears all of the options panels that have been added into the + options dialog. + """ + self._optionsPanels = [] + + + def AddOptionsPanel(self, optionsPanel): + """ + Adds an options panel to the options dialog. + """ + self._optionsPanels.append(optionsPanel) + + + def OnOptions(self, event): + """ + Shows the options dialog, called when the "Options" menu item is selected. + """ + if len(self._optionsPanels) == 0: + return + optionsDialog = OptionsDialog(wx.GetApp().GetTopWindow(), self._optionsPanels, self._docManager) + optionsDialog.CenterOnParent() + if optionsDialog.ShowModal() == wx.ID_OK: + optionsDialog.OnOK(optionsDialog) # wxBug: wxDialog should be calling this automatically but doesn't + optionsDialog.Destroy() + + +class OptionsDialog(wx.Dialog): + """ + A default options dialog used by the OptionsService that hosts a notebook + tab of options panels. + """ + + + def __init__(self, parent, optionsPanelClasses, docManager): + """ + Initializes the options dialog with a notebook page that contains new + instances of the passed optionsPanelClasses. + """ + wx.Dialog.__init__(self, parent, -1, _("Options")) + + self._optionsPanels = [] + self._docManager = docManager + + HALF_SPACE = 5 + SPACE = 10 + + sizer = wx.BoxSizer(wx.VERTICAL) + + if wx.Platform == "__WXMAC__": + optionsNotebook = wx.Listbook(self, wx.NewId(), style=wx.LB_DEFAULT) + else: + optionsNotebook = wx.Notebook(self, wx.NewId(), style=wx.NB_MULTILINE) # NB_MULTILINE is windows platform only + sizer.Add(optionsNotebook, 0, wx.ALL | wx.EXPAND, SPACE) + + if wx.Platform == "__WXMAC__": + iconList = wx.ImageList(16, 16, initialCount = len(optionsPanelClasses)) + self._iconIndexLookup = [] + + for optionsPanelClass in optionsPanelClasses: + optionsPanel = optionsPanelClass(optionsNotebook, -1) + self._optionsPanels.append(optionsPanel) + + # We need to populate the image list before setting notebook images + if hasattr(optionsPanel, "GetIcon"): + icon = optionsPanel.GetIcon() + else: + icon = None + if icon: + if icon.GetHeight() != 16 or icon.GetWidth() != 16: + icon.SetHeight(16) + icon.SetWidth(16) + if wx.GetApp().GetDebug(): + print "Warning: icon for '%s' isn't 16x16, not crossplatform" % template._docTypeName + iconIndex = iconList.AddIcon(icon) + self._iconIndexLookup.append((optionsPanel, iconIndex)) + + else: + # use -1 to represent that this panel has no icon + self._iconIndexLookup.append((optionsPanel, -1)) + + optionsNotebook.AssignImageList(iconList) + + # Add icons to notebook + for index in range(0, len(optionsPanelClasses)-1): + iconIndex = self._iconIndexLookup[index][1] + if iconIndex >= 0: + optionsNotebook.SetPageImage(index, iconIndex) + else: + for optionsPanelClass in optionsPanelClasses: + optionsPanel = optionsPanelClass(optionsNotebook, -1) + self._optionsPanels.append(optionsPanel) + + sizer.Add(self.CreateButtonSizer(wx.OK | wx.CANCEL), 0, wx.ALIGN_RIGHT | wx.RIGHT | wx.BOTTOM, HALF_SPACE) + self.SetSizer(sizer) + self.Layout() + self.Fit() + wx.CallAfter(self.DoRefresh) + + + def DoRefresh(self): + """ + wxBug: On Windows XP when using a multiline notebook the default page doesn't get + drawn, but it works when using a single line notebook. + """ + self.Refresh() + + + def GetDocManager(self): + """ + Returns the document manager passed to the OptionsDialog constructor. + """ + return self._docManager + + + def OnOK(self, event): + """ + Calls the OnOK method of all of the OptionDialog's embedded panels + """ + for optionsPanel in self._optionsPanels: + optionsPanel.OnOK(event) + + +class GeneralOptionsPanel(wx.Panel): + """ + A general options panel that is used in the OptionDialog to configure the + generic properties of a pydocview application, such as "show tips at startup" + and whether to use SDI or MDI for the application. + """ + + + def __init__(self, parent, id): + """ + Initializes the panel by adding an "Options" folder tab to the parent notebook and + populating the panel with the generic properties of a pydocview application. + """ + wx.Panel.__init__(self, parent, id) + SPACE = 10 + HALF_SPACE = 5 + config = wx.ConfigBase_Get() + self._showTipsCheckBox = wx.CheckBox(self, -1, _("Show tips at start up")) + self._showTipsCheckBox.SetValue(config.ReadInt("ShowTipAtStartup", True)) + if self._AllowModeChanges(): + supportedModes = wx.GetApp().GetService(DocOptionsService).GetSupportedModes() + choices = [] + self._sdiChoice = _("Show each document in its own window") + self._mdiChoice = _("Show all documents in a single window with tabs") + self._winMdiChoice = _("Show all documents in a single window with child windows") + if supportedModes & wx.lib.docview.DOC_SDI: + choices.append(self._sdiChoice) + choices.append(self._mdiChoice) + if wx.Platform == "__WXMSW__": + choices.append(self._winMdiChoice) + self._documentRadioBox = wx.RadioBox(self, -1, _("Document Display Style"), + choices = choices, + majorDimension=1, + ) + if config.ReadInt("UseWinMDI", False): + self._documentRadioBox.SetStringSelection(self._winMdiChoice) + elif config.ReadInt("UseMDI", True): + self._documentRadioBox.SetStringSelection(self._mdiChoice) + else: + self._documentRadioBox.SetStringSelection(self._sdiChoice) + def OnDocumentInterfaceSelect(event): + if not self._documentInterfaceMessageShown: + msgTitle = wx.GetApp().GetAppName() + if not msgTitle: + msgTitle = _("Document Options") + wx.MessageBox(_("Document interface changes will not appear until the application is restarted."), + msgTitle, + wx.OK | wx.ICON_INFORMATION, + self.GetParent()) + self._documentInterfaceMessageShown = True + wx.EVT_RADIOBOX(self, self._documentRadioBox.GetId(), OnDocumentInterfaceSelect) + optionsBorderSizer = wx.BoxSizer(wx.VERTICAL) + optionsSizer = wx.BoxSizer(wx.VERTICAL) + if self._AllowModeChanges(): + optionsSizer.Add(self._documentRadioBox, 0, wx.ALL, HALF_SPACE) + optionsSizer.Add(self._showTipsCheckBox, 0, wx.ALL, HALF_SPACE) + optionsBorderSizer.Add(optionsSizer, 0, wx.ALL, SPACE) + self.SetSizer(optionsBorderSizer) + self.Layout() + self._documentInterfaceMessageShown = False + parent.AddPage(self, _("General")) + + + def _AllowModeChanges(self): + supportedModes = wx.GetApp().GetService(DocOptionsService).GetSupportedModes() + return supportedModes & wx.lib.docview.DOC_SDI and supportedModes & wx.lib.docview.DOC_MDI or wx.Platform == "__WXMSW__" and supportedModes & wx.lib.docview.DOC_MDI # More than one mode is supported, allow selection + + + def OnOK(self, optionsDialog): + """ + Updates the config based on the selections in the options panel. + """ + config = wx.ConfigBase_Get() + config.WriteInt("ShowTipAtStartup", self._showTipsCheckBox.GetValue()) + if self._AllowModeChanges(): + config.WriteInt("UseMDI", (self._documentRadioBox.GetStringSelection() == self._mdiChoice)) + config.WriteInt("UseWinMDI", (self._documentRadioBox.GetStringSelection() == self._winMdiChoice)) + + + def GetIcon(self): + """ Return icon for options panel on the Mac. """ + return wx.GetApp().GetDefaultIcon() + + +class DocApp(wx.PySimpleApp): + """ + The DocApp class serves as the base class for pydocview applications and offers + functionality such as services, creation of SDI and MDI frames, show tips, + and a splash screen. + """ + + + def OnInit(self): + """ + Initializes the DocApp. + """ + self._services = [] + self._defaultIcon = None + self._registeredCloseEvent = False + self._useTabbedMDI = True + + if not hasattr(self, "_debug"): # only set if not already initialized + self._debug = False + if not hasattr(self, "_singleInstance"): # only set if not already initialized + self._singleInstance = True + + # if _singleInstance is TRUE only allow one single instance of app to run. + # When user tries to run a second instance of the app, abort startup, + # But if user also specifies files to open in command line, send message to running app to open those files + if self._singleInstance: + # create shared memory temporary file + if wx.Platform == '__WXMSW__': + tfile = tempfile.TemporaryFile(prefix="ag", suffix="tmp") + fno = tfile.fileno() + self._sharedMemory = mmap.mmap(fno, 1024, "shared_memory") + else: + tfile = file(os.path.join(tempfile.gettempdir(), tempfile.gettempprefix() + self.GetAppName() + '-' + wx.GetUserId() + "AGSharedMemory"), 'w+b') + tfile.write("*") + tfile.seek(1024) + tfile.write(" ") + tfile.flush() + fno = tfile.fileno() + self._sharedMemory = mmap.mmap(fno, 1024) + + self._singleInstanceChecker = wx.SingleInstanceChecker(self.GetAppName() + '-' + wx.GetUserId(), tempfile.gettempdir()) + if self._singleInstanceChecker.IsAnotherRunning(): + # have running single instance open file arguments + data = pickle.dumps(sys.argv[1:]) + while 1: + self._sharedMemory.seek(0) + marker = self._sharedMemory.read_byte() + if marker == '\0' or marker == '*': # available buffer + self._sharedMemory.seek(0) + self._sharedMemory.write_byte('-') # set writing marker + self._sharedMemory.write(data) # write files we tried to open to shared memory + self._sharedMemory.seek(0) + self._sharedMemory.write_byte('+') # set finished writing marker + self._sharedMemory.flush() + break + else: + time.sleep(1) # give enough time for buffer to be available + + return False + else: + self._timer = wx.PyTimer(self.DoBackgroundListenAndLoad) + self._timer.Start(250) + + return True + + + def OpenMainFrame(self): + docManager = self.GetDocumentManager() + if docManager.GetFlags() & wx.lib.docview.DOC_MDI: + if self.GetUseTabbedMDI(): + frame = wx.lib.pydocview.DocTabbedParentFrame(docManager, None, -1, self.GetAppName()) + else: + frame = wx.lib.pydocview.DocMDIParentFrame(docManager, None, -1, self.GetAppName()) + frame.Show(True) + + def MacOpenFile(self, filename): + self.GetDocumentManager().CreateDocument(os.path.normpath(filename), wx.lib.docview.DOC_SILENT) + + # force display of running app + topWindow = wx.GetApp().GetTopWindow() + if topWindow.IsIconized(): + topWindow.Iconize(False) + else: + topWindow.Raise() + + def DoBackgroundListenAndLoad(self): + """ + Open any files specified in the given command line argument passed in via shared memory + """ + self._timer.Stop() + + self._sharedMemory.seek(0) + if self._sharedMemory.read_byte() == '+': # available data + data = self._sharedMemory.read(1024-1) + self._sharedMemory.seek(0) + self._sharedMemory.write_byte("*") # finished reading, set buffer free marker + self._sharedMemory.flush() + args = pickle.loads(data) + for arg in args: + if (wx.Platform != "__WXMSW__" or arg[0] != "/") and arg[0] != '-' and os.path.exists(arg): + self.GetDocumentManager().CreateDocument(os.path.normpath(arg), wx.lib.docview.DOC_SILENT) + + # force display of running app + topWindow = wx.GetApp().GetTopWindow() + if topWindow.IsIconized(): + topWindow.Iconize(False) + else: + topWindow.Raise() + + + self._timer.Start(1000) # 1 second interval + + + def OpenCommandLineArgs(self): + """ + Called to open files that have been passed to the application from the + command line. + """ + args = sys.argv[1:] + for arg in args: + if (wx.Platform != "__WXMSW__" or arg[0] != "/") and arg[0] != '-' and os.path.exists(arg): + self.GetDocumentManager().CreateDocument(os.path.normpath(arg), wx.lib.docview.DOC_SILENT) + + + def GetDocumentManager(self): + """ + Returns the document manager associated to the DocApp. + """ + return self._docManager + + + def SetDocumentManager(self, docManager): + """ + Sets the document manager associated with the DocApp and loads the + DocApp's file history into the document manager. + """ + self._docManager = docManager + config = wx.ConfigBase_Get() + self.GetDocumentManager().FileHistoryLoad(config) + + + def ProcessEventBeforeWindows(self, event): + """ + Enables services to process an event before the main window has a chance to + process the window. + """ + for service in self._services: + if service.ProcessEventBeforeWindows(event): + return True + return False + + + def ProcessUpdateUIEventBeforeWindows(self, event): + """ + Enables services to process a UI event before the main window has a chance + to process the window. + """ + for service in self._services: + if service.ProcessUpdateUIEventBeforeWindows(event): + return True + return False + + + def ProcessEvent(self, event): + """ + Processes an event, searching event tables and calling zero or more + suitable event handler function(s). Note that the ProcessEvent + method is called from the wxPython docview framework directly since + wxPython does not have a virtual ProcessEvent function. + """ + for service in self._services: + if service.ProcessEvent(event): + return True + return False + + + def ProcessUpdateUIEvent(self, event): + """ + Processes a UI event, searching event tables and calling zero or more + suitable event handler function(s). Note that the ProcessEvent + method is called from the wxPython docview framework directly since + wxPython does not have a virtual ProcessEvent function. + """ + for service in self._services: + if service.ProcessUpdateUIEvent(event): + return True + return False + + + def InstallService(self, service): + """ + Installs an instance of a DocService into the DocApp. + """ + service.SetDocumentManager(self._docManager) + self._services.append(service) + return service + + + def GetServices(self): + """ + Returns the DocService instances that have been installed into the DocApp. + """ + return self._services + + + def GetService(self, type): + """ + Returns the instance of a particular type of service that has been installed + into the DocApp. For example, "wx.GetApp().GetService(pydocview.OptionsService)" + returns the isntance of the OptionsService that is running within the DocApp. + """ + for service in self._services: + if isinstance(service, type): + return service + return None + + + def OnExit(self): + """ + Called when the DocApp is exited, enables the installed DocServices to exit + and saves the DocManager's file history. + """ + for service in self._services: + service.OnExit() + config = wx.ConfigBase_Get() + self._docManager.FileHistorySave(config) + + if hasattr(self, "_singleInstanceChecker"): + del self._singleInstanceChecker + + + def GetDefaultDocManagerFlags(self): + """ + Returns the default flags to use when creating the DocManager. + """ + config = wx.ConfigBase_Get() + if config.ReadInt("UseMDI", True) or config.ReadInt("UseWinMDI", False): + flags = wx.lib.docview.DOC_MDI | wx.lib.docview.DOC_OPEN_ONCE + if config.ReadInt("UseWinMDI", False): + self.SetUseTabbedMDI(False) + else: + flags = wx.lib.docview.DOC_SDI | wx.lib.docview.DOC_OPEN_ONCE + return flags + + + def ShowTip(self, frame, tipProvider): + """ + Shows the tip window, generally this is called when an application starts. + A wx.TipProvider must be passed. + """ + config = wx.ConfigBase_Get() + showTip = config.ReadInt("ShowTipAtStartup", 1) + if showTip: + index = config.ReadInt("TipIndex", 0) + showTipResult = wx.ShowTip(wx.GetApp().GetTopWindow(), tipProvider, showAtStartup = showTip) + if showTipResult != showTip: + config.WriteInt("ShowTipAtStartup", showTipResult) + + + def GetEditMenu(self, frame): + """ + Utility method that finds the Edit menu within the menubar of a frame. + """ + menuBar = frame.GetMenuBar() + if not menuBar: + return None + editMenuIndex = menuBar.FindMenu(_("&Edit")) + if editMenuIndex == -1: + return None + return menuBar.GetMenu(editMenuIndex) + + + def GetUseTabbedMDI(self): + """ + Returns True if Windows MDI should use folder tabs instead of child windows. + """ + return self._useTabbedMDI + + + def SetUseTabbedMDI(self, useTabbedMDI): + """ + Set to True if Windows MDI should use folder tabs instead of child windows. + """ + self._useTabbedMDI = useTabbedMDI + + + def CreateDocumentFrame(self, view, doc, flags, id = -1, title = "", pos = wx.DefaultPosition, size = wx.DefaultSize, style = wx.DEFAULT_FRAME_STYLE): + """ + Called by the DocManager to create and return a new Frame for a Document. + Chooses whether to create an MDIChildFrame or SDI Frame based on the + DocManager's flags. + """ + docflags = self.GetDocumentManager().GetFlags() + if docflags & wx.lib.docview.DOC_SDI: + frame = self.CreateSDIDocumentFrame(doc, view, id, title, pos, size, style) + frame.Show() + + # wxBug: operating system bug, first window is set to the position of last window closed, ignoring passed in position on frame creation + # also, initial size is incorrect for the same reasons + if frame.GetPosition() != pos: + frame.Move(pos) + if frame.GetSize() != size: + frame.SetSize(size) + + if doc and doc.GetCommandProcessor(): + doc.GetCommandProcessor().SetEditMenu(self.GetEditMenu(frame)) + elif docflags & wx.lib.docview.DOC_MDI: + if self.GetUseTabbedMDI(): + frame = self.CreateTabbedDocumentFrame(doc, view, id, title, pos, size, style) + else: + frame = self.CreateMDIDocumentFrame(doc, view, id, title, pos, size, style) + if doc: + if doc.GetDocumentTemplate().GetIcon(): + frame.SetIcon(doc.GetDocumentTemplate().GetIcon()) + elif wx.GetApp().GetTopWindow().GetIcon(): + frame.SetIcon(wx.GetApp().GetTopWindow().GetIcon()) + if doc and doc.GetCommandProcessor(): + doc.GetCommandProcessor().SetEditMenu(self.GetEditMenu(wx.GetApp().GetTopWindow())) + if not frame.GetIcon() and self._defaultIcon: + frame.SetIcon(self.GetDefaultIcon()) + view.SetFrame(frame) + return frame + + + def CreateSDIDocumentFrame(self, doc, view, id=-1, title="", pos=wx.DefaultPosition, size=wx.DefaultSize, style=wx.DEFAULT_FRAME_STYLE): + """ + Creates and returns an SDI Document Frame. + """ + frame = DocSDIFrame(doc, view, None, id, title, pos, size, style) + return frame + + + def CreateTabbedDocumentFrame(self, doc, view, id=-1, title="", pos=wx.DefaultPosition, size=wx.DefaultSize, style=wx.DEFAULT_FRAME_STYLE): + """ + Creates and returns an MDI Document Frame for a Tabbed MDI view + """ + frame = DocTabbedChildFrame(doc, view, wx.GetApp().GetTopWindow(), id, title, pos, size, style) + return frame + + + def CreateMDIDocumentFrame(self, doc, view, id=-1, title="", pos=wx.DefaultPosition, size=wx.DefaultSize, style=wx.DEFAULT_FRAME_STYLE): + """ + Creates and returns an MDI Document Frame. + """ + # if any child windows are maximized, then user must want any new children maximized + # if no children exist, then use the default value from registry + # wxBug: Only current window is maximized, so need to check every child frame + parentFrame = wx.GetApp().GetTopWindow() + childrenMaximized = filter(lambda child: isinstance(child, wx.MDIChildFrame) and child.IsMaximized(), parentFrame.GetChildren()) + if childrenMaximized: + maximize = True + else: + children = filter(lambda child: isinstance(child, wx.MDIChildFrame), parentFrame.GetChildren()) + if children: + # other windows exist and none are maximized + maximize = False + else: + # get default setting from registry + maximize = wx.ConfigBase_Get().ReadInt("MDIChildFrameMaximized", False) + + frame = wx.lib.docview.DocMDIChildFrame(doc, view, wx.GetApp().GetTopWindow(), id, title, pos, size, style) + if maximize: # wxBug: Should already be maximizing new child frames if one is maximized but it's not so we have to force it to + frame.Maximize(True) + +## wx.EVT_MAXIMIZE(frame, self.OnMaximize) # wxBug: This doesn't work, need to save MDIChildFrameMaximized state on close of windows instead + wx.EVT_CLOSE(frame, self.OnCloseChildWindow) + if not self._registeredCloseEvent: + wx.EVT_CLOSE(parentFrame, self.OnCloseMainWindow) # need to check on this, but only once + self._registeredCloseEvent = True + + return frame + + + def SaveMDIDocumentFrameMaximizedState(self, maximized): + """ + Remember in the config whether the MDI Frame is maximized so that it can be restored + on open. + """ + config = wx.ConfigBase_Get() + maximizeFlag = config.ReadInt("MDIChildFrameMaximized", False) + if maximized != maximizeFlag: + config.WriteInt("MDIChildFrameMaximized", maximized) + + + def OnCloseChildWindow(self, event): + """ + Called when an MDI Child Frame is closed. Calls SaveMDIDocumentFrameMaximizedState to + remember whether the MDI Frame is maximized so that it can be restored on open. + """ + self.SaveMDIDocumentFrameMaximizedState(event.GetEventObject().IsMaximized()) + event.Skip() + + + def OnCloseMainWindow(self, event): + """ + Called when the MDI Parent Frame is closed. Remembers whether the MDI Parent Frame is + maximized. + """ + children = event.GetEventObject().GetChildren() + childrenMaximized = filter(lambda child: isinstance(child, wx.MDIChildFrame)and child.IsMaximized(), children) + if childrenMaximized: + self.SaveMDIDocumentFrameMaximizedState(True) + else: + childrenNotMaximized = filter(lambda child: isinstance(child, wx.MDIChildFrame), children) + + if childrenNotMaximized: + # other windows exist and none are maximized + self.SaveMDIDocumentFrameMaximizedState(False) + + event.Skip() + + + def GetDefaultIcon(self): + """ + Returns the application's default icon. + """ + return self._defaultIcon + + + def SetDefaultIcon(self, icon): + """ + Sets the application's default icon. + """ + self._defaultIcon = icon + + + def GetDebug(self): + """ + Returns True if the application is in debug mode. + """ + return self._debug + + + def SetDebug(self, debug): + """ + Sets the application's debug mode. + """ + self._debug = debug + + + def GetSingleInstance(self): + """ + Returns True if the application is in single instance mode. Used to determine if multiple instances of the application is allowed to launch. + """ + return self._singleInstance + + + def SetSingleInstance(self, singleInstance): + """ + Sets application's single instance mode. + """ + self._singleInstance = singleInstance + + + + def CreateChildDocument(self, parentDocument, documentType, objectToEdit, path=''): + """ + Creates a child window of a document that edits an object. The child window + is managed by the parent document frame, so it will be prompted to close if its + parent is closed, etc. Child Documents are useful when there are complicated + Views of a Document and users will need to tunnel into the View. + """ + for document in self.GetDocumentManager().GetDocuments()[:]: # Cloning list to make sure we go through all docs even as they are deleted + if isinstance(document, ChildDocument) and document.GetParentDocument() == parentDocument: + if document.GetData() == objectToEdit: + if hasattr(document.GetFirstView().GetFrame(), "SetFocus"): + document.GetFirstView().GetFrame().SetFocus() + return document + for temp in wx.GetApp().GetDocumentManager().GetTemplates(): + if temp.GetDocumentType() == documentType: + break + temp = None + newDoc = temp.CreateDocument(path, 0, data = objectToEdit, parentDocument = parentDocument) + newDoc.SetDocumentName(temp.GetDocumentName()) + newDoc.SetDocumentTemplate(temp) + if path == '': + newDoc.OnNewDocument() + else: + if not newDoc.OnOpenDocument(path): + newDoc.DeleteAllViews() # Implicitly deleted by DeleteAllViews + return None + return newDoc + + + def CloseChildDocuments(self, parentDocument): + """ + Closes the child windows of a Document. + """ + for document in self.GetDocumentManager().GetDocuments()[:]: # Cloning list to make sure we go through all docs even as they are deleted + if isinstance(document, ChildDocument) and document.GetParentDocument() == parentDocument: + if document.GetFirstView().GetFrame(): + document.GetFirstView().GetFrame().SetFocus() + if not document.GetFirstView().OnClose(): + return False + return True + + + def IsMDI(self): + """ + Returns True if the application is in MDI mode. + """ + return self.GetDocumentManager().GetFlags() & wx.lib.docview.DOC_MDI + + + def IsSDI(self): + """ + Returns True if the application is in SDI mode. + """ + return self.GetDocumentManager().GetFlags() & wx.lib.docview.DOC_SDI + + + def ShowSplash(self, image): + """ + Shows a splash window with the given image. Input parameter 'image' can either be a wx.Bitmap or a filename. + """ + if isinstance(image, wx.Bitmap): + splash_bmp = image + else: + splash_bmp = wx.Image(image).ConvertToBitmap() + self._splash = wx.SplashScreen(splash_bmp, wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_NO_TIMEOUT, 0, None, -1, style=wx.SIMPLE_BORDER|wx.FRAME_NO_TASKBAR) + self._splash.Show() + + + def CloseSplash(self): + """ + Closes the splash window. + """ + if self._splash: + self._splash.Close(True) + + +class _DocFrameFileDropTarget(wx.FileDropTarget): + """ + Class used to handle drops into the document frame. + """ + + def __init__(self, docManager, docFrame): + """ + Initializes the FileDropTarget class with the active docManager and the docFrame. + """ + wx.FileDropTarget.__init__(self) + self._docManager = docManager + self._docFrame = docFrame + + + def OnDropFiles(self, x, y, filenames): + """ + Called when files are dropped in the drop target and tells the docManager to open + the files. + """ + try: + for file in filenames: + self._docManager.CreateDocument(file, wx.lib.docview.DOC_SILENT) + except: + msgTitle = wx.GetApp().GetAppName() + if not msgTitle: + msgTitle = _("File Error") + wx.MessageBox("Could not open '%s'. '%s'" % (wx.lib.docview.FileNameFromPath(file), sys.exc_value), + msgTitle, + wx.OK | wx.ICON_EXCLAMATION, + self._docManager.FindSuitableParent()) + + +class DocMDIParentFrame(wx.lib.docview.DocMDIParentFrame, DocFrameMixIn, DocMDIParentFrameMixIn): + """ + The DocMDIParentFrame is the primary frame which the DocApp uses to host MDI child windows. It offers + features such as a default menubar, toolbar, and status bar, and a mechanism to manage embedded windows + on the edges of the DocMDIParentFrame. + """ + + + def __init__(self, docManager, parent, id, title, pos=wx.DefaultPosition, size=wx.DefaultSize, style=wx.DEFAULT_FRAME_STYLE, name="DocMDIFrame", embeddedWindows=0, minSize=20): + """ + Initializes the DocMDIParentFrame with the default menubar, toolbar, and status bar. Use the + optional embeddedWindows parameter with the embedded window constants to create embedded + windows around the edges of the DocMDIParentFrame. + """ + pos, size = self._GetPosSizeFromConfig(pos, size) + wx.lib.docview.DocMDIParentFrame.__init__(self, docManager, parent, id, title, pos, size, style, name) + self._InitFrame(embeddedWindows, minSize) + + + def _LayoutFrame(self): + """ + Lays out the frame. + """ + wx.LayoutAlgorithm().LayoutMDIFrame(self) + self.GetClientWindow().Refresh() + + + def ProcessEvent(self, event): + """ + Processes an event, searching event tables and calling zero or more + suitable event handler function(s). Note that the ProcessEvent + method is called from the wxPython docview framework directly since + wxPython does not have a virtual ProcessEvent function. + """ + if wx.GetApp().ProcessEventBeforeWindows(event): + return True + if wx.lib.docview.DocMDIParentFrame.ProcessEvent(self, event): + return True + return DocMDIParentFrameMixIn.ProcessEvent(self, event) + + + def ProcessUpdateUIEvent(self, event): + """ + Processes a UI event, searching event tables and calling zero or more + suitable event handler function(s). Note that the ProcessEvent + method is called from the wxPython docview framework directly since + wxPython does not have a virtual ProcessEvent function. + """ + if wx.GetApp().ProcessUpdateUIEventBeforeWindows(event): + return True + if wx.lib.docview.DocMDIParentFrame.ProcessUpdateUIEvent(self, event): # Let the views handle the event before the services + return True + if event.GetId() == wx.ID_ABOUT: # Using ID_ABOUT to update the window menu, the window menu items are not triggering + self.UpdateWindowMenu() + return True + return DocMDIParentFrameMixIn.ProcessUpdateUIEvent(self, event) + + + def UpdateWindowMenu(self): + """ + Updates the WindowMenu on Windows platforms. + """ + if wx.Platform == '__WXMSW__': + children = filter(lambda child: isinstance(child, wx.MDIChildFrame), self.GetChildren()) + windowCount = len(children) + hasWindow = windowCount >= 1 + has2OrMoreWindows = windowCount >= 2 + + windowMenu = self.GetWindowMenu() + if windowMenu: + windowMenu.Enable(wx.IDM_WINDOWTILE, hasWindow) + windowMenu.Enable(wx.IDM_WINDOWTILEHOR, hasWindow) + windowMenu.Enable(wx.IDM_WINDOWCASCADE, hasWindow) + windowMenu.Enable(wx.IDM_WINDOWICONS, hasWindow) + windowMenu.Enable(wx.IDM_WINDOWTILEVERT, hasWindow) + wx.IDM_WINDOWPREV = 4006 # wxBug: Not defined for some reason + windowMenu.Enable(wx.IDM_WINDOWPREV, has2OrMoreWindows) + windowMenu.Enable(wx.IDM_WINDOWNEXT, has2OrMoreWindows) + + + + def OnSize(self, event): + """ + Called when the DocMDIParentFrame is resized and lays out the MDI client window. + """ + # Needed in case there are splitpanels around the mdi frame + self._LayoutFrame() + + + def OnCloseWindow(self, event): + """ + Called when the DocMDIParentFrame is closed. Remembers the frame size. + """ + self.SaveEmbeddedWindowSizes() + + # save and close services last. + for service in wx.GetApp().GetServices(): + if not service.OnCloseFrame(event): + return + + # save and close documents + # documents with a common view, e.g. project view, should save the document, but not close the window + # and let the service close the window. + wx.lib.docview.DocMDIParentFrame.OnCloseWindow(self, event) + + +class DocSDIFrame(wx.lib.docview.DocChildFrame, DocFrameMixIn): + """ + The DocSDIFrame host DocManager Document windows. It offers features such as a default menubar, + toolbar, and status bar. + """ + + + def __init__(self, doc, view, parent, id, title, pos=wx.DefaultPosition, size=wx.DefaultSize, style=wx.DEFAULT_FRAME_STYLE, name="DocSDIFrame"): + """ + Initializes the DocSDIFrame with the default menubar, toolbar, and status bar. + """ + wx.lib.docview.DocChildFrame.__init__(self, doc, view, parent, id, title, pos, size, style, name) + self._fileMenu = None + if doc: + self._docManager = doc.GetDocumentManager() + else: + self._docManager = None + self.SetDropTarget(_DocFrameFileDropTarget(self._docManager, self)) + + wx.EVT_MENU(self, wx.ID_ABOUT, self.OnAbout) + wx.EVT_MENU(self, wx.ID_EXIT, self.OnExit) + wx.EVT_MENU_RANGE(self, wx.ID_FILE1, wx.ID_FILE9, self.OnMRUFile) + + self.InitializePrintData() + + menuBar = self.CreateDefaultMenuBar(sdi=True) + toolBar = self.CreateDefaultToolBar() + self.SetToolBar(toolBar) + statusBar = self.CreateDefaultStatusBar() + + for service in wx.GetApp().GetServices(): + service.InstallControls(self, menuBar = menuBar, toolBar = toolBar, statusBar = statusBar, document = doc) + + self.SetMenuBar(menuBar) # wxBug: Need to do this in SDI to mimic MDI... because have to set the menubar at the very end or the automatic MDI "window" menu doesn't get put in the right place when the services add new menus to the menubar + + + def _LayoutFrame(self): + """ + Lays out the Frame. + """ + self.Layout() + + + def OnExit(self, event): + """ + Called when the application is exitting. + """ + self._childView.GetDocumentManager().Clear(force = False) + + + def OnMRUFile(self, event): + """ + Opens the appropriate file when it is selected from the file history + menu. + """ + n = event.GetId() - wx.ID_FILE1 + filename = self._docManager.GetHistoryFile(n) + if filename: + self._docManager.CreateDocument(filename, wx.lib.docview.DOC_SILENT) + else: + self._docManager.RemoveFileFromHistory(n) + msgTitle = wx.GetApp().GetAppName() + if not msgTitle: + msgTitle = _("File Error") + wx.MessageBox("The file '%s' doesn't exist and couldn't be opened.\nIt has been removed from the most recently used files list" % docview.FileNameFromPath(file), + msgTitle, + wx.OK | wx.ICON_EXCLAMATION, + self) + + + def ProcessEvent(self, event): + """ + Processes an event, searching event tables and calling zero or more + suitable event handler function(s). Note that the ProcessEvent + method is called from the wxPython docview framework directly since + wxPython does not have a virtual ProcessEvent function. + """ + if wx.GetApp().ProcessEventBeforeWindows(event): + return True + if self._childView: + self._childView.Activate(True) + + id = event.GetId() + if id == SAVEALL_ID: + self.OnFileSaveAll(event) + return True + + if hasattr(self._childView, "GetDocumentManager") and self._childView.GetDocumentManager().ProcessEvent(event): # Need to call docmanager here since super class relies on DocParentFrame which we are not using + return True + else: + return wx.GetApp().ProcessEvent(event) + + + def ProcessUpdateUIEvent(self, event): + """ + Processes a UI event, searching event tables and calling zero or more + suitable event handler function(s). Note that the ProcessEvent + method is called from the wxPython docview framework directly since + wxPython does not have a virtual ProcessEvent function. + """ + if wx.GetApp().ProcessUpdateUIEventBeforeWindows(event): + return True + if self._childView: + if hasattr(self._childView, "GetDocumentManager"): + docMgr = self._childView.GetDocumentManager() + if docMgr: + if docMgr.GetCurrentDocument() != self._childView.GetDocument(): + return False + if docMgr.ProcessUpdateUIEvent(event): # Let the views handle the event before the services + return True + id = event.GetId() + if id == wx.ID_CUT: + event.Enable(False) + return True + elif id == wx.ID_COPY: + event.Enable(False) + return True + elif id == wx.ID_PASTE: + event.Enable(False) + return True + elif id == wx.ID_CLEAR: + event.Enable(False) + return True + elif id == wx.ID_SELECTALL: + event.Enable(False) + return True + elif id == SAVEALL_ID: + filesModified = False + docs = wx.GetApp().GetDocumentManager().GetDocuments() + for doc in docs: + if doc.IsModified(): + filesModified = True + break + + event.Enable(filesModified) + return True + else: + return wx.GetApp().ProcessUpdateUIEvent(event) + + + def OnCloseWindow(self, event): + """ + Called when the window is saved. Enables services to help close the frame. + """ + for service in wx.GetApp().GetServices(): + service.OnCloseFrame(event) + wx.lib.docview.DocChildFrame.OnCloseWindow(self, event) + if self._fileMenu and self._docManager: + self._docManager.FileHistoryRemoveMenu(self._fileMenu) + + +class AboutService(DocService): + """ + About Dialog Service that installs under the Help menu to show the properties of the current application. + """ + + def __init__(self, aboutDialog=None, image=None): + """ + Initializes the AboutService. + """ + if aboutDialog: + self._dlg = aboutDialog + self._image = None + else: + self._dlg = AboutDialog # use default AboutDialog + self._image = image + + + def ShowAbout(self): + """ + Show the AboutDialog + """ + if self._image: + dlg = self._dlg(wx.GetApp().GetTopWindow(), self._image) + else: + dlg = self._dlg(wx.GetApp().GetTopWindow()) + dlg.CenterOnParent() + dlg.ShowModal() + dlg.Destroy() + + + def SetAboutDialog(self, dlg): + """ + Customize the AboutDialog + """ + self._dlg = dlg + + +class AboutDialog(wx.Dialog): + """ + Opens an AboutDialog. Shared by DocMDIParentFrame and DocSDIFrame. + """ + + def __init__(self, parent, image=None): + """ + Initializes the about dialog. + """ + wx.Dialog.__init__(self, parent, -1, _("About ") + wx.GetApp().GetAppName(), style = wx.DEFAULT_DIALOG_STYLE) + + sizer = wx.BoxSizer(wx.VERTICAL) + if image: + imageItem = wx.StaticBitmap(self, -1, image.ConvertToBitmap(), (0,0), (image.GetWidth(), image.GetHeight())) + sizer.Add(imageItem, 0, wx.ALIGN_CENTER|wx.ALL, 0) + sizer.Add(wx.StaticText(self, -1, wx.GetApp().GetAppName()), 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) + sizer.Fit(self) + + + +class FilePropertiesService(DocService): + """ + Service that installs under the File menu to show the properties of the file associated + with the current document. + """ + + PROPERTIES_ID = wx.NewId() + + + def __init__(self): + """ + Initializes the PropertyService. + """ + self._customEventHandlers = [] + + + def InstallControls(self, frame, menuBar=None, toolBar=None, statusBar=None, document=None): + """ + Installs a File/Properties menu item. + """ + fileMenu = menuBar.GetMenu(menuBar.FindMenu(_("&File"))) + exitMenuItemPos = self.GetMenuItemPos(fileMenu, wx.ID_EXIT) + fileMenu.InsertSeparator(exitMenuItemPos) + fileMenu.Insert(exitMenuItemPos, FilePropertiesService.PROPERTIES_ID, _("&Properties"), _("Show file properties")) + wx.EVT_MENU(frame, FilePropertiesService.PROPERTIES_ID, self.ProcessEvent) + wx.EVT_UPDATE_UI(frame, FilePropertiesService.PROPERTIES_ID, self.ProcessUpdateUIEvent) + + + def ProcessEvent(self, event): + """ + Detects when the File/Properties menu item is selected. + """ + id = event.GetId() + if id == FilePropertiesService.PROPERTIES_ID: + for eventHandler in self._customEventHandlers: + if eventHandler.ProcessEvent(event): + return True + + self.ShowPropertiesDialog() + return True + else: + return False + + + def ProcessUpdateUIEvent(self, event): + """ + Updates the File/Properties menu item. + """ + id = event.GetId() + if id == FilePropertiesService.PROPERTIES_ID: + for eventHandler in self._customEventHandlers: + if eventHandler.ProcessUpdateUIEvent(event): + return True + + event.Enable(wx.GetApp().GetDocumentManager().GetCurrentDocument() != None) + return True + else: + return False + + + def ShowPropertiesDialog(self, filename=None): + """ + Shows the PropertiesDialog for the specified file. + """ + if not filename: + filename = wx.GetApp().GetDocumentManager().GetCurrentDocument().GetFilename() + + filePropertiesDialog = FilePropertiesDialog(wx.GetApp().GetTopWindow(), filename) + filePropertiesDialog.CenterOnParent() + if filePropertiesDialog.ShowModal() == wx.ID_OK: + pass # Handle OK + filePropertiesDialog.Destroy() + + + def GetCustomEventHandlers(self): + """ + Returns the custom event handlers for the PropertyService. + """ + return self._customEventHandlers + + + def AddCustomEventHandler(self, handler): + """ + Adds a custom event handlers for the PropertyService. A custom event handler enables + a different dialog to be provided for a particular file. + """ + self._customEventHandlers.append(handler) + + + def RemoveCustomEventHandler(self, handler): + """ + Removes a custom event handler from the PropertyService. + """ + self._customEventHandlers.remove(handler) + + + def chopPath(self, text, length=36): + """ + Simple version of textwrap. textwrap.fill() unfortunately chops lines at spaces + and creates odd word boundaries. Instead, we will chop the path without regard to + spaces, but pay attention to path delimiters. + """ + chopped = "" + textLen = len(text) + start = 0 + + while start < textLen: + end = start + length + if end > textLen: + end = textLen + + # see if we can find a delimiter to chop the path + if end < textLen: + lastSep = text.rfind(os.sep, start, end + 1) + if lastSep != -1 and lastSep != start: + end = lastSep + + if len(chopped): + chopped = chopped + '\n' + text[start:end] + else: + chopped = text[start:end] + + start = end + + return chopped + + +class FilePropertiesDialog(wx.Dialog): + """ + Dialog that shows the properties of a file. Invoked by the PropertiesService. + """ + + + def __init__(self, parent, filename): + """ + Initializes the properties dialog. + """ + wx.Dialog.__init__(self, parent, -1, _("File Properties"), size=(310, 330)) + + HALF_SPACE = 5 + SPACE = 10 + + filePropertiesService = wx.GetApp().GetService(FilePropertiesService) + + fileExists = os.path.exists(filename) + + 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) + gridSizer.Add(wx.StaticText(tab, -1, os.path.basename(filename)), 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.dirname(filename))), 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) + if fileExists: + 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) + if fileExists: + 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) + if fileExists: + 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) + if fileExists: + gridSizer.Add(wx.StaticText(tab, -1, time.ctime(os.path.getatime(filename))), row=6, 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")) + + 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 ChildDocument(wx.lib.docview.Document): + """ + A ChildDocument is a document that represents a portion of a Document. The child + document is managed by the parent document, so it will be prompted to close if its + parent is closed, etc. Child Documents are useful when there are complicated + Views of a Document and users will need to tunnel into the View. + """ + + + def GetData(self): + """ + Returns the data that the ChildDocument contains. + """ + return self._data + + + def SetData(self, data): + """ + Sets the data that the ChildDocument contains. + """ + self._data = data + + + def GetParentDocument(self): + """ + Returns the parent Document of the ChildDocument. + """ + return self._parentDocument + + + def SetParentDocument(self, parentDocument): + """ + Sets the parent Document of the ChildDocument. + """ + self._parentDocument = parentDocument + + + def OnSaveDocument(self, filename): + """ + Called when the ChildDocument is saved and does the minimum such that the + ChildDocument looks like a real Document to the framework. + """ + self.SetFilename(filename, True) + self.Modify(False) + self.SetDocumentSaved(True) + return True + + + def OnOpenDocument(self, filename): + """ + Called when the ChildDocument is opened and does the minimum such that the + ChildDocument looks like a real Document to the framework. + """ + self.SetFilename(filename, True) + self.Modify(False) + self.SetDocumentSaved(True) + self.UpdateAllViews() + return True + + + def Save(self): + """ + Called when the ChildDocument is saved and does the minimum such that the + ChildDocument looks like a real Document to the framework. + """ + return self.OnSaveDocument(self._documentFile) + + + def SaveAs(self): + """ + Called when the ChildDocument is saved and does the minimum such that the + ChildDocument looks like a real Document to the framework. + """ + return self.OnSaveDocument(self._documentFile) + + +class ChildDocTemplate(wx.lib.docview.DocTemplate): + """ + A ChildDocTemplate is a DocTemplate subclass that enables the creation of ChildDocuments + that represents a portion of a Document. The child document is managed by the parent document, + so it will be prompted to close if its parent is closed, etc. Child Documents are useful + when there are complicated Views of a Document and users will need to tunnel into the View. + """ + + + def __init__(self, manager, description, filter, dir, ext, docTypeName, viewTypeName, docType, viewType, flags=wx.lib.docview.TEMPLATE_INVISIBLE, icon=None): + """ + Initializes the ChildDocTemplate. + """ + wx.lib.docview.DocTemplate.__init__(self, manager, description, filter, dir, ext, docTypeName, viewTypeName, docType, viewType, flags=flags, icon=icon) + + + def CreateDocument(self, path, flags, data=None, parentDocument=None): + """ + Called when a ChildDocument is to be created and does the minimum such that the + ChildDocument looks like a real Document to the framework. + """ + doc = self._docType() + doc.SetFilename(path) + doc.SetData(data) + doc.SetParentDocument(parentDocument) + doc.SetDocumentTemplate(self) + self.GetDocumentManager().AddDocument(doc) + doc.SetCommandProcessor(doc.OnCreateCommandProcessor()) + if doc.OnCreate(path, flags): + return doc + else: + if doc in self.GetDocumentManager().GetDocuments(): + doc.DeleteAllViews() + return None + + +class WindowMenuService(DocService): + """ + The WindowMenuService is a service that implements a standard Window menu that is used + by the DocSDIFrame. The MDIFrame automatically includes a Window menu and does not use + the WindowMenuService. + """ + + #---------------------------------------------------------------------------- + # Constants + #---------------------------------------------------------------------------- + ARRANGE_WINDOWS_ID = wx.NewId() + SELECT_MORE_WINDOWS_ID = wx.NewId() + SELECT_NEXT_WINDOW_ID = wx.NewId() + SELECT_PREV_WINDOW_ID = wx.NewId() + CLOSE_CURRENT_WINDOW_ID = wx.NewId() + + + def __init__(self): + """ + Initializes the WindowMenu and its globals. + """ + self._selectWinIds = [] + for i in range(0, 9): + self._selectWinIds.append(wx.NewId()) + + + def InstallControls(self, frame, menuBar=None, toolBar=None, statusBar=None, document=None): + """ + Installs the Window menu. + """ + + windowMenu = None + if hasattr(frame, "GetWindowMenu"): + windowMenu = frame.GetWindowMenu() + if not windowMenu: + needWindowMenu = True + windowMenu = wx.Menu() + else: + needWindowMenu = False + + if self.GetDocumentManager().GetFlags() & wx.lib.docview.DOC_SDI: + if not _WINDOWS: # Arrange All and window navigation doesn't work on Linux + return + + item = windowMenu.Append(self.ARRANGE_WINDOWS_ID, _("&Arrange All"), _("Arrange the open windows")) + wx.EVT_MENU(frame, self.ARRANGE_WINDOWS_ID, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, self.ARRANGE_WINDOWS_ID, frame.ProcessUpdateUIEvent) + windowMenu.AppendSeparator() + + for i, id in enumerate(self._selectWinIds): + wx.EVT_MENU(frame, id, frame.ProcessEvent) + wx.EVT_MENU(frame, self.SELECT_MORE_WINDOWS_ID, frame.ProcessEvent) + elif wx.GetApp().GetUseTabbedMDI(): + item = windowMenu.Append(self.SELECT_PREV_WINDOW_ID, _("Previous"), _("Previous Tab")) + wx.EVT_MENU(frame, self.SELECT_PREV_WINDOW_ID, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, self.SELECT_PREV_WINDOW_ID, frame.ProcessUpdateUIEvent) + item = windowMenu.Append(self.SELECT_NEXT_WINDOW_ID, _("Next"), _("Next Tab")) + wx.EVT_MENU(frame, self.SELECT_NEXT_WINDOW_ID, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, self.SELECT_NEXT_WINDOW_ID, frame.ProcessUpdateUIEvent) + item = windowMenu.Append(self.CLOSE_CURRENT_WINDOW_ID, _("Close Current\tCtrl+F4"), _("Close Current Tab")) + wx.EVT_MENU(frame, self.CLOSE_CURRENT_WINDOW_ID, frame.ProcessEvent) + wx.EVT_UPDATE_UI(frame, self.CLOSE_CURRENT_WINDOW_ID, frame.ProcessUpdateUIEvent) + self._sep = None + + for i, id in enumerate(self._selectWinIds): + wx.EVT_MENU(frame, id, self.OnCtrlKeySelect) + + if needWindowMenu: + helpMenuIndex = menuBar.FindMenu(_("&Help")) + menuBar.Insert(helpMenuIndex, windowMenu, _("&Window")) + + self._lastFrameUpdated = None + + + def OnCtrlKeySelect(self, event): + i = self._selectWinIds.index(event.GetId()) + notebook = wx.GetApp().GetTopWindow()._notebook + if i < notebook.GetPageCount(): + notebook.SetSelection(i) + + + def ProcessEvent(self, event): + """ + Processes a Window menu event. + """ + id = event.GetId() + if id == self.ARRANGE_WINDOWS_ID: + self.OnArrangeWindows(event) + return True + elif id == self.SELECT_MORE_WINDOWS_ID: + self.OnSelectMoreWindows(event) + return True + elif id in self._selectWinIds: + self.OnSelectWindowMenu(event) + return True + elif wx.GetApp().GetUseTabbedMDI(): + if id == self.SELECT_NEXT_WINDOW_ID: + notebook = wx.GetApp().GetTopWindow()._notebook + i = notebook.GetSelection() + notebook.SetSelection(i+1) + return True + elif id == self.SELECT_PREV_WINDOW_ID: + notebook = wx.GetApp().GetTopWindow()._notebook + i = notebook.GetSelection() + notebook.SetSelection(i-1) + return True + elif id == self.CLOSE_CURRENT_WINDOW_ID: + notebook = wx.GetApp().GetTopWindow()._notebook + i = notebook.GetSelection() + if i != -1: + doc = notebook.GetPage(i).GetView().GetDocument() + wx.GetApp().GetDocumentManager().CloseDocument(doc, False) + return True + else: + return False + + + def ProcessUpdateUIEvent(self, event): + """ + Updates the Window menu items. + """ + id = event.GetId() + if id == self.ARRANGE_WINDOWS_ID: + frame = event.GetEventObject() + if not self._lastFrameUpdated or self._lastFrameUpdated != frame: + self.BuildWindowMenu(frame) # It's a new frame, so update the windows menu... this is as if the View::OnActivateMethod had been invoked + self._lastFrameUpdated = frame + return True + elif wx.GetApp().GetUseTabbedMDI(): + if id == self.SELECT_NEXT_WINDOW_ID: + self.BuildWindowMenu(event.GetEventObject()) # build file list only when we are updating the windows menu + + notebook = wx.GetApp().GetTopWindow()._notebook + i = notebook.GetSelection() + if i == -1: + event.Enable(False) + return True + i += 1 + if i >= notebook.GetPageCount(): + event.Enable(False) + return True + event.Enable(True) + return True + elif id == self.SELECT_PREV_WINDOW_ID: + notebook = wx.GetApp().GetTopWindow()._notebook + i = notebook.GetSelection() + if i == -1: + event.Enable(False) + return True + i -= 1 + if i < 0: + event.Enable(False) + return True + event.Enable(True) + return True + elif id == self.CLOSE_CURRENT_WINDOW_ID: + event.Enable(wx.GetApp().GetTopWindow()._notebook.GetSelection() != -1) + return True + + return False + else: + return False + + + def BuildWindowMenu(self, currentFrame): + """ + Builds the Window menu and adds menu items for all of the open documents in the DocManager. + """ + if wx.GetApp().GetUseTabbedMDI(): + currentFrame = wx.GetApp().GetTopWindow() + + windowMenuIndex = currentFrame.GetMenuBar().FindMenu(_("&Window")) + windowMenu = currentFrame.GetMenuBar().GetMenu(windowMenuIndex) + + if self.GetDocumentManager().GetFlags() & wx.lib.docview.DOC_SDI: + frames = self._GetWindowMenuFrameList(currentFrame) + max = WINDOW_MENU_NUM_ITEMS + if max > len(frames): + max = len(frames) + i = 0 + for i in range(0, max): + frame = frames[i] + item = windowMenu.FindItemById(self._selectWinIds[i]) + label = '&' + str(i + 1) + ' ' + frame.GetTitle() + if not item: + item = windowMenu.AppendCheckItem(self._selectWinIds[i], label) + else: + windowMenu.SetLabel(self._selectWinIds[i], label) + windowMenu.Check(self._selectWinIds[i], (frame == currentFrame)) + if len(frames) > WINDOW_MENU_NUM_ITEMS: # Add the more items item + if not windowMenu.FindItemById(self.SELECT_MORE_WINDOWS_ID): + windowMenu.Append(self.SELECT_MORE_WINDOWS_ID, _("&More Windows...")) + else: # Remove any extra items + if windowMenu.FindItemById(self.SELECT_MORE_WINDOWS_ID): + windowMenu.Remove(self.SELECT_MORE_WINDOWS_ID) + + for j in range(i + 1, WINDOW_MENU_NUM_ITEMS): + if windowMenu.FindItemById(self._selectWinIds[j]): + windowMenu.Remove(self._selectWinIds[j]) + + elif wx.GetApp().GetUseTabbedMDI(): + notebook = wx.GetApp().GetTopWindow()._notebook + numPages = notebook.GetPageCount() + + for id in self._selectWinIds: + item = windowMenu.FindItemById(id) + if item: + windowMenu.DeleteItem(item) + if numPages == 0 and self._sep: + windowMenu.DeleteItem(self._sep) + self._sep = None + + if numPages > len(self._selectWinIds): + for i in range(len(self._selectWinIds), numPages): + self._selectWinIds.append(wx.NewId()) + wx.EVT_MENU(currentFrame, self._selectWinIds[i], self.OnCtrlKeySelect) + + for i in range(0, numPages): + if i == 0 and not self._sep: + self._sep = windowMenu.AppendSeparator() + if i < 9: + menuLabel = "%s\tCtrl+%s" % (notebook.GetPageText(i), i+1) + else: + menuLabel = notebook.GetPageText(i) + windowMenu.Append(self._selectWinIds[i], menuLabel) + + + def _GetWindowMenuIDList(self): + """ + Returns a list of the Window menu item IDs. + """ + return self._selectWinIds + + + def _GetWindowMenuFrameList(self, currentFrame=None): + """ + Returns the Frame associated with each menu item in the Window menu. + """ + frameList = [] + # get list of windows for documents + for doc in self._docManager.GetDocuments(): + for view in doc.GetViews(): + frame = view.GetFrame() + if frame not in frameList: + if frame == currentFrame and len(frameList) >= WINDOW_MENU_NUM_ITEMS: + frameList.insert(WINDOW_MENU_NUM_ITEMS - 1, frame) + else: + frameList.append(frame) + # get list of windows for general services + for service in wx.GetApp().GetServices(): + view = service.GetView() + if view: + frame = view.GetFrame() + if frame not in frameList: + if frame == currentFrame and len(frameList) >= WINDOW_MENU_NUM_ITEMS: + frameList.insert(WINDOW_MENU_NUM_ITEMS - 1, frame) + else: + frameList.append(frame) + + return frameList + + + def OnArrangeWindows(self, event): + """ + Called by Window/Arrange and tiles the frames on the desktop. + """ + currentFrame = event.GetEventObject() + + tempFrame = wx.Frame(None, -1, "", pos = wx.DefaultPosition, size = wx.DefaultSize) + sizex = tempFrame.GetSize()[0] + sizey = tempFrame.GetSize()[1] + tempFrame.Destroy() + + posx = 0 + posy = 0 + delta = 0 + frames = self._GetWindowMenuFrameList() + frames.remove(currentFrame) + frames.append(currentFrame) # Make the current frame the last frame so that it is the last one to appear + for frame in frames: + if delta == 0: + delta = frame.GetClientAreaOrigin()[1] + frame.SetPosition((posx, posy)) + frame.SetSize((sizex, sizey)) + # TODO: Need to loop around if posx + delta + size > displaysize + frame.SetFocus() + posx = posx + delta + posy = posy + delta + if posx + sizex > wx.DisplaySize()[0] or posy + sizey > wx.DisplaySize()[1]: + posx = 0 + posy = 0 + currentFrame.SetFocus() + + + def OnSelectWindowMenu(self, event): + """ + Called when the Window menu item representing a Frame is selected and brings the selected + Frame to the front of the desktop. + """ + id = event.GetId() + index = self._selectWinIds.index(id) + if index > -1: + currentFrame = event.GetEventObject() + frame = self._GetWindowMenuFrameList(currentFrame)[index] + if frame: + wx.CallAfter(frame.Raise) + + + def OnSelectMoreWindows(self, event): + """ + Called when the "Window/Select More Windows..." menu item is selected and enables user to + select from the Frames that do not in the Window list. Useful when there are more than + 10 open frames in the application. + """ + frames = self._GetWindowMenuFrameList() # TODO - make the current window the first one + strings = map(lambda frame: frame.GetTitle(), frames) + # Should preselect the current window, but not supported by wx.GetSingleChoice + res = wx.GetSingleChoiceIndex(_("Select a window to show:"), + _("Select Window"), + strings, + self) + if res == -1: + return + frames[res].SetFocus() + + + +def getBlankIcon(): + return Blank.GetIcon() + +#---------------------------------------------------------------------------- +# File generated by encode_bitmaps.py +#---------------------------------------------------------------------------- +from wx.lib.embeddedimage import PyEmbeddedImage + +New = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAQRJ" + "REFUOI2lk71qAkEQx397Z21l5WPkCYKFhYVvYeUr2FrYJ2X6gNjYWBlSxAeQBNFKCBcMclxW" + "4eT8gLU499i9D6I4MOzsMr//zsyyQjgu91jJ3HxMl0rHvgwB8NYBAFJGdFp1kVEQjpv45PNb" + "LXcq8a9t7M+DiRqtlOq+jJWZLxwX59pSyyV4aNRod1+VeW614MuQ6iUOT/G62cflvw/fWF3a" + "KRQwQQ0DPDZrbE/wd4R+78nKL2xBw0AC55lVgbcOqOztBBPeHP4RkDKyQMjCi9m8WMAENazB" + "IEpn5gjoKadv1bC/ywpkhngLnCtwCwypFn68X+ud0wPLM3Hvb7z6LxTZGR/7imGH8vcWAAAA" + "AElFTkSuQmCC") + +Open = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAWdJ" + "REFUOI2lkz1LQlEYx39Xbyk1FTk0REMEIdELTS3R0FSBurf4AVqa2/oIQUMQDrWGukRDYENN" + "YU7ZC1FGN+J6wWt6zcSX06DevFxvRf2nc87z/H/nec6LJLnc/EcyQPFiQwAYJc0SHF7cl34E" + "GOdLom9mBYD+jkDDyPJ6ShOsK+b6eChhgcqid4qGkYWqbkX3DODzzwNQfXwzl5MRVcyF0yZE" + "7vSIsvo1KasomSIAuYdjCzsZ8QuAuXBasgAA01TLpQAo5FVm17ZtvacO1onHosIEPF/f4xUa" + "tVa/Y6ubNlM3yUZJQ7/L4BUavullfK1AQ791NJ3sblGZ3GkCAOofGhVdYahwY0uuvz85ggLB" + "kCQbuoI86OuaXMsp3XwUchU8rbGrmZhiZGLUcafvZLsFp92PDi+dAYW8ClgrOEtcmeUCeBb2" + "ugIkyeU2H0Zb2otunnKnAsGQ7W9I7d8Yj0XFbwyOgL/qE6gYiTXnTLM6AAAAAElFTkSuQmCC") + +Copy = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAV9J" + "REFUOI2Nkr9LAmEYxz93+ifY0KDkdtAkTY1CERGlQn9AQ9BSQ3trOEWDNLQ1ChX9gGhoaNKt" + "SYKkIbiEytQ7pfJS4W3Q+/VeHn7h5V6ee76f9/u8d0qpogtGapjfANTqBgBm22Jvc1EhTKWK" + "LqpfIrCOzsui/CnETv5UKGqEsatU0UUsGXeAjZdXdz9BItVu8iqWjBNLxtFSGlpKY2FpHoD0" + "aprt/aLwAULnG6nZHT47A1jZWPdBVG9Ds+vGlmW2LR5u77k5OQPg6vJCAERrdYMpzYWMO31u" + "OQ2A0YdivgCzM8MEZttymjsDv0mGGv3gAapt7AyGDfaNe832OwCzJwHeP5o+upxINrcseH6q" + "Oj1Rn7nnH0Wut1y2H+CNFUgkmRs/EkDX39APCk4hkZie2AwQvT7eVexvCnD3+OtLFGZ2Rshk" + "c87/vbZ1KLyJvBf2nxRFjQSK3kRhymRzyh8MSdTaiEWdxAAAAABJRU5ErkJggg==") + +Paste = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAZBJ" + "REFUOI2Nkz9IAmEYh59Td7GpIXHIsD9C0JgQRFMQmIJD0RIazQ1iJq0aFDRGYFvUIGhCUEtD" + "g1CLNOSpdFCCmIhgJIKL2HDddael/ZZ7v+/9vc/3fnf3CoLBCEBN7HTRKHgSZWbKTi4vEVzc" + "06Zweo2CEguCwUhN7HQtY/eqIRDJ4F1yUqy0AcjlJeK7RjVffAypEAM9CkQyACTvnsnlJXJ5" + "Sd4/6Oh86atUF8DUC9gKbKhx/aMlt9l44a09wnG6wo77VefvAwCYbdbvp7IzydttBvuKj+B5" + "isDEEADAzUVCjZfXfWq8sOYhfplk0TEEoC1SJF0nqDRNNMoFcDh/B9Q/WphtcgeVppz2b3uw" + "zrsAsAPpw9jwK2g7ePn8y9ULaD5QencxPgtnpyldqlEuAGDbDA8AaE70b3t+PfmpDlmxindJ" + "8w5qBbCYHwB4LdWwzsPRfkxXmBWrzE2P/q8DAHcwzFP9Z237w9f3Kyt31RYPkgnk6XpOhr6n" + "Mar7TFmxOhAgKOMMPwPyX7lXPcIXHPiHmgMS17kAAAAASUVORK5CYII=") + +Save = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAR1J" + "REFUOI2dkz1Ow0AQhb/1oi3xEaCNQ4+CoOWnpuIEtHAAGvsCEVT0pKDBwVwBkFIixaSJUkQU" + "BCSiQAGxBEPhyLFgHUNeNTs/782MdpRyNPHFp1CCYf+3b/1AK5SjuW8iMqhLGIZShMGHyN3z" + "WHzfFxGRm2MkiiJx8oxaa6v60xge3xK67waA2xOVxRxrRQnWtvfsBEmSlKr/xFwd8NL9G0GZ" + "OsBC/rG78UAQBDPFjzavAOh1wF2eEAz7QG2SsL84zR61rG3bOxi1wF0lOH3NhSuZ1Y6/Mvv8" + "sGcfIVWs4Hle5jLGcNZoFI40c4nGpMtbqVb/R6C1zoqXalu047iQQCkn/b7X9eKD6nXsfncn" + "mhIAXDbD0qss7GBefAO233xRLqcViQAAAABJRU5ErkJggg==") + +SaveAll = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAVVJ" + "REFUOI2dk79Lw0Acxd8lAWfrIB2Ki4NNEekWiov/QKDdFCcXF0Ey6SwIyRSCbi5ubpreP+BS" + "O7g4mXboYIYKLiKBtrS0fh3SxF6SavUDx/1+9+5xxzwXhCmjHgSGM/3xMKy3j2UmLPJcEPkG" + "cc4pSX9M9NonMk2TiIgeLkCcc2KSjLh4LqhYNtB6smPRPzmJHPzXiYIE6586WvVsJzs54PHq" + "FMoSsOLraDggKSkAAMWygY7EsbVHQtk8IBT2CffvJipHYfaZAlkMJsDHCHjpAUEQoHnJFheY" + "3RxR0Y2fBTzPEzbPIxVixMnuGyzLivtBEMTt88MBAMBvA0qpJrOGY9NyIVvkNxQgfBTPtxNK" + "Tlo3q8Lp3W43nrs+ywGYyaBUk1mT20iiqio0TUO1Ws10IISY+ihT8vn83CswJsmpwYbzfR2/" + "DaxthPXCAgBQd+9SmWTxBXqU0GLLIFPzAAAAAElFTkSuQmCC") + +Print = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAadJ" + "REFUOI2lUz1Lw1AUPe+1IA72F7gkWBCngrQKXQVBB+tkJf4Cl4LgoIOjiLVF/QdqC3HqBxgo" + "ODo1FqJI8YtOEgl06YvdWp5DfGmT1A/wwOHmvtx73rnJe4TQEP6DsHg4bfa4afc9L/utB0+e" + "XZ8lAQVCQyA0hB29x/14sh1emZwf5o55tVrlol6QCqGDuTGyeztw8PzhxFfbiePLGaiqikq5" + "xIcN0OHEb/kvCAPAxfkZB4D7r53Fri02KDTtPppvQQF6cX7GFUWBoiju4tSEE+VIsME/BgWA" + "YrHoFrzaDlvMoWn3Mfx38vm8R4QUCgUei8Wg1a6xvZWBZVkjZ2232558Y3MfdzeXJGxabcQA" + "LC0uIJs/wXRUHinAGEMkEgFjzofpvuuolEucTkdlaLVrAIDRqCORSECW5QAlScLjSwuapkGS" + "JFc4vJJaJevpNW406gGrnU7H4yI5H0dyPu5Zc49yOp0GYwyGYXgKTMs7ux+E0BAq5RJXVRXN" + "N2Bm8sd6AICu68jlclhJrRIibmOlXOJ7Ryq67/qvAqLZdSDgP+ffQTQDwCfjgskYZ5COXQAA" + "AABJRU5ErkJggg==") + +PrintPreview = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAW1J" + "REFUOI2Nkr1LAmEcxz93GoVFBDU4OFhIYdEUIUFCDS69B20tQnkuTf4DTgUO7fkSNAQ1BBXc" + "2iY0tURDSoqBRQkugpE42PBwT3feGX3h4Xf3Oz7f38tzyt1DuUOXHotV+ZzYXVa6v5ulAvim" + "xi1nZT0MQHApzMHhpa2AzaCXhvtgNbrzp4nbKVn/FvEieyVztzfXnc2tbds4NgMDDkXCBNtQ" + "a8F5MkVkZtp5hObbk4SM2GiLU2v1atxkUGqOWGBDBpw/iuPvL6PrOrH9PdsuLCM02nYYLW0B" + "YtDJ5k7lLlRzy05weg6ifpELeAAtbelELVc+JWjAH1+/Fe/rkK/B4hh4PSKCuBXZgXlZZhjg" + "rCLAwBCExkQ0y/IjdcOybWB0ACYHRTTLXXguUUim7CQT+DNxXrQ0Xo+4pSJwnIijkeGdNQAU" + "RXXJeZyk67rlfbaaAWDBB/M5l6IoqqsXK9Vd4PVkQ5r8y8DJ0DD5ATHJh/O00XSvAAAAAElF" + "TkSuQmCC") + +Cut = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAUhJ" + "REFUOI2Fkk9LAlEUxX+jBX0bocCdtKpmNpNhEONeg2zfxqUbSUG0oJxN5SKEGP8QOLvwk7R2" + "FWiprxavhbxhdF7Tgbc599zzznv3YiSShE+n05GbXPjU67W1egINnh4fpI5vNRsSYNDvBXWt" + "QS6Xw3Xv10xazYa07WxEqzXQQQjBbDZlPl/EG4hvAUDeyQeRa9dVaZqW1nhrkygUzg1AHh4c" + "RcSe55FKpTjOnhh/GgAsF6uYtp1FCCFN04pEVzCMRFJbULFVo++PIrdDzCfuvXfxPC9ozkyG" + "Wp32CW8XuzJtOTB6xvchMxmSthzG3auINnaMacsJmhXCS6Q1ULcXbysALKafABRvK+yfFVi6" + "l/EJVEMc/l1lgHapzLjrMvv44vWuQbtU1uoiYxz0e3L7ZfVZP6fVgFfRdwo3a6PU7oGKGBbq" + "OIBfXKKLiCHSUwgAAAAASUVORK5CYII=") + +Undo = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAadJ" + "REFUOI2lkL9LW1EYhp9zI41/QnXI1snBoYPQFqFDQCqYxKIS2o41m4MEB0FwaBcpFt3UwaFa" + "G8UmOaZQuneom5ugU7Dij1wlMV6N3vo5hHPx6qUGPHCGw/me57zvUcoK8ZDVdN9AuhCVcs0B" + "YO71b9WwIF2ICkDXs14ALq/O2J0+EIAfQ1ueSAVVSBeichOsnV9QcY8BKDk23xbXWP+4rwIF" + "BjagKzXmswsc7lcAeNHzFICvk7/Yy7jqTgXT17ya0d8ZjExApH6fmuyjs6cNgHwuKz7B+9Xn" + "MvDyLVWn7INj8YTXOZ/LSmqpL/gTy+dVLq/OcKUGUI8d8SeMxRMqn8uKJ+hfbBeA5Tcb6tSu" + "x3f+OZQc24MeDzQJwF7GVUZi7qxT22H41Qjd00/EdDeruG2TWvJ3vv1nSmsts8URxt6NU5WK" + "17/k2Oyc7HKyU2Zt+Q8zyRXfy16CWDyhBiMTDH0aBiDc/Mg38D+4HsEKoawQWmvpGGuVn0dT" + "8qU4Kp83U9KSDIvWWsxM0PYdtNbSkgzL6t8PDcF3BDcljcCBAiNpBFZWiGuCvt7CsLLqpwAA" + "AABJRU5ErkJggg==") + +Redo = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAYhJ" + "REFUOI2lksFLAkEUxr9RCbwUdEr7DzIIukTdOuUh0q0kL4JGUAaZhwShg0QQC4XUqYwgKII8" + "rc6p/8Bb0im8dQmy0KW1lBR6HWSHXRtCaODB8N77fjPvm2HM4cR/lkuW3Nb8ZO5PlDvWN8AU" + "RmfXRa5+pVPj00Ah/igFMXOEbc1PViEANL8NAIDeruJCu5RCXAAQuZoS4ixXRXFjflPs15QY" + "qq9ZKmVebBCH9bQsV7E8mBZxmFeht6sAgEanDgAoFjSyAsAcTii5cVJy48Q5J+Zwwhoz+6N0" + "+7xL509bpFbC5EsO2/pcABDzHAAAAkFFapTRMVD/epOVuoBe4fTeCAFAKfPC3mstIa41dejP" + "LQCAZ2mAbB70rsRqFGOJITFvramL2sb1ChZ2JrsWyH5isaCR+hBHNLJoO73R/hA9/OgeZ5G8" + "/AaBoMLSE6c4Ob6R3s4UB4IKQ6/r1uCcky85TGolTKnyHKXKc+QNuW2v8CfAhHhDblIr4V/i" + "vgBWiOyf9AUwIbL8D+DEtq2XUuV6AAAAAElFTkSuQmCC") + +Blank = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAF1J" + "REFUOI3tkzEOwDAIA22S//84cYe2QxGJqFg61BMDOhsBpDVU1O9Cc2jXSGvcAgBAihkkoTkU" + "QSyV84JHKdMA8jT3kB52B+4e9DrBSj/gC4DHHfgdZ8TqN5ZHOACokRkohSNQfwAAAABJRU5E" + "rkJggg==") + diff --git a/wx/lib/pyshell.py b/wx/lib/pyshell.py new file mode 100644 index 00000000..1dca90da --- /dev/null +++ b/wx/lib/pyshell.py @@ -0,0 +1,349 @@ +#---------------------------------------------------------------------- +# Name: wxPython.lib.pyshell +# Purpose: A Python Interactive Interpreter running in a wxStyledTextCtrl +# window. +# +# Author: Robin Dunn +# +# Created: 7-July-2000 +# RCS-ID: $Id$ +# Copyright: (c) 2000 by Total Control Software +# Licence: wxWindows license +#---------------------------------------------------------------------- +# 12/10/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o 2.5 compatability update. +# o Added deprecation warning. +# + +""" +PyShellWindow is a class that provides an Interactive Interpreter running +inside a wxStyledTextCtrl, similar to the Python shell windows found in +IDLE and PythonWin. + +There is still much to be done to improve this class, such as line +buffering/recall, autoindent, calltips, autocomplete, fixing the colourizer, +etc... But it's a good start. + + +8-10-2001 THIS MODULE IS NOW DEPRECATED. Please see the most excellent + PyCrust package instead. + +""" + +import keyword +import sys +import warnings + +from code import InteractiveInterpreter + +import wx +import wx.stc as stc + +warningmsg = r"""\ + +########################################\ +# THIS MODULE IS NOW DEPRECATED | +# | +# Please see the most excellent PyCrust | +# package instead. | +########################################/ + +""" + +warnings.warn(warningmsg, DeprecationWarning, stacklevel=2) + +#---------------------------------------------------------------------- +# default styles, etc. to use for the STC + +if wx.Platform == '__WXMSW__': + _defaultSize = 8 +else: + _defaultSize = 10 + + +_default_properties = { + 'selMargin' : 0, + 'marginWidth' : 1, + 'ps1' : '>>> ', + 'stdout' : 'fore:#0000FF', + 'stderr' : 'fore:#007f00', + 'trace' : 'fore:#FF0000', + + 'default' : 'size:%d' % _defaultSize, + 'bracegood' : 'fore:#FFFFFF,back:#0000FF,bold', + 'bracebad' : 'fore:#000000,back:#FF0000,bold', + + # properties for the various Python lexer styles + 'comment' : 'fore:#007F00', + 'number' : 'fore:#007F7F', + 'string' : 'fore:#7F007F,italic', + 'char' : 'fore:#7F007F,italic', + 'keyword' : 'fore:#00007F,bold', + 'triple' : 'fore:#7F0000', + 'tripledouble': 'fore:#7F0000', + 'class' : 'fore:#0000FF,bold,underline', + 'def' : 'fore:#007F7F,bold', + 'operator' : 'bold', + + } + + +# new style numbers +_stdout_style = 15 +_stderr_style = 16 +_trace_style = 17 + + +#---------------------------------------------------------------------- + +class PyShellWindow(stc.StyledTextCtrl, InteractiveInterpreter): + def __init__(self, parent, ID, pos=wx.DefaultPosition, + size=wx.DefaultSize, style=0, + locals=None, properties=None, banner=None): + stc.StyledTextCtrl.__init__(self, parent, ID, pos, size, style) + InteractiveInterpreter.__init__(self, locals) + + self.lastPromptPos = 0 + + # the line cache is used to cycle through previous commands + self.lines = [] + self.lastUsedLine = self.curLine = 0 + + # set defaults and then deal with any user defined properties + self.props = {} + self.props.update(_default_properties) + if properties: + self.props.update(properties) + self.UpdateProperties() + + # copyright/banner message + if banner is None: + self.write("Python %s on %s\n" % #%s\n(%s)\n" % + (sys.version, sys.platform, + #sys.copyright, self.__class__.__name__ + )) + else: + self.write("%s\n" % banner) + + # write the initial prompt + self.Prompt() + + # Event handlers + self.Bind(wx.EVT_KEY_DOWN, self.OnKey) + self.Bind(stc.EVT_STC_UPDATEUI, self.OnUpdateUI, id=ID) + #self.Bind(stc.EVT_STC_STYLENEEDED, self.OnStyle, id=ID) + + + def GetLocals(self): return self.locals + def SetLocals(self, locals): self.locals = locals + + def GetProperties(self): return self.props + def SetProperties(self, properties): + self.props.update(properties) + self.UpdateProperties() + + + def UpdateProperties(self): + """ + Reset the editor and other settings based on the contents of the + current properties dictionary. + """ + p = self.props + + #self.SetEdgeMode(stc.STC_EDGE_LINE) + #self.SetEdgeColumn(80) + + + # set the selection margin and window margin + self.SetMarginWidth(1, p['selMargin']) + self.SetMargins(p['marginWidth'], p['marginWidth']) + + # styles + self.StyleSetSpec(stc.STC_STYLE_DEFAULT, p['default']) + self.StyleClearAll() + self.StyleSetSpec(_stdout_style, p['stdout']) + self.StyleSetSpec(_stderr_style, p['stderr']) + self.StyleSetSpec(_trace_style, p['trace']) + + self.StyleSetSpec(stc.STC_STYLE_BRACELIGHT, p['bracegood']) + self.StyleSetSpec(stc.STC_STYLE_BRACEBAD, p['bracebad']) + self.StyleSetSpec(stc.STC_P_COMMENTLINE, p['comment']) + self.StyleSetSpec(stc.STC_P_NUMBER, p['number']) + self.StyleSetSpec(stc.STC_P_STRING, p['string']) + self.StyleSetSpec(stc.STC_P_CHARACTER, p['char']) + self.StyleSetSpec(stc.STC_P_WORD, p['keyword']) + self.StyleSetSpec(stc.STC_P_TRIPLE, p['triple']) + self.StyleSetSpec(stc.STC_P_TRIPLEDOUBLE, p['tripledouble']) + self.StyleSetSpec(stc.STC_P_CLASSNAME, p['class']) + self.StyleSetSpec(stc.STC_P_DEFNAME, p['def']) + self.StyleSetSpec(stc.STC_P_OPERATOR, p['operator']) + self.StyleSetSpec(stc.STC_P_COMMENTBLOCK, p['comment']) + + + # used for writing to stdout, etc. + def _write(self, text, style=_stdout_style): + self.lastPromptPos = 0 + pos = self.GetCurrentPos() + self.AddText(text) + self.StartStyling(pos, 0xFF) + self.SetStyling(len(text), style) + self.EnsureCaretVisible() + wx.Yield() + + write = _write + + def writeTrace(self, text): + self._write(text, _trace_style) + + + def Prompt(self): + # is the current line non-empty? + text, pos = self.GetCurLine() + if pos != 0: + self.AddText('\n') + self.AddText(self.props['ps1']) + self.lastPromptPos = self.GetCurrentPos() + self.EnsureCaretVisible() + self.ScrollToColumn(0) + + + def PushLine(self, text): + # TODO: Add the text to the line cache, manage the cache so + # it doesn't get too big. + pass + + + + def OnKey(self, evt): + key = evt.GetKeyCode() + if key == wx.WXK_RETURN: + pos = self.GetCurrentPos() + lastPos = self.GetTextLength() + + # if not on the last line, duplicate the current line + if self.GetLineCount()-1 != self.GetCurrentLine(): + text, col = self.GetCurLine() + prompt = self.props['ps1'] + lp = len(prompt) + if text[:lp] == prompt: + text = text[lp:] + + self.SetSelection(self.lastPromptPos, lastPos) + self.ReplaceSelection(text[:-1]) + + else: # try to execute the text from the prompt to the end + if lastPos == self.lastPromptPos: + self.AddText('\n') + self.Prompt() + return + + text = self.GetTextRange(self.lastPromptPos, lastPos) + self.AddText('\n') + + more = self.runsource(text) + if not more: + self.PushLine(text) + self.Prompt() + + # TODO: Add handlers for Alt-P and Alt-N to cycle through entries + # in the line cache + + else: + evt.Skip() + + + def OnStyle(self, evt): + # Only style from the prompt pos to the end + lastPos = self.GetTextLength() + if self.lastPromptPos and self.lastPromptPos != lastPos: + self.SetLexer(stc.STC_LEX_PYTHON) + self.SetKeywords(0, ' '.join(keyword.kwlist)) + + self.Colourise(self.lastPromptPos, lastPos) + + self.SetLexer(0) + + + def OnUpdateUI(self, evt): + # check for matching braces + braceAtCaret = -1 + braceOpposite = -1 + charBefore = None + caretPos = self.GetCurrentPos() + if caretPos > 0: + charBefore = self.GetCharAt(caretPos - 1) + styleBefore = self.GetStyleAt(caretPos - 1) + + # check before + if charBefore and chr(charBefore) in "[]{}()" and styleBefore == stc.STC_P_OPERATOR: + braceAtCaret = caretPos - 1 + + # check after + if braceAtCaret < 0: + charAfter = self.GetCharAt(caretPos) + styleAfter = self.GetStyleAt(caretPos) + if charAfter and chr(charAfter) in "[]{}()" and styleAfter == stc.STC_P_OPERATOR: + braceAtCaret = caretPos + + if braceAtCaret >= 0: + braceOpposite = self.BraceMatch(braceAtCaret) + + if braceAtCaret != -1 and braceOpposite == -1: + self.BraceBadlight(braceAtCaret) + else: + self.BraceHighlight(braceAtCaret, braceOpposite) + + + + #---------------------------------------------- + # overloaded methods from InteractiveInterpreter + def runsource(self, source): + stdout, stderr = sys.stdout, sys.stderr + sys.stdout = FauxFile(self, _stdout_style) + sys.stderr = FauxFile(self, _stderr_style) + + more = InteractiveInterpreter.runsource(self, source) + + sys.stdout, sys.stderr = stdout, stderr + return more + + def showsyntaxerror(self, filename=None): + self.write = self.writeTrace + InteractiveInterpreter.showsyntaxerror(self, filename) + self.write = self._write + + def showtraceback(self): + self.write = self.writeTrace + InteractiveInterpreter.showtraceback(self) + self.write = self._write + +#---------------------------------------------------------------------- + +class FauxFile: + def __init__(self, psw, style): + self.psw = psw + self.style = style + + def write(self, text): + self.psw.write(text, self.style) + + def writelines(self, lst): + map(self.write, lst) + + def flush(self): + pass + + +#---------------------------------------------------------------------- +# test code + +if __name__ == '__main__': + app = wx.PyWidgetTester(size = (640, 480)) + app.SetWidget(PyShellWindow, -1) + app.MainLoop() + + +#---------------------------------------------------------------------- + + diff --git a/wx/lib/rcsizer.py b/wx/lib/rcsizer.py new file mode 100644 index 00000000..b262aee4 --- /dev/null +++ b/wx/lib/rcsizer.py @@ -0,0 +1,228 @@ +#---------------------------------------------------------------------- +# Name: wxPython.lib.rcsizer +# Purpose: RowColSizer: +# +# Author: Robin Dunn, adapted from code by Niki Spahiev +# +# Created: 26-Feb-2002 +# RCS-ID: $Id$ +# Copyright: (c) 2002 by Total Control Software +# Licence: wxWindows license +#---------------------------------------------------------------------- +# 12/10/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o 2.5 compatability update. +# o There appears to be a prob with the wx.PySizer.GetSize() method. +# +# 12/23/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o wx.PySizer.GetSize() method working right now. +# + +""" +A pure-Python Sizer that lays out items in a grid similar to +wx.FlexGridSizer but item position is not implicit but explicitly +specified by row and col, and row/col spanning is supported. + +Adapted from code by Niki Spahiev. + +NOTE: There is now a C++ version of this class that has been wrapped +as wx.GridBagSizer. It is quicker and more capable so you are +encouraged to switch. +""" + +import operator +import wx + + +# After the lib and demo no longer uses this sizer enable this warning... + +## import warnings +## warningmsg = r"""\ + +## #####################################################\ +## # THIS MODULE IS NOW DEPRECATED | +## # | +## # The core wx library now contains a similar class | +## # wrapped as wx.GridBagSizer. | +## #####################################################/ + +## """ + +## warnings.warn(warningmsg, DeprecationWarning, stacklevel=2) + +#---------------------------------------------------------------------- + +class RowColSizer(wx.PySizer): + + # default sizes for cells with no item + col_w = 10 + row_h = 22 + + def __init__(self): + wx.PySizer.__init__(self) + self.growableRows = [] + self.growableCols = [] + + + def AddGrowableRow(self, idx): + self.growableRows.append(idx) + + def AddGrowableCol(self, idx): + self.growableCols.append(idx) + + + + #-------------------------------------------------- + def Add(self, item, option=0, flag=0, border=0, + # row, col and spanning can be specified individually... + row=-1, col=-1, + rowspan=1, colspan=1, + # or as tuples (row,col) and (rowspan,colspan) + pos=None, size=None, + ): + + if pos is not None: + row, col = pos + if size is not None: + rowspan, colspan = size + + assert row != -1, "Row must be specified" + assert col != -1, "Column must be specified" + + # Do I really want to do this? Probably not... + #if rowspan > 1 or colspan > 1: + # flag = flag | wx.EXPAND + + return wx.PySizer.Add(self, item, option, flag, border, + userData=(row, col, row+rowspan, col+colspan)) + + #AddWindow = Add + #AddSizer = Add + + def AddSpacer(self, width, height, option=0, flag=0, border=0, + row=-1, col=-1, + rowspan=1, colspan=1, + pos=None, size=None, + ): + if pos is not None: + row, col = pos + if size is not None: + rowspan, colspan = size + + assert row != -1, "Row must be specified" + assert col != -1, "Column must be specified" + + return wx.PySizer.Add(self, (width, height), option, flag, border, + userData=(row, col, row+rowspan, col+colspan)) + + #-------------------------------------------------- + def _add( self, size, dim ): + r, c, r2, c2 = dim # unpack coords and spanning + + # are the widths and heights lists long enough? + if r2 > len(self.rowHeights): + x = [self.row_h] * (r2-len(self.rowHeights)) + self.rowHeights.extend( x ) + if c2 > len(self.colWidths): + x = [self.col_w] * (c2-len(self.colWidths)) + self.colWidths.extend( x ) + + # set the widths and heights lists for this item + scale = (r2 - r) + for i in range(r, r2): + self.rowHeights[i] = max( self.rowHeights[i], size.height / scale ) + scale = (c2 - c) + for i in range(c, c2): + self.colWidths[i] = max( self.colWidths[i], size.width / scale ) + + + #-------------------------------------------------- + def CalcMin( self ): + self.rowHeights = [] + self.colWidths = [] + + items = self.GetChildren() + if not items: + return wx.Size(10, 10) + + for item in items: + self._add( item.CalcMin(), item.GetUserData() ) + + size = wx.Size( reduce( operator.add, self.colWidths), + reduce( operator.add, self.rowHeights) ) + return size + + + #-------------------------------------------------- + def RecalcSizes( self ): + # save current dimensions, etc. + curWidth, curHeight = self.GetSize() + px, py = self.GetPosition() + minWidth, minHeight = self.CalcMin() + + # Check for growables + if self.growableRows and curHeight > minHeight: + delta = (curHeight - minHeight) / len(self.growableRows) + extra = (curHeight - minHeight) % len(self.growableRows) + for idx in self.growableRows: + self.rowHeights[idx] += delta + self.rowHeights[self.growableRows[0]] += extra + + if self.growableCols and curWidth > minWidth: + delta = (curWidth - minWidth) / len(self.growableCols) + extra = (curWidth - minWidth) % len(self.growableCols) + for idx in self.growableCols: + self.colWidths[idx] += delta + self.colWidths[self.growableCols[0]] += extra + + rpos = [0] * len(self.rowHeights) + cpos = [0] * len(self.colWidths) + + for i in range(len(self.rowHeights)): + height = self.rowHeights[i] + rpos[i] = py + py += height + + for i in range(len(self.colWidths)): + width = self.colWidths[i] + cpos[i] = px + px += width + + # iterate children and set dimensions... + for item in self.GetChildren(): + r, c, r2, c2 = item.GetUserData() + width = reduce( operator.add, self.colWidths[c:c2] ) + height = reduce( operator.add, self.rowHeights[r:r2] ) + self.SetItemBounds( item, cpos[c], rpos[r], width, height ) + + + #-------------------------------------------------- + def SetItemBounds(self, item, x, y, w, h): + # calculate the item's actual size and position within + # its grid cell + ipt = wx.Point(x, y) + isz = item.CalcMin() + flag = item.GetFlag() + + if flag & wx.EXPAND or flag & wx.SHAPED: + isz = wx.Size(w, h) + else: + if flag & wx.ALIGN_CENTER_HORIZONTAL: + ipt.x = x + (w - isz.width) / 2 + elif flag & wx.ALIGN_RIGHT: + ipt.x = x + (w - isz.width) + + if flag & wx.ALIGN_CENTER_VERTICAL: + ipt.y = y + (h - isz.height) / 2 + elif flag & wx.ALIGN_BOTTOM: + ipt.y = y + (h - isz.height) + + item.SetDimension(ipt, isz) + + +#---------------------------------------------------------------------- +#---------------------------------------------------------------------- + + + diff --git a/wx/lib/resizewidget.py b/wx/lib/resizewidget.py new file mode 100644 index 00000000..0502d1cc --- /dev/null +++ b/wx/lib/resizewidget.py @@ -0,0 +1,247 @@ +#---------------------------------------------------------------------- +# Name: wx.lib.resizewidget +# Purpose: Adds a resize handle to any widget, with support for +# notifying parents when layout needs done. +# +# Author: Robin Dunn +# +# Created: 12-June-2008 +# RCS-ID: $Id: $ +# Copyright: (c) 2008 by Total Control Software +# Licence: wxWindows license +#---------------------------------------------------------------------- + +""" +Reparents a given widget into a specialized panel that provides a resize +handle for the widget. When the user drags the resize handle the widget is +resized accordingly, and an event is sent to notify parents that they should +recalculate their layout. +""" + +import wx +import wx.lib.newevent + +#----------------------------------------------------------------------------- + +# dimensions used for the handle +RW_THICKNESS = 4 +RW_LENGTH = 12 + +# colors for the handle +RW_PEN = 'black' +RW_FILL = '#A0A0A0' +RW_FILL2 = '#E0E0E0' + +# An event and event binder that will notify the containers that they should +# redo the layout in whatever way makes sense for their particular content. +_RWLayoutNeededEvent, EVT_RW_LAYOUT_NEEDED = wx.lib.newevent.NewCommandEvent() + + +# TODO: Add a style flag that indicates that the ResizeWidget should +# try to adjust the layout itself by looking up the sizer and +# containment hierachy. Maybe also a style that says that it is okay +# to adjust the size of top-level windows too. + +#----------------------------------------------------------------------------- + +class ResizeWidget(wx.PyPanel): + def __init__(self, *args, **kw): + wx.PyPanel.__init__(self, *args, **kw) + self._init() + + self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) + self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp) + self.Bind(wx.EVT_MOTION, self.OnMouseMove) + self.Bind(wx.EVT_LEAVE_WINDOW, self.OnMouseLeave) + + self.Bind(wx.EVT_SIZE, self.OnSize) + self.Bind(wx.EVT_PAINT, self.OnPaint) + + + def _init(self): + self._managedChild = None + self._bestSize = wx.Size(100,25) + self._resizeCursor = False + self._dragPos = None + self._resizeEnabled = True + self._reparenting = False + + + def SetManagedChild(self, child): + self._reparenting = True + child.Reparent(self) # This calls AddChild, so do the rest of the init there + self._reparenting = False + self.AdjustToChild() + + def GetManagedChild(self): + return self._managedChild + + ManagedChild = property(GetManagedChild, SetManagedChild) + + + def AdjustToChild(self): + self.AdjustToSize(self._managedChild.GetEffectiveMinSize()) + + + def AdjustToSize(self, size): + size = wx.Size(*size) + self._bestSize = size + (RW_THICKNESS, RW_THICKNESS) + self.SetSize(self._bestSize) + + + def EnableResize(self, enable=True): + self._resizeEnabled = enable + self.Refresh(False) + + + def IsResizeEnabled(self): + return self._resizeEnabled + + + #=== Event handler methods === + def OnLeftDown(self, evt): + if self._hitTest(evt.GetPosition()) and self._resizeEnabled: + self.CaptureMouse() + self._dragPos = evt.GetPosition() + + + def OnLeftUp(self, evt): + if self.HasCapture(): + self.ReleaseMouse() + self._dragPos = None + + + def OnMouseMove(self, evt): + # set or reset the drag cursor + pos = evt.GetPosition() + if self._hitTest(pos) and self._resizeEnabled: + if not self._resizeCursor: + self.SetCursor(wx.StockCursor(wx.CURSOR_SIZENWSE)) + self._resizeCursor = True + else: + if self._resizeCursor: + self.SetCursor(wx.StockCursor(wx.CURSOR_ARROW)) + self._resizeCursor = False + + # determine if a new size is needed + if evt.Dragging() and self._dragPos is not None: + delta = self._dragPos - pos + newSize = self.GetSize() - delta.Get() + self._adjustNewSize(newSize) + if newSize != self.GetSize(): + self.SetSize(newSize) + self._dragPos = pos + self._bestSize = newSize + self._sendEvent() + + + def _sendEvent(self): + event = _RWLayoutNeededEvent(self.GetId()) + event.SetEventObject(self) + self.GetEventHandler().ProcessEvent(event) + + + def _adjustNewSize(self, newSize): + if newSize.width < RW_LENGTH: + newSize.width = RW_LENGTH + if newSize.height < RW_LENGTH: + newSize.height = RW_LENGTH + + if self._managedChild: + minsize = self._managedChild.GetMinSize() + if minsize.width != -1 and newSize.width - RW_THICKNESS < minsize.width: + newSize.width = minsize.width + RW_THICKNESS + if minsize.height != -1 and newSize.height - RW_THICKNESS < minsize.height: + newSize.height = minsize.height + RW_THICKNESS + maxsize = self._managedChild.GetMaxSize() + if maxsize.width != -1 and newSize.width - RW_THICKNESS > maxsize.width: + newSize.width = maxsize.width + RW_THICKNESS + if maxsize.height != -1 and newSize.height - RW_THICKNESS > maxsize.height: + newSize.height = maxsize.height + RW_THICKNESS + + + def OnMouseLeave(self, evt): + if self._resizeCursor: + self.SetCursor(wx.StockCursor(wx.CURSOR_ARROW)) + self._resizeCursor = False + + + def OnSize(self, evt): + if not self._managedChild: + return + sz = self.GetSize() + self._managedChild.SetRect( + wx.RectPS((0,0), sz - (RW_THICKNESS, RW_THICKNESS))) + r = wx.Rect(sz.width - RW_LENGTH, + sz.height - RW_LENGTH, + RW_LENGTH, RW_LENGTH) + r.Inflate(2,2) + self.RefreshRect(r) + + + def OnPaint(self, evt): + # draw the resize handle + dc = wx.PaintDC(self) + w,h = self.GetSize() + points = [ (w - 1, h - RW_LENGTH), + (w - RW_THICKNESS, h - RW_LENGTH), + (w - RW_THICKNESS, h - RW_THICKNESS), + (w - RW_LENGTH, h - RW_THICKNESS), + (w - RW_LENGTH, h - 1), + (w - 1, h - 1), + (w - 1, h - RW_LENGTH), + ] + dc.SetPen(wx.Pen(RW_PEN, 1)) + if self._resizeEnabled: + fill = RW_FILL + else: + fill = RW_FILL2 + dc.SetBrush(wx.Brush(fill)) + dc.DrawPolygon(points) + + + def _hitTest(self, pos): + # is the position in the area to be used for the resize handle? + w, h = self.GetSize() + if ( w - RW_THICKNESS <= pos.x <= w + and h - RW_LENGTH <= pos.y <= h ): + return True + if ( w - RW_LENGTH <= pos.x <= w + and h - RW_THICKNESS <= pos.y <= h ): + return True + return False + + + #=== Overriden virtuals from the base class === + def AddChild(self, child): + assert self._managedChild is None, "Already managing a child widget, can only do one" + self._managedChild = child + wx.PyPanel.AddChild(self, child) + + # This little hack is needed because if this AddChild was called when + # the widget was first created, then the OOR values will get reset + # after this function call, and so the Python proxy object saved in + # the window may be different than the child object we have now, so we + # need to reset which proxy object we're using. Look for it by ID. + def _doAfterAddChild(self, id): + if not self: + return + child = self.FindWindowById(id) + self._managedChild = child + self.AdjustToChild() + self._sendEvent() + if self._reparenting: + _doAfterAddChild(self, child.GetId()) + else: + wx.CallAfter(_doAfterAddChild, self, child.GetId()) + + def RemoveChild(self, child): + self._init() + wx.PyPanel.RemoveChild(self, child) + + + def DoGetBestSize(self): + return self._bestSize + + +#----------------------------------------------------------------------------- diff --git a/wx/lib/rightalign.py b/wx/lib/rightalign.py new file mode 100644 index 00000000..3609866c --- /dev/null +++ b/wx/lib/rightalign.py @@ -0,0 +1,109 @@ +# -*- coding: iso-8859-1 -*- +#---------------------------------------------------------------------- +# Name: wxPython.lib.rightalign +# Purpose: A class derived from wxTextCtrl that aligns the text +# on the right side of the control, (except when editing.) +# +# Author: Josu Oyanguren +# +# Created: 19-October-2001 +# RCS-ID: $Id$ +# Copyright: (c) 2001 by Total Control Software +# Licence: wxWindows license +#---------------------------------------------------------------------- +# 12/11/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o 2.5 compatability update. +# o Added deprecation warning. +# +# 12/20/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o wxRightTextCtrl -> RightTextCtrl +# + +""" +Some time ago, I asked about how to right-align +wxTextCtrls. Answer was that it is not supported. I forgot it. + +Just a week ago, one of my clients asked me to have numbers right +aligned. (Indeed it was that numbers MUST be right aligned). + +So the game begun. Hacking, hacking, ... + +At last, i succeed. Here is some code that someone may find +useful. ubRightTextCtrl is right-aligned when you are not editing, but +left-aligned if it has focus. + +Hope this can help someone, as much as this list helps me. + +Josu Oyanguren +Ubera Servicios Informaticos. + + +P.S. This only works well on wxMSW. +""" + +import warnings +import wx + +#---------------------------------------------------------------------- + +warningmsg = r"""\ + +##############################################################\ +# THIS MODULE IS DEPRECATED | +# | +# This control still functions, but it is deprecated because | +# wx.TextCtrl now supports the wx.TE_RIGHT style flag | +##############################################################/ + + +""" + +warnings.warn(warningmsg, DeprecationWarning, stacklevel=2) + +#---------------------------------------------------------------------- + +class RightTextCtrl(wx.TextCtrl): + def __init__(self, parent, id, *args, **kwargs): + wx.TextCtrl.__init__(self, parent, id, *args, **kwargs) + self.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus) + self.Bind(wx.EVT_PAINT, self.OnPaint) + + def OnPaint(self, event): + dc = wx.PaintDC(self) + dc.SetFont(self.GetFont()) + dc.Clear() + text = self.GetValue() + textwidth, textheight = dc.GetTextExtent(text) + dcwidth, dcheight = self.GetClientSize() + + y = (dcheight - textheight) / 2 + x = dcwidth - textwidth - 2 + + if self.IsEnabled(): + fclr = self.GetForegroundColour() + else: + fclr = wx.SystemSettings_GetColour(wx.SYS_COLOUR_GRAYTEXT) + + dc.SetTextForeground(fclr) + + dc.SetClippingRegion(0, 0, dcwidth, dcheight) + dc.DrawText(text, x, y) + + if x < 0: + toofat = '...' + markwidth = dc.GetTextExtent(toofat)[0] + dc.SetPen(wx.Pen(dc.GetBackground().GetColour(), 1, wx.SOLID )) + dc.DrawRectangle(0,0, markwidth, dcheight) + dc.SetPen(wx.Pen(wx.RED, 1, wx.SOLID )) + dc.SetBrush(wx.TRANSPARENT_BRUSH) + dc.DrawRectangle(1, 1, dcwidth-2, dcheight-2) + dc.DrawText(toofat, 1, y) + + + def OnKillFocus(self, event): + if not self.GetParent(): return + self.Refresh() + event.Skip() + diff --git a/wx/lib/rpcMixin.py b/wx/lib/rpcMixin.py new file mode 100644 index 00000000..7a0c3420 --- /dev/null +++ b/wx/lib/rpcMixin.py @@ -0,0 +1,422 @@ +# +# This was modified from rpcMixin.py distributed with wxPython +# +#---------------------------------------------------------------------- +# Name: rpcMixin +# Version: 0.2.0 +# Purpose: provides xmlrpc server functionality for wxPython +# applications via a mixin class +# +# Requires: (1) Python with threading enabled. +# (2) xmlrpclib from PythonWare +# (http://www.pythonware.com/products/xmlrpc/) +# the code was developed and tested using version 0.9.8 +# +# Author: greg Landrum (Landrum@RationalDiscovery.com) +# +# Copyright: (c) 2000, 2001 by Greg Landrum and Rational Discovery LLC +# Licence: wxWindows license +#---------------------------------------------------------------------- +# 12/11/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o 2.5 compatability update. +# o xmlrpcserver not available. +# + +"""provides xmlrpc server functionality for wxPython applications via a mixin class + +**Some Notes:** + + 1) The xmlrpc server runs in a separate thread from the main GUI + application, communication between the two threads using a custom + event (see the Threads demo in the wxPython docs for more info). + + 2) Neither the server nor the client are particularly smart about + checking method names. So it's easy to shoot yourself in the foot + by calling improper methods. It would be pretty easy to add + either a list of allowed methods or a list of forbidden methods. + + 3) Authentication of xmlrpc clients is *not* performed. I think it + would be pretty easy to do this in a hacky way, but I haven't done + it yet. + + 4) See the bottom of this file for an example of using the class. + +**Obligatory disclaimer:** + This is my first crack at both using xmlrpc and multi-threaded + programming, so there could be huge horrible bugs or design + flaws. If you see one, I'd love to hear about them. + +""" + + +""" ChangeLog +23 May 2001: Version bumped to 0.2.0 + Numerous code and design changes + +21 Mar. 2001: Version bumped to 0.1.4 + Updated rpcMixin.OnExternal to support methods with further references + (i.e. now you can do rpcClient.foo.bar() and have it work) + This probably ain't super legal in xmlrpc land, but it works just fine here + and we need it. + +6 Mar. 2001: Version bumped to 0.1.3 + Documentation changes to make this compatible with happydoc + +21 Jan. 2001: Version bumped to 0.1.2 + OnExternal() method in the mixin class now uses getattr() to check if + a desired method is present. It should have been done this way in + the first place. +14 Dec. 2000: Version bumped to 0.1.1 + rearranged locking code and made other changes so that multiple + servers in one application are possible. + +""" + +import new +import SocketServer +import sys +import threading +import xmlrpclib +import xmlrpcserver + +import wx + +rpcPENDING = 0 +rpcDONE = 1 +rpcEXCEPT = 2 + +class RPCRequest: + """A wrapper to use for handling requests and their responses""" + status = rpcPENDING + result = None + +# here's the ID for external events +wxEVT_EXTERNAL_EVENT = wx.NewEventType() +EVT_EXTERNAL_EVENT = wx.PyEventBinder(wxEVT_EXTERNAL_EVENT, 0) + +class ExternalEvent(wx.PyEvent): + """The custom event class used to pass xmlrpc calls from + the server thread into the GUI thread + + """ + def __init__(self,method,args): + wx.PyEvent.__init__(self) + self.SetEventType(wxEVT_EXTERNAL_EVENT) + self.method = method + self.args = args + self.rpcStatus = RPCRequest() + self.rpcStatusLock = threading.Lock() + self.rpcCondVar = threading.Condition() + + def Destroy(self): + self.method=None + self.args=None + self.rpcStatus = None + self.rpcStatusLock = None + self.rpcondVar = None + +class Handler(xmlrpcserver.RequestHandler): + """The handler class that the xmlrpcserver actually calls + when a request comes in. + + """ + def log_message(self,*args): + """ causes the server to stop spewing messages every time a request comes in + + """ + pass + def call(self,method,params): + """When an xmlrpc request comes in, this is the method that + gets called. + + **Arguments** + + - method: name of the method to be called + + - params: arguments to that method + + """ + if method == '_rpcPing': + # we just acknowledge these without processing them + return 'ack' + + # construct the event + evt = ExternalEvent(method,params) + + # update the status variable + evt.rpcStatusLock.acquire() + evt.rpcStatus.status = rpcPENDING + evt.rpcStatusLock.release() + + evt.rpcCondVar.acquire() + # dispatch the event to the GUI + wx.PostEvent(self._app,evt) + + # wait for the GUI to finish + while evt.rpcStatus.status == rpcPENDING: + evt.rpcCondVar.wait() + evt.rpcCondVar.release() + evt.rpcStatusLock.acquire() + if evt.rpcStatus.status == rpcEXCEPT: + # The GUI threw an exception, release the status lock + # and re-raise the exception + evt.rpcStatusLock.release() + raise evt.rpcStatus.result[0],evt.rpcStatus.result[1] + else: + # everything went through without problems + s = evt.rpcStatus.result + + evt.rpcStatusLock.release() + evt.Destroy() + self._app = None + return s + +# this global Event is used to let the server thread +# know when it should quit +stopEvent = threading.Event() +stopEvent.clear() + +class _ServerThread(threading.Thread): + """ this is the Thread class which actually runs the server + + """ + def __init__(self,server,verbose=0): + self._xmlServ = server + threading.Thread.__init__(self,verbose=verbose) + + def stop(self): + stopEvent.set() + + def shouldStop(self): + return stopEvent.isSet() + + def run(self): + while not self.shouldStop(): + self._xmlServ.handle_request() + self._xmlServ = None + +class rpcMixin: + """A mixin class to provide xmlrpc server functionality to wxPython + frames/windows + + If you want to customize this, probably the best idea is to + override the OnExternal method, which is what's invoked when an + RPC is handled. + + """ + + # we'll try a range of ports for the server, this is the size of the + # range to be scanned + nPortsToTry=20 + if sys.platform == 'win32': + defPort = 800 + else: + defPort = 8023 + + def __init__(self,host='',port=-1,verbose=0,portScan=1): + """Constructor + + **Arguments** + + - host: (optional) the hostname for the server + + - port: (optional) the port the server will use + + - verbose: (optional) if set, the server thread will be launched + in verbose mode + + - portScan: (optional) if set, we'll scan across a number of ports + to find one which is avaiable + + """ + if port == -1: + port = self.defPort + self.verbose=verbose + self.Bind(EVT_EXTERNAL_EVENT,self.OnExternal) + if hasattr(self,'OnClose'): + self._origOnClose = self.OnClose + self.Disconnect(-1,-1,wx.EVT_CLOSE_WINDOW) + else: + self._origOnClose = None + self.OnClose = self.RPCOnClose + self.Bind(wx.EVT_CLOSE,self.RPCOnClose) + + tClass = new.classobj('Handler%d'%(port),(Handler,),{}) + tClass._app = self + if portScan: + self.rpcPort = -1 + for i in xrange(self.nPortsToTry): + try: + xmlServ = SocketServer.TCPServer((host,port+i),tClass) + except: + pass + else: + self.rpcPort = port+i + else: + self.rpcPort = port + try: + xmlServ = SocketServer.TCPServer((host,port),tClass) + except: + self.rpcPort = -1 + + if self.rpcPort == -1: + raise 'RPCMixinError','Cannot initialize server' + self.servThread = _ServerThread(xmlServ,verbose=self.verbose) + self.servThread.setName('XML-RPC Server') + self.servThread.start() + + def RPCOnClose(self,event): + """ callback for when the application is closed + + be sure to shutdown the server and the server thread before + leaving + + """ + # by setting the global stopEvent we inform the server thread + # that it's time to shut down. + stopEvent.set() + if event is not None: + # if we came in here from a user event (as opposed to an RPC event), + # then we'll need to kick the server one last time in order + # to get that thread to terminate. do so now + s1 = xmlrpclib.Server('http://localhost:%d'%(self.rpcPort)) + try: + s1._rpcPing() + except: + pass + + if self._origOnClose is not None: + self._origOnClose(event) + + def RPCQuit(self): + """ shuts down everything, including the rpc server + + """ + self.RPCOnClose(None) + def OnExternal(self,event): + """ this is the callback used to handle RPCs + + **Arguments** + + - event: an _ExternalEvent_ sent by the rpc server + + Exceptions are caught and returned in the global _rpcStatus + structure. This allows the xmlrpc server to report the + exception to the client without mucking up any of the delicate + thread stuff. + + """ + event.rpcStatusLock.acquire() + doQuit = 0 + try: + methsplit = event.method.split('.') + meth = self + for piece in methsplit: + meth = getattr(meth,piece) + except AttributeError,msg: + event.rpcStatus.result = 'No Such Method',msg + event.rpcStatus.status = rpcEXCEPT + else: + try: + res = apply(meth,event.args) + except: + import traceback + if self.verbose: traceback.print_exc() + event.rpcStatus.result = sys.exc_info()[:2] + event.rpcStatus.status = rpcEXCEPT + else: + if res is None: + # returning None across the xmlrpc interface is problematic + event.rpcStatus.result = [] + else: + event.rpcStatus.result = res + event.rpcStatus.status = rpcDONE + + event.rpcStatusLock.release() + + # broadcast (using the condition var) that we're done with the event + event.rpcCondVar.acquire() + event.rpcCondVar.notify() + event.rpcCondVar.release() + + +if __name__ == '__main__': + import time + if sys.platform == 'win32': + port = 800 + else: + port = 8023 + + class rpcFrame(wx.Frame,rpcMixin): + """A simple wxFrame with the rpcMixin functionality added + """ + def __init__(self,*args,**kwargs): + """ rpcHost or rpcPort keyword arguments will be passed along to + the xmlrpc server. + """ + mixinArgs = {} + if kwargs.has_key('rpcHost'): + mixinArgs['host'] = kwargs['rpcHost'] + del kwargs['rpcHost'] + if kwargs.has_key('rpcPort'): + mixinArgs['port'] = kwargs['rpcPort'] + del kwargs['rpcPort'] + if kwargs.has_key('rpcPortScan'): + mixinArgs['portScan'] = kwargs['rpcPortScan'] + del kwargs['rpcPortScan'] + + apply(wx.Frame.__init__,(self,)+args,kwargs) + apply(rpcMixin.__init__,(self,),mixinArgs) + + self.Bind(wx.EVT_CHAR,self.OnChar) + + def TestFunc(self,args): + """a demo method""" + return args + + def OnChar(self,event): + key = event.GetKeyCode() + if key == ord('q'): + self.OnQuit(event) + + def OnQuit(self,event): + self.OnClose(event) + + def OnClose(self,event): + self.Destroy() + + + + class MyApp(wx.App): + def OnInit(self): + self.frame = rpcFrame(None, -1, "wxPython RPCDemo", wx.DefaultPosition, + (300,300), rpcHost='localhost',rpcPort=port) + self.frame.Show(True) + return True + + + def testcon(port): + s1 = xmlrpclib.Server('http://localhost:%d'%(port)) + s1.SetTitle('Munged') + s1._rpcPing() + if doQuit: + s1.RPCQuit() + + doQuit = 1 + if len(sys.argv)>1 and sys.argv[1] == '-q': + doQuit = 0 + nT = threading.activeCount() + app = MyApp(0) + activePort = app.frame.rpcPort + t = threading.Thread(target=lambda x=activePort:testcon(x),verbose=0) + t.start() + + app.MainLoop() + # give the threads time to shut down + if threading.activeCount() > nT: + print 'waiting for all threads to terminate' + while threading.activeCount() > nT: + time.sleep(0.5) + + diff --git a/wx/lib/scrolledpanel.py b/wx/lib/scrolledpanel.py new file mode 100644 index 00000000..8f52aebb --- /dev/null +++ b/wx/lib/scrolledpanel.py @@ -0,0 +1,136 @@ +#---------------------------------------------------------------------------- +# Name: scrolledpanel.py +# Author: Will Sadkin +# Created: 03/21/2003 +# Copyright: (c) 2003 by Will Sadkin +# RCS-ID: $Id$ +# License: wxWindows license +#---------------------------------------------------------------------------- +# 12/11/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o 2.5 compatability update. +# +# 12/21/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o wxScrolledPanel -> ScrolledPanel +# + +import wx +import math + +class ScrolledPanel( wx.PyScrolledWindow ): + + """ ScrolledPanel fills a "hole" in the implementation of + wx.ScrolledWindow, providing automatic scrollbar and scrolling + behavior and the tab traversal management that wxScrolledWindow + lacks. This code was based on the original demo code showing how + to do this, but is now available for general use as a proper class + (and the demo is now converted to just use it.) + + It is assumed that the ScrolledPanel will have a sizer, as it is + used to calculate the minimal virtual size of the panel and etc. + """ + + def __init__(self, parent, id=-1, pos = wx.DefaultPosition, + size = wx.DefaultSize, style = wx.TAB_TRAVERSAL, + name = "scrolledpanel"): + + wx.PyScrolledWindow.__init__(self, parent, id, + pos=pos, size=size, + style=style, name=name) + self.scrollIntoView = True + self.SetInitialSize(size) + self.Bind(wx.EVT_CHILD_FOCUS, self.OnChildFocus) + + + def SetupScrolling(self, scroll_x=True, scroll_y=True, rate_x=20, rate_y=20, + scrollToTop=True, scrollIntoView=True): + """ + This function sets up the event handling necessary to handle + scrolling properly. It should be called within the __init__ + function of any class that is derived from ScrolledPanel, + once the controls on the panel have been constructed and + thus the size of the scrolling area can be determined. + + """ + self.scrollIntoView = scrollIntoView + + # The following is all that is needed to integrate the sizer and the scrolled window + if not scroll_x: rate_x = 0 + if not scroll_y: rate_y = 0 + + # Round up the virtual size to be a multiple of the scroll rate + sizer = self.GetSizer() + if sizer: + w, h = sizer.GetMinSize() + if rate_x: + w += rate_x - (w % rate_x) + if rate_y: + h += rate_y - (h % rate_y) + self.SetVirtualSize( (w, h) ) + self.SetScrollRate(rate_x, rate_y) + wx.CallAfter(self._SetupAfter, scrollToTop) # scroll back to top after initial events + + + def _SetupAfter(self, scrollToTop): + self.SetVirtualSize(self.GetBestVirtualSize()) + if scrollToTop: + self.Scroll(0,0) + + + def OnChildFocus(self, evt): + """ + If the child window that gets the focus is not fully visible, + this handler will try to scroll enough to see it. + """ + child = evt.GetWindow() + if self.scrollIntoView: + self.ScrollChildIntoView(child) + evt.Skip() + + + def ScrollChildIntoView(self, child): + """ + Scroll the panel so that the specified child window is in + view. NOTE. This method looks redundant if evt.Skip() is + called as well - the base wx.ScrolledWindow widget now seems + to be doing the same thing anyway + """ + sppu_x, sppu_y = self.GetScrollPixelsPerUnit() + vs_x, vs_y = self.GetViewStart() + cr = child.GetRect() + clntsz = self.GetClientSize() + new_vs_x, new_vs_y = -1, -1 + + # is it before the left edge? + if cr.x < 0 and sppu_x > 0: + new_vs_x = vs_x + (cr.x / sppu_x) + + # is it above the top? + if cr.y < 0 and sppu_y > 0: + new_vs_y = vs_y + (cr.y / sppu_y) + + # For the right and bottom edges, scroll enough to show the + # whole control if possible, but if not just scroll such that + # the top/left edges are still visible + + # is it past the right edge ? + if cr.right > clntsz.width and sppu_x > 0: + diff = math.ceil(1.0 * (cr.right - clntsz.width + 1) / sppu_x) + if cr.x - diff * sppu_x > 0: + new_vs_x = vs_x + diff + else: + new_vs_x = vs_x + (cr.x / sppu_x) + + # is it below the bottom ? + if cr.bottom > clntsz.height and sppu_y > 0: + diff = math.ceil(1.0 * (cr.bottom - clntsz.height + 1) / sppu_y) + if cr.y - diff * sppu_y > 0: + new_vs_y = vs_y + diff + else: + new_vs_y = vs_y + (cr.y / sppu_y) + + # if we need to adjust + if new_vs_x != -1 or new_vs_y != -1: + #print "%s: (%s, %s)" % (self.GetName(), new_vs_x, new_vs_y) + self.Scroll(new_vs_x, new_vs_y) diff --git a/wx/lib/sheet.py b/wx/lib/sheet.py new file mode 100644 index 00000000..fe8e77d7 --- /dev/null +++ b/wx/lib/sheet.py @@ -0,0 +1,349 @@ +# sheet.py +# CSheet - A wxPython spreadsheet class. +# This is free software. Feel free to adapt it as you like. +# Author: Mark F. Russo (russomf@hotmail.com) 2002/01/31 +#--------------------------------------------------------------------------- +# 12/11/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o 2.5 compatability update. +# o Untested. +# + +import string +import wx +import wx.grid + +#--------------------------------------------------------------------------- +class CTextCellEditor(wx.TextCtrl): + """ Custom text control for cell editing """ + def __init__(self, parent, id, grid): + wx.TextCtrl.__init__(self, parent, id, "", style=wx.NO_BORDER) + self._grid = grid # Save grid reference + self.Bind(wx.EVT_CHAR, self.OnChar) + + def OnChar(self, evt): # Hook OnChar for custom behavior + """Customizes char events """ + key = evt.GetKeyCode() + if key == wx.WXK_DOWN: + self._grid.DisableCellEditControl() # Commit the edit + self._grid.MoveCursorDown(False) # Change the current cell + elif key == wx.WXK_UP: + self._grid.DisableCellEditControl() # Commit the edit + self._grid.MoveCursorUp(False) # Change the current cell + elif key == wx.WXK_LEFT: + self._grid.DisableCellEditControl() # Commit the edit + self._grid.MoveCursorLeft(False) # Change the current cell + elif key == wx.WXK_RIGHT: + self._grid.DisableCellEditControl() # Commit the edit + self._grid.MoveCursorRight(False) # Change the current cell + + evt.Skip() # Continue event + +#--------------------------------------------------------------------------- +class CCellEditor(wx.grid.PyGridCellEditor): + """ Custom cell editor """ + def __init__(self, grid): + wx.grid.PyGridCellEditor.__init__(self) + self._grid = grid # Save a reference to the grid + + def Create(self, parent, id, evtHandler): + """ Create the actual edit control. Must derive from wxControl. + Must Override + """ + self._tc = CTextCellEditor(parent, id, self._grid) + self._tc.SetInsertionPoint(0) + self.SetControl(self._tc) + if evtHandler: + self._tc.PushEventHandler(evtHandler) + + def SetSize(self, rect): + """ Position/size the edit control within the cell rectangle. """ + # Size text control to exactly overlay in-cell editing + self._tc.SetDimensions(rect.x+3, rect.y+3, rect.width-2, rect.height-2) + + def Show(self, show, attr): + """ Show or hide the edit control. Use the attr (if not None) + to set colors or fonts for the control. + + NOTE: There is no need to everride this if you don't need + to do something out of the ordinary. + """ + super(CCellEditor, self).Show(show, attr) + + def PaintBackground(self, rect, attr): + """ Draws the part of the cell not occupied by the edit control. The + base class version just fills it with background colour from the + attribute. + + NOTE: There is no need to everride this if you don't need + to do something out of the ordinary. + """ + # Call base class method. + super(CCellEditor, self).PaintBackground(rect, attr) + + def BeginEdit(self, row, col, grid): + """ Fetch the value from the table and prepare edit control to begin editing. + Set the focus to the edit control. Must Override. + """ + self._startValue = grid.GetTable().GetValue(row, col) + self._tc.SetValue(self._startValue) + self._tc.SetFocus() + + # Select the text when initiating an edit so that subsequent typing + # replaces the contents. + self._tc.SetSelection(0, self._tc.GetLastPosition()) + + def EndEdit(self, row, col, grid): + """ Commit editing the current cell. Returns True if the value has changed. + If necessary, the control may be destroyed. Must Override. + """ + changed = False # Assume value not changed + val = self._tc.GetValue() # Get value in edit control + if val != self._startValue: # Compare + changed = True # If different then changed is True + grid.GetTable().SetValue(row, col, val) # Update the table + self._startValue = '' # Clear the class' start value + self._tc.SetValue('') # Clear contents of the edit control + + return changed + + def Reset(self): + """ Reset the value in the control back to its starting value. Must Override. """ + self._tc.SetValue(self._startValue) + self._tc.SetInsertionPointEnd() + + def IsAcceptedKey(self, evt): + """ Return True to allow the given key to start editing. The base class + version only checks that the event has no modifiers. F2 is special + and will always start the editor. + """ + return (not (evt.ControlDown() or evt.AltDown()) + and evt.GetKeyCode() != wx.WXK_SHIFT) + + def StartingKey(self, evt): + """ If the editor is enabled by pressing keys on the grid, this will be + called to let the editor react to that first key. + """ + key = evt.GetKeyCode() # Get the key code + ch = None # Handle num pad keys + if key in [ wx.WXK_NUMPAD0, wx.WXK_NUMPAD1, wx.WXK_NUMPAD2, wx.WXK_NUMPAD3, + wx.WXK_NUMPAD4, wx.WXK_NUMPAD5, wx.WXK_NUMPAD6, wx.WXK_NUMPAD7, + wx.WXK_NUMPAD8, wx.WXK_NUMPAD9]: + ch = chr(ord('0') + key - wx.WXK_NUMPAD0) + + elif key == wx.WXK_BACK: # Empty text control when init w/ back key + ch = "" + # Handle normal keys + elif key < 256 and key >= 0 and chr(key) in string.printable: + ch = chr(key) + if not evt.ShiftDown(): + ch = ch.lower() + + if ch is not None: # If are at this point with a key, + self._tc.SetValue(ch) # replace the contents of the text control. + self._tc.SetInsertionPointEnd() # Move to the end so that subsequent keys are appended + else: + evt.Skip() + + def StartingClick(self): + """ If the editor is enabled by clicking on the cell, this method will be + called to allow the editor to simulate the click on the control. + """ + pass + + def Destroy(self): + """ Final cleanup + + NOTE: There is no need to everride this if you don't need + to do something out of the ordinary. + """ + super(CCellEditor, self).Destroy() + + def Clone(self): + """ Create a new object which is the copy of this one. Must Override. """ + return CCellEditor() + +#--------------------------------------------------------------------------- +class CSheet(wx.grid.Grid): + def __init__(self, parent): + wx.grid.Grid.__init__(self, parent, -1) + + # Init variables + self._lastCol = -1 # Init last cell column clicked + self._lastRow = -1 # Init last cell row clicked + self._selected = None # Init range currently selected + # Map string datatype to default renderer/editor + self.RegisterDataType(wx.grid.GRID_VALUE_STRING, + wx.grid.GridCellStringRenderer(), + CCellEditor(self)) + + self.CreateGrid(4, 3) # By default start with a 4 x 3 grid + self.SetColLabelSize(18) # Default sizes and alignment + self.SetRowLabelSize(50) + self.SetRowLabelAlignment(wx.ALIGN_RIGHT, wx.ALIGN_BOTTOM) + self.SetColSize(0, 75) # Default column sizes + self.SetColSize(1, 75) + self.SetColSize(2, 75) + + # Sink events + self.Bind(wx.grid.EVT_GRID_CELL_LEFT_CLICK, self.OnLeftClick) + self.Bind(wx.grid.EVT_GRID_CELL_RIGHT_CLICK, self.OnRightClick) + self.Bind(wx.grid.EVT_GRID_CELL_LEFT_DCLICK, self.OnLeftDoubleClick) + self.Bind(wx.grid.EVT_GRID_RANGE_SELECT, self.OnRangeSelect) + self.Bind(wx.grid.EVT_GRID_ROW_SIZE, self.OnRowSize) + self.Bind(wx.grid.EVT_GRID_COL_SIZE, self.OnColSize) + self.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.OnCellChange) + self.Bind(wx.grid.EVT_GRID_SELECT_CELL, self.OnGridSelectCell) + + def OnGridSelectCell(self, event): + """ Track cell selections """ + # Save the last cell coordinates + self._lastRow, self._lastCol = event.GetRow(), event.GetCol() + event.Skip() + + def OnRowSize(self, event): + event.Skip() + + def OnColSize(self, event): + event.Skip() + + def OnCellChange(self, event): + event.Skip() + + def OnLeftClick(self, event): + """ Override left-click behavior to prevent left-click edit initiation """ + # Save the cell clicked + currCell = (event.GetRow(), event.GetCol()) + + # Suppress event if same cell clicked twice in a row. + # This prevents a single-click from initiating an edit. + if currCell != (self._lastRow, self._lastCol): event.Skip() + + def OnRightClick(self, event): + """ Move grid cursor when a cell is right-clicked """ + self.SetGridCursor( event.GetRow(), event.GetCol() ) + event.Skip() + + def OnLeftDoubleClick(self, event): + """ Initiate the cell editor on a double-click """ + # Move grid cursor to double-clicked cell + if self.CanEnableCellControl(): + self.SetGridCursor( event.GetRow(), event.GetCol() ) + self.EnableCellEditControl(True) # Show the cell editor + event.Skip() + + def OnRangeSelect(self, event): + """ Track which cells are selected so that copy/paste behavior can be implemented """ + # If a single cell is selected, then Selecting() returns False (0) + # and range coords are entire grid. In this case cancel previous selection. + # If more than one cell is selected, then Selecting() is True (1) + # and range accurately reflects selected cells. Save them. + # If more cells are added to a selection, selecting remains True (1) + self._selected = None + if event.Selecting(): + self._selected = ((event.GetTopRow(), event.GetLeftCol()), + (event.GetBottomRow(), event.GetRightCol())) + event.Skip() + + def Copy(self): + """ Copy the currently selected cells to the clipboard """ + # TODO: raise an error when there are no cells selected? + if self._selected == None: return + ((r1, c1), (r2, c2)) = self._selected + + # Build a string to put on the clipboard + # (Is there a faster way to do this in Python?) + crlf = chr(13) + chr(10) + tab = chr(9) + s = "" + for row in range(r1, r2+1): + for col in range(c1, c2): + s += self.GetCellValue(row,col) + s += tab + s += self.GetCellValue(row, c2) + s += crlf + + # Put the string on the clipboard + if wx.TheClipboard.Open(): + wx.TheClipboard.Clear() + wx.TheClipboard.SetData(wx.TextDataObject(s)) + wx.TheClipboard.Close() + + def Paste(self): + """ Paste the contents of the clipboard into the currently selected cells """ + # (Is there a better way to do this?) + if wx.TheClipboard.Open(): + td = wx.TextDataObject() + success = wx.TheClipboard.GetData(td) + wx.TheClipboard.Close() + if not success: return # Exit on failure + s = td.GetText() # Get the text + + crlf = chr(13) + chr(10) # CrLf characters + tab = chr(9) # Tab character + + rows = s.split(crlf) # split into rows + rows = rows[0:-1] # leave out last element, which is always empty + for i in range(0, len(rows)): # split rows into elements + rows[i] = rows[i].split(tab) + + # Get the starting and ending cell range to paste into + if self._selected == None: # If no cells selected... + r1 = self.GetGridCursorRow() # Start the paste at the current location + c1 = self.GetGridCursorCol() + r2 = self.GetNumberRows()-1 # Go to maximum row and col extents + c2 = self.GetNumberCols()-1 + else: # If cells selected, only paste there + ((r1, c1), (r2, c2)) = self._selected + + # Enter data into spreadsheet cells one at a time + r = r1 # Init row and column counters + c = c1 + for row in rows: # Loop over all rows + for element in row: # Loop over all row elements + self.SetCellValue(r, c, str(element)) # Set cell value + c += 1 # Increment the column counter + if c > c2: break # Do not exceed maximum column + r += 1 + if r > r2: break # Do not exceed maximum row + c = c1 + + def Clear(self): + """ Clear the currently selected cells """ + if self._selected == None: # If no selection... + r = self.GetGridCursorRow() # clear only current cell + c = self.GetGridCursorCol() + self.SetCellValue(r, c, "") + else: # Otherwise clear selected cells + ((r1, c1), (r2, c2)) = self._selected + for r in range(r1, r2+1): + for c in range(c1, c2+1): + self.SetCellValue(r, c, "") + + def SetNumberRows(self, numRows=1): + """ Set the number of rows in the sheet """ + # Check for non-negative number + if numRows < 0: return False + + # Adjust number of rows + curRows = self.GetNumberRows() + if curRows < numRows: + self.AppendRows(numRows - curRows) + elif curRows > numRows: + self.DeleteRows(numRows, curRows - numRows) + + return True + + def SetNumberCols(self, numCols=1): + """ Set the number of columns in the sheet """ + # Check for non-negative number + if numCols < 0: return False + + # Adjust number of rows + curCols = self.GetNumberCols() + if curCols < numCols: + self.AppendCols(numCols - curCols) + elif curCols > numCols: + self.DeleteCols(numCols, curCols - numCols) + + return True diff --git a/wx/lib/shell.py b/wx/lib/shell.py new file mode 100644 index 00000000..049e9a84 --- /dev/null +++ b/wx/lib/shell.py @@ -0,0 +1,376 @@ +# shell.py +#---------------------------------------------------------------------- +# 12/10/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o 2.5 compatability update. +# o Added deprecation warning. +# + +"""wxPython interactive shell + +Copyright (c) 1999 SIA "ANK" + +this module is free software. it may be used under same terms as Python itself + +Notes: +i would like to use command completion (see rlcompleter library module), +but i cannot load it because i don't have readline... + +History: + +* 03-oct-1999 [als] created +* 04-oct-1999 [als] PyShellOutput.intro moved from __init__ parameters to class + attributes; html debug disabled +* 04-oct-1999 [als] fixed bug with class attributes input prompts and output + styles added to customized demo some html cleanups +* 04-oct-1999 [rpd] Changed to use the new sizers +* 05-oct-1999 [als] changes inspired by code.InteractiveInterpreter() + from Python Library. if i knew about this class earlier, + i would rather inherit from it. + renamed to wxPyShell.py since i've renounced the 8.3 scheme +* 8-10-2001: THIS MODULE IS NOW DEPRECATED. Please see the most excellent + PyCrust package instead. + +""" +__version__ ="$Revision$" +# $RCSfile$ + +import code +import sys +import traceback +import warnings + +import wx +import wx.html + +warningmsg = r"""\ + +########################################\ +# THIS MODULE IS NOW DEPRECATED | +# | +# Please see the most excellent PyCrust | +# package instead. | +########################################/ + +""" + +warnings.warn(warningmsg, DeprecationWarning, stacklevel=2) + +#---------------------------------------------------------------------- + +class PyShellInput(wx.Panel): + """PyShell input window + + """ + PS1 =" Enter Command:" + PS2 ="... continue:" + def __init__(self, parent, shell, id=-1): + """Create input window + + shell must be a PyShell object. + it is used for exception handling, eval() namespaces, + and shell.output is used for output + (print's go to overridden stdout) + """ + wx.Panel.__init__(self, parent, id) + self.shell =shell + # make a private copy of class attrs + self.PS1 =PyShellInput.PS1 + self.PS2 =PyShellInput.PS2 + # create controls + self.label =wx.StaticText(self, -1, self.PS1) + tid =wx.NewId() + self.entry =wx.TextCtrl(self, tid, style = wx.TE_MULTILINE) + self.entry.Bind(wx.EVT_CHAR, self.OnChar) + self.entry.SetFont(wx.Font(9, wx.MODERN, wx.NORMAL, wx.NORMAL, False)) + sizer =wx.BoxSizer(wx.VERTICAL) + sizer.AddMany([(self.label, 0, wx.EXPAND), (self.entry, 1, wx.EXPAND)]) + self.SetSizer(sizer) + self.SetAutoLayout(True) + self.Bind(wx.EVT_SET_FOCUS, self.OnSetFocus) + # when in "continuation" mode, + # two consecutive newlines are required + # to avoid execution of unfinished block + self.first_line =1 + + def OnSetFocus(self, event): + self.entry.SetFocus() + + + def Clear(self, event=None): + """reset input state""" + self.label.SetLabel(self.PS1) + self.label.Refresh() + self.entry.SetSelection(0, self.entry.GetLastPosition()) + self.first_line =1 + # self.entry.SetFocus() + + def OnChar(self, event): + """called on CHARevent. executes input on newline""" + # print "On Char:", event.__dict__.keys() + if event.GetKeyCode() !=wx.WXK_RETURN: + # not of our business + event.Skip() + return + text =self.entry.GetValue() + # weird CRLF thingy + text = text.replace("\r\n", "\n") + # see if we've finished + if (not (self.first_line or text[-1] =="\n") # in continuation mode + or (text[-1] =="\\") # escaped newline + ): + # XXX should escaped newline put myself i "continuation" mode? + event.Skip() + return + # ok, we can try to execute this + rc =self.shell.TryExec(text) + if rc: + # code is incomplete; continue input + if self.first_line: + self.label.SetLabel(self.PS2) + self.label.Refresh() + self.first_line =0 + event.Skip() + else: + self.Clear() + +class PyShellOutput(wx.Panel): + """PyShell output window + + for now, it is based on simple wxTextCtrl, + but i'm looking at HTML classes to provide colorized output + """ + # attributes for for different (input, output, exception) display styles: + # begin tag, end tag, newline + in_style =(" >>> ", + "
\n", "
\n... ") + out_style =("", "\n", "
\n") + exc_style =("", + "\n", "
\n") + intro ="

wxPython Interactive Shell

\n" + html_debug =0 + # entity references + erefs =(("&", "&"), (">", ">"), ("<", "<"), (" ", "  ")) + def __init__(self, parent, id=-1): + wx.Panel.__init__(self, parent, id) + # make a private copy of class attrs + self.in_style =PyShellOutput.in_style + self.out_style =PyShellOutput.out_style + self.exc_style =PyShellOutput.exc_style + self.intro =PyShellOutput.intro + self.html_debug =PyShellOutput.html_debug + # create windows + if self.html_debug: + # this was used in html debugging, + # but i don't want to delete it; it's funny + splitter =wx.SplitterWindow(self, -1) + self.view =wx.TextCtrl(splitter, -1, + style = wx.TE_MULTILINE|wx.TE_READONLY|wx.HSCROLL) + self.html =wx.html.HtmlWindow(splitter) + splitter.SplitVertically(self.view, self.html) + splitter.SetSashPosition(40) + splitter.SetMinimumPaneSize(3) + self.client =splitter + else: + self.view =None + self.html =wx.html.HtmlWindow(self) + self.client =self.html # used in OnSize() + self.text =self.intro + self.html.SetPage(self.text) + self.html.SetAutoLayout(True) + self.line_buffer ="" + # refreshes are annoying + self.in_batch =0 + self.dirty =0 + self.Bind(wx.EVT_SIZE, self.OnSize) + self.Bind(wx.EVT_IDLE, self.OnIdle) + + def OnSize(self, event): + self.client.SetSize(self.GetClientSize()) + + def OnIdle(self, event): + """when there's nothing to do, we can update display""" + if self.in_batch and self.dirty: self.UpdWindow() + + def BeginBatch(self): + """do not refresh display till EndBatch()""" + self.in_batch =1 + + def EndBatch(self): + """end batch; start updating display immediately""" + self.in_batch =0 + if self.dirty: self.UpdWindow() + + def UpdWindow(self): + """sync display with text buffer""" + html =self.html + html.SetPage(self.text) + self.dirty =0 + # scroll to the end + (x,y) =html.GetVirtualSize() + html.Scroll(0, y) + + def AddText(self, text, style=None): + """write text to output window""" + # a trick needed to defer default from compile-time to execute-time + if style ==None: style =self.out_style + if 0 and __debug__: sys.__stdout__.write(text) + # handle entities + for (symbol, eref) in self.erefs: + text = text.replace(symbol, eref) + # replace newlines + text = text.replace("\n", style[2]) + # add to contents + self.text =self.text +style[0] +text +style[1] + if not self.in_batch: self.UpdWindow() + else: self.dirty =1 + if self.html_debug: + # html debug output needn't to be too large + self.view.SetValue(self.text[-4096:]) + + def write(self, str, style=None): + """stdout-like interface""" + if style ==None: style =self.out_style + # do not process incomplete lines + if len(str) <1: + # hm... what was i supposed to do? + return + elif str[-1] !="\n": + self.line_buffer =self.line_buffer +str + else: + self.AddText(self.line_buffer +str, style) + self.line_buffer ="" + + def flush(self, style=None): + """write out all that was left in line buffer""" + if style ==None: style =self.out_style + self.AddText(self.line_buffer +"\n", style) + + def write_in(self, str, style=None): + """write text in "input" style""" + if style ==None: style =self.in_style + self.AddText(str, style) + + def write_exc(self, str, style=None): + """write text in "exception" style""" + if style ==None: style =self.exc_style + self.AddText(str, style) + +class PyShell(wx.Panel): + """interactive Python shell with wxPython interface + + """ + def __init__(self, parent, globals=globals(), locals={}, + id=-1, pos=wx.DefaultPosition, size=wx.DefaultSize, + style=wx.TAB_TRAVERSAL, name="shell"): + """create PyShell window""" + wx.Panel.__init__(self, parent, id, pos, size, style, name) + self.globals =globals + self.locals =locals + splitter =wx.SplitterWindow(self, -1) + self.output =PyShellOutput(splitter) + self.input =PyShellInput(splitter, self) + self.input.SetFocus() + splitter.SplitHorizontally(self.input, self.output) + splitter.SetSashPosition(100) + splitter.SetMinimumPaneSize(20) + self.splitter =splitter + self.Bind(wx.EVT_SET_FOCUS, self.OnSetFocus) + self.Bind(wx.EVT_SIZE, self.OnSize) + + def OnSetFocus(self, event): + self.input.SetFocus() + + def TryExec(self, source, symbol="single"): + """Compile and run some source in the interpreter. + + borrowed from code.InteractiveInterpreter().runsource() + as i said above, i would rather like to inherit from that class + + returns 1 if more input is required, or 0, otherwise + """ + try: + cc = code.compile_command(source, symbol=symbol) + except (OverflowError, SyntaxError): + # [als] hm... never seen anything of that kind + self.ShowSyntaxError() + return 0 + if cc is None: + # source is incomplete + return 1 + # source is sucessfully compiled + out =self.output + # redirect system stdout to the output window + prev_out =sys.stdout + sys.stdout =out + # begin printout batch (html updates are deferred until EndBatch()) + out.BeginBatch() + out.write_in(source) + try: + exec cc in self.globals, self.locals + except SystemExit: + # SystemExit is not handled and has to be re-raised + raise + except: + # all other exceptions produce traceback output + self.ShowException() + # switch back to saved stdout + sys.stdout =prev_out + # commit printout + out.flush() + out.EndBatch() + return 0 + + def ShowException(self): + """display the traceback for the latest exception""" + (etype, value, tb) =sys.exc_info() + # remove myself from traceback + tblist =traceback.extract_tb(tb)[1:] + msg = ' '.join(traceback.format_exception_only(etype, value) + +traceback.format_list(tblist)) + self.output.write_exc(msg) + + def ShowSyntaxError(self): + """display message about syntax error (no traceback here)""" + (etype, value, tb) =sys.exc_info() + msg = ' '.join(traceback.format_exception_only(etype, value)) + self.output.write_exc(msg) + + def OnSize(self, event): + self.splitter.SetSize(self.GetClientSize()) + +#---------------------------------------------------------------------- +if __name__ == '__main__': + class MyFrame(wx.Frame): + """Very standard Frame class. Nothing special here!""" + def __init__(self, parent=None, id =-1, + title="wxPython Interactive Shell"): + wx.Frame.__init__(self, parent, id, title) + self.shell =PyShell(self) + + class MyApp(wx.App): + """Demonstrates usage of both default and customized shells""" + def OnInit(self): + frame = MyFrame() + frame.Show(True) + self.SetTopWindow(frame) +## PyShellInput.PS1 =" let's get some work done..." +## PyShellInput.PS2 =" ok, what do you really mean?" +## PyShellOutput.in_style =( +## ">>> ", +## "
\n", "
\n... ") +## PyShellOutput.out_style =( +## "", +## "
\n", "
\n") +## PyShellOutput.exc_style =("", +## "\n", "
\n") +## PyShellOutput.intro ="Customized wxPython Shell" \ +## "
<-- move this sash to see html debug output

\n" +## PyShellOutput.html_debug =1 +## frame = MyFrame(title="Customized wxPython Shell") +## frame.Show(True) + return True + + app = MyApp(0) + app.MainLoop() + diff --git a/wx/lib/sized_controls.py b/wx/lib/sized_controls.py new file mode 100644 index 00000000..cdf5e773 --- /dev/null +++ b/wx/lib/sized_controls.py @@ -0,0 +1,746 @@ +#---------------------------------------------------------------------- +# Name: sized_controls.py +# Purpose: Implements default, HIG-compliant sizers under the hood +# and provides a simple interface for customizing those sizers. +# +# Author: Kevin Ollivier +# +# Created: 26-May-2006 +# Copyright: (c) 2006 Kevin Ollivier +# Licence: wxWindows license +#---------------------------------------------------------------------- + +import wx +import wx.lib.scrolledpanel as sp + +# For HIG info: links to all the HIGs can be found here: +# http://en.wikipedia.org/wiki/Human_Interface_Guidelines + + +# useful defines for sizer prop values + +halign = { "left": wx.ALIGN_LEFT, + "center": wx.ALIGN_CENTER_HORIZONTAL, + "centre": wx.ALIGN_CENTRE_HORIZONTAL, + "right": wx.ALIGN_RIGHT, + } + +valign = { "top": wx.ALIGN_TOP, + "bottom": wx.ALIGN_BOTTOM, + "center": wx.ALIGN_CENTER_VERTICAL, + "centre": wx.ALIGN_CENTRE_VERTICAL, + } + +align = { "center": wx.ALIGN_CENTER, + "centre": wx.ALIGN_CENTRE, + } + +border = { "left": wx.LEFT, + "right": wx.RIGHT, + "top": wx.TOP, + "bottom": wx.BOTTOM, + "all": wx.ALL, + } + +minsize = { "fixed": wx.FIXED_MINSIZE, + } + +misc_flags = { "expand": wx.EXPAND, } + + +# My attempt at creating a more intuitive replacement for nesting box sizers +class TableSizer(wx.PySizer): + def __init__(self, rows=0, cols=0): + wx.PySizer.__init__(self) + self.rows = rows + self.cols = cols + self.fixed_width = 0 + self.fixed_height = 0 + self.hgrow = 0 + self.vgrow = 0 + + self.row_widths = [] + self.col_heights = [] + + # allow us to use 'old-style' proportions when emulating box sizers + self.isHorizontal = (self.rows == 1 and self.cols == 0) + self.isVertical = (self.cols == 1 and self.rows == 0) + + def CalcNumRowsCols(self): + numrows = self.rows + numcols = self.cols + numchild = len(self.GetChildren()) + + if numrows == 0 and numcols == 0: + return 0, 0 + + if numrows == 0: + rows, mod = divmod(numchild, self.cols) + if mod > 0: + rows += 1 + numrows = rows + + if numcols == 0: + cols, mod = divmod(numchild, self.rows) + if mod > 0: + cols += 1 + numcols = cols + + return numrows, numcols + + def CalcMin(self): + numrows, numcols = self.CalcNumRowsCols() + numchild = len(self.GetChildren()) + + if numchild == 0: + return wx.Size(10, 10) + + if numrows == 0 and numcols == 0: + print "TableSizer must have the number of rows or columns set. Cannot continue." + return wx.Size(10, 10) + + self.row_widths = [0 for x in range(0, numrows)] + self.col_heights = [0 for x in range(0, numcols)] + currentRow = 0 + currentCol = 0 + counter = 0 + self.hgrow = 0 + self.vgrow = 0 + + # get the max row width and max column height + for item in self.GetChildren(): + if self.cols != 0: + currentRow, currentCol = divmod(counter, numcols) + else: + currentCol, currentRow = divmod(counter, numrows) + + if item.IsShown(): + width, height = item.CalcMin() + + if self.isVertical and item.GetProportion() > 0: + self.hgrow += item.GetProportion() + elif self.isHorizontal and item.GetProportion() > 0: + self.vgrow += item.GetProportion() + + if width > self.row_widths[currentRow]: + self.row_widths[currentRow] = width + + if height > self.col_heights[currentCol]: + self.col_heights[currentCol] = height + + counter += 1 + + minwidth = 0 + for row_width in self.row_widths: + minwidth += row_width + + minheight = 0 + for col_height in self.col_heights: + minheight += col_height + + self.fixed_width = minwidth + self.fixed_height = minheight + + return wx.Size(minwidth, minheight) + + def RecalcSizes(self): + numrows, numcols = self.CalcNumRowsCols() + numchild = len(self.GetChildren()) + + if numchild == 0: + return + currentRow = 0 + currentCol = 0 + counter = 0 + + print "cols %d, rows %d" % (self.cols, self.rows) + print "fixed_height %d, fixed_width %d" % (self.fixed_height, self.fixed_width) + #print "self.GetSize() = " + `self.GetSize()` + + row_widths = [0 for x in range(0, numrows)] + col_heights = [0 for x in range(0, numcols)] + item_sizes = [0 for x in range(0, len(self.GetChildren()))] + grow_sizes = [0 for x in range(0, len(self.GetChildren()))] + + curHPos = 0 + curVPos = 0 + curCol = 0 + curRow = 0 + # first, we set sizes for all children, and while doing so, calc + # the maximum row heights and col widths. Then, afterwards we handle + # the positioning of the controls + + for item in self.GetChildren(): + if self.cols != 0: + currentRow, currentCol = divmod(counter, numcols) + else: + currentCol, currentRow = divmod(counter, numrows) + if item.IsShown(): + item_minsize = item.GetMinSizeWithBorder() + width = item_minsize[0] + height = item_minsize[1] + + print "row_height %d, row_width %d" % (self.col_heights[currentCol], self.row_widths[currentRow]) + growable_width = (self.GetSize()[0]) - width + growable_height = (self.GetSize()[1]) - height + + #if not self.isVertical and not self.isHorizontal: + # growable_width = self.GetSize()[0] - self.row_widths[currentRow] + # growable_height = self.GetSize()[1] - self.col_heights[currentCol] + + #print "grow_height %d, grow_width %d" % (growable_height, growable_width) + + item_vgrow = 0 + item_hgrow = 0 + # support wx.EXPAND for box sizers to be compatible + if item.GetFlag() & wx.EXPAND: + if self.isVertical: + if self.hgrow > 0 and item.GetProportion() > 0: + item_hgrow = (growable_width * item.GetProportion()) / self.hgrow + item_vgrow = growable_height + + elif self.isHorizontal: + if self.vgrow > 0 and item.GetProportion() > 0: + item_vgrow = (growable_height * item.GetProportion()) / self.vgrow + item_hgrow = growable_width + + if growable_width > 0 and item.GetHGrow() > 0: + item_hgrow = (growable_width * item.GetHGrow()) / 100 + print "hgrow = %d" % (item_hgrow) + + if growable_height > 0 and item.GetVGrow() > 0: + item_vgrow = (growable_height * item.GetVGrow()) / 100 + print "vgrow = %d" % (item_vgrow) + + grow_size = wx.Size(item_hgrow, item_vgrow) + size = item_minsize #wx.Size(item_minsize[0] + item_hgrow, item_minsize[1] + item_vgrow) + if size[0] + grow_size[0] > row_widths[currentRow]: + row_widths[currentRow] = size[0] + grow_size[0] + if size[1] + grow_size[1] > col_heights[currentCol]: + col_heights[currentCol] = size[1] + grow_size[1] + + grow_sizes[counter] = grow_size + item_sizes[counter] = size + + counter += 1 + + counter = 0 + for item in self.GetChildren(): + if self.cols != 0: + currentRow, currentCol = divmod(counter, numcols) + else: + currentCol, currentRow = divmod(counter, numrows) + + itempos = self.GetPosition() + if item.IsShown(): + rowstart = itempos[0] + for row in range(0, currentRow): + rowstart += row_widths[row] + + colstart = itempos[1] + for col in range(0, currentCol): + #print "numcols = %d, currentCol = %d, col = %d" % (numcols, currentCol, col) + colstart += col_heights[col] + + itempos[0] += rowstart + itempos[1] += colstart + + if item.GetFlag() & wx.ALIGN_RIGHT: + itempos[0] += (row_widths[currentRow] - item_sizes[counter][0]) + elif item.GetFlag() & (wx.ALIGN_CENTER | wx.ALIGN_CENTER_HORIZONTAL): + itempos[0] += (row_widths[currentRow] - item_sizes[counter][0]) / 2 + + if item.GetFlag() & wx.ALIGN_BOTTOM: + itempos[1] += (col_heights[currentCol] - item_sizes[counter][1]) + elif item.GetFlag() & (wx.ALIGN_CENTER | wx.ALIGN_CENTER_VERTICAL): + itempos[1] += (col_heights[currentCol] - item_sizes[counter][1]) / 2 + + hgrowth = (grow_sizes[counter][0] - itempos[0]) + if hgrowth > 0: + item_sizes[counter][0] += hgrowth + + vgrowth = (grow_sizes[counter][1] - itempos[1]) + if vgrowth > 0: + item_sizes[counter][1] += vgrowth + #item_sizes[counter][1] -= itempos[1] + item.SetDimension(itempos, item_sizes[counter]) + + counter += 1 + +def GetDefaultBorder(self): + border = 4 + if wx.Platform == "__WXMAC__": + border = 6 + elif wx.Platform == "__WXMSW__": + # MSW HIGs use dialog units, not pixels + pnt = self.ConvertDialogPointToPixels(wx.Point(4, 4)) + border = pnt[0] // 2 + elif wx.Platform == "__WXGTK__": + border = 3 + + return border + +def SetDefaultSizerProps(self): + item = self.GetParent().GetSizer().GetItem(self) + item.SetProportion(0) + item.SetFlag(wx.ALL) + item.SetBorder(self.GetDefaultHIGBorder()) + +def GetSizerProps(self): + """ + Returns a dictionary of prop name + value + """ + + props = {} + item = self.GetParent().GetSizer().GetItem(self) + if item is None: + return None + + props['proportion'] = item.GetProportion() + flags = item.GetFlag() + + if flags & border['all'] == border['all']: + props['border'] = (['all'], item.GetBorder()) + else: + borders = [] + for key in border: + if flags & border[key]: + borders.append(key) + + props['border'] = (borders, item.GetBorder()) + + if flags & align['center'] == align['center']: + props['align'] = 'center' + else: + for key in halign: + if flags & halign[key]: + props['halign'] = key + + for key in valign: + if flags & valign[key]: + props['valign'] = key + + for key in minsize: + if flags & minsize[key]: + props['minsize'] = key + + for key in misc_flags: + if flags & misc_flags[key]: + props[key] = "true" + + return props + +def SetSizerProp(self, prop, value): + """ + Sets a sizer property + + :param prop: valid strings are "proportion", "hgrow", "vgrow", + "align", "halign", "valign", "border", "minsize" and "expand" + :param value: corresponding value for the prop + """ + + lprop = prop.lower() + sizer = self.GetParent().GetSizer() + item = sizer.GetItem(self) + flag = item.GetFlag() + if lprop == "proportion": + item.SetProportion(int(value)) + elif lprop == "hgrow": + item.SetHGrow(int(value)) + elif lprop == "vgrow": + item.SetVGrow(int(value)) + elif lprop == "align": + flag = flag | align[value] + elif lprop == "halign": + flag = flag | halign[value] + elif lprop == "valign": + flag = flag | valign[value] + # elif lprop == "border": + # # this arg takes a tuple (dir, pixels) + # dirs, amount = value + # if dirs == "all": + # dirs = ["all"] + # for dir in dirs: + # flag = flag | border[dir] + # item.SetBorder(amount) + elif lprop == "border": + # this arg takes a tuple (dir, pixels) + dirs, amount = value + if dirs == "all": + dirs = ["all"] + else: + flag &= ~(wx.ALL) + for dir in dirs: + flag = flag | border[dir] + item.SetBorder(amount) + elif lprop == "minsize": + flag = flag | minsize[value] + elif lprop in misc_flags: + if not value or str(value) == "" or str(value).lower() == "false": + flag = flag &~ misc_flags[lprop] + else: + flag = flag | misc_flags[lprop] + + # auto-adjust growable rows/columns if expand or proportion is set + # on a sizer item in a FlexGridSizer + if lprop in ["expand", "proportion"] and isinstance(sizer, wx.FlexGridSizer): + cols = sizer.GetCols() + rows = sizer.GetRows() + # FIXME: I'd like to get the item index in the sizer instead, but + # doing sizer.GetChildren.index(item) always gives an error + itemnum = self.GetParent().GetChildren().index(self) + + col = 0 + row = 0 + if cols == 0: + col, row = divmod( itemnum, rows ) + else: + row, col = divmod( itemnum, cols ) + + if lprop == "expand" and not sizer.IsColGrowable(col): + sizer.AddGrowableCol(col) + elif lprop == "proportion" and int(value) != 0 and not sizer.IsRowGrowable(row): + sizer.AddGrowableRow(row) + + item.SetFlag(flag) + +def SetSizerProps(self, props={}, **kwargs): + """ + Allows to set multiple sizer properties + + :param props: a dictionary of prop name + value + :param kwargs: key words can be used for properties, e.g. expand=True + """ + + allprops = {} + allprops.update(props) + allprops.update(kwargs) + + for prop in allprops: + self.SetSizerProp(prop, allprops[prop]) + +def GetDialogBorder(self): + border = 6 + if wx.Platform == "__WXMAC__" or wx.Platform == "__WXGTK__": + border = 12 + elif wx.Platform == "__WXMSW__": + pnt = self.ConvertDialogPointToPixels(wx.Point(7, 7)) + border = pnt[0] + + return border + +def SetHGrow(self, proportion): + data = self.GetUserData() + if "HGrow" in data: + data["HGrow"] = proportion + self.SetUserData(data) + +def GetHGrow(self): + if self.GetUserData() and "HGrow" in self.GetUserData(): + return self.GetUserData()["HGrow"] + else: + return 0 + +def SetVGrow(self, proportion): + data = self.GetUserData() + if "VGrow" in data: + data["VGrow"] = proportion + self.SetUserData(data) + +def GetVGrow(self): + if self.GetUserData() and "VGrow" in self.GetUserData(): + return self.GetUserData()["VGrow"] + else: + return 0 + +def GetDefaultPanelBorder(self): + # child controls will handle their borders, so don't pad the panel. + return 0 + +# Why, Python?! Why do you make it so easy?! ;-) +wx.Dialog.GetDialogBorder = GetDialogBorder +wx.Panel.GetDefaultHIGBorder = GetDefaultPanelBorder +wx.Notebook.GetDefaultHIGBorder = GetDefaultPanelBorder +wx.SplitterWindow.GetDefaultHIGBorder = GetDefaultPanelBorder + +wx.Window.GetDefaultHIGBorder = GetDefaultBorder +wx.Window.SetDefaultSizerProps = SetDefaultSizerProps +wx.Window.SetSizerProp = SetSizerProp +wx.Window.SetSizerProps = SetSizerProps +wx.Window.GetSizerProps = GetSizerProps + +wx.SizerItem.SetHGrow = SetHGrow +wx.SizerItem.GetHGrow = GetHGrow +wx.SizerItem.SetVGrow = SetVGrow +wx.SizerItem.GetVGrow = GetVGrow + + +class SizedParent: + def AddChild(self, child): + # Note: The wx.LogNull is used here to suppress a log message + # on wxMSW that happens because when AddChild is called the + # widget's hwnd hasn't been set yet, so the GetWindowRect that + # happens as a result of sizer.Add (in wxSizerItem::SetWindow) + # fails. A better fix would be to defer this code somehow + # until after the child widget is fully constructed. + sizer = self.GetSizer() + nolog = wx.LogNull() + item = sizer.Add(child) + del nolog + item.SetUserData({"HGrow":0, "VGrow":0}) + + # Note: One problem is that the child class given to AddChild + # is the underlying wxWidgets control, not its Python subclass. So if + # you derive your own class, and override that class' GetDefaultBorder(), + # etc. methods, it will have no effect. + child.SetDefaultSizerProps() + + def GetSizerType(self): + return self.sizerType + + def SetSizerType(self, type, options={}): + """ + Sets the sizer type and automatically re-assign any children + to it. + + :param type: sizer type, valid values are "horizontal", "vertical", + "form", "table" and "grid" + :param options: dictionary of options depending on type + """ + + sizer = None + self.sizerType = type + if type == "horizontal": + sizer = wx.BoxSizer(wx.HORIZONTAL) # TableSizer(0, 1) + + elif type == "vertical": + sizer = wx.BoxSizer(wx.VERTICAL) # TableSizer(1, 0) + + elif type == "form": + #sizer = TableSizer(2, 0) + sizer = wx.FlexGridSizer(0, 2, 0, 0) + #sizer.AddGrowableCol(1) + + elif type == "table": + rows = cols = 0 + if options.has_key('rows'): + rows = int(options['rows']) + + if options.has_key('cols'): + cols = int(options['cols']) + + sizer = TableSizer(rows, cols) + + elif type == "grid": + sizer = wx.FlexGridSizer(0, 0, 0, 0) + if options.has_key('rows'): + sizer.SetRows(int(options['rows'])) + else: + sizer.SetRows(0) + if options.has_key('cols'): + sizer.SetCols(int(options['cols'])) + else: + sizer.SetCols(0) + + if options.has_key('growable_row'): + row, proportion = options['growable_row'] + sizer.SetGrowableRow(row, proportion) + + if options.has_key('growable_col'): + col, proportion = options['growable_col'] + sizer.SetGrowableCol(col, proportion) + + if options.has_key('hgap'): + sizer.SetHGap(options['hgap']) + + if options.has_key('vgap'): + sizer.SetVGap(options['vgap']) + if sizer: + self._SetNewSizer(sizer) + + def _DetachFromSizer(self, sizer): + props = {} + for child in self.GetChildren(): + # On the Mac the scrollbars and corner gripper of a + # ScrolledWindow will be in the list of children, but + # should not be managed by a sizer. So if there is a + # child that is not in a sizer make sure we don't track + # info for it nor add it to the next sizer. + csp = child.GetSizerProps() + if csp is not None: + props[child.GetId()] = csp + self.GetSizer().Detach(child) + + return props + + def _AddToNewSizer(self, sizer, props): + for child in self.GetChildren(): + csp = props.get(child.GetId(), None) + # See Mac comment above. + if csp is not None: + self.GetSizer().Add(child) + child.SetSizerProps(csp) + + +class SizedPanel(wx.PyPanel, SizedParent): + def __init__(self, *args, **kwargs): + """ + A sized panel + + Controls added to it will automatically be added to its sizer. + + Usage: + 'self' is a SizedPanel instance + + self.SetSizerType("horizontal") + + b1 = wx.Button(self, wx.ID_ANY) + t1 = wx.TextCtrl(self, -1) + t1.SetSizerProps(expand=True) + """ + + wx.PyPanel.__init__(self, *args, **kwargs) + sizer = wx.BoxSizer(wx.VERTICAL) #TableSizer(1, 0) + self.SetSizer(sizer) + self.sizerType = "vertical" + + def AddChild(self, child): + """ + Called automatically by wx, do not call it from user code + """ + + if wx.VERSION < (2,8): + wx.PyPanel.base_AddChild(self, child) + else: + wx.PyPanel.AddChild(self, child) + + SizedParent.AddChild(self, child) + + def _SetNewSizer(self, sizer): + props = self._DetachFromSizer(sizer) + wx.PyPanel.SetSizer(self, sizer) + self._AddToNewSizer(sizer, props) + + +class SizedScrolledPanel(sp.ScrolledPanel, SizedParent): + def __init__(self, *args, **kwargs): + """A sized scrolled panel + + Controls added to it will automatically be added to its sizer. + + Usage: + 'self' is a SizedScrolledPanel instance + + self.SetSizerType("horizontal") + + b1 = wx.Button(self, wx.ID_ANY) + t1 = wx.TextCtrl(self, -1) + t1.SetSizerProps(expand=True) + """ + + sp.ScrolledPanel.__init__(self, *args, **kwargs) + sizer = wx.BoxSizer(wx.VERTICAL) #TableSizer(1, 0) + self.SetSizer(sizer) + self.sizerType = "vertical" + self.SetupScrolling() + + def AddChild(self, child): + """ + Called automatically by wx, should not be called from user code + """ + + if wx.VERSION < (2,8): + sp.ScrolledPanel.base_AddChild(self, child) + else: + sp.ScrolledPanel.AddChild(self, child) + + SizedParent.AddChild(self, child) + + def _SetNewSizer(self, sizer): + props = self._DetachFromSizer(sizer) + sp.ScrolledPanel.SetSizer(self, sizer) + self._AddToNewSizer(sizer, props) + + +class SizedDialog(wx.Dialog): + def __init__(self, *args, **kwargs): + """A sized dialog + + Controls added to its content pane will automatically be added to + the panes sizer. + + Usage: + 'self' is a SizedDialog instance + + pane = self.GetContentsPane() + pane.SetSizerType("horizontal") + + b1 = wx.Button(pane, wx.ID_ANY) + t1 = wx.TextCtrl(pane, wx.ID_ANY) + t1.SetSizerProps(expand=True) + """ + + wx.Dialog.__init__(self, *args, **kwargs) + + self.SetExtraStyle(wx.WS_EX_VALIDATE_RECURSIVELY) + + self.borderLen = 12 + self.mainPanel = SizedPanel(self, -1) + + mysizer = wx.BoxSizer(wx.VERTICAL) + mysizer.Add(self.mainPanel, 1, wx.EXPAND | wx.ALL, self.GetDialogBorder()) + self.SetSizer(mysizer) + + self.SetAutoLayout(True) + + def GetContentsPane(self): + """ + Return the pane to add controls too + """ + return self.mainPanel + + def SetButtonSizer(self, sizer): + self.GetSizer().Add(sizer, 0, wx.EXPAND | wx.BOTTOM | wx.RIGHT, self.GetDialogBorder()) + + # Temporary hack to fix button ordering problems. + cancel = self.FindWindowById(wx.ID_CANCEL) + no = self.FindWindowById(wx.ID_NO) + if no and cancel: + cancel.MoveAfterInTabOrder(no) + +class SizedFrame(wx.Frame): + def __init__(self, *args, **kwargs): + """ + A sized frame + + Controls added to its content pane will automatically be added to + the panes sizer. + + Usage: + 'self' is a SizedFrame instance + + pane = self.GetContentsPane() + pane.SetSizerType("horizontal") + + b1 = wx.Button(pane, wx.ID_ANY) + t1 = wx.TextCtrl(pane, -1) + t1.SetSizerProps(expand=True) + """ + wx.Frame.__init__(self, *args, **kwargs) + + self.borderLen = 12 + # this probably isn't needed, but I thought it would help to make it consistent + # with SizedDialog, and creating a panel to hold things is often good practice. + self.mainPanel = SizedPanel(self, -1) + + mysizer = wx.BoxSizer(wx.VERTICAL) + mysizer.Add(self.mainPanel, 1, wx.EXPAND) + self.SetSizer(mysizer) + + self.SetAutoLayout(True) + + def GetContentsPane(self): + """ + Return the pane to add controls too + """ + return self.mainPanel diff --git a/wx/lib/softwareupdate.py b/wx/lib/softwareupdate.py new file mode 100644 index 00000000..02cdf15b --- /dev/null +++ b/wx/lib/softwareupdate.py @@ -0,0 +1,339 @@ +#---------------------------------------------------------------------- +# Name: wx.lib.softwareupdate +# Purpose: A mixin class using Esky that allows a frozen application +# to update itself when new versions of the software become +# available. +# +# Author: Robin Dunn +# +# Created: 1-Aug-2011 +# RCS-ID: $Id$ +# Copyright: (c) 2011 by Total Control Software +# Licence: wxWindows license +#---------------------------------------------------------------------- + +""" +This module provides a class designed to be mixed with wx.App to form a +derived class which is able to auto-self-update the application when new +versions are released. It is built upon the Esky package, available in PyPi at +http://pypi.python.org/pypi/esky. + +In order for the software update to work the application must be put into an +esky bundle using the bdist_esky distutils command, which in turn will use +py2app, py2exe or etc. to freeze the actual application. See Esky's docs for +more details. The code in this module will only have effect if the application +is frozen, and is silently ignored otherwise. +""" + +import wx +import sys +import os +import atexit +import urllib2 + +from wx.lib.dialogs import MultiMessageBox + +isFrozenApp = hasattr(sys, 'frozen') +if isFrozenApp: + import esky + import esky.util + +# wx 2.8 doesn't have [SG]etAppDisplayname... +try: + wx.App.GetAppDisplayName +except AttributeError: + wx.App.GetAppDisplayName = wx.App.GetAppName + wx.App.SetAppDisplayName = wx.App.SetAppName + + +#---------------------------------------------------------------------- + + +class UpdateAbortedError(RuntimeError): + pass + + +class SoftwareUpdate(object): + """ + Mix this class with wx.App and call InitForUpdates from the derived class' + OnInit method. Be sure that the wx.App has set a display name + (self.SetSppDisplayName) as that value will be used in the update dialogs. + """ + + _caption = "Software Update" + _networkFailureMsg = ( + "Unable to connect to %s to check for updates.\n\n" + "Perhaps your network is not enabled, the update server is down, or your" + "firewall is blocking the connection.") + + + def InitUpdates(self, updatesURL, changelogURL=None, icon=None): + """ + Set up the Esky object for doing software updates. Passing either the + base URL (with a trailing '/') for the location of the update + packages, or an instance of a class derived from the + esky.finder.VersionFinder class is required. A custom VersionFinder + can be used to find and fetch the newer verison of the software in + some other way, if desired. + + Call this method from the app's OnInit method. + """ + if isFrozenApp: + self._esky = esky.Esky(sys.executable, updatesURL) + self._updatesURL = updatesURL + self._changelogURL = changelogURL + self._icon = icon + self._pd = None + self._checkInProgress = False + try: + # get rid of the prior version if it is still here. + if self._esky.needs_cleanup(): + self._esky.cleanup() + except: + pass + self._fixSysExecutable() + + + def AutoCheckForUpdate(self, frequencyInDays, parentWindow=None, cfg=None): + """ + If it has been frequencyInDays since the last auto-check then check if + a software update is available and prompt the user to download and + install it. This can be called after a application has started up, and + if there is no update available the user will not be bothered. + """ + if not isFrozenApp: + return + if cfg is None: + cfg = wx.Config.Get() + cfg.SetPath('/autoUpdate') + lastCheck = cfg.ReadInt('lastCheck', 0) + lastCheckVersion = cfg.Read('lastCheckVersion', '') + today = int(wx.DateTime.Today().GetJulianDayNumber()) + active = self._esky.active_version + + if (today - lastCheck >= frequencyInDays + or lastCheckVersion != active): + self.CheckForUpdate(True, parentWindow, cfg) + + + def CheckForUpdate(self, silentUnlessUpdate=False, parentWindow=None, cfg=None): + """ + This method will check for the availability of a new update, and will + prompt the user with details if there is one there. By default it will + also tell the user if there is not a new update, but you can pass + silentUnlessUpdate=True to not bother the user if there isn't a new + update available. + + This method should be called from an event handler for a "Check for + updates" menu item, or something similar. The actual update check + will be run in a background thread and this function will return + immediately after starting the thread so the application is not + blocked if there is network communication problems. A callback to the + GUI thread will be made to do the update or report problems as + needed. + """ + if not isFrozenApp or self._checkInProgress: + return + self._checkInProgress = True + + + def doFindUpdate(): + try: + newest = self._esky.find_update() + chLogTxt = '' + if newest is not None and self._changelogURL: + req = urllib2.urlopen(self._changelogURL, timeout=4) + chLogTxt = req.read() + req.close() + return (newest, chLogTxt) + + except urllib2.URLError: + return 'URLError' + + + def processResults(result): + result = result.get() + self._checkInProgress = False + if result == 'URLError': + if not silentUnlessUpdate: + MultiMessageBox( + self._networkFailureMsg % self._updatesURL, + self._caption, parent=parentWindow, icon=self._icon) + return + + active = self._esky.active_version + if cfg: + today = int(wx.DateTime.Today().GetJulianDayNumber()) + cfg.WriteInt('lastCheck', today) + cfg.Write('lastCheckVersion', active) + cfg.Flush() + + newest, chLogTxt = result + if newest is None: + if not silentUnlessUpdate: + MultiMessageBox("You are already running the newest verison of %s." % + self.GetAppDisplayName(), + self._caption, parent=parentWindow, icon=self._icon) + return + self._parentWindow = parentWindow + + resp = MultiMessageBox("A new version of %s is available.\n\n" + "You are currently running verison %s; version %s is now " + "available for download. Do you wish to install it now?" + % (self.GetAppDisplayName(), active, newest), + self._caption, msg2=chLogTxt, style=wx.YES_NO, + parent=parentWindow, icon=self._icon, + btnLabels={wx.ID_YES:"Yes, install now", + wx.ID_NO:"No, maybe later"}) + if resp != wx.YES: + return + + # Ok, there is a little trickery going on here. We don't know yet if + # the user wants to restart the application after the update is + # complete, but since atexit functions are executed in a LIFO order we + # need to registar our function before we call auto_update and Esky + # possibly registers its own atexit function, because we want ours to + # be run *after* theirs. So we'll create an instance of an info object + # and register its method now, and then fill in the details below + # once we decide what we want to do. + class RestartInfo(object): + def __init__(self): + self.exe = None + def restart(self): + if self.exe is not None: + # Execute the program, replacing this process + os.execv(self.exe, [self.exe] + sys.argv[1:]) + info = RestartInfo() + atexit.register(info.restart) + + try: + # Let Esky handle all the rest of the update process so we can + # take advantage of the error checking and priviledge elevation + # (if neccessary) that they have done so we don't have to worry + # about that ourselves like we would if we broke down the proccess + # into component steps. + self._esky.auto_update(self._updateProgress) + + except UpdateAbortedError: + self._esky.cleanup() + self._esky.reinitialize() + MultiMessageBox("Update canceled.", self._caption, + parent=parentWindow, icon=self._icon) + if self._pd: + self._pd.Destroy() + return + + # Ask the user if they want the application to be restarted. + resp = MultiMessageBox("The upgrade to %s %s is ready to use; the application will " + "need to be restarted to begin using the new release.\n\n" + "Restart %s now?" + % (self.GetAppDisplayName(), newest, self.GetAppDisplayName()), + self._caption, style=wx.YES_NO, + parent=parentWindow, icon=self._icon, + btnLabels={wx.ID_YES:"Yes, restart now", + wx.ID_NO:"No, I'll restart later"}) + + if resp == wx.YES: + # Close all windows in this application... + for w in wx.GetTopLevelWindows(): + if isinstance(w, wx.Dialog): + w.Destroy() + elif isinstance(w, wx.Frame): + w.Close(True) # force close (can't be cancelled) + wx.Yield() + + # ...find the path of the esky bootstrap/wrapper program... + exe = esky.util.appexe_from_executable(sys.executable) + + # ...and tell our RestartInfo object about it. + info.exe = exe + + # Make sure the CWD not in the current version's appdir, so it can + # hopefully be cleaned up either as we exit or as the next verison + # is starting. + os.chdir(os.path.dirname(exe)) + + # With all the top level windows closed the MainLoop should exit + # automatically, but just in case tell it to exit so we can have a + # normal-as-possible shutdown of this process. Hopefully there + # isn't anything happening after we return from this function that + # matters. + self.ExitMainLoop() + + return + + # Start the worker thread that will check for an update, it will call + # processResults when it is finished. + import wx.lib.delayedresult as dr + dr.startWorker(processResults, doFindUpdate) + + + + def _updateProgress(self, status): + # Show progress of the download and install. This function is passed to Esky + # functions to use as a callback. + if self._pd is None and status.get('status') != 'done': + self._pd = wx.ProgressDialog('Software Update', ' '*40, + style=wx.PD_CAN_ABORT|wx.PD_APP_MODAL, + parent=self._parentWindow) + self._pd.Update(0, '') + + if self._parentWindow: + self._pd.CenterOnParent() + + simpleMsgMap = { 'searching' : 'Searching...', + 'retrying' : 'Retrying...', + 'ready' : 'Download complete...', + 'installing' : 'Installing...', + 'cleaning up' : 'Cleaning up...',} + + if status.get('status') in simpleMsgMap: + self._doUpdateProgress(True, simpleMsgMap[status.get('status')]) + + elif status.get('status') == 'found': + self._doUpdateProgress(True, 'Found version %s...' % status.get('new_version')) + + elif status.get('status') == 'downloading': + received = status.get('received') + size = status.get('size') + currentPercentage = 1.0 * received / size * 100 + self._doUpdateProgress(False, "Downloading...", int(currentPercentage)) + + elif status.get('status') == 'done': + if self._pd: + self._pd.Destroy() + self._pd = None + + wx.Yield() + + + def _doUpdateProgress(self, pulse, message, value=0): + if pulse: + keepGoing, skip = self._pd.Pulse(message) + else: + keepGoing, skip = self._pd.Update(value, message) + if not keepGoing: # user pressed the cancel button + self._pd.Destroy() + self._pd = None + raise UpdateAbortedError() + + + def _fixSysExecutable(self): + # It looks like at least some versions of py2app are setting + # sys.executable to ApplicationName.app/Contents/MacOS/python instead + # of ApplicationName.app/Contents/MacOS/applicationname, which is what + # should be used to relaunch the application. Other freezer tools set + # sys.executable to the actual executable as expected, so we'll tweak + # the setting here for Macs too. + if sys.platform == "darwin" and hasattr(sys, 'frozen') \ + and sys.frozen == 'macosx_app' and sys.executable.endswith('MacOS/python'): + names = os.listdir(os.path.dirname(sys.executable)) + assert len(names) == 2 # there should be only 2 + for name in names: + if name != 'python': + sys.executable = os.path.join(os.path.dirname(sys.executable), name) + break + +#---------------------------------------------------------------------- + + diff --git a/wx/lib/splashscreen.py b/wx/lib/splashscreen.py new file mode 100644 index 00000000..817ddf94 --- /dev/null +++ b/wx/lib/splashscreen.py @@ -0,0 +1,132 @@ +#---------------------------------------------------------------------- +# Name: wxPython.lib.splashscreen +# Purpose: A simple frame that can display a bitmap and closes itself +# after a specified timeout or a mouse click. +# +# Author: Mike Fletcher, Robin Dunn +# +# Created: 19-Nov-1999 +# RCS-ID: $Id$ +# Copyright: (c) 1999 by Total Control Software +# Licence: wxWindows license +#---------------------------------------------------------------------- +# 12/11/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o 2.5 compatability update. +# o Untested. +# + +""" +A Splash Screen implemented in Python. + +NOTE: Now that wxWindows has a wxSplashScrren class and it is wrapped +in wxPython this class is deprecated. See the docs for more details. +""" + +import warnings +import wx + +warningmsg = r"""\ + +#####################################################\ +# THIS MODULE IS NOW DEPRECATED | +# | +# The core wx library now contains an implementation | +# of the 'real' wx.SpashScreen. | +#####################################################/ + +""" + +warnings.warn(warningmsg, DeprecationWarning, stacklevel=2) + + +#---------------------------------------------------------------------- + +class SplashScreen(wx.Frame): + def __init__(self, parent, ID=-1, title="SplashScreen", + style=wx.SIMPLE_BORDER|wx.STAY_ON_TOP, + duration=1500, bitmapfile="bitmaps/splashscreen.bmp", + callback = None): + ''' + parent, ID, title, style -- see wx.Frame + duration -- milliseconds to display the splash screen + bitmapfile -- absolute or relative pathname to image file + callback -- if specified, is called when timer completes, callback is + responsible for closing the splash screen + ''' + ### Loading bitmap + self.bitmap = bmp = wx.Image(bitmapfile, wx.BITMAP_TYPE_ANY).ConvertToBitmap() + + ### Determine size of bitmap to size window... + size = (bmp.GetWidth(), bmp.GetHeight()) + + # size of screen + width = wx.SystemSettings_GetMetric(wx.SYS_SCREEN_X) + height = wx.SystemSettings_GetMetric(wx.SYS_SCREEN_Y) + pos = ((width-size[0])/2, (height-size[1])/2) + + # check for overflow... + if pos[0] < 0: + size = (wx.SystemSettings_GetSystemMetric(wx.SYS_SCREEN_X), size[1]) + if pos[1] < 0: + size = (size[0], wx.SystemSettings_GetSystemMetric(wx.SYS_SCREEN_Y)) + + wx.Frame.__init__(self, parent, ID, title, pos, size, style) + self.Bind(wx.EVT_LEFT_DOWN, self.OnMouseClick) + self.Bind(wx.EVT_CLOSE, self.OnCloseWindow) + self.Bind(wx.EVT_PAINT, self.OnPaint) + self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBG) + + self.Show(True) + + + class SplashTimer(wx.Timer): + def __init__(self, targetFunction): + self.Notify = targetFunction + wx.Timer.__init__(self) + + if callback is None: + callback = self.OnSplashExitDefault + + self.timer = SplashTimer(callback) + self.timer.Start(duration, 1) # one-shot only + + def OnPaint(self, event): + dc = wx.PaintDC(self) + dc.DrawBitmap(self.bitmap, 0,0, False) + + def OnEraseBG(self, event): + pass + + def OnSplashExitDefault(self, event=None): + self.Close(True) + + def OnCloseWindow(self, event=None): + self.Show(False) + self.timer.Stop() + del self.timer + self.Destroy() + + def OnMouseClick(self, event): + self.timer.Notify() + +#---------------------------------------------------------------------- + + +if __name__ == "__main__": + class DemoApp(wx.App): + def OnInit(self): + wx.InitAllImageHandlers() + self.splash = SplashScreen(None, bitmapfile="splashscreen.jpg", callback=self.OnSplashExit) + self.splash.Show(True) + self.SetTopWindow(self.splash) + return True + def OnSplashExit(self, event=None): + print "Yay! Application callback worked!" + self.splash.Close(True) + del self.splash + ### Build working windows here... + def test(sceneGraph=None): + app = DemoApp(0) + app.MainLoop() + test() diff --git a/wx/lib/splitter.py b/wx/lib/splitter.py new file mode 100644 index 00000000..4912c696 --- /dev/null +++ b/wx/lib/splitter.py @@ -0,0 +1,788 @@ +#---------------------------------------------------------------------- +# Name: wx.lib.splitter +# Purpose: A class similar to wx.SplitterWindow but that allows more +# than a single split +# +# Author: Robin Dunn +# +# Created: 9-June-2005 +# RCS-ID: $Id$ +# Copyright: (c) 2005 by Total Control Software +# Licence: wxWindows license +#---------------------------------------------------------------------- +""" +This module provides the `MultiSplitterWindow` class, which is very +similar to the standard `wx.SplitterWindow` except it can be split +more than once. +""" + +import wx + +_RENDER_VER = (2,6,1,1) + +#---------------------------------------------------------------------- + +class MultiSplitterWindow(wx.PyPanel): + """ + This class is very similar to `wx.SplitterWindow` except that it + allows for more than two windows and more than one sash. Many of + the same styles, constants, and methods behave the same as in + wx.SplitterWindow. The key differences are seen in the methods + that deal with the child windows managed by the splitter, and also + those that deal with the sash positions. In most cases you will + need to pass an index value to tell the class which window or sash + you are refering to. + + The concept of the sash position is also different than in + wx.SplitterWindow. Since the wx.Splitterwindow has only one sash + you can think of it's position as either relative to the whole + splitter window, or as relative to the first window pane managed + by the splitter. Once there is more than one sash then the + distinciton between the two concepts needs to be clairified. I've + chosen to use the second definition, and sash positions are the + distance (either horizontally or vertically) from the origin of + the window just before the sash in the splitter stack. + + NOTE: These things are not yet supported: + + * Using negative sash positions to indicate a position offset + from the end. + + * User controlled unsplitting (with double clicks on the sash + or dragging a sash until the pane size is zero.) + + * Sash gravity + + """ + def __init__(self, parent, id=-1, + pos = wx.DefaultPosition, size = wx.DefaultSize, + style = 0, name="multiSplitter"): + # always turn on tab traversal + style |= wx.TAB_TRAVERSAL + + # and turn off any border styles + style &= ~wx.BORDER_MASK + style |= wx.BORDER_NONE + + # initialize the base class + wx.PyPanel.__init__(self, parent, id, pos, size, style, name) + self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM) + + # initialize data members + self._windows = [] + self._sashes = [] + self._pending = {} + self._permitUnsplitAlways = self.HasFlag(wx.SP_PERMIT_UNSPLIT) + self._orient = wx.HORIZONTAL + self._dragMode = wx.SPLIT_DRAG_NONE + self._activeSash = -1 + self._oldX = 0 + self._oldY = 0 + self._checkRequestedSashPosition = False + self._minimumPaneSize = 0 + self._sashCursorWE = wx.StockCursor(wx.CURSOR_SIZEWE) + self._sashCursorNS = wx.StockCursor(wx.CURSOR_SIZENS) + self._sashTrackerPen = wx.Pen(wx.BLACK, 2, wx.SOLID) + self._needUpdating = False + self._isHot = False + self._drawSashInBackgroundColour = False + + # Bind event handlers + self.Bind(wx.EVT_PAINT, self._OnPaint) + self.Bind(wx.EVT_IDLE, self._OnIdle) + self.Bind(wx.EVT_SIZE, self._OnSize) + self.Bind(wx.EVT_MOUSE_EVENTS, self._OnMouse) + + + + def SetOrientation(self, orient): + """ + Set whether the windows managed by the splitter will be + stacked vertically or horizontally. The default is + horizontal. + """ + assert orient in [ wx.VERTICAL, wx.HORIZONTAL ] + self._orient = orient + + def GetOrientation(self): + """ + Returns the current orientation of the splitter, either + wx.VERTICAL or wx.HORIZONTAL. + """ + return self._orient + + def SetBackgroundColour(self,color): + wx.PyPanel.SetBackgroundColour(self,color) + self._drawSashInBackgroundColour = True + if wx.NullColour == color: + self._drawSashInBackgroundColour = False + + + def SetMinimumPaneSize(self, minSize): + """ + Set the smallest size that any pane will be allowed to be + resized to. + """ + self._minimumPaneSize = minSize + + def GetMinimumPaneSize(self): + """ + Returns the smallest allowed size for a window pane. + """ + return self._minimumPaneSize + + + + def AppendWindow(self, window, sashPos=-1): + """ + Add a new window to the splitter at the right side or bottom + of the window stack. If sashPos is given then it is used to + size the new window. + """ + self.InsertWindow(len(self._windows), window, sashPos) + + + def InsertWindow(self, idx, window, sashPos=-1): + """ + Insert a new window into the splitter at the position given in + ``idx``. + """ + assert window not in self._windows, "A window can only be in the splitter once!" + self._windows.insert(idx, window) + self._sashes.insert(idx, -1) + if not window.IsShown(): + window.Show() + if sashPos != -1: + self._pending[window] = sashPos + self._checkRequestedSashPosition = False + self._SizeWindows() + + + def DetachWindow(self, window): + """ + Removes the window from the stack of windows managed by the + splitter. The window will still exist so you should `Hide` or + `Destroy` it as needed. + """ + assert window in self._windows, "Unknown window!" + idx = self._windows.index(window) + del self._windows[idx] + del self._sashes[idx] + self._SizeWindows() + + + def ReplaceWindow(self, oldWindow, newWindow): + """ + Replaces oldWindow (which is currently being managed by the + splitter) with newWindow. The oldWindow window will still + exist so you should `Hide` or `Destroy` it as needed. + """ + assert oldWindow in self._windows, "Unknown window!" + idx = self._windows.index(oldWindow) + self._windows[idx] = newWindow + if not newWindow.IsShown(): + newWindow.Show() + self._SizeWindows() + + + def ExchangeWindows(self, window1, window2): + """ + Trade the positions in the splitter of the two windows. + """ + assert window1 in self._windows, "Unknown window!" + assert window2 in self._windows, "Unknown window!" + idx1 = self._windows.index(window1) + idx2 = self._windows.index(window2) + self._windows[idx1] = window2 + self._windows[idx2] = window1 + self._SizeWindows() + + + def GetWindow(self, idx): + """ + Returns the idx'th window being managed by the splitter. + """ + assert idx < len(self._windows) + return self._windows[idx] + + + def GetSashPosition(self, idx): + """ + Returns the position of the idx'th sash, measured from the + left/top of the window preceding the sash. + """ + assert idx < len(self._sashes) + return self._sashes[idx] + + + def SetSashPosition(self, idx, pos): + """ + Set the psition of the idx'th sash, measured from the left/top + of the window preceding the sash. + """ + assert idx < len(self._sashes) + self._sashes[idx] = pos + self._SizeWindows() + + + def SizeWindows(self): + """ + Reposition and size the windows managed by the splitter. + Useful when windows have been added/removed or when styles + have been changed. + """ + self._SizeWindows() + + + def DoGetBestSize(self): + """ + Overridden base class virtual. Determines the best size of + the control based on the best sizes of the child windows. + """ + best = wx.Size(0,0) + if not self._windows: + best = wx.Size(10,10) + + sashsize = self._GetSashSize() + if self._orient == wx.HORIZONTAL: + for win in self._windows: + winbest = win.GetEffectiveMinSize() + best.width += max(self._minimumPaneSize, winbest.width) + best.height = max(best.height, winbest.height) + best.width += sashsize * (len(self._windows)-1) + + else: + for win in self._windows: + winbest = win.GetEffectiveMinSize() + best.height += max(self._minimumPaneSize, winbest.height) + best.width = max(best.width, winbest.width) + best.height += sashsize * (len(self._windows)-1) + + border = 2 * self._GetBorderSize() + best.width += border + best.height += border + return best + + # ------------------------------------- + # Event handlers + + def _OnPaint(self, evt): + dc = wx.PaintDC(self) + self._DrawSash(dc) + + + def _OnSize(self, evt): + parent = wx.GetTopLevelParent(self) + if parent.IsIconized(): + evt.Skip() + return + self._SizeWindows() + + + def _OnIdle(self, evt): + evt.Skip() + # if this is the first idle time after a sash position has + # potentially been set, allow _SizeWindows to check for a + # requested size. + if not self._checkRequestedSashPosition: + self._checkRequestedSashPosition = True + self._SizeWindows() + + if self._needUpdating: + self._SizeWindows() + + + + def _OnMouse(self, evt): + if self.HasFlag(wx.SP_NOSASH): + return + + x, y = evt.GetPosition() + isLive = self.HasFlag(wx.SP_LIVE_UPDATE) + adjustNeighbor = evt.ShiftDown() + + # LeftDown: set things up for dragging the sash + if evt.LeftDown() and self._SashHitTest(x, y) != -1: + self._activeSash = self._SashHitTest(x, y) + self._dragMode = wx.SPLIT_DRAG_DRAGGING + + self.CaptureMouse() + self._SetResizeCursor() + + if not isLive: + self._pendingPos = (self._sashes[self._activeSash], + self._sashes[self._activeSash+1]) + self._DrawSashTracker(x, y) + + self._oldX = x + self._oldY = y + return + + # LeftUp: Finsish the drag + elif evt.LeftUp() and self._dragMode == wx.SPLIT_DRAG_DRAGGING: + self._dragMode = wx.SPLIT_DRAG_NONE + self.ReleaseMouse() + self.SetCursor(wx.STANDARD_CURSOR) + + if not isLive: + # erase the old tracker + self._DrawSashTracker(self._oldX, self._oldY) + + diff = self._GetMotionDiff(x, y) + + # determine if we can change the position + if isLive: + oldPos1, oldPos2 = (self._sashes[self._activeSash], + self._sashes[self._activeSash+1]) + else: + oldPos1, oldPos2 = self._pendingPos + newPos1, newPos2 = self._OnSashPositionChanging(self._activeSash, + oldPos1 + diff, + oldPos2 - diff, + adjustNeighbor) + if newPos1 == -1: + # the change was not allowed + return + + # TODO: check for unsplit? + + self._SetSashPositionAndNotify(self._activeSash, newPos1, newPos2, adjustNeighbor) + self._activeSash = -1 + self._pendingPos = (-1, -1) + self._SizeWindows() + + # Entering or Leaving a sash: Change the cursor + elif (evt.Moving() or evt.Leaving() or evt.Entering()) and self._dragMode == wx.SPLIT_DRAG_NONE: + if evt.Leaving() or self._SashHitTest(x, y) == -1: + self._OnLeaveSash() + else: + self._OnEnterSash() + + # Dragging the sash + elif evt.Dragging() and self._dragMode == wx.SPLIT_DRAG_DRAGGING: + diff = self._GetMotionDiff(x, y) + if not diff: + return # mouse didn't move far enough + + # determine if we can change the position + if isLive: + oldPos1, oldPos2 = (self._sashes[self._activeSash], + self._sashes[self._activeSash+1]) + else: + oldPos1, oldPos2 = self._pendingPos + newPos1, newPos2 = self._OnSashPositionChanging(self._activeSash, + oldPos1 + diff, + oldPos2 - diff, + adjustNeighbor) + if newPos1 == -1: + # the change was not allowed + return + + if newPos1 == self._sashes[self._activeSash]: + return # nothing was changed + + if not isLive: + # erase the old tracker + self._DrawSashTracker(self._oldX, self._oldY) + + if self._orient == wx.HORIZONTAL: + x = self._SashToCoord(self._activeSash, newPos1) + else: + y = self._SashToCoord(self._activeSash, newPos1) + + # Remember old positions + self._oldX = x + self._oldY = y + + if not isLive: + # draw a new tracker + self._pendingPos = (newPos1, newPos2) + self._DrawSashTracker(self._oldX, self._oldY) + else: + self._DoSetSashPosition(self._activeSash, newPos1, newPos2, adjustNeighbor) + self._needUpdating = True + + + # ------------------------------------- + # Internal helpers + + def _RedrawIfHotSensitive(self, isHot): + if not wx.VERSION >= _RENDER_VER: + return + if wx.RendererNative.Get().GetSplitterParams(self).isHotSensitive: + self._isHot = isHot + dc = wx.ClientDC(self) + self._DrawSash(dc) + + + def _OnEnterSash(self): + self._SetResizeCursor() + self._RedrawIfHotSensitive(True) + + + def _OnLeaveSash(self): + self.SetCursor(wx.STANDARD_CURSOR) + self._RedrawIfHotSensitive(False) + + + def _SetResizeCursor(self): + if self._orient == wx.HORIZONTAL: + self.SetCursor(self._sashCursorWE) + else: + self.SetCursor(self._sashCursorNS) + + + def _OnSashPositionChanging(self, idx, newPos1, newPos2, adjustNeighbor): + # TODO: check for possibility of unsplit (pane size becomes zero) + + # make sure that minsizes are honored + newPos1, newPos2 = self._AdjustSashPosition(idx, newPos1, newPos2, adjustNeighbor) + + # sanity check + if newPos1 <= 0: + newPos2 += newPos1 + newPos1 = 0 + + # send the events + evt = MultiSplitterEvent( + wx.wxEVT_COMMAND_SPLITTER_SASH_POS_CHANGING, self) + evt.SetSashIdx(idx) + evt.SetSashPosition(newPos1) + if not self._DoSendEvent(evt): + # the event handler vetoed the change + newPos1 = -1 + else: + # or it might have changed the value + newPos1 = evt.GetSashPosition() + + if adjustNeighbor and newPos1 != -1: + evt.SetSashIdx(idx+1) + evt.SetSashPosition(newPos2) + if not self._DoSendEvent(evt): + # the event handler vetoed the change + newPos2 = -1 + else: + # or it might have changed the value + newPos2 = evt.GetSashPosition() + if newPos2 == -1: + newPos1 = -1 + + return (newPos1, newPos2) + + + def _AdjustSashPosition(self, idx, newPos1, newPos2=-1, adjustNeighbor=False): + total = newPos1 + newPos2 + + # these are the windows on either side of the sash + win1 = self._windows[idx] + win2 = self._windows[idx+1] + + # make adjustments for window min sizes + minSize = self._GetWindowMin(win1) + if minSize == -1 or self._minimumPaneSize > minSize: + minSize = self._minimumPaneSize + minSize += self._GetBorderSize() + if newPos1 < minSize: + newPos1 = minSize + newPos2 = total - newPos1 + + if adjustNeighbor: + minSize = self._GetWindowMin(win2) + if minSize == -1 or self._minimumPaneSize > minSize: + minSize = self._minimumPaneSize + minSize += self._GetBorderSize() + if newPos2 < minSize: + newPos2 = minSize + newPos1 = total - newPos2 + + return (newPos1, newPos2) + + + def _DoSetSashPosition(self, idx, newPos1, newPos2=-1, adjustNeighbor=False): + newPos1, newPos2 = self._AdjustSashPosition(idx, newPos1, newPos2, adjustNeighbor) + if newPos1 == self._sashes[idx]: + return False + self._sashes[idx] = newPos1 + if adjustNeighbor: + self._sashes[idx+1] = newPos2 + return True + + + def _SetSashPositionAndNotify(self, idx, newPos1, newPos2=-1, adjustNeighbor=False): + # TODO: what is the thing about _requestedSashPosition for? + + self._DoSetSashPosition(idx, newPos1, newPos2, adjustNeighbor) + + evt = MultiSplitterEvent( + wx.wxEVT_COMMAND_SPLITTER_SASH_POS_CHANGED, self) + evt.SetSashIdx(idx) + evt.SetSashPosition(newPos1) + self._DoSendEvent(evt) + + if adjustNeighbor: + evt.SetSashIdx(idx+1) + evt.SetSashPosition(newPos2) + self._DoSendEvent(evt) + + + def _GetMotionDiff(self, x, y): + # find the diff from the old pos + if self._orient == wx.HORIZONTAL: + diff = x - self._oldX + else: + diff = y - self._oldY + return diff + + + def _SashToCoord(self, idx, sashPos): + coord = 0 + for i in range(idx): + coord += self._sashes[i] + coord += self._GetSashSize() + coord += sashPos + return coord + + + def _GetWindowMin(self, window): + # NOTE: Should this use GetEffectiveMinSize? + if self._orient == wx.HORIZONTAL: + return window.GetMinWidth() + else: + return window.GetMinHeight() + + + def _GetSashSize(self): + if self.HasFlag(wx.SP_NOSASH): + return 0 + if wx.VERSION >= _RENDER_VER: + return wx.RendererNative.Get().GetSplitterParams(self).widthSash + else: + return 5 + + + def _GetBorderSize(self): + if wx.VERSION >= _RENDER_VER: + return wx.RendererNative.Get().GetSplitterParams(self).border + else: + return 0 + + + def _DrawSash(self, dc): + if wx.VERSION >= _RENDER_VER: + if self.HasFlag(wx.SP_3DBORDER): + wx.RendererNative.Get().DrawSplitterBorder( + self, dc, self.GetClientRect()) + + # if there are no splits then we're done. + if len(self._windows) < 2: + return + + # if we are not supposed to use a sash then we're done. + if self.HasFlag(wx.SP_NOSASH): + return + + # Reverse the sense of the orientation, in this case it refers + # to the direction to draw the sash not the direction that + # windows are stacked. + orient = { wx.HORIZONTAL : wx.VERTICAL, + wx.VERTICAL : wx.HORIZONTAL }[self._orient] + + flag = 0 + if self._isHot: + flag = wx.CONTROL_CURRENT + + pos = 0 + for sash in self._sashes[:-1]: + pos += sash + if wx.VERSION >= _RENDER_VER and not self._drawSashInBackgroundColour: + wx.RendererNative.Get().DrawSplitterSash(self, dc, + self.GetClientSize(), + pos, orient, flag) + else: + dc.SetPen(wx.TRANSPARENT_PEN) + dc.SetBrush(wx.Brush(self.GetBackgroundColour())) + sashsize = self._GetSashSize() + if orient == wx.VERTICAL: + x = pos + y = 0 + w = sashsize + h = self.GetClientSize().height + else: + x = 0 + y = pos + w = self.GetClientSize().width + h = sashsize + dc.DrawRectangle(x, y, w, h) + + pos += self._GetSashSize() + + + def _DrawSashTracker(self, x, y): + # Draw a line to represent the dragging sash, for when not + # doing live updates + w, h = self.GetClientSize() + dc = wx.ScreenDC() + + if self._orient == wx.HORIZONTAL: + x1 = x + y1 = 2 + x2 = x + y2 = h-2 + if x1 > w: + x1 = w + x2 = w + elif x1 < 0: + x1 = 0 + x2 = 0 + else: + x1 = 2 + y1 = y + x2 = w-2 + y2 = y + if y1 > h: + y1 = h + y2 = h + elif y1 < 0: + y1 = 0 + y2 = 0 + + x1, y1 = self.ClientToScreenXY(x1, y1) + x2, y2 = self.ClientToScreenXY(x2, y2) + + dc.SetLogicalFunction(wx.INVERT) + dc.SetPen(self._sashTrackerPen) + dc.SetBrush(wx.TRANSPARENT_BRUSH) + dc.DrawLine(x1, y1, x2, y2) + dc.SetLogicalFunction(wx.COPY) + + + def _SashHitTest(self, x, y, tolerance=5): + # if there are no splits then we're done. + if len(self._windows) < 2: + return -1 + + if self._orient == wx.HORIZONTAL: + z = x + else: + z = y + + pos = 0 + for idx, sash in enumerate(self._sashes[:-1]): + pos += sash + hitMin = pos - tolerance + hitMax = pos + self._GetSashSize() + tolerance + + if z >= hitMin and z <= hitMax: + return idx + + pos += self._GetSashSize() + + return -1 + + + def _SizeWindows(self): + # no windows yet? + if not self._windows: + return + + # are there any pending size settings? + for window, spos in self._pending.items(): + idx = self._windows.index(window) + # TODO: this may need adjusted to make sure they all fit + # in the current client size + self._sashes[idx] = spos + del self._pending[window] + + # are there any that still have a -1? + for idx, spos in enumerate(self._sashes[:-1]): + if spos == -1: + # TODO: this should also be adjusted + self._sashes[idx] = 100 + + cw, ch = self.GetClientSize() + border = self._GetBorderSize() + sash = self._GetSashSize() + + if len(self._windows) == 1: + # there's only one, it's an easy layout + self._windows[0].SetDimensions(border, border, + cw - 2*border, ch - 2*border) + else: + if 'wxMSW' in wx.PlatformInfo: + self.Freeze() + if self._orient == wx.HORIZONTAL: + x = y = border + h = ch - 2*border + for idx, spos in enumerate(self._sashes[:-1]): + self._windows[idx].SetDimensions(x, y, spos, h) + x += spos + sash + # last one takes the rest of the space. TODO make this configurable + last = cw - 2*border - x + self._windows[idx+1].SetDimensions(x, y, last, h) + if last > 0: + self._sashes[idx+1] = last + else: + x = y = border + w = cw - 2*border + for idx, spos in enumerate(self._sashes[:-1]): + self._windows[idx].SetDimensions(x, y, w, spos) + y += spos + sash + # last one takes the rest of the space. TODO make this configurable + last = ch - 2*border - y + self._windows[idx+1].SetDimensions(x, y, w, last) + if last > 0: + self._sashes[idx+1] = last + if 'wxMSW' in wx.PlatformInfo: + self.Thaw() + + self._DrawSash(wx.ClientDC(self)) + self._needUpdating = False + + + def _DoSendEvent(self, evt): + return not self.GetEventHandler().ProcessEvent(evt) or evt.IsAllowed() + +#---------------------------------------------------------------------- + +class MultiSplitterEvent(wx.PyCommandEvent): + """ + This event class is almost the same as `wx.SplitterEvent` except + it adds an accessor for the sash index that is being changed. The + same event type IDs and event binders are used as with + `wx.SplitterEvent`. + """ + def __init__(self, type=wx.wxEVT_NULL, splitter=None): + wx.PyCommandEvent.__init__(self, type) + if splitter: + self.SetEventObject(splitter) + self.SetId(splitter.GetId()) + self.sashIdx = -1 + self.sashPos = -1 + self.isAllowed = True + + def SetSashIdx(self, idx): + self.sashIdx = idx + + def SetSashPosition(self, pos): + self.sashPos = pos + + def GetSashIdx(self): + return self.sashIdx + + def GetSashPosition(self): + return self.sashPos + + # methods from wx.NotifyEvent + def Veto(self): + self.isAllowed = False + def Allow(self): + self.isAllowed = True + def IsAllowed(self): + return self.isAllowed + + + +#---------------------------------------------------------------------- + + + diff --git a/wx/lib/statbmp.py b/wx/lib/statbmp.py new file mode 100644 index 00000000..3585446e --- /dev/null +++ b/wx/lib/statbmp.py @@ -0,0 +1,89 @@ +#---------------------------------------------------------------------- +# Name: wx.lib.statbmp +# Purpose: A generic StaticBitmap class. +# +# Author: Robin Dunn +# +# Created: 12-May-2004 +# RCS-ID: $Id$ +# Copyright: (c) 2004 by Total Control Software +# Licence: wxWindows license +#---------------------------------------------------------------------- + +import wx + +#---------------------------------------------------------------------- + +class GenStaticBitmap(wx.PyControl): + labelDelta = 1 + + def __init__(self, parent, ID, bitmap, + pos = wx.DefaultPosition, size = wx.DefaultSize, + style = 0, + name = "genstatbmp"): + if not style & wx.BORDER_MASK: + style = style | wx.BORDER_NONE + wx.PyControl.__init__(self, parent, ID, pos, size, style, + wx.DefaultValidator, name) + self._bitmap = bitmap + self.InheritAttributes() + self.SetInitialSize(size) + + self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground) + self.Bind(wx.EVT_PAINT, self.OnPaint) + + + def SetBitmap(self, bitmap): + self._bitmap = bitmap + self.SetInitialSize( (bitmap.GetWidth(), bitmap.GetHeight()) ) + self.Refresh() + + + def GetBitmap(self): + return self._bitmap + + + def DoGetBestSize(self): + """ + Overridden base class virtual. Determines the best size of the + control based on the bitmap size. + """ + return wx.Size(self._bitmap.GetWidth(), self._bitmap.GetHeight()) + + + def AcceptsFocus(self): + """Overridden base class virtual.""" + return False + + + def GetDefaultAttributes(self): + """ + Overridden base class virtual. By default we should use + the same font/colour attributes as the native StaticBitmap. + """ + return wx.StaticBitmap.GetClassDefaultAttributes() + + + def ShouldInheritColours(self): + """ + Overridden base class virtual. If the parent has non-default + colours then we want this control to inherit them. + """ + return True + + + def OnPaint(self, event): + dc = wx.PaintDC(self) + if self._bitmap: + dc.DrawBitmap(self._bitmap, 0, 0, True) + + + def OnEraseBackground(self, event): + pass + + + + +#---------------------------------------------------------------------- + + diff --git a/wx/lib/stattext.py b/wx/lib/stattext.py new file mode 100644 index 00000000..076a996e --- /dev/null +++ b/wx/lib/stattext.py @@ -0,0 +1,189 @@ +#---------------------------------------------------------------------- +# Name: wx.lib.stattext +# Purpose: A generic wxGenStaticText class. Using this should +# eliminate some of the platform differences in wxStaticText, +# such as background colours and mouse sensitivity. +# +# Author: Robin Dunn +# +# Created: 8-July-2002 +# RCS-ID: $Id$ +# Copyright: (c) 2002 by Total Control Software +# Licence: wxWindows license +#---------------------------------------------------------------------- +# 12/12/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o 2.5 compatability update. +# o Untested. +# + +import wx + +BUFFERED = 0 # In unbuffered mode we can let the theme shine through, + # otherwise we draw the background ourselves. + +if wx.Platform == "__WXMAC__": + from Carbon.Appearance import kThemeBrushDialogBackgroundActive + +#---------------------------------------------------------------------- + +class GenStaticText(wx.PyControl): + labelDelta = 1 + + def __init__(self, parent, ID=-1, label="", + pos=wx.DefaultPosition, size=wx.DefaultSize, + style=0, + name="genstattext"): + wx.PyControl.__init__(self, parent, ID, pos, size, style|wx.NO_BORDER, + wx.DefaultValidator, name) + + wx.PyControl.SetLabel(self, label) # don't check wx.ST_NO_AUTORESIZE yet + self.InheritAttributes() + self.SetInitialSize(size) + + self.Bind(wx.EVT_PAINT, self.OnPaint) + if BUFFERED: + self.defBackClr = self.GetBackgroundColour() + self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground) + else: + self.SetBackgroundStyle(wx.BG_STYLE_SYSTEM) + + + + def SetLabel(self, label): + """ + Sets the static text label and updates the control's size to exactly + fit the label unless the control has wx.ST_NO_AUTORESIZE flag. + """ + wx.PyControl.SetLabel(self, label) + style = self.GetWindowStyleFlag() + self.InvalidateBestSize() + if not style & wx.ST_NO_AUTORESIZE: + self.SetSize(self.GetBestSize()) + self.Refresh() + + + def SetFont(self, font): + """ + Sets the static text font and updates the control's size to exactly + fit the label unless the control has wx.ST_NO_AUTORESIZE flag. + """ + wx.PyControl.SetFont(self, font) + style = self.GetWindowStyleFlag() + self.InvalidateBestSize() + if not style & wx.ST_NO_AUTORESIZE: + self.SetSize(self.GetBestSize()) + self.Refresh() + + + def DoGetBestSize(self): + """ + Overridden base class virtual. Determines the best size of + the control based on the label size and the current font. + """ + label = self.GetLabel() + font = self.GetFont() + if not font: + font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) + dc = wx.ClientDC(self) + dc.SetFont(font) + + maxWidth = totalHeight = 0 + for line in label.split('\n'): + if line == '': + w, h = dc.GetTextExtent('W') # empty lines have height too + else: + w, h = dc.GetTextExtent(line) + totalHeight += h + maxWidth = max(maxWidth, w) + best = wx.Size(maxWidth, totalHeight) + self.CacheBestSize(best) + return best + + + def Enable(self, enable=True): + """Overridden Enable() method to properly refresh the widget. """ + + wx.PyControl.Enable(self, enable) + self.Refresh() + + + def Disable(self): + """Overridden Disable() method to properly refresh the widget. """ + + wx.PyControl.Disable(self) + self.Refresh() + + + def AcceptsFocus(self): + """Overridden base class virtual.""" + return False + + + def GetDefaultAttributes(self): + """ + Overridden base class virtual. By default we should use + the same font/colour attributes as the native StaticText. + """ + return wx.StaticText.GetClassDefaultAttributes() + + + def ShouldInheritColours(self): + """ + Overridden base class virtual. If the parent has non-default + colours then we want this control to inherit them. + """ + return True + + + def OnPaint(self, event): + if BUFFERED: + dc = wx.BufferedPaintDC(self) + else: + dc = wx.PaintDC(self) + width, height = self.GetClientSize() + if not width or not height: + return + + if BUFFERED: + clr = self.GetBackgroundColour() + if wx.Platform == "__WXMAC__" and clr == self.defBackClr: + # if colour is still the default then use the theme's background on Mac + themeColour = wx.MacThemeColour(kThemeBrushDialogBackgroundActive) + backBrush = wx.Brush(themeColour) + else: + backBrush = wx.Brush(clr, wx.SOLID) + dc.SetBackground(backBrush) + dc.Clear() + + if self.IsEnabled(): + dc.SetTextForeground(self.GetForegroundColour()) + else: + dc.SetTextForeground(wx.SystemSettings.GetColour(wx.SYS_COLOUR_GRAYTEXT)) + + dc.SetFont(self.GetFont()) + label = self.GetLabel() + style = self.GetWindowStyleFlag() + x = y = 0 + for line in label.split('\n'): + if line == '': + w, h = self.GetTextExtent('W') # empty lines have height too + else: + w, h = self.GetTextExtent(line) + if style & wx.ALIGN_RIGHT: + x = width - w + if style & wx.ALIGN_CENTER: + x = (width - w)/2 + dc.DrawText(line, x, y) + y += h + + + def OnEraseBackground(self, event): + pass + + + + +#---------------------------------------------------------------------- + + diff --git a/wx/lib/throbber.py b/wx/lib/throbber.py new file mode 100644 index 00000000..2812cdd9 --- /dev/null +++ b/wx/lib/throbber.py @@ -0,0 +1,322 @@ +""" +A throbber displays an animated image that can be +started, stopped, reversed, etc. Useful for showing +an ongoing process (like most web browsers use) or +simply for adding eye-candy to an application. + +Throbbers utilize a wxTimer so that normal processing +can continue unencumbered. +""" + +# +# throbber.py - Cliff Wells +# +# Thanks to Harald Massa for +# suggestions and sample code. +# +# $Id$ +# +# 12/12/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o 2.5 compatability update. +# + + +import os +import wx + +# ------------------------------------------------------------------------------ + +THROBBER_EVENT = wx.NewEventType() +EVT_UPDATE_THROBBER = wx.PyEventBinder(THROBBER_EVENT, 0) + +class UpdateThrobberEvent(wx.PyEvent): + def __init__(self): + wx.PyEvent.__init__(self) + self.SetEventType(THROBBER_EVENT) + +# ------------------------------------------------------------------------------ + +class Throbber(wx.PyPanel): + """ + The first argument is either the name of a file that will be split into frames + (a composite image) or a list of strings of image names that will be treated + as individual frames. If a single (composite) image is given, then additional + information must be provided: the number of frames in the image and the width + of each frame. The first frame is treated as the "at rest" frame (it is not + shown during animation, but only when Throbber.Rest() is called. + A second, single image may be optionally specified to overlay on top of the + animation. A label may also be specified to show on top of the animation. + """ + def __init__(self, parent, id, + bitmap, # single (composite) bitmap or list of bitmaps + pos = wx.DefaultPosition, + size = wx.DefaultSize, + frameDelay = 0.1,# time between frames + frames = 0, # number of frames (only necessary for composite image) + frameWidth = 0, # width of each frame (only necessary for composite image) + label = None, # optional text to be displayed + overlay = None, # optional image to overlay on animation + reverse = 0, # reverse direction at end of animation + style = 0, # window style + name = "throbber", + rest = 0, + current = 0, + direction = 1, + sequence = None + ): + wx.PyPanel.__init__(self, parent, id, pos, size, style, name) + self.name = name + self.label = label + self.running = (1 != 1) + _seqTypes = (type([]), type(())) + + # set size, guessing if necessary + width, height = size + if width == -1: + if type(bitmap) in _seqTypes: + width = bitmap[0].GetWidth() + else: + if frameWidth: + width = frameWidth + if height == -1: + if type(bitmap) in _seqTypes: + height = bitmap[0].GetHeight() + else: + height = bitmap.GetHeight() + self.width, self.height = width, height + + # double check it + assert width != -1 and height != -1, "Unable to guess size" + + if label: + extentX, extentY = self.GetTextExtent(label) + self.labelX = (width - extentX)/2 + self.labelY = (height - extentY)/2 + self.frameDelay = frameDelay + self.rest = rest + self.current = current + self.direction = direction + self.autoReverse = reverse + self.overlay = overlay + if overlay is not None: + self.overlay = overlay + self.overlayX = (width - self.overlay.GetWidth()) / 2 + self.overlayY = (height - self.overlay.GetHeight()) / 2 + self.showOverlay = overlay is not None + self.showLabel = label is not None + + # do we have a sequence of images? + if type(bitmap) in _seqTypes: + self.submaps = bitmap + self.frames = len(self.submaps) + # or a composite image that needs to be split? + else: + self.frames = frames + self.submaps = [] + for chunk in range(frames): + rect = (chunk * frameWidth, 0, width, height) + self.submaps.append(bitmap.GetSubBitmap(rect)) + + # self.sequence can be changed, but it's not recommended doing it + # while the throbber is running. self.sequence[0] should always + # refer to whatever frame is to be shown when 'resting' and be sure + # that no item in self.sequence >= self.frames or < 0!!! + self.SetSequence(sequence) + + self.SetClientSize((width, height)) + + timerID = wx.NewId() + self.timer = wx.Timer(self, timerID) + + self.Bind(EVT_UPDATE_THROBBER, self.Update) + self.Bind(wx.EVT_PAINT, self.OnPaint) + self.Bind(wx.EVT_TIMER, self.OnTimer, self.timer) + self.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroyWindow) + + + def DoGetBestSize(self): + return (self.width, self.height) + + + def OnTimer(self, event): + wx.PostEvent(self, UpdateThrobberEvent()) + + + def OnDestroyWindow(self, event): + self.Stop() + event.Skip() + + + def Draw(self, dc): + dc.DrawBitmap(self.submaps[self.sequence[self.current]], 0, 0, True) + if self.overlay and self.showOverlay: + dc.DrawBitmap(self.overlay, self.overlayX, self.overlayY, True) + if self.label and self.showLabel: + dc.DrawText(self.label, self.labelX, self.labelY) + dc.SetTextForeground(wx.WHITE) + dc.DrawText(self.label, self.labelX-1, self.labelY-1) + + + def OnPaint(self, event): + self.Draw(wx.PaintDC(self)) + event.Skip() + + + def Update(self, event): + self.Next() + + + def Wrap(self): + if self.current >= len(self.sequence): + if self.autoReverse: + self.Reverse() + self.current = len(self.sequence) - 1 + else: + self.current = 0 + if self.current < 0: + if self.autoReverse: + self.Reverse() + self.current = 0 + else: + self.current = len(self.sequence) - 1 + self.Draw(wx.ClientDC(self)) + + + # --------- public methods --------- + def SetFont(self, font): + """Set the font for the label""" + wx.Panel.SetFont(self, font) + self.SetLabel(self.label) + self.Draw(wx.ClientDC(self)) + + + def Rest(self): + """Stop the animation and return to frame 0""" + self.Stop() + self.current = self.rest + self.Draw(wx.ClientDC(self)) + + + def Reverse(self): + """Change the direction of the animation""" + self.direction = -self.direction + + + def Running(self): + """Returns True if the animation is running""" + return self.running + + + def Start(self): + """Start the animation""" + if not self.running: + self.running = not self.running + self.timer.Start(int(self.frameDelay * 1000)) + + + def Stop(self): + """Stop the animation""" + if self.running: + self.timer.Stop() + self.running = not self.running + + + def SetCurrent(self, current): + """Set current image""" + running = self.Running() + if not running: + #FIXME: need to make sure value is within range!!! + self.current = current + self.Draw(wx.ClientDC(self)) + + + def SetRest(self, rest): + """Set rest image""" + self.rest = rest + + + def SetSequence(self, sequence = None): + """Order to display images""" + + # self.sequence can be changed, but it's not recommended doing it + # while the throbber is running. self.sequence[0] should always + # refer to whatever frame is to be shown when 'resting' and be sure + # that no item in self.sequence >= self.frames or < 0!!! + + running = self.Running() + self.Stop() + + if sequence is not None: + #FIXME: need to make sure values are within range!!! + self.sequence = sequence + else: + self.sequence = range(self.frames) + + if running: + self.Start() + + + def Increment(self): + """Display next image in sequence""" + self.current += 1 + self.Wrap() + + + def Decrement(self): + """Display previous image in sequence""" + self.current -= 1 + self.Wrap() + + + def Next(self): + """Display next image in sequence according to direction""" + self.current += self.direction + self.Wrap() + + + def Previous(self): + """Display previous image in sequence according to direction""" + self.current -= self.direction + self.Wrap() + + + def SetFrameDelay(self, frameDelay = 0.05): + """Delay between each frame""" + self.frameDelay = frameDelay + if self.running: + self.Stop() + self.Start() + + + def ToggleOverlay(self, state = None): + """Toggle the overlay image""" + if state is None: + self.showOverlay = not self.showOverlay + else: + self.showOverlay = state + self.Draw(wx.ClientDC(self)) + + + def ToggleLabel(self, state = None): + """Toggle the label""" + if state is None: + self.showLabel = not self.showLabel + else: + self.showLabel = state + self.Draw(wx.ClientDC(self)) + + + def SetLabel(self, label): + """Change the text of the label""" + self.label = label + if label: + extentX, extentY = self.GetTextExtent(label) + self.labelX = (self.width - extentX)/2 + self.labelY = (self.height - extentY)/2 + self.Draw(wx.ClientDC(self)) + + + +# ------------------------------------------------------------------------------ + diff --git a/wx/lib/ticker.py b/wx/lib/ticker.py new file mode 100644 index 00000000..869e45ae --- /dev/null +++ b/wx/lib/ticker.py @@ -0,0 +1,215 @@ +#---------------------------------------------------------------------- +# Name: wx.lib.ticker +# Purpose: A news-ticker style scrolling text control +# +# Author: Chris Mellon +# +# Created: 29-Aug-2004 +# RCS-ID: $Id$ +# Copyright: (c) 2004 by Chris Mellon +# Licence: wxWindows license +#---------------------------------------------------------------------- + +"""News-ticker style scrolling text control + + * Can scroll from right to left or left to right. + + * Speed of the ticking is controlled by two parameters: + + - Frames per Second(FPS): How many times per second the ticker updates + + - Pixels per Frame(PPF): How many pixels the text moves each update + +Low FPS with high PPF will result in "jumpy" text, lower PPF with higher FPS +is smoother (but blurrier and more CPU intensive) text. +""" + +import wx + +#---------------------------------------------------------------------- + +class Ticker(wx.PyControl): + def __init__(self, + parent, + id=-1, + text=wx.EmptyString, #text in the ticker + fgcolor = wx.BLACK, #text/foreground color + bgcolor = wx.WHITE, #background color + start=True, #if True, the ticker starts immediately + ppf=2, #pixels per frame + fps=20, #frames per second + direction="rtl", #direction of ticking, rtl or ltr + pos=wx.DefaultPosition, size=wx.DefaultSize, style=wx.NO_BORDER, + name="Ticker" + ): + wx.PyControl.__init__(self, parent, id=id, pos=pos, size=size, style=style, name=name) + self.timer = wx.Timer(owner=self) + self._extent = (-1, -1) #cache value for the GetTextExtent call + self._offset = 0 + self._fps = fps #frames per second + self._ppf = ppf #pixels per frame + self.SetDirection(direction) + self.SetText(text) + self.SetInitialSize(size) + self.SetForegroundColour(fgcolor) + self.SetBackgroundColour(bgcolor) + wx.EVT_TIMER(self, -1, self.OnTick) + wx.EVT_PAINT(self, self.OnPaint) + wx.EVT_ERASE_BACKGROUND(self, self.OnErase) + if start: + self.Start() + + + def Stop(self): + """Stop moving the text""" + self.timer.Stop() + + + def Start(self): + """Starts the text moving""" + if not self.timer.IsRunning(): + self.timer.Start(1000 / self._fps) + + + def IsTicking(self): + """Is the ticker ticking? ie, is the text moving?""" + return self.timer.IsRunning() + + + def SetFPS(self, fps): + """Adjust the update speed of the ticker""" + self._fps = fps + self.Stop() + self.Start() + + + def GetFPS(self): + """Update speed of the ticker""" + return self._fps + + + def SetPPF(self, ppf): + """Set the number of pixels per frame the ticker moves - ie, how "jumpy" it is""" + self._ppf = ppf + + + def GetPPF(self): + """Pixels per frame""" + return self._ppf + + + def SetFont(self, font): + self._extent = (-1, -1) + wx.Control.SetFont(self, font) + + + def SetDirection(self, dir): + """Sets the direction of the ticker: right to left(rtl) or left to right (ltr)""" + if dir == "ltr" or dir == "rtl": + if self._offset <> 0: + #Change the offset so it's correct for the new direction + self._offset = self._extent[0] + self.GetSize()[0] - self._offset + self._dir = dir + else: + raise TypeError + + + def GetDirection(self): + return self._dir + + + def SetText(self, text): + """Set the ticker text.""" + self._text = text + self._extent = (-1, -1) + if not self._text: + self.Refresh() #Refresh here to clear away the old text. + + + def GetText(self): + return self._text + + + def UpdateExtent(self, dc): + """Updates the cached text extent if needed""" + if not self._text: + self._extent = (-1, -1) + return + if self._extent == (-1, -1): + self._extent = dc.GetTextExtent(self.GetText()) + + + def DrawText(self, dc): + """Draws the ticker text at the current offset using the provided DC""" + dc.SetTextForeground(self.GetForegroundColour()) + dc.SetFont(self.GetFont()) + self.UpdateExtent(dc) + if self._dir == "ltr": + offx = self._offset - self._extent[0] + else: + offx = self.GetSize()[0] - self._offset + offy = (self.GetSize()[1] - self._extent[1]) / 2 #centered vertically + dc.DrawText(self._text, offx, offy) + + + def OnTick(self, evt): + self._offset += self._ppf + w1 = self.GetSize()[0] + w2 = self._extent[0] + if self._offset >= w1+w2: + self._offset = 0 + self.Refresh() + + + def OnPaint(self, evt): + dc = wx.BufferedPaintDC(self) + brush = wx.Brush(self.GetBackgroundColour()) + dc.SetBackground(brush) + dc.Clear() + self.DrawText(dc) + + + def OnErase(self, evt): + """Noop because of double buffering""" + pass + + + def AcceptsFocus(self): + """Non-interactive, so don't accept focus""" + return False + + + def DoGetBestSize(self): + """Width we don't care about, height is either -1, or the character + height of our text with a little extra padding + """ + if self._extent == (-1, -1): + if not self._text: + h = self.GetCharHeight() + else: + h = self.GetTextExtent(self.GetText())[1] + else: + h = self._extent[1] + return (100, h+5) + + + def ShouldInheritColours(self): + """Don't get colours from our parent...""" + return False + + + +#testcase/demo +if __name__ == '__main__': + app = wx.PySimpleApp() + f = wx.Frame(None) + p = wx.Panel(f) + t = Ticker(p, text="Some sample ticker text") + #set ticker properties here if you want + s = wx.BoxSizer(wx.VERTICAL) + s.Add(t, flag=wx.GROW, proportion=0) + p.SetSizer(s) + f.Show() + app.MainLoop() + + diff --git a/wx/lib/ticker_xrc.py b/wx/lib/ticker_xrc.py new file mode 100644 index 00000000..38345968 --- /dev/null +++ b/wx/lib/ticker_xrc.py @@ -0,0 +1,49 @@ +#---------------------------------------------------------------------- +# Name: wx.lib.ticker_xrc +# Purpose: A XRC handler for wx.lib.ticker +# +# Author: Chris Mellon +# +# Created: 17-May-2005 +# RCS-ID: $Id$ +# Copyright: (c) 2005 by Chris Mellon +# Licence: wxWindows license +#---------------------------------------------------------------------- + +import wx +import wx.xrc as xrc +from wx.lib.ticker import Ticker + +class wxTickerXmlHandler(xrc.XmlResourceHandler): + def __init__(self): + xrc.XmlResourceHandler.__init__(self) + self.AddWindowStyles() + + def CanHandle(self, node): + return self.IsOfClass(node, "wxTicker") + + def DoCreateResource(self): + t = Ticker( + self.GetParentAsWindow(), + self.GetID(), + pos = self.GetPosition(), + size = self.GetSize(), + style=self.GetStyle() + ) + if self.HasParam("text"): + t.SetText(self.GetText("text")) + if self.HasParam("start"): + if self.GetBool("start"): + t.Start() + else: + t.Stop() + if self.HasParam("ppf"): + t.SetPPF(self.GetLong("ppf")) + if self.HasParam("fps"): + t.SetFPS(self.GetLong("fps")) + if self.HasParam("direction"): + t.SetDirection(self.GetText("direction")) + + self.SetupWindow(t) # handles font, bg/fg color + return t + diff --git a/wx/lib/utils.py b/wx/lib/utils.py new file mode 100644 index 00000000..25d8380d --- /dev/null +++ b/wx/lib/utils.py @@ -0,0 +1,79 @@ +#---------------------------------------------------------------------- +# Name: wx.lib.utils +# Purpose: Miscelaneous utility functions +# +# Author: Robin Dunn +# +# Created: 18-Jan-2009 +# RCS-ID: $Id$ +# Copyright: (c) 2009 Total Control Software +# Licence: wxWidgets license +#---------------------------------------------------------------------- + +""" +A few useful functions. (Ok, only one so far...) +""" + +import wx + +#--------------------------------------------------------------------------- + +def AdjustRectToScreen(rect, adjust=(0,0)): + """ + Compare the rect with the dinmensions of the display that the rect's + upper left corner is positioned on. If it doesn't fit entirely on + screen then attempt to make it do so either by repositioning the + rectangle, resizing it, or both. Returns the adjusted rectangle. + + If the adjustment value is given then it will be used to ensure that + the rectangle is at least that much smaller than the display's client + area. + """ + assert isinstance(rect, wx.Rect) + if -1 in rect.Get(): + # bail out if there are any -1's in the dimensions + return rect + + dispidx = wx.Display.GetFromPoint(rect.Position) + if dispidx == wx.NOT_FOUND: + dispidx = 0 + ca = wx.Display(dispidx).GetClientArea() + assert isinstance(ca, wx.Rect) + + # is it already fully visible? + if ca.ContainsRect(rect): + return rect + + # if not then try adjusting the position + if not ca.Contains(rect.Position): + rect.Position = ca.Position + if ca.ContainsRect(rect): + return rect + dx = dy = 0 + if rect.right > ca.right: + dx = ca.right - rect.right + if rect.bottom > ca.bottom: + dy = ca.bottom - rect.bottom + rect.OffsetXY(dx, dy) + + # if the rectangle has been moved too far, then readjust the position + # and also adjust the size + if rect.left < ca.left: + rect.width -= (ca.left - rect.left) + rect.left = ca.left + if rect.top < ca.top: + rect.height -= (ca.top - rect.top) + rect.top = ca.top + + # make final adjustments if needed + adjust = wx.Size(*adjust) + if rect.width > (ca.width - adjust.width): + rect.width = ca.width - adjust.width + if rect.height > (ca.height - adjust.height): + rect.height = ca.height - adjust.height + + # return the result + return rect + + +#--------------------------------------------------------------------------- diff --git a/wx/lib/wordwrap.py b/wx/lib/wordwrap.py new file mode 100644 index 00000000..d8aa81e4 --- /dev/null +++ b/wx/lib/wordwrap.py @@ -0,0 +1,97 @@ +#---------------------------------------------------------------------- +# Name: wx.lib.wordwrap +# Purpose: Contains a function to aid in word-wrapping some text +# +# Author: Robin Dunn +# +# Created: 15-Oct-2006 +# RCS-ID: $Id$ +# Copyright: (c) 2006 by Total Control Software +# Licence: wxWindows license +#---------------------------------------------------------------------- + +def wordwrap(text, width, dc, breakLongWords=True, margin=0): + """ + Returns a copy of text with newline characters inserted where long + lines should be broken such that they will fit within the given + width, with the given margin left and right, on the given `wx.DC` + using its current font settings. By default words that are wider + than the margin-adjusted width will be broken at the nearest + character boundary, but this can be disabled by passing ``False`` + for the ``breakLongWords`` parameter. + """ + + wrapped_lines = [] + text = text.split('\n') + for line in text: + pte = dc.GetPartialTextExtents(line) + wid = ( width - (2*margin+1)*dc.GetTextExtent(' ')[0] + - max([0] + [pte[i]-pte[i-1] for i in range(1,len(pte))]) ) + idx = 0 + start = 0 + startIdx = 0 + spcIdx = -1 + while idx < len(pte): + # remember the last seen space + if line[idx] == ' ': + spcIdx = idx + + # have we reached the max width? + if pte[idx] - start > wid and (spcIdx != -1 or breakLongWords): + if spcIdx != -1: + idx = min(spcIdx + 1, len(pte) - 1) + wrapped_lines.append(' '*margin + line[startIdx : idx] + ' '*margin) + start = pte[idx] + startIdx = idx + spcIdx = -1 + + idx += 1 + + wrapped_lines.append(' '*margin + line[startIdx : idx] + ' '*margin) + + return '\n'.join(wrapped_lines) + + + +if __name__ == '__main__': + import wx + class TestPanel(wx.Panel): + def __init__(self, parent): + wx.Panel.__init__(self, parent) + + self.tc = wx.TextCtrl(self, -1, "", (20,20), (150,150), wx.TE_MULTILINE) + self.Bind(wx.EVT_TEXT, self.OnDoUpdate, self.tc) + self.Bind(wx.EVT_SIZE, self.OnSize) + + + def OnSize(self, evt): + wx.CallAfter(self.OnDoUpdate, None) + + + def OnDoUpdate(self, evt): + WIDTH = self.GetSize().width - 220 + HEIGHT = 200 + bmp = wx.EmptyBitmap(WIDTH, HEIGHT) + mdc = wx.MemoryDC(bmp) + mdc.SetBackground(wx.Brush("white")) + mdc.Clear() + mdc.SetPen(wx.Pen("black")) + mdc.SetFont(wx.Font(10, wx.SWISS, wx.NORMAL, wx.NORMAL)) + mdc.DrawRectangle(0,0, WIDTH, HEIGHT) + + text = wordwrap(self.tc.GetValue(), WIDTH-2, mdc, False) + #print repr(text) + mdc.DrawLabel(text, (1,1, WIDTH-2, HEIGHT-2)) + + del mdc + dc = wx.ClientDC(self) + dc.DrawBitmap(bmp, 200, 20) + + + app = wx.App(False) + frm = wx.Frame(None, title="Test wordWrap") + pnl = TestPanel(frm) + frm.Show() + app.MainLoop() + + diff --git a/wx/lib/wxPlotCanvas.py b/wx/lib/wxPlotCanvas.py new file mode 100644 index 00000000..8b96c6e0 --- /dev/null +++ b/wx/lib/wxPlotCanvas.py @@ -0,0 +1,489 @@ +""" +This is a port of Konrad Hinsen's tkPlotCanvas.py plotting module. +After thinking long and hard I came up with the name "wxPlotCanvas.py". + +This file contains two parts; first the re-usable library stuff, then, after +a "if __name__=='__main__'" test, a simple frame and a few default plots +for testing. + +Harm van der Heijden, feb 1999 + +Original comment follows below: +# This module defines a plot widget for Tk user interfaces. +# It supports only elementary line plots at the moment. +# See the example at the end for documentation... +# +# Written by Konrad Hinsen +# With contributions from RajGopal Srinivasan +# Last revision: 1998-7-28 +# +""" +# 12/13/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o Updated for V2.5 compatability +# o wx.SpinCtl has some issues that cause the control to +# lock up. Noted in other places using it too, it's not this module +# that's at fault. +# o Added deprecation warning. +# + +import warnings +import wx + +warningmsg = r"""\ + +THIS MODULE IS NOW DEPRECATED + +This module has been replaced by wxPyPlot, which in wxPython +can be found in wx.lib.plot.py. + +""" + +warnings.warn(warningmsg, DeprecationWarning, stacklevel=2) + +# Not everybody will have Numeric, so let's be cool about it... +try: + import Numeric +except: + # bummer! + msg = """This module requires the Numeric module, which could not be +imported. It probably is not installed (it's not part of the standard +Python distribution). See the Python site (http://www.python.org) for +information on downloading source or binaries.""" + + print msg + if wx.Platform == '__WXMSW__' and wx.GetApp() is not None: + d = wx.MessageDialog(None, msg, "Numeric not found") + if d.ShowModal() == wx.ID_CANCEL: + d = wx.MessageDialog(None, "I kid you not! Pressing Cancel won't help you!", "Not a joke", wx.OK) + d.ShowModal() + raise + +# +# Plotting classes... +# +class PolyPoints: + + def __init__(self, points, attr): + self.points = Numeric.array(points) + self.scaled = self.points + self.attributes = {} + for name, value in self._attributes.items(): + try: + value = attr[name] + except KeyError: pass + self.attributes[name] = value + + def boundingBox(self): + return Numeric.minimum.reduce(self.points), \ + Numeric.maximum.reduce(self.points) + + def scaleAndShift(self, scale=1, shift=0): + self.scaled = scale*self.points+shift + + +class PolyLine(PolyPoints): + + def __init__(self, points, **attr): + PolyPoints.__init__(self, points, attr) + + _attributes = {'color': 'black', + 'width': 1} + + def draw(self, dc): + color = self.attributes['color'] + width = self.attributes['width'] + arguments = [] + dc.SetPen(wx.Pen(wx.NamedColour(color), width)) + dc.DrawLines(map(tuple,self.scaled)) + + +class PolyMarker(PolyPoints): + + def __init__(self, points, **attr): + + PolyPoints.__init__(self, points, attr) + + _attributes = {'color': 'black', + 'width': 1, + 'fillcolor': None, + 'size': 2, + 'fillstyle': wx.SOLID, + 'outline': 'black', + 'marker': 'circle'} + + def draw(self, dc): + color = self.attributes['color'] + width = self.attributes['width'] + size = self.attributes['size'] + fillcolor = self.attributes['fillcolor'] + fillstyle = self.attributes['fillstyle'] + marker = self.attributes['marker'] + + dc.SetPen(wx.Pen(wx.NamedColour(color),width)) + if fillcolor: + dc.SetBrush(wx.Brush(wx.NamedColour(fillcolor),fillstyle)) + else: + dc.SetBrush(wx.Brush(wx.NamedColour('black'), wx.TRANSPARENT)) + + self._drawmarkers(dc, self.scaled, marker, size) + + def _drawmarkers(self, dc, coords, marker,size=1): + f = eval('self._' +marker) + for xc, yc in coords: + f(dc, xc, yc, size) + + def _circle(self, dc, xc, yc, size=1): + dc.DrawEllipse(xc-2.5*size,yc-2.5*size, 5.*size,5.*size) + + def _dot(self, dc, xc, yc, size=1): + dc.DrawPoint(xc,yc) + + def _square(self, dc, xc, yc, size=1): + dc.DrawRectangle(xc-2.5*size,yc-2.5*size,5.*size,5.*size) + + def _triangle(self, dc, xc, yc, size=1): + dc.DrawPolygon([(-0.5*size*5,0.2886751*size*5), + (0.5*size*5,0.2886751*size*5), + (0.0,-0.577350*size*5)],xc,yc) + + def _triangle_down(self, dc, xc, yc, size=1): + dc.DrawPolygon([(-0.5*size*5,-0.2886751*size*5), + (0.5*size*5,-0.2886751*size*5), + (0.0,0.577350*size*5)],xc,yc) + + def _cross(self, dc, xc, yc, size=1): + dc.DrawLine(xc-2.5*size, yc-2.5*size, xc+2.5*size,yc+2.5*size) + dc.DrawLine(xc-2.5*size,yc+2.5*size, xc+2.5*size,yc-2.5*size) + + def _plus(self, dc, xc, yc, size=1): + dc.DrawLine(xc-2.5*size,yc, xc+2.5*size,yc) + dc.DrawLine(xc,yc-2.5*size,xc, yc+2.5*size) + +class PlotGraphics: + + def __init__(self, objects): + self.objects = objects + + def boundingBox(self): + p1, p2 = self.objects[0].boundingBox() + for o in self.objects[1:]: + p1o, p2o = o.boundingBox() + p1 = Numeric.minimum(p1, p1o) + p2 = Numeric.maximum(p2, p2o) + return p1, p2 + + def scaleAndShift(self, scale=1, shift=0): + for o in self.objects: + o.scaleAndShift(scale, shift) + + def draw(self, canvas): + for o in self.objects: + o.draw(canvas) + + def __len__(self): + return len(self.objects) + + def __getitem__(self, item): + return self.objects[item] + + +class PlotCanvas(wx.Window): + + def __init__(self, parent, id=-1, + pos = wx.DefaultPosition, size = wx.DefaultSize, + style = 0, name = 'plotCanvas'): + wx.Window.__init__(self, parent, id, pos, size, style, name) + self.border = (1,1) + self.SetClientSize((400,400)) + self.SetBackgroundColour("white") + + self.Bind(wx.EVT_SIZE,self.reconfigure) + self.Bind(wx.EVT_PAINT, self.OnPaint) + self._setsize() + self.last_draw = None +# self.font = self._testFont(font) + + def OnPaint(self, event): + pdc = wx.PaintDC(self) + if self.last_draw is not None: + apply(self.draw, self.last_draw + (pdc,)) + + def reconfigure(self, event): + (new_width,new_height) = self.GetClientSize() + if new_width == self.width and new_height == self.height: + return + self._setsize() + # self.redraw() + + def _testFont(self, font): + if font is not None: + bg = self.canvas.cget('background') + try: + item = CanvasText(self.canvas, 0, 0, anchor=NW, + text='0', fill=bg, font=font) + self.canvas.delete(item) + except TclError: + font = None + return font + + def _setsize(self): + (self.width,self.height) = self.GetClientSize(); + self.plotbox_size = 0.97*Numeric.array([self.width, -self.height]) + xo = 0.5*(self.width-self.plotbox_size[0]) + yo = self.height-0.5*(self.height+self.plotbox_size[1]) + self.plotbox_origin = Numeric.array([xo, yo]) + + def draw(self, graphics, xaxis = None, yaxis = None, dc = None): + if dc == None: dc = wx.ClientDC(self) + dc.BeginDrawing() + dc.Clear() + self.last_draw = (graphics, xaxis, yaxis) + p1, p2 = graphics.boundingBox() + xaxis = self._axisInterval(xaxis, p1[0], p2[0]) + yaxis = self._axisInterval(yaxis, p1[1], p2[1]) + text_width = [0., 0.] + text_height = [0., 0.] + if xaxis is not None: + p1[0] = xaxis[0] + p2[0] = xaxis[1] + xticks = self._ticks(xaxis[0], xaxis[1]) + bb = dc.GetTextExtent(xticks[0][1]) + text_height[1] = bb[1] + text_width[0] = 0.5*bb[0] + bb = dc.GetTextExtent(xticks[-1][1]) + text_width[1] = 0.5*bb[0] + else: + xticks = None + if yaxis is not None: + p1[1] = yaxis[0] + p2[1] = yaxis[1] + yticks = self._ticks(yaxis[0], yaxis[1]) + for y in yticks: + bb = dc.GetTextExtent(y[1]) + text_width[0] = max(text_width[0],bb[0]) + h = 0.5*bb[1] + text_height[0] = h + text_height[1] = max(text_height[1], h) + else: + yticks = None + text1 = Numeric.array([text_width[0], -text_height[1]]) + text2 = Numeric.array([text_width[1], -text_height[0]]) + scale = (self.plotbox_size-text1-text2) / (p2-p1) + shift = -p1*scale + self.plotbox_origin + text1 + self._drawAxes(dc, xaxis, yaxis, p1, p2, + scale, shift, xticks, yticks) + graphics.scaleAndShift(scale, shift) + graphics.draw(dc) + dc.EndDrawing() + + def _axisInterval(self, spec, lower, upper): + if spec is None: + return None + if spec == 'minimal': + if lower == upper: + return lower-0.5, upper+0.5 + else: + return lower, upper + if spec == 'automatic': + range = upper-lower + if range == 0.: + return lower-0.5, upper+0.5 + log = Numeric.log10(range) + power = Numeric.floor(log) + fraction = log-power + if fraction <= 0.05: + power = power-1 + grid = 10.**power + lower = lower - lower % grid + mod = upper % grid + if mod != 0: + upper = upper - mod + grid + return lower, upper + if type(spec) == type(()): + lower, upper = spec + if lower <= upper: + return lower, upper + else: + return upper, lower + raise ValueError, str(spec) + ': illegal axis specification' + + def _drawAxes(self, dc, xaxis, yaxis, + bb1, bb2, scale, shift, xticks, yticks): + dc.SetPen(wx.Pen(wx.NamedColour('BLACK'),1)) + if xaxis is not None: + lower, upper = xaxis + text = 1 + for y, d in [(bb1[1], -3), (bb2[1], 3)]: + p1 = scale*Numeric.array([lower, y])+shift + p2 = scale*Numeric.array([upper, y])+shift + dc.DrawLine(p1[0],p1[1], p2[0],p2[1]) + for x, label in xticks: + p = scale*Numeric.array([x, y])+shift + dc.DrawLine(p[0],p[1], p[0],p[1]+d) + if text: + dc.DrawText(label, p[0],p[1]) + text = 0 + + if yaxis is not None: + lower, upper = yaxis + text = 1 + h = dc.GetCharHeight() + for x, d in [(bb1[0], -3), (bb2[0], 3)]: + p1 = scale*Numeric.array([x, lower])+shift + p2 = scale*Numeric.array([x, upper])+shift + dc.DrawLine(p1[0],p1[1], p2[0],p2[1]) + for y, label in yticks: + p = scale*Numeric.array([x, y])+shift + dc.DrawLine(p[0],p[1], p[0]-d,p[1]) + if text: + dc.DrawText(label, + p[0]-dc.GetTextExtent(label)[0], p[1]-0.5*h) + text = 0 + + def _ticks(self, lower, upper): + ideal = (upper-lower)/7. + log = Numeric.log10(ideal) + power = Numeric.floor(log) + fraction = log-power + factor = 1. + error = fraction + for f, lf in self._multiples: + e = Numeric.fabs(fraction-lf) + if e < error: + error = e + factor = f + grid = factor * 10.**power + if power > 3 or power < -3: + format = '%+7.0e' + elif power >= 0: + digits = max(1, int(power)) + format = '%' + `digits`+'.0f' + else: + digits = -int(power) + format = '%'+`digits+2`+'.'+`digits`+'f' + ticks = [] + t = -grid*Numeric.floor(-lower/grid) + while t <= upper: + ticks.append( (t, format % (t,)) ) + t = t + grid + return ticks + + _multiples = [(2., Numeric.log10(2.)), (5., Numeric.log10(5.))] + + def redraw(self,dc=None): + if self.last_draw is not None: + apply(self.draw, self.last_draw + (dc,)) + + def clear(self): + self.canvas.delete('all') + +#--------------------------------------------------------------------------- +# if running standalone... +# +# ...a sample implementation using the above +# + + +if __name__ == '__main__': + def _InitObjects(): + # 100 points sin function, plotted as green circles + data1 = 2.*Numeric.pi*Numeric.arange(200)/200. + data1.shape = (100, 2) + data1[:,1] = Numeric.sin(data1[:,0]) + markers1 = PolyMarker(data1, color='green', marker='circle',size=1) + + # 50 points cos function, plotted as red line + data1 = 2.*Numeric.pi*Numeric.arange(100)/100. + data1.shape = (50,2) + data1[:,1] = Numeric.cos(data1[:,0]) + lines = PolyLine(data1, color='red') + + # A few more points... + pi = Numeric.pi + markers2 = PolyMarker([(0., 0.), (pi/4., 1.), (pi/2, 0.), + (3.*pi/4., -1)], color='blue', + fillcolor='green', marker='cross') + + return PlotGraphics([markers1, lines, markers2]) + + + class AppFrame(wx.Frame): + def __init__(self, parent, id, title): + wx.Frame.__init__(self, parent, id, title, + wx.DefaultPosition, (400, 400)) + + # Now Create the menu bar and items + self.mainmenu = wx.MenuBar() + + menu = wx.Menu() + menu.Append(200, '&Print...', 'Print the current plot') + self.Bind(wx.EVT_MENU, self.OnFilePrint, id=200) + menu.Append(209, 'E&xit', 'Enough of this already!') + self.Bind(wx.EVT_MENU, self.OnFileExit, id=209) + self.mainmenu.Append(menu, '&File') + + menu = wx.Menu() + menu.Append(210, '&Draw', 'Draw plots') + self.Bind(wx.EVT_MENU,self.OnPlotDraw, id=210) + menu.Append(211, '&Redraw', 'Redraw plots') + self.Bind(wx.EVT_MENU,self.OnPlotRedraw, id=211) + menu.Append(212, '&Clear', 'Clear canvas') + self.Bind(wx.EVT_MENU,self.OnPlotClear, id=212) + self.mainmenu.Append(menu, '&Plot') + + menu = wx.Menu() + menu.Append(220, '&About', 'About this thing...') + self.Bind(wx.EVT_MENU, self.OnHelpAbout, id=220) + self.mainmenu.Append(menu, '&Help') + + self.SetMenuBar(self.mainmenu) + + # A status bar to tell people what's happening + self.CreateStatusBar(1) + + self.client = PlotCanvas(self) + + def OnFilePrint(self, event): + d = wx.MessageDialog(self, +"""As of this writing, printing support in wxPython is shaky at best. +Are you sure you want to do this?""", "Danger!", wx.YES_NO) + if d.ShowModal() == wx.ID_YES: + psdc = wx.PostScriptDC("out.ps", True, self) + self.client.redraw(psdc) + + def OnFileExit(self, event): + self.Close() + + def OnPlotDraw(self, event): + self.client.draw(_InitObjects(),'automatic','automatic'); + + def OnPlotRedraw(self,event): + self.client.redraw() + + def OnPlotClear(self,event): + self.client.last_draw = None + dc = wx.ClientDC(self.client) + dc.Clear() + + def OnHelpAbout(self, event): + about = wx.MessageDialog(self, __doc__, "About...", wx.OK) + about.ShowModal() + + + + class MyApp(wx.App): + def OnInit(self): + frame = AppFrame(None, -1, "wxPlotCanvas") + frame.Show(True) + self.SetTopWindow(frame) + return True + + + app = MyApp(0) + app.MainLoop() + + + + +#---------------------------------------------------------------------------- diff --git a/wx/lib/wxcairo.py b/wx/lib/wxcairo.py new file mode 100644 index 00000000..acfb69c8 --- /dev/null +++ b/wx/lib/wxcairo.py @@ -0,0 +1,466 @@ +#---------------------------------------------------------------------- +# Name: wx.lib.wxcairo +# Purpose: Glue code to allow the pycairo package to be used +# with a wx.DC as the cairo surface. +# +# Author: Robin Dunn +# +# Created: 3-Sept-2008 +# RCS-ID: $Id$ +# Copyright: (c) 2008 by Total Control Software +# Licence: wxWindows license +#---------------------------------------------------------------------- + +""" +This module provides some glue code that allows the pycairo package to +be used for drawing direclty on wx.DCs. In cairo terms, the DC is the +drawing surface. The `CairoContextFromDC` function in this module +will return an instance of the pycairo Context class that is ready for +drawing, using the native cairo surface type for the current platform. + +This module requires the pycairo pacakge, and makes use of ctypes for +fetching the pycairo C API and also for digging into the cairo library +itself. + +To use Cairo with wxPython you will need to have a few dependencies +installed. On Linux and other unix-like systems you may already have +them, or can easily get them with your system's package manager. Just +check if libcairo and pycairo are installed. + +On Mac you can get Cairo from MacPorts or Fink. If you are also using +MacPorts or Fink for your Python installation then you should be able +to get pycairo the same way. Otherwise it's real easy to build and +install pycairo for the Python framework installation. Just get the +source tarball from http://pypi.python.org/pypi/pycairo and do the +normal 'python setup.py install' dance. + +On Windows you can get a Cairo DLL from here: + + http://www.gtk.org/download/win32.php + +You'll also want to get the zlib and libpng binaries from the same +page. Once you get those files extract the DLLs from each of the zip +files and copy them to some place on your PATH. Finally, there is an +installer for the pycairo pacakge here: + + http://wxpython.org/cairo/ + +""" + +# TODO: Support printer surfaces? + + +import wx +import cairo +import ctypes +import ctypes.util + + +#---------------------------------------------------------------------------- + +# A reference to the cairo shared lib via ctypes.CDLL +cairoLib = None + +# A reference to the pycairo C API structure +pycairoAPI = None + + +# a convenience funtion, just to save a bit of typing below +def voidp(ptr): + """Convert a SWIGged void* type to a ctypes c_void_p""" + return ctypes.c_void_p(int(ptr)) + +#---------------------------------------------------------------------------- + +def ContextFromDC(dc): + """ + Creates and returns a Cairo context object using the wxDC as the + surface. (Only window, client, paint and memory DC's are allowed + at this time.) + """ + if not isinstance(dc, wx.WindowDC) and not isinstance(dc, wx.MemoryDC): + raise TypeError, "Only window and memory DC's are supported at this time." + + if 'wxMac' in wx.PlatformInfo: + width, height = dc.GetSize() + + # use the CGContextRef of the DC to make the cairo surface + cgc = dc.GetCGContext() + assert cgc is not None, "Unable to get CGContext from DC." + cgref = voidp( cgc ) + surfaceptr = voidp( + cairoLib.cairo_quartz_surface_create_for_cg_context( + cgref, width, height) ) + + # create a cairo context for that surface + ctxptr = cairoLib.cairo_create(surfaceptr) + + # Turn it into a pycairo context object + ctx = pycairoAPI.Context_FromContext(ctxptr, pycairoAPI.Context_Type, None) + + # The context keeps its own reference to the surface + cairoLib.cairo_surface_destroy(surfaceptr) + + + elif 'wxMSW' in wx.PlatformInfo: + # This one is easy, just fetch the HDC and use PyCairo to make + # the surface and context. + hdc = dc.GetHDC() + surface = cairo.Win32Surface(hdc) + ctx = cairo.Context(surface) + + + elif 'wxGTK' in wx.PlatformInfo: + gdkLib = _findGDKLib() + + # Get the GdkDrawable from the dc + drawable = voidp( dc.GetGdkDrawable() ) + + # Call a GDK API to create a cairo context + gdkLib.gdk_cairo_create.restype = ctypes.c_void_p + ctxptr = gdkLib.gdk_cairo_create(drawable) + + # Turn it into a pycairo context object + ctx = pycairoAPI.Context_FromContext(ctxptr, pycairoAPI.Context_Type, None) + + else: + raise NotImplementedError, "Help me, I'm lost..." + + return ctx + + +#---------------------------------------------------------------------------- + +def FontFaceFromFont(font): + """ + Creates and returns a cairo.FontFace object from the native + information in a wx.Font. + """ + + if 'wxMac' in wx.PlatformInfo: + fontfaceptr = voidp( + cairoLib.cairo_quartz_font_face_create_for_cgfont( + voidp(font.OSXGetCGFont())) ) + fontface = pycairoAPI.FontFace_FromFontFace(fontfaceptr) + + + elif 'wxMSW' in wx.PlatformInfo: + fontfaceptr = voidp( cairoLib.cairo_win32_font_face_create_for_hfont( + ctypes.c_ulong(font.GetHFONT())) ) + fontface = pycairoAPI.FontFace_FromFontFace(fontfaceptr) + + + elif 'wxGTK' in wx.PlatformInfo: + gdkLib = _findGDKLib() + pcLib = _findPangoCairoLib() + + # wow, this is a hell of a lot of steps... + desc = voidp( font.GetPangoFontDescription() ) + + pcLib.pango_cairo_font_map_get_default.restype = ctypes.c_void_p + pcfm = voidp(pcLib.pango_cairo_font_map_get_default()) + + gdkLib.gdk_pango_context_get.restype = ctypes.c_void_p + pctx = voidp(gdkLib.gdk_pango_context_get()) + + pcLib.pango_font_map_load_font.restype = ctypes.c_void_p + pfnt = voidp( pcLib.pango_font_map_load_font(pcfm, pctx, desc) ) + + pcLib.pango_cairo_font_get_scaled_font.restype = ctypes.c_void_p + scaledfontptr = voidp( pcLib.pango_cairo_font_get_scaled_font(pfnt) ) + + cairoLib.cairo_scaled_font_get_font_face.restype = ctypes.c_void_p + fontfaceptr = voidp(cairoLib.cairo_scaled_font_get_font_face(scaledfontptr)) + cairoLib.cairo_font_face_reference(fontfaceptr) + + fontface = pycairoAPI.FontFace_FromFontFace(fontfaceptr) + + gdkLib.g_object_unref(pctx) + + else: + raise NotImplementedError, "Help me, I'm lost..." + + return fontface + + +#---------------------------------------------------------------------------- +# wxBitmap <--> ImageSurface + +def BitmapFromImageSurface(surface): + """ + Create a wx.Bitmap from a Cairo ImageSurface. + """ + format = surface.get_format() + if format not in [cairo.FORMAT_ARGB32, cairo.FORMAT_RGB24]: + raise TypeError("Unsupported format") + + width = surface.get_width() + height = surface.get_height() + stride = surface.get_stride() + data = surface.get_data() + if format == cairo.FORMAT_ARGB32: + fmt = wx.BitmapBufferFormat_ARGB32 + else: + fmt = wx.BitmapBufferFormat_RGB32 + + bmp = wx.EmptyBitmap(width, height, 32) + bmp.CopyFromBuffer(data, fmt, stride) + return bmp + + +def ImageSurfaceFromBitmap(bitmap): + """ + Create an ImageSurface from a wx.Bitmap + """ + width, height = bitmap.GetSize() + if bitmap.HasAlpha(): + format = cairo.FORMAT_ARGB32 + fmt = wx.BitmapBufferFormat_ARGB32 + else: + format = cairo.FORMAT_RGB24 + fmt = wx.BitmapBufferFormat_RGB32 + + try: + stride = cairo.ImageSurface.format_stride_for_width(format, width) + except AttributeError: + stride = width * 4 + + surface = cairo.ImageSurface(format, width, height) + bitmap.CopyToBuffer(surface.get_data(), fmt, stride) + surface.mark_dirty() + return surface + + +#---------------------------------------------------------------------------- +# Only implementation helpers after this point +#---------------------------------------------------------------------------- + +def _findCairoLib(): + """ + Try to locate the Cairo shared library and make a CDLL for it. + """ + global cairoLib + if cairoLib is not None: + return + + names = ['cairo', 'cairo-2', 'libcairo', 'libcairo-2'] + + # first look using just the base name + for name in names: + try: + cairoLib = ctypes.CDLL(name) + return + except: + pass + + # if that didn't work then use the ctypes util to search the paths + # appropriate for the system + for name in names: + location = ctypes.util.find_library(name) + try: + cairoLib = ctypes.CDLL(location) + return + except: + pass + + # If the above didn't find it on OS X then we still have a + # trick up our sleeve... + if 'wxMac' in wx.PlatformInfo: + # look at the libs linked to by the pycairo extension module + import macholib.MachO + m = macholib.MachO.MachO(cairo._cairo.__file__) + for h in m.headers: + for idx, name, path in h.walkRelocatables(): + if 'libcairo' in path: + try: + cairoLib = ctypes.CDLL(path) + return + except: + pass + + if not cairoLib: + raise RuntimeError, "Unable to find the Cairo shared library" + + + +#---------------------------------------------------------------------------- + +# For other DLLs we'll just use a dictionary to track them, as there +# probably isn't any need to use them outside of this module. +_dlls = dict() + +def _findHelper(names, key, msg): + dll = _dlls.get(key, None) + if dll is not None: + return dll + location = None + for name in names: + location = ctypes.util.find_library(name) + if location: + break + if not location: + raise RuntimeError, msg + dll = ctypes.CDLL(location) + _dlls[key] = dll + return dll + + +def _findGDKLib(): + return _findHelper(['gdk-x11-2.0'], 'gdk', + "Unable to find the GDK shared library") + +def _findPangoCairoLib(): + return _findHelper(['pangocairo-1.0'], 'pangocairo', + "Unable to find the pangocairo shared library") +def _findAppSvcLib(): + return _findHelper(['ApplicationServices'], 'appsvc', + "Unable to find the ApplicationServices Framework") + + +#---------------------------------------------------------------------------- + +# Pycairo exports a C API in a structure via a PyCObject. Using +# ctypes will let us use that API from Python too. We'll use it to +# convert a C pointer value to pycairo objects. The information about +# this API structure is gleaned from pycairo.h. + +class Pycairo_CAPI(ctypes.Structure): + if cairo.version_info < (1,8): # This structure is known good with pycairo 1.6.4 + _fields_ = [ + ('Context_Type', ctypes.py_object), + ('Context_FromContext', ctypes.PYFUNCTYPE(ctypes.py_object, + ctypes.c_void_p, + ctypes.py_object, + ctypes.py_object)), + ('FontFace_Type', ctypes.py_object), + ('FontFace_FromFontFace', ctypes.PYFUNCTYPE(ctypes.py_object, ctypes.c_void_p)), + ('FontOptions_Type', ctypes.py_object), + ('FontOptions_FromFontOptions', ctypes.PYFUNCTYPE(ctypes.py_object, ctypes.c_void_p)), + ('Matrix_Type', ctypes.py_object), + ('Matrix_FromMatrix', ctypes.PYFUNCTYPE(ctypes.py_object, ctypes.c_void_p)), + ('Path_Type', ctypes.py_object), + ('Path_FromPath', ctypes.PYFUNCTYPE(ctypes.py_object, ctypes.c_void_p)), + ('Pattern_Type', ctypes.py_object), + ('SolidPattern_Type', ctypes.py_object), + ('SurfacePattern_Type', ctypes.py_object), + ('Gradient_Type', ctypes.py_object), + ('LinearGradient_Type', ctypes.py_object), + ('RadialGradient_Type', ctypes.py_object), + ('Pattern_FromPattern', ctypes.PYFUNCTYPE(ctypes.py_object, ctypes.c_void_p)), + ('ScaledFont_Type', ctypes.py_object), + ('ScaledFont_FromScaledFont', ctypes.PYFUNCTYPE(ctypes.py_object, ctypes.c_void_p)), + ('Surface_Type', ctypes.py_object), + ('ImageSurface_Type', ctypes.py_object), + ('PDFSurface_Type', ctypes.py_object), + ('PSSurface_Type', ctypes.py_object), + ('SVGSurface_Type', ctypes.py_object), + ('Win32Surface_Type', ctypes.py_object), + ('XlibSurface_Type', ctypes.py_object), + ('Surface_FromSurface', ctypes.PYFUNCTYPE(ctypes.py_object, + ctypes.c_void_p, + ctypes.py_object)), + ('Check_Status', ctypes.PYFUNCTYPE(ctypes.c_int, ctypes.c_int))] + + # This structure is known good with pycairo 1.8.4. + # We have to also test for (1,10,8) because pycairo 1.8.10 has an + # incorrect version_info value + elif cairo.version_info < (1,9) or cairo.version_info == (1,10,8): + _fields_ = [ + ('Context_Type', ctypes.py_object), + ('Context_FromContext', ctypes.PYFUNCTYPE(ctypes.py_object, + ctypes.c_void_p, + ctypes.py_object, + ctypes.py_object)), + ('FontFace_Type', ctypes.py_object), + ('ToyFontFace_Type', ctypes.py_object), #** new in 1.8.4 + ('FontFace_FromFontFace', ctypes.PYFUNCTYPE(ctypes.py_object, ctypes.c_void_p)), + ('FontOptions_Type', ctypes.py_object), + ('FontOptions_FromFontOptions', ctypes.PYFUNCTYPE(ctypes.py_object, ctypes.c_void_p)), + ('Matrix_Type', ctypes.py_object), + ('Matrix_FromMatrix', ctypes.PYFUNCTYPE(ctypes.py_object, ctypes.c_void_p)), + ('Path_Type', ctypes.py_object), + ('Path_FromPath', ctypes.PYFUNCTYPE(ctypes.py_object, ctypes.c_void_p)), + ('Pattern_Type', ctypes.py_object), + ('SolidPattern_Type', ctypes.py_object), + ('SurfacePattern_Type', ctypes.py_object), + ('Gradient_Type', ctypes.py_object), + ('LinearGradient_Type', ctypes.py_object), + ('RadialGradient_Type', ctypes.py_object), + ('Pattern_FromPattern', ctypes.PYFUNCTYPE(ctypes.py_object, ctypes.c_void_p, + ctypes.py_object)), #** changed in 1.8.4 + ('ScaledFont_Type', ctypes.py_object), + ('ScaledFont_FromScaledFont', ctypes.PYFUNCTYPE(ctypes.py_object, ctypes.c_void_p)), + ('Surface_Type', ctypes.py_object), + ('ImageSurface_Type', ctypes.py_object), + ('PDFSurface_Type', ctypes.py_object), + ('PSSurface_Type', ctypes.py_object), + ('SVGSurface_Type', ctypes.py_object), + ('Win32Surface_Type', ctypes.py_object), + ('XlibSurface_Type', ctypes.py_object), + ('Surface_FromSurface', ctypes.PYFUNCTYPE(ctypes.py_object, + ctypes.c_void_p, + ctypes.py_object)), + ('Check_Status', ctypes.PYFUNCTYPE(ctypes.c_int, ctypes.c_int))] + + # This structure is known good with pycairo 1.10.0. They keep adding stuff + # to the middle of the structure instead of only adding to the end! + elif cairo.version_info < (1,11): + _fields_ = [ + ('Context_Type', ctypes.py_object), + ('Context_FromContext', ctypes.PYFUNCTYPE(ctypes.py_object, + ctypes.c_void_p, + ctypes.py_object, + ctypes.py_object)), + ('FontFace_Type', ctypes.py_object), + ('ToyFontFace_Type', ctypes.py_object), + ('FontFace_FromFontFace', ctypes.PYFUNCTYPE(ctypes.py_object, ctypes.c_void_p)), + ('FontOptions_Type', ctypes.py_object), + ('FontOptions_FromFontOptions', ctypes.PYFUNCTYPE(ctypes.py_object, ctypes.c_void_p)), + ('Matrix_Type', ctypes.py_object), + ('Matrix_FromMatrix', ctypes.PYFUNCTYPE(ctypes.py_object, ctypes.c_void_p)), + ('Path_Type', ctypes.py_object), + ('Path_FromPath', ctypes.PYFUNCTYPE(ctypes.py_object, ctypes.c_void_p)), + ('Pattern_Type', ctypes.py_object), + ('SolidPattern_Type', ctypes.py_object), + ('SurfacePattern_Type', ctypes.py_object), + ('Gradient_Type', ctypes.py_object), + ('LinearGradient_Type', ctypes.py_object), + ('RadialGradient_Type', ctypes.py_object), + ('Pattern_FromPattern', ctypes.PYFUNCTYPE(ctypes.py_object, ctypes.c_void_p, + ctypes.py_object)), #** changed in 1.8.4 + ('ScaledFont_Type', ctypes.py_object), + ('ScaledFont_FromScaledFont', ctypes.PYFUNCTYPE(ctypes.py_object, ctypes.c_void_p)), + ('Surface_Type', ctypes.py_object), + ('ImageSurface_Type', ctypes.py_object), + ('PDFSurface_Type', ctypes.py_object), + ('PSSurface_Type', ctypes.py_object), + ('SVGSurface_Type', ctypes.py_object), + ('Win32Surface_Type', ctypes.py_object), + ('Win32PrintingSurface_Type', ctypes.py_object), #** new + ('XCBSurface_Type', ctypes.py_object), #** new + ('XlibSurface_Type', ctypes.py_object), + ('Surface_FromSurface', ctypes.PYFUNCTYPE(ctypes.py_object, + ctypes.c_void_p, + ctypes.py_object)), + ('Check_Status', ctypes.PYFUNCTYPE(ctypes.c_int, ctypes.c_int))] + + +def _loadPycairoAPI(): + global pycairoAPI + if pycairoAPI is not None: + return + + PyCObject_AsVoidPtr = ctypes.pythonapi.PyCObject_AsVoidPtr + PyCObject_AsVoidPtr.argtypes = [ctypes.py_object] + PyCObject_AsVoidPtr.restype = ctypes.c_void_p + ptr = PyCObject_AsVoidPtr(cairo.CAPI) + pycairoAPI = ctypes.cast(ptr, ctypes.POINTER(Pycairo_CAPI)).contents + +#---------------------------------------------------------------------------- + +# Load these at import time. That seems a bit better than doing it at +# first use... +_findCairoLib() +_loadPycairoAPI() + +#---------------------------------------------------------------------------- diff --git a/wx/lib/wxpTag.py b/wx/lib/wxpTag.py new file mode 100644 index 00000000..e87c15f9 --- /dev/null +++ b/wx/lib/wxpTag.py @@ -0,0 +1,273 @@ +#---------------------------------------------------------------------- +# Name: wxPython.lib.wxpTag +# Purpose: A wxHtmlTagHandler that knows how to build and place +# wxPython widgets onto web pages. +# +# Author: Robin Dunn +# +# Created: 13-Sept-1999 +# RCS-ID: $Id$ +# Copyright: (c) 1999 by Total Control Software +# Licence: wxWindows license +#---------------------------------------------------------------------- +# 12/13/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o Updated for V2.5 compatability +# + +''' +wxPython.lib.wxpTag + +This module contains a wxHtmlTagHandler that knows how to build +and place wxPython widgets onto wxHtmlWindow web pages. + +You don\'t need to use anything in this module directly, just +importing it will create the tag handler and add it to any +wxHtmlWinParsers created from that time forth. + +Tags of the following form are recognised:: + + + + + + + +Both the begining and ending WXP tags are required. + +In the future support will be added for another tag that can be +embedded between the two begining and ending WXP tags and will +facilitate calling methods of the widget to help initialize it. +Additionally, support may be added to fetch the module from a web +server as is done with java applets. + +''' +#---------------------------------------------------------------------- + +import types + +import wx +import wx.html + + +#---------------------------------------------------------------------- + +WXPTAG = 'WXP' +PARAMTAG = 'PARAM' + +#---------------------------------------------------------------------- + +class wxpTagHandler(wx.html.HtmlWinTagHandler): + def __init__(self): + wx.html.HtmlWinTagHandler.__init__(self) + self.ctx = None + + def GetSupportedTags(self): + return WXPTAG+','+PARAMTAG + + + def HandleTag(self, tag): + name = tag.GetName() + if name == WXPTAG: + return self.HandleWxpTag(tag) + elif name == PARAMTAG: + return self.HandleParamTag(tag) + else: + raise ValueError, 'unknown tag: ' + name + + + def HandleWxpTag(self, tag): + # create a new context object + self.ctx = _Context() + + # find and import the module + modName = '' + if tag.HasParam('MODULE'): + modName = tag.GetParam('MODULE') + if modName: + self.ctx.classMod = _my_import(modName) + else: + self.ctx.classMod = wx + + # find and verify the class + if not tag.HasParam('CLASS'): + raise AttributeError, "WXP tag requires a CLASS attribute" + + className = tag.GetParam('CLASS') + self.ctx.classObj = getattr(self.ctx.classMod, className) + if type(self.ctx.classObj) not in [ types.ClassType, types.TypeType]: + raise TypeError, "WXP tag attribute CLASS must name a class" + + # now look for width and height + width = -1 + height = -1 + if tag.HasParam('WIDTH'): + width = tag.GetParam('WIDTH') + if width[-1] == '%': + self.ctx.floatWidth = int(width[:-1], 0) + width = self.ctx.floatWidth + else: + width = int(width) + if tag.HasParam('HEIGHT'): + height = int(tag.GetParam('HEIGHT')) + self.ctx.kwargs['size'] = wx.Size(width, height) + + # parse up to the closing tag, and gather any nested Param tags. + self.ParseInner(tag) + + # create the object + parent = self.GetParser().GetWindowInterface().GetHTMLWindow() + if parent: + obj = self.ctx.classObj(parent, **self.ctx.kwargs) + obj.Show(True) + + # add it to the HtmlWindow + self.GetParser().GetContainer().InsertCell( + wx.html.HtmlWidgetCell(obj, self.ctx.floatWidth)) + self.ctx = None + return True + + + def HandleParamTag(self, tag): + if not tag.HasParam('NAME'): + return False + + name = tag.GetParam('NAME') + value = "" + if tag.HasParam('VALUE'): + value = tag.GetParam('VALUE') + + # check for a param named 'id' + if name == 'id': + theID = -1 + try: + theID = int(value) + except ValueError: + theID = getattr(self.ctx.classMod, value) + value = theID + + + # check for something that should be evaluated + elif value and value[0] in '[{(' or value[:2] == 'wx': + saveVal = value + try: + value = eval(value, self.ctx.classMod.__dict__) + except: + value = saveVal + + # convert to wx.Colour + elif value and value[0] == '#': + try: + red = int('0x'+value[1:3], 16) + green = int('0x'+value[3:5], 16) + blue = int('0x'+value[5:], 16) + value = wx.Colour(red, green, blue) + except: + pass + + if self.ctx: + self.ctx.kwargs[str(name)] = value + return False + + +#---------------------------------------------------------------------- +# just a place to hold some values +class _Context: + def __init__(self): + self.kwargs = {} + self.width = -1 + self.height = -1 + self.classMod = None + self.classObj = None + self.floatWidth = 0 + + +#---------------------------------------------------------------------- +# Function to assist with importing packages +def _my_import(name): + mod = __import__(name) + components = name.split('.') + for comp in components[1:]: + mod = getattr(mod, comp) + return mod + + +#---------------------------------------------------------------------- +# Function to parse a param string (of the form 'item=value item2="value etc"' +# and creates a dictionary +def _param2dict(param): + i = 0; j = 0; s = len(param); d = {} + while 1: + while i=s: break + j = i + while j=s: + break + word = param[i:j] + i=j+1 + if (param[i] == '"'): + j=i+1 + while j