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
47 from wxPython
.wx
import *
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(wxFrame
):
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 wxFrame
150 constructor. 'fileName' is the name and path of a saved file to
151 load into this frame, if any.
153 wxFrame
.__init
__(self
, parent
, id, title
,
154 style
= wxDEFAULT_FRAME_STYLE | wxWANTS_CHARS |
155 wxNO_FULL_REPAINT_ON_RESIZE
)
157 # Setup our menu bar.
159 menuBar
= wxMenuBar()
161 self
.fileMenu
= wxMenu()
162 self
.fileMenu
.Append(wxID_NEW
, "New\tCTRL-N")
163 self
.fileMenu
.Append(wxID_OPEN
, "Open...\tCTRL-O")
164 self
.fileMenu
.Append(wxID_CLOSE
, "Close\tCTRL-W")
165 self
.fileMenu
.AppendSeparator()
166 self
.fileMenu
.Append(wxID_SAVE
, "Save\tCTRL-S")
167 self
.fileMenu
.Append(wxID_SAVEAS
, "Save As...")
168 self
.fileMenu
.Append(wxID_REVERT
, "Revert...")
169 self
.fileMenu
.AppendSeparator()
170 self
.fileMenu
.Append(wxID_EXIT
, "Quit\tCTRL-Q")
172 menuBar
.Append(self
.fileMenu
, "File")
174 self
.editMenu
= wxMenu()
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
= wxMenu()
186 self
.toolsMenu
.Append(menu_SELECT
, "Selection", kind
=wxITEM_CHECK
)
187 self
.toolsMenu
.Append(menu_LINE
, "Line", kind
=wxITEM_CHECK
)
188 self
.toolsMenu
.Append(menu_RECT
, "Rectangle", kind
=wxITEM_CHECK
)
189 self
.toolsMenu
.Append(menu_ELLIPSE
, "Ellipse", kind
=wxITEM_CHECK
)
190 self
.toolsMenu
.Append(menu_TEXT
, "Text", kind
=wxITEM_CHECK
)
192 menuBar
.Append(self
.toolsMenu
, "Tools")
194 self
.objectMenu
= wxMenu()
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
= wxMenu()
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(wxTB_HORIZONTAL |
212 wxNO_BORDER | wxTB_FLAT
)
214 self
.toolbar
.AddSimpleTool(wxID_NEW
,
215 wxBitmap("images/new.bmp",
218 self
.toolbar
.AddSimpleTool(wxID_OPEN
,
219 wxBitmap("images/open.bmp",
222 self
.toolbar
.AddSimpleTool(wxID_SAVE
,
223 wxBitmap("images/save.bmp",
226 self
.toolbar
.AddSeparator()
227 self
.toolbar
.AddSimpleTool(menu_UNDO
,
228 wxBitmap("images/undo.bmp",
231 self
.toolbar
.AddSeparator()
232 self
.toolbar
.AddSimpleTool(menu_DUPLICATE
,
233 wxBitmap("images/duplicate.bmp",
236 self
.toolbar
.AddSeparator()
237 self
.toolbar
.AddSimpleTool(menu_MOVE_FORWARD
,
238 wxBitmap("images/moveForward.bmp",
241 self
.toolbar
.AddSimpleTool(menu_MOVE_BACKWARD
,
242 wxBitmap("images/moveBack.bmp",
246 self
.toolbar
.Realize()
248 # Associate each menu/toolbar item with the method that handles that
251 EVT_MENU(self
, wxID_NEW
, self
.doNew
)
252 EVT_MENU(self
, wxID_OPEN
, self
.doOpen
)
253 EVT_MENU(self
, wxID_CLOSE
, self
.doClose
)
254 EVT_MENU(self
, wxID_SAVE
, self
.doSave
)
255 EVT_MENU(self
, wxID_SAVEAS
, self
.doSaveAs
)
256 EVT_MENU(self
, wxID_REVERT
, self
.doRevert
)
257 EVT_MENU(self
, wxID_EXIT
, self
.doExit
)
259 EVT_MENU(self
, menu_UNDO
, self
.doUndo
)
260 EVT_MENU(self
, menu_SELECT_ALL
, self
.doSelectAll
)
261 EVT_MENU(self
, menu_DUPLICATE
, self
.doDuplicate
)
262 EVT_MENU(self
, menu_EDIT_TEXT
, self
.doEditText
)
263 EVT_MENU(self
, menu_DELETE
, self
.doDelete
)
265 EVT_MENU(self
, menu_SELECT
, self
.doChooseSelectTool
)
266 EVT_MENU(self
, menu_LINE
, self
.doChooseLineTool
)
267 EVT_MENU(self
, menu_RECT
, self
.doChooseRectTool
)
268 EVT_MENU(self
, menu_ELLIPSE
, self
.doChooseEllipseTool
)
269 EVT_MENU(self
, menu_TEXT
, self
.doChooseTextTool
)
271 EVT_MENU(self
, menu_MOVE_FORWARD
, self
.doMoveForward
)
272 EVT_MENU(self
, menu_MOVE_TO_FRONT
, self
.doMoveToFront
)
273 EVT_MENU(self
, menu_MOVE_BACKWARD
, self
.doMoveBackward
)
274 EVT_MENU(self
, menu_MOVE_TO_BACK
, self
.doMoveToBack
)
276 EVT_MENU(self
, menu_ABOUT
, self
.doShowAbout
)
278 # Install our own method to handle closing the window. This allows us
279 # to ask the user if he/she wants to save before closing the window, as
280 # well as keeping track of which windows are currently open.
282 EVT_CLOSE(self
, self
.doClose
)
284 # Install our own method for handling keystrokes. We use this to let
285 # the user move the selected object(s) around using the arrow keys.
287 EVT_CHAR_HOOK(self
, self
.onKeyEvent
)
289 # Setup our top-most panel. This holds the entire contents of the
290 # window, excluding the menu bar.
292 self
.topPanel
= wxPanel(self
, -1, style
=wxSIMPLE_BORDER
)
294 # Setup our tool palette, with all our drawing tools and option icons.
296 self
.toolPalette
= wxBoxSizer(wxVERTICAL
)
298 self
.selectIcon
= ToolPaletteIcon(self
.topPanel
, id_SELECT
,
299 "select", "Selection Tool")
300 self
.lineIcon
= ToolPaletteIcon(self
.topPanel
, id_LINE
,
302 self
.rectIcon
= ToolPaletteIcon(self
.topPanel
, id_RECT
,
303 "rect", "Rectangle Tool")
304 self
.ellipseIcon
= ToolPaletteIcon(self
.topPanel
, id_ELLIPSE
,
305 "ellipse", "Ellipse Tool")
306 self
.textIcon
= ToolPaletteIcon(self
.topPanel
, id_TEXT
,
309 toolSizer
= wxGridSizer(0, 2, 5, 5)
310 toolSizer
.Add(self
.selectIcon
)
311 toolSizer
.Add((0, 0)) # Gap to make tool icons line up nicely.
312 toolSizer
.Add(self
.lineIcon
)
313 toolSizer
.Add(self
.rectIcon
)
314 toolSizer
.Add(self
.ellipseIcon
)
315 toolSizer
.Add(self
.textIcon
)
317 self
.optionIndicator
= ToolOptionIndicator(self
.topPanel
)
318 self
.optionIndicator
.SetToolTip(
319 wxToolTip("Shows Current Pen/Fill/Line Size Settings"))
321 optionSizer
= wxBoxSizer(wxHORIZONTAL
)
323 self
.penOptIcon
= ToolPaletteIcon(self
.topPanel
, id_PEN_OPT
,
324 "penOpt", "Set Pen Colour")
325 self
.fillOptIcon
= ToolPaletteIcon(self
.topPanel
, id_FILL_OPT
,
326 "fillOpt", "Set Fill Colour")
327 self
.lineOptIcon
= ToolPaletteIcon(self
.topPanel
, id_LINE_OPT
,
328 "lineOpt", "Set Line Size")
330 margin
= wxLEFT | wxRIGHT
331 optionSizer
.Add(self
.penOptIcon
, 0, margin
, 1)
332 optionSizer
.Add(self
.fillOptIcon
, 0, margin
, 1)
333 optionSizer
.Add(self
.lineOptIcon
, 0, margin
, 1)
335 margin
= wxTOP | wxLEFT | wxRIGHT | wxALIGN_CENTRE
336 self
.toolPalette
.Add(toolSizer
, 0, margin
, 5)
337 self
.toolPalette
.Add((0, 0), 0, margin
, 5) # Spacer.
338 self
.toolPalette
.Add(self
.optionIndicator
, 0, margin
, 5)
339 self
.toolPalette
.Add(optionSizer
, 0, margin
, 5)
341 # Make the tool palette icons respond when the user clicks on them.
343 EVT_BUTTON(self
.selectIcon
, -1, self
.onToolIconClick
)
344 EVT_BUTTON(self
.lineIcon
, -1, self
.onToolIconClick
)
345 EVT_BUTTON(self
.rectIcon
, -1, self
.onToolIconClick
)
346 EVT_BUTTON(self
.ellipseIcon
, -1, self
.onToolIconClick
)
347 EVT_BUTTON(self
.textIcon
, -1, self
.onToolIconClick
)
348 EVT_BUTTON(self
.penOptIcon
, -1, self
.onPenOptionIconClick
)
349 EVT_BUTTON(self
.fillOptIcon
, -1, self
.onFillOptionIconClick
)
350 EVT_BUTTON(self
.lineOptIcon
, -1, self
.onLineOptionIconClick
)
352 # Setup the main drawing area.
354 self
.drawPanel
= wxScrolledWindow(self
.topPanel
, -1,
355 style
=wxSUNKEN_BORDER
)
356 self
.drawPanel
.SetBackgroundColour(wxWHITE
)
358 self
.drawPanel
.EnableScrolling(True, True)
359 self
.drawPanel
.SetScrollbars(20, 20, PAGE_WIDTH
/ 20, PAGE_HEIGHT
/ 20)
361 EVT_LEFT_DOWN(self
.drawPanel
, self
.onMouseEvent
)
362 EVT_LEFT_DCLICK(self
.drawPanel
, self
.onDoubleClickEvent
)
363 EVT_RIGHT_DOWN(self
.drawPanel
, self
.onRightClick
)
364 EVT_MOTION(self
.drawPanel
, self
.onMouseEvent
)
365 EVT_LEFT_UP(self
.drawPanel
, self
.onMouseEvent
)
366 EVT_PAINT(self
.drawPanel
, self
.onPaintEvent
)
368 # Position everything in the window.
370 topSizer
= wxBoxSizer(wxHORIZONTAL
)
371 topSizer
.Add(self
.toolPalette
, 0)
372 topSizer
.Add(self
.drawPanel
, 1, wxEXPAND
)
374 self
.topPanel
.SetAutoLayout(True)
375 self
.topPanel
.SetSizer(topSizer
)
377 self
.SetSizeHints(minW
=250, minH
=200)
378 self
.SetSize(wxSize(600, 400))
380 # Select an initial tool.
383 self
._setCurrentTool
(self
.selectIcon
)
385 # Setup our frame to hold the contents of a sketch document.
388 self
.fileName
= fileName
389 self
.contents
= [] # front-to-back ordered list of DrawingObjects.
390 self
.selection
= [] # List of selected DrawingObjects.
391 self
.undoInfo
= None # Saved contents for undo.
392 self
.dragMode
= drag_NONE
# Current mouse-drag mode.
394 if self
.fileName
!= None:
399 # Finally, set our initial pen, fill and line options.
401 self
.penColour
= wxBLACK
402 self
.fillColour
= wxWHITE
405 # ============================
406 # == Event Handling Methods ==
407 # ============================
409 def onToolIconClick(self
, event
):
410 """ Respond to the user clicking on one of our tool icons.
412 iconID
= event
.GetEventObject().GetId()
414 if iconID
== id_SELECT
: self
.doChooseSelectTool()
415 elif iconID
== id_LINE
: self
.doChooseLineTool()
416 elif iconID
== id_RECT
: self
.doChooseRectTool()
417 elif iconID
== id_ELLIPSE
: self
.doChooseEllipseTool()
418 elif iconID
== id_TEXT
: self
.doChooseTextTool()
419 else: wxBell(); print "1"
422 def onPenOptionIconClick(self
, event
):
423 """ Respond to the user clicking on the "Pen Options" icon.
425 data
= wxColourData()
426 if len(self
.selection
) == 1:
427 data
.SetColour(self
.selection
[0].getPenColour())
429 data
.SetColour(self
.penColour
)
431 dialog
= wxColourDialog(self
, data
)
432 if dialog
.ShowModal() == wxID_OK
:
433 c
= dialog
.GetColourData().GetColour()
434 self
._setPenColour
(wxColour(c
.Red(), c
.Green(), c
.Blue()))
438 def onFillOptionIconClick(self
, event
):
439 """ Respond to the user clicking on the "Fill Options" icon.
441 data
= wxColourData()
442 if len(self
.selection
) == 1:
443 data
.SetColour(self
.selection
[0].getFillColour())
445 data
.SetColour(self
.fillColour
)
447 dialog
= wxColourDialog(self
, data
)
448 if dialog
.ShowModal() == wxID_OK
:
449 c
= dialog
.GetColourData().GetColour()
450 self
._setFillColour
(wxColour(c
.Red(), c
.Green(), c
.Blue()))
453 def onLineOptionIconClick(self
, event
):
454 """ Respond to the user clicking on the "Line Options" icon.
456 if len(self
.selection
) == 1:
457 menu
= self
._buildLineSizePopup
(self
.selection
[0].getLineSize())
459 menu
= self
._buildLineSizePopup
(self
.lineSize
)
461 pos
= self
.lineOptIcon
.GetPosition()
462 pos
.y
= pos
.y
+ self
.lineOptIcon
.GetSize().height
463 self
.PopupMenu(menu
, pos
)
467 def onKeyEvent(self
, event
):
468 """ Respond to a keypress event.
470 We make the arrow keys move the selected object(s) by one pixel in
473 if event
.GetKeyCode() == WXK_UP
:
474 self
._moveObject
(0, -1)
475 elif event
.GetKeyCode() == WXK_DOWN
:
476 self
._moveObject
(0, 1)
477 elif event
.GetKeyCode() == WXK_LEFT
:
478 self
._moveObject
(-1, 0)
479 elif event
.GetKeyCode() == WXK_RIGHT
:
480 self
._moveObject
(1, 0)
485 def onMouseEvent(self
, event
):
486 """ Respond to the user clicking on our main drawing panel.
488 How we respond depends on the currently selected tool.
490 if not (event
.LeftDown() or event
.Dragging() or event
.LeftUp()):
491 return # Ignore mouse movement without click/drag.
493 if self
.curTool
== self
.selectIcon
:
494 feedbackType
= feedback_RECT
495 action
= self
.selectByRectangle
496 actionParam
= param_RECT
499 elif self
.curTool
== self
.lineIcon
:
500 feedbackType
= feedback_LINE
501 action
= self
.createLine
502 actionParam
= param_LINE
505 elif self
.curTool
== self
.rectIcon
:
506 feedbackType
= feedback_RECT
507 action
= self
.createRect
508 actionParam
= param_RECT
511 elif self
.curTool
== self
.ellipseIcon
:
512 feedbackType
= feedback_ELLIPSE
513 action
= self
.createEllipse
514 actionParam
= param_RECT
517 elif self
.curTool
== self
.textIcon
:
518 feedbackType
= feedback_RECT
519 action
= self
.createText
520 actionParam
= param_RECT
528 mousePt
= self
._getEventCoordinates
(event
)
530 obj
, handle
= self
._getObjectAndSelectionHandleAt
(mousePt
)
532 if selecting
and (obj
!= None) and (handle
!= handle_NONE
):
534 # The user clicked on an object's selection handle. Let the
535 # user resize the clicked-on object.
537 self
.dragMode
= drag_RESIZE
538 self
.resizeObject
= obj
540 if obj
.getType() == obj_LINE
:
541 self
.resizeFeedback
= feedback_LINE
542 pos
= obj
.getPosition()
543 startPt
= wxPoint(pos
.x
+ obj
.getStartPt().x
,
544 pos
.y
+ obj
.getStartPt().y
)
545 endPt
= wxPoint(pos
.x
+ obj
.getEndPt().x
,
546 pos
.y
+ obj
.getEndPt().y
)
547 if handle
== handle_START_POINT
:
548 self
.resizeAnchor
= endPt
549 self
.resizeFloater
= startPt
551 self
.resizeAnchor
= startPt
552 self
.resizeFloater
= endPt
554 self
.resizeFeedback
= feedback_RECT
555 pos
= obj
.getPosition()
557 topLeft
= wxPoint(pos
.x
, pos
.y
)
558 topRight
= wxPoint(pos
.x
+ size
.width
, pos
.y
)
559 botLeft
= wxPoint(pos
.x
, pos
.y
+ size
.height
)
560 botRight
= wxPoint(pos
.x
+ size
.width
, pos
.y
+ size
.height
)
562 if handle
== handle_TOP_LEFT
:
563 self
.resizeAnchor
= botRight
564 self
.resizeFloater
= topLeft
565 elif handle
== handle_TOP_RIGHT
:
566 self
.resizeAnchor
= botLeft
567 self
.resizeFloater
= topRight
568 elif handle
== handle_BOTTOM_LEFT
:
569 self
.resizeAnchor
= topRight
570 self
.resizeFloater
= botLeft
571 elif handle
== handle_BOTTOM_RIGHT
:
572 self
.resizeAnchor
= topLeft
573 self
.resizeFloater
= botRight
576 self
.resizeOffsetX
= self
.resizeFloater
.x
- mousePt
.x
577 self
.resizeOffsetY
= self
.resizeFloater
.y
- mousePt
.y
578 endPt
= wxPoint(self
.curPt
.x
+ self
.resizeOffsetX
,
579 self
.curPt
.y
+ self
.resizeOffsetY
)
580 self
._drawVisualFeedback
(self
.resizeAnchor
, endPt
,
581 self
.resizeFeedback
, False)
583 elif selecting
and (self
._getObjectAt
(mousePt
) != None):
585 # The user clicked on an object to select it. If the user
586 # drags, he/she will move the object.
588 self
.select(self
._getObjectAt
(mousePt
))
589 self
.dragMode
= drag_MOVE
590 self
.moveOrigin
= mousePt
592 self
._drawObjectOutline
(0, 0)
596 # The user is dragging out a selection rect or new object.
598 self
.dragOrigin
= mousePt
600 self
.drawPanel
.SetCursor(wxCROSS_CURSOR
)
601 self
.drawPanel
.CaptureMouse()
602 self
._drawVisualFeedback
(mousePt
, mousePt
, feedbackType
,
604 self
.dragMode
= drag_DRAG
610 if self
.dragMode
== drag_RESIZE
:
612 # We're resizing an object.
614 mousePt
= self
._getEventCoordinates
(event
)
615 if (self
.curPt
.x
!= mousePt
.x
) or (self
.curPt
.y
!= mousePt
.y
):
616 # Erase previous visual feedback.
617 endPt
= wxPoint(self
.curPt
.x
+ self
.resizeOffsetX
,
618 self
.curPt
.y
+ self
.resizeOffsetY
)
619 self
._drawVisualFeedback
(self
.resizeAnchor
, endPt
,
620 self
.resizeFeedback
, False)
622 # Draw new visual feedback.
623 endPt
= wxPoint(self
.curPt
.x
+ self
.resizeOffsetX
,
624 self
.curPt
.y
+ self
.resizeOffsetY
)
625 self
._drawVisualFeedback
(self
.resizeAnchor
, endPt
,
626 self
.resizeFeedback
, False)
628 elif self
.dragMode
== drag_MOVE
:
630 # We're moving a selected object.
632 mousePt
= self
._getEventCoordinates
(event
)
633 if (self
.curPt
.x
!= mousePt
.x
) or (self
.curPt
.y
!= mousePt
.y
):
634 # Erase previous visual feedback.
635 self
._drawObjectOutline
(self
.curPt
.x
- self
.moveOrigin
.x
,
636 self
.curPt
.y
- self
.moveOrigin
.y
)
638 # Draw new visual feedback.
639 self
._drawObjectOutline
(self
.curPt
.x
- self
.moveOrigin
.x
,
640 self
.curPt
.y
- self
.moveOrigin
.y
)
642 elif self
.dragMode
== drag_DRAG
:
644 # We're dragging out a new object or selection rect.
646 mousePt
= self
._getEventCoordinates
(event
)
647 if (self
.curPt
.x
!= mousePt
.x
) or (self
.curPt
.y
!= mousePt
.y
):
648 # Erase previous visual feedback.
649 self
._drawVisualFeedback
(self
.dragOrigin
, self
.curPt
,
650 feedbackType
, dashedLine
)
652 # Draw new visual feedback.
653 self
._drawVisualFeedback
(self
.dragOrigin
, self
.curPt
,
654 feedbackType
, dashedLine
)
660 if self
.dragMode
== drag_RESIZE
:
662 # We're resizing an object.
664 mousePt
= self
._getEventCoordinates
(event
)
665 # Erase last visual feedback.
666 endPt
= wxPoint(self
.curPt
.x
+ self
.resizeOffsetX
,
667 self
.curPt
.y
+ self
.resizeOffsetY
)
668 self
._drawVisualFeedback
(self
.resizeAnchor
, endPt
,
669 self
.resizeFeedback
, False)
671 resizePt
= wxPoint(mousePt
.x
+ self
.resizeOffsetX
,
672 mousePt
.y
+ self
.resizeOffsetY
)
674 if (self
.resizeFloater
.x
!= resizePt
.x
) or \
675 (self
.resizeFloater
.y
!= resizePt
.y
):
676 self
._resizeObject
(self
.resizeObject
,
681 self
.drawPanel
.Refresh() # Clean up after empty resize.
683 elif self
.dragMode
== drag_MOVE
:
685 # We're moving a selected object.
687 mousePt
= self
._getEventCoordinates
(event
)
688 # Erase last visual feedback.
689 self
._drawObjectOutline
(self
.curPt
.x
- self
.moveOrigin
.x
,
690 self
.curPt
.y
- self
.moveOrigin
.y
)
691 if (self
.moveOrigin
.x
!= mousePt
.x
) or \
692 (self
.moveOrigin
.y
!= mousePt
.y
):
693 self
._moveObject
(mousePt
.x
- self
.moveOrigin
.x
,
694 mousePt
.y
- self
.moveOrigin
.y
)
696 self
.drawPanel
.Refresh() # Clean up after empty drag.
698 elif self
.dragMode
== drag_DRAG
:
700 # We're dragging out a new object or selection rect.
702 mousePt
= self
._getEventCoordinates
(event
)
703 # Erase last visual feedback.
704 self
._drawVisualFeedback
(self
.dragOrigin
, self
.curPt
,
705 feedbackType
, dashedLine
)
706 self
.drawPanel
.ReleaseMouse()
707 self
.drawPanel
.SetCursor(wxSTANDARD_CURSOR
)
708 # Perform the appropriate action for the current tool.
709 if actionParam
== param_RECT
:
710 x1
= min(self
.dragOrigin
.x
, self
.curPt
.x
)
711 y1
= min(self
.dragOrigin
.y
, self
.curPt
.y
)
712 x2
= max(self
.dragOrigin
.x
, self
.curPt
.x
)
713 y2
= max(self
.dragOrigin
.y
, self
.curPt
.y
)
721 if ((x2
-x1
) < 8) or ((y2
-y1
) < 8): return # Too small.
723 action(x1
, y1
, x2
-x1
, y2
-y1
)
724 elif actionParam
== param_LINE
:
725 action(self
.dragOrigin
.x
, self
.dragOrigin
.y
,
726 self
.curPt
.x
, self
.curPt
.y
)
728 self
.dragMode
= drag_NONE
# We've finished with this mouse event.
732 def onDoubleClickEvent(self
, event
):
733 """ Respond to a double-click within our drawing panel.
735 mousePt
= self
._getEventCoordinates
(event
)
736 obj
= self
._getObjectAt
(mousePt
)
737 if obj
== None: return
739 # Let the user edit the given object.
741 if obj
.getType() == obj_TEXT
:
742 editor
= EditTextObjectDialog(self
, "Edit Text Object")
743 editor
.objectToDialog(obj
)
744 if editor
.ShowModal() == wxID_CANCEL
:
750 editor
.dialogToObject(obj
)
754 self
.drawPanel
.Refresh()
760 def onRightClick(self
, event
):
761 """ Respond to the user right-clicking within our drawing panel.
763 We select the clicked-on item, if necessary, and display a pop-up
764 menu of available options which can be applied to the selected
767 mousePt
= self
._getEventCoordinates
(event
)
768 obj
= self
._getObjectAt
(mousePt
)
770 if obj
== None: return # Nothing selected.
772 # Select the clicked-on object.
776 # Build our pop-up menu.
779 menu
.Append(menu_DUPLICATE
, "Duplicate")
780 menu
.Append(menu_EDIT_TEXT
, "Edit...")
781 menu
.Append(menu_DELETE
, "Delete")
782 menu
.AppendSeparator()
783 menu
.Append(menu_MOVE_FORWARD
, "Move Forward")
784 menu
.Append(menu_MOVE_TO_FRONT
, "Move to Front")
785 menu
.Append(menu_MOVE_BACKWARD
, "Move Backward")
786 menu
.Append(menu_MOVE_TO_BACK
, "Move to Back")
788 menu
.Enable(menu_EDIT_TEXT
, obj
.getType() == obj_TEXT
)
789 menu
.Enable(menu_MOVE_FORWARD
, obj
!= self
.contents
[0])
790 menu
.Enable(menu_MOVE_TO_FRONT
, obj
!= self
.contents
[0])
791 menu
.Enable(menu_MOVE_BACKWARD
, obj
!= self
.contents
[-1])
792 menu
.Enable(menu_MOVE_TO_BACK
, obj
!= self
.contents
[-1])
794 EVT_MENU(self
, menu_DUPLICATE
, self
.doDuplicate
)
795 EVT_MENU(self
, menu_EDIT_TEXT
, self
.doEditText
)
796 EVT_MENU(self
, menu_DELETE
, self
.doDelete
)
797 EVT_MENU(self
, menu_MOVE_FORWARD
, self
.doMoveForward
)
798 EVT_MENU(self
, menu_MOVE_TO_FRONT
, self
.doMoveToFront
)
799 EVT_MENU(self
, menu_MOVE_BACKWARD
, self
.doMoveBackward
)
800 EVT_MENU(self
, menu_MOVE_TO_BACK
, self
.doMoveToBack
)
802 # Show the pop-up menu.
804 clickPt
= wxPoint(mousePt
.x
+ self
.drawPanel
.GetPosition().x
,
805 mousePt
.y
+ self
.drawPanel
.GetPosition().y
)
806 self
.drawPanel
.PopupMenu(menu
, clickPt
)
810 def onPaintEvent(self
, event
):
811 """ Respond to a request to redraw the contents of our drawing panel.
813 dc
= wxPaintDC(self
.drawPanel
)
814 self
.drawPanel
.PrepareDC(dc
)
817 for i
in range(len(self
.contents
)-1, -1, -1):
818 obj
= self
.contents
[i
]
819 if obj
in self
.selection
:
826 # ==========================
827 # == Menu Command Methods ==
828 # ==========================
830 def doNew(self
, event
):
831 """ Respond to the "New" menu command.
834 newFrame
= DrawingFrame(None, -1, "Untitled")
836 _docList
.append(newFrame
)
839 def doOpen(self
, event
):
840 """ Respond to the "Open" menu command.
845 fileName
= wxFileSelector("Open File", default_extension
="psk",
846 flags
= wxOPEN | wxFILE_MUST_EXIST
)
847 if fileName
== "": return
848 fileName
= os
.path
.join(os
.getcwd(), fileName
)
851 title
= os
.path
.basename(fileName
)
853 if (self
.fileName
== None) and (len(self
.contents
) == 0):
854 # Load contents into current (empty) document.
855 self
.fileName
= fileName
856 self
.SetTitle(os
.path
.basename(fileName
))
859 # Open a new frame for this document.
860 newFrame
= DrawingFrame(None, -1, os
.path
.basename(fileName
),
863 _docList
.append(newFrame
)
866 def doClose(self
, event
):
867 """ Respond to the "Close" menu command.
872 if not self
.askIfUserWantsToSave("closing"): return
874 _docList
.remove(self
)
878 def doSave(self
, event
):
879 """ Respond to the "Save" menu command.
881 if self
.fileName
!= None:
885 def doSaveAs(self
, event
):
886 """ Respond to the "Save As" menu command.
888 if self
.fileName
== None:
891 default
= self
.fileName
894 fileName
= wxFileSelector("Save File As", "Saving",
895 default_filename
=default
,
896 default_extension
="psk",
898 flags
= wxSAVE | wxOVERWRITE_PROMPT
)
899 if fileName
== "": return # User cancelled.
900 fileName
= os
.path
.join(os
.getcwd(), fileName
)
903 title
= os
.path
.basename(fileName
)
906 self
.fileName
= fileName
910 def doRevert(self
, event
):
911 """ Respond to the "Revert" menu command.
913 if not self
.dirty
: return
915 if wxMessageBox("Discard changes made to this document?", "Confirm",
916 style
= wxOK | wxCANCEL | wxICON_QUESTION
,
917 parent
=self
) == wxCANCEL
: return
921 def doExit(self
, event
):
922 """ Respond to the "Quit" menu command.
924 global _docList
, _app
926 if not doc
.dirty
: continue
928 if not doc
.askIfUserWantsToSave("quitting"): return
935 def doUndo(self
, event
):
936 """ Respond to the "Undo" menu command.
938 if self
.undoInfo
== None: return
940 undoData
= self
.undoInfo
941 self
._saveUndoInfo
() # For undoing the undo...
945 for type, data
in undoData
["contents"]:
946 obj
= DrawingObject(type)
948 self
.contents
.append(obj
)
951 for i
in undoData
["selection"]:
952 self
.selection
.append(self
.contents
[i
])
955 self
.drawPanel
.Refresh()
959 def doSelectAll(self
, event
):
960 """ Respond to the "Select All" menu command.
965 def doDuplicate(self
, event
):
966 """ Respond to the "Duplicate" menu command.
971 for obj
in self
.contents
:
972 if obj
in self
.selection
:
973 newObj
= DrawingObject(obj
.getType())
974 newObj
.setData(obj
.getData())
975 pos
= obj
.getPosition()
976 newObj
.setPosition(wxPoint(pos
.x
+ 10, pos
.y
+ 10))
979 self
.contents
= objs
+ self
.contents
981 self
.selectMany(objs
)
984 def doEditText(self
, event
):
985 """ Respond to the "Edit Text" menu command.
987 if len(self
.selection
) != 1: return
989 obj
= self
.selection
[0]
990 if obj
.getType() != obj_TEXT
: return
992 editor
= EditTextObjectDialog(self
, "Edit Text Object")
993 editor
.objectToDialog(obj
)
994 if editor
.ShowModal() == wxID_CANCEL
:
1000 editor
.dialogToObject(obj
)
1004 self
.drawPanel
.Refresh()
1008 def doDelete(self
, event
):
1009 """ Respond to the "Delete" menu command.
1011 self
._saveUndoInfo
()
1013 for obj
in self
.selection
:
1014 self
.contents
.remove(obj
)
1019 def doChooseSelectTool(self
, event
=None):
1020 """ Respond to the "Select Tool" menu command.
1022 self
._setCurrentTool
(self
.selectIcon
)
1023 self
.drawPanel
.SetCursor(wxSTANDARD_CURSOR
)
1027 def doChooseLineTool(self
, event
=None):
1028 """ Respond to the "Line Tool" menu command.
1030 self
._setCurrentTool
(self
.lineIcon
)
1031 self
.drawPanel
.SetCursor(wxCROSS_CURSOR
)
1036 def doChooseRectTool(self
, event
=None):
1037 """ Respond to the "Rect Tool" menu command.
1039 self
._setCurrentTool
(self
.rectIcon
)
1040 self
.drawPanel
.SetCursor(wxCROSS_CURSOR
)
1045 def doChooseEllipseTool(self
, event
=None):
1046 """ Respond to the "Ellipse Tool" menu command.
1048 self
._setCurrentTool
(self
.ellipseIcon
)
1049 self
.drawPanel
.SetCursor(wxCROSS_CURSOR
)
1054 def doChooseTextTool(self
, event
=None):
1055 """ Respond to the "Text Tool" menu command.
1057 self
._setCurrentTool
(self
.textIcon
)
1058 self
.drawPanel
.SetCursor(wxCROSS_CURSOR
)
1063 def doMoveForward(self
, event
):
1064 """ Respond to the "Move Forward" menu command.
1066 if len(self
.selection
) != 1: return
1068 self
._saveUndoInfo
()
1070 obj
= self
.selection
[0]
1071 index
= self
.contents
.index(obj
)
1072 if index
== 0: return
1074 del self
.contents
[index
]
1075 self
.contents
.insert(index
-1, obj
)
1077 self
.drawPanel
.Refresh()
1081 def doMoveToFront(self
, event
):
1082 """ Respond to the "Move to Front" menu command.
1084 if len(self
.selection
) != 1: return
1086 self
._saveUndoInfo
()
1088 obj
= self
.selection
[0]
1089 self
.contents
.remove(obj
)
1090 self
.contents
.insert(0, obj
)
1092 self
.drawPanel
.Refresh()
1096 def doMoveBackward(self
, event
):
1097 """ Respond to the "Move Backward" menu command.
1099 if len(self
.selection
) != 1: return
1101 self
._saveUndoInfo
()
1103 obj
= self
.selection
[0]
1104 index
= self
.contents
.index(obj
)
1105 if index
== len(self
.contents
) - 1: return
1107 del self
.contents
[index
]
1108 self
.contents
.insert(index
+1, obj
)
1110 self
.drawPanel
.Refresh()
1114 def doMoveToBack(self
, event
):
1115 """ Respond to the "Move to Back" menu command.
1117 if len(self
.selection
) != 1: return
1119 self
._saveUndoInfo
()
1121 obj
= self
.selection
[0]
1122 self
.contents
.remove(obj
)
1123 self
.contents
.append(obj
)
1125 self
.drawPanel
.Refresh()
1129 def doShowAbout(self
, event
):
1130 """ Respond to the "About pySketch" menu command.
1132 dialog
= wxDialog(self
, -1, "About pySketch") # ,
1133 #style=wxDIALOG_MODAL | wxSTAY_ON_TOP)
1134 dialog
.SetBackgroundColour(wxWHITE
)
1136 panel
= wxPanel(dialog
, -1)
1137 panel
.SetBackgroundColour(wxWHITE
)
1139 panelSizer
= wxBoxSizer(wxVERTICAL
)
1141 boldFont
= wxFont(panel
.GetFont().GetPointSize(),
1142 panel
.GetFont().GetFamily(),
1145 logo
= wxStaticBitmap(panel
, -1, wxBitmap("images/logo.bmp",
1148 lab1
= wxStaticText(panel
, -1, "pySketch")
1149 lab1
.SetFont(wxFont(36, boldFont
.GetFamily(), wxITALIC
, wxBOLD
))
1150 lab1
.SetSize(lab1
.GetBestSize())
1152 imageSizer
= wxBoxSizer(wxHORIZONTAL
)
1153 imageSizer
.Add(logo
, 0, wxALL | wxALIGN_CENTRE_VERTICAL
, 5)
1154 imageSizer
.Add(lab1
, 0, wxALL | wxALIGN_CENTRE_VERTICAL
, 5)
1156 lab2
= wxStaticText(panel
, -1, "A simple object-oriented drawing " + \
1158 lab2
.SetFont(boldFont
)
1159 lab2
.SetSize(lab2
.GetBestSize())
1161 lab3
= wxStaticText(panel
, -1, "pySketch is completely free " + \
1163 lab3
.SetFont(boldFont
)
1164 lab3
.SetSize(lab3
.GetBestSize())
1166 lab4
= wxStaticText(panel
, -1, "feel free to adapt or use this " + \
1167 "in any way you like.")
1168 lab4
.SetFont(boldFont
)
1169 lab4
.SetSize(lab4
.GetBestSize())
1171 lab5
= wxStaticText(panel
, -1, "Author: Erik Westra " + \
1172 "(ewestra@wave.co.nz)")
1173 lab5
.SetFont(boldFont
)
1174 lab5
.SetSize(lab5
.GetBestSize())
1176 btnOK
= wxButton(panel
, wxID_OK
, "OK")
1178 panelSizer
.Add(imageSizer
, 0, wxALIGN_CENTRE
)
1179 panelSizer
.Add((10, 10)) # Spacer.
1180 panelSizer
.Add(lab2
, 0, wxALIGN_CENTRE
)
1181 panelSizer
.Add((10, 10)) # Spacer.
1182 panelSizer
.Add(lab3
, 0, wxALIGN_CENTRE
)
1183 panelSizer
.Add(lab4
, 0, wxALIGN_CENTRE
)
1184 panelSizer
.Add((10, 10)) # Spacer.
1185 panelSizer
.Add(lab5
, 0, wxALIGN_CENTRE
)
1186 panelSizer
.Add((10, 10)) # Spacer.
1187 panelSizer
.Add(btnOK
, 0, wxALL | wxALIGN_CENTRE
, 5)
1189 panel
.SetAutoLayout(True)
1190 panel
.SetSizer(panelSizer
)
1191 panelSizer
.Fit(panel
)
1193 topSizer
= wxBoxSizer(wxHORIZONTAL
)
1194 topSizer
.Add(panel
, 0, wxALL
, 10)
1196 dialog
.SetAutoLayout(True)
1197 dialog
.SetSizer(topSizer
)
1198 topSizer
.Fit(dialog
)
1202 btn
= dialog
.ShowModal()
1205 # =============================
1206 # == Object Creation Methods ==
1207 # =============================
1209 def createLine(self
, x1
, y1
, x2
, y2
):
1210 """ Create a new line object at the given position and size.
1212 self
._saveUndoInfo
()
1214 topLeftX
= min(x1
, x2
)
1215 topLeftY
= min(y1
, y2
)
1216 botRightX
= max(x1
, x2
)
1217 botRightY
= max(y1
, y2
)
1219 obj
= DrawingObject(obj_LINE
, position
=wxPoint(topLeftX
, topLeftY
),
1220 size
=wxSize(botRightX
-topLeftX
,
1221 botRightY
-topLeftY
),
1222 penColour
=self
.penColour
,
1223 fillColour
=self
.fillColour
,
1224 lineSize
=self
.lineSize
,
1225 startPt
= wxPoint(x1
- topLeftX
, y1
- topLeftY
),
1226 endPt
= wxPoint(x2
- topLeftX
, y2
- topLeftY
))
1227 self
.contents
.insert(0, obj
)
1229 self
.doChooseSelectTool()
1233 def createRect(self
, x
, y
, width
, height
):
1234 """ Create a new rectangle object at the given position and size.
1236 self
._saveUndoInfo
()
1238 obj
= DrawingObject(obj_RECT
, position
=wxPoint(x
, y
),
1239 size
=wxSize(width
, height
),
1240 penColour
=self
.penColour
,
1241 fillColour
=self
.fillColour
,
1242 lineSize
=self
.lineSize
)
1243 self
.contents
.insert(0, obj
)
1245 self
.doChooseSelectTool()
1249 def createEllipse(self
, x
, y
, width
, height
):
1250 """ Create a new ellipse object at the given position and size.
1252 self
._saveUndoInfo
()
1254 obj
= DrawingObject(obj_ELLIPSE
, position
=wxPoint(x
, y
),
1255 size
=wxSize(width
, height
),
1256 penColour
=self
.penColour
,
1257 fillColour
=self
.fillColour
,
1258 lineSize
=self
.lineSize
)
1259 self
.contents
.insert(0, obj
)
1261 self
.doChooseSelectTool()
1265 def createText(self
, x
, y
, width
, height
):
1266 """ Create a new text object at the given position and size.
1268 editor
= EditTextObjectDialog(self
, "Create Text Object")
1269 if editor
.ShowModal() == wxID_CANCEL
:
1273 self
._saveUndoInfo
()
1275 obj
= DrawingObject(obj_TEXT
, position
=wxPoint(x
, y
),
1276 size
=wxSize(width
, height
))
1277 editor
.dialogToObject(obj
)
1280 self
.contents
.insert(0, obj
)
1282 self
.doChooseSelectTool()
1285 # =======================
1286 # == Selection Methods ==
1287 # =======================
1289 def selectAll(self
):
1290 """ Select every DrawingObject in our document.
1293 for obj
in self
.contents
:
1294 self
.selection
.append(obj
)
1295 self
.drawPanel
.Refresh()
1299 def deselectAll(self
):
1300 """ Deselect every DrawingObject in our document.
1303 self
.drawPanel
.Refresh()
1307 def select(self
, obj
):
1308 """ Select the given DrawingObject within our document.
1310 self
.selection
= [obj
]
1311 self
.drawPanel
.Refresh()
1315 def selectMany(self
, objs
):
1316 """ Select the given list of DrawingObjects.
1318 self
.selection
= objs
1319 self
.drawPanel
.Refresh()
1323 def selectByRectangle(self
, x
, y
, width
, height
):
1324 """ Select every DrawingObject in the given rectangular region.
1327 for obj
in self
.contents
:
1328 if obj
.objectWithinRect(x
, y
, width
, height
):
1329 self
.selection
.append(obj
)
1330 self
.drawPanel
.Refresh()
1333 # ======================
1334 # == File I/O Methods ==
1335 # ======================
1337 def loadContents(self
):
1338 """ Load the contents of our document into memory.
1340 f
= open(self
.fileName
, "rb")
1341 objData
= cPickle
.load(f
)
1344 for type, data
in objData
:
1345 obj
= DrawingObject(type)
1347 self
.contents
.append(obj
)
1351 self
.undoInfo
= None
1353 self
.drawPanel
.Refresh()
1357 def saveContents(self
):
1358 """ Save the contents of our document to disk.
1361 for obj
in self
.contents
:
1362 objData
.append([obj
.getType(), obj
.getData()])
1364 f
= open(self
.fileName
, "wb")
1365 cPickle
.dump(objData
, f
)
1371 def askIfUserWantsToSave(self
, action
):
1372 """ Give the user the opportunity to save the current document.
1374 'action' is a string describing the action about to be taken. If
1375 the user wants to save the document, it is saved immediately. If
1376 the user cancels, we return False.
1378 if not self
.dirty
: return True # Nothing to do.
1380 response
= wxMessageBox("Save changes before " + action
+ "?",
1381 "Confirm", wxYES_NO | wxCANCEL
, self
)
1383 if response
== wxYES
:
1384 if self
.fileName
== None:
1385 fileName
= wxFileSelector("Save File As", "Saving",
1386 default_extension
="psk",
1388 flags
= wxSAVE | wxOVERWRITE_PROMPT
)
1389 if fileName
== "": return False # User cancelled.
1390 self
.fileName
= fileName
1394 elif response
== wxNO
:
1395 return True # User doesn't want changes saved.
1396 elif response
== wxCANCEL
:
1397 return False # User cancelled.
1399 # =====================
1400 # == Private Methods ==
1401 # =====================
1403 def _adjustMenus(self
):
1404 """ Adjust our menus and toolbar to reflect the current state of the
1407 canSave
= (self
.fileName
!= None) and self
.dirty
1408 canRevert
= (self
.fileName
!= None) and self
.dirty
1409 canUndo
= self
.undoInfo
!= None
1410 selection
= len(self
.selection
) > 0
1411 onlyOne
= len(self
.selection
) == 1
1412 isText
= onlyOne
and (self
.selection
[0].getType() == obj_TEXT
)
1413 front
= onlyOne
and (self
.selection
[0] == self
.contents
[0])
1414 back
= onlyOne
and (self
.selection
[0] == self
.contents
[-1])
1416 # Enable/disable our menu items.
1418 self
.fileMenu
.Enable(wxID_SAVE
, canSave
)
1419 self
.fileMenu
.Enable(wxID_REVERT
, canRevert
)
1421 self
.editMenu
.Enable(menu_UNDO
, canUndo
)
1422 self
.editMenu
.Enable(menu_DUPLICATE
, selection
)
1423 self
.editMenu
.Enable(menu_EDIT_TEXT
, isText
)
1424 self
.editMenu
.Enable(menu_DELETE
, selection
)
1426 self
.toolsMenu
.Check(menu_SELECT
, self
.curTool
== self
.selectIcon
)
1427 self
.toolsMenu
.Check(menu_LINE
, self
.curTool
== self
.lineIcon
)
1428 self
.toolsMenu
.Check(menu_RECT
, self
.curTool
== self
.rectIcon
)
1429 self
.toolsMenu
.Check(menu_ELLIPSE
, self
.curTool
== self
.ellipseIcon
)
1430 self
.toolsMenu
.Check(menu_TEXT
, self
.curTool
== self
.textIcon
)
1432 self
.objectMenu
.Enable(menu_MOVE_FORWARD
, onlyOne
and not front
)
1433 self
.objectMenu
.Enable(menu_MOVE_TO_FRONT
, onlyOne
and not front
)
1434 self
.objectMenu
.Enable(menu_MOVE_BACKWARD
, onlyOne
and not back
)
1435 self
.objectMenu
.Enable(menu_MOVE_TO_BACK
, onlyOne
and not back
)
1437 # Enable/disable our toolbar icons.
1439 self
.toolbar
.EnableTool(wxID_NEW
, True)
1440 self
.toolbar
.EnableTool(wxID_OPEN
, True)
1441 self
.toolbar
.EnableTool(wxID_SAVE
, canSave
)
1442 self
.toolbar
.EnableTool(menu_UNDO
, canUndo
)
1443 self
.toolbar
.EnableTool(menu_DUPLICATE
, selection
)
1444 self
.toolbar
.EnableTool(menu_MOVE_FORWARD
, onlyOne
and not front
)
1445 self
.toolbar
.EnableTool(menu_MOVE_BACKWARD
, onlyOne
and not back
)
1448 def _setCurrentTool(self
, newToolIcon
):
1449 """ Set the currently selected tool.
1451 if self
.curTool
== newToolIcon
: return # Nothing to do.
1453 if self
.curTool
!= None:
1454 self
.curTool
.deselect()
1456 newToolIcon
.select()
1457 self
.curTool
= newToolIcon
1460 def _setPenColour(self
, colour
):
1461 """ Set the default or selected object's pen colour.
1463 if len(self
.selection
) > 0:
1464 self
._saveUndoInfo
()
1465 for obj
in self
.selection
:
1466 obj
.setPenColour(colour
)
1467 self
.drawPanel
.Refresh()
1469 self
.penColour
= colour
1470 self
.optionIndicator
.setPenColour(colour
)
1473 def _setFillColour(self
, colour
):
1474 """ Set the default or selected object's fill colour.
1476 if len(self
.selection
) > 0:
1477 self
._saveUndoInfo
()
1478 for obj
in self
.selection
:
1479 obj
.setFillColour(colour
)
1480 self
.drawPanel
.Refresh()
1482 self
.fillColour
= colour
1483 self
.optionIndicator
.setFillColour(colour
)
1486 def _setLineSize(self
, size
):
1487 """ Set the default or selected object's line size.
1489 if len(self
.selection
) > 0:
1490 self
._saveUndoInfo
()
1491 for obj
in self
.selection
:
1492 obj
.setLineSize(size
)
1493 self
.drawPanel
.Refresh()
1495 self
.lineSize
= size
1496 self
.optionIndicator
.setLineSize(size
)
1499 def _saveUndoInfo(self
):
1500 """ Remember the current state of the document, to allow for undo.
1502 We make a copy of the document's contents, so that we can return to
1503 the previous contents if the user does something and then wants to
1507 for obj
in self
.contents
:
1508 savedContents
.append([obj
.getType(), obj
.getData()])
1511 for i
in range(len(self
.contents
)):
1512 if self
.contents
[i
] in self
.selection
:
1513 savedSelection
.append(i
)
1515 self
.undoInfo
= {"contents" : savedContents
,
1516 "selection" : savedSelection
}
1519 def _resizeObject(self
, obj
, anchorPt
, oldPt
, newPt
):
1520 """ Resize the given object.
1522 'anchorPt' is the unchanging corner of the object, while the
1523 opposite corner has been resized. 'oldPt' are the current
1524 coordinates for this corner, while 'newPt' are the new coordinates.
1525 The object should fit within the given dimensions, though if the
1526 new point is less than the anchor point the object will need to be
1527 moved as well as resized, to avoid giving it a negative size.
1529 if obj
.getType() == obj_TEXT
:
1530 # Not allowed to resize text objects -- they're sized to fit text.
1534 self
._saveUndoInfo
()
1536 topLeft
= wxPoint(min(anchorPt
.x
, newPt
.x
),
1537 min(anchorPt
.y
, newPt
.y
))
1538 botRight
= wxPoint(max(anchorPt
.x
, newPt
.x
),
1539 max(anchorPt
.y
, newPt
.y
))
1541 newWidth
= botRight
.x
- topLeft
.x
1542 newHeight
= botRight
.y
- topLeft
.y
1544 if obj
.getType() == obj_LINE
:
1545 # Adjust the line so that its start and end points match the new
1546 # overall object size.
1548 startPt
= obj
.getStartPt()
1549 endPt
= obj
.getEndPt()
1551 slopesDown
= ((startPt
.x
< endPt
.x
) and (startPt
.y
< endPt
.y
)) or \
1552 ((startPt
.x
> endPt
.x
) and (startPt
.y
> endPt
.y
))
1554 # Handle the user flipping the line.
1556 hFlip
= ((anchorPt
.x
< oldPt
.x
) and (anchorPt
.x
> newPt
.x
)) or \
1557 ((anchorPt
.x
> oldPt
.x
) and (anchorPt
.x
< newPt
.x
))
1558 vFlip
= ((anchorPt
.y
< oldPt
.y
) and (anchorPt
.y
> newPt
.y
)) or \
1559 ((anchorPt
.y
> oldPt
.y
) and (anchorPt
.y
< newPt
.y
))
1561 if (hFlip
and not vFlip
) or (vFlip
and not hFlip
):
1562 slopesDown
= not slopesDown
# Line flipped.
1565 obj
.setStartPt(wxPoint(0, 0))
1566 obj
.setEndPt(wxPoint(newWidth
, newHeight
))
1568 obj
.setStartPt(wxPoint(0, newHeight
))
1569 obj
.setEndPt(wxPoint(newWidth
, 0))
1571 # Finally, adjust the bounds of the object to match the new dimensions.
1573 obj
.setPosition(topLeft
)
1574 obj
.setSize(wxSize(botRight
.x
- topLeft
.x
, botRight
.y
- topLeft
.y
))
1576 self
.drawPanel
.Refresh()
1579 def _moveObject(self
, offsetX
, offsetY
):
1580 """ Move the currently selected object(s) by the given offset.
1582 self
._saveUndoInfo
()
1584 for obj
in self
.selection
:
1585 pos
= obj
.getPosition()
1586 pos
.x
= pos
.x
+ offsetX
1587 pos
.y
= pos
.y
+ offsetY
1588 obj
.setPosition(pos
)
1590 self
.drawPanel
.Refresh()
1593 def _buildLineSizePopup(self
, lineSize
):
1594 """ Build the pop-up menu used to set the line size.
1596 'lineSize' is the current line size value. The corresponding item
1597 is checked in the pop-up menu.
1600 menu
.Append(id_LINESIZE_0
, "no line", kind
=wxITEM_CHECK
)
1601 menu
.Append(id_LINESIZE_1
, "1-pixel line", kind
=wxITEM_CHECK
)
1602 menu
.Append(id_LINESIZE_2
, "2-pixel line", kind
=wxITEM_CHECK
)
1603 menu
.Append(id_LINESIZE_3
, "3-pixel line", kind
=wxITEM_CHECK
)
1604 menu
.Append(id_LINESIZE_4
, "4-pixel line", kind
=wxITEM_CHECK
)
1605 menu
.Append(id_LINESIZE_5
, "5-pixel line", kind
=wxITEM_CHECK
)
1607 if lineSize
== 0: menu
.Check(id_LINESIZE_0
, True)
1608 elif lineSize
== 1: menu
.Check(id_LINESIZE_1
, True)
1609 elif lineSize
== 2: menu
.Check(id_LINESIZE_2
, True)
1610 elif lineSize
== 3: menu
.Check(id_LINESIZE_3
, True)
1611 elif lineSize
== 4: menu
.Check(id_LINESIZE_4
, True)
1612 elif lineSize
== 5: menu
.Check(id_LINESIZE_5
, True)
1614 EVT_MENU(self
, id_LINESIZE_0
, self
._lineSizePopupSelected
)
1615 EVT_MENU(self
, id_LINESIZE_1
, self
._lineSizePopupSelected
)
1616 EVT_MENU(self
, id_LINESIZE_2
, self
._lineSizePopupSelected
)
1617 EVT_MENU(self
, id_LINESIZE_3
, self
._lineSizePopupSelected
)
1618 EVT_MENU(self
, id_LINESIZE_4
, self
._lineSizePopupSelected
)
1619 EVT_MENU(self
, id_LINESIZE_5
, self
._lineSizePopupSelected
)
1624 def _lineSizePopupSelected(self
, event
):
1625 """ Respond to the user selecting an item from the line size popup menu
1628 if id == id_LINESIZE_0
: self
._setLineSize
(0)
1629 elif id == id_LINESIZE_1
: self
._setLineSize
(1)
1630 elif id == id_LINESIZE_2
: self
._setLineSize
(2)
1631 elif id == id_LINESIZE_3
: self
._setLineSize
(3)
1632 elif id == id_LINESIZE_4
: self
._setLineSize
(4)
1633 elif id == id_LINESIZE_5
: self
._setLineSize
(5)
1638 self
.optionIndicator
.setLineSize(self
.lineSize
)
1641 def _getEventCoordinates(self
, event
):
1642 """ Return the coordinates associated with the given mouse event.
1644 The coordinates have to be adjusted to allow for the current scroll
1647 originX
, originY
= self
.drawPanel
.GetViewStart()
1648 unitX
, unitY
= self
.drawPanel
.GetScrollPixelsPerUnit()
1649 return wxPoint(event
.GetX() + (originX
* unitX
),
1650 event
.GetY() + (originY
* unitY
))
1653 def _getObjectAndSelectionHandleAt(self
, pt
):
1654 """ Return the object and selection handle at the given point.
1656 We draw selection handles (small rectangles) around the currently
1657 selected object(s). If the given point is within one of the
1658 selection handle rectangles, we return the associated object and a
1659 code indicating which selection handle the point is in. If the
1660 point isn't within any selection handle at all, we return the tuple
1661 (None, handle_NONE).
1663 for obj
in self
.selection
:
1664 handle
= obj
.getSelectionHandleContainingPoint(pt
.x
, pt
.y
)
1665 if handle
!= handle_NONE
:
1668 return None, handle_NONE
1671 def _getObjectAt(self
, pt
):
1672 """ Return the first object found which is at the given point.
1674 for obj
in self
.contents
:
1675 if obj
.objectContainsPoint(pt
.x
, pt
.y
):
1680 def _drawObjectOutline(self
, offsetX
, offsetY
):
1681 """ Draw an outline of the currently selected object.
1683 The selected object's outline is drawn at the object's position
1684 plus the given offset.
1686 Note that the outline is drawn by *inverting* the window's
1687 contents, so calling _drawObjectOutline twice in succession will
1688 restore the window's contents back to what they were previously.
1690 if len(self
.selection
) != 1: return
1692 position
= self
.selection
[0].getPosition()
1693 size
= self
.selection
[0].getSize()
1695 dc
= wxClientDC(self
.drawPanel
)
1696 self
.drawPanel
.PrepareDC(dc
)
1698 dc
.SetPen(wxBLACK_DASHED_PEN
)
1699 dc
.SetBrush(wxTRANSPARENT_BRUSH
)
1700 dc
.SetLogicalFunction(wxINVERT
)
1702 dc
.DrawRectangle(position
.x
+ offsetX
, position
.y
+ offsetY
,
1703 size
.width
, size
.height
)
1708 def _drawVisualFeedback(self
, startPt
, endPt
, type, dashedLine
):
1709 """ Draw visual feedback for a drawing operation.
1711 The visual feedback consists of a line, ellipse, or rectangle based
1712 around the two given points. 'type' should be one of the following
1713 predefined feedback type constants:
1715 feedback_RECT -> draw rectangular feedback.
1716 feedback_LINE -> draw line feedback.
1717 feedback_ELLIPSE -> draw elliptical feedback.
1719 if 'dashedLine' is True, the feedback is drawn as a dashed rather
1722 Note that the feedback is drawn by *inverting* the window's
1723 contents, so calling _drawVisualFeedback twice in succession will
1724 restore the window's contents back to what they were previously.
1726 dc
= wxClientDC(self
.drawPanel
)
1727 self
.drawPanel
.PrepareDC(dc
)
1730 dc
.SetPen(wxBLACK_DASHED_PEN
)
1732 dc
.SetPen(wxBLACK_PEN
)
1733 dc
.SetBrush(wxTRANSPARENT_BRUSH
)
1734 dc
.SetLogicalFunction(wxINVERT
)
1736 if type == feedback_RECT
:
1737 dc
.DrawRectangle(startPt
.x
, startPt
.y
,
1738 endPt
.x
- startPt
.x
,
1739 endPt
.y
- startPt
.y
)
1740 elif type == feedback_LINE
:
1741 dc
.DrawLine(startPt
.x
, startPt
.y
, endPt
.x
, endPt
.y
)
1742 elif type == feedback_ELLIPSE
:
1743 dc
.DrawEllipse(startPt
.x
, startPt
.y
,
1744 endPt
.x
- startPt
.x
,
1745 endPt
.y
- startPt
.y
)
1749 #----------------------------------------------------------------------------
1751 class DrawingObject
:
1752 """ An object within the drawing panel.
1754 A pySketch document consists of a front-to-back ordered list of
1755 DrawingObjects. Each DrawingObject has the following properties:
1757 'type' What type of object this is (text, line, etc).
1758 'position' The position of the object within the document.
1759 'size' The size of the object within the document.
1760 'penColour' The colour to use for drawing the object's outline.
1761 'fillColour' Colour to use for drawing object's interior.
1762 'lineSize' Line width (in pixels) to use for object's outline.
1763 'startPt' The point, relative to the object's position, where
1764 an obj_LINE object's line should start.
1765 'endPt' The point, relative to the object's position, where
1766 an obj_LINE object's line should end.
1767 'text' The object's text (obj_TEXT objects only).
1768 'textFont' The text object's font name.
1769 'textSize' The text object's point size.
1770 'textBoldface' If True, this text object will be drawn in
1772 'textItalic' If True, this text object will be drawn in italic.
1773 'textUnderline' If True, this text object will be drawn underlined.
1776 # ==================
1777 # == Constructors ==
1778 # ==================
1780 def __init__(self
, type, position
=wxPoint(0, 0), size
=wxSize(0, 0),
1781 penColour
=wxBLACK
, fillColour
=wxWHITE
, lineSize
=1,
1782 text
=None, startPt
=wxPoint(0, 0), endPt
=wxPoint(0,0)):
1783 """ Standard constructor.
1785 'type' is the type of object being created. This should be one of
1786 the following constants:
1793 The remaining parameters let you set various options for the newly
1794 created DrawingObject.
1797 self
.position
= position
1799 self
.penColour
= penColour
1800 self
.fillColour
= fillColour
1801 self
.lineSize
= lineSize
1802 self
.startPt
= startPt
1805 self
.textFont
= wxSystemSettings_GetSystemFont(
1806 wxSYS_DEFAULT_GUI_FONT
).GetFaceName()
1808 self
.textBoldface
= False
1809 self
.textItalic
= False
1810 self
.textUnderline
= False
1812 # =============================
1813 # == Object Property Methods ==
1814 # =============================
1817 """ Return a copy of the object's internal data.
1819 This is used to save this DrawingObject to disk.
1821 return [self
.type, self
.position
.x
, self
.position
.y
,
1822 self
.size
.width
, self
.size
.height
,
1823 self
.penColour
.Red(),
1824 self
.penColour
.Green(),
1825 self
.penColour
.Blue(),
1826 self
.fillColour
.Red(),
1827 self
.fillColour
.Green(),
1828 self
.fillColour
.Blue(),
1830 self
.startPt
.x
, self
.startPt
.y
,
1831 self
.endPt
.x
, self
.endPt
.y
,
1840 def setData(self
, data
):
1841 """ Set the object's internal data.
1843 'data' is a copy of the object's saved data, as returned by
1844 getData() above. This is used to restore a previously saved
1847 #data = copy.deepcopy(data) # Needed?
1850 self
.position
= wxPoint(data
[1], data
[2])
1851 self
.size
= wxSize(data
[3], data
[4])
1852 self
.penColour
= wxColour(red
=data
[5],
1855 self
.fillColour
= wxColour(red
=data
[8],
1858 self
.lineSize
= data
[11]
1859 self
.startPt
= wxPoint(data
[12], data
[13])
1860 self
.endPt
= wxPoint(data
[14], data
[15])
1861 self
.text
= data
[16]
1862 self
.textFont
= data
[17]
1863 self
.textSize
= data
[18]
1864 self
.textBoldface
= data
[19]
1865 self
.textItalic
= data
[20]
1866 self
.textUnderline
= data
[21]
1870 """ Return this DrawingObject's type.
1875 def setPosition(self
, position
):
1876 """ Set the origin (top-left corner) for this DrawingObject.
1878 self
.position
= position
1881 def getPosition(self
):
1882 """ Return this DrawingObject's position.
1884 return self
.position
1887 def setSize(self
, size
):
1888 """ Set the size for this DrawingObject.
1894 """ Return this DrawingObject's size.
1899 def setPenColour(self
, colour
):
1900 """ Set the pen colour used for this DrawingObject.
1902 self
.penColour
= colour
1905 def getPenColour(self
):
1906 """ Return this DrawingObject's pen colour.
1908 return self
.penColour
1911 def setFillColour(self
, colour
):
1912 """ Set the fill colour used for this DrawingObject.
1914 self
.fillColour
= colour
1917 def getFillColour(self
):
1918 """ Return this DrawingObject's fill colour.
1920 return self
.fillColour
1923 def setLineSize(self
, lineSize
):
1924 """ Set the linesize used for this DrawingObject.
1926 self
.lineSize
= lineSize
1929 def getLineSize(self
):
1930 """ Return this DrawingObject's line size.
1932 return self
.lineSize
1935 def setStartPt(self
, startPt
):
1936 """ Set the starting point for this line DrawingObject.
1938 self
.startPt
= startPt
1941 def getStartPt(self
):
1942 """ Return the starting point for this line DrawingObject.
1947 def setEndPt(self
, endPt
):
1948 """ Set the ending point for this line DrawingObject.
1954 """ Return the ending point for this line DrawingObject.
1959 def setText(self
, text
):
1960 """ Set the text for this DrawingObject.
1966 """ Return this DrawingObject's text.
1971 def setTextFont(self
, font
):
1972 """ Set the typeface for this text DrawingObject.
1974 self
.textFont
= font
1977 def getTextFont(self
):
1978 """ Return this text DrawingObject's typeface.
1980 return self
.textFont
1983 def setTextSize(self
, size
):
1984 """ Set the point size for this text DrawingObject.
1986 self
.textSize
= size
1989 def getTextSize(self
):
1990 """ Return this text DrawingObject's text size.
1992 return self
.textSize
1995 def setTextBoldface(self
, boldface
):
1996 """ Set the boldface flag for this text DrawingObject.
1998 self
.textBoldface
= boldface
2001 def getTextBoldface(self
):
2002 """ Return this text DrawingObject's boldface flag.
2004 return self
.textBoldface
2007 def setTextItalic(self
, italic
):
2008 """ Set the italic flag for this text DrawingObject.
2010 self
.textItalic
= italic
2013 def getTextItalic(self
):
2014 """ Return this text DrawingObject's italic flag.
2016 return self
.textItalic
2019 def setTextUnderline(self
, underline
):
2020 """ Set the underling flag for this text DrawingObject.
2022 self
.textUnderline
= underline
2025 def getTextUnderline(self
):
2026 """ Return this text DrawingObject's underline flag.
2028 return self
.textUnderline
2030 # ============================
2031 # == Object Drawing Methods ==
2032 # ============================
2034 def draw(self
, dc
, selected
):
2035 """ Draw this DrawingObject into our window.
2037 'dc' is the device context to use for drawing. If 'selected' is
2038 True, the object is currently selected and should be drawn as such.
2040 if self
.type != obj_TEXT
:
2041 if self
.lineSize
== 0:
2042 dc
.SetPen(wxPen(self
.penColour
, self
.lineSize
, wxTRANSPARENT
))
2044 dc
.SetPen(wxPen(self
.penColour
, self
.lineSize
, wxSOLID
))
2045 dc
.SetBrush(wxBrush(self
.fillColour
, wxSOLID
))
2047 dc
.SetTextForeground(self
.penColour
)
2048 dc
.SetTextBackground(self
.fillColour
)
2050 self
._privateDraw
(dc
, self
.position
, selected
)
2052 # =======================
2053 # == Selection Methods ==
2054 # =======================
2056 def objectContainsPoint(self
, x
, y
):
2057 """ Returns True iff this object contains the given point.
2059 This is used to determine if the user clicked on the object.
2061 # Firstly, ignore any points outside of the object's bounds.
2063 if x
< self
.position
.x
: return False
2064 if x
> self
.position
.x
+ self
.size
.x
: return False
2065 if y
< self
.position
.y
: return False
2066 if y
> self
.position
.y
+ self
.size
.y
: return False
2068 if self
.type in [obj_RECT
, obj_TEXT
]:
2069 # Rectangles and text are easy -- they're always selected if the
2070 # point is within their bounds.
2073 # Now things get tricky. There's no straightforward way of knowing
2074 # whether the point is within the object's bounds...to get around this,
2075 # we draw the object into a memory-based bitmap and see if the given
2076 # point was drawn. This could no doubt be done more efficiently by
2077 # some tricky maths, but this approach works and is simple enough.
2079 bitmap
= wxEmptyBitmap(self
.size
.x
+ 10, self
.size
.y
+ 10)
2081 dc
.SelectObject(bitmap
)
2083 dc
.SetBackground(wxWHITE_BRUSH
)
2085 dc
.SetPen(wxPen(wxBLACK
, self
.lineSize
+ 5, wxSOLID
))
2086 dc
.SetBrush(wxBLACK_BRUSH
)
2087 self
._privateDraw
(dc
, wxPoint(5, 5), True)
2089 pixel
= dc
.GetPixel(x
- self
.position
.x
+ 5, y
- self
.position
.y
+ 5)
2090 if (pixel
.Red() == 0) and (pixel
.Green() == 0) and (pixel
.Blue() == 0):
2096 def getSelectionHandleContainingPoint(self
, x
, y
):
2097 """ Return the selection handle containing the given point, if any.
2099 We return one of the predefined selection handle ID codes.
2101 if self
.type == obj_LINE
:
2102 # We have selection handles at the start and end points.
2103 if self
._pointInSelRect
(x
, y
, self
.position
.x
+ self
.startPt
.x
,
2104 self
.position
.y
+ self
.startPt
.y
):
2105 return handle_START_POINT
2106 elif self
._pointInSelRect
(x
, y
, self
.position
.x
+ self
.endPt
.x
,
2107 self
.position
.y
+ self
.endPt
.y
):
2108 return handle_END_POINT
2112 # We have selection handles at all four corners.
2113 if self
._pointInSelRect
(x
, y
, self
.position
.x
, self
.position
.y
):
2114 return handle_TOP_LEFT
2115 elif self
._pointInSelRect
(x
, y
, self
.position
.x
+ self
.size
.width
,
2117 return handle_TOP_RIGHT
2118 elif self
._pointInSelRect
(x
, y
, self
.position
.x
,
2119 self
.position
.y
+ self
.size
.height
):
2120 return handle_BOTTOM_LEFT
2121 elif self
._pointInSelRect
(x
, y
, self
.position
.x
+ self
.size
.width
,
2122 self
.position
.y
+ self
.size
.height
):
2123 return handle_BOTTOM_RIGHT
2128 def objectWithinRect(self
, x
, y
, width
, height
):
2129 """ Return True iff this object falls completely within the given rect.
2131 if x
> self
.position
.x
: return False
2132 if x
+ width
< self
.position
.x
+ self
.size
.width
: return False
2133 if y
> self
.position
.y
: return False
2134 if y
+ height
< self
.position
.y
+ self
.size
.height
: return False
2137 # =====================
2138 # == Utility Methods ==
2139 # =====================
2141 def fitToText(self
):
2142 """ Resize a text DrawingObject so that it fits it's text exactly.
2144 if self
.type != obj_TEXT
: return
2146 if self
.textBoldface
: weight
= wxBOLD
2147 else: weight
= wxNORMAL
2148 if self
.textItalic
: style
= wxITALIC
2149 else: style
= wxNORMAL
2150 font
= wxFont(self
.textSize
, wxDEFAULT
, style
, weight
,
2151 self
.textUnderline
, self
.textFont
)
2153 dummyWindow
= wxFrame(None, -1, "")
2154 dummyWindow
.SetFont(font
)
2155 width
, height
= dummyWindow
.GetTextExtent(self
.text
)
2156 dummyWindow
.Destroy()
2158 self
.size
= wxSize(width
, height
)
2160 # =====================
2161 # == Private Methods ==
2162 # =====================
2164 def _privateDraw(self
, dc
, position
, selected
):
2165 """ Private routine to draw this DrawingObject.
2167 'dc' is the device context to use for drawing, while 'position' is
2168 the position in which to draw the object. If 'selected' is True,
2169 the object is drawn with selection handles. This private drawing
2170 routine assumes that the pen and brush have already been set by the
2173 if self
.type == obj_LINE
:
2174 dc
.DrawLine(position
.x
+ self
.startPt
.x
,
2175 position
.y
+ self
.startPt
.y
,
2176 position
.x
+ self
.endPt
.x
,
2177 position
.y
+ self
.endPt
.y
)
2178 elif self
.type == obj_RECT
:
2179 dc
.DrawRectangle(position
.x
, position
.y
,
2180 self
.size
.width
, self
.size
.height
)
2181 elif self
.type == obj_ELLIPSE
:
2182 dc
.DrawEllipse(position
.x
, position
.y
,
2183 self
.size
.width
, self
.size
.height
)
2184 elif self
.type == obj_TEXT
:
2185 if self
.textBoldface
: weight
= wxBOLD
2186 else: weight
= wxNORMAL
2187 if self
.textItalic
: style
= wxITALIC
2188 else: style
= wxNORMAL
2189 font
= wxFont(self
.textSize
, wxDEFAULT
, style
, weight
,
2190 self
.textUnderline
, self
.textFont
)
2192 dc
.DrawText(self
.text
, position
.x
, position
.y
)
2195 dc
.SetPen(wxTRANSPARENT_PEN
)
2196 dc
.SetBrush(wxBLACK_BRUSH
)
2198 if self
.type == obj_LINE
:
2199 # Draw selection handles at the start and end points.
2200 self
._drawSelHandle
(dc
, position
.x
+ self
.startPt
.x
,
2201 position
.y
+ self
.startPt
.y
)
2202 self
._drawSelHandle
(dc
, position
.x
+ self
.endPt
.x
,
2203 position
.y
+ self
.endPt
.y
)
2205 # Draw selection handles at all four corners.
2206 self
._drawSelHandle
(dc
, position
.x
, position
.y
)
2207 self
._drawSelHandle
(dc
, position
.x
+ self
.size
.width
,
2209 self
._drawSelHandle
(dc
, position
.x
,
2210 position
.y
+ self
.size
.height
)
2211 self
._drawSelHandle
(dc
, position
.x
+ self
.size
.width
,
2212 position
.y
+ self
.size
.height
)
2215 def _drawSelHandle(self
, dc
, x
, y
):
2216 """ Draw a selection handle around this DrawingObject.
2218 'dc' is the device context to draw the selection handle within,
2219 while 'x' and 'y' are the coordinates to use for the centre of the
2222 dc
.DrawRectangle(x
- 3, y
- 3, 6, 6)
2225 def _pointInSelRect(self
, x
, y
, rX
, rY
):
2226 """ Return True iff (x, y) is within the selection handle at (rX, ry).
2228 if x
< rX
- 3: return False
2229 elif x
> rX
+ 3: return False
2230 elif y
< rY
- 3: return False
2231 elif y
> rY
+ 3: return False
2234 #----------------------------------------------------------------------------
2236 class ToolPaletteIcon(wxBitmapButton
):
2237 """ An icon appearing in the tool palette area of our sketching window.
2239 Note that this is actually implemented as a wxBitmap rather
2240 than as a wxIcon. wxIcon has a very specific meaning, and isn't
2241 appropriate for this more general use.
2244 def __init__(self
, parent
, iconID
, iconName
, toolTip
):
2245 """ Standard constructor.
2247 'parent' is the parent window this icon will be part of.
2248 'iconID' is the internal ID used for this icon.
2249 'iconName' is the name used for this icon.
2250 'toolTip' is the tool tip text to show for this icon.
2252 The icon name is used to get the appropriate bitmap for this icon.
2254 bmp
= wxBitmap("images/" + iconName
+ "Icon.bmp", wxBITMAP_TYPE_BMP
)
2255 wxBitmapButton
.__init
__(self
, parent
, iconID
, bmp
, wxDefaultPosition
,
2256 wxSize(bmp
.GetWidth(), bmp
.GetHeight()))
2257 self
.SetToolTip(wxToolTip(toolTip
))
2259 self
.iconID
= iconID
2260 self
.iconName
= iconName
2261 self
.isSelected
= False
2265 """ Select the icon.
2267 The icon's visual representation is updated appropriately.
2269 if self
.isSelected
: return # Nothing to do!
2271 bmp
= wxBitmap("images/" + self
.iconName
+ "IconSel.bmp",
2273 self
.SetBitmapLabel(bmp
)
2274 self
.isSelected
= True
2278 """ Deselect the icon.
2280 The icon's visual representation is updated appropriately.
2282 if not self
.isSelected
: return # Nothing to do!
2284 bmp
= wxBitmap("images/" + self
.iconName
+ "Icon.bmp",
2286 self
.SetBitmapLabel(bmp
)
2287 self
.isSelected
= False
2289 #----------------------------------------------------------------------------
2291 class ToolOptionIndicator(wxWindow
):
2292 """ A visual indicator which shows the current tool options.
2294 def __init__(self
, parent
):
2295 """ Standard constructor.
2297 wxWindow
.__init
__(self
, parent
, -1, wxDefaultPosition
, wxSize(52, 32))
2299 self
.penColour
= wxBLACK
2300 self
.fillColour
= wxWHITE
2303 EVT_PAINT(self
, self
.OnPaint
)
2306 def setPenColour(self
, penColour
):
2307 """ Set the indicator's current pen colour.
2309 self
.penColour
= penColour
2313 def setFillColour(self
, fillColour
):
2314 """ Set the indicator's current fill colour.
2316 self
.fillColour
= fillColour
2320 def setLineSize(self
, lineSize
):
2321 """ Set the indicator's current pen colour.
2323 self
.lineSize
= lineSize
2327 def OnPaint(self
, event
):
2328 """ Paint our tool option indicator.
2330 dc
= wxPaintDC(self
)
2333 if self
.lineSize
== 0:
2334 dc
.SetPen(wxPen(self
.penColour
, self
.lineSize
, wxTRANSPARENT
))
2336 dc
.SetPen(wxPen(self
.penColour
, self
.lineSize
, wxSOLID
))
2337 dc
.SetBrush(wxBrush(self
.fillColour
, wxSOLID
))
2339 dc
.DrawRectangle(5, 5, self
.GetSize().width
- 10,
2340 self
.GetSize().height
- 10)
2344 #----------------------------------------------------------------------------
2346 class EditTextObjectDialog(wxDialog
):
2347 """ Dialog box used to edit the properties of a text object.
2349 The user can edit the object's text, font, size, and text style.
2352 def __init__(self
, parent
, title
):
2353 """ Standard constructor.
2355 wxDialog
.__init
__(self
, parent
, -1, title
)
2357 self
.textCtrl
= wxTextCtrl(self
, 1001, "", style
=wxTE_PROCESS_ENTER
,
2358 validator
=TextObjectValidator())
2359 extent
= self
.textCtrl
.GetFullTextExtent("Hy")
2360 lineHeight
= extent
[1] + extent
[3]
2361 self
.textCtrl
.SetSize(wxSize(-1, lineHeight
* 4))
2363 EVT_TEXT_ENTER(self
, 1001, self
._doEnter
)
2365 fonts
= wxFontEnumerator()
2366 fonts
.EnumerateFacenames()
2367 self
.fontList
= fonts
.GetFacenames()
2368 self
.fontList
.sort()
2370 fontLabel
= wxStaticText(self
, -1, "Font:")
2371 self
._setFontOptions
(fontLabel
, weight
=wxBOLD
)
2373 self
.fontCombo
= wxComboBox(self
, -1, "", wxDefaultPosition
,
2374 wxDefaultSize
, self
.fontList
,
2375 style
= wxCB_READONLY
)
2376 self
.fontCombo
.SetSelection(0) # Default to first available font.
2378 self
.sizeList
= ["8", "9", "10", "12", "14", "16",
2379 "18", "20", "24", "32", "48", "72"]
2381 sizeLabel
= wxStaticText(self
, -1, "Size:")
2382 self
._setFontOptions
(sizeLabel
, weight
=wxBOLD
)
2384 self
.sizeCombo
= wxComboBox(self
, -1, "", wxDefaultPosition
,
2385 wxDefaultSize
, self
.sizeList
,
2386 style
=wxCB_READONLY
)
2387 self
.sizeCombo
.SetSelection(3) # Default to 12 point text.
2389 gap
= wxLEFT | wxTOP | wxRIGHT
2391 comboSizer
= wxBoxSizer(wxHORIZONTAL
)
2392 comboSizer
.Add(fontLabel
, 0, gap | wxALIGN_CENTRE_VERTICAL
, 5)
2393 comboSizer
.Add(self
.fontCombo
, 0, gap
, 5)
2394 comboSizer
.Add((5, 5)) # Spacer.
2395 comboSizer
.Add(sizeLabel
, 0, gap | wxALIGN_CENTRE_VERTICAL
, 5)
2396 comboSizer
.Add(self
.sizeCombo
, 0, gap
, 5)
2398 self
.boldCheckbox
= wxCheckBox(self
, -1, "Bold")
2399 self
.italicCheckbox
= wxCheckBox(self
, -1, "Italic")
2400 self
.underlineCheckbox
= wxCheckBox(self
, -1, "Underline")
2402 self
._setFontOptions
(self
.boldCheckbox
, weight
=wxBOLD
)
2403 self
._setFontOptions
(self
.italicCheckbox
, style
=wxITALIC
)
2404 self
._setFontOptions
(self
.underlineCheckbox
, underline
=True)
2406 styleSizer
= wxBoxSizer(wxHORIZONTAL
)
2407 styleSizer
.Add(self
.boldCheckbox
, 0, gap
, 5)
2408 styleSizer
.Add(self
.italicCheckbox
, 0, gap
, 5)
2409 styleSizer
.Add(self
.underlineCheckbox
, 0, gap
, 5)
2411 self
.okButton
= wxButton(self
, wxID_OK
, "OK")
2412 self
.cancelButton
= wxButton(self
, wxID_CANCEL
, "Cancel")
2414 btnSizer
= wxBoxSizer(wxHORIZONTAL
)
2415 btnSizer
.Add(self
.okButton
, 0, gap
, 5)
2416 btnSizer
.Add(self
.cancelButton
, 0, gap
, 5)
2418 sizer
= wxBoxSizer(wxVERTICAL
)
2419 sizer
.Add(self
.textCtrl
, 1, gap | wxEXPAND
, 5)
2420 sizer
.Add((10, 10)) # Spacer.
2421 sizer
.Add(comboSizer
, 0, gap | wxALIGN_CENTRE
, 5)
2422 sizer
.Add(styleSizer
, 0, gap | wxALIGN_CENTRE
, 5)
2423 sizer
.Add((10, 10)) # Spacer.
2424 sizer
.Add(btnSizer
, 0, gap | wxALIGN_CENTRE
, 5)
2426 self
.SetAutoLayout(True)
2427 self
.SetSizer(sizer
)
2430 self
.textCtrl
.SetFocus()
2433 def objectToDialog(self
, obj
):
2434 """ Copy the properties of the given text object into the dialog box.
2436 self
.textCtrl
.SetValue(obj
.getText())
2437 self
.textCtrl
.SetSelection(0, len(obj
.getText()))
2439 for i
in range(len(self
.fontList
)):
2440 if self
.fontList
[i
] == obj
.getTextFont():
2441 self
.fontCombo
.SetSelection(i
)
2444 for i
in range(len(self
.sizeList
)):
2445 if self
.sizeList
[i
] == str(obj
.getTextSize()):
2446 self
.sizeCombo
.SetSelection(i
)
2449 self
.boldCheckbox
.SetValue(obj
.getTextBoldface())
2450 self
.italicCheckbox
.SetValue(obj
.getTextItalic())
2451 self
.underlineCheckbox
.SetValue(obj
.getTextUnderline())
2454 def dialogToObject(self
, obj
):
2455 """ Copy the properties from the dialog box into the given text object.
2457 obj
.setText(self
.textCtrl
.GetValue())
2458 obj
.setTextFont(self
.fontCombo
.GetValue())
2459 obj
.setTextSize(int(self
.sizeCombo
.GetValue()))
2460 obj
.setTextBoldface(self
.boldCheckbox
.GetValue())
2461 obj
.setTextItalic(self
.italicCheckbox
.GetValue())
2462 obj
.setTextUnderline(self
.underlineCheckbox
.GetValue())
2465 # ======================
2466 # == Private Routines ==
2467 # ======================
2469 def _setFontOptions(self
, ctrl
, family
=None, pointSize
=-1,
2470 style
=wxNORMAL
, weight
=wxNORMAL
,
2472 """ Change the font settings for the given control.
2474 The meaning of the 'family', 'pointSize', 'style', 'weight' and
2475 'underline' parameters are the same as for the wxFont constructor.
2476 If the family and/or pointSize isn't specified, the current default
2479 if family
== None: family
= ctrl
.GetFont().GetFamily()
2480 if pointSize
== -1: pointSize
= ctrl
.GetFont().GetPointSize()
2482 ctrl
.SetFont(wxFont(pointSize
, family
, style
, weight
, underline
))
2483 ctrl
.SetSize(ctrl
.GetBestSize()) # Adjust size to reflect font change.
2486 def _doEnter(self
, event
):
2487 """ Respond to the user hitting the ENTER key.
2489 We simulate clicking on the "OK" button.
2491 if self
.Validate(): self
.Show(False)
2493 #----------------------------------------------------------------------------
2495 class TextObjectValidator(wxPyValidator
):
2496 """ This validator is used to ensure that the user has entered something
2497 into the text object editor dialog's text field.
2500 """ Standard constructor.
2502 wxPyValidator
.__init
__(self
)
2506 """ Standard cloner.
2508 Note that every validator must implement the Clone() method.
2510 return TextObjectValidator()
2513 def Validate(self
, win
):
2514 """ Validate the contents of the given text control.
2516 textCtrl
= self
.GetWindow()
2517 text
= textCtrl
.GetValue()
2520 wxMessageBox("A text object must contain some text!", "Error")
2526 def TransferToWindow(self
):
2527 """ Transfer data from validator to window.
2529 The default implementation returns False, indicating that an error
2530 occurred. We simply return True, as we don't do any data transfer.
2532 return True # Prevent wxDialog from complaining.
2535 def TransferFromWindow(self
):
2536 """ Transfer data from window to validator.
2538 The default implementation returns False, indicating that an error
2539 occurred. We simply return True, as we don't do any data transfer.
2541 return True # Prevent wxDialog from complaining.
2543 #----------------------------------------------------------------------------
2545 class ExceptionHandler
:
2546 """ A simple error-handling class to write exceptions to a text file.
2548 Under MS Windows, the standard DOS console window doesn't scroll and
2549 closes as soon as the application exits, making it hard to find and
2550 view Python exceptions. This utility class allows you to handle Python
2551 exceptions in a more friendly manner.
2555 """ Standard constructor.
2558 if os
.path
.exists("errors.txt"):
2559 os
.remove("errors.txt") # Delete previous error log, if any.
2563 """ Write the given error message to a text file.
2565 Note that if the error message doesn't end in a carriage return, we
2566 have to buffer up the inputs until a carriage return is received.
2568 if (s
[-1] != "\n") and (s
[-1] != "\r"):
2569 self
._buff
= self
._buff
+ s
2576 if s
[:9] == "Traceback":
2577 # Tell the user than an exception occurred.
2578 wxMessageBox("An internal error has occurred.\nPlease " + \
2579 "refer to the 'errors.txt' file for details.",
2580 "Error", wxOK | wxCENTRE | wxICON_EXCLAMATION
)
2582 f
= open("errors.txt", "a")
2586 pass # Don't recursively crash on errors.
2588 #----------------------------------------------------------------------------
2590 class SketchApp(wxApp
):
2591 """ The main pySketch application object.
2594 """ Initialise the application.
2596 wxInitAllImageHandlers()
2601 if len(sys
.argv
) == 1:
2602 # No file name was specified on the command line -> start with a
2604 frame
= DrawingFrame(None, -1, "Untitled")
2607 _docList
.append(frame
)
2609 # Load the file(s) specified on the command line.
2610 for arg
in sys
.argv
[1:]:
2611 fileName
= os
.path
.join(os
.getcwd(), arg
)
2612 if os
.path
.isfile(fileName
):
2613 frame
= DrawingFrame(None, -1,
2614 os
.path
.basename(fileName
),
2617 _docList
.append(frame
)
2621 #----------------------------------------------------------------------------
2624 """ Start up the pySketch application.
2628 # Redirect python exceptions to a log file.
2630 sys
.stderr
= ExceptionHandler()
2632 # Create and start the pySketch application.
2638 if __name__
== "__main__":