QTText.c

//////////
//
//  File:       QTText.c
//
//  Contains:   QuickTime text media handler sample code.
//
//  Written by: Tim Monroe
//              parts based on QTTextSample code by Nick Thompson (see develop, issue 20).
//
//  Copyright:  © 1998-2000 by Apple Computer, Inc., all rights reserved.
//
//  Change History (most recent first):
//
//     <14>     07/20/00    rtm     replaced all calls to QTText_UpdateMovieAndController by MCMovieChanged;
//                                  reworked QTText_RemoveIndTextTrack to call MCMovieChanged right after
//                                  calling QTUtils_DeleteAllReferencesToTrack; this prevents crashes when
//                                  calling MCMovieChanged after QTText_RemoveIndTextTrack on Windows (this
//                                  happened when deleting a visible chapter track)
//     <13>     07/07/00    rtm     added HREFTrack support; added QTText_SyncWindowData
//     <12>     06/16/00    rtm     added call to TextMediaSetTextProc to QTText_AddTextTrack
//     <11>     06/02/00    rtm     added call to QTText_UpdateMovieAndController to QTText_EditText
//     <10>     03/17/00    rtm     made changes to get things running under CarbonLib
//     <9>      06/14/99    rtm     added a bunch more chapter track utilities
//     <8>      06/11/99    rtm     added QTText_GetChapterTrackForTrack
//     <7>      06/10/98    rtm     general clean-up; in QTText_AddTextTrack, set the time scale of the
//                                  text track media to that of the movie
//     <6>      06/08/98    rtm     fixed text track duration calculations
//     <5>      06/05/98    rtm     fixed track geometry calculations in QTText_AddTextTrack
//     <4>      06/03/98    rtm     added QTText_AddTextTrack and QTText_RemoveIndTextTrack; borrowed
//                                  QTText_CopyCStringToPascal from source file CGlue.c
//     <3>      05/29/98    rtm     added chapter track enabling/disabling
//     <2>      04/08/98    rtm     added text offset handling, so we can now search within the current sample;
//                                  added QTText_EditText to allow editing a sample's text; added compile flag
//                                  to use MovieSearchText instead of TextMediaFindNextText (see Note 3)
//     <1>      04/07/98    rtm     first file; conversion to personal coding style; updated to latest headers;
//                                  got basic searching working on Mac and Windows
//   
//  This source code shows how to do searches on text media, how to use a simple text procedure to retrieve
//  the text from a text media sample, and how to edit that text. It also shows how to convert a text track
//  into a chapter track (and vice versa), and how to add (and remove) text tracks to (and from) a movie.
//
// NOTES:
//
// *** (1) ***
// This code is based in part on the code provided with the develop article on QuickTime text by Nick Thompson
// (issue 20). Make sure to read that article for complete details on the techniques employed here. I have
// taken the liberty of reworking that code as necessary to make it run on Windows platforms and to bring it 
// into line with the other QuickTime code samples.
//
// *** (2) ***
// The editted text does NOT use the font, size, color, justification, or background color of the text
// it replaces. It would be straightforward to add this capability. See the develop article mentioned above for
// code that does all these things.
//
// *** (3) ***
// The Movie Toolbox provides two different functions that you can use to search for text in a text track: 
// TextMediaFindNextText (originally called FindNextText) and MovieSearchText. TextMediaFindNextText inspects
// only a specified track, while MovieSearchText can search all text tracks in a specified movie. Moreover,
// MovieSearchText will automatically go to and highlight the found text; these operations must be done manually
// if you're using TextMediaFindNextText. This sample code illustrates BOTH of these functions; you determine
// which is used by setting the USE_MOVIESEARCHTEXT compiler flag in QTText.h.
//
//////////
 
#include "QTText.h"
 
 
//////////
//
// global variables
//
//////////
 
Boolean                     gSearchForward = true;              // do we search forward or backward?
Boolean                     gSearchWrap = true;                 // do we wrap around when searching?
Boolean                     gSearchWithCase = false;            // is the search case sensitive?
Str255                      gSearchText;                        // the text we're searching for
Str255                      gSampleText;                        // the text of the current text media sample
long                        gOffset;                            // offset of current found text within sample
TextMediaUPP                gTextProcUPP = NULL;                // UPP to text handling procedure
 
extern ModalFilterUPP       gModalFilterUPP;
 
 
//////////
//
// QTText_InitWindowData
// Initialize any window-specific data for the text media handler.
//
//////////
 
ApplicationDataHdl QTText_InitWindowData (WindowObject theWindowObject)
{
    ApplicationDataHdl      myAppData = NULL;
    Track                   myTrack = NULL;
    MediaHandler            myHandler = NULL;
 
    myAppData = (ApplicationDataHdl)NewHandleClear(sizeof(ApplicationDataRecord));
    if (myAppData != NULL) {
    
        myTrack = GetMovieIndTrackType((**theWindowObject).fMovie, 1, TextMediaType, movieTrackMediaType | movieTrackEnabledOnly);
        if (myTrack != NULL) {
            // load the entire text track into RAM
            LoadTrackIntoRam(myTrack, 0L, GetTrackDuration(myTrack), 0L);
 
            // set the text handling procedure
            myHandler = GetMediaHandler(GetTrackMedia(myTrack));
            if (myHandler != NULL)
                TextMediaSetTextProc(myHandler, gTextProcUPP, (long)theWindowObject);
        }
    
        // remember the text track and media handler
        (**myAppData).fMovieHasText = (myTrack != NULL);
        (**myAppData).fTextIsChapter = QTText_TrackTypeHasAChapterTrack((**theWindowObject).fMovie, VideoMediaType);
        (**myAppData).fTextIsHREF = QTText_IsHREFTrack(myTrack);
        (**myAppData).fTextTrack = myTrack;
        (**myAppData).fTextHandler = myHandler;
    }
    
    return(myAppData);
}
 
 
//////////
//
// QTText_DumpWindowData
// Get rid of any window-specific data for the text media handler.
//
//////////
 
void QTText_DumpWindowData (WindowObject theWindowObject)
{
    ApplicationDataHdl      myAppData = NULL;
        
    myAppData = (ApplicationDataHdl)QTFrame_GetAppDataFromWindowObject(theWindowObject);
    if (myAppData != NULL)
        DisposeHandle((Handle)myAppData);
}
 
 
//////////
//
// QTText_SyncWindowData
// Synchronize any window-specific data for the text media handler.
//
//////////
 
void QTText_SyncWindowData (WindowObject theWindowObject)
{
    ApplicationDataHdl      myAppData = NULL;
    Track                   myTrack = NULL;
    MediaHandler            myHandler = NULL;
 
    myAppData = (ApplicationDataHdl)QTFrame_GetAppDataFromWindowObject(theWindowObject);
    if (myAppData != NULL) {
    
        myTrack = GetMovieIndTrackType((**theWindowObject).fMovie, 1, TextMediaType, movieTrackMediaType | movieTrackEnabledOnly);
        if (myTrack != NULL) {
            // load the entire text track into RAM
            LoadTrackIntoRam(myTrack, 0L, GetTrackDuration(myTrack), 0L);
 
            // set the text handling procedure
            myHandler = GetMediaHandler(GetTrackMedia(myTrack));
            if (myHandler != NULL)
                TextMediaSetTextProc(myHandler, gTextProcUPP, (long)theWindowObject);
        }
    
        // remember the text track and media handler
        (**myAppData).fMovieHasText = (myTrack != NULL);
        (**myAppData).fTextIsChapter = QTText_TrackTypeHasAChapterTrack((**theWindowObject).fMovie, VideoMediaType);
        (**myAppData).fTextIsHREF = QTText_IsHREFTrack(myTrack);
        (**myAppData).fTextTrack = myTrack;
        (**myAppData).fTextHandler = myHandler;
    }
}
  
 
//////////
//
// QTText_SetSearchText
// Let the user specify the text to be searched for.
//
//////////
 
void QTText_SetSearchText (void)
{
    DialogPtr       myDialog;
    short           myItem;
    short           myType;
    Handle          myItemHandle;
    Rect            myRect;
    
    // get the dialog that lets the user specify the search text
    myDialog = GetNewDialog(kTextDialogID, NULL, (WindowPtr)-1);
    if (myDialog == NULL)
        goto bail;
 
    SetDialogDefaultItem(myDialog, kTextOKIndex);
    
    // set the current search text into the edittext field
    GetDialogItem(myDialog, kTextTextEditIndex, &myType, &myItemHandle, &myRect);
    SetDialogItemText(myItemHandle, gSearchText);
    SelectDialogItemText(myDialog, kTextTextEditIndex, 0, 32767);   
        
    // now show the dialog
    MacShowWindow(GetDialogWindow(myDialog));
    MacSetPort(GetDialogPort(myDialog));
    
    do {
        ModalDialog(gModalFilterUPP, &myItem);
    } while (myItem != kTextOKIndex);
    
    // get the text in the edittext field
    GetDialogItemText(myItemHandle, gSearchText);
        
bail:
    if (myDialog != NULL)
        DisposeDialog(myDialog);
}
 
 
//////////
//
// QTText_FindText
// Find the specified string in the (first) text track of the specified window object.
//
//////////
 
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
    //////////
    //
    // METHOD ONE: Use MovieSearchText, your one-stop, find-the-text-and-do-the-right-thing function.
    //
    //////////
    
    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
    //////////
    //
    // METHOD TWO: Use TextMediaFindNextText. Here, you need to explicitly go to the found text and select it. 
    //
    //////////
 
    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 {
            // if the desired string wasn't found, beep
            QTFrame_Beep();
        }
    }
#endif // USE_MOVIESEARCHTEXT
 
    // update the current offset, if we're searching forward
    if (gSearchForward && (myErr == noErr))
        gOffset += theText[0];
}
 
 
//////////
//
// QTText_EditText
// Edit the text in the current sample of the (first) text track of the specified window object.
//
//////////
 
void QTText_EditText (WindowObject theWindowObject) 
{
    ApplicationDataHdl      myAppData = NULL;
    Movie                   myMovie = NULL;
    Track                   myTrack = NULL;
    Media                   myMedia = NULL;
    MediaHandler            myHandler = NULL;
    DialogPtr               myDialog = NULL;
    short                   myItem;
    short                   myType;
    Handle                  myItemHandle = NULL;
    Rect                    myRect;
    OSErr                   myErr = noErr;
        
    // get the movie and related stuff  
    myAppData = (ApplicationDataHdl)QTFrame_GetAppDataFromWindowObject(theWindowObject);
    if (myAppData == NULL)
        goto bail;
        
    myMovie = (**theWindowObject).fMovie;
    myTrack = (**myAppData).fTextTrack;
    myMedia = GetTrackMedia(myTrack);
    myHandler = (**myAppData).fTextHandler;
 
    // get the dialog that lets the user specify the text for the current sample
    myDialog = GetNewDialog(kEditDialogID, NULL, (WindowPtr)-1);
    if (myDialog == NULL)
        goto bail;
 
    SetDialogDefaultItem(myDialog, kEditOKIndex);
    SetDialogCancelItem(myDialog, kEditCancelIndex);
    
    // set the current sample text into the edittext field
    GetDialogItem(myDialog, kEditTextEditIndex, &myType, &myItemHandle, &myRect);
    SetDialogItemText(myItemHandle, gSampleText);
    SelectDialogItemText(myDialog, kEditTextEditIndex, 0, 32767);   
 
    // now show the dialog
    MacShowWindow(GetDialogWindow(myDialog));
    MacSetPort(GetDialogPort(myDialog));
    
    do {
        ModalDialog(gModalFilterUPP, &myItem);
    } while ((myItem != kEditOKIndex) && (myItem != kEditCancelIndex));
    
    // if the user hit the OK button, save the text and update the text sample 
    if (myItem == kEditOKIndex) {
        Fixed           myWidth;
        Fixed           myHeight;
        Rect            myBounds;   
        TimeValue       myMovieTime;
        TimeValue       mySampleTime;           
        TimeValue       myDuration;
        TimeValue       myMediaSampleDuration;
        TimeValue       myMediaSampleStartTime;
        TimeValue       myMediaCurrentTime;
        TimeValue       myInterestingTime;
        long            myMediaSampleIndex;
 
        // get the text in the edittext field
        GetDialogItemText(myItemHandle, gSampleText);
        
        // install that text as the current text media sample
 
        // first, we need to find the start and duration for this media sample;
        // get the current movie time and the time in media time of the current sample
        myMovieTime = GetMovieTime(myMovie, NULL);
            
        myMediaCurrentTime = TrackTimeToMediaTime(myMovieTime, myTrack);
                
        // get detailed information about the start and duration of the current sample      
        MediaTimeToSampleNum(   myMedia, 
                                myMediaCurrentTime, 
                                &myMediaSampleIndex, 
                                &myMediaSampleStartTime,
                                &myMediaSampleDuration);
                                
        // see where this text starts
        GetTrackNextInterestingTime(myTrack, nextTimeEdgeOK | nextTimeMediaSample, myMovieTime, -fixed1, &myInterestingTime, NULL);     
 
        // get the duration of the current sample
        myMovieTime = myInterestingTime;                    
        GetTrackNextInterestingTime(myTrack, nextTimeEdgeOK | nextTimeMediaSample, myMovieTime, fixed1, NULL, &myDuration);
                            
        myErr = BeginMediaEdits(myMedia);
        if (myErr != noErr) 
            goto bail;
                
        // delete the existing text         
        myErr = DeleteTrackSegment(myTrack, myInterestingTime, myDuration);
        if (myErr != noErr) 
            goto bail;
            
        // get the track bounds
        GetTrackDimensions(myTrack, &myWidth, &myHeight);
        myBounds.top = 0;
        myBounds.left = 0;
        myBounds.right = Fix2Long(myWidth);
        myBounds.bottom = Fix2Long(myHeight);   
 
        // write out the new data to the media
        myErr = TextMediaAddTextSample( myHandler, 
                                        (Ptr)(&gSampleText[1]), 
                                        gSampleText[0],
                                        0,
                                        0,
                                        0,
                                        NULL, 
                                        NULL, 
                                        teCenter,
                                        &myBounds, 
                                        dfClipToTextBox, 
                                        0, 
                                        0, 
                                        0, 
                                        NULL, 
                                        myMediaSampleDuration, 
                                        &mySampleTime);
                                
        if (myErr != noErr) 
            goto bail;
            
        EndMediaEdits(myMedia);
        
        // insert the new media into the track
        InsertMediaIntoTrack(myTrack, myInterestingTime, mySampleTime, myMediaSampleDuration, fixed1);
 
        // stamp the movie as dirty
        (**theWindowObject).fIsDirty = true;
        
        // update the chapter pop-up
        if ((**theWindowObject).fController != NULL)
            MCMovieChanged((**theWindowObject).fController, myMovie);
    }
    
bail:
    if (myDialog != NULL)
        DisposeDialog(myDialog);
}
 
 
//////////
//
// QTText_TextProc
// This function is called whenever a new text sample is about to be displayed.
// We'll use it to grab the text of the current text sample.
//
// NOTE: The theRefCon parameter is a handle to a window object record.
//
//////////
 
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);
}
 
 
///////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Text track utilities.
//
// Use these functions to manipulate text tracks in a movie.
//
///////////////////////////////////////////////////////////////////////////////////////////////////////////
 
//////////
//
// QTText_AddTextTrack
// Add a text track containing some text data to a movie; return the new track to the caller.
//
// The theStrings parameter is an array of C strings; each element is the text for a certain span of frames
// of the movie. The theFrames parameter is an array of frame counts; each of these counts determines how many
// frames the corresponding text element in the theStrings array applies to; the sum of all the values in this
// array should be equal to the total number of frames in the movie.
//
// If the isChapterTrack parameter is true, the text track is set to be a chapter track, attached to the (first)
// track of type theType.
//
//////////
 
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;
 
    //////////
    //
    // find the target track
    //
    //////////
 
    // 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);
            
#if USE_ADDMEDIASAMPLE
            {
                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);
            }
#else
            // 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);
#endif
        }
    }
 
    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 requested, 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);
}
 
 
//////////
//
// QTText_RemoveIndTextTrack
// Remove a text track, specified by its index, from a movie.
//
// If theIndex is kAllTextTracks (0), remove all text tracks from the movie.
//
//////////
 
OSErr QTText_RemoveIndTextTrack (WindowObject theWindowObject, short theIndex)
{
    MovieController     myMC = NULL;
    Movie               myMovie = NULL;
    Track               myTrack = NULL;
    OSErr               myErr = noErr;
    
    if (theWindowObject == NULL)
        return(paramErr);
            
    myMC = (**theWindowObject).fController;
    myMovie = (**theWindowObject).fMovie;
 
    if (theIndex == kAllTextTracks) {
        // remove ALL text tracks from the movie
        myTrack = GetMovieIndTrackType(myMovie, 1, TextMediaType, movieTrackMediaType);
        if (myTrack == NULL)
            myErr = badTrackIndex;
                
        while (myTrack != NULL) {
            QTUtils_DeleteAllReferencesToTrack(myTrack);
            MCMovieChanged(myMC, myMovie);
            DisposeMovieTrack(myTrack);
            myTrack = GetMovieIndTrackType(myMovie, 1, TextMediaType, movieTrackMediaType);
        }
    } else {
        // remove ONE text track from the movie
        myTrack = GetMovieIndTrackType(myMovie, theIndex, TextMediaType, movieTrackMediaType);
        if (myTrack == NULL) {
            myErr = badTrackIndex;
        } else {
            QTUtils_DeleteAllReferencesToTrack(myTrack);
            MCMovieChanged(myMC, myMovie);
            DisposeMovieTrack(myTrack);
        }
    }
 
    return(myErr);
}
 
 
///////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Chapter track utilities.
//
// Use these functions to manipulate chapter tracks in a movie.
//
// A chapter track is a text track that has been associated with some other track (often a video or sound
// track) in such a way that the movie controller will build, display, and handle a pop-up menu of titles
// of various parts of the associated 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 a user selects a
// chapter title in the pop-up menu, the movie controller jumps to the start time of the selected chapter.
//
// You create the association between a text track and some other track by creating a reference from that
// other track to the text track, where the reference is of type kTrackReferenceChapterList. Note that all
// the chapter titles must be contained in a single text track; you specify the starting time for chapters
// when you add the text to the text track by calling TextMediaAddTextSample. Note also that you need to
// create the chapter association only between the text track and one other track, not between the chapter
// track and all other tracks in the movie. (That "other" track must be enabled, but typically the chapter
// track is not enabled.)
//
// The pop-up menu will disappear from the controller bar if there isn't enough space to display the menu,
// the volume slider control, the step buttons, and the other controls. 
//
///////////////////////////////////////////////////////////////////////////////////////////////////////////
 
//////////
//
// QTText_SetTextTrackAsChapterTrack
// Set the (first) text track in the specified movie to be or not to be a chapter track
// for the (first) enabled track of the specified type.
//
// The isChapterTrack parameter determines the final state of the text track.
//
//////////
 
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);
}
 
 
//////////
//
// QTText_TrackTypeHasAChapterTrack
// Does the (first) enabled track of the specified type in the specified movie have a chapter track?
//
//////////
 
Boolean QTText_TrackTypeHasAChapterTrack (Movie theMovie, OSType theType)
{
    Track                   myTypeTrack = NULL;
    Track                   myTextTrack = NULL;
        
    myTypeTrack = GetMovieIndTrackType(theMovie, 1, theType, movieTrackMediaType | movieTrackEnabledOnly);
    if (myTypeTrack != NULL)
        myTextTrack = GetTrackReference(myTypeTrack, kTrackReferenceChapterList, 1);
 
    return(myTextTrack != NULL);
}
 
 
//////////
//
// QTText_TrackHasAChapterTrack
// Does the specified track have a chapter track?
//
//////////
 
Boolean QTText_TrackHasAChapterTrack (Track theTrack)
{
    return(GetTrackReference(theTrack, kTrackReferenceChapterList, 1) != NULL);
}
 
 
//////////
//
// QTText_MovieHasAChapterTrack
// Does the specified movie have a chapter track? In other words, is there are least one enabled track
// in the specified movie that has a chapter track associated wih it?
//
//////////
 
Boolean QTText_MovieHasAChapterTrack (Movie theMovie)
{
    long        myCount;
    long        myTrackCount = GetMovieTrackCount(theMovie);
    Track       myTrack = NULL;
    Boolean     myGotChapter = false;
    
    for (myCount = 1; myCount <= myTrackCount; myCount++) {
        myTrack = GetMovieIndTrack(theMovie, myCount);
        if (GetTrackEnabled(myTrack))
            myGotChapter = QTText_TrackHasAChapterTrack(myTrack);
            if (myGotChapter)
                break;
    }
    
    return(myGotChapter);
}
 
 
//////////
//
// QTText_GetChapterTrackForTrack
// Return the first chapter track (if any) associated with the specified track.
//
//////////
 
Track QTText_GetChapterTrackForTrack (Track theTrack)
{
    return(GetTrackReference(theTrack, kTrackReferenceChapterList, 1));
}
 
 
//////////
//
// QTText_GetChapterTrackForMovie
// Return the first chapter track (if any) in the specified movie.
//
// A movie can have more than one chapter track; if so, QuickTime uses the chapter track associated
// with the first enabled track that it finds. Accordingly, this function returns the first chapter
// track that we find as we iterate thru the movie's tracks.
//
//////////
 
Track QTText_GetChapterTrackForMovie (Movie theMovie)
{
    long        myCount;
    long        myTrackCount = GetMovieTrackCount(theMovie);
    Track       myTrack = NULL;
    Track       myChapTrack = NULL;
    
    for (myCount = 1; myCount <= myTrackCount; myCount++) {
        myTrack = GetMovieIndTrack(theMovie, myCount);
        if (GetTrackEnabled(myTrack))
            myChapTrack = QTText_GetChapterTrackForTrack(myTrack);
            if (myChapTrack != NULL)
                break;
    }
    
    return(myChapTrack);
}
 
 
//////////
//
// QTText_IsChapterTrack
// Is the specified track a chapter track?
//
//////////
 
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);
 
    // a chapter track is a text track that is referred to by some other track in the movie,
    // so we need to iterate thru all those tracks to see if any of them refers to the specified track
    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);
}
 
 
//////////
//
// QTText_GetFirstChapterTime
// Return, through the theTime parameter, the starting time of the first chapter of the
// specified chapter track.
//
// If this function encounters an error, it returns a (bogus) starting time of -1. Note that
// GetTrackNextInterestingTime also returns -1 as a starting time if the search criteria
// specified in the myFlags parameter are not matched by any interesting time in the movie. 
//
//////////
 
OSErr QTText_GetFirstChapterTime (Track theChapterTrack, TimeValue *theTime)
{
    short   myFlags = nextTimeMediaSample + nextTimeEdgeOK; // we want the first sample in the movie
    
    if (theChapterTrack == NULL) {
        *theTime = kBogusStartingTime;                      // a bogus time
        return(invalidTrack);
    }
    
    GetTrackNextInterestingTime(theChapterTrack, myFlags, (TimeValue)0, fixed1, theTime, NULL);
    return(GetMoviesError());
}
 
 
//////////
//
// QTText_GetNextChapterTime
// Return, through the theTime parameter, the starting time of the chapter of the
// specified chapter track that immediately follows the chapter starting at the time
// passed in through theTime.
//
//////////
 
OSErr QTText_GetNextChapterTime (Track theChapterTrack, TimeValue *theTime)
{
    short   myFlags = nextTimeMediaSample;                  // we want the next sample in the movie
    
    if (theChapterTrack == NULL) {
        *theTime = kBogusStartingTime;                      // a bogus time
        return(invalidTrack);
    }
    
    GetTrackNextInterestingTime(theChapterTrack, myFlags, *theTime, fixed1, theTime, NULL);
    return(GetMoviesError());
}
 
 
//////////
//
// QTText_GetIndChapterTime
// Return the starting time of the chapter in the specified chapter track that has the specified index.
//
//////////
 
TimeValue QTText_GetIndChapterTime (Track theChapterTrack, long theIndex)
{
    long            myCount = 1;
    TimeValue       myTime = kBogusStartingTime;
 
    if ((theChapterTrack == NULL) || (theIndex < 1))
        return(myTime);
 
    QTText_GetFirstChapterTime(theChapterTrack, &myTime);
    if (theIndex == 1)
        return(myTime);
        
    while (myCount < theIndex) {
        QTText_GetNextChapterTime(theChapterTrack, &myTime);
        myCount++;
    }
    
    return(myTime);
}
 
 
//////////
//
// QTText_GetIndChapterText
// Return the text of the chapter in the specified chapter track that has the specified index.
//
// The caller is responsible for disposing of the pointer returned by this function (by calling free).
//
//////////
 
char *QTText_GetIndChapterText (Track theChapterTrack, long theIndex)
{
    long            myCount = 1;
    long            mySize;         // size of entire handle returned, which may include appended style atoms
    short           myTextSize;
    TimeValue       myTime;
    Handle          myHandle = NewHandleClear(0);
    char            *myText = NULL;
 
    if ((theChapterTrack == NULL) || (theIndex < 1))
        return(myText);
 
    if (myHandle == NULL)
        return(myText);
 
    myTime = QTText_GetIndChapterTime(theChapterTrack, theIndex);
    if (myTime != kBogusStartingTime) {
    
        GetMediaSample( GetTrackMedia(theChapterTrack),
                        myHandle,
                        0,
                        &mySize,
                        TrackTimeToMediaTime(myTime, theChapterTrack),      // media time scale
                        NULL,
                        NULL,
                        NULL,
                        NULL,
                        0,
                        NULL,
                        NULL);
                    
        // for text media samples, the returned handle is a 16-bit size field followed by the actual text data
        myTextSize = *(short *)(*myHandle);
        myText = malloc(myTextSize + 1);
        BlockMove(*myHandle + sizeof(short), myText, myTextSize);
        myText[myTextSize] = '\0';
        
        DisposeHandle(myHandle);
    }
 
    return(myText);
}
 
 
//////////
//
// QTText_GetChapterCount
// Return the number of chapters in the specified chapter track.
//
//////////
 
long QTText_GetChapterCount (Track theChapterTrack)
{
    long            myCount = 0;
    TimeValue       myTime = kBogusStartingTime;
 
    if (theChapterTrack != NULL) {
        QTText_GetFirstChapterTime(theChapterTrack, &myTime);
            
        while (myTime != kBogusStartingTime) {
            myCount++;
            QTText_GetNextChapterTime(theChapterTrack, &myTime);
        }
    }
 
    return(myCount);
}
 
 
///////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// HREF track utilities.
//
// Use these functions to manipulate HREF tracks in a movie.
//
// An HREF track is a text track that has a special name ("HREFTrack") and some of whose samples specify
// URLs that are loaded when the user clicks in the movie box when the text sample is active or when the
// text sample first loads.
//
///////////////////////////////////////////////////////////////////////////////////////////////////////////
 
//////////
//
// QTText_SetTextTrackAsHREFTrack
// Set the specified track to be or not to be an HREF track.
//
//////////
 
OSErr QTText_SetTextTrackAsHREFTrack (Track theTrack, Boolean isHREFTrack)
{
    OSErr       myErr = noErr;
    
    myErr = QTUtils_SetTrackName(theTrack, isHREFTrack ? kHREFTrackName : kNonHREFTrackName);
 
    return(myErr);
}
 
 
//////////
//
// QTText_IsHREFTrack
// Is the specified track an HREF track?
//
// For the moment, we are content to count a track as an HREF track if its name is "HREFTrack";
// a more thorough test would be to look through the text samples for some actual URLs. This is
// left as an exercise for the reader.
//
//////////
 
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);
}
 
 
///////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Miscellaneous utilities.
//
///////////////////////////////////////////////////////////////////////////////////////////////////////////
 
//////////
//
// QTText_CopyCStringToPascal
// Convert the source C string to a destination Pascal string as it's copied.
//
// The destination string will be truncated to fit into a Str255 if necessary.
// If the C string pointer is NULL, the Pascal string's length is set to zero.
//
// This routine is borrowed from CGlue.c, by Nick Kledzik.
//
//////////
 
void QTText_CopyCStringToPascal (const char *theSrc, Str255 theDst)
{
    short                   myLength = 0;
    
    // handle case of overlapping strings
    if ((void *)theSrc == (void *)theDst) {
        unsigned char       *myCurDst = &theDst[1];
        unsigned char       myChar;
                
        myChar = *(const unsigned char *)theSrc++;
        while (myChar != '\0') {
            unsigned char   myNextChar;
            
            // use myNextChar so we don't overwrite what we are about to read
            myNextChar = *(const unsigned char *)theSrc++;
            *myCurDst++ = myChar;
            myChar = myNextChar;
            
            if (++myLength >= 255)
                break;
        }
    } else if (theSrc != NULL) {
        unsigned char       *myCurDst = &theDst[1];
        short               myOverflow = 255;       // count down, so test it loop is faster
        register char       myTemp;
    
        // we can't do the K&R C thing of Òwhile (*s++ = *t++)Ó because it will copy the trailing zero,
        // which might overrun the Pascal buffer; instead, we use a temp variable
        while ((myTemp = *theSrc++) != 0) {
            *(char *)myCurDst++ = myTemp;
                
            if (--myOverflow <= 0)
                break;
        }
        myLength = 255 - myOverflow;
    }
    
    // set the length of the destination Pascal string
    theDst[0] = myLength;
}