The Artima Developer Community
Sponsored Link

Agile Buzz Forum
Fun With TreeViews

0 replies on 1 page.

Welcome Guest
  Sign In

Go back to the topic listing  Back to Topic List Click to reply to this topic  Reply to this Topic Click to search messages in this forum  Search Forum Click for a threaded view of the topic  Threaded View   
Previous Topic   Next Topic
Flat View: This topic has 0 replies on 1 page
James Robertson

Posts: 29924
Nickname: jarober61
Registered: Jun, 2003

David Buck, Smalltalker at large
Fun With TreeViews Posted: Jun 8, 2007 2:08 PM
Reply to this message Reply

This post originated from an RSS feed registered with Agile Buzz by James Robertson.
Original Post: Fun With TreeViews
Feed Title: Pollock
Feed URL: http://www.cincomsmalltalk.com/rssBlog/pollock-rss.xml
Feed Description: Pollock - the next VW GUI
Latest Agile Buzz Posts
Latest Agile Buzz Posts by James Robertson
Latest Posts From Pollock

Advertisement

Today we look at the TreeView and along the way we look in detail into Attributes.

Design

We'll place a TreeView on our window, and fill it with the Widgetry PaneHelper hierarchy as our example tree. We'll add two CheckBoxes. One will be to turn on and off all scrollbars. The second will turn on or off special color highlighting of Agents, Frames and Artists. Finally, we'll make it so that when we Double Click on an item, we'll open a browser on the selected class.

Here's the UserInterface subclass:

	Smalltalk defineClass: #TreeViewWork
		superclass: #{Widgetry.UserInterface}
		indexedType: #none
		private: false
		instanceVariableNames: 'treeView scrollbars fancyColoring '
		classInstanceVariableNames: ''
		imports: 'private Widgetry.*'
		category: '(none)'

And here is our #createInterface method laying out our panes:

	createInterface

		treeView := TreeView new.
		treeView frame: (FractionalFrame
			fractionLeft: 0.05
			right: 0.95
			top: 0.05
			bottom: 0.75).
		self addComponent: treeView.
		scrollbars := CheckBox withLabelString: 'Scrollbars'.
		scrollbars frame: (FractionalFrame
			fractionLeft: 0.05
			right: 0.45
			top: 0.80
			bottom: 0.95).
		self addComponent: scrollbars.
		fancyColoring := CheckBox withLabelString: 'Fancy Colors'.
		fancyColoring frame: (FractionalFrame
			fractionLeft: 0.50
			right: 0.95
			top: 0.80
			bottom: 0.95).
		self addComponent: fancyColoring.

When we now execute TreeViewWork open, this is what we see:

The Morton Arboretum

Where the model for a ListBox is an ObservedList, the model for a TreeView is a Tree.

The simplest kind of Tree defines a root object, and then a block that gets children for that object.

It is not required that the children be actual children in any real sense, just that the block that answers children answer a collection of some kind, either empty or with some objects.

The children block when executed passes in the current object that it is seeking children for, be it the root object, or some other child object.

In our case, a Class easily answers our need by sending it the #subclasses message.

Here is our #hookupInterface, giving our TreeView its Tree.

	hookupInterface

		| tree |
		tree := Tree
			on: PaneHelper
			childrenBlock: [:value | value subclasses].
		treeView tree: tree.

And this is what it looks like when we now open our UserInterface:

If you click on the little box with the '+' in it, it expands a level. Double clicking anywhere on an item will open or close a node (if it can be opened or closed). If select an item, and then right click, we see the default helper menu:

Open Sesame

That doesn't look bad, but wouldn't it be nice if at least the first level of items was already opened when we open our interface? I think so.

There are many messages you can send to a Tree to help you with that. In our case there are two that we can use: #expandRoot and #expandToLevel:.

#expandRoot does just what it says, it expands the root object one level. Under the hood, #expandRoot eventually calls #expandToLevel: with the value of 1 as the argument for a Tree set up the way we have here.

For our use though, let's just send #expandRoot:

	hookupInterface

		| tree |
		tree := Tree
			on: PaneHelper
			childrenBlock: [:value | value subclasses].
		treeView tree: tree.
		tree expandRoot

Here's what it looks like:

An interesting note here is that we could have just as easily sent #expandRoot to the TreeView itself. Indeed, if you look at the 'expanding-contracting' protocol on TreeView and Tree, you'll see that it is pretty much the same!

Mail Room

Let's look at what our TreeView looks like when we open the Agent node:

To me, having the nodes in unsorted order is pretty ugly and harder to read. Let's fix that:

	hookupInterface

		| tree |
		tree := Tree
			on: PaneHelper
			childrenBlock: 
				[:value | value subclasses 
					asSortedCollection: [:each :other | each name < other name]].
		treeView tree: tree.
		tree expandRoot

Much better!

Scrollbars

This should be pretty easy. We've done this kind of thing before. We just hook up the ValueChanged announcement to the #allScrollbars: of our TreeView. This time, instead of making a special method, we'll do it all in one place using #when:do::

	hookupInterface

		| tree |
		tree := Tree
			on: PaneHelper
			childrenBlock: 
				[:value | value subclasses 
					asSortedCollection: [:each :other | each name < other name]].
		treeView tree: tree.
		tree expandRoot.
		scrollbars when: ValueChanged do: [treeView allScrollbars: scrollbars model value]

And here is how that looks with the AbstractFrame node opened:

Finger Paints

One of the extra features of many of the Widgetry panes are the ability to have some of the attributes of a pane be dynamic at runtime.

Attributes are used in these places. For a TreeView, we can dynamically change the foreground color, emphasis (such as bold, underlined or italic) and icon of each item. The TreeView supports a different value for these in general, as well as a different value for these depending if they are opened, closed or leaf nodes.

Here we'll just show how to do it in general, using the itemForegroundColorAttribute.

We'll make it so that Agents are Green, Artists are Blue and Frames are Red. Attributes will simply be black.

Attribute Details

Each Attribute expects that the result of being evaluated will be of a certain type, or nil. Here we are using a ColorAttribute, and our code thus needs to respond with anything that answers the message #isColor with the Boolean true. Note, it can still answer nil. All Attributes are set up with a default value, in this case it is the system's "list data foreground data" color, which is typically black. When a custom configured Attribute is evaluated and it answers nil, then the default value is substituted. This way, whenever you want the default, just answer nil from your custom code.

We have three choices when configuring an Attribute: We can have it act on a Block, we can give it a receiver and selector, or we can give it an ObservedValue.

Each has its own reason for use.

We should use an ObservedValue when we are using a single value that will be used, but which may change externally. This may be used when possibly you want to turn all items to be shown the same color, like when an error occurs, or something like that.

We should use a Block when there is simple code that we can express for the values. For instance, maybe you use as the objects in your tree things that answer their own color.

We should use the receiver/selector when the code is more complex than is appropriate for stuffing into a Block.

Our code could be put into a Block or set as a receiver/selector. I choose to use receiver/selector because I don't care for "configuration" Blocks that are more than two lines of code, which ours will be in this situation. The choice is up to you.

Attributes also come in two flavors, so to speak. Value and Rule.

A Rule Attribute is used in Widgetry when more than just the pane might be considered for determining the answering value. The itemForegroundColorAttribute is a Rule Attribute. When it is evaluating the configured value, it will send along the current item in the Tree that it is being asked to display.

A Value Attribute is used in Widgetry when only the pane itself may play a part in the final value. For instance, there is a backgroundAttribute for many panes in Widgetry. This will change the global background color for the pane.

Both Rule and Value Attributes are configured with an ObservedValue in the same way:

	anAttribute observable: anObservedValue

For Value Attributes, the Block or selector can have zero or one argument:

	aValueAttribute block: [something]
	aValueAttribute block: [:pane | something]
	aValueAttribute receiver: anObject selector: #something
	aValueAttribute receiver: anObject selector: #somethingPane:

For Rule Attributes, the Block or selector can have one or two arguments:

	aRuleAttribute block: [:value | something]
	aRuleAttribute block: [:value :pane | something]
	aRuleAttribute receiver: anObject selector: #somethingValue:
	aRuleAttribute receiver: anObject selector: #somethingValue:pane:

Back To Our Regularly Scheduled Program

In our case, we'll use the receiver/selector, and we only need the value. Here's our code:

	hookupInterface

		| tree |
		tree := Tree
			on: PaneHelper
			childrenBlock: 
				[:value | value subclasses 
					asSortedCollection: [:each :other | each name < other name]].
		treeView tree: tree.
		tree expandRoot.
		scrollbars when: ValueChanged do: [treeView allScrollbars: scrollbars model value].
		treeView itemForegroundColorAttribute receiver: self selector: #itemColorFor:

For the #itemColorFor: method, we'll first test to see if our fancy coloring is turned on. Answer nil if it isn't, and colors if it is:

	itemColorFor: aTreeItem

		fancyColoring model value ifFalse: [^nil].
		(aTreeItem includesBehavior: AbstractFrame) ifTrue: [^ColorValue red].
		(aTreeItem includesBehavior: Agent) ifTrue: [^ColorValue darkGreen].
		(aTreeItem includesBehavior: Artist) ifTrue: [^ColorValue blue].
		^nil

Finally, we make the Fancy Colors dynamic. When that value changes, we send the message #updateForAttributeChange to our TreeView. This is a general message that can be send to any pane whenever we might be changing an attribute behind a pane's back:

	hookupInterface

		| tree |
		tree := Tree
			on: PaneHelper
			childrenBlock: 
				[:value | value subclasses 
					asSortedCollection: [:each :other | each name < other name]].
		treeView tree: tree.
		tree expandRoot.
		scrollbars when: ValueChanged do: [treeView allScrollbars: scrollbars model value].
		treeView itemForegroundColorAttribute receiver: self selector: #itemColorFor:.
		fancyColoring when: ValueChanged send: #updateForAttributeChange to: treeView.

Extra Credit

As we noted at the top, we want to make it open that browser when we double click on an item. Note that doing this overrides the default behavior of opening or closing an item when it is double clicked:

	hookupInterface

		| tree |
		tree := Tree
			on: PaneHelper
			childrenBlock: 
				[:value | value subclasses 
					asSortedCollection: [:each :other | each name < other name]].
		treeView tree: tree.
		tree expandRoot.
		scrollbars when: ValueChanged do: [treeView allScrollbars: scrollbars model value].
		treeView itemForegroundColorAttribute receiver: self selector: #itemColorFor:.
		fancyColoring when: ValueChanged send: #invalidate to: treeView.
		treeView when: DoubleClicked do: [treeView selection browse]

Isn't that pretty?

And So It Goes
Sames

Read: Fun With TreeViews

Topic: Noble.NET Previous Topic   Next Topic Topic: Lost in the PS3 Weeds

Sponsored Links



Google
  Web Artima.com   

Copyright © 1996-2019 Artima, Inc. All Rights Reserved. - Privacy Policy - Terms of Use