Recent Progress in Tablelist

by

Csaba Nemethi

csaba.nemethi@t-online.de
http://www.nemethi.de

INFOSYS GmbH, Unterhaching, Germany

csaba.nemethi@infosys-consulting.de
http://www.infosys-consulting.de

Contents

  1. A Quick Tablelist Overview
  2. Using Menubuttons for Interactive Cell Editing
  3. Supported Tree Styles
  4. Moving an Item
  5. Searching for a Pattern

1. A Quick Tablelist Overview

A tablelist is a multi-column listbox and tree widget, supporting a large number of options and widget subcommands.  Here are just a few of them:


2. Using Menubuttons for Interactive Cell Editing

Support for interactive cell editing with the aid of the Tk core and tile menubutton widgets was added in Tablelist version 5.3.

From the manual page Interactive Cell Editing Using the menubutton Widget:

The temporary embedded menubutton widget used for interactive cell editing will be created with explicitly set values for its -anchor, -indicatoron, -justify, -padx, -pady, -relief, and -textvariable options.  In addition, a menu with its -tearoff option set to 0 and an appropriate script as the value of its -postcommand option is created and set as the value of the menubutton's -menu option. ... You can use the script corresponding to the -editstartcommand tablelist configuration option to set any other options of the menubutton and/or its associated menu.  You will, however, need this script in the first place for populating the menu, preferably with radiobutton entries.  For every radiobutton entry added to the menu, the Tablelist implementation will make sure that its value (which can be specified by setting the entry's -value or -label option) will be displayed in the menubutton as its text when the entry is selected.  (...)  For menu entries of types other than radiobutton (e.g., for command entries) it is the responsibility of the application to make sure that the selected entry's text will be shown in the menubutton (for example, with the aid of the menu entry's -command option).

From the manual page Interactive Cell Editing Using the tile menubutton Widget:

The temporary embedded tile menubutton widget used for interactive cell editing will be created with explicitly set values for its -style and -textvariable options.  In addition, ... (identical to the description above).

Menubuttons versus comboboxes:

The following sample code snippets are taken from the latest version of the demo script tileWidgets.tcl, as contained in the forthcomimg Tablelist release 5.7:

set dir [file dirname [info script]]

. . .

#
# Create the images "checkedImg" and "uncheckedImg", as well as 16 images of
# names like "img#FF0000", displaying colors identified by names like "red"
#
source [file join $dir images.tcl]

. . .

$tbl columnconfigure 10 -name color -editable yes -editwindow ttk::menubutton \
    -formatcommand emptyStr

. . .

proc editStartCmd {tbl row col text} {
    set w [$tbl editwinpath]

    switch [$tbl columncget $col -name] {

        . . .

	color {
	    #
	    # Populate the menu and make sure the menubutton will display the
	    # color name rather than $text, which is "", due to -formatcommand
	    #
	    set menu [$w cget -menu]
	    foreach name $::colorNames {
		$menu add radiobutton -compound left \
		    -image img$::colors($name) -label $name
	    }
	    $menu entryconfigure 8 -columnbreak 1
	    return [$tbl cellcget $row,$col -text]
	}
    }

    return $text
}

proc editEndCmd {tbl row col text} {
    switch [$tbl columncget $col -name] {

        . . .

	color {
	    #
	    # Update the image contained in the cell
	    #
	    $tbl cellconfigure $row,$col -image img$::colors($text)
	}
    }

    return $text
}

3. Supported Tree Styles

When a tablelist is used as a tree widget, one of its columns (specified by the -treecolumn option) will display the tree hierarchy with the aid of indentations and expand/collapse controls.  The look & feel of that column is controlled by the -treestyle option, which includes, among others, the images used for displaying the expand/collapse controls, the indentation width, and whether expand/collapse controls and indentations are to be protected when selecting a row or cell.  The Tablelist package chooses the correct default style depending on the windowing system, operating system version, and tile theme.  The number of supported tree styles has increased from 16 in Tablelist 5.0 to 25 in Tablelist 5.6:

aqua gtk newWave ubuntu mint
baghira phase oxygen1 oxygen2 klearlooks
winnative winxpBlue winxpOlive winxpSilver plastik
vistaAero vistaClassic win7Aero win7Classic plastique
ambiance dust dustSand radiance adwaita

In the forthcoming Tablelist version 5.7, the tree styles ubuntu and mint have been adapted to the Ubuntu Linux release 12.04 and Linux Mint release 13, respectively.  The screenshots above show these updated versions.


4. Moving an Item

The move subcommand was extended in Tablelist release 5.2 to support moving an item outside its parent programmatically, and version 5.3 added support for performing this interactively, too.  The subcommand now has two forms:

pathName move sourceIndex targetIndex
pathName move sourceIndex targetParentNodeIndex targetChildIndex
The first form of the command moves the item indicated by sourceIndex just before the one given by targetIndex if the tablelist's state is not disabled.  If targetIndex equals the nunber of items or is specified as end then the source item is moved after the last one.  The item specified by targetIndex must have the same parent as the one given by sourceIndex, or else it must be the item just below the last descendant of the source node's parent.

The command's second form moves the item indicated by sourceIndex just before the node having the parent indicated by targetParentNodeIndex and the index targetChildIndex in the parent's list of children if the tablelist's state is not disabledtargetChildIndex must be a number or end; if it equals the number of children of the target parent node or is specified as end then the source item is moved after the target parent node's last child.

Both forms of the command preserve the node hierarchy under the source item, by moving its descendants accordingly.  The return value is an empty string.

According to this description, the interactive row move operation now supports dragging an item outside its parent and dropping it under another item as a child.  During this local drag & drop, the new item position (if any) is visualized with the aid of a gap placed before the target row or a bar placed inside the latter (depending on the current mouse position), indicating whether the source item would be moved before this row or become a child of it.

Depending on the current mouse position during the local drag & drop, there can be a corresponding potential target row or not.  For instance, a tree item cannot become a sibling of one of its descendants, and not all items might be allowed to have children or to become top-level ones.  To decide whether the row corresponding to the y-coordinate of the current mouse position represents a valid potential target, the Tablelist code first checks whether moving the source item before that row or making it a child of the latter is allowed from the point of view of the general tree structure.  If this is the case and the move operation would change the source item's parent, then the command specified by the -acceptchildcommand configuration option (introduced in Tablelist 5.3) is used to decide whether to allow to move the dragged item to the intended target position:  If the value of this option is an empty string then the move operation is allowed (and the target position is visualized as described above).  Otherwise the command is concatenated with the name of the tablelist widget, the node index of the would-be new parent node, and the row index of the dragged item, the resulting script is evaluated in the global scope, and the return value (which must be a boolean) will determine whether to allow to move the source item to the current mouse position.

The following example is a slightly modified and extended version of the demo script dirViewer_tile.tcl:

proc displayContents dir {
    #
    # Create a scrolled tablelist widget with 3 dynamic-
    # width columns and interactive sort capability
    #
    set tf .tf
    ttk::frame $tf -class ScrollArea
    set tbl $tf.tbl
    set vsb $tf.vsb
    set hsb $tf.hsb
    tablelist::tablelist $tbl \
	-columns {0 "Name"	    left
		  0 "Size"	    right
		  0 "Date Modified" left} \
	-expandcommand expandCmd -collapsecommand collapseCmd \
	-xscrollcommand [list $hsb set] -yscrollcommand [list $vsb set] \
	-movablecolumns no -showseparators yes -height 18 -width 80 \
        -acceptchildcommand acceptChildCmd -movablerows true -selectmode single

    . . .

    #
    # Populate the tablelist with the contents of the given directory
    #
    $tbl sortbycolumn 0
    putContents $dir $tbl root
}

The procedure acceptChildCmd, specified as the value of the -acceptchildcommand configuration option, makes sure that the top-level items remain restricted to the ones displaying volumes and only directories will accept child items:

proc acceptChildCmd {tbl targetParentNodeIdx sourceRow} {
    if {[string compare $targetParentNodeIdx "root"] == 0} {
	#
	# Allow only volumes as top-level items
	#
	return [expr {[$tbl depth $sourceRow] == 1}]
    } else {
	#
	# Allow only directories as parent items
	#
	return [$tbl hasrowattrib $targetParentNodeIdx pathName]
    }
}

5. Searching for a Pattern

The searchcolumn subcommand was added in Tablelist version 5.3.  It is often used together with the -populatecommand configuration option, introduced in Tablelist version 5.4.  Here is a partly abbreviated version of the subcommand's description in the reference manual:

pathName searchcolumn columnIndex pattern ?options?
This subcommand searches the elements of the column given by columnIndex to see if one of them matches pattern.  If a match is found, the row index of the first matching element is returned as result (unless the option -all is specified).  If not, the return value is -1.  One or more of the following options may be specified to control the search:

-all Changes the result to be the list of all matching row indices, which will be in numeric order (or in reverse numeric order when used with the -backwards option).
 
-backwards The search will proceed backward through the given column's elements.
 
-check command Specifies an additional condition to be fulfilled by the matching elements. ... This option enables you to pass arbitrary additional matching criteria to the searching process.
 
-descend Search the elements of the specified column in all descendants of the tree node given by the -parent option. ...
 
-exact The matching element(s) must be identical to the literal string pattern.
 
-formatted Examine the formatted versions of the elements rather than the internal cell values.
 
-glob Treat pattern as a glob-style pattern and match it against the elements using the same rules as the  string match  command.
 
-nocase Causes comparisons to be handled in a case-insensitive manner.  Has no effect if combined with the -numeric option.
 
-not This option negates the sense of the match, ...
 
-numeric The elements are to be compared to pattern as integer or floating-point values, using the == comparison operator.  This option is only meaningful when used with -exact.
 
-parent nodeIndex  This option restricts the search to the children (or descendants, when used with -descend) of the tree node given by nodeIndex.  The default parent is root.
 
-regexp Treat pattern as a regular expression and match it against the elements using the rules described in the re_syntax reference page.
 
-start index The elements of the specified column are to be searched (forwards or backwards) starting at the row given by index.  This option makes it easy to provide incremental search.

If all matching style options -exact, -glob, and -regexp are omitted then the matching style defaults to -glob.  If more than one of them is specified, the last matching style given takes precedence.

Before examining the children (or descendants, when used with the -descend option) of a row whose children have not been inserted yet, the command specified as the value of the -populatecommand option (if any) is automatically concatenated with the name of the tablelist widget and the row index, and the resulting script is evaluated in the global scope.  This enables you to insert the children on demand, just before searching them for the specified pattern.

The following example is an extended version of the demo script dirViewer_tile.tcl.  The pop-up menu within the tablelist widget displaying the contents of a directory has a second command entry Search for Pattern..., which opens a dialog for entering the data needed for building the arguments to be passed to the searchcolumn subcommand:

proc displayContents dir {
    #
    # Create a scrolled tablelist widget with 3 dynamic-
    # width columns and interactive sort capability
    #
    set tf .tf
    ttk::frame $tf -class ScrollArea
    set tbl $tf.tbl
    set vsb $tf.vsb
    set hsb $tf.hsb
    tablelist::tablelist $tbl \
	-columns {0 "Name"	    left
		  0 "Size"	    right
		  0 "Date Modified" left} \
	-expandcommand expandCmd -collapsecommand collapseCmd \
	-xscrollcommand [list $hsb set] -yscrollcommand [list $vsb set] \
	-movablecolumns no -showseparators yes -height 18 -width 80 \
	-populatecommand populateCmd

    . . .

    #
    # Create a pop-up menu with 2 command entries; bind the script
    # associated with its first entry to the <Double-1> event, too
    #
    set menu .menu
    menu $menu -tearoff no
    foreach {label cmd} {"Display Contents" putContentsOfSelFolder
	"Search for Pattern..." openSearchDlgForSelFolder} {
	$menu add command -label $label -command [list $cmd $tbl]
    }
    set bodyTag [$tbl bodytag]
    bind $bodyTag <<Button3>>  [bind TablelistBody <Button-1>]
    bind $bodyTag <<Button3>> +[bind TablelistBody <ButtonRelease-1>]
    bind $bodyTag <<Button3>> +[list postPopupMenu %X %Y]
    bind $bodyTag <Double-1>   [list putContentsOfSelFolder $tbl]

    . . .

    #
    # Populate the tablelist with the contents of the given directory
    #
    $tbl sortbycolumn 0
    putContents $dir $tbl root
}

The procedure populateCmd, specified as the value of the -populatecommand configuration option, will be invoked automatically if (and only if) the searchcolumn subcommand has to examine the children of a tablelist item and these children are still unknown.  In the presence of the -descend subcommand option, this will be performed recursively  As described in the reference manual, the populateCmd procedure should just insert the children of the row in question, without expanding the node or changing its appearance in any other way.  In our example, these children correspond to the contents of the directory whose leaf name is displayed in the first cell of the specified row:

proc populateCmd {tbl row} {
    set dir [$tbl rowattrib $row pathName]
    putContents $dir $tbl $row
}

The procedure openSearchDlgForSelFolder, associated with the Search for Pattern... pop-up menu entry, creates a dialog window containing:

The procedure uses, among others, the mentry::dateTimeMentry command from the Mentry package for creating two multi-entry widgets that hold the modification date interval and the Wcb package for a strait-forward implementation of the text widget's read-only behavior:

proc openSearchDlgForSelFolder tbl {
    set row [$tbl curselection]
    set key [$tbl getkeys $row]
    set top .top$key
    if {[winfo exists $top]} {
	raise $top
	return ""
    }

    toplevel $top
    set dir [$tbl rowattrib $row pathName]
    wm title $top "Search in Directory \"[file nativename $dir]\""

    . . .

    #
    # "Modification Date" checkbuttons and mentry widgets
    #
    set lfLastModDateTime [ttk::labelframe $f.lfLastModDateTime -text \
			   "Modification Date"]
    set ::data($key-useMinDateTime) 0
    set ::data($key-useMaxDateTime) 0
    set ckMinDateTime [ttk::checkbutton $lfLastModDateTime.ckMinDateTime -text \
		       "After:" -variable data($key-useMinDateTime)]
    set meMinDateTime [mentry::dateTimeMentry \
		       $lfLastModDateTime.meMinDateTime YmdHM - : \
		       -justify center -background white]
    set ckMaxDateTime [ttk::checkbutton $lfLastModDateTime.ckMaxDateTime -text \
		       "Before:" -variable data($key-useMaxDateTime)]
    set meMaxDateTime [mentry::dateTimeMentry \
		       $lfLastModDateTime.meMaxDateTime YmdHM - : \
		       -justify center -background white]
    set maxClock [clock seconds]
    set minClock [expr {$maxClock - 24*60*60}]
    mentry::putClockVal $minClock $meMinDateTime
    mentry::putClockVal $maxClock $meMaxDateTime
    . . .

    #
    # "Find" and "Close" buttons
    #
    set fBtns [ttk::frame $f.fBtns]
    set bFind  [ttk::button $fBtns.bFind -text "Find" -default active \
		-command [list findPattern $tbl $key]]
    set bClose [ttk::button $fBtns.bClose -text "Close" -default normal \
		-command [list destroy $top]]
    . . .

    #
    # Readonly text widget displaying the search command
    #
    set tCmd [text $f.tCmd -background white -height 2 -width 70 -wrap word \
	      -highlightthickness 0 -insertwidth 0]
    wcb::callback $tCmd before insert cancelInput
    wcb::callback $tCmd before delete cancelInput
    proc cancelInput {w idx args} { wcb::cancel }
    set ::data($key-tCmd) $tCmd

    #
    # Scrollable tablelist widget displaying the search result
    #
    set fResult $f.fResult
    ttk::frame $fResult -class ScrollArea
    set resTbl $fResult.tbl
    set vsb $fResult.vsb
    set hsb $fResult.hsb
    tablelist::tablelist $resTbl \
	-columns {0 "Path"	    left
		  0 "Size"	    right
		  0 "Date Modified" left} \
	-xscrollcommand [list $hsb set] -yscrollcommand [list $vsb set] \
	-movablecolumns no -showseparators yes -height 12 -width 70

    . . .
}

The findPattern procedure associated with the Find button of the dialog described above builds the search command from the data entered by the user, evaluates it, and displays the data of the matching items in the search result tablelist widget :

proc findPattern {tbl key} {
    . . .

    #
    # Build, show, and evaluate the search command
    #
    set searchCmd [list $tbl searchcolumn 0 $::data($key-pattern) \
		   -parent k$key -all -formatted $::data($key-style)]
    if {$::data($key-descend)} {
	lappend searchCmd -descend
    }
    if {$::data($key-noCase)} {
	lappend searchCmd -nocase
    }
    if {$useMinDateTime || $useMaxDateTime} {
	set checkCmd [list checkDateTime $minDateTime $maxDateTime]
	lappend searchCmd -check $checkCmd
    }
    _$tCmd insert end $searchCmd
    update idletasks
    set rowList [eval $searchCmd]

    #
    # Populate the search result tablelist with the data of the matching items
    #
    foreach row $rowList {
	set item [$tbl get $row]
	foreach {name size dateTime} $item {}

	. . .
    }
}