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