75680266a5636971a5e9a1b5a422350961e63f1f
[WebKit-https.git] / Source / WebKit2 / UIProcess / ios / forms / WKFormSelectPicker.mm
1 /*
2  * Copyright (C) 2014 Apple 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 INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #import "config.h"
27 #import "WKFormSelectControl.h"
28
29 #if PLATFORM(IOS)
30
31 #import "WKContentView.h"
32 #import "WKContentViewInteraction.h"
33 #import "WKFormPopover.h"
34 #import "WebPageProxy.h"
35 #import <CoreFoundation/CFUniChar.h>
36 #import <UIKit/UIApplication_Private.h>
37 #import <UIKit/UIDevice_Private.h>
38 #import <UIKit/UIKeyboard_Private.h>
39 #import <UIKit/UIPickerContentView_Private.h>
40 #import <wtf/RetainPtr.h>
41
42 using namespace WebKit;
43
44 static const float DisabledOptionAlpha = 0.3;
45
46 @interface UIPickerView (UIPickerViewInternal)
47 - (BOOL)allowsMultipleSelection;
48 - (void)setAllowsMultipleSelection:(BOOL)aFlag;
49 - (UITableView*)tableViewForColumn:(NSInteger)column;
50 @end
51
52 @interface WKOptionPickerCell : UIPickerContentView {
53     BOOL _disabled;
54 }
55
56 @property(nonatomic) BOOL disabled;
57
58 - (instancetype)initWithOptionItem:(const OptionItem&)item;
59
60 @end
61
62 @implementation WKOptionPickerCell
63
64 - (BOOL)_isSelectable
65 {
66     return !self.disabled;
67 }
68
69 - (instancetype)init
70 {
71     if (!(self = [super initWithFrame:CGRectZero]))
72         return nil;
73     [[self titleLabel] setLineBreakMode:NSLineBreakByTruncatingMiddle];
74     return self;
75 }
76
77 - (instancetype)initWithOptionItem:(const OptionItem&)item
78 {
79     if (!(self = [self init]))
80         return nil;
81
82     NSMutableString *trimmedText = [[item.text mutableCopy] autorelease];
83     CFStringTrimWhitespace((CFMutableStringRef)trimmedText);
84
85     [[self titleLabel] setText:trimmedText];
86     [self setChecked:item.isSelected];
87     [self setDisabled:item.disabled];
88     if (_disabled)
89         [[self titleLabel] setTextColor:[UIColor colorWithWhite:0.0 alpha:DisabledOptionAlpha]];
90
91     return self;
92 }
93
94 @end
95
96
97 @interface WKOptionGroupPickerCell : WKOptionPickerCell
98 - (instancetype)initWithOptionItem:(const OptionItem&)item;
99 @end
100
101 @implementation WKOptionGroupPickerCell
102
103 - (instancetype)initWithOptionItem:(const OptionItem&)item
104 {
105     if (!(self = [self init]))
106         return nil;
107
108     NSMutableString *trimmedText = [[item.text mutableCopy] autorelease];
109     CFStringTrimWhitespace((CFMutableStringRef)trimmedText);
110
111     [[self titleLabel] setText:trimmedText];
112     [self setChecked:NO];
113     [[self titleLabel] setTextColor:[UIColor colorWithWhite:0.0 alpha:0.5]];
114     [self setDisabled:YES];
115
116     return self;
117 }
118
119 - (CGFloat)labelWidthForBounds:(CGRect)bounds
120 {
121     return CGRectGetWidth(bounds) - [UIPickerContentView _checkmarkOffset];
122 }
123
124 - (void)layoutSubviews
125 {
126     if (!self.titleLabel)
127         return;
128
129     CGRect bounds = self.bounds;
130     self.titleLabel.frame = CGRectMake([UIPickerContentView _checkmarkOffset], 0, CGRectGetMaxX(bounds) - [UIPickerContentView _checkmarkOffset], CGRectGetHeight(bounds));
131 }
132
133 @end
134
135
136 @implementation WKMultipleSelectPicker {
137     WKContentView *_view;
138     NSTextAlignment _textAlignment;
139     NSUInteger _singleSelectionIndex;
140     bool _allowsMultipleSelection;
141     CGFloat _layoutWidth;
142     CGFloat _fontSize;
143     CGFloat _maximumTextWidth;
144 }
145
146 - (instancetype)initWithView:(WKContentView *)view
147 {
148     if (!(self = [super initWithFrame:CGRectZero]))
149         return nil;
150
151     _view = view;
152     _allowsMultipleSelection = _view.assistedNodeInformation.isMultiSelect;
153     _singleSelectionIndex = NSNotFound;
154     [self setAutoresizingMask:UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight];
155     [self setDataSource:self];
156     [self setDelegate:self];
157     [self _setUsesCheckedSelection:YES];
158
159     [self _setMagnifierEnabled:NO];
160
161     UITextWritingDirection writingDirection = UITextWritingDirectionLeftToRight;
162     // FIXME: retrieve from WebProcess writing direction.
163     _textAlignment = (writingDirection == UITextWritingDirectionLeftToRight) ? NSTextAlignmentLeft : NSTextAlignmentRight;
164
165     [self setAllowsMultipleSelection:_allowsMultipleSelection];
166     [self setSize:[UIKeyboard defaultSizeForInterfaceOrientation:[UIApp interfaceOrientation]]];
167     [self reloadAllComponents];
168
169     if (!_allowsMultipleSelection) {
170         const Vector<OptionItem>& selectOptions = [_view assistedNodeSelectOptions];
171         for (size_t i = 0; i < selectOptions.size(); ++i) {
172             const OptionItem& item = selectOptions[i];
173             if (item.isGroup)
174                 continue;
175
176             if (item.isSelected) {
177                 _singleSelectionIndex = i;
178                 [self selectRow:_singleSelectionIndex inComponent:0 animated:NO];
179                 break;
180             }
181         }
182     }
183
184     return self;
185 }
186
187 - (void)dealloc
188 {
189     [self setDataSource:nil];
190     [self setDelegate:nil];
191
192     [super dealloc];
193 }
194
195 - (UIView *)controlView
196 {
197     return self;
198 }
199
200 - (void)controlBeginEditing
201 {
202 }
203
204 - (void)controlEndEditing
205 {
206 }
207
208 - (void)layoutSubviews
209 {
210     [super layoutSubviews];
211     if (_singleSelectionIndex != NSNotFound) {
212         [self selectRow:_singleSelectionIndex inComponent:0 animated:NO];
213     }
214
215     // Make sure all rows are sized properly after a rotation.
216     if (_layoutWidth != self.frame.size.width) {
217         [self reloadAllComponents];
218         _layoutWidth = self.frame.size.width;
219     }
220 }
221
222 - (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)rowIndex forComponent:(NSInteger)columnIndex reusingView:(UIView *)view
223 {
224     const OptionItem& item = [_view assistedNodeSelectOptions][rowIndex];
225     UIPickerContentView* pickerItem = item.isGroup ? [[[WKOptionGroupPickerCell alloc] initWithOptionItem:item] autorelease] : [[[WKOptionPickerCell alloc] initWithOptionItem:item] autorelease];
226
227     // The cell starts out with a null frame. We need to set its frame now so we can find the right font size.
228     UITableView *table = [pickerView tableViewForColumn:0];
229     CGRect frame = [table rectForRowAtIndexPath:[NSIndexPath indexPathForRow:rowIndex inSection:0]];
230     pickerItem.frame = frame;
231
232     UILabel *titleTextLabel = pickerItem.titleLabel;
233     float width = [pickerItem labelWidthForBounds:CGRectMake(0, 0, CGRectGetWidth(frame), CGRectGetHeight(frame))];
234     ASSERT(width > 0);
235
236     // Assume all cells have the same available text width.
237     UIFont *font = titleTextLabel.font;
238     if (width != _maximumTextWidth || _fontSize == 0) {
239         _maximumTextWidth = width;
240         _fontSize = adjustedFontSize(_maximumTextWidth, font, titleTextLabel.font.pointSize, [_view assistedNodeSelectOptions]);
241     }
242
243     [titleTextLabel setFont:[font fontWithSize:_fontSize]];
244     [titleTextLabel setLineBreakMode:NSLineBreakByWordWrapping];
245     [titleTextLabel setNumberOfLines:2];
246     [titleTextLabel setTextAlignment:_textAlignment];
247
248     return pickerItem;
249 }
250
251 - (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)aPickerView
252 {
253     return 1;
254 }
255
256 - (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)columnIndex
257 {
258     return [_view assistedNodeSelectOptions].size();
259 }
260
261 - (NSInteger)findItemIndexAt:(int)rowIndex
262 {
263     ASSERT(rowIndex >= 0 && (size_t)rowIndex < [_view assistedNodeSelectOptions].size());
264     NSInteger itemIndex = 0;
265     for (int i = 0; i < rowIndex; ++i) {
266         if ([_view assistedNodeSelectOptions][i].isGroup)
267             continue;
268         itemIndex++;
269     }
270
271     ASSERT(itemIndex >= 0);
272     return itemIndex;
273 }
274
275 - (void)pickerView:(UIPickerView *)pickerView row:(int)rowIndex column:(int)columnIndex checked:(BOOL)isChecked
276 {
277     if ((size_t)rowIndex >= [_view assistedNodeSelectOptions].size())
278         return;
279
280     OptionItem& item = [_view assistedNodeSelectOptions][rowIndex];
281
282     if ([self allowsMultipleSelection]) {
283         [_view page]->setAssistedNodeSelectedIndex([self findItemIndexAt:rowIndex], true);
284         item.isSelected = isChecked;
285     } else {
286         // Single selection.
287         item.isSelected = NO;
288         _singleSelectionIndex = rowIndex;
289
290         // This private delegate often gets called for multiple rows in the picker,
291         // so we only activate and set as selected the checked item in single selection.
292         if (isChecked) {
293             [_view page]->setAssistedNodeSelectedIndex([self findItemIndexAt:rowIndex]);
294             item.isSelected = YES;
295         }
296     }
297 }
298
299 @end
300
301 @implementation WKSelectSinglePicker {
302     WKContentView *_view;
303     NSInteger _selectedIndex;
304 }
305
306 - (instancetype)initWithView:(WKContentView *)view
307 {
308     if (!(self = [super initWithFrame:CGRectZero]))
309         return nil;
310
311     _view = view;
312     [self setDelegate:self];
313     [self setDataSource:self];
314     [self setAutoresizingMask:UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight];
315
316     _selectedIndex = NSNotFound;
317
318     for (size_t i = 0; i < [view assistedNodeSelectOptions].size(); ++i) {
319         if ([_view assistedNodeSelectOptions][i].isSelected) {
320             _selectedIndex = i;
321             break;
322         }
323     }
324
325     [self reloadAllComponents];
326
327     if (_selectedIndex != NSNotFound)
328         [self selectRow:_selectedIndex inComponent:0 animated:NO];
329
330     return self;
331 }
332
333 - (void)dealloc
334 {
335     [self setDelegate:nil];
336     [self setDataSource:nil];
337
338     [super dealloc];
339 }
340
341 - (UIView *)controlView
342 {
343     return self;
344 }
345
346 - (void)controlBeginEditing
347 {
348 }
349
350 - (void)controlEndEditing
351 {
352     if (_selectedIndex == NSNotFound)
353         return;
354
355     if (_selectedIndex < (NSInteger)[_view assistedNodeSelectOptions].size()) {
356         [_view assistedNodeSelectOptions][_selectedIndex].isSelected = true;
357         [_view page]->setAssistedNodeSelectedIndex(_selectedIndex);
358     }
359 }
360
361 - (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView
362 {
363     return 1;
364 }
365
366 - (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)columnIndex
367 {
368     return _view.assistedNodeInformation.selectOptions.size();
369 }
370
371 - (NSAttributedString *)pickerView:(UIPickerView *)pickerView attributedTitleForRow:(NSInteger)row forComponent:(NSInteger)component
372 {
373     if (row < 0 || row >= (NSInteger)[_view assistedNodeSelectOptions].size())
374         return nil;
375
376     const OptionItem& option = [_view assistedNodeSelectOptions][row];
377     NSMutableString *trimmedText = [[option.text mutableCopy] autorelease];
378     CFStringTrimWhitespace((CFMutableStringRef)trimmedText);
379
380     NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:trimmedText];
381     if (option.disabled)
382         [attributedString addAttribute:NSForegroundColorAttributeName value:[UIColor colorWithWhite:0.0 alpha:DisabledOptionAlpha] range:NSMakeRange(0, [trimmedText length])];
383
384     return [attributedString autorelease];
385 }
386
387 - (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component
388 {
389     if (row < 0 || row >= (NSInteger)[_view assistedNodeSelectOptions].size())
390         return;
391
392     const OptionItem& newSelectedOption = [_view assistedNodeSelectOptions][row];
393     if (newSelectedOption.disabled) {
394         NSInteger rowToSelect = NSNotFound;
395
396         // Search backwards for the previous enabled option.
397         for (NSInteger i = row - 1; i >= 0; --i) {
398             const OptionItem& earlierOption = [_view assistedNodeSelectOptions][i];
399             if (!earlierOption.disabled) {
400                 rowToSelect = i;
401                 break;
402             }
403         }
404
405         // If nothing previous, search forwards for the next enabled option.
406         if (rowToSelect == NSNotFound) {
407             for (size_t i = row + 1; i < [_view assistedNodeSelectOptions].size(); ++i) {
408                 const OptionItem& laterOption = [_view assistedNodeSelectOptions][i];
409                 if (!laterOption.disabled) {
410                     rowToSelect = i;
411                     break;
412                 }
413             }
414         }
415
416         if (rowToSelect == NSNotFound)
417             return;
418
419         [self selectRow:rowToSelect inComponent:0 animated:YES];
420         row = rowToSelect;
421     }
422
423     _selectedIndex = row;
424 }
425
426 @end
427
428 #endif  // PLATFORM(IOS)