Matthew Flatt
mflatt@cs.rice.edu
Rice University
Houston, TX
October 1995
Copyright ©1995 Matthew Flatt
Permission to use, copy, modify, and distribute this software and its documentation for any purpose is hereby granted without fee, provided that the above copyright notice, author statement and this permission notice appear in all copies of this software and related documentation.
THE SOFTWARE IS PROVIDED ``AS-IS'' AND WITHOUT WARRANTY OF ANY KIND, EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
IN NO EVENT SHALL MATTHEW FLATT BE LIABLE FOR ANY SPECIAL, INCIDENTAL, INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER OR NOT ADVISED OF THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF LIABILITY, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
wxWindows: Copyright ©1994 Artificial Intelligence Applications Institute, The University of Edinburgh. All rights reserved.
libscheme: Copyright ©1994 Brent Benson. All rights reserved.
Conservative garbage collector: Copyright ©1988, 1989 Hans-J. Boehm, Alan J. Demers. Copyright ©1991-1994 by Xerox Corporation. All rights reserved.
Collector C++ extension by Jesse Hull and John Ellis: Copyright ©1994 by Xerox Corporation. All rights reserved.
This manual attempts to explain what MrEd is, why MrEd might be useful to you, and how to use MrEd. For a complete description of MrEd, you will also need to refer several other documents described in Chapter 5.
The typesetting sources for this manual are taken from Reference Manual for wxWindows 1.60: a portable C++ GUI toolkit.
HTML documents were produced using latex2html.
Many thanks to Julian Smart for wxWindows and his help. Thanks also to Brent Benson for libscheme, and to Hans Boehm and John Ellis for the conservative garbage collector and their help. MrEd would not be possible without these fine packages.
Thanks also to Dan Grossman, Stephanie Weirich, Gann Bierner, Shriram Krishnamurthi, Matthias Felleisen, Cormac Flanagan, Robby Findler, Corky Cartwright, Kennis Koldewyn, and Bruce Duba, for feedback and help.
MrEd is a graphical user interface (GUI) development environment, providing access to an extensive GUI class library (wxWindows) within a full-featured, medium-performance Scheme implementation (MzScheme).
MrEd's GUI library includes a powerful editor class that handles both text (with styles) and arbitrary graphical objects. Non-text objects can be interactive; for example, an editing buffer can be embedded as a single item (i.e., one big ``character'') within another buffer. This generalizes the hyper-text model, where the buffer's text and pictures are static and the only interaction is ``click on me''.
MrEd's Scheme interpreter, MzScheme, is fully R4RS compliant, with additional features useful for GUI development: an object system and pre-emptive threads. MzScheme's performance is comparable to that of SCM (Aubrey Jaffer) or Scheme 48 (the Scheme Underground).
MrEd applications will ultimately be portable among the major windowing systems, including X-Windows, Microsoft Windows, Macintosh, and OS/2. Currently, MrEd runs under X-Windows (using either Motif or XView) and Microsoft Windows.
MrEd is appropriate for developing any Scheme-based application which needs a graphic interface. MrEd is particularly useful when the graphic interface needs sophisticated text or graphic-object manipulation.
MrEd provides the standard user interface tools that are common to every GUI toolbox: windows, buttons, menus, simple text items, canvases, etc. In addition, MrEd provides two high-level objects:
MrEd's editor and pasteboard objects share an extensible implementation for the sub-objects they contain. This extensibility is powerful enough that editors and pasteboards can be mutually nested to an arbitrary depth.
MrEd can also be used as an editor. The ``standard system'' which MrEd uses to start up provides an Emacs-like editing environment. However, the vast body of Emacs packages has not been ported to MrEd; only Scheme mode and a hyper-text mode have been fully implemented. As code is written to make MrEd's standard editor more powerful, new features can be inherited by all applications written in MrEd.
If your development language is C++ instead of Scheme, MrEd's editor classes are available as an extension to wxWindows. MzScheme can also be made available as an extension language for your application's users. Of course, we feel that Scheme is an inherently better development language for most tasks, largely due to its safety and improved prototyping abilities.
The GUI aspect of MrEd is much like Tcl/Tk or STk: both provide a toolbox of graphic objects with an interactive development language. MrEd's graphic object toolbox contains essentially the same elements as Tk. However, there are several architectural differences:
There are several all GUI development system which are comparable to MrEd in that they connect LISP or Scheme to an external GUI toolbox. These tools are typically not portable, and the GUI toolbox tends to be smaller or lower-level.
A list of other GUI development systems is available at:
http://www.cs.cmu.edu/afs/cs/user/bam/www/toolnames.html
MrEd's editing system follows the Emacs model of extensibility: the editor is written in its own extension language. However, there are fundamental differences:
MrEd currently works under X-Windows and Microsoft Windows. The Micorsoft Windows version still needs some work. When wxWindows is ready for Macintosh, MrEd will run on all three major platforms. This will probably happen sometime during 1995. (Warning: this is pure speculation!)
MrEd is built on top of several packages and modules:
Developers who use MrEd to create new applications will need to understand the role of each of these modules (except for the garbage collector), although only wxScheme is used directly.
MrEd's source code can be freely distributed. Please read the license agreement, as well as the license agreements for wxWindows, libscheme, and the conservative garbage collector.
Work on MrEd is still ongoing. The basic architecture is completely in place. The pasteboard implementation still needs the most work, and it is still incomplete in several areas due to lack of demand. MrEd is quite usable: we are using it now in an entry-level programming course.
Up-to-date information about MrEd can always be found at the MrEd Home
Page:
http://www.cs.rice.edu/~mflatt/mred.html
Compiled versions of MrEd are occasionally available from the MrEd Home Page (see More Information, above). At this time, most X-Windows users will need to compile the code from scratch. This is not a trivial task.
MrEd currently only compiles under X-Windows. You will probably need 20-30 MB of disk space. You will need to download the following packages:
First, read the wxWindows installation notes (in the ``install'' directory of the wxWindows distribution), but do not compile wxWindows until you have read all of the following instructions.
Set the WXDIR environment variable to the path of your wxWindows installation. All the directories from the wxWindows installation should be in the WXDIR directory.
In wxWindows's ``wx_setup.h'', you must turn on certain flags:
In wxWindows's ``make.env'', you will need to change some of the variables:
If you're unlucky, you'll also have to adjust the flags for compiling MzScheme. These are at the top of ``Makefile'' in .../mred/wxs/mzscheme and in ``sconfig.h'' in the same directory. (A description for each flag is included there.) Try compiling with the default flags unless you encounter a problem.
Once all of your setup files are ready, go to your MrEd distribution directory and type ``make depend''. Then type ``make wx'' to compile wxWindows. After wxWindows has compiled, type ``make'' to compile MrEd.
Here are a few compiling hints:
Before you run MrEd, you will need to make sure that MrEd's system files can be found. These files are in the system/ directory of the MrEd distribution. If you run MrEd from the distribution directory, the files will be found. Otherwise, you should set the environment variable MREDSYS to the full path of system/.
When you run MrEd, the window that appears is a console, which provides a read-eval-print loop connected to wxScheme. To evaluate an expression, type it after the prompt and hit return. You can re-evaluate old expressions by moving the caret to the end of the expression and hitting return. Evaluate a subexpression by hiliting it and hitting return. To stop evaluation, type Ctl-Shift-C.
To open a standard text-editing window from the console, use the ``Edit File...'' or ``Edit New File'' items in the ``File'' menu.
When MrEd starts up, it looks for a ``.mredrc'' file in the user's home directory. If such a file exists, it is loaded (as a Scheme file) before the console is opened. If you want to configure MrEd in any way, you must create a ``.mredrc'' file. Currently, the only documentation on configuration is the full development documentation.
To demonstrate the advantages of interactive GUI development in MrEd, we work through the development of an interactive phone book (See Figure ``Phonebook''). Our phone book interface consists of a name field, a toggle button to select ``home'' or ``office'', and a display field to show the phone number. The phone number field will be automatically updated whenever the name or the ``home''-``office'' toggle is changed by the user. Hitting the ``Quit'' button to close the frame.
- Figure: An interactive phone book interface -
To implement the interactive phone book, we first instantiate the graphical elements that make up the interface, focusing on the elements' appearance rather than their functionality. The basic graphical element in the interactive phone book is a frame, i.e., a top-level window. To create a frame, wemake an instance of the primitive class wx:frame% (The wx prefix indicates that the value is a built-in primitive; the % suffix indicates that the value is a class.) using the make-object procedure:
(define FRAME-WIDTH 400) (define FRAME-HEIGHT 175) (define a-frame (make-object wx:frame% '() ; No parent frame "Phone Book" ; The frame's title -1 -1 ; Use the default position FRAME-WIDTH FRAME-HEIGHT))The procedure make-object takes a class and class-specific initialization parameters and returns an object that is an instance of the class. The empty list is used here to indicate that the frame has no graphical parent (because it is a top-level frame). The remaining parameters specify the title, location, and size of the frame. When the above expressions are evaluated, a-frame is bound to an invisible graphical frame. Evaluating:
(send a-frame show \#t)makes the frame visible. The frame is empty at the moment.
We can add text fields and buttons to a-frame by creating a panel in the frame. A panel is a sub-window in a frame that is used to group together a set of controls, such as text fields and buttons. All controls in a frame must be placed in a panel. A panel is created by instantiating the wx:panel% class:
(define a-panel (make-object wx:panel% a-frame ; Panel is in a-frame 0 0 FRAME-WIDTH FRAME-HEIGHT)) ; Occupy whole frame
The first initialization parameter for a panel specifies its graphical parent, a-frame. When the panel is created, it will automatically install itself as a child of a-frame. The remaining parameters specify the location and size of the panel within the frame. Evaluating the above expression creates a panel in the graphical frame, although there will be no visible effect in the frame.
Next, we add controls to the panel, starting with the top-left element, the ``Name'' text field:
(define NAME-WIDTH 300) (define name-text (make-object wx:text% a-panel (lambda (self event) (refresh-number-info)) "Name" "" -1 -1 ; Default position NAME-WIDTH -1)) ; Specific width, default heightThe class wx:text% implements a simple text control. The first initialization parameter is the control's graphical parent, a-panel. The second parameter is a ``callback'' procedure that is invoked whenever this control gets keyboard input. When the text in this control is changed, a new phone number search will be performed; for now, this procedure remains unspecified. The third initialization parameter specifies a label for the item, and the fourth parameter is the initial string value in the control. The last parameters specify the location and size of the control. Evaluating the expression creates a new text control that immediately appears in the graphical frame. The new text control is graphically functional, i.e., text can be enetered into the control, but the control's value is not yet used to find a phone number.
Next, we create the ``Quit'' button:
(define quit-button (make-object wx:button% a-panel (lambda (self event) (send a-frame show \#f)) "Quit")) ; Button labelWhen this expression is evaluated, a button labelled ``Quit'' appears in the graphical frame. Unlike the text field, the callback procedure provided here already does something; clicking on this button will close the frame. When the frame is hidden, it is not destroyed. Evaluating (send a-frame show #t) will make the frame visible again.
The position and size for the button was left unspecified because the position and size can be deduced automatically. By default, each new control is placed to the right of the existing controls in the panel. Since our next control needs to be placed below the text field, we introduce a vertical break by invoking the new-line method of a-panel (This layout technique suffices for a simple interface. MrEd also provides tools for constraint-based layout of graphical elements.) :
(send a-panel new-line)
If we now add the ``home''-``office'' toggle selector, it appears on its own line:
(define number-selector (make-object wx:radio-box% a-panel (lambda (self event) (refresh-number-info)) "" ; No label -1 -1 -1 -1 ; Default position and size (list "Home Number" "Office Number")))A radio-box% graphical element contains a number of mutually exclusive switches. The last initialization argument specifies a list of labels for the switches. Evaluating the above expression creates a toggle selector with switches labelled ``Home Number'' and ``Office Number''. Like the ``Name'' text field, this toggle selector is immediately graphically functional, but the selection is not yet used to find a phone number.
Finally, we create the phone number display element as a text control:
(send a-panel new-line) (define NUMBER-WIDTH 300) (define number-text (make-object wx:text% a-panel (lambda (self event) \#f) ; No event-handling "Number" "(Unknown)" -1 -1 ; Default position NUMBER-WIDTH -1)) ; Specific width, default height (send number-text set-editable \#f)Evaluataing these expressions creates the ``Number'' display text control. The last expression configures the control so that the user cannot type a value into the field.
We have implemented the graphical aspects of the interface completely, but the only functional aspect of our interface that has been implemented is the ``Quit'' button. To make our interface fully functional, refresh-number-info must be implemented, using the current ``Name'' field and toggle selector values to find a phone number:
(define refresh-number-info (lambda () (let* ([name (send name-text get-value)] [home? (zero? (send number-selector get-selection))] [number (lookup-number name home?)] [number-string (if number number "(Unknown)")]) (send number-text set-value number-string))))The method get-value extracts the string value of the ``Name'' text field name-text. Similarly, get-selection extracts the index of the selected switch in the ``home''-``office'' toggle selector number-selector. The pre-supplied Scheme procedure lookup-number performs the phone book search, returning either a phone number string or #f. If no number is found in the phone book, the displayed result is ``(Unknown)''. Finally, the set-value method of number-text is invoked to update the ``Number'' display text field.
Our phone book interface triggers a phone book search every time the user types a character into the ``Name'' text field. This algorithm works if the phone book search is fast, but if a single search takes a relatively long time, then the user will be forced to wait after each character is typed.
This problem can be solved without changing the graphical interface by performing the phone book search in a separate thread. The procedure refresh-number-info should start a new thread each time it is called, returning immediately to process more user events. If a new search is started before the old one has completed, then refresh-number-info must send a cancellation signal to the old thread so that it does not try to display its search results. The threaded refresh-number-info is shown in Figure ``Thread''.
(define refresh-number-info (let ([previous-cancel-trigger (make-trigger)]) (lambda () (let ([this-cancel-trigger (make-trigger)]) (trigger-hit previous-cancel-trigger) (set! previous-cancel-trigger this-cancel-trigger) (thread (lambda () (send number-text set-value "(Searching...)") (let* ([name (send name-text get-value)] [home? (zero? (send number-selector get-selection))] [number (lookup-number name home?)] ; May take a while... [number-string (if number number "(Unknown)")]) (atomic (lambda () (unless (trigger-hit? this-cancel-trigger) (send number-text set-value number-string)))))))))))- Figure: A threaded phone book search -
To send a cancellation signal to an old thread, refresh-number-info uses a trigger. Triggers are a built-in facility in MrEd for communicating between threads. The local variable previous-cancel-trigger holds the trigger for communicating with the most-recently spawned thread. (Previous-cancel-trigger is initialized to a trigger that is not connected to a thread.) When refresh-number-info is invoked, it creates a new trigger to be used in a new thread. It then ``hits'' the previous thread's trigger, which will keep the previous thread from displaying its results. The new trigger is saved into previous-cancel-trigger for the next call to refresh-number-info.
After preparing a trigger, refresh-number-info spawns a new thread to perform the phone book lookup, and then returns to handle more user events. Meanwhile, the new thread finds a number in the phone book using the old lookup algorithm. Once the thread has a result to display, it first checks to see if its trigger has been hit; if the cancellation trigger has been not hit, then the thread displays its result into number-text.
Figure ``Class'' shows a class definition that encapsulates a phone book search session. The make-class syntactic form creates a new class. The first part of a make-class expression specifies a base class for the new class. In the the example, the empty list is used as the base class, indicating that there is no base class for pb-session%. The body of a make-class expressions defines the instance variables of the new class. Keywords such as public and private are used to declare new instance variables. The last part of a make-class expression specifies an initialization procedure that is invoked when an instance of the class is created.
(define pb-session% (make-class '() (public [FRAME-WIDTH 400] [FRAME-HEIGHT 175] [NAME-WIDTH 300] [NUMBER-WIDTH 300] [refresh-number-info ...] ; Same as before a-frame a-panel) (private name-text quit-button number-selector number-text) (lambda () (set! a-frame (make-object wx:frame% '() "Phonebook" -1 -1 FRAME-WIDTH FRAME-HEIGHT)) (send a-frame show #t) (set! a-panel (make-object wx:panel% a-frame 0 0 FRAME-WIDTH FRAME-HEIGHT)) (set! name-text (make-object wx:text% a-panel (lambda (self event) (refresh-number-info)) "Name" "" -1 -1 NAME-WIDTH -1)) (set! quit-button (make-object wx:button% a-panel (lambda (self event) (send a-frame show #f)) "Quit")) (send a-panel new-line) (set! number-selector (make-object wx:radio-box% a-panel (lambda (self event) (refresh-number-info)) "" -1 -1 -1 -1 (list "Home Number" "Office Number"))) (send a-panel new-line) (set! number-text (make-object wx:text% a-panel (lambda (self event) #f) "Number" "(Unknown)" -1 -1 NUMBER-WIDTH -1)) (send number-text set-editable #f))))- Figure: Class definition for a phone book session -
The pb-session% class has several public instance variables: the size parameters FRAME-WIDTH, etc., the procedure refresh-number-info, and the graphical elements a-frame and a-panel. While the instance variables for size parameters and refresh-number-info have intial values, a-frame and a-panel do not. Instead, a-frame and a-panel are initialized using set! in the initialization procedure for pb-session%. The instance variables for the control graphical elements are private; like a-frame and a-panel, they are assigned values in the initialization procedure.
An instance of pb-session% is created using make-object:
(define a-session (make-object pb-session%))
Since the initialization procedure for pb-session% takes no arguments, no intialization arguments are specified in this make-object expression. Evaluating this expression creates a phone book search frame, just as in the original implementation using global variables. The only difference is that multiple independent frames can be opened by making multiple instances of pb-session%.
The public instance variables of a-session can be accessed with the ivar form. The expression (ivar a-session a-frame) returns the frame object of a-session. Private variables cannot be obtained with ivar. Neither public nor private variables can be changed from ``outside'' the object.
The instance variable refresh-number-info has a procedure value. This procedure can be called directly to force a phone number search in a-session:
((ivar a-session refresh-number-info))This expression extracts the procedure value of the instance variable and then invokes the procedure. When an instance variable has a procedure value, then the instance variable is a method. Therefore, the send syntactic form can be used with refresh-number-info:
(send a-session refresh-number-info)This expression is equivalent to the previous one.
We can now derive a new class from pb-session% to extend the behavior of a phone book session. Figure ``Counter'' shows the derivation of the class pb-counted-session% from pb-session%. In the new class, a counter displays the number of phone book searches that have been performed so far.
(define pb-counted-session% (make-class pb-session% (inherit a-panel) ; We need to access the panel object... (rename [basic-refresh-number-info refresh-number-info]) ; and old refresh (private [search-counter 0] counter-text) ; Counter value and display (public [FRAME-HEIGHT 210] ; Make the frame taller [COUNTER-WIDTH 250] [refresh-number-info ; Increment the counter and call old refresh (lambda () (set! search-counter (add1 search-counter)) (send counter-text set-value (number->string search-counter)) (basic-refresh-number-info))]) (lambda () (super-init) ; Do base class initialization (send a-panel new-line) (set! counter-text (make-object wx:text% a-panel (lambda (self event) #f) "Number of Searches Started" "0" -1 -1 COUNTER-WIDTH -1)) (send counter-text set-editable #f))))- Figure: Class definition for a phone book session with a counter -
In the make-class expression for pb-counted-session%, pb-session% is used as the base class. This means that an instance of pb-counted-session% will have all of the instance variables that are declared in pb-session%. However, to access base class instance variables directly from methods defined in the derived class, the base class variables must be declared with the inherit or rename keyword. Instances variables in an inherit clause are made available in the derived class using their original name, while variables in a rename clause are made available under a new name. In the definition of pb-counted-session%, the instance variable a-panel is inherited so that a counter display can be added to the panel. The instance variable refresh-number-info is renamed to base-refresh-number-info; the variable is renamed because pb-counted-session% also overrides refresh-number-info, but the new method needs to access the original method specified in the base class.
The private clause declares instance variables for implementing the counter. In the public clause, the instance variable FRAME-HEIGHT is declared, overriding the declaration in pb-session%. This means that when the frame is created, the value for FRAME-HEIGHT specified in pb-counted-session% will be used instead of the value specified in pb-session%, even when FRAME-HEIGHT is used in an expression from the pb-session% definition. This override occurs only when making an instance of pb-counted-session%, not when making instances of pb-session%.
The instance variable refresh-number-info is also overridden in pb-counted-session%; the new method needs to extend the old functionality, rather than replace it completely, so the original refresh-number-info is invoked in the new method by using base-refresh-number-info.
In the initialization function for pb-counted-session%, the first expression is (super-init); this invokes the intialization procedure defined in the base class, pb-session%. The rest of the initialization function sets up the new counter display in the panel.
To make the example in Figure ``Counter'' a complete MrEd application, we need to add the following definition:
(define mred:startup (lambda args (make-object pb-counted-session%)))
When MrEd is started with this program, then mred:startup will be invoked, creating a phone book search session, which in turn creates a frame. When the user presses the ``Quit'' button, the frame will be closed. Since no more frames are open, the application will exit.
To develop applications and extensions within MrEd, you will have to understand all of MrEd's constituent modules, which are described in the section titled ``What are...''. Since each module is available as a separate package, MrEd's documentation is effectively distributed among the module documents:
Master Index for MrEd Documentation, distributed with MrEd, is useful for finding information across all of these documents.
It is unfortunate and unsatisfactory that a wxScheme programmer must read C++ class documentation and decipher the translation to Scheme. However, the size of the class libraries makes it prohibitively difficult to produce Scheme-based class documentation at this time.
enddocument