KWQListBox.mm   [plain text]


/*
 * Copyright (C) 2003 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 "KWQNSViewExtras.h"
#import "KWQView.h"
#import "WebCoreBridge.h"
#import "WebCoreScrollView.h"

#define MIN_LINES 4 /* ensures we have a scroll bar */

@interface KWQListBoxScrollView : WebCoreScrollView
@end

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

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 NSParagraphStyle *paragraphStyle(NSWritingDirection direction)
{
    static NSParagraphStyle *leftStyle;
    static NSParagraphStyle *rightStyle;
    NSParagraphStyle **style = direction == NSWritingDirectionRightToLeft ? &rightStyle : &leftStyle;
    if (*style == nil) {
        NSMutableParagraphStyle *mutableStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
        [mutableStyle setBaseWritingDirection:direction];
        *style = [mutableStyle copy];
        [mutableStyle release];
    }
    return *style;
}

static NSDictionary *stringAttributes(NSWritingDirection direction, bool isGroupLabel)
{
    static NSDictionary *attributeGlobals[4];
    NSDictionary **attributes = &attributeGlobals[(direction == NSWritingDirectionRightToLeft ? 0 : 1) + (isGroupLabel ? 0 : 2)];
    if (*attributes == nil) {
        *attributes = [[NSDictionary dictionaryWithObjectsAndKeys:
            isGroupLabel ? groupLabelFont() : itemFont(), NSFontAttributeName,
            paragraphStyle(direction), NSParagraphStyleAttributeName,
            nil] retain];
    }
    return *attributes;
}

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

    _items = [[NSMutableArray alloc] init];
    NSScrollView *scrollView = [[KWQListBoxScrollView alloc] init];
    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 items:_items];
    [scrollView setDocumentView:tableView];
    [tableView release];
    [scrollView setVerticalLineScroll:[tableView rowHeight]];
    
    KWQ_UNBLOCK_EXCEPTIONS;
}

QListBox::~QListBox()
{
    NSScrollView *scrollView = getView();
    
    KWQ_BLOCK_EXCEPTIONS;
    NSTableView *tableView = [scrollView documentView];
    [tableView setDelegate:nil];
    [tableView setDataSource:nil];
    [_items release];
    KWQ_UNBLOCK_EXCEPTIONS;
}

uint QListBox::count() const
{
    KWQ_BLOCK_EXCEPTIONS;
    return [_items count];
    KWQ_UNBLOCK_EXCEPTIONS;

    return 0;
}

void QListBox::clear()
{
    KWQ_BLOCK_EXCEPTIONS;
    [_items removeAllObjects];
    if (!_insertingItems) {
        NSScrollView *scrollView = getView();
        NSTableView *tableView = [scrollView documentView];
        [tableView reloadData];
    }
    KWQ_UNBLOCK_EXCEPTIONS;
    _widthGood = NO;
}

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

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

void QListBox::insertItem(const QString &text, int index, bool isLabel)
{
    ASSERT(index >= 0);

    KWQ_BLOCK_EXCEPTIONS;

    NSScrollView *scrollView = getView();
    KWQTableView *tableView = [scrollView documentView];

    NSAttributedString *s = [[NSAttributedString alloc] initWithString:text.getNSString()
        attributes:stringAttributes([tableView baseWritingDirection], isLabel)];

    int c = count();
    if (index >= c) {
        [_items addObject:s];
    } else {
        [_items replaceObjectAtIndex:index withObject:s];
    }
 
    [s release];

    if (!_insertingItems) {
        [tableView reloadData];
    }

    KWQ_UNBLOCK_EXCEPTIONS;

    _widthGood = NO;
}

void QListBox::beginBatchInsert()
{
    ASSERT(!_insertingItems);
    _insertingItems = true;
}

void QListBox::endBatchInsert()
{
    ASSERT(_insertingItems);
    _insertingItems = false;

    KWQ_BLOCK_EXCEPTIONS;

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

    KWQ_UNBLOCK_EXCEPTIONS;
}

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

    KWQ_BLOCK_EXCEPTIONS;

    NSScrollView *scrollView = 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);
    ASSERT(!_insertingItems);

    KWQ_BLOCK_EXCEPTIONS;

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

    KWQ_UNBLOCK_EXCEPTIONS;

    return false;
}

void QListBox::setEnabled(bool enabled)
{
    _enabled = enabled;
    // You would think this would work, but not until AK fixes 2177792
    //KWQ_BLOCK_EXCEPTIONS;
    //NSTableView *tableView = [(NSScrollView *)getView() documentView];
    //[tableView setEnabled:enabled];
    //KWQ_UNBLOCK_EXCEPTIONS;
}

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

QSize QListBox::sizeForNumberOfLines(int lines) const
{
    ASSERT(!_insertingItems);

    NSScrollView *scrollView = getView();

    NSSize size = {0,0};

    KWQ_BLOCK_EXCEPTIONS;
    NSTableView *tableView = [scrollView documentView];
    
    float width;
    if (_widthGood) {
        width = _width;
    } else {
        width = 0;
        NSCell *cell = [[[tableView tableColumns] objectAtIndex:0] dataCell];
        NSEnumerator *e = [_items objectEnumerator];
        NSString *text;
        while ((text = [e nextObject])) {
            [cell setStringValue:text];
            NSSize size = [cell cellSize];
            width = MAX(width, size.width);
        }
        _width = width;
        _widthGood = YES;
    }
    
    NSSize contentSize;
    contentSize.width = ceil(width);
    contentSize.height = ceil(([tableView rowHeight] + [tableView intercellSpacing].height) * MAX(MIN_LINES, lines));
    size = [NSScrollView frameSizeForContentSize:contentSize
        hasHorizontalScroller:NO hasVerticalScroller:YES borderType:NSBezelBorder];
    KWQ_UNBLOCK_EXCEPTIONS;

    return QSize(size);
}

QWidget::FocusPolicy QListBox::focusPolicy() const
{
    KWQ_BLOCK_EXCEPTIONS;
    
    // Add an additional check here.
    // For now, selects are only focused when full
    // keyboard access is turned on.
    unsigned keyboardUIMode = [KWQKHTMLPart::bridgeForWidget(this) keyboardUIMode];
    if ((keyboardUIMode & WebCoreKeyboardAccessFull) == 0)
        return NoFocus;
    
    KWQ_UNBLOCK_EXCEPTIONS;
    
    return QScrollView::focusPolicy();
}

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

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

    NSScrollView *scrollView = getView();
    KWQTableView *tableView = [scrollView documentView];
    NSWritingDirection direction = d == QPainter::RTL ? NSWritingDirectionRightToLeft : NSWritingDirectionLeftToRight;
    if ([tableView baseWritingDirection] != direction) {
        int n = count();
        for (int i = 0; i < n; i++) {
            NSAttributedString *o = [_items objectAtIndex:i];
            NSAttributedString *s = [[NSAttributedString alloc] initWithString:[o string]
                attributes:stringAttributes([tableView baseWritingDirection], itemIsGroupLabel(i))];
            [_items replaceObjectAtIndex:i withObject:s];
            [s release];
        }
        [tableView setBaseWritingDirection:direction];
        [tableView reloadData];
    }

    KWQ_UNBLOCK_EXCEPTIONS;
}

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

    KWQ_BLOCK_EXCEPTIONS;

    NSAttributedString *s = [_items objectAtIndex:index];
    NSFont *f = [s attribute:NSFontAttributeName atIndex:0 effectiveRange:NULL];
    return f == groupLabelFont();

    KWQ_UNBLOCK_EXCEPTIONS;

    return false;
}

@implementation KWQListBoxScrollView

- (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

- initWithListBox:(QListBox *)b items:(NSArray *)i
{
    [super init];
    _box = b;
    _items = i;

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

    [column setEditable:NO];
    [[column dataCell] setFont:itemFont()];

    [self addTableColumn:column];

    [column release];
    
    [self setAllowsMultipleSelection:NO];
    [self setHeaderView:nil];
    [self setIntercellSpacing:NSMakeSize(0, 0)];
    [self setRowHeight:ceil([[column dataCell] cellSize].height)];
    
    [self setDataSource:self];
    [self setDelegate:self];

    return self;
}

-(void)mouseDown:(NSEvent *)event
{
    processingMouseEvent = TRUE;
    [super mouseDown:event];
    processingMouseEvent = FALSE;

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

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

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

- (BOOL)becomeFirstResponder
{
    BOOL become = [super becomeFirstResponder];
    
    if (become) {
        if (!KWQKHTMLPart::currentEventIsMouseDownInWidget(_box)) {
            [self _KWQ_scrollFrameToVisible];
        }        
	[self _KWQ_setKeyboardFocusRingNeedsDisplay];
	QFocusEvent event(QEvent::FocusIn);
	const_cast<QObject *>(_box->eventFilterObject())->eventFilter(_box, &event);
    }

    return become;
}

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

-(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 [_items count];
}

- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)column row:(int)row
{
    return [_items objectAtIndex:row];
}

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

- (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(int)row
{
    return !_box->itemIsGroupLabel(row);
}

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

- (void)tableView:(NSTableView *)tableView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn row:(int)row
{
    ASSERT([cell isKindOfClass:[NSCell class]]);
    [(NSCell *)cell setEnabled:_box->isEnabled()];
}

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

- (QWidget *)widget
{
    return _box;
}

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

- (NSWritingDirection)baseWritingDirection
{
    return _direction;
}

@end