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