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