TweetFollow Us on Twitter

Nov 00 QTToolkit

Volume Number: 16 (2000)
Issue Number: 11
Column Tag: QuickTime Toolkit

Word is Out

by Tim Monroe

Using Text in QuickTime Movies

Introduction

When QuickTime was first introduced, it was able to handle two types of media data: video and sound. Curiously, the very next media type added to QuickTime (in version 1.5) was text, or the written word. Part of the motivation for adding text media was to provide the sort of "text below the picture" that you see in movie subtitles or television closed-captioning, as illustrated in Figure 1. Here, the text provides the words of a song, which can be useful to hearing-impaired or non-English speaking users. Similarly, the text might provide the dialogue of a play or a readable version of the narration. Of course, the text doesn't have to just mirror the voice part of an audio track; it can be any annotation that the movie creator deems useful for the viewer.


Figure 1. A movie containing a text track.

The text you see in Figure 1 is not part of the video track; rather, it is stored in a text track (whose associated media is of type TextMediaType). Typically the text track is situated below the video track (as in Figure 1), but in fact it can overlay part or all of the video track. In order for both the text and the overlain video to be visible, the background of the text track should be transparent or "keyed out"; the text is then called keyed text. Figure 2 shows some keyed text overlaying a video track. Keying can be computationally expensive, however, so keyed text is seen less often than below-the-video text.


Figure 2. A movie containing a keyed text track.

QuickTime provides the capability to search for a specific string of characters in a text track and to move the current movie time forward (or backward) to the next (or previous) occurrence of that string. In addition, the standard movie controller provides support for a special kind of text track called a chapter track. A chapter track is a text track that has been associated with some other track (often a video or sound track); when a movie contains a chapter track, the movie controller will build, display, and handle a pop-up menu that contains the text in the various samples in that track. The pop-up menu appears (space permitting) in the controller bar. The various parts of the associated track are called the track's chapters. When the user selects an item in the pop-up menu, the movie controller jumps to the start time of the selected chapter. Figure 3 shows our standard appearing-penguin movie with a chapter track that indicates the percentage of completion (both before and after the user clicks on the pop-up menu). Notice that we've had to hide the step buttons in the controller bar to make room for the chapter pop-up menu. Notice also that the text track itself is not visible.


Figure 3. A movie with a chapter track.

The QuickTime Player application, introduced with QuickTime 3.0, employs a slightly different user interface for accessing a movie's chapters. As you can see in Figure 4, a QuickTime Player movie window replaces the pop-up menu with a set of up- and down-arrow controls, which select the previous and next chapter.


Figure 4. The chapter controls in a QuickTime Player movie window.

QuickTime 3.0 also included a web browser plug-in that supports linked text. Linked text is contained in a hypertext reference track (usually shortened to HREF track), which is simply a text track that has a special name (to wit, "HREFTrack") and contains some media samples that pick out URL links. If a text sample contains text of the form <URL>, the QuickTime Plug-In will load the specified URL in the frame containing the movie when the user clicks in the movie box while that text sample is active. (Let's call this a clickable link.) If the text is if the form A<URL>, then the plug-in will load the specified URL automatically when that text sample becomes active. (Let's call this an automatic link.)

QuickTime 4 added one more text-handling tool, the ability to attach wired actions to data in a text track. A wired action is some action (such as setting a movie's volume or its current time) that is initiated by some particular event. The events that can trigger wired actions include both user events like moving or clicking the mouse and movie controller events like loading movies or processing idle events from the operating system. We'll investigate wired actions at length in a future article; for the moment, consider the movie shown in Figure 5. This movie contains only one track, a text track. The text track is configured so that clicking on the word "Apple" launches the user's default web browser and loads the URL http://www.apple.com; in addition, rolling the cursor over the word "CNN" loads the URL http://www.cnn.com/. (Let's call this wired text.)


Figure 1. A text track with wired actions.

In this article, we're going to take a look at the most basic ways of handling text in QuickTime movies. After we take a brief detour to upgrade the code that adjusts our Edit menu, we'll uncover some ways in which our existing sample applications can already interact with text. It turns out that these applications can do a surprising amount of work with text tracks; indeed, they can even create text tracks, in spite of the fact that they contain no text-specific code. So we'll spend a little bit of time to see how that's possible. Then we'll see how to create text tracks using the standard Movie Toolbox functions. We'll also learn how to search and edit text tracks. Toward the end of this article, we'll see how to create chapter tracks and HREF tracks. When all is said and done, we'll have at hand the essential tools that we need to create text tracks, keyed text, chapter tracks, and linked text. Figure 6 shows the Test menu of this month's sample application, named QTText.


Figure 6. The Test menu of QTText.

The Edit Menu Revisited

Let's begin by considering our code for enabling and disabling items in the Edit menu. (This might appear to have nothing at all to do with text handling, but it is actually fairly germane to this topic. Trust me.) Currently, when the user clicks in the menu bar to select one of our application's menus, our application framework calls the QTFrame_AdjustMenus function. (In our Macintosh framework, this happens in response to a mouseDown event in the inMenuBar window part; in our Windows framework, this happens when the MDI frame window receives the WM_INITMENU command.) Listing 1 shows the code in QTFrame_AdjustMenus that adjusts the Edit menu.

Listing 1: Adjusting the Edit menu (original version)

QTFrame_AdjustMenus
#if TARGET_OS_MAC
myMenu = GetMenuHandle(kEditMenuResID);
#endif
if (myMC != NULL) {
   long            myFlags;
      
   MCGetControllerInfo(myMC, &myFlags);
   
   QTFrame_SetMenuItemState(myMenu, IDM_EDITUNDO, 
                                 myFlags & mcInfoUndoAvailable ? 
                        kEnableMenuItem : kDisableMenuItem);
   QTFrame_SetMenuItemState(myMenu, IDM_EDITCUT, 
                                 myFlags & mcInfoCutAvailable ? 
                        kEnableMenuItem : kDisableMenuItem);
   QTFrame_SetMenuItemState(myMenu, IDM_EDITCOPY, 
                                 myFlags & mcInfoCopyAvailable ? 
                        kEnableMenuItem : kDisableMenuItem);
   QTFrame_SetMenuItemState(myMenu, IDM_EDITPASTE, 
                                 myFlags & mcInfoPasteAvailable ? 
                        kEnableMenuItem : kDisableMenuItem);
   QTFrame_SetMenuItemState(myMenu, IDM_EDITCLEAR, 
                                 myFlags & mcInfoClearAvailable ? 
                        kEnableMenuItem : kDisableMenuItem);
   QTFrame_SetMenuItemState(myMenu, IDM_EDITSELECTALL, 
                                 myFlags & mcInfoEditingEnabled ? 
                           kEnableMenuItem : kDisableMenuItem);
QTFrame_SetMenuItemState(myMenu, IDM_EDITSELECTNONE, 
                                 myFlags & mcInfoEditingEnabled ? 
                        kEnableMenuItem : kDisableMenuItem);
} else {
   QTFrame_SetMenuItemState(myMenu, IDM_EDITUNDO, 
                                                      kDisableMenuItem);
   QTFrame_SetMenuItemState(myMenu, IDM_EDITCUT,
                                                      kDisableMenuItem);
   QTFrame_SetMenuItemState(myMenu, IDM_EDITCOPY, 
                                                      kDisableMenuItem);
   QTFrame_SetMenuItemState(myMenu, IDM_EDITPASTE, 
                                                      kDisableMenuItem);
   QTFrame_SetMenuItemState(myMenu, IDM_EDITCLEAR, 
                                                      kDisableMenuItem);
   QTFrame_SetMenuItemState(myMenu, IDM_EDITSELECTALL, 
                                                      kDisableMenuItem);
   QTFrame_SetMenuItemState(myMenu, IDM_EDITSELECTNONE, 
                                                      kDisableMenuItem);
}


There's nothing particularly complicated here: if there is no movie controller associated with the frontmost window or there is no frontmost window, then we disable all the items in the Edit menu (that's the "else" portion). Otherwise, we call the MCGetControllerInfo function to determine the current status of the movie controller and its associated movie. MCGetControllerInfo returns a set of flags that indicate which editing operations currently make sense for the specified movie controller and its movie. For instance, if there is some data available for pasting and editing is enabled, then the mcInfoPasteAvailable flag is set in the 32-bit long integer returned by MCGetControllerInfo. In this case, our application should enable the Paste menu item. Conversely, if either editing is disabled for the specified movie or there is nothing to paste, then that flag is clear. In that case, the Paste menu item should be disabled. We call the function QTFrame_SetMenuItemState to enable or disable the Paste menu item, like this:

QTFrame_SetMenuItemState(myMenu, IDM_EDITPASTE, 
                           myFlags & mcInfoPasteAvailable ? 
                     kEnableMenuItem : kDisableMenuItem);

We've already considered QTFrame_SetMenuItemState in an earlier article (see "QuickTime 101" in MacTech, January 2000); it just calls the appropriate platform-specific function for enabling or disabling a menu item.

Emulating QuickTime Player

So far, so good. But there is a very important capability that we still need to add to our sample applications. If we launch the QuickTime Player application, open a movie, make a selection, and then hold down the Option key (or, on Windows, both the Ctrl and Alt keys) while clicking on the Edit menu, we'll see something like the menu shown in Figure 7:


Figure 7. The Edit menu of QuickTime Player (Option key down).

Notice that the Paste menu item is now labeled "Add" and the Clear menu item is now labeled "Trim". Similarly, if we hold down just the Shift key while clicking on the Edit menu, we'll see the menu shown in Figure 8:


Figure 8. The Edit menu of QuickTime Player (Shift key down).

Now the Paste menu item is labeled "Replace". Finally, if we hold down the Option and the Shift keys (or, on Windows, the Ctrl and Alt and Shift keys) while clicking on the Edit menu, the Paste menu item will be labeled "Add Scaled", as shown in Figure 9. (For the moment, don't worry about what these renamed menu items actually do; we'll get to that in the next section.)


Figure 9. The Edit menu of QuickTime Player (Shift and Option keys down).

What's happening here is that QuickTime Player is not using MCGetControllerInfo to do its Edit menu adjusting, at least for the first five menu commands. Instead, it's using the MCSetUpEditMenu function, which is specially designed to change the Edit menu item labels in the ways just described, depending on which keyboard modifier keys the user is holding down. MCSetUpEditMenu is declared essentially like this:

ComponentResult MCSetUpEditMenu (MovieController mc, 
                                    long modifiers, MenuHandle mh);

MCSetUpEditMenu correctly enables or disables and names the first five commands in the Edit menu specified by the menu handle mh, as long as those items have the standard arrangement (Undo, a separator line, Cut, Copy, Paste, and Clear).

It appears, then, that we can simplify our menu-adjusting code and gain the additional behaviors described above by using MCSetUpEditMenu ourselves. There are just a couple of changes we need to make to support MCSetUpEditMenu. Primarily, we need to add a parameter to our QTFrame_AdjustMenus function, so that we can pass it the current keyboard modifiers. Henceforth, QTFrame_AdjustMenus will be declared like this:

int QTFrame_AdjustMenus (WindowReference theWindow, 
                  MenuReference theMenu, long theModifiers);

Getting the appropriate keyboard modifiers in our Macintosh code is easy. Whenever we call QTFrame_AdjustMenus, either we don't care about the modifiers (so we can pass 0L) or we have an event record available (so we can pass (long)theEvent->modifiers).

Getting the Modifier Keys on Windows

When we call QTFrame_AdjustMenus on Windows, however, we need to do some additional work to determine which (if any) modifier keys the user is holding down when clicking on the Edit menu. Remember that we want to pass MCSetUpEditMenu a long integer whose bits indicate which modifier keys are active. The "gotcha" here is that these are supposed to be the modifier keys on a Macintosh keyboard. MCSetUpEditMenu knows nothing about the Alt or Ctrl keys found on Windows keyboards. Rather, it's expecting a 32-bit value in which the up or down state of the relevant modifier keys is encoded using these bits (defined in Events.h):

enum {
   cmdKey                     = 1 << cmdKeyBit,      // 0x0100
   shiftKey                  = 1 << shiftKeyBit,   // 0x0200
   alphaLock               = 1 << alphaLockBit,   // 0x0400
   optionKey               = 1 << optionKeyBit,    // 0x0800
   controlKey               = 1 << controlKeyBit   // 0x1000
};

For example, if only the Option key is down, the modifiers value should be 0x00000800. Similarly, if both the Shift and Control keys are down, the modifiers value should be 0x00001200.

QuickTime maps the Windows modifier keys to the Macintosh modifier keys in this manner:

  • The Windows Alt key is mapped to the Macintosh Control key.
  • The Windows Ctrl key is mapped to the Macintosh Command key.
  • The Windows Shift key is mapped to the Macintosh Shift key.
  • The Windows Caps Lock key is mapped to the Macintosh Caps Lock key.
  • The combination of the Windows Alt and Ctrl keys is mapped to the Macintosh Option key.

To help us construct a Mac-style modifiers long word, we'll add these constants and compiler macros to the file WinFramework.h:

#define VK_MAC_CONTROLKEY         VK_MENU
#define VK_MAC_COMMANDKEY         VK_CONTROL
#define VK_MAC_SHIFTKEY            VK_SHIFT
#define VK_MAC_CAPSKEY            VK_CAPITAL

#define QTFrame_IsControlKeyDown(theKeyState)      
         (theKeyState[VK_MAC_CONTROLKEY] & 0x80 ? 1 : 0) 
#define QTFrame_IsCommandKeyDown(theKeyState)      
         (theKeyState[VK_MAC_COMMANDKEY] & 0x80 ? 1 : 0)
#define QTFrame_IsShiftKeyDown(theKeyState)         
            (theKeyState[VK_MAC_SHIFTKEY] & 0x80 ? 1 : 0)   
#define QTFrame_IsAlphaLockKeyDown(theKeyState)      
            (theKeyState[VK_MAC_CAPSKEY] & 0x80 ? 1 : 0)   
#define QTFrame_IsOptionKeyDown(theKeyState)      
                  (QTFrame_IsControlKeyDown(theKeyState)) && 
                  (QTFrame_IsCommandKeyDown(theKeyState))

On Windows, a key state array (represented by the argument theKeyState in these macros) is a 256-byte array that contains information about each of the 256 virtual-key codes. If a key is down, then the high-order bit (0x80) of the corresponding element of this array will be set. For instance, if the Alt key is down, then the high-order bit of the array element whose index is 0x12 will be set. (The virtual-key code for the Alt key is VK_MENU, which is defined as 0x12 in the file Winuser.h.)

We can fill a key state array with the current values by calling the GetKeyboardState function. Then all we need to do is inspect the Windows modifier keys that are of interest to us and construct a Mac-style modifiers value that encodes that information. When we need to call QTFrame_AdjustMenus, we can get the current set of modifier keys by calling QTFrame_GetKeyboardModifiers, defined in Listing 2.

Listing 2: Getting the Windows keyboard modifier keys

QTFrame_GetKeyboardModifiers
static long QTFrame_GetKeyboardModifiers (void)
{
   long      myModifiers = 0L;
   BYTE      myKeyState[256];

   if (GetKeyboardState(&myKeyState[0])) {
      if (QTFrame_IsOptionKeyDown(myKeyState))
         myModifiers |= optionKey;
      else if (QTFrame_IsCommandKeyDown(myKeyState))
         myModifiers |= cmdKey;
      else if (QTFrame_IsControlKeyDown(myKeyState))
         myModifiers |= controlKey;
   
      if (QTFrame_IsShiftKeyDown(myKeyState))
         myModifiers |= shiftKey;
       if (QTFrame_IsAlphaLockKeyDown(myKeyState))
          myModifiers |= alphaLock;
    }
    
   return(myModifiers);
}

So, on Windows, we are now able to pass the correct set of modifier flags to MCSetUpEditMenu. But what do we pass for the third parameter, which on MacOS is a menu handle for the Edit menu? The answer, it turns out, is that we'll pass the value NULL. The reason for this is that on Windows we access our menus using a value of type HMENU, not MenuHandle. This means, however, that on Windows we cannot depend on MCSetUpEditMenu to either highlight or rename the items in the Edit menu. For that, we'll have to write our own code.

Renaming the Edit Menu Items on Windows

At this point, you might be wondering why we're bothering to call MCSetUpEditMenu on Windows, if it isn't going to help us with highlighting or renaming the items in the Edit menu. The answer is that MCSetUpEditMenu does more than simply enable or disable menu items and rename them to match the state of the active modifier keys. MCSetUpEditMenu also sets some flags maintained internally by the movie controller that affect the operation of subsequent editing commands. For instance, when we call MCPaste, it looks at those flags to determine whether it should paste, or replace, or add, or add scaled. In other words, if we don't call MCSetUpEditMenu, all our editing operations will just be the default undo, cut, copy, paste, and clear operations.

On Windows, we still have two tasks left to handle. First, we need to perform our own Edit menu item enabling and disabling. We already have code for this (see Listing 1 again), so we'll just conditionalize that code to be executed under Windows but not under MacOS. Second, we need to find a way to rename the Edit menu items according to the current state of the modifier keys. This task is actually relatively easy, since QuickTime provides the MCGetMenuString function, which we can use to retrieve the label for a particular menu item, given a set of modifier keys. Suppose, for instance, that we execute this line of code (here, myString is of type Str255):

MCGetMenuString(myMC, optionKey, mcMenuPaste, myString);

If MCGetMenuString completes successfully, then myString will hold the string "Add". All we need to do then is insert that string into our Windows Edit menu. The function QTFrame_ConvertMacToWinMenuItemLabel, defined in Listing 3, handles all of this for us.

Listing 3: Renaming a Windows Edit menu item

QTFrame_ConvertMacToWinMenuItemLabel

void QTFrame_ConvertMacToWinMenuItemLabel (
         MovieController theMC, MenuReference theWinMenu, 
         long theModifiers, UInt16 theMenuItem)
{
   Str255      myString;
   char       *myLabelText = NULL;
   char       *myBeginText = NULL;
   char       *myFinalText = NULL;
   short      myLabelSize = 0;

   // get the appropriate label for the specified item and keyboard modifiers
   MCGetMenuString(theMC, theModifiers, 
                                 MENU_ITEM(theMenuItem), myString);
   
   switch (theMenuItem) {
      case IDM_EDITUNDO:
         myBeginText = kAmpersandText;
         myFinalText = kWinUndoAccelerator;
         break;
      case IDM_EDITPASTE:
         myBeginText = "";
         myFinalText = kWinPasteAccelerator;
         break;
      case IDM_EDITCLEAR:
         myBeginText = kAmpersandText;
         myFinalText = kWinClearAccelerator;
         break;
      default:
         // currently, only the Undo, Paste, and Clear items are modified by
         // MCSetUpEditMenu, so that's all we'll handle here
         return;
   }
   
   myLabelSize = strlen(myBeginText) + myString[0] + 
                                                strlen(myFinalText) + 1;
   myLabelText = malloc(myLabelSize);
   if (myLabelText == NULL)
      return;
   
   BlockMove(myBeginText, myLabelText, strlen(myBeginText));
   BlockMove(&myString[1], myLabelText + strlen(myBeginText), 
                           myString[0]);
   BlockMove(myFinalText, myLabelText + strlen(myBeginText) + 
                           myString[0], strlen(myFinalText));
   myLabelText[myLabelSize - 1] = '\0';
   
   QTFrame_SetMenuItemLabel(theWinMenu, theMenuItem, 
                                                      myLabelText);

   free(myLabelText);
}



QTFrame_ConvertMacToWinMenuItemLabel also adds the ampersand (&) to the beginning of several of the Edit menu items (so that the first letter is underlined) and the appropriate keyboard accelerator label to the end of all of them.

Putting it All Together

We're finally ready to put all these pieces together. Listing 4 shows our revised version of Listing 1. When no movie controller is available, we disable all the Edit menu items in exactly the same manner we did in our earlier version. And for the "Select All" and "Select None" items, we call MCGetControllerInfo and QTFrame_SetMenuItemState, just like before. But for the five standard Edit menu commands, we now call MCSetUpEditMenu on both Mac and Windows. In addition, on Windows we need to do all menu item enabling and disabling ourselves, and we need to update the menu item labels, using our function QTFrame_ConvertMacToWinMenuItemLabel.

Listing 4: Adjusting the Edit menu (revised version)

QTFrame_AdjustMenus

#if TARGET_OS_MAC
myMenu = GetMenuHandle(kEditMenuResID);
#endif

if (myMC == NULL) {
   // if there is no movie controller, disable all the Edit menu items
   QTFrame_SetMenuItemState(myMenu, IDM_EDITUNDO, 
                                                         kDisableMenuItem);
   QTFrame_SetMenuItemState(myMenu, IDM_EDITCUT,
                                                         kDisableMenuItem);
   QTFrame_SetMenuItemState(myMenu, IDM_EDITCOPY, 
                                                         kDisableMenuItem);
   QTFrame_SetMenuItemState(myMenu, IDM_EDITPASTE, 
                                                         kDisableMenuItem);
   QTFrame_SetMenuItemState(myMenu, IDM_EDITCLEAR, 
                                                         kDisableMenuItem);
   QTFrame_SetMenuItemState(myMenu, IDM_EDITSELECTALL, 
                                                         kDisableMenuItem);
   QTFrame_SetMenuItemState(myMenu, IDM_EDITSELECTNONE, 
                                                         kDisableMenuItem);
} else {
   MCGetControllerInfo(myMC, &myFlags);

   QTFrame_SetMenuItemState(myMenu, IDM_EDITSELECTALL, 
                              myFlags & mcInfoEditingEnabled ? 
                              kEnableMenuItem : kDisableMenuItem);
   QTFrame_SetMenuItemState(myMenu, IDM_EDITSELECTNONE, 
                              myFlags & mcInfoEditingEnabled ? 
                              kEnableMenuItem : kDisableMenuItem);

#if TARGET_OS_MAC
   MCSetUpEditMenu(myMC, theModifiers, myMenu);
#endif
#if TARGET_OS_WIN32
   MCSetUpEditMenu(myMC, theModifiers, NULL);

   QTFrame_SetMenuItemState(myMenu, IDM_EDITUNDO, 
                              myFlags & mcInfoUndoAvailable ? 
                              kEnableMenuItem : kDisableMenuItem);
   QTFrame_SetMenuItemState(myMenu, IDM_EDITCUT, 
                              myFlags & mcInfoCutAvailable ? 
                              kEnableMenuItem : kDisableMenuItem);
   QTFrame_SetMenuItemState(myMenu, IDM_EDITCOPY, 
                              myFlags & mcInfoCopyAvailable ? 
                              kEnableMenuItem : kDisableMenuItem);
   QTFrame_SetMenuItemState(myMenu, IDM_EDITPASTE, 
                              myFlags & mcInfoPasteAvailable ? 
                              kEnableMenuItem : kDisableMenuItem);
   QTFrame_SetMenuItemState(myMenu, IDM_EDITCLEAR, 
                              myFlags & mcInfoClearAvailable ? 
                              kEnableMenuItem : kDisableMenuItem);
   
   QTFrame_ConvertMacToWinMenuItemLabel
                     (myMC, myMenu, theModifiers, IDM_EDITUNDO);
   QTFrame_ConvertMacToWinMenuItemLabel
                     (myMC, myMenu, theModifiers, IDM_EDITCUT);
   QTFrame_ConvertMacToWinMenuItemLabel
                     (myMC, myMenu, theModifiers, IDM_EDITCOPY);
   QTFrame_ConvertMacToWinMenuItemLabel
                     (myMC, myMenu, theModifiers, IDM_EDITPASTE);
   QTFrame_ConvertMacToWinMenuItemLabel
                     (myMC, myMenu, theModifiers, IDM_EDITCLEAR);
#endif
}


There is one final modification that we need to make to our Windows source code. Apparently, on Windows, calling the MCIsPlayerEvent function has the nasty side-effect of clearing the movie controller flags that store the current modifier key settings. So we need to make sure that we do not call MCIsPlayerEvent if we are about to execute an editing command. We can do this by adding the condition (theMessage != WM_COMMAND) in the movie window procedure QTFrame_MovieWndProc. See the version of WinFramework.c included in this month's code for the exact placement of this fix.

Text Importing

Suppose now that we've implemented all the changes described in the previous section. Let's see what all this work has bought us.

Importing Text from the Clipboard

Open a movie with a video track, perhaps even the penguin movie we created in an earlier article. Select part or all of the movie. Then switch to some application that can handle text; in that application, select some text and copy it. Then return to our upgraded application and execute the "Add Scaled" command in the Edit menu (that is, choose Paste while holding down the Shift and Option keys on the Mac, or the Shift and Ctrl and Alt keys on Windows). Voilà — we've just added a text track to our movie, positioned below the video track.

Keep in mind that our upgraded sample applications contain absolutely no special code for handling text media. So how did we manage to create a text track so effortlessly? The answer is that MCPaste looks to see what kind of data it's being asked to insert into the open movie. If it's a segment of a movie, then MCPaste just inserts the data as we'd expect. But if the data isn't movie data, MCPaste looks around for a QuickTime component that can import that kind of data as a movie. In other words, MCPaste goes looking for a suitable movie import component. In this case, it finds the text movie import component (component type MovieImportType and subtype TextMediaType), which inspects the current modifier flags cached by the movie controller and performs the operation corresponding to those flags.

  • If none of the relevant modifier flags is set, the text movie importer pastes the text data at the current position in the movie. If a text track already exists in the movie, the pasted text is inserted into that track and inherits all the spatial and visual characteristics of that track. But if no text track exists in the movie, the text movie importer creates a new track that has the same size and position as the current movie box. The pasted text is given a default duration of two seconds.
  • If only the Shift modifier flag is set, then MCPaste performs a Replace operation. If the movie has a non-empty selection, the pasted text replaces the current selection; otherwise, if there is no selection, the pasted text replaces the entire movie. In both cases, the duration of the pasted text sample is the default two seconds.
  • If only the Option modifier flag is set, then MCPaste performs an Add operation: the text track is positioned below the existing video track, with a height that accommodates the pasted text (this is called adding in parallel). The duration of the pasted text sample is the default two seconds.
  • If both the Shift modifier flag and the Option modifier flag are set, then MCPaste performs an Add Scaled operation: a text track is added in parallel for the duration of the current selection. If there is no selection, then the text track is added in parallel for the duration of the entire movie.

On Macintosh operating systems only, holding down the Control key and any other combination of modifier keys while selecting Paste in the Edit menu causes the text movie importer to display the text import settings dialog box, shown in Figure 10. This dialog box allows the user to configure some settings of the pasted text.


Figure 10. The text import settings dialog box Importing Text from a File

The text movie importer can also import text stored in a file, and indeed provides some additional capabilities that are not available when pasting text from the scrap. If we open a text file using any of our sample applications, the text importer creates a movie that has a text sample for every paragraph of text in the file. Each text sample will have the standard default duration of two seconds and will be drawn in the default text font, which is dependent upon the operating system.

Note that, since we're importing a file and not pasting data from the system scrap, our existing sample applications will exhibit this behavior, whether or not we've applied the changes described in the previous section. This is just another case of NewMovieFromFile detecting that the file we've asked it to process is not a QuickTime movie file and then looking around for a suitable movie importer to handle that data. (See "Quick on the Draw" in MacTech, April 2000 for more details on this.)

The text importer recognizes a large number of text descriptors that modify the default characteristics of the imported text. Suppose that we open a text file that contains these lines of text:

{QTtext}
{font:Tekton}{plain}{size:18}
{textColor: 0, 0, 0}{backColor: 65535, 65535, 0}
{justify:center}{timeScale:600}{width:240}{height:40}
{timeStamps:absolute}{language:0}{textEncoding:0}
{shrinkTextBox: on}
[00:00:00.000]
{textBox: 10, 0, 30, 240}We forgot to seed!
[00:00:01.000]
{textBox: 10, 0, 30, 240}D'Oh!
[00:00:01.100]
{textBox: 10, 20, 30, 240}D'Oh!
[00:00:01.200]
{textBox: 10, 40, 30, 240}D'Oh!
[00:00:01.300]
{textBox: 10, 60, 30, 240}D'Oh!
[00:00:01.400]
{textBox: 10, 80, 30, 240}D'Oh!
[00:00:01.500]

The text importer inspects the text descriptors found within the braces and creates the movie whose first frame is shown in Figure 11.


Figure 11. The imported movie.

Unfortunately, we don't have space to investigate text descriptors in more detail here. For complete documentation on using the available text descriptors, see the sources mentioned at the end of this article.

Text Tracks

As we've seen, the text movie importer provides our applications with a good deal of text-handling power at a very small cost. In fact, we didn't have to do anything at all to allow our applications to import text files, and we simply had to upgrade our Edit menu adjusting code to allow them to handle pasted text. But we still need to see how to create text tracks directly, without relying on the text movie importer. After all, we want to be able to work with text data that's not read from a file or from the system scrap.

Adding Text Media Samples

By this point in this series of articles, programmatically adding a track to a movie should be old-hat (since we've done this two or three times so far). We just need to call NewMovieTrack and NewTrackMedia to create a new track and media, call BeginMediaEdits to begin a media-editing session, call AddMediaSample to add samples to the media, call EndMediaEdits to end the media-editing session, and then call InsertMediaIntoTrack to insert the newly-edited media into the track. For any new kind of media that we encounter, we really need to ask only two questions: (1) what is the format of the data in the media samples? And, (2) what is the structure of the sample description that we need to pass to AddMediaSample?

For a text track, the media sample data is just the string of characters in the text itself, preceded by a 16-bit length field that specifies the number of characters in that string. And the appropriate sample description is a text description structure, defined by the TextDescription data type:

struct TextDescription {
   long                                          descSize;
   long                                          dataFormat;
   long                                          resvd1;
   short                                       resvd2;
   short                                       dataRefIndex;
   long                                          displayFlags;
   long                                          textJustification; 
   RGBColor                                 bgColor;
   Rect                                          defaultTextBox;
   ScrpSTElement                           defaultStyle;
   char                                          defaultFontName[1];
};

The first five fields, of course, are the first five fields of the generic SampleDescription structure. The remaining fields are specific to text media. The displayFlags field holds a set of flags that indicate how the text is to be displayed. These flags allow us to specify various scrolling options and other positioning options. For the moment, we'll be content to specify the dfClipToTextBox flag, which restricts any updates caused by changes in the text track to the area occupied by the text track. (By all means, however, you should experiment with some of the scrolling options, like dfScrollIn and dfScrollOut.)

The defaultTextBox field specifies the location of the box that encloses the text. The rectangle is interpreted as relative to the upper-left corner of the text track rectangle. The textJustification field contains a value that specifies how the text is to be justified within the text box. The Movie Toolbox recognizes these constants for specifying a text justification (defined in the header file TextEdit.h):

enum {
   teFlushDefault                  = 0,
   teCenter                           = 1,
   teFlushRight                     = -1,
   teFlushLeft                        = -2
};

The bgColor field specifies the background color of the text box. The default text color is black. Note that because we call NewHandleClear to allocate a TextDescription structure, the default background color will also be black unless we change the values in the bgColor field. To make the text visible, we'll set the background color to white, like this:

RGBColor                     myBGColor = {0xffff, 0xffff, 0xffff};

(**mySampleDesc).bgColor = myBGColor;

The last two fields of the TextDescription structure indicate the desired text style and font. We'll ignore these fields here.

Listing 5 shows a segment of the QTText_AddTextTrack function, which we use to add a new text track to a movie. As you can see, it allocates a handle to a text description structure, fills in some of the fields with appropriate values, calls PtrToHand and PtrAndHand to create the text media sample, and then calls AddMediaSample to add the text media sample to the text media.

Listing 5: Adding a text media sample

QTText_AddTextTrack

TextDescriptionHandle   mySampleDesc = NULL;
Handle                        mySample = NULL;
UInt16                        myLength;
RGBColor                     myBGColor = {0xffff, 0xffff, 0xffff};

mySampleDesc = (TextDescriptionHandle)
                           NewHandleClear(sizeof(TextDescription));
if (mySampleDesc == NULL)
   goto bail;
               
(**mySampleDesc).descSize = sizeof(TextDescription);
(**mySampleDesc).dataFormat = TextMediaType;
(**mySampleDesc).displayFlags = dfClipToTextBox;
(**mySampleDesc).textJustification = teCenter;
(**mySampleDesc).defaultTextBox = myBounds;
(**mySampleDesc).bgColor = myBGColor;
      
myLength = EndianU16_NtoB(mySampleText[0]);   
   
// create the text media sample: a 16-bit length word followed by the text
myErr = PtrToHand(&myLength, &mySample, sizeof(myLength));
if (myErr == noErr) {
   myErr = PtrAndHand((Ptr)(&mySampleText[1]), mySample, 
                                                         mySampleText[0]);
   if (myErr == noErr)
      AddMediaSample(   myMedia, mySample, 0,
                           GetHandleSize(mySample),
                           myTextSampleDuration,
                           (SampleDescriptionHandle)mySampleDesc,
                           1, 0, NULL);
   DisposeHandle(mySample);
}
            
DisposeHandle((Handle)mySampleDesc);

The Movie Toolbox also provides the TextMediaAddTextSample function, which allows us to simplify this process significantly. Indeed, all of the work done in Listing 5 can be accomplished with this single line of code:

myErr = TextMediaAddTextSample(
                                 myHandler,
                                 (Ptr)(&mySampleText[1]),
                                 mySampleText[0],
                                 0,
                                 0,
                                 0,
                                 NULL,
                                 NULL,
                                 teCenter,
                                 &myBounds,
                                 dfClipToTextBox,
                                 0,
                                 0,
                                 0,
                                 NULL,
                                 myTextSampleDuration,
                                 NULL);

TextMediaAddTextSample takes seventeen parameters (count 'em!), which is probably some kind of record for a Movie Toolbox function. The payoff for this complexity is that it allows us to dispense with allocating a sample description or a text sample and with worrying about endian issues. Instead, we pass it the text media handler, myHandler (which we can obtain by calling GetMediaHandler on the text media), the text, and a handful of other parameters describing the desired characteristics of the text track.

Positioning a Text Track

When we create a text track, using either AddMediaSample or TextMediaAddTextSample, we need to specify the size and location of the text track. QTText determines the width of the new text track by calling GetTrackDimensions on the first video track in the movie:

GetTrackDimensions(myTypeTrack, &myWidth, &myHeight);

QTText uses the constant kTextTrackHeight (defined as 20 pixels) as the height of the text track.

For below-the-video text, we can specify the position of the text track by setting the track matrix, like this:

GetTrackMatrix(myTextTrack, &myMatrix);
TranslateMatrix(&myMatrix, 0, myHeight);
SetTrackMatrix(myTextTrack, &myMatrix);

All we've done here is translate the matrix downward by the height of the video track (myHeight). For text that overlays a video track, of course, we'll need to reset the matrix in some other way.

Enabling or Disabling a Text Track

Each track in a QuickTime movie is either enabled or disabled. By default, a newly-created track is enabled, in which case its media data directly contributes to the overall user experience. For example, an enabled video track is visible (unless of course it's completely covered by other enabled tracks), and an enabled audio track is audible. Most other media types, including text media, are visual media types, so once again being enabled means being visible. On the flip side, a disabled track does not usually contribute audible or visible data to the movie. Disabling a track is a quick and easy way to hide or mute it.

We can enable or disable a track by calling the SetTrackEnabled function, passing it a track identifier and a Boolean value that indicates whether to enable (true) or disable (false) the specified track. When we create a text track, we make sure it's visible by enabling it, like this:

SetTrackEnabled(myTextTrack, true);

We can hide the text track by passing false to disable it. Even if a text track is disabled, however, it can still be of use in a movie. For instance, we can search for text in a disabled text track, and the movie controller scans all text tracks, including disabled ones, when looking for chapter tracks. Similarly, the QuickTime Plug-In searches all text tracks, even disabled ones, when looking for an HREF track. Indeed, chapter tracks and HREF tracks are usually disabled.

Creating a Text Track

Listing 6 shows our complete function QTText_AddTextTrack for adding a text track to a movie. The parameter theStrings is an array of C strings; each element of that array is the text for a specific text sample. The parameter theFrames is an array of integers; each element of that array indicates how many video frames a text sample is to span. The sum of all the values in theFrames should equal the total number of frames in the video track. Finally, the isChapterTrack parameter indicates whether the new text track is to be a chapter track; if isChapterTrack is true, then the new text track is attached as a chapter track to the first track whose type is specified by the theType parameter.

Listing 6: Adding a text track

QTText_AddTextTrack

Track QTText_AddTextTrack (Movie theMovie, 
   char *theStrings[], short theFrames[], short theNumFrames, 
   OSType theType, Boolean isChapterTrack)
{
   Track                  myTypeTrack = NULL;
   Track                  myTextTrack = NULL;
   Media                  myMedia = NULL;
   MediaHandler         myHandler = NULL;
   TimeScale            myTimeScale;
   MatrixRecord         myMatrix;
   Fixed                  myWidth;
   Fixed                  myHeight;
   OSErr                  myErr = noErr;

   // get the (first) track of the specified type; 
   // this track determines the width of the new text track
   // and (if isChapterTrack is true) is the target of the new chapter track
   myTypeTrack = GetMovieIndTrackType(theMovie, 1, 
                           theType, movieTrackMediaType);
   if (myTypeTrack == NULL)
      goto bail;
   
   // get the dimensions of the target track
   GetTrackDimensions(myTypeTrack, &myWidth, &myHeight);
   myTimeScale = GetMediaTimeScale
                           (GetTrackMedia(myTypeTrack));
   
   // create the text track and media
   myTextTrack = NewMovieTrack(theMovie, myWidth, 
                        FixRatio(kTextTrackHeight, 1), kNoVolume);
   if (myTextTrack == NULL)
      goto bail;
      
   myMedia = NewTrackMedia(myTextTrack, TextMediaType, 
                           myTimeScale, NULL, 0);
   if (myMedia == NULL)
      goto bail;
      
   myHandler = GetMediaHandler(myMedia);
   if (myHandler == NULL)
      goto bail;
   
   // figure out the text track geometry
   GetTrackMatrix(myTextTrack, &myMatrix);
   TranslateMatrix(&myMatrix, 0, myHeight);
   
   SetTrackMatrix(myTextTrack, &myMatrix);
   SetTrackEnabled(myTextTrack, true);
   
   // edit the track media
   myErr = BeginMediaEdits(myMedia);
   if (myErr == noErr) {
      Rect                  myBounds;
      short               myIndex;
      TimeValue         myTypeSampleDuration;
      TimeRecord         myTimeRec;
      
      myBounds.top = 0;
      myBounds.left = 0;
      myBounds.right = Fix2Long(myWidth);
      myBounds.bottom = Fix2Long(myHeight);
      
      // determine the duration of a sample in the track of the specified type
      myTypeSampleDuration = 
                        QTUtils_GetFrameDuration(myTypeTrack);
            

      for (myIndex = 0; myIndex < theNumFrames; myIndex++) {
         TimeValue      myTextSampleDuration;
         Str255            mySampleText;

         myTextSampleDuration = myTypeSampleDuration * 
                                             theFrames[myIndex];
         
         // set the time scale of the media to that of the movie
         myTimeRec.value.lo = myTextSampleDuration;
         myTimeRec.value.hi = 0;
         myTimeRec.scale = GetMovieTimeScale(theMovie);
         ConvertTimeScale(&myTimeRec, 
                                          GetMediaTimeScale(myMedia));
         myTextSampleDuration = myTimeRec.value.lo;

         QTText_CopyCStringToPascal(theStrings[myIndex], 
                                             mySampleText);

         // Listing 5 omitted at this point, for space reasons

         // write out the new data to the media
         myErr = TextMediaAddTextSample(   
                                 myHandler,
                                 (Ptr)(&mySampleText[1]),
                                 mySampleText[0],
                                 0,
                                 0,
                                 0,
                                 NULL,
                                 NULL,
                                 teCenter,
                                   &myBounds,
                                 dfClipToTextBox,
                                 0,
                                 0,
                                 0,
                                 NULL,
                                 myTextSampleDuration,
                                 NULL);
      }
   }

   myErr = EndMediaEdits(myMedia);
   if (myErr != noErr)
      goto bail;
   
   // insert the text media into the text track
   myErr = InsertMediaIntoTrack(myTextTrack, 0, 0, 
                                 GetMediaDuration(myMedia), fixed1);
   if (myErr != noErr)
      goto bail;

   // set the text handling procedure
   TextMediaSetTextProc(myHandler, gTextProcUPP, 
                  (long)QTFrame_GetWindowObjectFromFrontWindow());

// if desired, set the new text track as a chapter track for the track of the specified type
   if (isChapterTrack)
      AddTrackReference(myTypeTrack, myTextTrack, 
                                    kTrackReferenceChapterList, NULL);

bail:
   return(myTextTrack);
}


For the moment, you can ignore the calls to TextMediaSetTextProc and AddTrackReference. We'll explain them a little later.

Text Searching

The Movie Toolbox provides several functions that we can use to search for a specific word or series of words in a text track. If we are interested in simply finding out where in a text track the next occurrence of a string is located, we can use the TextMediaFindNextText function, like this:

myTimeValue = GetMovieTime(myMovie, NULL);
myErr = TextMediaFindNextText(   myHandler,
                                          (Ptr)(&theText[1]), 
                                          theText[0],
                                          myFlags,
                                          myTimeValue, 
                                          &myFoundTime,
                                          &myFoundDuration,
                                          &gOffset);

The first parameter, myHandler, is the text media handler associated with the text track. The second and third parameters specify the text to be searched for and the length of that text; here we're supposing that the text is contained in the variable theText, which is a Pascal string. The fourth parameter is a set of search flags, which indicate how TextMediaFindNextText is to search for the specified text. These flags are defined:

enum {
   findTextEdgeOK                              = 1 << 0,
   findTextCaseSensitive                     = 1 << 1,
   findTextReverseSearch                     = 1 << 2,
   findTextWrapAround                        = 1 << 3,
   findTextUseOffset                           = 1 << 4
};

These constants are pretty much self-explanatory, except for the first and the last. If findTextEdgeOK is set in the search flags, then TextMediaFindNextText will match text beginning at the movie time specified by the fifth parameter; otherwise, the text must occur in some later (or earlier, if findTextReverseSearch is set) sample. If findTextUseOffset is set, then TextMediaFindNextText will search beginning at the offset specified by the last parameter. This allows us to find separate occurrences of the search text in a single text sample.

Our QTText sample application maintains a couple of global variables that keep track of the kind of search the user wants to perform. We'll use those variables to set our search flags like this:

myFlags = findTextUseOffset;
if (!gSearchForward)
   myFlags |= findTextReverseSearch;
if (gSearchWrap)
   myFlags |= findTextWrapAround;
if (gSearchWithCase)
   myFlags |= findTextCaseSensitive;

If TextMediaFindNextText finds the text specified by the second and third parameters in some text sample, it returns the movie time of the beginning of that sample in the sixth parameter (here, &myFoundTime). It also returns the duration of that sample in the seventh parameter and, in the last parameter, the byte offset (from the beginning of the text portion of that sample) of the first character of that text.

Typically, we don't just want to find out where some text begins; we also want to advance the movie to that point and highlight the found text. We can use the MCDoAction function with the mcActionGoToTime action to set the current movie time to the time returned to us by TextMediaFindNextText, like so:

myNewTime.value.hi = 0;
myNewTime.value.lo = myFoundTime;
myNewTime.scale = GetMovieTimeScale(myMovie);
myNewTime.base = NULL;
                  
// go to the found text   
MCDoAction(myMC, mcActionGoToTime, &myNewTime);

And we can use the TextMediaHiliteTextSample function to highlight the selected text:

myColor.red = myColor.green = myColor.blue = 0x8000;  // grey
TextMediaHiliteTextSample(myHandler, myFoundTime, gOffset,       gOffset + theText[0], &myColor);

Once again, however, the Movie Toolbox provides a function that greatly simplifies our work here. The MovieSearchText function, introduced in QuickTime version 2.0, finds the text, sets the movie time to the beginning of the text sample containing that text, and highlights the found text in that sample. So we can replace all the code we've encountered so far in this section with this single line of code:

myErr = MovieSearchText(   myMovie,
                                    (Ptr)(&theText[1]), 
                                    theText[0],
                                    myFlags,
                                    NULL,
                                    &myTimeValue, 
                                    &gOffset);

When we call MovieSearchText, we pass in the movie to search, the search text and search text length, a set of flags, the first text track to search, and the movie time at which to start the search. The set of flags can include any of the search flags listed above, as well as any of these additional flags that are specific to the MovieSearchText function:

enum {
   searchTextDontGoToFoundTime            = 1L << 16,
   searchTextDontHiliteFoundText         = 1L << 17,
   searchTextOneTrackOnly                  = 1L << 18,
   searchTextEnabledTracksOnly            = 1L << 19
};

Including either of the first two flags, searchTextDontGoToFoundTime and searchTextDontHiliteFoundText, allows us to override the default "go-to-and-highlight" behavior of MovieSearchText. The next two flags modify the track-searching behavior. If we pass a track identifier in the fifth parameter, then MovieSearchText will search only that track if the searchTextOneTrackOnly flag is set; otherwise, it will search all text tracks in the specified movie, starting with that track. We can restrict the search to all enabled text tracks by setting the searchTextEnabledTracksOnly flag.

If MovieSearchText finds the specified text, it returns the movie time of the text sample in which the text was found and the byte offset within that sample of the found text. It also returns the track identifier of the track containing the text sample, unless the track parameter was set to NULL on input.

Listing 7 contains the definition of our function QTText_FindText, which we use to search for text. As you can see, it uses either the TextMediaFindNextText function or the MovieSearchText function, depending on the value of the compiler flag USE_MOVIESEARCHTEXT.

Listing 7: Finding some text

QTText_FindText

void QTText_FindText (WindowObject theWindowObject, 
                                                            Str255 theText) 
{
   ApplicationDataHdl         myAppData = NULL;
   Movie                           myMovie = NULL;
   MediaHandler                  myHandler = NULL;
   MovieController               myMC = NULL;
   long                              myFlags = 0L;
   TimeValue                     myTimeValue;
   OSErr                           myErr = noErr;
      
   myAppData = (ApplicationDataHdl)
            QTFrame_GetAppDataFromWindowObject(theWindowObject);
   if (myAppData == NULL)
      return;
      
   myMC = (**theWindowObject).fController;
   myMovie = (**theWindowObject).fMovie;
   myHandler = (**myAppData).fTextHandler;
   
   // set the search features
   myFlags = findTextUseOffset;
   if (!gSearchForward)
      myFlags |= findTextReverseSearch;   
   if (gSearchWrap)
      myFlags |= findTextWrapAround;
   if (gSearchWithCase)
      myFlags |= findTextCaseSensitive;

   myTimeValue = GetMovieTime(myMovie, NULL);

#if USE_MOVIESEARCHTEXT
   myFlags |= searchTextEnabledTracksOnly;
   
   myErr = MovieSearchText(myMovie, (Ptr)(&theText[1]), 
            theText[0], myFlags, NULL, &myTimeValue, &gOffset);
   if (myErr != noErr)
      QTFrame_Beep();      // if the desired string wasn't found, beep
#else
   if (myHandler != NULL) {
      TimeValue   myFoundTime, myFoundDuration;
      TimeRecord   myNewTime;
      RGBColor   myColor;
      
   myColor.red = myColor.green = myColor.blue = 0x8000;// grey
      
      // search for the specified text
      myErr = TextMediaFindNextText(myHandler, 
         (Ptr)(&theText[1]), theText[0], myFlags, myTimeValue, 
            &myFoundTime, &myFoundDuration, &gOffset);   
      if (myFoundTime != -1) {
         // convert the TimeValue to a TimeRecord
         myNewTime.value.hi = 0;
         myNewTime.value.lo = myFoundTime;
         myNewTime.scale = GetMovieTimeScale(myMovie);
         myNewTime.base = NULL;
                  
         // go to the found text   
         MCDoAction(myMC, mcActionGoToTime, &myNewTime);

         // highlight the text
         TextMediaHiliteTextSample(myHandler, myFoundTime, 
                        gOffset, gOffset + theText[0], &myColor);
         
      } else {
         QTFrame_Beep();      // if the desired string wasn't found, beep
      }
   }
#endif

   // update the current offset, if we're searching forward
   if (gSearchForward && (myErr == noErr))
      gOffset += theText[0];
}


Of course, your code won't need to use this compiler flag; you'll call just MovieSearchText or TextMediaFindNextText for your text searching. Here we simply want to illustrate how to call both of these functions.

Text Editing

Let's consider now how to edit the data in a text track. Conceptually, this is a fairly simple operation. We can just call DeleteTrackSegment to delete one or more existing text samples from a track; then we can call TextMediaAddTextSample to add a new text sample to the text media and then InsertMediaIntoTrack to place that text sample at the desired location in the track. For the moment, we'll limit ourselves to replacing a single existing text sample by another sample that occupies the same location in the track (that is, that has the same starting point and duration as the original sample).

When the user selects the "Edit Current Text..." menu item, we'll display the dialog box shown in Figure 12. If the user clicks the OK button, we'll retrieve the text from the edit text control in that dialog box and use that text as the replacement text data. There are only two things we still need to figure out: (1) how can we get the text of the current text sample (to put into the dialog box when it's first displayed)? And, (2) how can we determine the starting time and duration of the current text sample? Let's take these tasks in order.


Figure 12. The Edit Text dialog box Getting the Current Text

The first task is the easier of the two, mainly because whenever the text media handler is about to display a new text sample, it calls an application-defined text callback procedure that we've previously installed by calling TextMediaSetTextProc (in Listing 6). The text callback procedure is passed several parameters, one of which is a handle to the sample data of the current media sample. So all we need to do is make a copy of the sample text in a place we can find it when we are about to display the dialog box shown in Figure 12. Listing 8 shows our application's text callback procedure.

Listing 8: Getting the current text

QTText_TextProc

PASCAL_RTN OSErr QTText_TextProc (Handle theText, 
      Movie theMovie, short *theDisplayFlag, long theRefCon)
{
#pragma unused(theMovie, theRefCon)
   char            *myTextPtr = NULL;
   short         myTextSize;
   short         myIndex;
   
   // on entry to this function, theText is a handle to the text sample data,
   // which is a big-endian 16-bit length word followed by the text itself
   myTextSize = EndianU16_BtoN(*(short *)(*theText));
   myTextPtr = (char *)(*theText + sizeof(short));

   // copy the text into our global variable
   for (myIndex = 1; myIndex <= myTextSize; 
                                             myIndex++, myTextPtr++)
      gSampleText[myIndex] = *myTextPtr;

   gSampleText[0] = myTextSize;
   
   // ask for the default text display
   *theDisplayFlag = txtProcDefaultDisplay;

   return(noErr);
}



As you can see, we first parse the sample data to get the 16-bit length field and the location of the first character in the text string. Then we copy the characters into the global variable gSampleText, which is of type Str255. Finally, we return the value txtProcDefaultDisplay in the parameter theDisplayFlag; this instructs the text media handler to use the display flags contained in the displayFlags field of the text description structure for that text sample. (There are also constants to force the sample to be shown or not shown, regardless of the media's default display flags.)

Finding Sample Boundaries

Now we need to figure out how to find the starting time and duration of the current text media sample (so we know what segment of the text track to replace). An easy way to get the starting time would be to call GetMovieTime in our text callback procedure and then assign the returned value to a global variable. A better way — because it can be used with media types other than text — is to call the GetTrackNextInterestingTime function inside the QTText_EditText function. GetTrackNextInterestingTime allows us to search for specific times in a track, given a set of search criteria. The search criteria are specified by these flags:

enum {
   nextTimeMediaSample                     = 1 << 0,
   nextTimeMediaEdit                        = 1 << 1,
   nextTimeTrackEdit                        = 1 << 2,
   nextTimeSyncSample                     = 1 << 3,
   nextTimeStep                              = 1 << 4,
   nextTimeEdgeOK                           = 1 << 14,
   nextTimeIgnoreActiveSegment         = 1 << 15
};

For present purposes, we'll use the two flags nextTimeMediaSample and nextTimeEdgeOK, which tell GetTrackNextInterestingTime to search in the next sample in the track's media but to consider samples that begin or end at the search starting time.

We'll begin by getting the current movie time (which might not be the beginning of the current text sample), like this:

myMovieTime = GetMovieTime(myMovie, NULL);

Then we want to search backward to find the beginning of the current media sample:

GetTrackNextInterestingTime(
                              myTrack, 
                              nextTimeEdgeOK | nextTimeMediaSample, 
                              myMovieTime,
                               -fixed1, 
                              &myInterestingTime, 
                              NULL);

The third parameter specifies the starting time for the search and the fourth parameter indicates the direction of the search; because the value here is negative, the search goes backwards from the current movie time. Once GetTrackNextInterestingTime finds the beginning of the current media sample, it returns that time in the location pointed to by the fifth parameter. We've set the sixth parameter to NULL because we don't need the duration from the current time to the found interesting time to be returned to us.

So we've found the beginning of the current text sample. We can find the duration of that sample by calling GetTrackNextInterestingTime once more, this time searching forward from the beginning of the sample, like this:

myMovieTime = myInterestingTime;
GetTrackNextInterestingTime(
                              myTrack, 
                              nextTimeEdgeOK | nextTimeMediaSample, 
                              myMovieTime,
                               fixed1, 
                              NULL, 
                              &myDuration);

In this case, we want only the duration of the sample returned to us, so we pass NULL in the fifth parameter and &myDuration in the sixth.

Keep in mind that the time values that GetTrackNextInterestingTime returns to us are in the movie time scale. This is useful, since the parameters to DeleteTrackSegment must also be in the movie time scale. So we can now call DeleteTrackSegment to remove the current text sample from the track:

myErr = DeleteTrackSegment(myTrack, myInterestingTime, 
                                                   myDuration);

All that remains is to add a new text sample in place of the one we just removed. For this, we can call TextMediaAddTextSample as we did earlier. There is only one complication here: the duration we pass to TextMediaAddTextSample must be expressed in the media time scale, not the movie time scale. But the Movie Toolbox conveniently provides the MediaTimeToSampleNum function that we can use to get the start time and duration of the current media sample in the media time scale, like this:

myMovieTime = GetMovieTime(myMovie, NULL);
myMediaCurrentTime = TrackTimeToMediaTime
                                          (myMovieTime, myTrack);
MediaTimeToSampleNum(
                        myMedia, 
                        myMediaCurrentTime, 
                        &myMediaSampleIndex, 
                        &myMediaSampleStartTime,
                        &myMediaSampleDuration);

So, we've got all the information we need to call TextMediaAddTextSample and InsertMediaIntoTrack and thereby complete the text sample editing operation. For the complete definition of QTText_EditText, see the file QTText.c.

Chapter Tracks

We learned earlier that a chapter track is just a text track that has been associated in a particular way with some other track. Let's call this other track the target track. We create the association between a text track and the target track by creating a track reference from that target to the text track. In general, a track reference is simply a way for one track to establish a relationship with some other track. The type of the track reference indicates the nature of that relationship. The Movie Toolbox currently provides three constants for track reference types:

enum {
   kTrackReferenceChapterList      = FOUR_CHAR_CODE('chap'),
   kTrackReferenceTimeCode            = FOUR_CHAR_CODE('tmcd'),
   kTrackReferenceModifier            = FOUR_CHAR_CODE('ssrc')
};

A track reference of type kTrackReferenceChapterList is used to create a chapter track. A track reference of type kTrackReferenceTimeCode is used to create a timecode track, in which timecode values are associated with the samples of the target track. (We'll consider timecode tracks in more detail in the next QuickTime Toolkit article.) A track reference of type kTrackReferenceModifier is used to create a modifier track; modifier tracks are useful when you want one track to modify the appearance or behavior of a target track. For instance, a tween track is a kind of modifier track that can be used to change, say, the volume of a sound track as the movie progresses. We'll encounter modifier tracks in several upcoming articles, to change the current image of a sprite track and to apply a special effect to a video track. Other parts of QuickTime define additional types of track references. For example, the file QuickTimeVRFormat.h defines several types of track references that are used in building QuickTime VR movie files.

It's actually a rather trivial operation to create a chapter track once we have a text track at hand. If myTypeTrack is a track identifier for a target track (in the present case, a video track), then we can create a chapter track reference to our text track like this:

AddTrackReference(myTypeTrack, myTextTrack, 
                              kTrackReferenceChapterList, NULL);

The last parameter is a pointer to a long word in which AddTrackReference will return the index assigned to the new track reference; we don't need this information, so we set that parameter to NULL.

All the chapter titles must be contained in a single text track; we specify the starting time for chapters when we add the text to the text track by calling TextMediaAddTextSample. Note that we need to create the chapter association only between the text track and one other target track, not between the text track and all other tracks in the movie. The target track must be enabled, but typically the chapter track is not enabled (unless we want the text track to be visible).

It's also very easy to remove a track reference and hence to change a chapter track back into a non-chapter text track. Again, if myTypeTrack is the target track, then we can disassociate it from the text track by calling DeleteTrackReference, like this:

DeleteTrackReference(myTypeTrack, 
                              kTrackReferenceChapterList, 1);

Here the last parameter is the index of the track reference of the specified type that we want to remove. Our code attaches at most one chapter track to a target, so we can safely set that index to 1.

Listing 9 shows our complete function for turning a text track into a chapter track or turning a chapter track back into a text track. The Boolean parameter isChapterTrack determines whether the first text track in the movie becomes a chapter track or is demoted from that lofty rank.

Listing 9: Setting and unsetting chapter tracks

QTText_SetTextTrackAsChapterTrack

OSErr QTText_SetTextTrackAsChapterTrack 
      (WindowObject theWindowObject, OSType theType, 
                                             Boolean isChapterTrack)
{
   ApplicationDataHdl      myAppData = NULL;
   Movie                        myMovie = NULL;
   MovieController            myMC = NULL;
   Track                        myTypeTrack = NULL;
   Track                        myTextTrack = NULL;
   OSErr                        myErr = paramErr;
      
   // get the movie, controller, and related stuff
   myAppData = (ApplicationDataHdl)
            QTFrame_GetAppDataFromWindowObject(theWindowObject);
   if (myAppData == NULL)
      return(myErr);
      
   myMovie = (**theWindowObject).fMovie;
   myMC = (**theWindowObject).fController;
   myTextTrack = (**myAppData).fTextTrack;
   
   if ((myMovie != NULL) && (myMC != NULL)) {
      myTypeTrack = GetMovieIndTrackType(myMovie, 1, theType,
                     movieTrackMediaType | movieTrackEnabledOnly);
      if ((myTypeTrack != NULL) && (myTextTrack != NULL)) {
         // add or delete a track reference, as determined by the desired final state
         if (isChapterTrack)
            myErr = AddTrackReference(myTypeTrack, myTextTrack, 
                                    kTrackReferenceChapterList, NULL);
         else
            myErr = DeleteTrackReference(myTypeTrack, 
                                    kTrackReferenceChapterList, 1);
            
         // tell the movie controller we've changed aspects of the movie
         MCMovieChanged(myMC, myMovie);
         
         // stamp the movie as dirty
         (**theWindowObject).fIsDirty = true;
      }
   }

   return(myErr);
}


Note that after we call AddTrackReference or DeleteTrackReference, we need to call MCMovieChanged to inform the movie controller that we've changed the associated movie. This prompts the movie controller to redraw the movie controller bar to show or hide the chapter pop-up menu.

The file QTText.c contains a number of other chapter track utilities. Listing 10 defines the one we use for determining whether a track is a chapter track; we call this function when we need to determine whether to place a check mark next to the "Chapter Track" menu item.

Listing 10: Determining whether a track is a chapter track

QTText_IsChapterTrack

Boolean QTText_IsChapterTrack (Track theTrack)
{
   Movie            myMovie = NULL;
   Track            myTrack = NULL;
   long               myTrackCount = 0L;
   long               myTrRefCount = 0L;
   long               myTrackIndex;
   long               myTrRefIndex;

   myMovie = GetTrackMovie(theTrack);
   if (myMovie == NULL)
      return(false);

   myTrackCount = GetMovieTrackCount(myMovie);
   for (myTrackIndex = 1; myTrackIndex <= myTrackCount; 
                                                         myTrackIndex++) {
      myTrack = GetMovieIndTrack(myMovie, myTrackIndex);
      if ((myTrack != NULL) && (myTrack != theTrack)) {
      
         // iterate thru all track references of type kTrackReferenceChapterList
         myTrRefCount = GetTrackReferenceCount(myTrack, 
                                          kTrackReferenceChapterList);
         for (myTrRefIndex = 1; myTrRefIndex <= myTrRefCount; 
                                                         myTrRefIndex++) {
            Track   myRefTrack = NULL;

            myRefTrack = GetTrackReference(myTrack, 
                        kTrackReferenceChapterList, myTrRefIndex);
            if (myRefTrack == theTrack)
               return(true);
         }
      }
   }

   return(false);
}

Hypertext Reference Tracks

A hypertext reference track, or HREF track, is a text track in which some or all of the samples contain hypertext links, in the form of URLs. (Actually, there's no requirement that any of the samples in an HREF track contain a hypertext link, but then of course it's not very useful.) These URLs can be any kind of URL supported by QuickTime, including HTTP, HTTPS, FTP, file, RTSP, and JavaScript URLs. Indeed, if the QuickTime Plug-In finds a URL it doesn't recognize, it passes it to the web browser for processing. So, really, the sky's the limit in terms of the kind of URLs we can put in an HREF track.

From a programming perspective, creating an HREF track is even easier than creating a chapter track. All we need to do is set the name of a text track to "HREFTrack". The plug-in interprets the first text track in a movie having that name as the active HREF track. Listing 11 defines the function QTText_SetTextTrackAsHREFTrack that we can use to set and unset a text track as an HREF track.

Listing 11: Setting and unsetting HREF tracks

QTText_SetTextTrackAsHREFTrack

OSErr QTText_SetTextTrackAsHREFTrack 
                              (Track theTrack, Boolean isHREFTrack)
{
   OSErr      myErr = noErr;
   
   myErr = QTUtils_SetTrackName(theTrack, 
            isHREFTrack ? kHREFTrackName : kNonHREFTrackName);

   return(myErr);
}

A track's name is stored as part of the track's user data, so QTUtils_SetTrackName (defined in QTUtilities.c) calls SetUserDataItem to set the name. In QTText_SetTextTrackAsHREFTrack, we use these constants for the track names:

#define kHREFTrackName         "HREFTrack"
#define kNonHREFTrackName      "Text Track"

Ideally, each track should have a unique name (though this is not required). So instead of hard-coding the name for the non-HREF track, we can generate a track name dynamically, looking at the names that are already assigned to tracks in the movie. QTUtilities.c defines a function, QTUtils_MakeTrackNameByType, that we can call to accomplish this. Reworking QTText_SetTextTrackAsHREFTrack to use QTUtils_MakeTrackNameByType is left as an exercise for the reader.

Occasionally it's useful to know whether a specified text track is an HREF track. (For instance, QTText needs to know this to decide whether to put a check mark beside the "HREF Track" menu item.) The function QTText_IsHREFTrack, defined in Listing 12, returns a Boolean value that indicates whether a given text track is an HREF track.

Listing 12: Determining whether a track is an HREF track

QTText_IsHREFTrack

Boolean QTText_IsHREFTrack (Track theTrack)
{
   Boolean      isHREFTrack = false;
   char          *myTrackName = NULL;
   
   myTrackName = QTUtils_GetTrackName(theTrack);
   if (myTrackName != NULL)
   isHREFTrack = (strcmp(myTrackName, kHREFTrackName) == 0);
   
   free(myTrackName);
   return(isHREFTrack);
}

Some Loose Ends

Let's finish up by taking care of a few loose ends in our basic application framework that become apparent when we start working with text tracks. As you know, when we paste some data into a movie, the current movie time is set to the time immediately following the pasted data. (This is the standard behavior with any kind of pasting.) If pasting causes the movie box to expand, it might happen that the expanded portion of the movie box in the current frame contains areas that should be erased but which are not erased by the movie controller. For example, if we've added some text in parallel, we might see something like Figure 13. Here, the video media handler has redrawn the video portion of the movie box but the text media handler, thinking (correctly) that there's no text for the current movie time, has left the text portion of the movie box untouched. As a result, the image of the movie controller bar, which used to occupy the space now occupied by the text track, is not erased. This is not good, but it's easy enough to fix.


Figure 13. A movie window after adding in parallel.

When some part of the movie box needs to be redrawn, an update event is generated for that portion of the movie box. When we pass that event to MCIsPlayerEvent, the movie controller redraws the appropriate portion of the movie and clears that area from the update region of the window. The problem, as we've just seen, is that the movie controller doesn't think that the bottom portion needs to be redrawn and hence doesn't redraw it. We can solve this problem by erasing that portion of the window ourselves. Listing 13 shows our updated version of QTApp_Draw.

Listing 13: Redrawing a movie window

QTApp_Draw

void QTApp_Draw (WindowReference theWindow)
{
   GrafPtr         mySavedPort = NULL;
   GrafPtr         myWindowPort = NULL;
   WindowPtr      myWindow = NULL;
   Rect               myRect;
   
   GetPort(&mySavedPort);
   myWindowPort = 
                  QTFrame_GetPortFromWindowReference(theWindow);
myWindow = QTFrame_GetWindowFromWindowReference(theWindow);
   
   if (myWindowPort == NULL)
      return;
      
   MacSetPort(myWindowPort);
   
#if TARGET_API_MAC_CARBON
   GetPortBounds(myWindowPort, &myRect);
#else
   myRect = myWindowPort->portRect;
#endif

   BeginUpdate(myWindow);

   if (QTFrame_IsDocWindow(theWindow))
      EraseRect(&myRect);

   // ***insert application-specific drawing here***
   
   EndUpdate(myWindow);
   MacSetPort(mySavedPort);
}


As you can see, we call EraseRect on the entire window rectangle. Keep in mind, however, that BeginUpdate limits the redrawn portion to the intersection of the visible region of the window and the current update region. Since MCIsPlayerEvent will already have removed the active movie region from the update region, our call to EraseRect just redraws the visible portion of the update region that wasn't redrawn by the movie controller.

The last thing we need to do is make sure that the entire movie box is included in the update region when we call MCIsPlayerEvent. We can accomplish this by adding a few lines to our QTFrame_HandleEditMenuItem function. Essentially, we need to make sure to invalidate the entire movie box whenever the size of the movie box might have changed. Listing 14 shows the lines we'll add to the end of QTFrame_HandleEditMenuItem.

Listing 14: Invalidating a movie window

QTFrame_HandleEditMenuItem

   // if the size of the movie might have changed, invalidate the entire movie box
   if ((theMenuItem == IDM_EDITUNDO) || 
         (theMenuItem == IDM_EDITCUT) || 
         (theMenuItem == IDM_EDITPASTE) || 
         (theMenuItem == IDM_EDITCLEAR)) {
      Rect      myRect;
#if TARGET_OS_WIN32
      RECT      myWinRect;
#endif      
   
      MCGetControllerBoundsRect(myMC, &myRect);
#if TARGET_OS_MAC
      InvalWindowRect(QTFrame_GetWindowFromWindowReference
                                          (theWindow), &myRect);
#endif      
#if TARGET_OS_WIN32
      QTFrame_ConvertMacToWinRect(&myRect, &myWinRect);
      InvalidateRect(theWindow, &myWinRect, false);
#endif      
   }


With these changes made, we should see no glitches like those in Figure 13.

Conclusion

We've covered a fair amount of ground in this article. We've seen how to create text tracks, chapter tracks, and HREF tracks; we've also learned how to search a text track and edit the data in a text track. Along the way, we've seen how to upgrade our Edit menu item adjusting and our movie window redrawing, so that even our applications that are not directly concerned with text can import text from files or from the system scrap.

We've also learned a more general lesson: QuickTime often provides more than one way to accomplish some particular task. We've seen, for instance, that we can call either AddMediaSample or TextMediaAddTextSample to add media samples to a text track. And, we can call either TextMediaFindNextText or MovieSearchText to search for text within a text track. Which of these functions we use in any particular case is a matter of taste, no doubt, but also a matter of simplicity and code size. TextMediaAddTextSample and MovieSearchText hold the clear advantage when we consider the amount of source code we need to write and the kinds of details (like endian issues) that we need to attend to. In the future, we'll generally opt for the simpler, cleaner way of solving our programming tasks (and leave the dinosaur bones for the archeologists).

Acknowledgements and References

Thanks are due once again to Brian Friedkin, for his ever-helpful guidance on Windows-related issues. Some of the code in QTText is based on an earlier sample code package by Nick Thompson; you can find an explanation of that code in his article in develop, Issue 20 (archived at http://www.mactech.com/articles/develop/issue_20/20quicktime.html).

You can find a thorough explanation of text descriptors at http://www.apple.com/quicktime/authoring/textdescriptors.html; an even more readable account is found in Steve Gulie's indispensable book QuickTime for the Web (available at http://www.devdepot.com/ and at all good bookstores).


Tim Monroe works in the QuickTime Engineering group at Apple. You can contact him at monroe@apple.com.

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Latest Forum Discussions

See All

Summon your guild and prepare for war in...
Netmarble is making some pretty big moves with their latest update for Seven Knights Idle Adventure, with a bunch of interesting additions. Two new heroes enter the battle, there are events and bosses abound, and perhaps most interesting, a huge... | Read more »
Make the passage of time your plaything...
While some of us are still waiting for a chance to get our hands on Ash Prime - yes, don’t remind me I could currently buy him this month I’m barely hanging on - Digital Extremes has announced its next anticipated Prime Form for Warframe. Starting... | Read more »
If you can find it and fit through the d...
The holy trinity of amazing company names have come together, to release their equally amazing and adorable mobile game, Hamster Inn. Published by HyperBeard Games, and co-developed by Mum Not Proud and Little Sasquatch Studios, it's time to... | Read more »
Amikin Survival opens for pre-orders on...
Join me on the wonderful trip down the inspiration rabbit hole; much as Palworld seemingly “borrowed” many aspects from the hit Pokemon franchise, it is time for the heavily armed animal survival to also spawn some illegitimate children as Helio... | Read more »
PUBG Mobile teams up with global phenome...
Since launching in 2019, SpyxFamily has exploded to damn near catastrophic popularity, so it was only a matter of time before a mobile game snapped up a collaboration. Enter PUBG Mobile. Until May 12th, players will be able to collect a host of... | Read more »
Embark into the frozen tundra of certain...
Chucklefish, developers of hit action-adventure sandbox game Starbound and owner of one of the cutest logos in gaming, has released their roguelike deck-builder Wildfrost. Created alongside developers Gaziter and Deadpan Games, Wildfrost will... | Read more »
MoreFun Studios has announced Season 4,...
Tension has escalated in the ever-volatile world of Arena Breakout, as your old pal Randall Fisher and bosses Fred and Perrero continue to lob insults and explosives at each other, bringing us to a new phase of warfare. Season 4, Into The Fog of... | Read more »
Top Mobile Game Discounts
Every day, we pick out a curated list of the best mobile discounts on the App Store and post them here. This list won't be comprehensive, but it every game on it is recommended. Feel free to check out the coverage we did on them in the links below... | Read more »
Marvel Future Fight celebrates nine year...
Announced alongside an advertising image I can only assume was aimed squarely at myself with the prominent Deadpool and Odin featured on it, Netmarble has revealed their celebrations for the 9th anniversary of Marvel Future Fight. The Countdown... | Read more »
HoYoFair 2024 prepares to showcase over...
To say Genshin Impact took the world by storm when it was released would be an understatement. However, I think the most surprising part of the launch was just how much further it went than gaming. There have been concerts, art shows, massive... | Read more »

Price Scanner via MacPrices.net

Apple Watch Ultra 2 now available at Apple fo...
Apple has, for the first time, begun offering Certified Refurbished Apple Watch Ultra 2 models in their online store for $679, or $120 off MSRP. Each Watch includes Apple’s standard one-year warranty... Read more
AT&T has the iPhone 14 on sale for only $...
AT&T has the 128GB Apple iPhone 14 available for only $5.99 per month for new and existing customers when you activate unlimited service and use AT&T’s 36 month installment plan. The fine... Read more
Amazon is offering a $100 discount on every M...
Amazon is offering a $100 instant discount on each configuration of Apple’s new 13″ M3 MacBook Air, in Midnight, this weekend. These are the lowest prices currently available for new 13″ M3 MacBook... Read more
You can save $300-$480 on a 14-inch M3 Pro/Ma...
Apple has 14″ M3 Pro and M3 Max MacBook Pros in stock today and available, Certified Refurbished, starting at $1699 and ranging up to $480 off MSRP. Each model features a new outer case, shipping is... Read more
24-inch M1 iMacs available at Apple starting...
Apple has clearance M1 iMacs available in their Certified Refurbished store starting at $1049 and ranging up to $300 off original MSRP. Each iMac is in like-new condition and comes with Apple’s... Read more
Walmart continues to offer $699 13-inch M1 Ma...
Walmart continues to offer new Apple 13″ M1 MacBook Airs (8GB RAM, 256GB SSD) online for $699, $300 off original MSRP, in Space Gray, Silver, and Gold colors. These are new MacBook for sale by... Read more
B&H has 13-inch M2 MacBook Airs with 16GB...
B&H Photo has 13″ MacBook Airs with M2 CPUs, 16GB of memory, and 256GB of storage in stock and on sale for $1099, $100 off Apple’s MSRP for this configuration. Free 1-2 day delivery is available... Read more
14-inch M3 MacBook Pro with 16GB of RAM avail...
Apple has the 14″ M3 MacBook Pro with 16GB of RAM and 1TB of storage, Certified Refurbished, available for $300 off MSRP. Each MacBook Pro features a new outer case, shipping is free, and an Apple 1-... Read more
Apple M2 Mac minis on sale for up to $150 off...
Amazon has Apple’s M2-powered Mac minis in stock and on sale for $100-$150 off MSRP, each including free delivery: – Mac mini M2/256GB SSD: $499, save $100 – Mac mini M2/512GB SSD: $699, save $100 –... Read more
Amazon is offering a $200 discount on 14-inch...
Amazon has 14-inch M3 MacBook Pros in stock and on sale for $200 off MSRP. Shipping is free. Note that Amazon’s stock tends to come and go: – 14″ M3 MacBook Pro (8GB RAM/512GB SSD): $1399.99, $200... Read more

Jobs Board

Sublease Associate Optometrist- *Apple* Val...
Sublease Associate Optometrist- Apple Valley, CA- Target Optical Date: Apr 20, 2024 Brand: Target Optical Location: Apple Valley, CA, US, 92307 **Requisition Read more
*Apple* Systems Administrator - JAMF - Syste...
Title: Apple Systems Administrator - JAMF ALTA is supporting a direct hire opportunity. This position is 100% Onsite for initial 3-6 months and then remote 1-2 Read more
Relationship Banker - *Apple* Valley Financ...
Relationship Banker - Apple Valley Financial Center APPLE VALLEY, Minnesota **Job Description:** At Bank of America, we are guided by a common purpose to help Read more
IN6728 Optometrist- *Apple* Valley, CA- Tar...
Date: Apr 9, 2024 Brand: Target Optical Location: Apple Valley, CA, US, 92308 **Requisition ID:** 824398 At Target Optical, we help people see and look great - and Read more
Medical Assistant - Orthopedics *Apple* Hil...
Medical Assistant - Orthopedics Apple Hill York Location: WellSpan Medical Group, York, PA Schedule: Full Time Sign-On Bonus Eligible Remote/Hybrid Regular Apply Now Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.