Example: a Swing/Tcl file previewer

Here is a more complex example of how we can use Tcl Blend to "whip up" a Java GUI in Tcl. We'll use the new Swing user interface library from JavaSoft, since it contains some fairly sophisticated GUI elements that can save us a lot of work.

To be able to run the code on this page, you will need to download and install Swing. As of Early August 1998, we recommend the Swing 1.1 beta release, which requires registering with the Java Developer Connection (it's free). Swing is part of the Java Foundation Classes:

After unzipping the download file, you will need to add the file swingall.jar to your classpath. On Solaris, I set my CLASSPATH like this:
.:/users/johnr/tools/swing-1.1beta/swingall.jar
On Windows, I added the swing jar file to the CLASSPATH environment variable in the System control panel.
Bogon alert!
We are presenting this example to illustrate how Tcl can be used to rapidly explore and prototype Java GUIs. However, the demonstration is unreliable, and there appears to either a memory or threading problem with this combination of Tcl Blend and Swing. Also, the Swing HTML editor kit does not display <BLOCKQUOTE> tags at all. Since Swing is still in beta, though, we can't really complain.

You can try running the file previewer by sourcing javatute/preview.tcl. Here, we'll give some explanation of how the code works. (While reading this, remember that while we were writing this, we were able to create Java components and mess around with them to discover how they worked. This is the essence of rapid prototyping, and Tcl was very effective to help us learn about these Swing components!) We have removed a few non-essential lines to keep the explanation straight-forward. The first few line sets a variable to the package path, to save some typing:

  set swing com.sun.java.swing
(Wouldn't it be nice if Tcl Blend could "import" Java packages?) Then, we create an instance of JTree, which is the Swing tree viewer. When we create the tree, we pass it a root node, which contains the name of your home directory. We also place the tree into a JScrollPane so we get scrollbars if it's larger than the assigned screen area.:
  set root [java::new $swing.tree.DefaultMutableTreeNode [glob ~] true]
  set tree [java::new "$swing.JTree $swing.tree.TreeNode boolean" $root true]
  set treepane [java::new $swing.JScrollPane $tree]
To display the HTML text, all we need is an instance of the JEditorPane class. We put the editor pane into a scrolled pane as well:
  set view [java::new $swing.JEditorPane]
  set viewpane [java::new $swing.JScrollPane $view]
To display both the tree and the editor pane, we chose to use a JSplitPane, so that we can control the relative screen area allocated to them:
  set splitpane [java::new $swing.JSplitPane \
	[java::field $swing.JSplitPane HORIZONTAL_SPLIT] \
	$treepane $viewpane]
  $splitpane setContinuousLayout true
The final part of the layer is to place the split pane into a top-level window and display the window:
  set window [java::new $swing.JFrame "Preview!"]
  $window setSize 600 400
  [$window getContentPane] {add java.awt.Component} $splitpane
  $window show
Now, we need to set up the event bindings. The JTree generates events when items are selected, when tree nodes are expanded and collapsed, and so on. We will create bindings for item selection and node expansion:
  java::bind $tree valueChanged "selectionChanged $tree $view"
  java::bind $tree treeExpanded "treeExpanded $tree"
The selectionChanged procedure handles item selection events. (It's a bit simple, but it works as long as you don't select multiple items.) The utility procedure getFilePath (defined below) generate the full path name of the selected item. Then, if it's an HTML file, we construct a URL from the path name and give it to the editor pane:
  proc selectionChanged {tree view} {
      set filepath [getFilePath [java::event path]]
      if { [regexp {\(htm|html\)*$} $filepath] } {
          $view {setPage String} "file:$filepath"
      }
  }
When the tree is expanded, we call our loadDirectory procedure (defined below):
  proc treeExpanded {tree} {
      loadDirectory $tree [java::event path]
  }
The getFilePath procedure generates the file name from the tree path. Each node of the tree contains its name as a string, and the code follows the path down through the tree. At each node along the way, it uses toString to get the node name, and appends it to the path constructed so far:
  proc getFilePath {path} {
      set filepath ""
      for {set i 0} {$i < [$path getPathCount]} {incr i} {
	  set filepath [file join $filepath [[$path getPathComponent $i] toString]]
      }
      return $filepath
  }
The loadDirectory procedure loads a new directory. It looks at the given node to see if it has any children, and if not, creates them. Tcl's glob and file commands make it relatively quick and easy to figure out what nodes to add to the tree. The files are sorted and processed so that directories come first and HTML files after them. To add them to the tree, the tree model (the tree data representation) has its insertNodeInto method called:
proc loadDirectory {tree path} {
    set nodeClass com.sun.java.swing.tree.DefaultMutableTreeNode

    # Generate the child nodes if there aren't any
    set node [$path getLastPathComponent]
    if { [$node getChildCount] == 0 } {
        set filepath [getFilePath $path]
        set i -1
        set model [$tree getModel]

        # Directories
        foreach file [lsort [glob -nocomplain [file join $filepath *]]]  {
            if { [file isdirectory $file] } {
        	$model insertNodeInto \
                        [java::new $nodeClass [file tail $file] true] \
                        $node [incr i]
            }
        }
        # HTML files
        foreach file [lsort [glob -nocomplain [file join $filepath *]]]  {
            if { [regexp {\(htm|html\)*$} $file] } {
                $model insertNodeInto \
                        [java::new $nodeClass [file tail $file] false] \
                        $node [incr i]
            }
        }
    }
}
At the end of the file (after loadDirectory has been defined), we load the root directory. To do so, we construct a "path" through the tree that consists only of the root node, and then call loadDirectory:
  loadDirectory $tree [java::new "$swing.tree.TreePath Object" $root]

That's it! In about hundred lines of Tcl, we were able to glue together some sophisticated Java widgets to produce a custom file previewer. You can also try exploring the behavior of the Swing components using Tcl. For example, enter

  $splitpane setContinuousLayout false
and observe the difference when you move the vertical divider.

We also wrote an exactly equivalent Java version, in tutorial/tcltk98/JPreview.java. To try it out, compile it and then run it with

  > java tutorial.tcltk98.JPreview
We tried to make the Java code as close as possible to the Tcl code, so you can compare the two. Having prototyped the previewer in Tcl, we could go ahead and create a new Java class for it, and then proceed to use Tcl to add further functionality, such as menus, toolbars, and so on.