TweetFollow Us on Twitter

Scripts Menu
Volume Number:12
Issue Number:2
Column Tag:Open Scripting Architecture

Attaching a Scripts Menu

An introduction to using the OSA in PowerPlant

By Jeremy Roschelle

Note: Source code files accompanying article are located on MacTech CD-ROM or source code disks.

A fully AppleEvent-savvy application is scriptable, recordable, and attachable. In a scriptable application, any user can automate tasks, interconnect applications, and extend the capabilities of your application. A recordable application generates a script by observing the user’s actions. Yet these capabilities prove worthless if users have no easy way to execute their scripts. Unfortunately, Apple did not provide any standard human interface for attaching scripts to an application. And although the PowerPlant framework provides excellent support for scripting and recording, it provides no recipes for customizing your application to launch scripts. This article addresses these issues with a simple customizable Script menu which allows users to execute scripts (see Figure 1).

At first, implementing a script menu looks complex: it requires interacting with PowerPlant, the Menu Manager, the File Manager, the Open Scripting Architecture (OSA), and the scriptable Finder. On the bright side, PowerPlant and the OSA provide excellent modular, easy-to-use interfaces [see Jeremy’s article, “Powering Up AppleEvents in PowerPlant”, MacTech Magazine, 11:6 (1994) 33-46 - man]. In this article, I’ll present an implementation of an extension to the PowerPlant framework that that can compile and execute scripts from a standard pull-down menu. This script menu provides a relatively complete implementation of attachable scripts: it loads scripts at launch time from a “script menu items” folder, automatically supplies Balloon Help for each menu item, and can open a script for editing in the Script Editor. This article will also show you how easy it is to use OSA to compile and execute scripts.

Figure 1. The Script menu

The implementation also strives to use the capabilities of PowerPlant, C++, and AppleEvents to achieve modularity and encapsulation. For example, we use the LAttachment mechanism to encapsulate all the code for handling the script menu into a re-usable class. Likewise, we introduce a C++ iterator class for scanning through items in a folder. Finally, we use AppleEvents to connect our script menu to external applications that provide script editing services, thus avoiding the need to embed a script editor ourselves.

OSA Basics

From the user’s point of view, a script is a small program written in AppleScript or another OSA dialect. From the programmer’s point of view, a script is a data type containing code that the OSA can execute. The process of compiling a script reconciles these views: compiling converts AppleScript statements into an executable data type.

As a programmer, you manipulate a script as a Handle to some script data. The easiest way to get such a handle is to get the 'scpt' resource out of a ScriptEditor document. There is one 'scpt' resource in each ScriptEditor document, storing the script compiled by the user.

To run a script, you first load the script data into OSA. This results in an OSAID, a token that refers to the loaded script. The process of loading is relatively slow (a second or two on my Quadra 660AV). Once a script is loaded, running it is fast (small scripts seem as fast as hard-coded commands in my application). To execute the script, you pass the OSAID to the OSAExecute function. When you are through with a script (or any value returned by OSA), you dispose of its OSAID to free up the associated memory.

To hide the ugly details, I wrapped my code in a utility class with static methods:

UScripting::Initialize
void UScripting::Initialize()
{ 
    // sComponent is a static class member of type ComponentInstance             
 if (sComponent == nil)
 SetComponent(
 ::OpenDefaultComponent(kOSAComponentType, 'scpt');
} 

UScripting::LoadScript

OSErr UScripting::LoadScript(
 Handle inScript, 
 OSAID  &outScriptID)
{ 
 Initialize();

 AEDesc scriptDesc;
 scriptDesc.descriptorType = typeOSAGenericStorage;
 scriptDesc.dataHandle = inScript;
 return ::OSALoad(sComponent,
 &inScriptDesc,
 kOSAModeNull,
 &outScriptID);
} 

UScripting:: ExecuteScript

OSErr UScripting::ExecuteScript(
 OSAID inScriptID)
{ 
 Initialize();

 OSAID  resultID;
 OSErr  err;
 
 err = ::OSAExecute(sComponent,
 inScriptID,
 kOSANullScript,
 kOSAModeNull, &resultID);
 if (err) 
 return err;
 else ::OSADispose(sComponent, resultID);
 return noErr;
} 

Design Overview

The main challenge in designing a script menu is maintaining a correspondence between items in a Menu Manager menu and script data that we can execute. This script data (an FSSpec for a script file and an OSAID for an executable script) will be encapsulated by a class called SCScriptMenuItem. Because scripts will be added to the menu dynamically, we cannot specify the menu items ahead of time in our resource file and cannot use PowerPlant’s 'Mcmd' scheme for binding each menu item to a command number. Instead, the implementation builds a list of SCScriptMenuItems, where the index of the item in the list matches the index of the item in the menu.

Our application must use this correspondence to respond when the user selects an item from the Script menu. We could do this by overriding LApplication methods that handle menu commands. But PowerPlant’s LAttachment class provides a better solution. It allows the code to be completely encapsulated in a class, SCScriptMenuHandler. This class can be attached to any PowerPlant application with one AddAttachment call. (Such modularity and portability can be dangerous - your employer may come to expect it regularly!)

Your application will normally create one SCScriptMenuHandler at launch time. When created, this object will iterate through the designated folder and create one MacOS menu item and a corresponding SCScriptMenuItem for each script in the folder. When a user selects a script from the menu for execution or editing, the SCScriptMenuHandler calls the appropriate method of the corresponding SCScriptMenuItem.

The article covers the implementation starting from the basic structure of SCScriptMenuHandler and SCScriptMenuItem. Next, the article describes how to create Balloon Help for each script automatically. Finally, the article reviews the utility routines for interacting with the File Manager.

Creating the Script Menu

Like every menu, the Script menu requires a 'MENU' resource, a 'hmnu' resource for Balloon Help, and a reference to the correct ID in your 'MBAR' resource. The 'MENU' and 'hmnu' resources contain the fixed portion of the script menus: the menu title and a final menu item that allows the user to add a script to the menu while your application is running. (This additional feature is supported in the sample code, but not discussed in this article.) At run-time, we add additional menu and help items for each script.

In your application, you create a handler for this script menu, normally within the constructor for your application class. When creating the handler, you provide the resource id for the script menu, and the vRefNum and dirID for the folder from which you wish to load scripts.

YourApp constructor
YourApp::YourApp()
{ 
    // get folder id and volume number for the Scripts Folder, relative to launch spec
 FSSpec appSpec;
 long   folderID;
 
 UFinder::GetAppSpec(appSpec);
 folderID = UFinder::GetFolderID(
 appSpec, "\pScript Menu Items");
 
    // attach a new handler for the scripts menu
 AddAttachment(
 new SCScriptsMenuHandler(kScriptsMenuID,
 appSpec.vRefNum,
 folderID));
} 

When SCScriptsMenuHandler is constructed, it iterates through a folder, appending a script menu item for each script file it finds. To hide the ugly details of iterating through a folder, the implementation uses an iteration class, StFolderIterator.

SCScriptsMenuHandler constructor

SCScriptsMenuHandler::SCScriptsMenuHandler(
 ResIDT inMenuID,
 short  inVRefNum, 
 long   inParID,
 Int16  inMax) 
 : LAttachment(msg_AnyMessage, true), mMenuID(inMenuID)
{ 
    // appends menu items for each script in the designated folder
 if (inVRefNum != 0) {    
    // set up iteration structs
 Int16  count = 0;
 Str255 scriptFileName;
 HFileParam fInfo;
 fInfo.ioNamePtr = scriptFileName;
 
    // iterate through each item in the folder, inserting scripts
 StFolderIteratoriter(inVRefNum, inParID);
 while ((++count <= inMax) && iter.Next(fInfo)) { 
 if (fInfo.ioFlFndrInfo.fdType == kOSAFileType) { 
 FSSpec spec;
 FSMakeFSSpec(
 inVRefNum, inParID, scriptFileName, &spec);
 AppendScript(spec);
 } 
 } 
 } 
} 

To append each script, we first grab the menu. Then we insert an item into the menu, using the file name as the menu item name. To handle each menu item, we build a SCScriptMenuItem and insert it in the mScripts list, such that index numbers of the MacOS menu item and the SCScriptMenuItem correspond. Finally, we construct Balloon Help (as described later).

SCScriptsMenuHandler::AppendScript

void SCScriptsMenuHandler::AppendScript(
 FSSpec &inScriptFile)
{ 
 MenuHandle menu = ::GetMenu(mMenuID);
 if (! menu) return;
 
 SCScriptsMenuItem *item = 
 new SCScriptsMenuItem(inScriptFile);
 
    // insert into the menu
 ::InsMenuItem(
 menu, inScriptFile.name, mScripts.GetCount());
 
    // insert the corresponding class instance into the list
 mScripts.InsertItemsAt(1, arrayIndex_Last, &item);
 
    // insert balloon help into resource
 AttachBalloonHelp(inScriptFile, mScripts.GetCount());
} 

Running a Script

As described earlier, running a script in the OSA requires two simple steps. First you load the script, resulting in token called an OSAID that represents the executable. Then you pass the token to the OSA execute function.

Running scripts from a menu is only slightly more complicated. The AppendScript procedure created a SCScriptMenuItem for each menu item, storing the FSSpec of a script file. To compile a script, we need to extract the 'scpt' resource from this file and pass it to OSALoad to get an OSAID. Because loaded scripts execute much faster, we load the script and store the OSAID to service future requests to run the same script.

SCScriptsMenuHandler:: RunScript

OSErr SCScriptsMenuItem::RunScript()
{ 
 OSErr  err = noErr;
    // load the script if its not available yet
 if (mScriptID == kOSANullScript) { 
 Handle script = nil, text = nil;
 short  fRefNum = -1;
 
 Try_ { 
    // open resource fork
 fRefNum = ::FSpOpenResFile(&mFileSpec, fsRdPerm);
 ThrowIfResError_();
    // get the first script resource in the file
 script = ::Get1IndResource('scpt', 1); 
 FailNIL_(script);
    // Load it
 UScripting::LoadScript(script, mScriptID);
 } 
 Catch_(catchErr) { 
 err = catchErr;
 SysBeep(0);
 } 
 EndCatch_
 if (fRefNum != -1) ::CloseResFile(fRefNum);
 } 
 if (err == noErr) new URun1Script(mScriptID);
 return err;
} 

Testing reveals one additional complication. If the script brings a different application to the front while you are still handling a menu selection, a menubar drawing glitch occurs. To solve this problem, we create a LPeriodical task that runs immediately after the menu event completes (and the MacOS has removed the menu hiliting). URun1Script simply executes a loaded script with a given OSAID and then deletes itself.

URun1Script constructor

URun1Script::URun1Script(OSAID inScriptID) 
 : mScriptID(inScriptID) 
{ 
 StartRepeating();
} 
URun1Script::SpendTime
void URun1Script::SpendTime(
 const EventRecord &inMacEvent)
{ 
 UScripting::ExecuteScript(mScriptID);
 delete this;
} 

Handling The Menu Selection

Handling menu selection in an LAttachment is a matter of overriding ExecuteSelf. When the user selects the menu item, PowerPlant will generate a negative command number (because the menu has no 'Mcmd' resource). The menu id will be in the HiWord, and the item number in the LoWord.

Our handler must respond both to this command and to a command status message that enables the menu item. Since scripts are always available, we enable all menu items in the script menu. To respond to the command, we find the corresponding SCScriptMenuItem. Normally we run the script. However, if the command key is down we open it for editing. The methods for running a script were described above; the next section explains how to open a script.

SCScriptsMenuHandler::ExecuteSelf

void SCScriptsMenuHandler::ExecuteSelf(
 MessageT inMessage, 
 void   *ioParam)
{ 
 mExecuteHost = true;
    // update status
 if (inMessage == msg_CommandStatus) { 
 SCommandStatus  *status = (SCommandStatus *)ioParam;
 if (HiWord(- status->command) == mMenuID) { 
 *status->enabled = true;
 *status->usesMark = false;
 mExecuteHost = false; // we handled it
 } 
 } 
    // handle menu comand 
 else if (HiWord(-inMessage) == mMenuID) { 
 Int16  index = LoWord(-inMessage);
 SCScriptsMenuItem *item;
 if (mScripts.FetchItemAt(index, &item)) { 
 if (cmdKey & UEventUtils::GetModifiers())
 item->OpenScript(); // open on command key
 else item->RunScript();
 mExecuteHost = false; // we handled it
 } 
 } 
 } 
} 

Editing a Script, the AppleEvent Way

Providing support for users to edit scripts is not hard. OSA provides calls that get the text and style record for a script, which you can display in an LTextEdit pane. When the user finishes her changes, you can use OSA calls to compile the script, and then execute it. But there is an easier way: the ScriptEditor already provides full script editing capabilities. By sending an AppleEvent, we can open a file in ScriptEditor and let it handle editing.

Since we already have an FSSpec for each script in our menu, this is easy. Our SCScriptMenuItem method for opening a script calls a utility method to send the Finder an “open” event with the FSSpec. Before doing so, we dispose of the token that represents the loaded script. By doing this, we will force our RunScript method to re-load the script from the file. Thus, when the user edits and then saves the script, her next attempt to run it will load and execute the modified version.

SCScriptsMenuItem::OpenScript
OSErr SCScriptsMenuItem::OpenScript()
{ 
 if (mScriptID != kOSANullScript) { 
    // first unload script from OSA
 UScripting::DisposeScript(mScriptID))
 mScriptID = kOSANullScript;
 } 
 return UFinder::SendFinderAEOpen(mFileSpec);
} 

We could send an “open” event to ScriptEditor, but instead we send it to the scriptable Finder. The Finder will open the correct script editing application based on the creator of the file.

Sending an AppleEvent is not hard. The first step is to create a descriptor for the target of the event, in this case the Finder. The easiest type of process descriptor just uses the application signature. The second step is to create an AppleEvent with this process descriptor. The third step is to add any parameters to the event. In this case there is just one, the FSSpec. Finally we send the event and dispose of the reply.

The implementation uses exceptions to handle an error at any stage of the process, but it catches all errors, disposes of the memory in AEDescs and returns the error code.

UFinder::SendFinderAEOpen
OSErr UFinder::SendFinderAEOpen(
 FSSpec &inFile)
{ 
 OSErr  err = noErr;
 AEDesc processDesc;
 AppleEvent ae, aeReply;
 ae.descriptorType = 
 aeReply.descriptorType = 
 processDesc.descriptorType = typeNull;
 ae.dataHandle = 
 aeReply.dataHandle = 
 processDesc.dataHandle = 
 nil;
 
 Try_ { 
 DescType finderType = 'MACS';
 err = ::AECreateDesc(
 typeApplSignature,
 &finderType,
 sizeof(DescType),
 &processDesc);
 FailOSErr_(err);
 
 err  = ::AECreateAppleEvent(
 kCoreEventClass, 
 kAEOpen,
 &processDesc,
 kAutoGenerateReturnID,
 kAnyTransactionID,
 &ae);
 FailOSErr_(err);
 
 err = ::AEPutParamPtr(
 &ae,
 keyDirectObject,
 typeFSS,
 &inFile,
 sizeof(inFile));
 FailOSErr_(err);
 err = ::AESend(
 &ae,
 &aeReply,
 kAENoReply | kAENeverInteract, 
 kAENormalPriority,
 kAEDefaultTimeout,
 nil,
 nil);
 FailOSErr_(err);
 } 
 Catch_(catchErr) { err = catchErr;} EndCatch_
 
 if (processDesc.descriptorType != typeNull)
 ::AEDisposeDesc(&processDesc);
 if (ae.descriptorType != typeNull) 
 ::AEDisposeDesc(&ae);
 if (aeReply.descriptorType != typeNull) 
 ::AEDisposeDesc(&aeReply);
 return err;
} 

Writing Balloons Without Typing

As a final touch, it’s nice to provide Balloon Help for all menu items. But scripts are loaded at run time, so there’s no way to know in advance what scripts will be present. Yet there is a way to automatically create sensible help text for each script at run time. Here’s how.

When a user creates a script in ScriptEditor, the user can write an English description of the script in the area just below the window title. This description ends up in a 'TEXT' resource in the script file. The script menu can grab this text from the file, truncate it to 255 characters, and install it as Balloon Help for the menu item. Thus, the Script Editor description field becomes the Balloon Help automatically.

Here is the top-level routine that is called when the SCScriptsMenuHandler is constructed.

SCScriptsMenuHandler::AttachBalloonHelp

void SCScriptsMenuHandler::AttachBalloonHelp(
 FSSpec &inScriptFile, 
 Int16  inIndex)
{ 
 Str255 text;
 { 
    // get the text
 Int16  fRefNum = 
 ::FSpOpenResFile(&inScriptFile, fsRdPerm);
 if (ResError()) return;
 
    // the first text resource has the description of the script
 Handle outText = ::Get1IndResource('TEXT', 1);
 if (outText)
 UFinder::Handle2PStr(outText, text);
 else *text = 0;
  
 ::CloseResFile(fRefNum);
 }
  
 {
    // add the help
 char   buffer[500];
 MakeBalloonData(text, buffer);
 InsertBalloonData(inIndex, buffer);
 } 
} 

Once we have extracted the description text, the process of installing it is divided into 2 steps. First we construct a buffer containing a single entry for the 'hmnu' resource. Each entry begins with a size word for the size of the entry, and then a flag word indicating the type of the entry. We only deal with two kinds of entries, a “skip” entry for empty balloons, and a direct string entry. A direct string entry has 4 packed Pascal strings. The routine below writes an entry in this format, implementing the writes as if writing to a stream.

SCScriptsMenuHandler::MakeBalloonData

void SCScriptsMenuHandler::MakeBalloonData(
 Str255 inHelp,
 char   *ioBuffer)
{ 
 Int16  mark, data;
 Int32  zeros = 0;

    // leave room to write number of bytes to end
 mark = 2; 
 
 if (*inHelp == 0) {  
    // no data, so skip this item
 data = 0x0100;
 ::BlockMoveData(&data, ioBuffer[mark], sizeof(Int16));
 mark += sizeof(Int16);
 } 
 else { 
 data = 0x0001; // direct string type
 ::BlockMoveData(&data, &ioBuffer[mark], sizeof(Int16));
 mark += sizeof(Int16);
 
    // write out the string
 ::BlockMoveData(inHelp, &ioBuffer[mark], 1 + *inHelp);
 mark += 1 + *inHelp;
 
    // write out three zeros for the other strings
 ::BlockMoveData(&zeros, &ioBuffer[mark], 3);
 mark += 3;
 } 
 
    // align buffer to an even word boundary
 if (mark & 0x0001) ++mark;
 
    // add size to first word of buffer
 ::BlockMoveData(&mark, ioBuffer, sizeof(mark));
} 

Balloon data for a menu is packed into a single Handle. In order to insert an entry for a new menu item, we need to increment the count word, and then insert the entry in the right place. To find the right place we have to read the size of each preceding entry, and skip over that many bytes to arrive at the next entry. Once we find the right place, remaining entries are moved out of the way, and the new entry is copied into place.

SCScriptsMenuHandler:: InsertBalloonData

void SCScriptsMenuHandler::InsertBalloonData(
 Int16  inIndex, 
 char   *inBuffer)
{ 
 Handle hmnu = ::Get1Resource('hmnu', mMenuID);
 if (! hmnu) return;
 
 Int16  len = *(short *)inBuffer;
 
    // make some room in the handle
 ::SetHandleSize(hmnu, ::GetHandleSize(hmnu) + len);
 if (::MemError()) return;
 
    // lock it down so we can safely dereference it
 StHandleLocker  lock(hmnu);
 char   *help = *hmnu;
 
    // increment number of items
 ++*(short *)(help + 0x0A); // @ help + 0x0A
 
    // skip over existing items
 { 
    // skip default and title resource, don’t skip self
 Int16  itemsToSkip = inIndex + 2 - 1;
 help += 0x0C; // location of first msg record
 do { 
 help += *(Int16 *)help;  // add the number of bytes to skip
 } while (--itemsToSkip);
 } 
 
    // shift data out of the way
 { 
 char  *dest, *end;
 dest = help + len;
 end = ((char *)*hmnu + ::GetHandleSize(hmnu));
 ::BlockMoveData(help, dest, end - dest);
 } 

    // copy help data in
 ::BlockMoveData(inBuffer, help, len);
} 

Note that the implementation does not call ChangedResource, even though it did change the resource. This is because the resource is in the application, and calling ChangedResource would cause the application to store the Balloon data when it quit. We don’t want this data stored; it is re-computed every time the application is launched. We also don’t call ReleaseResource, so the changed resource will stay in memory for the duration of the session.

Finder Utilities

The implementation made use of a few Finder utilities: (a) for finding the FSSpec of the running application; (b) for finding a folder id, given a parent folder and a folder name; (c) for iterating through all the items in a folder. These are fairly common steps in many applications, but the techniques are not easy to find in standard Macintosh references. For the sake of completeness, the routines are presented below:

To find the FSSpec of the running application, you call the process manager, requesting information about the current process.

UFinder::GetAppSpec
UFinder::GetAppSpec(
 FSSpec &inSpec)
{ 
 ProcessSerialNumber psn;
 ProcessInfoRec  info;
 info.processAppSpec = &inSpec;
 info.processInfoLength = sizeof(info);
 info.processName = nil;
 ::GetCurrentProcess(&psn);
 ::GetProcessInformation(&psn, &info); 
} 

We find the folder of scripts by finding the folder that the application was launched from, and then looking for an enclosed folder named Script Menu Items. The routine below finds an enclosed folder id, given a parent folder and a name:

UFinder::GetFolderID
long UFinder::GetFolderID(
 FSSpec &inParentFolder, 
 Str255 inName)
{  
 CInfoPBRec pb;  
 DirInfo*dpb = (DirInfo *)&pb;
 OSErr  err;

 dpb->ioNamePtr = inName ;
 dpb->ioVRefNum = inParentFolder.vRefNum;
 dpb->ioDrDirID = inParentFolder.parID;
 dpb->ioFDirIndex = 0;
 err = PBGetCatInfo(&pb, false);

    // make sure its a folder
 if (err == noErr && dpb->ioFlAttrib & ( 1 << 4)) 
 return dpb->ioDrDirID;
 else return 0;
} 

The recipe for iterating through each item in a folder is really ugly. The class below encapsulates the details in an iterator:

StFolderIterator constructor
StFolderIterator::StFolderIterator(
 short inVRefNum, long inFolderID)
 : mVRefNum(inVRefNum), mFolderID(inFolderID), mIndex(0)
{ 
} 


StFolderIterator:: Next
Boolean StFolderIterator::Next(
 HFileParam &ioRec)
{ 
 ioRec.ioVRefNum = mVRefNum;
 ioRec.ioDirID = mFolderID;
    // reset name field
 if (ioRec.ioNamePtr) ioRec.ioNamePtr[0] = 0; 
 ioRec.ioFDirIndex = ++mIndex;
 ioRec.ioResult = noErr;
 
 PBHGetFInfo((HParmBlkPtr)&ioRec, false);
 return (ioRec.ioResult == noErr);
} 

Conclusion

Scripting adds very powerful capabilities to your application. The script menu makes it easy for users to attach scripts to a menu in your application. And the code is encapsulated in an LAttachment.

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Together 3.7 - Store and organize all of...
Together helps you organize your Mac, giving you the ability to store, edit and preview your files in a single clean, uncluttered interface. Features Smart storage. With simple drag-and-drop... Read more
EtreCheck 3.1.4 - For troubleshooting yo...
EtreCheck is an app that displays the important details of your system configuration and allow you to copy that information to the Clipboard. It is meant to be used with Apple Support Communities to... Read more
Postbox 5.0.9 - Powerful and flexible em...
Postbox is a new email application that helps you organize your work life and get stuff done. It has all the elegance and simplicity of Apple Mail, but with more power and flexibility to manage even... Read more
DiskCatalogMaker 6.5.16 - Catalog your d...
DiskCatalogMaker is a simple disk management tool which catalogs disks. Simple, light-weight, and fast. Finder-like intuitive look and feel. Super-fast search algorithm. Can compress catalog data... Read more
TrailRunner 3.8.827 - Route planning for...
TrailRunner is the perfect companion for runners, bikers, hikers, and all people wandering under the sky. Plan routes on a geographical map. Import GPS or workout recordings and journalize your... Read more
VueScan 9.5.61 - Scanner software with a...
VueScan is a scanning program that works with most high-quality flatbed and film scanners to produce scans that have excellent color fidelity and color balance. VueScan is easy to use, and has... Read more
Postbox 5.0.9 - Powerful and flexible em...
Postbox is a new email application that helps you organize your work life and get stuff done. It has all the elegance and simplicity of Apple Mail, but with more power and flexibility to manage even... Read more
VueScan 9.5.61 - Scanner software with a...
VueScan is a scanning program that works with most high-quality flatbed and film scanners to produce scans that have excellent color fidelity and color balance. VueScan is easy to use, and has... Read more
DiskCatalogMaker 6.5.16 - Catalog your d...
DiskCatalogMaker is a simple disk management tool which catalogs disks. Simple, light-weight, and fast. Finder-like intuitive look and feel. Super-fast search algorithm. Can compress catalog data... Read more
TrailRunner 3.8.827 - Route planning for...
TrailRunner is the perfect companion for runners, bikers, hikers, and all people wandering under the sky. Plan routes on a geographical map. Import GPS or workout recordings and journalize your... Read more

Latest Forum Discussions

See All

Great zombie games in the spirit of Dead...
Dead Rising 4 arrives tomorrow, giving enthusiasts a fresh chance to take selfies with zombies and get up to other ridiculous end-of-the-world shenanigans. To really get into the spirit of things, we've gone and gathered the best zombie games that... | Read more »
Amateur Surgeon 4 Guide: Advanced tips a...
Amateur Surgeon 4 is still tackling the competition at the top of the App Store charts, so if you haven't tried it out yet, you should probably do that right away. If you've been at it for a while, though, perhaps you're ready to start expanding... | Read more »
Amateur Surgeon 4 Guide: Become the worl...
It's time to wield your trusty pizza cutter again, as Amateur Surgeon has returned with a whole fresh set of challenges (and some old, familiar ones, too). Starting anew isn't easy, especially when all you have at your disposal is a lighter, the... | Read more »
Le Parker: Sous Chef Extraordinaire (Ga...
Le Parker: Sous Chef Extraordinaire 1.0 Device: iOS Universal Category: Games Price: $2.99, Version: 1.0 (iTunes) Description: | Read more »
Telltale Games really is working on a Gu...
Telltale Games' next episodic adventure is indeed Guardians of the Galaxy. A document tied to the voice actors strike suggested that the project was in the work, but now we have direct confirmation following an announcement at the Game Awards that... | Read more »
Amateur Surgeon returns to iOS and Andro...
Amateur Surgeon and its two sequels disappeared from the App Store some time and it was sad days for all. But now, just in time for the holidays, the Adult Swim favorite makes its joyous return in the shape of Amateur Surgeon 4, a remake with... | Read more »
The best board games on mobile
Sometimes you need to ditch all of the high speed, high action games in favor of something a little more traditional. If you don't feel like parting ways from your mobile device, though, there are still plenty of ways to get that old-school fix.... | Read more »
The best Facebook Messenger Instant Game...
Facebook's new Instant Games is now here, meaning you can play games with your friends directly via Facebook. It's a fun new way to connect with friends, of course, but it's also proving to be a solid gaming experience in its own right, with a... | Read more »
You can now play game's on Facebook...
Facebook launched its new Instant Games platform in an exciting new attempt to engage its user base. As a result, you can now play a number of different games directly through Facebook Messenger. All of these games run with HTML5, meaning you play... | Read more »
Apollo Justice Ace Attorney (Games)
Apollo Justice Ace Attorney 1.00.00 Device: iOS Universal Category: Games Price: $.99, Version: 1.00.00 (iTunes) Description: Court Is Back In Session Star as rookie defense attorney, Apollo Justice, as he visits crime scenes,... | Read more »

Price Scanner via MacPrices.net

Monday roundup of Holiday Mac sales: Up to $3...
Take up to $300 off MSRP on the price of a new Apple Mac at B&H Photo today as part of their Holiday sale. Shipping is free, and B&H charges NY sales tax only. Touch Bar MacBook Pros are in... Read more
12-inch WiFi Apple iPad Pros on sale for up t...
B&H Photo has 12″ WiFi Apple iPad Pros on sale for up to $50 off MSRP, each including free shipping. B&H charges sales tax in NY only: - 12″ Space Gray 32GB WiFi iPad Pro: $749 $50 off MSRP... Read more
9-inch Apple WiFi iPad Pros on sale for $20-$...
B&H Photo has 9.7″ Apple WiFi iPad Pros on sale for $20-$50 off MSRP, each including free shipping. B&H charges sales tax in NY only: - 9″ Space Gray 256GB WiFi iPad Pro: $779.95 $20 off MSRP... Read more
Holiday sale: Apple MacBook Airs available fo...
B&H Photo has 13″ MacBook Airs on sale for $100 off MSRP. Shipping is free, and B&H charges NY sales tax only: - 13″ 1.6GHz/128GB MacBook Air (MMGF2LL/A): $899 $100 off MSRP - 13″ 1.6GHz/... Read more
13-inch Silver Touch Bar MacBook Pro in stock...
Amazon has the new 2016 13″ 2.9GHz/256GB Silver Touch Bar MacBook Pro (MLVP2LL/A) in stock today and on sale for $1749 including free shipping. That’s $50 off MSRP, and it’s the lowest price... Read more
Parallels Toolbox 1.3 for Mac Offers 25 Singl...
Parallels has launched Parallels Toolbox 1.3 for Mac, an upgrade that adds five new utilities to the stand-alone application which was released in August and is available exclusively online at http... Read more
OWC Mercury Elite Pro Dual mini Ultra-Portabl...
OWC has introduced the new OWC Mercury Elite Pro Dual mini, a powerful yet ultra-portable dual-drive RAID solution. The new Mercury Elite Pro Dual mini packs phenomenal performance into a small... Read more
Clearance 13-inch Retina MacBook Pros availab...
B&H Photo has clearance 2015 13″ Retina Apple MacBook Pros available for up to $200 off original MSRP. Shipping is free, and B&H charges NY tax only: - 13″ 2.7GHz/128GB Retina MacBook Pro: $... Read more
Roundup of 2016 13-inch 2.0GHz MacBook Pro sa...
B&H has the non-Touch Bar 13″ MacBook Pros in stock today for $50-$100 off MSRP. Shipping is free, and B&H charges NY sales tax only: - 13″ 2.0GHz MacBook Pro Space Gray (MLL42LL/A): $1449 $... Read more
New 13-inch 2.0GHz Space Gray MacBook Pro in...
Adorama has the new 13″ 2.0GHz Space Gray MacBook Pro (non-Touch Bar, MLL42LL/A) in stock for $1499 including a free 3-year AppleCare Protection Plan. Shipping is free, and Adorama charges sales tax... Read more

Jobs Board

Lead *Apple* Solutions Consultant - Apple (...
# Lead Apple Solutions Consultant Job Number: 53586123 Pittsburgh, Pennsylvania, United States Posted: Nov. 28, 2016 Weekly Hours: 40.00 **Job Summary** The Lead ASC Read more
*Apple* Retail - Multiple Positions- Plano,...
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* Retail - Multiple Positions- Kansas...
Job Description:SalesSpecialist - Retail Customer Service and SalesTransform Apple Store visitors into loyal Apple customers. When customers enter the store, Read more
*Apple* Retail - Multiple Positions- Chicago...
Job Description: Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, Read more
Hardware Design Validation Engineer - *Apple...
The Apple Watch team is looking for a Hardware Design Validation Engineer. This person will be part of the Apple Watch hardware team with responsibilities for Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.