]> git.saurik.com Git - wxWidgets.git/blob - wxPython/samples/pySketch/pySketch.py
1. added default constructors for wxString iterators
[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 Original Author: Erik Westra (ewestra@wave.co.nz)
9
10 Other contributors: Bill Baxter (wbaxter@gmail.com)
11
12 #########################################################################
13
14 NOTE
15
16 pySketch requires wxPython version 2.3. If you are running an earlier
17 version, you need to patch your copy of wxPython to fix a bug which will
18 cause the "Edit Text Object" dialog box to crash.
19
20 To patch an earlier version of wxPython, edit the wxPython/windows.py file,
21 find the wxPyValidator.__init__ method and change the line which reads:
22
23 self._setSelf(self, wxPyValidator, 0)
24
25 to:
26
27 self._setSelf(self, wxPyValidator, 1)
28
29 This fixes a known bug in wxPython 2.2.5 (and possibly earlier) which has
30 now been fixed in wxPython 2.3.
31
32 #########################################################################
33
34 TODO:
35
36 * Add ARGV checking to see if a document was double-clicked on.
37
38 Known Bugs:
39
40 * Scrolling the window causes the drawing panel to be mucked up until you
41 refresh it. I've got no idea why.
42
43 * I suspect that the reference counting for some wxPoint objects is
44 getting mucked up; when the user quits, we get errors about being
45 unable to call del on a 'None' object.
46
47 * Saving files via pickling is not a robust cross-platform solution.
48 """
49 import sys
50 import cPickle, os.path
51 import copy
52 import wx
53 from wx.lib.buttons import GenBitmapButton,GenBitmapToggleButton
54
55
56 import traceback, types
57
58 #----------------------------------------------------------------------------
59 # System Constants
60 #----------------------------------------------------------------------------
61
62 # Our menu item IDs:
63
64 menu_DUPLICATE = wx.NewId() # Edit menu items.
65 menu_EDIT_PROPS = wx.NewId()
66
67 menu_SELECT = wx.NewId() # Tools menu items.
68 menu_LINE = wx.NewId()
69 menu_POLYGON = wx.NewId()
70 menu_RECT = wx.NewId()
71 menu_ELLIPSE = wx.NewId()
72 menu_TEXT = wx.NewId()
73
74 menu_DC = wx.NewId() # View menu items.
75 menu_GCDC = wx.NewId()
76
77 menu_MOVE_FORWARD = wx.NewId() # Object menu items.
78 menu_MOVE_TO_FRONT = wx.NewId()
79 menu_MOVE_BACKWARD = wx.NewId()
80 menu_MOVE_TO_BACK = wx.NewId()
81
82 menu_ABOUT = wx.NewId() # Help menu items.
83
84 # Our tool IDs:
85
86 id_SELECT = wx.NewId()
87 id_LINE = wx.NewId()
88 id_POLYGON = wx.NewId()
89 id_SCRIBBLE = wx.NewId()
90 id_RECT = wx.NewId()
91 id_ELLIPSE = wx.NewId()
92 id_TEXT = wx.NewId()
93
94 # Our tool option IDs:
95
96 id_FILL_OPT = wx.NewId()
97 id_PEN_OPT = wx.NewId()
98 id_LINE_OPT = wx.NewId()
99
100 id_LINESIZE_0 = wx.NewId()
101 id_LINESIZE_1 = wx.NewId()
102 id_LINESIZE_2 = wx.NewId()
103 id_LINESIZE_3 = wx.NewId()
104 id_LINESIZE_4 = wx.NewId()
105 id_LINESIZE_5 = wx.NewId()
106
107 # Size of the drawing page, in pixels.
108
109 PAGE_WIDTH = 1000
110 PAGE_HEIGHT = 1000
111
112 #----------------------------------------------------------------------------
113
114 class DrawingFrame(wx.Frame):
115 """ A frame showing the contents of a single document. """
116
117 # ==========================================
118 # == Initialisation and Window Management ==
119 # ==========================================
120
121 def __init__(self, parent, id, title, fileName=None):
122 """ Standard constructor.
123
124 'parent', 'id' and 'title' are all passed to the standard wx.Frame
125 constructor. 'fileName' is the name and path of a saved file to
126 load into this frame, if any.
127 """
128 wx.Frame.__init__(self, parent, id, title,
129 style = wx.DEFAULT_FRAME_STYLE | wx.WANTS_CHARS |
130 wx.NO_FULL_REPAINT_ON_RESIZE)
131
132 # Setup our menu bar.
133 menuBar = wx.MenuBar()
134
135 self.fileMenu = wx.Menu()
136 self.fileMenu.Append(wx.ID_NEW, "New\tCtrl-N", "Create a new document")
137 self.fileMenu.Append(wx.ID_OPEN, "Open...\tCtrl-O", "Open an existing document")
138 self.fileMenu.Append(wx.ID_CLOSE, "Close\tCtrl-W")
139 self.fileMenu.AppendSeparator()
140 self.fileMenu.Append(wx.ID_SAVE, "Save\tCtrl-S")
141 self.fileMenu.Append(wx.ID_SAVEAS, "Save As...")
142 self.fileMenu.Append(wx.ID_REVERT, "Revert...")
143 self.fileMenu.AppendSeparator()
144 self.fileMenu.Append(wx.ID_EXIT, "Quit\tCtrl-Q")
145
146 menuBar.Append(self.fileMenu, "File")
147
148 self.editMenu = wx.Menu()
149 self.editMenu.Append(wx.ID_UNDO, "Undo\tCtrl-Z")
150 self.editMenu.Append(wx.ID_REDO, "Redo\tCtrl-Y")
151 self.editMenu.AppendSeparator()
152 self.editMenu.Append(wx.ID_SELECTALL, "Select All\tCtrl-A")
153 self.editMenu.AppendSeparator()
154 self.editMenu.Append(menu_DUPLICATE, "Duplicate\tCtrl-D")
155 self.editMenu.Append(menu_EDIT_PROPS,"Edit...\tCtrl-E", "Edit object properties")
156 self.editMenu.Append(wx.ID_CLEAR, "Delete\tDel")
157
158 menuBar.Append(self.editMenu, "Edit")
159
160 self.viewMenu = wx.Menu()
161 self.viewMenu.Append(menu_DC, "Normal quality",
162 "Normal rendering using wx.DC",
163 kind=wx.ITEM_RADIO)
164 self.viewMenu.Append(menu_GCDC,"High quality",
165 "Anti-aliased rendering using wx.GCDC",
166 kind=wx.ITEM_RADIO)
167
168 menuBar.Append(self.viewMenu, "View")
169
170 self.toolsMenu = wx.Menu()
171 self.toolsMenu.Append(id_SELECT, "Selection", kind=wx.ITEM_RADIO)
172 self.toolsMenu.Append(id_LINE, "Line", kind=wx.ITEM_RADIO)
173 self.toolsMenu.Append(id_POLYGON, "Polygon", kind=wx.ITEM_RADIO)
174 self.toolsMenu.Append(id_SCRIBBLE,"Scribble", kind=wx.ITEM_RADIO)
175 self.toolsMenu.Append(id_RECT, "Rectangle", kind=wx.ITEM_RADIO)
176 self.toolsMenu.Append(id_ELLIPSE, "Ellipse", kind=wx.ITEM_RADIO)
177 self.toolsMenu.Append(id_TEXT, "Text", kind=wx.ITEM_RADIO)
178
179 menuBar.Append(self.toolsMenu, "Tools")
180
181 self.objectMenu = wx.Menu()
182 self.objectMenu.Append(menu_MOVE_FORWARD, "Move Forward")
183 self.objectMenu.Append(menu_MOVE_TO_FRONT, "Move to Front\tCtrl-F")
184 self.objectMenu.Append(menu_MOVE_BACKWARD, "Move Backward")
185 self.objectMenu.Append(menu_MOVE_TO_BACK, "Move to Back\tCtrl-B")
186
187 menuBar.Append(self.objectMenu, "Object")
188
189 self.helpMenu = wx.Menu()
190 self.helpMenu.Append(menu_ABOUT, "About pySketch...")
191
192 menuBar.Append(self.helpMenu, "Help")
193
194 self.SetMenuBar(menuBar)
195
196 # Create our statusbar
197
198 self.CreateStatusBar()
199
200 # Create our toolbar.
201
202 tsize = (15,15)
203 self.toolbar = self.CreateToolBar(wx.TB_HORIZONTAL | wx.NO_BORDER | wx.TB_FLAT)
204
205 artBmp = wx.ArtProvider.GetBitmap
206 self.toolbar.AddSimpleTool(
207 wx.ID_NEW, artBmp(wx.ART_NEW, wx.ART_TOOLBAR, tsize), "New")
208 self.toolbar.AddSimpleTool(
209 wx.ID_OPEN, artBmp(wx.ART_FILE_OPEN, wx.ART_TOOLBAR, tsize), "Open")
210 self.toolbar.AddSimpleTool(
211 wx.ID_SAVE, artBmp(wx.ART_FILE_SAVE, wx.ART_TOOLBAR, tsize), "Save")
212 self.toolbar.AddSimpleTool(
213 wx.ID_SAVEAS, artBmp(wx.ART_FILE_SAVE_AS, wx.ART_TOOLBAR, tsize),
214 "Save As...")
215 #-------
216 self.toolbar.AddSeparator()
217 self.toolbar.AddSimpleTool(
218 wx.ID_UNDO, artBmp(wx.ART_UNDO, wx.ART_TOOLBAR, tsize), "Undo")
219 self.toolbar.AddSimpleTool(
220 wx.ID_REDO, artBmp(wx.ART_REDO, wx.ART_TOOLBAR, tsize), "Redo")
221 self.toolbar.AddSeparator()
222 self.toolbar.AddSimpleTool(
223 menu_DUPLICATE, wx.Bitmap("images/duplicate.bmp", wx.BITMAP_TYPE_BMP),
224 "Duplicate")
225 #-------
226 self.toolbar.AddSeparator()
227 self.toolbar.AddSimpleTool(
228 menu_MOVE_FORWARD, wx.Bitmap("images/moveForward.bmp", wx.BITMAP_TYPE_BMP),
229 "Move Forward")
230 self.toolbar.AddSimpleTool(
231 menu_MOVE_BACKWARD, wx.Bitmap("images/moveBack.bmp", wx.BITMAP_TYPE_BMP),
232 "Move Backward")
233
234 self.toolbar.Realize()
235
236 # Associate menu/toolbar items with their handlers.
237 menuHandlers = [
238 (wx.ID_NEW, self.doNew),
239 (wx.ID_OPEN, self.doOpen),
240 (wx.ID_CLOSE, self.doClose),
241 (wx.ID_SAVE, self.doSave),
242 (wx.ID_SAVEAS, self.doSaveAs),
243 (wx.ID_REVERT, self.doRevert),
244 (wx.ID_EXIT, self.doExit),
245
246 (wx.ID_UNDO, self.doUndo),
247 (wx.ID_REDO, self.doRedo),
248 (wx.ID_SELECTALL, self.doSelectAll),
249 (menu_DUPLICATE, self.doDuplicate),
250 (menu_EDIT_PROPS, self.doEditObject),
251 (wx.ID_CLEAR, self.doDelete),
252
253 (id_SELECT, self.onChooseTool, self.updChooseTool),
254 (id_LINE, self.onChooseTool, self.updChooseTool),
255 (id_POLYGON, self.onChooseTool, self.updChooseTool),
256 (id_SCRIBBLE,self.onChooseTool, self.updChooseTool),
257 (id_RECT, self.onChooseTool, self.updChooseTool),
258 (id_ELLIPSE, self.onChooseTool, self.updChooseTool),
259 (id_TEXT, self.onChooseTool, self.updChooseTool),
260
261 (menu_DC, self.doChooseQuality),
262 (menu_GCDC, self.doChooseQuality),
263
264 (menu_MOVE_FORWARD, self.doMoveForward),
265 (menu_MOVE_TO_FRONT, self.doMoveToFront),
266 (menu_MOVE_BACKWARD, self.doMoveBackward),
267 (menu_MOVE_TO_BACK, self.doMoveToBack),
268
269 (menu_ABOUT, self.doShowAbout)]
270 for combo in menuHandlers:
271 id, handler = combo[:2]
272 self.Bind(wx.EVT_MENU, handler, id = id)
273 if len(combo)>2:
274 self.Bind(wx.EVT_UPDATE_UI, combo[2], id = id)
275
276 # Install our own method to handle closing the window. This allows us
277 # to ask the user if he/she wants to save before closing the window, as
278 # well as keeping track of which windows are currently open.
279
280 self.Bind(wx.EVT_CLOSE, self.doClose)
281
282 # Install our own method for handling keystrokes. We use this to let
283 # the user move the selected object(s) around using the arrow keys.
284
285 self.Bind(wx.EVT_CHAR_HOOK, self.onKeyEvent)
286
287 # Setup our top-most panel. This holds the entire contents of the
288 # window, excluding the menu bar.
289
290 self.topPanel = wx.Panel(self, -1, style=wx.SIMPLE_BORDER)
291
292 # Setup our tool palette, with all our drawing tools and option icons.
293
294 self.toolPalette = wx.BoxSizer(wx.VERTICAL)
295
296 self.selectIcon = ToolPaletteToggle(self.topPanel, id_SELECT,
297 "select", "Selection Tool", mode=wx.ITEM_RADIO)
298 self.lineIcon = ToolPaletteToggle(self.topPanel, id_LINE,
299 "line", "Line Tool", mode=wx.ITEM_RADIO)
300 self.polygonIcon = ToolPaletteToggle(self.topPanel, id_POLYGON,
301 "polygon", "Polygon Tool", mode=wx.ITEM_RADIO)
302 self.scribbleIcon = ToolPaletteToggle(self.topPanel, id_SCRIBBLE,
303 "scribble", "Scribble Tool", mode=wx.ITEM_RADIO)
304 self.rectIcon = ToolPaletteToggle(self.topPanel, id_RECT,
305 "rect", "Rectangle Tool", mode=wx.ITEM_RADIO)
306 self.ellipseIcon = ToolPaletteToggle(self.topPanel, id_ELLIPSE,
307 "ellipse", "Ellipse Tool", mode=wx.ITEM_RADIO)
308 self.textIcon = ToolPaletteToggle(self.topPanel, id_TEXT,
309 "text", "Text Tool", mode=wx.ITEM_RADIO)
310
311 # Create the tools
312 self.tools = {
313 'select' : (self.selectIcon, SelectDrawingTool()),
314 'line' : (self.lineIcon, LineDrawingTool()),
315 'polygon' : (self.polygonIcon, PolygonDrawingTool()),
316 'scribble': (self.scribbleIcon, ScribbleDrawingTool()),
317 'rect' : (self.rectIcon, RectDrawingTool()),
318 'ellipse' : (self.ellipseIcon, EllipseDrawingTool()),
319 'text' : (self.textIcon, TextDrawingTool())
320 }
321
322
323 toolSizer = wx.GridSizer(0, 2, 5, 5)
324 toolSizer.Add(self.selectIcon)
325 toolSizer.Add(self.lineIcon)
326 toolSizer.Add(self.rectIcon)
327 toolSizer.Add(self.ellipseIcon)
328 toolSizer.Add(self.polygonIcon)
329 toolSizer.Add(self.scribbleIcon)
330 toolSizer.Add(self.textIcon)
331
332 self.optionIndicator = ToolOptionIndicator(self.topPanel)
333 self.optionIndicator.SetToolTip(
334 wx.ToolTip("Shows Current Pen/Fill/Line Size Settings"))
335
336 optionSizer = wx.BoxSizer(wx.HORIZONTAL)
337
338 self.penOptIcon = ToolPaletteButton(self.topPanel, id_PEN_OPT,
339 "penOpt", "Set Pen Colour",)
340 self.fillOptIcon = ToolPaletteButton(self.topPanel, id_FILL_OPT,
341 "fillOpt", "Set Fill Colour")
342 self.lineOptIcon = ToolPaletteButton(self.topPanel, id_LINE_OPT,
343 "lineOpt", "Set Line Size")
344
345 margin = wx.LEFT | wx.RIGHT
346 optionSizer.Add(self.penOptIcon, 0, margin, 1)
347 optionSizer.Add(self.fillOptIcon, 0, margin, 1)
348 optionSizer.Add(self.lineOptIcon, 0, margin, 1)
349
350 margin = wx.TOP | wx.LEFT | wx.RIGHT | wx.ALIGN_CENTRE
351 self.toolPalette.Add(toolSizer, 0, margin, 5)
352 self.toolPalette.Add((0, 0), 0, margin, 5) # Spacer.
353 self.toolPalette.Add(self.optionIndicator, 0, margin, 5)
354 self.toolPalette.Add(optionSizer, 0, margin, 5)
355
356 # Make the tool palette icons respond when the user clicks on them.
357
358 for tool in self.tools.itervalues():
359 tool[0].Bind(wx.EVT_BUTTON, self.onChooseTool)
360
361 self.selectIcon.Bind(wx.EVT_BUTTON, self.onChooseTool)
362 self.lineIcon.Bind(wx.EVT_BUTTON, self.onChooseTool)
363
364
365 self.penOptIcon.Bind(wx.EVT_BUTTON, self.onPenOptionIconClick)
366 self.fillOptIcon.Bind(wx.EVT_BUTTON, self.onFillOptionIconClick)
367 self.lineOptIcon.Bind(wx.EVT_BUTTON, self.onLineOptionIconClick)
368
369 # Setup the main drawing area.
370
371 self.drawPanel = wx.ScrolledWindow(self.topPanel, -1,
372 style=wx.SUNKEN_BORDER|wx.NO_FULL_REPAINT_ON_RESIZE)
373 self.drawPanel.SetBackgroundColour(wx.WHITE)
374
375 self.drawPanel.EnableScrolling(True, True)
376 self.drawPanel.SetScrollbars(20, 20, PAGE_WIDTH / 20, PAGE_HEIGHT / 20)
377
378 self.drawPanel.Bind(wx.EVT_MOUSE_EVENTS, self.onMouseEvent)
379
380 self.drawPanel.Bind(wx.EVT_IDLE, self.onIdle)
381 self.drawPanel.Bind(wx.EVT_SIZE, self.onSize)
382 self.drawPanel.Bind(wx.EVT_PAINT, self.onPaint)
383 self.drawPanel.Bind(wx.EVT_ERASE_BACKGROUND, self.onEraseBackground)
384 self.drawPanel.Bind(wx.EVT_SCROLLWIN, self.onPanelScroll)
385
386 self.Bind(wx.EVT_TIMER, self.onIdle)
387
388
389 # Position everything in the window.
390
391 topSizer = wx.BoxSizer(wx.HORIZONTAL)
392 topSizer.Add(self.toolPalette, 0)
393 topSizer.Add(self.drawPanel, 1, wx.EXPAND)
394
395 self.topPanel.SetAutoLayout(True)
396 self.topPanel.SetSizer(topSizer)
397
398 self.SetSizeHints(250, 200)
399 self.SetSize(wx.Size(600, 400))
400
401 # Select an initial tool.
402
403 self.curToolName = None
404 self.curToolIcon = None
405 self.curTool = None
406 self.setCurrentTool("select")
407
408 # Set initial dc mode to fast
409 self.wrapDC = lambda dc: dc
410
411 # Setup our frame to hold the contents of a sketch document.
412
413 self.dirty = False
414 self.fileName = fileName
415 self.contents = [] # front-to-back ordered list of DrawingObjects.
416 self.selection = [] # List of selected DrawingObjects.
417 self.undoStack = [] # Stack of saved contents for undo.
418 self.redoStack = [] # Stack of saved contents for redo.
419
420 if self.fileName != None:
421 self.loadContents()
422
423 self._initBuffer()
424
425 self._adjustMenus()
426
427 # Finally, set our initial pen, fill and line options.
428
429 self._setPenColour(wx.BLACK)
430 self._setFillColour(wx.Colour(215,253,254))
431 self._setLineSize(2)
432
433 self.backgroundFillBrush = None # create on demand
434
435 # Start the background redraw timer
436 # This is optional, but it gives the double-buffered contents a
437 # chance to redraw even when idle events are disabled (like during
438 # resize and scrolling)
439 self.redrawTimer = wx.Timer(self)
440 self.redrawTimer.Start(700)
441
442
443 # ============================
444 # == Event Handling Methods ==
445 # ============================
446
447
448 def onPenOptionIconClick(self, event):
449 """ Respond to the user clicking on the "Pen Options" icon.
450 """
451 data = wx.ColourData()
452 if len(self.selection) == 1:
453 data.SetColour(self.selection[0].getPenColour())
454 else:
455 data.SetColour(self.penColour)
456
457 dialog = wx.ColourDialog(self, data)
458 dialog.SetTitle('Choose line colour')
459 if dialog.ShowModal() == wx.ID_OK:
460 c = dialog.GetColourData().GetColour()
461 self._setPenColour(wx.Colour(c.Red(), c.Green(), c.Blue()))
462 dialog.Destroy()
463
464
465 def onFillOptionIconClick(self, event):
466 """ Respond to the user clicking on the "Fill Options" icon.
467 """
468 data = wx.ColourData()
469 if len(self.selection) == 1:
470 data.SetColour(self.selection[0].getFillColour())
471 else:
472 data.SetColour(self.fillColour)
473
474 dialog = wx.ColourDialog(self, data)
475 dialog.SetTitle('Choose fill colour')
476 if dialog.ShowModal() == wx.ID_OK:
477 c = dialog.GetColourData().GetColour()
478 self._setFillColour(wx.Colour(c.Red(), c.Green(), c.Blue()))
479 dialog.Destroy()
480
481 def onLineOptionIconClick(self, event):
482 """ Respond to the user clicking on the "Line Options" icon.
483 """
484 if len(self.selection) == 1:
485 menu = self._buildLineSizePopup(self.selection[0].getLineSize())
486 else:
487 menu = self._buildLineSizePopup(self.lineSize)
488
489 pos = self.lineOptIcon.GetPosition()
490 pos.y = pos.y + self.lineOptIcon.GetSize().height
491 self.PopupMenu(menu, pos)
492 menu.Destroy()
493
494
495 def onKeyEvent(self, event):
496 """ Respond to a keypress event.
497
498 We make the arrow keys move the selected object(s) by one pixel in
499 the given direction.
500 """
501 step = 1
502 if event.ShiftDown():
503 step = 20
504
505 if event.GetKeyCode() == wx.WXK_UP:
506 self._moveObject(0, -step)
507 elif event.GetKeyCode() == wx.WXK_DOWN:
508 self._moveObject(0, step)
509 elif event.GetKeyCode() == wx.WXK_LEFT:
510 self._moveObject(-step, 0)
511 elif event.GetKeyCode() == wx.WXK_RIGHT:
512 self._moveObject(step, 0)
513 else:
514 event.Skip()
515
516
517 def onMouseEvent(self, event):
518 """ Respond to mouse events in the main drawing panel
519
520 How we respond depends on the currently selected tool.
521 """
522 if self.curTool is None: return
523
524 # Translate event into canvas coordinates and pass to current tool
525 origx,origy = event.X, event.Y
526 pt = self._getEventCoordinates(event)
527 event.m_x = pt.x
528 event.m_y = pt.y
529 handled = self.curTool.onMouseEvent(self,event)
530 event.m_x = origx
531 event.m_y = origy
532
533 if handled: return
534
535 # otherwise handle it ourselves
536 if event.RightDown():
537 self.doPopupContextMenu(event)
538
539
540 def doPopupContextMenu(self, event):
541 """ Respond to the user right-clicking within our drawing panel.
542
543 We select the clicked-on item, if necessary, and display a pop-up
544 menu of available options which can be applied to the selected
545 item(s).
546 """
547 mousePt = self._getEventCoordinates(event)
548 obj = self.getObjectAt(mousePt)
549
550 if obj == None: return # Nothing selected.
551
552 # Select the clicked-on object.
553
554 self.select(obj)
555
556 # Build our pop-up menu.
557
558 menu = wx.Menu()
559 menu.Append(menu_DUPLICATE, "Duplicate")
560 menu.Append(menu_EDIT_PROPS,"Edit...")
561 menu.Append(wx.ID_CLEAR, "Delete")
562 menu.AppendSeparator()
563 menu.Append(menu_MOVE_FORWARD, "Move Forward")
564 menu.Append(menu_MOVE_TO_FRONT, "Move to Front")
565 menu.Append(menu_MOVE_BACKWARD, "Move Backward")
566 menu.Append(menu_MOVE_TO_BACK, "Move to Back")
567
568 menu.Enable(menu_EDIT_PROPS, obj.hasPropertyEditor())
569 menu.Enable(menu_MOVE_FORWARD, obj != self.contents[0])
570 menu.Enable(menu_MOVE_TO_FRONT, obj != self.contents[0])
571 menu.Enable(menu_MOVE_BACKWARD, obj != self.contents[-1])
572 menu.Enable(menu_MOVE_TO_BACK, obj != self.contents[-1])
573
574 self.Bind(wx.EVT_MENU, self.doDuplicate, id=menu_DUPLICATE)
575 self.Bind(wx.EVT_MENU, self.doEditObject, id=menu_EDIT_PROPS)
576 self.Bind(wx.EVT_MENU, self.doDelete, id=wx.ID_CLEAR)
577 self.Bind(wx.EVT_MENU, self.doMoveForward, id=menu_MOVE_FORWARD)
578 self.Bind(wx.EVT_MENU, self.doMoveToFront, id=menu_MOVE_TO_FRONT)
579 self.Bind(wx.EVT_MENU, self.doMoveBackward,id=menu_MOVE_BACKWARD)
580 self.Bind(wx.EVT_MENU, self.doMoveToBack, id=menu_MOVE_TO_BACK)
581
582 # Show the pop-up menu.
583
584 clickPt = wx.Point(mousePt.x + self.drawPanel.GetPosition().x,
585 mousePt.y + self.drawPanel.GetPosition().y)
586 self.drawPanel.PopupMenu(menu, mousePt)
587 menu.Destroy()
588
589
590 def onSize(self, event):
591 """
592 Called when the window is resized. We set a flag so the idle
593 handler will resize the buffer.
594 """
595 self.requestRedraw()
596
597
598 def onIdle(self, event):
599 """
600 If the size was changed then resize the bitmap used for double
601 buffering to match the window size. We do it in Idle time so
602 there is only one refresh after resizing is done, not lots while
603 it is happening.
604 """
605 if self._reInitBuffer and self.IsShown():
606 self._initBuffer()
607 self.drawPanel.Refresh(False)
608
609 def requestRedraw(self):
610 """Requests a redraw of the drawing panel contents.
611
612 The actual redrawing doesn't happen until the next idle time.
613 """
614 self._reInitBuffer = True
615
616 def onPaint(self, event):
617 """
618 Called when the window is exposed.
619 """
620 # Create a buffered paint DC. It will create the real
621 # wx.PaintDC and then blit the bitmap to it when dc is
622 # deleted.
623 dc = wx.BufferedPaintDC(self.drawPanel, self.buffer)
624
625
626 # On Windows, if that's all we do things look a little rough
627 # So in order to make scrolling more polished-looking
628 # we iterate over the exposed regions and fill in unknown
629 # areas with a fall-back pattern.
630
631 if wx.Platform != '__WXMSW__':
632 return
633
634 # First get the update rects and subtract off the part that
635 # self.buffer has correct already
636 region = self.drawPanel.GetUpdateRegion()
637 panelRect = self.drawPanel.GetClientRect()
638 offset = list(self.drawPanel.CalcUnscrolledPosition(0,0))
639 offset[0] -= self.saved_offset[0]
640 offset[1] -= self.saved_offset[1]
641 region.Subtract(-offset[0],- offset[1],panelRect.Width, panelRect.Height)
642
643 # Now iterate over the remaining region rects and fill in with a pattern
644 rgn_iter = wx.RegionIterator(region)
645 if rgn_iter.HaveRects():
646 self.setBackgroundMissingFillStyle(dc)
647 offset = self.drawPanel.CalcUnscrolledPosition(0,0)
648 while rgn_iter:
649 r = rgn_iter.GetRect()
650 if r.Size != self.drawPanel.ClientSize:
651 dc.DrawRectangleRect(r)
652 rgn_iter.Next()
653
654
655 def setBackgroundMissingFillStyle(self, dc):
656 if self.backgroundFillBrush is None:
657 # Win95 can only handle a 8x8 stipple bitmaps max
658 #stippleBitmap = wx.BitmapFromBits("\xf0"*4 + "\x0f"*4,8,8)
659 # ...but who uses Win95?
660 stippleBitmap = wx.BitmapFromBits("\x06",2,2)
661 stippleBitmap.SetMask(wx.Mask(stippleBitmap))
662 bgbrush = wx.Brush(wx.WHITE, wx.STIPPLE_MASK_OPAQUE)
663 bgbrush.SetStipple(stippleBitmap)
664 self.backgroundFillBrush = bgbrush
665
666 dc.SetPen(wx.TRANSPARENT_PEN)
667 dc.SetBrush(self.backgroundFillBrush)
668 dc.SetTextForeground(wx.LIGHT_GREY)
669 dc.SetTextBackground(wx.WHITE)
670
671
672 def onEraseBackground(self, event):
673 """
674 Overridden to do nothing to prevent flicker
675 """
676 pass
677
678
679 def onPanelScroll(self, event):
680 """
681 Called when the user changes scrolls the drawPanel
682 """
683 # make a note to ourselves to redraw when we get a chance
684 self.requestRedraw()
685 event.Skip()
686 pass
687
688 def drawContents(self, dc):
689 """
690 Does the actual drawing of all drawing contents with the specified dc
691 """
692 # PrepareDC sets the device origin according to current scrolling
693 self.drawPanel.PrepareDC(dc)
694
695 gdc = self.wrapDC(dc)
696
697 # First pass draws objects
698 ordered_selection = []
699 for obj in self.contents[::-1]:
700 if obj in self.selection:
701 obj.draw(gdc, True)
702 ordered_selection.append(obj)
703 else:
704 obj.draw(gdc, False)
705
706 # First pass draws objects
707 if self.curTool is not None:
708 self.curTool.draw(gdc)
709
710 # Second pass draws selection handles so they're always on top
711 for obj in ordered_selection:
712 obj.drawHandles(gdc)
713
714
715
716 # ==========================
717 # == Menu Command Methods ==
718 # ==========================
719
720 def doNew(self, event):
721 """ Respond to the "New" menu command.
722 """
723 global _docList
724 newFrame = DrawingFrame(None, -1, "Untitled")
725 newFrame.Show(True)
726 _docList.append(newFrame)
727
728
729 def doOpen(self, event):
730 """ Respond to the "Open" menu command.
731 """
732 global _docList
733
734 curDir = os.getcwd()
735 fileName = wx.FileSelector("Open File", default_extension="psk",
736 flags = wx.OPEN | wx.FILE_MUST_EXIST)
737 if fileName == "": return
738 fileName = os.path.join(os.getcwd(), fileName)
739 os.chdir(curDir)
740
741 title = os.path.basename(fileName)
742
743 if (self.fileName == None) and (len(self.contents) == 0):
744 # Load contents into current (empty) document.
745 self.fileName = fileName
746 self.SetTitle(os.path.basename(fileName))
747 self.loadContents()
748 else:
749 # Open a new frame for this document.
750 newFrame = DrawingFrame(None, -1, os.path.basename(fileName),
751 fileName=fileName)
752 newFrame.Show(True)
753 _docList.append(newFrame)
754
755
756 def doClose(self, event):
757 """ Respond to the "Close" menu command.
758 """
759 global _docList
760
761 if self.dirty:
762 if not self.askIfUserWantsToSave("closing"): return
763
764 _docList.remove(self)
765 self.Destroy()
766
767
768 def doSave(self, event):
769 """ Respond to the "Save" menu command.
770 """
771 if self.fileName != None:
772 self.saveContents()
773
774
775 def doSaveAs(self, event):
776 """ Respond to the "Save As" menu command.
777 """
778 if self.fileName == None:
779 default = ""
780 else:
781 default = self.fileName
782
783 curDir = os.getcwd()
784 fileName = wx.FileSelector("Save File As", "Saving",
785 default_filename=default,
786 default_extension="psk",
787 wildcard="*.psk",
788 flags = wx.SAVE | wx.OVERWRITE_PROMPT)
789 if fileName == "": return # User cancelled.
790 fileName = os.path.join(os.getcwd(), fileName)
791 os.chdir(curDir)
792
793 title = os.path.basename(fileName)
794 self.SetTitle(title)
795
796 self.fileName = fileName
797 self.saveContents()
798
799
800 def doRevert(self, event):
801 """ Respond to the "Revert" menu command.
802 """
803 if not self.dirty: return
804
805 if wx.MessageBox("Discard changes made to this document?", "Confirm",
806 style = wx.OK | wx.CANCEL | wx.ICON_QUESTION,
807 parent=self) == wx.CANCEL: return
808 self.loadContents()
809
810
811 def doExit(self, event):
812 """ Respond to the "Quit" menu command.
813 """
814 global _docList, _app
815 for doc in _docList:
816 if not doc.dirty: continue
817 doc.Raise()
818 if not doc.askIfUserWantsToSave("quitting"): return
819 _docList.remove(doc)
820 doc.Destroy()
821
822 _app.ExitMainLoop()
823
824
825 def doUndo(self, event):
826 """ Respond to the "Undo" menu command.
827 """
828 if not self.undoStack: return
829
830 state = self._buildStoredState()
831 self.redoStack.append(state)
832 state = self.undoStack.pop()
833 self._restoreStoredState(state)
834
835 def doRedo(self, event):
836 """ Respond to the "Redo" menu.
837 """
838 if not self.redoStack: return
839
840 state = self._buildStoredState()
841 self.undoStack.append(state)
842 state = self.redoStack.pop()
843 self._restoreStoredState(state)
844
845 def doSelectAll(self, event):
846 """ Respond to the "Select All" menu command.
847 """
848 self.selectAll()
849
850
851 def doDuplicate(self, event):
852 """ Respond to the "Duplicate" menu command.
853 """
854 self.saveUndoInfo()
855
856 objs = []
857 for obj in self.contents:
858 if obj in self.selection:
859 newObj = copy.deepcopy(obj)
860 pos = obj.getPosition()
861 newObj.setPosition(wx.Point(pos.x + 10, pos.y + 10))
862 objs.append(newObj)
863
864 self.contents = objs + self.contents
865
866 self.selectMany(objs)
867
868
869 def doEditObject(self, event):
870 """ Respond to the "Edit..." menu command.
871 """
872 if len(self.selection) != 1: return
873
874 obj = self.selection[0]
875 if not obj.hasPropertyEditor():
876 assert False, "doEditObject called on non-editable"
877
878 ret = obj.doPropertyEdit(self)
879 if ret:
880 self.dirty = True
881 self.requestRedraw()
882 self._adjustMenus()
883
884
885 def doDelete(self, event):
886 """ Respond to the "Delete" menu command.
887 """
888 self.saveUndoInfo()
889
890 for obj in self.selection:
891 self.contents.remove(obj)
892 del obj
893 self.deselectAll()
894
895
896 def onChooseTool(self, event):
897 """ Respond to tool selection menu and tool palette selections
898 """
899 obj = event.GetEventObject()
900 id2name = { id_SELECT: "select",
901 id_LINE: "line",
902 id_POLYGON: "polygon",
903 id_SCRIBBLE: "scribble",
904 id_RECT: "rect",
905 id_ELLIPSE: "ellipse",
906 id_TEXT: "text" }
907 toolID = event.GetId()
908 name = id2name.get( toolID )
909
910 if name:
911 self.setCurrentTool(name)
912
913 def updChooseTool(self, event):
914 """UI update event that keeps tool menu in sync with the PaletteIcons"""
915 obj = event.GetEventObject()
916 id2name = { id_SELECT: "select",
917 id_LINE: "line",
918 id_POLYGON: "polygon",
919 id_SCRIBBLE: "scribble",
920 id_RECT: "rect",
921 id_ELLIPSE: "ellipse",
922 id_TEXT: "text" }
923 toolID = event.GetId()
924 event.Check( toolID == self.curToolIcon.GetId() )
925
926
927 def doChooseQuality(self, event):
928 """Respond to the render quality menu commands
929 """
930 if event.GetId() == menu_DC:
931 self.wrapDC = lambda dc: dc
932 else:
933 self.wrapDC = lambda dc: wx.GCDC(dc)
934 self._adjustMenus()
935 self.requestRedraw()
936
937 def doMoveForward(self, event):
938 """ Respond to the "Move Forward" menu command.
939 """
940 if len(self.selection) != 1: return
941
942 self.saveUndoInfo()
943
944 obj = self.selection[0]
945 index = self.contents.index(obj)
946 if index == 0: return
947
948 del self.contents[index]
949 self.contents.insert(index-1, obj)
950
951 self.requestRedraw()
952 self._adjustMenus()
953
954
955 def doMoveToFront(self, event):
956 """ Respond to the "Move to Front" menu command.
957 """
958 if len(self.selection) != 1: return
959
960 self.saveUndoInfo()
961
962 obj = self.selection[0]
963 self.contents.remove(obj)
964 self.contents.insert(0, obj)
965
966 self.requestRedraw()
967 self._adjustMenus()
968
969
970 def doMoveBackward(self, event):
971 """ Respond to the "Move Backward" menu command.
972 """
973 if len(self.selection) != 1: return
974
975 self.saveUndoInfo()
976
977 obj = self.selection[0]
978 index = self.contents.index(obj)
979 if index == len(self.contents) - 1: return
980
981 del self.contents[index]
982 self.contents.insert(index+1, obj)
983
984 self.requestRedraw()
985 self._adjustMenus()
986
987
988 def doMoveToBack(self, event):
989 """ Respond to the "Move to Back" menu command.
990 """
991 if len(self.selection) != 1: return
992
993 self.saveUndoInfo()
994
995 obj = self.selection[0]
996 self.contents.remove(obj)
997 self.contents.append(obj)
998
999 self.requestRedraw()
1000 self._adjustMenus()
1001
1002
1003 def doShowAbout(self, event):
1004 """ Respond to the "About pySketch" menu command.
1005 """
1006 dialog = wx.Dialog(self, -1, "About pySketch") # ,
1007 #style=wx.DIALOG_MODAL | wx.STAY_ON_TOP)
1008 dialog.SetBackgroundColour(wx.WHITE)
1009
1010 panel = wx.Panel(dialog, -1)
1011 panel.SetBackgroundColour(wx.WHITE)
1012
1013 panelSizer = wx.BoxSizer(wx.VERTICAL)
1014
1015 boldFont = wx.Font(panel.GetFont().GetPointSize(),
1016 panel.GetFont().GetFamily(),
1017 wx.NORMAL, wx.BOLD)
1018
1019 logo = wx.StaticBitmap(panel, -1, wx.Bitmap("images/logo.bmp",
1020 wx.BITMAP_TYPE_BMP))
1021
1022 lab1 = wx.StaticText(panel, -1, "pySketch")
1023 lab1.SetFont(wx.Font(36, boldFont.GetFamily(), wx.ITALIC, wx.BOLD))
1024 lab1.SetSize(lab1.GetBestSize())
1025
1026 imageSizer = wx.BoxSizer(wx.HORIZONTAL)
1027 imageSizer.Add(logo, 0, wx.ALL | wx.ALIGN_CENTRE_VERTICAL, 5)
1028 imageSizer.Add(lab1, 0, wx.ALL | wx.ALIGN_CENTRE_VERTICAL, 5)
1029
1030 lab2 = wx.StaticText(panel, -1, "A simple object-oriented drawing " + \
1031 "program.")
1032 lab2.SetFont(boldFont)
1033 lab2.SetSize(lab2.GetBestSize())
1034
1035 lab3 = wx.StaticText(panel, -1, "pySketch is completely free " + \
1036 "software; please")
1037 lab3.SetFont(boldFont)
1038 lab3.SetSize(lab3.GetBestSize())
1039
1040 lab4 = wx.StaticText(panel, -1, "feel free to adapt or use this " + \
1041 "in any way you like.")
1042 lab4.SetFont(boldFont)
1043 lab4.SetSize(lab4.GetBestSize())
1044
1045 lab5 = wx.StaticText(panel, -1,
1046 "Author: Erik Westra " + \
1047 "(ewestra@wave.co.nz)\n" + \
1048 "Contributors: Bill Baxter " +\
1049 "(wbaxter@gmail.com) ")
1050
1051 lab5.SetFont(boldFont)
1052 lab5.SetSize(lab5.GetBestSize())
1053
1054 btnOK = wx.Button(panel, wx.ID_OK, "OK")
1055
1056 panelSizer.Add(imageSizer, 0, wx.ALIGN_CENTRE)
1057 panelSizer.Add((10, 10)) # Spacer.
1058 panelSizer.Add(lab2, 0, wx.ALIGN_CENTRE)
1059 panelSizer.Add((10, 10)) # Spacer.
1060 panelSizer.Add(lab3, 0, wx.ALIGN_CENTRE)
1061 panelSizer.Add(lab4, 0, wx.ALIGN_CENTRE)
1062 panelSizer.Add((10, 10)) # Spacer.
1063 panelSizer.Add(lab5, 0, wx.ALIGN_CENTRE)
1064 panelSizer.Add((10, 10)) # Spacer.
1065 panelSizer.Add(btnOK, 0, wx.ALL | wx.ALIGN_CENTRE, 5)
1066
1067 panel.SetAutoLayout(True)
1068 panel.SetSizer(panelSizer)
1069 panelSizer.Fit(panel)
1070
1071 topSizer = wx.BoxSizer(wx.HORIZONTAL)
1072 topSizer.Add(panel, 0, wx.ALL, 10)
1073
1074 dialog.SetAutoLayout(True)
1075 dialog.SetSizer(topSizer)
1076 topSizer.Fit(dialog)
1077
1078 dialog.Centre()
1079
1080 btn = dialog.ShowModal()
1081 dialog.Destroy()
1082
1083 def getTextEditor(self):
1084 if not hasattr(self,'textEditor') or not self.textEditor:
1085 self.textEditor = EditTextObjectDialog(self, "Edit Text Object")
1086 return self.textEditor
1087
1088 # =============================
1089 # == Object Creation Methods ==
1090 # =============================
1091
1092 def addObject(self, obj, select=True):
1093 """Add a new drawing object to the canvas.
1094
1095 If select is True then also select the object
1096 """
1097 self.saveUndoInfo()
1098 self.contents.insert(0, obj)
1099 self.dirty = True
1100 if select:
1101 self.select(obj)
1102 #self.setCurrentTool('select')
1103
1104 def saveUndoInfo(self):
1105 """ Remember the current state of the document, to allow for undo.
1106
1107 We make a copy of the document's contents, so that we can return to
1108 the previous contents if the user does something and then wants to
1109 undo the operation.
1110
1111 This should be called only for a new modification to the document
1112 since it erases the redo history.
1113 """
1114 state = self._buildStoredState()
1115
1116 self.undoStack.append(state)
1117 self.redoStack = []
1118 self.dirty = True
1119 self._adjustMenus()
1120
1121 # =======================
1122 # == Selection Methods ==
1123 # =======================
1124
1125 def setCurrentTool(self, toolName):
1126 """ Set the currently selected tool.
1127 """
1128
1129 toolIcon, tool = self.tools[toolName]
1130 if self.curToolIcon is not None:
1131 self.curToolIcon.SetValue(False)
1132
1133 toolIcon.SetValue(True)
1134 self.curToolName = toolName
1135 self.curToolIcon = toolIcon
1136 self.curTool = tool
1137 self.drawPanel.SetCursor(tool.getDefaultCursor())
1138
1139
1140 def selectAll(self):
1141 """ Select every DrawingObject in our document.
1142 """
1143 self.selection = []
1144 for obj in self.contents:
1145 self.selection.append(obj)
1146 self.requestRedraw()
1147 self._adjustMenus()
1148
1149
1150 def deselectAll(self):
1151 """ Deselect every DrawingObject in our document.
1152 """
1153 self.selection = []
1154 self.requestRedraw()
1155 self._adjustMenus()
1156
1157
1158 def select(self, obj, add=False):
1159 """ Select the given DrawingObject within our document.
1160
1161 If 'add' is True obj is added onto the current selection
1162 """
1163 if not add:
1164 self.selection = []
1165 if obj not in self.selection:
1166 self.selection += [obj]
1167 self.requestRedraw()
1168 self._adjustMenus()
1169
1170 def selectMany(self, objs):
1171 """ Select the given list of DrawingObjects.
1172 """
1173 self.selection = objs
1174 self.requestRedraw()
1175 self._adjustMenus()
1176
1177
1178 def selectByRectangle(self, x, y, width, height):
1179 """ Select every DrawingObject in the given rectangular region.
1180 """
1181 self.selection = []
1182 for obj in self.contents:
1183 if obj.objectWithinRect(x, y, width, height):
1184 self.selection.append(obj)
1185 self.requestRedraw()
1186 self._adjustMenus()
1187
1188 def getObjectAndSelectionHandleAt(self, pt):
1189 """ Return the object and selection handle at the given point.
1190
1191 We draw selection handles (small rectangles) around the currently
1192 selected object(s). If the given point is within one of the
1193 selection handle rectangles, we return the associated object and a
1194 code indicating which selection handle the point is in. If the
1195 point isn't within any selection handle at all, we return the tuple
1196 (None, None).
1197 """
1198 for obj in self.selection:
1199 handle = obj.getSelectionHandleContainingPoint(pt.x, pt.y)
1200 if handle is not None:
1201 return obj, handle
1202
1203 return None, None
1204
1205
1206 def getObjectAt(self, pt):
1207 """ Return the first object found which is at the given point.
1208 """
1209 for obj in self.contents:
1210 if obj.objectContainsPoint(pt.x, pt.y):
1211 return obj
1212 return None
1213
1214
1215 # ======================
1216 # == File I/O Methods ==
1217 # ======================
1218
1219 def loadContents(self):
1220 """ Load the contents of our document into memory.
1221 """
1222
1223 try:
1224 f = open(self.fileName, "rb")
1225 objData = cPickle.load(f)
1226 f.close()
1227
1228 for klass, data in objData:
1229 obj = klass()
1230 obj.setData(data)
1231 self.contents.append(obj)
1232
1233 self.dirty = False
1234 self.selection = []
1235 self.undoStack = []
1236 self.redoStack = []
1237
1238 self.requestRedraw()
1239 self._adjustMenus()
1240 except:
1241 response = wx.MessageBox("Unable to load " + self.fileName + ".",
1242 "Error", wx.OK|wx.ICON_ERROR, self)
1243
1244
1245
1246 def saveContents(self):
1247 """ Save the contents of our document to disk.
1248 """
1249 # SWIG-wrapped native wx contents cannot be pickled, so
1250 # we have to convert our data to something pickle-friendly.
1251
1252 try:
1253 objData = []
1254 for obj in self.contents:
1255 objData.append([obj.__class__, obj.getData()])
1256
1257 f = open(self.fileName, "wb")
1258 cPickle.dump(objData, f)
1259 f.close()
1260
1261 self.dirty = False
1262 self._adjustMenus()
1263 except:
1264 response = wx.MessageBox("Unable to load " + self.fileName + ".",
1265 "Error", wx.OK|wx.ICON_ERROR, self)
1266
1267
1268 def askIfUserWantsToSave(self, action):
1269 """ Give the user the opportunity to save the current document.
1270
1271 'action' is a string describing the action about to be taken. If
1272 the user wants to save the document, it is saved immediately. If
1273 the user cancels, we return False.
1274 """
1275 if not self.dirty: return True # Nothing to do.
1276
1277 response = wx.MessageBox("Save changes before " + action + "?",
1278 "Confirm", wx.YES_NO | wx.CANCEL, self)
1279
1280 if response == wx.YES:
1281 if self.fileName == None:
1282 fileName = wx.FileSelector("Save File As", "Saving",
1283 default_extension="psk",
1284 wildcard="*.psk",
1285 flags = wx.SAVE | wx.OVERWRITE_PROMPT)
1286 if fileName == "": return False # User cancelled.
1287 self.fileName = fileName
1288
1289 self.saveContents()
1290 return True
1291 elif response == wx.NO:
1292 return True # User doesn't want changes saved.
1293 elif response == wx.CANCEL:
1294 return False # User cancelled.
1295
1296 # =====================
1297 # == Private Methods ==
1298 # =====================
1299
1300 def _initBuffer(self):
1301 """Initialize the bitmap used for buffering the display."""
1302 size = self.drawPanel.GetSize()
1303 self.buffer = wx.EmptyBitmap(max(1,size.width),max(1,size.height))
1304 dc = wx.BufferedDC(None, self.buffer)
1305 dc.SetBackground(wx.Brush(self.drawPanel.GetBackgroundColour()))
1306 dc.Clear()
1307 self.drawContents(dc)
1308 del dc # commits all drawing to the buffer
1309
1310 self.saved_offset = self.drawPanel.CalcUnscrolledPosition(0,0)
1311
1312 self._reInitBuffer = False
1313
1314
1315
1316 def _adjustMenus(self):
1317 """ Adjust our menus and toolbar to reflect the current state of the
1318 world.
1319
1320 Doing this manually rather than using an EVT_UPDATE_UI is a bit
1321 more efficient (since it's only done when it's really needed),
1322 but it means we have to remember to call _adjustMenus any time
1323 menus may need adjusting.
1324 """
1325 canSave = (self.fileName != None) and self.dirty
1326 canRevert = (self.fileName != None) and self.dirty
1327 canUndo = self.undoStack!=[]
1328 canRedo = self.redoStack!=[]
1329 selection = len(self.selection) > 0
1330 onlyOne = len(self.selection) == 1
1331 hasEditor = onlyOne and self.selection[0].hasPropertyEditor()
1332 front = onlyOne and (self.selection[0] == self.contents[0])
1333 back = onlyOne and (self.selection[0] == self.contents[-1])
1334
1335 # Enable/disable our menu items.
1336
1337 self.fileMenu.Enable(wx.ID_SAVE, canSave)
1338 self.fileMenu.Enable(wx.ID_REVERT, canRevert)
1339
1340 self.editMenu.Enable(wx.ID_UNDO, canUndo)
1341 self.editMenu.Enable(wx.ID_REDO, canRedo)
1342 self.editMenu.Enable(menu_DUPLICATE, selection)
1343 self.editMenu.Enable(menu_EDIT_PROPS,hasEditor)
1344 self.editMenu.Enable(wx.ID_CLEAR, selection)
1345
1346 self.objectMenu.Enable(menu_MOVE_FORWARD, onlyOne and not front)
1347 self.objectMenu.Enable(menu_MOVE_TO_FRONT, onlyOne and not front)
1348 self.objectMenu.Enable(menu_MOVE_BACKWARD, onlyOne and not back)
1349 self.objectMenu.Enable(menu_MOVE_TO_BACK, onlyOne and not back)
1350
1351 # Enable/disable our toolbar icons.
1352
1353 self.toolbar.EnableTool(wx.ID_NEW, True)
1354 self.toolbar.EnableTool(wx.ID_OPEN, True)
1355 self.toolbar.EnableTool(wx.ID_SAVE, canSave)
1356 self.toolbar.EnableTool(wx.ID_UNDO, canUndo)
1357 self.toolbar.EnableTool(wx.ID_REDO, canRedo)
1358 self.toolbar.EnableTool(menu_DUPLICATE, selection)
1359 self.toolbar.EnableTool(menu_MOVE_FORWARD, onlyOne and not front)
1360 self.toolbar.EnableTool(menu_MOVE_BACKWARD, onlyOne and not back)
1361
1362
1363 def _setPenColour(self, colour):
1364 """ Set the default or selected object's pen colour.
1365 """
1366 if len(self.selection) > 0:
1367 self.saveUndoInfo()
1368 for obj in self.selection:
1369 obj.setPenColour(colour)
1370 self.requestRedraw()
1371
1372 self.penColour = colour
1373 self.optionIndicator.setPenColour(colour)
1374
1375
1376 def _setFillColour(self, colour):
1377 """ Set the default or selected object's fill colour.
1378 """
1379 if len(self.selection) > 0:
1380 self.saveUndoInfo()
1381 for obj in self.selection:
1382 obj.setFillColour(colour)
1383 self.requestRedraw()
1384
1385 self.fillColour = colour
1386 self.optionIndicator.setFillColour(colour)
1387
1388
1389 def _setLineSize(self, size):
1390 """ Set the default or selected object's line size.
1391 """
1392 if len(self.selection) > 0:
1393 self.saveUndoInfo()
1394 for obj in self.selection:
1395 obj.setLineSize(size)
1396 self.requestRedraw()
1397
1398 self.lineSize = size
1399 self.optionIndicator.setLineSize(size)
1400
1401
1402 def _buildStoredState(self):
1403 """ Remember the current state of the document, to allow for undo.
1404
1405 We make a copy of the document's contents, so that we can return to
1406 the previous contents if the user does something and then wants to
1407 undo the operation.
1408
1409 Returns an object representing the current document state.
1410 """
1411 savedContents = []
1412 for obj in self.contents:
1413 savedContents.append([obj.__class__, obj.getData()])
1414
1415 savedSelection = []
1416 for i in range(len(self.contents)):
1417 if self.contents[i] in self.selection:
1418 savedSelection.append(i)
1419
1420 info = {"contents" : savedContents,
1421 "selection" : savedSelection}
1422
1423 return info
1424
1425 def _restoreStoredState(self, savedState):
1426 """Restore the state of the document to a previous point for undo/redo.
1427
1428 Takes a stored state object and recreates the document from it.
1429 Used by undo/redo implementation.
1430 """
1431 self.contents = []
1432
1433 for draw_class, data in savedState["contents"]:
1434 obj = draw_class()
1435 obj.setData(data)
1436 self.contents.append(obj)
1437
1438 self.selection = []
1439 for i in savedState["selection"]:
1440 self.selection.append(self.contents[i])
1441
1442 self.dirty = True
1443 self._adjustMenus()
1444 self.requestRedraw()
1445
1446 def _resizeObject(self, obj, anchorPt, oldPt, newPt):
1447 """ Resize the given object.
1448
1449 'anchorPt' is the unchanging corner of the object, while the
1450 opposite corner has been resized. 'oldPt' are the current
1451 coordinates for this corner, while 'newPt' are the new coordinates.
1452 The object should fit within the given dimensions, though if the
1453 new point is less than the anchor point the object will need to be
1454 moved as well as resized, to avoid giving it a negative size.
1455 """
1456 if isinstance(obj, TextDrawingObject):
1457 # Not allowed to resize text objects -- they're sized to fit text.
1458 wx.Bell()
1459 return
1460
1461 self.saveUndoInfo()
1462
1463 topLeft = wx.Point(min(anchorPt.x, newPt.x),
1464 min(anchorPt.y, newPt.y))
1465 botRight = wx.Point(max(anchorPt.x, newPt.x),
1466 max(anchorPt.y, newPt.y))
1467
1468 newWidth = botRight.x - topLeft.x
1469 newHeight = botRight.y - topLeft.y
1470
1471 if isinstance(obj, LineDrawingObject):
1472 # Adjust the line so that its start and end points match the new
1473 # overall object size.
1474
1475 startPt = obj.getStartPt()
1476 endPt = obj.getEndPt()
1477
1478 slopesDown = ((startPt.x < endPt.x) and (startPt.y < endPt.y)) or \
1479 ((startPt.x > endPt.x) and (startPt.y > endPt.y))
1480
1481 # Handle the user flipping the line.
1482
1483 hFlip = ((anchorPt.x < oldPt.x) and (anchorPt.x > newPt.x)) or \
1484 ((anchorPt.x > oldPt.x) and (anchorPt.x < newPt.x))
1485 vFlip = ((anchorPt.y < oldPt.y) and (anchorPt.y > newPt.y)) or \
1486 ((anchorPt.y > oldPt.y) and (anchorPt.y < newPt.y))
1487
1488 if (hFlip and not vFlip) or (vFlip and not hFlip):
1489 slopesDown = not slopesDown # Line flipped.
1490
1491 if slopesDown:
1492 obj.setStartPt(wx.Point(0, 0))
1493 obj.setEndPt(wx.Point(newWidth, newHeight))
1494 else:
1495 obj.setStartPt(wx.Point(0, newHeight))
1496 obj.setEndPt(wx.Point(newWidth, 0))
1497
1498 # Finally, adjust the bounds of the object to match the new dimensions.
1499
1500 obj.setPosition(topLeft)
1501 obj.setSize(wx.Size(botRight.x - topLeft.x, botRight.y - topLeft.y))
1502
1503 self.requestRedraw()
1504
1505
1506 def _moveObject(self, offsetX, offsetY):
1507 """ Move the currently selected object(s) by the given offset.
1508 """
1509 self.saveUndoInfo()
1510
1511 for obj in self.selection:
1512 pos = obj.getPosition()
1513 pos.x = pos.x + offsetX
1514 pos.y = pos.y + offsetY
1515 obj.setPosition(pos)
1516
1517 self.requestRedraw()
1518
1519
1520 def _buildLineSizePopup(self, lineSize):
1521 """ Build the pop-up menu used to set the line size.
1522
1523 'lineSize' is the current line size value. The corresponding item
1524 is checked in the pop-up menu.
1525 """
1526 menu = wx.Menu()
1527 menu.Append(id_LINESIZE_0, "no line", kind=wx.ITEM_CHECK)
1528 menu.Append(id_LINESIZE_1, "1-pixel line", kind=wx.ITEM_CHECK)
1529 menu.Append(id_LINESIZE_2, "2-pixel line", kind=wx.ITEM_CHECK)
1530 menu.Append(id_LINESIZE_3, "3-pixel line", kind=wx.ITEM_CHECK)
1531 menu.Append(id_LINESIZE_4, "4-pixel line", kind=wx.ITEM_CHECK)
1532 menu.Append(id_LINESIZE_5, "5-pixel line", kind=wx.ITEM_CHECK)
1533
1534 if lineSize == 0: menu.Check(id_LINESIZE_0, True)
1535 elif lineSize == 1: menu.Check(id_LINESIZE_1, True)
1536 elif lineSize == 2: menu.Check(id_LINESIZE_2, True)
1537 elif lineSize == 3: menu.Check(id_LINESIZE_3, True)
1538 elif lineSize == 4: menu.Check(id_LINESIZE_4, True)
1539 elif lineSize == 5: menu.Check(id_LINESIZE_5, True)
1540
1541 self.Bind(wx.EVT_MENU, self._lineSizePopupSelected, id=id_LINESIZE_0, id2=id_LINESIZE_5)
1542
1543 return menu
1544
1545
1546 def _lineSizePopupSelected(self, event):
1547 """ Respond to the user selecting an item from the line size popup menu
1548 """
1549 id = event.GetId()
1550 if id == id_LINESIZE_0: self._setLineSize(0)
1551 elif id == id_LINESIZE_1: self._setLineSize(1)
1552 elif id == id_LINESIZE_2: self._setLineSize(2)
1553 elif id == id_LINESIZE_3: self._setLineSize(3)
1554 elif id == id_LINESIZE_4: self._setLineSize(4)
1555 elif id == id_LINESIZE_5: self._setLineSize(5)
1556 else:
1557 wx.Bell()
1558 return
1559
1560 self.optionIndicator.setLineSize(self.lineSize)
1561
1562
1563 def _getEventCoordinates(self, event):
1564 """ Return the coordinates associated with the given mouse event.
1565
1566 The coordinates have to be adjusted to allow for the current scroll
1567 position.
1568 """
1569 originX, originY = self.drawPanel.GetViewStart()
1570 unitX, unitY = self.drawPanel.GetScrollPixelsPerUnit()
1571 return wx.Point(event.GetX() + (originX * unitX),
1572 event.GetY() + (originY * unitY))
1573
1574
1575 def _drawObjectOutline(self, offsetX, offsetY):
1576 """ Draw an outline of the currently selected object.
1577
1578 The selected object's outline is drawn at the object's position
1579 plus the given offset.
1580
1581 Note that the outline is drawn by *inverting* the window's
1582 contents, so calling _drawObjectOutline twice in succession will
1583 restore the window's contents back to what they were previously.
1584 """
1585 if len(self.selection) != 1: return
1586
1587 position = self.selection[0].getPosition()
1588 size = self.selection[0].getSize()
1589
1590 dc = wx.ClientDC(self.drawPanel)
1591 self.drawPanel.PrepareDC(dc)
1592 dc.BeginDrawing()
1593 dc.SetPen(wx.BLACK_DASHED_PEN)
1594 dc.SetBrush(wx.TRANSPARENT_BRUSH)
1595 dc.SetLogicalFunction(wx.INVERT)
1596
1597 dc.DrawRectangle(position.x + offsetX, position.y + offsetY,
1598 size.width, size.height)
1599
1600 dc.EndDrawing()
1601
1602
1603 #============================================================================
1604 class DrawingTool(object):
1605 """Base class for drawing tools"""
1606
1607 def __init__(self):
1608 pass
1609
1610 def getDefaultCursor(self):
1611 """Return the cursor to use by default which this drawing tool is selected"""
1612 return wx.STANDARD_CURSOR
1613
1614 def draw(self,dc):
1615 pass
1616
1617
1618 def onMouseEvent(self,parent, event):
1619 """Mouse events passed in from the parent.
1620
1621 Returns True if the event is handled by the tool,
1622 False if the canvas can try to use it.
1623 """
1624 event.Skip()
1625 return False
1626
1627 #----------------------------------------------------------------------------
1628 class SelectDrawingTool(DrawingTool):
1629 """Represents the tool for selecting things"""
1630
1631 def __init__(self):
1632 self.curHandle = None
1633 self.curObject = None
1634 self.objModified = False
1635 self.startPt = None
1636 self.curPt = None
1637
1638 def getDefaultCursor(self):
1639 """Return the cursor to use by default which this drawing tool is selected"""
1640 return wx.STANDARD_CURSOR
1641
1642 def draw(self, dc):
1643 if self._doingRectSelection():
1644 dc.SetPen(wx.BLACK_DASHED_PEN)
1645 dc.SetBrush(wx.TRANSPARENT_BRUSH)
1646 x = [self.startPt.x, self.curPt.x]; x.sort()
1647 y = [self.startPt.y, self.curPt.y]; y.sort()
1648 dc.DrawRectangle(x[0],y[0], x[1]-x[0],y[1]-y[0])
1649
1650
1651 def onMouseEvent(self,parent, event):
1652 handlers = { wx.EVT_LEFT_DOWN.evtType[0]: self.onMouseLeftDown,
1653 wx.EVT_MOTION.evtType[0]: self.onMouseMotion,
1654 wx.EVT_LEFT_UP.evtType[0]: self.onMouseLeftUp,
1655 wx.EVT_LEFT_DCLICK.evtType[0]: self.onMouseLeftDClick }
1656 handler = handlers.get(event.GetEventType())
1657 if handler is not None:
1658 return handler(parent,event)
1659 else:
1660 event.Skip()
1661 return False
1662
1663 def onMouseLeftDown(self,parent,event):
1664 mousePt = wx.Point(event.X,event.Y)
1665 obj, handle = parent.getObjectAndSelectionHandleAt(mousePt)
1666 self.startPt = mousePt
1667 self.curPt = mousePt
1668 if obj is not None and handle is not None:
1669 self.curObject = obj
1670 self.curHandle = handle
1671 else:
1672 self.curObject = None
1673 self.curHandle = None
1674
1675 obj = parent.getObjectAt(mousePt)
1676 if self.curObject is None and obj is not None:
1677 self.curObject = obj
1678 self.dragDelta = obj.position-mousePt
1679 self.curHandle = None
1680 parent.select(obj, event.ShiftDown())
1681
1682 return True
1683
1684 def onMouseMotion(self,parent,event):
1685 if not event.LeftIsDown(): return
1686
1687 self.curPt = wx.Point(event.X,event.Y)
1688
1689 obj,handle = self.curObject,self.curHandle
1690 if self._doingDragHandle():
1691 self._prepareToModify(parent)
1692 obj.moveHandle(handle,event.X,event.Y)
1693 parent.requestRedraw()
1694
1695 elif self._doingDragObject():
1696 self._prepareToModify(parent)
1697 obj.position = self.curPt + self.dragDelta
1698 parent.requestRedraw()
1699
1700 elif self._doingRectSelection():
1701 parent.requestRedraw()
1702
1703 return True
1704
1705 def onMouseLeftUp(self,parent,event):
1706
1707 obj,handle = self.curObject,self.curHandle
1708 if self._doingDragHandle():
1709 obj.moveHandle(handle,event.X,event.Y)
1710 obj.finalizeHandle(handle,event.X,event.Y)
1711
1712 elif self._doingDragObject():
1713 curPt = wx.Point(event.X,event.Y)
1714 obj.position = curPt + self.dragDelta
1715
1716 elif self._doingRectSelection():
1717 x = [event.X, self.startPt.x]
1718 y = [event.Y, self.startPt.y]
1719 x.sort()
1720 y.sort()
1721 parent.selectByRectangle(x[0],y[0],x[1]-x[0],y[1]-y[0])
1722
1723
1724 self.curObject = None
1725 self.curHandle = None
1726 self.curPt = None
1727 self.startPt = None
1728 self.objModified = False
1729 parent.requestRedraw()
1730
1731 return True
1732
1733 def onMouseLeftDClick(self,parent,event):
1734 event.Skip()
1735 mousePt = wx.Point(event.X,event.Y)
1736 obj = parent.getObjectAt(mousePt)
1737 if obj and obj.hasPropertyEditor():
1738 if obj.doPropertyEdit(parent):
1739 parent.requestRedraw()
1740 return True
1741
1742 return False
1743
1744
1745 def _prepareToModify(self,parent):
1746 if not self.objModified:
1747 parent.saveUndoInfo()
1748 self.objModified = True
1749
1750 def _doingRectSelection(self):
1751 return self.curObject is None \
1752 and self.startPt is not None \
1753 and self.curPt is not None
1754
1755 def _doingDragObject(self):
1756 return self.curObject is not None and self.curHandle is None
1757
1758 def _doingDragHandle(self):
1759 return self.curObject is not None and self.curHandle is not None
1760
1761
1762
1763 #----------------------------------------------------------------------------
1764 class LineDrawingTool(DrawingTool):
1765 """Represents the tool for drawing lines"""
1766
1767 def __init__(self):
1768 self.newObject = None
1769 self.startPt = None
1770
1771
1772 def getDefaultCursor(self):
1773 """Return the cursor to use by default which this drawing tool is selected"""
1774 return wx.StockCursor(wx.CURSOR_PENCIL)
1775
1776 def draw(self, dc):
1777 if self.newObject is None: return
1778 self.newObject.draw(dc,True)
1779
1780 def onMouseEvent(self,parent, event):
1781 handlers = { wx.EVT_LEFT_DOWN.evtType[0]: self.onMouseLeftDown,
1782 wx.EVT_MOTION.evtType[0]: self.onMouseMotion,
1783 wx.EVT_LEFT_UP.evtType[0]: self.onMouseLeftUp }
1784 handler = handlers.get(event.GetEventType())
1785 if handler is not None:
1786 return handler(parent,event)
1787 else:
1788 event.Skip()
1789 return False
1790
1791 def onMouseLeftDown(self,parent, event):
1792 self.startPt = wx.Point(event.GetX(), event.GetY())
1793 self.newObject = None
1794 event.Skip()
1795 return True
1796
1797 def onMouseMotion(self,parent, event):
1798 if not event.Dragging(): return
1799
1800 if self.newObject is None:
1801 obj = LineDrawingObject(startPt=wx.Point(0,0),
1802 penColour=parent.penColour,
1803 fillColour=parent.fillColour,
1804 lineSize=parent.lineSize,
1805 position=wx.Point(event.X,event.Y))
1806 self.newObject = obj
1807
1808 self._updateObjFromEvent(self.newObject, event)
1809
1810 parent.requestRedraw()
1811 event.Skip()
1812 return True
1813
1814 def onMouseLeftUp(self,parent, event):
1815
1816 if self.newObject is None:
1817 return
1818
1819 self._updateObjFromEvent(self.newObject,event)
1820
1821 parent.addObject(self.newObject)
1822
1823 self.newObject = None
1824 self.startPt = None
1825
1826 event.Skip()
1827 return True
1828
1829
1830 def _updateObjFromEvent(self,obj,event):
1831 obj.setEndPt(wx.Point(event.X,event.Y))
1832
1833
1834 #----------------------------------------------------------------------------
1835 class RectDrawingTool(DrawingTool):
1836 """Represents the tool for drawing rectangles"""
1837
1838 def __init__(self):
1839 self.newObject = None
1840
1841 def getDefaultCursor(self):
1842 """Return the cursor to use by default which this drawing tool is selected"""
1843 return wx.CROSS_CURSOR
1844
1845 def draw(self, dc):
1846 if self.newObject is None: return
1847 self.newObject.draw(dc,True)
1848
1849
1850 def onMouseEvent(self,parent, event):
1851 handlers = { wx.EVT_LEFT_DOWN.evtType[0]: self.onMouseLeftDown,
1852 wx.EVT_MOTION.evtType[0]: self.onMouseMotion,
1853 wx.EVT_LEFT_UP.evtType[0]: self.onMouseLeftUp }
1854 handler = handlers.get(event.GetEventType())
1855 if handler is not None:
1856 return handler(parent,event)
1857 else:
1858 event.Skip()
1859 return False
1860
1861 def onMouseLeftDown(self,parent, event):
1862 self.startPt = wx.Point(event.GetX(), event.GetY())
1863 self.newObject = None
1864 event.Skip()
1865 return True
1866
1867 def onMouseMotion(self,parent, event):
1868 if not event.Dragging(): return
1869
1870 if self.newObject is None:
1871 obj = RectDrawingObject(penColour=parent.penColour,
1872 fillColour=parent.fillColour,
1873 lineSize=parent.lineSize)
1874 self.newObject = obj
1875
1876 self._updateObjFromEvent(self.newObject, event)
1877
1878 parent.requestRedraw()
1879 event.Skip()
1880 return True
1881
1882 def onMouseLeftUp(self,parent, event):
1883
1884 if self.newObject is None:
1885 return
1886
1887 self._updateObjFromEvent(self.newObject,event)
1888
1889 parent.addObject(self.newObject)
1890
1891 self.newObject = None
1892
1893 event.Skip()
1894 return True
1895
1896
1897 def _updateObjFromEvent(self,obj,event):
1898 x = [event.X, self.startPt.x]
1899 y = [event.Y, self.startPt.y]
1900 x.sort()
1901 y.sort()
1902 width = x[1]-x[0]
1903 height = y[1]-y[0]
1904
1905 obj.setPosition(wx.Point(x[0],y[0]))
1906 obj.setSize(wx.Size(width,height))
1907
1908
1909
1910
1911 #----------------------------------------------------------------------------
1912 class EllipseDrawingTool(DrawingTool):
1913 """Represents the tool for drawing ellipses"""
1914
1915 def getDefaultCursor(self):
1916 """Return the cursor to use by default which this drawing tool is selected"""
1917 return wx.CROSS_CURSOR
1918
1919
1920 def __init__(self):
1921 self.newObject = None
1922
1923 def getDefaultCursor(self):
1924 """Return the cursor to use by default which this drawing tool is selected"""
1925 return wx.CROSS_CURSOR
1926
1927 def draw(self, dc):
1928 if self.newObject is None: return
1929 self.newObject.draw(dc,True)
1930
1931
1932 def onMouseEvent(self,parent, event):
1933 handlers = { wx.EVT_LEFT_DOWN.evtType[0]: self.onMouseLeftDown,
1934 wx.EVT_MOTION.evtType[0]: self.onMouseMotion,
1935 wx.EVT_LEFT_UP.evtType[0]: self.onMouseLeftUp }
1936 handler = handlers.get(event.GetEventType())
1937 if handler is not None:
1938 return handler(parent,event)
1939 else:
1940 event.Skip()
1941 return False
1942
1943 def onMouseLeftDown(self,parent, event):
1944 self.startPt = wx.Point(event.GetX(), event.GetY())
1945 self.newObject = None
1946 event.Skip()
1947 return True
1948
1949 def onMouseMotion(self,parent, event):
1950 if not event.Dragging(): return
1951
1952 if self.newObject is None:
1953 obj = EllipseDrawingObject(penColour=parent.penColour,
1954 fillColour=parent.fillColour,
1955 lineSize=parent.lineSize)
1956 self.newObject = obj
1957
1958 self._updateObjFromEvent(self.newObject, event)
1959
1960 parent.requestRedraw()
1961 event.Skip()
1962 return True
1963
1964 def onMouseLeftUp(self,parent, event):
1965
1966 if self.newObject is None:
1967 return
1968
1969 self._updateObjFromEvent(self.newObject,event)
1970
1971 parent.addObject(self.newObject)
1972
1973 self.newObject = None
1974
1975 event.Skip()
1976 return True
1977
1978
1979 def _updateObjFromEvent(self,obj,event):
1980 x = [event.X, self.startPt.x]
1981 y = [event.Y, self.startPt.y]
1982 x.sort()
1983 y.sort()
1984 width = x[1]-x[0]
1985 height = y[1]-y[0]
1986
1987 obj.setPosition(wx.Point(x[0],y[0]))
1988 obj.setSize(wx.Size(width,height))
1989
1990
1991 #----------------------------------------------------------------------------
1992 class PolygonDrawingTool(DrawingTool):
1993 """Represents the tool for drawing polygons"""
1994
1995 def __init__(self):
1996 self.newObject = None
1997
1998 def getDefaultCursor(self):
1999 """Return the cursor to use by default which this drawing tool is selected"""
2000 return wx.CROSS_CURSOR
2001
2002
2003 def draw(self, dc):
2004 if self.newObject is None: return
2005 self.newObject.draw(dc,True)
2006
2007
2008 def onMouseEvent(self,parent, event):
2009 handlers = { wx.EVT_LEFT_DOWN.evtType[0]: self.onMouseLeftDown,
2010 wx.EVT_MOTION.evtType[0]: self.onMouseMotion,
2011 wx.EVT_LEFT_UP.evtType[0]: self.onMouseLeftUp,
2012 wx.EVT_LEFT_DCLICK.evtType[0]:self.onMouseLeftDClick }
2013 handler = handlers.get(event.GetEventType())
2014 if handler is not None:
2015 return handler(parent,event)
2016 else:
2017 event.Skip()
2018 return False
2019
2020 def onMouseLeftDown(self,parent, event):
2021 event.Skip()
2022 self.startPt = (event.GetX(), event.GetY())
2023 if self.newObject is None:
2024 obj = PolygonDrawingObject(points=[(0,0)],penColour=parent.penColour,
2025 fillColour=parent.fillColour,
2026 lineSize=parent.lineSize,
2027 position=wx.Point(event.X, event.Y))
2028 obj.addPoint(event.X,event.Y)
2029 self.newObject = obj
2030 else:
2031 CLOSE_THRESH=3
2032 pt0 = self.newObject.getPoint(0)
2033 if abs(pt0[0]-event.X)<CLOSE_THRESH and abs(pt0[1]-event.Y)<CLOSE_THRESH:
2034 self.newObject.popPoint()
2035 parent.addObject(self.newObject)
2036 self.newObject = None
2037 else:
2038 self.newObject.addPoint(event.X,event.Y)
2039
2040 return True
2041
2042 def onMouseMotion(self,parent, event):
2043
2044 event.Skip()
2045 if self.newObject:
2046 self.newObject.movePoint(-1, event.X, event.Y)
2047 parent.requestRedraw()
2048 return True
2049
2050 return False
2051
2052 def onMouseLeftDClick(self,parent,event):
2053 event.Skip()
2054 if self.newObject:
2055 CLOSE_THRESH=3
2056 pt0 = self.newObject.getPoint(0)
2057 if abs(pt0[0]-event.X)<CLOSE_THRESH and abs(pt0[1]-event.Y)<CLOSE_THRESH:
2058 self.newObject.popPoint()
2059 self.newObject.popPoint()
2060 parent.addObject(self.newObject)
2061 self.newObject = None
2062
2063 return True
2064
2065 def onMouseLeftUp(self,parent, event):
2066 event.Skip()
2067 return True
2068
2069
2070
2071
2072 #----------------------------------------------------------------------------
2073 class ScribbleDrawingTool(DrawingTool):
2074 """Represents the tool for drawing scribble drawing objects"""
2075
2076 def __init__(self):
2077 self.newObject = None
2078
2079 def getDefaultCursor(self):
2080 """Return the cursor to use by default which this drawing tool is selected"""
2081 return wx.StockCursor(wx.CURSOR_PENCIL)
2082
2083 def draw(self, dc):
2084 if self.newObject is None: return
2085 self.newObject.draw(dc,True)
2086
2087
2088 def onMouseEvent(self,parent, event):
2089 handlers = { wx.EVT_LEFT_DOWN.evtType[0]: self.onMouseLeftDown,
2090 wx.EVT_MOTION.evtType[0]: self.onMouseMotion,
2091 wx.EVT_LEFT_UP.evtType[0]: self.onMouseLeftUp
2092 }
2093 handler = handlers.get(event.GetEventType())
2094 if handler is not None:
2095 return handler(parent,event)
2096 else:
2097 event.Skip()
2098 return False
2099
2100 def onMouseLeftDown(self,parent, event):
2101 event.Skip()
2102 obj = ScribbleDrawingObject(points=[(0,0)],penColour=parent.penColour,
2103 fillColour=parent.fillColour,
2104 lineSize=parent.lineSize,
2105 position=wx.Point(event.X, event.Y))
2106 self.newObject = obj
2107 return True
2108
2109 def onMouseMotion(self,parent, event):
2110 event.Skip()
2111 if self.newObject:
2112 self.newObject.addPoint(event.X,event.Y)
2113 parent.requestRedraw()
2114 return True
2115
2116 return False
2117
2118 def onMouseLeftUp(self,parent, event):
2119 event.Skip()
2120 if self.newObject:
2121 parent.addObject(self.newObject)
2122 self.newObject = None
2123 return True
2124
2125
2126
2127
2128
2129 #----------------------------------------------------------------------------
2130 class TextDrawingTool(DrawingTool):
2131 """Represents the tool for drawing text"""
2132
2133 def getDefaultCursor(self):
2134 """Return the cursor to use by default which this drawing tool is selected"""
2135 return wx.StockCursor(wx.CURSOR_IBEAM)
2136
2137 def onMouseEvent(self,parent, event):
2138 handlers = { #wx.EVT_LEFT_DOWN.evtType[0]: self.onMouseLeftDown,
2139 #wx.EVT_MOTION.evtType[0]: self.onMouseMotion,
2140 wx.EVT_LEFT_UP.evtType[0]: self.onMouseLeftUp
2141 }
2142 handler = handlers.get(event.GetEventType())
2143 if handler is not None:
2144 return handler(parent,event)
2145 else:
2146 event.Skip()
2147 return False
2148
2149 def onMouseLeftUp(self,parent, event):
2150
2151 editor = parent.getTextEditor()
2152 editor.SetTitle("Create Text Object")
2153 if editor.ShowModal() == wx.ID_CANCEL:
2154 editor.Hide()
2155 return True
2156
2157 obj = TextDrawingObject(position=wx.Point(event.X, event.Y))
2158 editor.dialogToObject(obj)
2159 editor.Hide()
2160
2161 parent.addObject(obj)
2162
2163 event.Skip()
2164 return True
2165
2166
2167
2168 #============================================================================
2169 class DrawingObject(object):
2170 """ Base class for objects within the drawing panel.
2171
2172 A pySketch document consists of a front-to-back ordered list of
2173 DrawingObjects. Each DrawingObject has the following properties:
2174
2175 'position' The position of the object within the document.
2176 'size' The size of the object within the document.
2177 'penColour' The colour to use for drawing the object's outline.
2178 'fillColour' Colour to use for drawing object's interior.
2179 'lineSize' Line width (in pixels) to use for object's outline.
2180 """
2181
2182 # ==================
2183 # == Constructors ==
2184 # ==================
2185
2186 def __init__(self, position=wx.Point(0, 0), size=wx.Size(0, 0),
2187 penColour=wx.BLACK, fillColour=wx.WHITE, lineSize=1,
2188 ):
2189 """ Standard constructor.
2190
2191 The remaining parameters let you set various options for the newly
2192 created DrawingObject.
2193 """
2194 # One must take great care with constructed default arguments
2195 # like wx.Point(0,0) above. *EVERY* caller that uses the
2196 # default will get the same instance. Thus, below we make a
2197 # deep copy of those arguments with object defaults.
2198
2199 self.position = wx.Point(position.x,position.y)
2200 self.size = wx.Size(size.x,size.y)
2201 self.penColour = penColour
2202 self.fillColour = fillColour
2203 self.lineSize = lineSize
2204
2205 # =============================
2206 # == Object Property Methods ==
2207 # =============================
2208
2209 def getData(self):
2210 """ Return a copy of the object's internal data.
2211
2212 This is used to save this DrawingObject to disk.
2213 """
2214 return [self.position.x, self.position.y,
2215 self.size.width, self.size.height,
2216 self.penColour.Red(),
2217 self.penColour.Green(),
2218 self.penColour.Blue(),
2219 self.fillColour.Red(),
2220 self.fillColour.Green(),
2221 self.fillColour.Blue(),
2222 self.lineSize]
2223
2224
2225 def setData(self, data):
2226 """ Set the object's internal data.
2227
2228 'data' is a copy of the object's saved data, as returned by
2229 getData() above. This is used to restore a previously saved
2230 DrawingObject.
2231
2232 Returns an iterator to any remaining data not consumed by
2233 this base class method.
2234 """
2235 #data = copy.deepcopy(data) # Needed?
2236
2237 d = iter(data)
2238 try:
2239 self.position = wx.Point(d.next(), d.next())
2240 self.size = wx.Size(d.next(), d.next())
2241 self.penColour = wx.Colour(red=d.next(),
2242 green=d.next(),
2243 blue=d.next())
2244 self.fillColour = wx.Colour(red=d.next(),
2245 green=d.next(),
2246 blue=d.next())
2247 self.lineSize = d.next()
2248 except StopIteration:
2249 raise ValueError('Not enough data in setData call')
2250
2251 return d
2252
2253
2254 def hasPropertyEditor(self):
2255 return False
2256
2257 def doPropertyEdit(self, parent):
2258 assert False, "Must be overridden if hasPropertyEditor returns True"
2259
2260 def setPosition(self, position):
2261 """ Set the origin (top-left corner) for this DrawingObject.
2262 """
2263 self.position = position
2264
2265
2266 def getPosition(self):
2267 """ Return this DrawingObject's position.
2268 """
2269 return self.position
2270
2271
2272 def setSize(self, size):
2273 """ Set the size for this DrawingObject.
2274 """
2275 self.size = size
2276
2277
2278 def getSize(self):
2279 """ Return this DrawingObject's size.
2280 """
2281 return self.size
2282
2283
2284 def setPenColour(self, colour):
2285 """ Set the pen colour used for this DrawingObject.
2286 """
2287 self.penColour = colour
2288
2289
2290 def getPenColour(self):
2291 """ Return this DrawingObject's pen colour.
2292 """
2293 return self.penColour
2294
2295
2296 def setFillColour(self, colour):
2297 """ Set the fill colour used for this DrawingObject.
2298 """
2299 self.fillColour = colour
2300
2301
2302 def getFillColour(self):
2303 """ Return this DrawingObject's fill colour.
2304 """
2305 return self.fillColour
2306
2307
2308 def setLineSize(self, lineSize):
2309 """ Set the linesize used for this DrawingObject.
2310 """
2311 self.lineSize = lineSize
2312
2313
2314 def getLineSize(self):
2315 """ Return this DrawingObject's line size.
2316 """
2317 return self.lineSize
2318
2319
2320 # ============================
2321 # == Object Drawing Methods ==
2322 # ============================
2323
2324 def draw(self, dc, selected):
2325 """ Draw this DrawingObject into our window.
2326
2327 'dc' is the device context to use for drawing.
2328
2329 If 'selected' is True, the object is currently selected.
2330 Drawing objects can use this to change the way selected objects
2331 are drawn, however the actual drawing of selection handles
2332 should be done in the 'drawHandles' method
2333 """
2334 if self.lineSize == 0:
2335 dc.SetPen(wx.Pen(self.penColour, self.lineSize, wx.TRANSPARENT))
2336 else:
2337 dc.SetPen(wx.Pen(self.penColour, self.lineSize, wx.SOLID))
2338 dc.SetBrush(wx.Brush(self.fillColour, wx.SOLID))
2339
2340 self._privateDraw(dc, self.position, selected)
2341
2342
2343 def drawHandles(self, dc):
2344 """Draw selection handles for this DrawingObject"""
2345
2346 # Default is to draw selection handles at all four corners.
2347 dc.SetPen(wx.BLACK_PEN)
2348 dc.SetBrush(wx.BLACK_BRUSH)
2349
2350 x,y = self.position
2351 self._drawSelHandle(dc, x, y)
2352 self._drawSelHandle(dc, x + self.size.width, y)
2353 self._drawSelHandle(dc, x, y + self.size.height)
2354 self._drawSelHandle(dc, x + self.size.width, y + self.size.height)
2355
2356
2357 # =======================
2358 # == Selection Methods ==
2359 # =======================
2360
2361 def objectContainsPoint(self, x, y):
2362 """ Returns True iff this object contains the given point.
2363
2364 This is used to determine if the user clicked on the object.
2365 """
2366 # Firstly, ignore any points outside of the object's bounds.
2367
2368 if x < self.position.x: return False
2369 if x > self.position.x + self.size.x: return False
2370 if y < self.position.y: return False
2371 if y > self.position.y + self.size.y: return False
2372
2373 # Now things get tricky. There's no straightforward way of
2374 # knowing whether the point is within an arbitrary object's
2375 # bounds...to get around this, we draw the object into a
2376 # memory-based bitmap and see if the given point was drawn.
2377 # This could no doubt be done more efficiently by some tricky
2378 # maths, but this approach works and is simple enough.
2379
2380 # Subclasses can implement smarter faster versions of this.
2381
2382 bitmap = wx.EmptyBitmap(self.size.x + 10, self.size.y + 10)
2383 dc = wx.MemoryDC()
2384 dc.SelectObject(bitmap)
2385 dc.BeginDrawing()
2386 dc.SetBackground(wx.WHITE_BRUSH)
2387 dc.Clear()
2388 dc.SetPen(wx.Pen(wx.BLACK, self.lineSize + 5, wx.SOLID))
2389 dc.SetBrush(wx.BLACK_BRUSH)
2390 self._privateDraw(dc, wx.Point(5, 5), True)
2391 dc.EndDrawing()
2392 pixel = dc.GetPixel(x - self.position.x + 5, y - self.position.y + 5)
2393 if (pixel.Red() == 0) and (pixel.Green() == 0) and (pixel.Blue() == 0):
2394 return True
2395 else:
2396 return False
2397
2398 handle_TOP = 0
2399 handle_BOTTOM = 1
2400 handle_LEFT = 0
2401 handle_RIGHT = 1
2402
2403 def getSelectionHandleContainingPoint(self, x, y):
2404 """ Return the selection handle containing the given point, if any.
2405
2406 We return one of the predefined selection handle ID codes.
2407 """
2408 # Default implementation assumes selection handles at all four bbox corners.
2409 # Return a list so we can modify the contents later in moveHandle()
2410 if self._pointInSelRect(x, y, self.position.x, self.position.y):
2411 return [self.handle_TOP, self.handle_LEFT]
2412 elif self._pointInSelRect(x, y, self.position.x + self.size.width,
2413 self.position.y):
2414 return [self.handle_TOP, self.handle_RIGHT]
2415 elif self._pointInSelRect(x, y, self.position.x,
2416 self.position.y + self.size.height):
2417 return [self.handle_BOTTOM, self.handle_LEFT]
2418 elif self._pointInSelRect(x, y, self.position.x + self.size.width,
2419 self.position.y + self.size.height):
2420 return [self.handle_BOTTOM, self.handle_RIGHT]
2421 else:
2422 return None
2423
2424 def moveHandle(self, handle, x, y):
2425 """ Move the specified selection handle to given canvas location.
2426 """
2427 assert handle is not None
2428
2429 # Default implementation assumes selection handles at all four bbox corners.
2430 pt = wx.Point(x,y)
2431 x,y = self.position
2432 w,h = self.size
2433 if handle[0] == self.handle_TOP:
2434 if handle[1] == self.handle_LEFT:
2435 dpos = pt - self.position
2436 self.position = pt
2437 self.size.width -= dpos.x
2438 self.size.height -= dpos.y
2439 else:
2440 dx = pt.x - ( x + w )
2441 dy = pt.y - ( y )
2442 self.position.y = pt.y
2443 self.size.width += dx
2444 self.size.height -= dy
2445 else: # BOTTOM
2446 if handle[1] == self.handle_LEFT:
2447 dx = pt.x - ( x )
2448 dy = pt.y - ( y + h )
2449 self.position.x = pt.x
2450 self.size.width -= dx
2451 self.size.height += dy
2452 else:
2453 dpos = pt - self.position
2454 dpos.x -= w
2455 dpos.y -= h
2456 self.size.width += dpos.x
2457 self.size.height += dpos.y
2458
2459
2460 # Finally, normalize so no negative widths or heights.
2461 # And update the handle variable accordingly.
2462 if self.size.height<0:
2463 self.position.y += self.size.height
2464 self.size.height = -self.size.height
2465 handle[0] = 1-handle[0]
2466
2467 if self.size.width<0:
2468 self.position.x += self.size.width
2469 self.size.width = -self.size.width
2470 handle[1] = 1-handle[1]
2471
2472
2473
2474 def finalizeHandle(self, handle, x, y):
2475 pass
2476
2477
2478 def objectWithinRect(self, x, y, width, height):
2479 """ Return True iff this object falls completely within the given rect.
2480 """
2481 if x > self.position.x: return False
2482 if x + width < self.position.x + self.size.width: return False
2483 if y > self.position.y: return False
2484 if y + height < self.position.y + self.size.height: return False
2485 return True
2486
2487 # =====================
2488 # == Private Methods ==
2489 # =====================
2490
2491 def _privateDraw(self, dc, position, selected):
2492 """ Private routine to draw this DrawingObject.
2493
2494 'dc' is the device context to use for drawing, while 'position' is
2495 the position in which to draw the object.
2496 """
2497 pass
2498
2499 def _drawSelHandle(self, dc, x, y):
2500 """ Draw a selection handle around this DrawingObject.
2501
2502 'dc' is the device context to draw the selection handle within,
2503 while 'x' and 'y' are the coordinates to use for the centre of the
2504 selection handle.
2505 """
2506 dc.DrawRectangle(x - 3, y - 3, 6, 6)
2507
2508
2509 def _pointInSelRect(self, x, y, rX, rY):
2510 """ Return True iff (x, y) is within the selection handle at (rX, ry).
2511 """
2512 if x < rX - 3: return False
2513 elif x > rX + 3: return False
2514 elif y < rY - 3: return False
2515 elif y > rY + 3: return False
2516 else: return True
2517
2518
2519 #----------------------------------------------------------------------------
2520 class LineDrawingObject(DrawingObject):
2521 """ DrawingObject subclass that represents one line segment.
2522
2523 Adds the following members to the base DrawingObject:
2524 'startPt' The point, relative to the object's position, where
2525 the line starts.
2526 'endPt' The point, relative to the object's position, where
2527 the line ends.
2528 """
2529
2530 def __init__(self, startPt=wx.Point(0,0), endPt=wx.Point(0,0), *varg, **kwarg):
2531 DrawingObject.__init__(self, *varg, **kwarg)
2532
2533 self.startPt = wx.Point(startPt.x,startPt.y)
2534 self.endPt = wx.Point(endPt.x,endPt.y)
2535
2536 # ============================
2537 # == Object Drawing Methods ==
2538 # ============================
2539
2540 def drawHandles(self, dc):
2541 """Draw selection handles for this DrawingObject"""
2542
2543 dc.SetPen(wx.BLACK_PEN)
2544 dc.SetBrush(wx.BLACK_BRUSH)
2545
2546 x,y = self.position
2547 # Draw selection handles at the start and end points.
2548 self._drawSelHandle(dc, x + self.startPt.x, y + self.startPt.y)
2549 self._drawSelHandle(dc, x + self.endPt.x, y + self.endPt.y)
2550
2551
2552
2553 # =======================
2554 # == Selection Methods ==
2555 # =======================
2556
2557
2558 handle_START_POINT = 1
2559 handle_END_POINT = 2
2560
2561 def getSelectionHandleContainingPoint(self, x, y):
2562 """ Return the selection handle containing the given point, if any.
2563
2564 We return one of the predefined selection handle ID codes.
2565 """
2566 # We have selection handles at the start and end points.
2567 if self._pointInSelRect(x, y, self.position.x + self.startPt.x,
2568 self.position.y + self.startPt.y):
2569 return self.handle_START_POINT
2570 elif self._pointInSelRect(x, y, self.position.x + self.endPt.x,
2571 self.position.y + self.endPt.y):
2572 return self.handle_END_POINT
2573 else:
2574 return None
2575
2576 def moveHandle(self, handle, x, y):
2577 """Move the handle to specified handle to the specified canvas coordinates
2578 """
2579 ptTrans = wx.Point(x-self.position.x, y-self.position.y)
2580 if handle == self.handle_START_POINT:
2581 self.startPt = ptTrans
2582 elif handle == self.handle_END_POINT:
2583 self.endPt = ptTrans
2584 else:
2585 raise ValueError("Bad handle type for a line")
2586
2587 self._updateBoundingBox()
2588
2589 # =============================
2590 # == Object Property Methods ==
2591 # =============================
2592
2593 def getData(self):
2594 """ Return a copy of the object's internal data.
2595
2596 This is used to save this DrawingObject to disk.
2597 """
2598 # get the basics
2599 data = DrawingObject.getData(self)
2600 # add our specifics
2601 data += [self.startPt.x, self.startPt.y,
2602 self.endPt.x, self.endPt.y]
2603 return data
2604
2605 def setData(self, data):
2606 """ Set the object's internal data.
2607
2608 'data' is a copy of the object's saved data, as returned by
2609 getData() above. This is used to restore a previously saved
2610 DrawingObject.
2611 """
2612 #data = copy.deepcopy(data) # Needed?
2613
2614 d = DrawingObject.setData(self, data)
2615
2616 try:
2617 self.startPt = wx.Point(d.next(), d.next())
2618 self.endPt = wx.Point(d.next(), d.next())
2619 except StopIteration:
2620 raise ValueError('Not enough data in setData call')
2621
2622 return d
2623
2624
2625 def setStartPt(self, startPt):
2626 """ Set the starting point for this line DrawingObject.
2627 """
2628 self.startPt = startPt - self.position
2629 self._updateBoundingBox()
2630
2631
2632 def getStartPt(self):
2633 """ Return the starting point for this line DrawingObject.
2634 """
2635 return self.startPt + self.position
2636
2637
2638 def setEndPt(self, endPt):
2639 """ Set the ending point for this line DrawingObject.
2640 """
2641 self.endPt = endPt - self.position
2642 self._updateBoundingBox()
2643
2644
2645 def getEndPt(self):
2646 """ Return the ending point for this line DrawingObject.
2647 """
2648 return self.endPt + self.position
2649
2650
2651 # =====================
2652 # == Private Methods ==
2653 # =====================
2654
2655
2656 def _privateDraw(self, dc, position, selected):
2657 """ Private routine to draw this DrawingObject.
2658
2659 'dc' is the device context to use for drawing, while 'position' is
2660 the position in which to draw the object. If 'selected' is True,
2661 the object is drawn with selection handles. This private drawing
2662 routine assumes that the pen and brush have already been set by the
2663 caller.
2664 """
2665 dc.DrawLine(position.x + self.startPt.x,
2666 position.y + self.startPt.y,
2667 position.x + self.endPt.x,
2668 position.y + self.endPt.y)
2669
2670 def _updateBoundingBox(self):
2671 x = [self.startPt.x, self.endPt.x]; x.sort()
2672 y = [self.startPt.y, self.endPt.y]; y.sort()
2673
2674 dp = wx.Point(-x[0],-y[0])
2675 self.position.x += x[0]
2676 self.position.y += y[0]
2677 self.size.width = x[1]-x[0]
2678 self.size.height = y[1]-y[0]
2679
2680 self.startPt += dp
2681 self.endPt += dp
2682
2683 #----------------------------------------------------------------------------
2684 class PolygonDrawingObject(DrawingObject):
2685 """ DrawingObject subclass that represents a poly-line or polygon
2686 """
2687 def __init__(self, points=[], *varg, **kwarg):
2688 DrawingObject.__init__(self, *varg, **kwarg)
2689 self.points = list(points)
2690
2691 # =======================
2692 # == Selection Methods ==
2693 # =======================
2694
2695 def getSelectionHandleContainingPoint(self, x, y):
2696 """ Return the selection handle containing the given point, if any.
2697
2698 We return one of the predefined selection handle ID codes.
2699 """
2700 # We have selection handles at the start and end points.
2701 for i,p in enumerate(self.points):
2702 if self._pointInSelRect(x, y,
2703 self.position.x + p[0],
2704 self.position.y + p[1]):
2705 return i+1
2706
2707 return None
2708
2709
2710 def addPoint(self, x,y):
2711 self.points.append((x-self.position.x,y-self.position.y))
2712 self._updateBoundingBox()
2713
2714 def getPoint(self, idx):
2715 x,y = self.points[idx]
2716 return (x+self.position.x,y+self.position.y)
2717
2718 def movePoint(self, idx, x,y):
2719 self.points[idx] = (x-self.position.x,y-self.position.y)
2720 self._updateBoundingBox()
2721
2722 def popPoint(self, idx=-1):
2723 self.points.pop(idx)
2724 self._updateBoundingBox()
2725
2726 # =====================
2727 # == Drawing Methods ==
2728 # =====================
2729
2730 def drawHandles(self, dc):
2731 """Draw selection handles for this DrawingObject"""
2732
2733 dc.SetPen(wx.BLACK_PEN)
2734 dc.SetBrush(wx.BLACK_BRUSH)
2735
2736 x,y = self.position
2737 # Draw selection handles at the start and end points.
2738 for p in self.points:
2739 self._drawSelHandle(dc, x + p[0], y + p[1])
2740
2741 def moveHandle(self, handle, x, y):
2742 """Move the specified handle"""
2743 self.movePoint(handle-1,x,y)
2744
2745
2746 # =============================
2747 # == Object Property Methods ==
2748 # =============================
2749
2750 def getData(self):
2751 """ Return a copy of the object's internal data.
2752
2753 This is used to save this DrawingObject to disk.
2754 """
2755 # get the basics
2756 data = DrawingObject.getData(self)
2757 # add our specifics
2758 data += [list(self.points)]
2759
2760 return data
2761
2762
2763 def setData(self, data):
2764 """ Set the object's internal data.
2765
2766 'data' is a copy of the object's saved data, as returned by
2767 getData() above. This is used to restore a previously saved
2768 DrawingObject.
2769 """
2770 #data = copy.deepcopy(data) # Needed?
2771 d = DrawingObject.setData(self, data)
2772
2773 try:
2774 self.points = d.next()
2775 except StopIteration:
2776 raise ValueError('Not enough data in setData call')
2777
2778 return d
2779
2780
2781 # =====================
2782 # == Private Methods ==
2783 # =====================
2784 def _privateDraw(self, dc, position, selected):
2785 """ Private routine to draw this DrawingObject.
2786
2787 'dc' is the device context to use for drawing, while 'position' is
2788 the position in which to draw the object. If 'selected' is True,
2789 the object is drawn with selection handles. This private drawing
2790 routine assumes that the pen and brush have already been set by the
2791 caller.
2792 """
2793 dc.DrawPolygon(self.points, position.x, position.y)
2794
2795 def _updateBoundingBox(self):
2796 x = min([p[0] for p in self.points])
2797 y = min([p[1] for p in self.points])
2798 x2 = max([p[0] for p in self.points])
2799 y2 = max([p[1] for p in self.points])
2800 dx = -x
2801 dy = -y
2802 self.position.x += x
2803 self.position.y += y
2804 self.size.width = x2-x
2805 self.size.height = y2-y
2806 # update coords also because they're relative to self.position
2807 for i,p in enumerate(self.points):
2808 self.points[i] = (p[0]+dx,p[1]+dy)
2809
2810
2811 #----------------------------------------------------------------------------
2812 class ScribbleDrawingObject(DrawingObject):
2813 """ DrawingObject subclass that represents a poly-line or polygon
2814 """
2815 def __init__(self, points=[], *varg, **kwarg):
2816 DrawingObject.__init__(self, *varg, **kwarg)
2817 self.points = list(points)
2818
2819 # =======================
2820 # == Selection Methods ==
2821 # =======================
2822
2823 def addPoint(self, x,y):
2824 self.points.append((x-self.position.x,y-self.position.y))
2825 self._updateBoundingBox()
2826
2827 def getPoint(self, idx):
2828 x,y = self.points[idx]
2829 return (x+self.position.x,y+self.position.y)
2830
2831 def movePoint(self, idx, x,y):
2832 self.points[idx] = (x-self.position.x,y-self.position.y)
2833 self._updateBoundingBox()
2834
2835 def popPoint(self, idx=-1):
2836 self.points.pop(idx)
2837 self._updateBoundingBox()
2838
2839
2840 # =============================
2841 # == Object Property Methods ==
2842 # =============================
2843
2844 def getData(self):
2845 """ Return a copy of the object's internal data.
2846
2847 This is used to save this DrawingObject to disk.
2848 """
2849 # get the basics
2850 data = DrawingObject.getData(self)
2851 # add our specifics
2852 data += [list(self.points)]
2853
2854 return data
2855
2856
2857 def setData(self, data):
2858 """ Set the object's internal data.
2859
2860 'data' is a copy of the object's saved data, as returned by
2861 getData() above. This is used to restore a previously saved
2862 DrawingObject.
2863 """
2864 #data = copy.deepcopy(data) # Needed?
2865 d = DrawingObject.setData(self, data)
2866
2867 try:
2868 self.points = d.next()
2869 except StopIteration:
2870 raise ValueError('Not enough data in setData call')
2871
2872 return d
2873
2874
2875 # =====================
2876 # == Private Methods ==
2877 # =====================
2878 def _privateDraw(self, dc, position, selected):
2879 """ Private routine to draw this DrawingObject.
2880
2881 'dc' is the device context to use for drawing, while 'position' is
2882 the position in which to draw the object. If 'selected' is True,
2883 the object is drawn with selection handles. This private drawing
2884 routine assumes that the pen and brush have already been set by the
2885 caller.
2886 """
2887 dc.SetBrush(wx.TRANSPARENT_BRUSH)
2888 dc.DrawLines(self.points, position.x, position.y)
2889
2890 def _updateBoundingBox(self):
2891 x = min([p[0] for p in self.points])
2892 y = min([p[1] for p in self.points])
2893 x2 = max([p[0] for p in self.points])
2894 y2 = max([p[1] for p in self.points])
2895 dx = -x
2896 dy = -y
2897 self.position = wx.Point(self.position.x + x,self.position.y + y)
2898 self.size = wx.Size(x2-x, y2-y)
2899 #self.position.x += x
2900 #self.position.y += y
2901 #self.size.width = x2-x
2902 #self.size.height = y2-y
2903 # update coords also because they're relative to self.position
2904 for i,p in enumerate(self.points):
2905 self.points[i] = (p[0]+dx,p[1]+dy)
2906
2907 #----------------------------------------------------------------------------
2908 class RectDrawingObject(DrawingObject):
2909 """ DrawingObject subclass that represents an axis-aligned rectangle.
2910 """
2911 def __init__(self, *varg, **kwarg):
2912 DrawingObject.__init__(self, *varg, **kwarg)
2913
2914 def objectContainsPoint(self, x, y):
2915 """ Returns True iff this object contains the given point.
2916
2917 This is used to determine if the user clicked on the object.
2918 """
2919 # Firstly, ignore any points outside of the object's bounds.
2920
2921 if x < self.position.x: return False
2922 if x > self.position.x + self.size.x: return False
2923 if y < self.position.y: return False
2924 if y > self.position.y + self.size.y: return False
2925
2926 # Rectangles are easy -- they're always selected if the
2927 # point is within their bounds.
2928 return True
2929
2930 # =====================
2931 # == Private Methods ==
2932 # =====================
2933
2934 def _privateDraw(self, dc, position, selected):
2935 """ Private routine to draw this DrawingObject.
2936
2937 'dc' is the device context to use for drawing, while 'position' is
2938 the position in which to draw the object. If 'selected' is True,
2939 the object is drawn with selection handles. This private drawing
2940 routine assumes that the pen and brush have already been set by the
2941 caller.
2942 """
2943 dc.DrawRectangle(position.x, position.y,
2944 self.size.width, self.size.height)
2945
2946
2947 #----------------------------------------------------------------------------
2948 class EllipseDrawingObject(DrawingObject):
2949 """ DrawingObject subclass that represents an axis-aligned ellipse.
2950 """
2951 def __init__(self, *varg, **kwarg):
2952 DrawingObject.__init__(self, *varg, **kwarg)
2953
2954 # =====================
2955 # == Private Methods ==
2956 # =====================
2957 def _privateDraw(self, dc, position, selected):
2958 """ Private routine to draw this DrawingObject.
2959
2960 'dc' is the device context to use for drawing, while 'position' is
2961 the position in which to draw the object. If 'selected' is True,
2962 the object is drawn with selection handles. This private drawing
2963 routine assumes that the pen and brush have already been set by the
2964 caller.
2965 """
2966 dc.DrawEllipse(position.x, position.y,
2967 self.size.width, self.size.height)
2968
2969
2970
2971
2972 #----------------------------------------------------------------------------
2973 class TextDrawingObject(DrawingObject):
2974 """ DrawingObject subclass that holds text.
2975
2976 Adds the following members to the base DrawingObject:
2977 'text' The object's text (obj_TEXT objects only).
2978 'textFont' The text object's font name.
2979 """
2980
2981 def __init__(self, text=None, *varg, **kwarg):
2982 DrawingObject.__init__(self, *varg, **kwarg)
2983
2984 self.text = text
2985 self.textFont = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT)
2986
2987
2988 # =============================
2989 # == Object Property Methods ==
2990 # =============================
2991
2992 def getData(self):
2993 """ Return a copy of the object's internal data.
2994
2995 This is used to save this DrawingObject to disk.
2996 """
2997 # get the basics
2998 data = DrawingObject.getData(self)
2999 # add our specifics
3000 data += [self.text, self.textFont.GetNativeFontInfoDesc()]
3001
3002 return data
3003
3004
3005 def setData(self, data):
3006 """ Set the object's internal data.
3007
3008 'data' is a copy of the object's saved data, as returned by
3009 getData() above. This is used to restore a previously saved
3010 DrawingObject.
3011 """
3012 d = DrawingObject.setData(self, data)
3013
3014 try:
3015 self.text = d.next()
3016 desc = d.next()
3017 self.textFont = wx.FontFromNativeInfoString(desc)
3018 except StopIteration:
3019 raise ValueError('Not enough data in setData call')
3020
3021 return d
3022
3023
3024 def hasPropertyEditor(self):
3025 return True
3026
3027 def doPropertyEdit(self, parent):
3028 editor = parent.getTextEditor()
3029 editor.SetTitle("Edit Text Object")
3030 editor.objectToDialog(self)
3031 if editor.ShowModal() == wx.ID_CANCEL:
3032 editor.Hide()
3033 return False
3034
3035 parent.saveUndoInfo()
3036
3037 editor.dialogToObject(self)
3038 editor.Hide()
3039
3040 return True
3041
3042
3043 def setText(self, text):
3044 """ Set the text for this DrawingObject.
3045 """
3046 self.text = text
3047
3048
3049 def getText(self):
3050 """ Return this DrawingObject's text.
3051 """
3052 return self.text
3053
3054
3055 def setFont(self, font):
3056 """ Set the font for this text DrawingObject.
3057 """
3058 self.textFont = font
3059
3060
3061 def getFont(self):
3062 """ Return this text DrawingObject's font.
3063 """
3064 return self.textFont
3065
3066
3067
3068 # ============================
3069 # == Object Drawing Methods ==
3070 # ============================
3071
3072 def draw(self, dc, selected):
3073 """ Draw this DrawingObject into our window.
3074
3075 'dc' is the device context to use for drawing. If 'selected' is
3076 True, the object is currently selected and should be drawn as such.
3077 """
3078 dc.SetTextForeground(self.penColour)
3079 dc.SetTextBackground(self.fillColour)
3080
3081 self._privateDraw(dc, self.position, selected)
3082
3083 def objectContainsPoint(self, x, y):
3084 """ Returns True iff this object contains the given point.
3085
3086 This is used to determine if the user clicked on the object.
3087 """
3088 # Firstly, ignore any points outside of the object's bounds.
3089
3090 if x < self.position.x: return False
3091 if x > self.position.x + self.size.x: return False
3092 if y < self.position.y: return False
3093 if y > self.position.y + self.size.y: return False
3094
3095 # Text is easy -- it's always selected if the
3096 # point is within its bounds.
3097 return True
3098
3099
3100 def fitToText(self):
3101 """ Resize a text DrawingObject so that it fits it's text exactly.
3102 """
3103
3104 dummyWindow = wx.Frame(None, -1, "")
3105 dummyWindow.SetFont(self.textFont)
3106 width, height = dummyWindow.GetTextExtent(self.text)
3107 dummyWindow.Destroy()
3108
3109 self.size = wx.Size(width, height)
3110
3111 # =====================
3112 # == Private Methods ==
3113 # =====================
3114
3115 def _privateDraw(self, dc, position, selected):
3116 """ Private routine to draw this DrawingObject.
3117
3118 'dc' is the device context to use for drawing, while 'position' is
3119 the position in which to draw the object. If 'selected' is True,
3120 the object is drawn with selection handles. This private drawing
3121 routine assumes that the pen and brush have already been set by the
3122 caller.
3123 """
3124 dc.SetFont(self.textFont)
3125 dc.DrawText(self.text, position.x, position.y)
3126
3127
3128
3129 #----------------------------------------------------------------------------
3130 class ToolPaletteToggleX(wx.ToggleButton):
3131 """ An icon appearing in the tool palette area of our sketching window.
3132
3133 Note that this is actually implemented as a wx.Bitmap rather
3134 than as a wx.Icon. wx.Icon has a very specific meaning, and isn't
3135 appropriate for this more general use.
3136 """
3137
3138 def __init__(self, parent, iconID, iconName, toolTip, mode = wx.ITEM_NORMAL):
3139 """ Standard constructor.
3140
3141 'parent' is the parent window this icon will be part of.
3142 'iconID' is the internal ID used for this icon.
3143 'iconName' is the name used for this icon.
3144 'toolTip' is the tool tip text to show for this icon.
3145 'mode' is one of wx.ITEM_NORMAL, wx.ITEM_CHECK, wx.ITEM_RADIO
3146
3147 The icon name is used to get the appropriate bitmap for this icon.
3148 """
3149 bmp = wx.Bitmap("images/" + iconName + "Icon.bmp", wx.BITMAP_TYPE_BMP)
3150 bmpsel = wx.Bitmap("images/" + iconName + "IconSel.bmp", wx.BITMAP_TYPE_BMP)
3151
3152 wx.ToggleButton.__init__(self, parent, iconID,
3153 size=(bmp.GetWidth()+1, bmp.GetHeight()+1)
3154 )
3155 self.SetLabel( iconName )
3156 self.SetToolTip(wx.ToolTip(toolTip))
3157 #self.SetBitmapLabel(bmp)
3158 #self.SetBitmapSelected(bmpsel)
3159
3160 self.iconID = iconID
3161 self.iconName = iconName
3162
3163 class ToolPaletteToggle(GenBitmapToggleButton):
3164 """ An icon appearing in the tool palette area of our sketching window.
3165
3166 Note that this is actually implemented as a wx.Bitmap rather
3167 than as a wx.Icon. wx.Icon has a very specific meaning, and isn't
3168 appropriate for this more general use.
3169 """
3170
3171 def __init__(self, parent, iconID, iconName, toolTip, mode = wx.ITEM_NORMAL):
3172 """ Standard constructor.
3173
3174 'parent' is the parent window this icon will be part of.
3175 'iconID' is the internal ID used for this icon.
3176 'iconName' is the name used for this icon.
3177 'toolTip' is the tool tip text to show for this icon.
3178 'mode' is one of wx.ITEM_NORMAL, wx.ITEM_CHECK, wx.ITEM_RADIO
3179
3180 The icon name is used to get the appropriate bitmap for this icon.
3181 """
3182 bmp = wx.Bitmap("images/" + iconName + "Icon.bmp", wx.BITMAP_TYPE_BMP)
3183 bmpsel = wx.Bitmap("images/" + iconName + "IconSel.bmp", wx.BITMAP_TYPE_BMP)
3184
3185 GenBitmapToggleButton.__init__(self, parent, iconID, bitmap=bmp,
3186 size=(bmp.GetWidth()+1, bmp.GetHeight()+1),
3187 style=wx.BORDER_NONE)
3188
3189 self.SetToolTip(wx.ToolTip(toolTip))
3190 self.SetBitmapLabel(bmp)
3191 self.SetBitmapSelected(bmpsel)
3192
3193 self.iconID = iconID
3194 self.iconName = iconName
3195
3196
3197 class ToolPaletteButton(GenBitmapButton):
3198 """ An icon appearing in the tool palette area of our sketching window.
3199
3200 Note that this is actually implemented as a wx.Bitmap rather
3201 than as a wx.Icon. wx.Icon has a very specific meaning, and isn't
3202 appropriate for this more general use.
3203 """
3204
3205 def __init__(self, parent, iconID, iconName, toolTip):
3206 """ Standard constructor.
3207
3208 'parent' is the parent window this icon will be part of.
3209 'iconID' is the internal ID used for this icon.
3210 'iconName' is the name used for this icon.
3211 'toolTip' is the tool tip text to show for this icon.
3212
3213 The icon name is used to get the appropriate bitmap for this icon.
3214 """
3215 bmp = wx.Bitmap("images/" + iconName + "Icon.bmp", wx.BITMAP_TYPE_BMP)
3216 GenBitmapButton.__init__(self, parent, iconID, bitmap=bmp,
3217 size=(bmp.GetWidth()+1, bmp.GetHeight()+1),
3218 style=wx.BORDER_NONE)
3219 self.SetToolTip(wx.ToolTip(toolTip))
3220 self.SetBitmapLabel(bmp)
3221
3222 self.iconID = iconID
3223 self.iconName = iconName
3224
3225
3226
3227 #----------------------------------------------------------------------------
3228
3229 class ToolOptionIndicator(wx.Window):
3230 """ A visual indicator which shows the current tool options.
3231 """
3232 def __init__(self, parent):
3233 """ Standard constructor.
3234 """
3235 wx.Window.__init__(self, parent, -1, wx.DefaultPosition, wx.Size(52, 32))
3236
3237 self.penColour = wx.BLACK
3238 self.fillColour = wx.WHITE
3239 self.lineSize = 1
3240
3241 # Win95 can only handle a 8x8 stipple bitmaps max
3242 #self.stippleBitmap = wx.BitmapFromBits("\xf0"*4 + "\x0f"*4,8,8)
3243 # ...but who uses Win95?
3244 self.stippleBitmap = wx.BitmapFromBits("\xff\x00"*8+"\x00\xff"*8,16,16)
3245 self.stippleBitmap.SetMask(wx.Mask(self.stippleBitmap))
3246
3247 self.Bind(wx.EVT_PAINT, self.onPaint)
3248
3249
3250 def setPenColour(self, penColour):
3251 """ Set the indicator's current pen colour.
3252 """
3253 self.penColour = penColour
3254 self.Refresh()
3255
3256
3257 def setFillColour(self, fillColour):
3258 """ Set the indicator's current fill colour.
3259 """
3260 self.fillColour = fillColour
3261 self.Refresh()
3262
3263
3264 def setLineSize(self, lineSize):
3265 """ Set the indicator's current pen colour.
3266 """
3267 self.lineSize = lineSize
3268 self.Refresh()
3269
3270
3271 def onPaint(self, event):
3272 """ Paint our tool option indicator.
3273 """
3274 dc = wx.PaintDC(self)
3275 dc.BeginDrawing()
3276
3277 dc.SetPen(wx.BLACK_PEN)
3278 bgbrush = wx.Brush(wx.WHITE, wx.STIPPLE_MASK_OPAQUE)
3279 bgbrush.SetStipple(self.stippleBitmap)
3280 dc.SetTextForeground(wx.LIGHT_GREY)
3281 dc.SetTextBackground(wx.WHITE)
3282 dc.SetBrush(bgbrush)
3283 dc.DrawRectangle(0, 0, self.GetSize().width,self.GetSize().height)
3284
3285 if self.lineSize == 0:
3286 dc.SetPen(wx.Pen(self.penColour, self.lineSize, wx.TRANSPARENT))
3287 else:
3288 dc.SetPen(wx.Pen(self.penColour, self.lineSize, wx.SOLID))
3289 dc.SetBrush(wx.Brush(self.fillColour, wx.SOLID))
3290
3291 size = self.GetSize()
3292 ctrx = size.x/2
3293 ctry = size.y/2
3294 radius = min(size)//2 - 5
3295 dc.DrawCircle(ctrx, ctry, radius)
3296
3297 dc.EndDrawing()
3298
3299 #----------------------------------------------------------------------------
3300
3301 class EditTextObjectDialog(wx.Dialog):
3302 """ Dialog box used to edit the properties of a text object.
3303
3304 The user can edit the object's text, font, size, and text style.
3305 """
3306
3307 def __init__(self, parent, title):
3308 """ Standard constructor.
3309 """
3310 wx.Dialog.__init__(self, parent, -1, title,
3311 style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
3312
3313 self.textCtrl = wx.TextCtrl(
3314 self, 1001, "Enter text here", style=wx.TE_PROCESS_ENTER|wx.TE_RICH,
3315 validator=TextObjectValidator()
3316 )
3317 extent = self.textCtrl.GetFullTextExtent("Hy")
3318 lineHeight = extent[1] + extent[3]
3319 self.textCtrl.SetSize(wx.Size(-1, lineHeight * 4))
3320 self.curFont = self.textCtrl.GetFont()
3321 self.curClr = wx.BLACK
3322
3323 self.Bind(wx.EVT_TEXT_ENTER, self._doEnter, id=1001)
3324
3325 fontBtn = wx.Button(self, -1, "Select Font...")
3326 self.Bind(wx.EVT_BUTTON, self.OnSelectFont, fontBtn)
3327
3328 gap = wx.LEFT | wx.TOP | wx.RIGHT
3329
3330 self.okButton = wx.Button(self, wx.ID_OK, "&OK")
3331 self.okButton.SetDefault()
3332 self.cancelButton = wx.Button(self, wx.ID_CANCEL, "&Cancel")
3333
3334 btnSizer = wx.StdDialogButtonSizer()
3335
3336 btnSizer.Add(self.okButton, 0, gap, 5)
3337 btnSizer.Add(self.cancelButton, 0, gap, 5)
3338
3339 sizer = wx.BoxSizer(wx.VERTICAL)
3340 sizer.Add(self.textCtrl, 1, gap | wx.EXPAND, 5)
3341 sizer.Add(fontBtn, 0, gap | wx.ALIGN_RIGHT, 5)
3342 sizer.Add((10, 10)) # Spacer.
3343 btnSizer.Realize()
3344 sizer.Add(btnSizer, 0, gap | wx.ALIGN_CENTRE, 5)
3345
3346 self.SetAutoLayout(True)
3347 self.SetSizer(sizer)
3348 sizer.Fit(self)
3349
3350 self.textCtrl.SetFocus()
3351
3352
3353 def OnSelectFont(self, evt):
3354 """Shows the font dialog and sets the font of the sample text"""
3355 data = wx.FontData()
3356 data.EnableEffects(True)
3357 data.SetColour(self.curClr) # set colour
3358 data.SetInitialFont(self.curFont)
3359
3360 dlg = wx.FontDialog(self, data)
3361
3362 if dlg.ShowModal() == wx.ID_OK:
3363 data = dlg.GetFontData()
3364 font = data.GetChosenFont()
3365 colour = data.GetColour()
3366
3367 self.curFont = font
3368 self.curClr = colour
3369
3370 self.textCtrl.SetFont(font)
3371 # Update dialog for the new height of the text
3372 self.GetSizer().Fit(self)
3373
3374 dlg.Destroy()
3375
3376
3377 def objectToDialog(self, obj):
3378 """ Copy the properties of the given text object into the dialog box.
3379 """
3380 self.textCtrl.SetValue(obj.getText())
3381 self.textCtrl.SetSelection(0, len(obj.getText()))
3382
3383 self.curFont = obj.getFont()
3384 self.textCtrl.SetFont(self.curFont)
3385
3386
3387
3388 def dialogToObject(self, obj):
3389 """ Copy the properties from the dialog box into the given text object.
3390 """
3391 obj.setText(self.textCtrl.GetValue())
3392 obj.setFont(self.curFont)
3393 obj.fitToText()
3394
3395 # ======================
3396 # == Private Routines ==
3397 # ======================
3398
3399 def _doEnter(self, event):
3400 """ Respond to the user hitting the ENTER key.
3401
3402 We simulate clicking on the "OK" button.
3403 """
3404 if self.Validate(): self.Show(False)
3405
3406 #----------------------------------------------------------------------------
3407
3408 class TextObjectValidator(wx.PyValidator):
3409 """ This validator is used to ensure that the user has entered something
3410 into the text object editor dialog's text field.
3411 """
3412 def __init__(self):
3413 """ Standard constructor.
3414 """
3415 wx.PyValidator.__init__(self)
3416
3417
3418 def Clone(self):
3419 """ Standard cloner.
3420
3421 Note that every validator must implement the Clone() method.
3422 """
3423 return TextObjectValidator()
3424
3425
3426 def Validate(self, win):
3427 """ Validate the contents of the given text control.
3428 """
3429 textCtrl = self.GetWindow()
3430 text = textCtrl.GetValue()
3431
3432 if len(text) == 0:
3433 wx.MessageBox("A text object must contain some text!", "Error")
3434 return False
3435 else:
3436 return True
3437
3438
3439 def TransferToWindow(self):
3440 """ Transfer data from validator to window.
3441
3442 The default implementation returns False, indicating that an error
3443 occurred. We simply return True, as we don't do any data transfer.
3444 """
3445 return True # Prevent wx.Dialog from complaining.
3446
3447
3448 def TransferFromWindow(self):
3449 """ Transfer data from window to validator.
3450
3451 The default implementation returns False, indicating that an error
3452 occurred. We simply return True, as we don't do any data transfer.
3453 """
3454 return True # Prevent wx.Dialog from complaining.
3455
3456 #----------------------------------------------------------------------------
3457
3458 class ExceptionHandler:
3459 """ A simple error-handling class to write exceptions to a text file.
3460
3461 Under MS Windows, the standard DOS console window doesn't scroll and
3462 closes as soon as the application exits, making it hard to find and
3463 view Python exceptions. This utility class allows you to handle Python
3464 exceptions in a more friendly manner.
3465 """
3466
3467 def __init__(self):
3468 """ Standard constructor.
3469 """
3470 self._buff = ""
3471 if os.path.exists("errors.txt"):
3472 os.remove("errors.txt") # Delete previous error log, if any.
3473
3474
3475 def write(self, s):
3476 """ Write the given error message to a text file.
3477
3478 Note that if the error message doesn't end in a carriage return, we
3479 have to buffer up the inputs until a carriage return is received.
3480 """
3481 if (s[-1] != "\n") and (s[-1] != "\r"):
3482 self._buff = self._buff + s
3483 return
3484
3485 try:
3486 s = self._buff + s
3487 self._buff = ""
3488
3489 f = open("errors.txt", "a")
3490 f.write(s)
3491 f.close()
3492
3493 if s[:9] == "Traceback":
3494 # Tell the user than an exception occurred.
3495 wx.MessageBox("An internal error has occurred.\nPlease " + \
3496 "refer to the 'errors.txt' file for details.",
3497 "Error", wx.OK | wx.CENTRE | wx.ICON_EXCLAMATION)
3498
3499
3500 except:
3501 pass # Don't recursively crash on errors.
3502
3503 #----------------------------------------------------------------------------
3504
3505 class SketchApp(wx.App):
3506 """ The main pySketch application object.
3507 """
3508 def OnInit(self):
3509 """ Initialise the application.
3510 """
3511 global _docList
3512 _docList = []
3513
3514 if len(sys.argv) == 1:
3515 # No file name was specified on the command line -> start with a
3516 # blank document.
3517 frame = DrawingFrame(None, -1, "Untitled")
3518 frame.Centre()
3519 frame.Show(True)
3520 _docList.append(frame)
3521 else:
3522 # Load the file(s) specified on the command line.
3523 for arg in sys.argv[1:]:
3524 fileName = os.path.join(os.getcwd(), arg)
3525 if os.path.isfile(fileName):
3526 frame = DrawingFrame(None, -1,
3527 os.path.basename(fileName),
3528 fileName=fileName)
3529 frame.Show(True)
3530 _docList.append(frame)
3531
3532 return True
3533
3534 #----------------------------------------------------------------------------
3535
3536 def main():
3537 """ Start up the pySketch application.
3538 """
3539 global _app
3540
3541 # Redirect python exceptions to a log file.
3542
3543 sys.stderr = ExceptionHandler()
3544
3545 # Create and start the pySketch application.
3546
3547 _app = SketchApp(0)
3548 _app.MainLoop()
3549
3550
3551 if __name__ == "__main__":
3552 main()
3553
3554