Reviewed by Dave.
[WebKit-https.git] / WebCore / kwq / KWQComboBox.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 "KWQComboBox.h"
27
28 #import "KWQAssertions.h"
29 #import "KWQButton.h"
30 #import "KWQExceptions.h"
31 #import "KWQKHTMLPart.h"
32 #import "KWQNSViewExtras.h"
33 #import "KWQView.h"
34 #import "WebCoreBridge.h"
35 #import "WebCoreTextRenderer.h"
36 #import "WebCoreTextRendererFactory.h"
37
38 @interface NSCell (KWQComboBoxKnowsAppKitSecrets)
39 - (NSMutableDictionary *)_textAttributes;
40 @end
41
42 enum {
43     topMargin,
44     bottomMargin,
45     leftMargin,
46     rightMargin,
47     baselineFudgeFactor,
48     widthNotIncludingText,
49     minimumTextWidth
50 };
51
52 @interface KWQComboBoxAdapter : NSObject
53 {
54     QComboBox *box;
55 }
56 - (id)initWithQComboBox:(QComboBox *)b;
57 - (void)action:(id)sender;
58 @end
59
60 @interface KWQPopUpButtonCell : NSPopUpButtonCell <KWQWidgetHolder>
61 {
62     QComboBox *box;
63     NSWritingDirection baseWritingDirection;
64 }
65 - (id)initWithQComboBox:(QComboBox *)b;
66 - (void)setBaseWritingDirection:(NSWritingDirection)direction;
67 - (NSWritingDirection)baseWritingDirection;
68 @end
69
70 @interface KWQPopUpButton : NSPopUpButton <KWQWidgetHolder>
71 {
72     BOOL inNextValidKeyView;
73 }
74 @end
75
76 QComboBox::QComboBox()
77     : _adapter(0)
78     , _widthGood(false)
79     , _currentItem(0)
80     , _menuPopulated(true)
81     , _activated(this, SIGNAL(activated(int)))
82 {
83     KWQ_BLOCK_EXCEPTIONS;
84
85     _adapter = [[KWQComboBoxAdapter alloc] initWithQComboBox:this];
86     KWQPopUpButton *button = [[KWQPopUpButton alloc] init];
87     setView(button);
88     [button release];
89     
90     KWQPopUpButtonCell *cell = [[KWQPopUpButtonCell alloc] initWithQComboBox:this];
91     [button setCell:cell];
92     [cell release];
93
94     [button setTarget:_adapter];
95     [button setAction:@selector(action:)];
96
97     [[button cell] setControlSize:NSSmallControlSize];
98     [button setFont:[NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:NSSmallControlSize]]];
99
100     KWQ_UNBLOCK_EXCEPTIONS;
101 }
102
103 QComboBox::~QComboBox()
104 {
105     KWQ_BLOCK_EXCEPTIONS;
106
107     KWQPopUpButton *button = (KWQPopUpButton *)getView();
108     [button setTarget:nil];
109     [_adapter release];
110
111     KWQ_UNBLOCK_EXCEPTIONS;
112 }
113
114 void QComboBox::appendItem(const QString &text)
115 {
116     KWQ_BLOCK_EXCEPTIONS;
117
118     _items.append(text);
119     if (_menuPopulated) {
120         KWQPopUpButton *button = (KWQPopUpButton *)getView();
121         if (![[button cell] isHighlighted]) {
122             _menuPopulated = false;
123         } else {
124             // We must add the item with no title and then set the title because
125             // addItemWithTitle does not allow duplicate titles.
126             [button addItemWithTitle:@""];
127             [[button lastItem] setTitle:text.getNSString()];
128         }
129     }
130     _widthGood = false;
131
132     KWQ_UNBLOCK_EXCEPTIONS;
133 }
134
135 QSize QComboBox::sizeHint() const 
136 {
137     KWQ_BLOCK_EXCEPTIONS;
138
139     KWQPopUpButton *button = (KWQPopUpButton *)getView();
140     
141     if (!_widthGood) {
142         float width = 0;
143         QValueListConstIterator<QString> i = const_cast<const QStringList &>(_items).begin();
144         QValueListConstIterator<QString> e = const_cast<const QStringList &>(_items).end();
145         if (i != e) {
146             id <WebCoreTextRenderer> renderer = [[WebCoreTextRendererFactory sharedFactory]
147                 rendererWithFont:[button font] usingPrinterFont:![NSGraphicsContext currentContextDrawingToScreen]];
148             WebCoreTextStyle style;
149             WebCoreInitializeEmptyTextStyle(&style);
150             do {
151                 const QString &s = *i;
152                 ++i;
153
154                 WebCoreTextRun run;
155                 int length = s.length();
156                 WebCoreInitializeTextRun(&run, reinterpret_cast<const UniChar *>(s.unicode()), length, 0, length);
157
158                 float textWidth = [renderer floatWidthForRun:&run style:&style widths:0];
159                 width = kMax(width, textWidth);
160             } while (i != e);
161         }
162         _width = kMax(static_cast<int>(ceilf(width)), dimensions()[minimumTextWidth]);
163         _widthGood = true;
164     }
165     
166     return QSize(_width + dimensions()[widthNotIncludingText],
167         static_cast<int>([[button cell] cellSize].height) - (dimensions()[topMargin] + dimensions()[bottomMargin]));
168
169     KWQ_UNBLOCK_EXCEPTIONS;
170
171     return QSize(0, 0);
172 }
173
174 QRect QComboBox::frameGeometry() const
175 {
176     QRect r = QWidget::frameGeometry();
177     return QRect(r.x() + dimensions()[leftMargin], r.y() + dimensions()[topMargin],
178         r.width() - (dimensions()[leftMargin] + dimensions()[rightMargin]),
179         r.height() - (dimensions()[topMargin] + dimensions()[bottomMargin]));
180 }
181
182 void QComboBox::setFrameGeometry(const QRect &r)
183 {
184     QWidget::setFrameGeometry(QRect(-dimensions()[leftMargin] + r.x(), -dimensions()[topMargin] + r.y(),
185         dimensions()[leftMargin] + r.width() + dimensions()[rightMargin],
186         dimensions()[topMargin] + r.height() + dimensions()[bottomMargin]));
187 }
188
189 int QComboBox::baselinePosition(int height) const
190 {
191     // Menu text is at the top.
192     KWQPopUpButton *button = (KWQPopUpButton *)getView();
193     return static_cast<int>(ceilf(-dimensions()[topMargin] + dimensions()[baselineFudgeFactor] + [[button font] ascender]));
194 }
195
196 void QComboBox::clear()
197 {
198     KWQPopUpButton *button = (KWQPopUpButton *)getView();
199     [button removeAllItems];
200     _widthGood = false;
201     _currentItem = 0;
202     _items.clear();
203     _menuPopulated = true;
204 }
205
206 void QComboBox::setCurrentItem(int index)
207 {
208     ASSERT(index < (int)_items.count());
209
210     KWQ_BLOCK_EXCEPTIONS;
211
212     KWQPopUpButton *button = (KWQPopUpButton *)getView();
213     if (_menuPopulated) {
214         [button selectItemAtIndex:index];
215     } else {
216         [button removeAllItems];
217         [button addItemWithTitle:@""];
218         [[button itemAtIndex:0] setTitle:_items[index].getNSString()];
219     }
220
221     KWQ_UNBLOCK_EXCEPTIONS;
222
223     _currentItem = index;
224 }
225
226 void QComboBox::itemSelected()
227 {
228     ASSERT(_menuPopulated);
229
230     KWQ_BLOCK_EXCEPTIONS;
231
232     KWQPopUpButton *button = (KWQPopUpButton *)getView();
233     int i = [button indexOfSelectedItem];
234     if (_currentItem == i) {
235         return;
236     }
237     _currentItem = i;
238
239     KWQ_UNBLOCK_EXCEPTIONS;
240
241     _activated.call(_currentItem);
242 }
243
244 void QComboBox::setFont(const QFont &f)
245 {
246     QWidget::setFont(f);
247
248     const NSControlSize size = KWQNSControlSizeForFont(f);
249     NSControl * const button = static_cast<NSControl *>(getView());
250
251     KWQ_BLOCK_EXCEPTIONS;
252
253     if (size != [[button cell] controlSize]) {
254         [[button cell] setControlSize:size];
255         [button setFont:[NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:size]]];
256         _widthGood = false;
257     }
258
259     KWQ_UNBLOCK_EXCEPTIONS;
260 }
261
262 const int *QComboBox::dimensions() const
263 {
264     // We empirically determined these dimensions.
265     // It would be better to get this info from AppKit somehow.
266     static const int w[3][7] = {
267         { 2, 3, 3, 3, 4, 34, 9 },
268         { 1, 3, 3, 3, 3, 31, 5 },
269         { 0, 0, 1, 1, 2, 32, 0 }
270     };
271     NSControl * const button = static_cast<NSControl *>(getView());
272
273     KWQ_BLOCK_EXCEPTIONS;
274     return  w[[[button cell] controlSize]];
275     KWQ_UNBLOCK_EXCEPTIONS;
276
277     return w[NSSmallControlSize];
278 }
279
280 QWidget::FocusPolicy QComboBox::focusPolicy() const
281 {
282     KWQ_BLOCK_EXCEPTIONS;
283     
284     // Menus are only focused when full keyboard access is turned on.
285     unsigned keyboardUIMode = [KWQKHTMLPart::bridgeForWidget(this) keyboardUIMode];
286     if ((keyboardUIMode & WebCoreKeyboardAccessFull) == 0)
287         return NoFocus;
288     
289     KWQ_UNBLOCK_EXCEPTIONS;
290     
291     return QWidget::focusPolicy();
292 }
293
294 void QComboBox::setWritingDirection(QPainter::TextDirection direction)
295 {
296     KWQ_BLOCK_EXCEPTIONS;
297
298     KWQPopUpButton *button = getView();
299     KWQPopUpButtonCell *cell = [button cell];
300     NSWritingDirection d = direction == QPainter::RTL ? NSWritingDirectionRightToLeft : NSWritingDirectionLeftToRight;
301     if ([cell baseWritingDirection] != d) {
302         [cell setBaseWritingDirection:d];
303         [button setNeedsDisplay:YES];
304     }
305
306     KWQ_UNBLOCK_EXCEPTIONS;
307 }
308
309 void QComboBox::populateMenu()
310 {
311     if (!_menuPopulated) {
312         KWQ_BLOCK_EXCEPTIONS;
313
314         KWQPopUpButton *button = getView();
315         [button removeAllItems];
316         QValueListConstIterator<QString> i = const_cast<const QStringList &>(_items).begin();
317         QValueListConstIterator<QString> e = const_cast<const QStringList &>(_items).end();
318         for (; i != e; ++i) {
319             // We must add the item with no title and then set the title because
320             // addItemWithTitle does not allow duplicate titles.
321             [button addItemWithTitle:@""];
322             [[button lastItem] setTitle:(*i).getNSString()];
323         }
324         [button selectItemAtIndex:_currentItem];
325
326         KWQ_UNBLOCK_EXCEPTIONS;
327
328         _menuPopulated = true;
329     }
330 }
331
332 @implementation KWQComboBoxAdapter
333
334 - (id)initWithQComboBox:(QComboBox *)b
335 {
336     box = b;
337     return [super init];
338 }
339
340 - (void)action:(id)sender
341 {
342     box->itemSelected();
343 }
344
345 @end
346
347 @implementation KWQPopUpButtonCell
348
349 - (id)initWithQComboBox:(QComboBox *)b
350 {
351     box = b;
352     return [super init];
353 }
354
355 - (BOOL)trackMouse:(NSEvent *)event inRect:(NSRect)rect ofView:(NSView *)view untilMouseUp:(BOOL)flag
356 {
357     WebCoreBridge *bridge = [KWQKHTMLPart::bridgeForWidget(box) retain];
358     BOOL result = [super trackMouse:event inRect:rect ofView:view untilMouseUp:flag];
359     if (result) {
360         // Give KHTML a chance to fix up its event state, since the popup eats all the
361         // events during tracking.  [NSApp currentEvent] is still the original mouseDown
362         // at this point!
363         [bridge part]->sendFakeEventsAfterWidgetTracking(event);
364     }
365     [bridge release];
366     return result;
367 }
368
369 - (QWidget *)widget
370 {
371     return box;
372 }
373
374 - (void)setBaseWritingDirection:(NSWritingDirection)direction
375 {
376     baseWritingDirection = direction;
377 }
378
379 - (NSWritingDirection)baseWritingDirection
380 {
381     return baseWritingDirection;
382 }
383
384 - (NSMutableDictionary *)_textAttributes
385 {
386     NSMutableDictionary *attributes = [super _textAttributes];
387     NSParagraphStyle *style = [attributes objectForKey:NSParagraphStyleAttributeName];
388     ASSERT(style != nil);
389     if ([style baseWritingDirection] != baseWritingDirection) {
390         NSMutableParagraphStyle *mutableStyle = [style mutableCopy];
391         [mutableStyle setBaseWritingDirection:baseWritingDirection];
392         [attributes setObject:mutableStyle forKey:NSParagraphStyleAttributeName];
393         [mutableStyle release];
394     }
395     return attributes;
396 }
397
398 - (void)setHighlighted:(BOOL)highlighted
399 {
400     if (highlighted) {
401         box->populateMenu();
402     }
403     [super setHighlighted:highlighted];
404 }
405
406 @end
407
408 @implementation KWQPopUpButton
409
410 - (QWidget *)widget
411 {
412     return [(KWQPopUpButtonCell *)[self cell] widget];
413 }
414
415 - (BOOL)becomeFirstResponder
416 {
417     BOOL become = [super becomeFirstResponder];
418     if (become) {
419         QWidget *widget = [self widget];
420         if (!KWQKHTMLPart::currentEventIsMouseDownInWidget(widget)) {
421             [self _KWQ_scrollFrameToVisible];
422         }
423         QFocusEvent event(QEvent::FocusIn);
424         const_cast<QObject *>(widget->eventFilterObject())->eventFilter(widget, &event);
425     }
426     return become;
427 }
428
429 - (BOOL)resignFirstResponder
430 {
431     BOOL resign = [super resignFirstResponder];
432     if (resign) {
433         QWidget *widget = [self widget];
434         QFocusEvent event(QEvent::FocusOut);
435         const_cast<QObject *>(widget->eventFilterObject())->eventFilter(widget, &event);
436     }
437     return resign;
438 }
439
440 - (NSView *)nextKeyView
441 {
442     QWidget *widget = [self widget];
443     return widget && inNextValidKeyView
444         ? KWQKHTMLPart::nextKeyViewForWidget(widget, KWQSelectingNext)
445         : [super nextKeyView];
446 }
447
448 - (NSView *)previousKeyView
449 {
450     QWidget *widget = [self widget];
451     return widget && inNextValidKeyView
452         ? KWQKHTMLPart::nextKeyViewForWidget(widget, KWQSelectingPrevious)
453         : [super previousKeyView];
454 }
455
456 - (NSView *)nextValidKeyView
457 {
458     inNextValidKeyView = YES;
459     NSView *view = [super nextValidKeyView];
460     inNextValidKeyView = NO;
461     return view;
462 }
463
464 - (NSView *)previousValidKeyView
465 {
466     inNextValidKeyView = YES;
467     NSView *view = [super previousValidKeyView];
468     inNextValidKeyView = NO;
469     return view;
470 }
471
472 @end