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