Fwd: (fwd) binding to a derived canvas widget

Steve Lidie <[email protected]>
Newsgroups gmane.comp.lang.perl.tk
Message-ID <[email protected]>
>
> Hi,
> Generally I avoid these problems, by not using objects and
> modules, but I wanted to try and put this in a module form. :-)

Great, properly packaged, a mega-widget "module" can be easily reused  
by the average Perl/Tk programmer. You spend the time and effort up  
front and we all take advantage of that later on :)

Hmm, orange smoke ... mumble, mumble ... installing File- 
Slurp-9999.12, File-Slurp-Tree, Number-Compare-0.01, Text-Glob-0.06,  
File-Find-Rule-0.28 .., ah, now I can begin testing! Okay, ready or  
not, let's talk about bindings, bindtags, mega-widgets (Scrolled, no  
less) and maybe more stuff.  This is just how I see things and what  
I'd likely do ...

>
> I'm working on a canvas based module to simulate the

Technically you are writing a derived mega-widget, in this case, you  
are extending the capabilities of the Canvas widget. You started with  
this code:

    package Tk::CanvasDirTree;
    use base  qw/Tk::Derived Tk::Canvas/;
    Construct Tk::Widget 'CanvasDirTree';

which just created a new CanvasDirTree widget, derived from a Canvas,  
that behaves *exactly* line a Canvas widget.  And in 3 lines of code,  
pretty amazing, I'd say.

>
> My problem is how to bind to the widget, from a main script with
> a Button-1 click.  Internally, in the object,  I use Canvasbind to
> bind Button-1, to activate the animation, and select a sub-directory.

That works because you wrote this in Populate(), the place where  
CanvasDirTree mega-widgets are instantiated:

    $self->{'can'}->CanvasBind( '<Button-1>' => [\&pick_one, $self] );

You have bound B1 to the actual Canvas widget (more on the apparent  
need for $self->{'can'} as we proceed).  So far, so good, when the  
user clicks B1 they invoke your callback code, the behavior you added  
that extends the capabilities of the standard Canvas widget.

>
> But when I try to bind Button-1  from the main script, to get the
> selected directory, it only works if I bind from $mw, and not from
> the $ztree. This presents problems, because I'm binding to everything
> in the mainwindow, and just shouldn't be done that way.

Correct, the act of clicking B1 sends the event to the window that  
the pointer is in. When you actually made the binding, in your main  
code outside the mega-widget, like this:

    $ztree->bind( '<Button-1>', sub{ ... } );

because $ztree ISA Canvas (you're writing a derived mega-widget), the  
Canvas bind() method was invoked instead of the "real" bind()  
method.  In Tcl, the bind command cannot be confused with the Canvas  
widget's bind subcommand.

Enter Tk::bind.  Tk::bind was externalized precisely to disambiguate  
the two bind() methods, so you could create bindings on Canvas  
widgets.  Thus, if you write $canvas->bind() you're calling the  
Canvas bind() method, but if you write $canvas->Tk::bind() you're  
calling the normal bind() method that every other widget uses to  
create bindings. Later on in Perl/Tk's development the Canvas method  
CanvasBind() was added as syntactic sugar for Tk::bind.

The command Tk::bindDump is really helpful in debugging binding  
oddities.  Strangely enough, I found a bug in it when examining your  
mega-widget: I needed to use Tk::bind in one place!  Oh, and it  
doesn't dump Canvas item bindings, that would be a nice addition if  
anyone is interested. The corrected version is at http:// 
www.lehigh.edu/~sol0/ptk/bindDump.pm.

>
> So run this script in a directory with some subdirs in it. I have it
> setup to work (almost) the way I want, but I'm binding Button-1
> to $mw, and I want to bind it only to $ztree.  (The bind statements
> are at the bottom, about 10 lines from the end).
>
> It has to be something simple, but I've already tried a bunch of
> different things, and figure that I am missing the trick on how to
> handle multiple binding.  I've read perldoc Tk::bind, and it said that
> if multiple bindings are made to say Button-1, the callback from the
> class will be called first, then the one from main. BUT the main isn't
> being called in this example.....that is the problem.

So, how do we fix the problem?  A first stab might be to use Tk::bind  
in the main code, and while that actually creates a binding on the  
Tk::CanvasDirTree object, it's not very useful because the only  
widget visible to click on is the Canvas! (The Delegates() method  
might be useful, but that's another discussion.)  And if a user went  
digging around the object and found the actual Canvas reference and  
then set her B1 binding to the Canvas, it would clobber the mega- 
widget's B1 binding.

We want the user to do the "natural" thing and be able to place  
standard bindings on the mega-widget, but that means we (well, you,  
the mega-widget developer) have to work a little bit harder.

The first thing you want to do (probably) is override the Tk::Canvas  
bind() method - that your derived Tk::CanvasDirTree mega-widget  
inherits -  with one that calls Tk::bind (or CanvasBind), like this:

    sub bind {
        my( $self, @args ) = @_;
        $self->{'real_can'}->Tk::bind( @args );
    }

Notice the use of self->{'real_can'}, more on that later.  I  
mentioned I also had something to say about $self->{'can'}.  The  
topics are related! The important point to realize is that this  
intercepts the user's bind() call on the Tk::CanvasDirTree widget and  
redirects it to the Canvas widget, rather than attempting to create a  
binding on a Canvas item.

Implicit in all this is the realization that, from a user of this  
mega-widget's perspective, a CanvasDirTree widget is in fact not a  
Canvas, and they have no need to do structured graphics using it.   
Thus, they will not be creating Canvas items, nor placing bindings on  
those items. Hey, perhaps a name change is required?

So, where do we stand?  Well, the user can now bind to your new mega- 
widget in the standard manner and it will work:

    $ztree->bind( '<Button-1>', sub{..));

But the class B1 binding, below, that you created, no longer works  
because it's been replaced:

    $self->{'can'}->CanvasBind( '<Button-1>' => [\&pick_one, $self] );

Here's that little bit of extra work you need to do: use binding  
tags.  Create a new, class-private tag, and bind B1 to the new tag.  
Leave the "standard tag(s)" to the user.

Here's how to do it (in Populate):

    $self->{'real_can'} = $self->{'can'}->Subwidget('scrolled');  
######## more on this later
    my( @bindtags ) = $self->{'real_can'}->bindtags;
    $self->{'real_can'}->bindtags( [ @bindtags, 'CanvasDirTree-B1' ] );
    $self->{'real_can'}->CanvasBind( 'CanvasDirTree-B1', '<Button-1>'  
=> [\&pick_one, $self] );

What the above code does is simply add a new bindtag named  
'CanvasDirTree-B1' to the list of bindtags associated with the widget  
(the "real canvas"), then sets a B1 button binding.  The result?   
When B1 is clicked over the Canvas both the class callback and user  
callback are executed, simply because Tk traverses the bindtags list,  
executing callbacks in bindtags order.

So, problem solved, with 8-ish lines of code, and a boatload of  
internals knowledge :)  My explanation was fast-paced, I hope it made  
sense.

But, there's one more thing .... I have suggestions for improving the  
fine structure of your mega-widget. I can simplify it and bring it  
inline with Perl/Tk conventions, if you do not mind.


------------------ Intermission ------------------


> package CanvasDirTree;
> use base  qw/Tk::Derived Tk::Canvas/;
> Construct Tk::Widget 'CanvasDirTree';

Your prolog, above, is fine, as we've seen.

Below, we come to various methods that the mega-widget writer may  
provide, that will override place-holders in a base class. We'll look  
at only three:  ClassInit(), SetBindtags(), and Populate().

>
> sub ClassInit
> {
>     my ($class, $mw) = @_;
>     $class->SUPER::ClassInit($mw);
> }

No problem with the above. So what if it doesn't do anything now, it  
might in the future.  It initializes the class (Tk::CanvasDirTree),  
and is called exactly once.

>
> sub SetBindtags {
>     my($self) = @_;
>     $self->SUPER::SetBindtags;
> }

Another place holder that does nothing.  Yet, we have a need to set  
bindtags.  Unlike ClassInit(), this method is called as each object  
(Tk::CanvasDirTree) is instantiated.  We'll need to make a few  
changes to the original CanvasDirTree code to take advantage of this.

>
> ######################################################
> sub Populate {
>    my ($self, $args) = @_;

Subroutine/method Populate() adds behavior and/or widgets that extend  
the basic Canvas.  By adding behavior - bindings and/or other widgets  
- it makes a CanvasDirTree what it is.

>
>   #-------------------------------------------------------------------
>    #take care of args which don't belong to the SUPER, see Tk::Derived
>    foreach my $extra ('backimage','imx','imy','dir','font','indfilla',
>                        
> 'indfilln','fontcolorn','fontcolora','scrollbars')
> {
>        my $xtra_arg = delete $args->{ "-$extra" };  #delete and read
> same time
>      if( defined $xtra_arg ) { $self->{$extra} = $xtra_arg }
>    }
>    #-----------------------------------------------------------------
>     #set some defaults
>     $self->{'indfilla'} ||= 'red';
>     $self->{'indfilln'} ||= 'pink';
>     $self->{'fontcolorn'} ||= 'black';
>     $self->{'fontcolora'} ||= 'red';
>     $self->{'scrollbars'} ||= 'osw';
>     $self->{'backimage'} ||= '';
>     $self->{'bimage'} ||= '';
>     $self->{'imx'} ||= 0;
>     $self->{'imy'} ||= 0;

For the above, I'd use ConfigSpecs(), it's the standard way of  
handling options.

>
>    $self->{'can'} = $self->Scrolled('Canvas',
>                     -scrollbars => $self->{'scrollbars'},
>                 );
>
>    $self->{'real_can'} = $self->{'can'}->Subwidget('scrolled');


OK, here's where I'd do things very differently.  First, because  
Tk::CanvasDirTree ISA Canvas, by definition $self ISA Canvas.  It's  
important to realize that you do not need to explicitly create a new  
Canvas - you already are a Canvas! At this point in mega-widget  
creation you are a Canvas and can invoke any Canvas method you'd like  
upon $self. Remember that 3-line code preamble I showed?  That's the  
magic that makes all this possible!

What we have now is a second - Scrolled! - Canvas inside another (the  
original, derived) Canvas. And now let the complications begin.

The Scrolled method is the coolest thing around, it automatically  
scrolls widgets in the X and Y directions.  Scrollbars can be  
entirely optional, popping into existence only when needed, or always  
present.  Scrolled works by first creating a Frame, and packing  
within the Frame the desired widget and its requisite Scrollbars. The  
widget reference returned by the Scrolled() method points to the  
enclosing Frame, not the scrolled widget.

So a Tk::CanvasDirTree widget hierarchy, as currently defined, looks  
something like this:

Tk::CanvasDirTree EQU Tk::Canvas
   Tk::Frame
     Tk::Canvas

So we have a Canvas underneath a Frame enclosing another Canvas  
(ignoring any Scrollbars). And now we come back to $self->{can}  
versus $self->{'real_can'}, as promised.

$self->{'can'} references the outer scrolled Frame, not the Canvas.   
So you use Subwidget() to get the actual object that's being scrolled  
and save that as $self->{'real_can'}. Now some code uses $self-> 
{'can'}, but other code needs to use $self->{'real_can'}, and things  
become confusing fast!

Simply by removing the unneeded Scrolled widget we'll see how things  
become much simpler.  For instance, here's what the new overridden  
bind command now looks like:

    sub bind {
        my( $self, @args ) = @_;
        $self->CanvasBind( @args );
    }

What's more, we can move the bindtags code out of Populate and make  
use of SetBindTags:

    sub SetBindtags {
        my($self) = @_;
        $self->SUPER::SetBindtags;
        my( @bindtags ) = $self->bindtags;
        $self->bindtags( [ @bindtags, 'CanvasDirTree-B1' ] );
    }

We couldn't do this before because SetBindTags() is called before  
Populate(), before the Scrolled Canvas is created, so the instance  
variable $self->{'real_can'} is undefined. Plus, the 40-ish places  
where the {'can'} and {'real_can'} instance variable are used can all  
be thrown away - $self is sufficient.

>
>    $self->SetBindtags();

The above explicit call to SetBindTags() in not needed, as Perl/Tk  
calls out to it automatically when instantiating a mega-widget.

We can make the actual B1 binding simpler too:

     $self->CanvasBind( 'CanvasDirTree-B1', '<Button-1>' =>  
'pick_one' );



> ###################################################################### 
> ######
> sub pick_one {
>      my ($canvas, $self) = @_;
>      my $item = $self->{'can'}->find('withtag','current'); #returns  
> aref

Which also makes pick_one simpler:

     sub pick_one {
          my ($self) = @_;
          my $item = $self->find('withtag','current'); #returns aref


In summary, if you remove the (really unneeded) Scrolled Canvas you:

. carry less baggage around because the widget hierarchy goes from  
Canvas/Frame/Canvas to just Canvas
. simplify the code and make it more readable
. conform to the mega-widget coding scheme, ensuring your new class  
behaves consistently

And if I want scrollbars on my Tk::CanvasDirTree widget, then by  
golly I can just do this:

my $ztree = $mw->Scrolled( 'CanvasDirTree', -scrollbars =>'osw' );

Don't forget the pod documentation :)

Steve


-++**==--++**==--++**==--++**==--++**==--++**==--++**==
This message was posted through the Stanford campus mailing list
server.  If you wish to unsubscribe from this mailing list, send the
message body of "unsubscribe ptk" to [email protected]
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.