[iOS] Replace "node assistance" terminology in WebKit with "focused element"
[WebKit-https.git] / Source / WebKit / UIProcess / ios / forms / WKFormSelectPopover.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 "WKFormSelectPopover.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 #import <UIKit/UIPickerView.h>
38 #import <WebCore/LocalizedStrings.h>
39 #import <wtf/RetainPtr.h>
40
41 using namespace WebKit;
42
43 static NSString* WKPopoverTableViewCellReuseIdentifier  = @"WKPopoverTableViewCellReuseIdentifier";
44
45 @interface UITableViewCell (Internal)
46 - (CGRect)textRectForContentRect:(CGRect)contentRect;
47 - (CGRect)contentRectForBounds:(CGRect)bounds;
48 @end
49
50 ALLOW_DEPRECATED_DECLARATIONS_BEGIN
51 static NSString *stringWithWritingDirection(NSString *string, UITextWritingDirection writingDirection, bool override)
52 {
53     if (![string length] || writingDirection == UITextWritingDirectionNatural)
54         return string;
55     
56     if (!override) {
57         UCharDirection firstCharacterDirection = u_charDirection([string characterAtIndex:0]);
58         if ((firstCharacterDirection == U_LEFT_TO_RIGHT && writingDirection == UITextWritingDirectionLeftToRight)
59             || (firstCharacterDirection == U_RIGHT_TO_LEFT && writingDirection == UITextWritingDirectionRightToLeft))
60             return string;
61     }
62     
63     const unichar leftToRightEmbedding = 0x202A;
64     const unichar rightToLeftEmbedding = 0x202B;
65     const unichar popDirectionalFormatting = 0x202C;
66     const unichar leftToRightOverride = 0x202D;
67     const unichar rightToLeftOverride = 0x202E;
68     
69     unichar directionalFormattingCharacter;
70     if (writingDirection == UITextWritingDirectionLeftToRight)
71         directionalFormattingCharacter = (override ? leftToRightOverride : leftToRightEmbedding);
72     else
73         directionalFormattingCharacter = (override ? rightToLeftOverride : rightToLeftEmbedding);
74     
75     return [NSString stringWithFormat:@"%C%@%C", directionalFormattingCharacter, string, popDirectionalFormatting];
76 }
77 ALLOW_DEPRECATED_DECLARATIONS_END
78
79 @class WKSelectPopover;
80
81 @interface WKSelectTableViewController : UITableViewController <UIKeyInput>
82 {
83     NSUInteger _singleSelectionIndex;
84     NSUInteger _singleSelectionSection;
85     NSInteger _numberOfSections;
86     BOOL _allowsMultipleSelection;
87     
88     CGFloat _fontSize;
89     CGFloat _maximumTextWidth;
90     NSTextAlignment _textAlignment;
91     
92     WKSelectPopover *_popover;
93     WKContentView *_contentView;
94 }
95
96 @property(nonatomic,assign) WKSelectPopover *popover;
97 @end
98
99 @implementation WKSelectTableViewController
100
101 - (id)initWithView:(WKContentView *)view hasGroups:(BOOL)hasGroups
102 {
103     if (!(self = [super initWithStyle:UITableViewStylePlain]))
104         return nil;
105     
106     _contentView = view;
107     Vector<OptionItem>& selectOptions = [_contentView focusedSelectElementOptions];
108     _allowsMultipleSelection = _contentView.focusedElementInformation.isMultiSelect;
109     
110     // Even if the select is empty, there is at least one tableview section.
111     _numberOfSections = 1;
112     _singleSelectionIndex = NSNotFound;
113     NSInteger currentIndex = 0;
114     for (size_t i = 0; i < selectOptions.size(); ++i) {
115         const OptionItem& item = selectOptions[i];
116         if (item.isGroup) {
117             _numberOfSections++;
118             currentIndex = 0;
119             continue;
120         }
121         if (!_allowsMultipleSelection && item.isSelected) {
122             _singleSelectionIndex = currentIndex;
123             _singleSelectionSection = item.parentGroupID;
124         }
125         currentIndex++;
126     }
127
128     ALLOW_DEPRECATED_DECLARATIONS_BEGIN
129     UITextWritingDirection writingDirection = _contentView.focusedElementInformation.isRTL ? UITextWritingDirectionRightToLeft : UITextWritingDirectionLeftToRight;
130     BOOL override = NO;
131     _textAlignment = (writingDirection == UITextWritingDirectionLeftToRight) ? NSTextAlignmentLeft : NSTextAlignmentRight;
132
133     // Typically UIKit apps have their writing direction follow the system
134     // language. However WebKit wants to follow the content direction.
135     // For that reason we have to override what the system thinks.
136     if (writingDirection == UITextWritingDirectionRightToLeft)
137         self.view.semanticContentAttribute = UISemanticContentAttributeForceRightToLeft;
138     [self setTitle:stringWithWritingDirection(_contentView.focusedElementInformation.title, writingDirection, override)];
139     ALLOW_DEPRECATED_DECLARATIONS_END
140
141     return self;
142 }
143
144 - (void)viewWillAppear:(BOOL)animated
145 {
146     [super viewWillAppear:animated];
147
148     if (_singleSelectionIndex == NSNotFound)
149         return;
150
151     if (_singleSelectionSection >= (NSUInteger)[self.tableView numberOfSections])
152         return;
153
154     if (_singleSelectionIndex >= (NSUInteger)[self.tableView numberOfRowsInSection:_singleSelectionSection])
155         return;
156
157     NSIndexPath *indexPath = [NSIndexPath indexPathForRow:_singleSelectionIndex inSection:_singleSelectionSection];
158     [self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionMiddle animated:NO];
159 }
160
161 #pragma mark UITableView delegate methods
162
163 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
164 {
165     return _numberOfSections;
166 }
167
168 - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
169 {
170     if ([_contentView focusedSelectElementOptions].isEmpty())
171         return 1;
172     
173     int rowCount = 0;
174     for (size_t i = 0; i < [_contentView focusedSelectElementOptions].size(); ++i) {
175         const OptionItem& item = [_contentView focusedSelectElementOptions][i];
176         if (item.isGroup)
177             continue;
178         if (item.parentGroupID == section)
179             rowCount++;
180         if (item.parentGroupID > section)
181             break;
182     }
183     return rowCount;
184 }
185
186 - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
187 {
188     // The first section never has a header. It is for selects without groups.
189     if (section == 0)
190         return nil;
191     
192     int groupCount = 0;
193     for (size_t i = 0; i < [_contentView focusedSelectElementOptions].size(); ++i) {
194         const OptionItem& item = [_contentView focusedSelectElementOptions][i];
195         if (!item.isGroup)
196             continue;
197         groupCount++;
198         if (item.isGroup && groupCount == section)
199             return item.text;
200     }
201     return nil;
202 }
203
204 - (void)populateCell:(UITableViewCell *)cell withItem:(const OptionItem&)item
205 {
206     [cell.textLabel setText:item.text];
207     [cell.textLabel setEnabled:!item.disabled];
208     [cell setSelectionStyle:item.disabled ? UITableViewCellSelectionStyleNone : UITableViewCellSelectionStyleBlue];
209     [cell setAccessoryType:item.isSelected ? UITableViewCellAccessoryCheckmark : UITableViewCellAccessoryNone];
210 }
211
212 - (NSInteger)findItemIndexAt:(NSIndexPath *)indexPath
213 {
214     ASSERT(indexPath.row >= 0);
215     ASSERT(indexPath.section <= _numberOfSections);
216     
217     int optionIndex = 0;
218     int rowIndex = 0;
219     for (size_t i = 0; i < [_contentView focusedSelectElementOptions].size(); ++i) {
220         const OptionItem& item = [_contentView focusedSelectElementOptions][i];
221         if (item.isGroup) {
222             rowIndex = 0;
223             continue;
224         }
225         if (item.parentGroupID == indexPath.section && rowIndex == indexPath.row)
226             return optionIndex;
227         optionIndex++;
228         rowIndex++;
229     }
230     return NSNotFound;
231 }
232
233 - (OptionItem *)findItemAt:(NSIndexPath *)indexPath
234 {
235     ASSERT(indexPath.row >= 0);
236     ASSERT(indexPath.section <= _numberOfSections);
237
238     int index = 0;
239     for (size_t i = 0; i < [_contentView focusedSelectElementOptions].size(); ++i) {
240         OptionItem& item = [_contentView focusedSelectElementOptions][i];
241         if (item.isGroup || item.parentGroupID != indexPath.section)
242             continue;
243         if (index == indexPath.row)
244             return &item;
245         index++;
246     }
247     return nil;
248 }
249
250 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
251 {
252     UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:WKPopoverTableViewCellReuseIdentifier];
253     if (!cell)
254         cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:WKPopoverTableViewCellReuseIdentifier] autorelease];
255     
256     cell.semanticContentAttribute = self.view.semanticContentAttribute;
257     cell.textLabel.textAlignment = _textAlignment;
258     
259     if (_contentView.focusedElementInformation.selectOptions.isEmpty()) {
260         cell.textLabel.enabled = NO;
261         cell.textLabel.text = WEB_UI_STRING_KEY("No Options", "No Options Select Popover", "Empty select list");
262         cell.accessoryType = UITableViewCellAccessoryNone;
263         cell.selectionStyle = UITableViewCellSelectionStyleNone;
264         return cell;
265     }
266     
267     CGRect textRect = [cell textRectForContentRect:[cell contentRectForBounds:[cell bounds]]];
268     ASSERT(textRect.size.width > 0.0);
269     
270     // Assume all cells have the same available text width.
271     UIFont *font = cell.textLabel.font;
272     CGFloat initialFontSize = font.pointSize;
273     ASSERT(initialFontSize);
274     if (textRect.size.width != _maximumTextWidth || _fontSize == 0) {
275         _maximumTextWidth = textRect.size.width;
276         _fontSize = adjustedFontSize(_maximumTextWidth, font, initialFontSize, _contentView.focusedElementInformation.selectOptions);
277     }
278     
279     const OptionItem* item = [self findItemAt:indexPath];
280     ASSERT(item);
281     
282     [self populateCell:cell withItem:*item];
283     [cell.textLabel setFont:[font fontWithSize:_fontSize]];
284     [cell.textLabel setLineBreakMode:NSLineBreakByWordWrapping];
285     [cell.textLabel setNumberOfLines:2];
286     return cell;
287 }
288
289 - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
290 {
291     if (_contentView.focusedElementInformation.selectOptions.isEmpty())
292         return;
293     
294     NSInteger itemIndex = [self findItemIndexAt:indexPath];
295     ASSERT(itemIndex != NSNotFound);
296     
297     if (_allowsMultipleSelection) {
298         [tableView deselectRowAtIndexPath:[tableView indexPathForSelectedRow] animated:NO];
299         
300         UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
301         if (!cell.textLabel.enabled)
302             return;
303         
304         BOOL newStateIsSelected = (cell.accessoryType == UITableViewCellAccessoryNone);
305         
306         cell.accessoryType = newStateIsSelected ? UITableViewCellAccessoryCheckmark : UITableViewCellAccessoryNone;
307         
308         ASSERT(itemIndex != NSNotFound);
309         
310         // To trigger onchange events programmatically we need to go through this
311         // SPI which mimics a user action on the <select>. Normally programmatic
312         // changes do not trigger "change" events on such selects.
313     
314         [_contentView page]->setFocusedElementSelectedIndex(itemIndex, true);
315         OptionItem& item = [_contentView focusedSelectElementOptions][itemIndex];
316         item.isSelected = newStateIsSelected;
317     } else {
318         [tableView deselectRowAtIndexPath:indexPath animated:NO];
319         
320         // It is possible for there to be no selection, for example with <select size="2">.
321         NSIndexPath *oldIndexPath = nil;
322         if (_singleSelectionIndex != NSNotFound) {
323             oldIndexPath = [NSIndexPath indexPathForRow:_singleSelectionIndex inSection:_singleSelectionSection];
324             if ([indexPath isEqual:oldIndexPath]) {
325                 [_popover _userActionDismissedPopover:nil];
326                 return;
327             }
328         }
329         
330         UITableViewCell *newCell = [tableView cellForRowAtIndexPath:indexPath];
331         
332         if (!newCell.textLabel.enabled)
333             return;
334         
335         if (oldIndexPath) {
336             UITableViewCell *oldCell = [tableView cellForRowAtIndexPath:oldIndexPath];
337             if (oldCell && oldCell.accessoryType == UITableViewCellAccessoryCheckmark)
338                 oldCell.accessoryType = UITableViewCellAccessoryNone;
339         }
340         
341         if (newCell && newCell.accessoryType == UITableViewCellAccessoryNone) {
342             newCell.accessoryType = UITableViewCellAccessoryCheckmark;
343             
344             _singleSelectionIndex = indexPath.row;
345             _singleSelectionSection = indexPath.section;
346  
347             [_contentView page]->setFocusedElementSelectedIndex(itemIndex);
348             OptionItem& newItem = [_contentView focusedSelectElementOptions][itemIndex];
349             newItem.isSelected = true;
350         }
351         
352         // Need to update the model even if there isn't a cell.
353         if (oldIndexPath) {
354             if (OptionItem* oldItem = [self findItemAt:oldIndexPath])
355                 oldItem->isSelected = false;
356         }
357         
358         [_popover _userActionDismissedPopover:nil];
359     }
360 }
361
362 #pragma mark UIKeyInput delegate methods
363
364 - (BOOL)hasText
365 {
366     return NO;
367 }
368
369 - (void)insertText:(NSString *)text
370 {
371 }
372
373 - (void)deleteBackward
374 {
375 }
376
377 @end
378
379 @implementation WKSelectPopover {
380     RetainPtr<WKSelectTableViewController> _tableViewController;
381 }
382
383 - (instancetype)initWithView:(WKContentView *)view hasGroups:(BOOL)hasGroups
384 {
385     if (!(self = [super initWithView:view]))
386         return nil;
387     
388     CGRect frame;
389     frame.origin = CGPointZero;
390     frame.size = [UIKeyboard defaultSizeForInterfaceOrientation:[UIApp interfaceOrientation]];
391
392     _tableViewController = adoptNS([[WKSelectTableViewController alloc] initWithView:view hasGroups:hasGroups]);
393     [_tableViewController setPopover:self];
394     UIViewController *popoverViewController = _tableViewController.get();
395     UINavigationController *navController = nil;
396     BOOL needsNavigationController = !view.focusedElementInformation.title.isEmpty();
397     if (needsNavigationController) {
398         navController = [[UINavigationController alloc] initWithRootViewController:_tableViewController.get()];
399         popoverViewController = navController;
400     }
401     
402     CGSize popoverSize = [_tableViewController.get().tableView sizeThatFits:CGSizeMake(320, CGFLOAT_MAX)];
403     if (needsNavigationController)
404         [(UINavigationController *)popoverViewController topViewController].preferredContentSize = popoverSize;
405     else
406         popoverViewController.preferredContentSize = popoverSize;
407     
408     ALLOW_DEPRECATED_DECLARATIONS_BEGIN
409     self.popoverController = [[[UIPopoverController alloc] initWithContentViewController:popoverViewController] autorelease];
410     ALLOW_DEPRECATED_DECLARATIONS_END
411
412     [navController release];
413     
414     [[UIKeyboardImpl sharedInstance] setDelegate:_tableViewController.get()];
415     
416     return self;
417 }
418
419 - (void)dealloc
420 {
421     [_tableViewController setPopover:nil];
422     _tableViewController.get().tableView.dataSource = nil;
423     _tableViewController.get().tableView.delegate = nil;
424     
425     [super dealloc];
426 }
427
428 - (UIView *)controlView
429 {
430     return nil;
431 }
432
433 - (void)controlBeginEditing
434 {
435     [self presentPopoverAnimated:NO];
436 }
437
438 - (void)controlEndEditing
439 {
440 }
441
442 - (void)_userActionDismissedPopover:(id)sender
443 {
444     [self accessoryDone];
445 }
446
447 - (UITableViewController *)tableViewController
448 {
449     return _tableViewController.get();
450 }
451
452 @end
453
454 @implementation WKSelectPopover(WKTesting)
455
456 - (void)selectRow:(NSInteger)rowIndex inComponent:(NSInteger)componentIndex extendingSelection:(BOOL)extendingSelection
457 {
458     NSIndexPath *indexPath = [NSIndexPath indexPathForRow:rowIndex inSection:componentIndex];
459     [[_tableViewController tableView] selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionMiddle];
460     // Inform the delegate, since -selectRowAtIndexPath:... doesn't do that.
461     [_tableViewController tableView:[_tableViewController tableView] didSelectRowAtIndexPath:indexPath];
462 }
463
464 @end
465
466 #endif  // PLATFORM(IOS_FAMILY)