From 0b0849b5a5d530095bc0da325d0a7a2101ddb0d6 Mon Sep 17 00:00:00 2001 From: Robin Dunn Date: Wed, 16 May 2007 23:39:42 +0000 Subject: [PATCH] Forward port recent changes on the 2.8 branch to HEAD git-svn-id: https://svn.wxwidgets.org/svn/wx/wxWidgets/trunk@46083 c3d73ce0-8a6f-49c7-b76d-6d57e0e08775 --- wxPython/contrib/gizmos/gizmos.i | 13 +- .../gizmos/wxCode/src/treelistctrl.cpp | 72 +- wxPython/demo/AUI_DockingWindowMgr.py | 7 +- wxPython/demo/CollapsiblePane.py | 3 +- wxPython/demo/DragImage.py | 16 +- wxPython/demo/FloatCanvas.py | 415 ++-- wxPython/demo/Main.py | 628 ++++-- wxPython/demo/MaskedNumCtrl.py | 41 +- wxPython/demo/OGL.py | 16 +- wxPython/demo/TreeMixin.py | 6 +- wxPython/demo/bmp_source/book.png | Bin 0 -> 622 bytes wxPython/demo/bmp_source/clipboard.png | Bin 0 -> 693 bytes wxPython/demo/bmp_source/code.png | Bin 0 -> 868 bytes wxPython/demo/bmp_source/core.png | Bin 0 -> 807 bytes wxPython/demo/bmp_source/custom.png | Bin 0 -> 833 bytes .../demo/bmp_source/deleteperspective.png | Bin 0 -> 892 bytes wxPython/demo/bmp_source/demo.png | Bin 0 -> 817 bytes wxPython/demo/bmp_source/dialog.png | Bin 0 -> 237 bytes wxPython/demo/bmp_source/exit.png | Bin 0 -> 995 bytes wxPython/demo/bmp_source/expansion.png | Bin 0 -> 894 bytes wxPython/demo/bmp_source/find.png | Bin 0 -> 985 bytes wxPython/demo/bmp_source/findnext.png | Bin 0 -> 811 bytes wxPython/demo/bmp_source/frame.png | Bin 0 -> 199 bytes wxPython/demo/bmp_source/images.png | Bin 0 -> 1141 bytes wxPython/demo/bmp_source/inspect.png | Bin 0 -> 766 bytes wxPython/demo/bmp_source/layout.png | Bin 0 -> 519 bytes wxPython/demo/bmp_source/miscellaneous.png | Bin 0 -> 716 bytes wxPython/demo/bmp_source/modifiedexists.png | Bin 0 -> 440 bytes wxPython/demo/bmp_source/morecontrols.png | Bin 0 -> 740 bytes wxPython/demo/bmp_source/moredialog.png | Bin 0 -> 495 bytes wxPython/demo/bmp_source/overview.png | Bin 0 -> 655 bytes wxPython/demo/bmp_source/process.png | Bin 0 -> 1059 bytes wxPython/demo/bmp_source/pyshell.png | Bin 0 -> 664 bytes wxPython/demo/bmp_source/recent.png | Bin 0 -> 378 bytes wxPython/demo/bmp_source/saveperspective.png | Bin 0 -> 1139 bytes wxPython/demo/encode_bitmaps.py | 28 + wxPython/distrib/DIRLIST | 1 + wxPython/distrib/README.win32.txt | 2 +- wxPython/distrib/all/build-all | 21 +- wxPython/distrib/all/build-chrpm | 61 + wxPython/distrib/make_installer.py | 3 + wxPython/docs/CHANGES.txt | 4 + wxPython/misc/image.png | Bin 17905 -> 0 bytes .../wxPIA_book/Chapter-09/progress_box.py | 5 +- wxPython/setup.py | 1 + wxPython/src/_app.i | 5 + wxPython/src/_dc.i | 4 +- wxPython/src/_defs.i | 1 + wxPython/src/_event.i | 2 + wxPython/src/_evtloop.i | 44 +- wxPython/src/_functions.i | 8 +- wxPython/src/_pywindows.i | 8 +- wxPython/src/_sound.i | 10 +- wxPython/src/_timer.i | 4 +- wxPython/src/_toolbar.i | 9 + wxPython/src/_vscroll.i | 12 +- wxPython/src/_window.i | 2 +- wxPython/src/aui.i | 9 +- wxPython/src/gtk/_core.py | 2 +- wxPython/src/gtk/_misc_wrap.cpp | 8 + wxPython/src/helpers.cpp | 1 + wxPython/src/mac/_core.py | 2 +- wxPython/src/msw/_core.py | 2 +- wxPython/tests/TreeMixinTest.py | 7 +- wxPython/tests/image.png | Bin 0 -> 17904 bytes wxPython/tests/test_dclick.py | 28 + wxPython/tests/test_transparentFrame.py | 71 + .../test_widgetLayout.py} | 28 +- .../widgets.cfg} | 19 +- wxPython/wx/lib/customtreectrl.py | 60 +- wxPython/wx/lib/floatcanvas/FloatCanvas.py | 2009 +++++++++-------- wxPython/wx/lib/floatcanvas/GUIMode.py | 347 +++ wxPython/wx/lib/floatcanvas/NavCanvas.py | 133 +- wxPython/wx/lib/floatcanvas/Utilities/BBox.py | 170 ++ .../wx/lib/floatcanvas/Utilities/BBoxTest.py | 354 +++ wxPython/wx/lib/floatcanvas/Utilities/GUI.py | 115 + .../wx/lib/floatcanvas/Utilities/__init__.py | 7 + wxPython/wx/lib/floatcanvas/__init__.py | 23 +- wxPython/wx/lib/imagebrowser.py | 5 +- wxPython/wx/lib/inspection.py | 5 + wxPython/wx/lib/masked/combobox.py | 160 +- wxPython/wx/lib/masked/maskededit.py | 97 +- wxPython/wx/lib/masked/numctrl.py | 102 +- wxPython/wx/lib/masked/textctrl.py | 51 +- wxPython/wx/lib/mixins/treemixin.py | 100 +- wxPython/wx/tools/pywxrc.py | 15 +- wxPython/wxaddons/sized_controls.py | 15 +- 87 files changed, 3809 insertions(+), 1588 deletions(-) create mode 100644 wxPython/demo/bmp_source/book.png create mode 100644 wxPython/demo/bmp_source/clipboard.png create mode 100644 wxPython/demo/bmp_source/code.png create mode 100644 wxPython/demo/bmp_source/core.png create mode 100644 wxPython/demo/bmp_source/custom.png create mode 100644 wxPython/demo/bmp_source/deleteperspective.png create mode 100644 wxPython/demo/bmp_source/demo.png create mode 100644 wxPython/demo/bmp_source/dialog.png create mode 100644 wxPython/demo/bmp_source/exit.png create mode 100644 wxPython/demo/bmp_source/expansion.png create mode 100644 wxPython/demo/bmp_source/find.png create mode 100644 wxPython/demo/bmp_source/findnext.png create mode 100644 wxPython/demo/bmp_source/frame.png create mode 100644 wxPython/demo/bmp_source/images.png create mode 100644 wxPython/demo/bmp_source/inspect.png create mode 100644 wxPython/demo/bmp_source/layout.png create mode 100644 wxPython/demo/bmp_source/miscellaneous.png create mode 100644 wxPython/demo/bmp_source/modifiedexists.png create mode 100644 wxPython/demo/bmp_source/morecontrols.png create mode 100644 wxPython/demo/bmp_source/moredialog.png create mode 100644 wxPython/demo/bmp_source/overview.png create mode 100644 wxPython/demo/bmp_source/process.png create mode 100644 wxPython/demo/bmp_source/pyshell.png create mode 100644 wxPython/demo/bmp_source/recent.png create mode 100644 wxPython/demo/bmp_source/saveperspective.png create mode 100755 wxPython/distrib/all/build-chrpm delete mode 100644 wxPython/misc/image.png create mode 100644 wxPython/tests/image.png create mode 100644 wxPython/tests/test_dclick.py create mode 100644 wxPython/tests/test_transparentFrame.py rename wxPython/{misc/widgetLayoutTest.py => tests/test_widgetLayout.py} (94%) rename wxPython/{misc/widgetLayoutTest.cfg => tests/widgets.cfg} (96%) create mode 100644 wxPython/wx/lib/floatcanvas/GUIMode.py create mode 100644 wxPython/wx/lib/floatcanvas/Utilities/BBox.py create mode 100644 wxPython/wx/lib/floatcanvas/Utilities/BBoxTest.py create mode 100644 wxPython/wx/lib/floatcanvas/Utilities/GUI.py create mode 100644 wxPython/wx/lib/floatcanvas/Utilities/__init__.py diff --git a/wxPython/contrib/gizmos/gizmos.i b/wxPython/contrib/gizmos/gizmos.i index 3c39f202dd..0f520ecc52 100644 --- a/wxPython/contrib/gizmos/gizmos.i +++ b/wxPython/contrib/gizmos/gizmos.i @@ -170,6 +170,8 @@ enum { wxEL_ALLOW_NEW, wxEL_ALLOW_EDIT, wxEL_ALLOW_DELETE, + wxEL_NO_REORDER, + wxEL_DEFAULT_STYLE }; @@ -190,9 +192,16 @@ public: const wxString& label = wxPyEmptyString, const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, - long style = wxEL_ALLOW_NEW | wxEL_ALLOW_EDIT | wxEL_ALLOW_DELETE, + long style = wxEL_DEFAULT_STYLE, const wxString& name = wxPyEditableListBoxNameStr); - + %RenameCtor(PreEditableListBox, wxEditableListBox()); + + bool Create(wxWindow *parent, wxWindowID id, + const wxString& label, + const wxPoint& pos = wxDefaultPosition, + const wxSize& size = wxDefaultSize, + long style = wxEL_DEFAULT_STYLE, + const wxString& name = wxEditableListBoxNameStr); void SetStrings(const wxArrayString& strings); diff --git a/wxPython/contrib/gizmos/wxCode/src/treelistctrl.cpp b/wxPython/contrib/gizmos/wxCode/src/treelistctrl.cpp index 0ee61d3b0b..1e527f1492 100644 --- a/wxPython/contrib/gizmos/wxCode/src/treelistctrl.cpp +++ b/wxPython/contrib/gizmos/wxCode/src/treelistctrl.cpp @@ -598,7 +598,8 @@ protected: wxTreeListItem *m_shiftItem; // item, where the shift key was pressed wxTreeListItem *m_editItem; // item, which is currently edited wxTreeListItem *m_selectItem; // current selected item, not with wxTR_MULTIPLE - + wxTreeListItem *m_select_me; + int m_curColumn; int m_btnWidth, m_btnWidth2; @@ -922,6 +923,29 @@ private: // implementation // =========================================================================== + +// --------------------------------------------------------------------------- +// internal helpers +// --------------------------------------------------------------------------- + +// check if the given item is under another one +static bool IsDescendantOf(const wxTreeListItem *parent, const wxTreeListItem *item) +{ + while ( item ) + { + if ( item == parent ) + { + // item is a descendant of parent + return true; + } + + item = item->GetItemParent(); + } + + return false; +} + + // --------------------------------------------------------------------------- // wxTreeListRenameTimer (internal) // --------------------------------------------------------------------------- @@ -1789,7 +1813,8 @@ void wxTreeListMainWindow::Init() { m_shiftItem = (wxTreeListItem*)NULL; m_editItem = (wxTreeListItem*)NULL; m_selectItem = (wxTreeListItem*)NULL; - + m_select_me = (wxTreeListItem*)NULL; + m_curColumn = -1; // no current column m_hasFocus = false; @@ -2418,6 +2443,27 @@ void wxTreeListMainWindow::Delete (const wxTreeItemId& itemId) { } wxTreeListItem *parent = item->GetItemParent(); + + + // m_select_me records whether we need to select + // a different item, in idle time. + if ( m_select_me && IsDescendantOf(item, m_select_me) ) + { + m_select_me = parent; + } + + if ( IsDescendantOf(item, m_curItem) ) + { + // Don't silently change the selection: + // do it properly in idle time, so event + // handlers get called. + + // m_current = parent; + m_curItem = NULL; + m_select_me = parent; + } + + // remove the item from the tree if (parent) { parent->GetChildren().Remove (item); // remove by value } @@ -2426,6 +2472,10 @@ void wxTreeListMainWindow::Delete (const wxTreeItemId& itemId) { SendDeleteEvent (item); if (m_selectItem == item) m_selectItem = (wxTreeListItem*)NULL; item->DeleteChildren (this); + + if (item == m_select_me) + m_select_me = NULL; + delete item; } @@ -2660,7 +2710,6 @@ void wxTreeListMainWindow::SelectItem (const wxTreeItemId& itemId, if (unselect_others) { m_selectItem = (item->IsSelected())? item: (wxTreeListItem*)NULL; } - } // send event to user code @@ -3627,7 +3676,6 @@ void wxTreeListMainWindow::OnChar (wxKeyEvent &event) { m_curItem = (wxTreeListItem*)newItem.m_pItem; // make the new item the current item RefreshLine (oldItem); } - } wxTreeItemId wxTreeListMainWindow::HitTest (const wxPoint& point, int& flags, int& column) { @@ -3969,9 +4017,23 @@ void wxTreeListMainWindow::OnIdle (wxIdleEvent &WXUNUSED(event)) { * we actually redraw the tree when everything is over */ if (!m_dirty) return; - m_dirty = false; + // Check if we need to select the root item + // because nothing else has been selected. + // Delaying it means that we can invoke event handlers + // as required, when a first item is selected. + if (!m_owner->HasFlag(wxTR_MULTIPLE) && !m_owner->GetSelection().IsOk()) + { + if (m_select_me) + m_owner->SelectItem(m_select_me); + else if (m_owner->GetRootItem().IsOk()) + m_owner->SelectItem(m_owner->GetRootItem()); + m_select_me = NULL; + m_curItem = (wxTreeListItem*)m_owner->GetSelection().m_pItem; + + } + CalculatePositions(); Refresh(); AdjustMyScrollbars(); diff --git a/wxPython/demo/AUI_DockingWindowMgr.py b/wxPython/demo/AUI_DockingWindowMgr.py index 794b9b2ec6..eaa912928c 100644 --- a/wxPython/demo/AUI_DockingWindowMgr.py +++ b/wxPython/demo/AUI_DockingWindowMgr.py @@ -395,7 +395,7 @@ class PyAUIFrame(wx.Frame): self.Bind(wx.EVT_MENU, self.OnChangeContentPane, id=ID_TextContent) self.Bind(wx.EVT_MENU, self.OnChangeContentPane, id=ID_SizeReportContent) self.Bind(wx.EVT_MENU, self.OnChangeContentPane, id=ID_HTMLContent) - self.Bind(wx.EVT_MENU, self.OnClose, id=wx.ID_EXIT) + self.Bind(wx.EVT_MENU, self.OnExit, id=wx.ID_EXIT) self.Bind(wx.EVT_MENU, self.OnAbout, id=ID_About) self.Bind(wx.EVT_UPDATE_UI, self.OnUpdateUI, id=ID_TransparentHint) @@ -431,12 +431,13 @@ class PyAUIFrame(wx.Frame): def OnClose(self, event): - self._mgr.UnInit() + del self._mgr self.Destroy() - event.Skip() + def OnExit(self, event): + self.Close() def OnAbout(self, event): diff --git a/wxPython/demo/CollapsiblePane.py b/wxPython/demo/CollapsiblePane.py index 15ab9ad54a..2c35d712ad 100644 --- a/wxPython/demo/CollapsiblePane.py +++ b/wxPython/demo/CollapsiblePane.py @@ -19,7 +19,8 @@ class TestPanel(wx.Panel): title.SetFont(wx.Font(18, wx.SWISS, wx.NORMAL, wx.BOLD)) title.SetForegroundColour("blue") - self.cp = cp = wx.CollapsiblePane(self, label=label1) + self.cp = cp = wx.CollapsiblePane(self, label=label1, + style=wx.CP_DEFAULT_STYLE|wx.CP_NO_TLW_RESIZE) self.Bind(wx.EVT_COLLAPSIBLEPANE_CHANGED, self.OnPaneChanged, cp) self.MakePaneContent(cp.GetPane()) diff --git a/wxPython/demo/DragImage.py b/wxPython/demo/DragImage.py index d8f7ef3458..d0173caf03 100644 --- a/wxPython/demo/DragImage.py +++ b/wxPython/demo/DragImage.py @@ -136,19 +136,11 @@ class DragCanvas(wx.ScrolledWindow): return shape return None - # Remove a shape from the display - def EraseShape(self, shape, dc): - r = shape.GetRect() - dc.SetClippingRect(r) - self.TileBackground(dc) - self.DrawShapes(dc) - dc.DestroyClippingRegion() # Clears the background, then redraws it. If the DC is passed, then # we only do so in the area so designated. Otherwise, it's the whole thing. def OnEraseBackground(self, evt): dc = evt.GetDC() - if not dc: dc = wx.ClientDC(self) rect = self.GetUpdateRegion().GetBox() @@ -231,11 +223,11 @@ class DragCanvas(wx.ScrolledWindow): if dx <= tolerance and dy <= tolerance: return - # erase the shape since it will be drawn independently now - dc = wx.ClientDC(self) + # refresh the area of the window where the shape was so it + # will get erased. self.dragShape.shown = False - self.EraseShape(self.dragShape, dc) - + self.RefreshRect(self.dragShape.GetRect(), True) + self.Update() if self.dragShape.text: self.dragImage = wx.DragString(self.dragShape.text, diff --git a/wxPython/demo/FloatCanvas.py b/wxPython/demo/FloatCanvas.py index 038d496c66..0b1c54cfd6 100644 --- a/wxPython/demo/FloatCanvas.py +++ b/wxPython/demo/FloatCanvas.py @@ -1,27 +1,18 @@ +#!/usr/bin/env python - -#print "running:", wx.__version__ -##First, make sure Numeric or numarray can be imported. try: - import Numeric - import RandomArray - haveNumeric = True + import numpy as N + import numpy.random as RandomArray + haveNumpy = True + #print "Using numpy, version:", N.__version__ except ImportError: - # Numeric isn't there, let's try numarray - try: - import numarray as Numeric - import numarray.random_array as RandomArray - haveNumeric = True - except ImportError: - # numarray isn't there either - haveNumeric = False - errorText = ( - "The FloatCanvas requires either the Numeric or numarray module\n\n" - "You can get them at:\n" - "http://sourceforge.net/projects/numpy\n\n" - "NOTE: The Numeric module is substantially faster than numarray for this\n" - "purpose, if you have lots of objects\n" - ) + # numpy isn't there + haveNumpy = False + errorText = ( + "The FloatCanvas requires the numpy module, version 1.* \n\n" + "You can get info about it at:\n" + "http://numpy.scipy.org/\n\n" + ) #--------------------------------------------------------------------------- @@ -100,7 +91,10 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import item = draw_menu.Append(-1, "&Arrows","Run a test of Arrows") self.Bind(wx.EVT_MENU, self.ArrowTest, item) - item = draw_menu.Append(-1, "&Hide","Run a test of the Show() Hide() Show() and methods") + item = draw_menu.Append(-1, "&ArrowLine Test","Run a test of drawing Arrow Lines") + self.Bind(wx.EVT_MENU, self.ArrowLineTest, item) + + item = draw_menu.Append(-1, "&Hide","Run a test of hiding and showing objects") self.Bind(wx.EVT_MENU, self.HideTest, item) MenuBar.Append(draw_menu, "&Tests") @@ -122,9 +116,11 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import # Add the Canvas - self.Canvas = NavCanvas.NavCanvas(self, - Debug = 0, - BackgroundColor = "DARK SLATE BLUE") + NC = NavCanvas.NavCanvas(self, + Debug = 0, + BackgroundColor = "DARK SLATE BLUE") + + self.Canvas = NC.Canvas # reference the contained FloatCanvas self.MsgWindow = wx.TextCtrl(self, wx.ID_ANY, "Look Here for output from events\n", @@ -135,13 +131,14 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import ##Create a sizer to manage the Canvas and message window MainSizer = wx.BoxSizer(wx.VERTICAL) - MainSizer.Add(self.Canvas, 4, wx.EXPAND) + MainSizer.Add(NC, 4, wx.EXPAND) MainSizer.Add(self.MsgWindow, 1, wx.EXPAND | wx.ALL, 5) self.SetSizer(MainSizer) - wx.EVT_CLOSE(self, self.OnCloseWindow) + self.Bind(wx.EVT_CLOSE, self.OnCloseWindow) - FloatCanvas.EVT_MOTION(self.Canvas, self.OnMove ) + self.Canvas.Bind(FloatCanvas.EVT_MOTION, self.OnMove) + self.Canvas.Bind(FloatCanvas.EVT_MOUSEWHEEL, self.OnWheel) self.EventsAreBound = False @@ -161,56 +158,39 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import def BindAllMouseEvents(self): if not self.EventsAreBound: ## Here is how you catch FloatCanvas mouse events - FloatCanvas.EVT_LEFT_DOWN(self.Canvas, self.OnLeftDown ) - FloatCanvas.EVT_LEFT_UP(self.Canvas, self.OnLeftUp ) - FloatCanvas.EVT_LEFT_DCLICK(self.Canvas, self.OnLeftDouble ) + self.Canvas.Bind(FloatCanvas.EVT_LEFT_DOWN, self.OnLeftDown) + self.Canvas.Bind(FloatCanvas.EVT_LEFT_UP, self.OnLeftUp) + self.Canvas.Bind(FloatCanvas.EVT_LEFT_DCLICK, self.OnLeftDouble) - FloatCanvas.EVT_MIDDLE_DOWN(self.Canvas, self.OnMiddleDown ) - FloatCanvas.EVT_MIDDLE_UP(self.Canvas, self.OnMiddleUp ) - FloatCanvas.EVT_MIDDLE_DCLICK(self.Canvas, self.OnMiddleDouble ) + self.Canvas.Bind(FloatCanvas.EVT_MIDDLE_DOWN, self.OnMiddleDown) + self.Canvas.Bind(FloatCanvas.EVT_MIDDLE_UP, self.OnMiddleUp) + self.Canvas.Bind(FloatCanvas.EVT_MIDDLE_DCLICK, self.OnMiddleDouble) - FloatCanvas.EVT_RIGHT_DOWN(self.Canvas, self.OnRightDown ) - FloatCanvas.EVT_RIGHT_UP(self.Canvas, self.OnRightUp ) - FloatCanvas.EVT_RIGHT_DCLICK(self.Canvas, self.OnRightDouble ) + self.Canvas.Bind(FloatCanvas.EVT_RIGHT_DOWN, self.OnRightDown) + self.Canvas.Bind(FloatCanvas.EVT_RIGHT_UP, self.OnRightUp) + self.Canvas.Bind(FloatCanvas.EVT_RIGHT_DCLICK, self.OnRightDouble) - FloatCanvas.EVT_MOUSEWHEEL(self.Canvas, self.OnWheel ) self.EventsAreBound = True + def UnBindAllMouseEvents(self): ## Here is how you unbind FloatCanvas mouse events - FloatCanvas.EVT_LEFT_DOWN(self.Canvas, None ) - FloatCanvas.EVT_LEFT_UP(self.Canvas, None ) - FloatCanvas.EVT_LEFT_DCLICK(self.Canvas, None) - - FloatCanvas.EVT_MIDDLE_DOWN(self.Canvas, None ) - FloatCanvas.EVT_MIDDLE_UP(self.Canvas, None ) - FloatCanvas.EVT_MIDDLE_DCLICK(self.Canvas, None ) - - FloatCanvas.EVT_RIGHT_DOWN(self.Canvas, None ) - FloatCanvas.EVT_RIGHT_UP(self.Canvas, None ) - FloatCanvas.EVT_RIGHT_DCLICK(self.Canvas, None ) - - FloatCanvas.EVT_MOUSEWHEEL(self.Canvas, None ) - FloatCanvas.EVT_LEFT_DOWN(self.Canvas, None ) - FloatCanvas.EVT_LEFT_UP(self.Canvas, None ) - FloatCanvas.EVT_LEFT_DCLICK(self.Canvas, None) - - FloatCanvas.EVT_MIDDLE_DOWN(self.Canvas, None ) - FloatCanvas.EVT_MIDDLE_UP(self.Canvas, None ) - FloatCanvas.EVT_MIDDLE_DCLICK(self.Canvas, None ) + self.Canvas.Unbind(FloatCanvas.EVT_LEFT_DOWN) + self.Canvas.Unbind(FloatCanvas.EVT_LEFT_UP) + self.Canvas.Unbind(FloatCanvas.EVT_LEFT_DCLICK) - FloatCanvas.EVT_RIGHT_DOWN(self.Canvas, None ) - FloatCanvas.EVT_RIGHT_UP(self.Canvas, None ) - FloatCanvas.EVT_RIGHT_DCLICK(self.Canvas, None ) + self.Canvas.Unbind(FloatCanvas.EVT_MIDDLE_DOWN) + self.Canvas.Unbind(FloatCanvas.EVT_MIDDLE_UP) + self.Canvas.Unbind(FloatCanvas.EVT_MIDDLE_DCLICK) - FloatCanvas.EVT_MOUSEWHEEL(self.Canvas, None ) + self.Canvas.Unbind(FloatCanvas.EVT_RIGHT_DOWN) + self.Canvas.Unbind(FloatCanvas.EVT_RIGHT_UP) + self.Canvas.Unbind(FloatCanvas.EVT_RIGHT_DCLICK) self.EventsAreBound = False def PrintCoords(self,event): - #print "coords are: %s"%(event.Coords,) - #print "pixel coords are: %s\n"%(event.GetPosition(),) self.Log("coords are: %s"%(event.Coords,)) self.Log("pixel coords are: %s\n"%(event.GetPosition(),)) @@ -256,7 +236,7 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import self.PrintCoords(event) def OnRightUp(self, event): - self.Log("RightDown") + self.Log("RightUp") self.PrintCoords(event) def OnRightDouble(self, event): @@ -278,6 +258,7 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import Updates the status bar with the world coordinates """ self.SetStatusText("%.2f, %.2f"%tuple(event.Coords)) + event.Skip() def OnAbout(self, event): dlg = wx.MessageDialog(self, @@ -293,8 +274,7 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import def Clear(self,event = None): self.UnBindAllMouseEvents() - self.Canvas.ClearAll() - self.Canvas.SetProjectionFun(None) + self.Canvas.InitAll() self.Canvas.Draw() def OnQuit(self,event): @@ -304,7 +284,12 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import self.Destroy() def DrawTest(self,event=None): - wx.GetApp().Yield() + """ + This demo draws a few of everything + + """ + + wx.GetApp().Yield(True) Range = (-10,10) colors = self.colors @@ -312,8 +297,12 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import self.BindAllMouseEvents() Canvas = self.Canvas - Canvas.ClearAll() - Canvas.SetProjectionFun(None) + Canvas.InitAll() + # + ## these set the limits for how much you can zoom in and out + Canvas.MinScale = 14 + Canvas.MaxScale = 500 + ############# Random tests of everything ############## @@ -417,6 +406,18 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import LineColor = colors[random.randint(0,len(colors)-1)], ArrowHeadAngle = random.uniform(20,90)) + # ArrowLines + for i in range(5): + points = [] + for j in range(random.randint(2,10)): + point = (random.randint(Range[0],Range[1]),random.randint(Range[0],Range[1])) + points.append(point) + lw = random.randint(1,10) + cf = random.randint(0,len(colors)-1) + cl = random.randint(0,len(colors)-1) + Canvas.AddArrowLine(points, LineWidth = lw, LineColor = colors[cl], ArrowHeadSize= 16) + + Canvas.ZoomToBB() def TestAnimation(self,event=None): @@ -429,15 +430,13 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import having to re-draw the whole background. """ - wx.GetApp().Yield() + wx.GetApp().Yield(True) Range = (-10,10) self.Range = Range self.UnBindAllMouseEvents() Canvas = self.Canvas - - Canvas.ClearAll() - Canvas.SetProjectionFun(None) + Canvas.InitAll() ## Random tests of everything: colors = self.colors @@ -543,14 +542,13 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import self.Timer.Start(self.FrameDelay) #print "Did %i frames in %f seconds"%(N, (time.time() - start) ) - def TestHitTest(self,event=None): - wx.GetApp().Yield() + def TestHitTest(self, event=None): + wx.GetApp().Yield(True) self.UnBindAllMouseEvents() Canvas = self.Canvas - Canvas.ClearAll() - Canvas.SetProjectionFun(None) + Canvas.InitAll() #Add a Hit-able rectangle w, h = 60, 20 @@ -558,7 +556,7 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import dx = 80 dy = 40 x, y = 20, 20 - FontSize = 8 + FontSize = 10 #Add one that is not HitAble Canvas.AddRectangle((x,y), (w, h), LineWidth = 2) @@ -709,7 +707,7 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import R.Name = color R.Bind(FloatCanvas.EVT_FC_ENTER_OBJECT, self.RectMouseOver) R.Bind(FloatCanvas.EVT_FC_LEAVE_OBJECT, self.RectMouseLeave) - Canvas.AddText("Mouse ENter&Leave", (x, y), Size = FontSize, Position = "bl") + Canvas.AddText("Mouse Enter&Leave", (x, y), Size = FontSize, Position = "bl") Canvas.AddText(R.Name, (x, y+h), Size = FontSize, Position = "tl") x = 20 @@ -722,7 +720,7 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import x += dx color = "SEA GREEN" - Points = Numeric.array(( (x, y), (x, y+2.*h/3), (x+w, y+h), (x+w, y+h/2.), (x + 2.*w/3, y+h/2.), (x + 2.*w/3,y) ), Numeric.Float) + Points = N.array(( (x, y), (x, y+2.*h/3), (x+w, y+h), (x+w, y+h/2.), (x + 2.*w/3, y+h/2.), (x + 2.*w/3,y) ), N.float_) R = Canvas.AddPolygon(Points, LineWidth = 2, FillColor = color) R.Name = color + " Polygon" R.Bind(FloatCanvas.EVT_FC_RIGHT_DOWN, self.RectGotHitRight) @@ -731,7 +729,7 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import x += dx color = "Red" - Points = Numeric.array(( (x, y), (x, y+2.*h/3), (x+w, y+h), (x+w, y+h/2.), (x + 2.*w/3, y+h/2.), (x + 2.*w/3,y) ), Numeric.Float) + Points = N.array(( (x, y), (x, y+2.*h/3), (x+w, y+h), (x+w, y+h/2.), (x + 2.*w/3, y+h/2.), (x + 2.*w/3,y) ), N.float_) R = Canvas.AddPointSet(Points, Diameter = 4, Color = color) R.Name = "PointSet" R.Bind(FloatCanvas.EVT_FC_LEFT_DOWN, self.PointSetGotHit) @@ -755,7 +753,7 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import x += dx color = "Cyan" Point = (x + w/2, y) - #Points = Numeric.array(( (x, y), (x, y+2.*h/3), (x+w, y+h), (x+w, y+h/2.), (x + 2.*w/3, y+h/2.), (x + 2.*w/3,y) ), Numeric.Float) + #Points = N.array(( (x, y), (x, y+2.*h/3), (x+w, y+h), (x+w, y+h/2.), (x + 2.*w/3, y+h/2.), (x + 2.*w/3,y) ), N.float_) R = Canvas.AddSquarePoint(Point, Size = 8, Color = color) R.Name = "SquarePoint" R.Bind(FloatCanvas.EVT_FC_LEFT_DOWN, self.RectGotHit) @@ -766,13 +764,12 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import self.Canvas.ZoomToBB() def TestHitTestForeground(self,event=None): - wx.GetApp().Yield() + wx.GetApp().Yield(True) self.UnBindAllMouseEvents() Canvas = self.Canvas - Canvas.ClearAll() - Canvas.SetProjectionFun(None) + Canvas.InitAll() #Add a Hitable rectangle w, h = 60, 20 @@ -871,13 +868,14 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import def TestText(self, event= None): - wx.GetApp().Yield() + wx.GetApp().Yield(True) self.BindAllMouseEvents() Canvas = self.Canvas - Canvas.ClearAll() - Canvas.SetProjectionFun(None) + Canvas.InitAll() + + DefaultSize = 12 Point = (3, 0) ## Add a non-visible rectangle, just to get a Bounding Box @@ -889,29 +887,29 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import # Text String = "Some text" - self.Canvas.AddText("Top Left",Point,Size = 14,Color = "Yellow",BackgroundColor = "Blue", Position = "tl") - self.Canvas.AddText("Bottom Left",Point,Size = 14,Color = "Cyan",BackgroundColor = "Black",Position = "bl") - self.Canvas.AddText("Top Right",Point,Size = 14,Color = "Black",BackgroundColor = "Cyan",Position = "tr") - self.Canvas.AddText("Bottom Right",Point,Size = 14,Color = "Blue",BackgroundColor = "Yellow",Position = "br") + self.Canvas.AddText("Top Left",Point,Size = DefaultSize,Color = "Yellow",BackgroundColor = "Blue", Position = "tl") + self.Canvas.AddText("Bottom Left",Point,Size = DefaultSize,Color = "Cyan",BackgroundColor = "Black",Position = "bl") + self.Canvas.AddText("Top Right",Point,Size = DefaultSize,Color = "Black",BackgroundColor = "Cyan",Position = "tr") + self.Canvas.AddText("Bottom Right",Point,Size = DefaultSize,Color = "Blue",BackgroundColor = "Yellow",Position = "br") Canvas.AddPointSet((Point), Color = "White", Diameter = 2) Point = (3, 2) Canvas.AddPointSet((Point), Color = "White", Diameter = 2) - self.Canvas.AddText("Top Center",Point,Size = 14,Color = "Black",Position = "tc") - self.Canvas.AddText("Bottom Center",Point,Size = 14,Color = "White",Position = "bc") + self.Canvas.AddText("Top Center",Point,Size = DefaultSize,Color = "Black",Position = "tc") + self.Canvas.AddText("Bottom Center",Point,Size = DefaultSize,Color = "White",Position = "bc") Point = (3, 4) Canvas.AddPointSet((Point), Color = "White", Diameter = 2) - self.Canvas.AddText("Center Right",Point,Size = 14,Color = "Black",Position = "cr") - self.Canvas.AddText("Center Left",Point,Size = 14,Color = "Black",Position = "cl") + self.Canvas.AddText("Center Right",Point,Size = DefaultSize,Color = "Black",Position = "cr") + self.Canvas.AddText("Center Left",Point,Size = DefaultSize,Color = "Black",Position = "cl") Point = (3, -2) Canvas.AddPointSet((Point), Color = "White", Diameter = 2) self.Canvas.AddText("Center Center", - Point, Size = 14, + Point, Size = DefaultSize, Color = "Black", Position = "cc") @@ -927,18 +925,17 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import self.Canvas.AddText("ROMAN ITALIC BOLD Font", (-10, -5), Family = wx.ROMAN, Weight=wx.BOLD, Style=wx.ITALIC) # NOTE: this font exists on my Linux box..who knows were else you'll find it! - Font = wx.Font(20, wx.DEFAULT, wx.ITALIC, wx.NORMAL, False, "zapf chancery") - self.Canvas.AddText("zapf chancery Font", (-10, -6), Font = Font) + Font = wx.Font(20, wx.DEFAULT, wx.ITALIC, wx.NORMAL, False, "helvetica") + self.Canvas.AddText("Helvetica Italic", (-10, -6), Font = Font) self.Canvas.ZoomToBB() def TestScaledText(self, event= None): - wx.GetApp().Yield() + wx.GetApp().Yield(True) self.BindAllMouseEvents() Canvas = self.Canvas - Canvas.ClearAll() - Canvas.SetProjectionFun(None) + Canvas.InitAll() Point = (0, 0) @@ -977,24 +974,23 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import Canvas.AddPointSet((x,0), Color = "White", Diameter = 4) - # NOTE: this font exists on my Linux box..who knows were else you'll find it! + # NOTE: this font exists on my OS-X.who knows were else you'll find it! Point = (-100, 50) - Font = wx.Font(12, wx.DEFAULT, wx.ITALIC, wx.NORMAL, False, "zapf chancery") - T = self.Canvas.AddScaledText("zapf chancery Font", Point, Size = 20, Font = Font, Position = 'bc') + Font = wx.Font(12, wx.DEFAULT, wx.ITALIC, wx.NORMAL, False, "helvetica") + T = self.Canvas.AddScaledText("Helvetica Italic", Point, Size = 20, Font = Font, Position = 'bc') Point = (-50, -50) - Font = wx.Font(12, wx.DEFAULT, wx.ITALIC, wx.NORMAL, False, "bookman") - T = self.Canvas.AddScaledText("Bookman Font", Point, Size = 8, Font = Font) + Font = wx.Font(12, wx.DEFAULT, wx.ITALIC, wx.NORMAL, False, "times") + T = self.Canvas.AddScaledText("Times Font", Point, Size = 8, Font = Font) self.Canvas.ZoomToBB() def TestScaledTextBox(self, event= None): - wx.GetApp().Yield() + wx.GetApp().Yield(True) self.UnBindAllMouseEvents() Canvas = self.Canvas - Canvas.ClearAll() - Canvas.SetProjectionFun(None) + Canvas.InitAll() Point = (45,40) Box = Canvas.AddScaledTextBox("A Two Line\nString", @@ -1010,7 +1006,7 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import Family = wx.ROMAN, Style = wx.NORMAL, Weight = wx.NORMAL, - Underline = False, + Underlined = False, Position = 'br', Alignment = "left", InForeground = False) @@ -1058,7 +1054,7 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import Family = wx.TELETYPE, Style = wx.NORMAL, Weight = wx.NORMAL, - Underline = False, + Underlined = False, Position = 'cr', Alignment = "left", InForeground = False) @@ -1076,7 +1072,7 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import Family = wx.TELETYPE, Style = wx.NORMAL, Weight = wx.NORMAL, - Underline = False, + Underlined = False, Position = 'cl', Alignment = "left", InForeground = False) @@ -1097,7 +1093,7 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import Family = wx.TELETYPE, Style = wx.NORMAL, Weight = wx.NORMAL, - Underline = False, + Underlined = False, Position = 'tc', Alignment = "left", InForeground = False) @@ -1115,7 +1111,7 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import Family = wx.TELETYPE, Style = wx.NORMAL, Weight = wx.NORMAL, - Underline = False, + Underlined = False, Position = 'bc', Alignment = "left", InForeground = False) @@ -1143,7 +1139,7 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import Family = wx.ROMAN, Alignment = "right" ) - Point = Numeric.array((100, -20), Numeric.Float) + Point = N.array((100, -20), N.float_) Box = Canvas.AddScaledTextBox("Here is even more auto wrapped text. This time the line spacing is set to 0.8. \n\nThe Padding is set to 0.", Point, Size = 3, @@ -1157,8 +1153,8 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import ) Canvas.AddPoint(Point, "Red", 2) - Point = Numeric.array((0, -40), Numeric.Float) - # Point = Numeric.array((0, 0), Numeric.Float) + Point = N.array((0, -40), N.float_) + # Point = N.array((0, 0), N.float_) for Position in ["tl", "bl", "tr", "br"]: # for Position in ["br"]: Box = Canvas.AddScaledTextBox("Here is a\nfour liner\nanother line\nPosition=%s"%Position, @@ -1176,7 +1172,7 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import ) Canvas.AddPoint(Point, "Red", 4) - Point = Numeric.array((-20, 60), Numeric.Float) + Point = N.array((-20, 60), N.float_) Box = Canvas.AddScaledTextBox("Here is some\ncentered\ntext", Point, Size = 4, @@ -1192,7 +1188,7 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import LineSpacing = 0.8 ) - Point = Numeric.array((-20, 20), Numeric.Float) + Point = N.array((-20, 20), N.float_) Box = Canvas.AddScaledTextBox("Here is some\nright aligned\ntext", Point, Size = 4, @@ -1207,7 +1203,7 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import LineSpacing = 0.8 ) - Point = Numeric.array((100, -60), Numeric.Float) + Point = N.array((100, -60), N.float_) Box = Canvas.AddScaledTextBox("Here is some auto wrapped text. This time it is centered, rather than right aligned.\n\nThe Padding is set to 2.", Point, Size = 3, @@ -1230,13 +1226,12 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import self.Log("I'm the TextBox") def TestBitmap(self, event= None): - wx.GetApp().Yield() + wx.GetApp().Yield(True) self.UnBindAllMouseEvents() Canvas = self.Canvas - Canvas.ClearAll() - Canvas.SetProjectionFun(None) - + Canvas.InitAll() + Canvas.AddRectangle((10, 20), (400, 100), LineWidth = 3, @@ -1295,16 +1290,17 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import self.Canvas.ZoomToBB() def DrawMap(self,event = None): - wx.GetApp().Yield() + wx.GetApp().Yield(True) import os, time + + self.Canvas.InitAll() + self.Canvas.SetProjectionFun("FlatEarth") self.BindAllMouseEvents() ## Test of Actual Map Data - self.Canvas.ClearAll() - self.Canvas.SetProjectionFun("FlatEarth") #start = time.clock() self.Log("Loading Map from a File") - wx.GetApp().Yield() # so log text will get displayed now. + wx.GetApp().Yield(True) # so log text will get displayed now. Shorelines = self.Read_MapGen(os.path.join("data",'world.dat'),stats = 0) #print "It took %f seconds to load %i shorelines"%(time.clock() - start,len(Shorelines) ) #start = time.clock() @@ -1316,17 +1312,17 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import #print "It took %f seconds to draw %i shorelines"%(time.clock() - start,len(Shorelines) ) + def LineTest(self,event = None): - wx.GetApp().Yield() + wx.GetApp().Yield(True) import os, time # import random colors = self.colors Range = (-10,10) ## Test of drawing lots of lines Canvas = self.Canvas - Canvas.ClearAll() - Canvas.SetProjectionFun(None) - #start = time.clock() + Canvas.InitAll() + #start = time.clock() linepoints = [] linecolors = [] linewidths = [] @@ -1345,17 +1341,44 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import Canvas.ZoomToBB() #print "It took %f seconds to draw %i lines"%(time.clock() - start,len(linepoints) ) + def ArrowLineTest(self,event = None): + wx.GetApp().Yield(True) + Canvas = self.Canvas + Canvas.InitAll() + # import os, time +## import random + Range = (-100,100) + colors = self.colors + + # Lines + for i in range(5): + points = [] + for j in range(random.randint(2,10)): + point = (random.randint(Range[0],Range[1]),random.randint(Range[0],Range[1])) + points.append(point) + lw = random.randint(1,4) + cf = random.randint(0,len(colors)-1) + cl = random.randint(0,len(colors)-1) + al = random.randint(8,20) + aa = random.randint(20,90) + Canvas.AddArrowLine(points, + LineWidth = lw, + LineColor = colors[cl], + ArrowHeadSize = al, + ArrowHeadAngle = aa) + + Canvas.ZoomToBB() + def SpeedTest(self,event=None): - wx.GetApp().Yield() + wx.GetApp().Yield(True) BigRange = (-1000,1000) colors = self.colors self.UnBindAllMouseEvents() Canvas = self.Canvas - Canvas.ClearAll() - Canvas.SetProjectionFun(None) - + Canvas.InitAll() + # Pointset coords = [] for i in range(1000): @@ -1369,7 +1392,7 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import Canvas.ZoomToBB() def PropertiesChangeTest(self,event=None): - wx.GetApp().Yield() + wx.GetApp().Yield(True) Range = (-10,10) colors = self.colors @@ -1377,9 +1400,8 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import self.UnBindAllMouseEvents() Canvas = self.Canvas - Canvas.ClearAll() - Canvas.SetProjectionFun(None) - + Canvas.InitAll() + self.ColorObjectsAll = [] self.ColorObjectsLine = [] self.ColorObjectsColor = [] @@ -1512,13 +1534,14 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import self.Canvas.Draw(Force = True) def ArrowTest(self,event=None): - wx.GetApp().Yield() + wx.GetApp().Yield(True) self.UnBindAllMouseEvents() Canvas = self.Canvas - Canvas.ClearAll() - Canvas.SetProjectionFun(None) - + Canvas.InitAll() + Canvas.MinScale = 15 + Canvas.MaxScale = 30 + # put in a rectangle to get a bounding box Canvas.AddRectangle((0,0), (20,20), LineColor = None) @@ -1534,16 +1557,18 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import Canvas.AddText("Clickable Arrow", (4,18), Position = "bc") Arrow = Canvas.AddArrow((4,18), 80, Direction = 90 ,LineWidth = 3, LineColor = "Red", ArrowHeadAngle = 30) + Arrow.HitLineWidth = 6 Arrow.Bind(FloatCanvas.EVT_FC_LEFT_DOWN, self.ArrowClicked) - Canvas.AddText("Changable Arrow", (16,4), Position = "cc") + Canvas.AddText("Changable Arrow: try clicking it", (16,4), Position = "tc") self.RotArrow = Canvas.AddArrow((16,4), 80, Direction = 0 ,LineWidth = 3, LineColor = "Green", ArrowHeadAngle = 30) + self.RotArrow.HitLineWidth = 6 self.RotArrow.Bind(FloatCanvas.EVT_FC_LEFT_DOWN, self.RotateArrow) Canvas.ZoomToBB() def ArrowClicked(self,event): - print "The Arrow was Clicked" + self.Log("The Arrow was Clicked") def RotateArrow(self,event): ##print "The Changeable Arrow was Clicked" @@ -1557,13 +1582,12 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import self.Canvas.Draw(Force = True) def HideTest(self, event=None): - wx.GetApp().Yield() + wx.GetApp().Yield(True) self.UnBindAllMouseEvents() Canvas = self.Canvas - Canvas.ClearAll() - Canvas.SetProjectionFun(None) - + Canvas.InitAll() + Range = (-10,10) # Create a couple random Polygons @@ -1616,22 +1640,32 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import def HidePoly(self, Button): Poly = Button.HidePoly + if Poly.Visible: - Poly.Hide() + Poly.Visible = False Button.SetText(Button.String.replace("Hide","Show")) else: - Poly.Show() + Poly.Visible = True Button.SetText(Button.String.replace("Show", "Hide")) self.Canvas.Draw(True) + def TempTest(self, event= None): - wx.GetApp().Yield() + """ + + This is the start of a poly editor test, but it's not complete + so you can only run it through a command line flag: + + python FloatCanvasDemo.py --temp + + """ + + wx.GetApp().Yield(True) self.UnBindAllMouseEvents() Canvas = self.Canvas - Canvas.ClearAll() - Canvas.SetProjectionFun(None) - + Canvas.InitAll() + Range = (-10,10) # Create a random Polygon @@ -1654,7 +1688,6 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import Canvas.ZoomToBB() def SelectPoly(self, Object): - print "In SelectPoly" Canvas = self.Canvas if Object is self.SelectedPoly: pass @@ -1673,7 +1706,7 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import Canvas.Draw() def SelectPointHit(self, Point): - print "Point Num: %i Hit"%Point.VerticeNum + self.Log("Point Num: %i Hit"%Point.VerticeNum) self.SelectedPoint = Point def Read_MapGen(self, filename, stats = 0,AllLines=0): @@ -1697,11 +1730,11 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import for line in data: if line: if line == "# -b": #New segment beginning - if segment: Shorelines.append(Numeric.array(segment)) + if segment: Shorelines.append(N.array(segment)) segment = [] else: segment.append(map(float,string.split(line))) - if segment: Shorelines.append(Numeric.array(segment)) + if segment: Shorelines.append(N.array(segment)) if stats: NumSegments = len(Shorelines) @@ -1728,13 +1761,16 @@ def BuildDrawFrame(): # this gets called when needed, rather than on import if __name__ == "__main__": + # running stand alone, Use wxversion: +# import wxversion +# wxversion.select("2.6") +# wxversion.select("2.8") import wx # check options: import sys, getopt - optlist, args = getopt.getopt(sys.argv[1:],'l',["local", - "all", + optlist, args = getopt.getopt(sys.argv[1:],'l',["all", "text", "map", "stext", @@ -1747,40 +1783,15 @@ if __name__ == "__main__": "temp", "props", "arrow", + "arrowline", "hide"]) - if not haveNumeric: + if not haveNumpy: raise ImportError(errorText) StartUpDemo = "all" # the default - for opt in optlist: - if opt[0] == "--all": - StartUpDemo = "all" - elif opt[0] == "--text": - StartUpDemo = "text" - elif opt[0] == "--map": - StartUpDemo = "map" - elif opt[0] == "--stext": - StartUpDemo = "stext" - elif opt[0] == "--stextbox": - StartUpDemo = "stextbox" - elif opt[0] == "--bitmap": - StartUpDemo = "bitmap" - elif opt[0] == "--hit": - StartUpDemo = "hit" - elif opt[0] == "--hitf": - StartUpDemo = "hitf" - elif opt[0] == "--animate": - StartUpDemo = "animate" - elif opt[0] == "--speed": - StartUpDemo = "speed" - elif opt[0] == "--temp": - StartUpDemo = "temp" - elif opt[0] == "--props": - StartUpDemo = "props" - elif opt[0] == "--arrow": - StartUpDemo = "arrow" - elif opt[0] == "--hide": - StartUpDemo = "hide" + if optlist: + StartUpDemo = optlist[0][0][2:] + class DemoApp(wx.App): """ @@ -1844,6 +1855,7 @@ if __name__ == "__main__": frame.Show() ## check to see if the demo is set to start in a particular mode. + ## fixme: should this be in a dict instead? if StartUpDemo == "text": frame.TestText() elif StartUpDemo == "stext": @@ -1859,25 +1871,20 @@ if __name__ == "__main__": elif StartUpDemo == "hit": frame.TestHitTest() elif StartUpDemo == "hitf": - "starting TestHitTestForeground" frame.TestHitTestForeground() elif StartUpDemo == "animate": - "starting TestAnimation" frame.TestAnimation() elif StartUpDemo == "speed": - "starting SpeedTest" frame.SpeedTest() elif StartUpDemo == "temp": - "starting temp Test" frame.TempTest() elif StartUpDemo == "props": - "starting PropertiesChange Test" frame.PropertiesChangeTest() elif StartUpDemo == "arrow": - "starting arrow Test" frame.ArrowTest() + elif StartUpDemo == "arrowline": + frame.ArrowLineTest() elif StartUpDemo == "hide": - "starting Hide Test" frame.HideTest() return True @@ -1889,14 +1896,14 @@ else: # It's not running stand-alone, set up for wxPython demo. # don't neeed wxversion here. import wx - if not haveNumeric: + if not haveNumpy: ## TestPanel and runTest used for integration into wxPython Demo class TestPanel(wx.Panel): def __init__(self, parent, log): self.log = log wx.Panel.__init__(self, parent, -1) - import images + from wx.lib.floatcanvas.ScreenShot import getScreenShotBitmap note1 = wx.StaticText(self, -1, errorText) note2 = wx.StaticText(self, -1, "This is what the FloatCanvas can look like:") @@ -1904,7 +1911,7 @@ else: S.Add((10, 10), 1) S.Add(note1, 0, wx.ALIGN_CENTER) S.Add(note2, 0, wx.ALIGN_CENTER | wx.BOTTOM, 4) - S.Add(wx.StaticBitmap(self,-1,images.getFloatCanvasBitmap()),0,wx.ALIGN_CENTER) + S.Add(wx.StaticBitmap(self,-1,getScreenShotBitmap()),0,wx.ALIGN_CENTER) S.Add((10, 10), 1) self.SetSizer(S) self.Layout() diff --git a/wxPython/demo/Main.py b/wxPython/demo/Main.py index 2c99409724..495acedf76 100644 --- a/wxPython/demo/Main.py +++ b/wxPython/demo/Main.py @@ -19,9 +19,11 @@ # * Annoying switching between tabs and resulting flicker # how to replace a page in the notebook without deleting/adding? # Where is SetPage!? tried freeze...tried reparent of dummy panel.... +# AG: It looks like this issue is fixed by Freeze()ing and Thaw()ing the +# main frame and not the notebook # TODO List: -# * UI design more prefessional +# * UI design more professional (is the new version more professional?) # * save file positions (new field in demoModules) (@ LoadDemoSource) # * Update main overview @@ -30,6 +32,7 @@ import sys, os, time, traceback, types import wx # This module uses the new wx namespace +import wx.aui import wx.html import images @@ -43,6 +46,15 @@ import images #--------------------------------------------------------------------------- +USE_CUSTOMTREECTRL = False +ALLOW_AUI_FLOATING = False +DEFAULT_PERSPECTIVE = "Default Perspective" + +#--------------------------------------------------------------------------- + +_demoPngs = ["overview", "recent", "frame", "dialog", "moredialog", "core", + "book", "customcontrol", "morecontrols", "layout", "process", "clipboard", + "images", "miscellaneous"] _treeList = [ # new stuff @@ -631,7 +643,9 @@ class DemoCodePanel(wx.Panel): def ActiveModuleChanged(self): self.LoadDemoSource(self.demoModules.GetSource()) self.UpdateControlState() + self.mainFrame.Freeze() self.ReloadDemo() + self.mainFrame.Thaw() def LoadDemoSource(self, source): @@ -726,24 +740,37 @@ class DemoCodePanel(wx.Panel): self.demoModules.LoadFromFile(modModified, modifiedFilename) self.ActiveModuleChanged() + self.mainFrame.SetTreeModified(True) + def OnRestore(self, event): # Handles the "Delete Modified" button modifiedFilename = GetModifiedFilename(self.demoModules.name) self.demoModules.Delete(modModified) os.unlink(modifiedFilename) # Delete the modified copy busy = wx.BusyInfo("Reloading demo module...") + self.ActiveModuleChanged() + self.mainFrame.SetTreeModified(False) + #--------------------------------------------------------------------------- def opj(path): """Convert paths to the platform-specific separator""" - str = apply(os.path.join, tuple(path.split('/'))) + st = apply(os.path.join, tuple(path.split('/'))) # HACK: on Linux, a leading / gets lost... if path.startswith('/'): - str = '/' + str - return str + st = '/' + st + return st + + +def GetDataDir(): + """ + Return the standard location on this platform for application data + """ + sp = wx.StandardPaths.Get() + return sp.GetUserDataDir() def GetModifiedDirectory(): @@ -751,7 +778,7 @@ def GetModifiedDirectory(): Returns the directory where modified versions of the demo files are stored """ - return opj(wx.GetHomeDir() + "/.wxPyDemo/modified/") + return os.path.join(GetDataDir(), "modified") def GetModifiedFilename(name): @@ -760,7 +787,7 @@ def GetModifiedFilename(name): """ if not name.endswith(".py"): name = name + ".py" - return GetModifiedDirectory() + name + return os.path.join(GetModifiedDirectory(), name) def GetOriginalFilename(name): @@ -780,6 +807,25 @@ def DoesModifiedExist(name): return False +def GetConfig(): + if not os.path.exists(GetDataDir()): + os.makedirs(GetDataDir()) + + config = wx.FileConfig( + localFilename=os.path.join(GetDataDir(), "options")) + return config + + +def SearchDemo(name, keyword): + """ Returns whether a demo contains the search keyword or not. """ + fid = open(GetOriginalFilename(name), "rt") + fullText = fid.read() + fid.close() + if fullText.find(keyword) >= 0: + return True + + return False + #--------------------------------------------------------------------------- class ModuleDictWrapper: @@ -1132,10 +1178,13 @@ class wxPythonDemo(wx.Frame): overviewText = "wxPython Overview" def __init__(self, parent, title): - wx.Frame.__init__(self, parent, -1, title, size = (950, 720), + wx.Frame.__init__(self, parent, -1, title, size = (970, 720), style=wx.DEFAULT_FRAME_STYLE | wx.NO_FULL_REPAINT_ON_RESIZE) self.SetMinSize((640,480)) + + self.mgr = wx.aui.AuiManager() + self.mgr.SetManagedWindow(self) self.loaded = False self.cwd = os.getcwd() @@ -1154,8 +1203,6 @@ class wxPythonDemo(wx.Frame): except: self.tbicon = None - wx.CallAfter(self.ShowTip) - self.otherWin = None self.Bind(wx.EVT_IDLE, self.OnIdle) self.Bind(wx.EVT_CLOSE, self.OnCloseWindow) @@ -1165,119 +1212,67 @@ class wxPythonDemo(wx.Frame): self.Centre(wx.BOTH) self.CreateStatusBar(1, wx.ST_SIZEGRIP) - splitter = wx.SplitterWindow(self, -1, style=wx.CLIP_CHILDREN | wx.SP_LIVE_UPDATE | wx.SP_3D) - splitter2 = wx.SplitterWindow(splitter, -1, style=wx.CLIP_CHILDREN | wx.SP_LIVE_UPDATE | wx.SP_3D) - - def EmptyHandler(evt): pass - #splitter.Bind(wx.EVT_ERASE_BACKGROUND, EmptyHandler) - #splitter2.Bind(wx.EVT_ERASE_BACKGROUND, EmptyHandler) - - # Prevent TreeCtrl from displaying all items after destruction when True self.dying = False + self.skipLoad = False + + def EmptyHandler(evt): pass + self.ReadConfigurationFile() + # Create a Notebook - self.nb = wx.Notebook(splitter2, -1, style=wx.CLIP_CHILDREN) - - # Make a File menu - self.mainmenu = wx.MenuBar() - menu = wx.Menu() - item = menu.Append(-1, '&Redirect Output', - 'Redirect print statements to a window', - wx.ITEM_CHECK) - self.Bind(wx.EVT_MENU, self.OnToggleRedirect, item) - - exitItem = menu.Append(-1, 'E&xit\tCtrl-Q', 'Get the heck outta here!') - self.Bind(wx.EVT_MENU, self.OnFileExit, exitItem) - wx.App.SetMacExitMenuItemId(exitItem.GetId()) - self.mainmenu.Append(menu, '&File') - - # Make a Demo menu - menu = wx.Menu() - for item in _treeList[:-1]: - submenu = wx.Menu() - for childItem in item[1]: - mi = submenu.Append(-1, childItem) - self.Bind(wx.EVT_MENU, self.OnDemoMenu, mi) - menu.AppendMenu(wx.NewId(), item[0], submenu) - self.mainmenu.Append(menu, '&Demo') - - - # Make a Help menu - menu = wx.Menu() - findItem = menu.Append(-1, '&Find\tCtrl-F', 'Find in the Demo Code') - findnextItem = menu.Append(-1, 'Find &Next\tF3', 'Find Next') - menu.AppendSeparator() - - shellItem = menu.Append(-1, 'Open Py&Shell Window\tF5', - 'An interactive interpreter window with the demo app and frame objects in the namesapce') - inspToolItem = menu.Append(-1, 'Open &Widget Inspector\tF6', - 'A tool that lets you browse the live widgets and sizers in an application') - menu.AppendSeparator() - helpItem = menu.Append(-1, '&About wxPython Demo', 'wxPython RULES!!!') - wx.App.SetMacAboutMenuItemId(helpItem.GetId()) - - self.Bind(wx.EVT_MENU, self.OnOpenShellWindow, shellItem) - self.Bind(wx.EVT_MENU, self.OnOpenWidgetInspector, inspToolItem) - self.Bind(wx.EVT_MENU, self.OnHelpAbout, helpItem) - self.Bind(wx.EVT_MENU, self.OnHelpFind, findItem) - self.Bind(wx.EVT_MENU, self.OnFindNext, findnextItem) - self.Bind(wx.EVT_FIND, self.OnFind) - self.Bind(wx.EVT_FIND_NEXT, self.OnFind) - self.Bind(wx.EVT_FIND_CLOSE, self.OnFindClose) - self.Bind(wx.EVT_UPDATE_UI, self.OnUpdateFindItems, findItem) - self.Bind(wx.EVT_UPDATE_UI, self.OnUpdateFindItems, findnextItem) - self.mainmenu.Append(menu, '&Help') - self.SetMenuBar(self.mainmenu) - + self.nb = wx.Notebook(self, -1, style=wx.CLIP_CHILDREN) + imgList = wx.ImageList(16, 16) + for png in ["overview", "code", "demo"]: + bmp = images.catalog[png].getBitmap() + imgList.Add(bmp) + self.nb.AssignImageList(imgList) + + self.BuildMenuBar() + self.finddata = wx.FindReplaceData() self.finddata.SetFlags(wx.FR_DOWN) - if False: - # This is another way to set Accelerators, in addition to - # using the '\t' syntax in the menu items. - aTable = wx.AcceleratorTable([(wx.ACCEL_ALT, ord('X'), exitItem.GetId()), - (wx.ACCEL_CTRL, ord('H'), helpItem.GetId()), - (wx.ACCEL_CTRL, ord('F'), findItem.GetId()), - (wx.ACCEL_NORMAL, wx.WXK_F3, findnextItem.GetId()), - (wx.ACCEL_NORMAL, wx.WXK_F9, shellItem.GetId()), - ]) - self.SetAcceleratorTable(aTable) - - # Create a TreeCtrl - tID = wx.NewId() - leftPanel = wx.Panel(splitter) + leftPanel = wx.Panel(self, style=wx.TAB_TRAVERSAL|wx.CLIP_CHILDREN) + self.treeMap = {} + self.searchItems = {} + + self.tree = wxPythonDemoTree(leftPanel) - self.filter = wx.SearchCtrl(leftPanel) + self.filter = wx.SearchCtrl(leftPanel, style=wx.TE_PROCESS_ENTER) self.filter.ShowCancelButton(True) self.filter.Bind(wx.EVT_TEXT, self.RecreateTree) self.filter.Bind(wx.EVT_SEARCHCTRL_CANCEL_BTN, lambda e: self.filter.SetValue('')) - - self.treeMap = {} - self.tree = wx.TreeCtrl(leftPanel, tID, style = - wx.TR_DEFAULT_STYLE #| wx.TR_HAS_VARIABLE_ROW_HEIGHT - ) + self.filter.Bind(wx.EVT_TEXT_ENTER, self.OnSearch) + + searchMenu = wx.Menu() + item = searchMenu.AppendRadioItem(-1, "Sample Name") + self.Bind(wx.EVT_MENU, self.OnSearchMenu, item) + item = searchMenu.AppendRadioItem(-1, "Sample Content") + self.Bind(wx.EVT_MENU, self.OnSearchMenu, item) + self.filter.SetMenu(searchMenu) - self.root = self.tree.AddRoot("wxPython Overview") self.RecreateTree() - self.tree.Bind(wx.EVT_TREE_ITEM_EXPANDED, self.OnItemExpanded, id=tID) - self.tree.Bind(wx.EVT_TREE_ITEM_COLLAPSED, self.OnItemCollapsed, id=tID) - self.tree.Bind(wx.EVT_TREE_SEL_CHANGED, self.OnSelChanged, id=tID) + self.tree.SetExpansionState(self.expansionState) + self.tree.Bind(wx.EVT_TREE_ITEM_EXPANDED, self.OnItemExpanded) + self.tree.Bind(wx.EVT_TREE_ITEM_COLLAPSED, self.OnItemCollapsed) + self.tree.Bind(wx.EVT_TREE_SEL_CHANGED, self.OnSelChanged) self.tree.Bind(wx.EVT_LEFT_DOWN, self.OnTreeLeftDown) # Set up a wx.html.HtmlWindow on the Overview Notebook page # we put it in a panel first because there seems to be a # refresh bug of some sort (wxGTK) when it is directly in # the notebook... + if 0: # the old way self.ovr = wx.html.HtmlWindow(self.nb, -1, size=(400, 400)) - self.nb.AddPage(self.ovr, self.overviewText) + self.nb.AddPage(self.ovr, self.overviewText, imageId=0) else: # hopefully I can remove this hacky code soon, see SF bug #216861 panel = wx.Panel(self.nb, -1, style=wx.CLIP_CHILDREN) self.ovr = wx.html.HtmlWindow(panel, -1, size=(400, 400)) - self.nb.AddPage(panel, self.overviewText) + self.nb.AddPage(panel, self.overviewText, imageId=0) def OnOvrSize(evt, ovr=self.ovr): ovr.SetSize(evt.GetSize()) @@ -1290,7 +1285,7 @@ class wxPythonDemo(wx.Frame): # Set up a log window - self.log = wx.TextCtrl(splitter2, -1, + self.log = wx.TextCtrl(self, -1, style = wx.TE_MULTILINE|wx.TE_READONLY|wx.HSCROLL) if wx.Platform == "__WXMAC__": self.log.MacCheckSpelling(False) @@ -1305,30 +1300,15 @@ class wxPythonDemo(wx.Frame): #wx.Log_SetActiveTarget(wx.LogStderr()) #wx.Log_SetTraceMask(wx.TraceMessages) - self.Bind(wx.EVT_ACTIVATE, self.OnActivate) wx.GetApp().Bind(wx.EVT_ACTIVATE_APP, self.OnAppActivate) # add the windows to the splitter and split it. - splitter2.SplitHorizontally(self.nb, self.log, -160) leftBox = wx.BoxSizer(wx.VERTICAL) leftBox.Add(self.tree, 1, wx.EXPAND) leftBox.Add(wx.StaticText(leftPanel, label = "Filter Demos:"), 0, wx.TOP|wx.LEFT, 5) leftBox.Add(self.filter, 0, wx.EXPAND|wx.ALL, 5) leftPanel.SetSizer(leftBox) - splitter.SplitVertically(leftPanel, splitter2, 220) - - splitter.SetMinimumPaneSize(120) - splitter2.SetMinimumPaneSize(60) - - # Make the splitter on the right expand the top window when resized - def SplitterOnSize(evt): - splitter = evt.GetEventObject() - sz = splitter.GetSize() - splitter.SetSashPosition(sz.height - 160, False) - evt.Skip() - - splitter2.Bind(wx.EVT_SIZE, SplitterOnSize) # select initial items self.nb.SetSelection(0) @@ -1348,32 +1328,289 @@ class wxPythonDemo(wx.Frame): self.tree.SelectItem(selectedDemo) self.tree.EnsureVisible(selectedDemo) + # Use the aui manager to set up everything + self.mgr.AddPane(self.nb, wx.aui.AuiPaneInfo().CenterPane().Name("Notebook")) + self.mgr.AddPane(leftPanel, + wx.aui.AuiPaneInfo(). + Left().Layer(2).BestSize((240, -1)). + MinSize((160, -1)). + Floatable(ALLOW_AUI_FLOATING).FloatingSize((240, 700)). + Caption("wxPython Demos"). + CloseButton(False). + Name("DemoTree")) + self.mgr.AddPane(self.log, + wx.aui.AuiPaneInfo(). + Bottom().BestSize((-1, 150)). + MinSize((-1, 60)). + Floatable(ALLOW_AUI_FLOATING).FloatingSize((500, 160)). + Caption("Demo Log Messages"). + CloseButton(False). + Name("LogWindow")) + + self.auiConfigurations[DEFAULT_PERSPECTIVE] = self.mgr.SavePerspective() + self.mgr.Update() + + self.mgr.SetFlags(self.mgr.GetFlags() ^ wx.aui.AUI_MGR_TRANSPARENT_DRAG) + + - #--------------------------------------------- - + def ReadConfigurationFile(self): + + self.auiConfigurations = {} + self.expansionState = [0, 1] + + config = GetConfig() + val = config.Read('ExpansionState') + if val: + self.expansionState = eval(val) + + val = config.Read('AUIPerspectives') + if val: + self.auiConfigurations = eval(val) + + + def BuildMenuBar(self): + + # Make a File menu + self.mainmenu = wx.MenuBar() + menu = wx.Menu() + item = menu.Append(-1, '&Redirect Output', + 'Redirect print statements to a window', + wx.ITEM_CHECK) + self.Bind(wx.EVT_MENU, self.OnToggleRedirect, item) + + exitItem = wx.MenuItem(menu, -1, 'E&xit\tCtrl-Q', 'Get the heck outta here!') + exitItem.SetBitmap(images.catalog['exit'].getBitmap()) + menu.AppendItem(exitItem) + self.Bind(wx.EVT_MENU, self.OnFileExit, exitItem) + wx.App.SetMacExitMenuItemId(exitItem.GetId()) + self.mainmenu.Append(menu, '&File') + + # Make a Demo menu + menu = wx.Menu() + for indx, item in enumerate(_treeList[:-1]): + menuItem = wx.MenuItem(menu, -1, item[0]) + submenu = wx.Menu() + for childItem in item[1]: + mi = submenu.Append(-1, childItem) + self.Bind(wx.EVT_MENU, self.OnDemoMenu, mi) + menuItem.SetBitmap(images.catalog[_demoPngs[indx+1]].getBitmap()) + menuItem.SetSubMenu(submenu) + menu.AppendItem(menuItem) + self.mainmenu.Append(menu, '&Demo') + + # Make an Option menu + # If we've turned off floatable panels then this menu is not needed + if ALLOW_AUI_FLOATING: + menu = wx.Menu() + auiPerspectives = self.auiConfigurations.keys() + auiPerspectives.sort() + perspectivesMenu = wx.Menu() + item = wx.MenuItem(perspectivesMenu, -1, DEFAULT_PERSPECTIVE, "Load startup default perspective", wx.ITEM_RADIO) + self.Bind(wx.EVT_MENU, self.OnAUIPerspectives, item) + perspectivesMenu.AppendItem(item) + for indx, key in enumerate(auiPerspectives): + if key == DEFAULT_PERSPECTIVE: + continue + item = wx.MenuItem(perspectivesMenu, -1, key, "Load user perspective %d"%indx, wx.ITEM_RADIO) + perspectivesMenu.AppendItem(item) + self.Bind(wx.EVT_MENU, self.OnAUIPerspectives, item) + + menu.AppendMenu(wx.ID_ANY, "&AUI Perspectives", perspectivesMenu) + self.perspectives_menu = perspectivesMenu + + item = wx.MenuItem(menu, -1, 'Save Perspective', 'Save AUI perspective') + item.SetBitmap(images.catalog['saveperspective'].getBitmap()) + menu.AppendItem(item) + self.Bind(wx.EVT_MENU, self.OnSavePerspective, item) + + item = wx.MenuItem(menu, -1, 'Delete Perspective', 'Delete AUI perspective') + item.SetBitmap(images.catalog['deleteperspective'].getBitmap()) + menu.AppendItem(item) + self.Bind(wx.EVT_MENU, self.OnDeletePerspective, item) + + menu.AppendSeparator() + + item = wx.MenuItem(menu, -1, 'Restore Tree Expansion', 'Restore the initial tree expansion state') + item.SetBitmap(images.catalog['expansion'].getBitmap()) + menu.AppendItem(item) + self.Bind(wx.EVT_MENU, self.OnTreeExpansion, item) + + self.mainmenu.Append(menu, '&Options') + + # Make a Help menu + menu = wx.Menu() + findItem = wx.MenuItem(menu, -1, '&Find\tCtrl-F', 'Find in the Demo Code') + findItem.SetBitmap(images.catalog['find'].getBitmap()) + findNextItem = wx.MenuItem(menu, -1, 'Find &Next\tF3', 'Find Next') + findNextItem.SetBitmap(images.catalog['findnext'].getBitmap()) + menu.AppendItem(findItem) + menu.AppendItem(findNextItem) + menu.AppendSeparator() + + shellItem = wx.MenuItem(menu, -1, 'Open Py&Shell Window\tF5', + 'An interactive interpreter window with the demo app and frame objects in the namesapce') + shellItem.SetBitmap(images.catalog['pyshell'].getBitmap()) + menu.AppendItem(shellItem) + inspToolItem = wx.MenuItem(menu, -1, 'Open &Widget Inspector\tF6', + 'A tool that lets you browse the live widgets and sizers in an application') + inspToolItem.SetBitmap(images.catalog['inspect'].getBitmap()) + menu.AppendItem(inspToolItem) + menu.AppendSeparator() + helpItem = menu.Append(-1, '&About wxPython Demo', 'wxPython RULES!!!') + wx.App.SetMacAboutMenuItemId(helpItem.GetId()) + + self.Bind(wx.EVT_MENU, self.OnOpenShellWindow, shellItem) + self.Bind(wx.EVT_MENU, self.OnOpenWidgetInspector, inspToolItem) + self.Bind(wx.EVT_MENU, self.OnHelpAbout, helpItem) + self.Bind(wx.EVT_MENU, self.OnHelpFind, findItem) + self.Bind(wx.EVT_MENU, self.OnFindNext, findNextItem) + self.Bind(wx.EVT_FIND, self.OnFind) + self.Bind(wx.EVT_FIND_NEXT, self.OnFind) + self.Bind(wx.EVT_FIND_CLOSE, self.OnFindClose) + self.Bind(wx.EVT_UPDATE_UI, self.OnUpdateFindItems, findItem) + self.Bind(wx.EVT_UPDATE_UI, self.OnUpdateFindItems, findNextItem) + self.mainmenu.Append(menu, '&Help') + self.SetMenuBar(self.mainmenu) + + if False: + # This is another way to set Accelerators, in addition to + # using the '\t' syntax in the menu items. + aTable = wx.AcceleratorTable([(wx.ACCEL_ALT, ord('X'), exitItem.GetId()), + (wx.ACCEL_CTRL, ord('H'), helpItem.GetId()), + (wx.ACCEL_CTRL, ord('F'), findItem.GetId()), + (wx.ACCEL_NORMAL, wx.WXK_F3, findnextItem.GetId()), + (wx.ACCEL_NORMAL, wx.WXK_F9, shellItem.GetId()), + ]) + self.SetAcceleratorTable(aTable) + + + #--------------------------------------------- def RecreateTree(self, evt=None): + # Catch the search type (name or content) + searchMenu = self.filter.GetMenu().GetMenuItems() + fullSearch = searchMenu[1].IsChecked() + + if evt: + if fullSearch: + # Do not`scan all the demo files for every char + # the user input, use wx.EVT_TEXT_ENTER instead + return + + expansionState = self.tree.GetExpansionState() + + current = None + item = self.tree.GetSelection() + if item: + prnt = self.tree.GetItemParent(item) + if prnt: + current = (self.tree.GetItemText(item), + self.tree.GetItemText(prnt)) + self.tree.Freeze() self.tree.DeleteAllItems() self.root = self.tree.AddRoot("wxPython Overview") + self.tree.SetItemImage(self.root, 0) + self.tree.SetItemPyData(self.root, 0) + + treeFont = self.tree.GetFont() + catFont = self.tree.GetFont() + + # The old native treectrl on MSW has a bug where it doesn't + # draw all of the text for an item if the font is larger than + # the default. It seems to be clipping the item's label as if + # it was the size of the same label in the default font. + if 'wxMSW' not in wx.PlatformInfo or wx.GetApp().GetComCtl32Version() >= 600: + treeFont.SetPointSize(treeFont.GetPointSize()+2) + treeFont.SetWeight(wx.BOLD) + catFont.SetWeight(wx.BOLD) + + self.tree.SetItemFont(self.root, treeFont) + firstChild = None + selectItem = None filter = self.filter.GetValue() + count = 0 + for category, items in _treeList: + count += 1 if filter: - items = [item for item in items if filter.lower() in item.lower()] + if fullSearch: + items = self.searchItems[category] + else: + items = [item for item in items if filter.lower() in item.lower()] if items: - child = self.tree.AppendItem(self.root, category) + child = self.tree.AppendItem(self.root, category, image=count) + self.tree.SetItemFont(child, catFont) + self.tree.SetItemPyData(child, count) if not firstChild: firstChild = child for childItem in items: - theDemo = self.tree.AppendItem(child, childItem) + image = count + if DoesModifiedExist(childItem): + image = len(_demoPngs) + theDemo = self.tree.AppendItem(child, childItem, image=image) + self.tree.SetItemPyData(theDemo, count) self.treeMap[childItem] = theDemo - + if current and (childItem, category) == current: + selectItem = theDemo + + self.tree.Expand(self.root) if firstChild: self.tree.Expand(firstChild) if filter: self.tree.ExpandAll() + elif expansionState: + self.tree.SetExpansionState(expansionState) + if selectItem: + self.skipLoad = True + self.tree.SelectItem(selectItem) + self.skipLoad = False + self.tree.Thaw() - + self.searchItems = {} + + + def OnSearchMenu(self, event): + + # Catch the search type (name or content) + searchMenu = self.filter.GetMenu().GetMenuItems() + fullSearch = searchMenu[1].IsChecked() + + if fullSearch: + self.OnSearch() + else: + self.RecreateTree() + + + def OnSearch(self, event=None): + + value = self.filter.GetValue() + if not value: + self.RecreateTree() + return + + wx.BeginBusyCursor() + + for category, items in _treeList: + self.searchItems[category] = [] + for childItem in items: + if SearchDemo(childItem, value): + self.searchItems[category].append(childItem) + + wx.EndBusyCursor() + self.RecreateTree() + + + def SetTreeModified(self, modified): + item = self.tree.GetSelection() + if modified: + image = len(_demoPngs) + else: + image = self.tree.GetItemPyData(item) + self.tree.SetItemImage(item, image) + + def WriteText(self, text): if text[-1:] == '\n': text = text[:-1] @@ -1405,7 +1642,7 @@ class wxPythonDemo(wx.Frame): #--------------------------------------------- def OnSelChanged(self, event): - if self.dying or not self.loaded: + if self.dying or not self.loaded or self.skipLoad: return item = event.GetItem() @@ -1416,6 +1653,7 @@ class wxPythonDemo(wx.Frame): def LoadDemo(self, demoName): try: wx.BeginBusyCursor() + self.Freeze() os.chdir(self.cwd) self.ShutdownDemoModule() @@ -1433,13 +1671,13 @@ class wxPythonDemo(wx.Frame): wx.LogMessage("Loading demo %s.py..." % demoName) self.demoModules = DemoModules(demoName) self.LoadDemoSource() - self.tree.Refresh() else: self.SetOverview("wxPython", mainOverview) self.codePage = None self.UpdateNotebook(0) finally: wx.EndBusyCursor() + self.Thaw() #--------------------------------------------- def LoadDemoSource(self): @@ -1470,6 +1708,10 @@ class wxPythonDemo(wx.Frame): self.demoPage = DemoErrorPanel(self.nb, self.codePage, DemoError(sys.exc_info()), self) + bg = self.nb.GetThemeBackgroundColour() + if bg: + self.demoPage.SetBackgroundColour(bg) + assert self.demoPage is not None, "runTest must return a window!" else: @@ -1480,7 +1722,7 @@ class wxPythonDemo(wx.Frame): self.SetOverview(self.demoModules.name + " Overview", overviewText) if self.firstTime: - # cahnge to the demo page the first time a module is run + # change to the demo page the first time a module is run self.UpdateNotebook(2) self.firstTime = False else: @@ -1500,6 +1742,7 @@ class wxPythonDemo(wx.Frame): def UpdateNotebook(self, select = -1): nb = self.nb debug = False + self.Freeze() def UpdatePage(page, pageText): pageExists = False @@ -1513,15 +1756,13 @@ class wxPythonDemo(wx.Frame): if page: if not pageExists: # Add a new page - nb.AddPage(page, pageText) + nb.AddPage(page, pageText, imageId=nb.GetPageCount()) if debug: wx.LogMessage("DBG: ADDED %s" % pageText) else: if nb.GetPage(pagePos) != page: # Reload an existing page - nb.Freeze() nb.DeletePage(pagePos) - nb.InsertPage(pagePos, page, pageText) - nb.Thaw() + nb.InsertPage(pagePos, page, pageText, imageId=pagePos) if debug: wx.LogMessage("DBG: RELOADED %s" % pageText) else: # Excellent! No redraw/flicker @@ -1541,7 +1782,9 @@ class wxPythonDemo(wx.Frame): if select >= 0 and select < nb.GetPageCount(): nb.SetSelection(select) - + + self.Thaw() + #--------------------------------------------- def SetOverview(self, name, text): self.curOverview = text @@ -1566,6 +1809,70 @@ class wxPythonDemo(wx.Frame): else: app.RestoreStdio() print "Print statements and other standard output will now be sent to the usual location." + + + def OnAUIPerspectives(self, event): + perspective = self.perspectives_menu.GetLabel(event.GetId()) + self.mgr.LoadPerspective(self.auiConfigurations[perspective]) + self.mgr.Update() + + + def OnSavePerspective(self, event): + dlg = wx.TextEntryDialog(self, "Enter a name for the new perspective:", "AUI Configuration") + + dlg.SetValue(("Perspective %d")%(len(self.auiConfigurations)+1)) + if dlg.ShowModal() != wx.ID_OK: + return + + perspectiveName = dlg.GetValue() + menuItems = self.perspectives_menu.GetMenuItems() + for item in menuItems: + if item.GetLabel() == perspectiveName: + wx.MessageBox("The selected perspective name:\n\n%s\n\nAlready exists."%perspectiveName, + "Error", style=wx.ICON_ERROR) + return + + item = wx.MenuItem(self.perspectives_menu, -1, dlg.GetValue(), + "Load user perspective %d"%(len(self.auiConfigurations)+1), + wx.ITEM_RADIO) + self.Bind(wx.EVT_MENU, self.OnAUIPerspectives, item) + self.perspectives_menu.AppendItem(item) + item.Check(True) + self.auiConfigurations.update({dlg.GetValue(): self.mgr.SavePerspective()}) + + + def OnDeletePerspective(self, event): + menuItems = self.perspectives_menu.GetMenuItems()[1:] + lst = [] + loadDefault = False + + for item in menuItems: + lst.append(item.GetLabel()) + + dlg = wx.MultiChoiceDialog(self, + "Please select the perspectives\nyou would like to delete:", + "Delete AUI Perspectives", lst) + + if dlg.ShowModal() == wx.ID_OK: + selections = dlg.GetSelections() + strings = [lst[x] for x in selections] + for sel in strings: + self.auiConfigurations.pop(sel) + item = menuItems[lst.index(sel)] + if item.IsChecked(): + loadDefault = True + self.perspectives_menu.GetMenuItems()[0].Check(True) + self.perspectives_menu.DeleteItem(item) + lst.remove(sel) + + if loadDefault: + self.mgr.LoadPerspective(self.auiConfigurations[DEFAULT_PERSPECTIVE]) + self.mgr.Update() + + + def OnTreeExpansion(self, event): + self.tree.SetExpansionState(self.expansionState) + def OnHelpAbout(self, event): from About import MyAboutBox @@ -1686,6 +1993,12 @@ class wxPythonDemo(wx.Frame): self.mainmenu = None if self.tbicon is not None: self.tbicon.Destroy() + + config = GetConfig() + config.Write('ExpansionState', str(self.tree.GetExpansionState())) + config.Write('AUIPerspectives', str(self.auiConfigurations)) + config.Flush() + self.Destroy() @@ -1699,18 +2012,20 @@ class wxPythonDemo(wx.Frame): #--------------------------------------------- def ShowTip(self): - try: - showTipText = open(opj("data/showTips")).read() + config = GetConfig() + showTipText = config.Read("tips") + if showTipText: showTip, index = eval(showTipText) - except IOError: + else: showTip, index = (1, 0) + if showTip: tp = wx.CreateFileTipProvider(opj("data/tips.txt"), index) ##tp = MyTP(0) showTip = wx.ShowTip(self, tp) index = tp.GetCurrentTip() - open(opj("data/showTips"), "w").write(str( (showTip, index) )) - + config.Write("tips", str( (showTip, index) )) + config.Flush() #--------------------------------------------- def OnDemoMenu(self, event): @@ -1775,8 +2090,54 @@ class MySplashScreen(wx.SplashScreen): frame.Show() if self.fc.IsRunning(): self.Raise() + wx.CallAfter(frame.ShowTip) + + + + +#--------------------------------------------------------------------------- + +from wx.lib.mixins.treemixin import ExpansionState +if USE_CUSTOMTREECTRL: + import wx.lib.customtreectrl as CT + TreeBaseClass = CT.CustomTreeCtrl +else: + TreeBaseClass = wx.TreeCtrl + + +class wxPythonDemoTree(ExpansionState, TreeBaseClass): + def __init__(self, parent): + TreeBaseClass.__init__(self, parent, style=wx.TR_DEFAULT_STYLE| + wx.TR_HAS_VARIABLE_ROW_HEIGHT) + self.BuildTreeImageList() + if USE_CUSTOMTREECTRL: + self.SetSpacing(10) + self.SetWindowStyle(self.GetWindowStyle() & ~wx.TR_LINES_AT_ROOT) + + def AppendItem(self, parent, text, image=-1, wnd=None): + if USE_CUSTOMTREECTRL: + item = TreeBaseClass.AppendItem(self, parent, text, image=image, wnd=wnd) + else: + item = TreeBaseClass.AppendItem(self, parent, text, image=image) + return item + + def BuildTreeImageList(self): + imgList = wx.ImageList(16, 16) + for png in _demoPngs: + imgList.Add(images.catalog[png].getBitmap()) + + # add the image for modified demos. + imgList.Add(images.catalog["custom"].getBitmap()) + self.AssignImageList(imgList) + + def GetItemIdentity(self, item): + return self.GetPyData(item) + + +#--------------------------------------------------------------------------- + class MyApp(wx.App): def OnInit(self): """ @@ -1785,7 +2146,8 @@ class MyApp(wx.App): """ wx.SystemOptions.SetOptionInt("mac.window-plain-transition", 1) - + self.SetAppName("wxPyDemo") + # For debugging #self.SetAssertMode(wx.PYAPP_ASSERT_DIALOG) diff --git a/wxPython/demo/MaskedNumCtrl.py b/wxPython/demo/MaskedNumCtrl.py index 28a488584b..6926f8da5a 100644 --- a/wxPython/demo/MaskedNumCtrl.py +++ b/wxPython/demo/MaskedNumCtrl.py @@ -54,6 +54,7 @@ The controls at the top reconfigure the resulting control at the bottom. self.limit_target = wx.CheckBox( panel, -1, "Limit control" ) + self.limit_on_field_change = wx.CheckBox( panel, -1, "Limit on field change" ) self.allow_none = wx.CheckBox( panel, -1, "Allow empty control" ) self.group_digits = wx.CheckBox( panel, -1, "Group digits" ) self.group_digits.SetValue( True ) @@ -102,21 +103,26 @@ value entry ctrl:""") grid1.Add( self.limit_target, 0, wx.ALIGN_LEFT|wx.ALL, 5 ) - grid1.Add( self.allow_none, 0, wx.ALIGN_LEFT|wx.ALL, 5 ) + grid1.Add( self.limit_on_field_change, 0, wx.ALIGN_LEFT|wx.ALL, 5 ) + hbox1 = wx.BoxSizer( wx.HORIZONTAL ) hbox1.Add( (17,5), 0, wx.ALIGN_LEFT|wx.ALL, 5) - hbox1.Add( self.group_digits, 0, wx.ALIGN_LEFT|wx.LEFT, 5 ) + hbox1.Add( self.allow_none, 0, wx.ALIGN_LEFT|wx.ALL, 5 ) grid1.Add( hbox1, 0, wx.ALIGN_LEFT|wx.ALL, 5) grid1.Add( (5,5), 0, wx.ALIGN_LEFT|wx.ALL, 5) - grid1.Add( self.allow_negative, 0, wx.ALIGN_LEFT|wx.ALL, 5 ) - grid1.Add( self.use_parens, 0, wx.ALIGN_LEFT|wx.ALL, 5 ) + grid1.Add( self.group_digits, 0, wx.ALIGN_LEFT|wx.LEFT, 5 ) + grid1.Add( self.allow_negative, 0, wx.ALIGN_LEFT|wx.ALL, 5 ) hbox2 = wx.BoxSizer( wx.HORIZONTAL ) hbox2.Add( (17,5), 0, wx.ALIGN_LEFT|wx.ALL, 5) - hbox2.Add( self.select_on_entry, 0, wx.ALIGN_LEFT|wx.LEFT, 5 ) + hbox2.Add( self.use_parens, 0, wx.ALIGN_LEFT|wx.ALL, 5 ) grid1.Add( hbox2, 0, wx.ALIGN_LEFT|wx.ALL, 5) grid1.Add( (5,5), 0, wx.ALIGN_LEFT|wx.ALL, 5) - + + grid1.Add( self.select_on_entry, 0, wx.ALIGN_LEFT|wx.LEFT, 5 ) + grid1.Add( (5,5), 0, wx.ALIGN_LEFT|wx.ALL, 5) + grid1.Add( (5,5), 0, wx.ALIGN_LEFT|wx.ALL, 5) + grid1.Add( (5,5), 0, wx.ALIGN_LEFT|wx.ALL, 5) grid2 = wx.FlexGridSizer( 0, 2, 0, 0 ) grid2.Add( label, 0, wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 ) @@ -151,7 +157,8 @@ value entry ctrl:""") self.Bind(masked.EVT_NUM, self.SetTargetMinMax, self.min ) self.Bind(masked.EVT_NUM, self.SetTargetMinMax, self.max ) - self.Bind(wx.EVT_CHECKBOX, self.SetTargetMinMax, self.limit_target ) + self.Bind(wx.EVT_CHECKBOX, self.OnSetLimited, self.limit_target ) + self.Bind(wx.EVT_CHECKBOX, self.OnSetLimitOnFieldChange, self.limit_on_field_change ) self.Bind(wx.EVT_CHECKBOX, self.OnSetAllowNone, self.allow_none ) self.Bind(wx.EVT_CHECKBOX, self.OnSetGroupDigits, self.group_digits ) self.Bind(wx.EVT_CHECKBOX, self.OnSetAllowNegative, self.allow_negative ) @@ -218,10 +225,26 @@ value entry ctrl:""") self.SetTargetMinMax() + def OnSetLimited( self, event ): + limited = self.limit_target.GetValue() + self.target_ctl.SetLimited( limited ) + limit_on_field_change = self.limit_on_field_change.GetValue() + if limited and limit_on_field_change: + self.limit_on_field_change.SetValue(False) + self.target_ctl.SetLimitOnFieldChange( False ) + self.SetTargetMinMax() + + + def OnSetLimitOnFieldChange( self, event ): + limit_on_field_change = self.limit_on_field_change.GetValue() + self.target_ctl.SetLimitOnFieldChange( limit_on_field_change ) + limited = self.limit_target.GetValue() + if limited and limit_on_field_change: + self.limit_target.SetValue(False) + self.target_ctl.SetLimited( False ) + def SetTargetMinMax( self, event=None ): min = max = None - self.target_ctl.SetLimited( self.limit_target.GetValue() ) - if self.set_min.GetValue(): min = self.min.GetValue() if self.set_max.GetValue(): diff --git a/wxPython/demo/OGL.py b/wxPython/demo/OGL.py index 871e6bc9cb..f0709c75a9 100644 --- a/wxPython/demo/OGL.py +++ b/wxPython/demo/OGL.py @@ -211,7 +211,8 @@ class MyEvtHandler(ogl.ShapeEvtHandler): if shape.Selected(): shape.Select(False, dc) - canvas.Redraw(dc) + #canvas.Redraw(dc) + canvas.Refresh(False) else: redraw = False shapeList = canvas.GetDiagram().GetShapeList() @@ -230,7 +231,8 @@ class MyEvtHandler(ogl.ShapeEvtHandler): for s in toUnselect: s.Select(False, dc) - canvas.Redraw(dc) + ##canvas.Redraw(dc) + canvas.Refresh(False) self.UpdateStatusBar(shape) @@ -251,9 +253,11 @@ class MyEvtHandler(ogl.ShapeEvtHandler): def OnMovePost(self, dc, x, y, oldX, oldY, display): + shape = self.GetShape() ogl.ShapeEvtHandler.OnMovePost(self, dc, x, y, oldX, oldY, display) - self.UpdateStatusBar(self.GetShape()) - + self.UpdateStatusBar(shape) + if "wxMac" in wx.PlatformInfo: + shape.GetCanvas().Refresh(False) def OnRightClick(self, *dontcare): self.log.WriteText("%s\n" % self.GetShape()) @@ -334,8 +338,8 @@ class TestWindow(ogl.ShapeCanvas): s.SetBitmap(bmp) self.MyAddShape(s, 225, 130, None, None, "Bitmap") - dc = wx.ClientDC(self) - self.PrepareDC(dc) + #dc = wx.ClientDC(self) + #self.PrepareDC(dc) for x in range(len(self.shapes)): fromShape = self.shapes[x] diff --git a/wxPython/demo/TreeMixin.py b/wxPython/demo/TreeMixin.py index 7c31599a3f..059ddd6179 100644 --- a/wxPython/demo/TreeMixin.py +++ b/wxPython/demo/TreeMixin.py @@ -1,6 +1,10 @@ import wx, wx.lib.customtreectrl, wx.gizmos -import treemixin +try: + import treemixin +except ImportError: + from wx.lib.mixins import treemixin +overview = treemixin.__doc__ class TreeModel(object): ''' TreeModel holds the domain objects that are shown in the different diff --git a/wxPython/demo/bmp_source/book.png b/wxPython/demo/bmp_source/book.png new file mode 100644 index 0000000000000000000000000000000000000000..7d863f949741ff83fd8373a77c0d95a3d95e441f GIT binary patch literal 622 zcmV-!0+IcRP)YeaZ-G+53gSTz{SPWVdFiPaPX+$~@n)fi9>qgJ zh4fN-QcEhq^wI<|vImjml9)pF*W2zl+qWbTPaSyKyqWKt`DS)j3xa?yVgf%ewhl|m zA$_0x^Rx4E@d&=>uRoSN*Cm&aL#`7&zr0;L(pKMn1G#$ZszFkMC^?B8f`0w&-UD$u zd-(?iK6#!;xZ`>J^A9DGiTfguewDOKwDEKoL}`ZE_M}0=xvsgf$usmJeV$oo28d0#yu!pdgpag$giC zhlKP!CMX3wsPMGgZ5n>Xg+hV--ENofAcH61#0B7Hvl-7Il}g2A6;lfG`FxbX8A>p$ z0P;GW4il9Mr9jO9)xB^Rgy%*CSTdQ6D;kDDnM{U{5Q21FA4#ZM71$_Dba8z<-Y_Na zS@|v#c0PZNDux9AoXD-ZoWP~q)7QrC`GWdKcSATcu_MrC3kGB7YQATcvKF)}(bF(4~2F)%O}y-DBz000McNliru z(*zh07b}fQ!#4l`010qNS#tmY3h)2`3h)6!tTdPa000DMK}|sb0I`n?{9y$E00Iq3 zL_t(|+O1O0YZE~f{${dUn`{iMDHh5m6qM*e1w}k6*pnCWAMhgh9|Y{dA6KCeQapIF zM^EBkV8xS%;-Mm<)__>0SObk4)2_Qa^Ts!uCKwaF=)mmTH}AdgeeVbGk4OB;&}cM{ zl}e>Y#uy!j;pKY0zWG;x^xlaD_?5c`qSmHzZs+LLd|~#8bX^xatu^hxx&OmgTiPgW z#L@2J;hUSQHqIGB%~Y3UxqMpA6!Erw2^Y>?M4G0VX>RUH?pf@33wi}=FFLC~eOTh> zGa~>*qUIbb;=zlPc=h@Tp0B-x);hCEDbfDg#>eC5aV>ZXl~8REHv}-60G%RV@X%~7 zkJM>#lmc$wS_CSBb%ikiGL(54mc5=rtF?z*&Ox`kjWFCo5Cm+?s3P5GHVTj=83Q7N zV$nghdH_O5#PKYm$j9{b!2ws$L;$TMxC|Z}IvtJm^$1f_F0 zP%LZ-nr)haq>LF%vm`DB85^5DOG36`zX51FGNxu3L{Z9Fzrb}J9_22Q}5VQk%3Yh zao9a|Jy^|VZK)1X?Ek*r;e}%Pehi@3{dR{J{jpX?*ydSh#X+{AinH2lH&c;*C!+Wv b{@3{lGGZ%X#{_ri00000NkvXXu0mjfwB;#r literal 0 HcmV?d00001 diff --git a/wxPython/demo/bmp_source/code.png b/wxPython/demo/bmp_source/code.png new file mode 100644 index 0000000000000000000000000000000000000000..8fcf0f09ae3046a7bace82cdd583ebcb2bb58beb GIT binary patch literal 868 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 SO7D080000H@%#N(Z{1qHS*%O>6o+OXO# zAnG)<+owDUrLmstGh*sK3j}X20Rv4knJ8ql1s;6*n9Xlf9K?Hwq8_hop*^sZ&i6?D zsZv%zcD+Hq)MQSU7oAS044^d_j1wPy`2OAB4)Ro#9{oWd`{*eq^EjEp8HHk%dZSHM zQK?iF%9RGrX^Zyndi9Up-KV?ScDpU!zyHC<*+Nq|h$-}Y9VBg?lT#gq${E9EJ+0OW zxq?EmSmDPEXjKztUtfnWJPfmA^7?M=u$x+{Zjl)B0YisXm0K(zO-l$nb_Br`$=~rrwZAOe58qF4o-_wM{5zNK` zH?Ci!eEbAmKSeg1RZ69jAOLtgp4IQa`{sw2M_P8wKXb+Gz}@v=wMh({B{UjP6s5Pn zf0)|b{5qLPB<^&(-N)&4`dtA)lB7%9+uJXuryT|fBM3dp^&&?{iQ~P!$jeYD^fVTW zg=)206evrQ^od@t|GZwWf2IRaRW-A-v$L-#wYgB}`C%jy2}PsPXQfi<1#k@XUe#Sy v)i1i;?vNk|mRDPF14BRpXun>I{{#O3eDFiOU6rK=00000NkvXXu0mjfi{guF literal 0 HcmV?d00001 diff --git a/wxPython/demo/bmp_source/dialog.png b/wxPython/demo/bmp_source/dialog.png new file mode 100644 index 0000000000000000000000000000000000000000..bf02592036af45bcc83ceff4aaa412a570a2d6a1 GIT binary patch literal 237 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`EX7WqAsj$Z!;#VfIC*im7>RppS8v`VI#V)w#VqH? z`}w|Kj}vvM&YGwf&%bNC$DuPf7+nv4{-|IyE{8~S4%hrvfeo( z$F@{WpRL~W1CN2J{>zgIQI9S)y{g^2Dm~q2(ULbIg*~gQl)N9jYfHTp!;tEZx5S@1^H2&HDcTKL-#%Okh(O8FMs?3c#B0tX<3S z;Otq3r>9OaFarJ0^y?SH>!(i{-stNy+_`s`;qS{g3|3Oo%)oG}1nC6`Ag}?vGU9>a zzg{!E-@lXL9}_c!ii-;a!_y}W48MOfF#i3^@c+pZ1_c`%2Eo)chI>~oF|hpm!63lH z;|kIX5J2EC;F6LQ{}C<6-z2Ll5$GlS%puM96cI~ZiT zx*3iO2r+OyzQ@4J%EANE3lKnH1O5U1_Vdek21X7xhW|h(|2=(zf$9H0hM(7Of`j<~ zw@(Z|PMl!)`~4dOn}Pzv&s)I2<=_Ix96$hp4fycx!~36#vSQ3nUo$WZ3NZY6^N!)s z&TR~TcW-3)^P7R;>)WpkLe{nnEIfP+|6jgj_|M41@Zs0*Zy>z@0R#?%*H2$;d&9)T z!1DGL!>=R97{psU7+A%n8Gbzf%JA*YR|ak&5eB91Zib&Xt}%SOc#h#4Cp*KlA3qO* z^a2DBI2yYd8TAyj)z4Zhi?V#V^^8Gs9nf}OE{3n`H#7VP1|pxI4+AS38^Z-JFNU8F zZZn)@VET9Q^HkLMm}H? zX8HS%;oqOX44=P#XLt=vjW>XPzwqT-2QYCpvjfxDLZDuN0D>C;3K0)&hU@tFVG7g{{H{=>i6&cAOHOA20Hl&BTyYH5Q7W=2p~pEvok<|0RYy)V;Aj; RqCx-w002ovPDHLkV1k%q%fkQw literal 0 HcmV?d00001 diff --git a/wxPython/demo/bmp_source/expansion.png b/wxPython/demo/bmp_source/expansion.png new file mode 100644 index 0000000000000000000000000000000000000000..477387c6111d7f115823587940e634a7a588d190 GIT binary patch literal 894 zcmV-^1A+XBP)42~`H$h>&p!?-=H)I0}cNLG~gf5 z04AU*w=S~$1_&S)HR1mZ33?0+uV24qIDh^egNuhN0|V3&24;v&|ACx;zk#m!^N->0 zzrPIs{`_ZPXJufBuV!KZ2p|@a8djk5SQ-B^{QdO}ECB~^uP`utdkZv#k>S6X5yL+Y zE`~oq&Hq?{1^_j(F#Tr$2p|>)pot*x4`>ZDGYbPU_^~#X;qUd6V3)Au9A@}0YYlV- z&;XzxfOazc{rjH*Ab?o@Li97Rv$HeYyLXRa>C&YPKY#vYFfg@YFc1)C_{q%<^e{8f zOUw*^fNKB$1A5~xFeVs5mi}V^2p|@)2Z7-C@81lPl9CKAE-pZ!{|ua5+zfwia4|4& z@q-Qc3pC(A&}NWkkj=n|1iIuO13&<={D&F6n(!K z{s5x{sf1sDWC|Nr<5w2KL>8SD+F|6rQ{0*D2spO24^ft`bcVgLU9 zz*zXjpr&umV9Eqc`=E3TjOxE&%mCI5b^*gbu=fB0h=m!F<`_7DnuCLaP{QIjVEYlKmf7qKKJ&|ljk4*{QL&A?bjb*ZUct) zZ&0oS+W23d;rAZ}p!jcwf8T+r^4|ppn1`8}7#VKd`|t-KfEfS($BYG106>5N05B?r UYclv4pa1{>07*qoM6N<$f=kki5C8xG literal 0 HcmV?d00001 diff --git a/wxPython/demo/bmp_source/find.png b/wxPython/demo/bmp_source/find.png new file mode 100644 index 0000000000000000000000000000000000000000..caa587d78f6d06db7891c3f71a9da411defc640e GIT binary patch literal 985 zcmV;~119{5P)m? zKmfrFIIwl6n3#+dhnlMggQkNbgD}w9Y@oo97iV~M?FqxfJNFp4g~b{8c)1x^*f<&3 z*;yEYo@4?DAh-d)e*Q@M!K=W}Bd^Ht?mYv;Z%&{AzySCG47^V~k_>;IzGIMN`^3P* z!wWQk9pq)8eun=50fdN!zkl`KeCK0$^8y$iz)1YU4GGoHKNuL^ePLqY{LRfE4^+*; z!3NaK&cMP934ee9g2%$wuixIX|NF!s#KpkC4|F>)z!`XfG&|4*jDHvz{{Q^Kz{JSJ zz`@SOz{$bNz`_KM1O|Wrf*bJZsx$+GrYr-4nj!;(DD!Iu z&c7cSxP?R+m|0jDK$bHyF@yC11Q6T+7k{tgnkr&%_br;oaP{O32F5Sn!T9Orn+*4l zA7uFYjMZNxB*|kd}a72z{cR_Y0Y5gY{BsE z<5yrxWMbeG;9>ao`2)kb3)u{UY{Cp2s;ms3uaz_W`Dz9X9j3nk0fds2Vb0=UV`h*N znz<)7-0D`-KjfM69jr&i& zUb=D#m~WVWg7WC!f1o@8!HlfD3@=108LloWV2=I&kIm9pj>AG-Fc=_!;06c?2!5G2 zqsw~LqNxHv<;eL82{8Yb=VuW6{pRr0-x@InSHEd}1_&?!{Pzrdo1Z{v00000NkvXX Hu0mjf7tG2j literal 0 HcmV?d00001 diff --git a/wxPython/demo/bmp_source/findnext.png b/wxPython/demo/bmp_source/findnext.png new file mode 100644 index 0000000000000000000000000000000000000000..f5a4e1dc06a5e2fab341751af4938c4883fee9a5 GIT binary patch literal 811 zcmV+`1JwM9P)JUn(CdmNL8oYaZirVa(QRa()QP=p{nS_CXuu%L)4D;^sbNDv9JU_%8` zHmG8OC@2erU_lp9sXPjXN@?1(h4eucNDC1WX^0a$!c;DcT zBcq2!6QF7;sv==qHKwL!aOy64G)!k_5~Wrnux-3b1#RnA+KOlUhrauKmeIpont-|A z{=_S{aB}trscbLXvfV@!iNA{`o#isVZBs860nlp_6jllaeivbPWO4Du+gI@YolCViOADYnP}#JWs%b;6RKlxNkW`hGLIJC{pX>Xd zteQW5<2M18RV}s!tF%TTp+P(ht`GGZxYa7cwkb_cV)xz6=6Bym`QposXIw3tpJ=n^ zo(;KqCAZBWCW8>L*b1vcurx7&-M@>jx8Fk!ffSNA0*0SDIN?;x;;Hdt9fml_;LaWp zFn@Ut)%TfCbg^q>7^z-Dj;4_QTcigcAExV$2!C9<#J9)4=HmBp^oUNYkw72&lz*l^ z=Dy`)$W8%&_ZX5A-UzTN7J;Gh$blj5e`t{9vV{;K;zo?BS>nRaFO$?#=zS{Y)z|Q> zdjy314OMQ~(lwcMTfABCXv*NOmrn4@M^7*iI!}CHKl!U4>YEMG-UMihDz4r5%bLDC z9SCGc0DiC*q!1vfEiAN+a%KD#`8g42`8^qZFqeF~33xQ0sXX)iJ42t1A2mQ)|5xiI pyFCfo-KC$7y8VZCU#ade{{_<6ICe=GJc$4R002ovPDHLkV1khXdkz2q literal 0 HcmV?d00001 diff --git a/wxPython/demo/bmp_source/frame.png b/wxPython/demo/bmp_source/frame.png new file mode 100644 index 0000000000000000000000000000000000000000..4f9be452ff2585e1a1058c42fa31f0a7ec205438 GIT binary patch literal 199 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`EX7WqAsj$Z!;#Vf3;j(8_Nh$#KhJZVM;f7tGl%-MXo9*;P@s;`&nZ3P36R#AAz$3Z_+tIu+-j-iBFxji@SSL@ vVfiEdCO_bnhLY`)>kIn!ZM-?H=D4uxfwrA98QWEWE@AL=^>bP0l+XkKpW93P literal 0 HcmV?d00001 diff --git a/wxPython/demo/bmp_source/images.png b/wxPython/demo/bmp_source/images.png new file mode 100644 index 0000000000000000000000000000000000000000..cc4a4b92120c63c0d0a64bafc1ee7e13d616d11d GIT binary patch literal 1141 zcmXw(Sx{346oyYo3<#G7B*2Y>CKY69EjsIeAXD~z}VLJ1LBMNCwZoAoxf(}(~3a~}Th;mkj$VPD!V zPxtli0N}|@Ny>1#*BiM}os;>B;5&^{z}d?IPz=Afrg71^Gs<>mmt__U%Z~9&1;EKK zJ|bXoi}(eC3;{pCqU4Ss)|qkTCULULCyq>tR(!}5TLBL@G)T|rid%ar>0vQGfn@Ry zO!v4r8B5O-M@|mbh{w(<JW{3AJY!^^U%fg5BLf4~z6g7Ypt!O8t}vw+2>V}Nw|j!xfBWIQ zu+*}&g-Q)Bp?P9S-M1RpoayWCj-92g`h z4hMh>2{6{Ky%k}(^>EL)33^qoU?&5xjWcj~zY(5KkHK8uw=jR^5QOA+gRieI(99Ni zY0w*8{>6}ULO|%fIj{lNdi}gQb9ZUk=DtDJM~Bfz04!5KjG%H*x=(?1V<3c1Xh5TW z2=oXpP$>Y*D|+*P7>efxzY5ys5PZ0^ua2|^f>Olj2hAKKOik#4ANLy4o~Yoy#}mjj zu%U{Z23^fvKuReE51KzvC}eP{uS-3Yl)OdVR@Zc@A|hr+Eaa91W7(}U2(msNMI|L# zYqCtI&isO^j9-U*ezPB?s0U-J#dZlNYj>x#xuKQx|7B($PTlamb%Xo%yTNoISpXJv z0H)Uf788Kk1c?L=iCPrwZgKE(ydPlI06;i^N+p2{*{S;3KdnrIHMmIcbJ{N@>dMFc z`!f8Fhg!^@Ez!zooBdU@?r&oOvWdow8W2|A)!$!W%DP+VWS`JE27}?uuU%+-Jk-)+ z#qYT)BAU{-R+X7aWL5mR=UT~S+|eOjoKMHZOH6d*%UE9dCA>*@>brHa)5l$gy%WeN z`UcRBNrJX-S6PeW8CSo4alJ-ovYNaS2gL%O?e$5{q@FFSw&YTm(?OXQ1zomfr>=TK^~=iN%P@jn90OyUR2ppnQoFJr z@iwd?dlGs?gz=QLqj@cQGJF|Gx2``Q`iLqN74nYeyI2=T6fXoy(E!_Sp7l|kW#^#| zhXBIzr~T;UT)j-^AG$|UC}*JM>77ao#ft~y*UGqtc~5`m-vI8;w4}>Bc(wlk7Mt~a literal 0 HcmV?d00001 diff --git a/wxPython/demo/bmp_source/inspect.png b/wxPython/demo/bmp_source/inspect.png new file mode 100644 index 0000000000000000000000000000000000000000..1dd74bea1685bd4e323814423c09d7f608b09849 GIT binary patch literal 766 zcmVS8CW+Gk?!Mq@r8TJsRP?1F#L@#FQ-+bssWj=Xn_UNUc zf^N`@SSe+OmS`rK4zGziZ=2Ii-Npa@?$=Anw9AX<={%hCf%D}ED5cm=3AVNUH-I`2 zr>=fpr=6aN<#`7T3wj7euA19r`7tV$(*9o{Ej8OKCp)JnEj^l>o|Ktd-?W)WzV~s7 z>4*2QMdh4_RKB~m;l}7cfrz~^ixXpg^SP&w$W-zG-qP*RSV;`Yoakt2a>+P3qgVU)_I~6YT@hJdM z24Kzk1Xr8Q>@SxpdtHdYzRvSTqfS3MHmDi4a4Ytm-VtG*Zhr2DxjEDR)T~edqOAZU z7E4~5Y)g?0VR#VA2^o=@0EV@7t4$1Wnv%CgrI{8N*z2Q#Pid5^RC9L4}BAp(k}v_TLJsLZc3k9$PPZORVSQ`%l{m- zBP@U+0uewRom0)xxz~@TYKDG%>+EVB1z_I_Z~%;z)dkmzFRR(%{@$!tAM3PY7k8^n z;URs|{5Wh;^I&YBWiUD@P-)T5%yXLnO6fWwWEp_I@?Jp|fU3-l6LCs~#Lr?gF9Zb# zd0z)}2(mIW8`Tzg`07*qoM6N<$g48l(R{#J2 literal 0 HcmV?d00001 diff --git a/wxPython/demo/bmp_source/layout.png b/wxPython/demo/bmp_source/layout.png new file mode 100644 index 0000000000000000000000000000000000000000..b4aaad9a45c9abbee2d47611a6963101b64a8023 GIT binary patch literal 519 zcmV+i0{H!jP)l0tqi6&npJ$x10^Z5HznC|jkhtXU{*?AXX+vSK$&lqrKo z4P`-MYRWlh-uHXoo+plGVQMtjm29&SnPTept1G}>;1qBiY)nF?X%bBWNhny_l)Z3d z&-N3@T)lWSVVldhUf%V8y7}mh3o^f*rno=*oe{IP>4|aPep(t*WGZD`whRe#WKrpOeww^A5*|8>ZEI3iJG3d@;ddSaaQQiv*3SWXm^*Pk(vkYVP=SQ%7gX+6s4|5yBQ}3^T_6XQ zZDbP)D!Ze~6zXIkQ9PYpWTZE?jfD>%Y1@{Swk5itNez`{Q)G&e7J>b9cP_BngG&!t zi|rp2nJV;T^4jymwofAMkUFri0;>ZDmaq-jpk-+0DUxkA;uk8$FcGE_?o$8&002ov JPDHLkV1i%8-68-0 literal 0 HcmV?d00001 diff --git a/wxPython/demo/bmp_source/miscellaneous.png b/wxPython/demo/bmp_source/miscellaneous.png new file mode 100644 index 0000000000000000000000000000000000000000..f06e510deac26017424d7b819a131e78ede7c5ec GIT binary patch literal 716 zcmV;-0yF)IP)7RZq3qHt@co9wD?gchVIvV|6-Opv_l zqJ?NP0xK%QLao%aZ6+IQnslc5{c~pi|LZ~m3!|cQ`JQv01MhjIQi}g6`KN(l>%4P> z@i|wo$=d%<0P99$!j_V#``D4O2r-t#HBYAZp3g4&D?oBVb!p!I(zCr*D-FH@ZAC?P zVelpic08imfs5H%>wO%RW2t2wc(6w?8Q23TR@<-7Ri1Sl(9 zTN!scbw+5Td%ZMdjGZ@E;l*o^abPG2^l*I47`{}r=*>GT?Md}*%-&pveVc;^U0@26 z$qQWTWU;eNYPyNt7l2VI(9{m`NwA~@OvRMiqm8Zc?c;m4-bwR@Giht}AZ?PlSu@E? z$l*m#%V-(;W-`*R4LFiH6_f*79)tfKx^99s+>g&j8*iAGkxj@56YA}!%3A{w1G)9~ zm+~vg&ZU+}1a!dP1xkSsXnls|ZHMP6iPk9y3-2{Qo<1*1nIOPs7|5$W)tDM1GM=v* z*WnC@;Nw?5MJz082-dY2YLhBy4>`7v{-#e8#irAO`YI7ZU~LR~BDW(o62Ki+opC~> zG_FkZ?YOVqyQrC~lv3S4eytt)SQ4A>con-mea~=@SMNLED~Rao8Yc$@DWueAA%s*) y8AGMB|Dv{62yq#Brj#;=OycjB2lbnO)xHBF10tFGV6%(>00003N8dOh1l84E~qeAXkn4sMr7G7 z1TU4hprVBYqQPiHvK!6BOJX*kg=93Ret6G&II5JA(rDFo>1=hAK2I?*KEcAwoB&Wu zU~DlUzn@NVr6jp9|*hf;tCOkVbHSy?&!=wwrDB#Umy^PDcE086N1!6lHq}&a`b2I zx2WxDl-~9l%%gk!H*S9J{t#sdps%f$rSLL7y^5pbm<23U(dCrmze0?R2H33IL+f&mm_@r3G}bmSIUSPov!C8_ydJ?mSWntx;yTk%ffBIL&B>K;_;va0 i$e}d(No^&|I=uml5si}q8v(@t0000AcJ@+I=O^EhN0+r07AmgA}NGTMfLwB_cnN5b9!4J^Q48tr3+NBh-=*ADw zjnGIJE`}inDt0R^nTiNHVq92bhJ-X|Vta1xJ!ckTW4h_HdpYpqAn>W}>VSJxIe*cZB`?HLUj8Lss zuYg1%K_ZdpTf5zE10aN;RXN7I{|DoDN#Bj|CbB3I$TB z6t!B7TCGN-(ZKUO#(-NhGgw;Vefory5+Ovd2LO)aaCmsg?(Qx^2(;D!gu`Kmq-3X3 zLD)8y5LlMg4+KF#I-Mq)&9b_>iq@J?C`3FS$8j7c?%d&JG|Ixmhy1d*h*FBd!NDG2 zSr$@C^7%ZqT8+)kO&rG|nM@LiM5s5L+|Ojt(I~ICw)(zZUYA8{jg*q*owi0iu6&G~<@ WDkA78d?YFW0000hz1LAyb_?fB`fA_h{Us3U zPaCDJSN2I%^QZk?2{+E&ZAZ=N*U$JVDv_+)!yCSmJ0Z=t<^20O3oE^@Q5~W)~oi0=@S7yJn zN}1)AV=Qg2d~)KVpd8@lU4nAJ(f&cakt2`_*bL*^002ovPDHLkV1ja*<;eg5 literal 0 HcmV?d00001 diff --git a/wxPython/demo/bmp_source/overview.png b/wxPython/demo/bmp_source/overview.png new file mode 100644 index 0000000000000000000000000000000000000000..12ec79e4e0ac23422fa057a1463049a9b68a50b2 GIT binary patch literal 655 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 Y0SIdSJYsEaB>(^b07*qoM6N<$g6XKB5dZ)H literal 0 HcmV?d00001 diff --git a/wxPython/demo/bmp_source/saveperspective.png b/wxPython/demo/bmp_source/saveperspective.png new file mode 100644 index 0000000000000000000000000000000000000000..8d8bd6fed0a1581cf873cf752fba8f379448b66b GIT binary patch literal 1139 zcmX|BdrXse6uteRwFLx(A(VFXE7DE{wX{awv~{3%Aj)fsQ=~ksP8h<76Bsh8QXW|} zETRObunBHsLm5%#q6Kj{*uY98L#$9<UBN zJ9s+)z-q~^s00FC{|PFE$SggQ5@0VA?-m2x>>|GE4{w?WjCHpa0rwJU^k+bcT0#`>M~g zgePa_2ET3+eJfc68A zRD;3rAl0Owb+KbGS`LZQo}G+L6|WdoDqdzLIq>EDZ{DnkqH~uNd&cV&3Qc3XYzs9h zd2LRyX)%ueL>dMdHM@(jOTS!Kw@^~@?l%1i$K{e!dGZG;RaIGi%oTA;d`gZkdbGqG3Av#Og%x!h(oUQNO(XkKZ!Ol>tse>6F>cD%T-H0X>%!M5{M9WECh zX7QMjma}F1ez`0W*Hjnl?)2&L&{x|5qcbKjyRl)ge+VLi+#xn)&LK|&-eP7bGpCU$#(zC z7|}{^NBj5fTJ3^s&^GgWm|h|+^uGq2=?RvO4w&q^4;xbxfQwVWXuyEU-9QrfIrGPd z)ht4)?Tx80o6X{8GHI~|Q_EDJB{PH*>Vm)J4VtT8!VBx4z(`961{=W|+AK^=>ETAl zJP7zWM1}YS_BvrG>s2biv@hUr(+V3yYdlkWpe${(O# z(gTy_1HR*r0bwja786))Ho==|wg6DszId0k?qZ}T6(i|tKZovtL1=WQnOO`9wnN}e zTAZ7P>snfwP|iIqic8z zvi6K1#;vxakgW^3AFoF()p?^Nhn7GQNd$qhNwuFY4J8}V zdkq60_Q&Fqj0?fNu$jxf<$Wym(W};i-&sY=7+#-DzW_x0Q2}rN9G|x^CCfZ$pWx`O ppm))(iyz(M@qWTKL{PSZYZ{;odBGw1Pz~`lKoT7n)w(@R{x?B%^K}3K literal 0 HcmV?d00001 diff --git a/wxPython/demo/encode_bitmaps.py b/wxPython/demo/encode_bitmaps.py index d2b5402c36..39366991d5 100644 --- a/wxPython/demo/encode_bitmaps.py +++ b/wxPython/demo/encode_bitmaps.py @@ -100,6 +100,34 @@ command_lines = [ "-a -u -n _book_green bmp_source/book_green.png images.py", "-a -u -n _book_blue bmp_source/book_blue.png images.py", + "-a -u -c bmp_source/book.png images.py", + "-a -u -c bmp_source/clipboard.png images.py", + "-a -u -c bmp_source/code.png images.py", + "-a -u -c bmp_source/core.png images.py", + "-a -u -c bmp_source/custom.png images.py", + "-a -u -c bmp_source/deleteperspective.png images.py", + "-a -u -c bmp_source/demo.png images.py", + "-a -u -c bmp_source/dialog.png images.py", + "-a -u -c bmp_source/exit.png images.py", + "-a -u -c bmp_source/expansion.png images.py", + "-a -u -c bmp_source/find.png images.py", + "-a -u -c bmp_source/findnext.png images.py", + "-a -u -c bmp_source/frame.png images.py", + "-a -u -c bmp_source/images.png images.py", + "-a -u -c bmp_source/inspect.png images.py", + "-a -u -c bmp_source/layout.png images.py", + "-a -u -c bmp_source/miscellaneous.png images.py", + "-a -u -c bmp_source/modifiedexists.png images.py", + "-a -u -c bmp_source/morecontrols.png images.py", + "-a -u -c bmp_source/moredialog.png images.py", + "-a -u -c bmp_source/overview.png images.py", + "-a -u -c bmp_source/process.png images.py", + "-a -u -c bmp_source/pyshell.png images.py", + "-a -u -c bmp_source/recent.png images.py", + "-a -u -c bmp_source/saveperspective.png images.py", + "-a -u -c bmp_source/customcontrol.png images.py", + + " -u -c bmp_source/001.png throbImages.py", "-a -u -c bmp_source/002.png throbImages.py", "-a -u -c bmp_source/003.png throbImages.py", diff --git a/wxPython/distrib/DIRLIST b/wxPython/distrib/DIRLIST index bd6d97a065..26908d2b5f 100644 --- a/wxPython/distrib/DIRLIST +++ b/wxPython/distrib/DIRLIST @@ -123,6 +123,7 @@ wxPython/wx/lib/art wxPython/wx/lib/colourchooser wxPython/wx/lib/editor wxPython/wx/lib/floatcanvas +wxPython/wx/lib/floatcanvas/Utilities wxPython/wx/lib/masked wxPython/wx/lib/mixins wxPython/wx/lib/ogl diff --git a/wxPython/distrib/README.win32.txt b/wxPython/distrib/README.win32.txt index 69f7b239c5..b502ae385f 100644 --- a/wxPython/distrib/README.win32.txt +++ b/wxPython/distrib/README.win32.txt @@ -1,7 +1,7 @@ wxPython win32 README --------------------- -The self-installer pacakge you have just installed contains the Python +The self-installer package you have just installed contains the Python extension modules, python modules and packages needed to run wxPython applications. If you selected the "Make this install be the default wxPython" option in the installer then this version will be the one diff --git a/wxPython/distrib/all/build-all b/wxPython/distrib/all/build-all index 425c9acbd5..2b117872b9 100755 --- a/wxPython/distrib/all/build-all +++ b/wxPython/distrib/all/build-all @@ -57,7 +57,10 @@ def getTasks(config_env): [config.OSX_HOST_panther, "2.3", "both"], env=config_env), Job("bigmac.24", "distrib/all/build-osx", - [config.OSX_HOST_panther, "2.4", "both"], env=config_env) + [config.OSX_HOST_panther, "2.4", "both"], env=config_env), + Job("bigmac.25", + "distrib/all/build-osx", + [config.OSX_HOST_panther, "2.5", "both"], env=config_env) ]) tigerTask = Task([ #Job("smallfry.23", @@ -88,9 +91,16 @@ def getTasks(config_env): # Job("co-mdk2006.24","distrib/all/build-rpm", ["beast", "co-mdk2006", "mdk2006", "2.4"], env=config_env), ]) - xavierTask = Task([ - Job("xavier.d", "distrib/all/build-deb", ["xavier", "/work/chroot/dapper", "dapper"], env=config_env), - Job("xavier.f", "distrib/all/build-deb", ["xavier", "/work/chroot/feisty", "feisty"], env=config_env), + cyclopsTask1 = Task([ + Job("cyclops.d", "distrib/all/build-deb", ["cyclops", "/work/chroot/dapper", "dapper"], env=config_env), + Job("cyclops.d64", "distrib/all/build-deb", ["cyclops", "/work/chroot/dapper64", "dapper64"], env=config_env), + Job("cyclops.fc6", "distrib/all/build-chrpm", + ["cyclops", "/work/chroot/fc6", "fc6", "fc6", "2.4"], env=config_env), + ]) + + cyclopsTask2 = Task([ + Job("cyclops.f", "distrib/all/build-deb", ["cyclops", "/work/chroot/feisty", "feisty"], env=config_env), + Job("cyclops.f64", "distrib/all/build-deb", ["cyclops", "/work/chroot/feisty64", "feisty64"], env=config_env), ]) buildTasks = [ #jaguarTask, @@ -98,7 +108,8 @@ def getTasks(config_env): tigerTask, beastTask1, beastTask2, - xavierTask, + cyclopsTask1, + cyclopsTask2, ] # Finalization. This is for things that must wait until all the diff --git a/wxPython/distrib/all/build-chrpm b/wxPython/distrib/all/build-chrpm new file mode 100755 index 0000000000..f8e2928186 --- /dev/null +++ b/wxPython/distrib/all/build-chrpm @@ -0,0 +1,61 @@ +#!/bin/bash +#---------------------------------------------------------------------- + +set -o errexit +#set -o xtrace + +host=$1 +chRootRoot=$2 +chRootName=$3 +reltag=$4 +pyver=$5 + + +if [ $buildansi = yes ]; then + CHARTYPE=both +else + CHARTYPE=unicode +fi + + +function TestOnline { + local host=$1 + local message=$2 + + if ping -q -c1 -w1 $host > /dev/null; then + return 0 + else + return 1 + fi +} + + + +if [ $skipdeb != yes ]; then + # We use a chroot environment on the build machine for the debian + # builds, so this build is pretty simple. Just copy the tarball + # and a build script to the build machine, and then run + # do-build-deb in the chroot. + + if TestOnline $host; then + + echo "The $host machine is online, build continuing..." + + echo "Copying source files and build script..." + ssh root@$host "mkdir -p $chRootRoot/$LINUX_BUILD && rm -rf $chRootRoot/$LINUX_BUILD/*" + scp $STAGING_DIR/wxPython-src* distrib/all/do-build-rpm $STAGING_DIR/wxPython.spec \ + root@$host:$chRootRoot/$LINUX_BUILD + + ssh root@$host "dchroot --chroot $chRootName --directory $LINUX_BUILD \"./do-build-rpm $reltag $skipclean $VERSION $pyver $CHARTYPE\"" + + echo "Fetching the results..." + ssh root@$host "rm $chRootRoot/$LINUX_BUILD/do-build-rpm" + scp "root@$host:$chRootRoot/$LINUX_BUILD/wxPython*.i[0-9]86.rpm" $STAGING_DIR + ssh root@$host "rm $chRootRoot/$LINUX_BUILD/wxPython*.i[0-9]86.rpm" + echo "Done!" + else + echo "The $host machine is **OFFLINE**, skipping the binary RPM build." + exit 0 + fi +fi + diff --git a/wxPython/distrib/make_installer.py b/wxPython/distrib/make_installer.py index 7811ed1432..290fdfa84e 100644 --- a/wxPython/distrib/make_installer.py +++ b/wxPython/distrib/make_installer.py @@ -121,6 +121,7 @@ Source: "wx\lib\mixins\*.py"; DestDir: "{app}\%(PKGDIR)s\wx\lib Source: "wx\lib\masked\*.py"; DestDir: "{app}\%(PKGDIR)s\wx\lib\masked"; Components: core Source: "wx\lib\ogl\*.py"; DestDir: "{app}\%(PKGDIR)s\wx\lib\ogl"; Components: core Source: "wx\lib\floatcanvas\*.py"; DestDir: "{app}\%(PKGDIR)s\wx\lib\floatcanvas"; Components: core +Source: "wx\lib\floatcanvas\Utilities\*.py"; DestDir: "{app}\%(PKGDIR)s\wx\lib\floatcanvas\Utilities"; Components: core Source: "wx\py\*.py"; DestDir: "{app}\%(PKGDIR)s\wx\py"; Components: core Source: "wx\py\*.txt"; DestDir: "{app}\%(PKGDIR)s\wx\py"; Components: core Source: "wx\py\*.ico"; DestDir: "{app}\%(PKGDIR)s\wx\py"; Components: core @@ -216,6 +217,8 @@ Type: files; Name: "{app}\%(PKGDIR)s\wx\lib\ogl\*.pyc"; Type: files; Name: "{app}\%(PKGDIR)s\wx\lib\ogl\*.pyo"; Type: files; Name: "{app}\%(PKGDIR)s\wx\lib\floatcanvas\*.pyc"; Type: files; Name: "{app}\%(PKGDIR)s\wx\lib\floatcanvas\*.pyo"; +Type: files; Name: "{app}\%(PKGDIR)s\wx\lib\floatcanvas\Utilities\*.pyc"; +Type: files; Name: "{app}\%(PKGDIR)s\wx\lib\floatcanvas\Utilities\*.pyo"; Type: files; Name: "{app}\%(PKGDIR)s\wx\py\*.pyc"; Type: files; Name: "{app}\%(PKGDIR)s\wx\py\*.pyo"; diff --git a/wxPython/docs/CHANGES.txt b/wxPython/docs/CHANGES.txt index e8fd57a3d2..2227a3c7fe 100644 --- a/wxPython/docs/CHANGES.txt +++ b/wxPython/docs/CHANGES.txt @@ -86,6 +86,10 @@ did rely on the old behaviour you will have to update your code to set the minimal sizes of the sizer items to be in the same proportion as the items proportions to return to the old behaviour. +Added support for toolbar buttons with dropdown menus. + + + diff --git a/wxPython/misc/image.png b/wxPython/misc/image.png deleted file mode 100644 index 5a4ce74b8970c81980eea62df1c214d9ffdf3a40..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17905 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\n (width, height, descent, externalLeading)", "Get the width, height, decent and leading of the text using the current or specified font. Only works for single line strings.", "", @@ -641,7 +641,7 @@ current or specified font. Only works for single line strings.", "", DocDeclAStr( void, GetMultiLineTextExtent(const wxString& text, wxCoord *OUTPUT, wxCoord *OUTPUT, wxCoord *OUTPUT, - wxFont *font = NULL), + const wxFont *font = NULL), "GetMultiLineTextExtent(wxString string, Font font=None) ->\n (width, height, lineHeight)", "Get the width, height, and line height of the text using the current or specified font. Works for single as well as multi-line diff --git a/wxPython/src/_defs.i b/wxPython/src/_defs.i index a7eab282d9..1eb46b0cf2 100644 --- a/wxPython/src/_defs.i +++ b/wxPython/src/_defs.i @@ -1162,6 +1162,7 @@ enum wxItemKind wxITEM_NORMAL, wxITEM_CHECK, wxITEM_RADIO, + wxITEM_DROPDOWN, wxITEM_MAX }; diff --git a/wxPython/src/_event.i b/wxPython/src/_event.i index 5f7c7140b4..6a2333ddfe 100644 --- a/wxPython/src/_event.i +++ b/wxPython/src/_event.i @@ -61,6 +61,7 @@ wxEventType wxNewEventType(); %constant wxEventType wxEVT_COMMAND_COMBOBOX_SELECTED; %constant wxEventType wxEVT_COMMAND_TOOL_RCLICKED; %constant wxEventType wxEVT_COMMAND_TOOL_ENTER; +%constant wxEventType wxEVT_COMMAND_TOOL_DROPDOWN_CLICKED; // Mouse event types %constant wxEventType wxEVT_LEFT_DOWN; @@ -354,6 +355,7 @@ EVT_TOOL_RANGE = wx.PyEventBinder( wxEVT_COMMAND_TOOL_CLICKED, 2) EVT_TOOL_RCLICKED = wx.PyEventBinder( wxEVT_COMMAND_TOOL_RCLICKED, 1) EVT_TOOL_RCLICKED_RANGE = wx.PyEventBinder( wxEVT_COMMAND_TOOL_RCLICKED, 2) EVT_TOOL_ENTER = wx.PyEventBinder( wxEVT_COMMAND_TOOL_ENTER, 1) +EVT_TOOL_DROPDOWN = wx.PyEventBinder( wxEVT_COMMAND_TOOL_DROPDOWN_CLICKED, 1) EVT_CHECKLISTBOX = wx.PyEventBinder( wxEVT_COMMAND_CHECKLISTBOX_TOGGLED, 1) diff --git a/wxPython/src/_evtloop.i b/wxPython/src/_evtloop.i index de95253c70..e699c90c8e 100644 --- a/wxPython/src/_evtloop.i +++ b/wxPython/src/_evtloop.i @@ -23,15 +23,17 @@ #if 0 // #ifdef __WXMAC__ // A dummy class that raises an exception if used... -class wxEventLoop +class wxEventLoopBase { public: - wxEventLoop() { wxPyRaiseNotImplemented(); } + wxEventLoopBase() { wxPyRaiseNotImplemented(); } + bool IsOk() const { return false; } int Run() { return 0; } void Exit(int rc = 0) {} bool Pending() const { return false; } bool Dispatch() { return false; } bool IsRunning() const { return false; } + void WakeUp() {} static wxEventLoop *GetActive() { wxPyRaiseNotImplemented(); return NULL; } static void SetActive(wxEventLoop* loop) { wxPyRaiseNotImplemented(); } }; @@ -43,12 +45,16 @@ public: #endif %} -class wxEventLoop +class wxEventLoopBase { public: - wxEventLoop(); + wxEventLoopBase(); virtual ~wxEventLoop(); + // use this to check whether the event loop was successfully created before + // using it + virtual bool IsOk() const; + // start the event loop, return the exit code when it is finished virtual int Run(); @@ -64,6 +70,8 @@ public: // is the event loop running now? virtual bool IsRunning() const; + virtual void WakeUp(); + // return currently active (running) event loop, may be NULL static wxEventLoop *GetActive(); @@ -72,6 +80,34 @@ public: }; +class wxEventLoopManual : public wxEventLoopBase +{ +public: + wxEventLoopManual(); +}; + + +class wxGUIEventLoop : public wxEventLoopBase +{ +public: + wxGUIEventLoop(); +}; + + +%pythoncode { + class EventLoop(GUIEventLoop): + """Class using the old name for compatibility.""" + pass +} + + +class wxModalEventLoop : public wxGUIEventLoop +{ +public: + wxModalEventLoop(wxWindow *winModal) +}; + + // This object sets the wxEventLoop given to the ctor as the currently active // one and unsets it in its dtor, this is especially useful in presence of diff --git a/wxPython/src/_functions.i b/wxPython/src/_functions.i index 481f99d87f..baf74525ac 100644 --- a/wxPython/src/_functions.i +++ b/wxPython/src/_functions.i @@ -337,8 +337,14 @@ MustHaveApp(wxGetTopLevelParent); wxWindow* wxGetTopLevelParent(wxWindow *win); +// flags for wxLaunchDefaultBrowser +enum +{ + wxBROWSER_NEW_WINDOW = 1 +}; + DocDeclStr( - bool , wxLaunchDefaultBrowser(const wxString& url), + bool , wxLaunchDefaultBrowser(const wxString& url, int flags = 0), "Launches the user's default browser and tells it to open the location at ``url``. Returns ``True`` if the application was successfully launched.", ""); diff --git a/wxPython/src/_pywindows.i b/wxPython/src/_pywindows.i index 3dfc58acab..5ec3889dc3 100644 --- a/wxPython/src/_pywindows.i +++ b/wxPython/src/_pywindows.i @@ -240,7 +240,7 @@ public: wxPyPanel(wxWindow* parent, const wxWindowID id, const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, - long style = 0, + long style = wxTAB_TRAVERSAL | wxNO_BORDER, const wxString& name = wxPyPanelNameStr) : wxPanel(parent, id, pos, size, style, name) {} @@ -334,7 +334,7 @@ public: wxPyPanel(wxWindow* parent, const wxWindowID id=-1, const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, - long style = 0, + long style = wxTAB_TRAVERSAL | wxNO_BORDER, const wxString& name = wxPyPanelNameStr); %RenameCtor(PrePyPanel, wxPyPanel()); @@ -415,7 +415,7 @@ public: wxPyScrolledWindow(wxWindow* parent, const wxWindowID id, const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, - long style = 0, + long style = wxHSCROLL | wxVSCROLL, const wxString& name = wxPyPanelNameStr) : wxScrolledWindow(parent, id, pos, size, style, name) {} @@ -508,7 +508,7 @@ public: wxPyScrolledWindow(wxWindow* parent, const wxWindowID id=-1, const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, - long style = 0, + long style = wxHSCROLL | wxVSCROLL, const wxString& name = wxPyPanelNameStr); %RenameCtor(PrePyScrolledWindow, wxPyScrolledWindow()); diff --git a/wxPython/src/_sound.i b/wxPython/src/_sound.i index bed230c51c..8bd1d79abd 100644 --- a/wxPython/src/_sound.i +++ b/wxPython/src/_sound.i @@ -33,7 +33,15 @@ enum wxSoundFlags %{ #if !wxUSE_SOUND -// A C++ stub class for wxWave for platforms that don't have it. +// A C++ stub class for wxSound for platforms that don't have it. + +enum wxSoundFlags +{ + wxSOUND_SYNC, + wxSOUND_ASYNC, + wxSOUND_LOOP +}; + class wxSound : public wxObject { public: diff --git a/wxPython/src/_timer.i b/wxPython/src/_timer.i index 3f227d5d37..74815e8607 100644 --- a/wxPython/src/_timer.i +++ b/wxPython/src/_timer.i @@ -140,8 +140,10 @@ class wxTimerEvent : public wxEvent public: wxTimerEvent(int timerid = 0, int interval = 0); int GetInterval() const; - + wxTimer& GetTimer() const; + %property(Interval, GetInterval, doc="See `GetInterval`"); + %property(Timer, GetTimer); }; diff --git a/wxPython/src/_toolbar.i b/wxPython/src/_toolbar.i index 629b1b2c36..f027e49a7c 100644 --- a/wxPython/src/_toolbar.i +++ b/wxPython/src/_toolbar.i @@ -102,6 +102,11 @@ public: void Detach(); void Attach(wxToolBarBase *tbar); + // these methods are only for tools of wxITEM_DROPDOWN kind (but even such + // tools can have a NULL associated menu) + void SetDropdownMenu(wxMenu *menu); + wxMenu *GetDropdownMenu() const; + //wxObject *GetClientData(); %extend { // convert the ClientData back to a PyObject @@ -404,6 +409,10 @@ public: size_t GetToolsCount() const; + // Set dropdown menu + bool SetDropdownMenu(int toolid, wxMenu *menu); + + %property(Margins, GetMargins, SetMargins, doc="See `GetMargins` and `SetMargins`"); %property(MaxCols, GetMaxCols, doc="See `GetMaxCols`"); %property(MaxRows, GetMaxRows, doc="See `GetMaxRows`"); diff --git a/wxPython/src/_vscroll.i b/wxPython/src/_vscroll.i index 512f817f0e..fb623fc0c0 100644 --- a/wxPython/src/_vscroll.i +++ b/wxPython/src/_vscroll.i @@ -110,9 +110,6 @@ public: virtual void RefreshRow(size_t row); virtual void RefreshRows(size_t from, size_t to); - virtual int HitTest(wxCoord y) const; - - size_t GetRowCount() const; size_t GetVisibleRowsBegin() const; size_t GetVisibleRowsEnd() const; @@ -144,8 +141,6 @@ public: virtual void RefreshColumn(size_t column); virtual void RefreshColumns(size_t from, size_t to); - virtual int HitTest(wxCoord x) const; - size_t GetColumnCount() const; size_t GetVisibleColumnsBegin() const; @@ -212,8 +207,8 @@ public: const wxPosition& to); // Override wxPanel::HitTest to use our version -// virtual wxPosition HitTest(wxCoord x, wxCoord y) const; - virtual wxPosition HitTest(const wxPoint &pos) const; +// wxPosition VirtualHitTest(wxCoord x, wxCoord y) const; + wxPosition VirtualHitTest(const wxPoint &pos) const; // replacement implementation of wxWindow::Layout virtual method. To // properly forward calls to wxWindow::Layout use @@ -508,7 +503,6 @@ public: const wxString& name = wxPyPanelNameStr); - int HitTest(const wxPoint& pt) const; wxCoord GetColumnsWidth(size_t columnMin, size_t columnMax) const; wxCoord EstimateTotalWidth() const; }; @@ -607,8 +601,6 @@ public: const wxString& name = wxPyPanelNameStr); - wxPosition HitTest(const wxPoint& pt) const; - wxCoord GetRowsHeight(size_t lineMin, size_t lineMax) const; wxCoord EstimateTotalHeight() const; diff --git a/wxPython/src/_window.i b/wxPython/src/_window.i index 2c3d50a741..ab8acd6b55 100644 --- a/wxPython/src/_window.i +++ b/wxPython/src/_window.i @@ -1114,7 +1114,7 @@ handler is handed off to the next one in the chain.", ""); void , PushEventHandler( wxEvtHandler *handler ), "Pushes this event handler onto the event handler stack for the window. An event handler is an object that is capable of processing the events -sent to a window. (In other words, is able to dispatch the events to +sent to a window. (In other words, is able to dispatch the events to a handler function.) By default, the window is its own event handler, but an application may wish to substitute another, for example to allow central implementation of event-handling for a variety of diff --git a/wxPython/src/aui.i b/wxPython/src/aui.i index cf177c9609..815e8f0b4e 100755 --- a/wxPython/src/aui.i +++ b/wxPython/src/aui.i @@ -58,7 +58,7 @@ interface: **Usage** The following example shows a simple implementation that utilizes -`wx.aui.FrameManager` to manage three text controls in a frame window:: +`wx.aui.AuiManager` to manage three text controls in a frame window:: import wx import wx.aui @@ -170,7 +170,7 @@ The following example shows a simple implementation that utilizes const wxPaneInfo& pane_info, const wxPoint& drop_pos); -// A typemap for the return value of wxFrameManager::GetAllPanes +// A typemap for the return value of wxAuiManager::GetAllPanes %typemap(out) wxAuiPaneInfoArray& { $result = PyList_New(0); for (size_t i=0; i < $1->GetCount(); i++) { @@ -179,6 +179,7 @@ The following example shows a simple implementation that utilizes } } +//%ignore wxAuiManager::~wxAuiManager; %nokwargs wxAuiTabContainer::SetActivePage; @@ -234,7 +235,7 @@ The following example shows a simple implementation that utilizes #undef wxColor //--------------------------------------------------------------------------- -// Methods to inject into the FrameManager class that will sort out calls to +// Methods to inject into the AuiManager class that will sort out calls to // the overloaded versions of GetPane and AddPane %extend wxAuiManager { @@ -247,7 +248,7 @@ The following example shows a simple implementation that utilizes widget reference or by pane name, which acts as a unique id for a window pane. The returned `PaneInfo` object may then be modified to change a pane's look, state or position. After one - or more modifications to the `PaneInfo`, `FrameManager.Update` + or more modifications to the `PaneInfo`, `AuiManager.Update` should be called to realize the changes to the user interface. If the lookup failed (meaning the pane could not be found in diff --git a/wxPython/src/gtk/_core.py b/wxPython/src/gtk/_core.py index 9c7fe0e164..71ef20270f 100644 --- a/wxPython/src/gtk/_core.py +++ b/wxPython/src/gtk/_core.py @@ -9360,7 +9360,7 @@ class Window(EvtHandler): Pushes this event handler onto the event handler stack for the window. An event handler is an object that is capable of processing the events - sent to a window. (In other words, is able to dispatch the events to + sent to a window. (In other words, is able to dispatch the events to a handler function.) By default, the window is its own event handler, but an application may wish to substitute another, for example to allow central implementation of event-handling for a variety of diff --git a/wxPython/src/gtk/_misc_wrap.cpp b/wxPython/src/gtk/_misc_wrap.cpp index acd06368b2..9cc7cb8f7b 100644 --- a/wxPython/src/gtk/_misc_wrap.cpp +++ b/wxPython/src/gtk/_misc_wrap.cpp @@ -3289,6 +3289,14 @@ public: #if !wxUSE_SOUND // A C++ stub class for wxWave for platforms that don't have it. + +enum wxSoundFlags +{ + wxSOUND_SYNC, + wxSOUND_ASYNC, + wxSOUND_LOOP +}; + class wxSound : public wxObject { public: diff --git a/wxPython/src/helpers.cpp b/wxPython/src/helpers.cpp index 3e7a8e9c9c..a99061c732 100644 --- a/wxPython/src/helpers.cpp +++ b/wxPython/src/helpers.cpp @@ -842,6 +842,7 @@ void wxPyOORClientData_dtor(wxPyOORClientData* self) { //Py_INCREF(deadObjectClass); Py_DECREF(klass); Py_DECREF(name); + Py_DECREF(dict); } } diff --git a/wxPython/src/mac/_core.py b/wxPython/src/mac/_core.py index 9c7fe0e164..71ef20270f 100644 --- a/wxPython/src/mac/_core.py +++ b/wxPython/src/mac/_core.py @@ -9360,7 +9360,7 @@ class Window(EvtHandler): Pushes this event handler onto the event handler stack for the window. An event handler is an object that is capable of processing the events - sent to a window. (In other words, is able to dispatch the events to + sent to a window. (In other words, is able to dispatch the events to a handler function.) By default, the window is its own event handler, but an application may wish to substitute another, for example to allow central implementation of event-handling for a variety of diff --git a/wxPython/src/msw/_core.py b/wxPython/src/msw/_core.py index 7d7963abfb..ef7641dfd2 100644 --- a/wxPython/src/msw/_core.py +++ b/wxPython/src/msw/_core.py @@ -9360,7 +9360,7 @@ class Window(EvtHandler): Pushes this event handler onto the event handler stack for the window. An event handler is an object that is capable of processing the events - sent to a window. (In other words, is able to dispatch the events to + sent to a window. (In other words, is able to dispatch the events to a handler function.) By default, the window is its own event handler, but an application may wish to substitute another, for example to allow central implementation of event-handling for a variety of diff --git a/wxPython/tests/TreeMixinTest.py b/wxPython/tests/TreeMixinTest.py index 1153f0243f..ac7909836e 100644 --- a/wxPython/tests/TreeMixinTest.py +++ b/wxPython/tests/TreeMixinTest.py @@ -1,4 +1,9 @@ -import wx, wx.gizmos, wx.lib.customtreectrl, unittest, treemixin +import wx, wx.gizmos, wx.lib.customtreectrl, unittest +try: + import treemixin +except ImportError: + from wx.lib.mixins import treemixin + # VirtualTree tests diff --git a/wxPython/tests/image.png b/wxPython/tests/image.png new file mode 100644 index 0000000000000000000000000000000000000000..fc5db22a70a50f6c13686389e17e93154ff14867 GIT binary patch literal 17904 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%0A3x4oe!O98JX{yoMpu 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 literal 0 HcmV?d00001 diff --git a/wxPython/tests/test_dclick.py b/wxPython/tests/test_dclick.py new file mode 100644 index 0000000000..639086c960 --- /dev/null +++ b/wxPython/tests/test_dclick.py @@ -0,0 +1,28 @@ +import wx + +def OnClick(evt): + print 'Click' + +def OnDClick(evt): + print 'DClick' + +def OnMouse(evt): + if evt.LeftDClick(): + print 'DClick' + elif evt.LeftDown(): + print 'Click' + +app = wx.App(redirect=False) +frame = wx.Frame(None, title="Test mouse clicks") +panel = wx.Panel(frame) + +if True: + # try separate + panel.Bind(wx.EVT_LEFT_DOWN, OnClick) + panel.Bind(wx.EVT_LEFT_DCLICK, OnDClick) +else: + # or together + panel.Bind(wx.EVT_MOUSE_EVENTS, OnMouse) + +frame.Show() +app.MainLoop() diff --git a/wxPython/tests/test_transparentFrame.py b/wxPython/tests/test_transparentFrame.py new file mode 100644 index 0000000000..7c785b5055 --- /dev/null +++ b/wxPython/tests/test_transparentFrame.py @@ -0,0 +1,71 @@ +import wx + +class Frame(wx.Frame): + def __init__(self): + wx.Frame.__init__(self, None, title="Am I transparent?") + self.amount = 255 + self.delta = -3 + + p = wx.Panel(self) + self.st = wx.StaticText(p, -1, str(self.amount), (25,25)) + self.st.SetFont(wx.Font(18, wx.SWISS, wx.NORMAL, wx.NORMAL)) + + self.timer = wx.Timer(self) + self.timer.Start(25) + self.Bind(wx.EVT_TIMER, self.AlphaCycle) + + self.Bind(wx.EVT_CLOSE, self.OnCloseWindow) + + + def OnCloseWindow(self, evt): + self.timer.Stop() + del self.timer + self.Destroy() + + + def AlphaCycle(self, evt): + self.amount += self.delta + if self.amount == 0 or self.amount == 255: + self.delta = -self.delta + self.st.SetLabel(str(self.amount)) + + # Note that we no longer need to use ctypes or win32api to + # make transparent windows, however I'm not removing the + # MakeTransparent code from this sample as it may be helpful + # for somebody someday. + #self.MakeTransparent(self.amount) + + # Instead we'll just call the SetTransparent method + self.SetTransparent(self.amount) + + + def MakeTransparent(self, amount): + hwnd = self.GetHandle() + try: + import ctypes + _winlib = ctypes.windll.user32 + style = _winlib.GetWindowLongA(hwnd, 0xffffffecL) + style |= 0x00080000 + _winlib.SetWindowLongA(hwnd, 0xffffffecL, style) + _winlib.SetLayeredWindowAttributes(hwnd, 0, amount, 2) + + except ImportError: + import win32api, win32con, winxpgui + _winlib = win32api.LoadLibrary("user32") + pSetLayeredWindowAttributes = win32api.GetProcAddress( + _winlib, "SetLayeredWindowAttributes") + if pSetLayeredWindowAttributes == None: + return + exstyle = win32api.GetWindowLong(hwnd, win32con.GWL_EXSTYLE) + if 0 == (exstyle & 0x80000): + win32api.SetWindowLong(hwnd, + win32con.GWL_EXSTYLE, + exstyle | 0x80000) + winxpgui.SetLayeredWindowAttributes(hwnd, 0, amount, 2) + + + +app = wx.App(False) +frm = Frame() +frm.Show() +app.MainLoop() diff --git a/wxPython/misc/widgetLayoutTest.py b/wxPython/tests/test_widgetLayout.py similarity index 94% rename from wxPython/misc/widgetLayoutTest.py rename to wxPython/tests/test_widgetLayout.py index 21a61861ca..8188e8ddf5 100644 --- a/wxPython/misc/widgetLayoutTest.py +++ b/wxPython/tests/test_widgetLayout.py @@ -14,6 +14,9 @@ print "wx.VERSION_STRING = ", wx.VERSION_STRING print "pid:", os.getpid() ##raw_input("Press Enter...") +testImage = os.path.join(os.path.dirname(sys.argv[0]), 'image.png') + + class LayoutTestFrame(wx.Frame): def __init__(self): wx.Frame.__init__(self, None, -1, "Widget Layout Tester") @@ -148,8 +151,7 @@ class LayoutTestFrame(wx.Frame): Load and eval a list of lists from a file, load the contents into self.testHistory """ - fname = os.path.join(os.path.dirname(sys.argv[0]), - 'widgetLayoutTest.cfg') + fname = os.path.join(os.path.dirname(sys.argv[0]), 'widgets.cfg') try: self.history = eval(open(fname).read()) except: @@ -171,8 +173,7 @@ class LayoutTestFrame(wx.Frame): def OnSaveHistory(self, evt): if self.needSaved: - fname = os.path.join(os.path.dirname(sys.argv[0]), - 'widgetLayoutTest.cfg') + fname = os.path.join(os.path.dirname(sys.argv[0]), 'widgets.cfg') f = open(fname, 'wb') f.write('[\n') for item in self.history: @@ -233,7 +234,7 @@ class LayoutTestFrame(wx.Frame): def OnHistoryActivate(self, evt): - btn = self.testHistory.GetParent().GetDefaultItem() + btn = self.testHistory.GetTopLevelParent().GetDefaultItem() if btn is not None: e = wx.CommandEvent(wx.wxEVT_COMMAND_BUTTON_CLICKED, btn.GetId()) btn.Command(e) @@ -408,8 +409,7 @@ class SizeInfoPane(wx.Panel): self._size = wx.TextCtrl(self, -1, "", style=wx.TE_READONLY) self._minsize = wx.TextCtrl(self, -1, "", style=wx.TE_READONLY) self._bestsize = wx.TextCtrl(self, -1, "", style=wx.TE_READONLY) - self._adjbstsize = wx.TextCtrl(self, -1, "", style=wx.TE_READONLY) - self._bestfit = wx.TextCtrl(self, -1, "", style=wx.TE_READONLY) + self._effmin = wx.TextCtrl(self, -1, "", style=wx.TE_READONLY) # setup the layout fgs = wx.FlexGridSizer(2, 2, 5, 5) @@ -427,13 +427,9 @@ class SizeInfoPane(wx.Panel): 0, wx.ALIGN_RIGHT|wx.ALIGN_CENTER_VERTICAL) fgs.Add(self._bestsize, 0, wx.EXPAND) - fgs.Add(wx.StaticText(self, -1, "AdjustedBestSize:"), - 0, wx.ALIGN_RIGHT|wx.ALIGN_CENTER_VERTICAL) - fgs.Add(self._adjbstsize, 0, wx.EXPAND) - - fgs.Add(wx.StaticText(self, -1, "BestFittingSize:"), + fgs.Add(wx.StaticText(self, -1, "EffectiveMinSize:"), 0, wx.ALIGN_RIGHT|wx.ALIGN_CENTER_VERTICAL) - fgs.Add(self._bestfit, 0, wx.EXPAND) + fgs.Add(self._effmin, 0, wx.EXPAND) sbs = wx.StaticBoxSizer(sb, wx.VERTICAL) sbs.Add(fgs, 0, wx.EXPAND|wx.ALL, 4) @@ -445,15 +441,13 @@ class SizeInfoPane(wx.Panel): self._size.SetValue( str(win.GetSize()) ) self._minsize.SetValue( str(win.GetMinSize()) ) self._bestsize.SetValue( str(win.GetBestSize()) ) - self._adjbstsize.SetValue( str(win.GetAdjustedBestSize()) ) - self._bestfit.SetValue( str(win.GetEffectiveMinSize()) ) + self._effmin.SetValue( str(win.GetEffectiveMinSize()) ) def Clear(self): self._size.SetValue("") self._minsize.SetValue("") self._bestsize.SetValue("") - self._adjbstsize.SetValue("") - self._bestfit.SetValue("") + self._effmin.SetValue("") diff --git a/wxPython/misc/widgetLayoutTest.cfg b/wxPython/tests/widgets.cfg similarity index 96% rename from wxPython/misc/widgetLayoutTest.cfg rename to wxPython/tests/widgets.cfg index fa9661b03e..22900c714f 100644 --- a/wxPython/misc/widgetLayoutTest.cfg +++ b/wxPython/tests/widgets.cfg @@ -1,11 +1,12 @@ [ ['wx', 'BitmapButton', '-1, wx.Bitmap("image.png")', ''], -['wx', 'Button', '-1, "normal"', ''], -['wx', 'Button', '-1, "with a longer, longer label"', ''], ['wx', 'Button', '-1, "default"', 'w.SetDefault()'], ['wx', 'Button', '-1, "larger font"', 'w.SetFont(wx.Font(20, wx.SWISS, wx.NORMAL, wx.NORMAL))\n\n\n'], -['wx', 'CheckBox', '-1, "checkbox"', ''], +['wx', 'Button', '-1, "normal"', ''], +['wx', 'Button', '-1, "with a longer, longer label"', ''], +['wx', 'Button', 'wx.ID_SAVE', ''], ['wx', 'CheckBox', '-1, "checkbox with longer label"', ''], +['wx', 'CheckBox', '-1, "checkbox"', ''], ['wx', 'CheckListBox', '-1, size=(100,-1), choices="one two three four five six seven eight".split()', ''], ['wx', 'Choice', '-1, choices="one two three four five six seven eight".split()', ''], ['wx', 'Choice', '-1, size=(50,-1), choices="one two three four five six seven eight".split()', ''], @@ -19,10 +20,10 @@ ['wx', 'ListCtrl', 'style=wx.LC_REPORT, size=(100,100)', 'w.InsertColumn(0, "Col1")\nw.InsertStringItem(0, "Item 0")\nw.InsertStringItem(0, "Item 1")\n#w.SetSizeHints((200,100))'], ['wx', 'Notebook', '', 'p = wx.Panel(w)\np.SetMinSize((150,150))\nw.AddPage(p, "test")\n'], ['wx', 'Panel', 'size=(150,150)', ''], -['wx', 'RadioBox', '-1, "label", majorDimension=2, choices="one two three four five six seven eight".split()', ''], -['wx', 'RadioBox', '-1, "label", style=wx.RA_VERTICAL, majorDimension=2, choices="one two three four five six seven eight".split()', ''], ['wx', 'RadioBox', '-1, "label", choices="one two three four".split()', ''], +['wx', 'RadioBox', '-1, "label", majorDimension=2, choices="one two three four five six seven eight".split()', ''], ['wx', 'RadioBox', '-1, "label", style=wx.RA_VERTICAL, choices="one two three four".split()', ''], +['wx', 'RadioBox', '-1, "label", style=wx.RA_VERTICAL, majorDimension=2, choices="one two three four five six seven eight".split()', ''], ['wx', 'RadioButton', '-1, "radio button"', ''], ['wx', 'ScrollBar', '', ''], ['wx', 'ScrollBar', 'style=wx.SB_VERTICAL', ''], @@ -36,7 +37,8 @@ ['wx', 'SpinButton', 'style=wx.SP_VERTICAL', ''], ['wx', 'SpinCtrl', '', ''], ['wx', 'SpinCtrl', 'size=(50, -1)', ''], -['wx', 'StaticBitmap', '-1, wx.Bitmap("image.png")', ''], +['wx', 'StaticBitmap', '-1, wx.Bitmap(testImage)', ''], +['wx', 'StaticBitmap', '-1, wx.Bitmap(testImage), style=wx.SUNKEN_BORDER', ''], ['wx', 'StaticBox', '-1, "a longer label"', ''], ['wx', 'StaticBox', '-1, "label"', ''], ['wx', 'StaticBox', '-1, "with a size", size=(100,75)', ''], @@ -51,10 +53,9 @@ ['wx', 'TextCtrl', '-1, "some\\ndefault text\\n", size=(200, -1), style=wx.TE_MULTILINE', 'w.AppendText("Here is some more text\\n")'], ['wx', 'TreeCtrl', '', ''], ['wx.calendar', 'CalendarCtrl', '-1', ''], -['wx.calendar', 'CalendarCtrl', '-1, style=wx.calendar.CAL_SEQUENTIAL_MONTH_SELECTION', ''], ['wx.calendar', 'CalendarCtrl', '-1', 'w.SetFont(wx.Font(12, wx.SWISS, wx.NORMAL, wx.NORMAL))\n'], -['wx.lib.stattext', 'GenStaticText', '-1, "New font"', 'f = wx.Font(18, wx.SWISS, wx.NORMAL, wx.BOLD)\nw.SetFont(f)\n'], +['wx.calendar', 'CalendarCtrl', '-1, style=wx.calendar.CAL_SEQUENTIAL_MONTH_SELECTION', ''], ['wx.grid', 'Grid', '', 'w.CreateGrid(5,5)\n'], ['wx.grid', 'Grid', 'size=(400,200)', 'w.CreateGrid(100,50)\n'], -['wx', 'Button', 'wx.ID_SAVE', ''], +['wx.lib.stattext', 'GenStaticText', '-1, "New font"', 'f = wx.Font(18, wx.SWISS, wx.NORMAL, wx.BOLD)\nw.SetFont(f)\n'], ] diff --git a/wxPython/wx/lib/customtreectrl.py b/wxPython/wx/lib/customtreectrl.py index 817fa01b54..a33b8574ce 100644 --- a/wxPython/wx/lib/customtreectrl.py +++ b/wxPython/wx/lib/customtreectrl.py @@ -1766,7 +1766,7 @@ class CustomTreeCtrl(wx.PyScrolledWindow): TR_NO_LINES # don't draw lines at all TR_LINES_AT_ROOT # connect top-level nodes TR_TWIST_BUTTONS # draw mac-like twist buttons - TR_SINGLE # single selection mode + TR_SINGLE # single selection mode TR_MULTIPLE # can select multiple items TR_EXTENDED # todo: allow extended selection TR_HAS_VARIABLE_ROW_HEIGHT # allows rows to have variable height @@ -1870,12 +1870,13 @@ class CustomTreeCtrl(wx.PyScrolledWindow): self._vistaselection = False # Connection lines style + grey = (160,160,160) if wx.Platform != "__WXMAC__": - self._dottedPen = wx.Pen("grey", 1, wx.USER_DASH) + self._dottedPen = wx.Pen(grey, 1, wx.USER_DASH) self._dottedPen.SetDashes([1,1]) self._dottedPen.SetCap(wx.CAP_BUTT) else: - self._dottedPen = wx.Pen("grey", 1) + self._dottedPen = wx.Pen(grey, 1) # Pen Used To Draw The Border Around Selected Items self._borderPen = wx.BLACK_PEN @@ -1893,8 +1894,6 @@ class CustomTreeCtrl(wx.PyScrolledWindow): if major < 10: style |= TR_ROW_LINES - self._windowStyle = style - # Create the default check image list self.SetImageListCheck(13, 13) @@ -2169,13 +2168,13 @@ class CustomTreeCtrl(wx.PyScrolledWindow): dc = wx.ClientDC(self) self.RefreshLine(item) - if self._windowStyle & TR_AUTO_CHECK_CHILD: + if self.HasFlag(TR_AUTO_CHECK_CHILD): ischeck = self.IsItemChecked(item) self.AutoCheckChild(item, ischeck) - if self._windowStyle & TR_AUTO_CHECK_PARENT: + if self.HasFlag(TR_AUTO_CHECK_PARENT): ischeck = self.IsItemChecked(item) self.AutoCheckParent(item, ischeck) - elif self._windowStyle & TR_AUTO_TOGGLE_CHILD: + elif self.HasFlag(TR_AUTO_TOGGLE_CHILD): self.AutoToggleChild(item) e = TreeEvent(wxEVT_TREE_ITEM_CHECKED, self.GetId()) @@ -2310,12 +2309,6 @@ class CustomTreeCtrl(wx.PyScrolledWindow): self._dirty = True - def HasFlag(self, flag): - """Returns whether CustomTreeCtrl has a flag.""" - - return self._windowStyle & flag - - def HasChildren(self, item): """Returns whether an item has children or not.""" @@ -2349,19 +2342,19 @@ class CustomTreeCtrl(wx.PyScrolledWindow): # want to update the inherited styles, but right now # none of the parents has updatable styles - if self._windowStyle & TR_MULTIPLE and not (styles & TR_MULTIPLE): + if self.HasFlag(TR_MULTIPLE) and not (styles & TR_MULTIPLE): selections = self.GetSelections() for select in selections[0:-1]: self.SelectItem(select, False) - self._windowStyle = styles + self.SetWindowStyle(styles) self._dirty = True def GetTreeStyle(self): """Returns the CustomTreeCtrl style.""" - return self._windowStyle + return self.GetWindowStyle() def HasButtons(self): @@ -3155,10 +3148,10 @@ class CustomTreeCtrl(wx.PyScrolledWindow): def DoInsertItem(self, parentId, previous, text, ct_type=0, wnd=None, image=-1, selImage=-1, data=None): """Actually inserts an item in the tree.""" - if wnd is not None and not (self._windowStyle & TR_HAS_VARIABLE_ROW_HEIGHT): + if wnd is not None and not self.HasFlag(TR_HAS_VARIABLE_ROW_HEIGHT): raise Exception("\nERROR: In Order To Append/Insert Controls You Have To Use The Style TR_HAS_VARIABLE_ROW_HEIGHT") - if text.find("\n") >= 0 and not (self._windowStyle & TR_HAS_VARIABLE_ROW_HEIGHT): + if text.find("\n") >= 0 and not self.HasFlag(TR_HAS_VARIABLE_ROW_HEIGHT): raise Exception("\nERROR: In Order To Append/Insert A MultiLine Text You Have To Use The Style TR_HAS_VARIABLE_ROW_HEIGHT") if ct_type < 0 or ct_type > 2: @@ -3190,10 +3183,10 @@ class CustomTreeCtrl(wx.PyScrolledWindow): if self._anchor: raise Exception("\nERROR: Tree Can Have Only One Root") - if wnd is not None and not (self._windowStyle & TR_HAS_VARIABLE_ROW_HEIGHT): + if wnd is not None and not self.HasFlag(TR_HAS_VARIABLE_ROW_HEIGHT): raise Exception("\nERROR: In Order To Append/Insert Controls You Have To Use The Style TR_HAS_VARIABLE_ROW_HEIGHT") - if text.find("\n") >= 0 and not (self._windowStyle & TR_HAS_VARIABLE_ROW_HEIGHT): + if text.find("\n") >= 0 and not self.HasFlag(TR_HAS_VARIABLE_ROW_HEIGHT): raise Exception("\nERROR: In Order To Append/Insert A MultiLine Text You Have To Use The Style TR_HAS_VARIABLE_ROW_HEIGHT") if ct_type < 0 or ct_type > 2: @@ -3226,10 +3219,10 @@ class CustomTreeCtrl(wx.PyScrolledWindow): def PrependItem(self, parent, text, ct_type=0, wnd=None, image=-1, selImage=-1, data=None): """Appends an item as a first child of parent.""" - if wnd is not None and not (self._windowStyle & TR_HAS_VARIABLE_ROW_HEIGHT): + if wnd is not None and not self.HasFlag(TR_HAS_VARIABLE_ROW_HEIGHT): raise Exception("\nERROR: In Order To Append/Insert Controls You Have To Use The Style TR_HAS_VARIABLE_ROW_HEIGHT") - if text.find("\n") >= 0 and not (self._windowStyle & TR_HAS_VARIABLE_ROW_HEIGHT): + if text.find("\n") >= 0 and not self.HasFlag(TR_HAS_VARIABLE_ROW_HEIGHT): raise Exception("\nERROR: In Order To Append/Insert A MultiLine Text You Have To Use The Style TR_HAS_VARIABLE_ROW_HEIGHT") return self.DoInsertItem(parent, 0, text, ct_type, wnd, image, selImage, data) @@ -3238,10 +3231,10 @@ class CustomTreeCtrl(wx.PyScrolledWindow): def InsertItemByItem(self, parentId, idPrevious, text, ct_type=0, wnd=None, image=-1, selImage=-1, data=None): """Auxiliary function to cope with the C++ hideous multifunction.""" - if wnd is not None and not (self._windowStyle & TR_HAS_VARIABLE_ROW_HEIGHT): + if wnd is not None and not self.HasFlag(TR_HAS_VARIABLE_ROW_HEIGHT): raise Exception("\nERROR: In Order To Append/Insert Controls You Have To Use The Style TR_HAS_VARIABLE_ROW_HEIGHT") - if text.find("\n") >= 0 and not (self._windowStyle & TR_HAS_VARIABLE_ROW_HEIGHT): + if text.find("\n") >= 0 and not self.HasFlag(TR_HAS_VARIABLE_ROW_HEIGHT): raise Exception("\nERROR: In Order To Append/Insert A MultiLine Text You Have To Use The Style TR_HAS_VARIABLE_ROW_HEIGHT") parent = parentId @@ -3264,10 +3257,10 @@ class CustomTreeCtrl(wx.PyScrolledWindow): def InsertItemByIndex(self, parentId, before, text, ct_type=0, wnd=None, image=-1, selImage=-1, data=None): """Auxiliary function to cope with the C++ hideous multifunction.""" - if wnd is not None and not (self._windowStyle & TR_HAS_VARIABLE_ROW_HEIGHT): + if wnd is not None and not self.HasFlag(TR_HAS_VARIABLE_ROW_HEIGHT): raise Exception("\nERROR: In Order To Append/Insert Controls You Have To Use The Style TR_HAS_VARIABLE_ROW_HEIGHT") - if text.find("\n") >= 0 and not (self._windowStyle & TR_HAS_VARIABLE_ROW_HEIGHT): + if text.find("\n") >= 0 and not self.HasFlag(TR_HAS_VARIABLE_ROW_HEIGHT): raise Exception("\nERROR: In Order To Append/Insert A MultiLine Text You Have To Use The Style TR_HAS_VARIABLE_ROW_HEIGHT") parent = parentId @@ -3282,10 +3275,10 @@ class CustomTreeCtrl(wx.PyScrolledWindow): def InsertItem(self, parentId, input, text, ct_type=0, wnd=None, image=-1, selImage=-1, data=None): """Inserts an item after the given previous.""" - if wnd is not None and not (self._windowStyle & TR_HAS_VARIABLE_ROW_HEIGHT): + if wnd is not None and not self.HasFlag(TR_HAS_VARIABLE_ROW_HEIGHT): raise Exception("\nERROR: In Order To Append/Insert Controls You Have To Use The Style TR_HAS_VARIABLE_ROW_HEIGHT") - if text.find("\n") >= 0 and not (self._windowStyle & TR_HAS_VARIABLE_ROW_HEIGHT): + if text.find("\n") >= 0 and not self.HasFlag(TR_HAS_VARIABLE_ROW_HEIGHT): raise Exception("\nERROR: In Order To Append/Insert A MultiLine Text You Have To Use The Style TR_HAS_VARIABLE_ROW_HEIGHT") if type(input) == type(1): @@ -3297,10 +3290,10 @@ class CustomTreeCtrl(wx.PyScrolledWindow): def AppendItem(self, parentId, text, ct_type=0, wnd=None, image=-1, selImage=-1, data=None): """Appends an item as a last child of its parent.""" - if wnd is not None and not (self._windowStyle & TR_HAS_VARIABLE_ROW_HEIGHT): + if wnd is not None and not self.HasFlag(TR_HAS_VARIABLE_ROW_HEIGHT): raise Exception("\nERROR: In Order To Append/Insert Controls You Have To Use The Style TR_HAS_VARIABLE_ROW_HEIGHT") - if text.find("\n") >= 0 and not (self._windowStyle & TR_HAS_VARIABLE_ROW_HEIGHT): + if text.find("\n") >= 0 and not self.HasFlag(TR_HAS_VARIABLE_ROW_HEIGHT): raise Exception("\nERROR: In Order To Append/Insert A MultiLine Text You Have To Use The Style TR_HAS_VARIABLE_ROW_HEIGHT") parent = parentId @@ -3562,7 +3555,8 @@ class CustomTreeCtrl(wx.PyScrolledWindow): for child in self._itemWithWindow: if not self.IsVisible(child): wnd = child.GetWindow() - wnd.Hide() + if wnd: + wnd.Hide() def Unselect(self): @@ -4565,7 +4559,7 @@ class CustomTreeCtrl(wx.PyScrolledWindow): else: # no custom buttons - if self._windowStyle & TR_TWIST_BUTTONS: + if self.HasFlag(TR_TWIST_BUTTONS): # We draw something like the Mac twist buttons dc.SetPen(wx.BLACK_PEN) diff --git a/wxPython/wx/lib/floatcanvas/FloatCanvas.py b/wxPython/wx/lib/floatcanvas/FloatCanvas.py index 5a9b74186c..60d773a1d0 100644 --- a/wxPython/wx/lib/floatcanvas/FloatCanvas.py +++ b/wxPython/wx/lib/floatcanvas/FloatCanvas.py @@ -1,82 +1,64 @@ + from __future__ import division try: - from Numeric import array,asarray,Float,cos, sin, pi,sum,minimum,maximum,Int32,zeros, ones, concatenate, sqrt, argmin, power, absolute, matrixmultiply, transpose, sometrue, arange, hypot + import numpy as N except ImportError: - try: - from numarray import array, asarray, Float, cos, sin, pi, sum, minimum, maximum, Int32, zeros, concatenate, matrixmultiply, transpose, sometrue, arange, hypot - except ImportError: - raise ImportError("I could not import either Numeric or numarray") - -from time import clock, sleep - -import Resources # A file with icons, etc for FloatCanvas + raise ImportError("I could not import numpy") +from time import clock import wx -import types -import os +from Utilities import BBox + ## A global variable to hold the Pixels per inch that wxWindows thinks is in use ## This is used for scaling fonts. ## This can't be computed on module __init__, because a wx.App might not have initialized yet. -global ScreenPPI +global FontScale -## a custom Exceptions: +## Custom Exceptions: class FloatCanvasError(Exception): pass ## Create all the mouse events -# I don't see a need for these two, but maybe some day! -#EVT_FC_ENTER_WINDOW = wx.NewEventType() -#EVT_FC_LEAVE_WINDOW = wx.NewEventType() -EVT_FC_LEFT_DOWN = wx.NewEventType() +EVT_FC_ENTER_WINDOW = wx.NewEventType() +EVT_FC_LEAVE_WINDOW = wx.NewEventType() +EVT_FC_LEFT_DOWN = wx.NewEventType() EVT_FC_LEFT_UP = wx.NewEventType() -EVT_FC_LEFT_DCLICK = wx.NewEventType() -EVT_FC_MIDDLE_DOWN = wx.NewEventType() -EVT_FC_MIDDLE_UP = wx.NewEventType() -EVT_FC_MIDDLE_DCLICK = wx.NewEventType() -EVT_FC_RIGHT_DOWN = wx.NewEventType() -EVT_FC_RIGHT_UP = wx.NewEventType() -EVT_FC_RIGHT_DCLICK = wx.NewEventType() -EVT_FC_MOTION = wx.NewEventType() -EVT_FC_MOUSEWHEEL = wx.NewEventType() +EVT_FC_LEFT_DCLICK = wx.NewEventType() +EVT_FC_MIDDLE_DOWN = wx.NewEventType() +EVT_FC_MIDDLE_UP = wx.NewEventType() +EVT_FC_MIDDLE_DCLICK = wx.NewEventType() +EVT_FC_RIGHT_DOWN = wx.NewEventType() +EVT_FC_RIGHT_UP = wx.NewEventType() +EVT_FC_RIGHT_DCLICK = wx.NewEventType() +EVT_FC_MOTION = wx.NewEventType() +EVT_FC_MOUSEWHEEL = wx.NewEventType() ## these two are for the hit-test stuff, I never make them real Events +## fixme: could I use the PyEventBinder for the Object events too? EVT_FC_ENTER_OBJECT = wx.NewEventType() EVT_FC_LEAVE_OBJECT = wx.NewEventType() -##Create all mouse event binding functions -#def EVT_ENTER_WINDOW( window, function ): -# window.Connect( -1, -1, EVT_FC_ENTER_WINDOW, function ) -#def EVT_LEAVE_WINDOW( window, function ): -# window.Connect( -1, -1,EVT_FC_LEAVE_WINDOW , function ) -def EVT_LEFT_DOWN( window, function ): - window.Connect( -1, -1,EVT_FC_LEFT_DOWN , function ) -def EVT_LEFT_UP( window, function ): - window.Connect( -1, -1,EVT_FC_LEFT_UP , function ) -def EVT_LEFT_DCLICK ( window, function ): - window.Connect( -1, -1,EVT_FC_LEFT_DCLICK , function ) -def EVT_MIDDLE_DOWN ( window, function ): - window.Connect( -1, -1,EVT_FC_MIDDLE_DOWN , function ) -def EVT_MIDDLE_UP ( window, function ): - window.Connect( -1, -1,EVT_FC_MIDDLE_UP , function ) -def EVT_MIDDLE_DCLICK ( window, function ): - window.Connect( -1, -1,EVT_FC_MIDDLE_DCLICK , function ) -def EVT_RIGHT_DOWN ( window, function ): - window.Connect( -1, -1,EVT_FC_RIGHT_DOWN , function ) -def EVT_RIGHT_UP( window, function ): - window.Connect( -1, -1,EVT_FC_RIGHT_UP , function ) -def EVT_RIGHT_DCLICK( window, function ): - window.Connect( -1, -1,EVT_FC_RIGHT_DCLICK , function ) -def EVT_MOTION( window, function ): - window.Connect( -1, -1,EVT_FC_MOTION , function ) -def EVT_MOUSEWHEEL( window, function ): - window.Connect( -1, -1,EVT_FC_MOUSEWHEEL , function ) +##Create all mouse event binding objects +EVT_LEFT_DOWN = wx.PyEventBinder(EVT_FC_LEFT_DOWN) +EVT_LEFT_UP = wx.PyEventBinder(EVT_FC_LEFT_UP) +EVT_LEFT_DCLICK = wx.PyEventBinder(EVT_FC_LEFT_DCLICK) +EVT_MIDDLE_DOWN = wx.PyEventBinder(EVT_FC_MIDDLE_DOWN) +EVT_MIDDLE_UP = wx.PyEventBinder(EVT_FC_MIDDLE_UP) +EVT_MIDDLE_DCLICK = wx.PyEventBinder(EVT_FC_MIDDLE_DCLICK) +EVT_RIGHT_DOWN = wx.PyEventBinder(EVT_FC_RIGHT_DOWN) +EVT_RIGHT_UP = wx.PyEventBinder(EVT_FC_RIGHT_UP) +EVT_RIGHT_DCLICK = wx.PyEventBinder(EVT_FC_RIGHT_DCLICK) +EVT_MOTION = wx.PyEventBinder(EVT_FC_MOTION) +EVT_ENTER_WINDOW = wx.PyEventBinder(EVT_FC_ENTER_WINDOW) +EVT_LEAVE_WINDOW = wx.PyEventBinder(EVT_FC_LEAVE_WINDOW) +EVT_MOUSEWHEEL = wx.PyEventBinder(EVT_FC_MOUSEWHEEL) class _MouseEvent(wx.PyCommandEvent): - """ + """! This event class takes a regular wxWindows mouse event as a parameter, and wraps it so that there is access to all the original methods. This @@ -99,24 +81,20 @@ class _MouseEvent(wx.PyCommandEvent): self.SetEventType( EventType ) self._NativeEvent = NativeEvent self.Coords = Coords - -# I don't think this is used. -# def SetCoords(self,Coords): -# self.Coords = Coords - + def GetCoords(self): return self.Coords def __getattr__(self, name): - #return eval(self.NativeEvent.__getattr__(name) ) return getattr(self._NativeEvent, name) def _cycleidxs(indexcount, maxvalue, step): - """ + """! Utility function used by _colorGenerator """ + if indexcount == 0: yield () else: @@ -126,97 +104,50 @@ def _cycleidxs(indexcount, maxvalue, step): def _colorGenerator(): - """ + """! - Generates a seris of unique colors used to do hit-tests with the HIt + Generates a series of unique colors used to do hit-tests with the Hit Test bitmap - """ - import sys - if sys.platform == 'darwin': - depth = 24 - else: - b = wx.EmptyBitmap(1,1) - depth = b.GetDepth() - if depth == 16: - step = 8 - elif depth >= 24: + + depth = wx.GetDisplayDepth() +## ##there have been problems with 16 bbp displays, to I'm disabling this for now. +## if depth == 16: +## print "Warning: There have been problems with hit-testing on 16bbp displays" +## step = 8 + if depth >= 24: step = 1 else: - raise "ColorGenerator does not work with depth = %s" % depth + msg= ["ColorGenerator does not work with depth = %s" % depth] + msg.append("It is required for hit testing -- binding events to mouse") + msg.append("actions on objects on the Canvas.") + msg.append("Please set your display to 24bit") + msg.append("Alternatively, the code could be adapted to 16 bit if that's required") + raise FloatCanvasError(msg) return _cycleidxs(indexcount=3, maxvalue=256, step=step) - -#### I don't know if the Set objects are useful, beyond the pointset -#### object. The problem is that when zoomed in, the BB is checked to see -#### whether to draw the object. A Set object can defeat this. One day -#### I plan to write some custon C++ code to draw sets of objects - -##class ObjectSetMixin: -## """ -## A mix-in class for draw objects that are sets of objects - -## It contains methods for setting lists of pens and brushes - -## """ -## def SetPens(self,LineColors,LineStyles,LineWidths): -## """ -## This method used when an object could have a list of pens, rather than just one -## It is used for LineSet, and perhaps others in the future. - -## fixme: this should be in a mixin - -## fixme: this is really kludgy, there has got to be a better way! - -## """ - -## length = 1 -## if type(LineColors) == types.ListType: -## length = len(LineColors) -## else: -## LineColors = [LineColors] - -## if type(LineStyles) == types.ListType: -## length = len(LineStyles) -## else: -## LineStyles = [LineStyles] - -## if type(LineWidths) == types.ListType: -## length = len(LineWidths) -## else: -## LineWidths = [LineWidths] - -## if length > 1: -## if len(LineColors) == 1: -## LineColors = LineColors*length -## if len(LineStyles) == 1: -## LineStyles = LineStyles*length -## if len(LineWidths) == 1: -## LineWidths = LineWidths*length - -## self.Pens = [] -## for (LineColor,LineStyle,LineWidth) in zip(LineColors,LineStyles,LineWidths): -## if LineColor is None or LineStyle is None: -## self.Pens.append(wx.TRANSPARENT_PEN) -## # what's this for?> self.LineStyle = 'Transparent' -## if not self.PenList.has_key((LineColor,LineStyle,LineWidth)): -## Pen = wx.Pen(LineColor,LineWidth,self.LineStyleList[LineStyle]) -## self.Pens.append(Pen) -## else: -## self.Pens.append(self.PenList[(LineColor,LineStyle,LineWidth)]) -## if length == 1: -## self.Pens = self.Pens[0] - class DrawObject: - """ + """! This is the base class for all the objects that can be drawn. One must subclass from this (and an assortment of Mixins) to create a new DrawObject. + \note This class contain a series of static dictionaries: + + * BrushList + * PenList + * FillStyleList + * LineStyleList + + Is this still necessary? + """ def __init__(self, InForeground = False, IsVisible = True): + """! \param InForeground (bool) + \param IsVisible (Bool) + """ self.InForeground = InForeground self._Canvas = None @@ -235,7 +166,7 @@ class DrawObject: self.Pen = None self.FillStyle = "Solid" - + self.Visible = IsVisible # I pre-define all these as class variables to provide an easier @@ -287,12 +218,27 @@ class DrawObject: "DotDash" : wx.DOT_DASH, } +# def BBFromPoints(self, Points): +# """! +# Calculates a Bounding box from a set of points (NX2 array of coordinates) +# \param Points (array?) +# """ +# +# ## fixme: this could be done with array.min() and vstack() in numpy. +# ## This could use the Utilities.BBox module now. +# #return N.array( (N.minimum.reduce(Points), +# # N.maximum.reduce(Points) ), +# # ) +# return BBox.fromPoints(Points) + def Bind(self, Event, CallBackFun): self.CallBackFuncs[Event] = CallBackFun self.HitAble = True self._Canvas.UseHitTest = True - if not self._Canvas._HTdc: - self._Canvas.MakeNewHTdc() + if self.InForeground and self._Canvas._ForegroundHTBitmap is None: + self._Canvas.MakeNewForegroundHTBitmap() + elif self._Canvas._HTBitmap is None: + self._Canvas.MakeNewHTBitmap() if not self.HitColor: if not self._Canvas.HitColorGenerator: self._Canvas.HitColorGenerator = _colorGenerator() @@ -343,6 +289,21 @@ class DrawObject: else: self.HitPen = self.PenList.setdefault( (HitColor, "solid", self.HitLineWidth), wx.Pen(HitColor, self.HitLineWidth, self.LineStyleList["Solid"]) ) + ## Just to make sure that they will always be there + ## the appropriate ones should be overridden in the subclasses + def SetColor(self, Color): + pass + def SetLineColor(self, LineColor): + pass + def SetLineStyle(self, LineStyle): + pass + def SetLineWidth(self, LineWidth): + pass + def SetFillColor(self, FillColor): + pass + def SetFillStyle(self, FillStyle): + pass + def PutInBackground(self): if self._Canvas and self.InForeground: self._Canvas._ForeDrawList.remove(self) @@ -358,11 +319,70 @@ class DrawObject: self.InForeground = True def Hide(self): + """! \brief Make an object hidden. + """ self.Visible = False def Show(self): + """! \brief Make an object visible on the canvas. + """ self.Visible = True +class Group(DrawObject): + """ + A group of other FloatCanvas Objects + + Not all DrawObject methods may apply here. In particular, you can't Bind events to a group. + + Note that if an object is in more than one group, it will get drawn more than once. + + """ + + def __init__(self, ObjectList=[], InForeground = False, IsVisible = True): + self.ObjectList = list(ObjectList) + DrawObject.__init__(self, InForeground, IsVisible) + self.CalcBoundingBox() + + def AddObject(self, obj): + self.ObjectList.append(obj) + self.BoundingBox.Merge(obj.BoundingBox) + + def AddObjects(self, Objects): + for o in Objects: + self.AddObject(o) + + def CalcBoundingBox(self): + if self.ObjectList: + BB = BBox.asBBox(self.ObjectList[0].BoundingBox) + for obj in self.ObjectList[1:]: + BB.Merge(obj.BoundingBox) + else: + BB = None + self.BoundingBox = BB + + def SetColor(self, Color): + for o in self.ObjectList: + o.SetColor(Color) + def SetLineColor(self, Color): + for o in self.ObjectList: + o.SetLineColor(Color) + def SetLineStyle(self, LineStyle): + for o in self.ObjectList: + o.SetLineStyle(LineStyle) + def SetLineWidth(self, LineWidth): + for o in self.ObjectList: + o.SetLineWidth(LineWidth) + def SetFillColor(self, Color): + for o in self.ObjectList: + o.SetFillColor(Color) + def SetFillStyle(self, FillStyle): + for o in self.ObjectList: + o.SetFillStyle(FillStyle) + def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel = None, HTdc=None): + for obj in self.ObjectList: + obj._Draw(dc, WorldToPixel, ScaleWorldToPixel, HTdc) + + class ColorOnlyMixin: """ @@ -375,7 +395,7 @@ class ColorOnlyMixin: self.SetPen(Color,"Solid",1) self.SetBrush(Color,"Solid") - SetFillColor = SetColor # Just to provide a consistant interface + SetFillColor = SetColor # Just to provide a consistant interface class LineOnlyMixin: """ @@ -388,7 +408,8 @@ class LineOnlyMixin: def SetLineColor(self, LineColor): self.LineColor = LineColor self.SetPen(LineColor,self.LineStyle,self.LineWidth) - + SetColor = SetLineColor# so that it will do somethign reasonable + def SetLineStyle(self, LineStyle): self.LineStyle = LineStyle self.SetPen(self.LineColor,LineStyle,self.LineWidth) @@ -411,7 +432,16 @@ class LineAndFillMixin(LineOnlyMixin): def SetFillStyle(self, FillStyle): self.FillStyle = FillStyle self.SetBrush(self.FillColor,FillStyle) - + + def SetUpDraw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc): + dc.SetPen(self.Pen) + dc.SetBrush(self.Brush) + if HTdc and self.HitAble: + HTdc.SetPen(self.HitPen) + HTdc.SetBrush(self.HitBrush) + return ( WorldToPixel(self.XY), + ScaleWorldToPixel(self.WH) ) + class XYObjectMixin: """ @@ -427,29 +457,28 @@ class XYObjectMixin: (dx,dy) pair. Ideally a Numpy array of shape (2,) """ - - Delta = asarray(Delta, Float) + + Delta = N.asarray(Delta, N.float) self.XY += Delta - self.BoundingBox = self.BoundingBox + Delta - + self.BoundingBox += Delta + if self._Canvas: - self._Canvas.BoundingBoxDirty = True + self._Canvas.BoundingBoxDirty = True def CalcBoundingBox(self): ## This may get overwritten in some subclasses - self.BoundingBox = array( (self.XY, self.XY), Float ) + self.BoundingBox = N.array( (self.XY, self.XY), N.float ) + self.BoundingBox = BBox.asBBox((self.XY, self.XY)) def SetPoint(self, xy): - xy = array( xy, Float) + xy = N.array(xy, N.float) xy.shape = (2,) - Delta = xy - self.XY - + self.XY = xy - self.BoundingBox = self.BoundingBox + Delta + self.CalcBoundingBox() - #self.CalcBoundingBox() if self._Canvas: - self._Canvas.BoundingBoxDirty = True + self._Canvas.BoundingBoxDirty = True class PointsObjectMixin: """ @@ -459,29 +488,21 @@ class PointsObjectMixin: """ - -## This is code for the PointsObjectMixin object, it needs to be adapted and tested. -## Is the neccesary at all: you can always do: -## Object.SetPoints( Object.Points + delta, copy = False) -## def Move(self, Delta ): -## """ - -## Move(Delta): moves the object by delta, where delta is an (dx, -## dy) pair. Ideally a Numpy array of shape (2,) - -## """ + def Move(self, Delta): + """ + Move(Delta): moves the object by delta, where delta is an (dx, + dy) pair. Ideally a Numpy array of shape (2,) + """ -## Delta = array(Delta, Float) -## self.XY += Delta -## self.BoundingBox = self.BoundingBox + Delta##array((self.XY, (self.XY + self.WH)), Float) -## if self._Canvas: -## self._Canvas.BoundingBoxDirty = True + Delta = N.asarray(Delta, N.float) + Delta.shape = (2,) + self.Points += Delta + self.BoundingBox += Delta + if self._Canvas: + self._Canvas.BoundingBoxDirty = True def CalcBoundingBox(self): - self.BoundingBox = array(((min(self.Points[:,0]), - min(self.Points[:,1]) ), - (max(self.Points[:,0]), - max(self.Points[:,1]) ) ), Float ) + self.BoundingBox = BBox.fromPoints(self.Points) if self._Canvas: self._Canvas.BoundingBoxDirty = True @@ -498,17 +519,17 @@ class PointsObjectMixin: Points = Object.Points Points += (5,10) # shifts the points 5 in the x dir, and 10 in the y dir. Object.SetPoints(Points, False) # Sets the points to the same array as it was - + """ if copy: - self.Points = array(Points, Float) + self.Points = N.array(Points, N.float) self.Points.shape = (-1,2) # Make sure it is a NX2 array, even if there is only one point else: - self.Points = asarray(Points, Float) + self.Points = N.asarray(Points, N.float) self.CalcBoundingBox() - -class Polygon(DrawObject,PointsObjectMixin,LineAndFillMixin): + +class Polygon(PointsObjectMixin, LineAndFillMixin, DrawObject): """ @@ -530,8 +551,8 @@ class Polygon(DrawObject,PointsObjectMixin,LineAndFillMixin): FillColor = None, FillStyle = "Solid", InForeground = False): - DrawObject.__init__(self,InForeground) - self.Points = array(Points,Float) # this DOES need to make a copy + DrawObject.__init__(self, InForeground) + self.Points = N.array(Points ,N.float) # this DOES need to make a copy self.CalcBoundingBox() self.LineColor = LineColor @@ -554,41 +575,8 @@ class Polygon(DrawObject,PointsObjectMixin,LineAndFillMixin): HTdc.SetPen(self.HitPen) HTdc.SetBrush(self.HitBrush) HTdc.DrawPolygon(Points) - -##class PolygonSet(DrawObject): -## """ -## The PolygonSet class takes a Geometry.Polygon object. -## so that Points[N] = (x1,y1) and Points[N+1] = (x2,y2). N must be an even number! - -## it creates a set of line segments, from (x1,y1) to (x2,y2) - -## """ - -## def __init__(self,PolySet,LineColors,LineStyles,LineWidths,FillColors,FillStyles,InForeground = False): -## DrawObject.__init__(self, InForeground) - -## ##fixme: there should be some error checking for everything being the right length. - - -## self.Points = array(Points,Float) -## self.BoundingBox = array(((min(self.Points[:,0]),min(self.Points[:,1])),(max(self.Points[:,0]),max(self.Points[:,1]))),Float) -## self.LineColors = LineColors -## self.LineStyles = LineStyles -## self.LineWidths = LineWidths -## self.FillColors = FillColors -## self.FillStyles = FillStyles - -## self.SetPens(LineColors,LineStyles,LineWidths) - -## #def _Draw(self,dc,WorldToPixel,ScaleWorldToPixel): -## def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): -## Points = WorldToPixel(self.Points) -## Points.shape = (-1,4) -## dc.DrawLineList(Points,self.Pens) - - -class Line(DrawObject,PointsObjectMixin,LineOnlyMixin): +class Line(PointsObjectMixin, LineOnlyMixin, DrawObject,): """ The Line class takes a list of 2-tuples, or a NX2 NumPy Float array @@ -606,7 +594,7 @@ class Line(DrawObject,PointsObjectMixin,LineOnlyMixin): DrawObject.__init__(self, InForeground) - self.Points = array(Points,Float) + self.Points = N.array(Points,N.float) self.CalcBoundingBox() self.LineColor = LineColor @@ -617,7 +605,7 @@ class Line(DrawObject,PointsObjectMixin,LineOnlyMixin): self.HitLineWidth = max(LineWidth,self.MinHitLineWidth) - + def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): Points = WorldToPixel(self.Points) dc.SetPen(self.Pen) @@ -626,18 +614,31 @@ class Line(DrawObject,PointsObjectMixin,LineOnlyMixin): HTdc.SetPen(self.HitPen) HTdc.DrawLines(Points) -class Arrow(DrawObject,XYObjectMixin,LineOnlyMixin): +class Spline(Line): + def __init__(self, *args, **kwargs): + Line.__init__(self, *args, **kwargs) + + def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): + Points = WorldToPixel(self.Points) + dc.SetPen(self.Pen) + dc.DrawSpline(Points) + if HTdc and self.HitAble: + HTdc.SetPen(self.HitPen) + HTdc.DrawSpline(Points) + + +class Arrow(XYObjectMixin, LineOnlyMixin, DrawObject): """ Arrow(XY, # coords of origin of arrow (x,y) Length, # length of arrow in pixels theta, # angle of arrow in degrees: zero is straight up - # angle is to the right + # +angle is to the right LineColor = "Black", LineStyle = "Solid", - LineWidth = 1, - ArrowHeadSize = 4, - ArrowHeadAngle = 45, + LineWidth = 1, + ArrowHeadSize = 4, # size of arrowhead in pixels + ArrowHeadAngle = 45, # angle of arrow head in degrees InForeground = False): It will draw an arrow , starting at the point, (X,Y) pointing in @@ -658,12 +659,12 @@ class Arrow(DrawObject,XYObjectMixin,LineOnlyMixin): DrawObject.__init__(self, InForeground) - self.XY = array(XY, Float) - self.XY.shape = (2,) # Make sure it is a 1X2 array, even if there is only one point + self.XY = N.array(XY, N.float) + self.XY.shape = (2,) # Make sure it is a length 2 vector self.Length = Length self.Direction = float(Direction) - self.ArrowHeadSize = ArrowHeadSize - self.ArrowHeadAngle = float(ArrowHeadAngle) + self.ArrowHeadSize = ArrowHeadSize + self.ArrowHeadAngle = float(ArrowHeadAngle) self.CalcArrowPoints() self.CalcBoundingBox() @@ -680,7 +681,7 @@ class Arrow(DrawObject,XYObjectMixin,LineOnlyMixin): def SetDirection(self, Direction): self.Direction = float(Direction) self.CalcArrowPoints() - + def SetLength(self, Length): self.Length = Length self.CalcArrowPoints() @@ -689,26 +690,37 @@ class Arrow(DrawObject,XYObjectMixin,LineOnlyMixin): self.Direction = float(Direction) self.Length = Length self.CalcArrowPoints() - - def SetLength(self, Length): - self.Length = Length - self.CalcArrowPoints() - ## fixme: cache this? +## def CalcArrowPoints(self): +## L = self.Length +## S = self.ArrowHeadSize +## phi = self.ArrowHeadAngle * N.pi / 360 +## theta = (self.Direction-90.0) * N.pi / 180 +## ArrowPoints = N.array( ( (0, L, L - S*N.cos(phi),L, L - S*N.cos(phi) ), +## (0, 0, S*N.sin(phi), 0, -S*N.sin(phi) ) ), +## N.float ) +## RotationMatrix = N.array( ( ( N.cos(theta), -N.sin(theta) ), +## ( N.sin(theta), N.cos(theta) ) ), +## N.float +## ) +## ArrowPoints = N.matrixmultiply(RotationMatrix, ArrowPoints) +## self.ArrowPoints = N.transpose(ArrowPoints) + def CalcArrowPoints(self): L = self.Length S = self.ArrowHeadSize - phi = self.ArrowHeadAngle * pi / 360 - theta = (self.Direction-90.0) * pi / 180 - ArrowPoints = array( ( (0, L, L - S*cos(phi),L, L - S*cos(phi) ), - (0, 0, S*sin(phi), 0, -S*sin(phi) ) ), - Float ) - RotationMatrix = array( ( ( cos(theta), -sin(theta) ), - ( sin(theta), cos(theta) ) ), - Float - ) - ArrowPoints = matrixmultiply(RotationMatrix, ArrowPoints) - self.ArrowPoints = transpose(ArrowPoints) + phi = self.ArrowHeadAngle * N.pi / 360 + theta = (270 - self.Direction) * N.pi / 180 + AP = N.array( ( (0,0), + (0,0), + (N.cos(theta - phi), -N.sin(theta - phi) ), + (0,0), + (N.cos(theta + phi), -N.sin(theta + phi) ), + ), N.float ) + AP *= S + shift = (-L*N.cos(theta), L*N.sin(theta) ) + AP[1:,:] += shift + self.ArrowPoints = AP def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): dc.SetPen(self.Pen) @@ -719,38 +731,86 @@ class Arrow(DrawObject,XYObjectMixin,LineOnlyMixin): HTdc.SetPen(self.HitPen) HTdc.DrawLines(ArrowPoints) -##class LineSet(DrawObject, ObjectSetMixin): -## """ -## The LineSet class takes a list of 2-tuples, or a NX2 NumPy array of point coordinates. -## so that Points[N] = (x1,y1) and Points[N+1] = (x2,y2). N must be an even number! - -## it creates a set of line segments, from (x1,y1) to (x2,y2) - -## """ - -## def __init__(self,Points,LineColors,LineStyles,LineWidths,InForeground = False): -## DrawObject.__init__(self, InForeground) -## NumLines = len(Points) / 2 -## ##fixme: there should be some error checking for everything being the right length. +class ArrowLine(PointsObjectMixin, LineOnlyMixin, DrawObject): + """ + + ArrowLine(Points, # coords of points + LineColor = "Black", + LineStyle = "Solid", + LineWidth = 1, + ArrowHeadSize = 4, # in pixels + ArrowHeadAngle = 45, + InForeground = False): - -## self.Points = array(Points,Float) -## self.BoundingBox = array(((min(self.Points[:,0]),min(self.Points[:,1])),(max(self.Points[:,0]),max(self.Points[:,1]))),Float) + It will draw a set of arrows from point to point. + + It takes a list of 2-tuples, or a NX2 NumPy Float array + of point coordinates. -## self.LineColors = LineColors -## self.LineStyles = LineStyles -## self.LineWidths = LineWidths -## self.SetPens(LineColors,LineStyles,LineWidths) + """ + + def __init__(self, + Points, + LineColor = "Black", + LineStyle = "Solid", + LineWidth = 1, # pixels + ArrowHeadSize = 8, # pixels + ArrowHeadAngle = 30, # degrees + InForeground = False): + + DrawObject.__init__(self, InForeground) + + self.Points = N.asarray(Points,N.float) + self.Points.shape = (-1,2) # Make sure it is a NX2 array, even if there is only one point + self.ArrowHeadSize = ArrowHeadSize + self.ArrowHeadAngle = float(ArrowHeadAngle) + + self.CalcArrowPoints() + self.CalcBoundingBox() + + self.LineColor = LineColor + self.LineStyle = LineStyle + self.LineWidth = LineWidth + + self.SetPen(LineColor,LineStyle,LineWidth) + + self.HitLineWidth = max(LineWidth,self.MinHitLineWidth) + + def CalcArrowPoints(self): + S = self.ArrowHeadSize + phi = self.ArrowHeadAngle * N.pi / 360 + Points = self.Points + n = Points.shape[0] + self.ArrowPoints = N.zeros((n-1, 3, 2), N.float) + for i in xrange(n-1): + dx, dy = self.Points[i] - self.Points[i+1] + theta = N.arctan2(dy, dx) + AP = N.array( ( + (N.cos(theta - phi), -N.sin(theta-phi)), + (0,0), + (N.cos(theta + phi), -N.sin(theta + phi)) + ), + N.float ) + self.ArrowPoints[i,:,:] = AP + self.ArrowPoints *= S + + def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): + Points = WorldToPixel(self.Points) + ArrowPoints = Points[1:,N.newaxis,:] + self.ArrowPoints + dc.SetPen(self.Pen) + dc.DrawLines(Points) + for arrow in ArrowPoints: + dc.DrawLines(arrow) + if HTdc and self.HitAble: + HTdc.SetPen(self.HitPen) + HTdc.DrawLines(Points) + for arrow in ArrowPoints: + HTdc.DrawLines(arrow) -## #def _Draw(self,dc,WorldToPixel,ScaleWorldToPixel): -## def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): -## Points = WorldToPixel(self.Points) -## Points.shape = (-1,4) -## dc.DrawLineList(Points,self.Pens) -class PointSet(DrawObject,PointsObjectMixin, ColorOnlyMixin): +class PointSet(PointsObjectMixin, ColorOnlyMixin, DrawObject): """ The PointSet class takes a list of 2-tuples, or a NX2 NumPy array of @@ -775,20 +835,20 @@ class PointSet(DrawObject,PointsObjectMixin, ColorOnlyMixin): def __init__(self, Points, Color = "Black", Diameter = 1, InForeground = False): DrawObject.__init__(self,InForeground) - self.Points = array(Points,Float) + self.Points = N.array(Points,N.float) self.Points.shape = (-1,2) # Make sure it is a NX2 array, even if there is only one point self.CalcBoundingBox() self.Diameter = Diameter - self.HitLineWidth = self.MinHitLineWidth + self.HitLineWidth = min(self.MinHitLineWidth, Diameter) self.SetColor(Color) def SetDiameter(self,Diameter): self.Diameter = Diameter - + def FindClosestPoint(self, XY): """ - + Returns the index of the closest point to the point, XY, given in World coordinates. It's essentially random which you get if there are more than one that are the same. @@ -799,8 +859,8 @@ class PointSet(DrawObject,PointsObjectMixin, ColorOnlyMixin): """ d = self.Points - XY - return argmin(hypot(d[:,0],d[:,1])) - + return N.argmin(N.hypot(d[:,0],d[:,1])) + def DrawD2(self, dc, Points): # A Little optimization for a diameter2 - point @@ -822,7 +882,7 @@ class PointSet(DrawObject,PointsObjectMixin, ColorOnlyMixin): ##fixme: I really should add a DrawCircleList to wxPython if len(Points) > 100: xy = Points - xywh = concatenate((xy-radius, ones(xy.shape) * self.Diameter ), 1 ) + xywh = N.concatenate((xy-radius, N.ones(xy.shape) * self.Diameter ), 1 ) dc.DrawEllipseList(xywh) else: for xy in Points: @@ -837,15 +897,15 @@ class PointSet(DrawObject,PointsObjectMixin, ColorOnlyMixin): else: if len(Points) > 100: xy = Points - xywh = concatenate((xy-radius, ones(xy.shape) * self.Diameter ), 1 ) + xywh = N.concatenate((xy-radius, N.ones(xy.shape) * self.Diameter ), 1 ) HTdc.DrawEllipseList(xywh) else: for xy in Points: HTdc.DrawCircle(xy[0],xy[1], radius) -class Point(DrawObject,XYObjectMixin,ColorOnlyMixin): +class Point(XYObjectMixin, ColorOnlyMixin, DrawObject): """ - + The Point class takes a 2-tuple, or a (2,) NumPy array of point coordinates. @@ -858,9 +918,9 @@ class Point(DrawObject,XYObjectMixin,ColorOnlyMixin): """ def __init__(self, XY, Color = "Black", Diameter = 1, InForeground = False): DrawObject.__init__(self, InForeground) - - self.XY = array(XY, Float) - self.XY.shape = (2,) # Make sure it is a 1X2 array, even if there is only one point + + self.XY = N.array(XY, N.float) + self.XY.shape = (2,) # Make sure it is a length 2 vector self.CalcBoundingBox() self.SetColor(Color) self.Diameter = Diameter @@ -888,9 +948,9 @@ class Point(DrawObject,XYObjectMixin,ColorOnlyMixin): HTdc.SetBrush(self.HitBrush) HTdc.DrawCircle(xy[0],xy[1], radius) -class SquarePoint(DrawObject,XYObjectMixin,ColorOnlyMixin): +class SquarePoint(XYObjectMixin, ColorOnlyMixin, DrawObject): """ - + The SquarePoint class takes a 2-tuple, or a (2,) NumPy array of point coordinates. It produces a square dot, centered on Point @@ -903,9 +963,9 @@ class SquarePoint(DrawObject,XYObjectMixin,ColorOnlyMixin): """ def __init__(self, Point, Color = "Black", Size = 4, InForeground = False): DrawObject.__init__(self, InForeground) - - self.XY = array(Point, Float) - self.XY.shape = (2,) # Make sure it is a 1X2 array, even if there is only one point + + self.XY = N.array(Point, N.float) + self.XY.shape = (2,) # Make sure it is a length 2 vector self.CalcBoundingBox() self.SetColor(Color) self.Size = Size @@ -919,7 +979,7 @@ class SquarePoint(DrawObject,XYObjectMixin,ColorOnlyMixin): Size = self.Size dc.SetPen(self.Pen) xc,yc = WorldToPixel(self.XY) - + if self.Size <= 1: dc.DrawPoint(xc, yc) else: @@ -935,7 +995,7 @@ class SquarePoint(DrawObject,XYObjectMixin,ColorOnlyMixin): HTdc.SetBrush(self.HitBrush) HTdc.DrawRectangle(x, y, Size, Size) -class RectEllipse(DrawObject, XYObjectMixin, LineAndFillMixin): +class RectEllipse(XYObjectMixin, LineAndFillMixin, DrawObject): def __init__(self, XY, WH, LineColor = "Black", LineStyle = "Solid", @@ -943,14 +1003,10 @@ class RectEllipse(DrawObject, XYObjectMixin, LineAndFillMixin): FillColor = None, FillStyle = "Solid", InForeground = False): - + DrawObject.__init__(self,InForeground) - self.XY = array( XY, Float) - self.XY.shape = (2,) - self.WH = array( WH, Float ) - self.WH.shape = (2,) - self.BoundingBox = array((self.XY, (self.XY + self.WH)), Float) + self.SetShape(XY, WH) self.LineColor = LineColor self.LineStyle = LineStyle self.LineWidth = LineWidth @@ -963,23 +1019,19 @@ class RectEllipse(DrawObject, XYObjectMixin, LineAndFillMixin): self.SetBrush(FillColor,FillStyle) def SetShape(self, XY, WH): - self.XY = array( XY, Float) - self.WH = array( WH, Float ) + self.XY = N.array( XY, N.float) + self.XY.shape = (2,) + self.WH = N.array( WH, N.float) + self.WH.shape = (2,) self.CalcBoundingBox() - def SetUpDraw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc): - dc.SetPen(self.Pen) - dc.SetBrush(self.Brush) - if HTdc and self.HitAble: - HTdc.SetPen(self.HitPen) - HTdc.SetBrush(self.HitBrush) - return ( WorldToPixel(self.XY), - ScaleWorldToPixel(self.WH) ) - def CalcBoundingBox(self): - self.BoundingBox = array((self.XY, (self.XY + self.WH) ), Float) - self._Canvas.BoundingBoxDirty = True + # you need this in case Width or Height are negative + corners = N.array((self.XY, (self.XY + self.WH) ), N.float) + self.BoundingBox = BBox.fromPoints(corners) + if self._Canvas: + self._Canvas.BoundingBoxDirty = True class Rectangle(RectEllipse): @@ -993,6 +1045,8 @@ class Rectangle(RectEllipse): if HTdc and self.HitAble: HTdc.DrawRectanglePointSize(XY, WH) + + class Ellipse(RectEllipse): def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): @@ -1005,9 +1059,9 @@ class Ellipse(RectEllipse): HTdc.DrawEllipsePointSize(XY, WH) class Circle(Ellipse): - + ## fixme: this should probably be use the DC.DrawCircle! def __init__(self, XY, Diameter, **kwargs): - self.Center = array(XY, Float) + self.Center = N.array(XY, N.float) Diameter = float(Diameter) RectEllipse.__init__(self , self.Center - Diameter/2.0, @@ -1020,7 +1074,7 @@ class Circle(Ellipse): self.SetShape(XY, (Diameter, Diameter) ) - + class TextObjectMixin(XYObjectMixin): """ @@ -1028,29 +1082,29 @@ class TextObjectMixin(XYObjectMixin): the Text objects """ - + ## I'm caching fonts, because on GTK, getting a new font can take a ## while. However, it gets cleared after every full draw as hanging ## on to a bunch of large fonts takes a massive amount of memory. FontList = {} - LayoutFontSize = 12 # font size used for calculating layout + LayoutFontSize = 16 # font size used for calculating layout - def SetFont(self, Size, Family, Style, Weight, Underline, FaceName): + def SetFont(self, Size, Family, Style, Weight, Underlined, FaceName): self.Font = self.FontList.setdefault( (Size, Family, Style, Weight, - Underline, + Underlined, FaceName), - wx.Font(Size, + #wx.FontFromPixelSize((0.45*Size,Size), # this seemed to give a decent height/width ratio on Windows + wx.Font(Size, Family, - Style, + Style, Weight, - Underline, + Underlined, FaceName) ) - return self.Font def SetColor(self, Color): self.Color = Color @@ -1086,16 +1140,16 @@ class TextObjectMixin(XYObjectMixin): ## pad is the extra space around the text ## if world = 1, the vertical shift is done in y-up coordinates ShiftFunDict = {'tl': lambda x, y, w, h, world=0, pad=0: (x + pad, y + pad - 2*world*pad), - 'tc': lambda x, y, w, h, world=0, pad=0: (x - w/2, y + pad - 2*world*pad), - 'tr': lambda x, y, w, h, world=0, pad=0: (x - w - pad, y + pad - 2*world*pad), - 'cl': lambda x, y, w, h, world=0, pad=0: (x + pad, y - h/2 + world*h), - 'cc': lambda x, y, w, h, world=0, pad=0: (x - w/2, y - h/2 + world*h), + 'tc': lambda x, y, w, h, world=0, pad=0: (x - w/2, y + pad - 2*world*pad), + 'tr': lambda x, y, w, h, world=0, pad=0: (x - w - pad, y + pad - 2*world*pad), + 'cl': lambda x, y, w, h, world=0, pad=0: (x + pad, y - h/2 + world*h), + 'cc': lambda x, y, w, h, world=0, pad=0: (x - w/2, y - h/2 + world*h), 'cr': lambda x, y, w, h, world=0, pad=0: (x - w - pad, y - h/2 + world*h), 'bl': lambda x, y, w, h, world=0, pad=0: (x + pad, y - h + 2*world*h - pad + world*2*pad) , - 'bc': lambda x, y, w, h, world=0, pad=0: (x - w/2, y - h + 2*world*h - pad + world*2*pad) , + 'bc': lambda x, y, w, h, world=0, pad=0: (x - w/2, y - h + 2*world*h - pad + world*2*pad) , 'br': lambda x, y, w, h, world=0, pad=0: (x - w - pad, y - h + 2*world*h - pad + world*2*pad)} -class Text(DrawObject, TextObjectMixin): +class Text(TextObjectMixin, DrawObject, ): """ This class creates a text object, placed at the coordinates, x,y. the "Position" argument is a two charactor string, indicating @@ -1113,48 +1167,48 @@ class Text(DrawObject, TextObjectMixin): Family: Font family, a generic way of referring to fonts without specifying actual facename. One of: - wx.DEFAULT: Chooses a default font. - wx.DECORATIVE: A decorative font. - wx.ROMAN: A formal, serif font. - wx.SCRIPT: A handwriting font. - wx.SWISS: A sans-serif font. + wx.DEFAULT: Chooses a default font. + wx.DECORATIVE: A decorative font. + wx.ROMAN: A formal, serif font. + wx.SCRIPT: A handwriting font. + wx.SWISS: A sans-serif font. wx.MODERN: A fixed pitch font. NOTE: these are only as good as the wxWindows defaults, which aren't so good. Style: One of wx.NORMAL, wx.SLANT and wx.ITALIC. Weight: One of wx.NORMAL, wx.LIGHT and wx.BOLD. - Underline: + Underlined: The value can be True or False. At present this may have an an effect on Windows only. Alternatively, you can set the kw arg: Font, to a wx.Font, and the above will be ignored. - + The size is fixed, and does not scale with the drawing. The hit-test is done on the entire text extent """ - + def __init__(self,String, xy, - Size = 12, + Size = 14, Color = "Black", BackgroundColor = None, Family = wx.MODERN, Style = wx.NORMAL, Weight = wx.NORMAL, - Underline = False, + Underlined = False, Position = 'tl', InForeground = False, Font = None): - + DrawObject.__init__(self,InForeground) self.String = String - # Input size in in Pixels, compute points size from PPI info. + # Input size in in Pixels, compute points size from FontScaleinfo. # fixme: for printing, we'll have to do something a little different - self.Size = int(round(72.0 * Size / ScreenPPI)) + self.Size = Size * FontScale self.Color = Color self.BackgroundColor = BackgroundColor @@ -1162,17 +1216,17 @@ class Text(DrawObject, TextObjectMixin): if not Font: FaceName = '' else: - FaceName = Font.GetFaceName() + FaceName = Font.GetFaceName() Family = Font.GetFamily() - Size = Font.GetPointSize() + Size = Font.GetPointSize() Style = Font.GetStyle() - Underlined = Font.GetUnderlined() + Underlined = Font.GetUnderlined() Weight = Font.GetWeight() - self.SetFont(Size, Family, Style, Weight, Underline, FaceName) + self.SetFont(Size, Family, Style, Weight, Underlined, FaceName) - self.BoundingBox = array((xy, xy),Float) + self.BoundingBox = BBox.asBBox((xy, xy)) - self.XY = asarray(xy) + self.XY = N.asarray(xy) self.XY.shape = (2,) (self.TextWidth, self.TextHeight) = (None, None) @@ -1196,7 +1250,7 @@ class Text(DrawObject, TextObjectMixin): HTdc.SetBrush(self.HitBrush) HTdc.DrawRectanglePointSize(XY, (self.TextWidth, self.TextHeight) ) -class ScaledText(DrawObject, TextObjectMixin): +class ScaledText(TextObjectMixin, DrawObject, ): """ This class creates a text object that is scaled when zoomed. It is placed at the coordinates, x,y. the "Position" argument is a two @@ -1213,25 +1267,25 @@ class ScaledText(DrawObject, TextObjectMixin): Family: Font family, a generic way of referring to fonts without specifying actual facename. One of: - wx.DEFAULT: Chooses a default font. - wx.DECORATI: A decorative font. - wx.ROMAN: A formal, serif font. - wx.SCRIPT: A handwriting font. - wx.SWISS: A sans-serif font. + wx.DEFAULT: Chooses a default font. + wx.DECORATI: A decorative font. + wx.ROMAN: A formal, serif font. + wx.SCRIPT: A handwriting font. + wx.SWISS: A sans-serif font. wx.MODERN: A fixed pitch font. NOTE: these are only as good as the wxWindows defaults, which aren't so good. Style: One of wx.NORMAL, wx.SLANT and wx.ITALIC. Weight: One of wx.NORMAL, wx.LIGHT and wx.BOLD. - Underline: + Underlined: The value can be True or False. At present this may have an an effect on Windows only. Alternatively, you can set the kw arg: Font, to a wx.Font, and the above will be ignored. The size of the font you specify will be ignored, but the rest of its attributes will be preserved. - + The size will scale as the drawing is zoomed. Bugs/Limitations: @@ -1239,7 +1293,7 @@ class ScaledText(DrawObject, TextObjectMixin): As fonts are scaled, the do end up a little different, so you don't get exactly the same picture as you scale up and doen, but it's pretty darn close. - + On wxGTK1 on my Linux system, at least, using a font of over about 3000 pts. brings the system to a halt. It's the Font Server using huge amounts of memory. My work around is to max the font size to @@ -1252,38 +1306,41 @@ class ScaledText(DrawObject, TextObjectMixin): optional, but I haven't gotten around to it. """ - - def __init__(self, String, XY , Size, + + def __init__(self, + String, + XY, + Size, Color = "Black", BackgroundColor = None, Family = wx.MODERN, Style = wx.NORMAL, Weight = wx.NORMAL, - Underline = False, + Underlined = False, Position = 'tl', Font = None, InForeground = False): - + DrawObject.__init__(self,InForeground) self.String = String - self.XY = array( XY, Float) + self.XY = N.array( XY, N.float) self.XY.shape = (2,) - self.Size = Size + self.Size = Size self.Color = Color self.BackgroundColor = BackgroundColor - self.Family = Family - self.Style = Style - self.Weight = Weight - self.Underline = Underline + self.Family = Family + self.Style = Style + self.Weight = Weight + self.Underlined = Underlined if not Font: self.FaceName = '' else: - self.FaceName = Font.GetFaceName() - self.Family = Font.GetFamily() - self.Style = Font.GetStyle() - self.Underlined = Font.GetUnderlined() - self.Weight = Font.GetWeight() + self.FaceName = Font.GetFaceName() + self.Family = Font.GetFamily() + self.Style = Font.GetStyle() + self.Underlined = Font.GetUnderlined() + self.Weight = Font.GetWeight() # Experimental max font size value on wxGTK2: this works OK on # my system. If it's a lot larger, there is a crash, with the @@ -1296,7 +1353,7 @@ class ScaledText(DrawObject, TextObjectMixin): # Windows and OS-X seem to be better behaved in this regard. # They may not draw it, but they don't crash either! self.MaxFontSize = 1000 - + self.ShiftFun = self.ShiftFunDict[Position] self.CalcBoundingBox() @@ -1313,13 +1370,14 @@ class ScaledText(DrawObject, TextObjectMixin): dc.SelectObject(bitmap) #wxMac needs a Bitmap selected for GetTextExtent to work. DrawingSize = 40 # pts This effectively determines the resolution that the BB is computed to. ScaleFactor = float(self.Size) / DrawingSize - dc.SetFont(self.SetFont(DrawingSize, self.Family, self.Style, self.Weight, self.Underline, self.FaceName) ) + self.SetFont(DrawingSize, self.Family, self.Style, self.Weight, self.Underlined, self.FaceName) + dc.SetFont(self.Font) (w,h) = dc.GetTextExtent(self.String) w = w * ScaleFactor h = h * ScaleFactor x, y = self.ShiftFun(self.XY[0], self.XY[1], w, h, world = 1) - self.BoundingBox = array(((x, y-h ),(x + w, y)),Float) - + self.BoundingBox = BBox.asBBox(((x, y-h ),(x + w, y))) + def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): (X,Y) = WorldToPixel( (self.XY) ) @@ -1329,7 +1387,8 @@ class ScaledText(DrawObject, TextObjectMixin): ## If so, limit it. Would it be better just to not draw it? ## note that this limit is dependent on how much memory you have, etc. Size = min(Size, self.MaxFontSize) - dc.SetFont(self.SetFont(Size, self.Family, self.Style, self.Weight, self.Underline, self.FaceName)) + self.SetFont(Size, self.Family, self.Style, self.Weight, self.Underlined, self.FaceName) + dc.SetFont(self.Font) dc.SetTextForeground(self.Color) if self.BackgroundColor: dc.SetBackgroundMode(wx.SOLID) @@ -1348,7 +1407,7 @@ class ScaledText(DrawObject, TextObjectMixin): HTdc.SetBrush(self.HitBrush) HTdc.DrawRectanglePointSize(xy, (w, h) ) -class ScaledTextBox(DrawObject, TextObjectMixin): +class ScaledTextBox(TextObjectMixin, DrawObject): """ This class creates a TextBox object that is scaled when zoomed. It is placed at the coordinates, x,y. @@ -1356,7 +1415,7 @@ class ScaledTextBox(DrawObject, TextObjectMixin): If the Width parameter is defined, the text will be wrapped to the width given. A Box can be drawn around the text, be specifying: - LineWidth and/or FillColor + LineWidth and/or FillColor A space(margin) can be put all the way around the text, be specifying: the PadSize argument in world coordinates. @@ -1376,25 +1435,25 @@ class ScaledTextBox(DrawObject, TextObjectMixin): Family: Font family, a generic way of referring to fonts without specifying actual facename. One of: - wx.DEFAULT: Chooses a default font. - wx.DECORATIVE: A decorative font. - wx.ROMAN: A formal, serif font. - wx.SCRIPT: A handwriting font. - wx.SWISS: A sans-serif font. + wx.DEFAULT: Chooses a default font. + wx.DECORATIVE: A decorative font. + wx.ROMAN: A formal, serif font. + wx.SCRIPT: A handwriting font. + wx.SWISS: A sans-serif font. wx.MODERN: A fixed pitch font. NOTE: these are only as good as the wxWindows defaults, which aren't so good. Style: One of wx.NORMAL, wx.SLANT and wx.ITALIC. Weight: One of wx.NORMAL, wx.LIGHT and wx.BOLD. - Underline: + Underlined: The value can be True or False. At present this may have an an effect on Windows only. Alternatively, you can set the kw arg: Font, to a wx.Font, and the above will be ignored. The size of the font you specify will be ignored, but the rest of its attributes will be preserved. - + The size will scale as the drawing is zoomed. Bugs/Limitations: @@ -1402,7 +1461,7 @@ class ScaledTextBox(DrawObject, TextObjectMixin): As fonts are scaled, they do end up a little different, so you don't get exactly the same picture as you scale up and down, but it's pretty darn close. - + On wxGTK1 on my Linux system, at least, using a font of over about 1000 pts. brings the system to a halt. It's the Font Server using huge amounts of memory. My work around is to max the font size to @@ -1415,7 +1474,7 @@ class ScaledTextBox(DrawObject, TextObjectMixin): optional, but I haven't gotten around to it. """ - + def __init__(self, String, Point, Size, @@ -1429,17 +1488,17 @@ class ScaledTextBox(DrawObject, TextObjectMixin): Family = wx.MODERN, Style = wx.NORMAL, Weight = wx.NORMAL, - Underline = False, + Underlined = False, Position = 'tl', Alignment = "left", Font = None, LineSpacing = 1.0, InForeground = False): - + DrawObject.__init__(self,InForeground) - self.XY = array(Point, Float) - self.Size = Size + self.XY = N.array(Point, N.float) + self.Size = Size self.Color = Color self.BackgroundColor = BackgroundColor self.LineColor = LineColor @@ -1450,22 +1509,22 @@ class ScaledTextBox(DrawObject, TextObjectMixin): self.PadSize = Size/10.0 else: self.PadSize = float(PadSize) - self.Family = Family - self.Style = Style - self.Weight = Weight - self.Underline = Underline + self.Family = Family + self.Style = Style + self.Weight = Weight + self.Underlined = Underlined self.Alignment = Alignment.lower() self.LineSpacing = float(LineSpacing) self.Position = Position - + if not Font: self.FaceName = '' else: - self.FaceName = Font.GetFaceName() - self.Family = Font.GetFamily() - self.Style = Font.GetStyle() - self.Underlined = Font.GetUnderlined() - self.Weight = Font.GetWeight() + self.FaceName = Font.GetFaceName() + self.Family = Font.GetFamily() + self.Style = Font.GetStyle() + self.Underlined = Font.GetUnderlined() + self.Weight = Font.GetWeight() # Experimental max font size value on wxGTK2: this works OK on # my system. If it's a lot larger, there is a crash, with the @@ -1496,8 +1555,8 @@ class ScaledTextBox(DrawObject, TextObjectMixin): DrawingSize = self.LayoutFontSize # pts This effectively determines the resolution that the BB is computed to. ScaleFactor = float(self.Size) / DrawingSize Width = (self.Width - 2*self.PadSize) / ScaleFactor #Width to wrap to - dc.SetFont(self.SetFont(DrawingSize, self.Family, self.Style, self.Weight, self.Underline, self.FaceName) ) - + self.SetFont(DrawingSize, self.Family, self.Style, self.Weight, self.Underlined, self.FaceName) + dc.SetFont(self.Font) NewStrings = [] for s in self.Strings: #beginning = True @@ -1546,13 +1605,13 @@ class ScaledTextBox(DrawObject, TextObjectMixin): DrawingSize = self.LayoutFontSize # pts This effectively determines the resolution that the BB is computed to. ScaleFactor = float(self.Size) / DrawingSize - dc.SetFont(self.SetFont(DrawingSize, self.Family, self.Style, self.Weight, self.Underline, self.FaceName) ) - + self.SetFont(DrawingSize, self.Family, self.Style, self.Weight, self.Underlined, self.FaceName) + dc.SetFont(self.Font) TextHeight = dc.GetTextExtent("X")[1] SpaceWidth = dc.GetTextExtent(" ")[0] LineHeight = TextHeight * self.LineSpacing - LineWidths = zeros((len(self.Strings),), Float) + LineWidths = N.zeros((len(self.Strings),), N.float) y = 0 Words = [] AllLinePoints = [] @@ -1560,7 +1619,7 @@ class ScaledTextBox(DrawObject, TextObjectMixin): for i, s in enumerate(self.Strings): LineWidths[i] = 0 LineWords = s.split(" ") - LinePoints = zeros((len(LineWords),2), Float) + LinePoints = N.zeros((len(LineWords),2), N.float) for j, word in enumerate(LineWords): if j > 0: LineWidths[i] += SpaceWidth @@ -1570,14 +1629,14 @@ class ScaledTextBox(DrawObject, TextObjectMixin): LineWidths[i] += w y -= LineHeight AllLinePoints.append(LinePoints) - TextWidth = maximum.reduce(LineWidths) + TextWidth = N.maximum.reduce(LineWidths) self.Words = Words if self.Width is None: BoxWidth = TextWidth * ScaleFactor + 2*self.PadSize else: # use the defined Width BoxWidth = self.Width - Points = zeros((0,2), Float) + Points = N.zeros((0,2), N.float) for i, LinePoints in enumerate(AllLinePoints): ## Scale to World Coords. @@ -1588,10 +1647,10 @@ class ScaledTextBox(DrawObject, TextObjectMixin): LinePoints[:,0] += (BoxWidth - LineWidths[i]*ScaleFactor)/2.0 elif self.Alignment == 'right': LinePoints[:,0] += (BoxWidth - LineWidths[i]*ScaleFactor-self.PadSize) - Points = concatenate((Points, LinePoints)) + Points = N.concatenate((Points, LinePoints)) BoxHeight = -(Points[-1,1] - (TextHeight * ScaleFactor)) + 2*self.PadSize - (x,y) = self.ShiftFun(self.XY[0], self.XY[1], BoxWidth, BoxHeight, world=1) + #(x,y) = self.ShiftFun(self.XY[0], self.XY[1], BoxWidth, BoxHeight, world=1) Points += (0, -self.PadSize) self.Points = Points self.BoxWidth = BoxWidth @@ -1601,14 +1660,14 @@ class ScaledTextBox(DrawObject, TextObjectMixin): def CalcBoundingBox(self): """ - + Calculates the Bounding Box """ w, h = self.BoxWidth, self.BoxHeight x, y = self.ShiftFun(self.XY[0], self.XY[1], w, h, world=1) - self.BoundingBox = array(((x, y-h ),(x + w, y)),Float) + self.BoundingBox = BBox.asBBox(((x, y-h ),(x + w, y))) def GetBoxRect(self): wh = (self.BoxWidth, self.BoxHeight) @@ -1618,9 +1677,9 @@ class ScaledTextBox(DrawObject, TextObjectMixin): def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): xy, wh = self.GetBoxRect() - - Points = self.Points + xy - Points = WorldToPixel(Points) + + Points = self.Points + xy + Points = WorldToPixel(Points) xy = WorldToPixel(xy) wh = ScaleWorldToPixel(wh) * (1,-1) @@ -1630,9 +1689,9 @@ class ScaledTextBox(DrawObject, TextObjectMixin): ## If so, limit it. Would it be better just to not draw it? ## note that this limit is dependent on how much memory you have, etc. Size = min(Size, self.MaxFontSize) - - font = self.SetFont(Size, self.Family, self.Style, self.Weight, self.Underline, self.FaceName) - dc.SetFont(font) + + self.SetFont(Size, self.Family, self.Style, self.Weight, self.Underlined, self.FaceName) + dc.SetFont(self.Font) dc.SetTextForeground(self.Color) dc.SetBackgroundMode(wx.TRANSPARENT) @@ -1641,7 +1700,7 @@ class ScaledTextBox(DrawObject, TextObjectMixin): dc.SetBrush(self.Brush) dc.SetPen(self.Pen) dc.DrawRectanglePointSize(xy , wh) - + # Draw the Text dc.DrawTextList(self.Words, Points) @@ -1651,7 +1710,7 @@ class ScaledTextBox(DrawObject, TextObjectMixin): HTdc.SetBrush(self.HitBrush) HTdc.DrawRectanglePointSize(xy, wh) -class Bitmap(DrawObject, TextObjectMixin): +class Bitmap(TextObjectMixin, DrawObject, ): """ This class creates a bitmap object, placed at the coordinates, x,y. the "Position" argument is a two charactor string, indicating @@ -1665,20 +1724,20 @@ class Bitmap(DrawObject, TextObjectMixin): The size is fixed, and does not scale with the drawing. """ - + def __init__(self,Bitmap,XY, Position = 'tl', InForeground = False): - + DrawObject.__init__(self,InForeground) if type(Bitmap) == wx._gdi.Bitmap: self.Bitmap = Bitmap elif type(Bitmap) == wx._core.Image: self.Bitmap = wx.BitmapFromImage(Bitmap) - + # Note the BB is just the point, as the size in World coordinates is not fixed - self.BoundingBox = array((XY,XY),Float) + self.BoundingBox = BBox.asBBox( (XY,XY) ) self.XY = XY @@ -1694,13 +1753,13 @@ class Bitmap(DrawObject, TextObjectMixin): HTdc.SetBrush(self.HitBrush) HTdc.DrawRectanglePointSize(XY, (self.Width, self.Height) ) -class ScaledBitmap(DrawObject, TextObjectMixin): +class ScaledBitmap(TextObjectMixin, DrawObject, ): """ - + This class creates a bitmap object, placed at the coordinates, XY, of Height, H, in World coorsinates. The width is calculated from the aspect ratio of the bitmap. - + the "Position" argument is a two charactor string, indicating where in relation to the coordinates the bitmap should be oriented. @@ -1712,23 +1771,23 @@ class ScaledBitmap(DrawObject, TextObjectMixin): The size scales with the drawing """ - + def __init__(self, Bitmap, XY, Height, Position = 'tl', InForeground = False): - + DrawObject.__init__(self,InForeground) if type(Bitmap) == wx._gdi.Bitmap: self.Image = Bitmap.ConvertToImage() elif type(Bitmap) == wx._core.Image: self.Image = Bitmap - + self.XY = XY - self.Height = Height + self.Height = Height (self.bmpWidth, self.bmpHeight) = self.Image.GetWidth(), self.Image.GetHeight() self.Width = self.bmpWidth / self.bmpHeight * Height self.ShiftFun = self.ShiftFunDict[Position] @@ -1738,9 +1797,10 @@ class ScaledBitmap(DrawObject, TextObjectMixin): def CalcBoundingBox(self): ## this isn't exact, as fonts don't scale exactly. - w,h = self.Width, self.Height + w, h = self.Width, self.Height x, y = self.ShiftFun(self.XY[0], self.XY[1], w, h, world = 1) - self.BoundingBox = array(((x, y-h ),(x + w, y)),Float) + self.BoundingBox = BBox.asBBox( ( (x, y-h ), (x + w, y) ) ) + def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): XY = WorldToPixel(self.XY) @@ -1758,10 +1818,258 @@ class ScaledBitmap(DrawObject, TextObjectMixin): HTdc.SetBrush(self.HitBrush) HTdc.DrawRectanglePointSize(XY, (W, H) ) +class ScaledBitmap2(TextObjectMixin, DrawObject, ): + """ + + An alternative scaled bitmap that only scaled the required amount of + the main bitmap when zoomed in: EXPERIMENTAL! + + """ + + def __init__(self, + Bitmap, + XY, + Height, + Width=None, + Position = 'tl', + InForeground = False): + + DrawObject.__init__(self,InForeground) + + if type(Bitmap) == wx._gdi.Bitmap: + self.Image = Bitmap.ConvertToImage() + elif type(Bitmap) == wx._core.Image: + self.Image = Bitmap + + self.XY = N.array(XY, N.float) + self.Height = Height + (self.bmpWidth, self.bmpHeight) = self.Image.GetWidth(), self.Image.GetHeight() + self.bmpWH = N.array((self.bmpWidth, self.bmpHeight), N.int32) + ## fixme: this should all accommodate different scales for X and Y + if Width is None: + self.BmpScale = float(self.bmpHeight) / Height + self.Width = self.bmpWidth / self.BmpScale + self.WH = N.array((self.Width, Height), N.float) + ##fixme: should this have a y = -1 to shift to y-up? + self.BmpScale = self.bmpWH / self.WH + + print "bmpWH:", self.bmpWH + print "Width, Height:", self.WH + print "self.BmpScale", self.BmpScale + self.ShiftFun = self.ShiftFunDict[Position] + self.CalcBoundingBox() + self.ScaledBitmap = None # cache of the last existing scaled bitmap + + def CalcBoundingBox(self): + ## this isn't exact, as fonts don't scale exactly. + w,h = self.Width, self.Height + x, y = self.ShiftFun(self.XY[0], self.XY[1], w, h, world = 1) + self.BoundingBox = BBox.asBBox( ((x, y-h ), (x + w, y)) ) + + def WorldToBitmap(self, Pw): + """ + computes bitmap coords from World coords + """ + delta = Pw - self.XY + Pb = delta * self.BmpScale + Pb *= (1, -1) ##fixme: this may only works for Yup projection! + ## and may only work for top left position + + return Pb.astype(N.int_) + + def _DrawEntireBitmap(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc): + """ + this is pretty much the old code + + Scales and Draws the entire bitmap. + + """ + XY = WorldToPixel(self.XY) + H = ScaleWorldToPixel(self.Height)[0] + W = H * (self.bmpWidth / self.bmpHeight) + if (self.ScaledBitmap is None) or (self.ScaledBitmap[0] != (0, 0, self.bmpWidth, self.bmpHeight, W, H) ): + #if True: #fixme: (self.ScaledBitmap is None) or (H <> self.ScaledHeight) : + self.ScaledHeight = H + print "Scaling to:", W, H + Img = self.Image.Scale(W, H) + bmp = wx.BitmapFromImage(Img) + self.ScaledBitmap = ((0, 0, self.bmpWidth, self.bmpHeight , W, H), bmp)# this defines the cached bitmap + else: + print "Using Cached bitmap" + bmp = self.ScaledBitmap[1] + XY = self.ShiftFun(XY[0], XY[1], W, H) + dc.DrawBitmapPoint(bmp, XY, True) + if HTdc and self.HitAble: + HTdc.SetPen(self.HitPen) + HTdc.SetBrush(self.HitBrush) + HTdc.DrawRectanglePointSize(XY, (W, H) ) + + def _DrawSubBitmap(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc): + """ + Subsets just the part of the bitmap that is visible + then scales and draws that. + + """ + BBworld = BBox.asBBox(self._Canvas.ViewPortBB) + BBbitmap = BBox.fromPoints(self.WorldToBitmap(BBworld)) + + XYs = WorldToPixel(self.XY) + # figure out subimage: + # fixme: this should be able to be done more succinctly! + + if BBbitmap[0,0] < 0: + Xb = 0 + elif BBbitmap[0,0] > self.bmpWH[0]: # off the bitmap + Xb = 0 + else: + Xb = BBbitmap[0,0] + XYs[0] = 0 # draw at origin + + if BBbitmap[0,1] < 0: + Yb = 0 + elif BBbitmap[0,1] > self.bmpWH[1]: # off the bitmap + Yb = 0 + ShouldDraw = False + else: + Yb = BBbitmap[0,1] + XYs[1] = 0 # draw at origin + + if BBbitmap[1,0] < 0: + #off the screen -- This should never happen! + Wb = 0 + elif BBbitmap[1,0] > self.bmpWH[0]: + Wb = self.bmpWH[0] - Xb + else: + Wb = BBbitmap[1,0] - Xb + + if BBbitmap[1,1] < 0: + # off the screen -- This should never happen! + Hb = 0 + ShouldDraw = False + elif BBbitmap[1,1] > self.bmpWH[1]: + Hb = self.bmpWH[1] - Yb + else: + Hb = BBbitmap[1,1] - Yb + + FullHeight = ScaleWorldToPixel(self.Height)[0] + scale = FullHeight / self.bmpWH[1] + Ws = int(scale * Wb + 0.5) # add the 0.5 to round + Hs = int(scale * Hb + 0.5) + if (self.ScaledBitmap is None) or (self.ScaledBitmap[0] != (Xb, Yb, Wb, Hb, Ws, Ws) ): + Img = self.Image.GetSubImage(wx.Rect(Xb, Yb, Wb, Hb)) + Img.Rescale(Ws, Hs) + bmp = wx.BitmapFromImage(Img) + self.ScaledBitmap = ((Xb, Yb, Wb, Hb, Ws, Ws), bmp)# this defines the cached bitmap + #XY = self.ShiftFun(XY[0], XY[1], W, H) + #fixme: get the shiftfun working! + else: + print "Using cached bitmap" + ##fixme: The cached bitmap could be used if the one needed is the same scale, but + ## a subset of the cached one. + bmp = self.ScaledBitmap[1] + dc.DrawBitmapPoint(bmp, XYs, True) + + if HTdc and self.HitAble: + HTdc.SetPen(self.HitPen) + HTdc.SetBrush(self.HitBrush) + HTdc.DrawRectanglePointSize(XYs, (Ws, Hs) ) + + def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None): + BBworld = BBox.asBBox(self._Canvas.ViewPortBB) + ## first see if entire bitmap is displayed: + if BBworld.Inside(self.BoundingBox): + print "Drawing entire bitmap with old code" + self._DrawEntireBitmap(dc , WorldToPixel, ScaleWorldToPixel, HTdc) + return None + elif BBworld.Overlaps(self.BoundingBox): + #BBbitmap = BBox.fromPoints(self.WorldToBitmap(BBworld)) + print "Drawing a sub-bitmap" + self._DrawSubBitmap(dc , WorldToPixel, ScaleWorldToPixel, HTdc) + else: + print "Not Drawing -- no part of image is showing" + +class DotGrid: + """ + An example of a Grid Object -- it is set on teh FloatCAnvas with one of: + + FloatCanvas.GridUnder = Grid + FloatCanvas.GridOver = Grid + + It will be drawn every time, regardless of the viewport. + + In its _Draw method, it computes what to draw, given the ViewPortBB + of the Canvas it's being drawn on. + + """ + def __init__(self, Spacing, Size = 2, Color = "Black", Cross=False, CrossThickness = 1): + + self.Spacing = N.array(Spacing, N.float) + self.Spacing.shape = (2,) + self.Size = Size + self.Color = Color + self.Cross = Cross + self.CrossThickness = CrossThickness + + def CalcPoints(self, Canvas): + ViewPortBB = Canvas.ViewPortBB + + Spacing = self.Spacing + + minx, miny = N.floor(ViewPortBB[0] / Spacing) * Spacing + maxx, maxy = N.ceil(ViewPortBB[1] / Spacing) * Spacing + + ##fixme: this could use vstack or something with numpy + x = N.arange(minx, maxx+Spacing[0], Spacing[0]) # making sure to get the last point + y = N.arange(miny, maxy+Spacing[1], Spacing[1]) # an extra is OK + Points = N.zeros((len(y), len(x), 2), N.float) + x.shape = (1,-1) + y.shape = (-1,1) + Points[:,:,0] += x + Points[:,:,1] += y + Points.shape = (-1,2) + + return Points + + def _Draw(self, dc, Canvas): + Points = self.CalcPoints(Canvas) + + Points = Canvas.WorldToPixel(Points) + + dc.SetPen(wx.Pen(self.Color,self.CrossThickness)) + + if self.Cross: # Use cross shaped markers + #Horizontal lines + LinePoints = N.concatenate((Points + (self.Size,0),Points + (-self.Size,0)),1) + dc.DrawLineList(LinePoints) + # Vertical Lines + LinePoints = N.concatenate((Points + (0,self.Size),Points + (0,-self.Size)),1) + dc.DrawLineList(LinePoints) + pass + else: # use dots + ## Note: this code borrowed from Pointset -- itreally shouldn't be repeated here!. + if self.Size <= 1: + dc.DrawPointList(Points) + elif self.Size <= 2: + dc.DrawPointList(Points + (0,-1)) + dc.DrawPointList(Points + (0, 1)) + dc.DrawPointList(Points + (1, 0)) + dc.DrawPointList(Points + (-1,0)) + else: + dc.SetBrush(wx.Brush(self.Color)) + radius = int(round(self.Size/2)) + ##fixme: I really should add a DrawCircleList to wxPython + if len(Points) > 100: + xy = Points + xywh = N.concatenate((xy-radius, N.ones(xy.shape) * self.Size ), 1 ) + dc.DrawEllipseList(xywh) + else: + for xy in Points: + dc.DrawCircle(xy[0],xy[1], radius) + + #--------------------------------------------------------------------------- class FloatCanvas(wx.Panel): - ## fixme: could this be a wx.Window? """ FloatCanvas.py @@ -1796,7 +2104,7 @@ class FloatCanvas(wx.Panel): havn't bothered to add any checks for that yet. Zooming: - I have set no zoom limits. What this means is that if you zoom in really + I have set no zoom limits. What this means is that if you zoom in really far, you can get integer overflows, and get wierd results. It doesn't seem to actually cause any problems other than wierd output, at least when I have run it. @@ -1823,10 +2131,10 @@ class FloatCanvas(wx.Panel): Mouse Events: At this point, there are a full set of custom mouse events. They are - just like the rebulsr mouse events, but include an extra attribute: + just like the regular mouse events, but include an extra attribute: Event.GetCoords(), that returns the (x,y) position in world coordinates, as a length-2 NumPy vector of Floats. - + Copyright: Christopher Barker License: Same as the version of wxPython you are using it with @@ -1837,8 +2145,8 @@ class FloatCanvas(wx.Panel): Chris.Barker@noaa.gov - """ - + """ + def __init__(self, parent, id = -1, size = wx.DefaultSize, ProjectionFun = None, @@ -1846,16 +2154,9 @@ class FloatCanvas(wx.Panel): Debug = False): wx.Panel.__init__( self, parent, id, wx.DefaultPosition, size) - - global ScreenPPI ## A global variable to hold the Pixels per inch that wxWindows thinks is in use. - dc = wx.ScreenDC() - ScreenPPI = dc.GetPPI()[1] # Pixel height - del dc - self.HitColorGenerator = None - self.UseHitTest = None - - self.NumBetweenBlits = 500 + self.ComputeFontScale() + self.InitAll() self.BackgroundBrush = wx.Brush(BackgroundColor,wx.SOLID) @@ -1863,22 +2164,55 @@ class FloatCanvas(wx.Panel): wx.EVT_PAINT(self, self.OnPaint) wx.EVT_SIZE(self, self.OnSize) - - wx.EVT_LEFT_DOWN(self, self.LeftDownEvent ) - wx.EVT_LEFT_UP(self, self.LeftUpEvent ) - wx.EVT_LEFT_DCLICK(self, self.LeftDoubleClickEvent ) - wx.EVT_MIDDLE_DOWN(self, self.MiddleDownEvent ) - wx.EVT_MIDDLE_UP(self, self.MiddleUpEvent ) - wx.EVT_MIDDLE_DCLICK(self, self.MiddleDoubleClickEvent ) + + wx.EVT_LEFT_DOWN(self, self.LeftDownEvent) + wx.EVT_LEFT_UP(self, self.LeftUpEvent) + wx.EVT_LEFT_DCLICK(self, self.LeftDoubleClickEvent) + wx.EVT_MIDDLE_DOWN(self, self.MiddleDownEvent) + wx.EVT_MIDDLE_UP(self, self.MiddleUpEvent) + wx.EVT_MIDDLE_DCLICK(self, self.MiddleDoubleClickEvent) wx.EVT_RIGHT_DOWN(self, self.RightDownEvent) - wx.EVT_RIGHT_UP(self, self.RightUpEvent ) - wx.EVT_RIGHT_DCLICK(self, self.RightDoubleCLickEvent ) - wx.EVT_MOTION(self, self.MotionEvent ) - wx.EVT_MOUSEWHEEL(self, self.WheelEvent ) + wx.EVT_RIGHT_UP(self, self.RightUpEvent) + wx.EVT_RIGHT_DCLICK(self, self.RightDoubleCLickEvent) + wx.EVT_MOTION(self, self.MotionEvent) + wx.EVT_MOUSEWHEEL(self, self.WheelEvent) ## CHB: I'm leaving these out for now. - #wx.EVT_ENTER_WINDOW(self, self. ) - #wx.EVT_LEAVE_WINDOW(self, self. ) + #wx.EVT_ENTER_WINDOW(self, self. ) + #wx.EVT_LEAVE_WINDOW(self, self. ) + + self.SetProjectionFun(ProjectionFun) + self.GUIMode = None + + # timer to give a delay when re-sizing so that buffers aren't re-built too many times. + self.SizeTimer = wx.PyTimer(self.OnSizeTimer) + + self.InitializePanel() + self.MakeNewBuffers() + +# self.CreateCursors() + + def ComputeFontScale(self): + ## A global variable to hold the scaling from pixel size to point size. + global FontScale + dc = wx.ScreenDC() + dc.SetFont(wx.Font(16, wx.ROMAN, wx.NORMAL, wx.NORMAL)) + E = dc.GetTextExtent("X") + FontScale = 16/E[1] + del dc + + def InitAll(self): + """ + InitAll() sets everything in the Canvas to default state. + + It can be used to reset the Canvas + + """ + + self.HitColorGenerator = None + self.UseHitTest = False + + self.NumBetweenBlits = 500 ## create the Hit Test Dicts: self.HitDict = None @@ -1889,95 +2223,54 @@ class FloatCanvas(wx.Panel): self._ForegroundBuffer = None self.BoundingBox = None self.BoundingBoxDirty = False - self.ViewPortCenter= array( (0,0), Float) + self.MinScale = None + self.MaxScale = None + self.ViewPortCenter= N.array( (0,0), N.float) - self.SetProjectionFun(ProjectionFun) - - self.MapProjectionVector = array( (1,1), Float) # No Projection to start! - self.TransformVector = array( (1,-1), Float) # default Transformation - - self.Scale = 1 + self.SetProjectionFun(None) - self.GUIMode = None - self.StartRBBox = None - self.PrevRBBox = None - self.StartMove = None - self.PrevMoveXY = None + self.MapProjectionVector = N.array( (1,1), N.float) # No Projection to start! + self.TransformVector = N.array( (1,-1), N.float) # default Transformation + + self.Scale = 1 self.ObjectUnderMouse = None - - # called just to make sure everything is initialized - # this is a bug on OS-X, maybe it's not required? - self.SizeTimer = wx.PyTimer(self.OnSizeTimer) # timer to give a delay when re-sizing so that bufferes aren't re-built too many times. - - self.InitializePanel() - self.MakeNewBuffers() - - self.InHereNum = 0 - self.CreateCursors() - - def CreateCursors(self): - - ## create all the Cursors, so they don't need to be created each time. - ## - if "wxMac" in wx.PlatformInfo: # use 16X16 cursors for wxMac - self.HandCursor = wx.CursorFromImage(Resources.getHand16Image()) - self.GrabHandCursor = wx.CursorFromImage(Resources.getGrabHand16Image()) - - img = Resources.getMagPlus16Image() - img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_X, 6) - img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_Y, 6) - self.MagPlusCursor = wx.CursorFromImage(img) - - img = Resources.getMagMinus16Image() - img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_X, 6) - img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_Y, 6) - self.MagMinusCursor = wx.CursorFromImage(img) - else: # use 24X24 cursors for GTK and Windows - self.HandCursor = wx.CursorFromImage(Resources.getHandImage()) - self.GrabHandCursor = wx.CursorFromImage(Resources.getGrabHandImage()) - - img = Resources.getMagPlusImage() - img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_X, 9) - img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_Y, 9) - self.MagPlusCursor = wx.CursorFromImage(img) - - img = Resources.getMagMinusImage() - img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_X, 9) - img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_Y, 9) - self.MagMinusCursor = wx.CursorFromImage(img) + self.GridUnder = None + self.GridOver = None + + self._BackgroundDirty = True def SetProjectionFun(self,ProjectionFun): if ProjectionFun == 'FlatEarth': - self.ProjectionFun = self.FlatEarthProjection + self.ProjectionFun = self.FlatEarthProjection elif callable(ProjectionFun): - self.ProjectionFun = ProjectionFun + self.ProjectionFun = ProjectionFun elif ProjectionFun is None: - self.ProjectionFun = lambda x=None: array( (1,1), Float) + self.ProjectionFun = lambda x=None: N.array( (1,1), N.float) else: - raise FloatCanvasError('Projectionfun must be either: "FlatEarth", None, or a callable object (function, for instance) that takes the ViewPortCenter and returns a MapProjectionVector') + raise FloatCanvasError('Projectionfun must be either:' + ' "FlatEarth", None, or a callable object ' + '(function, for instance) that takes the ' + 'ViewPortCenter and returns a MapProjectionVector') def FlatEarthProjection(self, CenterPoint): - return array((cos(pi*CenterPoint[1]/180),1),Float) - - def SetMode(self,Mode): - if Mode in ["ZoomIn","ZoomOut","Move","Mouse", None]: - if Mode == "Move": - self.SetCursor(self.HandCursor) - elif Mode == "ZoomIn": - self.SetCursor(self.MagPlusCursor) - elif Mode == "ZoomOut": - self.SetCursor(self.MagMinusCursor) - else: - self.SetCursor(wx.NullCursor) - + MaxLatitude = 75 # these were determined essentially arbitrarily + MinLatitude = -75 + Lat = min(CenterPoint[1],MaxLatitude) + Lat = max(Lat,MinLatitude) + return N.array((N.cos(N.pi*Lat/180),1),N.float) + + def SetMode(self, Mode): + ''' + Set the GUImode to any of the availble mode. + ''' + # Set mode self.GUIMode = Mode - - else: - raise FloatCanvasError('"%s" is Not a valid Mode'%Mode) + #self.GUIMode.SetCursor() + self.SetCursor(self.GUIMode.Cursor) def MakeHitDict(self): - ##fixme: Should this just be None if nothing has been bound? + ##fixme: Should this just be None if nothing has been bound? self.HitDict = {EVT_FC_LEFT_DOWN: {}, EVT_FC_LEFT_UP: {}, EVT_FC_LEFT_DCLICK: {}, @@ -1989,27 +2282,47 @@ class FloatCanvas(wx.Panel): EVT_FC_RIGHT_DCLICK: {}, EVT_FC_ENTER_OBJECT: {}, EVT_FC_LEAVE_OBJECT: {}, - } - + } + def _RaiseMouseEvent(self, Event, EventType): """ This is called in various other places to raise a Mouse Event """ - #print "in Raise Mouse Event", Event pt = self.PixelToWorld( Event.GetPosition() ) evt = _MouseEvent(EventType, Event, self.GetId(), pt) - self.GetEventHandler().ProcessEvent(evt) + self.GetEventHandler().ProcessEvent(evt) + + if wx.__version__ >= "2.8": + HitTestBitmapDepth = 32 + #print "Using hit test code for 2.8" + def GetHitTestColor(self, xy): + if self._ForegroundHTBitmap: + pdata = wx.AlphaPixelData(self._ForegroundHTBitmap) + else: + pdata = wx.AlphaPixelData(self._HTBitmap) + if not pdata: + raise RuntimeError("Trouble Accessing Hit Test bitmap") + pacc = pdata.GetPixels() + pacc.MoveTo(pdata, xy[0], xy[1]) + return pacc.Get()[:3] + else: + HitTestBitmapDepth = 24 + #print "using pre-2.8 hit test code" + def GetHitTestColor(self, xy ): + dc = wx.MemoryDC() + if self._ForegroundHTBitmap: + dc.SelectObject(self._ForegroundHTBitmap) + else: + dc.SelectObject(self._HTBitmap) + hitcolor = dc.GetPixelPoint( xy ) + return hitcolor.Get() def HitTest(self, event, HitEvent): if self.HitDict: # check if there are any objects in the dict for this event if self.HitDict[ HitEvent ]: xy = event.GetPosition() - if self._ForegroundHTdc: - hitcolor = self._ForegroundHTdc.GetPixelPoint( xy ) - else: - hitcolor = self._HTdc.GetPixelPoint( xy ) - color = ( hitcolor.Red(), hitcolor.Green(), hitcolor.Blue() ) + color = self.GetHitTestColor( xy ) if color in self.HitDict[ HitEvent ]: Object = self.HitDict[ HitEvent ][color] ## Add the hit coords to the Object @@ -2021,13 +2334,13 @@ class FloatCanvas(wx.Panel): def MouseOverTest(self, event): ##fixme: Can this be cleaned up? - if self.HitDict: + if (self.HitDict and + + (self.HitDict[EVT_FC_ENTER_OBJECT ] or + self.HitDict[EVT_FC_LEAVE_OBJECT ] ) + ): xy = event.GetPosition() - if self._ForegroundHTdc: - hitcolor = self._ForegroundHTdc.GetPixelPoint( xy ) - else: - hitcolor = self._HTdc.GetPixelPoint( xy ) - color = ( hitcolor.Red(), hitcolor.Green(), hitcolor.Blue() ) + color = self.GetHitTestColor( xy ) OldObject = self.ObjectUnderMouse ObjectCallbackCalled = False if color in self.HitDict[ EVT_FC_ENTER_OBJECT ]: @@ -2068,249 +2381,112 @@ class FloatCanvas(wx.Panel): except KeyError: pass # this means the leave event isn't bound for that object return ObjectCallbackCalled - + return False ## fixme: There is a lot of repeated code here - ## Is there a better way? - def LeftDoubleClickEvent(self,event): - if self.GUIMode == "Mouse": - EventType = EVT_FC_LEFT_DCLICK - if not self.HitTest(event, EventType): - self._RaiseMouseEvent(event, EventType) - - def MiddleDownEvent(self,event): - if self.GUIMode == "Mouse": - EventType = EVT_FC_MIDDLE_DOWN - if not self.HitTest(event, EventType): - self._RaiseMouseEvent(event, EventType) - - def MiddleUpEvent(self,event): - if self.GUIMode == "Mouse": - EventType = EVT_FC_MIDDLE_UP - if not self.HitTest(event, EventType): - self._RaiseMouseEvent(event, EventType) - - def MiddleDoubleClickEvent(self,event): - if self.GUIMode == "Mouse": - EventType = EVT_FC_MIDDLE_DCLICK - if not self.HitTest(event, EventType): - self._RaiseMouseEvent(event, EventType) - - def RightUpEvent(self,event): - if self.GUIMode == "Mouse": - EventType = EVT_FC_RIGHT_UP - if not self.HitTest(event, EventType): - self._RaiseMouseEvent(event, EventType) - - def RightDoubleCLickEvent(self,event): - if self.GUIMode == "Mouse": - EventType = EVT_FC_RIGHT_DCLICK - if not self.HitTest(event, EventType): - self._RaiseMouseEvent(event, EventType) - - def WheelEvent(self,event): - ##if self.GUIMode == "Mouse": - ## Why not always raise this? - self._RaiseMouseEvent(event, EVT_FC_MOUSEWHEEL) - - - def LeftDownEvent(self,event): + ## Is there a better way? + def LeftDoubleClickEvent(self, event): if self.GUIMode: - if self.GUIMode == "ZoomIn": - self.StartRBBox = array( event.GetPosition() ) - self.PrevRBBox = None - self.CaptureMouse() - elif self.GUIMode == "ZoomOut": - Center = self.PixelToWorld( event.GetPosition() ) - self.Zoom(1/1.5,Center) - elif self.GUIMode == "Move": - self.SetCursor(self.GrabHandCursor) - self.StartMove = array( event.GetPosition() ) - self.PrevMoveXY = (0,0) - elif self.GUIMode == "Mouse": - ## check for a hit - if not self.HitTest(event, EVT_FC_LEFT_DOWN): - self._RaiseMouseEvent(event,EVT_FC_LEFT_DOWN) - else: - pass + self.GUIMode.OnLeftDouble(event) + event.Skip() + + def MiddleDownEvent(self, event): + if self.GUIMode: + self.GUIMode.OnMiddleDown(event) + event.Skip() + + def MiddleUpEvent(self, event): + if self.GUIMode: + self.GUIMode.OnMiddleUp(event) + event.Skip() + + def MiddleDoubleClickEvent(self, event): + if self.GUIMode: + self.GUIMode.OnMiddleDouble(event) + event.Skip() - def LeftUpEvent(self,event): + def RightDoubleCLickEvent(self, event): + if self.GUIMode: + self.GUIMode.OnRightDouble(event) + event.Skip() + + def WheelEvent(self, event): + if self.GUIMode: + self.GUIMode.OnWheel(event) + event.Skip() + + def LeftDownEvent(self, event): + if self.GUIMode: + self.GUIMode.OnLeftDown(event) + event.Skip() + + def LeftUpEvent(self, event): if self.HasCapture(): self.ReleaseMouse() if self.GUIMode: - if self.GUIMode == "ZoomIn": - if event.LeftUp() and not self.StartRBBox is None: - self.PrevRBBox = None - EndRBBox = event.GetPosition() - StartRBBox = self.StartRBBox - # if mouse has moved less that ten pixels, don't use the box. - if ( abs(StartRBBox[0] - EndRBBox[0]) > 10 - and abs(StartRBBox[1] - EndRBBox[1]) > 10 ): - EndRBBox = self.PixelToWorld(EndRBBox) - StartRBBox = self.PixelToWorld(StartRBBox) - BB = array(((min(EndRBBox[0],StartRBBox[0]), - min(EndRBBox[1],StartRBBox[1])), - (max(EndRBBox[0],StartRBBox[0]), - max(EndRBBox[1],StartRBBox[1]))),Float) - self.ZoomToBB(BB) - else: - Center = self.PixelToWorld(StartRBBox) - self.Zoom(1.5,Center) - self.StartRBBox = None - elif self.GUIMode == "Move": - self.SetCursor(self.HandCursor) - if self.StartMove is not None: - StartMove = self.StartMove - EndMove = array((event.GetX(),event.GetY())) - if sum((StartMove-EndMove)**2) > 16: - self.MoveImage(StartMove-EndMove,'Pixel') - self.StartMove = None - elif self.GUIMode == "Mouse": - EventType = EVT_FC_LEFT_UP - if not self.HitTest(event, EventType): - self._RaiseMouseEvent(event, EventType) - else: - pass + self.GUIMode.OnLeftUp(event) + event.Skip() - def MotionEvent(self,event): + def MotionEvent(self, event): if self.GUIMode: - if self.GUIMode == "ZoomIn": - if event.Dragging() and event.LeftIsDown() and not (self.StartRBBox is None): - xy0 = self.StartRBBox - xy1 = array( event.GetPosition() ) - wh = abs(xy1 - xy0) - wh[0] = max(wh[0], int(wh[1]*self.AspectRatio)) - wh[1] = int(wh[0] / self.AspectRatio) - xy_c = (xy0 + xy1) / 2 - dc = wx.ClientDC(self) - dc.BeginDrawing() - dc.SetPen(wx.Pen('WHITE', 2, wx.SHORT_DASH)) - dc.SetBrush(wx.TRANSPARENT_BRUSH) - dc.SetLogicalFunction(wx.XOR) - if self.PrevRBBox: - dc.DrawRectanglePointSize(*self.PrevRBBox) - self.PrevRBBox = ( xy_c - wh/2, wh ) - dc.DrawRectanglePointSize( *self.PrevRBBox ) - dc.EndDrawing() - elif self.GUIMode == "Move": - if event.Dragging() and event.LeftIsDown() and not self.StartMove is None: - xy1 = array( event.GetPosition() ) - wh = self.PanelSize - xy_tl = xy1 - self.StartMove - dc = wx.ClientDC(self) - dc.BeginDrawing() - x1,y1 = self.PrevMoveXY - x2,y2 = xy_tl - w,h = self.PanelSize - ##fixme: This sure could be cleaner! - if x2 > x1 and y2 > y1: - xa = xb = x1 - ya = yb = y1 - wa = w - ha = y2 - y1 - wb = x2- x1 - hb = h - elif x2 > x1 and y2 <= y1: - xa = x1 - ya = y1 - wa = x2 - x1 - ha = h - xb = x1 - yb = y2 + h - wb = w - hb = y1 - y2 - elif x2 <= x1 and y2 > y1: - xa = x1 - ya = y1 - wa = w - ha = y2 - y1 - xb = x2 + w - yb = y1 - wb = x1 - x2 - hb = h - y2 + y1 - elif x2 <= x1 and y2 <= y1: - xa = x2 + w - ya = y1 - wa = x1 - x2 - ha = h - xb = x1 - yb = y2 + h - wb = w - hb = y1 - y2 - - dc.SetPen(wx.TRANSPARENT_PEN) - dc.SetBrush(self.BackgroundBrush) - dc.DrawRectangle(xa, ya, wa, ha) - dc.DrawRectangle(xb, yb, wb, hb) - self.PrevMoveXY = xy_tl - if self._ForeDrawList: - ##if self._ForegroundBuffer: - dc.DrawBitmapPoint(self._ForegroundBuffer,xy_tl) - else: - dc.DrawBitmapPoint(self._Buffer,xy_tl) - dc.EndDrawing() - elif self.GUIMode == "Mouse": - ## Only do something if there are mouse over events bound - if self.HitDict and (self.HitDict[ EVT_FC_ENTER_OBJECT ] or self.HitDict[ EVT_FC_LEAVE_OBJECT ] ): - if not self.MouseOverTest(event): - self._RaiseMouseEvent(event,EVT_FC_MOTION) - else: - pass - self._RaiseMouseEvent(event,EVT_FC_MOTION) - else: - pass + self.GUIMode.OnMove(event) + event.Skip() - def RightDownEvent(self,event): + def RightDownEvent(self, event): if self.GUIMode: - if self.GUIMode == "ZoomIn": - Center = self.PixelToWorld((event.GetX(),event.GetY())) - self.Zoom(1/1.5,Center) - elif self.GUIMode == "ZoomOut": - Center = self.PixelToWorld((event.GetX(),event.GetY())) - self.Zoom(1.5,Center) - elif self.GUIMode == "Mouse": - EventType = EVT_FC_RIGHT_DOWN - if not self.HitTest(event, EventType): - self._RaiseMouseEvent(event, EventType) - else: - pass - + self.GUIMode.OnRightDown(event) + event.Skip() + + def RightUpEvent(self, event): + if self.GUIMode: + self.GUIMode.OnRightUp(event) + event.Skip() + def MakeNewBuffers(self): self._BackgroundDirty = True # Make new offscreen bitmap: self._Buffer = wx.EmptyBitmap(*self.PanelSize) - #dc = wx.MemoryDC() - #dc.SelectObject(self._Buffer) - #dc.Clear() if self._ForeDrawList: self._ForegroundBuffer = wx.EmptyBitmap(*self.PanelSize) + if self.UseHitTest: + self.MakeNewHTBitmap() + else: + self._ForegroundHTBitmap = None else: self._ForegroundBuffer = None + self._ForegroundHTBitmap = None + if self.UseHitTest: - self.MakeNewHTdc() + self.MakeNewHTBitmap() else: - self._HTdc = None - self._ForegroundHTdc = None - - def MakeNewHTdc(self): - ## Note: While it's considered a "bad idea" to keep a - ## MemoryDC around I'm doing it here because a wx.Bitmap - ## doesn't have a GetPixel method so a DC is needed to do - ## the hit-test. It didn't seem like a good idea to re-create - ## a wx.MemoryDC on every single mouse event, so I keep it - ## around instead - self._HTdc = wx.MemoryDC() - self._HTBitmap = wx.EmptyBitmap(*self.PanelSize) - self._HTdc.SelectObject( self._HTBitmap ) - self._HTdc.SetBackground(wx.BLACK_BRUSH) - if self._ForeDrawList: - self._ForegroundHTdc = wx.MemoryDC() - self._ForegroundHTBitmap = wx.EmptyBitmap(*self.PanelSize) - self._ForegroundHTdc.SelectObject( self._ForegroundHTBitmap ) - self._ForegroundHTdc.SetBackground(wx.BLACK_BRUSH) - else: - self._ForegroundHTdc = None - + self._HTBitmap = None + self._ForegroundHTBitmap = None + + def MakeNewHTBitmap(self): + """ + Off screen Bitmap used for Hit tests on background objects + + """ + self._HTBitmap = wx.EmptyBitmap(self.PanelSize[0], + + self.PanelSize[1], + + depth=self.HitTestBitmapDepth) + + def MakeNewForegroundHTBitmap(self): + ## Note: the foreground and backround HT bitmaps are in separate functions + ## so that they can be created separate --i.e. when a foreground is + ## added after the backgound is drawn + """ + Off screen Bitmap used for Hit tests on foreground objects + + """ + self._ForegroundHTBitmap = wx.EmptyBitmap(self.PanelSize[0], + + self.PanelSize[1], + + depth=self.HitTestBitmapDepth) + def OnSize(self, event=None): self.InitializePanel() self.SizeTimer.Start(50, oneShot=True) @@ -2324,13 +2500,13 @@ class FloatCanvas(wx.Panel): if self.PanelSize == (0,0): ## OS-X sometimes gives a Size event when the panel is size (0,0) self.PanelSize = (2,2) - self.PanelSize = array(self.PanelSize, Int32) + self.PanelSize = N.array(self.PanelSize, N.int32) self.HalfPanelSize = self.PanelSize / 2 # lrk: added for speed in WorldToPixel if self.PanelSize[0] == 0 or self.PanelSize[1] == 0: self.AspectRatio = 1.0 else: self.AspectRatio = float(self.PanelSize[0]) / self.PanelSize[1] - + def OnPaint(self, event): dc = wx.PaintDC(self) if self._ForegroundBuffer: @@ -2340,6 +2516,18 @@ class FloatCanvas(wx.Panel): def Draw(self, Force=False): """ + + Canvas.Draw(Force=False) + + Re-draws the canvas. + + Note that the buffer will not be re-drawn unless something has + changed. If you change a DrawObject directly, then the canvas + will not know anything has changed. In this case, you can force + a re-draw by passing int True for the Force flag: + + Canvas.Draw(Force=True) + There is a main buffer set up to double buffer the screen, so you can get quick re-draws when the window gets uncovered. @@ -2351,26 +2539,37 @@ class FloatCanvas(wx.Panel): changing on the foreground, without having to wait for the background to get re-drawn. This can be used to support simple animation, for instance. - + """ - if sometrue(self.PanelSize <= 2 ): # it's possible for this to get called before being properly initialized. + + if N.sometrue(self.PanelSize <= 2 ): + # it's possible for this to get called before being properly initialized. return if self.Debug: start = clock() ScreenDC = wx.ClientDC(self) - ViewPortWorld = ( self.PixelToWorld((0,0)), - self.PixelToWorld(self.PanelSize) ) - ViewPortBB = array( ( minimum.reduce(ViewPortWorld), - maximum.reduce(ViewPortWorld) ) ) + ViewPortWorld = N.array(( self.PixelToWorld((0,0)), + self.PixelToWorld(self.PanelSize) ) + ) + self.ViewPortBB = N.array( ( N.minimum.reduce(ViewPortWorld), + N.maximum.reduce(ViewPortWorld) ) ) + #self.ViewPortWorld = ViewPortWorld + dc = wx.MemoryDC() dc.SelectObject(self._Buffer) if self._BackgroundDirty or Force: - #print "Background is Dirty" dc.SetBackground(self.BackgroundBrush) dc.Clear() - if self._HTdc: - self._HTdc.Clear() - self._DrawObjects(dc, self._DrawList, ScreenDC, ViewPortBB, self._HTdc) + if self._HTBitmap is not None: + HTdc = wx.MemoryDC() + HTdc.SelectObject(self._HTBitmap) + HTdc.Clear() + else: + HTdc = None + if self.GridUnder is not None: + self.GridUnder._Draw(dc, self) + self._DrawObjects(dc, self._DrawList, ScreenDC, self.ViewPortBB, HTdc) self._BackgroundDirty = False + del HTdc if self._ForeDrawList: ## If an object was just added to the Foreground, there might not yet be a buffer @@ -2381,62 +2580,56 @@ class FloatCanvas(wx.Panel): dc = wx.MemoryDC() ## I got some strange errors (linewidths wrong) if I didn't make a new DC here dc.SelectObject(self._ForegroundBuffer) dc.DrawBitmap(self._Buffer,0,0) - if self._ForegroundHTdc is None: - self._ForegroundHTdc = wx.MemoryDC() - self._ForegroundHTdc.SelectObject( wx.EmptyBitmap( - self.PanelSize[0], - self.PanelSize[1]) ) - if self._HTdc: - ## blit the background HT buffer to the foreground HT buffer - self._ForegroundHTdc.Blit(0, 0, - self.PanelSize[0], self.PanelSize[1], - self._HTdc, 0, 0) + if self._ForegroundHTBitmap is not None: + ForegroundHTdc = wx.MemoryDC() + ForegroundHTdc.SelectObject( self._ForegroundHTBitmap) + ForegroundHTdc.Clear() + if self._HTBitmap is not None: + #Draw the background HT buffer to the foreground HT buffer + ForegroundHTdc.DrawBitmap(self._HTBitmap, 0, 0) + else: + ForegroundHTdc = None self._DrawObjects(dc, self._ForeDrawList, ScreenDC, - ViewPortBB, - self._ForegroundHTdc) + self.ViewPortBB, + ForegroundHTdc) + if self.GridOver is not None: + self.GridOver._Draw(dc, self) ScreenDC.Blit(0, 0, self.PanelSize[0],self.PanelSize[1], dc, 0, 0) - # If the canvas is in the middle of a zoom or move, the Rubber Band box needs to be re-drawn - # This seeems out of place, but it works. - if self.PrevRBBox: - ScreenDC.SetPen(wx.Pen('WHITE', 2,wx.SHORT_DASH)) - ScreenDC.SetBrush(wx.TRANSPARENT_BRUSH) - ScreenDC.SetLogicalFunction(wx.XOR) - ScreenDC.DrawRectanglePointSize(*self.PrevRBBox) + # If the canvas is in the middle of a zoom or move, + # the Rubber Band box needs to be re-drawn + ##fixme: maybe GUIModes should never be None, and rather have a Do-nothing GUI-Mode. + if self.GUIMode is not None: + self.GUIMode.UpdateScreen() + if self.Debug: print "Drawing took %f seconds of CPU time"%(clock()-start) - ## Clear the font cache - ## IF you don't do this, the X font server starts to take up Massive amounts of memory - ## This is mostly a problem with very large fonts, that you get with scaled text when zoomed in. + ## Clear the font cache. If you don't do this, the X font server + ## starts to take up Massive amounts of memory This is mostly a + ## problem with very large fonts, that you get with scaled text + ## when zoomed in. DrawObject.FontList = {} def _ShouldRedraw(DrawList, ViewPortBB): # lrk: adapted code from BBCheck # lrk: Returns the objects that should be redrawn + ## fixme: should this check be moved into the object? + ## also: a BB object would make this cleaner too BB2 = ViewPortBB redrawlist = [] for Object in DrawList: BB1 = Object.BoundingBox + ## note: this could use the Utilities.BBCheck function + ## butthis saves a function call if (BB1[1,0] > BB2[0,0] and BB1[0,0] < BB2[1,0] and BB1[1,1] > BB2[0,1] and BB1[0,1] < BB2[1,1]): redrawlist.append(Object) - return redrawlist + #return redrawlist + ##fixme: disabled this!!!! + return redrawlist _ShouldRedraw = staticmethod(_ShouldRedraw) - -## def BBCheck(self, BB1, BB2): -## """ - -## BBCheck(BB1, BB2) returns True is the Bounding boxes intesect, False otherwise - -## """ -## if ( (BB1[1,0] > BB2[0,0]) and (BB1[0,0] < BB2[1,0]) and -## (BB1[1,1] > BB2[0,1]) and (BB1[0,1] < BB2[1,1]) ): -## return True -## else: -## return False - def MoveImage(self,shift,CoordType): """ move the image in the window. @@ -2455,44 +2648,46 @@ class FloatCanvas(wx.Panel): in Floating point world coordinates """ - - shift = asarray(shift,Float) - #print "shifting by:", shift + shift = N.asarray(shift,N.float) if CoordType == 'Panel':# convert from panel coordinates - shift = shift * array((-1,1),Float) *self.PanelSize/self.TransformVector + shift = shift * N.array((-1,1),N.float) *self.PanelSize/self.TransformVector elif CoordType == 'Pixel': # convert from pixel coordinates shift = shift/self.TransformVector elif CoordType == 'World': # No conversion pass else: raise FloatCanvasError('CoordType must be either "Panel", "Pixel", or "World"') - - self.ViewPortCenter = self.ViewPortCenter + shift + + self.ViewPortCenter = self.ViewPortCenter + shift self.MapProjectionVector = self.ProjectionFun(self.ViewPortCenter) - self.TransformVector = array((self.Scale,-self.Scale),Float) * self.MapProjectionVector + self.TransformVector = N.array((self.Scale,-self.Scale),N.float) * self.MapProjectionVector self._BackgroundDirty = True self.Draw() - def Zoom(self,factor,center = None): - + def Zoom(self, factor, center = None, centerCoords="world"): + """ Zoom(factor, center) changes the amount of zoom of the image by factor. If factor is greater than one, the image gets larger. If factor is less than one, the image gets smaller. - - Center is a tuple of (x,y) coordinates of the center of the viewport, after zooming. + + center is a tuple of (x,y) coordinates of the center of the viewport, after zooming. If center is not given, the center will stay the same. + + centerCoords is a flag indicating whether the center given is in pixel or world + coords. Options are: "world" or "pixel" """ self.Scale = self.Scale*factor if not center is None: - self.ViewPortCenter = array(center,Float) - self.MapProjectionVector = self.ProjectionFun(self.ViewPortCenter) - self.TransformVector = array((self.Scale,-self.Scale),Float) * self.MapProjectionVector - self._BackgroundDirty = True - self.Draw() - - def ZoomToBB(self, NewBB = None, DrawFlag = True): + if centerCoords == "pixel": + center = self.PixelToWorld( center ) + else: + center = N.array(center,N.float) + self.ViewPortCenter = center + self.SetToNewScale() + + def ZoomToBB(self, NewBB=None, DrawFlag=True): """ @@ -2501,18 +2696,18 @@ class FloatCanvas(wx.Panel): """ - if not NewBB is None: + if NewBB is not None: BoundingBox = NewBB else: if self.BoundingBoxDirty: self._ResetBoundingBox() BoundingBox = self.BoundingBox - if not BoundingBox is None: - self.ViewPortCenter = array(((BoundingBox[0,0]+BoundingBox[1,0])/2, - (BoundingBox[0,1]+BoundingBox[1,1])/2 ),Float) + if BoundingBox is not None: + self.ViewPortCenter = N.array(((BoundingBox[0,0]+BoundingBox[1,0])/2, + (BoundingBox[0,1]+BoundingBox[1,1])/2 ),N.float_) self.MapProjectionVector = self.ProjectionFun(self.ViewPortCenter) # Compute the new Scale - BoundingBox = BoundingBox * self.MapProjectionVector + BoundingBox = BoundingBox*self.MapProjectionVector # this does need to make a copy! try: self.Scale = min(abs(self.PanelSize[0] / (BoundingBox[1,0]-BoundingBox[0,0])), abs(self.PanelSize[1] / (BoundingBox[1,1]-BoundingBox[0,1])) )*0.95 @@ -2524,23 +2719,33 @@ class FloatCanvas(wx.Panel): self.Scale = (self.PanelSize[1] / (BoundingBox[1,1]-BoundingBox[0,1]))*0.95 except ZeroDivisionError: #zero size! (must be a single point) self.Scale = 1 - - self.TransformVector = array((self.Scale,-self.Scale),Float)* self.MapProjectionVector + if DrawFlag: self._BackgroundDirty = True - self.Draw() else: # Reset the shifting and scaling to defaults when there is no BB - self.ViewPortCenter= array( (0,0), Float) - self.MapProjectionVector = array( (1,1), Float) # No Projection to start! - self.TransformVector = array( (1,-1), Float) # default Transformation - self.Scale = 1 - + self.ViewPortCenter= N.array( (0,0), N.float) + self.Scale= 1 + self.SetToNewScale(DrawFlag=DrawFlag) + + def SetToNewScale(self, DrawFlag=True): + Scale = self.Scale + if self.MinScale is not None: + Scale = max(Scale, self.MinScale) + if self.MaxScale is not None: + Scale = min(Scale, self.MaxScale) + self.MapProjectionVector = self.ProjectionFun(self.ViewPortCenter) + self.TransformVector = N.array((Scale,-Scale),N.float) * self.MapProjectionVector + self.Scale = Scale + self._BackgroundDirty = True + if DrawFlag: + self.Draw() + def RemoveObjects(self, Objects): for Object in Objects: - self.RemoveObject(Object, ResetBB = False) + self.RemoveObject(Object, ResetBB=False) self.BoundingBoxDirty = True - + def RemoveObject(self, Object, ResetBB = True): ##fixme: Using the list.remove method is kind of slow if Object.InForeground: @@ -2554,7 +2759,15 @@ class FloatCanvas(wx.Panel): if ResetBB: self.BoundingBoxDirty = True - def ClearAll(self, ResetBB = True): + def ClearAll(self, ResetBB=True): + """ + ClearAll(ResetBB=True) + + Removes all DrawObjects from the Canvas + + If ResetBB is set to False, the original bounding box will remain + + """ self._DrawList = [] self._ForeDrawList = [] self._BackgroundDirty = True @@ -2565,52 +2778,34 @@ class FloatCanvas(wx.Panel): self.MakeNewBuffers() self.HitDict = None -## No longer called -## def _AddBoundingBox(self,NewBB): -## if self.BoundingBox is None: -## self.BoundingBox = NewBB -## self.ZoomToBB(NewBB,DrawFlag = False) -## else: -## self.BoundingBox = array( ( (min(self.BoundingBox[0,0],NewBB[0,0]), -## min(self.BoundingBox[0,1],NewBB[0,1])), -## (max(self.BoundingBox[1,0],NewBB[1,0]), -## max(self.BoundingBox[1,1],NewBB[1,1]))), -## Float) - - def _getboundingbox(bboxarray): # lrk: added this - - upperleft = minimum.reduce(bboxarray[:,0]) - lowerright = maximum.reduce(bboxarray[:,1]) - return array((upperleft, lowerright), Float) - - _getboundingbox = staticmethod(_getboundingbox) - def _ResetBoundingBox(self): if self._DrawList or self._ForeDrawList: - bboxarray = zeros((len(self._DrawList)+len(self._ForeDrawList), 2, 2),Float) - i = -1 # just in case _DrawList is empty - for (i, BB) in enumerate(self._DrawList): - bboxarray[i] = BB.BoundingBox - for (j, BB) in enumerate(self._ForeDrawList): - bboxarray[i+j+1] = BB.BoundingBox - self.BoundingBox = self._getboundingbox(bboxarray) + bblist = [] + for (i, obj) in enumerate(self._DrawList): + bblist.append(obj.BoundingBox) + for (j, obj) in enumerate(self._ForeDrawList): + bblist.append(obj.BoundingBox) + self.BoundingBox = BBox.fromBBArray(bblist) else: self.BoundingBox = None - self.ViewPortCenter= array( (0,0), Float) - self.TransformVector = array( (1,-1), Float) - self.MapProjectionVector = array( (1,1), Float) - self.Scale = 1 + self.ViewPortCenter= N.array( (0,0), N.float) + self.TransformVector = N.array( (1,-1), N.float) + self.MapProjectionVector = N.array( (1,1), N.float) + self.Scale = 1 self.BoundingBoxDirty = False - def PixelToWorld(self,Points): + def PixelToWorld(self, Points): """ Converts coordinates from Pixel coordinates to world coordinates. - - Points is a tuple of (x,y) coordinates, or a list of such tuples, or a NX2 Numpy array of x,y coordinates. - + + Points is a tuple of (x,y) coordinates, or a list of such tuples, + or a NX2 Numpy array of x,y coordinates. + """ - return (((asarray(Points,Float) - (self.PanelSize/2))/self.TransformVector) + self.ViewPortCenter) - + return (((N.asarray(Points, N.float) - + (self.PanelSize/2))/self.TransformVector) + + self.ViewPortCenter) + def WorldToPixel(self,Coordinates): """ This function will get passed to the drawing functions of the objects, @@ -2618,8 +2813,8 @@ class FloatCanvas(wx.Panel): Coordinates should be a NX2 array of (x,y) coordinates, or a 2-tuple, or sequence of 2-tuples. """ - #Note: this can be called by users code for various reasons, so asarray is needed. - return (((asarray(Coordinates,Float) - + #Note: this can be called by users code for various reasons, so N.asarray is needed. + return (((N.asarray(Coordinates,N.float) - self.ViewPortCenter)*self.TransformVector)+ (self.HalfPanelSize)).astype('i') @@ -2627,24 +2822,24 @@ class FloatCanvas(wx.Panel): """ This function will get passed to the drawing functions of the objects, to Change a length from world to pixel coordinates. - + Lengths should be a NX2 array of (x,y) coordinates, or a 2-tuple, or sequence of 2-tuples. """ - return ( (asarray(Lengths,Float)*self.TransformVector) ).astype('i') + return ( (N.asarray(Lengths, N.float)*self.TransformVector) ).astype('i') def ScalePixelToWorld(self,Lengths): """ This function computes a pair of x.y lengths, to change then from pixel to world coordinates. - + Lengths should be a NX2 array of (x,y) coordinates, or a 2-tuple, or sequence of 2-tuples. """ - return (asarray(Lengths,Float) / self.TransformVector) - - def AddObject(self,obj): + return (N.asarray(Lengths,N.float) / self.TransformVector) + + def AddObject(self, obj): # put in a reference to the Canvas, so remove and other stuff can work obj._Canvas = self if obj.InForeground: @@ -2656,11 +2851,15 @@ class FloatCanvas(wx.Panel): self.BoundingBoxDirty = True return True + def AddObjects(self, Objects): + for Object in Objects: + self.AddObject(Object) + def _DrawObjects(self, dc, DrawList, ScreenDC, ViewPortBB, HTdc = None): """ This is a convenience function; This function takes the list of objects and draws them to specified - device context. + device context. """ dc.SetBackground(self.BackgroundBrush) dc.BeginDrawing() @@ -2679,24 +2878,26 @@ class FloatCanvas(wx.Panel): def SaveAsImage(self, filename, ImageType=wx.BITMAP_TYPE_PNG): """ - + Saves the current image as an image file. The default is in the - PNG format. Other formats can be spcified using the wx flags: + PNG format. Other formats can be specified using the wx flags: + wx.BITMAP_TYPE_PNG + wx.BITMAP_TYPE_JPG wx.BITMAP_TYPE_BMP wx.BITMAP_TYPE_XBM wx.BITMAP_TYPE_XPM etc. (see the wx docs for the complete list) """ - + self._Buffer.SaveFile(filename, ImageType) def _makeFloatCanvasAddMethods(): ## lrk's code for doing this in module __init__ classnames = ["Circle", "Ellipse", "Rectangle", "ScaledText", "Polygon", - "Line", "Text", "PointSet","Point", "Arrow","ScaledTextBox", - "SquarePoint","Bitmap", "ScaledBitmap"] + "Line", "Text", "PointSet","Point", "Arrow", "ArrowLine", "ScaledTextBox", + "SquarePoint","Bitmap", "ScaledBitmap", "Spline", "Group"] for classname in classnames: klass = globals()[classname] def getaddshapemethod(klass=klass): @@ -2714,6 +2915,6 @@ def _makeFloatCanvasAddMethods(): ## lrk's code for doing this in module __init_ docstring += ", whose docstring is:\n%s" % klass.__doc__ FloatCanvas.__dict__[methodname].__doc__ = docstring -_makeFloatCanvasAddMethods() +_makeFloatCanvasAddMethods() diff --git a/wxPython/wx/lib/floatcanvas/GUIMode.py b/wxPython/wx/lib/floatcanvas/GUIMode.py new file mode 100644 index 0000000000..08ca47343f --- /dev/null +++ b/wxPython/wx/lib/floatcanvas/GUIMode.py @@ -0,0 +1,347 @@ +""" + +Module that holds the GUI modes used by FloatCanvas + + +Note that this can only be imported after a wx.App() has been created. + +This approach was inpired by Christian Blouin, who also wrote the initial +version of the code. + +""" + +import wx +## fixme: events should live in their own module, so all of FloatCanvas +## wouldn't have to be imported here. +import FloatCanvas, Resources +import numpy as N + +## create all the Cursors, so they don't need to be created each time. +if "wxMac" in wx.PlatformInfo: # use 16X16 cursors for wxMac + HandCursor = wx.CursorFromImage(Resources.getHand16Image()) + GrabHandCursor = wx.CursorFromImage(Resources.getGrabHand16Image()) + + img = Resources.getMagPlus16Image() + img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_X, 6) + img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_Y, 6) + MagPlusCursor = wx.CursorFromImage(img) + + img = Resources.getMagMinus16Image() + img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_X, 6) + img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_Y, 6) + MagMinusCursor = wx.CursorFromImage(img) +else: # use 24X24 cursors for GTK and Windows + HandCursor = wx.CursorFromImage(Resources.getHandImage()) + GrabHandCursor = wx.CursorFromImage(Resources.getGrabHandImage()) + + img = Resources.getMagPlusImage() + img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_X, 9) + img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_Y, 9) + MagPlusCursor = wx.CursorFromImage(img) + + img = Resources.getMagMinusImage() + img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_X, 9) + img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_Y, 9) + MagMinusCursor = wx.CursorFromImage(img) + + +class GUIBase: + """ + Basic Mouse mode and baseclass for other GUImode. + + This one does nothing with any event + + """ + def __init__(self, parent): + self.parent = parent + + Cursor = wx.NullCursor + + # Handlers + def OnLeftDown(self, event): + pass + def OnLeftUp(self, event): + pass + def OnLeftDouble(self, event): + pass + def OnRightDown(self, event): + pass + def OnRightUp(self, event): + pass + def OnRightDouble(self, event): + pass + def OnMiddleDown(self, event): + pass + def OnMiddleUp(self, event): + pass + def OnMiddleDouble(self, event): + pass + def OnWheel(self, event): + pass + def OnMove(self, event): + pass + + def UpdateScreen(self): + """ + Update gets called if the screen has been repainted in the middle of a zoom in + so the Rubber Band Box can get updated + """ + pass + +class GUIMouse(GUIBase): + """ + + Mouse mode checks for a hit test, and if nothing is hit, + raises a FloatCanvas mouse event for each event. + + """ + + Cursor = wx.NullCursor + + # Handlers + def OnLeftDown(self, event): + EventType = FloatCanvas.EVT_FC_LEFT_DOWN + if not self.parent.HitTest(event, EventType): + self.parent._RaiseMouseEvent(event, EventType) + + def OnLeftUp(self, event): + EventType = FloatCanvas.EVT_FC_LEFT_UP + if not self.parent.HitTest(event, EventType): + self.parent._RaiseMouseEvent(event, EventType) + + def OnLeftDouble(self, event): + EventType = FloatCanvas.EVT_FC_LEFT_DCLICK + if not self.parent.HitTest(event, EventType): + self.parent._RaiseMouseEvent(event, EventType) + + def OnMiddleDown(self, event): + EventType = FloatCanvas.EVT_FC_MIDDLE_DOWN + if not self.parent.HitTest(event, EventType): + self.parent._RaiseMouseEvent(event, EventType) + + def OnMiddleUp(self, event): + EventType = FloatCanvas.EVT_FC_MIDDLE_UP + if not self.parent.HitTest(event, EventType): + self.parent._RaiseMouseEvent(event, EventType) + + def OnMiddleDouble(self, event): + EventType = FloatCanvas.EVT_FC_MIDDLE_DCLICK + if not self.parent.HitTest(event, EventType): + self.parent._RaiseMouseEvent(event, EventType) + + def OnRightDown(self, event): + EventType = FloatCanvas.EVT_FC_RIGHT_DOWN + if not self.parent.HitTest(event, EventType): + self.parent._RaiseMouseEvent(event, EventType) + + def OnRightUp(self, event): + EventType = FloatCanvas.EVT_FC_RIGHT_UP + if not self.parent.HitTest(event, EventType): + self.parent._RaiseMouseEvent(event, EventType) + + def OnRightDouble(self, event): + EventType = FloatCanvas.EVT_FC_RIGHT_DCLICK + if not self.parent.HitTest(event, EventType): + self.parent._RaiseMouseEvent(event, EventType) + + def OnWheel(self, event): + EventType = FloatCanvas.EVT_FC_MOUSEWHEEL + self.parent._RaiseMouseEvent(event, EventType) + + def OnMove(self, event): + ## The Move event always gets raised, even if there is a hit-test + self.parent.MouseOverTest(event) + self.parent._RaiseMouseEvent(event,FloatCanvas.EVT_FC_MOTION) + + +class GUIMove(GUIBase): + + Cursor = HandCursor + GrabCursor = GrabHandCursor + def __init__(self, parent): + GUIBase.__init__(self, parent) + self.StartMove = None + self.PrevMoveXY = None + + def OnLeftDown(self, event): + self.parent.SetCursor(self.GrabCursor) + self.parent.CaptureMouse() + self.StartMove = N.array( event.GetPosition() ) + self.PrevMoveXY = (0,0) + + def OnLeftUp(self, event): + if self.StartMove is not None: + StartMove = self.StartMove + EndMove = N.array(event.GetPosition()) + DiffMove = StartMove-EndMove + if N.sum(DiffMove**2) > 16: + self.parent.MoveImage(DiffMove, 'Pixel') + self.StartMove = None + self.parent.SetCursor(self.Cursor) + + def OnMove(self, event): + # Allways raise the Move event. + self.parent._RaiseMouseEvent(event,FloatCanvas.EVT_FC_MOTION) + if event.Dragging() and event.LeftIsDown() and not self.StartMove is None: + xy1 = N.array( event.GetPosition() ) + wh = self.parent.PanelSize + xy_tl = xy1 - self.StartMove + dc = wx.ClientDC(self.parent) + dc.BeginDrawing() + x1,y1 = self.PrevMoveXY + x2,y2 = xy_tl + w,h = self.parent.PanelSize + ##fixme: This sure could be cleaner! + if x2 > x1 and y2 > y1: + xa = xb = x1 + ya = yb = y1 + wa = w + ha = y2 - y1 + wb = x2- x1 + hb = h + elif x2 > x1 and y2 <= y1: + xa = x1 + ya = y1 + wa = x2 - x1 + ha = h + xb = x1 + yb = y2 + h + wb = w + hb = y1 - y2 + elif x2 <= x1 and y2 > y1: + xa = x1 + ya = y1 + wa = w + ha = y2 - y1 + xb = x2 + w + yb = y1 + wb = x1 - x2 + hb = h - y2 + y1 + elif x2 <= x1 and y2 <= y1: + xa = x2 + w + ya = y1 + wa = x1 - x2 + ha = h + xb = x1 + yb = y2 + h + wb = w + hb = y1 - y2 + + dc.SetPen(wx.TRANSPARENT_PEN) + dc.SetBrush(self.parent.BackgroundBrush) + dc.DrawRectangle(xa, ya, wa, ha) + dc.DrawRectangle(xb, yb, wb, hb) + self.PrevMoveXY = xy_tl + if self.parent._ForeDrawList: + dc.DrawBitmapPoint(self.parent._ForegroundBuffer,xy_tl) + else: + dc.DrawBitmapPoint(self.parent._Buffer,xy_tl) + dc.EndDrawing() + + def OnWheel(self, event): + """ + By default, zoom in/out by a 0.1 factor per Wheel event. + """ + if event.GetWheelRotation() < 0: + self.parent.Zoom(0.9) + else: + self.parent.Zoom(1.1) + +class GUIZoomIn(GUIBase): + + Cursor = MagPlusCursor + + def __init__(self, parent): + GUIBase.__init__(self, parent) + self.StartRBBox = None + self.PrevRBBox = None + + def OnLeftDown(self, event): + self.StartRBBox = N.array( event.GetPosition() ) + self.PrevRBBox = None + self.parent.CaptureMouse() + + def OnLeftUp(self, event): + #if self.parent.HasCapture(): + # self.parent.ReleaseMouse() + if event.LeftUp() and not self.StartRBBox is None: + self.PrevRBBox = None + EndRBBox = event.GetPosition() + StartRBBox = self.StartRBBox + # if mouse has moved less that ten pixels, don't use the box. + if ( abs(StartRBBox[0] - EndRBBox[0]) > 10 + and abs(StartRBBox[1] - EndRBBox[1]) > 10 ): + EndRBBox = self.parent.PixelToWorld(EndRBBox) + StartRBBox = self.parent.PixelToWorld(StartRBBox) + BB = N.array(((min(EndRBBox[0],StartRBBox[0]), + min(EndRBBox[1],StartRBBox[1])), + (max(EndRBBox[0],StartRBBox[0]), + max(EndRBBox[1],StartRBBox[1]))),N.float_) + self.parent.ZoomToBB(BB) + else: + Center = self.parent.PixelToWorld(StartRBBox) + self.parent.Zoom(1.5,Center) + self.StartRBBox = None + + def OnMove(self, event): + # Allways raise the Move event. + self.parent._RaiseMouseEvent(event,FloatCanvas.EVT_FC_MOTION) + if event.Dragging() and event.LeftIsDown() and not (self.StartRBBox is None): + xy0 = self.StartRBBox + xy1 = N.array( event.GetPosition() ) + wh = abs(xy1 - xy0) + wh[0] = max(wh[0], int(wh[1]*self.parent.AspectRatio)) + wh[1] = int(wh[0] / self.parent.AspectRatio) + xy_c = (xy0 + xy1) / 2 + dc = wx.ClientDC(self.parent) + dc.BeginDrawing() + dc.SetPen(wx.Pen('WHITE', 2, wx.SHORT_DASH)) + dc.SetBrush(wx.TRANSPARENT_BRUSH) + dc.SetLogicalFunction(wx.XOR) + if self.PrevRBBox: + dc.DrawRectanglePointSize(*self.PrevRBBox) + self.PrevRBBox = ( xy_c - wh/2, wh ) + dc.DrawRectanglePointSize( *self.PrevRBBox ) + dc.EndDrawing() + + def UpdateScreen(self): + """ + Update gets called if the screen has been repainted in the middle of a zoom in + so the Rubber Band Box can get updated + """ + if self.PrevRBBox is not None: + dc = wx.ClientDC(self.parent) + dc.SetPen(wx.Pen('WHITE', 2, wx.SHORT_DASH)) + dc.SetBrush(wx.TRANSPARENT_BRUSH) + dc.SetLogicalFunction(wx.XOR) + dc.DrawRectanglePointSize(*self.PrevRBBox) + + def OnRightDown(self, event): + self.parent.Zoom(1/1.5, event.GetPosition(), centerCoords="pixel") + + def OnWheel(self, event): + if event.GetWheelRotation() < 0: + self.parent.Zoom(0.9) + else: + self.parent.Zoom(1.1) + +class GUIZoomOut(GUIBase): + + Cursor = MagMinusCursor + + def OnLeftDown(self, event): + self.parent.Zoom(1/1.5, event.GetPosition(), centerCoords="pixel") + + def OnRightDown(self, event): + self.parent.Zoom(1.5, event.GetPosition(), centerCoords="pixel") + + def OnWheel(self, event): + if event.GetWheelRotation() < 0: + self.parent.Zoom(0.9) + else: + self.parent.Zoom(1.1) + + def OnMove(self, event): + # Allways raise the Move event. + self.parent._RaiseMouseEvent(event,FloatCanvas.EVT_FC_MOTION) + diff --git a/wxPython/wx/lib/floatcanvas/NavCanvas.py b/wxPython/wx/lib/floatcanvas/NavCanvas.py index 1770a109e8..b2c6d67422 100644 --- a/wxPython/wx/lib/floatcanvas/NavCanvas.py +++ b/wxPython/wx/lib/floatcanvas/NavCanvas.py @@ -4,18 +4,9 @@ A Panel that includes the FloatCanvas and Navigation controls """ import wx - import FloatCanvas, Resources -ID_ZOOM_IN_BUTTON = wx.NewId() -ID_ZOOM_OUT_BUTTON = wx.NewId() -ID_ZOOM_TO_FIT_BUTTON = wx.NewId() -ID_MOVE_MODE_BUTTON = wx.NewId() -ID_POINTER_BUTTON = wx.NewId() - -#--------------------------------------------------------------------------- - class NavCanvas(wx.Panel): """ NavCanvas.py @@ -23,96 +14,80 @@ class NavCanvas(wx.Panel): This is a high level window that encloses the FloatCanvas in a panel and adds a Navigation toolbar. - Copyright: Christopher Barker) - - License: Same as the version of wxPython you are using it with - - Please let me know if you're using this!!! + """ - Contact me at: + def __init__(self, + parent, + id = wx.ID_ANY, + size = wx.DefaultSize, + **kwargs): # The rest just get passed into FloatCanvas + wx.Panel.__init__(self, parent, id, size=size) - Chris.Barker@noaa.gov + self.BuildToolbar() + ## Create the vertical sizer for the toolbar and Panel + box = wx.BoxSizer(wx.VERTICAL) + box.Add(self.ToolBar, 0, wx.ALL | wx.ALIGN_LEFT | wx.GROW, 4) - """ - - def __init__(self, parent, id = -1, - size = wx.DefaultSize, - **kwargs): # The rest just get passed into FloatCanvas + self.Canvas = FloatCanvas.FloatCanvas(self, **kwargs) + box.Add(self.Canvas, 1, wx.GROW) - wx.Panel.__init__( self, parent, id, wx.DefaultPosition, size) + self.SetSizerAndFit(box) - ## Create the vertical sizer for the toolbar and Panel - box = wx.BoxSizer(wx.VERTICAL) - box.Add(self.BuildToolbar(), 0, wx.ALL | wx.ALIGN_LEFT | wx.GROW, 4) - - self.Canvas = FloatCanvas.FloatCanvas( self, wx.NewId(), - size = wx.DefaultSize, - **kwargs) - box.Add(self.Canvas,1,wx.GROW) - box.Fit(self) - self.SetSizer(box) + import GUIMode # here so that it doesn't get imported before wx.App() + self.GUIZoomIn = GUIMode.GUIZoomIn(self.Canvas) + self.GUIZoomOut = GUIMode.GUIZoomOut(self.Canvas) + self.GUIMove = GUIMode.GUIMove(self.Canvas) + self.GUIMouse = GUIMode.GUIMouse(self.Canvas) # default to Mouse mode - self.ToolBar.ToggleTool(ID_POINTER_BUTTON,1) - self.Canvas.SetMode("Mouse") - - return None + self.ToolBar.ToggleTool(self.PointerTool.GetId(), True) + self.Canvas.SetMode(self.GUIMouse) - def __getattr__(self, name): - """ - Delegate all extra methods to the Canvas - """ - attrib = getattr(self.Canvas, name) - ## add the attribute to this module's dict for future calls - self.__dict__[name] = attrib - return attrib + return None def BuildToolbar(self): - tb = wx.ToolBar(self,-1) + tb = wx.ToolBar(self) self.ToolBar = tb - tb.SetToolBitmapSize((24,24)) - - tb.AddTool(ID_POINTER_BUTTON, Resources.getPointerBitmap(), isToggle=True, shortHelpString = "Pointer") - wx.EVT_TOOL(self, ID_POINTER_BUTTON, self.SetToolMode) - - tb.AddTool(ID_ZOOM_IN_BUTTON, Resources.getMagPlusBitmap(), isToggle=True, shortHelpString = "Zoom In") - wx.EVT_TOOL(self, ID_ZOOM_IN_BUTTON, self.SetToolMode) - - tb.AddTool(ID_ZOOM_OUT_BUTTON, Resources.getMagMinusBitmap(), isToggle=True, shortHelpString = "Zoom Out") - wx.EVT_TOOL(self, ID_ZOOM_OUT_BUTTON, self.SetToolMode) - - tb.AddTool(ID_MOVE_MODE_BUTTON, Resources.getHandBitmap(), isToggle=True, shortHelpString = "Move") - wx.EVT_TOOL(self, ID_MOVE_MODE_BUTTON, self.SetToolMode) - + + self.PointerTool = tb.AddRadioTool(wx.ID_ANY, bitmap=Resources.getPointerBitmap(), shortHelp = "Pointer") + self.Bind(wx.EVT_TOOL, lambda evt : self.SetMode(Mode=self.GUIMouse), self.PointerTool) + + self.ZoomInTool = tb.AddRadioTool(wx.ID_ANY, bitmap=Resources.getMagPlusBitmap(), shortHelp = "Zoom In") + self.Bind(wx.EVT_TOOL, lambda evt : self.SetMode(Mode=self.GUIZoomIn), self.ZoomInTool) + + self.ZoomOutTool = tb.AddRadioTool(wx.ID_ANY, bitmap=Resources.getMagMinusBitmap(), shortHelp = "Zoom Out") + self.Bind(wx.EVT_TOOL, lambda evt : self.SetMode(Mode=self.GUIZoomOut), self.ZoomOutTool) + + self.MoveTool = tb.AddRadioTool(wx.ID_ANY, bitmap=Resources.getHandBitmap(), shortHelp = "Move") + self.Bind(wx.EVT_TOOL, lambda evt : self.SetMode(Mode=self.GUIMove), self.MoveTool) + tb.AddSeparator() - - tb.AddControl(wx.Button(tb, ID_ZOOM_TO_FIT_BUTTON, "Zoom To Fit",wx.DefaultPosition, wx.DefaultSize)) - wx.EVT_BUTTON(self, ID_ZOOM_TO_FIT_BUTTON, self.ZoomToFit) + + self.ZoomButton = wx.Button(tb, label="Zoom To Fit") + tb.AddControl(self.ZoomButton) + self.ZoomButton.Bind(wx.EVT_BUTTON, self.ZoomToFit) tb.Realize() - S = tb.GetSize() - tb.SetSizeHints(S[0],S[1]) + ## fixme: remove this when the bug is fixed! + wx.CallAfter(self.HideShowHack) # this required on wxPython 2.8.3 on OS-X + return tb - def SetToolMode(self,event): - for id in [ID_ZOOM_IN_BUTTON, - ID_ZOOM_OUT_BUTTON, - ID_MOVE_MODE_BUTTON, - ID_POINTER_BUTTON]: - self.ToolBar.ToggleTool(id,0) - self.ToolBar.ToggleTool(event.GetId(),1) - if event.GetId() == ID_ZOOM_IN_BUTTON: - self.Canvas.SetMode("ZoomIn") - elif event.GetId() == ID_ZOOM_OUT_BUTTON: - self.Canvas.SetMode("ZoomOut") - elif event.GetId() == ID_MOVE_MODE_BUTTON: - self.Canvas.SetMode("Move") - elif event.GetId() == ID_POINTER_BUTTON: - self.Canvas.SetMode("Mouse") + def HideShowHack(self): + ##fixme: remove this when the bug is fixed! + """ + Hack to hide and show button on toolbar to get around OS-X bug on + wxPython2.8 on OS-X + """ + self.ZoomButton.Hide() + self.ZoomButton.Show() + def SetMode(self, Mode): + self.Canvas.SetMode(Mode) def ZoomToFit(self,Event): self.Canvas.ZoomToBB() + self.Canvas.SetFocus() # Otherwise the focus stays on the Button, and wheel events are lost. diff --git a/wxPython/wx/lib/floatcanvas/Utilities/BBox.py b/wxPython/wx/lib/floatcanvas/Utilities/BBox.py new file mode 100644 index 0000000000..281553112d --- /dev/null +++ b/wxPython/wx/lib/floatcanvas/Utilities/BBox.py @@ -0,0 +1,170 @@ +""" +A Bounding Box object and assorted utilities , subclassed from a numpy array + +""" + +import numpy as N + +class BBox(N.ndarray): + """ + A Bounding Box object: + + Takes Data as an array. Data is any python sequence that can be turned into a + 2x2 numpy array of floats: + + [[MinX, MinY ], + [MaxX, MaxY ]] + + It is a subclass of numpy.ndarray, so for the most part it can be used as + an array, and arrays that fit the above description can be used in its place. + + Usually created by the factory functions: + + asBBox + + and + + fromPoints + + """ + def __new__(subtype, data): + """ + Takes Data as an array. Data is any python sequence that can be turned into a + 2x2 numpy array of floats: + + [[MinX, MinY ], + [MaxX, MaxY ]] + + You don't usually call this directly. BBox objects are created with the factory functions: + + asBBox + + and + + fromPoints + + """ + arr = N.array(data, N.float) + arr.shape = (2,2) + if arr[0,0] > arr[1,0] or arr[0,1] > arr[1,1]: + # note: zero sized BB OK. + raise ValueError("BBox values not aligned: \n minimum values must be less that maximum values") + return N.ndarray.__new__(BBox, shape=arr.shape, dtype=arr.dtype, buffer=arr) + + def Overlaps(self, BB): + """ + Overlap(BB): + + Tests if the given Bounding Box overlaps with this one. + Returns True is the Bounding boxes overlap, False otherwise + If they are just touching, returns True + """ + + if ( (self[1,0] >= BB[0,0]) and (self[0,0] <= BB[1,0]) and + (self[1,1] >= BB[0,1]) and (self[0,1] <= BB[1,1]) ): + return True + else: + return False + + def Inside(self, BB): + """ + Inside(BB): + + Tests if the given Bounding Box is entirely inside this one. + + Returns True if it is entirely inside, or touching the + border. + + Returns False otherwise + """ + if ( (BB[0,0] >= self[0,0]) and (BB[1,0] <= self[1,0]) and + (BB[0,1] >= self[0,1]) and (BB[1,1] <= self[1,1]) ): + return True + else: + return False + + def Merge(self, BB): + """ + Joins this bounding box with the one passed in, maybe making this one bigger + + """ + + if BB[0,0] < self[0,0]: self[0,0] = BB[0,0] + if BB[0,1] < self[0,1]: self[0,1] = BB[0,1] + if BB[1,0] > self[1,0]: self[1,0] = BB[1,0] + if BB[1,1] > self[1,1]: self[1,1] = BB[1,1] + + ### This could be used for a make BB from a bunch of BBs + + #~ def _getboundingbox(bboxarray): # lrk: added this + #~ # returns the bounding box of a bunch of bounding boxes + #~ upperleft = N.minimum.reduce(bboxarray[:,0]) + #~ lowerright = N.maximum.reduce(bboxarray[:,1]) + #~ return N.array((upperleft, lowerright), N.float) + #~ _getboundingbox = staticmethod(_getboundingbox) + + + ## Save the ndarray __eq__ for internal use. + Array__eq__ = N.ndarray.__eq__ + def __eq__(self, BB): + """ + __eq__(BB) The equality operator + + A == B if and only if all the entries are the same + + """ + return N.all(self.Array__eq__(BB)) + + +def asBBox(data): + """ + returns a BBox object. + + If object is a BBox, it is returned unaltered + + If object is a numpy array, a BBox object is returned that shares a + view of the data with that array + + """ + + if isinstance(data, BBox): + return data + arr = N.asarray(data, N.float) + return N.ndarray.__new__(BBox, shape=arr.shape, dtype=arr.dtype, buffer=arr) + +def fromPoints(Points): + """ + fromPoints (Points). + + reruns the bounding box of the set of points in Points. Points can + be any python object that can be turned into a numpy NX2 array of Floats. + + If a single point is passed in, a zero-size Bounding Box is returned. + + """ + Points = N.asarray(Points, N.float).reshape(-1,2) + + arr = N.vstack( (Points.min(0), Points.max(0)) ) + return N.ndarray.__new__(BBox, shape=arr.shape, dtype=arr.dtype, buffer=arr) + +def fromBBArray(BBarray): + """ + Builds a BBox object from an array of Bounding Boxes. + The resulting Bounding Box encompases all the included BBs. + + The BBarray is in the shape: (Nx2x2) where BBarray[n] is a 2x2 array that represents a BBox + """ + + #upperleft = N.minimum.reduce(BBarray[:,0]) + #lowerright = N.maximum.reduce(BBarray[:,1]) + +# BBarray = N.asarray(BBarray, N.float).reshape(-1,2) +# arr = N.vstack( (BBarray.min(0), BBarray.max(0)) ) + BBarray = N.asarray(BBarray, N.float).reshape(-1,2,2) + arr = N.vstack( (BBarray[:,0,:].min(0), BBarray[:,1,:].max(0)) ) + return asBBox(arr) + #return asBBox( (upperleft, lowerright) ) * 2 + + + + diff --git a/wxPython/wx/lib/floatcanvas/Utilities/BBoxTest.py b/wxPython/wx/lib/floatcanvas/Utilities/BBoxTest.py new file mode 100644 index 0000000000..3cca2e95f9 --- /dev/null +++ b/wxPython/wx/lib/floatcanvas/Utilities/BBoxTest.py @@ -0,0 +1,354 @@ + +""" +Test code for the BBox Object + +""" + +import unittest + +from BBox import * + +class testCreator(unittest.TestCase): + def testCreates(self): + B = BBox(((0,0),(5,5))) + self.failUnless(isinstance(B, BBox)) + + def testType(self): + B = N.array(((0,0),(5,5))) + self.failIf(isinstance(B, BBox)) + + def testDataType(self): + B = BBox(((0,0),(5,5))) + self.failUnless(B.dtype == N.float) + + def testShape(self): + B = BBox((0,0,5,5)) + self.failUnless(B.shape == (2,2)) + + def testShape2(self): + self.failUnlessRaises(ValueError, BBox, (0,0,5) ) + + def testShape3(self): + self.failUnlessRaises(ValueError, BBox, (0,0,5,6,7) ) + + def testArrayConstruction(self): + A = N.array(((4,5),(10,12)), N.float_) + B = BBox(A) + self.failUnless(isinstance(B, BBox)) + + def testMinMax(self): + self.failUnlessRaises(ValueError, BBox, (0,0,-1,6) ) + + def testMinMax2(self): + self.failUnlessRaises(ValueError, BBox, (0,0,1,-6) ) + + def testMinMax(self): + # OK to have a zero-sized BB + B = BBox(((0,0),(0,5))) + self.failUnless(isinstance(B, BBox)) + + def testMinMax2(self): + # OK to have a zero-sized BB + B = BBox(((10.0,-34),(10.0,-34.0))) + self.failUnless(isinstance(B, BBox)) + + def testMinMax3(self): + # OK to have a tiny BB + B = BBox(((0,0),(1e-20,5))) + self.failUnless(isinstance(B, BBox)) + + def testMinMax4(self): + # Should catch tiny difference + self.failUnlessRaises(ValueError, BBox, ((0,0), (-1e-20,5)) ) + +class testAsBBox(unittest.TestCase): + + def testPassThrough(self): + B = BBox(((0,0),(5,5))) + C = asBBox(B) + self.failUnless(B is C) + + def testPassThrough2(self): + B = (((0,0),(5,5))) + C = asBBox(B) + self.failIf(B is C) + + def testPassArray(self): + # Different data type + A = N.array( (((0,0),(5,5))) ) + C = asBBox(A) + self.failIf(A is C) + + def testPassArray2(self): + # same data type -- should be a view + A = N.array( (((0,0),(5,5))), N.float_ ) + C = asBBox(A) + A[0,0] = -10 + self.failUnless(C[0,0] == A[0,0]) + +class testIntersect(unittest.TestCase): + + def testSame(self): + B = BBox(((-23.5, 456),(56, 532.0))) + C = BBox(((-23.5, 456),(56, 532.0))) + self.failUnless(B.Overlaps(C) ) + + def testUpperLeft(self): + B = BBox( ( (5, 10),(15, 25) ) ) + C = BBox( ( (0, 12),(10, 32.0) ) ) + self.failUnless(B.Overlaps(C) ) + + def testUpperRight(self): + B = BBox( ( (5, 10),(15, 25) ) ) + C = BBox( ( (12, 12),(25, 32.0) ) ) + self.failUnless(B.Overlaps(C) ) + + def testLowerRight(self): + B = BBox( ( (5, 10),(15, 25) ) ) + C = BBox( ( (12, 5),(25, 15) ) ) + self.failUnless(B.Overlaps(C) ) + + def testLowerLeft(self): + B = BBox( ( (5, 10),(15, 25) ) ) + C = BBox( ( (-10, 5),(8.5, 15) ) ) + self.failUnless(B.Overlaps(C) ) + + def testBelow(self): + B = BBox( ( (5, 10),(15, 25) ) ) + C = BBox( ( (-10, 5),(8.5, 9.2) ) ) + self.failIf(B.Overlaps(C) ) + + def testAbove(self): + B = BBox( ( (5, 10),(15, 25) ) ) + C = BBox( ( (-10, 25.001),(8.5, 32) ) ) + self.failIf(B.Overlaps(C) ) + + def testLeft(self): + B = BBox( ( (5, 10),(15, 25) ) ) + C = BBox( ( (4, 8),(4.95, 32) ) ) + self.failIf(B.Overlaps(C) ) + + def testRight(self): + B = BBox( ( (5, 10),(15, 25) ) ) + C = BBox( ( (17.1, 8),(17.95, 32) ) ) + self.failIf(B.Overlaps(C) ) + + def testInside(self): + B = BBox( ( (-15, -25),(-5, -10) ) ) + C = BBox( ( (-12, -22), (-6, -8) ) ) + self.failUnless(B.Overlaps(C) ) + + def testOutside(self): + B = BBox( ( (-15, -25),(-5, -10) ) ) + C = BBox( ( (-17, -26), (3, 0) ) ) + self.failUnless(B.Overlaps(C) ) + + def testTouch(self): + B = BBox( ( (5, 10),(15, 25) ) ) + C = BBox( ( (15, 8),(17.95, 32) ) ) + self.failUnless(B.Overlaps(C) ) + + def testCorner(self): + B = BBox( ( (5, 10),(15, 25) ) ) + C = BBox( ( (15, 25),(17.95, 32) ) ) + self.failUnless(B.Overlaps(C) ) + + def testZeroSize(self): + B = BBox( ( (5, 10),(15, 25) ) ) + C = BBox( ( (15, 25),(15, 25) ) ) + self.failUnless(B.Overlaps(C) ) + + def testZeroSize2(self): + B = BBox( ( (5, 10),(5, 10) ) ) + C = BBox( ( (15, 25),(15, 25) ) ) + self.failIf(B.Overlaps(C) ) + + def testZeroSize3(self): + B = BBox( ( (5, 10),(5, 10) ) ) + C = BBox( ( (0, 8),(10, 12) ) ) + self.failUnless(B.Overlaps(C) ) + + def testZeroSize4(self): + B = BBox( ( (5, 1),(10, 25) ) ) + C = BBox( ( (8, 8),(8, 8) ) ) + self.failUnless(B.Overlaps(C) ) + + + +class testEquality(unittest.TestCase): + def testSame(self): + B = BBox( ( (1.0, 2.0), (5.0, 10.0) ) ) + C = BBox( ( (1.0, 2.0), (5.0, 10.0) ) ) + self.failUnless(B == C) + + def testIdentical(self): + B = BBox( ( (1.0, 2.0), (5.0, 10.0) ) ) + self.failUnless(B == B) + + def testNotSame(self): + B = BBox( ( (1.0, 2.0), (5.0, 10.0) ) ) + C = BBox( ( (1.0, 2.0), (5.0, 10.1) ) ) + self.failIf(B == C) + + def testWithArray(self): + B = BBox( ( (1.0, 2.0), (5.0, 10.0) ) ) + C = N.array( ( (1.0, 2.0), (5.0, 10.0) ) ) + self.failUnless(B == C) + + def testWithArray2(self): + B = BBox( ( (1.0, 2.0), (5.0, 10.0) ) ) + C = N.array( ( (1.0, 2.0), (5.0, 10.0) ) ) + self.failUnless(C == B) + + def testWithArray2(self): + B = BBox( ( (1.0, 2.0), (5.0, 10.0) ) ) + C = N.array( ( (1.01, 2.0), (5.0, 10.0) ) ) + self.failIf(C == B) + +class testInside(unittest.TestCase): + def testSame(self): + B = BBox( ( (1.0, 2.0), (5.0, 10.0) ) ) + C = BBox( ( (1.0, 2.0), (5.0, 10.0) ) ) + self.failUnless(B.Inside(C)) + + def testPoint(self): + B = BBox( ( (1.0, 2.0), (5.0, 10.0) ) ) + C = BBox( ( (3.0, 4.0), (3.0, 4.0) ) ) + self.failUnless(B.Inside(C)) + + def testPointOutside(self): + B = BBox( ( (1.0, 2.0), (5.0, 10.0) ) ) + C = BBox( ( (-3.0, 4.0), (0.10, 4.0) ) ) + self.failIf(B.Inside(C)) + + def testUpperLeft(self): + B = BBox( ( (5, 10),(15, 25) ) ) + C = BBox( ( (0, 12),(10, 32.0) ) ) + self.failIf(B.Inside(C) ) + + def testUpperRight(self): + B = BBox( ( (5, 10),(15, 25) ) ) + C = BBox( ( (12, 12),(25, 32.0) ) ) + self.failIf(B.Inside(C) ) + + def testLowerRight(self): + B = BBox( ( (5, 10),(15, 25) ) ) + C = BBox( ( (12, 5),(25, 15) ) ) + self.failIf(B.Inside(C) ) + + def testLowerLeft(self): + B = BBox( ( (5, 10),(15, 25) ) ) + C = BBox( ( (-10, 5),(8.5, 15) ) ) + self.failIf(B.Inside(C) ) + + def testBelow(self): + B = BBox( ( (5, 10),(15, 25) ) ) + C = BBox( ( (-10, 5),(8.5, 9.2) ) ) + self.failIf(B.Inside(C) ) + + def testAbove(self): + B = BBox( ( (5, 10),(15, 25) ) ) + C = BBox( ( (-10, 25.001),(8.5, 32) ) ) + self.failIf(B.Inside(C) ) + + def testLeft(self): + B = BBox( ( (5, 10),(15, 25) ) ) + C = BBox( ( (4, 8),(4.95, 32) ) ) + self.failIf(B.Inside(C) ) + + def testRight(self): + B = BBox( ( (5, 10),(15, 25) ) ) + C = BBox( ( (17.1, 8),(17.95, 32) ) ) + self.failIf(B.Inside(C) ) + +class testFromPoints(unittest.TestCase): + + def testCreate(self): + Pts = N.array( ((5,2), + (3,4), + (1,6), + ), N.float_ ) + B = fromPoints(Pts) + #B = BBox( ( (1.0, 2.0), (5.0, 10.0) ) ) + self.failUnless(B[0,0] == 1.0 and + B[0,1] == 2.0 and + B[1,0] == 5.0 and + B[1,1] == 6.0 + ) + def testCreateInts(self): + Pts = N.array( ((5,2), + (3,4), + (1,6), + ) ) + B = fromPoints(Pts) + self.failUnless(B[0,0] == 1.0 and + B[0,1] == 2.0 and + B[1,0] == 5.0 and + B[1,1] == 6.0 + ) + + def testSinglePoint(self): + Pts = N.array( (5,2), N.float_ ) + B = fromPoints(Pts) + self.failUnless(B[0,0] == 5.0 and + B[0,1] == 2.0 and + B[1,0] == 5.0 and + B[1,1] == 2.0 + ) + + def testListTuples(self): + Pts = [ (3, 6.5), + (13, 43.2), + (-4.32, -4), + (65, -23), + (-0.0001, 23.432), + ] + B = fromPoints(Pts) + self.failUnless(B[0,0] == -4.32 and + B[0,1] == -23.0 and + B[1,0] == 65.0 and + B[1,1] == 43.2 + ) +class testMerge(unittest.TestCase): + A = BBox( ((-23.5, 456), (56, 532.0)) ) + B = BBox( ((-20.3, 460), (54, 465 )) )# B should be completely inside A + C = BBox( ((-23.5, 456), (58, 540.0)) )# up and to the right or A + D = BBox( ((-26.5, 12), (56, 532.0)) ) + + def testInside(self): + C = self.A.copy() + C.Merge(self.B) + self.failUnless(C == self.A) + + def testFullOutside(self): + C = self.B.copy() + C.Merge(self.A) + self.failUnless(C == self.A) + + def testUpRight(self): + A = self.A.copy() + A.Merge(self.C) + self.failUnless(A[0] == self.A[0] and A[1] == self.C[1]) + + def testDownLeft(self): + A = self.A.copy() + A.Merge(self.D) + self.failUnless(A[0] == self.D[0] and A[1] == self.A[1]) + +class testBBarray(unittest.TestCase): + BBarray = N.array( ( ((-23.5, 456), (56, 532.0)), + ((-20.3, 460), (54, 465 )), + ((-23.5, 456), (58, 540.0)), + ((-26.5, 12), (56, 532.0)), + ), + dtype=N.float) + print BBarray + BB = asBBox( ((-26.5, 12.), ( 58. , 540.)) ) + + def testJoin(self): + BB = fromBBArray(self.BBarray) + self.failUnless(BB == self.BB, "Wrong BB was created. It was:\n%s \nit should have been:\n%s"%(BB, self.BB)) + + +if __name__ == "__main__": + unittest.main() diff --git a/wxPython/wx/lib/floatcanvas/Utilities/GUI.py b/wxPython/wx/lib/floatcanvas/Utilities/GUI.py new file mode 100644 index 0000000000..5cf11fd8cf --- /dev/null +++ b/wxPython/wx/lib/floatcanvas/Utilities/GUI.py @@ -0,0 +1,115 @@ +""" + +Part of the floatcanvas.Utilities package. + +This module contains assorted GUI-related utilities that can be used +with FloatCanvas + +So far, they are: + +RubberBandBox: used to draw a RubberBand Box on the screen + +""" +import wx +from floatcanvas import FloatCanvas + +class RubberBandBox: + """ + Class to provide a rubber band box that can be drawn on a Window + + """ + + def __init__(self, Canvas, CallBack, Tol=5): + + """ + To initialize: + + RubberBandBox(Canvas, CallBack) + + Canvas: the FloatCanvas you want the Rubber band box to be used on + + CallBack: is the method you want called when the mouse is + released. That method will be called, passing in a rect + parameter, where rect is: (Point, WH) of the rect in + world coords. + + Tol: The tolerance for the smallest rectangle allowed. defaults + to 5. In pixels + + Methods: + + Enable() : Enables the Rubber Band Box (Binds the events) + + Disable() : Enables the Rubber Band Box (Unbinds the events) + + Attributes: + + CallBack: The callback function, if it's replaced you need to + call Enable() again. + + """ + + self.Canvas = Canvas + self.CallBack = CallBack + self.Tol = Tol + + self.Drawing = False + self.RBRect = None + self.StartPointWorld = None + + return None + + def Enable(self): + """ + Called when you want the rubber band box to be enabled + + """ + + # bind events: + self.Canvas.Bind(FloatCanvas.EVT_MOTION, self.OnMove ) + self.Canvas.Bind(FloatCanvas.EVT_LEFT_DOWN, self.OnLeftDown) + self.Canvas.Bind(FloatCanvas.EVT_LEFT_UP, self.OnLeftUp ) + + def Disable(self): + """ + Called when you don't want the rubber band box to be enabled + + """ + + # unbind events: + self.Canvas.Unbind(FloatCanvas.EVT_MOTION) + self.Canvas.Unbind(FloatCanvas.EVT_LEFT_DOWN) + self.Canvas.Unbind(FloatCanvas.EVT_LEFT_UP) + + def OnMove(self, event): + if self.Drawing: + x, y = self.StartPoint + Cornerx, Cornery = event.GetPosition() + w, h = ( Cornerx - x, Cornery - y) + if abs(w) > self.Tol and abs(h) > self.Tol: + # draw the RB box + dc = wx.ClientDC(self.Canvas) + dc.SetPen(wx.Pen('WHITE', 2, wx.SHORT_DASH)) + dc.SetBrush(wx.TRANSPARENT_BRUSH) + dc.SetLogicalFunction(wx.XOR) + if self.RBRect: + dc.DrawRectangle(*self.RBRect) + self.RBRect = (x, y, w, h ) + dc.DrawRectangle(*self.RBRect) + event.Skip() # skip so that other events can catch these + + def OnLeftDown(self, event): + # Start drawing + self.Drawing = True + self.StartPoint = event.GetPosition() + self.StartPointWorld = event.Coords + + def OnLeftUp(self, event): + # Stop Drawing + if self.Drawing: + self.Drawing = False + if self.RBRect: + WH = event.Coords - self.StartPointWorld + wx.CallAfter(self.CallBack, (self.StartPointWorld, WH)) + self.RBRect = None + self.StartPointWorld = None diff --git a/wxPython/wx/lib/floatcanvas/Utilities/__init__.py b/wxPython/wx/lib/floatcanvas/Utilities/__init__.py new file mode 100644 index 0000000000..b3162b3577 --- /dev/null +++ b/wxPython/wx/lib/floatcanvas/Utilities/__init__.py @@ -0,0 +1,7 @@ +""" +__init__ for the floatcanvas Utilities package + +""" +pass + + diff --git a/wxPython/wx/lib/floatcanvas/__init__.py b/wxPython/wx/lib/floatcanvas/__init__.py index 1c7f9a6d3c..b03b8fd7c8 100644 --- a/wxPython/wx/lib/floatcanvas/__init__.py +++ b/wxPython/wx/lib/floatcanvas/__init__.py @@ -29,8 +29,7 @@ It is double buffered, so re-draws after the window is uncovered by something else are very quick. It relies on NumPy, which is needed for speed (maybe, I haven't profiled -it). It will also use numarray, if you don't have Numeric, but it is -slower. +properly) and convenience. Bugs and Limitations: Lots: patches, fixes welcome @@ -60,12 +59,6 @@ If you are zoomed in, it checks the Bounding box of an object before drawing it. This makes it a great deal faster when there are a lot of objects and you are zoomed in so that only a few are shown. -One solution is to be able to pass some sort of object set to the DC -directly. I've used DC.DrawPointList(Points), and it helped a lot with -drawing lots of points. However, when zoomed in, the Bounding boxes need -to be checked, so I may some day write C++ code that does the loop and -checks the BBs. - Mouse Events: There are a full set of custom mouse events. They are just like the @@ -80,19 +73,25 @@ clicked, mouse-over'd, etc. See the Demo for what it can do, and how to use it. Copyright: Christopher Barker - License: Same as the version of wxPython you are using it with. -Check for updates or answers to questions, send me an email. +TRAC site for some docs and updates: +http://morticia.cs.dal.ca/FloatCanvas/ -Please let me know if you're using this!!! +SVN for latest code: +svn://morticia.cs.dal.ca/FloatCanvas +Mailing List: +http://mail.mithis.com/cgi-bin/mailman/listinfo/floatcanvas + +Check for updates or answers to questions, send me an email. +Please let me know if you're using this!!! Contact me at: Chris.Barker@noaa.gov """ -__version__ = "0.9.10" +__version__ = "0.9.18" diff --git a/wxPython/wx/lib/imagebrowser.py b/wxPython/wx/lib/imagebrowser.py index 576d0bcfbd..72b1346d53 100644 --- a/wxPython/wx/lib/imagebrowser.py +++ b/wxPython/wx/lib/imagebrowser.py @@ -95,7 +95,10 @@ class ImageView(wx.Window): if image is None: return - bmp = image.ConvertToBitmap() + try: + bmp = image.ConvertToBitmap() + except: + return iwidth = bmp.GetWidth() # dimensions of image file iheight = bmp.GetHeight() diff --git a/wxPython/wx/lib/inspection.py b/wxPython/wx/lib/inspection.py index 38137ea606..8b4789ba87 100644 --- a/wxPython/wx/lib/inspection.py +++ b/wxPython/wx/lib/inspection.py @@ -221,6 +221,8 @@ class InspectionFrame(wx.Frame): def OnClose(self, evt): self.SaveSettings(self.config) + self.mgr.UnInit() + del self.mgr evt.Skip() @@ -533,6 +535,9 @@ class InspectionInfoPanel(wx.stc.StyledTextCtrl): 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', diff --git a/wxPython/wx/lib/masked/combobox.py b/wxPython/wx/lib/masked/combobox.py index 33ac41ad2d..1eba82de0c 100644 --- a/wxPython/wx/lib/masked/combobox.py +++ b/wxPython/wx/lib/masked/combobox.py @@ -47,6 +47,30 @@ class MaskedComboBoxSelectEvent(wx.PyCommandEvent): 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 ): """ @@ -152,18 +176,15 @@ class BaseMaskedComboBox( wx.ComboBox, MaskedEditMixin ): 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 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_CHAR, self._OnChar ) ## handle each keypress - self.Bind(wx.EVT_KEY_DOWN, self._OnKeyDownInComboBox ) ## for special processing of up/down keys - self.Bind(wx.EVT_KEY_DOWN, self._OnKeyDown ) ## for processing the rest of the control keys - ## (next in evt chain) - self.Bind(wx.EVT_TEXT, self._OnTextChange ) ## color control appropriately & keep - ## track of previous value for undo + ## 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 ) @@ -171,6 +192,13 @@ class BaseMaskedComboBox( wx.ComboBox, MaskedEditMixin ): return "" % self.GetValue() + def OnWindowDestroy(self, event): + # clean up associated event handler object: + if self.RemoveEventHandler(self.evt_handler): + self.evt_handler.Destroy() + event.Skip() + + def _CalcSize(self, size=None): """ Calculate automatic size if allowed; augment base mixin function @@ -252,7 +280,9 @@ class BaseMaskedComboBox( wx.ComboBox, MaskedEditMixin ): # 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() @@ -264,11 +294,14 @@ class BaseMaskedComboBox( wx.ComboBox, MaskedEditMixin ): 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) @@ -306,15 +339,26 @@ class BaseMaskedComboBox( wx.ComboBox, MaskedEditMixin ): dateparts = value.split(' ') dateparts[0] = self._adjustDate(dateparts[0], fixcentury=True) value = string.join(dateparts, ' ') -## dbg('adjusted value: "%s"' % value) value = self._Paste(value, raise_on_invalid=True, just_return_value=True) else: raise +## dbg('adjusted value: "%s"' % value) - self._SetValue(value) -#### dbg('queuing insertion after .SetValue', replace_to) - wx.CallAfter(self._SetInsertionPoint, replace_to) - wx.CallAfter(self._SetSelection, replace_to, replace_to) + # 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): @@ -509,26 +553,60 @@ class BaseMaskedComboBox( wx.ComboBox, MaskedEditMixin ): Necessary override for bookkeeping on choice selection, to keep current value current. """ -## dbg('MaskedComboBox::SetSelection(%d)' % index) +## dbg('MaskedComboBox::SetSelection(%d)' % index, indent=1) if self._mask: self._prevValue = self._curValue - self._curValue = self._choices[index] 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... {:-( ) + 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: - self._OnChar(event) - return + 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): @@ -585,7 +663,7 @@ class BaseMaskedComboBox( wx.ComboBox, MaskedEditMixin ): Override mixin (empty) autocomplete handler, so that autocompletion causes combobox to update appropriately. """ -## dbg('MaskedComboBox::OnAutoSelect', field._index, indent=1) +## dbg('MaskedComboBox::OnAutoSelect(%d, %d)' % (field._index, match_index), indent=1) ## field._autoCompleteIndex = match_index if field == self._ctrl_constraints: self.SetSelection(match_index) @@ -594,7 +672,7 @@ class BaseMaskedComboBox( wx.ComboBox, MaskedEditMixin ): MaskedComboBoxSelectEvent( self.GetId(), match_index, self ) ) self._CheckValid() ## dbg('field._autoCompleteIndex:', match_index) -## dbg('self.GetSelection():', self.GetSelection()) +## 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 @@ -614,15 +692,24 @@ class BaseMaskedComboBox( wx.ComboBox, MaskedEditMixin ): item in the list. (and then does the usual OnReturn bit.) """ ## dbg('MaskedComboBox::OnReturn', indent=1) -## dbg('current value: "%s"' % self.GetValue(), 'current index:', self.GetSelection()) - if self.GetSelection() == -1 and self.GetValue().lower().strip() in self._ctrl_constraints._compareChoices: - wx.CallAfter(self.SetSelection, self._ctrl_constraints._autoCompleteIndex) - +## 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 @@ -659,6 +746,19 @@ class PreMaskedComboBox( BaseMaskedComboBox, MaskedEditAccessorsMixin ): __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 diff --git a/wxPython/wx/lib/masked/maskededit.py b/wxPython/wx/lib/masked/maskededit.py index b5696c7e1a..b2b77840e3 100644 --- a/wxPython/wx/lib/masked/maskededit.py +++ b/wxPython/wx/lib/masked/maskededit.py @@ -1,12 +1,12 @@ #---------------------------------------------------------------------------- # Name: maskededit.py -# Authors: Jeff Childers, Will Sadkin -# Email: jchilders_98@yahoo.com, wsadkin@parlancecorp.com +# 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-2006 +# Portions: (c) 2002 by Will Sadkin, 2002-2007 # RCS-ID: $Id$ -# License: wxWindows license +# License: wxWidgets license #---------------------------------------------------------------------------- # NOTE: # MaskedEdit controls are based on a suggestion made on [wxPython-Users] by @@ -27,7 +27,7 @@ # #---------------------------------------------------------------------------- # -# 03/30/2004 - Will Sadkin (wsadkin@nameconnector.com) +# 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 @@ -337,7 +337,18 @@ to individual fields: 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 @@ -1327,12 +1338,14 @@ class Field: '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') + 'compareNoCase', 'emptyInvalid', 'validRequired', 'raiseOnInvalidPaste', + 'stopFieldChangeIfInvalid') def __init__(self, **kwargs): """ @@ -3021,7 +3034,7 @@ class MaskedEditMixin: char = char.decode(self._defaultEncoding) else: char = unichr(event.GetUnicodeKey()) - dbg('unicode char:', char) +## dbg('unicode char:', char) excludes = u'' if type(field._excludeChars) != types.UnicodeType: excludes += field._excludeChars.decode(self._defaultEncoding) @@ -3767,6 +3780,21 @@ class MaskedEditMixin: ## 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(): @@ -3775,13 +3803,12 @@ class MaskedEditMixin: # 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.) - field = self._FindField(pos) - index = field._index - field_start = field._extent[0] + 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(): @@ -3821,8 +3848,6 @@ class MaskedEditMixin: else: # "Go forward" - field = self._FindField(sel_to) - field_start, field_end = field._extent if event.ControlDown(): ## dbg('queuing select to end of field:', pos, field_end) wx.CallAfter(self._SetInsertionPoint, pos) @@ -3888,10 +3913,19 @@ class MaskedEditMixin: 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()) @@ -4021,7 +4055,7 @@ class MaskedEditMixin: def _findNextEntry(self,pos, adjustInsert=True): """ Find the insertion point for the next valid entry character position.""" -## dbg('MaskedEditMixin::_findNextEntry', indent=1) +## 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 @@ -4280,7 +4314,9 @@ class MaskedEditMixin: #### 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: + 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) @@ -4317,11 +4353,14 @@ class MaskedEditMixin: # 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) ) ): # don't move to next field without explicit cursor movement + 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 @@ -5092,7 +5131,11 @@ class MaskedEditMixin: #### 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 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 @@ -5165,7 +5208,11 @@ class MaskedEditMixin: 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 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: @@ -6723,6 +6770,12 @@ __i=0 ## 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. ## diff --git a/wxPython/wx/lib/masked/numctrl.py b/wxPython/wx/lib/masked/numctrl.py index 73a837039b..dfcebc9065 100644 --- a/wxPython/wx/lib/masked/numctrl.py +++ b/wxPython/wx/lib/masked/numctrl.py @@ -2,7 +2,7 @@ # Name: wxPython.lib.masked.numctrl.py # Author: Will Sadkin # Created: 09/06/2003 -# Copyright: (c) 2003 by Will Sadkin +# Copyright: (c) 2003-2007 by Will Sadkin # RCS-ID: $Id$ # License: wxWidgets license #---------------------------------------------------------------------------- @@ -81,6 +81,7 @@ masked.NumCtrl: min = None, max = None, limited = False, + limitOnFieldChange = False, selectOnEntry = True, foregroundColour = "Black", signedForegroundColour = "Red", @@ -155,6 +156,12 @@ masked.NumCtrl: 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 @@ -312,6 +319,18 @@ 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 @@ -390,7 +409,7 @@ MININT = -maxint-1 from wx.tools.dbg import Logger from wx.lib.masked import MaskedEditMixin, Field, BaseMaskedTextCtrl -dbg = Logger() +##dbg = Logger() ##dbg(enable=1) #---------------------------------------------------------------------------- @@ -442,6 +461,7 @@ class NumCtrlAccessorsMixin: 'emptyInvalid', 'validFunc', 'validRequired', + 'stopFieldChangeIfInvalid', ) for param in exposed_basectrl_params: propname = param[0].upper() + param[1:] @@ -478,6 +498,7 @@ class NumCtrl(BaseMaskedTextCtrl, NumCtrlAccessorsMixin): '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", @@ -759,6 +780,12 @@ class NumCtrl(BaseMaskedTextCtrl, NumCtrlAccessorsMixin): 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) @@ -923,6 +950,43 @@ class NumCtrl(BaseMaskedTextCtrl, NumCtrlAccessorsMixin): 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 @@ -1346,7 +1410,32 @@ class NumCtrl(BaseMaskedTextCtrl, NumCtrlAccessorsMixin): def GetLimited(self): """ (For regularization of property accessors) """ - return self.IsLimited + 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): @@ -1794,6 +1883,13 @@ __i=0 ## 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. ## diff --git a/wxPython/wx/lib/masked/textctrl.py b/wxPython/wx/lib/masked/textctrl.py index d374d7b247..a2953913fc 100644 --- a/wxPython/wx/lib/masked/textctrl.py +++ b/wxPython/wx/lib/masked/textctrl.py @@ -157,13 +157,25 @@ class BaseMaskedTextCtrl( wx.TextCtrl, MaskedEditMixin ): 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")' % locals(), indent=1) +## 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 @@ -171,10 +183,27 @@ class BaseMaskedTextCtrl( wx.TextCtrl, MaskedEditMixin ): masked control. NOTE: this must be done in the class derived from the base wx control. """ -## dbg('MaskedTextCtrl::SetValue = "%s"' % value, indent=1) + 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: - wx.TextCtrl.SetValue(self, value) # revert to base control behavior + 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: @@ -198,7 +227,7 @@ class BaseMaskedTextCtrl( wx.TextCtrl, MaskedEditMixin ): value = value[1:] ## dbg('padded value = "%s"' % value) - # make SetValue behave the same as if you had typed the value in: + # 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: @@ -220,10 +249,12 @@ class BaseMaskedTextCtrl( wx.TextCtrl, MaskedEditMixin ): else: ## dbg('exception thrown', indent=0) raise - - self._SetValue(value) # note: to preserve similar capability, .SetValue() - # does not change IsModified() -#### dbg('queuing insertion after .SetValue', replace_to) + 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) @@ -372,6 +403,10 @@ class PreMaskedTextCtrl( BaseMaskedTextCtrl, MaskedEditAccessorsMixin ): __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. diff --git a/wxPython/wx/lib/mixins/treemixin.py b/wxPython/wx/lib/mixins/treemixin.py index 41ad9fb8c6..5df6a37d03 100644 --- a/wxPython/wx/lib/mixins/treemixin.py +++ b/wxPython/wx/lib/mixins/treemixin.py @@ -1,4 +1,4 @@ -''' +""" treemixin.py This module provides three mixin classes that can be used with tree @@ -18,7 +18,7 @@ controls: all items in the tree to restore it later. All mixin classes work with wx.TreeCtrl, wx.gizmos.TreeListCtrl, -and wx.lib.customtree.CustomTreeCtrl. They can be used together or +and wx.lib.customtreectrl.CustomTreeCtrl. They can be used together or separately. The VirtualTree and DragAndDrop mixins force the wx.TR_HIDE_ROOT style. @@ -30,15 +30,15 @@ Date: 15 April 2007 ExpansionState is based on code and ideas from Karsten Hilbert. Andrea Gavana provided help with the CustomTreeCtrl integration. -''' +""" -import wx, wx.lib.customtreectrl +import wx class TreeAPIHarmonizer(object): - ''' This class attempts to hide the differences in API between the - different tree controls that are part of wxPython. ''' + """ 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, @@ -165,12 +165,12 @@ class TreeAPIHarmonizer(object): **kwargs) def HitTest(self, *args, **kwargs): - ''' HitTest returns a two-tuple (item, flags) for tree controls + """ 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. ''' + to be True. """ alwaysReturnColumn = kwargs.pop('alwaysReturnColumn', False) hitTestResult = super(TreeAPIHarmonizer, self).HitTest(*args, **kwargs) if len(hitTestResult) == 2 and alwaysReturnColumn: @@ -209,11 +209,11 @@ class TreeAPIHarmonizer(object): class TreeHelper(object): - ''' This class provides methods that are not part of the API of any - tree control, but are convenient to have available. ''' + """ 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. ''' + """ Return the children of item as a list. """ if not item: item = self.GetRootItem() if not item: @@ -228,7 +228,7 @@ class TreeHelper(object): return children def GetIndexOfItem(self, item): - ''' Return the index of item. ''' + """ Return the index of item. """ parent = self.GetItemParent(item) if parent: parentIndices = self.GetIndexOfItem(parent) @@ -238,7 +238,7 @@ class TreeHelper(object): return () def GetItemByIndex(self, index): - ''' Return the item specified by index. ''' + """ Return the item specified by index. """ item = self.GetRootItem() for i in index: children = self.GetItemChildren(item) @@ -247,7 +247,7 @@ class TreeHelper(object): class VirtualTree(TreeAPIHarmonizer, TreeHelper): - ''' This is a mixin class that can be used to allow for virtual tree + """ 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. @@ -279,7 +279,7 @@ class VirtualTree(TreeAPIHarmonizer, TreeHelper): 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) | \ @@ -289,69 +289,69 @@ class VirtualTree(TreeAPIHarmonizer, TreeHelper): self.Bind(wx.EVT_TREE_ITEM_COLLAPSED, self.OnItemCollapsed) def OnGetChildrenCount(self, index): - ''' This function *must* be overloaded in the derived class. + """ 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. ''' + root items. """ raise NotImplementedError def OnGetItemText(self, index, column=0): - ''' This function *must* be overloaded in the derived class. It + """ This function *must* be overloaded in the derived class. It should return the string containing the text of the specified - item. ''' + 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. ''' + """ 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 + """ 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. ''' + specified item. """ return wx.NullColour def OnGetItemBackgroundColour(self, index): - ''' This function may be overloaded in the derived class. It + """ 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. ''' + 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 + """ 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. ''' + to associate an ImageList with the tree control. """ return -1 def OnGetItemType(self, index): - ''' This function may be overloaded in the derived class, but + """ 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. ''' + should return whether the item is actually checked. """ return 0 def OnGetItemChecked(self, index): - ''' This function may be overloaded in the derived class, but + """ 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. ''' + (radiobutton) for this item. """ return False def RefreshItems(self): - ''' Redraws all visible items. ''' + """ 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. ''' + """ Redraws the item with the specified index. """ try: item = self.GetItemByIndex(index) except IndexError: @@ -362,8 +362,8 @@ class VirtualTree(TreeAPIHarmonizer, TreeHelper): 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. ''' + """ 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) @@ -377,7 +377,7 @@ class VirtualTree(TreeAPIHarmonizer, TreeHelper): self.Delete(child) def RefreshItemRecursively(self, item, itemIndex): - ''' Refresh the item and its children recursively. ''' + """ 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 @@ -388,7 +388,7 @@ class VirtualTree(TreeAPIHarmonizer, TreeHelper): self.SetItemHasChildren(item, hasChildren) def DoRefreshItem(self, item, index, hasChildren): - ''' Refresh one item. ''' + """ Refresh one item. """ item = self.RefreshItemType(item, index) self.RefreshItemText(item, index) self.RefreshColumns(item, index) @@ -458,7 +458,7 @@ class VirtualTree(TreeAPIHarmonizer, TreeHelper): event.Skip() def __refreshAttribute(self, item, index, attribute, *args): - ''' Refresh the specified attribute if necessary. ''' + """ 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) @@ -467,7 +467,7 @@ class VirtualTree(TreeAPIHarmonizer, TreeHelper): class DragAndDrop(TreeAPIHarmonizer, TreeHelper): - ''' This is a mixin class that can be used to easily implement + """ 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. @@ -480,7 +480,7 @@ class DragAndDrop(TreeAPIHarmonizer, TreeHelper): 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. ''' + 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) | \ @@ -489,11 +489,11 @@ class DragAndDrop(TreeAPIHarmonizer, TreeHelper): self.Bind(wx.EVT_TREE_BEGIN_DRAG, self.OnBeginDrag) def OnDrop(self, dropItem, dragItem): - ''' This function must be overloaded in the derived class. + """ 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. ''' + tree control. """ raise NotImplementedError def OnBeginDrag(self, event): @@ -567,7 +567,7 @@ class DragAndDrop(TreeAPIHarmonizer, TreeHelper): class ExpansionState(TreeAPIHarmonizer, TreeHelper): - ''' This is a mixin class that can be used to save and restore + """ 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. @@ -590,20 +590,20 @@ class ExpansionState(TreeAPIHarmonizer, TreeHelper): 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. ''' + number in case of books. """ def GetItemIdentity(self, item): - ''' Return a hashable object that represents the identity of the + """ 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. ''' + 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). ''' + """ GetExpansionState() -> list of expanded items. Expanded items + are coded as determined by the result of GetItemIdentity(item). """ root = self.GetRootItem() if not root: return [] @@ -613,9 +613,9 @@ class ExpansionState(TreeAPIHarmonizer, TreeHelper): return self.GetExpansionStateOfItem(root) def SetExpansionState(self, listOfExpandedItems): - ''' SetExpansionState(listOfExpandedItems). Expands all tree items + """ SetExpansionState(listOfExpandedItems). Expands all tree items whose identity, as determined by GetItemIdentity(item), is present - in the list and collapses all other tree items. ''' + in the list and collapses all other tree items. """ root = self.GetRootItem() if not root: return diff --git a/wxPython/wx/tools/pywxrc.py b/wxPython/wx/tools/pywxrc.py index 9093b2e125..459a07674a 100644 --- a/wxPython/wx/tools/pywxrc.py +++ b/wxPython/wx/tools/pywxrc.py @@ -89,12 +89,19 @@ class xrc%(windowName)s(wx.%(windowClass)s): pre.thisown = 0 if hasattr(self, '_setOORInfo'): self._setOORInfo(self) - if hasattr(self, '_setCallbackInfo'): - self._setCallbackInfo(self, self.__class__) # Define variables for the menu items """ + MENUBAR_CLASS_HEADER = """\ +class xrc%(windowName)s(wx.%(windowClass)s): + def __init__(self): + pre = get_resources().LoadMenuBar("%(windowName)s") + self.PostCreate(pre) + + # Define variables for the menu items +""" + CREATE_MENUITEM_VAR = """\ self.%(widgetName)s = self.FindItemById(xrc.XRCID(\"%(widgetName)s\")) """ @@ -227,7 +234,9 @@ class XmlResourceCompiler: windowClass = re.sub("^wx", "", windowClass) windowName = topWindow.getAttribute("name") - if windowClass in ["Menu", "MenuItem"]: + if windowClass in ["MenuBar"]: + outputList.append(self.templates.MENUBAR_CLASS_HEADER % locals()) + elif windowClass in ["Menu"]: outputList.append(self.templates.MENU_CLASS_HEADER % locals()) else: outputList.append(self.templates.CLASS_HEADER % locals()) diff --git a/wxPython/wxaddons/sized_controls.py b/wxPython/wxaddons/sized_controls.py index 04d74c1fc7..4123752042 100644 --- a/wxPython/wxaddons/sized_controls.py +++ b/wxPython/wxaddons/sized_controls.py @@ -457,10 +457,21 @@ class SizedPanel(wx.PyPanel): self.sizerType = "vertical" def AddChild(self, child): - wx.PyPanel.base_AddChild(self, child) - + if wx.VERSION < (2,8): + wx.PyPanel.base_AddChild(self, child) + else: + wx.PyPanel.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 -- 2.45.2