分享

scala.swing JTree wrapper design

 miqi05 2010-04-05
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:

>
> Hi everyone,
>
> 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.

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 :)

>
> 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])

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.

>
> // 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.

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.


>
> 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
>    }
> }

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

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多