3     A simple object-oriented drawing program. 
   5     This is completely free software; please feel free to adapt or use this in 
   8     Author: Erik Westra (ewestra@wave.co.nz) 
  10     ######################################################################### 
  14     pySketch requires wxPython version 2.3.  If you are running an earlier 
  15     version, you need to patch your copy of wxPython to fix a bug which will 
  16     cause the "Edit Text Object" dialog box to crash. 
  18     To patch an earlier version of wxPython, edit the wxPython/windows.py file, 
  19     find the wxPyValidator.__init__ method and change the line which reads: 
  21         self._setSelf(self, wxPyValidator, 0) 
  25         self._setSelf(self, wxPyValidator, 1) 
  27     This fixes a known bug in wxPython 2.2.5 (and possibly earlier) which has 
  28     now been fixed in wxPython 2.3. 
  30     ######################################################################### 
  34       * Add ARGV checking to see if a document was double-clicked on. 
  38       * Scrolling the window causes the drawing panel to be mucked up until you 
  39         refresh it.  I've got no idea why. 
  41       * I suspect that the reference counting for some wxPoint objects is 
  42         getting mucked up; when the user quits, we get errors about being 
  43         unable to call del on a 'None' object. 
  46 import cPickle
, os
.path
 
  49 import traceback
, types
 
  51 #---------------------------------------------------------------------------- 
  53 #---------------------------------------------------------------------------- 
  57 menu_UNDO          
= 10001 # Edit menu items. 
  58 menu_SELECT_ALL    
= 10002 
  59 menu_DUPLICATE     
= 10003 
  60 menu_EDIT_TEXT     
= 10004 
  63 menu_SELECT        
= 10101 # Tools menu items. 
  69 menu_MOVE_FORWARD  
= 10201 # Object menu items. 
  70 menu_MOVE_TO_FRONT 
= 10202 
  71 menu_MOVE_BACKWARD 
= 10203 
  72 menu_MOVE_TO_BACK  
= 10204 
  74 menu_ABOUT         
= 10205 # Help menu items. 
  84 # Our tool option IDs: 
  97 # DrawObject type IDs: 
 104 # Selection handle IDs: 
 109 handle_BOTTOM_LEFT  
= 4 
 110 handle_BOTTOM_RIGHT 
= 5 
 111 handle_START_POINT  
= 6 
 114 # Dragging operations: 
 121 # Visual Feedback types: 
 127 # Mouse-event action parameter types: 
 132 # Size of the drawing page, in pixels. 
 137 #---------------------------------------------------------------------------- 
 139 class DrawingFrame(wx
.Frame
): 
 140     """ A frame showing the contents of a single document. """ 
 142     # ========================================== 
 143     # == Initialisation and Window Management == 
 144     # ========================================== 
 146     def __init__(self
, parent
, id, title
, fileName
=None): 
 147         """ Standard constructor. 
 149             'parent', 'id' and 'title' are all passed to the standard wx.Frame 
 150             constructor.  'fileName' is the name and path of a saved file to 
 151             load into this frame, if any. 
 153         wx
.Frame
.__init
__(self
, parent
, id, title
, 
 154                          style 
= wx
.DEFAULT_FRAME_STYLE | wx
.WANTS_CHARS |
 
 155                                  wx
.NO_FULL_REPAINT_ON_RESIZE
) 
 157         # Setup our menu bar. 
 159         menuBar 
= wx
.MenuBar() 
 161         self
.fileMenu 
= wx
.Menu() 
 162         self
.fileMenu
.Append(wx
.ID_NEW
,    "New\tCTRL-N") 
 163         self
.fileMenu
.Append(wx
.ID_OPEN
,   "Open...\tCTRL-O") 
 164         self
.fileMenu
.Append(wx
.ID_CLOSE
,  "Close\tCTRL-W") 
 165         self
.fileMenu
.AppendSeparator() 
 166         self
.fileMenu
.Append(wx
.ID_SAVE
,   "Save\tCTRL-S") 
 167         self
.fileMenu
.Append(wx
.ID_SAVEAS
, "Save As...") 
 168         self
.fileMenu
.Append(wx
.ID_REVERT
, "Revert...") 
 169         self
.fileMenu
.AppendSeparator() 
 170         self
.fileMenu
.Append(wx
.ID_EXIT
,   "Quit\tCTRL-Q") 
 172         menuBar
.Append(self
.fileMenu
, "File") 
 174         self
.editMenu 
= wx
.Menu() 
 175         self
.editMenu
.Append(menu_UNDO
,          "Undo\tCTRL-Z") 
 176         self
.editMenu
.AppendSeparator() 
 177         self
.editMenu
.Append(menu_SELECT_ALL
,    "Select All\tCTRL-A") 
 178         self
.editMenu
.AppendSeparator() 
 179         self
.editMenu
.Append(menu_DUPLICATE
,     "Duplicate\tCTRL-D") 
 180         self
.editMenu
.Append(menu_EDIT_TEXT
,     "Edit...\tCTRL-E") 
 181         self
.editMenu
.Append(menu_DELETE
,        "Delete\tDEL") 
 183         menuBar
.Append(self
.editMenu
, "Edit") 
 185         self
.toolsMenu 
= wx
.Menu() 
 186         self
.toolsMenu
.Append(menu_SELECT
,  "Selection", kind
=wx
.ITEM_CHECK
) 
 187         self
.toolsMenu
.Append(menu_LINE
,    "Line",      kind
=wx
.ITEM_CHECK
) 
 188         self
.toolsMenu
.Append(menu_RECT
,    "Rectangle", kind
=wx
.ITEM_CHECK
) 
 189         self
.toolsMenu
.Append(menu_ELLIPSE
, "Ellipse",   kind
=wx
.ITEM_CHECK
) 
 190         self
.toolsMenu
.Append(menu_TEXT
,    "Text",      kind
=wx
.ITEM_CHECK
) 
 192         menuBar
.Append(self
.toolsMenu
, "Tools") 
 194         self
.objectMenu 
= wx
.Menu() 
 195         self
.objectMenu
.Append(menu_MOVE_FORWARD
,  "Move Forward") 
 196         self
.objectMenu
.Append(menu_MOVE_TO_FRONT
, "Move to Front\tCTRL-F") 
 197         self
.objectMenu
.Append(menu_MOVE_BACKWARD
, "Move Backward") 
 198         self
.objectMenu
.Append(menu_MOVE_TO_BACK
,  "Move to Back\tCTRL-B") 
 200         menuBar
.Append(self
.objectMenu
, "Object") 
 202         self
.helpMenu 
= wx
.Menu() 
 203         self
.helpMenu
.Append(menu_ABOUT
, "About pySketch...") 
 205         menuBar
.Append(self
.helpMenu
, "Help") 
 207         self
.SetMenuBar(menuBar
) 
 209         # Create our toolbar. 
 211         self
.toolbar 
= self
.CreateToolBar(wx
.TB_HORIZONTAL |
 
 212                                           wx
.NO_BORDER | wx
.TB_FLAT
) 
 214         self
.toolbar
.AddSimpleTool(wx
.ID_NEW
, 
 215                                    wx
.Bitmap("images/new.bmp", 
 218         self
.toolbar
.AddSimpleTool(wx
.ID_OPEN
, 
 219                                    wx
.Bitmap("images/open.bmp", 
 222         self
.toolbar
.AddSimpleTool(wx
.ID_SAVE
, 
 223                                    wx
.Bitmap("images/save.bmp", 
 226         self
.toolbar
.AddSeparator() 
 227         self
.toolbar
.AddSimpleTool(menu_UNDO
, 
 228                                    wx
.Bitmap("images/undo.bmp", 
 231         self
.toolbar
.AddSeparator() 
 232         self
.toolbar
.AddSimpleTool(menu_DUPLICATE
, 
 233                                    wx
.Bitmap("images/duplicate.bmp", 
 236         self
.toolbar
.AddSeparator() 
 237         self
.toolbar
.AddSimpleTool(menu_MOVE_FORWARD
, 
 238                                    wx
.Bitmap("images/moveForward.bmp", 
 241         self
.toolbar
.AddSimpleTool(menu_MOVE_BACKWARD
, 
 242                                    wx
.Bitmap("images/moveBack.bmp", 
 246         self
.toolbar
.Realize() 
 248         # Associate each menu/toolbar item with the method that handles that 
 251         (wx
.ID_NEW
,    self
.doNew
), 
 252         (wx
.ID_OPEN
,   self
.doOpen
), 
 253         (wx
.ID_CLOSE
,  self
.doClose
), 
 254         (wx
.ID_SAVE
,   self
.doSave
), 
 255         (wx
.ID_SAVEAS
, self
.doSaveAs
), 
 256         (wx
.ID_REVERT
, self
.doRevert
), 
 257         (wx
.ID_EXIT
,   self
.doExit
), 
 259         (menu_UNDO
,          self
.doUndo
), 
 260         (menu_SELECT_ALL
,    self
.doSelectAll
), 
 261         (menu_DUPLICATE
,     self
.doDuplicate
), 
 262         (menu_EDIT_TEXT
,     self
.doEditText
), 
 263         (menu_DELETE
,        self
.doDelete
), 
 265         (menu_SELECT
,  self
.doChooseSelectTool
), 
 266         (menu_LINE
,    self
.doChooseLineTool
), 
 267         (menu_RECT
,    self
.doChooseRectTool
), 
 268         (menu_ELLIPSE
, self
.doChooseEllipseTool
), 
 269         (menu_TEXT
,    self
.doChooseTextTool
), 
 271         (menu_MOVE_FORWARD
,  self
.doMoveForward
), 
 272         (menu_MOVE_TO_FRONT
, self
.doMoveToFront
), 
 273         (menu_MOVE_BACKWARD
, self
.doMoveBackward
), 
 274         (menu_MOVE_TO_BACK
,  self
.doMoveToBack
), 
 276         (menu_ABOUT
, self
.doShowAbout
)] 
 277         for combo 
in menuHandlers
: 
 279                 self
.Bind(wx
.EVT_MENU
, handler
, id = id) 
 282         # Install our own method to handle closing the window.  This allows us 
 283         # to ask the user if he/she wants to save before closing the window, as 
 284         # well as keeping track of which windows are currently open. 
 286         self
.Bind(wx
.EVT_CLOSE
, self
.doClose
) 
 288         # Install our own method for handling keystrokes.  We use this to let 
 289         # the user move the selected object(s) around using the arrow keys. 
 291         self
.Bind(wx
.EVT_CHAR_HOOK
, self
.onKeyEvent
) 
 293         # Setup our top-most panel.  This holds the entire contents of the 
 294         # window, excluding the menu bar. 
 296         self
.topPanel 
= wx
.Panel(self
, -1, style
=wx
.SIMPLE_BORDER
) 
 298         # Setup our tool palette, with all our drawing tools and option icons. 
 300         self
.toolPalette 
= wx
.BoxSizer(wx
.VERTICAL
) 
 302         self
.selectIcon  
= ToolPaletteIcon(self
.topPanel
, id_SELECT
, 
 303                                            "select", "Selection Tool") 
 304         self
.lineIcon    
= ToolPaletteIcon(self
.topPanel
, id_LINE
, 
 306         self
.rectIcon    
= ToolPaletteIcon(self
.topPanel
, id_RECT
, 
 307                                            "rect", "Rectangle Tool") 
 308         self
.ellipseIcon 
= ToolPaletteIcon(self
.topPanel
, id_ELLIPSE
, 
 309                                            "ellipse", "Ellipse Tool") 
 310         self
.textIcon    
= ToolPaletteIcon(self
.topPanel
, id_TEXT
, 
 313         toolSizer 
= wx
.GridSizer(0, 2, 5, 5) 
 314         toolSizer
.Add(self
.selectIcon
) 
 315         toolSizer
.Add((0, 0)) # Gap to make tool icons line up nicely. 
 316         toolSizer
.Add(self
.lineIcon
) 
 317         toolSizer
.Add(self
.rectIcon
) 
 318         toolSizer
.Add(self
.ellipseIcon
) 
 319         toolSizer
.Add(self
.textIcon
) 
 321         self
.optionIndicator 
= ToolOptionIndicator(self
.topPanel
) 
 322         self
.optionIndicator
.SetToolTip( 
 323                 wx
.ToolTip("Shows Current Pen/Fill/Line Size Settings")) 
 325         optionSizer 
= wx
.BoxSizer(wx
.HORIZONTAL
) 
 327         self
.penOptIcon  
= ToolPaletteIcon(self
.topPanel
, id_PEN_OPT
, 
 328                                            "penOpt", "Set Pen Colour") 
 329         self
.fillOptIcon 
= ToolPaletteIcon(self
.topPanel
, id_FILL_OPT
, 
 330                                            "fillOpt", "Set Fill Colour") 
 331         self
.lineOptIcon 
= ToolPaletteIcon(self
.topPanel
, id_LINE_OPT
, 
 332                                            "lineOpt", "Set Line Size") 
 334         margin 
= wx
.LEFT | wx
.RIGHT
 
 335         optionSizer
.Add(self
.penOptIcon
,  0, margin
, 1) 
 336         optionSizer
.Add(self
.fillOptIcon
, 0, margin
, 1) 
 337         optionSizer
.Add(self
.lineOptIcon
, 0, margin
, 1) 
 339         margin 
= wx
.TOP | wx
.LEFT | wx
.RIGHT | wx
.ALIGN_CENTRE
 
 340         self
.toolPalette
.Add(toolSizer
,            0, margin
, 5) 
 341         self
.toolPalette
.Add((0, 0),               0, margin
, 5) # Spacer. 
 342         self
.toolPalette
.Add(self
.optionIndicator
, 0, margin
, 5) 
 343         self
.toolPalette
.Add(optionSizer
,          0, margin
, 5) 
 345         # Make the tool palette icons respond when the user clicks on them. 
 347         self
.selectIcon
.Bind(wx
.EVT_BUTTON
, self
.onToolIconClick
) 
 348         self
.lineIcon
.Bind(wx
.EVT_BUTTON
, self
.onToolIconClick
) 
 349         self
.rectIcon
.Bind(wx
.EVT_BUTTON
, self
.onToolIconClick
) 
 350         self
.ellipseIcon
.Bind(wx
.EVT_BUTTON
, self
.onToolIconClick
) 
 351         self
.textIcon
.Bind(wx
.EVT_BUTTON
, self
.onToolIconClick
) 
 352         self
.penOptIcon
.Bind(wx
.EVT_BUTTON
, self
.onPenOptionIconClick
) 
 353         self
.fillOptIcon
.Bind(wx
.EVT_BUTTON
, self
.onFillOptionIconClick
) 
 354         self
.lineOptIcon
.Bind(wx
.EVT_BUTTON
, self
.onLineOptionIconClick
) 
 356         # Setup the main drawing area. 
 358         self
.drawPanel 
= wx
.ScrolledWindow(self
.topPanel
, -1, 
 359                                           style
=wx
.SUNKEN_BORDER
) 
 360         self
.drawPanel
.SetBackgroundColour(wx
.WHITE
) 
 362         self
.drawPanel
.EnableScrolling(True, True) 
 363         self
.drawPanel
.SetScrollbars(20, 20, PAGE_WIDTH 
/ 20, PAGE_HEIGHT 
/ 20) 
 365         self
.drawPanel
.Bind(wx
.EVT_LEFT_DOWN
, self
.onMouseEvent
) 
 366         self
.drawPanel
.Bind(wx
.EVT_LEFT_DCLICK
, self
.onDoubleClickEvent
) 
 367         self
.drawPanel
.Bind(wx
.EVT_RIGHT_DOWN
, self
.onRightClick
) 
 368         self
.drawPanel
.Bind(wx
.EVT_MOTION
, self
.onMouseEvent
) 
 369         self
.drawPanel
.Bind(wx
.EVT_LEFT_UP
, self
.onMouseEvent
) 
 370         self
.drawPanel
.Bind(wx
.EVT_PAINT
, self
.onPaintEvent
) 
 372         # Position everything in the window. 
 374         topSizer 
= wx
.BoxSizer(wx
.HORIZONTAL
) 
 375         topSizer
.Add(self
.toolPalette
, 0) 
 376         topSizer
.Add(self
.drawPanel
, 1, wx
.EXPAND
) 
 378         self
.topPanel
.SetAutoLayout(True) 
 379         self
.topPanel
.SetSizer(topSizer
) 
 381         self
.SetSizeHints(250, 200) 
 382         self
.SetSize(wx
.Size(600, 400)) 
 384         # Select an initial tool. 
 387         self
._setCurrentTool
(self
.selectIcon
) 
 389         # Setup our frame to hold the contents of a sketch document. 
 392         self
.fileName  
= fileName
 
 393         self
.contents  
= []     # front-to-back ordered list of DrawingObjects. 
 394         self
.selection 
= []     # List of selected DrawingObjects. 
 395         self
.undoInfo  
= None   # Saved contents for undo. 
 396         self
.dragMode  
= drag_NONE 
# Current mouse-drag mode. 
 398         if self
.fileName 
!= None: 
 403         # Finally, set our initial pen, fill and line options. 
 405         self
.penColour  
= wx
.BLACK
 
 406         self
.fillColour 
= wx
.WHITE
 
 409     # ============================ 
 410     # == Event Handling Methods == 
 411     # ============================ 
 413     def onToolIconClick(self
, event
): 
 414         """ Respond to the user clicking on one of our tool icons. 
 416         iconID 
= event
.GetEventObject().GetId() 
 418         if   iconID 
== id_SELECT
:   self
.doChooseSelectTool() 
 419         elif iconID 
== id_LINE
:     self
.doChooseLineTool() 
 420         elif iconID 
== id_RECT
:     self
.doChooseRectTool() 
 421         elif iconID 
== id_ELLIPSE
:  self
.doChooseEllipseTool() 
 422         elif iconID 
== id_TEXT
:     self
.doChooseTextTool() 
 423         else:                       wx
.Bell(); print "1" 
 426     def onPenOptionIconClick(self
, event
): 
 427         """ Respond to the user clicking on the "Pen Options" icon. 
 429         data 
= wx
.ColourData() 
 430         if len(self
.selection
) == 1: 
 431             data
.SetColour(self
.selection
[0].getPenColour()) 
 433             data
.SetColour(self
.penColour
) 
 435         dialog 
= wx
.ColourDialog(self
, data
) 
 436         if dialog
.ShowModal() == wx
.ID_OK
: 
 437             c 
= dialog
.GetColourData().GetColour() 
 438             self
._setPenColour
(wx
.Colour(c
.Red(), c
.Green(), c
.Blue())) 
 442     def onFillOptionIconClick(self
, event
): 
 443         """ Respond to the user clicking on the "Fill Options" icon. 
 445         data 
= wx
.ColourData() 
 446         if len(self
.selection
) == 1: 
 447             data
.SetColour(self
.selection
[0].getFillColour()) 
 449             data
.SetColour(self
.fillColour
) 
 451         dialog 
= wx
.ColourDialog(self
, data
) 
 452         if dialog
.ShowModal() == wx
.ID_OK
: 
 453             c 
= dialog
.GetColourData().GetColour() 
 454             self
._setFillColour
(wx
.Colour(c
.Red(), c
.Green(), c
.Blue())) 
 457     def onLineOptionIconClick(self
, event
): 
 458         """ Respond to the user clicking on the "Line Options" icon. 
 460         if len(self
.selection
) == 1: 
 461             menu 
= self
._buildLineSizePopup
(self
.selection
[0].getLineSize()) 
 463             menu 
= self
._buildLineSizePopup
(self
.lineSize
) 
 465         pos 
= self
.lineOptIcon
.GetPosition() 
 466         pos
.y 
= pos
.y 
+ self
.lineOptIcon
.GetSize().height
 
 467         self
.PopupMenu(menu
, pos
) 
 471     def onKeyEvent(self
, event
): 
 472         """ Respond to a keypress event. 
 474             We make the arrow keys move the selected object(s) by one pixel in 
 477         if event
.GetKeyCode() == wx
.WXK_UP
: 
 478             self
._moveObject
(0, -1) 
 479         elif event
.GetKeyCode() == wx
.WXK_DOWN
: 
 480             self
._moveObject
(0, 1) 
 481         elif event
.GetKeyCode() == wx
.WXK_LEFT
: 
 482             self
._moveObject
(-1, 0) 
 483         elif event
.GetKeyCode() == wx
.WXK_RIGHT
: 
 484             self
._moveObject
(1, 0) 
 489     def onMouseEvent(self
, event
): 
 490         """ Respond to the user clicking on our main drawing panel. 
 492             How we respond depends on the currently selected tool. 
 494         if not (event
.LeftDown() or event
.Dragging() or event
.LeftUp()): 
 495             return # Ignore mouse movement without click/drag. 
 497         if self
.curTool 
== self
.selectIcon
: 
 498             feedbackType 
= feedback_RECT
 
 499             action       
= self
.selectByRectangle
 
 500             actionParam  
= param_RECT
 
 503         elif self
.curTool 
== self
.lineIcon
: 
 504             feedbackType 
= feedback_LINE
 
 505             action       
= self
.createLine
 
 506             actionParam  
= param_LINE
 
 509         elif self
.curTool 
== self
.rectIcon
: 
 510             feedbackType 
= feedback_RECT
 
 511             action       
= self
.createRect
 
 512             actionParam  
= param_RECT
 
 515         elif self
.curTool 
== self
.ellipseIcon
: 
 516             feedbackType 
= feedback_ELLIPSE
 
 517             action       
= self
.createEllipse
 
 518             actionParam  
= param_RECT
 
 521         elif self
.curTool 
== self
.textIcon
: 
 522             feedbackType 
= feedback_RECT
 
 523             action       
= self
.createText
 
 524             actionParam  
= param_RECT
 
 532             mousePt 
= self
._getEventCoordinates
(event
) 
 534                 obj
, handle 
= self
._getObjectAndSelectionHandleAt
(mousePt
) 
 536             if selecting 
and (obj 
!= None) and (handle 
!= handle_NONE
): 
 538                 # The user clicked on an object's selection handle.  Let the 
 539                 # user resize the clicked-on object. 
 541                 self
.dragMode     
= drag_RESIZE
 
 542                 self
.resizeObject 
= obj
 
 544                 if obj
.getType() == obj_LINE
: 
 545                     self
.resizeFeedback 
= feedback_LINE
 
 546                     pos  
= obj
.getPosition() 
 547                     startPt 
= wx
.Point(pos
.x 
+ obj
.getStartPt().x
, 
 548                                       pos
.y 
+ obj
.getStartPt().y
) 
 549                     endPt   
= wx
.Point(pos
.x 
+ obj
.getEndPt().x
, 
 550                                       pos
.y 
+ obj
.getEndPt().y
) 
 551                     if handle 
== handle_START_POINT
: 
 552                         self
.resizeAnchor  
= endPt
 
 553                         self
.resizeFloater 
= startPt
 
 555                         self
.resizeAnchor  
= startPt
 
 556                         self
.resizeFloater 
= endPt
 
 558                     self
.resizeFeedback 
= feedback_RECT
 
 559                     pos  
= obj
.getPosition() 
 561                     topLeft  
= wx
.Point(pos
.x
, pos
.y
) 
 562                     topRight 
= wx
.Point(pos
.x 
+ size
.width
, pos
.y
) 
 563                     botLeft  
= wx
.Point(pos
.x
, pos
.y 
+ size
.height
) 
 564                     botRight 
= wx
.Point(pos
.x 
+ size
.width
, pos
.y 
+ size
.height
) 
 566                     if handle 
== handle_TOP_LEFT
: 
 567                         self
.resizeAnchor  
= botRight
 
 568                         self
.resizeFloater 
= topLeft
 
 569                     elif handle 
== handle_TOP_RIGHT
: 
 570                         self
.resizeAnchor  
= botLeft
 
 571                         self
.resizeFloater 
= topRight
 
 572                     elif handle 
== handle_BOTTOM_LEFT
: 
 573                         self
.resizeAnchor  
= topRight
 
 574                         self
.resizeFloater 
= botLeft
 
 575                     elif handle 
== handle_BOTTOM_RIGHT
: 
 576                         self
.resizeAnchor  
= topLeft
 
 577                         self
.resizeFloater 
= botRight
 
 580                 self
.resizeOffsetX 
= self
.resizeFloater
.x 
- mousePt
.x
 
 581                 self
.resizeOffsetY 
= self
.resizeFloater
.y 
- mousePt
.y
 
 582                 endPt 
= wx
.Point(self
.curPt
.x 
+ self
.resizeOffsetX
, 
 583                                 self
.curPt
.y 
+ self
.resizeOffsetY
) 
 584                 self
._drawVisualFeedback
(self
.resizeAnchor
, endPt
, 
 585                                          self
.resizeFeedback
, False) 
 587             elif selecting 
and (self
._getObjectAt
(mousePt
) != None): 
 589                 # The user clicked on an object to select it.  If the user 
 590                 # drags, he/she will move the object. 
 592                 self
.select(self
._getObjectAt
(mousePt
)) 
 593                 self
.dragMode 
= drag_MOVE
 
 594                 self
.moveOrigin 
= mousePt
 
 596                 self
._drawObjectOutline
(0, 0) 
 600                 # The user is dragging out a selection rect or new object. 
 602                 self
.dragOrigin 
= mousePt
 
 604                 self
.drawPanel
.SetCursor(wx
.CROSS_CURSOR
) 
 605                 self
.drawPanel
.CaptureMouse() 
 606                 self
._drawVisualFeedback
(mousePt
, mousePt
, feedbackType
, 
 608                 self
.dragMode 
= drag_DRAG
 
 614             if self
.dragMode 
== drag_RESIZE
: 
 616                 # We're resizing an object. 
 618                 mousePt 
= self
._getEventCoordinates
(event
) 
 619                 if (self
.curPt
.x 
!= mousePt
.x
) or (self
.curPt
.y 
!= mousePt
.y
): 
 620                     # Erase previous visual feedback. 
 621                     endPt 
= wx
.Point(self
.curPt
.x 
+ self
.resizeOffsetX
, 
 622                                     self
.curPt
.y 
+ self
.resizeOffsetY
) 
 623                     self
._drawVisualFeedback
(self
.resizeAnchor
, endPt
, 
 624                                              self
.resizeFeedback
, False) 
 626                     # Draw new visual feedback. 
 627                     endPt 
= wx
.Point(self
.curPt
.x 
+ self
.resizeOffsetX
, 
 628                                     self
.curPt
.y 
+ self
.resizeOffsetY
) 
 629                     self
._drawVisualFeedback
(self
.resizeAnchor
, endPt
, 
 630                                              self
.resizeFeedback
, False) 
 632             elif self
.dragMode 
== drag_MOVE
: 
 634                 # We're moving a selected object. 
 636                 mousePt 
= self
._getEventCoordinates
(event
) 
 637                 if (self
.curPt
.x 
!= mousePt
.x
) or (self
.curPt
.y 
!= mousePt
.y
): 
 638                     # Erase previous visual feedback. 
 639                     self
._drawObjectOutline
(self
.curPt
.x 
- self
.moveOrigin
.x
, 
 640                                             self
.curPt
.y 
- self
.moveOrigin
.y
) 
 642                     # Draw new visual feedback. 
 643                     self
._drawObjectOutline
(self
.curPt
.x 
- self
.moveOrigin
.x
, 
 644                                             self
.curPt
.y 
- self
.moveOrigin
.y
) 
 646             elif self
.dragMode 
== drag_DRAG
: 
 648                 # We're dragging out a new object or selection rect. 
 650                 mousePt 
= self
._getEventCoordinates
(event
) 
 651                 if (self
.curPt
.x 
!= mousePt
.x
) or (self
.curPt
.y 
!= mousePt
.y
): 
 652                     # Erase previous visual feedback. 
 653                     self
._drawVisualFeedback
(self
.dragOrigin
, self
.curPt
, 
 654                                              feedbackType
, dashedLine
) 
 656                     # Draw new visual feedback. 
 657                     self
._drawVisualFeedback
(self
.dragOrigin
, self
.curPt
, 
 658                                              feedbackType
, dashedLine
) 
 664             if self
.dragMode 
== drag_RESIZE
: 
 666                 # We're resizing an object. 
 668                 mousePt 
= self
._getEventCoordinates
(event
) 
 669                 # Erase last visual feedback. 
 670                 endPt 
= wx
.Point(self
.curPt
.x 
+ self
.resizeOffsetX
, 
 671                                 self
.curPt
.y 
+ self
.resizeOffsetY
) 
 672                 self
._drawVisualFeedback
(self
.resizeAnchor
, endPt
, 
 673                                          self
.resizeFeedback
, False) 
 675                 resizePt 
= wx
.Point(mousePt
.x 
+ self
.resizeOffsetX
, 
 676                                    mousePt
.y 
+ self
.resizeOffsetY
) 
 678                 if (self
.resizeFloater
.x 
!= resizePt
.x
) or \
 
 679                    (self
.resizeFloater
.y 
!= resizePt
.y
): 
 680                    self
._resizeObject
(self
.resizeObject
, 
 685                     self
.drawPanel
.Refresh() # Clean up after empty resize. 
 687             elif self
.dragMode 
== drag_MOVE
: 
 689                 # We're moving a selected object. 
 691                 mousePt 
= self
._getEventCoordinates
(event
) 
 692                 # Erase last visual feedback. 
 693                 self
._drawObjectOutline
(self
.curPt
.x 
- self
.moveOrigin
.x
, 
 694                                         self
.curPt
.y 
- self
.moveOrigin
.y
) 
 695                 if (self
.moveOrigin
.x 
!= mousePt
.x
) or \
 
 696                    (self
.moveOrigin
.y 
!= mousePt
.y
): 
 697                     self
._moveObject
(mousePt
.x 
- self
.moveOrigin
.x
, 
 698                                      mousePt
.y 
- self
.moveOrigin
.y
) 
 700                     self
.drawPanel
.Refresh() # Clean up after empty drag. 
 702             elif self
.dragMode 
== drag_DRAG
: 
 704                 # We're dragging out a new object or selection rect. 
 706                 mousePt 
= self
._getEventCoordinates
(event
) 
 707                 # Erase last visual feedback. 
 708                 self
._drawVisualFeedback
(self
.dragOrigin
, self
.curPt
, 
 709                                          feedbackType
, dashedLine
) 
 710                 self
.drawPanel
.ReleaseMouse() 
 711                 self
.drawPanel
.SetCursor(wx
.STANDARD_CURSOR
) 
 712                 # Perform the appropriate action for the current tool. 
 713                 if actionParam 
== param_RECT
: 
 714                     x1 
= min(self
.dragOrigin
.x
, self
.curPt
.x
) 
 715                     y1 
= min(self
.dragOrigin
.y
, self
.curPt
.y
) 
 716                     x2 
= max(self
.dragOrigin
.x
, self
.curPt
.x
) 
 717                     y2 
= max(self
.dragOrigin
.y
, self
.curPt
.y
) 
 725                         if ((x2
-x1
) < 8) or ((y2
-y1
) < 8): return # Too small. 
 727                     action(x1
, y1
, x2
-x1
, y2
-y1
) 
 728                 elif actionParam 
== param_LINE
: 
 729                     action(self
.dragOrigin
.x
, self
.dragOrigin
.y
, 
 730                            self
.curPt
.x
, self
.curPt
.y
) 
 732             self
.dragMode 
= drag_NONE 
# We've finished with this mouse event. 
 736     def onDoubleClickEvent(self
, event
): 
 737         """ Respond to a double-click within our drawing panel. 
 739         mousePt 
= self
._getEventCoordinates
(event
) 
 740         obj 
= self
._getObjectAt
(mousePt
) 
 741         if obj 
== None: return 
 743         # Let the user edit the given object. 
 745         if obj
.getType() == obj_TEXT
: 
 746             editor 
= EditTextObjectDialog(self
, "Edit Text Object") 
 747             editor
.objectToDialog(obj
) 
 748             if editor
.ShowModal() == wx
.ID_CANCEL
: 
 754             editor
.dialogToObject(obj
) 
 758             self
.drawPanel
.Refresh() 
 764     def onRightClick(self
, event
): 
 765         """ Respond to the user right-clicking within our drawing panel. 
 767             We select the clicked-on item, if necessary, and display a pop-up 
 768             menu of available options which can be applied to the selected 
 771         mousePt 
= self
._getEventCoordinates
(event
) 
 772         obj 
= self
._getObjectAt
(mousePt
) 
 774         if obj 
== None: return # Nothing selected. 
 776         # Select the clicked-on object. 
 780         # Build our pop-up menu. 
 783         menu
.Append(menu_DUPLICATE
, "Duplicate") 
 784         menu
.Append(menu_EDIT_TEXT
, "Edit...") 
 785         menu
.Append(menu_DELETE
,    "Delete") 
 786         menu
.AppendSeparator() 
 787         menu
.Append(menu_MOVE_FORWARD
,   "Move Forward") 
 788         menu
.Append(menu_MOVE_TO_FRONT
,  "Move to Front") 
 789         menu
.Append(menu_MOVE_BACKWARD
,  "Move Backward") 
 790         menu
.Append(menu_MOVE_TO_BACK
,   "Move to Back") 
 792         menu
.Enable(menu_EDIT_TEXT
,     obj
.getType() == obj_TEXT
) 
 793         menu
.Enable(menu_MOVE_FORWARD
,  obj 
!= self
.contents
[0]) 
 794         menu
.Enable(menu_MOVE_TO_FRONT
, obj 
!= self
.contents
[0]) 
 795         menu
.Enable(menu_MOVE_BACKWARD
, obj 
!= self
.contents
[-1]) 
 796         menu
.Enable(menu_MOVE_TO_BACK
,  obj 
!= self
.contents
[-1]) 
 798         EVT_MENU(self
, menu_DUPLICATE
,     self
.doDuplicate
) 
 799         EVT_MENU(self
, menu_EDIT_TEXT
,     self
.doEditText
) 
 800         EVT_MENU(self
, menu_DELETE
,        self
.doDelete
) 
 801         EVT_MENU(self
, menu_MOVE_FORWARD
,  self
.doMoveForward
) 
 802         EVT_MENU(self
, menu_MOVE_TO_FRONT
, self
.doMoveToFront
) 
 803         EVT_MENU(self
, menu_MOVE_BACKWARD
, self
.doMoveBackward
) 
 804         EVT_MENU(self
, menu_MOVE_TO_BACK
,  self
.doMoveToBack
) 
 806         # Show the pop-up menu. 
 808         clickPt 
= wx
.Point(mousePt
.x 
+ self
.drawPanel
.GetPosition().x
, 
 809                           mousePt
.y 
+ self
.drawPanel
.GetPosition().y
) 
 810         self
.drawPanel
.PopupMenu(menu
, clickPt
) 
 814     def onPaintEvent(self
, event
): 
 815         """ Respond to a request to redraw the contents of our drawing panel. 
 817         dc 
= wx
.PaintDC(self
.drawPanel
) 
 818         self
.drawPanel
.PrepareDC(dc
) 
 821         for i 
in range(len(self
.contents
)-1, -1, -1): 
 822             obj 
= self
.contents
[i
] 
 823             if obj 
in self
.selection
: 
 830     # ========================== 
 831     # == Menu Command Methods == 
 832     # ========================== 
 834     def doNew(self
, event
): 
 835         """ Respond to the "New" menu command. 
 838         newFrame 
= DrawingFrame(None, -1, "Untitled") 
 840         _docList
.append(newFrame
) 
 843     def doOpen(self
, event
): 
 844         """ Respond to the "Open" menu command. 
 849         fileName 
= wx
.FileSelector("Open File", default_extension
="psk", 
 850                                   flags 
= wx
.OPEN | wx
.FILE_MUST_EXIST
) 
 851         if fileName 
== "": return 
 852         fileName 
= os
.path
.join(os
.getcwd(), fileName
) 
 855         title 
= os
.path
.basename(fileName
) 
 857         if (self
.fileName 
== None) and (len(self
.contents
) == 0): 
 858             # Load contents into current (empty) document. 
 859             self
.fileName 
= fileName
 
 860             self
.SetTitle(os
.path
.basename(fileName
)) 
 863             # Open a new frame for this document. 
 864             newFrame 
= DrawingFrame(None, -1, os
.path
.basename(fileName
), 
 867             _docList
.append(newFrame
) 
 870     def doClose(self
, event
): 
 871         """ Respond to the "Close" menu command. 
 876             if not self
.askIfUserWantsToSave("closing"): return 
 878         _docList
.remove(self
) 
 882     def doSave(self
, event
): 
 883         """ Respond to the "Save" menu command. 
 885         if self
.fileName 
!= None: 
 889     def doSaveAs(self
, event
): 
 890         """ Respond to the "Save As" menu command. 
 892         if self
.fileName 
== None: 
 895             default 
= self
.fileName
 
 898         fileName 
= wx
.FileSelector("Save File As", "Saving", 
 899                                   default_filename
=default
, 
 900                                   default_extension
="psk", 
 902                                   flags 
= wx
.SAVE | wx
.OVERWRITE_PROMPT
) 
 903         if fileName 
== "": return # User cancelled. 
 904         fileName 
= os
.path
.join(os
.getcwd(), fileName
) 
 907         title 
= os
.path
.basename(fileName
) 
 910         self
.fileName 
= fileName
 
 914     def doRevert(self
, event
): 
 915         """ Respond to the "Revert" menu command. 
 917         if not self
.dirty
: return 
 919         if wx
.MessageBox("Discard changes made to this document?", "Confirm", 
 920                         style 
= wx
.OK | wx
.CANCEL | wx
.ICON_QUESTION
, 
 921                         parent
=self
) == wx
.CANCEL
: return 
 925     def doExit(self
, event
): 
 926         """ Respond to the "Quit" menu command. 
 928         global _docList
, _app
 
 930             if not doc
.dirty
: continue 
 932             if not doc
.askIfUserWantsToSave("quitting"): return 
 939     def doUndo(self
, event
): 
 940         """ Respond to the "Undo" menu command. 
 942         if self
.undoInfo 
== None: return 
 944         undoData 
= self
.undoInfo
 
 945         self
._saveUndoInfo
() # For undoing the undo... 
 949         for type, data 
in undoData
["contents"]: 
 950             obj 
= DrawingObject(type) 
 952             self
.contents
.append(obj
) 
 955         for i 
in undoData
["selection"]: 
 956             self
.selection
.append(self
.contents
[i
]) 
 959         self
.drawPanel
.Refresh() 
 963     def doSelectAll(self
, event
): 
 964         """ Respond to the "Select All" menu command. 
 969     def doDuplicate(self
, event
): 
 970         """ Respond to the "Duplicate" menu command. 
 975         for obj 
in self
.contents
: 
 976             if obj 
in self
.selection
: 
 977                 newObj 
= DrawingObject(obj
.getType()) 
 978                 newObj
.setData(obj
.getData()) 
 979                 pos 
= obj
.getPosition() 
 980                 newObj
.setPosition(wx
.Point(pos
.x 
+ 10, pos
.y 
+ 10)) 
 983         self
.contents 
= objs 
+ self
.contents
 
 985         self
.selectMany(objs
) 
 988     def doEditText(self
, event
): 
 989         """ Respond to the "Edit Text" menu command. 
 991         if len(self
.selection
) != 1: return 
 993         obj 
= self
.selection
[0] 
 994         if obj
.getType() != obj_TEXT
: return 
 996         editor 
= EditTextObjectDialog(self
, "Edit Text Object") 
 997         editor
.objectToDialog(obj
) 
 998         if editor
.ShowModal() == wx
.ID_CANCEL
: 
1002         self
._saveUndoInfo
() 
1004         editor
.dialogToObject(obj
) 
1008         self
.drawPanel
.Refresh() 
1012     def doDelete(self
, event
): 
1013         """ Respond to the "Delete" menu command. 
1015         self
._saveUndoInfo
() 
1017         for obj 
in self
.selection
: 
1018             self
.contents
.remove(obj
) 
1023     def doChooseSelectTool(self
, event
=None): 
1024         """ Respond to the "Select Tool" menu command. 
1026         self
._setCurrentTool
(self
.selectIcon
) 
1027         self
.drawPanel
.SetCursor(wx
.STANDARD_CURSOR
) 
1031     def doChooseLineTool(self
, event
=None): 
1032         """ Respond to the "Line Tool" menu command. 
1034         self
._setCurrentTool
(self
.lineIcon
) 
1035         self
.drawPanel
.SetCursor(wx
.CROSS_CURSOR
) 
1040     def doChooseRectTool(self
, event
=None): 
1041         """ Respond to the "Rect Tool" menu command. 
1043         self
._setCurrentTool
(self
.rectIcon
) 
1044         self
.drawPanel
.SetCursor(wx
.CROSS_CURSOR
) 
1049     def doChooseEllipseTool(self
, event
=None): 
1050         """ Respond to the "Ellipse Tool" menu command. 
1052         self
._setCurrentTool
(self
.ellipseIcon
) 
1053         self
.drawPanel
.SetCursor(wx
.CROSS_CURSOR
) 
1058     def doChooseTextTool(self
, event
=None): 
1059         """ Respond to the "Text Tool" menu command. 
1061         self
._setCurrentTool
(self
.textIcon
) 
1062         self
.drawPanel
.SetCursor(wx
.CROSS_CURSOR
) 
1067     def doMoveForward(self
, event
): 
1068         """ Respond to the "Move Forward" menu command. 
1070         if len(self
.selection
) != 1: return 
1072         self
._saveUndoInfo
() 
1074         obj 
= self
.selection
[0] 
1075         index 
= self
.contents
.index(obj
) 
1076         if index 
== 0: return 
1078         del self
.contents
[index
] 
1079         self
.contents
.insert(index
-1, obj
) 
1081         self
.drawPanel
.Refresh() 
1085     def doMoveToFront(self
, event
): 
1086         """ Respond to the "Move to Front" menu command. 
1088         if len(self
.selection
) != 1: return 
1090         self
._saveUndoInfo
() 
1092         obj 
= self
.selection
[0] 
1093         self
.contents
.remove(obj
) 
1094         self
.contents
.insert(0, obj
) 
1096         self
.drawPanel
.Refresh() 
1100     def doMoveBackward(self
, event
): 
1101         """ Respond to the "Move Backward" menu command. 
1103         if len(self
.selection
) != 1: return 
1105         self
._saveUndoInfo
() 
1107         obj 
= self
.selection
[0] 
1108         index 
= self
.contents
.index(obj
) 
1109         if index 
== len(self
.contents
) - 1: return 
1111         del self
.contents
[index
] 
1112         self
.contents
.insert(index
+1, obj
) 
1114         self
.drawPanel
.Refresh() 
1118     def doMoveToBack(self
, event
): 
1119         """ Respond to the "Move to Back" menu command. 
1121         if len(self
.selection
) != 1: return 
1123         self
._saveUndoInfo
() 
1125         obj 
= self
.selection
[0] 
1126         self
.contents
.remove(obj
) 
1127         self
.contents
.append(obj
) 
1129         self
.drawPanel
.Refresh() 
1133     def doShowAbout(self
, event
): 
1134         """ Respond to the "About pySketch" menu command. 
1136         dialog 
= wx
.Dialog(self
, -1, "About pySketch") # , 
1137                           #style=wx.DIALOG_MODAL | wx.STAY_ON_TOP) 
1138         dialog
.SetBackgroundColour(wx
.WHITE
) 
1140         panel 
= wx
.Panel(dialog
, -1) 
1141         panel
.SetBackgroundColour(wx
.WHITE
) 
1143         panelSizer 
= wx
.BoxSizer(wx
.VERTICAL
) 
1145         boldFont 
= wx
.Font(panel
.GetFont().GetPointSize(), 
1146                           panel
.GetFont().GetFamily(), 
1149         logo 
= wx
.StaticBitmap(panel
, -1, wx
.Bitmap("images/logo.bmp", 
1150                                                   wx
.BITMAP_TYPE_BMP
)) 
1152         lab1 
= wx
.StaticText(panel
, -1, "pySketch") 
1153         lab1
.SetFont(wx
.Font(36, boldFont
.GetFamily(), wx
.ITALIC
, wx
.BOLD
)) 
1154         lab1
.SetSize(lab1
.GetBestSize()) 
1156         imageSizer 
= wx
.BoxSizer(wx
.HORIZONTAL
) 
1157         imageSizer
.Add(logo
, 0, wx
.ALL | wx
.ALIGN_CENTRE_VERTICAL
, 5) 
1158         imageSizer
.Add(lab1
, 0, wx
.ALL | wx
.ALIGN_CENTRE_VERTICAL
, 5) 
1160         lab2 
= wx
.StaticText(panel
, -1, "A simple object-oriented drawing " + \
 
1162         lab2
.SetFont(boldFont
) 
1163         lab2
.SetSize(lab2
.GetBestSize()) 
1165         lab3 
= wx
.StaticText(panel
, -1, "pySketch is completely free " + \
 
1167         lab3
.SetFont(boldFont
) 
1168         lab3
.SetSize(lab3
.GetBestSize()) 
1170         lab4 
= wx
.StaticText(panel
, -1, "feel free to adapt or use this " + \
 
1171                                        "in any way you like.") 
1172         lab4
.SetFont(boldFont
) 
1173         lab4
.SetSize(lab4
.GetBestSize()) 
1175         lab5 
= wx
.StaticText(panel
, -1, "Author: Erik Westra " + \
 
1176                                        "(ewestra@wave.co.nz)") 
1177         lab5
.SetFont(boldFont
) 
1178         lab5
.SetSize(lab5
.GetBestSize()) 
1180         btnOK 
= wx
.Button(panel
, wx
.ID_OK
, "OK") 
1182         panelSizer
.Add(imageSizer
, 0, wx
.ALIGN_CENTRE
) 
1183         panelSizer
.Add((10, 10)) # Spacer. 
1184         panelSizer
.Add(lab2
, 0, wx
.ALIGN_CENTRE
) 
1185         panelSizer
.Add((10, 10)) # Spacer. 
1186         panelSizer
.Add(lab3
, 0, wx
.ALIGN_CENTRE
) 
1187         panelSizer
.Add(lab4
, 0, wx
.ALIGN_CENTRE
) 
1188         panelSizer
.Add((10, 10)) # Spacer. 
1189         panelSizer
.Add(lab5
, 0, wx
.ALIGN_CENTRE
) 
1190         panelSizer
.Add((10, 10)) # Spacer. 
1191         panelSizer
.Add(btnOK
, 0, wx
.ALL | wx
.ALIGN_CENTRE
, 5) 
1193         panel
.SetAutoLayout(True) 
1194         panel
.SetSizer(panelSizer
) 
1195         panelSizer
.Fit(panel
) 
1197         topSizer 
= wx
.BoxSizer(wx
.HORIZONTAL
) 
1198         topSizer
.Add(panel
, 0, wx
.ALL
, 10) 
1200         dialog
.SetAutoLayout(True) 
1201         dialog
.SetSizer(topSizer
) 
1202         topSizer
.Fit(dialog
) 
1206         btn 
= dialog
.ShowModal() 
1209     # ============================= 
1210     # == Object Creation Methods == 
1211     # ============================= 
1213     def createLine(self
, x1
, y1
, x2
, y2
): 
1214         """ Create a new line object at the given position and size. 
1216         self
._saveUndoInfo
() 
1218         topLeftX  
= min(x1
, x2
) 
1219         topLeftY  
= min(y1
, y2
) 
1220         botRightX 
= max(x1
, x2
) 
1221         botRightY 
= max(y1
, y2
) 
1223         obj 
= DrawingObject(obj_LINE
, position
=wx
.Point(topLeftX
, topLeftY
), 
1224                             size
=wx
.Size(botRightX
-topLeftX
, 
1225                                         botRightY
-topLeftY
), 
1226                             penColour
=self
.penColour
, 
1227                             fillColour
=self
.fillColour
, 
1228                             lineSize
=self
.lineSize
, 
1229                             startPt 
= wx
.Point(x1 
- topLeftX
, y1 
- topLeftY
), 
1230                             endPt   
= wx
.Point(x2 
- topLeftX
, y2 
- topLeftY
)) 
1231         self
.contents
.insert(0, obj
) 
1233         self
.doChooseSelectTool() 
1237     def createRect(self
, x
, y
, width
, height
): 
1238         """ Create a new rectangle object at the given position and size. 
1240         self
._saveUndoInfo
() 
1242         obj 
= DrawingObject(obj_RECT
, position
=wx
.Point(x
, y
), 
1243                             size
=wx
.Size(width
, height
), 
1244                             penColour
=self
.penColour
, 
1245                             fillColour
=self
.fillColour
, 
1246                             lineSize
=self
.lineSize
) 
1247         self
.contents
.insert(0, obj
) 
1249         self
.doChooseSelectTool() 
1253     def createEllipse(self
, x
, y
, width
, height
): 
1254         """ Create a new ellipse object at the given position and size. 
1256         self
._saveUndoInfo
() 
1258         obj 
= DrawingObject(obj_ELLIPSE
, position
=wx
.Point(x
, y
), 
1259                             size
=wx
.Size(width
, height
), 
1260                             penColour
=self
.penColour
, 
1261                             fillColour
=self
.fillColour
, 
1262                             lineSize
=self
.lineSize
) 
1263         self
.contents
.insert(0, obj
) 
1265         self
.doChooseSelectTool() 
1269     def createText(self
, x
, y
, width
, height
): 
1270         """ Create a new text object at the given position and size. 
1272         editor 
= EditTextObjectDialog(self
, "Create Text Object") 
1273         if editor
.ShowModal() == wx
.ID_CANCEL
: 
1277         self
._saveUndoInfo
() 
1279         obj 
= DrawingObject(obj_TEXT
, position
=wx
.Point(x
, y
), 
1280                                       size
=wx
.Size(width
, height
)) 
1281         editor
.dialogToObject(obj
) 
1284         self
.contents
.insert(0, obj
) 
1286         self
.doChooseSelectTool() 
1289     # ======================= 
1290     # == Selection Methods == 
1291     # ======================= 
1293     def selectAll(self
): 
1294         """ Select every DrawingObject in our document. 
1297         for obj 
in self
.contents
: 
1298             self
.selection
.append(obj
) 
1299         self
.drawPanel
.Refresh() 
1303     def deselectAll(self
): 
1304         """ Deselect every DrawingObject in our document. 
1307         self
.drawPanel
.Refresh() 
1311     def select(self
, obj
): 
1312         """ Select the given DrawingObject within our document. 
1314         self
.selection 
= [obj
] 
1315         self
.drawPanel
.Refresh() 
1319     def selectMany(self
, objs
): 
1320         """ Select the given list of DrawingObjects. 
1322         self
.selection 
= objs
 
1323         self
.drawPanel
.Refresh() 
1327     def selectByRectangle(self
, x
, y
, width
, height
): 
1328         """ Select every DrawingObject in the given rectangular region. 
1331         for obj 
in self
.contents
: 
1332             if obj
.objectWithinRect(x
, y
, width
, height
): 
1333                 self
.selection
.append(obj
) 
1334         self
.drawPanel
.Refresh() 
1337     # ====================== 
1338     # == File I/O Methods == 
1339     # ====================== 
1341     def loadContents(self
): 
1342         """ Load the contents of our document into memory. 
1344         f 
= open(self
.fileName
, "rb") 
1345         objData 
= cPickle
.load(f
) 
1348         for type, data 
in objData
: 
1349             obj 
= DrawingObject(type) 
1351             self
.contents
.append(obj
) 
1355         self
.undoInfo  
= None 
1357         self
.drawPanel
.Refresh() 
1361     def saveContents(self
): 
1362         """ Save the contents of our document to disk. 
1365         for obj 
in self
.contents
: 
1366             objData
.append([obj
.getType(), obj
.getData()]) 
1368         f 
= open(self
.fileName
, "wb") 
1369         cPickle
.dump(objData
, f
) 
1375     def askIfUserWantsToSave(self
, action
): 
1376         """ Give the user the opportunity to save the current document. 
1378             'action' is a string describing the action about to be taken.  If 
1379             the user wants to save the document, it is saved immediately.  If 
1380             the user cancels, we return False. 
1382         if not self
.dirty
: return True # Nothing to do. 
1384         response 
= wx
.MessageBox("Save changes before " + action 
+ "?", 
1385                                 "Confirm", wx
.YES_NO | wx
.CANCEL
, self
) 
1387         if response 
== wx
.YES
: 
1388             if self
.fileName 
== None: 
1389                 fileName 
= wx
.FileSelector("Save File As", "Saving", 
1390                                           default_extension
="psk", 
1392                                           flags 
= wx
.SAVE | wx
.OVERWRITE_PROMPT
) 
1393                 if fileName 
== "": return False # User cancelled. 
1394                 self
.fileName 
= fileName
 
1398         elif response 
== wx
.NO
: 
1399             return True # User doesn't want changes saved. 
1400         elif response 
== wx
.CANCEL
: 
1401             return False # User cancelled. 
1403     # ===================== 
1404     # == Private Methods == 
1405     # ===================== 
1407     def _adjustMenus(self
): 
1408         """ Adjust our menus and toolbar to reflect the current state of the 
1411         canSave   
= (self
.fileName 
!= None) and self
.dirty
 
1412         canRevert 
= (self
.fileName 
!= None) and self
.dirty
 
1413         canUndo   
= self
.undoInfo 
!= None 
1414         selection 
= len(self
.selection
) > 0 
1415         onlyOne   
= len(self
.selection
) == 1 
1416         isText    
= onlyOne 
and (self
.selection
[0].getType() == obj_TEXT
) 
1417         front     
= onlyOne 
and (self
.selection
[0] == self
.contents
[0]) 
1418         back      
= onlyOne 
and (self
.selection
[0] == self
.contents
[-1]) 
1420         # Enable/disable our menu items. 
1422         self
.fileMenu
.Enable(wx
.ID_SAVE
,   canSave
) 
1423         self
.fileMenu
.Enable(wx
.ID_REVERT
, canRevert
) 
1425         self
.editMenu
.Enable(menu_UNDO
,      canUndo
) 
1426         self
.editMenu
.Enable(menu_DUPLICATE
, selection
) 
1427         self
.editMenu
.Enable(menu_EDIT_TEXT
, isText
) 
1428         self
.editMenu
.Enable(menu_DELETE
,    selection
) 
1430         self
.toolsMenu
.Check(menu_SELECT
,  self
.curTool 
== self
.selectIcon
) 
1431         self
.toolsMenu
.Check(menu_LINE
,    self
.curTool 
== self
.lineIcon
) 
1432         self
.toolsMenu
.Check(menu_RECT
,    self
.curTool 
== self
.rectIcon
) 
1433         self
.toolsMenu
.Check(menu_ELLIPSE
, self
.curTool 
== self
.ellipseIcon
) 
1434         self
.toolsMenu
.Check(menu_TEXT
,    self
.curTool 
== self
.textIcon
) 
1436         self
.objectMenu
.Enable(menu_MOVE_FORWARD
,  onlyOne 
and not front
) 
1437         self
.objectMenu
.Enable(menu_MOVE_TO_FRONT
, onlyOne 
and not front
) 
1438         self
.objectMenu
.Enable(menu_MOVE_BACKWARD
, onlyOne 
and not back
) 
1439         self
.objectMenu
.Enable(menu_MOVE_TO_BACK
,  onlyOne 
and not back
) 
1441         # Enable/disable our toolbar icons. 
1443         self
.toolbar
.EnableTool(wx
.ID_NEW
,           True) 
1444         self
.toolbar
.EnableTool(wx
.ID_OPEN
,          True) 
1445         self
.toolbar
.EnableTool(wx
.ID_SAVE
,          canSave
) 
1446         self
.toolbar
.EnableTool(menu_UNDO
,          canUndo
) 
1447         self
.toolbar
.EnableTool(menu_DUPLICATE
,     selection
) 
1448         self
.toolbar
.EnableTool(menu_MOVE_FORWARD
,  onlyOne 
and not front
) 
1449         self
.toolbar
.EnableTool(menu_MOVE_BACKWARD
, onlyOne 
and not back
) 
1452     def _setCurrentTool(self
, newToolIcon
): 
1453         """ Set the currently selected tool. 
1455         if self
.curTool 
== newToolIcon
: return # Nothing to do. 
1457         if self
.curTool 
!= None: 
1458             self
.curTool
.deselect() 
1460         newToolIcon
.select() 
1461         self
.curTool 
= newToolIcon
 
1464     def _setPenColour(self
, colour
): 
1465         """ Set the default or selected object's pen colour. 
1467         if len(self
.selection
) > 0: 
1468             self
._saveUndoInfo
() 
1469             for obj 
in self
.selection
: 
1470                 obj
.setPenColour(colour
) 
1471             self
.drawPanel
.Refresh() 
1473             self
.penColour 
= colour
 
1474             self
.optionIndicator
.setPenColour(colour
) 
1477     def _setFillColour(self
, colour
): 
1478         """ Set the default or selected object's fill colour. 
1480         if len(self
.selection
) > 0: 
1481             self
._saveUndoInfo
() 
1482             for obj 
in self
.selection
: 
1483                 obj
.setFillColour(colour
) 
1484             self
.drawPanel
.Refresh() 
1486             self
.fillColour 
= colour
 
1487             self
.optionIndicator
.setFillColour(colour
) 
1490     def _setLineSize(self
, size
): 
1491         """ Set the default or selected object's line size. 
1493         if len(self
.selection
) > 0: 
1494             self
._saveUndoInfo
() 
1495             for obj 
in self
.selection
: 
1496                 obj
.setLineSize(size
) 
1497             self
.drawPanel
.Refresh() 
1499             self
.lineSize 
= size
 
1500             self
.optionIndicator
.setLineSize(size
) 
1503     def _saveUndoInfo(self
): 
1504         """ Remember the current state of the document, to allow for undo. 
1506             We make a copy of the document's contents, so that we can return to 
1507             the previous contents if the user does something and then wants to 
1511         for obj 
in self
.contents
: 
1512             savedContents
.append([obj
.getType(), obj
.getData()]) 
1515         for i 
in range(len(self
.contents
)): 
1516             if self
.contents
[i
] in self
.selection
: 
1517                 savedSelection
.append(i
) 
1519         self
.undoInfo 
= {"contents"  : savedContents
, 
1520                          "selection" : savedSelection
} 
1523     def _resizeObject(self
, obj
, anchorPt
, oldPt
, newPt
): 
1524         """ Resize the given object. 
1526             'anchorPt' is the unchanging corner of the object, while the 
1527             opposite corner has been resized.  'oldPt' are the current 
1528             coordinates for this corner, while 'newPt' are the new coordinates. 
1529             The object should fit within the given dimensions, though if the 
1530             new point is less than the anchor point the object will need to be 
1531             moved as well as resized, to avoid giving it a negative size. 
1533         if obj
.getType() == obj_TEXT
: 
1534             # Not allowed to resize text objects -- they're sized to fit text. 
1535             wx
.Bell(); print "4" 
1538         self
._saveUndoInfo
() 
1540         topLeft  
= wx
.Point(min(anchorPt
.x
, newPt
.x
), 
1541                            min(anchorPt
.y
, newPt
.y
)) 
1542         botRight 
= wx
.Point(max(anchorPt
.x
, newPt
.x
), 
1543                            max(anchorPt
.y
, newPt
.y
)) 
1545         newWidth  
= botRight
.x 
- topLeft
.x
 
1546         newHeight 
= botRight
.y 
- topLeft
.y
 
1548         if obj
.getType() == obj_LINE
: 
1549             # Adjust the line so that its start and end points match the new 
1550             # overall object size. 
1552             startPt 
= obj
.getStartPt() 
1553             endPt   
= obj
.getEndPt() 
1555             slopesDown 
= ((startPt
.x 
< endPt
.x
) and (startPt
.y 
< endPt
.y
)) or \
 
1556                          ((startPt
.x 
> endPt
.x
) and (startPt
.y 
> endPt
.y
)) 
1558             # Handle the user flipping the line. 
1560             hFlip 
= ((anchorPt
.x 
< oldPt
.x
) and (anchorPt
.x 
> newPt
.x
)) or \
 
1561                     ((anchorPt
.x 
> oldPt
.x
) and (anchorPt
.x 
< newPt
.x
)) 
1562             vFlip 
= ((anchorPt
.y 
< oldPt
.y
) and (anchorPt
.y 
> newPt
.y
)) or \
 
1563                     ((anchorPt
.y 
> oldPt
.y
) and (anchorPt
.y 
< newPt
.y
)) 
1565             if (hFlip 
and not vFlip
) or (vFlip 
and not hFlip
): 
1566                 slopesDown 
= not slopesDown 
# Line flipped. 
1569                 obj
.setStartPt(wx
.Point(0, 0)) 
1570                 obj
.setEndPt(wx
.Point(newWidth
, newHeight
)) 
1572                 obj
.setStartPt(wx
.Point(0, newHeight
)) 
1573                 obj
.setEndPt(wx
.Point(newWidth
, 0)) 
1575         # Finally, adjust the bounds of the object to match the new dimensions. 
1577         obj
.setPosition(topLeft
) 
1578         obj
.setSize(wx
.Size(botRight
.x 
- topLeft
.x
, botRight
.y 
- topLeft
.y
)) 
1580         self
.drawPanel
.Refresh() 
1583     def _moveObject(self
, offsetX
, offsetY
): 
1584         """ Move the currently selected object(s) by the given offset. 
1586         self
._saveUndoInfo
() 
1588         for obj 
in self
.selection
: 
1589             pos 
= obj
.getPosition() 
1590             pos
.x 
= pos
.x 
+ offsetX
 
1591             pos
.y 
= pos
.y 
+ offsetY
 
1592             obj
.setPosition(pos
) 
1594         self
.drawPanel
.Refresh() 
1597     def _buildLineSizePopup(self
, lineSize
): 
1598         """ Build the pop-up menu used to set the line size. 
1600             'lineSize' is the current line size value.  The corresponding item 
1601             is checked in the pop-up menu. 
1604         menu
.Append(id_LINESIZE_0
, "no line",      kind
=wx
.ITEM_CHECK
) 
1605         menu
.Append(id_LINESIZE_1
, "1-pixel line", kind
=wx
.ITEM_CHECK
) 
1606         menu
.Append(id_LINESIZE_2
, "2-pixel line", kind
=wx
.ITEM_CHECK
) 
1607         menu
.Append(id_LINESIZE_3
, "3-pixel line", kind
=wx
.ITEM_CHECK
) 
1608         menu
.Append(id_LINESIZE_4
, "4-pixel line", kind
=wx
.ITEM_CHECK
) 
1609         menu
.Append(id_LINESIZE_5
, "5-pixel line", kind
=wx
.ITEM_CHECK
) 
1611         if   lineSize 
== 0: menu
.Check(id_LINESIZE_0
, True) 
1612         elif lineSize 
== 1: menu
.Check(id_LINESIZE_1
, True) 
1613         elif lineSize 
== 2: menu
.Check(id_LINESIZE_2
, True) 
1614         elif lineSize 
== 3: menu
.Check(id_LINESIZE_3
, True) 
1615         elif lineSize 
== 4: menu
.Check(id_LINESIZE_4
, True) 
1616         elif lineSize 
== 5: menu
.Check(id_LINESIZE_5
, True) 
1618         self
.Bind(wx
.EVT_MENU
, self
._lineSizePopupSelected
, id=id_LINESIZE_0
, id2
=id_LINESIZE_5
) 
1623     def _lineSizePopupSelected(self
, event
): 
1624         """ Respond to the user selecting an item from the line size popup menu 
1627         if   id == id_LINESIZE_0
: self
._setLineSize
(0) 
1628         elif id == id_LINESIZE_1
: self
._setLineSize
(1) 
1629         elif id == id_LINESIZE_2
: self
._setLineSize
(2) 
1630         elif id == id_LINESIZE_3
: self
._setLineSize
(3) 
1631         elif id == id_LINESIZE_4
: self
._setLineSize
(4) 
1632         elif id == id_LINESIZE_5
: self
._setLineSize
(5) 
1634             wx
.Bell(); print "5" 
1637         self
.optionIndicator
.setLineSize(self
.lineSize
) 
1640     def _getEventCoordinates(self
, event
): 
1641         """ Return the coordinates associated with the given mouse event. 
1643             The coordinates have to be adjusted to allow for the current scroll 
1646         originX
, originY 
= self
.drawPanel
.GetViewStart() 
1647         unitX
, unitY 
= self
.drawPanel
.GetScrollPixelsPerUnit() 
1648         return wx
.Point(event
.GetX() + (originX 
* unitX
), 
1649                        event
.GetY() + (originY 
* unitY
)) 
1652     def _getObjectAndSelectionHandleAt(self
, pt
): 
1653         """ Return the object and selection handle at the given point. 
1655             We draw selection handles (small rectangles) around the currently 
1656             selected object(s).  If the given point is within one of the 
1657             selection handle rectangles, we return the associated object and a 
1658             code indicating which selection handle the point is in.  If the 
1659             point isn't within any selection handle at all, we return the tuple 
1660             (None, handle_NONE). 
1662         for obj 
in self
.selection
: 
1663             handle 
= obj
.getSelectionHandleContainingPoint(pt
.x
, pt
.y
) 
1664             if handle 
!= handle_NONE
: 
1667         return None, handle_NONE
 
1670     def _getObjectAt(self
, pt
): 
1671         """ Return the first object found which is at the given point. 
1673         for obj 
in self
.contents
: 
1674             if obj
.objectContainsPoint(pt
.x
, pt
.y
): 
1679     def _drawObjectOutline(self
, offsetX
, offsetY
): 
1680         """ Draw an outline of the currently selected object. 
1682             The selected object's outline is drawn at the object's position 
1683             plus the given offset. 
1685             Note that the outline is drawn by *inverting* the window's 
1686             contents, so calling _drawObjectOutline twice in succession will 
1687             restore the window's contents back to what they were previously. 
1689         if len(self
.selection
) != 1: return 
1691         position 
= self
.selection
[0].getPosition() 
1692         size     
= self
.selection
[0].getSize() 
1694         dc 
= wx
.ClientDC(self
.drawPanel
) 
1695         self
.drawPanel
.PrepareDC(dc
) 
1697         dc
.SetPen(wx
.BLACK_DASHED_PEN
) 
1698         dc
.SetBrush(wx
.TRANSPARENT_BRUSH
) 
1699         dc
.SetLogicalFunction(wx
.INVERT
) 
1701         dc
.DrawRectangle(position
.x 
+ offsetX
, position
.y 
+ offsetY
, 
1702                          size
.width
, size
.height
) 
1707     def _drawVisualFeedback(self
, startPt
, endPt
, type, dashedLine
): 
1708         """ Draw visual feedback for a drawing operation. 
1710             The visual feedback consists of a line, ellipse, or rectangle based 
1711             around the two given points.  'type' should be one of the following 
1712             predefined feedback type constants: 
1714                 feedback_RECT     ->  draw rectangular feedback. 
1715                 feedback_LINE     ->  draw line feedback. 
1716                 feedback_ELLIPSE  ->  draw elliptical feedback. 
1718             if 'dashedLine' is True, the feedback is drawn as a dashed rather 
1721             Note that the feedback is drawn by *inverting* the window's 
1722             contents, so calling _drawVisualFeedback twice in succession will 
1723             restore the window's contents back to what they were previously. 
1725         dc 
= wx
.ClientDC(self
.drawPanel
) 
1726         self
.drawPanel
.PrepareDC(dc
) 
1729             dc
.SetPen(wx
.BLACK_DASHED_PEN
) 
1731             dc
.SetPen(wx
.BLACK_PEN
) 
1732         dc
.SetBrush(wx
.TRANSPARENT_BRUSH
) 
1733         dc
.SetLogicalFunction(wx
.INVERT
) 
1735         if type == feedback_RECT
: 
1736             dc
.DrawRectangle(startPt
.x
, startPt
.y
, 
1737                              endPt
.x 
- startPt
.x
, 
1738                              endPt
.y 
- startPt
.y
) 
1739         elif type == feedback_LINE
: 
1740             dc
.DrawLine(startPt
.x
, startPt
.y
, endPt
.x
, endPt
.y
) 
1741         elif type == feedback_ELLIPSE
: 
1742             dc
.DrawEllipse(startPt
.x
, startPt
.y
, 
1743                            endPt
.x 
- startPt
.x
, 
1744                            endPt
.y 
- startPt
.y
) 
1748 #---------------------------------------------------------------------------- 
1750 class DrawingObject
: 
1751     """ An object within the drawing panel. 
1753         A pySketch document consists of a front-to-back ordered list of 
1754         DrawingObjects.  Each DrawingObject has the following properties: 
1756             'type'          What type of object this is (text, line, etc). 
1757             'position'      The position of the object within the document. 
1758             'size'          The size of the object within the document. 
1759             'penColour'     The colour to use for drawing the object's outline. 
1760             'fillColour'    Colour to use for drawing object's interior. 
1761             'lineSize'      Line width (in pixels) to use for object's outline. 
1762             'startPt'       The point, relative to the object's position, where 
1763                             an obj_LINE object's line should start. 
1764             'endPt'         The point, relative to the object's position, where 
1765                             an obj_LINE object's line should end. 
1766             'text'          The object's text (obj_TEXT objects only). 
1767             'textFont'      The text object's font name. 
1768             'textSize'      The text object's point size. 
1769             'textBoldface'  If True, this text object will be drawn in 
1771             'textItalic'    If True, this text object will be drawn in italic. 
1772             'textUnderline' If True, this text object will be drawn underlined. 
1775     # ================== 
1776     # == Constructors == 
1777     # ================== 
1779     def __init__(self
, type, position
=wx
.Point(0, 0), size
=wx
.Size(0, 0), 
1780                  penColour
=wx
.BLACK
, fillColour
=wx
.WHITE
, lineSize
=1, 
1781                  text
=None, startPt
=wx
.Point(0, 0), endPt
=wx
.Point(0,0)): 
1782         """ Standard constructor. 
1784             'type' is the type of object being created.  This should be one of 
1785             the following constants: 
1792             The remaining parameters let you set various options for the newly 
1793             created DrawingObject. 
1796         self
.position          
= position
 
1798         self
.penColour         
= penColour
 
1799         self
.fillColour        
= fillColour
 
1800         self
.lineSize          
= lineSize
 
1801         self
.startPt           
= startPt
 
1804         self
.textFont          
= wx
.SystemSettings_GetFont( 
1805                                     wx
.SYS_DEFAULT_GUI_FONT
).GetFaceName() 
1807         self
.textBoldface      
= False 
1808         self
.textItalic        
= False 
1809         self
.textUnderline     
= False 
1811     # ============================= 
1812     # == Object Property Methods == 
1813     # ============================= 
1816         """ Return a copy of the object's internal data. 
1818             This is used to save this DrawingObject to disk. 
1820         return [self
.type, self
.position
.x
, self
.position
.y
, 
1821                 self
.size
.width
, self
.size
.height
, 
1822                 self
.penColour
.Red(), 
1823                 self
.penColour
.Green(), 
1824                 self
.penColour
.Blue(), 
1825                 self
.fillColour
.Red(), 
1826                 self
.fillColour
.Green(), 
1827                 self
.fillColour
.Blue(), 
1829                 self
.startPt
.x
, self
.startPt
.y
, 
1830                 self
.endPt
.x
, self
.endPt
.y
, 
1839     def setData(self
, data
): 
1840         """ Set the object's internal data. 
1842             'data' is a copy of the object's saved data, as returned by 
1843             getData() above.  This is used to restore a previously saved 
1846         #data = copy.deepcopy(data) # Needed? 
1849         self
.position          
= wx
.Point(data
[1], data
[2]) 
1850         self
.size              
= wx
.Size(data
[3], data
[4]) 
1851         self
.penColour         
= wx
.Colour(red
=data
[5], 
1854         self
.fillColour        
= wx
.Colour(red
=data
[8], 
1857         self
.lineSize          
= data
[11] 
1858         self
.startPt           
= wx
.Point(data
[12], data
[13]) 
1859         self
.endPt             
= wx
.Point(data
[14], data
[15]) 
1860         self
.text              
= data
[16] 
1861         self
.textFont          
= data
[17] 
1862         self
.textSize          
= data
[18] 
1863         self
.textBoldface      
= data
[19] 
1864         self
.textItalic        
= data
[20] 
1865         self
.textUnderline     
= data
[21] 
1869         """ Return this DrawingObject's type. 
1874     def setPosition(self
, position
): 
1875         """ Set the origin (top-left corner) for this DrawingObject. 
1877         self
.position 
= position
 
1880     def getPosition(self
): 
1881         """ Return this DrawingObject's position. 
1883         return self
.position
 
1886     def setSize(self
, size
): 
1887         """ Set the size for this DrawingObject. 
1893         """ Return this DrawingObject's size. 
1898     def setPenColour(self
, colour
): 
1899         """ Set the pen colour used for this DrawingObject. 
1901         self
.penColour 
= colour
 
1904     def getPenColour(self
): 
1905         """ Return this DrawingObject's pen colour. 
1907         return self
.penColour
 
1910     def setFillColour(self
, colour
): 
1911         """ Set the fill colour used for this DrawingObject. 
1913         self
.fillColour 
= colour
 
1916     def getFillColour(self
): 
1917         """ Return this DrawingObject's fill colour. 
1919         return self
.fillColour
 
1922     def setLineSize(self
, lineSize
): 
1923         """ Set the linesize used for this DrawingObject. 
1925         self
.lineSize 
= lineSize
 
1928     def getLineSize(self
): 
1929         """ Return this DrawingObject's line size. 
1931         return self
.lineSize
 
1934     def setStartPt(self
, startPt
): 
1935         """ Set the starting point for this line DrawingObject. 
1937         self
.startPt 
= startPt
 
1940     def getStartPt(self
): 
1941         """ Return the starting point for this line DrawingObject. 
1946     def setEndPt(self
, endPt
): 
1947         """ Set the ending point for this line DrawingObject. 
1953         """ Return the ending point for this line DrawingObject. 
1958     def setText(self
, text
): 
1959         """ Set the text for this DrawingObject. 
1965         """ Return this DrawingObject's text. 
1970     def setTextFont(self
, font
): 
1971         """ Set the typeface for this text DrawingObject. 
1973         self
.textFont 
= font
 
1976     def getTextFont(self
): 
1977         """ Return this text DrawingObject's typeface. 
1979         return self
.textFont
 
1982     def setTextSize(self
, size
): 
1983         """ Set the point size for this text DrawingObject. 
1985         self
.textSize 
= size
 
1988     def getTextSize(self
): 
1989         """ Return this text DrawingObject's text size. 
1991         return self
.textSize
 
1994     def setTextBoldface(self
, boldface
): 
1995         """ Set the boldface flag for this text DrawingObject. 
1997         self
.textBoldface 
= boldface
 
2000     def getTextBoldface(self
): 
2001         """ Return this text DrawingObject's boldface flag. 
2003         return self
.textBoldface
 
2006     def setTextItalic(self
, italic
): 
2007         """ Set the italic flag for this text DrawingObject. 
2009         self
.textItalic 
= italic
 
2012     def getTextItalic(self
): 
2013         """ Return this text DrawingObject's italic flag. 
2015         return self
.textItalic
 
2018     def setTextUnderline(self
, underline
): 
2019         """ Set the underling flag for this text DrawingObject. 
2021         self
.textUnderline 
= underline
 
2024     def getTextUnderline(self
): 
2025         """ Return this text DrawingObject's underline flag. 
2027         return self
.textUnderline
 
2029     # ============================ 
2030     # == Object Drawing Methods == 
2031     # ============================ 
2033     def draw(self
, dc
, selected
): 
2034         """ Draw this DrawingObject into our window. 
2036             'dc' is the device context to use for drawing.  If 'selected' is 
2037             True, the object is currently selected and should be drawn as such. 
2039         if self
.type != obj_TEXT
: 
2040             if self
.lineSize 
== 0: 
2041                 dc
.SetPen(wx
.Pen(self
.penColour
, self
.lineSize
, wx
.TRANSPARENT
)) 
2043                 dc
.SetPen(wx
.Pen(self
.penColour
, self
.lineSize
, wx
.SOLID
)) 
2044             dc
.SetBrush(wx
.Brush(self
.fillColour
, wx
.SOLID
)) 
2046             dc
.SetTextForeground(self
.penColour
) 
2047             dc
.SetTextBackground(self
.fillColour
) 
2049         self
._privateDraw
(dc
, self
.position
, selected
) 
2051     # ======================= 
2052     # == Selection Methods == 
2053     # ======================= 
2055     def objectContainsPoint(self
, x
, y
): 
2056         """ Returns True iff this object contains the given point. 
2058             This is used to determine if the user clicked on the object. 
2060         # Firstly, ignore any points outside of the object's bounds. 
2062         if x 
< self
.position
.x
: return False 
2063         if x 
> self
.position
.x 
+ self
.size
.x
: return False 
2064         if y 
< self
.position
.y
: return False 
2065         if y 
> self
.position
.y 
+ self
.size
.y
: return False 
2067         if self
.type in [obj_RECT
, obj_TEXT
]: 
2068             # Rectangles and text are easy -- they're always selected if the 
2069             # point is within their bounds. 
2072         # Now things get tricky.  There's no straightforward way of knowing 
2073         # whether the point is within the object's bounds...to get around this, 
2074         # we draw the object into a memory-based bitmap and see if the given 
2075         # point was drawn.  This could no doubt be done more efficiently by 
2076         # some tricky maths, but this approach works and is simple enough. 
2078         bitmap 
= wx
.EmptyBitmap(self
.size
.x 
+ 10, self
.size
.y 
+ 10) 
2080         dc
.SelectObject(bitmap
) 
2082         dc
.SetBackground(wx
.WHITE_BRUSH
) 
2084         dc
.SetPen(wx
.Pen(wx
.BLACK
, self
.lineSize 
+ 5, wx
.SOLID
)) 
2085         dc
.SetBrush(wx
.BLACK_BRUSH
) 
2086         self
._privateDraw
(dc
, wx
.Point(5, 5), True) 
2088         pixel 
= dc
.GetPixel(x 
- self
.position
.x 
+ 5, y 
- self
.position
.y 
+ 5) 
2089         if (pixel
.Red() == 0) and (pixel
.Green() == 0) and (pixel
.Blue() == 0): 
2095     def getSelectionHandleContainingPoint(self
, x
, y
): 
2096         """ Return the selection handle containing the given point, if any. 
2098             We return one of the predefined selection handle ID codes. 
2100         if self
.type == obj_LINE
: 
2101             # We have selection handles at the start and end points. 
2102             if self
._pointInSelRect
(x
, y
, self
.position
.x 
+ self
.startPt
.x
, 
2103                                           self
.position
.y 
+ self
.startPt
.y
): 
2104                 return handle_START_POINT
 
2105             elif self
._pointInSelRect
(x
, y
, self
.position
.x 
+ self
.endPt
.x
, 
2106                                             self
.position
.y 
+ self
.endPt
.y
): 
2107                 return handle_END_POINT
 
2111             # We have selection handles at all four corners. 
2112             if self
._pointInSelRect
(x
, y
, self
.position
.x
, self
.position
.y
): 
2113                 return handle_TOP_LEFT
 
2114             elif self
._pointInSelRect
(x
, y
, self
.position
.x 
+ self
.size
.width
, 
2116                 return handle_TOP_RIGHT
 
2117             elif self
._pointInSelRect
(x
, y
, self
.position
.x
, 
2118                                             self
.position
.y 
+ self
.size
.height
): 
2119                 return handle_BOTTOM_LEFT
 
2120             elif self
._pointInSelRect
(x
, y
, self
.position
.x 
+ self
.size
.width
, 
2121                                             self
.position
.y 
+ self
.size
.height
): 
2122                 return handle_BOTTOM_RIGHT
 
2127     def objectWithinRect(self
, x
, y
, width
, height
): 
2128         """ Return True iff this object falls completely within the given rect. 
2130         if x          
> self
.position
.x
:                    return False 
2131         if x 
+ width  
< self
.position
.x 
+ self
.size
.width
:  return False 
2132         if y          
> self
.position
.y
:                    return False 
2133         if y 
+ height 
< self
.position
.y 
+ self
.size
.height
: return False 
2136     # ===================== 
2137     # == Utility Methods == 
2138     # ===================== 
2140     def fitToText(self
): 
2141         """ Resize a text DrawingObject so that it fits it's text exactly. 
2143         if self
.type != obj_TEXT
: return 
2145         if self
.textBoldface
: weight 
= wx
.BOLD
 
2146         else:                 weight 
= wx
.NORMAL
 
2147         if self
.textItalic
: style 
= wx
.ITALIC
 
2148         else:               style 
= wx
.NORMAL
 
2149         font 
= wx
.Font(self
.textSize
, wx
.DEFAULT
, style
, weight
, 
2150                       self
.textUnderline
, self
.textFont
) 
2152         dummyWindow 
= wx
.Frame(None, -1, "") 
2153         dummyWindow
.SetFont(font
) 
2154         width
, height 
= dummyWindow
.GetTextExtent(self
.text
) 
2155         dummyWindow
.Destroy() 
2157         self
.size 
= wx
.Size(width
, height
) 
2159     # ===================== 
2160     # == Private Methods == 
2161     # ===================== 
2163     def _privateDraw(self
, dc
, position
, selected
): 
2164         """ Private routine to draw this DrawingObject. 
2166             'dc' is the device context to use for drawing, while 'position' is 
2167             the position in which to draw the object.  If 'selected' is True, 
2168             the object is drawn with selection handles.  This private drawing 
2169             routine assumes that the pen and brush have already been set by the 
2172         if self
.type == obj_LINE
: 
2173             dc
.DrawLine(position
.x 
+ self
.startPt
.x
, 
2174                         position
.y 
+ self
.startPt
.y
, 
2175                         position
.x 
+ self
.endPt
.x
, 
2176                         position
.y 
+ self
.endPt
.y
) 
2177         elif self
.type == obj_RECT
: 
2178             dc
.DrawRectangle(position
.x
, position
.y
, 
2179                              self
.size
.width
, self
.size
.height
) 
2180         elif self
.type == obj_ELLIPSE
: 
2181             dc
.DrawEllipse(position
.x
, position
.y
, 
2182                            self
.size
.width
, self
.size
.height
) 
2183         elif self
.type == obj_TEXT
: 
2184             if self
.textBoldface
: weight 
= wx
.BOLD
 
2185             else:                 weight 
= wx
.NORMAL
 
2186             if self
.textItalic
: style 
= wx
.ITALIC
 
2187             else:               style 
= wx
.NORMAL
 
2188             font 
= wx
.Font(self
.textSize
, wx
.DEFAULT
, style
, weight
, 
2189                           self
.textUnderline
, self
.textFont
) 
2191             dc
.DrawText(self
.text
, position
.x
, position
.y
) 
2194             dc
.SetPen(wx
.TRANSPARENT_PEN
) 
2195             dc
.SetBrush(wx
.BLACK_BRUSH
) 
2197             if self
.type == obj_LINE
: 
2198                 # Draw selection handles at the start and end points. 
2199                 self
._drawSelHandle
(dc
, position
.x 
+ self
.startPt
.x
, 
2200                                         position
.y 
+ self
.startPt
.y
) 
2201                 self
._drawSelHandle
(dc
, position
.x 
+ self
.endPt
.x
, 
2202                                         position
.y 
+ self
.endPt
.y
) 
2204                 # Draw selection handles at all four corners. 
2205                 self
._drawSelHandle
(dc
, position
.x
, position
.y
) 
2206                 self
._drawSelHandle
(dc
, position
.x 
+ self
.size
.width
, 
2208                 self
._drawSelHandle
(dc
, position
.x
, 
2209                                         position
.y 
+ self
.size
.height
) 
2210                 self
._drawSelHandle
(dc
, position
.x 
+ self
.size
.width
, 
2211                                         position
.y 
+ self
.size
.height
) 
2214     def _drawSelHandle(self
, dc
, x
, y
): 
2215         """ Draw a selection handle around this DrawingObject. 
2217             'dc' is the device context to draw the selection handle within, 
2218             while 'x' and 'y' are the coordinates to use for the centre of the 
2221         dc
.DrawRectangle(x 
- 3, y 
- 3, 6, 6) 
2224     def _pointInSelRect(self
, x
, y
, rX
, rY
): 
2225         """ Return True iff (x, y) is within the selection handle at (rX, ry). 
2227         if   x 
< rX 
- 3: return False 
2228         elif x 
> rX 
+ 3: return False 
2229         elif y 
< rY 
- 3: return False 
2230         elif y 
> rY 
+ 3: return False 
2233 #---------------------------------------------------------------------------- 
2235 class ToolPaletteIcon(wx
.BitmapButton
): 
2236     """ An icon appearing in the tool palette area of our sketching window. 
2238         Note that this is actually implemented as a wx.Bitmap rather 
2239         than as a wx.Icon.  wx.Icon has a very specific meaning, and isn't 
2240         appropriate for this more general use. 
2243     def __init__(self
, parent
, iconID
, iconName
, toolTip
): 
2244         """ Standard constructor. 
2246             'parent'   is the parent window this icon will be part of. 
2247             'iconID'   is the internal ID used for this icon. 
2248             'iconName' is the name used for this icon. 
2249             'toolTip'  is the tool tip text to show for this icon. 
2251             The icon name is used to get the appropriate bitmap for this icon. 
2253         bmp 
= wx
.Bitmap("images/" + iconName 
+ "Icon.bmp", wx
.BITMAP_TYPE_BMP
) 
2254         wx
.BitmapButton
.__init
__(self
, parent
, iconID
, bmp
, wx
.DefaultPosition
, 
2255                                 wx
.Size(bmp
.GetWidth(), bmp
.GetHeight())) 
2256         self
.SetToolTip(wx
.ToolTip(toolTip
)) 
2258         self
.iconID     
= iconID
 
2259         self
.iconName   
= iconName
 
2260         self
.isSelected 
= False 
2264         """ Select the icon. 
2266             The icon's visual representation is updated appropriately. 
2268         if self
.isSelected
: return # Nothing to do! 
2270         bmp 
= wx
.Bitmap("images/" + self
.iconName 
+ "IconSel.bmp", 
2272         self
.SetBitmapLabel(bmp
) 
2273         self
.isSelected 
= True 
2277         """ Deselect the icon. 
2279             The icon's visual representation is updated appropriately. 
2281         if not self
.isSelected
: return # Nothing to do! 
2283         bmp 
= wx
.Bitmap("images/" + self
.iconName 
+ "Icon.bmp", 
2285         self
.SetBitmapLabel(bmp
) 
2286         self
.isSelected 
= False 
2288 #---------------------------------------------------------------------------- 
2290 class ToolOptionIndicator(wx
.Window
): 
2291     """ A visual indicator which shows the current tool options. 
2293     def __init__(self
, parent
): 
2294         """ Standard constructor. 
2296         wx
.Window
.__init
__(self
, parent
, -1, wx
.DefaultPosition
, wx
.Size(52, 32)) 
2298         self
.penColour  
= wx
.BLACK
 
2299         self
.fillColour 
= wx
.WHITE
 
2302         self
.Bind(wx
.EVT_PAINT
, self
.OnPaint
) 
2305     def setPenColour(self
, penColour
): 
2306         """ Set the indicator's current pen colour. 
2308         self
.penColour 
= penColour
 
2312     def setFillColour(self
, fillColour
): 
2313         """ Set the indicator's current fill colour. 
2315         self
.fillColour 
= fillColour
 
2319     def setLineSize(self
, lineSize
): 
2320         """ Set the indicator's current pen colour. 
2322         self
.lineSize 
= lineSize
 
2326     def OnPaint(self
, event
): 
2327         """ Paint our tool option indicator. 
2329         dc 
= wx
.PaintDC(self
) 
2332         if self
.lineSize 
== 0: 
2333             dc
.SetPen(wx
.Pen(self
.penColour
, self
.lineSize
, wx
.TRANSPARENT
)) 
2335             dc
.SetPen(wx
.Pen(self
.penColour
, self
.lineSize
, wx
.SOLID
)) 
2336         dc
.SetBrush(wx
.Brush(self
.fillColour
, wx
.SOLID
)) 
2338         dc
.DrawRectangle(5, 5, self
.GetSize().width 
- 10, 
2339                                self
.GetSize().height 
- 10) 
2343 #---------------------------------------------------------------------------- 
2345 class EditTextObjectDialog(wx
.Dialog
): 
2346     """ Dialog box used to edit the properties of a text object. 
2348         The user can edit the object's text, font, size, and text style. 
2351     def __init__(self
, parent
, title
): 
2352         """ Standard constructor. 
2354         wx
.Dialog
.__init
__(self
, parent
, -1, title
) 
2356         self
.textCtrl 
= wx
.TextCtrl(self
, 1001, "", style
=wx
.TE_PROCESS_ENTER
, 
2357                                    validator
=TextObjectValidator()) 
2358         extent 
= self
.textCtrl
.GetFullTextExtent("Hy") 
2359         lineHeight 
= extent
[1] + extent
[3] 
2360         self
.textCtrl
.SetSize(wx
.Size(-1, lineHeight 
* 4)) 
2362         self
.Bind(wx
.EVT_TEXT_ENTER
, self
._doEnter
, id=1001) 
2364         fonts 
= wx
.FontEnumerator() 
2365         fonts
.EnumerateFacenames() 
2366         self
.fontList 
= fonts
.GetFacenames() 
2367         self
.fontList
.sort() 
2369         fontLabel 
= wx
.StaticText(self
, -1, "Font:") 
2370         self
._setFontOptions
(fontLabel
, weight
=wx
.BOLD
) 
2372         self
.fontCombo 
= wx
.ComboBox(self
, -1, "", wx
.DefaultPosition
, 
2373                                     wx
.DefaultSize
, self
.fontList
, 
2374                                     style 
= wx
.CB_READONLY
) 
2375         self
.fontCombo
.SetSelection(0) # Default to first available font. 
2377         self
.sizeList 
= ["8", "9", "10", "12", "14", "16", 
2378                          "18", "20", "24", "32", "48", "72"] 
2380         sizeLabel 
= wx
.StaticText(self
, -1, "Size:") 
2381         self
._setFontOptions
(sizeLabel
, weight
=wx
.BOLD
) 
2383         self
.sizeCombo 
= wx
.ComboBox(self
, -1, "", wx
.DefaultPosition
, 
2384                                     wx
.DefaultSize
, self
.sizeList
, 
2385                                     style
=wx
.CB_READONLY
) 
2386         self
.sizeCombo
.SetSelection(3) # Default to 12 point text. 
2388         gap 
= wx
.LEFT | wx
.TOP | wx
.RIGHT
 
2390         comboSizer 
= wx
.BoxSizer(wx
.HORIZONTAL
) 
2391         comboSizer
.Add(fontLabel
,      0, gap | wx
.ALIGN_CENTRE_VERTICAL
, 5) 
2392         comboSizer
.Add(self
.fontCombo
, 0, gap
, 5) 
2393         comboSizer
.Add((5, 5)) # Spacer. 
2394         comboSizer
.Add(sizeLabel
,      0, gap | wx
.ALIGN_CENTRE_VERTICAL
, 5) 
2395         comboSizer
.Add(self
.sizeCombo
, 0, gap
, 5) 
2397         self
.boldCheckbox      
= wx
.CheckBox(self
, -1, "Bold") 
2398         self
.italicCheckbox    
= wx
.CheckBox(self
, -1, "Italic") 
2399         self
.underlineCheckbox 
= wx
.CheckBox(self
, -1, "Underline") 
2401         self
._setFontOptions
(self
.boldCheckbox
,      weight
=wx
.BOLD
) 
2402         self
._setFontOptions
(self
.italicCheckbox
,    style
=wx
.ITALIC
) 
2403         self
._setFontOptions
(self
.underlineCheckbox
, underline
=True) 
2405         styleSizer 
= wx
.BoxSizer(wx
.HORIZONTAL
) 
2406         styleSizer
.Add(self
.boldCheckbox
,      0, gap
, 5) 
2407         styleSizer
.Add(self
.italicCheckbox
,    0, gap
, 5) 
2408         styleSizer
.Add(self
.underlineCheckbox
, 0, gap
, 5) 
2410         self
.okButton     
= wx
.Button(self
, wx
.ID_OK
,     "OK") 
2411         self
.cancelButton 
= wx
.Button(self
, wx
.ID_CANCEL
, "Cancel") 
2413         btnSizer 
= wx
.BoxSizer(wx
.HORIZONTAL
) 
2414         btnSizer
.Add(self
.okButton
,     0, gap
, 5) 
2415         btnSizer
.Add(self
.cancelButton
, 0, gap
, 5) 
2417         sizer 
= wx
.BoxSizer(wx
.VERTICAL
) 
2418         sizer
.Add(self
.textCtrl
, 1, gap | wx
.EXPAND
,       5) 
2419         sizer
.Add((10, 10)) # Spacer. 
2420         sizer
.Add(comboSizer
,    0, gap | wx
.ALIGN_CENTRE
, 5) 
2421         sizer
.Add(styleSizer
,    0, gap | wx
.ALIGN_CENTRE
, 5) 
2422         sizer
.Add((10, 10)) # Spacer. 
2423         sizer
.Add(btnSizer
,      0, gap | wx
.ALIGN_CENTRE
, 5) 
2425         self
.SetAutoLayout(True) 
2426         self
.SetSizer(sizer
) 
2429         self
.textCtrl
.SetFocus() 
2432     def objectToDialog(self
, obj
): 
2433         """ Copy the properties of the given text object into the dialog box. 
2435         self
.textCtrl
.SetValue(obj
.getText()) 
2436         self
.textCtrl
.SetSelection(0, len(obj
.getText())) 
2438         for i 
in range(len(self
.fontList
)): 
2439             if self
.fontList
[i
] == obj
.getTextFont(): 
2440                 self
.fontCombo
.SetSelection(i
) 
2443         for i 
in range(len(self
.sizeList
)): 
2444             if self
.sizeList
[i
] == str(obj
.getTextSize()): 
2445                 self
.sizeCombo
.SetSelection(i
) 
2448         self
.boldCheckbox
.SetValue(obj
.getTextBoldface()) 
2449         self
.italicCheckbox
.SetValue(obj
.getTextItalic()) 
2450         self
.underlineCheckbox
.SetValue(obj
.getTextUnderline()) 
2453     def dialogToObject(self
, obj
): 
2454         """ Copy the properties from the dialog box into the given text object. 
2456         obj
.setText(self
.textCtrl
.GetValue()) 
2457         obj
.setTextFont(self
.fontCombo
.GetValue()) 
2458         obj
.setTextSize(int(self
.sizeCombo
.GetValue())) 
2459         obj
.setTextBoldface(self
.boldCheckbox
.GetValue()) 
2460         obj
.setTextItalic(self
.italicCheckbox
.GetValue()) 
2461         obj
.setTextUnderline(self
.underlineCheckbox
.GetValue()) 
2464     # ====================== 
2465     # == Private Routines == 
2466     # ====================== 
2468     def _setFontOptions(self
, ctrl
, family
=None, pointSize
=-1, 
2469                                     style
=wx
.NORMAL
, weight
=wx
.NORMAL
, 
2471         """ Change the font settings for the given control. 
2473             The meaning of the 'family', 'pointSize', 'style', 'weight' and 
2474             'underline' parameters are the same as for the wx.Font constructor. 
2475             If the family and/or pointSize isn't specified, the current default 
2478         if family 
== None: family 
= ctrl
.GetFont().GetFamily() 
2479         if pointSize 
== -1: pointSize 
= ctrl
.GetFont().GetPointSize() 
2481         ctrl
.SetFont(wx
.Font(pointSize
, family
, style
, weight
, underline
)) 
2482         ctrl
.SetSize(ctrl
.GetBestSize()) # Adjust size to reflect font change. 
2485     def _doEnter(self
, event
): 
2486         """ Respond to the user hitting the ENTER key. 
2488             We simulate clicking on the "OK" button. 
2490         if self
.Validate(): self
.Show(False) 
2492 #---------------------------------------------------------------------------- 
2494 class TextObjectValidator(wx
.PyValidator
): 
2495     """ This validator is used to ensure that the user has entered something 
2496         into the text object editor dialog's text field. 
2499         """ Standard constructor. 
2501         wx
.PyValidator
.__init
__(self
) 
2505         """ Standard cloner. 
2507             Note that every validator must implement the Clone() method. 
2509         return TextObjectValidator() 
2512     def Validate(self
, win
): 
2513         """ Validate the contents of the given text control. 
2515         textCtrl 
= self
.GetWindow() 
2516         text 
= textCtrl
.GetValue() 
2519             wx
.MessageBox("A text object must contain some text!", "Error") 
2525     def TransferToWindow(self
): 
2526         """ Transfer data from validator to window. 
2528             The default implementation returns False, indicating that an error 
2529             occurred.  We simply return True, as we don't do any data transfer. 
2531         return True # Prevent wx.Dialog from complaining. 
2534     def TransferFromWindow(self
): 
2535         """ Transfer data from window to validator. 
2537             The default implementation returns False, indicating that an error 
2538             occurred.  We simply return True, as we don't do any data transfer. 
2540         return True # Prevent wx.Dialog from complaining. 
2542 #---------------------------------------------------------------------------- 
2544 class ExceptionHandler
: 
2545     """ A simple error-handling class to write exceptions to a text file. 
2547         Under MS Windows, the standard DOS console window doesn't scroll and 
2548         closes as soon as the application exits, making it hard to find and 
2549         view Python exceptions.  This utility class allows you to handle Python 
2550         exceptions in a more friendly manner. 
2554         """ Standard constructor. 
2557         if os
.path
.exists("errors.txt"): 
2558             os
.remove("errors.txt") # Delete previous error log, if any. 
2562         """ Write the given error message to a text file. 
2564             Note that if the error message doesn't end in a carriage return, we 
2565             have to buffer up the inputs until a carriage return is received. 
2567         if (s
[-1] != "\n") and (s
[-1] != "\r"): 
2568             self
._buff 
= self
._buff 
+ s
 
2575             if s
[:9] == "Traceback": 
2576                 # Tell the user than an exception occurred. 
2577                 wx
.MessageBox("An internal error has occurred.\nPlease " + \
 
2578                              "refer to the 'errors.txt' file for details.", 
2579                              "Error", wx
.OK | wx
.CENTRE | wx
.ICON_EXCLAMATION
) 
2581             f 
= open("errors.txt", "a") 
2585             pass # Don't recursively crash on errors. 
2587 #---------------------------------------------------------------------------- 
2589 class SketchApp(wx
.App
): 
2590     """ The main pySketch application object. 
2593         """ Initialise the application. 
2598         if len(sys
.argv
) == 1: 
2599             # No file name was specified on the command line -> start with a 
2601             frame 
= DrawingFrame(None, -1, "Untitled") 
2604             _docList
.append(frame
) 
2606             # Load the file(s) specified on the command line. 
2607             for arg 
in sys
.argv
[1:]: 
2608                 fileName 
= os
.path
.join(os
.getcwd(), arg
) 
2609                 if os
.path
.isfile(fileName
): 
2610                     frame 
= DrawingFrame(None, -1, 
2611                                          os
.path
.basename(fileName
), 
2614                     _docList
.append(frame
) 
2618 #---------------------------------------------------------------------------- 
2621     """ Start up the pySketch application. 
2625     # Redirect python exceptions to a log file. 
2627     sys
.stderr 
= ExceptionHandler() 
2629     # Create and start the pySketch application. 
2635 if __name__ 
== "__main__":