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 GoesSames