I've been working on a prototype for a JTree wrapper for a couple of weeks,
and my ideas have solidified enough to run up the flagpole here. USE CASES JTrees are commonly used in several situations: - A small, fixed tree as a navigation mechanism - A graphical representation of some data structure in tree form. - As above, but editable: nodes can be edited, added, removed, and moved. (Also dragged and dropped) - A representation of the filesystem, nested into directories - A deeply nested, infinite, or cyclic graph structure that needs to be constructed lazily. This is particularly arduous in Java, but is easily supportable in Scala. MODEL REPRESENTATION In Scala Swing, the actual Swing XXXModel classes are hidden, and real collections of user objects are used. For instance ListView[A] uses List[A] as its representation, and Table exposes what looks like a 2D Seq. While a tree has no natural Scala collection capable of displaying it, most user's tree structures will emerge naturally from their data structures, or with nested Products (ie case classes/tuples) or nested Seqs. The only reasonable model representation, IMHO is to provide a root object, and a function that finds child nodes given another node. This can easily allow for all the use cases above; our class signature is therefore: class Tree[A](root: A, createChildren: A => Seq[A]) // Usage examples: // Display filesystem new Tree[File](new File("."), f => if (f.isDirectory) f.listFiles else Seq.empty) // Recursively factorise all integers from 1 to 1000 (infinite depth): new Tree[Int](1000, n => 1 to n filter (n % _ == 0)} // Display some XML document new Tree[Node](<root><ch id="b">Foo</ch></root>, {_.child filterNot (_.text.trim.isEmpty)}) TYPED OR UNTYPED? It is debatable whether we should use a type parameter; many lists do not care about the type of their contents, or use nested containers and therefore have no common class between branch and leaf nodes. However, I think there is enough chance that a user WILL care about the node type that it is still useful. An example is displaying the filesystem; each branch and leaf would be a directory or file respectively. SELECTION REPRESENTATION In Java Swing, a unit of selection in a JTree is a TreePath. TreePath is basically a glorified List, and does not deserve to be propagated or wrapped in Scala. scala.List[A] fits perfectly in this role. However, it is probably appropriate to provide a type alias, such as: 1) object Tree {type Path[A] = List[A]} 2) class Tree[A]... {type Path = List[A]} I'm not sure which one is really appropriate though, if any; it makes some sense for the Path type to belong to the Tree instance. However, it's ugly; someone used to dealing with "TreePath" now has to deal with "Tree[A]#Path". Tree.Path[A] is slightly nicer. Alternatively, we could simply put type TreePath = List[A] in the scala.swing package object. NO BRAINERS Some design patterns can be directly copied from Tree's cousins, ListView and Table: - object selection {object paths; object rows} - class Tree.Renderer, Tree.AbstractRender, and Tree.GenericRenderer - Events: ComponentEvent +-- TreeEvent[A] +-- TreePathSelected[A] +-- TreeExpansionEvent[A] | +-- TreeCollapsed[A] | +-- TreeExpanded[A] | +-- TreeWillCollapse[A] | +-- TreeWillExpand[A] +-- TreeModelEvent[A] +-- TreeNodesChanged[A] +-- TreeNodesInserted[A] +-- TreeNodesRemove[A] +-- TreeStructureChanged[A] DEFAULT CHILD METHODS To facilitate easy use, the Tree companion object could expose createChildren functions for common use cases. For instance: object Tree { def files(f: File) def filteredFiles(filter: File => Boolean)(f: File): Seq[File] def products(node: Any) = node match { case s: Seq[_] => s case o => Seq.empty } def seqs(node: Any) = node match { case s: Seq[_] => s case o => Seq.empty } } MATTERS REQUIRING FRAMEWORK-WIDE CONSIDERATION: - Drag And Drop, and Cell Editing are features which have Swing-wide implementation in Java, and need to be Scala-fied in a way that works for everything (especially Tree, Table and ListView), not just Tree. I have some ideas for these, but it belongs in another thread, and should be treated as separate tasks. - There is a level of commonality between Tree, ListView, and Table which may need to be abstracted. For instance: - Drag and drop (as above) - Cell Editing (as above) - Selection by rows - Iteration through data model items - Cell Renderers (currently my Tree.Renderers are almost verbatim cut-and-pasted from Table and ListView, suggesting that there is commonality requiring a higher level of abstraction) I would be greatly interested on hearing Scala Swing users' opinions on these matters! As a long time Swing user (~10 years), I think Ingo's wrapper framework is absolutely fantastic, and it would be great to see it more feature-complete. Other Swing enthusiasts out there: lets get it moving. Ken Replies ---------
Hi Ken,
it's again a fair analysis of the subject, thanks. Here are my comments (inlined). On 2/28/10 6:46 AM, Ken Scambler wrote: ...[show rest of quote]
Did you have a look at how other framework do this? Collection controls with lazily created items are sometimes called "virtual". I guess we can do something similar with collection views (formerly known as projections). The question is whether its actually feasible/practical. Only an implementation can prove it :) ...[show rest of quote]
Minor nitpick: createChildren doesn't actually need to *create* children. I would expect a static tree view just returns a Seq that's already there somewhere, so simply "children" might a better name. ...[show rest of quote]
I guess you answered that question in the examples above already. I would definitely go for a generic TreeView. Adding a single (probably invariant) type parameter doesn't cost a lot. > > SELECTION REPRESENTATION > In Java Swing, a unit of selection in a JTree is a TreePath. TreePath is > basically a glorified List, and does not deserve to be propagated or wrapped > in Scala. scala.List[A] fits perfectly in this role. However, it is > probably appropriate to provide a type alias, such as: > 1) object Tree {type Path[A] = List[A]} > 2) class Tree[A]... {type Path = List[A]} The Path type probably does not belong to the instance (I assume you can use a path of one tree view for another). I would put it into the companion object instead of the package object, as this is more in line with the existing scala.swing design. ...[show rest of quote]
I disagree about these. Why the above and why not other things such as XML nodes, Document elements, Compiler tree nodes... They don't belong into a companion object, which is essentially a closed world (not across revisions but that's a different story :)). If we can come up with some really time/keystroke saving, really commonly used utils we can put them elsewhere. > > MATTERS REQUIRING FRAMEWORK-WIDE CONSIDERATION: > - Drag And Drop, and Cell Editing are features which have Swing-wide > implementation in Java, and need to be Scala-fied in a way that works for > everything (especially Tree, Table and ListView), not just Tree. I have > some ideas for these, but it belongs in another thread, and should be > treated as separate tasks. > Uh oh, d'n'd! any suggestions or better yet implementations welcome! > - There is a level of commonality between Tree, ListView, and Table which > may need to be abstracted. For instance: > - Drag and drop (as above) > - Cell Editing (as above) > - Selection by rows > - Iteration through data model items > - Cell Renderers (currently my Tree.Renderers are almost verbatim > cut-and-pasted from Table and ListView, suggesting that there is commonality > requiring a higher level of abstraction) Agreed. > > > I would be greatly interested on hearing Scala Swing users' opinions on > these matters! As a long time Swing user (~10 years), I think Ingo's > wrapper framework is absolutely fantastic, and it would be great to see it > more feature-complete. Other Swing enthusiasts out there: lets get it > moving. Other people's opinions on the above matters are indeed welcome! Thanks, Ingo Hi Ingo,
Thanks for responding so quickly! My comments inlined again: >> - A deeply nested, infinite, or cyclic graph structure that needs to be >> constructed lazily. This is particularly arduous in Java, but is easily >> supportable in Scala. > Did you have a look at how other framework do this? Collection controls > with lazily created items are sometimes called "virtual". I guess we can > do something similar with collection views (formerly known as > projections). The question is whether its actually feasible/practical. > Only an implementation can prove it :) I've only looked at Swing: the official way is to use TreeWillExpandListeners, and mutate the TreeModel on click. Now that I've googled for it, .NET does much the same thing. Scala can and should have a more natural usage, IMHO. Collection views were my first thought too, but surprisingly, my implementation works without either technique: (although I can't prove that Swing won't cause it to infinite loop if the tree doesn't terminate...) // In object Tree protected class LazyNode[A](val parent: Option[LazyNode[A]], val userObject: A, children: A => Seq[A]) extends TreeNode { lazy val childNodes: Seq[LazyNode[A]] = children(userObject) map {n => new LazyNode(Some(this), n, children)} def getChildAt(childIndex: Int): TreeNode = childNodes(childIndex) def getChildCount() = childNodes.size def getParent(): TreeNode = parent.orNull def getIndex(node: TreeNode) = childNodes indexOf node def getAllowsChildren() = true def isLeaf() = childNodes.isEmpty def children(): java.util.Enumeration[A] = new java.util.Enumeration[A] { val iterator = childNodes.iterator def hasMoreElements() = iterator.hasNext def nextElement = iterator.next.userObject } override def equals(o: Any) = o match { case r: AnyRef => r eq this case _ => false } override def toString() = userObject.toString() } >> Minor nitpick: createChildren doesn't actually need to *create* >> children. I would expect a static tree view just returns a Seq that's >> already there somewhere, so simply "children" might a better name. Agreed. > I guess you answered that question in the examples above already. I > would definitely go for a generic TreeView. Adding a single (probably > invariant) type parameter doesn't cost a lot. All good. > The Path type probably does not belong to the instance (I assume you can > use a path of one tree view for another). I would put it into the > companion object instead of the package object, as this is more in line > with the existing scala.swing design. No worries. >> DEFAULT CHILD METHODS >I disagree about these. Why the above and why not other things such as >XML nodes, Document elements, Compiler tree nodes... They don't belong >into a companion object, which is essentially a closed world (not across >revisions but that's a different story :)). If we can come up with some >really time/keystroke saving, really commonly used utils we can put them >elsewhere. Yeah fair point, that sort of thing probably belongs elsewhere. However, I think at the least there needs to be some convenient default usage to handle simple Tree literals (say for a fixed navigation menu), something like: new Tree("Hobbies" -> ("Surfing", "Lawn bowls", "Extreme Sports" -> ("Spelunking", "Speed dating")) Haven't worked out exactly the best way to do this yet though. Pleasingly though, just about anything I can think of can be treated as a special case of the root node/child function representation. > Uh oh, d'n'd! any suggestions or better yet implementations welcome! Can do. I'll deal with it as a separate thing for the moment though. >> - There is a level of commonality between Tree, ListView, and Table which >> may need to be abstracted. For instance: >> - Drag and drop (as above) >> - Cell Editing (as above) >> - Selection by rows >> - Iteration through data model items >> - Cell Renderers (currently my Tree.Renderers are almost verbatim >> cut-and-pasted from Table and ListView, suggesting that there is commonality >> requiring a higher level of abstraction) >Agreed. I'll do some more thinking about this, and come up with some sort of proposal for some sort of master trait for the three widgets. By the way, how do I go about contributing code? I have, or will soon, a rough first cut of a prototype, but I'm not sure where to go from here. Cheers, Ken On 2/28/10 2:45 PM, Ken Scambler wrote:
> By the way, how do I go about contributing code? I have, or will soon, a > rough first cut of a prototype, but I'm not sure where to go from here. Sorry, I missed that part when I read your mail the first time. The most convenient thing would be if you can put up a public git repository for now. By the time we integrate your code, we need a signed contributors agreement from you: http://www./sites/default/files/contributor_agreement.pdf Thanks! Ingo |
|