Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Design

Making Smalltalk With Widgets


MAY91: MAKING SMALLTALK WITH WIDGETS

MAKING SMALLTALK WITH WIDGETS

An extensive class library and a sophisticated interface editor highlight Widgets/V 286

Kenneth E. Ayers

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.


Widgets were once thought to be the ultimate generic product, manufactured by --who else?--the Acme Widget Company. No one seemed to know exactly what a widget was or what it did, but according to those sample financial statements found in business text books, a lot of widgets were being produced--and consumed! But with the introduction of its Widgets/V 286 package, Acumen Software has elevated the widget to the status of a real object. Well, sort of.

Widgets/V 286 is a software package that extends the user interface capabilities of the Smalltalk/V 286 environment. Widgets provides a set of one hundred (or so) classes that offer contemporary alternatives to the standard user interface components supplied as part of the basic Smalltalk image. The enhanced objects range in complexity from simple buttons to drop-down menus to multipart dialog windows. Each object class conforms to a standard creation protocol and each provides a rich set of methods for accessing its underlying behaviors--all of which makes Widgets extremely easy to use.

Complimenting the extensive library of classes is a sophisticated interface editor that allows you to assemble an application window interactively. With the editor's easy-to-use toolkit approach, you simply drop widgets onto a blank window, drag them around, resize them, and so on. When you're done, you just save your work and the editor does the rest by generating the Smalltalk code necessary to create your window.

This article presents an application, developed using the Widgets tools, that illustrates how the component parts of the Widgets/V 286 package can be assembled to create virtually any kind of window you might need. But first, a quick tour of the package is in order.

New and Improved Windows

The Smalltalk/V 286 environment does offer components that can be used to create window-based applications. However, the process of doing so can be tedious. This, in turn, can be attributed to the fact that Smalltalk's default user interface classes are primitive by today's standards.

In the first place, the programmer must tend to a considerable amount of detail just to get the parts of an application window to line up properly. Using Smalltalk's built-in facilities, you must find that magic combination of framingRatios and/or framingBlocks (don't forget to account for border widths!) that put all of the panes in their proper place.

By contrast, the Widgets/V 286 interface editor allows you to line everything up in the most natural way possible--visually. And, to guarantee that things stay lined up when a window's size changes, Widgets provides a concise set of framing parameters that allow you to keep a widget centered in a window or keep the origin of a widget anchored relative to any corner of a window. Here, too, the results are presented visually by the interface editor.

Secondly, many critical components such as buttons and menu bars are missing altogether from the standard Smalltalk environment. Those that are provided, such as pop-up menus and prompters, are relatively crude when compared to modern windowing systems such as Microsoft Windows or the X-based OpenLook toolkit.

To address this deficiency, Widgets/V 286 provides an extensive set of component parts with which to create sophisticated user interfaces. Table 1 outlines the categories of widget classes supplied with the package.

Table 1: User interface components offered by Widgets/V 286

  Widget Type            Description
  -------------------------------------------------------------------------

  Drawing Widgets        Provide functional and decorative "embellishments"
                         such as vertical and horizontal lines and
                         rectangles for separating regions within a window;
                         a static text widget for labelling fields; and a
                         general-purpose drawing widget onto which
                         arbitrary bit-mapped forms may be superimposed.

  Button Widgets         Offer a number of button-like widgets which invoke
                         an action when "pushed."  Among the varieties of
                         buttons are those with traditional text labels,
                         one whose label is any bit-mapped image, a
                         "transparent" button that transforms any screen
                         region into an action area, check-box buttons,
                         radio buttons, and an on-off button.

  Choice Widgets         Allow users to chose from several alternatives.
                         They include: radio button groups; drop-down menu
                         button; standard list selector box; multiple list
                         selector box; editable list selector box; iconic
                         menu; and iconic drop-down menu.

  Valuator Widgets       Allow users to modify some numeric quantity.  The
                         group includes sliders, bar gauges, a numeric edit
                         box, and vertical and horizontal scroll bars.

  Editable Text Widgets  Provide users with the ability to enter and
                         modify text.  Include a simple one-line editor
                         with an optional label (useful for data entry
                         fields) and a multiline text editor box.

  Pane Widgets           Provide high-level grouping capabilities and a
                         plain border, a titled border, and a border with
                         scroll bars.

  Miscellaneous Widgets  Includes editors for times (both standard and
                         24-hour format), dates, and points (X-Y
                         coordinates).

In the "stock" Smalltalk environment, the channels of interaction between a model and its window(s) are rather poorly defined and are not consistently implemented across the Pane/Dispatcher class hierarchies.

Widgets/V 286 helps on this front, too. It provides a mechanism by which an application may request notification of certain standard events such as window activation/deactivation, mouse button activity, mouse movement, and key presses. This event registration mechanism uses the on: anAction send: Message message, which registers the symbol anAction as being an event that the widget is interested in knowing about. Later, when that event occurs, the system will respond by sending aMessage. The interface editor identifies the events that are supported by each standard widget type and allows you to define the message to be sent. Registration is generated automatically by the editor.

Along these same lines, the Widget package provides an easy-to-use mechanism for assigning functions to specific keys. Basically, any widget that accepts input will have an instance variable known as a KeyBindingsDictionary. This dictionary associates keys (using sensible names such as "up arrow" and "esc," or compounds like "ctrl-alt-left arrow") with names of methods that implement the actual functions for those keys.

It stands to reason that, in a graphical user interface, graphic drawing operations are common. Yet, while Smalltalk supports a more-than-adequate set of graphics operations, it is often difficult to determine exactly how to draw graphic shapes on the screen (should I use a BitBlt or a Pen?). Furthermore, those drawing operations are generally defined only in terms of absolute screen coordinates, placing responsibility for relocation to the frame of a specific window squarely on the application's shoulder.

Widgets/V 286 alleviates both of these problems. First, all of the basic drawing operations are reimplemented in the base class, Widget. Thus, for instance, every widget knows how to draw an ellipse. Then, all drawing (and clipping) operations are performed relative to the coordinate frame of the enclosing widget, even if it is nested within other widgets.

Finally, Smalltalk provides no direct mechanism for opening windows. Yes, of course, windows do get opened; but it's the result of various combinations of magic and outright trickery! Widgets/V 286 simplifies this process by providing simple messages that can be used to simultaneously create a new instance of a window and display that window on the screen.

The open# message, for instance, provides a no-frills, "open this window and get on with it" approach. This message is sent directly to the class object, by-passing the need to use the <class> new open combination. Actually, the simple open message is just a convenient disguise for a much more versatile method of opening and initializing windows. Consider the default case of the open method, shown in Example 1. Any application-specific initialization can be accomplished simply by providing an instance method name initialize. However, if your application requires something special, such as an openOn: method, it might look like Example 2. In this case, you provide an instance method, initializeMe:, that handles initializing the window.

Example 1: The default open method

  open
      ^super new
         openWithInitializeMethod: #initialize
         arguments: #().

Example 2: initializeMe initializes the window for a given object, which is passed as an argument to the openOn: message

  openOn: someObject
          ^super new
         openWithInitializeMethod: #initializeMe:
         arguments: (Array with: someObject).

One other default opening mechanism is provided for dialog-type application windows. This is the prompt: aString message. Its internal representation is shown in Example 3. If this method of opening is appropriate for your particular application, all you do is reimplement the instance method promptString:.

Example 3: Internals of the prompt message

  prompt: aString
     ^super new
        openWithInitializeMethod: #promptString:
        arguments: (Array with: aString).

Constructing an Application

The ultimate question that any toolkit must answer is whether these tools can be used to create real applications. In order to put Acumen's claims to the test, I set out to construct a real application --an appointment management utility. The initial specifications were to be able to browse a list of existing appointments. In addition, I needed to add new appointments to the list and change or remove existing entries. An appointment should specify a date, the time at which the appointment starts, the time at which it ends, and a time at which I am to be notified. Furthermore, there must be a way to enter some text that describes the nature of the appointment. Finally, when it's time to notify me about an appointment, the system should beep the terminal and pop up a window with the appropriate information. Dismissing the window should remove the appointment from the list.

From these specifications, I determined that the project would need three classes of objects. The Appointment class (see Listing One, page 98) holds information about a specific appointment, including the name of the person the appointment is with and the starting and ending times of the appointment. AppointmentBrowser (see Listing Two, page 98) allows a user to browse his or her list of appointments, add new appointments, and/or change or remove existing appointments. AppointmentNotifier (see Listing Three, page 101) pops up a window when it's time to notify a user about an appointment. Two of the three classes, AppointmentBrowser and AppointmentNotifier, have user interfaces created with the Widget's interface editor.

In addition to the object classes, one global variable is required. It is created by evaluating the following Smalltalk expression Appointments: = Dictionary new. This public dictionary holds (sorted) lists of appointments accessed by a user's name. Consequently, the system is capable of managing appointments for more than one user. Also, before appointment notification can occur, the timer interrupt handler (the timerinterrupt class method in class Process) must be modified as shown in Listing Four (page 102).

Finally, an appointment browser window can be opened by either the AppointmentBrowser open0n: aUserName or AppointmentBrowser open message. (The latter message will prompt for the name of a user.)

Building the User Interface

Once the requirements for the appointment manager application are determined, the process of creating the program begins with the interface editor. Choosing the New Interface selection from the (new) system menu opens an interface editor window containing an empty application window. In this case, the empty application window is transformed into an appointment browser window by selecting various widgets from the editor's iconic menus and "dropping" them onto the empty application window. Figure 1 shows the interface editor on which the completed appointment browser has been constructed. It consists of a ListBox, a DateWidget, three TimeWidgets, a TextEditWidget (laid on top of a TitledPane), and three TitledButtons.

After the widgets are placed on the application window, specific details about each must be filled in. This is done by pointing at the widget and double clicking the left mouse button. In response, the widget pops up its own special editor. While many of the widget editors are unique, all follow the same general format.

In the case of the ListBox widget, its title has been set to "Appointments." I've also filled in the Instance Variable Name field with the name appointmentList. When the editor generates what source code it can, the class definition will include a declaration for an appointmentList instance variable.

Also, notice that the On: (select) and Send: (selectAppointment:) fields have been filled in. This instructs the interface editor to establish an event handler method for selections made from this list widget.

Each widget additionally has an editor for its framing parameters. This is a fairly complex editor (covered well in the Widgets/V 286 manual) that lets you specify how the size and position of a particular widget will be affected by resizing the parent window.

When the interface editor is instructed to save the newly created interface, it generates a class header containing named instance variables taken from those widgets for which the instance variable name has been specified; a complete createWindow method that will be called when the window is opened to construct the actual window object; empty ("stub") methods to handle events for widgets in which the event response (On: Send:) fields have been filled in (these methods must be completed later--by you--so that they implement the required functionality). It's important to note that saving a new version of an existing interface will not overwrite code that you've added (except for the createWindow method).

In addition to the user interface for the browser, a simple interface must be constructed for the AppointmentNotifier window. In operation, the notifier window is created by evaluating the expression, AppointmentNotifier open. Once opened, this window remains on the screen.

During its initialization, the notifier window forks a process that patiently waits for clock ticks. At each clock tick, the notifier object checks to see if the current minute has changed. If it has, the appointment lists (SortedCollections of Appointments) for all users are polled, searching for an appointment whose notification time has arrived.

When an appointment's time arrives, the notifier fills in its text widget with the appointment information, beeps the terminal, and forces its window to become the active window. At that time, the user may press the OK button to clear the window and remove the appointment from the list.

Browsing the Package

Other than a couple of minor gripes (mentioned later), I found the Widgets package delightful to use. Its editor has a clean, consistent, and intuitive user interface. The set of widgets is extensive (bordering on overwhelming) and in the spirit of Smalltalk's open environment, there is source code for everything!

I should also mention that if your hardware supports color (EGA or VGA), you're in for a real treat. The package sports a really nice three-dimensional look for all of its widgets: beveled edges, shaded buttons, the works! (See Figure 2.) To enhance the portability of an application, Widgets automatically detects a monochrome video mode (or you can force it, manually) and adjusts its visual style accordingly.

Even though I spent several hours trying to figure out how to handle timer interrupts (a problem with Smalltalk's documentation, not the Widgets package), I feel comfortable saying that the appointment manager application could have been completed in four hours tops. In contrast, I estimate that an equivalent application, using only the built-in Smalltalk windowing components (crude, but it could be done), would require a great deal more time depending upon how closely you tried to emulate the capabilities offered by the Widgets package.

I did, however, find a couple of minor bugs. One is in the class TimeWidget (lets you edit time values) and affects the way the meridian (A.M./P.M.) indicator rolls over at noon and midnight.

On a related issue, true military time spans the hours 0100 to 2459 with 2400 hours being midnight. It appears that Acumen's MilitaryTimeWidget uses what is commonly referred to as a 24-hour time format, which spans the hours 0000 to 2359, with 0000 hours being midnight. (Okay, I know this is probably nit-picking! But, as an ex-GI, I felt a peculiar urge to set the record straight.) However, this widget, too, has a bug that causes the time to roll over from 2359 hours to 0100 hours. Now, that's not right, in any format!

Products Mentioned

Widgets/V 286 Acumen Software 2140 Shattuck Ave, Suite 1008 Berkeley, CA 94704 415-649-0601, FAX: 415-649-0514 $149.95 Requirements: Smalltalk/V 286

I called Acumen to report the bugs with an ulterior motive of checking out the company's attitude toward technical support. No surprises here! The gentleman I talked to was extremely courteous. He was somewhat surprised that no one else had reported problems with the time editors, but assured me that he would look into the matter. I was also informed that Acumen will soon release a list of bug fixes--the fix to the time editors will be included.

Finally, since programming in Smalltalk is so intimately tied to the environment itself, I was somewhat disappointed that there was virtually no discussion of how the environment had been modified by the Widgets package. Again, this kind of information, which is available if you're willing to roll up your sleeves and dig into the source code, could have been put in another "oh, by the way" appendix.

Conclusions

My first exposure to the Widgets/V 286 package was through a slick brochure that was flashy and full of what I would normally consider to be marketing hype. Still, Widgets seemed to be just what I was looking for--something that would put a little excitement back into creating Smalltalk applications. I took a chance and purchased the package, sincerely hoping I wouldn't have to take advantage of Acumen's 30-day money-back guarantee.

Let me just say that it wasn't hype! Widgets is easy to use, it's fun, and in just a couple of hours you can be cranking out applications that, until now, you've only dreamed of. If you're interested in developing Smalltalk applications, and you hope to achieve the levels of efficiency and productivity that you know Smalltalk is capable of, Widgets/V286 is just the kind of toolkit you've been waiting for.


_MAKING SMALLTALK WITH WIDGETS_
by Kenneth E. Ayers


[LISTING ONE]
<a name="011f_000f">

"The Appointment class definition"

Object subclass: #Appointment
  instanceVariableNames:
    'user date startTime endTime notifyTime text '
  classVariableNames: ''
  poolDictionaries: ''

"****  Appointment class methods  ****"
timePrintString:aTime
        "Answer a string with a representation of
         aTime formatted as hh:mm AM/PM"

    | amPM hours minutes minStr |

    amPM := 'AM'.
    hours := aTime hours.
    minutes := aTime minutes.
    hours = 0
        ifTrue: [hours := 12.  amPM := 'PM']
        ifFalse:[
            hours >= 12
                ifTrue:[
                    amPM := 'PM'.
                    hours > 12
                        ifTrue:[hours := hours - 12]]].
    minutes < 10
        ifTrue: [minStr := '0', minutes printString]
        ifFalse:[minStr := minutes printString].
    ^hours printString, ':', minStr, ' ', amPM.

"****  Appointment instance methods  ****"
< anAppointment
        "Answer true if the receiver's date
         and notify time are earlier than
         those of anAppointment"

    ^(date < anAppointment date)
        or:[(date = anAppointment date)
            and:[notifyTime < anAppointment notifyTime]].

<= anAppointment
        "Answer true if the receiver's date
         and notify time are the same or
         earlier than those of anAppointment"

    ^(self < anAppointment) or:[self = anAppointment].

= anAppointment
        "Answer true if the receiver's date
         and notify time are the same as
         those of anAppointment"

    ^(date = anAppointment date)
        and:[notifyTime = anAppointment notifyTime].

> anAppointment
        "Answer true if the receiver's date
         anf notify time are later than
         those of anAppointment"

    ^(self <= anAppointment) not.

>= anAppointment
        "Answer true if the receiver's date
         and notify time are the same or
         later than those of anAppointment"

    ^(self < anAppointment) not.

checkTime:timeNow date:dateToday
        "Answer true if it is time to notify
         the user of his or her appointment"

    ^(date < dateToday)
        or:[(date = dateToday)
                and:[notifyTime < timeNow]].

date
        "Answer a Date, the appointment's date"
    ^date.

date:aDate
        "Set the appointment's date to aDate"
    date := aDate.
endTime
        "Answer a Time, the appointment's end time"
    ^endTime.
endTime:aTime
        "Set the appointment's end time to aTime"
    endTime := aTime.

info
        "Answer a String with the information on
         this appointment"

    ^OrderedCollection new
        add:user, ' has an appointment';
        add:'on ', date formPrint,
            ' at ', (self timePrintString:startTime);
        add:String new;
        addAll:text;
        yourself.

notifyTime
        "Answer a Time, the time at which the
         user is to be notified"
    ^notifyTime.

notifyTime:aTime
        "Set the time at which the user is to
         be notified to aTime"
    notifyTime := aTime.

printOn:aStream
        "Add a representation of the receiver
         to aStream"
    aStream
        nextPutAll:date formPrint;
        nextPutAll:' @ ';
        nextPutAll:(self timePrintString:startTime);
        nextPutAll:' - ';
        nextPutAll:(text at:1).

startTime
        "Answer a Time, the appointment's start time"
    ^startTime.

startTime:aTime
        "Set the appointment's start time to aTime"

    startTime := aTime.

text
        "Answer an Array containing the lines of text
         that describe the appointment"
    ^text.

text:aTextArray
        "Set aTextArray as the lines of text
         that describe the appointment"
    text := aTextArray.

timePrintString:aTime
        "Answer a string with a representation of
         aTime formatted as hh:mm AM/PM"
    ^self class timePrintString:aTime.

user
        "Answer the user for whom the appointment
         was created"
    ^user.

user:userName
        "Set the user for whom the appointment
         was created to userName"
    user := userName.






<a name="011f_0010">
<a name="011f_0011">
[LISTING TWO]
<a name="011f_0011">

"The AppointmentBrowser class definition"

ApplicationWindow subclass: #AppointmentBrowser
  instanceVariableNames:
    'user appointments appointmentList
     dateEditor textEditor notifyTimeEditor
     endTimeEditor startTimeEditor '
  classVariableNames: ''
  poolDictionaries: ''

"****  AppointmentBrowser class methods  ****"

open
        "Prompt for a user name and then open
         an AppointmentBrowser for that user"
    | userName |

    Cursor offset:Display boundingBox center.
    userName := PromptDialog
                    prompt:'Enter user name'.
    userName isNil ifTrue:[^nil].
    ^self openOn:userName.

openOn:aUserName
        "Open an AppointmentBrowser for aUserName"

    ^super new
        openWithInitializeMethod:#initializeUser:
            arguments:(Array with:aUserName).

"****  AppointmentBrowser instance methods  ****"

addAppointment
        "The user as pushed the 'ADD' button so
         we need to construct a new appointment
         record and add it to the user's list"
    | appointment index |

    appointment := Appointment new
            user:user;
            date:dateEditor date;
            startTime:startTimeEditor time;
            endTime:endTimeEditor time;
            notifyTime:notifyTimeEditor time;
            text:textEditor stringList deepCopy;
            yourself.
    appointments add:appointment.
    index := appointments indexOf:appointment.
    self updateAppointmentList.
    appointmentList selectItem:index.

changeAppointment
        "The user has pushed the 'CHANGE' button so
         we need to remove the currently selected
         appointment and then add a new on with the
         current values"

    appointmentList disableDrawing.
    self removeAppointment.
    appointmentList enableDrawing.
    self addAppointment.

createWindow
        "This method was generated by the
         Widgets/V 286 Interface Editor"

  ^TitledWindow new
    yourself;
    title: 'Appointment Browser';
    closable: true;
    iconizable: true;
    size: 415 @ 322;

    addWidget: (
        appointmentList := ListBox new
            yourself;
            nameForInstVar: 'appointmentList';
            on: #select send: #selectAppointment:;
            title: 'Appointments';
            size: 390 @ 133

    ) position: 12 @ 9;
    addWidget: (
        dateEditor := DateWidget new
            yourself;
            nameForInstVar: 'dateEditor';
            title: 'DATE:';
            date: (
                Date newDay: 16
                     month:  #December
                     year:   1990
            );
            size: 114 @ 18
    ) position: 47 @ 156;
    addWidget: (
        TitledPane new
            yourself;
            title: 'WHAT FOR?';
            size: 233 @ 116;

            addWidget: (
                textEditor := TextEditWidget new
                    yourself;
                    nameForInstVar: 'textEditor';
                    verticalScrollBar: true;
                    horizontalScrollBar: false;
                    size: 219 @ 90
            ) framer: (
              FramingParameters new
                xCentered;
                yCentered
            )
    ) position: 170 @ 151;

    addWidget: (
        notifyTimeEditor := TimeWidget new
            yourself;
            nameForInstVar: 'notifyTimeEditor';
            title: 'NOTIFY AT:';
            time: (
                Time new seconds: 74673
            );
            size: 144 @ 18
    ) position: 16 @ 269;
    addWidget: (
        TitledButton new
            yourself;
            on: #release send: #changeAppointment;
            title: 'CHANGE';
            size: 77 @ 19
    ) position: 326 @ 269;
    addWidget: (
        endTimeEditor := TimeWidget new
            yourself;
            nameForInstVar: 'endTimeEditor';
            title: 'END TIME:';
            time: (
                Time new seconds: 74673
            );
            size: 136 @ 18
    ) position: 24 @ 230;
    addWidget: (
        startTimeEditor := TimeWidget new
            yourself;
            nameForInstVar: 'startTimeEditor';
            on: #valueChanged send: #startTimeChanged:;
            title: 'START TIME:';
            time: (
                Time new seconds: 74613
            );
            size: 148 @ 18
    ) position: 12 @ 192;
    addWidget: (
        TitledButton new
            yourself;
            on: #release send: #addAppointment;
            title: 'ADD';
            size: 77 @ 19
    ) position: 170 @ 269;
    addWidget: (
        TitledButton new
            yourself;
            on: #release send: #removeAppointment;
            title: 'REMOVE';
            size: 77 @ 19
    ) position: 248 @ 269.
initializeUser:argArray
        "Initialize an AppointmentBrowser for the
         user whose name is given in argArray"

    user := argArray.
    (Appointments includesKey:user)
        ifFalse:[
            Appointments
                at:user
                put:(SortedCollection
                        sortBlock:[:a :b| a < b])].
    appointments := Appointments at:user.
    self window
        on:#activated send:#updateAppointmentList;
        title:'Appointments for ', user.
    dateEditor date:Date today.
    startTimeEditor time:(Time fromSeconds:32400).  "9:00 AM"
    self
        startTimeChanged:startTimeEditor time;
        updateAppointmentList.

removeAppointment
        "The user has pushed the 'REMOVE' button so
         we need to remove the selected appointment
         from the user's list"
    | index appointment |

    appointments size = 0 ifTrue:[^self].
    index := appointmentList selectionIndex.
    appointment := appointments at:index.
    appointments remove:appointment ifAbsent:[].
    self updateAppointmentList.
    index > appointments size
        ifTrue:[index := appointments size].
    appointmentList selectItem:index.

selectAppointment:aString
        "The user has selected an appointment so
         we need to fill-in all of the appropriate
         field editors"
    | appointment |

    appointment :=
        appointments
            at:appointmentList selectionIndex.
    dateEditor date:appointment date.
    startTimeEditor time:appointment startTime.
    endTimeEditor time:appointment endTime.
    notifyTimeEditor time:appointment notifyTime.
    textEditor stringList:appointment text.

startTimeChanged:aString
        "The user has changed the start time
         so we need to supply rerasonable
         defaults for the end time and the
         notify time"
    | aTime |

    aTime := startTimeEditor time.
    "Assume appointment is one hour long"

    endTimeEditor
        time:(aTime addTime:(Time fromSeconds:3600)).

    "Assume notify 5-minutes before"
    notifyTimeEditor
        time:(aTime subtractTime:(Time fromSeconds:300)).

updateAppointmentList
        "Update the list of appointments"
    | size list |

    (size := appointments size) = 0 ifTrue:[^self].
    list := Array new:size.
    1 to:size do:[:index|
        list
            at:index
            put:(appointments at:index) printString].
    appointmentList  stringList:list.

user
        "Answer the name of the user for which
         this browser was opened"

    ^user.

windowLocation
        "Make the window centered on the screen"

    ^#center.






<a name="011f_0012">
<a name="011f_0013">
[LISTING THREE]
<a name="011f_0013">

"The AppointmentNotifier class definition"

ApplicationWindow subclass: #AppointmentNotifier
  instanceVariableNames:
    'active running minute text appointment '
  classVariableNames: ''
  poolDictionaries: ''

"****  AppointmentNotifier instance methods  ****"

acknowledge
        "The user has pushed th 'OK' button, so we
         need to remove the current appointment
         from the user's list"
    | user list |

    appointment isNil ifTrue:[^self].
    user := appointment user.
    (list := Appointments
                at:user
                ifAbsent:[nil]) isNil
        ifFalse:[
            list
                remove:appointment
                ifAbsent:[]].
    text
        stringList:#();
        display.
    appointment := nil.
    minute := nil.

activateFor:anAppointment
        "Display the details of anAppointment and
         bring this window to the top"

    appointment isNil
        ifTrue:[
            "Previous appointment
             has been dismissed"
            text
                disableDrawing;
                stringList:anAppointment info;
                enableDrawing.
            appointment := anAppointment].
    Terminal bell;  bell.
    Cursor offset:window origin.
    ScreenManager activateWindow:self window.

clockEvent
        "A clock tick has been received so we have
         to determine if the minute has rolled over
         and, if so, are any appointments ready"
    | now thisMinute today list appointment |

    now := Time now.
    (thisMinute := now minutes) = minute
        ifTrue:[^self].
    minute := thisMinute.
    today := Date today.
    Appointments associationsDo:[:anEntry|
        (list := anEntry value) size > 0
            ifTrue:[
                ((appointment := list first)
                        checkTime:now date:today)
                    ifTrue:[self activateFor:appointment]]].
closeWindow
        "Before closing this window, terminate the
         process that's monitoring clock events"

    active := false.
    "Wait for the process to terminate"
    [running] whileTrue:[Processor yield].
    super closeWindow.

createWindow
        "This method was generated by the
         Widgets/V 286 Interface Editor"

  ^TitledWindow new
    yourself;
    title: 'Appointment Notifier';
    closable: true;
    size: 183 @ 155;

    addWidget: (
        text := TextEditWidget new
            yourself;
            nameForInstVar: 'text';
            verticalScrollBar: true;
            horizontalScrollBar: false;
            size: 169 @ 91
    ) position: 5 @ 6;
    addWidget: (
        TitledButton new
            yourself;
            on: #release send: #acknowledge;
            title: 'OK';
            default: true;
            size: 57 @ 23
    ) framer: (
      FramingParameters new
        xCentered;
        originY: 103 relativeTo: #origin
    ).

initialize
        "Initialize a new AppointmentNotifier"
    active := false.
    running := false.
    minute := Time now minutes.
    [self run] forkAt:Processor highUserPriority.
run
        "Run the process that handles clock events"
    | timerSemaphore |

    (timerSemaphore := Smalltalk
                        at:#TimerSemaphore
                        ifAbsent:[nil])
            isNil
        ifTrue:[
            timerSemaphore := Semaphore new.
            Smalltalk
                at:#TimerSemaphore
                put:timerSemaphore].
    active := true.
    running := true.
    [active]
        whileTrue:[
            timerSemaphore wait.
            self clockEvent].
    Smalltalk removeKey:#TimerSemaphore.
    running := false.

windowLocation
        "Make the window centered on the screen"

    ^#center.







<a name="011f_0014">
<a name="011f_0015">
[LISTING FOUR]
<a name="011f_0015">

"Modifications to the Process class methods"

timerInterrupt
        "Implement the timer interrupt."
    | timerSemaphore |

    PendingEvents add: (Message new
        selector: #clockEvent:;
        arguments: (Array with: 1)).
    KeyboardSemaphore signal.

    "**********************************************
    Added by Ken Ayers to support the
    Appointment Manager Application"

    timerSemaphore := Smalltalk at:#TimerSemaphore
                            ifAbsent:[nil].
    timerSemaphore notNil
        ifTrue:[timerSemaphore signal].
    "**********************************************"
    self enableInterrupts: true.





<a name="011f_0016">
<a name="011f_0017">
[LISTING FIVE]
<a name="011f_0017">

"Corrections to TimeWidget methods"
"****  TimeWidget instance methods  ****"

time
    | hours |

    hours := self hours.

    (self meridianEditor value = 'PM')
        ifTrue:[
            hours < 12 ifTrue:[hours := hours + 12]]
        ifFalse:[
            hours = 12 ifTrue:[ hours := 0]].

    ^(Time fromSeconds:0)
        hours:hours;
        minutes:self minutes.

time: newTime
    | hours |

    self time = newTime ifTrue: [^self].

    hours := newTime hours.

    (hours >= 12)
        ifTrue: [
            self meridianEditor value: 'PM'.
            hours > 12 ifTrue:[hours := hours - 12]]
        ifFalse: [
            self meridianEditor value: 'AM'.
            hours = 0 ifTrue:[hours := 12]].

    self hourEditor value: hours.

    self minuteEditor value: newTime minutes.


Copyright © 1991, Dr. Dobb's Journal


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.