TweetFollow Us on Twitter

Children of the Revolution

Volume Number: 19 (2003)
Issue Number: 10
Column Tag: Programming

QuickTime Toolkit

Children of the Revolution

by Tim Monroe

Editing QuickTime Movies with Revolution

Introduction

In the previous QuickTime Toolkit article ("Revolution" in MacTech, September 2003), we took a first look at Revolution, a rapid application development tool published by Runtime Revolution Ltd. We saw how to create a new application -- which we called RunRevVeez -- that can open and display QuickTime movies. We saw how to set things up so that the user can have several movies open at once, and we saw how to use a few of the built-in Revolution commands to modify the appearance of a movie player object at runtime. In terms of movie playback, RunRevVeez is just about complete.

The situation with movie editing is somewhat different, however. As I mentioned last time, Revolution has no built-in support for editing QuickTime movies. In addition (as far as I can tell), it provides no support for tracking changes to a window or document, and it provides no way to save an edited movie. We'd certainly like our application to be able to handle these tasks, so we'll have to go beyond the built-in capabilities of Revolution. We need to write a Revolution plug-in.

Happily, Runtime Revolution provides a software development kit (SDK) for writing Revolution plug-ins, and this makes writing our plug-in a snap. With just a few dozen lines of new C code and a handful of routines borrowed from our existing C-based sample application QTShell, we'll be able to handle all the basic editing operations, keep track of the modification state of a movie window, and save edited movies into new files.

Unhappily, even with this plug-in, there are a few things we won't be able to accomplish with Revolution. The Revolution runtime engine opens QuickTime movie files with read-only permission, which effectively prevents us from saving any changes to a movie into the file we opened the movie from. We will be able to write an edited movie into a new file. (In a nutshell, we'll be able to implement the "Save As" menu item but not the Save menu item.) Also, the Revolution runtime engine installs a movie controller action filter procedure, which effectively prevents us from installing our own procedure. This restricts our ability to access many important QuickTime capabilities. (You may recall that REALbasic currently has this same limitation; see "Basic Instinct" in MacTech, February, 2003.)

In this article, we'll continue our development of RunRevVeez. We'll implement the editing operations on a movie, which requires us to develop a plug-in and then to call the plug-in from within our scripts. We'll look at the file-handling operations (principally, "Save As" and Close) in the next article.

One final note before we begin: Runtime Revolution has recently released Revolution version 2.1. In these articles, I've used version 2.0.2. I would assume that the plug-in and Revolution project will work unchanged under 2.1, but I have not actually verified that.

Revolution Plug-Ins

The Revolution runtime engine is based largely on an existing product called MetaCard, which was introduced in 1990 as a competitor to Apple's HyperCard. Not surprisingly, the plug-in architecture used by MetaCard, and hence Revolution, is identical to that introduced by HyperCard. HyperCard can be extended by adding modules of commands and functions called externals. A set of external commands is called an XCMD and a set of external functions is called an XFCN.

Originally, XCMDs and XFCNs were packaged as executable code resources that were added to the resource fork of the application or to the resource fork of a stack. MetaCard and Revolution followed this example through Revolution version 1.1.1. In version 2.0 and later, the packaging of externals was changed; in current versions, externals on Mac OS X are packaged as bundles, which can be copied into the application bundle.

The packaging actually doesn't really matter all that much, since it will be taken care of by the project files provided with the plug-in SDK. The current SDK provides project files for both CodeWarrior and Project Builder. In this article, we'll work with the Project Builder version, whose project window is shown in Figure 1. (Notice that I've renamed the project as "QTExternal".) We'll need to modify only one file here, external.c. The file XCmdGlue.c contains a number of support routines for the external; we won't need to call any of those routines.


Figure 1: The Project Builder project

Connecting to the Runtime Engine

Our Revolution external will define a number of procedures and functions that can be called by RunRevVeez scripts. To expose those routines to the runtime engine, we need to declare two global variables, Xname and Xtable. The Xname variable specifies the name of the external:

char   Xname[] = "QuickTime Revolution External";

The Xtable variable contains an array of procedure specifiers. Each entry in the array specifies information about a single external function or command. Here's our array:

Xternal Xtable[] = {
   {"mcInitialize", XCOMMAND, 0, XCMD_MCInitialize, 
                                                            XCMD_Abort},
   {"mcUndo", XFUNCTION, 0, XCMD_MCUndo, XCMD_Abort},
   {"mcCut", XFUNCTION, 0, XCMD_MCCut, XCMD_Abort},
   {"mcCopy", XCOMMAND, 0, XCMD_MCCopy, XCMD_Abort},
   {"mcPaste", XFUNCTION, 0, XCMD_MCPaste, XCMD_Abort},
   {"mcClear", XFUNCTION, 0, XCMD_MCClear, XCMD_Abort},
   {"selectAll", XCOMMAND, 0, XCMD_SelectAll, XCMD_Abort},
   {"selectNone", XCOMMAND, 0, XCMD_SelectNone, 
                                                            XCMD_Abort},
   
   {"mcEnableEditMenuItem", XFUNCTION, 0, 
                           XCMD_MCEnableEditMenuItem, XCMD_Abort},
   
   {"windowSetModified", XFUNCTION, 0, 
                        XCMD_SetWindowModified, XCMD_Abort},
   {"saveAs", XFUNCTION, 0, XCMD_SaveAs, XCMD_Abort},
   {"", XNONE, 0, NULL, NULL}
};

The first item in a procedure specifier is the name of the routine that we'll use in our scripts. The second item indicates the type of routine; it's XCOMMAND for commands (which do not return a value to the caller) and XFUNCTION for functions (which do return a value to the caller). The third entry is used by the runtime engine and should be set to 0 by our external. The fourth entry is the name of the corresponding C language routine in the external. (In other words, it's the routine that is called when our script executes the first item.) Finally, the fifth item is the name of an abort routine, which is called when the user cancels the execution of an external routine. All our external routines will use the same abort routine, shown in Listing 1.

Listing 1: Handling user cancellations

XCMD_Abort
void XCMD_Abort()
{
   DebugStr("\pQuickTime Revolution External abort");
}

Our abort routine just prints a diagnostic message on the standard error output.

Handling Commands

When a script calls the mcInitialize command (for instance), the external function XCMD_MCInitialize is executed; XCMD_MCInitialize has this declaration:

void XCMD_MCInitialize (char *args[], int nargs, 
            char **retstring, Bool *pass, Bool *error);

The first parameter passed to XCMD_MCInitialize is an array of C strings that specifies the parameters that were passed to the mcInitialize command. The second parameter specifies the number of items in that array. We'll call mcInitialize with only one parameter, like this:

put the movieControllerID of player "MoviePlayer" \ 
               of stack newStackName into mc
mcInitialize(mc)

The third parameter, retstring, is a pointer to a C string that contains the results of the external routine. For procedures, this is ignored by the runtime engine; for functions, this string is returned to the script as the command result. The buffer for this string must be allocated by the external and is disposed of by the runtime engine.

The fourth and fifth parameters are used to pass other information back to the runtime engine. The pass parameter indicates whether we want the command (in this case, mcInitialize) to be passed up the message hierarchy after it is executed. In general, we shall return false in this parameter. The error parameter indicates the success or failure of the external routine. Once again, we'll always pass back false, to indicate that no error occurred. (Errors may indeed occur within our external routines, but RunRevVeez will have no capability to work around errors; so there's little point in letting it know that something went wrong.)

Configuring the Movie Controller

So let's see how we can implement the handler for the mcInitialize command. As we've seen, the args parameter will contain a single C string, which is the movie controller identifier encoded as a string. To get a value of type MovieController, we need to convert the string to a long.

mc = (MovieController)atol(args[0]);

Once we've got the movie controller identifier, we can call any QuickTime APIs that operate on a movie controller. In RunRevVeez, we need to enable editing (by calling MCEnableEditing) and enable keyboard event handling (by calling MCDoAction with the mcActionSetKeysEnabled selector). Listing 2 shows our complete handler for the mcInitialize command.

Listing 2: Initializing the movie controller

XCMD_MCInitialize
void XCMD_MCInitialize (char *args[], int nargs, 
            char **retstring, Bool *pass, Bool *error)
{
   MovieController mc = NULL;
   ComponentResult result = noErr;
   char *retstr = NULL;
   
   // initialize the movie controller as desired
   *pass = false;
   *error = false;
   if (nargs == 1) {
      mc = (MovieController)atol(args[0]);
      
      if (mc != NULL) {
         // enable editing
         result = MCEnableEditing(mc, true);
         
         // enable keyboard event handling
         MCDoAction(mc, mcActionSetKeysEnabled, (void *)true);
         
         // disable drag support
         MCDoAction(mc, mcActionSetDragEnabled, 
                                                            (void *)false);
      }
   }
   
   retstr = calloc(1, 2);
   if (retstr != NULL)
      retstr[0] = (result == noErr) ? '0': '1';
    
   *retstring = retstr;
}

As indicated just above, we set both pass and error to false. And we pass back, via retstring, a C string of length 1 that contains either "0" or "1". RunRevVeez ignores that value.

Once we've successfully called mcInitialize, the thumb in the controller bar will change to reflect that editing is enabled (as seen in Figure 2).


Figure 2: A movie window with editing enabled

Handling Edit Operations

So, we've enabled movie controller editing. Now we need to handle the various editing operations. In these cases, we need to pass a value back to the caller, indicating whether the operation completed successfully. That's so RunRevVeez can know to set the movie window as modified and that the movie has changed since last opened or saved. We'll return the string "1" if the edit operation fails and "0" if it succeeds. Listing 3 shows how we'll handle the mcUndo command.

Listing 3: Undoing a movie edit

XCMD_MCUndo
void XCMD_MCUndo (char *args[], int nargs, 
            char **retstring, Bool *pass, Bool *error)
{
   MovieController mc = NULL;
   ComponentResult result = noErr;
   char *retstr = NULL;
   
   *pass = false;
   *error = false;
   if (nargs == 1) {
      mc = (MovieController)atol(args[0]);
      if (mc != NULL)
         result = MCUndo(mc);
   }
   
   retstr = calloc(1, 2);
   if (retstr != NULL)
      retstr[0] = (result == noErr) ? '0': '1';
    
   *retstring = retstr;
}

We simply retrieve the movie controller identifier and call MCUndo. Then we call calloc to allocate a 2-byte buffer, to hold the returned character and the null terminating byte.

The other editing operations are quite similar. Listing 4 shows how we handle the mcCut command, and Listing 5 shows how we handle the mcCopy command. Notice in both cases that we call PutMovieOnScrap to place the cut or copied movie segment onto the scrap.

Listing 4: Cutting a movie selection

XCMD_MCCut
void XCMD_MCCut (char *args[], int nargs, 
            char **retstring, Bool *pass, Bool *error)
{
   MovieController mc = NULL;
   ComponentResult result = noErr;
   Movie editmovie = NULL;
   char *retstr = NULL;
   
   *pass = false;
   *error = false;
   if (nargs == 1) {
      mc = (MovieController)atol(args[0]);
      if (mc != NULL) {
         editmovie = MCCut(mc);
         result = (editmovie != NULL) ? result: invalidMovie;
      }
   }
   
   // place the cut movie segment onto the scrap
   if (editmovie != NULL) {
      PutMovieOnScrap(editmovie, 0L);
      DisposeMovie(editmovie);
   }
   retstr = calloc(1, 2);
   if (retstr != NULL)
      retstr[0] = (result == noErr) ? '0': '1';
      
   *retstring = retstr;
}

Listing 5: Copying a movie selection

XCMD_MCCopy
void XCMD_MCCopy (char *args[], int nargs, 
            char **retstring, Bool *pass, Bool *error)
{
   MovieController mc = NULL;
   ComponentResult result = noErr;
   Movie editmovie = NULL;
   char *retstr = NULL;
   
   *pass = false;
   *error = false;
   if (nargs == 1) {
      mc = (MovieController)atol(args[0]);
   
      if (mc != NULL) {
         editmovie = MCCopy(mc);
         result = (editmovie != NULL) ? result: invalidMovie;
      }
   }
   
   // place the copied movie segment onto the scrap
   if (editmovie != NULL) {
      PutMovieOnScrap(editmovie, 0L);
      DisposeMovie(editmovie);
   }
   retstr = calloc(1, 2);
   if (retstr != NULL)
      retstr[0] = (result == noErr) ? '0': '1';
      
   *retstring = retstr;
}

Implementation of XCMD_MCPaste and XCMD_MCClear is left as an easy exercise for the reader. (The complete code for the QuickTime external is of course contained in the source code accompanying this article.)

Selecting All or None of a Movie

Our Edit menu contains two further items, "Select All" and "Select None", which are once again easy to implement. In earlier articles, we've seen how to handle these items by calling MCDoAction with the mcActionSetSelectionDuration selector. Listing 6 shows how our Revolution external handles the selectAll command, and Listing 7 shows how our Revolution external handles the selectNone command.

Listing 6: Selecting all of a movie

XCMD_SelectAll
void XCMD_SelectAll (char *args[], int nargs, 
            char **retstring, Bool *pass, Bool *error)
{
   MovieController mc = NULL;
   Movie mv = NULL;
   ComponentResult result = noErr;
   TimeRecord tr;
   char *retstr = NULL;
   
   *pass = false;
   *error = false;
   if (nargs == 1) {
      mc = (MovieController)atol(args[0]);
   
      if (mc != NULL) {
         mv = MCGetMovie(mc);
         if (mv) {
            tr.value.hi = 0;
            tr.value.lo = 0;
            tr.base = 0;
            tr.scale = GetMovieTimeScale(mv);   
            result = MCDoAction(mc, 
                              mcActionSetSelectionBegin, &tr);
            
            tr.value.hi = 0;
            tr.value.lo = GetMovieDuration(mv);   
            tr.base = 0;
            tr.scale = GetMovieTimeScale(mv);   
            result = MCDoAction(mc, 
                              mcActionSetSelectionDuration, &tr);
         }
      }
   }
   
   retstr = calloc(1, 2);
   if (retstr != NULL)
      retstr[0] = (result == noErr) ? '0': '1';
      
   *retstring = retstr;
}

Listing 7: Selecting none of a movie

XCMD_SelectNone
void XCMD_SelectNone (char *args[], int nargs, 
            char **retstring, Bool *pass, Bool *error)
{
   MovieController mc = NULL;
   Movie mv = NULL;
   ComponentResult result = noErr;
   TimeRecord tr;
   char *retstr = NULL;
   
   *pass = false;
   *error = false;
   if (nargs == 1) {
      mc = (MovieController)atol(args[0]);
   
      if (mc != NULL) {
         mv = MCGetMovie(mc);
         if (mv) {
            tr.value.hi = 0;
            tr.value.lo = 0;   
            tr.base = 0;
            tr.scale = GetMovieTimeScale(mv);   
            result = MCDoAction(mc, 
                              mcActionSetSelectionDuration, &tr);
         }
      }
   }
   
   retstr = calloc(1, 2);
   if (retstr != NULL)
      retstr[0] = (result == noErr) ? '0': '1';
      
   *retstring = retstr;
}

Enabling and Disabling Edit Menu Items

RunRevVeez needs to enable and disable the Edit menu items according to the edit state of the movie in a movie window. For instance, when a movie is first opened and no edit operations have yet occurred, the Undo item should be disabled. QuickTime provides the MCGetControllerInfo function, which we've used in the past to adjust the states of our edit menu items. We'll use it again here, as shown in Listing 8.

Listing 8: Adjusting the Edit menu

XCMD_MCEnableEditMenuItem
void XCMD_MCEnableEditMenuItem (char *args[], int nargs, 
            char **retstring, Bool *pass, Bool *error)
{
   MovieController mc = NULL;
   ComponentResult result = noErr;
   long mcInfo = 0L;
   short index = 0;
   char *retstr = NULL;
   
   *pass = false;
   *error = false;
   retstr = malloc(2);      // either "0" or "1", plus the terminating null byte
   if (nargs == 2) {
      mc = (MovieController)atol(args[0]);
      index = (short)atoi(args[1]);
      
      if (mc != NULL)
         result = MCGetControllerInfo(mc, &mcInfo);
   }
   
   switch (index) {
      case kUndoItemIndex:
         retstr[0] = mcInfo & mcInfoUndoAvailable ? '1': '0';
         break;
   
      case kCutItemIndex:
         retstr[0] = mcInfo & mcInfoCutAvailable ? '1': '0';
         break;
   
      case kCopyItemIndex:
         retstr[0] = mcInfo & mcInfoCopyAvailable ? '1': '0';
         break;
   
      case kPasteItemIndex:
         retstr[0] = mcInfo & mcInfoPasteAvailable ? '1': '0';
         break;
   
      case kClearItemIndex:
         retstr[0] = mcInfo & mcInfoClearAvailable ? '1': '0';
         break;
   
      case kSelectAllItemIndex:
      case kSelectNoneItemIndex:
         retstr[0] = mcInfo & mcInfoEditingEnabled ? '1': '0';
         break;
         
      default:
         DebugStr("\pGOT AN INDEX WE DIDN'T EXPECT!");
         break;
   }   
   
   // tack on the terminating null byte
   retstr[1] = 0;
    
   *retstring = retstr;
}

Notice that our code here looks for two parameters, which are the movie controller identifier and the index of the menu item we want information about. If, according to MCGetControllerInfo, the menu item with that index should be enabled, XCMD_MCEnableEditMenuItem passes back the string "1"; otherwise it passes back the string "0".

In RunRevVeez, the code that enables or disables the menu items is contained in the script attached to the menu item group (and not to any particular menu or item). That's because, when the user clicks on the menu bar, a mouseDown message is sent to the menu item group. We want to call mcEnableEditMenuItem for each menu item index and adjust the menu item according to the value returned by it.

Listing 9: Adjusting the Edit menu

mouseDown
on mouseDown
   put first line of the openStacks into theTopStack
   put exists(player "MoviePlayer" of stack theTopStack) \
                  into gotPlayer
  
   repeat for each item itemIndex in "1,3,4,5,6,8,9"
      if gotPlayer then
         if mcEnableEditMenuItem(the movieControllerID of \
                  player "MoviePlayer" of stack theTopStack, \
                     itemIndex) is "1" then
            enable menuItem itemIndex of menu "Edit"
         else
            disable menuItem itemIndex of menu "Edit"
         end if
      else
         disable menuItem itemIndex of menu "Edit"
      end if
   end repeat
  
end mouseDown

We also need to adjust the states of the items in the File menu and the Movie menu. We'll postpone our consideration of the File menu to the next article. We can handle the Movie menu as shown in Listing 10.

Listing 10: Adjusting the Movie menu

mouseDown
if gotPlayer then
   enable menuItem kShowBarItemIndex of menu "Movie"
   enable menuItem kHideSpeakerItemIndex of menu "Movie"
else
   disable menuItem kShowBarItemIndex of menu "Movie"
   disable menuItem kHideSpeakerItemIndex of menu "Movie"
end if

Here we use a few constants that we've defined in our message handler:

constant kShowBarItemIndex = 1
constant kHideSpeakerItemIndex = 2
Setting the Window Status

In the Aqua interface, a window's close button contains a dot if the movie in the window has been modified since opened or last saved (compare Figure 3 with Figure 2).


Figure 3: A modified movie window

In earlier QuickTime Toolkit articles, we've seen that we can set the window modification state by calling SetWindowModified. With Revolution, we need to call into our external to do this. Listing 11 shows our definition of the XCMD_SetWindowModified function.

Listing 11: Setting the window modification state

XCMD_SetWindowModified
void XCMD_SetWindowModified (char *args[], int nargs, 
            char **retstring, Bool *pass, Bool *error)
{
   WindowPtr wID = NULL;
   Boolean state;
   OSErr result = noErr;
   char *retstr = NULL;
   *pass = false;
   *error = false;
   if (nargs == 2) {
      wID = (WindowPtr)atol(args[0]);
      state = (Boolean)atoi(args[1]);
      if (wID != NULL)
         result = SetWindowModified(wID, state);
   }
      
   retstr = calloc(1, 2);
   if (retstr != NULL)
      retstr[0] = (result == noErr) ? '0': '1';
      
   *retstring = retstr;
}

This is pretty much like all the external procedures we've seen so far, except that the first parameter here is of type WindowPtr. Our call to windowSetModified looks like this:

get windowSetModified(windowID of stack theTopStack, 1)

A stack's windowID property contains the operating system ID of the window containing the stack; on Mac OS, this ID is a window pointer. (By the way, notice that we invoke the windowSetModified command by passing it as an expression to the get command. The "get expr" command is a shortcut for the expression:

put expr into it

We need to treat windowSetModified as a function, since that's how we declared it. If we had declared it as a command, we would omit the get.)

We also need to keep track of a window's modification state within our scripts in RunRevVeez (so, for instance, we know whether to enable or disable some of the items in the File menu). We could implement yet another function in our external that calls IsWindowModified. Or we can define a custom property associated with the movie window stack that keeps track of this modification state. Let's use a custom property. Open the movie window's property inspector palette and select the "Custom Properties" panel in the pop-up menu. The original panel looks like Figure 4.


Figure 4: The movie window's custom properties (original)

Click the "+" icon to add a new property. Let's call the new property movieChanged. When a movie window is first opened, this property should be set to 0, so set the property contents accordingly. The property inspector palette now looks like Figure 5.


Figure 5: The movie window's custom properties (final)

Once we've done this, we can access the movieChanged property just like we access any of the built-in properties, for example like this:

set the movieChanged of stack theTopStack to true

We'll see some examples of this in the next section.

Movie Editing

We're now finished constructing the movie editing portions of our Revolution plug-in module. It's very easy to put them to work. When the user selects an item in the Edit menu, the menuPick message handler of the Edit menu is called. Listing 12 shows our complete menuPick handler. Notice that we check to make sure that the value returned by the editing operations (for example, mcCut) is the string "0", which indicates that the operation completed successfully.

Listing 12: Handling the Edit menu items

menuPick
on menuPick pWhich
  
   put first line of the openStacks into theTopStack
   if exists(player "MoviePlayer" of stack theTopStack) then
      put the movieControllerID of player "MoviePlayer" of \
                                 stack theTopStack into mc
      put false into changed
    
      switch pWhich
      case "Undo"
         if mcUndo(mc) = "0" then put true into changed
         break
      case "Cut"
         if mcCut(mc) = "0" then put true into changed
         break
      case "Copy"
         mcCopy(mc)
         break
      case "Paste"
         if mcPaste(mc) = "0" then put true into changed
         break
      case "Clear"
         if mcClear(mc) = "0" then put true into changed
         break
      case "Select All"
          selectAll(mc)
         break
      case "Select None"
         selectNone(mc)
         break
      end switch
     
      if changed then
         set the movieChanged of stack theTopStack to true
         get windowSetModified \
                                    (windowID of stack theTopStack, 1)
         sizeStackToMovie the short name of stack theTopStack
      end if
    
  end if
end menuPick

We also call the sizeStackToMovie method if the movie has been edited, since the size of the movie may have changed.

Conclusion

In this article, we've focused mainly on adding the ability to edit movies to our application RunRevVeez. We've seen how to construct a plug-in that allows our Revolution scripts to invoke external code modules. This is the primary avenue by which we can enhance the built-in behaviors and capabilities of Revolution.

We've got a little bit more work to do to get RunRevVeez to operate precisely as desired. We still need to handle the "Save As" and Close menu items in the File menu, and we need to tie up a few remaining loose ends. We'll tackle all that in the next article.

Credits

Thanks are due once again to Kevin Miller and Tuviah Snyder at Runtime Revolution Ltd. Tuviah was especially helpful with the plug-in. And a special thanks is again due to Geoff Canyon of Inspired Logic, LLC.


Tim Monroe is a member of the QuickTime engineering team at Apple. You can contact him at monroe@mactech.com. The views expressed here are not necessarily shared by his employer.

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Day One 6.1 - Maintain a daily journal.
Day One is an easy, great-looking way to use a journal / diary / text-logging application. Day One is well designed and extremely focused to encourage you to write more through quick Menu Bar entry,... Read more
Vivaldi 3.7.2218.55 - An advanced browse...
Vivaldi is a browser for our friends. We live in our browsers. Choose one that has the features you need, a style that fits and values you can stand by. From the look and feel, to how you interact... Read more
Macs Fan Control 1.5.9 - Monitor and con...
Macs Fan Control allows you to monitor and control almost any aspect of your computer's fans, with support for controlling fan speed, temperature sensors pane, menu-bar icon, and autostart with... Read more
Dragon Dictate 6.0 - Premium voice-recog...
With Dragon Dictate speech recognition software, you can use your voice to create and edit text or interact with your favorite Mac applications. Far more than just speech-to-text, Dragon Dictate lets... Read more
OmniFocus 3.11.7 - GTD task manager with...
OmniFocus is an organizer app. It uses projects to organize tasks naturally, and then add tags to organize across projects. Easily enter tasks when you’re on the go, and process them when you have... Read more
rekordbox 6.5.1.0009 - Professional DJ m...
rekordbox is the best way of preparing and managing your tracks, be it at home, in the studio, or even on the plane! It allows you to import music from other music-management software using the... Read more
1Password 7.8.1 - Powerful password mana...
1Password is a password manager that uniquely brings you both security and convenience. It is the only program that provides anti-phishing protection and goes beyond password management by adding Web... Read more
Ableton Live 10.1.35 - Record music usin...
Ableton Live lets you create and record music on your Mac. Use digital instruments, pre-recorded sounds, and sampled loops to arrange, produce, and perform your music like never before. Ableton Live... Read more
Microsoft OneNote 16.48 - Free digital n...
OneNote is your very own digital notebook. With OneNote, you can capture that flash of genius, that moment of inspiration, or that list of errands that's too important to forget. Whether you're at... Read more
Microsoft Office 365, 2019 16.48 - Popul...
Microsoft Office 365. The essentials to get it all done. Unmistakably Office, designed for Mac Get started quickly with new, modern versions of Word, Excel, PowerPoint, Outlook and OneNote-... Read more

Latest Forum Discussions

See All

Pokemon Masters EX's latest update...
Two new Sync Pairs have made their way into Pokemon Masters today. Both pairs hail from the Alola region, Elio & Popplio and Selene & Rowlet. Their arrival coincides with an event called Trials on the Isle. [Read more] | Read more »
Shrouded Citadel: navigate your escape i...
Having been cooped up over the past 12 months due to winter and covid, Pifer is encouraging gamers to start enjoying the great outdoors again with its recently launched AR adventure epic, Shrouded Citadel. | Read more »
Moonlight Sculptor is an upcoming MMORPG...
Kakao Games and XL Games – who you might be familiar with from their previous game ArcheAge – have announced that their MMORPG Moonlight Sculptor is now available to pre-order for iOS and Android devices. Moonlight Sculptor has previously launched... | Read more »
MU Archangel is now open for pre-registr...
MU Archangel is now open for pre-registration in Southeast Asia following its massive success in other territories. Players from Singapore, Thailand, Malaysia, Indonesia, and the Philippines (except Vietnam) can now join in on the fun by applying... | Read more »
Compete, a new social media app you can...
Whoever told you you can’t get rich making videos has obviously never heard of Compete, Competitive Media Technologies Limited’s hot new social media app where you can rake in all the dough just by doing what you love. Video monetization that... | Read more »
Bethesda has released a new DOOM mobile...
Bethesda Softworks has released a new DOOM game out of the blue exclusively for mobile devices. It’s called Mighty DOOM and is currently only available as an early access title on Android but will be expanding to more users in the future. [Read... | Read more »
Anagraphs is a word puzzle game with a t...
Cinq-Mars Media has released its word puzzle game Anagraphs for iOS and Android devices. The game released last week after a short delay in getting it onto the appropriate platforms. [Read more] | Read more »
These are the top 5 best iPhone games li...
Fortnite has been the big hitter in mobile gaming this year, and it's not hard to see why. Thanks to some excellent marketing, and a polished experience that almost anyone can enjoy, it's really taken the App Store by storm. But there are other... | Read more »
The top 5 best iPhone games like Pokemon...
Pokemon GO is still the, if you'll excuse the pun, go-to game if you want some AR action on your phone. But it's not the only choice out there, and if you've got a hankering for something a bit different, then your eyes might already have started... | Read more »
The top 5 best iPhone games like Starcra...
Starcraft sits at the top of the RTS tree for a number of very good reasons. It also isn't on mobile, again, for a number of very good reasons. But that doesn't mean you can't find a way to indulge your sci-fi, competitive, massive, or engaging RTS... | Read more »

Price Scanner via MacPrices.net

Roundup of Today’s Best M1 Mac mini Prices an...
Apple resellers are offering discounts on new M1 Mac minis ranging up to $140 off MSRP this week, with prices starting at only $589. These are all the same Mac minis sold by Apple in their retail and... Read more
New at Verizon: Apple iPhone SE for free with...
Verizon is offering the 64GB Apple iPhone SE for free for customers opening a new line of service with a Verizon Unlimited plan. Offer is valid for a limited time. Price is credited monthly over a 24... Read more
B&H is offering clearance prices on lefto...
Apple reseller B&H Photo has clearance 2020 13″ 1.4GHz Intel-based MacBook Pros on sale today for $200-$300 off Apple’s original MSRP with prices starting at only $1099. Expedited shipping is... Read more
Roundup of Today’s Best MacBook Deals: M1 Mac...
Apple resellers are offering sale prices on Apple’s M1-powered 13″ MacBook Airs ranging up to $190 off MSRP. Here’s where to pick one up today, and as always, keep an eye on our 13″ MacBook Air Price... Read more
Apple AirPods Pro drop to new low price of on...
Amazon has Apple’s AirPods Pro on sale today for a new low price of only $197 shipped. That’s $52 off MSRP and the lowest price currently available for a set of AirPods Pro from any Apple reseller.... Read more
Apple restocks clearance 13″ Intel-based MacB...
Apple has clearance, Certified Refurbished, 2020 13″ Intel-based MacBook Airs available starting at only $809 and up to $280 off original MSRP. Each MacBook features a new outer case, comes with a... Read more
OWC drops prices on 2020 Intel multi-core Mac...
Other World Computing has clearance 2020 Intel-based Mac minis on sale starting at only $499. Both 4-core and 6-core models are in stock today. These are new, unopened, factory-sealed minis: – 3.6GHz... Read more
Save $50 off Apple’s 10.9″ iPad Air today at...
B&H Photo has new 10.9″ Apple iPad Airs in stock and on sale today for up to $50 off MSRP. Expedited shipping is free to most addresses in the US. Note that some sale prices may be restricted to... Read more
Rare Apple sale: Get a HomePod mini for $10 o...
Apple reseller Expercom has the Space Gray HomePod mini on sale today for $89 shipped. Their price is $10 off Apple’s MSRP, and it’s currently the only sale price available for a HomePod mini among... Read more
Apple has M1 Mac minis available starting at...
Apple has a full line of standard configuration M1 Mac minis available in their Certified Refurbished section starting at only $589 and up to $140 off MSRP. Each mini comes with Apple’s one-year... Read more

Jobs Board

Geek Squad Advanced Repair *Apple* Professi...
**802113BR** **Job Title:** Geek Squad Advanced Repair Apple Professional **Job Category:** Store Associates **Store Number or Department:** 000399-Wausau-Store Read more
*Apple* Mobility Specialist - Best Buy (Unit...
**802109BR** **Job Title:** Apple Mobility Specialist **Job Category:** Store Associates **Store Number or Department:** 001540-Tuscaloosa-Store **Job Description:** Read more
*Apple* /MAC Technician - TEKsystems (United...
Description: Looking to add an IT Technician proficient in troubleshooting Apple products, including workstations, iPads and laptops. A 2 or 4 year degree degree is Read more
*Apple* Valley 20hr Teller - Wells Fargo (Un...
…or scheduled + Ability to stand for extended periods of time **Street Address** **MN- Apple Valley:** 14325 Cedar Ave - Apple Valley, MN **Disclaimer** All offers Read more
Executive Team Leader Specialty Sales (Assist...
…Specialty Sales (Assistant Manager Merchandising and Service)- Apple Valley, CaliforniaApply NowJob ID:R0000129723job family:Store Managementschedule:Full Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.