Version 0 of Multiline expanding entry widget

Updated 2004-03-22 23:28:41

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 well, warts and all, here's the code. Feel free to improve it.


 # multientry.tcl --
 #
 #   A multi-line entry widget. This is a version of the text widget which
 #   automatically expands it's 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.
 #
 # http://wiki.tcl.tk/11152
 # By Neil Madden.
 # Public Domain.

 package require Tcl 8.4
 package require Tk 8.4
 package require snit 0.92
 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::text {
     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 -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
     }
 }