3 A simple object-oriented drawing program.
5 This is completely free software; please feel free to adapt or use this in
8 Author: Erik Westra (ewestra@wave.co.nz)
10 #########################################################################
14 pySketch requires wxPython version 2.3. If you are running an earlier
15 version, you need to patch your copy of wxPython to fix a bug which will
16 cause the "Edit Text Object" dialog box to crash.
18 To patch an earlier version of wxPython, edit the wxPython/windows.py file,
19 find the wxPyValidator.__init__ method and change the line which reads:
21 self._setSelf(self, wxPyValidator, 0)
25 self._setSelf(self, wxPyValidator, 1)
27 This fixes a known bug in wxPython 2.2.5 (and possibly earlier) which has
28 now been fixed in wxPython 2.3.
30 #########################################################################
34 * Add ARGV checking to see if a document was double-clicked on.
38 * Scrolling the window causes the drawing panel to be mucked up until you
39 refresh it. I've got no idea why.
41 * I suspect that the reference counting for some wxPoint objects is
42 getting mucked up; when the user quits, we get errors about being
43 unable to call del on a 'None' object.
46 import cPickle
, os
.path
48 from wx
.lib
.buttons
import GenBitmapButton
51 import traceback
, types
53 #----------------------------------------------------------------------------
55 #----------------------------------------------------------------------------
59 menu_UNDO
= 10001 # Edit menu items.
60 menu_SELECT_ALL
= 10002
61 menu_DUPLICATE
= 10003
62 menu_EDIT_TEXT
= 10004
65 menu_SELECT
= 10101 # Tools menu items.
71 menu_MOVE_FORWARD
= 10201 # Object menu items.
72 menu_MOVE_TO_FRONT
= 10202
73 menu_MOVE_BACKWARD
= 10203
74 menu_MOVE_TO_BACK
= 10204
76 menu_ABOUT
= 10205 # Help menu items.
86 # Our tool option IDs:
99 # DrawObject type IDs:
106 # Selection handle IDs:
111 handle_BOTTOM_LEFT
= 4
112 handle_BOTTOM_RIGHT
= 5
113 handle_START_POINT
= 6
116 # Dragging operations:
123 # Visual Feedback types:
129 # Mouse-event action parameter types:
134 # Size of the drawing page, in pixels.
139 #----------------------------------------------------------------------------
141 class DrawingFrame(wx
.Frame
):
142 """ A frame showing the contents of a single document. """
144 # ==========================================
145 # == Initialisation and Window Management ==
146 # ==========================================
148 def __init__(self
, parent
, id, title
, fileName
=None):
149 """ Standard constructor.
151 'parent', 'id' and 'title' are all passed to the standard wx.Frame
152 constructor. 'fileName' is the name and path of a saved file to
153 load into this frame, if any.
155 wx
.Frame
.__init
__(self
, parent
, id, title
,
156 style
= wx
.DEFAULT_FRAME_STYLE | wx
.WANTS_CHARS |
157 wx
.NO_FULL_REPAINT_ON_RESIZE
)
159 # Setup our menu bar.
161 menuBar
= wx
.MenuBar()
163 self
.fileMenu
= wx
.Menu()
164 self
.fileMenu
.Append(wx
.ID_NEW
, "New\tCTRL-N")
165 self
.fileMenu
.Append(wx
.ID_OPEN
, "Open...\tCTRL-O")
166 self
.fileMenu
.Append(wx
.ID_CLOSE
, "Close\tCTRL-W")
167 self
.fileMenu
.AppendSeparator()
168 self
.fileMenu
.Append(wx
.ID_SAVE
, "Save\tCTRL-S")
169 self
.fileMenu
.Append(wx
.ID_SAVEAS
, "Save As...")
170 self
.fileMenu
.Append(wx
.ID_REVERT
, "Revert...")
171 self
.fileMenu
.AppendSeparator()
172 self
.fileMenu
.Append(wx
.ID_EXIT
, "Quit\tCTRL-Q")
174 menuBar
.Append(self
.fileMenu
, "File")
176 self
.editMenu
= wx
.Menu()
177 self
.editMenu
.Append(menu_UNDO
, "Undo\tCTRL-Z")
178 self
.editMenu
.AppendSeparator()
179 self
.editMenu
.Append(menu_SELECT_ALL
, "Select All\tCTRL-A")
180 self
.editMenu
.AppendSeparator()
181 self
.editMenu
.Append(menu_DUPLICATE
, "Duplicate\tCTRL-D")
182 self
.editMenu
.Append(menu_EDIT_TEXT
, "Edit...\tCTRL-E")
183 self
.editMenu
.Append(menu_DELETE
, "Delete\tDEL")
185 menuBar
.Append(self
.editMenu
, "Edit")
187 self
.toolsMenu
= wx
.Menu()
188 self
.toolsMenu
.Append(menu_SELECT
, "Selection", kind
=wx
.ITEM_CHECK
)
189 self
.toolsMenu
.Append(menu_LINE
, "Line", kind
=wx
.ITEM_CHECK
)
190 self
.toolsMenu
.Append(menu_RECT
, "Rectangle", kind
=wx
.ITEM_CHECK
)
191 self
.toolsMenu
.Append(menu_ELLIPSE
, "Ellipse", kind
=wx
.ITEM_CHECK
)
192 self
.toolsMenu
.Append(menu_TEXT
, "Text", kind
=wx
.ITEM_CHECK
)
194 menuBar
.Append(self
.toolsMenu
, "Tools")
196 self
.objectMenu
= wx
.Menu()
197 self
.objectMenu
.Append(menu_MOVE_FORWARD
, "Move Forward")
198 self
.objectMenu
.Append(menu_MOVE_TO_FRONT
, "Move to Front\tCTRL-F")
199 self
.objectMenu
.Append(menu_MOVE_BACKWARD
, "Move Backward")
200 self
.objectMenu
.Append(menu_MOVE_TO_BACK
, "Move to Back\tCTRL-B")
202 menuBar
.Append(self
.objectMenu
, "Object")
204 self
.helpMenu
= wx
.Menu()
205 self
.helpMenu
.Append(menu_ABOUT
, "About pySketch...")
207 menuBar
.Append(self
.helpMenu
, "Help")
209 self
.SetMenuBar(menuBar
)
211 # Create our toolbar.
214 self
.toolbar
= self
.CreateToolBar(wx
.TB_HORIZONTAL |
215 wx
.NO_BORDER | wx
.TB_FLAT
)
217 self
.toolbar
.AddSimpleTool(wx
.ID_NEW
,
218 wx
.ArtProvider
.GetBitmap(wx
.ART_NORMAL_FILE
, wx
.ART_TOOLBAR
, tsize
),
220 self
.toolbar
.AddSimpleTool(wx
.ID_OPEN
,
221 wx
.ArtProvider
.GetBitmap(wx
.ART_FILE_OPEN
, wx
.ART_TOOLBAR
, tsize
),
223 self
.toolbar
.AddSimpleTool(wx
.ID_SAVE
,
224 wx
.ArtProvider
.GetBitmap(wx
.ART_FILE_SAVE
, wx
.ART_TOOLBAR
, tsize
),
226 self
.toolbar
.AddSeparator()
227 self
.toolbar
.AddSimpleTool(menu_UNDO
,
228 wx
.ArtProvider
.GetBitmap(wx
.ART_UNDO
, wx
.ART_TOOLBAR
, tsize
),
230 self
.toolbar
.AddSeparator()
231 self
.toolbar
.AddSimpleTool(menu_DUPLICATE
,
232 wx
.Bitmap("images/duplicate.bmp",
235 self
.toolbar
.AddSeparator()
236 self
.toolbar
.AddSimpleTool(menu_MOVE_FORWARD
,
237 wx
.Bitmap("images/moveForward.bmp",
240 self
.toolbar
.AddSimpleTool(menu_MOVE_BACKWARD
,
241 wx
.Bitmap("images/moveBack.bmp",
245 self
.toolbar
.Realize()
247 # Associate each menu/toolbar item with the method that handles that
250 (wx
.ID_NEW
, self
.doNew
),
251 (wx
.ID_OPEN
, self
.doOpen
),
252 (wx
.ID_CLOSE
, self
.doClose
),
253 (wx
.ID_SAVE
, self
.doSave
),
254 (wx
.ID_SAVEAS
, self
.doSaveAs
),
255 (wx
.ID_REVERT
, self
.doRevert
),
256 (wx
.ID_EXIT
, self
.doExit
),
258 (menu_UNDO
, self
.doUndo
),
259 (menu_SELECT_ALL
, self
.doSelectAll
),
260 (menu_DUPLICATE
, self
.doDuplicate
),
261 (menu_EDIT_TEXT
, self
.doEditText
),
262 (menu_DELETE
, self
.doDelete
),
264 (menu_SELECT
, self
.doChooseSelectTool
),
265 (menu_LINE
, self
.doChooseLineTool
),
266 (menu_RECT
, self
.doChooseRectTool
),
267 (menu_ELLIPSE
, self
.doChooseEllipseTool
),
268 (menu_TEXT
, self
.doChooseTextTool
),
270 (menu_MOVE_FORWARD
, self
.doMoveForward
),
271 (menu_MOVE_TO_FRONT
, self
.doMoveToFront
),
272 (menu_MOVE_BACKWARD
, self
.doMoveBackward
),
273 (menu_MOVE_TO_BACK
, self
.doMoveToBack
),
275 (menu_ABOUT
, self
.doShowAbout
)]
276 for combo
in menuHandlers
:
278 self
.Bind(wx
.EVT_MENU
, handler
, id = id)
281 # Install our own method to handle closing the window. This allows us
282 # to ask the user if he/she wants to save before closing the window, as
283 # well as keeping track of which windows are currently open.
285 self
.Bind(wx
.EVT_CLOSE
, self
.doClose
)
287 # Install our own method for handling keystrokes. We use this to let
288 # the user move the selected object(s) around using the arrow keys.
290 self
.Bind(wx
.EVT_CHAR_HOOK
, self
.onKeyEvent
)
292 # Setup our top-most panel. This holds the entire contents of the
293 # window, excluding the menu bar.
295 self
.topPanel
= wx
.Panel(self
, -1, style
=wx
.SIMPLE_BORDER
)
297 # Setup our tool palette, with all our drawing tools and option icons.
299 self
.toolPalette
= wx
.BoxSizer(wx
.VERTICAL
)
301 self
.selectIcon
= ToolPaletteIcon(self
.topPanel
, id_SELECT
,
302 "select", "Selection Tool")
303 self
.lineIcon
= ToolPaletteIcon(self
.topPanel
, id_LINE
,
305 self
.rectIcon
= ToolPaletteIcon(self
.topPanel
, id_RECT
,
306 "rect", "Rectangle Tool")
307 self
.ellipseIcon
= ToolPaletteIcon(self
.topPanel
, id_ELLIPSE
,
308 "ellipse", "Ellipse Tool")
309 self
.textIcon
= ToolPaletteIcon(self
.topPanel
, id_TEXT
,
312 toolSizer
= wx
.GridSizer(0, 2, 5, 5)
313 toolSizer
.Add(self
.selectIcon
)
314 toolSizer
.Add((0, 0)) # Gap to make tool icons line up nicely.
315 toolSizer
.Add(self
.lineIcon
)
316 toolSizer
.Add(self
.rectIcon
)
317 toolSizer
.Add(self
.ellipseIcon
)
318 toolSizer
.Add(self
.textIcon
)
320 self
.optionIndicator
= ToolOptionIndicator(self
.topPanel
)
321 self
.optionIndicator
.SetToolTip(
322 wx
.ToolTip("Shows Current Pen/Fill/Line Size Settings"))
324 optionSizer
= wx
.BoxSizer(wx
.HORIZONTAL
)
326 self
.penOptIcon
= ToolPaletteIcon(self
.topPanel
, id_PEN_OPT
,
327 "penOpt", "Set Pen Colour")
328 self
.fillOptIcon
= ToolPaletteIcon(self
.topPanel
, id_FILL_OPT
,
329 "fillOpt", "Set Fill Colour")
330 self
.lineOptIcon
= ToolPaletteIcon(self
.topPanel
, id_LINE_OPT
,
331 "lineOpt", "Set Line Size")
333 margin
= wx
.LEFT | wx
.RIGHT
334 optionSizer
.Add(self
.penOptIcon
, 0, margin
, 1)
335 optionSizer
.Add(self
.fillOptIcon
, 0, margin
, 1)
336 optionSizer
.Add(self
.lineOptIcon
, 0, margin
, 1)
338 margin
= wx
.TOP | wx
.LEFT | wx
.RIGHT | wx
.ALIGN_CENTRE
339 self
.toolPalette
.Add(toolSizer
, 0, margin
, 5)
340 self
.toolPalette
.Add((0, 0), 0, margin
, 5) # Spacer.
341 self
.toolPalette
.Add(self
.optionIndicator
, 0, margin
, 5)
342 self
.toolPalette
.Add(optionSizer
, 0, margin
, 5)
344 # Make the tool palette icons respond when the user clicks on them.
346 self
.selectIcon
.Bind(wx
.EVT_BUTTON
, self
.onToolIconClick
)
347 self
.lineIcon
.Bind(wx
.EVT_BUTTON
, self
.onToolIconClick
)
348 self
.rectIcon
.Bind(wx
.EVT_BUTTON
, self
.onToolIconClick
)
349 self
.ellipseIcon
.Bind(wx
.EVT_BUTTON
, self
.onToolIconClick
)
350 self
.textIcon
.Bind(wx
.EVT_BUTTON
, self
.onToolIconClick
)
351 self
.penOptIcon
.Bind(wx
.EVT_BUTTON
, self
.onPenOptionIconClick
)
352 self
.fillOptIcon
.Bind(wx
.EVT_BUTTON
, self
.onFillOptionIconClick
)
353 self
.lineOptIcon
.Bind(wx
.EVT_BUTTON
, self
.onLineOptionIconClick
)
355 # Setup the main drawing area.
357 self
.drawPanel
= wx
.ScrolledWindow(self
.topPanel
, -1,
358 style
=wx
.SUNKEN_BORDER
)
359 self
.drawPanel
.SetBackgroundColour(wx
.WHITE
)
361 self
.drawPanel
.EnableScrolling(True, True)
362 self
.drawPanel
.SetScrollbars(20, 20, PAGE_WIDTH
/ 20, PAGE_HEIGHT
/ 20)
364 self
.drawPanel
.Bind(wx
.EVT_LEFT_DOWN
, self
.onMouseEvent
)
365 self
.drawPanel
.Bind(wx
.EVT_LEFT_DCLICK
, self
.onDoubleClickEvent
)
366 self
.drawPanel
.Bind(wx
.EVT_RIGHT_DOWN
, self
.onRightClick
)
367 self
.drawPanel
.Bind(wx
.EVT_MOTION
, self
.onMouseEvent
)
368 self
.drawPanel
.Bind(wx
.EVT_LEFT_UP
, self
.onMouseEvent
)
369 self
.drawPanel
.Bind(wx
.EVT_PAINT
, self
.onPaintEvent
)
371 # Position everything in the window.
373 topSizer
= wx
.BoxSizer(wx
.HORIZONTAL
)
374 topSizer
.Add(self
.toolPalette
, 0)
375 topSizer
.Add(self
.drawPanel
, 1, wx
.EXPAND
)
377 self
.topPanel
.SetAutoLayout(True)
378 self
.topPanel
.SetSizer(topSizer
)
380 self
.SetSizeHints(250, 200)
381 self
.SetSize(wx
.Size(600, 400))
383 # Select an initial tool.
386 self
._setCurrentTool
(self
.selectIcon
)
388 # Setup our frame to hold the contents of a sketch document.
391 self
.fileName
= fileName
392 self
.contents
= [] # front-to-back ordered list of DrawingObjects.
393 self
.selection
= [] # List of selected DrawingObjects.
394 self
.undoInfo
= None # Saved contents for undo.
395 self
.dragMode
= drag_NONE
# Current mouse-drag mode.
397 if self
.fileName
!= None:
402 # Finally, set our initial pen, fill and line options.
404 self
.penColour
= wx
.BLACK
405 self
.fillColour
= wx
.WHITE
408 # ============================
409 # == Event Handling Methods ==
410 # ============================
412 def onToolIconClick(self
, event
):
413 """ Respond to the user clicking on one of our tool icons.
415 iconID
= event
.GetEventObject().GetId()
416 if iconID
== id_SELECT
: self
.doChooseSelectTool()
417 elif iconID
== id_LINE
: self
.doChooseLineTool()
418 elif iconID
== id_RECT
: self
.doChooseRectTool()
419 elif iconID
== id_ELLIPSE
: self
.doChooseEllipseTool()
420 elif iconID
== id_TEXT
: self
.doChooseTextTool()
421 else: wx
.Bell(); print "1"
424 def onPenOptionIconClick(self
, event
):
425 """ Respond to the user clicking on the "Pen Options" icon.
427 data
= wx
.ColourData()
428 if len(self
.selection
) == 1:
429 data
.SetColour(self
.selection
[0].getPenColour())
431 data
.SetColour(self
.penColour
)
433 dialog
= wx
.ColourDialog(self
, data
)
434 if dialog
.ShowModal() == wx
.ID_OK
:
435 c
= dialog
.GetColourData().GetColour()
436 self
._setPenColour
(wx
.Colour(c
.Red(), c
.Green(), c
.Blue()))
440 def onFillOptionIconClick(self
, event
):
441 """ Respond to the user clicking on the "Fill Options" icon.
443 data
= wx
.ColourData()
444 if len(self
.selection
) == 1:
445 data
.SetColour(self
.selection
[0].getFillColour())
447 data
.SetColour(self
.fillColour
)
449 dialog
= wx
.ColourDialog(self
, data
)
450 if dialog
.ShowModal() == wx
.ID_OK
:
451 c
= dialog
.GetColourData().GetColour()
452 self
._setFillColour
(wx
.Colour(c
.Red(), c
.Green(), c
.Blue()))
455 def onLineOptionIconClick(self
, event
):
456 """ Respond to the user clicking on the "Line Options" icon.
458 if len(self
.selection
) == 1:
459 menu
= self
._buildLineSizePopup
(self
.selection
[0].getLineSize())
461 menu
= self
._buildLineSizePopup
(self
.lineSize
)
463 pos
= self
.lineOptIcon
.GetPosition()
464 pos
.y
= pos
.y
+ self
.lineOptIcon
.GetSize().height
465 self
.PopupMenu(menu
, pos
)
469 def onKeyEvent(self
, event
):
470 """ Respond to a keypress event.
472 We make the arrow keys move the selected object(s) by one pixel in
475 if event
.GetKeyCode() == wx
.WXK_UP
:
476 self
._moveObject
(0, -1)
477 elif event
.GetKeyCode() == wx
.WXK_DOWN
:
478 self
._moveObject
(0, 1)
479 elif event
.GetKeyCode() == wx
.WXK_LEFT
:
480 self
._moveObject
(-1, 0)
481 elif event
.GetKeyCode() == wx
.WXK_RIGHT
:
482 self
._moveObject
(1, 0)
487 def onMouseEvent(self
, event
):
488 """ Respond to the user clicking on our main drawing panel.
490 How we respond depends on the currently selected tool.
492 if not (event
.LeftDown() or event
.Dragging() or event
.LeftUp()):
493 return # Ignore mouse movement without click/drag.
495 if self
.curTool
== self
.selectIcon
:
496 feedbackType
= feedback_RECT
497 action
= self
.selectByRectangle
498 actionParam
= param_RECT
501 elif self
.curTool
== self
.lineIcon
:
502 feedbackType
= feedback_LINE
503 action
= self
.createLine
504 actionParam
= param_LINE
507 elif self
.curTool
== self
.rectIcon
:
508 feedbackType
= feedback_RECT
509 action
= self
.createRect
510 actionParam
= param_RECT
513 elif self
.curTool
== self
.ellipseIcon
:
514 feedbackType
= feedback_ELLIPSE
515 action
= self
.createEllipse
516 actionParam
= param_RECT
519 elif self
.curTool
== self
.textIcon
:
520 feedbackType
= feedback_RECT
521 action
= self
.createText
522 actionParam
= param_RECT
530 mousePt
= self
._getEventCoordinates
(event
)
532 obj
, handle
= self
._getObjectAndSelectionHandleAt
(mousePt
)
534 if selecting
and (obj
!= None) and (handle
!= handle_NONE
):
536 # The user clicked on an object's selection handle. Let the
537 # user resize the clicked-on object.
539 self
.dragMode
= drag_RESIZE
540 self
.resizeObject
= obj
542 if obj
.getType() == obj_LINE
:
543 self
.resizeFeedback
= feedback_LINE
544 pos
= obj
.getPosition()
545 startPt
= wx
.Point(pos
.x
+ obj
.getStartPt().x
,
546 pos
.y
+ obj
.getStartPt().y
)
547 endPt
= wx
.Point(pos
.x
+ obj
.getEndPt().x
,
548 pos
.y
+ obj
.getEndPt().y
)
549 if handle
== handle_START_POINT
:
550 self
.resizeAnchor
= endPt
551 self
.resizeFloater
= startPt
553 self
.resizeAnchor
= startPt
554 self
.resizeFloater
= endPt
556 self
.resizeFeedback
= feedback_RECT
557 pos
= obj
.getPosition()
559 topLeft
= wx
.Point(pos
.x
, pos
.y
)
560 topRight
= wx
.Point(pos
.x
+ size
.width
, pos
.y
)
561 botLeft
= wx
.Point(pos
.x
, pos
.y
+ size
.height
)
562 botRight
= wx
.Point(pos
.x
+ size
.width
, pos
.y
+ size
.height
)
564 if handle
== handle_TOP_LEFT
:
565 self
.resizeAnchor
= botRight
566 self
.resizeFloater
= topLeft
567 elif handle
== handle_TOP_RIGHT
:
568 self
.resizeAnchor
= botLeft
569 self
.resizeFloater
= topRight
570 elif handle
== handle_BOTTOM_LEFT
:
571 self
.resizeAnchor
= topRight
572 self
.resizeFloater
= botLeft
573 elif handle
== handle_BOTTOM_RIGHT
:
574 self
.resizeAnchor
= topLeft
575 self
.resizeFloater
= botRight
578 self
.resizeOffsetX
= self
.resizeFloater
.x
- mousePt
.x
579 self
.resizeOffsetY
= self
.resizeFloater
.y
- mousePt
.y
580 endPt
= wx
.Point(self
.curPt
.x
+ self
.resizeOffsetX
,
581 self
.curPt
.y
+ self
.resizeOffsetY
)
582 self
._drawVisualFeedback
(self
.resizeAnchor
, endPt
,
583 self
.resizeFeedback
, False)
585 elif selecting
and (self
._getObjectAt
(mousePt
) != None):
587 # The user clicked on an object to select it. If the user
588 # drags, he/she will move the object.
590 self
.select(self
._getObjectAt
(mousePt
))
591 self
.dragMode
= drag_MOVE
592 self
.moveOrigin
= mousePt
594 self
._drawObjectOutline
(0, 0)
598 # The user is dragging out a selection rect or new object.
600 self
.dragOrigin
= mousePt
602 self
.drawPanel
.SetCursor(wx
.CROSS_CURSOR
)
603 self
.drawPanel
.CaptureMouse()
604 self
._drawVisualFeedback
(mousePt
, mousePt
, feedbackType
,
606 self
.dragMode
= drag_DRAG
612 if self
.dragMode
== drag_RESIZE
:
614 # We're resizing an object.
616 mousePt
= self
._getEventCoordinates
(event
)
617 if (self
.curPt
.x
!= mousePt
.x
) or (self
.curPt
.y
!= mousePt
.y
):
618 # Erase previous visual feedback.
619 endPt
= wx
.Point(self
.curPt
.x
+ self
.resizeOffsetX
,
620 self
.curPt
.y
+ self
.resizeOffsetY
)
621 self
._drawVisualFeedback
(self
.resizeAnchor
, endPt
,
622 self
.resizeFeedback
, False)
624 # Draw new visual feedback.
625 endPt
= wx
.Point(self
.curPt
.x
+ self
.resizeOffsetX
,
626 self
.curPt
.y
+ self
.resizeOffsetY
)
627 self
._drawVisualFeedback
(self
.resizeAnchor
, endPt
,
628 self
.resizeFeedback
, False)
630 elif self
.dragMode
== drag_MOVE
:
632 # We're moving a selected object.
634 mousePt
= self
._getEventCoordinates
(event
)
635 if (self
.curPt
.x
!= mousePt
.x
) or (self
.curPt
.y
!= mousePt
.y
):
636 # Erase previous visual feedback.
637 self
._drawObjectOutline
(self
.curPt
.x
- self
.moveOrigin
.x
,
638 self
.curPt
.y
- self
.moveOrigin
.y
)
640 # Draw new visual feedback.
641 self
._drawObjectOutline
(self
.curPt
.x
- self
.moveOrigin
.x
,
642 self
.curPt
.y
- self
.moveOrigin
.y
)
644 elif self
.dragMode
== drag_DRAG
:
646 # We're dragging out a new object or selection rect.
648 mousePt
= self
._getEventCoordinates
(event
)
649 if (self
.curPt
.x
!= mousePt
.x
) or (self
.curPt
.y
!= mousePt
.y
):
650 # Erase previous visual feedback.
651 self
._drawVisualFeedback
(self
.dragOrigin
, self
.curPt
,
652 feedbackType
, dashedLine
)
654 # Draw new visual feedback.
655 self
._drawVisualFeedback
(self
.dragOrigin
, self
.curPt
,
656 feedbackType
, dashedLine
)
662 if self
.dragMode
== drag_RESIZE
:
664 # We're resizing an object.
666 mousePt
= self
._getEventCoordinates
(event
)
667 # Erase last visual feedback.
668 endPt
= wx
.Point(self
.curPt
.x
+ self
.resizeOffsetX
,
669 self
.curPt
.y
+ self
.resizeOffsetY
)
670 self
._drawVisualFeedback
(self
.resizeAnchor
, endPt
,
671 self
.resizeFeedback
, False)
673 resizePt
= wx
.Point(mousePt
.x
+ self
.resizeOffsetX
,
674 mousePt
.y
+ self
.resizeOffsetY
)
676 if (self
.resizeFloater
.x
!= resizePt
.x
) or \
677 (self
.resizeFloater
.y
!= resizePt
.y
):
678 self
._resizeObject
(self
.resizeObject
,
683 self
.drawPanel
.Refresh() # Clean up after empty resize.
685 elif self
.dragMode
== drag_MOVE
:
687 # We're moving a selected object.
689 mousePt
= self
._getEventCoordinates
(event
)
690 # Erase last visual feedback.
691 self
._drawObjectOutline
(self
.curPt
.x
- self
.moveOrigin
.x
,
692 self
.curPt
.y
- self
.moveOrigin
.y
)
693 if (self
.moveOrigin
.x
!= mousePt
.x
) or \
694 (self
.moveOrigin
.y
!= mousePt
.y
):
695 self
._moveObject
(mousePt
.x
- self
.moveOrigin
.x
,
696 mousePt
.y
- self
.moveOrigin
.y
)
698 self
.drawPanel
.Refresh() # Clean up after empty drag.
700 elif self
.dragMode
== drag_DRAG
:
702 # We're dragging out a new object or selection rect.
704 mousePt
= self
._getEventCoordinates
(event
)
705 # Erase last visual feedback.
706 self
._drawVisualFeedback
(self
.dragOrigin
, self
.curPt
,
707 feedbackType
, dashedLine
)
708 self
.drawPanel
.ReleaseMouse()
709 self
.drawPanel
.SetCursor(wx
.STANDARD_CURSOR
)
710 # Perform the appropriate action for the current tool.
711 if actionParam
== param_RECT
:
712 x1
= min(self
.dragOrigin
.x
, self
.curPt
.x
)
713 y1
= min(self
.dragOrigin
.y
, self
.curPt
.y
)
714 x2
= max(self
.dragOrigin
.x
, self
.curPt
.x
)
715 y2
= max(self
.dragOrigin
.y
, self
.curPt
.y
)
723 if ((x2
-x1
) < 8) or ((y2
-y1
) < 8): return # Too small.
725 action(x1
, y1
, x2
-x1
, y2
-y1
)
726 elif actionParam
== param_LINE
:
727 action(self
.dragOrigin
.x
, self
.dragOrigin
.y
,
728 self
.curPt
.x
, self
.curPt
.y
)
730 self
.dragMode
= drag_NONE
# We've finished with this mouse event.
734 def onDoubleClickEvent(self
, event
):
735 """ Respond to a double-click within our drawing panel.
737 mousePt
= self
._getEventCoordinates
(event
)
738 obj
= self
._getObjectAt
(mousePt
)
739 if obj
== None: return
741 # Let the user edit the given object.
743 if obj
.getType() == obj_TEXT
:
744 editor
= EditTextObjectDialog(self
, "Edit Text Object")
745 editor
.objectToDialog(obj
)
746 if editor
.ShowModal() == wx
.ID_CANCEL
:
752 editor
.dialogToObject(obj
)
756 self
.drawPanel
.Refresh()
762 def onRightClick(self
, event
):
763 """ Respond to the user right-clicking within our drawing panel.
765 We select the clicked-on item, if necessary, and display a pop-up
766 menu of available options which can be applied to the selected
769 mousePt
= self
._getEventCoordinates
(event
)
770 obj
= self
._getObjectAt
(mousePt
)
772 if obj
== None: return # Nothing selected.
774 # Select the clicked-on object.
778 # Build our pop-up menu.
781 menu
.Append(menu_DUPLICATE
, "Duplicate")
782 menu
.Append(menu_EDIT_TEXT
, "Edit...")
783 menu
.Append(menu_DELETE
, "Delete")
784 menu
.AppendSeparator()
785 menu
.Append(menu_MOVE_FORWARD
, "Move Forward")
786 menu
.Append(menu_MOVE_TO_FRONT
, "Move to Front")
787 menu
.Append(menu_MOVE_BACKWARD
, "Move Backward")
788 menu
.Append(menu_MOVE_TO_BACK
, "Move to Back")
790 menu
.Enable(menu_EDIT_TEXT
, obj
.getType() == obj_TEXT
)
791 menu
.Enable(menu_MOVE_FORWARD
, obj
!= self
.contents
[0])
792 menu
.Enable(menu_MOVE_TO_FRONT
, obj
!= self
.contents
[0])
793 menu
.Enable(menu_MOVE_BACKWARD
, obj
!= self
.contents
[-1])
794 menu
.Enable(menu_MOVE_TO_BACK
, obj
!= self
.contents
[-1])
796 EVT_MENU(self
, menu_DUPLICATE
, self
.doDuplicate
)
797 EVT_MENU(self
, menu_EDIT_TEXT
, self
.doEditText
)
798 EVT_MENU(self
, menu_DELETE
, self
.doDelete
)
799 EVT_MENU(self
, menu_MOVE_FORWARD
, self
.doMoveForward
)
800 EVT_MENU(self
, menu_MOVE_TO_FRONT
, self
.doMoveToFront
)
801 EVT_MENU(self
, menu_MOVE_BACKWARD
, self
.doMoveBackward
)
802 EVT_MENU(self
, menu_MOVE_TO_BACK
, self
.doMoveToBack
)
804 # Show the pop-up menu.
806 clickPt
= wx
.Point(mousePt
.x
+ self
.drawPanel
.GetPosition().x
,
807 mousePt
.y
+ self
.drawPanel
.GetPosition().y
)
808 self
.drawPanel
.PopupMenu(menu
, clickPt
)
812 def onPaintEvent(self
, event
):
813 """ Respond to a request to redraw the contents of our drawing panel.
815 dc
= wx
.PaintDC(self
.drawPanel
)
816 self
.drawPanel
.PrepareDC(dc
)
819 for i
in range(len(self
.contents
)-1, -1, -1):
820 obj
= self
.contents
[i
]
821 if obj
in self
.selection
:
828 # ==========================
829 # == Menu Command Methods ==
830 # ==========================
832 def doNew(self
, event
):
833 """ Respond to the "New" menu command.
836 newFrame
= DrawingFrame(None, -1, "Untitled")
838 _docList
.append(newFrame
)
841 def doOpen(self
, event
):
842 """ Respond to the "Open" menu command.
847 fileName
= wx
.FileSelector("Open File", default_extension
="psk",
848 flags
= wx
.OPEN | wx
.FILE_MUST_EXIST
)
849 if fileName
== "": return
850 fileName
= os
.path
.join(os
.getcwd(), fileName
)
853 title
= os
.path
.basename(fileName
)
855 if (self
.fileName
== None) and (len(self
.contents
) == 0):
856 # Load contents into current (empty) document.
857 self
.fileName
= fileName
858 self
.SetTitle(os
.path
.basename(fileName
))
861 # Open a new frame for this document.
862 newFrame
= DrawingFrame(None, -1, os
.path
.basename(fileName
),
865 _docList
.append(newFrame
)
868 def doClose(self
, event
):
869 """ Respond to the "Close" menu command.
874 if not self
.askIfUserWantsToSave("closing"): return
876 _docList
.remove(self
)
880 def doSave(self
, event
):
881 """ Respond to the "Save" menu command.
883 if self
.fileName
!= None:
887 def doSaveAs(self
, event
):
888 """ Respond to the "Save As" menu command.
890 if self
.fileName
== None:
893 default
= self
.fileName
896 fileName
= wx
.FileSelector("Save File As", "Saving",
897 default_filename
=default
,
898 default_extension
="psk",
900 flags
= wx
.SAVE | wx
.OVERWRITE_PROMPT
)
901 if fileName
== "": return # User cancelled.
902 fileName
= os
.path
.join(os
.getcwd(), fileName
)
905 title
= os
.path
.basename(fileName
)
908 self
.fileName
= fileName
912 def doRevert(self
, event
):
913 """ Respond to the "Revert" menu command.
915 if not self
.dirty
: return
917 if wx
.MessageBox("Discard changes made to this document?", "Confirm",
918 style
= wx
.OK | wx
.CANCEL | wx
.ICON_QUESTION
,
919 parent
=self
) == wx
.CANCEL
: return
923 def doExit(self
, event
):
924 """ Respond to the "Quit" menu command.
926 global _docList
, _app
928 if not doc
.dirty
: continue
930 if not doc
.askIfUserWantsToSave("quitting"): return
937 def doUndo(self
, event
):
938 """ Respond to the "Undo" menu command.
940 if self
.undoInfo
== None: return
942 undoData
= self
.undoInfo
943 self
._saveUndoInfo
() # For undoing the undo...
947 for type, data
in undoData
["contents"]:
948 obj
= DrawingObject(type)
950 self
.contents
.append(obj
)
953 for i
in undoData
["selection"]:
954 self
.selection
.append(self
.contents
[i
])
957 self
.drawPanel
.Refresh()
961 def doSelectAll(self
, event
):
962 """ Respond to the "Select All" menu command.
967 def doDuplicate(self
, event
):
968 """ Respond to the "Duplicate" menu command.
973 for obj
in self
.contents
:
974 if obj
in self
.selection
:
975 newObj
= DrawingObject(obj
.getType())
976 newObj
.setData(obj
.getData())
977 pos
= obj
.getPosition()
978 newObj
.setPosition(wx
.Point(pos
.x
+ 10, pos
.y
+ 10))
981 self
.contents
= objs
+ self
.contents
983 self
.selectMany(objs
)
986 def doEditText(self
, event
):
987 """ Respond to the "Edit Text" menu command.
989 if len(self
.selection
) != 1: return
991 obj
= self
.selection
[0]
992 if obj
.getType() != obj_TEXT
: return
994 editor
= EditTextObjectDialog(self
, "Edit Text Object")
995 editor
.objectToDialog(obj
)
996 if editor
.ShowModal() == wx
.ID_CANCEL
:
1000 self
._saveUndoInfo
()
1002 editor
.dialogToObject(obj
)
1006 self
.drawPanel
.Refresh()
1010 def doDelete(self
, event
):
1011 """ Respond to the "Delete" menu command.
1013 self
._saveUndoInfo
()
1015 for obj
in self
.selection
:
1016 self
.contents
.remove(obj
)
1021 def doChooseSelectTool(self
, event
=None):
1022 """ Respond to the "Select Tool" menu command.
1024 self
._setCurrentTool
(self
.selectIcon
)
1025 self
.drawPanel
.SetCursor(wx
.STANDARD_CURSOR
)
1029 def doChooseLineTool(self
, event
=None):
1030 """ Respond to the "Line Tool" menu command.
1032 self
._setCurrentTool
(self
.lineIcon
)
1033 self
.drawPanel
.SetCursor(wx
.CROSS_CURSOR
)
1038 def doChooseRectTool(self
, event
=None):
1039 """ Respond to the "Rect Tool" menu command.
1041 self
._setCurrentTool
(self
.rectIcon
)
1042 self
.drawPanel
.SetCursor(wx
.CROSS_CURSOR
)
1047 def doChooseEllipseTool(self
, event
=None):
1048 """ Respond to the "Ellipse Tool" menu command.
1050 self
._setCurrentTool
(self
.ellipseIcon
)
1051 self
.drawPanel
.SetCursor(wx
.CROSS_CURSOR
)
1056 def doChooseTextTool(self
, event
=None):
1057 """ Respond to the "Text Tool" menu command.
1059 self
._setCurrentTool
(self
.textIcon
)
1060 self
.drawPanel
.SetCursor(wx
.CROSS_CURSOR
)
1065 def doMoveForward(self
, event
):
1066 """ Respond to the "Move Forward" menu command.
1068 if len(self
.selection
) != 1: return
1070 self
._saveUndoInfo
()
1072 obj
= self
.selection
[0]
1073 index
= self
.contents
.index(obj
)
1074 if index
== 0: return
1076 del self
.contents
[index
]
1077 self
.contents
.insert(index
-1, obj
)
1079 self
.drawPanel
.Refresh()
1083 def doMoveToFront(self
, event
):
1084 """ Respond to the "Move to Front" menu command.
1086 if len(self
.selection
) != 1: return
1088 self
._saveUndoInfo
()
1090 obj
= self
.selection
[0]
1091 self
.contents
.remove(obj
)
1092 self
.contents
.insert(0, obj
)
1094 self
.drawPanel
.Refresh()
1098 def doMoveBackward(self
, event
):
1099 """ Respond to the "Move Backward" menu command.
1101 if len(self
.selection
) != 1: return
1103 self
._saveUndoInfo
()
1105 obj
= self
.selection
[0]
1106 index
= self
.contents
.index(obj
)
1107 if index
== len(self
.contents
) - 1: return
1109 del self
.contents
[index
]
1110 self
.contents
.insert(index
+1, obj
)
1112 self
.drawPanel
.Refresh()
1116 def doMoveToBack(self
, event
):
1117 """ Respond to the "Move to Back" menu command.
1119 if len(self
.selection
) != 1: return
1121 self
._saveUndoInfo
()
1123 obj
= self
.selection
[0]
1124 self
.contents
.remove(obj
)
1125 self
.contents
.append(obj
)
1127 self
.drawPanel
.Refresh()
1131 def doShowAbout(self
, event
):
1132 """ Respond to the "About pySketch" menu command.
1134 dialog
= wx
.Dialog(self
, -1, "About pySketch") # ,
1135 #style=wx.DIALOG_MODAL | wx.STAY_ON_TOP)
1136 dialog
.SetBackgroundColour(wx
.WHITE
)
1138 panel
= wx
.Panel(dialog
, -1)
1139 panel
.SetBackgroundColour(wx
.WHITE
)
1141 panelSizer
= wx
.BoxSizer(wx
.VERTICAL
)
1143 boldFont
= wx
.Font(panel
.GetFont().GetPointSize(),
1144 panel
.GetFont().GetFamily(),
1147 logo
= wx
.StaticBitmap(panel
, -1, wx
.Bitmap("images/logo.bmp",
1148 wx
.BITMAP_TYPE_BMP
))
1150 lab1
= wx
.StaticText(panel
, -1, "pySketch")
1151 lab1
.SetFont(wx
.Font(36, boldFont
.GetFamily(), wx
.ITALIC
, wx
.BOLD
))
1152 lab1
.SetSize(lab1
.GetBestSize())
1154 imageSizer
= wx
.BoxSizer(wx
.HORIZONTAL
)
1155 imageSizer
.Add(logo
, 0, wx
.ALL | wx
.ALIGN_CENTRE_VERTICAL
, 5)
1156 imageSizer
.Add(lab1
, 0, wx
.ALL | wx
.ALIGN_CENTRE_VERTICAL
, 5)
1158 lab2
= wx
.StaticText(panel
, -1, "A simple object-oriented drawing " + \
1160 lab2
.SetFont(boldFont
)
1161 lab2
.SetSize(lab2
.GetBestSize())
1163 lab3
= wx
.StaticText(panel
, -1, "pySketch is completely free " + \
1165 lab3
.SetFont(boldFont
)
1166 lab3
.SetSize(lab3
.GetBestSize())
1168 lab4
= wx
.StaticText(panel
, -1, "feel free to adapt or use this " + \
1169 "in any way you like.")
1170 lab4
.SetFont(boldFont
)
1171 lab4
.SetSize(lab4
.GetBestSize())
1173 lab5
= wx
.StaticText(panel
, -1, "Author: Erik Westra " + \
1174 "(ewestra@wave.co.nz)")
1175 lab5
.SetFont(boldFont
)
1176 lab5
.SetSize(lab5
.GetBestSize())
1178 btnOK
= wx
.Button(panel
, wx
.ID_OK
, "OK")
1180 panelSizer
.Add(imageSizer
, 0, wx
.ALIGN_CENTRE
)
1181 panelSizer
.Add((10, 10)) # Spacer.
1182 panelSizer
.Add(lab2
, 0, wx
.ALIGN_CENTRE
)
1183 panelSizer
.Add((10, 10)) # Spacer.
1184 panelSizer
.Add(lab3
, 0, wx
.ALIGN_CENTRE
)
1185 panelSizer
.Add(lab4
, 0, wx
.ALIGN_CENTRE
)
1186 panelSizer
.Add((10, 10)) # Spacer.
1187 panelSizer
.Add(lab5
, 0, wx
.ALIGN_CENTRE
)
1188 panelSizer
.Add((10, 10)) # Spacer.
1189 panelSizer
.Add(btnOK
, 0, wx
.ALL | wx
.ALIGN_CENTRE
, 5)
1191 panel
.SetAutoLayout(True)
1192 panel
.SetSizer(panelSizer
)
1193 panelSizer
.Fit(panel
)
1195 topSizer
= wx
.BoxSizer(wx
.HORIZONTAL
)
1196 topSizer
.Add(panel
, 0, wx
.ALL
, 10)
1198 dialog
.SetAutoLayout(True)
1199 dialog
.SetSizer(topSizer
)
1200 topSizer
.Fit(dialog
)
1204 btn
= dialog
.ShowModal()
1207 # =============================
1208 # == Object Creation Methods ==
1209 # =============================
1211 def createLine(self
, x1
, y1
, x2
, y2
):
1212 """ Create a new line object at the given position and size.
1214 self
._saveUndoInfo
()
1216 topLeftX
= min(x1
, x2
)
1217 topLeftY
= min(y1
, y2
)
1218 botRightX
= max(x1
, x2
)
1219 botRightY
= max(y1
, y2
)
1221 obj
= DrawingObject(obj_LINE
, position
=wx
.Point(topLeftX
, topLeftY
),
1222 size
=wx
.Size(botRightX
-topLeftX
,
1223 botRightY
-topLeftY
),
1224 penColour
=self
.penColour
,
1225 fillColour
=self
.fillColour
,
1226 lineSize
=self
.lineSize
,
1227 startPt
= wx
.Point(x1
- topLeftX
, y1
- topLeftY
),
1228 endPt
= wx
.Point(x2
- topLeftX
, y2
- topLeftY
))
1229 self
.contents
.insert(0, obj
)
1231 self
.doChooseSelectTool()
1235 def createRect(self
, x
, y
, width
, height
):
1236 """ Create a new rectangle object at the given position and size.
1238 self
._saveUndoInfo
()
1240 obj
= DrawingObject(obj_RECT
, position
=wx
.Point(x
, y
),
1241 size
=wx
.Size(width
, height
),
1242 penColour
=self
.penColour
,
1243 fillColour
=self
.fillColour
,
1244 lineSize
=self
.lineSize
)
1245 self
.contents
.insert(0, obj
)
1247 self
.doChooseSelectTool()
1251 def createEllipse(self
, x
, y
, width
, height
):
1252 """ Create a new ellipse object at the given position and size.
1254 self
._saveUndoInfo
()
1256 obj
= DrawingObject(obj_ELLIPSE
, position
=wx
.Point(x
, y
),
1257 size
=wx
.Size(width
, height
),
1258 penColour
=self
.penColour
,
1259 fillColour
=self
.fillColour
,
1260 lineSize
=self
.lineSize
)
1261 self
.contents
.insert(0, obj
)
1263 self
.doChooseSelectTool()
1267 def createText(self
, x
, y
, width
, height
):
1268 """ Create a new text object at the given position and size.
1270 editor
= EditTextObjectDialog(self
, "Create Text Object")
1271 if editor
.ShowModal() == wx
.ID_CANCEL
:
1275 self
._saveUndoInfo
()
1277 obj
= DrawingObject(obj_TEXT
, position
=wx
.Point(x
, y
),
1278 size
=wx
.Size(width
, height
))
1279 editor
.dialogToObject(obj
)
1282 self
.contents
.insert(0, obj
)
1284 self
.doChooseSelectTool()
1287 # =======================
1288 # == Selection Methods ==
1289 # =======================
1291 def selectAll(self
):
1292 """ Select every DrawingObject in our document.
1295 for obj
in self
.contents
:
1296 self
.selection
.append(obj
)
1297 self
.drawPanel
.Refresh()
1301 def deselectAll(self
):
1302 """ Deselect every DrawingObject in our document.
1305 self
.drawPanel
.Refresh()
1309 def select(self
, obj
):
1310 """ Select the given DrawingObject within our document.
1312 self
.selection
= [obj
]
1313 self
.drawPanel
.Refresh()
1317 def selectMany(self
, objs
):
1318 """ Select the given list of DrawingObjects.
1320 self
.selection
= objs
1321 self
.drawPanel
.Refresh()
1325 def selectByRectangle(self
, x
, y
, width
, height
):
1326 """ Select every DrawingObject in the given rectangular region.
1329 for obj
in self
.contents
:
1330 if obj
.objectWithinRect(x
, y
, width
, height
):
1331 self
.selection
.append(obj
)
1332 self
.drawPanel
.Refresh()
1335 # ======================
1336 # == File I/O Methods ==
1337 # ======================
1339 def loadContents(self
):
1340 """ Load the contents of our document into memory.
1342 f
= open(self
.fileName
, "rb")
1343 objData
= cPickle
.load(f
)
1346 for type, data
in objData
:
1347 obj
= DrawingObject(type)
1349 self
.contents
.append(obj
)
1353 self
.undoInfo
= None
1355 self
.drawPanel
.Refresh()
1359 def saveContents(self
):
1360 """ Save the contents of our document to disk.
1363 for obj
in self
.contents
:
1364 objData
.append([obj
.getType(), obj
.getData()])
1366 f
= open(self
.fileName
, "wb")
1367 cPickle
.dump(objData
, f
)
1373 def askIfUserWantsToSave(self
, action
):
1374 """ Give the user the opportunity to save the current document.
1376 'action' is a string describing the action about to be taken. If
1377 the user wants to save the document, it is saved immediately. If
1378 the user cancels, we return False.
1380 if not self
.dirty
: return True # Nothing to do.
1382 response
= wx
.MessageBox("Save changes before " + action
+ "?",
1383 "Confirm", wx
.YES_NO | wx
.CANCEL
, self
)
1385 if response
== wx
.YES
:
1386 if self
.fileName
== None:
1387 fileName
= wx
.FileSelector("Save File As", "Saving",
1388 default_extension
="psk",
1390 flags
= wx
.SAVE | wx
.OVERWRITE_PROMPT
)
1391 if fileName
== "": return False # User cancelled.
1392 self
.fileName
= fileName
1396 elif response
== wx
.NO
:
1397 return True # User doesn't want changes saved.
1398 elif response
== wx
.CANCEL
:
1399 return False # User cancelled.
1401 # =====================
1402 # == Private Methods ==
1403 # =====================
1405 def _adjustMenus(self
):
1406 """ Adjust our menus and toolbar to reflect the current state of the
1409 canSave
= (self
.fileName
!= None) and self
.dirty
1410 canRevert
= (self
.fileName
!= None) and self
.dirty
1411 canUndo
= self
.undoInfo
!= None
1412 selection
= len(self
.selection
) > 0
1413 onlyOne
= len(self
.selection
) == 1
1414 isText
= onlyOne
and (self
.selection
[0].getType() == obj_TEXT
)
1415 front
= onlyOne
and (self
.selection
[0] == self
.contents
[0])
1416 back
= onlyOne
and (self
.selection
[0] == self
.contents
[-1])
1418 # Enable/disable our menu items.
1420 self
.fileMenu
.Enable(wx
.ID_SAVE
, canSave
)
1421 self
.fileMenu
.Enable(wx
.ID_REVERT
, canRevert
)
1423 self
.editMenu
.Enable(menu_UNDO
, canUndo
)
1424 self
.editMenu
.Enable(menu_DUPLICATE
, selection
)
1425 self
.editMenu
.Enable(menu_EDIT_TEXT
, isText
)
1426 self
.editMenu
.Enable(menu_DELETE
, selection
)
1428 self
.toolsMenu
.Check(menu_SELECT
, self
.curTool
== self
.selectIcon
)
1429 self
.toolsMenu
.Check(menu_LINE
, self
.curTool
== self
.lineIcon
)
1430 self
.toolsMenu
.Check(menu_RECT
, self
.curTool
== self
.rectIcon
)
1431 self
.toolsMenu
.Check(menu_ELLIPSE
, self
.curTool
== self
.ellipseIcon
)
1432 self
.toolsMenu
.Check(menu_TEXT
, self
.curTool
== self
.textIcon
)
1434 self
.objectMenu
.Enable(menu_MOVE_FORWARD
, onlyOne
and not front
)
1435 self
.objectMenu
.Enable(menu_MOVE_TO_FRONT
, onlyOne
and not front
)
1436 self
.objectMenu
.Enable(menu_MOVE_BACKWARD
, onlyOne
and not back
)
1437 self
.objectMenu
.Enable(menu_MOVE_TO_BACK
, onlyOne
and not back
)
1439 # Enable/disable our toolbar icons.
1441 self
.toolbar
.EnableTool(wx
.ID_NEW
, True)
1442 self
.toolbar
.EnableTool(wx
.ID_OPEN
, True)
1443 self
.toolbar
.EnableTool(wx
.ID_SAVE
, canSave
)
1444 self
.toolbar
.EnableTool(menu_UNDO
, canUndo
)
1445 self
.toolbar
.EnableTool(menu_DUPLICATE
, selection
)
1446 self
.toolbar
.EnableTool(menu_MOVE_FORWARD
, onlyOne
and not front
)
1447 self
.toolbar
.EnableTool(menu_MOVE_BACKWARD
, onlyOne
and not back
)
1450 def _setCurrentTool(self
, newToolIcon
):
1451 """ Set the currently selected tool.
1453 if self
.curTool
== newToolIcon
: return # Nothing to do.
1455 if self
.curTool
!= None:
1456 self
.curTool
.deselect()
1458 newToolIcon
.select()
1459 self
.curTool
= newToolIcon
1462 def _setPenColour(self
, colour
):
1463 """ Set the default or selected object's pen colour.
1465 if len(self
.selection
) > 0:
1466 self
._saveUndoInfo
()
1467 for obj
in self
.selection
:
1468 obj
.setPenColour(colour
)
1469 self
.drawPanel
.Refresh()
1471 self
.penColour
= colour
1472 self
.optionIndicator
.setPenColour(colour
)
1475 def _setFillColour(self
, colour
):
1476 """ Set the default or selected object's fill colour.
1478 if len(self
.selection
) > 0:
1479 self
._saveUndoInfo
()
1480 for obj
in self
.selection
:
1481 obj
.setFillColour(colour
)
1482 self
.drawPanel
.Refresh()
1484 self
.fillColour
= colour
1485 self
.optionIndicator
.setFillColour(colour
)
1488 def _setLineSize(self
, size
):
1489 """ Set the default or selected object's line size.
1491 if len(self
.selection
) > 0:
1492 self
._saveUndoInfo
()
1493 for obj
in self
.selection
:
1494 obj
.setLineSize(size
)
1495 self
.drawPanel
.Refresh()
1497 self
.lineSize
= size
1498 self
.optionIndicator
.setLineSize(size
)
1501 def _saveUndoInfo(self
):
1502 """ Remember the current state of the document, to allow for undo.
1504 We make a copy of the document's contents, so that we can return to
1505 the previous contents if the user does something and then wants to
1509 for obj
in self
.contents
:
1510 savedContents
.append([obj
.getType(), obj
.getData()])
1513 for i
in range(len(self
.contents
)):
1514 if self
.contents
[i
] in self
.selection
:
1515 savedSelection
.append(i
)
1517 self
.undoInfo
= {"contents" : savedContents
,
1518 "selection" : savedSelection
}
1521 def _resizeObject(self
, obj
, anchorPt
, oldPt
, newPt
):
1522 """ Resize the given object.
1524 'anchorPt' is the unchanging corner of the object, while the
1525 opposite corner has been resized. 'oldPt' are the current
1526 coordinates for this corner, while 'newPt' are the new coordinates.
1527 The object should fit within the given dimensions, though if the
1528 new point is less than the anchor point the object will need to be
1529 moved as well as resized, to avoid giving it a negative size.
1531 if obj
.getType() == obj_TEXT
:
1532 # Not allowed to resize text objects -- they're sized to fit text.
1533 wx
.Bell(); print "4"
1536 self
._saveUndoInfo
()
1538 topLeft
= wx
.Point(min(anchorPt
.x
, newPt
.x
),
1539 min(anchorPt
.y
, newPt
.y
))
1540 botRight
= wx
.Point(max(anchorPt
.x
, newPt
.x
),
1541 max(anchorPt
.y
, newPt
.y
))
1543 newWidth
= botRight
.x
- topLeft
.x
1544 newHeight
= botRight
.y
- topLeft
.y
1546 if obj
.getType() == obj_LINE
:
1547 # Adjust the line so that its start and end points match the new
1548 # overall object size.
1550 startPt
= obj
.getStartPt()
1551 endPt
= obj
.getEndPt()
1553 slopesDown
= ((startPt
.x
< endPt
.x
) and (startPt
.y
< endPt
.y
)) or \
1554 ((startPt
.x
> endPt
.x
) and (startPt
.y
> endPt
.y
))
1556 # Handle the user flipping the line.
1558 hFlip
= ((anchorPt
.x
< oldPt
.x
) and (anchorPt
.x
> newPt
.x
)) or \
1559 ((anchorPt
.x
> oldPt
.x
) and (anchorPt
.x
< newPt
.x
))
1560 vFlip
= ((anchorPt
.y
< oldPt
.y
) and (anchorPt
.y
> newPt
.y
)) or \
1561 ((anchorPt
.y
> oldPt
.y
) and (anchorPt
.y
< newPt
.y
))
1563 if (hFlip
and not vFlip
) or (vFlip
and not hFlip
):
1564 slopesDown
= not slopesDown
# Line flipped.
1567 obj
.setStartPt(wx
.Point(0, 0))
1568 obj
.setEndPt(wx
.Point(newWidth
, newHeight
))
1570 obj
.setStartPt(wx
.Point(0, newHeight
))
1571 obj
.setEndPt(wx
.Point(newWidth
, 0))
1573 # Finally, adjust the bounds of the object to match the new dimensions.
1575 obj
.setPosition(topLeft
)
1576 obj
.setSize(wx
.Size(botRight
.x
- topLeft
.x
, botRight
.y
- topLeft
.y
))
1578 self
.drawPanel
.Refresh()
1581 def _moveObject(self
, offsetX
, offsetY
):
1582 """ Move the currently selected object(s) by the given offset.
1584 self
._saveUndoInfo
()
1586 for obj
in self
.selection
:
1587 pos
= obj
.getPosition()
1588 pos
.x
= pos
.x
+ offsetX
1589 pos
.y
= pos
.y
+ offsetY
1590 obj
.setPosition(pos
)
1592 self
.drawPanel
.Refresh()
1595 def _buildLineSizePopup(self
, lineSize
):
1596 """ Build the pop-up menu used to set the line size.
1598 'lineSize' is the current line size value. The corresponding item
1599 is checked in the pop-up menu.
1602 menu
.Append(id_LINESIZE_0
, "no line", kind
=wx
.ITEM_CHECK
)
1603 menu
.Append(id_LINESIZE_1
, "1-pixel line", kind
=wx
.ITEM_CHECK
)
1604 menu
.Append(id_LINESIZE_2
, "2-pixel line", kind
=wx
.ITEM_CHECK
)
1605 menu
.Append(id_LINESIZE_3
, "3-pixel line", kind
=wx
.ITEM_CHECK
)
1606 menu
.Append(id_LINESIZE_4
, "4-pixel line", kind
=wx
.ITEM_CHECK
)
1607 menu
.Append(id_LINESIZE_5
, "5-pixel line", kind
=wx
.ITEM_CHECK
)
1609 if lineSize
== 0: menu
.Check(id_LINESIZE_0
, True)
1610 elif lineSize
== 1: menu
.Check(id_LINESIZE_1
, True)
1611 elif lineSize
== 2: menu
.Check(id_LINESIZE_2
, True)
1612 elif lineSize
== 3: menu
.Check(id_LINESIZE_3
, True)
1613 elif lineSize
== 4: menu
.Check(id_LINESIZE_4
, True)
1614 elif lineSize
== 5: menu
.Check(id_LINESIZE_5
, True)
1616 self
.Bind(wx
.EVT_MENU
, self
._lineSizePopupSelected
, id=id_LINESIZE_0
, id2
=id_LINESIZE_5
)
1621 def _lineSizePopupSelected(self
, event
):
1622 """ Respond to the user selecting an item from the line size popup menu
1625 if id == id_LINESIZE_0
: self
._setLineSize
(0)
1626 elif id == id_LINESIZE_1
: self
._setLineSize
(1)
1627 elif id == id_LINESIZE_2
: self
._setLineSize
(2)
1628 elif id == id_LINESIZE_3
: self
._setLineSize
(3)
1629 elif id == id_LINESIZE_4
: self
._setLineSize
(4)
1630 elif id == id_LINESIZE_5
: self
._setLineSize
(5)
1632 wx
.Bell(); print "5"
1635 self
.optionIndicator
.setLineSize(self
.lineSize
)
1638 def _getEventCoordinates(self
, event
):
1639 """ Return the coordinates associated with the given mouse event.
1641 The coordinates have to be adjusted to allow for the current scroll
1644 originX
, originY
= self
.drawPanel
.GetViewStart()
1645 unitX
, unitY
= self
.drawPanel
.GetScrollPixelsPerUnit()
1646 return wx
.Point(event
.GetX() + (originX
* unitX
),
1647 event
.GetY() + (originY
* unitY
))
1650 def _getObjectAndSelectionHandleAt(self
, pt
):
1651 """ Return the object and selection handle at the given point.
1653 We draw selection handles (small rectangles) around the currently
1654 selected object(s). If the given point is within one of the
1655 selection handle rectangles, we return the associated object and a
1656 code indicating which selection handle the point is in. If the
1657 point isn't within any selection handle at all, we return the tuple
1658 (None, handle_NONE).
1660 for obj
in self
.selection
:
1661 handle
= obj
.getSelectionHandleContainingPoint(pt
.x
, pt
.y
)
1662 if handle
!= handle_NONE
:
1665 return None, handle_NONE
1668 def _getObjectAt(self
, pt
):
1669 """ Return the first object found which is at the given point.
1671 for obj
in self
.contents
:
1672 if obj
.objectContainsPoint(pt
.x
, pt
.y
):
1677 def _drawObjectOutline(self
, offsetX
, offsetY
):
1678 """ Draw an outline of the currently selected object.
1680 The selected object's outline is drawn at the object's position
1681 plus the given offset.
1683 Note that the outline is drawn by *inverting* the window's
1684 contents, so calling _drawObjectOutline twice in succession will
1685 restore the window's contents back to what they were previously.
1687 if len(self
.selection
) != 1: return
1689 position
= self
.selection
[0].getPosition()
1690 size
= self
.selection
[0].getSize()
1692 dc
= wx
.ClientDC(self
.drawPanel
)
1693 self
.drawPanel
.PrepareDC(dc
)
1695 dc
.SetPen(wx
.BLACK_DASHED_PEN
)
1696 dc
.SetBrush(wx
.TRANSPARENT_BRUSH
)
1697 dc
.SetLogicalFunction(wx
.INVERT
)
1699 dc
.DrawRectangle(position
.x
+ offsetX
, position
.y
+ offsetY
,
1700 size
.width
, size
.height
)
1705 def _drawVisualFeedback(self
, startPt
, endPt
, type, dashedLine
):
1706 """ Draw visual feedback for a drawing operation.
1708 The visual feedback consists of a line, ellipse, or rectangle based
1709 around the two given points. 'type' should be one of the following
1710 predefined feedback type constants:
1712 feedback_RECT -> draw rectangular feedback.
1713 feedback_LINE -> draw line feedback.
1714 feedback_ELLIPSE -> draw elliptical feedback.
1716 if 'dashedLine' is True, the feedback is drawn as a dashed rather
1719 Note that the feedback is drawn by *inverting* the window's
1720 contents, so calling _drawVisualFeedback twice in succession will
1721 restore the window's contents back to what they were previously.
1723 dc
= wx
.ClientDC(self
.drawPanel
)
1724 self
.drawPanel
.PrepareDC(dc
)
1727 dc
.SetPen(wx
.BLACK_DASHED_PEN
)
1729 dc
.SetPen(wx
.BLACK_PEN
)
1730 dc
.SetBrush(wx
.TRANSPARENT_BRUSH
)
1731 dc
.SetLogicalFunction(wx
.INVERT
)
1733 if type == feedback_RECT
:
1734 dc
.DrawRectangle(startPt
.x
, startPt
.y
,
1735 endPt
.x
- startPt
.x
,
1736 endPt
.y
- startPt
.y
)
1737 elif type == feedback_LINE
:
1738 dc
.DrawLine(startPt
.x
, startPt
.y
, endPt
.x
, endPt
.y
)
1739 elif type == feedback_ELLIPSE
:
1740 dc
.DrawEllipse(startPt
.x
, startPt
.y
,
1741 endPt
.x
- startPt
.x
,
1742 endPt
.y
- startPt
.y
)
1746 #----------------------------------------------------------------------------
1748 class DrawingObject
:
1749 """ An object within the drawing panel.
1751 A pySketch document consists of a front-to-back ordered list of
1752 DrawingObjects. Each DrawingObject has the following properties:
1754 'type' What type of object this is (text, line, etc).
1755 'position' The position of the object within the document.
1756 'size' The size of the object within the document.
1757 'penColour' The colour to use for drawing the object's outline.
1758 'fillColour' Colour to use for drawing object's interior.
1759 'lineSize' Line width (in pixels) to use for object's outline.
1760 'startPt' The point, relative to the object's position, where
1761 an obj_LINE object's line should start.
1762 'endPt' The point, relative to the object's position, where
1763 an obj_LINE object's line should end.
1764 'text' The object's text (obj_TEXT objects only).
1765 'textFont' The text object's font name.
1766 'textSize' The text object's point size.
1767 'textBoldface' If True, this text object will be drawn in
1769 'textItalic' If True, this text object will be drawn in italic.
1770 'textUnderline' If True, this text object will be drawn underlined.
1773 # ==================
1774 # == Constructors ==
1775 # ==================
1777 def __init__(self
, type, position
=wx
.Point(0, 0), size
=wx
.Size(0, 0),
1778 penColour
=wx
.BLACK
, fillColour
=wx
.WHITE
, lineSize
=1,
1779 text
=None, startPt
=wx
.Point(0, 0), endPt
=wx
.Point(0,0)):
1780 """ Standard constructor.
1782 'type' is the type of object being created. This should be one of
1783 the following constants:
1790 The remaining parameters let you set various options for the newly
1791 created DrawingObject.
1794 self
.position
= position
1796 self
.penColour
= penColour
1797 self
.fillColour
= fillColour
1798 self
.lineSize
= lineSize
1799 self
.startPt
= startPt
1802 self
.textFont
= wx
.SystemSettings_GetFont(
1803 wx
.SYS_DEFAULT_GUI_FONT
).GetFaceName()
1805 self
.textBoldface
= False
1806 self
.textItalic
= False
1807 self
.textUnderline
= False
1809 # =============================
1810 # == Object Property Methods ==
1811 # =============================
1814 """ Return a copy of the object's internal data.
1816 This is used to save this DrawingObject to disk.
1818 return [self
.type, self
.position
.x
, self
.position
.y
,
1819 self
.size
.width
, self
.size
.height
,
1820 self
.penColour
.Red(),
1821 self
.penColour
.Green(),
1822 self
.penColour
.Blue(),
1823 self
.fillColour
.Red(),
1824 self
.fillColour
.Green(),
1825 self
.fillColour
.Blue(),
1827 self
.startPt
.x
, self
.startPt
.y
,
1828 self
.endPt
.x
, self
.endPt
.y
,
1837 def setData(self
, data
):
1838 """ Set the object's internal data.
1840 'data' is a copy of the object's saved data, as returned by
1841 getData() above. This is used to restore a previously saved
1844 #data = copy.deepcopy(data) # Needed?
1847 self
.position
= wx
.Point(data
[1], data
[2])
1848 self
.size
= wx
.Size(data
[3], data
[4])
1849 self
.penColour
= wx
.Colour(red
=data
[5],
1852 self
.fillColour
= wx
.Colour(red
=data
[8],
1855 self
.lineSize
= data
[11]
1856 self
.startPt
= wx
.Point(data
[12], data
[13])
1857 self
.endPt
= wx
.Point(data
[14], data
[15])
1858 self
.text
= data
[16]
1859 self
.textFont
= data
[17]
1860 self
.textSize
= data
[18]
1861 self
.textBoldface
= data
[19]
1862 self
.textItalic
= data
[20]
1863 self
.textUnderline
= data
[21]
1867 """ Return this DrawingObject's type.
1872 def setPosition(self
, position
):
1873 """ Set the origin (top-left corner) for this DrawingObject.
1875 self
.position
= position
1878 def getPosition(self
):
1879 """ Return this DrawingObject's position.
1881 return self
.position
1884 def setSize(self
, size
):
1885 """ Set the size for this DrawingObject.
1891 """ Return this DrawingObject's size.
1896 def setPenColour(self
, colour
):
1897 """ Set the pen colour used for this DrawingObject.
1899 self
.penColour
= colour
1902 def getPenColour(self
):
1903 """ Return this DrawingObject's pen colour.
1905 return self
.penColour
1908 def setFillColour(self
, colour
):
1909 """ Set the fill colour used for this DrawingObject.
1911 self
.fillColour
= colour
1914 def getFillColour(self
):
1915 """ Return this DrawingObject's fill colour.
1917 return self
.fillColour
1920 def setLineSize(self
, lineSize
):
1921 """ Set the linesize used for this DrawingObject.
1923 self
.lineSize
= lineSize
1926 def getLineSize(self
):
1927 """ Return this DrawingObject's line size.
1929 return self
.lineSize
1932 def setStartPt(self
, startPt
):
1933 """ Set the starting point for this line DrawingObject.
1935 self
.startPt
= startPt
1938 def getStartPt(self
):
1939 """ Return the starting point for this line DrawingObject.
1944 def setEndPt(self
, endPt
):
1945 """ Set the ending point for this line DrawingObject.
1951 """ Return the ending point for this line DrawingObject.
1956 def setText(self
, text
):
1957 """ Set the text for this DrawingObject.
1963 """ Return this DrawingObject's text.
1968 def setTextFont(self
, font
):
1969 """ Set the typeface for this text DrawingObject.
1971 self
.textFont
= font
1974 def getTextFont(self
):
1975 """ Return this text DrawingObject's typeface.
1977 return self
.textFont
1980 def setTextSize(self
, size
):
1981 """ Set the point size for this text DrawingObject.
1983 self
.textSize
= size
1986 def getTextSize(self
):
1987 """ Return this text DrawingObject's text size.
1989 return self
.textSize
1992 def setTextBoldface(self
, boldface
):
1993 """ Set the boldface flag for this text DrawingObject.
1995 self
.textBoldface
= boldface
1998 def getTextBoldface(self
):
1999 """ Return this text DrawingObject's boldface flag.
2001 return self
.textBoldface
2004 def setTextItalic(self
, italic
):
2005 """ Set the italic flag for this text DrawingObject.
2007 self
.textItalic
= italic
2010 def getTextItalic(self
):
2011 """ Return this text DrawingObject's italic flag.
2013 return self
.textItalic
2016 def setTextUnderline(self
, underline
):
2017 """ Set the underling flag for this text DrawingObject.
2019 self
.textUnderline
= underline
2022 def getTextUnderline(self
):
2023 """ Return this text DrawingObject's underline flag.
2025 return self
.textUnderline
2027 # ============================
2028 # == Object Drawing Methods ==
2029 # ============================
2031 def draw(self
, dc
, selected
):
2032 """ Draw this DrawingObject into our window.
2034 'dc' is the device context to use for drawing. If 'selected' is
2035 True, the object is currently selected and should be drawn as such.
2037 if self
.type != obj_TEXT
:
2038 if self
.lineSize
== 0:
2039 dc
.SetPen(wx
.Pen(self
.penColour
, self
.lineSize
, wx
.TRANSPARENT
))
2041 dc
.SetPen(wx
.Pen(self
.penColour
, self
.lineSize
, wx
.SOLID
))
2042 dc
.SetBrush(wx
.Brush(self
.fillColour
, wx
.SOLID
))
2044 dc
.SetTextForeground(self
.penColour
)
2045 dc
.SetTextBackground(self
.fillColour
)
2047 self
._privateDraw
(dc
, self
.position
, selected
)
2049 # =======================
2050 # == Selection Methods ==
2051 # =======================
2053 def objectContainsPoint(self
, x
, y
):
2054 """ Returns True iff this object contains the given point.
2056 This is used to determine if the user clicked on the object.
2058 # Firstly, ignore any points outside of the object's bounds.
2060 if x
< self
.position
.x
: return False
2061 if x
> self
.position
.x
+ self
.size
.x
: return False
2062 if y
< self
.position
.y
: return False
2063 if y
> self
.position
.y
+ self
.size
.y
: return False
2065 if self
.type in [obj_RECT
, obj_TEXT
]:
2066 # Rectangles and text are easy -- they're always selected if the
2067 # point is within their bounds.
2070 # Now things get tricky. There's no straightforward way of knowing
2071 # whether the point is within the object's bounds...to get around this,
2072 # we draw the object into a memory-based bitmap and see if the given
2073 # point was drawn. This could no doubt be done more efficiently by
2074 # some tricky maths, but this approach works and is simple enough.
2076 bitmap
= wx
.EmptyBitmap(self
.size
.x
+ 10, self
.size
.y
+ 10)
2078 dc
.SelectObject(bitmap
)
2080 dc
.SetBackground(wx
.WHITE_BRUSH
)
2082 dc
.SetPen(wx
.Pen(wx
.BLACK
, self
.lineSize
+ 5, wx
.SOLID
))
2083 dc
.SetBrush(wx
.BLACK_BRUSH
)
2084 self
._privateDraw
(dc
, wx
.Point(5, 5), True)
2086 pixel
= dc
.GetPixel(x
- self
.position
.x
+ 5, y
- self
.position
.y
+ 5)
2087 if (pixel
.Red() == 0) and (pixel
.Green() == 0) and (pixel
.Blue() == 0):
2093 def getSelectionHandleContainingPoint(self
, x
, y
):
2094 """ Return the selection handle containing the given point, if any.
2096 We return one of the predefined selection handle ID codes.
2098 if self
.type == obj_LINE
:
2099 # We have selection handles at the start and end points.
2100 if self
._pointInSelRect
(x
, y
, self
.position
.x
+ self
.startPt
.x
,
2101 self
.position
.y
+ self
.startPt
.y
):
2102 return handle_START_POINT
2103 elif self
._pointInSelRect
(x
, y
, self
.position
.x
+ self
.endPt
.x
,
2104 self
.position
.y
+ self
.endPt
.y
):
2105 return handle_END_POINT
2109 # We have selection handles at all four corners.
2110 if self
._pointInSelRect
(x
, y
, self
.position
.x
, self
.position
.y
):
2111 return handle_TOP_LEFT
2112 elif self
._pointInSelRect
(x
, y
, self
.position
.x
+ self
.size
.width
,
2114 return handle_TOP_RIGHT
2115 elif self
._pointInSelRect
(x
, y
, self
.position
.x
,
2116 self
.position
.y
+ self
.size
.height
):
2117 return handle_BOTTOM_LEFT
2118 elif self
._pointInSelRect
(x
, y
, self
.position
.x
+ self
.size
.width
,
2119 self
.position
.y
+ self
.size
.height
):
2120 return handle_BOTTOM_RIGHT
2125 def objectWithinRect(self
, x
, y
, width
, height
):
2126 """ Return True iff this object falls completely within the given rect.
2128 if x
> self
.position
.x
: return False
2129 if x
+ width
< self
.position
.x
+ self
.size
.width
: return False
2130 if y
> self
.position
.y
: return False
2131 if y
+ height
< self
.position
.y
+ self
.size
.height
: return False
2134 # =====================
2135 # == Utility Methods ==
2136 # =====================
2138 def fitToText(self
):
2139 """ Resize a text DrawingObject so that it fits it's text exactly.
2141 if self
.type != obj_TEXT
: return
2143 if self
.textBoldface
: weight
= wx
.BOLD
2144 else: weight
= wx
.NORMAL
2145 if self
.textItalic
: style
= wx
.ITALIC
2146 else: style
= wx
.NORMAL
2147 font
= wx
.Font(self
.textSize
, wx
.DEFAULT
, style
, weight
,
2148 self
.textUnderline
, self
.textFont
)
2150 dummyWindow
= wx
.Frame(None, -1, "")
2151 dummyWindow
.SetFont(font
)
2152 width
, height
= dummyWindow
.GetTextExtent(self
.text
)
2153 dummyWindow
.Destroy()
2155 self
.size
= wx
.Size(width
, height
)
2157 # =====================
2158 # == Private Methods ==
2159 # =====================
2161 def _privateDraw(self
, dc
, position
, selected
):
2162 """ Private routine to draw this DrawingObject.
2164 'dc' is the device context to use for drawing, while 'position' is
2165 the position in which to draw the object. If 'selected' is True,
2166 the object is drawn with selection handles. This private drawing
2167 routine assumes that the pen and brush have already been set by the
2170 if self
.type == obj_LINE
:
2171 dc
.DrawLine(position
.x
+ self
.startPt
.x
,
2172 position
.y
+ self
.startPt
.y
,
2173 position
.x
+ self
.endPt
.x
,
2174 position
.y
+ self
.endPt
.y
)
2175 elif self
.type == obj_RECT
:
2176 dc
.DrawRectangle(position
.x
, position
.y
,
2177 self
.size
.width
, self
.size
.height
)
2178 elif self
.type == obj_ELLIPSE
:
2179 dc
.DrawEllipse(position
.x
, position
.y
,
2180 self
.size
.width
, self
.size
.height
)
2181 elif self
.type == obj_TEXT
:
2182 if self
.textBoldface
: weight
= wx
.BOLD
2183 else: weight
= wx
.NORMAL
2184 if self
.textItalic
: style
= wx
.ITALIC
2185 else: style
= wx
.NORMAL
2186 font
= wx
.Font(self
.textSize
, wx
.DEFAULT
, style
, weight
,
2187 self
.textUnderline
, self
.textFont
)
2189 dc
.DrawText(self
.text
, position
.x
, position
.y
)
2192 dc
.SetPen(wx
.TRANSPARENT_PEN
)
2193 dc
.SetBrush(wx
.BLACK_BRUSH
)
2195 if self
.type == obj_LINE
:
2196 # Draw selection handles at the start and end points.
2197 self
._drawSelHandle
(dc
, position
.x
+ self
.startPt
.x
,
2198 position
.y
+ self
.startPt
.y
)
2199 self
._drawSelHandle
(dc
, position
.x
+ self
.endPt
.x
,
2200 position
.y
+ self
.endPt
.y
)
2202 # Draw selection handles at all four corners.
2203 self
._drawSelHandle
(dc
, position
.x
, position
.y
)
2204 self
._drawSelHandle
(dc
, position
.x
+ self
.size
.width
,
2206 self
._drawSelHandle
(dc
, position
.x
,
2207 position
.y
+ self
.size
.height
)
2208 self
._drawSelHandle
(dc
, position
.x
+ self
.size
.width
,
2209 position
.y
+ self
.size
.height
)
2212 def _drawSelHandle(self
, dc
, x
, y
):
2213 """ Draw a selection handle around this DrawingObject.
2215 'dc' is the device context to draw the selection handle within,
2216 while 'x' and 'y' are the coordinates to use for the centre of the
2219 dc
.DrawRectangle(x
- 3, y
- 3, 6, 6)
2222 def _pointInSelRect(self
, x
, y
, rX
, rY
):
2223 """ Return True iff (x, y) is within the selection handle at (rX, ry).
2225 if x
< rX
- 3: return False
2226 elif x
> rX
+ 3: return False
2227 elif y
< rY
- 3: return False
2228 elif y
> rY
+ 3: return False
2231 #----------------------------------------------------------------------------
2233 class ToolPaletteIcon(GenBitmapButton
):
2234 """ An icon appearing in the tool palette area of our sketching window.
2236 Note that this is actually implemented as a wx.Bitmap rather
2237 than as a wx.Icon. wx.Icon has a very specific meaning, and isn't
2238 appropriate for this more general use.
2241 def __init__(self
, parent
, iconID
, iconName
, toolTip
):
2242 """ Standard constructor.
2244 'parent' is the parent window this icon will be part of.
2245 'iconID' is the internal ID used for this icon.
2246 'iconName' is the name used for this icon.
2247 'toolTip' is the tool tip text to show for this icon.
2249 The icon name is used to get the appropriate bitmap for this icon.
2251 bmp
= wx
.Bitmap("images/" + iconName
+ "Icon.bmp", wx
.BITMAP_TYPE_BMP
)
2252 GenBitmapButton
.__init
__(self
, parent
, iconID
, bmp
, wx
.DefaultPosition
,
2253 wx
.Size(bmp
.GetWidth(), bmp
.GetHeight()))
2254 self
.SetToolTip(wx
.ToolTip(toolTip
))
2256 self
.iconID
= iconID
2257 self
.iconName
= iconName
2258 self
.isSelected
= False
2262 """ Select the icon.
2264 The icon's visual representation is updated appropriately.
2266 if self
.isSelected
: return # Nothing to do!
2268 bmp
= wx
.Bitmap("images/" + self
.iconName
+ "IconSel.bmp",
2270 self
.SetBitmapLabel(bmp
)
2271 self
.isSelected
= True
2275 """ Deselect the icon.
2277 The icon's visual representation is updated appropriately.
2279 if not self
.isSelected
: return # Nothing to do!
2281 bmp
= wx
.Bitmap("images/" + self
.iconName
+ "Icon.bmp",
2283 self
.SetBitmapLabel(bmp
)
2284 self
.isSelected
= False
2286 #----------------------------------------------------------------------------
2288 class ToolOptionIndicator(wx
.Window
):
2289 """ A visual indicator which shows the current tool options.
2291 def __init__(self
, parent
):
2292 """ Standard constructor.
2294 wx
.Window
.__init
__(self
, parent
, -1, wx
.DefaultPosition
, wx
.Size(52, 32))
2296 self
.penColour
= wx
.BLACK
2297 self
.fillColour
= wx
.WHITE
2300 self
.Bind(wx
.EVT_PAINT
, self
.OnPaint
)
2303 def setPenColour(self
, penColour
):
2304 """ Set the indicator's current pen colour.
2306 self
.penColour
= penColour
2310 def setFillColour(self
, fillColour
):
2311 """ Set the indicator's current fill colour.
2313 self
.fillColour
= fillColour
2317 def setLineSize(self
, lineSize
):
2318 """ Set the indicator's current pen colour.
2320 self
.lineSize
= lineSize
2324 def OnPaint(self
, event
):
2325 """ Paint our tool option indicator.
2327 dc
= wx
.PaintDC(self
)
2330 if self
.lineSize
== 0:
2331 dc
.SetPen(wx
.Pen(self
.penColour
, self
.lineSize
, wx
.TRANSPARENT
))
2333 dc
.SetPen(wx
.Pen(self
.penColour
, self
.lineSize
, wx
.SOLID
))
2334 dc
.SetBrush(wx
.Brush(self
.fillColour
, wx
.SOLID
))
2336 dc
.DrawRectangle(5, 5, self
.GetSize().width
- 10,
2337 self
.GetSize().height
- 10)
2341 #----------------------------------------------------------------------------
2343 class EditTextObjectDialog(wx
.Dialog
):
2344 """ Dialog box used to edit the properties of a text object.
2346 The user can edit the object's text, font, size, and text style.
2349 def __init__(self
, parent
, title
):
2350 """ Standard constructor.
2352 wx
.Dialog
.__init
__(self
, parent
, -1, title
)
2354 self
.textCtrl
= wx
.TextCtrl(self
, 1001, "", style
=wx
.TE_PROCESS_ENTER
,
2355 validator
=TextObjectValidator())
2356 extent
= self
.textCtrl
.GetFullTextExtent("Hy")
2357 lineHeight
= extent
[1] + extent
[3]
2358 self
.textCtrl
.SetSize(wx
.Size(-1, lineHeight
* 4))
2360 self
.Bind(wx
.EVT_TEXT_ENTER
, self
._doEnter
, id=1001)
2362 fonts
= wx
.FontEnumerator()
2363 fonts
.EnumerateFacenames()
2364 self
.fontList
= fonts
.GetFacenames()
2365 self
.fontList
.sort()
2367 fontLabel
= wx
.StaticText(self
, -1, "Font:")
2368 self
._setFontOptions
(fontLabel
, weight
=wx
.BOLD
)
2370 self
.fontCombo
= wx
.ComboBox(self
, -1, "", wx
.DefaultPosition
,
2371 wx
.DefaultSize
, self
.fontList
,
2372 style
= wx
.CB_READONLY
)
2373 self
.fontCombo
.SetSelection(0) # Default to first available font.
2375 self
.sizeList
= ["8", "9", "10", "12", "14", "16",
2376 "18", "20", "24", "32", "48", "72"]
2378 sizeLabel
= wx
.StaticText(self
, -1, "Size:")
2379 self
._setFontOptions
(sizeLabel
, weight
=wx
.BOLD
)
2381 self
.sizeCombo
= wx
.ComboBox(self
, -1, "", wx
.DefaultPosition
,
2382 wx
.DefaultSize
, self
.sizeList
,
2383 style
=wx
.CB_READONLY
)
2384 self
.sizeCombo
.SetSelection(3) # Default to 12 point text.
2386 gap
= wx
.LEFT | wx
.TOP | wx
.RIGHT
2388 comboSizer
= wx
.BoxSizer(wx
.HORIZONTAL
)
2389 comboSizer
.Add(fontLabel
, 0, gap | wx
.ALIGN_CENTRE_VERTICAL
, 5)
2390 comboSizer
.Add(self
.fontCombo
, 0, gap
, 5)
2391 comboSizer
.Add((5, 5)) # Spacer.
2392 comboSizer
.Add(sizeLabel
, 0, gap | wx
.ALIGN_CENTRE_VERTICAL
, 5)
2393 comboSizer
.Add(self
.sizeCombo
, 0, gap
, 5)
2395 self
.boldCheckbox
= wx
.CheckBox(self
, -1, "Bold")
2396 self
.italicCheckbox
= wx
.CheckBox(self
, -1, "Italic")
2397 self
.underlineCheckbox
= wx
.CheckBox(self
, -1, "Underline")
2399 self
._setFontOptions
(self
.boldCheckbox
, weight
=wx
.BOLD
)
2400 self
._setFontOptions
(self
.italicCheckbox
, style
=wx
.ITALIC
)
2401 self
._setFontOptions
(self
.underlineCheckbox
, underline
=True)
2403 styleSizer
= wx
.BoxSizer(wx
.HORIZONTAL
)
2404 styleSizer
.Add(self
.boldCheckbox
, 0, gap
, 5)
2405 styleSizer
.Add(self
.italicCheckbox
, 0, gap
, 5)
2406 styleSizer
.Add(self
.underlineCheckbox
, 0, gap
, 5)
2408 self
.okButton
= wx
.Button(self
, wx
.ID_OK
, "OK")
2409 self
.cancelButton
= wx
.Button(self
, wx
.ID_CANCEL
, "Cancel")
2411 btnSizer
= wx
.BoxSizer(wx
.HORIZONTAL
)
2412 btnSizer
.Add(self
.okButton
, 0, gap
, 5)
2413 btnSizer
.Add(self
.cancelButton
, 0, gap
, 5)
2415 sizer
= wx
.BoxSizer(wx
.VERTICAL
)
2416 sizer
.Add(self
.textCtrl
, 1, gap | wx
.EXPAND
, 5)
2417 sizer
.Add((10, 10)) # Spacer.
2418 sizer
.Add(comboSizer
, 0, gap | wx
.ALIGN_CENTRE
, 5)
2419 sizer
.Add(styleSizer
, 0, gap | wx
.ALIGN_CENTRE
, 5)
2420 sizer
.Add((10, 10)) # Spacer.
2421 sizer
.Add(btnSizer
, 0, gap | wx
.ALIGN_CENTRE
, 5)
2423 self
.SetAutoLayout(True)
2424 self
.SetSizer(sizer
)
2427 self
.textCtrl
.SetFocus()
2430 def objectToDialog(self
, obj
):
2431 """ Copy the properties of the given text object into the dialog box.
2433 self
.textCtrl
.SetValue(obj
.getText())
2434 self
.textCtrl
.SetSelection(0, len(obj
.getText()))
2436 for i
in range(len(self
.fontList
)):
2437 if self
.fontList
[i
] == obj
.getTextFont():
2438 self
.fontCombo
.SetSelection(i
)
2441 for i
in range(len(self
.sizeList
)):
2442 if self
.sizeList
[i
] == str(obj
.getTextSize()):
2443 self
.sizeCombo
.SetSelection(i
)
2446 self
.boldCheckbox
.SetValue(obj
.getTextBoldface())
2447 self
.italicCheckbox
.SetValue(obj
.getTextItalic())
2448 self
.underlineCheckbox
.SetValue(obj
.getTextUnderline())
2451 def dialogToObject(self
, obj
):
2452 """ Copy the properties from the dialog box into the given text object.
2454 obj
.setText(self
.textCtrl
.GetValue())
2455 obj
.setTextFont(self
.fontCombo
.GetValue())
2456 obj
.setTextSize(int(self
.sizeCombo
.GetValue()))
2457 obj
.setTextBoldface(self
.boldCheckbox
.GetValue())
2458 obj
.setTextItalic(self
.italicCheckbox
.GetValue())
2459 obj
.setTextUnderline(self
.underlineCheckbox
.GetValue())
2462 # ======================
2463 # == Private Routines ==
2464 # ======================
2466 def _setFontOptions(self
, ctrl
, family
=None, pointSize
=-1,
2467 style
=wx
.NORMAL
, weight
=wx
.NORMAL
,
2469 """ Change the font settings for the given control.
2471 The meaning of the 'family', 'pointSize', 'style', 'weight' and
2472 'underline' parameters are the same as for the wx.Font constructor.
2473 If the family and/or pointSize isn't specified, the current default
2476 if family
== None: family
= ctrl
.GetFont().GetFamily()
2477 if pointSize
== -1: pointSize
= ctrl
.GetFont().GetPointSize()
2479 ctrl
.SetFont(wx
.Font(pointSize
, family
, style
, weight
, underline
))
2480 ctrl
.SetSize(ctrl
.GetBestSize()) # Adjust size to reflect font change.
2483 def _doEnter(self
, event
):
2484 """ Respond to the user hitting the ENTER key.
2486 We simulate clicking on the "OK" button.
2488 if self
.Validate(): self
.Show(False)
2490 #----------------------------------------------------------------------------
2492 class TextObjectValidator(wx
.PyValidator
):
2493 """ This validator is used to ensure that the user has entered something
2494 into the text object editor dialog's text field.
2497 """ Standard constructor.
2499 wx
.PyValidator
.__init
__(self
)
2503 """ Standard cloner.
2505 Note that every validator must implement the Clone() method.
2507 return TextObjectValidator()
2510 def Validate(self
, win
):
2511 """ Validate the contents of the given text control.
2513 textCtrl
= self
.GetWindow()
2514 text
= textCtrl
.GetValue()
2517 wx
.MessageBox("A text object must contain some text!", "Error")
2523 def TransferToWindow(self
):
2524 """ Transfer data from validator to window.
2526 The default implementation returns False, indicating that an error
2527 occurred. We simply return True, as we don't do any data transfer.
2529 return True # Prevent wx.Dialog from complaining.
2532 def TransferFromWindow(self
):
2533 """ Transfer data from window to validator.
2535 The default implementation returns False, indicating that an error
2536 occurred. We simply return True, as we don't do any data transfer.
2538 return True # Prevent wx.Dialog from complaining.
2540 #----------------------------------------------------------------------------
2542 class ExceptionHandler
:
2543 """ A simple error-handling class to write exceptions to a text file.
2545 Under MS Windows, the standard DOS console window doesn't scroll and
2546 closes as soon as the application exits, making it hard to find and
2547 view Python exceptions. This utility class allows you to handle Python
2548 exceptions in a more friendly manner.
2552 """ Standard constructor.
2555 if os
.path
.exists("errors.txt"):
2556 os
.remove("errors.txt") # Delete previous error log, if any.
2560 """ Write the given error message to a text file.
2562 Note that if the error message doesn't end in a carriage return, we
2563 have to buffer up the inputs until a carriage return is received.
2565 if (s
[-1] != "\n") and (s
[-1] != "\r"):
2566 self
._buff
= self
._buff
+ s
2573 if s
[:9] == "Traceback":
2574 # Tell the user than an exception occurred.
2575 wx
.MessageBox("An internal error has occurred.\nPlease " + \
2576 "refer to the 'errors.txt' file for details.",
2577 "Error", wx
.OK | wx
.CENTRE | wx
.ICON_EXCLAMATION
)
2579 f
= open("errors.txt", "a")
2583 pass # Don't recursively crash on errors.
2585 #----------------------------------------------------------------------------
2587 class SketchApp(wx
.App
):
2588 """ The main pySketch application object.
2591 """ Initialise the application.
2596 if len(sys
.argv
) == 1:
2597 # No file name was specified on the command line -> start with a
2599 frame
= DrawingFrame(None, -1, "Untitled")
2602 _docList
.append(frame
)
2604 # Load the file(s) specified on the command line.
2605 for arg
in sys
.argv
[1:]:
2606 fileName
= os
.path
.join(os
.getcwd(), arg
)
2607 if os
.path
.isfile(fileName
):
2608 frame
= DrawingFrame(None, -1,
2609 os
.path
.basename(fileName
),
2612 _docList
.append(frame
)
2616 #----------------------------------------------------------------------------
2619 """ Start up the pySketch application.
2623 # Redirect python exceptions to a log file.
2625 sys
.stderr
= ExceptionHandler()
2627 # Create and start the pySketch application.
2633 if __name__
== "__main__":