[iOS] iPhone should not allow selecting <optgroup> in <select multiple>
[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 static const float GroupOptionTextColorAlpha = 0.5;
46
47 @interface UIPickerView (UIPickerViewInternal)
48 - (BOOL)allowsMultipleSelection;
49 - (void)setAllowsMultipleSelection:(BOOL)aFlag;
50 - (UITableView*)tableViewForColumn:(NSInteger)column;
51 @end
52
53 @interface WKOptionPickerCell : UIPickerContentView {
54     BOOL _disabled;
55 }
56
57 @property(nonatomic) BOOL disabled;
58
59 - (instancetype)initWithOptionItem:(const OptionItem&)item;
60
61 @end
62
63 @implementation WKOptionPickerCell
64
65 - (BOOL)_isSelectable
66 {
67     return !self.disabled;
68 }
69
70 - (instancetype)init
71 {
72     if (!(self = [super initWithFrame:CGRectZero]))
73         return nil;
74     [[self titleLabel] setLineBreakMode:NSLineBreakByTruncatingMiddle];
75     return self;
76 }
77
78 - (instancetype)initWithOptionItem:(const OptionItem&)item
79 {
80     if (!(self = [self init]))
81         return nil;
82
83     NSMutableString *trimmedText = [[item.text mutableCopy] autorelease];
84     CFStringTrimWhitespace((CFMutableStringRef)trimmedText);
85
86     [[self titleLabel] setText:trimmedText];
87     [self setChecked:item.isSelected];
88     [self setDisabled:item.disabled];
89     if (_disabled)
90         [[self titleLabel] setTextColor:[UIColor colorWithWhite:0.0 alpha:DisabledOptionAlpha]];
91
92     return self;
93 }
94
95 @end
96
97
98 @interface WKOptionGroupPickerCell : WKOptionPickerCell
99 - (instancetype)initWithOptionItem:(const OptionItem&)item;
100 @end
101
102 @implementation WKOptionGroupPickerCell
103
104 - (instancetype)initWithOptionItem:(const OptionItem&)item
105 {
106     if (!(self = [self init]))
107         return nil;
108
109     NSMutableString *trimmedText = [[item.text mutableCopy] autorelease];
110     CFStringTrimWhitespace((CFMutableStringRef)trimmedText);
111
112     [[self titleLabel] setText:trimmedText];
113     [self setChecked:NO];
114     [[self titleLabel] setTextColor:[UIColor colorWithWhite:0.0 alpha:GroupOptionTextColorAlpha]];
115     [self setDisabled:YES];
116
117     return self;
118 }
119
120 - (CGFloat)labelWidthForBounds:(CGRect)bounds
121 {
122     return CGRectGetWidth(bounds) - [UIPickerContentView _checkmarkOffset];
123 }
124
125 - (void)layoutSubviews
126 {
127     if (!self.titleLabel)
128         return;
129
130     CGRect bounds = self.bounds;
131     self.titleLabel.frame = CGRectMake([UIPickerContentView _checkmarkOffset], 0, CGRectGetMaxX(bounds) - [UIPickerContentView _checkmarkOffset], CGRectGetHeight(bounds));
132 }
133
134 @end
135
136
137 @implementation WKMultipleSelectPicker {
138     WKContentView *_view;
139     NSTextAlignment _textAlignment;
140     NSUInteger _singleSelectionIndex;
141     bool _allowsMultipleSelection;
142     CGFloat _layoutWidth;
143     CGFloat _fontSize;
144     CGFloat _maximumTextWidth;
145 }
146
147 - (instancetype)initWithView:(WKContentView *)view
148 {
149     if (!(self = [super initWithFrame:CGRectZero]))
150         return nil;
151
152     _view = view;
153     _allowsMultipleSelection = _view.assistedNodeInformation.isMultiSelect;
154     _singleSelectionIndex = NSNotFound;
155     [self setAutoresizingMask:UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight];
156     [self setDataSource:self];
157     [self setDelegate:self];
158     [self _setUsesCheckedSelection:YES];
159
160     [self _setMagnifierEnabled:NO];
161
162     UITextWritingDirection writingDirection = UITextWritingDirectionLeftToRight;
163     // FIXME: retrieve from WebProcess writing direction.
164     _textAlignment = (writingDirection == UITextWritingDirectionLeftToRight) ? NSTextAlignmentLeft : NSTextAlignmentRight;
165
166     [self setAllowsMultipleSelection:_allowsMultipleSelection];
167     [self setSize:[UIKeyboard defaultSizeForInterfaceOrientation:[UIApp interfaceOrientation]]];
168     [self reloadAllComponents];
169
170     if (!_allowsMultipleSelection) {
171         const Vector<OptionItem>& selectOptions = [_view assistedNodeSelectOptions];
172         for (size_t i = 0; i < selectOptions.size(); ++i) {
173             const OptionItem& item = selectOptions[i];
174             if (item.isGroup)
175                 continue;
176
177             if (item.isSelected) {
178                 _singleSelectionIndex = i;
179                 [self selectRow:_singleSelectionIndex inComponent:0 animated:NO];
180                 break;
181             }
182         }
183     }
184
185     return self;
186 }
187
188 - (void)dealloc
189 {
190     [self setDataSource:nil];
191     [self setDelegate:nil];
192
193     [super dealloc];
194 }
195
196 - (UIView *)controlView
197 {
198     return self;
199 }
200
201 - (void)controlBeginEditing
202 {
203 }
204
205 - (void)controlEndEditing
206 {
207 }
208
209 - (void)layoutSubviews
210 {
211     [super layoutSubviews];
212     if (_singleSelectionIndex != NSNotFound) {
213         [self selectRow:_singleSelectionIndex inComponent:0 animated:NO];
214     }
215
216     // Make sure all rows are sized properly after a rotation.
217     if (_layoutWidth != self.frame.size.width) {
218         [self reloadAllComponents];
219         _layoutWidth = self.frame.size.width;
220     }
221 }
222
223 - (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)rowIndex forComponent:(NSInteger)columnIndex reusingView:(UIView *)view
224 {
225     const OptionItem& item = [_view assistedNodeSelectOptions][rowIndex];
226     UIPickerContentView* pickerItem = item.isGroup ? [[[WKOptionGroupPickerCell alloc] initWithOptionItem:item] autorelease] : [[[WKOptionPickerCell alloc] initWithOptionItem:item] autorelease];
227
228     // The cell starts out with a null frame. We need to set its frame now so we can find the right font size.
229     UITableView *table = [pickerView tableViewForColumn:0];
230     CGRect frame = [table rectForRowAtIndexPath:[NSIndexPath indexPathForRow:rowIndex inSection:0]];
231     pickerItem.frame = frame;
232
233     UILabel *titleTextLabel = pickerItem.titleLabel;
234     float width = [pickerItem labelWidthForBounds:CGRectMake(0, 0, CGRectGetWidth(frame), CGRectGetHeight(frame))];
235     ASSERT(width > 0);
236
237     // Assume all cells have the same available text width.
238     UIFont *font = titleTextLabel.font;
239     if (width != _maximumTextWidth || _fontSize == 0) {
240         _maximumTextWidth = width;
241         _fontSize = adjustedFontSize(_maximumTextWidth, font, titleTextLabel.font.pointSize, [_view assistedNodeSelectOptions]);
242     }
243
244     [titleTextLabel setFont:[font fontWithSize:_fontSize]];
245     [titleTextLabel setLineBreakMode:NSLineBreakByWordWrapping];
246     [titleTextLabel setNumberOfLines:2];
247     [titleTextLabel setTextAlignment:_textAlignment];
248
249     return pickerItem;
250 }
251
252 - (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)aPickerView
253 {
254     return 1;
255 }
256
257 - (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)columnIndex
258 {
259     return [_view assistedNodeSelectOptions].size();
260 }
261
262 - (NSInteger)findItemIndexAt:(int)rowIndex
263 {
264     ASSERT(rowIndex >= 0 && (size_t)rowIndex < [_view assistedNodeSelectOptions].size());
265     NSInteger itemIndex = 0;
266     for (int i = 0; i < rowIndex; ++i) {
267         if ([_view assistedNodeSelectOptions][i].isGroup)
268             continue;
269         itemIndex++;
270     }
271
272     ASSERT(itemIndex >= 0);
273     return itemIndex;
274 }
275
276 - (void)pickerView:(UIPickerView *)pickerView row:(int)rowIndex column:(int)columnIndex checked:(BOOL)isChecked
277 {
278     if ((size_t)rowIndex >= [_view assistedNodeSelectOptions].size())
279         return;
280
281     OptionItem& item = [_view assistedNodeSelectOptions][rowIndex];
282
283     // FIXME: Remove this workaround once <rdar://problem/18745253> is fixed.
284     // Group rows should not be checkable, but we are getting this delegate for
285     // those rows. As a workaround, if we get this delegate for a group row, reset
286     // the styles for the content view so it still appears unselected.
287     if (item.isGroup) {
288         UIPickerContentView *view = (UIPickerContentView *)[self viewForRow:rowIndex forComponent:columnIndex];
289         [view setChecked:NO];
290         [[view titleLabel] setTextColor:[UIColor colorWithWhite:0.0 alpha:GroupOptionTextColorAlpha]];
291         return;
292     }
293
294     if ([self allowsMultipleSelection]) {
295         [_view page]->setAssistedNodeSelectedIndex([self findItemIndexAt:rowIndex], true);
296         item.isSelected = isChecked;
297     } else {
298         // Single selection.
299         item.isSelected = NO;
300         _singleSelectionIndex = rowIndex;
301
302         // This private delegate often gets called for multiple rows in the picker,
303         // so we only activate and set as selected the checked item in single selection.
304         if (isChecked) {
305             [_view page]->setAssistedNodeSelectedIndex([self findItemIndexAt:rowIndex]);
306             item.isSelected = YES;
307         }
308     }
309 }
310
311 @end
312
313 @implementation WKSelectSinglePicker {
314     WKContentView *_view;
315     NSInteger _selectedIndex;
316 }
317
318 - (instancetype)initWithView:(WKContentView *)view
319 {
320     if (!(self = [super initWithFrame:CGRectZero]))
321         return nil;
322
323     _view = view;
324     [self setDelegate:self];
325     [self setDataSource:self];
326     [self setAutoresizingMask:UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight];
327
328     _selectedIndex = NSNotFound;
329
330     for (size_t i = 0; i < [view assistedNodeSelectOptions].size(); ++i) {
331         if ([_view assistedNodeSelectOptions][i].isSelected) {
332             _selectedIndex = i;
333             break;
334         }
335     }
336
337     [self reloadAllComponents];
338
339     if (_selectedIndex != NSNotFound)
340         [self selectRow:_selectedIndex inComponent:0 animated:NO];
341
342     return self;
343 }
344
345 - (void)dealloc
346 {
347     [self setDelegate:nil];
348     [self setDataSource:nil];
349
350     [super dealloc];
351 }
352
353 - (UIView *)controlView
354 {
355     return self;
356 }
357
358 - (void)controlBeginEditing
359 {
360 }
361
362 - (void)controlEndEditing
363 {
364     if (_selectedIndex == NSNotFound)
365         return;
366
367     if (_selectedIndex < (NSInteger)[_view assistedNodeSelectOptions].size()) {
368         [_view assistedNodeSelectOptions][_selectedIndex].isSelected = true;
369         [_view page]->setAssistedNodeSelectedIndex(_selectedIndex);
370     }
371 }
372
373 - (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView
374 {
375     return 1;
376 }
377
378 - (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)columnIndex
379 {
380     return _view.assistedNodeInformation.selectOptions.size();
381 }
382
383 - (NSAttributedString *)pickerView:(UIPickerView *)pickerView attributedTitleForRow:(NSInteger)row forComponent:(NSInteger)component
384 {
385     if (row < 0 || row >= (NSInteger)[_view assistedNodeSelectOptions].size())
386         return nil;
387
388     const OptionItem& option = [_view assistedNodeSelectOptions][row];
389     NSMutableString *trimmedText = [[option.text mutableCopy] autorelease];
390     CFStringTrimWhitespace((CFMutableStringRef)trimmedText);
391
392     NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:trimmedText];
393     if (option.disabled)
394         [attributedString addAttribute:NSForegroundColorAttributeName value:[UIColor colorWithWhite:0.0 alpha:DisabledOptionAlpha] range:NSMakeRange(0, [trimmedText length])];
395
396     return [attributedString autorelease];
397 }
398
399 - (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component
400 {
401     if (row < 0 || row >= (NSInteger)[_view assistedNodeSelectOptions].size())
402         return;
403
404     const OptionItem& newSelectedOption = [_view assistedNodeSelectOptions][row];
405     if (newSelectedOption.disabled) {
406         NSInteger rowToSelect = NSNotFound;
407
408         // Search backwards for the previous enabled option.
409         for (NSInteger i = row - 1; i >= 0; --i) {
410             const OptionItem& earlierOption = [_view assistedNodeSelectOptions][i];
411             if (!earlierOption.disabled) {
412                 rowToSelect = i;
413                 break;
414             }
415         }
416
417         // If nothing previous, search forwards for the next enabled option.
418         if (rowToSelect == NSNotFound) {
419             for (size_t i = row + 1; i < [_view assistedNodeSelectOptions].size(); ++i) {
420                 const OptionItem& laterOption = [_view assistedNodeSelectOptions][i];
421                 if (!laterOption.disabled) {
422                     rowToSelect = i;
423                     break;
424                 }
425             }
426         }
427
428         if (rowToSelect == NSNotFound)
429             return;
430
431         [self selectRow:rowToSelect inComponent:0 animated:YES];
432         row = rowToSelect;
433     }
434
435     _selectedIndex = row;
436 }
437
438 @end
439
440 #endif  // PLATFORM(IOS)