/* * 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 "KWQComboBox.h" #import "KWQAssertions.h" #import "KWQButton.h" #import "KWQExceptions.h" #import "KWQKHTMLPart.h" #import "KWQNSViewExtras.h" #import "KWQFoundationExtras.h" #import "KWQView.h" #import "WebCoreBridge.h" #import "WebCoreTextRenderer.h" #import "WebCoreTextRendererFactory.h" @interface NSCell (KWQComboBoxKnowsAppKitSecrets) - (NSMutableDictionary *)_textAttributes; @end enum { topMargin, bottomMargin, leftMargin, rightMargin, baselineFudgeFactor, widthNotIncludingText, minimumTextWidth }; @interface KWQPopUpButtonCell : NSPopUpButtonCell <KWQWidgetHolder> { QComboBox *box; NSWritingDirection baseWritingDirection; } - (id)initWithQComboBox:(QComboBox *)b; - (void)detachQComboBox; - (void)setBaseWritingDirection:(NSWritingDirection)direction; - (NSWritingDirection)baseWritingDirection; @end @interface KWQPopUpButton : NSPopUpButton <KWQWidgetHolder> { BOOL inNextValidKeyView; BOOL populatingMenu; } - (void)setPopulatingMenu:(BOOL)populating; @end QComboBox::QComboBox() : _widthGood(false) , _currentItem(0) , _menuPopulated(true) , _labelFont(nil) , _activated(this, SIGNAL(activated(int))) { KWQ_BLOCK_EXCEPTIONS; KWQPopUpButton *button = [[KWQPopUpButton alloc] init]; setView(button); [button release]; KWQPopUpButtonCell *cell = [[KWQPopUpButtonCell alloc] initWithQComboBox:this]; #ifndef OMIT_TIGER_FEATURES // Work around problem where the pop-up menu gets a "..." in it // by turning off the default "ellipsizing" behavior. [cell setLineBreakMode:NSLineBreakByClipping]; #endif [button setCell:cell]; [cell release]; [button setTarget:button]; [button setAction:@selector(action:)]; [[button cell] setControlSize:NSSmallControlSize]; [button setFont:[NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:NSSmallControlSize]]]; KWQ_UNBLOCK_EXCEPTIONS; } QComboBox::~QComboBox() { KWQ_BLOCK_EXCEPTIONS; KWQPopUpButton *button = (KWQPopUpButton *)getView(); [button setTarget:nil]; [[button cell] detachQComboBox]; KWQRelease(_labelFont); KWQ_UNBLOCK_EXCEPTIONS; } void QComboBox::setTitle(NSMenuItem *menuItem, const KWQListBoxItem &title) { if (title.isGroupLabel) { NSDictionary *attributes = [[NSDictionary alloc] initWithObjectsAndKeys:labelFont(), NSFontAttributeName, nil]; NSAttributedString *string = [[NSAttributedString alloc] initWithString:title.string.getNSString() attributes:attributes]; [menuItem setAttributedTitle:string]; [string release]; [attributes release]; [menuItem setAction:NULL /*@selector(fakeSelectorForDisabledItem)*/]; } else { [menuItem setTitle:title.string.getNSString()]; } } void QComboBox::appendItem(const QString &text, bool isLabel) { const KWQListBoxItem listItem(text, isLabel); _items.append(listItem); if (_menuPopulated) { KWQPopUpButton *button = (KWQPopUpButton *)getView(); if (![[button cell] isHighlighted]) { _menuPopulated = false; } else { // We must add the item with no title and then set the title because // addItemWithTitle does not allow duplicate titles. KWQ_BLOCK_EXCEPTIONS; [button addItemWithTitle:@""]; NSMenuItem *menuItem = [button lastItem]; setTitle(menuItem, listItem); KWQ_UNBLOCK_EXCEPTIONS; } } _widthGood = false; } QSize QComboBox::sizeHint() const { KWQ_BLOCK_EXCEPTIONS; KWQPopUpButton *button = (KWQPopUpButton *)getView(); 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) { id <WebCoreTextRenderer> itemRenderer = [[WebCoreTextRendererFactory sharedFactory] rendererWithFont:[button font] usingPrinterFont:![NSGraphicsContext currentContextDrawingToScreen]]; id <WebCoreTextRenderer> labelRenderer = nil; WebCoreTextStyle style; WebCoreInitializeEmptyTextStyle(&style); style.applyRunRounding = NO; style.applyWordRounding = NO; do { const QString &s = (*i).string; bool isLabel = (*i).isGroupLabel; ++i; WebCoreTextRun run; int length = s.length(); WebCoreInitializeTextRun(&run, reinterpret_cast<const UniChar *>(s.unicode()), length, 0, length); id <WebCoreTextRenderer> renderer; if (isLabel) { if (labelRenderer == nil) { labelRenderer = [[WebCoreTextRendererFactory sharedFactory] rendererWithFont:labelFont() usingPrinterFont:![NSGraphicsContext currentContextDrawingToScreen]]; } renderer = labelRenderer; } else { renderer = itemRenderer; } float textWidth = [renderer floatWidthForRun:&run style:&style widths:0]; width = kMax(width, textWidth); } while (i != e); } _width = kMax(static_cast<int>(ceilf(width)), dimensions()[minimumTextWidth]); _widthGood = true; } return QSize(_width + dimensions()[widthNotIncludingText], static_cast<int>([[button cell] cellSize].height) - (dimensions()[topMargin] + dimensions()[bottomMargin])); KWQ_UNBLOCK_EXCEPTIONS; return QSize(0, 0); } QRect QComboBox::frameGeometry() const { QRect r = QWidget::frameGeometry(); return QRect(r.x() + dimensions()[leftMargin], r.y() + dimensions()[topMargin], r.width() - (dimensions()[leftMargin] + dimensions()[rightMargin]), r.height() - (dimensions()[topMargin] + dimensions()[bottomMargin])); } void QComboBox::setFrameGeometry(const QRect &r) { QWidget::setFrameGeometry(QRect(-dimensions()[leftMargin] + r.x(), -dimensions()[topMargin] + r.y(), dimensions()[leftMargin] + r.width() + dimensions()[rightMargin], dimensions()[topMargin] + r.height() + dimensions()[bottomMargin])); } int QComboBox::baselinePosition(int height) const { // Menu text is at the top. KWQPopUpButton *button = (KWQPopUpButton *)getView(); return static_cast<int>(ceilf(-dimensions()[topMargin] + dimensions()[baselineFudgeFactor] + [[button font] ascender])); } void QComboBox::clear() { KWQPopUpButton *button = (KWQPopUpButton *)getView(); [button removeAllItems]; _widthGood = false; _currentItem = 0; _items.clear(); _menuPopulated = true; } void QComboBox::setCurrentItem(int index) { ASSERT(index < (int)_items.count()); KWQ_BLOCK_EXCEPTIONS; KWQPopUpButton *button = (KWQPopUpButton *)getView(); if (_menuPopulated) { [button selectItemAtIndex:index]; } else { [button removeAllItems]; [button addItemWithTitle:@""]; NSMenuItem *menuItem = [button itemAtIndex:0]; setTitle(menuItem, _items[index]); } KWQ_UNBLOCK_EXCEPTIONS; _currentItem = index; } void QComboBox::itemSelected() { ASSERT(_menuPopulated); KWQ_BLOCK_EXCEPTIONS; KWQPopUpButton *button = (KWQPopUpButton *)getView(); int i = [button indexOfSelectedItem]; if (_currentItem == i) { return; } _currentItem = i; KWQ_UNBLOCK_EXCEPTIONS; _activated.call(_currentItem); } void QComboBox::setFont(const QFont &f) { QWidget::setFont(f); const NSControlSize size = KWQNSControlSizeForFont(f); NSControl * const button = static_cast<NSControl *>(getView()); KWQ_BLOCK_EXCEPTIONS; if (size != [[button cell] controlSize]) { [[button cell] setControlSize:size]; [button setFont:[NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:size]]]; KWQRelease(_labelFont); _labelFont = nil; _widthGood = false; } KWQ_UNBLOCK_EXCEPTIONS; } NSFont *QComboBox::labelFont() const { if (_labelFont == nil) { NSControl * const button = static_cast<NSControl *>(getView()); _labelFont = KWQRetain([NSFont boldSystemFontOfSize:[[button font] pointSize]]); } return _labelFont; } const int *QComboBox::dimensions() const { // We empirically determined these dimensions. // It would be better to get this info from AppKit somehow. static const int w[3][7] = { { 2, 3, 3, 3, 4, 34, 9 }, { 1, 3, 3, 3, 3, 31, 5 }, { 0, 0, 1, 1, 2, 32, 0 } }; NSControl * const button = static_cast<NSControl *>(getView()); KWQ_BLOCK_EXCEPTIONS; return w[[[button cell] controlSize]]; KWQ_UNBLOCK_EXCEPTIONS; return w[NSSmallControlSize]; } QWidget::FocusPolicy QComboBox::focusPolicy() const { KWQ_BLOCK_EXCEPTIONS; WebCoreBridge *bridge = KWQKHTMLPart::bridgeForWidget(this); if (!bridge || ![bridge part] || ![bridge part]->tabsToAllControls()) { return NoFocus; } KWQ_UNBLOCK_EXCEPTIONS; return QWidget::focusPolicy(); } void QComboBox::setWritingDirection(QPainter::TextDirection direction) { KWQ_BLOCK_EXCEPTIONS; KWQPopUpButton *button = getView(); KWQPopUpButtonCell *cell = [button cell]; NSWritingDirection d = direction == QPainter::RTL ? NSWritingDirectionRightToLeft : NSWritingDirectionLeftToRight; if ([cell baseWritingDirection] != d) { [cell setBaseWritingDirection:d]; [button setNeedsDisplay:YES]; } KWQ_UNBLOCK_EXCEPTIONS; } void QComboBox::populateMenu() { if (!_menuPopulated) { KWQ_BLOCK_EXCEPTIONS; KWQPopUpButton *button = getView(); [button setPopulatingMenu:YES]; [button removeAllItems]; QValueListConstIterator<KWQListBoxItem> i = const_cast<const QValueList<KWQListBoxItem> &>(_items).begin(); QValueListConstIterator<KWQListBoxItem> e = const_cast<const QValueList<KWQListBoxItem> &>(_items).end(); for (; i != e; ++i) { // We must add the item with no title and then set the title because // addItemWithTitle does not allow duplicate titles. [button addItemWithTitle:@""]; NSMenuItem *menuItem = [button lastItem]; setTitle(menuItem, *i); } [button selectItemAtIndex:_currentItem]; [button setPopulatingMenu:NO]; KWQ_UNBLOCK_EXCEPTIONS; _menuPopulated = true; } } void QComboBox::populate() { populateMenu(); } @implementation KWQPopUpButtonCell - (id)initWithQComboBox:(QComboBox *)b { box = b; return [super init]; } - (void)detachQComboBox { box = 0; } - (BOOL)trackMouse:(NSEvent *)event inRect:(NSRect)rect ofView:(NSView *)view untilMouseUp:(BOOL)flag { WebCoreBridge *bridge = box ? [KWQKHTMLPart::bridgeForWidget(box) retain] : nil; BOOL result = [super trackMouse:event inRect:rect ofView:view untilMouseUp:flag]; if (result && bridge) { // Give KHTML a chance to fix up its event state, since the popup eats all the // events during tracking. [NSApp currentEvent] is still the original mouseDown // at this point! [bridge part]->sendFakeEventsAfterWidgetTracking(event); } [bridge release]; return result; } - (QWidget *)widget { return box; } - (void)setBaseWritingDirection:(NSWritingDirection)direction { baseWritingDirection = direction; } - (NSWritingDirection)baseWritingDirection { return baseWritingDirection; } - (NSMutableDictionary *)_textAttributes { NSMutableDictionary *attributes = [super _textAttributes]; NSParagraphStyle *style = [attributes objectForKey:NSParagraphStyleAttributeName]; ASSERT(style != nil); if ([style baseWritingDirection] != baseWritingDirection) { NSMutableParagraphStyle *mutableStyle = [style mutableCopy]; [mutableStyle setBaseWritingDirection:baseWritingDirection]; [attributes setObject:mutableStyle forKey:NSParagraphStyleAttributeName]; [mutableStyle release]; } return attributes; } - (void)setHighlighted:(BOOL)highlighted { if (highlighted && box) { box->populateMenu(); } [super setHighlighted:highlighted]; } @end @implementation KWQPopUpButton - (void)action:(id)sender { QComboBox *box = static_cast<QComboBox *>([self widget]); if (box) { box->itemSelected(); } } - (QWidget *)widget { return [[self cell] widget]; } - (void)mouseDown:(NSEvent *)event { QWidget::beforeMouseDown(self); [super mouseDown:event]; QWidget::afterMouseDown(self); } - (BOOL)becomeFirstResponder { BOOL become = [super becomeFirstResponder]; if (become) { QWidget *widget = [self widget]; if (widget) { if (!KWQKHTMLPart::currentEventIsMouseDownInWidget(widget)) { [self _KWQ_scrollFrameToVisible]; } QFocusEvent event(QEvent::FocusIn); const_cast<QObject *>(widget->eventFilterObject())->eventFilter(widget, &event); } } return become; } - (BOOL)resignFirstResponder { BOOL resign = [super resignFirstResponder]; if (resign) { QWidget *widget = [self widget]; if (widget) { QFocusEvent event(QEvent::FocusOut); const_cast<QObject *>(widget->eventFilterObject())->eventFilter(widget, &event); } } return resign; } - (BOOL)canBecomeKeyView { // Simplified method from NSView; overridden to replace NSView's way of checking // for full keyboard access with ours. QWidget *widget = [self widget]; if (widget && !KWQKHTMLPart::partForWidget([self widget])->tabsToAllControls()) { return NO; } return ([self window] != nil) && ![self isHiddenOrHasHiddenAncestor] && [self acceptsFirstResponder]; } - (NSView *)nextKeyView { QWidget *widget = [self widget]; return widget && inNextValidKeyView ? KWQKHTMLPart::nextKeyViewForWidget(widget, KWQSelectingNext) : [super nextKeyView]; } - (NSView *)previousKeyView { QWidget *widget = [self widget]; return widget && inNextValidKeyView ? KWQKHTMLPart::nextKeyViewForWidget(widget, 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; } - (void)setPopulatingMenu:(BOOL)populating { populatingMenu = populating; } - (void)setNeedsDisplayInRect:(NSRect)rect { if (!populatingMenu) { [super setNeedsDisplayInRect:rect]; } } @end