How to bind canvas events to tags?

Josef Wolf wrote:

    $canv->bind("foobar", '<1>' => sub {

       canvas.itembind(tag, '1', '%x %y') do |x, y|

In this case, we're binding mouse single click events (I assume that's the same as perl's '<1>') and we're grabbing the pointer location into block args x and y.

···

--
       vjoel : Joel VanderWerf : path berkeley edu : 510 665 3407

Yeah, that works. Thanks!

BTW: do you have a description of the "%x %y" formatting specification?

···

On Thu, Sep 10, 2009 at 10:27:54AM -0700, Joel VanderWerf wrote:

Josef Wolf wrote:

    $canv->bind("foobar", '<1>' => sub {

      canvas.itembind(tag, '1', '%x %y') do |x, y|

In this case, we're binding mouse single click events (I assume that's the
same as perl's '<1>') and we're grabbing the pointer location into block
args x and y.

Josef Wolf wrote:

BTW: do you have a description of the "%x %y" formatting specification?

http://www.tcl.tk/man/tcl8.4/TkCmd/bind.htm#M41

···

--
       vjoel : Joel VanderWerf : path berkeley edu : 510 665 3407

Ough. Every time I think I get closer (remember? I am trying to implement
a zoomable canvas), I find there is a lot more work to do :-()

My first attempt to adopt event handling to the zoom operation was like
this:

  class TkEvent::Event
    # FIXME: need a better name than "nonzoomed". Recommendations?
    attr_accessor :x_nonzoomed, :y_nonzoomed
  end

  class TkZoomCanvas < TkCanvas
    def itembind(tag, ev, cb)
      super(tag, ev, proc{ |e|
              if e.x then e.x_nonzoomed = e.x/@zoom ; end
              if e.y then e.y_nonzoomed = e.y/@zoom ; end
              cb.call e
            })
    end

    def bind(ev, cb)
      # looks similar to itembind
    end
  end

Now, itembind (and bind?) seem to accept various forms:

  1. canvas.itembind("mytag", "1", proc{|e| ... })
      This form works fine with above code

  2. canvas.itembind("mytag", "1", "%x %y") {|x, y| ... }
      In this case, I will have to build the argument list. Seems to be a
      tedious task. Maybe there already exist a Tk method I could use to
      build that list?

Are this the only two existing forms?

···

On Sat, Sep 12, 2009 at 02:23:30AM +0900, Joel VanderWerf wrote:

Josef Wolf wrote:

BTW: do you have a description of the "%x %y" formatting specification?

Tk Built-In Commands - bind manual page

Josef Wolf wrote:

Ough. Every time I think I get closer (remember? I am trying to implement
a zoomable canvas), I find there is a lot more work to do :-()

Do you need to exactly replicate the TkCanvas interface in the zoomable canvas? Why not get creative and develop a class with a new (possibly better) interface? (And maybe use delegation rather than inheritance.) The TkCanvas interface is close to Tcl/Tk's canvas, which is great for documentation and porting, but it's not necessarily very ruby-like.

···

--
       vjoel : Joel VanderWerf : path berkeley edu : 510 665 3407

Thanks for your suggestion (and sorry for my late reply).

Well, as you might have already guessed, I'm very new to ruby, so I don't
yet have a good feeling about what is "very ruby like". I'd love to see
suggestions.

For the particular topic about the binding, after some thought, I've come
to the conclusion that extending the format string is not the best idea.
The format string consists of one-letter formats. I'd pollute this
one-letter-namespace if I would introduce my own letters. Thus, I decided
to let the base class do the job if a string is passed.

Attached is my current implementation of the zoomable+scrollable canvas.
It works fine, AFAICS. I'd love to hear any suggestions about how to make
it more ruby-like.

#!/usr/bin/ruby

require 'tk'

class TkEvent::Event
  attr_accessor :x_nonzoom, :y_nonzoom
end

class TkScrolledCanvas < TkCanvas
  include TkComposite
  attr_reader :zoom

  def initialize_composite(keys={})
    @zoom = 1.0

    @h_scr = TkScrollbar.new(@frame)
    @v_scr = TkScrollbar.new(@frame)

    @canvas = TkCanvas.new(@frame)
    @path = @canvas.path

    @canvas.xscrollbar(@h_scr)
    @canvas.yscrollbar(@v_scr)

    TkGrid.rowconfigure(@frame, 0, :weight=>1, :minsize=>0)
    TkGrid.columnconfigure(@frame, 0, :weight=>1, :minsize=>0)

    @canvas.grid(:row=>0, :column=>0, :sticky=>'news')
    @h_scr.grid(:row=>1, :column=>0, :sticky=>'ew')
    @v_scr.grid(:row=>0, :column=>1, :sticky=>'ns')

    delegate('DEFAULT', @canvas)
    delegate('background', @frame, @h_scr, @v_scr)
    delegate('activeforeground', @h_scr, @v_scr)
    delegate('troughcolor', @h_scr, @v_scr)
    delegate('repeatdelay', @h_scr, @v_scr)
    delegate('repeatinterval', @h_scr, @v_scr)
    delegate('borderwidth', @frame)
    delegate('relief', @frame)

    delegate_alias('canvasborderwidth', 'borderwidth', @canvas)
    delegate_alias('canvasrelief', 'relief', @canvas)

    delegate_alias('scrollbarborderwidth', 'borderwidth', @h_scr, @v_scr)
    delegate_alias('scrollbarrelief', 'relief', @h_scr, @v_scr)

    configure(keys) unless keys.empty?
  end

  def zoom_by zf
    zf = Float(zf)
    @zoom *= zf
         
    vf = (1 - 1/zf) / 2
         
    x0, x1 = xview ; xf = x0 + vf * (x1-x0)
    y0, y1 = yview ; yf = y0 + vf * (y1-y0)
   
    scale 'all', 0, 0, zf, zf
    configure :scrollregion => bbox("all")
   
    xview "moveto", xf
    yview "moveto", yf
  end

  def zoom_to z
    zoom_by(z/@zoom)
  end

  def bind(ev, cb)
    if cb.class == String
      super
    else
      super(ev, proc{ |e| process_event(e, cb) })
    end
  end

  def itembind(tag, ev, cb)
    if cb.class == String
      super
    else
      super(tag, ev, proc{ |e| process_event(e, cb) })
    end
  end

  def coords(tag, *args)
    newargs = adjust_coords(@zoom, args)
    ret = super(tag, *newargs)
    return ret unless ret.class == Array
    ret.collect { |v| v / @zoom }
  end

  def move(tag, x, y)
    super(tag, x*@zoom, y*@zoom)
  end

  def create(type, *args)
    newargs = adjust_coords(@zoom, args)
    super(type, *newargs)
  end

  private

  def process_event(e, cb)
    if e.x then e.x_nonzoom=e.x/@zoom ; end
    if e.y then e.y_nonzoom=e.y/@zoom ; end
    cb.call e
  end

  def adjust_coords(mul, args)
    args.collect do |arg|
      arg.class == Array ? arg.collect { |v| v * mul } : arg
    end
  end
end

class TkcItem
  alias orig_initialize initialize

  def initialize(parent, *args)
    if parent.class == TkScrolledCanvas
      zoom = parent.zoom
      newargs = args.collect do |arg|
        arg.class == Array ? arg.collect { |v| v * zoom } : arg
      end
    else
      newargs = args
    end
    orig_initialize parent, *newargs
  end

  def bind(ev, cb)
    super(ev, proc{ |e|
            if @parent.class == TkScrolledCanvas
              zoom = @parent.zoom
              if e.x then e.x_nonzoom=e.x/zoom ; end
              if e.y then e.y_nonzoom=e.y/zoom ; end
              cb.call e
            end
          })
  end
end

···

On Mon, Sep 14, 2009 at 03:41:34AM +0900, Joel VanderWerf wrote:

Josef Wolf wrote:

Ough. Every time I think I get closer (remember? I am trying to implement
a zoomable canvas), I find there is a lot more work to do :-()

Do you need to exactly replicate the TkCanvas interface in the zoomable
canvas? Why not get creative and develop a class with a new (possibly
better) interface? (And maybe use delegation rather than inheritance.) The
TkCanvas interface is close to Tcl/Tk's canvas, which is great for
documentation and porting, but it's not necessarily very ruby-like.