KWQListBox.mm   [plain text]


/*
 * Copyright (C) 2004 Apple Computer, Inc.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE COMPUTER, INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 */

#import "KWQListBox.h"

#import "KWQAssertions.h"
#import "KWQExceptions.h"
#import "KWQKHTMLPart.h"
#import "KWQView.h"
#import "WebCoreBridge.h"
#import "WebCoreScrollView.h"
#import "WebCoreTextRenderer.h"
#import "WebCoreTextRendererFactory.h"

#import "render_form.h"

using khtml::RenderWidget;
using khtml::RenderLayer;

@interface NSTableView (KWQListBoxKnowsAppKitSecrets)
- (NSCell *)_accessibilityTableCell:(int)row tableColumn:(NSTableColumn *)tableColumn;
@end

const int minLines = 4; /* ensures we have a scroll bar */
const float bottomMargin = 1;
const float leftMargin = 2;
const float rightMargin = 2;

@interface KWQListBoxScrollView : WebCoreScrollView <KWQWidgetHolder>
{
    QListBox *_box;
}
@end

@interface KWQTableView : NSTableView <KWQWidgetHolder>
{
@public
    QListBox *_box;
    BOOL processingMouseEvent;
    BOOL clickedDuringMouseEvent;
    BOOL inNextValidKeyView;
    NSWritingDirection _direction;
    BOOL isSystemFont;
}
- (id)initWithListBox:(QListBox *)b;
- (void)detach;
- (void)_KWQ_setKeyboardFocusRingNeedsDisplay;
- (QWidget *)widget;
- (void)setBaseWritingDirection:(NSWritingDirection)direction;
- (NSWritingDirection)baseWritingDirection;
- (void)fontChanged;
@end

static id <WebCoreTextRenderer> itemScreenRenderer;
static id <WebCoreTextRenderer> itemPrinterRenderer;
static id <WebCoreTextRenderer> groupLabelScreenRenderer;
static id <WebCoreTextRenderer> groupLabelPrinterRenderer;

static NSFont *itemFont()
{
    static NSFont *font = [[NSFont systemFontOfSize:[NSFont smallSystemFontSize]] retain];
    return font;
}

static NSFont *groupLabelFont()
{
    static NSFont *font = [[NSFont boldSystemFontOfSize:[NSFont smallSystemFontSize]] retain];
    return font;
}

static id <WebCoreTextRenderer> itemTextRenderer()
{
    if ([NSGraphicsContext currentContextDrawingToScreen]) {
        if (itemScreenRenderer == nil) {
            itemScreenRenderer = [[[WebCoreTextRendererFactory sharedFactory]
                rendererWithFont:itemFont() usingPrinterFont:NO] retain];
        }
        return itemScreenRenderer;
    } else {
        if (itemPrinterRenderer == nil) {
            itemPrinterRenderer = [[[WebCoreTextRendererFactory sharedFactory]
                rendererWithFont:itemFont() usingPrinterFont:YES] retain];
        }
        return itemPrinterRenderer;
    }
}

static id <WebCoreTextRenderer> groupLabelTextRenderer()
{
    if ([NSGraphicsContext currentContextDrawingToScreen]) {
        if (groupLabelScreenRenderer == nil) {
            groupLabelScreenRenderer = [[[WebCoreTextRendererFactory sharedFactory]
                rendererWithFont:groupLabelFont() usingPrinterFont:NO] retain];
        }
        return groupLabelScreenRenderer;
    } else {
        if (groupLabelPrinterRenderer == nil) {
            groupLabelPrinterRenderer = [[[WebCoreTextRendererFactory sharedFactory]
                rendererWithFont:groupLabelFont() usingPrinterFont:YES] retain];
        }
        return groupLabelPrinterRenderer;
    }
}

QListBox::QListBox(QWidget *parent)
    : QScrollView(parent)
    , _changingSelection(false)
    , _enabled(true)
    , _widthGood(false)
    , _clicked(this, SIGNAL(clicked(QListBoxItem *)))
    , _selectionChanged(this, SIGNAL(selectionChanged()))
{
    KWQ_BLOCK_EXCEPTIONS;

    NSScrollView *scrollView = [[KWQListBoxScrollView alloc] initWithListBox:this];
    setView(scrollView);
    [scrollView release];
    
    [scrollView setBorderType:NSBezelBorder];
    [scrollView setHasVerticalScroller:YES];
    [[scrollView verticalScroller] setControlSize:NSSmallControlSize];

    // In WebHTMLView, we set a clip. This is not typical to do in an
    // NSView, and while correct for any one invocation of drawRect:,
    // it causes some bad problems if that clip is cached between calls.
    // The cached graphics state, which clip views keep around, does
    // cache the clip in this undesirable way. Consequently, we want to 
    // release the GState for all clip views for all views contained in 
    // a WebHTMLView. Here we do it for list boxes used in forms.
    // See these bugs for more information:
    // <rdar://problem/3226083>: REGRESSION (Panther): white box overlaying select lists at nvidia.com drivers page
    [[scrollView contentView] releaseGState];
    
    KWQTableView *tableView = [[KWQTableView alloc] initWithListBox:this];
    [scrollView setDocumentView:tableView];
    [tableView release];
    [scrollView setVerticalLineScroll:[tableView rowHeight]];
        
    KWQ_UNBLOCK_EXCEPTIONS;
}

QListBox::~QListBox()
{
    NSScrollView *scrollView = static_cast<NSScrollView *>(getView());
    
    KWQ_BLOCK_EXCEPTIONS;
    KWQTableView *tableView = [scrollView documentView];
    [tableView detach];
    KWQ_UNBLOCK_EXCEPTIONS;
}

void QListBox::clear()
{
    _items.clear();
    _widthGood = false;
}

void QListBox::setSelectionMode(SelectionMode mode)
{
    NSScrollView *scrollView = static_cast<NSScrollView *>(getView());

    KWQ_BLOCK_EXCEPTIONS;
    NSTableView *tableView = [scrollView documentView];
    [tableView setAllowsMultipleSelection:mode != Single];
    KWQ_UNBLOCK_EXCEPTIONS;
}

void QListBox::appendItem(const QString &text, KWQListBoxItemType type)
{
    _items.append(KWQListBoxItem(text, type));
    _widthGood = false;
}

void QListBox::doneAppendingItems()
{
    KWQ_BLOCK_EXCEPTIONS;

    NSScrollView *scrollView = static_cast<NSScrollView *>(getView());
    NSTableView *tableView = [scrollView documentView];
    [tableView reloadData];

    KWQ_UNBLOCK_EXCEPTIONS;
}

void QListBox::setSelected(int index, bool selectIt)
{
    ASSERT(index >= 0);

    KWQ_BLOCK_EXCEPTIONS;

    NSScrollView *scrollView = static_cast<NSScrollView *>(getView());
    NSTableView *tableView = [scrollView documentView];
    _changingSelection = true;
    if (selectIt) {
        [tableView selectRow:index byExtendingSelection:[tableView allowsMultipleSelection]];
        [tableView scrollRowToVisible:index];
    } else {
        [tableView deselectRow:index];
    }

    KWQ_UNBLOCK_EXCEPTIONS;

    _changingSelection = false;
}

bool QListBox::isSelected(int index) const
{
    ASSERT(index >= 0);

    KWQ_BLOCK_EXCEPTIONS;

    NSScrollView *scrollView = static_cast<NSScrollView *>(getView());
    NSTableView *tableView = [scrollView documentView];
    return [tableView isRowSelected:index]; 

    KWQ_UNBLOCK_EXCEPTIONS;

    return false;
}

void QListBox::setEnabled(bool enabled)
{
    if (enabled != _enabled) {
        // You would think this would work, but not until AppKit bug 2177792 is fixed.
        //KWQ_BLOCK_EXCEPTIONS;
        //NSTableView *tableView = [(NSScrollView *)getView() documentView];
        //[tableView setEnabled:enabled];
        //KWQ_UNBLOCK_EXCEPTIONS;

        _enabled = enabled;

        NSScrollView *scrollView = static_cast<NSScrollView *>(getView());
        NSTableView *tableView = [scrollView documentView];
        [tableView reloadData];
    }
}

bool QListBox::isEnabled()
{
    return _enabled;
}

QSize QListBox::sizeForNumberOfLines(int lines) const
{
    NSSize size = {0,0};

    KWQ_BLOCK_EXCEPTIONS;

    NSScrollView *scrollView = static_cast<NSScrollView *>(getView());
    KWQTableView *tableView = [scrollView documentView];
    
    if (!_widthGood) {
        float width = 0;
        QValueListConstIterator<KWQListBoxItem> i = const_cast<const QValueList<KWQListBoxItem> &>(_items).begin();
        QValueListConstIterator<KWQListBoxItem> e = const_cast<const QValueList<KWQListBoxItem> &>(_items).end();
        if (i != e) {
            WebCoreTextStyle style;
            WebCoreInitializeEmptyTextStyle(&style);
            style.rtl = [tableView baseWritingDirection] == NSWritingDirectionRightToLeft;
            style.applyRunRounding = NO;
            style.applyWordRounding = NO;
            
            id <WebCoreTextRenderer> renderer;
            id <WebCoreTextRenderer> groupLabelRenderer;
            
            if (tableView->isSystemFont) {        
                renderer = itemTextRenderer();
                groupLabelRenderer = groupLabelTextRenderer();
            }
            else {
                NSFont *f = font().getNSFont();
                QFont b = font();
                b.setWeight(QFont::Bold);
                NSFont *boldFont = b.getNSFont();
            
            
                renderer = [[WebCoreTextRendererFactory sharedFactory]
                    rendererWithFont:f usingPrinterFont:![NSGraphicsContext currentContextDrawingToScreen]];

                groupLabelRenderer = [[WebCoreTextRendererFactory sharedFactory]
                    rendererWithFont:boldFont usingPrinterFont:![NSGraphicsContext currentContextDrawingToScreen]];
            }
            
            do {
                const QString &s = (*i).string;

                WebCoreTextRun run;
                int length = s.length();
                WebCoreInitializeTextRun(&run, reinterpret_cast<const UniChar *>(s.unicode()), length, 0, length);

                float textWidth = [(((*i).type == KWQListBoxGroupLabel) ? groupLabelRenderer : renderer) floatWidthForRun:&run style:&style widths:0];
                width = kMax(width, textWidth);
                
                ++i;
            
            } while (i != e);
        }
        _width = ceilf(width);
        _widthGood = true;
    }
    
    NSSize tempSize = { _width, [tableView rowHeight] * MAX(minLines, lines) }; // workaround for 4213314
    size = [NSScrollView frameSizeForContentSize:tempSize
        hasHorizontalScroller:NO hasVerticalScroller:YES borderType:NSBezelBorder];
    size.width += [NSScroller scrollerWidthForControlSize:NSSmallControlSize] - [NSScroller scrollerWidth] + leftMargin + rightMargin;

    KWQ_UNBLOCK_EXCEPTIONS;

    return QSize(size);
}

QWidget::FocusPolicy QListBox::focusPolicy() const
{
    FocusPolicy policy = QWidget::focusPolicy();
    return policy == TabFocus ? StrongFocus : policy;
}

bool QListBox::checksDescendantsForFocus() const
{
    return true;
}

void QListBox::setWritingDirection(QPainter::TextDirection d)
{
    KWQ_BLOCK_EXCEPTIONS;

    NSScrollView *scrollView = static_cast<NSScrollView *>(getView());
    KWQTableView *tableView = [scrollView documentView];
    NSWritingDirection direction = d == QPainter::RTL ? NSWritingDirectionRightToLeft : NSWritingDirectionLeftToRight;
    if ([tableView baseWritingDirection] != direction) {
        [tableView setBaseWritingDirection:direction];
        [tableView reloadData];
    }

    KWQ_UNBLOCK_EXCEPTIONS;
}

void QListBox::clearCachedTextRenderers()
{
    [itemScreenRenderer release];
    itemScreenRenderer = nil;

    [itemPrinterRenderer release];
    itemPrinterRenderer = nil;

    [groupLabelScreenRenderer release];
    groupLabelScreenRenderer = nil;

    [groupLabelPrinterRenderer release];
    groupLabelPrinterRenderer = nil;
}

void QListBox::setFont(const QFont &font)
{
    QWidget::setFont(font);

    NSScrollView *scrollView = static_cast<NSScrollView *>(getView());
    KWQTableView *tableView = [scrollView documentView];
    [tableView fontChanged];
}

@implementation KWQListBoxScrollView

- (id)initWithListBox:(QListBox *)b
{
    if (!(self = [super init]))
        return nil;

    _box = b;
    return self;
}

- (QWidget *)widget
{
    return _box;
}

- (void)setFrameSize:(NSSize)size
{
    [super setFrameSize:size];
    NSTableColumn *column = [[[self documentView] tableColumns] objectAtIndex:0];
    [column setWidth:[self contentSize].width];
    [column setMinWidth:[self contentSize].width];
    [column setMaxWidth:[self contentSize].width];
}

- (BOOL)becomeFirstResponder
{
    KWQTableView *documentView = [self documentView];
    QWidget *widget = [documentView widget];
    [KWQKHTMLPart::bridgeForWidget(widget) makeFirstResponder:documentView];
    return YES;
}

@end

@implementation KWQTableView

- (id)initWithListBox:(QListBox *)b
{
    [super init];

    _box = b;

    NSTableColumn *column = [[NSTableColumn alloc] initWithIdentifier:nil];

    [column setEditable:NO];

    [self addTableColumn:column];

    [column release];
    
    [self setAllowsMultipleSelection:NO];
    [self setHeaderView:nil];
    [self setIntercellSpacing:NSMakeSize(0, 0)];
    
    [self setDataSource:self];
    [self setDelegate:self];

    return self;
}

- (void)detach
{
    _box = 0;
    [self setDelegate:nil];
    [self setDataSource:nil];
}

- (void)mouseDown:(NSEvent *)event
{
    if (!_box) {
        [super mouseDown:event];
        return;
    }

    processingMouseEvent = YES;
    NSView *outerView = [_box->getOuterView() retain];
    QWidget::beforeMouseDown(outerView);
    [super mouseDown:event];
    QWidget::afterMouseDown(outerView);
    [outerView release];
    processingMouseEvent = NO;

    if (clickedDuringMouseEvent) {
	clickedDuringMouseEvent = false;
    } else if (_box) {
	_box->sendConsumedMouseUp();
    }
}

- (void)keyDown:(NSEvent *)event
{
    if (!_box)  {
        return;
    }
    WebCoreBridge *bridge = KWQKHTMLPart::bridgeForWidget(_box);
    if (![bridge interceptKeyEvent:event toView:self]) {
	[super keyDown:event];
    }
}

- (void)keyUp:(NSEvent *)event
{
    if (!_box)  {
        return;
    }
    WebCoreBridge *bridge = KWQKHTMLPart::bridgeForWidget(_box);
    if (![bridge interceptKeyEvent:event toView:self]) {
	[super keyUp:event];
    }
}

- (BOOL)becomeFirstResponder
{
    if (!_box) {
        return NO;
    }

    BOOL become = [super becomeFirstResponder];
    
    if (become) {
        if (_box && !KWQKHTMLPart::currentEventIsMouseDownInWidget(_box)) {
            RenderWidget *widget = const_cast<RenderWidget *> (static_cast<const RenderWidget *>(_box->eventFilterObject()));
            RenderLayer *layer = widget->enclosingLayer();
            if (layer)
                layer->scrollRectToVisible(widget->absoluteBoundingBoxRect());
        }        
        [self _KWQ_setKeyboardFocusRingNeedsDisplay];

        if (_box) {
            QFocusEvent event(QEvent::FocusIn);
            if (_box->eventFilterObject())
                const_cast<QObject *>(_box->eventFilterObject())->eventFilter(_box, &event);
        }
    }

    return become;
}

- (BOOL)resignFirstResponder
{
    BOOL resign = [super resignFirstResponder];
    if (resign && _box) {
        QFocusEvent event(QEvent::FocusOut);
        if (_box->eventFilterObject())
            const_cast<QObject *>(_box->eventFilterObject())->eventFilter(_box, &event);
    }
    return resign;
}

- (BOOL)canBecomeKeyView
{
    // Simplified method from NSView; overridden to replace NSView's way of checking
    // for full keyboard access with ours.
    return ([self window] != nil) && ![self isHiddenOrHasHiddenAncestor] && [self acceptsFirstResponder];
}

- (NSView *)nextKeyView
{
    return _box && inNextValidKeyView
        ? KWQKHTMLPart::nextKeyViewForWidget(_box, KWQSelectingNext)
        : [super nextKeyView];
}

- (NSView *)previousKeyView
{
    return _box && inNextValidKeyView
        ? KWQKHTMLPart::nextKeyViewForWidget(_box, KWQSelectingPrevious)
        : [super previousKeyView];
}

- (NSView *)nextValidKeyView
{
    inNextValidKeyView = YES;
    NSView *view = [super nextValidKeyView];
    inNextValidKeyView = NO;
    return view;
}

- (NSView *)previousValidKeyView
{
    inNextValidKeyView = YES;
    NSView *view = [super previousValidKeyView];
    inNextValidKeyView = NO;
    return view;
}

- (int)numberOfRowsInTableView:(NSTableView *)tableView
{
    return _box ? _box->count() : 0;
}

- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)column row:(int)row
{
    return nil;
}

- (void)tableViewSelectionDidChange:(NSNotification *)notification
{
    if (_box) {
        _box->selectionChanged();
    }
    if (_box && !_box->changingSelection()) {
	if (processingMouseEvent) {
	    clickedDuringMouseEvent = true;
	    _box->sendConsumedMouseUp();
	}
        if (_box) {
            _box->clicked();
        }
    }
}

- (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(int)row
{
    return _box && _box->itemAtIndex(row).type == KWQListBoxOption;
}

- (BOOL)selectionShouldChangeInTableView:(NSTableView *)aTableView
{
    return _box && _box->isEnabled();
}

- (void)drawRow:(int)row clipRect:(NSRect)clipRect
{
    if (!_box) {
        return;
    }

    const KWQListBoxItem &item = _box->itemAtIndex(row);

    NSColor *color;
    if (_box->isEnabled()) {
        if ([self isRowSelected:row] && [[self window] firstResponder] == self && ([[self window] isKeyWindow] || ![[self window] canBecomeKeyWindow])) {
            color = [NSColor alternateSelectedControlTextColor];
        } else {
            color = [NSColor controlTextColor];
        }
    } else {
        color = [NSColor disabledControlTextColor];
    }

    bool RTL = _direction == NSWritingDirectionRightToLeft;

    NSFont *font = _box->font().getNSFont();
    
    id <WebCoreTextRenderer> renderer;
    if (isSystemFont) {
        renderer = (item.type == KWQListBoxGroupLabel) ? groupLabelTextRenderer() : itemTextRenderer();
    } else {
        if (item.type == KWQListBoxGroupLabel) {
            QFont boldFont = _box->font();
            boldFont.setWeight(QFont::Bold);
            font = boldFont.getNSFont();
        }
        renderer = [[WebCoreTextRendererFactory sharedFactory]
            rendererWithFont:font usingPrinterFont:![NSGraphicsContext currentContextDrawingToScreen]];
    }
   
    WebCoreTextStyle style;
    WebCoreInitializeEmptyTextStyle(&style);
    style.rtl = RTL;
    style.applyRunRounding = NO;
    style.applyWordRounding = NO;
    style.textColor = color;

    WebCoreTextRun run;
    int length = item.string.length();
    WebCoreInitializeTextRun(&run, reinterpret_cast<const UniChar *>(item.string.unicode()), length, 0, length);

    NSRect cellRect = [self frameOfCellAtColumn:0 row:row];
    NSPoint point;
    if (!RTL) {
        point.x = NSMinX(cellRect) + leftMargin;
    } else {
        point.x = NSMaxX(cellRect) - rightMargin - [renderer floatWidthForRun:&run style:&style widths:0];
    }
    point.y = NSMaxY(cellRect) + [font descender] - bottomMargin;

    WebCoreTextGeometry geometry;
    WebCoreInitializeEmptyTextGeometry(&geometry);
    geometry.point = point;
    
    [renderer drawRun:&run style:&style geometry:&geometry];
}

- (void)_KWQ_setKeyboardFocusRingNeedsDisplay
{
    [self setKeyboardFocusRingNeedsDisplayInRect:[self bounds]];
}

- (QWidget *)widget
{
    return _box;
}

- (void)setBaseWritingDirection:(NSWritingDirection)direction
{
    _direction = direction;
}

- (NSWritingDirection)baseWritingDirection
{
    return _direction;
}

- (NSCell *)_accessibilityTableCell:(int)row tableColumn:(NSTableColumn *)tableColumn
{
    NSCell *cell = [super _accessibilityTableCell:row tableColumn:tableColumn];
    if (_box) {
        [cell setStringValue:_box->itemAtIndex(row).string.getNSString()];
    }
    return cell;
}

- (void)fontChanged
{
    NSFont *font = _box->font().getNSFont();
    isSystemFont = [[font fontName] isEqualToString:[itemFont() fontName]] && ([font pointSize] == [itemFont() pointSize]);
    
    [self setRowHeight:ceilf([font ascender] - [font descender] + bottomMargin)];
    [self setNeedsDisplay:YES];
}

@end