TweetFollow Us on Twitter

Contextual Menu Modules

Volume Number: 14 (1998)
Issue Number: 2
Column Tag: Toolbox Techniques

Contextual Menu Modules

by Steve Sheets

By creating Contextual Menu Extension Modules for Mac OS 8, you can extend the behavior of all applications

A New type of Extension

The Contextual Menu (CM) Manager was introduced with Mac OS 8.0. Using this manager, developers now can provide contextual menus in their programs. When a user clicks some data while holding the control key down, a list of data specific commands can appear. For example, the Finder uses this API to display various commands (Open, Move to Trash, Get Info, Duplicate) when files & folders are control-clicked. As mentioned in last month's article, the commands that are listed come from two sources; the program and CM extension modules. This month's article will demonstrate how to create the example plug-in "CaseCharm".

At any point in a program that uses the CM API where "ContextualMenuSelect" is called, the programmer can provide 2 important pieces of information. First, he can provide the list of the commands to be displayed. Secondly, he can provide the data that is currently selected. CM extensions, also known as plug-ins, can use this data to decide what additional commands also can be added to the menu. If the user selects one of the commands that comes from the plug-in instead of from the program, the plug-in takes care of that action. The calling program is not aware that anything was selected; it just functions as if the user failed to select an item from the menu. Except for being able to look at the selected data, the plug-ins are transparent to the calling program, and the calling program is not aware of the plug-ins.

So what are CM extensions? They are PowerPC code fragments (that is, shared libraries) using the SOM Object model. Because of this, plug-ins can only run on PowerPC Macintoshes. The API is standard on Mac OS 8.0 computers, but it also can be installed on System 7.x machines. Do not assume that all the Mac OS 8.0 APIs are available for a contextual menu. Though uncommon, contextual menus can be used on a pre-8.0 machine.

CM plug-in files must have the file type of 'cmpi'. If they have the creator of 'cmnu', they will display the generic CM icon. The extension file must reside in the "Contextual Menu Items" folder in the System Folder. On Mac OS 8.0, dragging the extension onto the System Folder will automatically move the file to the correct location. On pre-Mac OS 8.0, you must drag the files manually. Once they are in the correct location, the System must be rebooted for the plug-in to be installed.

Being SOM objects, CM plug-ins are object-oriented subclasses of the super class AbstractCMPlugin. AbstractCMPlugin object has several calls that must be overridden for the extension to work. While you can create CM plug-ins that do not look at the selected data, but just provide some function, this is not a recommended. There are plenty of other way to provide commands in the Macintosh. Apple recommends that third party developers work on CM plug-ins that are data sensitive.

While the Contextual Menu Manager passes the selected data to the plug-in, the plug-in can not modify this data. At most, it can make a copy of the data, modify the copy, and then return the data by placing it in the Clipboard. The example project CaseCharm, discussed below, takes text data and shifts it to either upper or lower case. For example, in a word processor, a user would select a word, control-click that word, select the CaseCharm command, then paste the content of the Clipboard (the word all in upper or lower case) back into the word processor. This passing of modified data using the Clipboard is a common function of CM plug-ins.

Creating a Module Project

While a CM project can be created from scratch, the easiest way to set up one is to take an existing CM sample project and modify it for your needs. It is to easy to incorrectly set one of the required preferences in the project. In the CM Manager SDK (available from Developer CDs and websites), Apple provides a good sample program to start with. The example used in this article is based on Apple's sample code.

You must change a couple of project preferences when creating a new CM plug-in. For this project you must first change the PPC Target file name to "CaseCharm". Next, the PPC Linker Entry Point for Initialization must be changed to a new name. While it can be anything, the common usage is XXXInitialize, XXX is the name of the CM plug-in, in this case "CaseCharmInitialize".

Then you must modify the source files to match the name changes. The .cp, .h & .r files should be renamed to match the CM plug-in name. In this case, "CaseCharm.h", CaseCharm.cp" and "CaseCharm.r". Following sections will explain the changes within these files. In addition to these source files, the sample project includes the "UContextualMenuTools.cp" file. This code provides many useful routines to handle functions common to all CM plug-ins.

Additionally, various libraries must be linked in the project. These include AbstractCMPlugin, InterfaceLib, MSL RuntimePPC.Lib and SOMObjects(tm) for Mac OS. The file names for some of these libraries may be different for different development environments. The AppearanceLib library is required only if the CM plug-in uses the Appearance Manager.

Once all the changes are done, compile and link the code. As mentioned above, the resulting shared library must be dragged to the "Contextual Menu Items" folder and the Mac rebooted to test the code.

Adding Resources

A CM plug-in does not require much in the area of resources. Like all code fragments, the 'cfrg' resource is required. The fields containing the code fragment name & help information (in this case the strings "CaseCharm" and "A Contextual Menu Plugin to change the case of selected text") should be changed. It is always a good idea to include version information ('vers' resources) with any code.

Listing 1

CaseCharm.r
Typical resources for a Contextual Menu plug-in, in this case CaseCharm.

#define UseExtendedCFRGTemplate 1

#include "SysTypes.r"
#include "CodeFragmentTypes.r"

/*  Code Fragment Info */
resource 'cfrg' (0) {
  {  /* array memberArray: 4 elements */
    extendedEntry {
      kPowerPC,
      kFullLib,
      kNoVersionNum,
      kNoVersionNum,
      kDefaultStackSize,
      kNoAppSubFolder,
      kIsLib,
      kOnDiskFlat,
      kZeroOffset,
      kSegIDZero,
      "CaseCharm",           /* Changed for each Plug-In */
      kFragSOMClassLibrary,
      "AbstractCMPlugin",
      "",
      "",
      "A Contextual Menu Plugin to change the case of "
        "selected text."      /* Changed for each Plug-In */
    }
  }
};

/*  Version Info  */
resource 'vers' (1) {
  1, 0, final, 0, verUS, "1.0", "Mageware, 1997"
};

resource 'vers' (2) {
  1, 0, final, 0, verUS, "1.0", "CaseCharm 1.0"
};

Methods and Methods

As will be stated over and over, a CM plug-in is a SOM object. In this example, CaseCharm is a subclass of class AbstractCMPlugin. This new class overrides 4 methods of AbstractCMPlugin. CaseCharm also requires one special initialization routine for SOM to identify it. All CM plug-ins override these 4 methods (and provide an Initialization routine), so the header code is generally identical. Listing 2 shows the CaseCharm.h file. The name of the new subclass of AbstractCMPlugin must match the name given in the resource file (in this case, "CaseCharm").

Listing 2

CaseCharm.h
Typical header file for a Contextual Menu plug-in, in this case CaseCharm.

#pragma once
#include <AbstractCMPlugin.h>

class CaseCharm : virtual AbstractCMPlugin {

#pragma SOMReleaseOrder (Initialize, ExamineContext, HandleSelection, PostMenuCleanup)

public:

  virtual  OSStatus Initialize(Environment* ev,
            FSSpec *inFileSpec);
            
  virtual  OSStatus ExamineContext(Environment* ev,
            AEDesc *inContextDescriptor,
            SInt32 inTimeOutInTicks,
            AEDescList* ioCommands,
            Boolean* outNeedMoreTime);
            
  virtual  OSStatus HandleSelection(Environment* ev,
            AEDesc *inContextDescriptor,
            SInt32 inCommandID);
            
  virtual  OSStatus PostMenuCleanup(Environment* ev);
};

The four methods shown here are called by the CM API when needed. Initialize() handles the initialization of the current CM. This is different from the SOM Initialization routine (defined below). It is easy to confuse the two calls. When the CM Manager is started up with the computer, a single instance of all CM extension objects is created. At that time, the Initialize() call is made. This may be before other Managers have started, so generally nothing is done in this routine. The other 3 routines are all called when a contextual menu is being created for display. The ExamineContext() method is where the plug-in adds menu items to the popup menu. This is the first time the code can examine the selected data (if any). Based on the data, the method can put any number of menu items or sub menus into the current contextual menu. The HandleSelection() call is only called if a given menu item has been selected by the user. It can then handle the actual function selected. The PostMenuCleanUp() call is the routine to be called after the current contextual menu has been handled, regardless to who handled it. The first parameter for all four of the routines is a code fragment environment parameter and is rarely used.

SOM Fragment Initializing

Being a SOM object, CaseCharm is required to have an initialization routine. The name of this routine can be anything, but it must match the name entered in the Linker preferences (CaseCharmInitialize in this case). Listing 3 shows the section of the code dealing with this call. The function calls the SOM __initialize() routine, and defines the new class. Except for the name, there is no reason to modify this code.

Listing 3

CaseCharm.cp
SOM Fragment Initializer

//  Includes
#include <CodeFragments.h>
#include <som.xh>

//  External Functions
extern pascal OSErr __initialize(CFragInitBlockPtr);

//  Initializer
pascal OSErr CaseCharmInitialize(CFragInitBlockPtr init)
{
#pragma unused (init)

  OSErr theError = __initialize(init);
  
  if (theError == noErr)
    somNewClass(CaseCharm);

  return theError;
  
}

Setup and Shutdown

The Initialize() routine is called at startup of the current instance of a contextual menu, and is surprisingly useless. Memory and resources should be allocated when the contextual menu is being used and not kept around all the times. The PostMenuCleanUp() call can take care of anything started by ExamineContext(). Remember the HandleSelection() method may not be called if the user didn't select an items. It is more common, as in the CaseCharm example, for them to do nothing but return a noErr result.

Listing 4

CaseCharm.cp
Setup & Shutdown

//  CM Initialize
OSStatus CaseCharm::Initialize(Environment* ev,
            FSSpec* inFileSpec)
{
#pragma unused (ev, inFileSpec)

  return noErr;
}

//  CM Clean Up
OSStatus CaseCharm::PostMenuCleanup(Environment* ev)
{
#pragma unused (ev)

  return noErr;
}

Coercing Text Data

Both the ExamineContext() and the HandleSelection() call are passed inContextDescriptor, a pointer to an AEDesc. This descriptor contains the selected data that the user control-clicked. This CM extension can use this descriptor to examine the selected data. UContextualMenuTools has a few routines that provide commonly used requests. Listing 5 show G_ContextualMenuTools_Has_Text_Data() (which checks if text data has been passed), G_ContextualMenuTools_Get_Text_Hdl() (which returns a copy of text passed as handle) and G_ContextualMenuTools_Get_Text_Str() (which returns a copy of text passed as string).

Since AECoerceDesc is being used, the Apple Event Manager tries very hard to return text data, even if the original data was not exactly text. For example, control-clicking a file from the finder would pass a file reference. However, AECoerceDesc would successfully convert the name of the file into text, and return that text. This is not dangerous, just something that should be noted.

Listing 5

UContextualMenuTools.cp
Handling Text Data

//  Descriptor has text data stored in it
Boolean G_ContextualMenuTools_Has_Text_Data
          (AEDesc* p_context_descriptor_ptr)
{
  Boolean a_flag = false;
  
  AEDesc a_text_desc = { typeNull, NULL };
  if (AECoerceDesc(p_context_descriptor_ptr, typeChar, 
        &a_text_desc) == noErr)
    if (a_text_desc.descriptorType==typeChar) 
      if (a_text_desc.dataHandle) {
        long a_size = GetHandleSize(a_text_desc.dataHandle);
        
        a_flag = (a_size>0);
      }

  AEDisposeDesc(&a_text_desc);
  
  return a_flag;
}

//  Get Text Data from descriptor as handle
Handle G_ContextualMenuTools_Get_Text_Hdl
          (AEDesc* p_context_descriptor_ptr)
{
  AEDesc a_text_desc = { typeNull, NULL };
  if (AECoerceDesc(p_context_descriptor_ptr, typeChar, 
        &a_text_desc) == noErr)
    if (a_text_desc.descriptorType==typeChar)
      return a_text_desc.dataHandle;

  AEDisposeDesc(&a_text_desc);
    
  return NULL;
}

//  Get Text Data from descriptor as string
void G_ContextualMenuTools_Get_Text_Str
        (AEDesc* p_context_descriptor_ptr, Str255 p_str)
{
  p_str[0] = 0;
  
  Handle a_handle = G_ContextualMenuTools_Get_Text_Hdl(p_context_descriptor_ptr);
  if (a_handle) {
    HLock(a_handle);
    
    short a_len = GetHandleSize(a_handle);
    if (a_len>255)
      a_len = 255;
      
    BlockMove(*a_handle, &p_str[1], a_len);
    p_str[0] = a_len;

    HUnlock(a_handle);
    DisposeHandle(a_handle);
  }
}

Adding Menus & Submenus

Once the CM plug-in has identified that it was passed data that it can use, the code must add items and submenus to the contextual menu being created. Not surprisingly, the CM Manager uses ioCommands (an AEDesc) to do this. Each menu item requires a string and a ID number. The ID number is strictly unique for this CM plug-in. The CM Manager keeps track of ID between different plug-ins. Assuming the user selects one of the items this CM plug-in created, the HandleSelection() call will be passed that ID.

Listing 6 shows a number of routines that are useful for creating these menu items, including a routine to create a menu item: G_ContextualMenuTools_Add_MenuItem(), a routine to create a blank (dotted) menu item: G_ContextualMenuTools_Add_Blank_MenuItem(), a routine to start a sub menu: G_ContextualMenuTools_Start_SubMenu() and a routine to finish creating a sub menu: G_ContextualMenuTools_Finish_SubMenu().

Listing 6

UContextualMenuTools.cp
Building Menus

//  Add Menu Item to AEDescList
OSStatus G_ContextualMenuTools_Add_MenuItem
            (Str255 p_name, long p_command,
            AEDescList* p_command_list_ptr)
{
  OSStatus a_err = noErr;
  
  AERecord a_command_record = { typeNull, NULL };
  a_err = AECreateList(NULL, 0, true, &a_command_record);
    
  if (a_err==noErr) {
    a_err = AEPutKeyPtr(&a_command_record, keyAEName, 
              typeChar, &p_name[1], p_name[0]);
      
    if (a_err==noErr)
      a_err = AEPutKeyPtr(&a_command_record, 
                keyContextualMenuCommandID, typeLongInteger, 
                &p_command, sizeof (p_command));
    
    if (a_err==noErr)
      a_err = AEPutDesc
              (p_command_list_ptr, 0, &a_command_record);
    AEDisposeDesc(&a_command_record);
  }

  return a_err;
} 

//  Add Blank Menu Item
OSStatus G_ContextualMenuTools_Add_Blank_MenuItem
            (AEDescList* p_command_list_ptr)
{
  return G_ContextualMenuTools_Add_MenuItem
            ("\p-", 0, p_command_list_ptr);
}

//  Start making a SubMenu 
OSStatus G_ContextualMenuTools_Start_SubMenu
          (AEDescList* p_submenu_command_list_ptr)
{
  p_submenu_command_list_ptr->descriptorType = typeNull;
  p_submenu_command_list_ptr->dataHandle = NULL;
  
  return AECreateList
          (NULL, 0, false, p_submenu_command_list_ptr);
}

//  Finish making a SubMenu 
OSStatus G_ContextualMenuTools_Finish_SubMenu
            (AEDescList* p_command_list_ptr,
            AEDescList* p_submenu_command_list_ptr,
            Str255 p_supermenu_name)
{
  OSStatus a_err = noErr;
  
  AERecord p_supermenu_command_list = { typeNull, NULL };
  
  a_err = AECreateList
            (NULL, 0, true, &p_supermenu_command_list);
      
  if (a_err==noErr)
    a_err = AEPutKeyPtr(&p_supermenu_command_list, keyAEName, 
              typeChar, &p_supermenu_name[1], 
              p_supermenu_name[0]);
      
  if (a_err==noErr)
    a_err = AEPutKeyDesc(&p_supermenu_command_list, 
              keyContextualMenuSubmenu, 
              p_submenu_command_list_ptr);
      
  if (a_err==noErr)
    a_err = AEPutDesc(p_command_list_ptr, 0, 
              &p_supermenu_command_list);
  
  AEDisposeDesc(p_submenu_command_list_ptr);
  AEDisposeDesc(&p_supermenu_command_list);

  return a_err;
}

The add menu item function is passed the name of the menu item, the ID, and the AEDesc to attach the item to. It creates a temporary list descriptor, adds the data to it, and puts the list into the passed AEDesc. The temporary list is then disposed of. The function G_ContextualMenuTools_Add_Blank_MenuItem() calls the function G_ContextualMenuTools_Add_MenuItem() with the correct values to create such a blank menu item. When the ioCommands parameter is used with these routines, the menu item is put on the top-most menu. However, these routines also can be used on submenus.

To create a submenu, the call G_ContextualMenuTools_Start_SubMenu() to create an AEDesc. Use this descriptor just as if it was the ioCommands parameter and add menu items to it. When all the menu items are added, used G_ContextualMenuTools_Finish_SubMenu() to attach the submenu (with name) to the ioCommands. In this manner, you can create any number of submenus. Listing 7 shows how to do this in the CaseCharm code. A submenu is created. Assuming text is available, two commands (and a blank line) are added. No matter what, the About command is added, and the entire submenu is then added to the main menu.

The only other two yet to be explained parameters for ExamineContext are inTimeOutInTicks and outNeedMoreTime. The inTimeOutInTicks value is the amount of time the CM plug-in can use to examine the selected data. If the call takes longer than this, it should return, with the outNeedMoreTime parameter set to true. This allows the CM Manager to handle creating larger or more difficult menu items. For simple contextual menus that do not access the disk (like the example), it is safe to set outNeedMoreTime to false.

Listing 7

CaseCharm.cp
CM Exaime Content

OSStatus CaseCharm::ExamineContext(Environment* ev,
            AEDesc *inContextDescriptor,
            SInt32 inTimeOutInTicks,
            AEDescList* ioCommands,
            Boolean* outNeedMoreTime)
{
#pragma unused(ev, inTimeOutInTicks)
  
  if (inContextDescriptor != NULL) {
    AEDescList a_sub_menu_commands = { typeNull, NULL };
    
    G_ContextualMenuTools_Start_SubMenu(&a_sub_menu_commands);
    
    if (G_ContextualMenuTools_Has_Text_Data
        (inContextDescriptor)) 
    {
      G_ContextualMenuTools_Add_MenuItem
        ("\pConvert to Uppercase", 1002, 
        &a_sub_menu_commands);

      G_ContextualMenuTools_Add_MenuItem
        ("\pConvert to Lowercase", 1003, 
        &a_sub_menu_commands);

      G_ContextualMenuTools_Add_Blank_MenuItem
        (&a_sub_menu_commands);
    }
    
    G_ContextualMenuTools_Add_MenuItem
      ("\pAbout CaseCharm...", 1001, &a_sub_menu_commands);
      
    G_ContextualMenuTools_Finish_SubMenu
      (ioCommands, &a_sub_menu_commands, "\pCaseCharm");
  }
  
  *outNeedMoreTime = false;
  
  return noErr;
  
}

More and more CM plug-ins are currently being created. The room on the contextual menus is starting to run out. If the CM plug-in you create as more than one or two commands, I suggest putting the entire list of commands into a submenu. This is becoming a common user interface; a submenu with the name of the product, a number of command menu items, a blank line, and the About command menu item(s).

Outputting Result Through The Scrap

When a user selects a given menu item in the contextual menu, the ID is passed to the associated CM plug-in. Again, the selected data can be parsed out. Assuming some manipulation is done on it, the result can be passed back to the user in the Clipboard (that is the Scrap). Listing 8 shows some simple routines to return text (as handles or strings) to the user this way. Notice that G_ContextualMenuTools_Set_Scrap_Text_Hdl() does not dispose of the handle, so the program must do it explicitly after the routine is called.

Listing 8

UContextualMenuTools.cp
Set Clipboard Text

//  Set Text Scrap using handle
void G_ContextualMenuTools_Set_Scrap_Text_Hdl
        (Handle p_handle)
{
  ZeroScrap();

  if (p_handle) {
    HLock(p_handle);
    PutScrap(GetHandleSize(p_handle), 'TEXT', *p_handle);
    HUnlock(p_handle);
  }
}

//  Set Text Scrap using str
void G_ContextualMenuTools_Set_Scrap_Text_Str(Str255 p_str)
{
  ZeroScrap();

  if (p_str[0]) {
    PutScrap(p_str[0], 'TEXT', &p_str[1]);
  }
}

User Interface

Contextual menus can have user interfaces. If a user has selected an item, dialogs and alerts can be displayed. They have to be modal, and be cleaned up before the HandleSelection() routine returns. Listing 9 provides some tools for this. The function G_ContextualMenuTools_Standard_Alert() creates a standard information alert using the new Appearance Manager StandardAlert() call. Since the CM Manager may run on machine without the Appearance Manager, it uses the G_ContextualMenuTools_Get_Gestalt_Flag() to make sure the call is available.

More complex user interfaces can be created using resources stored in the CM plug-in file. However, this file is not open as a resource file when the code is called. The G_ContextualMenuTools_Open_Resfile() takes care of this. It uses FindFolder() to search for the Contextual Menu Folder. If that fails, it searches for the folder by name. (In pre-Mac OS 8 computers, the CM Installer appears to fail to set the Contextual Menu Folder to the correct ID.) Once the file is open, any type of resource can be accessed from it. Just remember to close the resource file when you are done.

Listing 9

UContextualMenuTools.cp
User Interfaces

//  Check bit flag of Gestalt Selector 
Boolean G_ContextualMenuTools_Get_Gestalt_Flag
          (OSType p_selector, short p_bit_num)
{
  long a_result;

  if (Gestalt(p_selector, &a_result) == noErr)
    return BitTst(&a_result, 31-p_bit_num);
  else
    return 0;
}

//  Standard Alert
void G_ContextualMenuTools_Standard_Alert
          (Str255 p_title, Str255 p_info)
{
  if (G_ContextualMenuTools_Get_Gestalt_Flag
        (gestaltFSAttr, gestaltHasFSSpecCalls)) {
    short a_hit;
    StandardAlert(kAlertNoteAlert, p_title, 
        p_info, NULL, &a_hit);
  }
}

//  Open ContextualMenu Resource File
short G_ContextualMenuTools_Open_Resfile(Str255 p_name)
{
  short a_result = -1;
  OSErr a_err;
  
  if (p_name[0]) {
    if (G_ContextualMenuTools_Get_Gestalt_Flag
        (gestaltFSAttr, gestaltHasFSSpecCalls) 
      && G_ContextualMenuTools_Get_Gestalt_Flag
        (gestaltFindFolderAttr, gestaltFindFolderPresent)) {
      long a_dir_id;
      short a_vol_ref_num;
      FSSpec a_file_spec;
      if (FindFolder(kOnSystemDisk, 'cmnu', kCreateFolder, 
          &a_vol_ref_num, &a_dir_id) == noErr) {
        BlockMove(&p_name[0], &a_file_spec.name[0], 64);
        a_file_spec.vRefNum = a_vol_ref_num;
        a_file_spec.parID = a_dir_id;
        
        a_result = FSpOpenResFile(&a_file_spec, fsRdPerm);
      }
      else if (FindFolder(kOnSystemDisk, kSystemFolderType, 
              kCreateFolder, &a_vol_ref_num, &a_dir_id) 
              == noErr) {
        Str255 a_dir_name = "\p:Contextual Menu Items:";
        short a_dir_name_len = a_dir_name[0];
        short a_name_len = p_name[0];
        
        BlockMove(&p_name[1], &a_dir_name[a_dir_name_len+1], 
            a_name_len);
        a_dir_name[0] = a_dir_name_len + a_name_len;

        a_err = FSMakeFSSpec(a_vol_ref_num, a_dir_id, 
                  a_dir_name, &a_file_spec);
        if (a_err==noErr)
          a_result = FSpOpenResFile(&a_file_spec, fsRdPerm);
      }
    }
  }

  return a_result;
}

Sample Code

The final listing, Listing 10, shows the HandleSelection() code for the example, CaseCharm. Depending on the command passed in inCommandID, it shows the About Box using the G_ContextualMenuTools_Standard_Alert() call, or it retrieves the text using the G_ContextualMenuTools_Get_Text_Hdl() call and passes it to the CaseCharmUpperLower(). That routine shifts the case, and returns it using G_ContextualMenuTools_Set_Scrap_Text_Hdl().

Listing 10

UContextualMenuTools.cp
Handle Selection

//  Convert the given text (upper or lower)
//    and place it in scrap book.

void CaseCharmUpperLower(Handle p_text,
            Boolean p_is_upper)
{
  if (p_text) {
    long a_length = GetHandleSize(p_text);
    if (a_length>0) {
      HLock(p_text);
      
      char a_char;
      for (long a_count = 0; a_count<a_length; a_count++) {
        a_char = *(*p_text+a_count);
        
        if (p_is_upper) {
          if ((a_char>='a') && (a_char<='z'))
            *(*p_text+a_count) = a_char - 'a' + 'A';
        }
        else {
          if ((a_char>='A') && (a_char<='Z'))
            *(*p_text+a_count) = a_char - 'A' + 'a';
        }
      }
      
      HUnlock(p_text);

      G_ContextualMenuTools_Set_Scrap_Text_Hdl(p_text);
    }
  }
}

Steve Sheets has been happily programming the Macintosh since 1983, which makes him older than he wishes, but not as young as he acts. Being a native Californian, his developer wanderings have led him to Northern Virginia. For those interested, his non-computer interests involve his family (wife & 2 daughters), Society for Creative Anachronism (Medieval Reenactment) and Martial Arts (Fencing, Tai Chi). He is currently an independent developer, taking on any and all projects and can be reached at MageSteve@aol.com.

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Microsoft Remote Desktop 8.0.34 - Connec...
With Microsoft Remote Desktop, you can connect to a remote PC and your work resources from almost anywhere. Experience the power of Windows with RemoteFX in a Remote Desktop client designed to help... Read more
Data Rescue 4.2.3 - Powerful hard drive...
Use Data Rescue to recover: crashed, corrupted or non-mounting hard drive deleted, damaged, or lost files reformatted or erased hard drive One powerful new feature found in Data Rescue 4 is... Read more
Viber 6.2.0 - Send messages and make cal...
Viber lets you send free messages and make free calls to other Viber users, on any device and network, in any country! Viber syncs your contacts, messages and call history with your mobile device, so... Read more
Lyn 1.7.2 - Lightweight image browser an...
Lyn is a fast, lightweight image browser and viewer designed for photographers, graphic artists, and Web designers. Featuring an extremely versatile and aesthetically pleasing interface, it delivers... Read more
Lyn 1.7.2 - Lightweight image browser an...
Lyn is a fast, lightweight image browser and viewer designed for photographers, graphic artists, and Web designers. Featuring an extremely versatile and aesthetically pleasing interface, it delivers... Read more
Tunnelblick 3.6.7beta02 - GUI for OpenVP...
Tunnelblick is a free, open source graphic user interface for OpenVPN on OS X. It provides easy control of OpenVPN client and/or server connections. It comes as a ready-to-use application with all... Read more
jAlbum Pro 13.4 - Organize your digital...
jAlbum Pro has all the features you love in jAlbum, but comes with a commercial license. You can create gorgeous custom photo galleries for the Web without writing a line of code! Beginner-friendly... Read more
calibre 2.65.1 - Complete e-book library...
Calibre is a complete e-book library manager. Organize your collection, convert your books to multiple formats, and sync with all of your devices. Let Calibre be your multi-tasking digital librarian... Read more
jAlbum 13.4 - Create custom photo galler...
With jAlbum, you can create gorgeous custom photo galleries for the Web without writing a line of code! Beginner-friendly, with pro results - Simply drag and drop photos into groups, choose a design... Read more
Backblaze 4.2.0.966 - Online backup serv...
Backblaze is an online backup service designed from the ground-up for the Mac. With unlimited storage available for $5 per month, as well as a free 15-day trial, peace of mind is within reach with... Read more

Mummy madness in new action game Tomb He...
Hot on the tail of Bump Hero, ZPlay is giving gamers another reason to get screen bashing with a brand new release. Tomb Heroes is a challenging action game in which you battle enemies in various tombs around the world. You can select from nine... | Read more »
Siralim 2 (RPG / Roguelike) (Games)
Siralim 2 (RPG / Roguelike) 1.0 Device: iOS Universal Category: Games Price: $4.99, Version: 1.0 (iTunes) Description: Siralim 2 is an old-school monster catching RPG. Summon and customize hundreds of creatures to fight for you as... | Read more »
Clean Text (Productivity)
Clean Text 1.0 Device: iOS Universal Category: Productivity Price: $3.99, Version: 1.0 (iTunes) Description: | Read more »
Gemini - A Journey of Two Stars (Games)
Gemini - A Journey of Two Stars 1.0.1 Device: iOS Universal Category: Games Price: $2.99, Version: 1.0.1 (iTunes) Description: *** SPECIAL LAUNCH SALE: $2.99 (25% off) *** "A mesmerizing and unexpectedly emotional journey." -- Los... | Read more »
How to get four NFL superstars for your...
Even though you're probably well on your way to building a top notch squad for the new season in Madden NFL Mobile, let's say you could beef it up by adding Rob Gronkowski, Antonio Brown, Von Miller, and Todd Gurley to your roster. That's... | Read more »
Cartoon Network Superstar Soccer: Goal!!...
Cartoon Network Superstar Soccer: Goal!!! – Multiplayer Sports Game Starring Your Favorite Characters 1.0 Device: iOS Universal Category: Games Price: $2.99, Version: 1.0 (iTunes) Description: Become a soccer superstar with your... | Read more »
NFL Huddle: What's new in Topps NFL...
Can you smell that? It's the scent of pigskin in the air, which either means that cliches be damned, pigs are flying in your neck of the woods, or the new NFL season is right around the corner. [Read more] | Read more »
FarmVille: Tropic Escape tips, tricks, a...
Maybe farming is passé in mobile games now. Ah, but farming -- and doing a lot of a other things too -- in an island paradise might be a little different. At least you can work on your tan and sip some pina coladas while tending to your crops. [... | Read more »
Become the King of Avalon in FunPlus’ la...
King Arthur is dead. Considering the legend dates back to the 5th century, it would be surprising if he wasn’t. But in the context of real-time MMO game King of Avalon: Dragon Warfare, Arthur’s death plunges the kingdom into chaos. Evil sorceress... | Read more »
Nightgate (Games)
Nightgate 1.0 Device: iOS Universal Category: Games Price: $2.99, Version: 1.0 (iTunes) Description: *** Launch Sale: 25% OFF for a limited time! *** In the year 2398, after a great war, a network of intelligent computers known as... | Read more »

Price Scanner via MacPrices.net

9-inch 32GB iPad Pros on sale for $70 off MSR...
B&H Photo has 9″ 32GB WiFi Apple iPad Pros on sale for $70 off MSRP, each including free shipping. B&H charges sales tax in NY only: - 9″ Space Gray 32GB WiFi iPad Pro: $529 $70 off MSRP - 9... Read more
Mac minis on sale for up to $140 off MSRP
Adorama has Mac minis on sale for up to $100 off MSRP including free shipping plus NY & NJ sales tax only: - 1.4GHz Mac mini: $449 $50 off MSRP - 2.6GHz Mac mini: $649 $50 off MSRP Amazon has the... Read more
Back To School with OtterBox Essentials
Back to school means back to an environment that is tough on tech. OtterBox has the back to school essentials you need to keep tech safe from drops, bumps, scratches and hallway havoc. Check out the... Read more
VRS Design Releases New iPhone 7 Plus Case Li...
With a device as large and costly as the iPhone 7 Plus, it is primal instinct to protect it from potential damage. According to a study by SquareTrade in 2012, iPhone damages cost Americans roughly $... Read more
MacBook Airs on sale for up to $101 off MSRP
Amazon has 11″ and 13″ MacBook Airs on sale for up to $101 off MSRP for a limited time. Shipping is free: - 11″ 1.6GHz/128GB MacBook Air (model MJVM2LL/A): $798 $101 off MSRP - 11″ 1.6GHz/256GB... Read more
Apple certified refurbished iPad mini 4s avai...
Apple has certified refurbished iPad mini 4s now available for up to $120 off the cost of new models. An Apple one-year warranty is included with each iPad, and shipping is free. The following models... Read more
Apple price trackers, updated continuously
Scan our Apple Price Trackers for the latest information on sales, bundles, and availability on systems from Apple’s authorized internet/catalog resellers. We update the trackers continuously: - 15″... Read more
Global Tablet Shipments Projected to Increase...
Digitimes’ Jim Hsiao reports that global tablet shipments will increase by 16.3 percent sequentially to reach nearly 47 million units in 2016′s third quarter, but that volume will still be down over... Read more
Apple’s 2016 Back to School promotion: Free B...
Purchase a new Mac or iPad using Apple’s Education Store and take up to $300 off MSRP. All teachers, students, and staff of any educational institution qualify for the discount. Shipping is free, and... Read more
Apple refurbished iPad Air 2s available start...
Apple has Certified Refurbished iPad Air 2 available starting at $339. Apple’s one-year warranty is included with each model, and shipping is free: - 128GB Wi-Fi iPad Air 2: $499 - 64GB Wi-Fi iPad... Read more

Jobs Board

*Apple* Solutions Consultant - Apple (United...
# Apple Solutions Consultant Job Number: 51218354 Fredericksburg, Virginia, United States Posted: Aug. 18, 2016 Weekly Hours: 40.00 **Job Summary** As an Apple Read more
*Apple* Retail - Multiple Positions Birmingh...
Job Description: Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, Read more
SW Engineer *Apple* TV - Apple Inc. (United...
The Apple TV team is looking for excellent software engineers with experience in hardware, media management, media playback, content delivery and a passion for Read more
*Apple* Retail - Multiple Positions Victor,...
Job Description: Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, Read more
*Apple* /Mac Support Engineer - GFI Digital,...
FI Digital, Inc. is currently seeking candidates for a full time Apple Support Engineer to add to our Maryland Heights, Missouri IT team. Candidates must be dynamic Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.