3     A simple object-oriented drawing program. 
   5     This is completely free software; please feel free to adapt or use this in 
   8     Author: Erik Westra (ewestra@wave.co.nz) 
  10     ######################################################################### 
  14     pySketch requires wxPython version 2.3.  If you are running an earlier 
  15     version, you need to patch your copy of wxPython to fix a bug which will 
  16     cause the "Edit Text Object" dialog box to crash. 
  18     To patch an earlier version of wxPython, edit the wxPython/windows.py file, 
  19     find the wxPyValidator.__init__ method and change the line which reads: 
  21         self._setSelf(self, wxPyValidator, 0) 
  25         self._setSelf(self, wxPyValidator, 1) 
  27     This fixes a known bug in wxPython 2.2.5 (and possibly earlier) which has 
  28     now been fixed in wxPython 2.3. 
  30     ######################################################################### 
  34       * Add ARGV checking to see if a document was double-clicked on. 
  38       * Scrolling the window causes the drawing panel to be mucked up until you 
  39         refresh it.  I've got no idea why. 
  41       * I suspect that the reference counting for some wxPoint objects is 
  42         getting mucked up; when the user quits, we get errors about being 
  43         unable to call del on a 'None' object. 
  46 import cPickle
, os
.path
 
  47 from wxPython
.wx 
import * 
  49 import traceback
, types
 
  51 #---------------------------------------------------------------------------- 
  53 #---------------------------------------------------------------------------- 
  57 menu_UNDO          
= 10001 # Edit menu items. 
  58 menu_SELECT_ALL    
= 10002 
  59 menu_DUPLICATE     
= 10003 
  60 menu_EDIT_TEXT     
= 10004 
  63 menu_SELECT        
= 10101 # Tools menu items. 
  69 menu_MOVE_FORWARD  
= 10201 # Object menu items. 
  70 menu_MOVE_TO_FRONT 
= 10202 
  71 menu_MOVE_BACKWARD 
= 10203 
  72 menu_MOVE_TO_BACK  
= 10204 
  74 menu_ABOUT         
= 10205 # Help menu items. 
  84 # Our tool option IDs: 
  97 # DrawObject type IDs: 
 104 # Selection handle IDs: 
 109 handle_BOTTOM_LEFT  
= 4 
 110 handle_BOTTOM_RIGHT 
= 5 
 111 handle_START_POINT  
= 6 
 114 # Dragging operations: 
 121 # Visual Feedback types: 
 127 # Mouse-event action parameter types: 
 132 # Size of the drawing page, in pixels. 
 137 #---------------------------------------------------------------------------- 
 139 class DrawingFrame(wxFrame
): 
 140     """ A frame showing the contents of a single document. """ 
 142     # ========================================== 
 143     # == Initialisation and Window Management == 
 144     # ========================================== 
 146     def __init__(self
, parent
, id, title
, fileName
=None): 
 147         """ Standard constructor. 
 149             'parent', 'id' and 'title' are all passed to the standard wxFrame 
 150             constructor.  'fileName' is the name and path of a saved file to 
 151             load into this frame, if any. 
 153         wxFrame
.__init
__(self
, parent
, id, title
, 
 154                          style 
= wxDEFAULT_FRAME_STYLE | wxWANTS_CHARS |
 
 155                                  wxNO_FULL_REPAINT_ON_RESIZE
) 
 157         # Setup our menu bar. 
 159         menuBar 
= wxMenuBar() 
 161         self
.fileMenu 
= wxMenu() 
 162         self
.fileMenu
.Append(wxID_NEW
,    "New\tCTRL-N") 
 163         self
.fileMenu
.Append(wxID_OPEN
,   "Open...\tCTRL-O") 
 164         self
.fileMenu
.Append(wxID_CLOSE
,  "Close\tCTRL-W") 
 165         self
.fileMenu
.AppendSeparator() 
 166         self
.fileMenu
.Append(wxID_SAVE
,   "Save\tCTRL-S") 
 167         self
.fileMenu
.Append(wxID_SAVEAS
, "Save As...") 
 168         self
.fileMenu
.Append(wxID_REVERT
, "Revert...") 
 169         self
.fileMenu
.AppendSeparator() 
 170         self
.fileMenu
.Append(wxID_EXIT
,   "Quit\tCTRL-Q") 
 172         menuBar
.Append(self
.fileMenu
, "File") 
 174         self
.editMenu 
= wxMenu() 
 175         self
.editMenu
.Append(menu_UNDO
,          "Undo\tCTRL-Z") 
 176         self
.editMenu
.AppendSeparator() 
 177         self
.editMenu
.Append(menu_SELECT_ALL
,    "Select All\tCTRL-A") 
 178         self
.editMenu
.AppendSeparator() 
 179         self
.editMenu
.Append(menu_DUPLICATE
,     "Duplicate\tCTRL-D") 
 180         self
.editMenu
.Append(menu_EDIT_TEXT
,     "Edit...\tCTRL-E") 
 181         self
.editMenu
.Append(menu_DELETE
,        "Delete\tDEL") 
 183         menuBar
.Append(self
.editMenu
, "Edit") 
 185         self
.toolsMenu 
= wxMenu() 
 186         self
.toolsMenu
.Append(menu_SELECT
,  "Selection", kind
=wxITEM_CHECK
) 
 187         self
.toolsMenu
.Append(menu_LINE
,    "Line",      kind
=wxITEM_CHECK
) 
 188         self
.toolsMenu
.Append(menu_RECT
,    "Rectangle", kind
=wxITEM_CHECK
) 
 189         self
.toolsMenu
.Append(menu_ELLIPSE
, "Ellipse",   kind
=wxITEM_CHECK
) 
 190         self
.toolsMenu
.Append(menu_TEXT
,    "Text",      kind
=wxITEM_CHECK
) 
 192         menuBar
.Append(self
.toolsMenu
, "Tools") 
 194         self
.objectMenu 
= wxMenu() 
 195         self
.objectMenu
.Append(menu_MOVE_FORWARD
,  "Move Forward") 
 196         self
.objectMenu
.Append(menu_MOVE_TO_FRONT
, "Move to Front\tCTRL-F") 
 197         self
.objectMenu
.Append(menu_MOVE_BACKWARD
, "Move Backward") 
 198         self
.objectMenu
.Append(menu_MOVE_TO_BACK
,  "Move to Back\tCTRL-B") 
 200         menuBar
.Append(self
.objectMenu
, "Object") 
 202         self
.helpMenu 
= wxMenu() 
 203         self
.helpMenu
.Append(menu_ABOUT
, "About pySketch...") 
 205         menuBar
.Append(self
.helpMenu
, "Help") 
 207         self
.SetMenuBar(menuBar
) 
 209         # Create our toolbar. 
 211         self
.toolbar 
= self
.CreateToolBar(wxTB_HORIZONTAL |
 
 212                                           wxNO_BORDER | wxTB_FLAT
) 
 214         self
.toolbar
.AddSimpleTool(wxID_NEW
, 
 215                                    wxBitmap("images/new.bmp", 
 218         self
.toolbar
.AddSimpleTool(wxID_OPEN
, 
 219                                    wxBitmap("images/open.bmp", 
 222         self
.toolbar
.AddSimpleTool(wxID_SAVE
, 
 223                                    wxBitmap("images/save.bmp", 
 226         self
.toolbar
.AddSeparator() 
 227         self
.toolbar
.AddSimpleTool(menu_UNDO
, 
 228                                    wxBitmap("images/undo.bmp", 
 231         self
.toolbar
.AddSeparator() 
 232         self
.toolbar
.AddSimpleTool(menu_DUPLICATE
, 
 233                                    wxBitmap("images/duplicate.bmp", 
 236         self
.toolbar
.AddSeparator() 
 237         self
.toolbar
.AddSimpleTool(menu_MOVE_FORWARD
, 
 238                                    wxBitmap("images/moveForward.bmp", 
 241         self
.toolbar
.AddSimpleTool(menu_MOVE_BACKWARD
, 
 242                                    wxBitmap("images/moveBack.bmp", 
 246         self
.toolbar
.Realize() 
 248         # Associate each menu/toolbar item with the method that handles that 
 251         EVT_MENU(self
, wxID_NEW
,    self
.doNew
) 
 252         EVT_MENU(self
, wxID_OPEN
,   self
.doOpen
) 
 253         EVT_MENU(self
, wxID_CLOSE
,  self
.doClose
) 
 254         EVT_MENU(self
, wxID_SAVE
,   self
.doSave
) 
 255         EVT_MENU(self
, wxID_SAVEAS
, self
.doSaveAs
) 
 256         EVT_MENU(self
, wxID_REVERT
, self
.doRevert
) 
 257         EVT_MENU(self
, wxID_EXIT
,   self
.doExit
) 
 259         EVT_MENU(self
, menu_UNDO
,          self
.doUndo
) 
 260         EVT_MENU(self
, menu_SELECT_ALL
,    self
.doSelectAll
) 
 261         EVT_MENU(self
, menu_DUPLICATE
,     self
.doDuplicate
) 
 262         EVT_MENU(self
, menu_EDIT_TEXT
,     self
.doEditText
) 
 263         EVT_MENU(self
, menu_DELETE
,        self
.doDelete
) 
 265         EVT_MENU(self
, menu_SELECT
,  self
.doChooseSelectTool
) 
 266         EVT_MENU(self
, menu_LINE
,    self
.doChooseLineTool
) 
 267         EVT_MENU(self
, menu_RECT
,    self
.doChooseRectTool
) 
 268         EVT_MENU(self
, menu_ELLIPSE
, self
.doChooseEllipseTool
) 
 269         EVT_MENU(self
, menu_TEXT
,    self
.doChooseTextTool
) 
 271         EVT_MENU(self
, menu_MOVE_FORWARD
,  self
.doMoveForward
) 
 272         EVT_MENU(self
, menu_MOVE_TO_FRONT
, self
.doMoveToFront
) 
 273         EVT_MENU(self
, menu_MOVE_BACKWARD
, self
.doMoveBackward
) 
 274         EVT_MENU(self
, menu_MOVE_TO_BACK
,  self
.doMoveToBack
) 
 276         EVT_MENU(self
, menu_ABOUT
, self
.doShowAbout
) 
 278         # Install our own method to handle closing the window.  This allows us 
 279         # to ask the user if he/she wants to save before closing the window, as 
 280         # well as keeping track of which windows are currently open. 
 282         EVT_CLOSE(self
, self
.doClose
) 
 284         # Install our own method for handling keystrokes.  We use this to let 
 285         # the user move the selected object(s) around using the arrow keys. 
 287         EVT_CHAR_HOOK(self
, self
.onKeyEvent
) 
 289         # Setup our top-most panel.  This holds the entire contents of the 
 290         # window, excluding the menu bar. 
 292         self
.topPanel 
= wxPanel(self
, -1, style
=wxSIMPLE_BORDER
) 
 294         # Setup our tool palette, with all our drawing tools and option icons. 
 296         self
.toolPalette 
= wxBoxSizer(wxVERTICAL
) 
 298         self
.selectIcon  
= ToolPaletteIcon(self
.topPanel
, id_SELECT
, 
 299                                            "select", "Selection Tool") 
 300         self
.lineIcon    
= ToolPaletteIcon(self
.topPanel
, id_LINE
, 
 302         self
.rectIcon    
= ToolPaletteIcon(self
.topPanel
, id_RECT
, 
 303                                            "rect", "Rectangle Tool") 
 304         self
.ellipseIcon 
= ToolPaletteIcon(self
.topPanel
, id_ELLIPSE
, 
 305                                            "ellipse", "Ellipse Tool") 
 306         self
.textIcon    
= ToolPaletteIcon(self
.topPanel
, id_TEXT
, 
 309         toolSizer 
= wxGridSizer(0, 2, 5, 5) 
 310         toolSizer
.Add(self
.selectIcon
) 
 311         toolSizer
.Add((0, 0)) # Gap to make tool icons line up nicely. 
 312         toolSizer
.Add(self
.lineIcon
) 
 313         toolSizer
.Add(self
.rectIcon
) 
 314         toolSizer
.Add(self
.ellipseIcon
) 
 315         toolSizer
.Add(self
.textIcon
) 
 317         self
.optionIndicator 
= ToolOptionIndicator(self
.topPanel
) 
 318         self
.optionIndicator
.SetToolTip( 
 319                 wxToolTip("Shows Current Pen/Fill/Line Size Settings")) 
 321         optionSizer 
= wxBoxSizer(wxHORIZONTAL
) 
 323         self
.penOptIcon  
= ToolPaletteIcon(self
.topPanel
, id_PEN_OPT
, 
 324                                            "penOpt", "Set Pen Colour") 
 325         self
.fillOptIcon 
= ToolPaletteIcon(self
.topPanel
, id_FILL_OPT
, 
 326                                            "fillOpt", "Set Fill Colour") 
 327         self
.lineOptIcon 
= ToolPaletteIcon(self
.topPanel
, id_LINE_OPT
, 
 328                                            "lineOpt", "Set Line Size") 
 330         margin 
= wxLEFT | wxRIGHT
 
 331         optionSizer
.Add(self
.penOptIcon
,  0, margin
, 1) 
 332         optionSizer
.Add(self
.fillOptIcon
, 0, margin
, 1) 
 333         optionSizer
.Add(self
.lineOptIcon
, 0, margin
, 1) 
 335         margin 
= wxTOP | wxLEFT | wxRIGHT | wxALIGN_CENTRE
 
 336         self
.toolPalette
.Add(toolSizer
,            0, margin
, 5) 
 337         self
.toolPalette
.Add((0, 0),               0, margin
, 5) # Spacer. 
 338         self
.toolPalette
.Add(self
.optionIndicator
, 0, margin
, 5) 
 339         self
.toolPalette
.Add(optionSizer
,          0, margin
, 5) 
 341         # Make the tool palette icons respond when the user clicks on them. 
 343         EVT_BUTTON(self
.selectIcon
,  -1, self
.onToolIconClick
) 
 344         EVT_BUTTON(self
.lineIcon
,    -1, self
.onToolIconClick
) 
 345         EVT_BUTTON(self
.rectIcon
,    -1, self
.onToolIconClick
) 
 346         EVT_BUTTON(self
.ellipseIcon
, -1, self
.onToolIconClick
) 
 347         EVT_BUTTON(self
.textIcon
,    -1, self
.onToolIconClick
) 
 348         EVT_BUTTON(self
.penOptIcon
,  -1, self
.onPenOptionIconClick
) 
 349         EVT_BUTTON(self
.fillOptIcon
, -1, self
.onFillOptionIconClick
) 
 350         EVT_BUTTON(self
.lineOptIcon
, -1, self
.onLineOptionIconClick
) 
 352         # Setup the main drawing area. 
 354         self
.drawPanel 
= wxScrolledWindow(self
.topPanel
, -1, 
 355                                           style
=wxSUNKEN_BORDER
) 
 356         self
.drawPanel
.SetBackgroundColour(wxWHITE
) 
 358         self
.drawPanel
.EnableScrolling(True, True) 
 359         self
.drawPanel
.SetScrollbars(20, 20, PAGE_WIDTH 
/ 20, PAGE_HEIGHT 
/ 20) 
 361         EVT_LEFT_DOWN(self
.drawPanel
, self
.onMouseEvent
) 
 362         EVT_LEFT_DCLICK(self
.drawPanel
, self
.onDoubleClickEvent
) 
 363         EVT_RIGHT_DOWN(self
.drawPanel
, self
.onRightClick
) 
 364         EVT_MOTION(self
.drawPanel
, self
.onMouseEvent
) 
 365         EVT_LEFT_UP(self
.drawPanel
, self
.onMouseEvent
) 
 366         EVT_PAINT(self
.drawPanel
, self
.onPaintEvent
) 
 368         # Position everything in the window. 
 370         topSizer 
= wxBoxSizer(wxHORIZONTAL
) 
 371         topSizer
.Add(self
.toolPalette
, 0) 
 372         topSizer
.Add(self
.drawPanel
, 1, wxEXPAND
) 
 374         self
.topPanel
.SetAutoLayout(True) 
 375         self
.topPanel
.SetSizer(topSizer
) 
 377         self
.SetSizeHints(minW
=250, minH
=200) 
 378         self
.SetSize(wxSize(600, 400)) 
 380         # Select an initial tool. 
 383         self
._setCurrentTool
(self
.selectIcon
) 
 385         # Setup our frame to hold the contents of a sketch document. 
 388         self
.fileName  
= fileName
 
 389         self
.contents  
= []     # front-to-back ordered list of DrawingObjects. 
 390         self
.selection 
= []     # List of selected DrawingObjects. 
 391         self
.undoInfo  
= None   # Saved contents for undo. 
 392         self
.dragMode  
= drag_NONE 
# Current mouse-drag mode. 
 394         if self
.fileName 
!= None: 
 399         # Finally, set our initial pen, fill and line options. 
 401         self
.penColour  
= wxBLACK
 
 402         self
.fillColour 
= wxWHITE
 
 405     # ============================ 
 406     # == Event Handling Methods == 
 407     # ============================ 
 409     def onToolIconClick(self
, event
): 
 410         """ Respond to the user clicking on one of our tool icons. 
 412         iconID 
= event
.GetEventObject().GetId() 
 414         if   iconID 
== id_SELECT
:   self
.doChooseSelectTool() 
 415         elif iconID 
== id_LINE
:     self
.doChooseLineTool() 
 416         elif iconID 
== id_RECT
:     self
.doChooseRectTool() 
 417         elif iconID 
== id_ELLIPSE
:  self
.doChooseEllipseTool() 
 418         elif iconID 
== id_TEXT
:     self
.doChooseTextTool() 
 419         else:                       wxBell(); print "1" 
 422     def onPenOptionIconClick(self
, event
): 
 423         """ Respond to the user clicking on the "Pen Options" icon. 
 425         data 
= wxColourData() 
 426         if len(self
.selection
) == 1: 
 427             data
.SetColour(self
.selection
[0].getPenColour()) 
 429             data
.SetColour(self
.penColour
) 
 431         dialog 
= wxColourDialog(self
, data
) 
 432         if dialog
.ShowModal() == wxID_OK
: 
 433             c 
= dialog
.GetColourData().GetColour() 
 434             self
._setPenColour
(wxColour(c
.Red(), c
.Green(), c
.Blue())) 
 438     def onFillOptionIconClick(self
, event
): 
 439         """ Respond to the user clicking on the "Fill Options" icon. 
 441         data 
= wxColourData() 
 442         if len(self
.selection
) == 1: 
 443             data
.SetColour(self
.selection
[0].getFillColour()) 
 445             data
.SetColour(self
.fillColour
) 
 447         dialog 
= wxColourDialog(self
, data
) 
 448         if dialog
.ShowModal() == wxID_OK
: 
 449             c 
= dialog
.GetColourData().GetColour() 
 450             self
._setFillColour
(wxColour(c
.Red(), c
.Green(), c
.Blue())) 
 453     def onLineOptionIconClick(self
, event
): 
 454         """ Respond to the user clicking on the "Line Options" icon. 
 456         if len(self
.selection
) == 1: 
 457             menu 
= self
._buildLineSizePopup
(self
.selection
[0].getLineSize()) 
 459             menu 
= self
._buildLineSizePopup
(self
.lineSize
) 
 461         pos 
= self
.lineOptIcon
.GetPosition() 
 462         pos
.y 
= pos
.y 
+ self
.lineOptIcon
.GetSize().height
 
 463         self
.PopupMenu(menu
, pos
) 
 467     def onKeyEvent(self
, event
): 
 468         """ Respond to a keypress event. 
 470             We make the arrow keys move the selected object(s) by one pixel in 
 473         if event
.GetKeyCode() == WXK_UP
: 
 474             self
._moveObject
(0, -1) 
 475         elif event
.GetKeyCode() == WXK_DOWN
: 
 476             self
._moveObject
(0, 1) 
 477         elif event
.GetKeyCode() == WXK_LEFT
: 
 478             self
._moveObject
(-1, 0) 
 479         elif event
.GetKeyCode() == WXK_RIGHT
: 
 480             self
._moveObject
(1, 0) 
 485     def onMouseEvent(self
, event
): 
 486         """ Respond to the user clicking on our main drawing panel. 
 488             How we respond depends on the currently selected tool. 
 490         if not (event
.LeftDown() or event
.Dragging() or event
.LeftUp()): 
 491             return # Ignore mouse movement without click/drag. 
 493         if self
.curTool 
== self
.selectIcon
: 
 494             feedbackType 
= feedback_RECT
 
 495             action       
= self
.selectByRectangle
 
 496             actionParam  
= param_RECT
 
 499         elif self
.curTool 
== self
.lineIcon
: 
 500             feedbackType 
= feedback_LINE
 
 501             action       
= self
.createLine
 
 502             actionParam  
= param_LINE
 
 505         elif self
.curTool 
== self
.rectIcon
: 
 506             feedbackType 
= feedback_RECT
 
 507             action       
= self
.createRect
 
 508             actionParam  
= param_RECT
 
 511         elif self
.curTool 
== self
.ellipseIcon
: 
 512             feedbackType 
= feedback_ELLIPSE
 
 513             action       
= self
.createEllipse
 
 514             actionParam  
= param_RECT
 
 517         elif self
.curTool 
== self
.textIcon
: 
 518             feedbackType 
= feedback_RECT
 
 519             action       
= self
.createText
 
 520             actionParam  
= param_RECT
 
 528             mousePt 
= self
._getEventCoordinates
(event
) 
 530                 obj
, handle 
= self
._getObjectAndSelectionHandleAt
(mousePt
) 
 532             if selecting 
and (obj 
!= None) and (handle 
!= handle_NONE
): 
 534                 # The user clicked on an object's selection handle.  Let the 
 535                 # user resize the clicked-on object. 
 537                 self
.dragMode     
= drag_RESIZE
 
 538                 self
.resizeObject 
= obj
 
 540                 if obj
.getType() == obj_LINE
: 
 541                     self
.resizeFeedback 
= feedback_LINE
 
 542                     pos  
= obj
.getPosition() 
 543                     startPt 
= wxPoint(pos
.x 
+ obj
.getStartPt().x
, 
 544                                       pos
.y 
+ obj
.getStartPt().y
) 
 545                     endPt   
= wxPoint(pos
.x 
+ obj
.getEndPt().x
, 
 546                                       pos
.y 
+ obj
.getEndPt().y
) 
 547                     if handle 
== handle_START_POINT
: 
 548                         self
.resizeAnchor  
= endPt
 
 549                         self
.resizeFloater 
= startPt
 
 551                         self
.resizeAnchor  
= startPt
 
 552                         self
.resizeFloater 
= endPt
 
 554                     self
.resizeFeedback 
= feedback_RECT
 
 555                     pos  
= obj
.getPosition() 
 557                     topLeft  
= wxPoint(pos
.x
, pos
.y
) 
 558                     topRight 
= wxPoint(pos
.x 
+ size
.width
, pos
.y
) 
 559                     botLeft  
= wxPoint(pos
.x
, pos
.y 
+ size
.height
) 
 560                     botRight 
= wxPoint(pos
.x 
+ size
.width
, pos
.y 
+ size
.height
) 
 562                     if handle 
== handle_TOP_LEFT
: 
 563                         self
.resizeAnchor  
= botRight
 
 564                         self
.resizeFloater 
= topLeft
 
 565                     elif handle 
== handle_TOP_RIGHT
: 
 566                         self
.resizeAnchor  
= botLeft
 
 567                         self
.resizeFloater 
= topRight
 
 568                     elif handle 
== handle_BOTTOM_LEFT
: 
 569                         self
.resizeAnchor  
= topRight
 
 570                         self
.resizeFloater 
= botLeft
 
 571                     elif handle 
== handle_BOTTOM_RIGHT
: 
 572                         self
.resizeAnchor  
= topLeft
 
 573                         self
.resizeFloater 
= botRight
 
 576                 self
.resizeOffsetX 
= self
.resizeFloater
.x 
- mousePt
.x
 
 577                 self
.resizeOffsetY 
= self
.resizeFloater
.y 
- mousePt
.y
 
 578                 endPt 
= wxPoint(self
.curPt
.x 
+ self
.resizeOffsetX
, 
 579                                 self
.curPt
.y 
+ self
.resizeOffsetY
) 
 580                 self
._drawVisualFeedback
(self
.resizeAnchor
, endPt
, 
 581                                          self
.resizeFeedback
, False) 
 583             elif selecting 
and (self
._getObjectAt
(mousePt
) != None): 
 585                 # The user clicked on an object to select it.  If the user 
 586                 # drags, he/she will move the object. 
 588                 self
.select(self
._getObjectAt
(mousePt
)) 
 589                 self
.dragMode 
= drag_MOVE
 
 590                 self
.moveOrigin 
= mousePt
 
 592                 self
._drawObjectOutline
(0, 0) 
 596                 # The user is dragging out a selection rect or new object. 
 598                 self
.dragOrigin 
= mousePt
 
 600                 self
.drawPanel
.SetCursor(wxCROSS_CURSOR
) 
 601                 self
.drawPanel
.CaptureMouse() 
 602                 self
._drawVisualFeedback
(mousePt
, mousePt
, feedbackType
, 
 604                 self
.dragMode 
= drag_DRAG
 
 610             if self
.dragMode 
== drag_RESIZE
: 
 612                 # We're resizing an object. 
 614                 mousePt 
= self
._getEventCoordinates
(event
) 
 615                 if (self
.curPt
.x 
!= mousePt
.x
) or (self
.curPt
.y 
!= mousePt
.y
): 
 616                     # Erase previous visual feedback. 
 617                     endPt 
= wxPoint(self
.curPt
.x 
+ self
.resizeOffsetX
, 
 618                                     self
.curPt
.y 
+ self
.resizeOffsetY
) 
 619                     self
._drawVisualFeedback
(self
.resizeAnchor
, endPt
, 
 620                                              self
.resizeFeedback
, False) 
 622                     # Draw new visual feedback. 
 623                     endPt 
= wxPoint(self
.curPt
.x 
+ self
.resizeOffsetX
, 
 624                                     self
.curPt
.y 
+ self
.resizeOffsetY
) 
 625                     self
._drawVisualFeedback
(self
.resizeAnchor
, endPt
, 
 626                                              self
.resizeFeedback
, False) 
 628             elif self
.dragMode 
== drag_MOVE
: 
 630                 # We're moving a selected object. 
 632                 mousePt 
= self
._getEventCoordinates
(event
) 
 633                 if (self
.curPt
.x 
!= mousePt
.x
) or (self
.curPt
.y 
!= mousePt
.y
): 
 634                     # Erase previous visual feedback. 
 635                     self
._drawObjectOutline
(self
.curPt
.x 
- self
.moveOrigin
.x
, 
 636                                             self
.curPt
.y 
- self
.moveOrigin
.y
) 
 638                     # Draw new visual feedback. 
 639                     self
._drawObjectOutline
(self
.curPt
.x 
- self
.moveOrigin
.x
, 
 640                                             self
.curPt
.y 
- self
.moveOrigin
.y
) 
 642             elif self
.dragMode 
== drag_DRAG
: 
 644                 # We're dragging out a new object or selection rect. 
 646                 mousePt 
= self
._getEventCoordinates
(event
) 
 647                 if (self
.curPt
.x 
!= mousePt
.x
) or (self
.curPt
.y 
!= mousePt
.y
): 
 648                     # Erase previous visual feedback. 
 649                     self
._drawVisualFeedback
(self
.dragOrigin
, self
.curPt
, 
 650                                              feedbackType
, dashedLine
) 
 652                     # Draw new visual feedback. 
 653                     self
._drawVisualFeedback
(self
.dragOrigin
, self
.curPt
, 
 654                                              feedbackType
, dashedLine
) 
 660             if self
.dragMode 
== drag_RESIZE
: 
 662                 # We're resizing an object. 
 664                 mousePt 
= self
._getEventCoordinates
(event
) 
 665                 # Erase last visual feedback. 
 666                 endPt 
= wxPoint(self
.curPt
.x 
+ self
.resizeOffsetX
, 
 667                                 self
.curPt
.y 
+ self
.resizeOffsetY
) 
 668                 self
._drawVisualFeedback
(self
.resizeAnchor
, endPt
, 
 669                                          self
.resizeFeedback
, False) 
 671                 resizePt 
= wxPoint(mousePt
.x 
+ self
.resizeOffsetX
, 
 672                                    mousePt
.y 
+ self
.resizeOffsetY
) 
 674                 if (self
.resizeFloater
.x 
!= resizePt
.x
) or \
 
 675                    (self
.resizeFloater
.y 
!= resizePt
.y
): 
 676                    self
._resizeObject
(self
.resizeObject
, 
 681                     self
.drawPanel
.Refresh() # Clean up after empty resize. 
 683             elif self
.dragMode 
== drag_MOVE
: 
 685                 # We're moving a selected object. 
 687                 mousePt 
= self
._getEventCoordinates
(event
) 
 688                 # Erase last visual feedback. 
 689                 self
._drawObjectOutline
(self
.curPt
.x 
- self
.moveOrigin
.x
, 
 690                                         self
.curPt
.y 
- self
.moveOrigin
.y
) 
 691                 if (self
.moveOrigin
.x 
!= mousePt
.x
) or \
 
 692                    (self
.moveOrigin
.y 
!= mousePt
.y
): 
 693                     self
._moveObject
(mousePt
.x 
- self
.moveOrigin
.x
, 
 694                                      mousePt
.y 
- self
.moveOrigin
.y
) 
 696                     self
.drawPanel
.Refresh() # Clean up after empty drag. 
 698             elif self
.dragMode 
== drag_DRAG
: 
 700                 # We're dragging out a new object or selection rect. 
 702                 mousePt 
= self
._getEventCoordinates
(event
) 
 703                 # Erase last visual feedback. 
 704                 self
._drawVisualFeedback
(self
.dragOrigin
, self
.curPt
, 
 705                                          feedbackType
, dashedLine
) 
 706                 self
.drawPanel
.ReleaseMouse() 
 707                 self
.drawPanel
.SetCursor(wxSTANDARD_CURSOR
) 
 708                 # Perform the appropriate action for the current tool. 
 709                 if actionParam 
== param_RECT
: 
 710                     x1 
= min(self
.dragOrigin
.x
, self
.curPt
.x
) 
 711                     y1 
= min(self
.dragOrigin
.y
, self
.curPt
.y
) 
 712                     x2 
= max(self
.dragOrigin
.x
, self
.curPt
.x
) 
 713                     y2 
= max(self
.dragOrigin
.y
, self
.curPt
.y
) 
 721                         if ((x2
-x1
) < 8) or ((y2
-y1
) < 8): return # Too small. 
 723                     action(x1
, y1
, x2
-x1
, y2
-y1
) 
 724                 elif actionParam 
== param_LINE
: 
 725                     action(self
.dragOrigin
.x
, self
.dragOrigin
.y
, 
 726                            self
.curPt
.x
, self
.curPt
.y
) 
 728             self
.dragMode 
= drag_NONE 
# We've finished with this mouse event. 
 732     def onDoubleClickEvent(self
, event
): 
 733         """ Respond to a double-click within our drawing panel. 
 735         mousePt 
= self
._getEventCoordinates
(event
) 
 736         obj 
= self
._getObjectAt
(mousePt
) 
 737         if obj 
== None: return 
 739         # Let the user edit the given object. 
 741         if obj
.getType() == obj_TEXT
: 
 742             editor 
= EditTextObjectDialog(self
, "Edit Text Object") 
 743             editor
.objectToDialog(obj
) 
 744             if editor
.ShowModal() == wxID_CANCEL
: 
 750             editor
.dialogToObject(obj
) 
 754             self
.drawPanel
.Refresh() 
 760     def onRightClick(self
, event
): 
 761         """ Respond to the user right-clicking within our drawing panel. 
 763             We select the clicked-on item, if necessary, and display a pop-up 
 764             menu of available options which can be applied to the selected 
 767         mousePt 
= self
._getEventCoordinates
(event
) 
 768         obj 
= self
._getObjectAt
(mousePt
) 
 770         if obj 
== None: return # Nothing selected. 
 772         # Select the clicked-on object. 
 776         # Build our pop-up menu. 
 779         menu
.Append(menu_DUPLICATE
, "Duplicate") 
 780         menu
.Append(menu_EDIT_TEXT
, "Edit...") 
 781         menu
.Append(menu_DELETE
,    "Delete") 
 782         menu
.AppendSeparator() 
 783         menu
.Append(menu_MOVE_FORWARD
,   "Move Forward") 
 784         menu
.Append(menu_MOVE_TO_FRONT
,  "Move to Front") 
 785         menu
.Append(menu_MOVE_BACKWARD
,  "Move Backward") 
 786         menu
.Append(menu_MOVE_TO_BACK
,   "Move to Back") 
 788         menu
.Enable(menu_EDIT_TEXT
,     obj
.getType() == obj_TEXT
) 
 789         menu
.Enable(menu_MOVE_FORWARD
,  obj 
!= self
.contents
[0]) 
 790         menu
.Enable(menu_MOVE_TO_FRONT
, obj 
!= self
.contents
[0]) 
 791         menu
.Enable(menu_MOVE_BACKWARD
, obj 
!= self
.contents
[-1]) 
 792         menu
.Enable(menu_MOVE_TO_BACK
,  obj 
!= self
.contents
[-1]) 
 794         EVT_MENU(self
, menu_DUPLICATE
,     self
.doDuplicate
) 
 795         EVT_MENU(self
, menu_EDIT_TEXT
,     self
.doEditText
) 
 796         EVT_MENU(self
, menu_DELETE
,        self
.doDelete
) 
 797         EVT_MENU(self
, menu_MOVE_FORWARD
,  self
.doMoveForward
) 
 798         EVT_MENU(self
, menu_MOVE_TO_FRONT
, self
.doMoveToFront
) 
 799         EVT_MENU(self
, menu_MOVE_BACKWARD
, self
.doMoveBackward
) 
 800         EVT_MENU(self
, menu_MOVE_TO_BACK
,  self
.doMoveToBack
) 
 802         # Show the pop-up menu. 
 804         clickPt 
= wxPoint(mousePt
.x 
+ self
.drawPanel
.GetPosition().x
, 
 805                           mousePt
.y 
+ self
.drawPanel
.GetPosition().y
) 
 806         self
.drawPanel
.PopupMenu(menu
, clickPt
) 
 810     def onPaintEvent(self
, event
): 
 811         """ Respond to a request to redraw the contents of our drawing panel. 
 813         dc 
= wxPaintDC(self
.drawPanel
) 
 814         self
.drawPanel
.PrepareDC(dc
) 
 817         for i 
in range(len(self
.contents
)-1, -1, -1): 
 818             obj 
= self
.contents
[i
] 
 819             if obj 
in self
.selection
: 
 826     # ========================== 
 827     # == Menu Command Methods == 
 828     # ========================== 
 830     def doNew(self
, event
): 
 831         """ Respond to the "New" menu command. 
 834         newFrame 
= DrawingFrame(None, -1, "Untitled") 
 836         _docList
.append(newFrame
) 
 839     def doOpen(self
, event
): 
 840         """ Respond to the "Open" menu command. 
 845         fileName 
= wxFileSelector("Open File", default_extension
="psk", 
 846                                   flags 
= wxOPEN | wxFILE_MUST_EXIST
) 
 847         if fileName 
== "": return 
 848         fileName 
= os
.path
.join(os
.getcwd(), fileName
) 
 851         title 
= os
.path
.basename(fileName
) 
 853         if (self
.fileName 
== None) and (len(self
.contents
) == 0): 
 854             # Load contents into current (empty) document. 
 855             self
.fileName 
= fileName
 
 856             self
.SetTitle(os
.path
.basename(fileName
)) 
 859             # Open a new frame for this document. 
 860             newFrame 
= DrawingFrame(None, -1, os
.path
.basename(fileName
), 
 863             _docList
.append(newFrame
) 
 866     def doClose(self
, event
): 
 867         """ Respond to the "Close" menu command. 
 872             if not self
.askIfUserWantsToSave("closing"): return 
 874         _docList
.remove(self
) 
 878     def doSave(self
, event
): 
 879         """ Respond to the "Save" menu command. 
 881         if self
.fileName 
!= None: 
 885     def doSaveAs(self
, event
): 
 886         """ Respond to the "Save As" menu command. 
 888         if self
.fileName 
== None: 
 891             default 
= self
.fileName
 
 894         fileName 
= wxFileSelector("Save File As", "Saving", 
 895                                   default_filename
=default
, 
 896                                   default_extension
="psk", 
 898                                   flags 
= wxSAVE | wxOVERWRITE_PROMPT
) 
 899         if fileName 
== "": return # User cancelled. 
 900         fileName 
= os
.path
.join(os
.getcwd(), fileName
) 
 903         title 
= os
.path
.basename(fileName
) 
 906         self
.fileName 
= fileName
 
 910     def doRevert(self
, event
): 
 911         """ Respond to the "Revert" menu command. 
 913         if not self
.dirty
: return 
 915         if wxMessageBox("Discard changes made to this document?", "Confirm", 
 916                         style 
= wxOK | wxCANCEL | wxICON_QUESTION
, 
 917                         parent
=self
) == wxCANCEL
: return 
 921     def doExit(self
, event
): 
 922         """ Respond to the "Quit" menu command. 
 924         global _docList
, _app
 
 926             if not doc
.dirty
: continue 
 928             if not doc
.askIfUserWantsToSave("quitting"): return 
 935     def doUndo(self
, event
): 
 936         """ Respond to the "Undo" menu command. 
 938         if self
.undoInfo 
== None: return 
 940         undoData 
= self
.undoInfo
 
 941         self
._saveUndoInfo
() # For undoing the undo... 
 945         for type, data 
in undoData
["contents"]: 
 946             obj 
= DrawingObject(type) 
 948             self
.contents
.append(obj
) 
 951         for i 
in undoData
["selection"]: 
 952             self
.selection
.append(self
.contents
[i
]) 
 955         self
.drawPanel
.Refresh() 
 959     def doSelectAll(self
, event
): 
 960         """ Respond to the "Select All" menu command. 
 965     def doDuplicate(self
, event
): 
 966         """ Respond to the "Duplicate" menu command. 
 971         for obj 
in self
.contents
: 
 972             if obj 
in self
.selection
: 
 973                 newObj 
= DrawingObject(obj
.getType()) 
 974                 newObj
.setData(obj
.getData()) 
 975                 pos 
= obj
.getPosition() 
 976                 newObj
.setPosition(wxPoint(pos
.x 
+ 10, pos
.y 
+ 10)) 
 979         self
.contents 
= objs 
+ self
.contents
 
 981         self
.selectMany(objs
) 
 984     def doEditText(self
, event
): 
 985         """ Respond to the "Edit Text" menu command. 
 987         if len(self
.selection
) != 1: return 
 989         obj 
= self
.selection
[0] 
 990         if obj
.getType() != obj_TEXT
: return 
 992         editor 
= EditTextObjectDialog(self
, "Edit Text Object") 
 993         editor
.objectToDialog(obj
) 
 994         if editor
.ShowModal() == wxID_CANCEL
: 
1000         editor
.dialogToObject(obj
) 
1004         self
.drawPanel
.Refresh() 
1008     def doDelete(self
, event
): 
1009         """ Respond to the "Delete" menu command. 
1011         self
._saveUndoInfo
() 
1013         for obj 
in self
.selection
: 
1014             self
.contents
.remove(obj
) 
1019     def doChooseSelectTool(self
, event
=None): 
1020         """ Respond to the "Select Tool" menu command. 
1022         self
._setCurrentTool
(self
.selectIcon
) 
1023         self
.drawPanel
.SetCursor(wxSTANDARD_CURSOR
) 
1027     def doChooseLineTool(self
, event
=None): 
1028         """ Respond to the "Line Tool" menu command. 
1030         self
._setCurrentTool
(self
.lineIcon
) 
1031         self
.drawPanel
.SetCursor(wxCROSS_CURSOR
) 
1036     def doChooseRectTool(self
, event
=None): 
1037         """ Respond to the "Rect Tool" menu command. 
1039         self
._setCurrentTool
(self
.rectIcon
) 
1040         self
.drawPanel
.SetCursor(wxCROSS_CURSOR
) 
1045     def doChooseEllipseTool(self
, event
=None): 
1046         """ Respond to the "Ellipse Tool" menu command. 
1048         self
._setCurrentTool
(self
.ellipseIcon
) 
1049         self
.drawPanel
.SetCursor(wxCROSS_CURSOR
) 
1054     def doChooseTextTool(self
, event
=None): 
1055         """ Respond to the "Text Tool" menu command. 
1057         self
._setCurrentTool
(self
.textIcon
) 
1058         self
.drawPanel
.SetCursor(wxCROSS_CURSOR
) 
1063     def doMoveForward(self
, event
): 
1064         """ Respond to the "Move Forward" menu command. 
1066         if len(self
.selection
) != 1: return 
1068         self
._saveUndoInfo
() 
1070         obj 
= self
.selection
[0] 
1071         index 
= self
.contents
.index(obj
) 
1072         if index 
== 0: return 
1074         del self
.contents
[index
] 
1075         self
.contents
.insert(index
-1, obj
) 
1077         self
.drawPanel
.Refresh() 
1081     def doMoveToFront(self
, event
): 
1082         """ Respond to the "Move to Front" menu command. 
1084         if len(self
.selection
) != 1: return 
1086         self
._saveUndoInfo
() 
1088         obj 
= self
.selection
[0] 
1089         self
.contents
.remove(obj
) 
1090         self
.contents
.insert(0, obj
) 
1092         self
.drawPanel
.Refresh() 
1096     def doMoveBackward(self
, event
): 
1097         """ Respond to the "Move Backward" menu command. 
1099         if len(self
.selection
) != 1: return 
1101         self
._saveUndoInfo
() 
1103         obj 
= self
.selection
[0] 
1104         index 
= self
.contents
.index(obj
) 
1105         if index 
== len(self
.contents
) - 1: return 
1107         del self
.contents
[index
] 
1108         self
.contents
.insert(index
+1, obj
) 
1110         self
.drawPanel
.Refresh() 
1114     def doMoveToBack(self
, event
): 
1115         """ Respond to the "Move to Back" menu command. 
1117         if len(self
.selection
) != 1: return 
1119         self
._saveUndoInfo
() 
1121         obj 
= self
.selection
[0] 
1122         self
.contents
.remove(obj
) 
1123         self
.contents
.append(obj
) 
1125         self
.drawPanel
.Refresh() 
1129     def doShowAbout(self
, event
): 
1130         """ Respond to the "About pySketch" menu command. 
1132         dialog 
= wxDialog(self
, -1, "About pySketch") # , 
1133                           #style=wxDIALOG_MODAL | wxSTAY_ON_TOP) 
1134         dialog
.SetBackgroundColour(wxWHITE
) 
1136         panel 
= wxPanel(dialog
, -1) 
1137         panel
.SetBackgroundColour(wxWHITE
) 
1139         panelSizer 
= wxBoxSizer(wxVERTICAL
) 
1141         boldFont 
= wxFont(panel
.GetFont().GetPointSize(), 
1142                           panel
.GetFont().GetFamily(), 
1145         logo 
= wxStaticBitmap(panel
, -1, wxBitmap("images/logo.bmp", 
1148         lab1 
= wxStaticText(panel
, -1, "pySketch") 
1149         lab1
.SetFont(wxFont(36, boldFont
.GetFamily(), wxITALIC
, wxBOLD
)) 
1150         lab1
.SetSize(lab1
.GetBestSize()) 
1152         imageSizer 
= wxBoxSizer(wxHORIZONTAL
) 
1153         imageSizer
.Add(logo
, 0, wxALL | wxALIGN_CENTRE_VERTICAL
, 5) 
1154         imageSizer
.Add(lab1
, 0, wxALL | wxALIGN_CENTRE_VERTICAL
, 5) 
1156         lab2 
= wxStaticText(panel
, -1, "A simple object-oriented drawing " + \
 
1158         lab2
.SetFont(boldFont
) 
1159         lab2
.SetSize(lab2
.GetBestSize()) 
1161         lab3 
= wxStaticText(panel
, -1, "pySketch is completely free " + \
 
1163         lab3
.SetFont(boldFont
) 
1164         lab3
.SetSize(lab3
.GetBestSize()) 
1166         lab4 
= wxStaticText(panel
, -1, "feel free to adapt or use this " + \
 
1167                                        "in any way you like.") 
1168         lab4
.SetFont(boldFont
) 
1169         lab4
.SetSize(lab4
.GetBestSize()) 
1171         lab5 
= wxStaticText(panel
, -1, "Author: Erik Westra " + \
 
1172                                        "(ewestra@wave.co.nz)") 
1173         lab5
.SetFont(boldFont
) 
1174         lab5
.SetSize(lab5
.GetBestSize()) 
1176         btnOK 
= wxButton(panel
, wxID_OK
, "OK") 
1178         panelSizer
.Add(imageSizer
, 0, wxALIGN_CENTRE
) 
1179         panelSizer
.Add((10, 10)) # Spacer. 
1180         panelSizer
.Add(lab2
, 0, wxALIGN_CENTRE
) 
1181         panelSizer
.Add((10, 10)) # Spacer. 
1182         panelSizer
.Add(lab3
, 0, wxALIGN_CENTRE
) 
1183         panelSizer
.Add(lab4
, 0, wxALIGN_CENTRE
) 
1184         panelSizer
.Add((10, 10)) # Spacer. 
1185         panelSizer
.Add(lab5
, 0, wxALIGN_CENTRE
) 
1186         panelSizer
.Add((10, 10)) # Spacer. 
1187         panelSizer
.Add(btnOK
, 0, wxALL | wxALIGN_CENTRE
, 5) 
1189         panel
.SetAutoLayout(True) 
1190         panel
.SetSizer(panelSizer
) 
1191         panelSizer
.Fit(panel
) 
1193         topSizer 
= wxBoxSizer(wxHORIZONTAL
) 
1194         topSizer
.Add(panel
, 0, wxALL
, 10) 
1196         dialog
.SetAutoLayout(True) 
1197         dialog
.SetSizer(topSizer
) 
1198         topSizer
.Fit(dialog
) 
1202         btn 
= dialog
.ShowModal() 
1205     # ============================= 
1206     # == Object Creation Methods == 
1207     # ============================= 
1209     def createLine(self
, x1
, y1
, x2
, y2
): 
1210         """ Create a new line object at the given position and size. 
1212         self
._saveUndoInfo
() 
1214         topLeftX  
= min(x1
, x2
) 
1215         topLeftY  
= min(y1
, y2
) 
1216         botRightX 
= max(x1
, x2
) 
1217         botRightY 
= max(y1
, y2
) 
1219         obj 
= DrawingObject(obj_LINE
, position
=wxPoint(topLeftX
, topLeftY
), 
1220                             size
=wxSize(botRightX
-topLeftX
, 
1221                                         botRightY
-topLeftY
), 
1222                             penColour
=self
.penColour
, 
1223                             fillColour
=self
.fillColour
, 
1224                             lineSize
=self
.lineSize
, 
1225                             startPt 
= wxPoint(x1 
- topLeftX
, y1 
- topLeftY
), 
1226                             endPt   
= wxPoint(x2 
- topLeftX
, y2 
- topLeftY
)) 
1227         self
.contents
.insert(0, obj
) 
1229         self
.doChooseSelectTool() 
1233     def createRect(self
, x
, y
, width
, height
): 
1234         """ Create a new rectangle object at the given position and size. 
1236         self
._saveUndoInfo
() 
1238         obj 
= DrawingObject(obj_RECT
, position
=wxPoint(x
, y
), 
1239                             size
=wxSize(width
, height
), 
1240                             penColour
=self
.penColour
, 
1241                             fillColour
=self
.fillColour
, 
1242                             lineSize
=self
.lineSize
) 
1243         self
.contents
.insert(0, obj
) 
1245         self
.doChooseSelectTool() 
1249     def createEllipse(self
, x
, y
, width
, height
): 
1250         """ Create a new ellipse object at the given position and size. 
1252         self
._saveUndoInfo
() 
1254         obj 
= DrawingObject(obj_ELLIPSE
, position
=wxPoint(x
, y
), 
1255                             size
=wxSize(width
, height
), 
1256                             penColour
=self
.penColour
, 
1257                             fillColour
=self
.fillColour
, 
1258                             lineSize
=self
.lineSize
) 
1259         self
.contents
.insert(0, obj
) 
1261         self
.doChooseSelectTool() 
1265     def createText(self
, x
, y
, width
, height
): 
1266         """ Create a new text object at the given position and size. 
1268         editor 
= EditTextObjectDialog(self
, "Create Text Object") 
1269         if editor
.ShowModal() == wxID_CANCEL
: 
1273         self
._saveUndoInfo
() 
1275         obj 
= DrawingObject(obj_TEXT
, position
=wxPoint(x
, y
), 
1276                                       size
=wxSize(width
, height
)) 
1277         editor
.dialogToObject(obj
) 
1280         self
.contents
.insert(0, obj
) 
1282         self
.doChooseSelectTool() 
1285     # ======================= 
1286     # == Selection Methods == 
1287     # ======================= 
1289     def selectAll(self
): 
1290         """ Select every DrawingObject in our document. 
1293         for obj 
in self
.contents
: 
1294             self
.selection
.append(obj
) 
1295         self
.drawPanel
.Refresh() 
1299     def deselectAll(self
): 
1300         """ Deselect every DrawingObject in our document. 
1303         self
.drawPanel
.Refresh() 
1307     def select(self
, obj
): 
1308         """ Select the given DrawingObject within our document. 
1310         self
.selection 
= [obj
] 
1311         self
.drawPanel
.Refresh() 
1315     def selectMany(self
, objs
): 
1316         """ Select the given list of DrawingObjects. 
1318         self
.selection 
= objs
 
1319         self
.drawPanel
.Refresh() 
1323     def selectByRectangle(self
, x
, y
, width
, height
): 
1324         """ Select every DrawingObject in the given rectangular region. 
1327         for obj 
in self
.contents
: 
1328             if obj
.objectWithinRect(x
, y
, width
, height
): 
1329                 self
.selection
.append(obj
) 
1330         self
.drawPanel
.Refresh() 
1333     # ====================== 
1334     # == File I/O Methods == 
1335     # ====================== 
1337     def loadContents(self
): 
1338         """ Load the contents of our document into memory. 
1340         f 
= open(self
.fileName
, "rb") 
1341         objData 
= cPickle
.load(f
) 
1344         for type, data 
in objData
: 
1345             obj 
= DrawingObject(type) 
1347             self
.contents
.append(obj
) 
1351         self
.undoInfo  
= None 
1353         self
.drawPanel
.Refresh() 
1357     def saveContents(self
): 
1358         """ Save the contents of our document to disk. 
1361         for obj 
in self
.contents
: 
1362             objData
.append([obj
.getType(), obj
.getData()]) 
1364         f 
= open(self
.fileName
, "wb") 
1365         cPickle
.dump(objData
, f
) 
1371     def askIfUserWantsToSave(self
, action
): 
1372         """ Give the user the opportunity to save the current document. 
1374             'action' is a string describing the action about to be taken.  If 
1375             the user wants to save the document, it is saved immediately.  If 
1376             the user cancels, we return False. 
1378         if not self
.dirty
: return True # Nothing to do. 
1380         response 
= wxMessageBox("Save changes before " + action 
+ "?", 
1381                                 "Confirm", wxYES_NO | wxCANCEL
, self
) 
1383         if response 
== wxYES
: 
1384             if self
.fileName 
== None: 
1385                 fileName 
= wxFileSelector("Save File As", "Saving", 
1386                                           default_extension
="psk", 
1388                                           flags 
= wxSAVE | wxOVERWRITE_PROMPT
) 
1389                 if fileName 
== "": return False # User cancelled. 
1390                 self
.fileName 
= fileName
 
1394         elif response 
== wxNO
: 
1395             return True # User doesn't want changes saved. 
1396         elif response 
== wxCANCEL
: 
1397             return False # User cancelled. 
1399     # ===================== 
1400     # == Private Methods == 
1401     # ===================== 
1403     def _adjustMenus(self
): 
1404         """ Adjust our menus and toolbar to reflect the current state of the 
1407         canSave   
= (self
.fileName 
!= None) and self
.dirty
 
1408         canRevert 
= (self
.fileName 
!= None) and self
.dirty
 
1409         canUndo   
= self
.undoInfo 
!= None 
1410         selection 
= len(self
.selection
) > 0 
1411         onlyOne   
= len(self
.selection
) == 1 
1412         isText    
= onlyOne 
and (self
.selection
[0].getType() == obj_TEXT
) 
1413         front     
= onlyOne 
and (self
.selection
[0] == self
.contents
[0]) 
1414         back      
= onlyOne 
and (self
.selection
[0] == self
.contents
[-1]) 
1416         # Enable/disable our menu items. 
1418         self
.fileMenu
.Enable(wxID_SAVE
,   canSave
) 
1419         self
.fileMenu
.Enable(wxID_REVERT
, canRevert
) 
1421         self
.editMenu
.Enable(menu_UNDO
,      canUndo
) 
1422         self
.editMenu
.Enable(menu_DUPLICATE
, selection
) 
1423         self
.editMenu
.Enable(menu_EDIT_TEXT
, isText
) 
1424         self
.editMenu
.Enable(menu_DELETE
,    selection
) 
1426         self
.toolsMenu
.Check(menu_SELECT
,  self
.curTool 
== self
.selectIcon
) 
1427         self
.toolsMenu
.Check(menu_LINE
,    self
.curTool 
== self
.lineIcon
) 
1428         self
.toolsMenu
.Check(menu_RECT
,    self
.curTool 
== self
.rectIcon
) 
1429         self
.toolsMenu
.Check(menu_ELLIPSE
, self
.curTool 
== self
.ellipseIcon
) 
1430         self
.toolsMenu
.Check(menu_TEXT
,    self
.curTool 
== self
.textIcon
) 
1432         self
.objectMenu
.Enable(menu_MOVE_FORWARD
,  onlyOne 
and not front
) 
1433         self
.objectMenu
.Enable(menu_MOVE_TO_FRONT
, onlyOne 
and not front
) 
1434         self
.objectMenu
.Enable(menu_MOVE_BACKWARD
, onlyOne 
and not back
) 
1435         self
.objectMenu
.Enable(menu_MOVE_TO_BACK
,  onlyOne 
and not back
) 
1437         # Enable/disable our toolbar icons. 
1439         self
.toolbar
.EnableTool(wxID_NEW
,           True) 
1440         self
.toolbar
.EnableTool(wxID_OPEN
,          True) 
1441         self
.toolbar
.EnableTool(wxID_SAVE
,          canSave
) 
1442         self
.toolbar
.EnableTool(menu_UNDO
,          canUndo
) 
1443         self
.toolbar
.EnableTool(menu_DUPLICATE
,     selection
) 
1444         self
.toolbar
.EnableTool(menu_MOVE_FORWARD
,  onlyOne 
and not front
) 
1445         self
.toolbar
.EnableTool(menu_MOVE_BACKWARD
, onlyOne 
and not back
) 
1448     def _setCurrentTool(self
, newToolIcon
): 
1449         """ Set the currently selected tool. 
1451         if self
.curTool 
== newToolIcon
: return # Nothing to do. 
1453         if self
.curTool 
!= None: 
1454             self
.curTool
.deselect() 
1456         newToolIcon
.select() 
1457         self
.curTool 
= newToolIcon
 
1460     def _setPenColour(self
, colour
): 
1461         """ Set the default or selected object's pen colour. 
1463         if len(self
.selection
) > 0: 
1464             self
._saveUndoInfo
() 
1465             for obj 
in self
.selection
: 
1466                 obj
.setPenColour(colour
) 
1467             self
.drawPanel
.Refresh() 
1469             self
.penColour 
= colour
 
1470             self
.optionIndicator
.setPenColour(colour
) 
1473     def _setFillColour(self
, colour
): 
1474         """ Set the default or selected object's fill colour. 
1476         if len(self
.selection
) > 0: 
1477             self
._saveUndoInfo
() 
1478             for obj 
in self
.selection
: 
1479                 obj
.setFillColour(colour
) 
1480             self
.drawPanel
.Refresh() 
1482             self
.fillColour 
= colour
 
1483             self
.optionIndicator
.setFillColour(colour
) 
1486     def _setLineSize(self
, size
): 
1487         """ Set the default or selected object's line size. 
1489         if len(self
.selection
) > 0: 
1490             self
._saveUndoInfo
() 
1491             for obj 
in self
.selection
: 
1492                 obj
.setLineSize(size
) 
1493             self
.drawPanel
.Refresh() 
1495             self
.lineSize 
= size
 
1496             self
.optionIndicator
.setLineSize(size
) 
1499     def _saveUndoInfo(self
): 
1500         """ Remember the current state of the document, to allow for undo. 
1502             We make a copy of the document's contents, so that we can return to 
1503             the previous contents if the user does something and then wants to 
1507         for obj 
in self
.contents
: 
1508             savedContents
.append([obj
.getType(), obj
.getData()]) 
1511         for i 
in range(len(self
.contents
)): 
1512             if self
.contents
[i
] in self
.selection
: 
1513                 savedSelection
.append(i
) 
1515         self
.undoInfo 
= {"contents"  : savedContents
, 
1516                          "selection" : savedSelection
} 
1519     def _resizeObject(self
, obj
, anchorPt
, oldPt
, newPt
): 
1520         """ Resize the given object. 
1522             'anchorPt' is the unchanging corner of the object, while the 
1523             opposite corner has been resized.  'oldPt' are the current 
1524             coordinates for this corner, while 'newPt' are the new coordinates. 
1525             The object should fit within the given dimensions, though if the 
1526             new point is less than the anchor point the object will need to be 
1527             moved as well as resized, to avoid giving it a negative size. 
1529         if obj
.getType() == obj_TEXT
: 
1530             # Not allowed to resize text objects -- they're sized to fit text. 
1534         self
._saveUndoInfo
() 
1536         topLeft  
= wxPoint(min(anchorPt
.x
, newPt
.x
), 
1537                            min(anchorPt
.y
, newPt
.y
)) 
1538         botRight 
= wxPoint(max(anchorPt
.x
, newPt
.x
), 
1539                            max(anchorPt
.y
, newPt
.y
)) 
1541         newWidth  
= botRight
.x 
- topLeft
.x
 
1542         newHeight 
= botRight
.y 
- topLeft
.y
 
1544         if obj
.getType() == obj_LINE
: 
1545             # Adjust the line so that its start and end points match the new 
1546             # overall object size. 
1548             startPt 
= obj
.getStartPt() 
1549             endPt   
= obj
.getEndPt() 
1551             slopesDown 
= ((startPt
.x 
< endPt
.x
) and (startPt
.y 
< endPt
.y
)) or \
 
1552                          ((startPt
.x 
> endPt
.x
) and (startPt
.y 
> endPt
.y
)) 
1554             # Handle the user flipping the line. 
1556             hFlip 
= ((anchorPt
.x 
< oldPt
.x
) and (anchorPt
.x 
> newPt
.x
)) or \
 
1557                     ((anchorPt
.x 
> oldPt
.x
) and (anchorPt
.x 
< newPt
.x
)) 
1558             vFlip 
= ((anchorPt
.y 
< oldPt
.y
) and (anchorPt
.y 
> newPt
.y
)) or \
 
1559                     ((anchorPt
.y 
> oldPt
.y
) and (anchorPt
.y 
< newPt
.y
)) 
1561             if (hFlip 
and not vFlip
) or (vFlip 
and not hFlip
): 
1562                 slopesDown 
= not slopesDown 
# Line flipped. 
1565                 obj
.setStartPt(wxPoint(0, 0)) 
1566                 obj
.setEndPt(wxPoint(newWidth
, newHeight
)) 
1568                 obj
.setStartPt(wxPoint(0, newHeight
)) 
1569                 obj
.setEndPt(wxPoint(newWidth
, 0)) 
1571         # Finally, adjust the bounds of the object to match the new dimensions. 
1573         obj
.setPosition(topLeft
) 
1574         obj
.setSize(wxSize(botRight
.x 
- topLeft
.x
, botRight
.y 
- topLeft
.y
)) 
1576         self
.drawPanel
.Refresh() 
1579     def _moveObject(self
, offsetX
, offsetY
): 
1580         """ Move the currently selected object(s) by the given offset. 
1582         self
._saveUndoInfo
() 
1584         for obj 
in self
.selection
: 
1585             pos 
= obj
.getPosition() 
1586             pos
.x 
= pos
.x 
+ offsetX
 
1587             pos
.y 
= pos
.y 
+ offsetY
 
1588             obj
.setPosition(pos
) 
1590         self
.drawPanel
.Refresh() 
1593     def _buildLineSizePopup(self
, lineSize
): 
1594         """ Build the pop-up menu used to set the line size. 
1596             'lineSize' is the current line size value.  The corresponding item 
1597             is checked in the pop-up menu. 
1600         menu
.Append(id_LINESIZE_0
, "no line",      kind
=wxITEM_CHECK
) 
1601         menu
.Append(id_LINESIZE_1
, "1-pixel line", kind
=wxITEM_CHECK
) 
1602         menu
.Append(id_LINESIZE_2
, "2-pixel line", kind
=wxITEM_CHECK
) 
1603         menu
.Append(id_LINESIZE_3
, "3-pixel line", kind
=wxITEM_CHECK
) 
1604         menu
.Append(id_LINESIZE_4
, "4-pixel line", kind
=wxITEM_CHECK
) 
1605         menu
.Append(id_LINESIZE_5
, "5-pixel line", kind
=wxITEM_CHECK
) 
1607         if   lineSize 
== 0: menu
.Check(id_LINESIZE_0
, True) 
1608         elif lineSize 
== 1: menu
.Check(id_LINESIZE_1
, True) 
1609         elif lineSize 
== 2: menu
.Check(id_LINESIZE_2
, True) 
1610         elif lineSize 
== 3: menu
.Check(id_LINESIZE_3
, True) 
1611         elif lineSize 
== 4: menu
.Check(id_LINESIZE_4
, True) 
1612         elif lineSize 
== 5: menu
.Check(id_LINESIZE_5
, True) 
1614         EVT_MENU(self
, id_LINESIZE_0
, self
._lineSizePopupSelected
) 
1615         EVT_MENU(self
, id_LINESIZE_1
, self
._lineSizePopupSelected
) 
1616         EVT_MENU(self
, id_LINESIZE_2
, self
._lineSizePopupSelected
) 
1617         EVT_MENU(self
, id_LINESIZE_3
, self
._lineSizePopupSelected
) 
1618         EVT_MENU(self
, id_LINESIZE_4
, self
._lineSizePopupSelected
) 
1619         EVT_MENU(self
, id_LINESIZE_5
, self
._lineSizePopupSelected
) 
1624     def _lineSizePopupSelected(self
, event
): 
1625         """ Respond to the user selecting an item from the line size popup menu 
1628         if   id == id_LINESIZE_0
: self
._setLineSize
(0) 
1629         elif id == id_LINESIZE_1
: self
._setLineSize
(1) 
1630         elif id == id_LINESIZE_2
: self
._setLineSize
(2) 
1631         elif id == id_LINESIZE_3
: self
._setLineSize
(3) 
1632         elif id == id_LINESIZE_4
: self
._setLineSize
(4) 
1633         elif id == id_LINESIZE_5
: self
._setLineSize
(5) 
1638         self
.optionIndicator
.setLineSize(self
.lineSize
) 
1641     def _getEventCoordinates(self
, event
): 
1642         """ Return the coordinates associated with the given mouse event. 
1644             The coordinates have to be adjusted to allow for the current scroll 
1647         originX
, originY 
= self
.drawPanel
.GetViewStart() 
1648         unitX
, unitY 
= self
.drawPanel
.GetScrollPixelsPerUnit() 
1649         return wxPoint(event
.GetX() + (originX 
* unitX
), 
1650                        event
.GetY() + (originY 
* unitY
)) 
1653     def _getObjectAndSelectionHandleAt(self
, pt
): 
1654         """ Return the object and selection handle at the given point. 
1656             We draw selection handles (small rectangles) around the currently 
1657             selected object(s).  If the given point is within one of the 
1658             selection handle rectangles, we return the associated object and a 
1659             code indicating which selection handle the point is in.  If the 
1660             point isn't within any selection handle at all, we return the tuple 
1661             (None, handle_NONE). 
1663         for obj 
in self
.selection
: 
1664             handle 
= obj
.getSelectionHandleContainingPoint(pt
.x
, pt
.y
) 
1665             if handle 
!= handle_NONE
: 
1668         return None, handle_NONE
 
1671     def _getObjectAt(self
, pt
): 
1672         """ Return the first object found which is at the given point. 
1674         for obj 
in self
.contents
: 
1675             if obj
.objectContainsPoint(pt
.x
, pt
.y
): 
1680     def _drawObjectOutline(self
, offsetX
, offsetY
): 
1681         """ Draw an outline of the currently selected object. 
1683             The selected object's outline is drawn at the object's position 
1684             plus the given offset. 
1686             Note that the outline is drawn by *inverting* the window's 
1687             contents, so calling _drawObjectOutline twice in succession will 
1688             restore the window's contents back to what they were previously. 
1690         if len(self
.selection
) != 1: return 
1692         position 
= self
.selection
[0].getPosition() 
1693         size     
= self
.selection
[0].getSize() 
1695         dc 
= wxClientDC(self
.drawPanel
) 
1696         self
.drawPanel
.PrepareDC(dc
) 
1698         dc
.SetPen(wxBLACK_DASHED_PEN
) 
1699         dc
.SetBrush(wxTRANSPARENT_BRUSH
) 
1700         dc
.SetLogicalFunction(wxINVERT
) 
1702         dc
.DrawRectangle(position
.x 
+ offsetX
, position
.y 
+ offsetY
, 
1703                          size
.width
, size
.height
) 
1708     def _drawVisualFeedback(self
, startPt
, endPt
, type, dashedLine
): 
1709         """ Draw visual feedback for a drawing operation. 
1711             The visual feedback consists of a line, ellipse, or rectangle based 
1712             around the two given points.  'type' should be one of the following 
1713             predefined feedback type constants: 
1715                 feedback_RECT     ->  draw rectangular feedback. 
1716                 feedback_LINE     ->  draw line feedback. 
1717                 feedback_ELLIPSE  ->  draw elliptical feedback. 
1719             if 'dashedLine' is True, the feedback is drawn as a dashed rather 
1722             Note that the feedback is drawn by *inverting* the window's 
1723             contents, so calling _drawVisualFeedback twice in succession will 
1724             restore the window's contents back to what they were previously. 
1726         dc 
= wxClientDC(self
.drawPanel
) 
1727         self
.drawPanel
.PrepareDC(dc
) 
1730             dc
.SetPen(wxBLACK_DASHED_PEN
) 
1732             dc
.SetPen(wxBLACK_PEN
) 
1733         dc
.SetBrush(wxTRANSPARENT_BRUSH
) 
1734         dc
.SetLogicalFunction(wxINVERT
) 
1736         if type == feedback_RECT
: 
1737             dc
.DrawRectangle(startPt
.x
, startPt
.y
, 
1738                              endPt
.x 
- startPt
.x
, 
1739                              endPt
.y 
- startPt
.y
) 
1740         elif type == feedback_LINE
: 
1741             dc
.DrawLine(startPt
.x
, startPt
.y
, endPt
.x
, endPt
.y
) 
1742         elif type == feedback_ELLIPSE
: 
1743             dc
.DrawEllipse(startPt
.x
, startPt
.y
, 
1744                            endPt
.x 
- startPt
.x
, 
1745                            endPt
.y 
- startPt
.y
) 
1749 #---------------------------------------------------------------------------- 
1751 class DrawingObject
: 
1752     """ An object within the drawing panel. 
1754         A pySketch document consists of a front-to-back ordered list of 
1755         DrawingObjects.  Each DrawingObject has the following properties: 
1757             'type'          What type of object this is (text, line, etc). 
1758             'position'      The position of the object within the document. 
1759             'size'          The size of the object within the document. 
1760             'penColour'     The colour to use for drawing the object's outline. 
1761             'fillColour'    Colour to use for drawing object's interior. 
1762             'lineSize'      Line width (in pixels) to use for object's outline. 
1763             'startPt'       The point, relative to the object's position, where 
1764                             an obj_LINE object's line should start. 
1765             'endPt'         The point, relative to the object's position, where 
1766                             an obj_LINE object's line should end. 
1767             'text'          The object's text (obj_TEXT objects only). 
1768             'textFont'      The text object's font name. 
1769             'textSize'      The text object's point size. 
1770             'textBoldface'  If True, this text object will be drawn in 
1772             'textItalic'    If True, this text object will be drawn in italic. 
1773             'textUnderline' If True, this text object will be drawn underlined. 
1776     # ================== 
1777     # == Constructors == 
1778     # ================== 
1780     def __init__(self
, type, position
=wxPoint(0, 0), size
=wxSize(0, 0), 
1781                  penColour
=wxBLACK
, fillColour
=wxWHITE
, lineSize
=1, 
1782                  text
=None, startPt
=wxPoint(0, 0), endPt
=wxPoint(0,0)): 
1783         """ Standard constructor. 
1785             'type' is the type of object being created.  This should be one of 
1786             the following constants: 
1793             The remaining parameters let you set various options for the newly 
1794             created DrawingObject. 
1797         self
.position          
= position
 
1799         self
.penColour         
= penColour
 
1800         self
.fillColour        
= fillColour
 
1801         self
.lineSize          
= lineSize
 
1802         self
.startPt           
= startPt
 
1805         self
.textFont          
= wxSystemSettings_GetSystemFont( 
1806                                     wxSYS_DEFAULT_GUI_FONT
).GetFaceName() 
1808         self
.textBoldface      
= False 
1809         self
.textItalic        
= False 
1810         self
.textUnderline     
= False 
1812     # ============================= 
1813     # == Object Property Methods == 
1814     # ============================= 
1817         """ Return a copy of the object's internal data. 
1819             This is used to save this DrawingObject to disk. 
1821         return [self
.type, self
.position
.x
, self
.position
.y
, 
1822                 self
.size
.width
, self
.size
.height
, 
1823                 self
.penColour
.Red(), 
1824                 self
.penColour
.Green(), 
1825                 self
.penColour
.Blue(), 
1826                 self
.fillColour
.Red(), 
1827                 self
.fillColour
.Green(), 
1828                 self
.fillColour
.Blue(), 
1830                 self
.startPt
.x
, self
.startPt
.y
, 
1831                 self
.endPt
.x
, self
.endPt
.y
, 
1840     def setData(self
, data
): 
1841         """ Set the object's internal data. 
1843             'data' is a copy of the object's saved data, as returned by 
1844             getData() above.  This is used to restore a previously saved 
1847         #data = copy.deepcopy(data) # Needed? 
1850         self
.position          
= wxPoint(data
[1], data
[2]) 
1851         self
.size              
= wxSize(data
[3], data
[4]) 
1852         self
.penColour         
= wxColour(red
=data
[5], 
1855         self
.fillColour        
= wxColour(red
=data
[8], 
1858         self
.lineSize          
= data
[11] 
1859         self
.startPt           
= wxPoint(data
[12], data
[13]) 
1860         self
.endPt             
= wxPoint(data
[14], data
[15]) 
1861         self
.text              
= data
[16] 
1862         self
.textFont          
= data
[17] 
1863         self
.textSize          
= data
[18] 
1864         self
.textBoldface      
= data
[19] 
1865         self
.textItalic        
= data
[20] 
1866         self
.textUnderline     
= data
[21] 
1870         """ Return this DrawingObject's type. 
1875     def setPosition(self
, position
): 
1876         """ Set the origin (top-left corner) for this DrawingObject. 
1878         self
.position 
= position
 
1881     def getPosition(self
): 
1882         """ Return this DrawingObject's position. 
1884         return self
.position
 
1887     def setSize(self
, size
): 
1888         """ Set the size for this DrawingObject. 
1894         """ Return this DrawingObject's size. 
1899     def setPenColour(self
, colour
): 
1900         """ Set the pen colour used for this DrawingObject. 
1902         self
.penColour 
= colour
 
1905     def getPenColour(self
): 
1906         """ Return this DrawingObject's pen colour. 
1908         return self
.penColour
 
1911     def setFillColour(self
, colour
): 
1912         """ Set the fill colour used for this DrawingObject. 
1914         self
.fillColour 
= colour
 
1917     def getFillColour(self
): 
1918         """ Return this DrawingObject's fill colour. 
1920         return self
.fillColour
 
1923     def setLineSize(self
, lineSize
): 
1924         """ Set the linesize used for this DrawingObject. 
1926         self
.lineSize 
= lineSize
 
1929     def getLineSize(self
): 
1930         """ Return this DrawingObject's line size. 
1932         return self
.lineSize
 
1935     def setStartPt(self
, startPt
): 
1936         """ Set the starting point for this line DrawingObject. 
1938         self
.startPt 
= startPt
 
1941     def getStartPt(self
): 
1942         """ Return the starting point for this line DrawingObject. 
1947     def setEndPt(self
, endPt
): 
1948         """ Set the ending point for this line DrawingObject. 
1954         """ Return the ending point for this line DrawingObject. 
1959     def setText(self
, text
): 
1960         """ Set the text for this DrawingObject. 
1966         """ Return this DrawingObject's text. 
1971     def setTextFont(self
, font
): 
1972         """ Set the typeface for this text DrawingObject. 
1974         self
.textFont 
= font
 
1977     def getTextFont(self
): 
1978         """ Return this text DrawingObject's typeface. 
1980         return self
.textFont
 
1983     def setTextSize(self
, size
): 
1984         """ Set the point size for this text DrawingObject. 
1986         self
.textSize 
= size
 
1989     def getTextSize(self
): 
1990         """ Return this text DrawingObject's text size. 
1992         return self
.textSize
 
1995     def setTextBoldface(self
, boldface
): 
1996         """ Set the boldface flag for this text DrawingObject. 
1998         self
.textBoldface 
= boldface
 
2001     def getTextBoldface(self
): 
2002         """ Return this text DrawingObject's boldface flag. 
2004         return self
.textBoldface
 
2007     def setTextItalic(self
, italic
): 
2008         """ Set the italic flag for this text DrawingObject. 
2010         self
.textItalic 
= italic
 
2013     def getTextItalic(self
): 
2014         """ Return this text DrawingObject's italic flag. 
2016         return self
.textItalic
 
2019     def setTextUnderline(self
, underline
): 
2020         """ Set the underling flag for this text DrawingObject. 
2022         self
.textUnderline 
= underline
 
2025     def getTextUnderline(self
): 
2026         """ Return this text DrawingObject's underline flag. 
2028         return self
.textUnderline
 
2030     # ============================ 
2031     # == Object Drawing Methods == 
2032     # ============================ 
2034     def draw(self
, dc
, selected
): 
2035         """ Draw this DrawingObject into our window. 
2037             'dc' is the device context to use for drawing.  If 'selected' is 
2038             True, the object is currently selected and should be drawn as such. 
2040         if self
.type != obj_TEXT
: 
2041             if self
.lineSize 
== 0: 
2042                 dc
.SetPen(wxPen(self
.penColour
, self
.lineSize
, wxTRANSPARENT
)) 
2044                 dc
.SetPen(wxPen(self
.penColour
, self
.lineSize
, wxSOLID
)) 
2045             dc
.SetBrush(wxBrush(self
.fillColour
, wxSOLID
)) 
2047             dc
.SetTextForeground(self
.penColour
) 
2048             dc
.SetTextBackground(self
.fillColour
) 
2050         self
._privateDraw
(dc
, self
.position
, selected
) 
2052     # ======================= 
2053     # == Selection Methods == 
2054     # ======================= 
2056     def objectContainsPoint(self
, x
, y
): 
2057         """ Returns True iff this object contains the given point. 
2059             This is used to determine if the user clicked on the object. 
2061         # Firstly, ignore any points outside of the object's bounds. 
2063         if x 
< self
.position
.x
: return False 
2064         if x 
> self
.position
.x 
+ self
.size
.x
: return False 
2065         if y 
< self
.position
.y
: return False 
2066         if y 
> self
.position
.y 
+ self
.size
.y
: return False 
2068         if self
.type in [obj_RECT
, obj_TEXT
]: 
2069             # Rectangles and text are easy -- they're always selected if the 
2070             # point is within their bounds. 
2073         # Now things get tricky.  There's no straightforward way of knowing 
2074         # whether the point is within the object's bounds...to get around this, 
2075         # we draw the object into a memory-based bitmap and see if the given 
2076         # point was drawn.  This could no doubt be done more efficiently by 
2077         # some tricky maths, but this approach works and is simple enough. 
2079         bitmap 
= wxEmptyBitmap(self
.size
.x 
+ 10, self
.size
.y 
+ 10) 
2081         dc
.SelectObject(bitmap
) 
2083         dc
.SetBackground(wxWHITE_BRUSH
) 
2085         dc
.SetPen(wxPen(wxBLACK
, self
.lineSize 
+ 5, wxSOLID
)) 
2086         dc
.SetBrush(wxBLACK_BRUSH
) 
2087         self
._privateDraw
(dc
, wxPoint(5, 5), True) 
2089         pixel 
= dc
.GetPixel(x 
- self
.position
.x 
+ 5, y 
- self
.position
.y 
+ 5) 
2090         if (pixel
.Red() == 0) and (pixel
.Green() == 0) and (pixel
.Blue() == 0): 
2096     def getSelectionHandleContainingPoint(self
, x
, y
): 
2097         """ Return the selection handle containing the given point, if any. 
2099             We return one of the predefined selection handle ID codes. 
2101         if self
.type == obj_LINE
: 
2102             # We have selection handles at the start and end points. 
2103             if self
._pointInSelRect
(x
, y
, self
.position
.x 
+ self
.startPt
.x
, 
2104                                           self
.position
.y 
+ self
.startPt
.y
): 
2105                 return handle_START_POINT
 
2106             elif self
._pointInSelRect
(x
, y
, self
.position
.x 
+ self
.endPt
.x
, 
2107                                             self
.position
.y 
+ self
.endPt
.y
): 
2108                 return handle_END_POINT
 
2112             # We have selection handles at all four corners. 
2113             if self
._pointInSelRect
(x
, y
, self
.position
.x
, self
.position
.y
): 
2114                 return handle_TOP_LEFT
 
2115             elif self
._pointInSelRect
(x
, y
, self
.position
.x 
+ self
.size
.width
, 
2117                 return handle_TOP_RIGHT
 
2118             elif self
._pointInSelRect
(x
, y
, self
.position
.x
, 
2119                                             self
.position
.y 
+ self
.size
.height
): 
2120                 return handle_BOTTOM_LEFT
 
2121             elif self
._pointInSelRect
(x
, y
, self
.position
.x 
+ self
.size
.width
, 
2122                                             self
.position
.y 
+ self
.size
.height
): 
2123                 return handle_BOTTOM_RIGHT
 
2128     def objectWithinRect(self
, x
, y
, width
, height
): 
2129         """ Return True iff this object falls completely within the given rect. 
2131         if x          
> self
.position
.x
:                    return False 
2132         if x 
+ width  
< self
.position
.x 
+ self
.size
.width
:  return False 
2133         if y          
> self
.position
.y
:                    return False 
2134         if y 
+ height 
< self
.position
.y 
+ self
.size
.height
: return False 
2137     # ===================== 
2138     # == Utility Methods == 
2139     # ===================== 
2141     def fitToText(self
): 
2142         """ Resize a text DrawingObject so that it fits it's text exactly. 
2144         if self
.type != obj_TEXT
: return 
2146         if self
.textBoldface
: weight 
= wxBOLD
 
2147         else:                 weight 
= wxNORMAL
 
2148         if self
.textItalic
: style 
= wxITALIC
 
2149         else:               style 
= wxNORMAL
 
2150         font 
= wxFont(self
.textSize
, wxDEFAULT
, style
, weight
, 
2151                       self
.textUnderline
, self
.textFont
) 
2153         dummyWindow 
= wxFrame(None, -1, "") 
2154         dummyWindow
.SetFont(font
) 
2155         width
, height 
= dummyWindow
.GetTextExtent(self
.text
) 
2156         dummyWindow
.Destroy() 
2158         self
.size 
= wxSize(width
, height
) 
2160     # ===================== 
2161     # == Private Methods == 
2162     # ===================== 
2164     def _privateDraw(self
, dc
, position
, selected
): 
2165         """ Private routine to draw this DrawingObject. 
2167             'dc' is the device context to use for drawing, while 'position' is 
2168             the position in which to draw the object.  If 'selected' is True, 
2169             the object is drawn with selection handles.  This private drawing 
2170             routine assumes that the pen and brush have already been set by the 
2173         if self
.type == obj_LINE
: 
2174             dc
.DrawLine(position
.x 
+ self
.startPt
.x
, 
2175                         position
.y 
+ self
.startPt
.y
, 
2176                         position
.x 
+ self
.endPt
.x
, 
2177                         position
.y 
+ self
.endPt
.y
) 
2178         elif self
.type == obj_RECT
: 
2179             dc
.DrawRectangle(position
.x
, position
.y
, 
2180                              self
.size
.width
, self
.size
.height
) 
2181         elif self
.type == obj_ELLIPSE
: 
2182             dc
.DrawEllipse(position
.x
, position
.y
, 
2183                            self
.size
.width
, self
.size
.height
) 
2184         elif self
.type == obj_TEXT
: 
2185             if self
.textBoldface
: weight 
= wxBOLD
 
2186             else:                 weight 
= wxNORMAL
 
2187             if self
.textItalic
: style 
= wxITALIC
 
2188             else:               style 
= wxNORMAL
 
2189             font 
= wxFont(self
.textSize
, wxDEFAULT
, style
, weight
, 
2190                           self
.textUnderline
, self
.textFont
) 
2192             dc
.DrawText(self
.text
, position
.x
, position
.y
) 
2195             dc
.SetPen(wxTRANSPARENT_PEN
) 
2196             dc
.SetBrush(wxBLACK_BRUSH
) 
2198             if self
.type == obj_LINE
: 
2199                 # Draw selection handles at the start and end points. 
2200                 self
._drawSelHandle
(dc
, position
.x 
+ self
.startPt
.x
, 
2201                                         position
.y 
+ self
.startPt
.y
) 
2202                 self
._drawSelHandle
(dc
, position
.x 
+ self
.endPt
.x
, 
2203                                         position
.y 
+ self
.endPt
.y
) 
2205                 # Draw selection handles at all four corners. 
2206                 self
._drawSelHandle
(dc
, position
.x
, position
.y
) 
2207                 self
._drawSelHandle
(dc
, position
.x 
+ self
.size
.width
, 
2209                 self
._drawSelHandle
(dc
, position
.x
, 
2210                                         position
.y 
+ self
.size
.height
) 
2211                 self
._drawSelHandle
(dc
, position
.x 
+ self
.size
.width
, 
2212                                         position
.y 
+ self
.size
.height
) 
2215     def _drawSelHandle(self
, dc
, x
, y
): 
2216         """ Draw a selection handle around this DrawingObject. 
2218             'dc' is the device context to draw the selection handle within, 
2219             while 'x' and 'y' are the coordinates to use for the centre of the 
2222         dc
.DrawRectangle(x 
- 3, y 
- 3, 6, 6) 
2225     def _pointInSelRect(self
, x
, y
, rX
, rY
): 
2226         """ Return True iff (x, y) is within the selection handle at (rX, ry). 
2228         if   x 
< rX 
- 3: return False 
2229         elif x 
> rX 
+ 3: return False 
2230         elif y 
< rY 
- 3: return False 
2231         elif y 
> rY 
+ 3: return False 
2234 #---------------------------------------------------------------------------- 
2236 class ToolPaletteIcon(wxBitmapButton
): 
2237     """ An icon appearing in the tool palette area of our sketching window. 
2239         Note that this is actually implemented as a wxBitmap rather 
2240         than as a wxIcon.  wxIcon has a very specific meaning, and isn't 
2241         appropriate for this more general use. 
2244     def __init__(self
, parent
, iconID
, iconName
, toolTip
): 
2245         """ Standard constructor. 
2247             'parent'   is the parent window this icon will be part of. 
2248             'iconID'   is the internal ID used for this icon. 
2249             'iconName' is the name used for this icon. 
2250             'toolTip'  is the tool tip text to show for this icon. 
2252             The icon name is used to get the appropriate bitmap for this icon. 
2254         bmp 
= wxBitmap("images/" + iconName 
+ "Icon.bmp", wxBITMAP_TYPE_BMP
) 
2255         wxBitmapButton
.__init
__(self
, parent
, iconID
, bmp
, wxDefaultPosition
, 
2256                                 wxSize(bmp
.GetWidth(), bmp
.GetHeight())) 
2257         self
.SetToolTip(wxToolTip(toolTip
)) 
2259         self
.iconID     
= iconID
 
2260         self
.iconName   
= iconName
 
2261         self
.isSelected 
= False 
2265         """ Select the icon. 
2267             The icon's visual representation is updated appropriately. 
2269         if self
.isSelected
: return # Nothing to do! 
2271         bmp 
= wxBitmap("images/" + self
.iconName 
+ "IconSel.bmp", 
2273         self
.SetBitmapLabel(bmp
) 
2274         self
.isSelected 
= True 
2278         """ Deselect the icon. 
2280             The icon's visual representation is updated appropriately. 
2282         if not self
.isSelected
: return # Nothing to do! 
2284         bmp 
= wxBitmap("images/" + self
.iconName 
+ "Icon.bmp", 
2286         self
.SetBitmapLabel(bmp
) 
2287         self
.isSelected 
= False 
2289 #---------------------------------------------------------------------------- 
2291 class ToolOptionIndicator(wxWindow
): 
2292     """ A visual indicator which shows the current tool options. 
2294     def __init__(self
, parent
): 
2295         """ Standard constructor. 
2297         wxWindow
.__init
__(self
, parent
, -1, wxDefaultPosition
, wxSize(52, 32)) 
2299         self
.penColour  
= wxBLACK
 
2300         self
.fillColour 
= wxWHITE
 
2303         EVT_PAINT(self
, self
.OnPaint
) 
2306     def setPenColour(self
, penColour
): 
2307         """ Set the indicator's current pen colour. 
2309         self
.penColour 
= penColour
 
2313     def setFillColour(self
, fillColour
): 
2314         """ Set the indicator's current fill colour. 
2316         self
.fillColour 
= fillColour
 
2320     def setLineSize(self
, lineSize
): 
2321         """ Set the indicator's current pen colour. 
2323         self
.lineSize 
= lineSize
 
2327     def OnPaint(self
, event
): 
2328         """ Paint our tool option indicator. 
2330         dc 
= wxPaintDC(self
) 
2333         if self
.lineSize 
== 0: 
2334             dc
.SetPen(wxPen(self
.penColour
, self
.lineSize
, wxTRANSPARENT
)) 
2336             dc
.SetPen(wxPen(self
.penColour
, self
.lineSize
, wxSOLID
)) 
2337         dc
.SetBrush(wxBrush(self
.fillColour
, wxSOLID
)) 
2339         dc
.DrawRectangle(5, 5, self
.GetSize().width 
- 10, 
2340                                self
.GetSize().height 
- 10) 
2344 #---------------------------------------------------------------------------- 
2346 class EditTextObjectDialog(wxDialog
): 
2347     """ Dialog box used to edit the properties of a text object. 
2349         The user can edit the object's text, font, size, and text style. 
2352     def __init__(self
, parent
, title
): 
2353         """ Standard constructor. 
2355         wxDialog
.__init
__(self
, parent
, -1, title
) 
2357         self
.textCtrl 
= wxTextCtrl(self
, 1001, "", style
=wxTE_PROCESS_ENTER
, 
2358                                    validator
=TextObjectValidator()) 
2359         extent 
= self
.textCtrl
.GetFullTextExtent("Hy") 
2360         lineHeight 
= extent
[1] + extent
[3] 
2361         self
.textCtrl
.SetSize(wxSize(-1, lineHeight 
* 4)) 
2363         EVT_TEXT_ENTER(self
, 1001, self
._doEnter
) 
2365         fonts 
= wxFontEnumerator() 
2366         fonts
.EnumerateFacenames() 
2367         self
.fontList 
= fonts
.GetFacenames() 
2368         self
.fontList
.sort() 
2370         fontLabel 
= wxStaticText(self
, -1, "Font:") 
2371         self
._setFontOptions
(fontLabel
, weight
=wxBOLD
) 
2373         self
.fontCombo 
= wxComboBox(self
, -1, "", wxDefaultPosition
, 
2374                                     wxDefaultSize
, self
.fontList
, 
2375                                     style 
= wxCB_READONLY
) 
2376         self
.fontCombo
.SetSelection(0) # Default to first available font. 
2378         self
.sizeList 
= ["8", "9", "10", "12", "14", "16", 
2379                          "18", "20", "24", "32", "48", "72"] 
2381         sizeLabel 
= wxStaticText(self
, -1, "Size:") 
2382         self
._setFontOptions
(sizeLabel
, weight
=wxBOLD
) 
2384         self
.sizeCombo 
= wxComboBox(self
, -1, "", wxDefaultPosition
, 
2385                                     wxDefaultSize
, self
.sizeList
, 
2386                                     style
=wxCB_READONLY
) 
2387         self
.sizeCombo
.SetSelection(3) # Default to 12 point text. 
2389         gap 
= wxLEFT | wxTOP | wxRIGHT
 
2391         comboSizer 
= wxBoxSizer(wxHORIZONTAL
) 
2392         comboSizer
.Add(fontLabel
,      0, gap | wxALIGN_CENTRE_VERTICAL
, 5) 
2393         comboSizer
.Add(self
.fontCombo
, 0, gap
, 5) 
2394         comboSizer
.Add((5, 5)) # Spacer. 
2395         comboSizer
.Add(sizeLabel
,      0, gap | wxALIGN_CENTRE_VERTICAL
, 5) 
2396         comboSizer
.Add(self
.sizeCombo
, 0, gap
, 5) 
2398         self
.boldCheckbox      
= wxCheckBox(self
, -1, "Bold") 
2399         self
.italicCheckbox    
= wxCheckBox(self
, -1, "Italic") 
2400         self
.underlineCheckbox 
= wxCheckBox(self
, -1, "Underline") 
2402         self
._setFontOptions
(self
.boldCheckbox
,      weight
=wxBOLD
) 
2403         self
._setFontOptions
(self
.italicCheckbox
,    style
=wxITALIC
) 
2404         self
._setFontOptions
(self
.underlineCheckbox
, underline
=True) 
2406         styleSizer 
= wxBoxSizer(wxHORIZONTAL
) 
2407         styleSizer
.Add(self
.boldCheckbox
,      0, gap
, 5) 
2408         styleSizer
.Add(self
.italicCheckbox
,    0, gap
, 5) 
2409         styleSizer
.Add(self
.underlineCheckbox
, 0, gap
, 5) 
2411         self
.okButton     
= wxButton(self
, wxID_OK
,     "OK") 
2412         self
.cancelButton 
= wxButton(self
, wxID_CANCEL
, "Cancel") 
2414         btnSizer 
= wxBoxSizer(wxHORIZONTAL
) 
2415         btnSizer
.Add(self
.okButton
,     0, gap
, 5) 
2416         btnSizer
.Add(self
.cancelButton
, 0, gap
, 5) 
2418         sizer 
= wxBoxSizer(wxVERTICAL
) 
2419         sizer
.Add(self
.textCtrl
, 1, gap | wxEXPAND
,       5) 
2420         sizer
.Add((10, 10)) # Spacer. 
2421         sizer
.Add(comboSizer
,    0, gap | wxALIGN_CENTRE
, 5) 
2422         sizer
.Add(styleSizer
,    0, gap | wxALIGN_CENTRE
, 5) 
2423         sizer
.Add((10, 10)) # Spacer. 
2424         sizer
.Add(btnSizer
,      0, gap | wxALIGN_CENTRE
, 5) 
2426         self
.SetAutoLayout(True) 
2427         self
.SetSizer(sizer
) 
2430         self
.textCtrl
.SetFocus() 
2433     def objectToDialog(self
, obj
): 
2434         """ Copy the properties of the given text object into the dialog box. 
2436         self
.textCtrl
.SetValue(obj
.getText()) 
2437         self
.textCtrl
.SetSelection(0, len(obj
.getText())) 
2439         for i 
in range(len(self
.fontList
)): 
2440             if self
.fontList
[i
] == obj
.getTextFont(): 
2441                 self
.fontCombo
.SetSelection(i
) 
2444         for i 
in range(len(self
.sizeList
)): 
2445             if self
.sizeList
[i
] == str(obj
.getTextSize()): 
2446                 self
.sizeCombo
.SetSelection(i
) 
2449         self
.boldCheckbox
.SetValue(obj
.getTextBoldface()) 
2450         self
.italicCheckbox
.SetValue(obj
.getTextItalic()) 
2451         self
.underlineCheckbox
.SetValue(obj
.getTextUnderline()) 
2454     def dialogToObject(self
, obj
): 
2455         """ Copy the properties from the dialog box into the given text object. 
2457         obj
.setText(self
.textCtrl
.GetValue()) 
2458         obj
.setTextFont(self
.fontCombo
.GetValue()) 
2459         obj
.setTextSize(int(self
.sizeCombo
.GetValue())) 
2460         obj
.setTextBoldface(self
.boldCheckbox
.GetValue()) 
2461         obj
.setTextItalic(self
.italicCheckbox
.GetValue()) 
2462         obj
.setTextUnderline(self
.underlineCheckbox
.GetValue()) 
2465     # ====================== 
2466     # == Private Routines == 
2467     # ====================== 
2469     def _setFontOptions(self
, ctrl
, family
=None, pointSize
=-1, 
2470                                     style
=wxNORMAL
, weight
=wxNORMAL
, 
2472         """ Change the font settings for the given control. 
2474             The meaning of the 'family', 'pointSize', 'style', 'weight' and 
2475             'underline' parameters are the same as for the wxFont constructor. 
2476             If the family and/or pointSize isn't specified, the current default 
2479         if family 
== None: family 
= ctrl
.GetFont().GetFamily() 
2480         if pointSize 
== -1: pointSize 
= ctrl
.GetFont().GetPointSize() 
2482         ctrl
.SetFont(wxFont(pointSize
, family
, style
, weight
, underline
)) 
2483         ctrl
.SetSize(ctrl
.GetBestSize()) # Adjust size to reflect font change. 
2486     def _doEnter(self
, event
): 
2487         """ Respond to the user hitting the ENTER key. 
2489             We simulate clicking on the "OK" button. 
2491         if self
.Validate(): self
.Show(False) 
2493 #---------------------------------------------------------------------------- 
2495 class TextObjectValidator(wxPyValidator
): 
2496     """ This validator is used to ensure that the user has entered something 
2497         into the text object editor dialog's text field. 
2500         """ Standard constructor. 
2502         wxPyValidator
.__init
__(self
) 
2506         """ Standard cloner. 
2508             Note that every validator must implement the Clone() method. 
2510         return TextObjectValidator() 
2513     def Validate(self
, win
): 
2514         """ Validate the contents of the given text control. 
2516         textCtrl 
= self
.GetWindow() 
2517         text 
= textCtrl
.GetValue() 
2520             wxMessageBox("A text object must contain some text!", "Error") 
2526     def TransferToWindow(self
): 
2527         """ Transfer data from validator to window. 
2529             The default implementation returns False, indicating that an error 
2530             occurred.  We simply return True, as we don't do any data transfer. 
2532         return True # Prevent wxDialog from complaining. 
2535     def TransferFromWindow(self
): 
2536         """ Transfer data from window to validator. 
2538             The default implementation returns False, indicating that an error 
2539             occurred.  We simply return True, as we don't do any data transfer. 
2541         return True # Prevent wxDialog from complaining. 
2543 #---------------------------------------------------------------------------- 
2545 class ExceptionHandler
: 
2546     """ A simple error-handling class to write exceptions to a text file. 
2548         Under MS Windows, the standard DOS console window doesn't scroll and 
2549         closes as soon as the application exits, making it hard to find and 
2550         view Python exceptions.  This utility class allows you to handle Python 
2551         exceptions in a more friendly manner. 
2555         """ Standard constructor. 
2558         if os
.path
.exists("errors.txt"): 
2559             os
.remove("errors.txt") # Delete previous error log, if any. 
2563         """ Write the given error message to a text file. 
2565             Note that if the error message doesn't end in a carriage return, we 
2566             have to buffer up the inputs until a carriage return is received. 
2568         if (s
[-1] != "\n") and (s
[-1] != "\r"): 
2569             self
._buff 
= self
._buff 
+ s
 
2576             if s
[:9] == "Traceback": 
2577                 # Tell the user than an exception occurred. 
2578                 wxMessageBox("An internal error has occurred.\nPlease " + \
 
2579                              "refer to the 'errors.txt' file for details.", 
2580                              "Error", wxOK | wxCENTRE | wxICON_EXCLAMATION
) 
2582             f 
= open("errors.txt", "a") 
2586             pass # Don't recursively crash on errors. 
2588 #---------------------------------------------------------------------------- 
2590 class SketchApp(wxApp
): 
2591     """ The main pySketch application object. 
2594         """ Initialise the application. 
2596         wxInitAllImageHandlers() 
2601         if len(sys
.argv
) == 1: 
2602             # No file name was specified on the command line -> start with a 
2604             frame 
= DrawingFrame(None, -1, "Untitled") 
2607             _docList
.append(frame
) 
2609             # Load the file(s) specified on the command line. 
2610             for arg 
in sys
.argv
[1:]: 
2611                 fileName 
= os
.path
.join(os
.getcwd(), arg
) 
2612                 if os
.path
.isfile(fileName
): 
2613                     frame 
= DrawingFrame(None, -1, 
2614                                          os
.path
.basename(fileName
), 
2617                     _docList
.append(frame
) 
2621 #---------------------------------------------------------------------------- 
2624     """ Start up the pySketch application. 
2628     # Redirect python exceptions to a log file. 
2630     sys
.stderr 
= ExceptionHandler() 
2632     # Create and start the pySketch application. 
2638 if __name__ 
== "__main__":