show all comments

Custom Panes

Finishing Up

September 12, 2007 17:43:41 EDT

M - I - CEEEEE

It seemed only appropriate that I finish the ScrollingCanvas discussion.

People wanted to know how to have a Canvas that they could scroll.

But there is a secret I have been hiding since the very beginning. There was no need to make a special Canvas like pane with scrollbars!

I undertook the discussion since it would reveal the workings of borders and interior decorations. In particular, the details of how a custom widget creator would add scrollbar support to a new pane. As we already saw, there is a lot of work to do, and there was a bunch more to go... Probably about 3 or 4 postings worth.

But those details are now moot.

K - E - YIIIIIII

Never fear though. For those who want to do this or something similar with Widgetry here is the secret I was hiding:

You can already do this with a Form and a Canvas!

You see, a Form already has a #contentsExtent:. Just like we were starting to put on our ScrollingCanvas!

So, all one has to do is create a Form with a

#contentsExtent

, put a Canvas inside it with its frame fully attached, and then do some simple drawing calculations.

M - O - U - S - EEEEE

Here's the code:

	Smalltalk defineClass: #ScrollingCanvasFormWork
		superclass: #{Widgetry.UserInterface}
		indexedType: #none
		private: false
		instanceVariableNames: 'form canvas '
		classInstanceVariableNames: ''
		imports: 'private Widgetry.*'
		category: '(none)'

Standard stuff there.

	createInterface

		form := Form new.
		form frame: (FractionalFrame
			fractionLeft: 0.05
			right: 0.95
			top: 0.05
			bottom: 0.95).
		self addComponent: form.
		canvas := Canvas new.
		canvas frame: FractionalFrame fullyAttached.
		form addComponent: canvas.

See that? I created the Canvas, gave it a FractionalFrame, fully attached, and put it in the Form.

	hookupInterface

		form contentsExtent: 500 @ 500.
		form allScrollbars: true.
		canvas drawActionBlock:
			[:aGraphicsContext :pane |
			| oldLineWidth oldPaint offset enclosingForm |
			enclosingForm := pane enclosingPane.
			offset := pane scrolledOffset negated.
			oldLineWidth := aGraphicsContext lineWidth.
			oldPaint := aGraphicsContext paint.
			aGraphicsContext lineWidth: 5.
			aGraphicsContext paint: ColorValue red.
			aGraphicsContext 
				displayLineFrom: offset 
				to: offset + enclosingForm contentsExtent.
			aGraphicsContext lineWidth: 10.
			aGraphicsContext paint: ColorValue blue.
			aGraphicsContext \
				displayLineFrom: (offset x @ (offset y + pane enclosingPane contentsExtent y)) 
				to: (offset x + pane enclosingPane contentsExtent x @ offset y).
			aGraphicsContext paint: oldPaint.
			aGraphicsContext lineWidth: oldLineWidth].

Here I tell the Form to have a big contentsExtent... Just like we were doing with our ScrolledCanvas.

In our drawActionBlock: we get the enclosing pane, the Form, so we can get its contentsExtent. Then we get the Canvas' #scrolledOffset.

When you are in any pane, you can ask it what its #scrolledOffset is. That value is 0 @ 0 when the pane is just in a window. But if it is in a Form, or in a Form in a Form, the value will be the total amount of scroll offset that any higher enclosing forms may be affecting the pane in any way.

The #scrolledOffset is always a value less than or equal to 0 @ 0. Why we get and use the negated value is an important but now boring detail.

Here it is in action:

Good Night Boys And Girls

Well, that's it! No more blog postings about Widgetry. No more blogging for me at all.

I personally feel that Widgetry was and is a success. Unfortunately we never had enough resources to get it out sooner, nor in June when it went 1.0, enough resources to create the Tools to support it.

Widgetry is now part of that statistic of projects that have failed. Not because of technical merit, but due to wider circumstances.

And So It Goes
Sames

Widgetry Update

Final Update

September 11, 2007 16:27:30 EDT

As noted in the Smalltalk Tidbits, Industry Rants blog item: Cincom Smalltalk Product Direction: UI blog, Widgetry is being put to bed.

It is only appropriate though to make a final official update of the code base.

Version 1.33 of Widgetry, and Version 1.10 of WidgetryTesting are now published to the public repository.

Here are the changes from 1.12 of Widgetry:

  • Center align input/display for InputField
  • Grid resizes cause the recreation of columns (and thus rows and cells) over and over
  • WordWrapped TextEdit can scroll instead of wrap if close to right edge insertion
  • Flicker in Grid header row when resizing
  • Resizer in Form doesn't update panes outside the edge of Resizer
  • Hitting navigation keys while in TreeView edit, moves instead of accepts edit.
  • ScheduledWindow closeEvent: announces twice
  • Sorting announces SelectionChanged when it shouldn't
  • Consistent Grid rows and columns accessors
  • Resizing TabControl has display errors external and internal
  • Row headers as buttons look bad and display no interior decoration
  • GridHeader label not displaying when scrolled
  • Displaying Images in Buttons in A Sub-form in a scrolled Form and Resizing Problems
  • Removing columns does not reset maximum item width
  • Bad comparisons in RelationalFrame
  • Bad comment in #exitTabStyle
  • Widgets in scrolled forms don't react to mouse events properly
  • Panes in a Form in a Scrolled Form (double nesting) don't react to clicks or display correctly
  • Setting barColor: on ProgressBar causes DNU
  • GridAgent can answer itself instead of pane for handlerForMouseEvent:
  • ResizingSplitter problem when there is a Toolbar

Version 1.10 of WidgetryTesting has as a prerequisite a new package named WidgetryTestExtensions. This package is a special version of RBSunitExtensions and effectively replaces that version. It is not Widgetry specific. What it does is it allows a Bundle to be run one Package at a time. This is important for Widgetry's test since each test case in each package used a common resource to set the look policy.

Without this package, in order to run Widgetry's tests, you had to select each package in the testing bundle, and run it, then select the next and so on. If you tried to run the bundle as a whole, it could take as long as seven hours, and the tests wouldn't be guaranteed to complete.

Also this final version of the tests have been somewhat optimized, and the overall time to complete them has improved by up to 25%. On my admittedly fast machine, that still takes about 45-50 minutes though.

It's Good To Be The King

I have one more Widgetry posting to make this later week before I say goodbye to this blog.

While I won't be out here talking about GUI any more, I am still a member of the VisualWorks engineering team. I will be moving on to lead the Store project come next week.

Let us raise our glasses one last time, and join me in one final salute:

Widgetry is Dead, Long Live Widgetry

And So It Goes
Sames

Custom Panes

Horizontal Scrollbar Plus Plus

September 07, 2007 15:01:11 EDT

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 Goes
Sames

Custom Panes

Vertical Scrollbar Starting API

August 31, 2007 16:06:22 EDT

We've created and set up our ScrollingCanvas and an artist for it, ScrollingCanvasArtist. Now we need to add the API that the Scrollbars need in order to interact with our pane.

Today we'll address only the vertical scrollbar.

Testing 1-2 3

First we need to tell the world if we have an interior decoration. This is a simple method on our Artist
	ScrollingCanvasArtist>>hasInteriorDecoration
		^interiorDecoration notNil

This method is used by the inner edge clipping mechanism we have previously discussed to know if it should take into account a decorations additional clipping area. The default implementation in Artist answers false. If we don't answer the right value here, then when it comes time to display the pane after a scrollbar is turned on, the area where the scrollbar should display will be over drawn by the pane itself.

Everything I Know I Learned While Sleeping

There are two basic pieces of information that any scrollbar needs to know in order to do its job.

First it needs to know how high or wide the full scrolling area is. For instance, for a ListBox, this information vertically would be the number of items in the list box multiplied by the height of each item. Horizontally, it would be the maximum width of the longest item in the list.

Second it needs to know how what the current offset from the top or left of the whole area is currently at the top of the pane. For instance, for a ListBox that has 100 items, and can display only 10, the top might be 5.

Each of these has an accessor on our Artist.

For the full scrolling area it requires the method #totalScrollHeight.

For the top offset, it requires the method #topItemIndex.

There is another value needed. Scrollbars have a notion of a list. This is because they typically deal with text or enumeration panes. So to scroll one step, when pressing the up or down buttons, it asks how big the list is, and how big an item is.

For now, we'll say that an item is 1 (one pixel), and that the top is fixed at 1, and the list size is the height of our drawing.

In order to get up and running, we'll just put in fake values for now, and hook them up to real values later.

	ScrollingCanvasArtist>>topItemIndex
		^1
	
	ScrollingCanvasArtist>>totalScrollHeight
		^1

	ScrollingCanvasArtist>>listSize
		^self frame bounds height

In a future posting we'll hook up #listSize to a larger value so that we can do actual scrolling, but for now, this value will do.

Now we go back to our CustomWidgetWork and it to turn on the vertical scrollbar:

	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

And if we execute CustomWidgetWork open we see this:

We don't see the Scrollbar yet, but we do see that the right edge of the drawing is clipped to make room for it.

So the question is, why didn't the Scrollbar display?

The answer is simple... We never told it to display!

Do What I Say

In the inherited #displayOn: method, there is no code to tell the interior decoration to display itself:

	CanvasArtist>>displayOn: aGraphicsContext 
		| oldPaint oldClippingRectangle |
		oldClippingRectangle := aGraphicsContext clippingBounds.
		oldPaint := aGraphicsContext paint.
		aGraphicsContext clippingRectangle: (self artistAdjustedFrameBounds intersect: oldClippingRectangle).
		self hasBorder ifTrue: [self border displayOn: aGraphicsContext in: self artistAdjustedFrameBounds].
		(drawAction notNil and: [oldClippingRectangle intersects: self artistAdjustedInnerBounds]) ifTrue: 
			[aGraphicsContext clippingRectangle: (self artistAdjustedInnerBounds intersect: oldClippingRectangle).
			self executeDrawActionWith: aGraphicsContext].
		aGraphicsContext clippingRectangle: oldClippingRectangle.
		aGraphicsContext paint: oldPaint

In Artists that have interior decorations, there would be a line right after the self hasBorder that reads:

	self hasInteriorDecoration ifTrue: [self interiorDecoration displayOn: aGraphicsContext in: self interiorDecorationRectangle].

So we have to write our own displayOn: for our Artist:

	ScrollingCanvasArtist>>displayOn: aGraphicsContext 
		| oldPaint oldClippingRectangle |
		oldClippingRectangle := aGraphicsContext clippingBounds.
		oldPaint := aGraphicsContext paint.
		aGraphicsContext clippingRectangle: (self artistAdjustedFrameBounds intersect: oldClippingRectangle).
		self hasBorder ifTrue: [self border displayOn: aGraphicsContext in: self artistAdjustedFrameBounds].
		self hasInteriorDecoration ifTrue: [self interiorDecoration displayOn: aGraphicsContext in: self interiorDecorationRectangle].
		(drawAction notNil and: [oldClippingRectangle intersects: self artistAdjustedInnerBounds]) ifTrue: 
			[aGraphicsContext clippingRectangle: (self artistAdjustedInnerBounds intersect: oldClippingRectangle).
			self executeDrawActionWith: aGraphicsContext].
		aGraphicsContext clippingRectangle: oldClippingRectangle.
		aGraphicsContext paint: oldPaint

And we have to give it a #interiorDecorationRectangle method too:

	ScrollingCanvasArtist>>interiorDecorationRectangle
		^self hasBorder 
			ifTrue: [self frame bounds insetBy: self border innerEdgeRectangle] 
			ifFalse: [self frame bounds]

Coffee Klatch

While our new #displayOn: is basically just a copy of what is in CanvasArtist, we should discuss what is going on here, so if you ever want to do a more custom #displayOn: method, you can do it for yourself.

In any #displayOn: method, there is always a prolog and epilog which gathers the existing clipping bounds and paint, and later resets them. This is a requirement you should always obey. You Have Been Warned!

The next thing is also boilerplate where we set the #clippingRectangle: for the GraphicsContext. We do this by taking the clipping rectangle that came out of the GraphicsContext when it came in and have it intersect with what is called the #artistAdjustedFrameBounds. This method, #artistAdjustedFrameBounds has a default implementation on Artist, and for most situations, we don't have to be concerned about it.

However, you should know what it is for in case you need it. For some widgets in some looks, the bounds the user gives a pane isn't totally available to be displayed in. For instance, in the MacOSX look, most panes have a blue haze around them when they are active. Thus, you'll see in the artists for many of the MacOSX look panes, this area is clipped out of the bounds.

The next thing you should know is why we do this clipping at all, even if there is no special #artistAdjustedFrameBounds. During the regular course of events, when a window is asked to display itself or parts of itself, it gathers up a rectangle that represents the minimum amount of area that is required to redraw the window. That area may be bigger or smaller than the area of our pane, or may otherwise intersect the bounds of our pane. By making sure that we give the GraphicsContext a new clipping area that is only within our bounds, we can do two things. First, if for some reason the bounds it is giving us is outside of our bounds, we can as you see below our new line of code, skip doing our drawing at all. A small optimization, but one that is important. Second, if you have a smart draw routine you can only draw in the area that needs drawing, instead of drawing the whole pane.

Since it is possible that only the interior decoration has asked to display itself, we do our test for the first scenario based on the method #artistAdjustedInnerBounds. This is similar to the #artistAdjustedFrameBounds, except it is based on the inner bounds of the pane. It too has a default implementation on Artist, but you can override that implementation in your artist if you need to.

Our ScrollingCanvas has no smart drawing code to utilize for the second scenario, but it is there if you need it. Enumeration panes and TextEdits in Widgetry do have smart drawing.

Then we have the simple draw border code, and simple interior decoration code. Note as we discussed a couple of postings ago, borders and interior decorations are displayed using the #displayOn:in: protocol. We pass in the bounds of the area in which we want the object to display.

For the border, we give it the same #artistAdjustedFrameBounds we used for the clipping. For the interior decoration, we have a new method named #interiorDecorationRectangle. And as you see in the code above, it gives us a rectangle that is the whole bounds if there isn't a border, and the bounds inset by the border's inner edge if there is a border. This makes sure that the interior decoration never displays over the area where a border might be.

If you look at other implementers of #interiorDecorationRectangle, you'll see that some do extra clipping. Just like for the #artistAdjustedFrameBounds, this gives us the ability to do look or pane relative tweaking if needed. We don't need anything special so our #interiorDecorationRectangle is simple.

Pretty Picture

Now if we open our CustomWidgetWork, we see this:

We now see our vertical scrollbar, but it is disabled. That's right though.

Because we have told it that the top item is 1, and the scroll height is 1, it thinks there is nothing to scroll, and in fact that is correct. The scrollbar thus displays itself disabled.

What's Next

We'll leave our vertical scrollbar and next time work on the horizontal scrollbar, adding the API it needs to also show itself disabled.

And So It Goes
Sames

Custom Panes

Interior Decorations

August 24, 2007 13:57:56 EDT

In Widgetry, an interior decoration is an optional part of a widget much like a border but which is implicit to a pane. A simple example of an interior decoration can be seen in a Win9x look button:

The interior decoration is that part of the button that makes it look 3D/raised. If you are thinking that this interior decoration looks like a border, well in fact it is. For a Button with the Win9x look the interior decoration is a raised border object. For an InputField in the Win9x look the interior decoration is a lowered border object:

In these cases, the simple difference is that the decorations are an intrinsic part of the panes. They reside on the Artist just like a Border does and in these cases they consist of border objects. Like a border object an interior decoration clips the area where things inside the pane are drawn.

Basically a border is something a developer can add to panes that surround a pane and an interior decoration is something that is built into a pane an sits on the interior edges of the pane. Where a user can add and remove borders, the interior decoration is something the custom widget designer must build into the pane itself.

Interior decorations don't stop at just border like objects. Scrollbars are implemented in Widgetry as a kind of interior decoration. The reason we even got into this lengthy discussion of scrollbars and interior decorations is because ScrollingCanvas is expected to have scrollbars.

Widgetry has a ScrollbarDecoration object that does about 1/2 of the work of supplying a scrollbar for a pane. We will be using this in our ScrollingCanvas pane. The hard part is that it is up to us as a custom widget designer to implement the other half.

Notation

A quick note about notation. When you see a method signature that reads SomeClass>>method, that means that the method is implemented on the instance side of SomeClass. Similarly, if you see SomeClass class>>method, that means that method is implemented on the class side of SomeClass. Since we are going to be working with Pane, Artist and Agent classes, I'll be using this notation to indicate on which class these methods are implemented.

Through The Looking Glass

This is where we start going down the rabbit hole.

There are two general parts of having a pane support a scrollbar. First is creation of the interior decoration and its simple support code. Second is the API the hosting pane must implement.

An interior decoration is created in the initialize method of a pane. Here is the code we add to our ScrollingCanvas:

	ScrollingCanvas>>initialize
		super initialize.
		self interiorDecoration: self defaultDecoration.

Of course, that means we need two more methods:

	ScrollingCanvas>>interiorDecoration: aDecoration
		artist interiorDecoration: aDecoration.
		
	ScrollingCanvas>>defaultDecoration
		^ScrollbarDecoration new

For completeness sake, we add an accessor to interiorDececoration on the pane:

	ScrollingCanvas>>interiorDecoration
		^artist interiorDecoration		

As noted above, the actual interior decoration sits in the Artist, and it is the Artist responsibility to display it when needed.

The inherited CanvasArtist has no interior decoration, therefore we first need to create a ScrollingCanvasArtist with an interiorDeocration instance variable:

	Smalltalk.Widgetry defineClass: #ScrollingCanvasArtist
		superclass: #{Widgetry.CanvasArtist}
		indexedType: #none
		private: false
		instanceVariableNames: 'interiorDecoration '
		classInstanceVariableNames: ''
		imports: ''
		category: 'CustomWidgetry'

And we need to have our ScrollingCanvas point to our new artist:

	ScrollingCanvas>>artistClass
		^ScrollingCanvasArtist

Now we can implement the artist version of #interiorDecoration and #interiorDecoration:

	ScrollingCanvasArtist>>interiorDecoration
		^interiorDecoration

	ScrollingCanvasArtist>>interiorDecoration: aDecoration
		interiorDecoration ifNotNil: [self interiorDecoration setPane: nil].
		interiorDecoration := aDecoration.
		aDecoration isNil
			ifFalse: 
				[self interiorDecoration setPane: pane.
				self frame innerEdgeRectangle: (self innerEdgeRectangleIncludingBorder: aDecoration)]
			ifTrue: [self frame innerEdgeRectangle: self borderInnerEdgeRectangle].
		pane invalidate

Time to discuss what is going on here. An interior decoration points back to the hosting pane. If there is already an interior decoration when we are setting a new one then first we unhook the existing decoration by clearing its back pointer.

Then we set the passed in decoration as the new interior decoration. Then we test if the new decoration is actually nil and if not, we set the back pointer for our pane and update the frame's inner edge rectangle, and finally invalidate the whole pane.

This implies that an interior decoration is pluggable, and in fact that is true. While there is nowhere in Widgetry that uses this capability a custom decoration developer could come along and use it.

Rabbits Everywhere

Now we go deeper down the rabbit hole. Writing your own custom widget has a lot of these, so get comfortable... we'll be digging around down here a lot.

When we played with the borders, we just added them and magically all of the right things happened. The border object displayed around the edges of our pane and the pane never displayed in the area where the border was. All this magic is implemented in ArtistWithBorder. There is no similar magic for interior decorations unless you are inheriting from a pane that already uses an interior decoration.

Frames

Besides defining where a pane is in a window, a Frame also supports supplying information about clipping areas within it. When we send #bounds to a frame, it answers the whole visible area of the pane. We can also send #innerBounds to a frame. This message takes the bounds of the pane, and subtracts out the inner edges that may be taken up by a border and/or a decoration.

When we add a border, the ArtistWithBorder automatically figures out the inner edge of the border and tells the frame using the above seen #innerEdgeRectange: method.

It is the Artist responsibility to keep the frame informed as to any changes to the inner edge area that the pane should not draw on. Then when the artist draws, if first sets the clipping bounds of the graphics contest to be the #bounds, draws any border and interior decoration. Then before it draws whatever is inside the border and decoration, it sets the clipping bounds to the #innerBounds.

Under the hood the inner edge is implemented as a rectangle. However it is what I call a degenerate rectangle. This is because instead of identifying an area it represents edge sizes. So if you look at the innerEdgeRectangle of a frame for a pane with a simple raised border, you'll see that the value is typically: 2 @ 2 corner: 2 @ 2. While the area of that rectangle is 0, if we ask for the left, right, top or bottom, we'll get the number 2.

Fortunately there are support methods for doing the work for us. In our Border last time we saw that it had an API that supplied the sizes of each edge. The ScrollbarDecoration does the same thing for us and by using the support methods #innerEdgeRectangleIncludingBorder: and #borderInnerEdgeRectangle which are implemented in the Artist class, we can set the inner edge rectangle value for our frame without having to do all the dirty work ourselves.

Turn Me On Dead Man

If we go ahead and now execute CustomWidgetWork open, we see that nothing seems to have changed:

As we saw in our previous How To postings, when working with scrollbars we have to turn them on in order to have them be seen. Without that our ScrollbarDecoration is telling the frame that the inner edge is 0.

Therefore we need to add the code to turn on our scrollbars. One method each for the vertical and horizontal scrollbar. They are implemented right on our pane:

	ScrollingCanvas>>horizontalScrollbar: aBoolean 
		self isOpen ifFalse: 
			[^self 
				configure: #horizontalScrollbar:
				for: self
				argument: aBoolean].
		self interiorDecoration horizontalScrollbar: aBoolean.
		self invalidateWithBackground.
		self announce: PreferredExtentChanged
		
	ScrollingCanvas>>verticalScrollbar: aBoolean 
		self isOpen ifFalse: 
			[^self 
				configure: #verticalScrollbar:
				for: self
				argument: aBoolean].
		self interiorDecoration verticalScrollbar: aBoolean.
		self invalidateWithBackground.
		self announce: PreferredExtentChanged

Configurations

One of the requirements of Widgetry is that it should never matter when you send a message to a pane. The user of a pane must be able to send a message to any pane without regard to if the pane is visible or it has been added to a window or even if that window is open yet.

Unfortunately there are many things that are not known when some messages are sent to a pane. In our case, when we turn on a scrollbar if the pane has not yet been added to a window, or the window isn't open yet, particularly if our pane is using a FractionalFrame, the size of the pane isn't known.

As you might imagine, without knowing the size of the pane it is not possible to figure such things such as how big the pane is in relation to the area that needs to be scrolled. Without that information we can't figure out how big the scrolling thumbs should be, or even if the scrollbar should be enabled or not.

Widgetry comes with a simple mechanism called DelayedConfigurationScript that takes care of this for us... but it is our responsibility as a developer of a custom widget to use it where needed.

Widgetry Panes take configuration messages #configure:for: and #configure:for:argument: and #configure:for:arguments:, collects and holds on to the messages represented by these configuration script messages, and when the window is finally open replays the messages once and then disposes of them.

The #isOpen message will only answer true when the pane it is sent to has been added to a window that is itself open. With that we can test and see if our pane is open, and if it is not we send a configuration script message that will call this same method when the window is open.

With this mechanism Widgetry doesn't require the #preBuild, #postBuild #postOpen horror that Wrapper required the user to manage. Instead it is up to Widgetry, or in this case, the developer of a Widgetry pane to make it so that the user never has to care about such stuff.

After the configuration code, we write the actual setting code for the scrollbars followed by an invalidate to show the newly turned on scrollbar and then an announcement.

Preferred Extent

That announcement of PreferredExtentChanged at the bottom of our scrollbar activation methods tell the world that a very important thing has changed about our pane.

The preferred extent of a pane is the minimum extent that a pane can have to display its contents. If you create a ListBox with no scrollbars turned on and no items, when you send the message #preferredExtent to the pane, it will answer the size of the interior decoration edge. If you add a border to the ListBox, then it would include the border. If you add items, it would add the to the Y value the result of the number of items times the line height, and it would add to the X value the width of the longest item's display width.

Most important to us is that if you turn on or off scrollbars, the size of the interior decoration changes.

The user of a pane may decide that if the preferred extent changes that they want to change the layout of a pane... Maybe make it larger or smaller. Users do strange things. Therefore it is yet another responsibility of the custom widget designer that if something that will change the preferred extent of a pane, such as adding or removing a scrollbar, that the pane announce PreferredExtentChanged.

As you see it takes a lot of work to make magic happen.

What's Next

At this point if we tried to turn on either or both of our scrollbars in our CustomWidgetWork, our image would either blow up or we'll get an exception depending on if we have turned on the "Use event faithful debugging" user setting.

There is still much more in the way of APIs we have to implement on our pane in order to get our scrollbars to even be visible. We'll start dealing with that next time.

And So It Goes
Sames

Custom Panes

Borders

August 17, 2007 17:29:57 EDT

Jumping right in today with little fanfare.

Border

Where a Pane has a Frame and an Agent, a border object only needs an Artist. It gets its layout information from the Pane which hosts it when it is asked to display itself. Indeed, a border object doesn't respond to the standard #displayOn: method. Instead, it uses a #displayOn:in: method, where the second argument is a rectangle which defines the bounds in which it should display itself. When in use, ArtistWithBorder or subclasses of that are required to use the #displayOn:in: method to display the border.

A border object has four values that describe the width of each of its edges. Typically, all of these edges are the same size, but there is no requirement that they be equal.

To create a new border object, one first creates a subclass of BorderDecoration. We'll create a border named TwistBorder, and use that in our example:

	Smalltalk.Widgetry defineClass: #TwistBorder
		superclass: #{Widgetry.BorderDecoration}
		indexedType: #none
		private: false
		instanceVariableNames: ''
		classInstanceVariableNames: ''
		imports: ''
		category: 'CustomWidgetry'

Each border object, as stated above, uses an Artist to do its actual displaying. The BorderDecoration class has a lot of default behavior which we will override.

Border Artist

By default, a BorderDecoration goes to the current widget policy, which then finds the current border policy, and gets a default artist. For instance, with the WinXP look, the default border artist class is WinXPWindowBorderArtist.

When the BorderDecoration is created, the border object and its Artist are hooked up to point at each other. This way the Artist can get information, such as the sizes of the edges, and the border can in turn, if it needs to ask the Artist about colors and so on.

The border object is sent the message #displayOn:in:, which it in turn sends to the artist. You'll find a lot of double dispatch like this going on in border objects and their artists. The upside to using this pattern is that we get a potential for reuse and plugability. For instance a simple RaisedBorder can use different look artists to do the same work. The downside of using this pattern, is that it makes it harder to keep track of what is going on.

For our TwistBorder though we aren't going to simply draw a bunch of vertical and horizontal lines for the edges. We're going to draw a twisted rectangle. Therefore, much of the double dispatch that the standard border and border artist objects use to draw standard lines won't be usable for us.

So, we'll create our own border artist. All border artists are instances or subclasses of BorderDecorationArtist. Ours will be called TwistBorderArtist:

	Smalltalk.Widgetry defineClass: #TwistBorderArtist
		superclass: #{Widgetry.BorderDecorationArtist}
		indexedType: #none
		private: false
		instanceVariableNames: ''
		classInstanceVariableNames: ''
		imports: ''
		category: 'CustomWidgetry'

Now we have to tell our TwistBorder to use the our new artist. We do that by supplying our own #defaultArtist method in TwistBorder:

	defaultArtist
		^TwistBorderArtist new

Edges

Another default in the BorderDecoration is the default size of the edges, which is 0. We are going to make our edges 6 wide. Yes that's a lot of space there, but our goal hear is education, not space saving.

We supply our own version of the method #setStandardAllEdgeSize, and define our own default edges in TwistBorder:

	setStandardAllEdgeSize
		self allEdgesSize: 6

Note, even though the default is set to 6 by this, a user of our TwistBorder could change it by using any of the accessing methods in BorderDecoration, the superclass of TwistBorder.

We can try to use our TwistBorder in our 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

But if we execute CustomWidgetWork open, we get an error:

It is saying that #displayLeftEdgeOn:in: is being called but it is a subclass responsibility. This is because if we look at the default behavior of #displayOn:in: in BorderDecorationArtist, we see that it calls four methods on the border object that aren't implemented in our TwistBorder class, but are implemented as #subclassResponsibility methods in the BorderDecoration class.

Drawing

We have two choices now. We can either write our own #displayOn:in: method right there in TwistBorderArtist to draw the edge lines, or we can supply our own four edge methods in our TwistBorder to do the drawing one at a time.

I'm going to be politically correct and chose the four edge drawing methods which are named #displayLeftEdgeOn:in:, #displayRightEdgeOn:in:, #displayTopEdgeOn:in: and #displayBottomEdgeOn:in:. Why? While there is nothing strictly wrong with just doing your displaying right there in the #displayOn:in: of TwistBorderArtist, that would break the #subclassResponsiblity rule that BorderDecoration states for these four methods.

There is no Subclass Responsibility Police to force you to make the same choice I have here, so you don't have to do what I do, but I feel obligated to do what I said I should do.

Here are those four methods for TwistBorder:

	displayTopEdgeOn: aGraphicsContext in: aRectangle

		| oldPaint |
		oldPaint := aGraphicsContext paint.
		aGraphicsContext paint: ColorValue red.
		aGraphicsContext 
			displayLineFrom: aRectangle origin
			to: aRectangle topRight + (self rightEdgeSize negated @ self topEdgeSize).
		aGraphicsContext paint: oldPaint

	displayRightEdgeOn: aGraphicsContext in: aRectangle

		| oldPaint |
		oldPaint := aGraphicsContext paint.
		aGraphicsContext paint: ColorValue blue.
		aGraphicsContext 
			displayLineFrom: aRectangle topRight + (self rightEdgeSize negated @ self topEdgeSize)
			to: aRectangle corner - 1.
		aGraphicsContext paint: oldPaint

	displayBottomEdgeOn: aGraphicsContext in: aRectangle

		| oldPaint |
		oldPaint := aGraphicsContext paint.
		aGraphicsContext paint: ColorValue red.
		aGraphicsContext 
			displayLineFrom: aRectangle corner - 1
			to: aRectangle bottomLeft + (self leftEdgeSize @ self bottomEdgeSize negated).
		aGraphicsContext paint: oldPaint

	displayRightEdgeOn: aGraphicsContext in: aRectangle

		| oldPaint |
		oldPaint := aGraphicsContext paint.
		aGraphicsContext paint: ColorValue blue.
		aGraphicsContext 
			displayLineFrom: aRectangle topRight + (self rightEdgeSize negated @ self topEdgeSize)
			to: aRectangle corner - 1.
		aGraphicsContext paint: oldPaint

And here is what it looks like now that we have the four methods written:

First we note that we use corner - 1 for our bottom right. This is because when drawing lines the actual place where a draw goes to isn't at the point we specify but rather the underlying draw mechanism uses that point as a place that is in effect the just above and to the left of where it draws. In effect you can consider that drawing happens between points, if that helps. In our case if we simply said corner then the line would fall outside the bounds of the pane. So we subtract 1.

Also instead of assuming what the edge sizes were, we used the accessor methods inherited from BorderDecoration to do our calculations. This makes sure that if someone comes along and uses the TwistBorder but tells the edges to be of a different size, or each a different size, then the drawing will still execute properly.

Next note that I take care in each case to save the old paint and restore it after I'm done doing my drawing. This is one of the many responsibilities you must accept when you become a creator of display code. If you set a value in a GraphicsContext, you should always reset it to whatever it was previous to your modification before you exit your drawing method.

Finally note that I specifically used direct ColorValues in the code. More appropriate would be for me to have four accessing methods on the Artist, one for each edge color, but using the default stated colors if the values are not set. This make it possible for someone to come in and not have to live with the blue and red I defined.

Extra Credit

Let's do that.

First we add instance variables to TwistBorderArtist:

	Smalltalk.Widgetry defineClass: #TwistBorderArtist
		superclass: #{Widgetry.BorderDecorationArtist}
		indexedType: #none
		private: false
		instanceVariableNames: 'topColor rightColor bottomColor leftColor '
		classInstanceVariableNames: ''
		imports: ''
		category: 'CustomWidgetry'

Then we add accessors that default to our standard colors and mutators that allow the user to override the colors:

	topColor
		^topColor ifNil: [ColorValue red]

	topColor: aColorValue
		topColor := aColorValue

	rightColor
		^rightColor ifNil: [ColorValue blue]

	rightColor: aColorValue
		rightColor := aColorValue

	bottomColor
		^bottomColor ifNil: [ColorValue red]

	bottomColor: aColorValue
		bottomColor := aColorValue

	leftColor
		^leftColor ifNil: [ColorValue blue]

	leftColor: aColorValue
		leftColor := aColorValue

Now we change our display methods in TwistBorder to use these new accessors:

	displayTopEdgeOn: aGraphicsContext in: aRectangle

		| oldPaint |
		oldPaint := aGraphicsContext paint.
		aGraphicsContext paint: artist topColor.
		aGraphicsContext 
			displayLineFrom: aRectangle origin
			to: aRectangle topRight + (self rightEdgeSize negated @ self topEdgeSize).
		aGraphicsContext paint: oldPaint

	displayRightEdgeOn: aGraphicsContext in: aRectangle

		| oldPaint |
		oldPaint := aGraphicsContext paint.
		aGraphicsContext paint: artist rightColor.
		aGraphicsContext 
			displayLineFrom: aRectangle topRight + (self rightEdgeSize negated @ self topEdgeSize)
			to: aRectangle corner - 1.
		aGraphicsContext paint: oldPaint

	displayBottomEdgeOn: aGraphicsContext in: aRectangle

		| oldPaint |
		oldPaint := aGraphicsContext paint.
		aGraphicsContext paint: artist bottomColor.
		aGraphicsContext 
			displayLineFrom: aRectangle corner - 1
			to: aRectangle bottomLeft + (self leftEdgeSize @ self bottomEdgeSize negated).
		aGraphicsContext paint: oldPaint

	displayLeftEdgeOn: aGraphicsContext in: aRectangle

		| oldPaint |
		oldPaint := aGraphicsContext paint.
		aGraphicsContext paint: artist leftColor.
		aGraphicsContext 
			displayLineFrom: aRectangle bottomLeft + (self leftEdgeSize @ self bottomEdgeSize negated)
			to: aRectangle origin.
		aGraphicsContext paint: oldPaint

There is a trade off here. While we have abstracted out and farmed out responsibility to various objects, we have at the same time made the artist and border object more tightly coupled. There is now a requirement implied that the artist in the use of our border object respond to these edge color methods.

A long time ago someone said: Design == Choices.

Where you may not care for the choices I have made here, such as edge color methods and the display messages being supplied by the border itself, you are not obliged to do it like I did. You are only required to think it through and accept the consequences of your choices.

What's Next

Well, instead of a couple of borders as I thought we would do last time, we only did one. Nonetheless we got to all the details that I wanted to get into. No harm no foul there.

Also last time I thought we would start looking into Decorations this time. However given the amount of information in today's posting it seems more appropriate to do that in a new posting next time.

Obviously my powers of foresight aren't very good on this. So, instead of making big promises I'll just say: Next Time: Decorations.

And So It Goes
Sames

Custom Panes

Creating Custom Panes - The Beginning

August 10, 2007 17:57:37 EDT

Here we are on the start of an adventure.

The ultimate goal is to create a new custom Pane for Widgetry. On one of the public forums, it was suggested, and I agreed, that it would be nice to have a scrolling version of the Canvas. The resulting Pane will be called ScrollingCanvas (yes I know, you're just amazed at my creativity).

Along the way, I'll try to educate you on how Widgetry works from the top down in order to create our ScrollingCanvas, taking time along the way to point out general topics and issues.

At various points I'll also express some of the philosophy behind the design of Widgetry.

Provisions

Our adventure here may be more boring at times than the previous Fun With postings. Certainly, there will be more code and less pictures along the way. There may be long periods of just words explaining things.

This is where you come in. If I am unclear about something, please post your questions or issues in a comment and I'll do my best to answer it.

As I said at the top, I'll be going out of my way to explain things from the top down. This means I won't assume you have studied the various parts of Widgetry beforehand. That means some of the prose here may seem tedious. I beg your forbearance.

Starting Gate

We'll start off with a working Package we'll call CustomWidgetry. I'll be posting any code we write to that Package on the Cincom Public Repository.

So, now that we have a place to write our code, as with any Smalltalk project, the next thing one has to do is determine where and in what hierarchy one should place their initial subclass.

When we were working on our Fun With work, we created subclasses of UserInterface. We want to create a Pane, so we want to create a subclass of one of the Pane classes.

Here are some places that you might want to subclass from depending on what kind of Pane you are creating:

    If you are creating a pane that is mostly about text and editing, you might want to choose EditingPane.
    If you are creating a pane that is mostly about a list of things, and manipulating the items in that list, you might want to choose EnumerationPane.
    If you were creating a pane that is made of two distinct horizontally side by side parts, like the DropDownList which has a button and an input field, you might want to choose ComponentPair.
    If you are creating a pane that is a composite of more than just two parts or parts that are not horizontally side by side, you might want to choose ComponentPane.
    If you are creating a pane that is nothing line anything else in the world, then you will want to choose Pane.

Of course, in our case, we pretty much know where we want to start. We're making a specialization of the Canvas pane, so, the right place to start is to make a subclass of Canvas, which as we said, we'll call ScrollingCanvas.

	Smalltalk.Widgetry defineClass: #ScrollingCanvas
		superclass: #{Widgetry.Canvas}
		indexedType: #none
		private: false
		instanceVariableNames: ''
		classInstanceVariableNames: ''
		imports: ''
		category: 'CustomWidgetry'

There is a choice here that I have made, but it may not seem obvious. When we did our Fun With work, we created our UserInterface subclasses in the Smalltalk name space, and imported (private) the Widgetry name space. Here I created ScrollingCanvas in the Widgetry name space itself.

The effect is the same when we're coding. All of Widgetry will be visible within our work.

But let us imagine that you are working on some custom widgets that you don't want to keep in your own name space. In that case, you have a few options. You could go to your name space and private import Widgetry:

	Smalltalk defineNameSpace: #MyNamespace
		private: false
		imports: 'private Widgetry.*'
		category: 'MyNamespace'

However, for whatever reason, maybe MyNamespace is supposed to stand alone. In that case, one would create our ScrollingCanvas like this:

	Smalltalk.MyNamespace defineClass: #ScrollingCanvas
		superclass: #{Widgetry.Canvas}
		indexedType: #none
		private: false
		instanceVariableNames: ''
		classInstanceVariableNames: ''
		imports: 'private Widgetry.*'
		category: 'CustomWidgetry'

Philosophy 101

There are many places in Widgetry that come with automatic default behavior. This is true for existing panes, as well as when you are creating custom panes.

We are at one of those places now. When you create any Pane subclass you don't have to do anything else but create the class itself in order to add it as a component to a UserInterface.

Fun With Custom Widgets

So, we'll now create a subclass of UserInterface, we'll call CustomWidgetWork:

	Smalltalk defineClass: #CustomWidgetWork
		superclass: #{Widgetry.UserInterface}
		indexedType: #none
		private: false
		instanceVariableNames: 'customWidget '
		classInstanceVariableNames: ''
		imports: 'private Widgetry.*'
		category: '(none)'

And in the #createInterface method, we'll add our new custom widget:

	createInterface

		customWidget := ScrollingCanvas new.
		customWidget frame: (FractionalFrame withFractionsFromRectangle: (0.1 @ 0.1 corner: 0.9 @ 0.9)).
		self addComponent: customWidget

And if we execute CustomWidgetWork open, we see our old friend the empty window:

What is important isn't that we see nothing, but that we didn't see an error show up. But to prove that all is really well, here is code to show where we are in the window, using the API that our superclass, Canvas, provides:

	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]

And now our CustomWidgetWork looks like this:

General Widgetry Design 101

There are four parts to any Widgetry Pane: Pane, Agent, Artist and Frame.

A Pane subclass is the focus of where a user interacts with a widget. This is the object a user instantiates. This is the object that from the user perspective all Announcements come from. This is the object that will hold any model information about the widget. This object also holds on to the Agent, Artist and Frame instances.

Most importantly, the Pane subclass is the place where all the public API for that widget must reside. This brings us to another Widgetry Philosophy point: The API on a Pane should be the only place a user has to go to manipulate or configure a widget. If a Pane user has to make a call to an Agent or Artist method, they should be able to do so without sending #agent or #artist in their code. In other words, from the Pane user perspective, the agent and artist shouldn't seem to exist. On the other hand, from a custom widget developer's perspective, our hands will at times get dirty.

An Agent subclass is the place where all of the keyboard and mouse behavior that is unique for the widget is written. An Agent holds on to help text for a widget if there is any. The Agent is responsible for keeping information about the visibility and enablement of a widget, if it cares. Depending on how the design of a pane is supposed to interact with a user, the Agent may tell the Artist to draw things, based on mouse or keyboard events it processes. The Agent is also where any local default popup menu resides.

An Artist subclass is the place where all the drawing takes place. An Artist subclass will hold on to a Border object if the widget is designed to have an external border. An Artist subclass will hold on to a Decoration object if that widget is designed to have an interior decoration. More on borders below, and decorations in the next posting.

A Frame (actually an AbstractFrame subclass) is where all the layout information for a widget is held. Calculations about where a widget is in the world of a window or a Form all take place in the AbstractFrame subclass. The Agent and Artist often ask the Frame directly or indirectly for information about where an event took place or where to draw things.

Unlike the Artist or the Agent, the Frame is there as a mostly public object visible to the user.

A Frame answers two sets of information about the layout of a widget. The bounds of the widget with a 0 @ 0 origin and some extent, and the bounds of the widget relative to the window. The former is fully public information. The latter, the window relative position, is mostly private or protected from the widget user point of view. From the custom pane developers point of view, nothing is sacred about a Frame.

A Frame also holds on to information about what is very important to the pane developer but not so much to the widget user. That is what is called the inner bounds. Where the bounds of a frame describes the whole area where a pane exists, the inner bounds describes the area within the widget that is available for general drawing. This inner bounds represents the whole bounds that is possibly clipped by the area of a border or a decoration that is never available to draw on.

Artists and Agents

Pane defines two default methods

	agentClass
		^Agent
		
	artistClass
		^Artist

Each subclass can override the #agentClass or #artistClass to supply its own default Artist or Agent.

Agent is pretty dumb. It simply knows how to do nothing. But it explicitly knows how to do that. It has a lot of default methods that do nothing that a smarter Agent subclass may want to implement.

Artist is also pretty dumb. Where Agent knows explicitly how to do nothing about keyboard and mouse events, Artist knows explicitly how to not draw anything.

The question then is, when you are creating a custom widget, should you use an existing Agent or Artist, or should you create your own, and if so, where should you subclass from?

For Agents there are four reasonable possibilities.

    If your widget won't interact with the keyboard or mouse and it will never be disabled or made not visible, then you can use Agent itself.
    If your widget won't interact with the keyboard or mouse, but it should be able to be disabled or made invisible, then you can use VisibilityAgent.
    If your widget is a subclass of an existing pane that already has its own agent, then you can simply use the agent from the superclass or subclass that agent and extend it.
    If your widget is in its own world, then you can subclass Agent or VisibiltyAgent, depending on if you want to have your pane have enablement and visibility behavior.

For Artists, there are also four similar possibilities

    If your widget doesn't want to draw anything, then you can use Artist itself... Maybe a widget that is all click and no draw.
    If your widget does want to draw but only a border (see below), then you can use ArtistWithBorder.
    If your widget is a subclass of an existing pane that has its own artist, you can simply use the artist from the superclass or subclass that artist and extend it.
    If your widget is in its own world, then you can subclass Artist or ArtistWithBorder, depending on if you want to have your pane to have a border or not.

Borders

Most but not all panes in Widgetry can have a Border. As implied above, this is determined by if the pane's Artist is a subclass of ArtistWithBorder. A Pane which uses a ArtistWithBorder or a subclass, has to have a simple API that talks to the artist:

	borderType: aSymbolOrNil
		artist borderType: aSymbolOrNil.

	border: aBorderDecorationOrNil
		artist setBorder: aBorderDecorationOrNil.
	
	border
		^artist border

The ArtistWithBorder knows how to deal with this API, and once you add it to your pane, or if you are using a pane subclass that already has it, your pane can have a border.

In our case, Canvas has an artist: CanvasArtist which is a subclass of ArtistWithBorder. Thus our ScrollingCanvas already has the ability to have a border.

There are five border types: #ridged, #raised, #etched, #lowered and #line.

Simply sending #border: <aBorderTypeSymbol> to our pane will automatically do the right thing.

Here are examples of all five with our ScrollingCanvas, each with one of the following lines uncommented:

	"Optional Border code:"
	"customWidget borderType: #ridged"
	"customWidget borderType: #raised"
	"customWidget borderType: #etched"
	"customWidget borderType: #lowered"
	"customWidget borderType: #line"


Note that when we give our pane a border, we don't have to do anything special in our display code to make sure we don't draw over the border. This is because one of the responsibilities of an Artist is to clip the display area of the graphics context to the area inside the border before it draws inside itself. This is where the Frame and the Artist start to interact.

When a border is added to a pane, it in effect tells the frame how big the edges of the border are. The frame remembers this information, and when it is asked what its inner bounds is, it answers only the area where the border isn't. The Artist uses this information to clip the display area as noted above, and from there, the pane just goes on its way, not even knowing that it isn't displaying where the border sits.

This is something we'll get into more depth about in the future as we write our own artist for our custom widget. Needless to say, there are some things that happen automatically for you, and other things you have to pay attention to.

One of those non automatic things is another part of the Widgetry Philosophy: Panes are dynamic. It should always be possible for the user to change a widget dynamically on the fly using the existing public protocol of a Pane. Thus, if you press the middle button on the mouse while over our ScrollingCanvas in our CustomWidgetWork window (what was historically called by Smalltalk the Blue button), and select Inspect Widget, you get an inspector, where you can execute one of the following:

	self borderType: #ridged
	self borderType: #raised
	self borderType: #etched
	self borderType: #lowered
	self borderType: #line
	self borderType: nil.

You'll notice that the widget changes (or removes in the case of nil) the border, and the drawing it does is correct in all circumstances.

What's Next

You notice I didn't get into the #border: method in the above. This is there in case a user, or you, want to create a custom border object that displays something other than just these five basic types.

Next time we'll start by making a couple of fancy Border objects and in doing so we'll understand the mechanics of the Border objects. Then we'll start looking into what an interior decoration is, and how that is different from a Border.

And So It Goes
Sames

How To

Fun With TabControls

August 03, 2007 16:35:23 EDT

Today we look at the TabControl, which after all our other HOW-TOs, seems pretty simple.

Design

We'll create a window with a TabControl in it, then we'll add pages to the TabControl using previous work, and then show how to make the pages display well.

As always, we start off with a subclass of UserInterface:

	Smalltalk defineClass: #TabControlWork
		superclass: #{Widgetry.UserInterface}
		indexedType: #none
		private: false
		instanceVariableNames: 'tabControl '
		classInstanceVariableNames: ''
		imports: 'private Widgetry.*'
		category: '(none)'

Now we'll add our TabControl in a #createInterface method:

	createInterface

		tabControl := TabControl new.
		tabControl frame: (FractionalFrame
			fractionLeft: 0.05
			right: 0.95
			top: 0.05
			bottom: 0.95).
		self addComponent: tabControl

Now if we execute TabControlWork open, this is what we see:

While that's not very exciting, there is one thing to note here. When there are no tabs, the area of the TabControl is just the full page. One of the options of the TabControl is to not show tabs at all with #displayTabs: false. The default is true.

If you do turn off displaying of tabs then the TabControl can be used as the basis of a Wizard. You fill up your TabControl with pages, and then use the #showPage: anInteger API to change the page. More on that in our Extra Credit

Like Sand Through An Hourglass

To add tabs to a TabControl, we add pages. A page is made up of a Form and optional tab features. The features are a label and/or an image, and if both an optional flag to display the image on the right or left.

We'll use the knowledge we gained last time to turn existing UserInterface subclasses into Forms, and add pages with the result. We'll start off with our last work: MoreFormWork and use that as our first page. Here is the #hookupInterface method:

	hookupInterface

		tabControl addPage: (Form new addComponentsFromClass: MoreFormWork).

Now if we open our example, this is what it looks like:

We see that spindly tab up there, but the page we added didn't show. This is not a bug. By default, the initial tab is page 0, or no page. If we go ahead and click on our first tab up there, this is now what we see:

That's better. Since total size of the form was created for a window that was 200 @ 200 in size, all that falls outside that in the TabControl's page is clipped. If you resize the window, the rest of the page will show as the TabControl resizes because of the FractionalFrame we used.

The Prisoner

Let's add another page, this time using RadioButtonWork from way back, and this time we'll add a label for the page:

	hookupInterface

		tabControl addPage: (Form new addComponentsFromClass: MoreFormWork).
		tabControl
			addPage: (Form new addComponentsFromClass: RadioButtonWork)
			label: 'Radio Buttons'

And now reopening and selecting the Radio Buttons tab, we see:

Again, you would have to resize the window to see all of the page.

The Cool Kids

Now we'll add another page, this time using ListBoxWork, and we'll use an image instead of a label. For fun, we'll just take the balloon image that is supplied by the ListIconLibrary class:

	hookupInterface

		tabControl addPage: (Form new addComponentsFromClass: MoreFormWork).
		tabControl
			addPage: (Form new addComponentsFromClass: RadioButtonWork)
			label: 'Radio Buttons'.
		tabControl
			addPage: (Form new addComponentsFromClass: ListBoxWork)
			image: (ListIconLibrary visualFor: #balloon)

And this is what that looks like when the balloon tab is selected.

Get The Lead Out

Now we'll add a page using the ResizingSplitterWork, and we'll have both a label and an image. This time, we'll use the bug image:

	hookupInterface

		tabControl addPage: (Form new addComponentsFromClass: MoreFormWork).
		tabControl
			addPage: (Form new addComponentsFromClass: RadioButtonWork)
			label: 'Radio Buttons'.
		tabControl
			addPage: (Form new addComponentsFromClass: ListBoxWork)
			image: (ListIconLibrary visualFor: #balloon).
		tabControl
			addPage:  (Form new addComponentsFromClass: ResizingSplitterWork)
			label: 'Mini Browser'
			image: (ListIconLibrary visualFor: #bug).

Now on opening our work, this is what we initially see:

The size of all the tabs is too big to show in our window, so the scroll arrow buttons now show. If we click on the right arrow button, this is what we see:

It seems that the total size of the remaining tabs is still too big to show with a single scroll. Let's click that right arrow button again, and this is what we see:

Ah, that's better. Now we click on our Mini Browser tab:

Reverse

We'll now add a page using the DropDownListWork, and we'll do the label and image thing, but this time, we'll tell it to put the image on the right, and we'll use the name space image:

	hookupInterface

		tabControl addPage: (Form new addComponentsFromClass: MoreFormWork).
		tabControl
			addPage: (Form new addComponentsFromClass: RadioButtonWork)
			label: 'Radio Buttons'.
		tabControl
			addPage: (Form new addComponentsFromClass: ListBoxWork)
			image: (ListIconLibrary visualFor: #balloon).
		tabControl
			addPage: (Form new addComponentsFromClass: ResizingSplitterWork)
			label: 'Mini Browser'
			image: (ListIconLibrary visualFor: #bug).
		tabControl
			addPage: (Form new addComponentsFromClass: DropDownListWork)
			label: 'Drop Downs'
			image: (ListIconLibrary visualFor: #name space)
			imageOnRight: true

Now after opening our work, and navigating to the last tab, this is what we see:

Extra Credit

For extra credit, we'll make our window much wider when we open it, and then tell the TabControl to show the second page which will then be seen when we open it:

	hookupWindow

		self mainWindow frame specifiedSize: 400 @ 250.
		
	hookupInterface

		tabControl addPage: (Form new addComponentsFromClass: MoreFormWork).
		tabControl
			addPage: (Form new addComponentsFromClass: RadioButtonWork)
			label: 'Radio Buttons'.
		tabControl
			addPage: (Form new addComponentsFromClass: ListBoxWork)
			image: (ListIconLibrary visualFor: #balloon).
		tabControl
			addPage: (Form new addComponentsFromClass: ResizingSplitterWork)
			label: 'Mini Browser'
			image: (ListIconLibrary visualFor: #bug).
		tabControl
			addPage: (Form new addComponentsFromClass: DropDownListWork)
			label: 'Drop Downs'
			image: (ListIconLibrary visualFor: #name space)
			imageOnRight: true.
		tabControl showPage: 2.

Isn't that pretty?

Pane In The Neck

This is the last of our How To examples for working with Widgetry Panes. We've covered all the Panes in Widgetry, if not all the features of every Pane.

It is time to move on to a new topic. Next time, we'll start looking at how to create a custom Pane in Widgetry.

And So It Goes
Sames