Model-View-Controller becomes Object-Pane Dispatcher
Ken is a software engineer at Eaton/IDT in Westerville, Ohio. He is involved in the design of real-time software for industrial graphic workstations. He also works part time as a consultant, specializing in prototyping custom-software systems and applications. Ken can be contacted at 7825 Larchwood St., Dublin, OH 43017.
My recent efforts at unraveling more of Smalltalk's mysteries centered around the windowing system. After creating a few applications and spending many, many hours browsing through the source code (thank heaven for the source code!), I finally began to form a mental picture of how the windowing system behaves. The picture is not a simple one. For instance, the example code presented in this article was extracted from the six pages of code involved just in opening a window.
I'll begin by describing the Model-View-Controller (MVC) paradigm as implemented in classic Smalltalk. Secondly, I'll describe how the architecture under Smalltalk/V286 differs from the classic paradigm. Finally, I'll focus in more detail on the mechanisms of creating, opening, and framing a window.
The MVC Paradigm
The classic Smalltalk system, as designed at Xerox PARC in the 1970s, is based on the Model-View-Controller paradigm, the conceptual framework of which was discussed in "Information Models, Views, and Controllers," by Adele Goldberg (DDJ, July 1990).
An application in Smalltalk-80 consists of three components: A model that produces the information, a view that displays it, and a controller that manages input events.
The model is in some sense the core of the application, the data structures that represent what the application is trying to accomplish. A view is an object that actually presents the information contained in a model on the display screen. Various types of view objects are provided by the system, each designed to display specific types of information. For example, one kind of view can display data as a scrollable list, while another presents text so it can be edited.
Controllers exist to process input from the keyboard and mouse. Generally speaking, for each type of view there is a corresponding type of controller. For example, a list controller recognizes particular input events as indicating a selection from the displayed list of items. The text editor's controller handles specific events for marking, cutting, or pasting a block of text or for scrolling a page.
In the Smalltalk-80 implementation, the MVC architecture is embodied in the hierarchies for the classes Model, View, and Controller. The implementors of Digitalk's Smalltalk/V286 concentrated the functionality of the MVC paradigm into a smaller set of more complex classes. The mechanisms are different enough that the two platforms are largely incompatible at the application level (even though the language implementations are almost identical at the syntactic level).
How Smalltalk/V and Smalltalk-80 Differ
The most obvious difference between Smalltalk/V and Smalltalk-80 is in the class names. First of all, class Model is gone! This isn't a serious problem. Ultimately, the application is the model; and the dependent notification mechanisms are available anyway -- through class Object.
In Smalltalk/V286, a view object is an instance of class Pane or one of its subclasses. Figure 1 shows the basic hierarchy for class Pane and Table 1 describes its principal classes. Similarly, controllers are derived from class Dispatcher, which is detailed in Figure 2 and Table 2.
Figure 1: The hierarchy for Pane provides protocols for displaying information within a window.
Pane TopPane SubPane GraphPane ListPane TextPane
Figure 2: The hierarchy for Dispatcher provides protocols for handling input events directed to a window.
DispatchManager Dispatcher TopDispatcher GraphDispatcher PointDispatcher ScreenDispatcher ScrollDispatcher ListSelector TextEditor PromptEditor
Table 1: Description of the principal Pane classes
Pane Provides the common instance variables and the default behaviors for its subclasses, such as drawing borders and popping up menus. TopPane Represents the window itself, embodied by the border and the title bar. SubPane Represents independent regions within a window. SubPane provides the common behaviors such as reframing for its subclasses. TextPane Provides methods for scrolling text and notifying the model when its contents change or are saved. ListPane Supports scrolling and displaying data in the form of a list of individual items; provides methods to visually indicate the current selection. GraphPane A generalized "canvas" for graphic drawing.
Smalltalk/V286 has several classes and global objects that provide high-level management functions for the window system. One of these is the global variable Display -- the only instance of class DisplayScreen. Display represents the physical display and provides access to the screen as a bitmap.
Another major player is the global object Scheduler. The only allowable instance of class DispatchManager, Scheduler maintains a list of all the windows on the screen and manages their activation and deactivation.
The global variable Processor is the only instance of class ProcessScheduler, whose responsibility it is to manage the various processes that might be active in the system at any given time. Among these is the user interface process that fields interrupts from the keyboard and the mouse and translates them into suitable input events.
Raw input is handed off to the global object Terminal -- an instance of class TerminalStream. Terminal takes care of the housekeeping associated with the input stream. Internally, its state machine maps multistage input events (mouse movements, button clicks, key scan codes, and so on) into a set of global event function codes (which are defined in the pool dictionary Function-Keys).
Finally, the global variable Transcript is an instance of class TextEditor. Transcript is used much like a console window to relay error or status information. However, being a generic text editor, Transcript is also available for use in evaluating any Smalltalk expression, via the "do it" or "show it" menu selections.
The Chain of Command
Each of the application's panes, including the top pane, has a Dispatcher associated with it for handling input events. Input comes into the application via the active dispatcher. How is this dispatcher activated?
First, let's assume that the cursor is initially positioned on the background screen, o tside any window. Under these conditions, the system scheduler (Scheduler) is calling the shots. Basically, Scheduler sits in a loop and steps through its list of windows, sending each one the message isControlWanted (which, by default, merely tests to see if the cursor is positioned within the window's borders).
Table 2: Description of the principal Dispatcher classes
DispatchManager Manages all of the windows on the screen. There is only one instance of this class: the global variable Scheduler. Dispatcher Provides the default behaviors for processing input from the mouse or keyboard. TopDispatcher Provides methods to process inputs, including cursor positioning and menu activation, directed at the window itself. ScrollDispatcher Provides the default behavior for processing input related to scrolling the image in a pane. TextEditor Processes input for its associated TextPane. ListSelector Processes input for its associated ListPane. GraphDispatcher Processes input directed to a GraphPane.
The first window that answers "yes" becomes the active window. Scheduler puts that window at the head of its list and sends its dispatcher(usually a TopDispatcher) the message activate Window.
Once a TopDispatcher acquires control, it enters its own control loop. Here, the TopDispatcher polls the dispatchers associated with each of the window's subpanes, asking if one of them wants control. The pane that contains the cursor is then marked as the active pane and its dispatcher assumes control of the input stream.
When the dispatcher for a subpane gains control, it goes through an activation sequence which, in the case of a GraphPane, for instance, includes sending the message activatePane to its model.
Finally, the subpane's dispatcher enters its own control loop to monitor input events. The specific type of dispatcher that ultimately gets control determines the nature of the system's reaction to input events.
Keeping Everybody Informed
Much of the power (and the complexity) of the Smalltalk/V286 windowing system is a result of the dialogue between a window (the pane and/or dispatcher) and its model. For almost every type of event that a user can generate, there is a mechanism for notifying the model about potential changes. Some events are handled transparently by the pane or its dispatcher, so they don't "bother" the application. However, from the viewpoint of a model's implementation, these event response capabilities are considered optional. Consequently, in most cases, the pane or dispatcher "asks" the model about its implementation, using a sequence of statements similar to that in Example 1(a).
Example 1: Creating and adding subpanes
(a) (model respondsTo:#activatePane) ifTrue: [model perform:#activatePane]. (b) topPane addSubPane: (aPane := GraphPane new <cascaded messages>; yourself).
The methods that a model may implement to handle window related events are listed in Table 3.
Table 3: Methods optionally implemented by Model-to-Handle user-generated events
reframePane:aPane Sent by a GraphPane when the size or position of a window is changed. The model should implement this message if there is more than one GraphPane in a window, because it provides the identity of the pane being reframed. The actual frame can be determined by the message aPane frame. reframe:aRectangle Also sent by a GraphPane when the window is reframed. The model should implement this message when there is only one GraphPane in the window. showWindow Sent by the TopPane when the window's contents must be refreshed for any reason. Serves as an indication that the window is being displayed and that any application information should be prepared for presentation. activatePane Sent by a GraphPane when the cursor enters its frame. This message can be used, for example, to change the cursor's shape so that the user receives a visual indication that the cursor has entered a particular region. deactivatePane Sent by a GraphPane when the cursor leaves its frame. close Sent when the window is being closed. This is useful for saving any unfinished business before the window disappears! label Sent when the model is expected to answer the string that will be used for the window's label. collapsedLabel Sent when the model should answer the string that it wants used to label the collapsed window. Receipt of this message can also indicate that the window is in the process of being collapsed. InitWindowSize Sent by the open method in class Dispatcher. This message gives the model an opportunity to specify the initial size of the window.
In addition to the messages described in Table 3, still other messages may be sent to the model by selecting one of the iconic "buttons" that appear in a window's title bar. The actions associated with these buttons, such as closing, collapsing, moving, or resizing, are directly related to the behavior of window. However, these same actions may also affect the way the model carries out its internal operations.
Therefore, when the top dispatcher detects a left mouse button click with the cursor positioned inside one of these buttons, it fetches the name of the icon (a Symbol) from a class variable. The icon's name is actually the name of a method that is supposed to perform the action. If the model is capable of responding to the message, it will be sent; otherwise, the top dispatcher performs some default operation. These messages are described in Table 4.
Table 4: Methods optionally implemented by Model-to-Handle icon button events
closelt Closes the window. zoom Normally applies only to instances of class TextPane, which will then expand to the full screen-size. If window contains no text panes, the zoom message will be sent to the model. reframe Indicates that the user wishes to change the size of the window. collapse Causes the window to collapse to an iconic form (an abbreviated title bar containing only the window's label).
All of the messages mentioned in Tables 3 and 4 are sent to the model by the window -- typically as the result of a user-initiated event. There are also many ways in which the model can influence the configuration and operation of its windows. In fact, much of the public protocol for the Dispatcher and Pane class hierarchies is available for just this purpose. Some of the more important messages involved in these operations are given in Table 5.
Table 5: Messages optionally sent by the Model to configure a window
label:aString Sent to a TopPane to specify the label which should appear at the top of the window. model:anObject Notifies a Pane that its controlling model is anObject. name:aSymbol Tells a Pane that aSymbol is the name of the method, implemented in the model class, the provides initialization for the pane. The specified method may or may not be expected to accept an argument, depending upon the type of pane. For example, a rectangle (the pane's frame) will be passed as an argument to the "name" method associated with a GraphPane. menu:aSymbol Supplies a Pane with the name of a method that will answer a Menu substituted for the pane's default menu. change:aSymbol Here, aSymbol is the name of a single-argument method that handles user-initiated selections (such as pressing the left mouse button) within the pane. selectUp:aSymbol For GraphPane only, aSymbol is the name of a message that is sent when the left mouse button is released. framingRatio:aRectangle Used to specify the region of the window's display that the subpane will occupy. framingBlock:aBlock Used to specify, more precisely, the position and size of a subpane within the window.
Constructing a Window
The first step in constructing an application window at runtime is to create the top pane for the window. This step, illustrated by the open method found in Listing One (page 175), is marked by the line: topPane := TopPane new. Following this is a series of cascaded messages, directed at the newly created topPane, that specify the initial setup for the window (its associated model, label, menu, and so on).
Once the topPane has been taken care of, the subpanes can be created and added to the topPane as in Example 1(b). Here the cascaded messages provide setup information to the subpane.
Notice that in Listing One there are three subpanes created and added to the topPane. One of these is a ListPane, while the other two are GraphphPanes. Also notice that each subpane is assigned a "name" by the statement: name:<aSymbol>;
This can be confusing, because the name given is not really the "name" of the subpane. It is, in fact, the name of a method selector that will be sent to the model during the process of opening the window. This method is supposed to perform application-specific initialization for the pane. In the case of the ListPane, the method should answer an array containing the list of items to be displayed. For a GraphPane, the initialization method is expected to answer a Form (bitmap) whose size is the same as the pane's frame (the frame is passed as an argument to the message).
Framing a Window
Framing a window is one of the more obscure parts of creating a window application in Smalltalk/V286. Each subpane must possess a means of determining its position and size relative to the window as a whole. The calculations must be such, that if the window's position or size is allowed to change, the position and size of each subpane can be adjusted to accommodate the new frame.
There are two ways of framing subpanes. Either of the messages framing-Ratio:aRectangle or framingBlock: aBlock can be used, depending upon the level of precision required. If the absolute position or size of the subpanes is not critical, framingRatio: is far easier to use. This message specifies the position and size of a subpane as fractions of the whole window. For example, framingRatio:(0 @ (3/4) extent:1/4 @ (1/4)); tells a subpane that it will be positioned at the left edge of the window and three quarters of the way down from the top. The subpane's size will be one quarter of the width of the window and one quarter of its height. In other words, it will occupy the lower left-hand corner of the parent window.
However, in those cases when an application demands that one or more of its subpanes be located in a specific position or be framed to a certain absolute size, the procedure becomes a bit more complicated. Listing Two (page 175) is a modified version of the open method in which the list pane is constrained to be ten characters wide and ten lines high. Here the message framingBlock: is sent to each subpane with a block of code as its argument. The block of code is not evaluated at the time this message is sent to the subpane, but when the window is opened or reframed. At that time, it is passed a single argument -- a rectangle defining the the window's interior frame. The block should answer a new rectangle -- the frame of the subpane.
Any variables local to the open method that are referenced within the framing block, will have the values they had when the open method completed. Be careful with this one -- don't try to calculate the dimensions of the subpane frames incrementally, using one set of variables. The results will not be what you expect!
Wrapping Up
There's no doubt that Smalltalk is a big and complex system and that the powerful array of features it offers can be quite intimidating. Fortunately, the environment provides the kinds of tools that make exploration considerably less difficult.
_THE MVC PARADIGMN AND SMALLTALK/V_ by Kenneth E. Ayers
[LISTING ONE]<a name="0253_0014">
open
| frame |
appName := String new.
saved := true.
editorPen := Pen new.
imagePen := Pen new.
frame := (Display boundingBox extent // 6)
extent:(Display boundingBox extent * 2 // 3).
topPane := TopPane new
model:self;
label:self label;
menu:#windowMenu;
minimumSize:frame extent;
yourself.
topPane addSubpane:
(listPane := ListPane new
model:self;
name:#appList;
change:#appSelection:;
returnIndex:false;
menu:#listMenu;
framingRatio:(0 @ 0 extent:1/4 @ (2/3));
yourself).
topPane addSubpane:
(imagePane := GraphPane new
model:self;
name:#initImage:;
menu:#noMenu;
framingRatio:(0 @ (2/3) extent:1/4 @ (1/3));
yourself).
topPane addSubpane:
(editorPane := GraphPane new
model:self;
name:#initEditor:;
menu:#editorMenu;
change:#editIcon:;
framingRatio:(1/4 @ 0 extent:3/4 @ 1);
yourself).
topPane reframe:frame.
topPane dispatcher openWindow scheduleWindow.
<a name="0253_0015"><a name="0253_0015">
<a name="0253_0016">
[LISTING TWO]<a name="0253_0016">
open
| frame listWid listHgt |
saved := true.
editorPen := Pen new.
imagePen := Pen new.
frame := (Display boundingBox extent // 6)
extent:(Display boundingBox extent * 2 // 3).
listWid := SysFont width * 10.
listHgt := SysFont height * 10.
topPane := TopPane new
label:self label;
model:self;
menu:#windowMenu;
minimumSize:frame extent;
yourself.
topPane addSubpane:
(listPane := ListPane new
model:self;
name:#appList;
change:#appSelection:;
returnIndex:false;
menu:#listMenu;
framingBlock:[:aFrame |
aFrame origin
extent:listWid @ listHgt];
yourself).
topPane addSubpane:
(imagePane := GraphPane new
model:self;
name:#initImage:;
menu:#noMenu;
framingBlock:[:aFrame|
aFrame origin + (0 @ listHgt)
extent:(listWid
@ (aFrame height - listHgt))];
yourself).
topPane addSubpane:
(editorPane := GraphPane new
model:self;
name:#initEditor:;
menu:#editorMenu;
change:#editIcon:;
framingBlock:[:aFrame|
aFrame origin + (listWid @ 0)
extent:((aFrame width - listWid)
@ aFrame height)];
yourself).
topPane reframe:frame.
topPane dispatcher openWindow scheduleWindow.
[COMPLETE SMALLTALK/V SOURCE KEN AYERS'S ARTICLE IN NOVEMBER 1990 ISSUE OF DDJ]
[Listing -- Class EmptyMenu]
Menu subclass: #EmptyMenu
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''.
"***************************************************************"
"** EmptyMenu instance methods **"
"***************************************************************"
popUpAt:aPoint
"An empty menu does nothing -- answer nil."
^nil.
popUpAt:aPoint for:anObject
"An empty menu does nothing -- answer nil."
^nil.
[Listing -- Class IconEditor]
Object subclass: #IconEditor
instanceVariableNames:
'scale cellSize cellOffset saved unZoom topPane listPane
editorPane imagePane iconLibrary iconName selectedIcon
gridForm editorPen imagePen'
classVariableNames:
'IconSize '
poolDictionaries:
'FunctionKeys CharacterConstants'.
"***************************************************************"
"** IconEditor class methods **"
"***************************************************************"
initialize
"Initialize the class variables."
IconSize isNil ifTrue:[IconSize := 32@32].
new
"Answer a new IconEditor."
self initialize.
^super new.
"***************************************************************"
"** IconEditor instance methods **"
"***************************************************************"
"-----------------------------"
"-- Window creation methods --"
"-----------------------------"
openOn:anIconLibrary
"Open an IconEditor window on the Dictionary
anIconLibrary."
iconLibrary := anIconLibrary.
self open.
open
"Open an IconEditor window."
| frame |
iconLibrary isNil
ifTrue:[self initLibrary].
iconName := String new.
saved := true.
editorPen := Pen new.
imagePen := Pen new.
frame := (Display boundingBox extent // 6)
extent:(Display boundingBox extent * 2 // 3).
topPane := TopPane new
model:self;
label:self label;
menu:#windowMenu;
minimumSize:frame extent;
yourself.
topPane addSubpane:
(listPane := ListPane new
model:self;
name:#iconList;
change:#iconSelection:;
returnIndex:false;
menu:#listMenu;
framingRatio:(0 @ 0 extent:1/4 @ (2/3));
yourself).
topPane addSubpane:
(imagePane := GraphPane new
model:self;
name:#initImage:;
menu:#noMenu;
framingRatio:(0 @ (2/3) extent:1/4 @ (1/3));
yourself).
topPane addSubpane:
(editorPane := GraphPane new
model:self;
name:#initEditor:;
menu:#editorMenu;
change:#editIcon:;
framingRatio:(1/4 @ 0 extent:3/4 @ 1);
yourself).
topPane reframe:frame.
topPane dispatcher openWindow scheduleWindow.
"----------------------------"
"-- Window support methods --"
"----------------------------"
windowMenu
"Answer the menu for the IconEditor window."
enu
labels:'collapse\cycle\frame\move\print\close' withCrs
lines:#(5)
selectors:#(collapse cycle resize
printWindow move closeIt).
listMenu
"Answer the menu for the form list pane."
enu
labels:'remove icon\create new icon\change size' withCrs
lines:#()
selectors:#(removeIt createIt resizeIt).
editorMenu
"Answer the menu for the Editor pane."
selectedIcon isNil ifTrue:[^EmptyMenu new].
enu
labels:('invert\border\erase\save\print') withCrs
lines:#(3)
selectors:#(invertIt borderIt eraseIt saveIt printIcon).
noMenu
"Answer a do-nothing menu."
^EmptyMenu new.
initEditor:aRect
"Inititalize the editor pane."
Display white:aRect.
^Form new extent:aRect extent.
initImage:aRect
"Inititalize the IconEditor image pane."
Display white:aRect.
^Form new extent:aRect extent.
iconList
"Answer a String Array containing the
names of the icons in the icon library."
^iconLibrary keys asArray.
iconSelection:anIconName
"The user has selected anIconName from
the list. Make it the selected icon
and re-initialize the editor."
| anIcon |
self saved ifFalse:[^self].
anIcon := iconLibrary at:anIconName ifAbsent:[nil].
anIcon isNil ifTrue:[^self].
selectedIcon := anIcon deepCopy.
iconName := anIconName.
selectedIcon extent = IconSize
ifTrue:[self displayIcon]
ifFalse:[
CursorManager execute change.
self resizeIt:selectedIcon extent].
editIcon:aPoint
"The select button has been pressed at aPoint.
If the cursor is in the editor image,
reverse that cell and all others that the
cursor passes over until the select button
is released."
| currX currY refX refY editing newX newY |
(editorPen clipRect containsPoint:Cursor offset)
ifFalse:[^self].
currX := -1.
currY := -1.
refX := editorPen clipRect origin x.
refY := editorPen clipRect origin y.
editing := true.
[editing]
whileTrue:[
newX := (Cursor offset x - refX) // scale.
newY := (Cursor offset y - refY) // scale.
((newX = currX) and:[newY = currY])
ifFalse:[
currX := newX.
currY := newY.
self setX:currX Y:currY.
saved := false].
editing :=
(editorPen clipRect containsPoint:Cursor offset)
and:[Terminal read ~= EndSelectFunction]].
selectedIcon
copy:imagePen clipRect
from:Display
to:0@0
rule:Form over.
"----------------------------"
"-- Window control methods --"
"----------------------------"
showWindow
"Redisplay the contents of the window's panes."
topPane collapsed
ifFalse:[self displayIcon].
activatePane
"If the cursor is in the Editor pane,
change its shape to a crosshair."
(editorPane frame containsPoint:Cursor offset)
ifTrue:[CursorManager hair change].
deactivatePane
"If the cursor is not in the Editor pane,
change its shape to the normal pointer."
(editorPane frame containsPoint:Cursor offset)
ifFalse:[CursorManager normal change].
reframePane:aPane
aPane == editorPane
ifTrue:[^self reframeEditor:aPane frame].
aPane == imagePane
ifTrue:[^self reframeImage:aPane frame].
zoom
"Zoom/unzoom the window."
| frame |
unZoom isNil
ifTrue:[ "Zoom to full screen"
unZoom := topPane frame.
frame := Display boundingBox]
ifFalse:[
frame := unZoom.
unZoom := nil].
CursorManager execute change.
topPane reframe:frame.
Scheduler resume.
close
"Prepare for closing the window."
self saved
ifFalse:[self saveIcon].
self release.
label
"Answer the window's label."
^'IconEditor (Ver 2.1 - 07/24/90 - KEA)'.
collapsedLabel
"Answer the window's label when collapsed."
^'IconEditor'.
"---------------------------------------"
"-- List-pane menu processing methods --"
"---------------------------------------"
createIt
"Create a new icon to edit."
self saved
ifTrue:[
self
eraseImage;
newIcon:self getIcon].
resizeIt
"Change the size of the icon."
| selection size |
self saved ifFalse:[^self].
selection := (Menu
labels: '8@8\16@16\32@32\64@64' withCrs
lines: #()
selectors:#(small medium large xLarge))
popUpAt:Cursor offset.
selection isNil ifTrue:[^self].
selection == #small ifTrue:[size := 8@8].
selection == #medium ifTrue:[size := 16@16].
selection == #large ifTrue:[size := 32@32].
selection == #xLarge ifTrue:[size := 64@64].
self resizeIt:size.
removeIt
"Remove the selectedIcon from the
icon library."
iconLibrary removeKey:iconName ifAbsent:[].
self
eraseEditor;
eraseImage;
changed:#iconList.
selectedIcon := nil.
iconName := ''.
"-----------------------------------------"
"-- Editor-pane menu processing methods --"
"-----------------------------------------"
eraseIt
"Erase icon."
(Form width:IconSize x height:IconSize y)
displayAt:imagePen clipRect origin rule:Form over.
(Form width:IconSize x * scale height:IconSize y * scale)
displayAt:editorPen clipRect origin rule:Form over.
gridForm
displayAt:editorPen clipRect origin rule:Form andRule.
self
border:editorPen clipRect
width:2
marksAt:(4 * scale).
self
border:imagePen clipRect
width:2
marksAt:0.
invertIt
"Invert the color of the icon."
selectedIcon := self getIcon reverse.
self displayIcon:selectedIcon.
saved := false.
borderIt
"Draw a border around the icon."
|inset |
(inset := Prompter prompt:'Inset?' default:'0') isNil
ifTrue:[^self].
Display border:(imagePen clipRect insetBy:inset asInteger).
self newIcon:self getIcon.
saved := false.
saveIt
"Save the new icon image."
self saveIcon.
self changed:#iconList.
listPane restoreSelected:iconName.
self activatePane.
printIcon
"Print the magnified icon from the editor pane."
CursorManager execute change.
(Form fromDisplay:(editorPen clipRect expandBy:4))
outputToPrinterUpright.
CursorManager normal change.
printWindow
"Print an image of the entire editor window."
CursorManager execute change.
(Form fromDisplay:topPane frame) outputToPrinterUpright.
CursorManager normal change.
"-----------------------------"
"-- Display support methods --"
"-----------------------------"
border:aRect width:aWid marksAt:aStep
"Draw a border, of width aWid, around aRect
with ruler marks ever aStep cells."
| box aForm orig wid hgt |
box := aRect expandBy:aWid.
0 to: aWid - 1 do:[:inset|
Display border:(box insetBy:inset)].
(aStep = 0)
ifTrue: ["** No ruler marks!!!! **" ^nil].
aForm := (Form width:aWid height:aWid) reverse.
orig := aRect origin.
wid := aRect width.
hgt := aRect height.
0 to:wid by:aStep do:[:x|
aForm
displayAt:(orig + (x @ ((aWid * 2) negated)));
displayAt:(orig + (x @ (hgt + aWid)))].
0 to:hgt by:aStep do:[:y|
aForm
displayAt:(orig + (((aWid * 2) negated) @ y));
displayAt:(orig + ((wid + aWid) @ y))].
editorBorder
"Draw the ruled border around the editor frame."
self
border:editorPen clipRect
width:2
marksAt:(4 * scale).
imageBorder
"Draw the solid border around the image frame."
self
border:imagePen clipRect
width:2
marksAt:0.
displayIcon
"Display the selectedIcon for editing."
selectedIcon isNil
ifFalse:[ self displayIcon:selectedIcon].
displayIcon:anIcon
"Display anIcon for editing."
CursorManager execute change.
self
eraseImage;
displayImage:anIcon;
eraseEditor;
editorBorder;
displayEditor:anIcon.
CursorManager normal change.
self activatePane.
displayEditor:anIcon
"Display anIcon in the editor pane."
| mask |
(anIcon
magnify:(0 @ 0 extent:anIcon extent)
by:scale @ scale)
displayAt:editorPen clipRect origin
rule:Form over.
mask := gridForm deepCopy reverse.
scale > 4
ifTrue:[
mask
displayOn:Display
at:(editorPen clipRect origin moveBy:(1@1))
clippingBox:editorPen clipRect
rule:Form orThru
mask:Form white].
mask displayOn:Display
at:(editorPen clipRect origin moveBy:(-1 @ -1))
clippingBox:editorPen clipRect
rule:Form orThru
mask:Form white.
gridForm
displayAt:editorPen clipRect origin
rule:Form andRule.
displayImage:anIcon
"Display anIcon in the image pane."
anIcon
displayAt:imagePen clipRect origin
rule:Form over.
eraseEditor
"Erase the editor pane."
Display white:editorPane frame.
eraseImage
"Erase the image pane."
Display white:imagePane frame.
getIcon
"Answer the currently displayed icon
taken from the image pane."
^((Form fromDisplay:imagePen clipRect)
offset:0 @ 0;
yourself).
"-----------------------------"
"-- Editing support methods --"
"-----------------------------"
setX:x Y:y
"Reverse a cell in both the Editor and
Icon Image panes."
editorPen
place:(self editorCellX:x y:y);
copyBits.
imagePen
place:(self imageCellX:x y:y);
copyBits.
editorCellX:x y:y
"Answer a Point with the location of the cell at
position x,y within the editor image."
^editorPen clipRect origin
+ ((x * scale @ (y * scale)) + cellOffset).
imageCellX:x y:y
"Answer a Point with the location of the cell at
position x,y within the image form."
^imagePen clipRect origin + (x @ y).
"-------------------------------"
"-- Reframing support methods --"
"-------------------------------"
reframeEditor:aFrame
"Reframe the editor pane to aFrame."
| w h yScale xScale penRect size |
w := aFrame width.
h := aFrame height.
xScale := w // (IconSize x + 2).
yScale := h // (IconSize y + 2).
scale := xScale min:yScale.
scale > 4
ifTrue:[
cellSize := (scale - 3) @ (scale - 3).
cellOffset := 2 @ 2]
ifFalse:[
cellSize := (scale - 2) @ (scale - 2).
cellOffset := 1 @ 1].
size := IconSize * scale.
gridForm := Form width:size x height:size y.
(Pen new:gridForm) grid:scale.
penRect := editorPane frame center - (size // 2) extent:size.
editorPen
clipRect:penRect;
defaultNib:cellSize;
combinationRule:Form reverse;
mask:nil.
reframeImage:aFrame
"Reframe the window's icon image pane to aFrame."
imagePen
clipRect:(aFrame center - (IconSize // 2)
extent:IconSize);
defaultNib:1 @ 1;
combinationRule:Form reverse;
mask:nil.
"-----------------------------"
"-- General support methods --"
"-----------------------------"
resizeIt:aSize
"Change the size of the icon to be aSize."
aSize = IconSize ifTrue:[^self].
(selectedIcon notNil
and:[selectedIcon extent ~= aSize])
ifTrue:[
selectedIcon := nil.
iconName := String new.
self
eraseEditor;
eraseImage.
listPane restoreSelected:iconName].
IconSize := aSize.
topPane reframe:topPane frame.
Scheduler resume.
saved
"If the image has not changed answer true;
otherwise ask user if changes are to be lost."
saved ifTrue:[^true].
(Menu message:'Image has changed -- discard changes?') isNil
ifTrue:[^false].
saved := true.
^saved.
saveIcon
"Save a modified icon image."
|name|
(name := Prompter
prompt: 'Enter name of new icon'
default: iconName) isNil
ifTrue:[^self].
(iconLibrary includesKey:name)
ifTrue:[
(Menu message:name, ' exists -- overwrite it?') isNil
ifTrue:[^self].
iconLibrary removeKey:name].
selectedIcon := self getIcon.
iconName := name.
iconLibrary at:name put:selectedIcon.
saved := true.
"----------------------------"
"-- Initialization methods --"
"----------------------------"
initLibrary
"Get the icon library to use."
(Smalltalk includesKey:#IconLibrary)
ifTrue:[
iconLibrary := Smalltalk at:#IconLibrary]
ifFalse:[
iconLibrary := Dictionary new.
Smalltalk at:#IconLibrary put:iconLibrary].
newIcon:anIcon
"Set up the editor and display a new icon."
selectedIcon := anIcon deepCopy.
self displayIcon.
[Listing -- Modified open Method]
openIt
"Open an IconEditor window."
| frame listWid listHgt |
iconLibrary isNil
ifTrue:[self initLibrary].
iconName := String new.
saved := true.
editorPen := Pen new.
imagePen := Pen new.
frame := (Display boundingBox extent // 6)
extent:(Display boundingBox extent * 2 // 3).
listWid := SysFont width * 10.
listHgt := SysFont height * 10.
topPane := TopPane new
label:self label;
model:self;
menu:#windowMenu;
minimumSize:frame extent;
yourself.
topPane addSubpane:
(listPane := ListPane new
model:self;
name:#iconList;
change:#iconSelection:;
returnIndex:false;
menu:#listMenu;
framingBlock:[:aFrame|
aFrame origin
extent:listWid @ listHgt];
yourself).
topPane addSubpane:
(imagePane := GraphPane new
model:self;
name:#initImage:;
menu:#noMenu;
framingBlock:[:aFrame|
aFrame origin + (0 @ listHgt)
extent:(listWid
@ (aFrame height - listHgt))];
yourself).
topPane addSubpane:
(editorPane := GraphPane new
model:self;
name:#initEditor:;
menu:#editorMenu;
change:#editIcon:;
framingBlock:[:aFrame|
aFrame origin + (listWid @ 0)
extent:((aFrame width - listWid)
@ aFrame height)];
yourself).
topPane reframe:frame.
topPane dispatcher openWindow scheduleWindow.