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