]> git.saurik.com Git - wxWidgets.git/blob - wxPython/samples/pySketch/pySketch.py
Applied doc patch [ 1426505 ] Failure to call wxWizard.Destroy() causes hang on exit
[wxWidgets.git] / wxPython / samples / pySketch / pySketch.py
1 """ pySketch
2
3 A simple object-oriented drawing program.
4
5 This is completely free software; please feel free to adapt or use this in
6 any way you like.
7
8 Author: Erik Westra (ewestra@wave.co.nz)
9
10 #########################################################################
11
12 NOTE
13
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.
17
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:
20
21 self._setSelf(self, wxPyValidator, 0)
22
23 to:
24
25 self._setSelf(self, wxPyValidator, 1)
26
27 This fixes a known bug in wxPython 2.2.5 (and possibly earlier) which has
28 now been fixed in wxPython 2.3.
29
30 #########################################################################
31
32 TODO:
33
34 * Add ARGV checking to see if a document was double-clicked on.
35
36 Known Bugs:
37
38 * Scrolling the window causes the drawing panel to be mucked up until you
39 refresh it. I've got no idea why.
40
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.
44 """
45 import sys
46 import cPickle, os.path
47 import wx
48 from wx.lib.buttons import GenBitmapButton
49
50
51 import traceback, types
52
53 #----------------------------------------------------------------------------
54 # System Constants
55 #----------------------------------------------------------------------------
56
57 # Our menu item IDs:
58
59 menu_UNDO = 10001 # Edit menu items.
60 menu_SELECT_ALL = 10002
61 menu_DUPLICATE = 10003
62 menu_EDIT_TEXT = 10004
63 menu_DELETE = 10005
64
65 menu_SELECT = 10101 # Tools menu items.
66 menu_LINE = 10102
67 menu_RECT = 10103
68 menu_ELLIPSE = 10104
69 menu_TEXT = 10105
70
71 menu_MOVE_FORWARD = 10201 # Object menu items.
72 menu_MOVE_TO_FRONT = 10202
73 menu_MOVE_BACKWARD = 10203
74 menu_MOVE_TO_BACK = 10204
75
76 menu_ABOUT = 10205 # Help menu items.
77
78 # Our tool IDs:
79
80 id_SELECT = 11001
81 id_LINE = 11002
82 id_RECT = 11003
83 id_ELLIPSE = 11004
84 id_TEXT = 11005
85
86 # Our tool option IDs:
87
88 id_FILL_OPT = 12001
89 id_PEN_OPT = 12002
90 id_LINE_OPT = 12003
91
92 id_LINESIZE_0 = 13001
93 id_LINESIZE_1 = 13002
94 id_LINESIZE_2 = 13003
95 id_LINESIZE_3 = 13004
96 id_LINESIZE_4 = 13005
97 id_LINESIZE_5 = 13006
98
99 # DrawObject type IDs:
100
101 obj_LINE = 1
102 obj_RECT = 2
103 obj_ELLIPSE = 3
104 obj_TEXT = 4
105
106 # Selection handle IDs:
107
108 handle_NONE = 1
109 handle_TOP_LEFT = 2
110 handle_TOP_RIGHT = 3
111 handle_BOTTOM_LEFT = 4
112 handle_BOTTOM_RIGHT = 5
113 handle_START_POINT = 6
114 handle_END_POINT = 7
115
116 # Dragging operations:
117
118 drag_NONE = 1
119 drag_RESIZE = 2
120 drag_MOVE = 3
121 drag_DRAG = 4
122
123 # Visual Feedback types:
124
125 feedback_LINE = 1
126 feedback_RECT = 2
127 feedback_ELLIPSE = 3
128
129 # Mouse-event action parameter types:
130
131 param_RECT = 1
132 param_LINE = 2
133
134 # Size of the drawing page, in pixels.
135
136 PAGE_WIDTH = 1000
137 PAGE_HEIGHT = 1000
138
139 #----------------------------------------------------------------------------
140
141 class DrawingFrame(wx.Frame):
142 """ A frame showing the contents of a single document. """
143
144 # ==========================================
145 # == Initialisation and Window Management ==
146 # ==========================================
147
148 def __init__(self, parent, id, title, fileName=None):
149 """ Standard constructor.
150
151 'parent', 'id' and 'title' are all passed to the standard wx.Frame
152 constructor. 'fileName' is the name and path of a saved file to
153 load into this frame, if any.
154 """
155 wx.Frame.__init__(self, parent, id, title,
156 style = wx.DEFAULT_FRAME_STYLE | wx.WANTS_CHARS |
157 wx.NO_FULL_REPAINT_ON_RESIZE)
158
159 # Setup our menu bar.
160
161 menuBar = wx.MenuBar()
162
163 self.fileMenu = wx.Menu()
164 self.fileMenu.Append(wx.ID_NEW, "New\tCTRL-N")
165 self.fileMenu.Append(wx.ID_OPEN, "Open...\tCTRL-O")
166 self.fileMenu.Append(wx.ID_CLOSE, "Close\tCTRL-W")
167 self.fileMenu.AppendSeparator()
168 self.fileMenu.Append(wx.ID_SAVE, "Save\tCTRL-S")
169 self.fileMenu.Append(wx.ID_SAVEAS, "Save As...")
170 self.fileMenu.Append(wx.ID_REVERT, "Revert...")
171 self.fileMenu.AppendSeparator()
172 self.fileMenu.Append(wx.ID_EXIT, "Quit\tCTRL-Q")
173
174 menuBar.Append(self.fileMenu, "File")
175
176 self.editMenu = wx.Menu()
177 self.editMenu.Append(menu_UNDO, "Undo\tCTRL-Z")
178 self.editMenu.AppendSeparator()
179 self.editMenu.Append(menu_SELECT_ALL, "Select All\tCTRL-A")
180 self.editMenu.AppendSeparator()
181 self.editMenu.Append(menu_DUPLICATE, "Duplicate\tCTRL-D")
182 self.editMenu.Append(menu_EDIT_TEXT, "Edit...\tCTRL-E")
183 self.editMenu.Append(menu_DELETE, "Delete\tDEL")
184
185 menuBar.Append(self.editMenu, "Edit")
186
187 self.toolsMenu = wx.Menu()
188 self.toolsMenu.Append(menu_SELECT, "Selection", kind=wx.ITEM_CHECK)
189 self.toolsMenu.Append(menu_LINE, "Line", kind=wx.ITEM_CHECK)
190 self.toolsMenu.Append(menu_RECT, "Rectangle", kind=wx.ITEM_CHECK)
191 self.toolsMenu.Append(menu_ELLIPSE, "Ellipse", kind=wx.ITEM_CHECK)
192 self.toolsMenu.Append(menu_TEXT, "Text", kind=wx.ITEM_CHECK)
193
194 menuBar.Append(self.toolsMenu, "Tools")
195
196 self.objectMenu = wx.Menu()
197 self.objectMenu.Append(menu_MOVE_FORWARD, "Move Forward")
198 self.objectMenu.Append(menu_MOVE_TO_FRONT, "Move to Front\tCTRL-F")
199 self.objectMenu.Append(menu_MOVE_BACKWARD, "Move Backward")
200 self.objectMenu.Append(menu_MOVE_TO_BACK, "Move to Back\tCTRL-B")
201
202 menuBar.Append(self.objectMenu, "Object")
203
204 self.helpMenu = wx.Menu()
205 self.helpMenu.Append(menu_ABOUT, "About pySketch...")
206
207 menuBar.Append(self.helpMenu, "Help")
208
209 self.SetMenuBar(menuBar)
210
211 # Create our toolbar.
212
213 tsize = (16,16)
214 self.toolbar = self.CreateToolBar(wx.TB_HORIZONTAL |
215 wx.NO_BORDER | wx.TB_FLAT)
216
217 self.toolbar.AddSimpleTool(wx.ID_NEW,
218 wx.ArtProvider.GetBitmap(wx.ART_NEW, wx.ART_TOOLBAR, tsize),
219 "New")
220 self.toolbar.AddSimpleTool(wx.ID_OPEN,
221 wx.ArtProvider.GetBitmap(wx.ART_FILE_OPEN, wx.ART_TOOLBAR, tsize),
222 "Open")
223 self.toolbar.AddSimpleTool(wx.ID_SAVE,
224 wx.ArtProvider.GetBitmap(wx.ART_FILE_SAVE, wx.ART_TOOLBAR, tsize),
225 "Save")
226 self.toolbar.AddSeparator()
227 self.toolbar.AddSimpleTool(menu_UNDO,
228 wx.ArtProvider.GetBitmap(wx.ART_UNDO, wx.ART_TOOLBAR, tsize),
229 "Undo")
230 self.toolbar.AddSeparator()
231 self.toolbar.AddSimpleTool(menu_DUPLICATE,
232 wx.Bitmap("images/duplicate.bmp",
233 wx.BITMAP_TYPE_BMP),
234 "Duplicate")
235 self.toolbar.AddSeparator()
236 self.toolbar.AddSimpleTool(menu_MOVE_FORWARD,
237 wx.Bitmap("images/moveForward.bmp",
238 wx.BITMAP_TYPE_BMP),
239 "Move Forward")
240 self.toolbar.AddSimpleTool(menu_MOVE_BACKWARD,
241 wx.Bitmap("images/moveBack.bmp",
242 wx.BITMAP_TYPE_BMP),
243 "Move Backward")
244
245 self.toolbar.Realize()
246
247 # Associate each menu/toolbar item with the method that handles that
248 # item.
249 menuHandlers = [
250 (wx.ID_NEW, self.doNew),
251 (wx.ID_OPEN, self.doOpen),
252 (wx.ID_CLOSE, self.doClose),
253 (wx.ID_SAVE, self.doSave),
254 (wx.ID_SAVEAS, self.doSaveAs),
255 (wx.ID_REVERT, self.doRevert),
256 (wx.ID_EXIT, self.doExit),
257
258 (menu_UNDO, self.doUndo),
259 (menu_SELECT_ALL, self.doSelectAll),
260 (menu_DUPLICATE, self.doDuplicate),
261 (menu_EDIT_TEXT, self.doEditText),
262 (menu_DELETE, self.doDelete),
263
264 (menu_SELECT, self.doChooseSelectTool),
265 (menu_LINE, self.doChooseLineTool),
266 (menu_RECT, self.doChooseRectTool),
267 (menu_ELLIPSE, self.doChooseEllipseTool),
268 (menu_TEXT, self.doChooseTextTool),
269
270 (menu_MOVE_FORWARD, self.doMoveForward),
271 (menu_MOVE_TO_FRONT, self.doMoveToFront),
272 (menu_MOVE_BACKWARD, self.doMoveBackward),
273 (menu_MOVE_TO_BACK, self.doMoveToBack),
274
275 (menu_ABOUT, self.doShowAbout)]
276 for combo in menuHandlers:
277 id, handler = combo
278 self.Bind(wx.EVT_MENU, handler, id = id)
279
280
281 # Install our own method to handle closing the window. This allows us
282 # to ask the user if he/she wants to save before closing the window, as
283 # well as keeping track of which windows are currently open.
284
285 self.Bind(wx.EVT_CLOSE, self.doClose)
286
287 # Install our own method for handling keystrokes. We use this to let
288 # the user move the selected object(s) around using the arrow keys.
289
290 self.Bind(wx.EVT_CHAR_HOOK, self.onKeyEvent)
291
292 # Setup our top-most panel. This holds the entire contents of the
293 # window, excluding the menu bar.
294
295 self.topPanel = wx.Panel(self, -1, style=wx.SIMPLE_BORDER)
296
297 # Setup our tool palette, with all our drawing tools and option icons.
298
299 self.toolPalette = wx.BoxSizer(wx.VERTICAL)
300
301 self.selectIcon = ToolPaletteIcon(self.topPanel, id_SELECT,
302 "select", "Selection Tool")
303 self.lineIcon = ToolPaletteIcon(self.topPanel, id_LINE,
304 "line", "Line Tool")
305 self.rectIcon = ToolPaletteIcon(self.topPanel, id_RECT,
306 "rect", "Rectangle Tool")
307 self.ellipseIcon = ToolPaletteIcon(self.topPanel, id_ELLIPSE,
308 "ellipse", "Ellipse Tool")
309 self.textIcon = ToolPaletteIcon(self.topPanel, id_TEXT,
310 "text", "Text Tool")
311
312 toolSizer = wx.GridSizer(0, 2, 5, 5)
313 toolSizer.Add(self.selectIcon)
314 toolSizer.Add((0, 0)) # Gap to make tool icons line up nicely.
315 toolSizer.Add(self.lineIcon)
316 toolSizer.Add(self.rectIcon)
317 toolSizer.Add(self.ellipseIcon)
318 toolSizer.Add(self.textIcon)
319
320 self.optionIndicator = ToolOptionIndicator(self.topPanel)
321 self.optionIndicator.SetToolTip(
322 wx.ToolTip("Shows Current Pen/Fill/Line Size Settings"))
323
324 optionSizer = wx.BoxSizer(wx.HORIZONTAL)
325
326 self.penOptIcon = ToolPaletteIcon(self.topPanel, id_PEN_OPT,
327 "penOpt", "Set Pen Colour")
328 self.fillOptIcon = ToolPaletteIcon(self.topPanel, id_FILL_OPT,
329 "fillOpt", "Set Fill Colour")
330 self.lineOptIcon = ToolPaletteIcon(self.topPanel, id_LINE_OPT,
331 "lineOpt", "Set Line Size")
332
333 margin = wx.LEFT | wx.RIGHT
334 optionSizer.Add(self.penOptIcon, 0, margin, 1)
335 optionSizer.Add(self.fillOptIcon, 0, margin, 1)
336 optionSizer.Add(self.lineOptIcon, 0, margin, 1)
337
338 margin = wx.TOP | wx.LEFT | wx.RIGHT | wx.ALIGN_CENTRE
339 self.toolPalette.Add(toolSizer, 0, margin, 5)
340 self.toolPalette.Add((0, 0), 0, margin, 5) # Spacer.
341 self.toolPalette.Add(self.optionIndicator, 0, margin, 5)
342 self.toolPalette.Add(optionSizer, 0, margin, 5)
343
344 # Make the tool palette icons respond when the user clicks on them.
345
346 self.selectIcon.Bind(wx.EVT_BUTTON, self.onToolIconClick)
347 self.lineIcon.Bind(wx.EVT_BUTTON, self.onToolIconClick)
348 self.rectIcon.Bind(wx.EVT_BUTTON, self.onToolIconClick)
349 self.ellipseIcon.Bind(wx.EVT_BUTTON, self.onToolIconClick)
350 self.textIcon.Bind(wx.EVT_BUTTON, self.onToolIconClick)
351 self.penOptIcon.Bind(wx.EVT_BUTTON, self.onPenOptionIconClick)
352 self.fillOptIcon.Bind(wx.EVT_BUTTON, self.onFillOptionIconClick)
353 self.lineOptIcon.Bind(wx.EVT_BUTTON, self.onLineOptionIconClick)
354
355 # Setup the main drawing area.
356
357 self.drawPanel = wx.ScrolledWindow(self.topPanel, -1,
358 style=wx.SUNKEN_BORDER)
359 self.drawPanel.SetBackgroundColour(wx.WHITE)
360
361 self.drawPanel.EnableScrolling(True, True)
362 self.drawPanel.SetScrollbars(20, 20, PAGE_WIDTH / 20, PAGE_HEIGHT / 20)
363
364 self.drawPanel.Bind(wx.EVT_LEFT_DOWN, self.onMouseEvent)
365 self.drawPanel.Bind(wx.EVT_LEFT_DCLICK, self.onDoubleClickEvent)
366 self.drawPanel.Bind(wx.EVT_RIGHT_DOWN, self.onRightClick)
367 self.drawPanel.Bind(wx.EVT_MOTION, self.onMouseEvent)
368 self.drawPanel.Bind(wx.EVT_LEFT_UP, self.onMouseEvent)
369 self.drawPanel.Bind(wx.EVT_PAINT, self.onPaintEvent)
370
371 # Position everything in the window.
372
373 topSizer = wx.BoxSizer(wx.HORIZONTAL)
374 topSizer.Add(self.toolPalette, 0)
375 topSizer.Add(self.drawPanel, 1, wx.EXPAND)
376
377 self.topPanel.SetAutoLayout(True)
378 self.topPanel.SetSizer(topSizer)
379
380 self.SetSizeHints(250, 200)
381 self.SetSize(wx.Size(600, 400))
382
383 # Select an initial tool.
384
385 self.curTool = None
386 self._setCurrentTool(self.selectIcon)
387
388 # Setup our frame to hold the contents of a sketch document.
389
390 self.dirty = False
391 self.fileName = fileName
392 self.contents = [] # front-to-back ordered list of DrawingObjects.
393 self.selection = [] # List of selected DrawingObjects.
394 self.undoInfo = None # Saved contents for undo.
395 self.dragMode = drag_NONE # Current mouse-drag mode.
396
397 if self.fileName != None:
398 self.loadContents()
399
400 self._adjustMenus()
401
402 # Finally, set our initial pen, fill and line options.
403
404 self.penColour = wx.BLACK
405 self.fillColour = wx.WHITE
406 self.lineSize = 1
407
408 # ============================
409 # == Event Handling Methods ==
410 # ============================
411
412 def onToolIconClick(self, event):
413 """ Respond to the user clicking on one of our tool icons.
414 """
415 iconID = event.GetEventObject().GetId()
416 if iconID == id_SELECT: self.doChooseSelectTool()
417 elif iconID == id_LINE: self.doChooseLineTool()
418 elif iconID == id_RECT: self.doChooseRectTool()
419 elif iconID == id_ELLIPSE: self.doChooseEllipseTool()
420 elif iconID == id_TEXT: self.doChooseTextTool()
421 else: wx.Bell(); print "1"
422
423
424 def onPenOptionIconClick(self, event):
425 """ Respond to the user clicking on the "Pen Options" icon.
426 """
427 data = wx.ColourData()
428 if len(self.selection) == 1:
429 data.SetColour(self.selection[0].getPenColour())
430 else:
431 data.SetColour(self.penColour)
432
433 dialog = wx.ColourDialog(self, data)
434 if dialog.ShowModal() == wx.ID_OK:
435 c = dialog.GetColourData().GetColour()
436 self._setPenColour(wx.Colour(c.Red(), c.Green(), c.Blue()))
437 dialog.Destroy()
438
439
440 def onFillOptionIconClick(self, event):
441 """ Respond to the user clicking on the "Fill Options" icon.
442 """
443 data = wx.ColourData()
444 if len(self.selection) == 1:
445 data.SetColour(self.selection[0].getFillColour())
446 else:
447 data.SetColour(self.fillColour)
448
449 dialog = wx.ColourDialog(self, data)
450 if dialog.ShowModal() == wx.ID_OK:
451 c = dialog.GetColourData().GetColour()
452 self._setFillColour(wx.Colour(c.Red(), c.Green(), c.Blue()))
453 dialog.Destroy()
454
455 def onLineOptionIconClick(self, event):
456 """ Respond to the user clicking on the "Line Options" icon.
457 """
458 if len(self.selection) == 1:
459 menu = self._buildLineSizePopup(self.selection[0].getLineSize())
460 else:
461 menu = self._buildLineSizePopup(self.lineSize)
462
463 pos = self.lineOptIcon.GetPosition()
464 pos.y = pos.y + self.lineOptIcon.GetSize().height
465 self.PopupMenu(menu, pos)
466 menu.Destroy()
467
468
469 def onKeyEvent(self, event):
470 """ Respond to a keypress event.
471
472 We make the arrow keys move the selected object(s) by one pixel in
473 the given direction.
474 """
475 if event.GetKeyCode() == wx.WXK_UP:
476 self._moveObject(0, -1)
477 elif event.GetKeyCode() == wx.WXK_DOWN:
478 self._moveObject(0, 1)
479 elif event.GetKeyCode() == wx.WXK_LEFT:
480 self._moveObject(-1, 0)
481 elif event.GetKeyCode() == wx.WXK_RIGHT:
482 self._moveObject(1, 0)
483 else:
484 event.Skip()
485
486
487 def onMouseEvent(self, event):
488 """ Respond to the user clicking on our main drawing panel.
489
490 How we respond depends on the currently selected tool.
491 """
492 if not (event.LeftDown() or event.Dragging() or event.LeftUp()):
493 return # Ignore mouse movement without click/drag.
494
495 if self.curTool == self.selectIcon:
496 feedbackType = feedback_RECT
497 action = self.selectByRectangle
498 actionParam = param_RECT
499 selecting = True
500 dashedLine = True
501 elif self.curTool == self.lineIcon:
502 feedbackType = feedback_LINE
503 action = self.createLine
504 actionParam = param_LINE
505 selecting = False
506 dashedLine = False
507 elif self.curTool == self.rectIcon:
508 feedbackType = feedback_RECT
509 action = self.createRect
510 actionParam = param_RECT
511 selecting = False
512 dashedLine = False
513 elif self.curTool == self.ellipseIcon:
514 feedbackType = feedback_ELLIPSE
515 action = self.createEllipse
516 actionParam = param_RECT
517 selecting = False
518 dashedLine = False
519 elif self.curTool == self.textIcon:
520 feedbackType = feedback_RECT
521 action = self.createText
522 actionParam = param_RECT
523 selecting = False
524 dashedLine = True
525 else:
526 wx.Bell(); print "2"
527 return
528
529 if event.LeftDown():
530 mousePt = self._getEventCoordinates(event)
531 if selecting:
532 obj, handle = self._getObjectAndSelectionHandleAt(mousePt)
533
534 if selecting and (obj != None) and (handle != handle_NONE):
535
536 # The user clicked on an object's selection handle. Let the
537 # user resize the clicked-on object.
538
539 self.dragMode = drag_RESIZE
540 self.resizeObject = obj
541
542 if obj.getType() == obj_LINE:
543 self.resizeFeedback = feedback_LINE
544 pos = obj.getPosition()
545 startPt = wx.Point(pos.x + obj.getStartPt().x,
546 pos.y + obj.getStartPt().y)
547 endPt = wx.Point(pos.x + obj.getEndPt().x,
548 pos.y + obj.getEndPt().y)
549 if handle == handle_START_POINT:
550 self.resizeAnchor = endPt
551 self.resizeFloater = startPt
552 else:
553 self.resizeAnchor = startPt
554 self.resizeFloater = endPt
555 else:
556 self.resizeFeedback = feedback_RECT
557 pos = obj.getPosition()
558 size = obj.getSize()
559 topLeft = wx.Point(pos.x, pos.y)
560 topRight = wx.Point(pos.x + size.width, pos.y)
561 botLeft = wx.Point(pos.x, pos.y + size.height)
562 botRight = wx.Point(pos.x + size.width, pos.y + size.height)
563
564 if handle == handle_TOP_LEFT:
565 self.resizeAnchor = botRight
566 self.resizeFloater = topLeft
567 elif handle == handle_TOP_RIGHT:
568 self.resizeAnchor = botLeft
569 self.resizeFloater = topRight
570 elif handle == handle_BOTTOM_LEFT:
571 self.resizeAnchor = topRight
572 self.resizeFloater = botLeft
573 elif handle == handle_BOTTOM_RIGHT:
574 self.resizeAnchor = topLeft
575 self.resizeFloater = botRight
576
577 self.curPt = mousePt
578 self.resizeOffsetX = self.resizeFloater.x - mousePt.x
579 self.resizeOffsetY = self.resizeFloater.y - mousePt.y
580 endPt = wx.Point(self.curPt.x + self.resizeOffsetX,
581 self.curPt.y + self.resizeOffsetY)
582 self._drawVisualFeedback(self.resizeAnchor, endPt,
583 self.resizeFeedback, False)
584
585 elif selecting and (self._getObjectAt(mousePt) != None):
586
587 # The user clicked on an object to select it. If the user
588 # drags, he/she will move the object.
589
590 self.select(self._getObjectAt(mousePt))
591 self.dragMode = drag_MOVE
592 self.moveOrigin = mousePt
593 self.curPt = mousePt
594 self._drawObjectOutline(0, 0)
595
596 else:
597
598 # The user is dragging out a selection rect or new object.
599
600 self.dragOrigin = mousePt
601 self.curPt = mousePt
602 self.drawPanel.SetCursor(wx.CROSS_CURSOR)
603 self.drawPanel.CaptureMouse()
604 self._drawVisualFeedback(mousePt, mousePt, feedbackType,
605 dashedLine)
606 self.dragMode = drag_DRAG
607
608 event.Skip()
609 return
610
611 if event.Dragging():
612 if self.dragMode == drag_RESIZE:
613
614 # We're resizing an object.
615
616 mousePt = self._getEventCoordinates(event)
617 if (self.curPt.x != mousePt.x) or (self.curPt.y != mousePt.y):
618 # Erase previous visual feedback.
619 endPt = wx.Point(self.curPt.x + self.resizeOffsetX,
620 self.curPt.y + self.resizeOffsetY)
621 self._drawVisualFeedback(self.resizeAnchor, endPt,
622 self.resizeFeedback, False)
623 self.curPt = mousePt
624 # Draw new visual feedback.
625 endPt = wx.Point(self.curPt.x + self.resizeOffsetX,
626 self.curPt.y + self.resizeOffsetY)
627 self._drawVisualFeedback(self.resizeAnchor, endPt,
628 self.resizeFeedback, False)
629
630 elif self.dragMode == drag_MOVE:
631
632 # We're moving a selected object.
633
634 mousePt = self._getEventCoordinates(event)
635 if (self.curPt.x != mousePt.x) or (self.curPt.y != mousePt.y):
636 # Erase previous visual feedback.
637 self._drawObjectOutline(self.curPt.x - self.moveOrigin.x,
638 self.curPt.y - self.moveOrigin.y)
639 self.curPt = mousePt
640 # Draw new visual feedback.
641 self._drawObjectOutline(self.curPt.x - self.moveOrigin.x,
642 self.curPt.y - self.moveOrigin.y)
643
644 elif self.dragMode == drag_DRAG:
645
646 # We're dragging out a new object or selection rect.
647
648 mousePt = self._getEventCoordinates(event)
649 if (self.curPt.x != mousePt.x) or (self.curPt.y != mousePt.y):
650 # Erase previous visual feedback.
651 self._drawVisualFeedback(self.dragOrigin, self.curPt,
652 feedbackType, dashedLine)
653 self.curPt = mousePt
654 # Draw new visual feedback.
655 self._drawVisualFeedback(self.dragOrigin, self.curPt,
656 feedbackType, dashedLine)
657
658 event.Skip()
659 return
660
661 if event.LeftUp():
662 if self.dragMode == drag_RESIZE:
663
664 # We're resizing an object.
665
666 mousePt = self._getEventCoordinates(event)
667 # Erase last visual feedback.
668 endPt = wx.Point(self.curPt.x + self.resizeOffsetX,
669 self.curPt.y + self.resizeOffsetY)
670 self._drawVisualFeedback(self.resizeAnchor, endPt,
671 self.resizeFeedback, False)
672
673 resizePt = wx.Point(mousePt.x + self.resizeOffsetX,
674 mousePt.y + self.resizeOffsetY)
675
676 if (self.resizeFloater.x != resizePt.x) or \
677 (self.resizeFloater.y != resizePt.y):
678 self._resizeObject(self.resizeObject,
679 self.resizeAnchor,
680 self.resizeFloater,
681 resizePt)
682 else:
683 self.drawPanel.Refresh() # Clean up after empty resize.
684
685 elif self.dragMode == drag_MOVE:
686
687 # We're moving a selected object.
688
689 mousePt = self._getEventCoordinates(event)
690 # Erase last visual feedback.
691 self._drawObjectOutline(self.curPt.x - self.moveOrigin.x,
692 self.curPt.y - self.moveOrigin.y)
693 if (self.moveOrigin.x != mousePt.x) or \
694 (self.moveOrigin.y != mousePt.y):
695 self._moveObject(mousePt.x - self.moveOrigin.x,
696 mousePt.y - self.moveOrigin.y)
697 else:
698 self.drawPanel.Refresh() # Clean up after empty drag.
699
700 elif self.dragMode == drag_DRAG:
701
702 # We're dragging out a new object or selection rect.
703
704 mousePt = self._getEventCoordinates(event)
705 # Erase last visual feedback.
706 self._drawVisualFeedback(self.dragOrigin, self.curPt,
707 feedbackType, dashedLine)
708 self.drawPanel.ReleaseMouse()
709 self.drawPanel.SetCursor(wx.STANDARD_CURSOR)
710 # Perform the appropriate action for the current tool.
711 if actionParam == param_RECT:
712 x1 = min(self.dragOrigin.x, self.curPt.x)
713 y1 = min(self.dragOrigin.y, self.curPt.y)
714 x2 = max(self.dragOrigin.x, self.curPt.x)
715 y2 = max(self.dragOrigin.y, self.curPt.y)
716
717 startX = x1
718 startY = y1
719 width = x2 - x1
720 height = y2 - y1
721
722 if not selecting:
723 if ((x2-x1) < 8) or ((y2-y1) < 8): return # Too small.
724
725 action(x1, y1, x2-x1, y2-y1)
726 elif actionParam == param_LINE:
727 action(self.dragOrigin.x, self.dragOrigin.y,
728 self.curPt.x, self.curPt.y)
729
730 self.dragMode = drag_NONE # We've finished with this mouse event.
731 event.Skip()
732
733
734 def onDoubleClickEvent(self, event):
735 """ Respond to a double-click within our drawing panel.
736 """
737 mousePt = self._getEventCoordinates(event)
738 obj = self._getObjectAt(mousePt)
739 if obj == None: return
740
741 # Let the user edit the given object.
742
743 if obj.getType() == obj_TEXT:
744 editor = EditTextObjectDialog(self, "Edit Text Object")
745 editor.objectToDialog(obj)
746 if editor.ShowModal() == wx.ID_CANCEL:
747 editor.Destroy()
748 return
749
750 self._saveUndoInfo()
751
752 editor.dialogToObject(obj)
753 editor.Destroy()
754
755 self.dirty = True
756 self.drawPanel.Refresh()
757 self._adjustMenus()
758 else:
759 wx.Bell(); print "3"
760
761
762 def onRightClick(self, event):
763 """ Respond to the user right-clicking within our drawing panel.
764
765 We select the clicked-on item, if necessary, and display a pop-up
766 menu of available options which can be applied to the selected
767 item(s).
768 """
769 mousePt = self._getEventCoordinates(event)
770 obj = self._getObjectAt(mousePt)
771
772 if obj == None: return # Nothing selected.
773
774 # Select the clicked-on object.
775
776 self.select(obj)
777
778 # Build our pop-up menu.
779
780 menu = wx.Menu()
781 menu.Append(menu_DUPLICATE, "Duplicate")
782 menu.Append(menu_EDIT_TEXT, "Edit...")
783 menu.Append(menu_DELETE, "Delete")
784 menu.AppendSeparator()
785 menu.Append(menu_MOVE_FORWARD, "Move Forward")
786 menu.Append(menu_MOVE_TO_FRONT, "Move to Front")
787 menu.Append(menu_MOVE_BACKWARD, "Move Backward")
788 menu.Append(menu_MOVE_TO_BACK, "Move to Back")
789
790 menu.Enable(menu_EDIT_TEXT, obj.getType() == obj_TEXT)
791 menu.Enable(menu_MOVE_FORWARD, obj != self.contents[0])
792 menu.Enable(menu_MOVE_TO_FRONT, obj != self.contents[0])
793 menu.Enable(menu_MOVE_BACKWARD, obj != self.contents[-1])
794 menu.Enable(menu_MOVE_TO_BACK, obj != self.contents[-1])
795
796 EVT_MENU(self, menu_DUPLICATE, self.doDuplicate)
797 EVT_MENU(self, menu_EDIT_TEXT, self.doEditText)
798 EVT_MENU(self, menu_DELETE, self.doDelete)
799 EVT_MENU(self, menu_MOVE_FORWARD, self.doMoveForward)
800 EVT_MENU(self, menu_MOVE_TO_FRONT, self.doMoveToFront)
801 EVT_MENU(self, menu_MOVE_BACKWARD, self.doMoveBackward)
802 EVT_MENU(self, menu_MOVE_TO_BACK, self.doMoveToBack)
803
804 # Show the pop-up menu.
805
806 clickPt = wx.Point(mousePt.x + self.drawPanel.GetPosition().x,
807 mousePt.y + self.drawPanel.GetPosition().y)
808 self.drawPanel.PopupMenu(menu, clickPt)
809 menu.Destroy()
810
811
812 def onPaintEvent(self, event):
813 """ Respond to a request to redraw the contents of our drawing panel.
814 """
815 dc = wx.PaintDC(self.drawPanel)
816 self.drawPanel.PrepareDC(dc)
817 dc.BeginDrawing()
818
819 for i in range(len(self.contents)-1, -1, -1):
820 obj = self.contents[i]
821 if obj in self.selection:
822 obj.draw(dc, True)
823 else:
824 obj.draw(dc, False)
825
826 dc.EndDrawing()
827
828 # ==========================
829 # == Menu Command Methods ==
830 # ==========================
831
832 def doNew(self, event):
833 """ Respond to the "New" menu command.
834 """
835 global _docList
836 newFrame = DrawingFrame(None, -1, "Untitled")
837 newFrame.Show(True)
838 _docList.append(newFrame)
839
840
841 def doOpen(self, event):
842 """ Respond to the "Open" menu command.
843 """
844 global _docList
845
846 curDir = os.getcwd()
847 fileName = wx.FileSelector("Open File", default_extension="psk",
848 flags = wx.OPEN | wx.FILE_MUST_EXIST)
849 if fileName == "": return
850 fileName = os.path.join(os.getcwd(), fileName)
851 os.chdir(curDir)
852
853 title = os.path.basename(fileName)
854
855 if (self.fileName == None) and (len(self.contents) == 0):
856 # Load contents into current (empty) document.
857 self.fileName = fileName
858 self.SetTitle(os.path.basename(fileName))
859 self.loadContents()
860 else:
861 # Open a new frame for this document.
862 newFrame = DrawingFrame(None, -1, os.path.basename(fileName),
863 fileName=fileName)
864 newFrame.Show(True)
865 _docList.append(newFrame)
866
867
868 def doClose(self, event):
869 """ Respond to the "Close" menu command.
870 """
871 global _docList
872
873 if self.dirty:
874 if not self.askIfUserWantsToSave("closing"): return
875
876 _docList.remove(self)
877 self.Destroy()
878
879
880 def doSave(self, event):
881 """ Respond to the "Save" menu command.
882 """
883 if self.fileName != None:
884 self.saveContents()
885
886
887 def doSaveAs(self, event):
888 """ Respond to the "Save As" menu command.
889 """
890 if self.fileName == None:
891 default = ""
892 else:
893 default = self.fileName
894
895 curDir = os.getcwd()
896 fileName = wx.FileSelector("Save File As", "Saving",
897 default_filename=default,
898 default_extension="psk",
899 wildcard="*.psk",
900 flags = wx.SAVE | wx.OVERWRITE_PROMPT)
901 if fileName == "": return # User cancelled.
902 fileName = os.path.join(os.getcwd(), fileName)
903 os.chdir(curDir)
904
905 title = os.path.basename(fileName)
906 self.SetTitle(title)
907
908 self.fileName = fileName
909 self.saveContents()
910
911
912 def doRevert(self, event):
913 """ Respond to the "Revert" menu command.
914 """
915 if not self.dirty: return
916
917 if wx.MessageBox("Discard changes made to this document?", "Confirm",
918 style = wx.OK | wx.CANCEL | wx.ICON_QUESTION,
919 parent=self) == wx.CANCEL: return
920 self.loadContents()
921
922
923 def doExit(self, event):
924 """ Respond to the "Quit" menu command.
925 """
926 global _docList, _app
927 for doc in _docList:
928 if not doc.dirty: continue
929 doc.Raise()
930 if not doc.askIfUserWantsToSave("quitting"): return
931 _docList.remove(doc)
932 doc.Destroy()
933
934 _app.ExitMainLoop()
935
936
937 def doUndo(self, event):
938 """ Respond to the "Undo" menu command.
939 """
940 if self.undoInfo == None: return
941
942 undoData = self.undoInfo
943 self._saveUndoInfo() # For undoing the undo...
944
945 self.contents = []
946
947 for type, data in undoData["contents"]:
948 obj = DrawingObject(type)
949 obj.setData(data)
950 self.contents.append(obj)
951
952 self.selection = []
953 for i in undoData["selection"]:
954 self.selection.append(self.contents[i])
955
956 self.dirty = True
957 self.drawPanel.Refresh()
958 self._adjustMenus()
959
960
961 def doSelectAll(self, event):
962 """ Respond to the "Select All" menu command.
963 """
964 self.selectAll()
965
966
967 def doDuplicate(self, event):
968 """ Respond to the "Duplicate" menu command.
969 """
970 self._saveUndoInfo()
971
972 objs = []
973 for obj in self.contents:
974 if obj in self.selection:
975 newObj = DrawingObject(obj.getType())
976 newObj.setData(obj.getData())
977 pos = obj.getPosition()
978 newObj.setPosition(wx.Point(pos.x + 10, pos.y + 10))
979 objs.append(newObj)
980
981 self.contents = objs + self.contents
982
983 self.selectMany(objs)
984
985
986 def doEditText(self, event):
987 """ Respond to the "Edit Text" menu command.
988 """
989 if len(self.selection) != 1: return
990
991 obj = self.selection[0]
992 if obj.getType() != obj_TEXT: return
993
994 editor = EditTextObjectDialog(self, "Edit Text Object")
995 editor.objectToDialog(obj)
996 if editor.ShowModal() == wx.ID_CANCEL:
997 editor.Destroy()
998 return
999
1000 self._saveUndoInfo()
1001
1002 editor.dialogToObject(obj)
1003 editor.Destroy()
1004
1005 self.dirty = True
1006 self.drawPanel.Refresh()
1007 self._adjustMenus()
1008
1009
1010 def doDelete(self, event):
1011 """ Respond to the "Delete" menu command.
1012 """
1013 self._saveUndoInfo()
1014
1015 for obj in self.selection:
1016 self.contents.remove(obj)
1017 del obj
1018 self.deselectAll()
1019
1020
1021 def doChooseSelectTool(self, event=None):
1022 """ Respond to the "Select Tool" menu command.
1023 """
1024 self._setCurrentTool(self.selectIcon)
1025 self.drawPanel.SetCursor(wx.STANDARD_CURSOR)
1026 self._adjustMenus()
1027
1028
1029 def doChooseLineTool(self, event=None):
1030 """ Respond to the "Line Tool" menu command.
1031 """
1032 self._setCurrentTool(self.lineIcon)
1033 self.drawPanel.SetCursor(wx.CROSS_CURSOR)
1034 self.deselectAll()
1035 self._adjustMenus()
1036
1037
1038 def doChooseRectTool(self, event=None):
1039 """ Respond to the "Rect Tool" menu command.
1040 """
1041 self._setCurrentTool(self.rectIcon)
1042 self.drawPanel.SetCursor(wx.CROSS_CURSOR)
1043 self.deselectAll()
1044 self._adjustMenus()
1045
1046
1047 def doChooseEllipseTool(self, event=None):
1048 """ Respond to the "Ellipse Tool" menu command.
1049 """
1050 self._setCurrentTool(self.ellipseIcon)
1051 self.drawPanel.SetCursor(wx.CROSS_CURSOR)
1052 self.deselectAll()
1053 self._adjustMenus()
1054
1055
1056 def doChooseTextTool(self, event=None):
1057 """ Respond to the "Text Tool" menu command.
1058 """
1059 self._setCurrentTool(self.textIcon)
1060 self.drawPanel.SetCursor(wx.CROSS_CURSOR)
1061 self.deselectAll()
1062 self._adjustMenus()
1063
1064
1065 def doMoveForward(self, event):
1066 """ Respond to the "Move Forward" menu command.
1067 """
1068 if len(self.selection) != 1: return
1069
1070 self._saveUndoInfo()
1071
1072 obj = self.selection[0]
1073 index = self.contents.index(obj)
1074 if index == 0: return
1075
1076 del self.contents[index]
1077 self.contents.insert(index-1, obj)
1078
1079 self.drawPanel.Refresh()
1080 self._adjustMenus()
1081
1082
1083 def doMoveToFront(self, event):
1084 """ Respond to the "Move to Front" menu command.
1085 """
1086 if len(self.selection) != 1: return
1087
1088 self._saveUndoInfo()
1089
1090 obj = self.selection[0]
1091 self.contents.remove(obj)
1092 self.contents.insert(0, obj)
1093
1094 self.drawPanel.Refresh()
1095 self._adjustMenus()
1096
1097
1098 def doMoveBackward(self, event):
1099 """ Respond to the "Move Backward" menu command.
1100 """
1101 if len(self.selection) != 1: return
1102
1103 self._saveUndoInfo()
1104
1105 obj = self.selection[0]
1106 index = self.contents.index(obj)
1107 if index == len(self.contents) - 1: return
1108
1109 del self.contents[index]
1110 self.contents.insert(index+1, obj)
1111
1112 self.drawPanel.Refresh()
1113 self._adjustMenus()
1114
1115
1116 def doMoveToBack(self, event):
1117 """ Respond to the "Move to Back" menu command.
1118 """
1119 if len(self.selection) != 1: return
1120
1121 self._saveUndoInfo()
1122
1123 obj = self.selection[0]
1124 self.contents.remove(obj)
1125 self.contents.append(obj)
1126
1127 self.drawPanel.Refresh()
1128 self._adjustMenus()
1129
1130
1131 def doShowAbout(self, event):
1132 """ Respond to the "About pySketch" menu command.
1133 """
1134 dialog = wx.Dialog(self, -1, "About pySketch") # ,
1135 #style=wx.DIALOG_MODAL | wx.STAY_ON_TOP)
1136 dialog.SetBackgroundColour(wx.WHITE)
1137
1138 panel = wx.Panel(dialog, -1)
1139 panel.SetBackgroundColour(wx.WHITE)
1140
1141 panelSizer = wx.BoxSizer(wx.VERTICAL)
1142
1143 boldFont = wx.Font(panel.GetFont().GetPointSize(),
1144 panel.GetFont().GetFamily(),
1145 wx.NORMAL, wx.BOLD)
1146
1147 logo = wx.StaticBitmap(panel, -1, wx.Bitmap("images/logo.bmp",
1148 wx.BITMAP_TYPE_BMP))
1149
1150 lab1 = wx.StaticText(panel, -1, "pySketch")
1151 lab1.SetFont(wx.Font(36, boldFont.GetFamily(), wx.ITALIC, wx.BOLD))
1152 lab1.SetSize(lab1.GetBestSize())
1153
1154 imageSizer = wx.BoxSizer(wx.HORIZONTAL)
1155 imageSizer.Add(logo, 0, wx.ALL | wx.ALIGN_CENTRE_VERTICAL, 5)
1156 imageSizer.Add(lab1, 0, wx.ALL | wx.ALIGN_CENTRE_VERTICAL, 5)
1157
1158 lab2 = wx.StaticText(panel, -1, "A simple object-oriented drawing " + \
1159 "program.")
1160 lab2.SetFont(boldFont)
1161 lab2.SetSize(lab2.GetBestSize())
1162
1163 lab3 = wx.StaticText(panel, -1, "pySketch is completely free " + \
1164 "software; please")
1165 lab3.SetFont(boldFont)
1166 lab3.SetSize(lab3.GetBestSize())
1167
1168 lab4 = wx.StaticText(panel, -1, "feel free to adapt or use this " + \
1169 "in any way you like.")
1170 lab4.SetFont(boldFont)
1171 lab4.SetSize(lab4.GetBestSize())
1172
1173 lab5 = wx.StaticText(panel, -1, "Author: Erik Westra " + \
1174 "(ewestra@wave.co.nz)")
1175 lab5.SetFont(boldFont)
1176 lab5.SetSize(lab5.GetBestSize())
1177
1178 btnOK = wx.Button(panel, wx.ID_OK, "OK")
1179
1180 panelSizer.Add(imageSizer, 0, wx.ALIGN_CENTRE)
1181 panelSizer.Add((10, 10)) # Spacer.
1182 panelSizer.Add(lab2, 0, wx.ALIGN_CENTRE)
1183 panelSizer.Add((10, 10)) # Spacer.
1184 panelSizer.Add(lab3, 0, wx.ALIGN_CENTRE)
1185 panelSizer.Add(lab4, 0, wx.ALIGN_CENTRE)
1186 panelSizer.Add((10, 10)) # Spacer.
1187 panelSizer.Add(lab5, 0, wx.ALIGN_CENTRE)
1188 panelSizer.Add((10, 10)) # Spacer.
1189 panelSizer.Add(btnOK, 0, wx.ALL | wx.ALIGN_CENTRE, 5)
1190
1191 panel.SetAutoLayout(True)
1192 panel.SetSizer(panelSizer)
1193 panelSizer.Fit(panel)
1194
1195 topSizer = wx.BoxSizer(wx.HORIZONTAL)
1196 topSizer.Add(panel, 0, wx.ALL, 10)
1197
1198 dialog.SetAutoLayout(True)
1199 dialog.SetSizer(topSizer)
1200 topSizer.Fit(dialog)
1201
1202 dialog.Centre()
1203
1204 btn = dialog.ShowModal()
1205 dialog.Destroy()
1206
1207 # =============================
1208 # == Object Creation Methods ==
1209 # =============================
1210
1211 def createLine(self, x1, y1, x2, y2):
1212 """ Create a new line object at the given position and size.
1213 """
1214 self._saveUndoInfo()
1215
1216 topLeftX = min(x1, x2)
1217 topLeftY = min(y1, y2)
1218 botRightX = max(x1, x2)
1219 botRightY = max(y1, y2)
1220
1221 obj = DrawingObject(obj_LINE, position=wx.Point(topLeftX, topLeftY),
1222 size=wx.Size(botRightX-topLeftX,
1223 botRightY-topLeftY),
1224 penColour=self.penColour,
1225 fillColour=self.fillColour,
1226 lineSize=self.lineSize,
1227 startPt = wx.Point(x1 - topLeftX, y1 - topLeftY),
1228 endPt = wx.Point(x2 - topLeftX, y2 - topLeftY))
1229 self.contents.insert(0, obj)
1230 self.dirty = True
1231 self.doChooseSelectTool()
1232 self.select(obj)
1233
1234
1235 def createRect(self, x, y, width, height):
1236 """ Create a new rectangle object at the given position and size.
1237 """
1238 self._saveUndoInfo()
1239
1240 obj = DrawingObject(obj_RECT, position=wx.Point(x, y),
1241 size=wx.Size(width, height),
1242 penColour=self.penColour,
1243 fillColour=self.fillColour,
1244 lineSize=self.lineSize)
1245 self.contents.insert(0, obj)
1246 self.dirty = True
1247 self.doChooseSelectTool()
1248 self.select(obj)
1249
1250
1251 def createEllipse(self, x, y, width, height):
1252 """ Create a new ellipse object at the given position and size.
1253 """
1254 self._saveUndoInfo()
1255
1256 obj = DrawingObject(obj_ELLIPSE, position=wx.Point(x, y),
1257 size=wx.Size(width, height),
1258 penColour=self.penColour,
1259 fillColour=self.fillColour,
1260 lineSize=self.lineSize)
1261 self.contents.insert(0, obj)
1262 self.dirty = True
1263 self.doChooseSelectTool()
1264 self.select(obj)
1265
1266
1267 def createText(self, x, y, width, height):
1268 """ Create a new text object at the given position and size.
1269 """
1270 editor = EditTextObjectDialog(self, "Create Text Object")
1271 if editor.ShowModal() == wx.ID_CANCEL:
1272 editor.Destroy()
1273 return
1274
1275 self._saveUndoInfo()
1276
1277 obj = DrawingObject(obj_TEXT, position=wx.Point(x, y),
1278 size=wx.Size(width, height))
1279 editor.dialogToObject(obj)
1280 editor.Destroy()
1281
1282 self.contents.insert(0, obj)
1283 self.dirty = True
1284 self.doChooseSelectTool()
1285 self.select(obj)
1286
1287 # =======================
1288 # == Selection Methods ==
1289 # =======================
1290
1291 def selectAll(self):
1292 """ Select every DrawingObject in our document.
1293 """
1294 self.selection = []
1295 for obj in self.contents:
1296 self.selection.append(obj)
1297 self.drawPanel.Refresh()
1298 self._adjustMenus()
1299
1300
1301 def deselectAll(self):
1302 """ Deselect every DrawingObject in our document.
1303 """
1304 self.selection = []
1305 self.drawPanel.Refresh()
1306 self._adjustMenus()
1307
1308
1309 def select(self, obj):
1310 """ Select the given DrawingObject within our document.
1311 """
1312 self.selection = [obj]
1313 self.drawPanel.Refresh()
1314 self._adjustMenus()
1315
1316
1317 def selectMany(self, objs):
1318 """ Select the given list of DrawingObjects.
1319 """
1320 self.selection = objs
1321 self.drawPanel.Refresh()
1322 self._adjustMenus()
1323
1324
1325 def selectByRectangle(self, x, y, width, height):
1326 """ Select every DrawingObject in the given rectangular region.
1327 """
1328 self.selection = []
1329 for obj in self.contents:
1330 if obj.objectWithinRect(x, y, width, height):
1331 self.selection.append(obj)
1332 self.drawPanel.Refresh()
1333 self._adjustMenus()
1334
1335 # ======================
1336 # == File I/O Methods ==
1337 # ======================
1338
1339 def loadContents(self):
1340 """ Load the contents of our document into memory.
1341 """
1342 f = open(self.fileName, "rb")
1343 objData = cPickle.load(f)
1344 f.close()
1345
1346 for type, data in objData:
1347 obj = DrawingObject(type)
1348 obj.setData(data)
1349 self.contents.append(obj)
1350
1351 self.dirty = False
1352 self.selection = []
1353 self.undoInfo = None
1354
1355 self.drawPanel.Refresh()
1356 self._adjustMenus()
1357
1358
1359 def saveContents(self):
1360 """ Save the contents of our document to disk.
1361 """
1362 objData = []
1363 for obj in self.contents:
1364 objData.append([obj.getType(), obj.getData()])
1365
1366 f = open(self.fileName, "wb")
1367 cPickle.dump(objData, f)
1368 f.close()
1369
1370 self.dirty = False
1371
1372
1373 def askIfUserWantsToSave(self, action):
1374 """ Give the user the opportunity to save the current document.
1375
1376 'action' is a string describing the action about to be taken. If
1377 the user wants to save the document, it is saved immediately. If
1378 the user cancels, we return False.
1379 """
1380 if not self.dirty: return True # Nothing to do.
1381
1382 response = wx.MessageBox("Save changes before " + action + "?",
1383 "Confirm", wx.YES_NO | wx.CANCEL, self)
1384
1385 if response == wx.YES:
1386 if self.fileName == None:
1387 fileName = wx.FileSelector("Save File As", "Saving",
1388 default_extension="psk",
1389 wildcard="*.psk",
1390 flags = wx.SAVE | wx.OVERWRITE_PROMPT)
1391 if fileName == "": return False # User cancelled.
1392 self.fileName = fileName
1393
1394 self.saveContents()
1395 return True
1396 elif response == wx.NO:
1397 return True # User doesn't want changes saved.
1398 elif response == wx.CANCEL:
1399 return False # User cancelled.
1400
1401 # =====================
1402 # == Private Methods ==
1403 # =====================
1404
1405 def _adjustMenus(self):
1406 """ Adjust our menus and toolbar to reflect the current state of the
1407 world.
1408 """
1409 canSave = (self.fileName != None) and self.dirty
1410 canRevert = (self.fileName != None) and self.dirty
1411 canUndo = self.undoInfo != None
1412 selection = len(self.selection) > 0
1413 onlyOne = len(self.selection) == 1
1414 isText = onlyOne and (self.selection[0].getType() == obj_TEXT)
1415 front = onlyOne and (self.selection[0] == self.contents[0])
1416 back = onlyOne and (self.selection[0] == self.contents[-1])
1417
1418 # Enable/disable our menu items.
1419
1420 self.fileMenu.Enable(wx.ID_SAVE, canSave)
1421 self.fileMenu.Enable(wx.ID_REVERT, canRevert)
1422
1423 self.editMenu.Enable(menu_UNDO, canUndo)
1424 self.editMenu.Enable(menu_DUPLICATE, selection)
1425 self.editMenu.Enable(menu_EDIT_TEXT, isText)
1426 self.editMenu.Enable(menu_DELETE, selection)
1427
1428 self.toolsMenu.Check(menu_SELECT, self.curTool == self.selectIcon)
1429 self.toolsMenu.Check(menu_LINE, self.curTool == self.lineIcon)
1430 self.toolsMenu.Check(menu_RECT, self.curTool == self.rectIcon)
1431 self.toolsMenu.Check(menu_ELLIPSE, self.curTool == self.ellipseIcon)
1432 self.toolsMenu.Check(menu_TEXT, self.curTool == self.textIcon)
1433
1434 self.objectMenu.Enable(menu_MOVE_FORWARD, onlyOne and not front)
1435 self.objectMenu.Enable(menu_MOVE_TO_FRONT, onlyOne and not front)
1436 self.objectMenu.Enable(menu_MOVE_BACKWARD, onlyOne and not back)
1437 self.objectMenu.Enable(menu_MOVE_TO_BACK, onlyOne and not back)
1438
1439 # Enable/disable our toolbar icons.
1440
1441 self.toolbar.EnableTool(wx.ID_NEW, True)
1442 self.toolbar.EnableTool(wx.ID_OPEN, True)
1443 self.toolbar.EnableTool(wx.ID_SAVE, canSave)
1444 self.toolbar.EnableTool(menu_UNDO, canUndo)
1445 self.toolbar.EnableTool(menu_DUPLICATE, selection)
1446 self.toolbar.EnableTool(menu_MOVE_FORWARD, onlyOne and not front)
1447 self.toolbar.EnableTool(menu_MOVE_BACKWARD, onlyOne and not back)
1448
1449
1450 def _setCurrentTool(self, newToolIcon):
1451 """ Set the currently selected tool.
1452 """
1453 if self.curTool == newToolIcon: return # Nothing to do.
1454
1455 if self.curTool != None:
1456 self.curTool.deselect()
1457
1458 newToolIcon.select()
1459 self.curTool = newToolIcon
1460
1461
1462 def _setPenColour(self, colour):
1463 """ Set the default or selected object's pen colour.
1464 """
1465 if len(self.selection) > 0:
1466 self._saveUndoInfo()
1467 for obj in self.selection:
1468 obj.setPenColour(colour)
1469 self.drawPanel.Refresh()
1470 else:
1471 self.penColour = colour
1472 self.optionIndicator.setPenColour(colour)
1473
1474
1475 def _setFillColour(self, colour):
1476 """ Set the default or selected object's fill colour.
1477 """
1478 if len(self.selection) > 0:
1479 self._saveUndoInfo()
1480 for obj in self.selection:
1481 obj.setFillColour(colour)
1482 self.drawPanel.Refresh()
1483 else:
1484 self.fillColour = colour
1485 self.optionIndicator.setFillColour(colour)
1486
1487
1488 def _setLineSize(self, size):
1489 """ Set the default or selected object's line size.
1490 """
1491 if len(self.selection) > 0:
1492 self._saveUndoInfo()
1493 for obj in self.selection:
1494 obj.setLineSize(size)
1495 self.drawPanel.Refresh()
1496 else:
1497 self.lineSize = size
1498 self.optionIndicator.setLineSize(size)
1499
1500
1501 def _saveUndoInfo(self):
1502 """ Remember the current state of the document, to allow for undo.
1503
1504 We make a copy of the document's contents, so that we can return to
1505 the previous contents if the user does something and then wants to
1506 undo the operation.
1507 """
1508 savedContents = []
1509 for obj in self.contents:
1510 savedContents.append([obj.getType(), obj.getData()])
1511
1512 savedSelection = []
1513 for i in range(len(self.contents)):
1514 if self.contents[i] in self.selection:
1515 savedSelection.append(i)
1516
1517 self.undoInfo = {"contents" : savedContents,
1518 "selection" : savedSelection}
1519
1520
1521 def _resizeObject(self, obj, anchorPt, oldPt, newPt):
1522 """ Resize the given object.
1523
1524 'anchorPt' is the unchanging corner of the object, while the
1525 opposite corner has been resized. 'oldPt' are the current
1526 coordinates for this corner, while 'newPt' are the new coordinates.
1527 The object should fit within the given dimensions, though if the
1528 new point is less than the anchor point the object will need to be
1529 moved as well as resized, to avoid giving it a negative size.
1530 """
1531 if obj.getType() == obj_TEXT:
1532 # Not allowed to resize text objects -- they're sized to fit text.
1533 wx.Bell(); print "4"
1534 return
1535
1536 self._saveUndoInfo()
1537
1538 topLeft = wx.Point(min(anchorPt.x, newPt.x),
1539 min(anchorPt.y, newPt.y))
1540 botRight = wx.Point(max(anchorPt.x, newPt.x),
1541 max(anchorPt.y, newPt.y))
1542
1543 newWidth = botRight.x - topLeft.x
1544 newHeight = botRight.y - topLeft.y
1545
1546 if obj.getType() == obj_LINE:
1547 # Adjust the line so that its start and end points match the new
1548 # overall object size.
1549
1550 startPt = obj.getStartPt()
1551 endPt = obj.getEndPt()
1552
1553 slopesDown = ((startPt.x < endPt.x) and (startPt.y < endPt.y)) or \
1554 ((startPt.x > endPt.x) and (startPt.y > endPt.y))
1555
1556 # Handle the user flipping the line.
1557
1558 hFlip = ((anchorPt.x < oldPt.x) and (anchorPt.x > newPt.x)) or \
1559 ((anchorPt.x > oldPt.x) and (anchorPt.x < newPt.x))
1560 vFlip = ((anchorPt.y < oldPt.y) and (anchorPt.y > newPt.y)) or \
1561 ((anchorPt.y > oldPt.y) and (anchorPt.y < newPt.y))
1562
1563 if (hFlip and not vFlip) or (vFlip and not hFlip):
1564 slopesDown = not slopesDown # Line flipped.
1565
1566 if slopesDown:
1567 obj.setStartPt(wx.Point(0, 0))
1568 obj.setEndPt(wx.Point(newWidth, newHeight))
1569 else:
1570 obj.setStartPt(wx.Point(0, newHeight))
1571 obj.setEndPt(wx.Point(newWidth, 0))
1572
1573 # Finally, adjust the bounds of the object to match the new dimensions.
1574
1575 obj.setPosition(topLeft)
1576 obj.setSize(wx.Size(botRight.x - topLeft.x, botRight.y - topLeft.y))
1577
1578 self.drawPanel.Refresh()
1579
1580
1581 def _moveObject(self, offsetX, offsetY):
1582 """ Move the currently selected object(s) by the given offset.
1583 """
1584 self._saveUndoInfo()
1585
1586 for obj in self.selection:
1587 pos = obj.getPosition()
1588 pos.x = pos.x + offsetX
1589 pos.y = pos.y + offsetY
1590 obj.setPosition(pos)
1591
1592 self.drawPanel.Refresh()
1593
1594
1595 def _buildLineSizePopup(self, lineSize):
1596 """ Build the pop-up menu used to set the line size.
1597
1598 'lineSize' is the current line size value. The corresponding item
1599 is checked in the pop-up menu.
1600 """
1601 menu = wx.Menu()
1602 menu.Append(id_LINESIZE_0, "no line", kind=wx.ITEM_CHECK)
1603 menu.Append(id_LINESIZE_1, "1-pixel line", kind=wx.ITEM_CHECK)
1604 menu.Append(id_LINESIZE_2, "2-pixel line", kind=wx.ITEM_CHECK)
1605 menu.Append(id_LINESIZE_3, "3-pixel line", kind=wx.ITEM_CHECK)
1606 menu.Append(id_LINESIZE_4, "4-pixel line", kind=wx.ITEM_CHECK)
1607 menu.Append(id_LINESIZE_5, "5-pixel line", kind=wx.ITEM_CHECK)
1608
1609 if lineSize == 0: menu.Check(id_LINESIZE_0, True)
1610 elif lineSize == 1: menu.Check(id_LINESIZE_1, True)
1611 elif lineSize == 2: menu.Check(id_LINESIZE_2, True)
1612 elif lineSize == 3: menu.Check(id_LINESIZE_3, True)
1613 elif lineSize == 4: menu.Check(id_LINESIZE_4, True)
1614 elif lineSize == 5: menu.Check(id_LINESIZE_5, True)
1615
1616 self.Bind(wx.EVT_MENU, self._lineSizePopupSelected, id=id_LINESIZE_0, id2=id_LINESIZE_5)
1617
1618 return menu
1619
1620
1621 def _lineSizePopupSelected(self, event):
1622 """ Respond to the user selecting an item from the line size popup menu
1623 """
1624 id = event.GetId()
1625 if id == id_LINESIZE_0: self._setLineSize(0)
1626 elif id == id_LINESIZE_1: self._setLineSize(1)
1627 elif id == id_LINESIZE_2: self._setLineSize(2)
1628 elif id == id_LINESIZE_3: self._setLineSize(3)
1629 elif id == id_LINESIZE_4: self._setLineSize(4)
1630 elif id == id_LINESIZE_5: self._setLineSize(5)
1631 else:
1632 wx.Bell(); print "5"
1633 return
1634
1635 self.optionIndicator.setLineSize(self.lineSize)
1636
1637
1638 def _getEventCoordinates(self, event):
1639 """ Return the coordinates associated with the given mouse event.
1640
1641 The coordinates have to be adjusted to allow for the current scroll
1642 position.
1643 """
1644 originX, originY = self.drawPanel.GetViewStart()
1645 unitX, unitY = self.drawPanel.GetScrollPixelsPerUnit()
1646 return wx.Point(event.GetX() + (originX * unitX),
1647 event.GetY() + (originY * unitY))
1648
1649
1650 def _getObjectAndSelectionHandleAt(self, pt):
1651 """ Return the object and selection handle at the given point.
1652
1653 We draw selection handles (small rectangles) around the currently
1654 selected object(s). If the given point is within one of the
1655 selection handle rectangles, we return the associated object and a
1656 code indicating which selection handle the point is in. If the
1657 point isn't within any selection handle at all, we return the tuple
1658 (None, handle_NONE).
1659 """
1660 for obj in self.selection:
1661 handle = obj.getSelectionHandleContainingPoint(pt.x, pt.y)
1662 if handle != handle_NONE:
1663 return obj, handle
1664
1665 return None, handle_NONE
1666
1667
1668 def _getObjectAt(self, pt):
1669 """ Return the first object found which is at the given point.
1670 """
1671 for obj in self.contents:
1672 if obj.objectContainsPoint(pt.x, pt.y):
1673 return obj
1674 return None
1675
1676
1677 def _drawObjectOutline(self, offsetX, offsetY):
1678 """ Draw an outline of the currently selected object.
1679
1680 The selected object's outline is drawn at the object's position
1681 plus the given offset.
1682
1683 Note that the outline is drawn by *inverting* the window's
1684 contents, so calling _drawObjectOutline twice in succession will
1685 restore the window's contents back to what they were previously.
1686 """
1687 if len(self.selection) != 1: return
1688
1689 position = self.selection[0].getPosition()
1690 size = self.selection[0].getSize()
1691
1692 dc = wx.ClientDC(self.drawPanel)
1693 self.drawPanel.PrepareDC(dc)
1694 dc.BeginDrawing()
1695 dc.SetPen(wx.BLACK_DASHED_PEN)
1696 dc.SetBrush(wx.TRANSPARENT_BRUSH)
1697 dc.SetLogicalFunction(wx.INVERT)
1698
1699 dc.DrawRectangle(position.x + offsetX, position.y + offsetY,
1700 size.width, size.height)
1701
1702 dc.EndDrawing()
1703
1704
1705 def _drawVisualFeedback(self, startPt, endPt, type, dashedLine):
1706 """ Draw visual feedback for a drawing operation.
1707
1708 The visual feedback consists of a line, ellipse, or rectangle based
1709 around the two given points. 'type' should be one of the following
1710 predefined feedback type constants:
1711
1712 feedback_RECT -> draw rectangular feedback.
1713 feedback_LINE -> draw line feedback.
1714 feedback_ELLIPSE -> draw elliptical feedback.
1715
1716 if 'dashedLine' is True, the feedback is drawn as a dashed rather
1717 than a solid line.
1718
1719 Note that the feedback is drawn by *inverting* the window's
1720 contents, so calling _drawVisualFeedback twice in succession will
1721 restore the window's contents back to what they were previously.
1722 """
1723 dc = wx.ClientDC(self.drawPanel)
1724 self.drawPanel.PrepareDC(dc)
1725 dc.BeginDrawing()
1726 if dashedLine:
1727 dc.SetPen(wx.BLACK_DASHED_PEN)
1728 else:
1729 dc.SetPen(wx.BLACK_PEN)
1730 dc.SetBrush(wx.TRANSPARENT_BRUSH)
1731 dc.SetLogicalFunction(wx.INVERT)
1732
1733 if type == feedback_RECT:
1734 dc.DrawRectangle(startPt.x, startPt.y,
1735 endPt.x - startPt.x,
1736 endPt.y - startPt.y)
1737 elif type == feedback_LINE:
1738 dc.DrawLine(startPt.x, startPt.y, endPt.x, endPt.y)
1739 elif type == feedback_ELLIPSE:
1740 dc.DrawEllipse(startPt.x, startPt.y,
1741 endPt.x - startPt.x,
1742 endPt.y - startPt.y)
1743
1744 dc.EndDrawing()
1745
1746 #----------------------------------------------------------------------------
1747
1748 class DrawingObject:
1749 """ An object within the drawing panel.
1750
1751 A pySketch document consists of a front-to-back ordered list of
1752 DrawingObjects. Each DrawingObject has the following properties:
1753
1754 'type' What type of object this is (text, line, etc).
1755 'position' The position of the object within the document.
1756 'size' The size of the object within the document.
1757 'penColour' The colour to use for drawing the object's outline.
1758 'fillColour' Colour to use for drawing object's interior.
1759 'lineSize' Line width (in pixels) to use for object's outline.
1760 'startPt' The point, relative to the object's position, where
1761 an obj_LINE object's line should start.
1762 'endPt' The point, relative to the object's position, where
1763 an obj_LINE object's line should end.
1764 'text' The object's text (obj_TEXT objects only).
1765 'textFont' The text object's font name.
1766 'textSize' The text object's point size.
1767 'textBoldface' If True, this text object will be drawn in
1768 boldface.
1769 'textItalic' If True, this text object will be drawn in italic.
1770 'textUnderline' If True, this text object will be drawn underlined.
1771 """
1772
1773 # ==================
1774 # == Constructors ==
1775 # ==================
1776
1777 def __init__(self, type, position=wx.Point(0, 0), size=wx.Size(0, 0),
1778 penColour=wx.BLACK, fillColour=wx.WHITE, lineSize=1,
1779 text=None, startPt=wx.Point(0, 0), endPt=wx.Point(0,0)):
1780 """ Standard constructor.
1781
1782 'type' is the type of object being created. This should be one of
1783 the following constants:
1784
1785 obj_LINE
1786 obj_RECT
1787 obj_ELLIPSE
1788 obj_TEXT
1789
1790 The remaining parameters let you set various options for the newly
1791 created DrawingObject.
1792 """
1793 self.type = type
1794 self.position = position
1795 self.size = size
1796 self.penColour = penColour
1797 self.fillColour = fillColour
1798 self.lineSize = lineSize
1799 self.startPt = startPt
1800 self.endPt = endPt
1801 self.text = text
1802 self.textFont = wx.SystemSettings_GetFont(
1803 wx.SYS_DEFAULT_GUI_FONT).GetFaceName()
1804 self.textSize = 12
1805 self.textBoldface = False
1806 self.textItalic = False
1807 self.textUnderline = False
1808
1809 # =============================
1810 # == Object Property Methods ==
1811 # =============================
1812
1813 def getData(self):
1814 """ Return a copy of the object's internal data.
1815
1816 This is used to save this DrawingObject to disk.
1817 """
1818 return [self.type, self.position.x, self.position.y,
1819 self.size.width, self.size.height,
1820 self.penColour.Red(),
1821 self.penColour.Green(),
1822 self.penColour.Blue(),
1823 self.fillColour.Red(),
1824 self.fillColour.Green(),
1825 self.fillColour.Blue(),
1826 self.lineSize,
1827 self.startPt.x, self.startPt.y,
1828 self.endPt.x, self.endPt.y,
1829 self.text,
1830 self.textFont,
1831 self.textSize,
1832 self.textBoldface,
1833 self.textItalic,
1834 self.textUnderline]
1835
1836
1837 def setData(self, data):
1838 """ Set the object's internal data.
1839
1840 'data' is a copy of the object's saved data, as returned by
1841 getData() above. This is used to restore a previously saved
1842 DrawingObject.
1843 """
1844 #data = copy.deepcopy(data) # Needed?
1845
1846 self.type = data[0]
1847 self.position = wx.Point(data[1], data[2])
1848 self.size = wx.Size(data[3], data[4])
1849 self.penColour = wx.Colour(red=data[5],
1850 green=data[6],
1851 blue=data[7])
1852 self.fillColour = wx.Colour(red=data[8],
1853 green=data[9],
1854 blue=data[10])
1855 self.lineSize = data[11]
1856 self.startPt = wx.Point(data[12], data[13])
1857 self.endPt = wx.Point(data[14], data[15])
1858 self.text = data[16]
1859 self.textFont = data[17]
1860 self.textSize = data[18]
1861 self.textBoldface = data[19]
1862 self.textItalic = data[20]
1863 self.textUnderline = data[21]
1864
1865
1866 def getType(self):
1867 """ Return this DrawingObject's type.
1868 """
1869 return self.type
1870
1871
1872 def setPosition(self, position):
1873 """ Set the origin (top-left corner) for this DrawingObject.
1874 """
1875 self.position = position
1876
1877
1878 def getPosition(self):
1879 """ Return this DrawingObject's position.
1880 """
1881 return self.position
1882
1883
1884 def setSize(self, size):
1885 """ Set the size for this DrawingObject.
1886 """
1887 self.size = size
1888
1889
1890 def getSize(self):
1891 """ Return this DrawingObject's size.
1892 """
1893 return self.size
1894
1895
1896 def setPenColour(self, colour):
1897 """ Set the pen colour used for this DrawingObject.
1898 """
1899 self.penColour = colour
1900
1901
1902 def getPenColour(self):
1903 """ Return this DrawingObject's pen colour.
1904 """
1905 return self.penColour
1906
1907
1908 def setFillColour(self, colour):
1909 """ Set the fill colour used for this DrawingObject.
1910 """
1911 self.fillColour = colour
1912
1913
1914 def getFillColour(self):
1915 """ Return this DrawingObject's fill colour.
1916 """
1917 return self.fillColour
1918
1919
1920 def setLineSize(self, lineSize):
1921 """ Set the linesize used for this DrawingObject.
1922 """
1923 self.lineSize = lineSize
1924
1925
1926 def getLineSize(self):
1927 """ Return this DrawingObject's line size.
1928 """
1929 return self.lineSize
1930
1931
1932 def setStartPt(self, startPt):
1933 """ Set the starting point for this line DrawingObject.
1934 """
1935 self.startPt = startPt
1936
1937
1938 def getStartPt(self):
1939 """ Return the starting point for this line DrawingObject.
1940 """
1941 return self.startPt
1942
1943
1944 def setEndPt(self, endPt):
1945 """ Set the ending point for this line DrawingObject.
1946 """
1947 self.endPt = endPt
1948
1949
1950 def getEndPt(self):
1951 """ Return the ending point for this line DrawingObject.
1952 """
1953 return self.endPt
1954
1955
1956 def setText(self, text):
1957 """ Set the text for this DrawingObject.
1958 """
1959 self.text = text
1960
1961
1962 def getText(self):
1963 """ Return this DrawingObject's text.
1964 """
1965 return self.text
1966
1967
1968 def setTextFont(self, font):
1969 """ Set the typeface for this text DrawingObject.
1970 """
1971 self.textFont = font
1972
1973
1974 def getTextFont(self):
1975 """ Return this text DrawingObject's typeface.
1976 """
1977 return self.textFont
1978
1979
1980 def setTextSize(self, size):
1981 """ Set the point size for this text DrawingObject.
1982 """
1983 self.textSize = size
1984
1985
1986 def getTextSize(self):
1987 """ Return this text DrawingObject's text size.
1988 """
1989 return self.textSize
1990
1991
1992 def setTextBoldface(self, boldface):
1993 """ Set the boldface flag for this text DrawingObject.
1994 """
1995 self.textBoldface = boldface
1996
1997
1998 def getTextBoldface(self):
1999 """ Return this text DrawingObject's boldface flag.
2000 """
2001 return self.textBoldface
2002
2003
2004 def setTextItalic(self, italic):
2005 """ Set the italic flag for this text DrawingObject.
2006 """
2007 self.textItalic = italic
2008
2009
2010 def getTextItalic(self):
2011 """ Return this text DrawingObject's italic flag.
2012 """
2013 return self.textItalic
2014
2015
2016 def setTextUnderline(self, underline):
2017 """ Set the underling flag for this text DrawingObject.
2018 """
2019 self.textUnderline = underline
2020
2021
2022 def getTextUnderline(self):
2023 """ Return this text DrawingObject's underline flag.
2024 """
2025 return self.textUnderline
2026
2027 # ============================
2028 # == Object Drawing Methods ==
2029 # ============================
2030
2031 def draw(self, dc, selected):
2032 """ Draw this DrawingObject into our window.
2033
2034 'dc' is the device context to use for drawing. If 'selected' is
2035 True, the object is currently selected and should be drawn as such.
2036 """
2037 if self.type != obj_TEXT:
2038 if self.lineSize == 0:
2039 dc.SetPen(wx.Pen(self.penColour, self.lineSize, wx.TRANSPARENT))
2040 else:
2041 dc.SetPen(wx.Pen(self.penColour, self.lineSize, wx.SOLID))
2042 dc.SetBrush(wx.Brush(self.fillColour, wx.SOLID))
2043 else:
2044 dc.SetTextForeground(self.penColour)
2045 dc.SetTextBackground(self.fillColour)
2046
2047 self._privateDraw(dc, self.position, selected)
2048
2049 # =======================
2050 # == Selection Methods ==
2051 # =======================
2052
2053 def objectContainsPoint(self, x, y):
2054 """ Returns True iff this object contains the given point.
2055
2056 This is used to determine if the user clicked on the object.
2057 """
2058 # Firstly, ignore any points outside of the object's bounds.
2059
2060 if x < self.position.x: return False
2061 if x > self.position.x + self.size.x: return False
2062 if y < self.position.y: return False
2063 if y > self.position.y + self.size.y: return False
2064
2065 if self.type in [obj_RECT, obj_TEXT]:
2066 # Rectangles and text are easy -- they're always selected if the
2067 # point is within their bounds.
2068 return True
2069
2070 # Now things get tricky. There's no straightforward way of knowing
2071 # whether the point is within the object's bounds...to get around this,
2072 # we draw the object into a memory-based bitmap and see if the given
2073 # point was drawn. This could no doubt be done more efficiently by
2074 # some tricky maths, but this approach works and is simple enough.
2075
2076 bitmap = wx.EmptyBitmap(self.size.x + 10, self.size.y + 10)
2077 dc = wx.MemoryDC()
2078 dc.SelectObject(bitmap)
2079 dc.BeginDrawing()
2080 dc.SetBackground(wx.WHITE_BRUSH)
2081 dc.Clear()
2082 dc.SetPen(wx.Pen(wx.BLACK, self.lineSize + 5, wx.SOLID))
2083 dc.SetBrush(wx.BLACK_BRUSH)
2084 self._privateDraw(dc, wx.Point(5, 5), True)
2085 dc.EndDrawing()
2086 pixel = dc.GetPixel(x - self.position.x + 5, y - self.position.y + 5)
2087 if (pixel.Red() == 0) and (pixel.Green() == 0) and (pixel.Blue() == 0):
2088 return True
2089 else:
2090 return False
2091
2092
2093 def getSelectionHandleContainingPoint(self, x, y):
2094 """ Return the selection handle containing the given point, if any.
2095
2096 We return one of the predefined selection handle ID codes.
2097 """
2098 if self.type == obj_LINE:
2099 # We have selection handles at the start and end points.
2100 if self._pointInSelRect(x, y, self.position.x + self.startPt.x,
2101 self.position.y + self.startPt.y):
2102 return handle_START_POINT
2103 elif self._pointInSelRect(x, y, self.position.x + self.endPt.x,
2104 self.position.y + self.endPt.y):
2105 return handle_END_POINT
2106 else:
2107 return handle_NONE
2108 else:
2109 # We have selection handles at all four corners.
2110 if self._pointInSelRect(x, y, self.position.x, self.position.y):
2111 return handle_TOP_LEFT
2112 elif self._pointInSelRect(x, y, self.position.x + self.size.width,
2113 self.position.y):
2114 return handle_TOP_RIGHT
2115 elif self._pointInSelRect(x, y, self.position.x,
2116 self.position.y + self.size.height):
2117 return handle_BOTTOM_LEFT
2118 elif self._pointInSelRect(x, y, self.position.x + self.size.width,
2119 self.position.y + self.size.height):
2120 return handle_BOTTOM_RIGHT
2121 else:
2122 return handle_NONE
2123
2124
2125 def objectWithinRect(self, x, y, width, height):
2126 """ Return True iff this object falls completely within the given rect.
2127 """
2128 if x > self.position.x: return False
2129 if x + width < self.position.x + self.size.width: return False
2130 if y > self.position.y: return False
2131 if y + height < self.position.y + self.size.height: return False
2132 return True
2133
2134 # =====================
2135 # == Utility Methods ==
2136 # =====================
2137
2138 def fitToText(self):
2139 """ Resize a text DrawingObject so that it fits it's text exactly.
2140 """
2141 if self.type != obj_TEXT: return
2142
2143 if self.textBoldface: weight = wx.BOLD
2144 else: weight = wx.NORMAL
2145 if self.textItalic: style = wx.ITALIC
2146 else: style = wx.NORMAL
2147 font = wx.Font(self.textSize, wx.DEFAULT, style, weight,
2148 self.textUnderline, self.textFont)
2149
2150 dummyWindow = wx.Frame(None, -1, "")
2151 dummyWindow.SetFont(font)
2152 width, height = dummyWindow.GetTextExtent(self.text)
2153 dummyWindow.Destroy()
2154
2155 self.size = wx.Size(width, height)
2156
2157 # =====================
2158 # == Private Methods ==
2159 # =====================
2160
2161 def _privateDraw(self, dc, position, selected):
2162 """ Private routine to draw this DrawingObject.
2163
2164 'dc' is the device context to use for drawing, while 'position' is
2165 the position in which to draw the object. If 'selected' is True,
2166 the object is drawn with selection handles. This private drawing
2167 routine assumes that the pen and brush have already been set by the
2168 caller.
2169 """
2170 if self.type == obj_LINE:
2171 dc.DrawLine(position.x + self.startPt.x,
2172 position.y + self.startPt.y,
2173 position.x + self.endPt.x,
2174 position.y + self.endPt.y)
2175 elif self.type == obj_RECT:
2176 dc.DrawRectangle(position.x, position.y,
2177 self.size.width, self.size.height)
2178 elif self.type == obj_ELLIPSE:
2179 dc.DrawEllipse(position.x, position.y,
2180 self.size.width, self.size.height)
2181 elif self.type == obj_TEXT:
2182 if self.textBoldface: weight = wx.BOLD
2183 else: weight = wx.NORMAL
2184 if self.textItalic: style = wx.ITALIC
2185 else: style = wx.NORMAL
2186 font = wx.Font(self.textSize, wx.DEFAULT, style, weight,
2187 self.textUnderline, self.textFont)
2188 dc.SetFont(font)
2189 dc.DrawText(self.text, position.x, position.y)
2190
2191 if selected:
2192 dc.SetPen(wx.TRANSPARENT_PEN)
2193 dc.SetBrush(wx.BLACK_BRUSH)
2194
2195 if self.type == obj_LINE:
2196 # Draw selection handles at the start and end points.
2197 self._drawSelHandle(dc, position.x + self.startPt.x,
2198 position.y + self.startPt.y)
2199 self._drawSelHandle(dc, position.x + self.endPt.x,
2200 position.y + self.endPt.y)
2201 else:
2202 # Draw selection handles at all four corners.
2203 self._drawSelHandle(dc, position.x, position.y)
2204 self._drawSelHandle(dc, position.x + self.size.width,
2205 position.y)
2206 self._drawSelHandle(dc, position.x,
2207 position.y + self.size.height)
2208 self._drawSelHandle(dc, position.x + self.size.width,
2209 position.y + self.size.height)
2210
2211
2212 def _drawSelHandle(self, dc, x, y):
2213 """ Draw a selection handle around this DrawingObject.
2214
2215 'dc' is the device context to draw the selection handle within,
2216 while 'x' and 'y' are the coordinates to use for the centre of the
2217 selection handle.
2218 """
2219 dc.DrawRectangle(x - 3, y - 3, 6, 6)
2220
2221
2222 def _pointInSelRect(self, x, y, rX, rY):
2223 """ Return True iff (x, y) is within the selection handle at (rX, ry).
2224 """
2225 if x < rX - 3: return False
2226 elif x > rX + 3: return False
2227 elif y < rY - 3: return False
2228 elif y > rY + 3: return False
2229 else: return True
2230
2231 #----------------------------------------------------------------------------
2232
2233 class ToolPaletteIcon(GenBitmapButton):
2234 """ An icon appearing in the tool palette area of our sketching window.
2235
2236 Note that this is actually implemented as a wx.Bitmap rather
2237 than as a wx.Icon. wx.Icon has a very specific meaning, and isn't
2238 appropriate for this more general use.
2239 """
2240
2241 def __init__(self, parent, iconID, iconName, toolTip):
2242 """ Standard constructor.
2243
2244 'parent' is the parent window this icon will be part of.
2245 'iconID' is the internal ID used for this icon.
2246 'iconName' is the name used for this icon.
2247 'toolTip' is the tool tip text to show for this icon.
2248
2249 The icon name is used to get the appropriate bitmap for this icon.
2250 """
2251 bmp = wx.Bitmap("images/" + iconName + "Icon.bmp", wx.BITMAP_TYPE_BMP)
2252 GenBitmapButton.__init__(self, parent, iconID, bmp, wx.DefaultPosition,
2253 wx.Size(bmp.GetWidth(), bmp.GetHeight()))
2254 self.SetToolTip(wx.ToolTip(toolTip))
2255
2256 self.iconID = iconID
2257 self.iconName = iconName
2258 self.isSelected = False
2259
2260
2261 def select(self):
2262 """ Select the icon.
2263
2264 The icon's visual representation is updated appropriately.
2265 """
2266 if self.isSelected: return # Nothing to do!
2267
2268 bmp = wx.Bitmap("images/" + self.iconName + "IconSel.bmp",
2269 wx.BITMAP_TYPE_BMP)
2270 self.SetBitmapLabel(bmp)
2271 self.isSelected = True
2272
2273
2274 def deselect(self):
2275 """ Deselect the icon.
2276
2277 The icon's visual representation is updated appropriately.
2278 """
2279 if not self.isSelected: return # Nothing to do!
2280
2281 bmp = wx.Bitmap("images/" + self.iconName + "Icon.bmp",
2282 wx.BITMAP_TYPE_BMP)
2283 self.SetBitmapLabel(bmp)
2284 self.isSelected = False
2285
2286 #----------------------------------------------------------------------------
2287
2288 class ToolOptionIndicator(wx.Window):
2289 """ A visual indicator which shows the current tool options.
2290 """
2291 def __init__(self, parent):
2292 """ Standard constructor.
2293 """
2294 wx.Window.__init__(self, parent, -1, wx.DefaultPosition, wx.Size(52, 32))
2295
2296 self.penColour = wx.BLACK
2297 self.fillColour = wx.WHITE
2298 self.lineSize = 1
2299
2300 self.Bind(wx.EVT_PAINT, self.OnPaint)
2301
2302
2303 def setPenColour(self, penColour):
2304 """ Set the indicator's current pen colour.
2305 """
2306 self.penColour = penColour
2307 self.Refresh()
2308
2309
2310 def setFillColour(self, fillColour):
2311 """ Set the indicator's current fill colour.
2312 """
2313 self.fillColour = fillColour
2314 self.Refresh()
2315
2316
2317 def setLineSize(self, lineSize):
2318 """ Set the indicator's current pen colour.
2319 """
2320 self.lineSize = lineSize
2321 self.Refresh()
2322
2323
2324 def OnPaint(self, event):
2325 """ Paint our tool option indicator.
2326 """
2327 dc = wx.PaintDC(self)
2328 dc.BeginDrawing()
2329
2330 if self.lineSize == 0:
2331 dc.SetPen(wx.Pen(self.penColour, self.lineSize, wx.TRANSPARENT))
2332 else:
2333 dc.SetPen(wx.Pen(self.penColour, self.lineSize, wx.SOLID))
2334 dc.SetBrush(wx.Brush(self.fillColour, wx.SOLID))
2335
2336 dc.DrawRectangle(5, 5, self.GetSize().width - 10,
2337 self.GetSize().height - 10)
2338
2339 dc.EndDrawing()
2340
2341 #----------------------------------------------------------------------------
2342
2343 class EditTextObjectDialog(wx.Dialog):
2344 """ Dialog box used to edit the properties of a text object.
2345
2346 The user can edit the object's text, font, size, and text style.
2347 """
2348
2349 def __init__(self, parent, title):
2350 """ Standard constructor.
2351 """
2352 wx.Dialog.__init__(self, parent, -1, title)
2353
2354 self.textCtrl = wx.TextCtrl(self, 1001, "", style=wx.TE_PROCESS_ENTER,
2355 validator=TextObjectValidator())
2356 extent = self.textCtrl.GetFullTextExtent("Hy")
2357 lineHeight = extent[1] + extent[3]
2358 self.textCtrl.SetSize(wx.Size(-1, lineHeight * 4))
2359
2360 self.Bind(wx.EVT_TEXT_ENTER, self._doEnter, id=1001)
2361
2362 fonts = wx.FontEnumerator()
2363 fonts.EnumerateFacenames()
2364 self.fontList = fonts.GetFacenames()
2365 self.fontList.sort()
2366
2367 fontLabel = wx.StaticText(self, -1, "Font:")
2368 self._setFontOptions(fontLabel, weight=wx.BOLD)
2369
2370 self.fontCombo = wx.ComboBox(self, -1, "", wx.DefaultPosition,
2371 wx.DefaultSize, self.fontList,
2372 style = wx.CB_READONLY)
2373 self.fontCombo.SetSelection(0) # Default to first available font.
2374
2375 self.sizeList = ["8", "9", "10", "12", "14", "16",
2376 "18", "20", "24", "32", "48", "72"]
2377
2378 sizeLabel = wx.StaticText(self, -1, "Size:")
2379 self._setFontOptions(sizeLabel, weight=wx.BOLD)
2380
2381 self.sizeCombo = wx.ComboBox(self, -1, "", wx.DefaultPosition,
2382 wx.DefaultSize, self.sizeList,
2383 style=wx.CB_READONLY)
2384 self.sizeCombo.SetSelection(3) # Default to 12 point text.
2385
2386 gap = wx.LEFT | wx.TOP | wx.RIGHT
2387
2388 comboSizer = wx.BoxSizer(wx.HORIZONTAL)
2389 comboSizer.Add(fontLabel, 0, gap | wx.ALIGN_CENTRE_VERTICAL, 5)
2390 comboSizer.Add(self.fontCombo, 0, gap, 5)
2391 comboSizer.Add((5, 5)) # Spacer.
2392 comboSizer.Add(sizeLabel, 0, gap | wx.ALIGN_CENTRE_VERTICAL, 5)
2393 comboSizer.Add(self.sizeCombo, 0, gap, 5)
2394
2395 self.boldCheckbox = wx.CheckBox(self, -1, "Bold")
2396 self.italicCheckbox = wx.CheckBox(self, -1, "Italic")
2397 self.underlineCheckbox = wx.CheckBox(self, -1, "Underline")
2398
2399 self._setFontOptions(self.boldCheckbox, weight=wx.BOLD)
2400 self._setFontOptions(self.italicCheckbox, style=wx.ITALIC)
2401 self._setFontOptions(self.underlineCheckbox, underline=True)
2402
2403 styleSizer = wx.BoxSizer(wx.HORIZONTAL)
2404 styleSizer.Add(self.boldCheckbox, 0, gap, 5)
2405 styleSizer.Add(self.italicCheckbox, 0, gap, 5)
2406 styleSizer.Add(self.underlineCheckbox, 0, gap, 5)
2407
2408 self.okButton = wx.Button(self, wx.ID_OK, "OK")
2409 self.cancelButton = wx.Button(self, wx.ID_CANCEL, "Cancel")
2410
2411 btnSizer = wx.BoxSizer(wx.HORIZONTAL)
2412 btnSizer.Add(self.okButton, 0, gap, 5)
2413 btnSizer.Add(self.cancelButton, 0, gap, 5)
2414
2415 sizer = wx.BoxSizer(wx.VERTICAL)
2416 sizer.Add(self.textCtrl, 1, gap | wx.EXPAND, 5)
2417 sizer.Add((10, 10)) # Spacer.
2418 sizer.Add(comboSizer, 0, gap | wx.ALIGN_CENTRE, 5)
2419 sizer.Add(styleSizer, 0, gap | wx.ALIGN_CENTRE, 5)
2420 sizer.Add((10, 10)) # Spacer.
2421 sizer.Add(btnSizer, 0, gap | wx.ALIGN_CENTRE, 5)
2422
2423 self.SetAutoLayout(True)
2424 self.SetSizer(sizer)
2425 sizer.Fit(self)
2426
2427 self.textCtrl.SetFocus()
2428
2429
2430 def objectToDialog(self, obj):
2431 """ Copy the properties of the given text object into the dialog box.
2432 """
2433 self.textCtrl.SetValue(obj.getText())
2434 self.textCtrl.SetSelection(0, len(obj.getText()))
2435
2436 for i in range(len(self.fontList)):
2437 if self.fontList[i] == obj.getTextFont():
2438 self.fontCombo.SetSelection(i)
2439 break
2440
2441 for i in range(len(self.sizeList)):
2442 if self.sizeList[i] == str(obj.getTextSize()):
2443 self.sizeCombo.SetSelection(i)
2444 break
2445
2446 self.boldCheckbox.SetValue(obj.getTextBoldface())
2447 self.italicCheckbox.SetValue(obj.getTextItalic())
2448 self.underlineCheckbox.SetValue(obj.getTextUnderline())
2449
2450
2451 def dialogToObject(self, obj):
2452 """ Copy the properties from the dialog box into the given text object.
2453 """
2454 obj.setText(self.textCtrl.GetValue())
2455 obj.setTextFont(self.fontCombo.GetValue())
2456 obj.setTextSize(int(self.sizeCombo.GetValue()))
2457 obj.setTextBoldface(self.boldCheckbox.GetValue())
2458 obj.setTextItalic(self.italicCheckbox.GetValue())
2459 obj.setTextUnderline(self.underlineCheckbox.GetValue())
2460 obj.fitToText()
2461
2462 # ======================
2463 # == Private Routines ==
2464 # ======================
2465
2466 def _setFontOptions(self, ctrl, family=None, pointSize=-1,
2467 style=wx.NORMAL, weight=wx.NORMAL,
2468 underline=False):
2469 """ Change the font settings for the given control.
2470
2471 The meaning of the 'family', 'pointSize', 'style', 'weight' and
2472 'underline' parameters are the same as for the wx.Font constructor.
2473 If the family and/or pointSize isn't specified, the current default
2474 value is used.
2475 """
2476 if family == None: family = ctrl.GetFont().GetFamily()
2477 if pointSize == -1: pointSize = ctrl.GetFont().GetPointSize()
2478
2479 ctrl.SetFont(wx.Font(pointSize, family, style, weight, underline))
2480 ctrl.SetSize(ctrl.GetBestSize()) # Adjust size to reflect font change.
2481
2482
2483 def _doEnter(self, event):
2484 """ Respond to the user hitting the ENTER key.
2485
2486 We simulate clicking on the "OK" button.
2487 """
2488 if self.Validate(): self.Show(False)
2489
2490 #----------------------------------------------------------------------------
2491
2492 class TextObjectValidator(wx.PyValidator):
2493 """ This validator is used to ensure that the user has entered something
2494 into the text object editor dialog's text field.
2495 """
2496 def __init__(self):
2497 """ Standard constructor.
2498 """
2499 wx.PyValidator.__init__(self)
2500
2501
2502 def Clone(self):
2503 """ Standard cloner.
2504
2505 Note that every validator must implement the Clone() method.
2506 """
2507 return TextObjectValidator()
2508
2509
2510 def Validate(self, win):
2511 """ Validate the contents of the given text control.
2512 """
2513 textCtrl = self.GetWindow()
2514 text = textCtrl.GetValue()
2515
2516 if len(text) == 0:
2517 wx.MessageBox("A text object must contain some text!", "Error")
2518 return False
2519 else:
2520 return True
2521
2522
2523 def TransferToWindow(self):
2524 """ Transfer data from validator to window.
2525
2526 The default implementation returns False, indicating that an error
2527 occurred. We simply return True, as we don't do any data transfer.
2528 """
2529 return True # Prevent wx.Dialog from complaining.
2530
2531
2532 def TransferFromWindow(self):
2533 """ Transfer data from window to validator.
2534
2535 The default implementation returns False, indicating that an error
2536 occurred. We simply return True, as we don't do any data transfer.
2537 """
2538 return True # Prevent wx.Dialog from complaining.
2539
2540 #----------------------------------------------------------------------------
2541
2542 class ExceptionHandler:
2543 """ A simple error-handling class to write exceptions to a text file.
2544
2545 Under MS Windows, the standard DOS console window doesn't scroll and
2546 closes as soon as the application exits, making it hard to find and
2547 view Python exceptions. This utility class allows you to handle Python
2548 exceptions in a more friendly manner.
2549 """
2550
2551 def __init__(self):
2552 """ Standard constructor.
2553 """
2554 self._buff = ""
2555 if os.path.exists("errors.txt"):
2556 os.remove("errors.txt") # Delete previous error log, if any.
2557
2558
2559 def write(self, s):
2560 """ Write the given error message to a text file.
2561
2562 Note that if the error message doesn't end in a carriage return, we
2563 have to buffer up the inputs until a carriage return is received.
2564 """
2565 if (s[-1] != "\n") and (s[-1] != "\r"):
2566 self._buff = self._buff + s
2567 return
2568
2569 try:
2570 s = self._buff + s
2571 self._buff = ""
2572
2573 if s[:9] == "Traceback":
2574 # Tell the user than an exception occurred.
2575 wx.MessageBox("An internal error has occurred.\nPlease " + \
2576 "refer to the 'errors.txt' file for details.",
2577 "Error", wx.OK | wx.CENTRE | wx.ICON_EXCLAMATION)
2578
2579 f = open("errors.txt", "a")
2580 f.write(s)
2581 f.close()
2582 except:
2583 pass # Don't recursively crash on errors.
2584
2585 #----------------------------------------------------------------------------
2586
2587 class SketchApp(wx.App):
2588 """ The main pySketch application object.
2589 """
2590 def OnInit(self):
2591 """ Initialise the application.
2592 """
2593 global _docList
2594 _docList = []
2595
2596 if len(sys.argv) == 1:
2597 # No file name was specified on the command line -> start with a
2598 # blank document.
2599 frame = DrawingFrame(None, -1, "Untitled")
2600 frame.Centre()
2601 frame.Show(True)
2602 _docList.append(frame)
2603 else:
2604 # Load the file(s) specified on the command line.
2605 for arg in sys.argv[1:]:
2606 fileName = os.path.join(os.getcwd(), arg)
2607 if os.path.isfile(fileName):
2608 frame = DrawingFrame(None, -1,
2609 os.path.basename(fileName),
2610 fileName=fileName)
2611 frame.Show(True)
2612 _docList.append(frame)
2613
2614 return True
2615
2616 #----------------------------------------------------------------------------
2617
2618 def main():
2619 """ Start up the pySketch application.
2620 """
2621 global _app
2622
2623 # Redirect python exceptions to a log file.
2624
2625 sys.stderr = ExceptionHandler()
2626
2627 # Create and start the pySketch application.
2628
2629 _app = SketchApp(0)
2630 _app.MainLoop()
2631
2632
2633 if __name__ == "__main__":
2634 main()
2635
2636