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. 
  45 import cPickle
, os
.path
 
  46 from wxPython
.wx 
import * 
  48 import traceback
, types
 
  50 #---------------------------------------------------------------------------- 
  52 #---------------------------------------------------------------------------- 
  56 menu_UNDO          
= 10001 # Edit menu items. 
  57 menu_SELECT_ALL    
= 10002 
  58 menu_DUPLICATE     
= 10003 
  59 menu_EDIT_TEXT     
= 10004 
  62 menu_SELECT        
= 10101 # Tools menu items. 
  68 menu_MOVE_FORWARD  
= 10201 # Object menu items. 
  69 menu_MOVE_TO_FRONT 
= 10202 
  70 menu_MOVE_BACKWARD 
= 10203 
  71 menu_MOVE_TO_BACK  
= 10204 
  73 menu_ABOUT         
= 10205 # Help menu items. 
  83 # Our tool option IDs: 
  96 # DrawObject type IDs: 
 103 # Selection handle IDs: 
 108 handle_BOTTOM_LEFT  
= 4 
 109 handle_BOTTOM_RIGHT 
= 5 
 110 handle_START_POINT  
= 6 
 113 # Dragging operations: 
 120 # Visual Feedback types: 
 126 # Mouse-event action parameter types: 
 131 # Size of the drawing page, in pixels. 
 136 #---------------------------------------------------------------------------- 
 138 class DrawingFrame(wxFrame
): 
 139     """ A frame showing the contents of a single document. """ 
 141     # ========================================== 
 142     # == Initialisation and Window Management == 
 143     # ========================================== 
 145     def __init__(self
, parent
, id, title
, fileName
=None): 
 146         """ Standard constructor. 
 148             'parent', 'id' and 'title' are all passed to the standard wxFrame 
 149             constructor.  'fileName' is the name and path of a saved file to 
 150             load into this frame, if any. 
 152         wxFrame
.__init
__(self
, parent
, id, title
, 
 153                          style 
= wxDEFAULT_FRAME_STYLE | wxWANTS_CHARS |
 
 154                                  wxNO_FULL_REPAINT_ON_RESIZE
) 
 156         # Setup our menu bar. 
 158         menuBar 
= wxMenuBar() 
 160         self
.fileMenu 
= wxMenu() 
 161         self
.fileMenu
.Append(wxID_NEW
,    "New\tCTRL-N") 
 162         self
.fileMenu
.Append(wxID_OPEN
,   "Open...\tCTRL-O") 
 163         self
.fileMenu
.Append(wxID_CLOSE
,  "Close\tCTRL-W") 
 164         self
.fileMenu
.AppendSeparator() 
 165         self
.fileMenu
.Append(wxID_SAVE
,   "Save\tCTRL-S") 
 166         self
.fileMenu
.Append(wxID_SAVEAS
, "Save As...") 
 167         self
.fileMenu
.Append(wxID_REVERT
, "Revert...") 
 168         self
.fileMenu
.AppendSeparator() 
 169         self
.fileMenu
.Append(wxID_EXIT
,   "Quit\tCTRL-Q") 
 171         menuBar
.Append(self
.fileMenu
, "File") 
 173         self
.editMenu 
= wxMenu() 
 174         self
.editMenu
.Append(menu_UNDO
,          "Undo\tCTRL-Z") 
 175         self
.editMenu
.AppendSeparator() 
 176         self
.editMenu
.Append(menu_SELECT_ALL
,    "Select All\tCTRL-A") 
 177         self
.editMenu
.AppendSeparator() 
 178         self
.editMenu
.Append(menu_DUPLICATE
,     "Duplicate\tCTRL-D") 
 179         self
.editMenu
.Append(menu_EDIT_TEXT
,     "Edit...\tCTRL-E") 
 180         self
.editMenu
.Append(menu_DELETE
,        "Delete\tDEL") 
 182         menuBar
.Append(self
.editMenu
, "Edit") 
 184         self
.toolsMenu 
= wxMenu() 
 185         self
.toolsMenu
.Append(menu_SELECT
,  "Selection", kind
=wxITEM_CHECK
) 
 186         self
.toolsMenu
.Append(menu_LINE
,    "Line",      kind
=wxITEM_CHECK
) 
 187         self
.toolsMenu
.Append(menu_RECT
,    "Rectangle", kind
=wxITEM_CHECK
) 
 188         self
.toolsMenu
.Append(menu_ELLIPSE
, "Ellipse",   kind
=wxITEM_CHECK
) 
 189         self
.toolsMenu
.Append(menu_TEXT
,    "Text",      kind
=wxITEM_CHECK
) 
 191         menuBar
.Append(self
.toolsMenu
, "Tools") 
 193         self
.objectMenu 
= wxMenu() 
 194         self
.objectMenu
.Append(menu_MOVE_FORWARD
,  "Move Forward") 
 195         self
.objectMenu
.Append(menu_MOVE_TO_FRONT
, "Move to Front\tCTRL-F") 
 196         self
.objectMenu
.Append(menu_MOVE_BACKWARD
, "Move Backward") 
 197         self
.objectMenu
.Append(menu_MOVE_TO_BACK
,  "Move to Back\tCTRL-B") 
 199         menuBar
.Append(self
.objectMenu
, "Object") 
 201         self
.helpMenu 
= wxMenu() 
 202         self
.helpMenu
.Append(menu_ABOUT
, "About pySketch...") 
 204         menuBar
.Append(self
.helpMenu
, "Help") 
 206         self
.SetMenuBar(menuBar
) 
 208         # Create our toolbar. 
 210         self
.toolbar 
= self
.CreateToolBar(wxTB_HORIZONTAL |
 
 211                                           wxNO_BORDER | wxTB_FLAT
) 
 213         self
.toolbar
.AddSimpleTool(wxID_NEW
, 
 214                                    wxBitmap("images/new.bmp", 
 217         self
.toolbar
.AddSimpleTool(wxID_OPEN
, 
 218                                    wxBitmap("images/open.bmp", 
 221         self
.toolbar
.AddSimpleTool(wxID_SAVE
, 
 222                                    wxBitmap("images/save.bmp", 
 225         self
.toolbar
.AddSeparator() 
 226         self
.toolbar
.AddSimpleTool(menu_UNDO
, 
 227                                    wxBitmap("images/undo.bmp", 
 230         self
.toolbar
.AddSeparator() 
 231         self
.toolbar
.AddSimpleTool(menu_DUPLICATE
, 
 232                                    wxBitmap("images/duplicate.bmp", 
 235         self
.toolbar
.AddSeparator() 
 236         self
.toolbar
.AddSimpleTool(menu_MOVE_FORWARD
, 
 237                                    wxBitmap("images/moveForward.bmp", 
 240         self
.toolbar
.AddSimpleTool(menu_MOVE_BACKWARD
, 
 241                                    wxBitmap("images/moveBack.bmp", 
 245         self
.toolbar
.Realize() 
 247         # Associate each menu/toolbar item with the method that handles that 
 250         EVT_MENU(self
, wxID_NEW
,    self
.doNew
) 
 251         EVT_MENU(self
, wxID_OPEN
,   self
.doOpen
) 
 252         EVT_MENU(self
, wxID_CLOSE
,  self
.doClose
) 
 253         EVT_MENU(self
, wxID_SAVE
,   self
.doSave
) 
 254         EVT_MENU(self
, wxID_SAVEAS
, self
.doSaveAs
) 
 255         EVT_MENU(self
, wxID_REVERT
, self
.doRevert
) 
 256         EVT_MENU(self
, wxID_EXIT
,   self
.doExit
) 
 258         EVT_MENU(self
, menu_UNDO
,          self
.doUndo
) 
 259         EVT_MENU(self
, menu_SELECT_ALL
,    self
.doSelectAll
) 
 260         EVT_MENU(self
, menu_DUPLICATE
,     self
.doDuplicate
) 
 261         EVT_MENU(self
, menu_EDIT_TEXT
,     self
.doEditText
) 
 262         EVT_MENU(self
, menu_DELETE
,        self
.doDelete
) 
 264         EVT_MENU(self
, menu_SELECT
,  self
.doChooseSelectTool
) 
 265         EVT_MENU(self
, menu_LINE
,    self
.doChooseLineTool
) 
 266         EVT_MENU(self
, menu_RECT
,    self
.doChooseRectTool
) 
 267         EVT_MENU(self
, menu_ELLIPSE
, self
.doChooseEllipseTool
) 
 268         EVT_MENU(self
, menu_TEXT
,    self
.doChooseTextTool
) 
 270         EVT_MENU(self
, menu_MOVE_FORWARD
,  self
.doMoveForward
) 
 271         EVT_MENU(self
, menu_MOVE_TO_FRONT
, self
.doMoveToFront
) 
 272         EVT_MENU(self
, menu_MOVE_BACKWARD
, self
.doMoveBackward
) 
 273         EVT_MENU(self
, menu_MOVE_TO_BACK
,  self
.doMoveToBack
) 
 275         EVT_MENU(self
, menu_ABOUT
, self
.doShowAbout
) 
 277         # Install our own method to handle closing the window.  This allows us 
 278         # to ask the user if he/she wants to save before closing the window, as 
 279         # well as keeping track of which windows are currently open. 
 281         EVT_CLOSE(self
, self
.doClose
) 
 283         # Install our own method for handling keystrokes.  We use this to let 
 284         # the user move the selected object(s) around using the arrow keys. 
 286         EVT_CHAR_HOOK(self
, self
.onKeyEvent
) 
 288         # Setup our top-most panel.  This holds the entire contents of the 
 289         # window, excluding the menu bar. 
 291         self
.topPanel 
= wxPanel(self
, -1, style
=wxSIMPLE_BORDER
) 
 293         # Setup our tool palette, with all our drawing tools and option icons. 
 295         self
.toolPalette 
= wxBoxSizer(wxVERTICAL
) 
 297         self
.selectIcon  
= ToolPaletteIcon(self
.topPanel
, id_SELECT
, 
 298                                            "select", "Selection Tool") 
 299         self
.lineIcon    
= ToolPaletteIcon(self
.topPanel
, id_LINE
, 
 301         self
.rectIcon    
= ToolPaletteIcon(self
.topPanel
, id_RECT
, 
 302                                            "rect", "Rectangle Tool") 
 303         self
.ellipseIcon 
= ToolPaletteIcon(self
.topPanel
, id_ELLIPSE
, 
 304                                            "ellipse", "Ellipse Tool") 
 305         self
.textIcon    
= ToolPaletteIcon(self
.topPanel
, id_TEXT
, 
 308         toolSizer 
= wxGridSizer(0, 2, 5, 5) 
 309         toolSizer
.Add(self
.selectIcon
) 
 310         toolSizer
.Add(0, 0) # Gap to make tool icons line up nicely. 
 311         toolSizer
.Add(self
.lineIcon
) 
 312         toolSizer
.Add(self
.rectIcon
) 
 313         toolSizer
.Add(self
.ellipseIcon
) 
 314         toolSizer
.Add(self
.textIcon
) 
 316         self
.optionIndicator 
= ToolOptionIndicator(self
.topPanel
) 
 317         self
.optionIndicator
.SetToolTip( 
 318                 wxToolTip("Shows Current Pen/Fill/Line Size Settings")) 
 320         optionSizer 
= wxBoxSizer(wxHORIZONTAL
) 
 322         self
.penOptIcon  
= ToolPaletteIcon(self
.topPanel
, id_PEN_OPT
, 
 323                                            "penOpt", "Set Pen Colour") 
 324         self
.fillOptIcon 
= ToolPaletteIcon(self
.topPanel
, id_FILL_OPT
, 
 325                                            "fillOpt", "Set Fill Colour") 
 326         self
.lineOptIcon 
= ToolPaletteIcon(self
.topPanel
, id_LINE_OPT
, 
 327                                            "lineOpt", "Set Line Size") 
 329         margin 
= wxLEFT | wxRIGHT
 
 330         optionSizer
.Add(self
.penOptIcon
,  0, margin
, 1) 
 331         optionSizer
.Add(self
.fillOptIcon
, 0, margin
, 1) 
 332         optionSizer
.Add(self
.lineOptIcon
, 0, margin
, 1) 
 334         margin 
= wxTOP | wxLEFT | wxRIGHT | wxALIGN_CENTRE
 
 335         self
.toolPalette
.Add(toolSizer
,            0, margin
, 5) 
 336         self
.toolPalette
.Add(0, 0,                 0, margin
, 5) # Spacer. 
 337         self
.toolPalette
.Add(self
.optionIndicator
, 0, margin
, 5) 
 338         self
.toolPalette
.Add(optionSizer
,          0, margin
, 5) 
 340         # Make the tool palette icons respond when the user clicks on them. 
 342         EVT_LEFT_DOWN(self
.selectIcon
,  self
.onToolIconClick
) 
 343         EVT_LEFT_DOWN(self
.lineIcon
,    self
.onToolIconClick
) 
 344         EVT_LEFT_DOWN(self
.rectIcon
,    self
.onToolIconClick
) 
 345         EVT_LEFT_DOWN(self
.ellipseIcon
, self
.onToolIconClick
) 
 346         EVT_LEFT_DOWN(self
.textIcon
,    self
.onToolIconClick
) 
 347         EVT_LEFT_DOWN(self
.penOptIcon
,  self
.onPenOptionIconClick
) 
 348         EVT_LEFT_DOWN(self
.fillOptIcon
, self
.onFillOptionIconClick
) 
 349         EVT_LEFT_DOWN(self
.lineOptIcon
, self
.onLineOptionIconClick
) 
 351         # Setup the main drawing area. 
 353         self
.drawPanel 
= wxScrolledWindow(self
.topPanel
, -1, 
 354                                           style
=wxSUNKEN_BORDER
) 
 355         self
.drawPanel
.SetBackgroundColour(wxWHITE
) 
 357         self
.drawPanel
.EnableScrolling(True, True) 
 358         self
.drawPanel
.SetScrollbars(20, 20, PAGE_WIDTH 
/ 20, PAGE_HEIGHT 
/ 20) 
 360         EVT_LEFT_DOWN(self
.drawPanel
, self
.onMouseEvent
) 
 361         EVT_LEFT_DCLICK(self
.drawPanel
, self
.onDoubleClickEvent
) 
 362         EVT_RIGHT_DOWN(self
.drawPanel
, self
.onRightClick
) 
 363         EVT_MOTION(self
.drawPanel
, self
.onMouseEvent
) 
 364         EVT_LEFT_UP(self
.drawPanel
, self
.onMouseEvent
) 
 365         EVT_PAINT(self
.drawPanel
, self
.onPaintEvent
) 
 367         # Position everything in the window. 
 369         topSizer 
= wxBoxSizer(wxHORIZONTAL
) 
 370         topSizer
.Add(self
.toolPalette
, 0) 
 371         topSizer
.Add(self
.drawPanel
, 1, wxEXPAND
) 
 373         self
.topPanel
.SetAutoLayout(True) 
 374         self
.topPanel
.SetSizer(topSizer
) 
 376         self
.SetSizeHints(minW
=250, minH
=200) 
 377         self
.SetSize(wxSize(600, 400)) 
 379         # Select an initial tool. 
 382         self
._setCurrentTool
(self
.selectIcon
) 
 384         # Setup our frame to hold the contents of a sketch document. 
 387         self
.fileName  
= fileName
 
 388         self
.contents  
= []     # front-to-back ordered list of DrawingObjects. 
 389         self
.selection 
= []     # List of selected DrawingObjects. 
 390         self
.undoInfo  
= None   # Saved contents for undo. 
 391         self
.dragMode  
= drag_NONE 
# Current mouse-drag mode. 
 393         if self
.fileName 
!= None: 
 398         # Finally, set our initial pen, fill and line options. 
 400         self
.penColour  
= wxBLACK
 
 401         self
.fillColour 
= wxWHITE
 
 404     # ============================ 
 405     # == Event Handling Methods == 
 406     # ============================ 
 408     def onToolIconClick(self
, event
): 
 409         """ Respond to the user clicking on one of our tool icons. 
 411         iconID 
= wxPyTypeCast(event
.GetEventObject(), "wxWindow").GetId() 
 412         if   iconID 
== id_SELECT
:   self
.doChooseSelectTool() 
 413         elif iconID 
== id_LINE
:     self
.doChooseLineTool() 
 414         elif iconID 
== id_RECT
:     self
.doChooseRectTool() 
 415         elif iconID 
== id_ELLIPSE
:  self
.doChooseEllipseTool() 
 416         elif iconID 
== id_TEXT
:     self
.doChooseTextTool() 
 420     def onPenOptionIconClick(self
, event
): 
 421         """ Respond to the user clicking on the "Pen Options" icon. 
 423         data 
= wxColourData() 
 424         if len(self
.selection
) == 1: 
 425             data
.SetColour(self
.selection
[0].getPenColour()) 
 427             data
.SetColour(self
.penColour
) 
 429         dialog 
= wxColourDialog(self
, data
) 
 430         if dialog
.ShowModal() == wxID_OK
: 
 431             c 
= dialog
.GetColourData().GetColour() 
 432             self
._setPenColour
(wxColour(c
.Red(), c
.Green(), c
.Blue())) 
 436     def onFillOptionIconClick(self
, event
): 
 437         """ Respond to the user clicking on the "Fill Options" icon. 
 439         data 
= wxColourData() 
 440         if len(self
.selection
) == 1: 
 441             data
.SetColour(self
.selection
[0].getFillColour()) 
 443             data
.SetColour(self
.fillColour
) 
 445         dialog 
= wxColourDialog(self
, data
) 
 446         if dialog
.ShowModal() == wxID_OK
: 
 447             c 
= dialog
.GetColourData().GetColour() 
 448             self
._setFillColour
(wxColour(c
.Red(), c
.Green(), c
.Blue())) 
 451     def onLineOptionIconClick(self
, event
): 
 452         """ Respond to the user clicking on the "Line Options" icon. 
 454         if len(self
.selection
) == 1: 
 455             menu 
= self
._buildLineSizePopup
(self
.selection
[0].getLineSize()) 
 457             menu 
= self
._buildLineSizePopup
(self
.lineSize
) 
 459         pos 
= self
.lineOptIcon
.GetPosition() 
 460         pos
.y 
= pos
.y 
+ self
.lineOptIcon
.GetSize().height
 
 461         self
.PopupMenu(menu
, pos
) 
 465     def onKeyEvent(self
, event
): 
 466         """ Respond to a keypress event. 
 468             We make the arrow keys move the selected object(s) by one pixel in 
 471         if event
.GetKeyCode() == WXK_UP
: 
 472             self
._moveObject
(0, -1) 
 473         elif event
.GetKeyCode() == WXK_DOWN
: 
 474             self
._moveObject
(0, 1) 
 475         elif event
.GetKeyCode() == WXK_LEFT
: 
 476             self
._moveObject
(-1, 0) 
 477         elif event
.GetKeyCode() == WXK_RIGHT
: 
 478             self
._moveObject
(1, 0) 
 483     def onMouseEvent(self
, event
): 
 484         """ Respond to the user clicking on our main drawing panel. 
 486             How we respond depends on the currently selected tool. 
 488         if not (event
.LeftDown() or event
.Dragging() or event
.LeftUp()): 
 489             return # Ignore mouse movement without click/drag. 
 491         if self
.curTool 
== self
.selectIcon
: 
 492             feedbackType 
= feedback_RECT
 
 493             action       
= self
.selectByRectangle
 
 494             actionParam  
= param_RECT
 
 497         elif self
.curTool 
== self
.lineIcon
: 
 498             feedbackType 
= feedback_LINE
 
 499             action       
= self
.createLine
 
 500             actionParam  
= param_LINE
 
 503         elif self
.curTool 
== self
.rectIcon
: 
 504             feedbackType 
= feedback_RECT
 
 505             action       
= self
.createRect
 
 506             actionParam  
= param_RECT
 
 509         elif self
.curTool 
== self
.ellipseIcon
: 
 510             feedbackType 
= feedback_ELLIPSE
 
 511             action       
= self
.createEllipse
 
 512             actionParam  
= param_RECT
 
 515         elif self
.curTool 
== self
.textIcon
: 
 516             feedbackType 
= feedback_RECT
 
 517             action       
= self
.createText
 
 518             actionParam  
= param_RECT
 
 526             mousePt 
= self
._getEventCoordinates
(event
) 
 528                 obj
, handle 
= self
._getObjectAndSelectionHandleAt
(mousePt
) 
 530             if selecting 
and (obj 
!= None) and (handle 
!= handle_NONE
): 
 532                 # The user clicked on an object's selection handle.  Let the 
 533                 # user resize the clicked-on object. 
 535                 self
.dragMode     
= drag_RESIZE
 
 536                 self
.resizeObject 
= obj
 
 538                 if obj
.getType() == obj_LINE
: 
 539                     self
.resizeFeedback 
= feedback_LINE
 
 540                     pos  
= obj
.getPosition() 
 541                     startPt 
= wxPoint(pos
.x 
+ obj
.getStartPt().x
, 
 542                                       pos
.y 
+ obj
.getStartPt().y
) 
 543                     endPt   
= wxPoint(pos
.x 
+ obj
.getEndPt().x
, 
 544                                       pos
.y 
+ obj
.getEndPt().y
) 
 545                     if handle 
== handle_START_POINT
: 
 546                         self
.resizeAnchor  
= endPt
 
 547                         self
.resizeFloater 
= startPt
 
 549                         self
.resizeAnchor  
= startPt
 
 550                         self
.resizeFloater 
= endPt
 
 552                     self
.resizeFeedback 
= feedback_RECT
 
 553                     pos  
= obj
.getPosition() 
 555                     topLeft  
= wxPoint(pos
.x
, pos
.y
) 
 556                     topRight 
= wxPoint(pos
.x 
+ size
.width
, pos
.y
) 
 557                     botLeft  
= wxPoint(pos
.x
, pos
.y 
+ size
.height
) 
 558                     botRight 
= wxPoint(pos
.x 
+ size
.width
, pos
.y 
+ size
.height
) 
 560                     if handle 
== handle_TOP_LEFT
: 
 561                         self
.resizeAnchor  
= botRight
 
 562                         self
.resizeFloater 
= topLeft
 
 563                     elif handle 
== handle_TOP_RIGHT
: 
 564                         self
.resizeAnchor  
= botLeft
 
 565                         self
.resizeFloater 
= topRight
 
 566                     elif handle 
== handle_BOTTOM_LEFT
: 
 567                         self
.resizeAnchor  
= topRight
 
 568                         self
.resizeFloater 
= botLeft
 
 569                     elif handle 
== handle_BOTTOM_RIGHT
: 
 570                         self
.resizeAnchor  
= topLeft
 
 571                         self
.resizeFloater 
= botRight
 
 574                 self
.resizeOffsetX 
= self
.resizeFloater
.x 
- mousePt
.x
 
 575                 self
.resizeOffsetY 
= self
.resizeFloater
.y 
- mousePt
.y
 
 576                 endPt 
= wxPoint(self
.curPt
.x 
+ self
.resizeOffsetX
, 
 577                                 self
.curPt
.y 
+ self
.resizeOffsetY
) 
 578                 self
._drawVisualFeedback
(self
.resizeAnchor
, endPt
, 
 579                                          self
.resizeFeedback
, False) 
 581             elif selecting 
and (self
._getObjectAt
(mousePt
) != None): 
 583                 # The user clicked on an object to select it.  If the user 
 584                 # drags, he/she will move the object. 
 586                 self
.select(self
._getObjectAt
(mousePt
)) 
 587                 self
.dragMode 
= drag_MOVE
 
 588                 self
.moveOrigin 
= mousePt
 
 590                 self
._drawObjectOutline
(0, 0) 
 594                 # The user is dragging out a selection rect or new object. 
 596                 self
.dragOrigin 
= mousePt
 
 598                 self
.drawPanel
.SetCursor(wxCROSS_CURSOR
) 
 599                 self
.drawPanel
.CaptureMouse() 
 600                 self
._drawVisualFeedback
(mousePt
, mousePt
, feedbackType
, 
 602                 self
.dragMode 
= drag_DRAG
 
 608             if self
.dragMode 
== drag_RESIZE
: 
 610                 # We're resizing an object. 
 612                 mousePt 
= self
._getEventCoordinates
(event
) 
 613                 if (self
.curPt
.x 
!= mousePt
.x
) or (self
.curPt
.y 
!= mousePt
.y
): 
 614                     # Erase previous visual feedback. 
 615                     endPt 
= wxPoint(self
.curPt
.x 
+ self
.resizeOffsetX
, 
 616                                     self
.curPt
.y 
+ self
.resizeOffsetY
) 
 617                     self
._drawVisualFeedback
(self
.resizeAnchor
, endPt
, 
 618                                              self
.resizeFeedback
, False) 
 620                     # Draw new visual feedback. 
 621                     endPt 
= wxPoint(self
.curPt
.x 
+ self
.resizeOffsetX
, 
 622                                     self
.curPt
.y 
+ self
.resizeOffsetY
) 
 623                     self
._drawVisualFeedback
(self
.resizeAnchor
, endPt
, 
 624                                              self
.resizeFeedback
, False) 
 626             elif self
.dragMode 
== drag_MOVE
: 
 628                 # We're moving a selected object. 
 630                 mousePt 
= self
._getEventCoordinates
(event
) 
 631                 if (self
.curPt
.x 
!= mousePt
.x
) or (self
.curPt
.y 
!= mousePt
.y
): 
 632                     # Erase previous visual feedback. 
 633                     self
._drawObjectOutline
(self
.curPt
.x 
- self
.moveOrigin
.x
, 
 634                                             self
.curPt
.y 
- self
.moveOrigin
.y
) 
 636                     # Draw new visual feedback. 
 637                     self
._drawObjectOutline
(self
.curPt
.x 
- self
.moveOrigin
.x
, 
 638                                             self
.curPt
.y 
- self
.moveOrigin
.y
) 
 640             elif self
.dragMode 
== drag_DRAG
: 
 642                 # We're dragging out a new object or selection rect. 
 644                 mousePt 
= self
._getEventCoordinates
(event
) 
 645                 if (self
.curPt
.x 
!= mousePt
.x
) or (self
.curPt
.y 
!= mousePt
.y
): 
 646                     # Erase previous visual feedback. 
 647                     self
._drawVisualFeedback
(self
.dragOrigin
, self
.curPt
, 
 648                                              feedbackType
, dashedLine
) 
 650                     # Draw new visual feedback. 
 651                     self
._drawVisualFeedback
(self
.dragOrigin
, self
.curPt
, 
 652                                              feedbackType
, dashedLine
) 
 658             if self
.dragMode 
== drag_RESIZE
: 
 660                 # We're resizing an object. 
 662                 mousePt 
= self
._getEventCoordinates
(event
) 
 663                 # Erase last visual feedback. 
 664                 endPt 
= wxPoint(self
.curPt
.x 
+ self
.resizeOffsetX
, 
 665                                 self
.curPt
.y 
+ self
.resizeOffsetY
) 
 666                 self
._drawVisualFeedback
(self
.resizeAnchor
, endPt
, 
 667                                          self
.resizeFeedback
, False) 
 669                 resizePt 
= wxPoint(mousePt
.x 
+ self
.resizeOffsetX
, 
 670                                    mousePt
.y 
+ self
.resizeOffsetY
) 
 672                 if (self
.resizeFloater
.x 
!= resizePt
.x
) or \
 
 673                    (self
.resizeFloater
.y 
!= resizePt
.y
): 
 674                    self
._resizeObject
(self
.resizeObject
, 
 679                     self
.drawPanel
.Refresh() # Clean up after empty resize. 
 681             elif self
.dragMode 
== drag_MOVE
: 
 683                 # We're moving a selected object. 
 685                 mousePt 
= self
._getEventCoordinates
(event
) 
 686                 # Erase last visual feedback. 
 687                 self
._drawObjectOutline
(self
.curPt
.x 
- self
.moveOrigin
.x
, 
 688                                         self
.curPt
.y 
- self
.moveOrigin
.y
) 
 689                 if (self
.moveOrigin
.x 
!= mousePt
.x
) or \
 
 690                    (self
.moveOrigin
.y 
!= mousePt
.y
): 
 691                     self
._moveObject
(mousePt
.x 
- self
.moveOrigin
.x
, 
 692                                      mousePt
.y 
- self
.moveOrigin
.y
) 
 694                     self
.drawPanel
.Refresh() # Clean up after empty drag. 
 696             elif self
.dragMode 
== drag_DRAG
: 
 698                 # We're dragging out a new object or selection rect. 
 700                 mousePt 
= self
._getEventCoordinates
(event
) 
 701                 # Erase last visual feedback. 
 702                 self
._drawVisualFeedback
(self
.dragOrigin
, self
.curPt
, 
 703                                          feedbackType
, dashedLine
) 
 704                 self
.drawPanel
.ReleaseMouse() 
 705                 self
.drawPanel
.SetCursor(wxSTANDARD_CURSOR
) 
 706                 # Perform the appropriate action for the current tool. 
 707                 if actionParam 
== param_RECT
: 
 708                     x1 
= min(self
.dragOrigin
.x
, self
.curPt
.x
) 
 709                     y1 
= min(self
.dragOrigin
.y
, self
.curPt
.y
) 
 710                     x2 
= max(self
.dragOrigin
.x
, self
.curPt
.x
) 
 711                     y2 
= max(self
.dragOrigin
.y
, self
.curPt
.y
) 
 719                         if ((x2
-x1
) < 8) or ((y2
-y1
) < 8): return # Too small. 
 721                     action(x1
, y1
, x2
-x1
, y2
-y1
) 
 722                 elif actionParam 
== param_LINE
: 
 723                     action(self
.dragOrigin
.x
, self
.dragOrigin
.y
, 
 724                            self
.curPt
.x
, self
.curPt
.y
) 
 726             self
.dragMode 
= drag_NONE 
# We've finished with this mouse event. 
 730     def onDoubleClickEvent(self
, event
): 
 731         """ Respond to a double-click within our drawing panel. 
 733         mousePt 
= self
._getEventCoordinates
(event
) 
 734         obj 
= self
._getObjectAt
(mousePt
) 
 735         if obj 
== None: return 
 737         # Let the user edit the given object. 
 739         if obj
.getType() == obj_TEXT
: 
 740             editor 
= EditTextObjectDialog(self
, "Edit Text Object") 
 741             editor
.objectToDialog(obj
) 
 742             if editor
.ShowModal() == wxID_CANCEL
: 
 748             editor
.dialogToObject(obj
) 
 752             self
.drawPanel
.Refresh() 
 758     def onRightClick(self
, event
): 
 759         """ Respond to the user right-clicking within our drawing panel. 
 761             We select the clicked-on item, if necessary, and display a pop-up 
 762             menu of available options which can be applied to the selected 
 765         mousePt 
= self
._getEventCoordinates
(event
) 
 766         obj 
= self
._getObjectAt
(mousePt
) 
 768         if obj 
== None: return # Nothing selected. 
 770         # Select the clicked-on object. 
 774         # Build our pop-up menu. 
 777         menu
.Append(menu_DUPLICATE
, "Duplicate") 
 778         menu
.Append(menu_EDIT_TEXT
, "Edit...") 
 779         menu
.Append(menu_DELETE
,    "Delete") 
 780         menu
.AppendSeparator() 
 781         menu
.Append(menu_MOVE_FORWARD
,   "Move Forward") 
 782         menu
.Append(menu_MOVE_TO_FRONT
,  "Move to Front") 
 783         menu
.Append(menu_MOVE_BACKWARD
,  "Move Backward") 
 784         menu
.Append(menu_MOVE_TO_BACK
,   "Move to Back") 
 786         menu
.Enable(menu_EDIT_TEXT
,     obj
.getType() == obj_TEXT
) 
 787         menu
.Enable(menu_MOVE_FORWARD
,  obj 
!= self
.contents
[0]) 
 788         menu
.Enable(menu_MOVE_TO_FRONT
, obj 
!= self
.contents
[0]) 
 789         menu
.Enable(menu_MOVE_BACKWARD
, obj 
!= self
.contents
[-1]) 
 790         menu
.Enable(menu_MOVE_TO_BACK
,  obj 
!= self
.contents
[-1]) 
 792         EVT_MENU(self
, menu_DUPLICATE
,     self
.doDuplicate
) 
 793         EVT_MENU(self
, menu_EDIT_TEXT
,     self
.doEditText
) 
 794         EVT_MENU(self
, menu_DELETE
,        self
.doDelete
) 
 795         EVT_MENU(self
, menu_MOVE_FORWARD
,  self
.doMoveForward
) 
 796         EVT_MENU(self
, menu_MOVE_TO_FRONT
, self
.doMoveToFront
) 
 797         EVT_MENU(self
, menu_MOVE_BACKWARD
, self
.doMoveBackward
) 
 798         EVT_MENU(self
, menu_MOVE_TO_BACK
,  self
.doMoveToBack
) 
 800         # Show the pop-up menu. 
 802         clickPt 
= wxPoint(mousePt
.x 
+ self
.drawPanel
.GetPosition().x
, 
 803                           mousePt
.y 
+ self
.drawPanel
.GetPosition().y
) 
 804         self
.drawPanel
.PopupMenu(menu
, clickPt
) 
 808     def onPaintEvent(self
, event
): 
 809         """ Respond to a request to redraw the contents of our drawing panel. 
 811         dc 
= wxPaintDC(self
.drawPanel
) 
 812         self
.drawPanel
.PrepareDC(dc
) 
 815         for i 
in range(len(self
.contents
)-1, -1, -1): 
 816             obj 
= self
.contents
[i
] 
 817             if obj 
in self
.selection
: 
 824     # ========================== 
 825     # == Menu Command Methods == 
 826     # ========================== 
 828     def doNew(self
, event
): 
 829         """ Respond to the "New" menu command. 
 832         newFrame 
= DrawingFrame(None, -1, "Untitled") 
 834         _docList
.append(newFrame
) 
 837     def doOpen(self
, event
): 
 838         """ Respond to the "Open" menu command. 
 843         fileName 
= wxFileSelector("Open File", default_extension
="psk", 
 844                                   flags 
= wxOPEN | wxFILE_MUST_EXIST
) 
 845         if fileName 
== "": return 
 846         fileName 
= os
.path
.join(os
.getcwd(), fileName
) 
 849         title 
= os
.path
.basename(fileName
) 
 851         if (self
.fileName 
== None) and (len(self
.contents
) == 0): 
 852             # Load contents into current (empty) document. 
 853             self
.fileName 
= fileName
 
 854             self
.SetTitle(os
.path
.basename(fileName
)) 
 857             # Open a new frame for this document. 
 858             newFrame 
= DrawingFrame(None, -1, os
.path
.basename(fileName
), 
 861             _docList
.append(newFrame
) 
 864     def doClose(self
, event
): 
 865         """ Respond to the "Close" menu command. 
 870             if not self
.askIfUserWantsToSave("closing"): return 
 872         _docList
.remove(self
) 
 876     def doSave(self
, event
): 
 877         """ Respond to the "Save" menu command. 
 879         if self
.fileName 
!= None: 
 883     def doSaveAs(self
, event
): 
 884         """ Respond to the "Save As" menu command. 
 886         if self
.fileName 
== None: 
 889             default 
= self
.fileName
 
 892         fileName 
= wxFileSelector("Save File As", "Saving", 
 893                                   default_filename
=default
, 
 894                                   default_extension
="psk", 
 896                                   flags 
= wxSAVE | wxOVERWRITE_PROMPT
) 
 897         if fileName 
== "": return # User cancelled. 
 898         fileName 
= os
.path
.join(os
.getcwd(), fileName
) 
 901         title 
= os
.path
.basename(fileName
) 
 904         self
.fileName 
= fileName
 
 908     def doRevert(self
, event
): 
 909         """ Respond to the "Revert" menu command. 
 911         if not self
.dirty
: return 
 913         if wxMessageBox("Discard changes made to this document?", "Confirm", 
 914                         style 
= wxOK | wxCANCEL | wxICON_QUESTION
, 
 915                         parent
=self
) == wxCANCEL
: return 
 919     def doExit(self
, event
): 
 920         """ Respond to the "Quit" menu command. 
 922         global _docList
, _app
 
 924             if not doc
.dirty
: continue 
 926             if not doc
.askIfUserWantsToSave("quitting"): return 
 933     def doUndo(self
, event
): 
 934         """ Respond to the "Undo" menu command. 
 936         if self
.undoInfo 
== None: return 
 938         undoData 
= self
.undoInfo
 
 939         self
._saveUndoInfo
() # For undoing the undo... 
 943         for type, data 
in undoData
["contents"]: 
 944             obj 
= DrawingObject(type) 
 946             self
.contents
.append(obj
) 
 949         for i 
in undoData
["selection"]: 
 950             self
.selection
.append(self
.contents
[i
]) 
 953         self
.drawPanel
.Refresh() 
 957     def doSelectAll(self
, event
): 
 958         """ Respond to the "Select All" menu command. 
 963     def doDuplicate(self
, event
): 
 964         """ Respond to the "Duplicate" menu command. 
 969         for obj 
in self
.contents
: 
 970             if obj 
in self
.selection
: 
 971                 newObj 
= DrawingObject(obj
.getType()) 
 972                 newObj
.setData(obj
.getData()) 
 973                 pos 
= obj
.getPosition() 
 974                 newObj
.setPosition(wxPoint(pos
.x 
+ 10, pos
.y 
+ 10)) 
 977         self
.contents 
= objs 
+ self
.contents
 
 979         self
.selectMany(objs
) 
 982     def doEditText(self
, event
): 
 983         """ Respond to the "Edit Text" menu command. 
 985         if len(self
.selection
) != 1: return 
 987         obj 
= self
.selection
[0] 
 988         if obj
.getType() != obj_TEXT
: return 
 990         editor 
= EditTextObjectDialog(self
, "Edit Text Object") 
 991         editor
.objectToDialog(obj
) 
 992         if editor
.ShowModal() == wxID_CANCEL
: 
 998         editor
.dialogToObject(obj
) 
1002         self
.drawPanel
.Refresh() 
1006     def doDelete(self
, event
): 
1007         """ Respond to the "Delete" menu command. 
1009         self
._saveUndoInfo
() 
1011         for obj 
in self
.selection
: 
1012             self
.contents
.remove(obj
) 
1017     def doChooseSelectTool(self
, event
=None): 
1018         """ Respond to the "Select Tool" menu command. 
1020         self
._setCurrentTool
(self
.selectIcon
) 
1021         self
.drawPanel
.SetCursor(wxSTANDARD_CURSOR
) 
1025     def doChooseLineTool(self
, event
=None): 
1026         """ Respond to the "Line Tool" menu command. 
1028         self
._setCurrentTool
(self
.lineIcon
) 
1029         self
.drawPanel
.SetCursor(wxCROSS_CURSOR
) 
1034     def doChooseRectTool(self
, event
=None): 
1035         """ Respond to the "Rect Tool" menu command. 
1037         self
._setCurrentTool
(self
.rectIcon
) 
1038         self
.drawPanel
.SetCursor(wxCROSS_CURSOR
) 
1043     def doChooseEllipseTool(self
, event
=None): 
1044         """ Respond to the "Ellipse Tool" menu command. 
1046         self
._setCurrentTool
(self
.ellipseIcon
) 
1047         self
.drawPanel
.SetCursor(wxCROSS_CURSOR
) 
1052     def doChooseTextTool(self
, event
=None): 
1053         """ Respond to the "Text Tool" menu command. 
1055         self
._setCurrentTool
(self
.textIcon
) 
1056         self
.drawPanel
.SetCursor(wxCROSS_CURSOR
) 
1061     def doMoveForward(self
, event
): 
1062         """ Respond to the "Move Forward" menu command. 
1064         if len(self
.selection
) != 1: return 
1066         self
._saveUndoInfo
() 
1068         obj 
= self
.selection
[0] 
1069         index 
= self
.contents
.index(obj
) 
1070         if index 
== 0: return 
1072         del self
.contents
[index
] 
1073         self
.contents
.insert(index
-1, obj
) 
1075         self
.drawPanel
.Refresh() 
1079     def doMoveToFront(self
, event
): 
1080         """ Respond to the "Move to Front" menu command. 
1082         if len(self
.selection
) != 1: return 
1084         self
._saveUndoInfo
() 
1086         obj 
= self
.selection
[0] 
1087         self
.contents
.remove(obj
) 
1088         self
.contents
.insert(0, obj
) 
1090         self
.drawPanel
.Refresh() 
1094     def doMoveBackward(self
, event
): 
1095         """ Respond to the "Move Backward" menu command. 
1097         if len(self
.selection
) != 1: return 
1099         self
._saveUndoInfo
() 
1101         obj 
= self
.selection
[0] 
1102         index 
= self
.contents
.index(obj
) 
1103         if index 
== len(self
.contents
) - 1: return 
1105         del self
.contents
[index
] 
1106         self
.contents
.insert(index
+1, obj
) 
1108         self
.drawPanel
.Refresh() 
1112     def doMoveToBack(self
, event
): 
1113         """ Respond to the "Move to Back" menu command. 
1115         if len(self
.selection
) != 1: return 
1117         self
._saveUndoInfo
() 
1119         obj 
= self
.selection
[0] 
1120         self
.contents
.remove(obj
) 
1121         self
.contents
.append(obj
) 
1123         self
.drawPanel
.Refresh() 
1127     def doShowAbout(self
, event
): 
1128         """ Respond to the "About pySketch" menu command. 
1130         dialog 
= wxDialog(self
, -1, "About pySketch") # , 
1131                           #style=wxDIALOG_MODAL | wxSTAY_ON_TOP) 
1132         dialog
.SetBackgroundColour(wxWHITE
) 
1134         panel 
= wxPanel(dialog
, -1) 
1135         panel
.SetBackgroundColour(wxWHITE
) 
1137         panelSizer 
= wxBoxSizer(wxVERTICAL
) 
1139         boldFont 
= wxFont(panel
.GetFont().GetPointSize(), 
1140                           panel
.GetFont().GetFamily(), 
1143         logo 
= wxStaticBitmap(panel
, -1, wxBitmap("images/logo.bmp", 
1146         lab1 
= wxStaticText(panel
, -1, "pySketch") 
1147         lab1
.SetFont(wxFont(36, boldFont
.GetFamily(), wxITALIC
, wxBOLD
)) 
1148         lab1
.SetSize(lab1
.GetBestSize()) 
1150         imageSizer 
= wxBoxSizer(wxHORIZONTAL
) 
1151         imageSizer
.Add(logo
, 0, wxALL | wxALIGN_CENTRE_VERTICAL
, 5) 
1152         imageSizer
.Add(lab1
, 0, wxALL | wxALIGN_CENTRE_VERTICAL
, 5) 
1154         lab2 
= wxStaticText(panel
, -1, "A simple object-oriented drawing " + \
 
1156         lab2
.SetFont(boldFont
) 
1157         lab2
.SetSize(lab2
.GetBestSize()) 
1159         lab3 
= wxStaticText(panel
, -1, "pySketch is completely free " + \
 
1161         lab3
.SetFont(boldFont
) 
1162         lab3
.SetSize(lab3
.GetBestSize()) 
1164         lab4 
= wxStaticText(panel
, -1, "feel free to adapt or use this " + \
 
1165                                        "in any way you like.") 
1166         lab4
.SetFont(boldFont
) 
1167         lab4
.SetSize(lab4
.GetBestSize()) 
1169         lab5 
= wxStaticText(panel
, -1, "Author: Erik Westra " + \
 
1170                                        "(ewestra@wave.co.nz)") 
1171         lab5
.SetFont(boldFont
) 
1172         lab5
.SetSize(lab5
.GetBestSize()) 
1174         btnOK 
= wxButton(panel
, wxID_OK
, "OK") 
1176         panelSizer
.Add(imageSizer
, 0, wxALIGN_CENTRE
) 
1177         panelSizer
.Add(10, 10) # Spacer. 
1178         panelSizer
.Add(lab2
, 0, wxALIGN_CENTRE
) 
1179         panelSizer
.Add(10, 10) # Spacer. 
1180         panelSizer
.Add(lab3
, 0, wxALIGN_CENTRE
) 
1181         panelSizer
.Add(lab4
, 0, wxALIGN_CENTRE
) 
1182         panelSizer
.Add(10, 10) # Spacer. 
1183         panelSizer
.Add(lab5
, 0, wxALIGN_CENTRE
) 
1184         panelSizer
.Add(10, 10) # Spacer. 
1185         panelSizer
.Add(btnOK
, 0, wxALL | wxALIGN_CENTRE
, 5) 
1187         panel
.SetAutoLayout(True) 
1188         panel
.SetSizer(panelSizer
) 
1189         panelSizer
.Fit(panel
) 
1191         topSizer 
= wxBoxSizer(wxHORIZONTAL
) 
1192         topSizer
.Add(panel
, 0, wxALL
, 10) 
1194         dialog
.SetAutoLayout(True) 
1195         dialog
.SetSizer(topSizer
) 
1196         topSizer
.Fit(dialog
) 
1200         btn 
= dialog
.ShowModal() 
1203     # ============================= 
1204     # == Object Creation Methods == 
1205     # ============================= 
1207     def createLine(self
, x1
, y1
, x2
, y2
): 
1208         """ Create a new line object at the given position and size. 
1210         self
._saveUndoInfo
() 
1212         topLeftX  
= min(x1
, x2
) 
1213         topLeftY  
= min(y1
, y2
) 
1214         botRightX 
= max(x1
, x2
) 
1215         botRightY 
= max(y1
, y2
) 
1217         obj 
= DrawingObject(obj_LINE
, position
=wxPoint(topLeftX
, topLeftY
), 
1218                             size
=wxSize(botRightX
-topLeftX
, 
1219                                         botRightY
-topLeftY
), 
1220                             penColour
=self
.penColour
, 
1221                             fillColour
=self
.fillColour
, 
1222                             lineSize
=self
.lineSize
, 
1223                             startPt 
= wxPoint(x1 
- topLeftX
, y1 
- topLeftY
), 
1224                             endPt   
= wxPoint(x2 
- topLeftX
, y2 
- topLeftY
)) 
1225         self
.contents
.insert(0, obj
) 
1227         self
.doChooseSelectTool() 
1231     def createRect(self
, x
, y
, width
, height
): 
1232         """ Create a new rectangle object at the given position and size. 
1234         self
._saveUndoInfo
() 
1236         obj 
= DrawingObject(obj_RECT
, position
=wxPoint(x
, y
), 
1237                             size
=wxSize(width
, height
), 
1238                             penColour
=self
.penColour
, 
1239                             fillColour
=self
.fillColour
, 
1240                             lineSize
=self
.lineSize
) 
1241         self
.contents
.insert(0, obj
) 
1243         self
.doChooseSelectTool() 
1247     def createEllipse(self
, x
, y
, width
, height
): 
1248         """ Create a new ellipse object at the given position and size. 
1250         self
._saveUndoInfo
() 
1252         obj 
= DrawingObject(obj_ELLIPSE
, position
=wxPoint(x
, y
), 
1253                             size
=wxSize(width
, height
), 
1254                             penColour
=self
.penColour
, 
1255                             fillColour
=self
.fillColour
, 
1256                             lineSize
=self
.lineSize
) 
1257         self
.contents
.insert(0, obj
) 
1259         self
.doChooseSelectTool() 
1263     def createText(self
, x
, y
, width
, height
): 
1264         """ Create a new text object at the given position and size. 
1266         editor 
= EditTextObjectDialog(self
, "Create Text Object") 
1267         if editor
.ShowModal() == wxID_CANCEL
: 
1271         self
._saveUndoInfo
() 
1273         obj 
= DrawingObject(obj_TEXT
, position
=wxPoint(x
, y
), 
1274                                       size
=wxSize(width
, height
)) 
1275         editor
.dialogToObject(obj
) 
1278         self
.contents
.insert(0, obj
) 
1280         self
.doChooseSelectTool() 
1283     # ======================= 
1284     # == Selection Methods == 
1285     # ======================= 
1287     def selectAll(self
): 
1288         """ Select every DrawingObject in our document. 
1291         for obj 
in self
.contents
: 
1292             self
.selection
.append(obj
) 
1293         self
.drawPanel
.Refresh() 
1297     def deselectAll(self
): 
1298         """ Deselect every DrawingObject in our document. 
1301         self
.drawPanel
.Refresh() 
1305     def select(self
, obj
): 
1306         """ Select the given DrawingObject within our document. 
1308         self
.selection 
= [obj
] 
1309         self
.drawPanel
.Refresh() 
1313     def selectMany(self
, objs
): 
1314         """ Select the given list of DrawingObjects. 
1316         self
.selection 
= objs
 
1317         self
.drawPanel
.Refresh() 
1321     def selectByRectangle(self
, x
, y
, width
, height
): 
1322         """ Select every DrawingObject in the given rectangular region. 
1325         for obj 
in self
.contents
: 
1326             if obj
.objectWithinRect(x
, y
, width
, height
): 
1327                 self
.selection
.append(obj
) 
1328         self
.drawPanel
.Refresh() 
1331     # ====================== 
1332     # == File I/O Methods == 
1333     # ====================== 
1335     def loadContents(self
): 
1336         """ Load the contents of our document into memory. 
1338         f 
= open(self
.fileName
, "rb") 
1339         objData 
= cPickle
.load(f
) 
1342         for type, data 
in objData
: 
1343             obj 
= DrawingObject(type) 
1345             self
.contents
.append(obj
) 
1349         self
.undoInfo  
= None 
1351         self
.drawPanel
.Refresh() 
1355     def saveContents(self
): 
1356         """ Save the contents of our document to disk. 
1359         for obj 
in self
.contents
: 
1360             objData
.append([obj
.getType(), obj
.getData()]) 
1362         f 
= open(self
.fileName
, "wb") 
1363         cPickle
.dump(objData
, f
) 
1369     def askIfUserWantsToSave(self
, action
): 
1370         """ Give the user the opportunity to save the current document. 
1372             'action' is a string describing the action about to be taken.  If 
1373             the user wants to save the document, it is saved immediately.  If 
1374             the user cancels, we return False. 
1376         if not self
.dirty
: return True # Nothing to do. 
1378         response 
= wxMessageBox("Save changes before " + action 
+ "?", 
1379                                 "Confirm", wxYES_NO | wxCANCEL
, self
) 
1381         if response 
== wxYES
: 
1382             if self
.fileName 
== None: 
1383                 fileName 
= wxFileSelector("Save File As", "Saving", 
1384                                           default_extension
="psk", 
1386                                           flags 
= wxSAVE | wxOVERWRITE_PROMPT
) 
1387                 if fileName 
== "": return False # User cancelled. 
1388                 self
.fileName 
= fileName
 
1392         elif response 
== wxNO
: 
1393             return True # User doesn't want changes saved. 
1394         elif response 
== wxCANCEL
: 
1395             return False # User cancelled. 
1397     # ===================== 
1398     # == Private Methods == 
1399     # ===================== 
1401     def _adjustMenus(self
): 
1402         """ Adjust our menus and toolbar to reflect the current state of the 
1405         canSave   
= (self
.fileName 
!= None) and self
.dirty
 
1406         canRevert 
= (self
.fileName 
!= None) and self
.dirty
 
1407         canUndo   
= self
.undoInfo 
!= None 
1408         selection 
= len(self
.selection
) > 0 
1409         onlyOne   
= len(self
.selection
) == 1 
1410         isText    
= onlyOne 
and (self
.selection
[0].getType() == obj_TEXT
) 
1411         front     
= onlyOne 
and (self
.selection
[0] == self
.contents
[0]) 
1412         back      
= onlyOne 
and (self
.selection
[0] == self
.contents
[-1]) 
1414         # Enable/disable our menu items. 
1416         self
.fileMenu
.Enable(wxID_SAVE
,   canSave
) 
1417         self
.fileMenu
.Enable(wxID_REVERT
, canRevert
) 
1419         self
.editMenu
.Enable(menu_UNDO
,      canUndo
) 
1420         self
.editMenu
.Enable(menu_DUPLICATE
, selection
) 
1421         self
.editMenu
.Enable(menu_EDIT_TEXT
, isText
) 
1422         self
.editMenu
.Enable(menu_DELETE
,    selection
) 
1424         self
.toolsMenu
.Check(menu_SELECT
,  self
.curTool 
== self
.selectIcon
) 
1425         self
.toolsMenu
.Check(menu_LINE
,    self
.curTool 
== self
.lineIcon
) 
1426         self
.toolsMenu
.Check(menu_RECT
,    self
.curTool 
== self
.rectIcon
) 
1427         self
.toolsMenu
.Check(menu_ELLIPSE
, self
.curTool 
== self
.ellipseIcon
) 
1428         self
.toolsMenu
.Check(menu_TEXT
,    self
.curTool 
== self
.textIcon
) 
1430         self
.objectMenu
.Enable(menu_MOVE_FORWARD
,  onlyOne 
and not front
) 
1431         self
.objectMenu
.Enable(menu_MOVE_TO_FRONT
, onlyOne 
and not front
) 
1432         self
.objectMenu
.Enable(menu_MOVE_BACKWARD
, onlyOne 
and not back
) 
1433         self
.objectMenu
.Enable(menu_MOVE_TO_BACK
,  onlyOne 
and not back
) 
1435         # Enable/disable our toolbar icons. 
1437         self
.toolbar
.EnableTool(wxID_NEW
,           True) 
1438         self
.toolbar
.EnableTool(wxID_OPEN
,          True) 
1439         self
.toolbar
.EnableTool(wxID_SAVE
,          canSave
) 
1440         self
.toolbar
.EnableTool(menu_UNDO
,          canUndo
) 
1441         self
.toolbar
.EnableTool(menu_DUPLICATE
,     selection
) 
1442         self
.toolbar
.EnableTool(menu_MOVE_FORWARD
,  onlyOne 
and not front
) 
1443         self
.toolbar
.EnableTool(menu_MOVE_BACKWARD
, onlyOne 
and not back
) 
1446     def _setCurrentTool(self
, newToolIcon
): 
1447         """ Set the currently selected tool. 
1449         if self
.curTool 
== newToolIcon
: return # Nothing to do. 
1451         if self
.curTool 
!= None: 
1452             self
.curTool
.deselect() 
1454         newToolIcon
.select() 
1455         self
.curTool 
= newToolIcon
 
1458     def _setPenColour(self
, colour
): 
1459         """ Set the default or selected object's pen colour. 
1461         if len(self
.selection
) > 0: 
1462             self
._saveUndoInfo
() 
1463             for obj 
in self
.selection
: 
1464                 obj
.setPenColour(colour
) 
1465             self
.drawPanel
.Refresh() 
1467             self
.penColour 
= colour
 
1468             self
.optionIndicator
.setPenColour(colour
) 
1471     def _setFillColour(self
, colour
): 
1472         """ Set the default or selected object's fill colour. 
1474         if len(self
.selection
) > 0: 
1475             self
._saveUndoInfo
() 
1476             for obj 
in self
.selection
: 
1477                 obj
.setFillColour(colour
) 
1478             self
.drawPanel
.Refresh() 
1480             self
.fillColour 
= colour
 
1481             self
.optionIndicator
.setFillColour(colour
) 
1484     def _setLineSize(self
, size
): 
1485         """ Set the default or selected object's line size. 
1487         if len(self
.selection
) > 0: 
1488             self
._saveUndoInfo
() 
1489             for obj 
in self
.selection
: 
1490                 obj
.setLineSize(size
) 
1491             self
.drawPanel
.Refresh() 
1493             self
.lineSize 
= size
 
1494             self
.optionIndicator
.setLineSize(size
) 
1497     def _saveUndoInfo(self
): 
1498         """ Remember the current state of the document, to allow for undo. 
1500             We make a copy of the document's contents, so that we can return to 
1501             the previous contents if the user does something and then wants to 
1505         for obj 
in self
.contents
: 
1506             savedContents
.append([obj
.getType(), obj
.getData()]) 
1509         for i 
in range(len(self
.contents
)): 
1510             if self
.contents
[i
] in self
.selection
: 
1511                 savedSelection
.append(i
) 
1513         self
.undoInfo 
= {"contents"  : savedContents
, 
1514                          "selection" : savedSelection
} 
1517     def _resizeObject(self
, obj
, anchorPt
, oldPt
, newPt
): 
1518         """ Resize the given object. 
1520             'anchorPt' is the unchanging corner of the object, while the 
1521             opposite corner has been resized.  'oldPt' are the current 
1522             coordinates for this corner, while 'newPt' are the new coordinates. 
1523             The object should fit within the given dimensions, though if the 
1524             new point is less than the anchor point the object will need to be 
1525             moved as well as resized, to avoid giving it a negative size. 
1527         if obj
.getType() == obj_TEXT
: 
1528             # Not allowed to resize text objects -- they're sized to fit text. 
1532         self
._saveUndoInfo
() 
1534         topLeft  
= wxPoint(min(anchorPt
.x
, newPt
.x
), 
1535                            min(anchorPt
.y
, newPt
.y
)) 
1536         botRight 
= wxPoint(max(anchorPt
.x
, newPt
.x
), 
1537                            max(anchorPt
.y
, newPt
.y
)) 
1539         newWidth  
= botRight
.x 
- topLeft
.x
 
1540         newHeight 
= botRight
.y 
- topLeft
.y
 
1542         if obj
.getType() == obj_LINE
: 
1543             # Adjust the line so that its start and end points match the new 
1544             # overall object size. 
1546             startPt 
= obj
.getStartPt() 
1547             endPt   
= obj
.getEndPt() 
1549             slopesDown 
= ((startPt
.x 
< endPt
.x
) and (startPt
.y 
< endPt
.y
)) or \
 
1550                          ((startPt
.x 
> endPt
.x
) and (startPt
.y 
> endPt
.y
)) 
1552             # Handle the user flipping the line. 
1554             hFlip 
= ((anchorPt
.x 
< oldPt
.x
) and (anchorPt
.x 
> newPt
.x
)) or \
 
1555                     ((anchorPt
.x 
> oldPt
.x
) and (anchorPt
.x 
< newPt
.x
)) 
1556             vFlip 
= ((anchorPt
.y 
< oldPt
.y
) and (anchorPt
.y 
> newPt
.y
)) or \
 
1557                     ((anchorPt
.y 
> oldPt
.y
) and (anchorPt
.y 
< newPt
.y
)) 
1559             if (hFlip 
and not vFlip
) or (vFlip 
and not hFlip
): 
1560                 slopesDown 
= not slopesDown 
# Line flipped. 
1563                 obj
.setStartPt(wxPoint(0, 0)) 
1564                 obj
.setEndPt(wxPoint(newWidth
, newHeight
)) 
1566                 obj
.setStartPt(wxPoint(0, newHeight
)) 
1567                 obj
.setEndPt(wxPoint(newWidth
, 0)) 
1569         # Finally, adjust the bounds of the object to match the new dimensions. 
1571         obj
.setPosition(topLeft
) 
1572         obj
.setSize(wxSize(botRight
.x 
- topLeft
.x
, botRight
.y 
- topLeft
.y
)) 
1574         self
.drawPanel
.Refresh() 
1577     def _moveObject(self
, offsetX
, offsetY
): 
1578         """ Move the currently selected object(s) by the given offset. 
1580         self
._saveUndoInfo
() 
1582         for obj 
in self
.selection
: 
1583             pos 
= obj
.getPosition() 
1584             pos
.x 
= pos
.x 
+ offsetX
 
1585             pos
.y 
= pos
.y 
+ offsetY
 
1586             obj
.setPosition(pos
) 
1588         self
.drawPanel
.Refresh() 
1591     def _buildLineSizePopup(self
, lineSize
): 
1592         """ Build the pop-up menu used to set the line size. 
1594             'lineSize' is the current line size value.  The corresponding item 
1595             is checked in the pop-up menu. 
1598         menu
.Append(id_LINESIZE_0
, "no line",      kind
=wxITEM_CHECK
) 
1599         menu
.Append(id_LINESIZE_1
, "1-pixel line", kind
=wxITEM_CHECK
) 
1600         menu
.Append(id_LINESIZE_2
, "2-pixel line", kind
=wxITEM_CHECK
) 
1601         menu
.Append(id_LINESIZE_3
, "3-pixel line", kind
=wxITEM_CHECK
) 
1602         menu
.Append(id_LINESIZE_4
, "4-pixel line", kind
=wxITEM_CHECK
) 
1603         menu
.Append(id_LINESIZE_5
, "5-pixel line", kind
=wxITEM_CHECK
) 
1605         if   lineSize 
== 0: menu
.Check(id_LINESIZE_0
, True) 
1606         elif lineSize 
== 1: menu
.Check(id_LINESIZE_1
, True) 
1607         elif lineSize 
== 2: menu
.Check(id_LINESIZE_2
, True) 
1608         elif lineSize 
== 3: menu
.Check(id_LINESIZE_3
, True) 
1609         elif lineSize 
== 4: menu
.Check(id_LINESIZE_4
, True) 
1610         elif lineSize 
== 5: menu
.Check(id_LINESIZE_5
, True) 
1612         EVT_MENU(self
, id_LINESIZE_0
, self
._lineSizePopupSelected
) 
1613         EVT_MENU(self
, id_LINESIZE_1
, self
._lineSizePopupSelected
) 
1614         EVT_MENU(self
, id_LINESIZE_2
, self
._lineSizePopupSelected
) 
1615         EVT_MENU(self
, id_LINESIZE_3
, self
._lineSizePopupSelected
) 
1616         EVT_MENU(self
, id_LINESIZE_4
, self
._lineSizePopupSelected
) 
1617         EVT_MENU(self
, id_LINESIZE_5
, self
._lineSizePopupSelected
) 
1622     def _lineSizePopupSelected(self
, event
): 
1623         """ Respond to the user selecting an item from the line size popup menu 
1626         if   id == id_LINESIZE_0
: self
._setLineSize
(0) 
1627         elif id == id_LINESIZE_1
: self
._setLineSize
(1) 
1628         elif id == id_LINESIZE_2
: self
._setLineSize
(2) 
1629         elif id == id_LINESIZE_3
: self
._setLineSize
(3) 
1630         elif id == id_LINESIZE_4
: self
._setLineSize
(4) 
1631         elif id == id_LINESIZE_5
: self
._setLineSize
(5) 
1636         self
.optionIndicator
.setLineSize(self
.lineSize
) 
1639     def _getEventCoordinates(self
, event
): 
1640         """ Return the coordinates associated with the given mouse event. 
1642             The coordinates have to be adjusted to allow for the current scroll 
1645         originX
, originY 
= self
.drawPanel
.GetViewStart() 
1646         unitX
, unitY 
= self
.drawPanel
.GetScrollPixelsPerUnit() 
1647         return wxPoint(event
.GetX() + (originX 
* unitX
), 
1648                        event
.GetY() + (originY 
* unitY
)) 
1651     def _getObjectAndSelectionHandleAt(self
, pt
): 
1652         """ Return the object and selection handle at the given point. 
1654             We draw selection handles (small rectangles) around the currently 
1655             selected object(s).  If the given point is within one of the 
1656             selection handle rectangles, we return the associated object and a 
1657             code indicating which selection handle the point is in.  If the 
1658             point isn't within any selection handle at all, we return the tuple 
1659             (None, handle_NONE). 
1661         for obj 
in self
.selection
: 
1662             handle 
= obj
.getSelectionHandleContainingPoint(pt
.x
, pt
.y
) 
1663             if handle 
!= handle_NONE
: 
1666         return None, handle_NONE
 
1669     def _getObjectAt(self
, pt
): 
1670         """ Return the first object found which is at the given point. 
1672         for obj 
in self
.contents
: 
1673             if obj
.objectContainsPoint(pt
.x
, pt
.y
): 
1678     def _drawObjectOutline(self
, offsetX
, offsetY
): 
1679         """ Draw an outline of the currently selected object. 
1681             The selected object's outline is drawn at the object's position 
1682             plus the given offset. 
1684             Note that the outline is drawn by *inverting* the window's 
1685             contents, so calling _drawObjectOutline twice in succession will 
1686             restore the window's contents back to what they were previously. 
1688         if len(self
.selection
) != 1: return 
1690         position 
= self
.selection
[0].getPosition() 
1691         size     
= self
.selection
[0].getSize() 
1693         dc 
= wxClientDC(self
.drawPanel
) 
1694         self
.drawPanel
.PrepareDC(dc
) 
1696         dc
.SetPen(wxBLACK_DASHED_PEN
) 
1697         dc
.SetBrush(wxTRANSPARENT_BRUSH
) 
1698         dc
.SetLogicalFunction(wxINVERT
) 
1700         dc
.DrawRectangle(position
.x 
+ offsetX
, position
.y 
+ offsetY
, 
1701                          size
.width
, size
.height
) 
1706     def _drawVisualFeedback(self
, startPt
, endPt
, type, dashedLine
): 
1707         """ Draw visual feedback for a drawing operation. 
1709             The visual feedback consists of a line, ellipse, or rectangle based 
1710             around the two given points.  'type' should be one of the following 
1711             predefined feedback type constants: 
1713                 feedback_RECT     ->  draw rectangular feedback. 
1714                 feedback_LINE     ->  draw line feedback. 
1715                 feedback_ELLIPSE  ->  draw elliptical feedback. 
1717             if 'dashedLine' is True, the feedback is drawn as a dashed rather 
1720             Note that the feedback is drawn by *inverting* the window's 
1721             contents, so calling _drawVisualFeedback twice in succession will 
1722             restore the window's contents back to what they were previously. 
1724         dc 
= wxClientDC(self
.drawPanel
) 
1725         self
.drawPanel
.PrepareDC(dc
) 
1728             dc
.SetPen(wxBLACK_DASHED_PEN
) 
1730             dc
.SetPen(wxBLACK_PEN
) 
1731         dc
.SetBrush(wxTRANSPARENT_BRUSH
) 
1732         dc
.SetLogicalFunction(wxINVERT
) 
1734         if type == feedback_RECT
: 
1735             dc
.DrawRectangle(startPt
.x
, startPt
.y
, 
1736                              endPt
.x 
- startPt
.x
, 
1737                              endPt
.y 
- startPt
.y
) 
1738         elif type == feedback_LINE
: 
1739             dc
.DrawLine(startPt
.x
, startPt
.y
, endPt
.x
, endPt
.y
) 
1740         elif type == feedback_ELLIPSE
: 
1741             dc
.DrawEllipse(startPt
.x
, startPt
.y
, 
1742                            endPt
.x 
- startPt
.x
, 
1743                            endPt
.y 
- startPt
.y
) 
1747 #---------------------------------------------------------------------------- 
1749 class DrawingObject
: 
1750     """ An object within the drawing panel. 
1752         A pySketch document consists of a front-to-back ordered list of 
1753         DrawingObjects.  Each DrawingObject has the following properties: 
1755             'type'          What type of object this is (text, line, etc). 
1756             'position'      The position of the object within the document. 
1757             'size'          The size of the object within the document. 
1758             'penColour'     The colour to use for drawing the object's outline. 
1759             'fillColour'    Colour to use for drawing object's interior. 
1760             'lineSize'      Line width (in pixels) to use for object's outline. 
1761             'startPt'       The point, relative to the object's position, where 
1762                             an obj_LINE object's line should start. 
1763             'endPt'         The point, relative to the object's position, where 
1764                             an obj_LINE object's line should end. 
1765             'text'          The object's text (obj_TEXT objects only). 
1766             'textFont'      The text object's font name. 
1767             'textSize'      The text object's point size. 
1768             'textBoldface'  If True, this text object will be drawn in 
1770             'textItalic'    If True, this text object will be drawn in italic. 
1771             'textUnderline' If True, this text object will be drawn underlined. 
1774     # ================== 
1775     # == Constructors == 
1776     # ================== 
1778     def __init__(self
, type, position
=wxPoint(0, 0), size
=wxSize(0, 0), 
1779                  penColour
=wxBLACK
, fillColour
=wxWHITE
, lineSize
=1, 
1780                  text
=None, startPt
=wxPoint(0, 0), endPt
=wxPoint(0,0)): 
1781         """ Standard constructor. 
1783             'type' is the type of object being created.  This should be one of 
1784             the following constants: 
1791             The remaining parameters let you set various options for the newly 
1792             created DrawingObject. 
1795         self
.position          
= position
 
1797         self
.penColour         
= penColour
 
1798         self
.fillColour        
= fillColour
 
1799         self
.lineSize          
= lineSize
 
1800         self
.startPt           
= startPt
 
1803         self
.textFont          
= wxSystemSettings_GetSystemFont( 
1804                                     wxSYS_DEFAULT_GUI_FONT
).GetFaceName() 
1806         self
.textBoldface      
= False 
1807         self
.textItalic        
= False 
1808         self
.textUnderline     
= False 
1810     # ============================= 
1811     # == Object Property Methods == 
1812     # ============================= 
1815         """ Return a copy of the object's internal data. 
1817             This is used to save this DrawingObject to disk. 
1819         return [self
.type, self
.position
.x
, self
.position
.y
, 
1820                 self
.size
.width
, self
.size
.height
, 
1821                 self
.penColour
.Red(), 
1822                 self
.penColour
.Green(), 
1823                 self
.penColour
.Blue(), 
1824                 self
.fillColour
.Red(), 
1825                 self
.fillColour
.Green(), 
1826                 self
.fillColour
.Blue(), 
1828                 self
.startPt
.x
, self
.startPt
.y
, 
1829                 self
.endPt
.x
, self
.endPt
.y
, 
1838     def setData(self
, data
): 
1839         """ Set the object's internal data. 
1841             'data' is a copy of the object's saved data, as returned by 
1842             getData() above.  This is used to restore a previously saved 
1845         #data = copy.deepcopy(data) # Needed? 
1848         self
.position          
= wxPoint(data
[1], data
[2]) 
1849         self
.size              
= wxSize(data
[3], data
[4]) 
1850         self
.penColour         
= wxColour(red
=data
[5], 
1853         self
.fillColour        
= wxColour(red
=data
[8], 
1856         self
.lineSize          
= data
[11] 
1857         self
.startPt           
= wxPoint(data
[12], data
[13]) 
1858         self
.endPt             
= wxPoint(data
[14], data
[15]) 
1859         self
.text              
= data
[16] 
1860         self
.textFont          
= data
[17] 
1861         self
.textSize          
= data
[18] 
1862         self
.textBoldface      
= data
[19] 
1863         self
.textItalic        
= data
[20] 
1864         self
.textUnderline     
= data
[21] 
1868         """ Return this DrawingObject's type. 
1873     def setPosition(self
, position
): 
1874         """ Set the origin (top-left corner) for this DrawingObject. 
1876         self
.position 
= position
 
1879     def getPosition(self
): 
1880         """ Return this DrawingObject's position. 
1882         return self
.position
 
1885     def setSize(self
, size
): 
1886         """ Set the size for this DrawingObject. 
1892         """ Return this DrawingObject's size. 
1897     def setPenColour(self
, colour
): 
1898         """ Set the pen colour used for this DrawingObject. 
1900         self
.penColour 
= colour
 
1903     def getPenColour(self
): 
1904         """ Return this DrawingObject's pen colour. 
1906         return self
.penColour
 
1909     def setFillColour(self
, colour
): 
1910         """ Set the fill colour used for this DrawingObject. 
1912         self
.fillColour 
= colour
 
1915     def getFillColour(self
): 
1916         """ Return this DrawingObject's fill colour. 
1918         return self
.fillColour
 
1921     def setLineSize(self
, lineSize
): 
1922         """ Set the linesize used for this DrawingObject. 
1924         self
.lineSize 
= lineSize
 
1927     def getLineSize(self
): 
1928         """ Return this DrawingObject's line size. 
1930         return self
.lineSize
 
1933     def setStartPt(self
, startPt
): 
1934         """ Set the starting point for this line DrawingObject. 
1936         self
.startPt 
= startPt
 
1939     def getStartPt(self
): 
1940         """ Return the starting point for this line DrawingObject. 
1945     def setEndPt(self
, endPt
): 
1946         """ Set the ending point for this line DrawingObject. 
1952         """ Return the ending point for this line DrawingObject. 
1957     def setText(self
, text
): 
1958         """ Set the text for this DrawingObject. 
1964         """ Return this DrawingObject's text. 
1969     def setTextFont(self
, font
): 
1970         """ Set the typeface for this text DrawingObject. 
1972         self
.textFont 
= font
 
1975     def getTextFont(self
): 
1976         """ Return this text DrawingObject's typeface. 
1978         return self
.textFont
 
1981     def setTextSize(self
, size
): 
1982         """ Set the point size for this text DrawingObject. 
1984         self
.textSize 
= size
 
1987     def getTextSize(self
): 
1988         """ Return this text DrawingObject's text size. 
1990         return self
.textSize
 
1993     def setTextBoldface(self
, boldface
): 
1994         """ Set the boldface flag for this text DrawingObject. 
1996         self
.textBoldface 
= boldface
 
1999     def getTextBoldface(self
): 
2000         """ Return this text DrawingObject's boldface flag. 
2002         return self
.textBoldface
 
2005     def setTextItalic(self
, italic
): 
2006         """ Set the italic flag for this text DrawingObject. 
2008         self
.textItalic 
= italic
 
2011     def getTextItalic(self
): 
2012         """ Return this text DrawingObject's italic flag. 
2014         return self
.textItalic
 
2017     def setTextUnderline(self
, underline
): 
2018         """ Set the underling flag for this text DrawingObject. 
2020         self
.textUnderline 
= underline
 
2023     def getTextUnderline(self
): 
2024         """ Return this text DrawingObject's underline flag. 
2026         return self
.textUnderline
 
2028     # ============================ 
2029     # == Object Drawing Methods == 
2030     # ============================ 
2032     def draw(self
, dc
, selected
): 
2033         """ Draw this DrawingObject into our window. 
2035             'dc' is the device context to use for drawing.  If 'selected' is 
2036             True, the object is currently selected and should be drawn as such. 
2038         if self
.type != obj_TEXT
: 
2039             if self
.lineSize 
== 0: 
2040                 dc
.SetPen(wxPen(self
.penColour
, self
.lineSize
, wxTRANSPARENT
)) 
2042                 dc
.SetPen(wxPen(self
.penColour
, self
.lineSize
, wxSOLID
)) 
2043             dc
.SetBrush(wxBrush(self
.fillColour
, wxSOLID
)) 
2045             dc
.SetTextForeground(self
.penColour
) 
2046             dc
.SetTextBackground(self
.fillColour
) 
2048         self
._privateDraw
(dc
, self
.position
, selected
) 
2050     # ======================= 
2051     # == Selection Methods == 
2052     # ======================= 
2054     def objectContainsPoint(self
, x
, y
): 
2055         """ Returns True iff this object contains the given point. 
2057             This is used to determine if the user clicked on the object. 
2059         # Firstly, ignore any points outside of the object's bounds. 
2061         if x 
< self
.position
.x
: return False 
2062         if x 
> self
.position
.x 
+ self
.size
.x
: return False 
2063         if y 
< self
.position
.y
: return False 
2064         if y 
> self
.position
.y 
+ self
.size
.y
: return False 
2066         if self
.type in [obj_RECT
, obj_TEXT
]: 
2067             # Rectangles and text are easy -- they're always selected if the 
2068             # point is within their bounds. 
2071         # Now things get tricky.  There's no straightforward way of knowing 
2072         # whether the point is within the object's bounds...to get around this, 
2073         # we draw the object into a memory-based bitmap and see if the given 
2074         # point was drawn.  This could no doubt be done more efficiently by 
2075         # some tricky maths, but this approach works and is simple enough. 
2077         bitmap 
= wxEmptyBitmap(self
.size
.x 
+ 10, self
.size
.y 
+ 10) 
2079         dc
.SelectObject(bitmap
) 
2081         dc
.SetBackground(wxWHITE_BRUSH
) 
2083         dc
.SetPen(wxPen(wxBLACK
, self
.lineSize 
+ 5, wxSOLID
)) 
2084         dc
.SetBrush(wxBLACK_BRUSH
) 
2085         self
._privateDraw
(dc
, wxPoint(5, 5), True) 
2087         pixel 
= dc
.GetPixel(x 
- self
.position
.x 
+ 5, y 
- self
.position
.y 
+ 5) 
2088         if (pixel
.Red() == 0) and (pixel
.Green() == 0) and (pixel
.Blue() == 0): 
2094     def getSelectionHandleContainingPoint(self
, x
, y
): 
2095         """ Return the selection handle containing the given point, if any. 
2097             We return one of the predefined selection handle ID codes. 
2099         if self
.type == obj_LINE
: 
2100             # We have selection handles at the start and end points. 
2101             if self
._pointInSelRect
(x
, y
, self
.position
.x 
+ self
.startPt
.x
, 
2102                                           self
.position
.y 
+ self
.startPt
.y
): 
2103                 return handle_START_POINT
 
2104             elif self
._pointInSelRect
(x
, y
, self
.position
.x 
+ self
.endPt
.x
, 
2105                                             self
.position
.y 
+ self
.endPt
.y
): 
2106                 return handle_END_POINT
 
2110             # We have selection handles at all four corners. 
2111             if self
._pointInSelRect
(x
, y
, self
.position
.x
, self
.position
.y
): 
2112                 return handle_TOP_LEFT
 
2113             elif self
._pointInSelRect
(x
, y
, self
.position
.x 
+ self
.size
.width
, 
2115                 return handle_TOP_RIGHT
 
2116             elif self
._pointInSelRect
(x
, y
, self
.position
.x
, 
2117                                             self
.position
.y 
+ self
.size
.height
): 
2118                 return handle_BOTTOM_LEFT
 
2119             elif self
._pointInSelRect
(x
, y
, self
.position
.x 
+ self
.size
.width
, 
2120                                             self
.position
.y 
+ self
.size
.height
): 
2121                 return handle_BOTTOM_RIGHT
 
2126     def objectWithinRect(self
, x
, y
, width
, height
): 
2127         """ Return True iff this object falls completely within the given rect. 
2129         if x          
> self
.position
.x
:                    return False 
2130         if x 
+ width  
< self
.position
.x 
+ self
.size
.width
:  return False 
2131         if y          
> self
.position
.y
:                    return False 
2132         if y 
+ height 
< self
.position
.y 
+ self
.size
.height
: return False 
2135     # ===================== 
2136     # == Utility Methods == 
2137     # ===================== 
2139     def fitToText(self
): 
2140         """ Resize a text DrawingObject so that it fits it's text exactly. 
2142         if self
.type != obj_TEXT
: return 
2144         if self
.textBoldface
: weight 
= wxBOLD
 
2145         else:                 weight 
= wxNORMAL
 
2146         if self
.textItalic
: style 
= wxITALIC
 
2147         else:               style 
= wxNORMAL
 
2148         font 
= wxFont(self
.textSize
, wxDEFAULT
, style
, weight
, 
2149                       self
.textUnderline
, self
.textFont
) 
2151         dummyWindow 
= wxFrame(None, -1, "") 
2152         dummyWindow
.SetFont(font
) 
2153         width
, height 
= dummyWindow
.GetTextExtent(self
.text
) 
2154         dummyWindow
.Destroy() 
2156         self
.size 
= wxSize(width
, height
) 
2158     # ===================== 
2159     # == Private Methods == 
2160     # ===================== 
2162     def _privateDraw(self
, dc
, position
, selected
): 
2163         """ Private routine to draw this DrawingObject. 
2165             'dc' is the device context to use for drawing, while 'position' is 
2166             the position in which to draw the object.  If 'selected' is True, 
2167             the object is drawn with selection handles.  This private drawing 
2168             routine assumes that the pen and brush have already been set by the 
2171         if self
.type == obj_LINE
: 
2172             dc
.DrawLine(position
.x 
+ self
.startPt
.x
, 
2173                         position
.y 
+ self
.startPt
.y
, 
2174                         position
.x 
+ self
.endPt
.x
, 
2175                         position
.y 
+ self
.endPt
.y
) 
2176         elif self
.type == obj_RECT
: 
2177             dc
.DrawRectangle(position
.x
, position
.y
, 
2178                              self
.size
.width
, self
.size
.height
) 
2179         elif self
.type == obj_ELLIPSE
: 
2180             dc
.DrawEllipse(position
.x
, position
.y
, 
2181                            self
.size
.width
, self
.size
.height
) 
2182         elif self
.type == obj_TEXT
: 
2183             if self
.textBoldface
: weight 
= wxBOLD
 
2184             else:                 weight 
= wxNORMAL
 
2185             if self
.textItalic
: style 
= wxITALIC
 
2186             else:               style 
= wxNORMAL
 
2187             font 
= wxFont(self
.textSize
, wxDEFAULT
, style
, weight
, 
2188                           self
.textUnderline
, self
.textFont
) 
2190             dc
.DrawText(self
.text
, position
.x
, position
.y
) 
2193             dc
.SetPen(wxTRANSPARENT_PEN
) 
2194             dc
.SetBrush(wxBLACK_BRUSH
) 
2196             if self
.type == obj_LINE
: 
2197                 # Draw selection handles at the start and end points. 
2198                 self
._drawSelHandle
(dc
, position
.x 
+ self
.startPt
.x
, 
2199                                         position
.y 
+ self
.startPt
.y
) 
2200                 self
._drawSelHandle
(dc
, position
.x 
+ self
.endPt
.x
, 
2201                                         position
.y 
+ self
.endPt
.y
) 
2203                 # Draw selection handles at all four corners. 
2204                 self
._drawSelHandle
(dc
, position
.x
, position
.y
) 
2205                 self
._drawSelHandle
(dc
, position
.x 
+ self
.size
.width
, 
2207                 self
._drawSelHandle
(dc
, position
.x
, 
2208                                         position
.y 
+ self
.size
.height
) 
2209                 self
._drawSelHandle
(dc
, position
.x 
+ self
.size
.width
, 
2210                                         position
.y 
+ self
.size
.height
) 
2213     def _drawSelHandle(self
, dc
, x
, y
): 
2214         """ Draw a selection handle around this DrawingObject. 
2216             'dc' is the device context to draw the selection handle within, 
2217             while 'x' and 'y' are the coordinates to use for the centre of the 
2220         dc
.DrawRectangle(x 
- 3, y 
- 3, 6, 6) 
2223     def _pointInSelRect(self
, x
, y
, rX
, rY
): 
2224         """ Return True iff (x, y) is within the selection handle at (rX, ry). 
2226         if   x 
< rX 
- 3: return False 
2227         elif x 
> rX 
+ 3: return False 
2228         elif y 
< rY 
- 3: return False 
2229         elif y 
> rY 
+ 3: return False 
2232 #---------------------------------------------------------------------------- 
2234 class ToolPaletteIcon(wxStaticBitmap
): 
2235     """ An icon appearing in the tool palette area of our sketching window. 
2237         Note that this is actually implemented as a wxStaticBitmap rather 
2238         than as a wxIcon.  wxIcon has a very specific meaning, and isn't 
2239         appropriate for this more general use. 
2242     def __init__(self
, parent
, iconID
, iconName
, toolTip
): 
2243         """ Standard constructor. 
2245             'parent'   is the parent window this icon will be part of. 
2246             'iconID'   is the internal ID used for this icon. 
2247             'iconName' is the name used for this icon. 
2248             'toolTip'  is the tool tip text to show for this icon. 
2250             The icon name is used to get the appropriate bitmap for this icon. 
2252         bmp 
= wxBitmap("images/" + iconName 
+ "Icon.bmp", wxBITMAP_TYPE_BMP
) 
2253         wxStaticBitmap
.__init
__(self
, parent
, iconID
, bmp
, wxDefaultPosition
, 
2254                                 wxSize(bmp
.GetWidth(), bmp
.GetHeight())) 
2255         self
.SetToolTip(wxToolTip(toolTip
)) 
2257         self
.iconID     
= iconID
 
2258         self
.iconName   
= iconName
 
2259         self
.isSelected 
= False 
2263         """ Select the icon. 
2265             The icon's visual representation is updated appropriately. 
2267         if self
.isSelected
: return # Nothing to do! 
2269         bmp 
= wxBitmap("images/" + self
.iconName 
+ "IconSel.bmp", 
2272         self
.isSelected 
= True 
2276         """ Deselect the icon. 
2278             The icon's visual representation is updated appropriately. 
2280         if not self
.isSelected
: return # Nothing to do! 
2282         bmp 
= wxBitmap("images/" + self
.iconName 
+ "Icon.bmp", 
2285         self
.isSelected 
= False 
2287 #---------------------------------------------------------------------------- 
2289 class ToolOptionIndicator(wxWindow
): 
2290     """ A visual indicator which shows the current tool options. 
2292     def __init__(self
, parent
): 
2293         """ Standard constructor. 
2295         wxWindow
.__init
__(self
, parent
, -1, wxDefaultPosition
, wxSize(52, 32)) 
2297         self
.penColour  
= wxBLACK
 
2298         self
.fillColour 
= wxWHITE
 
2301         EVT_PAINT(self
, self
.OnPaint
) 
2304     def setPenColour(self
, penColour
): 
2305         """ Set the indicator's current pen colour. 
2307         self
.penColour 
= penColour
 
2311     def setFillColour(self
, fillColour
): 
2312         """ Set the indicator's current fill colour. 
2314         self
.fillColour 
= fillColour
 
2318     def setLineSize(self
, lineSize
): 
2319         """ Set the indicator's current pen colour. 
2321         self
.lineSize 
= lineSize
 
2325     def OnPaint(self
, event
): 
2326         """ Paint our tool option indicator. 
2328         dc 
= wxPaintDC(self
) 
2331         if self
.lineSize 
== 0: 
2332             dc
.SetPen(wxPen(self
.penColour
, self
.lineSize
, wxTRANSPARENT
)) 
2334             dc
.SetPen(wxPen(self
.penColour
, self
.lineSize
, wxSOLID
)) 
2335         dc
.SetBrush(wxBrush(self
.fillColour
, wxSOLID
)) 
2337         dc
.DrawRectangle(5, 5, self
.GetSize().width 
- 10, 
2338                                self
.GetSize().height 
- 10) 
2342 #---------------------------------------------------------------------------- 
2344 class EditTextObjectDialog(wxDialog
): 
2345     """ Dialog box used to edit the properties of a text object. 
2347         The user can edit the object's text, font, size, and text style. 
2350     def __init__(self
, parent
, title
): 
2351         """ Standard constructor. 
2353         wxDialog
.__init
__(self
, parent
, -1, title
) 
2355         self
.textCtrl 
= wxTextCtrl(self
, 1001, "", style
=wxTE_PROCESS_ENTER
, 
2356                                    validator
=TextObjectValidator()) 
2357         extent 
= self
.textCtrl
.GetFullTextExtent("Hy") 
2358         lineHeight 
= extent
[1] + extent
[3] 
2359         self
.textCtrl
.SetSize(wxSize(-1, lineHeight 
* 4)) 
2361         EVT_TEXT_ENTER(self
, 1001, self
._doEnter
) 
2363         fonts 
= wxFontEnumerator() 
2364         fonts
.EnumerateFacenames() 
2365         self
.fontList 
= fonts
.GetFacenames() 
2366         self
.fontList
.sort() 
2368         fontLabel 
= wxStaticText(self
, -1, "Font:") 
2369         self
._setFontOptions
(fontLabel
, weight
=wxBOLD
) 
2371         self
.fontCombo 
= wxComboBox(self
, -1, "", wxDefaultPosition
, 
2372                                     wxDefaultSize
, self
.fontList
, 
2373                                     style 
= wxCB_READONLY
) 
2374         self
.fontCombo
.SetSelection(0) # Default to first available font. 
2376         self
.sizeList 
= ["8", "9", "10", "12", "14", "16", 
2377                          "18", "20", "24", "32", "48", "72"] 
2379         sizeLabel 
= wxStaticText(self
, -1, "Size:") 
2380         self
._setFontOptions
(sizeLabel
, weight
=wxBOLD
) 
2382         self
.sizeCombo 
= wxComboBox(self
, -1, "", wxDefaultPosition
, 
2383                                     wxDefaultSize
, self
.sizeList
, 
2384                                     style
=wxCB_READONLY
) 
2385         self
.sizeCombo
.SetSelection(3) # Default to 12 point text. 
2387         gap 
= wxLEFT | wxTOP | wxRIGHT
 
2389         comboSizer 
= wxBoxSizer(wxHORIZONTAL
) 
2390         comboSizer
.Add(fontLabel
,      0, gap | wxALIGN_CENTRE_VERTICAL
, 5) 
2391         comboSizer
.Add(self
.fontCombo
, 0, gap
, 5) 
2392         comboSizer
.Add(5, 5) # Spacer. 
2393         comboSizer
.Add(sizeLabel
,      0, gap | wxALIGN_CENTRE_VERTICAL
, 5) 
2394         comboSizer
.Add(self
.sizeCombo
, 0, gap
, 5) 
2396         self
.boldCheckbox      
= wxCheckBox(self
, -1, "Bold") 
2397         self
.italicCheckbox    
= wxCheckBox(self
, -1, "Italic") 
2398         self
.underlineCheckbox 
= wxCheckBox(self
, -1, "Underline") 
2400         self
._setFontOptions
(self
.boldCheckbox
,      weight
=wxBOLD
) 
2401         self
._setFontOptions
(self
.italicCheckbox
,    style
=wxITALIC
) 
2402         self
._setFontOptions
(self
.underlineCheckbox
, underline
=True) 
2404         styleSizer 
= wxBoxSizer(wxHORIZONTAL
) 
2405         styleSizer
.Add(self
.boldCheckbox
,      0, gap
, 5) 
2406         styleSizer
.Add(self
.italicCheckbox
,    0, gap
, 5) 
2407         styleSizer
.Add(self
.underlineCheckbox
, 0, gap
, 5) 
2409         self
.okButton     
= wxButton(self
, wxID_OK
,     "OK") 
2410         self
.cancelButton 
= wxButton(self
, wxID_CANCEL
, "Cancel") 
2412         btnSizer 
= wxBoxSizer(wxHORIZONTAL
) 
2413         btnSizer
.Add(self
.okButton
,     0, gap
, 5) 
2414         btnSizer
.Add(self
.cancelButton
, 0, gap
, 5) 
2416         sizer 
= wxBoxSizer(wxVERTICAL
) 
2417         sizer
.Add(self
.textCtrl
, 1, gap | wxEXPAND
,       5) 
2418         sizer
.Add(10, 10) # Spacer. 
2419         sizer
.Add(comboSizer
,    0, gap | wxALIGN_CENTRE
, 5) 
2420         sizer
.Add(styleSizer
,    0, gap | wxALIGN_CENTRE
, 5) 
2421         sizer
.Add(10, 10) # Spacer. 
2422         sizer
.Add(btnSizer
,      0, gap | wxALIGN_CENTRE
, 5) 
2424         self
.SetAutoLayout(True) 
2425         self
.SetSizer(sizer
) 
2428         self
.textCtrl
.SetFocus() 
2431     def objectToDialog(self
, obj
): 
2432         """ Copy the properties of the given text object into the dialog box. 
2434         self
.textCtrl
.SetValue(obj
.getText()) 
2435         self
.textCtrl
.SetSelection(0, len(obj
.getText())) 
2437         for i 
in range(len(self
.fontList
)): 
2438             if self
.fontList
[i
] == obj
.getTextFont(): 
2439                 self
.fontCombo
.SetSelection(i
) 
2442         for i 
in range(len(self
.sizeList
)): 
2443             if self
.sizeList
[i
] == str(obj
.getTextSize()): 
2444                 self
.sizeCombo
.SetSelection(i
) 
2447         self
.boldCheckbox
.SetValue(obj
.getTextBoldface()) 
2448         self
.italicCheckbox
.SetValue(obj
.getTextItalic()) 
2449         self
.underlineCheckbox
.SetValue(obj
.getTextUnderline()) 
2452     def dialogToObject(self
, obj
): 
2453         """ Copy the properties from the dialog box into the given text object. 
2455         obj
.setText(self
.textCtrl
.GetValue()) 
2456         obj
.setTextFont(self
.fontCombo
.GetValue()) 
2457         obj
.setTextSize(int(self
.sizeCombo
.GetValue())) 
2458         obj
.setTextBoldface(self
.boldCheckbox
.GetValue()) 
2459         obj
.setTextItalic(self
.italicCheckbox
.GetValue()) 
2460         obj
.setTextUnderline(self
.underlineCheckbox
.GetValue()) 
2463     # ====================== 
2464     # == Private Routines == 
2465     # ====================== 
2467     def _setFontOptions(self
, ctrl
, family
=None, pointSize
=-1, 
2468                                     style
=wxNORMAL
, weight
=wxNORMAL
, 
2470         """ Change the font settings for the given control. 
2472             The meaning of the 'family', 'pointSize', 'style', 'weight' and 
2473             'underline' parameters are the same as for the wxFont constructor. 
2474             If the family and/or pointSize isn't specified, the current default 
2477         if family 
== None: family 
= ctrl
.GetFont().GetFamily() 
2478         if pointSize 
== -1: pointSize 
= ctrl
.GetFont().GetPointSize() 
2480         ctrl
.SetFont(wxFont(pointSize
, family
, style
, weight
, underline
)) 
2481         ctrl
.SetSize(ctrl
.GetBestSize()) # Adjust size to reflect font change. 
2484     def _doEnter(self
, event
): 
2485         """ Respond to the user hitting the ENTER key. 
2487             We simulate clicking on the "OK" button. 
2489         if self
.Validate(): self
.Show(False) 
2491 #---------------------------------------------------------------------------- 
2493 class TextObjectValidator(wxPyValidator
): 
2494     """ This validator is used to ensure that the user has entered something 
2495         into the text object editor dialog's text field. 
2498         """ Standard constructor. 
2500         wxPyValidator
.__init
__(self
) 
2504         """ Standard cloner. 
2506             Note that every validator must implement the Clone() method. 
2508         return TextObjectValidator() 
2511     def Validate(self
, win
): 
2512         """ Validate the contents of the given text control. 
2514         textCtrl 
= wxPyTypeCast(self
.GetWindow(), "wxTextCtrl") 
2515         text 
= textCtrl
.GetValue() 
2518             wxMessageBox("A text object must contain some text!", "Error") 
2524     def TransferToWindow(self
): 
2525         """ Transfer data from validator to window. 
2527             The default implementation returns False, indicating that an error 
2528             occurred.  We simply return True, as we don't do any data transfer. 
2530         return True # Prevent wxDialog from complaining. 
2533     def TransferFromWindow(self
): 
2534         """ Transfer data from window to validator. 
2536             The default implementation returns False, indicating that an error 
2537             occurred.  We simply return True, as we don't do any data transfer. 
2539         return True # Prevent wxDialog from complaining. 
2541 #---------------------------------------------------------------------------- 
2543 class ExceptionHandler
: 
2544     """ A simple error-handling class to write exceptions to a text file. 
2546         Under MS Windows, the standard DOS console window doesn't scroll and 
2547         closes as soon as the application exits, making it hard to find and 
2548         view Python exceptions.  This utility class allows you to handle Python 
2549         exceptions in a more friendly manner. 
2553         """ Standard constructor. 
2556         if os
.path
.exists("errors.txt"): 
2557             os
.remove("errors.txt") # Delete previous error log, if any. 
2561         """ Write the given error message to a text file. 
2563             Note that if the error message doesn't end in a carriage return, we 
2564             have to buffer up the inputs until a carriage return is received. 
2566         if (s
[-1] != "\n") and (s
[-1] != "\r"): 
2567             self
._buff 
= self
._buff 
+ s
 
2574             if s
[:9] == "Traceback": 
2575                 # Tell the user than an exception occurred. 
2576                 wxMessageBox("An internal error has occurred.\nPlease " + \
 
2577                              "refer to the 'errors.txt' file for details.", 
2578                              "Error", wxOK | wxCENTRE | wxICON_EXCLAMATION
) 
2580             f 
= open("errors.txt", "a") 
2584             pass # Don't recursively crash on errors. 
2586 #---------------------------------------------------------------------------- 
2588 class SketchApp(wxApp
): 
2589     """ The main pySketch application object. 
2592         """ Initialise the application. 
2594         wxInitAllImageHandlers() 
2599         if len(sys
.argv
) == 1: 
2600             # No file name was specified on the command line -> start with a 
2602             frame 
= DrawingFrame(None, -1, "Untitled") 
2605             _docList
.append(frame
) 
2607             # Load the file(s) specified on the command line. 
2608             for arg 
in sys
.argv
[1:]: 
2609                 fileName 
= os
.path
.join(os
.getcwd(), arg
) 
2610                 if os
.path
.isfile(fileName
): 
2611                     frame 
= DrawingFrame(None, -1, 
2612                                          os
.path
.basename(fileName
), 
2615                     _docList
.append(frame
) 
2619 #---------------------------------------------------------------------------- 
2622     """ Start up the pySketch application. 
2626     # Redirect python exceptions to a log file. 
2628     sys
.stderr 
= ExceptionHandler() 
2630     # Create and start the pySketch application. 
2636 if __name__ 
== "__main__":