//////////
//
//	File:		AddHTActions.c
//
//	Contains:	Sample code for adding hypertext links to a QuickTime movie with a text track.
//
//	Written by:	Tim Monroe
//				Based on existing code by Bill Wright
//
//	Copyright:	© 1999 by Apple Computer, Inc., all rights reserved.
//
//	Change History (most recent first):
//
//	   <4>	 	07/10/02	rtm		tweaked position of CloseMovieFile in AddHTAct_AddHyperTextToTextMovie
//	   <3>	 	08/07/00	rtm		fixed bug in AddHTAct_PutFile that caused failure on Mac OS X
//	   <2>	 	03/21/00	rtm		changes for supporting CarbonLib
//	   <1>	 	07/19/99	rtm		first file from bw; revised to sample code coding style;
//									added Endian macros and other niceties for cross-platform
//									support; works fine on Windows and MacOS
//	   
//	This file contains some sample code that adds a few wired actions to a movie that has a
//	text track. In particular, it adds two go-to-URL actions to parts of the text track. 
//
//	A text media sample is a 16-bit length word followed by the text of that sample. Optionally,
//	one or more atoms of additional data (called text atom extensions) may follow the text in the
//	sample. These text atom extensions are organized as "classic" atom structures: a 32-bit length
//	field, followed by a 32-bit type field, followed by the data in the atom. Here, the length field
//	specifies the total length of the atom (that is, 8 plus the number of bytes in the data).
//	All the data in the text extension atom must be in big-endian format.
//	
//	To add hypertext actions to a text sample, you simply add a text atom extension of the type
//	kHyperTextTextAtomType to the end of the sample; the data of the text atom extension is the
//	container that holds the information about the actions. So our task boils down to this: find
//	a text sample, create an atom container holding information about the desired actions, and then
//	append a text extension atom whose data is that atom container to the end of the text sample.
//	Then replace the previous text sample with the new one in the text track.
//
//	For complete information about wired actions, see the chapter "Wired Sprites" in the book
//	Programming With QuickTime Sprites.
//
//////////

#include "AddHTActions.h"


Str255				gAppName;							// the name of this application


//////////
//
// main/WinMain 
// The main function for this application.
//
//////////

#if TARGET_OS_MAC
void main (void)
#elif TARGET_OS_WIN32
int CALLBACK WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR theCmdLine, int nCmdShow)
#endif
{
	OSType					myTypeList[1] = {MovieFileType};
	FSSpec					myFile;
	Boolean					myIsSelected = false;
	Boolean					myIsReplacing = false;	
	StringPtr				myPrompt = AddHTAct_ConvertCToPascalString(kSavePrompt);
	StringPtr				myDefaultName = AddHTAct_ConvertCToPascalString(kSaveFileName);
	OSErr					myErr = noErr;

#if TARGET_OS_WIN32
	InitializeQTML(0L);											// initialize QuickTime Media Layer
#endif	

#if TARGET_OS_MAC
#if !TARGET_API_MAC_CARBON	
	MaxApplZone();												// init everything
	InitGraf(&qd.thePort);
	InitFonts();
	FlushEvents(everyEvent, 0);
	InitWindows();
	InitMenus();
	InitDialogs(NULL);
	TEInit();
#endif
	InitCursor();
	
	// get the application's name from the resource file
	GetIndString(gAppName, kAppNameResID, kAppNameResIndex);
#endif	
	
	myErr = EnterMovies();
	if (myErr != noErr)
		goto bail;

	// elicit a file from the user to save the new movie into
	AddHTAct_PutFile(myPrompt, myDefaultName, &myFile, &myIsSelected, &myIsReplacing);

	// create a text movie with hypertext links
	if (myIsSelected) {
		myErr = AddHTAct_CreateTextMovie(&myFile);
		if (myErr == noErr)
			myErr = AddHTAct_AddHyperTextToTextMovie(&myFile);
	}
	
bail:
	free(myPrompt);
	free(myDefaultName);
	
	ExitMovies();

#if TARGET_OS_WIN32
	// terminate the QuickTime Media Layer
	TerminateQTML();
	return(1);
#endif	

#if TARGET_OS_MAC
	return;
#endif	
}


//////////
//
// AddHTAct_CreateTextMovie 
// Create a movie with a text track.
//
//////////

static OSErr AddHTAct_CreateTextMovie (FSSpec *theFSSpec)
{
	short			myResRefNum = -1;
	short			myResID = movieInDataForkResID;
	Movie			myMovie = NULL;
	Track			myTrack = NULL;
	Media			myMedia = NULL;
	MediaHandler	myHandler = NULL;
	Rect			myTextBox;
	RGBColor		myTextColor =	{0x6666, 0xCCCC, 0xCCCC};
	RGBColor		myBackColor =	{0x3333, 0x6666, 0x6666};
	RGBColor		myHiliteColor =	{0x0000, 0x0000, 0x0000};
	long			myDisplayFlags = 0;
	short			myHiliteStart = 0;
	short			myHiliteEnd = 0;
	TimeValue		myNewMediaTime;
	TimeValue		myScrollDelay = 0;
#if TARGET_OS_MAC
	char			myText[] = "\rPlease take me to Apple or CNN";
#else
	char			myText[] = "\nPlease take me to Apple or CNN";
#endif
	long			myFlags = createMovieFileDeleteCurFile | createMovieFileDontCreateResFile | newMovieActive;
	OSErr			myErr = noErr;
		
	//////////
	//
	// create a new text movie
	//
	//////////

	myErr = CreateMovieFile(theFSSpec, FOUR_CHAR_CODE('TVOD'), 0, myFlags, &myResRefNum, &myMovie);
	if (myErr != noErr)
		goto bail;
	
	myTrack = NewMovieTrack(myMovie, FixRatio(kWidth320, 1), FixRatio(kHeight240, 1), kTrackVolumeZero);	
	myMedia = NewTrackMedia(myTrack, TextMediaType, kTimeScale600, NULL, 0);
	if ((myTrack == NULL) || (myMedia == NULL))
		goto bail;
	
	myErr = BeginMediaEdits(myMedia);
	if (myErr != noErr)
		goto bail;

	myHandler = GetMediaHandler(myMedia);
	if (myHandler == NULL)
		goto bail;

	//////////
	//
	// add a text sample to the movie
	//
	//////////

	MacSetRect(&myTextBox, 0, 0, kWidth320, kHeight240);
	MacInsetRect(&myTextBox, kTextBoxInset, kTextBoxInset);
	
	myErr = (OSErr)TextMediaAddTextSample(	myHandler,
											myText,
											strlen(myText),
											kFontIDSymbol,
											kSize48,
											kFacePlain,
											&myTextColor,
											&myBackColor,
											teCenter,
											&myTextBox,
											myDisplayFlags,
											myScrollDelay,
											myHiliteStart,
											myHiliteEnd,
											&myHiliteColor,
											kTimeScale600,
											&myNewMediaTime);
	if (myErr != noErr)
		goto bail;

	myErr = EndMediaEdits(myMedia);
	if (myErr != noErr)
		goto bail;
		
	// add the media to the track, at time 0
	myErr = InsertMediaIntoTrack(myTrack, kTrackStartTimeZero, myNewMediaTime, kTimeScale600, fixed1);
	if (myErr != noErr)
		goto bail;
	
	// add the movie resource
	myErr = AddMovieResource(myMovie, myResRefNum, &myResID, NULL);
	
bail:
	if (myResRefNum != -1)
		CloseMovieFile(myResRefNum);
	
	if (myMovie != NULL)
		DisposeMovie(myMovie);
	
	return(myErr);
}


//////////
//
// AddHTAct_CreateHyperTextActionContainer
// Return, through the theActions parameter, an atom container that contains some hypertext actions.
//
//////////

static OSErr AddHTAct_CreateHyperTextActionContainer (QTAtomContainer *theActions)
{
	QTAtom			myEventAtom = 0;
	QTAtom			myActionAtom = 0;
	QTAtom			myHyperTextAtom = 0;
	QTAtom			myWiredObjectsAtom = 0;
	long			myAction;
	long			mySelStart1 = 19;
	long			mySelEnd1 = 24;
	long			mySelStart2 = 28;
	long			mySelEnd2 = 31;
	long			myValue;
	char			myAppleURL[64] = "\pwww.apple.com\0";
	char			myCnnURL[64] = "\pwww.cnn.com\0";
	OSErr			myErr = noErr;
	
	myErr = QTNewAtomContainer(theActions);
	if (myErr != noErr)
		goto bail;
	
	// create a wired objects atom
	myErr = QTInsertChild(*theActions, kParentAtomIsContainer, kTextWiredObjectsAtomType, kIndexOne, kIndexZero, kZeroDataLength, NULL, &myWiredObjectsAtom);
	if (myErr != noErr)
		goto bail;
	
	//////////
	//
	// add a hypertext link to the wired objects atom: ID 1
	//
	//////////
	
	myErr = QTInsertChild(*theActions, myWiredObjectsAtom, kHyperTextItemAtomType, kIDOne, kIndexZero, kZeroDataLength, NULL, &myHyperTextAtom);
	if (myErr != noErr)
		goto bail;
	
	myValue = EndianS32_NtoB(mySelStart1);
	myErr = QTInsertChild(*theActions, myHyperTextAtom, kRangeStart, kIDOne, kIndexZero, sizeof(long), &myValue, NULL);
	if (myErr != noErr)
		goto bail;
	
	myValue = EndianS32_NtoB(mySelEnd1);
	myErr = QTInsertChild(*theActions, myHyperTextAtom, kRangeEnd, kIDOne, kIndexZero, sizeof(long), &myValue, NULL);
	if (myErr != noErr)
		goto bail;
	
	// add an event atom to the hypertext atom;
	// the event type can be any of the five mouse events: down, up, trigger, enter, exit
	myErr = QTInsertChild(*theActions, myHyperTextAtom, kQTEventType, kQTEventMouseClick, kIndexZero, kZeroDataLength, NULL, &myEventAtom);
	if (myErr != noErr)
		goto bail;
	
	myErr = QTInsertChild(*theActions, myEventAtom, kAction, kIndexOne, kIndexOne, kZeroDataLength, NULL, &myActionAtom);
	if (myErr != noErr)
		goto bail;
	
	myAction = EndianS32_NtoB(kActionGoToURL);
	myErr = QTInsertChild(*theActions, myActionAtom, kWhichAction, kIndexOne, kIndexOne, sizeof(long), &myAction, NULL);
	if (myErr != noErr)
		goto bail;

	myErr = QTInsertChild(*theActions, myActionAtom, kActionParameter, kIndexOne, kIndexOne, myAppleURL[0] + 1, &myAppleURL[1], NULL);
	if (myErr != noErr)
		goto bail;
		
	//////////
	//
	// add a hypertext link to the wired objects atom: ID 2
	//
	//////////

	myErr = QTInsertChild(*theActions, myWiredObjectsAtom, kHyperTextItemAtomType, kIDTwo, kIndexZero, kZeroDataLength, NULL, &myHyperTextAtom);
	if (myErr != noErr)
		goto bail;
	
	myValue = EndianS32_NtoB(mySelStart2);
	myErr = QTInsertChild(*theActions, myHyperTextAtom, kRangeStart, kIDOne, kIndexZero, sizeof(long), &myValue, NULL);
	if (myErr != noErr)
		goto bail;
	
	myValue = EndianS32_NtoB(mySelEnd2);
	myErr = QTInsertChild(*theActions, myHyperTextAtom, kRangeEnd, kIDOne, kIndexZero, sizeof(long), &myValue, NULL);
	if (myErr != noErr)
		goto bail;
	
	// add an event atom to the hypertext atom;
	// the event type can be any of the five mouse events: down, up, trigger, enter, exit
	myErr = QTInsertChild(*theActions, myHyperTextAtom, kQTEventType, kQTEventMouseClick, kIndexZero, kZeroDataLength, NULL, &myEventAtom);
	if (myErr != noErr)
		goto bail;
	
	myErr = QTInsertChild(*theActions, myEventAtom, kAction, kIndexOne, kIndexOne, kZeroDataLength, NULL, &myActionAtom);
	if (myErr != noErr)
		goto bail;
	
	myAction = EndianS32_NtoB(kActionGoToURL);
	myErr = QTInsertChild(*theActions, myActionAtom, kWhichAction, kIndexOne, kIndexOne, sizeof(long), &myAction, NULL);
	if (myErr != noErr)
		goto bail;

	myErr = QTInsertChild(*theActions, myActionAtom, kActionParameter, kIndexOne, kIndexOne, myCnnURL[0] + 1, &myCnnURL[1], NULL);
	if (myErr != noErr)
		goto bail;

bail:
	return(myErr);
}	


//////////
//
// AddHTAct_AddHyperActionsToSample
// Add the specified atom container to the end of the specified media sample.
//
// Hypertext actions are stored at the end of the sample as a normal text atom extension.
// In this case, the text atom type is kHyperTextTextAtomType and the data is the container data.
//
//////////

static OSErr AddHTAct_AddHyperActionsToSample (Handle theSample, QTAtomContainer theActions)
{
	Ptr			myPtr = NULL;
	long		myHandleLength;
	long		myContainerLength;
	long		myNewLength;		
	OSErr		myErr = noErr;

	myHandleLength = GetHandleSize(theSample);
	myContainerLength = GetHandleSize((Handle)theActions);
	
	myNewLength = (long)(sizeof(long) + sizeof(OSType) + myContainerLength);
			
	SetHandleSize(theSample, (myHandleLength + myNewLength));
	myErr = MemError();		
	if (myErr != noErr)
		goto bail;
	
	HLock(theSample);
	
	// get a pointer to the beginning of the new block of space added to the sample
	// by the previous call to SetHandleSize; we need to format that space as a text
	// atom extension
	myPtr = *theSample + myHandleLength;
	
	// set the length of the text atom extension
	*(long *)myPtr = EndianS32_NtoB((long)(sizeof(long) + sizeof(OSType) + myContainerLength));
	myPtr += (sizeof(long));
	
	// set the type of the text atom extension
	*(OSType *)myPtr = EndianS32_NtoB(kHyperTextTextAtomType);
	myPtr += (sizeof(OSType));
	
	// set the data of the text atom extension;
	// we assume that this data is already in big-endian format
	HLock((Handle)theActions);
	BlockMove(*theActions, myPtr, myContainerLength);
	
	HUnlock((Handle)theActions);
	HUnlock(theSample);

bail:
	return(myErr);
}


//////////
//
// AddHTAct_AddHyperTextToTextMovie
// Add some hypertext actions to the specified text movie.
//
//////////

static OSErr AddHTAct_AddHyperTextToTextMovie (FSSpec *theFSSpec)
{
	short							myResID = 0;
	short							myResRefNum = -1;
	Movie							myMovie = NULL;
	Track							myTrack = NULL;
	Media							myMedia = NULL;
	TimeValue						myTrackOffset;
	TimeValue						myMediaTime;
	TimeValue						mySampleDuration;
	TimeValue						mySelectionDuration;
	TimeValue						myNewMediaTime;
	TextDescriptionHandle			myTextDesc = NULL;
	Handle							mySample = NULL;
	short							mySampleFlags;
	Fixed 							myTrackEditRate;
	QTAtomContainer					myActions = NULL;	
	OSErr							myErr = noErr;

	//////////
	//
	// open the movie file and get the first text track from the movie
	//
	//////////
	
	// open the movie file for reading and writing
	myErr = OpenMovieFile(theFSSpec, &myResRefNum, fsRdWrPerm);
	if (myErr != noErr)
		goto bail;

	myErr = NewMovieFromFile(&myMovie, myResRefNum, &myResID, NULL, newMovieActive, NULL);
	if (myErr != noErr)
		goto bail;
		
	// find first text track in the movie
	myTrack = GetMovieIndTrackType(myMovie, kIndexOne, TextMediaType, movieTrackMediaType);
	if (myTrack == NULL)
		goto bail;
	
	//////////
	//
	// get first media sample in the text track
	//
	//////////
	
	myMedia = GetTrackMedia(myTrack);
	if (myMedia == NULL)
		goto bail;
	
	myTrackOffset = GetTrackOffset(myTrack);
	myMediaTime = TrackTimeToMediaTime(myTrackOffset, myTrack);

	// allocate some storage to hold the sample description for the text track
	myTextDesc = (TextDescriptionHandle)NewHandle(4);
	if (myTextDesc == NULL)
		goto bail;

	mySample = NewHandle(0);
	if (mySample == NULL)
		goto bail;

	myErr = GetMediaSample(myMedia, mySample, 0, NULL, myMediaTime, NULL, &mySampleDuration, (SampleDescriptionHandle)myTextDesc, NULL, 1, NULL, &mySampleFlags);
	if (myErr != noErr)
		goto bail;

	//////////
	//
	// add hypertext actions to the first media sample
	//
	//////////
	
	// create an action container for hypertext actions
	myErr = AddHTAct_CreateHyperTextActionContainer(&myActions);
	if (myErr != noErr)
		goto bail;
	
	// add hypertext actions actions to sample
	myErr = AddHTAct_AddHyperActionsToSample(mySample, myActions);
	if (myErr != noErr)
		goto bail;
		
	//////////
	//
	// replace sample in media
	//
	//////////
	
	myTrackEditRate = GetTrackEditRate(myTrack, myTrackOffset);
	if (GetMoviesError() != noErr)
		goto bail;

	GetTrackNextInterestingTime(myTrack, nextTimeMediaSample | nextTimeEdgeOK, myTrackOffset, fixed1, NULL, &mySelectionDuration);
	if (GetMoviesError() != noErr)
		goto bail;

	myErr = DeleteTrackSegment(myTrack, myTrackOffset, mySelectionDuration);
	if (myErr != noErr)
		goto bail;
		
	myErr = BeginMediaEdits(myMedia);
	if (myErr != noErr)
		goto bail;
	
	myErr = AddMediaSample(	myMedia,
							mySample,
							0,
							GetHandleSize(mySample),
							mySampleDuration,
							(SampleDescriptionHandle)myTextDesc, 
							1,
							mySampleFlags,
							&myNewMediaTime);
	if (myErr != noErr)
		goto bail;
	
	myErr = EndMediaEdits(myMedia);
	if (myErr != noErr)
		goto bail;
	
	// add the media to the track
	myErr = InsertMediaIntoTrack(myTrack, myTrackOffset, myNewMediaTime, mySelectionDuration, myTrackEditRate);
	if (myErr != noErr)
		goto bail;

	//////////
	//
	// update the movie resource
	//
	//////////
	
	myErr = UpdateMovieResource(myMovie, myResRefNum, myResID, NULL);
	
bail:
	// close the movie file
	if (myResRefNum != -1)
		CloseMovieFile(myResRefNum);
		
	if (myActions != NULL)
		(void)QTDisposeAtomContainer(myActions);
	
	if (mySample != NULL)
		DisposeHandle(mySample);		
	
	if (myTextDesc != NULL)
		DisposeHandle((Handle)myTextDesc);		
	
	if (myMovie != NULL)
		DisposeMovie(myMovie);	

	return(myErr);
}

//////////
//
// AddHTAct_PutFile
// Save a file under the specified name. Return Boolean values indicating whether the user selected a file
// and whether the selected file is replacing an existing file.
//
//////////

OSErr AddHTAct_PutFile (ConstStr255Param thePrompt, ConstStr255Param theFileName, FSSpecPtr theFSSpecPtr, Boolean *theIsSelected, Boolean *theIsReplacing)
{
#if TARGET_OS_WIN32
	StandardFileReply	myReply;
#endif
#if TARGET_OS_MAC
	NavReplyRecord		myReply;
	NavDialogOptions	myDialogOptions;
#endif
	OSErr				myErr = noErr;

	if ((theFSSpecPtr == NULL) || (theIsSelected == NULL) || (theIsReplacing == NULL))
		return(paramErr);
	
	// assume we are not replacing an existing file
	*theIsReplacing = false;
	
#if TARGET_OS_WIN32
	StandardPutFile(thePrompt, theFileName, &myReply);
	*theFSSpecPtr = myReply.sfFile;
	*theIsSelected = myReply.sfGood;
	if (myReply.sfGood)
		*theIsReplacing = myReply.sfReplacing;
#endif

#if TARGET_OS_MAC
	// specify the options for the dialog box
	NavGetDefaultDialogOptions(&myDialogOptions);
	myDialogOptions.dialogOptionFlags += kNavNoTypePopup;
	myDialogOptions.dialogOptionFlags += kNavDontAutoTranslate;
	BlockMoveData(theFileName, myDialogOptions.savedFileName, theFileName[0] + 1);
	BlockMoveData(gAppName, myDialogOptions.clientName, gAppName[0] + 1);
	BlockMoveData(thePrompt, myDialogOptions.message, thePrompt[0] + 1);
	
	// prompt the user for a file
	myErr = NavPutFile(NULL, &myReply, &myDialogOptions, NULL, MovieFileType, FOUR_CHAR_CODE('TVOD'), NULL);
	if ((myErr == noErr) && myReply.validRecord) {
		AEKeyword		myKeyword;
		DescType		myActualType;
		Size			myActualSize = 0;
		
		// get the FSSpec for the selected file
		if (theFSSpecPtr != NULL)
			myErr = AEGetNthPtr(&(myReply.selection), 1, typeFSS, &myKeyword, &myActualType, theFSSpecPtr, sizeof(FSSpec), &myActualSize);
	}
		
	*theIsSelected = myReply.validRecord;
	if (myReply.validRecord)
		*theIsReplacing = myReply.replacing;
		
	NavDisposeReply(&myReply);
#endif

	return(myErr);
}


//////////
//
// AddHTAct_ConvertCToPascalString
// Convert a C string into a Pascal string.
//
// The caller is responsible for disposing of the pointer returned by this function (by calling free).
//
//////////

StringPtr AddHTAct_ConvertCToPascalString (char *theString)
{
	StringPtr	myString = malloc(strlen(theString) + 1);
	short		myIndex = 0;

	while (theString[myIndex] != '\0') {
		myString[myIndex + 1] = theString[myIndex];
		myIndex++;
	}
	
	myString[0] = (unsigned char)myIndex;
	
	return(myString);
}



