ftp.nice.ch/pub/next/graphics/movie/Movie.3.0.NIHS.bs.tar.gz#/Movie3.0/Source/MovieView.m

This is MovieView.m in view mode; [Download] [Up]

#include <objc/NXBundle.h>
#include <dpsclient/psops.h>
#include "wraps.h"

#import "MovieView.h"

/*
 *	Added xanim support
 *	(c) 1995 Andreas Windemuth
 */

#define VERBOSE 0

#define XANIM 1
#if XANIM
#import "Animation.h"
#endif

/*
 * Movie 2.51 - 5/7/92 pjf
 *
 * Differences between 2.5 and 2.51:
 * - the save: method actually has a prayer of working when the
 *   user saves on top of an existing movie and the old copy can't be renamed.
 *
 * Differences between 2.0 and 2.5:
 * - buttons to control cache depth
 *
 * - turn off multiframe .tiffs by default (define BC_VERSION_1 if you
 *   have a .tiff movie and are too lazy to use tiffutil to turn
 *   it into an .anim directory)
 *
 * - now able to save movie (currently-selected depth)
 *
 */

#define maxFrames 1024

@implementation MovieView

//
- initFrame:(const NXRect *) frameRect
 {
	const char *x;
	[(self = [super initFrame:frameRect]) allocateGState];
	state = STOPPED;
	mode = LOOP;
	maxSize.width = maxSize.height = -1.0;
	moviePath = NULL;
	movieFrame = NULL;
	frameCount = 0;
	anim = nil;
	pingDuringDisplay=NO;
	x=NXGetDefaultValue("Movie","DefaultDepth");
	if (!x) dmode=D_DEF; /* use default depth */
	else switch(atoi(x)) {
	    default:
	    case 0: dmode=D_DEF; break;
	    case 2: dmode=D_2; break;
	    case 8: dmode=D_8; break;
	    case 12: dmode=D_12; break;
	    case 24: dmode=D_24; break;
	};
	updateControls = NO;
	showFrameNumber = YES;
	noOriginals = YES;
	fromStream = YES;
	frameRate = 15.0;
	loading = NO;
	willClose = NO;
	numFrames = 0;
	return self;
 }

- updateControls
{
    if (loading) {
	[[fNumSlider setMaxValue:numFrames-1] setEnabled:YES];
	[nFramesText setIntValue:numFrames-1];
    }
    [fNumSlider setIntValue:currentFrameIndex];
    [fNumText setIntValue:currentFrameIndex];
    [fNumSlider setEnabled:YES];
    return self;
}

- drawSelf:(const NXRect *) rects :(int)count
 {
    NXImage *image;
    NXPoint origin = {0.0,0.0};
    
    if (!movieFrame) return nil;
    image = movieFrame[currentFrameIndex].image;
    if (!image) return self;
    [image composite:NX_SOVER toPoint:&origin];
    if ((!fromStream && loading) || showFrameNumber) {
	NXRect r;
	[self getBounds:&r];
	if (!fromStream && loading)
	    PSWtext(r.size.width, r.size.height, "Loading ...", currentFrameIndex);
	else
	    PSWframe(r.size.width, r.size.height, currentFrameIndex);
    }
    if (pingDuringDisplay) NXPing();
    if ((frameCount >= (int)frameRate)) {
	if (state != STOPPED) {
	    double t=[anim getDoubleRealTime]+[anim getSyncTime];
	    double afps=frameCount/(t-oldt);
	    [actualFpsText setDoubleValue:afps];
	    oldt=t;
	}
	frameCount=0;
    }
    if (updateControls || !anim) [self updateControls];
    return self;
 }


//
- (BOOL)open:sender
 {
#if XANIM
    const char *const types[] = {
	"tiff", "anim", "mpg", "mpeg", "iff", "gif", "txt", "fli", "dl", "pfx", "rle", "avi", "qt", "mov",
    NULL };
#else
    const char *const types[] = { "tiff", "anim", "mpg", "mpeg", (const char *) NULL };
#endif
    id pan = [OpenPanel new];
    const char *const *filenames;
    char filename[FILENAME_MAX];

    if (![pan runModalForTypes:types]) return NO;
    if ((filenames = [pan filenames]) == NULL) return NO;
    sprintf(filename,"%s/%s", [pan directory], filenames[0]);
    return [self openFile:filename];
 }

- (BOOL)load:sender
{
     fromStream = NO;
     noOriginals = NO;
     return [self open:sender];
}
 
- makeWindow;
 {
    Window *w=[self window];
    
    [w sizeWindow:maxSize.width:maxSize.height]; /* will recache images */
    [w setMiniwindowIcon:"movieDoc.tiff"];
    [w makeKeyAndOrderFront:self];
    [w display];
    return self;
 }
 
- makePanel:(char *)filename;
 {
    char ptitle[FILENAME_MAX];
    char *ptr=rindex(filename,'/')+1;
    
    if (ptr == (char *)1) ptr=filename;
    sprintf(ptitle,"Controls for %s",ptr);
    [panel setTitle:ptitle];
    [panel setNextResponder:[self window]];
    [depthButtons selectCellAt:(int)dmode:0];
    [self setFps:frameRate];
    [self updateControls];
    [panel orderFront:self];
    return self;
 }
 
- setFps:(float)fps;
 {
     frameRate = fps;
    [fpsSlider setFloatValue:fps];
    [fpsText setFloatValue:fps];
    switch(state) {
	case FORWARD: 
	case REVERSE: if (anim) {
	    double period = 1.0/frameRate;
	    [anim free];
	    anim = [[Animator alloc] initChronon:period adaptation:0.05
				    target:self action:@selector(tick:)
				    autoStart:YES eventMask:0];
	    break;
	    };
	case STOPPED: break;
    };
    return self;
 }

- setNoOriginals;
 {
    int i;
    Window *w=[self window];

    if (noOriginals) return self;
    if (movieFrame) for (i=0; i<numFrames; i++) if (movieFrame[i].original) {
	[movieFrame[i].original free];
	movieFrame[i].original = nil;
    }
    noOriginals = TRUE;
    [w setMinSize:&maxSize];
    [w setMaxSize:&maxSize];
    return self;
 }
 
//
//	Terrible kludge to be able to accept event from both 
//	the panel and the window (to close). Why can there
//	be no two parallel sessions? It seems to crash, then ...
//

- beginModal;
{
    [NXApp beginModalSession:&panelSession for:panel];
    return self;
}

#define EVENT 0

- runModal;
{
#if EVENT
    NXEvent ev;
    
    if ([NXApp peekNextEvent:[[self window] eventMask] into:&ev]) {
    	if (ev.window==[[self window] windowNum]) {
	    [NXApp endModalSession:&panelSession];
	    [NXApp beginModalSession:&windowSession for:[self window]];
	    [NXApp runModalSession:&windowSession];
	    [NXApp endModalSession:&windowSession];
	    [NXApp beginModalSession:&panelSession for:panel];
	}
    }
    if ([NXApp peekNextEvent:[panel eventMask] into:&ev]) {
    	if (ev.window==[panel windowNum]) {
#endif
	    [NXApp runModalSession:&panelSession];
#if EVENT
	}
    }
#endif
    return self;
}

- endModal;
{
    [NXApp endModalSession:&panelSession];
    return self;
}


- (BOOL)openFile:(char *)filename
 {
    if (VERBOSE) debug("openFile: %s\n", filename);
    [self makePanel:filename];
    [[self window] setTitleAsFilename:filename];
    moviePath = copy(filename);
    numFrames = 0;
    loading = YES;
    [self fwd:self];
    return YES;
}




- addBitmap:bm copy:(BOOL)copyFlag;
 {
    NXSize sz;
    NXRect r;
    int flen, flag=0;
    double period;

//    if (VERBOSE) debug("Add bitmap %d/%d (%d)\n", currentFrameIndex, numFrames, stopFrame);
    [bm getSize:&sz];
    if (sz.width > maxSize.width) maxSize.width=sz.width, flag=1;
    if (sz.height > maxSize.height) maxSize.height=sz.height, flag=1;
    if (flag || !numFrames) [[self window] sizeWindow:maxSize.width:maxSize.height];
    if (!numFrames) [self makeWindow];
    flen = ([bm pixelsWide]*[bm pixelsHigh]*[bm bitsPerPixel])>>3;
    if (numFrames*flen>20000000) [self setNoOriginals];
    if (!movieFrame) {
	currentFrameIndex = 0;
	NX_MALLOC(movieFrame, movieFrameStruct, currentFrameIndex+1);
    } else {
	currentFrameIndex++;
	NX_REALLOC(movieFrame, movieFrameStruct, currentFrameIndex+1);
    }
    if (numFrames<=currentFrameIndex) numFrames = currentFrameIndex+1;
    if (!noOriginals)
	movieFrame[currentFrameIndex].original = copyFlag?[bm copy]:bm;
    else movieFrame[currentFrameIndex].original=nil;
 //    printf("Frame # %d, Size: %8.3f %8.3f\n", currentFrameIndex, sz.width, sz.height);
    if (!fromStream || !currentFrameIndex) {
	movieFrame[currentFrameIndex].image=[[NXImage alloc] initSize:&sz];
	[movieFrame[currentFrameIndex].image setUnique:YES]; /* make caches disjoint */
	[movieFrame[currentFrameIndex].image setBackgroundColor:NX_COLORBLACK];
    } else {
	movieFrame[currentFrameIndex].image=movieFrame[currentFrameIndex-1].image;
    }
    [self getBounds:&r];
    if ([movieFrame[currentFrameIndex].image lockFocus]) {
	[bm drawIn:&r];
	[movieFrame[currentFrameIndex].image unlockFocus];
    } else error("Could not lock focus on image");
    [self display];
    if (noOriginals && !copyFlag) [bm free];
    period = 1.0/frameRate;
    [self runModal];
    while (currentFrameIndex>=stopFrame || state==STOPPED) {
	if (state==STOPPED) stopFrame = currentFrameIndex;
	[self runModal];
	usleep(10000);
	if (willClose) return self;
    }
    frameCount++;
    NXPing();
    return self;
 }

- loadFrames
{
    char *ptr, *p1, *p2;
    int n, i;

    if (VERBOSE) debug("loadFrames: %d %s\n", state, moviePath);
    if (!moviePath) error("Cannot load frames: no path\n");
    stopFrame = 0;
    currentFrameIndex = 0;

  /* get the bitmaps */
    ptr = rindex(moviePath,'.');
    if (ptr && !strcmp(ptr, ".anim")) {  /* the file is an Icon-style .anim directory */
	p1 = rindex(moviePath,'/');
	p1 = p1?(p1+1):moviePath;
	p2 = stringf("%s", p1);
	p2[ptr-p1] = '\0';
	[self openAnimDirectory: stringf("%s/%s", moviePath, p2)];
    }
    else if (ptr && (!strcmp(ptr,".mpg")||!strcmp(ptr,".mpeg"))) { /* an MPEG file */
	[self openMPEGfile:moviePath];
    }
    else if (ptr && !strcmp(ptr,".tiff")) { /* a slew o' TIFFs in one file */
	List *bitmaps;
	
	bitmaps = [NXBitmapImageRep newListFromFile:moviePath];
	if (!bitmaps) {
	    NXRunAlertPanel(NULL,"Couldn't get bitmaps from %s",
		NULL,NULL,NULL, moviePath);
	    return self;
	};
	n = [bitmaps count];
	for (i=0; i<n; i++) [self addBitmap:[bitmaps objectAt:i] copy:NO];
	[bitmaps free]; /* does not free elements */
    }
#if XANIM
    else if ([self openAnimation:moviePath]) {}
#endif
    else { /* this shouldn't happen */
	[self stop:self];
	NXRunAlertPanel(NULL,"Unknown movie type %s",NULL,NULL,NULL,moviePath);
	return self;
    };
    if (willClose) return self;
    if (fromStream) { 
	[self freeFrames];
	movieFrame = NULL;
	[self updateControls];
	if (numFrames>1 && mode == LOOP) [self loadFrames];
	if (willClose) return self;
	if (mode == BOUNCE) 
	    NXRunAlertPanel(NULL, "Cannot bounce while loading",NULL,NULL,NULL);
    }
    [self endModal];
    [self updateControls];
    loading = NO;
    [self fwd:self];
    return self;
 }


- (List *)listAnimDirectory:(char *)filenameRoot
 {
  List *bitmaps = [[List alloc] init];
  int i=0;
  
  while (1) {
    char buf[FILENAME_MAX];
    NXBitmapImageRep *newbitmap;
    sprintf(buf,"%s.%d.tiff",filenameRoot,i++);
    if ((access(buf,R_OK)) == -1) break;
    newbitmap = [[NXBitmapImageRep alloc] initFromFile:buf];
    if (!newbitmap) {
      NXRunAlertPanel(NULL,"Couldn't get bitmap from %s",NULL,NULL,NULL,
                    buf);
      [[bitmaps freeObjects] free];
      return nil;
      }
    else
      [bitmaps addObject:newbitmap];
    };
  return bitmaps;
 }

- openAnimDirectory:(char *)rfn;
 {
    int i;
    NXBitmapImageRep *newbitmap;
    char *fn;
    
    for (i=1; TRUE; i++) {
	fn = stringf("%s.%d.tiff", rfn, i);
	if ((access(fn, R_OK)) == -1) break;
	newbitmap = [[NXBitmapImageRep alloc] initFromFile:fn];
	if (!newbitmap) {
	    NXRunAlertPanel(NULL,"Couldn't get bitmap from %s",NULL,NULL,NULL, fn);
	    continue;
	}
	[self addBitmap:newbitmap copy:NO];
	if (willClose) break;
    }
    return self;
 }
 
#if XANIM
- (BOOL)openAnimation:(const char *)filename
 {
    int i;
    NXBitmapImageRep *bm;
    Animation *a;
    
    a = [[Animation alloc] initFrom:filename];
    if (!a) {
	[a free];
	return NO;
    }
    for (i=0; TRUE; i++) {
	bm = [a next];
	if (bm) [self addBitmap:bm copy:YES];
	if ([a isLast] || willClose) break;
    }
    [a getMaxSize:&maxSize];
    return YES;
 }
 #endif
 
- openMPEGfile:(char *)filename
 {
    int i, n, flen;
    char command[256];
    FILE *fd;
    mpegInfo *pInfo;
    long data;
    BOOL swab;
    NXStream *pStream;
    NXBitmapImageRep *bm;
    
    if (!(fd = fopen(filename, "r"))) error("Could not open %s", filename);
    fread(&data, 4, 1, fd);
    if (data!=0x000001b3 && data!=0xb3010000)
	error("%s does not contain an mpeg stream: %x", filename, data);
    if (data==0xb3010000) swab=YES; else swab=NO;
    if (!(pInfo = calloc(1, sizeof(mpegInfo)))) error("Could not allocate pInfo.");
	// Get horizontal and vertical size of image space
	// as two 12 bit words, respectively
	// then aspect ratio and picture rate
	// as two 4 bit words.
    fread(&data, 4, 1, fd); if (swab) data = NXSwapLong(data);
    pInfo->picture_rate = 0x0F & data;
    data >>= 4;
    pInfo->aspect_ratio = 0x0F & data;
    data >>= 4;
	// In Motorola format, least significant bits come last
	// v_size is actually the second value in the file
	// i.e. h:12,v:12,a:4,p:4
    pInfo->v_size = 0x0FFF & data;
    pInfo->h_size = 0x0FFF & data >> 12;
    maxSize.width = ((pInfo->h_size + 15) / 16) * 16.0;
    maxSize.height = ((pInfo->v_size + 15) / 16) * 16.0;
	// Get bit rate, vbv buffer size, and constrained parameter flag
    fread(&data, 4, 1, fd); if (swab) data = NXSwapLong(data);
	// throw away (non) intra quant matrix flags
    data >>= 2;
    pInfo->const_param_flag = 1 & data;
    data >>= 1;
    pInfo->vbv_buffer_size = 0x03FF & data;
    data >>= 10 + 1;	// 1 marker bit
    pInfo->bit_rate = 0x03FFFF & data;
    fclose(fd);
    pInfo->fps = pInfo->picture_rate;
    flen = 3 * (int)maxSize.width * (int)maxSize.height;
    
 //    printf("Dimensions: %d %d, buffer size:%d\n", (int)maxSize.width, (int)maxSize.height, flen);
    sprintf(command, "exec %s/mpegDecode %s",
	[[NXBundle mainBundle] directory], filename);
    if (!(fd = popen(command, "r")))
	error("Could not create MPEG process:\n   %s", command);
    pStream = NXOpenFile(fileno(fd), O_RDONLY);

 //    printf("Reading frames:\n");
    if (!numFrames) [self setFps:pInfo->fps];
    bm = [[NXBitmapImageRep alloc] initData:NULL
	    pixelsWide:(int)maxSize.width
	    pixelsHigh:(int)maxSize.height
	    bitsPerSample:8
	    samplesPerPixel:3	// (cSpace == RGB_COLOR) ? 3 : 1
	    hasAlpha:NO
	    isPlanar:NO
	    colorSpace:NX_RGBColorSpace
	    bytesPerRow:0
	    bitsPerPixel:0
    ];
    for (i=0; TRUE; i++) {
	if (4!=NXRead(pStream, &data, 4)) break;
 //	printf("Frame # %d.\n", data);
	n = NXRead(pStream, [bm data], flen);
	if (n!=flen) error("Error reading image data (%d/%d bytes read).", n, flen);
	[self addBitmap:bm copy:YES];
	if (willClose) break;
    }
    [bm free];
    if (numFrames<2) {
	NXRunAlertPanel("Read MPEG",
	    "Problem reading mpeg stream, no frames found",NULL,NULL,NULL);
    }
    return self;
 }

NXWindowDepth deps[] = {
  NX_DefaultDepth, NX_TwoBitGrayDepth,
  NX_EightBitGrayDepth, NX_TwelveBitRGBDepth,
  NX_TwentyFourBitRGBDepth /*,NX_PurinaCatChow__ChowChowChowDepth*/
  };

// set up Frame data structures and find max frame size
- allocateFrames:(List *)frames
 {
  int i;
  numFrames=[frames count];
  NX_MALLOC(movieFrame,movieFrameStruct,numFrames);
  for(i=0;i<numFrames;i++) {
    NXImage *nxi;
    NXBitmapImageRep *bm=[frames objectAt:i];
    NXSize sz;
    [bm getSize:&sz];
    movieFrame[i].original=bm;
    nxi=movieFrame[i].image=[[NXImage alloc] initSize:&sz];
    [nxi setUnique:YES]; /* make caches disjoint */
    [nxi setBackgroundColor:NX_COLORBLACK];
    /* keep track of largest frame */
    if (sz.width > maxSize.width) maxSize.width=sz.width;
    if (sz.height > maxSize.height) maxSize.height=sz.height;
    };
  return self;
 }

/*****************************************************************
 *****************************************************************/
- superviewSizeChanged:(NXSize *)old
 {
    [anim stopEntry];
    [super superviewSizeChanged:old];
    if (noOriginals) {
	if (!fromStream)
	    NXRunAlertPanel("Resize","Can't resize, no originals.",NULL,NULL,NULL);
    }
    else if (!loading) {
	[self recache];
	[self renderFrames];
    }
    if (movieFrame) [[self window] display];
    NXPing();
    [anim resetRealTime];
    [anim startEntry];
    return self;
 }

- renderFrames
 {
    int cfi;
    NXRect r;

    error("Fix renderFrames!\n");
    [self getBounds:&r];
    cfi = currentFrameIndex;
    for(currentFrameIndex=0; currentFrameIndex<numFrames; currentFrameIndex++) {
	if ([movieFrame[currentFrameIndex].image lockFocus]) {
	    [movieFrame[currentFrameIndex].original drawIn:&r];
	    [movieFrame[currentFrameIndex].image unlockFocus];
	} else {
	    fprintf(stderr,"Barf: couldn't lockFocus on image %d\n",
		    (int)movieFrame[currentFrameIndex].image);
	    abort();
	}
	[self display];
	NXPing();
    }
    currentFrameIndex = cfi;
    return self;
 }

- recache
 // assume depth & size both changed 
 //
 // Appkit bug?  Can one render down from 24 bit color to 2 bit gray?
 //
 {
    NXRect r;
    int i;
    [self getBounds:&r];
    [self freeCaches];
    for(i=0; i<numFrames; i++) {
	movieFrame[i].image=[[NXImage alloc] initSize:&r.size];
	[movieFrame[i].image useCacheWithDepth:deps[(int)dmode]];
    };
    return self;
 }

- save:sender
 {
  const char *type = "anim"; // will only save in .anim format.
  SavePanel *sp = [SavePanel new];
  [sp setDelegate:self];
  [sp setRequiredFileType:type];
  if ([sp runModal]) { // OK was hit
    int i;
    char cwd[MAXPATHLEN];
    /* if directory exists, rename it with a wiggle in back. */
    if (access([sp filename],F_OK) == 0) {
      /* I could do this with a couple of calls to system(), but noooo,
       * I had to do it the had way. yeccch. */
      char *buf=malloc(strlen([sp filename]+2));
      sprintf(buf,"%s~",[sp filename]);
      if (!getwd(cwd)) {
        NXRunAlertPanel("FATAL","Couldn't get current directory.",NULL,NULL,NULL);
        abort();
        };
      if (rename([sp filename],buf) == -1) {
        // sledgehammer time.
        struct direct *de;
        DIR *dp;
        chdir([sp filename]);
        dp=opendir(".");
        while(de=readdir(dp)) unlink(de->d_name);
        closedir(dp);
        chdir(cwd);
        unlink([sp filename]);
        };
      };
    mkdir([sp filename],0755);
    chdir([sp filename]);
    for(i=0;i<numFrames;i++) {
      char buf3[MAXPATHLEN];
      char buf2[MAXPATHLEN];
      char *ptr;
      int fd;
      NXStream *s;
      strcpy(buf3,[sp filename]);
      ptr=rindex(buf3,'/')+1;
      *(rindex(ptr,'.'))='\0';
      sprintf(buf2,"./%s.%d.tiff",ptr,i+1);
      fd=open(buf2,O_WRONLY|O_CREAT,0644);
      s=NXOpenFile(fd,NX_WRITEONLY);
      [movieFrame[i].image writeTIFF:s];
      NXClose(s);
      close(fd);
      };
    chdir(cwd);
    };
  return self;
 }

- (BOOL) panelValidateFilenames:sender
 {
  if (!strcmp([sender filename],moviePath)) {
    NXRunAlertPanel("Save","Cannot overwrite original movie",NULL,NULL,NULL);
    return NO;
    };
  return YES;
 }

- freeCaches
 {
    int i;

    if (fromStream) [movieFrame[currentFrameIndex].image free];
    else for(i=0;i<numFrames;i++) [movieFrame[i].image free];
    return self;
 }

- freeOriginals
 {
    int i;
    if (!noOriginals) for(i=0;i<numFrames;i++) {
	[movieFrame[i].original free];
	movieFrame[i].original = nil;
    }
    return self;
 }

- freeFrames
{
    [self freeCaches];
    if (!noOriginals) [self freeOriginals];
    NX_FREE(movieFrame);
    return self;
}

- free
{
    cfree(moviePath);
    [self freeGState];
    if (anim) [anim free];
    return [super free];
 }

- copy:sender
 {
    char *buffer;
    NXStream *stream;
    int length, maxLength;
    Pasteboard *pasteboard = [Pasteboard new];
    runState s=state;

    [anim stopEntry];
    if (state!=STOPPED) [self stop:self];
    [pasteboard declareTypes:&NXPostScriptPboardType num:1 owner:self];
    stream = NXOpenMemory(NULL, 0, NX_WRITEONLY);
    [self copyPSCodeInside:&bounds to:stream];
    NXFlush(stream);
    NXGetMemoryBuffer(stream, &buffer, &length, &maxLength);
    [pasteboard writeType:NXPostScriptPboard data:buffer length:length];
    NXCloseMemory(stream, NX_FREEBUFFER);
    switch(s) {
	case STOPPED: break;
	case FORWARD: 
	case REVERSE: [anim startEntry]; break;
    };
    return self;
 }

- tick:sender
 {
    int next, end;

//    if (VERBOSE) debug("tick: %d %d\n", state, loading);
    if (state==STOPPED) error("Orphaned tick\n");
    if (loading) {
	if (movieFrame) {
	    if (stopFrame<=currentFrameIndex) stopFrame++;
	    return self;
	}
	if (state!=FORWARD) error("Wrong loading direction: %d\n", state);
	[self beginModal];
	[self loadFrames];
	[self endModal];
	if (willClose) [window performClose:self];
	return self;
    }
    end = (state == FORWARD) ? numFrames-1 : 0;
    switch(mode) {
	case ONCE:
	    if (currentFrameIndex == end) {
		[self stop:self];
		return self;
	    } else currentFrameIndex += (int)state;
	    break;
	case LOOP:
	    next = currentFrameIndex + (int)state;
	    if (((state == FORWARD)&&(next>end)) ||
		((state == REVERSE)&&(next<end))) {
		currentFrameIndex = (state < 0) ? numFrames-1 : 0;
	    } else currentFrameIndex = next;
	    break;
	case BOUNCE:
	    next = currentFrameIndex + (int)state;
	    if (((state == FORWARD)&&(next>end)) ||
		((state == REVERSE)&&(next<end))) {
		if (state == FORWARD) [self selectStateButton:REV];
		if (state == REVERSE) [self selectStateButton:FWD];
		state *= -1;
		currentFrameIndex += (int)state;
	    } else currentFrameIndex = next;
	    break;
    };
    frameCount++;
    [self display];
    return self;
 }

/*****************************************************************
 *****************************************************************/

- fwd:sender
 {
    if (VERBOSE) debug("fwd: \n");
    if (state != STOPPED) [self stop:self];
    state = FORWARD;
    [self move:sender];
    return self;
 }

- rev:sender
 {
    if (VERBOSE) debug("rev: \n");
   if (loading) {
	NXRunAlertPanel(NULL,"Cannot reverse while loading.",NULL,NULL,NULL);
	return self;
    }
    if (state != STOPPED) [self stop:self];
    state = REVERSE;
    [self move:sender];
    return self;
 }

- move:sender
{
    double period = 1.0/frameRate;

    if (VERBOSE) debug("move: %d\n", state);
    anim = [[Animator alloc] 
	initChronon:period adaptation:0.05 /*?*/
	target:self action:@selector(tick:)
	autoStart:YES eventMask:0
    ];
    if (state == FORWARD) [self selectStateButton:FWD];
    if (state == REVERSE) [self selectStateButton:REV];
    [fNumText setStringValue:""];
    [fNumSlider setEnabled:NO];
    oldt=[anim getSyncTime];
    frameCount = 0;
    return self;
}

- stop:sender
 {
    if (loading && state==STOPPED) {  /* Quit this movie even while loading  */
	willClose = YES;   /* Has to propagate through recursive loadFrames calls */
	return self;
    }
    switch(state) {
	case FORWARD:
	case REVERSE: if (anim) [anim free]; anim=nil;
	case STOPPED: break;
    }
    state = STOPPED;
    [self selectStateButton:STOP];
    [self updateControls];
//    [self display];
    return self;
 }

- fwdStep:sender
 {
    [self step:(int) FORWARD];
    return self;
 }

- revStep:sender
 {
    if (loading) {
	NXRunAlertPanel(NULL, "Cannot reverse while loading.",NULL,NULL,NULL);
	return self;
    }
    [self step:(int) REVERSE];
    return self;
 }

- step:(int) direction
 {
    if (VERBOSE) debug("Step: %d    %d %d %d\n", direction,
	currentFrameIndex, stopFrame, numFrames);
    if (state != STOPPED) [self stop:self];
    if (loading) {
	stopFrame += direction;
	state = direction;
    } else {
	if (((currentFrameIndex = currentFrameIndex + direction) % numFrames) < 0)
	currentFrameIndex = numFrames + currentFrameIndex;
    }
    [self selectStateButton:STOP];
    return [self display];
 }

- reSize:(NXSize *)s
{
    if (noOriginals)
	NXRunAlertPanel("Resize","Resize disabled for lack of memory.",NULL,NULL,NULL);
    else [[self window] sizeWindow:s->width :s->height];
    return self;
}

- expand2x:sender
 {
  NXRect r;
  [self getBounds:&r];
  r.size.width *= 2.0;
  r.size.height *= 2.0;
  [self reSize:&r.size];
  return self;
 }

- reduce50pct:sender
 {
  NXRect r;
  [self getBounds:&r];
  r.size.width *= 0.5;
  r.size.height *= 0.5;
  [self reSize:&r.size];
  return self;
 }

- restore:sender
 {
  [self reSize:&maxSize];
  return self;
 }


- modeButtonsChanged:sender
 {
    mode = (runMode)[sender selectedRow];
    return self;
 }

- fNumSliderChanged:sender
 {
    stopFrame = [sender intValue];
    if (loading || currentFrameIndex == stopFrame) return self;
    currentFrameIndex = stopFrame;
    [self stop:self];
    [self display];
    return self;
 }

- fpsSliderChanged:sender
 {
    [self setFps:[sender floatValue]];
    return self;
 }

- pingButtonChanged:sender
 {
  switch([sender selectedRow]) {
    case 0: pingDuringDisplay=NO; break;
    case 1: pingDuringDisplay=YES; break;
    };
  return self;
 }

- selectStateButton:(runState)b
 {
  [stateButtons selectCellAt:0:((int)b)];
  return self;
 }

- depthButtonsChanged:sender
 {
    if (noOriginals) {
	NXRunAlertPanel("Depth","Can't change depth, no originals.",NULL,NULL,NULL);
	return self;
    }
    dmode=(depthMode)[sender selectedRow];
    [anim stopEntry];
    [self recache];
    [self renderFrames];
    [self display];
    [anim resetRealTime];
    [anim startEntry];
    return self;
 }

- updateCheckBoxChanged:sender
 {
  updateControls = !updateControls;
  return self;
 }

- frameCheckBoxChanged:sender
 {
    showFrameNumber = !showFrameNumber;
    return self;
 }


// Window's delegate methods

- windowWillClose:sender
 {
  [panel close];
  [self free];
  return self;
 }

-windowDidMiniaturize:sender
 {
  [panel orderOut:sender];
  [anim stopEntry];
  return self;
 }

-windowDidDeminiaturize:sender
 {
  [panel orderFront:sender];
  [anim resetRealTime];
  [anim startEntry];
  return self;
 }
@end

These are the contents of the former NiCE NeXT User Group NeXTSTEP/OpenStep software archive, currently hosted by Netfuture.ch.