Skip to content
Snippets Groups Projects
canvas.rb 14.7 KiB
Newer Older
  • Learn to ignore specific revisions
  • module Fox
      #
      # The Canvas module defines a framework similar to that provided by Tk's Canvas
      # widget (and subsequent improvements, such as GNOME's Canvas and wxWindows'
      # Object Graphics Library).
      #
      # Links
      # =====
      #
      # Tk's Canvas Widget
      #     http://starship.python.net/crew/fredrik/tkmanual/canvas.html
      #     http://www.dci.clrc.ac.uk/Publications/Cookbook/chap4.html
      #
      # GNOME's Canvas Widget
      #     http://developer.gnome.org/doc/whitepapers/canvas/canvas.html
      #
      module Canvas
    
        class CanvasError < Exception
        end
    
        class Shape
    
          attr_accessor :x, :y, :foreground, :target, :selector
    
          def initialize(x, y)
    
    Lars Kanis's avatar
    Lars Kanis committed
            @enabled = true
            @visible = true
            @selected = false
            @draggable = false
            @x = x
            @y = y
            @foreground = FXRGB(0, 0, 0)
            @target = nil
            @selector = 0
    
          end
    
          # Return the bounding box for this shape
          def bounds
    
    Lars Kanis's avatar
    Lars Kanis committed
            FXRectangle.new(x, y, width, height)
    
          end
    
          # Hit test
          def hit?(xpos, ypos)
    
    Lars Kanis's avatar
    Lars Kanis committed
            (xpos >= x) && (xpos < x+width) && (ypos >= y) && (ypos < y+height)
    
          end
    
          # Move shape to specified position
          def move(x, y)
    
    Lars Kanis's avatar
    Lars Kanis committed
            @x, @y = x, y
    
          end
    
          # Resize shape to specified width and height
          def resize(w, h)
          end
    
          # Move and resize the shape
          def position(x, y, w, h)
    
    Lars Kanis's avatar
    Lars Kanis committed
            move(x, y)
            resize(w, h)
    
          end
    
          # Enable this shape
          def enable
    
    Lars Kanis's avatar
    Lars Kanis committed
            @enabled = true
    
          end
    
          # Disable this shape
          def disable
    
    Lars Kanis's avatar
    Lars Kanis committed
            @enabled = false
    
          end
    
          # Is this shape enabled?
          def enabled?
    
    Lars Kanis's avatar
    Lars Kanis committed
            @enabled
    
          end
    
          # Show this shape
          def show
    
    Lars Kanis's avatar
    Lars Kanis committed
            @visible = true
    
          end
    
          # Hide this shape
          def hide
    
    Lars Kanis's avatar
    Lars Kanis committed
            @visible = false
    
          end
    
          # Is this shape visible?
          def visible?
    
    Lars Kanis's avatar
    Lars Kanis committed
            @visible
    
          end
    
          # Select this shape
          def select
    
    Lars Kanis's avatar
    Lars Kanis committed
            @selected = true
    
          end
    
          # Deselect this shape
          def deselect
    
    Lars Kanis's avatar
    Lars Kanis committed
            @selected = false
    
          end
    
          # Is this shape selected?
          def selected?
    
    Lars Kanis's avatar
    Lars Kanis committed
            @selected
    
          end
    
          # Set this shape's draggability
          def draggable=(d)
    
    Lars Kanis's avatar
    Lars Kanis committed
            @draggable = d
    
          end
    
          # Is this shape draggable?
          def draggable?
    
    Lars Kanis's avatar
    Lars Kanis committed
            @draggable
    
          end
    
          # Draw this shape into the specificed device context
          def draw(dc)
          end
    
          # Draws outline
          def drawOutline(dc, x, y, w, h)
    
    Lars Kanis's avatar
    Lars Kanis committed
            points = []
            points << FXPoint.new(x - 0.5*w, y - 0.5*h)
            points << FXPoint.new(x + 0.5*w, y)
            points << FXPoint.new(x + 0.5*w, y + 0.5*h)
            points << FXPoint.new(x - 0.5*w, y + 0.5*h)
            points << points[0]
            dc.drawLines(points)
    
          end
    
          # Default: make 6 control points
          def makeControlPoints
          end
        end
    
        class ShapeGroup
    
          include Enumerable
    
          # Initialize this shape group
          def initialize
    
    Lars Kanis's avatar
    Lars Kanis committed
            @shapes = []
    
          end
    
          # Does the group contain this shape?
          def include?(shape)
    
    Lars Kanis's avatar
    Lars Kanis committed
            @shapes.include?(shape)
    
          end
    
          # Add a shape to this group
          def addShape(shape)
    
    Lars Kanis's avatar
    Lars Kanis committed
            @shapes << shape
    
          end
    
          # Remove a shape from this group
          def removeShape(shape)
    
    Lars Kanis's avatar
    Lars Kanis committed
            @shapes.remove(shape)
    
    Lars Kanis's avatar
    Lars Kanis committed
            @shapes.each { |shape| yield shape }
    
          end
    
          def reverse_each
    
    Lars Kanis's avatar
    Lars Kanis committed
            @shapes.reverse_each { |shape| yield shape }
    
          end
        end
    
        class LineShape < Shape
    
          attr_accessor :lineWidth, :lineCap, :lineJoin, :lineStyle
          attr_accessor :x1, :y1, :x2, :y2
    
          def initialize(x1, y1, x2, y2)
    
    Lars Kanis's avatar
    Lars Kanis committed
            super(x1, y1)
            @x1, @y1, @x2, @y2 = x1, y1, x2, y2
            @lineWidth = 1
            @lineCap = CAP_NOT_LAST
            @lineJoin = JOIN_MITER
            @lineStyle = LINE_SOLID
    
    Lars Kanis's avatar
    Lars Kanis committed
            # Save old values
            oldForeground = dc.foreground
            oldLineWidth = dc.lineWidth
            oldLineCap = dc.lineCap
            oldLineJoin = dc.lineJoin
            oldLineStyle = dc.lineStyle
    
            # Set properties for this line
            dc.foreground = foreground
            dc.lineWidth = lineWidth
            dc.lineCap = lineCap
            dc.lineJoin = lineJoin
            dc.lineStyle = lineStyle
    
            # Draw the line
            dc.drawLine(x1, y1, x2, y2)
    
            # Restore old properties
            dc.lineWidth = oldLineWidth
            dc.lineCap = oldLineCap
            dc.lineJoin = oldLineJoin
            dc.lineStyle = oldLineStyle
            dc.foreground = oldForeground
    
          end
        end
    
        class RectangleShape < Shape
    
          attr_accessor :width, :height
    
          def initialize(x, y, w, h)
    
    Lars Kanis's avatar
    Lars Kanis committed
            super(x, y)
            @width = w
            @height = h
    
    Lars Kanis's avatar
    Lars Kanis committed
            oldForeground = dc.foreground
            dc.foreground = foreground
            dc.drawRectangle(x, y, width, height)
            dc.foreground = oldForeground
    
          end
        end
    
        class TextShape < RectangleShape
    
          attr_reader :font, :text
    
          def initialize(x, y, w, h, text=nil)
    
    Lars Kanis's avatar
    Lars Kanis committed
            super(x, y, w, h)
            @text = text
            @font = FXApp.instance.normalFont
    
    Lars Kanis's avatar
    Lars Kanis committed
            super(dc)
            oldForeground = dc.foreground
            oldTextFont = dc.font
            dc.font = @font
            dc.drawImageText(x, y, text)
            dc.font = oldTextFont if oldTextFont
            dc.foreground = oldForeground
    
          end
        end
    
        class CircleShape < Shape
    
          attr_accessor :radius
    
          def initialize(x, y, radius)
    
    Lars Kanis's avatar
    Lars Kanis committed
            super(x, y)
            @radius = radius
    
    Lars Kanis's avatar
    Lars Kanis committed
            2*radius
    
    Lars Kanis's avatar
    Lars Kanis committed
            2*radius
    
    Lars Kanis's avatar
    Lars Kanis committed
            oldForeground = dc.foreground
            oldLineWidth = dc.lineWidth
            dc.foreground = foreground
            dc.lineWidth = 5 if selected?
            dc.drawArc(x, y, width, height,      0, 64*180)
            dc.drawArc(x, y, width, height, 64*180, 64*360)
            dc.foreground = oldForeground
            dc.lineWidth = oldLineWidth
    
          end
        end
    
        class PolygonShape < Shape
        end
    
        class ImageShape < Shape
    
          attr_accessor :image
    
          def initialize(x, y, image)
    
    Lars Kanis's avatar
    Lars Kanis committed
            @image = image
    
    Lars Kanis's avatar
    Lars Kanis committed
            dc.drawImage(image)
    
          end
        end
    
        # Base class for canvas selection policies
        class SelectionPolicy
          def initialize(canvas)
    
    Lars Kanis's avatar
    Lars Kanis committed
            @canvas = canvas
    
          end
    
          def selectShape(shape, notify)
    
    Lars Kanis's avatar
    Lars Kanis committed
            unless shape.selected?
    
              shape.select
    
    Lars Kanis's avatar
    Lars Kanis committed
              @canvas.updateShape(shape)
              if notify && (@canvas.target != nil)
                @canvas.target.handle(@canvas, MKUINT(@canvas.message, SEL_SELECTED), shape)
              end
            end
    
          end
    
          def deselectShape(shape, notify)
    
    Lars Kanis's avatar
    Lars Kanis committed
            if shape.selected?
    
              shape.deselect
    
    Lars Kanis's avatar
    Lars Kanis committed
              @canvas.updateShape(shape)
              if notify && (@canvas.target != nil)
                @canvas.target.handle(@canvas, MKUINT(@canvas.message, SEL_DESELECTED), shape)
              end
            end
    
          end
        end
    
        # Single shape selected at one time
        class SingleSelectionPolicy < SelectionPolicy
          def initialize(canvas)
    
    Lars Kanis's avatar
    Lars Kanis committed
            super
    
          end
    
          def selectShape(shape, notify)
    
    Lars Kanis's avatar
    Lars Kanis committed
            unless shape.selected?
    
              @canvas.killSelection(notify)
    
    Lars Kanis's avatar
    Lars Kanis committed
            end
            super
    
          end
        end
    
        class ShapeCanvas < FXCanvas
    
          # Window state flags
          FLAG_SHOWN        = 0x00000001 # Is shown
          FLAG_ENABLED      = 0x00000002 # Able to receive input
          FLAG_UPDATE       = 0x00000004 # Is subject to GUI update
          FLAG_DROPTARGET   = 0x00000008 # Drop target
          FLAG_FOCUSED      = 0x00000010 # Has focus
          FLAG_DIRTY        = 0x00000020 # Needs layout
          FLAG_RECALC       = 0x00000040 # Needs recalculation
          FLAG_TIP          = 0x00000080 # Show tip
          FLAG_HELP         = 0x00000100 # Show help
          FLAG_DEFAULT      = 0x00000200 # Default widget
          FLAG_INITIAL      = 0x00000400 # Initial widget
          FLAG_SHELL        = 0x00000800 # Shell window
          FLAG_ACTIVE       = 0x00001000 # Window is active
          FLAG_PRESSED      = 0x00002000 # Button has been pressed
          FLAG_KEY          = 0x00004000 # Keyboard key pressed
          FLAG_CARET        = 0x00008000 # Caret is on
          FLAG_CHANGED      = 0x00010000 # Window data changed
          FLAG_LASSO        = 0x00020000 # Lasso mode
          FLAG_TRYDRAG      = 0x00040000 # Tentative drag mode
          FLAG_DODRAG       = 0x00080000 # Doing drag mode
          FLAG_SCROLLINSIDE = 0x00100000 # Scroll only when inside
          FLAG_SCROLLING    = 0x00200000 # Right mouse scrolling
    
          include Responder
    
          attr_accessor :scene
    
          def initialize(p, tgt=nil, sel=0, opts=FRAME_NORMAL, x=0, y=0, w=0, h=0)
    
    Lars Kanis's avatar
    Lars Kanis committed
            # Initialize base class
            super(p, tgt, sel, opts, x, y, w, h)
    
    Lars Kanis's avatar
    Lars Kanis committed
            # Start with an empty group
            @scene = ShapeGroup.new
    
    Lars Kanis's avatar
    Lars Kanis committed
            # Selection policy
            @selectionPolicy = SingleSelectionPolicy.new(self)
    
    Lars Kanis's avatar
    Lars Kanis committed
            @flags = 0
    
    Lars Kanis's avatar
    Lars Kanis committed
            # Map
            FXMAPFUNC(SEL_PAINT, 0, "onPaint")
            FXMAPFUNC(SEL_MOTION, 0, "onMotion")
            FXMAPFUNC(SEL_LEFTBUTTONPRESS, 0, "onLeftBtnPress")
            FXMAPFUNC(SEL_LEFTBUTTONRELEASE, 0, "onLeftBtnRelease")
            FXMAPFUNC(SEL_CLICKED,0,"onClicked")
            FXMAPFUNC(SEL_DOUBLECLICKED,0,"onDoubleClicked")
            FXMAPFUNC(SEL_TRIPLECLICKED,0,"onTripleClicked")
            FXMAPFUNC(SEL_COMMAND,0,"onCommand")
    
          end
    
          # Find the shape of the least depth containing this point
          def findShape(x, y)
    
    Lars Kanis's avatar
    Lars Kanis committed
            @scene.reverse_each do |shape|
    
              return shape if shape.hit?(x, y)
    
    Lars Kanis's avatar
    Lars Kanis committed
            end
            nil
    
          end
    
          # Repaint
          def updateShape(shape)
    
    Lars Kanis's avatar
    Lars Kanis committed
            if @scene.include?(shape)
    
    Lars Kanis's avatar
    Lars Kanis committed
            else
    
              raise CanvasError
    
    Lars Kanis's avatar
    Lars Kanis committed
            end
    
          end
    
          # Enable one shape
          def enableShape(shape)
    
    Lars Kanis's avatar
    Lars Kanis committed
            if @scene.include?(shape)
    
              unless shape.enabled?
    
    Lars Kanis's avatar
    Lars Kanis committed
                shape.enable
                updateShape(shape)
              end
            else
    
              raise CanvasError
    
    Lars Kanis's avatar
    Lars Kanis committed
            end
    
          end
    
          # Disable one shape
          def disableShape(shape)
    
    Lars Kanis's avatar
    Lars Kanis committed
            if @scene.include?(shape)
    
              if shape.enabled?
    
    Lars Kanis's avatar
    Lars Kanis committed
                shape.disable
                updateShape(shape)
              end
            else
    
              raise CanvasError
    
    Lars Kanis's avatar
    Lars Kanis committed
            end
    
          end
    
          # Select one shape
          def selectShape(shape, notify=false)
    
    Lars Kanis's avatar
    Lars Kanis committed
            if @scene.include?(shape)
    
              @selectionPolicy.selectShape(shape, notify)
    
    Lars Kanis's avatar
    Lars Kanis committed
            else
    
              raise CanvasError
    
    Lars Kanis's avatar
    Lars Kanis committed
            end
    
          end
    
          # Deselect one shape
          def deselectShape(shape, notify=false)
    
    Lars Kanis's avatar
    Lars Kanis committed
            if @scene.include?(shape)
    
              @selectionPolicy.deselectShape(shape, notify)
    
    Lars Kanis's avatar
    Lars Kanis committed
            else
    
              raise CanvasError
    
    Lars Kanis's avatar
    Lars Kanis committed
            end
    
          end
    
          # Kill selection
          def killSelection(notify)
    
    Lars Kanis's avatar
    Lars Kanis committed
            changes = false
            @scene.each do |shape|
    
              if shape.selected?
    
    Lars Kanis's avatar
    Lars Kanis committed
                shape.deselect
                updateShape(shape)
                changes = true
                if notify && (target != nil)
                  target.handle(self, MKUINT(message, SEL_DESELECTED), shape)
                end
              end
            end
            changes
    
          end
    
          # Paint
          def onPaint(sender, sel, evt)
    
    Lars Kanis's avatar
    Lars Kanis committed
            dc = FXDCWindow.new(self, evt)
            dc.foreground = backColor
            dc.fillRectangle(evt.rect.x, evt.rect.y, evt.rect.w, evt.rect.h)
            @scene.each do |shape|
    
              shape.draw(dc)
    
    Lars Kanis's avatar
    Lars Kanis committed
            end
            dc.end
            return 1
    
          end
    
          # Motion
          def onMotion(sender, sel, evt)
    
    Lars Kanis's avatar
    Lars Kanis committed
            # Drag and drop mode
            if (@flags & FLAG_DODRAG) != 0
    
              handle(self, MKUINT(0, SEL_DRAGGED), evt)
    
    Lars Kanis's avatar
    Lars Kanis committed
              return 1
            end
    
    Lars Kanis's avatar
    Lars Kanis committed
            # Tentative drag and drop
            if (@flags & FLAG_TRYDRAG) != 0
    
              if evt.moved?
    
    Lars Kanis's avatar
    Lars Kanis committed
                @flags &= ~FLAG_TRYDRAG
                if handle(this, MKUINT(0, SEL_BEGINDRAG), evt) != 0
                  @flags |= FLAG_DODRAG
                end
              end
              return 1
            end
    
          end
    
          # Left button press
          def onLeftBtnPress(sender, sel, evt)
    
    Lars Kanis's avatar
    Lars Kanis committed
            handle(self, MKUINT(0, SEL_FOCUS_SELF), evt)
            if enabled?
    
    Lars Kanis's avatar
    Lars Kanis committed
              flags &= ~FLAG_UPDATE
    
    Lars Kanis's avatar
    Lars Kanis committed
              # Give target the first chance at handling this
              return 1 if target && (target.handle(self, MKUINT(message, SEL_LEFTBUTTONPRESS), evt) != 0)
    
    Lars Kanis's avatar
    Lars Kanis committed
              # Locate shape
              shape = findShape(evt.win_x, evt.win_y)
    
    Lars Kanis's avatar
    Lars Kanis committed
              # No shape here
              if shape.nil?
                return 1
              end
    
    Lars Kanis's avatar
    Lars Kanis committed
              # Change current shape
              @currentShape = shape
    
    Lars Kanis's avatar
    Lars Kanis committed
              # Change item selection
              if shape.enabled? && !shape.selected?
                selectShape(shape, true)
              end
    
    Lars Kanis's avatar
    Lars Kanis committed
              # Are we dragging?
              if shape.selected? && shape.draggable?
                flags |= FLAG_TRYDRAG
              end
    
    Lars Kanis's avatar
    Lars Kanis committed
              flags |= FLAG_PRESSED
              return 1
            end
            return 0
    
          end
    
          # Left button release
          def onLeftBtnRelease(sender, sel, evt)
    
    Lars Kanis's avatar
    Lars Kanis committed
            flg = @flags
            if enabled?
    
    Lars Kanis's avatar
    Lars Kanis committed
              @flags |= FLAG_UPDATE
              @flags &= ~(FLAG_PRESSED|FLAG_TRYDRAG|FLAG_LASSO|FLAG_DODRAG)
    
              # First chance callback
              return 1 if target && target.handle(self, MKUINT(message, SEL_LEFTBUTTONRELEASE), evt) != 0
    
              # Was dragging
              if (flg & FLAG_DODRAG) != 0
                handle(self, MKUINT(0, SEL_ENDDRAG), evt)
                return 1
              end
    
              # Must have pressed
              if (flg & FLAG_PRESSED) != 0
                # Change selection
                if @currentShape && @currentShape.enabled?
                  deselectShape(@currentShape, true)
                end
    
                # Generate clicked callbacks
                if evt.click_count == 1
                  handle(self, MKUINT(0, SEL_CLICKED), @currentShape)
                elsif evt.click_count == 2
                  handle(self, MKUINT(0, SEL_DOUBLECLICKED), @currentShape)
                elseif evt.click_count == 3
                  handle(self, MKUINT(0, SEL_TRIPLECLICKED), @currentShape)
                end
    
                # Generate command callback only when clicked on item
                if @currentShape && @currentShape.enabled?
                  handle(self, MKUINT(0, SEL_COMMAND), @currentShape)
                end
                return 1
              end
              return 0
            end
    
          end
    
          # Command message
          def onCommand(sender, sel, ptr)
    
    Lars Kanis's avatar
    Lars Kanis committed
            return target && target.handle(self, MKUINT(message, SEL_COMMAND), ptr)
    
          end
    
          # Clicked on canvas
          def onClicked(sender, sel, ptr)
    
    Lars Kanis's avatar
    Lars Kanis committed
            return target && target.handle(self, MKUINT(message, SEL_CLICKED), ptr)
    
          end
    
          # Double-clicked on canvas
          def onDoubleClicked(sender, sel, ptr)
    
    Lars Kanis's avatar
    Lars Kanis committed
            return target && target.handle(self, MKUINT(message, SEL_DOUBLECLICKED), ptr)
    
          end
    
          # Triple-clicked on canvas
          def onTripleClicked(sender, sel, ptr)
    
    Lars Kanis's avatar
    Lars Kanis committed
            return target && target.handle(self, MKUINT(message, SEL_TRIPLECLICKED), ptr)