Channels ▼
RSS

Tools

The MVC Paradigm in Smalltalk/V

Source Code Accompanies This Article. Download It Now.


NOV90: THE MVC PARADIGM IN SMALLTALK/V

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.







Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video