Exemple #1
0
 def open_edit_panel(self, target):
     '''Open an edit panel aimed at the current target.'''
     if self.edit_panel and self.edit_panel.target == target:
         return
     self.close_edit_panel()
     self.edit_panel = EditPanel(self, target)
Exemple #2
0
class Canvas(wx.Panel):
    def __init__(self, parent, callbacks, *args, **kwargs):
        wx.Panel.__init__(self, parent, *args, **kwargs)
        
        self.SetBackgroundColour('#000000')
        
        self.Bind(wx.EVT_PAINT, self.paint)
        self.Bind(wx.EVT_MOTION, self.mouse_move)
        self.Bind(wx.EVT_LEFT_DOWN, self.mouse_lclick)
        self.Bind(wx.EVT_LEFT_DCLICK, self.mouse_dclick)
        self.Bind(wx.EVT_RIGHT_DOWN, self.show_menu)
        self.Bind(wx.EVT_LEFT_UP, self.mouse_lrelease)
        self.Bind(wx.EVT_MOUSEWHEEL, self.mouse_scroll)
        self.Bind(wx.EVT_SIZE, self.onViewChange)
        self.Bind(wx.EVT_CHAR, self.char)
        
        # Silly tricks to ensure that focus moves seamlessly
        # between the editor, canvas, and text boxes in the
        # edit window.
        def focus(event):
            if self.last_focus and type(self.last_focus) is wx.TextCtrl:
                self.last_focus.SetFocus()
            elif self.edit_panel:
                self.edit_panel.Hide()
                self.SetFocus()
                self.edit_panel.Show()
            else:
                self.SetFocus()
            self.last_focus = None
        def lost_focus(event):
            self.last_focus = self.FindFocus()
            self.Refresh()
        self.Bind(wx.EVT_ENTER_WINDOW, focus)
        self.Bind(wx.EVT_LEAVE_WINDOW, lost_focus)
        self.last_focus = None
        
        self.Bind(wx.EVT_ERASE_BACKGROUND, lambda e: None)
        
        # Used for click+drag operators
        mx, my = wx.GetMousePosition()
        dx, dy = self.GetScreenPosition()
        self.click = wx.Point(mx - dx, my - dy)
        self.mouse = wx.Point(mx - dx, my - dy)

        # Image boundaries
        self.center = [0.0, 0.0]        
        self.scale = 100.0
        
        # Populate self.view with initial values
        self.record_view()

        # Callback to trigger a rerender                
        self.view_change = callbacks['view']
        
        # Create a default image for the initial drawing
        width, height = self.Size
        self.image = wx.EmptyImage(*self.Size)
        self.image.view = self.view.copy()
        
        # Create an initial empty bitmap
        self.bitmap = wx.BitmapFromImage(self.image)
        self.bitmap.view = self.image.view.copy()
        
        self.dc = None
        
        # Interactive geometry tools
        self.shape_set = ShapeSet()
        globals.SHAPES = weakref.proxy(self.shape_set)
        
        self.edit_panel = None
        self.drag_target = None
        
        # Create an undo stack
        self.undo_stack = []
        self.push_stack(callback=False)
        
        # We are not unsaved at the start
        self.unsaved_callback = callbacks['unsaved']
        self.unsaved = False
        
################################################################################

    @property
    def border(self):
        try:
            return self._border
        except AttributeError:
            self._border = (100, 100, 100)
            return self._border
    @border.setter
    def border(self, value):
        self._border = value
        wx.CallAfter(self.Refresh)

################################################################################

    def mouse_move(self, event):
        '''Handles a mouse move across the canvas.'''
        self.mouse = wx.Point(event.GetX(), event.GetY())
        
        # Drags the view around
        changed = False
        if self.drag_target is not None:
            delta = self.mouse - self.click
            self.click = self.mouse
            
            self.drag_target.drag(delta.x/self.scale, -delta.y/self.scale)
            self.did_drag = True
            changed = True
        else:
            x, y = self.pixel_to_pos(*self.mouse)
            r    = 5/self.scale
            if self.shape_set.check_hover(x, y, r):
                changed = True
  
        if changed:
            self.Refresh()

########################################

    def mouse_lclick(self, event):
        '''Records a left click event in the canvas.'''
        self.click = self.mouse
        
        x, y = self.pixel_to_pos(*self.mouse)
        r    = 10/self.scale
        
        if self.drag_target:
            self.drag_target.drop()
            self.shape_set.check_hover(x, y, r)
        
        self.drag_target = self.shape_set.get_target(x, y, r)
        if self.drag_target is not None:
            self.drag_target.dragging = True
            self.did_select = True
        else:
            self.did_select = False
            self.drag_target = self
            
        self.did_drag = False
                    
        self.Refresh()
    
########################################

    def mouse_dclick(self, event):
        '''Double-click to open up the point editing box.'''
        
        x, y = self.pixel_to_pos(*self.mouse)
        r    = 10/self.scale
        
        target = self.shape_set.get_target(x, y, r)
        if target is not None:
            self.open_edit_panel(target)
    
########################################

    def mouse_lrelease(self, event):
        '''If we just did a null click (no dragging, no selecting)
           then deselect the currently selected node.'''
        if self.did_select == False and self.did_drag == False:
            self.close_edit_panel()

        if self.drag_target:
            self.drag_target.drop()
            
        self.push_stack()            
        self.drag_target = None
        
        self.Refresh()
    
########################################

    def mouse_scroll(self, event):
        '''Handles mouse scrolling by adjusting window scale.'''
        width, height = self.Size
        
        origin = ((width/2 - self.mouse[0]) / self.scale - self.center[0],
                  (self.mouse[1] - height/2) / self.scale - self.center[1])
                
        dScale = 1.0025
        if event.GetWheelRotation() < 0:
            dScale = 1 / dScale
        for i in range(abs(event.GetWheelRotation())):
            self.scale *= dScale
        if self.scale > (1 << 32):
            self.scale = 1 << 32
        
        # Reposition the center so that the point under the mouse cursor remains
        # under the mouse cursor post-zoom.
        self.center = ((width/2 - self.mouse[0]) / self.scale - origin[0],
                       (self.mouse[1] - height/2) / self.scale - origin[1])
                       
        self.onViewChange()
    
    
    def char(self, event):
        if event.CmdDown() and event.GetKeyCode() == ord('Z'):
            if self.can_undo:
                self.undo()
        elif event.GetKeyCode() == 127:
        
            x, y = self.pixel_to_pos(*self.mouse)
            r    = 10/self.scale
            
            target = self.shape_set.get_target(x, y, r)
            
            if self.edit_panel and self.edit_panel.target == target:
                self.close_edit_panel()
            self.shape_set.delete(target)

            self.push_stack()
            self.Refresh()
        else:
            event.Skip()
    
################################################################################

    def open_edit_panel(self, target):
        '''Open an edit panel aimed at the current target.'''
        if self.edit_panel and self.edit_panel.target == target:
            return
        self.close_edit_panel()
        self.edit_panel = EditPanel(self, target)
    
    def close_edit_panel(self, event=None):
        '''Safely closes the edit panel, doing nothing if there
           is not an edit panel currently visible.'''
        if self.edit_panel is None:
            return
        self.edit_panel.target.selected = False
        self.edit_panel.Destroy()
        self.edit_panel = None
        self.SetFocus()
    
########################################

    def edit_panel_closed(self, evt):
        '''Callback when an edit panel is closed - pushes geometry
           state to the stack.'''
        self.push_stack()
    
################################################################################
    
    def show_menu(self, event):
        menu = wx.Menu()
        
        # Function generator to fix sneaky for loop binding problem.
        def function_wrapper(constructor):
            return lambda e: build_from(constructor)
        
        # Constructor to create an object at the mouse position.
        def build_from(constructor):
            mx, my = wx.GetMousePosition()
            dx, dy = self.GetScreenPosition()
            scale = 100/self.scale
            
            self.click = wx.Point(mx - dx, my - dy)
            self.mouse = wx.Point(mx - dx, my - dy)
            
            x, y = self.pixel_to_pos(mx - dx, my - dy)
            objs = constructor(x, y, scale)
            self.shape_set.add_shapes(objs)
            self.push_stack()
            
            self.open_edit_panel(objs[-1])
            self.drag_target = objs[-1]
            objs[-1].dragging = True

        constructors = koko.shapes.menu.constructors
        for T in sorted(constructors):
            sub = wx.Menu()
            for name, constructor in sorted(constructors[T]):
                m = sub.Append(wx.ID_ANY, text=name)
                self.Bind(wx.EVT_MENU, function_wrapper(constructor), m)
            menu.AppendMenu(wx.ID_ANY, T, sub)
        
        menu.AppendSeparator()
        
        # Get the a target node to delete
        x, y = self.pixel_to_pos(*self.mouse)
        r    = 10/self.scale
        target = self.shape_set.get_target(x, y, r)
        
        def del_shape(evt):
            if self.edit_panel and self.edit_panel.target == target:
                self.close_edit_panel()
            self.shape_set.delete(target)

            self.push_stack()
            self.Refresh()
        
        delete = menu.Append(wx.ID_ANY, text='Delete')
        if target is not None:
            self.Bind(wx.EVT_MENU, del_shape, delete)
        else:
            delete.Enable(False)
            
        undo = menu.Append(wx.ID_ANY, text='Undo')
        if self.can_undo:
            self.Bind(wx.EVT_MENU, self.undo, undo)
        else:
            undo.Enable(False)

            
        self.PopupMenu(menu)
    
########################################

    def drag(self, dx, dy):
        self.center = (self.center[0] - dx, self.center[1] - dy)
        self.onViewChange()
    
    def drop(self):
        pass

################################################################################

    def pos_to_pixel(self, x, y=None):
        width, height = self.Size
        xcenter, ycenter = self.center
        
        if y is not None:
            return ((x - xcenter) * self.scale + (width / 2.),
                     height/2. - (y - ycenter) * self.scale)
        else:
            return x*self.scale
    
    
########################################

    def pixel_to_pos(self, x, y):
        width, height = self.Size
        xcenter, ycenter = self.center
        
        return ((x - width/2) / self.scale + xcenter,
               (height/2 - y) / self.scale + ycenter)


################################################################################

    def paint(self, event=None):
        '''Redraws the window.'''
        
        # Update the edit panel (coordinates, values, etc)
        if self.edit_panel and self.edit_panel.IsShown():
            self.edit_panel.update()
        
        self.dc = wx.PaintDC(self)
        
        width, height = self.Size
        xcenter, ycenter = self.center[0], self.center[1]
                
        # Rescale image bitmap if necessary
#        if self.bitmap.view['xmin'] < self.view['xmin']:
# TO COME LATER            
        
        if self.scale != self.bitmap.view['pixels/unit']:
            
            scale = self.scale / self.image.view['pixels/unit']
            if self.image.IsOk():
                img = self.image.Copy()
                if scale < 4 and scale > 0.1:
                    img.Rescale(self.image.Width  * scale,
                                self.image.Height * scale)
                    self.bitmap = wx.BitmapFromImage(img)
                    self.bitmap.view = self.image.view.copy()
                    self.bitmap.view['pixels/unit'] = self.scale
        
        # Draw image
        imgX, imgY = self.pos_to_pixel(self.bitmap.view['xmin'],
                                       self.bitmap.view['ymax'])
        self.dc.DrawBitmap(self.bitmap, imgX, imgY)
        
        # Draw x and y axes
        self.draw_axes()
            
        # Draw the rest of the geometry
        self.shape_set.draw()
        
        # Draw border
        self.draw_border()
        
        self.dc = None
        
    
    def SetPen(self, *args, **kwargs):
        if type(args[0]) is wx.Pen:
            self.dc.SetPen(args[0])
        else:
            self.dc.SetPen(wx.Pen(*args, **kwargs))
    def SetBrush(self, *args, **kwargs):
        if type(args[0]) is wx.Brush:
            self.dc.SetBrush(args[0])
        else:
            self.dc.SetBrush(wx.Brush(*args, **kwargs))
        
################################################################################

    def draw_axes(self):
        center = self.pos_to_pixel(0, 0)
        self.dc.SetPen(wx.Pen(wx.Colour(255, 0, 0), 2))
        self.dc.DrawLine(center[0], center[1], center[0] + 50, center[1])
        self.dc.SetPen(wx.Pen(wx.Colour(0, 255, 0), 2))
        self.dc.DrawLine(center[0], center[1], center[0], center[1] - 50)

    def draw_border(self):
        if self.border:
            self.dc.SetPen(wx.TRANSPARENT_PEN)
            self.dc.SetBrush(wx.Brush(self.border))
            
            border_width = 3
            self.dc.DrawRectangle(0, 0, self.Size[0], border_width)
            self.dc.DrawRectangle(0, self.Size[1]-border_width,
                                  self.Size[0], border_width)
            self.dc.DrawRectangle(0, 0, border_width, self.Size[1])
            self.dc.DrawRectangle(self.Size[0]-border_width, 0,
                                  border_width, self.Size[1])

################################################################################

    def create_bitmap(self):
        scale = self.scale / self.image.view['pixels/unit']
            
################################################################################

    def record_view(self):
        '''Record the current view into self.view'''
        width, height = self.Size
        
        xmin = self.center[0] - width/2./self.scale
        xmax = self.center[0] + width/2./self.scale
        ymin = self.center[1] - height/2./self.scale
        ymax = self.center[1] + height/2./self.scale
        
        self.view = {'xmin': xmin, 'xmax': xmax,
                     'ymin': ymin, 'ymax': ymax,
                     'pixels/unit': self.scale}      
    
    def onViewChange(self, event=None):
        '''Callback for when the window size or view changes.'''
        self.record_view()
        self.Refresh()
        self.view_change() # Run callback to rerender image

################################################################################

    def load_image(self, filename, view):
        '''Loads an image from a file.'''
        
        # Load the image from file and save its associated view
        self.image = wx.Image(filename)
        self.image.view = view
        
        # Create an initial bitmap, with the same view as the image
        if self.image.IsOk():
            self.bitmap = wx.BitmapFromImage(self.image)
            self.bitmap.view = self.image.view.copy()
            os.remove(filename)
            
        # Redraw the world
        self.Refresh()

################################################################################

    def snap_bounds(self, math):
        width, height = self.Size

        self.center = [math['xmin'] + math['dx']/2.,
                       math['ymin'] + math['dy']/2.]

        self.scale = min(width/math['dx'], height/math['dy'])
        self.onViewChange()

################################################################################
    
    @property
    def can_undo(self):
        return (len(self.undo_stack) >= 2 or
                self.shape_set.reconstructor() != self.undo_stack[-1])
                
########################################

    def push_stack(self, callback=True):
        shapes = self.shape_set.reconstructor()
        if self.undo_stack == [] or shapes != self.undo_stack[-1]:
            self.undo_stack.append(shapes)
            self.unsaved = True
            if callback:
                self.unsaved_callback()

########################################
    
    def undo(self, event=None):
        shapes = self.shape_set.reconstructor()
        
        # Save whoever the edit panel is pointing to - we'll
        # attempt to re-establish this connection after we reload
        # the shapes from the stack.
        if self.edit_panel:
            target = self.edit_panel.target['name'].expr
            id = [s[1]['name'] for s in shapes].index(target)
            self.close_edit_panel()
        else:
            target = None
        
        if shapes != self.undo_stack[-1]:
            self.shape_set.reconstruct(self.undo_stack[-1])
        elif len(self.undo_stack) >= 2:
            self.shape_set.reconstruct(self.undo_stack[-2])
            self.undo_stack = self.undo_stack[:-1]

        # Attempt to re-open the edit panel pointing at the same
        # thing.  This will fail under name changes, or if 
        # the undo deletes the target point.
        if target is not None:
            try:
                self.open_edit_panel(self.shape_set[target])
            except KeyError:
            
                # If that failed, try to get the item in the same position
                # as the previous object - this should compensate for name
                # changes.
                try:
                    old_name = self.shape_set.reconstructor()[id][1]['name']
                except IndexError:
                    pass
                else:
                    self.open_edit_panel(self.shape_set[old_name])
        
        self.unsaved = True
        self.unsaved_callback()
        
        self.Refresh()