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) @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 FXRectangle.new(x, y, width, height) end # Hit test def hit?(xpos, ypos) (xpos >= x) && (xpos < x+width) && (ypos >= y) && (ypos < y+height) end # Move shape to specified position def move(x, y) @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) move(x, y) resize(w, h) end # Enable this shape def enable @enabled = true end # Disable this shape def disable @enabled = false end # Is this shape enabled? def enabled? @enabled end # Show this shape def show @visible = true end # Hide this shape def hide @visible = false end # Is this shape visible? def visible? @visible end # Select this shape def select @selected = true end # Deselect this shape def deselect @selected = false end # Is this shape selected? def selected? @selected end # Set this shape's draggability def draggable=(d) @draggable = d end # Is this shape draggable? def draggable? @draggable end # Draw this shape into the specificed device context def draw(dc) end # Draws outline def drawOutline(dc, x, y, w, h) 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 @shapes = [] end # Does the group contain this shape? def include?(shape) @shapes.include?(shape) end # Add a shape to this group def addShape(shape) @shapes << shape end # Remove a shape from this group def removeShape(shape) @shapes.remove(shape) end def each @shapes.each { |shape| yield shape } end def reverse_each @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) super(x1, y1) @x1, @y1, @x2, @y2 = x1, y1, x2, y2 @lineWidth = 1 @lineCap = CAP_NOT_LAST @lineJoin = JOIN_MITER @lineStyle = LINE_SOLID end def width 0 end def height 0 end def draw(dc) # 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) super(x, y) @width = w @height = h end def draw(dc) 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) super(x, y, w, h) @text = text @font = FXApp.instance.normalFont end def draw(dc) 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) super(x, y) @radius = radius end def width 2*radius end def height 2*radius end def draw(dc) 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) @image = image end def draw(dc) dc.drawImage(image) end end # Base class for canvas selection policies class SelectionPolicy def initialize(canvas) @canvas = canvas end def selectShape(shape, notify) unless shape.selected? shape.select @canvas.updateShape(shape) if notify && (@canvas.target != nil) @canvas.target.handle(@canvas, Fox.MKUINT(@canvas.message, SEL_SELECTED), shape) end end end def deselectShape(shape, notify) if shape.selected? shape.deselect @canvas.updateShape(shape) if notify && (@canvas.target != nil) @canvas.target.handle(@canvas, Fox.MKUINT(@canvas.message, SEL_DESELECTED), shape) end end end end # Single shape selected at one time class SingleSelectionPolicy < SelectionPolicy def initialize(canvas) super end def selectShape(shape, notify) unless shape.selected? @canvas.killSelection(notify) 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) # Initialize base class super(p, tgt, sel, opts, x, y, w, h) # Start with an empty group @scene = ShapeGroup.new # Selection policy @selectionPolicy = SingleSelectionPolicy.new(self) @flags = 0 # 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) @scene.reverse_each do |shape| return shape if shape.hit?(x, y) end nil end # Repaint def updateShape(shape) if @scene.include?(shape) update else raise CanvasError end end # Enable one shape def enableShape(shape) if @scene.include?(shape) unless shape.enabled? shape.enable updateShape(shape) end else raise CanvasError end end # Disable one shape def disableShape(shape) if @scene.include?(shape) if shape.enabled? shape.disable updateShape(shape) end else raise CanvasError end end # Select one shape def selectShape(shape, notify=false) if @scene.include?(shape) @selectionPolicy.selectShape(shape, notify) else raise CanvasError end end # Deselect one shape def deselectShape(shape, notify=false) if @scene.include?(shape) @selectionPolicy.deselectShape(shape, notify) else raise CanvasError end end # Kill selection def killSelection(notify) changes = false @scene.each do |shape| if shape.selected? shape.deselect updateShape(shape) changes = true if notify && (target != nil) target.handle(self, Fox.MKUINT(message, SEL_DESELECTED), shape) end end end changes end # Paint def onPaint(sender, sel, evt) 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) end dc.end return 1 end # Motion def onMotion(sender, sel, evt) # Drag and drop mode if (@flags & FLAG_DODRAG) != 0 handle(self, Fox.MKUINT(0, SEL_DRAGGED), evt) return 1 end # Tentative drag and drop if (@flags & FLAG_TRYDRAG) != 0 if evt.moved? @flags &= ~FLAG_TRYDRAG if handle(this, Fox.MKUINT(0, SEL_BEGINDRAG), evt) != 0 @flags |= FLAG_DODRAG end end return 1 end end # Left button press def onLeftBtnPress(sender, sel, evt) handle(self, Fox.MKUINT(0, SEL_FOCUS_SELF), evt) if enabled? grab flags &= ~FLAG_UPDATE # Give target the first chance at handling this return 1 if target && (target.handle(self, Fox.MKUINT(message, SEL_LEFTBUTTONPRESS), evt) != 0) # Locate shape shape = findShape(evt.win_x, evt.win_y) # No shape here if shape.nil? return 1 end # Change current shape @currentShape = shape # Change item selection if shape.enabled? && !shape.selected? selectShape(shape, true) end # Are we dragging? if shape.selected? && shape.draggable? flags |= FLAG_TRYDRAG end flags |= FLAG_PRESSED return 1 end return 0 end # Left button release def onLeftBtnRelease(sender, sel, evt) flg = @flags if enabled? ungrab @flags |= FLAG_UPDATE @flags &= ~(FLAG_PRESSED|FLAG_TRYDRAG|FLAG_LASSO|FLAG_DODRAG) # First chance callback return 1 if target && target.handle(self, Fox.MKUINT(message, SEL_LEFTBUTTONRELEASE), evt) != 0 # Was dragging if (flg & FLAG_DODRAG) != 0 handle(self, Fox.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, Fox.MKUINT(0, SEL_CLICKED), @currentShape) elsif evt.click_count == 2 handle(self, Fox.MKUINT(0, SEL_DOUBLECLICKED), @currentShape) elseif evt.click_count == 3 handle(self, Fox.MKUINT(0, SEL_TRIPLECLICKED), @currentShape) end # Generate command callback only when clicked on item if @currentShape && @currentShape.enabled? handle(self, Fox.MKUINT(0, SEL_COMMAND), @currentShape) end return 1 end return 0 end end # Command message def onCommand(sender, sel, ptr) return target && target.handle(self, Fox.MKUINT(message, SEL_COMMAND), ptr) end # Clicked on canvas def onClicked(sender, sel, ptr) return target && target.handle(self, Fox.MKUINT(message, SEL_CLICKED), ptr) end # Double-clicked on canvas def onDoubleClicked(sender, sel, ptr) return target && target.handle(self, Fox.MKUINT(message, SEL_DOUBLECLICKED), ptr) end # Triple-clicked on canvas def onTripleClicked(sender, sel, ptr) return target && target.handle(self, Fox.MKUINT(message, SEL_TRIPLECLICKED), ptr) end end end end