Skip to content
Snippets Groups Projects
canvas.rb 14.7 KiB
Newer Older
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)