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