//
//  MiscSplitView.m
//
//  Written by Dwight D. Everhart.  Based on SideSplitView by Mary McNabb
//  of NeXT (from the ZooView mini-example).
//
//  Copyright (c) 1996, 1997 by Dwight D. Everhart.  All rights reserved.
//  Version 1.42; April 4, 1997
//
//      This notice may not be removed from this source code.
//
//  This object is included in the MiscKit by permission from the author
//  and its use is governed by the MiscKit license, found in the file
//  "LICENSE.rtf" in the MiscKit distribution.  Please refer to that file
//  for a list of all applicable permissions and restrictions.
//
//  Although this code started as SideSplitView, there is little that remains
//  of the original code.  Still, the author is grateful to Ms. McNabb for
//  providing a starting point for this code.  This is the distribution notice
//  that she put in her code:
//
//    You may freely copy, distribute, and reuse the code in this example.
//    NeXT disclaims any warranty of any kind, expressed or implied, as to its
//    fitness for any particular use.
//
//  Purpose:
//    This class implements a view which divides its frame, either vertically
//    or horizontally, into two or more adjacent areas in which are displayed
//    its subviews.  The user can change the proportion of the areas occupied
//    by the subviews relative to each other.  The class can also save and
//    restore its subview divisions to a string or a default.  The interface
//    is compatible with NXSplitView, and the divisions saving mechanism has
//    the same style of interface as Window's frame saving mechanism.
//
//  Requires:
//    MiscAppDefaults Application category
//    Dimple.tiff image (embedded into Dimple_tiff.m by Don Yacktman)
//
//  Release notes:
//    Version 1.11:  February 5, 1996
//      First release
//    Version 1.20:  April 18, 1996
//      Removed check for autosave name uniqueness.  This check caused
//      problems when an application used multiple MiscSplitViews with
//      the same autosave name, which would be the case if a MiscSplitView
//      were used as part of a document window in a multiple-document
//      architecture:  only the MiscSplitView in the first window opened
//      would have its divider positions saved in the defaults database;
//      the others would not.  If you prefer to have the check enabled,
//      you can enable it with the requireUniqueAutosaveNames: class method.
//    Version 1.21:  April 22, 1996
//      Fixed two bugs related to the autosave name hash table:
//      1. The removeDividersUsingName: class method incorrectly tried to
//         remove the hash table entries for the specified name by using
//         that name as the key.  Names are actually the values in the table;
//         the keys are the MiscSplitView objects.  The offending line was
//         removed.  Now no attempt is made to remove hash table entries
//         related to the name; only the default is removed from the user's
//         defaults database.
//      2. When a MiscSplitView was freed, its entry in the hash table was
//         not removed.  Now it is, in the newly added free method.  This
//         bug caused crashes when the hash table sent the freed objects
//         messages during rehashing, which occurs when the table is enlarged.
//    Version 1.30:  October 3, 1996
//      Fixed a bug that caused the subviews to be incorrectly sized if there
//      were more than two of them.  Thanks to Champciaux Laurent for finding
//      this bug.
//    Version 1.40:  February 27, 1997
//      Streamlined the model event loop in mouseDown:.  It also now
//      explicitly enables event coalescing, and it now uses a timeout so
//      that if something weird happens and the loop doesn't notice that the
//      dragging has stopped, the timeout will cause the loop to exit.
//    Version 1.41:  March 31, 1997
//      Fixed a bug which caused the left (or top) subview to be resized to a
//      width (or height) of zero if the divider was clicked on without being
//      moved.
//    Version 1.42:  April 4, 1997
//      Increased the defaultName string sizes from 64 to 128.
//

#import <misckit/MiscSplitView.h>
#import <misckit/MiscAppDefaults.h>

#include "Dimple_tiff.m"   /* contains Dimple.tiff */

// Retrace is 1/67 of sec * 10^6 micro secs per second.
#define RETRACE	(1000000 / 67)  // vertical retrace period in microsecs
#define MISC_SPLIT_VIEW_VERSION 1
#define DIVIDER_SIZE  8.0
#define INITIAL_LAST -1.0
#define DRAG_EVENT_MASK (NX_MOUSEUPMASK | NX_MOUSEDRAGGEDMASK)

#define nameToDefault(name,defaultName) \
   sprintf ((defaultName), "MiscSplitView Dividers %s", (name))


static NXImage *dimple;
static NXSize dimpleSize;
static HashTable *autosaveNames;
static BOOL requireUniqueNames = NO;



static void freeClassData (void)
{
   /*
     Don't free the dimple.  That'll generate an IOT. Something else frees it.
   */
   [autosaveNames free];
}



@implementation MiscSplitView



+ initialize
{
   if (![super initialize]) return nil;

   if (self != [MiscSplitView class]) return self;

   /* Set the class's version. */
   [self setVersion: MISC_SPLIT_VIEW_VERSION];

   /* Initialize random number generator for vertical retrace sleeping. */
   srandom (time (0));

   /* Get dimple image.  Modified by Don Yacktman--put tiff into a .m file */
   dimple = Dimple_tiff_fn();
   /* dimple = [NXImage findImageNamed: "Dimple"]; */
   [dimple getSize: &dimpleSize];

   /* Create autosave name hash table. */
   autosaveNames = [[HashTable allocFromZone: [self zone]]
		    initKeyDesc: "@" valueDesc: "%"];
   atexit (freeClassData);
   return self;
}



+ (BOOL) autosaveNameExists: (NXAtom) name
{
   NXHashState state = [autosaveNames initState];
   const void *key;
   void *value;

   while ([autosaveNames nextState: &state key: &key value: &value]) {
      if ((NXAtom) value == name) return YES;
   }

   return NO;
}



- initFrame: (const NXRect *) frameRect
{
   if (![super initFrame: frameRect]) return nil;
   [self setFlipped: YES];
   [self setAutoresizeSubviews: YES];
   return self;
}



- free
{
   [autosaveNames removeKey: (const void *) self];
   return [super free];
}



- delegate
{
   return delegate;
}



- setDelegate: anObject
{
   delegate = anObject;
   return self;
}



- (BOOL) isHorizontal
{
   return msvFlags.horizontal;
}



- setHorizontal: (BOOL) makeHorizontal
{
   if (msvFlags.horizontal != makeHorizontal) {
      msvFlags.horizontal = makeHorizontal;
      msvFlags.needsRetiling = YES;
   }

   return self;
}



- setDividersFromString: (const char *) data
{
   NXRect viewFrame;
   NXCoord newValue;
   View *view;
   int i, version;

   if (!data) return nil;

   /*
     Get our class version from the default in case
     we want to change its format later.
   */
   data += strspn (data, " \t");
   version = atoi (data);
   data += strspn (data, "0123456789");

   /*
     There's no need to set the origin values,
     since they will be set by resizeSubviews:.
   */
   for (i = 0; view = [subviews objectAt: i]; i++) {
      [view getFrame: &viewFrame];
      data += strspn (data, " \t");
      newValue = atoi (data);
      if (newValue < 1) break;
      data += strspn (data, "0123456789");
      if (msvFlags.horizontal) {
	 viewFrame.size.width  = newValue;
      } else {
	 viewFrame.size.height = newValue;
      }
      [view setFrame: &viewFrame];
   }

   [self resizeSubviews: &bounds.size];
   return self;
}



- saveDividersToString: (char *) string
{
   NXRect viewFrame;
   NXCoord value;
   View *view;
   int i;

   if (!string) return nil;

   sprintf (string, "%d ", [MiscSplitView version]);

   for (i = 0; view = [subviews objectAt: i]; i++) {
      [view getFrame: &viewFrame];
      string = &string[strlen (string)];
      if (msvFlags.horizontal) {
	 value = viewFrame.size.width;
      } else {
	 value = viewFrame.size.height;
      }
      sprintf (string, "%.0f ", value);
   }

   return self;
}



- setDividersUsingName: (const char *) name
{
   const char *divString;
   char defaultName[128];

   if (!name) return nil;

   nameToDefault (name, defaultName);
   divString = [NXApp defaultValue: defaultName];
   if (!divString) return nil;

   [self setDividersFromString: divString];
   return self;
}



- saveDividersUsingName: (const char *) name
{
   char divString[[subviews count] * 8];
   char defaultName[128];

   if (!name) return nil;

   if ([self saveDividersToString: divString]) {
      nameToDefault (name, defaultName);
      [NXApp setDefault: defaultName to: divString];
      return self;
   } else {
      return nil;
   }
}



+ removeDividersUsingName: (const char *) name
{
   char defaultName[128];

   nameToDefault (name, defaultName);
   NXRemoveDefault ([NXApp appName], defaultName);
   return self;
}



- (const char *) dividersAutosaveName
{
   return (const char *) [autosaveNames valueForKey: (const void *) self];
}



- setDividersAutosaveName: (const char *) name
{
   NXAtom saveName;

   if (!name) {
      [autosaveNames removeKey: (const void *) self];
      return self;
   } else {
      saveName = NXUniqueString (name);
      if (requireUniqueNames && [MiscSplitView autosaveNameExists: saveName]) {
	 return nil;
      } else {
	 [autosaveNames insertKey: (const void *) self
	  value: (void *) saveName];
	 [self setDividersUsingName: name];
	 return self;
      }
   }
}



+ requireUniqueAutosaveNames: (BOOL) require
{
   requireUniqueNames = require;
   return self;
}



- (NXCoord) dividerSize
{
   return DIVIDER_SIZE;
}



- (NXCoord) dividerHeight
{
   return [self dividerSize];
}



- (NXCoord) dividerWidth
{
   return [self dividerSize];
}



- drawSelf: (const NXRect *) rects : (int) rectCount
{
   NXCoord dividerSize;
   NXRect dividerFrame;
   View *view;
   int i, r;

   if (msvFlags.needsRetiling) [self resizeSubviews: &bounds.size];

   if (msvFlags.horizontal) {
      dividerSize = [self dividerWidth];
   } else {
      dividerSize = [self dividerHeight];
   }

   for (r = (rectCount == 1) ? 0 : 1; r < rectCount; r++) {
      for (i = 1; view = [subviews objectAt: i]; i++) {
	 [view getFrame: &dividerFrame];
	 if (msvFlags.horizontal) {
	    dividerFrame.origin.x   -= dividerSize;
	    dividerFrame.size.width  = dividerSize;
	 } else {
	    dividerFrame.origin.y   -= dividerSize;
	    dividerFrame.size.height = dividerSize;
	 }

	 if (NXIntersectsRect (&dividerFrame, &rects[r])) {
	    [self drawDivider: &dividerFrame];
	 }
      }
   }

   return self;
}



- drawDivider: (const NXRect *) aRect;
{
   NXPoint dimplePoint;

   PSsetgray (NX_LTGRAY);
   NXRectFill (aRect);

   /*
     The y coordinate is calculated differently
     because the view's coordinate system is flipped.
   */
   dimplePoint.x = floor (aRect->origin.x + (aRect->size.width  / 2.0) -
			  (dimpleSize.width  / 2.0));
   dimplePoint.y = floor (aRect->origin.y + (aRect->size.height / 2.0) +
			  (dimpleSize.height / 2.0));

   [dimple composite: NX_SOVER toPoint: &dimplePoint];
   return self;
}



- constrainDivider: (int) dividerIndex
  leftFrame:  (const NXRect *) leftFrame
  rightFrame: (const NXRect *) rightFrame
  min: (NXCoord *) min max: (NXCoord *) max
{
   NXCoord halfSize = [self dividerSize] / 2;
   NXCoord newMin, newMax;

   if (!(leftFrame && rightFrame)) return nil;

   /* Calculate how much the views can be resized. */
   if (msvFlags.horizontal) {
      *min = leftFrame->origin.x  + halfSize;
      *max = rightFrame->origin.x + rightFrame->size.width  - halfSize;
   } else {
      *min = leftFrame->origin.y  + halfSize;
      *max = rightFrame->origin.y + rightFrame->size.height - halfSize;
   }

   /* Ask the delegate to constrain the values. */
   if ((msvFlags.horizontal &&
	[delegate respondsTo:
	 @selector (splitView:getMinX:maxX:ofSubviewAt:)]) ||
       (!msvFlags.horizontal &&
	[delegate respondsTo:
	 @selector (splitView:getMinY:maxY:ofSubviewAt:)])) {

      newMin = *min;
      newMax = *max;

      if (msvFlags.horizontal) {
	 [delegate splitView: self getMinX: &newMin
	  maxX: &newMax ofSubviewAt: dividerIndex];
      } else {
	 [delegate splitView: self getMinY: &newMin
	  maxY: &newMax ofSubviewAt: dividerIndex];
      }

      if ((newMin > *min) && (newMin <= *max)) *min = newMin;
      if ((newMax < *max) && (newMax >= *min)) *max = newMax;
      if (*max < *min) *max = *min;
   }

   return self;
}



- constrainFramesLeft: (NXRect *) leftFrame andRight: (NXRect *) rightFrame
  besideDivider: (int) dividerIndex
{
   NXCoord dividerSize = [self dividerSize];
   NXCoord halfSize = dividerSize / 2;
   NXCoord min, max, minEdge, maxEdge, newEdge;

   if (![self constrainDivider: dividerIndex leftFrame: leftFrame
	 rightFrame: rightFrame min: &min max: &max]) {
      return nil;
   }

   minEdge = min + halfSize;
   maxEdge = max + halfSize;
   newEdge = 0.0;

   if (msvFlags.horizontal) {
      if (minEdge > rightFrame->origin.x) {
	 newEdge = minEdge;
      } else if (maxEdge < rightFrame->origin.x) {
	 newEdge = maxEdge;
      }

      if (newEdge > 0.0) {
	 leftFrame->size.width =
	    newEdge - leftFrame->origin.x - dividerSize;
	 rightFrame->size.width = rightFrame->origin.x +
	    rightFrame->size.width - newEdge;
	 rightFrame->origin.x = newEdge;
      }
   } else {
      if (minEdge > rightFrame->origin.y) {
	 newEdge = minEdge;
      } else if (maxEdge < rightFrame->origin.y) {
	 newEdge = maxEdge;
      }

      if (newEdge > 0.0) {
	 leftFrame->size.height =
	    newEdge - leftFrame->origin.y - dividerSize;
	 rightFrame->size.height = rightFrame->origin.y +
	    rightFrame->size.height - newEdge;
	 rightFrame->origin.y = newEdge;
      }
   }

   return self;
}



- adjustSubviews
{
   NXCoord dividerSize, oldSize, p;
   NXRect viewFrame, leftFrame, rightFrame;
   View *view, *leftView, *rightView;
   int lastView, i;

   if (msvFlags.horizontal) {
      dividerSize = [self dividerWidth];
   } else {
      dividerSize = [self dividerHeight];
   }

   oldSize = -dividerSize;
   for (i = 0; view = [subviews objectAt: i]; i++) {
      [view getFrame: &viewFrame];
      if (msvFlags.horizontal) {
	 oldSize += viewFrame.size.width  + dividerSize;
      } else {
	 oldSize += viewFrame.size.height + dividerSize;
      }
   }

   lastView = i - 1;
   leftView = nil;
   p = msvFlags.horizontal ? bounds.origin.x : bounds.origin.y;

   for (i = 0; rightView = [subviews objectAt: i]; i++) {
      if (msvFlags.horizontal) {
	 rightFrame.origin.x    = p;
	 rightFrame.origin.y    = bounds.origin.y;
	 rightFrame.size.height = bounds.size.height;
      } else {
	 rightFrame.origin.x    = bounds.origin.x;
	 rightFrame.origin.y    = p;
	 rightFrame.size.width  = bounds.size.width;
      }

      if (i < lastView) {
	 [rightView getFrame: &viewFrame];
	 if (msvFlags.horizontal) {
	    rightFrame.size.width =
	       rint (bounds.size.width  * (viewFrame.size.width  / oldSize));
	 } else {
	    rightFrame.size.height =
	       rint (bounds.size.height * (viewFrame.size.height / oldSize));
	 }
      } else {
	 if (msvFlags.horizontal) {
	    rightFrame.size.width  = bounds.size.width  - p;
	 } else {
	    rightFrame.size.height = bounds.size.height - p;
	 }
      }

      if (leftView) {
	 [self constrainFramesLeft: &leftFrame andRight: &rightFrame
	  besideDivider: i - 1];
	 [leftView setFrame: &leftFrame];
	 [window invalidateCursorRectsForView: leftView];
      }

      if (msvFlags.horizontal) {
	 p += rightFrame.size.width  + dividerSize;
      } else {
	 p += rightFrame.size.height + dividerSize;
      }

      leftView  = rightView;
      leftFrame = rightFrame;
   }

   /* Set the final view if there was more than one. */
   if (lastView > 0) {
      [leftView setFrame: &leftFrame];
      [window invalidateCursorRectsForView: leftView];
   }

   [self saveDividersUsingName: [self dividersAutosaveName]];
   msvFlags.needsRetiling = NO;
   return self;
}



/* called when the view gets resized */

- resizeSubviews: (const NXSize *) oldSize
{
   if ([delegate respondsTo: @selector (splitView:resizeSubviews:)]) {
      [delegate splitView: self resizeSubviews: oldSize];
      [self saveDividersUsingName: [self dividersAutosaveName]];
      msvFlags.needsRetiling = NO;
   } else {
      [self adjustSubviews];
   }

   if ([delegate respondsTo: @selector (splitViewDidResizeSubviews:)]) {
      [delegate splitViewDidResizeSubviews: self];
   }

   return self;
}



- setAutoresizeSubviews: (BOOL) flag
{
   /* Don't allow anyone to disable autoresize for us. */
   return [super setAutoresizeSubviews: YES];
}



- addSubview: aView
{
   msvFlags.needsRetiling = YES;
   return [super addSubview: aView];
}



- addSubview: aView : (int) place relativeTo: otherView
{
   msvFlags.needsRetiling = YES;
   return [super addSubview: aView : place relativeTo: otherView];
}



- replaceSubview: oldView with: newView
{
   msvFlags.needsRetiling = YES;
   return [super replaceSubview: oldView with: newView];
}



- (BOOL) acceptsFirstMouse
{
   return YES;
}



/* controls the movement of the divider as the user slides it with the mouse */

- mouseDown: (NXEvent *) theEvent
{
   NXPoint location = theEvent->location;
   NXCoord dividerSize, halfSize;
   NXCoord min, max, last, new, p;
   NXRect leftFrame, rightFrame;
   View *leftView, *rightView;
   int i, oldMask;
   BOOL wasCoalescing;

   if (msvFlags.horizontal) {
      dividerSize = [self dividerWidth];
   } else {
      dividerSize = [self dividerHeight];
   }

   halfSize = dividerSize / 2;

   /* Figure out which divider is being moved. */
   [self convertPoint: &location fromView: nil];
   for (i = 1; rightView = [subviews objectAt: i]; i++) {
      [rightView getFrame: &rightFrame];
      if ((msvFlags.horizontal && (location.x < rightFrame.origin.x) &&
	   (location.x >= rightFrame.origin.x - dividerSize)) ||
	  (!msvFlags.horizontal && (location.y < rightFrame.origin.y) &&
	   (location.y >= rightFrame.origin.y - dividerSize))) {
	 i--;
	 break;
      }
   }

   if (!rightView) return [super mouseDown: theEvent];

   /* Set the limits of the new divider position. */
   leftView = [subviews objectAt: i];
   [leftView getFrame: &leftFrame];
   [self constrainDivider: i leftFrame: &leftFrame
    rightFrame: &rightFrame min: &min max: &max];

   /* Prepare for drawing. */
   [self lockFocus];
   PSsetinstance (YES);
   PSsetgray (NX_DKGRAY);
   PSsetalpha (0.5);
   last = INITIAL_LAST;
   oldMask = [window addToEventMask: DRAG_EVENT_MASK];
   wasCoalescing = DPSSetTracking (YES);

   /*
     While the user is dragging, composite a slider outline in the new
     postion, watching for boundary conditions.  Exit the loop on the
     first non-mouse-drag event.
   */
   while ((theEvent = [NXApp getNextEvent: DRAG_EVENT_MASK
		       waitFor: 10 threshold: NX_MODALRESPTHRESHOLD]) &&
	  (theEvent->type == NX_MOUSEDRAGGED)) {
      location = theEvent->location;
      [self convertPoint: &location fromView: nil];
      p = msvFlags.horizontal ? location.x : location.y;

      if (p < min) p = min;
      if (p > max) p = max;
      if (p == last) continue;
      last = p;
      p -= halfSize;

      PSnewinstance();
      if (msvFlags.horizontal) {
	 PScompositerect (p, bounds.origin.y,
			  dividerSize, bounds.size.height, NX_SOVER);
      } else {
	 PScompositerect (bounds.origin.x, p,
			  bounds.size.width, dividerSize, NX_SOVER);
      }

      /*
	To reduce flicker, wait some random
	amount of time within the retrace period.
      */
      usleep (random() % RETRACE);
   }

   /* Finish drawing. */
   DPSSetTracking (wasCoalescing);
   PSsetinstance (NO);
   [self unlockFocus];
   [window setEventMask: oldMask];

   /* If the divider was not moved, don't change anything. */
   if (last == INITIAL_LAST) return self;

   /* Resize the subviews. */
   [window disableFlushWindow];

   /* First the left. */
   [leftView getFrame: &leftFrame];
   if (msvFlags.horizontal) {
      leftFrame.size.width  = last - halfSize - leftFrame.origin.x;
   } else {
      leftFrame.size.height = last - halfSize - leftFrame.origin.y;
   }
   [leftView setFrame: &leftFrame];

   /* Then the right. */
   [rightView getFrame: &rightFrame];
   new = last + halfSize;
   if (msvFlags.horizontal) {
      rightFrame.size.width =
	 rightFrame.origin.x + rightFrame.size.width - new;
      rightFrame.origin.x = new;
   } else {
      rightFrame.size.height =
	 rightFrame.origin.y + rightFrame.size.height - new;
      rightFrame.origin.y = new;
   }
   [rightView setFrame: &rightFrame];

   /* Display the newly resized views. */
   [self display];
   [window reenableFlushWindow];
   [window flushWindow];

   /* Update the default. */
   [self saveDividersUsingName: [self dividersAutosaveName]];

   /* Inform the delegate of the change. */
   if ([delegate respondsTo: @selector (splitViewDidResizeSubviews:)]) {
      [delegate splitViewDidResizeSubviews: self];
   }

   return self;
}



- write: (NXTypedStream *) stream
{
   unsigned char horizontal = msvFlags.horizontal;

   if (![super write: stream]) return nil;
   NXWriteType (stream, "c", &horizontal);
   NXWriteObjectReference (stream, delegate);
   return self;
}



- read: (NXTypedStream *) stream
{
   unsigned char horizontal;
   int version;

   if (![super read: stream]) return nil;

   version = NXTypedStreamClassVersion (stream, [[MiscSplitView class] name]);

   NXReadType (stream, "c", &horizontal);
   msvFlags.horizontal = horizontal;

   if (version > 0) delegate = NXReadObject (stream);
   return self;
}



@end

