How to totally handle listbox selection

ABU 5-apr-2005


The problem:

I have a listbox and a button. The button should be enabled when a selected element exists, and it should be disabled when no element is selected.


Before explaining the solution found, I think it is important to show the method used for testing. I will show you a simple user interface made with

  • an (enhanced) listbox,
  • a related button (that will be enabled only when there's a selection in the listbox)
  • an entry-widget

This latter entry-widget is important because its role is to 'claim' the selection, so that we can verify if the (enhanced) listbox properly reacts when it loses the selection.

NOTE: instead of an entry-widget we could use other widgets able to claim the selection. Among standard widgets these are:

  • entry
  • listbox
  • text

An entry-widget has been chosen just because it is the simpler widget. Anyway, the solution (the enhanced listbox) is independent of, or better, is unaware of, the 'other' widgets that could claim the selection.

It is important to remark here that a selection in a listbox could change for two kind of reasons:

  • in response to changes made by a PROGRAM
  • in response to changes made by a USER

It is a common and well-documented technique to intercept USER changes, by providing a callback for the <<ListboxSelect>> event. This technique is not totally perfect (as you will see later) but, more important, this technique cannot be used for intercepting changes made by a PROGRAM.

What I want to say is that, when a PROGRAM changes the selection in a listbox, e.g.

 # select the first row in a listbox $lb
 $lb select set 0  
 # delete the first row
 $lb delete 0

NO <<ListboxSelect>> event is generated.

In these cases it is the programmer's responsibility to add some code enabling/disabling the 'related button'.

A simple solution I recommend for these cases is the following:

  if you have some code that *may* change the selection in a listbox,
  simply generate a virtual event after.

E.g

    ...
    $lb selection clear 0 end
    ...
   should be written as
    ...
    $lb selection clear 0 end
    event generate $lb <<ListboxSelect>>
    ...

The other case, when the selection changes in response to changes made by a USER, is more complex ...

Of course the application cannot wait for user changes; some techniques for controlling the event-loop are required ...

I will show you how to provide a handler to deal with ALL changes made by the USER.


After this long premise, we are ready for the solution.

The solution is based on the following principles:

(A) intercept all the <<ListboxSelect>> generated events.

This way allows you to know when a user selects an element of the listbox and (if "-selectmode" option is "multiple" or "extended") when a user deselects the last element.

 bind $lb <<ListboxSelect>> [list ::cb_selectionChanged %W] 

 proc ::cb_selectionChanged {w args} {
  if { [$w curselection] == "" } {
     # listbox has no selected items
  } else {
     # listbox has one or more selected items
  }

Unfortunately there is lack; if user makes a selection in another widget (e.g an entry-widget), all the selected elements of the listbox disappear, and worse, NO event is generated.

NOTE: when user clicks-in an entry-widget, or he enters some text, nothing happens to the listbox; it is only when the user *selects* some text in an entry-widget that the listbox loses all its selected items (provided that listbox option "-exportselection" is "1" (default))

(B) register a callback so that listbox will be notified when another widget claims the selection.

Note that there can be only one widget registered for this notification, and this widget should be the widget currently owning the selection. Therefore, each time a listbox widget (you could have more than one) gets the selection, it should register itself for this kind of notification.

Fortunately, this is easy, because each time the <<ListboxSelect>> event is generated, (see above), it means that THAT listbox has got the selection.

All we need to add is the following code

 ## ----------------------------------------------------------------
 ## generic code for enhanced listbox
 ## ----------------------------------------------------------------
  
  # define an event-handler valid for the whole 'Listbox' class
 bind Listbox <<ListboxSelect>> [list gotSelectEvent %W]

 proc gotSelectEvent {w} {
   puts "Listbox-class received <<ListboxSelect>>"
   if { [$w curselection] != ""  &&  [$w cget -exportselection] == 1 } {
         selection own -command [list lostSelection $w] $w
   } 
 }

 proc lostSelection {w} {
   if { [$w cget -exportselection] == 1 } {
      puts "Listbox $w lost selection"
      $w selection clear 0 end
      event generate $w <<ListboxSelect>>
   }
 }

Note that I wrote anything tied to a 'particular' listbox instance, nor a 'particular' event-handler for a <<ListboxSelect>> tied to a listbox.

There's only an event-handler valid for ALL listboxes. Particular instances of listbox can (should) write their own peculiar event-handler. Something like this:

  # remember you MUST load the above 'generic code for enhanced listbox'

  # purpose of this proc is to enable/disable the button $b
  #  depending on the selected rows of the listbox $lb
 proc MY_selectHandler {lb b} {
   if { [$lb curselection] == "" } {
      $b configure -state disabled
   } else {
      $b configure -state normal
   }
 }

 listbox .lb
 button .b -state disabled -text "button related to listbox" 
 pack .lb .b
 bind .lb <<ListboxSelect>> [list MY_selectHandler .lb .b]
 ...

And now, here is a complete example. experiment it !

 ## ----------------------------------------------------------------
 ## generic code for enhanced listbox
 ## ----------------------------------------------------------------
  
  # define an event-handler valid for the whole 'Listbox' class
 bind Listbox <<ListboxSelect>> [list gotSelectEvent %W]

 proc gotSelectEvent {w} {
   puts "Listbox-class received <<ListboxSelect>>"
   if { [$w curselection] != ""  &&  [$w cget -exportselection] == 1 } {
         selection own -command [list lostSelection $w] $w
   } 
 }

 proc lostSelection {w} {
   if { [$w cget -exportselection] == 1 } {
      puts "Listbox $w lost selection"
      $w selection clear 0 end
      event generate $w <<ListboxSelect>>
   }
 }

 ## ----------------------------------------------------------------
 ## sample code for testing ....
 ## ----------------------------------------------------------------

  # define here a proc for creating a listbox with a related-button
  #  plus some checkbuttons/radiobuttons for changing its properties  
 proc myComplexListbox {w} {
   frame $w -relief raised -bd 2 -padx 5 -pady 5

    # create a listbox; it is an enhnaced listbox thanks to the
    #  extended Listbox-class handler defined above
   listbox $w.lb
   button $w.b -text "apply on selected item" \
      -command [list showSelected $w.lb]
   $w.b configure -state disabled
   pack $w.lb $w.b
    # bind event to a custom handler (enabling/disabling related button)
   bind $w.lb <<ListboxSelect>> [list mySelectHandler $w]

    # other controls ...
    
   set ::control($w,mode) browse
   foreach mode {single browse extended multiple} {
      radiobutton $w.rb_$mode -value $mode -variable ::control($w,mode) \
        -text $mode \
        -command [list $w.lb configure -selectmode $mode]
      pack $w.rb_$mode
   }
     
   set ::control($w,exportselection) 1
   checkbutton $w.expsel -text "-exportselection" \
      -variable ::control($w,exportselection) \
      -command [list setExportSelection $w]
   pack $w.expsel
   return $w
 }

 proc showSelected {lb} {
   puts "selected element is <[$lb curselection]>"
 }

 proc setExportSelection {w} {
  $w.lb configure -exportselection $::control($w,exportselection)
 } 

 proc mySelectHandler {w} {
  puts "<$w.lb> received <<ListboxSelect>>"
   # note that if -selectmode is multiple/extended, and the last item
   #  is deselected, then <<ListboxSelect>> is generated !
   
  if { [$w.lb curselection] != "" } {
     $w.b configure -state normal   
  } else {
     $w.b configure -state disabled
  }
 }


 pack [myComplexListbox .m1]
 .m1.lb insert 0 aa bb cc dd
 pack [myComplexListbox .m2]
 .m2.lb insert 0 xx yy zz

  # some 'other' standard widgets that could claim the selection ...
 pack [labelframe .other -text "other widgets.." -padx 5 -pady 5]
 pack [entry .other.e1]
 pack [entry .other.e2]

As a final exercise I rewrote the same solution in a 00-way using Snit.

This latter solution is more elegant, providing a new widget "extListbox" hiding all the internal details, without the need to occupy an event handler for the Listbox class.

Here is the 'generic' Snit code providing the new "extListBox" widget

 package require snit 0.97

 ::snit::widgetadaptor extListbox {
   
   option -exportselection 1

   delegate method * to hull
   delegate option * to hull except { -exportselection } 

    #define a 'pseudo' Class binding
   typeconstructor {
      bind ExtListbox <<ListboxSelect>> [myproc gotListboxSelect %W]
   }


   constructor {args} {
      installhull using listbox
      $self configurelist $args
      bindtags $win [linsert [bindtags $win] 1 ExtListbox]
   }
   

      # intercept option -exportselection.
   onconfigure -exportselection {value} {
      $hull configure -exportselection $value
      set options(-exportselection) $value
      if { $value &&  [$win curselection] != "" } { 
         selection own -command [myproc lostSelection $win] $win
      }      
   }


   proc gotListboxSelect {w} {
      if { [$w curselection] != ""  &&  [$w cget -exportselection] == 1 } {
         selection own -command [myproc lostSelection $w] $w
      } 
   }

   proc lostSelection {w} {
      if { [$w cget -exportselection] == 1 } {
         $w selection clear 0 end
         event generate $w <<ListboxSelect>>
      }
   }
 }

and here is the code for testing.

 # NOTE: Snit-widget extListbox is required

 proc user_gotListboxSelect {w} {
  puts "<$w.lb> received <<ListboxSelect>>"
   # note that if -selectmode is multiple/extended, and the last item
   #  is deselected, then <<ListboxSelect>> is generated !
   
  if { [$w.lb curselection] != "" } {
     $w.b configure -state normal   
  } else {
     $w.b configure -state disabled
  }

 }

 proc user_showSelected {lb} {
   puts "selected element is <[$lb curselection]>"
 }

 proc user_setExportSelection {w} {
  $w.lb configure -exportselection $::control($w,exportselection)
 } 

 proc myComplexListbox {w} {
   frame $w -relief raised -bd 2 -padx 5 -pady 5

   extListbox $w.lb
   button $w.b -text "apply on selected item" \
      -command [list user_showSelected $w.lb]
   $w.b configure -state disabled
   pack $w.lb $w.b
   
   bind $w.lb <<ListboxSelect>> [list user_gotListboxSelect $w]


    # other controls ...
    
   set ::control($w,mode) browse
   foreach mode {single browse extended multiple} {
      radiobutton $w.rb_$mode -value $mode -variable ::control($w,mode) -text $mode \
        -command [list $w.lb configure -selectmode $mode]
      pack $w.rb_$mode
   }
     
   set ::control($w,exportselection) 1
   checkbutton $w.expsel -text "-exportselection" \
      -variable ::control($w,exportselection) \
      -command [list user_setExportSelection $w ]
   pack $w.expsel
   return $w
 }


 pack [myComplexListbox .m1]
 .m1.lb insert 0 aa bb cc dd
 pack [myComplexListbox .m2]
 .m2.lb insert 0 xx yy zz

 pack [labelframe .other -text "other widgets.." -padx 5 -pady 5]
 pack [entry .other.e1]
 pack [entry .other.e2]

What's next ?

Well, I think this exercise can be generalized for other standard widgets, such as text-widget and tablelist.

I wish it could be an easier way to do it, but the better thing will be this idea be incorporated in the standard listbox widget ...