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