/*
 *
 *
 * 	CubicSlider 1.3, subclassed by Raymond Lutz mai 1993, 
 * 	class description in the accompaining 
 *      file CubicSliderCell.rtf
 *
 */

#import <dpsclient/psops.h>
#import <appkit/publicWraps.h> 
#import <math.h>
#import <appkit/graphics.h>
#import <appkit/Application.h>
#import <appkit/Form.h>
#import <appkit/Button.h>
#import <appkit/ButtonCell.h>
#import <appkit/Box.h>
#import <appkit/Text.h>
#import <appkit/Font.h>
#import "CubicSliderCell.h"
#import <objc/objc.h>

#define ONETHIRD (double)0.3333333333333
#define MINEDITDEFAULT YES
#define MAXEDITDEFAULT YES
#define CUBICDEFAULT YES
#define INAFEWSECS (float)0.001

#define XFORM (float)15
#define WFORM (float)52
#define XLEFTBUTT (float)0
#define XRIGHTBUTT  (float)70
#define WIDTHBUTT (float)15
#define WTOTAL   (float)85
 
 
// _______________________________________ SSSSSTTTTRRRREEETTTTCCCHH _ _ _ _ _ _ _ _ _ _ _ _ _ _  > 

@implementation CubicSliderCell
{}
- init
{
	id retVal;
	retVal = [super init];
	[self calcNewFct];
	[self setCubic: CUBICDEFAULT];
	[self setMinEditable: MINEDITDEFAULT];
	[self setMaxEditable: MAXEDITDEFAULT];
	[self clearAbsoluteMaxValue];
	[self clearAbsoluteMinValue];
	yFlat=value;
	return retVal;
}
- (BOOL) maxIsEditable;
{
    return maxIsEditable;
}
- (BOOL) minIsEditable;
{
    return minIsEditable;
}
- setMinEditable:(BOOL) aFlag;
{
   minIsEditable = aFlag;
   return self;
}
- setMaxEditable:(BOOL) aFlag;
{
   maxIsEditable = aFlag;
   return self;
}
  
- (BOOL) absoluteMaxValueSet;
{
    return absoluteMaxValueSet;
}
- (BOOL) absoluteMinValueSet;
{
    return absoluteMinValueSet;
}
- clearAbsoluteMaxValue;
{
    absoluteMaxValueSet = NO;
    absoluteMaxValue = maxValue;
    return self;
}

- clearAbsoluteMinValue;
{
    absoluteMinValueSet = NO;
    absoluteMinValue = minValue;
    return self;
}

- setAbsoluteMaxValue:(double) aDouble;
{
    absoluteMaxValueSet = YES;
    absoluteMaxValue = aDouble;
    if (maxValue > absoluteMaxValue) [self setMaxValue: absoluteMaxValue];
    return self;
}

- setAbsoluteMinValue:(double) aDouble;
{
    absoluteMinValueSet = YES;
    absoluteMinValue = aDouble;
    if (minValue < absoluteMinValue) [self setMinValue: absoluteMinValue];
    return self;
}

- (double) absoluteMaxValue;
{
	return absoluteMaxValue;
}

- (double) absoluteMinValue;
{
	return absoluteMinValue;
}


- setMinValue: (double) val
{
	id retval;
	double bufCubicVal = [self doubleValue];
	retval=[super setMinValue:val];	
	if (absoluteMinValueSet && val < absoluteMinValue) [self setAbsoluteMinValue: val];
	if (cubic) [self setDoubleValue: bufCubicVal];
	return retval;
}

- setMaxValue: (double) val
{
	id retval;
	retval=[super setMaxValue:val];	
	if (absoluteMaxValueSet && val > absoluteMaxValue) [self setAbsoluteMaxValue: val];
	if (cubic) [self calcNewFct];
	return retval;
}


- setTarget:anObject
{
    target = anObject;
	return self;
}

- setAction:(SEL)anAction
{
    action = anAction;
   	return self;
}

- newCover
{
    id font = [Font newFont: "Helvetica" size: 8.0];
    NXRect knobButtonFrame, quadButtonFrame;
    NXRect formFrame, minMaxViewFrame;

    NXSetRect(&formFrame, XFORM,0,WFORM,16);
    form=[[[[Form alloc]
    	initFrame: &formFrame]
	setFont: font]
	setBackgroundGray:NX_LTGRAY]; 
    [form addEntry:" "
	tag:0
	target:self
	action:@selector(takeFormValueForNew)]; 
    [[form setTextAlignment:NX_RIGHTALIGNED] sizeToFit];

    NXSetRect(&knobButtonFrame, 0,0, WIDTHBUTT,16);

    knobButton=[[Button alloc] initFrame: &knobButtonFrame
	icon:"NXscrollMenuDown"
	tag: 0
	target: self
	action: @selector(takeCurrentSliderValueForNew)
	key: '\0'
	enabled:YES];
    [[knobButton setFont:font] setAlignment: NX_CENTERED];

    NXSetRect(&quadButtonFrame, 0,0, WIDTHBUTT,16);
    quadButton=[[Button alloc] initFrame: &quadButtonFrame
	title: "<"
	tag: 0
	target: self
	action: @selector(quadRange)
	key: '\0'
	enabled:YES];
    [[quadButton setFont:font] setAlignment: NX_CENTERED];

    NXSetRect(&minMaxViewFrame, 0,0, WTOTAL,16);
    minMaxView=[[View alloc] initFrame: &minMaxViewFrame];


    cover=[[[Button alloc] initFrame: &minMaxViewFrame
	title: ""
	tag: 0
	target: self
	action: (SEL)0
	key: '\0'
	enabled:YES]
	setBordered: NX_NONE]; 
    [[cover cell] setHighlightsBy: NX_NONE];
    [minMaxView addSubview:knobButton];
    [minMaxView addSubview:quadButton];
    [minMaxView addSubview:form];
    [cover addSubview:minMaxView];

    return self;
}    

- drawSelf:(const NXRect *)cellFrame inView:controlView
{
    if (editing) {
         PSsetgray(NX_LTGRAY);
         NXRectFill(cellFrame);
         [minMaxView display];
	 return self;
    }
    else return [super drawSelf: cellFrame inView:controlView];
}

- drawInside:(const NXRect *)cellFrame inView:controlView
{
    if (editing) {
         PSsetgray(NX_LTGRAY);
         NXRectFill(cellFrame);
         [minMaxView display];
	 return self;
    }
    else return [super drawInside: cellFrame inView:controlView];
}
- (BOOL)trackMouse:(NXEvent *)theEvent
	inRect:(const NXRect *)cellFrame
	ofView:controlView
{
	NXRect knobFrame;
	BOOL retval;
	BOOL isFlipped = NO;
	BOOL onTheKnob;
	int AltKeyPressed = (theEvent->flags & NX_ALTERNATEMASK);
	int nClicks = theEvent->data.mouse.click;
	NXPoint	aPoint = theEvent->location;
	float mouseX ;
	float leftKnobSide,rightKnobSide,midKnob;
	BOOL dbleClick = (nClicks == 2);

	[controlView convertPoint:&aPoint fromView:nil];
	mouseX = aPoint.x;
	[self getKnobRect:&knobFrame flipped:isFlipped];
	leftKnobSide = NX_X(&knobFrame);
	rightKnobSide = NX_MAXX(&knobFrame);
	midKnob = (rightKnobSide+leftKnobSide)/2;
	onTheKnob = (leftKnobSide <= mouseX && mouseX <= rightKnobSide);

	if (AltKeyPressed) {
	    if (dbleClick) {
	       [self flipCubicFlag];
	       [self notifyTargets];
	    }
	    return NO; // don't track
	} 	

	if (dbleClick && onTheKnob && previousClickOnTheKnob) {
	      extremumType = (mouseX > midKnob);
	      if (extremumType && maxIsEditable || !extremumType && minIsEditable) {
//	    enter editing mode here 
	        [self popFormAndButtons];
	      } else NXBeep(); // no editing: beep.
	      return NO; // don't track, we pop (don't we?) 
	}
	previousClickOnTheKnob = onTheKnob;

	if (nClicks > 2) return NO; // prevents knob to reappear over 

	     retval=[super trackMouse:theEvent inRect:cellFrame ofView:controlView];
	     if (upTarget) [controlView sendAction:upAction to:upTarget];
	     return retval;
	}

- (BOOL)startTrackingAt:(const NXPoint *)startPoint
	inView:controlView
{
    mouseIsUp=NO;
    return [super startTrackingAt: startPoint inView:controlView];
}

- stopTracking:(NXPoint *)lastPoint 
	at:(NXPoint *)endPoint 
	inView:controlView 
	mouseIsUp:(BOOL)flag
{
    mouseIsUp = flag;
    [self doubleValue];
    yFlat = [self doubleValue]; // records the chosen value
    if (cubic && [super isContinuous]) [self calcNewFct];
    return self;
}


- setCubic: (BOOL) aFlag
{
  cubic = aFlag;
  return self;
}
- flipCubicFlag;
{
  cubic = !cubic;
  return self;
}  

- (BOOL) isCubic;
{
  return cubic;
}

- (BOOL) mouseIsUp
{
    return mouseIsUp;
}


- setEnabled: (BOOL) aFlag
{
  if (editing) {
	 [quadButton setIcon: aFlag ? "NXscrollMenuRight":"NXscrollMenuRightD"];
	 [knobButton setIcon: aFlag ? "NXscrollMenuDown":"NXscrollMenuDownD"];
	 [quadButton setEnabled:aFlag];
	 [knobButton setEnabled:aFlag];
	 [form setEnabled:aFlag];
  }
  return [super setEnabled:aFlag];
}

- setIntValue:(int) anInt
{
    yFlat = anInt;
    if (cubic) [self calcNewFct];
    else [super setIntValue: anInt];
    return self;    
}

- setFloatValue:(float) aFloat
{
    yFlat = aFloat;
    if (cubic) [self calcNewFct];
    else [super setFloatValue:aFloat];
    return self;    
}

- setDoubleValue:(double)aValue
{
    yFlat = aValue;
    if (cubic) [self calcNewFct];
    else [super setDoubleValue:aValue];
    return self;    
}



- (float) floatValue
{
	float aFloat=[self doubleValue];
    	return aFloat;
}

- (int) intValue
{
	int anInt=[self doubleValue];
    	return anInt;
}

- (double) doubleValue

/*
 * It's here that we modify the true slider value to be non-linear,
 * that is, pass it through a simple cubic polynomial of the form
 * y=x^3, adequately x and y translated to have the flat region around the
 * present (seen from the target) slider value and scaled to passes by
 * the points (minValue,minValue) and (maxValue,maxValue). Those parameters,
 * A, xFlat and yFlat are recalculated in calcNewFct on each mouseUp.
 *
 */

{
    double doubleValue = cubic ? (A*pow((value - xFlat),3.0) + yFlat):value;
    doubleValue = (doubleValue > maxValue) ? maxValue: doubleValue;
    doubleValue = (doubleValue < minValue) ? minValue: doubleValue;
    return doubleValue;
}

- (int) linearIntValue;
{
	return [super intValue];
}

- (float) linearFloatValue;
{
	return [super floatValue];
}

- (double) linearDoubleValue;
{
	return [super doubleValue];
}
- setUpTarget:anObject action:(SEL)anAction
{
    upTarget = anObject;
    upAction = anAction;
    return self;
}
    
- setUpTarget:anObject
{
    upTarget = anObject;
	return self;
}
- setUpAction:(SEL)anAction
{
    upAction = anAction;
   	return self;
}


- upTarget;
{
   	return upTarget;
}

- (SEL)upAction;
{
   	return upAction;
}



// inner work methods 

- calcNewFct

/* 
 * Before looking at this, see (double)doubleValue above.
 *
 * Here, given minValue, maxValue and the chosen slider value
 * we calculate the cubic parameters so that the function
 *
 * 		y = A*(x - xFlat)^3 + yFlat
 *
 * 	[1] passes through (minValue,minValue)
 * 	[2] passes through (maxValue,maxValue)
 * 	[3] has its flatest point at y = "chosen slider value"
 *
 */
 
{
	double epsilon; // a twice used quantity
/*
 * The expression for A has an inf/inf indetermination when the knob 
 * hits the min or max value, so there are TWO ways of determine it.
 */

  	if (yFlat > (minValue + maxValue)/2.0) { 
//		here yFlat -> maxValue and epsilon -> 0 
                if (yFlat > maxValue) yFlat = maxValue;
		epsilon = pow((maxValue - yFlat) /(yFlat - minValue), ONETHIRD);
		xFlat = (maxValue + epsilon *minValue)/(1.0 + epsilon);
	} else {  
//		here yFlat -> minValue and still epsilon -> 0
                if (yFlat < minValue) yFlat = minValue;
		epsilon = pow((yFlat - minValue) /(maxValue - yFlat), ONETHIRD);
		xFlat = (minValue + epsilon * maxValue)/(1.0 + epsilon);
	}
	A=(maxValue-minValue)/(pow((maxValue-xFlat),3.0)+pow((xFlat-minValue),3.0));
        [super setDoubleValue: xFlat];
	return self;
}

- popFormAndButtons
{
    float rangeFraction;
    NXRect frame;
    BOOL doubleIsOk,knobIsOK;
    
    NXSetRect(&frame , trackRect.origin.x-1, trackRect.origin.y-1,
		trackRect.size.width+2,trackRect.size.height+2); 
		
    if (NX_WIDTH(&frame) >= WTOTAL) { 	// the slider is big enough to pop, do it
    	  editing = YES;
	  if (!cover) [self newCover];
	  [cover setFrame: &frame];
	  if (extremumType) {
	       [[form setTitle: "max" at:0] setFloatValue: maxValue at:0];
	       knobIsOK = (yFlat != minValue);
	       doubleIsOk = !absoluteMaxValueSet || (maxValue != absoluteMaxValue);
	       [quadButton setIcon: doubleIsOk ? "NXscrollMenuRight":"NXscrollMenuRightD"];
	  }
	  else { 
	       [[form setTitle: "min" at:0] setFloatValue:  minValue at:0] ;
	       knobIsOK = (yFlat != maxValue);
	       doubleIsOk = !absoluteMaxValueSet || (minValue != absoluteMinValue);
	       [quadButton setIcon: doubleIsOk ? "NXscrollMenuLeft":"NXscrollMenuLeftD"];
	  }
	  [knobButton setEnabled: knobIsOK];
	  [knobButton setIcon: knobIsOK ? "NXscrollMenuDown":"NXscrollMenuDownD"];
	  [quadButton setEnabled: doubleIsOk];

// the following is there so the right button will fall under the mouse at the right time...
// depending on wheter or not the knob hit the side and min or max editing

//	  [minMaxView setFrame:&frame];         
          rangeFraction = (maxValue - minValue) / 30;
	  if (extremumType) {	  
	       [minMaxView moveTo: NX_WIDTH(&frame) - WTOTAL:0];         
	       if (value >= maxValue - rangeFraction) {
	            [quadButton moveTo: XRIGHTBUTT :0];
		    [knobButton moveTo: XLEFTBUTT :0];
	       } else {
	            [quadButton moveTo: XLEFTBUTT :0];
		    [knobButton moveTo: XRIGHTBUTT :0];
	       }
	  } else { 
	       [minMaxView moveTo: 0:0];         
	       if (value <= minValue + rangeFraction) {
	            [quadButton moveTo: XLEFTBUTT:0];
		    [knobButton moveTo: XRIGHTBUTT:0];
	       } else {
	            [quadButton moveTo: XRIGHTBUTT:0];
		    [knobButton moveTo: XLEFTBUTT:0];
	       }
	  }


// We're ready: pop!

	  [[self controlView] addSubview: cover];		
	  [[self controlView] updateCell: self];
	  [form selectTextAt:0];
    }
    return self;
    
}


- takeCurrentSliderValueForNew
{
	newExtremumValue = yFlat;
	[form setDoubleValue:yFlat at:0];
	[self popCellAndMakeChangeSoon];
	return self;
}

- quadRange
{
	newExtremumValue = extremumType ? 4*maxValue - 3*minValue : 4*minValue - 3*maxValue ;
	[form setDoubleValue:newExtremumValue at:0];
	[self popCellAndMakeChangeSoon];
	return self;
}

- takeFormValueForNew
{
	newExtremumValue = [form floatValueAt:0];
	[self popCellAndMakeChangeSoon];
	return self;
}

void IllBeBack (teNum, now, cubicSlider) 
DPSTimedEntry teNum;
double now;
id cubicSlider;
{
    [[cubicSlider popCellAndMakeChangeNow] stopTimedEntry]; 
    return;   
}


- popCellAndMakeChangeSoon

{
        if (!teNum) teNum = DPSAddTimedEntry(INAFEWSECS, &IllBeBack, self, NX_BASETHRESHOLD);
	return self;
}

/*
 * Why this TimedEntry? because we have  buttons which actions have as effect
 * to eventually zap themselves from the view hierarchy... but since they send their
 * message _before_ finishing to draw they are still lockfocused... we must
 * let them redraw and zap them later.
 */


- popCellAndMakeChangeNow
{
	double oldValue;
	[cover removeFromSuperview]; // it's OK now: we're later
	editing = NO;
	[[self controlView] updateCell:self];
	if (extremumType) {
	     if (newExtremumValue < minValue) {NXBeep(); return self;}
	     if (absoluteMaxValueSet && newExtremumValue > absoluteMaxValue) {
	          newExtremumValue = absoluteMaxValue;
		  NXBeep();}
	     if (yFlat > newExtremumValue ) [self setDoubleValue:newExtremumValue]; //narrowing
	     oldValue = maxValue;
	     [self setMaxValue: newExtremumValue];
	     if (newExtremumValue != oldValue) [self notifyTargets];
	} else {
	     if (newExtremumValue > maxValue) {NXBeep(); return self;}
	     if (absoluteMinValueSet && newExtremumValue < absoluteMinValue) {
	          newExtremumValue = absoluteMinValue;
		  NXBeep();}
	     if (yFlat < newExtremumValue ) [self setDoubleValue:newExtremumValue]; //narrowing
	     oldValue = minValue;
	     [self setMinValue: newExtremumValue];
	     if (newExtremumValue != oldValue) [self notifyTargets];
	} 	
	return self;
}

- notifyTargets
{
  if ([target respondsTo: @selector(takeCubicSliderStatusFrom:)])
        [target takeCubicSliderStatusFrom:self];
  if (upTarget && [upTarget respondsTo: @selector(takeCubicSliderStatusFrom:)])
        [upTarget takeCubicSliderStatusFrom:self];
	return self;
  return self;
}


- stopTimedEntry
{
    	if (teNum)
         DPSRemoveTimedEntry (teNum);
    	teNum = (DPSTimedEntry)0;
	return self;
}


- read:(NXTypedStream*)stream
{
	[super read:stream];
	upTarget = NXReadObject(stream);
	NXReadTypes(stream, "@iiiiiddddd", 
	   &upAction,
	   &cubic, &absoluteMinValueSet, &absoluteMaxValueSet, &minIsEditable, &maxIsEditable,
	   &absoluteMinValue, &absoluteMaxValue, &A, &yFlat, &xFlat);
	return self;
}
- write:(NXTypedStream*)stream
{
	[super write:stream];
	NXWriteObjectReference(stream, upTarget);
	NXWriteTypes(stream, "@iiiiiddddd", 
	   &upAction,
	   &cubic, &absoluteMinValueSet, &absoluteMaxValueSet, &minIsEditable, &maxIsEditable,
	   &absoluteMinValue, &absoluteMaxValue, &A, &yFlat, &xFlat);
	return self;
}

- (const char*)inspectorName
{
    NXEvent *e = [NXApp currentEvent];
    if (e->flags & NX_ALTERNATEMASK) return "SliderInspector";
    return "CubicSliderInspector";
}

-free
{
    [minMaxView free];
    return [super free]; 
}

@end

