Reviewed by Hyatt.
[WebKit-https.git] / WebCore / kwq / KWQListBox.mm
1 /*
2  * Copyright (C) 2004 Apple Computer, Inc.  All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
14  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE COMPUTER, INC. OR
17  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21  * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
24  */
25
26 #import "KWQListBox.h"
27
28 #import "KWQAssertions.h"
29 #import "KWQExceptions.h"
30 #import "KWQKHTMLPart.h"
31 #import "KWQNSViewExtras.h"
32 #import "KWQView.h"
33 #import "WebCoreBridge.h"
34 #import "WebCoreScrollView.h"
35 #import "WebCoreTextRenderer.h"
36 #import "WebCoreTextRendererFactory.h"
37
38 @interface NSTableView (KWQListBoxKnowsAppKitSecrets)
39 - (NSCell *)_accessibilityTableCell:(int)row tableColumn:(NSTableColumn *)tableColumn;
40 @end
41
42 const int minLines = 4; /* ensures we have a scroll bar */
43 const float bottomMargin = 1;
44 const float leftMargin = 2;
45 const float rightMargin = 2;
46
47 @interface KWQListBoxScrollView : WebCoreScrollView <KWQWidgetHolder>
48 {
49     QListBox *_box;
50 }
51 @end
52
53 @interface KWQTableView : NSTableView <KWQWidgetHolder>
54 {
55     QListBox *_box;
56     BOOL processingMouseEvent;
57     BOOL clickedDuringMouseEvent;
58     BOOL inNextValidKeyView;
59     NSWritingDirection _direction;
60 }
61 - (id)initWithListBox:(QListBox *)b;
62 - (void)detach;
63 - (void)_KWQ_setKeyboardFocusRingNeedsDisplay;
64 - (QWidget *)widget;
65 - (void)setBaseWritingDirection:(NSWritingDirection)direction;
66 - (NSWritingDirection)baseWritingDirection;
67 @end
68
69 static id <WebCoreTextRenderer> itemScreenRenderer;
70 static id <WebCoreTextRenderer> itemPrinterRenderer;
71 static id <WebCoreTextRenderer> groupLabelScreenRenderer;
72 static id <WebCoreTextRenderer> groupLabelPrinterRenderer;
73
74 static NSFont *itemFont()
75 {
76     static NSFont *font = [[NSFont systemFontOfSize:[NSFont smallSystemFontSize]] retain];
77     return font;
78 }
79
80 static NSFont *groupLabelFont()
81 {
82     static NSFont *font = [[NSFont boldSystemFontOfSize:[NSFont smallSystemFontSize]] retain];
83     return font;
84 }
85
86 static id <WebCoreTextRenderer> itemTextRenderer()
87 {
88     if ([NSGraphicsContext currentContextDrawingToScreen]) {
89         if (itemScreenRenderer == nil) {
90             itemScreenRenderer = [[[WebCoreTextRendererFactory sharedFactory]
91                 rendererWithFont:itemFont() usingPrinterFont:NO] retain];
92         }
93         return itemScreenRenderer;
94     } else {
95         if (itemPrinterRenderer == nil) {
96             itemPrinterRenderer = [[[WebCoreTextRendererFactory sharedFactory]
97                 rendererWithFont:itemFont() usingPrinterFont:YES] retain];
98         }
99         return itemPrinterRenderer;
100     }
101 }
102
103 static id <WebCoreTextRenderer> groupLabelTextRenderer()
104 {
105     if ([NSGraphicsContext currentContextDrawingToScreen]) {
106         if (groupLabelScreenRenderer == nil) {
107             groupLabelScreenRenderer = [[[WebCoreTextRendererFactory sharedFactory]
108                 rendererWithFont:groupLabelFont() usingPrinterFont:NO] retain];
109         }
110         return groupLabelScreenRenderer;
111     } else {
112         if (groupLabelPrinterRenderer == nil) {
113             groupLabelPrinterRenderer = [[[WebCoreTextRendererFactory sharedFactory]
114                 rendererWithFont:groupLabelFont() usingPrinterFont:YES] retain];
115         }
116         return groupLabelPrinterRenderer;
117     }
118 }
119
120 QListBox::QListBox(QWidget *parent)
121     : QScrollView(parent)
122     , _changingSelection(false)
123     , _enabled(true)
124     , _widthGood(false)
125     , _clicked(this, SIGNAL(clicked(QListBoxItem *)))
126     , _selectionChanged(this, SIGNAL(selectionChanged()))
127 {
128     KWQ_BLOCK_EXCEPTIONS;
129
130     NSScrollView *scrollView = [[KWQListBoxScrollView alloc] initWithListBox:this];
131     setView(scrollView);
132     [scrollView release];
133     
134     [scrollView setBorderType:NSBezelBorder];
135     [scrollView setHasVerticalScroller:YES];
136     [[scrollView verticalScroller] setControlSize:NSSmallControlSize];
137
138     // In WebHTMLView, we set a clip. This is not typical to do in an
139     // NSView, and while correct for any one invocation of drawRect:,
140     // it causes some bad problems if that clip is cached between calls.
141     // The cached graphics state, which clip views keep around, does
142     // cache the clip in this undesirable way. Consequently, we want to 
143     // release the GState for all clip views for all views contained in 
144     // a WebHTMLView. Here we do it for list boxes used in forms.
145     // See these bugs for more information:
146     // <rdar://problem/3226083>: REGRESSION (Panther): white box overlaying select lists at nvidia.com drivers page
147     [[scrollView contentView] releaseGState];
148     
149     KWQTableView *tableView = [[KWQTableView alloc] initWithListBox:this];
150     [scrollView setDocumentView:tableView];
151     [tableView release];
152     [scrollView setVerticalLineScroll:[tableView rowHeight]];
153     
154     KWQ_UNBLOCK_EXCEPTIONS;
155 }
156
157 QListBox::~QListBox()
158 {
159     NSScrollView *scrollView = getView();
160     
161     KWQ_BLOCK_EXCEPTIONS;
162     KWQTableView *tableView = [scrollView documentView];
163     [tableView detach];
164     KWQ_UNBLOCK_EXCEPTIONS;
165 }
166
167 void QListBox::clear()
168 {
169     _items.clear();
170     _widthGood = false;
171 }
172
173 void QListBox::setSelectionMode(SelectionMode mode)
174 {
175     NSScrollView *scrollView = getView();
176
177     KWQ_BLOCK_EXCEPTIONS;
178     NSTableView *tableView = [scrollView documentView];
179     [tableView setAllowsMultipleSelection:mode != Single];
180     KWQ_UNBLOCK_EXCEPTIONS;
181 }
182
183 void QListBox::appendItem(const QString &text, bool isLabel)
184 {
185     _items.append(KWQListBoxItem(text, isLabel));
186     _widthGood = false;
187 }
188
189 void QListBox::doneAppendingItems()
190 {
191     KWQ_BLOCK_EXCEPTIONS;
192
193     NSScrollView *scrollView = getView();
194     NSTableView *tableView = [scrollView documentView];
195     [tableView reloadData];
196
197     KWQ_UNBLOCK_EXCEPTIONS;
198 }
199
200 void QListBox::setSelected(int index, bool selectIt)
201 {
202     ASSERT(index >= 0);
203
204     KWQ_BLOCK_EXCEPTIONS;
205
206     NSScrollView *scrollView = getView();
207     NSTableView *tableView = [scrollView documentView];
208     _changingSelection = true;
209     if (selectIt) {
210         [tableView selectRow:index byExtendingSelection:[tableView allowsMultipleSelection]];
211         [tableView scrollRowToVisible:index];
212     } else {
213         [tableView deselectRow:index];
214     }
215
216     KWQ_UNBLOCK_EXCEPTIONS;
217
218     _changingSelection = false;
219 }
220
221 bool QListBox::isSelected(int index) const
222 {
223     ASSERT(index >= 0);
224
225     KWQ_BLOCK_EXCEPTIONS;
226
227     NSScrollView *scrollView = getView();
228     NSTableView *tableView = [scrollView documentView];
229     return [tableView isRowSelected:index]; 
230
231     KWQ_UNBLOCK_EXCEPTIONS;
232
233     return false;
234 }
235
236 void QListBox::setEnabled(bool enabled)
237 {
238     if (enabled != _enabled) {
239         // You would think this would work, but not until AppKit bug 2177792 if fixed.
240         //KWQ_BLOCK_EXCEPTIONS;
241         //NSTableView *tableView = [(NSScrollView *)getView() documentView];
242         //[tableView setEnabled:enabled];
243         //KWQ_UNBLOCK_EXCEPTIONS;
244
245         _enabled = enabled;
246
247         NSScrollView *scrollView = getView();
248         NSTableView *tableView = [scrollView documentView];
249         [tableView reloadData];
250     }
251 }
252
253 bool QListBox::isEnabled()
254 {
255     return _enabled;
256 }
257
258 QSize QListBox::sizeForNumberOfLines(int lines) const
259 {
260     NSSize size = {0,0};
261
262     KWQ_BLOCK_EXCEPTIONS;
263
264     NSScrollView *scrollView = getView();
265     KWQTableView *tableView = [scrollView documentView];
266     
267     if (!_widthGood) {
268         float width = 0;
269         QValueListConstIterator<KWQListBoxItem> i = const_cast<const QValueList<KWQListBoxItem> &>(_items).begin();
270         QValueListConstIterator<KWQListBoxItem> e = const_cast<const QValueList<KWQListBoxItem> &>(_items).end();
271         if (i != e) {
272             WebCoreTextStyle style;
273             WebCoreInitializeEmptyTextStyle(&style);
274             style.rtl = [tableView baseWritingDirection] == NSWritingDirectionRightToLeft;
275             style.applyRunRounding = NO;
276             style.applyWordRounding = NO;
277             do {
278                 const QString &s = (*i).string;
279                 id <WebCoreTextRenderer> renderer = (*i).isGroupLabel ? groupLabelTextRenderer() : itemTextRenderer();
280                 ++i;
281
282                 WebCoreTextRun run;
283                 int length = s.length();
284                 WebCoreInitializeTextRun(&run, reinterpret_cast<const UniChar *>(s.unicode()), length, 0, length);
285
286                 float textWidth = [renderer floatWidthForRun:&run style:&style widths:0];
287                 width = kMax(width, textWidth);
288             } while (i != e);
289         }
290         _width = ceilf(width);
291         _widthGood = true;
292     }
293     
294     size = [NSScrollView frameSizeForContentSize:NSMakeSize(_width, [tableView rowHeight] * MAX(minLines, lines))
295         hasHorizontalScroller:NO hasVerticalScroller:YES borderType:NSBezelBorder];
296     size.width += [NSScroller scrollerWidthForControlSize:NSSmallControlSize] - [NSScroller scrollerWidth] + leftMargin + rightMargin;
297
298     KWQ_UNBLOCK_EXCEPTIONS;
299
300     return QSize(size);
301 }
302
303 QWidget::FocusPolicy QListBox::focusPolicy() const
304 {
305     KWQ_BLOCK_EXCEPTIONS;
306     
307     WebCoreBridge *bridge = KWQKHTMLPart::bridgeForWidget(this);
308     if (!bridge || ![bridge part] || ![bridge part]->tabsToAllControls()) {
309         return NoFocus;
310     }
311     
312     KWQ_UNBLOCK_EXCEPTIONS;
313     
314     return QScrollView::focusPolicy();
315 }
316
317 bool QListBox::checksDescendantsForFocus() const
318 {
319     return true;
320 }
321
322 void QListBox::setWritingDirection(QPainter::TextDirection d)
323 {
324     KWQ_BLOCK_EXCEPTIONS;
325
326     NSScrollView *scrollView = getView();
327     KWQTableView *tableView = [scrollView documentView];
328     NSWritingDirection direction = d == QPainter::RTL ? NSWritingDirectionRightToLeft : NSWritingDirectionLeftToRight;
329     if ([tableView baseWritingDirection] != direction) {
330         [tableView setBaseWritingDirection:direction];
331         [tableView reloadData];
332     }
333
334     KWQ_UNBLOCK_EXCEPTIONS;
335 }
336
337 void QListBox::clearCachedTextRenderers()
338 {
339     [itemScreenRenderer release];
340     itemScreenRenderer = nil;
341
342     [itemPrinterRenderer release];
343     itemPrinterRenderer = nil;
344
345     [groupLabelScreenRenderer release];
346     groupLabelScreenRenderer = nil;
347
348     [groupLabelPrinterRenderer release];
349     groupLabelPrinterRenderer = nil;
350 }
351
352 @implementation KWQListBoxScrollView
353
354 - (id)initWithListBox:(QListBox *)b
355 {
356     if (!(self = [super init]))
357         return nil;
358
359     _box = b;
360     return self;
361 }
362
363 - (QWidget *)widget
364 {
365     return _box;
366 }
367
368 - (void)setFrameSize:(NSSize)size
369 {
370     [super setFrameSize:size];
371     NSTableColumn *column = [[[self documentView] tableColumns] objectAtIndex:0];
372     [column setWidth:[self contentSize].width];
373     [column setMinWidth:[self contentSize].width];
374     [column setMaxWidth:[self contentSize].width];
375 }
376
377 - (BOOL)becomeFirstResponder
378 {
379     KWQTableView *documentView = [self documentView];
380     QWidget *widget = [documentView widget];
381     [KWQKHTMLPart::bridgeForWidget(widget) makeFirstResponder:documentView];
382     return YES;
383 }
384
385 @end
386
387 @implementation KWQTableView
388
389 - (id)initWithListBox:(QListBox *)b
390 {
391     [super init];
392
393     _box = b;
394
395     NSTableColumn *column = [[NSTableColumn alloc] initWithIdentifier:nil];
396
397     [column setEditable:NO];
398
399     [self addTableColumn:column];
400
401     [column release];
402     
403     [self setAllowsMultipleSelection:NO];
404     [self setHeaderView:nil];
405     [self setIntercellSpacing:NSMakeSize(0, 0)];
406     [self setRowHeight:ceilf([itemFont() ascender] - [itemFont() descender] + bottomMargin)];
407     
408     [self setDataSource:self];
409     [self setDelegate:self];
410
411     return self;
412 }
413
414 - (void)detach
415 {
416     _box = 0;
417     [self setDelegate:nil];
418     [self setDataSource:nil];
419 }
420
421 - (void)mouseDown:(NSEvent *)event
422 {
423     processingMouseEvent = YES;
424     NSView *outerView = [_box->getOuterView() retain];
425     QWidget::beforeMouseDown(outerView);
426     [super mouseDown:event];
427     QWidget::afterMouseDown(outerView);
428     [outerView release];
429     processingMouseEvent = NO;
430
431     if (clickedDuringMouseEvent) {
432         clickedDuringMouseEvent = false;
433     } else if (_box) {
434         _box->sendConsumedMouseUp();
435     }
436 }
437
438 - (void)keyDown:(NSEvent *)event
439 {
440     if (!_box)  {
441         return;
442     }
443     WebCoreBridge *bridge = KWQKHTMLPart::bridgeForWidget(_box);
444     if (![bridge interceptKeyEvent:event toView:self]) {
445         [super keyDown:event];
446     }
447 }
448
449 - (void)keyUp:(NSEvent *)event
450 {
451     if (!_box)  {
452         return;
453     }
454     WebCoreBridge *bridge = KWQKHTMLPart::bridgeForWidget(_box);
455     if (![bridge interceptKeyEvent:event toView:self]) {
456         [super keyUp:event];
457     }
458 }
459
460 - (BOOL)becomeFirstResponder
461 {
462     if (!_box) {
463         return NO;
464     }
465
466     BOOL become = [super becomeFirstResponder];
467     
468     if (become) {
469         if (!KWQKHTMLPart::currentEventIsMouseDownInWidget(_box)) {
470             [self _KWQ_scrollFrameToVisible];
471         }        
472         [self _KWQ_setKeyboardFocusRingNeedsDisplay];
473         QFocusEvent event(QEvent::FocusIn);
474         const_cast<QObject *>(_box->eventFilterObject())->eventFilter(_box, &event);
475     }
476
477     return become;
478 }
479
480 - (BOOL)resignFirstResponder
481 {
482     BOOL resign = [super resignFirstResponder];
483     if (resign && _box) {
484         QFocusEvent event(QEvent::FocusOut);
485         const_cast<QObject *>(_box->eventFilterObject())->eventFilter(_box, &event);
486     }
487     return resign;
488 }
489
490 - (BOOL)canBecomeKeyView
491 {
492     // Simplified method from NSView; overridden to replace NSView's way of checking
493     // for full keyboard access with ours.
494     if (!_box || !KWQKHTMLPart::partForWidget(_box)->tabsToAllControls()) {
495         return NO;
496     }
497     return ([self window] != nil) && ![self isHiddenOrHasHiddenAncestor] && [self acceptsFirstResponder];
498 }
499
500 - (NSView *)nextKeyView
501 {
502     return _box && inNextValidKeyView
503         ? KWQKHTMLPart::nextKeyViewForWidget(_box, KWQSelectingNext)
504         : [super nextKeyView];
505 }
506
507 - (NSView *)previousKeyView
508 {
509     return _box && inNextValidKeyView
510         ? KWQKHTMLPart::nextKeyViewForWidget(_box, KWQSelectingPrevious)
511         : [super previousKeyView];
512 }
513
514 - (NSView *)nextValidKeyView
515 {
516     inNextValidKeyView = YES;
517     NSView *view = [super nextValidKeyView];
518     inNextValidKeyView = NO;
519     return view;
520 }
521
522 - (NSView *)previousValidKeyView
523 {
524     inNextValidKeyView = YES;
525     NSView *view = [super previousValidKeyView];
526     inNextValidKeyView = NO;
527     return view;
528 }
529
530 - (int)numberOfRowsInTableView:(NSTableView *)tableView
531 {
532     return _box->count();
533 }
534
535 - (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)column row:(int)row
536 {
537     return nil;
538 }
539
540 - (void)tableViewSelectionDidChange:(NSNotification *)notification
541 {
542     _box->selectionChanged();
543     if (_box && !_box->changingSelection()) {
544         if (processingMouseEvent) {
545             clickedDuringMouseEvent = true;
546             _box->sendConsumedMouseUp();
547         }
548         if (_box) {
549             _box->clicked();
550         }
551     }
552 }
553
554 - (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(int)row
555 {
556     return !_box->itemAtIndex(row).isGroupLabel;
557 }
558
559 - (BOOL)selectionShouldChangeInTableView:(NSTableView *)aTableView
560 {
561     return _box->isEnabled();
562 }
563
564 - (void)drawRow:(int)row clipRect:(NSRect)clipRect
565 {
566     if (!_box) {
567         return;
568     }
569
570     const KWQListBoxItem &item = _box->itemAtIndex(row);
571
572     NSColor *color;
573     if (_box->isEnabled()) {
574         if ([self isRowSelected:row] && [[self window] firstResponder] == self && ([[self window] isKeyWindow] || ![[self window] canBecomeKeyWindow])) {
575             color = [NSColor alternateSelectedControlTextColor];
576         } else {
577             color = [NSColor controlTextColor];
578         }
579     } else {
580         color = [NSColor disabledControlTextColor];
581     }
582
583     bool RTL = _direction == NSWritingDirectionRightToLeft;
584
585     id <WebCoreTextRenderer> renderer = item.isGroupLabel ? groupLabelTextRenderer() : itemTextRenderer();
586
587     WebCoreTextStyle style;
588     WebCoreInitializeEmptyTextStyle(&style);
589     style.rtl = RTL;
590     style.applyRunRounding = NO;
591     style.applyWordRounding = NO;
592     style.textColor = color;
593
594     WebCoreTextRun run;
595     int length = item.string.length();
596     WebCoreInitializeTextRun(&run, reinterpret_cast<const UniChar *>(item.string.unicode()), length, 0, length);
597
598     NSRect cellRect = [self frameOfCellAtColumn:0 row:row];
599     NSPoint point;
600     if (!RTL) {
601         point.x = NSMinX(cellRect) + leftMargin;
602     } else {
603         point.x = NSMaxX(cellRect) - rightMargin - [renderer floatWidthForRun:&run style:&style widths:0];
604     }
605     point.y = NSMaxY(cellRect) + [itemFont() descender] - bottomMargin;
606
607     WebCoreTextGeometry geometry;
608     WebCoreInitializeEmptyTextGeometry(&geometry);
609     geometry.point = point;
610     
611     [renderer drawRun:&run style:&style geometry:&geometry];
612 }
613
614 - (void)_KWQ_setKeyboardFocusRingNeedsDisplay
615 {
616     [self setKeyboardFocusRingNeedsDisplayInRect:[self bounds]];
617 }
618
619 - (QWidget *)widget
620 {
621     return _box;
622 }
623
624 - (void)setBaseWritingDirection:(NSWritingDirection)direction
625 {
626     _direction = direction;
627 }
628
629 - (NSWritingDirection)baseWritingDirection
630 {
631     return _direction;
632 }
633
634 - (NSCell *)_accessibilityTableCell:(int)row tableColumn:(NSTableColumn *)tableColumn
635 {
636     NSCell *cell = [super _accessibilityTableCell:row tableColumn:tableColumn];
637     if (_box) {
638         [cell setStringValue:_box->itemAtIndex(row).string.getNSString()];
639     }
640     return cell;
641 }
642
643 @end