Text Collapser

WJG (25th August, 2005) I thought that it would be useful to 'collapse' or 'fold' a selected range of content in a text widget. So, this is snippet I came up with.

 #----------------------------------------------------------------
 # TextCollapser.tcl
 #----------------------------------------------------------------
 # Create by William J Giddings, 2005
 #
 # Notes:
 # Blocks can be nested.
 #----------------------------------------------------------------


 #----------------------------------------------------------------
 # application namespace
 #----------------------------------------------------------------
 namespace eval collapser {
  # tag base name
  set tagbase cb_
        # button data
        image create photo collapser::downarrow -data  R0lGODlhDAAMAJEAAP///9TQyAAAAAAAACwAAAAADAAMAAACEIyPqcudAqNQcq7orNu8qwIAOw==
        image create photo collapser::uparrow -data R0lGODlhDAAMAJEAAP///9TQyAAAAAAAACwAAAAADAAMAAACEoyPqcsobcRrcq5qU6VZdQgmBQA7
        image create photo collapser::button -data R0lGODlhDAAMAJEAAP////4AAAAAAAAAACwAAAAADAAMAAACGoSPJ8ttDUWaJ9iLDd5B8+t9HTVFDmAyZFIAADs=
 }

 #----------------------------------------------------------------
 # obtain a unique name for a new block of text
 #----------------------------------------------------------------
 proc collapser::name {path} {
        
  # get a sorted list of the current windows
        set windowlist ""
        foreach {a b c} [$path dump -window 1.0 end] {
                if { [string first $path.$collapser::tagbase $b 0 ] != "-1" } {
      lappend windowlist $b
    } 
  }
        set windowlist [lsort $windowlist] 
        
        # get the last one the list
        set next [lindex $windowlist end]
  
  # increment the number and return the new tag name
  set j [string trimleft $next $path.${collapser::tagbase}]
        if {$j == "" } {set j 0}
  return ${collapser::tagbase}[incr j]

 }

 #----------------------------------------------------------------
 # add a toolbar button 
 #----------------------------------------------------------------
 proc collapser::addbutton {path} {
        button $path -text Block -borderwidth 0 -command collapser::block -image collapser::button
        pack $path -side left
 }

 #----------------------------------------------------------------
 # create a new collapable text block
 #----------------------------------------------------------------
 proc collapser::block {} {
  
  # get path to active widget
        set path [focus]
  
  # only works on text widgets
  if {[winfo class $path]!="Text"} {return}
  
  # get tag name for the new block
        set tag [collapser::name $path]
        
  # create and configure new tag
        if { [catch {  $path tag add $tag sel.first {sel.last lineend}  }] } {return;}
        $path tag configure $tag -foreground red 

        # create block toggle button
        button $path.$tag \
    -borderwidth 0 \
    -text {-} \
    -image collapser::downarrow \
    -cursor hand2 \
    -command "collapser::toggle $path $tag"

        # add the button to the text
        $path window create {insert linestart} -window $path.$tag
        
        # remove text tagging if the button is deleted
        bind $path.$tag <Destroy> [list $path tag delete $tag]

 }

 #----------------------------------------------------------------
 # toggle tag settings
 #----------------------------------------------------------------
 proc collapser::toggle {path tag} {
                if {[$path tag cget $tag -elide] == 1} {
                        $path tag configure $tag -elide 0
                        $path.$tag configure -text - -image collapser::downarrow
                } else {
                        $path tag configure $tag -elide 1
                        $path.$tag configure -text + -image collapser::uparrow
                }
 }

 #----------------------------------------------------------------
 # demo code to test the package
 #----------------------------------------------------------------
 proc demo {} {
        console show
        frame .fr
        pack .fr -fill x
        text .txt -font {Ariel 12}
        pack .txt -fill both -expand 1
        focus .txt
        collapser::addbutton .fr.b_1
 }

 #----------------------------------------------------------------
 # the ubiquitious demo!
 #----------------------------------------------------------------
 demo

escargo 26 Aug 2005 - I got this message deleting a tag:

 if 0 {
 can't read "path": no such variable
 can't read "path": no such variable
     while executing
 "$path] tag delete $tag"
     (command bound to event)
 }

I deleted the tag by selecting it and typing control-x on Windows XP Pro. When a tag is deleted, it appears that the text associated with the tag is also deleted.

It would be nice if the cursor shape could change over the tag.

LV on line 97 of this file, it looks like this bind is wrong

 if 0 {
        bind $path.$tag <Destroy> {$path] tag delete $tag}
 }

I don't see a reason for that ] in the command.

WJG (27 August,2005) Thanks for testing the code and giving the feedback. LV, fair comment on the stray ']' in the code. I've removed it. Escargo, the cursor, well, I've given it the finger. It now uses the Hand2 cursor.


escargo 29 Aug 2005 - I tried the new version, and found some interesting features that I don't think are intended.

  1. If you select a region that include the end of a list (but not the whole line) you get a button that disappears when clicked, so you can not get it back to click it again.
  2. On Microsoft Windows (at least) there are no scroll bars, but the underlying widget does honor scroll-mouse events.
  3. It would be nice if it did have scroll bars.
  4. It would be nice if there was a binding to restore all collapsed text. (Right button on the collapse icon maybe.)

There is still a problem deleting tags; I got this message deleting a tag:

 if 0 {
 can't read "path": no such variable
 can't read "path": no such variable
     while executing
 "$path tag delete $tag"
     (command bound to event)
 }

I deleted the tag by selecting it and typing control-x on Windows XP Pro.

MG I've made a couple of small changes to the code above, to make it work for me:

 if 0 {
        bind $path.$tag <Destroy> {$path} tag delete $tag}
 has become
        bind $path.$tag <Destroy> [list $path tag delete $tag]
 }

which fixes the error message above and removes an extra } in the code.

I also changed the range elided, from

 if 0 {
        $path tag add $tag {sel.first lineend} sel.last
 to
        $path tag add $tag sel.first {sel.last lineend}
 }

as before, the start index was greater than the end index, so it did nothing and nothing was elided. It might be better as just sel.first sel.last but I left the lineend in as I assume that's what the original author intended. I've also put an if / catch / return block around that line, so that if there is no selection, it just returns gracefully, instead of raising an error. Also fixed a typo of Ariel/Arial as the font in the demo.


See also Text widget elision