Today we show how to work with ResizingSplitters, and along the way, show a TreeView feature and create a mini browser.
Design
We'll put a TreeView in the upper left of our window, and fill it with the Pane hierarchy, then put a ListBox in the upper right of our window, and fill that with method selectors for the selected class in the TreeView. Then we'll put a TextEdit across the bottom, and when you select a method selector, we'll display the method source. We'll put a ResizingSplitter vertically between the TreeView and the List, and a horizontal ResizingSplitter between the top two panes (TreeView and ListBox) and the TextEdit at the bottom.
As always, we start with a subclass of UserInterface:
Smalltalk defineClass: #ResizingSplitterWork
superclass: #{Widgetry.UserInterface}
indexedType: #none
private: false
instanceVariableNames: 'treeView listBox textEdit horizontalSplitter verticalSplitter'
classInstanceVariableNames: ''
imports: 'private Widgetry.*'
category: '(none)'
Before we get on to our usual #createInterface, we'll make our main window open a bit bigger to fit all the stuff we're going to put into it:
hookupWindow
self mainWindow frame specifiedSize: 300 @ 400.
Now, we'll create our main widgets, and lay them out:
createInterface
treeView := TreeView new.
treeView frame: (FractionalFrame
fractionLeft: 0
right: 0.5
top: 0
bottom: 0.5).
(treeView frame)
bottomOffset: -2;
rightOffset: -2.
self addComponent: treeView.
listBox := ListBox new.
listBox frame: (FractionalFrame
fractionLeft: 0.5
right: 1
top: 0
bottom: 0.5).
(listBox frame)
leftOffset: 2;
bottomOffset: -2.
self addComponent: listBox.
textEdit := TextEdit new.
textEdit frame: (FractionalFrame
fractionLeft: 0
right: 1
top: 0.5
bottom: 1).
textEdit frame topOffset: 2.
self addComponent: textEdit
And here is what it looks like when we execute ResizingSplitterWork open:
Not very exciting. However, you do see where the offsets I added to the frames leaves room for our ResizingSplitters.
Hair Jell
Next we create our two ResizingSplitters. By default, ResizingSplitters will display horizontally, so the first thing we'll do in our #hookupInterface is make the vertical ResizingSplitter, well, vertical!
createInterface
treeView := TreeView new.
treeView frame: (FractionalFrame
fractionLeft: 0
right: 0.5
top: 0
bottom: 0.5).
(treeView frame)
bottomOffset: -2;
rightOffset: -2.
self addComponent: treeView.
listBox := ListBox new.
listBox frame: (FractionalFrame
fractionLeft: 0.5
right: 1
top: 0
bottom: 0.5).
(listBox frame)
leftOffset: 2;
bottomOffset: -2.
self addComponent: listBox.
textEdit := TextEdit new.
textEdit frame: (FractionalFrame
fractionLeft: 0
right: 1
top: 0.5
bottom: 1).
textEdit frame topOffset: 2.
self addComponent: textEdit.
horizontalSplitter := ResizingSplitter new.
horizontalSplitter frame: (FractionalFrame
fractionLeft: 0
right: 1
top: 0.5
bottom: 0.5).
(horizontalSplitter frame)
topOffset: -2;
bottomOffset: 2.
self addComponent: horizontalSplitter.
verticalSplitter := ResizingSplitter new.
verticalSplitter frame: (FractionalFrame
fractionLeft: 0.5
right: 0.5
top: 0
bottom: 0.5).
(verticalSplitter frame)
leftOffset: -2;
rightOffset: 2;
bottomOffset: -2.
self addComponent: verticalSplitter
hookupInterface
verticalSplitter beVertical
Now it looks like this:
I know... its barely different. However, you should be able to see the splitters and their default raised look. If one moves the mouse over one of the splitters, the cursor will change appropriately to a horizontal or vertical resizing cursor. Also, you can move the splitters, but the result will be ugly:
This is because only the splitters themselves are moving. Just putting them there doesn't do anything.
3... 2... 1... Contact!
We have to tell our ResizingSplitters which panes to control. To do this, we simply tell each splitter the IDs of each pane it should control with the appropriate configuration method. We haven't given our panes explicit ID names, but fortunately Widgetry automatically assigns IDs to each pane when we add them with the #addComponent: method. All we have to do is send ID to the pane to get the generated ID. Thus with a bit of clever programming we never even have to know:
hookupInterface
verticalSplitter beVertical.
horizontalSplitter aboveWidgets: (Array
with: treeView id
with: verticalSplitter id
with: listBox id).
horizontalSplitter belowWidgets: (Array
with: textEdit id).
verticalSplitter leftWidgets: (Array
with: treeView id).
verticalSplitter rightWidgets: (Array
with: listBox id).
And this is what it looks like with some resizing done:
If we had given the widgets IDs, our code could have been simpler, such as #(#thisPane #thatPane #otherPane). I leave it up to you to decide if you want to do that or not.
That's all there really is to ResizingSplitters, but that's not much fun... So the rest is all:
Extra Credit
We'll give our TreeView the Pane hierarchy telling it to initially expand the root and have it not show lines, and then fill in the list when a class is chosen, and then fill in the textEdit when a method is chosen. We'll make the textEdit word wrapped, and add vertical scrollbars all around:
hookupInterface
| tree |
verticalSplitter beVertical.
horizontalSplitter aboveWidgets: (Array
with: treeView id
with: verticalSplitter id
with: listBox id).
horizontalSplitter belowWidgets: (Array
with: textEdit id).
verticalSplitter leftWidgets: (Array
with: treeView id).
verticalSplitter rightWidgets: (Array
with: listBox id).
tree := Tree
on: Pane
childrenBlock:
[:value | value subclasses
asSortedCollection: [:each :other | each name < other name]].
treeView tree: tree.
tree expandRoot.
treeView showLines: false.
treeView when: SelectionChanged send: #fillInMethods to: self.
listBox when: SelectionChanged send: #showSource to: self.
textEdit wordWrapped: true.
textEdit verticalScrollbar: true.
listBox verticalScrollbar: true.
treeView verticalScrollbar: true
fillInMethods
treeView selection isNil
ifTrue: [listBox list: ObservedList new]
ifFalse: [listBox list: treeView selection selectors asSortedCollection]
showSource
treeView selection ifNil: [textEdit model value: UIText new].
listBox selection ifNil: [textEdit model value: UIText new].
listBox selection ifNotNil:
[:value | textEdit model value: (treeView selection sourceMethodAt: value)]
And here is what that looks like with things selected and so on:
Isn't that pretty?
And So It GoesSames