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