Today we'll address the horizontal scrollbar.
The Lateral Basics
As we noted in the last posting, there are two basic pieces of information that a scrollbar needs. For a horizontal scrollbar it is the maximum width and the current left offset.
Each of these has an accessor on our Artist.
For the maximum width the accessor is #maximumItemWidth, and for the offset it is #leftOffset. As you see, and saw in the last posting, some of the names come from the notion of dealing with a list of some kind. In Widgetry, there are scrollbars in three general places, on EnumerationPanes (ListBox, Grid and TreeView), TextEdit and Form.
While it seems that having a list oriented set of API names may be constraining, in fact you can think of a TextEdit as just a list of lines of text. That leaves only the Form and our new ScrollingCanvas sitting somewhat outside the meme. How we are dealing with that here is exactly how it is dealt with in the Form. More on that in later postings
We start off with giving our ScrollingCanvasArtist its first two horizontal scrollbar APIs:
ScrollingCanvasArtist>>maximumItemWidth
^self frame bounds width
ScrollingCanvasArtist>>leftOffset
^0
Like we did for the vertical scrollbar, our maximum size for now is the total width of the frame. For the offset, we start off by faking it with 0. Note that where the vertical scrollbar is 1 based for the offset (there it was topItemIndex) the horizontal scrollbar is 0 based.
The horizontal scrollbar needs one more bit of information in order to show up. This is called the #rightEdgeOffset. This is the amount of extra space the to the right of the longest visible item (in our case, the total width) should be chopped off in order to balance out any pane specific added left offset. In EnumerationPanes and the TextEdit, we don't display the items or the text fully to the left edge of the pane, and instead these have a bit of extra padding on the left. When doing calculations for how big thumbs are and so on, we need to compensate for that.
Our pane though will always display with no initial left offset, so our #rightEdgeOffset is 0.
ScrollingCanvasArtist>>rightEdgeOffset
^0
Now if we turn on the horizontalScrollbar in our CustomWidgetWork:
CustomWidgetWork>>hookupInterface
customWidget drawActionBlock:
[:aGraphicsContext :pane |
| oldLineWidth oldPaint |
oldLineWidth := aGraphicsContext lineWidth.
oldPaint := aGraphicsContext paint.
aGraphicsContext lineWidth: 5.
aGraphicsContext paint: ColorValue red.
aGraphicsContext displayLineFrom: 0 @ 0 to: pane frame bounds corner.
aGraphicsContext lineWidth: 10.
aGraphicsContext paint: ColorValue blue.
aGraphicsContext displayLineFrom: pane frame bounds bottomLeft to: pane frame bounds topRight.
aGraphicsContext paint: oldPaint.
aGraphicsContext lineWidth: oldLineWidth].
"Optional Border code:"
"customWidget borderType: #ridged"
"customWidget borderType: #raised"
"customWidget borderType: #etched"
"customWidget borderType: #lowered"
"customWidget borderType: #line"
"customWidget border: TwistBorder new"
"customWidget verticalScrollbar: true."
customWidget horizontalScrollbar: true
And then execute CustomWidgetWork open we see:
And if we uncomment the line customWidget verticalScrollbar: true, and open our tester again, we see this:
Horeshoes and Hand Grenades
As we see, the horizontal scrollbar is already compensating for the fact that the vertical scrollbar has taken up some of the inner edge of our pane and showing us that it is ready to scroll. On the other hand, the vertical scrollbar isn't showing the same thing, even though it should. Also if you click on the horizontal scrollbar, nothing happens.
The reason the vertical scrollbar isn't showing the same compensation is that last time we told it that the total scroll height was 1. As noted then, that was a fake value to get us started. However, the real value for now should be the total height of the pane, since that is the area we are drawing on. So if we change #totalScrollHeight to match what we did with #maximumItemWidth:
ScrollingCanvasArtist>>totalScrollHeight
^self frame bounds height
Now we see:
Lies, Damned Lines, Scrollbars
If we leave things as they are, there is nothing but fake numbers and sizes, and that isn't close to where we want to go. Our initial goal is to have a canvas that is possibly larger than the area of its frame and if so, have it be able to scroll. While we could easily just tell the drawing code we wrote to draw to coordinates outside the width and height of the frame, there is no way to tell the scrolling world how to deal with that.
Therefore, we are going to add a new attribute to our pane, which we'll call #contentsExtent. With this, we can specify a size of the contents of the pane that is independent of any frame that it might have.
We'll put the actual instance variable on the ScrollingCanvasArtist, since it will be used for drawing and calculating scrollbars. Since this is also a user supplied configuration value, we'll add accessors to the ScrollingCanvas pane itself so they don't have to ever talk to the artist directly.
So here are the changes to ScrollingCanvas
ScrollingCanvas>>contentsExtent
^self artist contentsExtent.
ScrollingCanvas>>contentsExtent: aPointOrNil
| oldValue |
oldValue := self artist contentsExtent.
self artist contentsExtent: aPointOrNil.
oldValue = self artist contentsExtent ifFalse: [self announce: PreferredExtentChanged]
Note here that for the setting method, we first get the existing value from the artist, set the new value and then test if it has changed, and if so, announce PreferredExtentChanged.
As we did with when we turn on the scrollbars, we always announce PreferredExtentChanged whenever we do something to the pane that might make the preferred extent change. Back to that in a moment.
For our ScrollingCanvasArtist, we add the contentsExtent instance variable:
Smalltalk.Widgetry defineClass: #ScrollingCanvasArtist
superclass: #{Widgetry.CanvasArtist}
indexedType: #none
private: false
instanceVariableNames: 'interiorDecoration contentsExent '
classInstanceVariableNames: ''
imports: ''
category: 'CustomWidgetry'
And then the accessor methods:
ScrollingCanvasArtist>>contentsExtent
^contentsExtent ifNil: [self frame bounds extent]
ScrollingCanvasArtist>>contentsExtent: aPointOrNil
contentsExtent := aPointOrNil
Note here that we set the default to be in effect the frame bounds extent. This is so that if a user doesn't set the contents extent, it will still be usable.
Pottery Barn Rule
Next we want to use this value in our existing scrollbar support methods. So we change #maximumItemWidth, #listSize and #totalScrollHeight to use this value:
ScrollingCanvasArtist>>maximumItemWidth
^self contentsExtent x
ScrollingCanvasArtist>>listSize
^self contentsExtent y
ScrollingCanvasArtist>>totalScrollHeight
^self contentsExtent y
If we open our CustomWidgetWork we don't see any difference, which is exactly what we want. So, let's change the #hookupInterface to use our new value:
CustomWidgetWork>>hookupInterface
customWidget drawActionBlock:
[:aGraphicsContext :pane |
| oldLineWidth oldPaint |
oldLineWidth := aGraphicsContext lineWidth.
oldPaint := aGraphicsContext paint.
aGraphicsContext lineWidth: 5.
aGraphicsContext paint: ColorValue red.
aGraphicsContext displayLineFrom: 0 @ 0 to: pane frame bounds corner.
aGraphicsContext lineWidth: 10.
aGraphicsContext paint: ColorValue blue.
aGraphicsContext displayLineFrom: pane frame bounds bottomLeft to: pane frame bounds topRight.
aGraphicsContext paint: oldPaint.
aGraphicsContext lineWidth: oldLineWidth].
"Optional Border code:"
"customWidget borderType: #ridged"
"customWidget borderType: #raised"
"customWidget borderType: #etched"
"customWidget borderType: #lowered"
"customWidget borderType: #line"
"customWidget border: TwistBorder new"
customWidget contentsExtent: 500 @ 500.
customWidget horizontalScrollbar: true.
customWidget verticalScrollbar: true.
Now if we open our CustomWidgetWork we see this:
Smoke Them If You Got Them
We see our scrollbars are now showing the effect of having what seems to be a bigger area to scroll, however our drawing doesn't take advantage of it. Therefore, we have one change to make, and that is to our #drawActionBlock.
Instead of using the frame bounds for the extent, we'll use the new #contentsExtent:
CustomWidgetWork>>hookupInterface
customWidget drawActionBlock:
[:aGraphicsContext :pane |
| oldLineWidth oldPaint |
oldLineWidth := aGraphicsContext lineWidth.
oldPaint := aGraphicsContext paint.
aGraphicsContext lineWidth: 5.
aGraphicsContext paint: ColorValue red.
aGraphicsContext displayLineFrom: 0 @ 0 to: pane contentsExtent.
aGraphicsContext lineWidth: 10.
aGraphicsContext paint: ColorValue blue.
aGraphicsContext displayLineFrom: (0 @ pane contentsExtent y) to: (pane contentsExtent x @ 0).
aGraphicsContext paint: oldPaint.
aGraphicsContext lineWidth: oldLineWidth].
"Optional Border code:"
"customWidget borderType: #ridged"
"customWidget borderType: #raised"
"customWidget borderType: #etched"
"customWidget borderType: #lowered"
"customWidget borderType: #line"
"customWidget border: TwistBorder new"
customWidget contentsExtent: 500 @ 500.
customWidget horizontalScrollbar: true.
customWidget verticalScrollbar: true.
Now this is what we see:
And if we resize our window, we see that indeed we are getting what we wanted:
Preferred Extent Again
Before we leave today, we want to talk again about the preferred extent. The basic notion of this is what is the minimum size on the screen a pane will take up given its current contents and attributes. For instance, the preferred extent for a CheckBox with no label is the size of the check image. If we add a label, then the preferred extent changes to include the size of the label plus the gap between the check image and the label.
There is a #preferredExtent method in the CanvasArtist which we inherit from that just gives the size of any border and any interior decoration. But that isn't enough for our ScrollingCanvas. We want to include the contents extent if there is any.
We thus add our own #preferredExtent method to our ScrollingCanvasArtist:
ScrollingCanvasArtist>>preferredExtent
| totalExtent |
totalExtent := super preferredExtent.
contentsExent ifNotNil: [totalExtent := totalExtent + contentsExent].
^totalExtent
This makes it possible for someone to create a pane with a #contentsExtent, add borders and turn on scrollbars, and then ask the pane #preferredExtent to determine how big the whole thing would be to make sure that everything could be seen. The mechanism is already on Pane, so we don't have to have a forwarding method from our pane to the artist.
Next Time
We'll look into making the vertical scrollbar actually work, which will mean creating a custom Agent for our pane.
And So It GoesSames