f33b5350e2f5837b1ca87893bf223b2e0915471f
[WebKit-https.git] / WebCore / kwq / KWQFileButton.mm
1 /*
2  * Copyright (C) 2003 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 "config.h"
27 #import "KWQFileButton.h"
28
29 #import "BlockExceptions.h"
30 #import "FoundationExtras.h"
31 #import "FrameMac.h"
32 #import "WebCoreFrameBridge.h"
33 #import "WebCoreStringTruncator.h"
34 #import "WebCoreViewFactory.h"
35 #import "render_form.h"
36
37 using namespace WebCore;
38
39 #define NO_FILE_SELECTED 
40
41 #define AFTER_BUTTON_SPACING 4
42 #define ICON_HEIGHT 16
43 #define ICON_WIDTH 16
44 #define ICON_FILENAME_SPACING 2
45
46 // We empirically determined that buttons have these extra pixels on all
47 // sides. It would be better to get this info from AppKit somehow.
48 #define BUTTON_TOP_MARGIN 4
49 #define BUTTON_BOTTOM_MARGIN 6
50 #define BUTTON_LEFT_MARGIN 5
51 #define BUTTON_RIGHT_MARGIN 5
52
53 // AppKit calls this kThemePushButtonSmallTextOffset.
54 #define BUTTON_VERTICAL_FUDGE_FACTOR 2
55
56 @interface WebFileChooserButton : NSButton
57 {
58     KWQFileButton *_widget;
59 }
60 - (id)initWithWidget:(KWQFileButton *)widget;
61 @end
62
63 @interface WebCoreFileButton : NSView <WebCoreOpenPanelResultListener>
64 {
65     NSString *_filename;
66     WebFileChooserButton *_button;
67     NSImage *_icon;
68     NSString *_label;
69     BOOL _inNextValidKeyView;
70     KWQFileButton *_widget;
71     BOOL _isCurrentDragTarget;
72 }
73 - (void)setFilename:(NSString *)filename;
74 - (void)performClick;
75 - (NSString *)filename;
76 - (float)baseline;
77 - (void)setVisualFrame:(NSRect)rect;
78 - (NSRect)visualFrame;
79 - (NSSize)bestVisualFrameSizeForCharacterCount:(int)count;
80 - (id)initWithWidget:(KWQFileButton *)widget;
81 @end
82
83 @implementation WebCoreFileButton
84
85 - (void)positionButton
86 {
87     [_button sizeToFit];
88     [_button setFrameOrigin:NSMakePoint(0, 0)];
89 }
90
91 - (id)initWithWidget:(KWQFileButton*)widget
92 {
93     self = [super init];
94     if (!self)
95         return nil;
96     [self registerForDraggedTypes:[NSArray arrayWithObject:NSFilenamesPboardType]];
97
98     _widget = widget;
99     return self;
100 }
101
102 - (id)initWithFrame:(NSRect)frame
103 {
104     self = [super initWithFrame:frame];
105     if (self) {
106         _button = [[WebFileChooserButton alloc] initWithWidget:_widget];
107         
108         [_button setTitle:[[WebCoreViewFactory sharedFactory] fileButtonChooseFileLabel]];
109         [[_button cell] setControlSize:NSSmallControlSize];
110         [_button setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
111         [_button setBezelStyle:NSRoundedBezelStyle];
112         [_button setTarget:self];
113         [_button setAction:@selector(chooseButtonPressed:)];
114         [_button setNextResponder:self];
115         
116         [self addSubview:_button];
117         
118         [self positionButton];
119         [self setFilename:nil];
120     }
121     return self;
122 }
123
124 - (void)dealloc
125 {
126     [_filename release];
127     [_button release];
128     [_icon release];
129     [_label release];
130     [super dealloc];
131 }
132
133 - (BOOL)isFlipped
134 {
135     return YES;
136 }
137         
138 - (void)drawRect:(NSRect)rect
139 {
140     NSRect bounds = [self bounds];
141     
142     [NSGraphicsContext saveGraphicsState];
143     NSRectClip(NSIntersectionRect(bounds, rect));
144     
145     if (_isCurrentDragTarget) {
146         [[NSColor colorWithCalibratedWhite:0.0 alpha:0.25f] set];
147         NSRectFillUsingOperation([self bounds], NSCompositeSourceOver);
148     }
149
150     float left = NSMaxX([_button frame]) + AFTER_BUTTON_SPACING;
151
152     if (_icon) {
153         float top = (bounds.size.height - BUTTON_BOTTOM_MARGIN - BUTTON_TOP_MARGIN - ICON_HEIGHT) / 2
154             + BUTTON_TOP_MARGIN;
155         [_icon drawInRect:NSMakeRect(left, top, ICON_WIDTH, ICON_HEIGHT)
156             fromRect:NSMakeRect(0, 0, [_icon size].width, [_icon size].height)
157             operation:NSCompositeSourceOver fraction:1.0];
158         left += ICON_WIDTH + ICON_FILENAME_SPACING;
159     }
160
161     NSFont *font = [_button font];
162     NSDictionary *attributes = [NSDictionary dictionaryWithObject:font forKey:NSFontAttributeName];
163     [_label drawAtPoint:NSMakePoint(left, [self baseline] - [[_button font] ascender]) withAttributes:attributes];
164
165     [NSGraphicsContext restoreGraphicsState];
166 }
167
168 - (void)updateLabel
169 {
170     [_label release];
171     
172     NSString *label;
173     if ([_filename length])
174         label = _filename;
175     else
176         label = [[WebCoreViewFactory sharedFactory] fileButtonNoFileSelectedLabel];
177     
178     float left = NSMaxX([_button frame]) + AFTER_BUTTON_SPACING;
179     if (_icon)
180         left += ICON_WIDTH + ICON_FILENAME_SPACING;
181     float labelWidth = [self bounds].size.width - left;
182
183     _label = labelWidth <= 0 ? nil : [[WebCoreStringTruncator centerTruncateString:
184         [[NSFileManager defaultManager] displayNameAtPath:label]
185         toWidth:labelWidth withFont:[_button font]] copy];
186 }
187
188 - (void)setFilename:(NSString *)filename
189 {
190     NSString *copy = [filename copy];
191     [_filename release];
192     _filename = copy;
193     
194     [_icon release];
195     if ([_filename length] == 0 || [_filename characterAtIndex:0] != '/')
196         _icon = nil;
197     else {
198         _icon = [[[NSWorkspace sharedWorkspace] iconForFile:_filename] retain];
199         // I'm not sure why this has any effect, but including this line of code seems to make
200         // the image appear right-side-up. As far as I know, the drawInRect method used above
201         // in our drawRect method should work regardless of whether the image is flipped or not.
202         [_icon setFlipped:YES];
203     }
204     
205     [self updateLabel];
206     
207     [self setNeedsDisplay:YES];
208 }
209
210 - (NSString *)filename
211 {
212     return [[_filename copy] autorelease];
213 }
214
215 - (void)setFrameSize:(NSSize)size
216 {
217     [super setFrameSize:size];
218     // FIXME: Can we just springs and struts instead of calling positionButton?
219     [self positionButton];
220     [self updateLabel];
221 }
222
223 - (NSSize)bestVisualFrameSizeForCharacterCount:(int)count
224 {
225     ASSERT(count > 0);
226     NSSize size = [[_button cell] cellSize];
227     size.height -= BUTTON_TOP_MARGIN + BUTTON_BOTTOM_MARGIN;
228     size.width -= BUTTON_LEFT_MARGIN + BUTTON_RIGHT_MARGIN;
229     size.width += AFTER_BUTTON_SPACING + ICON_WIDTH + ICON_FILENAME_SPACING;
230     size.width += count * [@"x" sizeWithAttributes:[NSDictionary dictionaryWithObject:[_button font] forKey:NSFontAttributeName]].width;
231     return size;
232 }
233
234 - (NSRect)visualFrame
235 {
236     ASSERT([self superview] == nil || [[self superview] isFlipped]);
237     NSRect frame = [self frame];
238     frame.origin.x += BUTTON_LEFT_MARGIN;
239     frame.size.width -= BUTTON_LEFT_MARGIN;
240     frame.origin.y += BUTTON_TOP_MARGIN;
241     frame.size.height -= BUTTON_TOP_MARGIN + BUTTON_BOTTOM_MARGIN;
242     return frame;
243 }
244
245 - (void)setVisualFrame:(NSRect)frame
246 {
247     ASSERT([self superview] == nil || [[self superview] isFlipped]);
248     frame.origin.x -= BUTTON_LEFT_MARGIN;
249     frame.size.width += BUTTON_LEFT_MARGIN;
250     frame.origin.y -= BUTTON_TOP_MARGIN;
251     frame.size.height += BUTTON_TOP_MARGIN + BUTTON_BOTTOM_MARGIN;
252     [self setFrame:frame];
253 }
254
255 - (float)baseline
256 {
257     // Button text is centered vertically, with a fudge factor to account for the shadow.
258     ASSERT(_button);
259     NSFont *buttonFont = [_button font];
260     float ascender = [buttonFont ascender];
261     float descender = [buttonFont descender];
262     return -BUTTON_TOP_MARGIN
263         + ([[_button cell] cellSize].height + BUTTON_TOP_MARGIN + BUTTON_BOTTOM_MARGIN - (ascender - descender)) / 2.0
264         + ascender - BUTTON_VERTICAL_FUDGE_FACTOR;
265 }
266
267 - (void)beginSheet
268 {
269     WebCoreFrameBridge *bridge = FrameMac::bridgeForWidget(_widget);
270     [bridge retain];
271     [bridge runOpenPanelForFileButtonWithResultListener:self];
272 }
273
274 - (void)changeFilename:(NSString *)filename
275 {
276     // The != check here makes sure we don't consider a change from nil to nil as a change.
277     if (_filename != filename && ![_filename isEqualToString:filename]) {
278         [self setFilename:filename];
279         if (_widget)
280             _widget->filenameChanged(DeprecatedString::fromNSString(filename));
281     }
282 }
283
284 - (void)chooseFilename:(NSString *)filename
285 {
286     [self changeFilename:filename];
287     WebCoreFrameBridge *bridge = FrameMac::bridgeForWidget(_widget);
288     [bridge release];
289 }
290
291 - (void)cancel
292 {
293     WebCoreFrameBridge *bridge = FrameMac::bridgeForWidget(_widget);
294     [bridge release];
295 }
296
297 - (void)chooseButtonPressed:(id)sender
298 {
299     if (_widget)
300         _widget->sendConsumedMouseUp();
301     if (_widget && _widget->client())
302         _widget->client()->clicked(_widget);
303     [self beginSheet];
304 }
305
306 - (void)mouseDown:(NSEvent *)event
307 {
308     [self beginSheet];
309 }
310
311 - (BOOL)acceptsFirstResponder
312 {
313     return YES;
314 }
315
316 - (BOOL)becomeFirstResponder
317 {
318     BOOL become = [_button acceptsFirstResponder];
319     if (become) {
320         if (_widget && _widget->client() && !FrameMac::currentEventIsMouseDownInWidget(_widget))
321             _widget->client()->scrollToVisible(_widget);
322         if (_widget && _widget->client())
323             _widget->client()->focusIn(_widget);
324         [[self window] makeFirstResponder:_button];
325     }
326     return become;
327 }
328
329 - (NSView *)nextKeyView
330 {
331     return _inNextValidKeyView
332     ? FrameMac::nextKeyViewForWidget(_widget, KWQSelectingNext)
333     : [super nextKeyView];
334 }
335
336 - (NSView *)previousKeyView
337 {
338     return _inNextValidKeyView
339     ? FrameMac::nextKeyViewForWidget(_widget, KWQSelectingPrevious)
340     : [super previousKeyView];
341 }
342
343 - (NSView *)nextValidKeyView
344 {
345     _inNextValidKeyView = YES;
346     NSView *view = [super nextValidKeyView];
347     _inNextValidKeyView = NO;
348     return view;
349 }
350
351 - (NSView *)previousValidKeyView
352 {
353     _inNextValidKeyView = YES;
354     NSView *view = [super previousValidKeyView];
355     _inNextValidKeyView = NO;
356     return view;
357 }
358
359 - (void)performClick
360 {
361     [_button performClick:nil];
362 }
363
364 static NSString *validFilenameFromPasteboard(NSPasteboard* pBoard)
365 {
366     NSArray *filenames = [pBoard propertyListForType:NSFilenamesPboardType];
367     if ([filenames count] == 1) {
368         NSString *filename = [filenames objectAtIndex:0];
369         NSDictionary *fileAttributes = [[NSFileManager defaultManager] fileAttributesAtPath:filename traverseLink:YES];
370         if ([[fileAttributes fileType] isEqualToString:NSFileTypeRegular])
371             return filename;
372     }
373     return nil;
374 }
375
376 - (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender
377 {
378     if (validFilenameFromPasteboard([sender draggingPasteboard])) {
379         _isCurrentDragTarget = YES;
380         [self setNeedsDisplay:YES];
381         return NSDragOperationCopy;
382     }
383     return NSDragOperationNone;
384 }
385
386 - (void)draggingExited:(id <NSDraggingInfo>)sender
387 {
388     if (_isCurrentDragTarget) {
389         _isCurrentDragTarget = NO;
390         [self setNeedsDisplay:YES];
391     }
392 }
393
394 - (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
395 {
396     _isCurrentDragTarget = NO;
397     NSString *filename = validFilenameFromPasteboard([sender draggingPasteboard]);
398     if (filename) {
399         [self changeFilename:filename];
400         [self setNeedsDisplay:YES];
401         return YES;
402     }
403     return NO;
404 }
405
406 @end
407
408 @implementation WebFileChooserButton
409
410 - (id)initWithWidget:(KWQFileButton *)widget
411 {
412     [super init];
413     _widget = widget;
414     return self;
415 }
416
417 - (NSView *)nextValidKeyView
418 {
419     return [[self superview] nextValidKeyView];
420 }
421
422 - (NSView *)previousValidKeyView
423 {
424     return [[self superview] previousValidKeyView];
425 }
426
427 - (BOOL)resignFirstResponder
428 {
429     if (_widget && _widget->client())
430         _widget->client()->focusOut(_widget);
431     return YES;
432 }
433
434 @end
435
436
437 KWQFileButton::KWQFileButton(Frame *frame)
438 {
439     BEGIN_BLOCK_OBJC_EXCEPTIONS;
440
441     _buttonView = [[WebCoreFileButton alloc] initWithWidget:this];
442     setView(_buttonView);
443     [_buttonView release];
444
445     END_BLOCK_OBJC_EXCEPTIONS;
446 }
447
448 void KWQFileButton::setFilename(const DeprecatedString &f)
449 {
450     BEGIN_BLOCK_OBJC_EXCEPTIONS;
451     [_buttonView setFilename:f.getNSString()];
452     END_BLOCK_OBJC_EXCEPTIONS;
453 }
454
455 void KWQFileButton::click(bool sendMouseEvents)
456 {
457     BEGIN_BLOCK_OBJC_EXCEPTIONS;
458     [_buttonView performClick];
459     END_BLOCK_OBJC_EXCEPTIONS;
460 }
461
462 IntSize KWQFileButton::sizeForCharacterWidth(int characters) const
463 {
464     ASSERT(characters > 0);
465
466     NSSize size = {0,0};
467     BEGIN_BLOCK_OBJC_EXCEPTIONS;
468     size = [_buttonView bestVisualFrameSizeForCharacterCount:characters];
469     return IntSize(size);
470     END_BLOCK_OBJC_EXCEPTIONS;
471     return IntSize(0, 0);
472 }
473
474 IntRect KWQFileButton::frameGeometry() const
475 {
476     BEGIN_BLOCK_OBJC_EXCEPTIONS;
477     return enclosingIntRect([_buttonView visualFrame]);
478     END_BLOCK_OBJC_EXCEPTIONS;
479     return IntRect();
480 }
481
482 void KWQFileButton::setFrameGeometry(const IntRect &rect)
483 {
484     BEGIN_BLOCK_OBJC_EXCEPTIONS;
485     [_buttonView setVisualFrame:rect];
486     END_BLOCK_OBJC_EXCEPTIONS;
487 }
488
489 int KWQFileButton::baselinePosition(int height) const
490 {
491     BEGIN_BLOCK_OBJC_EXCEPTIONS;
492     return (int)([_buttonView frame].origin.y + [_buttonView baseline] - [_buttonView visualFrame].origin.y);
493     END_BLOCK_OBJC_EXCEPTIONS;
494
495     return 0;
496 }
497
498 Widget::FocusPolicy KWQFileButton::focusPolicy() const
499 {
500     BEGIN_BLOCK_OBJC_EXCEPTIONS;
501     
502     WebCoreFrameBridge *bridge = FrameMac::bridgeForWidget(this);
503     if (!bridge || ![bridge impl] || ![bridge impl]->tabsToAllControls()) {
504         return NoFocus;
505     }
506     
507     END_BLOCK_OBJC_EXCEPTIONS;
508     
509     return Widget::focusPolicy();
510 }
511
512 void KWQFileButton::filenameChanged(const DeprecatedString& filename)
513 {
514     m_name = filename;
515     if (client())
516         client()->valueChanged(this);
517 }