X-Git-Url: https://git.saurik.com/wxWidgets.git/blobdiff_plain/0f5fa1118190e313a22236028f6348eddebf9ab0..fe45b493dcc42fea4ad4f781a4fb7844d2e95fd0:/wxPython/samples/pySketch/pySketch.py?ds=sidebyside diff --git a/wxPython/samples/pySketch/pySketch.py b/wxPython/samples/pySketch/pySketch.py index 57edf205fd..1e5aac2984 100644 --- a/wxPython/samples/pySketch/pySketch.py +++ b/wxPython/samples/pySketch/pySketch.py @@ -5,7 +5,9 @@ This is completely free software; please feel free to adapt or use this in any way you like. - Author: Erik Westra (ewestra@wave.co.nz) + Original Author: Erik Westra (ewestra@wave.co.nz) + + Other contributors: Bill Baxter (wbaxter@gmail.com) ######################################################################### @@ -36,14 +38,20 @@ Known Bugs: * Scrolling the window causes the drawing panel to be mucked up until you - refresh it. I've got no idea why. + 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. + unable to call del on a 'None' object. + + * Saving files via pickling is not a robust cross-platform solution. """ -import string, cPickle, os.path -from wxPython.wx import * +import sys +import cPickle, os.path +import copy +import wx +from wx.lib.buttons import GenBitmapButton,GenBitmapToggleButton + import traceback, types @@ -53,2568 +61,3475 @@ import traceback, types # Our menu item IDs: -menu_UNDO = 10001 # Edit menu items. -menu_SELECT_ALL = 10002 -menu_DUPLICATE = 10003 -menu_EDIT_TEXT = 10004 -menu_DELETE = 10005 +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_SELECT = 10101 # Tools menu items. -menu_LINE = 10102 -menu_RECT = 10103 -menu_ELLIPSE = 10104 -menu_TEXT = 10105 +menu_DC = wx.NewId() # View menu items. +menu_GCDC = wx.NewId() -menu_MOVE_FORWARD = 10201 # Object menu items. -menu_MOVE_TO_FRONT = 10202 -menu_MOVE_BACKWARD = 10203 -menu_MOVE_TO_BACK = 10204 +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 = 10205 # Help menu items. +menu_ABOUT = wx.NewId() # Help menu items. # Our tool IDs: -id_SELECT = 11001 -id_LINE = 11002 -id_RECT = 11003 -id_ELLIPSE = 11004 -id_TEXT = 11005 +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 = 12001 -id_PEN_OPT = 12002 -id_LINE_OPT = 12003 +id_FILL_OPT = wx.NewId() +id_PEN_OPT = wx.NewId() +id_LINE_OPT = wx.NewId() -id_LINESIZE_0 = 13001 -id_LINESIZE_1 = 13002 -id_LINESIZE_2 = 13003 -id_LINESIZE_3 = 13004 -id_LINESIZE_4 = 13005 -id_LINESIZE_5 = 13006 +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() -# DrawObject type IDs: +# Size of the drawing page, in pixels. -obj_LINE = 1 -obj_RECT = 2 -obj_ELLIPSE = 3 -obj_TEXT = 4 +PAGE_WIDTH = 1000 +PAGE_HEIGHT = 1000 -# Selection handle IDs: +#---------------------------------------------------------------------------- -handle_NONE = 1 -handle_TOP_LEFT = 2 -handle_TOP_RIGHT = 3 -handle_BOTTOM_LEFT = 4 -handle_BOTTOM_RIGHT = 5 -handle_START_POINT = 6 -handle_END_POINT = 7 +class DrawingFrame(wx.Frame): + """ A frame showing the contents of a single document. """ -# Dragging operations: + # ========================================== + # == Initialisation and Window Management == + # ========================================== -drag_NONE = 1 -drag_RESIZE = 2 -drag_MOVE = 3 -drag_DRAG = 4 + 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") -# Visual Feedback types: + 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) -feedback_LINE = 1 -feedback_RECT = 2 -feedback_ELLIPSE = 3 + 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) -# Mouse-event action parameter types: + # Make the tool palette icons respond when the user clicks on them. -param_RECT = 1 -param_LINE = 2 + for tool in self.tools.itervalues(): + tool[0].Bind(wx.EVT_BUTTON, self.onChooseTool) -# Size of the drawing page, in pixels. + self.selectIcon.Bind(wx.EVT_BUTTON, self.onChooseTool) + self.lineIcon.Bind(wx.EVT_BUTTON, self.onChooseTool) -PAGE_WIDTH = 1000 -PAGE_HEIGHT = 1000 -#---------------------------------------------------------------------------- + self.penOptIcon.Bind(wx.EVT_BUTTON, self.onPenOptionIconClick) + self.fillOptIcon.Bind(wx.EVT_BUTTON, self.onFillOptionIconClick) + self.lineOptIcon.Bind(wx.EVT_BUTTON, self.onLineOptionIconClick) -class DrawingFrame(wxFrame): - """ A frame showing the contents of a single document. """ + # Setup the main drawing area. - # ========================================== - # == Initialisation and Window Management == - # ========================================== + self.drawPanel = wx.ScrolledWindow(self.topPanel, -1, + style=wx.SUNKEN_BORDER|wx.NO_FULL_REPAINT_ON_RESIZE) + self.drawPanel.SetBackgroundColour(wx.WHITE) - def __init__(self, parent, id, title, fileName=None): - """ Standard constructor. - - 'parent', 'id' and 'title' are all passed to the standard wxFrame - constructor. 'fileName' is the name and path of a saved file to - load into this frame, if any. - """ - wxFrame.__init__(self, parent, id, title, - style = wxDEFAULT_FRAME_STYLE | wxWANTS_CHARS | - wxNO_FULL_REPAINT_ON_RESIZE) - - # Setup our menu bar. - - menuBar = wxMenuBar() - - self.fileMenu = wxMenu() - self.fileMenu.Append(wxID_NEW, "New\tCTRL-N") - self.fileMenu.Append(wxID_OPEN, "Open...\tCTRL-O") - self.fileMenu.Append(wxID_CLOSE, "Close\tCTRL-W") - self.fileMenu.AppendSeparator() - self.fileMenu.Append(wxID_SAVE, "Save\tCTRL-S") - self.fileMenu.Append(wxID_SAVEAS, "Save As...") - self.fileMenu.Append(wxID_REVERT, "Revert...") - self.fileMenu.AppendSeparator() - self.fileMenu.Append(wxID_EXIT, "Quit\tCTRL-Q") - - menuBar.Append(self.fileMenu, "File") - - self.editMenu = wxMenu() - self.editMenu.Append(menu_UNDO, "Undo\tCTRL-Z") - self.editMenu.AppendSeparator() - self.editMenu.Append(menu_SELECT_ALL, "Select All\tCTRL-A") - self.editMenu.AppendSeparator() - self.editMenu.Append(menu_DUPLICATE, "Duplicate\tCTRL-D") - self.editMenu.Append(menu_EDIT_TEXT, "Edit...\tCTRL-E") - self.editMenu.Append(menu_DELETE, "Delete\tDEL") - - menuBar.Append(self.editMenu, "Edit") - - self.toolsMenu = wxMenu() - self.toolsMenu.Append(menu_SELECT, "Selection", kind=wxITEM_CHECK) - self.toolsMenu.Append(menu_LINE, "Line", kind=wxITEM_CHECK) - self.toolsMenu.Append(menu_RECT, "Rectangle", kind=wxITEM_CHECK) - self.toolsMenu.Append(menu_ELLIPSE, "Ellipse", kind=wxITEM_CHECK) - self.toolsMenu.Append(menu_TEXT, "Text", kind=wxITEM_CHECK) - - menuBar.Append(self.toolsMenu, "Tools") - - self.objectMenu = wxMenu() - 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 = wxMenu() - self.helpMenu.Append(menu_ABOUT, "About pySketch...") - - menuBar.Append(self.helpMenu, "Help") - - self.SetMenuBar(menuBar) - - # Create our toolbar. - - self.toolbar = self.CreateToolBar(wxTB_HORIZONTAL | - wxNO_BORDER | wxTB_FLAT) - - self.toolbar.AddSimpleTool(wxID_NEW, - wxBitmap("images/new.bmp", - wxBITMAP_TYPE_BMP), - "New") - self.toolbar.AddSimpleTool(wxID_OPEN, - wxBitmap("images/open.bmp", - wxBITMAP_TYPE_BMP), - "Open") - self.toolbar.AddSimpleTool(wxID_SAVE, - wxBitmap("images/save.bmp", - wxBITMAP_TYPE_BMP), - "Save") - self.toolbar.AddSeparator() - self.toolbar.AddSimpleTool(menu_UNDO, - wxBitmap("images/undo.bmp", - wxBITMAP_TYPE_BMP), - "Undo") - self.toolbar.AddSeparator() - self.toolbar.AddSimpleTool(menu_DUPLICATE, - wxBitmap("images/duplicate.bmp", - wxBITMAP_TYPE_BMP), - "Duplicate") - self.toolbar.AddSeparator() - self.toolbar.AddSimpleTool(menu_MOVE_FORWARD, - wxBitmap("images/moveForward.bmp", - wxBITMAP_TYPE_BMP), - "Move Forward") - self.toolbar.AddSimpleTool(menu_MOVE_BACKWARD, - wxBitmap("images/moveBack.bmp", - wxBITMAP_TYPE_BMP), - "Move Backward") - - self.toolbar.Realize() - - # Associate each menu/toolbar item with the method that handles that - # item. - - EVT_MENU(self, wxID_NEW, self.doNew) - EVT_MENU(self, wxID_OPEN, self.doOpen) - EVT_MENU(self, wxID_CLOSE, self.doClose) - EVT_MENU(self, wxID_SAVE, self.doSave) - EVT_MENU(self, wxID_SAVEAS, self.doSaveAs) - EVT_MENU(self, wxID_REVERT, self.doRevert) - EVT_MENU(self, wxID_EXIT, self.doExit) - - EVT_MENU(self, menu_UNDO, self.doUndo) - EVT_MENU(self, menu_SELECT_ALL, self.doSelectAll) - EVT_MENU(self, menu_DUPLICATE, self.doDuplicate) - EVT_MENU(self, menu_EDIT_TEXT, self.doEditText) - EVT_MENU(self, menu_DELETE, self.doDelete) - - EVT_MENU(self, menu_SELECT, self.doChooseSelectTool) - EVT_MENU(self, menu_LINE, self.doChooseLineTool) - EVT_MENU(self, menu_RECT, self.doChooseRectTool) - EVT_MENU(self, menu_ELLIPSE, self.doChooseEllipseTool) - EVT_MENU(self, menu_TEXT, self.doChooseTextTool) - - EVT_MENU(self, menu_MOVE_FORWARD, self.doMoveForward) - EVT_MENU(self, menu_MOVE_TO_FRONT, self.doMoveToFront) - EVT_MENU(self, menu_MOVE_BACKWARD, self.doMoveBackward) - EVT_MENU(self, menu_MOVE_TO_BACK, self.doMoveToBack) - - EVT_MENU(self, menu_ABOUT, self.doShowAbout) - - # 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. - - EVT_CLOSE(self, 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. - - EVT_CHAR_HOOK(self, self.onKeyEvent) - - # Setup our top-most panel. This holds the entire contents of the - # window, excluding the menu bar. - - self.topPanel = wxPanel(self, -1, style=wxSIMPLE_BORDER) - - # Setup our tool palette, with all our drawing tools and option icons. - - self.toolPalette = wxBoxSizer(wxVERTICAL) - - self.selectIcon = ToolPaletteIcon(self.topPanel, id_SELECT, - "select", "Selection Tool") - self.lineIcon = ToolPaletteIcon(self.topPanel, id_LINE, - "line", "Line Tool") - self.rectIcon = ToolPaletteIcon(self.topPanel, id_RECT, - "rect", "Rectangle Tool") - self.ellipseIcon = ToolPaletteIcon(self.topPanel, id_ELLIPSE, - "ellipse", "Ellipse Tool") - self.textIcon = ToolPaletteIcon(self.topPanel, id_TEXT, - "text", "Text Tool") - - toolSizer = wxGridSizer(0, 2, 5, 5) - toolSizer.Add(self.selectIcon) - toolSizer.Add(0, 0) # Gap to make tool icons line up nicely. - toolSizer.Add(self.lineIcon) - toolSizer.Add(self.rectIcon) - toolSizer.Add(self.ellipseIcon) - toolSizer.Add(self.textIcon) - - self.optionIndicator = ToolOptionIndicator(self.topPanel) - self.optionIndicator.SetToolTip( - wxToolTip("Shows Current Pen/Fill/Line Size Settings")) - - optionSizer = wxBoxSizer(wxHORIZONTAL) - - self.penOptIcon = ToolPaletteIcon(self.topPanel, id_PEN_OPT, - "penOpt", "Set Pen Colour") - self.fillOptIcon = ToolPaletteIcon(self.topPanel, id_FILL_OPT, - "fillOpt", "Set Fill Colour") - self.lineOptIcon = ToolPaletteIcon(self.topPanel, id_LINE_OPT, - "lineOpt", "Set Line Size") - - margin = wxLEFT | wxRIGHT - optionSizer.Add(self.penOptIcon, 0, margin, 1) - optionSizer.Add(self.fillOptIcon, 0, margin, 1) - optionSizer.Add(self.lineOptIcon, 0, margin, 1) - - margin = wxTOP | wxLEFT | wxRIGHT | wxALIGN_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) + 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) - # Make the tool palette icons respond when the user clicks on them. - EVT_LEFT_DOWN(self.selectIcon, self.onToolIconClick) - EVT_LEFT_DOWN(self.lineIcon, self.onToolIconClick) - EVT_LEFT_DOWN(self.rectIcon, self.onToolIconClick) - EVT_LEFT_DOWN(self.ellipseIcon, self.onToolIconClick) - EVT_LEFT_DOWN(self.textIcon, self.onToolIconClick) - EVT_LEFT_DOWN(self.penOptIcon, self.onPenOptionIconClick) - EVT_LEFT_DOWN(self.fillOptIcon, self.onFillOptionIconClick) - EVT_LEFT_DOWN(self.lineOptIcon, self.onLineOptionIconClick) + # Position everything in the window. - # Setup the main drawing area. + topSizer = wx.BoxSizer(wx.HORIZONTAL) + topSizer.Add(self.toolPalette, 0) + topSizer.Add(self.drawPanel, 1, wx.EXPAND) - self.drawPanel = wxScrolledWindow(self.topPanel, -1, - style=wxSUNKEN_BORDER) - self.drawPanel.SetBackgroundColour(wxWHITE) + self.topPanel.SetAutoLayout(True) + self.topPanel.SetSizer(topSizer) - self.drawPanel.EnableScrolling(true, true) - self.drawPanel.SetScrollbars(20, 20, PAGE_WIDTH / 20, PAGE_HEIGHT / 20) + self.SetSizeHints(250, 200) + self.SetSize(wx.Size(600, 400)) - EVT_LEFT_DOWN(self.drawPanel, self.onMouseEvent) - EVT_LEFT_DCLICK(self.drawPanel, self.onDoubleClickEvent) - EVT_RIGHT_DOWN(self.drawPanel, self.onRightClick) - EVT_MOTION(self.drawPanel, self.onMouseEvent) - EVT_LEFT_UP(self.drawPanel, self.onMouseEvent) - EVT_PAINT(self.drawPanel, self.onPaintEvent) + # Select an initial tool. - # Position everything in the window. + self.curToolName = None + self.curToolIcon = None + self.curTool = None + self.setCurrentTool("select") - topSizer = wxBoxSizer(wxHORIZONTAL) - topSizer.Add(self.toolPalette, 0) - topSizer.Add(self.drawPanel, 1, wxEXPAND) + # Set initial dc mode to fast + self.wrapDC = lambda dc: dc - self.topPanel.SetAutoLayout(true) - self.topPanel.SetSizer(topSizer) + # Setup our frame to hold the contents of a sketch document. - self.SetSizeHints(minW=250, minH=200) - self.SetSize(wxSize(600, 400)) + 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. - # Select an initial tool. + if self.fileName != None: + self.loadContents() - self.curTool = None - self._setCurrentTool(self.selectIcon) + self._initBuffer() - # Setup our frame to hold the contents of a sketch document. + self._adjustMenus() - self.dirty = false - self.fileName = fileName - self.contents = [] # front-to-back ordered list of DrawingObjects. - self.selection = [] # List of selected DrawingObjects. - self.undoInfo = None # Saved contents for undo. - self.dragMode = drag_NONE # Current mouse-drag mode. + # 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) - if self.fileName != None: - self.loadContents() - - self._adjustMenus() - - # Finally, set our initial pen, fill and line options. - - self.penColour = wxBLACK - self.fillColour = wxWHITE - self.lineSize = 1 # ============================ # == Event Handling Methods == # ============================ - def onToolIconClick(self, event): - """ Respond to the user clicking on one of our tool icons. - """ - iconID = wxPyTypeCast(event.GetEventObject(), "wxWindow").GetId() - if iconID == id_SELECT: self.doChooseSelectTool() - elif iconID == id_LINE: self.doChooseLineTool() - elif iconID == id_RECT: self.doChooseRectTool() - elif iconID == id_ELLIPSE: self.doChooseEllipseTool() - elif iconID == id_TEXT: self.doChooseTextTool() - else: wxBell() - def onPenOptionIconClick(self, event): - """ Respond to the user clicking on the "Pen Options" icon. - """ - data = wxColourData() - if len(self.selection) == 1: - data.SetColour(self.selection[0].getPenColour()) - else: - data.SetColour(self.penColour) - - dialog = wxColourDialog(self, data) - if dialog.ShowModal() == wxID_OK: - c = dialog.GetColourData().GetColour() - self._setPenColour(wxColour(c.Red(), c.Green(), c.Blue())) + """ 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 = wxColourData() - if len(self.selection) == 1: - data.SetColour(self.selection[0].getFillColour()) - else: - data.SetColour(self.fillColour) - - dialog = wxColourDialog(self, data) - if dialog.ShowModal() == wxID_OK: - c = dialog.GetColourData().GetColour() - self._setFillColour(wxColour(c.Red(), c.Green(), c.Blue())) + """ 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 + """ 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. - """ - if event.GetKeyCode() == WXK_UP: - self._moveObject(0, -1) - elif event.GetKeyCode() == WXK_DOWN: - self._moveObject(0, 1) - elif event.GetKeyCode() == WXK_LEFT: - self._moveObject(-1, 0) - elif event.GetKeyCode() == WXK_RIGHT: - self._moveObject(1, 0) - else: - event.Skip() + """ 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 the user clicking on our main drawing panel. - - How we respond depends on the currently selected tool. - """ - if not (event.LeftDown() or event.Dragging() or event.LeftUp()): - return # Ignore mouse movement without click/drag. - - if self.curTool == self.selectIcon: - feedbackType = feedback_RECT - action = self.selectByRectangle - actionParam = param_RECT - selecting = true - dashedLine = true - elif self.curTool == self.lineIcon: - feedbackType = feedback_LINE - action = self.createLine - actionParam = param_LINE - selecting = false - dashedLine = false - elif self.curTool == self.rectIcon: - feedbackType = feedback_RECT - action = self.createRect - actionParam = param_RECT - selecting = false - dashedLine = false - elif self.curTool == self.ellipseIcon: - feedbackType = feedback_ELLIPSE - action = self.createEllipse - actionParam = param_RECT - selecting = false - dashedLine = false - elif self.curTool == self.textIcon: - feedbackType = feedback_RECT - action = self.createText - actionParam = param_RECT - selecting = false - dashedLine = true - else: - wxBell() - return - - if event.LeftDown(): - mousePt = self._getEventCoordinates(event) - if selecting: - obj, handle = self._getObjectAndSelectionHandleAt(mousePt) - - if selecting and (obj != None) and (handle != handle_NONE): - - # The user clicked on an object's selection handle. Let the - # user resize the clicked-on object. - - self.dragMode = drag_RESIZE - self.resizeObject = obj - - if obj.getType() == obj_LINE: - self.resizeFeedback = feedback_LINE - pos = obj.getPosition() - startPt = wxPoint(pos.x + obj.getStartPt().x, - pos.y + obj.getStartPt().y) - endPt = wxPoint(pos.x + obj.getEndPt().x, - pos.y + obj.getEndPt().y) - if handle == handle_START_POINT: - self.resizeAnchor = endPt - self.resizeFloater = startPt - else: - self.resizeAnchor = startPt - self.resizeFloater = endPt - else: - self.resizeFeedback = feedback_RECT - pos = obj.getPosition() - size = obj.getSize() - topLeft = wxPoint(pos.x, pos.y) - topRight = wxPoint(pos.x + size.width, pos.y) - botLeft = wxPoint(pos.x, pos.y + size.height) - botRight = wxPoint(pos.x + size.width, pos.y + size.height) - - if handle == handle_TOP_LEFT: - self.resizeAnchor = botRight - self.resizeFloater = topLeft - elif handle == handle_TOP_RIGHT: - self.resizeAnchor = botLeft - self.resizeFloater = topRight - elif handle == handle_BOTTOM_LEFT: - self.resizeAnchor = topRight - self.resizeFloater = botLeft - elif handle == handle_BOTTOM_RIGHT: - self.resizeAnchor = topLeft - self.resizeFloater = botRight - - self.curPt = mousePt - self.resizeOffsetX = self.resizeFloater.x - mousePt.x - self.resizeOffsetY = self.resizeFloater.y - mousePt.y - endPt = wxPoint(self.curPt.x + self.resizeOffsetX, - self.curPt.y + self.resizeOffsetY) - self._drawVisualFeedback(self.resizeAnchor, endPt, - self.resizeFeedback, false) - - elif selecting and (self._getObjectAt(mousePt) != None): - - # The user clicked on an object to select it. If the user - # drags, he/she will move the object. - - self.select(self._getObjectAt(mousePt)) - self.dragMode = drag_MOVE - self.moveOrigin = mousePt - self.curPt = mousePt - self._drawObjectOutline(0, 0) - - else: - - # The user is dragging out a selection rect or new object. - - self.dragOrigin = mousePt - self.curPt = mousePt - self.drawPanel.SetCursor(wxCROSS_CURSOR) - self.drawPanel.CaptureMouse() - self._drawVisualFeedback(mousePt, mousePt, feedbackType, - dashedLine) - self.dragMode = drag_DRAG - - event.Skip() - return - - if event.Dragging(): - if self.dragMode == drag_RESIZE: - - # We're resizing an object. - - mousePt = self._getEventCoordinates(event) - if (self.curPt.x != mousePt.x) or (self.curPt.y != mousePt.y): - # Erase previous visual feedback. - endPt = wxPoint(self.curPt.x + self.resizeOffsetX, - self.curPt.y + self.resizeOffsetY) - self._drawVisualFeedback(self.resizeAnchor, endPt, - self.resizeFeedback, false) - self.curPt = mousePt - # Draw new visual feedback. - endPt = wxPoint(self.curPt.x + self.resizeOffsetX, - self.curPt.y + self.resizeOffsetY) - self._drawVisualFeedback(self.resizeAnchor, endPt, - self.resizeFeedback, false) - - elif self.dragMode == drag_MOVE: - - # We're moving a selected object. - - mousePt = self._getEventCoordinates(event) - if (self.curPt.x != mousePt.x) or (self.curPt.y != mousePt.y): - # Erase previous visual feedback. - self._drawObjectOutline(self.curPt.x - self.moveOrigin.x, - self.curPt.y - self.moveOrigin.y) - self.curPt = mousePt - # Draw new visual feedback. - self._drawObjectOutline(self.curPt.x - self.moveOrigin.x, - self.curPt.y - self.moveOrigin.y) - - elif self.dragMode == drag_DRAG: - - # We're dragging out a new object or selection rect. - - mousePt = self._getEventCoordinates(event) - if (self.curPt.x != mousePt.x) or (self.curPt.y != mousePt.y): - # Erase previous visual feedback. - self._drawVisualFeedback(self.dragOrigin, self.curPt, - feedbackType, dashedLine) - self.curPt = mousePt - # Draw new visual feedback. - self._drawVisualFeedback(self.dragOrigin, self.curPt, - feedbackType, dashedLine) - - event.Skip() - return - - if event.LeftUp(): - if self.dragMode == drag_RESIZE: - - # We're resizing an object. - - mousePt = self._getEventCoordinates(event) - # Erase last visual feedback. - endPt = wxPoint(self.curPt.x + self.resizeOffsetX, - self.curPt.y + self.resizeOffsetY) - self._drawVisualFeedback(self.resizeAnchor, endPt, - self.resizeFeedback, false) - - resizePt = wxPoint(mousePt.x + self.resizeOffsetX, - mousePt.y + self.resizeOffsetY) - - if (self.resizeFloater.x != resizePt.x) or \ - (self.resizeFloater.y != resizePt.y): - self._resizeObject(self.resizeObject, - self.resizeAnchor, - self.resizeFloater, - resizePt) - else: - self.drawPanel.Refresh() # Clean up after empty resize. - - elif self.dragMode == drag_MOVE: - - # We're moving a selected object. - - mousePt = self._getEventCoordinates(event) - # Erase last visual feedback. - self._drawObjectOutline(self.curPt.x - self.moveOrigin.x, - self.curPt.y - self.moveOrigin.y) - if (self.moveOrigin.x != mousePt.x) or \ - (self.moveOrigin.y != mousePt.y): - self._moveObject(mousePt.x - self.moveOrigin.x, - mousePt.y - self.moveOrigin.y) - else: - self.drawPanel.Refresh() # Clean up after empty drag. - - elif self.dragMode == drag_DRAG: - - # We're dragging out a new object or selection rect. - - mousePt = self._getEventCoordinates(event) - # Erase last visual feedback. - self._drawVisualFeedback(self.dragOrigin, self.curPt, - feedbackType, dashedLine) - self.drawPanel.ReleaseMouse() - self.drawPanel.SetCursor(wxSTANDARD_CURSOR) - # Perform the appropriate action for the current tool. - if actionParam == param_RECT: - x1 = min(self.dragOrigin.x, self.curPt.x) - y1 = min(self.dragOrigin.y, self.curPt.y) - x2 = max(self.dragOrigin.x, self.curPt.x) - y2 = max(self.dragOrigin.y, self.curPt.y) - - startX = x1 - startY = y1 - width = x2 - x1 - height = y2 - y1 - - if not selecting: - if ((x2-x1) < 8) or ((y2-y1) < 8): return # Too small. - - action(x1, y1, x2-x1, y2-y1) - elif actionParam == param_LINE: - action(self.dragOrigin.x, self.dragOrigin.y, - self.curPt.x, self.curPt.y) - - self.dragMode = drag_NONE # We've finished with this mouse event. - event.Skip() - - - def onDoubleClickEvent(self, event): - """ Respond to a double-click within our drawing panel. - """ - mousePt = self._getEventCoordinates(event) - obj = self._getObjectAt(mousePt) - if obj == None: return - - # Let the user edit the given object. - - if obj.getType() == obj_TEXT: - editor = EditTextObjectDialog(self, "Edit Text Object") - editor.objectToDialog(obj) - if editor.ShowModal() == wxID_CANCEL: - editor.Destroy() - return - - self._saveUndoInfo() - - editor.dialogToObject(obj) - editor.Destroy() - - self.dirty = true - self.drawPanel.Refresh() - self._adjustMenus() - else: - wxBell() - - - def onRightClick(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 = wxMenu() - menu.Append(menu_DUPLICATE, "Duplicate") - menu.Append(menu_EDIT_TEXT, "Edit...") - menu.Append(menu_DELETE, "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_TEXT, obj.getType() == obj_TEXT) - 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]) - - EVT_MENU(self, menu_DUPLICATE, self.doDuplicate) - EVT_MENU(self, menu_EDIT_TEXT, self.doEditText) - EVT_MENU(self, menu_DELETE, self.doDelete) - EVT_MENU(self, menu_MOVE_FORWARD, self.doMoveForward) - EVT_MENU(self, menu_MOVE_TO_FRONT, self.doMoveToFront) - EVT_MENU(self, menu_MOVE_BACKWARD, self.doMoveBackward) - EVT_MENU(self, menu_MOVE_TO_BACK, self.doMoveToBack) - - # Show the pop-up menu. - - clickPt = wxPoint(mousePt.x + self.drawPanel.GetPosition().x, - mousePt.y + self.drawPanel.GetPosition().y) - self.drawPanel.PopupMenu(menu, clickPt) - menu.Destroy() - - - def onPaintEvent(self, event): - """ Respond to a request to redraw the contents of our drawing panel. - """ - dc = wxPaintDC(self.drawPanel) - self.drawPanel.PrepareDC(dc) - dc.BeginDrawing() + """ 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) - for i in range(len(self.contents)-1, -1, -1): - obj = self.contents[i] - if obj in self.selection: - obj.draw(dc, true) - else: - obj.draw(dc, false) - dc.EndDrawing() # ========================== # == 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) + """ 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 = wxFileSelector("Open File", default_extension="psk", - flags = wxOPEN | wxFILE_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) + """ 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 + """ Respond to the "Close" menu command. + """ + global _docList - if self.dirty: - if not self.askIfUserWantsToSave("closing"): return + if self.dirty: + if not self.askIfUserWantsToSave("closing"): return - _docList.remove(self) - self.Destroy() + _docList.remove(self) + self.Destroy() def doSave(self, event): - """ Respond to the "Save" menu command. - """ - if self.fileName != None: - self.saveContents() + """ 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 = wxFileSelector("Save File As", "Saving", - default_filename=default, - default_extension="psk", - wildcard="*.psk", - flags = wxSAVE | wxOVERWRITE_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() + """ 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 + """ Respond to the "Revert" menu command. + """ + if not self.dirty: return - if wxMessageBox("Discard changes made to this document?", "Confirm", - style = wxOK | wxCANCEL | wxICON_QUESTION, - parent=self) == wxCANCEL: return - self.loadContents() + 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() + """ 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() + _app.ExitMainLoop() def doUndo(self, event): - """ Respond to the "Undo" menu command. - """ - if self.undoInfo == None: return - - undoData = self.undoInfo - self._saveUndoInfo() # For undoing the undo... + """ Respond to the "Undo" menu command. + """ + if not self.undoStack: return - self.contents = [] + state = self._buildStoredState() + self.redoStack.append(state) + state = self.undoStack.pop() + self._restoreStoredState(state) - for type, data in undoData["contents"]: - obj = DrawingObject(type) - obj.setData(data) - self.contents.append(obj) - - self.selection = [] - for i in undoData["selection"]: - self.selection.append(self.contents[i]) - - self.dirty = true - self.drawPanel.Refresh() - self._adjustMenus() + 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() + """ 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 = DrawingObject(obj.getType()) - newObj.setData(obj.getData()) - pos = obj.getPosition() - newObj.setPosition(wxPoint(pos.x + 10, pos.y + 10)) - objs.append(newObj) + """ Respond to the "Duplicate" menu command. + """ + self.saveUndoInfo() - self.contents = objs + self.contents + 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.selectMany(objs) + self.contents = objs + self.contents + self.selectMany(objs) - def doEditText(self, event): - """ Respond to the "Edit Text" menu command. - """ - if len(self.selection) != 1: return - obj = self.selection[0] - if obj.getType() != obj_TEXT: return + def doEditObject(self, event): + """ Respond to the "Edit..." menu command. + """ + if len(self.selection) != 1: return - editor = EditTextObjectDialog(self, "Edit Text Object") - editor.objectToDialog(obj) - if editor.ShowModal() == wxID_CANCEL: - editor.Destroy() - return + obj = self.selection[0] + if not obj.hasPropertyEditor(): + assert False, "doEditObject called on non-editable" - self._saveUndoInfo() - - editor.dialogToObject(obj) - editor.Destroy() - - self.dirty = true - self.drawPanel.Refresh() - self._adjustMenus() + 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 doChooseSelectTool(self, event=None): - """ Respond to the "Select Tool" menu command. - """ - self._setCurrentTool(self.selectIcon) - self.drawPanel.SetCursor(wxSTANDARD_CURSOR) - self._adjustMenus() - - - def doChooseLineTool(self, event=None): - """ Respond to the "Line Tool" menu command. - """ - self._setCurrentTool(self.lineIcon) - self.drawPanel.SetCursor(wxCROSS_CURSOR) - self.deselectAll() - self._adjustMenus() - - - def doChooseRectTool(self, event=None): - """ Respond to the "Rect Tool" menu command. - """ - self._setCurrentTool(self.rectIcon) - self.drawPanel.SetCursor(wxCROSS_CURSOR) - self.deselectAll() - self._adjustMenus() - - - def doChooseEllipseTool(self, event=None): - """ Respond to the "Ellipse Tool" menu command. - """ - self._setCurrentTool(self.ellipseIcon) - self.drawPanel.SetCursor(wxCROSS_CURSOR) - self.deselectAll() - self._adjustMenus() - - - def doChooseTextTool(self, event=None): - """ Respond to the "Text Tool" menu command. - """ - self._setCurrentTool(self.textIcon) - self.drawPanel.SetCursor(wxCROSS_CURSOR) - self.deselectAll() - self._adjustMenus() - + """ 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 + """ Respond to the "Move Forward" menu command. + """ + if len(self.selection) != 1: return - self._saveUndoInfo() + self.saveUndoInfo() - obj = self.selection[0] - index = self.contents.index(obj) - if index == 0: return + obj = self.selection[0] + index = self.contents.index(obj) + if index == 0: return - del self.contents[index] - self.contents.insert(index-1, obj) + del self.contents[index] + self.contents.insert(index-1, obj) - self.drawPanel.Refresh() - self._adjustMenus() + self.requestRedraw() + self._adjustMenus() def doMoveToFront(self, event): - """ Respond to the "Move to Front" menu command. - """ - if len(self.selection) != 1: return + """ Respond to the "Move to Front" menu command. + """ + if len(self.selection) != 1: return - self._saveUndoInfo() + self.saveUndoInfo() - obj = self.selection[0] - self.contents.remove(obj) - self.contents.insert(0, obj) + obj = self.selection[0] + self.contents.remove(obj) + self.contents.insert(0, obj) - self.drawPanel.Refresh() - self._adjustMenus() + self.requestRedraw() + self._adjustMenus() def doMoveBackward(self, event): - """ Respond to the "Move Backward" menu command. - """ - if len(self.selection) != 1: return + """ Respond to the "Move Backward" menu command. + """ + if len(self.selection) != 1: return - self._saveUndoInfo() + self.saveUndoInfo() - obj = self.selection[0] - index = self.contents.index(obj) - if index == len(self.contents) - 1: return + 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) + del self.contents[index] + self.contents.insert(index+1, obj) - self.drawPanel.Refresh() - self._adjustMenus() + self.requestRedraw() + self._adjustMenus() def doMoveToBack(self, event): - """ Respond to the "Move to Back" menu command. - """ - if len(self.selection) != 1: return + """ Respond to the "Move to Back" menu command. + """ + if len(self.selection) != 1: return - self._saveUndoInfo() + self.saveUndoInfo() - obj = self.selection[0] - self.contents.remove(obj) - self.contents.append(obj) + obj = self.selection[0] + self.contents.remove(obj) + self.contents.append(obj) - self.drawPanel.Refresh() - self._adjustMenus() + self.requestRedraw() + self._adjustMenus() def doShowAbout(self, event): - """ Respond to the "About pySketch" menu command. - """ - dialog = wxDialog(self, -1, "About pySketch") # , - #style=wxDIALOG_MODAL | wxSTAY_ON_TOP) - dialog.SetBackgroundColour(wxWHITE) - - panel = wxPanel(dialog, -1) - panel.SetBackgroundColour(wxWHITE) - - panelSizer = wxBoxSizer(wxVERTICAL) - - boldFont = wxFont(panel.GetFont().GetPointSize(), - panel.GetFont().GetFamily(), - wxNORMAL, wxBOLD) - - logo = wxStaticBitmap(panel, -1, wxBitmap("images/logo.bmp", - wxBITMAP_TYPE_BMP)) - - lab1 = wxStaticText(panel, -1, "pySketch") - lab1.SetFont(wxFont(36, boldFont.GetFamily(), wxITALIC, wxBOLD)) - lab1.SetSize(lab1.GetBestSize()) - - imageSizer = wxBoxSizer(wxHORIZONTAL) - imageSizer.Add(logo, 0, wxALL | wxALIGN_CENTRE_VERTICAL, 5) - imageSizer.Add(lab1, 0, wxALL | wxALIGN_CENTRE_VERTICAL, 5) - - lab2 = wxStaticText(panel, -1, "A simple object-oriented drawing " + \ - "program.") - lab2.SetFont(boldFont) - lab2.SetSize(lab2.GetBestSize()) - - lab3 = wxStaticText(panel, -1, "pySketch is completely free " + \ - "software; please") - lab3.SetFont(boldFont) - lab3.SetSize(lab3.GetBestSize()) - - lab4 = wxStaticText(panel, -1, "feel free to adapt or use this " + \ - "in any way you like.") - lab4.SetFont(boldFont) - lab4.SetSize(lab4.GetBestSize()) - - lab5 = wxStaticText(panel, -1, "Author: Erik Westra " + \ - "(ewestra@wave.co.nz)") - lab5.SetFont(boldFont) - lab5.SetSize(lab5.GetBestSize()) - - btnOK = wxButton(panel, wxID_OK, "OK") - - panelSizer.Add(imageSizer, 0, wxALIGN_CENTRE) - panelSizer.Add(10, 10) # Spacer. - panelSizer.Add(lab2, 0, wxALIGN_CENTRE) - panelSizer.Add(10, 10) # Spacer. - panelSizer.Add(lab3, 0, wxALIGN_CENTRE) - panelSizer.Add(lab4, 0, wxALIGN_CENTRE) - panelSizer.Add(10, 10) # Spacer. - panelSizer.Add(lab5, 0, wxALIGN_CENTRE) - panelSizer.Add(10, 10) # Spacer. - panelSizer.Add(btnOK, 0, wxALL | wxALIGN_CENTRE, 5) - - panel.SetAutoLayout(true) - panel.SetSizer(panelSizer) - panelSizer.Fit(panel) - - topSizer = wxBoxSizer(wxHORIZONTAL) - topSizer.Add(panel, 0, wxALL, 10) - - dialog.SetAutoLayout(true) - dialog.SetSizer(topSizer) - topSizer.Fit(dialog) - - dialog.Centre() + """ 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() - 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 createLine(self, x1, y1, x2, y2): - """ Create a new line object at the given position and size. - """ - self._saveUndoInfo() - - topLeftX = min(x1, x2) - topLeftY = min(y1, y2) - botRightX = max(x1, x2) - botRightY = max(y1, y2) - - obj = DrawingObject(obj_LINE, position=wxPoint(topLeftX, topLeftY), - size=wxSize(botRightX-topLeftX, - botRightY-topLeftY), - penColour=self.penColour, - fillColour=self.fillColour, - lineSize=self.lineSize, - startPt = wxPoint(x1 - topLeftX, y1 - topLeftY), - endPt = wxPoint(x2 - topLeftX, y2 - topLeftY)) - self.contents.insert(0, obj) - self.dirty = true - self.doChooseSelectTool() - self.select(obj) - - - def createRect(self, x, y, width, height): - """ Create a new rectangle object at the given position and size. - """ - self._saveUndoInfo() - - obj = DrawingObject(obj_RECT, position=wxPoint(x, y), - size=wxSize(width, height), - penColour=self.penColour, - fillColour=self.fillColour, - lineSize=self.lineSize) - self.contents.insert(0, obj) - self.dirty = true - self.doChooseSelectTool() - self.select(obj) - - - def createEllipse(self, x, y, width, height): - """ Create a new ellipse object at the given position and size. - """ - self._saveUndoInfo() - - obj = DrawingObject(obj_ELLIPSE, position=wxPoint(x, y), - size=wxSize(width, height), - penColour=self.penColour, - fillColour=self.fillColour, - lineSize=self.lineSize) - self.contents.insert(0, obj) - self.dirty = true - self.doChooseSelectTool() - self.select(obj) - - - def createText(self, x, y, width, height): - """ Create a new text object at the given position and size. - """ - editor = EditTextObjectDialog(self, "Create Text Object") - if editor.ShowModal() == wxID_CANCEL: - editor.Destroy() - return - - self._saveUndoInfo() - - obj = DrawingObject(obj_TEXT, position=wxPoint(x, y), - size=wxSize(width, height)) - editor.dialogToObject(obj) - editor.Destroy() - - self.contents.insert(0, obj) - self.dirty = true - self.doChooseSelectTool() - self.select(obj) + 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.drawPanel.Refresh() - self._adjustMenus() + """ 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.drawPanel.Refresh() - self._adjustMenus() + """ Deselect every DrawingObject in our document. + """ + self.selection = [] + self.requestRedraw() + self._adjustMenus() - def select(self, obj): - """ Select the given DrawingObject within our document. - """ - self.selection = [obj] - self.drawPanel.Refresh() - 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.drawPanel.Refresh() - self._adjustMenus() + """ 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.drawPanel.Refresh() - self._adjustMenus() + """ 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. - """ - f = open(self.fileName, "rb") - objData = cPickle.load(f) - f.close() + """ Load the contents of our document into memory. + """ - for type, data in objData: - obj = DrawingObject(type) - obj.setData(data) - self.contents.append(obj) + try: + f = open(self.fileName, "rb") + objData = cPickle.load(f) + f.close() - self.dirty = false - self.selection = [] - self.undoInfo = None + for klass, data in objData: + obj = klass() + obj.setData(data) + self.contents.append(obj) - self.drawPanel.Refresh() - self._adjustMenus() + 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. - """ - objData = [] - for obj in self.contents: - objData.append([obj.getType(), obj.getData()]) - f = open(self.fileName, "wb") - cPickle.dump(objData, f) - f.close() - self.dirty = false + 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 = wxMessageBox("Save changes before " + action + "?", - "Confirm", wxYES_NO | wxCANCEL, self) - - if response == wxYES: - if self.fileName == None: - fileName = wxFileSelector("Save File As", "Saving", - default_extension="psk", - wildcard="*.psk", - flags = wxSAVE | wxOVERWRITE_PROMPT) - if fileName == "": return false # User cancelled. - self.fileName = fileName - - self.saveContents() - return true - elif response == wxNO: - return true # User doesn't want changes saved. - elif response == wxCANCEL: - return false # User cancelled. + """ 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. - """ - canSave = (self.fileName != None) and self.dirty - canRevert = (self.fileName != None) and self.dirty - canUndo = self.undoInfo != None - selection = len(self.selection) > 0 - onlyOne = len(self.selection) == 1 - isText = onlyOne and (self.selection[0].getType() == obj_TEXT) - 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(wxID_SAVE, canSave) - self.fileMenu.Enable(wxID_REVERT, canRevert) - - self.editMenu.Enable(menu_UNDO, canUndo) - self.editMenu.Enable(menu_DUPLICATE, selection) - self.editMenu.Enable(menu_EDIT_TEXT, isText) - self.editMenu.Enable(menu_DELETE, selection) - - self.toolsMenu.Check(menu_SELECT, self.curTool == self.selectIcon) - self.toolsMenu.Check(menu_LINE, self.curTool == self.lineIcon) - self.toolsMenu.Check(menu_RECT, self.curTool == self.rectIcon) - self.toolsMenu.Check(menu_ELLIPSE, self.curTool == self.ellipseIcon) - self.toolsMenu.Check(menu_TEXT, self.curTool == self.textIcon) - - 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(wxID_NEW, true) - self.toolbar.EnableTool(wxID_OPEN, true) - self.toolbar.EnableTool(wxID_SAVE, canSave) - self.toolbar.EnableTool(menu_UNDO, canUndo) - 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 _setCurrentTool(self, newToolIcon): - """ Set the currently selected tool. - """ - if self.curTool == newToolIcon: return # Nothing to do. - - if self.curTool != None: - self.curTool.deselect() - - newToolIcon.select() - self.curTool = newToolIcon + """ 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.drawPanel.Refresh() - else: - self.penColour = colour - self.optionIndicator.setPenColour(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.drawPanel.Refresh() - else: - self.fillColour = colour - self.optionIndicator.setFillColour(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.drawPanel.Refresh() - else: - self.lineSize = size - self.optionIndicator.setLineSize(size) - - - 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. - """ - savedContents = [] - for obj in self.contents: - savedContents.append([obj.getType(), obj.getData()]) - - savedSelection = [] - for i in range(len(self.contents)): - if self.contents[i] in self.selection: - savedSelection.append(i) - - self.undoInfo = {"contents" : savedContents, - "selection" : savedSelection} + 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. + """ 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 obj.getType() == obj_TEXT: - # Not allowed to resize text objects -- they're sized to fit text. - wxBell() - return + '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() + self.saveUndoInfo() - topLeft = wxPoint(min(anchorPt.x, newPt.x), - min(anchorPt.y, newPt.y)) - botRight = wxPoint(max(anchorPt.x, newPt.x), - max(anchorPt.y, newPt.y)) + 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 + newWidth = botRight.x - topLeft.x + newHeight = botRight.y - topLeft.y - if obj.getType() == obj_LINE: - # Adjust the line so that its start and end points match the new - # overall object size. + 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() + 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)) + 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. + # 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)) + 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 (hFlip and not vFlip) or (vFlip and not hFlip): + slopesDown = not slopesDown # Line flipped. - if slopesDown: - obj.setStartPt(wxPoint(0, 0)) - obj.setEndPt(wxPoint(newWidth, newHeight)) - else: - obj.setStartPt(wxPoint(0, newHeight)) - obj.setEndPt(wxPoint(newWidth, 0)) + 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. + # Finally, adjust the bounds of the object to match the new dimensions. - obj.setPosition(topLeft) - obj.setSize(wxSize(botRight.x - topLeft.x, botRight.y - topLeft.y)) + obj.setPosition(topLeft) + obj.setSize(wx.Size(botRight.x - topLeft.x, botRight.y - topLeft.y)) - self.drawPanel.Refresh() + self.requestRedraw() def _moveObject(self, offsetX, offsetY): - """ Move the currently selected object(s) by the given offset. - """ - self._saveUndoInfo() + """ 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) + for obj in self.selection: + pos = obj.getPosition() + pos.x = pos.x + offsetX + pos.y = pos.y + offsetY + obj.setPosition(pos) - self.drawPanel.Refresh() + 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 = wxMenu() - menu.Append(id_LINESIZE_0, "no line", kind=wxITEM_CHECK) - menu.Append(id_LINESIZE_1, "1-pixel line", kind=wxITEM_CHECK) - menu.Append(id_LINESIZE_2, "2-pixel line", kind=wxITEM_CHECK) - menu.Append(id_LINESIZE_3, "3-pixel line", kind=wxITEM_CHECK) - menu.Append(id_LINESIZE_4, "4-pixel line", kind=wxITEM_CHECK) - menu.Append(id_LINESIZE_5, "5-pixel line", kind=wxITEM_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) - - EVT_MENU(self, id_LINESIZE_0, self._lineSizePopupSelected) - EVT_MENU(self, id_LINESIZE_1, self._lineSizePopupSelected) - EVT_MENU(self, id_LINESIZE_2, self._lineSizePopupSelected) - EVT_MENU(self, id_LINESIZE_3, self._lineSizePopupSelected) - EVT_MENU(self, id_LINESIZE_4, self._lineSizePopupSelected) - EVT_MENU(self, id_LINESIZE_5, self._lineSizePopupSelected) - - return menu + """ 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: - wxBell() - return - - self.optionIndicator.setLineSize(self.lineSize) + """ 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. + """ Return the coordinates associated with the given mouse event. - The coordinates have to be adjusted to allow for the current scroll - position. - """ + The coordinates have to be adjusted to allow for the current scroll + position. + """ originX, originY = self.drawPanel.GetViewStart() unitX, unitY = self.drawPanel.GetScrollPixelsPerUnit() - return wxPoint(event.GetX() + (originX * unitX), - event.GetY() + (originY * unitY)) + return wx.Point(event.GetX() + (originX * unitX), + event.GetY() + (originY * unitY)) - def _getObjectAndSelectionHandleAt(self, pt): - """ Return the object and selection handle at the given point. + def _drawObjectOutline(self, offsetX, offsetY): + """ Draw an outline of the currently selected object. - 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, handle_NONE). - """ - for obj in self.selection: - handle = obj.getSelectionHandleContainingPoint(pt.x, pt.y) - if handle != handle_NONE: - return obj, handle + The selected object's outline is drawn at the object's position + plus the given offset. - return None, handle_NONE + 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() - 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 + 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) - 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 = wxClientDC(self.drawPanel) - self.drawPanel.PrepareDC(dc) - dc.BeginDrawing() - dc.SetPen(wxBLACK_DASHED_PEN) - dc.SetBrush(wxTRANSPARENT_BRUSH) - dc.SetLogicalFunction(wxINVERT) - - dc.DrawRectangle(position.x + offsetX, position.y + offsetY, - size.width, size.height) - - dc.EndDrawing() - - - def _drawVisualFeedback(self, startPt, endPt, type, dashedLine): - """ Draw visual feedback for a drawing operation. - - The visual feedback consists of a line, ellipse, or rectangle based - around the two given points. 'type' should be one of the following - predefined feedback type constants: - - feedback_RECT -> draw rectangular feedback. - feedback_LINE -> draw line feedback. - feedback_ELLIPSE -> draw elliptical feedback. - - if 'dashedLine' is true, the feedback is drawn as a dashed rather - than a solid line. - - Note that the feedback is drawn by *inverting* the window's - contents, so calling _drawVisualFeedback twice in succession will - restore the window's contents back to what they were previously. - """ - dc = wxClientDC(self.drawPanel) - self.drawPanel.PrepareDC(dc) - dc.BeginDrawing() - if dashedLine: - dc.SetPen(wxBLACK_DASHED_PEN) - else: - dc.SetPen(wxBLACK_PEN) - dc.SetBrush(wxTRANSPARENT_BRUSH) - dc.SetLogicalFunction(wxINVERT) - - if type == feedback_RECT: - dc.DrawRectangle(startPt.x, startPt.y, - endPt.x - startPt.x, - endPt.y - startPt.y) - elif type == feedback_LINE: - dc.DrawLine(startPt.x, startPt.y, endPt.x, endPt.y) - elif type == feedback_ELLIPSE: - dc.DrawEllipse(startPt.x, startPt.y, - endPt.x - startPt.x, - endPt.y - startPt.y) - - dc.EndDrawing() + 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) -class DrawingObject: - """ An object within the drawing panel. - - A pySketch document consists of a front-to-back ordered list of - DrawingObjects. Each DrawingObject has the following properties: - - 'type' What type of object this is (text, line, etc). - '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. - 'startPt' The point, relative to the object's position, where - an obj_LINE object's line should start. - 'endPt' The point, relative to the object's position, where - an obj_LINE object's line should end. - 'text' The object's text (obj_TEXT objects only). - 'textFont' The text object's font name. - 'textSize' The text object's point size. - 'textBoldface' If true, this text object will be drawn in - boldface. - 'textItalic' If true, this text object will be drawn in italic. - 'textUnderline' If true, this text object will be drawn underlined. - """ + 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) 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 + """ 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 + """ 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 + """ 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 + """ Return the ending point for this line DrawingObject. + """ + return self.endPt + self.position - def setText(self, text): - """ Set the text for this DrawingObject. - """ - self.text = text + # ===================== + # == Private Methods == + # ===================== - def getText(self): - """ Return this DrawingObject's text. - """ - return self.text + 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 setTextFont(self, font): - """ Set the typeface for this text DrawingObject. - """ - self.textFont = font + 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 - def getTextFont(self): - """ Return this text DrawingObject's typeface. - """ - return self.textFont + return None + + def addPoint(self, x,y): + self.points.append((x-self.position.x,y-self.position.y)) + self._updateBoundingBox() - def setTextSize(self, size): - """ Set the point size for this text DrawingObject. - """ - self.textSize = size + 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 getTextSize(self): - """ Return this text DrawingObject's text size. - """ - return self.textSize + def drawHandles(self, dc): + """Draw selection handles for this DrawingObject""" + dc.SetPen(wx.BLACK_PEN) + dc.SetBrush(wx.BLACK_BRUSH) - def setTextBoldface(self, boldface): - """ Set the boldface flag for this text DrawingObject. - """ - self.textBoldface = boldface + 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) - def getTextBoldface(self): - """ Return this text DrawingObject's boldface flag. - """ - return self.textBoldface + # ============================= + # == Object Property Methods == + # ============================= - def setTextItalic(self, italic): - """ Set the italic flag for this text DrawingObject. - """ - self.textItalic = italic + 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)] - def getTextItalic(self): - """ Return this text DrawingObject's italic flag. - """ - return self.textItalic + return data - def setTextUnderline(self, underline): - """ Set the underling flag for this text DrawingObject. - """ - self.textUnderline = underline + 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) - def getTextUnderline(self): - """ Return this text DrawingObject's underline flag. - """ - return self.textUnderline + try: + self.points = d.next() + except StopIteration: + raise ValueError('Not enough data in setData call') - # ============================ - # == Object Drawing Methods == - # ============================ + return d + - 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. - """ - if self.type != obj_TEXT: - if self.lineSize == 0: - dc.SetPen(wxPen(self.penColour, self.lineSize, wxTRANSPARENT)) - else: - dc.SetPen(wxPen(self.penColour, self.lineSize, wxSOLID)) - dc.SetBrush(wxBrush(self.fillColour, wxSOLID)) - else: - dc.SetTextForeground(self.penColour) - dc.SetTextBackground(self.fillColour) - - self._privateDraw(dc, self.position, selected) + # ===================== + # == 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 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 - - if self.type in [obj_RECT, obj_TEXT]: - # Rectangles and text are easy -- they're always selected if the - # point is within their bounds. - return true - - # Now things get tricky. There's no straightforward way of knowing - # whether the point is within the 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. - - bitmap = wxEmptyBitmap(self.size.x + 10, self.size.y + 10) - dc = wxMemoryDC() - dc.SelectObject(bitmap) - dc.BeginDrawing() - dc.SetBackground(wxWHITE_BRUSH) - dc.Clear() - dc.SetPen(wxPen(wxBLACK, self.lineSize + 5, wxSOLID)) - dc.SetBrush(wxBLACK_BRUSH) - self._privateDraw(dc, wxPoint(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 + 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 getSelectionHandleContainingPoint(self, x, y): - """ Return the selection handle containing the given point, if any. - - We return one of the predefined selection handle ID codes. - """ - if self.type == obj_LINE: - # 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 handle_START_POINT - elif self._pointInSelRect(x, y, self.position.x + self.endPt.x, - self.position.y + self.endPt.y): - return handle_END_POINT - else: - return handle_NONE - else: - # We have selection handles at all four corners. - if self._pointInSelRect(x, y, self.position.x, self.position.y): - return handle_TOP_LEFT - elif self._pointInSelRect(x, y, self.position.x + self.size.width, - self.position.y): - return handle_TOP_RIGHT - elif self._pointInSelRect(x, y, self.position.x, - self.position.y + self.size.height): - return handle_BOTTOM_LEFT - elif self._pointInSelRect(x, y, self.position.x + self.size.width, - self.position.y + self.size.height): - return handle_BOTTOM_RIGHT - else: - return handle_NONE + 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() - 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 + + # ============================= + # == 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 + # ===================== - # == Utility Methods == + # == 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) - def fitToText(self): - """ Resize a text DrawingObject so that it fits it's text exactly. - """ - if self.type != obj_TEXT: return +#---------------------------------------------------------------------------- +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. - if self.textBoldface: weight = wxBOLD - else: weight = wxNORMAL - if self.textItalic: style = wxITALIC - else: style = wxNORMAL - font = wxFont(self.textSize, wxDEFAULT, style, weight, - self.textUnderline, self.textFont) + This is used to determine if the user clicked on the object. + """ + # Firstly, ignore any points outside of the object's bounds. - dummyWindow = wxFrame(None, -1, "") - dummyWindow.SetFont(font) - width, height = dummyWindow.GetTextExtent(self.text) - dummyWindow.Destroy() + 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 - self.size = wxSize(width, height) + # 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. - """ - if self.type == obj_LINE: - dc.DrawLine(position.x + self.startPt.x, - position.y + self.startPt.y, - position.x + self.endPt.x, - position.y + self.endPt.y) - elif self.type == obj_RECT: - dc.DrawRectangle(position.x, position.y, - self.size.width, self.size.height) - elif self.type == obj_ELLIPSE: - dc.DrawEllipse(position.x, position.y, - self.size.width, self.size.height) - elif self.type == obj_TEXT: - if self.textBoldface: weight = wxBOLD - else: weight = wxNORMAL - if self.textItalic: style = wxITALIC - else: style = wxNORMAL - font = wxFont(self.textSize, wxDEFAULT, style, weight, - self.textUnderline, self.textFont) - dc.SetFont(font) - dc.DrawText(self.text, position.x, position.y) - - if selected: - dc.SetPen(wxTRANSPARENT_PEN) - dc.SetBrush(wxBLACK_BRUSH) - - if self.type == obj_LINE: - # Draw selection handles at the start and end points. - self._drawSelHandle(dc, position.x + self.startPt.x, - position.y + self.startPt.y) - self._drawSelHandle(dc, position.x + self.endPt.x, - position.y + self.endPt.y) - else: - # Draw selection handles at all four corners. - self._drawSelHandle(dc, position.x, position.y) - self._drawSelHandle(dc, position.x + self.size.width, - position.y) - self._drawSelHandle(dc, position.x, - position.y + self.size.height) - self._drawSelHandle(dc, position.x + self.size.width, - position.y + self.size.height) + """ 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) - 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) +#---------------------------------------------------------------------------- +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) - 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 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 ToolPaletteIcon(wxStaticBitmap): +class ToolPaletteToggle(GenBitmapToggleButton): """ An icon appearing in the tool palette area of our sketching window. - Note that this is actually implemented as a wxStaticBitmap rather - than as a wxIcon. wxIcon has a very specific meaning, and isn't - appropriate for this more general use. + 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. + 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 - '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) + bmpsel = wx.Bitmap("images/" + iconName + "IconSel.bmp", wx.BITMAP_TYPE_BMP) - The icon name is used to get the appropriate bitmap for this icon. - """ - bmp = wxBitmap("images/" + iconName + "Icon.bmp", wxBITMAP_TYPE_BMP) - wxStaticBitmap.__init__(self, parent, iconID, bmp, wxDefaultPosition, - wxSize(bmp.GetWidth(), bmp.GetHeight())) - self.SetToolTip(wxToolTip(toolTip)) + GenBitmapToggleButton.__init__(self, parent, iconID, bitmap=bmp, + size=(bmp.GetWidth()+1, bmp.GetHeight()+1), + style=wx.BORDER_NONE) - self.iconID = iconID - self.iconName = iconName - self.isSelected = false + self.SetToolTip(wx.ToolTip(toolTip)) + self.SetBitmapLabel(bmp) + self.SetBitmapSelected(bmpsel) + self.iconID = iconID + self.iconName = iconName - def select(self): - """ Select the icon. - The icon's visual representation is updated appropriately. - """ - if self.isSelected: return # Nothing to do! +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. - bmp = wxBitmap("images/" + self.iconName + "IconSel.bmp", - wxBITMAP_TYPE_BMP) - self.SetBitmap(bmp) - self.isSelected = true + '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) - def deselect(self): - """ Deselect the icon. + self.iconID = iconID + self.iconName = iconName - The icon's visual representation is updated appropriately. - """ - if not self.isSelected: return # Nothing to do! - bmp = wxBitmap("images/" + self.iconName + "Icon.bmp", - wxBITMAP_TYPE_BMP) - self.SetBitmap(bmp) - self.isSelected = false #---------------------------------------------------------------------------- -class ToolOptionIndicator(wxWindow): +class ToolOptionIndicator(wx.Window): """ A visual indicator which shows the current tool options. """ def __init__(self, parent): - """ Standard constructor. - """ - wxWindow.__init__(self, parent, -1, wxDefaultPosition, wxSize(52, 32)) + """ Standard constructor. + """ + wx.Window.__init__(self, parent, -1, wx.DefaultPosition, wx.Size(52, 32)) - self.penColour = wxBLACK - self.fillColour = wxWHITE - self.lineSize = 1 + self.penColour = wx.BLACK + self.fillColour = wx.WHITE + self.lineSize = 1 - EVT_PAINT(self, self.OnPaint) + # 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() + """ 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() + """ 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 = wxPaintDC(self) - dc.BeginDrawing() - - if self.lineSize == 0: - dc.SetPen(wxPen(self.penColour, self.lineSize, wxTRANSPARENT)) - else: - dc.SetPen(wxPen(self.penColour, self.lineSize, wxSOLID)) - dc.SetBrush(wxBrush(self.fillColour, wxSOLID)) - - dc.DrawRectangle(5, 5, self.GetSize().width - 10, - self.GetSize().height - 10) - - dc.EndDrawing() + """ 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(wxDialog): +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. + The user can edit the object's text, font, size, and text style. """ def __init__(self, parent, title): - """ Standard constructor. - """ - wxDialog.__init__(self, parent, -1, title) - - self.textCtrl = wxTextCtrl(self, 1001, "", style=wxTE_PROCESS_ENTER, - validator=TextObjectValidator()) - extent = self.textCtrl.GetFullTextExtent("Hy") - lineHeight = extent[1] + extent[3] - self.textCtrl.SetSize(wxSize(-1, lineHeight * 4)) + """ Standard constructor. + """ + wx.Dialog.__init__(self, parent, -1, title, + style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER) - EVT_TEXT_ENTER(self, 1001, self._doEnter) + 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 - fonts = wxFontEnumerator() - fonts.EnumerateFacenames() - self.fontList = fonts.GetFacenames() - self.fontList.sort() + self.Bind(wx.EVT_TEXT_ENTER, self._doEnter, id=1001) - fontLabel = wxStaticText(self, -1, "Font:") - self._setFontOptions(fontLabel, weight=wxBOLD) + fontBtn = wx.Button(self, -1, "Select Font...") + self.Bind(wx.EVT_BUTTON, self.OnSelectFont, fontBtn) - self.fontCombo = wxComboBox(self, -1, "", wxDefaultPosition, - wxDefaultSize, self.fontList, - style = wxCB_READONLY) - self.fontCombo.SetSelection(0) # Default to first available font. + gap = wx.LEFT | wx.TOP | wx.RIGHT - self.sizeList = ["8", "9", "10", "12", "14", "16", - "18", "20", "24", "32", "48", "72"] + self.okButton = wx.Button(self, wx.ID_OK, "&OK") + self.okButton.SetDefault() + self.cancelButton = wx.Button(self, wx.ID_CANCEL, "&Cancel") - sizeLabel = wxStaticText(self, -1, "Size:") - self._setFontOptions(sizeLabel, weight=wxBOLD) + btnSizer = wx.StdDialogButtonSizer() - self.sizeCombo = wxComboBox(self, -1, "", wxDefaultPosition, - wxDefaultSize, self.sizeList, - style=wxCB_READONLY) - self.sizeCombo.SetSelection(3) # Default to 12 point text. + btnSizer.Add(self.okButton, 0, gap, 5) + btnSizer.Add(self.cancelButton, 0, gap, 5) - gap = wxLEFT | wxTOP | wxRIGHT + 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) - comboSizer = wxBoxSizer(wxHORIZONTAL) - comboSizer.Add(fontLabel, 0, gap | wxALIGN_CENTRE_VERTICAL, 5) - comboSizer.Add(self.fontCombo, 0, gap, 5) - comboSizer.Add(5, 5) # Spacer. - comboSizer.Add(sizeLabel, 0, gap | wxALIGN_CENTRE_VERTICAL, 5) - comboSizer.Add(self.sizeCombo, 0, gap, 5) + self.SetAutoLayout(True) + self.SetSizer(sizer) + sizer.Fit(self) - self.boldCheckbox = wxCheckBox(self, -1, "Bold") - self.italicCheckbox = wxCheckBox(self, -1, "Italic") - self.underlineCheckbox = wxCheckBox(self, -1, "Underline") + self.textCtrl.SetFocus() - self._setFontOptions(self.boldCheckbox, weight=wxBOLD) - self._setFontOptions(self.italicCheckbox, style=wxITALIC) - self._setFontOptions(self.underlineCheckbox, underline=true) - styleSizer = wxBoxSizer(wxHORIZONTAL) - styleSizer.Add(self.boldCheckbox, 0, gap, 5) - styleSizer.Add(self.italicCheckbox, 0, gap, 5) - styleSizer.Add(self.underlineCheckbox, 0, gap, 5) + 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) - self.okButton = wxButton(self, wxID_OK, "OK") - self.cancelButton = wxButton(self, wxID_CANCEL, "Cancel") + dlg = wx.FontDialog(self, data) + + if dlg.ShowModal() == wx.ID_OK: + data = dlg.GetFontData() + font = data.GetChosenFont() + colour = data.GetColour() - btnSizer = wxBoxSizer(wxHORIZONTAL) - btnSizer.Add(self.okButton, 0, gap, 5) - btnSizer.Add(self.cancelButton, 0, gap, 5) + self.curFont = font + self.curClr = colour - sizer = wxBoxSizer(wxVERTICAL) - sizer.Add(self.textCtrl, 1, gap | wxEXPAND, 5) - sizer.Add(10, 10) # Spacer. - sizer.Add(comboSizer, 0, gap | wxALIGN_CENTRE, 5) - sizer.Add(styleSizer, 0, gap | wxALIGN_CENTRE, 5) - sizer.Add(10, 10) # Spacer. - sizer.Add(btnSizer, 0, gap | wxALIGN_CENTRE, 5) + self.textCtrl.SetFont(font) + # Update dialog for the new height of the text + self.GetSizer().Fit(self) - self.SetAutoLayout(true) - self.SetSizer(sizer) - sizer.Fit(self) - - self.textCtrl.SetFocus() + 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())) - - for i in range(len(self.fontList)): - if self.fontList[i] == obj.getTextFont(): - self.fontCombo.SetSelection(i) - break + """ Copy the properties of the given text object into the dialog box. + """ + self.textCtrl.SetValue(obj.getText()) + self.textCtrl.SetSelection(0, len(obj.getText())) - for i in range(len(self.sizeList)): - if self.sizeList[i] == str(obj.getTextSize()): - self.sizeCombo.SetSelection(i) - break + self.curFont = obj.getFont() + self.textCtrl.SetFont(self.curFont) - self.boldCheckbox.SetValue(obj.getTextBoldface()) - self.italicCheckbox.SetValue(obj.getTextItalic()) - self.underlineCheckbox.SetValue(obj.getTextUnderline()) def dialogToObject(self, obj): - """ Copy the properties from the dialog box into the given text object. - """ - obj.setText(self.textCtrl.GetValue()) - obj.setTextFont(self.fontCombo.GetValue()) - obj.setTextSize(string.atoi(self.sizeCombo.GetValue())) - obj.setTextBoldface(self.boldCheckbox.GetValue()) - obj.setTextItalic(self.italicCheckbox.GetValue()) - obj.setTextUnderline(self.underlineCheckbox.GetValue()) - obj.fitToText() + """ 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 _setFontOptions(self, ctrl, family=None, pointSize=-1, - style=wxNORMAL, weight=wxNORMAL, - underline=false): - """ Change the font settings for the given control. - - The meaning of the 'family', 'pointSize', 'style', 'weight' and - 'underline' parameters are the same as for the wxFont constructor. - If the family and/or pointSize isn't specified, the current default - value is used. - """ - if family == None: family = ctrl.GetFont().GetFamily() - if pointSize == -1: pointSize = ctrl.GetFont().GetPointSize() - - ctrl.SetFont(wxFont(pointSize, family, style, weight, underline)) - ctrl.SetSize(ctrl.GetBestSize()) # Adjust size to reflect font change. - - def _doEnter(self, event): - """ Respond to the user hitting the ENTER key. + """ Respond to the user hitting the ENTER key. - We simulate clicking on the "OK" button. - """ - if self.Validate(): self.Show(false) + We simulate clicking on the "OK" button. + """ + if self.Validate(): self.Show(False) #---------------------------------------------------------------------------- -class TextObjectValidator(wxPyValidator): +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. + into the text object editor dialog's text field. """ def __init__(self): - """ Standard constructor. - """ - wxPyValidator.__init__(self) + """ Standard constructor. + """ + wx.PyValidator.__init__(self) def Clone(self): - """ Standard cloner. + """ Standard cloner. - Note that every validator must implement the Clone() method. - """ - return TextObjectValidator() + Note that every validator must implement the Clone() method. + """ + return TextObjectValidator() def Validate(self, win): - """ Validate the contents of the given text control. - """ - textCtrl = wxPyTypeCast(self.GetWindow(), "wxTextCtrl") - text = textCtrl.GetValue() + """ Validate the contents of the given text control. + """ + textCtrl = self.GetWindow() + text = textCtrl.GetValue() - if len(text) == 0: - wxMessageBox("A text object must contain some text!", "Error") - return false - else: - return true + 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. + """ 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 wxDialog from complaining. + 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. + """ 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 wxDialog from complaining. + 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. + 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. + """ 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 = "" - - if s[:9] == "Traceback": - # Tell the user than an exception occurred. - wxMessageBox("An internal error has occurred.\nPlease " + \ - "refer to the 'errors.txt' file for details.", - "Error", wxOK | wxCENTRE | wxICON_EXCLAMATION) - - f = open("errors.txt", "a") - f.write(s) - f.close() - except: - pass # Don't recursively crash on errors. + """ 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(wxApp): +class SketchApp(wx.App): """ The main pySketch application object. """ def OnInit(self): - """ Initialise the application. - """ - wxInitAllImageHandlers() - - 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 + """ 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 #---------------------------------------------------------------------------- @@ -2636,3 +3551,4 @@ def main(): if __name__ == "__main__": main() +