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