* Tool menu items use wx.ITEM_RADIO * New polygon and scribble tools * Text tool uses wx.FontDialog rather than home grown dialog * Switched to double-buffering (code borrowed from doodle sample.) * Spent a lot of time on improving handling of canvas scrolling redraws which look bad under __WXMSW__, IMHO. * All tools are subclasses of DrawingTool now. * All objects are subclasses of the base DrawingObject now (rather than typecode switch) * Majority of event handling delegated to tools * Manipulation of handles refactored to move most of the logic into DrawingObject subclasses * Use GenBitmapToggleButton for the Tool buttons instead of reimplementing same functionality. * Nicer looking fill/stroke preview (I think anyway) * Toggle for choosing between wx.DC and wx.GCDC git-svn-id: https://svn.wxwidgets.org/svn/wx/wxWidgets/branches/WX_2_8_BRANCH@46370 c3d73ce0-8a6f-49c7-b76d-6d57e0e08775
3555 lines
116 KiB
Python
3555 lines
116 KiB
Python
""" pySketch
|
|
|
|
A simple object-oriented drawing program.
|
|
|
|
This is completely free software; please feel free to adapt or use this in
|
|
any way you like.
|
|
|
|
Original Author: Erik Westra (ewestra@wave.co.nz)
|
|
|
|
Other contributors: Bill Baxter (wbaxter@gmail.com)
|
|
|
|
#########################################################################
|
|
|
|
NOTE
|
|
|
|
pySketch requires wxPython version 2.3. If you are running an earlier
|
|
version, you need to patch your copy of wxPython to fix a bug which will
|
|
cause the "Edit Text Object" dialog box to crash.
|
|
|
|
To patch an earlier version of wxPython, edit the wxPython/windows.py file,
|
|
find the wxPyValidator.__init__ method and change the line which reads:
|
|
|
|
self._setSelf(self, wxPyValidator, 0)
|
|
|
|
to:
|
|
|
|
self._setSelf(self, wxPyValidator, 1)
|
|
|
|
This fixes a known bug in wxPython 2.2.5 (and possibly earlier) which has
|
|
now been fixed in wxPython 2.3.
|
|
|
|
#########################################################################
|
|
|
|
TODO:
|
|
|
|
* Add ARGV checking to see if a document was double-clicked on.
|
|
|
|
Known Bugs:
|
|
|
|
* Scrolling the window causes the drawing panel to be mucked up until you
|
|
refresh it. I've got no idea why.
|
|
|
|
* I suspect that the reference counting for some wxPoint objects is
|
|
getting mucked up; when the user quits, we get errors about being
|
|
unable to call del on a 'None' object.
|
|
|
|
* Saving files via pickling is not a robust cross-platform solution.
|
|
"""
|
|
import sys
|
|
import cPickle, os.path
|
|
import copy
|
|
import wx
|
|
from wx.lib.buttons import GenBitmapButton,GenBitmapToggleButton
|
|
|
|
|
|
import traceback, types
|
|
|
|
#----------------------------------------------------------------------------
|
|
# System Constants
|
|
#----------------------------------------------------------------------------
|
|
|
|
# Our menu item IDs:
|
|
|
|
menu_DUPLICATE = wx.NewId() # Edit menu items.
|
|
menu_EDIT_PROPS = wx.NewId()
|
|
|
|
menu_SELECT = wx.NewId() # Tools menu items.
|
|
menu_LINE = wx.NewId()
|
|
menu_POLYGON = wx.NewId()
|
|
menu_RECT = wx.NewId()
|
|
menu_ELLIPSE = wx.NewId()
|
|
menu_TEXT = wx.NewId()
|
|
|
|
menu_DC = wx.NewId() # View menu items.
|
|
menu_GCDC = wx.NewId()
|
|
|
|
menu_MOVE_FORWARD = wx.NewId() # Object menu items.
|
|
menu_MOVE_TO_FRONT = wx.NewId()
|
|
menu_MOVE_BACKWARD = wx.NewId()
|
|
menu_MOVE_TO_BACK = wx.NewId()
|
|
|
|
menu_ABOUT = wx.NewId() # Help menu items.
|
|
|
|
# Our tool IDs:
|
|
|
|
id_SELECT = wx.NewId()
|
|
id_LINE = wx.NewId()
|
|
id_POLYGON = wx.NewId()
|
|
id_SCRIBBLE = wx.NewId()
|
|
id_RECT = wx.NewId()
|
|
id_ELLIPSE = wx.NewId()
|
|
id_TEXT = wx.NewId()
|
|
|
|
# Our tool option IDs:
|
|
|
|
id_FILL_OPT = wx.NewId()
|
|
id_PEN_OPT = wx.NewId()
|
|
id_LINE_OPT = wx.NewId()
|
|
|
|
id_LINESIZE_0 = wx.NewId()
|
|
id_LINESIZE_1 = wx.NewId()
|
|
id_LINESIZE_2 = wx.NewId()
|
|
id_LINESIZE_3 = wx.NewId()
|
|
id_LINESIZE_4 = wx.NewId()
|
|
id_LINESIZE_5 = wx.NewId()
|
|
|
|
# Size of the drawing page, in pixels.
|
|
|
|
PAGE_WIDTH = 1000
|
|
PAGE_HEIGHT = 1000
|
|
|
|
#----------------------------------------------------------------------------
|
|
|
|
class DrawingFrame(wx.Frame):
|
|
""" A frame showing the contents of a single document. """
|
|
|
|
# ==========================================
|
|
# == Initialisation and Window Management ==
|
|
# ==========================================
|
|
|
|
def __init__(self, parent, id, title, fileName=None):
|
|
""" Standard constructor.
|
|
|
|
'parent', 'id' and 'title' are all passed to the standard wx.Frame
|
|
constructor. 'fileName' is the name and path of a saved file to
|
|
load into this frame, if any.
|
|
"""
|
|
wx.Frame.__init__(self, parent, id, title,
|
|
style = wx.DEFAULT_FRAME_STYLE | wx.WANTS_CHARS |
|
|
wx.NO_FULL_REPAINT_ON_RESIZE)
|
|
|
|
# Setup our menu bar.
|
|
menuBar = wx.MenuBar()
|
|
|
|
self.fileMenu = wx.Menu()
|
|
self.fileMenu.Append(wx.ID_NEW, "New\tCtrl-N", "Create a new document")
|
|
self.fileMenu.Append(wx.ID_OPEN, "Open...\tCtrl-O", "Open an existing document")
|
|
self.fileMenu.Append(wx.ID_CLOSE, "Close\tCtrl-W")
|
|
self.fileMenu.AppendSeparator()
|
|
self.fileMenu.Append(wx.ID_SAVE, "Save\tCtrl-S")
|
|
self.fileMenu.Append(wx.ID_SAVEAS, "Save As...")
|
|
self.fileMenu.Append(wx.ID_REVERT, "Revert...")
|
|
self.fileMenu.AppendSeparator()
|
|
self.fileMenu.Append(wx.ID_EXIT, "Quit\tCtrl-Q")
|
|
|
|
menuBar.Append(self.fileMenu, "File")
|
|
|
|
self.editMenu = wx.Menu()
|
|
self.editMenu.Append(wx.ID_UNDO, "Undo\tCtrl-Z")
|
|
self.editMenu.Append(wx.ID_REDO, "Redo\tCtrl-Y")
|
|
self.editMenu.AppendSeparator()
|
|
self.editMenu.Append(wx.ID_SELECTALL, "Select All\tCtrl-A")
|
|
self.editMenu.AppendSeparator()
|
|
self.editMenu.Append(menu_DUPLICATE, "Duplicate\tCtrl-D")
|
|
self.editMenu.Append(menu_EDIT_PROPS,"Edit...\tCtrl-E", "Edit object properties")
|
|
self.editMenu.Append(wx.ID_CLEAR, "Delete\tDel")
|
|
|
|
menuBar.Append(self.editMenu, "Edit")
|
|
|
|
self.viewMenu = wx.Menu()
|
|
self.viewMenu.Append(menu_DC, "Normal quality",
|
|
"Normal rendering using wx.DC",
|
|
kind=wx.ITEM_RADIO)
|
|
self.viewMenu.Append(menu_GCDC,"High quality",
|
|
"Anti-aliased rendering using wx.GCDC",
|
|
kind=wx.ITEM_RADIO)
|
|
|
|
menuBar.Append(self.viewMenu, "View")
|
|
|
|
self.toolsMenu = wx.Menu()
|
|
self.toolsMenu.Append(id_SELECT, "Selection", kind=wx.ITEM_RADIO)
|
|
self.toolsMenu.Append(id_LINE, "Line", kind=wx.ITEM_RADIO)
|
|
self.toolsMenu.Append(id_POLYGON, "Polygon", kind=wx.ITEM_RADIO)
|
|
self.toolsMenu.Append(id_SCRIBBLE,"Scribble", kind=wx.ITEM_RADIO)
|
|
self.toolsMenu.Append(id_RECT, "Rectangle", kind=wx.ITEM_RADIO)
|
|
self.toolsMenu.Append(id_ELLIPSE, "Ellipse", kind=wx.ITEM_RADIO)
|
|
self.toolsMenu.Append(id_TEXT, "Text", kind=wx.ITEM_RADIO)
|
|
|
|
menuBar.Append(self.toolsMenu, "Tools")
|
|
|
|
self.objectMenu = wx.Menu()
|
|
self.objectMenu.Append(menu_MOVE_FORWARD, "Move Forward")
|
|
self.objectMenu.Append(menu_MOVE_TO_FRONT, "Move to Front\tCtrl-F")
|
|
self.objectMenu.Append(menu_MOVE_BACKWARD, "Move Backward")
|
|
self.objectMenu.Append(menu_MOVE_TO_BACK, "Move to Back\tCtrl-B")
|
|
|
|
menuBar.Append(self.objectMenu, "Object")
|
|
|
|
self.helpMenu = wx.Menu()
|
|
self.helpMenu.Append(menu_ABOUT, "About pySketch...")
|
|
|
|
menuBar.Append(self.helpMenu, "Help")
|
|
|
|
self.SetMenuBar(menuBar)
|
|
|
|
# Create our statusbar
|
|
|
|
self.CreateStatusBar()
|
|
|
|
# Create our toolbar.
|
|
|
|
tsize = (15,15)
|
|
self.toolbar = self.CreateToolBar(wx.TB_HORIZONTAL | wx.NO_BORDER | wx.TB_FLAT)
|
|
|
|
artBmp = wx.ArtProvider.GetBitmap
|
|
self.toolbar.AddSimpleTool(
|
|
wx.ID_NEW, artBmp(wx.ART_NEW, wx.ART_TOOLBAR, tsize), "New")
|
|
self.toolbar.AddSimpleTool(
|
|
wx.ID_OPEN, artBmp(wx.ART_FILE_OPEN, wx.ART_TOOLBAR, tsize), "Open")
|
|
self.toolbar.AddSimpleTool(
|
|
wx.ID_SAVE, artBmp(wx.ART_FILE_SAVE, wx.ART_TOOLBAR, tsize), "Save")
|
|
self.toolbar.AddSimpleTool(
|
|
wx.ID_SAVEAS, artBmp(wx.ART_FILE_SAVE_AS, wx.ART_TOOLBAR, tsize),
|
|
"Save As...")
|
|
#-------
|
|
self.toolbar.AddSeparator()
|
|
self.toolbar.AddSimpleTool(
|
|
wx.ID_UNDO, artBmp(wx.ART_UNDO, wx.ART_TOOLBAR, tsize), "Undo")
|
|
self.toolbar.AddSimpleTool(
|
|
wx.ID_REDO, artBmp(wx.ART_REDO, wx.ART_TOOLBAR, tsize), "Redo")
|
|
self.toolbar.AddSeparator()
|
|
self.toolbar.AddSimpleTool(
|
|
menu_DUPLICATE, wx.Bitmap("images/duplicate.bmp", wx.BITMAP_TYPE_BMP),
|
|
"Duplicate")
|
|
#-------
|
|
self.toolbar.AddSeparator()
|
|
self.toolbar.AddSimpleTool(
|
|
menu_MOVE_FORWARD, wx.Bitmap("images/moveForward.bmp", wx.BITMAP_TYPE_BMP),
|
|
"Move Forward")
|
|
self.toolbar.AddSimpleTool(
|
|
menu_MOVE_BACKWARD, wx.Bitmap("images/moveBack.bmp", wx.BITMAP_TYPE_BMP),
|
|
"Move Backward")
|
|
|
|
self.toolbar.Realize()
|
|
|
|
# Associate menu/toolbar items with their handlers.
|
|
menuHandlers = [
|
|
(wx.ID_NEW, self.doNew),
|
|
(wx.ID_OPEN, self.doOpen),
|
|
(wx.ID_CLOSE, self.doClose),
|
|
(wx.ID_SAVE, self.doSave),
|
|
(wx.ID_SAVEAS, self.doSaveAs),
|
|
(wx.ID_REVERT, self.doRevert),
|
|
(wx.ID_EXIT, self.doExit),
|
|
|
|
(wx.ID_UNDO, self.doUndo),
|
|
(wx.ID_REDO, self.doRedo),
|
|
(wx.ID_SELECTALL, self.doSelectAll),
|
|
(menu_DUPLICATE, self.doDuplicate),
|
|
(menu_EDIT_PROPS, self.doEditObject),
|
|
(wx.ID_CLEAR, self.doDelete),
|
|
|
|
(id_SELECT, self.onChooseTool, self.updChooseTool),
|
|
(id_LINE, self.onChooseTool, self.updChooseTool),
|
|
(id_POLYGON, self.onChooseTool, self.updChooseTool),
|
|
(id_SCRIBBLE,self.onChooseTool, self.updChooseTool),
|
|
(id_RECT, self.onChooseTool, self.updChooseTool),
|
|
(id_ELLIPSE, self.onChooseTool, self.updChooseTool),
|
|
(id_TEXT, self.onChooseTool, self.updChooseTool),
|
|
|
|
(menu_DC, self.doChooseQuality),
|
|
(menu_GCDC, self.doChooseQuality),
|
|
|
|
(menu_MOVE_FORWARD, self.doMoveForward),
|
|
(menu_MOVE_TO_FRONT, self.doMoveToFront),
|
|
(menu_MOVE_BACKWARD, self.doMoveBackward),
|
|
(menu_MOVE_TO_BACK, self.doMoveToBack),
|
|
|
|
(menu_ABOUT, self.doShowAbout)]
|
|
for combo in menuHandlers:
|
|
id, handler = combo[:2]
|
|
self.Bind(wx.EVT_MENU, handler, id = id)
|
|
if len(combo)>2:
|
|
self.Bind(wx.EVT_UPDATE_UI, combo[2], id = id)
|
|
|
|
# Install our own method to handle closing the window. This allows us
|
|
# to ask the user if he/she wants to save before closing the window, as
|
|
# well as keeping track of which windows are currently open.
|
|
|
|
self.Bind(wx.EVT_CLOSE, self.doClose)
|
|
|
|
# Install our own method for handling keystrokes. We use this to let
|
|
# the user move the selected object(s) around using the arrow keys.
|
|
|
|
self.Bind(wx.EVT_CHAR_HOOK, self.onKeyEvent)
|
|
|
|
# Setup our top-most panel. This holds the entire contents of the
|
|
# window, excluding the menu bar.
|
|
|
|
self.topPanel = wx.Panel(self, -1, style=wx.SIMPLE_BORDER)
|
|
|
|
# Setup our tool palette, with all our drawing tools and option icons.
|
|
|
|
self.toolPalette = wx.BoxSizer(wx.VERTICAL)
|
|
|
|
self.selectIcon = ToolPaletteToggle(self.topPanel, id_SELECT,
|
|
"select", "Selection Tool", mode=wx.ITEM_RADIO)
|
|
self.lineIcon = ToolPaletteToggle(self.topPanel, id_LINE,
|
|
"line", "Line Tool", mode=wx.ITEM_RADIO)
|
|
self.polygonIcon = ToolPaletteToggle(self.topPanel, id_POLYGON,
|
|
"polygon", "Polygon Tool", mode=wx.ITEM_RADIO)
|
|
self.scribbleIcon = ToolPaletteToggle(self.topPanel, id_SCRIBBLE,
|
|
"scribble", "Scribble Tool", mode=wx.ITEM_RADIO)
|
|
self.rectIcon = ToolPaletteToggle(self.topPanel, id_RECT,
|
|
"rect", "Rectangle Tool", mode=wx.ITEM_RADIO)
|
|
self.ellipseIcon = ToolPaletteToggle(self.topPanel, id_ELLIPSE,
|
|
"ellipse", "Ellipse Tool", mode=wx.ITEM_RADIO)
|
|
self.textIcon = ToolPaletteToggle(self.topPanel, id_TEXT,
|
|
"text", "Text Tool", mode=wx.ITEM_RADIO)
|
|
|
|
# Create the tools
|
|
self.tools = {
|
|
'select' : (self.selectIcon, SelectDrawingTool()),
|
|
'line' : (self.lineIcon, LineDrawingTool()),
|
|
'polygon' : (self.polygonIcon, PolygonDrawingTool()),
|
|
'scribble': (self.scribbleIcon, ScribbleDrawingTool()),
|
|
'rect' : (self.rectIcon, RectDrawingTool()),
|
|
'ellipse' : (self.ellipseIcon, EllipseDrawingTool()),
|
|
'text' : (self.textIcon, TextDrawingTool())
|
|
}
|
|
|
|
|
|
toolSizer = wx.GridSizer(0, 2, 5, 5)
|
|
toolSizer.Add(self.selectIcon)
|
|
toolSizer.Add(self.lineIcon)
|
|
toolSizer.Add(self.rectIcon)
|
|
toolSizer.Add(self.ellipseIcon)
|
|
toolSizer.Add(self.polygonIcon)
|
|
toolSizer.Add(self.scribbleIcon)
|
|
toolSizer.Add(self.textIcon)
|
|
|
|
self.optionIndicator = ToolOptionIndicator(self.topPanel)
|
|
self.optionIndicator.SetToolTip(
|
|
wx.ToolTip("Shows Current Pen/Fill/Line Size Settings"))
|
|
|
|
optionSizer = wx.BoxSizer(wx.HORIZONTAL)
|
|
|
|
self.penOptIcon = ToolPaletteButton(self.topPanel, id_PEN_OPT,
|
|
"penOpt", "Set Pen Colour",)
|
|
self.fillOptIcon = ToolPaletteButton(self.topPanel, id_FILL_OPT,
|
|
"fillOpt", "Set Fill Colour")
|
|
self.lineOptIcon = ToolPaletteButton(self.topPanel, id_LINE_OPT,
|
|
"lineOpt", "Set Line Size")
|
|
|
|
margin = wx.LEFT | wx.RIGHT
|
|
optionSizer.Add(self.penOptIcon, 0, margin, 1)
|
|
optionSizer.Add(self.fillOptIcon, 0, margin, 1)
|
|
optionSizer.Add(self.lineOptIcon, 0, margin, 1)
|
|
|
|
margin = wx.TOP | wx.LEFT | wx.RIGHT | wx.ALIGN_CENTRE
|
|
self.toolPalette.Add(toolSizer, 0, margin, 5)
|
|
self.toolPalette.Add((0, 0), 0, margin, 5) # Spacer.
|
|
self.toolPalette.Add(self.optionIndicator, 0, margin, 5)
|
|
self.toolPalette.Add(optionSizer, 0, margin, 5)
|
|
|
|
# Make the tool palette icons respond when the user clicks on them.
|
|
|
|
for tool in self.tools.itervalues():
|
|
tool[0].Bind(wx.EVT_BUTTON, self.onChooseTool)
|
|
|
|
self.selectIcon.Bind(wx.EVT_BUTTON, self.onChooseTool)
|
|
self.lineIcon.Bind(wx.EVT_BUTTON, self.onChooseTool)
|
|
|
|
|
|
self.penOptIcon.Bind(wx.EVT_BUTTON, self.onPenOptionIconClick)
|
|
self.fillOptIcon.Bind(wx.EVT_BUTTON, self.onFillOptionIconClick)
|
|
self.lineOptIcon.Bind(wx.EVT_BUTTON, self.onLineOptionIconClick)
|
|
|
|
# Setup the main drawing area.
|
|
|
|
self.drawPanel = wx.ScrolledWindow(self.topPanel, -1,
|
|
style=wx.SUNKEN_BORDER|wx.NO_FULL_REPAINT_ON_RESIZE)
|
|
self.drawPanel.SetBackgroundColour(wx.WHITE)
|
|
|
|
self.drawPanel.EnableScrolling(True, True)
|
|
self.drawPanel.SetScrollbars(20, 20, PAGE_WIDTH / 20, PAGE_HEIGHT / 20)
|
|
|
|
self.drawPanel.Bind(wx.EVT_MOUSE_EVENTS, self.onMouseEvent)
|
|
|
|
self.drawPanel.Bind(wx.EVT_IDLE, self.onIdle)
|
|
self.drawPanel.Bind(wx.EVT_SIZE, self.onSize)
|
|
self.drawPanel.Bind(wx.EVT_PAINT, self.onPaint)
|
|
self.drawPanel.Bind(wx.EVT_ERASE_BACKGROUND, self.onEraseBackground)
|
|
self.drawPanel.Bind(wx.EVT_SCROLLWIN, self.onPanelScroll)
|
|
|
|
self.Bind(wx.EVT_TIMER, self.onIdle)
|
|
|
|
|
|
# Position everything in the window.
|
|
|
|
topSizer = wx.BoxSizer(wx.HORIZONTAL)
|
|
topSizer.Add(self.toolPalette, 0)
|
|
topSizer.Add(self.drawPanel, 1, wx.EXPAND)
|
|
|
|
self.topPanel.SetAutoLayout(True)
|
|
self.topPanel.SetSizer(topSizer)
|
|
|
|
self.SetSizeHints(250, 200)
|
|
self.SetSize(wx.Size(600, 400))
|
|
|
|
# Select an initial tool.
|
|
|
|
self.curToolName = None
|
|
self.curToolIcon = None
|
|
self.curTool = None
|
|
self.setCurrentTool("select")
|
|
|
|
# Set initial dc mode to fast
|
|
self.wrapDC = lambda dc: dc
|
|
|
|
# Setup our frame to hold the contents of a sketch document.
|
|
|
|
self.dirty = False
|
|
self.fileName = fileName
|
|
self.contents = [] # front-to-back ordered list of DrawingObjects.
|
|
self.selection = [] # List of selected DrawingObjects.
|
|
self.undoStack = [] # Stack of saved contents for undo.
|
|
self.redoStack = [] # Stack of saved contents for redo.
|
|
|
|
if self.fileName != None:
|
|
self.loadContents()
|
|
|
|
self._initBuffer()
|
|
|
|
self._adjustMenus()
|
|
|
|
# Finally, set our initial pen, fill and line options.
|
|
|
|
self._setPenColour(wx.BLACK)
|
|
self._setFillColour(wx.Colour(215,253,254))
|
|
self._setLineSize(2)
|
|
|
|
self.backgroundFillBrush = None # create on demand
|
|
|
|
# Start the background redraw timer
|
|
# This is optional, but it gives the double-buffered contents a
|
|
# chance to redraw even when idle events are disabled (like during
|
|
# resize and scrolling)
|
|
self.redrawTimer = wx.Timer(self)
|
|
self.redrawTimer.Start(700)
|
|
|
|
|
|
# ============================
|
|
# == Event Handling Methods ==
|
|
# ============================
|
|
|
|
|
|
def onPenOptionIconClick(self, event):
|
|
""" Respond to the user clicking on the "Pen Options" icon.
|
|
"""
|
|
data = wx.ColourData()
|
|
if len(self.selection) == 1:
|
|
data.SetColour(self.selection[0].getPenColour())
|
|
else:
|
|
data.SetColour(self.penColour)
|
|
|
|
dialog = wx.ColourDialog(self, data)
|
|
dialog.SetTitle('Choose line colour')
|
|
if dialog.ShowModal() == wx.ID_OK:
|
|
c = dialog.GetColourData().GetColour()
|
|
self._setPenColour(wx.Colour(c.Red(), c.Green(), c.Blue()))
|
|
dialog.Destroy()
|
|
|
|
|
|
def onFillOptionIconClick(self, event):
|
|
""" Respond to the user clicking on the "Fill Options" icon.
|
|
"""
|
|
data = wx.ColourData()
|
|
if len(self.selection) == 1:
|
|
data.SetColour(self.selection[0].getFillColour())
|
|
else:
|
|
data.SetColour(self.fillColour)
|
|
|
|
dialog = wx.ColourDialog(self, data)
|
|
dialog.SetTitle('Choose fill colour')
|
|
if dialog.ShowModal() == wx.ID_OK:
|
|
c = dialog.GetColourData().GetColour()
|
|
self._setFillColour(wx.Colour(c.Red(), c.Green(), c.Blue()))
|
|
dialog.Destroy()
|
|
|
|
def onLineOptionIconClick(self, event):
|
|
""" Respond to the user clicking on the "Line Options" icon.
|
|
"""
|
|
if len(self.selection) == 1:
|
|
menu = self._buildLineSizePopup(self.selection[0].getLineSize())
|
|
else:
|
|
menu = self._buildLineSizePopup(self.lineSize)
|
|
|
|
pos = self.lineOptIcon.GetPosition()
|
|
pos.y = pos.y + self.lineOptIcon.GetSize().height
|
|
self.PopupMenu(menu, pos)
|
|
menu.Destroy()
|
|
|
|
|
|
def onKeyEvent(self, event):
|
|
""" Respond to a keypress event.
|
|
|
|
We make the arrow keys move the selected object(s) by one pixel in
|
|
the given direction.
|
|
"""
|
|
step = 1
|
|
if event.ShiftDown():
|
|
step = 20
|
|
|
|
if event.GetKeyCode() == wx.WXK_UP:
|
|
self._moveObject(0, -step)
|
|
elif event.GetKeyCode() == wx.WXK_DOWN:
|
|
self._moveObject(0, step)
|
|
elif event.GetKeyCode() == wx.WXK_LEFT:
|
|
self._moveObject(-step, 0)
|
|
elif event.GetKeyCode() == wx.WXK_RIGHT:
|
|
self._moveObject(step, 0)
|
|
else:
|
|
event.Skip()
|
|
|
|
|
|
def onMouseEvent(self, event):
|
|
""" Respond to mouse events in the main drawing panel
|
|
|
|
How we respond depends on the currently selected tool.
|
|
"""
|
|
if self.curTool is None: return
|
|
|
|
# Translate event into canvas coordinates and pass to current tool
|
|
origx,origy = event.X, event.Y
|
|
pt = self._getEventCoordinates(event)
|
|
event.m_x = pt.x
|
|
event.m_y = pt.y
|
|
handled = self.curTool.onMouseEvent(self,event)
|
|
event.m_x = origx
|
|
event.m_y = origy
|
|
|
|
if handled: return
|
|
|
|
# otherwise handle it ourselves
|
|
if event.RightDown():
|
|
self.doPopupContextMenu(event)
|
|
|
|
|
|
def doPopupContextMenu(self, event):
|
|
""" Respond to the user right-clicking within our drawing panel.
|
|
|
|
We select the clicked-on item, if necessary, and display a pop-up
|
|
menu of available options which can be applied to the selected
|
|
item(s).
|
|
"""
|
|
mousePt = self._getEventCoordinates(event)
|
|
obj = self.getObjectAt(mousePt)
|
|
|
|
if obj == None: return # Nothing selected.
|
|
|
|
# Select the clicked-on object.
|
|
|
|
self.select(obj)
|
|
|
|
# Build our pop-up menu.
|
|
|
|
menu = wx.Menu()
|
|
menu.Append(menu_DUPLICATE, "Duplicate")
|
|
menu.Append(menu_EDIT_PROPS,"Edit...")
|
|
menu.Append(wx.ID_CLEAR, "Delete")
|
|
menu.AppendSeparator()
|
|
menu.Append(menu_MOVE_FORWARD, "Move Forward")
|
|
menu.Append(menu_MOVE_TO_FRONT, "Move to Front")
|
|
menu.Append(menu_MOVE_BACKWARD, "Move Backward")
|
|
menu.Append(menu_MOVE_TO_BACK, "Move to Back")
|
|
|
|
menu.Enable(menu_EDIT_PROPS, obj.hasPropertyEditor())
|
|
menu.Enable(menu_MOVE_FORWARD, obj != self.contents[0])
|
|
menu.Enable(menu_MOVE_TO_FRONT, obj != self.contents[0])
|
|
menu.Enable(menu_MOVE_BACKWARD, obj != self.contents[-1])
|
|
menu.Enable(menu_MOVE_TO_BACK, obj != self.contents[-1])
|
|
|
|
self.Bind(wx.EVT_MENU, self.doDuplicate, id=menu_DUPLICATE)
|
|
self.Bind(wx.EVT_MENU, self.doEditObject, id=menu_EDIT_PROPS)
|
|
self.Bind(wx.EVT_MENU, self.doDelete, id=wx.ID_CLEAR)
|
|
self.Bind(wx.EVT_MENU, self.doMoveForward, id=menu_MOVE_FORWARD)
|
|
self.Bind(wx.EVT_MENU, self.doMoveToFront, id=menu_MOVE_TO_FRONT)
|
|
self.Bind(wx.EVT_MENU, self.doMoveBackward,id=menu_MOVE_BACKWARD)
|
|
self.Bind(wx.EVT_MENU, self.doMoveToBack, id=menu_MOVE_TO_BACK)
|
|
|
|
# Show the pop-up menu.
|
|
|
|
clickPt = wx.Point(mousePt.x + self.drawPanel.GetPosition().x,
|
|
mousePt.y + self.drawPanel.GetPosition().y)
|
|
self.drawPanel.PopupMenu(menu, mousePt)
|
|
menu.Destroy()
|
|
|
|
|
|
def onSize(self, event):
|
|
"""
|
|
Called when the window is resized. We set a flag so the idle
|
|
handler will resize the buffer.
|
|
"""
|
|
self.requestRedraw()
|
|
|
|
|
|
def onIdle(self, event):
|
|
"""
|
|
If the size was changed then resize the bitmap used for double
|
|
buffering to match the window size. We do it in Idle time so
|
|
there is only one refresh after resizing is done, not lots while
|
|
it is happening.
|
|
"""
|
|
if self._reInitBuffer and self.IsShown():
|
|
self._initBuffer()
|
|
self.drawPanel.Refresh(False)
|
|
|
|
def requestRedraw(self):
|
|
"""Requests a redraw of the drawing panel contents.
|
|
|
|
The actual redrawing doesn't happen until the next idle time.
|
|
"""
|
|
self._reInitBuffer = True
|
|
|
|
def onPaint(self, event):
|
|
"""
|
|
Called when the window is exposed.
|
|
"""
|
|
# Create a buffered paint DC. It will create the real
|
|
# wx.PaintDC and then blit the bitmap to it when dc is
|
|
# deleted.
|
|
dc = wx.BufferedPaintDC(self.drawPanel, self.buffer)
|
|
|
|
|
|
# On Windows, if that's all we do things look a little rough
|
|
# So in order to make scrolling more polished-looking
|
|
# we iterate over the exposed regions and fill in unknown
|
|
# areas with a fall-back pattern.
|
|
|
|
if wx.Platform != '__WXMSW__':
|
|
return
|
|
|
|
# First get the update rects and subtract off the part that
|
|
# self.buffer has correct already
|
|
region = self.drawPanel.GetUpdateRegion()
|
|
panelRect = self.drawPanel.GetClientRect()
|
|
offset = list(self.drawPanel.CalcUnscrolledPosition(0,0))
|
|
offset[0] -= self.saved_offset[0]
|
|
offset[1] -= self.saved_offset[1]
|
|
region.Subtract(-offset[0],- offset[1],panelRect.Width, panelRect.Height)
|
|
|
|
# Now iterate over the remaining region rects and fill in with a pattern
|
|
rgn_iter = wx.RegionIterator(region)
|
|
if rgn_iter.HaveRects():
|
|
self.setBackgroundMissingFillStyle(dc)
|
|
offset = self.drawPanel.CalcUnscrolledPosition(0,0)
|
|
while rgn_iter:
|
|
r = rgn_iter.GetRect()
|
|
if r.Size != self.drawPanel.ClientSize:
|
|
dc.DrawRectangleRect(r)
|
|
rgn_iter.Next()
|
|
|
|
|
|
def setBackgroundMissingFillStyle(self, dc):
|
|
if self.backgroundFillBrush is None:
|
|
# Win95 can only handle a 8x8 stipple bitmaps max
|
|
#stippleBitmap = wx.BitmapFromBits("\xf0"*4 + "\x0f"*4,8,8)
|
|
# ...but who uses Win95?
|
|
stippleBitmap = wx.BitmapFromBits("\x06",2,2)
|
|
stippleBitmap.SetMask(wx.Mask(stippleBitmap))
|
|
bgbrush = wx.Brush(wx.WHITE, wx.STIPPLE_MASK_OPAQUE)
|
|
bgbrush.SetStipple(stippleBitmap)
|
|
self.backgroundFillBrush = bgbrush
|
|
|
|
dc.SetPen(wx.TRANSPARENT_PEN)
|
|
dc.SetBrush(self.backgroundFillBrush)
|
|
dc.SetTextForeground(wx.LIGHT_GREY)
|
|
dc.SetTextBackground(wx.WHITE)
|
|
|
|
|
|
def onEraseBackground(self, event):
|
|
"""
|
|
Overridden to do nothing to prevent flicker
|
|
"""
|
|
pass
|
|
|
|
|
|
def onPanelScroll(self, event):
|
|
"""
|
|
Called when the user changes scrolls the drawPanel
|
|
"""
|
|
# make a note to ourselves to redraw when we get a chance
|
|
self.requestRedraw()
|
|
event.Skip()
|
|
pass
|
|
|
|
def drawContents(self, dc):
|
|
"""
|
|
Does the actual drawing of all drawing contents with the specified dc
|
|
"""
|
|
# PrepareDC sets the device origin according to current scrolling
|
|
self.drawPanel.PrepareDC(dc)
|
|
|
|
gdc = self.wrapDC(dc)
|
|
|
|
# First pass draws objects
|
|
ordered_selection = []
|
|
for obj in self.contents[::-1]:
|
|
if obj in self.selection:
|
|
obj.draw(gdc, True)
|
|
ordered_selection.append(obj)
|
|
else:
|
|
obj.draw(gdc, False)
|
|
|
|
# First pass draws objects
|
|
if self.curTool is not None:
|
|
self.curTool.draw(gdc)
|
|
|
|
# Second pass draws selection handles so they're always on top
|
|
for obj in ordered_selection:
|
|
obj.drawHandles(gdc)
|
|
|
|
|
|
|
|
# ==========================
|
|
# == Menu Command Methods ==
|
|
# ==========================
|
|
|
|
def doNew(self, event):
|
|
""" Respond to the "New" menu command.
|
|
"""
|
|
global _docList
|
|
newFrame = DrawingFrame(None, -1, "Untitled")
|
|
newFrame.Show(True)
|
|
_docList.append(newFrame)
|
|
|
|
|
|
def doOpen(self, event):
|
|
""" Respond to the "Open" menu command.
|
|
"""
|
|
global _docList
|
|
|
|
curDir = os.getcwd()
|
|
fileName = wx.FileSelector("Open File", default_extension="psk",
|
|
flags = wx.OPEN | wx.FILE_MUST_EXIST)
|
|
if fileName == "": return
|
|
fileName = os.path.join(os.getcwd(), fileName)
|
|
os.chdir(curDir)
|
|
|
|
title = os.path.basename(fileName)
|
|
|
|
if (self.fileName == None) and (len(self.contents) == 0):
|
|
# Load contents into current (empty) document.
|
|
self.fileName = fileName
|
|
self.SetTitle(os.path.basename(fileName))
|
|
self.loadContents()
|
|
else:
|
|
# Open a new frame for this document.
|
|
newFrame = DrawingFrame(None, -1, os.path.basename(fileName),
|
|
fileName=fileName)
|
|
newFrame.Show(True)
|
|
_docList.append(newFrame)
|
|
|
|
|
|
def doClose(self, event):
|
|
""" Respond to the "Close" menu command.
|
|
"""
|
|
global _docList
|
|
|
|
if self.dirty:
|
|
if not self.askIfUserWantsToSave("closing"): return
|
|
|
|
_docList.remove(self)
|
|
self.Destroy()
|
|
|
|
|
|
def doSave(self, event):
|
|
""" Respond to the "Save" menu command.
|
|
"""
|
|
if self.fileName != None:
|
|
self.saveContents()
|
|
|
|
|
|
def doSaveAs(self, event):
|
|
""" Respond to the "Save As" menu command.
|
|
"""
|
|
if self.fileName == None:
|
|
default = ""
|
|
else:
|
|
default = self.fileName
|
|
|
|
curDir = os.getcwd()
|
|
fileName = wx.FileSelector("Save File As", "Saving",
|
|
default_filename=default,
|
|
default_extension="psk",
|
|
wildcard="*.psk",
|
|
flags = wx.SAVE | wx.OVERWRITE_PROMPT)
|
|
if fileName == "": return # User cancelled.
|
|
fileName = os.path.join(os.getcwd(), fileName)
|
|
os.chdir(curDir)
|
|
|
|
title = os.path.basename(fileName)
|
|
self.SetTitle(title)
|
|
|
|
self.fileName = fileName
|
|
self.saveContents()
|
|
|
|
|
|
def doRevert(self, event):
|
|
""" Respond to the "Revert" menu command.
|
|
"""
|
|
if not self.dirty: return
|
|
|
|
if wx.MessageBox("Discard changes made to this document?", "Confirm",
|
|
style = wx.OK | wx.CANCEL | wx.ICON_QUESTION,
|
|
parent=self) == wx.CANCEL: return
|
|
self.loadContents()
|
|
|
|
|
|
def doExit(self, event):
|
|
""" Respond to the "Quit" menu command.
|
|
"""
|
|
global _docList, _app
|
|
for doc in _docList:
|
|
if not doc.dirty: continue
|
|
doc.Raise()
|
|
if not doc.askIfUserWantsToSave("quitting"): return
|
|
_docList.remove(doc)
|
|
doc.Destroy()
|
|
|
|
_app.ExitMainLoop()
|
|
|
|
|
|
def doUndo(self, event):
|
|
""" Respond to the "Undo" menu command.
|
|
"""
|
|
if not self.undoStack: return
|
|
|
|
state = self._buildStoredState()
|
|
self.redoStack.append(state)
|
|
state = self.undoStack.pop()
|
|
self._restoreStoredState(state)
|
|
|
|
def doRedo(self, event):
|
|
""" Respond to the "Redo" menu.
|
|
"""
|
|
if not self.redoStack: return
|
|
|
|
state = self._buildStoredState()
|
|
self.undoStack.append(state)
|
|
state = self.redoStack.pop()
|
|
self._restoreStoredState(state)
|
|
|
|
def doSelectAll(self, event):
|
|
""" Respond to the "Select All" menu command.
|
|
"""
|
|
self.selectAll()
|
|
|
|
|
|
def doDuplicate(self, event):
|
|
""" Respond to the "Duplicate" menu command.
|
|
"""
|
|
self.saveUndoInfo()
|
|
|
|
objs = []
|
|
for obj in self.contents:
|
|
if obj in self.selection:
|
|
newObj = copy.deepcopy(obj)
|
|
pos = obj.getPosition()
|
|
newObj.setPosition(wx.Point(pos.x + 10, pos.y + 10))
|
|
objs.append(newObj)
|
|
|
|
self.contents = objs + self.contents
|
|
|
|
self.selectMany(objs)
|
|
|
|
|
|
def doEditObject(self, event):
|
|
""" Respond to the "Edit..." menu command.
|
|
"""
|
|
if len(self.selection) != 1: return
|
|
|
|
obj = self.selection[0]
|
|
if not obj.hasPropertyEditor():
|
|
assert False, "doEditObject called on non-editable"
|
|
|
|
ret = obj.doPropertyEdit(self)
|
|
if ret:
|
|
self.dirty = True
|
|
self.requestRedraw()
|
|
self._adjustMenus()
|
|
|
|
|
|
def doDelete(self, event):
|
|
""" Respond to the "Delete" menu command.
|
|
"""
|
|
self.saveUndoInfo()
|
|
|
|
for obj in self.selection:
|
|
self.contents.remove(obj)
|
|
del obj
|
|
self.deselectAll()
|
|
|
|
|
|
def onChooseTool(self, event):
|
|
""" Respond to tool selection menu and tool palette selections
|
|
"""
|
|
obj = event.GetEventObject()
|
|
id2name = { id_SELECT: "select",
|
|
id_LINE: "line",
|
|
id_POLYGON: "polygon",
|
|
id_SCRIBBLE: "scribble",
|
|
id_RECT: "rect",
|
|
id_ELLIPSE: "ellipse",
|
|
id_TEXT: "text" }
|
|
toolID = event.GetId()
|
|
name = id2name.get( toolID )
|
|
|
|
if name:
|
|
self.setCurrentTool(name)
|
|
|
|
def updChooseTool(self, event):
|
|
"""UI update event that keeps tool menu in sync with the PaletteIcons"""
|
|
obj = event.GetEventObject()
|
|
id2name = { id_SELECT: "select",
|
|
id_LINE: "line",
|
|
id_POLYGON: "polygon",
|
|
id_SCRIBBLE: "scribble",
|
|
id_RECT: "rect",
|
|
id_ELLIPSE: "ellipse",
|
|
id_TEXT: "text" }
|
|
toolID = event.GetId()
|
|
event.Check( toolID == self.curToolIcon.GetId() )
|
|
|
|
|
|
def doChooseQuality(self, event):
|
|
"""Respond to the render quality menu commands
|
|
"""
|
|
if event.GetId() == menu_DC:
|
|
self.wrapDC = lambda dc: dc
|
|
else:
|
|
self.wrapDC = lambda dc: wx.GCDC(dc)
|
|
self._adjustMenus()
|
|
self.requestRedraw()
|
|
|
|
def doMoveForward(self, event):
|
|
""" Respond to the "Move Forward" menu command.
|
|
"""
|
|
if len(self.selection) != 1: return
|
|
|
|
self.saveUndoInfo()
|
|
|
|
obj = self.selection[0]
|
|
index = self.contents.index(obj)
|
|
if index == 0: return
|
|
|
|
del self.contents[index]
|
|
self.contents.insert(index-1, obj)
|
|
|
|
self.requestRedraw()
|
|
self._adjustMenus()
|
|
|
|
|
|
def doMoveToFront(self, event):
|
|
""" Respond to the "Move to Front" menu command.
|
|
"""
|
|
if len(self.selection) != 1: return
|
|
|
|
self.saveUndoInfo()
|
|
|
|
obj = self.selection[0]
|
|
self.contents.remove(obj)
|
|
self.contents.insert(0, obj)
|
|
|
|
self.requestRedraw()
|
|
self._adjustMenus()
|
|
|
|
|
|
def doMoveBackward(self, event):
|
|
""" Respond to the "Move Backward" menu command.
|
|
"""
|
|
if len(self.selection) != 1: return
|
|
|
|
self.saveUndoInfo()
|
|
|
|
obj = self.selection[0]
|
|
index = self.contents.index(obj)
|
|
if index == len(self.contents) - 1: return
|
|
|
|
del self.contents[index]
|
|
self.contents.insert(index+1, obj)
|
|
|
|
self.requestRedraw()
|
|
self._adjustMenus()
|
|
|
|
|
|
def doMoveToBack(self, event):
|
|
""" Respond to the "Move to Back" menu command.
|
|
"""
|
|
if len(self.selection) != 1: return
|
|
|
|
self.saveUndoInfo()
|
|
|
|
obj = self.selection[0]
|
|
self.contents.remove(obj)
|
|
self.contents.append(obj)
|
|
|
|
self.requestRedraw()
|
|
self._adjustMenus()
|
|
|
|
|
|
def doShowAbout(self, event):
|
|
""" Respond to the "About pySketch" menu command.
|
|
"""
|
|
dialog = wx.Dialog(self, -1, "About pySketch") # ,
|
|
#style=wx.DIALOG_MODAL | wx.STAY_ON_TOP)
|
|
dialog.SetBackgroundColour(wx.WHITE)
|
|
|
|
panel = wx.Panel(dialog, -1)
|
|
panel.SetBackgroundColour(wx.WHITE)
|
|
|
|
panelSizer = wx.BoxSizer(wx.VERTICAL)
|
|
|
|
boldFont = wx.Font(panel.GetFont().GetPointSize(),
|
|
panel.GetFont().GetFamily(),
|
|
wx.NORMAL, wx.BOLD)
|
|
|
|
logo = wx.StaticBitmap(panel, -1, wx.Bitmap("images/logo.bmp",
|
|
wx.BITMAP_TYPE_BMP))
|
|
|
|
lab1 = wx.StaticText(panel, -1, "pySketch")
|
|
lab1.SetFont(wx.Font(36, boldFont.GetFamily(), wx.ITALIC, wx.BOLD))
|
|
lab1.SetSize(lab1.GetBestSize())
|
|
|
|
imageSizer = wx.BoxSizer(wx.HORIZONTAL)
|
|
imageSizer.Add(logo, 0, wx.ALL | wx.ALIGN_CENTRE_VERTICAL, 5)
|
|
imageSizer.Add(lab1, 0, wx.ALL | wx.ALIGN_CENTRE_VERTICAL, 5)
|
|
|
|
lab2 = wx.StaticText(panel, -1, "A simple object-oriented drawing " + \
|
|
"program.")
|
|
lab2.SetFont(boldFont)
|
|
lab2.SetSize(lab2.GetBestSize())
|
|
|
|
lab3 = wx.StaticText(panel, -1, "pySketch is completely free " + \
|
|
"software; please")
|
|
lab3.SetFont(boldFont)
|
|
lab3.SetSize(lab3.GetBestSize())
|
|
|
|
lab4 = wx.StaticText(panel, -1, "feel free to adapt or use this " + \
|
|
"in any way you like.")
|
|
lab4.SetFont(boldFont)
|
|
lab4.SetSize(lab4.GetBestSize())
|
|
|
|
lab5 = wx.StaticText(panel, -1,
|
|
"Author: Erik Westra " + \
|
|
"(ewestra@wave.co.nz)\n" + \
|
|
"Contributors: Bill Baxter " +\
|
|
"(wbaxter@gmail.com) ")
|
|
|
|
lab5.SetFont(boldFont)
|
|
lab5.SetSize(lab5.GetBestSize())
|
|
|
|
btnOK = wx.Button(panel, wx.ID_OK, "OK")
|
|
|
|
panelSizer.Add(imageSizer, 0, wx.ALIGN_CENTRE)
|
|
panelSizer.Add((10, 10)) # Spacer.
|
|
panelSizer.Add(lab2, 0, wx.ALIGN_CENTRE)
|
|
panelSizer.Add((10, 10)) # Spacer.
|
|
panelSizer.Add(lab3, 0, wx.ALIGN_CENTRE)
|
|
panelSizer.Add(lab4, 0, wx.ALIGN_CENTRE)
|
|
panelSizer.Add((10, 10)) # Spacer.
|
|
panelSizer.Add(lab5, 0, wx.ALIGN_CENTRE)
|
|
panelSizer.Add((10, 10)) # Spacer.
|
|
panelSizer.Add(btnOK, 0, wx.ALL | wx.ALIGN_CENTRE, 5)
|
|
|
|
panel.SetAutoLayout(True)
|
|
panel.SetSizer(panelSizer)
|
|
panelSizer.Fit(panel)
|
|
|
|
topSizer = wx.BoxSizer(wx.HORIZONTAL)
|
|
topSizer.Add(panel, 0, wx.ALL, 10)
|
|
|
|
dialog.SetAutoLayout(True)
|
|
dialog.SetSizer(topSizer)
|
|
topSizer.Fit(dialog)
|
|
|
|
dialog.Centre()
|
|
|
|
btn = dialog.ShowModal()
|
|
dialog.Destroy()
|
|
|
|
def getTextEditor(self):
|
|
if not hasattr(self,'textEditor') or not self.textEditor:
|
|
self.textEditor = EditTextObjectDialog(self, "Edit Text Object")
|
|
return self.textEditor
|
|
|
|
# =============================
|
|
# == Object Creation Methods ==
|
|
# =============================
|
|
|
|
def addObject(self, obj, select=True):
|
|
"""Add a new drawing object to the canvas.
|
|
|
|
If select is True then also select the object
|
|
"""
|
|
self.saveUndoInfo()
|
|
self.contents.insert(0, obj)
|
|
self.dirty = True
|
|
if select:
|
|
self.select(obj)
|
|
#self.setCurrentTool('select')
|
|
|
|
def saveUndoInfo(self):
|
|
""" Remember the current state of the document, to allow for undo.
|
|
|
|
We make a copy of the document's contents, so that we can return to
|
|
the previous contents if the user does something and then wants to
|
|
undo the operation.
|
|
|
|
This should be called only for a new modification to the document
|
|
since it erases the redo history.
|
|
"""
|
|
state = self._buildStoredState()
|
|
|
|
self.undoStack.append(state)
|
|
self.redoStack = []
|
|
self.dirty = True
|
|
self._adjustMenus()
|
|
|
|
# =======================
|
|
# == Selection Methods ==
|
|
# =======================
|
|
|
|
def setCurrentTool(self, toolName):
|
|
""" Set the currently selected tool.
|
|
"""
|
|
|
|
toolIcon, tool = self.tools[toolName]
|
|
if self.curToolIcon is not None:
|
|
self.curToolIcon.SetValue(False)
|
|
|
|
toolIcon.SetValue(True)
|
|
self.curToolName = toolName
|
|
self.curToolIcon = toolIcon
|
|
self.curTool = tool
|
|
self.drawPanel.SetCursor(tool.getDefaultCursor())
|
|
|
|
|
|
def selectAll(self):
|
|
""" Select every DrawingObject in our document.
|
|
"""
|
|
self.selection = []
|
|
for obj in self.contents:
|
|
self.selection.append(obj)
|
|
self.requestRedraw()
|
|
self._adjustMenus()
|
|
|
|
|
|
def deselectAll(self):
|
|
""" Deselect every DrawingObject in our document.
|
|
"""
|
|
self.selection = []
|
|
self.requestRedraw()
|
|
self._adjustMenus()
|
|
|
|
|
|
def select(self, obj, add=False):
|
|
""" Select the given DrawingObject within our document.
|
|
|
|
If 'add' is True obj is added onto the current selection
|
|
"""
|
|
if not add:
|
|
self.selection = []
|
|
if obj not in self.selection:
|
|
self.selection += [obj]
|
|
self.requestRedraw()
|
|
self._adjustMenus()
|
|
|
|
def selectMany(self, objs):
|
|
""" Select the given list of DrawingObjects.
|
|
"""
|
|
self.selection = objs
|
|
self.requestRedraw()
|
|
self._adjustMenus()
|
|
|
|
|
|
def selectByRectangle(self, x, y, width, height):
|
|
""" Select every DrawingObject in the given rectangular region.
|
|
"""
|
|
self.selection = []
|
|
for obj in self.contents:
|
|
if obj.objectWithinRect(x, y, width, height):
|
|
self.selection.append(obj)
|
|
self.requestRedraw()
|
|
self._adjustMenus()
|
|
|
|
def getObjectAndSelectionHandleAt(self, pt):
|
|
""" Return the object and selection handle at the given point.
|
|
|
|
We draw selection handles (small rectangles) around the currently
|
|
selected object(s). If the given point is within one of the
|
|
selection handle rectangles, we return the associated object and a
|
|
code indicating which selection handle the point is in. If the
|
|
point isn't within any selection handle at all, we return the tuple
|
|
(None, None).
|
|
"""
|
|
for obj in self.selection:
|
|
handle = obj.getSelectionHandleContainingPoint(pt.x, pt.y)
|
|
if handle is not None:
|
|
return obj, handle
|
|
|
|
return None, None
|
|
|
|
|
|
def getObjectAt(self, pt):
|
|
""" Return the first object found which is at the given point.
|
|
"""
|
|
for obj in self.contents:
|
|
if obj.objectContainsPoint(pt.x, pt.y):
|
|
return obj
|
|
return None
|
|
|
|
|
|
# ======================
|
|
# == File I/O Methods ==
|
|
# ======================
|
|
|
|
def loadContents(self):
|
|
""" Load the contents of our document into memory.
|
|
"""
|
|
|
|
try:
|
|
f = open(self.fileName, "rb")
|
|
objData = cPickle.load(f)
|
|
f.close()
|
|
|
|
for klass, data in objData:
|
|
obj = klass()
|
|
obj.setData(data)
|
|
self.contents.append(obj)
|
|
|
|
self.dirty = False
|
|
self.selection = []
|
|
self.undoStack = []
|
|
self.redoStack = []
|
|
|
|
self.requestRedraw()
|
|
self._adjustMenus()
|
|
except:
|
|
response = wx.MessageBox("Unable to load " + self.fileName + ".",
|
|
"Error", wx.OK|wx.ICON_ERROR, self)
|
|
|
|
|
|
|
|
def saveContents(self):
|
|
""" Save the contents of our document to disk.
|
|
"""
|
|
# SWIG-wrapped native wx contents cannot be pickled, so
|
|
# we have to convert our data to something pickle-friendly.
|
|
|
|
try:
|
|
objData = []
|
|
for obj in self.contents:
|
|
objData.append([obj.__class__, obj.getData()])
|
|
|
|
f = open(self.fileName, "wb")
|
|
cPickle.dump(objData, f)
|
|
f.close()
|
|
|
|
self.dirty = False
|
|
self._adjustMenus()
|
|
except:
|
|
response = wx.MessageBox("Unable to load " + self.fileName + ".",
|
|
"Error", wx.OK|wx.ICON_ERROR, self)
|
|
|
|
|
|
def askIfUserWantsToSave(self, action):
|
|
""" Give the user the opportunity to save the current document.
|
|
|
|
'action' is a string describing the action about to be taken. If
|
|
the user wants to save the document, it is saved immediately. If
|
|
the user cancels, we return False.
|
|
"""
|
|
if not self.dirty: return True # Nothing to do.
|
|
|
|
response = wx.MessageBox("Save changes before " + action + "?",
|
|
"Confirm", wx.YES_NO | wx.CANCEL, self)
|
|
|
|
if response == wx.YES:
|
|
if self.fileName == None:
|
|
fileName = wx.FileSelector("Save File As", "Saving",
|
|
default_extension="psk",
|
|
wildcard="*.psk",
|
|
flags = wx.SAVE | wx.OVERWRITE_PROMPT)
|
|
if fileName == "": return False # User cancelled.
|
|
self.fileName = fileName
|
|
|
|
self.saveContents()
|
|
return True
|
|
elif response == wx.NO:
|
|
return True # User doesn't want changes saved.
|
|
elif response == wx.CANCEL:
|
|
return False # User cancelled.
|
|
|
|
# =====================
|
|
# == Private Methods ==
|
|
# =====================
|
|
|
|
def _initBuffer(self):
|
|
"""Initialize the bitmap used for buffering the display."""
|
|
size = self.drawPanel.GetSize()
|
|
self.buffer = wx.EmptyBitmap(max(1,size.width),max(1,size.height))
|
|
dc = wx.BufferedDC(None, self.buffer)
|
|
dc.SetBackground(wx.Brush(self.drawPanel.GetBackgroundColour()))
|
|
dc.Clear()
|
|
self.drawContents(dc)
|
|
del dc # commits all drawing to the buffer
|
|
|
|
self.saved_offset = self.drawPanel.CalcUnscrolledPosition(0,0)
|
|
|
|
self._reInitBuffer = False
|
|
|
|
|
|
|
|
def _adjustMenus(self):
|
|
""" Adjust our menus and toolbar to reflect the current state of the
|
|
world.
|
|
|
|
Doing this manually rather than using an EVT_UPDATE_UI is a bit
|
|
more efficient (since it's only done when it's really needed),
|
|
but it means we have to remember to call _adjustMenus any time
|
|
menus may need adjusting.
|
|
"""
|
|
canSave = (self.fileName != None) and self.dirty
|
|
canRevert = (self.fileName != None) and self.dirty
|
|
canUndo = self.undoStack!=[]
|
|
canRedo = self.redoStack!=[]
|
|
selection = len(self.selection) > 0
|
|
onlyOne = len(self.selection) == 1
|
|
hasEditor = onlyOne and self.selection[0].hasPropertyEditor()
|
|
front = onlyOne and (self.selection[0] == self.contents[0])
|
|
back = onlyOne and (self.selection[0] == self.contents[-1])
|
|
|
|
# Enable/disable our menu items.
|
|
|
|
self.fileMenu.Enable(wx.ID_SAVE, canSave)
|
|
self.fileMenu.Enable(wx.ID_REVERT, canRevert)
|
|
|
|
self.editMenu.Enable(wx.ID_UNDO, canUndo)
|
|
self.editMenu.Enable(wx.ID_REDO, canRedo)
|
|
self.editMenu.Enable(menu_DUPLICATE, selection)
|
|
self.editMenu.Enable(menu_EDIT_PROPS,hasEditor)
|
|
self.editMenu.Enable(wx.ID_CLEAR, selection)
|
|
|
|
self.objectMenu.Enable(menu_MOVE_FORWARD, onlyOne and not front)
|
|
self.objectMenu.Enable(menu_MOVE_TO_FRONT, onlyOne and not front)
|
|
self.objectMenu.Enable(menu_MOVE_BACKWARD, onlyOne and not back)
|
|
self.objectMenu.Enable(menu_MOVE_TO_BACK, onlyOne and not back)
|
|
|
|
# Enable/disable our toolbar icons.
|
|
|
|
self.toolbar.EnableTool(wx.ID_NEW, True)
|
|
self.toolbar.EnableTool(wx.ID_OPEN, True)
|
|
self.toolbar.EnableTool(wx.ID_SAVE, canSave)
|
|
self.toolbar.EnableTool(wx.ID_UNDO, canUndo)
|
|
self.toolbar.EnableTool(wx.ID_REDO, canRedo)
|
|
self.toolbar.EnableTool(menu_DUPLICATE, selection)
|
|
self.toolbar.EnableTool(menu_MOVE_FORWARD, onlyOne and not front)
|
|
self.toolbar.EnableTool(menu_MOVE_BACKWARD, onlyOne and not back)
|
|
|
|
|
|
def _setPenColour(self, colour):
|
|
""" Set the default or selected object's pen colour.
|
|
"""
|
|
if len(self.selection) > 0:
|
|
self.saveUndoInfo()
|
|
for obj in self.selection:
|
|
obj.setPenColour(colour)
|
|
self.requestRedraw()
|
|
|
|
self.penColour = colour
|
|
self.optionIndicator.setPenColour(colour)
|
|
|
|
|
|
def _setFillColour(self, colour):
|
|
""" Set the default or selected object's fill colour.
|
|
"""
|
|
if len(self.selection) > 0:
|
|
self.saveUndoInfo()
|
|
for obj in self.selection:
|
|
obj.setFillColour(colour)
|
|
self.requestRedraw()
|
|
|
|
self.fillColour = colour
|
|
self.optionIndicator.setFillColour(colour)
|
|
|
|
|
|
def _setLineSize(self, size):
|
|
""" Set the default or selected object's line size.
|
|
"""
|
|
if len(self.selection) > 0:
|
|
self.saveUndoInfo()
|
|
for obj in self.selection:
|
|
obj.setLineSize(size)
|
|
self.requestRedraw()
|
|
|
|
self.lineSize = size
|
|
self.optionIndicator.setLineSize(size)
|
|
|
|
|
|
def _buildStoredState(self):
|
|
""" Remember the current state of the document, to allow for undo.
|
|
|
|
We make a copy of the document's contents, so that we can return to
|
|
the previous contents if the user does something and then wants to
|
|
undo the operation.
|
|
|
|
Returns an object representing the current document state.
|
|
"""
|
|
savedContents = []
|
|
for obj in self.contents:
|
|
savedContents.append([obj.__class__, obj.getData()])
|
|
|
|
savedSelection = []
|
|
for i in range(len(self.contents)):
|
|
if self.contents[i] in self.selection:
|
|
savedSelection.append(i)
|
|
|
|
info = {"contents" : savedContents,
|
|
"selection" : savedSelection}
|
|
|
|
return info
|
|
|
|
def _restoreStoredState(self, savedState):
|
|
"""Restore the state of the document to a previous point for undo/redo.
|
|
|
|
Takes a stored state object and recreates the document from it.
|
|
Used by undo/redo implementation.
|
|
"""
|
|
self.contents = []
|
|
|
|
for draw_class, data in savedState["contents"]:
|
|
obj = draw_class()
|
|
obj.setData(data)
|
|
self.contents.append(obj)
|
|
|
|
self.selection = []
|
|
for i in savedState["selection"]:
|
|
self.selection.append(self.contents[i])
|
|
|
|
self.dirty = True
|
|
self._adjustMenus()
|
|
self.requestRedraw()
|
|
|
|
def _resizeObject(self, obj, anchorPt, oldPt, newPt):
|
|
""" Resize the given object.
|
|
|
|
'anchorPt' is the unchanging corner of the object, while the
|
|
opposite corner has been resized. 'oldPt' are the current
|
|
coordinates for this corner, while 'newPt' are the new coordinates.
|
|
The object should fit within the given dimensions, though if the
|
|
new point is less than the anchor point the object will need to be
|
|
moved as well as resized, to avoid giving it a negative size.
|
|
"""
|
|
if isinstance(obj, TextDrawingObject):
|
|
# Not allowed to resize text objects -- they're sized to fit text.
|
|
wx.Bell()
|
|
return
|
|
|
|
self.saveUndoInfo()
|
|
|
|
topLeft = wx.Point(min(anchorPt.x, newPt.x),
|
|
min(anchorPt.y, newPt.y))
|
|
botRight = wx.Point(max(anchorPt.x, newPt.x),
|
|
max(anchorPt.y, newPt.y))
|
|
|
|
newWidth = botRight.x - topLeft.x
|
|
newHeight = botRight.y - topLeft.y
|
|
|
|
if isinstance(obj, LineDrawingObject):
|
|
# Adjust the line so that its start and end points match the new
|
|
# overall object size.
|
|
|
|
startPt = obj.getStartPt()
|
|
endPt = obj.getEndPt()
|
|
|
|
slopesDown = ((startPt.x < endPt.x) and (startPt.y < endPt.y)) or \
|
|
((startPt.x > endPt.x) and (startPt.y > endPt.y))
|
|
|
|
# Handle the user flipping the line.
|
|
|
|
hFlip = ((anchorPt.x < oldPt.x) and (anchorPt.x > newPt.x)) or \
|
|
((anchorPt.x > oldPt.x) and (anchorPt.x < newPt.x))
|
|
vFlip = ((anchorPt.y < oldPt.y) and (anchorPt.y > newPt.y)) or \
|
|
((anchorPt.y > oldPt.y) and (anchorPt.y < newPt.y))
|
|
|
|
if (hFlip and not vFlip) or (vFlip and not hFlip):
|
|
slopesDown = not slopesDown # Line flipped.
|
|
|
|
if slopesDown:
|
|
obj.setStartPt(wx.Point(0, 0))
|
|
obj.setEndPt(wx.Point(newWidth, newHeight))
|
|
else:
|
|
obj.setStartPt(wx.Point(0, newHeight))
|
|
obj.setEndPt(wx.Point(newWidth, 0))
|
|
|
|
# Finally, adjust the bounds of the object to match the new dimensions.
|
|
|
|
obj.setPosition(topLeft)
|
|
obj.setSize(wx.Size(botRight.x - topLeft.x, botRight.y - topLeft.y))
|
|
|
|
self.requestRedraw()
|
|
|
|
|
|
def _moveObject(self, offsetX, offsetY):
|
|
""" Move the currently selected object(s) by the given offset.
|
|
"""
|
|
self.saveUndoInfo()
|
|
|
|
for obj in self.selection:
|
|
pos = obj.getPosition()
|
|
pos.x = pos.x + offsetX
|
|
pos.y = pos.y + offsetY
|
|
obj.setPosition(pos)
|
|
|
|
self.requestRedraw()
|
|
|
|
|
|
def _buildLineSizePopup(self, lineSize):
|
|
""" Build the pop-up menu used to set the line size.
|
|
|
|
'lineSize' is the current line size value. The corresponding item
|
|
is checked in the pop-up menu.
|
|
"""
|
|
menu = wx.Menu()
|
|
menu.Append(id_LINESIZE_0, "no line", kind=wx.ITEM_CHECK)
|
|
menu.Append(id_LINESIZE_1, "1-pixel line", kind=wx.ITEM_CHECK)
|
|
menu.Append(id_LINESIZE_2, "2-pixel line", kind=wx.ITEM_CHECK)
|
|
menu.Append(id_LINESIZE_3, "3-pixel line", kind=wx.ITEM_CHECK)
|
|
menu.Append(id_LINESIZE_4, "4-pixel line", kind=wx.ITEM_CHECK)
|
|
menu.Append(id_LINESIZE_5, "5-pixel line", kind=wx.ITEM_CHECK)
|
|
|
|
if lineSize == 0: menu.Check(id_LINESIZE_0, True)
|
|
elif lineSize == 1: menu.Check(id_LINESIZE_1, True)
|
|
elif lineSize == 2: menu.Check(id_LINESIZE_2, True)
|
|
elif lineSize == 3: menu.Check(id_LINESIZE_3, True)
|
|
elif lineSize == 4: menu.Check(id_LINESIZE_4, True)
|
|
elif lineSize == 5: menu.Check(id_LINESIZE_5, True)
|
|
|
|
self.Bind(wx.EVT_MENU, self._lineSizePopupSelected, id=id_LINESIZE_0, id2=id_LINESIZE_5)
|
|
|
|
return menu
|
|
|
|
|
|
def _lineSizePopupSelected(self, event):
|
|
""" Respond to the user selecting an item from the line size popup menu
|
|
"""
|
|
id = event.GetId()
|
|
if id == id_LINESIZE_0: self._setLineSize(0)
|
|
elif id == id_LINESIZE_1: self._setLineSize(1)
|
|
elif id == id_LINESIZE_2: self._setLineSize(2)
|
|
elif id == id_LINESIZE_3: self._setLineSize(3)
|
|
elif id == id_LINESIZE_4: self._setLineSize(4)
|
|
elif id == id_LINESIZE_5: self._setLineSize(5)
|
|
else:
|
|
wx.Bell()
|
|
return
|
|
|
|
self.optionIndicator.setLineSize(self.lineSize)
|
|
|
|
|
|
def _getEventCoordinates(self, event):
|
|
""" Return the coordinates associated with the given mouse event.
|
|
|
|
The coordinates have to be adjusted to allow for the current scroll
|
|
position.
|
|
"""
|
|
originX, originY = self.drawPanel.GetViewStart()
|
|
unitX, unitY = self.drawPanel.GetScrollPixelsPerUnit()
|
|
return wx.Point(event.GetX() + (originX * unitX),
|
|
event.GetY() + (originY * unitY))
|
|
|
|
|
|
def _drawObjectOutline(self, offsetX, offsetY):
|
|
""" Draw an outline of the currently selected object.
|
|
|
|
The selected object's outline is drawn at the object's position
|
|
plus the given offset.
|
|
|
|
Note that the outline is drawn by *inverting* the window's
|
|
contents, so calling _drawObjectOutline twice in succession will
|
|
restore the window's contents back to what they were previously.
|
|
"""
|
|
if len(self.selection) != 1: return
|
|
|
|
position = self.selection[0].getPosition()
|
|
size = self.selection[0].getSize()
|
|
|
|
dc = wx.ClientDC(self.drawPanel)
|
|
self.drawPanel.PrepareDC(dc)
|
|
dc.BeginDrawing()
|
|
dc.SetPen(wx.BLACK_DASHED_PEN)
|
|
dc.SetBrush(wx.TRANSPARENT_BRUSH)
|
|
dc.SetLogicalFunction(wx.INVERT)
|
|
|
|
dc.DrawRectangle(position.x + offsetX, position.y + offsetY,
|
|
size.width, size.height)
|
|
|
|
dc.EndDrawing()
|
|
|
|
|
|
#============================================================================
|
|
class DrawingTool(object):
|
|
"""Base class for drawing tools"""
|
|
|
|
def __init__(self):
|
|
pass
|
|
|
|
def getDefaultCursor(self):
|
|
"""Return the cursor to use by default which this drawing tool is selected"""
|
|
return wx.STANDARD_CURSOR
|
|
|
|
def draw(self,dc):
|
|
pass
|
|
|
|
|
|
def onMouseEvent(self,parent, event):
|
|
"""Mouse events passed in from the parent.
|
|
|
|
Returns True if the event is handled by the tool,
|
|
False if the canvas can try to use it.
|
|
"""
|
|
event.Skip()
|
|
return False
|
|
|
|
#----------------------------------------------------------------------------
|
|
class SelectDrawingTool(DrawingTool):
|
|
"""Represents the tool for selecting things"""
|
|
|
|
def __init__(self):
|
|
self.curHandle = None
|
|
self.curObject = None
|
|
self.objModified = False
|
|
self.startPt = None
|
|
self.curPt = None
|
|
|
|
def getDefaultCursor(self):
|
|
"""Return the cursor to use by default which this drawing tool is selected"""
|
|
return wx.STANDARD_CURSOR
|
|
|
|
def draw(self, dc):
|
|
if self._doingRectSelection():
|
|
dc.SetPen(wx.BLACK_DASHED_PEN)
|
|
dc.SetBrush(wx.TRANSPARENT_BRUSH)
|
|
x = [self.startPt.x, self.curPt.x]; x.sort()
|
|
y = [self.startPt.y, self.curPt.y]; y.sort()
|
|
dc.DrawRectangle(x[0],y[0], x[1]-x[0],y[1]-y[0])
|
|
|
|
|
|
def onMouseEvent(self,parent, event):
|
|
handlers = { wx.EVT_LEFT_DOWN.evtType[0]: self.onMouseLeftDown,
|
|
wx.EVT_MOTION.evtType[0]: self.onMouseMotion,
|
|
wx.EVT_LEFT_UP.evtType[0]: self.onMouseLeftUp,
|
|
wx.EVT_LEFT_DCLICK.evtType[0]: self.onMouseLeftDClick }
|
|
handler = handlers.get(event.GetEventType())
|
|
if handler is not None:
|
|
return handler(parent,event)
|
|
else:
|
|
event.Skip()
|
|
return False
|
|
|
|
def onMouseLeftDown(self,parent,event):
|
|
mousePt = wx.Point(event.X,event.Y)
|
|
obj, handle = parent.getObjectAndSelectionHandleAt(mousePt)
|
|
self.startPt = mousePt
|
|
self.curPt = mousePt
|
|
if obj is not None and handle is not None:
|
|
self.curObject = obj
|
|
self.curHandle = handle
|
|
else:
|
|
self.curObject = None
|
|
self.curHandle = None
|
|
|
|
obj = parent.getObjectAt(mousePt)
|
|
if self.curObject is None and obj is not None:
|
|
self.curObject = obj
|
|
self.dragDelta = obj.position-mousePt
|
|
self.curHandle = None
|
|
parent.select(obj, event.ShiftDown())
|
|
|
|
return True
|
|
|
|
def onMouseMotion(self,parent,event):
|
|
if not event.LeftIsDown(): return
|
|
|
|
self.curPt = wx.Point(event.X,event.Y)
|
|
|
|
obj,handle = self.curObject,self.curHandle
|
|
if self._doingDragHandle():
|
|
self._prepareToModify(parent)
|
|
obj.moveHandle(handle,event.X,event.Y)
|
|
parent.requestRedraw()
|
|
|
|
elif self._doingDragObject():
|
|
self._prepareToModify(parent)
|
|
obj.position = self.curPt + self.dragDelta
|
|
parent.requestRedraw()
|
|
|
|
elif self._doingRectSelection():
|
|
parent.requestRedraw()
|
|
|
|
return True
|
|
|
|
def onMouseLeftUp(self,parent,event):
|
|
|
|
obj,handle = self.curObject,self.curHandle
|
|
if self._doingDragHandle():
|
|
obj.moveHandle(handle,event.X,event.Y)
|
|
obj.finalizeHandle(handle,event.X,event.Y)
|
|
|
|
elif self._doingDragObject():
|
|
curPt = wx.Point(event.X,event.Y)
|
|
obj.position = curPt + self.dragDelta
|
|
|
|
elif self._doingRectSelection():
|
|
x = [event.X, self.startPt.x]
|
|
y = [event.Y, self.startPt.y]
|
|
x.sort()
|
|
y.sort()
|
|
parent.selectByRectangle(x[0],y[0],x[1]-x[0],y[1]-y[0])
|
|
|
|
|
|
self.curObject = None
|
|
self.curHandle = None
|
|
self.curPt = None
|
|
self.startPt = None
|
|
self.objModified = False
|
|
parent.requestRedraw()
|
|
|
|
return True
|
|
|
|
def onMouseLeftDClick(self,parent,event):
|
|
event.Skip()
|
|
mousePt = wx.Point(event.X,event.Y)
|
|
obj = parent.getObjectAt(mousePt)
|
|
if obj and obj.hasPropertyEditor():
|
|
if obj.doPropertyEdit(parent):
|
|
parent.requestRedraw()
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def _prepareToModify(self,parent):
|
|
if not self.objModified:
|
|
parent.saveUndoInfo()
|
|
self.objModified = True
|
|
|
|
def _doingRectSelection(self):
|
|
return self.curObject is None \
|
|
and self.startPt is not None \
|
|
and self.curPt is not None
|
|
|
|
def _doingDragObject(self):
|
|
return self.curObject is not None and self.curHandle is None
|
|
|
|
def _doingDragHandle(self):
|
|
return self.curObject is not None and self.curHandle is not None
|
|
|
|
|
|
|
|
#----------------------------------------------------------------------------
|
|
class LineDrawingTool(DrawingTool):
|
|
"""Represents the tool for drawing lines"""
|
|
|
|
def __init__(self):
|
|
self.newObject = None
|
|
self.startPt = None
|
|
|
|
|
|
def getDefaultCursor(self):
|
|
"""Return the cursor to use by default which this drawing tool is selected"""
|
|
return wx.StockCursor(wx.CURSOR_PENCIL)
|
|
|
|
def draw(self, dc):
|
|
if self.newObject is None: return
|
|
self.newObject.draw(dc,True)
|
|
|
|
def onMouseEvent(self,parent, event):
|
|
handlers = { wx.EVT_LEFT_DOWN.evtType[0]: self.onMouseLeftDown,
|
|
wx.EVT_MOTION.evtType[0]: self.onMouseMotion,
|
|
wx.EVT_LEFT_UP.evtType[0]: self.onMouseLeftUp }
|
|
handler = handlers.get(event.GetEventType())
|
|
if handler is not None:
|
|
return handler(parent,event)
|
|
else:
|
|
event.Skip()
|
|
return False
|
|
|
|
def onMouseLeftDown(self,parent, event):
|
|
self.startPt = wx.Point(event.GetX(), event.GetY())
|
|
self.newObject = None
|
|
event.Skip()
|
|
return True
|
|
|
|
def onMouseMotion(self,parent, event):
|
|
if not event.Dragging(): return
|
|
|
|
if self.newObject is None:
|
|
obj = LineDrawingObject(startPt=wx.Point(0,0),
|
|
penColour=parent.penColour,
|
|
fillColour=parent.fillColour,
|
|
lineSize=parent.lineSize,
|
|
position=wx.Point(event.X,event.Y))
|
|
self.newObject = obj
|
|
|
|
self._updateObjFromEvent(self.newObject, event)
|
|
|
|
parent.requestRedraw()
|
|
event.Skip()
|
|
return True
|
|
|
|
def onMouseLeftUp(self,parent, event):
|
|
|
|
if self.newObject is None:
|
|
return
|
|
|
|
self._updateObjFromEvent(self.newObject,event)
|
|
|
|
parent.addObject(self.newObject)
|
|
|
|
self.newObject = None
|
|
self.startPt = None
|
|
|
|
event.Skip()
|
|
return True
|
|
|
|
|
|
def _updateObjFromEvent(self,obj,event):
|
|
obj.setEndPt(wx.Point(event.X,event.Y))
|
|
|
|
|
|
#----------------------------------------------------------------------------
|
|
class RectDrawingTool(DrawingTool):
|
|
"""Represents the tool for drawing rectangles"""
|
|
|
|
def __init__(self):
|
|
self.newObject = None
|
|
|
|
def getDefaultCursor(self):
|
|
"""Return the cursor to use by default which this drawing tool is selected"""
|
|
return wx.CROSS_CURSOR
|
|
|
|
def draw(self, dc):
|
|
if self.newObject is None: return
|
|
self.newObject.draw(dc,True)
|
|
|
|
|
|
def onMouseEvent(self,parent, event):
|
|
handlers = { wx.EVT_LEFT_DOWN.evtType[0]: self.onMouseLeftDown,
|
|
wx.EVT_MOTION.evtType[0]: self.onMouseMotion,
|
|
wx.EVT_LEFT_UP.evtType[0]: self.onMouseLeftUp }
|
|
handler = handlers.get(event.GetEventType())
|
|
if handler is not None:
|
|
return handler(parent,event)
|
|
else:
|
|
event.Skip()
|
|
return False
|
|
|
|
def onMouseLeftDown(self,parent, event):
|
|
self.startPt = wx.Point(event.GetX(), event.GetY())
|
|
self.newObject = None
|
|
event.Skip()
|
|
return True
|
|
|
|
def onMouseMotion(self,parent, event):
|
|
if not event.Dragging(): return
|
|
|
|
if self.newObject is None:
|
|
obj = RectDrawingObject(penColour=parent.penColour,
|
|
fillColour=parent.fillColour,
|
|
lineSize=parent.lineSize)
|
|
self.newObject = obj
|
|
|
|
self._updateObjFromEvent(self.newObject, event)
|
|
|
|
parent.requestRedraw()
|
|
event.Skip()
|
|
return True
|
|
|
|
def onMouseLeftUp(self,parent, event):
|
|
|
|
if self.newObject is None:
|
|
return
|
|
|
|
self._updateObjFromEvent(self.newObject,event)
|
|
|
|
parent.addObject(self.newObject)
|
|
|
|
self.newObject = None
|
|
|
|
event.Skip()
|
|
return True
|
|
|
|
|
|
def _updateObjFromEvent(self,obj,event):
|
|
x = [event.X, self.startPt.x]
|
|
y = [event.Y, self.startPt.y]
|
|
x.sort()
|
|
y.sort()
|
|
width = x[1]-x[0]
|
|
height = y[1]-y[0]
|
|
|
|
obj.setPosition(wx.Point(x[0],y[0]))
|
|
obj.setSize(wx.Size(width,height))
|
|
|
|
|
|
|
|
|
|
#----------------------------------------------------------------------------
|
|
class EllipseDrawingTool(DrawingTool):
|
|
"""Represents the tool for drawing ellipses"""
|
|
|
|
def getDefaultCursor(self):
|
|
"""Return the cursor to use by default which this drawing tool is selected"""
|
|
return wx.CROSS_CURSOR
|
|
|
|
|
|
def __init__(self):
|
|
self.newObject = None
|
|
|
|
def getDefaultCursor(self):
|
|
"""Return the cursor to use by default which this drawing tool is selected"""
|
|
return wx.CROSS_CURSOR
|
|
|
|
def draw(self, dc):
|
|
if self.newObject is None: return
|
|
self.newObject.draw(dc,True)
|
|
|
|
|
|
def onMouseEvent(self,parent, event):
|
|
handlers = { wx.EVT_LEFT_DOWN.evtType[0]: self.onMouseLeftDown,
|
|
wx.EVT_MOTION.evtType[0]: self.onMouseMotion,
|
|
wx.EVT_LEFT_UP.evtType[0]: self.onMouseLeftUp }
|
|
handler = handlers.get(event.GetEventType())
|
|
if handler is not None:
|
|
return handler(parent,event)
|
|
else:
|
|
event.Skip()
|
|
return False
|
|
|
|
def onMouseLeftDown(self,parent, event):
|
|
self.startPt = wx.Point(event.GetX(), event.GetY())
|
|
self.newObject = None
|
|
event.Skip()
|
|
return True
|
|
|
|
def onMouseMotion(self,parent, event):
|
|
if not event.Dragging(): return
|
|
|
|
if self.newObject is None:
|
|
obj = EllipseDrawingObject(penColour=parent.penColour,
|
|
fillColour=parent.fillColour,
|
|
lineSize=parent.lineSize)
|
|
self.newObject = obj
|
|
|
|
self._updateObjFromEvent(self.newObject, event)
|
|
|
|
parent.requestRedraw()
|
|
event.Skip()
|
|
return True
|
|
|
|
def onMouseLeftUp(self,parent, event):
|
|
|
|
if self.newObject is None:
|
|
return
|
|
|
|
self._updateObjFromEvent(self.newObject,event)
|
|
|
|
parent.addObject(self.newObject)
|
|
|
|
self.newObject = None
|
|
|
|
event.Skip()
|
|
return True
|
|
|
|
|
|
def _updateObjFromEvent(self,obj,event):
|
|
x = [event.X, self.startPt.x]
|
|
y = [event.Y, self.startPt.y]
|
|
x.sort()
|
|
y.sort()
|
|
width = x[1]-x[0]
|
|
height = y[1]-y[0]
|
|
|
|
obj.setPosition(wx.Point(x[0],y[0]))
|
|
obj.setSize(wx.Size(width,height))
|
|
|
|
|
|
#----------------------------------------------------------------------------
|
|
class PolygonDrawingTool(DrawingTool):
|
|
"""Represents the tool for drawing polygons"""
|
|
|
|
def __init__(self):
|
|
self.newObject = None
|
|
|
|
def getDefaultCursor(self):
|
|
"""Return the cursor to use by default which this drawing tool is selected"""
|
|
return wx.CROSS_CURSOR
|
|
|
|
|
|
def draw(self, dc):
|
|
if self.newObject is None: return
|
|
self.newObject.draw(dc,True)
|
|
|
|
|
|
def onMouseEvent(self,parent, event):
|
|
handlers = { wx.EVT_LEFT_DOWN.evtType[0]: self.onMouseLeftDown,
|
|
wx.EVT_MOTION.evtType[0]: self.onMouseMotion,
|
|
wx.EVT_LEFT_UP.evtType[0]: self.onMouseLeftUp,
|
|
wx.EVT_LEFT_DCLICK.evtType[0]:self.onMouseLeftDClick }
|
|
handler = handlers.get(event.GetEventType())
|
|
if handler is not None:
|
|
return handler(parent,event)
|
|
else:
|
|
event.Skip()
|
|
return False
|
|
|
|
def onMouseLeftDown(self,parent, event):
|
|
event.Skip()
|
|
self.startPt = (event.GetX(), event.GetY())
|
|
if self.newObject is None:
|
|
obj = PolygonDrawingObject(points=[(0,0)],penColour=parent.penColour,
|
|
fillColour=parent.fillColour,
|
|
lineSize=parent.lineSize,
|
|
position=wx.Point(event.X, event.Y))
|
|
obj.addPoint(event.X,event.Y)
|
|
self.newObject = obj
|
|
else:
|
|
CLOSE_THRESH=3
|
|
pt0 = self.newObject.getPoint(0)
|
|
if abs(pt0[0]-event.X)<CLOSE_THRESH and abs(pt0[1]-event.Y)<CLOSE_THRESH:
|
|
self.newObject.popPoint()
|
|
parent.addObject(self.newObject)
|
|
self.newObject = None
|
|
else:
|
|
self.newObject.addPoint(event.X,event.Y)
|
|
|
|
return True
|
|
|
|
def onMouseMotion(self,parent, event):
|
|
|
|
event.Skip()
|
|
if self.newObject:
|
|
self.newObject.movePoint(-1, event.X, event.Y)
|
|
parent.requestRedraw()
|
|
return True
|
|
|
|
return False
|
|
|
|
def onMouseLeftDClick(self,parent,event):
|
|
event.Skip()
|
|
if self.newObject:
|
|
CLOSE_THRESH=3
|
|
pt0 = self.newObject.getPoint(0)
|
|
if abs(pt0[0]-event.X)<CLOSE_THRESH and abs(pt0[1]-event.Y)<CLOSE_THRESH:
|
|
self.newObject.popPoint()
|
|
self.newObject.popPoint()
|
|
parent.addObject(self.newObject)
|
|
self.newObject = None
|
|
|
|
return True
|
|
|
|
def onMouseLeftUp(self,parent, event):
|
|
event.Skip()
|
|
return True
|
|
|
|
|
|
|
|
|
|
#----------------------------------------------------------------------------
|
|
class ScribbleDrawingTool(DrawingTool):
|
|
"""Represents the tool for drawing scribble drawing objects"""
|
|
|
|
def __init__(self):
|
|
self.newObject = None
|
|
|
|
def getDefaultCursor(self):
|
|
"""Return the cursor to use by default which this drawing tool is selected"""
|
|
return wx.StockCursor(wx.CURSOR_PENCIL)
|
|
|
|
def draw(self, dc):
|
|
if self.newObject is None: return
|
|
self.newObject.draw(dc,True)
|
|
|
|
|
|
def onMouseEvent(self,parent, event):
|
|
handlers = { wx.EVT_LEFT_DOWN.evtType[0]: self.onMouseLeftDown,
|
|
wx.EVT_MOTION.evtType[0]: self.onMouseMotion,
|
|
wx.EVT_LEFT_UP.evtType[0]: self.onMouseLeftUp
|
|
}
|
|
handler = handlers.get(event.GetEventType())
|
|
if handler is not None:
|
|
return handler(parent,event)
|
|
else:
|
|
event.Skip()
|
|
return False
|
|
|
|
def onMouseLeftDown(self,parent, event):
|
|
event.Skip()
|
|
obj = ScribbleDrawingObject(points=[(0,0)],penColour=parent.penColour,
|
|
fillColour=parent.fillColour,
|
|
lineSize=parent.lineSize,
|
|
position=wx.Point(event.X, event.Y))
|
|
self.newObject = obj
|
|
return True
|
|
|
|
def onMouseMotion(self,parent, event):
|
|
event.Skip()
|
|
if self.newObject:
|
|
self.newObject.addPoint(event.X,event.Y)
|
|
parent.requestRedraw()
|
|
return True
|
|
|
|
return False
|
|
|
|
def onMouseLeftUp(self,parent, event):
|
|
event.Skip()
|
|
if self.newObject:
|
|
parent.addObject(self.newObject)
|
|
self.newObject = None
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
#----------------------------------------------------------------------------
|
|
class TextDrawingTool(DrawingTool):
|
|
"""Represents the tool for drawing text"""
|
|
|
|
def getDefaultCursor(self):
|
|
"""Return the cursor to use by default which this drawing tool is selected"""
|
|
return wx.StockCursor(wx.CURSOR_IBEAM)
|
|
|
|
def onMouseEvent(self,parent, event):
|
|
handlers = { #wx.EVT_LEFT_DOWN.evtType[0]: self.onMouseLeftDown,
|
|
#wx.EVT_MOTION.evtType[0]: self.onMouseMotion,
|
|
wx.EVT_LEFT_UP.evtType[0]: self.onMouseLeftUp
|
|
}
|
|
handler = handlers.get(event.GetEventType())
|
|
if handler is not None:
|
|
return handler(parent,event)
|
|
else:
|
|
event.Skip()
|
|
return False
|
|
|
|
def onMouseLeftUp(self,parent, event):
|
|
|
|
editor = parent.getTextEditor()
|
|
editor.SetTitle("Create Text Object")
|
|
if editor.ShowModal() == wx.ID_CANCEL:
|
|
editor.Hide()
|
|
return True
|
|
|
|
obj = TextDrawingObject(position=wx.Point(event.X, event.Y))
|
|
editor.dialogToObject(obj)
|
|
editor.Hide()
|
|
|
|
parent.addObject(obj)
|
|
|
|
event.Skip()
|
|
return True
|
|
|
|
|
|
|
|
#============================================================================
|
|
class DrawingObject(object):
|
|
""" Base class for objects within the drawing panel.
|
|
|
|
A pySketch document consists of a front-to-back ordered list of
|
|
DrawingObjects. Each DrawingObject has the following properties:
|
|
|
|
'position' The position of the object within the document.
|
|
'size' The size of the object within the document.
|
|
'penColour' The colour to use for drawing the object's outline.
|
|
'fillColour' Colour to use for drawing object's interior.
|
|
'lineSize' Line width (in pixels) to use for object's outline.
|
|
"""
|
|
|
|
# ==================
|
|
# == Constructors ==
|
|
# ==================
|
|
|
|
def __init__(self, position=wx.Point(0, 0), size=wx.Size(0, 0),
|
|
penColour=wx.BLACK, fillColour=wx.WHITE, lineSize=1,
|
|
):
|
|
""" Standard constructor.
|
|
|
|
The remaining parameters let you set various options for the newly
|
|
created DrawingObject.
|
|
"""
|
|
# One must take great care with constructed default arguments
|
|
# like wx.Point(0,0) above. *EVERY* caller that uses the
|
|
# default will get the same instance. Thus, below we make a
|
|
# deep copy of those arguments with object defaults.
|
|
|
|
self.position = wx.Point(position.x,position.y)
|
|
self.size = wx.Size(size.x,size.y)
|
|
self.penColour = penColour
|
|
self.fillColour = fillColour
|
|
self.lineSize = lineSize
|
|
|
|
# =============================
|
|
# == Object Property Methods ==
|
|
# =============================
|
|
|
|
def getData(self):
|
|
""" Return a copy of the object's internal data.
|
|
|
|
This is used to save this DrawingObject to disk.
|
|
"""
|
|
return [self.position.x, self.position.y,
|
|
self.size.width, self.size.height,
|
|
self.penColour.Red(),
|
|
self.penColour.Green(),
|
|
self.penColour.Blue(),
|
|
self.fillColour.Red(),
|
|
self.fillColour.Green(),
|
|
self.fillColour.Blue(),
|
|
self.lineSize]
|
|
|
|
|
|
def setData(self, data):
|
|
""" Set the object's internal data.
|
|
|
|
'data' is a copy of the object's saved data, as returned by
|
|
getData() above. This is used to restore a previously saved
|
|
DrawingObject.
|
|
|
|
Returns an iterator to any remaining data not consumed by
|
|
this base class method.
|
|
"""
|
|
#data = copy.deepcopy(data) # Needed?
|
|
|
|
d = iter(data)
|
|
try:
|
|
self.position = wx.Point(d.next(), d.next())
|
|
self.size = wx.Size(d.next(), d.next())
|
|
self.penColour = wx.Colour(red=d.next(),
|
|
green=d.next(),
|
|
blue=d.next())
|
|
self.fillColour = wx.Colour(red=d.next(),
|
|
green=d.next(),
|
|
blue=d.next())
|
|
self.lineSize = d.next()
|
|
except StopIteration:
|
|
raise ValueError('Not enough data in setData call')
|
|
|
|
return d
|
|
|
|
|
|
def hasPropertyEditor(self):
|
|
return False
|
|
|
|
def doPropertyEdit(self, parent):
|
|
assert False, "Must be overridden if hasPropertyEditor returns True"
|
|
|
|
def setPosition(self, position):
|
|
""" Set the origin (top-left corner) for this DrawingObject.
|
|
"""
|
|
self.position = position
|
|
|
|
|
|
def getPosition(self):
|
|
""" Return this DrawingObject's position.
|
|
"""
|
|
return self.position
|
|
|
|
|
|
def setSize(self, size):
|
|
""" Set the size for this DrawingObject.
|
|
"""
|
|
self.size = size
|
|
|
|
|
|
def getSize(self):
|
|
""" Return this DrawingObject's size.
|
|
"""
|
|
return self.size
|
|
|
|
|
|
def setPenColour(self, colour):
|
|
""" Set the pen colour used for this DrawingObject.
|
|
"""
|
|
self.penColour = colour
|
|
|
|
|
|
def getPenColour(self):
|
|
""" Return this DrawingObject's pen colour.
|
|
"""
|
|
return self.penColour
|
|
|
|
|
|
def setFillColour(self, colour):
|
|
""" Set the fill colour used for this DrawingObject.
|
|
"""
|
|
self.fillColour = colour
|
|
|
|
|
|
def getFillColour(self):
|
|
""" Return this DrawingObject's fill colour.
|
|
"""
|
|
return self.fillColour
|
|
|
|
|
|
def setLineSize(self, lineSize):
|
|
""" Set the linesize used for this DrawingObject.
|
|
"""
|
|
self.lineSize = lineSize
|
|
|
|
|
|
def getLineSize(self):
|
|
""" Return this DrawingObject's line size.
|
|
"""
|
|
return self.lineSize
|
|
|
|
|
|
# ============================
|
|
# == Object Drawing Methods ==
|
|
# ============================
|
|
|
|
def draw(self, dc, selected):
|
|
""" Draw this DrawingObject into our window.
|
|
|
|
'dc' is the device context to use for drawing.
|
|
|
|
If 'selected' is True, the object is currently selected.
|
|
Drawing objects can use this to change the way selected objects
|
|
are drawn, however the actual drawing of selection handles
|
|
should be done in the 'drawHandles' method
|
|
"""
|
|
if self.lineSize == 0:
|
|
dc.SetPen(wx.Pen(self.penColour, self.lineSize, wx.TRANSPARENT))
|
|
else:
|
|
dc.SetPen(wx.Pen(self.penColour, self.lineSize, wx.SOLID))
|
|
dc.SetBrush(wx.Brush(self.fillColour, wx.SOLID))
|
|
|
|
self._privateDraw(dc, self.position, selected)
|
|
|
|
|
|
def drawHandles(self, dc):
|
|
"""Draw selection handles for this DrawingObject"""
|
|
|
|
# Default is to draw selection handles at all four corners.
|
|
dc.SetPen(wx.BLACK_PEN)
|
|
dc.SetBrush(wx.BLACK_BRUSH)
|
|
|
|
x,y = self.position
|
|
self._drawSelHandle(dc, x, y)
|
|
self._drawSelHandle(dc, x + self.size.width, y)
|
|
self._drawSelHandle(dc, x, y + self.size.height)
|
|
self._drawSelHandle(dc, x + self.size.width, y + self.size.height)
|
|
|
|
|
|
# =======================
|
|
# == Selection Methods ==
|
|
# =======================
|
|
|
|
def objectContainsPoint(self, x, y):
|
|
""" Returns True iff this object contains the given point.
|
|
|
|
This is used to determine if the user clicked on the object.
|
|
"""
|
|
# Firstly, ignore any points outside of the object's bounds.
|
|
|
|
if x < self.position.x: return False
|
|
if x > self.position.x + self.size.x: return False
|
|
if y < self.position.y: return False
|
|
if y > self.position.y + self.size.y: return False
|
|
|
|
# Now things get tricky. There's no straightforward way of
|
|
# knowing whether the point is within an arbitrary object's
|
|
# bounds...to get around this, we draw the object into a
|
|
# memory-based bitmap and see if the given point was drawn.
|
|
# This could no doubt be done more efficiently by some tricky
|
|
# maths, but this approach works and is simple enough.
|
|
|
|
# Subclasses can implement smarter faster versions of this.
|
|
|
|
bitmap = wx.EmptyBitmap(self.size.x + 10, self.size.y + 10)
|
|
dc = wx.MemoryDC()
|
|
dc.SelectObject(bitmap)
|
|
dc.BeginDrawing()
|
|
dc.SetBackground(wx.WHITE_BRUSH)
|
|
dc.Clear()
|
|
dc.SetPen(wx.Pen(wx.BLACK, self.lineSize + 5, wx.SOLID))
|
|
dc.SetBrush(wx.BLACK_BRUSH)
|
|
self._privateDraw(dc, wx.Point(5, 5), True)
|
|
dc.EndDrawing()
|
|
pixel = dc.GetPixel(x - self.position.x + 5, y - self.position.y + 5)
|
|
if (pixel.Red() == 0) and (pixel.Green() == 0) and (pixel.Blue() == 0):
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
handle_TOP = 0
|
|
handle_BOTTOM = 1
|
|
handle_LEFT = 0
|
|
handle_RIGHT = 1
|
|
|
|
def getSelectionHandleContainingPoint(self, x, y):
|
|
""" Return the selection handle containing the given point, if any.
|
|
|
|
We return one of the predefined selection handle ID codes.
|
|
"""
|
|
# Default implementation assumes selection handles at all four bbox corners.
|
|
# Return a list so we can modify the contents later in moveHandle()
|
|
if self._pointInSelRect(x, y, self.position.x, self.position.y):
|
|
return [self.handle_TOP, self.handle_LEFT]
|
|
elif self._pointInSelRect(x, y, self.position.x + self.size.width,
|
|
self.position.y):
|
|
return [self.handle_TOP, self.handle_RIGHT]
|
|
elif self._pointInSelRect(x, y, self.position.x,
|
|
self.position.y + self.size.height):
|
|
return [self.handle_BOTTOM, self.handle_LEFT]
|
|
elif self._pointInSelRect(x, y, self.position.x + self.size.width,
|
|
self.position.y + self.size.height):
|
|
return [self.handle_BOTTOM, self.handle_RIGHT]
|
|
else:
|
|
return None
|
|
|
|
def moveHandle(self, handle, x, y):
|
|
""" Move the specified selection handle to given canvas location.
|
|
"""
|
|
assert handle is not None
|
|
|
|
# Default implementation assumes selection handles at all four bbox corners.
|
|
pt = wx.Point(x,y)
|
|
x,y = self.position
|
|
w,h = self.size
|
|
if handle[0] == self.handle_TOP:
|
|
if handle[1] == self.handle_LEFT:
|
|
dpos = pt - self.position
|
|
self.position = pt
|
|
self.size.width -= dpos.x
|
|
self.size.height -= dpos.y
|
|
else:
|
|
dx = pt.x - ( x + w )
|
|
dy = pt.y - ( y )
|
|
self.position.y = pt.y
|
|
self.size.width += dx
|
|
self.size.height -= dy
|
|
else: # BOTTOM
|
|
if handle[1] == self.handle_LEFT:
|
|
dx = pt.x - ( x )
|
|
dy = pt.y - ( y + h )
|
|
self.position.x = pt.x
|
|
self.size.width -= dx
|
|
self.size.height += dy
|
|
else:
|
|
dpos = pt - self.position
|
|
dpos.x -= w
|
|
dpos.y -= h
|
|
self.size.width += dpos.x
|
|
self.size.height += dpos.y
|
|
|
|
|
|
# Finally, normalize so no negative widths or heights.
|
|
# And update the handle variable accordingly.
|
|
if self.size.height<0:
|
|
self.position.y += self.size.height
|
|
self.size.height = -self.size.height
|
|
handle[0] = 1-handle[0]
|
|
|
|
if self.size.width<0:
|
|
self.position.x += self.size.width
|
|
self.size.width = -self.size.width
|
|
handle[1] = 1-handle[1]
|
|
|
|
|
|
|
|
def finalizeHandle(self, handle, x, y):
|
|
pass
|
|
|
|
|
|
def objectWithinRect(self, x, y, width, height):
|
|
""" Return True iff this object falls completely within the given rect.
|
|
"""
|
|
if x > self.position.x: return False
|
|
if x + width < self.position.x + self.size.width: return False
|
|
if y > self.position.y: return False
|
|
if y + height < self.position.y + self.size.height: return False
|
|
return True
|
|
|
|
# =====================
|
|
# == Private Methods ==
|
|
# =====================
|
|
|
|
def _privateDraw(self, dc, position, selected):
|
|
""" Private routine to draw this DrawingObject.
|
|
|
|
'dc' is the device context to use for drawing, while 'position' is
|
|
the position in which to draw the object.
|
|
"""
|
|
pass
|
|
|
|
def _drawSelHandle(self, dc, x, y):
|
|
""" Draw a selection handle around this DrawingObject.
|
|
|
|
'dc' is the device context to draw the selection handle within,
|
|
while 'x' and 'y' are the coordinates to use for the centre of the
|
|
selection handle.
|
|
"""
|
|
dc.DrawRectangle(x - 3, y - 3, 6, 6)
|
|
|
|
|
|
def _pointInSelRect(self, x, y, rX, rY):
|
|
""" Return True iff (x, y) is within the selection handle at (rX, ry).
|
|
"""
|
|
if x < rX - 3: return False
|
|
elif x > rX + 3: return False
|
|
elif y < rY - 3: return False
|
|
elif y > rY + 3: return False
|
|
else: return True
|
|
|
|
|
|
#----------------------------------------------------------------------------
|
|
class LineDrawingObject(DrawingObject):
|
|
""" DrawingObject subclass that represents one line segment.
|
|
|
|
Adds the following members to the base DrawingObject:
|
|
'startPt' The point, relative to the object's position, where
|
|
the line starts.
|
|
'endPt' The point, relative to the object's position, where
|
|
the line ends.
|
|
"""
|
|
|
|
def __init__(self, startPt=wx.Point(0,0), endPt=wx.Point(0,0), *varg, **kwarg):
|
|
DrawingObject.__init__(self, *varg, **kwarg)
|
|
|
|
self.startPt = wx.Point(startPt.x,startPt.y)
|
|
self.endPt = wx.Point(endPt.x,endPt.y)
|
|
|
|
# ============================
|
|
# == Object Drawing Methods ==
|
|
# ============================
|
|
|
|
def drawHandles(self, dc):
|
|
"""Draw selection handles for this DrawingObject"""
|
|
|
|
dc.SetPen(wx.BLACK_PEN)
|
|
dc.SetBrush(wx.BLACK_BRUSH)
|
|
|
|
x,y = self.position
|
|
# Draw selection handles at the start and end points.
|
|
self._drawSelHandle(dc, x + self.startPt.x, y + self.startPt.y)
|
|
self._drawSelHandle(dc, x + self.endPt.x, y + self.endPt.y)
|
|
|
|
|
|
|
|
# =======================
|
|
# == Selection Methods ==
|
|
# =======================
|
|
|
|
|
|
handle_START_POINT = 1
|
|
handle_END_POINT = 2
|
|
|
|
def getSelectionHandleContainingPoint(self, x, y):
|
|
""" Return the selection handle containing the given point, if any.
|
|
|
|
We return one of the predefined selection handle ID codes.
|
|
"""
|
|
# We have selection handles at the start and end points.
|
|
if self._pointInSelRect(x, y, self.position.x + self.startPt.x,
|
|
self.position.y + self.startPt.y):
|
|
return self.handle_START_POINT
|
|
elif self._pointInSelRect(x, y, self.position.x + self.endPt.x,
|
|
self.position.y + self.endPt.y):
|
|
return self.handle_END_POINT
|
|
else:
|
|
return None
|
|
|
|
def moveHandle(self, handle, x, y):
|
|
"""Move the handle to specified handle to the specified canvas coordinates
|
|
"""
|
|
ptTrans = wx.Point(x-self.position.x, y-self.position.y)
|
|
if handle == self.handle_START_POINT:
|
|
self.startPt = ptTrans
|
|
elif handle == self.handle_END_POINT:
|
|
self.endPt = ptTrans
|
|
else:
|
|
raise ValueError("Bad handle type for a line")
|
|
|
|
self._updateBoundingBox()
|
|
|
|
# =============================
|
|
# == Object Property Methods ==
|
|
# =============================
|
|
|
|
def getData(self):
|
|
""" Return a copy of the object's internal data.
|
|
|
|
This is used to save this DrawingObject to disk.
|
|
"""
|
|
# get the basics
|
|
data = DrawingObject.getData(self)
|
|
# add our specifics
|
|
data += [self.startPt.x, self.startPt.y,
|
|
self.endPt.x, self.endPt.y]
|
|
return data
|
|
|
|
def setData(self, data):
|
|
""" Set the object's internal data.
|
|
|
|
'data' is a copy of the object's saved data, as returned by
|
|
getData() above. This is used to restore a previously saved
|
|
DrawingObject.
|
|
"""
|
|
#data = copy.deepcopy(data) # Needed?
|
|
|
|
d = DrawingObject.setData(self, data)
|
|
|
|
try:
|
|
self.startPt = wx.Point(d.next(), d.next())
|
|
self.endPt = wx.Point(d.next(), d.next())
|
|
except StopIteration:
|
|
raise ValueError('Not enough data in setData call')
|
|
|
|
return d
|
|
|
|
|
|
def setStartPt(self, startPt):
|
|
""" Set the starting point for this line DrawingObject.
|
|
"""
|
|
self.startPt = startPt - self.position
|
|
self._updateBoundingBox()
|
|
|
|
|
|
def getStartPt(self):
|
|
""" Return the starting point for this line DrawingObject.
|
|
"""
|
|
return self.startPt + self.position
|
|
|
|
|
|
def setEndPt(self, endPt):
|
|
""" Set the ending point for this line DrawingObject.
|
|
"""
|
|
self.endPt = endPt - self.position
|
|
self._updateBoundingBox()
|
|
|
|
|
|
def getEndPt(self):
|
|
""" Return the ending point for this line DrawingObject.
|
|
"""
|
|
return self.endPt + self.position
|
|
|
|
|
|
# =====================
|
|
# == Private Methods ==
|
|
# =====================
|
|
|
|
|
|
def _privateDraw(self, dc, position, selected):
|
|
""" Private routine to draw this DrawingObject.
|
|
|
|
'dc' is the device context to use for drawing, while 'position' is
|
|
the position in which to draw the object. If 'selected' is True,
|
|
the object is drawn with selection handles. This private drawing
|
|
routine assumes that the pen and brush have already been set by the
|
|
caller.
|
|
"""
|
|
dc.DrawLine(position.x + self.startPt.x,
|
|
position.y + self.startPt.y,
|
|
position.x + self.endPt.x,
|
|
position.y + self.endPt.y)
|
|
|
|
def _updateBoundingBox(self):
|
|
x = [self.startPt.x, self.endPt.x]; x.sort()
|
|
y = [self.startPt.y, self.endPt.y]; y.sort()
|
|
|
|
dp = wx.Point(-x[0],-y[0])
|
|
self.position.x += x[0]
|
|
self.position.y += y[0]
|
|
self.size.width = x[1]-x[0]
|
|
self.size.height = y[1]-y[0]
|
|
|
|
self.startPt += dp
|
|
self.endPt += dp
|
|
|
|
#----------------------------------------------------------------------------
|
|
class PolygonDrawingObject(DrawingObject):
|
|
""" DrawingObject subclass that represents a poly-line or polygon
|
|
"""
|
|
def __init__(self, points=[], *varg, **kwarg):
|
|
DrawingObject.__init__(self, *varg, **kwarg)
|
|
self.points = list(points)
|
|
|
|
# =======================
|
|
# == Selection Methods ==
|
|
# =======================
|
|
|
|
def getSelectionHandleContainingPoint(self, x, y):
|
|
""" Return the selection handle containing the given point, if any.
|
|
|
|
We return one of the predefined selection handle ID codes.
|
|
"""
|
|
# We have selection handles at the start and end points.
|
|
for i,p in enumerate(self.points):
|
|
if self._pointInSelRect(x, y,
|
|
self.position.x + p[0],
|
|
self.position.y + p[1]):
|
|
return i+1
|
|
|
|
return None
|
|
|
|
|
|
def addPoint(self, x,y):
|
|
self.points.append((x-self.position.x,y-self.position.y))
|
|
self._updateBoundingBox()
|
|
|
|
def getPoint(self, idx):
|
|
x,y = self.points[idx]
|
|
return (x+self.position.x,y+self.position.y)
|
|
|
|
def movePoint(self, idx, x,y):
|
|
self.points[idx] = (x-self.position.x,y-self.position.y)
|
|
self._updateBoundingBox()
|
|
|
|
def popPoint(self, idx=-1):
|
|
self.points.pop(idx)
|
|
self._updateBoundingBox()
|
|
|
|
# =====================
|
|
# == Drawing Methods ==
|
|
# =====================
|
|
|
|
def drawHandles(self, dc):
|
|
"""Draw selection handles for this DrawingObject"""
|
|
|
|
dc.SetPen(wx.BLACK_PEN)
|
|
dc.SetBrush(wx.BLACK_BRUSH)
|
|
|
|
x,y = self.position
|
|
# Draw selection handles at the start and end points.
|
|
for p in self.points:
|
|
self._drawSelHandle(dc, x + p[0], y + p[1])
|
|
|
|
def moveHandle(self, handle, x, y):
|
|
"""Move the specified handle"""
|
|
self.movePoint(handle-1,x,y)
|
|
|
|
|
|
# =============================
|
|
# == Object Property Methods ==
|
|
# =============================
|
|
|
|
def getData(self):
|
|
""" Return a copy of the object's internal data.
|
|
|
|
This is used to save this DrawingObject to disk.
|
|
"""
|
|
# get the basics
|
|
data = DrawingObject.getData(self)
|
|
# add our specifics
|
|
data += [list(self.points)]
|
|
|
|
return data
|
|
|
|
|
|
def setData(self, data):
|
|
""" Set the object's internal data.
|
|
|
|
'data' is a copy of the object's saved data, as returned by
|
|
getData() above. This is used to restore a previously saved
|
|
DrawingObject.
|
|
"""
|
|
#data = copy.deepcopy(data) # Needed?
|
|
d = DrawingObject.setData(self, data)
|
|
|
|
try:
|
|
self.points = d.next()
|
|
except StopIteration:
|
|
raise ValueError('Not enough data in setData call')
|
|
|
|
return d
|
|
|
|
|
|
# =====================
|
|
# == Private Methods ==
|
|
# =====================
|
|
def _privateDraw(self, dc, position, selected):
|
|
""" Private routine to draw this DrawingObject.
|
|
|
|
'dc' is the device context to use for drawing, while 'position' is
|
|
the position in which to draw the object. If 'selected' is True,
|
|
the object is drawn with selection handles. This private drawing
|
|
routine assumes that the pen and brush have already been set by the
|
|
caller.
|
|
"""
|
|
dc.DrawPolygon(self.points, position.x, position.y)
|
|
|
|
def _updateBoundingBox(self):
|
|
x = min([p[0] for p in self.points])
|
|
y = min([p[1] for p in self.points])
|
|
x2 = max([p[0] for p in self.points])
|
|
y2 = max([p[1] for p in self.points])
|
|
dx = -x
|
|
dy = -y
|
|
self.position.x += x
|
|
self.position.y += y
|
|
self.size.width = x2-x
|
|
self.size.height = y2-y
|
|
# update coords also because they're relative to self.position
|
|
for i,p in enumerate(self.points):
|
|
self.points[i] = (p[0]+dx,p[1]+dy)
|
|
|
|
|
|
#----------------------------------------------------------------------------
|
|
class ScribbleDrawingObject(DrawingObject):
|
|
""" DrawingObject subclass that represents a poly-line or polygon
|
|
"""
|
|
def __init__(self, points=[], *varg, **kwarg):
|
|
DrawingObject.__init__(self, *varg, **kwarg)
|
|
self.points = list(points)
|
|
|
|
# =======================
|
|
# == Selection Methods ==
|
|
# =======================
|
|
|
|
def addPoint(self, x,y):
|
|
self.points.append((x-self.position.x,y-self.position.y))
|
|
self._updateBoundingBox()
|
|
|
|
def getPoint(self, idx):
|
|
x,y = self.points[idx]
|
|
return (x+self.position.x,y+self.position.y)
|
|
|
|
def movePoint(self, idx, x,y):
|
|
self.points[idx] = (x-self.position.x,y-self.position.y)
|
|
self._updateBoundingBox()
|
|
|
|
def popPoint(self, idx=-1):
|
|
self.points.pop(idx)
|
|
self._updateBoundingBox()
|
|
|
|
|
|
# =============================
|
|
# == Object Property Methods ==
|
|
# =============================
|
|
|
|
def getData(self):
|
|
""" Return a copy of the object's internal data.
|
|
|
|
This is used to save this DrawingObject to disk.
|
|
"""
|
|
# get the basics
|
|
data = DrawingObject.getData(self)
|
|
# add our specifics
|
|
data += [list(self.points)]
|
|
|
|
return data
|
|
|
|
|
|
def setData(self, data):
|
|
""" Set the object's internal data.
|
|
|
|
'data' is a copy of the object's saved data, as returned by
|
|
getData() above. This is used to restore a previously saved
|
|
DrawingObject.
|
|
"""
|
|
#data = copy.deepcopy(data) # Needed?
|
|
d = DrawingObject.setData(self, data)
|
|
|
|
try:
|
|
self.points = d.next()
|
|
except StopIteration:
|
|
raise ValueError('Not enough data in setData call')
|
|
|
|
return d
|
|
|
|
|
|
# =====================
|
|
# == Private Methods ==
|
|
# =====================
|
|
def _privateDraw(self, dc, position, selected):
|
|
""" Private routine to draw this DrawingObject.
|
|
|
|
'dc' is the device context to use for drawing, while 'position' is
|
|
the position in which to draw the object. If 'selected' is True,
|
|
the object is drawn with selection handles. This private drawing
|
|
routine assumes that the pen and brush have already been set by the
|
|
caller.
|
|
"""
|
|
dc.SetBrush(wx.TRANSPARENT_BRUSH)
|
|
dc.DrawLines(self.points, position.x, position.y)
|
|
|
|
def _updateBoundingBox(self):
|
|
x = min([p[0] for p in self.points])
|
|
y = min([p[1] for p in self.points])
|
|
x2 = max([p[0] for p in self.points])
|
|
y2 = max([p[1] for p in self.points])
|
|
dx = -x
|
|
dy = -y
|
|
self.position = wx.Point(self.position.x + x,self.position.y + y)
|
|
self.size = wx.Size(x2-x, y2-y)
|
|
#self.position.x += x
|
|
#self.position.y += y
|
|
#self.size.width = x2-x
|
|
#self.size.height = y2-y
|
|
# update coords also because they're relative to self.position
|
|
for i,p in enumerate(self.points):
|
|
self.points[i] = (p[0]+dx,p[1]+dy)
|
|
|
|
#----------------------------------------------------------------------------
|
|
class RectDrawingObject(DrawingObject):
|
|
""" DrawingObject subclass that represents an axis-aligned rectangle.
|
|
"""
|
|
def __init__(self, *varg, **kwarg):
|
|
DrawingObject.__init__(self, *varg, **kwarg)
|
|
|
|
def objectContainsPoint(self, x, y):
|
|
""" Returns True iff this object contains the given point.
|
|
|
|
This is used to determine if the user clicked on the object.
|
|
"""
|
|
# Firstly, ignore any points outside of the object's bounds.
|
|
|
|
if x < self.position.x: return False
|
|
if x > self.position.x + self.size.x: return False
|
|
if y < self.position.y: return False
|
|
if y > self.position.y + self.size.y: return False
|
|
|
|
# Rectangles are easy -- they're always selected if the
|
|
# point is within their bounds.
|
|
return True
|
|
|
|
# =====================
|
|
# == Private Methods ==
|
|
# =====================
|
|
|
|
def _privateDraw(self, dc, position, selected):
|
|
""" Private routine to draw this DrawingObject.
|
|
|
|
'dc' is the device context to use for drawing, while 'position' is
|
|
the position in which to draw the object. If 'selected' is True,
|
|
the object is drawn with selection handles. This private drawing
|
|
routine assumes that the pen and brush have already been set by the
|
|
caller.
|
|
"""
|
|
dc.DrawRectangle(position.x, position.y,
|
|
self.size.width, self.size.height)
|
|
|
|
|
|
#----------------------------------------------------------------------------
|
|
class EllipseDrawingObject(DrawingObject):
|
|
""" DrawingObject subclass that represents an axis-aligned ellipse.
|
|
"""
|
|
def __init__(self, *varg, **kwarg):
|
|
DrawingObject.__init__(self, *varg, **kwarg)
|
|
|
|
# =====================
|
|
# == Private Methods ==
|
|
# =====================
|
|
def _privateDraw(self, dc, position, selected):
|
|
""" Private routine to draw this DrawingObject.
|
|
|
|
'dc' is the device context to use for drawing, while 'position' is
|
|
the position in which to draw the object. If 'selected' is True,
|
|
the object is drawn with selection handles. This private drawing
|
|
routine assumes that the pen and brush have already been set by the
|
|
caller.
|
|
"""
|
|
dc.DrawEllipse(position.x, position.y,
|
|
self.size.width, self.size.height)
|
|
|
|
|
|
|
|
|
|
#----------------------------------------------------------------------------
|
|
class TextDrawingObject(DrawingObject):
|
|
""" DrawingObject subclass that holds text.
|
|
|
|
Adds the following members to the base DrawingObject:
|
|
'text' The object's text (obj_TEXT objects only).
|
|
'textFont' The text object's font name.
|
|
"""
|
|
|
|
def __init__(self, text=None, *varg, **kwarg):
|
|
DrawingObject.__init__(self, *varg, **kwarg)
|
|
|
|
self.text = text
|
|
self.textFont = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT)
|
|
|
|
|
|
# =============================
|
|
# == Object Property Methods ==
|
|
# =============================
|
|
|
|
def getData(self):
|
|
""" Return a copy of the object's internal data.
|
|
|
|
This is used to save this DrawingObject to disk.
|
|
"""
|
|
# get the basics
|
|
data = DrawingObject.getData(self)
|
|
# add our specifics
|
|
data += [self.text, self.textFont.GetNativeFontInfoDesc()]
|
|
|
|
return data
|
|
|
|
|
|
def setData(self, data):
|
|
""" Set the object's internal data.
|
|
|
|
'data' is a copy of the object's saved data, as returned by
|
|
getData() above. This is used to restore a previously saved
|
|
DrawingObject.
|
|
"""
|
|
d = DrawingObject.setData(self, data)
|
|
|
|
try:
|
|
self.text = d.next()
|
|
desc = d.next()
|
|
self.textFont = wx.FontFromNativeInfoString(desc)
|
|
except StopIteration:
|
|
raise ValueError('Not enough data in setData call')
|
|
|
|
return d
|
|
|
|
|
|
def hasPropertyEditor(self):
|
|
return True
|
|
|
|
def doPropertyEdit(self, parent):
|
|
editor = parent.getTextEditor()
|
|
editor.SetTitle("Edit Text Object")
|
|
editor.objectToDialog(self)
|
|
if editor.ShowModal() == wx.ID_CANCEL:
|
|
editor.Hide()
|
|
return False
|
|
|
|
parent.saveUndoInfo()
|
|
|
|
editor.dialogToObject(self)
|
|
editor.Hide()
|
|
|
|
return True
|
|
|
|
|
|
def setText(self, text):
|
|
""" Set the text for this DrawingObject.
|
|
"""
|
|
self.text = text
|
|
|
|
|
|
def getText(self):
|
|
""" Return this DrawingObject's text.
|
|
"""
|
|
return self.text
|
|
|
|
|
|
def setFont(self, font):
|
|
""" Set the font for this text DrawingObject.
|
|
"""
|
|
self.textFont = font
|
|
|
|
|
|
def getFont(self):
|
|
""" Return this text DrawingObject's font.
|
|
"""
|
|
return self.textFont
|
|
|
|
|
|
|
|
# ============================
|
|
# == Object Drawing Methods ==
|
|
# ============================
|
|
|
|
def draw(self, dc, selected):
|
|
""" Draw this DrawingObject into our window.
|
|
|
|
'dc' is the device context to use for drawing. If 'selected' is
|
|
True, the object is currently selected and should be drawn as such.
|
|
"""
|
|
dc.SetTextForeground(self.penColour)
|
|
dc.SetTextBackground(self.fillColour)
|
|
|
|
self._privateDraw(dc, self.position, selected)
|
|
|
|
def objectContainsPoint(self, x, y):
|
|
""" Returns True iff this object contains the given point.
|
|
|
|
This is used to determine if the user clicked on the object.
|
|
"""
|
|
# Firstly, ignore any points outside of the object's bounds.
|
|
|
|
if x < self.position.x: return False
|
|
if x > self.position.x + self.size.x: return False
|
|
if y < self.position.y: return False
|
|
if y > self.position.y + self.size.y: return False
|
|
|
|
# Text is easy -- it's always selected if the
|
|
# point is within its bounds.
|
|
return True
|
|
|
|
|
|
def fitToText(self):
|
|
""" Resize a text DrawingObject so that it fits it's text exactly.
|
|
"""
|
|
|
|
dummyWindow = wx.Frame(None, -1, "")
|
|
dummyWindow.SetFont(self.textFont)
|
|
width, height = dummyWindow.GetTextExtent(self.text)
|
|
dummyWindow.Destroy()
|
|
|
|
self.size = wx.Size(width, height)
|
|
|
|
# =====================
|
|
# == Private Methods ==
|
|
# =====================
|
|
|
|
def _privateDraw(self, dc, position, selected):
|
|
""" Private routine to draw this DrawingObject.
|
|
|
|
'dc' is the device context to use for drawing, while 'position' is
|
|
the position in which to draw the object. If 'selected' is True,
|
|
the object is drawn with selection handles. This private drawing
|
|
routine assumes that the pen and brush have already been set by the
|
|
caller.
|
|
"""
|
|
dc.SetFont(self.textFont)
|
|
dc.DrawText(self.text, position.x, position.y)
|
|
|
|
|
|
|
|
#----------------------------------------------------------------------------
|
|
class ToolPaletteToggleX(wx.ToggleButton):
|
|
""" An icon appearing in the tool palette area of our sketching window.
|
|
|
|
Note that this is actually implemented as a wx.Bitmap rather
|
|
than as a wx.Icon. wx.Icon has a very specific meaning, and isn't
|
|
appropriate for this more general use.
|
|
"""
|
|
|
|
def __init__(self, parent, iconID, iconName, toolTip, mode = wx.ITEM_NORMAL):
|
|
""" Standard constructor.
|
|
|
|
'parent' is the parent window this icon will be part of.
|
|
'iconID' is the internal ID used for this icon.
|
|
'iconName' is the name used for this icon.
|
|
'toolTip' is the tool tip text to show for this icon.
|
|
'mode' is one of wx.ITEM_NORMAL, wx.ITEM_CHECK, wx.ITEM_RADIO
|
|
|
|
The icon name is used to get the appropriate bitmap for this icon.
|
|
"""
|
|
bmp = wx.Bitmap("images/" + iconName + "Icon.bmp", wx.BITMAP_TYPE_BMP)
|
|
bmpsel = wx.Bitmap("images/" + iconName + "IconSel.bmp", wx.BITMAP_TYPE_BMP)
|
|
|
|
wx.ToggleButton.__init__(self, parent, iconID,
|
|
size=(bmp.GetWidth()+1, bmp.GetHeight()+1)
|
|
)
|
|
self.SetLabel( iconName )
|
|
self.SetToolTip(wx.ToolTip(toolTip))
|
|
#self.SetBitmapLabel(bmp)
|
|
#self.SetBitmapSelected(bmpsel)
|
|
|
|
self.iconID = iconID
|
|
self.iconName = iconName
|
|
|
|
class ToolPaletteToggle(GenBitmapToggleButton):
|
|
""" An icon appearing in the tool palette area of our sketching window.
|
|
|
|
Note that this is actually implemented as a wx.Bitmap rather
|
|
than as a wx.Icon. wx.Icon has a very specific meaning, and isn't
|
|
appropriate for this more general use.
|
|
"""
|
|
|
|
def __init__(self, parent, iconID, iconName, toolTip, mode = wx.ITEM_NORMAL):
|
|
""" Standard constructor.
|
|
|
|
'parent' is the parent window this icon will be part of.
|
|
'iconID' is the internal ID used for this icon.
|
|
'iconName' is the name used for this icon.
|
|
'toolTip' is the tool tip text to show for this icon.
|
|
'mode' is one of wx.ITEM_NORMAL, wx.ITEM_CHECK, wx.ITEM_RADIO
|
|
|
|
The icon name is used to get the appropriate bitmap for this icon.
|
|
"""
|
|
bmp = wx.Bitmap("images/" + iconName + "Icon.bmp", wx.BITMAP_TYPE_BMP)
|
|
bmpsel = wx.Bitmap("images/" + iconName + "IconSel.bmp", wx.BITMAP_TYPE_BMP)
|
|
|
|
GenBitmapToggleButton.__init__(self, parent, iconID, bitmap=bmp,
|
|
size=(bmp.GetWidth()+1, bmp.GetHeight()+1),
|
|
style=wx.BORDER_NONE)
|
|
|
|
self.SetToolTip(wx.ToolTip(toolTip))
|
|
self.SetBitmapLabel(bmp)
|
|
self.SetBitmapSelected(bmpsel)
|
|
|
|
self.iconID = iconID
|
|
self.iconName = iconName
|
|
|
|
|
|
class ToolPaletteButton(GenBitmapButton):
|
|
""" An icon appearing in the tool palette area of our sketching window.
|
|
|
|
Note that this is actually implemented as a wx.Bitmap rather
|
|
than as a wx.Icon. wx.Icon has a very specific meaning, and isn't
|
|
appropriate for this more general use.
|
|
"""
|
|
|
|
def __init__(self, parent, iconID, iconName, toolTip):
|
|
""" Standard constructor.
|
|
|
|
'parent' is the parent window this icon will be part of.
|
|
'iconID' is the internal ID used for this icon.
|
|
'iconName' is the name used for this icon.
|
|
'toolTip' is the tool tip text to show for this icon.
|
|
|
|
The icon name is used to get the appropriate bitmap for this icon.
|
|
"""
|
|
bmp = wx.Bitmap("images/" + iconName + "Icon.bmp", wx.BITMAP_TYPE_BMP)
|
|
GenBitmapButton.__init__(self, parent, iconID, bitmap=bmp,
|
|
size=(bmp.GetWidth()+1, bmp.GetHeight()+1),
|
|
style=wx.BORDER_NONE)
|
|
self.SetToolTip(wx.ToolTip(toolTip))
|
|
self.SetBitmapLabel(bmp)
|
|
|
|
self.iconID = iconID
|
|
self.iconName = iconName
|
|
|
|
|
|
|
|
#----------------------------------------------------------------------------
|
|
|
|
class ToolOptionIndicator(wx.Window):
|
|
""" A visual indicator which shows the current tool options.
|
|
"""
|
|
def __init__(self, parent):
|
|
""" Standard constructor.
|
|
"""
|
|
wx.Window.__init__(self, parent, -1, wx.DefaultPosition, wx.Size(52, 32))
|
|
|
|
self.penColour = wx.BLACK
|
|
self.fillColour = wx.WHITE
|
|
self.lineSize = 1
|
|
|
|
# Win95 can only handle a 8x8 stipple bitmaps max
|
|
#self.stippleBitmap = wx.BitmapFromBits("\xf0"*4 + "\x0f"*4,8,8)
|
|
# ...but who uses Win95?
|
|
self.stippleBitmap = wx.BitmapFromBits("\xff\x00"*8+"\x00\xff"*8,16,16)
|
|
self.stippleBitmap.SetMask(wx.Mask(self.stippleBitmap))
|
|
|
|
self.Bind(wx.EVT_PAINT, self.onPaint)
|
|
|
|
|
|
def setPenColour(self, penColour):
|
|
""" Set the indicator's current pen colour.
|
|
"""
|
|
self.penColour = penColour
|
|
self.Refresh()
|
|
|
|
|
|
def setFillColour(self, fillColour):
|
|
""" Set the indicator's current fill colour.
|
|
"""
|
|
self.fillColour = fillColour
|
|
self.Refresh()
|
|
|
|
|
|
def setLineSize(self, lineSize):
|
|
""" Set the indicator's current pen colour.
|
|
"""
|
|
self.lineSize = lineSize
|
|
self.Refresh()
|
|
|
|
|
|
def onPaint(self, event):
|
|
""" Paint our tool option indicator.
|
|
"""
|
|
dc = wx.PaintDC(self)
|
|
dc.BeginDrawing()
|
|
|
|
dc.SetPen(wx.BLACK_PEN)
|
|
bgbrush = wx.Brush(wx.WHITE, wx.STIPPLE_MASK_OPAQUE)
|
|
bgbrush.SetStipple(self.stippleBitmap)
|
|
dc.SetTextForeground(wx.LIGHT_GREY)
|
|
dc.SetTextBackground(wx.WHITE)
|
|
dc.SetBrush(bgbrush)
|
|
dc.DrawRectangle(0, 0, self.GetSize().width,self.GetSize().height)
|
|
|
|
if self.lineSize == 0:
|
|
dc.SetPen(wx.Pen(self.penColour, self.lineSize, wx.TRANSPARENT))
|
|
else:
|
|
dc.SetPen(wx.Pen(self.penColour, self.lineSize, wx.SOLID))
|
|
dc.SetBrush(wx.Brush(self.fillColour, wx.SOLID))
|
|
|
|
size = self.GetSize()
|
|
ctrx = size.x/2
|
|
ctry = size.y/2
|
|
radius = min(size)//2 - 5
|
|
dc.DrawCircle(ctrx, ctry, radius)
|
|
|
|
dc.EndDrawing()
|
|
|
|
#----------------------------------------------------------------------------
|
|
|
|
class EditTextObjectDialog(wx.Dialog):
|
|
""" Dialog box used to edit the properties of a text object.
|
|
|
|
The user can edit the object's text, font, size, and text style.
|
|
"""
|
|
|
|
def __init__(self, parent, title):
|
|
""" Standard constructor.
|
|
"""
|
|
wx.Dialog.__init__(self, parent, -1, title,
|
|
style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
|
|
|
|
self.textCtrl = wx.TextCtrl(
|
|
self, 1001, "Enter text here", style=wx.TE_PROCESS_ENTER|wx.TE_RICH,
|
|
validator=TextObjectValidator()
|
|
)
|
|
extent = self.textCtrl.GetFullTextExtent("Hy")
|
|
lineHeight = extent[1] + extent[3]
|
|
self.textCtrl.SetSize(wx.Size(-1, lineHeight * 4))
|
|
self.curFont = self.textCtrl.GetFont()
|
|
self.curClr = wx.BLACK
|
|
|
|
self.Bind(wx.EVT_TEXT_ENTER, self._doEnter, id=1001)
|
|
|
|
fontBtn = wx.Button(self, -1, "Select Font...")
|
|
self.Bind(wx.EVT_BUTTON, self.OnSelectFont, fontBtn)
|
|
|
|
gap = wx.LEFT | wx.TOP | wx.RIGHT
|
|
|
|
self.okButton = wx.Button(self, wx.ID_OK, "&OK")
|
|
self.okButton.SetDefault()
|
|
self.cancelButton = wx.Button(self, wx.ID_CANCEL, "&Cancel")
|
|
|
|
btnSizer = wx.StdDialogButtonSizer()
|
|
|
|
btnSizer.Add(self.okButton, 0, gap, 5)
|
|
btnSizer.Add(self.cancelButton, 0, gap, 5)
|
|
|
|
sizer = wx.BoxSizer(wx.VERTICAL)
|
|
sizer.Add(self.textCtrl, 1, gap | wx.EXPAND, 5)
|
|
sizer.Add(fontBtn, 0, gap | wx.ALIGN_RIGHT, 5)
|
|
sizer.Add((10, 10)) # Spacer.
|
|
btnSizer.Realize()
|
|
sizer.Add(btnSizer, 0, gap | wx.ALIGN_CENTRE, 5)
|
|
|
|
self.SetAutoLayout(True)
|
|
self.SetSizer(sizer)
|
|
sizer.Fit(self)
|
|
|
|
self.textCtrl.SetFocus()
|
|
|
|
|
|
def OnSelectFont(self, evt):
|
|
"""Shows the font dialog and sets the font of the sample text"""
|
|
data = wx.FontData()
|
|
data.EnableEffects(True)
|
|
data.SetColour(self.curClr) # set colour
|
|
data.SetInitialFont(self.curFont)
|
|
|
|
dlg = wx.FontDialog(self, data)
|
|
|
|
if dlg.ShowModal() == wx.ID_OK:
|
|
data = dlg.GetFontData()
|
|
font = data.GetChosenFont()
|
|
colour = data.GetColour()
|
|
|
|
self.curFont = font
|
|
self.curClr = colour
|
|
|
|
self.textCtrl.SetFont(font)
|
|
# Update dialog for the new height of the text
|
|
self.GetSizer().Fit(self)
|
|
|
|
dlg.Destroy()
|
|
|
|
|
|
def objectToDialog(self, obj):
|
|
""" Copy the properties of the given text object into the dialog box.
|
|
"""
|
|
self.textCtrl.SetValue(obj.getText())
|
|
self.textCtrl.SetSelection(0, len(obj.getText()))
|
|
|
|
self.curFont = obj.getFont()
|
|
self.textCtrl.SetFont(self.curFont)
|
|
|
|
|
|
|
|
def dialogToObject(self, obj):
|
|
""" Copy the properties from the dialog box into the given text object.
|
|
"""
|
|
obj.setText(self.textCtrl.GetValue())
|
|
obj.setFont(self.curFont)
|
|
obj.fitToText()
|
|
|
|
# ======================
|
|
# == Private Routines ==
|
|
# ======================
|
|
|
|
def _doEnter(self, event):
|
|
""" Respond to the user hitting the ENTER key.
|
|
|
|
We simulate clicking on the "OK" button.
|
|
"""
|
|
if self.Validate(): self.Show(False)
|
|
|
|
#----------------------------------------------------------------------------
|
|
|
|
class TextObjectValidator(wx.PyValidator):
|
|
""" This validator is used to ensure that the user has entered something
|
|
into the text object editor dialog's text field.
|
|
"""
|
|
def __init__(self):
|
|
""" Standard constructor.
|
|
"""
|
|
wx.PyValidator.__init__(self)
|
|
|
|
|
|
def Clone(self):
|
|
""" Standard cloner.
|
|
|
|
Note that every validator must implement the Clone() method.
|
|
"""
|
|
return TextObjectValidator()
|
|
|
|
|
|
def Validate(self, win):
|
|
""" Validate the contents of the given text control.
|
|
"""
|
|
textCtrl = self.GetWindow()
|
|
text = textCtrl.GetValue()
|
|
|
|
if len(text) == 0:
|
|
wx.MessageBox("A text object must contain some text!", "Error")
|
|
return False
|
|
else:
|
|
return True
|
|
|
|
|
|
def TransferToWindow(self):
|
|
""" Transfer data from validator to window.
|
|
|
|
The default implementation returns False, indicating that an error
|
|
occurred. We simply return True, as we don't do any data transfer.
|
|
"""
|
|
return True # Prevent wx.Dialog from complaining.
|
|
|
|
|
|
def TransferFromWindow(self):
|
|
""" Transfer data from window to validator.
|
|
|
|
The default implementation returns False, indicating that an error
|
|
occurred. We simply return True, as we don't do any data transfer.
|
|
"""
|
|
return True # Prevent wx.Dialog from complaining.
|
|
|
|
#----------------------------------------------------------------------------
|
|
|
|
class ExceptionHandler:
|
|
""" A simple error-handling class to write exceptions to a text file.
|
|
|
|
Under MS Windows, the standard DOS console window doesn't scroll and
|
|
closes as soon as the application exits, making it hard to find and
|
|
view Python exceptions. This utility class allows you to handle Python
|
|
exceptions in a more friendly manner.
|
|
"""
|
|
|
|
def __init__(self):
|
|
""" Standard constructor.
|
|
"""
|
|
self._buff = ""
|
|
if os.path.exists("errors.txt"):
|
|
os.remove("errors.txt") # Delete previous error log, if any.
|
|
|
|
|
|
def write(self, s):
|
|
""" Write the given error message to a text file.
|
|
|
|
Note that if the error message doesn't end in a carriage return, we
|
|
have to buffer up the inputs until a carriage return is received.
|
|
"""
|
|
if (s[-1] != "\n") and (s[-1] != "\r"):
|
|
self._buff = self._buff + s
|
|
return
|
|
|
|
try:
|
|
s = self._buff + s
|
|
self._buff = ""
|
|
|
|
f = open("errors.txt", "a")
|
|
f.write(s)
|
|
f.close()
|
|
|
|
if s[:9] == "Traceback":
|
|
# Tell the user than an exception occurred.
|
|
wx.MessageBox("An internal error has occurred.\nPlease " + \
|
|
"refer to the 'errors.txt' file for details.",
|
|
"Error", wx.OK | wx.CENTRE | wx.ICON_EXCLAMATION)
|
|
|
|
|
|
except:
|
|
pass # Don't recursively crash on errors.
|
|
|
|
#----------------------------------------------------------------------------
|
|
|
|
class SketchApp(wx.App):
|
|
""" The main pySketch application object.
|
|
"""
|
|
def OnInit(self):
|
|
""" Initialise the application.
|
|
"""
|
|
global _docList
|
|
_docList = []
|
|
|
|
if len(sys.argv) == 1:
|
|
# No file name was specified on the command line -> start with a
|
|
# blank document.
|
|
frame = DrawingFrame(None, -1, "Untitled")
|
|
frame.Centre()
|
|
frame.Show(True)
|
|
_docList.append(frame)
|
|
else:
|
|
# Load the file(s) specified on the command line.
|
|
for arg in sys.argv[1:]:
|
|
fileName = os.path.join(os.getcwd(), arg)
|
|
if os.path.isfile(fileName):
|
|
frame = DrawingFrame(None, -1,
|
|
os.path.basename(fileName),
|
|
fileName=fileName)
|
|
frame.Show(True)
|
|
_docList.append(frame)
|
|
|
|
return True
|
|
|
|
#----------------------------------------------------------------------------
|
|
|
|
def main():
|
|
""" Start up the pySketch application.
|
|
"""
|
|
global _app
|
|
|
|
# Redirect python exceptions to a log file.
|
|
|
|
sys.stderr = ExceptionHandler()
|
|
|
|
# Create and start the pySketch application.
|
|
|
|
_app = SketchApp(0)
|
|
_app.MainLoop()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
|
|
|