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.
45 import string
, cPickle
, os
.path
46 from wxPython
.wx
import *
48 import traceback
, types
50 #----------------------------------------------------------------------------
52 #----------------------------------------------------------------------------
56 menu_UNDO
= 10001 # Edit menu items.
57 menu_SELECT_ALL
= 10002
58 menu_DUPLICATE
= 10003
59 menu_EDIT_TEXT
= 10004
62 menu_SELECT
= 10101 # Tools menu items.
68 menu_MOVE_FORWARD
= 10201 # Object menu items.
69 menu_MOVE_TO_FRONT
= 10202
70 menu_MOVE_BACKWARD
= 10203
71 menu_MOVE_TO_BACK
= 10204
73 menu_ABOUT
= 10205 # Help menu items.
83 # Our tool option IDs:
96 # DrawObject type IDs:
103 # Selection handle IDs:
108 handle_BOTTOM_LEFT
= 4
109 handle_BOTTOM_RIGHT
= 5
110 handle_START_POINT
= 6
113 # Dragging operations:
120 # Visual Feedback types:
126 # Mouse-event action parameter types:
131 # Size of the drawing page, in pixels.
136 #----------------------------------------------------------------------------
138 class DrawingFrame(wxFrame
):
139 """ A frame showing the contents of a single document. """
141 # ==========================================
142 # == Initialisation and Window Management ==
143 # ==========================================
145 def __init__(self
, parent
, id, title
, fileName
=None):
146 """ Standard constructor.
148 'parent', 'id' and 'title' are all passed to the standard wxFrame
149 constructor. 'fileName' is the name and path of a saved file to
150 load into this frame, if any.
152 wxFrame
.__init
__(self
, parent
, id, title
,
153 style
= wxDEFAULT_FRAME_STYLE | wxWANTS_CHARS |
154 wxNO_FULL_REPAINT_ON_RESIZE
)
156 # Setup our menu bar.
158 menuBar
= wxMenuBar()
160 self
.fileMenu
= wxMenu()
161 self
.fileMenu
.Append(wxID_NEW
, "New\tCTRL-N")
162 self
.fileMenu
.Append(wxID_OPEN
, "Open...\tCTRL-O")
163 self
.fileMenu
.Append(wxID_CLOSE
, "Close\tCTRL-W")
164 self
.fileMenu
.AppendSeparator()
165 self
.fileMenu
.Append(wxID_SAVE
, "Save\tCTRL-S")
166 self
.fileMenu
.Append(wxID_SAVEAS
, "Save As...")
167 self
.fileMenu
.Append(wxID_REVERT
, "Revert...")
168 self
.fileMenu
.AppendSeparator()
169 self
.fileMenu
.Append(wxID_EXIT
, "Quit\tCTRL-Q")
171 menuBar
.Append(self
.fileMenu
, "File")
173 self
.editMenu
= wxMenu()
174 self
.editMenu
.Append(menu_UNDO
, "Undo\tCTRL-Z")
175 self
.editMenu
.AppendSeparator()
176 self
.editMenu
.Append(menu_SELECT_ALL
, "Select All\tCTRL-A")
177 self
.editMenu
.AppendSeparator()
178 self
.editMenu
.Append(menu_DUPLICATE
, "Duplicate\tCTRL-D")
179 self
.editMenu
.Append(menu_EDIT_TEXT
, "Edit...\tCTRL-E")
180 self
.editMenu
.Append(menu_DELETE
, "Delete\tDEL")
182 menuBar
.Append(self
.editMenu
, "Edit")
184 self
.toolsMenu
= wxMenu()
185 self
.toolsMenu
.Append(menu_SELECT
, "Selection", kind
=wxITEM_CHECK
)
186 self
.toolsMenu
.Append(menu_LINE
, "Line", kind
=wxITEM_CHECK
)
187 self
.toolsMenu
.Append(menu_RECT
, "Rectangle", kind
=wxITEM_CHECK
)
188 self
.toolsMenu
.Append(menu_ELLIPSE
, "Ellipse", kind
=wxITEM_CHECK
)
189 self
.toolsMenu
.Append(menu_TEXT
, "Text", kind
=wxITEM_CHECK
)
191 menuBar
.Append(self
.toolsMenu
, "Tools")
193 self
.objectMenu
= wxMenu()
194 self
.objectMenu
.Append(menu_MOVE_FORWARD
, "Move Forward")
195 self
.objectMenu
.Append(menu_MOVE_TO_FRONT
, "Move to Front\tCTRL-F")
196 self
.objectMenu
.Append(menu_MOVE_BACKWARD
, "Move Backward")
197 self
.objectMenu
.Append(menu_MOVE_TO_BACK
, "Move to Back\tCTRL-B")
199 menuBar
.Append(self
.objectMenu
, "Object")
201 self
.helpMenu
= wxMenu()
202 self
.helpMenu
.Append(menu_ABOUT
, "About pySketch...")
204 menuBar
.Append(self
.helpMenu
, "Help")
206 self
.SetMenuBar(menuBar
)
208 # Create our toolbar.
210 self
.toolbar
= self
.CreateToolBar(wxTB_HORIZONTAL |
211 wxNO_BORDER | wxTB_FLAT
)
213 self
.toolbar
.AddSimpleTool(wxID_NEW
,
214 wxBitmap("images/new.bmp",
217 self
.toolbar
.AddSimpleTool(wxID_OPEN
,
218 wxBitmap("images/open.bmp",
221 self
.toolbar
.AddSimpleTool(wxID_SAVE
,
222 wxBitmap("images/save.bmp",
225 self
.toolbar
.AddSeparator()
226 self
.toolbar
.AddSimpleTool(menu_UNDO
,
227 wxBitmap("images/undo.bmp",
230 self
.toolbar
.AddSeparator()
231 self
.toolbar
.AddSimpleTool(menu_DUPLICATE
,
232 wxBitmap("images/duplicate.bmp",
235 self
.toolbar
.AddSeparator()
236 self
.toolbar
.AddSimpleTool(menu_MOVE_FORWARD
,
237 wxBitmap("images/moveForward.bmp",
240 self
.toolbar
.AddSimpleTool(menu_MOVE_BACKWARD
,
241 wxBitmap("images/moveBack.bmp",
245 self
.toolbar
.Realize()
247 # Associate each menu/toolbar item with the method that handles that
250 EVT_MENU(self
, wxID_NEW
, self
.doNew
)
251 EVT_MENU(self
, wxID_OPEN
, self
.doOpen
)
252 EVT_MENU(self
, wxID_CLOSE
, self
.doClose
)
253 EVT_MENU(self
, wxID_SAVE
, self
.doSave
)
254 EVT_MENU(self
, wxID_SAVEAS
, self
.doSaveAs
)
255 EVT_MENU(self
, wxID_REVERT
, self
.doRevert
)
256 EVT_MENU(self
, wxID_EXIT
, self
.doExit
)
258 EVT_MENU(self
, menu_UNDO
, self
.doUndo
)
259 EVT_MENU(self
, menu_SELECT_ALL
, self
.doSelectAll
)
260 EVT_MENU(self
, menu_DUPLICATE
, self
.doDuplicate
)
261 EVT_MENU(self
, menu_EDIT_TEXT
, self
.doEditText
)
262 EVT_MENU(self
, menu_DELETE
, self
.doDelete
)
264 EVT_MENU(self
, menu_SELECT
, self
.doChooseSelectTool
)
265 EVT_MENU(self
, menu_LINE
, self
.doChooseLineTool
)
266 EVT_MENU(self
, menu_RECT
, self
.doChooseRectTool
)
267 EVT_MENU(self
, menu_ELLIPSE
, self
.doChooseEllipseTool
)
268 EVT_MENU(self
, menu_TEXT
, self
.doChooseTextTool
)
270 EVT_MENU(self
, menu_MOVE_FORWARD
, self
.doMoveForward
)
271 EVT_MENU(self
, menu_MOVE_TO_FRONT
, self
.doMoveToFront
)
272 EVT_MENU(self
, menu_MOVE_BACKWARD
, self
.doMoveBackward
)
273 EVT_MENU(self
, menu_MOVE_TO_BACK
, self
.doMoveToBack
)
275 EVT_MENU(self
, menu_ABOUT
, self
.doShowAbout
)
277 # Install our own method to handle closing the window. This allows us
278 # to ask the user if he/she wants to save before closing the window, as
279 # well as keeping track of which windows are currently open.
281 EVT_CLOSE(self
, self
.doClose
)
283 # Install our own method for handling keystrokes. We use this to let
284 # the user move the selected object(s) around using the arrow keys.
286 EVT_CHAR_HOOK(self
, self
.onKeyEvent
)
288 # Setup our top-most panel. This holds the entire contents of the
289 # window, excluding the menu bar.
291 self
.topPanel
= wxPanel(self
, -1, style
=wxSIMPLE_BORDER
)
293 # Setup our tool palette, with all our drawing tools and option icons.
295 self
.toolPalette
= wxBoxSizer(wxVERTICAL
)
297 self
.selectIcon
= ToolPaletteIcon(self
.topPanel
, id_SELECT
,
298 "select", "Selection Tool")
299 self
.lineIcon
= ToolPaletteIcon(self
.topPanel
, id_LINE
,
301 self
.rectIcon
= ToolPaletteIcon(self
.topPanel
, id_RECT
,
302 "rect", "Rectangle Tool")
303 self
.ellipseIcon
= ToolPaletteIcon(self
.topPanel
, id_ELLIPSE
,
304 "ellipse", "Ellipse Tool")
305 self
.textIcon
= ToolPaletteIcon(self
.topPanel
, id_TEXT
,
308 toolSizer
= wxGridSizer(0, 2, 5, 5)
309 toolSizer
.Add(self
.selectIcon
)
310 toolSizer
.Add(0, 0) # Gap to make tool icons line up nicely.
311 toolSizer
.Add(self
.lineIcon
)
312 toolSizer
.Add(self
.rectIcon
)
313 toolSizer
.Add(self
.ellipseIcon
)
314 toolSizer
.Add(self
.textIcon
)
316 self
.optionIndicator
= ToolOptionIndicator(self
.topPanel
)
317 self
.optionIndicator
.SetToolTip(
318 wxToolTip("Shows Current Pen/Fill/Line Size Settings"))
320 optionSizer
= wxBoxSizer(wxHORIZONTAL
)
322 self
.penOptIcon
= ToolPaletteIcon(self
.topPanel
, id_PEN_OPT
,
323 "penOpt", "Set Pen Colour")
324 self
.fillOptIcon
= ToolPaletteIcon(self
.topPanel
, id_FILL_OPT
,
325 "fillOpt", "Set Fill Colour")
326 self
.lineOptIcon
= ToolPaletteIcon(self
.topPanel
, id_LINE_OPT
,
327 "lineOpt", "Set Line Size")
329 margin
= wxLEFT | wxRIGHT
330 optionSizer
.Add(self
.penOptIcon
, 0, margin
, 1)
331 optionSizer
.Add(self
.fillOptIcon
, 0, margin
, 1)
332 optionSizer
.Add(self
.lineOptIcon
, 0, margin
, 1)
334 margin
= wxTOP | wxLEFT | wxRIGHT | wxALIGN_CENTRE
335 self
.toolPalette
.Add(toolSizer
, 0, margin
, 5)
336 self
.toolPalette
.Add(0, 0, 0, margin
, 5) # Spacer.
337 self
.toolPalette
.Add(self
.optionIndicator
, 0, margin
, 5)
338 self
.toolPalette
.Add(optionSizer
, 0, margin
, 5)
340 # Make the tool palette icons respond when the user clicks on them.
342 EVT_LEFT_DOWN(self
.selectIcon
, self
.onToolIconClick
)
343 EVT_LEFT_DOWN(self
.lineIcon
, self
.onToolIconClick
)
344 EVT_LEFT_DOWN(self
.rectIcon
, self
.onToolIconClick
)
345 EVT_LEFT_DOWN(self
.ellipseIcon
, self
.onToolIconClick
)
346 EVT_LEFT_DOWN(self
.textIcon
, self
.onToolIconClick
)
347 EVT_LEFT_DOWN(self
.penOptIcon
, self
.onPenOptionIconClick
)
348 EVT_LEFT_DOWN(self
.fillOptIcon
, self
.onFillOptionIconClick
)
349 EVT_LEFT_DOWN(self
.lineOptIcon
, self
.onLineOptionIconClick
)
351 # Setup the main drawing area.
353 self
.drawPanel
= wxScrolledWindow(self
.topPanel
, -1,
354 style
=wxSUNKEN_BORDER
)
355 self
.drawPanel
.SetBackgroundColour(wxWHITE
)
357 self
.drawPanel
.EnableScrolling(true
, true
)
358 self
.drawPanel
.SetScrollbars(20, 20, PAGE_WIDTH
/ 20, PAGE_HEIGHT
/ 20)
360 EVT_LEFT_DOWN(self
.drawPanel
, self
.onMouseEvent
)
361 EVT_LEFT_DCLICK(self
.drawPanel
, self
.onDoubleClickEvent
)
362 EVT_RIGHT_DOWN(self
.drawPanel
, self
.onRightClick
)
363 EVT_MOTION(self
.drawPanel
, self
.onMouseEvent
)
364 EVT_LEFT_UP(self
.drawPanel
, self
.onMouseEvent
)
365 EVT_PAINT(self
.drawPanel
, self
.onPaintEvent
)
367 # Position everything in the window.
369 topSizer
= wxBoxSizer(wxHORIZONTAL
)
370 topSizer
.Add(self
.toolPalette
, 0)
371 topSizer
.Add(self
.drawPanel
, 1, wxEXPAND
)
373 self
.topPanel
.SetAutoLayout(true
)
374 self
.topPanel
.SetSizer(topSizer
)
376 self
.SetSizeHints(minW
=250, minH
=200)
377 self
.SetSize(wxSize(600, 400))
379 # Select an initial tool.
382 self
._setCurrentTool
(self
.selectIcon
)
384 # Setup our frame to hold the contents of a sketch document.
387 self
.fileName
= fileName
388 self
.contents
= [] # front-to-back ordered list of DrawingObjects.
389 self
.selection
= [] # List of selected DrawingObjects.
390 self
.undoInfo
= None # Saved contents for undo.
391 self
.dragMode
= drag_NONE
# Current mouse-drag mode.
393 if self
.fileName
!= None:
398 # Finally, set our initial pen, fill and line options.
400 self
.penColour
= wxBLACK
401 self
.fillColour
= wxWHITE
404 # ============================
405 # == Event Handling Methods ==
406 # ============================
408 def onToolIconClick(self
, event
):
409 """ Respond to the user clicking on one of our tool icons.
411 iconID
= wxPyTypeCast(event
.GetEventObject(), "wxWindow").GetId()
412 if iconID
== id_SELECT
: self
.doChooseSelectTool()
413 elif iconID
== id_LINE
: self
.doChooseLineTool()
414 elif iconID
== id_RECT
: self
.doChooseRectTool()
415 elif iconID
== id_ELLIPSE
: self
.doChooseEllipseTool()
416 elif iconID
== id_TEXT
: self
.doChooseTextTool()
420 def onPenOptionIconClick(self
, event
):
421 """ Respond to the user clicking on the "Pen Options" icon.
423 data
= wxColourData()
424 if len(self
.selection
) == 1:
425 data
.SetColour(self
.selection
[0].getPenColour())
427 data
.SetColour(self
.penColour
)
429 dialog
= wxColourDialog(self
, data
)
430 if dialog
.ShowModal() == wxID_OK
:
431 c
= dialog
.GetColourData().GetColour()
432 self
._setPenColour
(wxColour(c
.Red(), c
.Green(), c
.Blue()))
436 def onFillOptionIconClick(self
, event
):
437 """ Respond to the user clicking on the "Fill Options" icon.
439 data
= wxColourData()
440 if len(self
.selection
) == 1:
441 data
.SetColour(self
.selection
[0].getFillColour())
443 data
.SetColour(self
.fillColour
)
445 dialog
= wxColourDialog(self
, data
)
446 if dialog
.ShowModal() == wxID_OK
:
447 c
= dialog
.GetColourData().GetColour()
448 self
._setFillColour
(wxColour(c
.Red(), c
.Green(), c
.Blue()))
451 def onLineOptionIconClick(self
, event
):
452 """ Respond to the user clicking on the "Line Options" icon.
454 if len(self
.selection
) == 1:
455 menu
= self
._buildLineSizePopup
(self
.selection
[0].getLineSize())
457 menu
= self
._buildLineSizePopup
(self
.lineSize
)
459 pos
= self
.lineOptIcon
.GetPosition()
460 pos
.y
= pos
.y
+ self
.lineOptIcon
.GetSize().height
461 self
.PopupMenu(menu
, pos
)
465 def onKeyEvent(self
, event
):
466 """ Respond to a keypress event.
468 We make the arrow keys move the selected object(s) by one pixel in
471 if event
.GetKeyCode() == WXK_UP
:
472 self
._moveObject
(0, -1)
473 elif event
.GetKeyCode() == WXK_DOWN
:
474 self
._moveObject
(0, 1)
475 elif event
.GetKeyCode() == WXK_LEFT
:
476 self
._moveObject
(-1, 0)
477 elif event
.GetKeyCode() == WXK_RIGHT
:
478 self
._moveObject
(1, 0)
483 def onMouseEvent(self
, event
):
484 """ Respond to the user clicking on our main drawing panel.
486 How we respond depends on the currently selected tool.
488 if not (event
.LeftDown() or event
.Dragging() or event
.LeftUp()):
489 return # Ignore mouse movement without click/drag.
491 if self
.curTool
== self
.selectIcon
:
492 feedbackType
= feedback_RECT
493 action
= self
.selectByRectangle
494 actionParam
= param_RECT
497 elif self
.curTool
== self
.lineIcon
:
498 feedbackType
= feedback_LINE
499 action
= self
.createLine
500 actionParam
= param_LINE
503 elif self
.curTool
== self
.rectIcon
:
504 feedbackType
= feedback_RECT
505 action
= self
.createRect
506 actionParam
= param_RECT
509 elif self
.curTool
== self
.ellipseIcon
:
510 feedbackType
= feedback_ELLIPSE
511 action
= self
.createEllipse
512 actionParam
= param_RECT
515 elif self
.curTool
== self
.textIcon
:
516 feedbackType
= feedback_RECT
517 action
= self
.createText
518 actionParam
= param_RECT
526 mousePt
= self
._getEventCoordinates
(event
)
528 obj
, handle
= self
._getObjectAndSelectionHandleAt
(mousePt
)
530 if selecting
and (obj
!= None) and (handle
!= handle_NONE
):
532 # The user clicked on an object's selection handle. Let the
533 # user resize the clicked-on object.
535 self
.dragMode
= drag_RESIZE
536 self
.resizeObject
= obj
538 if obj
.getType() == obj_LINE
:
539 self
.resizeFeedback
= feedback_LINE
540 pos
= obj
.getPosition()
541 startPt
= wxPoint(pos
.x
+ obj
.getStartPt().x
,
542 pos
.y
+ obj
.getStartPt().y
)
543 endPt
= wxPoint(pos
.x
+ obj
.getEndPt().x
,
544 pos
.y
+ obj
.getEndPt().y
)
545 if handle
== handle_START_POINT
:
546 self
.resizeAnchor
= endPt
547 self
.resizeFloater
= startPt
549 self
.resizeAnchor
= startPt
550 self
.resizeFloater
= endPt
552 self
.resizeFeedback
= feedback_RECT
553 pos
= obj
.getPosition()
555 topLeft
= wxPoint(pos
.x
, pos
.y
)
556 topRight
= wxPoint(pos
.x
+ size
.width
, pos
.y
)
557 botLeft
= wxPoint(pos
.x
, pos
.y
+ size
.height
)
558 botRight
= wxPoint(pos
.x
+ size
.width
, pos
.y
+ size
.height
)
560 if handle
== handle_TOP_LEFT
:
561 self
.resizeAnchor
= botRight
562 self
.resizeFloater
= topLeft
563 elif handle
== handle_TOP_RIGHT
:
564 self
.resizeAnchor
= botLeft
565 self
.resizeFloater
= topRight
566 elif handle
== handle_BOTTOM_LEFT
:
567 self
.resizeAnchor
= topRight
568 self
.resizeFloater
= botLeft
569 elif handle
== handle_BOTTOM_RIGHT
:
570 self
.resizeAnchor
= topLeft
571 self
.resizeFloater
= botRight
574 self
.resizeOffsetX
= self
.resizeFloater
.x
- mousePt
.x
575 self
.resizeOffsetY
= self
.resizeFloater
.y
- mousePt
.y
576 endPt
= wxPoint(self
.curPt
.x
+ self
.resizeOffsetX
,
577 self
.curPt
.y
+ self
.resizeOffsetY
)
578 self
._drawVisualFeedback
(self
.resizeAnchor
, endPt
,
579 self
.resizeFeedback
, false
)
581 elif selecting
and (self
._getObjectAt
(mousePt
) != None):
583 # The user clicked on an object to select it. If the user
584 # drags, he/she will move the object.
586 self
.select(self
._getObjectAt
(mousePt
))
587 self
.dragMode
= drag_MOVE
588 self
.moveOrigin
= mousePt
590 self
._drawObjectOutline
(0, 0)
594 # The user is dragging out a selection rect or new object.
596 self
.dragOrigin
= mousePt
598 self
.drawPanel
.SetCursor(wxCROSS_CURSOR
)
599 self
.drawPanel
.CaptureMouse()
600 self
._drawVisualFeedback
(mousePt
, mousePt
, feedbackType
,
602 self
.dragMode
= drag_DRAG
608 if self
.dragMode
== drag_RESIZE
:
610 # We're resizing an object.
612 mousePt
= self
._getEventCoordinates
(event
)
613 if (self
.curPt
.x
!= mousePt
.x
) or (self
.curPt
.y
!= mousePt
.y
):
614 # Erase previous visual feedback.
615 endPt
= wxPoint(self
.curPt
.x
+ self
.resizeOffsetX
,
616 self
.curPt
.y
+ self
.resizeOffsetY
)
617 self
._drawVisualFeedback
(self
.resizeAnchor
, endPt
,
618 self
.resizeFeedback
, false
)
620 # Draw new visual feedback.
621 endPt
= wxPoint(self
.curPt
.x
+ self
.resizeOffsetX
,
622 self
.curPt
.y
+ self
.resizeOffsetY
)
623 self
._drawVisualFeedback
(self
.resizeAnchor
, endPt
,
624 self
.resizeFeedback
, false
)
626 elif self
.dragMode
== drag_MOVE
:
628 # We're moving a selected object.
630 mousePt
= self
._getEventCoordinates
(event
)
631 if (self
.curPt
.x
!= mousePt
.x
) or (self
.curPt
.y
!= mousePt
.y
):
632 # Erase previous visual feedback.
633 self
._drawObjectOutline
(self
.curPt
.x
- self
.moveOrigin
.x
,
634 self
.curPt
.y
- self
.moveOrigin
.y
)
636 # Draw new visual feedback.
637 self
._drawObjectOutline
(self
.curPt
.x
- self
.moveOrigin
.x
,
638 self
.curPt
.y
- self
.moveOrigin
.y
)
640 elif self
.dragMode
== drag_DRAG
:
642 # We're dragging out a new object or selection rect.
644 mousePt
= self
._getEventCoordinates
(event
)
645 if (self
.curPt
.x
!= mousePt
.x
) or (self
.curPt
.y
!= mousePt
.y
):
646 # Erase previous visual feedback.
647 self
._drawVisualFeedback
(self
.dragOrigin
, self
.curPt
,
648 feedbackType
, dashedLine
)
650 # Draw new visual feedback.
651 self
._drawVisualFeedback
(self
.dragOrigin
, self
.curPt
,
652 feedbackType
, dashedLine
)
658 if self
.dragMode
== drag_RESIZE
:
660 # We're resizing an object.
662 mousePt
= self
._getEventCoordinates
(event
)
663 # Erase last visual feedback.
664 endPt
= wxPoint(self
.curPt
.x
+ self
.resizeOffsetX
,
665 self
.curPt
.y
+ self
.resizeOffsetY
)
666 self
._drawVisualFeedback
(self
.resizeAnchor
, endPt
,
667 self
.resizeFeedback
, false
)
669 resizePt
= wxPoint(mousePt
.x
+ self
.resizeOffsetX
,
670 mousePt
.y
+ self
.resizeOffsetY
)
672 if (self
.resizeFloater
.x
!= resizePt
.x
) or \
673 (self
.resizeFloater
.y
!= resizePt
.y
):
674 self
._resizeObject
(self
.resizeObject
,
679 self
.drawPanel
.Refresh() # Clean up after empty resize.
681 elif self
.dragMode
== drag_MOVE
:
683 # We're moving a selected object.
685 mousePt
= self
._getEventCoordinates
(event
)
686 # Erase last visual feedback.
687 self
._drawObjectOutline
(self
.curPt
.x
- self
.moveOrigin
.x
,
688 self
.curPt
.y
- self
.moveOrigin
.y
)
689 if (self
.moveOrigin
.x
!= mousePt
.x
) or \
690 (self
.moveOrigin
.y
!= mousePt
.y
):
691 self
._moveObject
(mousePt
.x
- self
.moveOrigin
.x
,
692 mousePt
.y
- self
.moveOrigin
.y
)
694 self
.drawPanel
.Refresh() # Clean up after empty drag.
696 elif self
.dragMode
== drag_DRAG
:
698 # We're dragging out a new object or selection rect.
700 mousePt
= self
._getEventCoordinates
(event
)
701 # Erase last visual feedback.
702 self
._drawVisualFeedback
(self
.dragOrigin
, self
.curPt
,
703 feedbackType
, dashedLine
)
704 self
.drawPanel
.ReleaseMouse()
705 self
.drawPanel
.SetCursor(wxSTANDARD_CURSOR
)
706 # Perform the appropriate action for the current tool.
707 if actionParam
== param_RECT
:
708 x1
= min(self
.dragOrigin
.x
, self
.curPt
.x
)
709 y1
= min(self
.dragOrigin
.y
, self
.curPt
.y
)
710 x2
= max(self
.dragOrigin
.x
, self
.curPt
.x
)
711 y2
= max(self
.dragOrigin
.y
, self
.curPt
.y
)
719 if ((x2
-x1
) < 8) or ((y2
-y1
) < 8): return # Too small.
721 action(x1
, y1
, x2
-x1
, y2
-y1
)
722 elif actionParam
== param_LINE
:
723 action(self
.dragOrigin
.x
, self
.dragOrigin
.y
,
724 self
.curPt
.x
, self
.curPt
.y
)
726 self
.dragMode
= drag_NONE
# We've finished with this mouse event.
730 def onDoubleClickEvent(self
, event
):
731 """ Respond to a double-click within our drawing panel.
733 mousePt
= self
._getEventCoordinates
(event
)
734 obj
= self
._getObjectAt
(mousePt
)
735 if obj
== None: return
737 # Let the user edit the given object.
739 if obj
.getType() == obj_TEXT
:
740 editor
= EditTextObjectDialog(self
, "Edit Text Object")
741 editor
.objectToDialog(obj
)
742 if editor
.ShowModal() == wxID_CANCEL
:
748 editor
.dialogToObject(obj
)
752 self
.drawPanel
.Refresh()
758 def onRightClick(self
, event
):
759 """ Respond to the user right-clicking within our drawing panel.
761 We select the clicked-on item, if necessary, and display a pop-up
762 menu of available options which can be applied to the selected
765 mousePt
= self
._getEventCoordinates
(event
)
766 obj
= self
._getObjectAt
(mousePt
)
768 if obj
== None: return # Nothing selected.
770 # Select the clicked-on object.
774 # Build our pop-up menu.
777 menu
.Append(menu_DUPLICATE
, "Duplicate")
778 menu
.Append(menu_EDIT_TEXT
, "Edit...")
779 menu
.Append(menu_DELETE
, "Delete")
780 menu
.AppendSeparator()
781 menu
.Append(menu_MOVE_FORWARD
, "Move Forward")
782 menu
.Append(menu_MOVE_TO_FRONT
, "Move to Front")
783 menu
.Append(menu_MOVE_BACKWARD
, "Move Backward")
784 menu
.Append(menu_MOVE_TO_BACK
, "Move to Back")
786 menu
.Enable(menu_EDIT_TEXT
, obj
.getType() == obj_TEXT
)
787 menu
.Enable(menu_MOVE_FORWARD
, obj
!= self
.contents
[0])
788 menu
.Enable(menu_MOVE_TO_FRONT
, obj
!= self
.contents
[0])
789 menu
.Enable(menu_MOVE_BACKWARD
, obj
!= self
.contents
[-1])
790 menu
.Enable(menu_MOVE_TO_BACK
, obj
!= self
.contents
[-1])
792 EVT_MENU(self
, menu_DUPLICATE
, self
.doDuplicate
)
793 EVT_MENU(self
, menu_EDIT_TEXT
, self
.doEditText
)
794 EVT_MENU(self
, menu_DELETE
, self
.doDelete
)
795 EVT_MENU(self
, menu_MOVE_FORWARD
, self
.doMoveForward
)
796 EVT_MENU(self
, menu_MOVE_TO_FRONT
, self
.doMoveToFront
)
797 EVT_MENU(self
, menu_MOVE_BACKWARD
, self
.doMoveBackward
)
798 EVT_MENU(self
, menu_MOVE_TO_BACK
, self
.doMoveToBack
)
800 # Show the pop-up menu.
802 clickPt
= wxPoint(mousePt
.x
+ self
.drawPanel
.GetPosition().x
,
803 mousePt
.y
+ self
.drawPanel
.GetPosition().y
)
804 self
.drawPanel
.PopupMenu(menu
, clickPt
)
808 def onPaintEvent(self
, event
):
809 """ Respond to a request to redraw the contents of our drawing panel.
811 dc
= wxPaintDC(self
.drawPanel
)
812 self
.drawPanel
.PrepareDC(dc
)
815 for i
in range(len(self
.contents
)-1, -1, -1):
816 obj
= self
.contents
[i
]
817 if obj
in self
.selection
:
824 # ==========================
825 # == Menu Command Methods ==
826 # ==========================
828 def doNew(self
, event
):
829 """ Respond to the "New" menu command.
832 newFrame
= DrawingFrame(None, -1, "Untitled")
834 _docList
.append(newFrame
)
837 def doOpen(self
, event
):
838 """ Respond to the "Open" menu command.
843 fileName
= wxFileSelector("Open File", default_extension
="psk",
844 flags
= wxOPEN | wxFILE_MUST_EXIST
)
845 if fileName
== "": return
846 fileName
= os
.path
.join(os
.getcwd(), fileName
)
849 title
= os
.path
.basename(fileName
)
851 if (self
.fileName
== None) and (len(self
.contents
) == 0):
852 # Load contents into current (empty) document.
853 self
.fileName
= fileName
854 self
.SetTitle(os
.path
.basename(fileName
))
857 # Open a new frame for this document.
858 newFrame
= DrawingFrame(None, -1, os
.path
.basename(fileName
),
861 _docList
.append(newFrame
)
864 def doClose(self
, event
):
865 """ Respond to the "Close" menu command.
870 if not self
.askIfUserWantsToSave("closing"): return
872 _docList
.remove(self
)
876 def doSave(self
, event
):
877 """ Respond to the "Save" menu command.
879 if self
.fileName
!= None:
883 def doSaveAs(self
, event
):
884 """ Respond to the "Save As" menu command.
886 if self
.fileName
== None:
889 default
= self
.fileName
892 fileName
= wxFileSelector("Save File As", "Saving",
893 default_filename
=default
,
894 default_extension
="psk",
896 flags
= wxSAVE | wxOVERWRITE_PROMPT
)
897 if fileName
== "": return # User cancelled.
898 fileName
= os
.path
.join(os
.getcwd(), fileName
)
901 title
= os
.path
.basename(fileName
)
904 self
.fileName
= fileName
908 def doRevert(self
, event
):
909 """ Respond to the "Revert" menu command.
911 if not self
.dirty
: return
913 if wxMessageBox("Discard changes made to this document?", "Confirm",
914 style
= wxOK | wxCANCEL | wxICON_QUESTION
,
915 parent
=self
) == wxCANCEL
: return
919 def doExit(self
, event
):
920 """ Respond to the "Quit" menu command.
922 global _docList
, _app
924 if not doc
.dirty
: continue
926 if not doc
.askIfUserWantsToSave("quitting"): return
933 def doUndo(self
, event
):
934 """ Respond to the "Undo" menu command.
936 if self
.undoInfo
== None: return
938 undoData
= self
.undoInfo
939 self
._saveUndoInfo
() # For undoing the undo...
943 for type, data
in undoData
["contents"]:
944 obj
= DrawingObject(type)
946 self
.contents
.append(obj
)
949 for i
in undoData
["selection"]:
950 self
.selection
.append(self
.contents
[i
])
953 self
.drawPanel
.Refresh()
957 def doSelectAll(self
, event
):
958 """ Respond to the "Select All" menu command.
963 def doDuplicate(self
, event
):
964 """ Respond to the "Duplicate" menu command.
969 for obj
in self
.contents
:
970 if obj
in self
.selection
:
971 newObj
= DrawingObject(obj
.getType())
972 newObj
.setData(obj
.getData())
973 pos
= obj
.getPosition()
974 newObj
.setPosition(wxPoint(pos
.x
+ 10, pos
.y
+ 10))
977 self
.contents
= objs
+ self
.contents
979 self
.selectMany(objs
)
982 def doEditText(self
, event
):
983 """ Respond to the "Edit Text" menu command.
985 if len(self
.selection
) != 1: return
987 obj
= self
.selection
[0]
988 if obj
.getType() != obj_TEXT
: return
990 editor
= EditTextObjectDialog(self
, "Edit Text Object")
991 editor
.objectToDialog(obj
)
992 if editor
.ShowModal() == wxID_CANCEL
:
998 editor
.dialogToObject(obj
)
1002 self
.drawPanel
.Refresh()
1006 def doDelete(self
, event
):
1007 """ Respond to the "Delete" menu command.
1009 self
._saveUndoInfo
()
1011 for obj
in self
.selection
:
1012 self
.contents
.remove(obj
)
1017 def doChooseSelectTool(self
, event
=None):
1018 """ Respond to the "Select Tool" menu command.
1020 self
._setCurrentTool
(self
.selectIcon
)
1021 self
.drawPanel
.SetCursor(wxSTANDARD_CURSOR
)
1025 def doChooseLineTool(self
, event
=None):
1026 """ Respond to the "Line Tool" menu command.
1028 self
._setCurrentTool
(self
.lineIcon
)
1029 self
.drawPanel
.SetCursor(wxCROSS_CURSOR
)
1034 def doChooseRectTool(self
, event
=None):
1035 """ Respond to the "Rect Tool" menu command.
1037 self
._setCurrentTool
(self
.rectIcon
)
1038 self
.drawPanel
.SetCursor(wxCROSS_CURSOR
)
1043 def doChooseEllipseTool(self
, event
=None):
1044 """ Respond to the "Ellipse Tool" menu command.
1046 self
._setCurrentTool
(self
.ellipseIcon
)
1047 self
.drawPanel
.SetCursor(wxCROSS_CURSOR
)
1052 def doChooseTextTool(self
, event
=None):
1053 """ Respond to the "Text Tool" menu command.
1055 self
._setCurrentTool
(self
.textIcon
)
1056 self
.drawPanel
.SetCursor(wxCROSS_CURSOR
)
1061 def doMoveForward(self
, event
):
1062 """ Respond to the "Move Forward" menu command.
1064 if len(self
.selection
) != 1: return
1066 self
._saveUndoInfo
()
1068 obj
= self
.selection
[0]
1069 index
= self
.contents
.index(obj
)
1070 if index
== 0: return
1072 del self
.contents
[index
]
1073 self
.contents
.insert(index
-1, obj
)
1075 self
.drawPanel
.Refresh()
1079 def doMoveToFront(self
, event
):
1080 """ Respond to the "Move to Front" menu command.
1082 if len(self
.selection
) != 1: return
1084 self
._saveUndoInfo
()
1086 obj
= self
.selection
[0]
1087 self
.contents
.remove(obj
)
1088 self
.contents
.insert(0, obj
)
1090 self
.drawPanel
.Refresh()
1094 def doMoveBackward(self
, event
):
1095 """ Respond to the "Move Backward" menu command.
1097 if len(self
.selection
) != 1: return
1099 self
._saveUndoInfo
()
1101 obj
= self
.selection
[0]
1102 index
= self
.contents
.index(obj
)
1103 if index
== len(self
.contents
) - 1: return
1105 del self
.contents
[index
]
1106 self
.contents
.insert(index
+1, obj
)
1108 self
.drawPanel
.Refresh()
1112 def doMoveToBack(self
, event
):
1113 """ Respond to the "Move to Back" menu command.
1115 if len(self
.selection
) != 1: return
1117 self
._saveUndoInfo
()
1119 obj
= self
.selection
[0]
1120 self
.contents
.remove(obj
)
1121 self
.contents
.append(obj
)
1123 self
.drawPanel
.Refresh()
1127 def doShowAbout(self
, event
):
1128 """ Respond to the "About pySketch" menu command.
1130 dialog
= wxDialog(self
, -1, "About pySketch") # ,
1131 #style=wxDIALOG_MODAL | wxSTAY_ON_TOP)
1132 dialog
.SetBackgroundColour(wxWHITE
)
1134 panel
= wxPanel(dialog
, -1)
1135 panel
.SetBackgroundColour(wxWHITE
)
1137 panelSizer
= wxBoxSizer(wxVERTICAL
)
1139 boldFont
= wxFont(panel
.GetFont().GetPointSize(),
1140 panel
.GetFont().GetFamily(),
1143 logo
= wxStaticBitmap(panel
, -1, wxBitmap("images/logo.bmp",
1146 lab1
= wxStaticText(panel
, -1, "pySketch")
1147 lab1
.SetFont(wxFont(36, boldFont
.GetFamily(), wxITALIC
, wxBOLD
))
1148 lab1
.SetSize(lab1
.GetBestSize())
1150 imageSizer
= wxBoxSizer(wxHORIZONTAL
)
1151 imageSizer
.Add(logo
, 0, wxALL | wxALIGN_CENTRE_VERTICAL
, 5)
1152 imageSizer
.Add(lab1
, 0, wxALL | wxALIGN_CENTRE_VERTICAL
, 5)
1154 lab2
= wxStaticText(panel
, -1, "A simple object-oriented drawing " + \
1156 lab2
.SetFont(boldFont
)
1157 lab2
.SetSize(lab2
.GetBestSize())
1159 lab3
= wxStaticText(panel
, -1, "pySketch is completely free " + \
1161 lab3
.SetFont(boldFont
)
1162 lab3
.SetSize(lab3
.GetBestSize())
1164 lab4
= wxStaticText(panel
, -1, "feel free to adapt or use this " + \
1165 "in any way you like.")
1166 lab4
.SetFont(boldFont
)
1167 lab4
.SetSize(lab4
.GetBestSize())
1169 lab5
= wxStaticText(panel
, -1, "Author: Erik Westra " + \
1170 "(ewestra@wave.co.nz)")
1171 lab5
.SetFont(boldFont
)
1172 lab5
.SetSize(lab5
.GetBestSize())
1174 btnOK
= wxButton(panel
, wxID_OK
, "OK")
1176 panelSizer
.Add(imageSizer
, 0, wxALIGN_CENTRE
)
1177 panelSizer
.Add(10, 10) # Spacer.
1178 panelSizer
.Add(lab2
, 0, wxALIGN_CENTRE
)
1179 panelSizer
.Add(10, 10) # Spacer.
1180 panelSizer
.Add(lab3
, 0, wxALIGN_CENTRE
)
1181 panelSizer
.Add(lab4
, 0, wxALIGN_CENTRE
)
1182 panelSizer
.Add(10, 10) # Spacer.
1183 panelSizer
.Add(lab5
, 0, wxALIGN_CENTRE
)
1184 panelSizer
.Add(10, 10) # Spacer.
1185 panelSizer
.Add(btnOK
, 0, wxALL | wxALIGN_CENTRE
, 5)
1187 panel
.SetAutoLayout(true
)
1188 panel
.SetSizer(panelSizer
)
1189 panelSizer
.Fit(panel
)
1191 topSizer
= wxBoxSizer(wxHORIZONTAL
)
1192 topSizer
.Add(panel
, 0, wxALL
, 10)
1194 dialog
.SetAutoLayout(true
)
1195 dialog
.SetSizer(topSizer
)
1196 topSizer
.Fit(dialog
)
1200 btn
= dialog
.ShowModal()
1203 # =============================
1204 # == Object Creation Methods ==
1205 # =============================
1207 def createLine(self
, x1
, y1
, x2
, y2
):
1208 """ Create a new line object at the given position and size.
1210 self
._saveUndoInfo
()
1212 topLeftX
= min(x1
, x2
)
1213 topLeftY
= min(y1
, y2
)
1214 botRightX
= max(x1
, x2
)
1215 botRightY
= max(y1
, y2
)
1217 obj
= DrawingObject(obj_LINE
, position
=wxPoint(topLeftX
, topLeftY
),
1218 size
=wxSize(botRightX
-topLeftX
,
1219 botRightY
-topLeftY
),
1220 penColour
=self
.penColour
,
1221 fillColour
=self
.fillColour
,
1222 lineSize
=self
.lineSize
,
1223 startPt
= wxPoint(x1
- topLeftX
, y1
- topLeftY
),
1224 endPt
= wxPoint(x2
- topLeftX
, y2
- topLeftY
))
1225 self
.contents
.insert(0, obj
)
1227 self
.doChooseSelectTool()
1231 def createRect(self
, x
, y
, width
, height
):
1232 """ Create a new rectangle object at the given position and size.
1234 self
._saveUndoInfo
()
1236 obj
= DrawingObject(obj_RECT
, position
=wxPoint(x
, y
),
1237 size
=wxSize(width
, height
),
1238 penColour
=self
.penColour
,
1239 fillColour
=self
.fillColour
,
1240 lineSize
=self
.lineSize
)
1241 self
.contents
.insert(0, obj
)
1243 self
.doChooseSelectTool()
1247 def createEllipse(self
, x
, y
, width
, height
):
1248 """ Create a new ellipse object at the given position and size.
1250 self
._saveUndoInfo
()
1252 obj
= DrawingObject(obj_ELLIPSE
, position
=wxPoint(x
, y
),
1253 size
=wxSize(width
, height
),
1254 penColour
=self
.penColour
,
1255 fillColour
=self
.fillColour
,
1256 lineSize
=self
.lineSize
)
1257 self
.contents
.insert(0, obj
)
1259 self
.doChooseSelectTool()
1263 def createText(self
, x
, y
, width
, height
):
1264 """ Create a new text object at the given position and size.
1266 editor
= EditTextObjectDialog(self
, "Create Text Object")
1267 if editor
.ShowModal() == wxID_CANCEL
:
1271 self
._saveUndoInfo
()
1273 obj
= DrawingObject(obj_TEXT
, position
=wxPoint(x
, y
),
1274 size
=wxSize(width
, height
))
1275 editor
.dialogToObject(obj
)
1278 self
.contents
.insert(0, obj
)
1280 self
.doChooseSelectTool()
1283 # =======================
1284 # == Selection Methods ==
1285 # =======================
1287 def selectAll(self
):
1288 """ Select every DrawingObject in our document.
1291 for obj
in self
.contents
:
1292 self
.selection
.append(obj
)
1293 self
.drawPanel
.Refresh()
1297 def deselectAll(self
):
1298 """ Deselect every DrawingObject in our document.
1301 self
.drawPanel
.Refresh()
1305 def select(self
, obj
):
1306 """ Select the given DrawingObject within our document.
1308 self
.selection
= [obj
]
1309 self
.drawPanel
.Refresh()
1313 def selectMany(self
, objs
):
1314 """ Select the given list of DrawingObjects.
1316 self
.selection
= objs
1317 self
.drawPanel
.Refresh()
1321 def selectByRectangle(self
, x
, y
, width
, height
):
1322 """ Select every DrawingObject in the given rectangular region.
1325 for obj
in self
.contents
:
1326 if obj
.objectWithinRect(x
, y
, width
, height
):
1327 self
.selection
.append(obj
)
1328 self
.drawPanel
.Refresh()
1331 # ======================
1332 # == File I/O Methods ==
1333 # ======================
1335 def loadContents(self
):
1336 """ Load the contents of our document into memory.
1338 f
= open(self
.fileName
, "rb")
1339 objData
= cPickle
.load(f
)
1342 for type, data
in objData
:
1343 obj
= DrawingObject(type)
1345 self
.contents
.append(obj
)
1349 self
.undoInfo
= None
1351 self
.drawPanel
.Refresh()
1355 def saveContents(self
):
1356 """ Save the contents of our document to disk.
1359 for obj
in self
.contents
:
1360 objData
.append([obj
.getType(), obj
.getData()])
1362 f
= open(self
.fileName
, "wb")
1363 cPickle
.dump(objData
, f
)
1369 def askIfUserWantsToSave(self
, action
):
1370 """ Give the user the opportunity to save the current document.
1372 'action' is a string describing the action about to be taken. If
1373 the user wants to save the document, it is saved immediately. If
1374 the user cancels, we return false.
1376 if not self
.dirty
: return true
# Nothing to do.
1378 response
= wxMessageBox("Save changes before " + action
+ "?",
1379 "Confirm", wxYES_NO | wxCANCEL
, self
)
1381 if response
== wxYES
:
1382 if self
.fileName
== None:
1383 fileName
= wxFileSelector("Save File As", "Saving",
1384 default_extension
="psk",
1386 flags
= wxSAVE | wxOVERWRITE_PROMPT
)
1387 if fileName
== "": return false
# User cancelled.
1388 self
.fileName
= fileName
1392 elif response
== wxNO
:
1393 return true
# User doesn't want changes saved.
1394 elif response
== wxCANCEL
:
1395 return false
# User cancelled.
1397 # =====================
1398 # == Private Methods ==
1399 # =====================
1401 def _adjustMenus(self
):
1402 """ Adjust our menus and toolbar to reflect the current state of the
1405 canSave
= (self
.fileName
!= None) and self
.dirty
1406 canRevert
= (self
.fileName
!= None) and self
.dirty
1407 canUndo
= self
.undoInfo
!= None
1408 selection
= len(self
.selection
) > 0
1409 onlyOne
= len(self
.selection
) == 1
1410 isText
= onlyOne
and (self
.selection
[0].getType() == obj_TEXT
)
1411 front
= onlyOne
and (self
.selection
[0] == self
.contents
[0])
1412 back
= onlyOne
and (self
.selection
[0] == self
.contents
[-1])
1414 # Enable/disable our menu items.
1416 self
.fileMenu
.Enable(wxID_SAVE
, canSave
)
1417 self
.fileMenu
.Enable(wxID_REVERT
, canRevert
)
1419 self
.editMenu
.Enable(menu_UNDO
, canUndo
)
1420 self
.editMenu
.Enable(menu_DUPLICATE
, selection
)
1421 self
.editMenu
.Enable(menu_EDIT_TEXT
, isText
)
1422 self
.editMenu
.Enable(menu_DELETE
, selection
)
1424 self
.toolsMenu
.Check(menu_SELECT
, self
.curTool
== self
.selectIcon
)
1425 self
.toolsMenu
.Check(menu_LINE
, self
.curTool
== self
.lineIcon
)
1426 self
.toolsMenu
.Check(menu_RECT
, self
.curTool
== self
.rectIcon
)
1427 self
.toolsMenu
.Check(menu_ELLIPSE
, self
.curTool
== self
.ellipseIcon
)
1428 self
.toolsMenu
.Check(menu_TEXT
, self
.curTool
== self
.textIcon
)
1430 self
.objectMenu
.Enable(menu_MOVE_FORWARD
, onlyOne
and not front
)
1431 self
.objectMenu
.Enable(menu_MOVE_TO_FRONT
, onlyOne
and not front
)
1432 self
.objectMenu
.Enable(menu_MOVE_BACKWARD
, onlyOne
and not back
)
1433 self
.objectMenu
.Enable(menu_MOVE_TO_BACK
, onlyOne
and not back
)
1435 # Enable/disable our toolbar icons.
1437 self
.toolbar
.EnableTool(wxID_NEW
, true
)
1438 self
.toolbar
.EnableTool(wxID_OPEN
, true
)
1439 self
.toolbar
.EnableTool(wxID_SAVE
, canSave
)
1440 self
.toolbar
.EnableTool(menu_UNDO
, canUndo
)
1441 self
.toolbar
.EnableTool(menu_DUPLICATE
, selection
)
1442 self
.toolbar
.EnableTool(menu_MOVE_FORWARD
, onlyOne
and not front
)
1443 self
.toolbar
.EnableTool(menu_MOVE_BACKWARD
, onlyOne
and not back
)
1446 def _setCurrentTool(self
, newToolIcon
):
1447 """ Set the currently selected tool.
1449 if self
.curTool
== newToolIcon
: return # Nothing to do.
1451 if self
.curTool
!= None:
1452 self
.curTool
.deselect()
1454 newToolIcon
.select()
1455 self
.curTool
= newToolIcon
1458 def _setPenColour(self
, colour
):
1459 """ Set the default or selected object's pen colour.
1461 if len(self
.selection
) > 0:
1462 self
._saveUndoInfo
()
1463 for obj
in self
.selection
:
1464 obj
.setPenColour(colour
)
1465 self
.drawPanel
.Refresh()
1467 self
.penColour
= colour
1468 self
.optionIndicator
.setPenColour(colour
)
1471 def _setFillColour(self
, colour
):
1472 """ Set the default or selected object's fill colour.
1474 if len(self
.selection
) > 0:
1475 self
._saveUndoInfo
()
1476 for obj
in self
.selection
:
1477 obj
.setFillColour(colour
)
1478 self
.drawPanel
.Refresh()
1480 self
.fillColour
= colour
1481 self
.optionIndicator
.setFillColour(colour
)
1484 def _setLineSize(self
, size
):
1485 """ Set the default or selected object's line size.
1487 if len(self
.selection
) > 0:
1488 self
._saveUndoInfo
()
1489 for obj
in self
.selection
:
1490 obj
.setLineSize(size
)
1491 self
.drawPanel
.Refresh()
1493 self
.lineSize
= size
1494 self
.optionIndicator
.setLineSize(size
)
1497 def _saveUndoInfo(self
):
1498 """ Remember the current state of the document, to allow for undo.
1500 We make a copy of the document's contents, so that we can return to
1501 the previous contents if the user does something and then wants to
1505 for obj
in self
.contents
:
1506 savedContents
.append([obj
.getType(), obj
.getData()])
1509 for i
in range(len(self
.contents
)):
1510 if self
.contents
[i
] in self
.selection
:
1511 savedSelection
.append(i
)
1513 self
.undoInfo
= {"contents" : savedContents
,
1514 "selection" : savedSelection
}
1517 def _resizeObject(self
, obj
, anchorPt
, oldPt
, newPt
):
1518 """ Resize the given object.
1520 'anchorPt' is the unchanging corner of the object, while the
1521 opposite corner has been resized. 'oldPt' are the current
1522 coordinates for this corner, while 'newPt' are the new coordinates.
1523 The object should fit within the given dimensions, though if the
1524 new point is less than the anchor point the object will need to be
1525 moved as well as resized, to avoid giving it a negative size.
1527 if obj
.getType() == obj_TEXT
:
1528 # Not allowed to resize text objects -- they're sized to fit text.
1532 self
._saveUndoInfo
()
1534 topLeft
= wxPoint(min(anchorPt
.x
, newPt
.x
),
1535 min(anchorPt
.y
, newPt
.y
))
1536 botRight
= wxPoint(max(anchorPt
.x
, newPt
.x
),
1537 max(anchorPt
.y
, newPt
.y
))
1539 newWidth
= botRight
.x
- topLeft
.x
1540 newHeight
= botRight
.y
- topLeft
.y
1542 if obj
.getType() == obj_LINE
:
1543 # Adjust the line so that its start and end points match the new
1544 # overall object size.
1546 startPt
= obj
.getStartPt()
1547 endPt
= obj
.getEndPt()
1549 slopesDown
= ((startPt
.x
< endPt
.x
) and (startPt
.y
< endPt
.y
)) or \
1550 ((startPt
.x
> endPt
.x
) and (startPt
.y
> endPt
.y
))
1552 # Handle the user flipping the line.
1554 hFlip
= ((anchorPt
.x
< oldPt
.x
) and (anchorPt
.x
> newPt
.x
)) or \
1555 ((anchorPt
.x
> oldPt
.x
) and (anchorPt
.x
< newPt
.x
))
1556 vFlip
= ((anchorPt
.y
< oldPt
.y
) and (anchorPt
.y
> newPt
.y
)) or \
1557 ((anchorPt
.y
> oldPt
.y
) and (anchorPt
.y
< newPt
.y
))
1559 if (hFlip
and not vFlip
) or (vFlip
and not hFlip
):
1560 slopesDown
= not slopesDown
# Line flipped.
1563 obj
.setStartPt(wxPoint(0, 0))
1564 obj
.setEndPt(wxPoint(newWidth
, newHeight
))
1566 obj
.setStartPt(wxPoint(0, newHeight
))
1567 obj
.setEndPt(wxPoint(newWidth
, 0))
1569 # Finally, adjust the bounds of the object to match the new dimensions.
1571 obj
.setPosition(topLeft
)
1572 obj
.setSize(wxSize(botRight
.x
- topLeft
.x
, botRight
.y
- topLeft
.y
))
1574 self
.drawPanel
.Refresh()
1577 def _moveObject(self
, offsetX
, offsetY
):
1578 """ Move the currently selected object(s) by the given offset.
1580 self
._saveUndoInfo
()
1582 for obj
in self
.selection
:
1583 pos
= obj
.getPosition()
1584 pos
.x
= pos
.x
+ offsetX
1585 pos
.y
= pos
.y
+ offsetY
1586 obj
.setPosition(pos
)
1588 self
.drawPanel
.Refresh()
1591 def _buildLineSizePopup(self
, lineSize
):
1592 """ Build the pop-up menu used to set the line size.
1594 'lineSize' is the current line size value. The corresponding item
1595 is checked in the pop-up menu.
1598 menu
.Append(id_LINESIZE_0
, "no line", kind
=wxITEM_CHECK
)
1599 menu
.Append(id_LINESIZE_1
, "1-pixel line", kind
=wxITEM_CHECK
)
1600 menu
.Append(id_LINESIZE_2
, "2-pixel line", kind
=wxITEM_CHECK
)
1601 menu
.Append(id_LINESIZE_3
, "3-pixel line", kind
=wxITEM_CHECK
)
1602 menu
.Append(id_LINESIZE_4
, "4-pixel line", kind
=wxITEM_CHECK
)
1603 menu
.Append(id_LINESIZE_5
, "5-pixel line", kind
=wxITEM_CHECK
)
1605 if lineSize
== 0: menu
.Check(id_LINESIZE_0
, true
)
1606 elif lineSize
== 1: menu
.Check(id_LINESIZE_1
, true
)
1607 elif lineSize
== 2: menu
.Check(id_LINESIZE_2
, true
)
1608 elif lineSize
== 3: menu
.Check(id_LINESIZE_3
, true
)
1609 elif lineSize
== 4: menu
.Check(id_LINESIZE_4
, true
)
1610 elif lineSize
== 5: menu
.Check(id_LINESIZE_5
, true
)
1612 EVT_MENU(self
, id_LINESIZE_0
, self
._lineSizePopupSelected
)
1613 EVT_MENU(self
, id_LINESIZE_1
, self
._lineSizePopupSelected
)
1614 EVT_MENU(self
, id_LINESIZE_2
, self
._lineSizePopupSelected
)
1615 EVT_MENU(self
, id_LINESIZE_3
, self
._lineSizePopupSelected
)
1616 EVT_MENU(self
, id_LINESIZE_4
, self
._lineSizePopupSelected
)
1617 EVT_MENU(self
, id_LINESIZE_5
, self
._lineSizePopupSelected
)
1622 def _lineSizePopupSelected(self
, event
):
1623 """ Respond to the user selecting an item from the line size popup menu
1626 if id == id_LINESIZE_0
: self
._setLineSize
(0)
1627 elif id == id_LINESIZE_1
: self
._setLineSize
(1)
1628 elif id == id_LINESIZE_2
: self
._setLineSize
(2)
1629 elif id == id_LINESIZE_3
: self
._setLineSize
(3)
1630 elif id == id_LINESIZE_4
: self
._setLineSize
(4)
1631 elif id == id_LINESIZE_5
: self
._setLineSize
(5)
1636 self
.optionIndicator
.setLineSize(self
.lineSize
)
1639 def _getEventCoordinates(self
, event
):
1640 """ Return the coordinates associated with the given mouse event.
1642 The coordinates have to be adjusted to allow for the current scroll
1645 originX
, originY
= self
.drawPanel
.GetViewStart()
1646 unitX
, unitY
= self
.drawPanel
.GetScrollPixelsPerUnit()
1647 return wxPoint(event
.GetX() + (originX
* unitX
),
1648 event
.GetY() + (originY
* unitY
))
1651 def _getObjectAndSelectionHandleAt(self
, pt
):
1652 """ Return the object and selection handle at the given point.
1654 We draw selection handles (small rectangles) around the currently
1655 selected object(s). If the given point is within one of the
1656 selection handle rectangles, we return the associated object and a
1657 code indicating which selection handle the point is in. If the
1658 point isn't within any selection handle at all, we return the tuple
1659 (None, handle_NONE).
1661 for obj
in self
.selection
:
1662 handle
= obj
.getSelectionHandleContainingPoint(pt
.x
, pt
.y
)
1663 if handle
!= handle_NONE
:
1666 return None, handle_NONE
1669 def _getObjectAt(self
, pt
):
1670 """ Return the first object found which is at the given point.
1672 for obj
in self
.contents
:
1673 if obj
.objectContainsPoint(pt
.x
, pt
.y
):
1678 def _drawObjectOutline(self
, offsetX
, offsetY
):
1679 """ Draw an outline of the currently selected object.
1681 The selected object's outline is drawn at the object's position
1682 plus the given offset.
1684 Note that the outline is drawn by *inverting* the window's
1685 contents, so calling _drawObjectOutline twice in succession will
1686 restore the window's contents back to what they were previously.
1688 if len(self
.selection
) != 1: return
1690 position
= self
.selection
[0].getPosition()
1691 size
= self
.selection
[0].getSize()
1693 dc
= wxClientDC(self
.drawPanel
)
1694 self
.drawPanel
.PrepareDC(dc
)
1696 dc
.SetPen(wxBLACK_DASHED_PEN
)
1697 dc
.SetBrush(wxTRANSPARENT_BRUSH
)
1698 dc
.SetLogicalFunction(wxINVERT
)
1700 dc
.DrawRectangle(position
.x
+ offsetX
, position
.y
+ offsetY
,
1701 size
.width
, size
.height
)
1706 def _drawVisualFeedback(self
, startPt
, endPt
, type, dashedLine
):
1707 """ Draw visual feedback for a drawing operation.
1709 The visual feedback consists of a line, ellipse, or rectangle based
1710 around the two given points. 'type' should be one of the following
1711 predefined feedback type constants:
1713 feedback_RECT -> draw rectangular feedback.
1714 feedback_LINE -> draw line feedback.
1715 feedback_ELLIPSE -> draw elliptical feedback.
1717 if 'dashedLine' is true, the feedback is drawn as a dashed rather
1720 Note that the feedback is drawn by *inverting* the window's
1721 contents, so calling _drawVisualFeedback twice in succession will
1722 restore the window's contents back to what they were previously.
1724 dc
= wxClientDC(self
.drawPanel
)
1725 self
.drawPanel
.PrepareDC(dc
)
1728 dc
.SetPen(wxBLACK_DASHED_PEN
)
1730 dc
.SetPen(wxBLACK_PEN
)
1731 dc
.SetBrush(wxTRANSPARENT_BRUSH
)
1732 dc
.SetLogicalFunction(wxINVERT
)
1734 if type == feedback_RECT
:
1735 dc
.DrawRectangle(startPt
.x
, startPt
.y
,
1736 endPt
.x
- startPt
.x
,
1737 endPt
.y
- startPt
.y
)
1738 elif type == feedback_LINE
:
1739 dc
.DrawLine(startPt
.x
, startPt
.y
, endPt
.x
, endPt
.y
)
1740 elif type == feedback_ELLIPSE
:
1741 dc
.DrawEllipse(startPt
.x
, startPt
.y
,
1742 endPt
.x
- startPt
.x
,
1743 endPt
.y
- startPt
.y
)
1747 #----------------------------------------------------------------------------
1749 class DrawingObject
:
1750 """ An object within the drawing panel.
1752 A pySketch document consists of a front-to-back ordered list of
1753 DrawingObjects. Each DrawingObject has the following properties:
1755 'type' What type of object this is (text, line, etc).
1756 'position' The position of the object within the document.
1757 'size' The size of the object within the document.
1758 'penColour' The colour to use for drawing the object's outline.
1759 'fillColour' Colour to use for drawing object's interior.
1760 'lineSize' Line width (in pixels) to use for object's outline.
1761 'startPt' The point, relative to the object's position, where
1762 an obj_LINE object's line should start.
1763 'endPt' The point, relative to the object's position, where
1764 an obj_LINE object's line should end.
1765 'text' The object's text (obj_TEXT objects only).
1766 'textFont' The text object's font name.
1767 'textSize' The text object's point size.
1768 'textBoldface' If true, this text object will be drawn in
1770 'textItalic' If true, this text object will be drawn in italic.
1771 'textUnderline' If true, this text object will be drawn underlined.
1774 # ==================
1775 # == Constructors ==
1776 # ==================
1778 def __init__(self
, type, position
=wxPoint(0, 0), size
=wxSize(0, 0),
1779 penColour
=wxBLACK
, fillColour
=wxWHITE
, lineSize
=1,
1780 text
=None, startPt
=wxPoint(0, 0), endPt
=wxPoint(0,0)):
1781 """ Standard constructor.
1783 'type' is the type of object being created. This should be one of
1784 the following constants:
1791 The remaining parameters let you set various options for the newly
1792 created DrawingObject.
1795 self
.position
= position
1797 self
.penColour
= penColour
1798 self
.fillColour
= fillColour
1799 self
.lineSize
= lineSize
1800 self
.startPt
= startPt
1803 self
.textFont
= wxSystemSettings_GetSystemFont(
1804 wxSYS_DEFAULT_GUI_FONT
).GetFaceName()
1806 self
.textBoldface
= false
1807 self
.textItalic
= false
1808 self
.textUnderline
= false
1810 # =============================
1811 # == Object Property Methods ==
1812 # =============================
1815 """ Return a copy of the object's internal data.
1817 This is used to save this DrawingObject to disk.
1819 return [self
.type, self
.position
.x
, self
.position
.y
,
1820 self
.size
.width
, self
.size
.height
,
1821 self
.penColour
.Red(),
1822 self
.penColour
.Green(),
1823 self
.penColour
.Blue(),
1824 self
.fillColour
.Red(),
1825 self
.fillColour
.Green(),
1826 self
.fillColour
.Blue(),
1828 self
.startPt
.x
, self
.startPt
.y
,
1829 self
.endPt
.x
, self
.endPt
.y
,
1838 def setData(self
, data
):
1839 """ Set the object's internal data.
1841 'data' is a copy of the object's saved data, as returned by
1842 getData() above. This is used to restore a previously saved
1845 #data = copy.deepcopy(data) # Needed?
1848 self
.position
= wxPoint(data
[1], data
[2])
1849 self
.size
= wxSize(data
[3], data
[4])
1850 self
.penColour
= wxColour(red
=data
[5],
1853 self
.fillColour
= wxColour(red
=data
[8],
1856 self
.lineSize
= data
[11]
1857 self
.startPt
= wxPoint(data
[12], data
[13])
1858 self
.endPt
= wxPoint(data
[14], data
[15])
1859 self
.text
= data
[16]
1860 self
.textFont
= data
[17]
1861 self
.textSize
= data
[18]
1862 self
.textBoldface
= data
[19]
1863 self
.textItalic
= data
[20]
1864 self
.textUnderline
= data
[21]
1868 """ Return this DrawingObject's type.
1873 def setPosition(self
, position
):
1874 """ Set the origin (top-left corner) for this DrawingObject.
1876 self
.position
= position
1879 def getPosition(self
):
1880 """ Return this DrawingObject's position.
1882 return self
.position
1885 def setSize(self
, size
):
1886 """ Set the size for this DrawingObject.
1892 """ Return this DrawingObject's size.
1897 def setPenColour(self
, colour
):
1898 """ Set the pen colour used for this DrawingObject.
1900 self
.penColour
= colour
1903 def getPenColour(self
):
1904 """ Return this DrawingObject's pen colour.
1906 return self
.penColour
1909 def setFillColour(self
, colour
):
1910 """ Set the fill colour used for this DrawingObject.
1912 self
.fillColour
= colour
1915 def getFillColour(self
):
1916 """ Return this DrawingObject's fill colour.
1918 return self
.fillColour
1921 def setLineSize(self
, lineSize
):
1922 """ Set the linesize used for this DrawingObject.
1924 self
.lineSize
= lineSize
1927 def getLineSize(self
):
1928 """ Return this DrawingObject's line size.
1930 return self
.lineSize
1933 def setStartPt(self
, startPt
):
1934 """ Set the starting point for this line DrawingObject.
1936 self
.startPt
= startPt
1939 def getStartPt(self
):
1940 """ Return the starting point for this line DrawingObject.
1945 def setEndPt(self
, endPt
):
1946 """ Set the ending point for this line DrawingObject.
1952 """ Return the ending point for this line DrawingObject.
1957 def setText(self
, text
):
1958 """ Set the text for this DrawingObject.
1964 """ Return this DrawingObject's text.
1969 def setTextFont(self
, font
):
1970 """ Set the typeface for this text DrawingObject.
1972 self
.textFont
= font
1975 def getTextFont(self
):
1976 """ Return this text DrawingObject's typeface.
1978 return self
.textFont
1981 def setTextSize(self
, size
):
1982 """ Set the point size for this text DrawingObject.
1984 self
.textSize
= size
1987 def getTextSize(self
):
1988 """ Return this text DrawingObject's text size.
1990 return self
.textSize
1993 def setTextBoldface(self
, boldface
):
1994 """ Set the boldface flag for this text DrawingObject.
1996 self
.textBoldface
= boldface
1999 def getTextBoldface(self
):
2000 """ Return this text DrawingObject's boldface flag.
2002 return self
.textBoldface
2005 def setTextItalic(self
, italic
):
2006 """ Set the italic flag for this text DrawingObject.
2008 self
.textItalic
= italic
2011 def getTextItalic(self
):
2012 """ Return this text DrawingObject's italic flag.
2014 return self
.textItalic
2017 def setTextUnderline(self
, underline
):
2018 """ Set the underling flag for this text DrawingObject.
2020 self
.textUnderline
= underline
2023 def getTextUnderline(self
):
2024 """ Return this text DrawingObject's underline flag.
2026 return self
.textUnderline
2028 # ============================
2029 # == Object Drawing Methods ==
2030 # ============================
2032 def draw(self
, dc
, selected
):
2033 """ Draw this DrawingObject into our window.
2035 'dc' is the device context to use for drawing. If 'selected' is
2036 true, the object is currently selected and should be drawn as such.
2038 if self
.type != obj_TEXT
:
2039 if self
.lineSize
== 0:
2040 dc
.SetPen(wxPen(self
.penColour
, self
.lineSize
, wxTRANSPARENT
))
2042 dc
.SetPen(wxPen(self
.penColour
, self
.lineSize
, wxSOLID
))
2043 dc
.SetBrush(wxBrush(self
.fillColour
, wxSOLID
))
2045 dc
.SetTextForeground(self
.penColour
)
2046 dc
.SetTextBackground(self
.fillColour
)
2048 self
._privateDraw
(dc
, self
.position
, selected
)
2050 # =======================
2051 # == Selection Methods ==
2052 # =======================
2054 def objectContainsPoint(self
, x
, y
):
2055 """ Returns true iff this object contains the given point.
2057 This is used to determine if the user clicked on the object.
2059 # Firstly, ignore any points outside of the object's bounds.
2061 if x
< self
.position
.x
: return false
2062 if x
> self
.position
.x
+ self
.size
.x
: return false
2063 if y
< self
.position
.y
: return false
2064 if y
> self
.position
.y
+ self
.size
.y
: return false
2066 if self
.type in [obj_RECT
, obj_TEXT
]:
2067 # Rectangles and text are easy -- they're always selected if the
2068 # point is within their bounds.
2071 # Now things get tricky. There's no straightforward way of knowing
2072 # whether the point is within the object's bounds...to get around this,
2073 # we draw the object into a memory-based bitmap and see if the given
2074 # point was drawn. This could no doubt be done more efficiently by
2075 # some tricky maths, but this approach works and is simple enough.
2077 bitmap
= wxEmptyBitmap(self
.size
.x
+ 10, self
.size
.y
+ 10)
2079 dc
.SelectObject(bitmap
)
2081 dc
.SetBackground(wxWHITE_BRUSH
)
2083 dc
.SetPen(wxPen(wxBLACK
, self
.lineSize
+ 5, wxSOLID
))
2084 dc
.SetBrush(wxBLACK_BRUSH
)
2085 self
._privateDraw
(dc
, wxPoint(5, 5), true
)
2087 pixel
= dc
.GetPixel(x
- self
.position
.x
+ 5, y
- self
.position
.y
+ 5)
2088 if (pixel
.Red() == 0) and (pixel
.Green() == 0) and (pixel
.Blue() == 0):
2094 def getSelectionHandleContainingPoint(self
, x
, y
):
2095 """ Return the selection handle containing the given point, if any.
2097 We return one of the predefined selection handle ID codes.
2099 if self
.type == obj_LINE
:
2100 # We have selection handles at the start and end points.
2101 if self
._pointInSelRect
(x
, y
, self
.position
.x
+ self
.startPt
.x
,
2102 self
.position
.y
+ self
.startPt
.y
):
2103 return handle_START_POINT
2104 elif self
._pointInSelRect
(x
, y
, self
.position
.x
+ self
.endPt
.x
,
2105 self
.position
.y
+ self
.endPt
.y
):
2106 return handle_END_POINT
2110 # We have selection handles at all four corners.
2111 if self
._pointInSelRect
(x
, y
, self
.position
.x
, self
.position
.y
):
2112 return handle_TOP_LEFT
2113 elif self
._pointInSelRect
(x
, y
, self
.position
.x
+ self
.size
.width
,
2115 return handle_TOP_RIGHT
2116 elif self
._pointInSelRect
(x
, y
, self
.position
.x
,
2117 self
.position
.y
+ self
.size
.height
):
2118 return handle_BOTTOM_LEFT
2119 elif self
._pointInSelRect
(x
, y
, self
.position
.x
+ self
.size
.width
,
2120 self
.position
.y
+ self
.size
.height
):
2121 return handle_BOTTOM_RIGHT
2126 def objectWithinRect(self
, x
, y
, width
, height
):
2127 """ Return true iff this object falls completely within the given rect.
2129 if x
> self
.position
.x
: return false
2130 if x
+ width
< self
.position
.x
+ self
.size
.width
: return false
2131 if y
> self
.position
.y
: return false
2132 if y
+ height
< self
.position
.y
+ self
.size
.height
: return false
2135 # =====================
2136 # == Utility Methods ==
2137 # =====================
2139 def fitToText(self
):
2140 """ Resize a text DrawingObject so that it fits it's text exactly.
2142 if self
.type != obj_TEXT
: return
2144 if self
.textBoldface
: weight
= wxBOLD
2145 else: weight
= wxNORMAL
2146 if self
.textItalic
: style
= wxITALIC
2147 else: style
= wxNORMAL
2148 font
= wxFont(self
.textSize
, wxDEFAULT
, style
, weight
,
2149 self
.textUnderline
, self
.textFont
)
2151 dummyWindow
= wxFrame(None, -1, "")
2152 dummyWindow
.SetFont(font
)
2153 width
, height
= dummyWindow
.GetTextExtent(self
.text
)
2154 dummyWindow
.Destroy()
2156 self
.size
= wxSize(width
, height
)
2158 # =====================
2159 # == Private Methods ==
2160 # =====================
2162 def _privateDraw(self
, dc
, position
, selected
):
2163 """ Private routine to draw this DrawingObject.
2165 'dc' is the device context to use for drawing, while 'position' is
2166 the position in which to draw the object. If 'selected' is true,
2167 the object is drawn with selection handles. This private drawing
2168 routine assumes that the pen and brush have already been set by the
2171 if self
.type == obj_LINE
:
2172 dc
.DrawLine(position
.x
+ self
.startPt
.x
,
2173 position
.y
+ self
.startPt
.y
,
2174 position
.x
+ self
.endPt
.x
,
2175 position
.y
+ self
.endPt
.y
)
2176 elif self
.type == obj_RECT
:
2177 dc
.DrawRectangle(position
.x
, position
.y
,
2178 self
.size
.width
, self
.size
.height
)
2179 elif self
.type == obj_ELLIPSE
:
2180 dc
.DrawEllipse(position
.x
, position
.y
,
2181 self
.size
.width
, self
.size
.height
)
2182 elif self
.type == obj_TEXT
:
2183 if self
.textBoldface
: weight
= wxBOLD
2184 else: weight
= wxNORMAL
2185 if self
.textItalic
: style
= wxITALIC
2186 else: style
= wxNORMAL
2187 font
= wxFont(self
.textSize
, wxDEFAULT
, style
, weight
,
2188 self
.textUnderline
, self
.textFont
)
2190 dc
.DrawText(self
.text
, position
.x
, position
.y
)
2193 dc
.SetPen(wxTRANSPARENT_PEN
)
2194 dc
.SetBrush(wxBLACK_BRUSH
)
2196 if self
.type == obj_LINE
:
2197 # Draw selection handles at the start and end points.
2198 self
._drawSelHandle
(dc
, position
.x
+ self
.startPt
.x
,
2199 position
.y
+ self
.startPt
.y
)
2200 self
._drawSelHandle
(dc
, position
.x
+ self
.endPt
.x
,
2201 position
.y
+ self
.endPt
.y
)
2203 # Draw selection handles at all four corners.
2204 self
._drawSelHandle
(dc
, position
.x
, position
.y
)
2205 self
._drawSelHandle
(dc
, position
.x
+ self
.size
.width
,
2207 self
._drawSelHandle
(dc
, position
.x
,
2208 position
.y
+ self
.size
.height
)
2209 self
._drawSelHandle
(dc
, position
.x
+ self
.size
.width
,
2210 position
.y
+ self
.size
.height
)
2213 def _drawSelHandle(self
, dc
, x
, y
):
2214 """ Draw a selection handle around this DrawingObject.
2216 'dc' is the device context to draw the selection handle within,
2217 while 'x' and 'y' are the coordinates to use for the centre of the
2220 dc
.DrawRectangle(x
- 3, y
- 3, 6, 6)
2223 def _pointInSelRect(self
, x
, y
, rX
, rY
):
2224 """ Return true iff (x, y) is within the selection handle at (rX, ry).
2226 if x
< rX
- 3: return false
2227 elif x
> rX
+ 3: return false
2228 elif y
< rY
- 3: return false
2229 elif y
> rY
+ 3: return false
2232 #----------------------------------------------------------------------------
2234 class ToolPaletteIcon(wxStaticBitmap
):
2235 """ An icon appearing in the tool palette area of our sketching window.
2237 Note that this is actually implemented as a wxStaticBitmap rather
2238 than as a wxIcon. wxIcon has a very specific meaning, and isn't
2239 appropriate for this more general use.
2242 def __init__(self
, parent
, iconID
, iconName
, toolTip
):
2243 """ Standard constructor.
2245 'parent' is the parent window this icon will be part of.
2246 'iconID' is the internal ID used for this icon.
2247 'iconName' is the name used for this icon.
2248 'toolTip' is the tool tip text to show for this icon.
2250 The icon name is used to get the appropriate bitmap for this icon.
2252 bmp
= wxBitmap("images/" + iconName
+ "Icon.bmp", wxBITMAP_TYPE_BMP
)
2253 wxStaticBitmap
.__init
__(self
, parent
, iconID
, bmp
, wxDefaultPosition
,
2254 wxSize(bmp
.GetWidth(), bmp
.GetHeight()))
2255 self
.SetToolTip(wxToolTip(toolTip
))
2257 self
.iconID
= iconID
2258 self
.iconName
= iconName
2259 self
.isSelected
= false
2263 """ Select the icon.
2265 The icon's visual representation is updated appropriately.
2267 if self
.isSelected
: return # Nothing to do!
2269 bmp
= wxBitmap("images/" + self
.iconName
+ "IconSel.bmp",
2272 self
.isSelected
= true
2276 """ Deselect the icon.
2278 The icon's visual representation is updated appropriately.
2280 if not self
.isSelected
: return # Nothing to do!
2282 bmp
= wxBitmap("images/" + self
.iconName
+ "Icon.bmp",
2285 self
.isSelected
= false
2287 #----------------------------------------------------------------------------
2289 class ToolOptionIndicator(wxWindow
):
2290 """ A visual indicator which shows the current tool options.
2292 def __init__(self
, parent
):
2293 """ Standard constructor.
2295 wxWindow
.__init
__(self
, parent
, -1, wxDefaultPosition
, wxSize(52, 32))
2297 self
.penColour
= wxBLACK
2298 self
.fillColour
= wxWHITE
2301 EVT_PAINT(self
, self
.OnPaint
)
2304 def setPenColour(self
, penColour
):
2305 """ Set the indicator's current pen colour.
2307 self
.penColour
= penColour
2311 def setFillColour(self
, fillColour
):
2312 """ Set the indicator's current fill colour.
2314 self
.fillColour
= fillColour
2318 def setLineSize(self
, lineSize
):
2319 """ Set the indicator's current pen colour.
2321 self
.lineSize
= lineSize
2325 def OnPaint(self
, event
):
2326 """ Paint our tool option indicator.
2328 dc
= wxPaintDC(self
)
2331 if self
.lineSize
== 0:
2332 dc
.SetPen(wxPen(self
.penColour
, self
.lineSize
, wxTRANSPARENT
))
2334 dc
.SetPen(wxPen(self
.penColour
, self
.lineSize
, wxSOLID
))
2335 dc
.SetBrush(wxBrush(self
.fillColour
, wxSOLID
))
2337 dc
.DrawRectangle(5, 5, self
.GetSize().width
- 10,
2338 self
.GetSize().height
- 10)
2342 #----------------------------------------------------------------------------
2344 class EditTextObjectDialog(wxDialog
):
2345 """ Dialog box used to edit the properties of a text object.
2347 The user can edit the object's text, font, size, and text style.
2350 def __init__(self
, parent
, title
):
2351 """ Standard constructor.
2353 wxDialog
.__init
__(self
, parent
, -1, title
)
2355 self
.textCtrl
= wxTextCtrl(self
, 1001, "", style
=wxTE_PROCESS_ENTER
,
2356 validator
=TextObjectValidator())
2357 extent
= self
.textCtrl
.GetFullTextExtent("Hy")
2358 lineHeight
= extent
[1] + extent
[3]
2359 self
.textCtrl
.SetSize(wxSize(-1, lineHeight
* 4))
2361 EVT_TEXT_ENTER(self
, 1001, self
._doEnter
)
2363 fonts
= wxFontEnumerator()
2364 fonts
.EnumerateFacenames()
2365 self
.fontList
= fonts
.GetFacenames()
2366 self
.fontList
.sort()
2368 fontLabel
= wxStaticText(self
, -1, "Font:")
2369 self
._setFontOptions
(fontLabel
, weight
=wxBOLD
)
2371 self
.fontCombo
= wxComboBox(self
, -1, "", wxDefaultPosition
,
2372 wxDefaultSize
, self
.fontList
,
2373 style
= wxCB_READONLY
)
2374 self
.fontCombo
.SetSelection(0) # Default to first available font.
2376 self
.sizeList
= ["8", "9", "10", "12", "14", "16",
2377 "18", "20", "24", "32", "48", "72"]
2379 sizeLabel
= wxStaticText(self
, -1, "Size:")
2380 self
._setFontOptions
(sizeLabel
, weight
=wxBOLD
)
2382 self
.sizeCombo
= wxComboBox(self
, -1, "", wxDefaultPosition
,
2383 wxDefaultSize
, self
.sizeList
,
2384 style
=wxCB_READONLY
)
2385 self
.sizeCombo
.SetSelection(3) # Default to 12 point text.
2387 gap
= wxLEFT | wxTOP | wxRIGHT
2389 comboSizer
= wxBoxSizer(wxHORIZONTAL
)
2390 comboSizer
.Add(fontLabel
, 0, gap | wxALIGN_CENTRE_VERTICAL
, 5)
2391 comboSizer
.Add(self
.fontCombo
, 0, gap
, 5)
2392 comboSizer
.Add(5, 5) # Spacer.
2393 comboSizer
.Add(sizeLabel
, 0, gap | wxALIGN_CENTRE_VERTICAL
, 5)
2394 comboSizer
.Add(self
.sizeCombo
, 0, gap
, 5)
2396 self
.boldCheckbox
= wxCheckBox(self
, -1, "Bold")
2397 self
.italicCheckbox
= wxCheckBox(self
, -1, "Italic")
2398 self
.underlineCheckbox
= wxCheckBox(self
, -1, "Underline")
2400 self
._setFontOptions
(self
.boldCheckbox
, weight
=wxBOLD
)
2401 self
._setFontOptions
(self
.italicCheckbox
, style
=wxITALIC
)
2402 self
._setFontOptions
(self
.underlineCheckbox
, underline
=true
)
2404 styleSizer
= wxBoxSizer(wxHORIZONTAL
)
2405 styleSizer
.Add(self
.boldCheckbox
, 0, gap
, 5)
2406 styleSizer
.Add(self
.italicCheckbox
, 0, gap
, 5)
2407 styleSizer
.Add(self
.underlineCheckbox
, 0, gap
, 5)
2409 self
.okButton
= wxButton(self
, wxID_OK
, "OK")
2410 self
.cancelButton
= wxButton(self
, wxID_CANCEL
, "Cancel")
2412 btnSizer
= wxBoxSizer(wxHORIZONTAL
)
2413 btnSizer
.Add(self
.okButton
, 0, gap
, 5)
2414 btnSizer
.Add(self
.cancelButton
, 0, gap
, 5)
2416 sizer
= wxBoxSizer(wxVERTICAL
)
2417 sizer
.Add(self
.textCtrl
, 1, gap | wxEXPAND
, 5)
2418 sizer
.Add(10, 10) # Spacer.
2419 sizer
.Add(comboSizer
, 0, gap | wxALIGN_CENTRE
, 5)
2420 sizer
.Add(styleSizer
, 0, gap | wxALIGN_CENTRE
, 5)
2421 sizer
.Add(10, 10) # Spacer.
2422 sizer
.Add(btnSizer
, 0, gap | wxALIGN_CENTRE
, 5)
2424 self
.SetAutoLayout(true
)
2425 self
.SetSizer(sizer
)
2428 self
.textCtrl
.SetFocus()
2431 def objectToDialog(self
, obj
):
2432 """ Copy the properties of the given text object into the dialog box.
2434 self
.textCtrl
.SetValue(obj
.getText())
2435 self
.textCtrl
.SetSelection(0, len(obj
.getText()))
2437 for i
in range(len(self
.fontList
)):
2438 if self
.fontList
[i
] == obj
.getTextFont():
2439 self
.fontCombo
.SetSelection(i
)
2442 for i
in range(len(self
.sizeList
)):
2443 if self
.sizeList
[i
] == str(obj
.getTextSize()):
2444 self
.sizeCombo
.SetSelection(i
)
2447 self
.boldCheckbox
.SetValue(obj
.getTextBoldface())
2448 self
.italicCheckbox
.SetValue(obj
.getTextItalic())
2449 self
.underlineCheckbox
.SetValue(obj
.getTextUnderline())
2452 def dialogToObject(self
, obj
):
2453 """ Copy the properties from the dialog box into the given text object.
2455 obj
.setText(self
.textCtrl
.GetValue())
2456 obj
.setTextFont(self
.fontCombo
.GetValue())
2457 obj
.setTextSize(string
.atoi(self
.sizeCombo
.GetValue()))
2458 obj
.setTextBoldface(self
.boldCheckbox
.GetValue())
2459 obj
.setTextItalic(self
.italicCheckbox
.GetValue())
2460 obj
.setTextUnderline(self
.underlineCheckbox
.GetValue())
2463 # ======================
2464 # == Private Routines ==
2465 # ======================
2467 def _setFontOptions(self
, ctrl
, family
=None, pointSize
=-1,
2468 style
=wxNORMAL
, weight
=wxNORMAL
,
2470 """ Change the font settings for the given control.
2472 The meaning of the 'family', 'pointSize', 'style', 'weight' and
2473 'underline' parameters are the same as for the wxFont constructor.
2474 If the family and/or pointSize isn't specified, the current default
2477 if family
== None: family
= ctrl
.GetFont().GetFamily()
2478 if pointSize
== -1: pointSize
= ctrl
.GetFont().GetPointSize()
2480 ctrl
.SetFont(wxFont(pointSize
, family
, style
, weight
, underline
))
2481 ctrl
.SetSize(ctrl
.GetBestSize()) # Adjust size to reflect font change.
2484 def _doEnter(self
, event
):
2485 """ Respond to the user hitting the ENTER key.
2487 We simulate clicking on the "OK" button.
2489 if self
.Validate(): self
.Show(false
)
2491 #----------------------------------------------------------------------------
2493 class TextObjectValidator(wxPyValidator
):
2494 """ This validator is used to ensure that the user has entered something
2495 into the text object editor dialog's text field.
2498 """ Standard constructor.
2500 wxPyValidator
.__init
__(self
)
2504 """ Standard cloner.
2506 Note that every validator must implement the Clone() method.
2508 return TextObjectValidator()
2511 def Validate(self
, win
):
2512 """ Validate the contents of the given text control.
2514 textCtrl
= wxPyTypeCast(self
.GetWindow(), "wxTextCtrl")
2515 text
= textCtrl
.GetValue()
2518 wxMessageBox("A text object must contain some text!", "Error")
2524 def TransferToWindow(self
):
2525 """ Transfer data from validator to window.
2527 The default implementation returns false, indicating that an error
2528 occurred. We simply return true, as we don't do any data transfer.
2530 return true
# Prevent wxDialog from complaining.
2533 def TransferFromWindow(self
):
2534 """ Transfer data from window to validator.
2536 The default implementation returns false, indicating that an error
2537 occurred. We simply return true, as we don't do any data transfer.
2539 return true
# Prevent wxDialog from complaining.
2541 #----------------------------------------------------------------------------
2543 class ExceptionHandler
:
2544 """ A simple error-handling class to write exceptions to a text file.
2546 Under MS Windows, the standard DOS console window doesn't scroll and
2547 closes as soon as the application exits, making it hard to find and
2548 view Python exceptions. This utility class allows you to handle Python
2549 exceptions in a more friendly manner.
2553 """ Standard constructor.
2556 if os
.path
.exists("errors.txt"):
2557 os
.remove("errors.txt") # Delete previous error log, if any.
2561 """ Write the given error message to a text file.
2563 Note that if the error message doesn't end in a carriage return, we
2564 have to buffer up the inputs until a carriage return is received.
2566 if (s
[-1] != "\n") and (s
[-1] != "\r"):
2567 self
._buff
= self
._buff
+ s
2574 if s
[:9] == "Traceback":
2575 # Tell the user than an exception occurred.
2576 wxMessageBox("An internal error has occurred.\nPlease " + \
2577 "refer to the 'errors.txt' file for details.",
2578 "Error", wxOK | wxCENTRE | wxICON_EXCLAMATION
)
2580 f
= open("errors.txt", "a")
2584 pass # Don't recursively crash on errors.
2586 #----------------------------------------------------------------------------
2588 class SketchApp(wxApp
):
2589 """ The main pySketch application object.
2592 """ Initialise the application.
2594 wxInitAllImageHandlers()
2599 if len(sys
.argv
) == 1:
2600 # No file name was specified on the command line -> start with a
2602 frame
= DrawingFrame(None, -1, "Untitled")
2605 _docList
.append(frame
)
2607 # Load the file(s) specified on the command line.
2608 for arg
in sys
.argv
[1:]:
2609 fileName
= os
.path
.join(os
.getcwd(), arg
)
2610 if os
.path
.isfile(fileName
):
2611 frame
= DrawingFrame(None, -1,
2612 os
.path
.basename(fileName
),
2615 _docList
.append(frame
)
2619 #----------------------------------------------------------------------------
2622 """ Start up the pySketch application.
2626 # Redirect python exceptions to a log file.
2628 sys
.stderr
= ExceptionHandler()
2630 # Create and start the pySketch application.
2636 if __name__
== "__main__":