Multiline expanding entry widget

NEM 22Mar2004 - This is an entry widget replacement that can contain multiple display lines (but only 1 logical line - so no newlines allowed). It automatically grows in height to fit the text being typed into it, up until the -maxheight option has been reached. After that it automatically displays a scrollbar. Works the other way too - will shrink down as stuff is deleted. Some limitations:

  • The algorithm for calculating the number of lines in the text widget is slightly flawed, and doesn't always work correctly (out by a few chars occassionally). If someone can spot the problem, please fix it!
  • Works better for fixed-width fonts. Proportional fonts work, but it makes more errors in the algorithm (which, as I say, is flawed).
  • Doesn't take into account -wrap settings, so only use with -wrap char. It sort of works with -wrap word, but is generally quite wrong. -wrap none won't work at all for obvious reasons (you don't need this if you don't want to wrap!)
  • Oh, and I haven't yet implemented some of the entry options (like -textvariable and such)...

Oh well, warts and all, here's the code. Feel free to improve it.

dbn 29 Sept 2009 - I think the problem mentioned in the first bullet is because the -highlightthickness needed to be accounted for when determining the widget's width; once I included that, it seemed to work more reliably. I updated the code below.


 # multientry.tcl --
 #
 #   A multi-line entry widget. This is a version of the text widget that
 #   automatically expands its height to fit the text it is displaying. When
 #   the height reaches a maximum value (-maxheight option; default=4 lines)
 #   then a scrollbar is created. This is the behaviour of (for instance) the
 #   To: and Cc: entry fields in some mail applications/newsreaders.
 #
 # https://wiki.tcl-lang.org/11152
 # By Neil Madden.
 # Public Domain.
 
 package require Tcl 8.4
 package require Tk 8.4
 package require snit 0.91
 package provide multientry 0.1

 namespace eval multientry {
     namespace export multientry
 }
 
 # multientry --
 #
 #  The entry widget replacement. Automatically grows in height until the
 #  -maxheight level has been reached, and then adds a scrollbar. Works
 #  shrinking too. The algorithm is not perfect, and you may occasionally type
 #  extra characters before the entry grows, or sometimes, too few. I'm sure
 #  I'm just missing something simple here. Doesn't work very well.
 snit::widget multientry::multientry {
     delegate method * to text
     delegate option * to text
 
     variable scrolled 0
 
     constructor {args} {
         install text using multientry::mtext $win.t -parent $self \
             -height 1 -borderwidth 1 \
             -relief sunken -yscrollcommand [list $win.vsb set]
         scrollbar $win.vsb -orient vertical -command [list $win.t yview]
         grid $win.t -row 0 -column 0 -sticky nsew
         grid columnconfigure $win 0 -weight 1
 
         $self configurelist $args
     }
     
     method IsScrolled {} { return $scrolled }
 
     method AddScrollbar {} {
         grid $win.vsb -row 0 -column 1 -sticky ns
         set scrolled 1
     }
 
     method RemoveScrollbar {} {
         grid forget $win.vsb
         set scrolled 0
     }
 }
 
 # Helper widget - a text widget which handles it's own resizing. Calls back to
 # the parent widget if it decides it needs a scrollbar. Not to be used
 # directly, only via the multientry widget (below).
 snit::widgetadaptor multientry::mtext {
     delegate method Insert to hull as insert
     delegate method Delete to hull as delete
     delegate method * to hull
     delegate option * to hull
 
     option -parent ""
     option -maxheight 4
 
     constructor {args} {
         installhull using text
         $self configurelist $args
     }
 
     method insert {index args} {
         set arglist [list]
         # Remove newlines - only one line allowed!
         foreach {str tags} $args {
             lappend arglist [string map {\n ""} $str] $tags
         }
         eval [list $self Insert $index] $arglist
         $self AdjustHeight
     }
     method delete {args} {
         eval [list $self Delete] $args
         $self AdjustHeight
     }
     method AdjustHeight {} {
         # Adjust height if needed
         set tw [font measure [$self cget -font] -displayof $win \
             [$self get 1.0 end-1c]]
         # Calculate the actual size of the text widget internals
         set sw [expr {[winfo width $win]-
             (2*[$self cget -borderwidth]
             + 2*[$self cget -selectborderwidth]
             + 2*[$self cget -highlightthickness])}]
         set h [expr {$tw/$sw + 1}]
         if {$h != [$self cget -height]} {
             if {$h <= $options(-maxheight)} {
                 if {[$options(-parent) IsScrolled]} {
                     $options(-parent) RemoveScrollbar
                 }
                 $self configure -height $h
             } else {
                 if {![$options(-parent) IsScrolled]} {
                     $options(-parent) AddScrollbar
                 }
             }
         }
         return
     }
 }

And some demo code (if needed!):

 pack [multientry::multientry .me -maxheight 6] -fill both -expand 1

escargo 22 Mar 2004 - Does it really require Snit 0.92? 0.91 is more commonly available....

LES: I have ActiveTcl 8.4.4 and snit is 0.82. Doesn't work.

NEM 0.91 should do. Simple typo. I guess snit changed between 0.82 and 0.91 (as it's pre-1.0 release stuff the API seems to change quite frequently).


See also: