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