Previous Book Contents Book Index Next

Inside Macintosh: Programmer's Guide to MacApp / Part 2 - Working With MacApp
Chapter 13 - Working With Events and Commands


Recipes--Events and Commands

The recipes and sample code in this section describe how to create command objects to respond to events, how to implement an undoable command, how to change the current target object, how to ensure idle time for an event-handler object, and how to use a behavior object to modify the operation of an event-handler object.

Creating a Command Object to Perform an Action--A General Overview

When the application object receives an event that specifies an action, it dispatches the event by calling a method of an event-handler object. The application dispatches mouse-down events by calling the DoMouseCommand method, menu commands by calling DoMenuCommand, Apple events by calling DoScriptCommand, and so on. In any of those methods, the event-handler object can create and post a command object to perform the required action. Command objects are capable of doing, undoing, and redoing the action, as well as sending an Apple event to record the action or to allow an attached script to handle the operation.

For information on handling menu commands, see "Working With Menus--A General Outline," beginning on page 313.

To create a command object to perform an action, you follow these general steps:

  1. Define a command class to perform the action:

    • Define a subclass of TCommand or one of its subclasses.
    • Implement constructor, destructor, and initialization methods for the command class.
    • Override the DoIt method to perform the command action. If the command is undoable, override the UndoIt and RedoIt methods.
    • If the command needs to perform an action only if it is completed in the done state, override the Commit method.
    • If the command allocates storage, free it in the destructor. If the storage must be freed before the destructor is called, override the Free method.
    • If the command is recordable or can be handled by an attached script, set the fUseAppleEvent field to TRUE (usually in the routine that creates the command) and override the MakeAppleEvent method to create an Apple event that describes the command action.

  2. Decide which of your event-handling classes will create the command object.
  3. If a view class will create the command object and the view is created as part of a view hierarchy defined in a view resource, you may need to specify which view in the hierarchy is the target view.
  4. To create and initialize the command object and post it to the MacApp command queue, you override the appropriate method in your event-handling class:

    • DoMenuCommand to respond to a menu choice
    • DoMouseCommand to respond to a mouse event
    • DoKeyEvent or DoCommandKeyEvent to respond to a key event
    • DoScriptCommand to respond to an Apple event

Recipe--Implementing an Undoable Zoom In Menu Command

This recipe is based on the IconEdit application, which displays an icon and allows the user to zoom in to any desired magnification. In IconEdit, the Zoom In menu command appears on the Icon menu and is handled by the TIconDocument class.

To add a Zoom In menu command to a menu and respond when a user chooses that menu command, you perform these steps:

  1. Define a command-number constant for the Zoom In menu item.
  2. Add the Zoom In menu item to a 'CMNU' resource in your resource file.
  3. Implement a command class that handles zoom in.

    • Override the DoIt, UndoIt, and RedoIt methods.

  4. Override the DoSetupMenus method in the view class to enable the Zoom In menu item when appropriate.
  5. Override the DoMenuCommand method in the document class to create and post a command object to perform the zoom operation.

Note
The steps for adding a Zoom Out menu command are nearly identical.
The sample code shown in this recipe is from the IconEdit application. For information on making the Zoom In command recordable, see "Recipe--Defining a Recordable Command Class," beginning on page 353.

Define a Command-Number Constant for the Zoom In Menu Item

The IconEdit application defines a constant for the Zoom In item in both its resource file and its implementation file. The resource file, IconEdit.r, contains the following line:

#define cZoomIn1000
The implementation file, UIconEdit.cp, contains the following line:

const CommandNumber cZoomIn = 1000; // Zoom In menu command.
The constants defined in the resource file and implementation file must match or your application will not be able to respond to the menu choice correctly.

Add the Zoom In Menu Item to a 'CMNU' Resource

The 'CMNU' resource for the Icon menu is located in the file IconEdit.r. It contains the following line:

/* [1] */"Zoom In", noIcon, "M", noMark, plain, cZoomIn;
This line specifies that the Zoom In item is the first item in the menu, it has no icon associated with it, it has the Command-key equivalent of Command-M, it is not marked with a checkmark, it is displayed in plain text, and it has the command number cZoomIn.

Implement a Command Class That Handles Zoom In

The IconEdit application defines the TZoomInCommand to implement zooming in. The class is defined as follows:

class TZoomInCommand : public TCommand
{
   MA_DECLARE_CLASS;       // Generate RTTI information.
   
  public:
    TIconDocument * fIconDocument;// The document that is zoomed.

   TZoomInCommand();       // Constructor.
   virtual ~TZoomInCommand();// Destructor.
   virtual void IZoomInCommand(TIconDocument* itsIconDocument);
   virtual void DoIt();    // Override.
   virtual void RedoIt();  // Override.
   virtual void UndoIt();  // Override.
   virtual TAppleEvent* MakeAppleEvent();// Override.
};
The constructor, TZoomInCommand, sets the fIconDocument field to NULL. The destructor, ~TZoomInCommand, does nothing. The MakeAppleEvent method creates an Apple event that specifies the Zoom In command. MakeAppleEvent is not shown in this recipe but is described in "Recipe--Defining a Recordable Command Class," beginning on page 353. All the other methods of the TZoomInCommand class are shown below.

The IZoomInCommand Method

The initialization method for the Zoom In command class sets a reference to the icon document and calls the initialization method of its parent class, TCommand:

void  TZoomInCommand::IZoomInCommand(TIconDocument* itsIconDocument)
{
   // Save a reference to the icon document.
   fIconDocument = itsIconDocument;

   // Initialize the inherited command.
   this->ICommand(cZoomIn,
               itsIconDocument, 
               kCanUndo, 
               kCausesChange, 
               itsIconDocument); // No view or scroller to track.
}
The kCanUndo constant specifies that the command is undoable, while the kCausesChange constant specifies that the command changes its object to notify. A command's fObjectToNotify field may point to an object, such as a document, to notify when the command operation is completed. For example, a document might update its change count to show it has been modified.

The DoIt Method

The DoIt method for the TZoomInCommand command gets the icon view from the icon document and increases the magnification value by 2.

void TZoomInCommand::DoIt()
{ 
   TIconView * theIconView = fIconDocument->GetIconView();
   if (theIconView != NULL)
   {
      short currentMagnification = theIconView->GetMagnification();
      theIconView->SetMagnification(currentMagnification + 2);
   }
}

The RedoIt Method

The RedoIt method for the TZoomInCommand command is identical to the DoIt method--it gets the icon view from the icon document and increases the magnification value by 2. In fact, the RedoIt method could just call the DoIt method, as many command classes do in similar circumstances.

The UndoIt Method

The UndoIt method for the TZoomInCommand command is similar to the DoIt method--it gets the icon view from the icon document and decreases the magnification value by 2:

void TZoomInCommand::UndoIt()
{ 
   TIconView * theIconView = fIconDocument->GetIconView();
   if (theIconView != NULL)
   {
      short currentMagnification = theIconView->GetMagnification();
      theIconView->SetMagnification(currentMagnification - 2);
   }
}

Override DoSetupMenus in the View Class

The following is the DoSetupMenus method for the TIconEditView class:

void TIconEditView::DoSetupMenus()
{
   // Set up inherited menus.
   Inherited::DoSetupMenus();

   // Can always zoom in.
   Enable(cZoomIn, TRUE);

   // Can zoom out if not at smallest size.
   Enable(cZoomOut, fMagnification > 1);

   // Can always cut, copy, and clear, or set the color.
   Enable(cSetColor, TRUE);
   Enable(cCut, TRUE);
   Enable(cCopy, TRUE);
   Enable(cClear, TRUE);

   // Can paste if 'ICON' data is available.
   gClipboardMgr->CanPaste(kIconClipType);

}  // TIconEditView::DoSetupMenus
The IconEdit application uses the icon view class to enable Zoom In because whenever the icon view is present, the user can zoom in on the icon. The DoSetupMenus method for TIconEditView calls Inherited to allow other classes in the view hierarchy to enable command items. It then calls MacApp's Enable routine to enable various menu items, including Zoom In. The Enable routine calls the Toolbox routine EnableItem.

Note
A DoSetupMenus method doesn't normally disable any menu items, because MacApp disables all menu items before calling DoSetupMenus.

Override DoMenuCommand in the Document Class

The IconEdit application handles the Zoom In menu command in the document class because the document owns the view where zooming takes place. When the DoMenuCommand method is called with the command number cZoomIn, it creates, initializes, and posts a TZoomInCommand object:

void TIconDocument::DoMenuCommand (CommandNumber aCommandNumber) 
{
   switch (aCommandNumber)
   {
      // Handling for some command numbers not shown.

      case cZoomIn:
         TZoomInCommand * theZoomInCommand = new TZoomInCommand();
         theZoomInCommand->IZoomInCommand(this);
         theZoomInCommand->fUseAppleEvent = TRUE;
         this->PostCommand(theZoomInCommand);
      break;
      
      default:
         Inherited::DoMenuCommand(aCommandNumber);  
      break;  
   }
}
The DoMenuCommand method sets the fUseAppleEvent field to TRUE, indicating that when MacApp executes the Zoom In command it should send an Apple event describing the operation. This makes the operation recordable and gives any attached script a chance to handle the command. For more information, see "Recipe--Defining a Recordable Command Class," beginning on page 353.

Recipe--Changing the Current Target Object and
Performing Validation

MacApp's target chain mechanism is described in "Target Chain Dispatching," beginning on page 107. The current target object is the object that gets the first chance to handle user actions such as mouse clicks and menu command choices. A typical target chain consists of the application object, an open document, the document's window, and one or more nested subviews in the window, with a view designated as the current target object.

MacApp automatically changes the target object for standard operations such as switching between application windows. This recipe describes how you can set the target object directly in your code and how you can validate view data when a target change is attempted.

Changing the Current Target Object

To change the current target object, you call the BecomeTarget method of the window or view object that you want to set as the current target object.

The DemoDialogs sample application uses the TMonthlyDialog view class to display a window containing 12 number-entry views labeled with the months of the year. To demonstrate setting the target object, the DoKeyEvent method of the TMonthlyDialog class sets the target to the view's window whenever the Option-Tab combination is pressed. Although DemoDialogs does this merely for show, your application can use similar code to set the target view.

void TMonthlyDialog::DoKeyEvent(TToolboxEvent* event) // Override.
{
   TWindow * theWindow = NULL;
   Boolean dummy;
   
   // If Option-Tab is pressed, make the view's window the current
   // target. This is for test purposes only.
   if ((event->fCharacter == chTab) && (event->IsOptionKeyPressed()))
   {
      // Get the view's window and make it the current target.
      // The return value indicates whether the window actually
      // became the target. Since we're just testing, ignore the
      // return value.
      theWindow = this->GetWindow();
      if (theWindow != NULL)
         dummy = theWindow->BecomeTarget();
   }
   else
      Inherited::DoKeyEvent(event);
}
The BecomeTarget method belongs to the view's TEventHandler ancestor. It does nothing if the view is already the current target object. If the view is not already the current target, BecomeTarget calls the ResignTarget method of the current target.

If the current target object is willing to resign, ResignTarget returns TRUE and BecomeTarget sets the event handler (in this case the view) to be the current target. If the current target is not willing to resign, ResignTarget returns FALSE and the view does not become the target.

Performing Validation When a Target Change Is Attempted

The TEventHandler::ResignTarget method calls the WillingToResignTarget method. MacApp's TDialogTEView::WillingToResignTarget method gives a text-editing view an opportunity to validate its data and refuse to resign as target if it contains invalid data:

long TDialogTEView::WillingToResignTarget() // Override.
{
   long result = 0;

   // Give the current edit text view a chance to validate its data.
   if (fEditText)
   {
      // Some code not shown.
      result = fEditText->GetValidationError();
   }
   return result;
}
This code shows that when the target view changes from a TDialogTEView view to another view, a TEditText or TNumberText view within the TDialogTEView view gets a chance to validate its data. The TEditText::GetValidationError method checks for too many characters. The TNumberText::GetValidationError method checks for a valid numeric value. Your subclass of either of these views can provide additional checking.

Recipe--Ensuring Idle Time for an Event-Handler Object

When a MacApp application isn't busy, it distributes idle time as described in "MacApp's Idling Mechanism," beginning on page 134. MacApp automatically gives idle time to the event-handler objects (objects based on classes that descend from the TEventhandler class) in the target chain.

An event-handler object performs idle operations in its DoIdle method. To specify how often the DoIdle method should be called, you set the fIdleFreq field. The smaller the value of fIdleFreq, the more frequently the DoIdle method is called. A value of 0 indicates that the DoIdle method should be called as often as possible, while the default value of LONG_MAX (231 - 1, or 2,147,483,647) essentially disables idling.

MacApp also distributes idle time to event-handler objects in the cohandler chain (a list of event-handler objects pointed to by the application object's fHeadCohandler field). To ensure that an event-handler object gets idle time, even if it isn't always (or ever) part of the target chain, you install the object in the cohandler chain. An event-handler object used in the cohandler chain is referred to as a cohandler.

To install a cohandler, you call the TApplication::InstallCohandler method; you use the same method to remove the cohandler when it has finished its task. You specify how often a cohandler's DoIdle method should be called by setting the fIdleFreq field, as described above.

To guarantee that an event-handler object will get idle time, you perform these steps:

  1. Override the DoIdle method.
  2. Install the object in the cohandler chain.
  3. Set the fIdleFreq field to specify how often the DoIdle method should be called.
  4. When the object has completed its task, remove it from the cohandler chain.

The sample code in this recipe is for a hypothetical application.

Override the DoIdle Method

Your cohandler class that performs a background or polling task during idle time must override the DoIdle method. You add the following line to the class definition:

virtual Boolean DoIdle (IdlePhase aPhase);
The DoIdle method is a Boolean routine. If it doesn't complete its task, it returns FALSE. If it does complete its task, it removes itself from the cohandler chain and returns TRUE. The DoIdle method for your cohandler class should look something like the following:

Boolean TYourCohandler::DoIdle ( IdlePhase  aPhase )
{
   Boolean taskComplete = FALSE;

   // Your cohandler code--the code that you want executed
   // periodically--goes here.
   // If the task is completed, set taskComplete to TRUE.

   // If the cohandler has completed its task, remove it.
   if (taskComplete)
      gApplication->InstallCohandler(this, FALSE);

   return taskComplete;
}

Install the Object in the Cohandler Chain

A cohandler object can install itself in the cohandler chain with a line like the following:

gApplication->InstallCohandler (this, TRUE);

Set the fIdleFreq Field to Specify How Often DoIdle Should Be Called

A cohandler object that needs to be called as often as possible sets its fIdleFreq field with code like the following:

this->SetIdleFreq(0);
If your cohandler's DoIdle method should be called less often, pass a larger value to SetIdleFreq.

Remove the Object From the Cohandler Chain

You call the same application method to remove a cohandler that you call to install one, InstallCohandler. The following code, from the DoIdle method shown above, demonstrates how to remove a cohandler from the cohandler chain:

// If the cohandler has completed its task, remove it.
if (taskComplete)
   gApplication->InstallCohandler(this, FALSE);

Recipe--Using a Behavior Object to Implement Alphabetic Type-Ahead

The Dialogs menu of the DemoDialogs application has a Format Dialog menu choice that displays a format dialog box. The dialog box includes a font list implemented with a TFontListView object. When the font list view is active, you can use alphabetic type-ahead to specify a font in the list. The TFontListView class provides this service by adding a TKeySelectionBehavior behavior object.

To use a behavior object to add alphabetic type-ahead to a view, you perform these steps:

  1. Define a view class that descends from MacApp's TTextListView class.
  2. Add a TKeySelectionBehavior behavior object to the view object.

The sample code shown in this recipe is from the DemoDialogs application.

For another recipe that uses behavior objects, see "Recipe--Using Dependencies and Behaviors to Synchronize Control Views," beginning on page 588.

Define a View Class That Descends From TTextListView

The class definition for the TFontListView class in the DemoDialogs application includes the following code:

class TFontListView : public TTextListView
{
   MA_DECLARE_CLASS;
   
public:
   FontListPtr fFontList;// Font resource IDs.
   .
   .
   .
   // Build an array of font resource IDs whose names are
   // in alphabetical order.
   virtual void InitFontList();
   .
   .
   .
}
The TFontListView class descends from MacApp's TTextListView class, a class designed to display text items in a one-dimensional list. TFontListView defines the InitFontList method to initialize the font list and put the items in the list into alphabetical order.

Add a TKeySelectionBehavior Behavior to the View Object

Your view class adds a TKeySelectionBehavior behavior object in its initialization method. The TFontListView class does so with the following code, taken from the InitFontList method:

TKeySelectionBehavior *behavior;
behavior = new TKeySelectionBehavior;
behavior->IKeySelectionBehavior('KYSL');
this->AddBehavior(behavior);
The value passed to the IKeySelectionBehavior method, 'KYSL', is an arbitrary identifier for the behavior object. Once the behavior is added to the view by the call to AddBehavior, it works together with the view to supply alphabetic type-ahead.


Previous Book Contents Book Index Next

© Apple Computer, Inc.
25 JUL 1996