3 A simple object-oriented drawing program.
5 This is completely free software; please feel free to adapt or use this in
8 Original Author: Erik Westra (ewestra@wave.co.nz)
10 Other contributors: Bill Baxter (wbaxter@gmail.com)
12 #########################################################################
16 pySketch requires wxPython version 2.3. If you are running an earlier
17 version, you need to patch your copy of wxPython to fix a bug which will
18 cause the "Edit Text Object" dialog box to crash.
20 To patch an earlier version of wxPython, edit the wxPython/windows.py file,
21 find the wxPyValidator.__init__ method and change the line which reads:
23 self._setSelf(self, wxPyValidator, 0)
27 self._setSelf(self, wxPyValidator, 1)
29 This fixes a known bug in wxPython 2.2.5 (and possibly earlier) which has
30 now been fixed in wxPython 2.3.
32 #########################################################################
36 * Add ARGV checking to see if a document was double-clicked on.
40 * Scrolling the window causes the drawing panel to be mucked up until you
41 refresh it. I've got no idea why.
43 * I suspect that the reference counting for some wxPoint objects is
44 getting mucked up; when the user quits, we get errors about being
45 unable to call del on a 'None' object.
47 * Saving files via pickling is not a robust cross-platform solution.
50 import cPickle
, os
.path
53 from wx
.lib
.buttons
import GenBitmapButton
,GenBitmapToggleButton
56 import traceback
, types
58 #----------------------------------------------------------------------------
60 #----------------------------------------------------------------------------
64 menu_DUPLICATE
= wx
.NewId() # Edit menu items.
65 menu_EDIT_PROPS
= wx
.NewId()
67 menu_SELECT
= wx
.NewId() # Tools menu items.
68 menu_LINE
= wx
.NewId()
69 menu_POLYGON
= wx
.NewId()
70 menu_RECT
= wx
.NewId()
71 menu_ELLIPSE
= wx
.NewId()
72 menu_TEXT
= wx
.NewId()
74 menu_DC
= wx
.NewId() # View menu items.
75 menu_GCDC
= wx
.NewId()
77 menu_MOVE_FORWARD
= wx
.NewId() # Object menu items.
78 menu_MOVE_TO_FRONT
= wx
.NewId()
79 menu_MOVE_BACKWARD
= wx
.NewId()
80 menu_MOVE_TO_BACK
= wx
.NewId()
82 menu_ABOUT
= wx
.NewId() # Help menu items.
86 id_SELECT
= wx
.NewId()
88 id_POLYGON
= wx
.NewId()
89 id_SCRIBBLE
= wx
.NewId()
91 id_ELLIPSE
= wx
.NewId()
94 # Our tool option IDs:
96 id_FILL_OPT
= wx
.NewId()
97 id_PEN_OPT
= wx
.NewId()
98 id_LINE_OPT
= wx
.NewId()
100 id_LINESIZE_0
= wx
.NewId()
101 id_LINESIZE_1
= wx
.NewId()
102 id_LINESIZE_2
= wx
.NewId()
103 id_LINESIZE_3
= wx
.NewId()
104 id_LINESIZE_4
= wx
.NewId()
105 id_LINESIZE_5
= wx
.NewId()
107 # Size of the drawing page, in pixels.
112 #----------------------------------------------------------------------------
114 class DrawingFrame(wx
.Frame
):
115 """ A frame showing the contents of a single document. """
117 # ==========================================
118 # == Initialisation and Window Management ==
119 # ==========================================
121 def __init__(self
, parent
, id, title
, fileName
=None):
122 """ Standard constructor.
124 'parent', 'id' and 'title' are all passed to the standard wx.Frame
125 constructor. 'fileName' is the name and path of a saved file to
126 load into this frame, if any.
128 wx
.Frame
.__init
__(self
, parent
, id, title
,
129 style
= wx
.DEFAULT_FRAME_STYLE | wx
.WANTS_CHARS |
130 wx
.NO_FULL_REPAINT_ON_RESIZE
)
132 # Setup our menu bar.
133 menuBar
= wx
.MenuBar()
135 self
.fileMenu
= wx
.Menu()
136 self
.fileMenu
.Append(wx
.ID_NEW
, "New\tCtrl-N", "Create a new document")
137 self
.fileMenu
.Append(wx
.ID_OPEN
, "Open...\tCtrl-O", "Open an existing document")
138 self
.fileMenu
.Append(wx
.ID_CLOSE
, "Close\tCtrl-W")
139 self
.fileMenu
.AppendSeparator()
140 self
.fileMenu
.Append(wx
.ID_SAVE
, "Save\tCtrl-S")
141 self
.fileMenu
.Append(wx
.ID_SAVEAS
, "Save As...")
142 self
.fileMenu
.Append(wx
.ID_REVERT
, "Revert...")
143 self
.fileMenu
.AppendSeparator()
144 self
.fileMenu
.Append(wx
.ID_EXIT
, "Quit\tCtrl-Q")
146 menuBar
.Append(self
.fileMenu
, "File")
148 self
.editMenu
= wx
.Menu()
149 self
.editMenu
.Append(wx
.ID_UNDO
, "Undo\tCtrl-Z")
150 self
.editMenu
.Append(wx
.ID_REDO
, "Redo\tCtrl-Y")
151 self
.editMenu
.AppendSeparator()
152 self
.editMenu
.Append(wx
.ID_SELECTALL
, "Select All\tCtrl-A")
153 self
.editMenu
.AppendSeparator()
154 self
.editMenu
.Append(menu_DUPLICATE
, "Duplicate\tCtrl-D")
155 self
.editMenu
.Append(menu_EDIT_PROPS
,"Edit...\tCtrl-E", "Edit object properties")
156 self
.editMenu
.Append(wx
.ID_CLEAR
, "Delete\tDel")
158 menuBar
.Append(self
.editMenu
, "Edit")
160 self
.viewMenu
= wx
.Menu()
161 self
.viewMenu
.Append(menu_DC
, "Normal quality",
162 "Normal rendering using wx.DC",
164 self
.viewMenu
.Append(menu_GCDC
,"High quality",
165 "Anti-aliased rendering using wx.GCDC",
168 menuBar
.Append(self
.viewMenu
, "View")
170 self
.toolsMenu
= wx
.Menu()
171 self
.toolsMenu
.Append(id_SELECT
, "Selection", kind
=wx
.ITEM_RADIO
)
172 self
.toolsMenu
.Append(id_LINE
, "Line", kind
=wx
.ITEM_RADIO
)
173 self
.toolsMenu
.Append(id_POLYGON
, "Polygon", kind
=wx
.ITEM_RADIO
)
174 self
.toolsMenu
.Append(id_SCRIBBLE
,"Scribble", kind
=wx
.ITEM_RADIO
)
175 self
.toolsMenu
.Append(id_RECT
, "Rectangle", kind
=wx
.ITEM_RADIO
)
176 self
.toolsMenu
.Append(id_ELLIPSE
, "Ellipse", kind
=wx
.ITEM_RADIO
)
177 self
.toolsMenu
.Append(id_TEXT
, "Text", kind
=wx
.ITEM_RADIO
)
179 menuBar
.Append(self
.toolsMenu
, "Tools")
181 self
.objectMenu
= wx
.Menu()
182 self
.objectMenu
.Append(menu_MOVE_FORWARD
, "Move Forward")
183 self
.objectMenu
.Append(menu_MOVE_TO_FRONT
, "Move to Front\tCtrl-F")
184 self
.objectMenu
.Append(menu_MOVE_BACKWARD
, "Move Backward")
185 self
.objectMenu
.Append(menu_MOVE_TO_BACK
, "Move to Back\tCtrl-B")
187 menuBar
.Append(self
.objectMenu
, "Object")
189 self
.helpMenu
= wx
.Menu()
190 self
.helpMenu
.Append(menu_ABOUT
, "About pySketch...")
192 menuBar
.Append(self
.helpMenu
, "Help")
194 self
.SetMenuBar(menuBar
)
196 # Create our statusbar
198 self
.CreateStatusBar()
200 # Create our toolbar.
203 self
.toolbar
= self
.CreateToolBar(wx
.TB_HORIZONTAL | wx
.NO_BORDER | wx
.TB_FLAT
)
205 artBmp
= wx
.ArtProvider
.GetBitmap
206 self
.toolbar
.AddSimpleTool(
207 wx
.ID_NEW
, artBmp(wx
.ART_NEW
, wx
.ART_TOOLBAR
, tsize
), "New")
208 self
.toolbar
.AddSimpleTool(
209 wx
.ID_OPEN
, artBmp(wx
.ART_FILE_OPEN
, wx
.ART_TOOLBAR
, tsize
), "Open")
210 self
.toolbar
.AddSimpleTool(
211 wx
.ID_SAVE
, artBmp(wx
.ART_FILE_SAVE
, wx
.ART_TOOLBAR
, tsize
), "Save")
212 self
.toolbar
.AddSimpleTool(
213 wx
.ID_SAVEAS
, artBmp(wx
.ART_FILE_SAVE_AS
, wx
.ART_TOOLBAR
, tsize
),
216 self
.toolbar
.AddSeparator()
217 self
.toolbar
.AddSimpleTool(
218 wx
.ID_UNDO
, artBmp(wx
.ART_UNDO
, wx
.ART_TOOLBAR
, tsize
), "Undo")
219 self
.toolbar
.AddSimpleTool(
220 wx
.ID_REDO
, artBmp(wx
.ART_REDO
, wx
.ART_TOOLBAR
, tsize
), "Redo")
221 self
.toolbar
.AddSeparator()
222 self
.toolbar
.AddSimpleTool(
223 menu_DUPLICATE
, wx
.Bitmap("images/duplicate.bmp", wx
.BITMAP_TYPE_BMP
),
226 self
.toolbar
.AddSeparator()
227 self
.toolbar
.AddSimpleTool(
228 menu_MOVE_FORWARD
, wx
.Bitmap("images/moveForward.bmp", wx
.BITMAP_TYPE_BMP
),
230 self
.toolbar
.AddSimpleTool(
231 menu_MOVE_BACKWARD
, wx
.Bitmap("images/moveBack.bmp", wx
.BITMAP_TYPE_BMP
),
234 self
.toolbar
.Realize()
236 # Associate menu/toolbar items with their handlers.
238 (wx
.ID_NEW
, self
.doNew
),
239 (wx
.ID_OPEN
, self
.doOpen
),
240 (wx
.ID_CLOSE
, self
.doClose
),
241 (wx
.ID_SAVE
, self
.doSave
),
242 (wx
.ID_SAVEAS
, self
.doSaveAs
),
243 (wx
.ID_REVERT
, self
.doRevert
),
244 (wx
.ID_EXIT
, self
.doExit
),
246 (wx
.ID_UNDO
, self
.doUndo
),
247 (wx
.ID_REDO
, self
.doRedo
),
248 (wx
.ID_SELECTALL
, self
.doSelectAll
),
249 (menu_DUPLICATE
, self
.doDuplicate
),
250 (menu_EDIT_PROPS
, self
.doEditObject
),
251 (wx
.ID_CLEAR
, self
.doDelete
),
253 (id_SELECT
, self
.onChooseTool
, self
.updChooseTool
),
254 (id_LINE
, self
.onChooseTool
, self
.updChooseTool
),
255 (id_POLYGON
, self
.onChooseTool
, self
.updChooseTool
),
256 (id_SCRIBBLE
,self
.onChooseTool
, self
.updChooseTool
),
257 (id_RECT
, self
.onChooseTool
, self
.updChooseTool
),
258 (id_ELLIPSE
, self
.onChooseTool
, self
.updChooseTool
),
259 (id_TEXT
, self
.onChooseTool
, self
.updChooseTool
),
261 (menu_DC
, self
.doChooseQuality
),
262 (menu_GCDC
, self
.doChooseQuality
),
264 (menu_MOVE_FORWARD
, self
.doMoveForward
),
265 (menu_MOVE_TO_FRONT
, self
.doMoveToFront
),
266 (menu_MOVE_BACKWARD
, self
.doMoveBackward
),
267 (menu_MOVE_TO_BACK
, self
.doMoveToBack
),
269 (menu_ABOUT
, self
.doShowAbout
)]
270 for combo
in menuHandlers
:
271 id, handler
= combo
[:2]
272 self
.Bind(wx
.EVT_MENU
, handler
, id = id)
274 self
.Bind(wx
.EVT_UPDATE_UI
, combo
[2], id = id)
276 # Install our own method to handle closing the window. This allows us
277 # to ask the user if he/she wants to save before closing the window, as
278 # well as keeping track of which windows are currently open.
280 self
.Bind(wx
.EVT_CLOSE
, self
.doClose
)
282 # Install our own method for handling keystrokes. We use this to let
283 # the user move the selected object(s) around using the arrow keys.
285 self
.Bind(wx
.EVT_CHAR_HOOK
, self
.onKeyEvent
)
287 # Setup our top-most panel. This holds the entire contents of the
288 # window, excluding the menu bar.
290 self
.topPanel
= wx
.Panel(self
, -1, style
=wx
.SIMPLE_BORDER
)
292 # Setup our tool palette, with all our drawing tools and option icons.
294 self
.toolPalette
= wx
.BoxSizer(wx
.VERTICAL
)
296 self
.selectIcon
= ToolPaletteToggle(self
.topPanel
, id_SELECT
,
297 "select", "Selection Tool", mode
=wx
.ITEM_RADIO
)
298 self
.lineIcon
= ToolPaletteToggle(self
.topPanel
, id_LINE
,
299 "line", "Line Tool", mode
=wx
.ITEM_RADIO
)
300 self
.polygonIcon
= ToolPaletteToggle(self
.topPanel
, id_POLYGON
,
301 "polygon", "Polygon Tool", mode
=wx
.ITEM_RADIO
)
302 self
.scribbleIcon
= ToolPaletteToggle(self
.topPanel
, id_SCRIBBLE
,
303 "scribble", "Scribble Tool", mode
=wx
.ITEM_RADIO
)
304 self
.rectIcon
= ToolPaletteToggle(self
.topPanel
, id_RECT
,
305 "rect", "Rectangle Tool", mode
=wx
.ITEM_RADIO
)
306 self
.ellipseIcon
= ToolPaletteToggle(self
.topPanel
, id_ELLIPSE
,
307 "ellipse", "Ellipse Tool", mode
=wx
.ITEM_RADIO
)
308 self
.textIcon
= ToolPaletteToggle(self
.topPanel
, id_TEXT
,
309 "text", "Text Tool", mode
=wx
.ITEM_RADIO
)
313 'select' : (self
.selectIcon
, SelectDrawingTool()),
314 'line' : (self
.lineIcon
, LineDrawingTool()),
315 'polygon' : (self
.polygonIcon
, PolygonDrawingTool()),
316 'scribble': (self
.scribbleIcon
, ScribbleDrawingTool()),
317 'rect' : (self
.rectIcon
, RectDrawingTool()),
318 'ellipse' : (self
.ellipseIcon
, EllipseDrawingTool()),
319 'text' : (self
.textIcon
, TextDrawingTool())
323 toolSizer
= wx
.GridSizer(0, 2, 5, 5)
324 toolSizer
.Add(self
.selectIcon
)
325 toolSizer
.Add(self
.lineIcon
)
326 toolSizer
.Add(self
.rectIcon
)
327 toolSizer
.Add(self
.ellipseIcon
)
328 toolSizer
.Add(self
.polygonIcon
)
329 toolSizer
.Add(self
.scribbleIcon
)
330 toolSizer
.Add(self
.textIcon
)
332 self
.optionIndicator
= ToolOptionIndicator(self
.topPanel
)
333 self
.optionIndicator
.SetToolTip(
334 wx
.ToolTip("Shows Current Pen/Fill/Line Size Settings"))
336 optionSizer
= wx
.BoxSizer(wx
.HORIZONTAL
)
338 self
.penOptIcon
= ToolPaletteButton(self
.topPanel
, id_PEN_OPT
,
339 "penOpt", "Set Pen Colour",)
340 self
.fillOptIcon
= ToolPaletteButton(self
.topPanel
, id_FILL_OPT
,
341 "fillOpt", "Set Fill Colour")
342 self
.lineOptIcon
= ToolPaletteButton(self
.topPanel
, id_LINE_OPT
,
343 "lineOpt", "Set Line Size")
345 margin
= wx
.LEFT | wx
.RIGHT
346 optionSizer
.Add(self
.penOptIcon
, 0, margin
, 1)
347 optionSizer
.Add(self
.fillOptIcon
, 0, margin
, 1)
348 optionSizer
.Add(self
.lineOptIcon
, 0, margin
, 1)
350 margin
= wx
.TOP | wx
.LEFT | wx
.RIGHT | wx
.ALIGN_CENTRE
351 self
.toolPalette
.Add(toolSizer
, 0, margin
, 5)
352 self
.toolPalette
.Add((0, 0), 0, margin
, 5) # Spacer.
353 self
.toolPalette
.Add(self
.optionIndicator
, 0, margin
, 5)
354 self
.toolPalette
.Add(optionSizer
, 0, margin
, 5)
356 # Make the tool palette icons respond when the user clicks on them.
358 for tool
in self
.tools
.itervalues():
359 tool
[0].Bind(wx
.EVT_BUTTON
, self
.onChooseTool
)
361 self
.selectIcon
.Bind(wx
.EVT_BUTTON
, self
.onChooseTool
)
362 self
.lineIcon
.Bind(wx
.EVT_BUTTON
, self
.onChooseTool
)
365 self
.penOptIcon
.Bind(wx
.EVT_BUTTON
, self
.onPenOptionIconClick
)
366 self
.fillOptIcon
.Bind(wx
.EVT_BUTTON
, self
.onFillOptionIconClick
)
367 self
.lineOptIcon
.Bind(wx
.EVT_BUTTON
, self
.onLineOptionIconClick
)
369 # Setup the main drawing area.
371 self
.drawPanel
= wx
.ScrolledWindow(self
.topPanel
, -1,
372 style
=wx
.SUNKEN_BORDER|wx
.NO_FULL_REPAINT_ON_RESIZE
)
373 self
.drawPanel
.SetBackgroundColour(wx
.WHITE
)
375 self
.drawPanel
.EnableScrolling(True, True)
376 self
.drawPanel
.SetScrollbars(20, 20, PAGE_WIDTH
/ 20, PAGE_HEIGHT
/ 20)
378 self
.drawPanel
.Bind(wx
.EVT_MOUSE_EVENTS
, self
.onMouseEvent
)
380 self
.drawPanel
.Bind(wx
.EVT_IDLE
, self
.onIdle
)
381 self
.drawPanel
.Bind(wx
.EVT_SIZE
, self
.onSize
)
382 self
.drawPanel
.Bind(wx
.EVT_PAINT
, self
.onPaint
)
383 self
.drawPanel
.Bind(wx
.EVT_ERASE_BACKGROUND
, self
.onEraseBackground
)
384 self
.drawPanel
.Bind(wx
.EVT_SCROLLWIN
, self
.onPanelScroll
)
386 self
.Bind(wx
.EVT_TIMER
, self
.onIdle
)
389 # Position everything in the window.
391 topSizer
= wx
.BoxSizer(wx
.HORIZONTAL
)
392 topSizer
.Add(self
.toolPalette
, 0)
393 topSizer
.Add(self
.drawPanel
, 1, wx
.EXPAND
)
395 self
.topPanel
.SetAutoLayout(True)
396 self
.topPanel
.SetSizer(topSizer
)
398 self
.SetSizeHints(250, 200)
399 self
.SetSize(wx
.Size(600, 400))
401 # Select an initial tool.
403 self
.curToolName
= None
404 self
.curToolIcon
= None
406 self
.setCurrentTool("select")
408 # Set initial dc mode to fast
409 self
.wrapDC
= lambda dc
: dc
411 # Setup our frame to hold the contents of a sketch document.
414 self
.fileName
= fileName
415 self
.contents
= [] # front-to-back ordered list of DrawingObjects.
416 self
.selection
= [] # List of selected DrawingObjects.
417 self
.undoStack
= [] # Stack of saved contents for undo.
418 self
.redoStack
= [] # Stack of saved contents for redo.
420 if self
.fileName
!= None:
427 # Finally, set our initial pen, fill and line options.
429 self
._setPenColour
(wx
.BLACK
)
430 self
._setFillColour
(wx
.Colour(215,253,254))
433 self
.backgroundFillBrush
= None # create on demand
435 # Start the background redraw timer
436 # This is optional, but it gives the double-buffered contents a
437 # chance to redraw even when idle events are disabled (like during
438 # resize and scrolling)
439 self
.redrawTimer
= wx
.Timer(self
)
440 self
.redrawTimer
.Start(700)
443 # ============================
444 # == Event Handling Methods ==
445 # ============================
448 def onPenOptionIconClick(self
, event
):
449 """ Respond to the user clicking on the "Pen Options" icon.
451 data
= wx
.ColourData()
452 if len(self
.selection
) == 1:
453 data
.SetColour(self
.selection
[0].getPenColour())
455 data
.SetColour(self
.penColour
)
457 dialog
= wx
.ColourDialog(self
, data
)
458 dialog
.SetTitle('Choose line colour')
459 if dialog
.ShowModal() == wx
.ID_OK
:
460 c
= dialog
.GetColourData().GetColour()
461 self
._setPenColour
(wx
.Colour(c
.Red(), c
.Green(), c
.Blue()))
465 def onFillOptionIconClick(self
, event
):
466 """ Respond to the user clicking on the "Fill Options" icon.
468 data
= wx
.ColourData()
469 if len(self
.selection
) == 1:
470 data
.SetColour(self
.selection
[0].getFillColour())
472 data
.SetColour(self
.fillColour
)
474 dialog
= wx
.ColourDialog(self
, data
)
475 dialog
.SetTitle('Choose fill colour')
476 if dialog
.ShowModal() == wx
.ID_OK
:
477 c
= dialog
.GetColourData().GetColour()
478 self
._setFillColour
(wx
.Colour(c
.Red(), c
.Green(), c
.Blue()))
481 def onLineOptionIconClick(self
, event
):
482 """ Respond to the user clicking on the "Line Options" icon.
484 if len(self
.selection
) == 1:
485 menu
= self
._buildLineSizePopup
(self
.selection
[0].getLineSize())
487 menu
= self
._buildLineSizePopup
(self
.lineSize
)
489 pos
= self
.lineOptIcon
.GetPosition()
490 pos
.y
= pos
.y
+ self
.lineOptIcon
.GetSize().height
491 self
.PopupMenu(menu
, pos
)
495 def onKeyEvent(self
, event
):
496 """ Respond to a keypress event.
498 We make the arrow keys move the selected object(s) by one pixel in
502 if event
.ShiftDown():
505 if event
.GetKeyCode() == wx
.WXK_UP
:
506 self
._moveObject
(0, -step
)
507 elif event
.GetKeyCode() == wx
.WXK_DOWN
:
508 self
._moveObject
(0, step
)
509 elif event
.GetKeyCode() == wx
.WXK_LEFT
:
510 self
._moveObject
(-step
, 0)
511 elif event
.GetKeyCode() == wx
.WXK_RIGHT
:
512 self
._moveObject
(step
, 0)
517 def onMouseEvent(self
, event
):
518 """ Respond to mouse events in the main drawing panel
520 How we respond depends on the currently selected tool.
522 if self
.curTool
is None: return
524 # Translate event into canvas coordinates and pass to current tool
525 origx
,origy
= event
.X
, event
.Y
526 pt
= self
._getEventCoordinates
(event
)
529 handled
= self
.curTool
.onMouseEvent(self
,event
)
535 # otherwise handle it ourselves
536 if event
.RightDown():
537 self
.doPopupContextMenu(event
)
540 def doPopupContextMenu(self
, event
):
541 """ Respond to the user right-clicking within our drawing panel.
543 We select the clicked-on item, if necessary, and display a pop-up
544 menu of available options which can be applied to the selected
547 mousePt
= self
._getEventCoordinates
(event
)
548 obj
= self
.getObjectAt(mousePt
)
550 if obj
== None: return # Nothing selected.
552 # Select the clicked-on object.
556 # Build our pop-up menu.
559 menu
.Append(menu_DUPLICATE
, "Duplicate")
560 menu
.Append(menu_EDIT_PROPS
,"Edit...")
561 menu
.Append(wx
.ID_CLEAR
, "Delete")
562 menu
.AppendSeparator()
563 menu
.Append(menu_MOVE_FORWARD
, "Move Forward")
564 menu
.Append(menu_MOVE_TO_FRONT
, "Move to Front")
565 menu
.Append(menu_MOVE_BACKWARD
, "Move Backward")
566 menu
.Append(menu_MOVE_TO_BACK
, "Move to Back")
568 menu
.Enable(menu_EDIT_PROPS
, obj
.hasPropertyEditor())
569 menu
.Enable(menu_MOVE_FORWARD
, obj
!= self
.contents
[0])
570 menu
.Enable(menu_MOVE_TO_FRONT
, obj
!= self
.contents
[0])
571 menu
.Enable(menu_MOVE_BACKWARD
, obj
!= self
.contents
[-1])
572 menu
.Enable(menu_MOVE_TO_BACK
, obj
!= self
.contents
[-1])
574 self
.Bind(wx
.EVT_MENU
, self
.doDuplicate
, id=menu_DUPLICATE
)
575 self
.Bind(wx
.EVT_MENU
, self
.doEditObject
, id=menu_EDIT_PROPS
)
576 self
.Bind(wx
.EVT_MENU
, self
.doDelete
, id=wx
.ID_CLEAR
)
577 self
.Bind(wx
.EVT_MENU
, self
.doMoveForward
, id=menu_MOVE_FORWARD
)
578 self
.Bind(wx
.EVT_MENU
, self
.doMoveToFront
, id=menu_MOVE_TO_FRONT
)
579 self
.Bind(wx
.EVT_MENU
, self
.doMoveBackward
,id=menu_MOVE_BACKWARD
)
580 self
.Bind(wx
.EVT_MENU
, self
.doMoveToBack
, id=menu_MOVE_TO_BACK
)
582 # Show the pop-up menu.
584 clickPt
= wx
.Point(mousePt
.x
+ self
.drawPanel
.GetPosition().x
,
585 mousePt
.y
+ self
.drawPanel
.GetPosition().y
)
586 self
.drawPanel
.PopupMenu(menu
, mousePt
)
590 def onSize(self
, event
):
592 Called when the window is resized. We set a flag so the idle
593 handler will resize the buffer.
598 def onIdle(self
, event
):
600 If the size was changed then resize the bitmap used for double
601 buffering to match the window size. We do it in Idle time so
602 there is only one refresh after resizing is done, not lots while
605 if self
._reInitBuffer
and self
.IsShown():
607 self
.drawPanel
.Refresh(False)
609 def requestRedraw(self
):
610 """Requests a redraw of the drawing panel contents.
612 The actual redrawing doesn't happen until the next idle time.
614 self
._reInitBuffer
= True
616 def onPaint(self
, event
):
618 Called when the window is exposed.
620 # Create a buffered paint DC. It will create the real
621 # wx.PaintDC and then blit the bitmap to it when dc is
623 dc
= wx
.BufferedPaintDC(self
.drawPanel
, self
.buffer)
626 # On Windows, if that's all we do things look a little rough
627 # So in order to make scrolling more polished-looking
628 # we iterate over the exposed regions and fill in unknown
629 # areas with a fall-back pattern.
631 if wx
.Platform
!= '__WXMSW__':
634 # First get the update rects and subtract off the part that
635 # self.buffer has correct already
636 region
= self
.drawPanel
.GetUpdateRegion()
637 panelRect
= self
.drawPanel
.GetClientRect()
638 offset
= list(self
.drawPanel
.CalcUnscrolledPosition(0,0))
639 offset
[0] -= self
.saved_offset
[0]
640 offset
[1] -= self
.saved_offset
[1]
641 region
.Subtract(-offset
[0],- offset
[1],panelRect
.Width
, panelRect
.Height
)
643 # Now iterate over the remaining region rects and fill in with a pattern
644 rgn_iter
= wx
.RegionIterator(region
)
645 if rgn_iter
.HaveRects():
646 self
.setBackgroundMissingFillStyle(dc
)
647 offset
= self
.drawPanel
.CalcUnscrolledPosition(0,0)
649 r
= rgn_iter
.GetRect()
650 if r
.Size
!= self
.drawPanel
.ClientSize
:
651 dc
.DrawRectangleRect(r
)
655 def setBackgroundMissingFillStyle(self
, dc
):
656 if self
.backgroundFillBrush
is None:
657 # Win95 can only handle a 8x8 stipple bitmaps max
658 #stippleBitmap = wx.BitmapFromBits("\xf0"*4 + "\x0f"*4,8,8)
659 # ...but who uses Win95?
660 stippleBitmap
= wx
.BitmapFromBits("\x06",2,2)
661 stippleBitmap
.SetMask(wx
.Mask(stippleBitmap
))
662 bgbrush
= wx
.Brush(wx
.WHITE
, wx
.STIPPLE_MASK_OPAQUE
)
663 bgbrush
.SetStipple(stippleBitmap
)
664 self
.backgroundFillBrush
= bgbrush
666 dc
.SetPen(wx
.TRANSPARENT_PEN
)
667 dc
.SetBrush(self
.backgroundFillBrush
)
668 dc
.SetTextForeground(wx
.LIGHT_GREY
)
669 dc
.SetTextBackground(wx
.WHITE
)
672 def onEraseBackground(self
, event
):
674 Overridden to do nothing to prevent flicker
679 def onPanelScroll(self
, event
):
681 Called when the user changes scrolls the drawPanel
683 # make a note to ourselves to redraw when we get a chance
688 def drawContents(self
, dc
):
690 Does the actual drawing of all drawing contents with the specified dc
692 # PrepareDC sets the device origin according to current scrolling
693 self
.drawPanel
.PrepareDC(dc
)
695 gdc
= self
.wrapDC(dc
)
697 # First pass draws objects
698 ordered_selection
= []
699 for obj
in self
.contents
[::-1]:
700 if obj
in self
.selection
:
702 ordered_selection
.append(obj
)
706 # First pass draws objects
707 if self
.curTool
is not None:
708 self
.curTool
.draw(gdc
)
710 # Second pass draws selection handles so they're always on top
711 for obj
in ordered_selection
:
716 # ==========================
717 # == Menu Command Methods ==
718 # ==========================
720 def doNew(self
, event
):
721 """ Respond to the "New" menu command.
724 newFrame
= DrawingFrame(None, -1, "Untitled")
726 _docList
.append(newFrame
)
729 def doOpen(self
, event
):
730 """ Respond to the "Open" menu command.
735 fileName
= wx
.FileSelector("Open File", default_extension
="psk",
736 flags
= wx
.OPEN | wx
.FILE_MUST_EXIST
)
737 if fileName
== "": return
738 fileName
= os
.path
.join(os
.getcwd(), fileName
)
741 title
= os
.path
.basename(fileName
)
743 if (self
.fileName
== None) and (len(self
.contents
) == 0):
744 # Load contents into current (empty) document.
745 self
.fileName
= fileName
746 self
.SetTitle(os
.path
.basename(fileName
))
749 # Open a new frame for this document.
750 newFrame
= DrawingFrame(None, -1, os
.path
.basename(fileName
),
753 _docList
.append(newFrame
)
756 def doClose(self
, event
):
757 """ Respond to the "Close" menu command.
762 if not self
.askIfUserWantsToSave("closing"): return
764 _docList
.remove(self
)
768 def doSave(self
, event
):
769 """ Respond to the "Save" menu command.
771 if self
.fileName
!= None:
775 def doSaveAs(self
, event
):
776 """ Respond to the "Save As" menu command.
778 if self
.fileName
== None:
781 default
= self
.fileName
784 fileName
= wx
.FileSelector("Save File As", "Saving",
785 default_filename
=default
,
786 default_extension
="psk",
788 flags
= wx
.SAVE | wx
.OVERWRITE_PROMPT
)
789 if fileName
== "": return # User cancelled.
790 fileName
= os
.path
.join(os
.getcwd(), fileName
)
793 title
= os
.path
.basename(fileName
)
796 self
.fileName
= fileName
800 def doRevert(self
, event
):
801 """ Respond to the "Revert" menu command.
803 if not self
.dirty
: return
805 if wx
.MessageBox("Discard changes made to this document?", "Confirm",
806 style
= wx
.OK | wx
.CANCEL | wx
.ICON_QUESTION
,
807 parent
=self
) == wx
.CANCEL
: return
811 def doExit(self
, event
):
812 """ Respond to the "Quit" menu command.
814 global _docList
, _app
816 if not doc
.dirty
: continue
818 if not doc
.askIfUserWantsToSave("quitting"): return
825 def doUndo(self
, event
):
826 """ Respond to the "Undo" menu command.
828 if not self
.undoStack
: return
830 state
= self
._buildStoredState
()
831 self
.redoStack
.append(state
)
832 state
= self
.undoStack
.pop()
833 self
._restoreStoredState
(state
)
835 def doRedo(self
, event
):
836 """ Respond to the "Redo" menu.
838 if not self
.redoStack
: return
840 state
= self
._buildStoredState
()
841 self
.undoStack
.append(state
)
842 state
= self
.redoStack
.pop()
843 self
._restoreStoredState
(state
)
845 def doSelectAll(self
, event
):
846 """ Respond to the "Select All" menu command.
851 def doDuplicate(self
, event
):
852 """ Respond to the "Duplicate" menu command.
857 for obj
in self
.contents
:
858 if obj
in self
.selection
:
859 newObj
= copy
.deepcopy(obj
)
860 pos
= obj
.getPosition()
861 newObj
.setPosition(wx
.Point(pos
.x
+ 10, pos
.y
+ 10))
864 self
.contents
= objs
+ self
.contents
866 self
.selectMany(objs
)
869 def doEditObject(self
, event
):
870 """ Respond to the "Edit..." menu command.
872 if len(self
.selection
) != 1: return
874 obj
= self
.selection
[0]
875 if not obj
.hasPropertyEditor():
876 assert False, "doEditObject called on non-editable"
878 ret
= obj
.doPropertyEdit(self
)
885 def doDelete(self
, event
):
886 """ Respond to the "Delete" menu command.
890 for obj
in self
.selection
:
891 self
.contents
.remove(obj
)
896 def onChooseTool(self
, event
):
897 """ Respond to tool selection menu and tool palette selections
899 obj
= event
.GetEventObject()
900 id2name
= { id_SELECT
: "select",
902 id_POLYGON
: "polygon",
903 id_SCRIBBLE
: "scribble",
905 id_ELLIPSE
: "ellipse",
907 toolID
= event
.GetId()
908 name
= id2name
.get( toolID
)
911 self
.setCurrentTool(name
)
913 def updChooseTool(self
, event
):
914 """UI update event that keeps tool menu in sync with the PaletteIcons"""
915 obj
= event
.GetEventObject()
916 id2name
= { id_SELECT
: "select",
918 id_POLYGON
: "polygon",
919 id_SCRIBBLE
: "scribble",
921 id_ELLIPSE
: "ellipse",
923 toolID
= event
.GetId()
924 event
.Check( toolID
== self
.curToolIcon
.GetId() )
927 def doChooseQuality(self
, event
):
928 """Respond to the render quality menu commands
930 if event
.GetId() == menu_DC
:
931 self
.wrapDC
= lambda dc
: dc
933 self
.wrapDC
= lambda dc
: wx
.GCDC(dc
)
937 def doMoveForward(self
, event
):
938 """ Respond to the "Move Forward" menu command.
940 if len(self
.selection
) != 1: return
944 obj
= self
.selection
[0]
945 index
= self
.contents
.index(obj
)
946 if index
== 0: return
948 del self
.contents
[index
]
949 self
.contents
.insert(index
-1, obj
)
955 def doMoveToFront(self
, event
):
956 """ Respond to the "Move to Front" menu command.
958 if len(self
.selection
) != 1: return
962 obj
= self
.selection
[0]
963 self
.contents
.remove(obj
)
964 self
.contents
.insert(0, obj
)
970 def doMoveBackward(self
, event
):
971 """ Respond to the "Move Backward" menu command.
973 if len(self
.selection
) != 1: return
977 obj
= self
.selection
[0]
978 index
= self
.contents
.index(obj
)
979 if index
== len(self
.contents
) - 1: return
981 del self
.contents
[index
]
982 self
.contents
.insert(index
+1, obj
)
988 def doMoveToBack(self
, event
):
989 """ Respond to the "Move to Back" menu command.
991 if len(self
.selection
) != 1: return
995 obj
= self
.selection
[0]
996 self
.contents
.remove(obj
)
997 self
.contents
.append(obj
)
1003 def doShowAbout(self
, event
):
1004 """ Respond to the "About pySketch" menu command.
1006 dialog
= wx
.Dialog(self
, -1, "About pySketch") # ,
1007 #style=wx.DIALOG_MODAL | wx.STAY_ON_TOP)
1008 dialog
.SetBackgroundColour(wx
.WHITE
)
1010 panel
= wx
.Panel(dialog
, -1)
1011 panel
.SetBackgroundColour(wx
.WHITE
)
1013 panelSizer
= wx
.BoxSizer(wx
.VERTICAL
)
1015 boldFont
= wx
.Font(panel
.GetFont().GetPointSize(),
1016 panel
.GetFont().GetFamily(),
1019 logo
= wx
.StaticBitmap(panel
, -1, wx
.Bitmap("images/logo.bmp",
1020 wx
.BITMAP_TYPE_BMP
))
1022 lab1
= wx
.StaticText(panel
, -1, "pySketch")
1023 lab1
.SetFont(wx
.Font(36, boldFont
.GetFamily(), wx
.ITALIC
, wx
.BOLD
))
1024 lab1
.SetSize(lab1
.GetBestSize())
1026 imageSizer
= wx
.BoxSizer(wx
.HORIZONTAL
)
1027 imageSizer
.Add(logo
, 0, wx
.ALL | wx
.ALIGN_CENTRE_VERTICAL
, 5)
1028 imageSizer
.Add(lab1
, 0, wx
.ALL | wx
.ALIGN_CENTRE_VERTICAL
, 5)
1030 lab2
= wx
.StaticText(panel
, -1, "A simple object-oriented drawing " + \
1032 lab2
.SetFont(boldFont
)
1033 lab2
.SetSize(lab2
.GetBestSize())
1035 lab3
= wx
.StaticText(panel
, -1, "pySketch is completely free " + \
1037 lab3
.SetFont(boldFont
)
1038 lab3
.SetSize(lab3
.GetBestSize())
1040 lab4
= wx
.StaticText(panel
, -1, "feel free to adapt or use this " + \
1041 "in any way you like.")
1042 lab4
.SetFont(boldFont
)
1043 lab4
.SetSize(lab4
.GetBestSize())
1045 lab5
= wx
.StaticText(panel
, -1,
1046 "Author: Erik Westra " + \
1047 "(ewestra@wave.co.nz)\n" + \
1048 "Contributors: Bill Baxter " +\
1049 "(wbaxter@gmail.com) ")
1051 lab5
.SetFont(boldFont
)
1052 lab5
.SetSize(lab5
.GetBestSize())
1054 btnOK
= wx
.Button(panel
, wx
.ID_OK
, "OK")
1056 panelSizer
.Add(imageSizer
, 0, wx
.ALIGN_CENTRE
)
1057 panelSizer
.Add((10, 10)) # Spacer.
1058 panelSizer
.Add(lab2
, 0, wx
.ALIGN_CENTRE
)
1059 panelSizer
.Add((10, 10)) # Spacer.
1060 panelSizer
.Add(lab3
, 0, wx
.ALIGN_CENTRE
)
1061 panelSizer
.Add(lab4
, 0, wx
.ALIGN_CENTRE
)
1062 panelSizer
.Add((10, 10)) # Spacer.
1063 panelSizer
.Add(lab5
, 0, wx
.ALIGN_CENTRE
)
1064 panelSizer
.Add((10, 10)) # Spacer.
1065 panelSizer
.Add(btnOK
, 0, wx
.ALL | wx
.ALIGN_CENTRE
, 5)
1067 panel
.SetAutoLayout(True)
1068 panel
.SetSizer(panelSizer
)
1069 panelSizer
.Fit(panel
)
1071 topSizer
= wx
.BoxSizer(wx
.HORIZONTAL
)
1072 topSizer
.Add(panel
, 0, wx
.ALL
, 10)
1074 dialog
.SetAutoLayout(True)
1075 dialog
.SetSizer(topSizer
)
1076 topSizer
.Fit(dialog
)
1080 btn
= dialog
.ShowModal()
1083 def getTextEditor(self
):
1084 if not hasattr(self
,'textEditor') or not self
.textEditor
:
1085 self
.textEditor
= EditTextObjectDialog(self
, "Edit Text Object")
1086 return self
.textEditor
1088 # =============================
1089 # == Object Creation Methods ==
1090 # =============================
1092 def addObject(self
, obj
, select
=True):
1093 """Add a new drawing object to the canvas.
1095 If select is True then also select the object
1098 self
.contents
.insert(0, obj
)
1102 #self.setCurrentTool('select')
1104 def saveUndoInfo(self
):
1105 """ Remember the current state of the document, to allow for undo.
1107 We make a copy of the document's contents, so that we can return to
1108 the previous contents if the user does something and then wants to
1111 This should be called only for a new modification to the document
1112 since it erases the redo history.
1114 state
= self
._buildStoredState
()
1116 self
.undoStack
.append(state
)
1121 # =======================
1122 # == Selection Methods ==
1123 # =======================
1125 def setCurrentTool(self
, toolName
):
1126 """ Set the currently selected tool.
1129 toolIcon
, tool
= self
.tools
[toolName
]
1130 if self
.curToolIcon
is not None:
1131 self
.curToolIcon
.SetValue(False)
1133 toolIcon
.SetValue(True)
1134 self
.curToolName
= toolName
1135 self
.curToolIcon
= toolIcon
1137 self
.drawPanel
.SetCursor(tool
.getDefaultCursor())
1140 def selectAll(self
):
1141 """ Select every DrawingObject in our document.
1144 for obj
in self
.contents
:
1145 self
.selection
.append(obj
)
1146 self
.requestRedraw()
1150 def deselectAll(self
):
1151 """ Deselect every DrawingObject in our document.
1154 self
.requestRedraw()
1158 def select(self
, obj
, add
=False):
1159 """ Select the given DrawingObject within our document.
1161 If 'add' is True obj is added onto the current selection
1165 if obj
not in self
.selection
:
1166 self
.selection
+= [obj
]
1167 self
.requestRedraw()
1170 def selectMany(self
, objs
):
1171 """ Select the given list of DrawingObjects.
1173 self
.selection
= objs
1174 self
.requestRedraw()
1178 def selectByRectangle(self
, x
, y
, width
, height
):
1179 """ Select every DrawingObject in the given rectangular region.
1182 for obj
in self
.contents
:
1183 if obj
.objectWithinRect(x
, y
, width
, height
):
1184 self
.selection
.append(obj
)
1185 self
.requestRedraw()
1188 def getObjectAndSelectionHandleAt(self
, pt
):
1189 """ Return the object and selection handle at the given point.
1191 We draw selection handles (small rectangles) around the currently
1192 selected object(s). If the given point is within one of the
1193 selection handle rectangles, we return the associated object and a
1194 code indicating which selection handle the point is in. If the
1195 point isn't within any selection handle at all, we return the tuple
1198 for obj
in self
.selection
:
1199 handle
= obj
.getSelectionHandleContainingPoint(pt
.x
, pt
.y
)
1200 if handle
is not None:
1206 def getObjectAt(self
, pt
):
1207 """ Return the first object found which is at the given point.
1209 for obj
in self
.contents
:
1210 if obj
.objectContainsPoint(pt
.x
, pt
.y
):
1215 # ======================
1216 # == File I/O Methods ==
1217 # ======================
1219 def loadContents(self
):
1220 """ Load the contents of our document into memory.
1224 f
= open(self
.fileName
, "rb")
1225 objData
= cPickle
.load(f
)
1228 for klass
, data
in objData
:
1231 self
.contents
.append(obj
)
1238 self
.requestRedraw()
1241 response
= wx
.MessageBox("Unable to load " + self
.fileName
+ ".",
1242 "Error", wx
.OK|wx
.ICON_ERROR
, self
)
1246 def saveContents(self
):
1247 """ Save the contents of our document to disk.
1249 # SWIG-wrapped native wx contents cannot be pickled, so
1250 # we have to convert our data to something pickle-friendly.
1254 for obj
in self
.contents
:
1255 objData
.append([obj
.__class
__, obj
.getData()])
1257 f
= open(self
.fileName
, "wb")
1258 cPickle
.dump(objData
, f
)
1264 response
= wx
.MessageBox("Unable to load " + self
.fileName
+ ".",
1265 "Error", wx
.OK|wx
.ICON_ERROR
, self
)
1268 def askIfUserWantsToSave(self
, action
):
1269 """ Give the user the opportunity to save the current document.
1271 'action' is a string describing the action about to be taken. If
1272 the user wants to save the document, it is saved immediately. If
1273 the user cancels, we return False.
1275 if not self
.dirty
: return True # Nothing to do.
1277 response
= wx
.MessageBox("Save changes before " + action
+ "?",
1278 "Confirm", wx
.YES_NO | wx
.CANCEL
, self
)
1280 if response
== wx
.YES
:
1281 if self
.fileName
== None:
1282 fileName
= wx
.FileSelector("Save File As", "Saving",
1283 default_extension
="psk",
1285 flags
= wx
.SAVE | wx
.OVERWRITE_PROMPT
)
1286 if fileName
== "": return False # User cancelled.
1287 self
.fileName
= fileName
1291 elif response
== wx
.NO
:
1292 return True # User doesn't want changes saved.
1293 elif response
== wx
.CANCEL
:
1294 return False # User cancelled.
1296 # =====================
1297 # == Private Methods ==
1298 # =====================
1300 def _initBuffer(self
):
1301 """Initialize the bitmap used for buffering the display."""
1302 size
= self
.drawPanel
.GetSize()
1303 self
.buffer = wx
.EmptyBitmap(max(1,size
.width
),max(1,size
.height
))
1304 dc
= wx
.BufferedDC(None, self
.buffer)
1305 dc
.SetBackground(wx
.Brush(self
.drawPanel
.GetBackgroundColour()))
1307 self
.drawContents(dc
)
1308 del dc
# commits all drawing to the buffer
1310 self
.saved_offset
= self
.drawPanel
.CalcUnscrolledPosition(0,0)
1312 self
._reInitBuffer
= False
1316 def _adjustMenus(self
):
1317 """ Adjust our menus and toolbar to reflect the current state of the
1320 Doing this manually rather than using an EVT_UPDATE_UI is a bit
1321 more efficient (since it's only done when it's really needed),
1322 but it means we have to remember to call _adjustMenus any time
1323 menus may need adjusting.
1325 canSave
= (self
.fileName
!= None) and self
.dirty
1326 canRevert
= (self
.fileName
!= None) and self
.dirty
1327 canUndo
= self
.undoStack
!=[]
1328 canRedo
= self
.redoStack
!=[]
1329 selection
= len(self
.selection
) > 0
1330 onlyOne
= len(self
.selection
) == 1
1331 hasEditor
= onlyOne
and self
.selection
[0].hasPropertyEditor()
1332 front
= onlyOne
and (self
.selection
[0] == self
.contents
[0])
1333 back
= onlyOne
and (self
.selection
[0] == self
.contents
[-1])
1335 # Enable/disable our menu items.
1337 self
.fileMenu
.Enable(wx
.ID_SAVE
, canSave
)
1338 self
.fileMenu
.Enable(wx
.ID_REVERT
, canRevert
)
1340 self
.editMenu
.Enable(wx
.ID_UNDO
, canUndo
)
1341 self
.editMenu
.Enable(wx
.ID_REDO
, canRedo
)
1342 self
.editMenu
.Enable(menu_DUPLICATE
, selection
)
1343 self
.editMenu
.Enable(menu_EDIT_PROPS
,hasEditor
)
1344 self
.editMenu
.Enable(wx
.ID_CLEAR
, selection
)
1346 self
.objectMenu
.Enable(menu_MOVE_FORWARD
, onlyOne
and not front
)
1347 self
.objectMenu
.Enable(menu_MOVE_TO_FRONT
, onlyOne
and not front
)
1348 self
.objectMenu
.Enable(menu_MOVE_BACKWARD
, onlyOne
and not back
)
1349 self
.objectMenu
.Enable(menu_MOVE_TO_BACK
, onlyOne
and not back
)
1351 # Enable/disable our toolbar icons.
1353 self
.toolbar
.EnableTool(wx
.ID_NEW
, True)
1354 self
.toolbar
.EnableTool(wx
.ID_OPEN
, True)
1355 self
.toolbar
.EnableTool(wx
.ID_SAVE
, canSave
)
1356 self
.toolbar
.EnableTool(wx
.ID_UNDO
, canUndo
)
1357 self
.toolbar
.EnableTool(wx
.ID_REDO
, canRedo
)
1358 self
.toolbar
.EnableTool(menu_DUPLICATE
, selection
)
1359 self
.toolbar
.EnableTool(menu_MOVE_FORWARD
, onlyOne
and not front
)
1360 self
.toolbar
.EnableTool(menu_MOVE_BACKWARD
, onlyOne
and not back
)
1363 def _setPenColour(self
, colour
):
1364 """ Set the default or selected object's pen colour.
1366 if len(self
.selection
) > 0:
1368 for obj
in self
.selection
:
1369 obj
.setPenColour(colour
)
1370 self
.requestRedraw()
1372 self
.penColour
= colour
1373 self
.optionIndicator
.setPenColour(colour
)
1376 def _setFillColour(self
, colour
):
1377 """ Set the default or selected object's fill colour.
1379 if len(self
.selection
) > 0:
1381 for obj
in self
.selection
:
1382 obj
.setFillColour(colour
)
1383 self
.requestRedraw()
1385 self
.fillColour
= colour
1386 self
.optionIndicator
.setFillColour(colour
)
1389 def _setLineSize(self
, size
):
1390 """ Set the default or selected object's line size.
1392 if len(self
.selection
) > 0:
1394 for obj
in self
.selection
:
1395 obj
.setLineSize(size
)
1396 self
.requestRedraw()
1398 self
.lineSize
= size
1399 self
.optionIndicator
.setLineSize(size
)
1402 def _buildStoredState(self
):
1403 """ Remember the current state of the document, to allow for undo.
1405 We make a copy of the document's contents, so that we can return to
1406 the previous contents if the user does something and then wants to
1409 Returns an object representing the current document state.
1412 for obj
in self
.contents
:
1413 savedContents
.append([obj
.__class
__, obj
.getData()])
1416 for i
in range(len(self
.contents
)):
1417 if self
.contents
[i
] in self
.selection
:
1418 savedSelection
.append(i
)
1420 info
= {"contents" : savedContents
,
1421 "selection" : savedSelection
}
1425 def _restoreStoredState(self
, savedState
):
1426 """Restore the state of the document to a previous point for undo/redo.
1428 Takes a stored state object and recreates the document from it.
1429 Used by undo/redo implementation.
1433 for draw_class
, data
in savedState
["contents"]:
1436 self
.contents
.append(obj
)
1439 for i
in savedState
["selection"]:
1440 self
.selection
.append(self
.contents
[i
])
1444 self
.requestRedraw()
1446 def _resizeObject(self
, obj
, anchorPt
, oldPt
, newPt
):
1447 """ Resize the given object.
1449 'anchorPt' is the unchanging corner of the object, while the
1450 opposite corner has been resized. 'oldPt' are the current
1451 coordinates for this corner, while 'newPt' are the new coordinates.
1452 The object should fit within the given dimensions, though if the
1453 new point is less than the anchor point the object will need to be
1454 moved as well as resized, to avoid giving it a negative size.
1456 if isinstance(obj
, TextDrawingObject
):
1457 # Not allowed to resize text objects -- they're sized to fit text.
1463 topLeft
= wx
.Point(min(anchorPt
.x
, newPt
.x
),
1464 min(anchorPt
.y
, newPt
.y
))
1465 botRight
= wx
.Point(max(anchorPt
.x
, newPt
.x
),
1466 max(anchorPt
.y
, newPt
.y
))
1468 newWidth
= botRight
.x
- topLeft
.x
1469 newHeight
= botRight
.y
- topLeft
.y
1471 if isinstance(obj
, LineDrawingObject
):
1472 # Adjust the line so that its start and end points match the new
1473 # overall object size.
1475 startPt
= obj
.getStartPt()
1476 endPt
= obj
.getEndPt()
1478 slopesDown
= ((startPt
.x
< endPt
.x
) and (startPt
.y
< endPt
.y
)) or \
1479 ((startPt
.x
> endPt
.x
) and (startPt
.y
> endPt
.y
))
1481 # Handle the user flipping the line.
1483 hFlip
= ((anchorPt
.x
< oldPt
.x
) and (anchorPt
.x
> newPt
.x
)) or \
1484 ((anchorPt
.x
> oldPt
.x
) and (anchorPt
.x
< newPt
.x
))
1485 vFlip
= ((anchorPt
.y
< oldPt
.y
) and (anchorPt
.y
> newPt
.y
)) or \
1486 ((anchorPt
.y
> oldPt
.y
) and (anchorPt
.y
< newPt
.y
))
1488 if (hFlip
and not vFlip
) or (vFlip
and not hFlip
):
1489 slopesDown
= not slopesDown
# Line flipped.
1492 obj
.setStartPt(wx
.Point(0, 0))
1493 obj
.setEndPt(wx
.Point(newWidth
, newHeight
))
1495 obj
.setStartPt(wx
.Point(0, newHeight
))
1496 obj
.setEndPt(wx
.Point(newWidth
, 0))
1498 # Finally, adjust the bounds of the object to match the new dimensions.
1500 obj
.setPosition(topLeft
)
1501 obj
.setSize(wx
.Size(botRight
.x
- topLeft
.x
, botRight
.y
- topLeft
.y
))
1503 self
.requestRedraw()
1506 def _moveObject(self
, offsetX
, offsetY
):
1507 """ Move the currently selected object(s) by the given offset.
1511 for obj
in self
.selection
:
1512 pos
= obj
.getPosition()
1513 pos
.x
= pos
.x
+ offsetX
1514 pos
.y
= pos
.y
+ offsetY
1515 obj
.setPosition(pos
)
1517 self
.requestRedraw()
1520 def _buildLineSizePopup(self
, lineSize
):
1521 """ Build the pop-up menu used to set the line size.
1523 'lineSize' is the current line size value. The corresponding item
1524 is checked in the pop-up menu.
1527 menu
.Append(id_LINESIZE_0
, "no line", kind
=wx
.ITEM_CHECK
)
1528 menu
.Append(id_LINESIZE_1
, "1-pixel line", kind
=wx
.ITEM_CHECK
)
1529 menu
.Append(id_LINESIZE_2
, "2-pixel line", kind
=wx
.ITEM_CHECK
)
1530 menu
.Append(id_LINESIZE_3
, "3-pixel line", kind
=wx
.ITEM_CHECK
)
1531 menu
.Append(id_LINESIZE_4
, "4-pixel line", kind
=wx
.ITEM_CHECK
)
1532 menu
.Append(id_LINESIZE_5
, "5-pixel line", kind
=wx
.ITEM_CHECK
)
1534 if lineSize
== 0: menu
.Check(id_LINESIZE_0
, True)
1535 elif lineSize
== 1: menu
.Check(id_LINESIZE_1
, True)
1536 elif lineSize
== 2: menu
.Check(id_LINESIZE_2
, True)
1537 elif lineSize
== 3: menu
.Check(id_LINESIZE_3
, True)
1538 elif lineSize
== 4: menu
.Check(id_LINESIZE_4
, True)
1539 elif lineSize
== 5: menu
.Check(id_LINESIZE_5
, True)
1541 self
.Bind(wx
.EVT_MENU
, self
._lineSizePopupSelected
, id=id_LINESIZE_0
, id2
=id_LINESIZE_5
)
1546 def _lineSizePopupSelected(self
, event
):
1547 """ Respond to the user selecting an item from the line size popup menu
1550 if id == id_LINESIZE_0
: self
._setLineSize
(0)
1551 elif id == id_LINESIZE_1
: self
._setLineSize
(1)
1552 elif id == id_LINESIZE_2
: self
._setLineSize
(2)
1553 elif id == id_LINESIZE_3
: self
._setLineSize
(3)
1554 elif id == id_LINESIZE_4
: self
._setLineSize
(4)
1555 elif id == id_LINESIZE_5
: self
._setLineSize
(5)
1560 self
.optionIndicator
.setLineSize(self
.lineSize
)
1563 def _getEventCoordinates(self
, event
):
1564 """ Return the coordinates associated with the given mouse event.
1566 The coordinates have to be adjusted to allow for the current scroll
1569 originX
, originY
= self
.drawPanel
.GetViewStart()
1570 unitX
, unitY
= self
.drawPanel
.GetScrollPixelsPerUnit()
1571 return wx
.Point(event
.GetX() + (originX
* unitX
),
1572 event
.GetY() + (originY
* unitY
))
1575 def _drawObjectOutline(self
, offsetX
, offsetY
):
1576 """ Draw an outline of the currently selected object.
1578 The selected object's outline is drawn at the object's position
1579 plus the given offset.
1581 Note that the outline is drawn by *inverting* the window's
1582 contents, so calling _drawObjectOutline twice in succession will
1583 restore the window's contents back to what they were previously.
1585 if len(self
.selection
) != 1: return
1587 position
= self
.selection
[0].getPosition()
1588 size
= self
.selection
[0].getSize()
1590 dc
= wx
.ClientDC(self
.drawPanel
)
1591 self
.drawPanel
.PrepareDC(dc
)
1593 dc
.SetPen(wx
.BLACK_DASHED_PEN
)
1594 dc
.SetBrush(wx
.TRANSPARENT_BRUSH
)
1595 dc
.SetLogicalFunction(wx
.INVERT
)
1597 dc
.DrawRectangle(position
.x
+ offsetX
, position
.y
+ offsetY
,
1598 size
.width
, size
.height
)
1603 #============================================================================
1604 class DrawingTool(object):
1605 """Base class for drawing tools"""
1610 def getDefaultCursor(self
):
1611 """Return the cursor to use by default which this drawing tool is selected"""
1612 return wx
.STANDARD_CURSOR
1618 def onMouseEvent(self
,parent
, event
):
1619 """Mouse events passed in from the parent.
1621 Returns True if the event is handled by the tool,
1622 False if the canvas can try to use it.
1627 #----------------------------------------------------------------------------
1628 class SelectDrawingTool(DrawingTool
):
1629 """Represents the tool for selecting things"""
1632 self
.curHandle
= None
1633 self
.curObject
= None
1634 self
.objModified
= False
1638 def getDefaultCursor(self
):
1639 """Return the cursor to use by default which this drawing tool is selected"""
1640 return wx
.STANDARD_CURSOR
1643 if self
._doingRectSelection
():
1644 dc
.SetPen(wx
.BLACK_DASHED_PEN
)
1645 dc
.SetBrush(wx
.TRANSPARENT_BRUSH
)
1646 x
= [self
.startPt
.x
, self
.curPt
.x
]; x
.sort()
1647 y
= [self
.startPt
.y
, self
.curPt
.y
]; y
.sort()
1648 dc
.DrawRectangle(x
[0],y
[0], x
[1]-x
[0],y
[1]-y
[0])
1651 def onMouseEvent(self
,parent
, event
):
1652 handlers
= { wx
.EVT_LEFT_DOWN
.evtType
[0]: self
.onMouseLeftDown
,
1653 wx
.EVT_MOTION
.evtType
[0]: self
.onMouseMotion
,
1654 wx
.EVT_LEFT_UP
.evtType
[0]: self
.onMouseLeftUp
,
1655 wx
.EVT_LEFT_DCLICK
.evtType
[0]: self
.onMouseLeftDClick
}
1656 handler
= handlers
.get(event
.GetEventType())
1657 if handler
is not None:
1658 return handler(parent
,event
)
1663 def onMouseLeftDown(self
,parent
,event
):
1664 mousePt
= wx
.Point(event
.X
,event
.Y
)
1665 obj
, handle
= parent
.getObjectAndSelectionHandleAt(mousePt
)
1666 self
.startPt
= mousePt
1667 self
.curPt
= mousePt
1668 if obj
is not None and handle
is not None:
1669 self
.curObject
= obj
1670 self
.curHandle
= handle
1672 self
.curObject
= None
1673 self
.curHandle
= None
1675 obj
= parent
.getObjectAt(mousePt
)
1676 if self
.curObject
is None and obj
is not None:
1677 self
.curObject
= obj
1678 self
.dragDelta
= obj
.position
-mousePt
1679 self
.curHandle
= None
1680 parent
.select(obj
, event
.ShiftDown())
1684 def onMouseMotion(self
,parent
,event
):
1685 if not event
.LeftIsDown(): return
1687 self
.curPt
= wx
.Point(event
.X
,event
.Y
)
1689 obj
,handle
= self
.curObject
,self
.curHandle
1690 if self
._doingDragHandle
():
1691 self
._prepareToModify
(parent
)
1692 obj
.moveHandle(handle
,event
.X
,event
.Y
)
1693 parent
.requestRedraw()
1695 elif self
._doingDragObject
():
1696 self
._prepareToModify
(parent
)
1697 obj
.position
= self
.curPt
+ self
.dragDelta
1698 parent
.requestRedraw()
1700 elif self
._doingRectSelection
():
1701 parent
.requestRedraw()
1705 def onMouseLeftUp(self
,parent
,event
):
1707 obj
,handle
= self
.curObject
,self
.curHandle
1708 if self
._doingDragHandle
():
1709 obj
.moveHandle(handle
,event
.X
,event
.Y
)
1710 obj
.finalizeHandle(handle
,event
.X
,event
.Y
)
1712 elif self
._doingDragObject
():
1713 curPt
= wx
.Point(event
.X
,event
.Y
)
1714 obj
.position
= curPt
+ self
.dragDelta
1716 elif self
._doingRectSelection
():
1717 x
= [event
.X
, self
.startPt
.x
]
1718 y
= [event
.Y
, self
.startPt
.y
]
1721 parent
.selectByRectangle(x
[0],y
[0],x
[1]-x
[0],y
[1]-y
[0])
1724 self
.curObject
= None
1725 self
.curHandle
= None
1728 self
.objModified
= False
1729 parent
.requestRedraw()
1733 def onMouseLeftDClick(self
,parent
,event
):
1735 mousePt
= wx
.Point(event
.X
,event
.Y
)
1736 obj
= parent
.getObjectAt(mousePt
)
1737 if obj
and obj
.hasPropertyEditor():
1738 if obj
.doPropertyEdit(parent
):
1739 parent
.requestRedraw()
1745 def _prepareToModify(self
,parent
):
1746 if not self
.objModified
:
1747 parent
.saveUndoInfo()
1748 self
.objModified
= True
1750 def _doingRectSelection(self
):
1751 return self
.curObject
is None \
1752 and self
.startPt
is not None \
1753 and self
.curPt
is not None
1755 def _doingDragObject(self
):
1756 return self
.curObject
is not None and self
.curHandle
is None
1758 def _doingDragHandle(self
):
1759 return self
.curObject
is not None and self
.curHandle
is not None
1763 #----------------------------------------------------------------------------
1764 class LineDrawingTool(DrawingTool
):
1765 """Represents the tool for drawing lines"""
1768 self
.newObject
= None
1772 def getDefaultCursor(self
):
1773 """Return the cursor to use by default which this drawing tool is selected"""
1774 return wx
.StockCursor(wx
.CURSOR_PENCIL
)
1777 if self
.newObject
is None: return
1778 self
.newObject
.draw(dc
,True)
1780 def onMouseEvent(self
,parent
, event
):
1781 handlers
= { wx
.EVT_LEFT_DOWN
.evtType
[0]: self
.onMouseLeftDown
,
1782 wx
.EVT_MOTION
.evtType
[0]: self
.onMouseMotion
,
1783 wx
.EVT_LEFT_UP
.evtType
[0]: self
.onMouseLeftUp
}
1784 handler
= handlers
.get(event
.GetEventType())
1785 if handler
is not None:
1786 return handler(parent
,event
)
1791 def onMouseLeftDown(self
,parent
, event
):
1792 self
.startPt
= wx
.Point(event
.GetX(), event
.GetY())
1793 self
.newObject
= None
1797 def onMouseMotion(self
,parent
, event
):
1798 if not event
.Dragging(): return
1800 if self
.newObject
is None:
1801 obj
= LineDrawingObject(startPt
=wx
.Point(0,0),
1802 penColour
=parent
.penColour
,
1803 fillColour
=parent
.fillColour
,
1804 lineSize
=parent
.lineSize
,
1805 position
=wx
.Point(event
.X
,event
.Y
))
1806 self
.newObject
= obj
1808 self
._updateObjFromEvent
(self
.newObject
, event
)
1810 parent
.requestRedraw()
1814 def onMouseLeftUp(self
,parent
, event
):
1816 if self
.newObject
is None:
1819 self
._updateObjFromEvent
(self
.newObject
,event
)
1821 parent
.addObject(self
.newObject
)
1823 self
.newObject
= None
1830 def _updateObjFromEvent(self
,obj
,event
):
1831 obj
.setEndPt(wx
.Point(event
.X
,event
.Y
))
1834 #----------------------------------------------------------------------------
1835 class RectDrawingTool(DrawingTool
):
1836 """Represents the tool for drawing rectangles"""
1839 self
.newObject
= None
1841 def getDefaultCursor(self
):
1842 """Return the cursor to use by default which this drawing tool is selected"""
1843 return wx
.CROSS_CURSOR
1846 if self
.newObject
is None: return
1847 self
.newObject
.draw(dc
,True)
1850 def onMouseEvent(self
,parent
, event
):
1851 handlers
= { wx
.EVT_LEFT_DOWN
.evtType
[0]: self
.onMouseLeftDown
,
1852 wx
.EVT_MOTION
.evtType
[0]: self
.onMouseMotion
,
1853 wx
.EVT_LEFT_UP
.evtType
[0]: self
.onMouseLeftUp
}
1854 handler
= handlers
.get(event
.GetEventType())
1855 if handler
is not None:
1856 return handler(parent
,event
)
1861 def onMouseLeftDown(self
,parent
, event
):
1862 self
.startPt
= wx
.Point(event
.GetX(), event
.GetY())
1863 self
.newObject
= None
1867 def onMouseMotion(self
,parent
, event
):
1868 if not event
.Dragging(): return
1870 if self
.newObject
is None:
1871 obj
= RectDrawingObject(penColour
=parent
.penColour
,
1872 fillColour
=parent
.fillColour
,
1873 lineSize
=parent
.lineSize
)
1874 self
.newObject
= obj
1876 self
._updateObjFromEvent
(self
.newObject
, event
)
1878 parent
.requestRedraw()
1882 def onMouseLeftUp(self
,parent
, event
):
1884 if self
.newObject
is None:
1887 self
._updateObjFromEvent
(self
.newObject
,event
)
1889 parent
.addObject(self
.newObject
)
1891 self
.newObject
= None
1897 def _updateObjFromEvent(self
,obj
,event
):
1898 x
= [event
.X
, self
.startPt
.x
]
1899 y
= [event
.Y
, self
.startPt
.y
]
1905 obj
.setPosition(wx
.Point(x
[0],y
[0]))
1906 obj
.setSize(wx
.Size(width
,height
))
1911 #----------------------------------------------------------------------------
1912 class EllipseDrawingTool(DrawingTool
):
1913 """Represents the tool for drawing ellipses"""
1915 def getDefaultCursor(self
):
1916 """Return the cursor to use by default which this drawing tool is selected"""
1917 return wx
.CROSS_CURSOR
1921 self
.newObject
= None
1923 def getDefaultCursor(self
):
1924 """Return the cursor to use by default which this drawing tool is selected"""
1925 return wx
.CROSS_CURSOR
1928 if self
.newObject
is None: return
1929 self
.newObject
.draw(dc
,True)
1932 def onMouseEvent(self
,parent
, event
):
1933 handlers
= { wx
.EVT_LEFT_DOWN
.evtType
[0]: self
.onMouseLeftDown
,
1934 wx
.EVT_MOTION
.evtType
[0]: self
.onMouseMotion
,
1935 wx
.EVT_LEFT_UP
.evtType
[0]: self
.onMouseLeftUp
}
1936 handler
= handlers
.get(event
.GetEventType())
1937 if handler
is not None:
1938 return handler(parent
,event
)
1943 def onMouseLeftDown(self
,parent
, event
):
1944 self
.startPt
= wx
.Point(event
.GetX(), event
.GetY())
1945 self
.newObject
= None
1949 def onMouseMotion(self
,parent
, event
):
1950 if not event
.Dragging(): return
1952 if self
.newObject
is None:
1953 obj
= EllipseDrawingObject(penColour
=parent
.penColour
,
1954 fillColour
=parent
.fillColour
,
1955 lineSize
=parent
.lineSize
)
1956 self
.newObject
= obj
1958 self
._updateObjFromEvent
(self
.newObject
, event
)
1960 parent
.requestRedraw()
1964 def onMouseLeftUp(self
,parent
, event
):
1966 if self
.newObject
is None:
1969 self
._updateObjFromEvent
(self
.newObject
,event
)
1971 parent
.addObject(self
.newObject
)
1973 self
.newObject
= None
1979 def _updateObjFromEvent(self
,obj
,event
):
1980 x
= [event
.X
, self
.startPt
.x
]
1981 y
= [event
.Y
, self
.startPt
.y
]
1987 obj
.setPosition(wx
.Point(x
[0],y
[0]))
1988 obj
.setSize(wx
.Size(width
,height
))
1991 #----------------------------------------------------------------------------
1992 class PolygonDrawingTool(DrawingTool
):
1993 """Represents the tool for drawing polygons"""
1996 self
.newObject
= None
1998 def getDefaultCursor(self
):
1999 """Return the cursor to use by default which this drawing tool is selected"""
2000 return wx
.CROSS_CURSOR
2004 if self
.newObject
is None: return
2005 self
.newObject
.draw(dc
,True)
2008 def onMouseEvent(self
,parent
, event
):
2009 handlers
= { wx
.EVT_LEFT_DOWN
.evtType
[0]: self
.onMouseLeftDown
,
2010 wx
.EVT_MOTION
.evtType
[0]: self
.onMouseMotion
,
2011 wx
.EVT_LEFT_UP
.evtType
[0]: self
.onMouseLeftUp
,
2012 wx
.EVT_LEFT_DCLICK
.evtType
[0]:self
.onMouseLeftDClick
}
2013 handler
= handlers
.get(event
.GetEventType())
2014 if handler
is not None:
2015 return handler(parent
,event
)
2020 def onMouseLeftDown(self
,parent
, event
):
2022 self
.startPt
= (event
.GetX(), event
.GetY())
2023 if self
.newObject
is None:
2024 obj
= PolygonDrawingObject(points
=[(0,0)],penColour
=parent
.penColour
,
2025 fillColour
=parent
.fillColour
,
2026 lineSize
=parent
.lineSize
,
2027 position
=wx
.Point(event
.X
, event
.Y
))
2028 obj
.addPoint(event
.X
,event
.Y
)
2029 self
.newObject
= obj
2032 pt0
= self
.newObject
.getPoint(0)
2033 if abs(pt0
[0]-event
.X
)<CLOSE_THRESH
and abs(pt0
[1]-event
.Y
)<CLOSE_THRESH
:
2034 self
.newObject
.popPoint()
2035 parent
.addObject(self
.newObject
)
2036 self
.newObject
= None
2038 self
.newObject
.addPoint(event
.X
,event
.Y
)
2042 def onMouseMotion(self
,parent
, event
):
2046 self
.newObject
.movePoint(-1, event
.X
, event
.Y
)
2047 parent
.requestRedraw()
2052 def onMouseLeftDClick(self
,parent
,event
):
2056 pt0
= self
.newObject
.getPoint(0)
2057 if abs(pt0
[0]-event
.X
)<CLOSE_THRESH
and abs(pt0
[1]-event
.Y
)<CLOSE_THRESH
:
2058 self
.newObject
.popPoint()
2059 self
.newObject
.popPoint()
2060 parent
.addObject(self
.newObject
)
2061 self
.newObject
= None
2065 def onMouseLeftUp(self
,parent
, event
):
2072 #----------------------------------------------------------------------------
2073 class ScribbleDrawingTool(DrawingTool
):
2074 """Represents the tool for drawing scribble drawing objects"""
2077 self
.newObject
= None
2079 def getDefaultCursor(self
):
2080 """Return the cursor to use by default which this drawing tool is selected"""
2081 return wx
.StockCursor(wx
.CURSOR_PENCIL
)
2084 if self
.newObject
is None: return
2085 self
.newObject
.draw(dc
,True)
2088 def onMouseEvent(self
,parent
, event
):
2089 handlers
= { wx
.EVT_LEFT_DOWN
.evtType
[0]: self
.onMouseLeftDown
,
2090 wx
.EVT_MOTION
.evtType
[0]: self
.onMouseMotion
,
2091 wx
.EVT_LEFT_UP
.evtType
[0]: self
.onMouseLeftUp
2093 handler
= handlers
.get(event
.GetEventType())
2094 if handler
is not None:
2095 return handler(parent
,event
)
2100 def onMouseLeftDown(self
,parent
, event
):
2102 obj
= ScribbleDrawingObject(points
=[(0,0)],penColour
=parent
.penColour
,
2103 fillColour
=parent
.fillColour
,
2104 lineSize
=parent
.lineSize
,
2105 position
=wx
.Point(event
.X
, event
.Y
))
2106 self
.newObject
= obj
2109 def onMouseMotion(self
,parent
, event
):
2112 self
.newObject
.addPoint(event
.X
,event
.Y
)
2113 parent
.requestRedraw()
2118 def onMouseLeftUp(self
,parent
, event
):
2121 parent
.addObject(self
.newObject
)
2122 self
.newObject
= None
2129 #----------------------------------------------------------------------------
2130 class TextDrawingTool(DrawingTool
):
2131 """Represents the tool for drawing text"""
2133 def getDefaultCursor(self
):
2134 """Return the cursor to use by default which this drawing tool is selected"""
2135 return wx
.StockCursor(wx
.CURSOR_IBEAM
)
2137 def onMouseEvent(self
,parent
, event
):
2138 handlers
= { #wx.EVT_LEFT_DOWN.evtType[0]: self.onMouseLeftDown,
2139 #wx.EVT_MOTION.evtType[0]: self.onMouseMotion,
2140 wx
.EVT_LEFT_UP
.evtType
[0]: self
.onMouseLeftUp
2142 handler
= handlers
.get(event
.GetEventType())
2143 if handler
is not None:
2144 return handler(parent
,event
)
2149 def onMouseLeftUp(self
,parent
, event
):
2151 editor
= parent
.getTextEditor()
2152 editor
.SetTitle("Create Text Object")
2153 if editor
.ShowModal() == wx
.ID_CANCEL
:
2157 obj
= TextDrawingObject(position
=wx
.Point(event
.X
, event
.Y
))
2158 editor
.dialogToObject(obj
)
2161 parent
.addObject(obj
)
2168 #============================================================================
2169 class DrawingObject(object):
2170 """ Base class for objects within the drawing panel.
2172 A pySketch document consists of a front-to-back ordered list of
2173 DrawingObjects. Each DrawingObject has the following properties:
2175 'position' The position of the object within the document.
2176 'size' The size of the object within the document.
2177 'penColour' The colour to use for drawing the object's outline.
2178 'fillColour' Colour to use for drawing object's interior.
2179 'lineSize' Line width (in pixels) to use for object's outline.
2182 # ==================
2183 # == Constructors ==
2184 # ==================
2186 def __init__(self
, position
=wx
.Point(0, 0), size
=wx
.Size(0, 0),
2187 penColour
=wx
.BLACK
, fillColour
=wx
.WHITE
, lineSize
=1,
2189 """ Standard constructor.
2191 The remaining parameters let you set various options for the newly
2192 created DrawingObject.
2194 # One must take great care with constructed default arguments
2195 # like wx.Point(0,0) above. *EVERY* caller that uses the
2196 # default will get the same instance. Thus, below we make a
2197 # deep copy of those arguments with object defaults.
2199 self
.position
= wx
.Point(position
.x
,position
.y
)
2200 self
.size
= wx
.Size(size
.x
,size
.y
)
2201 self
.penColour
= penColour
2202 self
.fillColour
= fillColour
2203 self
.lineSize
= lineSize
2205 # =============================
2206 # == Object Property Methods ==
2207 # =============================
2210 """ Return a copy of the object's internal data.
2212 This is used to save this DrawingObject to disk.
2214 return [self
.position
.x
, self
.position
.y
,
2215 self
.size
.width
, self
.size
.height
,
2216 self
.penColour
.Red(),
2217 self
.penColour
.Green(),
2218 self
.penColour
.Blue(),
2219 self
.fillColour
.Red(),
2220 self
.fillColour
.Green(),
2221 self
.fillColour
.Blue(),
2225 def setData(self
, data
):
2226 """ Set the object's internal data.
2228 'data' is a copy of the object's saved data, as returned by
2229 getData() above. This is used to restore a previously saved
2232 Returns an iterator to any remaining data not consumed by
2233 this base class method.
2235 #data = copy.deepcopy(data) # Needed?
2239 self
.position
= wx
.Point(d
.next(), d
.next())
2240 self
.size
= wx
.Size(d
.next(), d
.next())
2241 self
.penColour
= wx
.Colour(red
=d
.next(),
2244 self
.fillColour
= wx
.Colour(red
=d
.next(),
2247 self
.lineSize
= d
.next()
2248 except StopIteration:
2249 raise ValueError('Not enough data in setData call')
2254 def hasPropertyEditor(self
):
2257 def doPropertyEdit(self
, parent
):
2258 assert False, "Must be overridden if hasPropertyEditor returns True"
2260 def setPosition(self
, position
):
2261 """ Set the origin (top-left corner) for this DrawingObject.
2263 self
.position
= position
2266 def getPosition(self
):
2267 """ Return this DrawingObject's position.
2269 return self
.position
2272 def setSize(self
, size
):
2273 """ Set the size for this DrawingObject.
2279 """ Return this DrawingObject's size.
2284 def setPenColour(self
, colour
):
2285 """ Set the pen colour used for this DrawingObject.
2287 self
.penColour
= colour
2290 def getPenColour(self
):
2291 """ Return this DrawingObject's pen colour.
2293 return self
.penColour
2296 def setFillColour(self
, colour
):
2297 """ Set the fill colour used for this DrawingObject.
2299 self
.fillColour
= colour
2302 def getFillColour(self
):
2303 """ Return this DrawingObject's fill colour.
2305 return self
.fillColour
2308 def setLineSize(self
, lineSize
):
2309 """ Set the linesize used for this DrawingObject.
2311 self
.lineSize
= lineSize
2314 def getLineSize(self
):
2315 """ Return this DrawingObject's line size.
2317 return self
.lineSize
2320 # ============================
2321 # == Object Drawing Methods ==
2322 # ============================
2324 def draw(self
, dc
, selected
):
2325 """ Draw this DrawingObject into our window.
2327 'dc' is the device context to use for drawing.
2329 If 'selected' is True, the object is currently selected.
2330 Drawing objects can use this to change the way selected objects
2331 are drawn, however the actual drawing of selection handles
2332 should be done in the 'drawHandles' method
2334 if self
.lineSize
== 0:
2335 dc
.SetPen(wx
.Pen(self
.penColour
, self
.lineSize
, wx
.TRANSPARENT
))
2337 dc
.SetPen(wx
.Pen(self
.penColour
, self
.lineSize
, wx
.SOLID
))
2338 dc
.SetBrush(wx
.Brush(self
.fillColour
, wx
.SOLID
))
2340 self
._privateDraw
(dc
, self
.position
, selected
)
2343 def drawHandles(self
, dc
):
2344 """Draw selection handles for this DrawingObject"""
2346 # Default is to draw selection handles at all four corners.
2347 dc
.SetPen(wx
.BLACK_PEN
)
2348 dc
.SetBrush(wx
.BLACK_BRUSH
)
2351 self
._drawSelHandle
(dc
, x
, y
)
2352 self
._drawSelHandle
(dc
, x
+ self
.size
.width
, y
)
2353 self
._drawSelHandle
(dc
, x
, y
+ self
.size
.height
)
2354 self
._drawSelHandle
(dc
, x
+ self
.size
.width
, y
+ self
.size
.height
)
2357 # =======================
2358 # == Selection Methods ==
2359 # =======================
2361 def objectContainsPoint(self
, x
, y
):
2362 """ Returns True iff this object contains the given point.
2364 This is used to determine if the user clicked on the object.
2366 # Firstly, ignore any points outside of the object's bounds.
2368 if x
< self
.position
.x
: return False
2369 if x
> self
.position
.x
+ self
.size
.x
: return False
2370 if y
< self
.position
.y
: return False
2371 if y
> self
.position
.y
+ self
.size
.y
: return False
2373 # Now things get tricky. There's no straightforward way of
2374 # knowing whether the point is within an arbitrary object's
2375 # bounds...to get around this, we draw the object into a
2376 # memory-based bitmap and see if the given point was drawn.
2377 # This could no doubt be done more efficiently by some tricky
2378 # maths, but this approach works and is simple enough.
2380 # Subclasses can implement smarter faster versions of this.
2382 bitmap
= wx
.EmptyBitmap(self
.size
.x
+ 10, self
.size
.y
+ 10)
2384 dc
.SelectObject(bitmap
)
2386 dc
.SetBackground(wx
.WHITE_BRUSH
)
2388 dc
.SetPen(wx
.Pen(wx
.BLACK
, self
.lineSize
+ 5, wx
.SOLID
))
2389 dc
.SetBrush(wx
.BLACK_BRUSH
)
2390 self
._privateDraw
(dc
, wx
.Point(5, 5), True)
2392 pixel
= dc
.GetPixel(x
- self
.position
.x
+ 5, y
- self
.position
.y
+ 5)
2393 if (pixel
.Red() == 0) and (pixel
.Green() == 0) and (pixel
.Blue() == 0):
2403 def getSelectionHandleContainingPoint(self
, x
, y
):
2404 """ Return the selection handle containing the given point, if any.
2406 We return one of the predefined selection handle ID codes.
2408 # Default implementation assumes selection handles at all four bbox corners.
2409 # Return a list so we can modify the contents later in moveHandle()
2410 if self
._pointInSelRect
(x
, y
, self
.position
.x
, self
.position
.y
):
2411 return [self
.handle_TOP
, self
.handle_LEFT
]
2412 elif self
._pointInSelRect
(x
, y
, self
.position
.x
+ self
.size
.width
,
2414 return [self
.handle_TOP
, self
.handle_RIGHT
]
2415 elif self
._pointInSelRect
(x
, y
, self
.position
.x
,
2416 self
.position
.y
+ self
.size
.height
):
2417 return [self
.handle_BOTTOM
, self
.handle_LEFT
]
2418 elif self
._pointInSelRect
(x
, y
, self
.position
.x
+ self
.size
.width
,
2419 self
.position
.y
+ self
.size
.height
):
2420 return [self
.handle_BOTTOM
, self
.handle_RIGHT
]
2424 def moveHandle(self
, handle
, x
, y
):
2425 """ Move the specified selection handle to given canvas location.
2427 assert handle
is not None
2429 # Default implementation assumes selection handles at all four bbox corners.
2433 if handle
[0] == self
.handle_TOP
:
2434 if handle
[1] == self
.handle_LEFT
:
2435 dpos
= pt
- self
.position
2437 self
.size
.width
-= dpos
.x
2438 self
.size
.height
-= dpos
.y
2440 dx
= pt
.x
- ( x
+ w
)
2442 self
.position
.y
= pt
.y
2443 self
.size
.width
+= dx
2444 self
.size
.height
-= dy
2446 if handle
[1] == self
.handle_LEFT
:
2448 dy
= pt
.y
- ( y
+ h
)
2449 self
.position
.x
= pt
.x
2450 self
.size
.width
-= dx
2451 self
.size
.height
+= dy
2453 dpos
= pt
- self
.position
2456 self
.size
.width
+= dpos
.x
2457 self
.size
.height
+= dpos
.y
2460 # Finally, normalize so no negative widths or heights.
2461 # And update the handle variable accordingly.
2462 if self
.size
.height
<0:
2463 self
.position
.y
+= self
.size
.height
2464 self
.size
.height
= -self
.size
.height
2465 handle
[0] = 1-handle
[0]
2467 if self
.size
.width
<0:
2468 self
.position
.x
+= self
.size
.width
2469 self
.size
.width
= -self
.size
.width
2470 handle
[1] = 1-handle
[1]
2474 def finalizeHandle(self
, handle
, x
, y
):
2478 def objectWithinRect(self
, x
, y
, width
, height
):
2479 """ Return True iff this object falls completely within the given rect.
2481 if x
> self
.position
.x
: return False
2482 if x
+ width
< self
.position
.x
+ self
.size
.width
: return False
2483 if y
> self
.position
.y
: return False
2484 if y
+ height
< self
.position
.y
+ self
.size
.height
: return False
2487 # =====================
2488 # == Private Methods ==
2489 # =====================
2491 def _privateDraw(self
, dc
, position
, selected
):
2492 """ Private routine to draw this DrawingObject.
2494 'dc' is the device context to use for drawing, while 'position' is
2495 the position in which to draw the object.
2499 def _drawSelHandle(self
, dc
, x
, y
):
2500 """ Draw a selection handle around this DrawingObject.
2502 'dc' is the device context to draw the selection handle within,
2503 while 'x' and 'y' are the coordinates to use for the centre of the
2506 dc
.DrawRectangle(x
- 3, y
- 3, 6, 6)
2509 def _pointInSelRect(self
, x
, y
, rX
, rY
):
2510 """ Return True iff (x, y) is within the selection handle at (rX, ry).
2512 if x
< rX
- 3: return False
2513 elif x
> rX
+ 3: return False
2514 elif y
< rY
- 3: return False
2515 elif y
> rY
+ 3: return False
2519 #----------------------------------------------------------------------------
2520 class LineDrawingObject(DrawingObject
):
2521 """ DrawingObject subclass that represents one line segment.
2523 Adds the following members to the base DrawingObject:
2524 'startPt' The point, relative to the object's position, where
2526 'endPt' The point, relative to the object's position, where
2530 def __init__(self
, startPt
=wx
.Point(0,0), endPt
=wx
.Point(0,0), *varg
, **kwarg
):
2531 DrawingObject
.__init
__(self
, *varg
, **kwarg
)
2533 self
.startPt
= wx
.Point(startPt
.x
,startPt
.y
)
2534 self
.endPt
= wx
.Point(endPt
.x
,endPt
.y
)
2536 # ============================
2537 # == Object Drawing Methods ==
2538 # ============================
2540 def drawHandles(self
, dc
):
2541 """Draw selection handles for this DrawingObject"""
2543 dc
.SetPen(wx
.BLACK_PEN
)
2544 dc
.SetBrush(wx
.BLACK_BRUSH
)
2547 # Draw selection handles at the start and end points.
2548 self
._drawSelHandle
(dc
, x
+ self
.startPt
.x
, y
+ self
.startPt
.y
)
2549 self
._drawSelHandle
(dc
, x
+ self
.endPt
.x
, y
+ self
.endPt
.y
)
2553 # =======================
2554 # == Selection Methods ==
2555 # =======================
2558 handle_START_POINT
= 1
2559 handle_END_POINT
= 2
2561 def getSelectionHandleContainingPoint(self
, x
, y
):
2562 """ Return the selection handle containing the given point, if any.
2564 We return one of the predefined selection handle ID codes.
2566 # We have selection handles at the start and end points.
2567 if self
._pointInSelRect
(x
, y
, self
.position
.x
+ self
.startPt
.x
,
2568 self
.position
.y
+ self
.startPt
.y
):
2569 return self
.handle_START_POINT
2570 elif self
._pointInSelRect
(x
, y
, self
.position
.x
+ self
.endPt
.x
,
2571 self
.position
.y
+ self
.endPt
.y
):
2572 return self
.handle_END_POINT
2576 def moveHandle(self
, handle
, x
, y
):
2577 """Move the handle to specified handle to the specified canvas coordinates
2579 ptTrans
= wx
.Point(x
-self
.position
.x
, y
-self
.position
.y
)
2580 if handle
== self
.handle_START_POINT
:
2581 self
.startPt
= ptTrans
2582 elif handle
== self
.handle_END_POINT
:
2583 self
.endPt
= ptTrans
2585 raise ValueError("Bad handle type for a line")
2587 self
._updateBoundingBox
()
2589 # =============================
2590 # == Object Property Methods ==
2591 # =============================
2594 """ Return a copy of the object's internal data.
2596 This is used to save this DrawingObject to disk.
2599 data
= DrawingObject
.getData(self
)
2601 data
+= [self
.startPt
.x
, self
.startPt
.y
,
2602 self
.endPt
.x
, self
.endPt
.y
]
2605 def setData(self
, data
):
2606 """ Set the object's internal data.
2608 'data' is a copy of the object's saved data, as returned by
2609 getData() above. This is used to restore a previously saved
2612 #data = copy.deepcopy(data) # Needed?
2614 d
= DrawingObject
.setData(self
, data
)
2617 self
.startPt
= wx
.Point(d
.next(), d
.next())
2618 self
.endPt
= wx
.Point(d
.next(), d
.next())
2619 except StopIteration:
2620 raise ValueError('Not enough data in setData call')
2625 def setStartPt(self
, startPt
):
2626 """ Set the starting point for this line DrawingObject.
2628 self
.startPt
= startPt
- self
.position
2629 self
._updateBoundingBox
()
2632 def getStartPt(self
):
2633 """ Return the starting point for this line DrawingObject.
2635 return self
.startPt
+ self
.position
2638 def setEndPt(self
, endPt
):
2639 """ Set the ending point for this line DrawingObject.
2641 self
.endPt
= endPt
- self
.position
2642 self
._updateBoundingBox
()
2646 """ Return the ending point for this line DrawingObject.
2648 return self
.endPt
+ self
.position
2651 # =====================
2652 # == Private Methods ==
2653 # =====================
2656 def _privateDraw(self
, dc
, position
, selected
):
2657 """ Private routine to draw this DrawingObject.
2659 'dc' is the device context to use for drawing, while 'position' is
2660 the position in which to draw the object. If 'selected' is True,
2661 the object is drawn with selection handles. This private drawing
2662 routine assumes that the pen and brush have already been set by the
2665 dc
.DrawLine(position
.x
+ self
.startPt
.x
,
2666 position
.y
+ self
.startPt
.y
,
2667 position
.x
+ self
.endPt
.x
,
2668 position
.y
+ self
.endPt
.y
)
2670 def _updateBoundingBox(self
):
2671 x
= [self
.startPt
.x
, self
.endPt
.x
]; x
.sort()
2672 y
= [self
.startPt
.y
, self
.endPt
.y
]; y
.sort()
2674 dp
= wx
.Point(-x
[0],-y
[0])
2675 self
.position
.x
+= x
[0]
2676 self
.position
.y
+= y
[0]
2677 self
.size
.width
= x
[1]-x
[0]
2678 self
.size
.height
= y
[1]-y
[0]
2683 #----------------------------------------------------------------------------
2684 class PolygonDrawingObject(DrawingObject
):
2685 """ DrawingObject subclass that represents a poly-line or polygon
2687 def __init__(self
, points
=[], *varg
, **kwarg
):
2688 DrawingObject
.__init
__(self
, *varg
, **kwarg
)
2689 self
.points
= list(points
)
2691 # =======================
2692 # == Selection Methods ==
2693 # =======================
2695 def getSelectionHandleContainingPoint(self
, x
, y
):
2696 """ Return the selection handle containing the given point, if any.
2698 We return one of the predefined selection handle ID codes.
2700 # We have selection handles at the start and end points.
2701 for i
,p
in enumerate(self
.points
):
2702 if self
._pointInSelRect
(x
, y
,
2703 self
.position
.x
+ p
[0],
2704 self
.position
.y
+ p
[1]):
2710 def addPoint(self
, x
,y
):
2711 self
.points
.append((x
-self
.position
.x
,y
-self
.position
.y
))
2712 self
._updateBoundingBox
()
2714 def getPoint(self
, idx
):
2715 x
,y
= self
.points
[idx
]
2716 return (x
+self
.position
.x
,y
+self
.position
.y
)
2718 def movePoint(self
, idx
, x
,y
):
2719 self
.points
[idx
] = (x
-self
.position
.x
,y
-self
.position
.y
)
2720 self
._updateBoundingBox
()
2722 def popPoint(self
, idx
=-1):
2723 self
.points
.pop(idx
)
2724 self
._updateBoundingBox
()
2726 # =====================
2727 # == Drawing Methods ==
2728 # =====================
2730 def drawHandles(self
, dc
):
2731 """Draw selection handles for this DrawingObject"""
2733 dc
.SetPen(wx
.BLACK_PEN
)
2734 dc
.SetBrush(wx
.BLACK_BRUSH
)
2737 # Draw selection handles at the start and end points.
2738 for p
in self
.points
:
2739 self
._drawSelHandle
(dc
, x
+ p
[0], y
+ p
[1])
2741 def moveHandle(self
, handle
, x
, y
):
2742 """Move the specified handle"""
2743 self
.movePoint(handle
-1,x
,y
)
2746 # =============================
2747 # == Object Property Methods ==
2748 # =============================
2751 """ Return a copy of the object's internal data.
2753 This is used to save this DrawingObject to disk.
2756 data
= DrawingObject
.getData(self
)
2758 data
+= [list(self
.points
)]
2763 def setData(self
, data
):
2764 """ Set the object's internal data.
2766 'data' is a copy of the object's saved data, as returned by
2767 getData() above. This is used to restore a previously saved
2770 #data = copy.deepcopy(data) # Needed?
2771 d
= DrawingObject
.setData(self
, data
)
2774 self
.points
= d
.next()
2775 except StopIteration:
2776 raise ValueError('Not enough data in setData call')
2781 # =====================
2782 # == Private Methods ==
2783 # =====================
2784 def _privateDraw(self
, dc
, position
, selected
):
2785 """ Private routine to draw this DrawingObject.
2787 'dc' is the device context to use for drawing, while 'position' is
2788 the position in which to draw the object. If 'selected' is True,
2789 the object is drawn with selection handles. This private drawing
2790 routine assumes that the pen and brush have already been set by the
2793 dc
.DrawPolygon(self
.points
, position
.x
, position
.y
)
2795 def _updateBoundingBox(self
):
2796 x
= min([p
[0] for p
in self
.points
])
2797 y
= min([p
[1] for p
in self
.points
])
2798 x2
= max([p
[0] for p
in self
.points
])
2799 y2
= max([p
[1] for p
in self
.points
])
2802 self
.position
.x
+= x
2803 self
.position
.y
+= y
2804 self
.size
.width
= x2
-x
2805 self
.size
.height
= y2
-y
2806 # update coords also because they're relative to self.position
2807 for i
,p
in enumerate(self
.points
):
2808 self
.points
[i
] = (p
[0]+dx
,p
[1]+dy
)
2811 #----------------------------------------------------------------------------
2812 class ScribbleDrawingObject(DrawingObject
):
2813 """ DrawingObject subclass that represents a poly-line or polygon
2815 def __init__(self
, points
=[], *varg
, **kwarg
):
2816 DrawingObject
.__init
__(self
, *varg
, **kwarg
)
2817 self
.points
= list(points
)
2819 # =======================
2820 # == Selection Methods ==
2821 # =======================
2823 def addPoint(self
, x
,y
):
2824 self
.points
.append((x
-self
.position
.x
,y
-self
.position
.y
))
2825 self
._updateBoundingBox
()
2827 def getPoint(self
, idx
):
2828 x
,y
= self
.points
[idx
]
2829 return (x
+self
.position
.x
,y
+self
.position
.y
)
2831 def movePoint(self
, idx
, x
,y
):
2832 self
.points
[idx
] = (x
-self
.position
.x
,y
-self
.position
.y
)
2833 self
._updateBoundingBox
()
2835 def popPoint(self
, idx
=-1):
2836 self
.points
.pop(idx
)
2837 self
._updateBoundingBox
()
2840 # =============================
2841 # == Object Property Methods ==
2842 # =============================
2845 """ Return a copy of the object's internal data.
2847 This is used to save this DrawingObject to disk.
2850 data
= DrawingObject
.getData(self
)
2852 data
+= [list(self
.points
)]
2857 def setData(self
, data
):
2858 """ Set the object's internal data.
2860 'data' is a copy of the object's saved data, as returned by
2861 getData() above. This is used to restore a previously saved
2864 #data = copy.deepcopy(data) # Needed?
2865 d
= DrawingObject
.setData(self
, data
)
2868 self
.points
= d
.next()
2869 except StopIteration:
2870 raise ValueError('Not enough data in setData call')
2875 # =====================
2876 # == Private Methods ==
2877 # =====================
2878 def _privateDraw(self
, dc
, position
, selected
):
2879 """ Private routine to draw this DrawingObject.
2881 'dc' is the device context to use for drawing, while 'position' is
2882 the position in which to draw the object. If 'selected' is True,
2883 the object is drawn with selection handles. This private drawing
2884 routine assumes that the pen and brush have already been set by the
2887 dc
.SetBrush(wx
.TRANSPARENT_BRUSH
)
2888 dc
.DrawLines(self
.points
, position
.x
, position
.y
)
2890 def _updateBoundingBox(self
):
2891 x
= min([p
[0] for p
in self
.points
])
2892 y
= min([p
[1] for p
in self
.points
])
2893 x2
= max([p
[0] for p
in self
.points
])
2894 y2
= max([p
[1] for p
in self
.points
])
2897 self
.position
= wx
.Point(self
.position
.x
+ x
,self
.position
.y
+ y
)
2898 self
.size
= wx
.Size(x2
-x
, y2
-y
)
2899 #self.position.x += x
2900 #self.position.y += y
2901 #self.size.width = x2-x
2902 #self.size.height = y2-y
2903 # update coords also because they're relative to self.position
2904 for i
,p
in enumerate(self
.points
):
2905 self
.points
[i
] = (p
[0]+dx
,p
[1]+dy
)
2907 #----------------------------------------------------------------------------
2908 class RectDrawingObject(DrawingObject
):
2909 """ DrawingObject subclass that represents an axis-aligned rectangle.
2911 def __init__(self
, *varg
, **kwarg
):
2912 DrawingObject
.__init
__(self
, *varg
, **kwarg
)
2914 def objectContainsPoint(self
, x
, y
):
2915 """ Returns True iff this object contains the given point.
2917 This is used to determine if the user clicked on the object.
2919 # Firstly, ignore any points outside of the object's bounds.
2921 if x
< self
.position
.x
: return False
2922 if x
> self
.position
.x
+ self
.size
.x
: return False
2923 if y
< self
.position
.y
: return False
2924 if y
> self
.position
.y
+ self
.size
.y
: return False
2926 # Rectangles are easy -- they're always selected if the
2927 # point is within their bounds.
2930 # =====================
2931 # == Private Methods ==
2932 # =====================
2934 def _privateDraw(self
, dc
, position
, selected
):
2935 """ Private routine to draw this DrawingObject.
2937 'dc' is the device context to use for drawing, while 'position' is
2938 the position in which to draw the object. If 'selected' is True,
2939 the object is drawn with selection handles. This private drawing
2940 routine assumes that the pen and brush have already been set by the
2943 dc
.DrawRectangle(position
.x
, position
.y
,
2944 self
.size
.width
, self
.size
.height
)
2947 #----------------------------------------------------------------------------
2948 class EllipseDrawingObject(DrawingObject
):
2949 """ DrawingObject subclass that represents an axis-aligned ellipse.
2951 def __init__(self
, *varg
, **kwarg
):
2952 DrawingObject
.__init
__(self
, *varg
, **kwarg
)
2954 # =====================
2955 # == Private Methods ==
2956 # =====================
2957 def _privateDraw(self
, dc
, position
, selected
):
2958 """ Private routine to draw this DrawingObject.
2960 'dc' is the device context to use for drawing, while 'position' is
2961 the position in which to draw the object. If 'selected' is True,
2962 the object is drawn with selection handles. This private drawing
2963 routine assumes that the pen and brush have already been set by the
2966 dc
.DrawEllipse(position
.x
, position
.y
,
2967 self
.size
.width
, self
.size
.height
)
2972 #----------------------------------------------------------------------------
2973 class TextDrawingObject(DrawingObject
):
2974 """ DrawingObject subclass that holds text.
2976 Adds the following members to the base DrawingObject:
2977 'text' The object's text (obj_TEXT objects only).
2978 'textFont' The text object's font name.
2981 def __init__(self
, text
=None, *varg
, **kwarg
):
2982 DrawingObject
.__init
__(self
, *varg
, **kwarg
)
2985 self
.textFont
= wx
.SystemSettings_GetFont(wx
.SYS_DEFAULT_GUI_FONT
)
2988 # =============================
2989 # == Object Property Methods ==
2990 # =============================
2993 """ Return a copy of the object's internal data.
2995 This is used to save this DrawingObject to disk.
2998 data
= DrawingObject
.getData(self
)
3000 data
+= [self
.text
, self
.textFont
.GetNativeFontInfoDesc()]
3005 def setData(self
, data
):
3006 """ Set the object's internal data.
3008 'data' is a copy of the object's saved data, as returned by
3009 getData() above. This is used to restore a previously saved
3012 d
= DrawingObject
.setData(self
, data
)
3015 self
.text
= d
.next()
3017 self
.textFont
= wx
.FontFromNativeInfoString(desc
)
3018 except StopIteration:
3019 raise ValueError('Not enough data in setData call')
3024 def hasPropertyEditor(self
):
3027 def doPropertyEdit(self
, parent
):
3028 editor
= parent
.getTextEditor()
3029 editor
.SetTitle("Edit Text Object")
3030 editor
.objectToDialog(self
)
3031 if editor
.ShowModal() == wx
.ID_CANCEL
:
3035 parent
.saveUndoInfo()
3037 editor
.dialogToObject(self
)
3043 def setText(self
, text
):
3044 """ Set the text for this DrawingObject.
3050 """ Return this DrawingObject's text.
3055 def setFont(self
, font
):
3056 """ Set the font for this text DrawingObject.
3058 self
.textFont
= font
3062 """ Return this text DrawingObject's font.
3064 return self
.textFont
3068 # ============================
3069 # == Object Drawing Methods ==
3070 # ============================
3072 def draw(self
, dc
, selected
):
3073 """ Draw this DrawingObject into our window.
3075 'dc' is the device context to use for drawing. If 'selected' is
3076 True, the object is currently selected and should be drawn as such.
3078 dc
.SetTextForeground(self
.penColour
)
3079 dc
.SetTextBackground(self
.fillColour
)
3081 self
._privateDraw
(dc
, self
.position
, selected
)
3083 def objectContainsPoint(self
, x
, y
):
3084 """ Returns True iff this object contains the given point.
3086 This is used to determine if the user clicked on the object.
3088 # Firstly, ignore any points outside of the object's bounds.
3090 if x
< self
.position
.x
: return False
3091 if x
> self
.position
.x
+ self
.size
.x
: return False
3092 if y
< self
.position
.y
: return False
3093 if y
> self
.position
.y
+ self
.size
.y
: return False
3095 # Text is easy -- it's always selected if the
3096 # point is within its bounds.
3100 def fitToText(self
):
3101 """ Resize a text DrawingObject so that it fits it's text exactly.
3104 dummyWindow
= wx
.Frame(None, -1, "")
3105 dummyWindow
.SetFont(self
.textFont
)
3106 width
, height
= dummyWindow
.GetTextExtent(self
.text
)
3107 dummyWindow
.Destroy()
3109 self
.size
= wx
.Size(width
, height
)
3111 # =====================
3112 # == Private Methods ==
3113 # =====================
3115 def _privateDraw(self
, dc
, position
, selected
):
3116 """ Private routine to draw this DrawingObject.
3118 'dc' is the device context to use for drawing, while 'position' is
3119 the position in which to draw the object. If 'selected' is True,
3120 the object is drawn with selection handles. This private drawing
3121 routine assumes that the pen and brush have already been set by the
3124 dc
.SetFont(self
.textFont
)
3125 dc
.DrawText(self
.text
, position
.x
, position
.y
)
3129 #----------------------------------------------------------------------------
3130 class ToolPaletteToggleX(wx
.ToggleButton
):
3131 """ An icon appearing in the tool palette area of our sketching window.
3133 Note that this is actually implemented as a wx.Bitmap rather
3134 than as a wx.Icon. wx.Icon has a very specific meaning, and isn't
3135 appropriate for this more general use.
3138 def __init__(self
, parent
, iconID
, iconName
, toolTip
, mode
= wx
.ITEM_NORMAL
):
3139 """ Standard constructor.
3141 'parent' is the parent window this icon will be part of.
3142 'iconID' is the internal ID used for this icon.
3143 'iconName' is the name used for this icon.
3144 'toolTip' is the tool tip text to show for this icon.
3145 'mode' is one of wx.ITEM_NORMAL, wx.ITEM_CHECK, wx.ITEM_RADIO
3147 The icon name is used to get the appropriate bitmap for this icon.
3149 bmp
= wx
.Bitmap("images/" + iconName
+ "Icon.bmp", wx
.BITMAP_TYPE_BMP
)
3150 bmpsel
= wx
.Bitmap("images/" + iconName
+ "IconSel.bmp", wx
.BITMAP_TYPE_BMP
)
3152 wx
.ToggleButton
.__init
__(self
, parent
, iconID
,
3153 size
=(bmp
.GetWidth()+1, bmp
.GetHeight()+1)
3155 self
.SetLabel( iconName
)
3156 self
.SetToolTip(wx
.ToolTip(toolTip
))
3157 #self.SetBitmapLabel(bmp)
3158 #self.SetBitmapSelected(bmpsel)
3160 self
.iconID
= iconID
3161 self
.iconName
= iconName
3163 class ToolPaletteToggle(GenBitmapToggleButton
):
3164 """ An icon appearing in the tool palette area of our sketching window.
3166 Note that this is actually implemented as a wx.Bitmap rather
3167 than as a wx.Icon. wx.Icon has a very specific meaning, and isn't
3168 appropriate for this more general use.
3171 def __init__(self
, parent
, iconID
, iconName
, toolTip
, mode
= wx
.ITEM_NORMAL
):
3172 """ Standard constructor.
3174 'parent' is the parent window this icon will be part of.
3175 'iconID' is the internal ID used for this icon.
3176 'iconName' is the name used for this icon.
3177 'toolTip' is the tool tip text to show for this icon.
3178 'mode' is one of wx.ITEM_NORMAL, wx.ITEM_CHECK, wx.ITEM_RADIO
3180 The icon name is used to get the appropriate bitmap for this icon.
3182 bmp
= wx
.Bitmap("images/" + iconName
+ "Icon.bmp", wx
.BITMAP_TYPE_BMP
)
3183 bmpsel
= wx
.Bitmap("images/" + iconName
+ "IconSel.bmp", wx
.BITMAP_TYPE_BMP
)
3185 GenBitmapToggleButton
.__init
__(self
, parent
, iconID
, bitmap
=bmp
,
3186 size
=(bmp
.GetWidth()+1, bmp
.GetHeight()+1),
3187 style
=wx
.BORDER_NONE
)
3189 self
.SetToolTip(wx
.ToolTip(toolTip
))
3190 self
.SetBitmapLabel(bmp
)
3191 self
.SetBitmapSelected(bmpsel
)
3193 self
.iconID
= iconID
3194 self
.iconName
= iconName
3197 class ToolPaletteButton(GenBitmapButton
):
3198 """ An icon appearing in the tool palette area of our sketching window.
3200 Note that this is actually implemented as a wx.Bitmap rather
3201 than as a wx.Icon. wx.Icon has a very specific meaning, and isn't
3202 appropriate for this more general use.
3205 def __init__(self
, parent
, iconID
, iconName
, toolTip
):
3206 """ Standard constructor.
3208 'parent' is the parent window this icon will be part of.
3209 'iconID' is the internal ID used for this icon.
3210 'iconName' is the name used for this icon.
3211 'toolTip' is the tool tip text to show for this icon.
3213 The icon name is used to get the appropriate bitmap for this icon.
3215 bmp
= wx
.Bitmap("images/" + iconName
+ "Icon.bmp", wx
.BITMAP_TYPE_BMP
)
3216 GenBitmapButton
.__init
__(self
, parent
, iconID
, bitmap
=bmp
,
3217 size
=(bmp
.GetWidth()+1, bmp
.GetHeight()+1),
3218 style
=wx
.BORDER_NONE
)
3219 self
.SetToolTip(wx
.ToolTip(toolTip
))
3220 self
.SetBitmapLabel(bmp
)
3222 self
.iconID
= iconID
3223 self
.iconName
= iconName
3227 #----------------------------------------------------------------------------
3229 class ToolOptionIndicator(wx
.Window
):
3230 """ A visual indicator which shows the current tool options.
3232 def __init__(self
, parent
):
3233 """ Standard constructor.
3235 wx
.Window
.__init
__(self
, parent
, -1, wx
.DefaultPosition
, wx
.Size(52, 32))
3237 self
.penColour
= wx
.BLACK
3238 self
.fillColour
= wx
.WHITE
3241 # Win95 can only handle a 8x8 stipple bitmaps max
3242 #self.stippleBitmap = wx.BitmapFromBits("\xf0"*4 + "\x0f"*4,8,8)
3243 # ...but who uses Win95?
3244 self
.stippleBitmap
= wx
.BitmapFromBits("\xff\x00"*8+"\x00\xff"*8,16,16)
3245 self
.stippleBitmap
.SetMask(wx
.Mask(self
.stippleBitmap
))
3247 self
.Bind(wx
.EVT_PAINT
, self
.onPaint
)
3250 def setPenColour(self
, penColour
):
3251 """ Set the indicator's current pen colour.
3253 self
.penColour
= penColour
3257 def setFillColour(self
, fillColour
):
3258 """ Set the indicator's current fill colour.
3260 self
.fillColour
= fillColour
3264 def setLineSize(self
, lineSize
):
3265 """ Set the indicator's current pen colour.
3267 self
.lineSize
= lineSize
3271 def onPaint(self
, event
):
3272 """ Paint our tool option indicator.
3274 dc
= wx
.PaintDC(self
)
3277 dc
.SetPen(wx
.BLACK_PEN
)
3278 bgbrush
= wx
.Brush(wx
.WHITE
, wx
.STIPPLE_MASK_OPAQUE
)
3279 bgbrush
.SetStipple(self
.stippleBitmap
)
3280 dc
.SetTextForeground(wx
.LIGHT_GREY
)
3281 dc
.SetTextBackground(wx
.WHITE
)
3282 dc
.SetBrush(bgbrush
)
3283 dc
.DrawRectangle(0, 0, self
.GetSize().width
,self
.GetSize().height
)
3285 if self
.lineSize
== 0:
3286 dc
.SetPen(wx
.Pen(self
.penColour
, self
.lineSize
, wx
.TRANSPARENT
))
3288 dc
.SetPen(wx
.Pen(self
.penColour
, self
.lineSize
, wx
.SOLID
))
3289 dc
.SetBrush(wx
.Brush(self
.fillColour
, wx
.SOLID
))
3291 size
= self
.GetSize()
3294 radius
= min(size
)//2 - 5
3295 dc
.DrawCircle(ctrx
, ctry
, radius
)
3299 #----------------------------------------------------------------------------
3301 class EditTextObjectDialog(wx
.Dialog
):
3302 """ Dialog box used to edit the properties of a text object.
3304 The user can edit the object's text, font, size, and text style.
3307 def __init__(self
, parent
, title
):
3308 """ Standard constructor.
3310 wx
.Dialog
.__init
__(self
, parent
, -1, title
,
3311 style
=wx
.DEFAULT_DIALOG_STYLE|wx
.RESIZE_BORDER
)
3313 self
.textCtrl
= wx
.TextCtrl(
3314 self
, 1001, "Enter text here", style
=wx
.TE_PROCESS_ENTER|wx
.TE_RICH
,
3315 validator
=TextObjectValidator()
3317 extent
= self
.textCtrl
.GetFullTextExtent("Hy")
3318 lineHeight
= extent
[1] + extent
[3]
3319 self
.textCtrl
.SetSize(wx
.Size(-1, lineHeight
* 4))
3320 self
.curFont
= self
.textCtrl
.GetFont()
3321 self
.curClr
= wx
.BLACK
3323 self
.Bind(wx
.EVT_TEXT_ENTER
, self
._doEnter
, id=1001)
3325 fontBtn
= wx
.Button(self
, -1, "Select Font...")
3326 self
.Bind(wx
.EVT_BUTTON
, self
.OnSelectFont
, fontBtn
)
3328 gap
= wx
.LEFT | wx
.TOP | wx
.RIGHT
3330 self
.okButton
= wx
.Button(self
, wx
.ID_OK
, "&OK")
3331 self
.okButton
.SetDefault()
3332 self
.cancelButton
= wx
.Button(self
, wx
.ID_CANCEL
, "&Cancel")
3334 btnSizer
= wx
.StdDialogButtonSizer()
3336 btnSizer
.Add(self
.okButton
, 0, gap
, 5)
3337 btnSizer
.Add(self
.cancelButton
, 0, gap
, 5)
3339 sizer
= wx
.BoxSizer(wx
.VERTICAL
)
3340 sizer
.Add(self
.textCtrl
, 1, gap | wx
.EXPAND
, 5)
3341 sizer
.Add(fontBtn
, 0, gap | wx
.ALIGN_RIGHT
, 5)
3342 sizer
.Add((10, 10)) # Spacer.
3344 sizer
.Add(btnSizer
, 0, gap | wx
.ALIGN_CENTRE
, 5)
3346 self
.SetAutoLayout(True)
3347 self
.SetSizer(sizer
)
3350 self
.textCtrl
.SetFocus()
3353 def OnSelectFont(self
, evt
):
3354 """Shows the font dialog and sets the font of the sample text"""
3355 data
= wx
.FontData()
3356 data
.EnableEffects(True)
3357 data
.SetColour(self
.curClr
) # set colour
3358 data
.SetInitialFont(self
.curFont
)
3360 dlg
= wx
.FontDialog(self
, data
)
3362 if dlg
.ShowModal() == wx
.ID_OK
:
3363 data
= dlg
.GetFontData()
3364 font
= data
.GetChosenFont()
3365 colour
= data
.GetColour()
3368 self
.curClr
= colour
3370 self
.textCtrl
.SetFont(font
)
3371 # Update dialog for the new height of the text
3372 self
.GetSizer().Fit(self
)
3377 def objectToDialog(self
, obj
):
3378 """ Copy the properties of the given text object into the dialog box.
3380 self
.textCtrl
.SetValue(obj
.getText())
3381 self
.textCtrl
.SetSelection(0, len(obj
.getText()))
3383 self
.curFont
= obj
.getFont()
3384 self
.textCtrl
.SetFont(self
.curFont
)
3388 def dialogToObject(self
, obj
):
3389 """ Copy the properties from the dialog box into the given text object.
3391 obj
.setText(self
.textCtrl
.GetValue())
3392 obj
.setFont(self
.curFont
)
3395 # ======================
3396 # == Private Routines ==
3397 # ======================
3399 def _doEnter(self
, event
):
3400 """ Respond to the user hitting the ENTER key.
3402 We simulate clicking on the "OK" button.
3404 if self
.Validate(): self
.Show(False)
3406 #----------------------------------------------------------------------------
3408 class TextObjectValidator(wx
.PyValidator
):
3409 """ This validator is used to ensure that the user has entered something
3410 into the text object editor dialog's text field.
3413 """ Standard constructor.
3415 wx
.PyValidator
.__init
__(self
)
3419 """ Standard cloner.
3421 Note that every validator must implement the Clone() method.
3423 return TextObjectValidator()
3426 def Validate(self
, win
):
3427 """ Validate the contents of the given text control.
3429 textCtrl
= self
.GetWindow()
3430 text
= textCtrl
.GetValue()
3433 wx
.MessageBox("A text object must contain some text!", "Error")
3439 def TransferToWindow(self
):
3440 """ Transfer data from validator to window.
3442 The default implementation returns False, indicating that an error
3443 occurred. We simply return True, as we don't do any data transfer.
3445 return True # Prevent wx.Dialog from complaining.
3448 def TransferFromWindow(self
):
3449 """ Transfer data from window to validator.
3451 The default implementation returns False, indicating that an error
3452 occurred. We simply return True, as we don't do any data transfer.
3454 return True # Prevent wx.Dialog from complaining.
3456 #----------------------------------------------------------------------------
3458 class ExceptionHandler
:
3459 """ A simple error-handling class to write exceptions to a text file.
3461 Under MS Windows, the standard DOS console window doesn't scroll and
3462 closes as soon as the application exits, making it hard to find and
3463 view Python exceptions. This utility class allows you to handle Python
3464 exceptions in a more friendly manner.
3468 """ Standard constructor.
3471 if os
.path
.exists("errors.txt"):
3472 os
.remove("errors.txt") # Delete previous error log, if any.
3476 """ Write the given error message to a text file.
3478 Note that if the error message doesn't end in a carriage return, we
3479 have to buffer up the inputs until a carriage return is received.
3481 if (s
[-1] != "\n") and (s
[-1] != "\r"):
3482 self
._buff
= self
._buff
+ s
3489 f
= open("errors.txt", "a")
3493 if s
[:9] == "Traceback":
3494 # Tell the user than an exception occurred.
3495 wx
.MessageBox("An internal error has occurred.\nPlease " + \
3496 "refer to the 'errors.txt' file for details.",
3497 "Error", wx
.OK | wx
.CENTRE | wx
.ICON_EXCLAMATION
)
3501 pass # Don't recursively crash on errors.
3503 #----------------------------------------------------------------------------
3505 class SketchApp(wx
.App
):
3506 """ The main pySketch application object.
3509 """ Initialise the application.
3514 if len(sys
.argv
) == 1:
3515 # No file name was specified on the command line -> start with a
3517 frame
= DrawingFrame(None, -1, "Untitled")
3520 _docList
.append(frame
)
3522 # Load the file(s) specified on the command line.
3523 for arg
in sys
.argv
[1:]:
3524 fileName
= os
.path
.join(os
.getcwd(), arg
)
3525 if os
.path
.isfile(fileName
):
3526 frame
= DrawingFrame(None, -1,
3527 os
.path
.basename(fileName
),
3530 _docList
.append(frame
)
3534 #----------------------------------------------------------------------------
3537 """ Start up the pySketch application.
3541 # Redirect python exceptions to a log file.
3543 sys
.stderr
= ExceptionHandler()
3545 # Create and start the pySketch application.
3551 if __name__
== "__main__":