Professional Documents
Culture Documents
Preface
Welcome!
This book is about programming computers running NeXTSTEP. It’s a no-
nonsense, hands-on book that teaches programmers how to write applica-
tion programs that take full advantage of NeXTSTEP, the operating envi-
ronment from NeXT Computer Inc.
We wrote this book out of frustration: at the time, there was no single book
that explained step-by-step how to write programs for NeXTSTEP. Instead,
a programmer trying to approach the platform was confronted by the NeXT
technical documentation and the source-code for several dozen example
programs. There was no mention of style, program structure or design con-
siderations. It seemed that there was a black art to writing programs for the
black computer, along with a priesthood which promised that newcomers
might one day become members – once we paid our dues.
With this book, we’ve paid a lot of your dues for you.
This book is for anybody who knows how to program in C and wants to do
something substantial with NeXTSTEP. Our goal is to get you up-and-run-
ning as quickly as possible. This isn’t a textbook or a reference manual –
Brad Cox (the inventor of Objective-C) and the technical writing depart-
ment at NeXT have already written those. This book is instead a jumping-
off point. Consequently, we will occasionally sacrifice depth for breadth
viii Preface
and brevity. You won’t learn all of the options of Interface Builder or all the
obscure methods that the Text object implements by reading this book. But
you’ll acquire the conceptual framework necessary to understand NeXT’s
excellent documentation, enabling you to discover what you need to know
about the Text object and everything else.
All of the examples in this book were developed and tested under both ver-
sion 2.1 and 3.0 of the NeXTSTEP operating environment. (When we write
NeXTSTEP 2.1 this includes NeXTSTEP 2.0, 2.1, and 2.2.) Although
NeXTSTEP 3.0 offers many features not found in the 2.1 operating system,
from the programmer’s point of view the two releases are actually quite
similar. Furthermore, we’ve tried to limit the subject matter of this book to
those underlying concepts and features of the operating system – the parts
that haven’t changed much since the initial release of NeXTSTEP 1.0 in
1989.
To help deal with this difference, we’ve explained how to use Interface
Builder under both 2.1 and 3.0. We’ve also covered explanatory informa-
tion about Project Builder, where appropriate.
What is NeXTSTEP?
NeXTSTEP is the object-oriented environment that runs on NeXT, 80486-
based, and possibly other computers. It is the main reason that the NeXT
computer is different, both for users and programmers.
From the user’s point of view, NeXTSTEP is the unified graphical operat-
ing environment that makes a computer running it easy to use:
• Display PostScript, a version of Adobe’s PostScript imaging system
that displays information on a computer’s screen.
• Workspace, NeXTSTEP’s graphical interface to the UNIX file system.
• Mail, Digital Librarian, the Edit editor, and other NeXT-supplied appli-
cation programs.
What You Need ix
• The Preferences and Defaults system, which lets users store their pref-
erences for individual programs without having to directly modify spe-
cial files stored in their home directories.
If you have a NeXTstation with a 105MB hard disk, you’re out of luck:
there’s just no way to make the files and programs that you need for the
development environment fit onto this hard disk and still have room left to
do anything useful. Your best bet is to buy a 400MB (or larger) external
hard disk, put the Extended Edition on it, and boot it as your main disk: use
the internal hard disk for backup and for virtual memory swapping space.
If you have a NeXTstation with a 200MB or 250MB hard disk, you can
copy the individual files that you need to develop programs and still have
some breathing space left over to do some serious work. Don’t worry about
copyright restrictions: the Standard Edition includes a license for the
Extended Edition. Just copy the files from a friend or a friendly computer
store. The files are also on the NeXTSTEP 3.0 CD-ROM.
If you are using an Intel 486-based computer with NeXTSTEP 486, then
you must specially purchase the Developer’s Edition if you wish to write
programs.
Directories:
/lib, /usr/lib/nib, /usr/include
nm size
cc gdb
nm++ strip
cc++ kgdb
otool ebadexec
ranlib pswrap
kl_ld 1
Library Files:
/usr/lib/*.a
Note: UNIX libraries are files that end with the extension .a. If you copy a
library with the cp command, you will need to run ranlib on the library to
“refresh” the date stored in it.
Additional Programs
Besides the bare minimum, NeXTSTEP comes with a lot of documentation
and additional programs that make programming a pleasure. Below we list
the directories and programs that will make programming with NeXTSTEP
a lot easier.
/NextLibrary/Documentation/NextDev
/NextLibrary/Documentation/UNIX
/usr/lib/emacs
/usr/bin/etags
1. /usr/bin/kl_ld and kgdb are only needed if you wish to do kernel programming.
xii Preface
This book is divided into five main sections, two appendices, and a floppy
disk.
Part 1
The first part of this book introduces NeXTSTEP, NeXTSTEP program-
ming, and the Objective-C language in which NeXTSTEP is written.
Part 2
The second part of this book is focused upon building a simple application
program – a calculator – which we slowly extend through four chapters.
The Organization of this Book xiii
Part 3
The third part of this book focuses on building a new application program
called MathPaper. MathPaper is like a word processor, except when you hit
return, the application program solves the equation that you typed in the
window. The application uses a back-end mathematical processor called
Evaluator to do its work.
In Chapter 11, “Spawning Multiple Processes and the Text Object,” we tie
MathPaper’s front end and back end together with a Process object that can
spawn subprocesses. MathPaper is fully functional by the end of this chap-
ter.
Chapter 12, “Text and Rich Text,” discusses Microsoft’s Rich Text format,
which NeXTSTEP uses to encode information such as font, point size, and
alignment into a text stream. We use Rich Text to make MathPaper’s output
look more professional.
Part 4
The fourth section of this book is about drawing in NeXTSTEP Views with
Display PostScript. These chapters assume some familiarity with the Post-
Script language, but you can probably pick it up along the way if you need
to.
Chapter 15, “Draw Yourself: All About NeXTSTEP Views,” leaves Math-
Paper. We spend the whole chapter exploring the View class in general and
the drawSelf:: method in particular.
Chapter 17, “Color,” shows how to draw color with Display PostScript and
NeXTSTEP.
Chapter 18, “View Resizing and Mouse Tracking,” shows how to catch two
kinds of events with Views: resizes and mouse clicks. We do this by modi-
The Organization of this Book xv
fying the GraphPaper application so that it can display the value of the
(x,y) pairs of the graph for wherever the user places the mouse.
Part 5
The last part of this book fills out topics that haven’t been covered any-
where else – things that you don’t need to get your application going, but
that you do need to make it polished.
Chapter 19, “Zooming and Saving Graphics Files,” shows how to put a
zoom-button on a view to zoom in or zoom out. We also show how to save
a graphics image as an Encapsulated PostScript file or as a TIFF bitmap
image.
Chapter 20, “The Pasteboard and Services,” shows how to put data on the
Pasteboard and take it off. We also show how to make GraphPaper a
NeXTSTEP Service, so you can graph equations that are in other applica-
tion programs.
Appendices
In “Appendix A: Source Code Listings,” we’ve included complete source
code listings for the three major applications in this book: Calculator,
MathPaper, and GraphPaper.
“Appendix B: References,” lists other books that you might find helpful in
programming NeXTSTEP.
Floppy Disk
The 1.44MB floppy disk included with this book contains the complete
source code for the programs that we develop during the course of each
chapter. The data is provided in the form of a single file that has been pro-
cessed with the UNIX compress and tar commands. The disk is in MS-
DOS file format so that you can read it on either a NeXTstation, an indus-
try-standard personal computer running NeXTSTEP 486, or any PC run-
ning MS-DOS.
To unload the program files from the floppy, it is necessary to copy the file
into one of your directories and process the file. The simple instructions on
how to do this are located on the floppy disk in a file called README.
xvi Preface
We recommend that you use this floppy disk as a last resort. You’ll learn
more about programming NeXTSTEP if you take the time to type in the
demonstration programs, rather than simply loading them off the floppy
disk and running them. We’ve provided the disk so you have something to
fall back upon in the event that the programs you type in don’t work.
This book was designed by Michael K. Mahoney and is set in Times and
Courier. It was produced entirely on NeXT Computers. The book was
built and formatted using FrameMaker 3.0. The screen shots were
“grabbed” using the Grab application. The icons were built in IconBuilder.
The code was written using Emacs and Edit.
Acknowledgments
Bringing this book from an idea to final publication took a lot of time on
the part of a lot of people. The initial idea for the book came from Marc
Davis, Simson’s office mate at the MIT Media Laboratory. Simson started
writing this book in October 1990.
Allan Wylde has also opened to the door for us to write “STEP TWO” and
“STEP THREE” books on NeXTSTEP programming. These books would
cover advanced topics such as the PhoneKit, database and the DBKit, and
3D graphics and the 3DKit. These kits were introduced in NeXTSTEP 3.0.
We are proud that this book is the very first book to be published by
TELOS, Springer-Verlag’s “The Electronic Library of Science.” By pub-
lishing with TELOS, we are able not only to bring a paper book into print,
but we have also opened the door to electronic publishing of this book’s
contents. Look for a CD-ROM related to this book – possibly with an
indexed version of the book with more information and interactive exam-
ples – at some point in the future. Fill out the perforated card in the book
and mail it so TELOS can keep you apprised of the latest details on this
exciting possibility. We’d like to thank Howard Ratner and Henry Krell at
Springer, and Cindy Peterson and especially Allan Wylde at TELOS for
their help with the production and editing of this book.
Lots of people at NeXT and elsewhere provided a lot of help: Mike Hawley
(originally at NeXT, now at the MIT Media Lab) gave Simson his first
introduction to the computer in 1988. Adam Hertz was responsible for get-
ting Simson a NeXTcube in 1990 and writing Simson’s first series of pro-
grams on it. Bob Clover (originally at CSU Long Beach, now at Cal Poly
San Luis Obispo) was responsible for getting Michael his first NeXTcube
in January 1989. It was part of the initial California State University sys-
tem-wide purchase of 44 NeXTcubes. Bruce Blumberg and Randy Nelson
put up with Michael and Simson (respectively) at (separate) NeXT devel-
oper camps. Simson’s stay was arranged by Jeanne Etcheverry. Jayson
Adams provided technical support and gave us programs that we could tear
apart to see how they worked. David Spitzler provided moral support. Alan
Belanger, Matt Black, Alison Bomar, Dave Bradley, Henry Chiu, Janet
Leimer, Yvette Perry, John Powell, Lorraine Rapp, Chuck Schneebeck,
Stein Tumert, and Dennis Volper were of great help to Michael at CSULB.
xviii Preface
Claude Anderson “beta tested” the book with his SE 103 class at Rose Hul-
man University during the spring of 1992. Claude found many, many typos
and problems with our example programs, and we are indebted to him for
his time and effort. Other early copies of this book were reviewed by Ali
Ozer at NeXT, Eric Bergeson at Objective Technologies, Inc., Charles L.
Perkins at The Cube Route, Inc., Suzanne Woolf Strauss at the USC Infor-
mation Sciences Institute, William Ballew of Aerospace Corp., and John
Glover at the University of Houston. All of these reviewers made signifi-
cant contributions to the clarity of this book.
Authors’ Biographies
Simson L. Garfinkel is Senior Editor at NeXTWORLD Magazine, an IDG
publication dedicated to following the world of NeXTSTEP computers. He
is also president of Simson Garfinkel and Associates, a Cambridge-based
company that develops NeXTSTEP applications.
Simson L. Garfinkel
52 1/2 Pleasant St.
Cambridge, MA 02139
Authors’ Biographies xix
Michael K. Mahoney
Dept. of Computer Engineering and Computer Science
California State University, Long Beach
Long Beach, CA 90840-8302
This book may be ordered by calling (800) 777-4643, which can be easily
remembered as (800) SPRINGER.
Corrections and comments concerning this book may be sent to the follow-
ing e-mail address:
nextbook@csulb.edu
xx Preface
xv
Contents
Preface i
What is NeXTSTEP? ii
What You Need iii
Programming with NeXTSTEP 2.1 iv
Programming with NeXTSTEP 3.0 v
Additional Programs v
The Organization of this Book vi
Part 1 vi
Part 2 vi
Part 3 vii
Part 4 viii
Part 5 ix
Appendices ix
Floppy Disk ix
Conventions Used in this Book x
Acknowledgments x
Authors’ Biographies xii
Window Types 8
Standard Windows 8
Panels 9
Menus 11
Pop-Up Lists 11
Pull-Down Lists 12
Miniwindows 12
Freestanding and Docked Icons 13
Main and Key Windows 14
Window Order 15
Controls 16
Buttons 17
Menu Commands 17
Text Fields 17
Sliders and Scrollers 17
Browsers and Selection Lists 18
Color Wells 18
The Workspace Manager Application 19
The File Viewer 19
The Workspace Menu Structure 21
Workspace Manager Panels 23
Configuring Your Workspace, Step by Step 23
Menu Guidelines and Keyboard Alternatives 29
Main Menu 29
Info Submenu 30
Document (or File) Submenu 30
Edit Submenu 31
Find Submenu 31
Format Submenu 31
Font Submenu 32
Windows Submenu 32
Services Submenu 33
Working with the File System, Step by Step 33
New Features in the NeXTSTEP 3.0 Interface 43
Workspace Manager 3.0 Menus: New Commands 43
Workspace Manager 3.0 Panels: New Features 44
NextApps, NextDeveloper/Apps Folders in NeXTSTEP 3.0 44
Preferences Application: New Features 46
Summary 46
Contents xix
4 Creating an Application
Without Interface Builder 87
xx Contents
5 Building a Project:
A Four Function Calculator 121
Getting Started Building the Calculator Project 122
Building a Project in NeXTSTEP 2.1 124
Building the Calculator’s User Interface 126
Customizing the Main Window 126
Adding Controls in a Window 128
Matrix Dragging Options 130
Building the Calculator's Controller Class 131
Designing the Controller Class 131
Creating the Controller Class 132
Outlets and Connections 133
Adding Outlets to an Object 134
Making the Connection 134
Contents xxi
15 Draw Yourself:
All About NeXTSTEP Views 381
The Advantages of View’s drawSelf:: Method 382
BlackView: A View That Paints Itself Black 382
A Closer Look at the View Class 385
View Coordinate Systems 385
xxvi Contents
16 GraphPaper: A Multi-Threaded
Application with a Display List 407
GraphPaper’s Design 408
The Interface 408
Connecting to the Back End 408
Why Use a Display List? 410
Working with Multiple Threads 410
The Difficulty of Using Threads 411
C Threads 413
Using C Threads 413
Threads and Objective-C 414
Building the GraphPaper Application 415
Changes to the Evaluator Back End 415
Building GraphPaper’s Interface 416
The GraphView Class Interface File 419
The GraphView Class Implementation File 422
The Data Stuffer Methods 423
Stopping a Running Graph 425
The Data Stuffer Function 425
Constructing the Graph 427
The drawSelf:: Method 431
Contents xxvii
17 Color 449
Colors and Color Objects 449
Colors From a Programmer’s Point of View 450
Colors From a User’s Point of View 452
Programming with Colors 454
Adding Color to GraphPaper 454
Creating a Preferences Panel 455
The Controller Class 456
Creating the Preferences Nib, Panel, and Controller 458
PrefController Class Implementation 459
ColorGraphView 461
Adding Color to GraphPaper 461
Changes to the GraphView Class 463
The ColorGraphView Class Implementation 463
Setting the Colors 464
Setting the Initial Color 466
Changes to the Segment and Label Classes 466
Testing GraphPaper’s Color 468
Summary 468
Process.h 580
Process.m 581
RTF.h 583
RTF.m 583
GraphPaper Application 586
Controller.h 586
Controller.m 587
GraphPaper_main.m 593
GraphView.h 594
GraphView.m 595
Label.h 602
Label.m 603
PrefController.h 605
PrefController.m 605
Process.h 608
Process.m 609
Segment.h 611
Segment.m 612
TrackingGraphView.h 614
TrackingGraphView.m 615
ZoomScrollView.h 618
ZoomScrollView.m 618
Index 625
1
Introduction to the NeXTSTEP
Graphical User Interface
This chapter contains an introduction to the NeXTSTEP GUI and its guide-
lines. No previous experience with NeXTSTEP is assumed. All screen
shots will be taken from NeXTSTEP 3.0, but most of the discussion applies
to NeXTSTEP 2.1 as well. When there are significant differences between
the two NeXTSTEP releases, we’ll point them out. You can use this chap-
ter (and this book) regardless of which release you are using.
1
2 Introduction to the NeXTSTEP Graphical User Interface
Workspace Manager, a program which lets you manage what goes on in the
workspace, or screen environment. The Workspace Manager is like the
Macintosh Finder or Microsoft Windows’ Program and File Managers in
that you use it to start up programs and manage the file system. Unlike
these programs, however, the Workspace Manager enables you to harness
the power of a much more powerful underlying engine, and it manages to
do this without being difficult to use. For example, using the Workspace
Manager, a user could copy 10 MB of files from one disk to another, launch
(run) several programs, open and print an 80 page document, recursively
change the permissions on files, and view a graphics file in a panel, all at
the same time!
at the bottom left of the screen. (A panel is a special type of window which
gives information about or instructions to an application.) The Inspector
panel contains a matrix of switches to set file permissions, and buttons
labeled Revert and OK to cancel or activate permission choices. We’ll dis-
cuss these screen objects in more detail later in this chapter.
The Application Dock at the right of Figure 1 contains 13 icons, the first 12
representing applications (programs) and the last representing the Recycler.
The NeXT icon, which represents the Workspace Manager application, is
always at the top of the dock and doubles as a handle. You can grab the
NeXT icon to drag the dock downward to create more screen real estate or
back upward to see more of the dock.
The Recycler icon at the bottom of the dock represents a folder (directory)
where files can be stored for deletion or restoration later. It works like the
Macintosh Trash Can – files are deleted only when you choose the Empty
4 Introduction to the NeXTSTEP Graphical User Interface
Recycler command from the Workspace File submenu. The little ball
inside the recycler symbol means that the recycler has files inside it, wait-
ing to be emptied.
The text windows with white backgrounds and the two icons at the bottom
middle of the screen in Figure 1 belong to the Edit application. The titled
icon on the left is an Edit miniwindow, which represents an Edit text win-
dow that was miniaturized. The icon next to it is Edit’s freestanding appli-
cation icon. Larger copies of these two icons are at the left of this
paragraph. Although the Edit application is running, it is not active in the
sense that it’s not the application in the foreground (the Workspace Man-
ager application is) and Edit’s main menu is not displayed. You can activate
the Edit application by clicking in one of Edit’s text windows or double-
clicking one of the Edit icons. If the Edit application icon was in the dock
then it would not appear as a freestanding icon.
With NeXTSTEP, the primary instrument that you use to interact with the
computer is the mouse. Compared with the computer’s keyboard, the
The Mouse 5
mouse seems more natural for most users because using the mouse is a bet-
ter analogy for how we interact with objects in the real world. For example,
if a window in the workspace represents a piece of paper on a desk, then it
is more natural for a user to move that window by dragging the window to
a new place on the screen than by typing a sequence of keyboard com-
mands. On the other hand, you may wish to learn some of the common
keyboard alternatives (such as Command-x for Cut and Command-v for
Paste) and use them to increase your efficiency.
The Mouse
There are two basic things that you can do with a mouse: move it and click
its buttons. From these basic actions, several different mouse events
(actions) can be derived:
• clicking – pressing and releasing a mouse button (mouse down and
mouse up) without changing the position of the mouse
• multiple-clicking – pressing and releasing a mouse button two or three
times quickly without changing the position of the mouse
• dragging – pressing and holding down a mouse button and then mov-
ing the mouse (and thus the cursor); release the mouse button to end
dragging
• pressing – pressing and holding down a mouse button in place; release
the mouse button to end pressing
Although the NeXTSTEP mouse has two buttons, they initially work the
same way. Through NeXTSTEP’s Preferences application it is possible to
make one of the mouse buttons bring up the main menu for the active
application.
Cursors
The NeXTSTEP cursor is a graphics image 16 pixels square which moves
with the mouse. Moving the mouse quickly will move the cursor farther
than moving it slowly – even if the distance moved is the same. Picking up
the mouse and placing it elsewhere does not change the position of the cur-
sor.
There are many different shapes the cursor can take depending on the con-
text. The cursor can change in response to entering or exiting a window or
graphics area and in response to tool or target selection. The most common
cursors are shown below. The hot spot, or the location of the screen
referred to by the cursor, depends on the type of cursor currently displayed.
Arrow – the most common cursor; for selecting, clicking, etc.; the hot spot
is at the tip of the arrow. For certain operations (e.g., moving a file in the
NeXTSTEP 3.0 Workspace) the interior of the arrow will turn white ( ).
I-bar – for text input positioning, editing, etc.; the hot spot is at the middle.
Pencil – for drawing lines in a graphics editor or other such program; the
hot spot is at the tip.
Window Types
Other than the cursor, everything you see on the screen, including icons,
menus and panels are windows. On-screen objects fall into seven principal
categories:
• Standard Windows
• Panels
• Menus
• Pop-up Lists
• Pull-down Lists
• Miniwindows
• Freestanding and Docked Icons
Standard Windows
A standard window is the main working area of an application. The win-
dow containing the file being edited in a word processor or the image being
manipulated in a drawing program is a standard window. Most standard
windows, like the Edit text windows in Figure 1 and the window in
Figure 2 below, will have a resize bar and both miniaturize and close but-
tons. They will usually contain vertical or horizontal scrollers when the
window contents are too large to fit in the window. An application can have
many standard windows open at the same time.
Window Types 9
Contents
Scroll knob
Scroll
button
Resize bar
Panels
A panel supports the work done in the main window of an application by
providing information to the user or a vehicle for the user to give instruc-
tions to the application. Panels fall into two groups, attention panels and
control panels.
An attention panel requires a user response before work can be done in any
other window of the application and will disappear after serving its pur-
pose. Attention panels may be brought up as a result of a menu command
(e.g., Print), or as a warning to give the user a chance to take corrective
steps (e.g., a panel that interrupts a close window command to allow the
user to save an altered file). A NeXTSTEP attention panel should appear in
the middle of the screen, in front of any other window that might be
present, and remain on-screen until it has served its purpose, even if
another application becomes active.
large type
identifying icon
cancel button
default action
an attention panel in the Edit application
title
close button
Menus
A menu contains a vertical list of commands and submenus, or menu cells,
which can be chosen with the click of a mouse button. An arrowhead at the
right of a menu cell indicates a submenu whereas a character indicates a
keyboard alternative, or key equivalent, to the mouse. Key alternatives are
used in combination with one of the Command keys at the bottom of the
keyboard. Menu commands which bring up panels are followed by three
dots. Grayed-out (or dimmed) menu commands are disabled in the applica-
tion’s current context. Submenus may be “torn off” and placed anywhere
on the screen. Menus and submenus should float on top of all other win-
dows (except attention panels) and be visible only when the associated
application is active. In NeXTSTEP 3.0, the locations of the main menu
and torn-off submenus are automatically remembered between application
launches. See Figure 4 below for examples of menus.
Pop-Up Lists
A pop-up list is a menu-like list that appears on top of a button when the
button is pressed. It is used in a window or panel instead of a series of
mutually exclusive switches to save space. Unlike menu commands, pop-
up-list commands should set a state rather than initiate an action. To select
a command from a pop-up-list, a user would press the pop-up list button,
drag to the desired command and finally release the mouse button. The
chosen command remains atop the pop-up-list button representing the cho-
sen state. Pop-up lists are sometimes given numeric keyboard alternatives
(e.g. Command-1). See Figure 5 below for an example of a pop-up list.
12 Introduction to the NeXTSTEP Graphical User Interface
Pull-Down Lists
A pull-down list is a menu-like list that appears below a button when the
button is pressed. Commands in a pull-down list should initiate actions
much like menu commands. A command is selected from a pull-down list
in the same way as from a pop-up list. Unlike a pop-up list, the title on a
pull-down list doesn’t change. Pull-down lists aren’t very common because
submenus are usually more appropriate. See Figure 6 below for an example
of a pull-down list.
Pop-up and pull-down lists are little windows which float on top of other
windows. However, they act more like controls because they prompt an
action when one of their commands is chosen. We’ll discuss controls in the
section titled “Controls” on page 16.
Miniwindows
A miniwindow is a small (icon size) titled window representing a window
that’s been miniaturized. To expand the window to its original size and
position, double-click the miniwindow. See Figure 7 below for an example
of a window and its associated miniwindow, and Figure 1, “NeXTSTEP
3.0 Screen,” on page 3 for an example of a miniwindow in the workspace.
Window Types 13
miniwindow represent-
ing the standard win-
a standard window before being minia-
dow at the left;
turized; miniaturize it by clicking
the button at the upper left double-click it to make it
normal size
the Edit icon as it would appear the Edit icon as it would appear
freestanding or in the dock in the dock when Edit is not
when Edit is running running; note the three dots
14 Introduction to the NeXTSTEP Graphical User Interface
For example, if you are editing a file and choose the Print menu command,
a Print panel will appear. The Print panel will temporarily become the key
window and let you choose how many copies to print from the keyboard.
The document window would still be the main window but would not
accept key strokes.
Standard windows which are neither key nor main windows have light gray
title bars. An application can only have one key and one main window at
any given instant. The following table lists the three possibilities for a stan-
dard window:
The following table lists the two possibilities for a panel, which can never
be the main window and thus cannot have a dark gray title bar:
Figure 9 contains a partial screen shot of two standard windows and the
Print panel from the NeXTSTEP 3.0 Edit application. The Print panel is the
Window Types 15
key window (black title bar) and will respond to any keyboard actions (e.g.,
if a user types the “4” key then the selected number of copies, “1” in
Figure 9, will change to “4”). The standard window titled “Window_1” is
the main window (dark gray title bar) and was also the key window before
the Print menu command was chosen. The standard window titled
“Window_2” is neither the key nor the main window (light gray title bar).
Window Order
When using NeXTSTEP on a large display there are often 30 or more win-
dows on-screen (recall that icons, menus, and panels are all special types of
windows). Without a clear window ordering scheme a user’s screen would
often be in chaos and the GUI would lose much of its ease of use. For
example, suppose a new user had spent hours writing a document within an
application without saving his work and an attention panel for that applica-
tion demanded his action before he could save the document. If the atten-
tion panel was completely hidden by other windows then the user might
think he had a hung application, resign himself to losing hours of work and
kill the application (or worse, reboot). If the attention panel was front and
center then this probably wouldn’t happen. As another example, suppose a
user couldn’t find a menu for an application you wrote because it was hid-
den under several windows. The user wouldn’t be very productive if she
had to find the menu whenever she needed it and wouldn’t have a great
desire to use your application again.
16 Introduction to the NeXTSTEP Graphical User Interface
Controls
NeXTSTEP controls are on-screen objects which perform like physical
control devices we use every day. Consider the example of a car stereo sys-
tem that has an on-off switch with indicator light, a row of buttons to select
a radio station, a sliding knob to set volume, and a push button for ejecting
a tape. Each of these devices is a control device with a different functional-
ity. The on-off switch is a toggle, the radio buttons allow a choice of one
out of many, the slider sets a level or value, and the push button forces an
action. All of these physical control devices have analogous on-screen con-
trols in NeXTSTEP.
There are seven standard control types in the NeXTSTEP user interface:
• Buttons
• Menu Commands
• Text fields
• Sliders
• Scrollers
• Browsers and Selection Lists
Controls 17
• Color Wells
Buttons
On-screen buttons fall into two main groups: action buttons and two-state
buttons. An action button performs a single task such as opening a panel,
saving a file, or closing a window. A two-state button sets a single feature
or attribute on or off, such as whether or not text symbols should be shown
in a document or borders should be shown around graphics objects in an
image. In the stereo system analogy the eject button is an action button
while the on-off switch is a two-state button. The set of car radio buttons is
analogous to a matrix (group) of two-state buttons, each indicating whether
the associated radio station is selected or not. There is more structure to this
matrix of radio buttons, however, because only one of the two-state buttons
may be selected at any one time.
Menu Commands
Menus are hierarchically arranged sets of buttons. Pop-up and pull-down
lists are special kinds of menus. Menus are discussed extensively in “Menu
Guidelines and Keyboard Alternatives” on page 29.
Text Fields
A text field is a rectangular area which can display a single line of text. The
text is usually editable, so the user can change it, and selectable, so the user
can drag across it or multiple-click it for subsequent cut, copy, and paste
operations. Text fields are often arranged in groups where the Tab key can
move the selection from one text field to the next. When the user types
some text and then hits the Return key, the text field usually makes some-
thing happen; typically the text is read and some action is performed with it
(e.g., a file is saved under a name typed into a text field in a Save panel).
The size of a scroll knob within the scroller well indicates the relative size
of what you see compared with the total area. Thus in Figure 10 we see
18 Introduction to the NeXTSTEP Graphical User Interface
slightly less than half of the icons at the top of the Preferences window.
Scrollers often contain scroll buttons for slow, consistent scrolling through
an area. See the standard window in Figure 2 for a vertical scroller with
scroll buttons.
scroller
sliders
the size of the scroll knob indicates that less than half
the icons above it are visible
the size of a slider knob doesn’t change
Color Wells
A color well is used to select and manipulate colors. If you press the mouse
down inside the color area of a color well (the white area in the color well
at the left) and drag outward, you will drag out a chip of “color” which can
The Workspace Manager Application 19
FIGURE 12. File Viewer with Icon View of the /NextApps Folder
Shelf
Icon path
Browser
(icon view)
A File Viewer is made up of three major parts, the shelf, the icon path, and
the browser (so named even though it might display an Icon or Listing
view). The shelf is the area below a File Viewer’s title bar which stores
icons representing any type of folder or file. The icon in the upper left cor-
ner of the shelf you see on login always represents your home folder, nor-
mally a house icon. To place a file or folder icon on the shelf, simply drag it
from the icon path or browser and release it in the shelf area. To take a file
or folder icon off your shelf, drag it off and drop it in the workspace.
The Workspace Manager Application 21
You should use the shelf mainly to store icons representing folders and
document files you access frequently. (The shelf can also store file icons
representing applications, but the dock is usually a better place because it
cannot store any other type of icon.) A single click on any folder icon on
the shelf opens that folder in the browser. Thus folders you access regularly
are only a click away.
Double-clicking any document file icon on the shelf will launch (or acti-
vate) the associated application and open that file within the application.
Thus document files you need to edit or view regularly are only a double-
click away.
The shelf is also a handy place to temporarily put files or folders when
moving them from one folder to another. We’ll show you how to do this
and also how to make your shelf resizable in the step-by-step exercises
later in this chapter.
The icon path in the middle of a File Viewer shows an ordered sequence of
icons which represents the path from the root folder to the current working
folder, whose files and folders are shown in the browser. If there are too
many folders to fit in the icon path, then a scroller allowing access to all the
icons appears just below the icon path.
torn off submenu so that the submenu may be closed when it’s no longer
needed. Figure 13 shows the hierarchical nature of the submenus for the
NeXTSTEP 3.0 Workspace Manager application (NeXTSTEP 2.1 is very
similar). This hierarchy is typical of NeXTSTEP applications.
If any applications are running, you must quit them before they can be
dragged out of your dock. Running applications are those whose appli-
cation icons do not contain 3 dots at the lower left corner. To quit a run-
ning application, double-click its icon and choose Quit from its main
menu. Don’t quit the Workspace application – that logs you out!
5. Select the /NextApps folder (directory) in your File Viewer by
clicking the leftmost folder icon (probably a NeXT display monitor) in
its icon path and selecting NextApps in the browser.
6. Add the Preferences application to your dock by selecting it in the
browser, dragging its icon from your icon path and dropping it just
below the NeXT icon in your dock.
7. Add the Mail, Terminal, Edit, Librarian, and Webster applications
to your dock in a similar fashion. Only one application at a time can
be dropped into the dock. If your machine isn’t connected to a network
you’ll probably want to remove Mail later.
8. Select the /NextDeveloper/Demos folder in your File Viewer by
selecting NextDeveloper and then Demos in your browser.
9. Add Billiards or any other interesting applications you like to your
dock as was done above. There are so many demos that you’ll have to
drag the vertical scroll knob downward to see them all. You may also
want to add applications from /NextDeveloper/Apps and /LocalApps
to your dock. /NextDeveloper/Apps is the folder where some (most in
NeXTSTEP 3.0) development tools are located. /LocalApps is the
folder where third party applications are usually installed.
10. Drag the NeXT icon at the top of your dock down as far as it will
go and then drag it back up to the top of the screen. The NeXT icon
remains visible so you can always retrieve your dock and activate the
Workspace application. Note that you can add “real estate” to your
screen working area when necessary.
Some application icons change to indicate something. For example, the
running Preferences icon is a clock and the running Mail icon shows a
sheaf of letters to indicate that you have new mail. You may wish to
have these icons near the top of your dock so you can drag part of the
dock below the screen and still see those “indicator” icons.
11. Experiment with the different ways of displaying files in your File
Viewer by choosing View→Icon and View→Listing from the
Workspace menu. Sort Icons only works in Icon View mode, which is
why it may be grayed out.
12. Put your File Viewer back in Browser mode.
Configuring Your Workspace, Step by Step 25
1. Many workstations that use the X Windows window system use point-to-focus
(or mouse-to-focus) rather than click-to-focus. With point-to-focus, windows
become active when you move the cursor on top of them. Point-to-focus can result
in commands being accidentally sent to the wrong application if your mouse is
bumped. This is one of the reasons that NeXTSTEP doesn’t use this interaction sys-
tem. On the other hand, some people prefer point-to-focus because it requires less
clicking.
26 Introduction to the NeXTSTEP Graphical User Interface
Preferences
application icon
(while running)
28 Introduction to the NeXTSTEP Graphical User Interface
31. Choose an application font (one which shows up in your File Viewer
and other windows on subsequent login) by clicking the NeXTcube
icon button in the Preferences window, clicking the Set Font (or Font
Panel) button and then selecting a font from the Font Panel (finish by
clicking the Set button in the Font panel). Choose a size in the range
10-14. Close the Font Panel by clicking its close box.
32. Choose a system beep by selecting a sound from the list of choices in
the box titled “System Beep”.
33. Choose a clock style by clicking the clock icon button atop the
Preferences window and then the clock button in the middle
repeatedly until your favorite clock style appears. See Figure 15.
You can change the time by clicking the arrow buttons and the time
zone by dragging on the vertical white time line.1
34. Choose a default menu position for all applications by clicking the
menu icon button atop the Preferences window (you may have to first
drag the scroll knob below the icons to the right) and then dragging
the little menu in the Menu Location box. The best menu position for
most people is at the top left of the screen.
35. Force your File Viewer and other file windows to display UNIX
system files by clicking the “UNIX” button atop the Preferences
window (drag the scroll knob, if necessary) and then clicking the
“UNIX Expert” switch.
Dot files (those whose names begin with a “.”) and files in the /bin,
/etc, and other system folders will immediately show up in your File
Viewers, Open and Save Panels, etc. (Being a “UNIX Expert” in
NeXTSTEP 2.1 also enables you to create UNIX Shell windows from
the Workspace Tools submenu on subsequent login.)
36. Make your new clock style choice show up in the dock by choosing
Hide from Preferences’ main menu. Note that the Workspace Manager
automatically becomes the active application (since it was the active
application just before Preferences was activated).
37. See all of your new system preferences take effect by logging out
and logging back in. Note the applications which auto-launch in your
dock and the new text style in your File Viewer.
1. Often networked workstations are configured so that only the superuser can
change the time.
Menu Guidelines and Keyboard Alternatives 29
38. “Tear off” the Workspace Tools submenu by choosing Tools from
the Workspace main menu and then dragging the Tools submenu title
bar downward about an inch. Note the new close button in the title bar.
39. Again choose the Tools submenu item from the Workspace main
menu and note that the attached submenu won’t stay up (because the
Tools submenu has been torn off).
40. Log out (or type Command-q).
Main Menu
The main menu for an application should always display the application’s
(possibly abbreviated) name in the title bar and contain Info, Services,
Hide and Quit commands. The other commands in the menu on the left are
30 Introduction to the NeXTSTEP Graphical User Interface
Info Submenu
Info submenu commands should allow users to get information about the
application as a whole, and set preferences for the application as a whole.
Additional commands, such as those to bring up a License or Copyright
panel, should be added immediately after the Info Panel command. The
Show Menus command will show all the menus of the application, but is
not implemented in NeXTSTEP release 3.0 or earlier. Help is the only
command in the Info submenu with a key alternative because it’s the only
command used often enough to warrant one. The Info Panel command
should never have a keyboard alternative.
will both save the contents of the main window to a file with a new name,
but only Save As will change the name of the file in the main window
itself.
Edit Submenu
Edit submenu commands can be used to manipulate text, graphics and
other objects in the key window. Their keyboard alternatives are perhaps
the most worthwhile to learn because they are used often in a variety of
places. They can be used in most text and graphics areas in any main win-
dow or panel which is the key window. Edit submenu key alternatives are
very convenient, especially for right-handed people, because they can be
performed easily with your left hand while your right hand is on the mouse.
Occasionally the Find command is promoted from the Edit submenu to the
main menu.
Find Submenu
Find submenu commands allow easy access to Find panel choices such as
finding a character string and finding the next or previous appearance of
the same string. The Enter Selection command enters the current selected
text into the Find panel’s Find field for subsequent searches.
Format Submenu
Format submenu commands affect the layout of text and graphics docu-
ments. Usually the Font command is a choice in the Format submenu, but
it can be promoted to the main menu. The only common keyboard alterna-
tive is for Page Layout, which brings up a standard panel with portrait,
landscape, page size, and other choices.
Font Submenu
Font submenu commands such as Bold and Italic affect one aspect of text
font. There are several other common Font submenu commands which
affect font size and style but are not shown here. You can find out what
these common commands are by exploring Edit and Interface Builder. The
Font Panel command brings up a standard panel with font family, type-
face, and size choices.
Keyboard Alternative Menu Command
Command-t Font Panel
Command-b Bold
Command-i Italic
Windows Submenu
Windows submenu commands apply to windows within the active applica-
tion. The Miniaturize and Close Window commands apply only to the key
window (which might be a panel). The Arrange in Front command will
arrange all main windows for the active application front and center in a
cascaded fashion. The Window_1 and Window_2 commands in the sub-
menu at the left were added dynamically as main windows for the applica-
tion were opened. Choosing one of them will make that window the key
window and bring it to the front.
The particular Windows menu shown above belongs to the Edit applica-
tion we saw in Figure 9, “Main, Key, and Standard Windows,” on page 15.
The contents of Window_2 had been saved, but the contents of Window_1
had not, as indicated by the symbols on the left of the menu cells and the
close buttons in Figure 9. The number of items in the Windows submenu
Working with the File System, Step by Step 33
grows and shrinks dynamically as main windows are opened and closed in
an application.
Services Submenu
Services submenu commands allow for communication between different
programs. Most services take the selected text or object in the key window
and perform some sort of function. For example, if you select a word and
then use the Librarian service Search, the word will be searched in NeXT-
STEP’s Digital Librarian application.
6. Make your stuff folder easily accessible by dragging its icon from
your icon path and dropping it on your shelf. Whenever you need to
access the stuff folder, all you need to do is click its icon on the shelf. It
works much like a radio button in your car.
7. Open the /NextLibrary/Sounds folder in your File Viewer by simply
entering “/NextLibrary/Sounds” (the Workspace Finder panel
automatically opens) on the keyboard. While you are typing the Finder
is the key window (black title bar) and the File Viewer is the main
window (dark gray title bar). See Figure 16 below.
In NeXTSTEP 3.0 it’s possible that a separate folder window opened in
this step; it depends on how the Finder option in the Preferences panel
is set. If this occurred, then close the folder window after Step 15
below is completed.
UNIX file names are case sensitive – an uppercase “A” is a different
character than a lowercase “a” –¼ so be sure to use the correct capital-
ization. Also, use forward slashes as separators, and do not type the
quotes.
8. Select the Funk.snd file by clicking the file name “Funk.snd” in the
browser. Note the Funk.snd icon in the icon path.
9. Without releasing the mouse button, drag the Funk.snd icon from the
icon path so it’s on top of the stuff folder icon on your shelf.
Working with the File System, Step by Step 35
Note that the folder icon “opens” and the cursor changes to a little two-
page ( ) icon. This indicates that a copy operation is about to take
place.
10. Now drop (i.e., release the mouse button) the Funk.snd icon atop
the stuff folder icon and note that “Copying...” appears at the lower
right corner of your shelf.
While they are being executed in the background, copy, move, and
other file operations are indicated by small text in the lower right cor-
ner of your shelf. These operations are in the background in the sense
that you may request that other Workspace operations be performed
simultaneously.
The small text in the lower left corner of your shelf indicates the
amount of free space remaining on the disk containing the folder.
11. Again drag the Funk.snd icon from the icon path and drop it atop
the stuff folder icon on your shelf. Your chosen system beep sounds
and the Workspace Processes panel opens to alert you that the file
Funk.snd already exists in your stuff folder. See Figure 17 below and
note the two-page ( ) icon in the Processes panel to the left of the file
being copied.
12. Stop the copy operation by clicking the Stop button in the Processes
panel.
13. Move the Processes panel to a relatively unused area of the screen,
say just below the Workspace main menu. Do not close the Processes
panel as we’ll use it later.
14. Hear the contents of the Funk.snd file by double-clicking its icon in
your icon path. The Sound (SoundPlayer in NeXTSTEP 2.1)
application in /NextDeveloper/Demos automatically launches. Play
the sound by clicking the Play button.
15. Quit the active Sound (or SoundPlayer) application by choosing
Quit at the bottom of its menu. Note that the previous active
application, namely the Workspace Manager, automatically becomes
active.
16. Discover which applications are running in the Workspace by
pressing on the Background pop-up list button and dragging to
Applications in the Processes panel. (If it’s not displayed, you can
display the Processes panel by choosing the Tools→Processes
command.) The applications listed as running in your Processes panel
probably differ from those in Figure 17.
36 Introduction to the NeXTSTEP Graphical User Interface
18. Create another new folder called “junk” under your home folder
by typing Command-n (the key alternative for File→New Folder), and
renaming “NewFolder” as in Steps 4-5 above. To type Command-n
hold down one of the Command keys while typing the “n” key.
19. Make your junk folder easily accessible by dragging its icon from
your icon path and dropping it on your shelf.
20. Open the stuff folder by clicking its icon on your shelf.
21. Select the file Funk.snd in your browser (use a single-click).
22. Make a copy of the Funk.snd file in the stuff folder by choosing
File→Duplicate from the Workspace menu (or type Command-d).
Note that “Duplicating...” appears at the lower right corner of your
shelf and you soon have a new file named “CopyOfFunk.snd” in your
browser.
23. Without releasing the mouse button, drag the Funk.snd icon from
the icon path so it’s atop the junk folder icon on your shelf.
The two-page cursor does not appear this time, instead the cursor
move remains an arrow (and turns white in NeXTSTEP 3.0). The arrow indi-
cursor cates that the file Funk.snd will be moved to the junk folder, not cop-
in 3.0 ied.
24. Release the mouse button and check the contents of the stuff and
junk folders. Note that the Funk.snd file appears only in the junk
folder. “Moving...” would have appeared at the bottom right of your
shelf if the move operation took longer.
Move If you are the owner of both folders and they reside on the same physi-
vs. cal disk, then the file in the drag-and-drop operations above is moved,
Copy not copied. Otherwise, the file is copied. This behavior also applies
when you drag and drop multiple files or folders.
25. Open the junk folder by selecting its icon in your shelf.
26. Select the Funk.snd file in your browser.
27. Without releasing the mouse button, drag the Funk.snd icon from
your icon path atop each of the icons in your shelf and your icon
path. Some of the folder icons (e.g., your home icon) change to
“open” icons indicating that you have permission to move or copy files
into those folders.
38 Introduction to the NeXTSTEP Graphical User Interface
28. Keeping the mouse button pressed, drag the Funk.snd icon atop
the stuff folder icon on your shelf and press an Alternate key. Note
that the two-page cursor appears. Release the mouse button while
pressing the Alternate key and note that the Funk.snd file is copied
into the stuff folder.
You have forced a copy by using the Alt key.
29. Open the junk folder “as a folder” by first clicking the junk icon in
your icon path and then choosing File→Open as Folder from the
Workspace menu (or type Command-O).
The new folder window that opens functions the same as a File Viewer
except that it can only access files located under the junk folder.
30. Close the junk folder window by clicking its close button.
Note that a file called “.dir3_0.wmd” (“.dir.wmd” in NeXTSTEP 2.1)
is automatically saved in the junk folder. This file contains informa-
tion about the folder window such as its location, size, and shelf icons.
This information will be used the next time you open junk as a folder.
(If the “.dir3_0.wmd” doesn’t show up, then type Command-u (or
choose to update the File Viewer. If it still doesn’t show up, then you
didn’t turn on the UNIX Expert switch in the Preferences application.)
31. Use the Workspace Finder together with file name completion to
open the /NextDeveloper/Examples/PostScript folder as follows:
(a) type “/NextD” and then the Escape (Esc) key
(b) type “/E” and then the Escape key again,
(c) type “/Po” and then the Escape key one more time.
(d) hit the Return key
Note how the folder names are completed when the Escape key is
typed. Partially typed folder (or file) names must be unique to be com-
pleted. The system beep will alert you if the name isn’t completed.
32. Select multiple files in your browser by pressing the mouse button
down on “Arrows.eps” and dragging downward across five file
names. When you release the mouse button a hand-of-cards icon
representing the files appears in your icon path. (In NeXTSTEP 2.1,
the number of selected files is not displayed beneath the hands-of-
cards icon.)
33. Deselect the CircularText.eps file in the browser by holding down a
Shift key while clicking the name “CircularText.eps”.
This action is known as Shift-clicking. It toggles whether the object
you click is selected or not.
Working with the File System, Step by Step 39
34. Drag the hand-of-cards icon (which now represents only four files)
and drop it on your shelf.
35. Launch the Preview application and see the contents of all selected
“.eps” (Encapsulated PostScript) files simultaneously by simply
double-clicking the hand-of-cards icon in your icon path.
If an application other than Preview was launched then the account
you’re using has been configured so that “.eps” files launch that other
application. This was done in the Workspace Inspector panel which
we’ll discuss later.
36. Hide the application that was launched in the previous step by
typing Command-h. Note how all the windows displaying “.eps” files
“hide” behind the freestanding (or docked) application icon.
37. If you see a freestanding icon near the bottom left of the screen, then
move it around the screen by dragging any part of it. If you can’t see
one, then either there isn’t one (because the application icon is in the
dock) or it may be hidden behind windows which cover the bottom left
of your screen. In the latter case, move the windows.
As with miniwindows, an application icon will not cover the dock or
menus (or attention panels if any were on the screen) once it’s released;
however, it “floats” above other windows.
38. Activate the application by double-clicking its freestanding (or
docked) icon.
39. Move your File Viewer without activating the Workspace Manager
by Alt-dragging its title bar.
Alt-dragging (i.e., holding down an Alternate key while dragging) lets
you rearrange an application’s windows without having to activate the
application.
40. Send the File Viewer to the back (i.e., behind other windows) by
Command-clicking on its title bar (i.e., pressing a Command key while
clicking the mouse on the title bar).
41. Cycle through all the windows and panels on the screen by holding
down a Command key while repeatedly pressing the up (or down)
arrow key. Note how you can find hidden windows in non-active
applications without changing the active application.
42. Quit the application (probably Preview) displaying the “.eps” files.
40 Introduction to the NeXTSTEP Graphical User Interface
43. Copy the files represented by the hand-of-cards icon to your junk
folder by dragging the icon from your shelf and dropping it atop the
junk folder icon which is also on your shelf. Watch the Processes
panel and note that all the files are copied.
You can drag-and-drop icons from the File Viewer’s shelf, icon path,
or browser (in Icon view) to any other area of the File Viewer – or any
other application that accepts drag-and-drop.
44. Open your junk folder by clicking its icon on your shelf.
45. Select your new Compositing.eps and Funk.snd files by clicking
Compositing.eps and then Shift-clicking Funk.snd in your browser.
Shift-clicking lets you to select or deselect files (or folders) when they
aren’t consecutive in a browser listing.
46. Drag the hand-of-cards icon representing the two files and drop it
on the recycler icon at the bottom of your dock. Note that this hand-
of-cards icon looks like the previous one (except the number of items).
Moving, copying or deleting hands-of-cards can be dangerous.
The ball in the middle of the recycler icon indicates that the recycler
contains files.
47. See which files are stored in the recycler by double-clicking the
recycler icon. The Recycler window opens.
48. Recover the “recycled” Funk.snd file by dragging it from the
recycler and dropping it on the junk folder icon on your shelf (or icon
path).
49. Delete the file(s) in the recycler by choosing File→Empty Recycler
from the Workspace menu. Close the Recycler window.
50. Select the Arrows.eps file in your junk folder in your File Viewer.
51. Make a link to the Arrows.eps file in the stuff folder by Control-
dragging the Arrows.eps file icon from your icon path and dropping it
on your stuff folder. Note that the cursor changes to the link cursor
( ) to indicate that a UNIX file link is being made. The Arrows.eps
entry in the stuff folder is a reference to the Arrows.eps file in junk.
52. Inspect the attributes of your junk folder by selecting it in your File
Viewer and choosing Tools→Inspector from the Workspace menu.
See Figure 18 below. If necessary move the Inspector panel so it
doesn’t cover any part of your File Viewer.
53. Determine the size (in bytes) of your junk folder by clicking the
Compute Size button in the Attributes Inspector panel. Note that
“Sizing...” appears at the lower right corner of your shelf.
Working with the File System, Step by Step 41
Copy Options command lets you set what happens when a link is
encountered during file copying. The Animation command from
NeXTSTEP 2.1 has been removed.
• Inspector Panel now has four commands in its pop-up list. The
Access Control command is new but its functionality (changing file
permissions) was available in NeXTSTEP 2.1 via the Attributes and
Contents commands. Other minor changes have been made in this
panel but the functionality is essentially the same as in NeXTSTEP 2.1.
• Finder Panel is more functional because a user may store folders to
search in a bookshelf-like area at the top of the panel. See Figure 16,
“Workspace Finder Panel,” on page 34 for an example.
• Initialize Disk Panel will now format floppy disks in NeXT, DOS, or
Macintosh format. See Figure 21 below.
FIGURE 21. Initialize Disk Panel in NeXTSTEP 3.0
46 Introduction to the NeXTSTEP Graphical User Interface
was in the /NextApps folder in NeXTSTEP 2.1, and Yap (PostScript code
builder and previewer), which was in the NextDeveloper/Demos folder in
NeXTSTEP 2.1, have both been moved to NextDeveloper/Apps in NeXT-
STEP 3.0. HeaderViewer, a valuable new developer’s tool for accessing
on-line information, can be found in the 3.0 NextDeveloper/Demos folder.
The NeXTSTEP 3.0 NextApps folder (see Figure 23 below) contains one
new application, called Grab, which will grab screen shots. It’s similar to
the Grab application in NextDeveloper/Demos from NeXTSTEP 2.1, but
is more stable and can now take “timed” screen shots. Grab was used to
capture all of the graphic images in this text.
Summary
Although there is far more to learn about using NeXTSTEP, this brief intro-
duction is probably enough to get you started. In the next chapter, we will
look at some of the NeXTSTEP developer’s tools. Then in Chapter 3, we’ll
start creating our first program.
48 Introduction to the NeXTSTEP Graphical User Interface
2
NeXTSTEP Development Tools
There are several bundled NeXTSTEP applications which are very useful
when you are writing NeXTSTEP programs. The Terminal application
enables you to work in a UNIX terminal emulator setting and perform
numerous programming related operations. The Edit application provides
an easy way to enter, edit, and debug code files. The (Digital) Librarian
and HeaderViewer are tools which will search through on-line documenta-
tion. IconBuilder (Icon in NeXTSTEP 2.1) provides an easy way to build
application and document icons for your applications. The standard UNIX
make program is used to determine which parts of your program need to be
compiled and calls the compiler. The GNU debugger, gdb, is useful for
debugging your programs and will work together with Edit.
Interface Builder will actually build your application’s user interface from
your (mostly graphical) specifications. It will save your on-screen user
interface design by saving the user interface object specifications in a file.
Project Builder (Interface Builder in NeXTSTEP 2.1) will create skeletal
source code files and specify how they can be compiled together to make a
stand-alone executable file (program). In other words, Interface Builder
together with Project Builder will build an application for you without any
programming whatsoever! Of course, the application won’t have much of a
computational engine (back end) unless you add application-specific code,
47
48 NeXTSTEP Development Tools
but it will have a pretty face and will run. We’ll see how to use Interface
Builder in the next chapter and Project Builder in Chapter 5.
As with Chapter 1 we will write this chapter for users of both NeXTSTEP
3.0 and NeXTSTEP 2.1, and take most screen shots from NeXTSTEP 3.0.
Figure 1 shows a screen shot of a Terminal window running vi, the visual
editor available with every version of UNIX. The UNIX shell in effect
(csh) and the number of text columns and rows is shown in the window’s
title bar (you must set a Terminal preference for the size to appear in
NeXTSTEP 3.0). Terminal windows work like most VT100 terminals and
thus we’ll only discuss the NeXTSTEP improvements here.
When you print, take note of the buttons in the Print panel’s Range box
(Extent box in 2.1), which lets you print all the text output to the window
(which is stored in a buffer), the selected text, or simply what’s visible in
the window.
Perhaps the most useful Terminal submenu is Shell, in the main menu’s
second slot where Document or File usually sits. The name “Shell” makes
sense in the second slot because its New command creates a new shell win-
dow in the Terminal application, just as New creates a new file or docu-
ment in an editor application.
you could move the cursor into the Terminal shell window (which would
temporarily become the key window) and type gdb debugging commands,
thereby “stealing” keystrokes from MyApp.
Next we’ll run you through a few steps so you can become familiar with
the Terminal application. These steps work in the Terminal applications for
both NeXTSTEP 3.0 and NeXTSTEP 2.1.
1. Launch the Terminal application (from your dock or /NextApps). A
UNIX shell window will likely open (if it doesn’t, then choose
Shell→New and check your Terminal preferences). See Figure 3.
Developer Mode
17. Arrange all the directory and file windows front and center by
choosing Windows→Arrange in Front from Edit’s menu. Note that
the LinesView.h miniwindow expands and that the X in the
LinesView.h menu command has a hole, indicating it contains unsaved
changes. The X has the same meaning as it does inside a close button
in a window’s title bar.
18. Undo all editing changes performed in the LinesView.h file since
the last save by choosing File→Revert to Saved in Edit’s menu and
then clicking Revert in the attention panel that is displayed.
19. Bring the LinesView.m file to the front by choosing
Windows→LinesView.m from Edit’s menu.
20. Find the first instance of the “initFrame” string by typing
Command-f (or choosing Edit→Find→Find Panel) and entering
“initFrame” in the Find Panel.
21. Select the entire initFrame: method (procedure) in LinesView.m by
clicking just before the minus (“-”) character in front of initFrame and
then Shift-clicking just after the ending curly brace. See Figure 6
below.
Using Edit and Librarian, Step by Step 55
Shift-clicking selects all of the text from the insertion point to the posi-
tion where the shift-click occurs. Shift-clicking can also be used to
extend or shorten a selection.
22. Select the body of the initFrame: method in the LinesView.m file by
double-clicking the opening curly brace on the line after the
initFrame: header. The block contained between this curly brace and
the matching closing curly brace should be selected.
23. Contract the selected block by choosing
Format→Structure→Contract Sel from Edit’s menu. The white
arrow ( ) indicates contracted code. See Figure 7 below.
24. Expand the contracted blocks by clicking the white arrows
representing the blocks.
25. Search for the initFrame: method in the on-line documentation by
double-clicking initFrame to select it and then choosing
Services→Librarian→Search.
The Librarian application automatically launches (or activates) and a
search for initFrame is made. If none are found, then click the NeXT
Developer icon at the top of the Librarian window and then click the
Search button in the window. See Figure 8 below.
56 NeXTSTEP Development Tools
32. Select line 2 in the LinesView.m file by entering “2” in the Line and
Character Range panel. This feature is very useful for finding where
compiler errors are located because the compiler will tell you the line
numbers where it found compiler errors.
33. Select lines 3-14 in the LinesView.m file by first typing Command-l
to bring up the Line and Character Range panel and then typing 3:14
in the panel followed by Return.
34. Quit Edit.
58 NeXTSTEP Development Tools
HeaderViewer
HeaderViewer is a great new tool in NeXTSTEP 3.0 for finding and view-
ing information in NeXTSTEP toolkits and documentation. It’s better than
Librarian for finding information important for NeXTSTEP development.
HeaderViewer can be found in the /NextDeveloper/Demos directory.
Creating Icons
Icons can be used to identify applications and documents belonging to
applications in the Workspace. These icons are normally 48 x 48 pixels in
size and have an alpha channel (for transparency information). In this sec-
tion we’ll show you how to create an icon. Then in Chapter 6 we’ll show
you how to use Project Builder (or Interface Builder in NeXTSTEP 2.1) to
set up application and document icons for your own applications.
chosen color
four modes
11. Make the pencil width larger by dragging the slider knob in the
Pencil Inspector to about 7 (see Figure 10 above).
12. Draw a “χ” symbol or whatever else you want on top of the square
gray icon. Note that the cursor changes to the pencil ( ) while in the
drawing area.
13. Save the image in your home directory with the name “MyIcon.tiff”
by choosing Document→Save As and entering “~/MyIcon.tiff”.
14. Quit the IconBuilder application.
A target of the make utility is usually an executable file, but there are other
possible targets such as intermediate object code files, and manipulation of
directories. To run make in a UNIX shell window, change to the directory
containing the source files and Makefile and type make target at the com-
mand line. In the following table we list the possible make commands
(with targets) and the corresponding tasks performed:
Command Task
make (optimized) compile and link into an
(or make all) executable file named
make debug compile and link into an executable file named
appname.debug for use with gdb
make clean removes all object and executable files
make install installs the application into a chosen directory,
such as /LocalApps or ~/Apps
gdb – Debugging Programs 63
Command Task
make installsrc installs the source files into a chosen directory
for archiving a completed project
make depend generates a Makefile.dependencies file con-
taining a dependency graph for the project
make diff compares the source of two projects
make profile compile and link into an executable named
appname.profile for use with UNIX gprof
make help lists the targets that can be made
(We’ll use the convention that the commands you type are in boldface.
localhost is the name of the host computer.)
To which you should type the name of the file that you want to debug:
64 NeXTSTEP Development Tools
The advantage to using gdb from Emacs is that Emacs will automatically
split the screen into two windows, giving you a gdb buffer in one and fol-
lowing the program that you are debugging in the other. Many program-
mers find this an effective way to work.
gdb Commands
gdb is a complicated program with dozens of commands. Fortunately, you
only need to know a very few basic commands.
Typically, when you are using gdb, you will set a breakpoint and then run
your program until you reach that breakpoint. Your program will then auto-
matically stop running and you will be free to inspect the contents of vari-
ables.
Once your program reaches a breakpoint (or if your program crashes), you
will return to the gdb command-line. You can also interrupt your program’s
execution by typing Control-c. From the command-line, you may find the
following commands useful in viewing your program’s state.
In this example, gdb called the function printf which printed the line
beginning “this is a test.” The printf function itself then returned the value
0, which gdb printed.
You can also use the print command to send a message to an object:
(gdb) p [NXApp mainWindow]
66 NeXTSTEP Development Tools
$7 = 7176016
(gdb)
In this case, we sent the mainWindow message to the NXApp object. The
returned value was 7176016. In order to understand what this returned
value means, we must know that the mainWindow message returns the id
of a Window object. We could use that id as the source of another mes-
sage. For example, to ask that window what its title is, we could send it the
title message. To see the printable string that is the window’s title, we fur-
ther need to cast the returned value into a char *:
(gdb) p [[NXApp mainWindow] title]
$8 = 3286712
(gdb) p (char *)[[NXApp mainWindow] title]
$9 = (char *) 0x3226b8 "Address Book"
(gdb)
symbol information into gdb, launch the Edit application, and finally add a
new menu item called Gdb to Edit’s menu. All of this occurs by clicking a
single button! Choosing the Gdb menu command in Edit brings up a panel
which provides a graphical interface to most basic gdb commands. This
panel for an application called Calculator can be seen in Figure 12 below.
To properly address user interface design would take another book.1 How-
ever, it’s so important that we’ll list a few tips below for novice designers.2
1. For example, see Designing the User Interface by Ben Shneiderman (Addison-
Wesley) or Software User Interface Design by Deborah Mayhew (Prentice-Hall).
2. Thanks to Dave Grady of NeXT Computer, Inc. for most of these tips .
68 NeXTSTEP Development Tools
• Create the user interface first. Then the user interface will more likely
be written for the user, and not the programmer. There’s nothing worse
than making an interface conform to code. Remember, you are writing
your application for users, not yourself.
• Don’t put too many windows on the screen immediately after your
application is launched. If you do, the user will be confused and may
not even know where the focus of your application lies.
• Don’t violate user’s expectations. In part, this means that you should
follow the NeXTSTEP user interface guidelines for menus, windows,
panels, and so on. As an example of such a violation, look at the loca-
tion of the Print command in the main menu of the NeXTSTEP 2.1
Edit application. It violates the main menu guideline that Print should
be immediately above Services. Another violation is the Send button in
the mailbox windows of the NeXTSTEP 2.1 Mail application. To better
reflect its function, the name of this button has been changed to Com-
pose in NeXTSTEP 3.0.
• Don’t confuse the grouping of functionality. Some applications are rid-
dled with menus and panels which confuse functionality.
• Provide sufficient WYSIWYG before a choice is set. For example, in the
standard NeXTSTEP 2.1 Font panel, the resulting font isn’t displayed
until after the Set button is clicked. The Preview button helps, but why
not make the preview immediate? An example where the preview is
excellent can be found in Lotus Improv’s Format panel. The format of a
spreadsheet cell is clearly displayed before the choice is set (thank
goodness for that, since Improv’s lack of Undo is a serious user inter-
face problem).
Summary
In this chapter, we took a brief look at the NeXTSTEP development tools
Terminal, Edit, Librarian, HeaderViewer, Icon Builder, Icon, make, and
gdb. When using these applications, keep in mind that one can often be
accessed from another through the Services menu.
In the next chapter we’ll take a close look at Interface Builder, NeXT-
STEP’s powerful tool for building application interfaces, and create our
first program.
3
Creating a Simple Application
With Interface Builder
69
70 Creating a Simple Application With Interface Builder
This chapter introduces you to Interface Builder. We’ll build a very simple
interface for an application and test it with Interface Builder’s Test Inter-
face command. We won’t use this interface beyond this chapter; we won’t
even save it in a file. To keep things simple, we also won’t build a complete
project, and thus won’t use Project Builder at all.
Getting Started with Interface Builder 71
Your screen should look similar to the screen shot in Figure 2 below. Inter-
face Builder’s main menu is in the upper-left corner, its Palettes window is
near the upper-right corner of the screen, and its Inspector is below the Pal-
ettes window (you can specify which windows show up at launch time in
Interface Builder’s Preferences panel). Now that we’ve launched Interface
Builder, we can start building our own application.
The screen has now become more interesting with the addition of three
new objects. In the center of the screen in Figure 3 is a big empty window
titled “My Window.” Below Interface Builder’s menu is another menu
titled “UNTITLED.” Don’t try choosing commands from this menu right
now – it’s the main menu for the application that you are building. In sub-
sequent chapters we show how you can tailor this menu with commands
and submenus to suit a particular application. A larger picture of the
UNTITLED menu appears in Figure 4 below.
In the lower-left hand corner of the screen in Figure 3 is the File window
for the new application. (A larger picture of it appears in Figure 4 below.)
The File window and menu are titled “UNTITLED” because you have yet
to give the application a name. (In NeXTSTEP 2.1, the File window’s con-
tents are arranged differently, but are essentially the same.)
Getting Started with Interface Builder 73
The File window’s close button ( ) in its upper right-hand corner has a
hole in its middle because the application being built hasn’t been saved.
The hole means that the contents of the window have been modified, or are
dirty. After you save it, the button will change to this: .
Let’s look at the individual objects in the Objects view for a new applica-
tion, like the one in Figure 4.
The File’s Owner icon represents the main object in charge of running
your application. Normally this object is called “NXApp” and is of the
Application class type. We’ll go into the details of what this object is and
what it does later; for now, just think of it as the thing that’s controlling
your program and providing an interface to the computer’s hardware.
the Edit submenu. The Hide and Quit commands are currently the only
ones that have any effect.
The MyWindow icon represents the application’s main window, the bland
window titled “My Window” that Interface Builder created automatically
when you chose Document→New Application. The File window will
contain an icon for every standard window and panel in your application. If
a window or panel in the application you’re building isn’t visible (perhaps
it was closed to simplify the screen), then you can make it visible by dou-
ble-clicking its icon in the File window.
Next we’ll take a closer look at the three other views – Images, Sounds,
and Classes – in the File window.
The Images suitcase (Icons suitcase in NeXTSTEP 2.1) icon in the File
window represents the icons and other images that are available to the
application. When you click the Images suitcase, Interface Builder will
display the available images in the File window as in Figure 5 below. (In
NeXTSTEP 2.1, a separate Icons window with similar contents opens.)
The Sounds suitcase is similar to the Images suitcase, except that it repre-
sents sounds rather than images. When you click the Sounds suitcase,
Interface Builder will display icons for the available sounds in the File win-
dow as in Figure 6 below. (In NeXTSTEP 2.1, a separate Sounds window
with similar contents opens.)
The Classes suitcase icon in the File window represents all of the Objec-
tive-C classes that your application “knows” about. (Classes define and
create objects such as windows and buttons, as we’ll see in subsequent
chapters.) When you click the Classes suitcase, Interface Builder will dis-
play the hierarchy of available classes in the File window (In NeXTSTEP
2.1, a separate Classes window opens.) Commands available in this win-
dow let you add, modify and perform other operations on classes.
All of the icons in the Objects, Images, and Sounds views in the File win-
dow have their names displayed below them. Some of the names are in
black, while others are in dark gray. You can change a name in black by
double-clicking it and typing a new name. Changing an icon’s name in the
File window has no effect on the rest of the program. For instance, if you
change the name of the icon My Window to Steve’s Window, the window
itself will still be titled My Window. These icon names are generally only
for the convenience of the developer.
Notice that you don’t need to know the exact height and width of this win-
dow to set its size; you simply resize it visually and you’re done. This is a
good example of the basic philosophy of Interface Builder – graphical
things are best done graphically. It is this philosophy that is at the heart of
NeXTSTEP’s ease of programming. (On the other hand, you can resize the
window to precise dimensions using the “Window inspector,” if necessary.)
the top of the Palettes window, you can choose which one of the palettes is
visible. This is called a multi-view window. (The File window is also a
multi-view window.)
buttons
to select
palette
Pop-up
list
TextFields
these palette
Button objects can be
dragged into
Switch your app’s
button windows
Box
Vertical Custom View
slider
Radio Color
button Well
matrix Form
Horizontal
slider
78 Creating a Simple Application With Interface Builder
6. Add a button to your application by dragging the Button icon from the
Palettes window and dropping it into the window titled My Window.
The window should look something like this:
You can resize this new button by dragging any of the little dark gray
squares, or handles, around the button’s perimeter. You can move the but-
ton by pressing in the button’s middle and dragging the button to the
desired location within the window.
7. Resize the button so it’s about twice as wide.
8. Move the button so it’s in the same location as in the screen shot
above.
9. Change the name of the button. To do this, double-click the button’s
title (“Button”) and the button’s text will highlight in light gray to
indicate it’s been selected. You can then type anything you want. For
example, if you type “Noise” (and then hit Return), you’ll change the
button’s title to “Noise.”
Now you’ve got a simple window with a button. Let’s try it out!
10. Choose the Document→Test Interface menu command (or type the
keyboard equivalent Command-r). (In NeXTSTEP 2.1, choose
File→Test Interface.)
All of the Interface Builder windows including the Interface Builder menu
will hide and you’ll be left with your new application’s main window and
menu. It looks as though the application is running, but in fact we’re only
testing its interface – there is no executable file.
Adding Objects to Your Application 79
Notice that Interface Builder icon in your dock has changed from its origi-
nal build mode to the “big Frankenstein switch” icon shown at the left. This
new icon indicates that your program is now in Test Interface mode. You
can move, resize, miniaturize, and even close My Window. Everything
that’s there works except the Info and Edit submenu commands (of course,
the application doesn’t do much.) Press the Noise button and note that it
looks like a physical button that’s been pushed – the white highlights and
dark “shadows” around the edges of the button made it seem as though
there’s a light coming from the upper left.
11. Quit the Test Interface mode either by choosing Quit from the
UNTITLED menu or by double-clicking the mutated Interface
Builder icon in your dock.
The link cursor means that the button is linked to the Funk system sound
(In NeXTSTEP 2.1, the link cursor does not appear.)
a special icon just to demonstrate this feature. The icon can be placed in a
different location on the button using the Button Inspector window, which
we’ll discuss in a later chapter.
5. Choose Document→Test Interface. When you click the Noise button,
you’ll hear the sound that you dropped on top of the button.
6. Choose Quit from UNTITLED’s main menu to return to Interface
Builder’s build mode.
application doesn’t need to know the class of the target object before the
cut: message is sent.
Every NeXTSTEP object belongs to a class, which both defines and cre-
ates the object. Many of the NeXTSTEP class names are fairly self-explan-
atory. For example, the classes of the on-screen objects in the application
described above are listed in the following table.
Object in the Application Class
the window (“My Window”) Window
the button (“Noise”) Button
the main menu (“UNTITLED”) Menu
the menu items (Info, Edit, Hide, Quit) MenuCell
There are three other objects present in this application that aren’t immedi-
ately apparent because they have no obvious corresponding object on the
screen. They are listed in the table below. (It’s not imperative that you
understand everything in the remainder of this section right now, but we’re
going to give it to you anyway.)
One of the objects in this table has a funny “name” with square brackets.
The square brackets illustrate the Objective-C messaging operator. The
82 Creating a Simple Application With Interface Builder
Messaging is one of two major features that you need to learn about the
Objective-C language in order to write NeXTSTEP programs. The other is
constructing your own classes. The three bundled classes Window, View
and Application, are the very heart and soul of NeXTSTEP. If you under-
stand them, you understand a lot of what you need to know about the
Application Kit to write NeXTSTEP programs.
For example, when you choose the Quit menu command from an applica-
tion’s menu, the associated MenuCell object sends a terminate: message
to the controlling object NXApp (also known as the File’s Owner). You
can think of this as calling a function called terminate: inside the NXApp
object.
To see how this all works in practice, we’re going to add two more Control
objects to MyWindow, a text field and a slider. First we’ll put them in the
window, and then we’ll wire (connect) them together so that a message can
be sent from the slider to the text field.
Objects, Messages, and Targets 83
1. Drag the icons for the TextField and Slider objects (seen at the left of
the page) from the Palettes window into My Window. When you’re
done, you should have something that looks like this:
Next, we’ll make a connection so that the Slider object can send a message
to the TextField object whenever the Slider object’s knob is moved.
2. Hold down the Control key on the keyboard and drag from the Slider
object to the TextField object (note the direction). You will see a
“connection wire” appear between the two objects:
Connections from a control object to another object have two parts: a target
and an action. You already specified the TextField object as the target (or
receiver or destination) of a message when you connected the Slider object
to the TextField. You specify which action (method) the target should per-
form (in response to an event) in the Connections Inspector. Your choices
are listed in the right hand column of the Connections Inspector window in
Figure 8 below. (If you had connected the Slider object to a different tar-
get, say the Button object, then you would see a different list of actions in
the Connections Inspector, because a Button object can perform a different
set of actions than a TextField object.)
sage for its double (real number) value. This double value corresponds to
the position of the Slider object’s knob.
4. Select the takeDoubleValueFrom: action in the Connections
Inspector and then click the Connect button. (Alternatively, you can
double-click the takeDoubleValueFrom: action.) After you make the
connection, the Connect button becomes a Disconnect button, as in
Figure 8, and the connection dot ( ) appears next to the action name
in the Inspector.
5. Test the interface again by choosing Document→Test Interface.
6. Drag the Slider object’s knob. As you move it, the TextField object
will update the number it displays according to the knob’s position.
7. Choose Quit from UNTITLED’s main menu.
Objects, Messages, and Targets 85
Figure 9 below shows the communication between the Slider and Text-
Field objects. The mouse drag event causes the Slider to send the take-
DoubleValueFrom: message to the target TextField object, which
executes its takeDoubleValueFrom: method. The takeDoubleValue-
From: method sends the doubleValue message back to the Slider, which
returns the value .6427 to the TextField, and then the TextField displays
the result. (We didn’t have to tell the TextField object’s takeDoubleValue-
From: action procedure to send the doubleValue message back to the
Slider; the TextField is smart enough to know how to get a double value
from the sending object.) It may seem like a lot of overhead, but messaging
is actually quite fast – only slightly slower than a standard function call.
takeDoubleValueFrom:
doubleValue
.6427
Within the Slider Attributes Inspector you can specify the values that a
slider will return at its minimum and maximum positions, its initial posi-
tion, and whether it sends an action message to the target continuously as
the slider is dragged, or only when it is released (Continuous or not). You
can also disable the slider so that nothing happens when the user manipu-
lates it and set a Tag so a slider can be distinguished from others in a matrix
(group) of sliders. Every Application Kit object has its own Attributes
Inspector.
9. Quit Interface Builder. There’s no need to save the interface
specifications.
86 Creating a Simple Application With Interface Builder
Congratulations!
In this chapter, you’ve learned a little bit about the workings of Interface
Builder and Objective-C. You’ve seen that on-screen objects have corre-
sponding Objective-C objects inside the computer’s memory. You’ve also
seen a special class of object called a Control. A Control can have a tar-
get, which is another object that is automatically sent an action message to
perform an action when the Control object is manipulated by the user.
87
88 Creating an Application Without Interface Builder
/* Tiny.m
*
* A tiny NeXTSTEP application which creates a
* menu and a window and displays graphics
* in the window.
*/
#import <math.h>
#import <dpsclient/wraps.h>
#import <appkit/Application.h>
#import <appkit/Window.h>
#import <appkit/Menu.h>
#import <appkit/View.h>
void demo(void)
{
id myWindow, myView, myMenu;
NXRect graphicsRect;
float f, g; /* to draw the pattern */
float pi = 2 * acos(0.0);
const n = 31;
The Tiny.m Program 89
*/
[myWindow flushWindow];
void main()
{
/* Create the application object.*/
NXApp = [Application new];
The vi editor is the antique UNIX “visual” editor Bill Joy (of Sun Micro-
systems, Inc.) wrote. It’s mostly there for historic reasons, but some people
prefer it. Both Emacs and vi must be run from the NeXTSTEP Terminal
application which was described in Chapter 2.
Tiny.m: line-by-line 91
After you’ve typed the source code in the file Tiny.m, open up a Terminal
window and compile it with the following command in bold type (“local-
host>” is our UNIX command line prompt where “localhost” is the host
name of the computer – yours may differ):
localhost> cc -Wall -o Tiny Tiny.m -lNeXT_s -lsys_s
localhost>
If the program compiles without errors, you can run it by typing its name:
localhost> Tiny
You should see the window displayed in Figure 1 above. To see the menu
you must make Tiny the active application by clicking in its window or
double-clicking its generic application icon (seen at the left) near the bot-
tom of the screen (the icon does not have a title in NeXTSTEP 3.0).
Tiny.m: line-by-line
Neat? Now let’s take a look at this program line-by-line.
/* Tiny.m
*
* A tiny NeXTSTEP application which creates a
* menu and a window and displays graphics
* in the window.
*/
#import <appkit/Application.h>
#import <appkit/Window.h>
#import <appkit/Menu.h>
#import <appkit/View.h>
Now then, what’s being #import-ed? The first line should be familiar:
math.h contains the standard C language prototypes for the floating-point
math library. You need to include this file since Tiny.m uses the functions
Classes and Objects, Methods and Messages 93
sin() and cos() to create a graphical pattern. The next line includes the
file /usr/include/dpsclient/wraps.h, which contains the prototypes for
the PostScript (PS) functions that Tiny.m uses to display graphics.
1. This may be somewhat confusing to you if you already know C++, another
“object-oriented” language based upon C. Objective-C and C++ are very different
languages, largely because Objective-C has a run time system that handles messag-
ing, while C++ doesn’t.
94 Creating an Application Without Interface Builder
stored as part of the class in memory. These instance methods are shared by
all objects created by the class, which of course makes memory usage more
efficient.
The terms method and message appear to be used interchangeably and may
appear to mean the same thing, but they don’t. A method is a procedure
inside a class that’s executed when you send that class or instance of that
class a message.
For example, we could send the message [Application new] to the Appli-
cation class to invoke the new class method, or the message [NXApp run]
to an Application object to invoke the run instance method. As an exam-
ple with arguments, we could send the message
[myWindow sizeWindow: 200 : 300]
Objective-C adds only one new data type, id, to the C programming lan-
guage. An id variable is a pointer to an object in the computer’s memory.
Classes and Objects, Methods and Messages 95
If you wanted to set the center of aCircle at position (x,y), you might send
it another message:
[aCircle setX: 32.0 andY: 64.0];
Arguments always follow colons in messages, and thus a method has the
same number of arguments as there are colons in its name. The colons are
actually part of the Objective-C message, and thus the method above is
named setX:andY:, not simply setX or even setX andY.
The aCircle object might also provide methods that let you find out the x
and y locations of its center. We’ll call these accessor methods because
they give you access to a variable encapsulated inside the aCircle object.
For example:
printf("The circle is centered at %f,%f\n",
[aCircle x], [aCircle y] );
In this case, the methods x and y return floating point numbers and the out-
put would be:
The circle is centered at 32.0,64.0
It’s a good idea to add accessor methods to the classes you write: they hide
the implementation details of how an object actually works. For example,
the programmer who wrote the Circle class which built aCircle would
likely arrange for aCircle to store the circle’s center in instance variables.
In this case, the methods x and y would simply return these stored vari-
ables. Alternatively, the programmer might be storing the locations of the
corners of a square that is inscribed in the circle; in this case, the methods x
96 Creating an Application Without Interface Builder
The aCircle = [Circle alloc] statement sends the alloc message to the Cir-
cle class, asking it to allocate memory (create) a new Circle object. Thus,
alloc is a factory method. The other methods are instance methods,
because they are invoked by messages that we send directly to a Circle
object, in this example the circle object pointed to by the id variable aCir-
cle. (By convention, class names begin with upper-case letters, while meth-
ods and instances begin with lower-case letters.)
Since all of the methods in the above example return the id of the aCircle
instance itself (including the alloc message sent to the Circle class), we
could be really cute and cryptic and write the above code like this:
id aCircle;
[[[[[Circle alloc]
init]
Classes and Objects, Methods and Messages 97
This example might be a bit extreme, but the nesting technique is usually
used with alloc and init methods as follows:
aCircle = [ [Circle alloc] init];
@interface Circle:Object
{
float x;
float y;
float radius;
}
+ alloc;
- init;
- setX: (float)x andY:(float)y;
- setRadius: (float)radius;
- (float) x;
- (float) y;
- display;
- free
@end
This line tells the compiler that we’re about to define the Circle class and
that the Circle class inherits from the Object class. We’ll discuss inherit-
ance a bit later.
The next five lines define the instance variables that every Circle object
contains. These variables are private to each object of type Circle; the only
way you should access them is by sending messages to the object.
The eight lines which follow define methods. The alloc method is the one
which creates new objects (or instances) of the Circle type (class). The
plus sign (“+”) means that it’s a factory method; one that is invoked by a
message you send to the Circle class itself, rather than to an instance of the
Circle class. The other methods are preceded with minus signs (“-”) which
means that they are invoked by messages sent to class instances. (By the
way, you can have class and instance methods with the same name; the
Objective-C run-time system automatically figures out if you are sending
the message to a class or an instance of that class.)
The methods x and y both return floating point numbers, while the other
methods, including the factory method alloc, return the default return type
id, a pointer to an object.
At this point, you know everything about the Circle class to use it effec-
tively in one of your programs. Of course, you have no idea how the class
is actually implemented but it doesn’t really matter. That’s the key to
object-oriented programming: independent modules that work “as adver-
tised” through well-documented interfaces, without forcing the program-
mer to understand the internals of how they work. This is known as
procedural (or functional) abstraction.
One last point: although the words “sending a message” might suggest con-
currency, message invocations are really little more than traditional C-lan-
guage function calls.1 If your program sends a message to an object, that
object’s corresponding method has to finish executing before your program
can continue.
1. The Mach operating system, upon which NeXTSTEP is based, offers another
kind of messaging called “Mach messages.” Mach messages should not be con-
fused with Objective-C messages.
The NeXTSTEP Classes Used by Tiny.m 99
These statements bring in the Objective-C class definitions for four impor-
tant NeXTSTEP classes: Application, Window, View, and Menu. These
classes implement the basic behavior of NeXTSTEP programs.
Every NeXTSTEP program has one, and only one, instance of the Appli-
cation class. It’s usually created inside the program’s main() function by
sending the new message to the Application class. (NeXTSTEP uses the
new message to create a new object when you are only going to create
exactly one instance of the object.)
The Application object is the most crucial object in the program because it
provides the framework for program execution. The Application class
connects the program to the Window Server, initializes the PostScript envi-
ronment, and maintains a list of all the windows in the application. This
enables it to receive events from the Window Server and distribute them to
the proper Window objects, which in turn distribute the events to the
proper objects inside the on-screen windows.
For example, if a user presses a button inside a window, then the Window
Server gets a mouse down event from the hardware and passes it to the
Application object of the program which owns the window. The Applica-
tion object then passes the mouse down event to the proper Window
object, which in turn passes it to the Button object that controls the on-
screen button that was pressed.
The Window class is where the master control of your program’s on-screen
windows is defined. For every window that your program displays, there is
an associated instance object of the Window class inside the computer’s
memory. You can send messages to Window objects that make the associ-
ated on-screen windows move, resize, reorder to the top of the window dis-
play list (placing themselves on top of the other windows) and perform
many other operations.
100 Creating an Application Without Interface Builder
The View class is the class which plays the most central role in NeXTSTEP
applications. Many of the classes in the Application Kit, including all Con-
trol and Text classes, inherit from the View class (see the next section for
the definition of inherit). View objects are responsible for drawing in win-
dows and receiving events. Each View object can have any number of
views that it contains, called subviews. When a window receives a mouse
event, it automatically finds the correct View object to receive that event.
The Menu class implements the NeXTSTEP menu system. Methods in the
Menu class allow you to create new menus, add items to them, display
them, and perform other functions. Each item within the menu is actually
handled by another class called MenuCell. You can look at the include file
/usr/include/appkit/MenuCell.h if you are interested in seeing the meth-
ods that the MenuCell implements. In fact, all of the NeXTSTEP classes
have interface (.h) files in the directory /NextDeveloper/Headers/appkit
(or /usr/include/appkit in NeXTSTEP 2.1).
Inheritance
If you look at the file /NextDeveloper/Headers/appkit/Menu.h, you’ll
notice something curious in the Menu class interface:
@interface Menu : Panel
{
...
Unlike the Circle class we discussed above, the Menu class doesn’t inherit
directly from the Object class; instead, it inherits from the Panel class. In
turn, the Panel class inherits from the Window class. This means that
NeXTSTEP menus are really a special kind of panel with additional fea-
tures. Panels, in turn, are special kinds of windows. As we go down the
class inheritance chain from Window to Panel to Menu, each class has
additional instance variables and custom behavior, where desired, but oth-
erwise they basically work like objects from which they inherit. We say
that the Menu class is a subclass of the Panel class and the Panel class is a
subclass of the Window class. (If you’ve forgotten the distinction between
a panel and a window you can look back in Chapter 1.)
Back to Tiny.m 101
Yes and no. Although menus, panels, and windows look different on the
NeXTSTEP screen, they are all types of windows that are displayed on the
naked NeXTSTEP monitor’s background. The only way to draw on NeXT-
STEP’s Display PostScript monitor is by creating a window. Therefore,
menus and panels all need to be kinds of windows in order to be displayed.
The easiest way to make it all work is to specify menus as a special kind of
panel which itself is a special kind of window. This is called inheritance,
and is another powerful feature of Objective-C.
Back to Tiny.m
The program Tiny.m consists of two functions: demo() and main(). As
with C, every Objective-C program contains a function called main()
which is called by the operating system to start the program. The main()
function in Tiny.m isn’t very complicated:
void main()
{
/* Create the application object.*/
NXApp = [Application new];
Menu PopUpList
Font
FontPanel
FontManager
PrintPanel
FIGURE 2.
PageLayout
NXSplitView Slider
ScrollView Scroller
Speaker
ClipView NXColorWell
Listener
The Application Kit Hierarchy of Classes
NXJournaler FormCell
ActionCell TextFieldCell
The second statement in main() calls the function demo(), which contains
the code which makes the Tiny program unique. We’ll discuss this in detail
in the next section.
The reason for setting up graphicsRect is to specify where the new win-
dow will be located and how big it will be. The window itself gets created
in the next Tiny.m program line when the alloc message is sent to the Win-
dow class (alloc is a factory method).
The new instance object is then initialized within the nested message. The
id of the new Window object that is created is assigned to the variable
myWindow:
myWindow = [ [Window alloc]
Back to Tiny.m 105
initContent: &graphicsRect
style: NX_TITLEDSTYLE
backing: NX_BUFFERED
buttonMask: NX_MINIATURIZEBUTTONMASK
defer: NO];
One of the many nice features of Objective-C is that, for the most part,
arguments are labeled. It makes Objective-C programs easy to read, as
we’ll see in the discussion of arguments below.
defer:NO]; This tells the Window Server that we want our window cre-
ated now, rather than later.
The first statement creates and initializes a new Menu object with title
“Tiny” and assigns the id of the object to the id variable myMenu. The
second line adds a Quit menu item to myMenu. The argument
action:@selector(terminate:) specifies that the terminate: action method
should be invoked when Quit is chosen (@selector() is a directive that
returns the unique unsigned int identifier of a compiled method.) The ter-
minate: action causes the Application object to stop the main event loop.
The argument keyEquivalent:’q’ specifies Command-q as the key alterna-
tive to the Quit menu command.
The second statement is for cosmetic reasons: it sends the sizeToFit mes-
sage to instruct myMenu to recalculate its width and height to just fit the
text of the menu cells that it contains and resize its window as necessary.
The last statement of the group above sends a message to NXApp, the id of
the Application object that was created in main(), informing it that
myMenu is the id of the application’s main menu.
Back to Tiny.m 107
The next four statements create an object of the View class and set up the
Window for drawing. We need to describe the View class before we dis-
cuss these statements thoroughly.
Views
The View class and its subclasses are the primary mechanism by which
NeXTSTEP users and applications interact. Applications draw on the
screen by first invoking View instance methods to establish communica-
tion with the Window Server (in particular, Display PostScript), and then
by sending PostScript commands. Going the other way, the AppKit will
send a message to an object of the View class when the user does some-
thing which creates an event, like clicking the mouse or pressing a key on
the keyboard.
One of the most important methods in the View class is the drawSelf::
method. The drawSelf:: method in your View object is invoked when its
containing view (or window) wants your view to draw itself. (NeXTSTEP
invokes the drawSelf:: method automatically for you.) In this example,
however, we’re just trying to make something that works, so we’ll draw in
the View object directly.
myView = [ [View alloc] initFrame: &graphicsRect];
[myView setOpaque: YES];
The first of the two statements above contains nested messages which cre-
ate the View object, initialize it to the location and size of graphicsRect,
and finally returns an id which is assigned to myView. The second state-
ment sets the new myView object so it covers the area specified by graph-
icsRect (i.e., myView isn’t transparent). You may have noticed a potential
problem here: we’ve created myView with a size of 400×400, the same
size as the content area of the myWindow, but it has an offset of 150×350
relative to the window’s (0.0,0.0) origin. Don’t worry, we’ll take care of
that before we do any drawing.
[myWindow setContentView: myView];
108 Creating an Application Without Interface Builder
This statement sets up the View object that we’ve just created as the con-
tent view of the window that we created in the last section. Every window
contains precisely one content view, which represents the area of the win-
dow that is accessible to the application program. That is, it contains the
entire window except the title bar, border, and resize bar (if present). The
setContentView: method also changes the offset and the size of the View
object that we created so that it is precisely aligned with the window.
[myWindow makeKeyAndOrderFront: nil];
The lockFocus and unlockFocus methods tell the Display PostScript inter-
preter that all of the drawing you’re about to do (i.e., all the PostScript
commands you’re about to execute) will take place in the myView object.
It is very important to lockFocus before you issue any PostScript com-
mands. It is equally important to unlockFocus when you’re done.
Back to Tiny.m 109
The next group of statements contain the declarations and PostScript com-
mands (actually C functions that send PostScript commands to the window
server) that cause the design to be drawn.
float f, g;
float pi = 2 * acos(0.0);
const n = 31;
...
PSnewpath();
PStranslate(200.0, 200.0); /* place center */
When drawing a picture with PostScript, you first define the path, then use
the stroke (or fill) operator to make the path appear. It’s much like the way
“STOP” is painted on our roads by government workers. First the stencil is
cut out and then the paint is sprayed through. Display PostScript works the
same way: first the path is cut out with the newpath, moveto, and lineto
operators. Then the color is set with the setgray operator and drawn with
the stroke operator. Sending the flushWindow message to myWindow
causes the graphics buffer to make sure that everything is displayed on the
screen. It is necessary to flush the window because it is NX_BUFFERED.
(Normally, NeXTSTEP flushes the window for you automatically.)
This completes our discussion of the Tiny application. Next we’ll discuss
NeXTSTEP events and then modify Tiny to demonstrate event-handling.
110 Creating an Application Without Interface Builder
Responding to Events
The previous section showed how you might use a NeXTSTEP View
object to draw something on the screen. This section shows the other half
of View’s functionality: intercepting (and processing) events.
Here is the NXEvent structure which can be found in the same event.h file
mentioned previously.
typedef struct _NXEvent {
int type; /* event type */
NXPoint location; /* mouse location */
long time; /* time since launch */
int flags; /* (Shift, Control, Alt) */
unsigned int window; /* window number of event */
NXEventData data; /* event type-dependent data */
DPSContext ctxt; /* PostScript context*/
} NXEvent, *NXEventPtr;
The NXEventData structure contains the raw data about each event, as
provided by the Window Server. The DPSContext variable ctxt contains
the PostScript drawing context identification (similar to a window) where
the event originated. The other variables are self-explanatory.
NeXTSTEP supports 31 kinds of events types but only about half of them,
such as mouse-down and key-up, are of interest to developers. They are
#define-ed in the include file events.h and discussed in more detail later in
this book. Each event has a unique integer. The Application object
receives events and translates them from event numbers into Objective-C
messages which are sent to the appropriate objects. The following list of
methods is invoked as a result of these messages (bold indicates the
method name).
- mouseDown: (NXEvent *)theEvent;
- rightMouseDown: (NXEvent *)theEvent;
- mouseUp: (NXEvent *)theEvent;
- rightMouseUp: (NXEvent *)theEvent;
- mouseMoved: (NXEvent *)theEvent;
- mouseDragged: (NXEvent *)theEvent;
An Introduction to Event Handling 111
The names of these methods are also self-explanatory. They are defined for
objects of the Responder class and all of its subclasses. In order for our
programs to respond to most of these events in custom ways we simply
have to implement (actually override) these methods in our own class
implementations. With overriding, you can augment or replace the behav-
ior of a method in the superclass of the class that has the same name.
Of the objects we’ve learned about so far, Application, Window, and View
are all subclasses of the Responder class. They all receive events and
either process them or pass them along. We’ll learn more about events and
responders as we learn more about NeXTSTEP.
After the Application object receives the event, it determines the Window
object where the event occurred. The Application object then translates the
event number into a Responder message. For example, the
NX_LMOUSEDOWN event becomes a mouseDown: message, which is
sent to the appropriate window where the event occurred. When the Win-
dow object receives the event, it determines in which one of its Views the
event took place. It then sends the message to that View object. Thus, in
order to handle events, we need to create a subclass of the View class and
place an instance of it on the screen.
112 Creating an Application Without Interface Builder
#import <appkit/View.h>
@interface CView:View
{
}
- clear:sender;
An Introduction to Event Handling 113
- mouseDown:(NXEvent *)theEvent;
@end
According to the interface for the CView class, we’re going to be defining
two methods: clear: and mouseDown:. The clear: method is new; an
object of the View class doesn’t respond to it. On the other hand, the
mouseDown: method is already in the View class (actually inherited from
the Responder class). By defining our own version of this method, objects
of our subclass respond to the message first. This technique of re-defining a
method inherited from a superclass is called overriding a method.
#import "CView.h"
#import <dpsclient/wraps.h>
@implementation CView
*/
[self convertPoint:¢er fromView:nil];
To use this new subclass of View, we’ll have to make several changes to
the function demo() in Tiny.m. If you plan on typing in this code, begin by
making a copy of Tiny.m and renaming it Tiny2.m. Much of the code in
the two programs is identical. A complete listing of Tiny2.m follows.
We’ll discuss its differences from Tiny.m after the listing.
/* Tiny2.m
*
* A Tiny NeXTSTEP application which creates Menu,
* Window, and CView objects and displays circles in
* the window in response to mouseDown events.
* Links to the CView class.
*/
An Introduction to Event Handling 115
#import <dpsclient/wraps.h>
#import <appkit/Application.h>
#import <appkit/Window.h>
#import <appkit/Menu.h>
#import <appkit/MenuCell.h>
#import "CView.h" /* needed to create a CView */
void demo(void)
{
id myWindow, myView, myMenu;
NXRect graphicsRect;
[myMenu sizeToFit];
[NXApp setMainMenu:myMenu];
}
116 Creating an Application Without Interface Builder
The first major change we made in Tiny2.m (from Tiny.m) is with the
include files. The Tiny2.m program needs the interface defined in the
MenuCell.h and CView.h files because we need to send a message to a
new MenuCell object (titled “Clear”) and we also need to create a new
CView object. To create an instance of the CView class instead of the View
class, we only had to change from this line:
myView = [ [View alloc] initFrame: &graphicsRect];
to this line:
myView = [ [CView alloc] initFrame: &graphicsRect];
The next major change we made was to add a new menu item titled Clear.
This was done by the following statement:
[ [myMenu addItem: "Clear"
action: @selector(clear:)
keyEquivalent: 'k']
setTarget: myView];
The only differences from the previous compiler call is that we’ve changed
names from Tiny to Tiny2 and we command that CView.m be compiled as
well. Run the program by typing:
localhost> Tiny2
Click the mouse in a few places on the window, and you’ll end up with
something like the picture in Figure 3 above. Have fun!
General Rules
NeXT uses capital letters to delimit embedded words, such as mouseDown
and lockFocus. The underscore character (_) is used only in constants such
as NX_TITLEDSTYLE and macros such as NX_RAISE(), so variable
names like invalid_variable are generally not used.
Some NeXT classes and functions begin with the letters NX. Others don’t.
Generally, those classes that were part of the NeXTSTEP 1.0 release don’t
begin with the letters NX. With release 2.0, NeXT adopted the NX conven-
tion so that they would not use names for classes that might have already
been used by third-party developers.
118 Creating an Application Without Interface Builder
NeXT also uses the letters PS for PostScript bindings and DPS for Display
PostScript bindings.
Objective-C Methods
Method names begin with lower-case letters. Method names that are
defined with a plus sign (+) are class or factory methods, while method
names defined with a minus sign (-) are instance methods.
Methods have a colon for every argument. Thus the setTitle: method takes
a single argument, moveTo:: takes two arguments, and
initContent:contentRect:style:backing:buttonMask:
defer:screen:
Summary
In this chapter, we’ve written a program that used Objective-C but not
Interface Builder. In the previous chapter, Chapter 3, we were limited by
the tools that Interface Builder provided us with: there was no way to let
the programmer create loops, do math, or perform most other tasks that we
associate with “programming” a computational engine. We did lots of pro-
gramming in this chapter, but we had to do all of the work and thinking
about the mechanics of the program that Interface Builder did for us in
Chapter 3.
The real power of the NeXT programming environment is that it lets you
combine Interface Builder (and Project Builder) with Objective-C, taking
advantage of each one for what it does best: Interface Builder for creating
the interface and making connections which lead to message passing, and
Objective-C for creating new classes and the actual writing of the computa-
tional engine. Throughout the rest of this book, we’ll learn how to write
powerful application programs while writing a relatively small amount of
code.
120 Creating an Application Without Interface Builder
5
Building a Project:
A Four Function Calculator
In this chapter we’ll build a simple calculator application with four func-
tions: add, subtract, multiply, and divide. When we’re done, our calculator
will contain the menu and window shown below. In the process of building
the calculator, we’ll learn a lot more about Interface Builder, connections,
and some of the commonly-used NeXTSTEP Application Kit (AppKit)
classes.
121
122 Building a Project: A Four Function Calculator
We’ve chosen to build a calculator as the first “real” application in this text
for several reasons. First of all, calculators are familiar. We’ve all used one,
and we sort of know how they work. (When creating an application, the
first thing to understand is the problem to solve.) Second, calculators are
useful. As programmers, we’re constantly having to do silly little things
like add two numbers together or convert a number from decimal to hexa-
decimal (the hex part will be built in the next chapter). It’s a tool that you
can put to work after you build it.
Creating your own calculator puts you in charge of its design. After all,
there are so many different kinds of calculators: some are scientific, some
financial, and some are just simple four function calculators. Our calculator
will let you key in the sequence “3+4=” by clicking four buttons in a win-
dow and will display (in order) 3, 3, 4, and 7 in a text output area. If you
don’t like the decisions we’ve made and want to change or add functions
and features, go right ahead! Our aim is to give you the know-how to create
your own applications.
The main window in Project Builder contains five buttons just below its
title bar. The two buttons on the left, Run and Debug, are “action” buttons
that run and debug your project. The three buttons on the right, Attributes,
Files, and Builder, select the content (or view) of what you see in the win-
dow, much like the buttons at the top of Interface Builder’s Palettes win-
dow. We’ll discuss these buttons in more detail later.
When you first see PB’s main window the Files view is displayed. While in
the Files view, the middle part of the main window contains an icon path
124 Building a Project: A Four Function Calculator
similar that in a Workspace File Viewer. Below the icon path is a browser
which contains a list of all the file types associated with a project. We’ll
discuss these different file types in “The Files in a Project” on page 160.
4. Click Interfaces in the main window’s browser and then click
Calculator.nib under Interfaces. The Calculator.nib file icon appears
in the icon path as in Figure 3.
5. Double-click the Calculator.nib file icon in the main window’s icon
path. Interface Builder (IB) will automatically launch and display the
Calculator.nib interface provided by PB, which includes a main menu
titled “Calculator” and a main window titled “My Window.” An
associated File window is also displayed in the lower left corner of the
screen.
6. In order to simplify the screen, click in Project Builder’s main window
and then type Command-h to hide Project Builder.
Skip the next section, which only applies to NeXTSTEP 2.1, and go
directly to “Building the Calculator’s User Interface” on page 126.
Getting Started Building the Calculator Project 125
When you click Create, Interface Builder creates two things: a directory
named Calculator, and a file named Calculator.nib in that directory. IB
also changes the menu’s title to “Calculator.”
5. Choose IB’s File→Project menu command to create a project.
In the lower-right hand corner of the screen, you’ll see Interface Builder’s
Project Inspector, which is one of the views in IB’s Inspector. There are
four kinds of projects that IB can create. The Application choice is what
you usually use; it tells IB that you’re creating a project that’s going to be
compiled into an application program. The Custom IB and IB Palette
choices are used for creating your own version of IB and your own IB pal-
ette, respectively. Your own palette can be added to IB and used just like
the palettes delivered by NeXT. There are also a growing number of third-
party commercial and public-domain palettes available for NeXTSTEP.
Subproject is used for managing projects that are too complicated to put
everything in a single directory. This option lets you put nibs and source
code into their own directories, which get separately compiled and linked
into the main program.
6. Leave the selection on Application and click OK in IB’s Project
Inspector. This will create a NeXTSTEP 2.1 project consisting of a
Makefile and three other supporting files in the Calculator directory.
A fifth project file, Calculator.nib was already saved in the directory.
126 Building a Project: A Four Function Calculator
Now you’ll see the “real” Project Inspector which lets you monitor all of
the files that are part of your project. The Add button allows you to add
files to your project, while the Remove button allows you to remove them.
If you want to edit a file, simply click the file’s name in the right-hand col-
umn and click the Open button (alternatively, you can double-click the
file’s name). If the file is a nib file, Interface Builder will open up that nib’s
File window and display the nib’s objects. Otherwise, Interface Builder
will send a message to Workspace Manager asking it to open the file. This
is very handy when you are creating class files, because you can go directly
from IB to editing the class files in the Edit application.
The nib files are stored in a NeXTSTEP proprietary binary format that is
undocumented. Fortunately, it doesn’t need to be documented – all of the
management of the nib files is done by IB. IB is a nib editor: when it opens
a nib file, it reads the specifications and displays the associated objects.
After you make your modifications to the program, IB writes out a new nib
file, replacing the old one.
Now that we’ve created the project we’ll add and customize the windows,
panels and menus needed for the Calculator’s user interface.
1. In NeXTSTEP 2.1, nibs can also be stored directly in a section in the executable
file. See Chapter 9 for more on sections.
Building the Calculator’s User Interface 127
To see the Window Inspector for a particular window, the window must be
selected. You can select a window by simply clicking in its background, or
by clicking its icon in the File window’s Objects view. If you click an
object (e.g., button) inside a window object in IB, then the button, not the
window, will be selected.
In general, the title and contents of IB’s Inspector panel change in response
to which object in the interface is selected. When the Inspector changes in
response to a selection, then you may still have to choose which aspect of
the object you want to inspect: its attributes, connections, or something
else. You can make this choice by dragging to it in the Inspector’s pop-up
list or by typing Command-1 for Attributes, Command-2 for Connec-
tions, and so forth.
The resize bar in the now-renamed Calculator window will disappear. The
close button will not disappear until the interface is tested or the applica-
tion is running. This is so you can close the window to simplify the screen
while building the interface.
1. These steps work in both NeXTSTEP 3.0 and 2.1; however, the screen shots are
all taken from NeXTSTEP 3.0 and may differ slightly in 2.1. Where there are sig-
nificant differences between 2.1 and 3.0, we’ll mention them in footnotes or paren-
thetic remarks.
128 Building a Project: A Four Function Calculator
Wait a minute! How do you resize the window if you just made the resize
bar go away? Well, that’s the purpose of the funny icon ( ) in the upper-
left hand corner of the Calculator window. We’ll refer to this as the resize
button.
11. Click the resize button and the resize bar will temporarily reappear at
the bottom of the Calculator window.
12. Resize the Calculator window so that it is about 3 inches square. After
you resize the window, the resize bar will disappear again, so you
might have to click the resize button several times if you wish to try
several sizes.
13. Make sure the Views palette is visible by clicking the Views button at
the top of IB’s Palettes window.
14. Drag a TextField ( ) object from the Palettes window and
drop it near the top right of the Calculator window. The window should
look like the one on the left of Figure 5 below.
FIGURE 5. Adding a TextField and a Button to a Window
15. Drag the left-middle handle to the left to widen the TextField so that it
is almost the width of the Calculator window, as in the window on the
right of Figure 5.
16. Drag a Button ( ) object from the Palettes window and drop it
in the lower left corner of the Calculator window as in Figure 5.
17. While holding the Alternate key on your keyboard, drag the upper-
right-hand handle of the button up and to the right. Release the mouse
button when there are four rows by three columns of buttons, as in the
window on the left of Figure 6 below. Congratulations! You’ve just
created a matrix of buttons.
The buttons in the Matrix we created above will be used to represent digit
keys on our Calculator, and thus we’ll change their names from “Button” to
the 10 decimal digits (and disable the remaining two). We also need to set
some less obvious attributes of the buttons, called tags, to make the buttons
work properly. In order to explain how tags work and better understand
why we make certain choices while creating an interface, we’ll postpone
finishing the interface to discuss the Objective-C class that we’ll create to
handle the button clicks.
Controllers generally don’t have main event loops; instead, they perform
actions in response to events that are received and interpreted by other
objects. A good rule of thumb is to place as little code in your controller as
necessary. If it is possible to create a second controller that is only used for
a particular function, do so – the less complicated you make your applica-
tion’s objects, the easier they are to debug. Our Calculator’s controller will
contain the code to perform the arithmetic and thus can be thought of as the
computational engine or back end of the application.
Before you start coding, it’s a good idea to sit down and think about your
problem. What does the controller have to do? What sort of messages will
it need to respond to? What sort of internal state does it have to keep in
order to perform those functions? Recall that our Calculator will let a user
key in the sequence “2*5=” by clicking four buttons in a window and will
display (in order) 2, 2, 5, and 10 in a text output area. Thus for our Calcula-
tor, the answers are fairly straightforward.
• Clear the display and all internal registers (value holders) when a clear
button is clicked.
• Allow the user to click a digit button on the numeric keypad and display
the corresponding digit immediately after it is typed.
• Allow the user to click a function button (e.g., add, subtract).
• Clear the display when the user starts entering a second number.
• Perform the appropriate arithmetic operation when the user presses the
“equals” button or another function button.
Our Calculator must also maintain the following state to perform these
functions:
• The first number entered.
• The function button clicked.
• The second number entered.
It turns out that, in order to work properly, our controller object needs two
more pieces of information:
• A flag that indicates when a function button has been clicked. If the flag
is set, then the text display area (which we’ll call readout) should be
cleared the next time that a digit button is clicked, because the user is
entering a second number.
• The location in the readout text display area where the numbers should
be displayed.
If you think about it, what we are actually doing is using Objective-C to
create a simulation of a real, physical calculator. That’s what object-ori-
ented programming is often about: constructing progressively better simu-
lations of physical objects inside the computer’s memory, and then running
them to get real work done. When the simulation is functionally indistin-
guishable from the real-life object being simulated, the job is finished.
The Object class name is displayed in gray, which means that you can’t
change any of its properties or built-in behaviors without subclassing it. So
that’s what we’ll have to do.
3. Drag to Subclass in the Operations pull-down list. A new class called
MyObject will appear under Object in the class hierarchy as in
Figure 7 below.
4. Double-click the MyObject class name in the white text field area in
the Class Inspector (at the lower right of the screen), change the name
from MyObject to Controller, and hit Return.
You’ve just created a new class called Controller. Right now it doesn’t do
anything different than the Object class. Next we’ll give the Controller
class some custom behavior by adding some “outlets” and “actions.”
1. In NeXTSTEP 2.1, double-click the Classes suitcase icon in the File window.
This will open up a separate Classes window.
134 Building a Project: A Four Function Calculator
When an outlet is set to store the id of another object in the nib file, Inter-
face Builder calls this a connection. NeXTSTEP automatically maintains
connections for you. When object specifications are saved in a nib file, the
connections we set up between them in IB are saved as well. These connec-
tions are automatically restored when the nib file is loaded back into IB.
For example, suppose that you have two object specifications in a nib file:
object A and object B. Suppose also that object A contains an outlet that
points to object B. When NeXTSTEP loads this nib file, it will first create
new instances of object A and object B. NeXTSTEP will then automati-
cally set the outlet in object A to point at object B. That is, it sets the outlet
A to be the id of object B.
Outlets therefore give you an easy way to track down the id of objects that
are dynamically loaded with nib files! They are the mechanism that NeXT-
STEP gives you to “wire up” an interface without writing any code.
1. In NeXTSTEP 2.1, make sure the word Outlet is highlighted in black on the
button in the Class Inspector (as it is here).
136 Building a Project: A Four Function Calculator
We’ll use this Controller outlet to point to the text display area (TextField)
object, so the Controller can send it messages. Next, we’ll add action
methods to the Controller class.
In light of our discussion of the design of the Controller class, the function
of these four actions should seem fairly self-evident. The Controller Class
Inspector should look like the one on the right in Figure 8 above.
Notice that there’s only one action to handle all digit button clicks (enter-
Digit:) and only one action to handle all the function buttons (enterOp:).
The way we determine which digit or function button is clicked is to use
the single argument of these actions, the id of the sender of the message.
By querying the sender of the message, the methods enterDigit: and
enterOp: find out which digit or which function button was clicked and
1. In NeXTSTEP 2.1, make sure the word Action is highlighted in black on the
Actions-Outlets button in the Class Inspector.
Building the Calculator's Controller Class 137
can then perform the appropriate action. This is a much more economic
means of method dispatch than creating a separate method for each button
on our Calculator – it takes less code but it runs virtually just as fast.
12. IB next asks if these files should be inserted into the project. (See the
attention panel at the bottom of Figure 9.) Click Yes.
138 Building a Project: A Four Function Calculator
After you complete the last step above, the Project Builder tool reappears
and displays the new Controller class implementation file, Controller.m,
in the Files view as in Figure 10 below. The Controller.h class interface
file appears under Headers in PB’s Files view. These new Controller class
files reside in the ~/Calculator directory and contain only a skeleton of the
Controller class. In order to make our Controller work, we’ve got to write
some Objective-C code.1
1. In NeXTSTEP 2.1, the Project Inspector reappears and displays the class files.
Most of the information and functions available in IB’s Project Inspector in NeXT-
STEP 2.1 have been moved to Project Builder in NeXTSTEP 3.0.
Building the Calculator's Controller Class 139
We’re going to get the Controller class working in stages, testing them one
at a time. Generally, this is a good approach to writing any program, large
or small. Object-oriented programming makes it easy to test the individual
parts, because they are all fairly self-contained.
First, we’ll get numeric entry and the clear keys working. Later we’ll han-
dle the arithmetic functions.
13. Open the Controller.h class interface file in your favorite editor.
You can open the Controller.h file in the Edit application by double-
clicking Controller in IB’s File Window or by double-clicking Con-
troller.h under Headers in PB’s Files view.
Below we list the Controller.h file containing all the lines generated by
Interface Builder in medium Courier type and all the lines you need to
insert in bold Courier type (we’ll use this type convention throughout
the book when we mix “old” code with “new” code to be inserted).
/* Controller.h */
#import <appkit/appkit.h>
@interface Controller:Object
{
id readout;
BOOL enterFlag;
BOOL yFlag;
int operation;
float X;
float Y;
}
- clear:sender;
- clearAll:sender;
- enterDigit:sender;
- enterOp:sender;
- displayX;
@end
Interface Builder generated the first two (non-bold) lines because we sub-
classed Object to create the Controller class (appkit.h will import the
Object.h class interface file)1. Since we added readout as an outlet in the
1. In NeXTSTEP 2.1, the Object.h class file is imported explicitly and appkit.h is
not imported. We’ll discuss appkit.h further on page 142.
140 Building a Project: A Four Function Calculator
Below we list the Controller.m file. As with the Controller.h file above,
we list the lines generated by IB in medium type and the lines you need to
insert in bold type.
#import "Controller.h"
@implementation Controller
- clear:sender
{
X = 0.0;
[self displayX];
return self;
}
- clearAll:sender
{
X = 0.0;
Y = 0.0;
yFlag = NO;
enterFlag = NO;
[self displayX];
return self;
}
- enterDigit:sender
{
if (enterFlag) {
Y = X;
X = 0.0;
enterFlag = NO;
}
- enterOp:sender
{
return self;
}
- displayX
{
char buf[256];
@end
When a message is sent to an object of a class, then the class interface file
definition for that class should be #import-ed in the class definition. But
#import statements for the TextFieldCell and Matrix class interface file
definitions are not listed in the code above, so what’s going on? Fortu-
nately, the #import "Controller.h" line in Controller.m together with the
#import <appkit/appkit.h> line in Controller.h take care of importing
the TextFieldCell and Matrix class interface file definitions for us.1 In
fact, they import all Application Kit class definitions. This is very ineffi-
cient in NeXTSTEP 2.1 because compilation time is greatly increased.
However, in NeXTSTEP 3.0 the AppKit class headers are all precompiled
so it’s okay to import them all. (A precompiled header file has been prepro-
cessed and parsed, thereby improving compile time and reducing symbol
142 Building a Project: A Four Function Calculator
table size.) This is why IB in NeXTSTEP 3.0 inserts the #import <appkit/
appkit.h> line in all class interface files it generates.
The clearAll: method in the Controller.m file sets the X and Y registers to
0.0 and the two flags to false, and then sends the displayX message to self
(the Controller object itself) to display 0.0 in the text display area. The
clear: method is similar but only needs to set the X register to 0.0 and
redisplay. We’ll discuss the enterDigit: and enterOp: methods after we
finish setting up the user interface and making all the connections.
This will create an icon called Controller in the Objects view in the Calcu-
lator’s File window as in Figure 11 below (IB automatically displays the
Objects view). This icon represents an instance object of the Controller
class; it can be used as the target object of action messages and also to ini-
tialize outlets. You can change the name Controller if you want; it isn’t
used for anything except your convenience.
FIGURE 11. Controller Instance Object in File Window
Controller
instance
A tag is a reference integer for a Control object and can be set and read in
IB’s Inspector panel. Tags are not used by the AppKit; rather, their purpose
is to allow your program to distinguish cells (objects) in a matrix (or other
controls) from one another when different cells of the matrix send the same
message to an object.
1. In NeXTSTEP 2.1, you must click the OK or hit Return to make changes in the
Inspector take effect.
144 Building a Project: A Four Function Calculator
ButtonCell selected
same ButtonCell
being “inspected”
For example, when one of the buttons in the matrix of digit buttons is
clicked, we’ll arrange for the Matrix object to send the enterDigit: mes-
sage to our Controller object. The Controller object needs to know which
button (i.e., which digit) was clicked, and thus sends a message back to the
sender (Matrix object) to determine which of the button cells in the
Matrix object was selected. The Controller can then get the tag for that
cell and use it in the enterDigit: method as if it were a digit (since the tags
will correspond to the digits on the buttons).
6. Change the titles and tags of the other keypad buttons to reflect the
digits that they represent, as in Figure 13 below. The button with title 1
should have tag 1, the button with title 2 should have tag 2, etc. You
can use the tab key to easily move between cells in the Matrix.
7. Double-click the lower-left hand button in the Matrix (which is
invisible in Figure 13) to select it.
8. In the ButtonCell Inspector, deselect the Bordered switch, select the
Disabled switch, delete the “Button” title, and hit Return. This will
make the button disappear and become “unclickable.”
9. Repeat Step 8 above for the lower-right hand button.
Customizing Buttons and Making Connections 145
A small black square will appear in the middle of the Matrix and a “con-
nection line” will connect the Matrix object to the Controller icon. Be
careful to connect the Matrix and not one of the individual Matrix buttons!
When you release the mouse button, Interface Builder will display the
Matrix Connections Inspector. You can determine the source object of the
connection by the name in the title bar of the Inspector – in this case it
146 Building a Project: A Four Function Calculator
The connection we just made means the following: whenever a user clicks
any one of the digit buttons in the matrix, the Matrix object will send the
enterDigit: action message to an instance of our Controller class.
Next, we’ll add clear and clear all buttons in the Matrix object as we did
with the digit buttons. This time, however, we’ll connect the buttons indi-
vidually to the Controller object, not as a matrix.
14. Add a second matrix of buttons above the digits matrix in the
Calculator window. This new matrix should have two buttons with
titles CA and C in a font such as Helvetica Bold 16 point.
Customizing Buttons and Making Connections 147
Your window should look like the one on the right in Figure 13, “Cus-
tomizing the Calculator’s Buttons,” on page 145. If you don’t remem-
ber how to add a matrix to your window refer back to “Adding
Controls in a Window” on page 128. If you can’t find the Palettes win-
dow then choose in IB’s Tools→Palettes menu command.
15. Double-click the CA button to select it. The ButtonCell Inspector, not
the Matrix Inspector, should appear.
16. Connect the CA button to the Controller instance object icon by
Control-dragging from the button to the icon and then double-clicking
the clearAll: action in the ButtonCell Inspector (the double-click has
the same effect as clicking the action name and then clicking the
Connect button).
17. Similarly, double-click the C button to select it and then connect it to
the Controller instance icon. This time, double-click on the clear:
action.
148 Building a Project: A Four Function Calculator
You can make Interface Builder show you an existing connection from a
source object by first selecting the source object and then clicking target or
the action method with a dimple ( ) in the Connections Inspector.
18. Double-click the word “Text” in the TextField object and hit the
Delete (Backspace) key to erase the word “Text.”
19. If necessary, drag to Attributes in the TextField Inspector (or type
Command-1).
20. Deselect the Editable option in the TextField Inspector so that the text
in the TextField object is not editable.
21. Set the alignment to be right-justified by clicking the icon that looks
like this:
The source of every Calculator connection we’ve made so far was a user
interface object, while the destination was always the Controller object. In
the next connection we make, the direction will be reversed; the source will
be the Controller while the destination will be an interface object. This
second type of connection requires an outlet.
22. Control-drag from the Controller instance object icon to the TextField
object. The black “connection line” should look like the one in
Figure 16 below.
23. In the Connections Inspector, double-click the readout outlet to
complete the connection.
Connecting the readout outlet to the TextField object causes the readout
instance variable in the Controller object to get initialized to the id of the
TextField when the nib section gets loaded at run time. Initializing an out-
let in an instance object is the only way to determine the id of an object cre-
ated with Interface Builder, and thus outlets must be used when sending
messages to user interface objects.
24. Type Command-s in IB to save the Calculator.nib file.
If you click an error message in the middle of the main PB window, then
the file with the error will automatically open in the Edit application and
the line containing the error will be selected. It’s easy!
If you get any compiler errors, we suggest that you re-examine your code
line-by-line, rather than resorting to the floppy disk included with this
book. Examining your code is an important skill to develop, and when you
Tags and the enterDigit: Action Method 153
are working on your own projects, you won’t have a floppy disk with the
completed program to fall back on.
For the error reported here, you could open the Controller.m file in the
Edit application, type Command-l to bring up the Line and Character
Range panel, enter “48,” the line where the error was reported, and inspect
the code on line 48 and previous lines. You might also take some time to go
back to Chapter 2 and review “gdb – Debugging Programs” on page 63.
If you compiled your program in a Terminal shell window, then you would
get essentially the same feedback as in Figure 19 above.
The first part of the function is self explanatory: if the enterFlag instance
variable is set, then the value of the X register is copied into the Y register
and both the X register and enterFlag are cleared. Note that the scope of
instance variables (e.g., enterFlag) is the entire class definition. All meth-
ods within a class have access to all instance variables defined in that class.
The next line contains the magic: the value in the X register is multiplied
by 10 and added to the returned value [[sender selectedCell] tag]. This
154 Building a Project: A Four Function Calculator
performs a base 10 left-shift operation on X and then adds the last number
pressed. Let’s look at this nested method expression in pieces.
These codes correspond to the tags that we will give the buttons in the
arithmetic operations Matrix. The Controller object will determine the tag
of the button that sends it the action message to decide which function but-
ton the user has clicked.
2. Using an editor, insert the lines in bold below into the method
enterOp: in the Controller.m file.
- enterOp:sender
{
if (yFlag) { /* something is stored in Y */
switch (operation) {
case PLUS:
X = Y + X;
break;
Adding the Four Calculator Functions 155
case SUBTRACT:
X = Y - X;
break;
case MULTIPLY:
X = Y * X;
break;
case DIVIDE:
X = Y / X;
break;
}
}
Y = X;
yFlag = YES;
[self displayX];
return self;
}
The enterOp: method is the real heart of our Calculator. It performs the
arithmetic operation that was stored in the operation instance variable, sets
up the registers and flags for another operation or another button click, and
then displays the contents of the X register on the screen.
3. Activate Interface Builder and create a Matrix with two rows and
three columns in the upper-right hand corner of your Calculator
window. See Figure 20 below.
4. Set the title of each button to correspond with one of the six basic
functions, as in Figure 20 below. Use a larger font (e.g., Helvetica Bold
16 pt.) so the titles are easily readable (see Step 10 on page 145 for
details). Don’t worry about the unary minus “+/-” button for now.
5. Set the tag of each button to correspond with the enum defined in the
Controller.h file above.
6. Connect the function Matrix to the Controller by Control-dragging
from the Matrix to the Controller instance icon and double-clicking
the enterOp: action in the Matrix Inspector. This connection is similar
to the one we made for the numeric keypad.
7. Save the Calculator.nib and Controller class files.
156 Building a Project: A Four Function Calculator
8. Compile your program and run it. If you use PB, all you have to do is
click the Run button and PB will make the program when necessary.
All buttons except the unary minus should work as you expect.
For easy access, we recommend that you keep the Project Builder, Inter-
face Builder, Edit, Terminal, and Librarian, and HeaderViewer application
icons in your dock while developing applications.
It doesn’t matter where you put this method in Controller.m, as long as it’s
between the directives @implementation and @end. However, we sug-
gest you order the method implementations in the same way as the method
declarations are ordered in the Controller.h class interface file.
When a button in a Matrix object has its own target, the button’s target
overrides the target of the Matrix. So, when the user clicks on the unary
minus button, the button will send the doUnaryMinus: message to its own
target, rather than sending the enterOp: message to the target of the
Matrix.
10. Save all pertinent files, make and run the program. The unary minus
function should behave as expected.
Adding Unary Minus to the Controller Class 159
You might be wondering why IB’s Parse operation didn’t bring in the defi-
nition of the displayX method in addition to the doUnaryMinus: method.
The reason is that IB only looks for action methods when parsing a class
interface (.h) file. An action method is a method declared in the form
- methodname:sender;
with a single argument called sender. As we’ll see later, IB will also bring
in outlet declarations when parsing a class interface file. These outlet dec-
larations must be instance variables of the form
id outletname;
Action methods and outlets are the only types of information that Interface
Builder learns about a class when it parses a class interface file.
160 Building a Project: A Four Function Calculator
Project Builder’s Files view uses a browser to list each project file by type,
as in Figure 23 below. You can change which type of files are displayed by
clicking a file type (e.g., Images) in the left-hand column of the browser. In
the table below we briefly discuss what each file type means.1
FIGURE 23. Supporting Files in Project Builder
Several files are automatically placed in your project and project directory
when you create a new project. Some of these, for example, Makefile, the
The Makefile contains the names of all of the nib, Objective-C, tiff (bit-
map), snd (sound), psw (PostScript), library, and other files that get linked
together to produce your final application program. It can also contain
installation information, include file dependencies, and other information.
Let’s take a close look at the Makefile that Project Builder generated for us
when we chose Project→New to create the Calculator project.
# Generated by the NeXT Project Builder.
#
# NOTE: Do NOT change this file --
# Project Builder maintains it.
#
# Put all of your customizations in files called
# Makefile.preamble and Makefile.postamble
# (both optional), and Makefile will include them.
NAME = Calculator
PROJECTVERSION = 1.1
LANGUAGE = English
LOCAL_RESOURCES = Calculator.nib
CLASSES = Controller.m
HFILES = Controller.h
MFILES = Calculator_main.m
OTHERSRCS = Makefile
MAKEFILEDIR = /NextDeveloper/Makefiles/app
INSTALLDIR = $(HOME)/Apps
INSTALLFLAGS = -c -s -m 755
SOURCEMODE = 444
-include Makefile.preamble
include $(MAKEFILEDIR)/app.make
The Files in a Project 163
-include Makefile.postamble
-include Makefile.dependencies
Since Project Builder (IB in NeXTSTEP 2.1) updates your project’s Make-
file, and since the Makefile is used to compile your program, it’s important
to keep PB up-to-date. When you add a file to your project, do it by using
the PB, rather than modifying the Makefile directly. Indeed, you should
never edit the Makefile directly; if you need to add commands not avail-
able via the Project Inspector, then put them in one of the files Make-
file.preamble or Makefile.postamble, which are included in the Makefile
if they exist in the project directory.
You can use the file Makefile.dependencies to tell make about dependen-
cies created by #include and #import statements in your source code. This
will force make to recompile a file if any of its included files are modified.
From a UNIX shell command line, type “make depend” to create the file
Makefile.dependencies: this will cause the C preprocessor to search for
#include and #import statements and create a dependency file that make
will include automatically.
To learn more about Project Builder’s use of Makefiles, look at the master
NeXTSTEP Makefile in /NextDeveloper/Makefiles/app/app.make.
The Files in a Project 165
#import <appkit/Application.h>
[NXApp free];
exit(0);
}
[NXApp loadNibSection:"Calculator.nib"
owner:NXApp withNames:NO]
This line sends the loadNibSection:owner:withNames: message to
NXApp, which causes the Calculator.nib file to get “loaded” from the
Calculator.app (or Calculator.debug) directory into your application’s
memory map. The objects that were specified in Interface Builder will be
created in memory and any initialization that you’ve defined for these
objects (e.g., the size of a button, an outlet to another object) automatically
gets executed at this time.
166 Building a Project: A Four Function Calculator
[NXApp run];
This line sends the run message to NXApp, which causes the application’s
(Application object’s) main event loop to run. The main event loop han-
dles menu clicks, keystrokes and all of the other events to which an appli-
cation can respond. Control doesn’t return to main() until the object
NXApp gets a stop: or terminate: message, usually in response to a user
choosing the application’s Quit menu item.
[NXApp free];
This line sends the free message to NXApp, which closes all the applica-
tion’s windows, breaks the connection to the Window Server, and frees the
Application object that was created when the new message was sent to the
Application class.
exit(0);
The standard C exit() library function which gracefully ends the applica-
tion.
Every main() program file generated by NeXTSTEP has these same lines,
although the name of the nib loaded is usually different. (Sometimes the
class name Application is replaced by a subclass of the Application class.)
1. In NeXTSTEP 2.1, the project file is called IB.proj, and there is no PB.gdbinit
file or English.lproj directory.
Summary 167
Summary
We did quite a bit in this Chapter! We started by building a “real” project
with Project Builder. Then we used Interface Builder and Objective-C to
build user interface objects, create and customize our own class, and con-
nect these user interface objects with an object of our new class. We also
learned a little more about Objective-C and some AppKit classes, and a lot
about the files that PB generates. In the process we used all four of the
operations that IB can perform on classes: Subclass, Instantiate, Unparse,
and Parse.
In the next chapter, we’ll add an Info Panel and some icons to our Calcula-
tor application and find out how to increase the efficiency of a NeXTSTEP
application by using separate nib files.
168 Building a Project: A Four Function Calculator
6
Nibs and Icons
In the first half of this chapter, we’ll create an Info Panel for our Calculator.
The Info Panel performs the function of telling people about the author of
the program. We will use the Info Panel to demonstrate how to manage
multiple nib files within a single application. In the second half of this
chapter, we’ll see how NeXTSTEP allows you to specify an icon for an
application and a directory. We’ll assume you’ve read the section on creat-
ing icons with Icon Builder (Icon in NeXTSTEP 2.1) in Chapter 2.
167
168 Nibs and Icons
This can be real drag – especially if your program doesn’t need most of the
objects in the main nib file for normal operation. For this reason, NeXT-
STEP lets you take objects that you don’t use often and place them in sepa-
rate nib files. These auxiliary nib files are loaded only when needed.
NeXT recommends that programs use auxiliary nib files for their Info
Panel. As an experiment, start up Edit (or any other NeXTSTEP applica-
tion) and choose the Info Panel menu command from the Info menu. You
may see the NeXTSTEP spinning disk cursor ( ) for a moment or two,
and then the Info Panel will display. The delay is caused because Edit is
loading the Info Panel’s nib file into memory. If you click the Info Panel’s
close button and then choose Info→Info Panel again, you won’t get any
delay at all; this is because the panel, once loaded, stays in the computer’s
memory. Because Edit’s Info Panel is stored in a separate nib, the user has
to wait for the Info Panel to load only when he or she wants to see it – not
every time the program is run.
Now we’ll create a similar Info Panel nib for our Calculator. This will con-
sist of three steps:
(i) Modifying the Controller class to load the nib and display the Info
Panel.
(ii) Modifying the Calculator’s main nib.
(iii) Creating the new nib for the Info Panel.
You might think that the easiest way to create the new outlet and action
method is to add them in Interface Builder’s Class Inspector, as we did in
the previous chapter. This is precisely the wrong thing to do. The reason is
that we’ve already added code to the Controller.m and Controller.h class
files; if we use IB’s Unparse command to generate new files, then we will
lose all of the coding that we’ve added so far!
Instead, the thing to do is to add the new outlet and action method in the
Controller.m and Controller.h files directly, and then use the IB’s Parse
command to read them into IB’s internal description of the class. The new
outlet and action will then appear in IB’s Class Inspector and we’ll be able
to use them to make connections with user interface objects. (Recall that
Managing Multiple Nibs 169
- clear:sender;
- clearAll:sender;
- enterDigit:sender;
- enterOp:sender;
- displayX;
- doUnaryMinus:sender;
- showInfo:sender;
@end
The infoPanel outlet will hold the id of the Info Panel. The showInfo:
action method will display the Info Panel in response to a menu choice.
2. Launch Project Builder (PB) by double-clicking the PB.project file
icon in the Calculator project directory in the Workspace File Viewer.
This will bring the entire project into PB. (In NeXTSTEP 2.1, double-
click the IB.proj file in the project directory to launch IB.)
We suggest you keep the PB.project file icon for your current project
on your File Viewer’s shelf for easy access.
3. Now double-click Calculator.nib in the Files view (under Interfaces)
of PB’s main window. This will open the Calculator.nib file in IB.
4. Open the Classes view in IB’s File window by clicking the Classes (h)
suitcase icon. (In NeXTSTEP 2.1, double-click the Classes suitcase
icon in the File window to open up a separate Classes window.)
5. Select the Controller class (under Object) in the Classes browser.
170 Nibs and Icons
6. Drag to Parse in the Operations pull-down list and you’ll see IB’s
Parse panel. Click OK in the panel to parse the new definition of the
Controller class from the Controller.h file on disk.
Note the new infoPanel outlet and showInfo: action method in the
Class Inspector as in Figure 1 below. (In NeXTSTEP 2.1 you’ll get a
different panel; click its Replace button. Type Command-1 to see the
new declarations.)
7. Using an editor, insert the entire new showInfo: method below into
Controller.m. We suggest you place it just before the @end directive.
- showInfo:sender
{
if (infoPanel == nil) {
if (![NXApp
loadNibSection: "info.nib"
owner: self
withNames: NO]) {
return nil; /* Load failed */
}
}
Managing Multiple Nibs 171
All Objective-C instance variables are initialized to nil (0) when an object
is created.1 When the Calculator application starts up, the infoPanel outlet
in the Controller instance will not be explicitly set, so its value will be nil.
Thus, when the showInfo: method is invoked the first time, the “if” state-
ment will cause the loadNibSection:owner:withNames: message to be
sent to NXApp. (Recall that NXApp is the global variable which contains
the id of the application’s Application object.)
When the info.nib file is loaded, it will automatically initialize the info-
Panel outlet to the id of the Info Panel. (We’ll show you how to create the
Info Panel and set up the initialization a bit later.) Finally, the showInfo:
method will send the Info Panel the makeKeyAndOrderFront: message,
which makes the Info Panel the key window and brings it to the front of the
window display list (making it visible).
The second time the showInfo: method is called, the infoPanel outlet will
already be initialized. Thus, the statement sending the loadNibSec-
tion:owner:withNames: message will be skipped, preventing a second
copy of the nib from being loaded. But since the nib that we loaded the first
time through is still in memory, the Info Panel will be displayed without the
loading delay.
With NeXTSTEP Release 1.0, the Info command was one of the com-
mands in the application’s main menu. That is, you chose Info and the pro-
gram’s Info Panel popped up. Since NeXTSTEP 2.0, the Info command is
1. Note that Objective-C initializes only instance variables. Variables that are static
or local to a method are still uninitialized, just like in standard ANSI-C.
172 Nibs and Icons
properly located in the Info submenu with several other commands. Nor-
mally there are at least two commands in the Info submenu: Info Panel
and Preferences. Other common commands you might want to add are
Help and Show Menus. The first three of these commands bring up a panel
and should thus be followed by three dots (e.g., “Info...”). The NeXTSTEP
User Interface Guide says that bringing up a panel should be a safe and
reversible option: a user should be able to make the panel go away by
clicking a cancel or close button without any ill-effects for the application.
14. Disable the Help menu cell by first clicking it and then selecting the
Disabled option in the MenuCell Inspector.
174 Nibs and Icons
15. Open the Objects view in IB’s File window by clicking the Objects
suitcase icon. Note the Controller instance icon.
16. Connect the Info Panel menu cell to the Controller by Control-
dragging from the menu cell to the Controller instance icon and
double-clicking the showInfo: action method in the MenuCell
Inspector.
Connecting the Info Panel menu cell to the Controller instance actu-
ally sets the MenuCell object’s target instance variable to be the id of
the Controller instance.
This section only applies to NeXTSTEP 3.0. NeXTSTEP 2.1 auxiliary nibs
are created much differently than in 3.0 and you should skip to “Creating
the Info Panel’s Nib in NeXTSTEP 2.1” on page 180.
17. Choose IB’s Document→New Module→New Info Panel command.
You’ll see a new Info Panel and File window as in Figure 5 below.
Note that there are now two File windows at the lower left of the screen,
one for Calculator.nib and the other for the new nib that was created with
IB’s Document→New Module→New Info Panel command. The icon
titled Info in the new File window represents the new Info Panel. (All com-
mands in the Document→New Module submenu create new nibs and all
nibs have their own File window.)
18. Type Command-s to save this new nib and you’ll see a Save panel. See
Figure 6 below. Note that IB automatically displays the directory
(Calculator/English.lproj for English language nibs) where the new
nib should be saved.
19. Type info.nib and click the OK button (we’ll capitalize the names of
main nibs and not capitalize the names of auxiliary nibs). IB will
automatically display the attention panel in Figure 7 below. Click Yes.
(Note that the name in the title bar of the new File window changes to
info.nib and Project Builder automatically shows that the new nib has
been added to the project.)
Managing Multiple Nibs 175
21. Customize the text in the Info Panel with clicks, double-clicks and the
keyboard. We customized our Info Panel as in Figure 9 below.
FIGURE 9. Customized Info Panel
Don’t worry about changing the Info Panel’s icon – the default termi-
nal icon on the left side of the panel – for now. We’ll be replacing it
with our own custom icon later in this chapter.
22. Click the Classes (h) suitcase icon in the info.nib (not Calculator.nib)
File window to see the Classes view (browser) for this nib.
Each nib has its own list of classes that are available to that nib so be
careful to make changes in the correct File window.
We want the Controller instance to load info.nib when the user chooses
our Calculator’s Info→Info Panel menu command. We’ve already set up
the connection from the menu command to the Controller. We still have to
set up communication between the Controller and the Info Panel, which is
part of the separate info.nib file. As of now, info.nib doesn’t even “know”
that a Controller class exists (you can see this by looking at the subclasses
of Object in the info.nib Classes view). We’ll take care of this in the next
few steps.
23. Select the Object class in the info.nib Classes view and then drag to
Subclass in the Operations pull-down list. As we saw for
Calculator.nib in Chapter 5, a new class called MyObject appears
under Object in the class hierarchy.
24. Double-click the MyObject class name in the white text field area in
the Class Inspector (at the lower right of the screen) and change the
name from MyObject to Controller.
25. Drag to Parse in the Operations pull-down list and you’ll get IB’s
Parse panel with our project’s Controller.h file selected. (From now
on, we’ll abbreviate this by simply saying “Parse the Controller class
definition.”)
26. Click OK in IB’s Parse panel to parse the Controller.h file on disk.
You should see the Controller’s outlets and actions in the Class
Inspector as in Figure 1, “New Outlet and Method in Class Inspector,”
on page 170.
Step 22 through Step 26 (inclusive) above can actually be performed in one
simple step: simply drag the Controller.h icon from your File Viewer and
drop it in the info.nib File window! IB will automatically insert the Con-
troller class into the info.nib hierarchy and Parse the class definition on
disk. We’ll use this technique in subsequent chapters.
We need to make a connection from the Controller instance (created by
Calculator.nib) to the Info Panel to initialize the infoPanel outlet. We
can’t do this using a different Controller instance instantiated by info.nib.
It must be the same one that is instantiated by Calculator.nib because
that’s the one that will be controlling the program. The easiest way to do
this is to use the File’s Owner. The File’s Owner is an object that “owns”
a nib file; every loaded nib must have one. We’ll arrange for the Controller
instance created by Calculator.nib to be the File’s Owner of info.nib.
We’ll discuss this further after the next few steps.
27. Click the Objects suitcase icon in the info.nib (not Calculator.nib)
File window to see the Objects view for this nib.
28. Make info.nib’s File’s Owner be of the Controller class by clicking
the File’s Owner icon in info.nib’s File window and then clicking
Controller in the Class Inspector as in Figure 10 below (scroll
upward, if necessary).
29. Make the infoPanel outlet in the File’s Owner point to the Info Panel.
Do this by Control-dragging from the File’s Owner icon in the
info.nib (not Calculator.nib) File window to the title bar of the Info
Panel, and then double-clicking the infoPanel outlet in the File’s
Owner Connections Inspector.
When info.nib is loaded, it will create the Info Panel. It will then set
the infoPanel outlet in the File’s Owner object (the Controller object
which loads info.nib) to the id of the newly-created panel.
What is the File’s Owner? Recall the Objective-C statement in the show-
Info: method in Controller.m which, when the program runs, loads
info.nib:
Managing Multiple Nibs 179
The File’s Owner is the object that is specified by the self in the clause
owner:self. In this case, the owner is the Controller instance (self) that
sends the above message to NXApp. Thus the infoPanel outlet in the Con-
troller instance is set to the id of the Info Panel that is loaded.
The first time you choose the Calculator’s Info→Info Panel menu com-
mand, there will be a slight delay before the Info Panel shows up. How-
ever, if you click the Info Panel’s close button and then choose Info→Info
Panel again, it should appear immediately.
180 Nibs and Icons
Each Interface Builder File window corresponds to a nib file. The first
thing to do is to give this new nib a name.
19. Choose File→Save As and save the new nib in the file info.nib in the
project directory. The File window’s title bar will change to reflect its
new name.
20. Add the info.nib to your project. Type Command-6 and click the
Attributes/Files button in IB’s Project Inspector so that the word Files
is highlighted. Click .nib in the Type column, then click the Add but-
ton. You’ll get an Open panel.
21. Double-click the info.nib file name in the Open panel to add info.nib
to your project. When the program is compiled, info.nib will automati-
cally be linked to the executable file.
22. Create the Info panel. To do this, click the “Windows” button (seen at
the left) at the top of Interface Builder’s Palettes window.
23. Drag the icon labeled Info from the Palettes window and drop it some-
where (anywhere) in the middle of the screen. You’ll see Interface
Builder’s default Info panel.
24. Customize the text in the Info Panel with clicks, double-clicks and the
keyboard. We customized our Info Panel as in Figure 9 above.
Don’t worry about changing the Info Panel’s icon – the terminal icon
on the left side of the panel – for now. We’ll be replacing it with our
own custom icon later in this chapter.
25. Double-click the Classes (.h) icon of the to info.nib File window to
bring up the Classes window for this nib.
Each nib has its own list of classes that are available to that nib so be
careful to make changes in the correct File window.
Managing Multiple Nibs 181
26. Select the Controller class in the Classes browser and then choose
Parse from the Operations pull-down list. (From now on, we’ll abbre-
viate this simply by saying “Parse the Controller class definition.”)
We need to make a connection from the Controller instance (created by
Calculator.nib) to the Info Panel to initialize the infoPanel outlet. We
can’t do this using a different Controller instance instantiated by info.nib.
It must be the same instance that is instantiated by Calculator.nib because
that’s the one that will be controlling the program. The easiest way to make
this connection is to use info.nib’s File’s Owner. (A File’s Owner is an
object that “owns” a nib file; every loaded nib must have one.)
What is the File’s Owner? Recall the statement in the showInfo: method in
Controller.m which, when the program runs, loads info.nib:
[NXApp loadNibSection: "info.nib"
owner: self
withNames: NO])
The File’s Owner is the object that is specified by the self in the clause
owner:self. In this case, the owner is the Controller instance (self) that
sends the above message to NXApp. Thus the infoPanel outlet in the Con-
troller instance is set to the id of the Info Panel that is loaded.
The first time you choose the Calculator’s Info→Info Panel menu com-
mand, it may take a moment for the Info Panel to display. However, if you
182 Nibs and Icons
click the Info Panel’s close button and then choose Info→Info Panel
again, it should appear immediately.
Directories can also have icons. For example, the icon at the left, which
looks like a book of NeXT documentation, is the icon for the directory /
NextLibrary/Documentation/NextDev/Summaries.
NeXTSTEP icons are represented by standard TIFF (Tag Image File For-
mat) files. All icons are displayed on a 48x48 pixel grid, although NeXT-
STEP will automatically scale the icon that you provide if it is a different
size. Icons are often stored 2-bits deep with alpha transparency informa-
tion, although with the growing number of color computers running NeXT-
STEP, color icons are becoming increasingly common.
If you want to use a color icon, you should store both a color icon and a
black-and-white icon in the same file so that the icon will look good when
it is displayed on a black-and-white system. Use the NeXTSTEP tiffutil
utility if you need to put more than one TIFF image into a single file. Find
out how to use it by searching the UNIX manual pages in Librarian.
Creating Icons
There are many ways to create icons in NeXTSTEP; any application which
can save a graphic in a TIFF file can create an icon. The bundled icon
building tool in NeXTSTEP 3.0 is Icon Builder, which resides in the /Next-
Developer/Apps directory. The bundled icon building tool in NeXTSTEP
Adding Icons to Your Application 183
We’re not very good artists (certainly not as good as Keith Ohlfs!), but we
did manage to create the icons in Figure 11 below. The one on the left will
be used to represent our Calculator application while the one on the right
will be used to represent our Calculator’s project directory. They are dis-
played here in the Obese Bits format in the Icon Builder application. For a
little variety, we’ve used Symbol font for the mathematical symbols in the
application icon and Helvetica bold for them in the directory icon.
Strictly speaking, you don’t have to create an icon for the Calculator direc-
tory. But it’s nice to have one – it helps make the directory stand out when
you look at it in the File Viewer. You don’t have to make the Calculator
directory’s icon different from the Calculator itself; but you probably want
to make them different, so you can tell them apart.
1. For icon design guidelines we suggest you consult a book on user interface
design, such as Designing the User Interface by Ben Shneiderman or Software User
Interface Design by Deborah Mayhew.
2. You may also specify a TIFF to be shown while files are dragged into the direc-
tory by placing it in the directory with the name .opendir.tiff.
184 Nibs and Icons
The following three steps only work in NeXTSTEP 3.0. If you’re using
NeXTSTEP 2.1, then skip to the section “Changing an Application Icon in
NeXTSTEP 2.1” on page 187.
1. Back in Project Builder, click the Attributes button to display the
attributes of your Calculator project.
Changing the Calculator’s Application Icon 185
2. Drag the Calculator.tiff icon from your File Viewer and drop it in the
Application Icon area at the lower left of the Attributes view in
Project Builder. If necessary, the Calculator.tiff icon will be copied
into the project directory. The icon should show up in the Application
Icon well and (eventually) on the Run button as in Figure 13 below.
If you look again at your Calculator directory in the File Viewer, you’ll
see that the Calculator application icon has changed from the default to the
Calculator.tiff icon. You may need to choose Workspace’s View→Update
Viewers menu command (or type Command-u) to force the Workspace
Manager to recognize the new icon.
186 Nibs and Icons
By adding the Calculator.tiff application icon we get one other benefit for
free; the icon also shows up in the Info Panel! Run your Calculator applica-
tion and choose Info→Info Panel to see it, as in Figure 15 below.
The reason the application icon shows up can be seen in Interface Builder.
Get back into IB and click the icon in the Info Panel. The Button Inspector
appears as in Figure 16 below because the icon is actually displayed on top
of a disabled button. (Putting an image on a disabled button in a window is
an easy way to display an image in a window.) In the Button Inspector, the
Changing the Calculator’s Application Icon 187
Icon field contains app, the Mach-O section name for the application icon
we added earlier.
FIGURE 16. Button Inspector with Application (app) Icon
again because make is smart enough to know that their object code files in
the ~Calculator/debug_obj directory are up-to-date.
If you look again at your Calculator directory in the File Viewer, you’ll
see that the Calculator application icon has changed from the default to
the Calculator.tiff icon. You may need to choose Workspace’s View→Up-
date Viewers command (or type Command-u) to force the Workspace
Manager to recognize the new icon.
There’s still one little piece of unfinished business: the icon on our Calcula-
tor’s Info Panel. Recall that when last saw the Info Panel it contained the
default application icon (see Figure 9 above). Clearly, what we want is the
Calculator’s application icon, rather than the funny little default icon that
Interface Builder gave us.
4. Back in IB, bring up the Info Panel (double-click its icon in the File
window, if necessary) and click the funny little terminal icon so it’s
selected.
The icon is actually a disabled button. Putting an image on a disabled
button in a window is perhaps the easiest way to display an image in a
window or panel.
5. Type Command-1 to bring up the Button Inspector.
NeXTSTEP lets you give a name to every icon associated with your appli-
cation. The icons can be included in your application’s executable file in
the __ICON segment or stored in your application’s app wrapper (seg-
ments will be described in Chapter 9; app wrappers were described on
page 42). Interface Builder lets you specify the image that your button first
displays when the program starts up. You can change the image displayed
while your program is running by sending the button the setIcon: or setIm-
age: message with the name of the icon or image you want to display.
6. Using the Button Inspector, change the icon name from
defaultappicon to app. Click the OK button.
You’ll see the icon in your Info Panel change from the default applica-
tion icon to Interface Builder’s icon.
NeXTSTEP Images
NeXTSTEP uses objects of the NXImage class to display both TIFF and
EPS images on the Display PostScript screen. One of the nice features of
NXImage is its facility for dealing with images by name. As we have seen,
each image in a nib file or in the application wrapper is given a name;
NXImage lets you retrieve these images by name and display them on the
screen.
Many NeXT developers enable the Info Panel’s application icon button, so
that clicking it does something interesting. For example, clicking the icon
might play a sound, display a “secret” message, or perform some sort of
animation. There’s really no limit to what you can do, as you can see by
clicking the application icon in the Info Panels of the Preferences and
Workspace Manager applications. To add your own magic, treat the appli-
cation icon button like any other button: enable it and have it send a mes-
sage to your Controller object (the File’s Owner of the auxiliary nib file).
You can refer to your new icon by name, just like any of the other icons,
and display it on a button or in any of your custom views. We’ll show you
how to display an image without using a button in a later chapter.
When you drag the image (e.g., MyImage.tiff) into your application’s File
window in IB, you’ll be asked whether you want to add the image file to
your project or create a local image, as in the attention panel in Figure 18
below. If you click Create Local Image then the image itself will be cop-
ied into the active nib in IB. If you click Yes then the image will be copied
into the project directory and will show up under Images in Project
Builder’s Files view (this is also the case when you drop the image into
Project Builder itself). Images that are stored in the nib file cannot easily be
used by objects in other nibs. For this reason, it is usually better to store
images in the project directory rather than in a nib.
Summary
In the first part of this chapter, we added a second nib file to our Calculator
application which provided it with an Info Panel. Using multiple nib files is
a good way to speed the performance of your program: with multiple nibs,
objects are only created when they need to be used.
In the second part of this chapter, we learned how to add two icons to our
Calculator project: one for the project directory, the other for the applica-
tion program itself. A side benefit of adding the application icon was that it
192 Nibs and Icons
In the next chapter, we’ll learn about delegation, a powerful tool for con-
trolling the functionality and extending the behavior of the NeXTSTEP
Application Kit objects.
7
Delegation and Resizing
In this chapter, we are going to modify our Calculator so a user can choose
to work with any of the following bases: base 2, 8, 10, and 16. To do this,
we’ll modify the Controller class to keep track of the current base and
update the display accordingly. We’ll also have to modify the keyboard-
input routines to work with the proper base, called the radix, and ignore
key presses (digit button clicks) that are invalid for a particular base. But
most importantly, we will introduce the concept of delegation, a technique
for specifying objects that perform functions for other objects. As for the
user interface, we’ll set up a matrix of radio buttons (for the user to change
the base) and change the size of a window under program control.
193
194 Delegation and Resizing
Radio button matrices work like the other button matrices we’ve used
except that exactly one cell is always selected: the last button the user
clicked. (They work the same as the buttons on a car radio.) Other than
that, all the button matrices are essentially the same, with tag instance vari-
ables and selectedCell methods and all the other stuff that you get for free
whenever you use the Matrix class.
5. While holding down an Alternate key, drag the bottom middle handle
of the selected radio button matrix downward so that the matrix
consists of four buttons.
1. In NeXTSTEP 2.1, double-click the IB.proj file in the Calculator project direc-
tory to bring the entire project into Interface Builder.
Handling Different Bases 195
6. Label the buttons Binary, Octal, Decimal, and Hex and set the tags of
the buttons to 2, 8, 10, and 16, respectively. (Double-click each radio
button, type Command-1 to display the Button Attributes Inspector,
and set the tags.)1
7. Make the Decimal radio button the selected one in the matrix by
double-clicking it and then clicking the Selected switch in the Button
Attributes Inspector. (Later we’ll initialize the Controller object so
that base 10 will be in effect when the application launches.)
Your Calculator window should now look like the one in Figure 1 above.
- showInfo:sender;
- enterOp:sender;
- clearAll:sender;
- clear:sender;
- enterDigit:sender;
- doUnaryMinus:sender;
- showInfo:sender;
- setRadix:sender;
@end
9. Insert the ltob() function below into Controller.m before the
@implementation directive.
char *ltob(unsigned long val, char *buf)
{
int i;
A function in your class implementation file can be used by any other part
of your program – it’s a regular C language function. (The function does
not have access to the class instance variables because it is outside the class
implementation.) The ltob() function listed above changes a long integer
into an ASCII binary representation. It places that ASCII representation in
a buffer that is passed into the function, rather than an internal static buffer,
so that it will work properly in a multi-threaded environment. We need this
function because NeXTSTEP lacks a general purpose function for convert-
ing integers to ASCII-encoded strings of arbitrary bases.
10. Insert the setRadix: method below into Controller.m immediately
before the @end directive.
- setRadix:sender
{
radix = [ [sender selectedCell] tag];
[self displayX];
return self;
}
This method sets the radix instance variable to be the tag (2, 8, 10, or 16)
of the radio button cell that was clicked and updates the X register.
11. Replace the original displayX method in Controller.m with the new
one below. The new lines, which handle the new bases, are in bold.
Handling Different Bases 197
- displayX
{
char buf[256];
char bbuf[256];
switch(radix) {
case 10:
sprintf( buf, "%15.10g", X );
break;
case 16:
sprintf( buf, "%x", (unsigned int)X);
break;
case 8:
sprintf( buf, "%o", (unsigned int)X);
break;
case 2:
strcpy( buf, ltob( (int)X, bbuf ) );
break;
}
[readout setStringValue:buf];
return self;
}
This displayX method converts the contents of the X register to the base
(radix) that the user previously selected and displays the result in the Cal-
culator window’s text display area (recall that the readout outlet points to
the TextField object near the top of the Calculator window). Note that the
base is “known” to displayX through the radix instance variable. Since
radix is an instance variable, it is “global” within the class implementation
and thus accessible to any Controller method.1
In order for the radix instance variable to be set to the user-selected base,
we need to arrange for the radio button matrix to send the setRadix: mes-
sage to the Controller. As always, when we need an on-screen object to
pass information to a custom object, we must set up a target-action connec-
tion in Interface Builder. But IB doesn’t know about the setRadix: action
method yet, so we must first Parse the new Controller class definition.
12. Back in IB, open up the Classes view and Parse the new definition for
the Controller class to bring in the setRadix: action method
declaration (make sure you have first saved the Controller class files).
13. Connect the Matrix of radio buttons to the Controller instance icon
(in the Objects view in IB’s File window). Arrange for the Matrix to
send the setRadix: action message to the Controller by double-
clicking setRadix: in the Matrix Inspector.
14. Type Command-s to save Calculator.nib.
15. Finally, make and run your application. When it starts up, click the
digit button labeled 9.
Wait! Nothing happens! What’s wrong? Before you panic, try clicking the
radio button labeled Decimal. The number 9 should appear. Then click the
radio button labeled Octal. The display should change to 11. Click the
Binary button and you’ll see 1001.
The technique that we will use here to initialize the Controller object
involves introducing a new technique called delegation, which we will use
so that a method inside the Controller class is automatically called by the
NXApp Application object when the program starts executing.
Delegation
NeXTSTEP uses a technique called delegation to allow objects to specify
other objects, called delegates, to handle certain messages for them. Thus
one object can delegate the responsibility for handling certain messages to
another object. Delegation gives the programmer a system for modifying or
controlling the behavior of NeXTSTEP’s more complicated objects, such
Delegation 199
For more information about respondsTo: and similar messages, see Digital
Librarian’s documentation on the Object class. The respondsTo: method
is defined in the root Object class and thus is inherited by all other objects.
A Will message gets sent before an event takes place. Some Will methods
let you control the behavior of an Application object by the value your
message returns. Here are the Will messages for an Application object:
Will methods let you do fairly complicated things with ease. For example,
suppose you set up a delegate object for your application’s Application
object, and the delegate implements the appWillTerminate: method.
When the user chooses the Quit menu command from your application, the
Application object receives the terminate: message and in turn sends the
appWillTerminate: message to its delegate. The delegate’s appWillTer-
minate: method could then display a panel asking the user “Do you really
want to quit?” If the user answers No, then the delegate returns nil to the
Application object, and the application doesn’t terminate. If the user
answers Yes, the delegate returns self, and the application terminates.
Not every class in the Application Kit (AppKit) makes use of delegation;
generally, anything that you want to do with delegation can be done just as
well with subclassing. If you have a choice, use delegation: it is easier to
debug and it affords you a greater opportunity to reuse your code from one
project to another. It also frees you from having to subclass a lot of the
AppKit classes. For most applications, you’ll only need to subclass the
Object or View classes.
But what should the delegate be? The answer to this question depends on
your application. Sometimes you will create a special object whose sole
mission in life is to be the delegate of one or more other objects. By using
one object as the delegate for several other objects, you can centralize con-
trol for handling events for common objects. Other times, an object may
serve double duty, being both the delegate for another object and having a
life of its own.
In our example, we’ll make our Calculator’s Controller object be the dele-
gate of the Application object. We’ll do this for two reasons: first, the
Controller is fairly simple. By making it the Application’s delegate, we
eliminate the complexity of creating a second class.1
We’ll need to add a new outlet to the Controller class so that the delegate
method can determine the initial radio button (radix) that is selected in the
on-screen matrix. This outlet, which we’ll call radixMatrix, will be set to
the id of the matrix of radio buttons.
3. Using an editor, insert the radixMatrix outlet into Controller.h.
...
@interface Controller:Object
...
int radix;
id radixMatrix;
}
...
4. Parse the Controller class definition again so IB knows about the new
radixMatrix outlet (make sure the Controller.h file has been saved).
5. Connect (Control-drag from) the Controller instance icon to the
Matrix object containing the radio buttons and double-click the
radixMatrix outlet in the CustomObject Connections Inspector. Be
careful to connect the outlet to the Matrix object and not to one of the
Cells inside the Matrix (the text near the bottom of the CustomObject
Inspector tells you the class of the destination object).
- appDidInit:sender
{
[self setRadix:radixMatrix];
[self clearAll:self];
return self;
}
@end
You can add as many Controller delegate methods as you like between the
@implementation Controller(ApplicationDelegate) and @end direc-
tives. You can also use this syntax construct for adding methods to AppKit
classes, but you will need to set up separate interface and implementation
files. ApplicationDelegate is called a category. You cannot use a category
to add new instance variables to a class, but methods in a category have full
access to all of the instance variables defined in the object class itself. Next
we show the matching class interface for the Controller(ApplicationDele-
gate) category.
7. Insert the ApplicationDelegate declaration code below after the
original @end directive in Controller.h.
@interface Controller(ApplicationDelegate)
- appDidInit:sender;
@end
When the application starts up, the Controller’s appDidInit: method auto-
matically gets executed. The appDidInit: method will then invoke the
Controller’s setRadix: method, supplying radixMatrix as the sender.
This simulates the user clicking one of the radio buttons. The setRadix:
method looks at the sender, which points to the Matrix of radio buttons
(recall we made a connection which made the radixMatrix outlet point to
this Matrix), finds the selected cell and sets the radix variable appropri-
ately. This is a little tricky because we’re supplying a “phony” sender.
But that’s not the only necessary change: we’ve also got to change the key-
pad so that particular buttons are deactivated when certain bases are
selected. For example, a user shouldn’t be able to press the 8 key when the
octal base is chosen. Also, it would be nice to make buttons for the num-
bers A, B, C, D, E, and F appear when the user asks for Hex input.
To scan all of the digit buttons in the Matrix, we’ll need its id. We’ll also
need the id of each individual button that the matrix contains. As we will
see, the id of each cell inside a Matrix object is stored in yet another
206 Delegation and Resizing
The keyPad outlet will provide our program with the id of the digit button
Matrix object when the application starts up. As usual, we’ll arrange for
this initialization in IB.
3. Back in IB, Parse the new Controller class definition so IB knows
about the new keyPad outlet (make sure Controller.h has been saved).
4. Connect the newly-created keyPad outlet in the Controller instance to
your Calculator’s digit button Matrix object. As before, make sure you
connect to the matrix and not a digit button within the matrix.
- setRadix:sender
{
int i;
id cellList;
[self displayX];
return self;
}
7. Enter the number 258. The main Calculator window should look like
the one at the top of Figure 3 on the next page.
FIGURE 3. Calculator Window with Base 10 and Base 2
8. Now, click the Binary radio button. The number 258 should change to
its binary representation, and the numbers 2 through 9 on the digit
buttons should turn gray, as in the Calculator window at the bottom of
Figure 3.
In this example, the setRadix: method disables the buttons in the matrix
that have a tag that is equal to or greater than the current radix – so, for
example, the keys 2-9 don’t work when you’re in binary mode. But rather
than hard-coding the keys the setRadix: method needs to disable for each
radix, we have setRadix: find these keys by scanning through the associ-
ated List object which contains the matrix cells. Likewise, rather than
hard-coding into setRadix: the number of buttons in the matrix, we have
setRadix: determine the number by asking the List how many objects it
contains. This way, we can change the number of cells in the matrix while
in Interface Builder and not have to make any changes to the setRadix:
method.
To accomplish this magic, we’ll widen the Calculator window and enlarge
the keypad, adding the new digit buttons on the right-hand side. We’ll also
set the window back to its original size, covering the newly-added buttons.
(iii) Arrange for our Calculator to remember how big it is when it starts out,
and to restore the window to that size when the user wants to make the
extra buttons “disappear.”
Notice that we haven’t entered any sizes such as how big the matrix is, how
big the Calculator window is, or how big the window has to grow. We sim-
ply don’t need to find out this information ahead of time and hardcode it
into our program. Instead, we’ll have the Controller object send messages
to the Matrix and Window objects to find out this information. The Con-
troller will then calculate how much larger the window needs to grow in
order to make the additional hex buttons visible, and send a message to the
Window object to change its size accordingly.
...
@interface Controller:Object
...
id keyPad;
NXRect originalViewSize;
}
...
The new originalViewSize instance variable will hold the size of the Cal-
culator window’s content view before it is expanded. The content view of a
window holds the contents of the window – all of the visible window
except for the title bar, the black border, and the resize bar (if present).
8. Insert the line in bold below into the appDidInit: method in
Controller.m.
- appDidInit:sender
{
[self setRadix:radixMatrix];
[self clearAll:self];
return self;
}
NXRect keyFrame;
NXRect newWindowFrame;
[keyPad getFrame:&keyFrame];
[keyWindow getFrame:&newWindowFrame];
NX_WIDTH(&newWindowFrame) +=
NX_X(&keyFrame) + NX_WIDTH(&keyFrame)
- NX_WIDTH(&originalViewSize)
+ 4.0;
[keyWindow
placeWindowAndDisplay:&newWindowFrame];
}
/* placeWindowAndDisplay: gives a cleaner redraw
* when making the window bigger.
*/
[self displayX];
return self;
}
Don’t worry if this code seems very complicated now. It uses a few meth-
ods in the Window and View classes that will be described shortly.
The two new declarations set up the oldRadix variable to contain the old
radix and the keyWindow variable to contain the id of the window that
contains the Calculator’s keypad matrix. The actual changing of the Calcu-
lator window’s size is done within the two “if” blocks that follow. The
first “if” block makes the window bigger when switching to base 16 from
another base. The second “if” block makes the window smaller when
switching from base 16 to one of the others.
In the first case – making the window bigger – the program gets the size
and position of the keypad matrix by sending it the getFrame message.
All NeXTSTEP screen controls like Matrix and Slider are actually sub-
classes of the View class. All Views respond to the getFrame: message by
returning the size of their “frame,” or rectangular region in the window.
The size returned is the size of the entire matrix, not just the part that is vis-
ible.
214 Delegation and Resizing
To calculate the width of the bigger window, the method first gets the cur-
rent window’s X coordinate using the NX_X() macro. It then adds to this X
coordinate the width plus offset of the keypad matrix frame, subtracts the
original width of the window, and adds a 4 pixel boundary.
The second “if” block – executed if the user is switching from base 16 to
another base – simply restores the window to its original width stored in
originalViewSize. Thus, it is not necessary to calculate how wide to make
the window so that it correctly obscures the part of the keypad matrix that
should be obscured.
10. Save all pertinent files and make and run your Calculator.
11. Click the 5, 8, and 7 digit buttons and then click the radio button
labeled Hex. The Calculator window should change from the window
at the top of Figure 5 to the window at the bottom. The number in the
readout text area should change from 587 to 24b.
12. Now click the Binary radio button and the window should return to its
original size.
Each window contains at least one instance of the View class (described
below) called the window’s content view. Although you can work directly
The Window and View Classes 215
with a window’s content view, normally you will create subviews of the
content view in which you do your actual drawing and event processing.
We’ll discuss these ideas in great depth in the following chapters.
Some of the most common instance methods for Window objects are listed
alphabetically in the table below. We’ll use the same type convention as is
used in NeXTSTEP documentation. Methods are in bold type, arguments
216 Delegation and Resizing
are in italic type, and data types are in normal type. Recall that if an argu-
ment’s data type isn’t specified (e.g., sender), then it’s an id by default.
All of the methods available to the Window class are described in the doc-
ument /NextLibrary/Documentation/NextDev/GeneralRef/
02_ApplicationKit/Window.rtf. Two easy ways to find and read this file
are to use the Librarian and /NextDeveloper/Demos/HeaderViewer appli-
cations.
Every window contains at least one view – the content view. This view cov-
ers the window except for the title bar, resize handles and black border. The
window’s content view automatically stretches and shrinks with the win-
dow when the window is resized.
Every view can have zero, one or more subviews. After a view draws itself,
it redraws any of the objects in its subview hierarchy (more on view hierar-
chies later) whose appearance has been changed or altered. In this way,
what is on the NeXTSTEP screen properly corresponds with what is stored
in the computer’s memory.
The View class is one of the most powerful abstractions in the NeXTSTEP
Application Kit. Some of its most useful methods are listed below.
View Instance Method Purpose
addSubview: aView Adds aView as a subview to the View.
display Causes the View to redisplay itself and
all of its subviews by invoking the
drawSelf:: for all of these Views.
drawSelf Implemented by subclasses of the View
:(const NXRect *)rects class to draw themselves. This single
:(int)rectCount method handles displaying on the
NeXTSTEP screen, printing, faxing,
and scrolling. It should be invoked only
by the display method, never directly.
findViewWithTag: Searches a View and all of its subviews
(int)aTag for a given tag.
218 Delegation and Resizing
You can find the rest of the View methods using the Librarian or Header-
Viewer applications.
There are two ways that you can specify a variable that holds a pointer to
an object in Objective-C. You can use the id type to declare a pointer to any
kind of object. Alternatively, if you know in advance what kind of object
you are going to have, you can construct a pointer to the specific object
type specifically by using the object’s class name as a type. Thus, a pointer
to a List object can be declared like this:
id aList;
Strong Typing with Objective-C 219
The second way of declaring aList is called strong (or static) typing.
Strong typing provides better compiler type-checking and the ability to
directly access public variables stored inside the List object. An object’s
public variables are those variables specified after the @public declara-
tion. You access those variables with C’s arrow (->) notation, just as if the
variable aList was a pointer to a structure (which in fact, it is!).
You should generally refrain from using public variables in the classes that
you design, because public variables make your objects less modular and
more difficult to update without causing future problems. Using public
variables also violates the notion of object encapsulation. Public variables
are part of the language specification for those few cases when you need
the utmost efficiency and speed. We will use them later in the GraphPaper
application to access instance variables of an object without having to issue
an Objective-C message.
Below are the variable declarations for the List class copied from the file
/NextDeveloper/Headers/objc/List.h:
@interface List: Object
{
@public
id *dataPtr; /* data of the List object */
unsigned numElements; /* Actual # of elements */
unsigned maxElements; /* Total allocated elts. */
}
Summary
In this chapter we learned about delegation, a system which lets a program-
mer specify objects that should automatically be sent messages when cer-
tain events happen. We used delegation to catch the Application object’s
appDidInit: message, which is the standard technique for specifying code
that should be run when an application is initialized.
Having done this, we modified our Calculator so that it could change its
size using the placeWindowAndDisplay: and sizeWindow:: methods. We
then learned a little bit more about Window and View objects.
In the next chapter, we’ll revisit our ongoing discussion of events – the
basic data type used by NeXTSTEP to keep track of actions initiated by the
user. We’ll then see how events are handled by the Responder class, the
abstract superclass of Application, View, and Window which contains
much of the NeXTSTEP event handling mechanism.
8
Events and Responders
We had our first taste of events in Chapter 4, with the tiny2 program that
displayed little circles near the cursor whenever the mouse was clicked in
the program’s main window. In this chapter we’ll learn more about events
and the chain of objects that NeXTSTEP uses to respond to events. At the
end of the chapter, we’ll see how to catch events from the keyboard in our
Calculator application.
221
222 Events and Responders
Of these, the mouse and keyboard events are the most important when writ-
ing most applications.
What Is An Event?
Simply put, an event is a message that the Window Server (described
below) sends to an application in response to some action taken by the user.
Pressing a key on the keyboard generates an event, as does releasing that
same key. Pressing the mouse button in a window generates an event, as
does releasing the mouse button (and moving the mouse).
Events drive applications. Every action that a user takes gets turned into an
event by the Window Server and sent to the appropriate application. Each
window has an event mask that it uses to tell the Window Server which
events it wants to receive. Event masks will be described in more detail
later.
What actually gets sent to the application is an event record, in the form of
an NXEvent data structure. The Application object stores events in an
event queue, a circular data structure which can hold up to 50 events.
/* keyboard events */
#define NX_KEYDOWN 10 /* key-down event */
#define NX_KEYUP 11 /* key-up event */
#define NX_FLAGSCHANGED 12 /* flags-changed */
/* composite events */
#define NX_KITDEFINED 13 /* appkit-defined */
#define NX_SYSDEFINED 14 /* system-defined */
#define NX_APPDEFINED 15 /* app. defined */
#define NX_TIMER 16 /* timer */
#define NX_CURSORUPDATE 17 /* cursor tracking */
#define NX_JOURNALEVENT 18 /* journaling */
The NXEvent field called location is the point in the window’s coordinate
system where the event took place. You can convert this point from Win-
dow to View coordinates with the View method convertPoint:fromView:
by specifying nil as the second argument.
struct {
/* For appkit-defined, sys-defined, app-defined
* events
*/
short reserved;
short subtype; /* event subtype */
union {
float F[2]; /* for compound events */
long L[2]; /* for compound events */
short S[4]; /* for compound events */
char C[8]; /* for compound events */
} misc;
} compound;
} NXEventData;
Normally you’ll use this data field in methods that respond to keypress
events to find out which key was pressed. That information is contained in
the union element data.key.charCode. You can tell if the key press was an
actual keypress or a repeat (caused by holding down the key) by looking at
the element data.key.repeat.
Events and the Application Object 225
Notice that the Responder methods all have the NXEvent structure as
their only argument; this is the same NXEvent structure that the Window
Server passes to the Application object, unmodified. No Responder (your
own Views included) should ever change the contents of this structure.
Application and Window objects contain their own instance variables (in
addition to the nextResponder instance variable inherited from
Responder) for the processing of keyboard events. An Application object
contains an id pointer called keyWindow, which points to the window that
is currently receiving keyboard events. A Window object contains an id
pointer called firstResponder, which points to the View object inside the
window that should be sent keyboard events when they are received from
the Application object.
These linked Responders are often called the event or responder chain.
Graphically it looks like the diagram in Figure 1 below.
Events and the Application Object 227
Window
Server
Events
Application
keyWindow
Window Window
contentView contentView
Subview
Subview Subview
Subview Subview
firstResponder
Mouse clicks are
keyboard events passed directly to the
are sent to the subview in which they
keyWindow’s take place
firstResponder
(iii) The Window object uses the hitTest: message to determine the sub-
view (i.e., the View object inside the window) in which the cursor was
located when the mouse press occurred.
At this point, what follows depends on if the window is the key window or
not.
(iv) If the window is already the key window, then the event gets sent to the
subview where the mouse press occurred; or
If the window is not the key window, it makes itself the key window. It
then sends the acceptsFirstMouse message to the subview. If the sub-
view returns YES, the event is sent to it. If the view returns NO, no
further processing takes place on the event.
Some Views, such as Button objects, accept the first mouse click. Thus
you can click a button in a window (and have it do something) even when
the window is not the key window. Other Views, such as the Text object,
do not accept the first mouse click. This means that you must first click in
the window to make it the key window before you can successfully click
on an object inside the window. This prevents the cursor from moving
inside a Text object in a window when you click in the window. As an
example, try the following in the Edit application: open a file in Edit, acti-
vate another application, and then click in the Edit window. The cursor
does not move to where you clicked because the Text object inside the Edit
window returns NO when sent the acceptsFirstMouse message from the
window.
If the Window object sends the event to the subview, the following addi-
tional processing takes place:
(v) If the subview cannot process the event, it sends the event to its next-
Responder. The nextResponder of a View object is by default its
superview – the View that contains the View receiving the event.
(vi) Inevitably, if none of the subviews can process the mouse click, it gets
sent to the Window’s contentView. If the contentView cannot process
the event, the event gets sent to the contentView’s nextResponder –
the Window itself.
For the most part, all of this handling of responders and first responders is
automatic – you don’t have to worry about it unless you are trying to do
something non-standard.
Mouse events other than mouseDown: are handled a little differently. The
mouseUp: event is sent to the View object that was sent the corresponding
Events and the Application Object 229
mouseDown: event, regardless of the position of the mouse when the but-
ton is released. This lets buttons that have been pressed know when they
are released.
On the other hand, if the Command key is down, the Application object
sends the commandKey: message to every Panel object (including
230 Events and Responders
menus) in its window list, until one of the Panels returns YES. If one of the
Panels returns YES, then the Panel translates the commandKey: message
into a performKeyEquivalent: message for its Views. The per-
formKeyEquivalent: message is then passed down the view hierarchy
until it is sent to a View which will respond to the message. This is how
NeXTSTEP makes tear-off menus work.
Now click anywhere inside the white Text object area in the Controller.h
window and a cursor will appear at the location where you clicked. With
this click you have made the Text object the first responder, and the Win-
dow’s firstResponder instance variable will be set accordingly. If you
press the “k” key again, then a “k” will appear at the cursor location.
After you clicked in the Text object area, the Window sends the accepts-
FirstResponder message to ask the Text object whether it wants to
become the first responder. If the text displayed by a Text object is editable
or selectable, as it is in Edit windows, then the Text object answers YES.
The Text object’s acceptsFirstResponder method would look something
like this:
- (BOOL) acceptsFirstResponder
{
return YES;
}
Events and the Application Object 231
After the Text object answers YES, the Window tries to make it the first
responder by sending the makeFirstResponder: message to itself (self)
with a pointer to the Text object as the argument. The makeFirstRe-
sponder: method sends the resignFirstResponder message to the current
first responder. If the current first responder refuses to relinquish its role,
then it returns nil and the first responder doesn’t change. In this example
the Window is the current first responder and it returns self, which means
that it will relinquish its role as first responder.
Now suppose you click in the Text object in the (other) Window object,
the one displaying the Controller.m file contents. It receives a mouse-
down event from Edit’s Application object and becomes the key window.
If you then press the “k” key, you will hear the system beep again because
this Window is the first responder. A second click in the Text object area
will make it the first responder and a cursor will appear.
This isn’t exactly true. Instead of having the Slider object send the mes-
sage directly to the TextField object, NeXTSTEP uses Control’s sendAc-
tion:to: method to send the message (Slider is a subclass of Control and
therefore inherits the sendAction:to: method). The reason for this is to
allow some actions to be context sensitive – to change their behavior
depending on which View and Window are currently selected.
232 Events and Responders
For example, suppose you had a simple window with three text fields, as in
Figure 4 below. Suppose also that you chose the Edit→Cut menu com-
mand. NeXTSTEP needs some way of sending the cut: action message to
the object containing the piece of text that you’ve selected. If the Cut menu
cell had a specific target, say the first field, then the cut: message would
often get sent to the wrong place.
If you look at the target of the Cut menu cell within Interface Builder,
you’ll see that it has been set to the First Responder icon (see Figure 5
below). Furthermore, if you look at the Connections Inspector in Figure 6
below, you’ll see a wide choice of actions to which a First Responder
object can respond. This is all especially confusing because there is no
First Responder class. We describe what is happening below.
SEL action;
id _view;
}
234 Events and Responders
The search continues until an object is found that implements the action
method, and the action method returns a value other than nil. If no object
handles the message, the message is ignored.
Let’s again look at the Calculator_main.m file, which contains the main()
function used by our Calculator application.
#import <appkit/Application.h>
[NXApp free];
exit(0);
}
The first line in the main() function creates an instance of the Application
object. The second line loads the nib section Calculator.nib from the exe-
cutable, specifying the newly-created Application object as the nib’s
owner. (This is why Interface Builder uses the phrase File’s Owner instead
of Application in its File window: it’s possible that something other than
an Application object is specified in the loadNibSection:owner:with-
Names: method.)
The third line is the most important of all for events. It sends the run mes-
sage to the Application object, NXApp. That’s where a lot of the magic of
NeXTSTEP begins.
• If there is data pending at a watched file descriptor, read it and call the
appropriate function.
• Repeat until the stop: message is received.
The most common way of breaking out of the Application’s main event
loop is by sending a terminate: message to NXApp. Here’s what happens
when the terminate: message is received:
(i) If the NXApp object has a delegate that implements the appWillTer-
minate: message, this message is sent to the delegate.
(ii) If the delegate’s appWillTerminate: message returns nil, the termi-
nate: method is aborted, and the main event loop continues to run.
(iii) Otherwise, the application is terminated.
The fact that NeXTSTEP handles the main event loop for the programmer
is one of the primary differences between programming in NeXTSTEP and
programming in other window-based environments. By handling most
events automatically, the programmer is freed from this tedious task, which
in turn makes programs behave in a more reliable and unified fashion.
Nevertheless, NeXTSTEP does allow you to write your own event loop or
take over the event loop while something out-of-the-ordinary is happening.
Typically you would do this for a special purpose that isn’t handled well by
the Application Kit. For example, Interface Builder uses its own event loop
when you Control-drag a connection from one object to another; the event
loop exits when the mouse button is released.
Hash Tables
We will use a HashTable object to determine if incoming keyboard events
match a button in the window. The HashTable class is provided by NeXT
as an Objective-C common class. A HashTable object stores key and value
pairs. When you store a value in a HashTable object, you provide a key.
Later, when you present the HashTable object with the same key, you get
the value back. Both the key and value can be any 32-bit value, including
id, int, void * and char *. (To hash strings, you must use the NXUniqueS-
tring() function to turn the string into a unique 32-bit number.)
tained in the window that have a single character title. The value stored in
the hash table will be the id of the button.
@interface CalcWindow:Window
{
id keyTable;
}
- findButtons;
- checkView:aView;
- checkButton:aButton;
- checkMatrix:aMatrix;
@end
The keyTable instance variable will point to the HashTable object. We’ll
use the four new methods to search the Calculator window for all of the
buttons, as follows:
Method Purpose
findButtons Start searching the window’s content View.
checkView: aView Search aView object. If a Button object is
found, check it with checkButton:. If a
Matrix is found, check all of its buttons
with checkMatrix:. If a subview is found,
check it recursively with checkView:.
1. The only required imports are shown below. See CalcWindow.m for details.
#import <appkit/Matrix.h>
#import <appkit/Button.h>
#import <objc/HashTable.h>
#import <objc/List.h>
#import <NXCType.h>
240 Events and Responders
Method Purpose
checkButton: aButton Check aButton to see if it has a single-
character title. If so, add the button to the
HashTable object with the title as its key.
checkMatrix: aMatrix Check each of the buttons inside aMatrix
by repeatedly invoking the checkButton:
method.
The checkView: method sends the isKindOf: message to the aView object
(Matrix, Button, or other) that is passed as an argument to determine its
class. (The class factory method forces a class object to return its id.) If the
aView object is of the Matrix or Button class then the checkMatrix: or
checkButton: method is invoked. If not, then the checkView: method
recursively invokes itself for all of the subviews contained within the View.
Actually, the objects that are passed to checkView: are instances of sub-
classes of the View class, not instances of View itself. But since they
inherit from the View class we still refer to them as Views.
The checkButton: method checks a button to see if its title is a single char-
acter. If it is, the button is stored in the keyTable object (which is a Hash-
Table object that we’ll create when the CalcWindow object is initialized,
as we’ll see a bit later).
- checkButton:aButton
{
const char *title = [aButton title];
First this method gets the list of cells (Buttons, in our example) that are
contained in the Matrix. Then it invokes the checkButton: method for
each one. Now we have the four methods we need to set up the HashTable
object with key-value pairs.
if (button) {
return [button performClick:self];
}
return [super keyDown:theEvent];
}
This method is invoked only when a keyboard event is not otherwise han-
dled by the Responder chain. It sends the valueForKey: message to the
HashTable object (keyTable) to obtain the id for the Button whose title is
the same as the keyboard character (data.key.charCode) that was typed.
Catching Keyboard Events for the Calculator 243
But which method is the designated initializer? Normally, it’s the method
that has the most arguments. But the Window class is different. The desig-
nated initializer is the second method listed. That’s because the initCon-
tent:style:backing:buttonMask:defer: method was designated as the
designated initializer before NeXTSTEP supported multiple screens (see
the screen: argument in the last initialization method).
244 Events and Responders
All this method does is to create a HashTable object (keyTable) with inte-
ger (“i”) keys and then sends the identical message to its superclass (Win-
dow) to actually do the work of initializing the CalcWindow object. This
method is invoked automatically whenever an instance of the CalcWin-
dow class is created.1 Now it’s time to put all this code into the CalcWin-
dow.m file.
9. Insert the code we’ve discussed above into CalcWindow.m. In
particular, insert the implementations of the following six methods:
findButtons
checkView:
checkButton:
checkMatrix:
keyDown:
initContent:style:backing:buttonMask:defer:
1. The keyTable could have been created on-demand in the findButtons method
with a piece of code like this:
if (!keyTable) {
keyTable = [[HashTable alloc] initKeyDesc:”i”];
}
This style of on-demand allocation of resources is called lazy allocation. Lazy allo-
cation generally improves performance, because it defers creating objects until they
are needed (recall our discussion of separate nib files). If they never are needed,
they never get created. “It is always a good thing and something that developers
should be encouraged to do,” says Ali Ozer of NeXT Computer, Inc.
We didn’t use lazy allocation in our example because we wanted to show how to
create a designated initializer for the CalcWindow subclass.
Catching Keyboard Events for the Calculator 245
@implementation Controller(ApplicationDelegate)
-appDidInit:sender
{
id kwin = [keyPad window];
[self setRadix:radixMatrix];
[self clearAll:self];
return self;
}
@end
If your Calculator doesn’t work properly, make sure that you’ve properly
subclassed the Calculator’s Window and properly set up the CalcWindow
class definition.
246 Events and Responders
Summary
In this chapter, we took a close look at events and the responder chain, and
saw how they interact with the Window object. We’ll periodically revisit
these topics throughout the rest of the book, looking more closely at how
events interact with the View class.
This chapter also marks the end of the evolution of our Calculator applica-
tion. In the following chapter, we’ll learn more about the system software
that underlies NeXTSTEP. Then in Chapter 10 we’ll start on a new applica-
tion – MathPaper – which we’ll use to learn about interprocess communi-
cation and controlling multiple main windows.
9
Mach and the Window Server
247
248 Mach and the Window Server
The Microprocessor
As of this writing, most computers running NeXTSTEP use the Motorola
68040 microprocessor, or central processing unit (CPU), to read and exe-
cute instructions and read and process data from the computer’s memory.
But since very little of Mach and NeXTSTEP are written in the 68040’s
native assembly language, NeXTSTEP can easily be ported to other micro-
processors simply by recompiling the system and rewriting a small number
of device drivers. Soon you should be able to get NeXTSTEP for comput-
ers based on the Intel 486 microprocessor (as well as others, we believe).
This book, like the NeXTSTEP application development environment, is
hardware independent; the information in it applies to NeXTSTEP running
on any platform.
The microkernel implements virtual memory, a system that uses the com-
puter’s hard disk to simulate a much larger block of random-access mem-
ory (RAM). Virtual memory is transparent to running programs (although
it can slow them down considerably). A program that needs a 10 megabyte
block of memory simply allocates a 10 megabyte block of memory; the
microkernel automatically shuffles data from the computer’s internal mem-
The Mach Operating System 249
ory to the hard disk and back, as necessary. This is called swapping or pag-
ing.
NeXTSTEP’s UNIX environment also contains all of the device drivers for
managing the computer’s hardware devices, including the keyboard, the
screen, the serial ports and the SCSI bus. UNIX is an intimate part of
NeXTSTEP, and it is therefore important to understand how it manages
users and processes.
250 Mach and the Window Server
UNIX also uses some special system “users” for a variety of special pur-
poses. There is root, also called the super user, which performs accounting
and low-level system functions. The uucp user manages the UUCP (UNIX
to UNIX copy) system. These users are not actually people who log in, but
merely different UIDs used to run daemons (background processes - noth-
ing evil about them) with different kinds of privileges.
The Mach kernel assigns every running process a unique number called the
process identifier, or PID. The first process that runs when the computer
starts up is called init; this process is given the number 1. Any process can
fork, and by doing, create a new process. All of the processes on your com-
puter are descendents of process number 1.
Mach process numbers can range from 1 to 65535; the microkernel guaran-
tees that no two active processes will ever have the same number.
Mach-O: The Mach Object File Format 251
Each Mach-O segment is used to store a different kind of data. The table
below lists some of the segment names used by NeXTSTEP; you can also
create your own. (There are two underscores before each segment name.)
The Mach size command displays the size of segments and sections in a
Mach-O executable file. For example, take a look at the segments and sec-
tions in the Librarian application executable file by entering the following
commands (in bold) in a shell window:
localhost> cd /NextApps/Librarian.app
localhost> size -m Librarian
Segment __PAGEZERO: 8192
Segment __TEXT: 122880
Section __text: 104472
252 Mach and the Window Server
... ...
Mach provides a number of other tools, otool and segedit in particular, for
manipulating the contents of a Mach-O executable file. You can use otool
to print the contents of Mach-O files, and segedit for assembling new
Mach-O files or taking apart old ones.
For further information on all of these commands, search the on-line docu-
mentation and header files in the Librarian and HeaderViewer applications.
Make sure both the NeXT Developer and UNIX Manual Pages “books”
are selected in Librarian’s main window (use Shift-click to select the sec-
ond book).
The Window Server manages the screen (or screens), the keyboard, the
mouse and any printers. It assures that programs draw in their own rectan-
gular piece of real estate on the screen, and sees to it that events destined
for one program don’t accidentally get sent to another. The Window Server
frees you from having to worry about interactions between your program
The Window Server and Display PostScript 253
and other programs that might be running simultaneously. For all intents
and purposes, you can design your program as if it is the only one running.
The Window Server also isolates you from the particulars of the com-
puter’s hardware. If your program is running on a NeXTSTEP computer
that has a different sized screen, or a slightly different keyboard or mouse,
these differences are automatically hidden from you by the Window Server.
PostScript
PostScript is a device-independent graphics description language which
handles all aspects of line drawing, typesetting and image presentation on
the computer’s screen and printer. Device-independent means that Post-
Script hides all differences in resolution from your program: to draw a line,
you simply tell the PostScript interpreter to draw a line. PostScript auto-
matically figures out which pixels on the screen (or dots on the printed
page) should be turned on or turned off.
PostScript also handles output attributes such as line width and fill color. If
you want to draw a dark gray line, you simply set the color to dark gray and
draw the line. If the output device is black-and-white only, PostScript will
automatically dither or halftone the line as necessary (dithering and half-
toning are techniques for showing continuous-tone images on devices
which can only display a few shades of color or black-and-white). If your
output device can handle gray tones, PostScript automatically chooses val-
ues of gray according to what you selected.
Because these communications are made through Mach ports, they can be
made over the network, from different computers running NeXTSTEP. In
other words, a NeXTSTEP application running on one computer can dis-
play its windows on a different computer running NeXTSTEP, over the
network. (To do this, you need to launch the application program from the
command line and specify the option -NXHost. Make sure that the account
on the second computer has set the “Public Window Server” switch in the
Preferences application.)
You can find out which shared libraries an application depends on by using
the otool command. Enter the following commands in bold to find out
which shared libraries the Librarian application uses:
localhost> cd /NextApps/Librarian.app
localhost> otool -L Librarian
/usr/shlib/libIndexing_s.A.shlib (minor version 1)
/usr/shlib/libMedia_s.A.shlib (minor version 12)
/usr/shlib/libNeXT_s.C.shlib (minor version 57)
/usr/shlib/libsys_s.B.shlib (minor version 55)
localhost>
Seeing All the Processes 255
Enter ps -aux in a shell window. Your listing should contain many of the
same programs as in the listing in Figure 1 below, which was generated
while running NeXTSTEP 2.1. Some of the processes running on your
computer are bound to differ; it depends on which programs you are run-
ning and how your system is configured. Some of the commands in the
COMMAND field in Figure 1 were truncated due to the 80 column width
in our shell window; make your window wider and enter ps -auxww if you
want to see the entire command.
The table below contains descriptions of the meaning of the different fields
in the listing in Figure 1.
Field Meaning
USER username of the user who owns the process, usually
root for processes run by the system or your username
for processes run by you
PID process identifier of each process
%CPU percentage of the CPU time that the process is using
%MEM percentage of physical memory the process is using
VSIZE amount of virtual memory that the process is using
RSIZE amount of process that is resident in physical memory
TT terminal being used by the process; a “?” means that the
process is not associated with any terminal
STAT status of the process: R is running, S is stopped, W is
waiting, N is “niced” (running with reduced priority)
TIME length of time the process has been running
COMMAND command that ran the program
You can look up the ps command with the Librarian application to learn
further details concerning this command’s output.
256 Mach and the Window Server
Process Function
ps -aux The ps command that was run to dis-
play the process listing.
console (WindowServer) The Window Server console process,
which captures all output to /dev/
console.
/LocalApps/FrameMaker.app FrameMaker, which we used to write
this book.
- (csh) The C shell.
/usr/lib/NextStep/Work The Workspace Manager application.
Its full name is /usr/lib/NeXTSTEP/
Workspace.app/Workspace.
/usr/etc/kern_loader -n The Mach Kernel loader.
/usr/etc/syslogd The system logging daemon.
/usr/etc/nmserver The Mach name server.
(portmap) The portmapper, for RPC (remote
procedure calls).
(nibindd) The NetInfo (network administrative
information) binding daemon,
responsible for finding, creating, and
destroying NetInfo servers.
/usr/etc/netinfod local The NetInfo server for the local Net-
Info domain, executed by nibindd.
(lookupd) The lookup daemon, which makes
NetInfo run faster.
(mach_init) The program that starts up Mach.
<mach-task> An internal Mach process.
(inetd) The inet daemon, which launches
Internet servers.
-accepting connections (s The sendmail daemon, which
receives mail from the network.
/usr/lib/lpd The line printer daemon, which han-
dles printing.
258 Mach and the Window Server
Process Function
(pbs) The NeXTSTEP Pasteboard server,
which coordinates sharing of data on
the various pasteboards.
(autonfsmount) The auto NFS mounter, which auto-
matically mounts NFS filesystems
when requested.
(cron) A program that automatically runs
programs listed in the file /usr/lib/
crontab at predetermined times.
update A program which executes the
sync(2) system call every 30 seconds
to assure file system consistency.
(WindowServer) The NeXTSTEP Window Server
itself.
<mach-task> Another internal Mach task.
/usr/etc/pbs -a Another part of the NeXTSTEP
Pasteboard server.
(appkitServer) An internal server used by the NeXT-
STEP Application Kit.
(npd) The NeXT printer filter that handles
local printing. Its full name is
/usr/lib/NextPrinter/npd.
/NextApps/Edit.app/Edit - The NeXTSTEP Edit program.
/NextApps/Mail.app/Mail - The NeXTSTEP Mail program.
/NextLibrary/Services/NeX The NeXTSTEP speller program. Its
full name is /NextLibrary/Services/
NeXTspell.service/nextspell.
/NextDeveloper/Apps/Proje The NeXTSTEP Project Builder
application.
/NextApps/Preferences.app/ The NeXTSTEP Preferences pro-
gram.
/NextApps/Librarian.app/ The NeXTSTEP Librarian program.
Its full name is /NextApps/
Librarian.app/Librarian.
/usr/etc/init -xx The init program.
front
end
back
end
10
MathPaper and
Multiple Windows
In this and the following several chapters, we are going to start over and
build a new, more sophisticated calculator-like system called MathPaper.
MathPaper will manage multiple windows, use fonts to convey informa-
tion, and use interprocess communication to make a request to another pro-
gram to do the actual calculation. In writing it we will learn a lot more
about the workings of the NeXTSTEP Application Kit.
MathPaper
When we’re done MathPaper will be a scratch pad mathematics application
that looks like a text editor: it will display a little text window into which
you can type. The neat thing about MathPaper is that when you hit the
Return key, the application will automatically calculate the value of what
you’ve typed and display the result. Figure 1 below contains an example of
a main window of the running MathPaper application.
259
260 MathPaper and Multiple Windows
solving back end can be used by more than one application program – as
long as its interface is well-documented.
In the remainder of this chapter we’ll build the two major modules of
MathPaper: the Evaluator back end which we’ll test in a UNIX shell win-
dow, and the front end which we’ll test in the workspace. In the next chap-
ter we’ll hook these two modules together with an object called Process
that manages UNIX subprocesses. In the chapter following that,
Chapter 12, we’ll arrange for each piece of math paper to save its data into
a file and then read the data back from the file when the data file icon is
double-clicked.
The “big picture” of the MathPaper application with three open math paper
windows (all like the window in Figure 1) is shown in Figure 2 below. It
indicates that the MathController object creates one PaperControl object
for each window. Each PaperControl object will create its own Process
object to manage a copy of the Evaluator back end.
MathController
(8-76+32) / 3.2
The Evaluator writes its output directly to standard output, which right
now displays on the screen (in a shell window). When we run the Evaluator
as a subprocess from MathPaper, the Evaluator’s standard output will be
returned to the MathPaper application.
In the rest of this section and the next, we’ll be discussing how to build the
back end. These sections contain material which is not essential to under-
standing NeXTSTEP programming and may be skipped if you have an
executable copy of the Evaluator program (the Evaluator file must be cop-
ied into your ~/Apps directory). If you don’t have an executable copy of
the Evaluator program, then you can (blindly?) follow the steps in the next
section to create one without having to fully understand what’s going on.
All you really have to know to continue with MathPaper is that the Evalua-
tor will perform the actual calculations for MathPaper and will run as a
separate UNIX process. So, if you plan to skip this material, you should
continue with the section “Building the MathPaper Front End” on
page 268.
The task of the Evaluator back end breaks down into two parts: lexical
analysis and parsing. Lexical analysis involves reading the input stream
and determining which characters correspond to numbers and which corre-
spond to operations. The character stream is then turned into a stream of
tokens, or symbols. For example, the input to Evaluator shown above
would generate the following token stream:
<1> <+> <2> <*> <3> <+> <4> <newline>
<2> <+> <5> <*> <sin> <(> <3> <)> <newline>
<9> </> <4> <newline>
<3> <+> <newline>
<(> <8> <-> <76> <+> <32> <)> <3.2> <newline>
The second part of the back end is the parser. The parser reads the token
stream generated by the lexical analyzer, performs the requested math, and
prints the correct result.
The Evaluator Back End 263
Parsers and lexical analyzers are not trivial programs to write. Fortunately
UNIX comes with two programs for constructing lexical analyzers and
parsers from (relatively) simple input files. These program-generating-pro-
grams are called lex and yacc. It’s not important that you understand how
lex and yacc work in order to understand the MathPaper program. The only
thing that really matters is that, using lex and yacc, we are able to build a
relatively powerful and reliable back end with a very small amount of
work.
Yacc reads an input grammar file (in this case, the file grammar.y) that
describes a particular grammar and generates two C source code files:
y.tab.h and y.tab.c. Lex reads the include file y.tab.h and a second file (in
this case, the file rules.lex) that describes a set of lexical rules and gener-
ates a C source file called lex.yy.c. The source code in y.tab.c and lex.yy.c
is then compiled and linked to form the Evaluator program.
We get a lot of power by using yacc and lex. Not only do we get a full-fea-
tured numerical evaluator which properly interprets parentheses and order
of evaluation (for example, evaluating multiplication before addition), but
we also get a system to which it is easy to add new formulas and rules. For
example, adding a new function to the Evaluator simply requires adding
two new lines, one to the set of rules and one to the grammar. We’ll be
doing much of our work from the csh command line, so make sure your
dock contains NeXTSTEP’s Terminal application.
264 MathPaper and Multiple Windows
install: Evaluator
strip Evaluator
cp Evaluator $(HOME)/Apps
clean:
/bin/rm -f Evaluator lex.yy.c y.tab.?
y.tab.h: grammar.y
yacc -d grammar.y
y.tab.c: grammar.y
yacc grammar.y
This Makefile contains the targets install, clean, and so forth. Make tar-
gets were explained in Chapter 2.
3. Using an editor, create a file called rules.lex in your Evaluator
directory which contains the following:
%{
#include "y.tab.h"
#include <stdlib.h>
%}
%%
"\n" return('\n');
[0-9]*("."[0-9]*("e"[-+][0-9]+)?)? \
{yylval.dval = atof(yytext); return(NUMBER);}
sin return(SIN);
cos return(COS);
tan return(TAN);
Building the Back End 265
asin return(ASIN);
acos return(ACOS);
atan return(ATAN);
sinh return(SINH);
cosh return(COSH);
tanh return(TANH);
asinh return(ASINH);
acosh return(ACOSH);
atanh return(ATANH);
mod return(MOD);
ln return(LN);
log return(LOG);
sqrt return(SQRT);
pi return(PI);
[ \t] ;
. {return(yytext[0]);}
%%
4. Using an editor, create a file called grammar.y in your Evaluator
directory which contains the following:
%{
#include <libc.h>
#include <math.h>
int printingError = 0;
%}
%start list
%union
{
int ival;
double dval;
}
list : stat
| list stat
;
while (!feof(stdin)) {
yyparse();
}
exit(0);
}
Unlike most lex and yacc programs, Evaluator contains all of the auxiliary
C code that it needs to run in the grammar.y file. Yacc automatically
passes this code along to the C compiler with the parser that it generates.
5. Open up a UNIX shell window by double-clicking the Terminal icon.
6. Compile the Evaluator with the make utility by typing make in the
Terminal shell window. What you should type is indicated in bold:
localhost> cd ~/Evaluator
localhost> make
yacc grammar.y
yacc -d grammar.y
lex rules.lex
cc -O -o Evaluator y.tab.c lex.yy.c -ly -ll -lm
localhost>
10+20
30
sin(2*pi)+cos(4*pi)
1
^Clocalhost>
(Type Control-c to exit the program. The “^C”, which indicates where you
should type Control-c, will show up in the shell window where indicated.)
8. Install the Evaluator program in your ~/Apps directory by typing
make install as follows:
localhost> make install
strip Evaluator
cp Evaluator /simsong/Apps
localhost>
Stripping an executable file removes its symbol table, which has the effect
of making the program dramatically smaller. In this case, the size of the
Evaluator executable dropped from about 53,000 bytes to about 16,000.
(Stripping the executable also makes it harder to debug the resulting exe-
cutable, so be careful!)
Congratulations! You’re finished with the back end! If you don’t under-
stand it all, don’t worry too much. All you have to know to continue with
MathPaper is that the Evaluator will perform the actual calculations for
MathPaper and will run as a separate UNIX process.
By having a separate nib for the MathPaper window, we can create new
instances of the window simply by loading the nib multiple times. We’ll
see how this works later in this chapter.
The Menus palette together with descriptions of its menu cells can be seen
in Figure 3 below. Although you can change the name of any menu, many
of the menu cells in IB’s Menus palette automatically link (connect) to
Application object instance methods. For example, if you drag the Win-
dows menu cell into your application, the Application object will automat-
ically update the Windows submenu as you create and delete main
windows within your application. (Isn’t object-oriented programming won-
derful?!) You won’t get this behavior if you simply drag out the menu item
called Submenu and change its name to Windows.
The Edit submenu you prefer will depend on your application. If your
users are going to be entering text, you’ll probably want to delete the first
Edit submenu and drop in the extended one, because it will give the user
several useful capabilities for free. If Cut and Paste are going to be used
1. If you’re using NeXTSTEP 2.1, refer to Chapter 5 and Chapter 6 for details on
how to create a project, auxiliary nib file, etc.
270 MathPaper and Multiple Windows
selects Menus
Palette
discussed in
Chapter 6
only for graphical objects, however, the other Edit submenu commands
probably aren’t needed. For our MathPaper application, we’ll keep the
default Edit submenu.
You can cut and paste menus and submenus with Interface Builder’s
Edit→Cut and Edit→Paste commands. If you want a particular submenu
(like the extended Edit submenu) but you don’t want a particular submenu
command (like Edit→Paste As), simply cut it out of the submenu. (You
Building the MathPaper Front End 271
can also delete menus by pressing the backspace key when a menu cell is
highlighted, so be careful!)
5. In IB, select the Info cell in the MathPaper menu by clicking it.
MathPaper’s menu should look like the one at the left of the page.
6. Type Command-x (or choose IB’s Edit→Cut) to remove this
command from the menu. (Alternatively, you can disable the button
with the Menu Button Inspector. This way, you’ll remember to
reenable the button and write the corresponding code before you
deliver your application!)
Every application should have an Info menu command; we’ll be add-
ing ours in Chapter 12, where we’ll also create a fancy Info panel.
7. Drag Document, Windows, and Services submenus from the Menus
palette and drop them in MathPaper’s main menu so that it looks like
the menu system in Figure 5 below. Make sure you follow the
NeXTSTEP menu guidelines and place the cells in the main menu
positions shown in Figure 5.
The Windows submenu has commands for dealing with windows – bring-
ing them to the front of the window display list, miniaturizing them, and
closing them. When your program is running, your Windows submenu will
include additional menu cells for each standard window (but not panel) that
your program creates: this is done automatically for you by the Applica-
tion object which keeps a list of the application’s windows. (Yes, object-
272 MathPaper and Multiple Windows
Don’t worry that your application doesn’t have any windows in it right
now. We’ll be adding a window in the paperwindow.nib file.
10. Subclass the Object class and create a new class called
MathController (open IB’s Classes view, select Object, drag to the
Subclass operation, and change the name of the new class).
11. Add an outlet called newCalc and two actions called appDidInit: and
newCalc: to the MathController class (select the MathController
class in the Classes view and then add the outlet and actions in the
Class Inspector).
The newCalc outlet will be used to hold the id of a new piece of math
paper after it’s created. The newCalc: action method will create that piece
Building the MathPaper Front End 273
nib (and thus creates a new math paper window) will be invoked indirectly
by choosing Calculator→New Calculator (or by typing Command-n). We
now continue with the project and set up the paperwindow nib.
17. Choose IB’s Document→New Module→New Empty command to
create a new nib module. You’ll see a new File window without a new
menu or window, since it’s a new empty nib.
18. Type Command-s and save this new nib in the file named
paperwindow.nib in your MathPaper/English.lproj directory. Insert
paperwindow.nib into the MathPaper project; PB will show that
paperwindow.nib has been added to the project.
19. Command-double-click the IB icon to make IB the only visible
application, and then double-click the NeXT icon to make your File
Viewer visible as well. Also, make sure that the paperwindow.nib file
is active by clicking in its File window (so the remaining steps in this
section affect paperwindow.nib and not MathPaper.nib).
Next we’ll set up the PaperControl class, which will spawn a new control-
ling object for each new MathPaper window.
23. Subclass the Object class in paperwindow.nib and create a new class
called PaperControl.
24. Add the following four outlets to the PaperControl class:
Building the MathPaper Front End 275
proc – to hold the id of the Process object (see the next chapter)
scroller – to hold the id of the ScrollView object (see below)
text – to hold the id of the Text object inside the ScrollView
window – to hold the id of the MathPaper window
25. Unparse the PaperControl class. Respond Yes to the questions in
both attention panels. PB shows the new class in its Files view.
26. Back in IB, Instantiate a PaperControl object. Having this instance
object in the File window causes NeXTSTEP to create a new
PaperControl instance every time the nib is loaded.
27. Click the window icon button at the top of IB’s Palettes window to
view the Windows palette.
28. Drag a window icon from IB’s Windows palette and drop it in the
workspace; it will expand to a full window. Note the new window icon
in the paperwindow.nib File window (Objects view).
29. Resize the new window so it’s about 3 inches wide by 4 inches tall.
30. Change the title of the window to be “MathPaper %d” in the Window
Attributes Inspector. We will use the window’s title as a format string
in a sprintf C language statement to number the MathPaper windows.
NeXTSTEP itself does not directly interpret the %d.
31. Set the window options Free When Closed and Deferred and unset
the others in the Window Attributes Inspector.
The last step above will reduce memory requirements for your application
in two ways. First, when the window is closed the memory for it will be
freed by the Window Server. Second, the Window Server will defer allocat-
ing memory for the window until the window is displayed on the screen.
We’ll call this object a Text ScrollView, and not just a ScrollView (as
many NeXTSTEP programmers do), because the object on the palette is
276 MathPaper and Multiple Windows
selects
TextViews
palette
Browser Text
ScrollView
35. Select the Text ScrollView by clicking it. Then drag to Size in the
ScrollView Inspector (or type Command-3) to bring up the Size
Inspector as in Figure 10 below.1
The ScrollView Size Inspector lets you specify how a View object should
change when the view’s containing view, or superview, is resized. In this
case, we are specifying the resizing characteristics of the Text ScrollView
when the window’s content View is resized (which occurs whenever the
window is resized). In Figure 10 the small square represents the selected
View, whereas the large square represents the superview.
36. Click the lines in the Autosizing area in the Size Inspector so it
contains the “springs” as in the screen shot at the bottom of Figure 10.
We’ve already made two connections in the MathPaper.nib file. Now let’s
wire up the outlets in the paperwindow.nib file.
37. Wire up the newCalc, window, scroller, and delegate outlets in the
nib paperwindow.nib as specified in Figure 11 below. In this case the
File’s Owner icon represents the MathController instance object
which loads copies of the paperwindow nib.
When connecting the scroller outlet, release the Control-drag action in
the dark gray vertical scroller area to connect the scroller outlet to the
ScrollView and not the Text object within the ScrollView. You can see
the class of the destination objects near the bottom of the Connections
Inspector, as in Figure 12 below.
1. In NeXTSTEP 2.1, it’s called the Autosizing Inspector. Also, the Frame size is
displayed in the Miscellaneous Inspector.
278 MathPaper and Multiple Windows
newCalc outlet
window outlet
scroller outlet
delegate outlet
The connections involving the window and scroller outlets let the Paper-
Control instance send messages to the MathPaper Window and Scroll-
View objects, respectively. We’ll discuss the connection which makes
PaperControl instance the Window object’s delegate in the next section.
Source Object Source Outlet Target
File’s Owner newCalc PaperControl instance
PaperControl window the window
PaperControl scroller the scroller
Window delegate PaperControl instance
Window Delegates
Like the Application object, a NeXTSTEP Window object can have a del-
egate object that is automatically notified in response to certain conditions.
You can also use a Window delegate to control an on-screen window’s
behavior. Each Window object has its own delegate outlet. All of the Win-
dow objects in an application can have the same delegate object, or you can
specify a different delegate (possibly with a different class) for each Win-
dow. As with the Application object, you can change a Window object’s
delegate at run-time by sending it the setDelegate: message.
Window Did messages are sent to the Window’s delegate after certain
changes in the window take place. They keep your application apprised of
what is happening to its windows. The table below summarizes them:
Will delegate methods are sent to the delegate before a change in a window
takes place. Will methods therefore let you control the behavior of the win-
dow. Here are a Window object’s Will methods:
Did messages are sent to the delegate object after an action has occurred,
while the Will messages are sent just before an action is about to occur.
Thus Did messages allow the delegate to coordinate actions after certain
actions occur. On the other hand, Will methods provide the delegate with a
chance to block or modify the impending action.
@interface MathController:Object
{
id newCalc;
float offset;
int calcNum;
}
- appDidInit:sender;
- newCalc:sender;
@end
@implementation MathController
- appDidInit:sender
{
[self newCalc:self];
return self;
}
- newCalc:sender
{
id win;
if ([NXApp loadNibSection: "paperwindow.nib"
owner: self] == nil) {
return nil;
}
if ([newCalc setUp]) {
win = [newCalc window];
if (win) {
NXRect frame;
char buf[256];
@end
284 MathPaper and Multiple Windows
The newCalc: method loads the paperwindow nib to create the new math
paper window and PaperControl instance. Each time this nib is loaded,
the MathController instance variable newCalc is set to be the id of the
PaperControl instance that was just created. The old value of this instance
variable is lost. It turns out that we really don’t care about the old value
because the MathController doesn’t need to send any messages to Paper-
Control instance after that piece of math paper is set up.
After the paperwindow nib is loaded, the newCalc: method sends the
window message to the PaperControl instance that was just loaded. The
window method (which is shown below) returns the id of the paperwin-
dow.nib window. If the window exists, we find where its frame is and
move it over a bit (by offset) so MathPaper’s windows are not all in the
same place on the screen. We then set up the window’s title to distinguish it
by number (calcNum), and finally display the window on the screen.
NX_X() and NX_Y() are macros which return the X and Y coordinates of
the frame rectangle in screen coordinates.
@interface PaperControl:Object
{
id proc;
id scroller;
id text;
id window;
Building the MathPaper Front End 285
- setUp;
- window;
- windowWillClose:sender;
@end
Below we show the implementation of the three new methods, setUp, win-
dow, and windowWillClose: that we made in PaperControl.h. The dele-
gate notification method windowWillClose: will be invoked whenever the
PaperControl window is about to close.
41. Insert the three new method implementations in bold below into
PaperControl.m.
#import "PaperControl.h"
@implementation PaperControl
- setUp
{
return self;
}
- window
{
return window;
}
- windowWillClose:sender
{
[sender setDelegate: nil];
[self free]; /* free PaperControl object */
return self; /* Window will free itself on close */
}
@end
The window accessor method allows another object to get the id of the
window instance variable stored inside a PaperControl object. The setUp
method sets up a PaperControl object after the nib has been loaded. We’ll
show you the code for it in the next chapter. (We can’t do our initialization
in a PaperControl init method because it might be invoked before the rest
of paperwindow.nib is loaded.)
user tries to close the window. We use this as a signal to free the memory
associated with the object. Before we do this, it is important to reset the
sender’s delegate to be nil, so the object doesn’t receive any messages
after it is freed. (If you take out the [sender setDelegate:nil] statement, the
program will cause a bus error if you try to close the main window, because
the Window object will then try to send the windowDidResignMain:
message to the former delegate.)
42. Make an icon for the MathPaper application and put it in the file
Mathpaper.tiff. Drop the file icon in Project Builder’s Attributes view
to make Mathpaper.tiff the MathPaper’s application icon. Our attempt
at creating an icon is shown at the left.
Testing MathPaper
43. Choose Document→Save All from IB’s menu and save all class files.
44. Compile and run your MathPaper application by clicking the Run
button in PB. A MathPaper window will be created automatically (due
to the appDidInit: method).
45. Press Command-n four times. Four more windows will be created, as
in Figure 13 below.
46. Quit MathPaper.
Summary
Well, that’s all we’ve got right now – nothing else works! In the next chap-
ter, we’ll tie the front end and the back end together. To do that, we’ll have
to write more code for the PaperControl class, as well as create a new
class, called Process, which will handle the interprocess communications.
Summary 287
In the previous chapter we built the MathPaper front and back ends; the
back end was created as a separate program called Evaluator. In this chap-
ter we’ll tie the ends together, learn more about processes, and modify the
PaperControl class to display the results of our calculations.
287
288 Spawning Multiple Processes and the Text Object
UNIX uses the fork() system function to “spawn” (create) child subpro-
cesses. After a subprocess is spawned, it can change what program it is
running by calling the execv() system function. One way of communicat-
ing with subprocesses is with a pipe, a special kind of file object used by
UNIX to transmit information between processes. The actual details of
how fork(), execv() and pipe() work are not important for an understand-
ing of this chapter. For further information on pipes and the fork() and
execv() functions, use the Librarian to search the UNIX Manual pages for
their names.
While creating and using pipes can be complicated, the Process class pre-
sented below does all of the hard work for you. As with any class, you need
to understand the Process class interface in order to use it, but need not be
concerned with the details of how it actually works. In order to be complete
and to empower you to create similar classes, however, we’ll describe this
class in some detail.
1. Using an editor, create a file called Process.h in your MathPaper
directory which contains the following:
/*
* Process.h: spawn and control a subprocess
*/
#import <appkit/appkit.h>
@interface Process:Object
{
int toProcess[2];
int fromProcess[2];
int pid;
BOOL fdHandlerInstalled;
}
- initFromCommand:(char **)argv;
- free;
- (int)toFd;
- (int)fromFd;
- writeLine:(const char *)aLine;
- dpsWatchFD:(DPSFDProc)handler
data:(void *)userData priority:(int)pri;
@end
The instance variables toProcess and fromProcess are used to hold the
two pairs of file descriptors that reference the pipe. The variable pid is used
to store the process identifier of the child (sub)process. The boolean vari-
able fdHandlerInstalled works together with the dpsWatchFD:data:pri-
ority: method, which we’ll describe a bit later.
Spawning Processes with the Process Class 289
The Process class uses a watched file descriptor to notify the parent pro-
cess whenever the child has information available for it to read. We’ll now
go through the implementation for the Process class one method at a time.
The nice thing about this class is that it works. Even if you don’t under-
stand about forks, children, and pipes, if you know how to use this class,
you can spawn a subprocess, send the subprocess data, and set up a func-
tion to be called when that process has data to return.
2. Create a file called Process.m in your MathPaper directory which
contains the following:
/*
* Process.m
*/
#import "Process.h"
@implementation Process
- initFromCommand:(char **)argv
{
[super init];
if (pipe(toProcess) == -1) {
[self free];
return nil; /* could not open first pipe */
}
if (pipe(fromProcess) == -1) {
close(toProcess[0]);
close(toProcess[1]);
[self free];
return nil; /* could not open second pipe */
}
pid = fork();
290 Spawning Multiple Processes and the Text Object
if (pid == -1){
[self free];
return nil; /* no more processes */
}
close(toProcess[1]);
close(fromProcess[0]);
close(2); /* stderr */
dup2(fromProcess[1], 2); /* stderr */
execv(argv[0], argv);
perror(NXArgv[0]);
exit(1);
}
/* executed by the parent */
/* close the other ends of the pipe */
close(toProcess[0]);
close(fromProcess[1]);
return self;
}
if (toProcess[1]) close(toProcess[1]);
if (fromProcess[0]) close(fromProcess[0]);
if (pid>0) kill(pid, 9);
return [super free];
}
The free method removes the function that watches the pipe for data, if the
handler is installed. It then closes the pipes, if they were created, and kills
the child process, if it exists. Finally it invokes the free method of its super-
class and returns.
The next two methods, toFd and fromFd, simply make available the file
descriptors that are used to communicate with the child process. We won’t
use them in the MathPaper application, but we will use them in an applica-
tion called GraphPaper that we’ll start building in Chapter 16. (When
you’re designing a class, it’s good practice to provide functionality that you
might need later on.)
4. Insert the toFd and fromFd methods below at the end of Process.m.
- (int)toFd
{
return toProcess[1];
}
- (int)fromFd
{
return fromProcess[0];
}
The Process class needs two more methods: one that sends data to the child
process, and one that installs the function that gets called automatically
when the child process has data ready to send back. The writeLine:
method listed below performs the first of these tasks, it “writes” a line of
data to the child process. If the line given by the user doesn’t end with a
newline character, one is added as a courtesy.
5. Insert the writeLine: method below at the end of Process.m.
/* send a line to the process */
- writeLine:(const char *)aLine
{
int len = strlen(aLine);
}
return self;
}
@end
You can think of the ScrollView object as a View that contains three other
views:
• The docView, the View object being scrolled.
• The vertical scroller, which controls up-down scrolling and shows
where you are in the document.
• The horizontal scroller, which controls left-right scrolling and shows
where you are in the document.
horizontal
scroller
You can control whether or not each scroller is displayed by sending the
setVertScrollerRequired: or setHorizScrollerRequired: messages with
the arguments YES or NO to the ScrollView. The Text ScrollView that
you take off IB’s palette displays the vertical scroller, but not the horizontal
one.
Normally ScrollView objects are used with Text objects, which is why
Interface Builder gives them to us that way. We’ll learn other ways to cre-
ate ScrollViews in the GraphPaper application later in this book.
Every Window object has a special Text object called the field editor that
can be assigned minor editing tasks for the Window. This Text object is
shared among Form, Matrix, NXBrowser, and TextField objects located
within the associated on-screen window. When you are working with text
in one of these objects in a window, the field editor reads in the text and lets
you edit it. When you’re done, the field editor Text object spits out its con-
tents and puts it back into the appropriate location. The shuttling about of
the field editor is all fairly transparent and is handled automatically by
these classes.
Most often, you’ll use a Text object to display a chunk of text for the user:
the contents of an article, a mail message, or some other sort of thing.
Alternatively, you might use a Text object to let the user enter some free-
form text: a mail message or other such thing. If you’re writing a full-fea-
tured text editor, you probably won't want to use a Text object, because it
doesn’t give you enough control over the placement of characters and inter-
pretation of keystrokes. But for most applications, a Text object is just fine.
When it runs, the Text object contains a copy of the entire text that you are
editing. The more text that you have, the longer it will take to load the text
into the Text object and to have it first displayed. For this reason, if you
wish to display a piece of text that is longer than a few hundred kilobytes,
you may wish to implement your own text object that only reads in the
parts that are needed for display.
Changes to the PaperControl Class 295
argv[0] = malloc(strlen(NXHomeDirectory())+32);
strcpy(argv[0], NXHomeDirectory());
strcat(argv[0], EVALUATOR_FILENAME);
if (!proc) {
NXRunAlertPanel(0,"Cannot create calculator: %s",
0, 0, 0, strerror(errno));
[window performClose: self];
return nil;
}
296 Spawning Multiple Processes and the Text Object
return self;
}
(Note: If you are using NeXTSTEP 3.0, you may wish to install the Evalu-
ator program inside MathPaper’s app wrapper. The application wrapper is
the directory with the “.app” extension that holds the application execut-
able and other files. Under NeXTSTEP 2.1 application wrappers were
optional; Under NeXTSTEP 3.0, all applications are stored inside wrap-
pers. You can use Project Builder to put the Evaluator into the wrapper.
You can then use the NXBundle class to find its file name.)
The line with the alloc class message tries to create and initialize a Process
object. If the setUp method can’t create this object, then it displays an error
message and sends the performClose: message to the Window object to
close the on-screen MathPaper window. (Recall that we set PaperCon-
trol’s window outlet in IB in the previous chapter.) The NeXTSTEP func-
tion NXRunAlertPanel() displays the error message in an alert panel; the
0’s tell the function to display the default value for the panel’s title and
option buttons. Use the Librarian or HeaderViewer to obtain full descrip-
tions of the NeXTSTEP functions used here.
If the process is successfully created, the setUp method sends the dps-
WatchFD:data:priority: message to the Process object that’s been cre-
ated. The first argument, printer, is a function that should be called
automatically when data is available to display. The second argument, self,
is a 32-bit value that is sent to the printer() function as an argument when-
ever it is called. By sending self, we are providing the printer() function
with the id of a PaperControl instance. This is necessary if the printer()
function wishes to send a message to the PaperControl instance. The last
argument, NX_BASETHRESHOLD, specifies a priority for the part of
the event loop that checks to see if data is available for the file descriptor.
NX_BASETHRESHOLD means that the file descriptor will be checked
for data during the Application object’s main event loop, but it won’t be
checked when alert panels are being displayed.
Changes to the PaperControl Class 297
The arguments to the printer() function are the file descriptor (fd) which
has the data and the void* pointer which was originally passed to the
DPSAddFD() function. Recall that the void* pointer was actually the id of
the PaperControl object that controls the particular window. We need the
id if we want to send it a message.
The printer() function reads the data that is available on the file descriptor.
If any is present, it sends the appendToText:fromPipe: message to the
PaperControl object, which causes the text to be displayed. It also draws
the horizontal line that is displayed between results.
298 Spawning Multiple Processes and the Text Object
Finally the printer function gets the id of the window’s Text object with
the text accessor method (which we’ll show later). It then sets the Text
object to be editable. (In a method discussed later, we set the text to be not
editable after the user presses return.)
Making this function work requires that we add a new method called
appendToText:fromPipe: to the PaperControl class. This method will
automatically add a string to the Text object; the fromPipe: flag lets us
specify whether the text was returned by the Evaluator or not. Later we’ll
use this to indicate whether or not the text should be printed in bold.
12. Insert the appendToText:fromPipe: method below immediately
before the @end directive in PaperControl.m.
- appendToText:(const char *)val fromPipe:(BOOL)flag
{
int length = [text textLength];
return self;
}
In order for this method to work properly, we need the instance variable
text to contain the id of the Text object in the MathPaper window. In
Changes to the PaperControl Class 299
We’ll use the setScroller: outlet-setting method to set the scroller outlet
we defined in IB in the previous chapter. We’ll also use setScroller: to set
the text outlet (also defined in IB the previous chapter) to be the id of the
Text object inside the ScrollView object.
13. Insert the setScroller: method below immediately before the @end
directive in PaperControl.m.
- setScroller:aScroller
{
scroller = aScroller;
text = [aScroller docView];
[text setDelegate:self];
[text setCharFilter:NXFieldFilter];
[text selectAll:self];
return self;
}
ter() (more on this below). Finally, it sends the Text object the selectAll:
message which, since the Text object starts off empty, has the same effect
as clicking the mouse in the on-screen Text object.
A Text object delegate can receive all sorts of special messages when
things happen to the Text object. The one that we care about here is the
textDidEnd:endChar: message, which gets sent to the delegate when the
user stops entering new text into the Text object. This delegate method is
actually invoked when the user clicks in another View in the window con-
taining the Text object (i.e., when the Text object relinquishes its first
responder status) or when the user enters a field ending character such as
the Return character.
Field ending characters are determined by the Text object’s character filter.
NeXT provides us with two filter functions: NXEditorFilter() (the
default) and NXFieldFilter(). The NXFieldFilter() filter function inter-
prets Tab and Return characters as an “end of text” command. The NXEdi-
torFilter() filter function lets the user enter whatever he wishes into the
text, including Tab and Return characters.
int len,maxlen;
return self;
}
@end
Once the memory stream is filled with the contents of the Text object, the
method gets the memory buffer associated with the stream and scans back-
wards to find the last complete line. If it finds a line, it sends it to the Eval-
uator back end and makes the Text object (sender) not editable, which
prevents the user from typing anything into the Text object while the back
end is “thinking.”
- text
{
return text;
}
Now we’ll update the PaperControl interface class file to reflect the newly
added methods.
17. Insert the three new method declarations in bold below into
PaperControl.h.
#import <appkit/appkit.h>
@interface PaperControl:Object
{
id proc;
id scroller;
id text;
id window;
}
- setUp;
- window;
- windowWillClose:sender;
- text;
- setScroller:aScroller;
- appendToText:(const char *)val
fromPipe:(BOOL)flag;
@end
18. Save all pertinent files (nib, class, project) and Build (compile) but do
not run your MathPaper project.
Changes to the PaperControl Class 303
19. Open up a UNIX shell window and type ps -ux to display all of the
currently running processes that you own, as in Figure 2 below.
PID Process
128 The NeXTSTEP Window Server.
1435 The C-shell where the ps -ux command was typed.
721 The NeXTSTEP Workspace Manager. Note that this is
a process that is distinct from the Window Server.
720 The Application Kit server.
1391 /NextApps/Edit.app is the Edit application, which we
happened to be running at the time ps -ux was entered.
20. Now run the MathPaper application from PB or the Workspace. You’ll
see its application icon and its initial window appear.
21. Verify that MathPaper works by typing 2+2 and hitting Return, as in
Figure 3 below.
22. Back in the shell window, type ps -ux again, as in Figure 4 below.
304 Spawning Multiple Processes and the Text Object
23. Click in the MathPaper 1 window and then press Command-n three
times, so that you’ve created four MathPaper windows in total.
Changes to the PaperControl Class 305
24. Then try typing ps -ux again. Our results are shown in Figure 5 below.
FIGURE 5. ps -ux Listing With Several MathPaper Windows
localhost> ps -ux
USER PID %CPU %MEM VSIZE RSIZE TT STAT TIME COMMAND
simsong 128 15.0 24.0 27.0M 5.76M ? S 73:41 - console (WindowServer)
simsong 1435 0.0 1.5 1.53M 368K p1 S 0:00 -csh (csh)
simsong 721 2.3 10.4 5.41M 2.48M ? S 32:12 /usr/lib/NextStep/Workspa
simsong 720 0.0 0.9 1.99M 216K ? S 0:00 appkitServer
simsong 1391 0.0 7.0 4.44M 1.69M ? SW 0:16 /NextApps/Edit.app/Edit
simsong 205 0.0 7.6 3.83M 1.22M ? SW 0:02 /simsong/MathPaper/MathPa
simsong 206 0.0 1.4 1.37M 232K ? SW 0:00 /simsong/Apps/Evaluator
simsong 210 0.0 1.2 1.37M 200K ? SW 0:00 /simsong/Apps/Evaluator
simsong 211 0.0 1.2 1.37M 200K ? SW 0:00 /simsong/Apps/Evaluator
simsong 212 0.0 1.4 1.37M 232K ? SW 0:00 /simsong/Apps/Evaluator
localhost>
There are now four copies of Evaluator running, each one connected to its
own window.
25. Choose the Windows menu command and notice that the four
MathPaper windows are shown in separate menu cells. The Windows
menu works without any programming effort whatsoever – fabulous!
26. Choose the Services menu command and note that it works too!
27. Now close one of the MathPaper windows and type ps -ux again in a
shell window. There are now only three copies of Evaluator running
because we properly killed one in the windowWillClose: method of
the associated PaperControl object.
The text displayed in the alert panel in Figure 6 was determined by the sec-
ond argument in the NXRunAlertPanel() function call we inserted in the
setUp method in the MathController class implementation:
NXRunAlertPanel(0,"Cannot create calculator: %s",
0, 0, 0, strerror(errno));
306 Spawning Multiple Processes and the Text Object
Summary
In this chapter we created a class which creates subprocesses and showed
how to tie it together with the MathPaper application we developed in the
previous chapter. In the next chapter we’ll learn about Rich Text and more
about the Text class in order to use fonts and formatting in MathPaper win-
dows.
12
Text and Rich Text
The MathPaper window that we built in the last chapter didn’t quite live up
to what was promised. We promised what you see on the left of Figure 1
but gave what you see on the right.
FIGURE 1. What We Promised and What We Delivered
307
308 Text and Rich Text
The big difference between the two windows is fonts and formatting.
Although the Text object allows a great deal of control over fonts and for-
matting, when we use the replaceSel: method we’re simply pasting plain
ASCII text into the selection. That comes up as left-justified monofont text
– not very interesting. In order to get the promised fonts and formatting, we
need to learn about the Rich Text Format (RTF).
One way that you can do this is by using the Text class as if it were a sim-
ple text editor, sending an instance of it commands to insert text, select the
text, and then change the text to the desired size. Although this is an ineffi-
cient way to manipulate the Text class, it is conceptually easy.
What Is Rich Text? 309
Below is a method that will display the text in the Text object inside the
ScrollView object (aScroller) in Figure 2 above:
- slowFontDemo:sender
{
float s;
id text = [aScroller docView];
Clearly we’re on the right track. We just need a better way of writing multi-
font text to the screen.
Each Rich Text command begins with a backslash (\) and consists of a
string of letters followed by an optional numeric argument. (Curly) braces
have a special meaning: they create Rich Text graphics states. If you
change the state of a font within a graphics state, the change gets lost when
the state is closed.
Don’t be alarmed if Rich Text seems a little complicated! There are really
only a few Rich Text commands that you need to be concerned about – and
later in this chapter, we’ll introduce an RTF object that handles them for
you automatically.
Try it and use the UNIX cat command to list the file’s contents in a shell
window. (The contents of your file may differ slightly, depending on your
defaults.) If you add three lines of text, in Helvetica, you’ll end up with
something like this:
{\rtf0\ansi{\fonttbl\f0\fswiss Helvetica;}
\paperw11440
\paperh9000
\margl120
\margr120
312 Text and Rich Text
\pard\tx533\tx1067\tx1601\tx2135\tx2668\tx3202
\tx3736\tx4270\tx4803
\tx5337\f0\b0\i0\ul0\fs24 This is line 1\
This is line 2\
This is line 3\
}
When Edit reads a file, it checks the first six characters to see if they are
“{\rtf0”. If they are, Edit assumes that the file is in Rich Text Format.
Let’s experiment with text weight and angle changes. The window in
Figure 3 below produces the following RTF file:
{\rtf0\ansi{\fonttbl\f0\fswiss Helvetica;}
\paperw4720
\paperh2520
\margl120
\margr120
\pard\tx533\tx1067\tx1601\tx2135\tx2668\tx3202
\tx3736\tx4270\tx4803
\tx5337\f0\b0\i0\ul0\fs24 This is plain\
\b This is bold\
\b0\i This is italic\
\b This is bold italic\
}
Now let’s try changing the font size. The window on the left of Figure 4
below produces this RTF file:
{\rtf0\ansi{\fonttbl\f0\fswiss Helvetica;}
\paperw4820
\paperh2260
\margl120
\margr120
What Is Rich Text? 313
\pard\tx355\tx711\tx1067\tx1423\tx1779\tx2135\tx2490
\tx2846
\tx3202\tx3558\f0\b0\i0\ul0\fs16 This is 8 point\
\fs20 This is 10 point\
\fs24 This is 12 point\
\fs28 This is 14 point\
\fs32 This is 16 point
}
Finally, let’s try changing fonts. The window on the right of Figure 4 pro-
duces this RTF file:
{\rtf0\ansi{\fonttbl\f0\fswiss Helvetica;
\f2\fnil Times-Roman;\f1\fmodern Courier;\f4\fmodern
Ohlfs;}
\paperw4400
\paperh2160
\margl120
\margr120
\pard\tx533\tx1067\tx1601\tx2135\tx2668\tx3202
\tx3736\tx4270
\tx4803\tx5337\f0\b0\i0\ul0\fs24 This is Helvetica\
\f2 This is Times\
\f1 This is Courier\
\f4 This is Ohlfs\
}
An RTF document begins with the character string “{\rtf0” and ends with a
closing brace “}”. Inside the RTF document you can have control symbols,
which begin with a backslash (\), and text. Control symbols are interpreted
as commands, while text is displayed or printed.
You can have additional pairs of braces within Rich Text. Any formatting
commands that you issue within a pair of braces will be used but not
printed when the Rich Text is printed. For example, the following sequence
in an RTF file:
This is {\b a test} of Rich Text.
Commands can appear anywhere in the text. For example, the strings
This is \b a test \plain of Rich Text.
and
This is \b a test \b0 of Rich Text.
print like this (the same as the string with braces above):
This is a test of Rich Text.
Normally, Rich Text ignores carriage returns. If you want a carriage return,
precede it with a backslash (\). If you want a backslash, type a double
backslash (\\).
You can define any number of fonts within an RTF document. Fonts are
given numbers; you usually define them within a set of braces at the begin-
ning of the document. In our example above, the following string defined a
single font table with font \f0 being Helvetica:
{\rtf0\ansi{\fonttbl\f0\fswiss Helvetica;}
Font Definitions:
\fonttbl begins definition of a font table
What Is Rich Text? 315
Formatting:
\ql left justify text (quad left)
\qc center justify text (quad center)
\qr right justify text (quad right)
\tab tabstop
\paperwnnnn paper width in twips
\paperhnnnn paper height in twips
\marglnnn left margin
\margrnnn right margin
\finnnn first line indent in twips
\linnnn left indent in twips
If you specify a font which isn’t available on the machine you’re using, you
are likely to get Courier.
Of course there is: create an Objective-C class for building and managing
Rich Text segments. Below is the interface for such a class, a subclass of
the Object class, which works with the Text object.
RTF.h:
#import <appkit/appkit.h>
#import <streams/streams.h>
@interface RTF:Object
{
NXStream *textStream;
}
- (NXStream *)stream;
- appendRTF:(const char *)string;
- append:(const char *)string;
A Rich Text Object 317
- bold:(BOOL)flag;
- setJustify:(int)mode;
- free;
@end
This time, we’ll describe these methods before we show how they are
coded, to demonstrate that you don’t need to know how an object is imple-
mented in order to use it. Here is a description of the instance methods
declared in the RTF class:
#import "RTF.h"
@implementation RTF
318 Text and Rich Text
- init
{
[super init];
- (NXStream *)stream
{
NXSeek(textStream, 0L, NX_FROMSTART);
return textStream;
}
NXPutc(textStream, *string);
string++;
}
return self;
}
- bold:(BOOL)flag
{
[self appendRTF: flag ? "\\b " : "\\b0 "];
return self;
}
- setJustify:(int)mode
{
switch(mode) {
case NX_LEFTALIGNED:
case NX_JUSTIFIED:
[self appendRTF: "\\ql "];
break;
case NX_CENTERED:
[self appendRTF: "\\qc "];
break;
case NX_RIGHTALIGNED:
[self appendRTF: "\\qr "];
break;
}
return self;
}
- free
{
NXCloseMemory(textStream, NX_FREEBUFFER);
return [super free];
}
@end
The init, appendRTF:, stream, and free methods all use “NX” functions
that are part of the NeXTSTEP streams package.
Notice that, before the stream is returned, its pointer is set to the beginning
of the stream, and before a string is appended, the pointer is reset to the
end. The user of the object doesn’t have any way of knowing that this is
done, and indeed, as long as the stream works properly for the user, the
user doesn’t need to know. This is another example of why it is better to
use accessor methods than using Objective-C’s @public feature and
accessing an object’s instance variables directly.
Now that our new RTF class has been added to the project, we proceed to
make the necessary changes in the PaperControl class.
4. Insert the directive below after the other #import statements near the
top of PaperControl.m.
#import "RTF.h"
5. Replace the appendtoText:fromPipe: method implementation in
PaperControl.m with the new implementation below.
- appendToText:(const char *)val fromPipe:(BOOL)flag
{
int length = [text textLength];
if (flag) {
id rtf =
[ [RTF allocFromZone:[self zone] ] init];
[rtf bold:YES];
[rtf setJustify:NX_RIGHTALIGNED];
[rtf append:val];
Summary 321
[rtf bold:NO];
[rtf setJustify:NX_LEFTALIGNED];
[rtf append:" "];
[text replaceSelWithRichText:[rtf stream] ];
[rtf free];
}
else {
[text replaceSel:val];
}
[text scrollSelToVisible];
[text display];
return self;
}
6. Save all pertinent files, and make and run your MathPaper project. The
RTF object should behave as shown in the screen shot on the left of
Figure 1, “What We Promised and What We Delivered,” on page 307.
7. Quit MathPaper.
Summary
In this chapter we learned more about the Text class. Specifically, we
learned how to exert precise control over the contents of a Text object by
using Rich Text to encode font, size, and justification information into a
stream before copying the information into the Text object.
In the next chapter, we’ll see how to use methods built into the Text class
to save the contents of a MathPaper window into a file. We’ll also learn
how to catch the message that Workspace Manager generates when a user
double-clicks a file icon in the File Viewer and see how to open that file in
our application program.
322 Text and Rich Text
13
Saving, Loading, and Printing
In the previous chapter we saw how the contents of a Text object can be
translated into a “Rich Text” stream of characters. In this chapter, we’ll see
how to take that stream and save it into the file; we’ll also see how to load
one of those files and place its contents in a new window. Finally, we will
learn about printing.
Right now, our MathPaper application makes a good try at handling multi-
ple windows, but it’s missing many basic functions such as:
• Saving the contents of a window into a file.
• Loading a saved file, so that you can continue calculating where you
left off.
• Marking an edited window with the “unsaved” close button ( ), so
that you know it has been edited.
• Alerting the user when he or she tries to close an edited window with-
out first saving it.
• Graying out menu items that are not appropriate in a given context (e.g.,
the Save menu item when there are no open documents).
• Printing the contents of a window.
323
324 Saving, Loading, and Printing
Saving to a File
Saving a the contents of a window means writing all of the states associ-
ated with the window into a file, so that we can reconstruct the window’s
current state as closely as possible when it’s reloaded. In the case of a
MathPaper window, not many states need to be saved. Since neither the
Process object nor the Evaluator back end retain any states between
launches, all we’ve got to do to reload the state of the MathPaper window
is to start up a Process object and an Evaluator back end process and reload
the former contents of the window.
We can use the Text class to archive the contents of the window in the form
of a Rich Text stream. All we need to do is to open a stream and tell a Text
object to send its contents into that stream. Then we copy the stream into a
file. Simple! But what sort of file should contain the contents of a MathPa-
per window? Read on...
In the table below we list most of the commonly used extensions and their
meanings. These are “standard” extensions used by NeXTSTEP.
File Extension File Type
.a UNIX library file
.app Directory containing an application
.bshlf (Digital) Librarian bookshelf file
.c C language source file
.compressed Compressed file in Workspace Manager
.debug Directory containing an application with debug-
ging information
.dsp Binary file for the digital signal processor (DSP)
.eps Encapsulated PostScript file
.f FORTRAN source file
Saving to a File 325
The extensions in the following table have been adopted by third party
application programs. There are many others.
1. In NeXTSTEP 2.1, add the document file icon and extension in the Attributes
view of IB’s Project Inspector. The process is very similar to adding an application
icon. Use the Doc 1 icon well for the document icon and enter the extension name.
328 Saving, Loading, and Printing
The FirstResponder icon in the File window is a place holder for the
application’s current first responder, that is, the object which will be the
first to try to respond to keyboard events and menu commands such as Cut,
Copy, and Paste. You can also think of it as a pointer to a list of
Responder objects. Any message sent to the FirstResponder object gets
sent one-by-one to the objects listed below, until an object is found that can
receive the message and respond with a value other than nil.
• The key window
• The key window’s delegate
• The application’s main window
• The application’s main window’s delegate
• The application’s Application object
• The application’s Application object’s delegate
To make the Save and Save As menu commands work, we’ll arrange for
their menu cells to send the save: and saveAs: action messages to the Fir-
stResponder; the messages will automatically be dispatched to the dele-
gate of the currently selected (key) window. We’ll then implement save:
and saveAs: methods for the PaperControl class. Each MathPaper win-
dow has a PaperControl instance as its delegate, so the correct instance
will get sent the save: (saveAs:) message when the user chooses the Save
(Save As) menu command.
6. Double-click MathPaper.nib in the Files view of PB’s main window
to open this nib file in Interface Builder, and then Command-double-
click IB’s application icon to simplify the screen.
Saving to a File 329
Remember that the FirstResponder class isn’t a real class; its just a hold-
ing place for messages that you might want to send to the current FirstRe-
sponder object.
8. If necessary, type Command-1 to see the Attributes Inspector for the
FirstResponder class. See the Inspector in Figure 2 below.
Notice that this Attributes Inspector has lots of actions. Don’t worry about
them – these are all of the actions that Interface Builder thinks you might
want to send to the FirstResponder. If you send a message to the FirstRe-
sponder that is not implemented by the current first responder, or any other
object in the responder chain, the message is ignored and your program
continues running.
9. Add the following two new actions to the FirstResponder “class”
(remember, it’s not really a class), as in Figure 2:
save:
saveAs:
10. Connect the Calculator→Save and Calculator→Save As menu cells
(which were previously enabled ) to the FirstResponder icon. The
Calculator→Save menu cell should send the save: action and the
Calculator→Save As menu cell should send the saveAs: action.
11. Insert the four lines in bold below into PaperControl.h.
#import <appkit/appkit.h>
@interface PaperControl:Object
{
id proc;
id scroller;
id text;
id window;
char *filename;
}
- setUp;
- window;
- windowWillClose:sender;
- text;
- setScroller:aScroller;
- appendToText:(const char *)val
fromPipe:(BOOL)flag;
- setFilename:(const char *)aFilename;
- saveAs:sender;
- save:sender;
@end
12. Insert the three method implementations below into PaperControl.m.
Put them immediately before the first @end directive, not in the
TextDelegate section.
- setFilename:(const char *)aFilename
{
Saving to a File 331
if (filename) free(filename);
filename = malloc(strlen(aFilename)+1);
strcpy(filename, aFilename);
[window setTitleAsFilename:aFilename];
return self;
}
- saveAs:sender
{
id panel;
const char *dir;
char *file;
if (filename==0) {
/* no filename; set up defaults */
dir = NXHomeDirectory();
file = (char *)[window title];
}
else {
file = rindex(filename, '/');
if (file) {
dir = filename;
*file = 0;
file++;
}
else {
dir = filename;
file = (char *)[window title];
}
}
- save:sender
332 Saving, Loading, and Printing
{
int fd;
NXStream *theStream;
NXClose(theStream);
close(fd);
We also added the saveAs: action method that prompts the user for a file
name, using a NeXTSTEP SavePanel object, and then saves it into the file.
Lastly we added a save: method that first checks to see if a file name has
been specified for the window. If a file name has not been specified, it
invokes the saveAs: method, which prompts the user for a file name. Oth-
erwise, it simply saves the file under the name that was previously speci-
fied.
17. If necessary, click Home in the Save panel to open your home
directory. Then type a file name and click OK (or hit return) to save the
file in your home directory. You’ll see the document icon appear above
your chosen file name in your File Viewer, as in Figure 4 below.
Note also that the title bar of the MathPaper window has changed, as in the
window in Figure 5 below.
18. Try Calculator→Save As to see that you can save the file under a
different file name.
334 Saving, Loading, and Printing
There are two ways that the user should be able to load a file into our Math-
Paper application:
• by choosing MathPaper’s Calculator→Open menu command
• by double-clicking a MathPaper document file icon in the File Viewer.
Now we’ll add an action method that gets invoked when the user chooses
MathPaper’s Calculator→Open menu command.
1. Insert the new loadCalc: action method declaration below into
MathController.h, immediately before the @end directive.
- loadCalc:sender;
2. Insert the loadCalc: action method implementation below into
MathController.m, immediately before the @end directive.
- loadCalc:sender
{
char *types[2] = {"mathpaper", 0};
return self;
}
[[OpenPanel new] filename] simply returns the file name that was
selected the last time that the OpenPanel was activated. (Remember, there
is exactly one OpenPanel instance per application.)
Next we’ll add code to MathController so that files with the mathpaper
extension can be opened within MathPaper when their file icons are dou-
ble-clicked in the File Viewer. The “delegate” code organized in a category
below works because we made MathController the delegate of the Appli-
cation object previously (in Chapter 10).
3. Insert the following code after the @end directive in
MathController.m.
@implementation MathController(ApplicationDelegate)
- (BOOL)appAcceptsAnotherFile:sender
{
return YES;
}
- (int)app:sender
openFile:(const char *)filename
type:(const char *)aType
{
Loading From a File 337
if ([self newCalc:self]) {
if ([newCalc loadCalcFile:filename]) {
return YES;
}
}
return NO;
}
@end
4. For consistency, move MathController’s appDidInit: delegate
method to the MathController(ApplicationDelegate) section and
delete the appDidInit: declaration from MathController.h.
(ApplicationDelegate methods should not be declared in the class
interface file, unless they are declared in a separate
ApplicationDelegate interface section, which is optional here.)
When you double-click a document file icon in your File Viewer, the
Workspace Manager starts up the associated application (if it isn’t already
running). The application’s Application delegate will then receive the
appAcceptsAnotherFile: message to see if another file can be opened. If
the application returns YES (1), then it can open another file. Some appli-
cations can only have a single file open at a time, and return NO if they
already have one open.
Exception Handling
In MathController’s loadCalc: and app:openFile:type: methods above
we invoked, but didn’t define the loadCalcFile: method. We need to add
this method to PaperControl to load a piece of math paper from a file.
Before we show the method, however, we need to introduce two new con-
cepts: exceptions and exception handling.
Exceptions are special conditions that interrupt the normal flow of a pro-
gram. NeXTSTEP has a rich system for catching exceptions and handling
them within the course of your program’s execution. Exception handling is
338 Saving, Loading, and Printing
To use the NeXTSTEP exception system, you specify a region of code that
is protected and a second region of code that follows the protected region
called the exception handler. If an exception is “raised” within the pro-
tected region of code – or within any function called within your protected
region of code – control jumps to the exception handler. If an exception is
not raised, the exception handler is skipped.
These are macros, not functions or compiler directives, and should not be
followed by a semicolon. They can be found by searching in HeaderViewer
or Librarian or by simply looking in the file /NextDeveloper/Headers/
objc/error.h. For more information about exceptions and exception han-
dling in general, search for “exceptions” in Librarian.
The most common place to use an exception handler is when loading infor-
mation from a disk file: if the file is inconsistent, an exception will be
raised, and your handler will execute. We’ll use this type of exception han-
dler below to protect the PaperControl class while a MathPaper file is
being loaded into the Text object.
5. Insert the new loadCalcFile: method declaration below into
PaperControl.h, immediately before the @end directive.
- loadCalcFile:(const char *)aFile;
6. Insert the loadCalcFile: method implementation below into
PaperControl.m, immediately before the first @end directive.
- loadCalcFile:(const char *)aFile
{
NXStream *theStream= NXMapFile(aFile,NX_READONLY);
if (theStream) {
id ret = self;
[window setTitle:"Loading..."];
Loading From a File 339
NX_DURING
[text readRichText:theStream];
[self setFilename:aFile];
NXCloseMemory(theStream, NX_FREEBUFFER);
return ret;
}
return nil;
}
also sets the title bar), and then the stream is closed. If the file can’t be
loaded, the method returns nil.
7. Open up MathPaper.nib in Interface Builder.
8. Drag the MathController.h file icon from your Workspace File
Viewer and drop it into the MathPaper.nib File window in IB. IB will
automatically Parse the outlets and actions in MathController.h (only
loadCalc: is new). Make sure MathController.h has been saved.
9. Connect the Calculator→Open menu cell (which should be enabled)
to the MathController instance so that it sends the loadCalc: action
message.
10. Save all pertinent files, make and run your updated application.
11. Enter some mathematical expressions in the MathPaper window, and
save the contents of a window using the Calculator→Save menu
command.
12. Try double-clicking the file icon for the file that you saved both when
MathPaper is and is not running. The Workspace Manager should
automatically launch MathPaper when it isn’t running and should
create a window with the contents of the file that you just saved in both
cases.
13. Try to open the file you saved by choosing the Calculator→Open
command. It should work.
14. Quit MathPaper.
{
int length = [text textLength];
[window setDocEdited:YES];
[text setSel:length :length];
. . .
}
- save:sender
. . .
[window setTitleAsFilename:filename];
[window setDocEdited:NO];
return self;
}
We’ll also modify the windowWillClose: delegate method so that the user
will be warned before a window with edited text is closed (so he gets a
chance to change his mind). We’ll do this by displaying an attention (warn-
ing, alert) panel if the user attempts to close a window containing edited
text.
2. Insert the lines in bold below into the windowWillClose: method in
PaperControl.m:
- windowWillClose:sender
{
if ([sender isDocEdited]) {
const char *fname;
int q;
q = NXRunAlertPanel("Save",
"Save changes to %s?",
"Save", "Don’t Save", "Cancel", fname);
if (q==1) { /* save */
if (![self save:nil]) {
return nil; /* didn't save */
}
}
if (q==-1) { /* cancel */
return nil;
}
}
/* tell Window we're gone */
[sender setDelegate:nil];
[proc free];
342 Saving, Loading, and Printing
The second new method, saveAll:, goes through the window list, looking
at the delegate of each window to make sure that it’s an instance of the
PaperControl class. (We don’t want to send the save: message to the win-
dows that contain the application’s menus or panels.) It then sends the
save: message to each window delegate.
5. Insert the saveAll: method implementation below into
MathController.m, immediately before the first @end directive.
- saveAll:sender
{
id winList;
int i;
return self;
}
Now that we’ve implemented the saveAll: method, it’s very easy to give
functionality to the Calculator→Save All menu command.
Enabling and Disabling Menu Cells 345
Window Updates
Until now, every NeXTSTEP user interface object we’ve used updates
itself on-screen whenever we make a change to that object’s internal state.
For example, when we sent the setStringValue: message to a TextField in
the displayX method in the Calculator application, the value displayed in
the on-screen text field changed immediately.
NeXTSTEP uses a similar system to update the status of menu cells. For
each menu cell, you define an update action. Whenever a menu needs to be
updated, NeXTSTEP sends the update action down the responder chain for
every exposed menu cell. The update action returns YES if the menu cell
needs to change its state, either from enabled to disabled or vice versa. If
the update action returns NO, the menu cell does not need to change its
state.
But how often do menu cells need to be updated? It’s impossible for
NeXTSTEP to know – that depends on your application. You can force a
window update by hand whenever you want by sending the updateWin-
dows message to NXApp. Alternatively, you can send NXApp the mes-
sage [NXApp setAutoupdate:YES]. Auto-updating makes NeXTSTEP
update the windows after each event is processed. Although it seems that
auto-updating will impose a severe performance penalty, in practice it
doesn’t.
In order to make the menu cells properly enable and disable, we also need
to know the id of each cell and the id of the menu that contains them. It’s
easy enough to get the id of the Save and Save As cells: simply create out-
lets in the MathController object and connect them to the cells.
Getting the id of their containing menu is a little more difficult. You can’t
connect an outlet to the menu directly. Instead, we’ll connect an outlet to
the Calculator menu cell in the main menu. The target of this menu cell
will be the id of the Calculator menu itself.
The first step is to create three new outlets in the MathController object
with the id of the menu cells that we wish to control.
1. Insert the three new outlets and new method declaration in bold below
into MathController.h.
#import <appkit/appkit.h>
@interface MathController:Object
...
/* three outlets for updating menu cells */
id calculatorSubmenuCell;
id saveMenuCell;
id saveAsMenuCell;
}
...
- saveAll:sender;
- (BOOL)menuActive:menuCell;
@end
2. Back in IB with MathPaper.nib open, Parse the new class definition
of MathController.
3. Connect the saveMenuCell outlet in the MathController instance to
the Save menu cell in the Calculator submenu. Note that the direction
here is different from our previous connections involving menus.
4. Connect the saveAsMenuCell outlet in the MathController instance
to the Save As menu cell in the Calculator submenu.
5. Connect the calculatorSubmenuCell outlet in the MathController
instance to the Calculator menu cell in the main menu (not to anything
in the Calculator submenu).
348 Saving, Loading, and Printing
Now we need to add the code to the MathController class that will set the
updateAction for these menu cells to be a new method in the class. We
also need to write the updateAction method.
6. Insert the menuActive: method implementation below into
MathController.m, immediately before the first @end directive.
- (BOOL)menuActive:menuCell
{
BOOL shouldBeEnabled;
[self newCalc:self];
[saveMenuCell
setUpdateAction: @selector(menuActive:)
forMenu: docMenu];
[saveAsMenuCell
setUpdateAction: @selector(menuActive:)
forMenu: docMenu];
Adding Printing Capability 349
The first new statement gets the id of the Calculator submenu. We then
send a message to each menu cell telling each their update action (menu-
Active:) and the menu in which they reside. Finally, we send NXApp the
setAutoupdate:YES message so that window update messages are auto-
matically generated after every event is processed.
8. Save all pertinent files, make and run your updated MathPaper.
9. Open the Calculator submenu and close the (only) MathPaper main
window. The Save and Save As cells will become disabled, as in the
menu at the left. Note that we should also disable the Save All menu
command; try it yourself!
10. Quit MathPaper.
The NeXTSTEP View class provides a rich set of methods for controlling
printing. The important method is printPSCode:, which causes the View
to display the standard Print panel. If the user clicks the Print button in the
Print panel, the PostScript code is turned into a bit image which is then sent
to the printer. (It’s a “dumb” printer – the PostScript is handled entirely in
the computer – which keeps the cost of the printer down.) Thus the only
thing that you need do to make a View object print its contents is send it the
printPSCode: message. If you wish to capture the PostScript code that a
View would generate if printed, you can call the method copyPSCodeIn-
side:to: to send the PostScript code for a particular bounding box to a
stream of your choice.
You can override methods in the View class to get more control over print-
ing. For example, you can override the beginPageSetupRect:placement:
350 Saving, Loading, and Printing
Recall that when a message is sent to the FirstResponder object, the mes-
sage is sent to the following objects in the following order, until a recipient
to the message is found:
• The key window
• The key window’s delegate
• The application’s main window
• The application’s main window’s delegate
• The application’s Application object
• The application’s Application object’s delegate
We’re going to modify the PaperControl instance, the key window’s dele-
gate, so that it responds to the printCalc: action method we added in IB.
We’ll be lazy and simply have the printCalc: method send the printP-
SCode: method to the window’s Text object. The Text object will automat-
ically provide for pagination if it contains more than one page of
calculations.
We are being lazy because we are not going to print any nice features, such
as a headline, page numbers, or vertical lines to denote the sides of the out-
put. When we’re done the output should look similar to what you see in
Figure 8 below (we added the rectangular boundary).
Adding Printing Capability 351
In order to implement the nice print features, we would have to create our
own custom View, which we’ll do in a later chapter. For now, let’s just
make the thing print!
6. Insert the new printCalc: method declaration below into
PaperControl.h, immediately before the @end directive.
- printCalc:sender;
7. Insert the printCalc: method implementation below into
PaperControl.m, immediately before the first @end directive.
- printCalc:sender
{
[text printPSCode:nil];
return self;
}
8. Save all pertinent files, make and run your updated application.
9. Try out the new Print menu command. When you type Command-p,
you’ll see the Print Panel appear as in Figure 9 below (note that our
application icon was automatically added). Print the contents of a
MathPaper window; you’ll get something similar to what’s in Figure 8
above.
10. Quit MathPaper and congratulate yourself on learning a heck of a lot in
this chapter.
352 Saving, Loading, and Printing
Summary
In this chapter we learned how to save and load the contents of a window
using Application delegate, SavePanel, and OpenPanel objects and meth-
ods. We then learned how to mark a window as edited, and how to erase
that mark after the window’s contents are saved. We also learned how to
check to see if a window is edited before closing it, how to check to see if
an application has any edited windows before quitting, and how to display
appropriate warning panels to ask the user what to do in these situations.
Finally, as frosting on the cake, we learned a quick and dirty way to make
an object print “itself.”
In the next chapter, we’ll have some fun with a little animation. It’s the last
chapter that involves MathPaper. Then in the following chapter, we’ll go
into more depth about custom Views.
DPS 14
DPS
DPS
DPS
Drawing with
Display PostScript
353
354 Drawing with Display PostScript
Panel shows off what a programmer can do; rightly or wrongly, the quality
of the Info Panel might be taken as a representation of the overall quality of
the application itself.
The MathPaper Info Panel will have two kinds of animation: motion and
dissolving. Motion animation is just that – something moves on the screen.
If you look at the Info Panel for the Terminal application, you’ll see the
string “>MACH” scroll up in the little terminal window. See Figure 1
below.
In our animation, we’ll arrange for the plus, minus, times, and divide sym-
bols to move from the top of MathPaper’s Info Panel to the bottom. The
string “MathPaper” and the MathPaper icon will then dissolve into the win-
dow, as if they were being beamed down from the Starship PostScript. The
final panel will look like the one in Figure 3 below. Making this happen
will be the subject of the rest of this chapter.
PostScript, and their programs are designed to execute in the user’s address
space, rather than in the Window Server. In the Adobe Display PostScript
environment, this communication is accomplished by means of a system
called PostScript wraps.
Let’s see how pswrap works. Below is a simple wrap for the PostScript
show (i.e., show output) operator. Suppose it is saved in the file named
PSshow.psw (the psw extension means PostScript wrap).
defineps PSshow()
show
endps
You can see that the syntax is a strange conglomeration of C and Post-
Script. Procedures start with the defineps directive and end with the endps
directive. The name of the function follows the keyword defineps; any
arguments are placed within the parentheses (more on arguments later).
356 Drawing with Display PostScript
Only valid PostScript code can be placed between the defineps and endps
statements. In this case, the wrap is called PSshow(), and it generates a call
to the PostScript show operator.
You can run the pswrap command from the command line (what you type
is in bold):
localhost> pswrap PSshow.psw -h PSshow.h -o PSshow.c
localhost> ls -l PS*
total 3
-rw-r--r-- 1 simsong 627 Feb 14 14:45 PSshow.c
-rw-r--r-- 1 simsong 172 Feb 14 14:45 PSshow.h
-rw-r--r-- 1 simsong 30 Feb 14 14:45 PSshow.psw
localhost>
The -h argument tells pswrap the name of the .h file where you want the
header information written, while the -o argument tells pswrap the name
of the .c file where you want the C source code written.
pswrap: Wrapping PostScript into a C Function 357
Below we list the C-code generated by pswrap in PSshow.c. It’s not essen-
tial that you understand what it means, so we’ll spend very little time on it.
/* PSshow.c generated from PSshow.psw
* by unix pswrap V1.009 Wed Apr 19 17:50:24 PDT 1989
*/
#include <dpsclient/dpsfriends.h>
#include <string.h>
#line 1 "PSshow.psw"
#line 10 "PSshow.c"
void PSshow()
{
typedef struct {
unsigned char tokenType;
unsigned char topLevelCount;
unsigned short nBytes;
DPSBinObjGeneric obj0;
} _dpsQ;
Not very clear, is it? The two key functions used by the wrap are
DPSPrivCurrentContext() and DPSBinObjSeqWrite(). The function
DPSPrivCurrentContext() returns the current Display PostScript context.
The function DPSBinObjSeqWrite() sends a binary-encoded sequence of
objects to that context.
(Note: You should never modify or edit the contents of the .c file generated
from a .psw or .pswm file. We have shown the contents of this file for
illustrative purposes only.)
printer.” Each has its own set of stacks, input/output facilities, dictionaries,
and memory.
Sometimes a single client can have more than one context. NeXTSTEP
creates a second PostScript context when you print a view. The NeXT-
STEP Mail application can display EPS images in their own context for
improved security.
Below is simple wrap that copies the value on the top of the PostScript
stack (using the PostScript dup operator) and returns the value to the user.
The value on the top of the stack is assumed to be a floating point number.
Notice also that output arguments in a wrap are specified after the vertical
bar (|) in the wrap definition.
defineps PSgettop(|float *val)
dup
endps
This wrap needs to return information from the Display PostScript inter-
preter to your program. So, when you call this wrap, your program will
wait until the Display PostScript interpreter “catches up” with all of the
PostScript that you have asked the interpreter to execute. This is called
“making a round-trip” and can substantially degrade the performance of
your application.
pswrap: Wrapping PostScript into a C Function 359
Buffering
The underlying communication channel between an application and the
Display PostScript interpreter is buffered and bidirectional. Buffered means
that the PostScript commands get saved up and sent to the server in
batches, to minimize the amount of communication overhead associated
with any single operation. Bidirectional means that you can send informa-
tion to the interpreter and it can send information back.
Normally, buffering is precisely what you want, because it makes your pro-
gram run more efficiently. But sometimes you need to synchronize your
user program with the Display PostScript interpreter. For example, if you
are displaying animation on the screen, you want each PostScript draw
command to be executed, one by one, rather than having them saved up
and executed all in a single batch.
As we saw above, calling a wrap that returns a value is one way to force
your program to synchronize with the PostScript interpreter. Another way
to synchronize the two processes is to call the NeXTSTEP function
NXPing(), which places a call to the Window Server and waits for it to
return.
Occasionally, calling a function like NXPing() can make your program run
more efficiently, because while your program is waiting for the interpreter
to catch up, they are not both competing for the CPU. Usually, though,
your program simply looks like it is running more efficiently, because its
drawing is smooth rather than jerky.
We could then use this supershow operator by first placing four PostScript
arguments on the stack, like this:
x y graylevel string supershow
One of the nice things about PostScript wrapping is that your PostScript
function can use any argument at any time, without having to carry out
strange gyrations with the PostScript stack.
Next, we’ll arrange for Project Builder to automatically run the pswrap
program and compile the resulting generated C code.1
2. Launch Project Builder by double-clicking the PB.project file icon in
your MathPaper project directory in the Workspace File Viewer.
3. View your project’s “other” source files by clicking the Files button at
the top of PB’s main window and then the Other Sources file type in
PB’s browser. See Figure 4 below.
4. Add the supershow.psw file to the project by dragging its icon from
your File Viewer into PB’s Files view. Figure 4 shows that
supershow.psw has been added to your MathPaper project.
When you make (build) your project, the supershow.psw file will auto-
matically be processed by pswrap to generate the files supershow.c and
supershow.h. These pswrap-generated files will also be automatically
compiled and linked.
The first technique is used more often. When you implement code with
drawSelf::, it gets called automatically when it is needed. All you need to
concentrate on is how to draw the object; NeXTSTEP handles the when.
When you are learning how to draw with Display PostScript, it is often eas-
ier to draw manually by locking the focus and issuing the PostScript com-
mands yourself. This is also the best way to create animation in your
program.
362 Drawing with Display PostScript
Now that we have the Info Panel, let’s create the View in which we’ll be
displaying animation.
11. Subclass the View class (under Responder) in the info.nib Classes
view and rename the new class InfoView.
1. If you’re using NeXTSTEP 2.1, refer to Chapter 6 for details on how to create an
auxiliary nib for an Info Panel.
Adding the Animated Info Panel to MathPaper 363
16. Unparse the InfoView class and insert it into your MathPaper project.
Later we’ll create an Info→Info Panel menu cell (which must be part of
the main nib MathPaper.nib) and connect it the MathController instance
so that it sends a message to load info.nib. When this nib is loaded the Info
Panel and InfoView instance will be displayed on the screen. We’ll need a
way to send messages from the MathController instance to the InfoView
object in order to display the animation. We’ll use an outlet called info-
View which points to the InfoView object to do the job.
364 Drawing with Display PostScript
This directive is needed because messages will be sent from the MathCon-
troller instance to the InfoView instance (see the showInfoPanel: method
above). Notice that we don’t need the id of the Info Panel itself, just the
View to which we wish to send the animateInfo: message. If we needed
the id of the window that contains the View, we could get it by sending the
window message to the View. This is another example of the coherence
that we saw in an earlier chapter.
Adding the Animated Info Panel to MathPaper 365
Recall that supershow is our PostScript wrap that displays a given string at
a particular location. The supershow.h file contains the supershow() func-
tion prototype; it is important to import this prototype for two reasons:
• it avoids compiler warnings about un-prototyped functions, and
366 Drawing with Display PostScript
In addition to importing all the Application Kit class headers, the #import
<appkit/appkit.h> statement imports the <dpsclient/wraps.h> file which
brings in all of NeXT’s standard PostScript wraps (i.e., the functions that
begin with “PS”).
29. Insert the animateDrop: method below into InfoView.m.
Since it will be invoked only by another InfoView method (ani-
mateInfo:, shown below), the animateDrop: method declaration need
not be included in InfoView.h. In this case, however, its implementa-
tion should be placed above the animateInfo: implementation.
- animateDrop:(float)size
{
int i,j;
int order[4] = {0,2,1,3};
char *dropstrings[4] = {"+", "-", "\264", "\270"};
float x,y;
[self lockFocus];
[ [Font newFont:"Symbol" size:size
matrix:NX_IDENTITYMATRIX] set];
for (i=0; i<4; i++) {
float const step = 2.0;
j = order[i];
x = ( (size * j * 2.0)/3.0) + 10.0;
/* Note that “frame” below is an instance
* variable inherited from the View class.
*/
for (y=NX_HEIGHT(&frame); y>=0; y-=step) {
supershow(x, y+step,NX_WHITE,dropstrings[j]);
supershow(x, y, NX_BLACK, dropstrings[j]);
usleep(2000); /* wait 2 ms */
NXPing(); /* synchronize with server */
if (NXUserAborted()) {
supershow(x, y, NX_WHITE, dropstrings[j]);
supershow(x,0.0, NX_BLACK, dropstrings[j]);
break;
}
}
}
[self unlockFocus];
return self;
}
Adding the Animated Info Panel to MathPaper 367
After the animateDrop: method sets the current font to 72-point Symbol
(the 72.0 is passed in the size argument), it starts two loops. The outer loop
repeats for the plus, subtract, times, and divide symbols (in that order),
which are stored in the dropstrings[] array. The inner loop does the actual
animation, moving the symbols one at a time from the top of the View to
the bottom. The inner loop calls the NXPing() function to synchronize the
method with the Display PostScript interpreter. If you take out the call to
this function, the animation will appear jerky; try it.
The inner loop also calls the NXUserAborted() function. This function
returns YES if the user types “Command-.” (Command-period) since the
last event was read by the Application object’s main event loop. The
method uses the “Command-.” user abort sequence to terminate the anima-
tion (think of the period as “the end”). Whenever you perform an animation
that takes a long time and prevents the user from doing anything else while
it is running, you should give the user some means for aborting the
sequence.
30. Insert the two lines in bold below to the animateInfo: action method
implementation in InfoView.m.
- animateInfo:sender
368 Drawing with Display PostScript
{
[self display]; /* display invokes drawSelf:: */
[self animateDrop:72.0];
return self;
}
Compositing
The way that the NXImage class transfers an image to the screen is with a
process called compositing. Compositing is a way of combining two
images, a source image and a destination image (the image already in place
on the screen). The combining is done with a special function called the
compositing operator, which combines the two images on a pixel-by-pixel
basis and displays the result.
instance is the source. The destination can be any locked focus, including a
View, another NXImage or even a PostScript graphics state.
To get the image into a View, use the method composite:toPoint:, which
has the form:
composite:(int)operator
toPoint:(const NXPoint *)aPoint
First we’ll insert the method that performs the dissolving animation.
1. Insert the animateDissolve: method below into InfoView.m. Put it
above the animateInfo: method to avoid a compiler warning from
using an undeclared method (or declare these methods in InfoView.h
so the order is unimportant – we prefer the latter).
- animateDissolve:(float)size
{
id image;
id textFont;
NXPoint myPoint;
NXSize isize;
float x, y, width;
float gray;
char *slgName = "Created by Simson L. Garfinkel";
char *mkmName = " tweaking by Michael K. Mahoney";
[self lockFocus];
[ [Font newFont:"Times-Roman" size:size * .75
matrix:NX_IDENTITYMATRIX ] set];
PSrectfill(NX_X(&frame), size,
NX_WIDTH(&frame), 2.0);
supershow(x, y, 1.0 - gray,
(char *)[NXApp appName]);
}
[self unlockFocus];
return self;
}
Instead of doing all of this in the main command event loop, NeXTSTEP
provides a system for having a small function called repeatedly at regular
time intervals. The system is called timed entries and is managed by the
NeXTSTEP functions DPSAddTimedEntry() and DPSRemoveTime-
dEntry().
• The period of the timed entry, i.e., how often it should be called. This
argument is expressed in seconds.
• A C language function, called a handler, that is automatically called
whenever the timed entry is executed.
• Data to be provided to the handler each time it is called.
• The timed entry’s priority.
The teNumber argument is the timed entry number that caused this function
to be called. The now argument is a counter; it records the number of sec-
onds from some arbitrary time in the past. The userData argument is the
same pointer that you provided to DPSAddTimedEntry(). It might be a
real pointer; it might also be simply a number.
Nothing is guaranteed about timed entries. If you ask that your handler be
called every 5 seconds, it may actually be called every 5.3 seconds. Your
handler might be called at 5 seconds, then at 10.3 seconds, then at 15.7 sec-
onds, then at 20 seconds. This is one of the reasons that the argument now
is provided.
Many timed entries remove themselves when they are no longer needed;
this eliminates the need to record the timed entry number in an instance
variable.
Because the handler function takes a single four-byte quantity of user data,
the easiest way to combine timed entries with Objective-C is to pass to the
timed entry the id of an object cast into a void *. The handler function then
casts its void * argument into an id and sends the object an appropriate
message.
In the remainder of this chapter, we’ll rewrite the InfoView class so that it
uses timed entries instead of a delay loop.
1. Replace the entire contents of the InfoView.h file with the InfoView
class interface code below.
#import <appkit/appkit.h>
@interface InfoView:View
{
DPSTimedEntry animateTE;
BOOL animatingDrop;
BOOL animatingDissolve;
int animationStep;
float animationSize;
float animationFloat;
id symbolFont;
id bigTextFont;
id smallTextFont;
}
@end
376 Drawing with Display PostScript
The animateTE instance variable will keep track of the timed entry num-
ber. The next five instance variables will keep track of the current stage of
the animation – and what to do next. The last three instance variables will
hold the ids of the fonts that we are using. This time we declared all of the
instance methods in InfoView.h, which is a good way to document the
methods in a class.
2. Replace the entire contents of InfoView.m with the code below.
#define SIZE 72.0
#import "InfoView.h"
#import "supershow.h"
@implementation InfoView
return self;
}
[window setDelegate:self];
animatingDrop = YES; /* start with this */
animationStep = 0; /* and this */
animateTE = DPSAddTimedEntry(.002,
(DPSTimedEntryProc)handler,
self,NX_BASETHRESHOLD);
return self;
}
- windowWillClose:sender
{
[self removeTE]; /* make sure it is gone */
return self;
}
Removing a timed entry that doesn’t exist generates a PostScript error. But
since a timed entry cannot have the number 0, we can use the instance vari-
able animateTE both to hold the timed entry number and to note that a
timed entry has been posted. The removeTE method sets the animateTE
instance variable to 0 after the timed entry is removed so that if the
removeTE is accidentally called a second time it won’t generate an error.
6. Insert the new animationClick method at the end of InfoView.m.
378 Drawing with Display PostScript
- animationClick
{
if (animatingDrop) [self animateDrop];
if (animatingDissolve) [self animateDissolve];
NXPing(); /* synchronize with server */
return self;
}
This method is called by the timed entry. It calls the appropriate animation
method depending on the values of the animatingDrop and animat-
ingDissolve instance variables.
7. Insert the updated animateDrop: method below at the end of
InfoView.m. Notice that this method is somewhat simpler than the old
animateDrop: method that it replaces because it does not contain any
loops – the looping is now performed by Display PostScript.
- animateDrop
{
int order[4] = {0,2,1,3};
char *dropstrings[4] = {"+","-","\264","\270"};
float const step=2.0;
float x;
float y = NX_HEIGHT(&frame) - animationFloat;
int j = order[animationStep];
[self lockFocus];
[symbolFont set];
animationFloat += 2.0;
if (animationFloat >= NX_HEIGHT(&frame)) {
animationFloat = 0; /* reset Y */
animationStep++; /* go to next step */
if(animationStep==4) { /* go to next effect */
animationStep = 0;
animatingDrop = NO;
animatingDissolve = YES;
animationFloat = 0.0;
}
}
[self unlockFocus];
return self;
}
Display PostScript Timed Entries 379
[self lockFocus];
[bigTextFont set];
[image dissolve:animationFloat
toPoint:&myPoint]; /* draw image */
PSsetgray(1.0 - (animationFloat/2.0));
PSrectfill(NX_X(&frame),
SIZE, NX_WIDTH(&frame), 2.0);
animationFloat += 0.005;
if(animationFloat < 1.0) {
[self unlockFocus];
return self;
}
/* finish up */
[smallTextFont set];
[self removeTE];
[self unlockFocus];
return self;
}
9. Insert the drawSelf:: method (the same one as before) and the @end
directive below at the end of InfoView.m.
- drawSelf:(const NXRect *)rects :(int)rectCount
{
PSsetgray(NX_WHITE);
NXRectFill(&bounds);
return self;
}
@end
10. Save the new InfoView.h and InfoView.m files, make and run your
updated application.
Your new version of MathPaper should work the same, but with one impor-
tant improvement: you can now use your MathPaper windows while the
animation is taking place. You can also cancel it by clicking the Info
Panel’s close button. The “wait” cursor ( ) does not appear during the
animation.
Summary
In this chapter we learned about PostScript wraps – the way that the NeXT-
STEP Application Kit communicates with the Display PostScript Window
Server. We also learned a little bit about drawing directly with PostScript
inside a View object, and then explored Display PostScript timed entries.
This chapter marks the end of our MathPaper odyssey, although we’ll be
using parts of it in our next major application, GraphPaper, which starts in
Chapter 16. Before that in the next chapter, we’ll learn much more about
the drawSelf:: method – the proper way to make your View show its stuff.
15
Draw Yourself:
All About NeXTSTEP Views
In the previous chapter we saw how to draw in a View with simple Post-
Script commands in two ways:
(i) by nesting the PostScript commands between the lockFocus and
unlockFocus messages to the View in which the drawing was to take
place, and
(ii) by placing the PostScript commands within the drawSelf:: method of
a View.
While it’s fine to call the focus methods for simple animation as in (i), this
isn’t the way that most NeXTSTEP programs go about drawing in win-
dows. Instead of explicitly invoking calls to lockFocus and unlockFocus,
most NeXTSTEP programs accomplish drawing by subclassing the View
class and then placing all of the actual PostScript drawing commands in a
single method called drawSelf::.
The purpose of this chapter is to make you feel at home with the draw-
Self:: method and NeXTSTEP Views in general.
381
382 Draw Yourself: All About NeXTSTEP Views
The inherited bounds instance variable describes the frame of the View in
its own coordinate system.
6. Back in IB, resize MyWindow so it’s about two inches wide and one
inch tall. The size you end up with isn’t important.
7. Drag a CustomView icon from IB’s Views palette and drop it in
MyWindow.
8. Change the class of the custom View to BlackView in the
CustomView Inspector.
9. Resize the BlackView instance so it’s about the size shown in the
window on the left of Figure 1 below.
The message appears because the BlackView class isn’t compiled into the
version of Interface Builder that you’re using. (You can create your own
custom palettes in Interface Builder if you want; consult the Interface
Builder documentation to learn how to do this.) To see BlackView work,
you need to make the application.
11. Save the BlackViewDemo.nib and BlackView.m files, make and run
your BlackViewDemo. You’ll see the BlackView instance in all its
glory, as in the window on the right of Figure 1 above.
(All make had to do was to copy the new BlackViewDemo.nib file into
the BlackViewDemo.app (or BlackViewDemo.debug) directory, because
A Closer Look at the View Class 385
we only changed the nib file and it’s not bundled into the Mach-O execut-
able.)
When you change a View’s coordinate system, its bounds instance variable
is automatically updated to reflect the change, while its frame instance
variable remains the same. The View class provides the following methods
for inspecting and changing a View’s coordinate system:
View Method Purpose
(float) boundsAngle Returns a floating point number for
the angle, in degrees, between a
View’s coordinate system and the
coordinate system of its superview.
(float) frameAngle Returns the angle of the View's
frame relative to its superview's
coordinate system. A value of 0
means that the View has not been
rotated (but its coordinate system
may have been).
(BOOL) isRotatedFromBase Returns TRUE if a View or any of
its ancestors have been rotated
from the window coordinate sys-
tem.
386 Draw Yourself: All About NeXTSTEP Views
If you supply nil as an argument to the methods that take aView as an argu-
ment, the methods will convert to or from window coordinates.
Flipping
Views may be flipped, which means that increasing y coordinates move
down the screen, instead of up (the way the Display PostScript coordinate
system normally works). A flipped coordinate system is handy for building
Views like the Text object, which naturally move down, and for which you
want to be able to calculate a y coordinate by multiplying a line number by
a constant.
Each View has an instance variable called opaque (really a bit field) which
specifies whether or not a View completely fills its frame when it is drawn
(so that you can’t see anything behind the View). If your View has holes in
it, or does not completely set every pixel within its frame, opaque should
be set to FALSE. It is important to set this variable properly to reflect what
your View does; this minimizes the amount of redrawing that needs to be
done when your Views are redisplayed.
When the mouse is clicked in your View, the Window object uses the hit-
Test: method (shown below) to determine if the View was clicked or not.
You can override this method if parts of your View should not be mouse
sensitive – for example, if your View displays itself as a triangle:
Displaying Views
There are several different display methods, all which eventually invoke
drawSelf::.
If your View will never draw beyond its boundaries, you can turn clipping
off with the setClipping: method, which will further speed drawing:
Controlling Redisplay
Most NeXTSTEP Views need to redisplay themselves when something
about their internal state changes. For example, a TextField object needs to
redisplay itself when the contents of the TextField change. If you write
your own custom View, you may override these methods to improve draw-
ing performance under certain circumstances.
The following methods are used for managing the redisplay of Views.
Resizing
When a window is resized, the Window class automatically sends a resize-
Subviews: method to the Window’s content view. The resizeSubviews:
method is then passed down through the view hierarchy, resizing or not
resizing the subviews as necessary.
11. Connect the slider to the BarView instance so that it sends the
takePercentage: action.
12. Unparse the BarView class and insert it into your project.
13. Insert the four lines in bold below into BarView.h.
#import <appkit/appkit.h>
@interface BarView:View
{
float percentage;
}
- takePercentage:sender;
- initFrame:(NXRect *)r;
- drawSelf:(const NXRect *)rects :(int)rectCount;
- setPercentage:(float) newPercentage;
@end
The takePercentage: method is the action that the slider takes when it’s
been manipulated. It gets the value of the slider using [sender floatValue]
and then invokes the setPercentage: method (shown below) to set the per-
centage in the on-screen BarView.
394 Draw Yourself: All About NeXTSTEP Views
15. Insert the three new methods implementations below into BarView.m.
The first two override methods in the View class.
/* designated initializer */
- initFrame:(NXRect *)r
{
[super initFrame: r];
[self setDrawSize: 1.0 :1.0];
[self setOpaque: YES];
return self;
}
rect = bounds;
NX_HEIGHT(&rect) = percentage;
PSsetgray(NX_WHITE);
NXRectFill(&rect);
NX_Y(&rect) = percentage;
NX_HEIGHT(&rect) = 1.0 - percentage;
PSsetgray(NX_BLACK);
NXRectFill(&rect);
return self;
}
The setPercentage: method sets the percentage instance variable, tells the
View’s superclass that redisplay is needed, and then redisplays itself if
autoDisplay is true.
The setPercentage: method is included in the class interface so you can set
the value in the BarView directly from an Objective-C statement in your
program, without having to use a NeXTSTEP Control object such as a but-
ton or a slider. When you are designing classes, you should try to include
methods that will make the class useful in future application programs as
well as the current program on which you are working.
16. Save all pertinent files and make and run BarViewDemo.
17. Drag the slider knob and the bar graph should lower and raise, as in
Figure 4 below.
18. Quit BarViewDemo.
9. Set the range of the slider to be from 3 to 30 and set its current value to
3 in the Slider Attributes Inspector.
10. Add the takeNumSidesFrom: action to the PolygonView class in the
CustomView Inspector.
11. Connect the slider to the PolygonView instance so that it sends the
takeNumSidesFrom: action message.
12. Unparse the PolygonView class and add it to your project.
PolygonView: A Non-Opaque View 397
@implementation PolygonView
- takeNumSidesFrom:sender
{
[self setNumSides:[sender intValue] ];
return self;
}
- initFrame:(NXRect *)r
{
[super initFrame:r];
[self setDrawSize:2.0 :2.0];
[self setDrawOrigin:-1.0 :-1.0];
[self setOpaque:NO];
sides = 3;
return self;
}
The initFrame: method sets the coordinates for the drawing system to
range from (-1,-1) to (1,1). It then sends the [self setOpaque:NO] message
to tell PolygonView that it will not be fully covering its frame rectangle.
We’ll discuss the takeNumSidesFrom: method after the next step.
15. Insert the two methods below into PolygonView.m.
- drawSelf:(const NXRect *)rects :(int)rectCount
{
float theta;
PSmoveto(sin(0.0), cos(0.0));
PSlineto(sin(theta),cos(theta));
}
PSsetgray(NX_BLACK);
PSfill();
return self;
}
- setNumSides:(int)val
{
if (val>0 && sides!=val) {
sides = val;
[self setNeedsDisplay:YES];
if ([self isAutodisplay]) {
[self displayFromOpaqueAncestor:&bounds
:0 :NO];
}
}
return self;
}
The drawSelf:: method above traces the outline of the polygon, sets the
color that we will be drawing with, then fills it in. The takeNumSides-
From: and setNumSides: methods work together to react to slider manipu-
lations and set the number of sides of the polygon to be displayed.
[self convertRectToSuperview:&obounds];
[self sizeTo:size :size];
[self setDrawSize:2.0 :2.0];
[self setDrawOrigin:-1.0 :-1.0];
[self convertRectFromSuperview:&obounds];
[self displayFromOpaqueAncestor:
NXUnionRect(&obounds,&bounds) :1 :NO];
return self;
}
- takeFloatSize:sender
{
[self setSize:[sender floatValue] ];
return self;
}
400 Draw Yourself: All About NeXTSTEP Views
The setSize: method is a little tricky, because the “size” is the coordinate
system of the containing View, rather than the PolygonView itself (which
is scaled from -1 to 1 in each dimension for easy drawing).
The method first gets the “old” bounds of the PolygonView instance and
converts it to the coordinate system of its superview (the window’s content
View). The method then resizes (sizeTo::), sets the scale of (setDraw-
Size::), translates (setDrawOrigin::) the PolygonView instance (self) so
that it ranges from (-1,-1) to (1,1), and converts its old bounds from the
superview’s coordinate system back to its own coordinate system. We need
to play this switching game because the coordinate system changed when
we changed the size of the PolygonView.
The polygon gets larger and trespasses into territory (the slider area) where
it shouldn’t! It looks terrible and is not the correct way to handle such a sit-
uation. We’ll discuss a remedy right away.
27. Quit PolygonViewDemo.
This time, as you make the PolygonView bigger, the ScrollView will auto-
matically scale the scroll knobs to accommodate the change in size, as in
the window on the right of Figure 9. Notice that the scroll knobs and but-
tons automatically appear and disappear as needed: they are handled auto-
matically for you by the NeXTSTEP ScrollView and Scroller objects.
Furthermore, the PolygonView object doesn’t know that it is being drawn
inside a ScrollView: you didn’t have to modify any of your code.
In the remainder of this section, we’ll show how to receive and interpret the
mouse-down and mouse-up events.
NXRunAlertPanel([NXApp appName],
"Mouse clicked %s polygon.", 0, 0, 0,
res ? "inside" : "outside");
return self;
}
This mouseDown: method receives the data (theEvent) from the mouse-
down event and converts the location where the event occurred from Win-
dow coordinates to the View’s own coordinate system. It then locks the
PostScript focus, redraws the path, and then uses the PostScript function
PSinfill() to find out if the mouse click was inside or outside the filled area.
The path is not redisplayed because we don’t send a PostScript display
operator, such as stroke or fill, to the Display PostScript interpreter.
The method then displays an Alert panel saying that the mouse was either
“inside” or “outside” the polygon.
An improvement for this class would be to put the path-drawing code for
both drawSelf:: and mouseDown: into a module used by both – or better
yet, use a PostScript user path to describe the path only once. For more
information about user paths, see Adobe’s “purple book,” Programming
the Display PostScript System with NeXTSTEP.
3. Save the PolygonView class files and make and run
PolygonViewDemo.
4. Drag the vertical slider, and click the mouse outside the polygon. See
Figure 10 below.
5. Click OK and then click the mouse inside the polygon.
6. Quit PolygonViewDemo.
hand slider should remained anchored to the right hand side of the window,
while the bottom slider should remain anchored to the bottom. Fortunately,
NeXTSTEP gives us an easy way to add these features, through IB’s Size
Inspector.
The horizontal springs are for horizontal resizes; the vertical springs are for
vertical ones. The inside box is for stretching, while the outside box is for
anchoring. A spring in the inside box indicates that the object should
stretch when it is resized. A line on the outside box indicates that the dis-
tance between the object and the side of the window should remain fixed if
at all possible; a spring indicates that it should be resizable.
1. Back in IB, select the PolygonView instance and type Command-3 to
bring up the Size Inspector. Click the lines in the inside box so four
springs appear as on the left of Figure 11 below.
2. Select the horizontal slider and click lines in the inside and outside
boxes so three springs appear as at the bottom of Figure 11.
3. Select the vertical slider and click lines in the inside and outside boxes
so three springs appear as on the right of Figure 11.
406 Draw Yourself: All About NeXTSTEP Views
The one problem with resizing right now is not stretching, but shrinking. If
you make the window too small, you’ll end up with junk, as in the window
at the bottom of Figure 12. In NeXTSTEP 3.0, you can use Interface
Builder to set a minimum window size. We’ll see how to do that in
Chapter 18.
5. Quit IB’s Test Interface mode.
6. Save the PolygonViewDemo.nib file and make and run
PolygonViewDemo.
7. Drag the vertical slider knob, and then drag the different parts of the
window’s resize bar. It works!
8. Quit PolygonViewDemo.
Summary
In this chapter we learned a lot more about the View class, in particular the
drawSelf:: method, and a bit more about resizing. In the next chapter we’ll
start building our next major application, GraphPaper, which has a window
that graphs equations.
408 Draw Yourself: All About NeXTSTEP Views
16
GraphPaper: A Multi-Threaded
Application with a Display List
In this chapter, we’ll use the Evaluator back end that we built in Chapter 10
as the basis for a program that graphs single-valued functions. The pro-
gram’s main window will end up looking like the one in Figure 1 below.
407
408 GraphPaper: A Multi-Threaded Application with a Display List
In the process of developing this program, we’ll learn more about the
drawSelf:: method, see how to construct a complicated image out of many
individual pieces, and learn a little bit about threads – Mach’s system for
lightweight multiprocessing.
GraphPaper’s Design
Conceptually, our program to graph a function will contain four main parts:
(i) an interface that lets the user specify the function and the graph param-
eters, and start and stop the graphing process,
(ii) a pair generator, which takes the graph parameters and generates pairs
of (x,y) points to be plotted,
(iii) a graph builder, which takes the pairs from the pair generator and
builds the data structure of the graph, and
(iv) a graph displayer, which takes the data structure and displays it on the
screen.
The Interface
We’ll build GraphPaper’s interface with Interface Builder (of course!). The
interface will consist of a NeXTSTEP Form object containing several text
fields (xmin, ymax, etc. in Figure 1), a button (Graph) to start the graph-
ing process, and a custom View called the GraphView. The GraphView
object will contain the brains of the GraphPaper application.
substitute the value of the variable x for the letter “x” for every point that it
graphs before it sends it to the Evaluator. That’s what we mean when we
say “numeric” algebraic pairs. For example, suppose the user wanted the
graph the equation
y(x) = 2*x + 1
The Evaluator, in turn, would evaluate each of these expressions and send
them back in a form that looks like this:
0, 1
1, 3
2, 5
...
10, 21
The third part of the GraphView object will “watch” for the results from
the Evaluator and incorporate them into a data structure called a display
list. The display list that GraphView will use is an Objective-C List
object, which contains a list of other objects. Each object in this list will
know how to respond to two methods: bounds and drawPSInView:. We
will have to implement these methods for each class whose members we
wish to put into the display list.
When an object in the display list receives the bounds message, the object
returns a pointer to an NXRect structure that describes the object’s size and
position. When an object in the display list receives a drawPSInView:
message, it generates the PostScript code to “draw itself.”
Initially, we’ll only have one kind of object that can be put into the display
list. It will be called Segment, and it will be used to represent a line seg-
ment of the final plot, from one (x,y) pair to another. In addition to
responding to the bounds and drawPSInView: messages, a Segment
object will also have a special initx1:y1:x2:y2: method for initialization
and a free method, which frees the memory associated with a Segment
instance.
410 GraphPaper: A Multi-Threaded Application with a Display List
The last part of the GraphView object does the actual drawing of the
graph. This part is taken care of by the drawSelf:: method. The drawSelf::
method will look at the rectangle where it has been requested to perform
drawing and send a message to the objects in the display list that intersect
that region. Another set of methods in the GraphView class will take care
of scaling the GraphView to the size requested by the user.
Handling (i) and (ii) at the same time is no problem: we saw how to do that
with the MathPaper application in Chapter 11. The NeXTSTEP Applica-
tion object’s event loop that watches for user events will also watch for
data on a file descriptor.1 The problem is (iii) – sending data to the Evalua-
tor process. Doing this concurrently with (i) and (ii) presents a problem
with NeXTSTEP. The problem has to do with the way that UNIX handles
pipes (interprocess communication channels).
When two programs are connected with a pipe, UNIX makes allowances
for the fact that one of the programs might be able to send data before the
1. File descriptors are also called file handles. They are the small integers that are
returned by the UNIX open() system call and are used by the read(), write(), and
close() calls.
Working with Multiple Threads 411
other program is ready to read it by allocating a buffer for the pipe. Instead
of sending the data directly from program A to program B, UNIX sends the
data from program A to the pipe buffer, then from the buffer to program B.
This lets program A write the data and keep going.
Since it is much faster to send data to the Evaluator than it is for the Evalu-
ator to process the data and send it back, it’s reasonable to assume that any
process sending data to the Evaluator is eventually going to be blocked.
Unfortunately, the Evaluator is sending data back to the same process from
which the data came. The returned data is being sent back through another
pipe. That pipe can then fill up just as easily as the pipe that sends data to
the Evaluator. This could result in a deadlock condition, with both pipes
filled and both processes blocked, each waiting for the other to empty the
pipe from which it is reading. The main GraphPaper process would block
because the Evaluator couldn’t accept any more data, and the Evaluator
would be blocked because the GraphPaper application wasn’t emptying its
pipe either.
The solution is to use a third process – one that only has the job of sending
data to the Evaluator. In the example, we call this process the stuffer
thread. When the Evaluator gets busy and the pipeline gets filled, the
stuffer thread blocks. But since all this thread does is send data to the Eval-
uator, it doesn’t matter if it gets blocked, because no blocked process will
be waiting for the stuffer.
Although we could send the data to the Evaluator with a completely differ-
ent process, a far more elegant (and efficient) way to do it is with a light-
weight process, or Mach thread. Simply put, a thread is a second process
that shares the same program and data space with the program that created
it. A thread can access the same global variables as the program that creates
it, but runs on its own schedule and can lock its own resources. Threads
and multi-thread programming are an important part of the Mach operating
system.
interactions in adverse ways unless you are careful to anticipate and avoid
such interactions. For example, every time you want to use a global vari-
able, you’ve got to lock it, so that another thread doesn’t change it out from
under you.
To see how this could happen consider the following simple example. Sup-
pose a function in a multi-threaded program wanted to increment a global
variable called count. In a single threaded application program, you would
use an expression like this:
extern int count;
count++;
The way around this problem is by using an exclusive lock called a mutex,
short for mutual exclusion. You can create a mutex with the mutex_alloc()
function:
mutex_t count_lock;
count_lock = mutex_alloc();
You could then write the “incrementing” code discussed above using a
mutex as follows:
extern int count;
extern mutex_t count_lock;
mutex_lock(count_lock);
count++;
mutex_unlock(count_lock);
It’s obviously more work to write an application that uses multiple threads.
These applications are also harder to debug. For these reasons, the Applica-
tion Kit isn’t multi-threaded. This means that if you write a multi-threaded
application, you should send messages to App Kit objects only from your
application’s main thread.
Although the Application Kit isn’t multi-threaded, it doesn’t mean that you
shouldn’t use multiple threads – just don’t use them to update the screen.
For example, the Sound Kit uses multiple threads to play music. You
should generally write your application so that each thread only interacts
with a single NeXTSTEP kit (such as the App Kit, DBKit, or Sound Kit).
C Threads
NeXTSTEP uses the Mach C Threads package to handle multiple threads.
The C Threads package enables you to do the following:
• create a new thread from your main process (called forking),
• wait for a thread that you’ve created to terminate (called joining),
• create and lock mutexes to protect global variables from simultaneous
modification, and
• synchronize execution between threads.
Using C Threads
To use C Threads you must include the cthreads.h file (in NextDeveloper/
Headers/mach) in your program. Once you’ve done that, you can start a
new thread by simply calling the cthread_fork() function as follows:
cthread_fork( (cthread_fn_t)aFunction,
(any_t)anArgument);
414 GraphPaper: A Multi-Threaded Application with a Display List
This function call causes a new thread to be created. The thread will start
by calling the function aFunction with the argument anArgument. If you
need more than one argument, you can make anArgument a pointer to a
structure that contains more. The thread terminates either by calling the
function cthread_exit() or by returning from the function aFunction.
If you never intend to rejoin a thread that you have created, you might want
to consider detaching the thread. You can’t join a thread once you detach it.
The advantage is that a detached thread runs slightly more efficiently than
one that isn’t. (When a thread is detached, an internal variable is set which
tells the C Threads package to ignore the exit value of the thread. If you
don’t care how your thread terminates, detach it. Otherwise, don’t.) For
more information about these function calls, search for “cthread” in Head-
erViewer or Librarian.
1. In NeXTSTEP 3.0, if you must use Objective-C from more than one thread, you
can call Objective-C function objc_setMultithreaded(), which will cause Objec-
tive-C to use a thread-safe version of its messaging function. Using this function
results in a substantial performance penalty to the Objective-C runtime system
(each message call takes three times longer than it would otherwise). Furthermore,
even though the runtime system is threadsafe after calling this function, few of the
Objective-C classes, such as List and HashTable – and none of the Application Kit
– are threadsafe. For this reason, we do not recommend using Objective-C from any
thread in your application program other than the main thread.
Building the GraphPaper Application 415
localhost> make
yacc grammar.y
yacc -d grammar.y
lex rules.lex
cc -O -o Evaluator y.tab.c lex.yy.c -ly -ll -lm
localhost>
4. Install the Evaluator program in your ~/Apps directory by typing
make install as follows:
localhost> make install
strip Evaluator
cp Evaluator /simsong/Apps
localhost>
5. Test the new Evaluator from the command line to be sure that it
understands the new comma notation.
localhost> Evaluator
3,2*3+1
3,7
^Clocalhost>
The GraphView class we’ll use to display function graphs will be a sub-
class of the View class. It will need outlets to point to most of the on-screen
objects and actions to start and stop the graphing.
Building the GraphPaper Application 417
5. Subclass the View class in IB’s File window. Rename the new class
GraphView.
6. In the Class Inspector, add the following outlets and actions to
GraphView:
outlets: actions:
graphButton graph:
xminCell stopGraph:
xmaxCell
xstepCell
yminCell
ymaxCell
formulaField
7. Unparse the GraphView class and insert it into your project.
12. Drag a Form object (seen at the left) from IB’s Views palette and drop
it in the right side of the GraphPaper window.
13. Alt-drag the bottom middle handle of the Form object to create three
more FormCells, for a total of five FormCells.
14. Make the Form labels larger by typing Command-t to bring up IB’s
Font panel and then selecting an appropriate font (e.g., Helvetica Bold
14 pt.).
15. Change the labels on the FormCells to xmin, xmax, etc., as in
Figure 2 above. (Use the tab key to move quickly from one FormCell
to the next.)
16. Enter the numbers 0.0, 10.0, 0.1, -1.0, 1.0 in the five text areas of the
Form, as in Figure 2 above, to set up defaults that will show the user a
good looking graph at start up time.
17. Select the Form matrix as a whole and change the Text to be right-
aligned ( ) in the Form Attributes Inspector.
18. Drag a TextField icon (seen at the left) from IB’s Views palette and
drop it near the bottom of the GraphPaper window.
19. Make the text in the TextField larger using IB’s Font panel. Make the
TextField wider as well.
20. Enter a function which has an interesting graph in the TextField. We’ll
use sin(3*x), which will lead to an interesting graph at launch time.
21. Drag a Title icon from IB’s Views palette and drop it to the left of the
TextField object. Change the text to “y(x) =” as in Figure 2 above.
This Title icon actually represents another TextField object with
attributes such as uneditable, gray background, etc.
22. Drag a Button object from IB’s Views palette and drop it in the
GraphPaper window below the Form as in Figure 2. Change the
label on the Button object to “Graph” and make the button text larger.
23. Connect the seven GraphView outlets to the appropriate on-screen
objects. That is, connect the graphButton outlet to the Graph button,
the xmaxCell outlet to the FormCell labeled xmax, and so on. The
formulaField outlet should be connected to the TextField object.
See the Connections Inspector in Figure 3 below.
(Don’t be confused that these connections are being made between two
objects in the same GraphPaper window. The GraphView instance in
the main window simply has some outlets that you are setting.)
Building the GraphPaper Application 419
Later in this chapter we’ll use the graphButton outlet to show how to tem-
porarily change the title on the button from “Graph” to “Stop” while
GraphPaper is drawing a graph.
24. Connect the Graph button to the on-screen GraphView instance.
Make it send the graph: action.
@interface GraphView:View
{
id formulaField;
id graphButton;
id xmaxCell;
id xminCell;
id xstepCell;
id ymaxCell;
id yminCell;
id graphProc;
id displayList;
BOOL first; /* first datapoint */
double lastx, lasty; /* for drawing graph */
char graphBuf[17000]; /* buf to graph */
double ymin,ymax;
cthread_t stuffer_thread;
BOOL graphing;
char *formula;
int toFd;
double xmin, xmax, xstep;
}
- graph:sender;
- stopGraph:sender;
- addBufToGraph:(const char *)buf;
- graphLine:(const char *)buf;
- addGraphSegmentx1:(float)x1 y1:(float)y1
x2:(float)x2 y2:(float)y2;
- drawSelf:(const NXRect *)rects :(int)rectCount;
- clear;
- awakeFromNib;
@end
#define GRAPH_TAG 0
#define LABEL_TAG 1
#define AXES_TAG 2
Building the GraphPaper Application 421
The first seven id statements declare the outlets we set up and connected in
IB. The remaining instance variables are a little more complicated. Here is
a brief description of what the first two inserted variables do:
The next group of variables are used for constructing the graph data struc-
ture with the (x,y) pairs returned by the Evaluator:
The next group of variables (on the next page) are used by the thread pro-
cess that sends data to the Evaluator. Most of them are @public variables
that are used to provide communication between the main process and the
stuffer thread. We use @public variables so we can access them directly
from the stuffer thread, without having to message the GraphView
instance.1
We’ll discuss the new methods as we progress through this chapter. Lastly,
the #define statements set up the tags that we will use for various parts of
the graph.
We’ve discussed the Process.h include file in our description of the Math-
Paper application in Chapter 11. The new Segment class is where a line
segment element is created to draw part of the graph. We’ll set up the Seg-
ment class later in this chapter.
Building the GraphPaper Application 423
Although the function dataStuffer() appears in the source code before the
two methods, we’ll discuss it last for clarity. The graph: method starts the
graphing. It’s invoked when the user presses the Graph button.
28. Insert the lines in bold below into the graph: action method in
GraphView.m.
- graph:sender
{
/* initialize for a graph */
[self clear];
if ( xstep<=0 ) {
NXRunAlertPanel(0,"Need a positive step",0,0,0);
return self;
}
if (formula) {
/* Free the old formula if we have one */
NXZoneFree([self zone], formula);
formula = 0;
}
formula = NXCopyStringBufferFromZone(
[formulaField stringValue],
[self zone]);
The next two lines set the scale and the origin of the graph. The inherited
View method
- setDrawSize:(NXCoord)width :(NXCoord)height
sets the scale of units of the coordinate system used by GraphView so that
it is width units wide and height units high. The inherited View method
- setDrawOrigin:(NXCoord)x :(NXCoord)y
readjusts the GraphView’s origin so that the point (x,y) is drawn in the
lower-left hand corner.
The “formula =” line makes a copy of the contents of the formula cell.
Notice that we use the NeXTSTEP function NXCopyStringBufferFrom-
Zone() which automatically allocates a block of memory for a null-termi-
nated string from a particular memory zone (in this case, the memory zone
associated with the GraphView instance itself.)
The next line changes the title of the on-screen button from “Graph” to
“Stop” and changes its action so that it sends the stopGraph: message. If
Building the GraphPaper Application 425
the user now clicks the button, the stopGraph: message gets sent to the
GraphView. This is a cute way to rewire an application while it is running.
The statement first=YES sets the first instance variable so that the method
that builds the graph will know that a new graph is being created. The state-
ment graphing=1 sets the instance variable graphing, which is used to
control the stuffer thread. Finally the line containing the cthread_fork()
call causes the data stuffer thread to be created.
This method clears the graphing state variable and then calls the
cthread_join() function, which causes the main process to wait until the
data stuffer thread finishes executing. The stopGraph: method then frees
its copy of the formula. Finally, it changes the title of the Button object
back to “Graph” and resets the Button’s action to send the graph: mes-
sage.
30. Insert the dataStuffer() function below into GraphView.m. Put it after
the #import directives but before the @implementation directive.
/* dataStuffer: a thread fork function that
* puts data into the pipe.
*/
int dataStuffer(GraphView *obj)
{
double x;
Recall that when the data stuffer thread was created, it was passed a single
variable, namely self. This is the id of the GraphView object, which lets
the dataStuffer() function access all of GraphView’s @public variables.
The body of the loop is a little complicated. It builds the expressions that
are sent to the Evaluator. To do this, it must search the formula for all
occurrences of the letter “x” and replace them with the current value of the
variable x.
Building the GraphPaper Application 427
When the loop finishes, the function sends the number 999 to the Evalua-
tor. This number is used as a flag to indicate that no more data is coming
through the pipe. The procedure that constructs the graph looks for a 999
on a line by itself and uses that flag as its way of knowing that the graph is
finished. The digits 999 really don’t matter: what’s important is that the
Evaluator is sent a line of data with one expression and no comma. Finally
the dataStuffer() function returns, which terminates the thread.
And the Evaluator sends back a series of numbers that look like this:
0, 1
1, 3
2, 5
The GraphView object needs to take those pairs of numbers and construct
a graph. To do this we use a function that is called by Display PostScript
when data is available, and three methods for processing the data.
This function takes the data and invokes the addBufToGraph: method
(discussed below), which in turn adds the data to the GraphView’s internal
buffer and then reads the data out line-by-line.
32. Insert the addBufToGraph: method below into GraphView.m after
the @implementation directive but before the @end directive.
- addBufToGraph:(const char *)buf
{
char *cc;
if (graphing==0) {
return self; /* not graphing */
}
return self;
}
The reason for the line-by-line buffering is that the Evaluator might send
more than one line of data to the GraphView object before it is scheduled
to read the data. (This is because the data is being generated by a different
execution thread.) It might also send an incomplete line, due to blocking on
the pipe. The GraphView object therefore needs to buffer the data that it
receives and then read it out a line at a time.The addBufToGraph: method
does that buffering, and invokes the graphLine: method for each line.
It’s important to note that the addBufToGraph: method ignores the data
that it is sent if the graphing instance variable is 0. This means that once
the user clicks on the Stop button, all of the rest of the data in the pipeline
will be ignored. This gives the application a nice snappy feeling.
The next two methods perform the actual graphing. The first is graph-
Line:, which constructs the graph’s line segments from successive (x,y)
pairs.
33. Insert the graphLine: method implementation below into
GraphView.m.
- graphLine:(const char *)buf
{
double x, y;
int num;
if (!first) {
id seg = [self addGraphSegmentx1:lastx y1:lasty
x2:x y2:y ];
lastx = x;
lasty = y;
first = NO;
return self;
}
The graphLine: method uses the sscanf() function to turn the line of text
from the Evaluator back into binary numbers. If num!=2, then there were
not two numbers separated by a comma to read; the method checks for the
999 termination code, which means that the graph is finished. If the termi-
nation code was sent, the graphLine: method invokes the stopGraph:
method and the graph stops.
If this data pair is the first data pair, the execution drops down to the last
four lines. These lines set the instance variables lastx and lasty to be the
coordinates of the current point, then unsets the first variable and returns.
On all other data pairs other than the first, the middle section of this method
gets executed. This conditional code first creates a Segment object
(described below) with endpoints at (lastx,lasty) and (x,y). The Graph-
View then locks focus and sends the drawPSInView: message to the Seg-
ment object, which causes it to draw itself. The NXPing() function
synchronizes the action. Finally, the segment gets added to the display list.
The last “graph-constructing” method adds the graph segment to the dis-
play list.
34. Insert the addGraphSegmentx1:y1:x2:y2: method below into
GraphView.m.
- addGraphSegmentx1:(float)x1 y1:(float)y1
x2:(float)x2 y2:(float)y2
{
id seg = [ [Segment alloc]
initx1:x1 y1:y1 x2:x2 y2:y2 ];
[seg setTag:GRAPH_TAG];
[displayList addObject:seg];
return seg;
}
This method also sets the tag of the segment to GRAPH_TAG. We’ll use
the tags later on to distinguish between different objects stored inside the
display list. Note that this method returns the id of the newly-created Seg-
ment object rather than self. Although the
addGraphSegmentx1:y1:x2:y2: method might seem a little trivial right
Building the GraphPaper Application 431
if (displayList) {
for (j=0; j < [displayList count]; j++) {
id obj = [displayList objectAt: j];
NXRect oBounds = [obj bounds];
if (NXIntersectsRect(&rects[i],
&oBounds)) {
[obj drawPSInView:self];
}
}
}
}
return self;
}
As you can see, for the first time we are using the drawSelf:: parameters.
We’re finally going to find out what they mean!
redraw the entire image – especially if you only need to redraw a tiny sliver
of the image that’s been exposed because the user has moved the slider a
smidgen. That’s what drawSelf::’s arguments are for – they tell your
drawSelf:: method which part of the screen to redraw.
That’s it for the drawSelf:: method. This simple method will not only han-
dle printing and faxing, but it’s optimized to redraw the absolute minimum
amount of the graph that’s ever required.
[self display];
return self;
}
if(![[NXBundle mainBundle]
getPath: path forResource: "Evaluator"
434 GraphPaper: A Multi-Threaded Application with a Display List
ofType: ""]){
NXRunAlertPanel(0,"Can't find Evaluator",0,0,0);
[NXApp terminate:self];
return nil;
}
argv[0] = path;
if (!graphProc) {
NXRunAlertPanel(0,
"Cannot create calculator: %s",
0, 0, 0, strerror(errno));
[NXApp terminate: self];
return nil;
}
return self;
}
38. View your project’s “other” resource files by clicking the Files button
at the top of PB’s main window and then the Other Resources file
type in PB’s browser. See Figure 4 below.
FIGURE 4. Adding An “Other Resource” to GraphPaper
39. Add the Evaluator file to the project by dragging its icon from your
File Viewer into PB’s Files view. PB will automatically copy the file
into the project directory. Figure 4 shows that Evaluator has been
added to the MathPaper project.
When you make (build) your project, the Evaluator file will automatically
be copied into the .app or .debug directory.
@interface Segment:Object
{
NXPoint start;
NXPoint end;
float gray;
int tag;
}
- initx1:(float)x1 y1:(float)y1
x2:(float)x2 y2:(float)y2;
- setGray:(float)aGray;
- (float)x;
- (float)y;
- drawPSInView:aView;
- (NXRect)bounds;
- (int)tag;
- setTag:(int)aTag;
@end
41. Create a file called Segment.m containing the Segment class
implementation code below.
#import "Segment.h"
@implementation Segment
Building the GraphPaper Application 437
- initx1:(float)x1 y1:(float)y1
x2:(float)x2 y2:(float)y2
{
[super init]; /* must init the Object class! */
start.x = x1;
start.y = y1;
end.x = x2;
end.y = y2;
[self setGray: NX_BLACK];
return self;
}
- setGray:(float)aGray
{
gray = aGray;
return self;
}
- (float)x
{
return (start.x + end.x)/2.0;
}
- (float)y
{
return (start.y + end.y)/2.0;
}
- drawPSInView:aView
{
NXSize sz = {1.0, 1.0}; /* a default size */
PSsetgray(gray);
PSmoveto(start.x, start.y);
PSlineto(end.x, end.y);
PSsetlinewidth(1.0);
PSstroke();
- (NXRect)bounds
{
NXRect bounds;
NX_X(&bounds) = MIN(start.x,end.x) ;
NX_Y(&bounds) = MIN(start.y,end.y) ;
NX_WIDTH(&bounds) = fabs(start.x-end.x)+MINFLOAT;
NX_HEIGHT(&bounds) = fabs(start.y-end.y)+MINFLOAT;
return bounds;
}
- (int)tag
{
return tag;
}
- setTag:(int)aTag
{
tag = aTag;
return self;
}
@end
Notice our use of MINFLOAT in the bounds method. This is used so that
lines that are vertical and horizontal will still have a width or height that is
Building the GraphPaper Application 439
non-zero. MINFLOAT is the smallest floating point number that the IEEE
floating point package can represent. By adding MINFLOAT to the calcu-
lated width and height, we guarantee that these values will not be zero. If
they are computed to be a number that is larger than MINFLOAT – for
example, the number 5 – adding MINFLOAT will have no significant
effect.
Testing GraphPaper
Now that we’ve got the interface built, the connections made, and all the
classes implemented, we’re finally ready to make and test GraphPaper.
44. Save all pertinent files and then make (build) the GraphPaper project.
45. Drag the GraphPaper file application icon from your File Viewer into
your dock and double-click it to launch GraphPaper.
46. Click the Graph button and you’ll get a graph as in Figure 5 below.
47. Click the Graph button again and then quickly interrupt the graphing
process by clicking the Stop button (the same button with a different
label while the graph is being drawn). See Figure 6 below.
48. Try graphing another function such as cos(x)/x with a different step
and ranges. See Figure 7 below.
49. Quit GraphPaper.
ish off this chapter by showing how to add different objects to GraphPa-
per’s display list.
FIGURE 5. GraphPaper at Runtime
Adding Axes
Adding X and Y axes to GraphView is quite simple, since we already have
the Segment class to draw the line. All we need to draw the axes is to cre-
ate a new method that draws the axes and then arrange for it to be called by
GraphView’s graphLine: method.
1. Insert the addAxesx1:y1:x2:y2: method declaration below into
GraphView.h.
- addAxesx1:(float)x1 y1:(float)y1
442 GraphPaper: A Multi-Threaded Application with a Display List
x2:(float)x2 y2:(float)y2;
2. Insert the addAxesx1:y1:x2:y2: method implementation below into
GraphView.m.
- addAxesx1:(float)x1 y1:(float)y1
x2:(float)x2 y2:(float)y2
{
id axes = [ [Segment alloc]
initx1: x1 y1: y1
x2: x2 y2: y2 ];
[axes setTag:AXES_TAG];
[axes setGray:NX_DKGRAY];
[displayList addObject:axes];
return axes;
}
3. Insert the six lines in bold below into the graphLine: method into
GraphView.m.
- graphLine:(const char *)buf;
...
return self; /* perhaps an invalid segment */
}
if (first) {
/* put in graph axes */
[self addAxesx1:xmin y1:0.0 x2:xmax y2:0.0 ];
[self addAxesx1:0.0 y1:ymin x2:0.0 y2:ymax ];
[self display];
}
if (!first) {
...
}
4. Save the GraphView class files and then make and run GraphPaper.
5. To see both the axes in GraphPaper, enter the values as in Figure 8.
When the first data point comes through, the new if statement in the graph-
Line: method above creates two Segment objects which correspond to the
X and Y axes. The “color” of these segments is set to dark gray and the tags
are set to 1. The axis lines are added to the display list and then the graph is
displayed. The window in Figure 8 below shows what the graph looks like
with axes. We set the tag to AXES_TAG so that we can distinguish these
segments from the segments used to draw the graph itself. This distinction
will be important in Chapter 18, when we want to make the graph segments
sensitive to mouse clicks without making the axes sensitive.
Extending the Display List 443
Adding Labeling
In addition to axes, we can add a function label fairly easily by creating a
Label class that responds to the same bounds and drawPSInView: meth-
ods as the Segment class. Since Objective-C uses dynamic binding –
where messages are resolved when they are sent, rather than when the pro-
gram is compiled – we won’t need to make any changes to GraphView’s
drawSelf:: method. Label objects will be stored in the display list, along
with the Segment objects. (The Label object must have its own initializa-
tion methods, however, because line segments and text labels need to be set
up in different ways.)
1. Create a file called Label.h containing the Label class interface code
below.
#import <appkit/appkit.h>
@interface Label:Object
{
NXRect bounds;
char *text;
float gray;
int tag;
id font;
}
444 GraphPaper: A Multi-Threaded Application with a Display List
Notice that this interface is remarkably similar to the interface used by Seg-
ment. The main difference is that the two objects use different initializa-
tion methods, which of course they must. The Label class implementation
itself is a bit more complicated than the Segment class. It starts out with
the initx:y:text:size:forView: method which we show in the next step.
2. Create a file called Label.m containing the (incomplete) Label class
implementation code below.
#import "Label.h"
@implementation Label
[super init];
fontScale.width = 1.0;
fontScale.height = 1.0;
text = NXCopyStringBufferFromZone(aText,
[self zone]);
Extending the Display List 445
[self setGray:NX_BLACK];
return self;
}
After the matrix is calculated, it is used to create a new font. The Label
object then makes its own private copy of the label that it displays. We use
the NXCopyStringBufferFromZone() method so that this string is allo-
cated in the same memory zone as the object itself. Objects frequently run
faster when all of their data is located in the same region of memory,
because the computer’s virtual memory system does not need to page as
much in order to access the data referenced by the object.
Finally, the method calculates a bounding box for this piece of text and
stores it in the bounds instance variable.
Since Label creates local storage (its private copy of the label string
pointed to by text), this storage should be released when the object is freed.
This is done by the free method below.
3. Insert the free method below into Label.m.
- free
{
if (text){
NXZoneFree([self zone], text);
text = 0;
}
return [super free];
}
446 GraphPaper: A Multi-Threaded Application with a Display List
The bounds, setGray:, tag and setTag: methods below simply provide
access to instance variables from outside the class.
4. Insert the four methods below into Label.m.
- setGray:(float)aGray
{
gray = aGray;
return self;
}
- (NXRect)bounds
{
return bounds;
}
- (int)tag
{
return tag;
}
- setTag:(int)aTag
{
tag = aTag;
return self;
}
- drawPSInView:aView
{
[font set];
PSsetgray(gray);
PSmoveto(NX_X(&bounds), NX_Y(&bounds));
PSshow(text);
return self;
}
@end
Extending the Display List 447
Notice that this drawPSInView: method sends the [font set] message
every time it is invoked. This is necessary to support printing. If the
GraphView that contains this label is printed, the font used needs to be
rasterized at a different resolution. A PostScript error results unless the
[font set] message is sent before PostScript code is generated for the
printer. (A more efficient approach would be to implement a protocol by
which objects in the display list could be told to invoke the set method
whenever PostScript was to be generated for a new context.)
[self display];
}
if (!first) {
...
}
9. Insert the new #import directive in below into GraphView.m.
#import "Label.h"
10. Back in PB, add the Label class to the GraphPaper project.
11. Save all pertinent files and make and run GraphPaper.
12. Enter a function and ranges and click the Graph button to see the new
label, as in Figure 1, “Main Window in GraphPaper Application,” on
page 407.
Summary
Wow, we’ve really done a lot in this chapter! In the first half of the chapter
we got a taste of multi-threaded programming in Mach by creating an
application with two execution threads – one which sends data to a back
end program and another which reads the resultant values. In the second
half, we learned how to draw a picture by building a display list of objects,
each object knowing how to draw itself.
In the next chapter, we’ll see how GraphView’s modular design makes it
easy to add new functionality to the class. In particular, we’ll see how to
add color by subclassing the GraphView class and overriding some of its
methods.
17
Color
If you are running NeXTSTEP on a color display, you may be feeling a lit-
tle left out, since all of the examples so far have been in black-and-white.
Fortunately, NeXTSTEP’s Display PostScript makes drawing in color rela-
tively easy.
449
450 Color
how to blend a color with the colors already present in the background
when the color is displayed. Alpha is measured on a scale from 0.0 to 1.0.
An alpha of 0.0 is completely transparent; an alpha of 1.0 is opaque.
NeXTSTEP allows you to specify the color using several different models.
Like most computer systems, you can specify values for the amount of red,
green and blue (RGB) you want to mix in an additive color model, where
the colors are added together like colored lights (mix them all and you get
white). Alternatively, you can also specify a color by specifying a hue, sat-
uration and brightness (HSB). You can also specify the amount of cyan,
yellow, magenta, and black (CMYK) “inks” to mix at any point (called a
subtractive color model since the colors are subtracted from white: mix
them all and you get a muddy brown). CMYK colors are device-dependent.
NeXTSTEP 3.0 also allows the user to specify color by PANTONE number.
PANTONE is a trademark system for specifying a color by industry-stan-
dard numbers. PANTONE colors are also device-dependent, however there
are tables which specify the correct device-dependent value for different
printers, allowing some degree of conversion.
The following eight functions all return an NXColor structure for the color
specified by their arguments. Only the first four allow an alpha transpar-
ency factor.
NXColor NXConvertRGBAToColor(float red,
float green, float blue, float alpha)
If you have a NXColor, you can determine its components with one these
functions:
void NXConvertColorToRGBA(NXColor color,
float *red, float *green, float *blue,
float *alpha)
The buttons along the top of the panel change the way that the Colors panel
displays colors. By selecting a color and then clicking a button at the top of
the Colors panel, you can see how the color is represented in different color
models. For example, in the Colors panel on the left of Figure 2 below we
chose fully saturated “green” in RGB mode and then clicked a button to see
the corresponding color in the CMYK mode in the Colors panel on the
right of Figure 2. (Unfortunately, it’s difficult to display a good “green” in
a monochrome book – you’ll have to play with the Colors panel on a com-
puter which displays NeXTSTEP in color to see the real thing).
The space along the bottom of the Colors panel is a holding area for colors.
You can drag a color from the color well at the panel’s right into the hold-
ing area. You can also drag colors directly into application programs.
The Colors panel works closely with another NeXTSTEP class called the
NXColorWell (the NXColorWell icon from Interface Builder’s Views pal-
Colors and Color Objects 453
ette can be seen at the left). If you press the mouse in a NXColorWell
instance and drag it away, you’ll take a little dab of color, called a color
chip, with you (see Figure 3 below). Every NXColorWell has a color; you
can change its color by dragging a color chip from a different NXColor-
Well and dropping it inside. The gray rectangle at the upper right of the
Colors panel is a color well whose color is linked to the current position of
the Colors panel sliders.
To isolate the parts of the GraphView that deal solely with color, we’ll cre-
ate a GraphView subclass called ColorGraphView. This way, we won’t
need to make any changes to the GraphView class itself, yet we can use all
its functionality. This is called reusability of classes. Unfortunately, we will
need to make some minor changes to the Segment and Label classes so
Adding Color to GraphPaper 455
that we can change their colors and have them draw using color PostScript
commands.
The user probably won’t want to change the graph’s colors every time the
GraphPaper application is run, so we’ll put the Preferences panel in its own
nib file rather than in GraphPaper.nib. That way the Preferences panel
will be loaded and take up memory only when the user chooses to see it.
The ColorGraphView class will have three NXColor instance variables to
keep track of the colors currently being drawn. We’ll set up a new class
called PrefController to take care of modifying these instance variables
when the Preferences panel is displayed. If the Preferences panel isn’t
loaded, the ColorGraphView class will make up some reasonable defaults
for the color instance variables. In Chapter 21, we’ll see how to set the
value for these colors from the NeXTSTEP “defaults” database the, appli-
cation defaults information stored in every NeXTSTEP user’s ~/.NeXT
directory.
In the upcoming sections we’ll make a simple Preferences panel for setting
the color of a graph in GraphPaper. This panel won’t be a fully-functional
Preferences panel, though, because it will be missing the OK and Revert
buttons for saving the Preferences information into the defaults database.
We’ll add those in Chapter 21.
First we’ll make a Controller class for our application which will act as a
central coordinator of the activity of the GraphView object and the Prefer-
ences panel.
456 Color
We’ll set up the Controller object so that it is the NXApp delegate. This
outlet will make it possible for any object in our application to get the id of
the GraphView instance by evaluating the expression:
[ [NXApp delegate] graphView]
Putting accessor methods in classes when you design them is good pro-
gramming practice. It’s called planning ahead.
In order to have the Preferences panel in its own nib, we will also need to
have the Controller class to load the nib on demand. This is similar to code
we saw in previous chapters to load the Info Panel nib on demand.
1. Open your GraphPaper project in Project Builder and the
GraphPaper.nib file in Interface Builder.
2. Subclass the Object class. Rename the new class Controller.
3. Add an outlet to the Controller class called graphView.
4. Add another outlet to the Controller class called prefController.
5. Add an action to the Controller class called showPrefs:.
6. Unparse the Controller class and insert the class files to the project.
7. Instantiate the Controller class in IB’s File window. A new icon
labeled Controller will show up in the Objects view.
8. Make the Controller the File’s Owner’s delegate by Control-dragging
from the File’s Owner icon to the Controller icon and double-clicking
delegate in the File’s Owner Inspector.
9. Connect the Controller’s graphView outlet to the on-screen
GraphView instance in GraphPaper’s main window.
10. Insert the graphView method declaration in bold below into
Controller.h.
#import <appkit/appkit.h>
@interface Controller:Object
{
id graphView;
Adding Color to GraphPaper 457
id prefController;
}
- graphView;
- showPrefs:sender;
@end
11. Insert the lines in bold below into Controller.m.
#import "Controller.h"
#import "GraphView.h"
@implementation Controller
- graphView
{
return graphView;
}
- showPrefs:sender
{
if (!prefController) {
[NXApp loadNibSection: "preferences.nib"
owner: self];
}
[ [prefController window]
makeKeyAndOrderFront:sender];
return self;
}
@end
Controller may seem like a gratuitous class: why not just make the
GraphView instance the delegate of the NXApp object? The answer will
become more clear as we add more features to the GraphPaper application:
having a separate Controller object will make it easier to add new func-
tionality.
The first time the showPrefs: action method is invoked it loads the prefer-
ences nib. Every time the showPrefs: action method is invoked it asks the
458 Color
prefController for the id of its window, and then exposes the window with
the makeKeyAndOrderFront: message.
12. Back in IB, cut (Command-x) out GraphPaper’s Info menu cell.
13. Drag an Info→ submenu from IB’s Menus palette and drop it just
below the main menu title bar, as at the left.
14. Enable the Info→Preferences menu cell.
15. Connect the Info→Preferences menu cell to GraphPaper’s Controller
instance so that it sends the showPrefs: action message.
16. Save GraphPaper.nib.
Don’t be concerned with the revert: and okay: actions now; we won’t be
using them until Chapter 21.
23. Unparse the PrefController class and insert its class files to the
GraphPaper project.
Adding Color to GraphPaper 459
#import <appkit/appkit.h>
@interface PrefController:Object
{
id axesColorWell;
id graphColorWell;
id labelColorWell;
id window;
}
- okay:sender;
- revert:sender;
- window;
- setUpWell:well defaultColor:(NXColor)aColor
tag:(int)aTag;
- awakeFromNib;
@end
31. Insert the two #imports below into PrefController.m.
#import "Controller.h"
#import "GraphView.h"
32. Insert the two method implementations below into PrefController.m.
- window
{
return window;
}
- setUpWell:well defaultColor:(NXColor)aColor
tag:(int)aTag
{
[well setColor:aColor];
[well setTag:aTag];
[well setContinuous:YES];
[well setTarget:[ [NXApp delegate] graphView] ];
[well setAction:@selector(setObjectColor:) ];
return self;
}
between nibs (which can only be done graphically inside Interface Builder
by using the File’s Owner).
33. Insert the method implementation below into PrefController.m.
- awakeFromNib
{
[ [NXColorPanel sharedInstance:YES]
setContinuous:YES];
[self setUpWell:axesColorWell
defaultColor:NX_COLORDKGRAY tag:AXES_TAG];
[self setUpWell:labelColorWell
defaultColor:NX_COLORBLACK tag:LABEL_TAG];
[self setUpWell:graphColorWell
defaultColor:NX_COLORBLACK tag:GRAPH_TAG];
return self;
}
The Application Kit uses a single NXColorPanel object for each running
application. The method [NXColorPanel sharedInstance:YES] returns
the id of that shared instance; if the Colors panel hasn’t been created yet, it
gets created. If we had specified sharedInstance:NO, the id of the panel
would have been returned only if the panel had been previously created.
ColorGraphView
In this section we’ll create the ColorGraphView class which knows how
to draw a graph in color and how to change the colors of the objects in the
display list.
34. Back in IB, select GraphPaper’s main window and turn on the “Wants
to be color” switch in the Window Attributes Inspector.
35. Subclass the GraphView class (under View) in the GraphPaper.nib
Classes view. Rename the new class ColorGraphView.
36. Change the class of the on-screen GraphView instance in
GraphPaper’s main window to ColorGraphView in the Inspector.
37. If necessary, connect the Graph button to the on-screen
ColorGraphView instance. Make it send the graph: action.
38. Add the setObjectColor: action to the ColorGraphView class in the
Inspector. (Since GraphView is ColorGraphView’s superclass, its
outlets and actions show up in gray, uneditable text in the Inspector.)
39. Unparse the ColorGraphView class and insert the files in the project.
40. Insert the lines in bold below into ColorGraphView.h.
#import <appkit/appkit.h>
#import "GraphView.h"
@interface ColorGraphView:GraphView
{
NXColor axesColor;
NXColor graphColor;
NXColor labelColor;
}
- setObjectColor:sender;
- addGraphSegmentx1:(float)x1 y1:(float)y1
x2:(float)x2 y2:(float)y2;
- addAxesx1:(float)x1 y1:(float)y1
x2:(float)x2 y2:(float)y2;
- addLabel:(const char *)title
atx:(float)x y:(float)y
size:(float)size;
- setObjectColor:(NXColor)theColor forTag:(int)aTag;
- awakeFromNib;
@end
The key definitions that we need for color are contained in the NXColor-
Well.h and NXColorPanel.h files. It’s a good idea to briefly look at these
files, as well as color.h, in the /NextDeveloper/Headers/appkit directory,
to learn the basic structures, constants, and methods provided by NeXT-
STEP for handling color.
The first three methods in the class implementation below override Graph-
View’s methods for drawing parts of the graph. Each of these methods
invokes the method with the same name in the GraphView class (Color-
GraphView’s superclass), and then changes the color of the newly-created
object to be the color specified by the appropriate NXColorWell. These
methods work because their corresponding methods in the GraphView
class all return the id of the object that’s been created, rather than self.
42. Insert the three method implementations below into
ColorGraphView.m.
- addGraphSegmentx1:(float)x1 y1:(float)y1
x2:(float)x2 y2:(float)y2
{
id newSeg = [super addGraphSegmentx1:x1 y1:y1
x2:x2 y2:y2];
[newSeg setColor:graphColor];
return newSeg;
464 Color
- addAxesx1:(float)x1 y1:(float)y1
x2:(float)x2 y2:(float)y2
{
id newAxes = [super addAxesx1:x1 y1:y1
x2:x2 y2:y2 ];
[newAxes setColor:axesColor];
return newAxes;
}
As this example shows, part of a good class design means being able to
easily change or enhance functionality by subclassing.1
These three methods are all that is necessary to have newly-drawn graphs
displayed in the colors wanted by the user. But if you want to let the user
change the colors on an existing graph, you’ll need to add a few more
methods.
1. An earlier version of the GraphView class not shown in this book didn’t have
these three methods, but simply drew the graph segments, axes, and labels in the
graphLine: method. It proved to be impossible to add color to that version of
GraphView without making changes to the GraphView class itself (i.e., subclass-
ing didn’t work). The new version of GraphView, that we showed in the previous
chapter, overcame those problems through better design.
ColorGraphView 465
variable to figure out which NXColorWell sent the message. (Recall that
the setUpWell:defaultColor:tag: method in PrefController set the tag of
each color well to a unique value.)
43. Insert the line in bold below into the setObjectColor: method in
ColorGraphView.m.
- setObjectColor:sender
{
[self setObjectColor:[sender color]
forTag:[sender tag] ];
return self;
}
This method gets the color and the tag from the NXColorWell that sends
the message and then invokes the setObjectColor:forTag: method shown
below.
44. Insert the setObjectColor:forTag: method implementation below into
ColorGraphView.m.
- setObjectColor:(NXColor)theColor forTag:(int)aTag
{
int i;
switch(aTag) {
case AXES_TAG: axesColor = theColor; break;
case GRAPH_TAG: graphColor = theColor; break;
case LABEL_TAG: labelColor = theColor; break;
}
[self lockFocus];
for (i=0; i < [displayList count]; i++) {
id obj = [displayList objectAt:i];
if ([obj tag]==aTag) {
[ [obj setColor:theColor] drawPSInView:self];
}
}
[self unlockFocus];
[window flushWindow];
return self;
}
This method scans the display list for objects with a given tag and changes
their color to the color requested. This method relies on the fact that each
466 Color
kind of object in the display list has a unique tag, and that those tags are the
same as the tags of the corresponding NXColorWells.
Note that we preserve the semantics of the setGray: method for both these
classes – the method still exists, but now it creates a gray “color.” This is an
example of data encapsulation. Any Objective-C code that we wrote that
used the setGray: method will still work, even though the internal repre-
sentation of colors has changed, because we have preserved a backwards-
Changes to the Segment and Label Classes 467
By now it’s becoming clear that a better initial design decision would have
been to make the Label and Segment subclasses of some abstract super-
class that supported the setGray:, setColor:, and bounds methods. Fre-
quently these sorts of design possibilities only become obvious when you
are partway into a project. With Objective-C, it would be easy to make that
sort of change (which is left as an exercise for the reader).
Summary
In this chapter we learned that drawing in color with Display PostScript is
nearly as easy as drawing in black-and-white. Then we saw how a well-
designed class makes it easy to improve its functionality through subclass-
ing. Finally, we showed another way to use the tag facility with the display
list to change the color attribute for a set of objects.
In the next chapter we’ll learn more about Views – specifically, we’ll see
how to put any view in a scroller and add a pop-up magnification control,
and we’ll show how to subclass the ColorGraphView to handle mouse
tracking.
Summary 469
These two resizing problems have two different solutions which we’ll
show in the first part of this chapter. In the second part of this chapter we’ll
see how to make the GraphView more useful by reporting the (x,y) loca-
tion of the cursor (mouse) as it moves over the graph and also by changing
the cursor to a more appropriate image.
471
472 View Resizing and Mouse Tracking
5. Click the Graph button and enlarge the GraphPaper window to get
something that looks like the window in Figure 3 below.
But merely changing the size of the GraphView isn’t enough. As you can
see in Figure 3, stretching the window increases the size of the View, but
the graph of the function is the same size. The problem is that the Graph-
View’s coordinate system does not scale automatically when the Graph-
View changes size. You’ve got to do that explicitly in your class definition.
In order to change the scale of the coordinate system, it’s necessary for the
GraphView object to be notified when its size has been changed. Fortu-
nately, there is a View method that is automatically invoked to change the
size of a View. That method is sizeTo::. The method’s arguments are the
View’s new width and height.
View resizing, and then scale itself. Below we present a method that does
the trick.
6. Insert the sizeTo:: method declaration below into GraphView.h.
- sizeTo:(NXCoord)width :(NXCoord)height;
7. Insert the sizeTo:: method implementation below into GraphView.m.
- sizeTo:(NXCoord)width :(NXCoord)height
{
[super sizeTo:width :height];
if (xmax!=xmin && ymax!=ymin) {
[self setDrawSize:xmax-xmin :ymax-ymin];
[self setDrawOrigin:xmin :ymin];
[self display];
}
return self;
}
8. Save the GraphView class files, make and run GraphPaper.
9. Enter a function, click the Graph button, and drag the GraphPaper
window’s resize bar in several directions. The View should handle
resize events properly as in Figure 4 below. (It can even handle
resizing while the graph is being drawn!)
More About Resizing 475
Tracking Rectangles
Until now, we’ve thought of NeXTSTEP events as being in two categories:
mouse events and keyboard events. Another way of classifying events is
according to the sort of programmatic gyrations you have to go through in
order to receive them.
The simplest events to use are those that require no setup. You merely cre-
ate a custom View and implement the event methods to receive them. Here
are the simple events:
- mouseDown: (NXEvent *)theEvent
- rightMouseDown: (NXEvent *)theEvent
- mouseUp: (NXEvent *)theEvent
- rightMouseUp: (NXEvent *)theEvent
A second group of mouse events involve the responder chain. Usually, your
View receives these events by making itself a First Responder. This can
happen automatically if the user clicks the mouse in your View and your
View responds YES to the message acceptsFirstResponder:. The
responder chain events include the keyboard events as well as events that
have to deal with simple mouse motion. Here are the First Responder
events:
- keyDown: (NXEvent *)theEvent
- keyUp: (NXEvent *)theEvent
- flagsChanged: (NXEvent *)theEvent
- mouseMoved: (NXEvent *)theEvent
- mouseDragged: (NXEvent *)theEvent
- rightMouseDragged: (NXEvent *)theEvent
Argument Meaning
aRect This argument is a pointer to the tracking rectangle in
the Window’s coordinate system. The tracking rect-
angle does not change if the Window is resized.
insideFlag Set this flag to indicate whether you think the mouse
cursor is initially inside or outside the tracking rectan-
gle. If this flag is NO, then the mouseEntered: event
will be sent the first time that the cursor moves into
the tracking rectangle.
anObject This is the object that should receive the mouseEn-
tered: and mouseExited: messages. It’s usually a
View or an NXCursor object, but not always.
trackNum You identify the tracking rectangle with this integer.
You will use this integer again to destroy the tracking
rectangle when you’re done with it. Don’t use a nega-
tive value, because NeXTSTEP uses those numbers to
implement cursor rectangles.
leftDown If this parameter is TRUE, the events will be sent
only if the cursor crosses the tracking rectangle when
the left mouse button is down.
rightDown If this parameter is TRUE, the events will be sent
only if the mouse crosses the tracking rectangle when
the right mouse button is down.
The event mask is a 32-bit number, with each bit corresponding to a differ-
ent event. (This is the reason why NeXTSTEP only supports 32 different
events.) A 1 in a bit position means that the event is sent to the window; a 0
in that bit position suppresses the sending of the event.
By default, there are only eight events that get sent to your application:
- mouseDown: (NXEvent *)theEvent
- rightMouseDown: (NXEvent *)theEvent
- mouseUp: (NXEvent *)theEvent
- rightMouseUp: (NXEvent *)theEvent
- mouseEntered: (NXEvent *)theEvent
- mouseExited: (NXEvent *)theEvent
- keyDown: (NXEvent *)theEvent
- keyUp: (NXEvent *)theEvent
When you don’t need these events anymore, you can send the message:
[aWindow removeFromEventMask:NX_MOUSEMOVEDMASK]
The mask also allows App Kit defined events, Application defined events,
and system-defined events to be sent. You’ll find a listing of all of the event
masks in the file /NextDeveloper/Headers/dpsclient/event.h.
480 View Resizing and Mouse Tracking
7. Using the Size Inspector, set the “springs” for both TextFields in the
same way that was done for the Form object (see Figure 2,
“Autosizing “Springs” for CustomView and Form,” on page 473).
8. Connect the TrackingGraphView’s xCell outlet to the TextField
labeled x: and the yCell outlet to the one labeled y:.
9. Unparse the TrackingGraphView class and insert it into your project.
this variable will hold that tracking rectangle’s number. We need to main-
tain this information because NeXTSTEP generates an error if you try to
discard a tracking rectangle that does not exist.
10. Insert the ten lines in bold below into TrackingGraphView.h.
#import "ColorGraphView.h"
@interface TrackingGraphView:ColorGraphView
{
id xCell;
id yCell;
BOOL inside;
id highlightedSegment;
int trackingRect;
}
- setMyTrackingRect:(BOOL)flag;
- awakeFromNib;
- mouseEntered:(NXEvent *)theEvent;
- mouseExited:(NXEvent *)theEvent;
- sizeTo:(NXCoord)width :(NXCoord)height;
- unhighlightSegment;
- mouseMoved:(NXEvent *)theEvent;
@end
if (flag){
/* Set new tracking rect if requested
* and if visible.
*/
if ([self getVisibleRect:&visible]) {
[self convertRect:&visible toView:nil];
[window setTrackingRect:&visible
inside:NO owner:self
tag:1 left:NO right:NO];
trackingRect = 1;
}
}
return self;
}
- mouseEntered:(NXEvent *)theEvent
{
[window makeFirstResponder:self];
[window addToEventMask:NX_MOUSEMOVEDMASK];
inside = YES;
return self;
}
- mouseExited:(NXEvent *)theEvent
{
[self unhighlightSegment];
[window removeFromEventMask:NX_MOUSEMOVEDMASK];
inside = NO;
return self;
}
[self unlockFocus];
[xCell setStringValue:"x: "];
[yCell setStringValue:"y: "];
highlightedSegment = nil;
}
return self;
}
17. Insert the mouseMoved: method below into TrackingGraphView.m.
- mouseMoved:(NXEvent *)theEvent
{
NXPoint pt;
char buf[256];
pt = theEvent->location;
[self convertPoint: &pt fromView: nil];
if (obj != highlightedSegment) {
[self unhighlightSegment];/* erase old */
/* update positions */
sprintf(buf, "x: %g", [obj x]);
[xCell setStringValue: buf];
sprintf(buf, "y: %g", [obj y]);
[yCell setStringValue: buf];
highlightedSegment = obj;
}
return self;
}
}
}
/* no segment should be highlighted */
486 View Resizing and Mouse Tracking
[self unhighlightSegment];
return self;
}
The window in Figure 7 below shows what the x and y cells and high-
lighted Segment look like when GraphPaper runs. Note the arrow cursor.
NeXTSTEP provides a simple way for giving Views or parts of Views their
own cursors. If a View has its own cursor, the mouse cursor automatically
changes to that View’s cursor when the mouse moves on top of that View,
and automatically reverts to what it was before when the mouse cursor
moves off that View. This section shows how to set up cursors that change
automatically.
To use an image as a cursor, you must make its image accessible to your
program. There are three ways to do this:
• Add the cursor’s image to your application’s nib.
• Add the cursor’s image to your project’s TIFF or EPS file list.
• Place the cursor’s image file in the directory in which the program
resides (called a package).
First we’ll show how to add the cursor image to your application’s nib.
1. Create a cursor that you like (or use an existing one such as cross.tiff
in /NextDeveloper/Examples/AppKit/Draw) and store it in a file
(we’ll use the name CrossHair.tiff here).
2. Drag the icon for the CrossHair.tiff file into the Project Builder’s main
window. The image will be added to your project (under the Images
file type) and copied into your project directory, if necessary.
488 View Resizing and Mouse Tracking
Cursor rectangles are only used to change the shape of the cursor when the
user moves the cursor into a particular rectangular region of the window.
Although they are implemented with mouse tracking rectangles, they have
no other impact on mouse tracking.
To make all of this work, you need to add an instance variable to the
TrackingGraphView object that will hold the id of the NXCursor object.
3. Insert the new instance variable below into TrackingGraphView.h.
id cursor;
4. Insert the new resetCursorRects method declaration below into
TrackingGraphView.h.
- resetCursorRects;
[super awakeFromNib];
hotSpot.x = 8.0;
Changing the Cursor 489
hotSpot.y = 8.0;
This method sets up the cursor tracking rectangle so that the cursor auto-
matically changes from the arrow to the crosshair when the mouse moves
inside the TrackingGraphView. The rectangle is set to be merely the
amount of the View that is visible; this is important if part of the View is
hidden behind a scroller. The resetCursorRects message is sent to the key
window by the Application object whenever it determines that the key
window has invalid cursor rectangles (e.g., when the View is resized). The
key window then sends the resetCursorRects message to each of its sub-
views, which includes TrackingGraphView in this example.
7. Save all pertinent files, make and run GraphPaper.
8. Click the Graph button and move the cursor over the graph. The
crosshair cursor should appear, as in the middle of the GraphPaper
window in Figure 8 below.
490 View Resizing and Mouse Tracking
Summary
In this chapter, we continued our investigation of the NeXTSTEP View
class by looking at the way the class handles resize events and controls cur-
sors. In the next chapter we’ll learn more about NeXTSTEP ScrollViews,
we’ll see how to put a zoom button on the GraphPaper application, and
we’ll make provisions for saving a graph as an EPS or TIFF file.
19
Zooming and
Saving Graphics Files
This chapter shows you how to do a few odds and ends with Views. In the
first part we’ll show how to put a zoom pop-up list in a ScrollView. In the
second part we’ll show you how to generate encapsulated PostScript or
TIFF from a View, how to save the graph into an EPS or TIFF file, and how
to add controls to a Save panel.
The reason has to do with the NeXTSTEP user interface guidelines. The
window is supposed to be just that – a window into a page of an imaginary
document. The document shouldn’t get bigger when you stretch its win-
dow. Stretching the window should merely let you see more of the docu-
ment.
491
492 Zooming and Saving Graphics Files
What NeXTSTEP applications should use to let the user see more detail is
a zoom button. A zoom button is a little pop-up list button at the bottom of
a ScrollView that allows you change the magnification of the ScrollView.
Appsoft’s word processor WriteNow has such a button (see Figure 1).
The window in Figure 1 is set to a zoom factor of 100%. Pressing the but-
ton reveals a pop-up list of different magnification settings, which in turn
lets you change the size of the text that is displayed, as in Figure 2.
It’s really quite easy to add a zoom button to any ScrollView. The trick
requires knowing a little bit about how the ScrollView works.
Adding a Zoom Button To GraphPaper 493
You can change the magnification of the View displayed in the ScrollView
simply by sending a scale: message to the ClipView, which will cause it to
scale the View that it contains when it is drawn. Zooming happens without
the knowledge or cooperation of the docView.
In this section, we will subclass the ScrollView class to make a new class
called ZoomScrollView. This class will have outlets for its docView – the
TrackingGraphView that we created in the previous chapter – as well as
for the zoom pop-up list. We’ll control the ZoomScrollView instance from
the Controller object.
Changes to GraphPaper.nib
1. Open your GraphPaper project in Project Builder and the
GraphPaper.nib file in Interface Builder.
2. Resize the TrackingGraphView in the GraphPaper window so that
it’s smaller and put it somewhere in the window that is out of the way.
(Don’t worry about the size or position of the TrackingGraphView –
it will automatically be resized and placed in the proper position when
GraphPaper runs.)
3. Subclass the ScrollView class and create the ZoomScrollView class.
4. Add the following outlets and action to the ZoomScrollView class in
IB’s Inspector.
Outlets Action
subView, zoomButton changeZoom:
494 Zooming and Saving Graphics Files
5. Drag a CustomView icon from IB’s Views palette and drop it in the
GraphPaper window. Change its class to ZoomScrollView.
6. Resize the on-screen ZoomScrollView instance to be as large as the
previous TrackingGraphView, as in Figure 3 below.
7. Choose IB’s Format→Layout→Send To Back menu command to put
the ZoomScrollView instance “behind” the TrackingGraphView
instance, as in Figure 3 below. (The only reason to send the
ZoomScrollView to the back is to see both Views at the same time.)
When you double-click the pop-up list button, you’ll see the three menu
cells that the associated pop-up list initially contains. The on-screen pop-up
list is controlled by an instance of the PopUpList class which itself is a
subclass of the Menu class. You can add more items to an open pop-up list
in IB by dragging them from the Menus palette and dropping them in the
pop-up list. (You can’t add submenus to a pop-up list, however.)
Adding a Zoom Button To GraphPaper 495
The pop-up list button itself is actually an instance of the NeXTSTEP But-
ton class that sends the popUp: message to the PopUpList instance. After
a selection is made, the PopUpList automatically changes the title of the
Button by sending it the setTitle: message.
11. One-by-one, drag three more menu items from the Menus palette and
drop them in the pop-up list.
12. Name the menu cells 100%, 125%, 150%, 175%, 200%, and 300%
and give them the tags 100, 125, 150, 175, 200, and 300, respectively
(use the MenuCell Attributes Inspector). When you’re done, the pop-
up list should look like the one at the left.
13. Connect each menu cell in the pop-up list so that they all send the
changeZoom: action message to ZoomScrollView. Be sure to drag the
connections from each item in the menu, and not from the pop-up
button, or “cover.” The title in the Inspector’s title bar indicates the
source of the connection.
14. Click the 100% item and close the pop-up list by clicking somewhere
else in the window.
15. Connect ZoomScrollView’s zoomButton outlet to the pop-up list
button (you’ll have to temporarily move the pop-up list button outside
the area covered by the ZoomScrollView instance).
16. Connect ZoomScrollView’s subView outlet to the
TrackingGraphView.
17. Unparse the class definition for ZoomScrollView and insert its class
files into the project.
Changes to ZoomScrollView
18. Insert the six lines in bold below to ZoomScrollView.h.
#import <appkit/appkit.h>
@interface ZoomScrollView:ScrollView
{
id subView;
id zoomButton;
float scaleFactor;
NXRect originalContentViewFrame;
}
- changeZoom:sender;
- initFrame:(const NXRect *)theFrame;
- awakeFromNib;
496 Zooming and Saving Graphics Files
- setScaleFactor:(float)aFactor;
- tile;
@end
We’ll use the scaleFactor instance variable to store the current scale factor
of the ZoomScrollView. When the user changes the zoom factor using the
pop-up list, the PopUpList object will send the changeZoom: message to
the ZoomScrollView object, which will in turn send the setScaleFactor:
message to itself. ZoomScrollView’s setScaleFactor: method will then
compare the new zoom factor with the current one and calculate the proper
arguments to the scale: message. If the new magnification isn’t different
from the old, the ZoomScrollView won’t do anything.
19. Insert the initFrame: method below into ZoomScrollView.m.
- initFrame:(const NXRect *)theFrame
{
[super initFrame: theFrame];
[self setBackgroundGray: NX_WHITE];
scaleFactor = 1.0;
return self;
}
The initFrame: method is the designated initializer for the View class.
ZoomScrollView’s initFrame: method sends the initFrame: message to
its superclass and then sets the background of the ScrollView to be white.
Finally it sets the current scale factor to be 1.0, which corresponds to the
100% menu item in the pop-up list.
return self;
}
This method also tells the ZoomScrollView that both scrollers are required
and that its border should be of type NX_LINE (as opposed to NX_BEZEL
or NX_NOBORDER). Finally it sizes the TrackingGraphView (sub-
View) to be the size of the ScrollView’s contentView, which is the Clip-
View.
The next method is the one that actually changes the magnification of the
ClipView object. ClipViews are used by the ScrollView class to do the
actual displaying of the ScrollView.
21. Insert the setScaleFactor: method below into ZoomScrollView.m.
- setScaleFactor:(float)aFactor
{
if (scaleFactor != aFactor) {
float delta = aFactor/scaleFactor;
scaleFactor = aFactor;
[contentView scale: delta : delta];
}
return self;
}
[super tile];
return self;
}
This tile method gets the frame of the horizontal scroller, snips off 60 pix-
els, and gives that space to the zoom button. (NeXTSTEP’s “Rectangle
Functions” are really handy; search for “NXDivideRect” in HeaderViewer
or Librarian to see what’s available.)
Bug Fixes
In making the ZoomScrollView, we’ve inadvertently introduced two bugs
into our GraphPaper application having to do with the TrackingGraph-
View’s tracking rectangles: when the GraphPaper window is resized, parts
500 Zooming and Saving Graphics Files
of the graph sometimes lose their mouse sensitivity. Another bug that
we’ve had from the beginning is that the application continues to track the
mouse even when the GraphPaper window is not key.
The problem with resizing owes itself to the fact that the TrackingGraph-
View depends upon catching the superviewSizeChanged: method in order
to discard its old tracking rectangle and create a new one. Unfortunately,
when a View is placed inside a ScrollView, this message is not forwarded,
because the ClipView (which is the superview of the docView of a Scroll-
View) does not resize the View when you stretch the window. This makes
sense– if you make a ScrollView larger, you may be able to see more of the
docView, but the docView itself doesn't resize to be larger. We therefore
need to pick a different strategy for noticing that the visible portion of the
TrackingGraphView has changed, and then reset the tracking rectangle
appropriately.
Because this problem arises when the window is resized, an easy way to
solve it is to catch the window resize event and take the appropriate action.
The way to do this is to make the TrackingGraphView the delegate of its
window and to implement the windowDidResize: delegate method.
28. Back in IB, connect the GraphPaper window’s delegate outlet to the
TrackingGraphView (Control-drag from the MyWindow icon in the
Objects view to the TrackingGraphView instance in the window).
29. Insert the three method declarations below into
TrackingGraphView.h (not ZoomScrollView.h).
- windowDidResize:sender;
- windowDidBecomeMain:sender;
- windowDidResignMain:sender;
30. Insert the method below into the file TrackingGraphView.m.
- windowDidResize:sender
{
return [self setMyTrackingRect: YES];
}
This will fix the resizing problem; the tracking rectangle should now auto-
matically be resized when the window is resized.
The second bug – that the application continues to track the mouse when it
is no longer the active application – is also solved through the use of win-
dow delegate methods. This time, we’ll use the windowDidBecomeMain:
and the windowDidResignMain: methods so that the application only
tracks the mouse when it is the main window.
Saving a View as Encapsulated PostScript 501
- windowDidResignMain:sender
{
return [self setMyTrackingRect: NO];
}
32. Save all pertinent files and then make and run GraphPaper.
The tracking now turns off when you activate another application, and
turns back on when you reactivate GraphPaper.
Changes to GraphPaper.nib
1. Back in IB, add a menu item to the GraphPaper menu. Change its title
to Save Image and give it the “s” key alternative.
2. Select the FirstResponder class in the Classes view (under Object)
and add an action called save: in the Class Attributes Inspector.
502 Zooming and Saving Graphics Files
3. Connect the Save Image menu cell so that it sends the save: action
message to the FirstResponder “object” in the Objects view.
if ([save runModal]==1) {
NXStream *aStream;
This method displays the Save panel and asks the user for a file name with
extension EPS. If the user provides a valid name, the method opens a mem-
ory-resident NXStream, invokes the copyPSToStream:forView: method
to fill the stream with an EPS representation of the contents of the Zoom-
ScrollView, and finally saves the stream to a file and frees its memory.
The following method, the one that actually generates the EPS, is even
simpler.
Saving a View as Encapsulated PostScript 503
[view getBounds:&bounds];
[view copyPSCodeInside:&bounds to:aStream];
return self;
}
You can incorporate this EPS file directly into a word processor document.
One nice thing about EPS files is that they are automatically imaged at the
resolution of the PostScript device in use. For example, we can incorporate
the EPS file into this book, as in Figure 6 below.
If you compare this image of the GraphView with the others in this book,
you’ll see that the letters “sin(3*x)” have no jaggies and that the line itself
looks smoother. That’s because the PostScript file has been imaged at the
1270 dpi (dots per inch) resolution of our phototypesetter. The other
images of the GraphView were captured off the screen at 92 dpi and had
their pixels replicated to get to the 1270 dpi of the phototypesetter.
504 Zooming and Saving Graphics Files
sin(3*x)
Saving a View in Tag Image File Format 505
We’ll use the NXImage class to generate the TIFF file. First we’ll capture
the PostScript for the GraphView into a stream. Then we’ll feed this
stream into an instance of the NXImage class. Finally we’ll ask the NXIm-
age instance to write its image, in TIFF format, to a second stream. This
second stream will then be saved to the file that the user specifies.
1. Insert the method declaration below into Controller.h.
- copyTIFFToStream:(NXStream *)aStream forView:view;
2. Insert the copyTIFFToStream:forView: method below into
Controller.m.
- copyTIFFToStream:(NXStream *)aStream forView:view
{
NXRect bounds;
id image;
NXStream *workStream;
[image setCacheDepthBounded:NO];
*/
[image writeTIFF: aStream];
[image free];
return self;
}
Next we’ll create a window with the accessory View, as at the left.
6. Drag a Window icon from IB’s Windows palette and drop it into the
workspace. The window will be added to the file GraphPaper.nib.
Resize the window so it’s about the same size as the one at the left.
7. In the Window Attributes Inspector, set the attributes of the window as
Nonretained and not Visible at Launch Time. The window is made
Nonretained so that it uses the least amount of computer resources.
Making the window not Visible at Launch Time prevents the user
from ever seeing it, since we won’t be displaying it in any other way.
8. Drag a radio button matrix from IB’s Views palette and drop it in the
window. Give it two choices: EPS and TIFF. Make EPS the default.
Change the font using the Font panel, if you like.
9. Select the radio button matrix and choose IB’s
Format→Layout→Group menu command to group it in a box.
10. Rename the box Format. The window should now look very similar to
the one above.
11. Connect the Controller’s formatBox outlet to the box titled Format.
Read the text near the bottom of the Inspector so you are sure which
object was connected.
12. Connect the Controller’s formatMatrix outlet to the radio button
matrix (a gray, not black rectangle should surround the button matrix).
13. Connect the radio button matrix to the Controller instance icon so that
it sends the setFormat: action. You must double-click the matrix so
that you select the matrix rather than its containing box.
char *format;
char *cc;
format = NXCopyStringBuffer(
[ [sender selectedCell] title]);
for (cc=format; *cc; cc++) {
*cc = NXToLower(*cc);
}
[ [SavePanel new] setRequiredFileType:format];
free(format);
return self;
}
Lastly we need to modify the save: method to rip the box (and the sub-
views that it contains) out of the window that we created above and put it
into the Save Panel as an accessory View. This is done with the Save-
Panel’s setAccessoryView: method. The SavePanel object automatically
resizes the on-screen Save panel to accommodate the accessory View.
We also need to modify the save: method to look at the title of the selected
cell and choose the appropriate method to send output to the stream. Both
of these modifications are presented below:
15. Replace the previous version of the save: method in Controller.m with
the one below.
- save:sender
{
id save = [SavePanel new];
if ([save runModal]==1) {
NXStream *aStream;
aStream = NXOpenMemory(0,0,NX_WRITEONLY);
Summary 509
Summary
You may notice that things are starting to happen really quickly – that’s
because we’ve reached critical mass with NeXTSTEP. Everything we’ve
learned is starting to jell and build on everything else that we’ve learned.
The result is that with each step we now take, we can do more things with
less effort.
From here on, you could probably figure out everything else yourself, since
you’ve now mastered the basic concepts. The last two chapters of this book
will walk you through the cut-and-paste system and the NeXTSTEP
defaults database system.
510 Zooming and Saving Graphics Files
511
512 The Pasteboard and Services
You can also create your own pasteboards in addition to these and use them
between different applications that you write. Of course, other people’s
applications are not likely to know of their existence.
Pasteboards are generally transparent to the user. That is, users don’t real-
ize that there are five distinct pasteboards – they simply know that when
they cut text from one application and paste it into another, it doesn’t
change the last ruler that they copied or pasted. Likewise, users are gener-
ally unaware that NeXTSTEP pasteboards can hold data in multiple repre-
sentations at the same time. It just happens automatically, like magic!
For example, when you copy a definition from the Webster program to the
pasteboard, Webster puts data on the pasteboard in both ASCII and Rich
Text. If you paste the definition into a Terminal window, the Terminal
application reads the ASCII definition. But if you paste the definition into a
word processor document, you might get the Rich Text version (depending
on which word processor).
Interface Builder uses the pasteboard to let you paste windows, controls,
objects and practically anything else.
There’s also nothing like the pasteboard for showing bugs and implementa-
tion errors with NeXTSTEP applications. For example, FrameMaker 3.0
has both a Paste and a Paste RTF command, even though this violates
NeXT’s style guidelines. WriteNow 2.0 won’t cut or paste RTF – the pro-
gram only cuts and pastes ASCII, TIFF and EPS images. Unfortunately,
when you find one of these bugs, all you can do is report them to the pro-
gram’s author and go on: the pasteboard system doesn’t give you a way for
fixing the bugs in other people’s programs.
There are six commonly used Pasteboard methods. The following two
class methods return the id of a Pasteboard object:
Class Method What it does
+ new Returns the id of the
Selection pasteboard.
+ newName: (const char *)aName Returns the id of a pasteboard
with a given name.
There are two instance methods for putting data onto a pasteboard. They
are usually invoked by (your override of) the cut: or copy: methods that
handle the Cut or Copy commands.
Instance Method What it does
- declareTypes: num: owner: Tells the pasteboard which
types you can provide.
- writeType: data: length: Writes the data to the paste-
board.
514 The Pasteboard and Services
Two other instance methods take data from the pasteboard. These are usu-
ally called by a paste: method that handles the Paste command.
Data on the pasteboard is stored as an array of bytes, the same way that you
might store it in a file.
Most NeXTSTEP programs write one format onto the pasteboard when the
user chooses a cut or copy operation, and lazy evaluation is used to provide
the other kinds of representations that the application knows about. The
Using the Pasteboard in GraphPaper 515
char *data;
int length, maxlen;
NXAtom typelist[2];
typelist[0] = NXPostScriptPboardType;
typelist[1] = NXTIFFPboardType;
[pboard declareTypes: typelist num: 2 owner:self];
- copy:sender
{
[self copyToPasteboard: [Pasteboard new] ];
return self;
}
Don’t be thrown by the NXAtom type. This is a typedef for const char *.
NXAtoms are allocated by the function NXUniqueString() and reside in a
special portion of the application’s memory which is usually set to be read-
only.
(iii) Specify an object (via the owner: argument) that the pasteboard can
message to provide any types necessary for lazy evaluation. Whenever
there is a request for lazy data from the Pasteboard, the pasteboard will
send the pasteboard:provideData: message to the object specified by
the owner: argument.
Even if the receiving program knows what kind of data it wants, the pro-
gram must first send the Pasteboard the types message to set it up for
returning the requested data. Once the types message is sent, the receiving
program can ask for either type and be reasonably well-assured of getting
it.
needs to take the EPS image from the pasteboard and convert it to a TIFF
image directly. It does this conversion by using an NXImage object.
3. Insert the pasteboard:provideData: method below to Controller.m.
/* Invoked to convert EPS to TIFF.
* Done in its own zone, which is later destroyed.
*/
- pasteboard:sender provideData:(NXAtom)type
{
if (type==NXTIFFPboardType) {
char *epsData;
int epsLen;
/* clean up */
Using the Pasteboard in GraphPaper 519
NXCloseMemory(tiffStream, NX_FREEBUFFER);
NXDestroyZone(zone);
vm_deallocate(task_self(),
(vm_address_t)epsData, epsLen);
return self;
}
}
return nil; /* failed */
}
This method uses Mach memory zones. A zone is a region of memory that
is dedicated to a particular task. This method creates a zone with the func-
tion NXCreateZone() and then uses the allocFromZone: method to allo-
cate an NXImage object from the zone that was just created. When the
method is finished executing, it destroys the zone. Using zones for special
purposes such as this reduces memory fragmentation and speeds applica-
tion performance.
The second new thing about this method is that it reads information off the
pasteboard in addition to putting data on the Pasteboard. The data is read
off the Pasteboard with the method readType:data:length:. Data from the
pasteboard arrives in the form of a Mach message and must be specially
freed by using the vm_deallocate() function. Although these functions are
more cumbersome than the traditional malloc() and free() C language
functions, they are much faster, because the information is transferred
directly from the address space of one application to another without hav-
ing to be copied. This is why NeXTSTEP doesn’t “choke” when you cut-
and-paste hundreds or thousands of kilobytes of information at a time.
If the user quits GraphPaper after some of its data has been copied to the
pasteboard, its Application object will automatically force the pasteboard
owner to turn all of the lazy data into real data. This lets the user paste data
into another application even if the source (application) of the data copied
to the pasteboard is no longer running.
Services
In addition to cut, copy, and paste, NeXTSTEP provides a nearly transpar-
ent system for applications to work together called Services. Services work
with NeXTSTEP’s concept of “selection” to provide a system for automat-
ically sending information from one application to accomplish a specific
function in another. For example, if you are using the Edit text editor and
do not have any text selected, your Services menu might look like the one
Services 521
When a user logs in, the Workspace Manager scans all of the applications
in the standard applications paths (/NextApps, /LocalApps, /NextDevel-
oper/Apps, /NextDeveloper/Demos, and the user’s ~/Apps directory)
looking for applications that advertise that they can handle the Services
protocol.1 This advertisement consists of a list of the messages that the pro-
gram can handle, what sort of data types it can accept, and what kind it can
return. For example, the Webster application’s “advertisement” is given
below:
Message: defineWord
Port: Webster
Send Type: NeXT plain ascii pasteboard type
Menu Item: Define in Webster
KeyEquivalent: =
It’s also possible for an application to have a “Return Type:” field, but Web-
ster doesn’t. When a user runs an application, the application sends the fol-
lowing message to NXApp to register which types it can send and receive:
registerServicesMenuSendTypes : sendTypes
andReturnTypes : returnTypes
sendTypes and returnTypes are both arrays of types similar to those used by
Pasteboard’s declareTypes:num:owner method (except these arrays are
null-terminated, instead of having their length explicitly coded). After the
program starts running, the
validRequestorForSendType : (NXAtom)typeSent
andReturnType : (NXAtom)typeReturned
method is sent down the responder chain for every combination of send
and return types that the application can handle. If a responder can handle a
particular combination, it should return something other than NULL. For
1. In NeXTSTEP 3.0, you can also control which Services appear in Services
menus using the Preferences application.
Services 523
example, the Text class implements this method returning self for the fol-
lowing combinations and NULL for all others:
When the user selects an item from the Services menu, your object will be
sent the message:
(BOOL)writeSelectionToPasteboard : pboard
types : (NXAtom *)types
If the service returns data, your object should also implement the message:
(BOOL)readSelectionFromPasteboard : pboard
types : (NXAtom *)types
If you are creating an object that does not handle selection, you do not need
to implement or even worry about these methods. (The Text object is an
example of an object that does handle selection.) For the remainder of this
chapter, we will concentrate solely on the other half of the process – offer-
ing services to other applications.
524 The Pasteboard and Services
Field Meaning
Message: The name of the message to be sent.
Port: The name of the Mach port where the message
should be sent.
User Data: Additional data that should be provided to the
method invoked (like a tag).
Send Type: The pasteboard types that method can receive.
Return Type: The pasteboard types that the method can return.
Menu Item: The name of the menu item that should appear in
the Services menu.
KeyEquivalent: The key equivalent, if any, that the menu item
should have.
Incoming Services messages are sent to a special delegate called the Ser-
vices delegate. In NeXTSTEP releases 2.0 and 3.0, the Services delegate is
set by the application’s Listener object, which is part of NeXTSTEP’s
Speaker/Listener system. (In future releases of NeXTSTEP, Speaker/Lis-
tener will be replaced by NeXTSTEP’s distributed object system.) The ser-
vice methods use a private pasteboard to exchange data between the
sending and the receiving applications.
A modal session is like the standard application event loop that we have
used until now except the application object ignores events from all other
windows except the window designated in the runModalSession: mes-
sage. This is how NeXTSTEP implements Open and Save panels.
The GraphView object will signal that the graph is finished using the stop-
Modal: method, which will return control to the Controller and signal for
the completed graph to be sent back to the calling program.
1. Using an editor, create a file called services.txt in your GraphPaper
project directory with the six lines below (it’s important that there be a
carriage return but no blank lines at the end of the advertisement, or
else the Workspace Manager may not register the Service properly).
Message: graphEquation
Port: GraphPaper
Send Type: NXAsciiPboardType
Return Type: NXPostScriptPboardType
Return Type: NXTIFFPboardType
Menu Item: Graph/Equation
This advertisement tells Workspace Manager that your Service should have
a single menu item called Services→Graph→Equation (see the last of the
six lines), which responds to the message graphEquation:user-
526 The Pasteboard and Services
Data:error: (see the first line and the description of the <service-
Name>:userData:error: method above).
2. Create a file called Makefile.preamble in your GraphPaper project
directory with the following line:
LDFLAGS=-sectcreate __ICON __services services.txt
(Note that there are two underscore characters before the letters “ICON”
and “services.”)
This line tells the loader to create a segment called __services in the
__ICON section of the executable from the services file.
3. Insert the instance variable below into GraphView.h.
BOOL runningAsService;
4. Insert the two method declarations below into GraphView.h.
- setRunningAsService:(BOOL)flag;
- formulaField;
if (runningAsService) {
[NXApp stopModal:NX_RUNSTOPPED];
}
return self;
}
- formulaField
{
return formulaField;
}
7. Insert the graphEquation:userData:error: and appDidInit: method
declarations below into Controller.h (not GraphView.m).
- graphEquation:pasteboard
userData:(const char *)userData
error:(char **)msg;
- appDidInit:sender;
8. Insert the graphEquation:userData:error: method below into
Controller.m.
- graphEquation:pasteboard
userData:(const char *)userData error:(char **)msg
{
id win = [graphView window];
NXModalSession session;
const NXAtom *types;
char *formula;
int formulaLen;
char *buf;
Next, the method brings the GraphPaper window to the front of the display
list, sets the runningAsService instance variable, and starts the graph. It
then uses the application modal session commands to create a small loop
which processes events. Ideally, the lines:
[NXApp beginModalSession:&session for:win];
while(1) {
if ([NXApp runModalSession:&session] !=
NX_RUNCONTINUES)
break;
}
[NXApp endModalSession:&session];
When the modal session is finished, this method unsets the runningAsSer-
vice flag, copies the graph to the pasteboard that was provided by the Ser-
vices manager, and pushes the GraphPaper window to the back of the
display list. The copyToPasteboard: method will put the EPS version of
the graph on the pasteboard; if your application wants the TIFF version, it
will be provided through lazy evaluation.
9. Insert the appDidInit: method below to the file Controller.m. (Recall
that Controller is the delegate of the Application object.)
- appDidInit:sender
{
[ [NXApp appListener] setServicesDelegate:self];
return self;
}
This method sets the Controller object as the services delegate. You must
do this in order to receive services messages.
If it’s not already running, the Workspace Manager will start up the Graph-
Paper application. GraphPaper should then create the graph and then the
Summary 531
If you get an “Error providing services Equation” then you probably made
a spelling error either in the services.txt file or the Controller.m file.
Summary
In this chapter we worked with two systems that NeXTSTEP uses for inter-
application communication: the cut, copy, and paste system; and services.
Using these features, you can greatly increase the power and usefulness of
532 The Pasteboard and Services
The NeXTSTEP defaults database stores the preferences that you set in the
Preferences panels of all applications. As a NeXTSTEP programmer, you
can use the defaults system to store whatever information you want.
In this chapter, we’ll modify the GraphPaper program to work with the
defaults database system. We’ll use the database to store the colors used to
draw the graph, axes, and labels. In the second half of this chapter, we’ll
use the defaults system to store the initial values for the graph parameters.
Then finally, we’ll see how to create a multi-view inspector to switch
between these two preferences options.
533
534 Preferences and Defaults
Let’s use the “dread -l” command to see all the variables and defaults in a
typical database:
localhost> dread -l
BackSpace priority 0
BackSpace screenSaver On
BackSpace viewType All
BackSpace windowType 0
Create ColorPickerH 200
Create ColorPickerW 318
Create ColorPickerX 61
Create ColorPickerY 446
Create NXFontPanelPreviewFrame "0 0 281 47"
Diagram ClickDragMove 1
Diagram InspectorIsOpen 0
Diagram NXFontPanelPreviewFrame "0 0 281 46"
Diagram ShowMarkers 1
Edit DeleteBackup NO
Edit "Find Panel" 53:316:391:189
Edit IndentWidth 3
Edit NXFixedPitchFontSize 14
How Preference Defaults Work 535
Terminal Keypad NO
Terminal Meta 27
Terminal NXFixedPitchFont Ohlfs
Terminal NXFixedPitchFontSize 10
Terminal NXFontPanelFrame "178 -2 308 711"
Terminal NXFontPanelPreviewFrame "0 0 291 115"
Terminal Rows 24
Terminal Scrollback YES
Terminal Shell /bin/csh
TouchType NXFontPanelPreviewFrame "0 0 281 47"
VOID GameNumber 0
VOID Outline 0
VOID ShipType 0
VOID UserName ""
Webster DictionaryOpen YES
Webster ExactMatch YES
Webster FontSize 14
Webster Frames 115,198,515,381,2,71,303
Webster FullWordIndex NO
Webster PrintPictures YES
Webster ShowPictures YES
Webster ThesaurusOpen YES
WordPerfect NXFontPanelFrame "428 -3 298 749"
WordPerfect NXFontPanelPreviewFrame "0 0 281 133"
WriteNow InitialUntitled NO
WriteNow NXFontPanelPreviewFrame "0 0 281 40"
WriteNow NXFontSize 12.0
Yap NXFontPanelPreviewFrame "0 0 281 48"
Yap "NXWindow Frame NXFontPanel" "90 144 299 330 "
Yap "NXWindow Frame Output Window" "555 450 422 374 "
As you can see, practically every program used stores information in the
defaults database. When storing information in the database, it is therefore
important to pick a unique owner name so that the preferences from your
application do not accidentally coincide with another’s. NeXT recom-
mends that commercial applications use the “full market name” of the
application, which should be unique. NeXT further recommends that non-
commercial applications use the name of the program and the author’s
name or institution name, for example, “MikeysTestProgram.”
If you look through this list, you will see some common preference names
stored in several applications. Two such preferences are NXFontPanel-
Frame and NXFontPanelPreviewFrame, which get written into the data-
base automatically by the NeXTSTEP FontPanel object. The FontPanel
object uses the defaults database to remember its size and the size of the
preview area between invocations of an application.
Adding Defaults to GraphPaper 537
NeXT recommends that you register your defaults in a method called +ini-
tialize. The +initialize method is a special class method that is invoked
when your class is first used. The Objective-C runtime system assures that
the +initialize message is sent once and only once to each class in your
program. The +initialize message is always sent to a class’s superclass
before it is sent to the class itself.
1. Insert the class method declaration below into Controller.h.
+ initialize;
2. Insert the class method below into Controller.m.
+ initialize
{
static NXDefaultsVector GraphPaperDefaults = {
{"AxesColor", "0.3333333:0.3333333:0.3333333"},
{"LabelColor", "0:0:0"},
{"GraphColor", "0:0:0"},
{"xstep", "0.1"},
{"xmin", "0.0"},
{"xmax", "10.0"},
{"ymin", "-5.0"},
{"ymax", "5.0"},
{"Function", "sin(x)"},
{NULL, NULL}
};
NXRegisterDefaults("GraphPaper",
GraphPaperDefaults);
return self;
}
538 Preferences and Defaults
Since the defaults system can only store printable ASCII characters, we
will convert colors (e.g., black stored as an NXColor) into red, blue, and
green triplets and store them in the defaults database separated by colons
(e.g., "0:0:0"). The two functions we show below perform this conversion
and its inverse.
3. Insert the two functions below into Controller.m. Place them after the
#import directives but before the @implementation directive.
char *colorToASCII(NXColor color, char *buf)
{
float r, g, b;
where owner is the name of your program and name is the name of the
default that you wish to access.
The NXGetDefaultValue() function looks for the default value in the four
locations discussed below.
(i) The function first checks the UNIX command line that was used to
launch the program, if one exists. This lets you temporarily change the
value of a preference for a single run of a program. The syntax on the
command line is:
programName -name value {-name value, ...}
For example, suppose we had launched the GraphPaper application
from the command line with
localhost> open GraphPaper.app -xstep 5
Then the function call
NXGetDefaultValue("GraphPaper", "xstep")
would return the value “5” instead of what was stored in the defaults
database.1
(ii) If no command line value was given, the function next checks the
user’s defaults database for a default with the same owner/name com-
bination.
(iii) If no owner/name combination is found in the defaults database, the
function next checks for a default owned by GLOBAL with the same
name.
(iv) If there is no global value for this default, the function returns the value
that was specified in the registration table with the function NXRegis-
terDefaults().
When your program starts up, it needs to read the user’s default values and
set the state of the associated objects in the application. ColorGraph-
View’s setUp method performs this function. Recall that in Chapter 17, we
simply hard-coded values to use for defaults. Here’s how we modify the
method to work with the defaults system:
5. Insert the #import directive below into ColorGraphView.m.
#import "Controller.h"
1. This is the way that a program finds out if it was automatically launched by
Workspace when the user logs in. If the program was auto-launched, the function
call NXGetDefaultValue(owner, "NXAutoLaunch") returns the string “YES”.
540 Preferences and Defaults
axesColor = ASCIIToColor(
NXGetDefaultValue("GraphPaper","AxesColor") );
labelColor = ASCIIToColor(
NXGetDefaultValue("GraphPaper","LabelColor") );
graphColor = ASCIIToColor(
NXGetDefaultValue("GraphPaper","GraphColor") );
return self;
}
The first time that the user runs GraphPaper, there are no values in the
defaults database, and the NXGetDefaultValue() function calls in awake-
FromNib return the values that were originally registered with the function
NXRegisterDefaults() in the +initialize method. If the user changes his or
her personal value for one of these defaults (using the Preferences panel,
described below), the function NXGetDefaultValue() returns that new
value instead.
We’ll modify the PrefController class so that it sends itself the revert:
message the first time the panel is displayed. This will make PrefControl-
ler read the values out of the registration table or defaults database.
9. Add the Revert and OK buttons as in the panel on the left of Figure 1.
To add the return icon ( ) on the OK button, drag it from the Images
view in IB’s File window and drop it on the button.
Note the link cursor ( ) that you see when dropping the return icon
on the button, as in the panel on the right of Figure 1. This link cursor
means that the when the user hits the Return key while the panel is the
key window, the button will act as if it was clicked. It’s actually a con-
nection involving a performClick: action message, which is sent to
the button. It’s easy!
10. Connect the Revert button to the PrefController instance icon so that
it sends the revert: message.
11. Connect the OK button to the PrefController instance icon so that it
sends the okay: message.
12. Save preferences.nib.
542 Preferences and Defaults
The revert: action method, which gets invoked when the user clicks the
Revert button, simply replaces the current color of each color well on the
Preferences panel with the value taken out of the defaults database.
13. Insert the lines in bold below into the revert: method in
PrefController.m (the method stub should already be there).
- revert:sender
{
[graphColorWell setColor:ASCIIToColor(
NXGetDefaultValue("GraphPaper","GraphColor")) ];
[labelColorWell setColor:ASCIIToColor(
NXGetDefaultValue("GraphPaper","LabelColor")) ];
[axesColorWell setColor:ASCIIToColor(
NXGetDefaultValue("GraphPaper","AxesColor")) ];
return self;
}
newDefaults[0].value = alloca(256);
strcpy(newDefaults[0].value,
colorToASCII([labelColorWell color],buf));
newDefaults[1].value = alloca(256);
strcpy(newDefaults[1].value,
colorToASCII([axesColorWell color],buf));
Making the Preferences Panel Work with Defaults 543
newDefaults[2].value = alloca(256);
strcpy(newDefaults[2].value,
colorToASCII([graphColorWell color],buf));
NXWriteDefaults("GraphPaper", newDefaults);
return self;
}
This okay: method creates a new defaults vector (newDefaults) and fills
the value for each of the properties with the ASCII representation of the
appropriate color, as returned by the NXColorWell objects. Notice that we
use the alloca() function to allocate memory; the memory allocated by this
function is automatically freed when the method exits. This method also
removes the Preferences and Colors panels from the screen after the data-
base is updated.
Lastly, we need to modify PrefController’s awakeFromNib method so
that the initial values for the NXColorWell objects in the Preferences panel
are taken from the defaults database. The easiest way to do this is by invok-
ing the revert: method we discussed above. We’ll also take this opportu-
nity to simplify the setUpWell:tag: method that sets up the tag and other
default values of the NXColorWells, so that the default color need not be
specified. The simplified method has one less argument.
15. Replace the awakeFromNib method implementation in
PrefController.m with the one below.
- awakeFromNib
{
[ [NXColorPanel sharedInstance:YES]
setContinuous:YES];
[self setUpWell:axesColorWell tag:AXES_TAG];
[self setUpWell:labelColorWell tag:LABEL_TAG];
[self setUpWell:graphColorWell tag:GRAPH_TAG];
[self revert:self];
return self;
}
16. Replace the setUpWell:defaultColor:tag: method declaration in
PrefController.h with the setUpWell:tag: declaration below.
- setUpWell:well tag:(int)aTag;
544 Preferences and Defaults
that will show up when you enter the dread command. These are automat-
ically generated by NeXTSTEP. We’ll discuss additional defaults for
GraphPaper in the next section.
Setting Up a Multi-View Panel 545
Multi-View Panels
The basic idea behind a multi-view panel is having a single panel that can
be used to look at a variety of different Views. Usually, the View selection
is implemented with an on-screen pop-up list.
The way we’re going to implement the multi-view panel is by putting the
various content Views for the Preferences panel on a second panel, which
we’ll title “Holding Place” (see Figure 3 below). The Preferences panel
will contain PopUpList and Box objects, as in Figure 4 below. When a
user chooses a pop-up list item in the Preferences panel, the appropriate
content View (a Box with contents) will be copied from the Holding Place
panel and into the Box in the Preferences panel. This is similar to the way
that we put auxiliary Views in the Save panel in Chapter 19.
546 Preferences and Defaults
id colorView;
id initialView;
id multiView;
}
...
- setPref:sender;
- setPrefToView:theView;
@end
The colorView and initialView outlets will store the ids of the Views
(Boxes in the Holding Place panel) that we will eventually copy into our
multi-view Preferences panel. The multiView outlet will store the id of the
Box (in the Preferences panel) into which we’ll copy those Views.
To implement the multi-view panel, we’ll add two new methods to the
PrefController class. The first, called setPref:, will be invoked whenever
the user drags to an item in the pop-up list that changes the View.
2. Insert the setPref: method below into PrefController.m.
- setPref:sender
{
id newView = nil;
[self setPrefToView:newView];
return self;
}
This method determines which View the user chose by looking at the tag of
the PopUpList cell (item) which sent the message. It then gets the content
View of the appropriate box and invokes the setPrefToView: method,
shown below.
3. Insert the setPrefToView: method below into PrefController.m.
- setPrefToView:theView
{
NXRect boxRect, viewRect;
[multiView getFrame:&boxRect];
[theView getFrame:&viewRect];
548 Preferences and Defaults
[multiView setContentView:theView];
NX_X(&viewRect)=
(NX_WIDTH(&boxRect)-NX_WIDTH(&viewRect)) / 2.0;
NX_Y(&viewRect) =
(NX_HEIGHT(&boxRect)-NX_HEIGHT(&viewRect))/ 2.0;
9. Cut the Box from the Preferences panel and paste it into the Holding
Place panel as follows: select the “Colors” Box and its contents in the
Preferences panel, choose IB’s Edit→Cut, click in the Holding Place
panel, and choose IB’s Edit→Paste.
Note that it is critical that all of the objects you choose be grouped inside
the Box, rather than merely be resting on top of it. In this case, the group
does more than make the panel visually pleasing. It makes all of the objects
subviews of the Box object. We can then manipulate the objects as a whole
by manipulating that Box. You can tell if the items are in the Box by click-
ing on the Box to select it and then moving the Box. If the objects move
with the Box, then they are grouped inside it.
10. Reconnect each “well” outlet in PrefController instance to the
appropriate NXColorWell object (IB automatically breaks all
connections when an object is pasted).
11. Connect the colorView outlet in the PrefController instance to the
Colors Box in the Holding Place panel.
Now we’ll set up the “Initial Graph Conditions” Box in the Holding Place
panel as in Figure 5 below.
12. Drag a Form object from IB’s Views palette and drop it in the panel.
13. Alt-drag on the bottom middle handle of the Form to create four more
FormCells. Label the six FormCells as in Figure 5.
14. Set the text in the Form to be right aligned and choose a font.
550 Preferences and Defaults
- awakeFromNib
...
[graphProc dpsWatchFD:gotGraphData data:self
priority:NX_RUNMODALTHRESHOLD];
[xminCell setStringValue:
NXGetDefaultValue("GraphPaper", "xmin") ];
[xmaxCell setStringValue:
NXGetDefaultValue("GraphPaper", "xmax") ];
[xstepCell setStringValue:
NXGetDefaultValue("GraphPaper", "xstep") ];
[yminCell setStringValue:
NXGetDefaultValue("GraphPaper", "ymin") ];
[ymaxCell setStringValue:
NXGetDefaultValue("GraphPaper", "ymax") ];
[formulaField setStringValue:
NXGetDefaultValue("GraphPaper", "Formula")];
return self;
}
We need to add six outlets to the PrefController so that we can read the
contents of the initial values and save them in the defaults database.
25. Insert the six new outlets below into PrefController.h.
/* for initial values */
id xminCell;
id xmaxCell;
id xstepCell;
id yminCell;
id ymaxCell;
id formulaField;
26. Insert the lines in bold below into the revert: method in
PrefController.m.
- revert:sender
...
[axesColorWell setColor:ASCIIToColor(
NXGetDefaultValue("GraphPaper", "AxesColor"))];
[xminCell setStringValue:
NXGetDefaultValue("GraphPaper", "xmin")];
[xmaxCell setStringValue:
NXGetDefaultValue("GraphPaper", "xmax")];
[xstepCell setStringValue:
NXGetDefaultValue("GraphPaper", "xstep")];
[yminCell setStringValue:
552 Preferences and Defaults
NXGetDefaultValue("GraphPaper", "ymin")];
[ymaxCell setStringValue:
NXGetDefaultValue("GraphPaper", "ymax")];
[formulaField setStringValue:
NXGetDefaultValue("GraphPaper", "Formula")];
return self;
}
27. Insert the lines in bold below to the okay: method in
PrefController.m.
- okay:sender
{
static NXDefaultsVector newDefaults = {
{"LabelColor", ""}, /* 0 */
{"AxesColor", ""}, /* 1 */
{"GraphColor", ""}, /* 2 */
{"xmin", ""}, /* 3 */
{"xmax", ""}, /* 4 */
{"xstep", ""}, /* 5 */
{"ymin", ""}, /* 6 */
{"ymax", ""}, /* 7 */
{"Formula", ""}, /* 8 */
{NULL, NULL}
};
...
newDefaults[2].value = alloca(256);
strcpy(newDefaults[2].value,
colorToASCII([graphColorWell color],buf));
newDefaults[3].value =
(char *) [xminCell stringValue];
newDefaults[4].value =
(char *) [xmaxCell stringValue];
newDefaults[5].value =
(char *) [xstepCell stringValue];
newDefaults[6].value =
(char *) [yminCell stringValue];
newDefaults[7].value =
(char *) [ymaxCell stringValue];
newDefaults[8].value =
(char *) [formulaField stringValue];
NXWriteDefaults("GraphPaper", newDefaults);
return self;
}
28. Back in IB, Parse PrefController’s updated class definition in
preferences.nib.
29. Connect each of the six new outlets in the PrefController instance to
the appropriate FormCell in the Form in the Holding Place panel.
30. Save all pertinent files and make and run GraphPaper.
31. Choose the Info→Preferences menu command to bring up the
Preferences panel. You should see the defaults “Colors” View.
32. Press the pop-up list button in the Preferences panel and drag to
Initial.
33. Enter values for all the initial parameters and click OK (or hit Return).
The OK and Revert buttons work as before. See Figure 2 above.
34. Quit GraphPaper.
35. Now run GraphPaper again, and the initial values should show up in
both the GraphPaper window and the Preferences panel.
36. Quit GraphPaper.
37. To see the values written into the defaults database, open up a Terminal
shell window and enter the dread command below.
localhost> dread -o GraphPaper
GraphPaper AxesColor 0.333338:0.333338:0.333338
GraphPaper GraphColor 0.000000:0.000000:0.000000
GraphPaper xmax 10.0
GraphPaper xstep 0.1
GraphPaper ymax 5.0
GraphPaper ymin -5.0
GraphPaper Formula cos(x)
GraphPaper LabelColor 0.000000:0.000000:0.000000
GraphPaper NXColorPanelMode 6
GraphPaper NXSwatchSize 22.000000
GraphPaper xmin 0.0
38. Congratulate yourself for making it through this book! We salute you!
554 Preferences and Defaults
Summary
Well, we’ve finally come to the end of the book. In this chapter, you
learned the last of what you need to write a professional quality NeXT-
STEP application program – how to use NeXTSTEP’s defaults database.
Although there’s lots more to write about NeXTSTEP, from here on you
should be able to get most of what you need from the on-line documenta-
tion (or from our next book!). If you’ve been with us until now, you’ve
learned the basics of NeXTSTEP’s three main classes – Application,
View, and Window – learned how they interact, and learned how to modify
their functions as necessary to get done what you want.
The source code from the three major applications in the book, Calculator,
MathPaper, and GraphPaper, is listed in this appendix. The source code
consists primarily of our custom NeXTSTEP 3.0 class interface and class
implementation files but also includes the main() function files generated
by Project Builder. Within each application, the files are listed in alphabeti-
cal order.
Calculator Application
Source files included:
CalcWindow.h CalcWindow.m
Calculator_main.m
Controller.h Controller.m
CalcWindow.h
#import <appkit/appkit.h>
@interface CalcWindow:Window
{
555
556 Appendix A: Source Code Listings
id keyTable;
}
- findButtons;
- checkView:aView;
- checkButton:aButton;
- checkMatrix:aMatrix;
@end
CalcWindow.m
#import "CalcWindow.h"
@implementation CalcWindow
- findButtons
{
/* check all the views recursively */
[keyTable empty];
[self checkView:contentView];
return self;
}
- checkView:aView
{
id subViews = [aView subviews];
int numSubViews = [subViews count];
int i;
- checkButton:aButton
{
const char *title = [aButton title];
return self;
}
- checkMatrix:aMatrix
{
id cellList = [aMatrix cellList];
int numCells = [cellList count];
int i;
return self;
}
- keyDown:(NXEvent *)theEvent
{
id button;
if (button) {
return [button performClick:self];
}
@end
Calculator_main.m
/* Generated by the NeXT Project Builder
NOTE: Do NOT change this file --
Project Builder maintains it.
*/
#import <appkit/Application.h>
[Application new];
if ([NXApp loadNibSection:"Calculator.nib"
owner:NXApp withNames:NO])
[NXApp run];
[NXApp free];
exit(0);
}
Controller.h
enum {
PLUS = 1001,
SUBTRACT = 1002,
MULTIPLY = 1003,
DIVIDE = 1004,
EQUALS = 1005
};
#import <appkit/appkit.h>
Calculator Application 559
@interface Controller:Object
{
id readout;
BOOL enterFlag;
BOOL yFlag;
int operation;
float X;
float Y;
id infoPanel;
int radix;
id radixMatrix;
id keyPad;
NXRect originalViewSize;
}
- clear:sender;
- clearAll:sender;
- enterDigit:sender;
- enterOp:sender;
- displayX;
- doUnaryMinus:sender;
- showInfo:sender;
- setRadix:sender;
@end
@interface Controller(ApplicationDelegate)
- appDidInit:sender;
@end
Controller.m
#import "Controller.h"
#import "CalcWindow.h"
return (&buf[31]);
}
@implementation Controller
- clear:sender
{
X = 0.0;
[self displayX];
return self;
}
- clearAll:sender
{
X = 0.0;
Y = 0.0;
yFlag = NO;
enterFlag = NO;
[self displayX];
return self;
}
- enterDigit:sender
{
if (enterFlag) {
Y = X;
X = 0.0;
enterFlag = NO;
}
- enterOp:sender
{
if (yFlag) { /* something is stored in Y */
switch (operation) {
case PLUS:
X = Y + X;
break;
case SUBTRACT:
X = Y - X;
Calculator Application 561
break;
case MULTIPLY:
X = Y * X;
break;
case DIVIDE:
X = Y / X;
break;
}
}
Y = X;
yFlag = YES;
[self displayX];
return self;
}
- displayX
{
char buf[256];
char bbuf[256];
switch(radix) {
case 10:
sprintf( buf, "%15.10g", X );
break;
case 16:
sprintf( buf, "%x", (unsigned int)X);
break;
case 8:
sprintf( buf, "%o", (unsigned int)X);
break;
case 2:
strcpy( buf, ltob( (int)X, bbuf ) );
break;
}
[readout setStringValue:buf];
return self;
}
562 Appendix A: Source Code Listings
- doUnaryMinus:sender
{
X = -X;
[self displayX];
return self;
}
- showInfo:sender
{
if (infoPanel == nil) {
if ( ![NXApp
loadNibSection: "info.nib"
owner: self
withNames: NO]) {
return nil; /* Load failed */
}
}
[infoPanel makeKeyAndOrderFront: nil];
return self;
}
- setRadix:sender
{
int i;
id cellList;
int oldRadix = radix;
id keyWindow = [keyPad window];
[keyWindow getFrame:&newWindowFrame];
Calculator Application 563
NX_WIDTH(&newWindowFrame) +=
NX_X(&keyFrame) + NX_WIDTH(&keyFrame)
- NX_WIDTH(&originalViewSize)
+ 4.0;
[keyWindow
placeWindowAndDisplay:&newWindowFrame];
}
/* placeWindowAndDisplay: gives a cleaner redraw
* when making the window bigger.
*/
[self displayX];
return self;
}
@end
@implementation Controller(ApplicationDelegate)
- appDidInit:sender
{
id kwin = [keyPad window];
[self setRadix:radixMatrix];
[self clearAll:self];
return self;
}
@end
564 Appendix A: Source Code Listings
MathPaper Application
Source files included:
InfoView.h InfoView.m
MathController.h MathController.m
MathPaper_main.m
PaperControl.h PaperControl.m
Process.h Process.m
RTF.h RTF.m
InfoView.h
#import <appkit/appkit.h>
@interface InfoView:View
{
DPSTimedEntry animateTE;
BOOL animatingDrop;
BOOL animatingDissolve;
int animationStep;
float animationSize;
float animationFloat;
id symbolFont;
id bigTextFont;
id smallTextFont;
}
@end
InfoView.m
#define SIZE 72.0
#import "InfoView.h"
#import "supershow.h"
{
id obj = (id)userData;
[obj animationClick];
}
@implementation InfoView
return self;
}
- animateInfo:sender
{
[self display]; /* clear the window */
[self removeTE]; /* remove timed-entry, if nec.*/
[window setDelegate:self];
animatingDrop = YES; /* start with this */
animationStep = 0; /* and this */
animateTE = DPSAddTimedEntry(.002,
(DPSTimedEntryProc)handler,
self,NX_BASETHRESHOLD);
return self;
}
- removeTE
{
if (animateTE) {
DPSRemoveTimedEntry(animateTE);
animateTE = 0; /* note that it's gone */
}
return self;
}
566 Appendix A: Source Code Listings
- windowWillClose:sender
{
[self removeTE]; /* make sure it is gone */
return self;
}
- animationClick
{
if (animatingDrop)[self animateDrop];
if (animatingDissolve) [self animateDissolve];
NXPing(); /* synchronize with server */
return self;
}
- animateDrop
{
int order[4] = {0,2,1,3};
char *dropstrings[4] = {"+","-","\264","\270"};
float const step = 2.0;
float x;
float y = NX_HEIGHT(&frame) - animationFloat;
int j = order[animationStep];
[self lockFocus];
[symbolFont set];
animationFloat += 2.0;
if (animationFloat >= NX_HEIGHT(&frame)) {
animationFloat = 0; /* reset Y */
animationStep++; /* go to next step */
if(animationStep==4) { /* go to next effect */
animationStep = 0;
animatingDrop = NO;
animatingDissolve = YES;
animationFloat = 0.0;
}
}
[self unlockFocus];
return self;
}
MathPaper Application 567
- animateDissolve
{
id image;
NXPoint myPoint;
NXSize isize;
float x, y, width;
char *slgName ="Created by Simson L. Garfinkel";
char *mkmName =" tweaking by Michael K. Mahoney";
[self lockFocus];
[bigTextFont set];
[image dissolve:animationFloat
toPoint:&myPoint]; /* draw image */
PSsetgray(1.0 - (animationFloat/2.0));
PSrectfill(NX_X(&frame),
SIZE, NX_WIDTH(&frame), 2.0);
animationFloat += 0.005;
if(animationFloat < 1.0) {
[self unlockFocus];
return self;
}
/* finish up */
[smallTextFont set];
width = [smallTextFont getWidthOf:slgName];
supershow(NX_X(&frame)
+ NX_WIDTH(&frame) - width - 5.0,
y-20.0, 0.0, slgName);
[self removeTE];
[self unlockFocus];
return self;
}
@end
MathController.h
#import <appkit/appkit.h>
@interface MathController:Object
{
id newCalc;
float offset;
int calcNum;
id infoView;
}
- newCalc:sender;
- loadCalc:sender;
- (int)countEditedWindows;
- saveAll:sender;
- (BOOL)menuActive:menuCell;
- showInfoPanel:sender;
@end
MathController.m
#import "MathController.h"
#import "PaperControl.h"
#import "InfoView.h"
MathPaper Application 569
@implementation MathController
- newCalc:sender
{
id win;
if ([newCalc setUp]) {
win = [newCalc window];
if (win) {
NXRect frame;
char buf[256];
- loadCalc:sender
{
char *types[2] = {"mathpaper", 0};
}
return self;
}
- (int)countEditedWindows
{
id winList;
int i;
int count = 0;
- saveAll:sender
{
id winList;
int i;
- (BOOL)menuActive:menuCell
{
BOOL shouldBeEnabled;
- showInfoPanel:sender
{
if (infoView==0) {
[NXApp loadNibSection:"info.nib"
owner:self];
}
[ [infoView window] makeKeyAndOrderFront:self];
[infoView animateInfo:self];
return self;
}
@end
@implementation MathController(ApplicationDelegate)
- appDidInit:sender
{
id docMenu = [calculatorSubmenuCell target];
[self newCalc:self];
[saveMenuCell
setUpdateAction: @selector(menuActive:)
forMenu: docMenu];
[saveAsMenuCell
setUpdateAction: @selector(menuActive:)
forMenu: docMenu];
return self;
}
572 Appendix A: Source Code Listings
- (BOOL)appAcceptsAnotherFile:sender
{
return YES;
}
- appWillTerminate:sender
{
if ([self countEditedWindows]>0) {
int q = NXRunAlertPanel("Quit",
"There are edited windows.",
"Review Unsaved",
"Quit Anyway", "Cancel");
if (q==1) { /* Review */
int i;
id winList;
@end
MathPaper Application 573
MathPaper_main.m
/* Generated by the NeXT Project Builder
NOTE: Do NOT change this file --
Project Builder maintains it.
*/
#import <appkit/Application.h>
[Application new];
if ([NXApp loadNibSection:"MathPaper.nib"
owner:NXApp withNames:NO])
[NXApp run];
[NXApp free];
exit(0);
}
PaperControl.h
#import <appkit/appkit.h>
@interface PaperControl:Object
{
id proc;
id scroller;
id text;
id window;
char *filename;
}
- setUp;
- window;
- windowWillClose:sender;
- text;
- setScroller:aScroller;
- appendToText:(const char *)val
fromPipe:(BOOL)flag;
- setFilename:(const char *)aFilename;
- saveAs:sender;
- save:sender;
- loadCalcFile:(const char *)aFile;
- printCalc:sender;
@end
574 Appendix A: Source Code Listings
PaperControl.m
#define EVALUATOR_FILENAME "/Apps/Evaluator"
#import "PaperControl.h"
#import "Process.h"
#import "RTF.h"
@implementation PaperControl
- setUp
{
char *argv[2] = {0,0};
argv[0] = malloc(strlen(NXHomeDirectory())+32);
strcpy(argv[0], NXHomeDirectory());
strcat(argv[0], EVALUATOR_FILENAME);
if (!proc) {
NXRunAlertPanel(0,"Cannot create calculator: %s",
0, 0, 0, strerror(errno));
[window performClose: self];
return nil;
}
- window
{
return window;
}
- windowWillClose:sender
{
if ([sender isDocEdited]) {
const char *fname;
int q;
q = NXRunAlertPanel("Save",
"Save changes to %s?",
"Save", "Don't Save", "Cancel", fname);
if (q==1) { /* save */
if (![self save:nil]) {
return nil; /* didn't save */
}
}
if (q==-1) { /* cancel */
return nil;
}
}
[window setDocEdited:YES];
[text setSel:length :length];
if (flag) {
id rtf =
[ [RTF allocFromZone:[self zone] ] init];
[rtf bold:YES];
[rtf setJustify:NX_RIGHTALIGNED];
[rtf append:val];
[rtf bold:NO];
[rtf setJustify:NX_LEFTALIGNED];
[rtf append:" "];
[text replaceSelWithRichText:[rtf stream] ];
[rtf free];
}
else {
[text replaceSel:val];
}
[text scrollSelToVisible];
[text display];
return self;
}
- setScroller:aScroller
{
scroller = aScroller;
text = [aScroller docView];
[text setDelegate:self];
[text setCharFilter:NXFieldFilter];
[text selectAll:self];
return self;
}
- text
{
return text;
}
MathPaper Application 577
- saveAs:sender
{
id panel;
const char *dir;
char *file;
if (filename==0) {
/* no filename; set up defaults */
dir = NXHomeDirectory();
file = (char *)[window title];
}
else {
file = rindex(filename, '/');
if (file) {
dir = filename;
*file = 0;
file++;
}
else {
dir = filename;
file = (char *)[window title];
}
}
- save:sender
{
int fd;
NXStream *theStream;
NXClose(theStream);
close(fd);
NX_DURING
[text readRichText:theStream];
[self setFilename:aFile];
case NX_illegalRead:
NXRunAlertPanel(0,"Illegal read on stream",
0,0,0);
ret = nil;
break;
case NX_illegalSeek:
NXRunAlertPanel(0,"Illegal seek on stream",
0,0,0);
ret = nil;
break;
case NX_illegalStream:
NXRunAlertPanel(0,"invalid stream",0,0,0);
ret = nil;
break;
case NX_streamVMError:
NXRunAlertPanel(0,"VM error on stream",
0,0,0,aFile);
ret = nil;
break;
}
NXCloseMemory(theStream, NX_FREEBUFFER);
return ret;
}
return nil;
}
- printCalc:sender
{
[text printPSCode:nil];
return self;
}
@end
@implementation PaperControl(TextDelegate)
return self;
}
@end
Process.h
/*
* Process.h: spawn and control a subprocess
*/
#import <appkit/appkit.h>
@interface Process:Object
{
int toProcess[2];
int fromProcess[2];
int pid;
BOOL fdHandlerInstalled;
}
- initFromCommand:(char **)argv;
- free;
MathPaper Application 581
- (int)toFd;
- (int)fromFd;
- writeLine:(const char *)aLine;
- dpsWatchFD:(DPSFDProc)handler
data:(void *)userData priority:(int)pri;
@end
Process.m
#import "Process.h"
@implementation Process
- initFromCommand:(char **)argv
{
[super init];
if (pipe(toProcess) == -1) {
[self free];
return nil; /* could not open first pipe */
}
if (pipe(fromProcess) == -1) {
close(toProcess[0]);
close(toProcess[1]);
[self free];
return nil; /* could not open second pipe */
}
pid = fork();
if (pid == -1){
[self free];
return nil; /* no more processes */
}
close(toProcess[1]);
close(fromProcess[0]);
582 Appendix A: Source Code Listings
close(2); /* stderr */
dup2(fromProcess[1], 2); /* stderr */
execv(argv[0], argv);
perror(NXArgv[0]);
exit(1);
}
/* executed by the parent */
/* close the other ends of the pipe */
close(toProcess[0]);
close(fromProcess[1]);
return self;
}
- free
{
if (fdHandlerInstalled)
DPSRemoveFD(fromProcess[0]);
if (toProcess[1]) close(toProcess[1]);
if (fromProcess[0]) close(fromProcess[0]);
if (pid>0) kill(pid, 9);
return [super free];
}
- (int)toFd
{
return toProcess[1];
}
- (int)fromFd
{
return fromProcess[0];
}
- dpsWatchFD:(DPSFDProc)handler
data:(void *)userData priority:(int)pri
{
DPSAddFD(fromProcess[0], handler, userData, pri);
fdHandlerInstalled = YES;
return self;
}
@end
RTF.h
#import <appkit/appkit.h>
#import <streams/streams.h>
@interface RTF:Object
{
NXStream *textStream;
}
- (NXStream *)stream;
- appendRTF:(const char *)string;
- append:(const char *)string;
- bold:(BOOL)flag;
- setJustify:(int)mode;
- free;
@end
RTF.m
#import "RTF.h"
@implementation RTF
- init
{
584 Appendix A: Source Code Listings
[super init];
- (NXStream *)stream
{
NXSeek(textStream, 0L, NX_FROMSTART);
return textStream;
}
}
return self;
}
- bold:(BOOL)flag
{
[self appendRTF: flag ? "\\b " : "\\b0 "];
return self;
}
- setJustify:(int)mode
{
switch(mode) {
case NX_LEFTALIGNED:
case NX_JUSTIFIED:
[self appendRTF: "\\ql "];
break;
case NX_CENTERED:
[self appendRTF: "\\qc "];
break;
case NX_RIGHTALIGNED:
[self appendRTF: "\\qr "];
break;
}
return self;
}
- free
{
NXCloseMemory(textStream, NX_FREEBUFFER);
return [super free];
}
@end
586 Appendix A: Source Code Listings
GraphPaper Application
Source files included:
Controller.h Controller.m
GraphPaper_main.m
GraphView.h GraphView.m
Label.h Label.m
PrefController.h PrefController.m
Process.h Process.m
Segment.h Segment.m
TrackGraphView.h TrackGraphView.m
ZoomScrollView.h ZoomScrollView.m
Controller.h
#import <appkit/appkit.h>
@interface Controller:Object
{
id graphView;
id prefController;
id formatBox;
id formatMatrix;
}
- graphView;
- showPrefs:sender;
- save:sender;
- copyPSToStream:(NXStream *)aStream forView:view;
- copyTIFFToStream:(NXStream *)aStream forView:view;
- setFormat:sender;
- copyToPasteboard:pboard;
- copy:sender;
- pasteboard:sender provideData:(NXAtom)type;
- cut:sender;
- graphEquation:pasteboard
userData:(const char *)userData
error:(char **)msg;
- appDidInit:sender;
+ initialize;
@end
GraphPaper Application 587
Controller.m
#import "Controller.h"
#import "GraphView.h"
@implementation Controller
- graphView
{
return graphView;
}
- showPrefs:sender
{
if (!prefController) {
[NXApp loadNibSection: "preferences.nib"
owner: self];
}
[ [prefController window]
makeKeyAndOrderFront:sender];
return self;
}
- save:sender
{
id save = [SavePanel new];
if ([save runModal]==1) {
NXStream *aStream;
aStream = NXOpenMemory(0,0,NX_WRITEONLY);
if (!strcmp([ [formatMatrix selectedCell]
title], "EPS")) {
[self copyPSToStream: aStream
forView: graphView];
}
else {
[self copyTIFFToStream: aStream
forView: graphView];
}
NXSaveToFile(aStream, [save filename]);
NXCloseMemory(aStream, NX_FREEBUFFER);
}
return self;
}
[view getBounds:&bounds];
[view copyPSCodeInside:&bounds to:aStream];
return self;
}
workStream = NXOpenMemory(0,0,NX_READWRITE);
[view getBounds: &bounds];
[view copyPSCodeInside: &bounds to: workStream];
[image setCacheDepthBounded:NO];
return self;
}
- setFormat:sender
{
char *format;
char *cc;
format = NXCopyStringBuffer(
[ [sender selectedCell] title]);
for (cc=format; *cc; cc++) {
*cc = NXToLower(*cc);
}
[ [SavePanel new] setRequiredFileType:format];
free(format);
return self;
}
- copyToPasteboard:pboard
{
NXStream *stream;
char *data;
int length, maxlen;
NXAtom typelist[2];
590 Appendix A: Source Code Listings
typelist[0] = NXPostScriptPboardType;
typelist[1] = NXTIFFPboardType;
[pboard declareTypes: typelist num: 2 owner:self];
- copy:sender
{
[self copyToPasteboard: [Pasteboard new] ];
return self;
}
/* clean up */
NXCloseMemory(tiffStream, NX_FREEBUFFER);
NXDestroyZone(zone);
vm_deallocate(task_self(),
(vm_address_t)epsData, epsLen);
return self;
}
}
- cut:sender
{
[self copy:sender];
[graphView clear];
return self;
}
- graphEquation:pasteboard
userData:(const char *)userData error:(char **)msg
{
id win = [graphView window];
NXModalSession session;
const NXAtom *types;
char *formula;
int formulaLen;
char *buf;
592 Appendix A: Source Code Listings
- appDidInit:sender
{
[ [NXApp appListener] setServicesDelegate:self];
return self;
}
+ initialize
{
static NXDefaultsVector GraphPaperDefaults = {
{"LabelColor", "0:0:0"},
{"AxesColor", "0.3333333:0.3333333:0.3333333"},
{"GraphColor", "0:0:0"},
{"xstep", "0.1"},
{"xmin", "0.0"},
{"xmax", "10.0"},
{"ymin", "-5.0"},
{"ymax", "5.0"},
{"Function", "sin(x)"},
{NULL, NULL}
};
NXRegisterDefaults("GraphPaper",
GraphPaperDefaults);
return self;
}
@end
GraphPaper_main.m
/* Generated by the NeXT Project Builder
NOTE: Do NOT change this file --
Project Builder maintains it.
*/
#import <appkit/Application.h>
[Application new];
if ([NXApp loadNibSection:"GraphPaper.nib"
owner:NXApp withNames:NO])
[NXApp run];
[NXApp free];
exit(0);
}
594 Appendix A: Source Code Listings
GraphView.h
#import <appkit/appkit.h>
@interface GraphView:View
{
id formulaField;
id graphButton;
id xmaxCell;
id xminCell;
id xstepCell;
id ymaxCell;
id yminCell;
id graphProc;
id displayList;
BOOL first; /* first datapoint */
double lastx, lasty; /* for drawing graph */
char graphBuf[17000]; /* buf to graph */
double ymin,ymax;
cthread_t stuffer_thread;
BOOL runningAsService;
BOOL graphing;
char *formula;
int toFd;
double xmin, xmax, xstep;
}
- graph:sender;
- stopGraph:sender;
- addBufToGraph:(const char *)buf;
- graphLine:(const char *)buf;
- addGraphSegmentx1:(float)x1 y1:(float)y1
x2:(float)x2 y2:(float)y2;
- drawSelf:(const NXRect *)rects :(int)rectCount;
- clear;
- awakeFromNib;
- addAxesx1:(float)x1 y1:(float)y1
x2:(float)x2 y2:(float)y2;
- addLabel:(const char *)aTitle
atx:(float)x y:(float)y
size:(float)size;
- sizeTo:(NXCoord)width :(NXCoord)height;
- setRunningAsService:(BOOL)flag;
- formulaField;
GraphPaper Application 595
@end
#define GRAPH_TAG 0
#define LABEL_TAG 1
#define AXES_TAG 2
GraphView.m
#import "GraphView.h"
#import "Process.h"
#import "Segment.h"
#import "Label.h"
@implementation GraphView
- graph:sender
{
/* initialize for a graph */
[self clear];
if ( xstep<=0 ) {
NXRunAlertPanel(0,"Need a positive step",0,0,0);
return self;
}
if (formula) {
/* Free the old formula if we have one */
NXZoneFree([self zone],formula);
formula = 0;
}
formula = NXCopyStringBufferFromZone(
[formulaField stringValue],
[self zone]);
- stopGraph:sender
{
graphing = 0;
/* wait for the stuffer thread to finish */
cthread_join(stuffer_thread);
if (runningAsService) {
[NXApp stopModal:NX_RUNSTOPPED];
}
return self;
}
if (graphing==0) {
return self; /* not graphing */
}
598 Appendix A: Source Code Listings
strcat(graphBuf, buf);
while (cc = index(graphBuf, '\n')) {
*cc = '\0'; /* terminate the line */
/* now graph what we have */
[self graphLine: graphBuf];
memmove(graphBuf,
cc+1,
sizeof(graphBuf) - (cc-graphBuf));
}
return self;
}
if (first) {
/* put in graph axes */
[self addAxesx1:xmin y1:0.0 x2:xmax y2:0.0 ];
[self addAxesx1:0.0 y1:ymin x2:0.0 y2:ymax ];
/* add the label */
[self addLabel:formula
atx:(xmin+xmax)/2.0 y:(ymin*10.0+ymax)/11.0
size:24.0];
[self display];
}
if (!first) {
id seg = [self addGraphSegmentx1:lastx y1:lasty
x2:x y2:y ];
GraphPaper Application 599
lastx = x;
lasty = y;
first = NO;
return self;
}
- addGraphSegmentx1:(float)x1 y1:(float)y1
x2:(float)x2 y2:(float)y2
{
id seg = [ [Segment alloc]
initx1:x1 y1:y1 x2:x2 y2:y2 ];
[seg setTag:GRAPH_TAG];
[displayList addObject:seg];
return seg;
}
if (displayList) {
for (j=0; j < [displayList count]; j++) {
id obj = [displayList objectAt: j];
NXRect oBounds = [obj bounds];
if (NXIntersectsRect(&rects[i],
&oBounds)) {
[obj drawPSInView:self];
}
}
600 Appendix A: Source Code Listings
}
}
return self;
}
- clear
{
/* clear the display list */
if (displayList) {
[ [displayList
makeObjectsPerform:@selector(free)] empty];
}
else {
displayList = [ [List alloc] init];
}
[self display];
return self;
}
- awakeFromNib
{
char *argv[2] = {0,0};
char path[MAXPATHLEN];
if(![[NXBundle mainBundle]
getPath: path forResource: "Evaluator"
ofType: ""]){
NXRunAlertPanel(0,"Can't find Evaluator",0,0,0);
[NXApp terminate:self];
return nil;
}
argv[0] = path;
if (!graphProc) {
NXRunAlertPanel(0,
"Cannot create calculator: %s",
0, 0, 0, strerror(errno));
[NXApp terminate: self];
return nil;
}
GraphPaper Application 601
[xminCell setStringValue:
NXGetDefaultValue("GraphPaper", "xmin") ];
[xmaxCell setStringValue:
NXGetDefaultValue("GraphPaper", "xmax") ];
[xstepCell setStringValue:
NXGetDefaultValue("GraphPaper", "xstep") ];
[yminCell setStringValue:
NXGetDefaultValue("GraphPaper", "ymin") ];
[ymaxCell setStringValue:
NXGetDefaultValue("GraphPaper", "ymax") ];
[formulaField setStringValue:
NXGetDefaultValue("GraphPaper", "Formula")];
return self;
}
- addAxesx1:(float)x1 y1:(float)y1
x2:(float)x2 y2:(float)y2
{
id axes = [ [Segment alloc]
initx1: x1 y1: y1
x2: x2 y2: y2 ];
[axes setTag:AXES_TAG];
[axes setGray:NX_DKGRAY];
[displayList addObject:axes];
return axes;
}
return label;
}
602 Appendix A: Source Code Listings
- sizeTo:(NXCoord)width :(NXCoord)height
{
[super sizeTo:width :height];
if (xmax!=xmin && ymax!=ymin) {
[self setDrawSize:xmax-xmin :ymax-ymin];
[self setDrawOrigin:xmin :ymin];
[self display];
}
return self;
}
- setRunningAsService:(BOOL)flag
{
runningAsService = flag;
return self;
}
- formulaField
{
return formulaField;
}
@end
Label.h
#import <appkit/appkit.h>
@interface Label:Object
{
NXRect bounds;
char *text;
NXColor myColor;
int tag;
id font;
}
Label.m
#import "Label.h"
@implementation Label
[super init];
fontScale.width = 1.0;
fontScale.height = 1.0;
text = NXCopyStringBufferFromZone(aText,
[self zone]);
[self setGray:NX_BLACK];
return self;
}
- free
{
if (text){
NXZoneFree([self zone], text);
text = 0;
}
return [super free];
}
604 Appendix A: Source Code Listings
- setGray:(float)aGray
{
myColor = NXConvertGrayToColor(aGray);
return self;
}
- (NXRect)bounds
{
return bounds;
}
- (int)tag
{
return tag;
}
- setTag:(int)aTag
{
tag = aTag;
return self;
}
- setColor:(NXColor)aColor
{
myColor = aColor;
return self;
}
@end
GraphPaper Application 605
PrefController.h
#import <appkit/appkit.h>
@interface PrefController:Object
{
id axesColorWell;
id graphColorWell;
id labelColorWell;
id window;
id colorView;
id initialView;
id multiView;
- okay:sender;
- revert:sender;
- window;
- setUpWell:well tag:(int)aTag;
- awakeFromNib;
- setPref:sender;
- setPrefToView:theView;
@end
PrefController.m
#import "PrefController.h"
#import "Controller.h"
#import "GraphView.h"
@implementation PrefController
- window
{
return window;
}
606 Appendix A: Source Code Listings
- setUpWell:well tag:(int)aTag
{
[well setTag:aTag];
[well setContinuous:YES];
[well setTarget:[ [NXApp delegate] graphView] ];
[well setAction:@selector(setObjectColor:) ];
return self;
}
- awakeFromNib
{
[ [NXColorPanel sharedInstance:YES]
setContinuous:YES];
[self setUpWell:axesColorWell tag:AXES_TAG];
[self setUpWell:labelColorWell tag:LABEL_TAG];
[self setUpWell:graphColorWell tag:GRAPH_TAG];
[self revert:self];
[self setPrefToView:[colorView contentView] ];
return self;
}
- okay:sender
{
char buf[256];
static NXDefaultsVector newDefaults = {
{"LabelColor", ""}, /* 0 */
{"AxesColor", ""}, /* 1 */
{"GraphColor", ""}, /* 2 */
{"xmin", ""}, /* 3 */
{"xmax", ""}, /* 4 */
{"xstep", ""}, /* 5 */
{"ymin", ""}, /* 6 */
{"ymax", ""}, /* 7 */
{"Formula", ""}, /* 8 */
{NULL, NULL}
};
newDefaults[0].value = alloca(256);
strcpy(newDefaults[0].value,
colorToASCII([labelColorWell color],buf));
newDefaults[1].value = alloca(256);
strcpy(newDefaults[1].value,
colorToASCII([axesColorWell color],buf));
newDefaults[2].value = alloca(256);
GraphPaper Application 607
strcpy(newDefaults[2].value,
colorToASCII([graphColorWell color],buf));
newDefaults[3].value =
(char *) [xminCell stringValue];
newDefaults[4].value =
(char *) [xmaxCell stringValue];
newDefaults[5].value =
(char *) [xstepCell stringValue];
newDefaults[6].value =
(char *) [yminCell stringValue];
newDefaults[7].value =
(char *) [ymaxCell stringValue];
newDefaults[8].value =
(char *) [formulaField stringValue];
NXWriteDefaults("GraphPaper", newDefaults);
return self;
}
- revert:sender
{
[graphColorWell setColor:ASCIIToColor(
NXGetDefaultValue("GraphPaper","GraphColor")) ];
[labelColorWell setColor:ASCIIToColor(
NXGetDefaultValue("GraphPaper","LabelColor")) ];
[axesColorWell setColor:ASCIIToColor(
NXGetDefaultValue("GraphPaper","AxesColor")) ];
[xminCell setStringValue:
NXGetDefaultValue("GraphPaper", "xmin")];
[xmaxCell setStringValue:
NXGetDefaultValue("GraphPaper", "xmax")];
[xstepCell setStringValue:
NXGetDefaultValue("GraphPaper", "xstep")];
[yminCell setStringValue:
NXGetDefaultValue("GraphPaper", "ymin")];
[ymaxCell setStringValue:
NXGetDefaultValue("GraphPaper", "ymax")];
[formulaField setStringValue:
608 Appendix A: Source Code Listings
NXGetDefaultValue("GraphPaper", "Formula")];
return self;
}
- setPref:sender
{
id newView = nil;
[self setPrefToView:newView];
return self;
}
- setPrefToView:theView
{
NXRect boxRect, viewRect;
[multiView getFrame:&boxRect];
[theView getFrame:&viewRect];
[multiView setContentView:theView];
NX_X(&viewRect)=
(NX_WIDTH(&boxRect)-NX_WIDTH(&viewRect)) / 2.0;
NX_Y(&viewRect) =
(NX_HEIGHT(&boxRect)-NX_HEIGHT(&viewRect))/ 2.0;
@end
Process.h
/*
* Process.h: spawn and control a subprocess
*/
GraphPaper Application 609
#import <appkit/appkit.h>
@interface Process:Object
{
int toProcess[2];
int fromProcess[2];
int pid;
BOOL fdHandlerInstalled;
}
- initFromCommand:(char **)argv;
- free;
- (int)toFd;
- (int)fromFd;
- writeLine:(const char *)aLine;
- dpsWatchFD:(DPSFDProc)handler
data:(void *)userData priority:(int)pri;
@end
Process.m
/*
* Process.m
*/
#import "Process.h"
@implementation Process
- initFromCommand:(char **)argv
{
[super init];
if (pipe(toProcess) == -1) {
[self free];
return nil; /* could not open first pipe */
}
if (pipe(fromProcess) == -1) {
close(toProcess[0]);
close(toProcess[1]);
[self free];
return nil; /* could not open second pipe */
}
610 Appendix A: Source Code Listings
pid = fork();
if (pid == -1){
[self free];
return nil; /* no more processes */
}
close(toProcess[1]);
close(fromProcess[0]);
close(2); /* stderr */
dup2(fromProcess[1], 2); /* stderr */
execv(argv[0], argv);
perror(NXArgv[0]);
exit(1);
}
/* executed by the parent */
/* close the other ends of the pipe */
close(toProcess[0]);
close(fromProcess[1]);
return self;
}
- free
{
if (fdHandlerInstalled)
DPSRemoveFD(fromProcess[0]);
if (toProcess[1]) close(toProcess[1]);
if (fromProcess[0]) close(fromProcess[0]);
if (pid>0) kill(pid, 9);
return [super free];
}
- (int)toFd
{
return toProcess[1];
}
GraphPaper Application 611
- (int)fromFd
{
return fromProcess[0];
}
- dpsWatchFD:(DPSFDProc)handler
data:(void *)userData priority:(int)pri
{
DPSAddFD(fromProcess[0], handler, userData, pri);
fdHandlerInstalled = YES;
return self;
}
@end
Segment.h
#import <appkit/appkit.h>
@interface Segment:Object
{
NXPoint start;
NXPoint end;
NXColor myColor;
int tag;
}
- initx1:(float)x1 y1:(float)y1
x2:(float)x2 y2:(float)y2;
- setGray:(float)aGray;
- (float)x;
- (float)y;
612 Appendix A: Source Code Listings
- drawPSInView:aView;
- (NXRect)bounds;
- (int)tag;
- setTag:(int)aTag;
- setColor:(NXColor)aColor;
@end
Segment.m
#import "Segment.h"
@implementation Segment
- initx1:(float)x1 y1:(float)y1
x2:(float)x2 y2:(float)y2
{
[super init]; /* must init the Object class! */
start.x = x1;
start.y = y1;
end.x = x2;
end.y = y2;
[self setGray: NX_BLACK];
return self;
}
- setGray:(float)aGray
{
myColor = NXConvertGrayToColor(aGray);
return self;
}
- (float)x
{
return (start.x + end.x)/2.0;
}
- (float)y
{
return (start.y + end.y)/2.0;
}
GraphPaper Application 613
- drawPSInView:aView
{
NXSize sz = {1.0, 1.0}; /* a default size */
NXSetColor(myColor);
PSmoveto(start.x, start.y);
PSlineto(end.x, end.y);
return self;
}
- (NXRect)bounds
{
NXRect bounds;
NX_X(&bounds) = MIN(start.x,end.x) ;
NX_Y(&bounds) = MIN(start.y,end.y) ;
NX_WIDTH(&bounds) = fabs(start.x-end.x)+MINFLOAT;
NX_HEIGHT(&bounds) = fabs(start.y-end.y)+MINFLOAT;
return bounds;
}
- (int)tag
{
return tag;
}
614 Appendix A: Source Code Listings
- setTag:(int)aTag
{
tag = aTag;
return self;
}
- setColor:(NXColor)aColor
{
myColor = aColor;
return self;
}
@end
TrackingGraphView.h
#import <appkit/appkit.h>
#import "ColorGraphView.h"
@interface TrackingGraphView:ColorGraphView
{
id xCell;
id yCell;
BOOL inside;
id highlightedSegment;
int trackingRect;
id cursor;
}
- setMyTrackingRect:(BOOL)flag;
- awakeFromNib;
- mouseEntered:(NXEvent *)theEvent;
- mouseExited:(NXEvent *)theEvent;
- sizeTo:(NXCoord)width :(NXCoord)height;
- unhighlightSegment;
- mouseMoved:(NXEvent *)theEvent;
- resetCursorRects;
- windowDidResize:sender;
- windowDidBecomeMain:sender;
- windowDidResignMain:sender;
@end
GraphPaper Application 615
TrackingGraphView.m
#import "TrackingGraphView.h"
#import "Segment.h"
@implementation TrackingGraphView
- setMyTrackingRect:(BOOL)flag
{
NXRect visible;
if (flag) {
/* Set new tracking rect if requested
* and if visible.
*/
if ([self getVisibleRect:&visible]) {
[self convertRect:&visible toView:nil];
[window setTrackingRect:&visible
inside:NO owner:self
tag:1 left:NO right:NO];
trackingRect = 1;
}
}
return self;
}
- awakeFromNib
{
NXPoint hotSpot;
[super awakeFromNib];
- mouseEntered:(NXEvent *)theEvent
{
[window makeFirstResponder:self];
[window addToEventMask:NX_MOUSEMOVEDMASK];
inside = YES;
return self;
}
- mouseExited:(NXEvent *)theEvent
{
[self unhighlightSegment];
[window removeFromEventMask:NX_MOUSEMOVEDMASK];
inside = NO;
return self;
}
- sizeTo:(NXCoord)width :(NXCoord)height
{
[super sizeTo:width :height];
return [self setMyTrackingRect: YES];
}
- unhighlightSegment
{
if (highlightedSegment) {
[self lockFocus];
[ [highlightedSegment setColor: graphColor]
drawPSInView: self];
[self unlockFocus];
[xCell setStringValue:"x: "];
[yCell setStringValue:"y: "];
highlightedSegment = nil;
}
return self;
}
- mouseMoved:(NXEvent *)theEvent
{
NXPoint pt;
char buf[256];
pt = theEvent->location;
[self convertPoint: &pt fromView: nil];
GraphPaper Application 617
if (obj != highlightedSegment) {
[self unhighlightSegment];/* erase old */
/* update positions */
sprintf(buf, "x: %g", [obj x]);
[xCell setStringValue: buf];
sprintf(buf, "y: %g", [obj y]);
[yCell setStringValue: buf];
highlightedSegment = obj;
}
return self;
}
}
}
- resetCursorRects
{
NXRect visible;
if ([self getVisibleRect:&visible]) {
[self addCursorRect:&visible cursor:cursor];
}
return self;
}
618 Appendix A: Source Code Listings
- windowDidResize:sender
{
return [self setMyTrackingRect: YES];
}
- windowDidBecomeMain:sender
{
return [self setMyTrackingRect: YES];
}
- windowDidResignMain:sender
{
return [self setMyTrackingRect: NO];
}
@end
ZoomScrollView.h
#import <appkit/appkit.h>
@interface ZoomScrollView:ScrollView
{
id subView;
id zoomButton;
float scaleFactor;
NXRect originalContentViewFrame;
}
- changeZoom:sender;
- initFrame:(const NXRect *)theFrame;
- awakeFromNib;
- setScaleFactor:(float)aFactor;
- tile;
@end
ZoomScrollView.m
#import "ZoomScrollView.h"
@implementation ZoomScrollView
- changeZoom:sender
{
GraphPaper Application 619
[self setScaleFactor:
[ [sender selectedCell] tag] / 100.0];
return self;
}
return self;
}
- awakeFromNib
{
[self setVertScrollerRequired: YES];
[self setHorizScrollerRequired:YES];
[self setBorderType:NX_LINE];
return self;
}
- setScaleFactor:(float)aFactor
{
if (scaleFactor != aFactor) {
float delta = aFactor/scaleFactor;
scaleFactor = aFactor;
[contentView scale: delta : delta];
}
return self;
}
620 Appendix A: Source Code Listings
- tile
{
NXRect scrollerRect, buttonRect;
[super tile];
return self;
}
@end
Appendix B: References
621
622 Appendix B: References
UNIX
The Design and Implementation of the 4.3BSD UNIX Operating System.
Samuel J. Leffler, Marshall Kirk McKusick, Michael J. Karels, and John S.
Quarterman. Addison-Wesley Publishing Company, New York, 1988.
The UNIX C Shell Field Guide. Gail Anderson and Paul Anderson. Pren-
tice-Hall, 1986.
GNU Tools
GNU Emacs Manual. Richard Stallman. Free Software Foundation, 1987.
An instruction manual for GNU EMACS, one of the editors that comes
with the NeXTSTEP Extended edition.
Although the GNU manuals are available in some bookstores, you can
order them directly from the Free Software Foundation, 675 Mass Avenue,
Cambridge, MA 02139. (617) 876-3296
Computer Security
Practical UNIX Security, Simson L. Garfinkel, and Gene Spafford.
O’Reilly and Associates, Cambridge, MA, 1991. A thorough description of
the UNIX operating system, from a security-minded point of view.
Compositing
“Compositing Digital Images.” Thomas Porter and Tom Duff. Computer
Graphics (SIGGRAPH '84 Conference Proceedings), Vol. 18, No. 3, July
1984, pp. 253-259. The authoritative paper describing the compositing
operator.
Data Formats
Rich Text Format (RTF) Specification. Microsoft Corporation.
624 Appendix B: References
Index
A Carnegie Mellon University 247
abstract superclass 101, 107, 225 case sensitive 34
Access Control 44 category 204, 336
accessor method 95, 285 Cell class 129
accessory View 506 center method 215
action method 82, 136, 156, 159, 460 central processing unit 248
ActionCell class 233 character filter 299
active application 6, 26, 28, 32, 35, 39 child 288
additive color model 450 class 80, 93, 142
addSubview 217 Class Inspector 134
alert panel 295 class method 93, 118, 537
alloc method 96 class object 93
alpha 182, 449 Classes suitcase icon 75
Alternate-dragging 39 Classes view 132, 169
anonymous ftp 29 Classes window 132, 169
app wrapper 42, 51, 126, 188, 296 click-to-focus 25
app.make file 62 clipping rectangle 365
appkit.h 139, 366 clock style 28
Application class 225 close button 51
application font 27 CMYK 450
application icon 183 coherence 208
Application Kit 102, 104, 141 color 449
Application object 235 color well 18
application-defined events 222 Colors panel 452
appWillTerminate method 236 Command-clicking 39
arguments 95 common class 206, 238
Arrange in Front 32 compiler errors 57
assembly language 248 compiling a program 149
auto- launch 25 compositing 369
Autosizing 404, 471 compressed file 43
auxiliary nib 174 computational engine 47, 131
awakeFromNib method 198, 433 connection 82, 133, 146, 233
Connections Inspector 83
B content View 108, 211, 215
back end 47, 131, 260 context sensitive actions 231
background 35 Contract Sel 55
bidirectional 358 Control- dragging 40
blocking functions, read and fread 292 Control object 77, 82
bounds 385 Control panel 9
breakpoint 64 controller object 131
browser 18 crosshair cursor 59, 61
Browser mode 23 csh 48
Browser view 19 cursor 486
buffering in Display PostScript 359 cursor-update events 221
build mode 78
D
C daemons 250
C Threads 413 data encapsulation 93
capitalization conventions 117 data hiding 438
626 Index
deadlock 411 E
default application icon 150 Edit application 47, 51, 90
default return type 98 Edit submenu 31, 269
defaults database 533 Edit tabs 52
defaults registration table 537 Emacs 90, 92
defineps 356 Encapsulated PostScript 38
delegation 193, 198 Encapsulated PostScript images 368
Demos directory 35 encapsulation 93, 219
designated initializer method 243 endps 356
destination image 369 English.lproj 166
developer’s mode 52 .eps file 38
device-independent 253 Escape key 38
Did method 199, 280 Evaluator back end 260
.dir.tiff 21, 62 event 110
.dir.wmd 38 event chain 226
directory icon 183 event handler 136
dirty 73 event handling 227
display 215, 217 event loop 103, 234
display list 409 event mask 222, 478
display method 361, 390 events 74, 221, 476
Display PostScript 108 exception handling 338
Display PostScript operator 109 execv() 288
dissolve:toPoint: method 370, 372
dissolve operator 371 F
dithering 253 factory method 93, 118
docEdited 340 factory object 93
dock 23 field editor 294, 300
document file extension 328 field ending character 300
document icon 327, 328 file descriptor 410
Document submenu 30, 271 file handle 410
document view 299 file name completion 38
documentation 56, 58 file name extensions 324
docView 293 file permissions 40, 41, 44
docView message 299 file system 33
dot files 28 File Viewer 19, 24
DPS 252 File window 72, 174
DPSAddFD() function 292 File’s Owner 73, 178, 181, 273, 278
DPSAddTimedEntry() 373 Find panel 31, 34, 44, 54
DPSBinObjSeqWrite() 357 find pasteboard 512
DPSPrivCurrentContext() 357 Find submenu 31
DPSRemoveTimedEntry() 373 Finder Options 44
drag and drop 37 findViewWithTag 217
Drag pasteboard 512 first responder 74, 309, 328
drag-and-drop 39 First Responder events 477
drawSelf:: method 217, 365 flipped coordinate system 367
dread command 51, 534 folder window 37
dremove command 534 Font class 367
duplicate file command 37 font pasteboard 512
dwrite 51, 534 Font submenu 32
dynamic binding 80 fork() 288
forking 413
627
V
vertical bar 358
vertical scroller 293
vi editor 48, 90
View class 213, 217
View coordinate system 385
View methods 217, 385
Views palette 77
virtual memory 248
vm_deallocate 519
W
white arrow 55
white arrow cursor 37
Will method 200, 281
window method 218
window backing 105
Window class 215, 226
Window delegate 280
Window instance methods 215
Window Server 222, 252, 355
Windows palette 275
Windows submenu 32, 54, 271
workspace 2
Workspace Help panel 43
Workspace Manager 2, 19
WYSIWYG 2
Y
yacc 262
Z
zone 97, 519