// Copyright 1997-2000 Omni Development, Inc.  All rights reserved.
//
// This software may only be used and reproduced according to the
// terms in the file OmniSourceLicense.html, which should be
// distributed with this project and can also be found at
// http://www.omnigroup.com/DeveloperResources/OmniSourceLicense.html.

#import "OHHTMLView.h"

#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>
#import <OmniBase/OmniBase.h>
#import <OmniFoundation/OmniFoundation.h>
#import <OmniAppKit/OmniAppKit.h>
#import <OWF/OWF.h>
#import <OIF/OIF.h>

#import "OWAddress-OHActions.h"
#import "OWAddress-OHDragging.h"
#import "OWAddress-OHImage.h"
#import "OWAddress-OHPasteboard.h"
#import "OHBreak.h"
#import "OHBasicCell.h"
#import "OHDownloader.h"
#import "OHTextBuilder.h"
#import "OHHTMLAnchor.h"
#import "OHHTMLDocument.h"
#import "OHHTMLPageView.h"
#import "OHHTMLView-TrackingRect.h"
#import "OHImageCell.h"
#import "OHInlineImageCell.h"
#import "OHLay.h"
#import "OHLine.h"
#import "OHParagraph.h"
#import "OHWord.h"
#import "OWScriptEventHandlerHolder.h"
#import "NSToolTipManager.h"

RCS_ID("$Header: /Network/Source/CVS/OmniGroup/Frameworks/OmniHTML/View.subproj/OHHTMLView.m,v 1.86 2000/02/03 01:48:20 kc Exp $")

@interface OHHTMLView (Private)
// Layout
- (void)scanParagraphsFromParagraphIndex:(unsigned int)paragraphIndex;
- (void)layoutLines;
- (NSRect)newLayoutBoundsAfterTakingMarginalCellsFromLayoutBounds:(NSRect)layoutBounds characterIndex:(unsigned int)characterIndex;
- (float)newYAfterTakingClearBreaksFromY:(float)y characterIndex:(unsigned int)characterIndex firstUnlaidClearBreakIndex:(unsigned int *)firstUnlaidClearBreakIndex;
- (float)newYAfterTakingClearBreakFromY:(float)y clearBreakType:(OHBreakType)breakType;
//
- (unsigned int)characterIndexForGlyphIndex:(unsigned int)glyphIndex fractionOfDistanceThroughGlyph:(float)fraction granularity:(NSSelectionGranularity)granularity;
- (NSRange)maximalCharacterRangeForRect:(NSRect)rect;
- (unsigned int)lineIndexAtY:(float)y wantsLineAboveIfNoMatch:(BOOL)wantsLineAboveIfNoMatch lineComparesToY:(NSComparisonResult *)lineComparesToY;
- (void)updateFontManager;

- (void)mainThreadAppendAttributedString:(NSAttributedString *)attributedString;

// Drawing
- (void)drawMarginalCellsInRect:(NSRect)drawRect;
- (void)drawBoxAroundSelectedAnchor;

// Mouse handling
- (void)mouseDown:(NSEvent *)event onAnchor:(OHHTMLAnchor *)clickAnchor;
- (OHBasicCell *)marginalCellAtPoint:(NSPoint)aPoint;
- (OHHTMLLink *)linkAtPoint:(NSPoint)point;
- (OHHTMLAnchor *)linkAtCharacterIndex:(unsigned int)characterIndex;
- (OHBasicCell *)cellAtCharacterIndex:(unsigned int)characterIndex;

// Tracking rects
- (NSArray *)resetCursorRects:(BOOL)resetCursors andCalculateTrackingRects:(BOOL)resetTracking;
- (void)setTrackingRects:(NSArray *)newTrackingArray;
- (void)resetAnchorTrackingRects;
- (void)trackingRectRelevantNotification:(NSNotification *)note;

// Updating the layout
- (void)relayoutIfNeeded;

// Calculating widths
- (OHWidthRange)widthRangeForContents;

- (unsigned int)layIndexForCharacterIndex:(unsigned int)characterIndex;
- (NSRange)layRangeContainingCharacterRange:(NSRange)characterRange;

// Background image handling
- (void)setBackgroundOmniImage:(OIImage *)anOmniImage;

// Find helpers
- (void)findSelectionInSubviews;
- (BOOL)checkIfFirstResponder:(NSView *)firstResponder isInSearchableContentViewAtCharacterIndex:(unsigned int)attachmentIndex isMarginal:(BOOL)isMarginal;
- (NSView <OASearchableContent> *)searchableContentViewAtCharacterIndex:(unsigned int)attachmentIndex isMarginal:(BOOL)isMarginal;
- (BOOL)findString:(NSString *)textPattern ignoreCase:(BOOL)ignoreCase backwards:(BOOL)backwards ignoreSelection:(BOOL)ignoreSelection inAttachmentAtIndex:(unsigned int)attachmentIndex isMarginal:(BOOL)isMarginal;
- (NSRange)findNextAttachmentAfterRange:(NSRange)previousAttachmentRange backwards:(BOOL)backwards isMarginal:(BOOL *)isMarginal;
- (unsigned int)characterIndexOfMarginalCellAroundCharacterIndex:(unsigned int)characterIndex searchDirection:(NSComparisonResult)searchDirection;
- (unsigned int)indexOfMarginalCellAroundCharacterIndex:(unsigned int)characterIndex searchDirection:(NSComparisonResult)searchDirection;

// Shutting down
- (void)nullifyHTMLOwnerBackpointersInCellsInHTMLStorage:(NSTextStorage *)htmlStorage;

@end

#define DRAG_SLOP 4

@implementation OHHTMLView

static NSCursor *linkCursor;
static NSMutableArray *freeLayoutManagers;
static NSLock *freeLayoutManagersLock;
static CSBitmap nonWhitespaceCSBitmap;
static CSBitmap whitespaceOrAttachmentCSBitmap;
static NSMenu *linkMenu, *imageMenu;
static NSString *attachmentString;
static CSBitmap attachmentCSBitmap;

static float averageNumberOfLettersPerWordIncludingSpaces = 3.2;
static float averageNumberOfWordsPerLineIncludingSpaces = 12.5;
static float averageNumberOfLettersPerParagraphIncludingSpaces = 62;


+ (void)initialize;
{
    static BOOL initialized = NO;
    NSCharacterSet *nonWhitespaceCharacterSet;
    NSMutableCharacterSet *whitespaceOrAttachmentCharacterSet;
    NSBundle *bundle;
    NSImage *linkCursorImage;
    unichar attachmentCharacterBuffer[] = {NSAttachmentCharacter};
    NSCharacterSet *attachmentCharacterSet;

    [super initialize];

    if (initialized)
        return;
    initialized = YES;

    bundle = [NSBundle bundleForClass:self];
    linkCursorImage = [[NSImage alloc] initWithContentsOfFile:[bundle pathForResource:@"OHLinkCursor" ofType:@"tiff"]];
    linkCursor = [[NSCursor alloc] initWithImage:linkCursorImage hotSpot:NSMakePoint(6.0, 1.0)];
    [linkCursor setOnMouseEntered:YES];

    freeLayoutManagers = [[NSMutableArray alloc] init];
    freeLayoutManagersLock = [[NSLock alloc] init];

    nonWhitespaceCharacterSet = [[NSCharacterSet whitespaceCharacterSet] invertedSet];
    nonWhitespaceCSBitmap = bitmapForCharacterSetDoRetain(nonWhitespaceCharacterSet, YES);

    whitespaceOrAttachmentCharacterSet = [[NSCharacterSet whitespaceAndNewlineCharacterSet] mutableCopy];
    [whitespaceOrAttachmentCharacterSet addCharactersInRange:NSMakeRange(NSAttachmentCharacter, 1)];
    whitespaceOrAttachmentCSBitmap = bitmapForCharacterSetDoRetain(whitespaceOrAttachmentCharacterSet, YES);
    [whitespaceOrAttachmentCharacterSet release];

    linkMenu = [[NSMenu alloc] initWithTitle:@"Link Menu"];
    [linkMenu addItemWithTitle:@"Show linked document in new window" action:@selector(newWindowWithSelectedLink:) keyEquivalent:@""];
    [linkMenu addItemWithTitle:[NSString stringWithFormat:@"Save linked document to%@", [NSString horizontalEllipsisString]] action:@selector(saveSelectedLink:) keyEquivalent:@""];
    [linkMenu addItemWithTitle:@"Copy link address" action:@selector(copy:) keyEquivalent:@""];
    [linkMenu addItemWithTitle:@"Bookmark link address" action:@selector(addSelectedLinkToBookmarks:) keyEquivalent:@""];

    imageMenu = [[NSMenu alloc] initWithTitle:@"Image Menu"];
    [imageMenu addItemWithTitle:@"Show image in this window" action:@selector(foo:) keyEquivalent:@""];
    [imageMenu addItemWithTitle:[NSString stringWithFormat:@"Save image to%@", [NSString horizontalEllipsisString]] action:@selector(foo:) keyEquivalent:@""];
    [imageMenu addItemWithTitle:@"Copy image" action:@selector(foo:) keyEquivalent:@""];
    [imageMenu addItemWithTitle:@"Copy image address" action:@selector(foo:) keyEquivalent:@""];
    [imageMenu addItemWithTitle:@"Load image" action:@selector(foo:) keyEquivalent:@""];

    [NSApp registerServicesMenuSendTypes:[NSArray arrayWithObjects: NSStringPboardType, NSRTFPboardType, NSRTFDPboardType, nil] returnTypes:nil];

    attachmentString = [[NSString alloc] initWithCharacters:attachmentCharacterBuffer length:1];
    attachmentCharacterSet = [NSCharacterSet characterSetWithCharactersInString:attachmentString];
    attachmentCSBitmap = bitmapForCharacterSetDoRetain(attachmentCharacterSet, YES);
}

+ (NSCursor *)linkCursor;
{
    return linkCursor;
}

// Init and dealloc

- (id)initWithHTMLOwner:(OHHTMLOwner *)anHTMLOwner;
{
    OFZone *myZone;
    
    if (![super initWithFrame:NSMakeRect(0.0, 0.0, 1e6, 0.0)])
        return nil;

    nonretainedHTMLOwner = anHTMLOwner;
#ifdef RHAPSODY
    flags.shouldAntialiasText = [[OFUserDefaults sharedUserDefaults] boolForKey:@"OHAntialiasText"];
#else
    flags.shouldAntialiasText = NO;
#endif
    flags.currentlyAntialiased = NO;
    
    requiredWidth = 0.0;
    myZone = [OFZone zoneForObject:self];

    [self setAutoresizingMask:NSViewWidthSizable];
    [self setAutoresizesSubviews:YES];

    textContainer = [[NSTextContainer allocWithZone:[myZone nsZone]] initWithContainerSize:NSMakeSize(1e100, 1e100)];
    [textContainer setLineFragmentPadding:1.0];  // Causes bugs in OS4.2 when set to 0.

    layoutManager = [[NSLayoutManager allocWithZone:[myZone nsZone]] init];

    if (flags.shouldAntialiasText)
        [layoutManager setUsesScreenFonts:NO];

    [layoutManager addTextContainer:textContainer];

    textStorage = [[NSTextStorage allocWithZone:[myZone nsZone]] init];
    [textStorage addLayoutManager:layoutManager];

    paragraphs = [[OFStaticArray allocWithZone:[myZone nsZone]] initWithClass:[OHParagraph class] capacity:0 extendBy:0];
    lines = [[OFStaticArray allocWithZone:[myZone nsZone]] initWithClass:[OHLine class] capacity:0 extendBy:0];
    lays = [[OFStaticArray allocWithZone:[myZone nsZone]] initWithClass:[OHLay class] capacity:0 extendBy:0];
    words = [[OFStaticArray allocWithZone:[myZone nsZone]] initWithClass:[OHWord class] capacity:0 extendBy:0];
    marginalCells = nil;
    clearBreakObjects = nil;
    trackingRects = [[NSMutableSet allocWithZone:[myZone nsZone]] init];

    selectedRange = NSMakeRange(NSNotFound, 0);

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(trackingRectRelevantNotification:) name:NSViewFrameDidChangeNotification object:self];

    selectionHoldingCellIndexHint = NSNotFound;
    
    return self;
}

- (id)initWithFrame:(NSRect)frameRect;
{
    OBRequestConcreteImplementation(self, _cmd);
    return nil; // Not reached
}

- (void)dealloc;
{
    // Background image cleanup
    [OWPipeline cancelTarget:self];

    [backgroundImageAddress release];
    [backgroundOmniImage removeObserver:self];
    [backgroundOmniImage release];
    [backgroundImage release];

    // Text
    [textContainer release];
    [layoutManager release];
    [textStorage release];

    [paragraphs release];
    [words release];
    [lines release];
    [lays release];

    [[NSNotificationCenter defaultCenter] removeObserver:self];
    [self setTrackingRects:nil];
    [trackingRects release];
    trackingRects = nil;

    [marginalCells release];
    [clearBreakObjects release];

    [backgroundColor release];
    
    [super dealloc];
}


// API

- (void)setHTMLOwner:(OHHTMLOwner *)anHTMLOwner;
{
    OBASSERT(nonretainedHTMLOwner == nil && anHTMLOwner != nil); // Use nullifyHTMLOwner to set anHTMLOwner to nil
}

- (void)nullifyHTMLOwner;
{
    NSTextStorage *htmlStorage;

    htmlStorage = [nonretainedHTMLOwner htmlStorage];
    OBASSERT(nonretainedHTMLOwner != nil);
    nonretainedHTMLOwner = nil;
    [self nullifyHTMLOwnerBackpointersInCellsInHTMLStorage:htmlStorage];
}

- (OHHTMLOwner *)htmlOwner;
{
    return nonretainedHTMLOwner;
}

- (unsigned int)glyphIndexForPoint:(NSPoint)point fractionOfDistanceThroughGlyph:(float *)fraction isReallyOnGlyph:(BOOL *)isReallyOnGlyph;
{
    unsigned int lineIndex;
    OHLine *line;
    NSComparisonResult lineComparesToY;

    if (isReallyOnGlyph)
        *isReallyOnGlyph = NO;
    
    lineIndex = [self lineIndexAtY:point.y wantsLineAboveIfNoMatch:YES lineComparesToY:&lineComparesToY];

    if (lineIndex == NSNotFound)
        return NSNotFound;
    
    line = [lines objectAtIndex:lineIndex];

    if (lineComparesToY == NSOrderedSame)
        return [line glyphIndexForPoint:point fractionOfDistanceThroughGlyph:fraction isReallyOnGlyph:isReallyOnGlyph inHTMLView:self];

   if (lineComparesToY == NSOrderedDescending) {
        // Before first line
        OBASSERT(lineIndex == 0);
        if (fraction)
            *fraction = 0;
        return 0;
    }

    // Click was after the line, either after last line or in-between two lines that are forces apart by a break clear=x
    // (lineComparesToY == NSOrderedAscending)
    if (fraction)
        *fraction = 1.0;
    return NSMaxRange([self glyphRangeForWordRange:line->wordRange])-1;
}

- (const NSRect *)rectArrayForCharacterRange:(NSRange)characterRange rectCount:(unsigned int *)returnRectCountPtr;
{
    NSMutableData *rectArrayData;
    unsigned int dataLength;
    NSRect *rectArray;
    unsigned int rectCount;
    NSRange layRange;
    unsigned int layIndex, maxLayIndex;
    OHLine *currentLine;
    unsigned int nextLineIndex;

    if (characterRange.length == 0) {
        *returnRectCountPtr = 0;
        return NULL;
    }
    layRange = [self layRangeContainingCharacterRange:characterRange];
    if (layRange.length == 0) {
        *returnRectCountPtr = 0;
        return NULL;
    }
    dataLength = layRange.length * sizeof(NSRect);
    rectArrayData = [OFFastMutableData newFastMutableDataWithLength:dataLength];
    rectArray = [rectArrayData mutableBytes];
    OBASSERT(rectArray != NULL);
    rectCount = 0;
    maxLayIndex = NSMaxRange(layRange);
    nextLineIndex = 0;
    currentLine = [lines objectAtIndex:nextLineIndex++];
    for (layIndex = layRange.location; layIndex < maxLayIndex; layIndex++) {
        OHLay *lay;

        lay = [lays objectAtIndex:layIndex];
        while (layIndex >= NSMaxRange(currentLine->layRange))
            currentLine = [lines objectAtIndex:nextLineIndex++];
        if (layIndex == layRange.location) {
            if (layIndex == maxLayIndex - 1) {
                // Starts and ends in the same lay, rect for characterRange
                rectArray[rectCount++] = [currentLine rectForCharacterRange:characterRange inContainedLay:lay inHTMLView:self];
            } else {
                // First lay of many, rect from first character
                rectArray[rectCount++] = [currentLine rectForContainedLay:lay fromCharacterAtIndex:characterRange.location inHTMLView:self];
            }
        } else if (layIndex == maxLayIndex - 1) {
            // Last lay of many, rect to last character
            rectArray[rectCount++] = [currentLine rectForContainedLay:lay toCharacterAtIndex:NSMaxRange(characterRange) inHTMLView:self];
        } else {
            // Middle lay, entire lay rect
            rectArray[rectCount++] = [currentLine rectForContainedLay:lay];
        }
    }
    
    OBASSERT(rectCount <= layRange.length);
    *returnRectCountPtr = rectCount;
    [rectArrayData autorelease];
    return rectCount > 0 ? rectArray : NULL;
}

- (const NSRect *)rectArrayForLayRange:(NSRange)layRange rectCount:(unsigned int *)returnRectCountPtr;
{
    NSMutableData *rectArrayData;
    unsigned int dataLength;
    NSRect *rectArray;
    unsigned int rectCount;
    unsigned int layIndex, maxLayIndex;
    unsigned int nextLineIndex;
    OHLine *currentLine;

    if (layRange.length == 0) {
        *returnRectCountPtr = 0;
        return NULL;
    }
    dataLength = layRange.length * sizeof(NSRect);
    rectArrayData = [OFFastMutableData newFastMutableDataWithLength:dataLength];
    rectArray = [rectArrayData mutableBytes];
    rectCount = 0;
    maxLayIndex = NSMaxRange(layRange);
    nextLineIndex = 0;
    currentLine = [lines objectAtIndex:nextLineIndex++];
    for (layIndex = layRange.location; layIndex < maxLayIndex; layIndex++) {
        OHLay *lay;

        lay = [lays objectAtIndex:layIndex];
        while (layIndex >= NSMaxRange(currentLine->layRange))
            currentLine = [lines objectAtIndex:nextLineIndex++];
        rectArray[rectCount++] = [currentLine rectForContainedLay:lay];
    }
    
    *returnRectCountPtr = rectCount;
    [rectArrayData autorelease];
    return rectArray;
}

static inline NSRect expandUnionRectToContainRect(NSRect unionRect, NSRect aRect)
{
    if (NSMinX(aRect) < NSMinX(unionRect))
        unionRect.origin.x = NSMinX(aRect);
    if (NSMinY(aRect) < NSMinY(unionRect))
        unionRect.origin.y = NSMinY(aRect);
    if (NSMaxX(aRect) > NSMaxX(unionRect))
        unionRect.size.width = NSMaxX(aRect) - NSMinX(unionRect);
    if (NSMaxY(aRect) > NSMaxY(unionRect))
        unionRect.size.height = NSMaxY(aRect) - NSMinY(unionRect);
    return unionRect;
}

- (NSRect)containingRectForCharacterRange:(NSRange)characterRange;
{
    const NSRect *rectArray;
    unsigned int rectIndex, rectCount;
    NSRect unionRect;

    rectArray = [self rectArrayForCharacterRange:characterRange rectCount:&rectCount];
    if (rectCount == 0)
        return NSMakeRect(-1.0, -1.0, 0.0, 0.0);
    unionRect = rectArray[0];
    for (rectIndex = 1; rectIndex < rectCount; rectIndex++) {
        unionRect = expandUnionRectToContainRect(unionRect, rectArray[rectIndex]);
    }
    return unionRect;
}

- (NSRect)containingRectForLayRange:(NSRange)layRange;
{
    const NSRect *rectArray;
    unsigned int rectIndex, rectCount;
    NSRect unionRect;

    rectArray = [self rectArrayForLayRange:layRange rectCount:&rectCount];
    if (rectCount == 0)
        return NSMakeRect(-1.0, -1.0, 0.0, 0.0);
    unionRect = rectArray[0];
    for (rectIndex = 1; rectIndex < rectCount; rectIndex++) {
        unionRect = NSUnionRect(unionRect, rectArray[rectIndex]);
    }
    return unionRect;
}

- (void)setNeedsDisplayForCharacterRange:(NSRange)characterRange;
{
    [self setNeedsDisplayInRect:[self containingRectForCharacterRange:characterRange]];
}

- (void)scrollCharacterRangeToTop:(NSRange)characterRange;
{
    NSPoint anchorPoint;

    if (characterRange.location == 0) {
        anchorPoint = NSMakePoint(0.0, 0.0);
    } else {
        NSRect containingRect;

        if (characterRange.location > 0 && characterRange.length == 0) {
            characterRange.length = 1;
            if (characterRange.location == [textStorage length])
                characterRange.location -= 1;
        }
        containingRect = [self containingRectForCharacterRange:characterRange];
        if (NSHeight(containingRect) == 0.0)
            return;
        anchorPoint.x = 0.0;
        anchorPoint.y = NSMinY(containingRect);
    }
    [self scrollPoint:anchorPoint];
}

- (void)scrollCharacterRangeToVisible:(NSRange)characterRange;
{
    NSRect anchorRect;

    anchorRect = [self containingRectForCharacterRange:characterRange];
    if (NSHeight(anchorRect) == 0.0)
        return;

    if (characterRange.location == 0) {
        // If we're scrolling to make the first character visible, include the view's origin.
        anchorRect = NSUnionRect(anchorRect, NSMakeRect(0.0, 0.0, 1.0, 1.0));
    }
    [self scrollRectToVisible:anchorRect];
}

- (void)scrollCellToTop:(OHBasicCell *)aCell;
{
    [self scrollPoint:[aCell cellFrame].origin];
}

- (void)scrollCellToVisible:(OHBasicCell *)aCell;
{
    [self scrollRectToVisible:[aCell cellFrame]];
}

    // call from any thread
- (void)redisplayCell:(OHBasicCell *)aCell;
{
    if (![NSThread inMainThread]) {
        [self queueSelectorOnce:@selector(redisplayCell:) withObject:aCell];
        return;
    }
    
    [self setNeedsDisplayInRect:[aCell cellFrame]];
}

    // call from any thread
- (void)relayoutCell:(OHBasicCell *)aCell;
{
    // TODO - Make this only relayout from the cell downwards.
    [self cachedWidthsAreInvalid];
}

- (void)dragAddress:(OWAddress *)address event:(NSEvent *)event;
{
    NSPoint point;
    NSSize size;
    NSImage *icon;

    // Get info for drag
    icon = [address drawImage];

    point = [self convertPoint:[event locationInWindow] fromView:nil];
#warning WJS: I think it is wrong to warp the mouse here in dragAddress:event:.
    // REALLY, we should be using the offset: parameter when we make the final drag call, rather than warping the mouse at this point.  This would eliminate all this code.
    size = [icon size];
    point.x -= rint(size.width / 2.0);
    point.y += rint(size.height / 2.0);

    draggingStartingLocationInWindow = [event locationInWindow];
    [address startDragFromView:self atPoint:point event:event];
}

- (void)dragAnchor:(OHHTMLAnchor *)anAnchor event:(NSEvent *)mouseDown;
{
    OWAddress *address;

    address = [anAnchor address];

    // Hack: if the anchor's address doesn't have a title, we'll guess that it's the text of the anchor
    if (![OWDocumentTitle titleForAddress:address])
          [OWDocumentTitle cacheGuessTitle:[anAnchor label] forAddress:address];

    draggingStartingLocationInWindow = [mouseDown locationInWindow];
    // Drag anchor
    [self dragAddress:address event:mouseDown];
}

- (void)setMarginSize:(NSSize)newMarginSize advisory:(BOOL)isAdvisory;
{
    if (isAdvisory && flags.marginSizeIsAuthoritative)
        return; // We already have a more authoritative source
    if (!isAdvisory)
        flags.marginSizeIsAuthoritative = YES;
    if (NSEqualSizes(newMarginSize, textContainerInset))
        return;
    textContainerInset = newMarginSize;

    [self scheduleRelayout];
}

- (NSSize)marginSize;
{
    // Note: if this changes, change -textContainerInset also
    return textContainerInset;
}

// Actions

- (IBAction)relayout:(id)sender;
{
    [self scanParagraphsFromParagraphIndex:0];
    [self relayout];
}

- (IBAction)showSelectedLink:(id)sender;
{
    OHHTMLPageView *htmlPageView;
    OHHTMLAnchor *selectedAnchor;

    htmlPageView = [self htmlPageView];
    selectedAnchor = [htmlPageView selectedAnchor];

    [htmlPageView followAnchor:selectedAnchor withModifierFlags:0];
}

- (IBAction)saveSelectedLink:(id)sender;
{
    [OHDownloader downloadAddressAndPromptForFilename:[[[self htmlPageView] selectedAnchor] address]];
}

- (IBAction)newWindowWithSelectedLink:(id)sender;
{
    [[[[self htmlPageView] selectedAnchor] address] openDocumentInNewWindow];
}

- (IBAction)addSelectedLinkToBookmarks:(id)sender;
{
    [[[[self htmlPageView] selectedAnchor] address] addToBookmarks];
}

- (IBAction)jumpToSelection:(id)sender;
{
    if (selectedRange.length == 0)
        return;
    
    // Make our selected range visible
    [self scrollRangeToVisible:selectedRange];
}


// Calculating dimensions

- (NSSize)pageSize;
{
#warning -pageSize frequently returns height of 0
    return [(NSClipView *)[self superview] documentVisibleRect].size;
}

- (OHWidthRange)widthRange;
{
    if (!flags.cachedWidthRangeIsValid) {
        cachedWidthRange = [self widthRangeForContents];
        if (cachedWidthRange.minimum < requiredWidth) {
            cachedWidthRange.minimum = requiredWidth;
            if (cachedWidthRange.maximum < cachedWidthRange.minimum)
                cachedWidthRange.maximum = cachedWidthRange.minimum;
        }
        flags.cachedWidthRangeIsValid = YES;
    }
    return OHAddWidthToWidthRange(2.0 * textContainerInset.width, cachedWidthRange);
}

- (void)setFrameSize:(NSSize)newSize relayout:(BOOL)shouldRelayout;
{
    if (NSEqualSizes([self frame].size, newSize))
        return;

    [super setFrameSize:newSize];
    if (shouldRelayout)
        [self relayout];
}

- (void)setWidth:(unsigned int)newWidth;
{
    NSSize newFrameSize;

    OBASSERT(newWidth >= 0);
    OBASSERT(OHWidthIsGreaterThanOrEqualToMinimumWidthInRange(newWidth, [self widthRange]));
    if (newWidth == (int)NSWidth([self bounds]))
        return;
    
    newFrameSize = [self frame].size;
    newFrameSize.width = newWidth;
    [self setFrameSize:newFrameSize];
}

- (float)heightForCurrentWidth;
{
    [self relayoutIfNeeded];
    return NSHeight([self bounds]);
}


// NSResponder subclass

- (BOOL)acceptsFirstResponder;
{
    return YES;
}

- (BOOL)becomeFirstResponder;
{
    [[[self htmlPageView] frameView] becomeActiveFrame];
    [self updateFontManager];
    return YES;
}

- (BOOL)resignFirstResponder;
{
    [self setSelectedRange:NSMakeRange(NSNotFound, 0)];
    [[[self htmlPageView] frameView] resignActiveFrame];
    // Because of a bug in NS4.2, displayIfNeeded in the mouseDown: below will mark the superview as not needing display, so it will never lose its selection.
    [self displayIfNeeded];
    return YES;
}

- (void)mouseDown:(NSEvent *)event;
{
    NSSelectionGranularity currentGranularity;
    NSDate *distantFuture;
    BOOL extendSelection;
    unsigned int anchorCharacterIndex, currentCharacterIndex;
    unsigned int glyphIndex;
    float fraction;
    BOOL isReallyOnGlyph;
    BOOL periodEventsStarted = NO;
    NSEvent *lastMouseEvent = nil;
    NSPoint point;
    NSPoint mouseDownPoint;
    OHBasicCell *cell;
    NSRect bounds;

    bounds = [self bounds];
    
    extendSelection = ([event modifierFlags] & NSShiftKeyMask) && (selectedRange.location != NSNotFound);
    if (!extendSelection) {
        switch ([event clickCount]) {
            case 1:
                currentGranularity = NSSelectByCharacter;
                break;
            case 2:
                currentGranularity = NSSelectByWord;
                break;
            default:
                currentGranularity = NSSelectByParagraph;
                break;
        }
    } else
        currentGranularity = selectionGranularity;
    distantFuture = [NSDate distantFuture];

    mouseDownPoint = point = [self convertPoint:[event locationInWindow] fromView:nil];

    cell = [self marginalCellAtPoint:point];
    if (cell) {
        OHHTMLAnchor *clickAnchor;

        if ([cell wantsToTrackMouse] && [cell trackMouse:event inHTMLView:self])
            return;
        clickAnchor = [cell link];
        if (clickAnchor)
            [self mouseDown:event onAnchor:clickAnchor];
        return;
    }

    if ([lines count] == 0)
        return;

    glyphIndex = [self glyphIndexForPoint:point fractionOfDistanceThroughGlyph:&fraction isReallyOnGlyph:&isReallyOnGlyph];

    if (isReallyOnGlyph) {
        OHHTMLAnchor *clickAnchor;
        unsigned int hitCharacterIndex;

        hitCharacterIndex = [layoutManager characterIndexForGlyphAtIndex:glyphIndex];
        cell = [self cellAtCharacterIndex:hitCharacterIndex];
        // If we click on a cell that wants to track, let it.
        if (cell && [cell wantsToTrackMouse] && [self mouse:point inRect:[cell cellFrame]] && [cell trackMouse:event inHTMLView:self])
            return;
        
        // If we actually clicked on an anchor, follow it
        clickAnchor = [self linkAtCharacterIndex:hitCharacterIndex];
        if (clickAnchor) {
            [self mouseDown:event onAnchor:clickAnchor];
            return;
        }
    }
    
    // If user didn't click a link, unselect any selected links.
    [[self htmlPageView] selectAnchor:nil];

    currentCharacterIndex = [self characterIndexForGlyphIndex:glyphIndex fractionOfDistanceThroughGlyph:fraction granularity:currentGranularity];

    if (!extendSelection)
        anchorCharacterIndex = currentCharacterIndex;
    else {
        unsigned int distanceFromBeginning, distanceFromEnd;

        // Determine which end of old selection we're closer to
        distanceFromBeginning = MAX(currentCharacterIndex, selectedRange.location) - MIN(currentCharacterIndex, selectedRange.location);
        distanceFromEnd = MAX(currentCharacterIndex, NSMaxRange(selectedRange)) - MIN(currentCharacterIndex, NSMaxRange(selectedRange));

        // Set our anchor to be the farther end
        if (distanceFromBeginning < distanceFromEnd)
            anchorCharacterIndex = NSMaxRange(selectedRange);
        else
            anchorCharacterIndex = selectedRange.location;
    }

    while (1) {
        unsigned int eventType = NSLeftMouseDown;
        unsigned int selectionStartIndex, selectionEndIndex;
        NSRange newSelectedRange;

        selectionStartIndex = MIN(anchorCharacterIndex, currentCharacterIndex);
        selectionEndIndex = MAX(anchorCharacterIndex, currentCharacterIndex);

        // Change selection based on currentGranularity
        newSelectedRange = [self selectionRangeForProposedRange:NSMakeRange(selectionStartIndex, selectionEndIndex - selectionStartIndex) granularity:currentGranularity];

        if (!NSEqualRanges(newSelectedRange, selectedRange)) {
            NSRect dirtyRect;
            
            // Mark old selection as needing to be redrawn
            dirtyRect = [self containingRectForCharacterRange:selectedRange];
            
            [self setSelectedRange:newSelectedRange affinity:NSSelectionAffinityUpstream stillSelecting:YES];

            // Mark new selection as needing to be redrawn
            dirtyRect = NSUnionRect(dirtyRect, [self containingRectForCharacterRange:selectedRange]);

            // Draw all the way to margins, because the selection draws that far
            dirtyRect.origin.x = bounds.origin.x;
            dirtyRect.size.width = bounds.size.width;
            [self displayRect:dirtyRect];
        }
        
        event = [NSApp nextEventMatchingMask:NSLeftMouseDraggedMask | NSLeftMouseUpMask | NSPeriodicMask untilDate:distantFuture inMode:NSEventTrackingRunLoopMode dequeue:YES];

        eventType = [event type];
        if (eventType == NSLeftMouseUp)
            break;

        // Find out which glyph mouse is over
        switch (eventType) {
            case NSLeftMouseDragged:
                point = [self convertPoint:[event locationInWindow] fromView:nil];
                if ([self mouse:point inRect:[self visibleRect]]) {
                    if (periodEventsStarted) {
                        [NSEvent stopPeriodicEvents];
                        periodEventsStarted = NO;
                    }
                } else {
                    if (!periodEventsStarted) {
                        [NSEvent startPeriodicEventsAfterDelay:0.0 withPeriod:0.1];
                        periodEventsStarted = YES;
                    }
                    // Retain last mouse event for autoscrolling
                    [lastMouseEvent release];
                    lastMouseEvent = [event retain];
                }
                break;
            case NSPeriodic:
                event = lastMouseEvent; // To update
                [self autoscroll:event];
                point = [self convertPoint:[event locationInWindow] fromView:nil];
                break;
            default:
                // NOT REACHED
                break;
        }

        // When user drags three pixels, special case turns off
        if (ABS(point.x - mouseDownPoint.x) > 3.0 || ABS(point.y - mouseDownPoint.y) > 3.0) {
            // TODO: And we're on a linefeed, do something
        }

        glyphIndex = [self glyphIndexForPoint:point fractionOfDistanceThroughGlyph:&fraction isReallyOnGlyph:NULL];
        currentCharacterIndex = [self characterIndexForGlyphIndex:glyphIndex fractionOfDistanceThroughGlyph:fraction granularity:currentGranularity];
    }

    [lastMouseEvent release];
    if (periodEventsStarted)
        [NSEvent stopPeriodicEvents];

    [self setSelectedRange:selectedRange affinity:NSSelectionAffinityUpstream stillSelecting:NO];
    selectionGranularity = currentGranularity;
}

- (void)rightMouseDown:(NSEvent *)event;
{
    NSPoint point;
    OHBasicCell *cell;
    OHHTMLAnchor *clickAnchor = nil;

    // TODO: Fix this so it shares common code with -mouseDown: rather than having a bunch of similar code which has to be updated separately.
    [[self window] makeFirstResponder:self];
    [self setSelectedRange:NSMakeRange(0,0)];

    point = [self convertPoint:[event locationInWindow] fromView:nil];

    cell = [self marginalCellAtPoint:point];
    if (cell) {
        OHHTMLAnchor *clickAnchor;

        if ([cell wantsToTrackMouse] && [cell trackMouse:event inHTMLView:self])
            return;
        clickAnchor = [cell link];
        if (clickAnchor)
            [self mouseDown:event onAnchor:clickAnchor];
        return;
    }

    if (!clickAnchor) {
        unsigned int glyphIndex;
        float fraction;
        BOOL isReallyOnGlyph;

        glyphIndex = [self glyphIndexForPoint:point fractionOfDistanceThroughGlyph:&fraction isReallyOnGlyph:&isReallyOnGlyph];

        if (isReallyOnGlyph) {
            unsigned int hitCharacterIndex;

            hitCharacterIndex = [layoutManager characterIndexForGlyphAtIndex:glyphIndex];
            cell = [self cellAtCharacterIndex:hitCharacterIndex];
            // If we click on a cell that wants to track, let it.
            if (cell && [cell wantsToTrackMouse] && [self mouse:point inRect:[cell cellFrame]] && [cell trackMouse:event inHTMLView:self]) {
                return;
            } else {
                // OK, see if we clicked on an anchor
                clickAnchor = [self linkAtCharacterIndex:hitCharacterIndex];
            }
        }
    }
    
    if (clickAnchor) {
        // If we clicked on an anchor, select it
        [[self htmlPageView] selectAnchor:clickAnchor];
    }
    [self showContextMenuForMouseEvent:event];
}



- (void)keyDown:(NSEvent *)event;
{
    [[self htmlPageView] keyDown:event];
}

- (void)selectAll:(id)sender;
{
    [self setSelectedRange:NSMakeRange(0, [textStorage length])];
}

- (void)copy:(id)sender;
{
    if (selectedRange.length == 0) {
        [[[[self htmlPageView] selectedAnchor] address] copyToGeneralPasteboard];
        return;
    }

    // TODO: this should just check the selectedRange for attachments, not all the text.
    [self writeSelectionToPasteboard:[NSPasteboard generalPasteboard] types:[NSArray arrayWithObjects:NSStringPboardType, NSRTFPboardType, ([textStorage containsAttachments] ? NSRTFDPboardType : nil), nil]];
}

- (id)validRequestorForSendType:(NSString *)sendType returnType:(NSString *)returnType
{
    BOOL sendTypeIsRTFD;
    
    if (returnType)
        return nil;

    sendTypeIsRTFD = [sendType isEqualToString:NSRTFDPboardType];
    // TODO: this should just check the selectedRange for attachments, not all the text.
    if (sendTypeIsRTFD && ![textStorage containsAttachments])
        return nil;
    
    if (!([sendType isEqualToString:NSStringPboardType] || [sendType isEqualToString:NSRTFPboardType] || sendTypeIsRTFD))
        return nil;

// TODO: Add ability to send anchors to services menu items
//    if ([[self htmlPageView] selectedAnchor] != nil)
//        return YES;
    if (selectedRange.length == 0)
        return nil;
    return self;

}


// NSView subclass

- (void)drawRect:(NSRect)rect;
{
    unsigned int lineCount;

#if defined(RHAPSODY) && (OBOperatingSystemMajorVersion > 5)
    flags.isDrawingToScreen = [[NSGraphicsContext currentContext] isDrawingToScreen];
#else
    flags.isDrawingToScreen = [[NSDPSContext currentContext] isDrawingToScreen];
#endif
    // Reset the antialias flag to its base state
    flags.currentlyAntialiased = NO;
	
    // Note: In a table, the table calls -drawBackgroundForRect: on us (so it can fill the entire bounds), so we don't do it here.  If we're a htmlPageView, this method is subclassed so -drawBackgroundForRect: is called first.

    lineCount = [lines count];
    if (lineCount > 0) {
        unsigned int lineIndex;
        NSRange selectedGlyphRange;

        lineIndex = [self lineIndexAtY:NSMinY(rect) wantsLineAboveIfNoMatch:YES lineComparesToY:NULL];

        // Calculate selected glyph range
        if (flags.isDrawingToScreen)
            selectedGlyphRange = [layoutManager glyphRangeForCharacterRange:selectedRange actualCharacterRange:NULL];
        else // Don't draw selection during copying EPS or printing
            selectedGlyphRange = NSMakeRange(0,0);

        // Draw lines in rect
        for (; lineIndex < lineCount; lineIndex++) {
            if (![[lines objectAtIndex:lineIndex] drawRect:rect selectedGlyphRange:selectedGlyphRange inHTMLView:self])
                break;
        }
    }

    // Turn antialiasing off
    [self setAntialias:NO];

    // Draw our side-aligned images
    [self drawMarginalCellsInRect:rect];

    // Draw box around selected anchor
    [self drawBoxAroundSelectedAnchor];

    // Draw the frame border
    [[[self htmlPageView] frameView] drawFrameBorderForDocumentView:self rect:rect];
}

- (void)setFrameSize:(NSSize)newSize;
{
    [self setFrameSize:newSize relayout:YES];
}

- (BOOL)isFlipped;
{
    return YES;
}

- (BOOL)acceptsFirstMouse:(NSEvent *)event
{
    return YES;
}

- (void)scrollPoint:(NSPoint)aPoint;
{
    OHHTMLView *htmlPageView;

    htmlPageView = [self htmlPageView];
    if (htmlPageView == self) {
        aPoint.x = 0.0;
        [super scrollPoint:aPoint];
    } else {
        aPoint = [self convertPoint:aPoint toView:htmlPageView];
        aPoint.x = 0.0;
        [htmlPageView scrollPoint:aPoint];
    }
}

- (BOOL)scrollRectToVisible:(NSRect)aRect;
{
    OHHTMLView *htmlPageView;

    htmlPageView = [self htmlPageView];
    if (htmlPageView == self) {
        aRect.origin.x = 0.0;
        aRect.size.width = 1.0;
        return [super scrollRectToVisible:aRect];
    } else {
        aRect = [self convertRect:aRect toView:htmlPageView];
        aRect.origin.x = 0.0;
        aRect.size.width = 1.0;
        return [htmlPageView scrollRectToVisible:aRect];
    }
}

- (NSMenu *)menuForEvent:(NSEvent *)event
{
    OHHTMLPageView *htmlPageView;
    OHHTMLLink *clickLink;
    OHHTMLAnchor *clickAnchor;

    /*
    clickLink = [self linkAtPoint:[self convertPoint:[event locationInWindow] fromView:nil]];
    if (!clickLink || ![clickLink address])
        return [super menuForEvent:event];

    if (![clickLink isKindOfClass:[OHHTMLAnchor class]]) {
        // Image map links aren't anchors, so let's create one.
        clickAnchor = [[[OHHTMLAnchor alloc] initWithColorPalette:[OHColorPalette defaultPalette]] autorelease];
        [clickAnchor setAddress:[clickLink address]];
    } else {
        clickAnchor = (OHHTMLAnchor *)clickLink;
    }
    htmlPageView = [self htmlPageView];
    if (clickAnchor != [htmlPageView selectedAnchor])
        [htmlPageView selectAnchor:clickAnchor];
    [htmlPageView selectAnchor:clickAnchor];
*/
    
    return linkMenu;
}

- (void)viewWillMoveToWindow:(NSWindow *)newWindow;
{
    NSWindow *oldWindow;

    oldWindow = [self window];
    if (oldWindow == newWindow)
        return;
    if (oldWindow) {
        [[NSNotificationCenter defaultCenter] removeObserver:self name:NSWindowDidResignKeyNotification object:oldWindow];
        [self setTrackingRects:nil];
        if ([oldWindow isKeyWindow]) {
            [self setToolTip:nil];
            [[NSToolTipManager sharedToolTipManager] mainThreadPerformSelector:@selector(abortToolTip)];
        }
    }
    [super viewWillMoveToWindow:newWindow];
    if (newWindow) {
        // Presumably, a frame changed notification will be generated soon, which will cause us to reset our tracking rects if we're still on screen.
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(trackingRectRelevantNotification:) name:NSWindowDidResignKeyNotification object:newWindow];
    }
}

- (void)resetCursorRects;
{
    [self resetCursorRects:YES andCalculateTrackingRects:NO];
    [self queueSelectorOnce:@selector(resetAnchorTrackingRects)];
}

- (void)setNeedsDisplay:(BOOL)needsDisplay;
{
    if (![NSThread inMainThread]) {
        [self queueSelector:@selector(setNeedsDisplay:) withBool:needsDisplay];
        return;
    }

    [super setNeedsDisplay:needsDisplay];
}


// NSTextView look-alike

- (NSString *)string;
{
    return [textStorage string];
}

- (void)setBackgroundColor:(NSColor *)aColor;
{
    if (aColor == backgroundColor)
        return;
    
    [backgroundColor release];
    backgroundColor = [aColor retain];
    [self setNeedsDisplay:YES];
}

- (BOOL)drawsBackground;
{
    return flags.usesOwnBackgroundColor;
}

- (void)setTextContainerInset:(NSSize)inset;
{
    [self setMarginSize:inset advisory:YES];
}

- (NSSize)textContainerInset;
{
    return textContainerInset;
}

- (NSRange)selectedRange;
{
    return selectedRange;
}

- (void)setSelectedRange:(NSRange)charRange;
{
    [self setSelectedRange:charRange affinity:NSSelectionAffinityUpstream stillSelecting:NO];
}

- (void)setSelectedRange:(NSRange)charRange affinity:(NSSelectionAffinity)affinity
    stillSelecting:(BOOL)stillSelecting;
{
    OBASSERT(charRange.location == NSNotFound || NSMaxRange(charRange) <= [textStorage length]);

    selectedRange = charRange;
    selectionGranularity = NSSelectByCharacter;

    selectionHoldingCellIndexHint = NSNotFound; // Not strictly necessary, since we always check to make sure this is valid before we use it, but it makes debugging easier.
    
    if (!stillSelecting) {
        flags.selectedRangeChangedSinceLastFontCalculation = YES;
        [self updateFontManager];
// WJS: I commented this out because NSTextView doesn't make itself first responder when a selection is set, contrary to what we thought before.
//        if (selectedRange.location != NSNotFound)
//            [[self window] makeFirstResponder:self];
        [self setNeedsDisplay:YES];
    }
}

- (NSRange)selectionRangeForProposedRange:(NSRange)proposedSelRange granularity:(NSSelectionGranularity)granularity;
{
    switch (granularity) {
        default:
            return proposedSelRange;
        case NSSelectByWord:
        {
            NSRange leftEdge, rightEdge;

            leftEdge = [textStorage doubleClickAtIndex:proposedSelRange.location];
            if (proposedSelRange.length == 0)
                return leftEdge;

            rightEdge = [textStorage doubleClickAtIndex:NSMaxRange(proposedSelRange)];
            return NSUnionRange(leftEdge, rightEdge);
        }
        case NSSelectByParagraph:
            proposedSelRange.length++; // Wacky, but it fixes a bug where we don't select until after we drag past the first character on a line.
            return [[textStorage string] lineRangeForRange:proposedSelRange];
    }
}

- (void)scrollRangeToVisible:(NSRange)range;
{
    [self scrollCharacterRangeToVisible:range];
}


// NSServicesRequests informal protocol

- (BOOL)readSelectionFromPasteboard:(NSPasteboard *)pasteboard;
{
    return NO;
}

- (BOOL)writeSelectionToPasteboard:(NSPasteboard *)pasteboard types:(NSArray *)types;
{
    [pasteboard declareTypes:types owner:nil];

    // NSStringPboardType
    if ([types containsObject:NSStringPboardType])
        [pasteboard setString:[[textStorage string] substringWithRange:selectedRange] forType:NSStringPboardType];

    // NSRTFPboardType
    if ([types containsObject:NSRTFPboardType])
        [pasteboard setData:[textStorage RTFFromRange:selectedRange documentAttributes:nil] forType:NSRTFPboardType];

    // NSRTFDPboardType
    if ([types containsObject:NSRTFDPboardType])
        [pasteboard setData:[textStorage RTFDFromRange:selectedRange documentAttributes:nil] forType:NSRTFDPboardType];

    return YES;
}



// NSMenuActionResponder informal protocol

- (BOOL)validateMenuItem:(NSMenuItem *)item
{
    if ([item action] == @selector(copy:)) {
        if ([[self htmlPageView] selectedAnchor] != nil)
            return YES;
        if (selectedRange.length == 0)
            return NO;
        return YES;
    }
    return YES;
}


// Building the HTML view

- (void)appendAttributedString:(NSAttributedString *)attributedString;
{
    [self queueSelector:@selector(mainThreadAppendAttributedString:) withObject:attributedString];
}

- (void)setRequiredWidth:(float)aWidth;
{
    if (aWidth > requiredWidth) {
        requiredWidth = aWidth;
        [self cachedWidthsAreInvalid];
    }
}

- (void)addMarginalCell:(OHBasicCell *)cell;
{
    OBASSERT([cell characterPosition] != NSNotFound);
    if (!marginalCells)
        marginalCells = [[NSMutableArray allocWithZone:[self zone]] init];
    [marginalCells addObject:cell];
    [self scheduleRelayout];
}

- (NSArray *)marginalCells;
{
    return marginalCells;
}

- (void)setValidMarginalCellCount:(unsigned int)newCount;
{
    firstUnlaidMarginalCellIndex = newCount;
}

- (void)addClearBreakObject:(OHBreak *)breakObject;
{
    if (!clearBreakObjects)
        clearBreakObjects = [[NSMutableArray allocWithZone:[self zone]] init];
    [clearBreakObjects addObject:breakObject];
    [self scheduleRelayout];
}

- (NSArray *)clearBreakObjects;
{
    return clearBreakObjects;
}

// Background

- (void)setBackgroundImageAddress:(OWAddress *)anAddress;
{
    OWPipeline *pipeline;

    if (backgroundImageAddress == anAddress)
        return;

    // If background images are disabled, we go ahead and create the pipeline (so that it shows up in the inspector where it can be reloaded), we just never start it.

    [backgroundImageAddress release];
    backgroundImageAddress = [anAddress retain];
    pipeline = [[OWWebPipeline alloc] initWithContent:backgroundImageAddress target:self];
    [pipeline setContextObject:@"YES" forKey:@"DisableIncrementalDisplay"];
    if (![[OFUserDefaults sharedUserDefaults] boolForKey:@"OHBackgroundImagesDisabled"])
        [pipeline startProcessingContent];
    [pipeline release];
}

- (BOOL)usesOwnBackgroundColor;
{
    return flags.usesOwnBackgroundColor;
}

- (void)setUsesOwnBackgroundColor:(BOOL)newSetting;
{
    flags.usesOwnBackgroundColor = newSetting;
}

- (void)drawBackgroundForRect:(NSRect)rect backgroundOrigin:(NSPoint)backgroundOrigin;
{
    if ([[OFUserDefaults sharedUserDefaults] boolForKey:@"OHBackgroundImagePrintingDisabled"]) {
#if defined(RHAPSODY) && (OBOperatingSystemMajorVersion > 5)
        if (![[NSGraphicsContext currentContext] isDrawingToScreen])
#else    
        if (![[NSDPSContext currentContext] isDrawingToScreen])
#endif
        {
            [[NSColor whiteColor] set];
            NSRectFill(rect);
            return;
        }
    }
    if ([self drawsBackground]) {
        [[self backgroundColor] set];
        NSRectFill(rect);
    }
    [self drawBackgroundImageInRect:rect backgroundOrigin:backgroundOrigin];
}

- (void)setAntialias:(BOOL)newAntialias;
{
    if (!flags.isDrawingToScreen || !flags.shouldAntialiasText)
        return;
    if (newAntialias == flags.currentlyAntialiased)
        return;
    // TODO: Get this doing the right thing on OSX DP2 and above.
#if defined(RHAPSODY) && (OBOperatingSystemMajorVersion <= 5)
    if (newAntialias) {
        PSsetantialias();
    } else {
        PSresetantialias();
    }
#endif
    flags.currentlyAntialiased = newAntialias;
}

// OHOptionalDocumentView protocol

- (NSPoint)draggingStartingLocationInWindow;
{
    return draggingStartingLocationInWindow;
}

// Context menu

- (void)showContextMenuForMouseEvent:(NSEvent *)mouseEvent;
{
#if defined(RHAPSODY) || defined(WIN32)
    [super rightMouseDown:mouseEvent];
#endif
}

// OHHTMLTextViewWithBackgroundImage protocol

- (NSImage *)backgroundImage;
{
    return backgroundImage;
}

#define MINIMUM_IMAGE_EDGE 64

- (void)setBackgroundImage:(NSImage *)image;
{
    NSSize imageSize;
    NSImage *bigImage;

    if (backgroundImage == image)
        return;

    [backgroundImage release];
    backgroundImage = nil;

    imageSize = [image size];

    if (imageSize.width < 0.01 || imageSize.height < 0.01)
        return;

    if (imageSize.width < MINIMUM_IMAGE_EDGE ||
        imageSize.height < MINIMUM_IMAGE_EDGE) {
        int widthMultiple, thisWidthMultiple;
        int heightMultiple, thisHeightMultiple;
        NSSize bigImageSize;
        double widthOffset;
        double heightOffset;

        // Ken:  I'm guessing we're adding 0.00001 here to avoid some rounding errors on some architecture where casting the return value of ceil() to an int is actually returning the lower int.  But that's just a guess...  Anyone care to confirm?
        // Ken says:  I think that perhaps ceil() was sometimes returning 6.9999999, so we add this to make it 7.0000099, which then truncates back to 7 when it's cast to an int.  But I don't actually know for sure, I guess we could try without it...
        widthMultiple = ceil(MINIMUM_IMAGE_EDGE / imageSize.width) + 0.00001;
        heightMultiple = ceil(MINIMUM_IMAGE_EDGE / imageSize.height) + 0.00001;

        bigImageSize = NSMakeSize(imageSize.width * widthMultiple, imageSize.height * heightMultiple);
        bigImage = [[NSImage allocWithZone:[self zone]] initWithSize:bigImageSize];
        [bigImage setCachedSeparately:YES];
        NS_DURING {
            [bigImage lockFocus];
            for (widthOffset = 0, thisWidthMultiple = widthMultiple;
                 thisWidthMultiple--;
                 widthOffset += imageSize.width) {
                for (heightOffset = 0, thisHeightMultiple = heightMultiple;
                     thisHeightMultiple--;
                     heightOffset += imageSize.height) {
                    NSPoint point;

                    // I'd guess we're not using NSMakePoint() here because it was generating spurious may-be-uninitialized warnings (due to the NS_DURING block).
                    point.x = widthOffset;
                    point.y = heightOffset;
                    [image compositeToPoint:point operation:NSCompositeCopy];
                }
            }
            [bigImage unlockFocus];
        } NS_HANDLER {
            [bigImage release];
            bigImage = nil;
        } NS_ENDHANDLER;

        backgroundImage = bigImage;
    } else
        backgroundImage = [image retain];

    [self setNeedsDisplay:YES];
}

- (void)drawBackgroundImageInRect:(NSRect)rect backgroundOrigin:(NSPoint)backgroundOrigin;
{
    NSRect tileRect;
    float horizontalSkipCount, verticalSkipCount;
    
    if (!backgroundImage)
        return;

    tileRect.size = [backgroundImage size];

    horizontalSkipCount = floor((NSMinX(rect) - backgroundOrigin.x) / NSWidth(tileRect));
    verticalSkipCount = floor((NSMinY(rect) - backgroundOrigin.y) / NSHeight(tileRect));
    
    NS_DURING {
        for (tileRect.origin.x = backgroundOrigin.x + horizontalSkipCount * NSWidth(tileRect);
             NSMinX(tileRect) < NSMaxX(rect);
             tileRect.origin.x += NSWidth(tileRect)) {

            for (tileRect.origin.y = backgroundOrigin.y + verticalSkipCount * NSHeight(tileRect);
                 NSMinY(tileRect) < NSMaxY(rect);
                 tileRect.origin.y += NSHeight(tileRect)) {
                NSRect intersectionRect, fromRect;
                NSPoint compositePoint;

                intersectionRect = NSIntersectionRect(tileRect, rect);
                if (NSIsEmptyRect(intersectionRect))
                    continue;

                // Using NSMakePoint here causes a warning about longjmp clobbering
                compositePoint.x = NSMinX(intersectionRect);
                compositePoint.y = NSMaxY(intersectionRect);
                fromRect = NSMakeRect(NSMinX(intersectionRect) - NSMinX(tileRect), NSMaxY(tileRect) - NSMaxY(intersectionRect), NSWidth(intersectionRect), NSHeight(intersectionRect));

                [backgroundImage compositeToPoint:compositePoint fromRect:fromRect operation:NSCompositeSourceOver];
            }
        }
    } NS_HANDLER {
        [NSApp reportException:localException];
    } NS_ENDHANDLER;
}

// OHHTMLPageContent protocol

- (OHHTMLPageView *)htmlPageView;
{
    return [[nonretainedHTMLOwner htmlDocument] htmlPageView];
}

- (NSColor *)backgroundColor;
{
    if ([self drawsBackground] || [self class] == [OHHTMLPageView class]) {
        return backgroundColor;
    } else {
        id <OHHTMLPageContent> parentContent;

        // Return our parentContent's background color
        parentContent = (id <OHHTMLPageContent>)[self superview];
        return [parentContent backgroundColor];
    }
}

- (void)cachedWidthsAreInvalid;
{
    flags.cachedWidthRangeIsValid = NO;
    [self scheduleRelayout];
}

    // Call from any thread
- (void)scheduleRelayout;
{
    if (flags.needsRelayout)
        return;

    flags.needsRelayout = YES;

    // Note:  This used to be a -queueSelectorOnce: before I added the short-circuit for the case where flags.needsRelayout is already set to YES.
    // Hack:  Since we don't retain our nonretainedHTMLOwner, we need for it to be retained by the message queue (or we will crash if the page is switched in between this call and when the message gets sent).  Right now, we hack this retain by pretending it's a parameter to the message.
    [self queueSelector:@selector(relayoutIfNeeded) withObject:nonretainedHTMLOwner];
}

    // Main thread only
- (void)relayout;
{
    flags.needsRelayout = NO;
    [self layoutLines];
    [self setNeedsDisplay:YES];
    [[self window] invalidateCursorRectsForView:self];
    [self queueSelectorOnce:@selector(resetAnchorTrackingRects)];
}


// OWTarget protocol

- (OWContentType *)targetContentType;
{
    return [OIImage contentType];
}

- (OWTargetContentDisposition)pipeline:(OWPipeline *)aPipeline hasContent:(id <OWContent>)someContent;
{
    [self setBackgroundOmniImage:(OIImage *)someContent];
    return OWTargetContentDisposition_ContentAccepted;
}

- (OWTargetContentDisposition)pipeline:(OWPipeline *)aPipeline hasAlternateContent:(id <OWContent>)someContent;
{
    if ([someContent isKindOfClass:[OWDataStream class]] && [(OWDataStream *)someContent resetContentTypeAndEncoding]) {
        // OK, we've reset the content type and encoding, let's try again
        [aPipeline addContent:someContent];
        [aPipeline startProcessingContent];
        return OWTargetContentDisposition_ContentUpdatedOrTargetChanged;
    } else {
        return OWTargetContentDisposition_ContentRejectedCancelPipeline;
    }
}

- (NSString *)targetTypeFormatString;
{
    return @"Background %@";
}

- (NSString *)expectedContentTypeString;
{
    return @"Image";
}

- (OWContentInfo *)parentContentInfo;
{
    return [[self htmlPageView] parentContentInfo];
}

// OIImageObserver protocol

- (void)imageDidSize:(OIImage *)anOmniImage;
{
}

- (void)imageDidUpdate:(OIImage *)anOmniImage;
{
    if (anOmniImage != backgroundOmniImage)
	return;
    [self mainThreadPerformSelectorOnce:@selector(backgroundImageDidUpdate)];
}

- (void)imageDidAbort:(OIImage *)anOmniImage;
{
}


// OASearchableContent protocol

- (BOOL)findString:(NSString *)textPattern ignoreCase:(BOOL)ignoreCase backwards:(BOOL)backwards ignoreSelection:(BOOL)ignoreSelection;
{
    NSString *string;
    unsigned int stringLength;
    NSRange startRange, matchStringRange, attachmentRange;
    unsigned int options;
    BOOL attachmentIsMarginal = NO;

    string = [textStorage string];
    if (!string || (stringLength = [string length]) == 0)
        return NO;

    // Get our starting selection
    if (ignoreSelection)
        startRange = NSMakeRange((backwards ? stringLength : 0), 0);
    else {
        [self findSelectionInSubviews];

        if (selectionHoldingCellIndexHint == NSNotFound) {
            startRange = selectedRange;
            if (NSMaxRange(startRange) > stringLength)
                startRange = NSMakeRange(0, 0);
        } else {
            if ([self findString:textPattern ignoreCase:ignoreCase backwards:backwards ignoreSelection:NO inAttachmentAtIndex:selectionHoldingCellIndexHint isMarginal:flags.selectionHoldingCellIsMarginal])
                return YES;
            startRange = NSMakeRange(selectionHoldingCellIndexHint, !flags.selectionHoldingCellIsMarginal);
            attachmentIsMarginal = flags.selectionHoldingCellIsMarginal;
        }
    }

    // Set our search options
    options = (backwards ? NSBackwardsSearch : 0) | (ignoreCase ? NSCaseInsensitiveSearch : 0);

    // Search for a match
    matchStringRange = [string findString:textPattern selectedRange:startRange options:options wrap:NO];

    // Search through attachments until we reach the match or end of text (either direction)
    attachmentRange = [self findNextAttachmentAfterRange:startRange backwards:backwards isMarginal:&attachmentIsMarginal];
    
    while (attachmentRange.location != NSNotFound) {
        // Stop if the attachment we found is after the match we found earlier
        if ((matchStringRange.length > 0)
            && ((backwards && (attachmentRange.location < NSMaxRange(matchStringRange)))
                || (!backwards && (attachmentRange.location > matchStringRange.location))))
            break;

        if ([self findString:textPattern ignoreCase:ignoreCase backwards:backwards ignoreSelection:YES inAttachmentAtIndex:attachmentRange.location isMarginal:attachmentIsMarginal])
            return YES;
        attachmentRange = [self findNextAttachmentAfterRange:attachmentRange backwards:backwards isMarginal:&attachmentIsMarginal];
    }

    if (matchStringRange.length > 0) {
        // Match found
        [self setSelectedRange:matchStringRange];
        [self scrollRangeToVisible:matchStringRange];
        [[self window] makeFirstResponder:self];
        return YES;
    }

    // No match found
    return NO;
}


// OHManyToolTipView protocol

- (NSString *)toolTipForPoint:(NSPoint)point;
{
    if ([[OFUserDefaults sharedUserDefaults] boolForKey:@"OHLinkToolTipDisabled"])
        return nil;

    return [[[self linkAtPoint:[self convertPoint:point fromView:nil]] address] addressString];
}

@end


@implementation OHHTMLView (TextSystemOnly)

- (NSTextStorage *)textStorage;
{
    return textStorage;
}

- (NSLayoutManager *)layoutManager;
{
    return layoutManager;
}

- (NSTextContainer *)textContainer;
{
    return textContainer;
}

- (OFStaticArray *)paragraphs;
{
    return paragraphs;
}

- (OFStaticArray *)words;
{
    return words;
}

- (OFStaticArray *)lines;
{
    return lines;
}

- (OFStaticArray *)lays;
{
    return lays;
}

- (NSRange)characterRangeForWordRange:(NSRange)wordRange;
{
    NSRange characterRange;

    characterRange.location = ((OHWord *)[words objectAtIndex:wordRange.location])->characterRange.location;
    characterRange.length = NSMaxRange(((OHWord *)[words objectAtIndex:NSMaxRange(wordRange) - 1])->characterRange) - characterRange.location;
    return characterRange;
}

- (NSRange)glyphRangeForWordRange:(NSRange)wordRange;
{
    return [layoutManager glyphRangeForCharacterRange:[self characterRangeForWordRange:wordRange] actualCharacterRange:NULL];
}

@end

@implementation OHHTMLView (SubclassesOnly)

- (void)laidOutContentsAtSize:(NSSize)contentSize;
{
    // OHHTMLPageView subclasses this to change its size based on the layout size
}


// Background image handling

- (void)backgroundImageDidUpdate;
{
    // Force -setBackgroundImage: to redraw the image even if it's the same NSImage object, since its backing store presumably changed.
    [backgroundImage release];
    backgroundImage = nil;
    [self setBackgroundImage:[backgroundOmniImage image]];
    [self setNeedsDisplay:YES];
}

@end


@implementation OHHTMLView (Private)

// Layout

- (void)scanParagraphsFromParagraphIndex:(unsigned int)paragraphIndex;
{
    unsigned int numberOfGlyphs;
    unsigned int preallocationCount;
    OFStringScanner *scanner;
    NSRange attributesRange = NSMakeRange(0,0);
    unsigned int wordIndex;
    NSDictionary *attributes = nil;

    flags.cachedWidthRangeIsValid = NO;

    numberOfGlyphs = [layoutManager numberOfGlyphs];
    if (numberOfGlyphs == 0)
        return;
    [layoutManager locationForGlyphAtIndex:numberOfGlyphs - 1]; // Force generation of all glyphs, possible speedup

    scanner = [[OFStringScanner alloc] initWithString:[textStorage string]];

    if (paragraphIndex >= [paragraphs count]) {
        [paragraphs removeAllObjects];
        [words removeAllObjects];
        wordIndex = 0;
    } else {
        OHParagraph *firstRelaidParagraph;
        OHWord *firstRelaidWord;

        firstRelaidParagraph = [paragraphs objectAtIndex:paragraphIndex];
        wordIndex = firstRelaidParagraph->wordRange.location;
        firstRelaidWord = [words objectAtIndex:wordIndex];
        [scanner setScanLocation:firstRelaidWord->characterRange.location];
        
        [paragraphs setCount:paragraphIndex];
        [words setCount:wordIndex];
    }

    preallocationCount = numberOfGlyphs / averageNumberOfLettersPerParagraphIncludingSpaces;
    if (preallocationCount > [paragraphs capacity])
        [paragraphs setCapacity:preallocationCount];
    preallocationCount = numberOfGlyphs / averageNumberOfLettersPerWordIncludingSpaces;
    if (preallocationCount > [words capacity])
        [words setCapacity:preallocationCount];

    while (scannerHasData(scanner))
        [[paragraphs newObject] initWithStringScanner:scanner wordIndex:&wordIndex words:words attributes:&attributes attributesRange:&attributesRange inHTMLView:self];

    [scanner release];
}

- (void)layoutLines;
{
    NSPoint boundsOrigin;
    unsigned int paragraphIndex, paragraphCount;
    float y, maxX = 0.0;
    NSSize pageSize;
    NSRange commonAttributesRange = NSMakeRange(0,0);
    NSDictionary *attributes = nil;
    unsigned int firstUnlaidClearBreakIndex = 0;

    boundsOrigin = [self bounds].origin;
    pageSize = [self pageSize];

    y = boundsOrigin.y + textContainerInset.height;

    [lines removeAllObjects];
    [lines setCapacity:[words count] / averageNumberOfWordsPerLineIncludingSpaces];
    [lays removeAllObjects];
    [lays setCapacity:[words count] / averageNumberOfWordsPerLineIncludingSpaces];
    firstUnlaidMarginalCellIndex = 0;

    paragraphCount = [paragraphs count];

    // Lay out a paragraph at a time
    for (paragraphIndex = 0; paragraphIndex < paragraphCount; paragraphIndex++) {
        OHParagraph *paragraph;
        NSParagraphStyle *paragraphStyle;
        NSTextAlignment alignment;
        float firstLineHeadIndent, headIndent, tailIndent;
        unsigned int wordIndex, paragraphMaxWordIndex;

        paragraph = [paragraphs objectAtIndex:paragraphIndex];
        wordIndex = paragraph->wordRange.location;
        paragraphMaxWordIndex = NSMaxRange(paragraph->wordRange) - 1;

        // Get attributes for start of this paragraph
        while (NSMaxRange(commonAttributesRange) <= ((OHWord *)[words objectAtIndex:paragraph->wordRange.location])->characterRange.location)
            attributes = [textStorage attributesAtIndex:NSMaxRange(commonAttributesRange) effectiveRange:&commonAttributesRange];

        paragraphStyle = [attributes objectForKey:NSParagraphStyleAttributeName];
        if (!paragraphStyle)
            paragraphStyle = [NSParagraphStyle defaultParagraphStyle];
        alignment = [paragraphStyle alignment];
        firstLineHeadIndent = [paragraphStyle firstLineHeadIndent];
        headIndent = [paragraphStyle headIndent];
        tailIndent = [paragraphStyle tailIndent];

        // Within each paragraph lay its words into multiple lines
        while (wordIndex <= paragraphMaxWordIndex) {
            unsigned int characterIndex = NSNotFound;
            float thisLineHeadIndent, thisLineWidth;
            NSRect layoutBounds;
            OHLine *line;

            // First line in paragraph
            if (wordIndex == paragraph->wordRange.location)
                thisLineHeadIndent = firstLineHeadIndent;
            else
                thisLineHeadIndent = headIndent;

            if (marginalCells) {
                characterIndex = ((OHWord *)[words objectAtIndex:wordIndex])->characterRange.location;
                if (clearBreakObjects)
                    y = [self newYAfterTakingClearBreaksFromY:y characterIndex:characterIndex firstUnlaidClearBreakIndex:&firstUnlaidClearBreakIndex];
            }


            if (tailIndent <= 0)
                thisLineWidth = pageSize.width - 2 * textContainerInset.width + tailIndent - thisLineHeadIndent;
            else
                thisLineWidth = MIN((pageSize.width - 2 * textContainerInset.width), (tailIndent - thisLineHeadIndent));

            layoutBounds = NSMakeRect(boundsOrigin.x + textContainerInset.width + thisLineHeadIndent, y, thisLineWidth, pageSize.height);

            if (marginalCells)
                layoutBounds = [self newLayoutBoundsAfterTakingMarginalCellsFromLayoutBounds:layoutBounds characterIndex:characterIndex];
            
            line = [[lines newObject] initFromParagraph:paragraph wordIndex:&wordIndex lastWordIndex:paragraphMaxWordIndex maxX:&maxX bounds:layoutBounds alignment:alignment attributes:&attributes attributesRange:&commonAttributesRange inHTMLView:self];

            OBASSERT(line != nil);
            y = NSMaxY([line bounds]);
        }
    }

    // Lay out any marginal cell on the VERY last character (won't get laid out normally, because it'll show up as being at [textStorage length], which isn't a character.
    if (marginalCells) {
        unsigned int characterIndex = [textStorage length];

        if (clearBreakObjects)
            y = [self newYAfterTakingClearBreaksFromY:y characterIndex:characterIndex firstUnlaidClearBreakIndex:&firstUnlaidClearBreakIndex];
        [self newLayoutBoundsAfterTakingMarginalCellsFromLayoutBounds:NSMakeRect(boundsOrigin.x + textContainerInset.width, y, pageSize.width - 2 * textContainerInset.width, pageSize.height) characterIndex:characterIndex];
    }

    // Do an automatic break clear=all so that any final marginal cells will be given room
    y = [self newYAfterTakingClearBreakFromY:y clearBreakType:OHBreakClearBothSides];

    [self laidOutContentsAtSize:NSMakeSize(maxX - boundsOrigin.x - textContainerInset.width, y - boundsOrigin.y - textContainerInset.height)];
}

- (NSRect)newLayoutBoundsAfterTakingMarginalCellsFromLayoutBounds:(NSRect)layoutBounds characterIndex:(unsigned int)characterIndex;
{
    unsigned int marginalCellIndex, marginalCellCount;

    marginalCellCount = [marginalCells count];

    // Chop laid-out marginal cells out of our layout bounds
    for (marginalCellIndex = 0; marginalCellIndex < firstUnlaidMarginalCellIndex; marginalCellIndex++) {
        // Note: this will end up chopping out cells that are BELOW the current line if we have any marginal cells laid-out from a previous layoutLines call.  Right now this can't happen, but if we ever try to get fancy and start laying out lines in the middle of the text object and NOT lay out all the way to the end, this will break.
        layoutBounds = [[marginalCells objectAtIndex:marginalCellIndex] cutFromLayoutBounds:layoutBounds];
    }

    // Layout unlaid marginal cells on this line if they appear before or on the first character of the first word.
    while (firstUnlaidMarginalCellIndex < marginalCellCount) {
        OHBasicCell *marginalCell;
        unsigned int firstUnlaidMarginalCellCharacterIndex;

        marginalCell = [marginalCells objectAtIndex:firstUnlaidMarginalCellIndex];
        firstUnlaidMarginalCellCharacterIndex = [marginalCell characterPosition];

        if (firstUnlaidMarginalCellCharacterIndex > characterIndex)
            break;
        
        [marginalCell setLayoutBounds:layoutBounds];
        layoutBounds = [marginalCell cutFromLayoutBounds:layoutBounds];

        firstUnlaidMarginalCellIndex++;
    }

    return layoutBounds;
}

- (float)newYAfterTakingClearBreaksFromY:(float)y characterIndex:(unsigned int)characterIndex firstUnlaidClearBreakIndex:(unsigned int *)firstUnlaidClearBreakIndex;
{
    unsigned int clearBreakCount;

    clearBreakCount = [clearBreakObjects count];
    
    while (*firstUnlaidClearBreakIndex < clearBreakCount) {
        OHBreak *clearBreak;
        unsigned int firstUnlaidClearBreakCharacterIndex;

        clearBreak = [clearBreakObjects objectAtIndex:*firstUnlaidClearBreakIndex];
        firstUnlaidClearBreakCharacterIndex = [clearBreak characterPosition];

        if (firstUnlaidClearBreakCharacterIndex != characterIndex)
            break;
        
        y = [self newYAfterTakingClearBreakFromY:y clearBreakType:[clearBreak type]];

        (*firstUnlaidClearBreakIndex)++;
    }

    return y;
}

- (float)newYAfterTakingClearBreakFromY:(float)y clearBreakType:(OHBreakType)breakType;
{
    unsigned int marginalCellIndex, marginalCellCount;

    marginalCellCount = [marginalCells count];
    for (marginalCellIndex = 0; marginalCellIndex < MIN(firstUnlaidMarginalCellIndex, marginalCellCount); marginalCellIndex++) {
        OHBasicCell *marginalCell;
        NSRect cellFrame;

        marginalCell = [marginalCells objectAtIndex:marginalCellIndex];
        if (breakType != OHBreakClearBothSides) {
            OHCellAlignment marginalAlignment;

            marginalAlignment = [marginalCell marginalAlignment];
            if ((breakType == OHBreakClearLeftSide && marginalAlignment == OHCellAlignMarginalRight) || (breakType == OHBreakClearRightSide && marginalAlignment == OHCellAlignMarginalLeft))
                continue;
        }
        
        cellFrame = [marginalCell cellFrame];
        if (NSMinY(cellFrame) <= y && y < NSMaxY(cellFrame))
            y = NSMaxY(cellFrame);
    }

    return y;
}

// Selection

- (unsigned int)characterIndexForGlyphIndex:(unsigned int)glyphIndex fractionOfDistanceThroughGlyph:(float)fraction granularity:(NSSelectionGranularity)granularity;
{
    unsigned int characterIndex;

    characterIndex = [layoutManager characterIndexForGlyphAtIndex:glyphIndex];

    if (granularity != NSSelectByCharacter || fraction < .5)
        return characterIndex;

    glyphIndex++;

    if (glyphIndex < [layoutManager numberOfGlyphs])
        return [layoutManager characterIndexForGlyphAtIndex:glyphIndex];
    else
        return [textStorage length];
}

- (void)updateFontManager;
{
    unsigned int textLength;
    NSRange checkRange;
    NSRange effectiveRange;
    NSFont *font;

    if (selectedRange.location == NSNotFound)
        return;

    // If there is an empty selection at the end of the text, selectedRange will be equal to textLength, so we have to decrement it to check the last character.
    textLength = [textStorage length];
    if (!textLength)
        return;
    
    checkRange = selectedRange;
    if (checkRange.location >= textLength)
        checkRange.location = textLength - 1;
    
    font = [textStorage attribute:NSFontAttributeName atIndex:checkRange.location effectiveRange:&effectiveRange];
    
    if (selectedRange.length <= 1)
        flags.selectedRangeContainsMultipleFonts = NO;
    else if (flags.selectedRangeChangedSinceLastFontCalculation) {

        flags.selectedRangeChangedSinceLastFontCalculation = NO;
        // Step through selectedRange, figure out if there are multiple fonts in selection.
        while ((NSMaxRange(effectiveRange) < NSMaxRange(checkRange)) && (NSMaxRange(effectiveRange) < textLength)) {
            NSFont *otherFont;

            otherFont = [textStorage attribute:NSFontAttributeName atIndex:NSMaxRange(effectiveRange) effectiveRange:&effectiveRange];

            if (![font isEqual:otherFont]) {
                flags.selectedRangeContainsMultipleFonts = YES;
                break;
            }
        }
    }
    
    [[NSFontManager sharedFontManager] setSelectedFont:font isMultiple:flags.selectedRangeContainsMultipleFonts];
}

- (NSRange)maximalCharacterRangeForRect:(NSRect)rect;
{
    OHLine *firstLine, *lastLine;
    unsigned int firstWordIndex, lastWordIndex;

    if ([lines count] == 0)
        return NSMakeRange(0,0);
    
    // Binary search for top line, bottom line
    firstLine = [lines objectAtIndex:[self lineIndexAtY:NSMinY(rect) wantsLineAboveIfNoMatch:YES lineComparesToY:NULL]];
    lastLine = [lines objectAtIndex:[self lineIndexAtY:NSMaxY(rect) wantsLineAboveIfNoMatch:NO lineComparesToY:NULL]];

    // Make range of first character of top line to last character of bottom line
    firstWordIndex = firstLine->wordRange.location;
    lastWordIndex = NSMaxRange(lastLine->wordRange);

    OBASSERT(lastWordIndex > firstWordIndex);
    
    return [self characterRangeForWordRange:NSMakeRange(firstWordIndex, lastWordIndex-firstWordIndex)];
}

// TODO: Move to foundation, after making OFStaticArray subclass of NSArray (not mutable, though)
- (unsigned int)lineIndexAtY:(float)y wantsLineAboveIfNoMatch:(BOOL)wantsLineAboveIfNoMatch lineComparesToY:(NSComparisonResult *)lineComparesToY;
{
    unsigned int lineCount;
    OHLine *line;
    NSComparisonResult result;
    unsigned int lowerLineIndex, upperLineIndex;

    lineCount = [lines count];
    if (lineCount == 0) {
        if (lineComparesToY)
            *lineComparesToY = NSOrderedAscending;
        return NSNotFound;
    }

    lowerLineIndex = 0;
    upperLineIndex = lineCount - 1;

    // Check first line
    result = [[lines objectAtIndex:lowerLineIndex] compareToY:y];
    if (lineComparesToY)
        *lineComparesToY = result;
    
    if (result != NSOrderedAscending)
        // Before or on first line
        return lowerLineIndex;

    // Check last line
    result = [[lines objectAtIndex:upperLineIndex] compareToY:y];
    if (lineComparesToY)
        *lineComparesToY = result;

    if (result != NSOrderedDescending)
        // After or on last line
        return upperLineIndex;

    // Somewhere between first and last line, find out where
    while (lowerLineIndex + 1 < upperLineIndex) {
        unsigned int middleLineIndex;

        middleLineIndex = (upperLineIndex + lowerLineIndex) / 2;
        line = [lines objectAtIndex:middleLineIndex];

        switch ([line compareToY:y]) {
            case NSOrderedDescending:
                upperLineIndex = middleLineIndex;
                break;
            case NSOrderedAscending:
                lowerLineIndex = middleLineIndex;
                break;
            case NSOrderedSame:
                if (lineComparesToY)
                    *lineComparesToY = NSOrderedSame;
                return middleLineIndex;
        }
    }

    if (wantsLineAboveIfNoMatch) {
        if (lineComparesToY)
            *lineComparesToY = NSOrderedAscending;
        return lowerLineIndex;
    } else {
        if (lineComparesToY)
            *lineComparesToY = NSOrderedDescending;
        return upperLineIndex;
    }
}


// AttributedText handling

- (void)mainThreadAppendAttributedString:(NSAttributedString *)attributedString;
{
    unsigned int paragraphCount;

    [textStorage appendAttributedString:attributedString];

    paragraphCount = [paragraphs count];
    [self scanParagraphsFromParagraphIndex:paragraphCount ? paragraphCount - 1 : 0];
    [self relayout]; // TODO: partial line layout
}


// Drawing

- (void)drawMarginalCellsInRect:(NSRect)drawRect;
{
    unsigned int marginalCellIndex, marginalCellCount;

    // Draw side-aligned cells
    marginalCellCount = [marginalCells count];
    for (marginalCellIndex = 0; marginalCellIndex < MIN(firstUnlaidMarginalCellIndex, marginalCellCount); marginalCellIndex++) {
        OHBasicCell * marginalCell;
        NSRect cellFrame;

        marginalCell = [marginalCells objectAtIndex:marginalCellIndex];

        cellFrame = [marginalCell cellFrame];
        if (NSMinY(cellFrame) >= NSMaxY(drawRect))
            break; // We are below drawRect, so no cells after us will be in it

        if (NSIntersectsRect(cellFrame, drawRect))
            [marginalCell drawRect:drawRect inHTMLView:self];
    }
}

- (void)drawBoxAroundSelectedAnchor;
{
    OHHTMLAnchor *selectedAnchor;
    NSColor *highlightColor;
    const NSRect *rectArray;
    unsigned int rectCount;

#if defined(RHAPSODY) && (OBOperatingSystemMajorVersion > 5)
    if (![[NSGraphicsContext currentContext] isDrawingToScreen])
#else
    if (![[NSDPSContext currentContext] isDrawingToScreen])
#endif
        return;
    
    selectedAnchor = [[self htmlPageView] selectedAnchor];
    if (!selectedAnchor || [selectedAnchor htmlView] != self)
        return;

    highlightColor = [[NSColor selectedTextBackgroundColor] blendedColorWithFraction:0.2 ofColor:[NSColor blackColor]];
    [highlightColor set];

    rectArray = [selectedAnchor rectArrayAndCount:&rectCount];
    while (rectCount--) {
        NSRect rect;

        rect = rectArray[rectCount];

        // In a fit of fanciness, I draw a three-pixel purple hightlight around the current anchor, with transparency (less in the middle).  The outer edge has missing corners.
        
        PSsetalpha(0.4);
        // Draw top and bottom outside
        PScompositerect(NSMinX(rect), NSMinY(rect) - 1, NSWidth(rect), 1, NSCompositeSourceOver);
        PScompositerect(NSMinX(rect), NSMaxY(rect), NSWidth(rect), 1, NSCompositeSourceOver);
        // Draw left and right outside
        PScompositerect(NSMinX(rect) - 1, NSMinY(rect), 1, NSHeight(rect), NSCompositeSourceOver);
        PScompositerect(NSMaxX(rect), NSMinY(rect), 1, NSHeight(rect), NSCompositeSourceOver);

        // Draw top and bottom inside
        PScompositerect(NSMinX(rect) + 1, NSMinY(rect) + 1, NSWidth(rect) - 2, 1, NSCompositeSourceOver);
        PScompositerect(NSMinX(rect) + 1, NSMaxY(rect) - 2, NSWidth(rect) - 2, 1, NSCompositeSourceOver);
        // Draw left and right inside
        PScompositerect(NSMinX(rect) + 1, NSMinY(rect) + 2, 1, NSHeight(rect) - 4, NSCompositeSourceOver);
        PScompositerect(NSMaxX(rect) - 2, NSMinY(rect) + 2, 1, NSHeight(rect) - 4, NSCompositeSourceOver);

        PSsetalpha(0.7);
        // Draw top and bottom middle
        PScompositerect(NSMinX(rect), NSMinY(rect), NSWidth(rect), 1, NSCompositeSourceOver);
        PScompositerect(NSMinX(rect), NSMaxY(rect) - 1, NSWidth(rect), 1, NSCompositeSourceOver);
        // Draw left and right middle
        PScompositerect(NSMinX(rect), NSMinY(rect) + 1, 1, NSHeight(rect) - 2, NSCompositeSourceOver);
        PScompositerect(NSMaxX(rect) - 1, NSMinY(rect) + 1, 1, NSHeight(rect) - 2, NSCompositeSourceOver);

    }
}

// Mouse handling

- (void)mouseDown:(NSEvent *)event onAnchor:(OHHTMLAnchor *)clickAnchor;
{
    OHHTMLPageView *htmlPageView;
    unsigned int modifierFlags;

    htmlPageView = [self htmlPageView];

    // Deselect text since we hit an anchor
    [self setSelectedRange:NSMakeRange(0,0)];

    if ([self shouldStartDragFromMouseDownEvent:event dragSlop:DRAG_SLOP finalEvent:NULL]) {
        [self dragAnchor:clickAnchor event:event];
        return;
    }
    
    // alt-click and control-click mean just select, don't follow
    modifierFlags = [event modifierFlags];
    if ((modifierFlags & NSAlternateKeyMask) || (modifierFlags & NSControlKeyMask)) {
        // Select the anchor
        [htmlPageView selectAnchor:clickAnchor];
    } else {
        // If the page's currently selected anchor isn't the click anchor, we should deselect it (since we're following another anchor).
        if (clickAnchor != [htmlPageView selectedAnchor])
            [htmlPageView selectAnchor:nil];

        // Follow the anchor
        [clickAnchor followFromEvent:event];
    }
}

- (OHBasicCell *)marginalCellAtPoint:(NSPoint)aPoint;
{
    unsigned int cellIndex;

    for (cellIndex = 0; cellIndex < firstUnlaidMarginalCellIndex; cellIndex++) {
        OHBasicCell * cell;
        NSRect marginalCellFrame;

        cell = [marginalCells objectAtIndex:cellIndex];
        marginalCellFrame = [cell cellFrame];
        if (NSMouseInRect(aPoint, marginalCellFrame, YES)) {
            return cell;
        } else {
            if (aPoint.y < NSMinY(marginalCellFrame))
                // We're done
                break;
        }
    }
    return nil;
}

- (OHHTMLLink *)linkAtPoint:(NSPoint)point;
{
    unsigned int glyphIndex;
    unsigned int hitCharacterIndex;
    OHBasicCell *cell;
    BOOL isReallyOnGlyph;

    cell = [self marginalCellAtPoint:point];
    if (cell)
        return [cell linkAtPoint:point];

    glyphIndex = [self glyphIndexForPoint:point fractionOfDistanceThroughGlyph:NULL isReallyOnGlyph:&isReallyOnGlyph];

    if (!isReallyOnGlyph)
        return nil;

    hitCharacterIndex = [layoutManager characterIndexForGlyphAtIndex:glyphIndex];
    cell = [self cellAtCharacterIndex:hitCharacterIndex];
    if (cell) {
        OHHTMLLink *link;

        link = [cell linkAtPoint:point];
        if (link)
            return link;
    }
    return [self linkAtCharacterIndex:hitCharacterIndex];
}

- (OHHTMLAnchor *)linkAtCharacterIndex:(unsigned int)characterIndex;
{
    OHHTMLAnchor *anchor;
    
    OBASSERT(characterIndex < [textStorage length]);
    if (characterIndex >= [textStorage length])
        return nil;

    anchor = [textStorage attribute:OHHTMLAnchorAttributeName atIndex:characterIndex effectiveRange:NULL];

    return [anchor address] ? anchor : nil;
}

- (OHBasicCell *)cellAtCharacterIndex:(unsigned int)characterIndex;
{
    OBASSERT(characterIndex < [textStorage length]);
    if (characterIndex >= [textStorage length])
        return nil;

    return (OHBasicCell *)[[textStorage attribute:NSAttachmentAttributeName atIndex:characterIndex effectiveRange:NULL] attachmentCell];
}

// Tracking rects

// NOTE: Return value is not autoreleased (but it is guaranteed to be nil if resetTracking is NO)
- (NSArray *)resetCursorRects:(BOOL)resetCursors andCalculateTrackingRects:(BOOL)resetTracking
{
    NSRect visibleRect;
    NSRange visibleRange, effectiveRange;
    unsigned int marginalCellIndex, marginalCellCount;
    NSMutableArray *newTrackingArray;
    OFZone *zone = [OFZone zoneForObject:self];

    visibleRect = [self visibleRect];
    visibleRange = [self maximalCharacterRangeForRect:visibleRect];
    effectiveRange = NSMakeRange(visibleRange.location, 0);

    if (resetTracking) {
        newTrackingArray = [[NSMutableArray allocWithZone:[zone nsZone]] initWithCapacity:[trackingRects count]];
    } else
        newTrackingArray = nil;

    // Run through storage, looking for anchors
    while (NSMaxRange(effectiveRange) < NSMaxRange(visibleRange)) {
        unsigned int characterIndex;
        OHHTMLAnchor *anchor;
        const NSRect *rectArray;
        unsigned int rectCount;

        characterIndex = NSMaxRange(effectiveRange);
        anchor = [textStorage attribute:OHHTMLAnchorAttributeName atIndex:characterIndex effectiveRange:&effectiveRange];
        if (anchor && [anchor address]) {
            rectArray = [anchor rectArrayAndCount:&rectCount];

            while (rectCount--) {
                NSRect visiblePortion;
                OHHTMLView_TrackingRect *trackingRect;

                if (!NSIntersectsRect(rectArray[rectCount], visibleRect))
                    continue;

                // For each rectangle, register a cursor rect ANDed with visibleRect
                visiblePortion = NSIntersectionRect(rectArray[rectCount], visibleRect);
                if (resetCursors)
                    [self addCursorRect:visiblePortion cursor:linkCursor];
                // Also record it in our array of tracking rects to be
                if (resetTracking) {
                    trackingRect = [[OHHTMLView_TrackingRect allocWithZone:[zone nsZone]] initWithRectangle:visiblePortion anchor:anchor];
                    [newTrackingArray addObject:trackingRect];
                    [trackingRect release];
                }
            }
        } else {
            OHBasicCell *cell;
            NSRect visiblePortion;
            OHHTMLView_TrackingRect *trackingRect;

            cell = [self cellAtCharacterIndex:characterIndex];
            if (!cell || ![cell wantsLinkCursor])
                continue;

            visiblePortion = NSIntersectionRect([cell contentFrame], visibleRect);
            if (resetCursors)
                [self addCursorRect:visiblePortion cursor:linkCursor];
            if (resetTracking && [cell respondsToSelector:@selector(eventHandlers)]) {
                trackingRect = [[OHHTMLView_TrackingRect allocWithZone:[zone nsZone]] initWithRectangle:visiblePortion imageCell:cell];
                [newTrackingArray addObject:trackingRect];
                [trackingRect release];
            }
        }
    }

    marginalCellCount = [marginalCells count];
    for (marginalCellIndex = 0; marginalCellIndex < marginalCellCount; marginalCellIndex++) {
        OHBasicCell *marginalCell;
        NSRect cellFrame;
        NSRect visiblePortion;
        OHHTMLView_TrackingRect *trackingRect;

        marginalCell = [marginalCells objectAtIndex:marginalCellIndex];

        cellFrame = [marginalCell cellFrame];
        if (NSMinY(cellFrame) >= NSMaxY(visibleRect))
            break; // We are below visibleRect, so no cells after us will be in it

        if (!NSIntersectsRect(cellFrame, visibleRect) || ![marginalCell link])
            continue;

        visiblePortion = NSIntersectionRect(cellFrame, visibleRect);
        if (resetCursors)
            [self addCursorRect:visiblePortion cursor:linkCursor];
        if (resetTracking && [marginalCell respondsToSelector:@selector(eventHandlers)]) {
            trackingRect = [[OHHTMLView_TrackingRect allocWithZone:[zone nsZone]] initWithRectangle:visiblePortion imageCell:marginalCell];
            [newTrackingArray addObject:trackingRect];
            [trackingRect release];
        }
    }

    // NOTE: Not autoreleased
    return newTrackingArray;
}

- (void)setTrackingRects:(NSArray *)newTrackingArray
{
    int rectCount, rectIndex;
    OHHTMLView_TrackingRect *rect;
    NSMutableSet *discardRects;
    NSEnumerator *discardEnumerator;
    int stat_before, stat_after, stat_new, stat_keep, stat_discard;
    BOOL wasSync;

    // We can be called in a subthread from -dealloc, but we should never have any tracking rects then (because we're not in a window).
    if ([trackingRects count] == 0 && (newTrackingArray == nil || [newTrackingArray count] == 0))
        return;

    ASSERT_IN_MAIN_THREAD(@"Tracking rects may only be updated in one thread");

    stat_before = [trackingRects count];
    
    discardRects = [trackingRects mutableCopy];

    // TODO: Find the equivalent of isSynchronized for OSX DP2 and later, if necessary
#if defined(RHAPSODY) && (OBOperatingSystemMajorVersion <= 5)
    wasSync = [[NSDPSContext currentContext] isSynchronized];
    [[NSDPSContext currentContext] setSynchronized:YES];
#endif

    
    // TODO: There is a minor bug here. If the mouse is inside any rectangle when the rects are recalculated (e.g. because of a relayout), then a mouse-exited event will be generated (above), even if the mouse is inside a rectangle when all is done. The mouse will have to exit and reenter in order to generate a mouse-entered event.

    stat_new = stat_keep = 0;
    if (newTrackingArray && [newTrackingArray count]) {
        rectCount = [newTrackingArray count];
        for(rectIndex = 0; rectIndex < rectCount; rectIndex++) {
            rect = [newTrackingArray objectAtIndex:rectIndex];

            if ([trackingRects containsObject:rect]) {
                // Keep this rectangle; remove it from the discard set
                [discardRects removeObject:rect];
                stat_keep++;
            } else {
                // This is a new rect.
                [rect setTag:[self addTrackingRect:[rect rectangle] owner:rect userData:self assumeInside:NO]];
            //    NSLog(@"  Created  id %d", [rect tag]);
                [trackingRects addObject:rect];
                stat_new++;
            }
        }
    }


    // Un-set any rectangles that are still in discardRects.
    stat_discard = 0;
    discardEnumerator = [discardRects objectEnumerator];
    while ((rect = [discardEnumerator nextObject])) {
        [trackingRects removeObject:rect];
  //      NSLog(@"  Clearing id %d", [rect tag]);
        [self removeTrackingRect:[rect tag]];
        if ([rect isInside])
            [rect mouseExited:nil]; // keeps things consistent
        stat_discard++;
    }
    [discardRects release];
    
    stat_after = [trackingRects count];

#if 0
    NSLog(@"setTrackingRects: before=%d after=%d; kept %d, discarded %d, %d new",
          stat_before, stat_after,
          stat_keep, stat_discard, stat_new);
#endif

    // TODO: Find the equivalent of isSynchronized for OSX DP2 and later, if necessary
#if defined(RHAPSODY) && (OBOperatingSystemMajorVersion <= 5)
    [[NSDPSContext currentContext] setSynchronized:wasSync];
#endif
    
}

- (void)resetAnchorTrackingRects;
{
    if ([self window] && [[self window] isKeyWindow]) {
        NSArray *newTrackingArray;

        newTrackingArray = [self resetCursorRects:NO andCalculateTrackingRects:YES];
        [self setTrackingRects:newTrackingArray];
        [newTrackingArray release];
    } else {
        [self setTrackingRects:nil];
    }
}

- (void)trackingRectRelevantNotification:(NSNotification *)note
{
    [self queueSelectorOnce:@selector(resetAnchorTrackingRects)];
}


// Updating the layout

- (void)relayoutIfNeeded;
{
    if (flags.needsRelayout)
        [self relayout];
}

// Calculating widths

- (OHWidthRange)widthRangeForContents;
{
    OHWidthRange maximumParagraphWidthRange = OHMakeWidthRange(0,0);
    unsigned int paragraphIndex, paragraphCount;
    unsigned int longestWordMinimumWidth = 0;
    unsigned int marginalCellIndex, marginalCellCount;

    [self relayoutIfNeeded];
    paragraphCount = [paragraphs count];
    for (paragraphIndex = 0; paragraphIndex < paragraphCount; paragraphIndex++) {
        OHParagraph *paragraph;
        OHWidthRange paragraphWidthRange = OHMakeWidthRange(0,0);
        unsigned int wordIndex, maxWordIndex;
        NSParagraphStyle *paragraphStyle;
        float headIndent, tailIndent;

        paragraph = [paragraphs objectAtIndex:paragraphIndex];

        paragraphStyle = [paragraph paragraphStyleInHTMLView:self];
        headIndent = MAX([paragraphStyle firstLineHeadIndent], [paragraphStyle headIndent]);
        // We ignore positive tailIndent, because that just limits how far past the headIndent words will be laid out, it does not enforce a minimum gutter on the right side the way a negative tail indent does.
        tailIndent = MAX(-[paragraphStyle tailIndent], 0.0);

        maxWordIndex = NSMaxRange(paragraph->wordRange);
        for (wordIndex = paragraph->wordRange.location; wordIndex < maxWordIndex; wordIndex++) {
            OHWord *word;
            OHWidthRange wordWidthRange;

            word = [words objectAtIndex:wordIndex];
            wordWidthRange = [word widthRangeInHTMLView:self];
            longestWordMinimumWidth = MAX(longestWordMinimumWidth, wordWidthRange.minimum + headIndent + tailIndent);
            paragraphWidthRange = OHAddWidthRanges(paragraphWidthRange, wordWidthRange);
        }

        paragraphWidthRange = OHAddWidthToWidthRange(headIndent + tailIndent, paragraphWidthRange);
        
        maximumParagraphWidthRange = OHMaximumWidthRange(maximumParagraphWidthRange, paragraphWidthRange);
    }

    // Special case for when our entire contents are a single &nbsp;--in that event (a common one for table cells), we're willing to size all the way down to zero even though &nbsp; theoretically requires more width.  (After all, it's just whitespace.)
    if (paragraphCount == 1) {
        NSString *textStorageString;

        textStorageString = [textStorage string];
        if ([textStorageString length] == 1 && [textStorageString characterAtIndex:0] == NO_BREAK_SPACE) {
            longestWordMinimumWidth = 0;
        }
    }

    // TODO: WJS: Move this up above paragraph calculations, (or during), make sure that we only add the minimum of the headIndent (or tailIndent) and the marginalCell's bounds.
    // Make sure we leave enough room for the widest marginal cell
    marginalCellCount = [marginalCells count];
    for (marginalCellIndex = 0; marginalCellIndex < marginalCellCount; marginalCellIndex++) {
        OHBasicCell *marginalCell;
        OHWidthRange cellWidthRange;

        marginalCell = [marginalCells objectAtIndex:marginalCellIndex];
        cellWidthRange = [marginalCell widthRange];
        if (cellWidthRange.minimum > longestWordMinimumWidth)
            longestWordMinimumWidth = cellWidthRange.minimum;
        maximumParagraphWidthRange = OHMaximumWidthRange(maximumParagraphWidthRange, cellWidthRange);
    }

    return OHMakeWidthRange(longestWordMinimumWidth, maximumParagraphWidthRange.maximum);
}

- (unsigned int)layIndexForCharacterIndex:(unsigned int)characterIndex;
{
    unsigned int layCount;
    unsigned int lowerLayIndex, upperLayIndex;

    layCount = [lays count];
    if (layCount == 0)
        return NSNotFound;

    lowerLayIndex = 0;
    switch ([(OHLay *)[lays objectAtIndex:lowerLayIndex] compareToCharacterIndex:characterIndex inHTMLView:self]) {
        case NSOrderedSame:
            return lowerLayIndex; // On first lay
        case NSOrderedDescending:
            [NSException raise:NSRangeException format:@"Character at index %d preceeds first lay", characterIndex];
            return NSNotFound; // Not reached
        case NSOrderedAscending:
            break; // continue
    }

    upperLayIndex = layCount - 1;
    switch ([(OHLay *)[lays objectAtIndex:upperLayIndex] compareToCharacterIndex:characterIndex inHTMLView:self]) {
        case NSOrderedSame:
            return upperLayIndex; // On last lay
        case NSOrderedDescending:
            break; // continue
        case NSOrderedAscending:
            return layCount;
    }

    while (lowerLayIndex + 1 < upperLayIndex) {
        unsigned int middleLayIndex;
        OHLay *lay;

        middleLayIndex = (upperLayIndex + lowerLayIndex) / 2;
        lay = [lays objectAtIndex:middleLayIndex];

        switch ([lay compareToCharacterIndex:characterIndex inHTMLView:self]) {
            case NSOrderedDescending:
                upperLayIndex = middleLayIndex;
                break;
            case NSOrderedAscending:
                lowerLayIndex = middleLayIndex;
                break;
            case NSOrderedSame:
                return middleLayIndex;
                break;
        }
    }
    [NSException raise:NSInternalInconsistencyException format:@"Failed to find lay containing character at index %d", characterIndex];
    return NSNotFound; // Not reached
}

- (NSRange)layRangeContainingCharacterRange:(NSRange)characterRange;
{
    unsigned int layCount;
    NSRange layRange;

    layCount = [lays count];
    if (layCount == 0)
        return NSMakeRange(NSNotFound, 0);

    layRange.location = [self layIndexForCharacterIndex:characterRange.location];
    switch (characterRange.length) {
        case 0:
            layRange.length = 0;
            break;
        case 1:
            // We'll be getting back the same lay index, might as well optimize this common case
            layRange.length = 1;
            break;
        default:
            layRange.length = [self layIndexForCharacterIndex:NSMaxRange(characterRange) - 1] - layRange.location + 1;
            break;
    }
    return layRange;
}


// Background image handling

- (void)setBackgroundOmniImage:(OIImage *)anOmniImage;
{
    if (backgroundOmniImage == anOmniImage)
	return;
    [backgroundOmniImage removeObserver:self];
    [backgroundOmniImage release];
    backgroundOmniImage = [anOmniImage retain];
    [backgroundOmniImage addObserver:self];
    [self mainThreadPerformSelectorOnce:@selector(backgroundImageDidUpdate)];
}


// Find helpers

- (void)findSelectionInSubviews;
{
    NSView *firstResponder;
    unsigned int indexToTest;
    NSString *string;
    unsigned int stringLength;
    NSRange attachmentRange;
    BOOL attachmentIsMarginal = NO;

    indexToTest = selectionHoldingCellIndexHint;
    selectionHoldingCellIndexHint = NSNotFound;
    
    firstResponder = (NSView *)[[self window] firstResponder];

    if ([[self window] firstResponder] == self)
        return; // This view contains the selection

    if (![firstResponder isKindOfClass:[NSView class]] || ![firstResponder isDescendantOf:self])
        return; // There is a selection in this window, but it isn't in one of our children, so bail


    if (indexToTest != NSNotFound) {
        // We think we might know which cell contains the selection, but we have to check
        if ([self checkIfFirstResponder:firstResponder isInSearchableContentViewAtCharacterIndex:indexToTest isMarginal:flags.selectionHoldingCellIsMarginal])
            return; // Yup, the selection is still there
    }

    // We aren't sure which cell contains the selection, so we run through our cells and ask each of 'em the slow way.  Note that this isn't efficient, but it ONLY happens after the user has manually changed the selection (after a find we update the hint index ourselves), so it's just not that common.
    string = [textStorage string];
    if (!string || (stringLength = [string length]) == 0)
        return;

    attachmentRange = [self findNextAttachmentAfterRange:NSMakeRange(0,0) backwards:NO isMarginal:&attachmentIsMarginal];
    while (attachmentRange.location != NSNotFound) {
        if ([self checkIfFirstResponder:firstResponder isInSearchableContentViewAtCharacterIndex:attachmentRange.location isMarginal:attachmentIsMarginal])
            return; // selectionHoldingCellIndexHint will have been set correctly
        attachmentRange = [self findNextAttachmentAfterRange:attachmentRange backwards:NO isMarginal:&attachmentIsMarginal];
    }
}

- (BOOL)checkIfFirstResponder:(NSView *)firstResponder isInSearchableContentViewAtCharacterIndex:(unsigned int)attachmentIndex isMarginal:(BOOL)isMarginal;
{
    NSView <OASearchableContent> *searchableContentView;

    searchableContentView = [self searchableContentViewAtCharacterIndex:attachmentIndex isMarginal:isMarginal];
    if (searchableContentView && [firstResponder isDescendantOf:searchableContentView]) {
        selectionHoldingCellIndexHint = attachmentIndex;
        flags.selectionHoldingCellIsMarginal = isMarginal;
        return YES;
    }
    return NO;
}

- (NSView <OASearchableContent> *)searchableContentViewAtCharacterIndex:(unsigned int)attachmentIndex isMarginal:(BOOL)isMarginal;
{
    OHBasicCell *basicCell;
    
    if (isMarginal) {
        unsigned int marginalCellIndex;
        
        marginalCellIndex = [self indexOfMarginalCellAroundCharacterIndex:attachmentIndex searchDirection:NSOrderedSame];
        if (marginalCellIndex == NSNotFound)
            return nil;

        basicCell = [marginalCells objectAtIndex:marginalCellIndex];
    } else {
        if (attachmentIndex >= [textStorage length])
            return nil;

        basicCell = [(OHBasicCell *)[textStorage attribute:NSAttachmentAttributeName atIndex:attachmentIndex effectiveRange:NULL] attachmentCell];
    }
    return [basicCell searchableContentView];
}

- (BOOL)findString:(NSString *)textPattern ignoreCase:(BOOL)ignoreCase backwards:(BOOL)backwards ignoreSelection:(BOOL)ignoreSelection inAttachmentAtIndex:(unsigned int)attachmentIndex isMarginal:(BOOL)isMarginal;
{
    NSView <OASearchableContent> *searchableContentView;

    searchableContentView = [self searchableContentViewAtCharacterIndex:attachmentIndex isMarginal:isMarginal];
    if (!searchableContentView)
        return NO;

    // We can search this attachment
    if ([searchableContentView findString:textPattern ignoreCase:ignoreCase backwards:backwards ignoreSelection:ignoreSelection]) {
        selectionHoldingCellIndexHint = attachmentIndex;
        flags.selectionHoldingCellIsMarginal = isMarginal;
        return YES;
    }
    return NO;
}

- (NSRange)findNextAttachmentAfterRange:(NSRange)previousAttachmentRange backwards:(BOOL)backwards isMarginal:(BOOL *)isMarginal;
{
    NSRange attachmentRange;
    BOOL previousAttachmentWasMarginal = *isMarginal;
    NSString *string;
    unsigned int nextMarginalCellCharacterIndex;
    
    string = [textStorage string];
    
    if (previousAttachmentWasMarginal
        && ((backwards && (previousAttachmentRange.location == 0))
            || (!backwards && (NSMaxRange(previousAttachmentRange) >= [string length]))))
        return NSMakeRange(NSNotFound, 0);
    
    attachmentRange = [string findString:attachmentString selectedRange:previousAttachmentRange options:(backwards ? NSBackwardsSearch : 0) wrap:NO];

    if (backwards) {
        unsigned int characterIndex;

        if (previousAttachmentWasMarginal)
            characterIndex = previousAttachmentRange.location-1;
        else
            characterIndex = previousAttachmentRange.location;
            
        nextMarginalCellCharacterIndex = [self characterIndexOfMarginalCellAroundCharacterIndex:characterIndex searchDirection:NSOrderedDescending];
        if (nextMarginalCellCharacterIndex != NSNotFound) {
            if (attachmentRange.location == NSNotFound)
                *isMarginal = YES;
            else
                *isMarginal = nextMarginalCellCharacterIndex > attachmentRange.location;
            if (*isMarginal)
                attachmentRange = NSMakeRange(nextMarginalCellCharacterIndex, 0);
        }
    } else {
        unsigned int characterIndex;

        if (previousAttachmentWasMarginal)
            characterIndex = previousAttachmentRange.location+1;
        else
            characterIndex = NSMaxRange(previousAttachmentRange);
            
        nextMarginalCellCharacterIndex = [self characterIndexOfMarginalCellAroundCharacterIndex:characterIndex searchDirection:NSOrderedAscending];
        if (nextMarginalCellCharacterIndex != NSNotFound) {
            if (attachmentRange.location == NSNotFound)
                *isMarginal = YES;
            else
                *isMarginal = nextMarginalCellCharacterIndex <= attachmentRange.location;
            if (*isMarginal)
                attachmentRange = NSMakeRange(nextMarginalCellCharacterIndex, 0);
        }
    }

    return attachmentRange;
}

- (unsigned int)characterIndexOfMarginalCellAroundCharacterIndex:(unsigned int)characterIndex searchDirection:(NSComparisonResult)searchDirection;
{
    unsigned int marginalCellIndex;

    marginalCellIndex = [self indexOfMarginalCellAroundCharacterIndex:characterIndex searchDirection:searchDirection];
    if (marginalCellIndex == NSNotFound)
        return NSNotFound;

    return [(OHBasicCell *)[marginalCells objectAtIndex:marginalCellIndex] characterPosition];
}
    
- (unsigned int)indexOfMarginalCellAroundCharacterIndex:(unsigned int)characterIndex searchDirection:(NSComparisonResult)searchDirection;
{
    unsigned int cellCount, cellIndex;

    cellCount = [marginalCells count];
    if (cellCount == 0)
        return NSNotFound;
    
    cellIndex = (searchDirection == NSOrderedDescending) ? cellCount-1 : 0;
    while (1) {
        OHBasicCell *basicCell;
        unsigned int cellCharacterIndex;

        basicCell = [marginalCells objectAtIndex:cellIndex];
        cellCharacterIndex = [basicCell characterPosition];
        switch (searchDirection) {
            case NSOrderedSame:
                if (cellCharacterIndex == characterIndex)
                    return cellIndex;
                else if (cellCharacterIndex > characterIndex)
                    return NSNotFound;
                    break;
            case NSOrderedDescending:
                if (cellCharacterIndex <= characterIndex)
                    return cellIndex;
                break;
            case NSOrderedAscending:
                if (cellCharacterIndex >= characterIndex)
                    return cellIndex;
                break;
        }

        if (searchDirection == NSOrderedDescending) {
            if (cellIndex == 0)
                return NSNotFound;
            cellIndex--;
        } else {
            if (cellIndex == cellCount-1)
                return NSNotFound;
            cellIndex++;
        }
    }
    return NSNotFound; // Not reached
}

- (void)nullifyHTMLOwnerBackpointersInCellsInHTMLStorage:(NSTextStorage *)htmlStorage;
{
    OFStringScanner *stringScanner;

    // Nullify backpointers for inline cells
    stringScanner = [[OFStringScanner alloc] initWithString:[htmlStorage string]];
    while (scannerHasData(stringScanner)) {
        scannerScanUpToCharacterInCSBitmap(stringScanner, attachmentCSBitmap);
        while (scannerPeekCharacter(stringScanner) == NSAttachmentCharacter) {
            NSTextAttachment *attachment;
            OHBasicCell *attachmentCell;

            attachment = [htmlStorage attribute:NSAttachmentAttributeName atIndex:scannerScanLocation(stringScanner) effectiveRange:NULL];
            attachmentCell = (OHBasicCell *)[attachment attachmentCell];
            [attachmentCell nullifyHTMLOwner];
            scannerSkipPeekedCharacter(stringScanner);
        }
    }
    [stringScanner release];

    // Nullify backpointers for marginal cells
    if (marginalCells != nil) {
        unsigned int marginalCellIndex, marginalCellCount;

        marginalCellCount = [marginalCells count];
        for (marginalCellIndex = 0; marginalCellIndex < firstUnlaidMarginalCellIndex; marginalCellIndex++) {
            OHBasicCell *marginalCell;

            marginalCell = [marginalCells objectAtIndex:marginalCellIndex];
            [marginalCell nullifyHTMLOwner];
        }
    }
}


// Debugging

- (NSMutableDictionary *)debugDictionary;
{
    NSMutableDictionary *debugDictionary;

    debugDictionary = [super debugDictionary];

    if (flags.cachedWidthRangeIsValid)
        [debugDictionary setObject:OHStringFromWidthRange(cachedWidthRange) forKey:@"cachedWidthRange"];
    if (paragraphs)
        [debugDictionary setObject:paragraphs forKey:@"paragraphs"];
    if (words)
        [debugDictionary setObject:words forKey:@"words"];
    if (lines)
        [debugDictionary setObject:lines forKey:@"lines"];
    if (lays)
        [debugDictionary setObject:lays forKey:@"lays"];
    if (marginalCells)
        [debugDictionary setObject:marginalCells forKey:@"marginalCells"];
    if (clearBreakObjects)
        [debugDictionary setObject:clearBreakObjects forKey:@"clearBreakObjects"];
    if (backgroundOmniImage)
        [debugDictionary setObject:backgroundOmniImage forKey:@"backgroundOmniImage"];

    return debugDictionary;
}

@end
