WKFormSelectPicker.mm   [plain text]

/*
* Copyright (C) 2014 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/

#import "config.h"
#import "WKFormSelectPicker.h"

#if PLATFORM(IOS_FAMILY)

#import "UIKitSPI.h"
#import "WKContentView.h"
#import "WKContentViewInteraction.h"
#import "WKFormPopover.h"
#import "WKFormSelectControl.h"
#import "WebPageProxy.h"

using namespace WebKit;

static const float DisabledOptionAlpha = 0.3;
static const float GroupOptionTextColorAlpha = 0.5;

@interface UIPickerView (UIPickerViewInternal)
- (BOOL)allowsMultipleSelection;
- (void)setAllowsMultipleSelection:(BOOL)aFlag;
- (UITableView*)tableViewForColumn:(NSInteger)column;
@end

@interface WKOptionPickerCell : UIPickerContentView {
BOOL _disabled;
}

@property(nonatomic) BOOL disabled;

- (instancetype)initWithOptionItem:(const OptionItem&)item;

@end

@implementation WKOptionPickerCell

- (BOOL)_isSelectable
{
return !self.disabled;
}

- (instancetype)init
{
if (!(self = [super initWithFrame:CGRectZero]))
return nil;
[[self titleLabel] setLineBreakMode:NSLineBreakByTruncatingMiddle];
return self;
}

- (instancetype)initWithOptionItem:(const OptionItem&)item
{
if (!(self = [self init]))
return nil;

NSMutableString *trimmedText = [[item.text mutableCopy] autorelease];
CFStringTrimWhitespace((CFMutableStringRef)trimmedText);

[[self titleLabel] setText:trimmedText];
[self setChecked:item.isSelected];
[self setDisabled:item.disabled];
if (_disabled)
[[self titleLabel] setTextColor:[UIColor colorWithWhite:0.0 alpha:DisabledOptionAlpha]];

return self;
}

@end

@interface WKOptionGroupPickerCell : WKOptionPickerCell
- (instancetype)initWithOptionItem:(const OptionItem&)item;
@end

@implementation WKOptionGroupPickerCell

- (instancetype)initWithOptionItem:(const OptionItem&)item
{
if (!(self = [self init]))
return nil;

NSMutableString *trimmedText = [[item.text mutableCopy] autorelease];
CFStringTrimWhitespace((CFMutableStringRef)trimmedText);

[[self titleLabel] setText:trimmedText];
[self setChecked:NO];
[[self titleLabel] setTextColor:[UIColor colorWithWhite:0.0 alpha:GroupOptionTextColorAlpha]];
[self setDisabled:YES];

return self;
}

- (CGFloat)labelWidthForBounds:(CGRect)bounds
{
return CGRectGetWidth(bounds) - [UIPickerContentView _checkmarkOffset];
}

- (void)layoutSubviews
{
if (!self.titleLabel)
return;

CGRect bounds = self.bounds;
self.titleLabel.frame = CGRectMake([UIPickerContentView _checkmarkOffset], 0, CGRectGetMaxX(bounds) - [UIPickerContentView _checkmarkOffset], CGRectGetHeight(bounds));
}

@end

@implementation WKMultipleSelectPicker {
WKContentView *_view;
NSTextAlignment _textAlignment;
NSUInteger _singleSelectionIndex;
bool _allowsMultipleSelection;
CGFloat _layoutWidth;
CGFloat _fontSize;
CGFloat _maximumTextWidth;
}

- (instancetype)initWithView:(WKContentView *)view
{
if (!(self = [super initWithFrame:CGRectZero]))
return nil;

_view = view;
_allowsMultipleSelection = _view.focusedElementInformation.isMultiSelect;
_singleSelectionIndex = NSNotFound;
[self setAutoresizingMask:UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight];
[self setDataSource:self];
[self setDelegate:self];
[self _setUsesCheckedSelection:YES];

[self _setMagnifierEnabled:NO];
ALLOW_DEPRECATED_DECLARATIONS_BEGIN
UITextWritingDirection writingDirection = UITextWritingDirectionLeftToRight;
// FIXME: retrieve from WebProcess writing direction.
_textAlignment = (writingDirection == UITextWritingDirectionLeftToRight) ? NSTextAlignmentLeft : NSTextAlignmentRight;
ALLOW_DEPRECATED_DECLARATIONS_END

[self setAllowsMultipleSelection:_allowsMultipleSelection];
[self setSize:[UIKeyboard defaultSizeForInterfaceOrientation:view.interfaceOrientation]];
[self reloadAllComponents];

if (!_allowsMultipleSelection) {
const Vector<OptionItem>& selectOptions = [_view focusedSelectElementOptions];
for (size_t i = 0; i < selectOptions.size(); ++i) {
const OptionItem& item = selectOptions[i];
if (item.isGroup)
continue;

if (item.isSelected) {
_singleSelectionIndex = i;
[self selectRow:_singleSelectionIndex inComponent:0 animated:NO];
break;
}
}
}

return self;
}

- (void)dealloc
{
[self setDataSource:nil];
[self setDelegate:nil];

[super dealloc];
}

- (UIView *)controlView
{
return self;
}

- (void)controlBeginEditing
{
}

- (void)controlEndEditing
{
}

- (void)layoutSubviews
{
[super layoutSubviews];
if (_singleSelectionIndex != NSNotFound) {
[self selectRow:_singleSelectionIndex inComponent:0 animated:NO];
}

// Make sure all rows are sized properly after a rotation.
if (_layoutWidth != self.frame.size.width) {
[self reloadAllComponents];
_layoutWidth = self.frame.size.width;
}
}

- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)rowIndex forComponent:(NSInteger)columnIndex reusingView:(UIView *)view
{
const OptionItem& item = [_view focusedSelectElementOptions][rowIndex];
UIPickerContentView* pickerItem = item.isGroup ? [[[WKOptionGroupPickerCell alloc] initWithOptionItem:item] autorelease] : [[[WKOptionPickerCell alloc] initWithOptionItem:item] autorelease];

// The cell starts out with a null frame. We need to set its frame now so we can find the right font size.
UITableView *table = [pickerView tableViewForColumn:0];
CGRect frame = [table rectForRowAtIndexPath:[NSIndexPath indexPathForRow:rowIndex inSection:0]];
pickerItem.frame = frame;

UILabel *titleTextLabel = pickerItem.titleLabel;
float width = [pickerItem labelWidthForBounds:CGRectMake(0, 0, CGRectGetWidth(frame), CGRectGetHeight(frame))];
ASSERT(width > 0);

// Assume all cells have the same available text width.
UIFont *font = titleTextLabel.font;
if (width != _maximumTextWidth || _fontSize == 0) {
_maximumTextWidth = width;
_fontSize = adjustedFontSize(_maximumTextWidth, font, titleTextLabel.font.pointSize, [_view focusedSelectElementOptions]);
}

[titleTextLabel setFont:[font fontWithSize:_fontSize]];
[titleTextLabel setLineBreakMode:NSLineBreakByWordWrapping];
[titleTextLabel setNumberOfLines:2];
[titleTextLabel setTextAlignment:_textAlignment];

return pickerItem;
}

- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)aPickerView
{
return 1;
}

- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)columnIndex
{
return [_view focusedSelectElementOptions].size();
}

- (NSInteger)findItemIndexAt:(int)rowIndex
{
ASSERT(rowIndex >= 0 && (size_t)rowIndex < [_view focusedSelectElementOptions].size());
NSInteger itemIndex = 0;
for (int i = 0; i < rowIndex; ++i) {
if ([_view focusedSelectElementOptions][i].isGroup)
continue;
itemIndex++;
}

ASSERT(itemIndex >= 0);
return itemIndex;
}

- (void)pickerView:(UIPickerView *)pickerView row:(int)rowIndex column:(int)columnIndex checked:(BOOL)isChecked
{
if ((size_t)rowIndex >= [_view focusedSelectElementOptions].size())
return;

OptionItem& item = [_view focusedSelectElementOptions][rowIndex];

// FIXME: Remove this workaround once <rdar://problem/18745253> is fixed.
// Group rows should not be checkable, but we are getting this delegate for
// those rows. As a workaround, if we get this delegate for a group row, reset
// the styles for the content view so it still appears unselected.
if (item.isGroup) {
UIPickerContentView *view = (UIPickerContentView *)[self viewForRow:rowIndex forComponent:columnIndex];
[view setChecked:NO];
[[view titleLabel] setTextColor:[UIColor colorWithWhite:0.0 alpha:GroupOptionTextColorAlpha]];
return;
}

if ([self allowsMultipleSelection]) {
[_view page]->setFocusedElementSelectedIndex([self findItemIndexAt:rowIndex], true);
item.isSelected = isChecked;
} else {
// Single selection.
item.isSelected = NO;
_singleSelectionIndex = rowIndex;

// This private delegate often gets called for multiple rows in the picker,
// so we only activate and set as selected the checked item in single selection.
if (isChecked) {
[_view page]->setFocusedElementSelectedIndex([self findItemIndexAt:rowIndex]);
item.isSelected = YES;
}
}
}

// WKSelectTesting
- (void)selectRow:(NSInteger)rowIndex inComponent:(NSInteger)componentIndex extendingSelection:(BOOL)extendingSelection
{
// FIXME: handle extendingSelection.
[self selectRow:rowIndex inComponent:0 animated:NO];
// Progammatic selection changes don't call the delegate, so do that manually.
[self.delegate pickerView:self didSelectRow:rowIndex inComponent:0];
}

@end

@implementation WKSelectSinglePicker {
WKContentView *_view;
NSInteger _selectedIndex;
}

- (instancetype)initWithView:(WKContentView *)view
{
if (!(self = [super initWithFrame:CGRectZero]))
return nil;

_view = view;
[self setDelegate:self];
[self setDataSource:self];
[self setAutoresizingMask:UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight];

_selectedIndex = NSNotFound;

for (size_t i = 0; i < [view focusedSelectElementOptions].size(); ++i) {
if ([_view focusedSelectElementOptions][i].isSelected) {
_selectedIndex = i;
break;
}
}

[self reloadAllComponents];

if (_selectedIndex != NSNotFound)
[self selectRow:_selectedIndex inComponent:0 animated:NO];

return self;
}

- (void)dealloc
{
[self setDelegate:nil];
[self setDataSource:nil];

[super dealloc];
}

- (UIView *)controlView
{
return self;
}

- (void)controlBeginEditing
{
}

- (void)controlEndEditing
{
if (_selectedIndex == NSNotFound)
return;

if (_selectedIndex < (NSInteger)[_view focusedSelectElementOptions].size()) {
[_view focusedSelectElementOptions][_selectedIndex].isSelected = true;
[_view page]->setFocusedElementSelectedIndex(_selectedIndex);
}
}

- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView
{
return 1;
}

- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)columnIndex
{
return _view.focusedElementInformation.selectOptions.size();
}

- (NSAttributedString *)pickerView:(UIPickerView *)pickerView attributedTitleForRow:(NSInteger)row forComponent:(NSInteger)component
{
if (row < 0 || row >= (NSInteger)[_view focusedSelectElementOptions].size())
return nil;

const OptionItem& option = [_view focusedSelectElementOptions][row];
NSMutableString *trimmedText = [[option.text mutableCopy] autorelease];
CFStringTrimWhitespace((CFMutableStringRef)trimmedText);

NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:trimmedText];
if (option.disabled)
[attributedString addAttribute:NSForegroundColorAttributeName value:[UIColor colorWithWhite:0.0 alpha:DisabledOptionAlpha] range:NSMakeRange(0, [trimmedText length])];

return [attributedString autorelease];
}

- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component
{
if (row < 0 || row >= (NSInteger)[_view focusedSelectElementOptions].size())
return;

const OptionItem& newSelectedOption = [_view focusedSelectElementOptions][row];
if (newSelectedOption.disabled) {
NSInteger rowToSelect = NSNotFound;

// Search backwards for the previous enabled option.
for (NSInteger i = row - 1; i >= 0; --i) {
const OptionItem& earlierOption = [_view focusedSelectElementOptions][i];
if (!earlierOption.disabled) {
rowToSelect = i;
break;
}
}

// If nothing previous, search forwards for the next enabled option.
if (rowToSelect == NSNotFound) {
for (size_t i = row + 1; i < [_view focusedSelectElementOptions].size(); ++i) {
const OptionItem& laterOption = [_view focusedSelectElementOptions][i];
if (!laterOption.disabled) {
rowToSelect = i;
break;
}
}
}

if (rowToSelect == NSNotFound)
return;

[self selectRow:rowToSelect inComponent:0 animated:YES];
row = rowToSelect;
}

_selectedIndex = row;
}

// WKSelectTesting
- (void)selectRow:(NSInteger)rowIndex inComponent:(NSInteger)componentIndex extendingSelection:(BOOL)extendingSelection
{
// FIXME: handle extendingSelection.
[self selectRow:rowIndex inComponent:0 animated:NO];
// Progammatic selection changes don't call the delegate, so do that manually.
[self.delegate pickerView:self didSelectRow:rowIndex inComponent:0];
}

@end

#endif // PLATFORM(IOS_FAMILY)